aboutsummaryrefslogtreecommitdiff
path: root/app/ui
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-27 17:03:57 -0400
committerhistoria <historiavg@proton.me>2026-08-27 17:03:57 -0400
commitcef2352a5e81b272d067c2c02eb9588e54edfcfd (patch)
tree83af416a82d9e854f44b6f3207eb8ea648b0896d /app/ui
parent6527240aa69a08f06e36e796818721abeec9f592 (diff)
downloadtts-audiobook-generator-cef2352a5e81b272d067c2c02eb9588e54edfcfd.tar.gz
feat: automatic updates added to configure backend menu
Diffstat (limited to 'app/ui')
-rw-r--r--app/ui/hub.py69
1 files changed, 64 insertions, 5 deletions
diff --git a/app/ui/hub.py b/app/ui/hub.py
index 9c1bd97..a57ab6c 100644
--- a/app/ui/hub.py
+++ b/app/ui/hub.py
@@ -154,7 +154,9 @@ class _Hub:
separated from the rest by a blank line. The remaining actions are
populated from the detected statuses: configure each configurable
backend (qwen offers its per-model weight (un)installer there),
- install (backends with nothing on disk), and uninstall.
+ install (backends with nothing on disk), update (every installed
+ backend refreshed to the latest upstream version in one task-view
+ run), and uninstall.
Selecting one pushes the next screen; Esc pops back to the main
menu. The Build action downloads any missing models alongside the
build (a split view), so it heals a configured-but-unbuilt backend
@@ -202,13 +204,16 @@ class _Hub:
for info in installed if _configurable(info)]
if any(_installable(info, by_key) for info in REGISTRY):
options.append(("Install Backend", "install"))
+ if any(_updatable(info, by_key) for info in REGISTRY):
+ options.append(("Update backends", "update"))
if any(_uninstallable(info, by_key) for info in REGISTRY):
options.append(("Uninstall Backend", "uninstall"))
choice = tui.menu(
self.stdscr, "Configure backends", options,
back_value=tui.Wizard.BACK,
- help_lines=["Install, configure, or remove a TTS backend."],
+ help_lines=["Install, update, configure, or remove a TTS "
+ "backend."],
table_title="Backend status",
table_rows=_status_rows(statuses),
notice_lines=_notice_lines())
@@ -216,6 +221,10 @@ class _Hub:
return tui.Wizard.BACK
if choice == "install":
return self.screen_install
+ if choice == "update":
+ _update_backends_action(self.stdscr)
+ invalidate_detect_cache()
+ continue # an inline action: re-show this same menu
if choice == "uninstall":
return self.screen_uninstall
if choice == "download_models":
@@ -644,6 +653,16 @@ def _uninstallable(info, by_key: dict) -> bool:
return status is not None and status.installed
+def _updatable(info, by_key: dict) -> bool:
+ """True when the "Update backends" action has something to do for INFO.
+
+ The same on-disk predicate as _uninstallable — update acts on exactly
+ what uninstall removes (the pip package / the checkout) — plus the
+ backend must implement an update action at all.
+ """
+ return info.update is not None and _uninstallable(info, by_key)
+
+
def _download_models_action(stdscr) -> None:
"""Run the "Download Missing Models (audio.cpp)" action inside the TUI.
@@ -695,6 +714,45 @@ def _download_models_action(stdscr) -> None:
"err")
+def _update_backends_action(stdscr) -> None:
+ """Run the "Update backends" action inside the TUI.
+
+ One task-view step per installed backend that implements update, in
+ registry order; each update stops its managed server first (best-
+ effort) and then refreshes — pip install -U for the pip backends,
+ git fetch + hard reset for the checkouts, with audio.cpp's binary
+ rebuilt when its checkout moved. A failing backend's step is marked
+ [FAIL] and the remaining backends still update (the run's exit code
+ is the first failure). A flash summarizes the result; the status
+ table re-detects when the menu re-shows.
+ """
+ statuses = detect_all()
+ by_key = {st.key: st for st in statuses}
+ targets = [info for info in REGISTRY if _updatable(info, by_key)]
+ if not targets:
+ tui.flash(stdscr, "No installed backend supports updating.")
+ return
+
+ def make_work(info):
+ def work(emit, cancel):
+ return info.update(emit=emit, cancel=cancel)
+ return work
+
+ steps = [taskview.TaskStep(f"Update {info.label}", make_work(info))
+ for info in targets]
+ rc = taskview.run_steps(stdscr, "Update backends", steps)
+ if rc == 0:
+ tui.flash(stdscr, "Every backend is up to date (or just "
+ "updated).", "ok")
+ elif rc == 130:
+ tui.flash(stdscr, "Update cancelled — re-run 'Update backends' "
+ "any time.", "warn")
+ else:
+ tui.flash(stdscr, "Some updates did not complete (failed or "
+ "cancelled) — see the log above. Re-run 'Update "
+ "backends' to retry.", "err")
+
+
def _status_mark(status: Optional[BackendStatus]) -> Tuple[str, str, str]:
"""Map a backend's state to (status_text, status_kind, name_kind).
@@ -705,8 +763,9 @@ def _status_mark(status: Optional[BackendStatus]) -> Tuple[str, str, str]:
"running [local, remote]". Otherwise a backend set up only part-way
(``status.partial``) shows that label verbatim (amber), e.g. audio.cpp's
"downloaded (not built)" or "built (not configured)"; 'installed'
- (orange/warn) when the backend is present on disk; or 'unavailable'
- (red/err). A backend that is neither installed nor running is unusable,
+ (green/ok) when the backend is present on disk — amber (warn) instead
+ when its models are missing — or 'unavailable' (red/err). A backend
+ that is neither installed nor running is unusable,
so its name is dimmed (NAME_KIND). A multi-model backend (qwen) also
names which models
answered in parentheses, e.g. "running [local, remote] (Base,
@@ -734,7 +793,7 @@ def _status_mark(status: Optional[BackendStatus]) -> Tuple[str, str, str]:
if status is not None and status.installed:
if status.models_missing and not status.running:
return ("installed (models missing)", "warn", "body")
- return ("installed", "warn", "body")
+ return ("installed", "ok", "body")
return ("unavailable", "err", "dim")