#!/bin/bash set -euo pipefail # Downloads Kraken HTR models, runs OCR evaluation on sample PDFs, and activates the winner. # # Usage: # ./scripts/download-kraken-models.sh # download models + run evaluation # ./scripts/download-kraken-models.sh --activate 1 # activate model 1 # ./scripts/download-kraken-models.sh --activate 2 # activate model 2 # ./scripts/download-kraken-models.sh --eval-only # re-run evaluation (models already downloaded) COMPOSE_SERVICE="ocr-service" MODEL_DIR="/app/models" ACTIVE_MODEL="$MODEL_DIR/german_kurrent.mlmodel" # Kraken 7 uses DOIs to identify models from HTR-United / Zenodo MODEL_1_DOI="10.5281/zenodo.7933463" MODEL_1_NAME="german_handwriting" MODEL_1_DESC="HTR model for German manuscripts (handwritten text recognition)" MODEL_1_PATH="$MODEL_DIR/$MODEL_1_NAME.mlmodel" MODEL_2_DOI="10.5281/zenodo.13788177" MODEL_2_NAME="mccatmus" MODEL_2_DESC="McCATMuS — generic model for handwritten, printed & typewritten (16th c. onward)" MODEL_2_PATH="$MODEL_DIR/$MODEL_2_NAME.mlmodel" EVAL_PDFS=("Eu-0693.pdf" "Eu-0692.pdf" "W-0150.pdf" "W-0575.pdf") IMPORT_DIR="./import" OUTPUT_BASE="./ocr-model-evaluation" # ─── Functions ──────────────────────────────────────────────────────────────── download_model() { local doi="$1" local dest="$2" local name="$3" echo " Downloading $name ($doi)..." # kraken get downloads to /root/.local/share/htrmopo//.mlmodel # Parse the "Model dir: " line from kraken output to locate the file docker compose run --rm "$COMPOSE_SERVICE" sh -c ' OUTPUT=$(kraken get '"$doi"' 2>&1) echo "$OUTPUT" MODEL_DIR=$(echo "$OUTPUT" | grep -oP "Model dir: \K[^ ]+") if [ -n "$MODEL_DIR" ] && [ -d "$MODEL_DIR" ]; then FOUND=$(find "$MODEL_DIR" -name "*.mlmodel" | head -1) if [ -n "$FOUND" ]; then cp "$FOUND" '"$dest"' echo "Saved to '"$dest"'" else echo "ERROR: No .mlmodel file in $MODEL_DIR" ls -la "$MODEL_DIR" exit 1 fi else echo "ERROR: Could not parse model directory from kraken output" exit 1 fi ' } download_models() { echo "Downloading Kraken HTR models into the ocr_models volume..." echo "" echo "Model 1: $MODEL_1_NAME" echo " $MODEL_1_DESC" download_model "$MODEL_1_DOI" "$MODEL_1_PATH" "$MODEL_1_NAME" echo "" echo "Model 2: $MODEL_2_NAME" echo " $MODEL_2_DESC" download_model "$MODEL_2_DOI" "$MODEL_2_PATH" "$MODEL_2_NAME" echo "" echo "Both models downloaded." } run_evaluation() { echo "" echo "═══════════════════════════════════════════════════════" echo " Running OCR evaluation on ${#EVAL_PDFS[@]} documents" echo "═══════════════════════════════════════════════════════" echo "" local dir_1="$OUTPUT_BASE/$MODEL_1_NAME" local dir_2="$OUTPUT_BASE/$MODEL_2_NAME" mkdir -p "$dir_1" "$dir_2" for pdf in "${EVAL_PDFS[@]}"; do local src="$IMPORT_DIR/$pdf" local basename="${pdf%.pdf}" if [[ ! -f "$src" ]]; then echo "SKIP: $src not found" continue fi echo "──── $pdf ────" # Run both models inside a single container run: # 1. Extract PDF pages as PNGs (pypdfium2 is already installed) # 2. Run kraken on each page image for both models # 3. Concatenate per-page output into one file per model docker compose run --rm \ -v "$(cd "$IMPORT_DIR" && pwd):/eval-input:ro" \ -v "$(cd "$dir_1" && pwd):/eval-out-1" \ -v "$(cd "$dir_2" && pwd):/eval-out-2" \ "$COMPOSE_SERVICE" \ python3 -c " import pypdfium2 as pdfium, subprocess, sys, os pdf = pdfium.PdfDocument('/eval-input/$pdf') pages = [] for i in range(len(pdf)): bmp = pdf[i].render(scale=300/72) path = f'/tmp/page_{i:04d}.png' bmp.to_pil().save(path) pages.append(path) print(f'Extracted {len(pages)} pages') for label, model, outdir in [ ('Model 1', '$MODEL_1_PATH', '/eval-out-1'), ('Model 2', '$MODEL_2_PATH', '/eval-out-2'), ]: print(f' {label}...') combined = '' for p in pages: args = ['kraken', '-i', p, '/dev/stdout', 'segment', '-bl', 'ocr', '-m', model] r = subprocess.run(args, capture_output=True, text=True) combined += r.stdout if r.returncode != 0: print(f' ⚠ failed on {os.path.basename(p)}: {r.stderr[:200]}', file=sys.stderr) with open(f'{outdir}/${basename}.txt', 'w') as f: f.write(combined) lines = combined.count(chr(10)) print(f' → {lines} lines') " || echo " ⚠ Failed on $pdf" echo "" done echo "═══════════════════════════════════════════════════════" echo " Evaluation complete. Results:" echo "" echo " Model 1 — $MODEL_1_NAME ($MODEL_1_DOI):" for f in "$dir_1"/*.txt; do [[ -f "$f" ]] && echo " $(basename "$f"): $(wc -l < "$f") lines, $(wc -c < "$f") bytes" done echo "" echo " Model 2 — $MODEL_2_NAME ($MODEL_2_DOI):" for f in "$dir_2"/*.txt; do [[ -f "$f" ]] && echo " $(basename "$f"): $(wc -l < "$f") lines, $(wc -c < "$f") bytes" done echo "" echo " Compare outputs:" echo " diff $dir_1/Eu-0693.txt $dir_2/Eu-0693.txt" echo " # or view individually:" echo " cat $dir_1/Eu-0693.txt" echo "" echo " Activate the better model:" echo " ./scripts/download-kraken-models.sh --activate 1 # $MODEL_1_NAME" echo " ./scripts/download-kraken-models.sh --activate 2 # $MODEL_2_NAME" echo "═══════════════════════════════════════════════════════" } activate_model() { local choice="$1" case "$choice" in 1) echo "Activating model 1: $MODEL_1_NAME" docker compose run --rm "$COMPOSE_SERVICE" cp "$MODEL_1_PATH" "$ACTIVE_MODEL" ;; 2) echo "Activating model 2: $MODEL_2_NAME" docker compose run --rm "$COMPOSE_SERVICE" cp "$MODEL_2_PATH" "$ACTIVE_MODEL" ;; *) echo "Error: --activate expects 1 or 2" exit 1 ;; esac echo "Active model is now: $ACTIVE_MODEL" echo "Restart the OCR service to load the new model:" echo " docker compose restart ocr-service" } # ─── Main ───────────────────────────────────────────────────────────────────── case "${1:-}" in --activate) activate_model "${2:-}" ;; --eval-only) run_evaluation ;; *) download_models run_evaluation ;; esac