실시간 STT 공급자를 OpenAI Realtime에서 ElevenLabs로 바꾼다
맥락
이틀 전(2026-07-15) 교체용 STT 추상화를 전부 걷어내며 OpenAI Realtime(gpt-realtime-whisper,
PCM16/24kHz/mono)을 구체 구현으로 확정했다. 공급자 중립 계층을 두는 대신 현재 제품의 전사 흐름·장애
처리·검증 가능성을 우선한 판단이었다. 그 대가로 "공급자를 바꾸려면 코드를 고쳐야 한다"를 명시적으로
받아들인 상태였다. 그리고 이틀 뒤 실제로 공급자를 바꿨다. 커밋 하나가 OpenAI 구현 5개 파일을 지우고
ElevenLabs 구현을 세웠다.
교체 사유는 어느 문서에도 남아 있지 않다. 커밋 메시지는 한 줄이고 본문이 없으며 설계 문서도 이슈 번호도 붙지 않았다. 2026-08-01 STT 벤더 비교가 "현재 쓰는 ElevenLabs"를 전제로 쓰였으니 그 사이에 확정됐다는 것만 확인된다. 근거로 쓸 수 있는 것은 코드 diff뿐이다. 왜 OpenAI를 떠났는지는 미확인이다.
결정
- 실시간 STT는 ElevenLabs
wss://api.elevenlabs.io/v1/speech-to-text/realtime하나로 고정한다. - 공급자 중립 계층은 되살리지 않는다. 두 번째 공급자를 붙이면서도 추상화는 만들지 않는다는 규칙을
여기서도 지킨다.
ElevenLabsRealtime*은 구체 클래스이고 interface도 provider 전략도 만들지 않는다. - 구간 확정 파라미터는 우리 설정이 소유한다.
connect-timeout 5s,final-drain-timeout 10s,vad-silence-threshold 1s,vad-threshold 0.4넷이다. 구간 확정은 사용자 조작에서 떼어내 자동(무음 감지·누적 상한)으로 두기로 했고, 그때 정한 임계값이 여기 산다. - 도메인 종료 사유에서 공급자 이름을 없앤다.
OPENAI_ERROR→STT_PROVIDER_ERROR. 마이그레이션이 기존 행을 UPDATE하고 CHECK 제약을 다시 건다(READY_TIMEOUT·CLIENT_DISCONNECTED·CLIENT_PROTOCOL_ERROR·STT_PROVIDER_ERROR·INTERNAL_ERROR다섯). - API 키는
ELEVENLABS_API_KEY환경변수로만 받는다. 실제 API를 때리는 검증은elevenlabs-smoke태그 opt-in 테스트로 격리해 상시 CI에서 뺀다.
대안
기록 없음. 비교 문서도 후보 목록도 이 시점에는 없다. 2026-08-01 STT 벤더 비교(Soniox 외 후보 5종)는 이 교체보다 2주 뒤 문서다. 이 결정의 근거로는 쓸 수 없고, 결과를 나중에 재검토한 기록이다.
결과
- 도메인에서 공급자 이름이 빠져(
STT_PROVIDER_ERROR) 다음 교체 때 종료 사유 enum은 손대지 않는다. - 교체 사유가 기록되지 않았다. 다음 공급자 판단에서는 "왜 OpenAI를 떠났는가"를 처음부터 다시 조사해야 한다. 남은 것은 코드 diff뿐이다.
- 두 번째 교체를 하면서도 추상화를 안 만들었으므로 세 번째 교체도 같은 크기의 커밋이 된다. wire protocol은 여전히 DTO·오디오 포맷·오류 처리에 그대로 붙어 있다.
- ElevenLabs는 한도 수치를 하나도 공개하지 않아 운영 한도를 우리가 실측해야 한다. 세션 상한 3,600초와 처리 큐 상한 약 20초는 프로덕션 로그로 확정했다(APP-294). 3,600초 절단이 우리 문제가 아님은 설정 없는 맥북에서 3,599,962ms를 재현해 배제했다. 정본은 공식 문서가 아니라 우리 실측이다. 상대가 말없이 바꾸면 그 수치는 그대로 낡고 아무도 모른다.
- 실시간 API에 화자 분리 파라미터가 없다. 화자를 나누려면 공급자를 바꾸거나 우리가 붙여야 한다. 그 갈림길이 ADR-0019다.
- 공식 SDK에 auto-reconnect가 없어 재연결이 앱 책임이 된다. 원본 오디오를 저장하지 않기로 한 결정과 합쳐져 "끊기면 앱이 다시 연다"가 됐다.