""" Generate a self-contained HTML verification report from olmOCR-bench results. Requires results saved with --save-raw-outputs. Usage: # 1. Run benchmark with raw outputs saved python benchmark/ocr/bench_sglang.py --port 30000 --split arxiv_math \\ --max-samples 20 --save-raw-outputs # 2. Generate HTML report for a single split python benchmark/ocr/generate_report.py --split arxiv_math # 3. Generate HTML report for all splits in a results directory python benchmark/ocr/generate_report.py --results-dir ./ocr_bench_results # 4. Show only failing tests python benchmark/ocr/generate_report.py --split arxiv_math --failures-only Open the generated .html file in any browser โ formulas are rendered via MathJax. """ import argparse import html import json import re import sys from pathlib import Path # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- _MATH_DELIM_RE = re.compile( r"(\\\[[\s\S]*?\\\]" # \[...\] r"|\\\([\s\S]*?\\\)" # \(...\) r"|\$\$[\s\S]*?\$\$" # $$...$$ r"|\$[^$\n]+?\$)", # $...$ re.DOTALL, ) def _ocr_to_html(text: str) -> str: """ Convert raw OCR output to readable HTML. Preserves LaTeX delimiters for MathJax, strips bounding-box annotations, and wraps block-level elements in
tags.
"""
lines = text.splitlines()
out_lines = []
for line in lines:
# Strip bounding box annotations like text[[x1, y1, x2, y2]]
line = re.sub(r"^\s*\w[\w_]*\[\[\d+,\s*\d+,\s*\d+,\s*\d+\]\]\s*", "", line)
# HTML-escape everything EXCEPT LaTeX delimiters
parts = _MATH_DELIM_RE.split(line)
escaped = ""
for i, part in enumerate(parts):
if i % 2 == 0:
escaped += html.escape(part)
else:
escaped += part # LaTeX โ pass through for MathJax
out_lines.append(escaped)
return "
".join(out_lines)
def _latex_to_display(latex: str) -> str:
"""Wrap a raw LaTeX string in display-math delimiters for MathJax."""
stripped = latex.strip()
# Already delimited โ pass through
if stripped.startswith(("\\[", "$$", "\\(")):
return stripped
return f"\\[ {stripped} \\]"
# ---------------------------------------------------------------------------
# HTML template
# ---------------------------------------------------------------------------
_HTML_HEAD = """\
ERROR: {html.escape(str(error))}
' ) else: parts.append('type={ttype!r}"
+ (
f' {html.escape(test_err)}'
if test_err
else ""
)
+ f"{html.escape(latex)}")
if latex:
parts.append(
f'{html.escape(ti.get('text', ''))}")
elif ttype in ("order", "natural_reading_order"):
before = ti.get("before", "")
after = ti.get("after", "")
parts.append(
f"before: {html.escape(before)}\nafter: {html.escape(after)}"
)
parts.append("โฆ and {len(matches) - 6} more
' ) else: parts.append( 'No LaTeX delimiters found in OCR output.
' ) else: parts.append( '(see full OCR output below)
' ) parts.append("No raw OCR output stored. '
"Re-run with --save-raw-outputs to include it here.
Showing failures only.
' samples = split_result.get("samples", []) if not samples: body += 'No per-sample data found. The result JSON may not include sample-level details.
' else: for sample in samples: body += _render_sample(sample, failures_only) body += _HTML_TAIL out_path.write_text(body, encoding="utf-8") print(f"Report โ {out_path}") # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- def main() -> None: parser = argparse.ArgumentParser( description="Generate HTML verification report from olmOCR-bench results", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( "--results-dir", default="./ocr_bench_results", help="Directory containing result JSON files", ) parser.add_argument( "--split", default=None, help="Single split to report (default: all JSONs in results-dir)", ) parser.add_argument( "--output-dir", default=None, help="Where to write HTML files (default: same as results-dir)", ) parser.add_argument( "--failures-only", action="store_true", help="Include only samples/tests that failed", ) args = parser.parse_args() results_dir = Path(args.results_dir) output_dir = Path(args.output_dir) if args.output_dir else results_dir output_dir.mkdir(parents=True, exist_ok=True) if args.split: json_files = [results_dir / f"{args.split}.json"] else: json_files = sorted(results_dir.glob("*.json")) json_files = [f for f in json_files if f.stem != "summary"] if not json_files: sys.exit(f"No result JSON files found in {results_dir}") for jf in json_files: if not jf.exists(): print(f" SKIP (not found): {jf}") continue split_name = jf.stem with open(jf, encoding="utf-8") as fh: data = json.load(fh) suffix = "_failures" if args.failures_only else "" out_path = output_dir / f"{split_name}{suffix}_report.html" generate_report(split_name, data, out_path, failures_only=args.failures_only) if __name__ == "__main__": main()