"""Registry of the TTS backends the audiobook generator can talk to. Each backend (audio.cpp, qwen, faster) lives in its own module and owns its setup wizard, its status detection, and the launch command it prints once configured. This package aggregates them into a single registry so ``audiobook.py``'s TUI hub and future tools can iterate backends without hardcoding their names: ``backends.detect_all()`` reports which are set up, and ``backends.REGISTRY`` drives the hub's setup/modify menus. Adding a backend: create ``backends/.py`` exposing ``detect() -> BackendStatus``, ``run_tui() -> int`` and ``modify_actions: list[ModifyAction]``, then append a ``BackendInfo`` in ``_build_registry`` below. ``audiobook.py`` and the hub pick it up automatically. """ from dataclasses import dataclass, field from typing import Callable, List, Optional @dataclass class BackendStatus: """How far a backend is set up, plus the command to start it. INSTALLED means the backend itself is present (a cloned + built checkout, or a pip package). CONFIGURED means the supporting files are in place (a server.json / voices.json and a converter/config.py that points at the right port). DETAILS are short status lines for the hub. LAUNCH_HINT is the exact command the user runs to start the server. """ key: str label: str installed: bool configured: bool details: List[str] = field(default_factory=list) launch_hint: str = "" @property def ready(self) -> bool: """True when the backend is installed and configured for use.""" return self.installed and self.configured @dataclass class ModifyAction: """A per-backend "modify" menu entry (e.g. "New server.json").""" label: str run: Callable[[], int] @dataclass class BackendInfo: """One registry entry: identity, detector, setup wizard, modify menu.""" key: str label: str detect: Callable[[], BackendStatus] setup_tui: Callable[[], int] modify_actions: List[ModifyAction] = field(default_factory=list) REGISTRY: List[BackendInfo] = [] _BY_KEY: dict = {} def _build_registry() -> None: """Import the backend modules and wire up REGISTRY (once).""" if REGISTRY: return from . import audiocpp, faster, qwen REGISTRY.append(BackendInfo( key="audiocpp", label="audio.cpp", detect=audiocpp.detect, setup_tui=audiocpp.run_tui, modify_actions=audiocpp.modify_actions, )) REGISTRY.append(BackendInfo( key="qwen", label="Qwen3-TTS (demo server)", detect=qwen.detect, setup_tui=qwen.run_tui, modify_actions=qwen.modify_actions, )) REGISTRY.append(BackendInfo( key="faster", label="faster-qwen3-tts", detect=faster.detect, setup_tui=faster.run_tui, modify_actions=faster.modify_actions, )) for info in REGISTRY: _BY_KEY[info.key] = info def get(key: str) -> Optional[BackendInfo]: """Return the registry entry for KEY, or None.""" _build_registry() return _BY_KEY.get(key) def detect_all() -> List[BackendStatus]: """Detect every registered backend's status, in registry order.""" _build_registry() return [info.detect() for info in REGISTRY] def detect(key: str) -> Optional[BackendStatus]: """Detect a single backend by key.""" info = get(key) return info.detect() if info is not None else None _build_registry()