#!/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[@]}"}