diff options
| author | historia <historiavg@proton.me> | 2026-08-28 18:18:47 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-28 18:18:47 -0400 |
| commit | 1487796d74a7b171f2c53c4212e446759d3a4bec (patch) | |
| tree | be0546fe36404a7bc6b98c1a472af574f4b28868 /app/backends/audiocpp/build.py | |
| parent | e66eb0e7d4342ae1c58e9bbd341843753be548f0 (diff) | |
| download | tts-audiobook-generator-1487796d74a7b171f2c53c4212e446759d3a4bec.tar.gz | |
feat: offer to download prebuilt mac/win binaries, new macos build fallback if xcode is not installed
Diffstat (limited to 'app/backends/audiocpp/build.py')
| -rw-r--r-- | app/backends/audiocpp/build.py | 120 |
1 files changed, 111 insertions, 9 deletions
diff --git a/app/backends/audiocpp/build.py b/app/backends/audiocpp/build.py index 22d2fa1..f840193 100644 --- a/app/backends/audiocpp/build.py +++ b/app/backends/audiocpp/build.py @@ -14,6 +14,7 @@ import logging_kit from backends import common, servers from backends.common import APP_DIR +from . import prebuilt as _prebuilt from .catalog import _BACKEND_TOKEN_RE, detect_backend, load_server_config from .constants import AUDIOCPP_DIR_NAME, BACKENDS, PATCH_DIR @@ -54,14 +55,19 @@ def update(*, emit=None, cancel=None) -> int: """Update the audio.cpp backend: refresh the checkout, rebuild if stale. A managed server that is running is stopped first (best-effort): it - serves the binary whose sources are being replaced. Phases: stop - server / git update / rebuild — CANCEL is honored between phases only, - so a started phase always completes. The git update is a fetch plus - hard reset to origin's HEAD (see ``common.git_update``): everything - that matters lives untracked in the checkout (models, build trees, - server.json) and survives, while the vendored-ggml patch edit is - intentionally wiped — the rebuild re-applies it (the patch step is - idempotent and fails loudly when upstream re-shaped the file). + serves the binary whose sources are being replaced. Prebuilt installs + (a ``prebuilt.json`` marker next to the binary, see + ``backends.audiocpp.prebuilt``) take a different route: they skip the + git update and rebuild entirely and instead re-download when upstream + published a newer release (see ``_update_prebuilt``). Source-built + checkouts keep the original flow — phases: stop server / git update / + rebuild — CANCEL is honored between phases only, so a started phase + always completes. The git update is a fetch plus hard reset to origin's + HEAD (see ``common.git_update``): everything that matters lives + untracked in the checkout (models, build trees, server.json) and + survives, while the vendored-ggml patch edit is intentionally wiped — + the rebuild re-applies it (the patch step is idempotent and fails + loudly when upstream re-shaped the file). The rebuild target is the backend recorded in server.json, else the one detected from existing build directories; when neither names one @@ -85,6 +91,9 @@ def update(*, emit=None, cancel=None) -> int: if checkout is None: print("[INFO] No audio.cpp checkout to update.") return 0 + backend = _rebuild_backend(checkout) + if _prebuilt.installed_release(checkout, backend) is not None: + return _update_prebuilt(checkout, backend, emit=emit, cancel=cancel) head_before = common.git_head(checkout) rc = common.git_update(checkout, emit=emit, cancel=cancel) if rc != 0: @@ -94,7 +103,6 @@ def update(*, emit=None, cancel=None) -> int: head_after = common.git_head(checkout) if common.cancel_requested(cancel): return 130 - backend = _rebuild_backend(checkout) if backend is None: print("[INFO] audiocpp_server was never built for a known " "backend; skipping the rebuild. 'Build audio.cpp Server' " @@ -125,6 +133,43 @@ def update(*, emit=None, cancel=None) -> int: return build_rc +def _update_prebuilt(checkout: Path, backend: Optional[str], *, + emit=None, cancel=None) -> int: + """The update route for a prebuilt (release-downloaded) install. + + Source builds update by rebuilding after a git pull; a prebuilt + install instead re-downloads when upstream published a newer release + (the marker's tag is compared against the latest release tag). The + recorded/detected backend selects the asset, mirroring what was + originally installed. A GitHub outage is not fatal — the installed + binary keeps working. Returns the install exit code (0 when already + current or when the check could not run). + """ + marker = _prebuilt.installed_release(checkout, backend) + release = _prebuilt.fetch_latest_release() + if release is None: + print("[WARNING] Could not check GitHub for a newer prebuilt " + "audiocpp_server; keeping the installed one (" + f"{marker.get('tag') if marker else 'unknown'}).") + return 0 + tag = str(release.get("tag_name") or "") + if marker and tag and tag == marker.get("tag"): + print(f"[OK] Prebuilt audiocpp_server is current ({tag}).") + return 0 + print(f"[INFO] audio.cpp {tag} is available; downloading the prebuilt " + f"audiocpp_server ({backend})...") + rc = _prebuilt.install_prebuilt(checkout, backend, emit=emit, + cancel=cancel) + if rc == 0: + print(f"[OK] audiocpp_server updated to {tag}.") + else: + print(f"[WARNING] prebuilt update failed (exit {rc}); the " + "installed audiocpp_server was left in place. Retry " + "'Update Backends' later, or build from source with " + "'Build audio.cpp Server'.") + return rc + + def _rebuild_needed(checkout: Path, binary: Optional[Path], *, moved: bool) -> bool: """True when audiocpp_server must be (re)built after an update. @@ -464,6 +509,49 @@ def apply_ggml_patches(audiocpp_dir: Path, *, emit=None, cancel=None) -> int: return 0 +def _metal_compiler_available() -> bool: + """Whether Xcode's offline Metal shader compiler is installed. + + ``build_metal.sh`` hard-requires it (``xcrun --find metal``), but the + compiler ships only with full Xcode — Command Line Tools report + ``unable to find utility "metal"``. When it is missing the darwin + build falls back to direct cmake with ``GGML_METAL_EMBED_LIBRARY=ON``: + the ggml Metal build then only embeds the shader *source* and macOS's + built-in Metal runtime compiles it on first GPU init, so Apple's + Command Line Tools (clang) plus cmake are enough. + """ + proc = common.run_console_subprocess_quiet( + ["xcrun", "--sdk", "macosx", "--find", "metal"], timeout=30) + return proc is not None and proc.returncode == 0 + + +def _darwin_cmake_script() -> str: + """The one-line cmake invocation replacing build_metal.sh's two steps. + + Mirrors the script's defaults for our target (RelWithDebInfo, OpenMP + off, llamafile/native-CPU on, deployment build on) plus + ``GGML_METAL_EMBED_LIBRARY=ON`` — the flag that makes the build + independent of the offline Metal compiler (see + ``_metal_compiler_available``). The build directory is the same + ``build/macos-metal-release`` the script uses, so backend detection + (``-metal-`` -> ``cpu``) is unaffected. Relative paths: the command + runs with cwd = the checkout. + """ + build_dir = "build/macos-metal-release" + jobs = os.cpu_count() or 4 + return ( + "cmake -S . -B " + build_dir + " -DCMAKE_BUILD_TYPE=RelWithDebInfo" + " -DENGINE_ENABLE_CUDA=OFF -DENGINE_ENABLE_VULKAN=OFF" + " -DENGINE_ENABLE_METAL=ON -DENGINE_ENABLE_OPENMP=OFF" + " -DGGML_OPENMP=OFF -DENGINE_ENABLE_LLAMAFILE=ON" + " -DENGINE_ENABLE_NATIVE_CPU=ON" + " -DGGML_METAL_EMBED_LIBRARY=ON" + " -DAUDIOCPP_DEPLOYMENT_BUILD=ON" + f" && cmake --build {build_dir} --parallel {jobs}" + " --target audiocpp_server" + ) + + def build_audiocpp(audiocpp_dir: Path, backend: str, *, emit=None, cancel=None) -> int: """Build audiocpp_server for BACKEND, streaming output. @@ -476,6 +564,12 @@ def build_audiocpp(audiocpp_dir: Path, backend: str, *, Windows (presets ``windows-{cuda,vulkan,cpu}-release``), and ``build_linux.sh`` under bash everywhere else. + macOS exception: when Xcode's offline Metal compiler is missing (only + full Xcode ships it), ``build_metal.sh`` would abort at its probe — + the build instead runs cmake directly with the Metal shaders embedded + as source (see ``_darwin_cmake_script``), so the Apple Command Line + Tools plus cmake are enough. + With EMIT None the build script runs on the console (inherits the terminal); with EMIT given (the in-TUI task view) its output streams line by line to EMIT so the view can show progress, and CANCEL aborts it. @@ -509,6 +603,14 @@ def build_audiocpp(audiocpp_dir: Path, backend: str, *, else: argv += ["-Preset", f"windows-{backend}-release", "-Target", "audiocpp_server", "-DeploymentBuild"] + elif sys.platform == "darwin" and not _metal_compiler_available(): + say = emit if emit is not None else print + say("[INFO] Xcode's offline Metal compiler is not installed; " + "building with cmake directly (Apple Command Line Tools " + "suffice — Metal shaders are compiled by macOS at runtime on " + "first use, adding a short delay to the first model load; " + "install cmake, e.g. `brew install cmake`).") + argv = ["bash", "-c", _darwin_cmake_script()] else: argv = ["bash", str(script)] if sys.platform != "darwin": |
