
AI가 헛것을 듣는 날 ― 음성 AI의 오검출 대책과, 받아쓰기 붕괴의 범인을 로그로 추적하기
요약
실시간 음성 통화 앱 운영 중 발생한 VAD 오검출, WebSocket 전환 시 음성 인식 오류, 컨테이너 환경의 프로세스 점유 문제를 해결한 과정을 다룹니다. 로그 추적을 통해 하드웨어 특성과 샘플 레이트 설정의 중요성을 규명했습니다.
핵심 포인트
- VAD 오검출 방지를 위해 OpenAI 노이즈 리덕션과 브라우저 설정을 병행 적용
- AudioContext 샘플 레이트 강제 대신 AudioWorklet을 통한 리샘플링 권장
- 컨테이너 환경 검증 시 이전 프로세스의 포트 점유 여부 확인 필수
실시간 음성 통화 앱(WebSocket 중계/WebRTC 직결 × PTT/핸즈프리)을 운용하며 3가지 문제를 해결했다. 그중 2개는 로그를 통해 범인을 추적하는 수수께끼 풀이 과정이었기에, 그 과정을 공유한다.
문제 1: 책상을 두드리면 AI가 반응한다
핸즈프리 모드의 서버 VAD(Voice Activity Detection)가 너무 민감해서, 머그컵을 놓는 소리나 키보드 타건음을 「발화」로 오인한다. AI 응답 중 발생하는 소음으로 인해 응답이 중단되는 것(오버버진(Over-birgin))이 특히 뼈아프다.
흥미로운 점은 재현 실험 결과다. 합성한 충격음(노크, 풀스케일 박수, 키보드 연타, 잔향이 포함된 모의음)을 흘려보내도, semantic_vad는 전혀 속지 않았다. 오검출은 실제 마이크 특유의 무언가(AGC가 끌어올린 잔향, 기기 본체로 직접 전달되는 진동)로 인해 발생하고 있었다.
이쪽에서 재현할 수 없는 이상, 대책은 정공법뿐이다. OpenAI 세션 설정의 입력 노이즈 리덕션(noise_reduction: near_field)
- 브라우저 측
noiseSuppression
의 2단계 방어 체계를 구축했다. 「박수 3번 → 즉시 발화」로 이어지는 연결 음원으로, 박수 소리만 무시되고 직후의 발화가 정확하게 받아쓰기(Transcription)되는 것을 확인하며 해결했다.
문제 2: WebSocket 회선으로 전환하면 대화가 망가진다
WebRTC 회선에서는 쾌조였으나, WS 회선으로 전환하자마자 다음과 같은 상황이 발생했다:
사용자 「쇼터에 대한 것입니다」 → AI (정확하게 응답)
—— 회선을 WebSocket으로 전환 ——
사용자 「숀 아비브.」
...
먹먹한 목소리 + 엉터리 인식 —— 속도가 어긋난 음성을 ASR(Automatic Speech Recognition)이 「그럴듯한 일본어」로 억지로 해석할 때 나타나는 증상이다.
가짜 마이크(getUserMedia를 교체하여 합성 WAV를 흘려보냄)로 E2E 재현을 해보니, 의외로 받아쓰기는 완벽했다. 전환 로직에는 문제가 없었으며, 문제는 실제 디바이스인 「iPhone의 연동 마이크」에 의존하고 있었다.
범인은 24kHz로 지정된 AudioContext였다. Chrome에서 샘플 레이트(Sample Rate)를 강제하면 OS(CoreAudio)의 입력 디바이스 설정에까지 개입하게 되며, 가상 디바이스에서는 「실제 레이트와 레이블이 어긋난 음성」이 될 수 있다. WebRTC 회선이 무사했던 이유는 Chrome 내장 WebRTC 스택(Native Rate/Opus)이 이 경로를 거치지 않기 때문이다.
대책은 「레이트를 강제하지 않는 것」이다. AudioContext는 네이티브 레이트(Native Rate)로 열고, 24kHz 변환은 AudioWorklet 측의 면적 평균 리샘플러(Area Average Resampler)로 일원화했다. 교훈: 강제는 개입이다. 원하는 형태로의 변환은 자신의 손안에서 직접 처리하는 것이 더 안전했다.
문제 3(번외): 유령 프로세스 사건
컨테이너화 검증 과정에서, 설정했을 터인 noise_reduction
이 null로 반환된다. 코드는 정상, JSON도 정상인데, 컨테이너 로그에 접속 로그가 단 한 줄도 없다 ——
정체는, 이전 검증에서 제대로 종료시키지 못한 구버전 코드의 uvicorn이 포트 8001을 점유하여, 검증 트래픽을 전부 가로채고 있었던 것이었다. 「컨테이너 검증」이라고 생각했던 것은 처음부터 끝까지 유령(머지 전의 오래된 코드)과의 대화였다.
이후의 약속: 검증 전 lsof 확인과, docker logs에 접속 로그가 찍히고 있는지 확인하기. 「테스트가 통과했다」와 「테스트했다고 생각한 상대와 대화하고 있었다」는 별개의 문제다.
상세 버전(0.0.0.0의 함정, Cognito Hosted UI의 Cookie 동작 등)은 원문 블로그에서 확인하시기 바랍니다:
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기