vignette/docs/decisions/local-voice-stack.md
Yun Chan 05aa7b312e 로컬 whisper STT 사이드카 디바이스 폴백과 deprecated audioop 제거
cuDNN 부재 환경에서 ctranslate2 는 Python 예외가 아니라 네이티브 크래시로 죽어
프로세스가 통째로 사라진다. 같은 프로세스의 try/except 로는 절대 잡을 수 없어
디바이스 확인을 버릴 수 있는 자식 프로세스(--self-check)로 분리했다.
auto 는 CUDA 프로브 실패 시 CPU(int8)로 폴백하고, 명시적 --device cuda 는 조용히
강등하지 않는다.

함께 고친 것:
- warmup PCM 생성의 연산자 우선순위 버그(b"\x00\x00" * N // 2 가 바이트열 정수
  나눗셈이 되어 TypeError). warmup_pcm() 으로 분리하고 테스트로 고정했다.
- audioop 은 Python 3.13 에서 제거되므로 array + 정수 연산으로 RMS 를 직접 구한다.

실측(저장소 무참조 synthetic seed 8.72초, CPU int8 small):
interim 9 · final 5 · word timestamp 12개 present, 전사는 원문과 한 글자 차이.
사이드카 회귀 37/37.
2026-08-08 01:39:58 +09:00

92 lines
5.7 KiB
Markdown

# 로컬 음성 스택 결정 — 노트북 faster-whisper STT + Higgs TTS
Date: 2026-08-08
Status: accepted (STT 구현 완료 · TTS 운영 사용은 라이선스 확인 대기)
Owner decision: 윤찬
## 결정
음성 캐스케이드의 양쪽을 **개발 노트북에 상주하는 로컬 모델**로 운영한다.
- **STT(듣기)**: faster-whisper (`local_whisper` provider)
- **TTS(말하기)**: Higgs Audio v3 TTS 4B
## 이 결정이 뒤집는 것
`MASTERPLAN.md`의 음성 행은 "OpenAI `gpt-4o-mini-tts` + **Deepgram STT**, voice_id 추상화(Higgs/GPT-SoVITS
로컬 폴백)"였다. 그 기록에서 로컬 모델은 *폴백*이었고 주 경로는 외부 API였다. 이 문서가 그 행을 대체한다.
## 왜 이 결정이 필요했는가
2026-08-08 확인 결과 상태가 서로 어긋나 있었다.
- 실제 런타임은 **Deepgram을 쓰고 있지 않았다.** `.env`에 STT provider 설정이 없어 코드 기본값 `openai`
(배치 전사)가 적용됐고, NAS 프리뷰도 `VIGNETTE_VOICE_STT_PROVIDER=openai`였다.
- 그런데 G7 종료 체커(`scripts/check-g7-external-proof.py`)는 `expected_stt_provider == "deepgram"`
**하드코딩**하고 있었다. 이는 결정 기록이 아니라 벤더 한 줄이었고, 그 때문에 "G7을 닫으려면 운영
Deepgram 키가 필요하다"는 잘못된 요구가 만들어졌다.
- 코드에는 **로컬 STT 경로가 아예 없었다.** `voice_stt_provider``openai|deepgram` 둘뿐이었고,
interim/final 스트리밍 구현은 Deepgram 전용이었다. Higgs는 TTS 전용이라 STT를 대신하지 못한다.
## 근거
- **데이터 주권**: 상담 훈련 오디오가 호스트를 벗어나지 않는다. 미성년 원본 활용동의·한신대 데이터
거버넌스 게이트가 열려 있는 상태에서 외부 STT로 원음을 보내는 것보다 경계가 단순하다.
- **외부 키 의존 제거**: 운영 Deepgram key/quota 없이도 G7의 interim/final 스트리밍 계약을 만족한다.
- **자산 보유**: 노트북에 `faster-whisper 1.0.3` + `ctranslate2 4.5.0`과 모델
(`large-v3`/`large-v3-turbo`/`medium`/`small`/`base`)이 이미 캐시돼 있고, Higgs v3 4B도 ComfyUI에 있다.
## 구현
- `scripts/local-whisper-stt-server.py` — loopback WebSocket 사이드카. Higgs TTS 서버와 같은 상주 모델
방식이다. linear16 PCM을 받아 RMS 기반 endpointing으로 발화를 나누고 interim/final과 word timestamp를
낸다. 오디오는 발화 단위 메모리 버퍼로만 다루고 확정 즉시 버린다. 디스크에 쓰지 않는다.
- `apps/api/app/services/voice.py``LocalWhisperStreamingSession` — Deepgram 세션과 **같은 공개 표면**
(`send_audio`/`finish`/`abort`)이라 WebSocket 라우트는 provider로 분기하지 않는다.
- `VIGNETTE_VOICE_STT_PROVIDER=local_whisper` + `VIGNETTE_LOCAL_WHISPER_*` 설정.
- 실행: `scripts/start-local-whisper-stt.ps1`.
## 실측 증거 (2026-08-08)
저장소의 무참조 synthetic seed 음성(8.72초, 원문이 manifest에 기록됨)으로 종단 검증했다.
| 항목 | 값 |
|---|---|
| interim 프레임 | 9 |
| final 프레임 | 5 (발화 분절 동작) |
| word timestamp | 12개 전부 present |
| 전사 | `안녕하세여. 저는 서연이에요. 오늘은 천천히 너무 밝지 않게 하지만 또렷하게 말해 볼게요.` |
| 원문 | `안녕하세요. 저는 서연이에요. 오늘은 천천히, 너무 밝지 않게, 하지만 또렷하게 말해볼게요.` |
`small` + CPU int8에서 한 글자(`안녕하세요``안녕하세여`) 차이였다. 회귀는 사이드카 37/37,
API 전체 908 passed, gateway 58, G7 checker 23, ruff clean이다.
## 알려진 제약
1. **cuDNN 부재로 GPU 추론 불가.** 이 노트북은 CUDA 장치가 보이지만 `cudnn_ops64_9.dll`이 없어
ctranslate2가 **네이티브 크래시**로 죽는다. Python 예외가 아니라 프로세스가 통째로 죽으므로 같은
프로세스의 `try/except`로는 잡을 수 없다. 그래서 디바이스 확인을 버릴 수 있는 자식 프로세스
(`--self-check`)로 분리하고 `auto`에서 CPU(int8)로 폴백한다. 명시적 `--device cuda`는 조용히 강등하지
않는다. **cuDNN 9를 설치하면** GPU float16으로 올라가고 `large-v3`도 실시간권에 들어온다. 설치는
환경 변경이라 소유자 판단으로 남겼다.
2. **Higgs 운영 사용은 아직 막혀 있다.** `apps/api/app/config.py``environment != dev`에서
`VIGNETTE_VOICE_TTS_PROVIDER=higgs`를 차단한다(Higgs Audio v3 4B 연구/비상업 라이선스). 이 가드는
법적 판단이라 코드에서 임의로 제거하지 않았다. 산학협력 연구·교육 용도가 라이선스 범위에 들어가는지
확인하고 그 근거를 이 문서에 추가한 뒤 가드를 조정한다. 그 전까지 Higgs는 dev 전용이고 운영 TTS는
OpenAI `gpt-4o-mini-tts`를 유지한다.
3. **CPU 폴백 성능.** CPU int8에서는 `small`이 현실적이다. `large-v3`는 CPU에서 실시간 스트리밍에
맞추기 어렵다.
## G7 게이트에 미친 영향
`check-g7-external-proof.py`의 벤더 하드코딩을 **운영하기로 한 provider 허용목록**으로 바꿨다.
```python
ALLOWED_STT_PROVIDERS = ("local_whisper", "deepgram")
ALLOWED_TTS_PROVIDERS = ("higgs", "openai")
```
게이트를 약화시키지 않았다. 배치 STT(`openai`)는 interim/final 계약을 만족할 수 없어 목록에 없고,
`expected_* == ready_*` 결속(선언한 provider와 실제로 돈 provider가 같아야 함)은 그대로다.
G7의 나머지 세 artifact(물리 마이크 50분 soak, worker/topology high-water, 독립 blind human
voice-gain pack)는 이 결정과 무관하게 그대로 남아 있다.