#!/usr/bin/env bash set -euo pipefail # Transcribe an audio file using faster-whisper with automatic setup. # - Creates Python venv in .venv # - Installs ffmpeg and espeak-ng (best-effort) for test audio generation # - Installs faster-whisper (and CUDA stack if NVIDIA is present) # - Runs tools/transcribe_fw.py to produce .txt and .srt next to the input SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$SCRIPT_DIR" TOOLS_DIR="$PROJECT_DIR/tools" PY_RUNNER="$TOOLS_DIR/transcribe_fw.py" PY_HELPERS="$TOOLS_DIR/transcribe_helpers.py" VENV_DIR="$PROJECT_DIR/.venv" usage() { cat << USAGE Usage: $(basename "$0") [--online] [--prepare-model NAME --model-dir DIR] [-m model] [-l lang] [-o outdir] [audio_file] Options: --online Allow network to install deps and/or download models (default: offline) --prepare-model NAME Download a model for offline use (implies --online) --model-dir DIR Directory to store or load local models (default: ./models) -m model Model size or path (tiny, base, small, medium, large-v3, etc.). Default: large-v3 -l lang Language code (e.g., en). Default: auto-detect -o outdir Output directory (default: alongside input) [env] FW_DIARIZE=1 Enable diarization (speaker labels). Optional: FW_NUM_SPEAKERS=N. When --online, installs soundfile, speechbrain, and CPU-only torch/torchaudio. -h Show help USAGE } log() { echo "[$(date +'%H:%M:%S')]" "$@" } detect_pkg_mgr() { if command -v apt-get > /dev/null 2>&1; then echo apt return fi if command -v dnf > /dev/null 2>&1; then echo dnf return fi if command -v yum > /dev/null 2>&1; then echo yum return fi if command -v pacman > /dev/null 2>&1; then echo pacman return fi if command -v zypper > /dev/null 2>&1; then echo zypper return fi echo none } has_libcublas12() { # Common system locations for d in \ /usr/lib \ /usr/lib64 \ /usr/local/cuda/lib64 \ /usr/local/cuda-12*/lib64 \ /opt/cuda/lib64 \ /opt/cuda/targets/x86_64-linux/lib; do if [[ -e "$d/libcublas.so.12" ]]; then return 0 fi done # venv-provided NVIDIA CUDA libs if [[ -x "$VENV_DIR/bin/python" ]]; then local pyver pyver="$("$VENV_DIR"/bin/python "$PY_HELPERS" python-version 2> /dev/null || true)" if [[ -n $pyver ]]; then for d in "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib" \ "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib" \ "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib"; do if [[ -e "$d/libcublas.so.12" ]]; then return 0 fi done fi fi return 1 } ensure_cuda_runtime() { local mgr mgr="$(detect_pkg_mgr)" if [[ $OFFLINE -eq 1 ]]; then if has_libcublas12; then return 0; fi echo "CUDA runtime (libcublas.so.12) not found and offline mode is enabled. Install CUDA 12 runtime or rerun with --online." >&2 exit 6 fi if has_libcublas12; then return 0 fi if ! command -v sudo > /dev/null 2>&1; then log "sudo not found; skipping CUDA runtime install attempt." else log "CUDA cuBLAS 12 not found; attempting to install CUDA runtime (manager: $mgr)" set +e case "$mgr" in pacman) sudo pacman -Sy --noconfirm cuda cudnn || true ;; apt) sudo apt-get update -y || true sudo apt-get install -y nvidia-cuda-toolkit || true ;; dnf | yum) sudo "$mgr" install -y cuda cudnn || true ;; zypper) sudo zypper install -y cuda cudnn || true ;; *) log "Unknown package manager; cannot install CUDA automatically." ;; esac set -e fi # Re-check if ! has_libcublas12; then echo "CUDA runtime (libcublas.so.12) not found after attempted install. Please install CUDA 12 toolkit/runtime and re-run." >&2 exit 6 fi } install_system_deps() { have_cmd() { command -v "$1" > /dev/null 2>&1; } local need_ffmpeg=0 need_espeak=0 have_cmd ffmpeg || need_ffmpeg=1 have_cmd espeak-ng || need_espeak=1 # If diarization requested and online, we may also try to ensure libsndfile local need_libsndfile=0 if [[ ${FW_DIARIZE:-} == "1" ]]; then # Heuristic: check common library file if [[ ! -e /usr/lib/x86_64-linux-gnu/libsndfile.so && ! -e /usr/lib/libsndfile.so && ! -e /usr/lib64/libsndfile.so ]]; then need_libsndfile=1 fi fi if [[ $need_ffmpeg -eq 0 && $need_espeak -eq 0 && $need_libsndfile -eq 0 ]]; then log "System deps present: ffmpeg, espeak-ng${FW_DIARIZE:+, libsndfile}" return 0 fi if [[ $OFFLINE -eq 1 ]]; then echo "Missing system dependencies (ffmpeg/espeak-ng) but running in offline mode. Install them or rerun with --online." >&2 exit 5 fi local mgr mgr="$(detect_pkg_mgr)" log "Detected package manager: $mgr (installing missing: $([[ $need_ffmpeg -eq 1 ]] && echo ffmpeg)$([[ $need_espeak -eq 1 ]] && echo espeak-ng)$([[ $need_libsndfile -eq 1 ]] && echo libsndfile))" if ! command -v sudo > /dev/null 2>&1; then log "sudo not found; skipping system package installation attempt." return 0 fi # Avoid exiting on install errors; continue best-effort set +e case "$mgr" in apt) sudo apt-get update -y || log "apt-get update failed; continuing" pkgs=(python3-venv python3-pip) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) if [[ $need_libsndfile -eq 1 ]]; then # Try both names across releases pkgs+=(libsndfile1) sudo apt-get install -y libsndfile1 || true # If that failed, try libsndfile2 (newer distros) sudo apt-get install -y libsndfile2 || true fi sudo apt-get install -y "${pkgs[@]}" || log "apt-get install failed; continuing" ;; dnf) pkgs=(python3-venv python3-pip) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) sudo dnf install -y "${pkgs[@]}" || log "dnf install failed; continuing" ;; yum) pkgs=(python3-venv python3-pip) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) sudo yum install -y "${pkgs[@]}" || log "yum install failed; continuing" ;; pacman) pkgs=(python-virtualenv python-pip) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) sudo pacman -Sy --noconfirm "${pkgs[@]}" || log "pacman install failed; continuing" ;; zypper) pkgs=(python311-virtualenv python311-pip) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile1) sudo zypper install -y "${pkgs[@]}" || log "zypper install failed; continuing" ;; *) log "Unknown package manager; please ensure ffmpeg and espeak-ng are installed." ;; esac set -e } setup_venv() { if [[ ! -d $VENV_DIR ]]; then log "Creating venv at $VENV_DIR" python3 -m venv "$VENV_DIR" fi # shellcheck disable=SC1091 source "$VENV_DIR/bin/activate" if [[ $OFFLINE -eq 0 ]]; then python -m pip install --upgrade pip wheel setuptools fi } install_python_deps() { # Install deps; if NVIDIA GPU is present, prefer CUDA-capable stack (cu12) local has_nvidia_flag="${1:-0}" log "Installing faster-whisper and dependencies" export PIP_DISABLE_PIP_VERSION_CHECK=1 export PIP_DEFAULT_TIMEOUT=${PIP_DEFAULT_TIMEOUT:-20} if [[ $OFFLINE -eq 1 ]]; then # Offline: do not install, just verify modules if ! python "$PY_HELPERS" check-faster-whisper; then exit 7 fi # If diarization requested offline, check for its deps too (warn-only) if [[ ${FW_DIARIZE:-} == "1" ]]; then python "$PY_HELPERS" check-diarization || true fi return 0 fi if [[ $has_nvidia_flag -eq 1 ]]; then # If ctranslate2 is not installed, attempt CUDA-enabled wheel (with fallback) if ! "$VENV_DIR/bin/python" "$PY_HELPERS" check-ctranslate2 2> /dev/null; then log "Installing CUDA-enabled CTranslate2 (cu12 wheel)" python -m pip install --progress-bar on --retries 1 --upgrade "ctranslate2<5,>=4.0" --extra-index-url https://download.opennmt.net/ctranslate2/cu12 || log "Warning: could not reach cu12 wheel index; will proceed with available ctranslate2" fi # Ensure NVIDIA CUDA 12 runtime libs are available inside the venv python -m pip install --progress-bar on --retries 1 --upgrade nvidia-cublas-cu12 nvidia-cuda-runtime-cu12 nvidia-cudnn-cu12 || log "Warning: failed to install NVIDIA cu12 runtime libs via pip" fi python -m pip install --progress-bar on --retries 1 --upgrade faster-whisper ffmpeg-python # If diarization requested and online, install its Python deps best-effort if [[ ${FW_DIARIZE:-} == "1" ]]; then python -m pip install --progress-bar on --retries 1 --upgrade soundfile speechbrain || log "Warning: failed to install soundfile/speechbrain" # Torch and torchaudio CPU wheels (force to avoid mismatched CUDA builds) python -m pip install --progress-bar on --retries 1 --upgrade --force-reinstall --index-url https://download.pytorch.org/whl/cpu torch torchaudio || log "Warning: failed to install torch/torchaudio CPU wheels" fi python "$PY_HELPERS" deps-installed } ensure_runner() { if [[ ! -f $PY_RUNNER ]]; then echo "Runner not found: $PY_RUNNER" >&2 exit 3 fi } generate_test_audio() { local tmpwav tmpwav="${PROJECT_DIR}/test_fw.wav" if command -v espeak-ng > /dev/null 2>&1; then log "Generating test audio via espeak-ng -> $tmpwav" >&2 espeak-ng -w "$tmpwav" "This is a quick test of faster whisper transcription." > /dev/null 2>&1 || true fi # If espeak-ng failed or not present, try espeak if [[ ! -s $tmpwav ]] && command -v espeak > /dev/null 2>&1; then log "espeak-ng unavailable or failed; trying espeak -> $tmpwav" >&2 espeak -w "$tmpwav" "This is a quick test of faster whisper transcription." > /dev/null 2>&1 || true fi # Fallback: generate tone via Python stdlib (no external deps) if [[ ! -s $tmpwav ]]; then log "Generating 3s 1kHz WAV via Python stdlib -> $tmpwav" >&2 python3 "$PY_HELPERS" generate-wav --file "$tmpwav" || true fi # Final fallback: tone via ffmpeg if [[ ! -s $tmpwav ]]; then log "Creating a 3s sine tone WAV via ffmpeg -> $tmpwav" >&2 ffmpeg -f lavfi -i sine=frequency=1000:duration=3 -ar 16000 -ac 1 -f wav -y "$tmpwav" > /dev/null 2>&1 || true fi echo "$tmpwav" } prepare_model() { # Download a model for offline use into MODEL_DIR local name="$1" mkdir -p "$MODEL_DIR" # shellcheck disable=SC1091 source "$VENV_DIR/bin/activate" log "Preparing model '$name' into $MODEL_DIR" python "$PY_HELPERS" prepare-model --model "$name" --model-dir "$MODEL_DIR" } main() { # Defaults OFFLINE=1 PREPARE_MODEL="" MODEL_DIR="$PROJECT_DIR/models" MODEL="large-v3" LANGUAGE="" OUTDIR="" INPUT_FILE="" # Parse args PARSED=$(getopt -o m:l:o:h -l online,prepare-model:,model-dir: -- "$@") || { usage exit 2 } eval set -- "$PARSED" while true; do case "$1" in -m) MODEL="$2" shift 2 ;; -l) LANGUAGE="$2" shift 2 ;; -o) OUTDIR="$2" shift 2 ;; -h) usage exit 0 ;; --online) OFFLINE=0 shift ;; --prepare-model) PREPARE_MODEL="$2" OFFLINE=0 shift 2 ;; --model-dir) MODEL_DIR="$2" shift 2 ;; --) shift break ;; *) break ;; esac done INPUT_FILE="${1:-}" if [[ $OFFLINE -eq 1 ]]; then export HF_HUB_OFFLINE=1 export TRANSFORMERS_OFFLINE=1 fi install_system_deps setup_venv # If asked to prepare a model, do that and exit if [[ -n $PREPARE_MODEL ]]; then if [[ $OFFLINE -eq 1 ]]; then echo "--prepare-model requires network; rerun with --online." >&2 exit 2 fi install_python_deps 0 prepare_model "$PREPARE_MODEL" log "Model '$PREPARE_MODEL' downloaded to $MODEL_DIR" exit 0 fi # Detect NVIDIA GPU and enforce CUDA if present has_nvidia=0 if command -v nvidia-smi > /dev/null 2>&1 && nvidia-smi -L > /dev/null 2>&1; then has_nvidia=1 fi install_python_deps "$has_nvidia" ensure_runner local input="$INPUT_FILE" if [[ -z $input ]]; then input="$(generate_test_audio)" if [[ ! -s $input ]]; then echo "Failed to generate test audio. Please provide an audio file." >&2 exit 4 fi fi if [[ ! -f $input ]]; then echo "Input file not found: $input" >&2 exit 2 fi local args=("$input" "--model" "$MODEL") [[ -n $LANGUAGE ]] && args+=("--language" "$LANGUAGE") [[ -n $OUTDIR ]] && args+=("--outdir" "$OUTDIR") # Pass diarization via env if requested if [[ ${FW_DIARIZE:-} == "1" ]]; then args+=("--diarize") if [[ -n ${FW_NUM_SPEAKERS:-} ]]; then args+=("--num-speakers" "${FW_NUM_SPEAKERS}") fi fi if [[ $has_nvidia -eq 1 ]]; then ensure_cuda_runtime # Export common CUDA paths in case the env lacks them export CUDA_HOME="${CUDA_HOME:-/usr/local/cuda}" # Include system and possible venv-provided CUDA libs local pyver venv_cuda_paths="" if [[ -x "$VENV_DIR/bin/python" ]]; then pyver="$("$VENV_DIR"/bin/python "$PY_HELPERS" python-version 2> /dev/null || true)" if [[ -n $pyver ]]; then venv_cuda_paths="$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib:$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib:$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib" fi fi export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}:${CUDA_HOME}/lib64:/usr/lib/x86_64-linux-gnu:/opt/cuda/lib64:/opt/cuda/targets/x86_64-linux/lib:${venv_cuda_paths}" export PATH="${PATH}:${CUDA_HOME}/bin" # shellcheck disable=SC1091 source "$VENV_DIR/bin/activate" python "$PY_HELPERS" test-cuda || { echo "CUDA environment check failed. Aborting as requested." >&2 exit 6 } args+=("--device" "cuda") fi log "Transcribing: $input" # shellcheck disable=SC1091 source "$VENV_DIR/bin/activate" if [[ $has_nvidia -eq 1 ]]; then if ! python "$PY_RUNNER" "${args[@]}"; then echo "CUDA execution requested due to detected NVIDIA GPU, but it failed. Aborting as requested (no CPU fallback)." >&2 exit 6 fi else # Offline: prefer local directory if present; otherwise use cache without network if [[ $OFFLINE -eq 1 ]]; then local local_model_path="" if [[ -d $MODEL ]]; then local_model_path="$MODEL" elif [[ -d "$MODEL_DIR/$MODEL" ]]; then local_model_path="$MODEL_DIR/$MODEL" fi if [[ -n $local_model_path ]]; then args=("$input" "--model" "$local_model_path") [[ -n $LANGUAGE ]] && args+=("--language" "$LANGUAGE") [[ -n $OUTDIR ]] && args+=("--outdir" "$OUTDIR") fi fi python "$PY_RUNNER" "${args[@]}" fi } main "$@"