- 전용 venv 워커(8002)로 격리, MeloTTS(8788)와 병행 - 남녀 10종 음성, 한국어/자동감지(다국어), 속도·피치 - 모델 ~260MB 베이킹, torch/GPU 불필요 - entrypoint로 워커+메인앱 동시 기동
95 lines
3.0 KiB
Python
95 lines
3.0 KiB
Python
"""Supertonic 엔진 (프록시).
|
|
|
|
실제 합성은 전용 venv 워커(worker.py)가 담당하고, 이 엔진은 HTTP로 요청을
|
|
전달한다. 워커 URL(SUPERTONIC_URL)이 설정된 경우에만 활성화된다.
|
|
자연스러운 한국어 음성(수퍼톤), ONNX 기반이라 GPU 불필요.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
from .base import BaseEngine
|
|
|
|
WORKER_URL = os.environ.get("SUPERTONIC_URL", "").rstrip("/")
|
|
|
|
_VOICES = [
|
|
{"id": "F1", "label": "여성 1"},
|
|
{"id": "F2", "label": "여성 2"},
|
|
{"id": "F3", "label": "여성 3"},
|
|
{"id": "F4", "label": "여성 4"},
|
|
{"id": "F5", "label": "여성 5"},
|
|
{"id": "M1", "label": "남성 1"},
|
|
{"id": "M2", "label": "남성 2"},
|
|
{"id": "M3", "label": "남성 3"},
|
|
{"id": "M4", "label": "남성 4"},
|
|
{"id": "M5", "label": "남성 5"},
|
|
]
|
|
|
|
|
|
class SupertonicEngine(BaseEngine):
|
|
id = "supertonic"
|
|
label = "Supertonic (자연 음성)"
|
|
license = "MIT"
|
|
uses_gpu = False
|
|
notes = "수퍼톤의 자연스러운 다국어 음성. 한국어/자동감지, 남녀 10종, 속도·피치."
|
|
|
|
@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},
|
|
{"code": "AUTO", "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("Supertonic 워커가 설정되지 않았습니다.")
|
|
|
|
lang = "na" if (language or "KO").upper() == "AUTO" else "ko"
|
|
payload = json.dumps(
|
|
{
|
|
"text": text,
|
|
"voice": speaker or "F1",
|
|
"lang": lang,
|
|
"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=120) as resp:
|
|
return resp.read()
|
|
except urllib.error.HTTPError as exc:
|
|
detail = exc.read().decode("utf-8", errors="ignore")
|
|
raise ValueError(f"Supertonic 합성 실패: {detail}")
|
|
except Exception as exc: # pragma: no cover
|
|
raise ValueError(f"Supertonic 워커 연결 실패: {exc}")
|