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>
205 lines
11 KiB
Python
205 lines
11 KiB
Python
#!/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())
|