TTS Blob src 교체 AbortError — AI 발화 말끊김 세션 로그 원인 분석 원인 확인
마지막 업데이트 2026-07-22
1. 결론
AI TTS 말끝이 잘릴 수 있는 직접 원인은 확인됐다. 이전 TTS 청크의
HTMLAudioElement.play()가 완료되기 전에 새 Blob URL을 같은 audio 엘리먼트의 src에 설정했다. 브라우저는 이전 재생 요청을 취소하고 AbortError를 반환한다.청크 도착 간격이 일정하지 않은 것은 이 현상을 촉발하는 조건일 뿐, 근본 문제는 새 청크를 즉시 같은 엘리먼트에 덮어쓰는 재생 경합 처리다. 이 로그만으로 TTS 공급 서버가 비정상적으로 빠르게 청크를 보냈다고 단정할 수는 없다.
2. 직접 증거
| 시각 | 컴포넌트 | 로그와 판정 |
|---|---|---|
| 19:00:50.822 KST 10:00:50.822 UTC | TTS_PLAYER | AbortError: The play() request was interrupted by a new load request.새 로드가 기존 재생을 중단했다는 브라우저의 직접 진단. |
| 19:01:52.549 KST 10:01:52.549 UTC | TTS_PLAYER | 동일한 AbortError 재발. 일회성 사용자 권한/네트워크 실패가 아니라 재생 경합 패턴. |
[2026-07-20T10:00:50.822Z] [WARN] [TTS_PLAYER]
TTS local audio element playback failed
{
"name": "AbortError",
"message": "The play() request was interrupted by a new load request. https://goo.gl/LdLk22"
}
3. 코드 경로와 발생 조건
apps/web/entities/guest-session/lib/tts-player.ts의 playBlobAudioElement()는 재생 중인 엘리먼트에도 즉시 다음 순서로 동작한다.
audio.srcObject = null;
audio.src = objectUrl; // 새 TTS 청크의 Blob URL
void audio.play().catch(...);
청크 A 도착
audio.src = Aplay() 시작→
청크 A 재생 중아직
ended 전→
청크 B 도착
audio.src = B로 즉시 교체→
결과A의 재생 요청 취소
AbortError 및 말끝 절단 가능코드 주석도 이전 src를 새 Blob으로 교체하면 이전 재생이 즉시 중단된다고 명시한다. 따라서 로그와 구현이 동일한 원인을 가리킨다.
4. 함께 관찰된 신호와 범위
| 신호 | 로그 시각 | 의미 / 관계 |
|---|---|---|
ai-audio outbound stalled | 19:12:08 | producer는 live·unmuted·transport connected이나 5초 표본의 bytes/packets/energy delta가 모두 0. 원격 진행자에게 전달되는 AI 오디오가 멈출 수 있는 별도 경로이며, 위의 local Blob AbortError와는 별도 조사 대상. |
AUDIO_PROBE suspected silent | 세션 중 5회 | AudioContext가 running인데 hardware clock이 전진하지 않았다. 기기 출력 파이프라인 이상 후보이나, TTS AbortError의 직접 원인은 아니다. |
| noise reduction mismatch | 19:08:49 | 원하는 far_field와 실제 null 불일치. 아동 입력 잡음/인식 품질 후보이며 TTS 출력 말끊김의 원인은 아니다. |
| External STT duplicate skipped | 강제 퇴장 직후 77회/6.619초, 재차 9회/0.706초 | 종료 뒤 잔여 STT 콜백 또는 동일·무음 청크 정황. TTS 재생 중단의 근거로 사용하지 않는다. |
5. 권고안 — 재생 큐로 단일 소비자 보장
권고안이며 이 문서에서 구현을 확정하지 않는다. 새 청크가 도착해도 현재 재생의
src를 변경하지 말고 FIFO 큐에 넣는다. 현재 청크의 ended 후에만 다음 Blob URL을 설정한다.- 청크 도착: Blob URL을 재생 큐에 추가하고, 재생 중이면 반환한다.
- 단일 async 재생 루프: 큐 맨 앞을
audio.src에 설정 →play()→ended를 기다린 뒤 다음 항목으로 진행한다. - 정상 완료 뒤에만 해당 Blob URL을 revoke한다.
- 세션 종료·사용자 끼어들기처럼 의도적으로 중단할 때만
pause(),src초기화 및 큐 비우기를 수행한다. - 의도적 취소에는 세션 세대/generation을 남겨 예상된 AbortError로 분류하고, 그 외 AbortError는 재생 경합 오류로 유지한다.
청크 간 무음까지 최소화해야 하는 요구가 확인되면, 후속 대안으로 Web Audio의 예약 재생을 검토할 수 있다. 그러나 현재 직접 증거에 대한 최소 대응은 단일 소비자 FIFO 큐다.
6. 재현·검증 기준
- 청크 A 재생 도중 청크 B를 의도적으로 주입해도 A의
ended전audio.src가 바뀌지 않는다. - 정상 연속 발화에서
TTS local audio element playback failed / AbortError가 0건이다. - 의도적 취소 시에는 이전 음성이 즉시 중단되고, 취소 사유·generation이 로그에 남는다.
- 원격 출력 검증은
ai-audiooutbound RTP delta를 별도로 확인한다. local TTS 재생 성공만으로 SFU 송신 정상까지 판단하지 않는다.