#!/bin/bash # OCRs image-only textbook PDFs into extracted-text///.ocr.txt # Language from filename suffix: *-en.pdf -> eng, *-ur.pdf -> urd (project tessdata). # Renders pages at 300dpi grayscale with pdftoppm, OCRs pages in parallel, concatenates in order. # # Usage: ./ocr.sh [ ...] # specific files # ./ocr.sh --all-missing # every corpus PDF with no extracted text yet set -u ROOT="$(cd "$(dirname "$0")/.." && pwd)" export TESSDATA_PREFIX="$ROOT/scripts/tessdata" OUT="$ROOT/extracted-text" JOBS=8 ocr_one() { f="$1" rel=${f#"$ROOT"/} board=$(echo "$rel" | cut -d/ -f1); cdir=$(echo "$rel" | cut -d/ -f2) base=$(basename "$f" .pdf) case "$base" in # Sindhi is NOT Urdu. It uses an extended Arabic script with ~16 additional consonants # (ٻ ڀ ٺ ٽ ٿ ڏ ڊ ڍ ڌ ڪ ڳ ڱ ڻ ڇ ڄ ڃ), and the Urdu model has no glyphs for any of them — # it silently substitutes Urdu letters, producing text that looks plausible and is wrong. # A first pass on Sindh's Social Studies 6 returned ZERO Sindhi-specific letters. *-si) lang=snd ;; *-ur) lang=urd ;; *-un) # A file still marked unknown-medium would silently get the English model, which # produces unreadable noise for an Urdu book. Refuse rather than guess. echo "SKIP $rel — medium is still 'un'. Run ./scripts/detect_medium.sh first."; return ;; *) lang=eng ;; esac mkdir -p "$OUT/$board/$cdir" txt="$OUT/$board/$cdir/$base.ocr.txt" [ -s "$txt" ] && { echo "SKIP (done): $txt"; return; } # Short template on purpose. BSD xargs caps a constructed -I argument at 255 bytes, and a # long book name pushed the two absolute page paths past it: xargs printed "command line # cannot be assembled, too long" to stderr and the loop below then wrote a 0-character file # that looked like a completed extraction. Running from inside the directory keeps the # substituted names to ~12 bytes each regardless of how the book is called. tmp=$(mktemp -d "${TMPDIR:-/tmp}/ocr.XXXXXX") echo "OCR [$lang] $rel ..." pdftoppm -gray -r 300 "$f" "$tmp/pg" 2>/dev/null ( cd "$tmp" && ls pg-*.p*m 2>/dev/null | sort | \ xargs -P "$JOBS" -I{} tesseract "{}" "{}" -l "$lang" --psm 3 2>/dev/null ) : > "$txt.part" for p in $(ls "$tmp"/pg-*.txt 2>/dev/null | sort); do pgnum=$(basename "$p" .txt | sed -E 's/\.p[gp]m$//; s/^pg-0*//') printf '\n===== PAGE %s =====\n' "$pgnum" >> "$txt.part" cat "$p" >> "$txt.part" done rm -rf "$tmp" chars=$(tr -d '[:space:]' < "$txt.part" | wc -c | tr -d ' ') # An empty result is a failure, not an extraction. Leaving a 0-byte .ocr.txt on disk makes # every later "is this book done?" check answer yes, which is how a silent xargs failure # turned into twenty books that looked processed and held nothing. if [ "$chars" -lt 200 ]; then rm -f "$txt.part" echo " FAIL: produced only ${chars} chars — no text written, book still marked missing" return fi mv "$txt.part" "$txt" echo " done: $chars chars -> $txt" } if [ "${1:-}" = "--all-missing" ]; then # other/ holds the scholarly library — published papers that already carry their own text # layer and are not part of the corpus. OCRing them wastes hours and produces nothing used. find "$ROOT" -name "*.pdf" -not -path "*/extracted-text/*" -not -path "*/other/*" -not -path "*/site/*" | sort | while read -r f; do base=$(basename "$f" .pdf) rel=${f#"$ROOT"/}; board=$(echo "$rel" | cut -d/ -f1); cdir=$(echo "$rel" | cut -d/ -f2) if [ ! -s "$OUT/$board/$cdir/$base.txt" ] && [ ! -s "$OUT/$board/$cdir/$base.ocr.txt" ]; then ocr_one "$f" fi done else for f in "$@"; do ocr_one "$(cd "$(dirname "$f")" && pwd)/$(basename "$f")"; done fi