aboutsummaryrefslogtreecommitdiff
path: root/app/backends/audiocpp
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-09-01 14:32:05 -0400
committerhistoria <historiavg@proton.me>2026-09-01 14:32:05 -0400
commit6cfcd564c0684c52618235e6366f4a81c02b9a5b (patch)
tree55321760a8103bc6b5d79489fac4135a60e6e3ba /app/backends/audiocpp
parentdc6e7cd43029da62dabe2513fb5aa8a34df1bd6d (diff)
downloadtts-audiobook-generator-6cfcd564c0684c52618235e6366f4a81c02b9a5b.tar.gz
slop refactor/dedup
Diffstat (limited to 'app/backends/audiocpp')
-rw-r--r--app/backends/audiocpp/catalog.py16
-rw-r--r--app/backends/audiocpp/configsync.py17
-rw-r--r--app/backends/audiocpp/models.py90
-rw-r--r--app/backends/audiocpp/remote.py1
-rw-r--r--app/backends/audiocpp/status.py34
-rw-r--r--app/backends/audiocpp/wizard.py2
6 files changed, 65 insertions, 95 deletions
diff --git a/app/backends/audiocpp/catalog.py b/app/backends/audiocpp/catalog.py
index 1048185..048e325 100644
--- a/app/backends/audiocpp/catalog.py
+++ b/app/backends/audiocpp/catalog.py
@@ -44,6 +44,8 @@ def request_options_families(audiocpp_dir: Path) -> Dict[str, dict]:
spec = json.loads(spec_path.read_text(encoding="utf-8"))
except (OSError, ValueError):
continue
+ if not isinstance(spec, dict):
+ continue
options = spec.get("options")
request = options.get("request") if isinstance(options, dict) else None
if not isinstance(request, list) or not request:
@@ -161,12 +163,12 @@ def _default_package(packages: List[dict]) -> Optional[dict]:
if not packages:
return None
for package in packages:
- if package.get("default"):
+ if isinstance(package, dict) and package.get("default"):
return package
for package in packages:
- if package.get("format") == "gguf":
+ if isinstance(package, dict) and package.get("format") == "gguf":
return package
- return packages[0]
+ return packages[0] if isinstance(packages[0], dict) else None
def spec_gguf_rooted(spec: dict) -> bool:
@@ -292,6 +294,11 @@ def load_model_catalog(audiocpp_dir: Path) -> List[dict]:
spec = json.loads(spec_path.read_text(encoding="utf-8"))
except (OSError, ValueError):
continue
+ if not isinstance(spec, dict):
+ # Valid JSON that is not an object (a list, a string, a
+ # number): skip it like an unparsable one instead of crashing
+ # the wizard on a half-written spec file.
+ continue
sanitize_model_spec(spec)
tasks = spec.get("tasks") or []
if tasks:
@@ -305,7 +312,8 @@ def load_model_catalog(audiocpp_dir: Path) -> List[dict]:
# No task list: fall back to the category as before.
continue
family = spec.get("family") or spec_path.stem
- packages = spec.get("packages") or []
+ packages = [package for package in (spec.get("packages") or [])
+ if isinstance(package, dict)]
package = _default_package(packages)
if package is None:
# No installable package: skip (cannot be hosted from a path).
diff --git a/app/backends/audiocpp/configsync.py b/app/backends/audiocpp/configsync.py
index bce8142..0a0e5ad 100644
--- a/app/backends/audiocpp/configsync.py
+++ b/app/backends/audiocpp/configsync.py
@@ -14,10 +14,7 @@ from .constants import FALLBACK_PORT
def config_port() -> int:
"""Return the port of AUDIOCPP_API_URL in app/converter/config.py."""
- try:
- return urllib.parse.urlsplit(config.AUDIOCPP_API_URL).port or FALLBACK_PORT
- except ValueError:
- return FALLBACK_PORT
+ return common.port_of(config.AUDIOCPP_API_URL, FALLBACK_PORT)
def update_config_api_url_port(port: int, config_path: Optional[Path] = None) -> bool:
@@ -75,18 +72,6 @@ def update_server_config_port(port: int) -> bool:
return True
-def _apply_port_sync(port: int, accepted: bool) -> None:
- """Write the port into app/converter/config.py, or report when declined."""
- if accepted:
- if not update_config_api_url_port(port):
- print(f"[WARNING] Could not update {CONFIG_PATH}; edit "
- "AUDIOCPP_API_URL by hand so audiobook.py uses the "
- "new port")
- else:
- print("[WARNING] Left AUDIOCPP_API_URL unchanged; audiobook.py "
- f"will still use port {config_port()}")
-
-
def update_server_backend(backend: str) -> bool:
"""Rewrite the 'backend' in the checkout's server.json, or True when none.
diff --git a/app/backends/audiocpp/models.py b/app/backends/audiocpp/models.py
index 16c61e9..d150f62 100644
--- a/app/backends/audiocpp/models.py
+++ b/app/backends/audiocpp/models.py
@@ -16,10 +16,6 @@ from . import catalog as _catalog
# runner kills it and reports exit 124 (see run_console_subprocess).
DOWNLOAD_STALL_TIMEOUT = 300
-# Spec sanitizing lives with the catalog (the wizard's catalog view applies
-# the same in-memory repair; the download path materializes it through its
-# sanitized specs copy). The alias keeps this module's historical name.
-_sanitize_model_spec = _catalog.sanitize_model_spec
def _installed_display_names(audiocpp_dir: Path,
model_entries: Optional[List[dict]],
@@ -255,52 +251,21 @@ def _manager_supports_progress(manager: Path) -> bool:
and _manager_supports_flag(manager, "--cancel-file"))
-def _sanitize_model_spec(spec: dict) -> bool:
- """Repair dot ``strip_prefix`` packages in SPEC, in place.
-
- A package's ``strip_prefix`` is stripped from the front of every file
- path to get the local layout, so it only works when every file is
- listed under that prefix (``<prefix>/<file>``). A dot prefix ("." or
- "./") is meant for files written ``./<file>``; when the package instead
- lists repo-root files bare (``model.gguf``), the manager rejects the
- whole package ("file path does not start with strip_prefix '.': ...")
- and nothing can be downloaded. Root-level files need no prefix at all
- (upstream specs like minimax_music3.json store ""), so dropping the dot
- prefix is the safe repair. Prefixes naming a real directory are left
- alone — the correct remote paths cannot be guessed. Returns True when
- SPEC changed.
- """
- changed = False
- for package in spec.get("packages") or []:
- if not isinstance(package, dict):
- continue
- prefix = str(package.get("strip_prefix") or "").rstrip("/")
- if prefix not in (".", ".."):
- continue
- files = package.get("files")
- if not isinstance(files, list) or not files:
- continue
- if all(isinstance(item, str) and item.startswith(prefix + "/")
- for item in files):
- continue
- package["strip_prefix"] = ""
- changed = True
- return changed
-
-
def _prepare_specs_dir(audiocpp_dir: Path) -> Optional[Path]:
- """Return a temp specs dir with dot ``strip_prefix`` entries repaired.
+ """Return a temp specs dir with broken ``strip_prefix`` entries repaired.
audio.cpp's model manager accepts ``--specs-dir``, so a checkout whose
specs carry a broken ``strip_prefix`` can be installed from a sanitized
copy without modifying the checkout. Every ``model_specs/*.json`` is
- copied; the ones needing a repair are rewritten via
- ``_sanitize_model_spec`` (specs that fail to parse are copied verbatim
- so the manager reports them exactly as it would upstream). Returns None
- when no spec needed a repair (or the specs directory is missing or
- unreadable) — the caller then runs against the checkout's own specs.
- The caller owns the returned directory and removes it when the installs
- are done.
+ copied; the ones needing a repair are rewritten via the catalog's
+ ``sanitize_model_spec`` (both upstream spec bug classes: dot prefixes
+ and missing prefixes on $gguf-rooted single-GGUF packages — glm_tts/
+ outetts shipped like that). Specs that fail to parse are copied
+ verbatim so the manager reports them exactly as it would upstream.
+ Returns None when no spec needed a repair (or the specs directory is
+ missing or unreadable) — the caller then runs against the checkout's
+ own specs. The caller owns the returned directory and removes it when
+ the installs are done.
"""
specs_dir = audiocpp_dir / "model_specs"
try:
@@ -321,7 +286,7 @@ def _prepare_specs_dir(audiocpp_dir: Path) -> Optional[Path]:
except ValueError:
payloads.append((spec_path.name, text))
continue
- if isinstance(spec, dict) and _sanitize_model_spec(spec):
+ if isinstance(spec, dict) and _catalog.sanitize_model_spec(spec):
sanitized = True
text = json.dumps(spec, indent=2, ensure_ascii=False) + "\n"
payloads.append((spec_path.name, text))
@@ -707,12 +672,15 @@ def delete_model_files(server_json: Path, entries: List[dict]) -> int:
Each entry's ``rel`` is resolved exactly like the server resolves it
(relative against ``server_json``'s directory; absolute paths honored),
- then removed as a directory tree or a single file. Missing entries are
- ignored. Returns the number of paths removed. Used by the wizard's
+ then removed as a directory tree or a single file. Paths that escape
+ the checkout (``..`` segments, or an absolute path outside the
+ checkout's tree) are refused rather than deleted — the value comes
+ from a user-editable server.json. Missing entries are ignored.
+ Returns the number of paths removed. Used by the wizard's
"Delete unused models?" step — the regenerated server.json already only
lists the kept models, so no entry cleanup is needed here.
"""
- base = server_json.parent
+ base = server_json.parent.resolve()
removed = 0
for item in entries:
rel = item.get("rel")
@@ -720,16 +688,28 @@ def delete_model_files(server_json: Path, entries: List[dict]) -> int:
continue
path = Path(rel) if Path(rel).is_absolute() else base / rel
try:
- if not path.exists():
+ resolved = path.resolve()
+ except OSError:
+ continue
+ if base not in resolved.parents and resolved != base:
+ print(f"[WARNING] Refusing to remove {path}: outside the "
+ "audio.cpp checkout")
+ continue
+ if resolved == base:
+ print(f"[WARNING] Refusing to remove {path}: it is the "
+ "checkout directory itself")
+ continue
+ try:
+ if not resolved.exists():
continue
- if path.is_dir():
- shutil.rmtree(path, ignore_errors=True)
+ if resolved.is_dir():
+ shutil.rmtree(resolved, ignore_errors=True)
else:
- path.unlink()
+ resolved.unlink()
except OSError as exc:
- print(f"[WARNING] Could not remove {path}: {exc}")
+ print(f"[WARNING] Could not remove {resolved}: {exc}")
continue
- print(f"[OK] Removed unused model {path}")
+ print(f"[OK] Removed unused model {resolved}")
removed += 1
return removed
diff --git a/app/backends/audiocpp/remote.py b/app/backends/audiocpp/remote.py
index 31eddbf..20d4cef 100644
--- a/app/backends/audiocpp/remote.py
+++ b/app/backends/audiocpp/remote.py
@@ -1,6 +1,7 @@
"""Query a running audiocpp_server for its models and voices."""
import json
+import urllib.parse
import urllib.request
from typing import Dict, List, Optional
diff --git a/app/backends/audiocpp/status.py b/app/backends/audiocpp/status.py
index 9ccf8cc..a687066 100644
--- a/app/backends/audiocpp/status.py
+++ b/app/backends/audiocpp/status.py
@@ -54,16 +54,20 @@ def detect() -> BackendStatus:
launch = format_launch_hint(specs)
managed = servers.manages(specs)
remote_running, remote_urls = _detect_remote(managed)
+ running = managed or remote_running
# A more specific "part-way set up" label than unavailable/installed:
- # cloned but never built, or built but not configured.
+ # cloned but never built, or built but not configured. Only meaningful
+ # while the backend is not usable (a running server makes even a
+ # non-built checkout usable remotely — see the BackendStatus docstring).
partial = ""
- if not built:
- partial = "downloaded (not built)"
- elif not configured:
- partial = "built (not configured)"
+ if not running:
+ if not built:
+ partial = "downloaded (not built)"
+ elif not configured:
+ partial = "built (not configured)"
return BackendStatus("audiocpp", "audio.cpp", installed=built,
configured=configured,
- running=managed or remote_running,
+ running=running,
details=details, launch_hint=launch,
servers=specs, managed=managed,
remote=remote_running, remote_urls=remote_urls,
@@ -73,18 +77,12 @@ def detect() -> BackendStatus:
def _detect_remote(managed: bool = False) -> Tuple[bool, dict]:
"""Detect an externally-run audiocpp_server at the remote URL.
- Returns ``(running, {spec_name: url})``. The remote URL is probed only
- when configured (non-empty); a server answering there is ignored when it
- is this tool's own managed server (remote URL == local URL and our pid is
- still alive) — that instance is already reported as "[local]".
+ Returns ``(running, {spec_name: url})``; see probe.detect_remote_url
+ for the shared semantics.
"""
- url = (config.AUDIOCPP_REMOTE_URL or "").strip()
- if not url:
- return False, {}
- if managed and probe.same_endpoint(url, config.AUDIOCPP_API_URL):
- return False, {}
- if probe.identify_server(url) == probe.IDENTITY_AUDIOCPP:
- return True, {"audiocpp": url}
- return False, {}
+ return probe.detect_remote_url(config.AUDIOCPP_REMOTE_URL,
+ config.AUDIOCPP_API_URL,
+ probe.IDENTITY_AUDIOCPP, "audiocpp",
+ managed)
diff --git a/app/backends/audiocpp/wizard.py b/app/backends/audiocpp/wizard.py
index a1a2a6a..52a4f3f 100644
--- a/app/backends/audiocpp/wizard.py
+++ b/app/backends/audiocpp/wizard.py
@@ -331,7 +331,6 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
"build_mode": s.get("build_mode"),
"prebuilt_forced": False,
"lazy_load": True,
- "sync_port": None,
"wav_dir": s["wav_dir"],
"plan": s["plan"],
"download": s["download"],
@@ -1093,7 +1092,6 @@ def _collect_from_flags(args: argparse.Namespace,
"build_mode": build_mode,
"prebuilt_forced": prebuilt_forced,
"lazy_load": lazy_load,
- "sync_port": None,
"wav_dir": wav_dir,
"plan": plan,
"download": args.download,