| 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: |
| 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: |
| 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) |
|
|