Eleven v4를 소개합니다역대 가장 감성적인 모델, Eleven v4를 만나보세요.

콘텐츠로 건너뛰기

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와 달리 메인 스레드 지연의 영향을 받지 않습니다. 워크릿의 역할은 브라우저 기본 float 샘플을 16비트 PCM으로 변환하여 메인 스레드에 전달하는 것이며, 메인 스레드는 이를 WebSocket으로 전송합니다.

워크릿 프로세서의 핵심은 float를 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
});

엔드포인트별 요소는 아래 두 어댑터 함수에만 국한됩니다. 필드 이름을 Speech to Text 레퍼런스의 정확한 이름으로 바꾸면 나머지 파이프라인은 변경할 필요가 없습니다:

// 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을 설정하면 됩니다. 다음으로 오디오 파일과 참조문을 불러와 그 전체에 대해 측정을 호출하세요. 배포 환경을 대표하는 머신에서, 사용자를 대표하는 오디오로 실행하면 재현 가능한 비교 결과를 얻을 수 있습니다.

실시간 음성-텍스트 변환 달성 방법 정리 

이 글에서는 시스템을 점진적으로 개선하고 실시간 음성-텍스트 변환에 가까워지도록 돕는 여러 아키텍처 변경 사항을 살펴보았습니다.

프로덕션용 실시간 STT 시스템은 몇 가지 핵심 결정으로 귀결됩니다:

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

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

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

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

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

개발할 준비가 되었다면 무료 ElevenLabs 계정을 만드세요 그리고 오늘 첫 전사문을 스트리밍하세요.

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

작성자

Tadas는 개발자가 ElevenLabs 제품군을 활용해 개발할 수 있도록 돕는 개발자 관계 엔지니어입니다. 이전에는 개발자 교육 플랫폼의 창립자였으며, Agora의 수석 개발자 애드보킷과 Zero To Mastery의 강사로 활동했고, YouTube에서 개발자를 가르치며 커뮤니티를 구축했습니다.

유사한 기사

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