diff options
| author | historia <historiavg@proton.me> | 2026-09-01 14:32:05 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-09-01 14:32:05 -0400 |
| commit | 6cfcd564c0684c52618235e6366f4a81c02b9a5b (patch) | |
| tree | 55321760a8103bc6b5d79489fac4135a60e6e3ba /app/backends/audiocpp | |
| parent | dc6e7cd43029da62dabe2513fb5aa8a34df1bd6d (diff) | |
| download | tts-audiobook-generator-6cfcd564c0684c52618235e6366f4a81c02b9a5b.tar.gz | |
slop refactor/dedup
Diffstat (limited to 'app/backends/audiocpp')
| -rw-r--r-- | app/backends/audiocpp/catalog.py | 16 | ||||
| -rw-r--r-- | app/backends/audiocpp/configsync.py | 17 | ||||
| -rw-r--r-- | app/backends/audiocpp/models.py | 90 | ||||
| -rw-r--r-- | app/backends/audiocpp/remote.py | 1 | ||||
| -rw-r--r-- | app/backends/audiocpp/status.py | 34 | ||||
| -rw-r--r-- | app/backends/audiocpp/wizard.py | 2 |
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, |
