본문으로 건너뛰기
김신건의 로그

[Voice AI] STT 스트리밍: partial vs final, endpointing, gRPC/WS

· 수정 · 📖 약 2분 · 673자/단어 #stt #streaming #asr #partial #final #endpointing
STT streaming, partial transcript, final transcript, endpointing, time-to-final, interim result, gRPC streaming STT, WebSocket STT

정의

스트리밍 STT = 오디오 청크 가 들어올 때마다 부분 결과 (partial) 를 즉시 반환 + 발화 종료최종 결과 (final) 확정.

IMPORTANT

대화형 음성 AI 의 첫 단계. 종단 지연 1초 미만시작점.

partial vs final

sequenceDiagram
    autonumber
    participant Mic
    participant STT
    participant App

    Mic->>STT: chunk 1 (100ms)
    STT-->>App: partial: "안녕"
    Mic->>STT: chunk 2
    STT-->>App: partial: "안녕하세"
    Mic->>STT: chunk 3
    STT-->>App: partial: "안녕하세요"
    Mic->>STT: chunk 4 (침묵)
    Note over STT: endpoint 감지
    STT-->>App: FINAL: "안녕하세요"
    App->>App: LLM 호출
partial (interim)final
의미현재 추정 (변경 가능)확정
빈도매 100-300msendpointing 후 1회
정확도낮음 (변경됨)높음
사용UI 라이브 자막LLM 호출, action

CAUTION

partial 로 LLM 호출 금지. 반복 호출 + 변경된 텍스트 → 낭비 + 일관성 깨짐.

Endpointing (발화 종료 감지)

사용자 침묵 → 얼마나 기다린 후 final?
전략의미
에너지 기반 VAD침묵 N ms 후 종료
신경망 VAD (Silero)음성/비음성 확률 기반
시맨틱 endpointing문장 의미 + VAD (LiveKit 0.4)
고정 timeout무조건 N초 후
일반: 침묵 500-800ms 후 final
short: 200-400ms (빠른 대화)
long: 1000-1500ms (사용자가 멈춤 많음)

시맨틱 endpointing 의 가치

❌ 순수 VAD: "내 번호는 010-..." [500ms 침묵] → final "내 번호는 010" → 사용자 끊김!
✓ 시맨틱: "내 번호는 010-1234-5678" 까지 *기다림* (전화번호 패턴 인식)

LiveKit, Pipecat 의 자체 모델오디오 + 텍스트 보고 ~300ms 까지 단축.

프로토콜: WebSocket vs gRPC

flowchart LR
    Client[Client] -->|WebSocket binary frames| Server1["STT (WebSocket)"]
    Client -->|gRPC stream| Server2["STT (gRPC)"]
    Client -->|HTTP/2 + chunks| Server3["STT (HTTP/2)"]
프로토콜장점단점
WebSocket브라우저 native, 단순텍스트/binary 혼합 어색
gRPC bidi-streambinary 효율적, schema브라우저는 grpc-web 필요
HTTP/2 chunked표준session 관리 직접

2026 시점 서버↔서버 = gRPC (CLOVA, Google), 브라우저↔서버 = WebSocket (Deepgram, AssemblyAI) 분기.

WebSocket 예시 (Deepgram)

const ws = new WebSocket('wss://api.deepgram.com/v1/listen?model=nova-3&language=ko&interim_results=true&endpointing=500');

ws.binaryType = 'arraybuffer';

ws.onmessage = (e) => {
  const data = JSON.parse(e.data);
  if (data.channel?.alternatives[0]) {
    const transcript = data.channel.alternatives[0].transcript;
    const isFinal = data.is_final;
    if (isFinal) {
      onFinalTranscript(transcript);
    } else {
      onPartial(transcript);
    }
  }
};

// 마이크 → WebSocket 오디오 청크
const audioContext = new AudioContext({ sampleRate: 16000 });
const processor = audioContext.createScriptProcessor(2048, 1, 1);
processor.onaudioprocess = (e) => {
  const pcm = float32To16BitPCM(e.inputBuffer.getChannelData(0));
  ws.send(pcm);
};

gRPC 예시 (CLOVA, Python)

import grpc
import nest_pb2, nest_pb2_grpc

def request_generator(audio_stream):
    yield nest_pb2.NestRequest(
        type=nest_pb2.RequestType.CONFIG,
        config=nest_pb2.NestConfig(
            language='ko-KR',
            sample_rate=16000,
            encoding=nest_pb2.AudioEncoding.LINEAR16,
        )
    )
    for chunk in audio_stream:
        yield nest_pb2.NestRequest(
            type=nest_pb2.RequestType.AUDIO,
            audio=chunk,
        )

with grpc.secure_channel('clovaspeech-gw.ncloud.com:443', creds) as channel:
    stub = nest_pb2_grpc.NestServiceStub(channel)
    metadata = [('authorization', f'Bearer {token}')]
    for response in stub.Recognize(request_generator(audio_iter), metadata=metadata):
        if response.is_final:
            print('FINAL:', response.transcript)
        else:
            print('partial:', response.transcript)

지표

지표의미목표
TTP (Time-to-Partial)첫 partial 까지< 200ms
TTF (Time-to-Final)발화 종료 → final< 500ms
TTL (Time-to-Last partial)마지막 partial 까지< 100ms
Stabilitypartial 의 변경 빈도< 5%

자세한 latency 분석은 latency-percentiles.

흔한 함정

WARNING

  1. partial 로 LLM 호출 = 낭비. final 까지 기다리거나 시맨틱 endpointing.
  2. endpointing 너무 김 (1.5초+) = 대화 어색. 사용자가 답답함.
  3. endpointing 너무 짧음 (200ms) = 사용자 말끊김. 시맨틱 권장.
  4. WebSocket idle timeout = 침묵 시 connection close. ping/pong 주기적.
  5. 언어 자동 감지 = 짧은 발화 / 다국어 혼용 시 깨짐. 명시.

오디오 포맷

항목권장이유
Sample rate16,000 HzASR 모델 학습 표준
Bit depth16-bit Linear PCM무손실, 변환 오버헤드 없음
Channels1 (mono)스테레오는 다운믹스 후 전송
Chunk 크기100-200mspartial 응답 주기와 균형
CodecRaw PCM 또는 OpusOpus 는 대역폭 절약 (WebRTC 표준)
# Web Audio API Float32 -> Linear PCM 16-bit 변환
import numpy as np

def float32_to_int16(audio: np.ndarray) -> bytes:
    clipped = np.clip(audio, -1.0, 1.0)
    return (clipped * 32767).astype(np.int16).tobytes()

연결 안정성

스트리밍은 수 분~수 시간 연결 유지가 필요. 네트워크 상태에 따라 끊길 수 있음.

KeepAlive (WebSocket)

// Deepgram: 10초마다 KeepAlive 메시지 전송, 침묵 시 연결 종료 방지
let backoff = 1000;
const keepAlive = setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: 'KeepAlive' }));
  }
}, 10_000);

ws.onclose = () => {
  clearInterval(keepAlive);
  setTimeout(connect, Math.min(backoff * 2, 30_000)); // exponential backoff
};

gRPC Keepalive (Python)

channel_options = [
    ('grpc.keepalive_time_ms', 15_000),
    ('grpc.keepalive_timeout_ms', 5_000),
    ('grpc.keepalive_permit_without_calls', True),
]
channel = grpc.secure_channel(host, creds, options=channel_options)

화자 분리 (Diarization)

여러 화자가 동시에 등장하는 환경 (회의, 콜센터) 에서 화자별 전사 필요.

// Deepgram diarize=true 예시
const ws = new WebSocket(
  'wss://api.deepgram.com/v1/listen?diarize=true&model=nova-3&language=ko'
);

ws.onmessage = (e) => {
  const { is_final, channel } = JSON.parse(e.data);
  if (is_final) {
    const words = channel.alternatives[0].words;
    words.forEach(w => {
      console.log(`[Speaker ${w.speaker}] ${w.word}`);
    });
  }
};
상황권장
사용자:AI 1:1 대화diarization 불필요
회의 전사diarize=true + 화자 수 힌트
콜센터채널 분리 후 각 채널 별도 전사

음성 에이전트 통합 아키텍처

flowchart TD
    MIC["마이크 / 전화"] --> VAD["VAD (Silero)"]
    VAD -->|"음성 구간 감지"| STT["STT 엔진"]
    STT -->|partial| UI["UI 라이브 자막"]
    STT -->|final| EP["Endpointing 모듈"]
    EP -->|"발화 완료"| LLM["LLM"]
    LLM -->|"응답 텍스트"| TTS["TTS 스트리밍"]
    TTS --> SPK["스피커 / 전화"]
    LLM -->|"barge-in 신호"| VAD

partial 이 들어오는 동안 barge-in 감지 시 TTS 중단 + VAD 재활성화.

관련 위키

이 글의 용어 (7개)
[Django] Channels: WebSocket, Consumer, Groupdjango
정의 Django Channels는 Django에 WebSocket, 비동기 프로토콜, 백그라운드 작업을 추가하는 공식 확장. 기본 Django는 HTTP만, Channels는 …
[Network] gRPC: HTTP/2 + Protobuf, 4가지 streaming 패턴network
정의 gRPC 는 HTTP/2 위에서 Protobuf 직렬화 로 동작하는 고성능 RPC 프레임워크. Google 내부 Stubby 의 오픈소스 후계. 핵심 4가지: 1. Prot…
[Voice AI] 음성 에이전트 아키텍처: Cascading / Streaming / S2S + Latency Budgetai
정의 실시간 음성 AI 의 3가지 패턴. 각자 지연 vs 유연성 vs 비용 트레이드오프. 3 패턴 비교 | | Cascading | Streaming | S2S | |---|--…
[Voice AI] P50/P95/P99: latency 분포와 SLOdevops
정의 P50 / P95 / P99 = latency 분포의 분위수. 평균은 outlier 에 가려져 무의미. 음성 에이전트의 사용자 경험 은 p95/p99 가 결정. [!IMPO…
[Voice AI] STT 모델: Whisper, Deepgram, AssemblyAI, CLOVAvoice
정의 STT (Speech-to-Text) / ASR (Automatic Speech Recognition) = 음성 → 텍스트 변환. 2026 시점 전사 정확도 인간 수준, 실…
[Voice AI] Turn Detection + Barge-in + AEC: 자연스러운 대화voice
정의 자연스러운 음성 대화의 3 요소: 1. Turn Detection - 사용자가 말끝났는지 정확히 판단 2. Barge-in - 에이전트가 말하는 중에 사용자가 끼어들 수 있…
[Voice AI] VAD: Silero, 에너지 기반, endpointing 임계값voice
정의 VAD (Voice Activity Detection) = 오디오 스트림에서 음성 vs 비음성 (침묵, 노이즈) 구분. 음성 에이전트의 발화 시작 / 종료 감지에 필수. 종…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기