feat: OuteTTS 엔진 추가(개인용/비상업 CC-BY-NC, 한국어)
- Qwen2 500M + WavTokenizer, torch cu128 GPU, 전용 venv 워커(8003) - 한국어 프리셋 4종(male/female), 속도·피치 librosa 후처리 - transformers 4.49로 torchvision 강제 import 회피, soundfile 저장 - scenema-audio는 VRAM 24GB+/Gemma12B 게이트로 8GB GPU에선 불가 → 미포함
This commit is contained in:
@@ -41,6 +41,16 @@ def _build() -> None:
|
||||
except Exception as exc: # pragma: no cover
|
||||
print(f"[registry] Supertonic 엔진 스킵: {exc}")
|
||||
|
||||
# OuteTTS (개인용/비상업, 전용 워커 URL 이 설정된 경우에만)
|
||||
try:
|
||||
from .outetts_engine import OuteTTSEngine
|
||||
|
||||
if OuteTTSEngine.available():
|
||||
e = OuteTTSEngine()
|
||||
_ENGINES[e.id] = e
|
||||
except Exception as exc: # pragma: no cover
|
||||
print(f"[registry] OuteTTS 엔진 스킵: {exc}")
|
||||
|
||||
_INITED = True
|
||||
|
||||
|
||||
|
||||
84
app/engines/outetts_engine.py
Normal file
84
app/engines/outetts_engine.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""OuteTTS 엔진 (프록시, 개인용/비상업).
|
||||
|
||||
CC-BY-NC-4.0 라이선스라 개인/비상업 용도로만 사용. 워커 URL(OUTETTS_URL)이
|
||||
설정된 경우에만 활성화된다.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
from .base import BaseEngine
|
||||
|
||||
WORKER_URL = os.environ.get("OUTETTS_URL", "").rstrip("/")
|
||||
|
||||
_VOICES = [
|
||||
{"id": "female_1", "label": "여성 1"},
|
||||
{"id": "female_2", "label": "여성 2"},
|
||||
{"id": "male_1", "label": "남성 1"},
|
||||
{"id": "male_2", "label": "남성 2"},
|
||||
]
|
||||
|
||||
|
||||
class OuteTTSEngine(BaseEngine):
|
||||
id = "outetts"
|
||||
label = "OuteTTS (개인용·비상업)"
|
||||
license = "CC-BY-NC-4.0"
|
||||
uses_gpu = True
|
||||
notes = "Qwen2 기반 한국어 TTS. 비상업 라이선스라 개인 사용 전용. 속도·피치 후처리."
|
||||
|
||||
@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": "KO", "label": "한국어", "speakers": _VOICES},
|
||||
],
|
||||
"default_language": "KO",
|
||||
"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("OuteTTS 워커가 설정되지 않았습니다.")
|
||||
|
||||
payload = json.dumps(
|
||||
{
|
||||
"text": text,
|
||||
"voice": speaker or "female_1",
|
||||
"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"OuteTTS 합성 실패: {detail}")
|
||||
except Exception as exc: # pragma: no cover
|
||||
raise ValueError(f"OuteTTS 워커 연결 실패: {exc}")
|
||||
Reference in New Issue
Block a user