aboutsummaryrefslogtreecommitdiff
path: root/producer.sh
diff options
context:
space:
mode:
Diffstat (limited to 'producer.sh')
-rwxr-xr-xproducer.sh186
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[@]}"}