#!/usr/bin/env bash # Regression test for the whisper service: runs every fixture under # tests/fixtures/dictations/ through /asr twice (with and without hotwords) # and reports word error rate vs the golden `.expected.txt`. # # Usage: # ./scripts/regress_whisper.sh # WHISPER_URL=http://minerva.lan:9001 ./scripts/regress_whisper.sh # ./scripts/regress_whisper.sh --verbose # also print each hypothesis # # Exit code is the sum of mismatches across all hotwords runs (0 = perfect). set -euo pipefail WHISPER_URL="${WHISPER_URL:-http://minerva.lan:9001}" REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" DIR="$REPO_ROOT/tests/fixtures/dictations" VERBOSE=0 [[ "${1:-}" == "--verbose" || "${1:-}" == "-v" ]] && VERBOSE=1 die() { echo "error: $*" >&2; exit 1; } [ -d "$DIR" ] || die "fixtures dir missing: $DIR" command -v python3 >/dev/null || die "python3 not found" # POST a single fixture. If hotwords arg is non-empty, forward it as a form # field so the whisper service can feed it into faster_whisper. run_once() { local m4a="$1" hotwords="$2" if [ -n "$hotwords" ]; then curl -sS --max-time 120 -X POST "$WHISPER_URL/asr?output=txt&language=de" \ -F "audio_file=@$m4a" \ -F "hotwords=$hotwords" else curl -sS --max-time 120 -X POST "$WHISPER_URL/asr?output=txt&language=de" \ -F "audio_file=@$m4a" fi } # Word error rate via Levenshtein. Prints "errors/ref_len". wer() { python3 - "$1" "$2" <<'PY' import re, sys, unicodedata def norm(path): with open(path, encoding="utf-8") as f: s = f.read() s = unicodedata.normalize("NFC", s).lower() s = re.sub(r"[^\w\s]", " ", s, flags=re.UNICODE) return s.split() def lev(a, b): m, n = len(a), len(b) if m == 0 or n == 0: return max(m, n) prev = list(range(n + 1)) for i, ca in enumerate(a, 1): cur = [i] + [0] * n for j, cb in enumerate(b, 1): cost = 0 if ca == cb else 1 cur[j] = min(cur[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost) prev = cur return prev[n] ref, hyp = norm(sys.argv[1]), norm(sys.argv[2]) print(f"{lev(ref, hyp)}/{len(ref)}") PY } printf "%-38s %-13s %-13s\n" "fixture" "no_hotwords" "with_hotwords" printf "%-38s %-13s %-13s\n" "--------------------------------------" "-------------" "-------------" total_hw_errs=0 for m4a in "$DIR"/*.m4a; do name="$(basename "$m4a" .m4a)" exp="$DIR/$name.expected.txt" hw_file="$DIR/$name.hotwords.txt" [ -f "$exp" ] || { echo "skip $name (no .expected.txt)"; continue; } tmp_no="$(mktemp)" tmp_hw="$(mktemp)" trap 'rm -f "$tmp_no" "$tmp_hw"' EXIT run_once "$m4a" "" > "$tmp_no" wer_no="$(wer "$exp" "$tmp_no")" if [ -f "$hw_file" ]; then hw="$(tr -d '\n' < "$hw_file")" run_once "$m4a" "$hw" > "$tmp_hw" wer_hw="$(wer "$exp" "$tmp_hw")" total_hw_errs=$(( total_hw_errs + ${wer_hw%/*} )) else wer_hw="—" total_hw_errs=$(( total_hw_errs + ${wer_no%/*} )) fi printf "%-38s %-13s %-13s\n" "$name" "$wer_no" "$wer_hw" if [ "$VERBOSE" -eq 1 ]; then echo " expected: $(tr -d '\n' < "$exp")" echo " no_hotwords: $(tr -d '\n' < "$tmp_no")" if [ -f "$hw_file" ]; then echo " with_hotwords: $(tr -d '\n' < "$tmp_hw")" fi fi rm -f "$tmp_no" "$tmp_hw" done echo echo "Total hotwords-run word errors: $total_hw_errs" exit "$total_hw_errs"