#!/usr/bin/env bash # install.sh — interactive setup for `publish`. # # Detects OS + GPU, walks through: # 1. system dependencies (ffmpeg, cmake, git, go, GPU runtime) # 2. whisper.cpp checkout + build for the chosen backend # 3. ggml model download # 4. publish binary build + symlink into ~/.local/bin # # Re-runnable. Each step is idempotent and skippable. # # Pass --doctor to print detection info and exit. set -euo pipefail REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PREFIX="${PREFIX:-$HOME/.local}" BINDIR="$PREFIX/bin" MODELDIR="$HOME/.cache/whisper.cpp" WHISPER_REPO="${WHISPER_REPO:-$HOME/Git Repos/whisper.cpp}" WHISPER_GIT="https://github.com/ggerganov/whisper.cpp" MODEL_BASE_URL="https://huggingface.co/ggerganov/whisper.cpp/resolve/main" DOCTOR_ONLY=0 [[ "${1:-}" == "--doctor" ]] && DOCTOR_ONLY=1 bold() { printf '\033[1m%s\033[0m\n' "$*"; } info() { printf ' %s\n' "$*"; } warn() { printf '\033[33m warn: %s\033[0m\n' "$*" >&2; } err() { printf '\033[31m error: %s\033[0m\n' "$*" >&2; } hr() { printf -- '----------------------------------------\n'; } # Portable CPU count: nproc on Linux, sysctl on macOS, fallback 4. ncpu() { if command -v nproc >/dev/null 2>&1; then nproc elif command -v sysctl >/dev/null 2>&1; then sysctl -n hw.ncpu 2>/dev/null || echo 4 else echo 4; fi } ask_yn() { # ask_yn "prompt" default(Y|N) local prompt="$1" default="${2:-Y}" reply local hint="[Y/n]"; [[ "$default" == "N" ]] && hint="[y/N]" while true; do read -r -p " $prompt $hint " reply || true reply="${reply:-$default}" case "$reply" in [Yy]*) return 0 ;; [Nn]*) return 1 ;; esac done } ask_choice() { # ask_choice "prompt" default option1 option2 ... local prompt="$1" default="$2"; shift 2 local options=("$@") reply local opts_joined; opts_joined="$(IFS='/'; echo "${options[*]}")" while true; do read -r -p " $prompt [$opts_joined] (default: $default): " reply || true reply="${reply:-$default}" for opt in "${options[@]}"; do [[ "$reply" == "$opt" ]] && { echo "$reply"; return 0; } done warn "must be one of: $opts_joined" done } # ---------- detection ---------- detect_os() { case "$(uname -s)" in Linux*) if command -v pacman >/dev/null 2>&1; then echo "arch" elif command -v apt-get >/dev/null 2>&1; then echo "debian" elif command -v dnf >/dev/null 2>&1; then echo "fedora" else echo "linux-other"; fi ;; Darwin*) echo "macos" ;; *) echo "unknown" ;; esac } detect_gpu() { # apple silicon if [[ "$(uname -s)" == "Darwin" ]]; then [[ "$(uname -m)" == "arm64" ]] && { echo "apple"; return; } echo "none"; return fi # nvidia if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then echo "nvidia"; return fi if command -v lspci >/dev/null 2>&1; then local vga; vga="$(lspci 2>/dev/null | grep -iE 'vga|3d|display' || true)" if echo "$vga" | grep -iq 'nvidia'; then echo "nvidia"; return; fi if echo "$vga" | grep -iqE 'amd|ati|radeon'; then echo "amd"; return; fi if echo "$vga" | grep -iq 'intel'; then echo "intel"; return; fi fi echo "none" } default_backend_for() { case "$1" in nvidia) echo "cuda" ;; amd) echo "vulkan" ;; # easier to set up than rocm; user can pick rocm apple) echo "metal" ;; intel) echo "vulkan" ;; *) echo "cpu" ;; esac } OS="$(detect_os)" GPU="$(detect_gpu)" DEFAULT_BACKEND="$(default_backend_for "$GPU")" # ---------- doctor ---------- print_detection() { bold "=== publish — environment ===" info "OS: $OS ($(uname -s) $(uname -r))" info "Arch: $(uname -m)" info "GPU: $GPU" info "Default backend: $DEFAULT_BACKEND" info "Repo dir: $REPO_DIR" info "Install prefix: $PREFIX" info "Whisper repo: $WHISPER_REPO" info "Model dir: $MODELDIR" hr bold "Dependencies" local deps=(go ffmpeg cmake git curl) case "$DEFAULT_BACKEND" in cuda) deps+=(nvidia-smi nvcc) ;; rocm) deps+=(rocminfo hipcc) ;; vulkan) deps+=(vulkaninfo glslc) ;; esac case "$OS" in macos) deps+=(brew xcode-select pbcopy) ;; *) deps+=(claude wl-copy xclip) ;; esac # Known off-PATH locations for tools that linux distros tuck away. extra_path_for() { case "$1" in nvcc) echo /opt/cuda/bin/nvcc ;; hipcc) echo /opt/rocm/bin/hipcc ;; rocminfo) echo /opt/rocm/bin/rocminfo ;; *) echo "" ;; esac } for d in "${deps[@]}"; do if command -v "$d" >/dev/null 2>&1; then info "$(printf '%-14s %s' "$d:" "$(command -v "$d")")" else extra="$(extra_path_for "$d")" if [[ -n "$extra" && -x "$extra" ]]; then info "$(printf '%-14s %s' "$d:" "$extra (not on PATH)")" else info "$(printf '%-14s %s' "$d:" "MISSING")" fi fi done } print_detection if [[ $DOCTOR_ONLY -eq 1 ]]; then exit 0 fi hr echo # ---------- pick backend ---------- bold "Step 1 — pick whisper.cpp backend" echo echo " Detected default: $DEFAULT_BACKEND" echo " Options: cuda | rocm | vulkan | metal | cpu | skip" echo " cuda — NVIDIA GPU, fastest if you have one" echo " rocm — AMD GPU via ROCm/HIP, fastest on supported AMD cards" echo " vulkan — any GPU with a Vulkan driver (good cross-vendor fallback)" echo " metal — Apple Silicon" echo " cpu — no GPU, use the system whisper.cpp package" echo " skip — leave whisper.cpp alone (e.g. you've already built it)" echo BACKEND="$(ask_choice "Backend" "$DEFAULT_BACKEND" cuda rocm vulkan metal cpu skip)" # ---------- macOS preflight ---------- if [[ "$OS" == "macos" ]]; then hr bold "macOS preflight — Xcode Command Line Tools + Homebrew" echo if ! xcode-select -p >/dev/null 2>&1; then warn "Xcode Command Line Tools not installed." echo " Run: xcode-select --install" echo " Then re-run 'make install'." exit 1 else info "Xcode CLT present at $(xcode-select -p)" fi if ! command -v brew >/dev/null 2>&1; then warn "Homebrew not installed." echo echo " Install with:" echo ' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' echo if ask_yn "Install Homebrew now?" Y; then /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # Make brew available for the rest of this run on Apple Silicon (default prefix /opt/homebrew). if [[ -x /opt/homebrew/bin/brew ]]; then eval "$(/opt/homebrew/bin/brew shellenv)" elif [[ -x /usr/local/bin/brew ]]; then eval "$(/usr/local/bin/brew shellenv)" fi else err "Homebrew is required for the macOS install path. Install it and re-run." exit 1 fi else info "Homebrew present at $(command -v brew)" fi fi # ---------- system deps ---------- hr bold "Step 2 — system dependencies" echo base_pkgs_arch=(go ffmpeg cmake git curl) base_pkgs_debian=(golang-go ffmpeg cmake git curl build-essential) base_pkgs_fedora=(golang ffmpeg cmake git curl gcc-c++) base_pkgs_macos=(go ffmpeg cmake git curl) backend_pkgs_arch_cuda=(cuda gcc15) backend_pkgs_arch_rocm=(rocm-hip-sdk rocm-hip-runtime hipblas rocblas) backend_pkgs_arch_vulkan=(vulkan-headers vulkan-icd-loader shaderc) backend_pkgs_arch_cpu=(whisper.cpp) backend_pkgs_macos_metal=() # xcode-select on macOS provides Metal toolchain backend_pkgs_macos_cpu=(whisper-cpp) pkgs_to_install=() case "$OS" in arch) pkgs_to_install+=("${base_pkgs_arch[@]}") case "$BACKEND" in cuda) pkgs_to_install+=("${backend_pkgs_arch_cuda[@]}") ;; rocm) pkgs_to_install+=("${backend_pkgs_arch_rocm[@]}") ;; vulkan) pkgs_to_install+=("${backend_pkgs_arch_vulkan[@]}") ;; cpu) pkgs_to_install+=("${backend_pkgs_arch_cpu[@]}") ;; esac ;; macos) pkgs_to_install+=("${base_pkgs_macos[@]}") [[ "$BACKEND" == "cpu" ]] && pkgs_to_install+=("${backend_pkgs_macos_cpu[@]}") ;; debian) pkgs_to_install+=("${base_pkgs_debian[@]}") warn "Debian/Ubuntu: GPU runtime install for $BACKEND is distro-specific; you'll need to handle it manually." ;; fedora) pkgs_to_install+=("${base_pkgs_fedora[@]}") warn "Fedora: GPU runtime install for $BACKEND is distro-specific; you'll need to handle it manually." ;; *) warn "Unknown OS '$OS' — install $BACKEND runtime, ffmpeg, cmake, git, and Go manually." ;; esac missing=() for p in "${pkgs_to_install[@]}"; do case "$p" in # arch package -> binary mapping for "is it installed?" checks go) command -v go >/dev/null 2>&1 || missing+=("$p") ;; golang-go|golang) command -v go >/dev/null 2>&1 || missing+=("$p") ;; ffmpeg) command -v ffmpeg >/dev/null 2>&1 || missing+=("$p") ;; cmake) command -v cmake >/dev/null 2>&1 || missing+=("$p") ;; git) command -v git >/dev/null 2>&1 || missing+=("$p") ;; curl) command -v curl >/dev/null 2>&1 || missing+=("$p") ;; cuda) command -v nvcc >/dev/null 2>&1 || missing+=("$p") ;; gcc15) [[ -x /usr/bin/g++-15 ]] || missing+=("$p") ;; rocm-hip-sdk) command -v hipcc >/dev/null 2>&1 || missing+=("$p") ;; rocm-hip-runtime|hipblas|rocblas) [[ -d /opt/rocm ]] || missing+=("$p") ;; vulkan-headers|vulkan-icd-loader) command -v vulkaninfo >/dev/null 2>&1 || missing+=("$p") ;; shaderc) command -v glslc >/dev/null 2>&1 || missing+=("$p") ;; whisper.cpp|whisper-cpp) command -v whisper-cli >/dev/null 2>&1 || command -v whisper-cpp >/dev/null 2>&1 || missing+=("$p") ;; build-essential|gcc-c++) command -v g++ >/dev/null 2>&1 || missing+=("$p") ;; *) missing+=("$p") ;; esac done if (( ${#missing[@]} == 0 )); then info "All system dependencies present." else info "Missing: ${missing[*]}" case "$OS" in arch) cmd="sudo pacman -S --needed ${missing[*]}" ;; debian) cmd="sudo apt-get update && sudo apt-get install -y ${missing[*]}" ;; fedora) cmd="sudo dnf install -y ${missing[*]}" ;; macos) cmd="brew install ${missing[*]}" ;; *) cmd="" ;; esac if [[ -n "$cmd" ]]; then echo echo " Suggested install command:" echo " $cmd" echo if ask_yn "Run it now?" Y; then bash -c "$cmd" else warn "Skipped. Re-run after installing manually." fi fi fi # ---------- whisper.cpp build ---------- build_whisper() { local backend="$1" if [[ "$backend" == "cpu" ]]; then if command -v whisper-cli >/dev/null 2>&1 || command -v whisper-cpp >/dev/null 2>&1; then info "Using system whisper.cpp; no build needed." return 0 fi err "No system whisper.cpp found and 'cpu' chosen; install the package or pick another backend." return 1 fi if [[ "$backend" == "skip" ]]; then info "Skipping whisper.cpp." return 0 fi if [[ ! -d "$WHISPER_REPO/.git" ]]; then info "Cloning whisper.cpp into $WHISPER_REPO" mkdir -p "$(dirname "$WHISPER_REPO")" git clone --depth=1 "$WHISPER_GIT" "$WHISPER_REPO" else if ask_yn "whisper.cpp already at $WHISPER_REPO — git pull latest?" N; then git -C "$WHISPER_REPO" pull --ff-only || warn "git pull failed; continuing with current checkout" fi fi local build_dir="$WHISPER_REPO/build-$backend" info "Configuring $backend build in $build_dir" case "$backend" in cuda) local host_cxx="" [[ -x /usr/bin/g++-15 ]] && host_cxx="-DCMAKE_CUDA_HOST_COMPILER=/usr/bin/g++-15" local arch="86" if command -v nvidia-smi >/dev/null 2>&1; then local cap; cap="$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null | head -1 | tr -d '.')" [[ -n "$cap" ]] && arch="$cap" fi info " CUDA arch: sm_$arch" PATH="/opt/cuda/bin:$PATH" cmake -S "$WHISPER_REPO" -B "$build_dir" \ -DGGML_CUDA=1 \ -DCMAKE_CUDA_ARCHITECTURES="$arch" \ $host_cxx \ -DCMAKE_BUILD_TYPE=Release PATH="/opt/cuda/bin:$PATH" cmake --build "$build_dir" -j"$(ncpu)" --config Release ;; rocm) local gpu_arch="gfx1102" if command -v rocminfo >/dev/null 2>&1; then local detected; detected="$(rocminfo 2>/dev/null | awk '/Name:[[:space:]]+gfx/ {print $2; exit}')" [[ -n "$detected" ]] && gpu_arch="$detected" fi info " AMDGPU target: $gpu_arch" HIPCXX="${HIPCXX:-/opt/rocm/llvm/bin/clang++}" \ cmake -S "$WHISPER_REPO" -B "$build_dir" \ -DGGML_HIP=1 \ -DAMDGPU_TARGETS="$gpu_arch" \ -DCMAKE_BUILD_TYPE=Release cmake --build "$build_dir" -j"$(ncpu)" ;; vulkan) cmake -S "$WHISPER_REPO" -B "$build_dir" \ -DGGML_VULKAN=1 \ -DCMAKE_BUILD_TYPE=Release cmake --build "$build_dir" -j"$(ncpu)" ;; metal) # Metal is on by default on Apple Silicon; no special flag. cmake -S "$WHISPER_REPO" -B "$build_dir" -DCMAKE_BUILD_TYPE=Release cmake --build "$build_dir" -j"$(ncpu)" ;; *) err "Unknown backend: $backend"; return 1 ;; esac mkdir -p "$BINDIR" local link="$BINDIR/whisper-cli-$backend" ln -sf "$build_dir/bin/whisper-cli" "$link" info "linked $link -> $build_dir/bin/whisper-cli" } hr bold "Step 3 — whisper.cpp" echo build_whisper "$BACKEND" # ---------- model download ---------- hr bold "Step 4 — whisper model" echo mkdir -p "$MODELDIR" existing_models=() while IFS= read -r m; do existing_models+=("$(basename "$m")"); done < <(ls "$MODELDIR"/ggml-*.bin 2>/dev/null || true) if (( ${#existing_models[@]} > 0 )); then info "Existing models in $MODELDIR:" for m in "${existing_models[@]}"; do info " - $m"; done fi if ask_yn "Download a model now?" $([ ${#existing_models[@]} -eq 0 ] && echo Y || echo N); then echo echo " Sizes (English-only suffix .en is faster on English audio):" echo " tiny.en ~75MB" echo " base.en ~142MB (default; good speed/quality balance)" echo " small.en ~466MB" echo " medium.en ~1.5GB" echo " large-v3 ~2.9GB (multilingual, highest quality)" echo SIZE="$(ask_choice "Model" "base.en" tiny.en base.en small.en medium.en large-v3)" target="$MODELDIR/ggml-$SIZE.bin" if [[ -f "$target" ]]; then info "$target already exists; skipping download." else info "Downloading ggml-$SIZE.bin ..." curl -L --fail -o "$target" "$MODEL_BASE_URL/ggml-$SIZE.bin" info "saved to $target" fi fi # ---------- publish build + symlink ---------- hr bold "Step 5 — publish binary" echo if ! command -v go >/dev/null 2>&1; then err "Go is not installed; cannot build publish." exit 1 fi (cd "$REPO_DIR" && go build -o publish .) info "built $REPO_DIR/publish" mkdir -p "$BINDIR" ln -sf "$REPO_DIR/publish" "$BINDIR/publish" info "linked $BINDIR/publish -> $REPO_DIR/publish" # ---------- summarizer hint ---------- hr bold "Step 6 — summarizer" echo if command -v claude >/dev/null 2>&1; then info "Found 'claude' CLI — default --summarizer claude-cli will work." elif [[ -n "${ANTHROPIC_API_KEY:-}" ]]; then info "ANTHROPIC_API_KEY set — use --summarizer claude-api." else warn "Neither 'claude' CLI nor ANTHROPIC_API_KEY found. Install Claude Code or export ANTHROPIC_API_KEY before running summaries." fi # ---------- done ---------- hr bold "Done." echo echo " Quick sanity check:" echo " publish --help" echo " publish --doctor # via this script: bash scripts/install.sh --doctor" echo echo " Make sure $BINDIR is on your PATH." case ":$PATH:" in *":$BINDIR:"*) ;; *) warn "$BINDIR is not currently on your PATH." ;; esac