#!/usr/bin/env python3 """Result page of a whole book: every formula with original, set LaTeX, status and reason. python3 tools/buch_bericht.py AUSGABE [--pdf BUCH.pdf] [--out ORDNER] Reads the workspace of a finished `run` (nothing is recomputed, no model is asked) and writes /bericht/index.html with images in bericht/img/ (default). The page lives next to the book because it shows excerpts of it. Filter buttons: all, automatically replaced, review, image. """ from __future__ import annotations import argparse import html import io import re import statistics import sys from pathlib import Path import numpy as np from PIL import Image sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from formula_img2latex.pipeline import _ref_for, latest_by_engine, open_all # noqa: E402 from formula_img2latex.render import render_many # noqa: E402 from formula_img2latex.tex import strip_delimiters # noqa: E402 LABEL = {"local": "Qwen", "p2t": "pix2text", "local2": "Gemma", "cloud": "GLM"} STATUS = {"auto_accepted": ("automatisch ersetzt", "good"), "accepted_by_human": ("von Hand übernommen", "good"), "needs_review": ("Review", "rev"), "not_formula": ("keine Formel", "img"), "rejected_keep_image": ("bleibt Bild", "img"), "kept_by_human": ("bleibt Bild (Hand)", "img"), "render_failed": ("nicht setzbar", "img"), "pending": ("nicht geprüft", "img")} def page_key(f) -> tuple[int, int]: m = re.search(r"Seite (\d+), Box (\d+)", f.source_name or "") return (int(m.group(1)), int(m.group(2))) if m else (10**6, 0) def save_png(img: Image.Image, path: Path) -> None: img.save(path, "PNG", optimize=True) def original_png(data: bytes, max_h: int = 110, max_w: int = 900) -> Image.Image: im = Image.open(io.BytesIO(data)) if im.mode in ("P", "LA", "RGBA") or "transparency" in im.info: im = Image.alpha_composite(Image.new("RGBA", im.size, "white"), im.convert("RGBA")) im = im.convert("L") f = min(1.0, max_h / max(im.height, 1), max_w / max(im.width, 1)) if f < 1.0: im = im.resize((max(1, round(im.width * f)), max(1, round(im.height * f))), Image.LANCZOS) return im def main() -> int: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("source", type=Path) ap.add_argument("--pdf", type=Path, default=None) ap.add_argument("--out", type=Path, default=None) ap.add_argument("--titel", default=None) args = ap.parse_args() source, ws = open_all(args.source, None, pdf=args.pdf) out = args.out or ws.root / "bericht" (out / "img").mkdir(parents=True, exist_ok=True) formulas = sorted(ws.all(), key=page_key) preset = ws.settings.get("preset") or "cm" # the LaTeX shown: the replacement if there is one, else pix2text's reading (rule p2t) shown = [] for f in formulas: tex = f.final_tex if not tex: p = latest_by_engine(f).get("p2t") tex = p.get("tex") if p else None shown.append(strip_delimiters(tex) if tex else None) rendered = render_many([(t or "\\text{–}", f.display) for t, f in zip(shown, formulas)], preset) counts: dict[str, int] = {} secs: dict[str, list[float]] = {} cards = [] for n, (f, tex, rr) in enumerate(zip(formulas, shown, rendered), 1): counts[f.status] = counts.get(f.status, 0) + 1 latest = latest_by_engine(f) for e, r in latest.items(): if r.get("seconds"): secs.setdefault(e, []).append(float(r["seconds"])) try: o = out / "img" / f"{f.id}-o.png" if not o.exists(): # the original never changes; cutting it from the PDF is the slow part save_png(original_png(source.original_bytes(_ref_for(source, f))), o) orig = f"" except Exception: # noqa: BLE001 orig = "Original nicht lesbar" if tex and rr.ok: g = (rr.rasterize(1.6) * 255).astype(np.uint8) save_png(Image.fromarray(255 - g), out / "img" / f"{f.id}-l.png") latex = f"" else: latex = "–" label, cls = STATUS.get(f.status, (f.status, "img")) used = "ersetzt durch" if f.final_tex else "Lesart pix2text" reads = "".join(f"{LABEL.get(e, e)}{html.escape(r.get('tex') or '–')}" for e, r in sorted(latest.items(), key=lambda kv: ["p2t", "local", "local2", "cloud"].index(kv[0]) if kv[0] in ("p2t", "local", "local2", "cloud") else 9)) occ = f.occurrences[0] if f.occurrences else {} cards.append( f"
{n} · {html.escape(f.source_name)} · " f"Zeile {occ.get('line', '?')} · {label}
" f"
{html.escape(f.reason or '')}
" f"
Original (PDF){orig}
{used}{latex}
" f"{html.escape(tex or '–')}" f"
alle Lesarten{reads}
") total = len(formulas) good = counts.get("auto_accepted", 0) + counts.get("accepted_by_human", 0) rev = counts.get("needs_review", 0) img = total - good - rev time_rows = "".join(f"Ø {LABEL.get(e, e)}{statistics.mean(v):.2f} s ({len(v)} Formeln)" for e, v in sorted(secs.items())) status_rows = "".join(f"{STATUS.get(k, (k,))[0]}{v} ({v / max(total, 1):.0%})" for k, v in sorted(counts.items(), key=lambda kv: -kv[1])) titel = args.titel or f"Formeln: {source.path.name}" page = ( f"{html.escape(titel)}" f"

{html.escape(titel)}

" f"

{total} Formelbilder (abgesetzte Formeln aus der PDF). Regel p2t: pix2text liest, Qwen " "bestätigt, Gemma nur bei Uneinigkeit, dazu Pandoc und Bild-Veto. Links das Original aus der PDF, rechts das " "gesetzte LaTeX.

" f"{status_rows}{time_rows}
" "
" f"" f"
" + "".join(cards) + "") (out / "index.html").write_text(page, encoding="utf-8") print(out / "index.html") return 0 if __name__ == "__main__": sys.exit(main())