cpu-tts / app /services /kitten_engine.py
grimshaw's picture
Fix KittenTTS preload for current wheel
675842a verified
Raw
History Blame Contribute Delete
4.54 kB
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
from app.config import Settings
from app.errors import OpenAICompatibleError
from app.services.supertonic_engine import SynthesisResult
try:
from kittentts import KittenTTS
except ImportError: # pragma: no cover
KittenTTS = None
@dataclass(frozen=True)
class KittenVoiceSpec:
alias: str
canonical_name: str
provider_voice_id: str
source: str = "builtin"
style_path: str = ""
class KittenEngine:
public_model_ids = [
"kitten-tts-micro-0.8",
"kitten-tts-micro",
"kitten-tts",
"kitten",
]
primary_model_id = "kitten-tts-micro-0.8"
sample_rate = 24000
voice_aliases = {
"bella": {"canonical_name": "Bella", "provider_voice_id": "expr-voice-2-f"},
"jasper": {"canonical_name": "Jasper", "provider_voice_id": "expr-voice-2-m"},
"luna": {"canonical_name": "Luna", "provider_voice_id": "expr-voice-3-f"},
"bruno": {"canonical_name": "Bruno", "provider_voice_id": "expr-voice-3-m"},
"rosie": {"canonical_name": "Rosie", "provider_voice_id": "expr-voice-4-f"},
"hugo": {"canonical_name": "Hugo", "provider_voice_id": "expr-voice-4-m"},
"kiki": {"canonical_name": "Kiki", "provider_voice_id": "expr-voice-5-f"},
"leo": {"canonical_name": "Leo", "provider_voice_id": "expr-voice-5-m"},
}
def __init__(self, tts: object | None, model_name: str) -> None:
self.tts = tts
self.model_name = model_name
@classmethod
def from_settings(cls, settings: Settings) -> "KittenEngine":
if KittenTTS is None:
return cls(tts=None, model_name=settings.kitten_model_name)
try:
tts = KittenTTS(
model_name=settings.kitten_model_name,
cache_dir=settings.kitten_cache_dir,
)
return cls(tts=tts, model_name=settings.kitten_model_name)
except Exception as exc: # pragma: no cover
raise OpenAICompatibleError(
status_code=500,
message=f"Failed to initialize Kitten TTS engine: {exc}",
error_type="server_error",
code="engine_init_failed",
) from exc
def list_voice_bindings(self) -> list[dict[str, str]]:
return [
{
"alias": voice,
"canonical_name": spec["canonical_name"],
"provider_voice_id": spec["provider_voice_id"],
"source": "builtin",
"style_path": "",
"model": self.primary_model_id,
}
for voice, spec in self.voice_aliases.items()
]
def supports_voice(self, voice: str) -> bool:
return voice.strip().lower() in self.voice_aliases
def synthesize(
self,
*,
text: str,
voice: str,
speed: float,
quality: str,
model_name: str,
lang: str,
) -> SynthesisResult:
del quality, model_name, lang
if self.tts is None:
raise OpenAICompatibleError(
status_code=500,
message="Kitten TTS engine is not available.",
error_type="server_error",
code="engine_unavailable",
)
normalized_voice = voice.strip().lower()
if normalized_voice not in self.voice_aliases:
raise OpenAICompatibleError(
status_code=400,
message=f"Unsupported Kitten voice '{voice}'.",
param="voice",
code="unsupported_voice",
)
if speed <= 0:
raise OpenAICompatibleError(
status_code=400,
message="Kitten speed must be greater than 0.",
param="speed",
code="invalid_speed",
)
try:
provider_voice_id = self.voice_aliases[normalized_voice][
"provider_voice_id"
]
audio = self.tts.generate(
text, voice=provider_voice_id, speed=speed, clean_text=False
)
except Exception as exc:
raise OpenAICompatibleError(
status_code=500,
message=f"Speech synthesis failed: {exc}",
error_type="server_error",
code="synthesis_failed",
) from exc
return SynthesisResult(waveform=np.asarray(audio), sample_rate=self.sample_rate)