Files
Gabriel Brown 89761a7da3 Keep the personal half of the desktop in one place, and ask before installing it
Agent instructions, skills, SSH host aliases and expansion triggers are worth
having identical on every machine one person owns, and belong in none of the
shared configuration. They live in user/ now, with a manifest saying where each
piece goes and a link-user stage that puts it there.

That stage does nothing unless the machine said yes. Somebody who clones Panama
to try the desktop keeps their own ~/.claude/CLAUDE.md exactly where it was;
the question names the destinations and defaults to no. Anything displaced goes
to config/old rather than being deleted.

~/.claude/CLAUDE.md and ~/.codex/AGENTS.md were byte-identical copies of one
file, which is the drift this exists to prevent.

Also adds the vitals toggles for the battery and Claude usage readouts, which
had preferences and no way to reach them.
2026-08-22 08:54:43 -04:00

129 lines
4.3 KiB
Bash
Executable File

#!/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 <media-file> <output-dir> [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:
# <output-dir>/<basename>.transcript.txt
# <output-dir>/<basename>.transcript.srt
set -euo pipefail
usage() {
echo "Usage: $(basename "$0") <media-file> <output-dir> [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"