TTS Blob src 교체 AbortError — AI 발화 말끊김 세션 로그 원인 분석 원인 확인

마지막 업데이트 2026-07-22

작성일: 2026-07-21 · 발생일: 2026-07-20 · 대상 로그: 6-019f7ee8…/0/2dfd0dd5… · 앱: tbwewz/ppi-prod

1. 결론

AI TTS 말끝이 잘릴 수 있는 직접 원인은 확인됐다. 이전 TTS 청크의 HTMLAudioElement.play()가 완료되기 전에 새 Blob URL을 같은 audio 엘리먼트의 src에 설정했다. 브라우저는 이전 재생 요청을 취소하고 AbortError를 반환한다.

청크 도착 간격이 일정하지 않은 것은 이 현상을 촉발하는 조건일 뿐, 근본 문제는 새 청크를 즉시 같은 엘리먼트에 덮어쓰는 재생 경합 처리다. 이 로그만으로 TTS 공급 서버가 비정상적으로 빠르게 청크를 보냈다고 단정할 수는 없다.

2. 직접 증거

시각컴포넌트로그와 판정
19:00:50.822 KST
10:00:50.822 UTC
TTS_PLAYERAbortError: 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.tsplayBlobAudioElement()는 재생 중인 엘리먼트에도 즉시 다음 순서로 동작한다.

audio.srcObject = null;
audio.src = objectUrl;       // 새 TTS 청크의 Blob URL
void audio.play().catch(...);
청크 A 도착audio.src = A
play() 시작
청크 A 재생 중아직 ended
청크 B 도착audio.src = B로 즉시 교체
결과A의 재생 요청 취소
AbortError 및 말끝 절단 가능

코드 주석도 이전 src를 새 Blob으로 교체하면 이전 재생이 즉시 중단된다고 명시한다. 따라서 로그와 구현이 동일한 원인을 가리킨다.

4. 함께 관찰된 신호와 범위

신호로그 시각의미 / 관계
ai-audio outbound stalled19:12:08producer는 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 mismatch19:08:49원하는 far_field와 실제 null 불일치. 아동 입력 잡음/인식 품질 후보이며 TTS 출력 말끊김의 원인은 아니다.
External STT duplicate skipped강제 퇴장 직후 77회/6.619초, 재차 9회/0.706초종료 뒤 잔여 STT 콜백 또는 동일·무음 청크 정황. TTS 재생 중단의 근거로 사용하지 않는다.

5. 권고안 — 재생 큐로 단일 소비자 보장

권고안이며 이 문서에서 구현을 확정하지 않는다. 새 청크가 도착해도 현재 재생의 src를 변경하지 말고 FIFO 큐에 넣는다. 현재 청크의 ended 후에만 다음 Blob URL을 설정한다.
  1. 청크 도착: Blob URL을 재생 큐에 추가하고, 재생 중이면 반환한다.
  2. 단일 async 재생 루프: 큐 맨 앞을 audio.src에 설정 → play()ended를 기다린 뒤 다음 항목으로 진행한다.
  3. 정상 완료 뒤에만 해당 Blob URL을 revoke한다.
  4. 세션 종료·사용자 끼어들기처럼 의도적으로 중단할 때만 pause(), src 초기화 및 큐 비우기를 수행한다.
  5. 의도적 취소에는 세션 세대/generation을 남겨 예상된 AbortError로 분류하고, 그 외 AbortError는 재생 경합 오류로 유지한다.

청크 간 무음까지 최소화해야 하는 요구가 확인되면, 후속 대안으로 Web Audio의 예약 재생을 검토할 수 있다. 그러나 현재 직접 증거에 대한 최소 대응은 단일 소비자 FIFO 큐다.

6. 재현·검증 기준

7. 관련 문서