#!/usr/bin/env bash # Transcribes a video/audio file using whisper.cpp in a podman container, using # hardware acceleration when it's available on the host: # - NVIDIA GPU present -> ghcr.io/ggml-org/whisper.cpp:main-cuda (CDI nvidia device) # - /dev/dri + vainfo -> ghcr.io/ggml-org/whisper.cpp:main-vulkan (AMD/Intel, Vulkan) # - neither -> ghcr.io/ggml-org/whisper.cpp:main (CPU) # # If the accelerated container fails to run (missing CDI setup, no such image tag on # this whisper.cpp release, etc.) this script automatically falls back to the CPU # image rather than failing outright, and says so on stderr. # # Usage: transcribe.sh [model] # model defaults to "base.en" (small, fast, English). Pass e.g. "small" for a # multilingual model, or "medium.en" for higher accuracy. # On a multi-GPU AMD/Intel host, set WHISPER_VULKAN_DEVICE to the desired render # node, e.g. WHISPER_VULKAN_DEVICE=/dev/dri/renderD128. # # Writes: # /.transcript.txt # /.transcript.srt set -euo pipefail usage() { echo "Usage: $(basename "$0") [model=base.en]" >&2 exit 1 } [ $# -ge 2 ] || usage MEDIA="$1" OUT_DIR="$2" MODEL="${3:-base.en}" [ -f "$MEDIA" ] || { echo "No such file: $MEDIA" >&2; exit 1; } command -v podman >/dev/null 2>&1 || { echo "podman is required for transcription" >&2; exit 1; } command -v curl >/dev/null 2>&1 || { echo "curl is required to fetch the whisper model" >&2; exit 1; } MEDIA="$(cd "$(dirname "$MEDIA")" && pwd)/$(basename "$MEDIA")" BASENAME="$(basename "${MEDIA%.*}")" WORKDIR="$(mktemp -d)" trap 'rm -rf "$WORKDIR"' EXIT WAV="$WORKDIR/audio.wav" echo "Extracting audio..." >&2 if command -v ffmpeg >/dev/null 2>&1; then ffmpeg -y -i "$MEDIA" -ar 16000 -ac 1 -c:a pcm_s16le "$WAV" -loglevel error else echo "ffmpeg not found on host, extracting via podman instead" >&2 podman run --rm \ -v "$(dirname "$MEDIA")":/in:Z \ -v "$WORKDIR":/out:Z \ jrottenberg/ffmpeg:6-alpine \ -y -i "/in/$(basename "$MEDIA")" -ar 16000 -ac 1 -c:a pcm_s16le /out/audio.wav -loglevel error fi MODEL_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/ticket-skill/whisper-models" mkdir -p "$MODEL_DIR" MODEL_FILE="$MODEL_DIR/ggml-${MODEL}.bin" if [ ! -f "$MODEL_FILE" ]; then echo "Downloading whisper model '${MODEL}' (first run only, cached under ${MODEL_DIR})..." >&2 curl -sSL -o "$MODEL_FILE" "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-${MODEL}.bin" fi ACCEL="cpu" IMAGE="ghcr.io/ggml-org/whisper.cpp:main" DEVICE_ARGS=() if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then ACCEL="cuda" IMAGE="ghcr.io/ggml-org/whisper.cpp:main-cuda" DEVICE_ARGS=(--device nvidia.com/gpu=all) elif [ -d /dev/dri ] && command -v vainfo >/dev/null 2>&1; then ACCEL="vulkan" IMAGE="ghcr.io/ggml-org/whisper.cpp:main-vulkan" if [ -n "${WHISPER_VULKAN_DEVICE:-}" ]; then VULKAN_DEVICE="$(readlink -f "$WHISPER_VULKAN_DEVICE")" [ -e "$VULKAN_DEVICE" ] || { echo "WHISPER_VULKAN_DEVICE does not exist: $WHISPER_VULKAN_DEVICE" >&2 exit 1 } case "$VULKAN_DEVICE" in /dev/dri/renderD*) ;; *) echo "WHISPER_VULKAN_DEVICE must resolve to /dev/dri/renderD*: $VULKAN_DEVICE" >&2 exit 1 ;; esac DEVICE_ARGS=(--device "$VULKAN_DEVICE") else DEVICE_ARGS=(--device /dev/dri) fi fi echo "Selected acceleration: ${ACCEL} (image: ${IMAGE})" >&2 run_whisper() { local image="$1" shift podman run --rm "$@" \ --entrypoint /app/build/bin/whisper-cli \ -v "$WORKDIR":/data:Z \ -v "$MODEL_DIR":/models:Z \ "$image" \ -m "/models/ggml-${MODEL}.bin" -f /data/audio.wav -otxt -osrt -of /data/transcript } SUCCESS=0 if [ "$ACCEL" != "cpu" ]; then if run_whisper "$IMAGE" "${DEVICE_ARGS[@]}"; then SUCCESS=1 else echo "Accelerated transcription (${ACCEL}) failed, falling back to CPU." >&2 fi fi if [ "$SUCCESS" -eq 0 ]; then run_whisper "ghcr.io/ggml-org/whisper.cpp:main" fi [ -f "$WORKDIR/transcript.txt" ] || { echo "Transcription did not produce output" >&2; exit 1; } mkdir -p "$OUT_DIR" cp "$WORKDIR/transcript.txt" "$OUT_DIR/${BASENAME}.transcript.txt" [ -f "$WORKDIR/transcript.srt" ] && cp "$WORKDIR/transcript.srt" "$OUT_DIR/${BASENAME}.transcript.srt" echo "Wrote ${OUT_DIR}/${BASENAME}.transcript.txt"