마지막 업데이트 2026-08-18
livekit이면 Half-Cascade, livekit_vp면 Voice Pipeline. 기본값은 livekit(HC)입니다.configRef → LiveKit RoomAgentDispatch → ppi-agent 워커까지는 두 모드가 완전히 동일하고,
갈라지는 지점은 워커 안 단 한 줄(agent.py:3461)입니다.modalities=["text"]).
VP는 STT(Soniox) · LLM(GPT-5.4) · TTS(Typecast)를 각각 붙입니다.local_stt).
음성 런타임은 4갈래이고, 그중 LiveKit 두 갈래만 ppi-agent를 씁니다. 판정은 전부 resolver.ts 한 함수 안에서 끝납니다.
livekit (conversation-mode.ts:3)half_cascadevoice_pipeline| 모드 값 | UI 라벨 | 런타임 | 담당 구성 | 근거 |
|---|---|---|---|---|
livekit | LiveKit HC | livekit | OpenAI Realtime(STT+LLM) + Typecast TTS | livekit-token.ts:204 |
livekit_vp | LiveKit VP | livekit | Soniox STT + GPT-5.4 + Typecast TTS | livekit-token.ts:192 |
tts | TTS 분리 | tts | OpenAI Realtime 텍스트 + 브라우저 Typecast 재생 | resolver.ts:142-149 |
realtime | Realtime | realtime | OpenAI Realtime 음성 직결 | resolver.ts:112-116 |
avatar.ttsVoice)가 있을 때만 성립합니다.
없으면 voiceSession.livekit이 아예 undefined가 되고 런타임이 realtime으로 내려갑니다
(fallbackReason = avatar_livekit_tts_voice_missing · resolver.ts:119-121, 150-157).
즉 "VP로 설정했는데 OpenAI 음성이 나온다"면 코드 버그가 아니라 아바타 보이스 미설정일 수 있습니다.
livekit_vp와 pipelineMode는 이 브랜치에서 추가됐습니다.
develop의 ConversationMode는 livekit | tts | realtime 3종이고 수업 경로는 항상 half-cascade였습니다.
워커(agent.py)는 develop에서도 두 모드 빌더를 모두 갖고 있었고, 이번 변경은 수업 경로에서 VP를 고를 수 있게 한 것 + 완료 계약 분기입니다.
모드 차이는 워커가 세션을 조립하는 순간에만 나타납니다. 토큰 발급·설정 전달·dispatch·미디어 경로는 공유 자산입니다.
/api/livekit/callRoomAgentDispatch(agentName=ppi-agent) · TTL 2400s_fetch_model_config()가 configRef로 스냅샷 fetchturn_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).
아동의 발화 한 턴이 AI 음성 한 턴으로 바뀌는 경로입니다. 빨간 칸이 외부 벤더 홉이고, HC는 벤더 홉 2개 · VP는 3개입니다.
pipeline_mode: "half_cascade" · 기본 모드gpt-4o-transcribe +modalities=["text"])ssfm-v30 · 아바타 보이스pipeline_mode: "voice_pipeline" · livekit_vpstt-rt-v5 · kogpt-5.4 (Chat)ssfm-v30 · 동일InstrumentedSTT가 붙습니다(Soniox일 때만). interim/final/endpoint/transport 실패가 앱 소유 경계에서 로그로 남습니다(stt_boundary.py:14-32)._assert_voice_pipeline_stt_contract()로 실제 구성된 STT 인스턴스의 모듈 패밀리가
config의 stt_provider와 일치하는지 검사하고, 어긋나면 세션을 시작하지 않고 예외를 던집니다(agent.py:872-881 · stt_provider.py:73-80).
"설정은 Soniox인데 조용히 다른 STT로 돌아가는" 상황을 막는 장치이므로, 잘못된 config는 무음이 아니라 세션 실패로 드러납니다.
| 구성요소 | 책임 | Half-Cascade | Voice 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 | |
코드로 확정 가능한 항목만 판정했고, 측정이 필요한 항목은 추정으로 표시했습니다.
| 비교 항목 | Half-Cascade | Voice 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 |
allow_interruptions=false인데 엔진이 openai_realtime이면
워커가 엔진을 livekit_vad로 강제 폴백합니다(agent.py:1065-1073). UI가 정방향으로 막고 있어 정상 경로에서는 안 걸리지만,
구버전 클라이언트·수동 payload에서는 설정한 엔진과 실제 엔진이 다를 수 있습니다. 런타임 판단은 _effective_turn_detection_engine 기준입니다.
turn_detection_model: "none"은 의도된 값수업 경로는 EOU 분류기를 끄고 순수 VAD+endpointing으로 갑니다(livekit-token.ts:200).
"multilingual을 켜면 더 좋아지지 않나"는 별개의 튜닝 논의이고, 지금 코드 기준으로는 두 모드 모두 EOU 분류기를 쓰지 않습니다.
stt_provider: "openai"는 STT 인스턴스를 만들지 않는다웹은 HC에도 stt_provider·stt_use_realtime을 실어 보내지만,
_build_half_cascade_session()은 STT를 구성하지 않습니다(전사는 RealtimeModel 내장). HC 로그에서 STT 경계 진단을 찾으면 없습니다.
vendor_exact ↔ local_stt는 자동전환·응답 종료 판정의 근거가 다르다는 뜻입니다.
한 모드에서 재현된 종료·자동전환 이슈를 다른 모드 로그로 반증하려면, 어떤 완료 근거를 쓰는 경로인지 먼저 확인해야 합니다.
agent.py는 google chirp·inference STT, google/openai/inference TTS, dynamic endpointing,
semantic eagerness 등을 지원하지만 수업 경로(buildLiveKitModelConfig)가 보내는 값은 그 부분집합입니다.
워커 코드에 있는 분기를 보고 "수업에서 그 조합이 쓰인다"고 읽으면 안 됩니다.