"""Build a human-inspectable report of every vision-OCR'd image. Reads corpus/.vision-cache/*.json (written by scrape/vision.py) and emits a self-contained HTML page that puts each SOURCE IMAGE next to its OCR transcription, so a human can eyeball accuracy. Uncertain transcriptions (the two OCR samples disagreed) sort to the top. Images are downscaled and inlined as data URIs so the page is fully self-contained (no external requests) and the original full-res image is one click away. Usage: python -m scripts.vision_report # -> vision_report.html python -m scripts.vision_report --out /tmp/r.html python -m scripts.vision_report --no-embed # link images, don't inline (tiny file) """ from __future__ import annotations import argparse import base64 import glob import html import io import json import re import sys from pathlib import Path import requests ROOT = Path(__file__).resolve().parent.parent CACHE_DIR = ROOT / "corpus" / ".vision-cache" CORPUS = ROOT / "corpus" UA = "morpheus-docs-mcp/0.1 (+https://git.jpaul.io/justin/morpheus-docs)" MAX_W = 1100 # downscale width for the inlined preview def _basename(url: str) -> str: return re.sub(r"\?.*$", "", url.rsplit("/", 1)[-1]) def _pages_for(basename: str) -> list[str]: """Which corpus pages reference this image (by filename), as bundle/page.""" hits: list[str] = [] for md in CORPUS.glob("*/*.md"): try: if basename in md.read_text(): hits.append(f"{md.parent.name}/{md.stem}") except Exception: continue return sorted(hits) def _thumb(url: str, embed: bool) -> str | None: if not embed: return None try: data = requests.get(url, headers={"User-Agent": UA}, timeout=60).content from PIL import Image im = Image.open(io.BytesIO(data)) if im.mode not in ("RGB", "L"): im = im.convert("RGB") if im.width > MAX_W: im = im.resize((MAX_W, round(im.height * MAX_W / im.width)), Image.LANCZOS) buf = io.BytesIO() im.save(buf, format="JPEG", quality=82) return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode() except Exception as e: print(f" ! thumb failed for {url[:70]}: {e}", file=sys.stderr) return None def _md_table_to_html(md: str) -> str: """Render a markdown table as an HTML table; fall back to
 otherwise."""
    rows = [ln for ln in md.splitlines() if ln.strip().startswith("|")]
    if len(rows) < 2:
        return f"
{html.escape(md)}
" def cells(line: str) -> list[str]: return [c.strip() for c in line.strip().strip("|").split("|")] head = cells(rows[0]) body = [cells(r) for r in rows[2:]] # rows[1] is the |---| separator out = [""] + [f"" for c in head] + [""] for r in body: out.append("" + "".join(f"" for c in r) + "") out.append("
{html.escape(c)}
{html.escape(c)}
") return "".join(out) def build(embed: bool) -> str: entries = [] for f in sorted(glob.glob(str(CACHE_DIR / "*.json"))): try: entries.append(json.load(open(f))) except Exception: continue withmd = [e for e in entries if e.get("markdown")] # uncertain first, then by source URL for stable ordering withmd.sort(key=lambda e: (bool(e.get("certain")), e.get("src", ""))) n_uncertain = sum(1 for e in withmd if not e.get("certain")) cards = [] for i, e in enumerate(withmd, 1): src = e.get("src", "") bn = _basename(src) pages = _pages_for(bn) thumb = _thumb(src, embed) badge = ("certain" if e.get("certain") else "UNCERTAIN — samples disagreed") img_html = (f"source image" if thumb else f"
[not inlined — open original]
") pages_html = " ".join(f"{html.escape(p)}" for p in pages) or "(none found)" cards.append(f"""
#{i} {badge} {html.escape(e.get('model',''))}
pages: {pages_html}
open original image ↗
{img_html}
{_md_table_to_html(e.get('markdown',''))}
""") style = """ :root{color-scheme:light dark} body{font:14px/1.5 system-ui,sans-serif;margin:0;padding:24px;max-width:1400px;margin:auto} h1{font-size:20px} .summary{color:#666;margin-bottom:20px} .card{border:1px solid #8883;border-radius:10px;padding:16px;margin:0 0 20px} .meta{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:12px;font-size:13px} .num{font-weight:700} .ok{background:#1a7f37;color:#fff;padding:1px 8px;border-radius:10px;font-size:12px} .warn{background:#b34700;color:#fff;padding:1px 8px;border-radius:10px;font-size:12px} .model{color:#888;font-family:monospace} .pages{flex-basis:100%;color:#777} .pages code{font-size:12px} .orig{margin-left:auto} .pair{display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start} @media(max-width:900px){.pair{grid-template-columns:1fr}} .src img{max-width:100%;border:1px solid #8884;border-radius:6px} .noimg{color:#999;padding:40px;text-align:center;border:1px dashed #8884;border-radius:6px} table{border-collapse:collapse;font-size:12px;width:100%} th,td{border:1px solid #8884;padding:3px 7px;text-align:left} th{background:#8881} pre{white-space:pre-wrap;font-size:12px;background:#8881;padding:10px;border-radius:6px} """ return f""" Vision OCR inspection report

Vision OCR inspection report

{len(withmd)} transcribed image(s) — {n_uncertain} uncertain, {len(withmd)-n_uncertain} certain. Each source image (downscaled preview; click “open original” for full-res) is shown next to its OCR transcription. Uncertain items are listed first.
{''.join(cards)} """ def main() -> int: p = argparse.ArgumentParser(description="Build the vision-OCR inspection report.") p.add_argument("--out", default=str(ROOT / "vision_report.html")) p.add_argument("--no-embed", action="store_true", help="link images instead of inlining them") args = p.parse_args() if not CACHE_DIR.exists(): print(f"no cache dir at {CACHE_DIR} — run the scraper with VISION_OCR=1 first", file=sys.stderr) return 1 html_doc = build(embed=not args.no_embed) Path(args.out).write_text(html_doc) print(f"wrote {args.out}", file=sys.stderr) return 0 if __name__ == "__main__": sys.exit(main())