블로그 목록
AI12분 읽기

Agora Real-Time STT — 전사·번역 메시지 처리

Agora Real-Time STT 작업이 RTC channel의 audio를 전사하고 Beta 번역 결과를 DataStream으로 전달하는 흐름을 설명합니다. 공식 `SttMessage.proto` schema로 payload를 역직렬화하고 `(uid, sentence_id)` 단위로 중간 결과를 갱신합니다. UI debounce는 protocol 규칙이 아닌 application 정책으로 분리합니다.

IntegrationSTTTranslationDataStreamRTC

Agora Real-Time STT는 RTC 채널의 음성을 실시간으로 전사하고, Beta 기능인 번역 결과를 자막 데이터로 채널에 보낼 수 있습니다. Conversational AI의 음성 에이전트 API와는 별도 제품·엔드포인트입니다. 이 글에서는 STT 작업 시작, 번역 설정, Protobuf 메시지 파싱과 자막 카드 상태 관리를 다룹니다.

전체 데이터 흐름

[사용자 마이크] → Agora RTC 채널
                       ↓
            Real-Time STT 작업이 채널 참여
                       ↓
                 ASR → Translation
                       ↓
            RTC DataStream으로 Protobuf 결과 전송
                       ↓
            클라이언트: Protobuf 역직렬화 → UI 렌더링

클라이언트가 직접 음성 인식을 하는 구조가 아닙니다. Real-Time STT 작업이 지정한 RTC 사용자 음성을 구독하고, 전사·번역 결과를 DataStream으로 채널에 게시합니다.


핵심 구성 요소

구성 요소역할
/api/tokenRTC 토큰 생성 (agora-token 패키지)
/api/sttReal-Time STT REST API 프록시(REST 자격 증명 보호)
page.tsx클라이언트 — RTC 연결, 미디어, STT, UI

UID 구성

UID용도
Random 1000~100999로컬 사용자
88888STT subtitle-pushing bot(pubBotUid)
55555화면 공유 (별도 RTC client)

STT Agent 시작/중지

STT 시작 — 작업 생성

const requestBody = {
  name: `stt-agent-${Date.now()}`,
  languages: ["ko-KR"],
  maxIdleTime: 300,
  rtcConfig: {
    channelName,
    pubBotUid: "88888",
    pubBotToken, // App Certificate 사용 시 필요
    subscribeAudioUids: [String(localUid)],
  },
  translateConfig: {
    languages: [
      { source: "ko-KR", target: ["en-US"] },
    ],
  },
};

// 서버 프록시가 POST /api/speech-to-text/v1/projects/{appid}/join 호출
const res = await fetch("/api/stt", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ action: "join", requestBody }),
});
const { agent_id: agentId } = await res.json();

Real-Time STT v1의 시작 엔드포인트는 /api/speech-to-text/v1/projects/{appid}/join입니다. 서버 프록시는 Customer ID/Secret 기반 Basic 인증을 보관하고, 브라우저에는 노출하지 않습니다. pubBotUid는 채널 안에서 고유해야 하며, App Certificate를 사용하면 해당 UID의 RTC 토큰을 pubBotToken으로 전달합니다.

STT 중지 — Agent Leave

await fetch("/api/stt", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ action: "leave", agentId }),
});

DataStream 메시지 파싱

Real-Time STT의 기본 자막 포맷은 Agora가 제공하는 SttMessage.proto의 Agora.SpeechToText.Text 메시지입니다. msgId|partIdx|partSum|base64 형식은 현재 공식 STT 프로토콜이 아니므로 직접 청크를 조립하거나 atob()로 JSON을 만들지 않습니다. Web에서는 공식 .proto로 JavaScript 코드를 생성한 뒤 stream-message payload를 역직렬화합니다.

import protoRoot from "@/protobuf/SttMessage_es6.js";

rtcClient.on("stream-message", (uid, payload) => {
  if (String(uid) !== STT_PUB_BOT_UID) return;

  const Text = protoRoot.Agora.SpeechToText.lookup("Text");
  const message = Text.decode(payload);
  handleSttMessage(message);
});

rtcConfig.enableJsonProtocol: true를 선택하면 JSON을 gzip 압축한 형식으로 받을 수도 있습니다. 이 글은 기본값인 Protobuf 경로를 사용합니다.


메시지 타입

data_type: "transcribe" — 음성 인식 결과

{
  "uid": "1234",
  "data_type": "transcribe",
  "words": [{ "text": "안녕하세요", "is_final": false }],
  "culture": "ko-KR",
  "text_ts": "1753359520754",
  "sentence_id": "1753359518654"
}
  • words[].is_final이 false이면 해당 결과는 이후 갱신될 수 있습니다.
  • sentence_id와 text_ts를 사용해 같은 자막 문장의 갱신을 연결합니다.

data_type: "translate" — 번역 결과

{
  "data_type": "translate",
  "trans": [
    { "is_final": true, "lang": "en-US", "texts": ["Hello"] }
  ],
  "original_transcript": {
    "culture": "ko-KR",
    "words": [{ "text": "안녕하세요", "is_final": true }]
  },
  "sentence_id": "1753359518654"
}
  • original_transcript: 번역의 원문 전사
  • trans[].lang, trans[].texts: 대상 언어와 번역 결과

자막 카드와 세그먼트 상태

중간 전사 결과는 같은 문장에 대해 여러 번 도착할 수 있습니다. UI 상태는 사용자 UID와 sentence_id를 함께 키로 사용해야 기존 문장을 덮어쓰지 않으면서 중간 결과를 갱신할 수 있습니다.

구조

진행 중 (activeCards)                 완료 (completedCards)
┌──────────────────────────┐         ┌──────────────────────┐
│  UID "1234":             │         │ (완료된 발화들)       │
│    ├ 안녕하세요   → Hello │         │                      │
│    ├ 반갑습니다   → Nice  │         │                      │
└──────────────────────────┘         └──────────────────────┘
개념역할
카드한 사용자의 연속 발화를 묶음
세그먼트(uid, sentence_id)별 전사·번역 텍스트 저장
타이머애플리케이션이 정한 무음 시간 뒤 진행 중 카드를 완료 처리
clearTimeout새 결과가 오면 기존 완료 타이머를 취소

sentence_id로 중간 결과 갱신하기

같은 sentence_id로 들어오는 중간 결과는 기존 세그먼트를 갱신하고, 새 sentence_id는 새 세그먼트로 추가합니다. 서로 다른 사용자가 같은 값을 가질 가능성에 대비해 UID도 키에 포함합니다.

const segmentKey = `${message.uid}:${message.sentence_id}`;

segments.set(segmentKey, {
  ...segments.get(segmentKey),
  transcript: message.words.map((word) => word.text).join(""),
  isFinal: message.words.every((word) => word.is_final),
  textTs: message.text_ts,
});

번역 결과는 trans[].lang별로 저장하고, original_transcript와 sentence_id를 이용해 원문 세그먼트에 연결합니다. words[].is_final 또는 trans[].is_final이 false인 동안에는 같은 키의 값이 다시 올 수 있으므로 추가가 아니라 갱신으로 처리합니다.


연속 발화 묶기 — 애플리케이션 디바운스

아래 1초 값은 Agora 프로토콜이 보장하는 발화 경계가 아니라 이 데모의 UI 정책입니다. 사용 환경의 발화 속도와 자막 지연을 측정해 조정해야 합니다.

최종 번역 도착 → 1초 타이머 시작
                  ↓
같은 사용자의 새 결과 도착 → clearTimeout(타이머)
                  ↓
                 기존 카드 갱신

실제 시나리오: "안녕하세요" → (0.3초 쉼) → "반갑습니다"

단계이벤트카드 상태타이머
1transcribe (sentence_id=A, "안녕하세요")새 카드와 세그먼트 생성없음
2translate (sentence_id=A, "Hello", final)번역 추가1초 타이머 시작
30.3초 후 transcribe (sentence_id=B, "반갑습니다")clearTimeout 후 같은 카드에 세그먼트 추가취소
4translate (sentence_id=B, "Nice to meet you")번역 추가1초 타이머 재시작
51초 동안 새 결과 없음카드를 완료 목록으로 이동실행

화면 공유 구조

Agora SDK는 하나의 RTC client에서 2개의 video track을 publish할 수 없습니다. 화면 공유를 위해 별도 RTC client(UID 55555)를 생성합니다.

Main Client (UID: random)     Screen Client (UID: 55555)
├── Audio Track (mic)          ├── Video Track (screen)
├── Video Track (camera)       └── (audio disabled)
└── STT stream-message listener
// 별도 client 생성 및 채널 참여
const screenClient = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
await screenClient.join(
  APP_ID,
  channelName,
  screenToken,
  SCREEN_SHARE_UID_SUFFIX,
);

// 화면 캡처 트랙 생성 및 publish
const screenTrack = await AgoraRTC.createScreenVideoTrack(
  { optimizationMode: "detail" },
  "disable", // 오디오 비활성화
);
await screenClient.publish([screenTrack]);

브라우저의 "공유 중지" 버튼 클릭 시 track-ended 이벤트로 자동 정리됩니다.


지원 언어와 제한

languages에는 전사할 언어를, translateConfig.languages에는 원문과 대상 언어를 지정합니다. 현재 v1 API는 전사 언어를 최대 4개까지 받고, subscribeAudioUids는 최대 32개 UID를 받을 수 있습니다. 실시간 번역은 Beta 기능이며 지원 언어와 제한은 바뀔 수 있으므로 고정 목록을 코드나 문서에 복제하지 말고 공식 지원 언어 표를 배포 시점에 확인합니다.

번역 설정은 소스 언어를 최대 4개, 소스별 대상 언어를 최대 10개까지 받을 수 있습니다. 번역이 동시에 처리하는 화자는 최대 5명입니다. 대규모 회의에서는 전체 RTC 참가자 수와 별개로 이 제한을 반영해 구독 대상을 선택해야 합니다.


핵심 요약

  • Real-Time STT 작업이 채널의 음성을 구독하고 전사·번역 결과를 DataStream으로 전송
  • 기본 결과는 SttMessage.proto 기반 Protobuf이므로 공식 스키마로 역직렬화
  • (uid, sentence_id)를 키로 중간 결과를 갱신하고 새 문장을 별도 세그먼트로 보존
  • 1초 디바운스는 프로토콜 규칙이 아니라 데모의 UI 그룹화 정책
  • 화면 공유는 별도 RTC client로 구현 (SDK 제약 때문)

관련 글

참고 자료

실제 데모

/agora-demo/stt

© 2026 Frank Kim. All rights reserved.