diff options
Diffstat (limited to 'app/backends/audiocpp/wizard.py')
| -rw-r--r-- | app/backends/audiocpp/wizard.py | 220 |
1 files changed, 165 insertions, 55 deletions
diff --git a/app/backends/audiocpp/wizard.py b/app/backends/audiocpp/wizard.py index 6d2d33a..5b237c4 100644 --- a/app/backends/audiocpp/wizard.py +++ b/app/backends/audiocpp/wizard.py @@ -24,6 +24,7 @@ from ui import taskview, tui from . import build as _build from . import configsync as _configsync from . import models as _models +from . import prebuilt as _prebuilt from . import voices as _voices from .catalog import (_backend_options, build_model_entry, build_server_config, detect_backend, @@ -35,6 +36,22 @@ from .constants import (AUDIOCPP_DIR_NAME, AUDIOCPP_GIT_URL, BACKENDS, _GO_BACK = object() +def _flag_build_mode(args: argparse.Namespace, backend: str) -> str: + """Resolve the ``--prebuilt`` flag into a concrete build mode. + + ``auto`` (the default) downloads the prebuilt release when this + platform/backend has one and otherwise builds from source; ``yes`` + forces the download, ``no`` forces the source build. Only consulted + when a build/install is actually pending. + """ + choice = getattr(args, "prebuilt", "auto") + if choice == "no": + return "source" + if choice == "yes" or _prebuilt.prebuilt_supported(backend): + return "prebuilt" + return "source" + + class _GoBack(Exception): """Internal signal: Esc was pressed inside one of a screen's sub-prompts. @@ -285,6 +302,7 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser "port": port, "backend": backend, "build": build, + "build_mode": s.get("build_mode"), "lazy_load": True, "sync_port": None, "wav_dir": s["wav_dir"], @@ -351,9 +369,25 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser # the build row — honor that by re-checking at apply time. if s["backend"] is None: s["backend"] = result["backend"] - s["build"] = bool(result.get("build")) and ( - _build.built_server_binary(s["audiocpp_dir"], - s["backend"]) is None) + if _build.built_server_binary(s["audiocpp_dir"], + s["backend"]) is not None: + s["build"] = False + s["build_mode"] = None + else: + # The form reports every field's value, including hidden + # ones, so the mode is chosen by whether this backend has + # a prebuilt asset at all — not by which field was shown. + # Only the visible field's answer is meaningful: the + # choice field when a prebuilt asset exists, otherwise the + # plain build question. + if _prebuilt.prebuilt_supported(s["backend"]): + mode = result.get("build_mode") + if mode not in ("prebuilt", "source", "skip"): + mode = "prebuilt" + else: + mode = "source" if bool(result.get("build")) else "skip" + s["build_mode"] = mode + s["build"] = mode in ("prebuilt", "source") # Clone-voice directory: only meaningful for clone-capable picks. if args.input_dir is not None: @@ -409,16 +443,21 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser if args.build_backend is not None: s["backend"] = args.build_backend s["build"] = s["detected_backend"] is None + s["build_mode"] = _flag_build_mode(args, s["backend"]) \ + if s["build"] else None elif args.backend is not None: s["backend"] = args.backend s["build"] = False + s["build_mode"] = None elif s["detected_backend"] is not None: # Already built: use the detected backend, no menu, no build. s["backend"] = s["detected_backend"] s["build"] = False + s["build_mode"] = None else: s["backend"] = None # decided by the form s["build"] = None + s["build_mode"] = None # Clone-voice directory seed: the voice_dir recorded by the # server.json being modified, else the project voices/ dir (the @@ -452,10 +491,30 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser "kind": "choice", "value": default_backend, "choices": options, }) + # How to get the binary: macOS and Windows have upstream + # release binaries (no toolchain needed — see + # backends.audiocpp.prebuilt), everything else builds from + # source. HIP on Windows has no release asset, so it keeps + # the plain build question. + fields.append({ + "key": "build_mode", + "label": "Get audiocpp_server", + "kind": "choice", "value": "prebuilt", + "choices": [ + ("Download prebuilt server (recommended)", "prebuilt"), + ("Build from source", "source"), + ("Skip for now", "skip"), + ], + "visible": lambda fs: ( + needs_build(fs) and _prebuilt.prebuilt_supported( + _field_val(fs, "backend", default_backend))), + }) fields.append({ "key": "build", "label": "Build audiocpp_server now?", "kind": "bool", "value": True, - "visible": needs_build, + "visible": lambda fs: ( + needs_build(fs) and not _prebuilt.prebuilt_supported( + _field_val(fs, "backend", default_backend))), }) wav_field = { @@ -514,15 +573,15 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser audiocpp_dir = _build.find_local_checkout() if audiocpp_dir is None: target = APP_DIR / AUDIOCPP_DIR_NAME + # The ggml build patches are deliberately NOT applied here: they + # belong to the source-build path (build_audiocpp applies them + # right before building), and a prebuilt install checks out the + # release tag, which the patches may not fit. rc = taskview.run_steps(stdscr, "Clone audio.cpp", [ taskview.TaskStep( f"Cloning audio.cpp into {target}", lambda emit, cancel: common.git_clone( AUDIOCPP_GIT_URL, target, emit=emit, cancel=cancel)), - taskview.TaskStep( - "Apply ggml build patches", - lambda emit, cancel: _build.apply_ggml_patches( - target, emit=emit, cancel=cancel)), ]) if rc == 130: # Cancelled from the task view: abort the wizard quietly. @@ -544,10 +603,12 @@ def _execute_lanes(settings: dict, The same work ``_execute`` runs on the console, split into two lanes so the view can run the build in one pane while configuring and downloading - models in the other (both progress bars visible at once). The build lane - exists only when ``settings["build"]`` is set; the models lane always - exists (transcribe → write server.json → download/print commands). - Shared results (the transcription mapping) travel through a small closure + models in the other (both progress bars visible at once). The install + lane exists only when ``settings["build"]`` is set: it downloads the + prebuilt release (``settings["build_mode"] == "prebuilt"``) or builds + from source, per the user's choice. The models lane always exists + (transcribe → write server.json → download/print commands). Shared + results (the transcription mapping) travel through a small closure dict scoped to the models lane. Each step's ``work(emit, cancel)`` returns its exit code; subprocess steps stream through EMIT and abort on CANCEL, while print()-based steps are captured by the view's stdout @@ -559,25 +620,44 @@ def _execute_lanes(settings: dict, lanes: List[taskview.TaskLane] = [] if build: - def build_step(emit, cancel): - rc = _build.build_audiocpp(audiocpp_dir, settings["backend"], - emit=emit, cancel=cancel) - if rc == 124: - print("[WARNING] build went silent and was stopped; the " - "server.json was still written — build " - "audiocpp_server manually before starting it") - elif rc != 0: - print(f"[WARNING] build exited with code {rc}; the server.json " - "was still written — build audiocpp_server manually " - "before starting it") - else: - print("[OK] build complete") - return rc + mode = settings.get("build_mode") or "source" + if mode == "prebuilt": + def build_step(emit, cancel): + rc = _prebuilt.install_prebuilt( + audiocpp_dir, settings["backend"], + emit=emit, cancel=cancel) + if rc == 130: + return rc + if rc != 0: + print(f"[WARNING] prebuilt download failed (exit {rc}); " + "the server.json was still written — build " + "audiocpp_server from source ('Build audio.cpp " + "Server', or re-run with --prebuilt no) before " + "starting it") + else: + print("[OK] prebuilt audiocpp_server installed") + return rc + build_title = (f"Download prebuilt audiocpp_server " + f"({settings['backend']})") + else: + def build_step(emit, cancel): + rc = _build.build_audiocpp(audiocpp_dir, settings["backend"], + emit=emit, cancel=cancel) + if rc == 124: + print("[WARNING] build went silent and was stopped; the " + "server.json was still written — build " + "audiocpp_server manually before starting it") + elif rc != 0: + print(f"[WARNING] build exited with code {rc}; the " + "server.json was still written — build " + "audiocpp_server manually before starting it") + else: + print("[OK] build complete") + return rc + build_title = f"Build audiocpp_server ({settings['backend']})" lanes.append(taskview.TaskLane( "Build", - [taskview.TaskStep( - f"Build audiocpp_server ({settings['backend']})", - build_step)])) + [taskview.TaskStep(build_title, build_step)])) def transcribe(emit, cancel): args.input_dir = settings["wav_dir"] @@ -703,18 +783,22 @@ def setup_screen(stdscr) -> int: def build_screen(stdscr) -> int: - """Build audiocpp_server from the hub when the checkout has no binary. - - Asks which backend to build for (pre-selecting the backend an existing - server.json records, else cuda), runs the build inside the TUI task view - — alongside a download of any missing models when server.json is already - configured and those models map to an install command (the split view), - or just the build otherwise — then updates server.json's ``backend`` - field to match. Returns 0 on success, non-zero when the user backed out, - cancelled, or the build failed. This is the hub's "Build audio.cpp - server" action, so a checkout that was cloned but never built is always - buildable from the TUI; the standalone "Download Missing Models" action - stays as the fallback when the download fails or is interrupted. + """Install audiocpp_server from the hub when the checkout has none. + + Asks which backend to install for (pre-selecting the backend an + existing server.json records, else cuda), then — where a prebuilt + release asset exists (macOS and Windows, see + ``backends.audiocpp.prebuilt``) — whether to download it or build + from source. The chosen action runs inside the TUI task view — + alongside a download of any missing models when server.json is + already configured and those models map to an install command (the + split view), or alone otherwise — then updates server.json's + ``backend`` field to match. Returns 0 on success, non-zero when the + user backed out, cancelled, or the install failed. This is the hub's + "Build audio.cpp Server" action, so a checkout that was cloned but + never built is always installable from the TUI; the standalone + "Download Missing Models" action stays as the fallback when a model + download fails or is interrupted. """ checkout = _build.find_local_checkout() if checkout is None: @@ -736,12 +820,28 @@ def build_screen(stdscr) -> int: if backend is _GO_BACK: return 1 + mode = "source" + if _prebuilt.prebuilt_supported(backend): + mode = tui.menu( + stdscr, "Install audiocpp_server:", + [("Download prebuilt server from GitHub releases " + "(recommended)", "prebuilt"), + ("Build from source", "source")], + default_index=0, back_value=_GO_BACK) + if mode is _GO_BACK: + return 1 + def build_step(emit, cancel): - return _build.build_audiocpp(checkout, backend, emit=emit, cancel=cancel) + if mode == "prebuilt": + return _prebuilt.install_prebuilt(checkout, backend, + emit=emit, cancel=cancel) + return _build.build_audiocpp(checkout, backend, emit=emit, + cancel=cancel) - lanes = [taskview.TaskLane( - "Build", [taskview.TaskStep( - f"Build audiocpp_server ({backend})", build_step)])] + action = "Download prebuilt audiocpp_server" if mode == "prebuilt" \ + else f"Build audiocpp_server ({backend})" + lanes = [taskview.TaskLane("Build", [taskview.TaskStep(action, + build_step)])] # Missing models this build can also fetch, so a configured backend that # lost its binary is restored to "installed" in one step. @@ -758,13 +858,16 @@ def build_screen(stdscr) -> int: "Download models", [taskview.TaskStep("Download missing models", download_step)])) - title = "Build & download models" if len(lanes) == 2 \ - else "Build audiocpp_server" + install_title = ("Download prebuilt audiocpp_server" + if mode == "prebuilt" else "Build audiocpp_server") + title = f"{install_title} & download models" if len(lanes) == 2 \ + else install_title rc = taskview.run_lanes(stdscr, title, lanes) if rc != 0: return rc if not _configsync.update_server_backend(backend): - tui.flash(stdscr, f"audiocpp_server built for {backend}. (Could not " + verb = "installed" if mode == "prebuilt" else "built" + tui.flash(stdscr, f"audiocpp_server {verb} for {backend}. (Could not " "update server.json's backend field — reconfigure audio.cpp " "if it was already configured.)", "warn") # Models that can't be mapped to an install command still need hand @@ -818,6 +921,8 @@ def _collect_from_flags(args: argparse.Namespace, default-location fallback then also exists). """ # Checkout: ./app/audio.cpp, else --clone clones one there. + # (The ggml build patches are applied by build_audiocpp itself, so a + # prebuilt install never needs them.) audiocpp_dir = _build.find_local_checkout() if audiocpp_dir is None and args.clone: target = APP_DIR / AUDIOCPP_DIR_NAME @@ -825,13 +930,6 @@ def _collect_from_flags(args: argparse.Namespace, if rc != 0: parser.error(f"git clone failed (exit {rc}); clone audio.cpp " f"manually: git clone {AUDIOCPP_GIT_URL} {target}") - patch_rc = _build.apply_ggml_patches(target) - if patch_rc != 0: - parser.error( - f"ggml build patches could not be applied to {target} " - f"(exit {patch_rc}); see messages above. The audio.cpp " - f"fork's vendored ggml may have changed — re-evaluate " - f"app/backends/patches/.") audiocpp_dir = target if audiocpp_dir is None: parser.error( @@ -895,6 +993,10 @@ def _collect_from_flags(args: argparse.Namespace, else: backend = "cuda" build = False + # How a pending install happens: the prebuilt release by default on + # macOS/Windows (``--prebuilt no`` forces the source build), always + # the source build elsewhere. + build_mode = _flag_build_mode(args, backend) if build else None port = _configsync.config_port() lazy_load = True @@ -936,6 +1038,7 @@ def _collect_from_flags(args: argparse.Namespace, "port": port, "backend": backend, "build": build, + "build_mode": build_mode, "lazy_load": lazy_load, "sync_port": None, "wav_dir": wav_dir, @@ -981,6 +1084,13 @@ def build_parser() -> argparse.ArgumentParser: parser.add_argument("--build-backend", choices=BACKENDS, default=None, help="Build audiocpp_server for this backend when it " "is not built yet, and use it in server.json") + parser.add_argument("--prebuilt", choices=("auto", "yes", "no"), + default="auto", + help="How to install audiocpp_server when it is " + "missing: auto downloads the prebuilt release " + "on macOS/Windows and builds from source " + "elsewhere; yes forces the prebuilt download; " + "no forces a source build") parser.add_argument("--whisper-model", type=str, default="base", help="Whisper model size for transcription " "(default: base)") |
