aboutsummaryrefslogtreecommitdiff
path: root/app/backends
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-25 03:42:28 -0400
committerhistoria <historiavg@proton.me>2026-08-25 03:42:28 -0400
commit0cc01d1da0a629e104202053feb0bb0db91d578d (patch)
tree8c52b151cc3457002043d6d499968d34349d7f5f /app/backends
parentd6460459ee95d8c2298b029fa4b2dc266e80a0cc (diff)
downloadtts-audiobook-generator-0cc01d1da0a629e104202053feb0bb0db91d578d.tar.gz
feat(tui): simultaneous build and model download
Diffstat (limited to 'app/backends')
-rwxr-xr-xapp/backends/audiocpp.py144
1 files changed, 107 insertions, 37 deletions
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 <checkout> &&`` because the server
discovers model_specs/<family>.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/<platform>-<backend>-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