voiceclone-th-api / tts_engine.py
paomailbox's picture
Deploy VoiceClone TH API (F5-TTS-THAI)
d14f1d9 verified
Raw
History Blame Contribute Delete
5.77 kB
"""
VoiceClone AI — TTS engine abstraction
โหลดเอนจินโคลนเสียงจริงตามลำดับความพร้อม:
1) F5-TTS (รองรับ checkpoint ภาษาไทย เช่น VIZINTZOR/F5-TTS-THAI)
2) Coqui XTTS-v2 (OpenVoice-style zero-shot cloning ทางเลือก)
เลือกเอนจินได้ด้วย env: TTS_ENGINE = auto | f5 | xtts
ถ้าไม่มีเอนจินใดติดตั้งไว้ get_engine() จะคืน None และ /generate จะตอบ 503
ทำให้ frontend สลับไปใช้ Web Speech โดยอัตโนมัติ
"""
import os
from pathlib import Path
class EngineUnavailable(Exception):
pass
class BaseEngine:
name = "base"
model_name = None
def clone(self, text, ref_wav, ref_text, out_path,
speed=1.0, emotion="neutral", language="th"):
raise NotImplementedError
class F5TTSEngine(BaseEngine):
"""F5-TTS zero-shot voice cloning.
ตั้ง checkpoint ภาษาไทยผ่าน env:
F5_MODEL (ชื่อโมเดลพื้นฐาน, ค่าเริ่มต้น F5TTS_v1_Base)
F5_CKPT_FILE (path ไปยัง .safetensors/.pt ของ F5-TTS-THAI)
F5_VOCAB_FILE (path ไปยัง vocab.txt ของโมเดลไทย)
"""
name = "F5-TTS"
def __init__(self):
try:
from f5_tts.api import F5TTS
except Exception as e: # pragma: no cover
raise EngineUnavailable(f"f5-tts ยังไม่ได้ติดตั้ง: {e}")
here = Path(__file__).parent
ckpt = os.getenv("F5_CKPT_FILE")
vocab = os.getenv("F5_VOCAB_FILE")
# 1) ตรวจหา checkpoint ภาษาไทยที่ดาวน์โหลดไว้ใน backend/models/ (เครื่อง dev)
if not ckpt:
cand = here / "models" / "model_1000000.pt"
if cand.exists():
ckpt = str(cand)
vcand = here / "models" / "vocab.txt"
if not vocab and vcand.exists():
vocab = str(vcand)
else:
# 2) ไม่มีในเครื่อง → ดาวน์โหลดจาก HF Hub (เหมาะกับ deploy เช่น HF Spaces / Fly)
repo = os.getenv("F5_HF_REPO", "VIZINTZOR/F5-TTS-THAI")
if repo:
from huggingface_hub import hf_hub_download
ckpt = hf_hub_download(repo, os.getenv("F5_HF_CKPT", "model_1000000.pt"))
if not vocab:
vocab = hf_hub_download(repo, os.getenv("F5_HF_VOCAB", "vocab.txt"))
# โมเดลไทยอิงสถาปัตยกรรม F5TTS_Base (ไม่ใช่ v1)
default_arch = "F5TTS_Base" if ckpt else "F5TTS_v1_Base"
model_name = os.getenv("F5_MODEL", default_arch)
kwargs = {}
if ckpt:
kwargs["ckpt_file"] = ckpt
if vocab:
kwargs["vocab_file"] = vocab
self.model = F5TTS(model=model_name, **kwargs)
is_thai = bool(ckpt and ("THAI" in ckpt.upper() or "1000000" in ckpt))
self.name = "F5-TTS-THAI" if is_thai else "F5-TTS"
self.model_name = (Path(ckpt).name if ckpt else model_name)
def clone(self, text, ref_wav, ref_text, out_path,
speed=1.0, emotion="neutral", language="th"):
self.model.infer(
ref_file=str(ref_wav),
ref_text=ref_text or "",
gen_text=text,
file_wave=str(out_path),
speed=float(speed or 1.0),
nfe_step=32,
cfg_strength=2.0,
remove_silence=True,
)
return out_path
class XTTSEngine(BaseEngine):
"""Coqui XTTS-v2 — zero-shot cloning จากตัวอย่างเสียงสั้น ๆ"""
name = "XTTS-v2"
def __init__(self):
try:
from TTS.api import TTS
import torch
except Exception as e: # pragma: no cover
raise EngineUnavailable(f"coqui TTS ยังไม่ได้ติดตั้ง: {e}")
dev = "cuda" if torch.cuda.is_available() else "cpu"
self.tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to(dev)
self.model_name = "xtts_v2"
def clone(self, text, ref_wav, ref_text, out_path,
speed=1.0, emotion="neutral", language="th"):
lang = "th" if language == "th" else "en"
try:
self.tts.tts_to_file(text=text, speaker_wav=str(ref_wav),
language=lang, file_path=str(out_path),
speed=float(speed or 1.0))
except Exception:
# บางรุ่นของ XTTS ไม่รองรับไทยอย่างเป็นทางการ — ถอยไปใช้ en
self.tts.tts_to_file(text=text, speaker_wav=str(ref_wav),
language="en", file_path=str(out_path))
return out_path
def load_engine():
pref = os.getenv("TTS_ENGINE", "auto").lower()
order = []
if pref in ("f5", "f5-tts", "auto"):
order.append(F5TTSEngine)
if pref in ("xtts", "coqui", "openvoice", "auto"):
order.append(XTTSEngine)
errors = []
for Engine in order:
try:
return Engine()
except EngineUnavailable as e:
errors.append(str(e))
raise EngineUnavailable("ไม่มีเอนจิน TTS พร้อมใช้งาน: " + " | ".join(errors))