LiveKit 파이프라인 모드 — Half-Cascade vs Voice Pipeline 역할과 장단점 (시각화) 아키텍처

마지막 업데이트 2026-08-18

작성일: 2026-08-18 대상: 아동↔핑퐁이 음성 대화의 pipeline_mode 두 갈래와 각 구성요소의 역할·트레이드오프 기준 브랜치: feature/livekit-vp-voice-pipeline
💬 대화로 먼저 이해하기 — "통역사 한 명이냐, 전문가 세 명이냐" (비개발자·처음 읽는 사람용)
QLiveKit 모드가 두 개(HC / VP)라고 하는데 무슨 차이인가요?
A아동의 말을 ①받아쓰고 ②무슨 말을 할지 정하고 ③목소리로 읽어주는 세 가지 일을 누가 나눠 맡느냐의 차이입니다. Half-Cascade는 ①+②를 OpenAI Realtime 한 곳이 통째로 맡고, Voice Pipeline은 ①Soniox · ②GPT · ③Typecast로 셋을 따로 씁니다. ③번(목소리)은 두 모드 모두 Typecast입니다.
Q한 곳이 다 하는 게 좋아 보이는데요?
A왕복이 한 번 줄어드는 대신, 그 한 곳이 못 하는 기능은 포기해야 합니다. 예를 들어 아동이 말을 끝내기 전에 답을 미리 만들어 두는 기능(preemptive generation)은 Realtime 경로에서는 SDK가 막아 둡니다. 반대로 세 곳을 쓰면 각 단계를 따로 바꾸고 따로 들여다볼 수 있지만, 실패 지점이 세 개가 됩니다.
Q어느 모드로 갈지는 누가 정하나요?
A아동 계정의 "대화 모드" 설정 한 개입니다. livekit이면 Half-Cascade, livekit_vp면 Voice Pipeline. 기본값은 livekit(HC)입니다.
Q말이 끝났는지 판단하는 건 누가 하죠?
AVoice Pipeline은 항상 LiveKit 쪽 VAD가 판단합니다. Half-Cascade는 LiveKit VAD로도, OpenAI Realtime 쪽 판단(server_vad / semantic_vad)으로도 돌릴 수 있어 다이얼이 하나 더 있습니다.

TL;DR — 세 줄 요약

① 같은 배관, 다른 심장. 세션 발급 → Valkey configRef → LiveKit RoomAgentDispatchppi-agent 워커까지는 두 모드가 완전히 동일하고, 갈라지는 지점은 워커 안 단 한 줄(agent.py:3461)입니다.
② HC는 벤더 한 곳(OpenAI Realtime)이 STT+LLM을 묶어 처리하고 음성만 Typecast로 뽑습니다(modalities=["text"]). VP는 STT(Soniox) · LLM(GPT-5.4) · TTS(Typecast)를 각각 붙입니다.
③ 트레이드오프의 핵심은 "홉 수" vs "제어권"입니다. HC는 왕복이 적고 의미 기반 턴 감지를 쓸 수 있지만 preemptive generation·EOU 분류기·adaptive 끼어들기를 못 씁니다. VP는 그 세 개를 다 쓸 수 있고 단계별 관측·교체가 되지만, 실패·지연 지점이 셋으로 늘고 응답 완료 판정을 자체 STT에 의존합니다(local_stt).
PPI web (브라우저·Next.js) LiveKit 서버/SFU ppi-agent 워커 (ECS) Valkey 스냅샷 외부 벤더 (OpenAI · Soniox · Typecast)

01모드 다이얼 — 아동 계정 설정 하나가 런타임을 고른다

음성 런타임은 4갈래이고, 그중 LiveKit 두 갈래만 ppi-agent를 씁니다. 판정은 전부 resolver.ts 한 함수 안에서 끝납니다.

user.conversationMode아동 계정 설정 · 기본값 livekit (conversation-mode.ts:3)
livekit
LiveKit HCpipelineMode
half_cascade
livekit_vp
LiveKit VPpipelineMode
voice_pipeline
tts
Realtime + Typecast기존 직결 경로
(agent 미사용)
realtime
OpenAI 음성Realtime 음성 그대로
(agent 미사용)
모드 값UI 라벨런타임담당 구성근거
livekitLiveKit HClivekitOpenAI Realtime(STT+LLM) + Typecast TTSlivekit-token.ts:204
livekit_vpLiveKit VPlivekitSoniox STT + GPT-5.4 + Typecast TTSlivekit-token.ts:192
ttsTTS 분리ttsOpenAI Realtime 텍스트 + 브라우저 Typecast 재생resolver.ts:142-149
realtimeRealtimerealtimeOpenAI Realtime 음성 직결resolver.ts:112-116
폴백 함정LiveKit 두 모드는 아바타에 Typecast 보이스(avatar.ttsVoice)가 있을 때만 성립합니다. 없으면 voiceSession.livekit이 아예 undefined가 되고 런타임이 realtime으로 내려갑니다 (fallbackReason = avatar_livekit_tts_voice_missing · resolver.ts:119-121, 150-157). 즉 "VP로 설정했는데 OpenAI 음성이 나온다"면 코드 버그가 아니라 아바타 보이스 미설정일 수 있습니다.
develop 대비livekit_vppipelineMode는 이 브랜치에서 추가됐습니다. develop의 ConversationModelivekit | tts | realtime 3종이고 수업 경로는 항상 half-cascade였습니다. 워커(agent.py)는 develop에서도 두 모드 빌더를 모두 갖고 있었고, 이번 변경은 수업 경로에서 VP를 고를 수 있게 한 것 + 완료 계약 분기입니다.

02두 모드가 공유하는 배관 — 여기까지는 완전히 같다

모드 차이는 워커가 세션을 조립하는 순간에만 나타납니다. 토큰 발급·설정 전달·dispatch·미디어 경로는 공유 자산입니다.

아동 브라우저POST /api/livekit/call
buildLiveKitModelConfig()공통 config(TTS·VAD·endpointing·노이즈·프롬프트) + pipeline_mode 분기
livekit-token.ts:101-212
Valkey 설정 스냅샷TTL 7200s · 최대 1MB
livekit-session-config-store.ts:4-6
participant tokenRoomAgentDispatch(agentName=ppi-agent) · TTL 2400s
livekit-token.ts:66-97
ppi-agent 워커 (ECS/Fargate)_fetch_model_config()가 configRef로 스냅샷 fetch
agent.py:308-349
pipeline_mode == "half_cascade"
_build_half_cascade_session()agent.py:1047
그 외 (voice_pipeline)
_build_voice_pipeline_session()agent.py:883
브라우저 오디오 체인 + mediasoup 브리지AI 음성·아동 마이크를 두 번째 SFU로 재발행 → 진행자 모니터
두 SFU 브리지·오디오 체인·RPC 제어의 상세는 LiveKit 음성 에이전트 흐름 & 두 SFU 구조 문서를 봅니다. 이 문서는 그 위에서 파이프라인 내부만 다룹니다.
공통 config에 들어가는 것Typecast 보이스·감정 옵션, turn_detection_engine, allow_interruptions, endpointing(fixed + min/max_delay_ms), livekit_vad(Silero 파라미터), server_noise_processing(기본 ai_coustics_quail_l · enhancement 0.9), 활동 프롬프트·인사말, greeting_enabled: false. 모드와 무관하게 동일하게 전달됩니다(livekit-token.ts:137-181).

03파이프라인 나란히 보기 — 같은 5단계, 다른 담당

아동의 발화 한 턴이 AI 음성 한 턴으로 바뀌는 경로입니다. 빨간 칸이 외부 벤더 홉이고, HC는 벤더 홉 2개 · VP는 3개입니다.

Half-Cascade pipeline_mode: "half_cascade" · 기본 모드
STT와 LLM이 한 벤더 세션 안에서 처리되고, 음성만 밖에서 만든다.
① 마이크브라우저 오디오 체인 → LiveKit 트랙
② 노이즈 + 턴 감지ai-coustics 정제 후
LiveKit Silero VAD 또는
Realtime server/semantic VAD
③④ OpenAI Realtime전사 gpt-4o-transcribe +
응답 생성 (modalities=["text"])
agent.py:1129-1144
⑤ Typecast TTSssfm-v30 · 아바타 보이스
agent.py:390-417
⑥ 오디오 트랙agent → 브라우저 재생 → 모니터 브리지
③④가 한 칸인 것이 HC의 정체입니다. Realtime 세션이 오디오를 직접 받아 전사와 응답을 함께 내보내고, PPI는 텍스트만 받습니다(음성 모달리티를 끔).
Voice Pipeline pipeline_mode: "voice_pipeline" · livekit_vp
세 단계가 각각 다른 벤더. 각 경계에서 로그·교체·정책이 가능해진다.
① 마이크동일
② 노이즈 + 턴 감지ai-coustics 정제 후
LiveKit Silero VAD 고정
+ endpointing(fixed)
③ Soniox STTstt-rt-v5 · ko
streaming interim/final
stt_provider.py:119-126
④ OpenAI LLMgpt-5.4 (Chat)
livekit-token.ts:21-22
⑤ Typecast TTSssfm-v30 · 동일
⑥ 오디오 트랙동일
③에는 PPI 자체 계측 래퍼 InstrumentedSTT가 붙습니다(Soniox일 때만). interim/final/endpoint/transport 실패가 앱 소유 경계에서 로그로 남습니다(stt_boundary.py:14-32).
시작 시 fail-closedVP는 _assert_voice_pipeline_stt_contract()실제 구성된 STT 인스턴스의 모듈 패밀리가 config의 stt_provider와 일치하는지 검사하고, 어긋나면 세션을 시작하지 않고 예외를 던집니다(agent.py:872-881 · stt_provider.py:73-80). "설정은 Soniox인데 조용히 다른 STT로 돌아가는" 상황을 막는 장치이므로, 잘못된 config는 무음이 아니라 세션 실패로 드러납니다.

04구성요소별 역할 — 누가 무엇을 책임지나

구성요소책임Half-CascadeVoice Pipeline근거
브라우저 오디오 체인마이크 캡처·게이팅·AI 음성 재생·모니터 브리지 두 모드 동일 (livekit-client-session.ts) livekit-client-session.ts
서버 노이즈 처리업링크 음성 정제 ai-coustics quail_l · enhancement 0.9 (동일) agent.py:748-846
턴 감지 엔진"아동이 말을 끝냈다" 판정 livekit_vad(기본) 또는 openai_realtime(server_vad / semantic_vad) livekit_vad 고정 agent.py:1063-1104 / agent.py:920-926
EOU 분류기전사 텍스트로 발화 종료 확률 판정 사용 불가 — "none"으로 강제 (live STT transcript가 없음) 구조적으로 가능하나 수업 경로는 "none"으로 전달 agent.py:1107-1119 / livekit-token.ts:200
STT아동 발화 → 텍스트 Realtime 내장 전사 gpt-4o-transcribe (별도 STT 인스턴스 없음) Soniox stt-rt-v5 + InstrumentedSTT 계측 agent.py:1136-1141 / stt_provider.py:83-141
LLM응답 텍스트 생성 openai.realtime.RealtimeModel(gpt-realtime), 텍스트 모달리티만 openai.LLM(gpt-5.4) — 일반 Chat 경로 agent.py:1144 / agent.py:943-953
TTS텍스트 → 음성 Typecast ssfm-v30 + 아바타 보이스·감정 옵션 (동일 _build_tts) agent.py:372-427
입력 게이트듣기 ON/OFF · AI 발화 중 입력 차단(half-duplex) 동일 HalfDuplexInputGuardAgent + DeferredInputGate agent.py:1360-1396 · input_gate.py
응답 완료 계약"AI 한 턴이 끝났다"의 근거 vendor_exact — 벤더 완료 이벤트를 그대로 신뢰 local_stt — 자체 STT final/turn id로 판정 agent.py:3470 · agent.py:1395-1396
설정 스냅샷프롬프트·모델·VAD 전달 Valkey 스냅샷 + 토큰 metadata의 configRef (동일) livekit_model_config_store.py

05장단점 비교 — 항목별로 어느 쪽이 유리한가

코드로 확정 가능한 항목만 판정했고, 측정이 필요한 항목은 추정으로 표시했습니다.

비교 항목Half-CascadeVoice Pipeline유리
외부 벤더 홉 수 2개 (Realtime → Typecast) 3개 (Soniox → OpenAI → Typecast) HC
첫 음성까지 지연 추정 전사·응답이 한 세션 안에서 이어져 왕복이 한 번 적다 STT final → LLM 호출이 직렬로 붙어 누적되나, preemptive generation으로 상쇄 가능 측정 필요
preemptive generation
발화 종료 전 응답 선생성
불가 — SDK가 RealtimeModel이면 자동 차단하므로 옵션 자체를 전달하지 않음 가능 — turn_handling.preemptive_generation(enabled·preemptive_tts·max_speech_duration·max_retries) VP
끼어들기 세부 제어 livekit_vad일 때만 세부 옵션 적용. openai_realtime 엔진에서는 discard_audio·resume_false_interruption 등이 무시됨 interruption.mode = adaptive / vad 선택 가능 (adaptive는 STT의 aligned transcript 지원 필요) VP
턴 감지 방식의 폭 의미 기반(semantic_vad)까지 선택 가능 — 침묵 길이 외 신호를 쓸 수 있는 유일한 경로 Silero VAD + endpointing 지연으로만 판정 HC
모델 교체 자유도 Realtime 모델군에 묶임. 전사 모델도 Realtime이 제공하는 것 중에서만 STT·LLM·TTS를 독립 교체 (지원 STT: google·openai·soniox·inference) VP
단계별 관측성 벤더 이벤트에 의존. STT 내부 경계는 앱이 볼 수 없다 InstrumentedSTT로 interim/final/endpoint/transport 실패를 앱 경계에서 기록 + provider 계약 검증 VP
완료 판정의 견고함 벤더 완료 이벤트가 오지 않으면 턴이 미해결로 남는다 — 자동전환 누락 사례의 배경 자체 STT final을 근거로 판정하므로 벤더 이벤트에 덜 묶이지만, STT 누락·중복이 그대로 판정에 반영된다 실패 모드가 다름
전사 품질 영향 범위 전사가 틀려도 LLM은 원본 오디오 맥락을 함께 본다 추정 STT 텍스트가 LLM 입력의 전부 — 오인식이 곧 응답 오류 HC
장애 격리 벤더 한 곳이 죽으면 전사와 응답이 함께 죽는다 단계별로 죽고, 어느 단계인지 로그로 분리된다 (단 실패 확률 지점이 3개) 성격 차이
운영 복잡도 키·계약 2개 키·계약 3개(Soniox 추가) + 계약 검증 실패 시 세션 시작 불가 HC
요약HC는 단순함·왕복 수·의미 기반 턴 감지가 강점, VP는 선생성·끼어들기 제어·단계별 관측·모델 교체가 강점입니다. "어느 쪽이 빠른가"는 코드만으로 결론 낼 수 없고, VP는 preemptive generation을 켰을 때 비교해야 공정합니다.

06읽는 사람이 자주 걸리는 함정

1. HC에서 끼어들기 OFF + server VAD는 공존 불가allow_interruptions=false인데 엔진이 openai_realtime이면 워커가 엔진을 livekit_vad로 강제 폴백합니다(agent.py:1065-1073). UI가 정방향으로 막고 있어 정상 경로에서는 안 걸리지만, 구버전 클라이언트·수동 payload에서는 설정한 엔진과 실제 엔진이 다를 수 있습니다. 런타임 판단은 _effective_turn_detection_engine 기준입니다.
2. VP config의 turn_detection_model: "none"은 의도된 값수업 경로는 EOU 분류기를 끄고 순수 VAD+endpointing으로 갑니다(livekit-token.ts:200). "multilingual을 켜면 더 좋아지지 않나"는 별개의 튜닝 논의이고, 지금 코드 기준으로는 두 모드 모두 EOU 분류기를 쓰지 않습니다.
3. HC config의 stt_provider: "openai"는 STT 인스턴스를 만들지 않는다웹은 HC에도 stt_provider·stt_use_realtime을 실어 보내지만, _build_half_cascade_session()STT를 구성하지 않습니다(전사는 RealtimeModel 내장). HC 로그에서 STT 경계 진단을 찾으면 없습니다.
4. 모드가 바뀌면 완료 이벤트 해석이 바뀐다vendor_exactlocal_stt는 자동전환·응답 종료 판정의 근거가 다르다는 뜻입니다. 한 모드에서 재현된 종료·자동전환 이슈를 다른 모드 로그로 반증하려면, 어떤 완료 근거를 쓰는 경로인지 먼저 확인해야 합니다.
5. 워커의 다이얼 공간이 수업 경로보다 넓다agent.py는 google chirp·inference STT, google/openai/inference TTS, dynamic endpointing, semantic eagerness 등을 지원하지만 수업 경로(buildLiveKitModelConfig)가 보내는 값은 그 부분집합입니다. 워커 코드에 있는 분기를 보고 "수업에서 그 조합이 쓰인다"고 읽으면 안 됩니다.

관련 문서