iPad AI 음성 밀림 1차 대응
영어 가드 LiveKit OFF · 저전력 프로브 · 재생 지연 감시 1차 커밋2차 로컬 · 실기기 미검증

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

저전력 × WebAudio 렌더 부하 재현 실험 뒤 코드에 반영한 첫 대응이다. 운영 동작이 바뀐 곳은 영어 가드를 LiveKit 런타임에서 끈 것 하나다. 나머지 둘 — 저전력 모드 관측 프로브와 AI 원격 오디오 재생 지연 감시 — 는 기본값이 관측 전용이라 세션 로그에 판별 재료만 추가한다. 재생 지연이 임계를 넘으면 오디오 엘리먼트를 재attach해 누적 버퍼를 버리는 리셋은 ?livekitPlayoutReset=on을 붙인 기기에서만 동작한다.

작성일: 2026-09-11 브랜치: PPI-1290 · 1차 4ca9c122(푸시) · 2차 워킹트리(미커밋) 런타임: LiveKit / VoicePipeline 게스트 실기기 검증: 미수행

2026-09-13 · 커밋 기준 업데이트

커밋 c7ad8cb42db27429057e6fff0c2185b8dcc68db3에는 아래 영어 가드·저전력 프로브·재생 지연 감시와 함께, OFF인 마이크 입력 처리 노드의 실제 우회 및 테스트용 Context·렌더 부하 도구가 포함됐다. 자동 재생 리셋은 기본 OFF이며 실기기 개선 효과는 이번 문서 등록에서 검증하지 않았다.

큰 그림으로 커밋 전체 보기 → 쉬운 설명

아래 기존 본문은 2026-09-11의 단계별 분석·검증 기록이다. ‘2차 로컬·미커밋’ 및 ‘운영 동작 변경은 영어 가드 하나’라는 표기는 당시 범위이며, 최종 커밋 범위는 위 쉬운 설명을 기준으로 확인한다.

한 줄 결론

현장 부하 원천이 미확정인 상태에서는 WebAudio를 줄이는 것만으로 밀림을 보장할 수 없으므로, 확실한 죽은 부하 하나를 제거하고 나머지는 “무엇이 쌓이는지”를 재는 계측 두 개로 시작했다. 계측 중 재생 지연 감시는 리셋 동작을 이미 내장하고 있어, dry-run 로그로 임계와 층을 확정한 뒤 튜너블 하나로 켤 수 있다.

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

1. 적용 범위

#변경운영 동작 변화상태근거 등급
1영어 가드를 Realtime 런타임에서만 활성있음 — LiveKit/VP 게스트에서 영어 가드용 AudioContext·ScriptProcessor·MFCC 연산이 더 이상 생성되지 않음커밋 4ca9c122A 게이트 조건 / B LiveKit에서 무의미하다는 판단
2저전력 모드 관측 프로브 LOW_POWER_PROBE없음 — 30초마다 로그 1건커밋 4ca9c122A 판별 함수 / C 30fps 캡 = 저전력이라는 대응
3AI 원격 오디오 재생 지연 감시 + 재attach 리셋 LIVEKIT_PLAYOUT기본(off) 없음 — 5초마다 로그 1건. on일 때만 리셋로컬 워킹트리A 판정·정책 / C 재attach가 밀림을 실제로 푸는지

보류한 것. ScriptProcessor→AudioWorklet 전환은 연산이 렌더 스레드로 옮겨가 실험이 지목한 병목을 더 바쁘게 만들 수 있어 제외했다(B). 저전력 감지 시 아바타 RMS·입력 체인 analyser를 줄이는 완화층(B층)은 프로브 로그로 상관이 확인된 뒤로 미뤘다. 마이크 입력 체인의 OFF 노드 우회는 최소 변경 완화 설계가 별도로 다룬다.

2. 왜 이 순서인가 — 실험에서 코드까지

[실험] 저전력 ON + WebAudio 렌더 부하 150% → 밀림 재현 / 저전력 단독·부하 단독 → 정상 (bugs 문서, 9/10) ↓ 현장에서 그 부하를 만드는 앱 그래프는 미확정 ↓ LiveKit AI 스텝 게스트에 살아 있는 WebAudio를 코드로 열거 입력 체인(LiveKit 프로필은 게이트·EQ·컴프 전부 OFF, 거의 패스스루) · 아바타 RMS analyser 1개 영어 가드(별도 ctx + ScriptProcessor + MFCC, guest-layout에서 항상 true) ← 유일하게 “LiveKit에서 의미 없는” 상시 부하 외부 STT ScriptProcessor 2개(진행자 토글 시만) · 노이즈 가드·이퀄라이저(LiveKit 게스트에서 이미 OFF) ↓ [1] 영어 가드 게이트에 isRealtimeMode 추가 — 공짜 절감, 밀림 개선은 미확정 ↓ 남은 그래프를 다 합쳐도 150%에 못 미침(B) → WebAudio 축소만으로는 보장 불가 ↓ [2] 저전력 프로브 — “저전력 구간 = 신고 구간”인지 세션 로그로 확정하는 재료 ↓ [3] 재생 지연 감시 — 원인층과 무관하게 지연 상한을 강제할 수 있는 층. 먼저 dry-run으로 어느 층(출력 싱크 vs NetEQ)이 쌓이는지 기록 [다음] dry-run 로그에서 renderDeficitMs 우세 → ?livekitPlayoutReset=on 실기기 검증 / jitterBufferAvgMs 우세 → 재attach 무효 가능, 다른 수단 검토

3. [1] 영어 가드 LiveKit OFF

파일변경
apps/web/entities/guest-page-session/model/use-guest-page-session.ts:1523effectiveEnglishGuardEnabled = !isLowPerfDevice && isRealtimeMode && (override ?? englishGuardEnabled). isRealtimeMode는 노이즈 가드가 이미 쓰던 aiSession.resolvedVoiceRuntime === "realtime" 조건이다.

4. [2] 저전력 모드 관측 프로브

파일역할
apps/web/hooks/use-low-power-mode-probe.tssummarizeFrameIntervals(:24) 순수 판별 + useLowPowerModeProbe(:63) 훅
apps/web/hooks/use-avatar-rms.ts:48peekSharedAudioContext — 생성·resume 부작용 없이 기존 공유 ctx만 조회. 프로브가 ctx를 새로 만들면 그 자체가 렌더 부하가 되기 때문
use-guest-page-session.ts:1530게스트 페이지에서 enabled: true로 상시 연결

판별

로그

ctxlevelmsg시점
LOW_POWER_PROBEwarnLow power mode suspected (rAF capped near 30fps)의심 진입 시 1회
infoLow power mode suspicion cleared해제 시 1회
infoLow power probe sample그 외 30초마다

S3 세션 로그 포함은 코드 추가 없이 성립한다 A. createLogger 출력은 logger.js의 싱크 → lib/lesson-log-sink.ts:96 registerLoggerSinklessonLogBuffer로 컨텍스트 필터 없이 들어가고(minLevel 기본 debug), 그 버퍼가 lesson-logs .gz로 업로드된다. use-low-power-mode-probe.lesson-log.test.ts가 실제 logger→싱크→링버퍼 경로로 drain 결과에 ctx: "LOW_POWER_PROBE" 라인이 남는 것을 확인한다.

5. [3] AI 원격 오디오 재생 지연 감시 + 재attach 리셋

파일역할
apps/web/lib/voice-agent/livekit-playout-drift-monitor.tsresolvePlayoutResetConfig(:23) 튜너블 · readInboundAudioReceiverStats(:77) · createPlayoutDriftTracker(:138) 판정 · createPlayoutResetPolicy(:289) 쿨다운/상한 · startPlayoutDriftMonitor(:361) 런타임
apps/web/lib/voice-agent/livekit-client-session.ts:2228 세션 수명 리셋 정책 생성, :2229 정지 함수 · :3138~ agent 참가자 오디오 attach 직후 시작, :3153 재attach 리셋 콜백 · :2842 teardown, :3063 Disconnected, :3213 TrackUnsubscribed에서 정지
apps/web/package.jsontest:livekit-vp 체인에 테스트 14건 등록

측정 (1초 주기)

지표계산어느 층인가
renderDeficitMs기준점 이후 (단조 시계 경과 − audioElement.currentTime 경과), 0 미만은 0출력 싱크 층 C. 한계: MediaStream을 재생하는 엘리먼트의 currentTime은 WebKit 구현에서 스피커 출력이 아닌 재생 클럭으로 진행할 수 있어 출력단만 밀리는 경우를 못 볼 수 있다(외부 리뷰 P2). 그래서 샘플마다 coverage{elementClock, jitterBuffer, speakerOutput:false}로 관측 범위를 명시하고, inputContextOutputLatencyMs(마이크 입력 처리용 공유 ctx의 outputLatency, AI 출력 경로 자체는 아님)를 동봉해 실기기에서 가청 지연과 대조한 뒤 판정 지표를 확정한다
jitterBufferAvgMsRemoteTrack.receiver.getStats() inbound-rtp의 ΔjitterBufferDelay / ΔjitterBufferEmittedCount × 1000NetEQ(수신 지터 버퍼) 층. livekit-client의 getReceiverStats()는 emittedCount를 노출하지 않아 raw 리포트를 직접 읽는다
concealedRatioΔconcealedSamples / ΔtotalSamplesReceived참고 지표. 패킷 손실·PLC 비율
driftMsrenderDeficitMs + (jitterBufferAvgMs ?? 0)판정 대상

판정과 조치

오탐 가드 3종 A

상황처리이유
엘리먼트 paused측정 제외 + 재기준화비AI 스텝 게이트로 멈춘 구간은 currentTime이 서는 게 정상
currentTime 역행재기준화재attach로 srcObject가 바뀌면 0으로 돌아간다
ΔtotalSamplesReceived = 0유휴로 보고 제외 + 재기준화AI 침묵·패킷 중단 구간에서 currentTime이 멈추는 것을 결손으로 세면 on 모드에서 불필요한 리셋이 나고 쿨다운·상한을 소진한다(리뷰에서 P1로 잡아 추가)

로그

ctxlevelmsg시점
LIVEKIT_PLAYOUTinfoPlayout drift monitor started (speakerOutputObservable:false 고정 명시) / stoppedattach·정지
infoPlayout drift sample (+ coverage·inputContextOutputLatencyMs·inputContextBaseLatencyMs·inputContextState)5샘플(≈5초)마다
warnPlayout drift suspected의심 진입 1회
infoPlayout drift cleared해제 1회
warnPlayout reset applied (audio element reattach)on 리셋 실행
warnPlayout reset failed리셋 예외

audioDebug(개발 환경 콘솔)에는 audio_element_reattach_requested / audio_element_reattached가 엘리먼트 스냅샷과 함께 남는다.

6. 실기기 튜너블과 판별법

?livekitPlayoutReset=on 리셋 활성 (localStorage 동일 키도 인정, URL 우선) ?livekitPlayoutDriftMs=800 의심 임계 조정, 100~5000 범위 밖은 400 유지 세션 로그(S3 lesson-logs)에서: ctx == "LOW_POWER_PROBE" && data.lowPowerSuspected == true → 저전력 구간 ctx == "LIVEKIT_PLAYOUT" && level == "warn" → 밀림 의심 / 리셋 data.renderDeficitMs ≫ data.jitterBufferAvgMs → 출력 싱크 층, 재attach 유효 가능성 data.jitterBufferAvgMs ≫ data.renderDeficitMs → NetEQ 상류, 재attach로 안 풀릴 가능성 data.jitterBufferAvgMs == null 지속 → WebKit이 jitterBufferEmittedCount 미지원, renderDeficit만으로 판정

사용자 확인 체크리스트 (미수행)

  1. 정상 전원 iPad: LOW_POWER_PROBE warn이 뜨는지. 뜨면 판별 임계(28/40ms) 재조정.
  2. 케이블 분리 + 저전력 ON: warn이 뜨고 fps≈30, outputLatencyMs가 정상 대비 커지는지.
  3. 같은 조건에서 AI 스텝 진행: LIVEKIT_PLAYOUT sample의 renderDeficitMs·jitterBufferAvgMs 중 어느 것이 쌓이는지. 밀림 청감과 suspected warn 시각이 맞는지.
  4. 3에서 renderDeficit 우세면 ?livekitPlayoutReset=on으로 재진입: reset applied 뒤 밀림이 풀리는지, 끊김이 100~200ms 수준인지, AI 침묵 구간에 불필요 리셋이 없는지.
  5. LiveKit 영어 가드 OFF로 인한 변화 없음 확인: 활동 전환·듣기 토글·에코 등 기존 동작.

7. 검증 결과와 남은 위험

항목결과
단위 테스트프로브 9건 + 세션 로그 포함 2건(vitest) · 재생 지연 감시 18건(node:test, test:livekit-vp) 통과
tsc / prettier통과. 사전 존재 에러 1건(host-disconnected.png 모듈 선언, develop 동일) 제외
기존 LiveKit 테스트livekit-client-session / livekit-audio-chain-processor / shared-input-audio-context 3파일 exit 0. pnpm test:livekit-vp 전체는 이 브랜치가 건드리지 않은 lesson-session.service.disconnect-race.test.ts가 사전 실패라 체인이 끊긴다(develop 동일)
실기기미수행

남은 위험. ① 리셋 순간 100~200ms 끊김(C). ② 밀림이 NetEQ 상류면 재attach로 안 풀릴 수 있음(C) — dry-run 로그로 먼저 층을 가른다. ②-1 renderDeficitMs가 출력단 정지를 못 볼 수 있어(WebKit MediaStream currentTime = 재생 클럭 가능성, C) 실기기에서 가청 밀림 시각과 renderDeficitMs·jitterBufferAvgMs·inputContextOutputLatencyMs 셋을 대조해 어느 지표가 반응하는지 확정해야 한다. 셋 다 무반응이면 판정 지표 자체를 바꿔야 한다. ③ 네이티브 30Hz 디스플레이는 저전력 프로브 오탐 라벨 가능(로그만). ④ 게스트당 로그량 증가: LOW_POWER_PROBE 약 120건/시간, LIVEKIT_PLAYOUT AI 스텝 중 약 720건/시간(LIVEKIT_AUDIO_CHAIN health와 같은 5초 주기). ⑤ 프로브가 아바타 RMS 훅의 모듈 상태에 의존(P2, 최소 변경 우선으로 수용).

관련 문서 · 코드