diff options
| author | historia <historiavg@proton.me> | 2026-08-25 18:10:08 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-25 18:10:08 -0400 |
| commit | 4b49797b4d57c2d2cf63472636622a7a6280a38e (patch) | |
| tree | e3687c734de5d6159cdad288e025057ed6cd3a16 /app/backends | |
| parent | bcac6c42eaf9e004716d72960bddefb1db68a93c (diff) | |
| download | tts-audiobook-generator-4b49797b4d57c2d2cf63472636622a7a6280a38e.tar.gz | |
feat: no console drop when uninstalling backends
Diffstat (limited to 'app/backends')
| -rw-r--r-- | app/backends/__init__.py | 7 | ||||
| -rwxr-xr-x | app/backends/audiocpp.py | 15 | ||||
| -rw-r--r-- | app/backends/common.py | 23 | ||||
| -rw-r--r-- | app/backends/envs.py | 9 | ||||
| -rwxr-xr-x | app/backends/faster.py | 17 | ||||
| -rw-r--r-- | app/backends/qwen.py | 16 |
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") |
