#!/usr/bin/env python3 """Visual before/after report of two runs on the same readings (--reuse), e.g. a rule change. python3 tools/vergleich_bericht.py --epub1 BAND1.epub --epub2 BAND2.epub \\ --regeltest regeltest1-ohne:regeltest1-struktur --regeltest regeltest2-ohne:regeltest2-struktur \\ --kalibrierung VORHER.json:NACHHER.json [--name struktur] [--titel "…"] Each pair is VORHER:NACHHER (regeltest: names under vergleich/, calibration: report files). Writes vergleich//index.html (not versioned): summary table, every changed decision or truth verdict, and every match found only by the structural comparison, with original image and rendered readings. """ from __future__ import annotations import argparse import html import json import sys import zipfile from pathlib import Path import numpy as np from PIL import Image sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from konsens_bericht import orig_img, png_uri # noqa: E402 from formula_img2latex.calibrate import _dvipng_jpeg, _rule_outcome # noqa: E402 from formula_img2latex.decide import consensus_thresholds, decide_pair_tiebreak # noqa: E402 from formula_img2latex.render import render_many # noqa: E402 from formula_img2latex.tex import strip_delimiters # noqa: E402 ROOT = Path(__file__).resolve().parents[1] / "vergleich" LABEL = {"local": "Qwen", "p2t": "pix2text", "local2": "Gemma", "cloud": "GLM"} class Page: """Collects cards; renders all TeX in one pdflatex batch at the end.""" def __init__(self) -> None: self.parts: list[str] = [] self.texes: list[tuple[str, bool, str]] = [] # (tex, display, preset) def img(self, tex: str | None, display: bool, preset: str) -> str: self.texes.append((strip_delimiters(tex or "") or "\\text{–}", display, preset)) return f"@@IMG{len(self.texes) - 1}@@" def html(self) -> str: out = "".join(self.parts) for preset in sorted({p for _t, _d, p in self.texes}): idx = [k for k, (_t, _d, p) in enumerate(self.texes) if p == preset] for k, rr in zip(idx, render_many([self.texes[k][:2] for k in idx], preset)): if rr.ok: g = (rr.rasterize(2.0) * 255).astype(np.uint8) tag = f"" else: tag = "nicht setzbar" out = out.replace(f"@@IMG{k}@@", tag) return out def original(z: zipfile.ZipFile | None, name: str, truth: str, display: bool) -> str: try: if name.endswith("#synth.jpg"): data = _dvipng_jpeg(strip_delimiters(truth), display, 102) or b"" else: data = z.read(name) if z else b"" return f"" except Exception: # noqa: BLE001 return "–" def card(page: Page, cls: str, head: str, orig: str, truth: str, display: bool, preset: str, rows: list[tuple[str, str | None]]) -> None: trs = [f"Original{orig}", f"richtig{page.img(truth, display, preset)}{html.escape(truth)}"] for label, tex in rows: trs.append(f"{label}{page.img(tex, display, preset) if tex else '–'}" f"{html.escape(tex or '–')}") page.parts.append(f"
{head}
{''.join(trs)}
") def mark(ok: bool) -> str: return "✓" if ok else "✗" def regeltest(page: Page, summary: list[str], pair: str, z: zipfile.ZipFile | None) -> None: a, b = pair.split(":") ra, rb = (json.loads((ROOT / n / "ergebnis.json").read_text(encoding="utf-8")) for n in (a, b)) sa, sb = ra["summary"], rb["summary"] st = sb.get("structural", {}) summary.append(f"{html.escape(b)} " f"(vorher){sa['formulas']}" f"{sa['accepted']} / {sa['wrong']}{sb['accepted']} / {sb['wrong']}" f"{sa['review']} → {sb['review']}{st.get('agree', 0)} / {st.get('truth', 0)}") old = {c["name"]: c for c in ra["cases"]} preset = rb.get("preset", "cm") tools = ["local", "p2t", "local2"] for c in rb["cases"]: o = old[c["name"]] changed = c["status"] != o["status"] struct = [i for i, j, how in c["agree"] if how == "strukturell"] or [ k for k, h in enumerate(c.get("ok_how", [])) if h == "strukturell"] verdict = [p for p in range(3) if c["readings"][p] and o["readings"][p] and c["ok"][p] != o["ok"][p]] if not (changed or struct or verdict): continue right = c["status"] == "auto_accepted" and c["right"] cls = "bad" if c["status"] == "auto_accepted" and not c["right"] else "good" if right else "rev" head = (f"{html.escape(b)} · {'Entscheidung geändert: ' + o['status'] + ' → ' if changed else ''}" f"{c['status']} · {html.escape(c['reason'])}" + (" · Urteil gegen Wahrheit geändert: " + ", ".join(LABEL[tools[p]] for p in verdict) if verdict else "")) rows = [(f"{mark(c['ok'][p])} {LABEL[t]}" + (" (" + c["ok_how"][p] + ")" if c.get("ok_how") and c["ok_how"][p] else ""), (c["readings"][p] or {}).get("tex")) for p, t in enumerate(tools) if c["readings"][p]] card(page, cls, head, original(z, c["name"], c["truth"], c["display"]), c["truth"], c["display"], preset, rows) def kalibrierung(page: Page, summary: list[str], pair: str, z: zipfile.ZipFile | None) -> None: a, b = pair.split(":") ka, kb = (json.loads(Path(p).read_text(encoding="utf-8")) for p in (a, b)) th = consensus_thresholds() for sa, sb in zip(ka["sets"], kb["sets"]): names = sb["engines"] idx = [names.index(e) for e in ("local", "p2t", "local2")] old = {r["name"]: r for r in sa["results"]} acc = {"ohne": [0, 0], "mit": [0, 0]} synth = sb["label"].endswith("Gehrke-Stil]") preset = "cm" if synth else "txfonts" for r in sb["results"]: o = old[r["name"]] use_o = _rule_outcome(o, idx, th, decide_pair_tiebreak, True) use_n = _rule_outcome(r, idx, th, decide_pair_tiebreak, True) for key, rr, u in (("ohne", o, use_o), ("mit", r, use_n)): if u is not None: acc[key][0] += 1 acc[key][1] += not rr["ok"][u] struct = [(i, j) for i, j, how in r["agree_all"] if how == "strukturell"] struct_t = [k for k, h in enumerate(r.get("ok_how", [])) if h == "strukturell"] verdict_ch = [k for k in range(len(names)) if r["ok"][k] != o["ok"][k]] if (use_o is None) == (use_n is None) and not struct and not struct_t and not verdict_ch: continue verdict = ("übernommen" if use_n is not None else "nicht übernommen") if (use_o is None) != (use_n is None): verdict = f"Entscheidung geändert: {'übernommen' if use_o is not None else 'nicht übernommen'} → {verdict}" wrong = use_n is not None and not r["ok"][use_n] cls = "bad" if wrong else "good" if use_n is not None else "rev" how = "; ".join([f"{LABEL.get(names[i], names[i])} = {LABEL.get(names[j], names[j])} strukturell" for i, j in struct] + [f"{LABEL.get(names[k], names[k])} = Wahrheit strukturell" for k in struct_t] + ["Urteil gegen Wahrheit geändert: " + ", ".join(LABEL.get(names[k], names[k]) for k in verdict_ch)] * bool(verdict_ch)) rows = [(f"{mark(r['ok'][k])} {LABEL.get(e, e)}" + (" ← verwendet" if k == use_n else ""), r["readings"][k].get("tex")) for k, e in enumerate(names)] card(page, cls, f"Kalibrierung {html.escape(sb['label'])} · {verdict} · {html.escape(how)}", original(z, r["name"], r["truth"], r["display"]), r["truth"], r["display"], preset, rows) n = sb["formulas"] summary.append(f"Kalibrierung Band 1 {html.escape(sb['label'].split('[')[-1].rstrip(']'))}{n}" f"{acc['ohne'][0]} / {acc['ohne'][1]}{acc['mit'][0]} / {acc['mit'][1]}" f"–{len(sb.get('structural_examples', []))}") def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--epub1", type=Path, default=None, help="Göllmann Band 1 (Kalibrierung)") ap.add_argument("--epub2", type=Path, default=None, help="Göllmann Band 2 (Regeltests)") ap.add_argument("--regeltest", action="append", default=[], help="OHNE:MIT (Namen unter vergleich/)") ap.add_argument("--kalibrierung", action="append", default=[], help="OHNE.json:MIT.json") ap.add_argument("--name", default="struktur") ap.add_argument("--titel", default="Vergleich vorher / nachher") args = ap.parse_args() z1 = zipfile.ZipFile(args.epub1) if args.epub1 else None z2 = zipfile.ZipFile(args.epub2) if args.epub2 else None page = Page() summary: list[str] = [] for pair in args.regeltest: regeltest(page, summary, pair, z2) for pair in args.kalibrierung: kalibrierung(page, summary, pair, z1) head = (f"

{html.escape(args.titel)}

" "

Dieselben gespeicherten Lesarten, zweimal entschieden: vorher und nachher. " "Regel p2t, mit Pandoc und Bild-Veto. ✓/✗ = gleiche Formel wie die Wahrheit.

" "" "" + "".join(summary) + "
MessungFormelnvorher: automatisch / falschnachher: automatisch / falschReviewnur strukturell gleich (Paare / Wahrheit)
" "

Unten: jede geänderte Entscheidung, jedes geänderte Urteil gegen die Wahrheit und jede " "Übereinstimmung, die nur die Strukturstufe gefunden hat. Grün = übernommen und richtig, Rot = übernommen, aber falsch, " "Gelb = Review. Bitte prüfen: Sind die als gleich gewerteten Lesarten wirklich dieselbe Formel?

") page.parts.insert(0, head) out = ROOT / args.name out.mkdir(parents=True, exist_ok=True) (out / "index.html").write_text( "Vergleich" + page.html(), encoding="utf-8") print(out / "index.html") return 0 if __name__ == "__main__": sys.exit(main())