feat: CosyVoice 한국어 엔진 통합 (Apache-2.0, GPU)
- vendor/CosyVoice: 패치된 CosyVoice 코드 + Matcha-TTS 벤더링 - cosyvoice_worker: 전용 venv에서 상주하는 합성 워커(FastAPI, 모델 1회 로드) - app/engines/cosyvoice_engine: HTTP 프록시 엔진(속도/피치) - Dockerfile: 전용 venv(torch cu128) + 모델 베이킹 + 빌드 워밍업 - entrypoint.sh: 워커(8001) + 메인앱(8788) 동시 기동
This commit is contained in:
@@ -31,15 +31,15 @@ def _build() -> None:
|
||||
except Exception as exc: # pragma: no cover
|
||||
print(f"[registry] MeloTTS 로드 실패: {exc}")
|
||||
|
||||
# Coqui GlowTTS-KSS (전용 venv + 체크포인트가 있을 때만)
|
||||
# CosyVoice 한국어 (전용 워커 URL 이 설정된 경우에만)
|
||||
try:
|
||||
from .coqui_engine import CoquiKssEngine
|
||||
from .cosyvoice_engine import CosyVoiceEngine
|
||||
|
||||
if CoquiKssEngine.available():
|
||||
e = CoquiKssEngine()
|
||||
if CosyVoiceEngine.available():
|
||||
e = CosyVoiceEngine()
|
||||
_ENGINES[e.id] = e
|
||||
except Exception as exc: # pragma: no cover
|
||||
print(f"[registry] Coqui 엔진 스킵: {exc}")
|
||||
print(f"[registry] CosyVoice 엔진 스킵: {exc}")
|
||||
|
||||
_INITED = True
|
||||
|
||||
|
||||
82
app/engines/cosyvoice_engine.py
Normal file
82
app/engines/cosyvoice_engine.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""CosyVoice 엔진 (프록시).
|
||||
|
||||
실제 합성은 전용 venv에서 도는 워커(worker.py)가 담당하고, 이 엔진은 HTTP로
|
||||
요청을 전달한다. 워커 URL(COSYVOICE_URL)이 설정된 경우에만 활성화된다.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
from .base import BaseEngine
|
||||
|
||||
WORKER_URL = os.environ.get("COSYVOICE_URL", "").rstrip("/")
|
||||
|
||||
|
||||
class CosyVoiceEngine(BaseEngine):
|
||||
id = "cosyvoice"
|
||||
label = "CosyVoice (한국어)"
|
||||
license = "Apache-2.0"
|
||||
uses_gpu = True
|
||||
notes = "LLM 기반 한국어 음성(프리셋 화자). 속도 지원, 피치는 후처리."
|
||||
|
||||
@staticmethod
|
||||
def available() -> bool:
|
||||
return bool(WORKER_URL)
|
||||
|
||||
def describe(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"label": self.label,
|
||||
"license": self.license,
|
||||
"uses_gpu": self.uses_gpu,
|
||||
"notes": self.notes,
|
||||
"languages": [
|
||||
{
|
||||
"code": "KR",
|
||||
"label": "한국어",
|
||||
"speakers": [
|
||||
{"id": "韩语女", "label": "한국어 여성 (기본)"},
|
||||
],
|
||||
}
|
||||
],
|
||||
"supports": {"speed": True, "pitch": True},
|
||||
}
|
||||
|
||||
def synth_wav(
|
||||
self,
|
||||
text: str,
|
||||
language: str,
|
||||
speaker: str | None = None,
|
||||
speed: float = 1.0,
|
||||
pitch: float = 0.0,
|
||||
) -> bytes:
|
||||
text = (text or "").strip()
|
||||
if not text:
|
||||
raise ValueError("텍스트가 비어 있습니다.")
|
||||
if not WORKER_URL:
|
||||
raise ValueError("CosyVoice 워커가 설정되지 않았습니다.")
|
||||
|
||||
payload = json.dumps(
|
||||
{
|
||||
"text": text,
|
||||
"speaker": speaker or "韩语女",
|
||||
"speed": speed,
|
||||
"pitch": pitch,
|
||||
}
|
||||
).encode("utf-8")
|
||||
request = urllib.request.Request(
|
||||
WORKER_URL + "/synth",
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=180) as resp:
|
||||
return resp.read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", errors="ignore")
|
||||
raise ValueError(f"CosyVoice 합성 실패: {detail}")
|
||||
except Exception as exc: # pragma: no cover
|
||||
raise ValueError(f"CosyVoice 워커 연결 실패: {exc}")
|
||||
Reference in New Issue
Block a user