"""TTS backend registry and factory.
Each backend module self-registers via ``@register(name)``.
Usage:
>>> from voice_tts.tts import create_engine
>>> engine = create_engine("s2") # by name
>>> engine = create_engine() # from settings.TTS_BACKEND
"""
from pathlib import Path
from voice_tts.config import settings
from voice_tts.tts.engine import TTSEngine, DummyTTSEngine
_BACKENDS: dict[str, type[TTSEngine]] = {
"dummy": DummyTTSEngine,
}
def register(name: str):
"""Decorator that registers a :class:`TTSEngine` subclass."""
def decorator(cls):
_BACKENDS[name] = cls
return cls
return decorator
def get_backend(name: str | None = None) -> type[TTSEngine]:
"""Look up a backend class by name (defaults to ``settings.tts_backend``)."""
name = name or settings.tts_backend
if name not in _BACKENDS:
raise RuntimeError(
f"Unknown TTS backend: {name}. "
f"Available backends: {list(_BACKENDS.keys())}"
)
return _BACKENDS[name]
def list_backends() -> list[str]:
"""Return names of all registered backends."""
return list(_BACKENDS.keys())
def create_engine(name: str | None = None) -> TTSEngine:
"""Create a TTS engine instance.
Each backend reads its configuration from ``settings``; no arguments
are passed to the constructor besides ``name``.
"""
cls = get_backend(name)
return cls()
# Lazy-import backends so they self-register via ``@register``.
# Each submodule gracefully handles missing optional dependencies.
from voice_tts.tts import ( # noqa: E402, F401
s2_backend,
f5_backend,
xtts_backend,
fish_speech_backend,
)