aboutsummaryrefslogtreecommitdiff
path: root/app/backends/audiocpp.py
diff options
context:
space:
mode:
Diffstat (limited to 'app/backends/audiocpp.py')
-rwxr-xr-xapp/backends/audiocpp.py101
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]: