마지막 업데이트 2026-07-29
aiAudioStream의 float 신호가 이미 ±1.0을 초과한 과레벨 상태로 들어오고, STT 캡처 경로의 toInt16 하드 클램프(Math.min(1, ...))가 초과분을 풀스케일에서 깎아내며 flat-top 클리핑(지지직/찌그러짐)을 만든다. WAV 실측이 이를 확정했다: peak 0.0dBFS, 클리핑 샘플 941개(1.8%), RMS -3~-5dBFS.
감지 경로(커밋 id-008)는 실측 검증 완료 — 문제 파일은 t=1.15s에 감지, 정상 파일 오탐 0. 단, 본질이 reactive라 초반 ~250ms는 이미 들린 뒤이며, 예방(재생단 리미터)과 과레벨 게인의 출처는 미해결로 남아 있다.
핑퐁이(AI)가 말할 때 "지지직" 소리가 난다는 신고. 다행히 현장 증거물이 남아 있었다 — STT 검증용으로 캡처된 AI 발화 오디오 파일. 무손실 PCM이라 파형 부검이 가능했다.
ai_stt_req19 (1).wav (무손실 PCM).
use-ai-audio-noise-guard.ts)가 쓰는 음향 지표(RMS dB · ZCR · CV · FFT)를 오프라인 재현 + 클리핑/스펙트로그램 분석.
귀로 듣기 전에 숫자부터 확인했다. 노이즈 가드가 쓰는 것과 같은 지표를 오프라인으로 재현해 파형을 뜯어보니, 모든 지표가 한 방향을 가리켰다 — 이 신호는 풀스케일에 머리를 박고 있었다.
| 항목 | 값 | 해석 |
|---|---|---|
| 포맷 | 16kHz mono PCM, 3.33s | AI STT 캡처본, 무손실 |
| peak | 0.0 dBFS | 풀스케일 도달 |
| 클리핑 샘플 | 941개 (1.8%), 0.9s~3.3s 전반 | 하드 클리핑(flat-top) |
| 큰 구간 RMS | -3 ~ -5 dBFS | 정상 음성(-18~-12)보다 훨씬 과도 |
| 스펙트로그램 | 전대역 에너지 번짐 | 클리핑 고조파(왜곡) 전형 |
| CV<0.35 기계음 후보 | 17프레임 (1.25~1.5s, 1.95~2.3s 등) | 클리핑으로 파형이 납작해진 구간과 일치 |
파형이 flat-top으로 잘려 있다는 것은 어딘가에서 신호를 하드 클램프했다는 뜻이다. 다음 질문은 명확했다 — 코드의 어느 지점에서 잘리는가.
AI STT 캡처 경로(use-guest-page-session.ts:847~854)의 Float32 → Int16 변환:
const toInt16 = (float32) => { for (...) { const s = Math.max(-1, Math.min(1, float32[i])); // ±1.0 초과를 하드 클램프 int16[i] = s < 0 ? s * 0x8000 : s * 0x7fff; } };
Math.min(1, ...) 클램프가 ±1.0을 넘는 샘플을 풀스케일로 깎아낸다. 측정된 flat-top 클리핑이 정확히 이것 → 즉 aiAudioStream의 float 신호가 이미 ±1.0을 초과한 과레벨 상태로 들어오고 있다.
캡처는 aiAudioStream에 붙인 ScriptProcessorNode(16kHz)로 떠가며, AI 발화 종료 시 requestAIAudioSTT()가 트리거(use-guest-page-session.ts:790~791).
클램프는 흉기일 뿐 진범이 아니다. 클램프는 소스 float가 1.0을 넘을 때만 발동하므로, 진짜 범인은 그 앞 어딘가에서 신호를 과레벨로 만든 무언가다. 그리고 이 캡처본의 잘림이 아동이 실제로 들은 지지직과 같은 것인지도 확인해야 했다.
여기서 한 가지를 확실히 해둬야 했다. 수사 기록(LogRocket)에는 "지지직"이라는 같은 인상착의의 다른 용의자가 이미 있었다 — 네트워크 PLC. 같은 별명, 다른 범인. 혼동하면 엉뚱한 곳을 고치게 된다.
LogRocket 로그 분석에서 본 또 다른 지지직 경로와 혼동 주의. "AI 지지직"은 두 가지 별개 경로가 있다.
| 경로 | 원인 | 로그/지표 | 양상 |
|---|---|---|---|
| ① 네트워크 PLC | guest↔OpenAI WebRTC 패킷 손실 → 디코더 보정 아티팩트 | AI_NOISE_GUARD "suppressed during PLC-active window" · concealedSampleRatio | 간헐적 |
| ② 과레벨 클리핑 (본 문서) | aiAudioStream float > ±1.0 → toInt16 하드 클램프 | WAV peak 0dBFS · 941 클리핑 샘플 · RMS -3~-5dBFS | 발화 전반 지속 |
concealedSamples/packetsLost/jitter)는 WebRTC inbound-rtp 통계로, WAV(최종 파형)에는 들어있지 않다 → 파일만으론 ①/② 구분이 어렵고, 로그 타임라인 교차검증이 필요.
원인은 확정했지만 게인의 출처(진범)는 아직 특정하지 못했다. 그래서 수사는 두 갈래로 갈라졌다 — 하나는 재발 시 즉시 검거하는 감지 장치(커밋 id-008)가 실전에서 작동하는지 검증하는 것, 다른 하나는 애초에 범행을 예방하는 재생단 레벨 제어를 검토하는 것.
커밋 id-008 (feat: AI 발화 하드 클리핑 고속 감지 경로 추가)가 실제 WAV에서 동작하는지, 정상 발화 오탐은 없는지 독립 재현·검증.
CLIP_CREST_THRESHOLD_DB=6, CLIP_SAMPLE_LEVEL=0.98, CLIP_SAMPLE_RATIO_THRESHOLD=0.01, CLIP_RING_SIZE=4, CLIP_FRAME_THRESHOLD=3, grace 500ms, silence floor -50dB)를 그대로 재현. 라이브 AnalyserNode와 동일하게 48kHz·2048샘플 윈도우·50ms 틱으로 시뮬레이션(16kHz 캡처본은 48kHz 리샘플).
| 파일 | 결과 | 비고 |
|---|---|---|
| 아동330_4회기_지지직 (문제) | 🔴 감지 t=1.15s | 첫 클립프레임 1.00s → 3프레임 누적 후 확정 |
| ai_stt_req18 (정상) | 🟢 미감지 | 클립프레임 0 — 오탐 없음 |
| ai_stt_req19 (정상) | 🟢 미감지 | 클립프레임 0 — 오탐 없음 |
onNoiseDetected(응답 중단 + 이슈 리포트) 트리거.clipRatio ≥ 1% 게이트. 정상 발화는 peak -1.4dBFS라 0.98 초과 샘플이 사실상 없어 clipRatio≈0 → 절대 클립프레임 안 됨. 견고.cancelResponse(공유 핸들러): 지지직은 멈추나 그 발화 나머지가 끊김. 호스트 이슈 라벨은 "AI 기계음 감지"로 클리핑과 구분 안 됨(로그만 Hard clipping ... (fast path) 구분).toInt16 클램프 후(peak=1.0) 파일 기준. 라이브 pre-clamp가 over-unity(>1.0)면 더 잘 감지 → 실제로는 최소 이만큼은 잡힘.검거 장치는 작동한다. 하지만 검거는 범행 뒤에 온다 — 초반 ~250ms의 지지직은 이미 아동의 귀에 도달한 뒤다. "안 들리게 막기"가 목표라면 재생 직전 단계에서 레벨을 잡아야 한다.
"안 들리게 막기"는 감지가 아니라 재생 직전 단계에서 레벨을 잡아야 한다. 현재 재생은 <audio>.srcObject = MediaStream 직결(use-ai-session.ts:1683/1706)이라 그래프가 없다.
createMediaStreamSource → Limiter → MediaStreamDestination → <audio> 경유가 유일. AudioContext.destination 직접 출력은 금지(:1681 주석상 AEC 에코 참조 보존 위해 <audio> 재생 유지 필요).과레벨이라는 범행 수법은 확정했지만, 게인이 어디서 들어오는지(OpenAI 출력 vs 프로젝트 게인)는 아직 잡지 못했다. 수배 전단은 다음과 같다.
aiAudioStream 생성~재생까지 게인 스테이지 추적 — 과레벨이 어디서 들어오는지(OpenAI 출력 vs 프로젝트 게인) 확정.