"""OCR helper for part B. Wraps the local `tesseract` binary via pytesseract. Deterministic on identical pixels; no network. We drew the overlay text ourselves (clean, high-contrast), so tesseract reads it back reliably. """ from __future__ import annotations import re import pytesseract from PIL import Image def read_text(image: Image.Image) -> str: """Return the raw OCR text of ``image``. Convert to luminance ("L") first: tesseract binarizes internally, and on the raw RGB composite (colored background + light-on-dark text bands) its global threshold drops every glyph. A single-channel luminance image gives a clean histogram it reads reliably. """ return pytesseract.image_to_string(image.convert("L")) def read_band(image: Image.Image, band: tuple[float, float]) -> str: """OCR ONE horizontal band with single-line page segmentation. Whole-image OCR with Tesseract's DEFAULT page segmentation silently drops text regions when they are widely separated — measured at 151/1088 of our combos. Cropping to the band and forcing --psm 7 (treat the image as a single text line) reads them reliably. """ width, height = image.size y0, y1 = band crop = image.crop((0, int(height * y0), width, int(height * y1))) return pytesseract.image_to_string(crop.convert("L"), config="--psm 7") def _normalize(s: str) -> str: """Lowercase; collapse every run of non-alphanumerics to a single space.""" return re.sub(r"[^a-z0-9]+", " ", s.lower()).strip() def text_present(ocr_text: str, phrase: str, cutoff: float = 0.8) -> bool: """True if ``phrase`` appears in ``ocr_text`` (normalized). Exact normalized substring wins; otherwise fall back to token overlap (fraction of the phrase's tokens found) against ``cutoff``. """ norm_ocr = _normalize(ocr_text) norm_phrase = _normalize(phrase) if not norm_phrase: return True if norm_phrase in norm_ocr: return True tokens = norm_phrase.split() if not tokens: return True hits = sum(1 for t in tokens if t in norm_ocr) return hits / len(tokens) >= cutoff