From 0cc01d1da0a629e104202053feb0bb0db91d578d Mon Sep 17 00:00:00 2001 From: historia Date: Tue, 25 Aug 2026 03:42:28 -0400 Subject: feat(tui): simultaneous build and model download --- app/backends/audiocpp.py | 144 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 107 insertions(+), 37 deletions(-) (limited to 'app/backends') diff --git a/app/backends/audiocpp.py b/app/backends/audiocpp.py index 9cb8e24..f95216e 100755 --- a/app/backends/audiocpp.py +++ b/app/backends/audiocpp.py @@ -891,7 +891,7 @@ def _decide_download(audiocpp_dir: Path, return False return confirm( "Automatically download the selected models with model_manager_v2.py " - "now?", False) + "now?", True) def _build_tree_families(catalog: List[dict]) -> List[dict]: @@ -1981,17 +1981,26 @@ def _build_audiocpp_tui(emit, cancel, argv: List[str], command: str, return rc -def _print_launch_hint(audiocpp_dir: Path, output_path: Path) -> None: +def _print_launch_hint(audiocpp_dir: Path, output_path: Path, + pending_build: bool = False) -> None: """Print the exact command to start the server (or build guidance). The command is prefixed with ``cd &&`` because the server discovers model_specs/.json relative to its working directory. + PENDING_BUILD is True when the server is still building in a parallel + lane; instead of "build it first" remediation the hint then names the + command to run once that build finishes. """ binary = find_audiocpp_server_bin(audiocpp_dir) print() if binary is not None: print("Start the server with:") print(f" cd {audiocpp_dir} && {binary} --config {output_path}") + elif pending_build: + print("The server is still building in the other panel — once it " + "finishes, start it with:") + print(f" cd {audiocpp_dir} && ./build/--release" + f"/bin/audiocpp_server --config {output_path}") else: print("[INFO] audiocpp_server binary not found. Build it first, e.g.:") script = find_build_script(audiocpp_dir) @@ -2002,23 +2011,30 @@ def _print_launch_hint(audiocpp_dir: Path, output_path: Path) -> None: f"-release/bin/audiocpp_server --config {output_path}") -def _execute_steps(settings: dict, - args: argparse.Namespace) -> List[taskview.TaskStep]: - """Build the ordered setup steps for the in-TUI task view. - - The same work ``_execute`` runs on the console, split into named steps so - the view can show per-step state (build / transcribe / write / download) - and progress. Shared results (the transcription mapping) travel through a - small closure dict. 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 redirect. +def _execute_lanes(settings: dict, + args: argparse.Namespace, + parallel: bool = False) -> List[taskview.TaskLane]: + """Build the ordered setup steps for the in-TUI task view, per lane. + + 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 + 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 + routing. PARALLEL marks the launch hint as concurrent-with-build so it + does not claim the binary is missing while the build is still running. """ audiocpp_dir = settings["audiocpp_dir"] state: dict = {} - steps: List[taskview.TaskStep] = [] + build = settings.get("build") + lanes: List[taskview.TaskLane] = [] - if settings.get("build"): - def build(emit, cancel): + if build: + def build_step(emit, cancel): rc = build_audiocpp(audiocpp_dir, settings["backend"], emit=emit, cancel=cancel) if rc != 0: @@ -2028,8 +2044,11 @@ def _execute_steps(settings: dict, else: print("[OK] build complete") return rc - steps.append(taskview.TaskStep( - f"Build audiocpp_server ({settings['backend']})", build)) + lanes.append(taskview.TaskLane( + "Build", + [taskview.TaskStep( + f"Build audiocpp_server ({settings['backend']})", + build_step)])) def transcribe(emit, cancel): args.input_dir = settings["wav_dir"] @@ -2045,7 +2064,6 @@ def _execute_steps(settings: dict, state["transcripts"] = transcripts state["write_prompt"] = write_prompt return 0 - steps.append(taskview.TaskStep("Transcribe reference voices", transcribe)) def write(emit, cancel): # Port sync (applied now that the terminal is back). @@ -2074,17 +2092,36 @@ def _execute_steps(settings: dict, settings["sync_model_ids"]) print_empty_transcript_warning(state["transcripts"]) return 0 - steps.append(taskview.TaskStep("Write server.json & sync config", write)) def install(emit, cancel): _install_models(audiocpp_dir, settings["install_guidance"], settings["download"], emit=emit, cancel=cancel) - _print_launch_hint(audiocpp_dir, settings["output_path"]) + _print_launch_hint(audiocpp_dir, settings["output_path"], + pending_build=bool(build and parallel)) return 0 install_title = "Download models" if settings.get("download") \ else "Print model install commands" - steps.append(taskview.TaskStep(install_title, install)) + lanes.append(taskview.TaskLane( + "Configure & download", + [taskview.TaskStep("Transcribe reference voices", transcribe), + taskview.TaskStep("Write server.json & sync config", write), + taskview.TaskStep(install_title, install)])) + + return lanes + + +def _execute_steps(settings: dict, + args: argparse.Namespace) -> List[taskview.TaskStep]: + """The ordered setup steps for the sequential console path. + + The lanes ``_execute_lanes`` builds, flattened into one ordered list + (build first, then transcribe → write → download), so the console tail + is byte-identical to the pre-lanes behavior. + """ + steps: List[taskview.TaskStep] = [] + for lane in _execute_lanes(settings, args): + steps.extend(lane.steps) return steps @@ -2104,30 +2141,38 @@ def setup_screen(stdscr) -> int: The hub drives this as one screen of its own ``tui.Wizard`` stack, so Esc on the wizard's first screen simply returns here and the hub pops - back to the menu that launched it. The setup tail (build/transcribe/ - write/download) runs inside the TUI task view on this same screen, so + back to the menu that launched it. The setup tail (build, transcribe, + write, download) runs inside the TUI task view on this same screen, so the hub's curses session stays intact and the user sees per-step status - and progress instead of being dropped to the console. Returns 0 on - completion, 1 when the user aborted. + and progress instead of being dropped to the console. On a fresh install + the build and the model setup run as two parallel lanes (a split view), + so cloning → configuring → building+downloading is one continuous, + one-click flow; the individual "Build" and "Download Missing Models" hub + actions remain only as fallbacks when something fails or is interrupted. + Returns 0 on completion, 1 when the user aborted. """ parser = build_parser() args = parser.parse_args([]) settings = _wizard(stdscr, args, parser) if settings is None: return 1 - return taskview.run_steps(stdscr, "Setting up audio.cpp", - _execute_steps(settings, args)) + return taskview.run_lanes(stdscr, "Setting up audio.cpp", + _execute_lanes(settings, args, parallel=True)) 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, - 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. + 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. """ checkout = find_local_checkout() if checkout is None: @@ -2148,11 +2193,32 @@ def build_screen(stdscr) -> int: "for?", options, default_index=default, back_value=_GO_BACK) if backend is _GO_BACK: return 1 - rc = taskview.run_steps(stdscr, "Build audiocpp_server", [ - taskview.TaskStep( - f"Build audiocpp_server ({backend})", - lambda emit, cancel: build_audiocpp( - checkout, backend, emit=emit, cancel=cancel))]) + + def build_step(emit, cancel): + return build_audiocpp(checkout, backend, emit=emit, cancel=cancel) + + lanes = [taskview.TaskLane( + "Build", [taskview.TaskStep( + f"Build audiocpp_server ({backend})", build_step)])] + + # Missing models this build can also fetch, so a configured backend that + # lost its binary is restored to "installed" in one step. + server_json = checkout / "server.json" + missing = missing_model_entries(server_json) if server_json.exists() else [] + guidance = missing_model_install_guidance(checkout, missing) \ + if missing else [] + + if guidance: + def download_step(emit, cancel): + install_models(checkout, guidance, emit=emit, cancel=cancel) + return 0 + lanes.append(taskview.TaskLane( + "Download models", + [taskview.TaskStep("Download missing models", download_step)])) + + title = "Build & download models" if len(lanes) == 2 \ + else "Build audiocpp_server" + rc = taskview.run_lanes(stdscr, title, lanes) if rc != 0: return rc if update_server_backend(backend): @@ -2161,6 +2227,10 @@ def build_screen(stdscr) -> int: tui.flash(stdscr, f"audiocpp_server built 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 + # installation; say so now rather than leaving the user in the dark. + if missing and not guidance: + tui.flash(stdscr, hand_install_guidance(checkout, missing), "err") return 0 -- cgit v1.2.3