AI 음성 밀림 복구 절차 설계
서버 발화 중단 · 재attach · 재구독 설계 제안 · 미구현

마지막 업데이트 2026-09-11

외부 리뷰가 제안한 3부 구성(OFF 노드 우회 / 경량 모드 / 복구 절차) 중 이미 밀린 소리를 끊는 복구 절차만이 원인층과 무관하게 효과를 낼 수 있는 층이라고 판단했다. 이 문서는 그 판단 근거, 5단계 복구 절차의 단계별 목적·기존 코드·신규 필요분·리스크, 그리고 “언제 돌리나”의 판단 기준이 아직 확정될 수 없는 이유와 확정 절차를 정리한다. LiveKit·VP 두 모드는 브라우저 재생 경로가 같아 판단이 동일하다.

작성일: 2026-09-11 브랜치 기준: PPI-1290 64d0f7cd 상태: 설계 제안 · 코드 미반영 · 실기기 미검증

한 줄 결론

복구 절차(③)를 진행자 수동 버튼으로 먼저 만들어 실기기에서 효과를 검증하고, OFF 노드 우회(①)는 저렴한 상시 절감으로 붙이되, 경량 모드(②)는 dry-run 로그로 상관이 확인되기 전까지 투자하지 않는다. 자동 트리거는 현재 감시가 출력단을 못 보므로 판단 기준을 로그로 확정한 뒤에만 붙인다.

주장 등급: A 코드로 확인 / B 코드 구조 근거 추론 / C 실기기로 확인해야 할 가설.

1. 3부 제안 개선 가능성 판단

제안개선 가능성근거
① OFF 노드 실제 우회 (마이크 입력 체인)낮~중A LiveKit 프로필에서 OFF여도 EQ 3·컴프레서·리미터가 모두 연결된다(livekit-audio-chain-processor.ts:339~346, 리미터는 ratio=1로만 무력화). DynamicsCompressor 2개는 파라미터가 중립이어도 envelope 연산을 하므로 렌더 절감은 실제(B). 그러나 하네스가 150% 부하를 걸어야 재현된 밀림에 비해 절감 폭이 작아 이것만으로 밀림이 사라진다는 근거는 없다(C). 리스크 낮고 저렴 — “하면 좋은 것”. outputGain → agentInputGain / relayDestination 분기는 유지해야 AI 듣기 OFF·진행자 청취·녹음 계약이 깨지지 않는다.
② 경량 모드 (진단 프로브·아바타 RMS 해제)낮음A 진단 프로브는 호출마다 ctx+analyser를 새로 만들지만(audio-level-observer.ts:29~32) 원격 프로브는 AI 발화 시작 후 1.5초 창에서만 산다(livekit-client-session.ts:2278). 아바타 analyser는 렌더 스레드에선 링버퍼 유지만 하고 FFT는 메인스레드에서 계산한다. 제거해도 렌더 스레드 여유가 거의 늘지 않는다(B). 진입 판정도 문제 — 현재 감시는 출력단 밀림을 못 보고, 진행자는 아동 단말의 로컬 재생 지연을 모니터로 들을 수 없다(모니터는 별도 릴레이 스트림). 마이크 입력 체인 상세는 최소 변경 완화 설계.
③ 복구 절차 (서버 중단 → 재attach/재구독 → 정책 재적용)중~높음원인층과 무관하게 누적 지연을 끊는 유일한 층. A 수동 중단 RPC는 이미 있다(livekit-client-session.ts:2395agent.py _handle_manual_interrupt). 재구독(setSubscribed)은 코드베이스에 사용처가 없어 신규지만 수신기를 재생성하므로 재attach가 못 비우는 NetEQ 층까지 리셋된다(B). iPad 출력단 하드웨어 버퍼까지 비워지는지는 실기기 확인 필요(C). 비용은 AI 발화 1회 손실 + 진행자 재개.

2. 복구 절차 5단계

[밀림 확정] ↓ ① 보류 자동 응답·자동 전환 잠시 OFF (복구 중 스텝이 넘어가거나 AI가 새 발화를 시작하지 않게) ↓ ② 턴 보존 아동 발화 중이면 대기 또는 현재 user turn 보존 (복구가 아동 말을 지우지 않게) ↓ ③ 서버 수동 중단 RPC → agent session.interrupt(force) → 완료 이벤트 확인 (새 오디오 유입 차단) ↓ ④ 클라 <audio> 재attach → 효과 없으면 트랙 재구독 setSubscribed(false→true) (누적 버퍼 폐기) ↓ ⑤ 재적용 AI 듣기·진행자 음소거 정책 재적용 후 진행자가 재개
단계목적기존 코드신규 필요분빠뜨리면
① 보류복구 중 부작용 차단autoResponseEnabled(use-guest-page-session.ts:376)·자동 전환 토글복구 시작 시 끄고 종료 시 원복하는 래핑③의 중단이 “AI 발화 완료”로 해석돼 종료 멘트 스텝이 자동 전환되거나, 침묵 필러가 새 발화를 시작해 방금 비운 버퍼를 다시 채운다
② 턴 보존아동 발화 유실 방지서버 핸들러가 turn_guard.manual_interrupt_preserved_turn_id가 있으면 그 턴을 지우지 않고 stale epoch만 정리(agent.py _handle_manual_interrupt)aiSession.isUserTalking이면 복구를 미루거나 보존 경로로 호출하는 시점 판단중단 RPC가 진행 중인 아동 턴을 함께 지운다
③ 서버 발화 중단새 오디오 유입 차단sendManualInterruptRpc(livekit-client-session.ts:2395) → 서버 manual_interrupt_requested 이벤트 발행 → 미해결 턴 정리 → session.interrupt(force=True)RPC 반환 또는 agent 발화 stop 이벤트로 “완료” 확인하는 대기④만 하면 서버가 같은 발화의 나머지를 계속 밀어 넣어 버퍼가 다시 쌓인다
④ 출력 복구누적 버퍼 폐기재attach = 현재 LIVEKIT_PLAYOUT 리셋(detach→attach→게이트 재적용, livekit-client-session.ts:3153)재구독 RemoteTrackPublication.setSubscribed(false→true). 재구독 후 TrackSubscribed가 다시 오므로 지금의 attach·감시 시작 경로가 재실행된다재attach는 출력 싱크 층만, 재구독은 NetEQ 층까지. 둘 다 못 비우는 것은 iPad CoreAudio 출력단 하드웨어 버퍼(C)
⑤ 정책 재적용·재개원래 계약 복원applyLiveKitRemoteAudioPlaybackGate(muted·play), applyGuestVoiceControlPolicyAtomically(use-guest-page-session.ts:1146, AI 듣기·진행자 음소거)①에서 끈 토글 복원, 진행자 재개 신호재구독 뒤 muted 상태나 듣기 의도가 어긋난다. 재구독은 원격 트랙만 건드리므로 마이크 송신·릴레이·녹음 트랙은 영향이 없어야 하지만 실기기 확인 항목(C)

리스크 요약. AI 발화 1회 손실(의도된 비용) / 재구독 중 무음 수백 ms / ①을 빠뜨리면 스텝 중복 진행 / ②를 빠뜨리면 아동 발화 유실 / CoreAudio 출력단 잔여 버퍼는 이 절차로 못 비울 수 있음(C).

3. 판단 기준 — “언제 돌리나”는 아직 확정할 수 없다

현재 코드에 있는 판단 기준은 하나뿐이고, 그것도 출력단을 못 본다.

기준 후보무엇을 보나현재 상태신뢰도
LIVEKIT_PLAYOUT 판정식driftMs(엘리먼트 currentTime 결손 + NetEQ 평균 지연) ≥ 400ms가 3샘플 연속구현됨, dry-run(off)수신 지터·엘리먼트 클럭 층만 관측(coverage.speakerOutput=false). WebKit MediaStream currentTime이 재생 클럭이면 출력단 밀림에 무반응 가능 C
inputContextOutputLatencyMs 증가단말 오디오 출력 계층 I/O 버퍼 확대샘플 로그에 동봉만저전력 시 CoreAudio 버퍼 확대 가설과 대응 C. 판정에 안 씀
LOW_POWER_PROBE 의심rAF 30fps 캡구현됨, 로그만밀림 “조건”(증폭기)이지 밀림 자체가 아님. 단독 트리거 부적합
진행자·아동 신고사람 청감채널 없음가장 확실하지만 진행자는 아동 단말 로컬 지연을 모니터로 듣지 못해 아동·양육자 신고 의존

확정 절차

  1. 밀림 신고가 있는 실기기 세션에서 신고 시각과 세 지표(renderDeficitMs·jitterBufferAvgMs·inputContextOutputLatencyMs)를 대조한다.
  2. 반응하는 지표가 있으면 그 지표·임계·지속 시간이 판정 기준이 된다. renderDeficitMs가 반응하면 지금 판정식 그대로 ?livekitPlayoutReset=on으로 켜면 되고, inputContextOutputLatencyMs만 반응하면 판정식에 그 항을 넣어야 한다.
  3. 셋 다 무반응이면 브라우저에서 잡을 지표가 없다는 뜻이라 자동 복구는 포기하고 진행자 수동 복구 버튼만 둔다.

4. 권장 구현 순서

  1. 진행자 수동 복구 버튼 (진행자 채팅 패널 → 소켓 → 게스트가 ①~⑤ 실행). 판단 기준이 없어도 절차의 효과를 실기기에서 검증할 수 있고, “재attach로 풀리는가 / 재구독까지 필요한가 / 둘 다 안 되는가”를 가려 자동화 여부까지 함께 결정한다.
  2. 효과가 확인되면 OFF 노드 우회(①)를 저렴한 상시 절감으로 추가한다.
  3. dry-run 로그에서 가청 밀림과 상관이 확인된 지표가 있을 때만 자동 트리거를 붙이고, 경량 모드(②)도 그때 재검토한다.

검증 합격 기준 (두 모드 모두). 밀림 감소 · AI 입력 차단 유지 · 진행자 청취 및 녹음 유지 · 스텝 중복 진행 없음 · 아동 발화 유실 없음. 하나라도 깨지면 자동화하지 않는다.

관련 문서 · 코드