콘텐츠로 건너뛰기

200ms 이하 실시간 음성 인식: 아키텍처 가이드

게시일
최종 업데이트

듣기이 글 오디오로 듣기

실시간 음성 텍스트 변환(STT)은 사람이 말하는 동안 오디오를 능동적으로 전사해 수백 밀리초 안에 텍스트로 반환합니다. 하지만 STT 지연 시간을 낮게 유지하는 일은 모델 문제인 만큼 아키텍처 문제이기도 합니다. 엔지니어는 전송, 청크 분할, 발화 종료 감지, 캡처 경로를 모두 설계해야 하며, 각각이 지연 시간에 영향을 줍니다. 이 중 하나라도 비효율적이면 200ms 예산을 초과할 수 있습니다.

이 가이드에서는 전송 계층부터 실시간 음성 텍스트 변환 파이프라인을 구축하는 데 활용할 수 있는 실용적인 시스템을 소개합니다. 기준으로 삼을 제품은 Scribe v2 Realtime이며, 약 150ms의 모델 지연 시간으로 부분 전사를 제공하고, 90개 이상의 언어를 지원하며, PCM(8kHz~48kHz) 및 mu-law 오디오를 수신하고, 세그먼트 완료를 위한 음성 활동 감지와 수동 커밋 제어를 제공합니다.

오디오가 서버에 도달하는 과정, 가설이 확정 텍스트로 발전하는 방식, 스트림 내 기능의 비용, 그리고 오디오를 올바르게 캡처하고 전달하는 방법을 살펴보겠습니다.

요약

  • 실시간 음성 텍스트 변환 시스템을 구축하려면 전체 파이프라인에서 낮은 지연 시간을 유지하도록 아키텍처를 세밀하게 조정해야 합니다.
  • WebRTC는 다양한 이점을 제공하지만 더 복잡하므로, 대부분의 파이프라인에서는 WebSocket이 적절한 기본 선택입니다.
  • 음성 활동 감지는 핸즈프리 세그먼트 분할을 처리하며, 수동 커밋을 사용하면 애플리케이션이 발화 차례가 끝났음을 알 때 이를 재정의할 수 있습니다.
  • 부분 결과는 잠정적이고 최종 결과는 확정되므로, 서로 다르게 표시해야 합니다. 
  • 약 100ms의 작은 PCM 청크를 사용하면 첫 부분 결과까지의 지연 시간을 최소화할 수 있습니다.

실시간 음성 텍스트 변환을 위한 WebSocket과 WebRTC 비교

전사가 시작되기 전에 오디오는 소스에서 음성 인식기로 이동해야 합니다. 선택하는 채널이 이후 모든 단계의 지연 시간 하한을 결정합니다. 오디오를 전사 계층으로 전달하는 데는 두 가지 실용적인 방법이 있습니다.

WebSocket은 TCP 기반의 지속형, 순서 보장, 신뢰성, 양방향 채널입니다. 연결을 열고 바이너리 오디오 프레임을 전송한 뒤 전사 이벤트를 수신합니다. 클라이언트와 서버 모두에서 간단하며, 이미 HTTPS를 허용하는 기업 프록시와 방화벽을 통과할 수 있고, 모든 브라우저 및 서버 런타임에서 지원합니다. 

WebSocket의 제약은 TCP 위에서 동작한다는 점입니다. 패킷이 손실되면 TCP는 재전송하고, 누락된 부분이 채워질 때까지 이후 데이터를 대기시킵니다. 네트워크 상태가 좋다면 눈에 띄지 않습니다. 하지만 패킷 손실이 발생하면 헤드 오브 라인 블로킹이 생겨 오디오가 잠시 밀렸다가 한꺼번에 도착할 수 있습니다.

WebRTC는 실시간 미디어를 위해 설계되었습니다. UDP(SRTP 경유)로 미디어를 전송하므로 패킷이 손실되어도 스트림이 멈추지 않고 파이프라인이 계속 진행됩니다. 패킷 도착 시간의 변동을 흡수하는 지터 버퍼를 포함하며, ICE/STUN/TURN으로 NAT 통과를 협상해 라우터 뒤의 피어도 연결할 수 있고, 자체 오디오 캡처 및 인코딩 기능도 갖추고 있습니다. 

직접 연결할 수 없는 클라이언트에는 일반적으로 TURN 서버가 필요하며, 서버 측에서는 바이트 스트림을 읽는 대신 미디어 스트림을 종료해야 합니다.

핵심적인 절충점은 다음과 같습니다.

WebSocket
Transport
TCP (reliable, ordered)
Behavior under packet loss
Head-of-line blocking, bursty recovery
Jitter handling
Your responsibility
NAT traversal
Not needed (client-initiated)
Browser support
Universal, trivial
Server complexity
Low
WebRTC
Transport
UDP/SRTP (real-time, loss-tolerant)
Behavior under packet loss
Graceful degradation
Jitter handling
Built-in jitter buffer
NAT traversal
Requires ICE/STUN/TURN
Browser support
Universal, but more API surface
Server complexity
High (media server or SFU)

대부분의 사용 사례에서는 WebSocket이 적합합니다. 클라이언트의 연결 상태가 양호하고 캡처 경로를 제어할 수 있을 때 사용하세요. 예를 들어 서버 간 파이프라인, 데스크톱 앱, 광대역 환경의 브라우저 앱, 그리고 오디오가 다른 방식으로 이미 서버에 도착하는 대부분의 컨택센터 백엔드가 이에 해당합니다. 

불안정한 모바일 네트워크에서 소비자 기기로부터 직접 캡처하는 경우, 양방향 오디오용 WebRTC 스택을 이미 운영하는 경우(예: 음성으로 응답하는 음성 에이전트), 또는 구현 단순성보다 손실에 강한 실시간 동작이 더 중요한 경우에는 WebRTC를 선택하세요.

이 가이드의 나머지 부분에서는 음성 인식기 연결에 WebSocket 전송을 사용합니다. 구성 요소를 명확하게 볼 수 있고 대부분의 팀에 적합한 시작점이기 때문입니다. 여기서 설명하는 내용은 WebSocket에만 국한되지 않으므로, 나중에 앞단에 WebRTC 미디어 구간을 추가하고 서버에서 오디오를 PCM으로 디코딩한 뒤 동일한 청크를 파이프라인으로 전달할 수 있습니다.

부분 결과와 최종 전사본: 중간 결과 이해하기

실시간 음성 인식기는 완전한 문장이 끝날 때까지 기다리지 않습니다. 대신 오디오가 더 들어올수록 정교해지는 추정 결과를 연속적으로 내보낸 다음 확정합니다. 이 두 상태의 차이를 이해하면 생동감 있는 전사본과 어색한 전사본을 구분할 수 있습니다. 

부분(중간) 가설은 지금까지 수신한 오디오를 바탕으로 한 모델의 최선의 추정입니다. 부분 결과는 설계상 불안정합니다. 오디오가 더 들어오면 모델은 앞선 단어를 수정합니다. 예를 들어 "I want to"는 이후 문맥에서 모호성이 해소되면 "I want two tickets"가 될 수 있습니다. 부분 결과는 빠르게 도착하며(~150ms 지연 시간 수치가 이를 의미함), 덮어쓰도록 설계되어 있습니다.

최종 가설은 이후 변경되지 않는 확정 세그먼트입니다. 세그먼트가 완료되면 음성 인식기는 다음으로 넘어가고, 이후 가설은 뒤따르는 오디오를 설명합니다. 최종 결과는 저장하거나 LLM으로 보내거나 전사본으로 보관하는 데이터입니다.

부분 결과와 최종 결과의 차이는 이를 혼동하면 잘못 처리하게 될 다음 세 가지를 좌우합니다.

  • 사용자 경험: 부분 결과를 표시하면 전사본이 실시간으로 느껴집니다. 사용자는 말하는 동안 단어가 나타나는 것을 보고 마이크가 작동하며 시스템이 듣고 있음을 확인합니다.
  • 발화 종료 감지: 부분 결과는 지속적인 음성 활동 신호를 제공합니다. VAD와 결합하면 화자가 실제로 멈춘 시점을 판단할 수 있습니다.
  • 후속 단계 타이밍: 음성 에이전트 파이프라인에서는 오디오 입력 후 음성 텍스트 변환, LLM, 텍스트 음성 변환, 그리고 오디오 출력 순으로 진행됩니다. 부분 결과를 바탕으로 추정 작업을 먼저 시작하고 최종 결과에서 확인하면, 가끔 추정 작업을 폐기해야 하는 대신 체감 응답 시간을 줄일 수 있습니다.

부분 결과와 최종 결과는 다르게 표시하세요. 간단하면서도 효과적인 패턴은 최신 부분 결과에 연결된 변경 가능한 단일 "현재 줄"을 유지하고, 최종 결과가 도착할 때 추가 전용 전사본에 확정하는 방식입니다.

type TranscriptState = {
  committed: string[]; // finalized segments, never rewritten
  current: string;     // latest partial, overwritten on each update
};

const onPartial = (s: TranscriptState, text: string): TranscriptState =>
  ({ ...s, current: text });

const onFinal = (s: TranscriptState, text: string): TranscriptState =>
  ({ committed: [...s.committed, text], current: "" });

시각적으로는 확정된 텍스트는 안정된 스타일로, 현재 텍스트는 더 연하거나 기울임꼴 스타일로 표시해 사용자에게 변경될 수 있음을 알리세요.

발화 종료 감지와 음성 활동 감지(VAD)

무엇을 말했는지 아는 것은 절반에 불과합니다. 음성 인식기는 생각이 끝난 시점도 알아야 합니다. 이 판단에 따라 세그먼트를 완료하는 시점과, 에이전트에서 시스템이 응답을 시작하는 시점이 결정됩니다. 

발화 종료 감지는 발화가 끝났다고 판단하는 작업입니다. 너무 일찍 완료하면 문장 중간에 사용자의 말을 끊게 됩니다. 너무 늦게 완료하면 사용자가 분명히 말을 끝낸 뒤에도 에이전트가 침묵하게 됩니다.

Scribe v2 Realtime은 함께 사용할 수 있는 두 가지 상호 보완적 기능을 제공합니다.

  • 음성 활동 감지는 무음을 기준으로 오디오를 세그먼트로 나눕니다: 음성 인식기는 발화가 지속적인 무음으로 전환되는 시점을 감지하고, 이 경계를 사용해 세그먼트를 자동으로 완료합니다. VAD는 타이밍을 직접 추적하지 않아도 자연스러운 발화 리듬에 적응하므로 대화형 인터페이스에 적합한 기본 선택입니다.
  • 수동 커밋 제어: 수동 커밋 제어를 사용하면 무음과 관계없이 애플리케이션이 현재 세그먼트를 완료할 시점을 결정할 수 있습니다. 커밋 신호를 보내면 음성 인식기가 현재 세그먼트를 닫고 최종 결과를 내보냅니다. 푸시투토크 버튼을 놓는 경우, "전송" 동작 또는 외부 턴테이킹 정책처럼 애플리케이션이 이미 발화 차례가 끝났음을 아는 경우에 적합합니다.

두 기능은 함께 잘 작동합니다. 일반적인 음성 에이전트는 핸즈프리 동작에는 VAD를 사용하고 수동 커밋을 재정의 수단으로 제공하므로, 생각하기 위해 잠시 멈춘 사용자의 말을 끊지 않으면서 버튼을 누른 사용자에게는 즉시 경계를 제공할 수 있습니다.

무음 임계값은 보편적으로 정답인 값이 없는 वास्तविक한 절충점입니다.

  • 짧은 발화 종료 타임아웃(예: 약 200~400ms의 무음 후 완료)은 시스템의 반응성을 높여 줍니다. 하지만 절 사이에서 자연스럽게 멈추는 사용자의 말을 끊어 하나의 생각을 여러 세그먼트로 나눌 수 있으며, 에이전트에서는 성급한 응답을 유발할 수 있습니다.
  • 긴 타임아웃(예: 약 800~1200ms)은 자연스러운 멈춤을 허용하고 발화를 온전히 유지하지만, 시스템이 반응하기까지 눈에 띄는 지연이 생깁니다.

여기에 적용할 전역 상수는 없습니다. 상호작용에 맞게 임계값을 조정하세요.

  • 사용자는 문장 중간에 생각하기 때문에 받아쓰기와 메모 작성은 더 긴 멈춤을 허용합니다. 더 긴 타임아웃을 우선하고 VAD에 의존하세요.
  • 명령 및 제어, 트랜잭션 에이전트는 발화 차례가 짧고 명확하므로 더 짧은 타임아웃과 수동 커밋의 조합이 유리합니다.
  • 다국어 사용자 또는 비원어민 화자는 더 자주 멈추므로, 완료 전에 더 긴 무음 시간을 확보하세요.

이 팁을 활용하면 효과적인 발화 종료 감지 시스템을 구축하고 실시간 음성 텍스트 변환에 한 걸음 더 다가갈 수 있습니다.

스트림 내 기능: 언어 감지와 화자 분리

스트리밍 인식은 단어를 생성하는 것 이상의 기능을 수행할 수 있습니다. 다만 요청하는 추가 신호마다 지연 시간과 안정성에 영향을 줍니다. 원칙은 실시간 경험에 필요한 기능만 활성화하고 나머지는 배치 처리로 미루는 것입니다.

자동 언어 인식을 사용하면 Scribe v2 Realtime이 사전에 언어를 지정하지 않아도 지원하는 90개 이상의 언어 중 발화 언어를 식별합니다. 다만 모델이 확신 있게 판단하려면 짧은 구간의 오디오가 필요하므로, 언어가 확정되는 동안 스트림의 초기 부분 결과는 덜 안정적일 수 있습니다. 이미 언어를 알고 있다면 이를 지정해 모호성을 없애고 초기 부분 결과를 더 안정적으로 만들 수 있습니다.

화자 분리는 발화를 서로 다른 화자에게 할당하여 누가 무엇을 말했는지 식별합니다. 배치 전사에서는 모델이 전체 파일을 볼 수 있으므로 비교적 쉽습니다. 스트리밍에서는 더 어렵습니다. 음성 인식기는 지금까지 수신한 오디오만으로 화자 레이블을 할당해야 하며, 해당 화자의 음성을 더 들은 후 초기 오디오에 부여한 레이블을 수정해야 할 수 있습니다. 스트리밍 화자 레이블도 부분 텍스트처럼 처리하세요. 세그먼트가 완료되기 전까지는 잠정적입니다.

단어 단위 타이밍과 엔터티 컨텍스트도 같은 논리를 따릅니다. 토큰별 메타데이터를 더 많이 요청할수록 모델과 전송 구간 모두가 처리해야 할 데이터가 늘어납니다. 대부분의 실시간 UI에는 텍스트와 세그먼트 경계만 실시간으로 필요하며, 세부 메타데이터는 Scribe v2를 사용한 통화 후 배치 처리로 미룰 수 있습니다.

스트리밍용 오디오 형식: PCM 및 mu-law

전송과 인식 로직이 대부분의 주목을 받지만, 실제 환경의 버그 중 놀랄 만큼 많은 비율은 한 계층 아래인 오디오 인코딩과 청크 분할 방식에서 발생합니다. 형식과 청크 크기를 올바르게 설정하는 것은 가장 비용 효율적인 음성 텍스트 변환 지연 시간 개선 방법입니다.

PCM(선형, 16비트 부호 있음, 리틀 엔디언)은 캡처를 제어할 수 있을 때 사용할 형식입니다. 샘플링 레이트가 높을수록 더 많은 음향 세부 정보를 담습니다. 16kHz는 음성 인식의 표준 최소 수준이며 일반적으로 충분합니다. 8kHz는 전화 품질로, 고주파 콘텐츠가 손실됩니다. 소스에 맞는 레이트를 사용하세요. 복원할 정보 자체가 없으므로 8kHz 전화 오디오를 48kHz로 업샘플링해도 이점이 없습니다.

8kHz mu-law는 전화용 형식입니다. Twilio 같은 제공업체에서 통화를 수집하면 오디오는 8kHz mu-law로 도착하며, 두 번 트랜스코딩하지 말고 해당 형식으로 전달해야 합니다. 소스 형식을 맞추면 리샘플링 아티팩트와 불필요한 변환 단계를 피할 수 있습니다.

청크 크기는 체감 지연 시간을 가장 직접적으로 좌우하는 요소입니다. 오디오는 청크 단위로 전송하고, 음성 인식기는 청크가 도착할 때마다 부분 결과를 생성합니다. 청크가 작을수록 업데이트가 더 자주 발생하고 첫 부분 결과까지의 지연 시간이 줄어듭니다. 청크가 클수록 메시지 수는 줄고 추론당 문맥은 조금 늘어납니다. 실용적인 범위는 청크당 오디오 20~250ms입니다. 구체적으로 16kHz 모노 16비트 PCM에서는 1초 오디오가 32,000바이트이므로 100ms 청크는 약 3,200바이트입니다.

브라우저에서 마이크 입력 캡처하기

브라우저에서는 Web Audio API와 AudioWorklet을 사용하는 것이 적합합니다. 워크릿은 오디오 렌더링 스레드에서 실행되고 작은 프레임 단위로 오디오를 수신하므로, 이전 ScriptProcessorNode와 달리 메인 스레드 지연의 영향을 받지 않습니다. 워크릿의 역할은 브라우저의 기본 부동 소수점 샘플을 16비트 PCM으로 변환해 메인 스레드에 전달하는 것이며, 메인 스레드는 이를 WebSocket으로 전달합니다.

워크릿 프로세서의 핵심은 부동 소수점 샘플을 PCM으로 변환하는 작업입니다.

// pcm-worklet.ts - registered via audioContext.audioWorklet.addModule()
class PCMWorklet extends AudioWorkletProcessor {
  process(inputs: Float32Array[][]) {
    const channel = inputs[0]?.[0]; // mono; Float32, range [-1, 1]
    if (!channel) return true;
    const pcm = new Int16Array(channel.length);
    for (let i = 0; i < channel.length; i++) {
      const s = Math.max(-1, Math.min(1, channel[i]));
      pcm[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
    }
    // Transfer the buffer to the main thread without copying.
    this.port.postMessage(pcm.buffer, [pcm.buffer]);
    return true;
  }
}
registerProcessor("pcm-worklet", PCMWorklet);

코드로 살펴보는 파이프라인

파이프라인은 세 부분으로 구성됩니다. 마이크를 캡처하고 PCM을 서버로 스트리밍하는 브라우저 클라이언트, 오디오를 Scribe v2 Realtime으로 전달하고 전사본을 다시 전달하는 Node 서버, 그리고 파일 또는 전화 브리지에서 PCM을 스트리밍하는 스크립트형 클라이언트입니다.

음성 인식기를 브라우저에 직접 노출하지 않고 서버가 중계해야 하는 중요한 이유가 있습니다. ElevenLabs API 키는 비밀 정보이므로 클라이언트 측 코드에 절대 포함되어서는 안 됩니다. 키는 서버에 보관합니다. 브라우저가 음성 인식기와 직접 통신해야 한다면, 서버 측에서 수명이 짧은 일회용 토큰을 발급해 API 키 대신 클라이언트에 전달하세요.

브라우저 클라이언트

클라이언트는 서버에 WebSocket을 열고, 위의 워크릿을 통해 마이크를 캡처하며, 생성되는 각 PCM 프레임을 전달합니다. 수신 이벤트는 이미 서버에서 { type, text } 형식으로 정규화되며, 앞서 설명한 부분/최종 상태를 제어합니다.

// client.ts - runs in the browser. ws is an open WebSocket to your server.
const audioContext = new AudioContext({ sampleRate: 16000 });
await audioContext.audioWorklet.addModule("pcm-worklet.js");

const mediaStream = await navigator.mediaDevices.getUserMedia({
  audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true },
});

const source = audioContext.createMediaStreamSource(mediaStream);
const worklet = new AudioWorkletNode(audioContext, "pcm-worklet");

// Forward each PCM frame to the server the moment it is produced.
worklet.port.onmessage = (e: MessageEvent<ArrayBuffer>) => {
  if (ws.readyState === WebSocket.OPEN) ws.send(e.data);
};
source.connect(worklet);

// Manual commit: tell the server to finalize the current segment.
const commit = () => ws.send(JSON.stringify({ type: "commit" }));

서버 중계

서버는 클라이언트당 하나의 음성 인식기 연결을 열고, API 키를 서버에 보관하며, 바이너리 PCM을 그대로 전달하고, 음성 인식기 이벤트를 클라이언트가 사용하는 안정적인 { type, text } 형식으로 정규화합니다.

// server.ts - Node, using the `ws` library. ELEVENLABS_API_KEY and the
// recognizer URL come from the environment; see the Speech to Text reference
// for the exact path and query parameters.
import { WebSocketServer, WebSocket } from "ws";

new WebSocketServer({ port: 8080 }).on("connection", (client) => {
  // The API key stays on the server, never on the wire to the browser.
  const recognizer = new WebSocket(process.env.RECOGNIZER_WSS_URL!, {
    headers: { "xi-api-key": process.env.ELEVENLABS_API_KEY! },
  });

  // Browser -> recognizer: forward binary PCM, translate control messages.
  client.on("message", (data, isBinary) => {
    if (recognizer.readyState !== WebSocket.OPEN) return;
    if (isBinary) recognizer.send(data); // raw PCM bytes
    else if (JSON.parse(data.toString()).type === "commit")
      recognizer.send(sendCommit());
  });

  // Recognizer -> browser: normalize events into a stable shape.
  recognizer.on("message", (raw) => {
    const event = parseRecognizerEvent(raw.toString());
    if (event && client.readyState === WebSocket.OPEN)
      client.send(JSON.stringify(event));
  });

  // ... open handshake, queueing pre-open audio, and teardown on close/error
});

엔드포인트별 처리는 아래의 두 어댑터 함수에만 한정됩니다. 필드 이름을 텍스트 음성 변환 레퍼런스의 정확한 이름으로 바꾸면 되며, 나머지 파이프라인은 변경되지 않습니다.

// The single place that knows the recognizer's wire format.
const sendCommit = (): string => JSON.stringify({ type: "commit" });

type NormalizedEvent =
  | { type: "partial"; text: string }
  | { type: "final"; text: string }
  | { type: "vad"; speaking: boolean };

function parseRecognizerEvent(raw: string): NormalizedEvent | null {
  const msg = JSON.parse(raw);
  if (msg.is_final === true || msg.type === "final")
    return { type: "final", text: msg.text ?? "" };
  if (msg.type === "vad") return { type: "vad", speaking: !!msg.speaking };
  if (typeof msg.text === "string")
    return { type: "partial", text: msg.text };
  return null;
}

스크립트형 백엔드 클라이언트

백엔드 파이프라인과 아래 벤치마크에서는 브라우저 없이도 동일한 음성 인식기 연결을 사용할 수 있습니다. 모든 소스에서 PCM을 읽고, 실시간 청크 주기에 맞춰 전송하며, 이벤트를 다시 수신하면 됩니다. API 키와 URL은 서버와 마찬가지로 환경 변수에서 가져옵니다.

// stream-stt.ts - pace ~100ms chunks at real time, then commit the tail.
const SAMPLE_RATE = 16000, CHUNK_MS = 100;
const CHUNK_BYTES = (SAMPLE_RATE * 2 * CHUNK_MS) / 1000; // 3200 bytes
const ws = new WebSocket(process.env.RECOGNIZER_WSS_URL!, {
  headers: { "xi-api-key": process.env.ELEVENLABS_API_KEY! },
});

// Send: walk the PCM buffer in 100ms chunks, sleeping between to mimic a
// live source. For audio that already arrives in real time, drop the sleep.
async function sendAudio(pcm: Buffer) {
  for (let off = 0; off < pcm.length; off += CHUNK_BYTES) {
    ws.send(pcm.subarray(off, off + CHUNK_BYTES));
    await new Promise((r) => setTimeout(r, CHUNK_MS));
  }
  ws.send(JSON.stringify({ type: "commit" })); // finalize the trailing segment
}

// Receive: print partials in place, append finals.
ws.on("message", (raw) => {
  const e = parseRecognizerEvent(raw.toString());
  if (e?.type === "final") console.log(`[final]   ${e.text}`);
  else if (e?.type === "partial") process.stdout.write(`[partial] ${e.text}\r`);
});

음성 텍스트 변환 지연 시간 및 단어 오류율 벤치마킹

지연 시간과 단어 오류율은 화자, 언어, 음향 조건, 오디오 길이, 각 제공업체의 가장 가까운 리전까지의 네트워크 경로, 그리고 각 서비스의 현재 부하에 따라 달라집니다. 

한 도시의 노트북에서 측정한 결과는 다른 도시의 프로덕션 인프라에 일반화할 수 없습니다. 프로덕션과 유사한 인프라에서 실제 입력과 유사한 오디오로 테스트 하니스를 실행하고, 단일 수치 대신 범위와 분포를 보고하세요.

중요한 지연 시간 및 정확도 수치는 프로덕션과 유사한 인프라에서 자체 오디오로 측정한 수치뿐입니다. 다음은 음성 텍스트 변환 지연 시간을 벤치마킹하는 방법입니다.

음성 텍스트 변환 지연 시간에서 측정할 항목

실시간 음성 텍스트 변환 지연 시간을 벤치마킹할 때 측정해야 할 주요 지표는 다음과 같습니다.

  • 첫 부분 결과까지의 시간: 첫 오디오 청크 전송부터 비어 있지 않은 첫 부분 결과 수신까지의 시간입니다.
  • 부분 결과에서 최종 결과까지의 지연: 발화의 마지막 오디오 청크부터 최종 가설까지의 시간입니다.
  • 단어 오류율(WER): 사람이 작성한 기준 전사본과 최종 전사본 간의 WER로, 모든 시스템에서 동일하게 계산합니다.
  • 안정성 변동:완료되기 전 부분 결과가 몇 번 수정되는지를 나타냅니다. 이 지표는 실시간 UI가 얼마나 자주 변경되어 보일지 보여주는 대리 지표입니다.

통제 항목

신뢰할 수 없는 데이터를 피하려면 일관성을 유지할 수 있도록 실험에 여러 통제 항목을 적용해야 합니다.

음성 텍스트 변환 지연 시간 벤치마킹에 사용할 주요 통제 항목은 다음과 같습니다.

  • 동일한 오디오: 모든 시스템에 동일한 파일, 샘플링 레이트, 인코딩을 입력으로 사용하세요.
  • 동일한 전송 주기: 모든 시스템을 동일한 실시간 청크 주기(예: 100ms 청크)로 스트리밍하세요.
  • 반복 실행 및 분포 보고:하루 동안 각 파일을 여러 번 실행하고 중앙값과 꼬리 값(p50/p95)을 보고하세요.
  • 동일한 기준 전사본 및 점수 산정:WER을 계산하기 전에 텍스트를 동일한 방식으로 정규화하세요(대소문자, 문장부호, 숫자).
  • 리전 및 네트워크 공개:테스트 하니스가 실행된 위치와 각 제공업체까지의 경로를 명시하세요.

이 모든 요소를 동일하게 유지하면 더 정확한 지표를 얻을 수 있습니다.

하니스 골격

측정 코어는 제공업체 어댑터를 받아 첫 부분 결과까지의 시간, 완료 지연, 부분 결과 변동을 기록합니다.

// benchmark.ts - measurement core; one StreamFn adapter per provider.
type StreamFn = (
  audioPath: string,
  onEvent: (kind: "partial" | "final", text: string) => void,
  result: RunResult
) => Promise<void>; // adapter sets result.lastChunkSentAt on the final chunk

interface RunResult {
  firstPartialMs?: number;
  finalLagMs?: number;
  hypothesis: string;
  partialEdits: number;
  lastChunkSentAt: number;
  startedAt: number;
}

async function measure(streamFn: StreamFn, audioPath: string): Promise<RunResult> {
  const result: RunResult = {
    hypothesis: "", partialEdits: 0, lastChunkSentAt: 0,
    startedAt: performance.now(),
  };
  let prevPartial = "";

  await streamFn(audioPath, (kind, text) => {
    const now = performance.now();
    if (kind === "partial") {
      if (text && result.firstPartialMs === undefined)
        result.firstPartialMs = now - result.startedAt;
      if (text !== prevPartial) { result.partialEdits++; prevPartial = text; }
    } else { // final
      result.hypothesis = result.hypothesis ? `${result.hypothesis} ${text}` : text;
      if (result.lastChunkSentAt)
        result.finalLagMs = now - result.lastChunkSentAt;
    }
  }, result);

  return result;
}

단어 오류율은 정규화된 텍스트에 대한 표준 토큰 수준 레벤슈타인 거리입니다. 계산 전에 기준 전사본과 가설 모두에 동일하게 소문자 처리 및 문장부호 제거를 적용해야 합니다. 그렇지 않으면 모델이 아니라 정규화기를 측정하게 됩니다. 단일 샘플은 네트워크 변동의 영향을 크게 받으므로, 각 제공업체에서 파일별로 약 10회 실행하는 루프로 이 지표를 감싸고 첫 부분 결과까지의 중앙값과 중앙 WER(p50/p95)을 보고하세요.

실행하려면 두 가지를 제공해야 합니다. 먼저 시스템마다 StreamFn 어댑터를 하나씩 작성하세요. 위의 스크립트형 클라이언트는 이미 하나이며, 다른 어댑터도 동일한 (audioPath, onEvent, result) 계약을 따르고 마지막 오디오 청크가 전송될 때 result.lastChunkSentAt을 설정하면 됩니다. 다음으로 오디오 파일과 기준 전사본을 불러와 전체에 걸쳐 measure를 호출하세요. 배포 환경을 대표하는 머신에서 사용자 오디오를 대표하는 입력으로 실행하면 재현 가능한 비교 결과를 얻을 수 있습니다.

실시간 음성 텍스트 변환을 구현하는 방법 요약 

이 글에서는 시스템을 반복적으로 개선하고 실시간 음성 텍스트 변환에 다가갈 수 있도록 다양한 아키텍처 변경 사항을 다뤘습니다.

프로덕션 환경의 실시간 STT 시스템은 몇 가지 핵심 결정으로 요약됩니다.

  • 전송: 단순성과 제어된 네트워크 환경이 필요하면 WebSocket을 선택하고, 손실 허용이 필요하며 소비자 기기에서 캡처한다면 WebRTC를 선택하세요.
  • 부분 결과와 최종 결과: 부분 결과는 잠정적, 최종 결과는 확정된 것으로 처리하고 서로 다르게 표시해 사용자가 실시간 텍스트를 신뢰할 수 있게 하세요.
  • 발화 종료 감지: 핸즈프리 세그먼트 분할에는 VAD를 사용하고, 수동 커밋을 재정의 수단으로 사용하며, 고정된 상수가 아닌 상호작용에 맞춰 무음 임계값을 조정하세요.
  • 스트림 내 기능: 실시간 경험에 필요한 경우에만 스트림 내 기능을 활성화하고, 나머지는 Scribe v2를 활용한 배치 처리로 미루세요.
  • 오디오 형식: 작은 PCM 프레임으로 캡처하고 약 100ms 청크로 전송하며, 전화 오디오에는 소스 형식에 맞추세요.
  • 벤치마킹: 자체 오디오와 목표 지표를 기준으로 정확도와 지연 시간 간의 설정값을 경험적으로 조정하세요.
  • API 보안: API 키는 서버에 보관하세요, 또는 직접 클라이언트 연결에는 일회용 토큰을 발급하세요.

음성 에이전트에서 지연 시간 최적화하기 방법이 궁금하시다면, 이를 위한 가이드도 준비되어 있습니다.

Scribe v2 Realtime으로 실시간 음성 텍스트 변환 시스템 구축하기

Scribe v2 Realtime은 약 150ms의 모델 지연 시간으로 부분 결과를 생성합니다. 사용자가 이 수치 또는 그보다 큰 지연 시간을 경험하는지는 주변 아키텍처에 달려 있으며, 이 부분은 직접 제어할 수 있습니다. 이 글에서 소개한 전략을 적용하면 지연 시간을 줄이고 고객 경험을 개선하는 향상된 파이프라인 아키텍처를 구축할 수 있습니다. 

더 자세히 알아보려면 텍스트 음성 변환 기능 개요를 살펴보고, 전체 기능 및 언어 목록은 모델 레퍼런스에서 확인하세요. 실시간 제품 페이지도 방문해 보세요: 실시간 텍스트 음성 변환 API 및 실시간 텍스트 음성 변환.

개발을 시작할 준비가 되셨다면 무료 ElevenLabs 계정 만들기를 통해 오늘 첫 전사본을 스트리밍하세요.

실시간 음성 텍스트 변환 지연 시간 FAQ

유사한 기사

최고 품질의 AI 오디오로 창작하세요