설치형 로컬 TTS를 MeloTTS 한국어(MIT)로 채택하고 엔드포인트로 연결

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.
This commit is contained in:
Yun Chan 2026-08-08 09:29:57 +09:00
parent 05aa7b312e
commit 2624d49984
15 changed files with 749 additions and 33 deletions

93
scripts/start-melotts.ps1 Normal file
View file

@ -0,0 +1,93 @@
param(
[int]$Port = 9883,
[string]$Language = 'KR',
[ValidateSet('auto', 'cuda', 'cpu')]
[string]$Device = 'auto',
[int]$WaitReadySeconds = 300
)
# 노트북 상주 MeloTTS 한국어 TTS 사이드카를 띄운다.
# MIT 라이선스 사전학습 다화자 모델이라 상업 사용 제약도, 실존 인물 reference 문제도 없다.
# 그래서 Higgs 와 달리 dev 전용 가드나 P1 프리셋 한정이 필요 없다.
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$repoRoot = Split-Path -Parent $PSScriptRoot
$serverScript = Join-Path $PSScriptRoot 'melotts-server.py'
$venvPython = 'C:\Users\encep\.venvs\vignette-melotts\Scripts\python.exe'
$healthUrl = "http://127.0.0.1:$Port/health"
$runtimeLogDir = Join-Path $env:TEMP 'Vignette\melotts'
$stdoutLog = Join-Path $runtimeLogDir 'server.out.log'
$stderrLog = Join-Path $runtimeLogDir 'server.err.log'
if (!(Test-Path -LiteralPath $serverScript)) {
throw "MeloTTS 서버 스크립트를 찾지 못했습니다: $serverScript"
}
if (!(Test-Path -LiteralPath $venvPython)) {
throw @"
MeloTTS 전용 venv 찾지 못했습니다: $venvPython
설치 순서:
python -m venv C:\Users\encep\.venvs\vignette-melotts
& C:\Users\encep\.venvs\vignette-melotts\Scripts\python.exe -m pip install "git+https://github.com/myshell-ai/MeloTTS.git"
& ... -m pip install "setuptools<81" eunjeon # librosa 0.9.1 의 pkg_resources, 한국어 g2p
& ... -m unidic download # MeloTTS 가 무조건 임포트하는 일본어 사전
"@
}
try {
$currentHealth = Invoke-RestMethod -Uri $healthUrl -Method Get -TimeoutSec 3
if ($currentHealth.status -eq 'ok') {
Write-Output "MeloTTS가 이미 준비됐습니다: $healthUrl"
$currentHealth | ConvertTo-Json -Depth 4 -Compress
return
}
} catch {
# 아직 서버가 없으면 아래에서 시작한다.
}
$listener = Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue
if ($null -ne $listener) {
throw "포트 $Port 를 다른 프로세스가 사용 중입니다. 임의 종료하지 않았습니다."
}
New-Item -ItemType Directory -Path $runtimeLogDir -Force | Out-Null
$serverArgs = @(
'-X', 'utf8', $serverScript,
'--host', '127.0.0.1',
'--port', "$Port",
'--language', $Language,
'--device', $Device
)
$process = Start-Process -WindowStyle Hidden -FilePath $venvPython `
-ArgumentList $serverArgs `
-WorkingDirectory $repoRoot `
-RedirectStandardOutput $stdoutLog `
-RedirectStandardError $stderrLog `
-PassThru
Write-Output "MeloTTS 로드를 시작했습니다. PID=$($process.Id) language=$Language device=$Device"
Write-Output "로그: $stdoutLog"
if ($WaitReadySeconds -le 0) {
return
}
$deadline = (Get-Date).AddSeconds($WaitReadySeconds)
while ((Get-Date) -lt $deadline) {
if ($process.HasExited) {
throw "MeloTTS가 준비되기 전에 종료됐습니다. stderr=$stderrLog"
}
try {
$health = Invoke-RestMethod -Uri $healthUrl -Method Get -TimeoutSec 3
if ($health.status -eq 'ok') {
Write-Output "MeloTTS 준비 완료: $healthUrl"
$health | ConvertTo-Json -Depth 4 -Compress
return
}
} catch {
Start-Sleep -Seconds 3
}
}
throw "MeloTTS 준비 시간이 ${WaitReadySeconds}초를 넘었습니다. 로그: $stdoutLog"