aboutsummaryrefslogtreecommitdiff
path: root/app/backends
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-25 18:10:08 -0400
committerhistoria <historiavg@proton.me>2026-08-25 18:10:08 -0400
commit4b49797b4d57c2d2cf63472636622a7a6280a38e (patch)
treee3687c734de5d6159cdad288e025057ed6cd3a16 /app/backends
parentbcac6c42eaf9e004716d72960bddefb1db68a93c (diff)
downloadtts-audiobook-generator-4b49797b4d57c2d2cf63472636622a7a6280a38e.tar.gz
feat: no console drop when uninstalling backends
Diffstat (limited to 'app/backends')
-rw-r--r--app/backends/__init__.py7
-rwxr-xr-xapp/backends/audiocpp.py15
-rw-r--r--app/backends/common.py23
-rw-r--r--app/backends/envs.py9
-rwxr-xr-xapp/backends/faster.py17
-rw-r--r--app/backends/qwen.py16
6 files changed, 67 insertions, 20 deletions
diff --git a/app/backends/__init__.py b/app/backends/__init__.py
index 83f9866..63f0709 100644
--- a/app/backends/__init__.py
+++ b/app/backends/__init__.py
@@ -147,13 +147,16 @@ class BackendInfo:
SETUP_SCREEN runs the setup wizard on an already-open curses screen
(the hub's), returning 0 on completion and non-zero when aborted; the
- hub calls it as one screen of its own ``tui.Wizard`` stack.
+ hub calls it as one screen of its own ``tui.Wizard`` stack. UNINSTALL
+ removes the backend (stops its servers, pip-uninstalls, deletes its
+ files); the hub runs it inside the task view, calling it with optional
+ ``emit``/``cancel`` keywords (cancel honored between phases only).
"""
key: str
label: str
detect: Callable[[], BackendStatus]
setup_screen: Callable[[object], int]
- uninstall: Callable[[], int] = lambda: 0
+ uninstall: Callable[..., int] = lambda *args, **kwargs: 0
REGISTRY: List[BackendInfo] = []
diff --git a/app/backends/audiocpp.py b/app/backends/audiocpp.py
index 0feb480..31aa01d 100755
--- a/app/backends/audiocpp.py
+++ b/app/backends/audiocpp.py
@@ -1650,16 +1650,25 @@ def delete_model_files(server_json: Path, entries: List[dict]) -> int:
return removed
-def uninstall() -> int:
+def uninstall(*, emit=None, cancel=None) -> int:
"""Remove the audio.cpp backend entirely: stop its server, delete the checkout.
The checkout (``app/audio.cpp``, or wherever ``find_local_checkout``
resolves it) holds the built binary, the downloaded models, and the
server.json, so removing the directory uninstalls the backend. A running
- server this tool started is stopped first (best-effort). Returns the exit
- code.
+ server this tool started is stopped first (best-effort).
+
+ EMIT is accepted for registry symmetry with the other backends but is
+ unused here — this uninstall has no subprocess phase, and its prints are
+ captured by the task view when run in the TUI. CANCEL is a
+ ``threading.Event`` honored between phases only (after the server has
+ been stopped, before the checkout is deleted), so a started phase always
+ completes and the uninstall never tears halfway. Returns the exit code
+ (130 when cancelled before a remaining phase).
"""
servers.stop("audiocpp")
+ if common.cancel_requested(cancel):
+ return 130
checkout = find_local_checkout()
if checkout is None:
print("[INFO] No audio.cpp checkout to remove.")
diff --git a/app/backends/common.py b/app/backends/common.py
index cb573c3..10b4ccf 100644
--- a/app/backends/common.py
+++ b/app/backends/common.py
@@ -71,6 +71,18 @@ def drain_post_tui_notices() -> List[str]:
return notices
+def cancel_requested(cancel) -> bool:
+ """True when CANCEL (a ``threading.Event``) is given and set.
+
+ Shared guard for the multi-phase uninstall actions: cancellation is
+ honored only between phases (stop servers / pip / delete files), so a
+ phase that already started always runs to completion and an uninstall
+ never tears halfway. Callers return 130 when this fires before a
+ pending phase.
+ """
+ return cancel is not None and cancel.is_set()
+
+
def normalize_dir_arg(value: str) -> Path:
"""Normalize a user-supplied path argument.
@@ -301,8 +313,7 @@ def run_console_subprocess(argv: List[str], cwd: Optional[Path] = None,
"""Run a subprocess, streaming output to the console or to EMIT.
With EMIT None the child inherits the real terminal and its output
- appears normally (used by the non-interactive CLI paths and the quick
- ``tui.suspend`` actions like uninstall). With EMIT given (a
+ appears normally (the non-interactive CLI paths). With EMIT given (a
``callable(str)``) the child's stdout/stderr are merged, read line by
line (splitting on both ``\\n`` and ``\\r`` so carriage-return progress
updates like git's or tqdm's surface as lines), and each line is passed
@@ -459,11 +470,13 @@ def pip_install(packages: List[str]) -> int:
return envs.pip_install(packages)
-def pip_uninstall(packages: List[str]) -> int:
+def pip_uninstall(packages: List[str], *, emit=None) -> int:
"""pip uninstall PACKAGES from the managed venv. Returns exit code.
Delegates to ``backends.envs.pip_uninstall`` (local import to avoid a
- circular import). Used by the backends' ``uninstall`` action.
+ circular import). Used by the backends' ``uninstall`` action. With EMIT
+ given (the in-TUI task view) pip runs piped, streaming into EMIT, so
+ its output never touches the terminal behind curses.
"""
from backends import envs
- return envs.pip_uninstall(packages)
+ return envs.pip_uninstall(packages, emit=emit)
diff --git a/app/backends/envs.py b/app/backends/envs.py
index 7b3c54b..cfeeec6 100644
--- a/app/backends/envs.py
+++ b/app/backends/envs.py
@@ -113,18 +113,21 @@ def pip_install(packages: List[str], *, emit=None, cancel=None) -> int:
return common.run_console_subprocess(argv, emit=emit, cancel=cancel)
-def pip_uninstall(packages: List[str]) -> int:
+def pip_uninstall(packages: List[str], *, emit=None) -> int:
"""pip uninstall PACKAGES from the venv. Returns pip's exit code.
Used by the backends' ``uninstall`` action to remove pip-installed TTS
packages from the managed environment. A missing env is a no-op (there
- is nothing to uninstall from), reported as success.
+ is nothing to uninstall from), reported as success. With EMIT given
+ (the in-TUI task view) pip runs with its output piped and streamed to
+ EMIT, so nothing writes to the terminal behind curses.
"""
if not env_exists():
return 0
print(f"[INFO] pip uninstall {' '.join(packages)} from {ENV_DIR}...")
return common.run_console_subprocess(
- [str(env_python()), "-m", "pip", "uninstall", "-y", *packages])
+ [str(env_python()), "-m", "pip", "uninstall", "-y", *packages],
+ emit=emit)
def module_available(module: str) -> bool:
diff --git a/app/backends/faster.py b/app/backends/faster.py
index 585e480..8c64183 100755
--- a/app/backends/faster.py
+++ b/app/backends/faster.py
@@ -591,21 +591,32 @@ def _detect_remote(managed: bool = False):
return False, {}
-def uninstall() -> int:
+def uninstall(*, emit=None, cancel=None) -> int:
"""Remove the faster-qwen3-tts backend entirely.
Uninstalls the pip package (``faster-qwen3-tts``) from the managed venv
and deletes the cloned checkout (``app/faster-qwen3-tts``, which holds
examples/openai_server.py and voices.json). A running server this tool
- started is stopped first (best-effort). Returns the exit code.
+ started is stopped first (best-effort).
+
+ With EMIT given (the in-TUI task view) pip runs piped, streaming into
+ EMIT, so its output never touches the terminal behind curses. CANCEL is
+ a ``threading.Event`` honored between phases only (stop server / pip /
+ delete checkout) — a started phase always completes, so pip is never
+ killed mid-run. Returns the exit code (130 when cancelled before a
+ remaining phase).
"""
servers.stop("faster")
- rc = common.pip_uninstall(["faster-qwen3-tts"])
+ if common.cancel_requested(cancel):
+ return 130
+ rc = common.pip_uninstall(["faster-qwen3-tts"], emit=emit)
if rc != 0:
print("[WARNING] pip uninstall failed (exit "
f"{rc}); remove faster-qwen3-tts from the managed venv manually")
else:
print("[OK] faster-qwen3-tts removed.")
+ if common.cancel_requested(cancel):
+ return 130
checkout = _checkout()
if checkout.is_dir():
print(f"[INFO] Removing checkout {checkout}...")
diff --git a/app/backends/qwen.py b/app/backends/qwen.py
index be7ebfe..c6a11bf 100644
--- a/app/backends/qwen.py
+++ b/app/backends/qwen.py
@@ -354,17 +354,25 @@ def _detect_remote(managed: bool = False):
return remote_models, remote_urls
-def uninstall() -> int:
+def uninstall(*, emit=None, cancel=None) -> int:
"""Remove the qwen-tts backend entirely: stop its servers, pip uninstall.
qwen-tts is a pip package (``qwen_tts`` + the ``qwen-tts-demo`` script)
installed into the managed venv, so uninstalling it removes the backend.
- Any server this tool started is stopped first (best-effort). Returns the
- exit code.
+ Any server this tool started is stopped first (best-effort).
+
+ With EMIT given (the in-TUI task view) pip runs piped, streaming into
+ EMIT, so its output never touches the terminal behind curses. CANCEL is
+ a ``threading.Event`` honored between phases only (after the servers
+ have been stopped, before pip starts) — a started phase always completes,
+ so pip is never killed mid-run. Returns the exit code (130 when
+ cancelled before pip ran).
"""
servers.stop("qwen-custom")
servers.stop("qwen-clone")
- rc = common.pip_uninstall([QWEN_PIP_PKG])
+ if common.cancel_requested(cancel):
+ return 130
+ rc = common.pip_uninstall([QWEN_PIP_PKG], emit=emit)
if rc != 0:
print(f"[WARNING] pip uninstall failed (exit {rc}); remove "
f"{QWEN_PIP_PKG} from the managed venv manually")