diff options
| author | historia <historiavg@proton.me> | 2026-09-07 06:47:47 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-09-07 06:47:47 -0400 |
| commit | 84dd2d068317998f6fb59400c534ef5be6b51b53 (patch) | |
| tree | 025293e9d9229e02960374771ae522d9de2628ce /producer.sh | |
| parent | 39b0f2bbed74f6487a41b82501ae3c6799e4b5c4 (diff) | |
| download | producer-84dd2d068317998f6fb59400c534ef5be6b51b53.tar.gz | |
Diffstat (limited to 'producer.sh')
| -rwxr-xr-x | producer.sh | 186 |
1 files changed, 186 insertions, 0 deletions
diff --git a/producer.sh b/producer.sh new file mode 100755 index 0000000..3c792f3 --- /dev/null +++ b/producer.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash +# Producer: the single entry point for VoiceForge. The first run bootstraps a +# fully contained runtime under lib/ (managed Python 3.13, dependencies, +# bundled FFmpeg, and later the AI environments and model weights); every run +# then executes the program from that runtime. Nothing is installed outside +# this directory: no PATH changes, no sudo, no system Python changes. +# +# Trust boundary for downloads: official PyPI (uv, application wheels) and +# Astral's managed CPython distribution. AI setup additionally trusts +# download.pytorch.org and the pinned DeepFilterNet3 model host. Host +# package-manager settings (extra indexes, targets, constraints, config +# files) cannot add to or redirect these sources. +# +# First bootstrap needs an existing `uv`, or a Python 3 with venv support. +# Later runs, including moving or renaming this whole directory, are handled +# by the launcher itself; caches make the repair fast. +set -euo pipefail + +SOURCE="${BASH_SOURCE[0]}" +while [[ -L "$SOURCE" ]]; do + DIR="$(CDPATH='' cd -- "$(dirname -- "$SOURCE")" && pwd)" + SOURCE="$(readlink -- "$SOURCE")" + [[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE" +done +ROOT="$(CDPATH='' cd -- "$(dirname -- "$SOURCE")" && pwd)" +LIB="$ROOT/lib" +RUNTIME="$LIB/runtime" +APP="$RUNTIME/app" +PROJECT="$LIB/project" +READY="$RUNTIME/.producer-ready" +# Bump when the bootstrap recipe changes (python version, install steps...). +RUNTIME_VERSION="2" + +usage() { + cat <<'USAGE' +Producer - local speech cleanup and podcast mastering (VoiceForge). + +Usage: ./producer.sh COMMAND [options] + + process RECORDING.wav... Master one or more WAV files + preview RECORDING.wav Render loudness-matched profile previews + analyze RECORDING.wav Print measurements as JSON on stdout + config Print resolved settings (TOML, or JSON with --json) + doctor Check FFmpeg, data directory and CUDA driver + setup --device cpu|cuda Install and self-test the AI backend + +Outputs are written beside each input by default. Run +'./producer.sh COMMAND --help' for the options of a command. +'./producer.sh --rebuild COMMAND...' re-runs the bootstrap first, for example +after changing the source code under lib/project. A rebuild waits for +running producer jobs to finish. +USAGE +} + +log() { printf '[producer] %s\n' "$*" >&2; } +fail() { printf '[producer] Error: %s\n' "$*" >&2; exit 1; } + +case "${1:-}" in + -h|--help) usage; exit 0 ;; +esac + +REBUILD=0 +ARGS=() +for arg in "$@"; do + case "$arg" in + --rebuild) REBUILD=1 ;; + *) ARGS+=("$arg") ;; + esac +done +# 'producer.sh --rebuild --help' asks for help, not a rebuild. +if [[ ${#ARGS[@]} -eq 1 && ( "${ARGS[0]}" == "-h" || "${ARGS[0]}" == "--help" ) ]]; then + usage + exit 0 +fi + +mkdir -p -- "$RUNTIME/cache/pip" "$RUNTIME/cache/uv" "$RUNTIME/uv" "$RUNTIME/tmp" \ + || fail "Cannot create the runtime directory under $LIB" + +# Contained environment: host Python settings and package-manager caches +# cannot leak in, and nothing (temporary files included) is written outside +# lib/runtime. Installation-target, index, and constraint overrides from the +# host are dropped and package-manager configuration files are disabled, so +# packages can only come from the explicitly passed trusted indexes and land +# inside lib/runtime. The application reads VOICEFORGE_HOME for AI envs and +# models. +unset PYTHONPATH PYTHONHOME VIRTUAL_ENV \ + PIP_TARGET PIP_PREFIX PIP_USER PIP_FIND_LINKS PIP_INDEX_URL PIP_EXTRA_INDEX_URL \ + PIP_CONSTRAINT \ + UV_INDEX_URL UV_DEFAULT_INDEX UV_EXTRA_INDEX_URL UV_INDEX UV_FIND_LINKS \ + UV_CONSTRAINT +export PYTHONNOUSERSITE=1 +export PIP_CACHE_DIR="$RUNTIME/cache/pip" UV_CACHE_DIR="$RUNTIME/cache/uv" +export PIP_CONFIG_FILE=/dev/null UV_NO_CONFIG=1 +export UV_PYTHON_INSTALL_DIR="$RUNTIME/uv/python" XDG_CACHE_HOME="$RUNTIME/cache" +export TMPDIR="$RUNTIME/tmp" VOICEFORGE_HOME="$RUNTIME" + +marker_current() { + # The marker must name this exact path, and the interpreter must exist: + # a failed or interrupted rebuild leaves neither, forcing a repair. + [[ -f "$READY" ]] \ + && [[ "$(cat -- "$READY" 2>/dev/null)" == "$RUNTIME v$RUNTIME_VERSION" ]] \ + && [[ -x "$APP/bin/python" ]] +} + +bootstrap() { + command -v flock >/dev/null 2>&1 \ + || fail "The 'flock' utility (util-linux) is required but was not found." + # Bootstrap lock (fd 9): serializes bootstrap across launches and covers + # the whole destructive window below. It is released before the + # application starts. + exec 9>"$RUNTIME/bootstrap.lock" + flock 9 + if [[ "$REBUILD" -eq 0 ]] && marker_current; then + exec 9>&- + # Runtime lock (fd 8): held shared for the application's lifetime so + # a later --rebuild cannot delete files this process runs from. + exec 8>"$RUNTIME/app.lock" + flock -s 8 + return 0 + fi + # Exclusive runtime lock: waits out running jobs before anything is + # deleted, so a rebuild can never clobber a live environment. + exec 8>"$RUNTIME/app.lock" + if ! flock -n 8; then + log "Waiting for running producer jobs to finish before building the runtime" + flock 8 + fi + # Invalidate the marker before any destructive work: if this bootstrap + # fails or is interrupted, the next launch repairs instead of trusting + # a half-built environment. + rm -f -- "$READY" "$READY.tmp" + log "Preparing the contained runtime in lib/runtime (first run, or the directory moved)" + + local UV="" + local err="" + if [[ -x "$RUNTIME/bootstrap/bin/uv" ]] \ + && "$RUNTIME/bootstrap/bin/uv" --version >/dev/null 2>&1; then + UV="$RUNTIME/bootstrap/bin/uv" + elif command -v uv >/dev/null 2>&1; then + UV="$(command -v uv)" + else + command -v python3 >/dev/null 2>&1 \ + || fail "First-run bootstrap needs 'uv' or Python 3 with venv support; neither was found." + log "Bootstrapping uv from PyPI" + rm -rf -- "$RUNTIME/bootstrap" + python3 -m venv "$RUNTIME/bootstrap" \ + || fail "python3 -m venv failed; install venv support with your system package manager and retry. Nothing was run with sudo." + "$RUNTIME/bootstrap/bin/python" -m pip install --index-url https://pypi.org/simple 'uv==0.8.17' >/dev/null \ + || fail "Downloading uv from PyPI failed; check network access." + UV="$RUNTIME/bootstrap/bin/uv" + fi + + # Managed-Python alias links are absolute; after a move they dangle while + # the downloaded payload is still intact. Drop stale links so uv + # re-resolves what is already on disk instead of re-downloading. + find "$RUNTIME/uv" -maxdepth 1 -type l ! -exec test -e {} \; -delete 2>/dev/null || true + + rm -rf -- "$APP" + # Python 3.13 for the application runtime: earlier musl builds from uv + # misreport their extension suffix and cannot load musl-tagged wheels. + # The AI worker keeps its own pinned Python 3.11 (glibc only). + "$UV" venv --python 3.13 --managed-python --relocatable --allow-existing "$APP" \ + || fail "Creating the managed Python 3.13 environment failed; check network access and free space." + # The bundled FFmpeg wheel is preferred; where it does not exist (for + # example musl systems), fall back to a system FFmpeg at runtime. + if ! err="$("$UV" pip install --python "$APP/bin/python" --index-url https://pypi.org/simple \ + "$PROJECT[bundled-ffmpeg]" 2>&1 >/dev/null)"; then + log "Bundled FFmpeg wheel unavailable; installing core dependencies (a system FFmpeg is then required)." + if [[ -n "$err" ]]; then + sed 's/^/[producer] /' <<<"$err" >&2 + fi + "$UV" pip install --python "$APP/bin/python" --index-url https://pypi.org/simple "$PROJECT" \ + || fail "Installing the application dependencies failed; check network access and free space." + fi + "$APP/bin/python" -I -c "import voiceforge" >/dev/null 2>&1 \ + || fail "The application environment failed verification." + + printf '%s v%s\n' "$RUNTIME" "$RUNTIME_VERSION" > "$READY.tmp" + mv -f -- "$READY.tmp" "$READY" + # Downgrade to the shared runtime lock for the application's lifetime. + flock -s 8 + exec 9>&- +} + +bootstrap +exec "$APP/bin/python" -I -m voiceforge ${ARGS[@]+"${ARGS[@]}"} |
