aboutsummaryrefslogtreecommitdiff
path: root/app/backends/__init__.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-24 02:59:26 -0400
committerhistoria <historiavg@proton.me>2026-08-24 02:59:26 -0400
commitf00249db9d1ea051d29aa1bcca869fc4b88e83eb (patch)
treea75f076fac1b63e0b4bf2eb8f54affbcc681a891 /app/backends/__init__.py
parent9dd4f9595be3b1d76a3a07dc3eca90cfaf8a3f97 (diff)
downloadtts-audiobook-generator-f00249db9d1ea051d29aa1bcca869fc4b88e83eb.tar.gz
refactor: add app directory, dir structure change
Diffstat (limited to 'app/backends/__init__.py')
-rw-r--r--app/backends/__init__.py146
1 files changed, 146 insertions, 0 deletions
diff --git a/app/backends/__init__.py b/app/backends/__init__.py
new file mode 100644
index 0000000..ed772d4
--- /dev/null
+++ b/app/backends/__init__.py
@@ -0,0 +1,146 @@
+"""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 whether their server is currently running), and the registry
+drives the hub's setup/configure menus.
+
+The registry is built lazily on the first call to ``get``/``detect_all``/
+``detect`` (not at package import time), because the backend modules pull
+in ``converter.tts`` and its third-party dependencies, which are only
+available inside the managed venv that ``audiobook.py`` bootstraps before
+importing them. ``backends.envs`` is imported during that bootstrap, so
+importing this package must stay cheap and dependency-free.
+
+Adding a backend: create ``backends/<name>.py`` exposing
+``detect() -> BackendStatus``, ``run_tui() -> int`` and
+``configure_actions: list[ConfigureAction]``, then append a ``BackendInfo`` in
+``_build_registry`` below. ``audiobook.py`` and the hub pick it up
+automatically.
+"""
+
+import shlex
+from dataclasses import dataclass, field
+from typing import Callable, List, Optional
+
+
+@dataclass
+class ServerSpec:
+ """One launchable server process for a backend.
+
+ A backend may expose more than one server (qwen runs CustomVoice and Base
+ on separate ports). ARGV is the exact command line the hub spawns (using
+ the managed venv's absolute binaries, so no shell activation is needed);
+ URL is the endpoint ``common.server_running`` probes to decide readiness.
+ """
+ name: str
+ url: str
+ argv: List[str]
+
+
+@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 an app/converter/config.py that
+ points at the right port). RUNNING means an external server is
+ currently accepting connections on the configured port (probed by
+ ``backends.common.server_running``). DETAILS are short status lines for
+ the hub. LAUNCH_HINT is the human-readable command(s) the user runs to
+ start the server, derived from SERVERS by ``format_launch_hint``.
+ SERVERS is the machine-usable list of server processes the hub can
+ start/stop (empty when the backend is not yet configured).
+ """
+ key: str
+ label: str
+ installed: bool
+ configured: bool
+ running: bool = False
+ details: List[str] = field(default_factory=list)
+ launch_hint: str = ""
+ servers: List[ServerSpec] = field(default_factory=list)
+
+ @property
+ def ready(self) -> bool:
+ """True when the backend is installed and configured for use."""
+ return self.installed and self.configured
+
+
+def format_launch_hint(servers: List[ServerSpec]) -> str:
+ """Join a backend's server argvs into a copy-pasteable launch hint."""
+ return " ; ".join(shlex.join(s.argv) for s in servers)
+
+
+@dataclass
+class ConfigureAction:
+ """A per-backend "configure" menu entry (e.g. "New server.json")."""
+ label: str
+ run: Callable[[], int]
+
+
+@dataclass
+class BackendInfo:
+ """One registry entry: identity, detector, setup wizard, configure menu."""
+ key: str
+ label: str
+ detect: Callable[[], BackendStatus]
+ setup_tui: Callable[[], int]
+ configure_actions: List[ConfigureAction] = 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,
+ configure_actions=audiocpp.configure_actions,
+ ))
+ REGISTRY.append(BackendInfo(
+ key="qwen",
+ label="qwen-tts",
+ detect=qwen.detect,
+ setup_tui=qwen.run_tui,
+ configure_actions=qwen.configure_actions,
+ ))
+ REGISTRY.append(BackendInfo(
+ key="faster",
+ label="faster-qwen3-tts",
+ detect=faster.detect,
+ setup_tui=faster.run_tui,
+ configure_actions=faster.configure_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