Newer
Older
voice / src / voice_tts / tts / __init__.py
"""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,
)