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>
This commit is contained in:
Dieter Schlüter 2026-10-20 12:26:49 +02:00
commit 6a0e9587f5
7 changed files with 871 additions and 581 deletions

View file

@ -4,17 +4,32 @@ Dieses Programm ersetzt Formelbilder in den Markdown-Dateien von epub2md und pdf
LaTeX-Formeln. Dadurch passen die Formeln beim Neubau als EPUB oder PDF in Größe und Schrift zum Text.
Es liest jede Formel zuerst mit dem Formel-OCR **pix2text** und ersetzt sie nur, wenn das Bildmodell
**Qwen** dieselbe Formel liest (bei Uneinigkeit darf das zweite Bildmodell Gemma pix2text bestätigen) und
das gerenderte LaTeX dem Bild grob ähnelt. Eingesetzt wird die Lesart von pix2text, ohne ihre
überflüssigen Leerzeichen. Alles läuft lokal. Bei Zweifeln fragt es dich in einer
Review-Seite im Browser. Ohne deine Zustimmung bleibt die Formel ein Bild.
**Qwen** dieselbe Formel liest (bei Uneinigkeit darf das zweite Bildmodell **Gemma** pix2text bestätigen)
und das gerenderte LaTeX dem Bild grob ähnelt. Eingesetzt wird die Lesart von pix2text. Alles läuft lokal.
Bei Zweifeln fragt es dich in einer Review-Seite im Browser; ohne deine Zustimmung bleibt die Formel ein
Bild.
„Dieselbe Formel“ heißt: jedes Zeichen stimmt und steht an der richtigen Stelle. Abstände, Pfeillängen,
Schriftgröße, kursiv/aufrecht und Fettdruck dürfen abweichen.
Schriftgröße, kursiv/aufrecht und Fettdruck dürfen abweichen. Wie das intern geprüft wird, steht in
[FUNKTIONSWEISE.md](FUNKTIONSWEISE.md).
---
## 1. Einrichtung (einmalig)
## Inhalt
1. [Installation](#1-installation)
2. [Modellserver starten und beenden](#2-modellserver-starten-und-beenden)
3. [Ein Buch bearbeiten](#3-ein-buch-bearbeiten)
4. [Einzelne Schritte und Optionen](#4-einzelne-schritte-und-optionen)
5. [Konfiguration](#5-konfiguration)
6. [Betrieb](#6-betrieb)
7. [Werkzeuge zum Messen und Einstellen](#7-werkzeuge-zum-messen-und-einstellen)
8. [Fehlersuche](#8-fehlersuche)
9. [Häufige Fragen](#9-häufige-fragen)
---
## 1. Installation
```bash
sudo apt install texlive-latex-extra texlive-fonts-extra texlive-science pandoc dvipng
@ -23,40 +38,14 @@ pip install -r requirements.txt
python3 formula_img2latex.py --version
```
### Die beiden lokalen Bildmodelle starten
Zwei Vision-Sprachmodelle lesen jede Formel, je eines pro Grafikkarte (je ~18–21 GB). In zwei Terminals:
```bash
# Modell A: Qwen3-VL auf GPU 1, Port 8011
M=~/endeavour_home/lm_studio/models/lmstudio-community/Qwen3-VL-30B-A3B-Instruct-GGUF
CUDA_VISIBLE_DEVICES=1 ~/.local/bin/llama-server \
-m $M/Qwen3-VL-30B-A3B-Instruct-Q4_K_M.gguf --mmproj $M/mmproj-Qwen3-VL-30B-A3B-Instruct-F16.gguf \
--host 127.0.0.1 --port 8011 -ngl 99 -c 32768 --parallel 8
# Modell B: Gemma 4 auf GPU 2, Port 8012
M=~/nvme2n1p7_home/huggingface/models/gemma4
CUDA_VISIBLE_DEVICES=2 ~/.local/bin/llama-server \
-m $M/gemma-4-26B-A4B-it-Q4_0.gguf --mmproj $M/mmproj-gemma-4-26B-A4B-it-BF16.gguf \
--host 127.0.0.1 --port 8012 -ngl 99 -c 16384 --parallel 4
```
Qwen läuft mit 8 Plätzen zu je 4096 Tokens (zusammen 32768) und belegt rund 22,6 GB; `run` stellt bis zu
8 Anfragen gleichzeitig. Gemma braucht auf GPU 2 rund 17,5 GB. Laufen dort Chatterbox-TTS und der STT-Server, reicht der Speicher
nicht (Gemma bricht beim ersten Bild mit „out of memory“ ab); dann vorher stoppen und danach wieder starten:
```bash
systemctl --user stop chatterbox-tts.service open-notebook-stt.service
systemctl --user start chatterbox-tts.service open-notebook-stt.service # nach der Arbeit
```
pix2text läuft zusätzlich auf der ersten sichtbaren GPU (unter 1 GB). Beenden mit Strg+C in beiden
Terminals.
- **TeX Live** setzt die Formeln für den Bildabgleich (pdflatex, Schriften `lmodern`, `newtx`, `newpx`,
`stix2`, `eulervm`, `txfonts`).
- **Pandoc** prüft, dass jede Formel später als MathML gesetzt werden kann.
- **dvipng** braucht nur `calibrate --synthetic`.
### pix2text (Formel-OCR)
pix2text läuft ohne Server im Programm selbst, auf der ersten sichtbaren Grafikkarte (sonst auf der CPU,
dann ~5× langsamer). Einmalig:
pix2text läuft ohne Server im Programm selbst. Einmalig:
```bash
pip install "pix2text[multilingual]"
@ -64,24 +53,64 @@ pip install --no-deps pix2tex munch # nur für post_p
pip uninstall -y onnxruntime && pip install onnxruntime-gpu # für die GPU
```
Das Modell (~300 MB) wird beim ersten Lauf nach `~/.pix2text` geladen. Ohne Angabe nimmt pix2text die
Grafikkarte mit dem meisten freien Speicher und liest die Formeln gebündelt. Gerät festlegen mit
`FORMULA_P2T_DEVICE=cpu` oder `cuda:N` (`N` zählt innerhalb von `CUDA_VISIBLE_DEVICES`).
`pix2tex` wird ohne Abhängigkeiten installiert, weil es sonst eine alte `timm`-Version erzwingen würde.
Das Modell (~300 MB) wird beim ersten Lauf nach `~/.pix2text` geladen. pix2text nimmt die sichtbare
Grafikkarte mit dem meisten freien Speicher (unter 1 GB) und liest die Formeln gebündelt; ohne
`onnxruntime-gpu` läuft es auf der CPU, dann etwa 5× langsamer.
Gemma (Port 8012) wird nur für Formeln gefragt, bei denen pix2text und Qwen uneinig sind. Läuft Gemma
nicht, bleiben diese Formeln im Review.
### Modelle für die Bildmodelle
### Frühere Regel mit Cloud (optional)
| Modell | Datei | Ort |
|---|---|---|
| Qwen3-VL-30B-A3B-Instruct | `Qwen3-VL-30B-A3B-Instruct-Q4_K_M.gguf` + `mmproj-Qwen3-VL-30B-A3B-Instruct-F16.gguf` | `~/endeavour_home/lm_studio/models/lmstudio-community/Qwen3-VL-30B-A3B-Instruct-GGUF/` |
| Gemma-4-26B-A4B-it | `gemma-4-26B-A4B-it-Q4_0.gguf` + `mmproj-gemma-4-26B-A4B-it-BF16.gguf` (ggml-org, zusammen 15,8 GB) | `~/nvme2n1p7_home/huggingface/models/gemma4/` |
Bis Version 0.2.0 kam die Bestätigung von **GLM-4.6V** über OpenRouter (Qwen = Gemma = GLM). Diese Regel
ist weiter wählbar: im Arbeitsordner in `workspace.json` unter `settings` den Eintrag
`"consensus_rule": "vlm3"` setzen; dann muss `OPENROUTER_API_KEY` gesetzt sein.
Gemma neu laden:
```bash
cd ~/nvme2n1p7_home/huggingface/models/gemma4
hf download ggml-org/gemma-4-26B-A4B-it-GGUF gemma-4-26B-A4B-it-Q4_0.gguf \
mmproj-gemma-4-26B-A4B-it-BF16.gguf --local-dir .
```
Optional, nur für den Strukturvergleich (aus): `pip install sympy lark`.
---
## 2. Ein Buch bearbeiten
## 2. Modellserver starten und beenden
### Schritt 1: Buch wie gewohnt umwandeln
Zwei Bildmodelle laufen als llama.cpp-Server, je eines pro Grafikkarte. In zwei Terminals:
```bash
# Qwen3-VL auf GPU 1, Port 8011: 8 Plätze zu je 4096 Tokens, rund 22,6 GB
M=~/endeavour_home/lm_studio/models/lmstudio-community/Qwen3-VL-30B-A3B-Instruct-GGUF
CUDA_VISIBLE_DEVICES=1 ~/.local/bin/llama-server \
-m $M/Qwen3-VL-30B-A3B-Instruct-Q4_K_M.gguf --mmproj $M/mmproj-Qwen3-VL-30B-A3B-Instruct-F16.gguf \
--host 127.0.0.1 --port 8011 -ngl 99 -c 32768 --parallel 8
# Gemma 4 auf GPU 2, Port 8012: rund 17,5 GB
M=~/nvme2n1p7_home/huggingface/models/gemma4
CUDA_VISIBLE_DEVICES=2 ~/.local/bin/llama-server \
-m $M/gemma-4-26B-A4B-it-Q4_0.gguf --mmproj $M/mmproj-gemma-4-26B-A4B-it-BF16.gguf \
--host 127.0.0.1 --port 8012 -ngl 99 -c 16384 --parallel 4
```
Laufen auf GPU 2 Chatterbox-TTS und der STT-Server, reicht der Speicher für Gemma nicht. Dann vorher
stoppen und nach der Arbeit wieder starten:
```bash
systemctl --user stop chatterbox-tts.service open-notebook-stt.service
systemctl --user start chatterbox-tts.service open-notebook-stt.service # nach der Arbeit
```
Gemma wird nur für Formeln gefragt, bei denen pix2text und Qwen uneinig sind. Läuft Gemma nicht, meldet
`run` das einmal, und diese Formeln bleiben im Review. Beenden: Strg+C in beiden Terminals.
---
## 3. Ein Buch bearbeiten
### Schritt 1: Buch umwandeln
```bash
cd ~/Python_Programs/epub2md
@ -90,7 +119,7 @@ python3 epub2md.py buch.epub output/buch
Bei PDFs mit pdf2md immer `--images` (oder `--epub`) angeben, sonst gibt es keine Formelbilder. Für
Mathebücher **`--formulas`** verwenden: Dann findet pdf2md jede Formel auf der Seite, auch im Fließtext und
in Merkkästen, und schneidet sie als Bild aus (braucht pix2text, siehe oben):
in Merkkästen, und schneidet sie als Bild aus (braucht pix2text):
```bash
cd ~/Python_Programs/pdf2md
@ -106,10 +135,11 @@ cd ~/Python_Programs/formula_img2latex
python3 formula_img2latex.py run ~/Python_Programs/epub2md/output/buch
```
Bei pdf2md zusätzlich die Original-PDF angeben. Dann schneidet das Programm jede Formel scharf aus dem PDF:
Bei pdf2md die Markdown-Datei angeben und die Original-PDF dazu; Box-Bilder werden dann scharf aus dem PDF
neu ausgeschnitten:
```bash
python3 formula_img2latex.py run Output/buch.md --pdf buch.pdf
python3 formula_img2latex.py run ~/Python_Programs/pdf2md/Output/buch/buch.md --pdf buch.pdf
```
Das Programm arbeitet in Paketen zu 50 Formeln und meldet nach jedem Paket den Stand und die Zeiten je
@ -119,7 +149,7 @@ Stufe (die Stufen laufen teils gleichzeitig, daher ist die Summe größer als di
100/940 verarbeitet: automatisch ersetzt 78 (78%), Review 20, bleibt Bild/keine Formel 2 [Paket 35 s; pix2text 3 s, Qwen 30 s, Prüfung 14 s, Gemma 21 s]
```
Am Ende steht eine Übersicht je Status, beim Gehrke-Brückenkurs zum Beispiel:
Am Ende steht eine Übersicht je Status, zum Beispiel:
```text
auto_accepted 942 ← Lesart bestätigt, wird ersetzt
@ -128,15 +158,7 @@ Am Ende steht eine Übersicht je Status, beim Gehrke-Brückenkurs zum Beispiel:
pending 539 ← Abbildungen/Symbole, nicht geprüft
```
Dauer: etwa 0,8 s je Formel, also 35–50 Sekunden je Paket (Corral mit `--formulas`: 940 Formeln in
13 Minuten, das ganze Buch mit 9239 Formeln rund 2 Stunden). Am längsten braucht die Bestätigung durch Qwen.
Der Scan liest beim ersten Mal alle Bilder ein (Corral: 6 Minuten); beim Fortsetzen entfällt er, solange
sich das Markdown nicht geändert hat.
Abbrechen (Strg+C) und erneut starten ist kein Problem: Bereits gelesene Formeln werden nicht noch einmal
gefragt, der Lauf setzt fort. `--chunk 100` ändert die Paketgröße, `--limit 200` verarbeitet nur die ersten
200 Bilder.
Neben dem Buchordner entsteht ein Arbeitsordner `buch.formulas/`. Er enthält alle Ergebnisse. Lösch ihn nicht,
Neben dem Buchordner entsteht ein Arbeitsordner `buch.formulas/` mit allen Ergebnissen. Lösch ihn nicht,
solange du am Buch arbeitest.
### Schritt 3: Zweifelsfälle im Browser entscheiden
@ -152,12 +174,10 @@ Der Browser öffnet `http://127.0.0.1:8765/`. Für jede Formel siehst du:
stimmt etwas nicht (Exponent, Vorzeichen, Klammer).
- **Diff:** schwarz = beide gleich, rot = nur im Original, blau = nur im LaTeX, gelb = deutliche Abweichung.
- **LaTeX:** Text im Eingabefeld (die Lesart von pix2text), darunter die Lesarten der Modelle zum
Anklicken. Oben steht der Grund für das Review, z. B. „Modelle uneinig“ mit den abweichenden Lesarten.
Schaltflächen:
Anklicken. Oben steht der Grund für das Review, z. B. „pix2text und Qwen uneinig“.
| Schaltfläche | Wirkung |
| --- | --- |
|---|---|
| LaTeX prüfen | rendert dein (korrigiertes) LaTeX neu und zeigt Overlay und Messwerte |
| LaTeX übernehmen | die Formel wird beim nächsten `apply` ersetzt |
| Bild behalten | die Formel bleibt ein Bild |
@ -165,7 +185,8 @@ Schaltflächen:
| Zurücksetzen | Entscheidung zurücknehmen |
Jede Entscheidung wird sofort gespeichert. Du kannst jederzeit aufhören (Strg+C im Terminal) und später
weitermachen. `--all` zeigt auch die automatisch angenommenen Formeln.
weitermachen. `--all` zeigt auch die automatisch angenommenen Formeln, `--port` ändert den Port,
`--no-browser` öffnet keinen Browser.
### Schritt 4: Formeln ins Markdown schreiben
@ -176,31 +197,21 @@ python3 formula_img2latex.py apply ~/Python_Programs/epub2md/output/buch
Aus `![…](../media/images/0042-…jpg)` wird `$L = \{-\sqrt{2};\sqrt{2}\}$` im Text bzw. `$$…$$` als eigener
Absatz. Die Bilddateien bleiben liegen. Hat die Formelerkennung bei einer Formel im Text ein Satzzeichen
mit ausgeschnitten, steht es danach als normaler Text hinter der Formel (`$a^{x}$,`).
mit ausgeschnitten, steht es danach als normaler Text hinter der Formel (`$a^{x}$,`). In deutschen
Büchern werden Dezimalzahlen zu `3{,}6`; eine Gleichungsnummer steht als Text hinter der Formel.
Bei pdf2md-Büchern baut `pdf2md.py --from-md` das EPUB neu, mit allen Formeln als MathML:
```bash
cd ~/Python_Programs/pdf2md && python3 pdf2md.py --from-md Output/buch/buch.md
```
**Danach die Gesamtdatei neu bauen**, sie enthält sonst noch die Bilder:
**Danach das Buch neu bauen:**
```bash
# epub2md: Gesamtdatei neu, dann z. B. EPUB mit MathML
cd ~/Python_Programs/epub2md
python3 epub2md_merge.py output/buch --force
```
Für EPUB und PDF mit Formeln gelten die Pandoc-Befehle aus der epub2md-Anleitung mit `--mathml`, z. B.:
```bash
cd ~/Python_Programs/epub2md/output/buch
P=~/Python_Programs/epub2md/pandoc
cd output/buch && P=~/Python_Programs/epub2md/pandoc
pandoc buch.md --toc --mathml --lua-filter $P/callouts.lua -o buch.epub
```
In deutschen Büchern schreibt `apply` Dezimalzahlen als `3{,}6`, sonst setzt LaTeX „3, 6“. Eine
Gleichungsnummer steht als Text hinter der Formel, wie bei epub2md.
# pdf2md: EPUB mit allen Formeln als MathML
cd ~/Python_Programs/pdf2md && python3 pdf2md.py --from-md Output/buch/buch.md
```
### Rückgängig machen
@ -208,64 +219,173 @@ Gleichungsnummer steht als Text hinter der Formel, wie bei epub2md.
python3 formula_img2latex.py revert ~/Python_Programs/epub2md/output/buch
```
Das stellt die Kapiteldateien wie vor dem ersten `apply` wieder her. Hast du eine Datei seitdem von Hand
geändert, wird sie übersprungen. `--force` setzt sie trotzdem zurück, dann ist die Handänderung weg.
Das stellt die Markdown-Dateien wie vor dem ersten `apply` wieder her. Hast du eine Datei seitdem von Hand
geändert, wird sie übersprungen; `--force` setzt sie trotzdem zurück, dann ist die Handänderung weg.
---
## 3. Einzelne Schritte statt `run`
## 4. Einzelne Schritte und Optionen
```bash
python3 formula_img2latex.py scan AUSGABE # Bilder finden
python3 formula_img2latex.py recognize AUSGABE --engine p2t # pix2text (zuerst)
python3 formula_img2latex.py recognize AUSGABE --engine local # Qwen (bestätigt)
python3 formula_img2latex.py scan AUSGABE # Bilder finden (immer neu)
python3 formula_img2latex.py recognize AUSGABE --engine p2t # pix2text
python3 formula_img2latex.py recognize AUSGABE --engine local # Qwen
python3 formula_img2latex.py verify AUSGABE # prüfen und entscheiden
python3 formula_img2latex.py recognize AUSGABE --engine local2 --only-unresolved # Gemma (Stichwahl)
python3 formula_img2latex.py recognize AUSGABE --engine local2 --only-unresolved # Gemma
python3 formula_img2latex.py verify AUSGABE
python3 formula_img2latex.py status AUSGABE
python3 formula_img2latex.py status AUSGABE # Zählung je Status (--json)
```
Nützliche Optionen:
| Option | Befehle | Wirkung |
|---|---|---|
| `--limit 50` | run, recognize | nur die ersten 50 Bilder (zum Ausprobieren) |
| `--chunk 100` | run | Paketgröße (Standard 50) |
| `--pdf BUCH.pdf` | run, scan | pdf2md: Original-PDF für scharfe Neuausschnitte |
| `--preset cm` | run, verify | Schrift für den Bildabgleich fest vorgeben (`cm`, `times`, `txfonts`, `palatino`, `stix`, `euler`); Standard: automatisch je Buch |
| `--include-figures` | run, recognize, verify | auch Bilder versuchen, die wie Abbildungen aussehen |
| `--model NAME`, `--url URL` | run, recognize | anderes Modell bzw. anderer Server |
| `--workers N` | run, recognize, verify | parallele Anfragen bzw. Prüfprozesse |
| `--redo` | verify | bereits geprüfte Formeln neu prüfen |
| `--workspace ORDNER` | alle außer calibrate | Arbeitsordner an anderem Ort |
| `--quiet`, `-q` | alle | nur Fehler ausgeben |
| Option | Wirkung |
| --- | --- |
| `--limit 50` | nur die ersten 50 Bilder (zum Ausprobieren) |
| `--preset cm` | Schrift fest vorgeben (`cm`, `times`, `txfonts`, `palatino`, `stix`, `euler`); Standard: automatisch |
| `--include-figures` | auch Bilder versuchen, die wie Abbildungen aussehen |
| `--model NAME` | anderes Modell (lokal: Servername, Cloud: OpenRouter-ID) |
| `--workspace ORDNER` | Arbeitsordner an anderem Ort |
| `--redo` (verify) | bereits geprüfte Formeln neu prüfen, z. B. nach einem Programm-Update |
Alle Optionen: `python3 formula_img2latex.py BEFEHL --help`.
---
## 4. Häufige Fragen
## 5. Konfiguration
### Umgebungsvariablen
| Variable | Standard | Wirkung |
|---|---|---|
| `FORMULA_LOCAL_URL`, `FORMULA_LOCAL_MODEL` | `http://127.0.0.1:8011/v1`, Modell des Servers | Qwen-Server |
| `FORMULA_LOCAL2_URL`, `FORMULA_LOCAL2_MODEL` | `http://127.0.0.1:8012/v1`, Modell des Servers | Gemma-Server |
| `FORMULA_LOCAL_API_KEY`, `FORMULA_LOCAL2_API_KEY` | – | falls der Server einen Schlüssel verlangt |
| `FORMULA_P2T_DEVICE` | `auto` | pix2text: `auto`, `cpu` oder `cuda:N` (`N` zählt innerhalb von `CUDA_VISIBLE_DEVICES`) |
| `FORMULA_P2T_MODEL` | `mfr-1.5` | pix2text-Modell |
| `FORMULA_CLOUD_URL`, `FORMULA_CLOUD_MODEL` | `https://openrouter.ai/api/v1`, `z-ai/glm-4.6v` | nur Regel `vlm3` |
| `OPENROUTER_API_KEY` | – | nur Regel `vlm3` |
| `FORMULA_LOCAL_THINKING`, `FORMULA_LOCAL2_THINKING`, `FORMULA_CLOUD_THINKING` | `0` | `1` schaltet das „Nachdenken“ der Modelle wieder ein |
| `FORMULA_STRUCTURAL` | `0` | `1` schaltet den Strukturvergleich mit sympy ein |
### Einstellungen je Buch (`workspace.json`)
Im Arbeitsordner unter `settings`, zum Beispiel:
```json
{
"settings": {
"consensus_rule": "p2t",
"consensus_thresholds": {"veto_ncc": 0.65, "bold_relevant": 0}
}
}
```
| Schlüssel | Werte | Wirkung |
|---|---|---|
| `consensus_rule` | `p2t` (Standard), `vlm3` | Regel `vlm3`: Qwen = Gemma, bestätigt von GLM-4.6V über OpenRouter (bis 0.2.0) |
| `decide` | `consensus` (Standard), `pixel` | `pixel`: alter Pixel-Entscheider (bis 0.1.0) |
| `consensus_thresholds` | siehe [FUNKTIONSWEISE.md, Schwellen](FUNKTIONSWEISE.md#schwellen-settingsconsensus_thresholds-in-workspacejson) | Veto-Schwelle, Fettdruck, Schiedsrichter, PDF-Textebene … |
| `preset` | wird beim ersten Prüfen gesetzt | Schrift für den Bildabgleich |
Nach einer Änderung `verify` aufrufen; die Modelle werden nicht neu gefragt.
---
## 6. Betrieb
**Dauer:** etwa 0,8 s je Formel, also 35–50 Sekunden je Paket (Corral mit `--formulas`: 940 Formeln in
13 Minuten, das ganze Buch mit 9239 Formeln rund 2 Stunden). Am längsten braucht die Bestätigung durch
Qwen. Der Scan liest beim ersten Mal alle Bilder ein (Corral: 6 Minuten); beim Fortsetzen entfällt er,
solange sich das Markdown nicht geändert hat.
**Abbrechen und fortsetzen:** Strg+C und erneut `run` ist kein Problem. Bereits gelesene Formeln werden
nicht noch einmal gefragt; fehlgeschlagene Anfragen werden wiederholt.
**Neu umgewandeltes Buch:** Nach `epub2md --force` erst `scan`, dann `apply`. Die Entscheidungen hängen am
Bildinhalt und bleiben erhalten.
**Arbeitsordner:** `<ausgabe>.formulas/` neben der Ausgabe. Er enthält die Datensätze, Ausschnitte,
Vergleichsbilder und die Sicherung von `apply`. Aufbau: [FUNKTIONSWEISE.md, Arbeitsordner](FUNKTIONSWEISE.md#3-arbeitsordner-und-datensätze).
**Datenschutz:** Mit der Standardregel verlässt nichts den Rechner. Die Regel `vlm3` schickt Formelbilder,
bei denen Qwen und Gemma einig sind, samt eines kurzen Textausschnitts um die Formel (höchstens 240
Zeichen) an OpenRouter; beim Gehrke-Brückenkurs waren das 967 Anfragen für zusammen rund 0,15–0,20 $.
---
## 7. Werkzeuge zum Messen und Einstellen
Die Werkzeuge schreiben nach `vergleich/` bzw. in den Arbeitsordner; beides wird nicht ins Git übernommen
(Buchausschnitte).
| Werkzeug | Zweck |
|---|---|
| `tools/vergleich.py AUSGABE` | feste Stichprobe von 50 Formeln eines Buchs, verarbeitet wie `run`; Seite `vergleich/<name>/index.html` mit Original, gerenderten Lesarten, Entscheidung und Bildabgleich |
| `tools/buch_bericht.py AUSGABE` | Ergebnisseite eines ganzen Buchs nach `run`, filterbar; liegt in `<arbeitsordner>/bericht/index.html` |
| `tools/regeltest.py EPUB [--n 50] [--reuse …] [--refresh local,local2]` | Standardregel an Formeln mit bekanntem TeX, wie `run`; Seite mit wahrer Formel, allen Lesarten und Urteil |
| `tools/zeittest.py EPUB` | Zeit und Trefferquote je Erkenner, seriell an 15 Formeln mit bekanntem TeX |
| `tools/vergleich_bericht.py --regeltest VORHER:NACHHER` | Vorher/Nachher-Seite zweier Läufe auf denselben Lesarten |
| `tools/konsens_bericht.py BERICHT.json EPUB` | Übersichtsseite zu `calibrate --consensus --report` |
| `formula_img2latex.py calibrate --consensus --engines local,local2,cloud,p2t --report X.json EPUB` | alle Regeln an Formeln mit bekanntem TeX messen; `--reuse X.json` rechnet ohne Modellabfragen neu, `--synthetic` erzeugt zusätzlich Gehrke-ähnliche Nachbildungen |
Formeln mit bekanntem TeX liefern Springer-EPUBs, die das TeX jeder Formel im alt-Text tragen (z. B.
Göllmann).
---
## 8. Fehlersuche
| Meldung oder Beobachtung | Ursache | Abhilfe |
|---|---|---|
| `Lokaler Modellserver unter http://127.0.0.1:8011/v1 nicht erreichbar` | Qwen-Server läuft nicht | Server starten ([2](#2-modellserver-starten-und-beenden)) |
| `Stichwahl (Gemma) nicht möglich, strittige Formeln bleiben im Review` | Gemma-Server läuft nicht | Gemma starten und `run` erneut aufrufen |
| Gemma bricht beim ersten Bild mit „out of memory“ ab | GPU 2 ist belegt | Chatterbox-TTS und STT stoppen ([2](#2-modellserver-starten-und-beenden)) |
| `pix2text ist nicht installiert` / `pix2tex ist nicht installiert` | Pakete fehlen | Befehle aus [1](#pix2text-formel-ocr) ausführen |
| `onnxruntime ohne CUDA – pip install onnxruntime-gpu` | nur die CPU-Version von onnxruntime | `pip uninstall -y onnxruntime && pip install onnxruntime-gpu` |
| pix2text sehr langsam | läuft auf der CPU | `onnxruntime-gpu` installieren oder `FORMULA_P2T_DEVICE=cuda:0` |
| `pdflatex nicht gefunden` / `pandoc nicht gefunden` | TeX Live bzw. Pandoc fehlen | `sudo apt install texlive-latex-extra pandoc` |
| `Kein Arbeitsordner gefunden … (zuerst 'scan' ausführen)` | `verify`, `apply` usw. vor dem ersten `scan`/`run` | `scan` oder `run` ausführen |
| `… ist weder eine epub2md-Ausgabe … noch eine pdf2md-Ausgabe` | falscher Pfad | epub2md-Ordner bzw. pdf2md-`.md`-Datei angeben |
| `Arbeitsordner existiert und ist kein formula_img2latex-Ordner` | Zielordner von `--workspace` belegt | anderen Ordner wählen |
| `Bildverweis nicht mehr gefunden: datei.md:123` | Markdown wurde neu erzeugt oder von Hand geändert | `scan` ausführen |
| `revert` überspringt Dateien | seit `apply` von Hand geändert | prüfen, dann `revert --force` |
| `OPENROUTER_API_KEY ist nicht gesetzt` | Regel `vlm3` oder `--engine cloud` ohne Schlüssel | Schlüssel setzen oder Regel `p2t` verwenden |
| Formel bleibt in EPUB/PDF als TeX-Text stehen | Pandoc konnte sie nicht umsetzen (sollte die Pandoc-Prüfung verhindern) | Formel im Review korrigieren, Fall melden |
**Rückgabecodes:**
| Code | Bedeutung |
|---|---|
| 0 | Erfolg |
| 1 | Bedienfehler: Parameter, Eingabe oder Arbeitsordner nicht lesbar oder belegt |
| 2 | Eingabe ist keine epub2md- oder pdf2md-Ausgabe |
| 3 | Externes Werkzeug fehlt oder versagt: pdflatex, pandoc, Modellserver, pix2text |
| 4 | Unerwarteter interner Fehler (Traceback wird ausgegeben) |
---
## 9. Häufige Fragen
**Das Buch wurde mit epub2md `--force` neu umgewandelt. Ist meine Arbeit weg?**
Nein. Die Entscheidungen hängen am Inhalt der Bilder, nicht an Dateinamen. `scan`, dann `apply`.
**Warum landet ein Teil im Review?**
Ersetzt wird nur, was pix2text liest und Qwen (oder bei Uneinigkeit Gemma) bestätigt. Sind Qwen und Gemma
gemeinsam anderer Meinung als pix2text, entscheidest du. Häufige Gründe für Review: ein Modell liest ein
Zeichen anders (oft pix2text: `\nu` statt fettem v, Wörter wie „fir“ statt „für“), sehr kleine oder
unscharfe Bilder, mehrzeilige Rechnungen mit anderen Zeilenumbrüchen oder `\underbrace`. Verschiedene
Rahmen derselben Zeilen (`aligned`, `cases`, `&`) gelten seit 0.4.0 als gleich. Im Review stehen die Lesarten
gemeinsam anderer Meinung als pix2text, entscheidest du. Häufige Gründe: ein Modell liest ein Zeichen anders
(oft pix2text: `\nu` statt fettem v, Wörter wie „fir“ statt „für“), sehr kleine oder unscharfe Bilder,
mehrzeilige Rechnungen mit anderen Zeilenumbrüchen oder `\underbrace`. Im Review stehen die Lesarten
nebeneinander; oft genügt ein Blick und „LaTeX übernehmen“.
**Wie sicher ist „automatisch ersetzt“?**
Gemessen an 300 Formeln mit bekannter Lösung: keine einzige falsche Formel wurde angenommen (Fehlerquote
sehr wahrscheinlich unter 2 %). Am Gehrke-Brückenkurs wurden 62 automatisch ersetzte Formeln von Hand
geprüft, alle korrekt. Fettdruck wird bewusst nicht geprüft: Ein fetter Vektor **v** kann als normales v
übernommen werden.
An einigen hundert Formeln mit bekannter Lösung wurde keine falsche Formel angenommen (Fehlerquote sehr
wahrscheinlich unter 2 %). Fettdruck wird bewusst nicht geprüft: Ein fetter Vektor **v** kann als normales
v übernommen werden. Messungen: [FUNKTIONSWEISE.md](FUNKTIONSWEISE.md#12-messungen).
**Ich will erst ein paar Formeln ansehen, bevor das ganze Buch läuft.**
`python3 tools/vergleich.py AUSGABE` verarbeitet eine feste Stichprobe von 50 Formeln genau wie `run`
und schreibt `vergleich/<name>/index.html`: Originalbild, die gerenderten Lesarten aller Modelle, die
Entscheidung mit Grund und den Bildabgleich. Der Ordner `vergleich/` wird nicht ins Git übernommen.
`python3 tools/vergleich.py AUSGABE` verarbeitet eine feste Stichprobe von 50 Formeln genau wie `run` und
schreibt `vergleich/<name>/index.html`.
**Geht die blaue Hinterlegung mancher Formeln verloren?**
Ja. Das Bild hatte die Farbe eingebaut, LaTeX-Formeln haben keinen Hintergrund.
**Werden Buchinhalte ins Internet geschickt?**
Nein, mit der Standardregel bleibt alles lokal. Nur die frühere Regel `vlm3` schickt die Formelbilder, bei
denen Qwen und Gemma einig sind, samt eines kurzen Textausschnitts um die Formel (höchstens 240 Zeichen)
an OpenRouter/GLM.

View file

@ -1,136 +1,151 @@
# Changelog
# Änderungsprotokoll
## Unveröffentlicht
Alle nennenswerten Änderungen an formula_img2latex. Format nach
[Keep a Changelog](https://keepachangelog.com/de/1.1.0/), Versionen nach
[Semantic Versioning](https://semver.org/lang/de/) (vor 1.0: Nebenversion = möglicher Bruch).
Messergebnisse im Einzelnen: [FUNKTIONSWEISE.md, Messungen](FUNKTIONSWEISE.md#12-messungen).
## [Unreleased]
Alle Formeln eines PDF-Buchs, auch im Fließtext, und etwa dreimal schneller.
- **pdf2md --formulas** (Nachbarprojekt): Die Formelerkennung von pix2text (MFD) findet jede Formel auf der
Seite, eingebettet oder abgesetzt, auch in Merkkästen. Sie wird zeichengenau aus dem Text geschnitten
(300 dpi) und mit Seite, Rechteck und PDF-Text in `formulas-*.json` vermerkt. "Formel"-Boxen mit Text
werden zu Text, solche mit Kurven zu Grafiken. formula_img2latex liest die Liste
(`sources/pdf2md.listed`). Corral, 10 % der 9239 Formeln: **79 % automatisch** (im Text 83 %).
- **Eingebettete Formeln:** Ein Satzzeichen am Ende wird vor dem Vergleich abgetrennt und als Text hinter
die Formel gesetzt (`final_suffix`).
- **Normalform:** `\underset{…}{\lim}` = `\lim_{…}`, Gleichungsnummer am Ende = `\tag`, `\textbf` als
Fettdruck, `\cfrac` = `\frac` (pix2text schreibt `\dfrac`), wörtliches `\n` aus Modellantworten entfällt.
- **Tempo** (Pilot 869 Formeln: 70 → ~24 min):
- Markdown-Verweise und Ausschnitte nur einmal lesen (`crops/`);
- pix2text gebündelt nach Pixelbudget, auf der freiesten GPU, einmal geladen;
- Lesen und Prüfen überlappen, die Prüfprozesse starten per `forkserver`;
- Qwen mit 8 parallelen Anfragen (`llama-server --parallel 8 -c 32768`);
- pdflatex beim Entscheiden nur, wenn gleiche Bilder möglich sind;
- kein neuer Scan beim Fortsetzen;
- je Paket nur dessen Datensätze laden;
- Zeiten je Stufe in der Statuszeile.
- **Bildprüfung:** Originale höchstens 1600 px breit, über 2,5 MP nicht, Lesarten mit mehr als 2,5-fach
falschen Proportionen sofort abgelehnt. Vorher hielten einzelne Formeln den Lauf über 10 Minuten auf.
- **Gemessen und verworfen:**
- PDF-Textebene als Bestätigung: 12 von 319 widersprachen Qwen, fehlende Punkte und Indizes; Schalter
`pdftext` bleibt aus.
- pix2texts eigene Sicherheit: Fehler auch bei 1,0.
- Qwen3-VL-8B: nicht schneller als das MoE-30B-Modell (3B aktiv), etwas schlechter.
- **Offen:** Formeln in Tabellen.
### Neu
- **Formeln im Fließtext** über `pdf2md --formulas` (Nachbarprojekt): Die Formelerkennung von pix2text (MFD)
findet jede Formel auf der Seite, auch in Merkkästen; pdf2md schneidet sie zeichengenau aus (300 dpi) und
vermerkt Seite, Rechteck und PDF-Text in `formulas-*.json`. formula_img2latex liest die Liste
(`sources/pdf2md.listed`). Corral, Stichprobe 10 % von 9239 Formeln: 79 % automatisch (im Text 83 %).
- Ein Satzzeichen am Ende einer Formel im Text wird vor dem Vergleich abgetrennt und als Text hinter die
Formel gesetzt (`final_suffix`).
- Normalform: `\underset{…}{\lim}` = `\lim_{…}`, Gleichungsnummer am Ende = `\tag`, `\textbf` als Fettdruck,
`\cfrac` = `\frac`, wörtliches `\n` aus Modellantworten entfällt.
- Schalter `pdftext` (aus): Die PDF-Textebene einer flachen Formel darf pix2text bestätigen.
- `LICENSE` (MIT) und `FUNKTIONSWEISE.md` (Aufbau, Datenfluss, Designentscheidungen, Grenzen).
## 0.4.0 – 2026-10-09
### Geändert
- **Tempo** (Pilot 869 Formeln: 70 → ~24 Minuten): Markdown-Verweise und Ausschnitte nur einmal lesen
(`crops/`); pix2text gebündelt nach Pixelbudget auf der freiesten GPU, einmal geladen; Lesen und Prüfen
überlappen, die Prüfprozesse starten per `forkserver`; Qwen mit 8 parallelen Anfragen; pdflatex beim
Entscheiden nur, wenn gleiche Bilder möglich sind; kein neuer Scan beim Fortsetzen; je Paket nur dessen
Datensätze laden; Zeiten je Stufe in der Statuszeile.
- Bildprüfung: Originale höchstens 1600 px breit, über 2,5 MP nicht verglichen, Lesarten mit mehr als
2,5-fach falschen Proportionen sofort abgelehnt (einzelne Formeln hielten den Lauf über 10 Minuten auf).
- Dokumentation nach dem Projektstandard gegliedert: README knapp mit Abschnitt „Documentation“,
Bedienungsanleitung mit Konfiguration, Betrieb und Fehlersuche, Hintergrund und Messungen in
`FUNKTIONSWEISE.md`.
- Hilfetexte der Kommandozeile beschreiben die Regel `p2t` statt des Pixelvergleichs; Cloud-Standard
`z-ai/glm-4.6v` statt `google/gemini-2.5-flash`.
### Behoben
- `requirements.txt` enthält `requests` (von den Modell-Engines benötigt).
Gemessen und verworfen: PDF-Textebene als Bestätigung (12 von 319 widersprachen Qwen), pix2texts eigene
Sicherheit (Fehler auch bei 1,0), Qwen3-VL-8B (nicht schneller, etwas schlechter).
### Beim Update zu tun
- `pip install -r requirements.txt`.
- Qwen-Server mit `--parallel 8 -c 32768` neu starten (BEDIENUNGSANLEITUNG, Abschnitt 2).
- Für Formeln im Fließtext: aktuelles pdf2md verwenden und das Buch mit `--formulas` neu umwandeln.
- Bestehende Arbeitsordner bleiben gültig; Lesarten werden nicht neu gefragt.
## [0.4.0] – 2026-10-09
Bessere Normalform: Lesarten, die sich nur in der Schreibweise unterscheiden, gelten als gleich.
Göllmann (400 Formeln mit bekannter Lösung, gleiche Lesarten): **+17 automatisch, weiterhin 0 falsch**.
Göllmann (400 Formeln mit bekannter Lösung, gleiche Lesarten): +17 automatisch, weiterhin 0 falsch.
- **Mehrzeilige Formeln:** gleich bei gleichen Zeilen mit gleichen Zeichen, egal in welchem Rahmen
### Neu
- **Mehrzeilige Formeln** sind gleich bei gleichen Zeilen mit gleichen Zeichen, egal in welchem Rahmen
(`aligned`/`gathered`/`split` mit `&` oder nur `\\`; `cases` = `\left\{array\right.` =
`\left\{aligned\right.`); eine leere letzte Zeile zählt nicht. In Matrizen trennt `&` weiter Spalten.
- **Argumente nach Argumentzahl:** Eine Gruppe ist nur Argument, wenn der Befehl noch eines erwartet
(`t^{2}\,{\sqrt{2}}` = `t^2\sqrt{2}`, `\Delta{x_{n}}` = `\Delta x_n`).
- **Weitere Gleichsetzungen:** `\cdot\cdot\cdot` und `...` = `\dots`, `\root3\of{x}` = `\sqrt[3]{x}`,
- Weitere Gleichsetzungen: `\cdot\cdot\cdot` und `...` = `\dots`, `\root3\of{x}` = `\sqrt[3]{x}`,
`\left.`/`\right.` entfallen.
- **Messung genauer:** Der Wahrheitsvergleich nutzt dieselbe Normalform; richtige mehrzeilige Lesarten
zählten bisher als falsch (pix2text allein richtig: Band 2 jetzt 45/50 und 47/50).
- **Ergebnis:** Band 1 89 % / 88 % automatisch (bisher 86 % / 85 %), Band 2 44 und 45 von 50 (bisher 39
und 43), 0 falsch.
- **Strukturvergleich mit sympy** (`structure.py`, neu, optional, aus): gleicher unausgewerteter
Ausdrucksbaum, nichts umgerechnet; Relationen nie an sympy, Zeichen durch Platzhalter geschützt.
Brachte vor der neuen Normalform +1 Formel, danach nichts – `structural` = 1 bzw.
`FORMULA_STRUCTURAL=1` schaltet ihn ein (`pip install sympy lark`).
- **Bild-Schiedsrichter** (`verify.image_arbiter`, gemessen, aus): bei „Qwen = Gemma gegen pix2text“
entscheidet der Bildbeleg. +11 Formeln ab Schwelle 8, aber der gemeinsame Fehler `\mathcal{E}_r`
bekam Beleg 7,2 – zu knapp. Einstellungen `arbiter`, `arbiter_margin`.
- **Prompt-Stilvorgabe** getestet und verworfen (Band 2: 44/44 statt 44/45; die Modelle schrieben nach
`\\` ein wörtliches `\n`). Prompt-Version bleibt 2, gespeicherte Lesarten bleiben gültig.
- **Werkzeuge:** `tools/vergleich_bericht.py` (Vorher/Nachher-Seite zweier Läufe), `regeltest.py
--refresh` (Engines trotz `--reuse` neu fragen) und `--arbiter`; `calibrate` misst den
Schiedsrichter (Werte je Fall, Schwellen) und listet nur strukturell gefundene Übereinstimmungen;
`tools/buch_bericht.py` (Ergebnisseite eines ganzen Buchs: jede Formel mit Original, gesetztem
LaTeX, Status, Begründung und allen Lesarten, filterbar).
- **Erstes ganzes PDF-Buch** (Corral, *Elementary Calculus*, 346 Seiten, pdf2md): 1150 Ausschnitte,
**429 automatisch ersetzt (37 %)**, 633 Review, 61 keine Formel. Gefundene und behobene Probleme:
- pix2text schreibt hier Brüche als `\cfrac`: in der Normalform = `\frac`, beim Bereinigen zu
`\dfrac` (Pandoc kennt `\cfrac` nicht und ließ die Formel als TeX-Text stehen).
- Qwen und Gemma schreiben Zeilenumbrüche als wörtliches `\n` (`\begin{aligned}\n…`): wird zu
Leerraum (`tex.fix_literal_newlines`), echte Befehle wie `\nu`, `\neq` bleiben.
- 600-dpi-Ausschnitte bis 3800 × 2700 Pixel: Qwen lehnte sie ab („400 Bad Request“, Kontext voll),
eine ganzseitige Abbildung (33 MP) brachte den Bildvergleich zum Absturz. Jetzt: Bilder für die
Modelle höchstens 1600 Pixel lang, Bildvergleich nur bis 4 MP (größte übernommene Formel: 1,7 MP).
- Fehlgeschlagene Modellanfragen werden beim nächsten Lauf wiederholt (zählten als „schon gefragt“).
- Göllmann danach unverändert: 0 falsch.
- Grenze: pdf2md schneidet in diesem Buch auch Merkkästen mit Text, Tabellen und ganze
Herleitungsblöcke als „Formel“ aus; sie bilden den Großteil des Reviews.
- **Strukturvergleich mit sympy** (`structure.py`, optional, aus): gleicher unausgewerteter
Ausdrucksbaum. Einschalten mit `structural` = 1 bzw. `FORMULA_STRUCTURAL=1`.
- **Bild-Schiedsrichter** (`verify.image_arbiter`, aus): bei „Qwen = Gemma gegen pix2text“ entscheidet
der Bildbeleg. Einstellungen `arbiter`, `arbiter_margin`.
- Werkzeuge: `tools/vergleich_bericht.py` (Vorher/Nachher-Seite), `tools/buch_bericht.py`
(Ergebnisseite eines ganzen Buchs), `regeltest.py --refresh` und `--arbiter`.
## 0.3.0 – 2026-10-09
### Geändert
- Eine Klammergruppe ist nur Argument, wenn der Befehl noch eines erwartet (`t^{2}\,{\sqrt{2}}` =
`t^2\sqrt{2}`, `\Delta{x_{n}}` = `\Delta x_n`).
- Der Wahrheitsvergleich der Messung nutzt dieselbe Normalform (richtige mehrzeilige Lesarten zählten
bisher als falsch).
- Bilder für die Modelle höchstens 1600 Pixel lang.
Neue Standardregel `p2t`: **pix2text liest zuerst, Qwen bestätigt, Gemma nur bei Uneinigkeit**; GLM (Cloud)
wird nicht mehr benutzt.
### Behoben
- pix2texts `\cfrac` wird beim Bereinigen zu `\dfrac` (Pandoc kennt `\cfrac` nicht und ließ die Formel als
TeX-Text stehen).
- Wörtliches `\n` in Antworten von Qwen und Gemma wird zu Leerraum (`tex.fix_literal_newlines`).
- Sehr große Ausschnitte: Qwen lehnte sie ab („400 Bad Request“), eine ganzseitige Abbildung brachte den
Bildvergleich zum Absturz.
- Fehlgeschlagene Modellanfragen werden beim nächsten Lauf wiederholt (zählten als „schon gefragt“).
Gemessen und verworfen: Strukturvergleich (nach der neuen Normalform kein Gewinn), Bild-Schiedsrichter
(der gemeinsame Fehler `\mathcal{E}_r` bekam Beleg 7,2), Prompt-Stilvorgabe (keine Verbesserung).
Erstes ganzes PDF-Buch: Corral, *Elementary Calculus*, 1150 Ausschnitte, 429 automatisch (37 %).
### Beim Update zu tun
- Nichts Pflichtiges. Prompt-Version bleibt 2, gespeicherte Lesarten bleiben gültig; `run` rechnet die
Entscheidungen mit der neuen Normalform neu.
- Optional für den Strukturvergleich: `pip install sympy lark`.
## [0.3.0] – 2026-10-09
Neue Standardregel `p2t`: pix2text liest zuerst, Qwen bestätigt, Gemma nur bei Uneinigkeit; GLM (Cloud) wird
nicht mehr benutzt. Göllmann Band 1: 86 % / 85 % automatisch, 0 falsch (bisher 78 % / 70 %).
### Neu
- **pix2text** (Formel-OCR MFR 1.5, ONNX) als Engine `p2t`, im Prozess, auf der GPU mit `onnxruntime-gpu`
(`FORMULA_P2T_DEVICE`: `auto`, `cpu`, `cuda:N`). Allein am häufigsten richtig (Göllmann Band 1: 90 % /
85 %) und am schnellsten (0,1 s/Formel auf der GPU, Qwen 0,7 s, Gemma 1,8 s).
- **Regel:** übernommen wird immer pix2texts Lesart (trifft den Fettdruck am besten), wenn Qwen sie
bestätigt oder – nur bei Uneinigkeit gefragt – Gemma. Qwen und Gemma gemeinsam gegen pix2text gehen
an einen Menschen (so kamen an Band 1 die gemeinsamen Fehler der beiden Bildmodelle durch). Der
Schlusspunkt muss bestätigt sein. Bild-Veto und Pandoc wie bisher, geprüft auf pix2texts Lesart.
Die alte Regel bleibt als `"consensus_rule": "vlm3"` in `workspace.json` wählbar.
- **pix2text-Ausgabe bereinigt:** Leerzeichen mit `post_process` aus pix2tex (installiert mit
`--no-deps`), Fettschreibweisen `\mathrm{\boldmath~ x ~}` / `\mathrm{\bf~ r o t}` →
`\boldsymbol{x}` / `\mathbf{rot}`. An 400 Lesarten geprüft: gleiches gesetztes Bild.
- **Normalform:** `\operatorname{l i m}` = `\lim`, `\ddot{u}` in Text = ü, `^{\prime}` = `'`,
abschließendes `\\` vor `\end`, `\begin{array}[t]`, `\mathop{…}`.
- **calibrate:** beliebig viele Engines (`--engines`), Regel-Tabelle, `--reuse` rechnet aus einem
früheren Bericht ohne Modellabfragen; wahre Formeln aus dem alt-Text werden bereinigt
(einzeiliges `aligned`, `\mathop{}\!\mathrm{d}`, upgreek).
- **Werkzeuge:** `tools/zeittest.py` (Zeit und Trefferquote je Tool), `tools/regeltest.py` (Standardregel
an Formeln mit bekanntem TeX, Seite zum Mitbeurteilen; `--reuse`, `--exclude`).
- **Messung:** Göllmann Band 1 (2 × 150) 86 % / 85 % automatisch, 0 falsch (bisher 78 % / 70 %);
Band 2, zwei frische Stichproben zu 50: 78 % und 86 % automatisch, 0 falsch.
(`FORMULA_P2T_DEVICE`: `auto`, `cpu`, `cuda:N`).
- **Regel `p2t`:** übernommen wird pix2texts Lesart, wenn Qwen sie bestätigt oder – nur bei Uneinigkeit
gefragt – Gemma. Qwen und Gemma gemeinsam gegen pix2text gehen an einen Menschen.
- `calibrate`: beliebig viele Engines (`--engines`), Regel-Tabelle, `--reuse` ohne Modellabfragen.
- Werkzeuge: `tools/zeittest.py`, `tools/regeltest.py`.
## 0.2.0 – 2026-10-07
### Geändert
- pix2text-Ausgabe bereinigt: Leerzeichen mit `post_process` aus pix2tex, Fettschreibweisen
`\mathrm{\boldmath~ x ~}` → `\boldsymbol{x}`. An 400 Lesarten geprüft: gleiches gesetztes Bild.
- Normalform: `\operatorname{l i m}` = `\lim`, `\ddot{u}` in Text = ü, `^{\prime}` = `'`, abschließendes
`\\` vor `\end`, `\begin{array}[t]`, `\mathop{…}`.
Entscheider ist jetzt der **Konsens dreier Bildmodelle** statt eines Pixelvergleichs.
### Beim Update zu tun
- pix2text installieren: `pip install "pix2text[multilingual]"`, `pip install --no-deps pix2tex munch`,
für die GPU `pip uninstall -y onnxruntime && pip install onnxruntime-gpu`.
- Bestehende Arbeitsordner: `run` erneut aufrufen; pix2text liest nach, Qwen-Lesarten bleiben gültig.
- Wer die Regel mit GLM behalten will: `"consensus_rule": "vlm3"` in `workspace.json`.
- **Annahmeregel:** jedes Zeichen richtig und an der richtigen Stelle; Abstände, Längen, Schriftgröße,
kursiv/aufrecht und Fettdruck egal.
- **Konsens:** Qwen3-VL-30B-A3B (lokal) = Gemma-4-26B-A4B (lokal) = GLM-4.6V (OpenRouter, andere
Modellfamilie); Pandoc muss die Formel umsetzen; grobes Bild-Veto (elastischer Vergleich,
Ähnlichkeit ≥ 0,65). Bei 2:1 entscheidet ein Mensch.
- **`consensus.py`:** Normalform auf Baumbasis (Abstände, `\left`/`\right`, freistehende Klammern,
`\vec`/`\overrightarrow`, `\frac`/`\dfrac`, `\iff`/`\Leftrightarrow`, Matrix-Umgebungen, TeX-Umlaute,
`\atop`/`\substack`, kursiv/aufrecht, fett/normal) und Render-Gleichheit als Rückfallebene.
- **Engines:** zweite lokale Engine `local2`; Token-Wahrscheinlichkeiten aus Logprobs; Nachdenken
abgeschaltet (Gemma ~10× schneller, GLM besser und ~25× billiger); Antworten auf 800 Tokens begrenzt;
Prompt fragt ausdrücklich nach Fettdruck; Prompt-Version und Nachdenk-Einstellung im Cache-Schlüssel.
- **`run`:** in Paketen zu 50 mit Statuszeile; beide lokalen Modelle parallel; GLM nur für Einige;
geprüft wird nur die übernommene Lesart; Prüfergebnisse mit Verfahrensversion.
- **Bildvergleich:** elastische Ausrichtung (DTW über Spalten und Zeilen, Dehnen kostet proportional zur
Tinte); layouttreue Prüfvarianten und rauschnormierter Kontrast im Pixel-Modus (`decide=pixel`).
- **apply:** deutsches Dezimalkomma `3{,}6`.
- **Behoben:** Gleichungsnummern wurden aus alten, verworfenen Lesarten übernommen (erfundene „(3.7)“);
sie kommen jetzt nur aus der übernommenen Lesart. Gleichzeitige Speicherungen zweier Engines sind
abgesichert. Verify stürzte bei Formeln ohne aktuelle Lesart ab.
- **Werkzeuge:** `calibrate --consensus`, `tools/vergleich.py` (50er-Stichprobe mit Bildern),
`tools/konsens_bericht.py`; Ausgaben in `vergleich/` (nicht versioniert).
- **Messung** (2 × 150 Formeln mit bekanntem TeX): 63–68 % automatisch, 0 falsch.
Gehrke-Brückenkurs: 942 von 1505 automatisch (63 %), 62 von 62 geprüften korrekt.
## [0.2.0] – 2026-10-07
## 0.1.0 – 2026-10-07
Entscheider ist der Konsens dreier Bildmodelle statt eines Pixelvergleichs.
Gehrke-Brückenkurs: 942 von 1505 automatisch (63 %), 62 von 62 geprüften korrekt.
Erste Fassung: scan, recognize (ein Vision-Modell), verify mit Pixelvergleich und Kontrasttest gegen
Verwechslungsvarianten, Review-Galerie, apply/revert, calibrate. Sicher (0 Falsch-Annahmen), aber an
echten Büchern nur 1–2 % automatisch.
### Neu
- **Konsens:** Qwen3-VL-30B-A3B (lokal) = Gemma-4-26B-A4B (lokal) = GLM-4.6V (OpenRouter); Pandoc muss die
Formel umsetzen; grobes Bild-Veto (elastischer Vergleich, Ähnlichkeit ≥ 0,65).
- `consensus.py`: Normalform auf Baumbasis und Render-Gleichheit als Rückfallebene.
- Zweite lokale Engine `local2`; Token-Wahrscheinlichkeiten aus Logprobs.
- `run` in Paketen zu 50 mit Statuszeile; `calibrate --consensus`; `tools/vergleich.py`,
`tools/konsens_bericht.py`.
- `apply`: deutsches Dezimalkomma `3{,}6`.
### Geändert
- Nachdenken der Modelle abgeschaltet (Gemma ~10× schneller, GLM besser und ~25× billiger); Antworten auf
800 Tokens begrenzt; Prompt fragt ausdrücklich nach Fettdruck.
- Elastischer Bildvergleich (DTW über Spalten und Zeilen, Dehnen kostet proportional zur Tinte).
- Geprüft wird nur die übernommene Lesart; Prüfergebnisse tragen eine Verfahrensversion.
### Behoben
- Gleichungsnummern wurden aus alten, verworfenen Lesarten übernommen (erfundene „(3.7)“).
- Gleichzeitiges Speichern zweier Engines; Absturz bei Formeln ohne aktuelle Lesart.
### Beim Update zu tun
- Zwei llama.cpp-Server starten (Qwen auf Port 8011, Gemma auf Port 8012) und Gemma laden
(BEDIENUNGSANLEITUNG, Abschnitte 1 und 2); `OPENROUTER_API_KEY` setzen.
- Arbeitsordner aus 0.1.0: Standard ist jetzt der Konsens; den alten Entscheider behält
`"decide": "pixel"` in `workspace.json`.
## [0.1.0] – 2026-10-07
Erstveröffentlichung: scan, recognize (ein Bildmodell), verify mit Pixelvergleich und Kontrasttest gegen
Verwechslungsvarianten, Review-Galerie, apply/revert, calibrate. Sicher (0 Falsch-Annahmen), aber an echten
Büchern nur 1–2 % automatisch.

432
FUNKTIONSWEISE.md Normal file
View file

@ -0,0 +1,432 @@
# 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 | – |

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Dieter Schlüter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

377
README.md
View file

@ -3,357 +3,56 @@
Nachbearbeitungs-Werkzeug für die Markdown-Ausgaben von
[epub2md](https://kitux.de/forgejo/dschlueter/epub2md) und
[pdf2md](https://kitux.de/forgejo/dschlueter/pdf2md): Formeln, die nur als Bild vorliegen, werden durch
LaTeX ersetzt – **aber nur, wenn zwei unabhängige Erkenner dieselbe Formel lesen** (zuerst das Formel-OCR
pix2text, bestätigt vom Bildmodell Qwen; bei Uneinigkeit darf Gemma pix2text bestätigen) und ein grober
Bildabgleich nicht widerspricht. Alles andere geht in eine Review-Galerie oder bleibt Bild.
LaTeX ersetzt. Beim Neubau als EPUB oder PDF passen sie dann in Größe und Schrift zum Text.
Für PDF-Bücher findet `pdf2md --formulas` jede Formel auf der Seite, auch im Fließtext und in Merkkästen,
und schneidet sie als Bild aus; formula_img2latex übersetzt sie dann ebenso (siehe [Quellen](#quellen)).
- **Nur bestätigte Formeln:** Das Formel-OCR **pix2text** liest jede Formel, das Bildmodell **Qwen3-VL**
muss dieselbe Formel lesen (bei Uneinigkeit darf **Gemma 4** pix2text bestätigen), und ein grober
Bildabgleich darf nicht widersprechen. Alles andere geht in eine Review-Galerie oder bleibt Bild.
- **Annahmeregel:** jedes Zeichen richtig und an der richtigen Stelle; Abstände, Längen, Schriftgröße,
kursiv/aufrecht und Fettdruck dürfen abweichen.
- **Lokal:** pix2text im Prozess, Qwen und Gemma über llama.cpp; keine Buchinhalte verlassen den Rechner.
- **Wiederholbar und umkehrbar:** Ergebnisse liegen in einem Arbeitsordner neben der Ausgabe, `apply`
schreibt aus einer Sicherung, `revert` stellt den alten Stand wieder her.
- **PDF-Bücher:** Mit `pdf2md --formulas` werden auch Formeln im Fließtext und in Merkkästen erfasst.
Schritt-für-Schritt-Anleitung: [BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md). Änderungen je Version:
[CHANGELOG.md](CHANGELOG.md). Hintergrund und ursprüngliche Planung: `Idee/`.
## Schnellstart
## Annahmeregel
> 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.
Eine Formel wird automatisch ersetzt (Regel `p2t`, Standard), wenn
1. **pix2text** (Formel-OCR MFR 1.5, lokal, GPU) sie zuerst liest und **Qwen3-VL-30B-A3B** (Bildmodell,
lokal, llama.cpp) dieselbe Formel liest **oder**, wenn die beiden uneinig sind, **Gemma-4-26B-A4B**
(lokal) die Lesart von pix2text bestätigt. Übernommen wird immer **pix2texts Lesart** – sie trifft den
Fettdruck am besten. Ihre überflüssigen Leerzeichen entfernt vorher `post_process` aus pix2tex (LaTeX-OCR):
`\frac{\alpha} {3 6 0}` → `\frac{\alpha}{360}`; am gesetzten Bild ändert das nichts, geprüft an
400 Lesarten. Auch der Schlusspunkt muss bestätigt sein,
2. Pandoc sie nach MathML umsetzen kann (sonst bliebe sie im EPUB/PDF roher TeX-Text) und
3. das gerenderte LaTeX dem Original grob ähnelt (Bild-Veto, elastischer Vergleich, Ähnlichkeit ≥ 0,65).
Sind Qwen und Gemma gemeinsam gegen pix2text einig, entscheidet ein Mensch: Genau das sind die Fehler,
die beide Bildmodelle teilen (`\mathcal{E}_r` statt `\varepsilon_r`, erfundenes `\tilde{z}`).
Die frühere Regel `vlm3` (Qwen = Gemma, bestätigt von **GLM-4.6V** über OpenRouter) bleibt wählbar:
`"consensus_rule": "vlm3"` unter `settings` in `workspace.json` des Arbeitsordners.
„Dieselbe Formel“ prüft `consensus.py`: gleiche Normalform (Abstände, `\left`/`\right`, überflüssige
Klammern, `\vec`/`\overrightarrow`, `\frac`/`\dfrac`, `\iff`/`\Leftrightarrow`, `pmatrix`/`\left(\begin{matrix}`,
TeX-Umlaute, `\atop`/`\substack`, `\cdot\cdot\cdot`/`\cdots`, `\root3\of{x}`/`\sqrt[3]{x}`, kursiv/aufrecht,
fett/normal …) oder – als Rückfallebene – zeichengleiches Rendering der beiden Lesarten (Render gegen
Render, nicht gegen das verrauschte Original). Eine Gleichungsnummer muss in allen Lesarten
übereinstimmen und wird als Text hinter die Formel gesetzt.
**Mehrzeilige Formeln** (seit 0.4.0): gleich, wenn sie dieselben Zeilen mit denselben Zeichen haben,
egal in welchem Rahmen. `aligned`/`gathered`/`split` und ihr `&` fallen weg (`a\\b` =
`\begin{aligned}a&\\b\end{aligned}`), eine leere letzte Zeile zählt nicht; `cases`,
`\left\{\begin{array}{ll}…\right.` und pix2texts `\left\{\begin{aligned}…\right.` sind dasselbe.
Matrizen bleiben Matrizen: Dort trennt `&` Spalten (`1&2` ≠ `12`). Ob eine Gruppe ein Argument ist,
entscheidet die Argumentzahl des Befehls (`\frac` 2, `\sqrt` 1, `\Delta` 0), nicht ihre Stellung:
`t^{2}\,{\sqrt{2}}` = `t^2\sqrt{2}`, `\Delta{x_{n}}` = `\Delta x_n`.
**Gleiche Struktur** (`structure.py`, sympy, **optional, standardmäßig aus**): Beide Lesarten ergeben
denselben *unausgewerteten* Ausdrucksbaum (`\frac{a}{b}` = `a/b`, `a\cdot b` = `ab`); nichts wird
umgerechnet oder umgestellt (`a+b` ≠ `b+a`, `2x=4` ≠ `x=2`, `a-b` ≠ `a+(-b)`). Relationen gehen nie an
sympy (es wertet `v=(3,4)ᵀ` und `\nu=(3,4)ᵀ` beide zu `False` aus), jedes Zeichen wird durch einen
Platzhalter geschützt (sympy macht aus `\varepsilon` und `\epsilon` ein Symbol), sichtbare Klammern
zählen, Ungedecktes fällt auf den Render-Vergleich zurück. Gemessen: vor der mehrzeiligen Normalform
+1 Formel bei 400, danach **nichts mehr** – deshalb aus. Einschalten: `structural` = 1 oder
`FORMULA_STRUCTURAL=1`; braucht `pip install sympy lark`.
Sind sich die Modelle uneinig oder widerspricht das Veto, entscheidet ein Mensch im Review.
## Messung
### Regel `p2t` (Standard)
Göllmann Band 1, 150 echte Bilder und 150 Nachbildungen im Gehrke-Stil, Lesarten aller Modelle:
| Regel | echt: automatisch / falsch | Gehrke-Stil: automatisch / falsch |
| --- | --- | --- |
| pix2text = Qwen | 81 % / 0 | 76 % / 0 |
| + Stichwahl: Gemma bestätigt pix2text, Qwens Lesart (erste Fassung) | 85 % / 0 | 80 % / 0 |
| **pix2text zuerst und übernommen, bereinigt, Stichwahl Gemma** (Standard) | **86 % / 0** | **85 % / 0** |
| + Stichwahl auch Qwen = Gemma gegen pix2text | 92 % / 1 | 91 % / 2 |
| Qwen = Gemma = GLM (Regel `vlm3`) | 78 % / 0 | 70 % / 0 |
Ab 0.4.0 mit mehrzeiliger Normalform, gleiche Lesarten (`--reuse`): **89 % / 0** (134) und **88 % / 0**
(132); Band 2 (`tools/regeltest.py`, zwei Stichproben zu 50): **44 und 45** automatisch statt 39 und 43,
**0 falsch**, 6 bzw. 5 Review. Zusammen +17 Formeln bei 400. Die Tabelle und die folgenden Absätze
zeigen den Stand 0.3.0.
Allein richtig: pix2text 90 % / 85 %, Qwen 86 % / 84 %, Gemma 86 % / 80 %, GLM 86 % / 83 %.
Zeit je Formel (seriell, 15 Formeln): pix2text 0,10 s (GPU) bzw. 0,51 s (CPU), Qwen 0,72 s, GLM 1,19 s,
Gemma 1,77 s. Gegenprobe an Göllmann Band 2 (`tools/regeltest.py`, zwei getrennte Stichproben zu 50
Formeln): 39 bzw. 43 automatisch (78 % bzw. 86 %), **0 falsch**, 11 bzw. 7 Review; Gemma wurde für 14
bzw. 9 Formeln gefragt. Lesen je Formel im Mittel 2,3 bzw. 1,5 s (alle Tools nacheinander, ohne Bildprüfung).
pix2text ist nicht unfehlbar: In den Reviews standen auch Fälle, in denen nur pix2text sich verlesen hatte
(fehlende Wurzelexponenten, zerfallene Vektoren, `\nu` statt fettem v) – deshalb bleibt die Bestätigung
durch Qwen Pflicht.
**Bild-Schiedsrichter** (0.4.0, gemessen, **aus**): Bei „Qwen = Gemma gegen pix2text“ misst
`verify.image_arbiter`, wie deutlich das Original Qwens Lesart zeigt (gleiches Kontrastmaß wie der
Variantentest). In 16 solchen Fällen (Band 1 und 2) hatte Qwen 14-mal recht, mit Belegen von 3 bis 50.
Aber: Beim gemeinsamen Fehler `\mathcal{E}_r` statt `\varepsilon_r` sprach das Bild mit 7,2 sogar für
die falsche Lesart. Ab Schwelle 8 wären es +11 Formeln bei 0 falsch – der Abstand ist bei zwei
Gegenbeispielen nicht belastbar. Einschalten: `arbiter` = 1, `arbiter_margin` (5).
**Prompt-Stilvorgabe** (0.4.0, gemessen, verworfen): feste Schreibweisen für `aligned`, `pmatrix`,
`cases`, `\dots`, `\text`. Band 2: 44 und 44 statt 44 und 45 – die Normalform gleicht die Schreibweisen
schon aus, und mit der Zeilenvorgabe schrieben beide Modelle nach `\\` ein wörtliches `\n`.
### Regel `vlm3` (Standard bis 0.2.0)
An Formelbildern mit bekanntem TeX (Göllmann, Springer: TeX im alt-Text), je 150 echte Bilder und 150
Nachbildungen im Stil von Gehrke (LaTeX + dvipng mit Bitmap-Schriften, ~102 dpi, JPEG):
| Regel | echt: automatisch / davon falsch | Gehrke-Stil: automatisch / davon falsch |
| --- | --- | --- |
| Qwen = Gemma | 73 % / 1 | 69 % / 2 |
| Qwen = Gemma = Gemini 2.5 Flash | 67 % / 0 | 57 % / 1 |
| **Qwen = Gemma = GLM-4.6V** ohne Nachdenken (Standard) | **68 % / 0** | **63 % / 0** |
- Fehler, bei denen zwei Modelle gemeinsam irren, gibt es: `\mathcal{E}_r` statt `\varepsilon_r`, ein
erfundenes `\tilde{z}`, `x^{f}` statt `x'`. Die dritte Lesart aus einer **anderen Modellfamilie** fängt
sie ab; Gemini (mit Gemma verwandt) machte einen davon mit.
- Selbstauskünfte der Modelle („95 % sicher“) sind wertlos. Token-Wahrscheinlichkeiten (Logprobs) werden
gespeichert, brachten aber neben der dritten Lesart keinen Gewinn (Schwellen bleiben einstellbar).
- „Nachdenken“ (Reasoning) ist für alle Modelle abgeschaltet: Gemma 4 wird ~10× schneller, GLM-4.6V liest
ohne Nachdenken sogar besser (85 % statt 79 % allein richtig) und braucht ~25× weniger Tokens.
- Fettdruck lesen alle Modelle schlecht (fette Vektoren werden zu normalen Buchstaben); nach Vorgabe zählt
er nicht. Mit `bold_relevant=1` würden diese Fälle ins Review gehen.
- Das Bild-Veto fing den einzigen Fall ab, in dem alle drei Modelle gemeinsam irrten (Ähnlichkeit
0,5–0,6); richtige Lesarten lagen bei ≥ 0,66. Daher die Schwelle 0,65. Eine Prüfung der Schriftgröße
(Pixel pro Punkt gegen das Buchmittel) erzeugte nur Fehlalarme und ist abgeschaltet.
- Die Messung lief **ohne** Textausschnitt um die Formel; `run` gibt den Modellen einen kurzen
Textausschnitt (≤ 240 Zeichen) mit. Die Stichproben am Buch (62 von 62 korrekt) sprechen nicht gegen
den Kontext, gemessen ist er aber nicht.
Gehrke, Brückenkurs Mathematik (EPUB, 1505 Formelbilder): siehe [Stand](#stand).
## Ablauf
```text
scan Bildverweise in chapters/*.md (bzw. pdf2md-Markdown) finden, Formel/Abbildung vorklassifizieren
run in Paketen zu 50: pix2text, dann Qwen lesen, prüfen, Gemma nur für Uneinige, prüfen;
nach jedem Paket eine Statuszeile mit Zeiten je Stufe (Regel vlm3: Qwen + Gemma, GLM nur
für Einige). Die GPUs lesen schon das nächste Paket, während die CPU das vorige prüft.
review Zweifelsfälle im Browser: Original / Render / Overlay / Diff, LaTeX korrigieren und neu prüfen
apply angenommene Formeln ins Markdown schreiben (aus einer Sicherung, idempotent)
revert Markdown-Dateien auf den Stand vor apply zurücksetzen
recognize einzelne Engine (local, local2, cloud, p2t) laufen lassen
verify prüfen und entscheiden (ohne neue Modellabfragen)
status Zählung je Status
calibrate Messung an Formelbildern mit bekanntem TeX (--consensus: Konsensregel)
```
```bash
python3 formula_img2latex.py run output/buch # epub2md-Ausgabeordner
python3 formula_img2latex.py review output/buch
python3 formula_img2latex.py apply output/buch
python3 epub2md_merge.py output/buch --force # Gesamtdatei danach neu bauen
```
PDF-Buch mit allen Formeln, auch im Fließtext:
```bash
cd ~/Python_Programs/pdf2md
python3 pdf2md.py --formulas --epub buch.pdf Output/buch/buch.md # Formeln als Bilder + Liste
cd ~/Python_Programs/formula_img2latex
python3 formula_img2latex.py run ~/Python_Programs/pdf2md/Output/buch/buch.md --pdf buch.pdf
python3 formula_img2latex.py apply ~/Python_Programs/pdf2md/Output/buch/buch.md
cd ~/Python_Programs/pdf2md && python3 pdf2md.py --from-md Output/buch/buch.md # EPUB mit MathML
```
Status je Formel: `auto_accepted`, `needs_review`, `rejected_keep_image`, `render_failed`, `no_candidate`,
`not_formula`, nach Review `accepted_by_human`, `corrected_by_human`, `kept_by_human`.
## Modelle und Server
| Engine | Standard | Konfiguration |
| --- | --- | --- |
| `local` | llama.cpp `http://127.0.0.1:8011/v1` (Qwen3-VL-30B-A3B-Instruct Q4_K_M + mmproj), 8 parallele Anfragen | `FORMULA_LOCAL_URL`, `FORMULA_LOCAL_MODEL` |
| `local2` | llama.cpp `http://127.0.0.1:8012/v1` (Gemma-4-26B-A4B-it Q4_0 + mmproj) | `FORMULA_LOCAL2_URL`, `FORMULA_LOCAL2_MODEL` |
| `cloud` | OpenRouter, `z-ai/glm-4.6v` (nur Regel `vlm3`) | `OPENROUTER_API_KEY`, `FORMULA_CLOUD_MODEL` |
| `p2t` | pix2text MFR 1.5 (ONNX) im Prozess, die sichtbare GPU mit dem meisten freien Speicher, sonst CPU; gebündelt nach Pixelbudget | `FORMULA_P2T_DEVICE` (`auto`, `cpu`, `cuda:N`), `FORMULA_P2T_MODEL` |
pix2text auf der GPU braucht `onnxruntime-gpu` statt `onnxruntime` (cuDNN kommt aus den nvidia-Paketen
von torch). Für `post_process` wird `pix2tex` ohne seine Abhängigkeiten installiert
(`pip install --no-deps pix2tex munch`), weil es sonst `timm==0.5.4` erzwingen würde. `cuda:N` zählt innerhalb von `CUDA_VISIBLE_DEVICES`.
`FORMULA_<ENGINE>_THINKING=1` schaltet das Nachdenken wieder ein. Antworten sind auf 800 Tokens begrenzt
(lange mehrzeilige Formeln brauchen ~400; einzelne Antworten von Gemma liefen sonst in
Wiederholungsschleifen bis 165 s). Jede Lesart wird mit Modell, Prompt-Version und Nachdenk-Einstellung
gespeichert; dieselbe Frage wird nie zweimal gestellt, ein abgebrochener Lauf setzt dort fort.
Qwen-Server mit `--parallel 8 -c 32768` (je Platz 4096 Tokens) starten; `run` stellt bis zu 8 Anfragen
gleichzeitig (BEDIENUNGSANLEITUNG). Gemessen und verworfen: Qwen3-VL-8B statt 30B-A3B ist nicht schneller
(das MoE-Modell rechnet je Token nur ~3B Parameter) und liest etwas schlechter (Göllmann 42/44 statt 44/45
automatisch).
Kosten der Cloud-Stufe (nur Regel `vlm3`): GLM liest nur Formeln, bei denen Qwen und Gemma einig sind. Gehrke: 967
Anfragen, Ø 2,6 s, zusammen rund 0,15–0,20 $.
## Bildvergleich (Veto)
`compare.py` rendert die Lesart mit pdflatex (Schrift je Buch automatisch: `cm`, `times`, `txfonts`,
`palatino`, `stix`, `euler`), richtet sie auf das Original aus und verzerrt sie **elastisch**
(dynamische Zeitverzerrung über Spalten und Zeilen; Dehnen kostet proportional zur Tinte), damit
Abstände und Längen keine Rolle spielen. Die Ähnlichkeit danach ist das Veto. Geprüft wird nur
die Lesart, die übernommen werden kann: die von pix2text (Regel `vlm3`: die von Qwen). Bestätigende
Lesarten werden nie übernommen und brauchen keine Prüfung. Die Prüfergebnisse tragen eine
Versionsnummer des Verfahrens und werden nach Änderungen neu berechnet. Der alte Pixel-Entscheider mit
Verwechslungsvarianten und Kontrasttest bleibt als Modus `decide=pixel` erhalten.
Sicherheit: Kandidaten kommen von Modellen und gelten als unsicher. Vor pdflatex werden Befehle wie
`\input`, `\write`, `\def`, `\catcode` abgelehnt; pdflatex läuft mit `-no-shell-escape`,
`openout_any=p`, Zeitlimit.
## Arbeitsordner
```text
<ausgabe>.formulas/ # neben der Ausgabe, weil epub2md --force die Ausgabe ersetzt
├── workspace.json # Quelle, Schrift, Einstellungen, apply-Zustand
├── formulas/f-<sha12>-<d|i>.json # je Bild und Darstellung: Lesarten, Prüfung, Entscheidung
├── crops/<id>.png # Original je Formel, einmal ausgeschnitten (PDF-Quellen)
├── review/<id>-{orig,render,rigid,diff}.png
├── bericht/ # Ergebnisseite (tools/buch_bericht.py)
└── backup/ # Markdown-Dateien vor dem ersten apply
```
Die Datensätze hängen am SHA-256 des Bildes: Ein erneuter epub2md-Lauf verliert keine Entscheidungen.
Einstellungen in `workspace.json` → `settings.consensus_thresholds`: `require_third` (1),
`bold_relevant` (0), `structural` (0), `arbiter` (0), `arbiter_margin` (5), `veto_ncc` (0,65),
`veto_scale_dev` (0 = aus), `min_p_mean`/`min_p_min` (0), `pdftext` (0: die PDF-Textebene bestätigt nie;
gemessen: in 12 von 319 Fällen fehlten dort Punkte, Striche oder Indizes, die Qwen sah).
## Quellen
| Quelle | Erkennung | Begrenzer |
| --- | --- | --- |
| epub2md-Ordner | `conversion-report.json`; Warnungen `formula-image` markieren sichere Formeln | `--math auto`: `$…$`/`$$…$$`, `--math tex`: `\(…\)`/`\[…\]`; deutsche Bücher: `3{,}6` |
| pdf2md (`<name>.md` + `<name>_images/` oder `--split`-Ordner) | Bildname `<pdf>-<seite>-<box>`; mit `--pdf` per Template-Matching auf der Seite gefunden und mit 600 dpi aus dem Vektor-PDF neu ausgeschnitten | `$…$`/`$$…$$` |
| pdf2md `--formulas` | Bildname `<pdf>-<seite>-f<nr>.png` (300 dpi), Liste `<name>_images/formulas-*.json` mit Seite, Rechteck, Art (im Text/abgesetzt) und PDF-Text; das Bild wird direkt verwendet | `$…$`/`$$…$$` |
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 (statt Glyphensalat wie `x4n`).
Ein Satzzeichen am Ende einer eingebetteten Formel wird vor dem Vergleich abgetrennt und als Text hinter
die Formel gesetzt (`$a^{x}$,`). Codeblöcke werden nie verändert. Formeln in Tabellen erfasst die
Erkennung noch nicht.
## Werkzeuge zum Einstellen
- `tools/vergleich.py AUSGABE` – feste Stichprobe (50 Formeln), verarbeitet wie `run`, schreibt
`vergleich/<name>/index.html` mit Original, gerenderten Lesarten, Entscheidung und Veto-Overlay.
- `tools/konsens_bericht.py BERICHT.json EPUB` – Übersichtsseite zu `calibrate --consensus --report`.
- `tools/zeittest.py EPUB` – Zeit und Trefferquote je Tool, seriell an 15 Formeln mit bekanntem TeX.
- `tools/regeltest.py EPUB [--n 50] [--reuse ergebnis.json] [--refresh local,local2] [--arbiter 8]` – die
Standardregel an Formeln mit bekanntem TeX, wie `run`; Seite mit Original, wahrer Formel, allen
Lesarten und Entscheidung. `--refresh` fragt trotz `--reuse` diese Engines neu (Prompt-Test).
- `tools/buch_bericht.py AUSGABE` – Ergebnisseite eines ganzen Buchs nach `run`: jede Formel mit
Original, gesetztem LaTeX, Status, Begründung und allen Lesarten, filterbar; liegt in
`<arbeitsordner>/bericht/index.html` (Buchausschnitte, nicht versioniert).
- `tools/vergleich_bericht.py --regeltest VORHER:NACHHER --kalibrierung VORHER.json:NACHHER.json` –
Vorher/Nachher-Seite zweier Läufe auf denselben Lesarten: Zahlen, jede geänderte Entscheidung und
jedes geänderte Urteil gegen die Wahrheit, mit Original und gerenderten Lesarten.
- `calibrate --consensus --engines local,local2,cloud,p2t --report X.json` misst alle Regeln;
`--reuse X.json` rechnet sie ohne Modellabfragen neu.
- `vergleich/` ist nicht versioniert (Ausschnitte aus Büchern).
## Stand
**Formeln im Fließtext** (unveröffentlicht, nach 0.4.0): Corral mit `pdf2md --formulas`: 9239 Formelbilder
(davon ~88 % eingebettet). Stichprobe 10 % (940): **747 automatisch (79 %)**, im Text 83 %, abgesetzte
Box-Bilder (Herleitungen, Kästen) nur ~29 %; 174 Review, 19 keine Formel; 13 Minuten, hochgerechnet ~2 h
für das ganze Buch. Beschleunigung am Pilot (869 Formeln): 70 → ~24 Minuten, Entscheidungen zu 863/876
gleich (die Abweichungen kommen von den Modellen selbst). Qualität ist hier ohne bekannte Lösung –
vor dem Einsatz eine Stichprobe der übernommenen Formeln von Hand prüfen.
Version 0.4.0 (Regel `p2t`, mehrzeilige Normalform) ist an Göllmann Band 1 und 2 gemessen (siehe
[Messung](#messung)), aber noch **nicht am ganzen Gehrke** gelaufen.
**Erstes ganzes PDF-Buch** (0.4.0): Corral, *Elementary Calculus* (346 Seiten, TeX-PDF, pdf2md mit
600-dpi-Neuausschnitt): 1150 Ausschnitte → **429 automatisch ersetzt (37 %)**, 633 Review, 61 keine
Formel, 27 Abbildungen. EPUB (`pdf2md --from-md`, jetzt mit `--mathml`): 429 Formeln als MathML, kein
TeX-Rest. Laufzeit rund 4 Stunden (pix2text Ø 3,9 s, Qwen Ø 8,1 s, Gemma Ø 15 s je Anfrage – große
Ausschnitte). Das Review besteht überwiegend aus Ausschnitten, die keine einzelne Formel sind
(Merkkästen mit Text, Tabellen, Diagramme, ganze Herleitungen) – das muss pdf2md besser trennen. Die folgenden Zahlen stammen von Version 0.2.0 (Regel `vlm3`, mit GLM).
Version 0.2.0 (Konsens), getestet am Gehrke, Brückenkurs Mathematik (EPUB, CM-Formeln als kleine JPGs):
| | Anzahl |
| --- | --- |
| Formelbilder verarbeitet (sicher oder unklar klassifiziert) | 1505 |
| **automatisch ersetzt** | **942 (63 %)** |
| Review: Qwen und Gemma uneinig | 410 |
| Review: GLM weicht ab | 65 |
| Review: Bild-Veto / Pandoc | 2 / 1 |
| keine Formel (laut Modellen) | 85 |
| nicht geprüft (Abbildungen, Symbole) | 539 |
- **Von Hand geprüft:** die 42 automatisch ersetzten Formeln der festen 50er-Stichprobe und 20 zufällig
gezogene weitere – **62 von 62 Zeichen für Zeichen korrekt** (Brüche, Wurzeln mit Index, `cases`,
Grenzwerte, Integrale, Vektorpfeile, Überstriche über mehreren Buchstaben).
- **apply** an einer Kopie: 946 Ersetzungen in 258 Kapiteldateien (manche Formeln kommen mehrfach vor).
`epub2md_merge.py` → 945 Formeln in der Gesamtdatei (eine stand in einer doppelten Fußnote, die das
Zusammenfügen entfernt). Pandoc `--mathml`: **945 von 945** als MathML, keine „Could not convert TeX
math“-Meldung. EPUB daraus in 10 s gebaut.
- **Laufzeit** mit beiden lokalen Modellen parallel (zwei RTX 3090) und GLM: ~5 Minuten je 50 Formeln,
das Buch also rund 2,5 Stunden.
Grenzen und offene Punkte:
- Mehrzeilige Rechnungen: Rahmen und `&` gleicht die Normalform seit 0.4.0 aus; verschiedene
Zeilenumbrüche, `\underbrace` und fehlende Integrationsgrenzen bleiben im Review.
- Formeln auf farbig hinterlegten Kästen verlieren beim Ersetzen die Hinterlegung.
- Fettdruck wird nicht geprüft (Vorgabe).
- Die Messung umfasst 2 × 150 Formeln eines Buches ohne Textausschnitt; „0 Fehler“ bedeutet eine
Fehlerquote sehr wahrscheinlich unter ~2 %.
- pdf2md-Bücher: am ganzen Corral getestet (ohne `--formulas` 0.4.0, mit `--formulas` 10 %); Formeln in
Tabellen fehlen noch.
- Einzelne Riesenausschnitte (Grafiken, dreizeilige Herleitungen mit völlig anderer Lesart) werden ohne
Bildvergleich abgelehnt: über 2,5 MP oder mehr als 2,5-fach falsche Proportionen; breite Originale werden
für den Vergleich auf 1600 px verkleinert.
## Installation
Voraussetzungen und das Starten der Modellserver: [BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md).
```bash
sudo apt install texlive-latex-extra texlive-fonts-extra texlive-science pandoc dvipng
pip install -r requirements.txt # pymupdf, numpy, scipy, scikit-image, opencv, Pillow, lxml, requests
pip install -r requirements-dev.txt # pytest
pip install -r requirements.txt
pip install "pix2text[multilingual]" && pip install --no-deps pix2tex munch
python3 formula_img2latex.py run ~/Python_Programs/epub2md/output/buch # lesen, prüfen, entscheiden
python3 formula_img2latex.py review ~/Python_Programs/epub2md/output/buch # Zweifelsfälle im Browser
python3 formula_img2latex.py apply ~/Python_Programs/epub2md/output/buch # Formeln ins Markdown schreiben
```
Modelle: siehe BEDIENUNGSANLEITUNG (llama-server starten).
## Stand
## Tests
Version 0.4.0 plus unveröffentlichte Änderungen (siehe [CHANGELOG.md](CHANGELOG.md)). An Formelbildern
mit bekannter Lösung (Göllmann, Springer) werden 88–89 % automatisch ersetzt, **keine davon falsch**.
Ganze Bücher: Gehrke-Brückenkurs (EPUB) 63 %, alle 62 von Hand geprüften korrekt; Corral mit
`pdf2md --formulas` 79 % (Stichprobe). Einzelheiten: [FUNKTIONSWEISE.md, Messungen](FUNKTIONSWEISE.md#12-messungen).
## Documentation
| Dokument | Inhalt |
|---|---|
| [BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md) | Installation, Modellserver, ein Buch bearbeiten, Konfiguration, Betrieb, Fehlersuche |
| [FUNKTIONSWEISE.md](FUNKTIONSWEISE.md) | Aufbau, Datenfluss, Entscheidungsregel, Normalform, Bildabgleich, Messungen, Designentscheidungen, Grenzen |
| [CHANGELOG.md](CHANGELOG.md) | Änderungen je Version und was beim Update zu tun ist |
| [LICENSE](LICENSE) | MIT |
| `Idee/` | ursprüngliche Problemstellung und Planung |
## Entwicklung
```bash
pip install -r requirements-dev.txt
python3 -m pytest -q # ca. 45 s; synthetische Bilder, kein Modellserver, keine Buchinhalte
```
Rückgabecodes: `0` Erfolg, `1` Bedienfehler, `2` Eingabe ist keine epub2md-/pdf2md-Ausgabe,
`3` externes Werkzeug fehlt (pdflatex, pandoc, Modellserver), `4` interner Fehler.
## License
## Architektur
```text
formula_img2latex/
├── cli.py Unterbefehle, Exit-Codes
├── pipeline.py scan, recognize, verify, Entscheidung (Konsens/Pixel), run in Paketen, apply/revert
├── consensus.py Normalform, Render-Gleichheit: sagen zwei Lesarten dasselbe?
├── decide.py Annahmeregeln und Schwellen (Konsens- und Pixel-Modus)
├── engines/ Vision-Modelle über die OpenAI-API (llama.cpp, OpenRouter), Logprobs, Prompt-Version;
│ p2t.py: pix2text-Formel-OCR (ONNX, CPU/GPU)
├── compare.py Ausrichtung, elastische Verzerrung, Kennzahlen, Kontrasttest, Diff-Bilder
├── confusables.py Verwechslungsvarianten (Pixel-Modus)
├── verify.py Bildprüfung einer Lesart (Veto bzw. Pixel-Entscheider)
├── render.py pdflatex-Batch, Schrift-Presets, Rasterung
├── imageio.py Originale laden (PNG/GIF/JPG/SVG, Transparenz, Tönung)
├── sources/ epub2md.py, pdf2md.py, base.py (Markdown-Scan, Begrenzer, Dezimalkomma)
├── structure.py Strukturvergleich mit sympy (optional, aus)
├── pdfcrop.py Formelbild auf der PDF-Seite finden, 600-dpi-Neuausschnitt
├── classify.py Formel / unklar / Abbildung
├── checks.py Pandoc/MathML-Prüfung
├── calibrate.py Messung an Formelbildern mit bekanntem TeX
├── store.py Arbeitsordner, Datensätze
└── review/ lokaler Review-Server + Seite
```
MIT, siehe [LICENSE](LICENSE).

View file

@ -15,13 +15,14 @@ from .render import PRESETS
DESCRIPTION = """\
Ersetzt Formelbilder in Markdown-Ausgaben von epub2md und pdf2md durch LaTeX – aber nur, wenn
das gerenderte LaTeX nachweislich so aussieht wie das Original. Zweifelsfälle gehen in eine
Review-Galerie, alles andere bleibt Bild.
zwei Erkenner dieselbe Formel lesen (pix2text, bestätigt von Qwen oder bei Uneinigkeit von Gemma)
und ein grober Bildabgleich nicht widerspricht. Zweifelsfälle gehen in eine Review-Galerie,
alles andere bleibt Bild.
"""
EPILOG = """\
Typischer Ablauf:
formula_img2latex.py run output/buch # scan, erkennen (lokal, dann Cloud), prüfen
formula_img2latex.py run output/buch # scan, lesen (pix2text, Qwen, Gemma), prüfen
formula_img2latex.py review output/buch # Zweifelsfälle im Browser entscheiden
formula_img2latex.py apply output/buch # geprüfte Formeln ins Markdown schreiben
formula_img2latex.py revert output/buch # alles rückgängig machen
@ -56,10 +57,10 @@ def build_parser() -> argparse.ArgumentParser:
sp.add_argument("--quiet", "-q", action="store_true", help="nur Fehler ausgeben")
def recog_opts(sp: argparse.ArgumentParser) -> None:
sp.add_argument("--model", default=None, help="Modellname (Standard: local = Modell des Servers, "
"cloud = $FORMULA_CLOUD_MODEL oder google/gemini-2.5-flash)")
sp.add_argument("--model", default=None, help="Modellname (Standard: local/local2 = Modell des Servers, "
"cloud = $FORMULA_CLOUD_MODEL oder z-ai/glm-4.6v, p2t = mfr-1.5)")
sp.add_argument("--url", default=None, help="API-Basis-URL (Standard: local http://127.0.0.1:8011/v1, "
"cloud https://openrouter.ai/api/v1)")
"local2 http://127.0.0.1:8012/v1, cloud https://openrouter.ai/api/v1)")
sp.add_argument("--include-figures", action="store_true",
help="auch Bilder versuchen, die wie Abbildungen aussehen")
sp.add_argument("--limit", type=int, default=0, help="höchstens N Bilder (zum Ausprobieren)")
@ -70,7 +71,7 @@ def build_parser() -> argparse.ArgumentParser:
sp.add_argument("--pdf", type=Path, default=None,
help="pdf2md: Original-PDF, um Formeln scharf neu auszuschneiden")
sp = sub.add_parser("recognize", help="LaTeX-Kandidaten mit einem Vision-Modell erzeugen")
sp = sub.add_parser("recognize", help="Lesarten einer Engine erzeugen (p2t, local, local2, cloud)")
common(sp)
recog_opts(sp)
sp.add_argument("--engine", choices=["local", "local2", "cloud", "p2t"], default="local")
@ -85,13 +86,13 @@ def build_parser() -> argparse.ArgumentParser:
sp.add_argument("--include-figures", action="store_true")
sp.add_argument("--redo", action="store_true", help="auch bereits geprüfte Formeln neu prüfen")
sp = sub.add_parser("run", help="scan + recognize (lokal, dann Cloud) + verify")
sp = sub.add_parser("run", help="scan + lesen + prüfen + entscheiden, in Paketen mit Statuszeile")
common(sp)
recog_opts(sp)
sp.add_argument("--pdf", type=Path, default=None)
sp.add_argument("--cloud-model", default=None, help="Modell für die Cloud-Stufe")
sp.add_argument("--no-local", action="store_true", help="lokale Stufe überspringen")
sp.add_argument("--no-cloud", action="store_true", help="Cloud-Stufe überspringen")
sp.add_argument("--cloud-model", default=None, help="Regel vlm3: Modell für die Cloud-Stufe")
sp.add_argument("--no-local", action="store_true", help="nur Modus decide=pixel: lokale Stufe überspringen")
sp.add_argument("--no-cloud", action="store_true", help="Regel vlm3 bzw. decide=pixel: Cloud-Stufe überspringen")
sp.add_argument("--preset", choices=["auto", *PRESETS], default="auto")
sp.add_argument("--chunk", type=int, default=50, help="Paketgröße; nach jedem Paket eine Statuszeile (Standard 50)")

View file

@ -5,4 +5,6 @@ scikit-image>=0.22
opencv-python-headless>=4.8
Pillow>=10.0
lxml>=5.0
requests>=2.31
# pix2text separat, siehe BEDIENUNGSANLEITUNG: pip install "pix2text[multilingual]"; pip install --no-deps pix2tex munch
# optional, only for the structural comparison (FORMULA_STRUCTURAL=1): sympy>=1.13, lark>=1.1