aboutsummaryrefslogtreecommitdiff
path: root/ui/hub.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-24 01:57:13 -0400
committerhistoria <historiavg@proton.me>2026-08-24 01:58:17 -0400
commitc02d66b2d3221c0c5f5e8f2cb2ae218f1e325a0a (patch)
treee9ec4f35c18102d4624d3cd59358d192be7bbfcb /ui/hub.py
parent194c63e4d11e6de9792a736a7b99788f1db78741 (diff)
downloadtts-audiobook-generator-c02d66b2d3221c0c5f5e8f2cb2ae218f1e325a0a.tar.gz
feat: manage venv for all backends
Diffstat (limited to 'ui/hub.py')
-rw-r--r--ui/hub.py177
1 files changed, 164 insertions, 13 deletions
diff --git a/ui/hub.py b/ui/hub.py
index 3115f54..f3e2b7e 100644
--- a/ui/hub.py
+++ b/ui/hub.py
@@ -16,15 +16,27 @@ import json
from pathlib import Path
from typing import Optional, Tuple
-from ui import tui
import audiobook
-from backends import REGISTRY, BackendStatus, detect_all, get
+from backends import (
+ REGISTRY,
+ BackendStatus,
+ ServerSpec,
+ common,
+ detect_all,
+ get,
+ servers,
+)
from backends import audiocpp as audiocpp_backend
from backends import faster as faster_backend
from converter import config
from converter.converter import AUDIO_FORMATS
-from converter.tts import AUDIOCPP_FAMILY_QWEN3_TTS, BACKEND_AUDIOCPP, \
- BACKEND_FASTER, BACKEND_QWEN
+from converter.tts import (
+ AUDIOCPP_FAMILY_QWEN3_TTS,
+ BACKEND_AUDIOCPP,
+ BACKEND_FASTER,
+ BACKEND_QWEN,
+)
+from ui import tui
_GO_BACK = object()
@@ -54,6 +66,8 @@ def run() -> int:
info.configure_actions[command[2]].run()
elif kind == "convert":
_run_conversion(command[1], command[2])
+ elif kind == "server":
+ _run_server_action(command[1], command[2])
def _hub_menu(stdscr) -> Optional[tuple]:
@@ -64,6 +78,7 @@ def _hub_menu(stdscr) -> Optional[tuple]:
if any(st.installed or st.running for st in statuses):
options.insert(0, ("Convert books...", "convert"))
options.append(("Configure a backend...", "configure"))
+ options.append(("Server...", "server"))
options.append(("Quit", "quit"))
rows = [(st.label, *_status_mark(st)) for st in statuses]
choice = tui.menu(
@@ -83,6 +98,10 @@ def _hub_menu(stdscr) -> Optional[tuple]:
cmd = _configure_menu(stdscr, statuses)
if cmd is not None:
return cmd
+ elif choice == "server":
+ cmd = _server_menu(stdscr, statuses)
+ if cmd is not None:
+ return cmd
def _setup_menu(stdscr, statuses) -> Optional[tuple]:
@@ -162,12 +181,17 @@ def _convert_menu(stdscr, statuses) -> Optional[tuple]:
if key == "__setup__":
return _setup_menu(stdscr, statuses)
if key == BACKEND_AUDIOCPP:
- return _convert_audiocpp(stdscr, statuses)
- if key == BACKEND_QWEN:
- return _convert_qwen(stdscr)
- if key == BACKEND_FASTER:
- return _convert_faster(stdscr)
- return None
+ cmd = _convert_audiocpp(stdscr, statuses)
+ elif key == BACKEND_QWEN:
+ cmd = _convert_qwen(stdscr)
+ elif key == BACKEND_FASTER:
+ cmd = _convert_faster(stdscr)
+ else:
+ return None
+ if cmd is None:
+ return None
+ _add_autostart(stdscr, cmd, statuses)
+ return cmd
def _convert_audiocpp(stdscr, statuses) -> Optional[tuple]:
@@ -350,14 +374,141 @@ def _common_options(stdscr) -> Optional[dict]:
def _run_conversion(backend: str, kwargs: dict) -> None:
- """Run a conversion in the plain console (after the TUI returns)."""
+ """Run a conversion in the plain console (after the TUI returns).
+
+ When the convert menu recorded an ``autostart`` server (the user opted to
+ have the hub start it), spawn it now and abort the conversion if it does
+ not come up. After the conversion, offer to stop a server we started.
+ """
+ autostart = kwargs.pop("autostart", None)
status = next((s for s in detect_all() if s.key == backend), None)
if status is not None and not status.ready and not status.running:
print(f"[WARNING] {status.label} is not fully set up.")
- if status is not None and not status.running and status.launch_hint:
+ if autostart:
+ spec = _find_spec(autostart)
+ if spec is None:
+ print(f"[WARNING] no server named '{autostart}'; continuing")
+ elif not servers.start(spec):
+ print("[ERROR] could not start the server; aborting conversion.")
+ if status is not None and status.launch_hint:
+ print("Start it manually and run the conversion again:")
+ print(f" {status.launch_hint}")
+ return
+ elif status is not None and not status.running and status.launch_hint:
print("[INFO] Make sure the server is running. Start it with:")
print(f" {status.launch_hint}")
- audiobook.convert(backend=backend, **kwargs)
+ try:
+ audiobook.convert(backend=backend, **kwargs)
+ finally:
+ if autostart:
+ _maybe_stop_server(autostart)
+
+
+def _maybe_stop_server(name: str) -> None:
+ """Ask (in the plain console) whether to stop a server we auto-started."""
+ try:
+ ans = input(f"\n[?] Stop the '{name}' server now? [y/N] ").strip().lower()
+ except EOFError:
+ return
+ if ans in ("y", "yes"):
+ servers.stop(name)
+
+
+def _add_autostart(stdscr, cmd: tuple, statuses) -> None:
+ """Offer to auto-start the conversion's target server when it isn't running.
+
+ Records the chosen server spec name as ``kwargs['autostart']`` for
+ ``_run_conversion`` to act on. Mode-aware for qwen (custom vs clone).
+ """
+ _, key, kwargs = cmd
+ status = next((s for s in statuses if s.key == key), None)
+ if status is None or not status.servers:
+ return
+ spec = _select_spec(status, kwargs)
+ if spec is None:
+ return
+ if common.server_running(spec.url):
+ return
+ choice = tui.confirm(stdscr, f"The {status.label} server is not running. "
+ "Start it automatically?", default=True,
+ cancel_value=False)
+ if choice is True:
+ kwargs["autostart"] = spec.name
+
+
+def _select_spec(status, kwargs) -> Optional[ServerSpec]:
+ """The server spec this conversion needs (mode-aware for qwen)."""
+ if status.key == BACKEND_QWEN:
+ wanted = "qwen-clone" if kwargs.get("clone") else "qwen-custom"
+ return next((s for s in status.servers if s.name == wanted), None)
+ return status.servers[0] if status.servers else None
+
+
+def _find_spec(name: str) -> Optional[ServerSpec]:
+ """Look up a server spec by name across every backend's detect()."""
+ for st in detect_all():
+ for spec in st.servers:
+ if spec.name == name:
+ return spec
+ return None
+
+
+def _run_server_action(spec_name: str, action: str) -> None:
+ """Run a Start/Stop action in the plain console (after the TUI returns)."""
+ if action == "start":
+ spec = _find_spec(spec_name)
+ if spec is None:
+ print(f"[ERROR] no server named '{spec_name}'")
+ return
+ servers.start(spec)
+ elif action == "stop":
+ servers.stop(spec_name)
+
+
+def _server_menu(stdscr, statuses) -> Optional[tuple]:
+ """Pick a backend, then one of its servers and a Start/Stop action."""
+ candidates = [st for st in statuses if st.servers or st.running]
+ if not candidates:
+ tui.flash(stdscr, "No backend with a server is available. "
+ "Set one up first.")
+ return None
+ options = [(st.label, st.key) for st in candidates]
+ key = tui.menu(stdscr, "Start / Stop a server", options,
+ back_value=_GO_BACK)
+ if key is _GO_BACK or key is None:
+ return None
+ status = next((s for s in statuses if s.key == key), None)
+ if status is None:
+ return None
+ return _server_actions(stdscr, status)
+
+
+def _server_actions(stdscr, status) -> Optional[tuple]:
+ """Pick a server spec (qwen has two) and a Start or Stop action."""
+ specs = status.servers
+ if not specs:
+ tui.flash(stdscr, f"{status.label} has no server configured. "
+ "Run 'Set up a backend' first.")
+ return None
+ if len(specs) == 1:
+ spec = specs[0]
+ else:
+ options = [(f"{s.name} ({'running' if common.server_running(s.url) else 'stopped'})",
+ s.name) for s in specs]
+ name = tui.menu(stdscr, f"{status.label} server", options,
+ back_value=_GO_BACK)
+ if name is _GO_BACK or name is None:
+ return None
+ spec = next((s for s in specs if s.name == name), None)
+ if spec is None:
+ return None
+ running = common.server_running(spec.url)
+ action = tui.menu(
+ stdscr, f"{spec.name} ({'running' if running else 'stopped'})",
+ [("Start", "start"), ("Stop", "stop")], back_value=_GO_BACK)
+ if action is _GO_BACK or action is None:
+ return None
+ return ("server", spec.name, action)
def _list_voices(voice_dir: str) -> list: