송아인 24회기 AI발화·영상오디오 “지지직”
— 아동 단말 출력 경로 오염 원인 분석 원인 미확정 · 유력가설 1건 · 수정 미적용

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

쉬운 설명 한 장 보기 · 사전지식 없이 읽는 요약

작성일: 2026-08-27사건: 2026-08-27 18:35–18:54 KSTroom: bcd09fea…_24대상: 세션로그 11,029건 · LiveKit 서버로그 · 녹음 합본 17.6분런타임: LiveKit

결론

“Typecast 기계음”은 오진이다. 이 세션은 클라이언트 TTS를 한 번도 쓰지 않았고(runtime: livekit, tts_player_skipped 9건), 아동 단말은 iPad가 아니다(비-iOS Chromium). 따라서 Typecast 디코드·WebRTC loopback·iOS AVAudioSession은 모두 이 사건의 경로가 아니다. 남는 유일한 공통 하류는 아동 단말 브라우저/OS의 공용 출력 스트림(캡처·AEC와 결합된 통신 오디오 경로)이며, 이것이 오염되면 AI 발화와 영상 오디오가 동시에 깨지고 탭 리로드로는 회복되지 않는 세 가지 제약이 한꺼번에 설명된다.

확정이 아니다. 노이즈가 시작된 정확한 시각을 특정할 로그 신호가 없고(관측 공백), 아동 단말의 UA·기종·CPU 지문이 로그에 전혀 남지 않는다. 아래 “즉시 판별 3단”이 남아 있다.

시간순 인과 다이어그램

아동 단말(비-iOS Chromium) 세션 시작
   → 마이크 캡처 EC(AEC)+NS+AGC ON 으로 시작
   → 브라우저가 저지연 "통신 출력 경로"로 전환   [실측] outputLatency 0.06 → 0.008 (18:36:13)
   → 이 경로 위에서 앱의 모든 소리가 재생됨
        ├ AI 발화 = LiveKit remote Opus track → audioElement.srcObject (브라우저 WebRTC 디코더)
        └ 영상 오디오 = <video> 직접 재생 (비-iOS는 WebAudio/loopback 미경유)
   → 활동 스텝 전환마다 LiveKit room·PeerConnection·AudioWorklet 전량 재생성 x9
     + 매번 mic 재캡처(AEC 재초기화)   [실측] "Defer mic: applied after AEC stabilization delay" x8
   → (가설) 반복되는 캡처/AEC 세션 재개설 + 영상 디코드 + mediasoup producer 2개 부하
     → 브라우저 오디오 서비스의 통신 출력 스트림(8ms 버퍼)이 언더런/리샘플 파손 상태로 진입
   → 그 경로를 공유하는 AI 발화 + 영상 오디오가 동시에 "지지직"        [제약 1 충족]
   → 탭 리로드는 렌더러만 재생성 — 오디오 서비스/HAL 상태는 그대로     [제약 3 충족]
   → 크롬 앱 완전 종료 시에만 오디오 서비스가 죽고 스트림 재초기화 → 회복

[진행자가 들은 소리는 별개 경로다]
   ① 진행자는 같은 영상을 자기 단말에서 직접 재생 중 (volume 1, muted false) [실측]
   ② 진행자 기본 출력이 "다중 출력 기기(Aggregate)" + BlackHole 2ch          [실측]
   ③ 아동 스피커 → 아동 마이크 음향 누출 → mic-audio 업링크                  (가설)
   ④ ai-audio 업링크 = 아동이 디코드한 remote track의 clone — 출력단보다 앞에서 분기
      ⇒ 출력단 오염이면 ④는 깨끗해야 한다. 이것이 결정적 판별자다.

실측 근거

1. 아동 단말은 iPad가 아니다 사실

독립 근거 4개가 모두 같은 방향을 가리킨다. 신고에는 “크롬앱 종료 후 재시작”이라 적혔지만 단말은 비-iOS Chromium이다.

관측값iOS라면 나와야 할 값근거 코드
audioSessionState: nulliOS 16.4+는 navigator.audioSession.state 지원 → 문자열apps/web/shared/lib/audio-output-probe.ts:79-82
outputLatency: 0.008 / 0.06 / 0.062WebKit은 AudioContext.outputLatency 미구현 → nullaudio-output-probe.ts:92-95
outputDeviceCount: 1, labels: ["기본값"]iOS WebKit은 enumerateDevices()에 audiooutput 미노출 → 0
카메라 라벨 "camera 1, facing front" + permissions{camera:granted}iOS는 “전면 카메라”류 이름, permissions.query('camera') 미지원

귀결: isIosFamilyDevice 게이팅(apps/web/lib/utils.ts:22-25) 때문에 이 단말에서는 영상 WebRTC loopback이 애초에 생성되지 않는다(apps/web/shared/ui/guest-layout-content.tsx:86-89meet-video.tsx:114에서 즉시 return). TTS 로컬 재생 모드도 blob이다(use-ai-session.ts:2857,2906). 세션 로그에 loopback 문자열이 0건인 것은 관측 공백이 아니라 실행되지 않았기 때문이다.

2. 이 세션은 Typecast(클라이언트 TTS) 경로가 아니다 사실

따라서 “Typecast 응답 디코드 → TtsPlayer masterGain → aiAudioStream” 형태의 인과도는 이 사건에 존재하지 않는다. 실제 경로는 agent TTS(서버) → LiveKit Opus → 아동 브라우저 WebRTC 디코드 → (분기) element 재생 / clone 업링크다.

3. 출력 경로가 캡처 상태에 따라 재구성된다 사실(상관)

18:36:12  warmup                   outputLatency 0.06
18:36:13  meet-video-first-play    outputLatency 0.008   ← 이후 18:49:39까지 계속 0.008
18:53:25  STEP_TRANSITION "Muting microphone for non-AI step"
18:53:26  meet-video-first-play    outputLatency 0.062   ← mic mute 직후 복귀
sampleRate 는 13건 전부 48000 으로 불변, ctxState 는 항상 running

즉 이 단말에서는 앱 출력 버퍼/경로가 마이크 캡처·AEC 상태에 결합되어 재구성된다. AI 발화와 영상 오디오의 공통 하류 지점이 JS 노드가 아니라 브라우저/OS의 공용 출력 스트림이라는 직접 증거다. 다만 이 변화는 세션 종료 1분 전이라 노이즈 발생 트리거로 해석해서는 안 된다.

4. 18분 세션에서 오디오 체인이 9회 전량 재생성됐다 사실

18:36:40 Audio chain graph built  #1 → 18:38:35 room disconnected
18:39:25 #2 → 18:40:04    18:41:05 #3 → 18:42:42    18:44:10 #4 → 18:45:46
18:45:47 #5 → 18:48:20    18:48:21 #6 → 18:49:15    18:49:16 #7 → 18:49:39
18:50:08 #8 → 18:52:13    18:52:14 #9
동반 로그: AI_SESSION "DataChannel closed" 9건 · "Defer mic: applied after AEC stabilization delay" 8건
LiveKit 서버(Loki service_name="ppi-livekit")에는 teardown dtls timeout warn 뿐, 오디오 손상 신호 없음

부수 발견 — LiveKit 프로파일은 gate/EQ/compressor/limiter/autoGain이 전부 false라 오디오 체인이 사실상 pass-through인데도(apps/web/hooks/audio-processing-presets.ts:125-156), 그 pass-through 워클렛까지 스텝마다 9회 재생성된다. 이득 없는 부하다.

5. 녹음 합본의 클리핑 밀도가 대조군보다 뚜렷이 높다 참고 · 단독으로는 미확정

같은 날 녹음 4건을 48kHz mono float로 디코드해 동일 지표로 비교했다(|x|≥0.999 = 클리핑 샘플, 인접 샘플 점프 >0.25 = 클릭/지지직 후보).

세션길이전체 RMS클리핑/초점프/초
송아인 (신고 건)1059.6s-20.3 dB5.325.51
대조 A1352.8s-24.6 dB1.745.49
대조 B1400.5s-25.1 dB1.180.80
대조 C1083.4s-25.5 dB0.030.30

해석 주의: 합본은 서버 amix + mp3 인코딩 이후라 AI 트랙과 마이크 트랙을 분리할 수 없다. 전체 RMS가 4~5dB 높다는 것은 “믹스가 더 컸다”는 뜻이기도 해서, 클리핑 밀도 차이가 곧 지지직의 증거는 아니다. stem 분리 청취가 필요하다.

서버측 점검 결과 — 문제 없음

“아동 단말” 결론을 내리기 전에 서버 4개 축을 전수 점검했다. 이 세션과 관련된 서버측 이상은 발견되지 않았다.

점검 대상결과
LiveKit SFU 로그
{service_name="ppi-livekit"}
이상 없음 이 세션 방 10개 전체 조회 — 방 종료 시 dtls timeout warn(정상 teardown)만 있고 오디오 손상 신호 0건
agent TTS 텔레메트리
(세션로그 릴레이 250건)
경미 50턴 전부 outcome: completed, errorClass: None. 단 tts_provider_stream_eof elapsedMs 중앙값이 전반 757ms → 후반 943ms(최대 1633ms, 18:50:53)로 완만히 증가 — 느려진 것이지 오디오가 깨졌다는 증거는 아니다
mediasoup SFU / 녹음 서버
{service_name="ppi-socket"}
이상 없음 이 방(bcd09fea…_24) 관련 level=error 0건. 같은 시간대 다른 방에서 나온 Active stem ffmpeg exited abnormally · Failed to consume · Failed to stop recording가 이 방에는 없다
서버 리소스 (18:30–19:00)이상 없음 ppi-livekit-prod CPU 평균 4.9%(최대 6.4%), ppi-socket-prod 18.1%(최대 48.4% — 저녁 녹음 러시의 평시 패턴), ppi-web-prod 13.0%

발견했으나 범인이 아닌 것 — Dropping unreadable/corrupt stem segment. 이 방에서 child stem 4개(seg11/14/17/23)가 ffprobe 실패로 믹스에서 제외됐다. 그러나 같은 날 17–22시 구간에서 다른 방들은 10~18건씩 나온다 — 이 세션은 오히려 적은 편이다. 전 세션 공통 기저 패턴이며 판별력이 없다(빈 OGG 세그먼트 분석 참고).

확인하지 못한 서버측 1건: agent가 실제로 합성한 PCM 파형의 품질. Loki service_name 라벨 목록에 ppi-livekit-agent존재하지 않아(alb, livekit-test, ppi-api, ppi-batch, ppi-livekit, ppi-scheduler, ppi-socket, ppi-stt, ppi-web, unknown_service) agent 프로세스 원본 로그를 조회할 수 없다. 텔레메트리는 타이밍·성공여부만 알려준다. 합성 파형이 깨졌는지는 녹음 stem 청취로만 확정된다 — 그래서 “즉시 판별 3단”의 1번이 stem이다.

배제된 원인

가설판정근거
Echo Cancellation(iOS는 EC/NS/AGC가 voice-processing 한 덩어리)이 꺼져서 발생기각
단, 절반은 유효
iOS에서 echoCancellation이 voice-processing I/O 유닛 전체 스위치라는 전제는 맞다(이 레포 주석에도 명시: shared/ui/guest-layout-content.tsx:84, shared/lib/video-audio-loopback.ts:105). 그러나 ⑴ 이 단말은 iOS가 아니고(§1), ⑵ 런타임에 EC를 끄는 코드 경로가 없다 — 전 경로 echoCancellation: true 고정(hooks/audio-processing-presets.ts:69,128, lib/voice-agent/livekit-recognition-config.ts:71), applyConstraints는 화면공유 비디오에만 사용(hooks/use-screen-share.ts:110), echoCancellation: false는 테스트 픽스처와 프롬프트테스트 게이트 UI에만 존재. EC OFF의 전형 증상은 에코·하울링이지 지지직도 아니다.

다만 직관의 핵심은 살아 있다. Android도 echoCancellation: true면 캡처가 VOICE_COMMUNICATION + AEC/NS/AGC로 열리고 출력이 저지연 통신 경로에 결합된다. 실측 outputLatency 0.008(통신) ↔ 0.062(미디어)가 그 결합의 흔적이다. 즉 범인 후보는 “꺼짐”이 아니라 스텝마다 9회 반복되는 “껐다 켜기에 준하는 재개설”이다(Defer mic: applied after AEC stabilization delay 8건, use-ai-session.ts:4122, AEC_STABILIZATION_DELAY_MS=300 :169).
Typecast 클라이언트 TTS 디코드/버퍼 이어붙임 불연속기각해당 경로 자체가 이 세션에 없다(tts_player_skipped 9건).
WebRTC loopback의 256kbps CBR stereo Opus 과부하기각비-iOS 게이팅으로 loopback 미실행(lib/utils.ts:22-25, guest-layout-content.tsx:86-89).
iOS AVAudioSession / WKWebView 미디어 프로세스기각단말이 iOS가 아니다(위 §1).
공유 AudioContext 샘플레이트 오염 (48k↔24k mismatch)기각sampleRate 13건 전부 48000, currentTimeAdvanced: true. 게다가 비-iOS에서는 영상 오디오가 공유 ctx를 타지 않으므로 “영상까지 지지직”을 설명할 수 없다.
autoGainMultiplier 래치 증폭기각LiveKit 프로파일 autoGainEnabled: false, 20ms마다 재계산(lib/voice-agent/livekit-audio-chain-processor.ts:321-378, lib/audio-processing/output-gain-policy.ts:19-36).
“원격에서도 들림 → 업링크 신호가 이미 오염”전제 오류진행자는 같은 영상을 자기 단말에서 직접 재생한다(monitor MEET_VIDEO "Video audio health" volume:1, muted:false 6건). 진행자가 들은 영상 소리는 아동에서 온 소리가 아니다. 코드상 영상 오디오는 모니터로 업링크되지 않는다.
JS 미디어 런타임 재생성으로 복구 가능기각페이지 리로드가 그 상위집합인데 회복되지 않았고, 세션 중 room/PC/워클렛/producer를 이미 9회 전량 재생성했는데도 회복되지 않았다. 사실상 사전 실험이 9회 실패한 셈이다.
기존 문서의 “과레벨 클리핑”(STT 캡처 WAV)부적합AI발화 지지직 — STT 캡처 WAV 클리핑 분석의 경로는 toInt16 하드클램프가 걸리는 STT 캡처이지 재생 경로가 아니다. 다만 상위 원인인 “AI 오디오 레벨 과대”는 공유될 수 있다(ai-audio producer audioLevel 최대 0.468 관측).

추정 원인

순위가설확정도3제약 충족
1아동 단말 브라우저/OS 공용 출력 스트림(통신 경로) 오염 — 반복되는 캡처/AEC 세션 재개설과 미디어 부하로 저지연 출력 스트림이 언더런·리샘플 파손 상태에 빠짐유력 가설1 ✅ 2 ✅ 3 ✅
2진행자측 자체 잡음 — 기본 출력이 다중 출력 기기(Aggregate) + BlackHole 2ch 구성. Aggregate 장치는 클럭 드리프트로 주기적 클릭·지지직을 만드는 대표 구성이며, monitor 쪽에서만 Unexpected pause 6건 + SEEK_REBUFFER 6건 발생가설 · 부분 설명아동 청취를 설명 못 함
3서버 agent TTS 합성 품질 — 텔레메트리 50턴 전부 outcome: completed, errorClass: null이나 tts_provider_stream_eof elapsedMs 중앙값이 전반 757ms → 후반 943ms(최대 1633ms)로 완만히 증가미확정영상 오디오를 설명 못 함

오염 트리거는 여전히 미확정이다. “무엇이 언제 출력 경로를 망가뜨렸는가”를 특정할 로그 신호가 없다. 노이즈 시작 시각조차 로그로 확정되지 않는다.

영향 범위

범위
발생 조건비-iOS Chromium(Android/ChromeOS) 아동 단말 + 활동 스텝 전환이 잦은 커리큘럼(18분에 9회) + 영상 스텝과 AI 스텝 혼재
노출면아동 로컬 청취 전체(AI 발화·영상 오디오·효과음). 아동이 세션을 이어갈 수 없을 정도면 수업 중단
회복페이지 리로드·재접속 무효. 브라우저 앱 완전 종료 후 재시작만 유효 — 수업 중 대응 비용이 크다
모니터 영향ai-audio 업링크 오염 여부는 미확정. 영상 오디오는 구조적으로 업링크되지 않으므로 진행자가 들은 영상 지지직은 ①②③④ 중 미분리 상태
재현성같은 진행자의 같은 날 다른 세션에서는 신고 없음 — 단발성. 단, 관측 공백 때문에 “다른 세션엔 없었다”가 아니라 “신고되지 않았다”가 정확하다

추천 해결책

즉시 판별 3단 코드 변경 0

  1. 녹음 stem 청취ai-audio stem이 깨끗한데 아동만 노이즈를 들었다면 아동 단말 출력단 오염 확정. stem에도 노이즈가 있으면 출력단보다 앞(agent 합성·전송·디코드) 확정. 단일 검사로 가설 공간이 반으로 갈린다. (합본 mp3만으로는 amix 이후라 불가)
  2. 진행자 기본 출력을 내장 스피커로 바꾸고 재확인 — Aggregate/BlackHole 경로를 즉시 배제 또는 확정.
  3. 재발 시 아동에게 물을 것 — “AI 소리만 / 영상 소리만 / 둘 다”, 그리고 “브라우저 밖 다른 앱 소리도 지지직인가”. 브라우저 프로세스 범위인지 기기 전체 범위인지를 가르는 가장 값싼 결정적 실험이다.

앱측 조치 (우선순위 순)

#조치장점사이드 이펙트 · 한계
1관측 공백 제거 — 단말 지문(UA·platform·hardwareConcurrency·deviceMemory) 1회 로깅 + AUDIO_PROBE를 세션 중 주기 실행(5~10초)이번처럼 기기 판정에 4단 추론이 필요 없어진다. 노이즈 시작 시각의 outputLatency/sampleRate를 잡을 수 있다저사양 단말 CPU가 용의자인데 계측이 부하를 더한다 → 상시 20ms 폴링 금지, 이벤트 기반 스냅샷으로 제한
2출력단 vs 업스트림 판별 지표 추가 — 아동측 remote AI track의 RMS/스펙트럼(analyser, 재생 비침습) + 동시각 모니터 ai-audio 인바운드 + inbound concealedSamples/insertedSamplesForDecelerationNetEQ 은닉량은 지지직의 디코더·지터 기원을 직접 잡는 유일한 수치다. 현재 미수집현행 outbound stats는 producer 생성 5초 뒤 1회뿐이라 노이즈 구간을 담지 못한다 — 이번 세션의 audioLevel:0 샘플 14건이 그래서 무의미했다
3스텝 전환마다의 room/PC/워클렛 전량 재생성 축소 — LiveKit room 재사용 또는 캡처 스트림 유지오염 트리거 후보를 구조적으로 줄이는 유일한 앱측 레버. pass-through 워클렛 9회 재생성이라는 무의미한 부하도 함께 제거자동전환·EC 안정화(AEC_STABILIZATION_DELAY_MS)·활동 instruction 갱신과 얽혀 있어 별도 설계 필요. 범위가 크다
4미디어 런타임 강제 재생성 복구권고하지 않음. 세션 중 9회 자연 재생성이 이미 실패했고 리로드도 무효였다. 오히려 재생성 자체가 오염 원인 후보라 증상을 악화시킬 수 있다

회귀 위험 (사이드 이펙트)

조치위험
계측 추가(#1·#2)저사양 아동 단말의 CPU 여유를 더 깎아 증상을 유발할 수 있다. 주기·해상도를 보수적으로 잡고, 저사양 판정 시 자동 비활성화가 필요하다
room 재사용(#3)스텝별 instruction 갱신·자동전환 판정·EC 재안정화 타이밍이 모두 room 라이프사이클에 묶여 있다. 잘못 건드리면 종료멘트 자동전환 실패 계열 사이드 이펙트가 난다
loopback 관련 변경손대지 말 것. 이 사건과 무관하며(비-iOS), iPad 덕킹 회피는 여러 접근이 실기기에서 실패한 끝에 WebRTC loopback만 검증된 상태다. 되돌리면 iPad 덕킹이 재발한다
AI 오디오 레벨 하향ai-audio audioLevel 최대 0.468은 높은 편이나, 레벨을 낮추면 진행자 모니터 청취와 STT 인식률에 동시에 영향이 간다. 원인 확정 전 선제 조정은 권하지 않는다

미확인 목록

항목상태
아동 단말이 Android 태블릿인가 ChromeOS인가미확정 — UA/platform이 로그에 없다
노이즈가 시작된 정확한 시각미확정 — 특정할 로그 신호 없음
아동 ai-audio/mic-audio 업링크가 실제로 노이즈를 실었는가미확정 — stats가 producer 생성 직후 1회뿐
agent 서버 TTS 합성 오디오의 파형 품질미확정 — 에러 마커 0건이나 파형 미확인 (Grafana service=ppi-livekit-agent)
진행자 Aggregate/BlackHole 출력이 자체 잡음원인가미확정 — 우선 검증 권고
NetEQ 은닉량(concealedSamples)미수집 지표

조사 방법

아동 세션 로그 11,029건(S3 prod/lesson-logs/bcd09fea…/lesson-24/archives/)을 KST 시각·ctx·role로 정렬해 복원했고, LiveKit 서버 로그는 Loki service_name="ppi-livekit"에서 userId로 방 10개를 식별해 교차 조회했다. 녹음 합본 4건(신고 1 + 대조 3)은 48kHz mono float로 디코드해 클리핑·인접샘플 점프 밀도를 동일 지표로 비교했다. 원인 분석은 developer(Codex)·reviewer(Claude Code) 2단계 교차 검토를 거쳤으며, reviewer가 developer의 전제 2개(단말이 iOS다 / Typecast 경로다)를 코드로 뒤집었다. 이 문서는 뒤집힌 쪽을 반영한 결과다.

신고 문구와 실제의 괴리: 제목의 “Typecast 기계음”과 “iPad”는 둘 다 사실과 달랐다. 증상 신고의 기술 용어를 전제로 삼지 말고 로그로 단말·런타임부터 확정할 것.

수정 미적용: 이 문서는 원인 분석과 방안 정리까지다. 코드 변경은 없다.