Higgs Audio v3 는 연구/비상업 라이선스라 config.py 가 environment != dev 에서 차단하고 있었다. 그 가드를 푸는 건 법적 판단이라 코드로 결정할 수 없어서, 상업 사용이 허용된 설치형을 다시 찾아 MeloTTS Korean 으로 바꿨다. 결과적으로 가드를 건드릴 필요 자체가 사라졌다 — Higgs 가드는 그대로 두고 provider 만 melotts 로 두면 운영에서도 동작한다. 검토 결과: - MeloTTS MIT 한국어 지원 -> 채택. CPU 실시간, 사전학습 다화자 - Kokoro-82M Apache2.0 한국어 없음 -> 탈락. 공식 VOICES.md 언어 목록에 부재 - Piper GPL -> 탈락 - XTTS-v2 / Fish Speech 비상업 -> 탈락. Higgs 와 같은 문제 사전학습 다화자 모델이라 실존 인물 reference 를 쓰지 않는다. Higgs 경로가 P1 프리셋 한정이던 이유가 없으므로 모든 페르소나 프리셋에 적용된다. 구현: - scripts/melotts-server.py loopback HTTP 사이드카(/health, POST /tts -> WAV) - voice_tts_provider=melotts 경로와 VIGNETTE_MELOTTS_TTS_* 설정 - scripts/start-melotts.ps1 런처(설치 순서 안내 포함) 실측: - CPU 정상 상태 RTF 0.27~0.28(실시간 3.6배). 첫 실행 13.25 는 모델 다운로드 - POST /tts 200, WAV 350,566 bytes, 3.61s, 헤더 provider/model/license - 빈 텍스트 422, 미지 경로 404 로 fail-closed - 왕복 검증: MeloTTS 합성음을 로컬 faster-whisper 가 완전 일치 전사 "그렇게 느끼셨군요. 조금 더 이야기해 주실 수 있을까요?" (word timestamp 8개) 설치 함정 3가지를 decisions/local-voice-stack.md 에 남겼다. librosa 0.9.1 의 pkg_resources(setuptools<81), MeloTTS 가 언어와 무관하게 임포트하는 일본어 unidic 사전, Windows 한국어 g2p 의 eunjeon. G7 게이트의 TTS 허용목록에 melotts 를 추가했다. 선언/실제 불일치 차단과 배치 STT 배제는 그대로다. 검증: API 914 passed, 사이드카 melotts 16/16 + whisper 37/37, SSOT FAIL 0, ruff clean.
8.8 KiB
로컬 음성 스택 결정 — 노트북 faster-whisper STT + MeloTTS TTS
Date: 2026-08-08 (같은 날 TTS 재선정으로 개정) Status: accepted — STT·TTS 모두 구현·실측 완료, 라이선스 제약 없음 Owner decision: 윤찬
결정
음성 캐스케이드의 양쪽을 개발 노트북에 상주하는 로컬 모델로 운영한다. 둘 다 허용적 라이선스(MIT) 라 운영 사용에 제약이 없다.
- STT(듣기): faster-whisper — MIT (
local_whisperprovider) - TTS(말하기): MeloTTS Korean — MIT (
melottsprovider)
TTS 재선정 (Higgs → MeloTTS)
처음에는 노트북에 이미 있던 Higgs Audio v3 TTS 4B를 쓰기로 했으나, 그 모델은 연구/비상업
라이선스라 config.py가 environment != dev에서 차단하고 있었다. 그 가드를 제거하는 건 법적
판단이라 코드로 결정할 수 없었다. 그래서 상업 사용이 허용된 설치형을 다시 찾았고 MeloTTS로
바꿨다. 결과적으로 가드를 건드릴 필요 자체가 사라졌다 — Higgs 가드는 그대로 두고 provider 만
melotts로 두면 운영에서도 동작한다.
검토한 대안과 탈락 이유:
| 후보 | 라이선스 | 한국어 | 판정 |
|---|---|---|---|
| MeloTTS | MIT | 지원 | 채택. CPU 실시간, 사전학습 다화자 |
| Kokoro-82M | Apache 2.0 | 미지원 | 탈락. 공식 VOICES.md 언어 목록에 한국어 없음 |
| Piper | GPL | 제한적 | 탈락. 상업 배포에 부담 |
| XTTS-v2 / Fish Speech | 비상업 | 지원 | 탈락. Higgs 와 같은 문제 |
| Higgs Audio v3 4B | 연구/비상업 | 지원 | 보류. dev 전용 유지 |
MeloTTS는 사전학습 다화자 모델이라 실존 인물 음성 reference 를 전혀 쓰지 않는다. Higgs 경로가 P1 프리셋 한정이었던 이유(권리 안전한 synthetic reference 를 P1만 보유)가 MeloTTS 에는 없으므로 모든 페르소나 프리셋에 적용된다.
이 결정이 뒤집는 것
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. scripts/melotts-server.py— loopback HTTP 사이드카(/health,POST /tts→ WAV).VIGNETTE_VOICE_TTS_PROVIDER=melotts+VIGNETTE_MELOTTS_TTS_URL설정. 실행:scripts/start-melotts.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이다.
MeloTTS 실측 증거 (2026-08-08)
설치: 전용 venv C:\Users\encep\.venvs\vignette-melotts, melotts 0.1.2.
설치 중 걸린 것 3가지와 해법을 남긴다(다음 사람이 같은 데서 막힌다).
librosa 0.9.1(MeloTTS 핀)이pkg_resources를 쓰는데 설치가 올린setuptools 83에서 제거됐다 →pip install "setuptools<81".- MeCab 이 일본어
unidic사전을 요구한다(MeloTTS 가 언어와 무관하게 임포트) →python -m unidic download. - 한국어 g2p(
g2pkk)가 Windows 에서eunjeon을 요구한다 →pip install eunjeon.
성능(CPU, torch 2.13.0+cpu, GPU 미사용):
| 회차 | 텍스트 길이 | 오디오 길이 | 합성 시간 | RTF |
|---|---|---|---|---|
| 1 (웜업) | 22자 | 4.78s | 3.89s | 0.81 |
| 2 | 13자 | 2.99s | 0.82s | 0.28 |
| 3 | 31자 | 6.16s | 1.65s | 0.27 |
정상 상태 RTF 0.27~0.28 로 실시간의 약 3.6배 빠르다. 첫 실행의 13.25는 모델 다운로드가 섞인 값이다.
왕복 검증 — 로컬 TTS 로 합성한 음성을 로컬 STT 로 되돌렸다. 둘 다 노트북 안에서만 돈다.
입력 그렇게 느끼셨군요. 조금 더 이야기해 주실 수 있을까요?
전사 그렇게 느끼셨군요. 조금 더 이야기해 주실 수 있을까요? (완전 일치, word timestamp 8개)
HTTP 엔드포인트 POST /tts 실측: 200, WAV 350,566 bytes, 3.61s,
헤더 X-Vignette-TTS-Provider: melotts / Model: melotts-korean / License: MIT.
빈 텍스트 422, 알 수 없는 경로 404 로 fail-closed. 이 응답 WAV 도 왕복 전사에서 완전 일치했다.
회귀: 사이드카 16/16, API 전체 914 passed, 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를 차단한다. MeloTTS 채택으로 이 가드를 풀 이유가 없어졌다. Higgs 를 굳이 운영에서 쓰고 싶어지면 그때 라이선스 근거를 이 문서에 추가하고 가드를 조정한다. - CPU 폴백 성능. CPU int8에서는
small이 현실적이다.large-v3는 CPU에서 실시간 스트리밍에 맞추기 어렵다.
G7 게이트에 미친 영향
check-g7-external-proof.py의 벤더 하드코딩을 운영하기로 한 provider 허용목록으로 바꿨다.
ALLOWED_STT_PROVIDERS = ("local_whisper", "deepgram")
ALLOWED_TTS_PROVIDERS = ("melotts", "higgs", "openai")
게이트를 약화시키지 않았다. 배치 STT(openai)는 interim/final 계약을 만족할 수 없어 목록에 없고,
expected_* == ready_* 결속(선언한 provider와 실제로 돈 provider가 같아야 함)은 그대로다.
G7의 나머지 세 artifact(물리 마이크 50분 soak, worker/topology high-water, 독립 blind human
voice-gain pack)는 이 결정과 무관하게 그대로 남아 있다.