aboutsummaryrefslogtreecommitdiff
path: root/app/backends/audiocpp/build.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-28 18:18:47 -0400
committerhistoria <historiavg@proton.me>2026-08-28 18:18:47 -0400
commit1487796d74a7b171f2c53c4212e446759d3a4bec (patch)
treebe0546fe36404a7bc6b98c1a472af574f4b28868 /app/backends/audiocpp/build.py
parente66eb0e7d4342ae1c58e9bbd341843753be548f0 (diff)
downloadtts-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.py120
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":