formula_img2latex/tools/vergleich_bericht.py

205 lines
11 KiB
Python
Raw Permalink Normal View History

feat: 0.4.0 – mehrzeilige Normalform, erstes ganzes PDF-Buch Normalform (Göllmann, 400 Formeln: +17 automatisch, weiterhin 0 falsch): - mehrzeilige Formeln gleich bei gleichen Zeilen, egal in welchem Rahmen (aligned/&/\\, cases = \left\{array\right. = \left\{aligned\right.), leere letzte Zeile zählt nicht; Matrizen behalten & als Spaltentrenner - Argumente nach Argumentzahl des Befehls (t^{2}\,{\sqrt{2}} = t^2\sqrt{2}) - \cdot\cdot\cdot = \dots, \root3\of{x} = \sqrt[3]{x}, \left./\right. entfallen, \cfrac = \frac; wörtliche \n aus Modellantworten werden zu Leerraum Gemessen und abgeschaltet bzw. verworfen: - Strukturvergleich mit sympy (structure.py, optional, FORMULA_STRUCTURAL=1): nach der neuen Normalform kein Gewinn mehr - Bild-Schiedsrichter (verify.image_arbiter, arbiter=0): der gemeinsame Fehler \mathcal{E}_r bekam Beleg 7,2 – Abstand zu knapp - Prompt-Stilvorgabe: kein Gewinn, Prompt-Version bleibt 2 Ganzes PDF-Buch (Corral, Elementary Calculus): 429 von 1123 automatisch. - Bilder für die Modelle höchstens 1600 Pixel (Qwen lehnte große ab), Bildvergleich nur bis 4 MP (ganzseitige Abbildung brachte ihn zum Absturz) - fehlgeschlagene Modellanfragen werden wiederholt - pix2text: \cfrac -> \dfrac (Pandoc kennt \cfrac nicht) Werkzeuge: tools/buch_bericht.py (Ergebnisseite eines Buchs), tools/vergleich_bericht.py (Vorher/Nachher), regeltest --refresh/--arbiter, calibrate misst Schiedsrichter und strukturelle Treffer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:30:15 +02:00
#!/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/<name>/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"<img src='{png_uri(Image.fromarray(255 - g))}'>"
else:
tag = "<i>nicht setzbar</i>"
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"<img src='{png_uri(orig_img(data, 2))}'>"
except Exception: # noqa: BLE001
return "<i>–</i>"
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"<tr><th>Original</th><td>{orig}</td><td></td></tr>",
f"<tr><th>richtig</th><td>{page.img(truth, display, preset)}</td><td><code>{html.escape(truth)}</code></td></tr>"]
for label, tex in rows:
trs.append(f"<tr><th>{label}</th><td>{page.img(tex, display, preset) if tex else '<i>–</i>'}</td>"
f"<td><code>{html.escape(tex or '–')}</code></td></tr>")
page.parts.append(f"<section class={cls}><div class=v>{head}</div><table>{''.join(trs)}</table></section>")
def mark(ok: bool) -> str:
return "<b class=ok>✓</b>" if ok else "<b class=no>✗</b>"
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"<tr><td><a href='../{b}/index.html'>{html.escape(b)}</a> "
f"<small>(<a href='../{a}/index.html'>vorher</a>)</small></td><td>{sa['formulas']}</td>"
f"<td>{sa['accepted']} / {sa['wrong']}</td><td><b>{sb['accepted']} / {sb['wrong']}</b></td>"
f"<td>{sa['review']} → {sb['review']}</td><td>{st.get('agree', 0)} / {st.get('truth', 0)}</td></tr>")
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"<tr><td>Kalibrierung Band 1 {html.escape(sb['label'].split('[')[-1].rstrip(']'))}</td><td>{n}</td>"
f"<td>{acc['ohne'][0]} / {acc['ohne'][1]}</td><td><b>{acc['mit'][0]} / {acc['mit'][1]}</b></td>"
f"<td>–</td><td>{len(sb.get('structural_examples', []))}</td></tr>")
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"<h1>{html.escape(args.titel)}</h1>"
"<p>Dieselben gespeicherten Lesarten, zweimal entschieden: vorher und nachher. "
"Regel <code>p2t</code>, mit Pandoc und Bild-Veto. ✓/✗ = gleiche Formel wie die Wahrheit.</p>"
"<table class=sum><tr><th>Messung</th><th>Formeln</th><th>vorher: automatisch / falsch</th>"
"<th>nachher: automatisch / falsch</th><th>Review</th><th>nur strukturell gleich (Paare / Wahrheit)</th></tr>"
+ "".join(summary) + "</table>"
"<p>Unten: jede geänderte Entscheidung, jedes geänderte Urteil gegen die Wahrheit und jede "
"Übereinstimmung, die <b>nur</b> die Strukturstufe gefunden hat. Grün = übernommen und richtig, <b style='color:#a32020'>Rot = übernommen, aber falsch</b>, "
"Gelb = Review. Bitte prüfen: Sind die als gleich gewerteten Lesarten wirklich dieselbe Formel?</p>")
page.parts.insert(0, head)
out = ROOT / args.name
out.mkdir(parents=True, exist_ok=True)
(out / "index.html").write_text(
"<!doctype html><meta charset=utf-8><title>Vergleich</title><style>"
"body{font:14px system-ui,sans-serif;margin:16px;background:#f6f6f3}section{background:#fff;border:1px solid #ddd;"
"border-left:6px solid #999;border-radius:6px;padding:6px 10px;margin:10px 0}section.good{border-left-color:#1f7a3a}"
"section.bad{border-left-color:#a32020;background:#fff5f5}section.rev{border-left-color:#c88a00}"
".v{font-weight:600;font-size:13px}table.sum{border-collapse:collapse;background:#fff}"
"table.sum td,table.sum th{border:1px solid #ccc;padding:4px 10px;width:auto}"
"th{text-align:left;font-weight:400;color:#555;width:190px;vertical-align:middle}td{padding:3px 8px;vertical-align:middle}"
".ok{color:#1f7a3a}.no{color:#a32020}code{font-size:12px}img{max-width:640px}</style>" + page.html(),
encoding="utf-8")
print(out / "index.html")
return 0
if __name__ == "__main__":
sys.exit(main())