인공지능(AI)이 세상을 지배하고 있습니다. 저항은 무의미합니다. 이제 우리는 불가피한 것을 맞서 싸울 것인지, 아니면 그에 굴복할 것인지 선택해야 합니다. 이 가이드에서는 AI를 Agora와 결합하여 방금 진행한 통화 내용을 요약하는 AI 기반 비디오 통화 앱을 만드는 방법을 단계별로 설명합니다.

필수 조건
- Flutter
- 아고라 개발자 계정 (Agora)
- Agora Speech-to-Text 서버 (이 예제 서버를 사용할 수 있습니다: this example server)
- Gemini API 키
프로젝트 설정
시작점은 Agora를 사용하여 구축된 간단한 비디오 통화 앱입니다. 이 가이드는 Agora를 사용하여 간단한 비디오 통화가 어떻게 작동하는지에 대한 기본적인 이해를 전제로 합니다.
Agora 기본 개념을 이해하지 못하신 경우, 문서 내의 Flutter 빠른 시작 가이드를 참고하시거나, Video Call with Agora Flutter 과정을 통해 더 깊이 학습하실 수 있습니다.
이 가이드는 간단한 시작용 비디오 통화 앱을 기반으로 구축됩니다. 이 앱은 여기에서 확인할 수 있습니다.
스타터 코드에는 단 하나의 버튼이 있는 랜딩 화면이 있습니다. 이 버튼은 사용자를 통화 참여로 초대합니다. 이 통화는 test
라는 단일 채널에서 발생합니다.(데모용입니다, 괜찮죠?) 통화 화면에는 원격 사용자의 비디오, 로컬 비디오, 통화 종료 버튼이 있습니다. 이벤트 핸들러를 사용하여 사용자를 뷰에 추가하거나 제거합니다.
음성 텍스트 변환
Agora에는 특정 채널의 통화를 실시간으로 텍스트로 변환하는 Real Time Transcription 제품이 있습니다.
Real-Time Transcription은 AI 마이크로서비스를 통해 통화 연결 및 음성 텍스트 변환을 수행하는 RESTful API입니다. 이 텍스트 변환 결과는 onStreamMessage
이벤트를 통해 비디오 통화 화면에 직접 스트리밍됩니다. 선택적으로 클라우드 제공업체에 저장할 수도 있으며, 이 가이드에서도 해당 방법을 구현할 것입니다.
백엔드

실시간 텍스트 변환은 비즈니스 서버에 구현해야 하는 몇 가지 이유가 있습니다. 백엔드가 마이크로서비스를 관리함으로써 각 채널 내에서 실시간 텍스트 변환이 단일 인스턴스만 실행되도록 보장할 수 있습니다. 또한 텍스트 변환 서비스에 토큰을 전달해야 하므로, 백엔드에서 이를 처리하면 클라이언트 측에 토큰이 노출되지 않습니다.
우리는 이 서버를 백엔드로 사용할 것입니다. 이 서버는 두 개의 엔드포인트를 노출합니다: 하나는 트랜스크립션을 시작하기 위한 것이고, 다른 하나는 트랜스크립션을 종료하기 위한 것입니다.
실시간 트랜스크립션 시작
/start-transcribing/<--채널 이름-->
성공적인 응답에는 Task ID와 Builder Token이 포함되며, 이 값은 앱에 저장해야 합니다. 이 값은 트랜스크립션을 중지할 때 필요합니다.
{taskId: <--Task ID Value-->, builderToken: <--Builder Token Value-->}
실시간 트랜스크립션 중지
/stop-transcribing/<--채널 이름-->/<--Task ID-->/<--Builder Token-->
통화 내 트랜스크립션 시작
Flutter 애플리케이션에서 네트워크 호출을 수행하려면 http
패키지를 사용할 수 있습니다. 앱과 백엔드 서버에서 동일한 App ID를 사용해야 합니다. 그런 다음 API를 호출하여 트랜스크립션을 시작합니다.
call.dart
파일 내에 다음 startTranscription
함수를 추가할 수 있습니다:
Future<void> startTranscription({required String channelName}) async {
final response = await post(
Uri.parse('$serverUrl/start-transcribing/$channelName'),
);
if (response.statusCode == 200) {
print('Transcription Started');
taskId = jsonDecode(response.body)['taskId'];
builderToken = jsonDecode(response.body)['builderToken'];
} else {
print('Couldn\'t start the transcription : ${response.statusCode}');
}
}
이 함수는 join 호출 메서드 직후에 호출되도록 설정되어 첫 번째 사용자가 채널에 가입하자마자 실행됩니다. 성공적인 응답의 일환으로 Task ID와 Builder Token을 받게 됩니다. 이 값들은 트랜스크립션을 중지하기 위해 필요하므로 반드시 저장해 두세요.
전사 작업이 성공적으로 시작되면 이는 “봇”이 콜에 참여한 것으로 간주됩니다. 이는 실제 사용자가 아니지만, 백엔드 서버 내에서 정의된 고유한 UID를 가지고 있습니다. 위에서 링크한 서버를 사용 중이라면 UID는 101
입니다. 이 UID는 onUserJoined
이벤트에서 원격 사용자 목록에서 제외할 수 있습니다.
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
if (remoteUid == 101) return;
setState(() {
_remoteUsers.add(remoteUid);
});
}
전사 종료
전사를 종료하려면 시작 기능과 유사한 기능을 사용합니다. 이 기능은 stopTranscription
로 명명되며, 실시간 전사 서비스를 중지하기 위해 작업 ID와 빌더 토큰을 전달해야 합니다.
Future<void> stopTranscription() async {
final response = await post(
Uri.parse('$serverUrl/stop-transcribing/$taskId/$builderToken'),
);
if (response.statusCode == 200) {
print('Transcription Stopped');
} else {
print('Couldn\'t stop the transcription : ${response.statusCode}');
}
}
우리는 콜 화면의 dispose
메서드에서 stopTranscription
메서드를 호출할 것입니다. 이로써 채널을 떠나기 전에 트랜스크립션을 중지하고 엔진 리소스를 해제합니다.
트랜스크립션 가져오기
비디오 통화 중에는 이벤트 핸들러에서 onStreamMessage
이벤트를 사용하여 트랜스크립션에 액세스할 수 있습니다.
onStreamMessage: (RtcConnection connection, int uid, int streamId,
Uint8List message, int messageType, int messageSize) {
print(message);
}
위 코드는 숫자 배열을 출력합니다. 이 숫자들은 당신이 모든 것을 아는 AI가 아니라면 의미가 없습니다. 이 숫자들은 Google의 Protocol Buffers(프로토버퍼라고도 함)를 사용하여 생성되었습니다.
Protobufs는 플랫폼에 독립적인 방식으로 데이터를 인코딩합니다. 이는 앱이나 소프트웨어가 이 데이터를 가져와서 자신의 언어에 맞게 시리얼화할 수 있음을 의미합니다.
전사문 해독
우리는 Protocol Buffer를 사용하여 메시지를 해독할 것입니다. 이 경우, 무작위로 보이는 숫자들을 Message
라는 객체로 시리얼화할 것입니다.
.proto
파일을 다음과 같은 내용으로 생성합니다:
syntax = "proto3";
package call_summary;
message Message {
int32 vendor = 1;
int32 version = 2;
int32 seqnum = 3;
int32 uid = 4;
int32 flag = 5;
int64 time = 6;
int32 lang = 7;
int32 starttime = 8;
int32 offtime = 9;
repeated Word words = 10;
}
message Word {
string text = 1;
int32 start_ms = 2;
int32 duration_ms = 3;
bool is_final = 4;
double confidence = 5;
}
lib/protobuf/file.proto.
이 파일은 생성기가 Message
객체를 생성하기 위한 입력 파일입니다.
protobuf를 사용하려면 컴퓨터에 protobuf 컴파일러를 설치해야 합니다. Mac( brew install protobuf
) 및 Linux( apt install -y protobuf-compiler
)용 패키지 관리자를 통해 설치 가능합니다. Windows 또는 특정 버전이 필요한 경우 Prottobuf 다운로드 페이지를 확인하세요.
프로젝트 내부에 protobuf
dart 패키지를 설치해야 합니다. 이를 위해 flutter pub add protobuf
를 실행하세요.
이제 터미널에서 다음 명령어를 실행하세요. 동일한 lib/protobuf
폴더에 4개의 파일이 생성됩니다.
protoc --proto_path= --dart_out=. lib/protobuf/file.proto
프로토버프가 설정되었으므로, 새로운 Message
객체를 사용하여 영어로 변환된 텍스트를 가져올 수 있습니다. 이 객체에는 변환된 문장들이 포함된 words
배열이 있습니다. isFinal
변수를 사용하여 문장이 완료될 때마다 출력 문장을 실행합니다.
onStreamMessage: (RtcConnection connection, int uid, int streamId,
Uint8List message, int messageType, int messageSize) {
Message text = Message.fromBuffer(message);
if (text.words[0].isFinal) {
print(text.words[0].text);
}
},
받아쓰기 내용 저장
받아쓰기 부분은 완료되었습니다. 이제 전사된 텍스트를 가져와 저장한 후, 이를 AI에 입력하여 요약문을 생성하도록 할 필요가 있습니다. 실시간 전사 서비스는 전사된 오디오를 조각으로 나누어 전송합니다. 오디오 조각이 처리될 때마다 시리얼화된 데이터가 버스트로 전송되며, 각 버스트는 onStreamMessage
이벤트를 트리거합니다. 이를 저장하는 가장 간단한 방법은 응답을 긴 문자열로 연결하는 것입니다. 더 복잡한 방법도 있지만, 이 데모에는 이 정도면 충분합니다.
transcription
이라는 문자열을 저장하고 텍스트가 최종 확정될 때마다 추가할 수 있습니다.
onStreamMessage: (RtcConnection connection, int uid, int streamId,
Uint8List message, int messageType, int messageSize) {
Message text = Message.fromBuffer(message);
if (text.words[0].isFinal) {
print(text.words[0].text);
transcription += text.words[0].text;
}
},
요약 받기
main.dart
에서 API 키를 사용하여 Gemini에 연결하고 비디오 통화를 요약하도록 요청할 수 있습니다. 이 응답을 받으면 setState
를 호출하고 summary
변수를 업데이트하여 변경 사항이 메인 페이지에 반영되는 것을 확인할 수 있습니다.
이 앱을 테스트하는 동안 응답이 전달한 트랜스크립트를 언급하는 경향이 있음을 발견했습니다. 이 때문에 트랜스크립트를 언급하지 않도록 추가 프롬프트를 추가했습니다.
late final GenerativeModel model;
@override
void initState() {
super.initState();
model = GenerativeModel(model: 'gemini-pro', apiKey: apiKey);
}
void retrieveSummary(String transcription) async {
final content = [
Content.text(
'This is a transcript of a video call that occurred. Please summarize this call in a few sentences. Dont talk about the transcript just give the summary. This is the transcript: $transcription',
),
];
final response = await model.generateContent(content);
setState(() {
summary = response.text ?? '';
});
}
트랜스크립트 문자열을 retrieveSummary
에 전달해야 할 때, 우리는 해당 함수를 call.dart
에 전달하고 호출이 완료되면 이를 호출합니다.
완료

이로써, 채널에 누군가가 가입하자마자 실시간 트랜스크립션 서비스를 트리거하는 애플리케이션을 구축했습니다. 이 트랜스크립트는 클라이언트 측에 저장되어 Gemini에게 요약문을 요청하고 사용자와 공유할 수 있도록 합니다.
축하합니다. 이제 AI 지배자들에게 굴복하는 길에 들어섰습니다.
완전한 코드는 여기에서 확인할 수 있습니다. 이 가이드를 기반으로 더 깊이 탐구하려면 실시간 받아쓰기 문서를 참고하세요.
읽어주셔서 감사합니다!