Deepgram Speech-to-Text API 로 실시간 자막 생성하기
졸업프로젝트 주제
AI 기반 학습자 맞춤형 실시간 자막/대체텍스트 및 대체학습자료 생성 프로그램 COMMA
COMMA의 주기능 3가지 중 하나가 "강의자료의 키워드 정확도를 높인 실시간 자막 생성" 이다.
"Deepgram Speech-to-Text API" 를 Flutter에 적용하여 안드로이드 앱에서 실시간으로 자막이 생성되도록 하는 방법을 소개하고자 한다.
설명 순서
1. Deepgram Speech-to-Text API 키 발급받기!
2. Flutter에 Deepgram STT API 간단히 적용해보기!
3. 졸업프로젝트에 Deepgram STT API 사용하기!
4. 결과!
1. Deepgram Speech-to-Text API 키 발급받기
1. Free 요금제 선택 및 회원가입
"Free"를 선택하고 새로 회원가입하면 200 크레딧을 무료로 받을 수 있다. 본 프로젝트에서도 이 요금제를 사용했는데 여전히 195 크레딧이나 남아있다..
2. Dashboard에서 크레딧 확인
Dashboard에서 남은 크레딧을 확인할 수 있다. Speech-to-text API의 경우 분당 0.0043달러(6원) 정도이기 때문에 200 크레딧이면 2500시간 이상을 사용할 수 있는 듯!
3. Create API Key
필요한 정보를 입력하고 키를 생성한다. 생성이 되면 "API Keys" 창에 생성된 키가 보이고, 관리할 수 있다.
2. Flutter에 Deepgram Speech-to-Text API 간단히 적용해보기
<미리 준비해야할 것>
* Flutter 설치 => https://docs.flutter.dev/get-started/install
* Flutter 프로젝트 생성 => 참고: https://docs.flutter.dev/get-started/editor
1. 녹음 관련 권한 설정
android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
=> INTERNET, RECORD_AUDIO 권한 추가
android/app/src/build.gradle
// minSdkVersion flutter.minSdkVersion
// targetSdkVersion flutter.targetSdkVersion
// 아래 코드로 변경
minSdkVersion 24
targetSdkVersion 32
=> sdk version 변경
2. Dependencies 설치
pubspec.yaml
web_socket_channel: ^3.0.1
sound_stream: ^0.4.1
permission_handler: ^11.3.1
// 추가 후 flutter pub get
3. 간단한 UI 구성
Deepgram STT API를 적용하기 위해 Flutter로 간단한 테스트 화면을 구현해보았다
lib/main.dart
전체 코드 ▽
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
backgroundColor: const Color(0xFF2571B0),
title: const Text(
'Live Transcription with Deepgram',
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Row(
children: <Widget>[
Expanded(
flex: 3,
child: SizedBox(
width: 150,
child: Text(
"자막이 여기에 표시됩니다",
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 50,
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
),
),
),
],
),
const SizedBox(height: 20),
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
OutlinedButton(
style: ButtonStyle(
backgroundColor:
WidgetStateProperty.all<Color>(Colors.blue),
foregroundColor:
WidgetStateProperty.all<Color>(Colors.white),
),
onPressed: () {},
child: const Text('Start', style: TextStyle(fontSize: 30)),
),
const SizedBox(width: 5),
OutlinedButton(
style: ButtonStyle(
backgroundColor:
WidgetStateProperty.all<Color>(Colors.red),
foregroundColor:
WidgetStateProperty.all<Color>(Colors.white),
),
onPressed: () {},
child: const Text('Stop', style: TextStyle(fontSize: 30)),
),
],
),
),
],
),
),
);
}
녹음을 시작할 Start 버튼, 종료할 Stop 버튼, 화면 가운데에 자막이 출력되도록 구현하였다
4. Text State 관련 코드 추가
ⓐ myText 추가
class _MyHomePageState extends State<MyHomePage> 아래에 변수 myText 추가
myText에 자막이 담기고 화면에 myText를 출력하는 것!
String myText = "실시간 자막을 시작하려면 Start 버튼을 누르세요!";
ⓑ "자막이 여기에 표시됩니다" 부분에 myText 변수 넣기
child: Text(
// "자막이 여기에 표시됩니다",
myText,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 50,
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
),
이때 코드 곳곳에서 오류가 날 수 있는데 "const" 를 제거하면 된다! myText는 실시간으로 계속 변하는 값이기 때문에 const 가 들어가면 안됨
ⓒ updateText 함수 / resetText 함수
void updateText(newText) {
setState(() {
myText = myText + ' ' + newText;
});
}
void resetText() {
setState(() {
myText = '';
});
}
myText 변수 밑에 추가한다.
updateText는 자막을 업데이트하고, resetText는 변수 값을 재설정하여 화면에서 자막을 지운다.
ⓓ Start 버튼을 누르면 updateText 함수 실행되도록
OutlinedButton(
style: ButtonStyle(
backgroundColor:
WidgetStateProperty.all<Color>(Colors.blue),
foregroundColor:
WidgetStateProperty.all<Color>(Colors.white),
),
onPressed: () {
updateText('');
},
child: const Text('Start', style: TextStyle(fontSize: 30)),
),
onPressed: () {}에 updateText(' ') 를 추가한다
5. 오디오 입력 처리
ⓐ 라이브러리 추가
import 'dart:async';
import 'dart:convert';
import 'package:sound_stream/sound_stream.dart';
import 'package:web_socket_channel/io.dart';
import 'package:permission_handler/permission_handler.dart';
ⓑ 연결 URL 및 API Key 추가
const serverUrl =
'wss://api.deepgram.com/v1/listen?encoding=linear16&sample_rate=16000&language=ko';
const apiKey = '<your Deepgram API key>';
import 밑에 추가하면 된다.
serverUrl은 WebSocket이 연결할 URL을 정의한다. apiKey는 인증을 위한 Deepgram API 키로, 좀 전에 발급받은 key를 넣으면 된다.
ⓒ 마이크 접근 권한 요청
@override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback(onLayoutDone);
}
void onLayoutDone(Duration timeStamp) async {
await Permission.microphone.request();
setState(() {});
}
updateText, resetText 있는 위치에 마이크 접근 권한 요청 코드를 추가한다.
처음 앱이 로드되면 마이크 권한을 요청하는 이 작업이 수행된다.
ⓓ WebSocket과 sound_stream 객체
final RecorderStream _recorder = RecorderStream();
late StreamSubscription _recorderStatus;
late StreamSubscription _audioStream;
late IOWebSocketChannel channel;
WebSocket과 sound_stream을 위한 객체를 초기화한다. myText 변수 밑에 추가한다.
ⓔ WebSocket 초기화
초기화 코드 ▽
Future<void> _initStream() async {
channel = IOWebSocketChannel.connect(Uri.parse(serverUrl),
headers: {'Authorization': 'Token $apiKey'});
channel.stream.listen((event) async {
final parsedJson = jsonDecode(event);
updateText(parsedJson['channel']['alternatives'][0]['transcript']);
});
_audioStream = _recorder.audioStream.listen((data) {
channel.sink.add(data);
});
_recorderStatus = _recorder.status.listen((status) {
if (mounted) {
setState(() {});
}
});
await Future.wait([
_recorder.initialize(),
]);
}
ⓕ 자막 생성 시작/중지 함수 추가
void _startRecord() async {
resetText();
_initStream();
await _recorder.start();
setState(() {});
}
void _stopRecord() async {
await _recorder.stop();
setState(() {});
}
_startRecord 함수가 호출되면 sound_stream이 마이크를 키고, STT가 시작된다.
_stopRecord 함수가 호출되면 녹음이 중지된다.
ⓖ Start / Stop 버튼과 위의 함수 연결
// Start 버튼 쪽
onPressed: () {
updateText('');
_startRecord();
},
// Stop 버튼 쪽
onPressed: () {
_stopRecord();
},
Start 버튼과 Stop 버튼 쪽의 onPressed 에 위의 함수들을 추가하여, 버튼을 눌렀을 때 작동되도록 한다.
ⓗ WebSocket 채널 닫기
@override
void dispose() {
_recorderStatus.cancel();
_audioStream.cancel();
channel.sink.close();
super.dispose();
}
6. 실행
3. 졸업프로젝트에 Deepgram STT API 사용하기
위에서 적용한 Deepgram STT API를 졸업 프로젝트로 진행 중인 COMMA 앱에 적용하였다. 위에서 추가한 dependencies나 함수들은 똑같이 적용했고, 아래에서는 졸업프로젝트에 맞춤으로 변경한 부분만 소개할게요
1. 연결 URL 변경
const serverUrl =
'wss://api.deepgram.com/v1/listen?model=nova-2&encoding=linear16&sample_rate=16000&language=ko-KR&punctuate=true';
졸업프로젝트에서는 STT 모델을 nova-2 로 지정했고, punctuate=true 를 넣어 자막 문장에 자동으로 구두점을 출력해주는 기능을 활성화하였다.
2. 키워드 부스팅 기능
Deepgram STT API를 호출할 때의 URL 뒤에 keywords 매개변수를 하나 이상 추가하면 지정된 단어의 인식률이 향상된다.
ex) wss://api.deepgram.com/v1/listen?model=nova-2&keywords=사자&keywords=호랑이
졸업 프로젝트 COMMA의 주기능 중 하나가 청각 장애인 학습자를 위한 솔루션으로, "강의자료의 핵심 키워드 정확도를 높인 실시간 자막 생성" 이기 때문에, 이 키워드 부스팅 기능을 사용하였다.
String buildServerUrlWithKeywords(String baseUrl, List<String> keywordList) {
final List<String> keywords = keywordList
.expand((keyword) => keyword.split(','))
.map((keyword) => keyword.trim())
.toList();
final keywordQuery =
keywords.map((keyword) => 'keywords=$keyword').join('&');
return '$baseUrl&$keywordQuery';
}
강의자료에서 추출한 핵심 키워드 리스트를 받아서 API URL 뒤에 "keywords=$keyword" 형식으로 이어 붙였다.
4. 결과
끝!
참고자료
https://deepgram.com/learn/flutter-speech-to-text-tutorial