- 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>
432 lines
26 KiB
Markdown
432 lines
26 KiB
Markdown
# 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 | – |
|