From cef2352a5e81b272d067c2c02eb9588e54edfcfd Mon Sep 17 00:00:00 2001 From: historia Date: Thu, 27 Aug 2026 17:03:57 -0400 Subject: feat: automatic updates added to configure backend menu --- app/backends/audiocpp/__init__.py | 6 +- app/backends/audiocpp/build.py | 114 +++++++++++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 4 deletions(-) (limited to 'app/backends/audiocpp') diff --git a/app/backends/audiocpp/__init__.py b/app/backends/audiocpp/__init__.py index b97032b..f55cf35 100644 --- a/app/backends/audiocpp/__init__.py +++ b/app/backends/audiocpp/__init__.py @@ -10,7 +10,8 @@ Modules: models install state on disk, missing-model guidance, downloads voices reference-.wav transcription planning and execution configsync app/converter/config.py + server.json port/id/backend sync - build checkout lifecycle: ggml patches, binary build, uninstall + build checkout lifecycle: ggml patches, binary build, update, + uninstall remote querying a running server for its models/voices status detect() for the hub's backend menu wizard the TUI wizard and the CLI entry points @@ -66,6 +67,7 @@ from .build import ( find_local_checkout, find_audiocpp_server_bin, uninstall, + update, ) from .remote import fetch_server_models, fetch_server_voices from .wizard import ( @@ -99,7 +101,7 @@ __all__ = [ "update_server_backend", # build "find_local_checkout", "find_audiocpp_server_bin", "find_build_script", - "apply_ggml_patches", "build_audiocpp", "uninstall", + "apply_ggml_patches", "build_audiocpp", "uninstall", "update", # remote "fetch_server_models", "fetch_server_voices", # wizard / status diff --git a/app/backends/audiocpp/build.py b/app/backends/audiocpp/build.py index e63a799..b850859 100644 --- a/app/backends/audiocpp/build.py +++ b/app/backends/audiocpp/build.py @@ -11,8 +11,8 @@ from typing import List, Optional from backends import common, servers from backends.common import APP_DIR -from .catalog import _BACKEND_TOKEN_RE -from .constants import AUDIOCPP_DIR_NAME, PATCH_DIR +from .catalog import _BACKEND_TOKEN_RE, detect_backend, load_server_config +from .constants import AUDIOCPP_DIR_NAME, BACKENDS, PATCH_DIR def uninstall(*, emit=None, cancel=None) -> int: """Remove the audio.cpp backend entirely: stop its server, delete the checkout. @@ -47,6 +47,116 @@ def uninstall(*, emit=None, cancel=None) -> int: return 0 +def update(*, emit=None, cancel=None) -> int: + """Update the audio.cpp backend: refresh the checkout, rebuild if stale. + + A managed server that is running is stopped first (best-effort): it + serves the binary whose sources are being replaced. Phases: stop + server / git update / rebuild — CANCEL is honored between phases only, + so a started phase always completes. The git update is a fetch plus + hard reset to origin's HEAD (see ``common.git_update``): everything + that matters lives untracked in the checkout (models, build trees, + server.json) and survives, while the vendored-ggml patch edit is + intentionally wiped — the rebuild re-applies it (the patch step is + idempotent and fails loudly when upstream re-shaped the file). + + The rebuild target is the backend recorded in server.json, else the + one detected from existing build directories; when neither names one + (nothing was ever built) the update stops after the checkout refresh + — 'Build audio.cpp server' handles a first build. The rebuild itself + runs when the sources changed (HEAD moved) or the on-disk binary is + missing or older than HEAD's commit time — the latter heals an + interrupted (cancelled or failed) earlier rebuild, which leaves the + previous binary in place against already-updated sources. An + up-to-date checkout with a fresh binary costs one fetch. Returns the + exit code (130 when cancelled before a remaining phase). + """ + # Only stop when a pid file exists: without one this tool never + # started the server, so the "not started by this tool" notice would + # be uninstall-time noise. + if servers.pid_for("audiocpp") is not None: + 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 update.") + return 0 + head_before = common.git_head(checkout) + rc = common.git_update(checkout, emit=emit, cancel=cancel) + if rc != 0: + print(f"[WARNING] checkout update failed (exit {rc}); update " + f"manually: git -C {checkout} pull") + return rc + head_after = common.git_head(checkout) + if common.cancel_requested(cancel): + return 130 + backend = _rebuild_backend(checkout) + if backend is None: + print("[INFO] audiocpp_server was never built for a known " + "backend; skipping the rebuild. 'Build audio.cpp server' " + "builds one.") + return 0 + binary = built_server_binary(checkout, backend) + if not _rebuild_needed(checkout, binary, + moved=head_after not in (None, head_before)): + print(f"[OK] {checkout} is already at origin's HEAD with an " + "up-to-date audiocpp_server.") + return 0 + if head_after in (None, head_before): + print(f"[INFO] audiocpp_server on disk is older than the " + f"checked-out sources (earlier build interrupted?); " + f"rebuilding for {backend}.") + else: + print(f"[OK] Updated {checkout} to {head_after[:12]}; rebuilding " + f"audiocpp_server for {backend}.") + build_rc = build_audiocpp(checkout, backend, emit=emit, cancel=cancel) + if build_rc != 0: + print(f"[WARNING] rebuild exited with code {build_rc}; see the " + "messages above (the build log under app/logs/ has the " + "full output). The binary on disk is now older than the " + "checked-out sources; re-running 'Update backends' will " + "retry the rebuild.") + else: + print("[OK] rebuild complete.") + return build_rc + + +def _rebuild_needed(checkout: Path, binary: Optional[Path], + *, moved: bool) -> bool: + """True when audiocpp_server must be (re)built after an update. + + True when the checkout moved, the binary is missing, its age cannot + be compared (no commit time), or it predates HEAD's commit — the + last case is what a cancelled or failed earlier rebuild leaves + behind (old binary, already-updated sources). + """ + if moved or binary is None: + return True + commit_time = common.git_commit_time(checkout) + if commit_time is None: + return True + try: + return binary.stat().st_mtime <= commit_time + except OSError: + return True + + +def _rebuild_backend(checkout: Path) -> Optional[str]: + """The inference backend to rebuild for after an update, or None. + + server.json's recorded backend wins (it is what the managed server + launches); an existing build directory's token is the fallback for a + checkout that was built but never configured. None means neither + names a valid backend — there is no binary to keep fresh. + """ + server_config = load_server_config(checkout / "server.json") or {} + recorded = server_config.get("backend") + if recorded in BACKENDS: + return recorded + return detect_backend(checkout) + + def find_local_checkout() -> Optional[Path]: """Return the managed audio.cpp checkout at ``app/audio.cpp``. -- cgit v1.2.3