diff options
| author | historia <historiavg@proton.me> | 2026-08-24 17:37:34 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-24 17:37:34 -0400 |
| commit | d950fc8e64ee508334e608f6045d687d73a464be (patch) | |
| tree | 87e5539b486c7f15ffba53bbba6ef6bb3a02540e /app/backends/audiocpp.py | |
| parent | 919544c0931d53bb81904b6212ff14f856549da3 (diff) | |
| download | tts-audiobook-generator-d950fc8e64ee508334e608f6045d687d73a464be.tar.gz | |
feat: tui backend server progress and generate script progress
Diffstat (limited to 'app/backends/audiocpp.py')
| -rwxr-xr-x | app/backends/audiocpp.py | 101 |
1 files changed, 93 insertions, 8 deletions
diff --git a/app/backends/audiocpp.py b/app/backends/audiocpp.py index ee0ee21..a502c8f 100755 --- a/app/backends/audiocpp.py +++ b/app/backends/audiocpp.py @@ -1276,6 +1276,78 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser } +def _model_path_present(path: Path) -> bool: + """True when a server.json model path holds actual model files. + + A present path is either a file (a single-model package) or a non-empty + directory (the usual GGUF package target directory; an empty one means a + download that never ran or was cleaned up halfway). + """ + try: + if path.is_file(): + return True + if path.is_dir(): + return any(path.iterdir()) + except OSError: + return False + return False + + +def missing_model_entries(server_json: Path) -> List[dict]: + """Return the server.json model entries whose files are not on disk. + + Paths resolve exactly like audiocpp_server resolves them (relative paths + against the server.json's directory). Each returned entry carries the + entry ``id`` and ``rel`` (the configured path string); used by ``detect`` + to warn that a conversion would fail until the models are installed. + """ + try: + data = json.loads(server_json.read_text(encoding="utf-8")) + except (OSError, ValueError): + return [] + if not isinstance(data, dict): + return [] + base = server_json.parent + missing: List[dict] = [] + for entry in data.get("models") or []: + if not isinstance(entry, dict): + continue + rel = entry.get("path") + if not isinstance(rel, str) or not rel: + continue + path = Path(rel) if Path(rel).is_absolute() else base / rel + if _model_path_present(path): + continue + missing.append({"id": str(entry.get("id") or rel), "rel": rel}) + return missing + + +def model_install_hints(audiocpp_dir: Path, + missing: List[dict]) -> List[str]: + """Remediation lines for MISSING model entries (see missing_model_entries). + + Maps each entry's configured path back to the catalog package that + installs it (``models/<target_directory>`` -> install id) so the line + carries the exact ``model_manager_v2.py install`` command; entries whose + directory matches no catalog package just name the path. + """ + by_path: Dict[str, str] = {} + try: + for entry in load_model_catalog(audiocpp_dir): + by_path[entry["default_path"]] = entry["install_id"] + except (NotADirectoryError, OSError): + pass + hints: List[str] = [] + for item in missing: + install_id = by_path.get(item["rel"]) + hint = f"model not downloaded: {item['id']} ({item['rel']})" + if install_id: + hint += (f" — install with: python tools/model_manager_v2.py " + f"install {install_id}") + hints.append(hint) + return hints + + def find_local_checkout() -> Optional[Path]: """Best-effort location of an audio.cpp checkout with model_specs. @@ -1421,20 +1493,24 @@ def build_audiocpp(audiocpp_dir: Path, backend: str) -> int: def _print_launch_hint(audiocpp_dir: Path, output_path: Path) -> None: - """Print the exact command to start the server (or build guidance).""" + """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. + """ binary = find_audiocpp_server_bin(audiocpp_dir) print() if binary is not None: print("Start the server with:") - print(f" {binary} --config {output_path}") + print(f" cd {audiocpp_dir} && {binary} --config {output_path}") else: print("[INFO] audiocpp_server binary not found. Build it first, e.g.:") script = find_build_script(audiocpp_dir) if script is not None: print(f" sh {script} --backend <cuda|vulkan|hip|cpu> " "--target audiocpp_server") - print(f" then run: ./build/<platform>-<backend>-release/bin/" - f"audiocpp_server --config {output_path}") + print(f" then run: cd {audiocpp_dir} && ./build/<platform>-<backend>" + f"-release/bin/audiocpp_server --config {output_path}") def _execute(settings: dict, args: argparse.Namespace) -> int: @@ -1767,15 +1843,23 @@ def detect() -> BackendStatus: server_json = checkout / "server.json" configured = server_json.exists() specs: List[ServerSpec] = [] + missing = missing_model_entries(server_json) if configured else [] if configured: details.append(f"config: {server_json}") + if missing: + # The config references model files that are not on disk; a + # conversion would fail at model-load time, so say so now. + details.extend(model_install_hints(checkout, missing)) if built: + # Spawned from the checkout: audiocpp_server discovers + # model_specs/<family>.json relative to its working directory. specs = [ServerSpec( "audiocpp", config.AUDIOCPP_API_URL, - [str(binary), "--config", str(server_json)])] + [str(binary), "--config", str(server_json)], + cwd=checkout, identity=probe.IDENTITY_AUDIOCPP)] else: - launch = (f"./build/<platform>-<backend>-release/bin/" - f"audiocpp_server --config {server_json}") + launch = (f"cd {checkout} && ./build/<platform>-<backend>-release" + f"/bin/audiocpp_server --config {server_json}") else: details.append("no server.json — run setup to configure models") if specs: @@ -1787,7 +1871,8 @@ def detect() -> BackendStatus: running=managed or remote_running, details=details, launch_hint=launch, servers=specs, managed=managed, - remote=remote_running, remote_urls=remote_urls) + remote=remote_running, remote_urls=remote_urls, + models_missing=bool(missing)) def _detect_remote(managed: bool = False) -> Tuple[bool, dict]: |
