마지막 업데이트 2026-07-24
POST /api/livekit/call로 토큰을 발급받으면, 웹은 AI 워커를 직접 호출하지 않고 토큰 안의
roomConfig.agents(RoomAgentDispatch)로 LiveKit 서버가 ppi-agent 워커를 소환합니다.
큰 프롬프트/설정은 토큰에 싣지 않고 Valkey에 저장 후 configRef(참조 키)만 넘깁니다.
미디어는 LiveKit Cloud SFU를 통해 오가고(mic↑ / TTS↓), 브라우저·워커는 별도 RPC·Data 채널로 턴·VAD·끼어들기를 제어합니다.
PPI에는 SFU가 둘(LiveKit Cloud = AI 음성, mediasoup = 수업 중계)이며, 아동 브라우저가 둘을 잇는 브리지입니다.
세션을 만들기 전 resolveVoiceSessionConfig()가 아동의 대화 모드와 아바타 보이스를 조합해 런타임을 정합니다.
LiveKit은 아바타에 Typecast TTS 보이스가 있을 때만 성립하고, 없으면 Realtime으로 폴백합니다.
resolveLiveKitVad().
call route는 runtime !== "livekit"이면 409 LIVEKIT_NOT_AVAILABLE 반환.
웹은 AI 워커를 직접 호출하지 않습니다. 발급하는 JWT 안의 roomConfig.agents(RoomAgentDispatch)에
ppi-agent를 지정해두면, 아동이 룸에 입장하는 순간 LiveKit 서버가 이름으로 워커를 소환합니다.
큰 프롬프트/설정은 토큰에 싣지 않고 Valkey에 저장한 뒤 configRef만 전달합니다.
agent_name=ppi-agent 워커를 named dispatch_publish_ppi_agent_ready() → 브라우저 RoomEvent.DataReceived 수신 →
agentReadiness.markReady(). 10초 내 미수신 시 readiness timeout.
agent-name은 웹/워커 양쪽 기본값 ppi-agent로 일치해야 매칭됩니다.
아동 마이크는 브라우저 내 WebAudio 체인(게이트 → EQ → 컴프레서 → 리미터)을 거친 뒤 두 갈래로 나뉩니다.
하나는 SFU를 통해 AI 워커로 가는 processedTrack, 다른 하나는 진행자 모니터로 가는 relayProcessedTrack입니다.
agentInputGain만 0으로 낮춰
AI로 가는 입력만 차단하고 진행자 릴레이는 유지합니다. 다른 모드는 트랙 mute라 릴레이까지 함께 죽습니다.
shouldAttachRealtimeRemoteAudio()가 false —
Realtime PeerConnection의 remote 오디오를 붙이지 않고, AI 음성은 오직 LiveKit TrackSubscribed 경로로만 재생됩니다.
livekit-audio-chain-processor.ts:196–209
연결이 서면 브라우저와 워커는 미디어와 별개로 RPC · Data 채널로 대화합니다.
브라우저는 입력/VAD/끼어들기를 RPC로 제어하고, 워커는 전사·턴 상태를 agent-event-log 토픽으로 흘려보내
브라우저가 VoiceSessionEvent로 정규화해 자막·입력 게이트를 구동합니다.
| 방향 | 채널 / 메서드 | 용도 |
|---|---|---|
| 브라우저 → 워커 | RPC agent.setInputEnabled | AI로 가는 입력 on/off (워치독 강제 종료 포함) |
| 브라우저 → 워커 | RPC agent.setVadOptions | VAD 임계 · 엔드포인팅 · 프리픽스 패딩 실시간 변경 |
| 브라우저 → 워커 | RPC agent.setAllowInterruptions | 아동 발화의 AI 끼어들기 허용 토글 |
| 브라우저 → 워커 | RPC agent.interrupt | 진행자의 AI 응답 수동 중단 |
| 브라우저 → 워커 | data · topic ppi-user-text | 텍스트 메시지 주입 (응답 지시 포함 가능) |
| 워커 → 브라우저 | data · topic agent-event-log | user_stt_segment · agent_transcript_delta/done · user_state_changed · user_turn_committed/dropped → VoiceSessionEvent |
| 워커 → 브라우저 | ActiveSpeakersChanged | AI 발화 시작/종료 감지 → onAiTalkingChange |
isTrustedLiveKitAgentData() = 참가자 kind가 AGENT이고 topic이 agent-event-log일 때만 신뢰.
원격 참가자(워커)가 아직 없으면 RPC는 pending 큐에 쌓였다가 ParticipantConnected 시 재전송됩니다.
PPI에는 서로 독립된 SFU가 둘 있습니다. AI 음성은 LiveKit Cloud SFU(관리형), 수업 미디어(아동 영상·화면공유·소리 중계)는
자체 mediasoup SFU(apps/socket)가 담당합니다. 두 SFU는 직접 통신하지 않고,
아동 브라우저가 양쪽에 동시에 참여하며 다리를 놓습니다.
aiAudioStream
mic-audio(아동 릴레이) · ai-audio(핑퐁이) · camera-video
↓ 진행자 화면공유 → 아동
onRemoteAudioStream → setAiAudioStream, 오디오 체인의 relayProcessedTrack → guestMicStream.
이 둘이 mediasoup producer ai-audio·mic-audio로 재발행됩니다 (use-mediasoup-producer.ts:634,743).
두 SFU는 서로를 알지 못하며 브라우저만 양쪽에 물려 있습니다.
ai-audio producer outbound stalled 계열 증상이 발생하는 지점입니다
(LiveKit→브라우저 수신은 정상인데 브라우저→mediasoup 재발행 트랙이 무음).