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.
5.7 KiB
로컬 음성 스택 결정 — 노트북 faster-whisper STT + Higgs TTS
Date: 2026-08-08 Status: accepted (STT 구현 완료 · TTS 운영 사용은 라이선스 확인 대기) Owner decision: 윤찬
결정
음성 캐스케이드의 양쪽을 개발 노트북에 상주하는 로컬 모델로 운영한다.
- STT(듣기): faster-whisper (
local_whisperprovider) - 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이다.
알려진 제약
- cuDNN 부재로 GPU 추론 불가. 이 노트북은 CUDA 장치가 보이지만
cudnn_ops64_9.dll이 없어 ctranslate2가 네이티브 크래시로 죽는다. Python 예외가 아니라 프로세스가 통째로 죽으므로 같은 프로세스의try/except로는 잡을 수 없다. 그래서 디바이스 확인을 버릴 수 있는 자식 프로세스 (--self-check)로 분리하고auto에서 CPU(int8)로 폴백한다. 명시적--device cuda는 조용히 강등하지 않는다. cuDNN 9를 설치하면 GPU float16으로 올라가고large-v3도 실시간권에 들어온다. 설치는 환경 변경이라 소유자 판단으로 남겼다. - Higgs 운영 사용은 아직 막혀 있다.
apps/api/app/config.py가environment != dev에서VIGNETTE_VOICE_TTS_PROVIDER=higgs를 차단한다(Higgs Audio v3 4B 연구/비상업 라이선스). 이 가드는 법적 판단이라 코드에서 임의로 제거하지 않았다. 산학협력 연구·교육 용도가 라이선스 범위에 들어가는지 확인하고 그 근거를 이 문서에 추가한 뒤 가드를 조정한다. 그 전까지 Higgs는 dev 전용이고 운영 TTS는 OpenAIgpt-4o-mini-tts를 유지한다. - CPU 폴백 성능. CPU int8에서는
small이 현실적이다.large-v3는 CPU에서 실시간 스트리밍에 맞추기 어렵다.
G7 게이트에 미친 영향
check-g7-external-proof.py의 벤더 하드코딩을 운영하기로 한 provider 허용목록으로 바꿨다.
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)는 이 결정과 무관하게 그대로 남아 있다.