Neue Standardregel p2t (GLM/Cloud wird nicht mehr benutzt):
- pix2text liest zuerst; übernommen wird immer seine Lesart, wenn Qwen sie
bestätigt oder – nur bei Uneinigkeit gefragt – Gemma. Qwen = Gemma gegen
pix2text geht an einen Menschen. Schlusspunkt muss bestätigt sein; Bild-Veto
und Pandoc auf pix2texts Lesart. Alte Regel als consensus_rule=vlm3 wählbar.
- pix2text auf der GPU (onnxruntime-gpu, FORMULA_P2T_DEVICE=auto/cpu/cuda:N)
- Ausgabe bereinigt: post_process aus pix2tex, Fettschreibweisen \boldmath/\bf
-> \boldsymbol/\mathbf; an 400 Lesarten gleiches gesetztes Bild
- Normalform: \mathop, \begin{array}[t]
- calibrate: --engines, Regel-Tabelle, --reuse; wahre Formeln aus dem alt-Text
bereinigt (aligned, \mathop{}\!, upgreek)
- tools/zeittest.py, tools/regeltest.py (--reuse, --exclude)
Messung: Göllmann Band 1 (2 x 150) 86 % / 85 % automatisch, 0 falsch
(bisher 78 % / 70 %); Band 2, zwei Stichproben zu 50: 78 % und 86 %, 0 falsch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
7.4 KiB
Python
145 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
||
"""Time and accuracy per recognition tool on formulas whose TeX is known (serial, one at a time).
|
||
|
||
python3 tools/zeittest.py EPUB [--n 15] [--tools local,local2,cloud,p2t-cpu,p2t-gpu] [--gpu cuda:0]
|
||
|
||
Same sample as `calibrate --consensus` (seed 42, real images). Every tool reads every formula alone,
|
||
one request after the other, after one uncounted warm-up on another formula. Writes
|
||
vergleich/zeittest/index.html (not versioned): table per tool, then per formula the original, the
|
||
true formula and every reading set with pdflatex, with time and verdict; plus ergebnis.json.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import html
|
||
import json
|
||
import random
|
||
import statistics
|
||
import sys
|
||
import time
|
||
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 _best_preset, corpus # noqa: E402
|
||
from formula_img2latex.consensus import same_reading # noqa: E402
|
||
from formula_img2latex.engines import make_engine # noqa: E402
|
||
from formula_img2latex.engines.p2t import Pix2TextEngine # noqa: E402
|
||
from formula_img2latex.render import render_many # noqa: E402
|
||
from formula_img2latex.tex import strip_delimiters # noqa: E402
|
||
|
||
LABELS = {"local": "Qwen3-VL-30B (lokal, GPU)", "local2": "Gemma-4-26B (lokal, GPU)", "cloud": "GLM-4.6V (Cloud)",
|
||
"p2t-cpu": "pix2text MFR 1.5 (CPU)", "p2t-gpu": "pix2text MFR 1.5 (GPU)"}
|
||
|
||
|
||
def engine_for(tool: str, gpu: str):
|
||
if tool == "p2t-cpu":
|
||
return Pix2TextEngine(device="cpu")
|
||
if tool == "p2t-gpu":
|
||
return Pix2TextEngine(device=gpu)
|
||
return make_engine(tool)
|
||
|
||
|
||
def main() -> int:
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("epub", type=Path)
|
||
ap.add_argument("--n", type=int, default=15)
|
||
ap.add_argument("--tools", default="local,local2,cloud,p2t-cpu,p2t-gpu")
|
||
ap.add_argument("--gpu", default="cuda", help="Gerät für p2t-gpu (Index relativ zu CUDA_VISIBLE_DEVICES)")
|
||
ap.add_argument("--name", default="zeittest")
|
||
args = ap.parse_args()
|
||
tools = [t.strip() for t in args.tools.split(",") if t.strip()]
|
||
|
||
items = corpus(args.epub)
|
||
smp = random.Random(42).sample(items, min(150, len(items))) # as calibrate_consensus
|
||
formulas, warm = smp[:args.n], smp[args.n]
|
||
preset = _best_preset(smp[:12])
|
||
|
||
runs: dict[str, dict] = {}
|
||
for tool in tools:
|
||
eng = engine_for(tool, args.gpu)
|
||
t0 = time.time()
|
||
eng.check()
|
||
load = time.time() - t0
|
||
eng.recognize(warm[1], warm[0], "", warm[3]) # warm-up, not counted
|
||
reads = []
|
||
for name, data, truth, disp in formulas:
|
||
r = eng.recognize(data, name, "", disp)
|
||
ok = bool(r.tex) and bool(same_reading(r.tex, truth, disp, preset))
|
||
reads.append({"tex": r.tex, "seconds": r.seconds, "ok": ok, "error": r.error,
|
||
"tokens_in": r.tokens_in, "tokens_out": r.tokens_out})
|
||
print(f"{tool:8s} {r.seconds:6.2f}s {'✓' if ok else '✗'} {r.tex or r.error}", file=sys.stderr, flush=True)
|
||
secs = [x["seconds"] for x in reads]
|
||
runs[tool] = {"label": LABELS.get(tool, tool), "model": eng.model, "load_s": load, "reads": reads,
|
||
"mean_s": statistics.mean(secs), "median_s": statistics.median(secs),
|
||
"correct": sum(x["ok"] for x in reads), "errors": sum(bool(x["error"]) for x in reads),
|
||
"tokens_in": sum(x["tokens_in"] for x in reads), "tokens_out": sum(x["tokens_out"] for x in reads)}
|
||
|
||
out = Path(__file__).resolve().parents[1] / "vergleich" / args.name
|
||
out.mkdir(parents=True, exist_ok=True)
|
||
(out / "ergebnis.json").write_text(json.dumps(
|
||
{"epub": args.epub.name, "preset": preset, "truth": [f[2] for f in formulas], "runs": runs},
|
||
ensure_ascii=False, indent=1), encoding="utf-8")
|
||
|
||
n = len(formulas)
|
||
rows = "".join(
|
||
f"<tr><td>{html.escape(r['label'])}</td><td>{r['mean_s']:.2f}</td><td>{r['median_s']:.2f}</td>"
|
||
f"<td>{r['correct']}/{n} ({r['correct'] / n:.0%})</td><td>{r['load_s']:.1f}</td>"
|
||
f"<td>{r['tokens_in'] + r['tokens_out'] or '–'}</td></tr>" for r in runs.values())
|
||
parts = [f"<h1>Zeittest: {html.escape(args.epub.name[:70])}</h1>",
|
||
f"<p>{n} Formeln mit bekanntem TeX (alt-Text), jedes Tool seriell, eine Formel nach der anderen, "
|
||
"nach einem nicht gezählten Aufwärmlauf. Alle Formeln mit pdflatex gesetzt. ✓ = gleiche Zeichen an "
|
||
"gleicher Stelle wie die wahre Formel (Abstände, kursiv/aufrecht, Fettdruck egal).</p>",
|
||
"<table class=sum><tr><th>Tool</th><th>Ø s/Formel</th><th>Median s</th><th>richtig</th>"
|
||
f"<th>Laden s</th><th>Tokens</th></tr>{rows}</table>"]
|
||
|
||
texes = []
|
||
for i, (_n, _d, truth, disp) in enumerate(formulas):
|
||
texes.append((strip_delimiters(truth), disp))
|
||
texes += [(runs[t]["reads"][i]["tex"] or "\\text{–}", disp) for t in tools]
|
||
rendered = render_many(texes, preset)
|
||
per = 1 + len(tools)
|
||
for i, (name, data, truth, _disp) in enumerate(formulas):
|
||
imgs = []
|
||
for rr in rendered[per * i: per * (i + 1)]:
|
||
if rr.ok:
|
||
g = (rr.rasterize(2.0) * 255).astype(np.uint8)
|
||
imgs.append(f"<img src='{png_uri(Image.fromarray(255 - g))}'>")
|
||
else:
|
||
imgs.append("<i>nicht setzbar</i>")
|
||
body = [f"<tr><th>Original</th><td><img src='{png_uri(orig_img(data, 2))}'></td><td></td></tr>",
|
||
f"<tr><th>richtig</th><td>{imgs[0]}</td><td><code>{html.escape(truth)}</code></td></tr>"]
|
||
for k, t in enumerate(tools):
|
||
r = runs[t]["reads"][i]
|
||
mark = "<b class=ok>✓</b>" if r["ok"] else "<b class=no>✗</b>"
|
||
body.append(f"<tr><th>{mark} {html.escape(runs[t]['label'])}<br><small>{r['seconds']:.2f} s</small></th>"
|
||
f"<td>{imgs[k + 1]}</td><td><code>{html.escape(r['tex'] or r['error'] or '–')}</code></td></tr>")
|
||
parts.append(f"<section><div class=v>Formel {i + 1} · {html.escape(name)}</div><table>{''.join(body)}</table></section>")
|
||
(out / "index.html").write_text(
|
||
"<!doctype html><meta charset=utf-8><title>Zeittest</title><style>"
|
||
"body{font:14px system-ui,sans-serif;margin:16px;background:#f6f6f3}section{background:#fff;border:1px solid #ddd;"
|
||
"border-radius:6px;padding:6px 10px;margin:10px 0}.v{font-weight:600;font-size:13px;margin-bottom:4px}"
|
||
"table.sum{border-collapse:collapse;background:#fff}table.sum td,table.sum th{border:1px solid #ccc;padding:4px 10px}"
|
||
"th{text-align:left;font-weight:400;color:#555;width:210px;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>" + "".join(parts),
|
||
encoding="utf-8")
|
||
|
||
print(f"\n| Tool | Ø s/Formel | Median s | richtig (n={n}) | Laden s |")
|
||
print("|---|---|---|---|---|")
|
||
for r in runs.values():
|
||
print(f"| {r['label']} | {r['mean_s']:.2f} | {r['median_s']:.2f} | {r['correct']}/{n} | {r['load_s']:.1f} |")
|
||
if "p2t-cpu" in runs and "p2t-gpu" in runs:
|
||
same = sum(a["tex"] == b["tex"] for a, b in zip(runs["p2t-cpu"]["reads"], runs["p2t-gpu"]["reads"]))
|
||
print(f"\npix2text CPU und GPU gleicher Text: {same}/{n}")
|
||
print(out / "index.html")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|