Files
Brummel bb584b6ea0 Refactor: Remove unused Whisper variants
This commit removes the `whisper_variant` and `whisper_hotwords_variant`
fields from the `run_id` generation and the `print_meta_summary`
function.

These variants are no longer used as the project is shifting focus to
LLM-based generation. The `run_full_case.rs` example has also been
updated to reflect this change.
2026-04-30 16:56:12 +02:00

218 lines
7.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Start doctate-server with the chosen ASR backend (whisper or canary).
#
# Coordinates the GPU host (minerva.lan) so the chosen ASR container is
# the only heavy GPU resident:
# - Whisper (~1.5-3 GB) coexists with Ollama (~3-4 GB) on the RTX 3060.
# - Canary (~9.7 GB) does NOT — Ollama must be unloaded first, else
# Canary OOMs on its last few MB of model weights.
# See docs/canary.md §2.6 for the VRAM accounting.
#
# Flow:
# 1. cargo build -p doctate-server (fail fast, BEFORE touching minerva)
# 2. ssh minerva.lan: docker compose stop the OTHER ASR stack
# 3. (canary only) unload all Ollama models — must happen BEFORE the
# canary container starts, otherwise canary loads its model into a
# still-occupied GPU and OOMs in the last few MB. Order matters:
# „start canary, then free VRAM" is too late — canary's model load
# runs in the container background as soon as `compose up -d` returns.
# 4. ssh minerva.lan: docker compose up -d the chosen stack — for
# canary, the GPU is now guaranteed empty.
# 5. poll the chosen backend until it answers, with progress pulses
# and periodic container-log snapshots so a stuck start is visible
# 6. exec cargo run -p doctate-server with ASR_BACKEND= exported
#
# Usage:
# scripts/run-server.sh whisper
# scripts/run-server.sh canary
#
# Env overrides (all optional):
# MINERVA_HOST default minerva.lan
# WHISPER_STACK default /opt/stacks/doctate-whisper
# CANARY_STACK default /opt/stacks/doctate-canary
# WHISPER_HOST_PORT default 9001
# CANARY_HOST_PORT default 9002
# OLLAMA_HOST_PORT default 11434
# HEALTH_TIMEOUT_SECS default 600 (cold model-download room)
# HEALTH_POLL_SECS default 3 (interval between probes)
# LOG_SNAPSHOT_SECS default 30 (interval between log dumps)
set -euo pipefail
MINERVA_HOST="${MINERVA_HOST:-minerva.lan}"
WHISPER_STACK="${WHISPER_STACK:-/opt/stacks/doctate-whisper}"
CANARY_STACK="${CANARY_STACK:-/opt/stacks/doctate-canary}"
WHISPER_HOST_PORT="${WHISPER_HOST_PORT:-9001}"
CANARY_HOST_PORT="${CANARY_HOST_PORT:-9002}"
OLLAMA_HOST_PORT="${OLLAMA_HOST_PORT:-11434}"
HEALTH_TIMEOUT_SECS="${HEALTH_TIMEOUT_SECS:-600}"
HEALTH_POLL_SECS="${HEALTH_POLL_SECS:-3}"
LOG_SNAPSHOT_SECS="${LOG_SNAPSHOT_SECS:-30}"
usage() {
cat <<EOF >&2
Usage: $0 <whisper|canary>
Coordinates the GPU host (\$MINERVA_HOST) so only the matching ASR
container runs (and, for canary, Ollama is unloaded), then starts
doctate-server locally with the matching ASR_BACKEND env var.
See header comment for env overrides.
EOF
exit 64
}
[[ $# -eq 1 ]] || usage
case "$1" in
whisper|canary) BACKEND="$1" ;;
*) usage ;;
esac
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT/server"
# ---- Step 1: build before touching minerva. -------------------------------
echo ">>> [1/6] cargo build -p doctate-server"
cargo build -p doctate-server
# ---- Step 2: pick stop/start targets. -------------------------------------
if [[ "$BACKEND" == "whisper" ]]; then
STOP_STACK="$CANARY_STACK"
START_STACK="$WHISPER_STACK"
HEALTH_KIND="tcp"
HEALTH_PORT="$WHISPER_HOST_PORT"
HEALTH_DESC="TCP $MINERVA_HOST:$WHISPER_HOST_PORT"
else
STOP_STACK="$WHISPER_STACK"
START_STACK="$CANARY_STACK"
HEALTH_KIND="http"
HEALTH_URL="http://$MINERVA_HOST:$CANARY_HOST_PORT/health"
HEALTH_DESC="HTTP $HEALTH_URL"
fi
echo ">>> [2/6] $MINERVA_HOST: stop $STOP_STACK"
ssh "$MINERVA_HOST" "cd '$STOP_STACK' && docker compose stop"
# ---- Step 3 (canary only): unload Ollama models. --------------------------
# Ollama keeps the active model warm for `keep_alive` seconds (300 in
# settings.toml at the time of writing). When the operator switches to
# canary in the middle of that window, the model is still resident and
# canary OOMs on the last few MB. We trigger a synchronous unload via
# the Ollama API: posting `keep_alive: 0` with an empty prompt drops
# the model immediately and frees VRAM. (See ollama/ollama issue #2546.)
unload_ollama_models() {
local base="http://$MINERVA_HOST:$OLLAMA_HOST_PORT"
local ps_file
ps_file=$(mktemp)
trap 'rm -f "$ps_file"' RETURN
if ! curl -fsS --max-time 5 "$base/api/ps" -o "$ps_file"; then
echo " Ollama API unreachable at $base/api/ps — skipping unload" >&2
return 0
fi
# Parse the JSON model list with python3 (avoid jq as a hard dep).
local models
models=$(python3 -c "
import json, sys
try:
d = json.load(open('$ps_file'))
for m in d.get('models', []):
print(m.get('name', ''))
except Exception:
sys.exit(0)
")
if [[ -z "$models" ]]; then
echo " Ollama: no models loaded — VRAM already free"
return 0
fi
while IFS= read -r name; do
[[ -z "$name" ]] && continue
echo " unloading: $name"
curl -fsS --max-time 10 "$base/api/generate" \
-H 'Content-Type: application/json' \
-d "{\"model\":\"$name\",\"prompt\":\"\",\"keep_alive\":0}" \
-o /dev/null \
|| echo " (warn: unload request for $name returned non-2xx)" >&2
done <<< "$models"
# Give the GPU driver a moment to actually reclaim the pages before
# canary starts allocating its own.
sleep 2
}
if [[ "$BACKEND" == "canary" ]]; then
echo ">>> [3/6] freeing VRAM: unloading Ollama models BEFORE canary starts"
unload_ollama_models
else
echo ">>> [3/6] (whisper) Ollama coexistence is fine — skipping unload"
fi
# ---- Step 4: NOW start the chosen stack. ----------------------------------
# For canary this is the critical moment — the GPU has just been freed
# by step 3, and canary's container will allocate into an empty GPU
# instead of fighting Ollama for the last 20 MB.
echo ">>> [4/6] $MINERVA_HOST: start $START_STACK"
ssh "$MINERVA_HOST" "cd '$START_STACK' && docker compose up -d"
# ---- Step 5: poll until the chosen backend answers. -----------------------
echo ">>> [5/6] waiting for $HEALTH_DESC (up to ${HEALTH_TIMEOUT_SECS}s)"
probe_once() {
if [[ "$HEALTH_KIND" == "tcp" ]]; then
if (exec 3<>"/dev/tcp/$MINERVA_HOST/$HEALTH_PORT") 2>/dev/null; then
exec 3<&- 3>&-
return 0
fi
return 1
else
curl -fsS --max-time 5 "$HEALTH_URL" >/dev/null 2>&1
fi
}
start=$(date +%s)
deadline=$(( start + HEALTH_TIMEOUT_SECS ))
last_log_snapshot=0
attempt=0
while :; do
if probe_once; then
echo ""
echo " backend is up after $(( $(date +%s) - start ))s"
break
fi
attempt=$(( attempt + 1 ))
printf '.'
now=$(date +%s)
if (( now >= deadline )); then
echo ""
echo "timeout waiting for $HEALTH_DESC after ${HEALTH_TIMEOUT_SECS}s" >&2
echo "--- container logs (tail 40) ---" >&2
ssh "$MINERVA_HOST" "cd '$START_STACK' && docker compose logs --tail 40" >&2 || true
exit 1
fi
# Periodic log snapshot: every LOG_SNAPSHOT_SECS, print elapsed time
# and the last few log lines so the operator can SEE if the container
# is loading, crashing, or stalled.
if (( now - last_log_snapshot >= LOG_SNAPSHOT_SECS )); then
echo ""
elapsed=$(( now - start ))
echo " [${elapsed}s elapsed] still waiting; recent container log:"
ssh "$MINERVA_HOST" "cd '$START_STACK' && docker compose logs --tail 5 --no-log-prefix 2>&1" \
| sed 's/^/ | /' \
|| echo " (could not read logs)" >&2
last_log_snapshot=$now
fi
sleep "$HEALTH_POLL_SECS"
done
# ---- Step 6: start the server. --------------------------------------------
# dotenvy::dotenv() in server/src/main.rs does NOT override env vars that
# are already set, so this export wins over any ASR_BACKEND= in .env.
echo ">>> [6/6] starting doctate-server (ASR_BACKEND=$BACKEND)"
export ASR_BACKEND="$BACKEND"
exec cargo run -p doctate-server