1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
"""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
``backends.REGISTRY`` drives the hub's setup/configure menus.
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.
"""
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). 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 exact command the user runs to start the
server.
"""
key: str
label: str
installed: bool
configured: bool
running: bool = False
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 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
_build_registry()
|