formula_img2latex/FUNKTIONSWEISE.md
dschlueter 6a0e9587f5 docs: Dokumentation nach Projektstandard, MIT-Lizenz
- README knapp: Zweck, Schnellstart, Stand, Abschnitt "Documentation", Lizenz
- FUNKTIONSWEISE.md neu: Aufbau, Datenfluss (Mermaid), Entscheidungsregel,
  Normalform, elastischer Bildvergleich, Quellen, Sicherheit, Messungen,
  Designentscheidungen, Grenzen
- BEDIENUNGSANLEITUNG gegliedert: Installation, Modellserver, Benutzung,
  Konfiguration (Umgebungsvariablen, workspace.json), Betrieb, Werkzeuge,
  Fehlersuche mit echten Meldungen und Rückgabecodes
- CHANGELOG im Format Keep a Changelog mit "Beim Update zu tun"
- LICENSE (MIT)
- cli.py: Hilfetexte beschreiben die Regel p2t und z-ai/glm-4.6v statt des
  Pixelvergleichs und gemini-2.5-flash
- requirements.txt: requests ergänzt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:26:49 +02:00

432 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# formula_img2latex – Funktionsweise
Dieses Dokument erklärt, **wie formula_img2latex innen arbeitet und warum es so gebaut ist**. Für die
Benutzung siehe [BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md), für Änderungen je Version
[CHANGELOG.md](CHANGELOG.md).
Stand: Version 0.4.0 und unveröffentlichte Änderungen (siehe CHANGELOG, Abschnitt `[Unreleased]`)
---
## Inhaltsverzeichnis
1. [Überblick](#1-überblick)
2. [Module](#2-module)
3. [Arbeitsordner und Datensätze](#3-arbeitsordner-und-datensätze)
4. [Ablauf von `run`](#4-ablauf-von-run)
5. [Die Erkenner](#5-die-erkenner)
6. [Entscheidungsregel](#6-entscheidungsregel)
7. [Wann zwei Lesarten dieselbe Formel sind](#7-wann-zwei-lesarten-dieselbe-formel-sind)
8. [Bild-Veto: der elastische Bildvergleich](#8-bild-veto-der-elastische-bildvergleich)
9. [apply und revert](#9-apply-und-revert)
10. [Quellen: epub2md und pdf2md](#10-quellen-epub2md-und-pdf2md)
11. [Sicherheit](#11-sicherheit)
12. [Messungen](#12-messungen)
13. [Designentscheidungen](#13-designentscheidungen)
14. [Grenzen](#14-grenzen)
---
## 1. Überblick
Ein Formelbild wird nur durch LaTeX ersetzt, wenn **zwei verschiedenartige Erkenner dieselbe Formel lesen**
und ein grober Bildabgleich nicht widerspricht. Die Annahmeregel des Projekts:
> Jedes Zeichen muss stimmen und an der richtigen Stelle relativ zu den anderen stehen. Abstände,
> Pfeil- und Bruchstrichlängen, Schriftgröße, kursiv/aufrecht und Fettdruck dürfen abweichen.
```mermaid
flowchart TB
A["Markdown von<br/>epub2md oder pdf2md<br/>mit Formelbildern"] --> S["scan<br/>Bildverweise finden,<br/>Formel oder Abbildung?"]
S --> R1["pix2text<br/>(Formel-OCR)<br/>liest jede Formel"]
R1 --> R2["Qwen3-VL<br/>(Bildmodell)<br/>liest jede Formel"]
R2 --> V["Bild-Veto<br/>pix2texts Lesart rendern<br/>und elastisch vergleichen"]
V --> D{"Entscheidung<br/>pix2text = Qwen?"}
D -->|"uneinig"| G["Gemma 4<br/>(zweites Bildmodell)<br/>Stichwahl"]
G --> D2{"Gemma = pix2text?"}
D -->|"einig, Veto und<br/>Pandoc bestanden"| OK["auto_accepted"]
D2 -->|"ja"| OK
D2 -->|"nein"| RV["needs_review<br/>Review-Galerie"]
RV -->|"Mensch übernimmt<br/>oder korrigiert"| OK
OK --> AP["apply<br/>Bildverweis im Markdown<br/>→ LaTeX-Formel"]
```
Übernommen wird immer die Lesart von pix2text; Qwen und Gemma bestätigen nur. Was nicht bestätigt ist,
bleibt Bild, bis ein Mensch entscheidet.
---
## 2. Module
| Modul | Aufgabe | Wichtigste Namen |
|---|---|---|
| `cli.py` | Unterbefehle, Rückgabecodes | `main`, `build_parser` |
| `pipeline.py` | scan, recognize, verify, Entscheidung, `run` in Paketen, apply/revert | `scan`, `recognize`, `verify_all`, `decide_all_consensus`, `run_consensus`, `apply`, `revert` |
| `decide.py` | Annahmeregeln und Schwellen | `decide_pair_tiebreak` (Regel `p2t`), `decide_consensus` (Regel `vlm3`), `decide` (Pixel-Modus), `CONSENSUS_DEFAULTS` |
| `consensus.py` | Sagen zwei Lesarten dasselbe? | `canonical`, `same_reading`, `render_equal` |
| `structure.py` | Strukturvergleich mit sympy (optional, aus) | `structural_equal` |
| `engines/p2t.py` | pix2text (MFR 1.5, ONNX) im Prozess, gebündelt, CPU oder GPU | `Pix2TextEngine`, `tidy_p2t`, `resolve_device` |
| `engines/vlm.py` | Bildmodelle über die OpenAI-API (llama.cpp, OpenRouter), Logprobs | `VisionEngine`, `PROMPT`, `PROMPT_VERSION`, `prepare_image`, `parse_answer` |
| `verify.py` | Bildprüfung einer Lesart (Veto bzw. Pixel-Entscheider) | `verify`, `image_arbiter` |
| `compare.py` | Ausrichtung, elastische Verzerrung, Kennzahlen, Kontrast, Diff-Bilder | `compare`, `elastic_warp`, `contrast`, `diff_image` |
| `render.py` | pdflatex-Batch, Schrift-Presets, Rasterung | `render_many`, `PRESETS` |
| `confusables.py` | Verwechslungsvarianten (nur Pixel-Modus) | `variants`, `EQUIVALENT` |
| `tex.py` | TeX-Hilfen: Begrenzer, Sicherheitsprüfung, Gleichungsnummern | `unsafe_reason`, `split_tag`, `prepare_for_render`, `text_to_tex` |
| `imageio.py` | Originale laden: PNG/GIF/JPG/SVG, Transparenz, getönter Hintergrund | `load_ink`, `gray_to_ink`, `color_stats` |
| `classify.py` | Vorfilter: Formel, unklar oder Abbildung | `classify` |
| `checks.py` | Pandoc/MathML-Prüfung | `pandoc_mathml_ok` |
| `sources/` | Quellen: Markdown-Scan, Begrenzer, Dezimalkomma; epub2md, pdf2md | `open_source`, `scan_markdown`, `Epub2MdSource`, `Pdf2MdSource` |
| `pdfcrop.py` | Box-Bild auf der PDF-Seite finden, 600-dpi-Neuausschnitt | `PdfCropper` |
| `store.py` | Arbeitsordner, ein JSON-Datensatz je Formel | `Workspace`, `Formula`, `STATUSES` |
| `calibrate.py` | Messung an Formelbildern mit bekanntem TeX | `calibrate`, `calibrate_consensus` |
| `review/` | lokaler Review-Server und Seite | `serve` |
| `tools/` | Mess- und Berichtswerkzeuge (nicht Teil des Pakets) | `vergleich.py`, `regeltest.py`, `zeittest.py`, `buch_bericht.py`, `konsens_bericht.py`, `vergleich_bericht.py` |
---
## 3. Arbeitsordner und Datensätze
```text
<ausgabe>.formulas/
├── workspace.json # Quelle, Schrift, Einstellungen, letzter Scan, apply-Zustand
├── formulas/f-<sha12>-<d|i>.json # ein Datensatz je Bild und Darstellung (d = abgesetzt, i = im Text)
├── crops/<id>.png # Original je Formel, einmal ausgeschnitten (PDF-Quellen)
├── review/<id>-{orig,render,rigid,diff}.png # Bilder des Bildabgleichs
├── bericht/ # Ergebnisseite (tools/buch_bericht.py)
└── backup/ # Markdown-Dateien vor dem ersten apply
```
Der Arbeitsordner liegt **neben** der Ausgabe, weil `epub2md --force` die Ausgabe vollständig ersetzt.
Ein Datensatz (`store.Formula`) hält: Bildpfad und SHA-256, Darstellung (abgesetzt oder im Text), alle
Fundstellen im Markdown (Datei, Zeile, genauer Bildverweis, Textausschnitt), die Vorklassifikation, **alle
Lesarten** (je Engine, Modell, Prompt-Version und Nachdenk-Einstellung, mit Zeit und Token-Wahrscheinlichkeiten),
das Ergebnis des Bildabgleichs, Status, Begründung und das endgültige LaTeX (`final_tex`, dazu
`final_suffix` für ein abgetrenntes Satzzeichen).
**Schlüssel und Zwischenspeicher:**
| Was | Schlüssel | Wirkung |
|---|---|---|
| Datensatz | SHA-256 des Bildes + Darstellung | ein erneuter epub2md-Lauf verliert keine Entscheidung |
| Lesart | Engine, Modell, `PROMPT_VERSION`, Nachdenken | dieselbe Frage wird nie zweimal gestellt; fehlgeschlagene Anfragen werden wiederholt |
| Bildabgleich | `VERIFY_VERSION` + geprüfte Lesart | nach einer Änderung am Vergleich wird neu gerechnet |
| Scan | Größe und Zeitstempel aller Markdown-Dateien | beim Fortsetzen entfällt der Scan, solange sich das Markdown nicht ändert |
**Status:** `pending`, `auto_accepted`, `needs_review`, `not_formula`, `rejected_keep_image`,
`render_failed`, `no_candidate`; nach dem Review `accepted_by_human`, `corrected_by_human`,
`kept_by_human`. `apply` schreibt `auto_accepted`, `accepted_by_human` und `corrected_by_human`.
---
## 4. Ablauf von `run`
`run` verarbeitet die Formeln in **Paketen zu 50** und meldet nach jedem Paket eine Statuszeile mit Zeiten
je Stufe. Beide Stufen laufen gleichzeitig: Während die CPU Paket n gegen die Bilder prüft, lesen
pix2text und Qwen auf der GPU schon Paket n+1.
```mermaid
flowchart TB
subgraph GPU["GPU: Lesen (Paket n+1)"]
direction TB
P["pix2text<br/>gebündelt nach<br/>Pixelbudget"] --> Q["Qwen<br/>bis 8 Anfragen<br/>gleichzeitig"]
end
subgraph CPU["CPU: Prüfen (Paket n)"]
direction TB
V1["Bild-Veto<br/>Prozesspool"] --> E1["Entscheidung"]
E1 --> T{"Stichwahl<br/>nötig?"}
T -->|"ja"| G["Gemma<br/>(zweite GPU)"]
G --> E2["Entscheidung"]
T -->|"nein"| Z["Statuszeile"]
E2 --> Z
end
```
- **Scan** (`pipeline.scan`): findet jeden Bildverweis außerhalb von Codeblöcken, fasst gleiche Bilder
zusammen, klassifiziert (Farbe der Tinte, Graustufen, Höhe, Stellung im Text) und speichert. Abbildungen
(`figure`) werden nicht gelesen, unklare (`maybe`) schon.
- **Lesen:** pix2text liest jede Formel, Qwen ebenfalls. Gemma nur, wenn pix2text und Qwen uneinig sind.
- **Prüfen** (`verify_all`): rendert nur die Lesart, die übernommen werden kann (pix2texts), und vergleicht
sie mit dem Original. Die Prozesse starten per `forkserver`, weil der Lesefaden gleichzeitig in
CUDA/pix2text arbeitet und ein `fork` mitten darin halb gehaltene Sperren erbt.
- **Entscheiden** (`decide_all_consensus`): siehe [6](#6-entscheidungsregel).
Ein abgebrochener Lauf setzt beim nächsten `run` fort: Lesarten und Prüfergebnisse liegen im
Arbeitsordner.
---
## 5. Die Erkenner
| Engine | Modell | Art | Rolle (Regel `p2t`) |
|---|---|---|---|
| `p2t` | pix2text MFR 1.5 (ONNX) | Formel-OCR, im Prozess | liest zuerst, **seine Lesart wird übernommen** |
| `local` | Qwen3-VL-30B-A3B-Instruct (llama.cpp, Port 8011) | Bildmodell (MoE, ~3B aktiv) | bestätigt |
| `local2` | Gemma-4-26B-A4B-it (llama.cpp, Port 8012) | Bildmodell | Stichwahl bei Uneinigkeit |
| `cloud` | GLM-4.6V über OpenRouter | Bildmodell | nur Regel `vlm3` |
**pix2text** (`engines/p2t.py`) läuft auf der sichtbaren GPU mit dem meisten freien Speicher (sonst CPU) und
liest gebündelt. Seine Ausgabe wird bereinigt (`tidy_p2t`): überflüssige Leerzeichen entfernt
`post_process` aus pix2tex (`\frac{\alpha} {3 6 0}` → `\frac{\alpha}{360}`), Fettschreibweisen wie
`\mathrm{\boldmath~ x ~}` werden zu `\boldsymbol{x}`, `\cfrac` zu `\dfrac` (Pandoc kennt `\cfrac` nicht),
eine Gleichungsnummer nach großem Abstand zu `\tag{…}`.
**Bildmodelle** (`engines/vlm.py`) bekommen das Bild vorbereitet (`prepare_image`): auf Weiß gelegt,
kleine Formeln auf mindestens 96 px Höhe vergrößert, große auf höchstens 1600 px Kantenlänge verkleinert.
Der Prompt verlangt eine buchstabengetreue Abschrift (ausdrücklich mit Fettdruck), `\tag{}` für
Gleichungsnummern und eine JSON-Antwort; ein kurzer Textausschnitt um die Formel wird zur Orientierung
mitgegeben. Weitere Festlegungen:
- **Nachdenken aus:** Gemma 4 wird ~10× schneller, GLM liest ohne besser und billiger.
- **Antwortlänge** höchstens 800 Tokens: lange mehrzeilige Formeln brauchen ~400, Endlosschleifen werden
gekappt.
- **Token-Wahrscheinlichkeiten** (Logprobs) des LaTeX-Teils werden gespeichert. Die Selbstauskunft
(„confidence“) wird erfragt, entscheidet aber nicht.
- **Robuste Auswertung:** JSON mit einfachen Backslashes (`\frac` → Seitenvorschub + `rac`) wird erkannt
und wörtlich neu gelesen; wörtliches `\n` in Antworten wird zu Leerraum.
---
## 6. Entscheidungsregel
### Regel `p2t` (Standard, `decide.decide_pair_tiebreak`)
Lesarten: Qwen (0), pix2text (1), Gemma (2, nur bei Bedarf).
```mermaid
flowchart TB
S["Qwen: keine Formel<br/>(und Gemma auch)?"] -->|"ja"| NF["not_formula"]
S -->|"nein"| A{"pix2text = Qwen?"}
A -->|"ja"| C["Prüfungen"]
A -->|"nein"| B{"Gemma gefragt?"}
B -->|"nein"| W["needs_review:<br/>Stichwahl fehlt<br/>→ Gemma fragen"]
B -->|"ja"| G{"Gemma = pix2text?"}
G -->|"ja"| C
G -->|"nein, aber<br/>Gemma = Qwen"| H["needs_review:<br/>Mensch entscheidet"]
G -->|"alle uneinig"| H
C --> K{"Pandoc ok und<br/>Ähnlichkeit ≥ 0,65?"}
K -->|"ja"| OK["auto_accepted<br/>(pix2texts Lesart)"]
K -->|"nein"| R["needs_review"]
```
- Übernommen wird **immer pix2texts Lesart**: Sie trifft den Fettdruck am besten.
- **Qwen und Gemma gemeinsam gegen pix2text** gehen an einen Menschen. In der Messung waren genau das die
Fehler, die beide Bildmodelle teilen (`\mathcal{E}_r` statt `\varepsilon_r`, ein erfundenes `\tilde{z}`).
Der Bild-Schiedsrichter (`arbiter`, siehe [12](#12-messungen)) könnte diese Fälle entscheiden; er ist aus.
- **Satzzeichen:** Bei abgesetzten Formeln muss auch der Schlusspunkt bestätigt sein. Bei Formeln im Text
wird ein Satzzeichen am Ende vor dem Vergleich abgetrennt und als Text hinter die Formel gesetzt
(`$a^{x}$,`), denn es gehört zum Satz.
- **PDF-Textebene** (`pdftext`, aus): Bei `pdf2md --formulas` könnte der PDF-Text einer „flachen“ Formel
(eine Grundlinie, eine Größe) Qwen ersetzen. Gemessen fehlten dort in 12 von 319 Fällen Punkte, Striche
oder Indizes.
### Regel `vlm3` (bis 0.2.0, `decide.decide_consensus`)
Qwen = Gemma, bestätigt von GLM-4.6V aus einer anderen Modellfamilie; eine 2:1-Mehrheit reicht nicht.
Übernommen wird Qwens Lesart; der bestätigenden Lesart darf der Schlusspunkt fehlen. Wählbar mit
`"consensus_rule": "vlm3"` in `workspace.json`.
### Pixel-Modus (bis 0.1.0, `decide.decide`)
Eine Lesart wird gerendert und muss jede Verwechslungsvariante (`confusables.variants`: andere Ziffer,
hoch statt tief, `,`/`;`, ähnliche Buchstaben …) im Bild deutlich schlagen. Sicher, aber an kleinen
JPEG-Formeln zu streng. Wählbar mit `"decide": "pixel"`.
### Schwellen (`settings.consensus_thresholds` in `workspace.json`)
| Schlüssel | Standard | Bedeutung |
|---|---|---|
| `veto_ncc` | 0,65 | Mindestähnlichkeit nach elastischer Ausrichtung |
| `veto_scale_dev` | 0 (aus) | erlaubte Abweichung der Schriftgröße vom Buchmittel |
| `bold_relevant` | 0 | 1: fett und normal gelten als verschiedene Zeichen |
| `require_third` | 1 | Regel `vlm3`: dritte Lesart muss zustimmen |
| `min_p_mean`, `min_p_min` | 0 | Mindest-Token-Wahrscheinlichkeiten (Mittel, kleinste) |
| `structural` | 0 | Strukturvergleich mit sympy |
| `arbiter`, `arbiter_margin` | 0, 5 | Bild-Schiedsrichter bei „Qwen = Gemma gegen pix2text“ |
| `pdftext` | 0 | PDF-Textebene darf pix2text bestätigen |
---
## 7. Wann zwei Lesarten dieselbe Formel sind
`consensus.same_reading` prüft in dieser Reihenfolge:
1. **Normalform** (`canonical`): Beide Lesarten werden in Tokens zerlegt und vereinheitlicht.
- **Entfällt:** Abstände (`\,`, `\quad` …), `\left`/`\right`/`\big`, `\displaystyle`, aufrecht/kursiv
(`\mathrm`, `\text`, `\operatorname` behalten nur ihren Inhalt), standardmäßig auch Fettdruck.
- **Synonyme:** `\vec` = `\overrightarrow`, `\frac` = `\dfrac` = `\cfrac`, `\le` = `\leq`,
`\iff` = `\Leftrightarrow`, `\cdots` = `\dots`, `\root3\of{x}` = `\sqrt[3]{x}`,
`\underset{…}{\lim}` = `\lim_{…}`, TeX-Umlaute (`f{\"u}r` = `für`), `\atop` = `\substack`.
- **Umgebungen:** `pmatrix` = `\left(\begin{matrix}…\right)`, `array` = `matrix`. Mehrzeilige Formeln
sind gleich, wenn sie dieselben Zeilen mit denselben Zeichen haben, egal ob `aligned`, `gathered`,
`split` oder nur `\\`; `cases` = `\left\{\begin{array}…\right.`. In Matrizen trennt `&` weiter Spalten.
- **Klammergruppen:** Eine Gruppe ist nur dann Argument, wenn der Befehl davor noch eines erwartet
(`\frac` 2, `\sqrt` 1, `\Delta` 0). So wird `t^{2}\,{\sqrt{2}}` = `t^2\sqrt{2}`, aber
`\frac{a}{b+c}` ≠ `\frac{a}{b}+c`.
- Eine Gleichungsnummer ist Teil der Normalform: Alle Lesarten müssen dieselbe tragen.
2. **Strukturvergleich** (`structure.py`, nur mit `structural` = 1): gleicher *unausgewerteter* sympy-Baum
(`\frac{a}{b}` = `a/b`). Nichts wird umgerechnet (`a+b` ≠ `b+a`), Relationen gehen nie an sympy, jedes
Zeichen ist durch einen Platzhalter geschützt.
3. **Render-Gleichheit** (`render_equal`): Beide Lesarten werden mit pdflatex gesetzt und Pixel gegen Pixel
verglichen (höchstens 0,2 % abweichende Tintenpixel). Das ist ein Vergleich **Render gegen Render**,
exakt und rauschfrei. Haben die Normalformen verschiedene Buchstaben oder Ziffern, entfällt dieser
Schritt.
---
## 8. Bild-Veto: der elastische Bildvergleich
Das Veto fängt Fehler ab, die mehrere Erkenner gemeinsam machen (weggelassener Term, mitgelesene
Gleichungsnummer, Halluzination). Es entscheidet nicht über Kleinigkeiten.
1. **Rendern** (`render.py`): pdflatex mit dem Paket `preview`, viele Formeln je Lauf. Formeln mit
TeX-Fehlern werden über die Zeilennummern im Log erkannt und herausgenommen, der Rest neu übersetzt.
Die Schrift wählt `choose_preset` je Buch an einer Stichprobe: `cm`, `times`, `txfonts`, `palatino`,
`stix` oder `euler`.
2. **Normalisieren** (`imageio.gray_to_ink`): Transparenz auf Weiß, helle Schrift auf dunklem Grund wird
umgekehrt, die Tinte zwischen Papierton und dunkelstem Pixel gestreckt (ein getönter Kasten wird so zu
Papier), JPEG-Rauschen unter einer Schwelle entfernt, zugeschnitten. Breite Originale werden auf
1600 px verkleinert, Ausschnitte über 2,5 MP nicht verglichen.
3. **Ausrichten** (`compare.py`): gleichmäßig skalieren (nie strecken), auf Viertelpixel verschieben.
Weichen die Proportionen mehr als 2,5-fach ab, gibt es keinen Vergleich.
4. **Elastisch verzerren** (`elastic_warp`): dynamische Zeitverzerrung (DTW) über Spalten, dann Zeilen.
Jede Originalspalte schiebt den Kandidaten um 0–3 Spalten weiter. Dehnen kostet proportional zur Tinte
der Spalte, übersprungene Tinte ihren vollen Wert. Abstände und Pfeilschäfte dürfen sich so ändern,
Zeichenformen praktisch nicht, und kein Zeichen kann verschwinden.
5. **Messen:** Korrelation (NCC) nach der Verzerrung; das Veto verlangt ≥ 0,65.
Die Bilder (Original, starr und elastisch ausgerichtetes Render, farbiges Diff) landen in `review/` und
erscheinen in der Review-Galerie.
---
## 9. apply und revert
- **Sicherung:** Das erste `apply` kopiert jede betroffene Markdown-Datei nach `backup/`. Jedes `apply`
baut aus dieser Sicherung neu auf, ist also wiederholbar. Hat sich eine Datei seitdem geändert
(neuer epub2md-Lauf), gilt sie als neues Original.
- **Ersetzen:** Der genaue Bildverweis in der gespeicherten Zeile wird ersetzt: abgesetzt durch `$$…$$`,
im Text durch `$…$` (epub2md `--math tex`: `\[…\]` bzw. `\(…\)`).
- **Gleichungsnummer:** Sie kommt nur aus der übernommenen Lesart und steht bei abgesetzten Formeln als
`\qquad\text{(3.7)}` hinter der Formel, wie bei epub2md.
- **Deutsche Bücher:** Dezimalzahlen werden zu `3{,}6`, sonst setzt LaTeX „3, 6“.
- **revert** spielt die Sicherung zurück, außer bei Dateien, die seit `apply` von Hand geändert wurden
(mit `--force` auch diese).
---
## 10. Quellen: epub2md und pdf2md
| Quelle | Erkennung | Original für den Vergleich |
|---|---|---|
| epub2md-Ordner | `conversion-report.json` (`tool: epub2md`); Warnungen `formula-image` markieren sichere Formeln | das extrahierte Bild |
| pdf2md (Box-Bilder) | Bildname `<pdf>-<seite>-<box>`; mit `--pdf` per Template-Matching auf der Seite gefunden | 600-dpi-Neuausschnitt aus dem Vektor-PDF (`pdfcrop.py`), einmal in `crops/` |
| pdf2md `--formulas` | Bildname `<pdf>-<seite>-f<nr>.png`, Liste `formulas-*.json` mit Seite, Rechteck, Art und PDF-Text | das Bild selbst (300 dpi aus dem Vektor-PDF) |
Mit `pdf2md --formulas` findet die Formelerkennung von pix2text (MFD) jede Formel auf der Seite, auch im
Fließtext und in Merkkästen; pdf2md schneidet sie zeichengenau aus dem Text. Ob ein Box-Bild eine Formel
ist, verraten bei Box-Bildern die Schriften im Ausschnitt (CMMI, CMSY, EURM …).
---
## 11. Sicherheit
- **Lesarten sind unsicher:** Sie kommen von Modellen. Vor pdflatex lehnt `tex.unsafe_reason` Befehle ab,
die Dateien lesen oder schreiben, Code ausführen oder Makros umdefinieren (`\input`, `\write`, `\def`,
`\catcode`, `\csname` …), außerdem `^^`-Zeichencodes, Begrenzer und unausgeglichene Klammern.
- **pdflatex** läuft mit `-no-shell-escape`, `openout_any=p`, `openin_any=p` und Zeitlimit.
- **Review-Server** bindet nur an `127.0.0.1`; jede Entscheidung muss ein Zufallstoken der Seite
mitschicken, damit fremde Webseiten im selben Browser nichts entscheiden können.
- **Daten nach außen:** Mit der Standardregel verlässt nichts den Rechner. Nur die Regel `vlm3` schickt
Formelbilder samt Textausschnitt an OpenRouter.
---
## 12. Messungen
Gemessen wird an Formelbildern mit **bekanntem TeX**: Springer-EPUBs (Göllmann) tragen das TeX jeder Formel
im alt-Text. Dazu kommen Nachbildungen im Stil von Gehrke (LaTeX + dvipng mit Bitmap-Schriften, ~102 dpi,
JPEG). Werkzeuge: `calibrate --consensus`, `tools/regeltest.py`, `tools/zeittest.py`.
### Regeln im Vergleich (Göllmann Band 1, je 150 echte und 150 nachgebildete Formeln, Stand 0.3.0)
| Regel | echt: automatisch / falsch | Gehrke-Stil: automatisch / falsch |
|---|---|---|
| Qwen = Gemma | 73 % / 1 | 69 % / 2 |
| Qwen = Gemma = Gemini 2.5 Flash | 67 % / 0 | 57 % / 1 |
| Qwen = Gemma = GLM-4.6V (Regel `vlm3`) | 78 % / 0 | 70 % / 0 |
| pix2text = Qwen | 81 % / 0 | 76 % / 0 |
| **pix2text zuerst, bereinigt, Stichwahl Gemma** (Regel `p2t`) | **86 % / 0** | **85 % / 0** |
| + Stichwahl auch Qwen = Gemma gegen pix2text | 92 % / 1 | 91 % / 2 |
Mit der mehrzeiligen Normalform (0.4.0), gleiche Lesarten: **89 % / 0** und **88 % / 0**; Göllmann Band 2
(zwei Stichproben zu 50): 44 und 45 automatisch, 0 falsch.
Allein richtig (Stand 0.3.0): pix2text 90 % / 85 %, Qwen 86 % / 84 %, Gemma 86 % / 80 %, GLM 86 % / 83 %.
Zeit je Formel seriell: pix2text 0,10 s (GPU) bzw. 0,51 s (CPU), Qwen 0,72 s, GLM 1,19 s, Gemma 1,77 s.
### Ganze Bücher (ohne bekannte Lösung)
| Buch | Version | Formelbilder | automatisch | Review | Prüfung von Hand |
|---|---|---|---|---|---|
| Gehrke, Brückenkurs Mathematik (EPUB) | 0.2.0, Regel `vlm3` | 1505 | 942 (63 %) | 478 | 62 von 62 korrekt |
| Corral, *Elementary Calculus* (PDF, Box-Bilder) | 0.4.0 | 1150 | 429 (37 %) | 633 | – |
| Corral mit `pdf2md --formulas`, Stichprobe 10 % | unveröffentlicht | 940 | 747 (79 %) | 174 | – |
Bei Gehrke ergab Pandoc `--mathml` nach `apply` für 945 von 945 Formeln MathML. Bei Corral (Box-Bilder)
bestand das Review überwiegend aus Ausschnitten, die keine einzelne Formel waren (Merkkästen mit Text,
Tabellen, Herleitungen); `--formulas` behebt das weitgehend.
### Gemessen und verworfen
- **Selbstauskunft** der Modelle („95 % sicher“) und **pix2texts eigene Sicherheit**: Fehler auch bei 1,0.
- **Token-Wahrscheinlichkeiten** als Schwelle: neben der Bestätigung kein Gewinn, nur weniger Abdeckung.
- **Gemini als dritte Lesart:** mit Gemma verwandt, ließ einen gemeinsamen Fehler durch.
- **GLM mit Nachdenken:** 79 % statt 85 % allein richtig, ~25× mehr Tokens.
- **Bild-Schiedsrichter** (`verify.image_arbiter`) bei „Qwen = Gemma gegen pix2text“: In 16 Fällen hatte
Qwen 14-mal recht, aber der gemeinsame Fehler `\mathcal{E}_r` bekam Beleg 7,2 – zu knapp für eine
Schwelle von 8.
- **Strukturvergleich mit sympy:** nach der mehrzeiligen Normalform kein zusätzlicher Treffer.
- **Prompt-Stilvorgabe** (feste Schreibweisen): keine Verbesserung, die Modelle schrieben wörtliches `\n`.
- **PDF-Textebene als Bestätigung:** 12 von 319 widersprachen Qwen, mit fehlenden Punkten und Indizes.
- **Qwen3-VL-8B statt 30B-A3B:** nicht schneller (das MoE-Modell rechnet nur ~3B je Token), etwas schlechter.
- **Schriftgrößen-Prüfung im Veto:** nur Fehlalarme.
„0 falsch“ bei einigen hundert Formeln bedeutet eine Fehlerquote sehr wahrscheinlich unter ~2 %, nicht
null. Die Kalibrierung lief ohne den Textausschnitt, den `run` mitgibt.
---
## 13. Designentscheidungen
| Entscheidung | Verworfene Alternativen | Grund |
|---|---|---|
| **Eigenes Nachbearbeitungs-Werkzeug** | Option in epub2md/pdf2md | Die Konverter bleiben schlank und offline; torch, Modelle und GPU-Server gehören nicht hinein. |
| **Erkenner entscheiden, Bild nur Veto** | Pixelvergleich als Entscheider (0.1.0) | Bei 15 px hohen JPEG-Formeln mit Bitmap-Hinting trennte der Pixelvergleich richtig und falsch zu schlecht: 1–2 % automatisch. |
| **Zwei verschiedenartige Erkenner** | ein Modell mit Selbstauskunft; zwei Modelle einer Familie | Selbstauskünfte sind wertlos; verwandte Modelle machen dieselben Fehler (Gemini/Gemma). pix2text (Formel-OCR) und Qwen (Bildmodell) irren verschieden. |
| **pix2texts Lesart übernehmen** | Lesart des Bildmodells | pix2text liest allein am häufigsten richtig und trifft den Fettdruck am besten. |
| **Qwen + Gemma gegen pix2text → Mensch** | 2:1-Mehrheit | Genau diese Fälle waren die gemeinsamen Fehler der Bildmodelle (+6 % Abdeckung, aber Fehler). |
| **Normalform statt String-Vergleich** | Zeichenkette; sympy-Auswertung | Die Erkenner schreiben dieselbe Formel verschieden; sympy rechnet um und verwischt Unterschiede (`v=(3,4)ᵀ` und `\nu=(3,4)ᵀ` werden beide `False`). |
| **Render gegen Render als Rückfallebene** | Render gegen Original | Zwei Renders sind exakt vergleichbar; das Original ist verrauscht und anders gesetzt. |
| **Elastischer Vergleich mit Tintenkosten** | starre Ausrichtung; freie Verzerrung | Starr scheitert an anderen Abständen und Pfeillängen; frei könnte ein falsches Zeichen passend biegen. |
| **Fettdruck zählt nicht** | Fettdruck als eigenes Zeichen | Vorgabe des Nutzers; alle Bildmodelle lesen Fettdruck schlecht. |
| **Nachdenken der Modelle aus** | Standard der Server | Schneller, billiger, bei GLM sogar genauer. |
| **Arbeitsordner neben der Ausgabe, Schlüssel = Bildinhalt** | Daten in der Ausgabe; Dateinamen als Schlüssel | `epub2md --force` ersetzt die Ausgabe; Dateinamen ändern sich, Bildinhalte nicht. |
| **apply aus Sicherung** | Ersetzen im laufenden Markdown | Wiederholbar, rückgängig zu machen, neue Konverterläufe werden erkannt. |
| **Pakete zu 50, Lesen und Prüfen überlappt** | alles nacheinander | Statuszeilen für den Nutzer; die GPUs warten nicht auf die CPU (Pilot 869 Formeln: 70 → ~24 Minuten). |
| **Lokale Modelle, Cloud optional** | Cloud als Standard | Buchinhalte bleiben auf dem Rechner; die lokale Regel misst besser als die mit GLM. |
| **MIT-Lizenz** | GPL | Einfache Wiederverwendung. |
---
## 14. Grenzen
| Grenze | Auswirkung | Mögliche Abhilfe |
|---|---|---|
| Mehrzeilige Rechnungen mit verschiedenen Zeilenumbrüchen, `\underbrace`, fehlenden Integrationsgrenzen | landen im Review | Normalform erweitern |
| Fettdruck wird nicht geprüft | ein fetter Vektor kann als normales v übernommen werden | `bold_relevant` = 1 schickt solche Fälle ins Review |
| Formeln auf farbig hinterlegten Kästen | verlieren beim Ersetzen die Hinterlegung | – |
| Formeln in Tabellen | erfasst `pdf2md --formulas` noch nicht | Erkennung in Tabellenzellen |
| Riesenausschnitte (Grafiken, ganze Herleitungen) | ohne Bildvergleich abgelehnt (über 2,5 MP, Proportionen über 2,5-fach) | bessere Trennung in pdf2md |
| Messbasis | einige hundert Formeln eines Verlags; Textausschnitt nicht kalibriert | weitere Bücher mit bekannter Lösung |
| Laufzeit | ~0,8 s je Formel mit zwei RTX 3090 | – |