Files
breakpilot-lehrer/docs-src/services/klausur-service/OCR-Pipeline.md
Benjamin Admin 85fe0a73d6
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 30s
CI / test-go-edu-search (push) Successful in 30s
CI / test-python-klausur (push) Failing after 2m28s
CI / test-python-agent-core (push) Successful in 17s
CI / test-nodejs-website (push) Successful in 18s
docs: Add OCR Kombi Pipeline to MkDocs and cross-reference from OCR Pipeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 16:09:40 +01:00

1752 lines
76 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OCR Pipeline - Schrittweise Seitenrekonstruktion
**Version:** 5.1.0
**Status:** Produktiv (Schritte 110 + Grid Editor + Regression Framework)
**URL:** https://macmini:3002/ai/ocr-pipeline
## Uebersicht
Die OCR Pipeline zerlegt den OCR-Prozess in **10 einzelne Schritte**, um eingescannte Seiten
aus mehrspaltig gedruckten Schulbuechern Wort fuer Wort zu rekonstruieren.
Jeder Schritt kann individuell geprueft, korrigiert und mit Ground-Truth-Daten versehen werden.
**Ziel:** 10 Vokabelseiten fehlerfrei rekonstruieren.
### Pipeline-Schritte
| Schritt | Name | Beschreibung | Status |
|---------|------|--------------|--------|
| 1 | Orientierung | 90/180/270° Drehungen von Scannern korrigieren | Implementiert |
| 2 | Begradigung (Deskew) | Scan begradigen (Hough Lines + Word Alignment) | Implementiert |
| 3 | Entzerrung (Dewarp) | Buchwoelbung entzerren (Vertikalkanten-Analyse) | Implementiert |
| 4 | Zuschneiden (Crop) | Content-basierter Crop: Buchruecken-Schatten + Ink-Projektion | Implementiert |
| 5 | Spaltenerkennung | Unsichtbare Spalten finden (Projektionsprofile + Wortvalidierung) | Implementiert |
| 6 | Zeilenerkennung | Horizontale Zeilen + Kopf-/Fusszeilen-Klassifikation + Luecken-Heilung | Implementiert |
| 7 | Worterkennung | Hybrid-Grid (v2) oder Words-First (bottom-up) | Implementiert |
| 8 | Korrektur | Zeichenverwirrung + regel-basierte Rechtschreibkorrektur (SSE-Stream) | Implementiert |
| 9 | Rekonstruktion | Interaktive Zellenbearbeitung auf Bildhintergrund (Fabric.js) | Implementiert |
| 10 | Validierung | Ground-Truth-Vergleich und Qualitaetspruefung | Implementiert |
!!! note "Reihenfolge-Aenderung (v4.1)"
Crop wurde hinter Deskew/Dewarp verschoben. Das Bild ist dann bereits gerade,
was den Content-basierten Crop deutlich zuverlaessiger macht — insbesondere
bei Buchscans mit Ruecken-Schatten und weissem Scanner-Hintergrund.
---
## Dokumenttyp-Erkennung und Pipeline-Pfade
### Automatische Weiche: `detect_document_type()`
Nicht jedes Dokument durchlaeuft denselben Pfad. Nach den gemeinsamen Vorverarbeitungsschritten
(Orientierung, Deskew, Dewarp, Crop) analysiert `detect_document_type()` die Seitenstruktur
**ohne OCR** — rein ueber Projektionsprofile und Textdichte-Analyse (< 2 Sekunden).
```
detect_document_type(ocr_img, img_bgr) → DocumentTypeResult
```
#### Entscheidungslogik
```mermaid
flowchart TD
A[Bild-Input] --> B[Vertikales Projektionsprofil]
B --> C{Interne Spalten-Gaps >= 2?}
C -->|Ja| D{Zeilen-Gaps >= 5?}
D -->|Ja| E["vocab_table<br/>pipeline = cell_first<br/>confidence 0.70.95"]
D -->|Nein| F{Zeilen-Gaps >= 3?}
C -->|Nein| G{Interne Spalten-Gaps >= 1?}
G -->|Ja| F
G -->|Nein| H["full_text<br/>pipeline = full_page<br/>skip: columns, rows"]
F -->|Ja| I["generic_table<br/>pipeline = cell_first<br/>confidence 0.50.85"]
F -->|Nein| H
```
| Dokumenttyp | Spalten-Gaps | Zeilen-Gaps | Pipeline | Beispiel |
|-------------|-------------|-------------|----------|----------|
| `vocab_table` | ≥ 2 | ≥ 5 | `cell_first` | 3-spaltige Schulbuch-Vokabeltabelle |
| `generic_table` | ≥ 1 | ≥ 3 | `cell_first` | 2-spaltiges Glossar |
| `full_text` | 0 | egal | `full_page` | Fliesstext, Aufsatz, Buchseite |
### Komplett-Flussdiagramm
```
┌─────────────────────────────────────────────────────────────────────┐
│ GEMEINSAME VORVERARBEITUNG (alle Dokumente) │
│ │
│ Schritt 1: Orientierung (90/180/270° Drehung korrigieren) │
│ Schritt 2: Deskew (Hough Lines + Iterative Projektion + Ensemble) │
│ Schritt 3: Dewarp (Vertikalkanten-Drift, Ensemble Shear) │
│ Schritt 4: Crop (Content-basiert: Schatten + Ink-Projektion) │
└─────────────────────────────────────┬───────────────────────────────┘
detect_document_type()
┌──────────────────┼──────────────────┐
▼ ▼ ▼
FULL-TEXT PFAD WORDS-FIRST PFAD CELL-FIRST PFAD
(pipeline= (grid_method= (grid_method=
'full_page') 'words_first') 'v2', default)
│ │ │
Keine Spalten/ Tesseract Full-Page Spaltenerkennung
Zeilen word_boxes detect_column_geometry()
analyze_layout_ _cluster_columns() _detect_sub_columns()
by_words() _cluster_rows() expand_narrow_columns()
│ _build_cells() Zeilenerkennung
│ │ detect_row_geometry()
│ build_grid_from_ │
│ words() build_cell_grid_v2()
│ │ │
│ │ ┌─────────┴──────────┐
│ │ ▼ ▼
│ │ Breite Spalten Schmale Spalten
│ │ (>= 15% Breite) (< 15% Breite)
│ │ Full-Page Words Cell-Crop OCR
│ │ word_lookup cell_crop_v2
│ │ │ │
└──────────────────┴────┴────────────────────┘
Post-Processing Pipeline
(Lautschrift, Komma-Split, etc.)
Schritt 8: Korrektur (Spell)
Schritt 9: Rekonstruktion
Schritt 10: Validierung
```
---
## Architektur
```
Admin-Lehrer (Next.js) klausur-service (FastAPI :8086)
┌────────────────────┐ ┌─────────────────────────────┐
│ /ai/ocr-pipeline │ │ /api/v1/ocr-pipeline/ │
│ │ REST │ │
│ PipelineStepper │◄────────►│ Sessions CRUD │
│ StepDeskew │ │ Image Serving │
│ StepDewarp │ SSE │ Deskew/Dewarp/Columns/Rows │
│ StepColumnDetection│◄────────►│ Word Recognition │
│ StepRowDetection │ │ Correction (Spell-Checker) │
│ StepWordRecognition│ │ Reconstruction │
│ StepLlmReview │ │ Ground Truth │
│ StepReconstruction │ └─────────────────────────────┘
│ StepGroundTruth │ │
└────────────────────┘ ▼
┌─────────────────────┐
│ PostgreSQL │
│ ocr_pipeline_sessions│
│ (Images + JSONB) │
└─────────────────────┘
```
### Dateistruktur
```
klausur-service/backend/
├── services/
│ └── cv_vocab_pipeline.py # Computer Vision + NLP Algorithmen
├── ocr_pipeline_api.py # FastAPI Router (Schritte 2-10)
├── orientation_crop_api.py # FastAPI Router (Schritte 1 + 4)
├── grid_editor_api.py # Grid Editor: build-grid, save-grid, grid-editor
├── grid_editor_helpers.py # Footer-Filterung, Seitenzahl-Extraktion
├── cv_ocr_engines.py # OCR-Engines, IPA-Korrektur, Britfone-Woerterbuch
├── cv_syllable_detect.py # Deutsche Silbentrennung (Silben:DE Modus)
├── cv_box_detect.py # Box-Erkennung + Zonen-Aufteilung
├── cv_graphic_detect.py # Grafik-/Bilderkennung (Region-basiert)
├── cv_color_detect.py # Farbtext-Erkennung (HSV-Analyse)
├── cv_words_first.py # Words-First Grid Builder (bottom-up)
├── cv_vocab_types.py # Datentypen: PageZone, ColumnGeometry, etc.
├── page_crop.py # Content-basierter Crop-Algorithmus
├── ocr_pipeline_session_store.py # PostgreSQL Persistence
├── layout_reconstruction_service.py # Fabric.js JSON + PDF/DOCX Export
├── tests/
│ └── test_grid_editor_api.py # 27 Tests fuer Grid Editor + IPA
└── migrations/
├── 002_ocr_pipeline_sessions.sql # Basis-Schema
├── 003_add_row_result.sql # Row-Result Spalte
└── 004_add_word_result.sql # Word-Result Spalte
admin-lehrer/
├── app/(admin)/ai/ocr-pipeline/
│ ├── page.tsx # Haupt-Page mit Session-Management
│ └── types.ts # TypeScript Interfaces
├── app/(admin)/ai/ocr-overlay/
│ ├── page.tsx # OCR Overlay: 3 Modi (Pipeline/Paddle/Kombi)
│ └── types.ts # OVERLAY_/PADDLE_DIRECT_/KOMBI_STEPS
├── components/ocr-overlay/
│ ├── PaddleDirectStep.tsx # Wiederverwendbar fuer Paddle Direct + Kombi
│ └── OverlayReconstruction.tsx # Overlay-Anzeige auf Bildhintergrund
└── components/ocr-pipeline/
├── PipelineStepper.tsx # Fortschritts-Stepper
├── StepOrientation.tsx # Schritt 1: Orientierung
├── StepDeskew.tsx # Schritt 2: Begradigung
├── StepDewarp.tsx # Schritt 3: Entzerrung
├── StepCrop.tsx # Schritt 4: Zuschneiden
├── StepColumnDetection.tsx # Schritt 5: Spaltenerkennung
├── StepRowDetection.tsx # Schritt 6: Zeilenerkennung
├── StepWordRecognition.tsx # Schritt 7: Worterkennung
├── StepStructureDetection.tsx # Schritt 8: Strukturerkennung
├── StepLlmReview.tsx # Schritt 9: Korrektur (SSE-Stream)
├── StepReconstruction.tsx # Schritt 9: Rekonstruktion (Canvas + Overlay)
├── usePixelWordPositions.ts # Shared Hook: Pixel-basierte Wortpositionierung
├── FabricReconstructionCanvas.tsx # Fabric.js Editor
└── StepGroundTruth.tsx # Schritt 10: Validierung
```
---
## API-Referenz
Alle Endpoints unter `/api/v1/ocr-pipeline/`.
### Sessions
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `POST` | `/sessions` | Neue Session erstellen (Bild hochladen) |
| `GET` | `/sessions` | Alle Sessions auflisten |
| `GET` | `/sessions/{id}` | Session-Info mit allen Step-Results |
| `PUT` | `/sessions/{id}` | Session umbenennen |
| `DELETE` | `/sessions/{id}` | Session loeschen |
| `POST` | `/sessions/{id}/detect-type` | Dokumenttyp erkennen |
### Bilder
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `GET` | `/sessions/{id}/image/original` | Originalbild |
| `GET` | `/sessions/{id}/image/oriented` | Orientiertes Bild |
| `GET` | `/sessions/{id}/image/deskewed` | Begradigtes Bild |
| `GET` | `/sessions/{id}/image/dewarped` | Entzerrtes Bild |
| `GET` | `/sessions/{id}/image/cropped` | Zugeschnittenes Bild |
| `GET` | `/sessions/{id}/image/binarized` | Binarisiertes Bild |
| `GET` | `/sessions/{id}/image/columns-overlay` | Spalten-Overlay |
| `GET` | `/sessions/{id}/image/rows-overlay` | Zeilen-Overlay |
| `GET` | `/sessions/{id}/image/words-overlay` | Wort-Grid-Overlay |
### Schritt 1: Orientierung
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `POST` | `/sessions/{id}/orientation` | 90/180/270° Drehung erkennen und korrigieren |
### Schritt 2: Begradigung
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `POST` | `/sessions/{id}/deskew` | Automatische Begradigung |
| `POST` | `/sessions/{id}/deskew/manual` | Manuelle Winkelkorrektur |
| `POST` | `/sessions/{id}/ground-truth/deskew` | Ground Truth speichern |
### Schritt 3: Entzerrung
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `POST` | `/sessions/{id}/dewarp` | Automatische Entzerrung |
| `POST` | `/sessions/{id}/dewarp/manual` | Manueller Scherbungswinkel |
| `POST` | `/sessions/{id}/adjust-combined` | Kombinierte Rotation + Shear Feinabstimmung |
| `POST` | `/sessions/{id}/ground-truth/dewarp` | Ground Truth speichern |
### Schritt 4: Zuschneiden
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `POST` | `/sessions/{id}/crop` | Automatischer Content-Crop |
| `POST` | `/sessions/{id}/crop/manual` | Manueller Crop (Prozent-Koordinaten) |
| `POST` | `/sessions/{id}/crop/skip` | Crop ueberspringen |
### Schritt 5: Spalten
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `POST` | `/sessions/{id}/columns` | Automatische Spaltenerkennung |
| `POST` | `/sessions/{id}/columns/manual` | Manuelle Spalten-Definition |
| `POST` | `/sessions/{id}/ground-truth/columns` | Ground Truth speichern |
### Schritt 6: Zeilen
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `POST` | `/sessions/{id}/rows` | Automatische Zeilenerkennung |
| `POST` | `/sessions/{id}/rows/manual` | Manuelle Zeilen-Definition |
| `POST` | `/sessions/{id}/ground-truth/rows` | Ground Truth speichern |
| `GET` | `/sessions/{id}/ground-truth/rows` | Ground Truth abrufen |
### Schritt 7: Worterkennung
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `POST` | `/sessions/{id}/words` | Wort-Grid erstellen |
| `POST` | `/sessions/{id}/ground-truth/words` | Ground Truth speichern |
| `GET` | `/sessions/{id}/ground-truth/words` | Ground Truth abrufen |
**Query-Parameter fuer `/sessions/{id}/words`:**
| Parameter | Default | Beschreibung |
|-----------|---------|--------------|
| `engine` | `auto` | OCR-Engine: `auto`, `tesseract`, `rapid`, `paddle` |
| `pronunciation` | `british` | IPA-Woerterbuch: `british` oder `american` |
| `stream` | `false` | SSE-Streaming (nur bei `grid_method=v2`) |
| `skip_heal_gaps` | `false` | Zeilen-Luecken nicht heilen (Overlay-Modus) |
| `grid_method` | `v2` | Grid-Strategie: `v2` (top-down) oder `words_first` (bottom-up) |
### Schritt 8: Strukturerkennung
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `POST` | `/sessions/{id}/detect-structure` | Boxen, Zonen, Farben und Grafiken erkennen |
| `GET` | `/sessions/{id}/image/structure-overlay` | Overlay mit allen Strukturelementen |
### Schritt 9: Korrektur
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `POST` | `/sessions/{id}/llm-review?stream=true` | SSE-Stream Korrektur starten |
| `POST` | `/sessions/{id}/llm-review/apply` | Ausgewaehlte Korrekturen speichern |
### Schritt 10: Rekonstruktion
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `POST` | `/sessions/{id}/reconstruction` | Zellaenderungen speichern |
| `GET` | `/sessions/{id}/reconstruction/fabric-json` | Fabric.js Canvas-Daten |
| `GET` | `/sessions/{id}/reconstruction/export/pdf` | PDF-Export (reportlab) |
| `GET` | `/sessions/{id}/reconstruction/export/docx` | DOCX-Export (python-docx) |
| `POST` | `/sessions/{id}/reconstruction/detect-images` | Bildbereiche per VLM erkennen |
| `POST` | `/sessions/{id}/reconstruction/generate-image` | Bild per mflux generieren |
| `POST` | `/sessions/{id}/reconstruction/validate` | Validierung speichern (Step 10) |
| `GET` | `/sessions/{id}/reconstruction/validation` | Validierungsdaten abrufen |
### Grid Editor (Excel-aehnlich)
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `POST` | `/sessions/{id}/build-grid` | Strukturiertes Grid aus Kombi-Wortdaten erstellen |
| `POST` | `/sessions/{id}/save-grid` | Bearbeitetes Grid speichern |
| `GET` | `/sessions/{id}/grid-editor` | Grid-Editor-Daten abrufen |
---
## Schritt 4: Zuschneiden/Crop (Detail)
### Warum Crop nach Deskew/Dewarp?
In frueheren Versionen lief Crop als Schritt 2 (vor Deskew). Das fuehrte zu Problemen:
- **Schiefes Bild**: `boundingRect` einer schiefen Seite schliesst viel Scanner-Hintergrund ein
- **Buchscans**: Otsu-Binarisierung versagt bei weiss-auf-weiss (Seite auf weissem Scanner)
- **Buchruecken**: Gradueller Schatten-Uebergang wird nicht als Kante erkannt
**Loesung (v4.1):** Crop laeuft jetzt nach Dewarp — das Bild ist dann gerade.
### Algorithmus: Content-basierte 4-Kanten-Erkennung
Datei: `page_crop.py`
```
Input: Entzerrtes BGR-Bild
├─ Adaptive Threshold (Gauss, blockSize=51)
│ → binary (Text=255, Hintergrund=0)
├─ Linker Rand (Buchruecken-Schatten):
│ 1. Grauwert-Spaltenmittel in linken 25%
│ 2. Glaetten mit Boxcar-Kernel
│ 3. Transition hell→dunkel finden (> 60% des Helligkeitsbereichs)
│ 4. Fallback: Binaere Vertikal-Projektion
├─ Rechter Rand: Binaere Vertikal-Projektion (letzte Ink-Spalte)
├─ Oben/Unten: Binaere Horizontal-Projektion (erste/letzte Ink-Zeile)
├─ Rausch-Filter: Runs < 0.5% der Dimension ignorieren
├─ Sanity-Checks:
│ - Mindestens eine Kante > 2% Border
│ - Crop-Flaeche >= 40% des Originals
└─ Crop + konfigurierbarer Rand (default 1%)
```
### Vergleich alt vs. neu
| Eigenschaft | Alt (Otsu + Kontur) | Neu (Content-basiert) |
|-------------|--------------------|-----------------------|
| Binarisierung | Otsu (global) | Adaptive Threshold |
| Methode | Groesste Kontur → boundingRect | 4-Kanten Ink-Projektion |
| Buchruecken | Nicht erkannt | Schatten-Gradient-Erkennung |
| Weiss-auf-weiss | Versagt | Funktioniert (adaptive) |
| Format-Matching | A4/Letter erzwungen | Kein Format-Matching (Content-Bounds) |
| Position in Pipeline | Vor Deskew (Schritt 2) | Nach Dewarp (Schritt 4) |
---
## Schritt 3: Entzerrung/Dewarp (Detail)
### Algorithmus: Vertikalkanten-Drift
Die Dewarp-Erkennung misst die **vertikale Spaltenkippung** (dx/dy) statt Textzeilen-Neigung:
1. Woerter werden nach X-Position in vertikale Spaltencluster gruppiert
2. Pro Cluster: Lineare Regression `x = a*y + b``a = dx/dy = tan(shear_angle)`
3. Ensemble aus drei Methoden: Textzeilen (1.5× Gewicht), Projektionsprofil (2-Pass), Vertikalkanten
4. Qualitaetspruefung: Horizontale Projektionsvarianz vor/nach Korrektur
**Schwellenwerte:**
| Parameter | Wert | Beschreibung |
|-----------|------|--------------|
| Min. Korrekturwinkel | 0.08° | Unter 0.08° wird nicht korrigiert |
| Ensemble Min-Confidence | 0.35 | Mindest-Konfidenz fuer Korrektur |
| Quality-Gate Skip | < 0.5° | Kleine Korrekturen ueberspringen Quality-Gate |
### Feinabstimmung (Combined Adjust)
Der Endpoint `POST /sessions/{id}/adjust-combined` erlaubt die kombinierte Feinabstimmung von
Rotation und Shear in einem Schritt. Im Frontend stehen **7 Schieberegler** zur Verfuegung:
**Rotation (3 Paesse):**
| Slider | Bereich | Beschreibung |
|--------|---------|--------------|
| P1 Iterative | ±5° | Erster Deskew-Pass (Hough Lines) |
| P2 Word-Alignment | ±3° | Zweiter Pass (Wort-Ausrichtung) |
| P3 Textline | ±3° | Dritter Pass (Textzeilen-Regression) |
Die Summe aller drei ergibt den finalen Rotationswinkel.
**Shear (4 Methoden, Radio-Auswahl):**
| Slider | Bereich | Beschreibung |
|--------|---------|--------------|
| A: Textline Drift | ±5° | Textzeilen-Drift |
| B: Projection Profile | ±5° | 2-Pass Projektionsprofil |
| C: Vertical Edges | ±5° | Vertikalkanten-Analyse |
| D: Ensemble | ±5° | Gewichteter Ensemble-Wert |
Nur der per Radio-Button ausgewaehlte Shear-Wert wird verwendet.
```
POST /sessions/{id}/adjust-combined
Body: {"rotation_degrees": 1.23, "shear_degrees": -0.45}
Response: {"method_used": "manual_combined", "shear_degrees": -0.45, "dewarped_image_url": "..."}
```
---
## Schritt 5: Spaltenerkennung (Detail)
### Algorithmus: `detect_column_geometry()`
Mehrstufige Erkennung: Seite segmentieren, vertikale Projektionsprofile finden Luecken, Wort-Bounding-Boxes validieren.
```
Bild → Binarisierung → Seiten-Segmentierung → Vertikalprofil → Lueckenerkennung → Wort-Validierung → ColumnGeometry
```
**Wichtige Implementierungsdetails:**
- **Initialer Tesseract-Scan:** Laeuft auf der vollen Bildbreite `[left_x : w]` (nicht nur bis zur Content-Grenze `right_x`), damit Woerter am rechten Rand der letzten Spalte nicht uebersehen werden.
- **Letzte Spalte:** Wird immer bis zur vollen Bildbreite `w` ausgedehnt, nicht nur bis zur erkannten Content-Grenze.
- **Phantom-Spalten-Filter (Step 9):** Spalten mit Breite < 3 % der Content-Breite UND < 3 Woerter werden als Artefakte entfernt; die angrenzenden Spalten schliessen die Luecke.
- **Spaltenzuweisung:** Woerter werden anhand des groessten horizontalen Ueberlappungsbereichs einer Spalte zugeordnet.
### Seiten-Segmentierung an Sub-Headern
Farbige Zwischenueberschriften (z.B. „Unit 4: Bonnie Scotland" mit blauem Hintergrund)
erzeugen nach Binarisierung Tinte ueber die gesamte Seitenbreite. Diese Baender fuellen
Spaltenluecken im vertikalen Projektionsprofil auf und fuehren zu fragmentierten Spalten
(z.B. 11 statt 5).
**Loesung: Horizontale Gap-Segmentierung (Step 2b)**
1. **Horizontales Projektionsprofil** berechnen: Zeilensummen ueber den Content-Bereich
2. **Leere Zeilen** erkennen: Zeilen mit < 2% Tinten-Dichte (`H_GAP_THRESH = 0.02`)
3. **Gaps sammeln**: Zusammenhaengende leere Zeilen zu Gaps buendeln (Mindestlaenge: `max(5, h/200)`)
4. **Grosse Gaps identifizieren**: Gaps > 1.8× Median-Gap-Hoehe = Sub-Header-Trennungen
5. **Segmente bilden**: Seite an grossen Gaps aufteilen
6. **Groesstes Segment waehlen**: Das hoechste Segment wird fuer die vertikale Projektion verwendet
```
┌─────────────────────────────────┐
│ Header / Titel │ ─── grosser Gap ───
├─────────────────────────────────┤
│ EN │ DE │ Example │ Page │ ← Segment 1 (groesster)
│ ... │ ... │ ... │ ... │
├─────────────────────────────────┤
│ Unit 4: Bonnie Scotland │ ─── grosser Gap ───
├─────────────────────────────────┤
│ EN │ DE │ Example │ Page │ ← Segment 2
│ ... │ ... │ ... │ ... │
└─────────────────────────────────┘
```
**Segment-gefilterte Wort-Validierung:**
Die Wort-Validierung (Step 5) nutzt nur Tesseract-Woerter **innerhalb des gewaehlten Segments**.
Woerter aus Sub-Header-Bereichen (die die volle Breite einnehmen) werden so ausgeschlossen
und koennen die Spaltenluecken-Validierung nicht verfaelschen.
### Word-Coverage Gap Detection (Fallback)
Wenn die pixel-basierte Projektion keine ausreichenden Spaltenluecken findet
(z.B. bei Seiten mit Illustrationen, die Spaltenluecken teilweise verdecken),
greift ein Fallback auf Basis der Tesseract-Wort-Bounding-Boxes:
1. X-Achse in 2px-Bins aufteilen
2. Pro Bin zaehlen, wie viele Segment-Woerter ihn ueberdecken
3. Zusammenhaengende Bins mit 0 Woertern = Gap-Kandidaten
4. Nur Gaps im inneren 90%-Bereich beruecksichtigen (Raender ignorieren)
5. Gaps mit Mindestbreite (`max(8px, content_w * 0.5%)`) werden als Spaltenluecken akzeptiert
### Sub-Spalten-Erkennung: `_detect_sub_columns()`
Erkennt versteckte Sub-Spalten innerhalb breiter Spalten (z.B. Seitenzahl-Spalte links neben EN-Vokabeln).
**Algorithmus (Left-Edge Alignment Clustering):**
1. Fuer jede Spalte mit `width_ratio >= 0.15` und `word_count >= 5`:
2. Left-Edges aller Woerter mit `conf >= 30` sammeln
3. In Alignment-Bins clustern (8px Toleranz)
4. Linkester Bin mit >= 10% der Woerter = wahrer Spaltenanfang
5. Woerter links davon = Sub-Spalte, wenn >= 2 und < 35% Anteil
6. Neue ColumnGeometry-Objekte mit korrekten Indizes erzeugen
**Koordinatensystem:** Word `left`-Werte sind relativ zum Content-ROI (`left_x`), `ColumnGeometry.x` ist absolut. `left_x` wird als Parameter durchgereicht.
### Spalten-Erweiterung: `expand_narrow_columns()`
Laeuft **nach** `_detect_sub_columns()`. Erweitert sehr schmale Spalten (< 10% Content-Breite,
z.B. `page_ref`, `marker`) in den Weissraum zum Nachbar-Spalte hinein, aber nie ueber die
naechsten Woerter im Nachbarn hinaus (4px Sicherheitsabstand).
### Spaltentyp-Klassifikation: `classify_column_types()`
| Spaltentyp | Beschreibung | Erkennung |
|------------|--------------|-----------|
| `column_en` | Englische Vokabeln | EN-Funktionswoerter (the, a, is...) |
| `column_de` | Deutsche Uebersetzung | DE-Funktionswoerter (der, die, das...) |
| `column_example` | Beispielsaetze | Abkuerzungen, Grammatik-Marker |
| `page_ref` | Seitenzahlen | Schmal (< 20% Breite), wenige Woerter |
| `column_marker` | Dekorative Markierungen | Sehr schmal, spezielle Zeichen |
| `column_text` | Generischer Text | Fallback |
### Konfigurierbare Parameter
```python
# Mindestbreite fuer echte Spalten (automatisch: max(20px, 3% content_w))
min_real_col_w = max(20, int(content_w * 0.03))
```
---
## Schritt 6: Zeilenerkennung (Detail)
### Algorithmus: `detect_row_geometry()`
Horizontale Projektionsprofile finden Zeilen-Luecken; word-level Validierung verhindert Fehlschnitte.
**Zusaetzliche Post-Processing-Schritte:**
1. **Artefakt-Zeilen entfernen** (`_is_artifact_row`):
Zeilen, in denen alle erkannten Tokens nur 1 Zeichen lang sind (Scan-Schatten, leere Zeilen),
werden als Artefakte klassifiziert und aus dem Grid entfernt.
2. **Luecken-Heilung** (`_heal_row_gaps`):
Nach dem Entfernen leerer/Artefakt-Zeilen werden die verbleibenden Zeilen auf die Mitte
der entstehenden Luecke ausgedehnt, damit kein Zeileninhalt durch schrumpfende Grenzen
abgeschnitten wird.
3. **Box-Boundary-Schutz** (`box_ranges_inner`, neu in v4.2):
Bei Seiten mit Box-Zonen (Sub-Sessions) werden Zeilen am Box-Rand nicht faelschlich
ausgeschlossen. Das Problem: Die letzte Textzeile ueber einer Box ueberlappt haeufig
mit dem Box-Rahmen. Loesung: Die Exclusion-Zone wird um `max(border_thickness, 5px)`
geschrumpft, sodass nur Zeilen **innerhalb** der Box ausgeschlossen werden.
```python
def _is_artifact_row(row: RowGeometry) -> bool:
"""Zeile ist Artefakt wenn alle Tokens <= 1 Zeichen."""
if row.word_count == 0: return True
return all(len(w.get('text','').strip()) <= 1 for w in row.words)
def _heal_row_gaps(rows, top_bound, bottom_bound):
"""Verbleibende Zeilen auf Mitte der Luecken ausdehnen."""
...
```
### Box-Zonen und Content-Strips (Detail)
Seiten mit Box-Bereichen (z.B. Grammatik-Tipps, Uebungsboxen) werden in Zonen aufgeteilt:
```
┌──────────────────────────┐
│ Content Zone 0 (Zeilen) │ ← Vokabeltabelle oben
├──────────────────────────┤
│ ███ Box Zone (border) ███│ ← Sub-Session mit eigener OCR
├──────────────────────────┤
│ Content Zone 2 (Zeilen) │ ← Vokabeltabelle unten
└──────────────────────────┘
```
**Content-Strip-Verfahren** (`detect_rows` in `ocr_pipeline_api.py`):
1. Box-Zonen identifizieren, `box_ranges_inner` berechnen (geschrumpft um Border-Dicke)
2. Content-Strips = Seitenbereiche **ohne** Box-Inneres, vertikal gestapelt
3. Zeilenerkennung auf gestapeltem Bild, Y-Koordinaten zurueckgemappt
4. Wort-Filterung: Woerter in Box-Innerem werden ausgeschlossen
**Wichtig:** `box_ranges_inner` (nicht `box_ranges`) wird verwendet, damit
Zeilen am Box-Rand nicht abgeschnitten werden. Minimum 5px Margin.
---
## Schritt 7: Worterkennung (Detail)
Schritt 7 bietet zwei Grid-Strategien, auswaehlbar per `grid_method`-Parameter:
| Strategie | Parameter | Ansatz | Benoetigt Spalten/Zeilen? |
|-----------|-----------|--------|--------------------------|
| **Hybrid-Grid v2** | `grid_method=v2` (Default) | Top-down: Spalten → Zeilen → Zellen → OCR | Ja (Schritte 5+6) |
| **Words-First** | `grid_method=words_first` | Bottom-up: Woerter → Spalten clustern → Zeilen clustern → Zellen | Nein |
---
### Words-First Grid Builder: `build_grid_from_words()`
**Datei:** `cv_words_first.py`
Der Words-First Builder arbeitet bottom-up: Er nimmt die pixelgenauen `word_boxes` aus einem
Tesseract Full-Page-Lauf und clustert sie direkt zu Spalten und Zeilen — ohne die
vorherige Spalten-/Zeilenerkennung (Schritte 5+6) zu benoetigen.
#### Algorithmus
```
Eingabe: word_dicts (flat list), img_w, img_h
┌───────────┴───────────┐
│ 1. Confidence-Filter │
│ conf >= 30 │
│ Whitespace entf. │
└───────────┬───────────┘
┌───────────┴───────────┐
│ 2. _cluster_columns() │
│ X-Gap-Analyse │
│ Schwelle: median_h │
× 3 (min 3% Breite)│
└───────────┬───────────┘
┌───────────┴───────────┐
│ 3. _cluster_rows() │
│ Y-Proximity-Grupp. │
│ Toleranz: median_h │
│ / 2 │
└───────────┬───────────┘
┌───────────┴───────────┐
│ 4. _build_cells() │
│ Wort → (col, row) │
│ Text + bbox + conf │
│ word_boxes pro Zelle│
└───────────┬───────────┘
Ausgabe: cells[], columns_meta[]
(identisch zu build_cell_grid_v2)
```
#### Spalten-Clustering
1. Alle Woerter nach X-Mitte sortieren
2. Aufeinanderfolgende X-Gaps berechnen
3. Adaptiver Schwellwert: `median_word_height × 3` (min 3% Bildbreite)
4. Gaps > Schwellwert = Spaltengrenzen
5. Kein Gap gefunden → 1 Spalte (`column_text`)
6. Spaltentypen: `column_1`, `column_2`, ... (generisch, positionsbasiert)
#### Zeilen-Clustering
1. Woerter zu visuellen Zeilen gruppieren (Y-Toleranz: halbe Worthoehe)
2. Jede visuelle Zeile = eine Zeile im Grid
3. Sortiert von oben nach unten
#### Edge Cases
| Fall | Behandlung |
|------|------------|
| Einzelne Spalte (Fliesstext) | Kein X-Gap → 1 Spalte `column_text` |
| Keine Woerter erkannt | Leeres Ergebnis `([], [])` |
| Ueberschriften (grosse Schrift) | Eigene Zeile durch Y-Gap |
| Bilder/Grafiken | Keine Woerter → automatisch leerer Bereich |
| Schmale Spalten (Seitenzahlen) | Eigene Spalte durch X-Gap |
#### Vergleich v2 vs. Words-First
| Kriterium | v2 (Top-Down) | Words-First (Bottom-Up) |
|-----------|---------------|------------------------|
| **Abhaengigkeiten** | Spalten + Zeilen noetig | Nur Tesseract-Woerter |
| **Spaltentypen** | Semantisch (EN, DE, ...) | Positionsbasiert (1, 2, ...) |
| **OCR** | Hybrid (full-page + cell-crop) | Nur full-page Tesseract |
| **Robustheit** | Abhaengig von Spalten-/Zeilenerkennung | Direkt aus Wortpositionen |
| **Geschwindigkeit** | Langsamer (cell-crop pro Zelle) | Schneller (kein OCR-Lauf) |
| **Genauigkeit** | Besser bei schmalen Spalten | Besser bei ungewoehnlichen Layouts |
---
### Hybrid-Grid v2: `build_cell_grid_v2()`
Schritt 7 nutzt im Default eine **Hybrid-Strategie**: Breite Spalten verwenden die Full-Page-Tesseract-Woerter,
schmale Spalten werden isoliert per Cell-Crop OCR verarbeitet.
!!! success "Warum Hybrid?"
Full-Page OCR liefert gute Ergebnisse fuer breite Spalten (Saetze, IPA-Klammern, Interpunktion).
Aber bei schmalen Spalten (Seitenzahlen, Marker) „bluten" Woerter aus Nachbar-Spalten ein.
Cell-Crop isoliert jede Zelle und verhindert dieses Bleeding.
### Broad vs. Narrow — Die 15%-Schwelle
```python
_NARROW_COL_THRESHOLD_PCT = 15.0 # cv_vocab_pipeline.py
```
| Eigenschaft | Breite Spalten (>= 15%) | Schmale Spalten (< 15%) |
|-------------|------------------------|------------------------|
| **OCR-Quelle** | Full-Page Tesseract (vorher gelaufen) | Isolierter Cell-Crop |
| **Wort-Zuweisung** | `_assign_row_words_to_columns()` | Direktes Zell-OCR |
| **Confidence-Filter** | `conf >= 30` | `conf >= 30` |
| **Text-Bereinigung** | `_clean_cell_text()` (mittel) | `_clean_cell_text_lite()` (aggressiv) |
| **Neighbour-Bleeding** | Risiko vorhanden | Verhindert (isoliert) |
| **Parallelisierung** | Sequentiell | Parallel (`max_workers=4`) |
| **OCR-Engine Label** | `word_lookup` | `cell_crop_v2` |
| **Typische Spalten** | EN-Vokabeln, DE-Uebersetzung, Beispielsaetze | Seitenzahlen, Marker |
**Empirische Grundlage:** Typische breite Spalten liegen bei 2040% Bildbreite,
typische schmale bei 312%. Die 15%-Grenze trennt diese Gruppen sauber.
!!! note "Offener Punkt: Schwellen-Validierung"
Die 15%-Schwelle wurde an Vokabeltabellen mit 35 Spalten validiert.
Fuer eine breitere Validierung werden diverse Schulbuchseiten mit unterschiedlichen
Layouts (2-, 3-, 4-, 5-spaltig, verschiedene Verlage) benoetigt. Aktuell gibt es
in der Datenbank nur Sessions mit demselben Arbeitsblatt-Typ.
### Cell-Crop OCR: `_ocr_cell_crop()`
Isolierte OCR einer einzelnen Zelle (Spalte × Zeile Schnittflaeche):
1. **Crop:** Exakte Spalten- × Zeilengrenzen mit 3px internem Padding
2. **Density-Check:** Ueberspringe leere Zellen (`dark_ratio < 0.005`)
3. **Upscaling:** Kleine Crops (Hoehe < 80px) werden 3× vergroessert
4. **OCR:** Engine-spezifisch (Tesseract, TrOCR, RapidOCR, LightON, PaddleOCR)
5. **Fallback:** Bei leerem Ergebnis → PSM 7 (Einzelzeile) statt PSM 6
6. **Bereinigung:** `_clean_cell_text_lite()` (aggressives Noise-Filtering)
### PaddleOCR Remote-Engine (`engine=paddle`)
PaddleOCR (PP-OCRv5 Latin) laeuft als eigenstaendiger Microservice auf einem Hetzner x86_64 Server,
da PaddlePaddle nicht auf ARM64 (Mac Mini) laeuft.
```
Mac Mini (klausur-service) Hetzner (paddleocr-service)
│ HTTPS POST + Bild │
│ ──────────────────────────▶ │ PP-OCRv5 Latin
│ │ FastAPI (Port 8095)
│ JSON word_boxes │ API-Key Auth
│ ◀────────────────────────── │
```
**Besonderheiten:**
- Erzwingt automatisch `grid_method=words_first` (full-page OCR, kein cell-crop)
- Async HTTP-Client (`paddleocr_remote.py`) mit 30s Timeout
- Koordinaten sind bereits absolut (kein content_bounds Offset noetig)
- API-Key Authentifizierung ueber `X-API-Key` Header
- Dateien: `paddleocr-service/main.py`, `services/paddleocr_remote.py`, `cv_ocr_engines.py:ocr_region_paddle()`
### Ablauf von `build_cell_grid_v2()`
```
Eingabe: ocr_img, column_regions, row_geometries
┌───────────┴───────────┐
│ Filter │
│ • Phantom-Zeilen │
│ • Artefakt-Zeilen │
│ • Irrelevante Spalten │
│ (header, footer, │
│ margin, ignore) │
└───────────┬───────────┘
┌───────────┴───────────┐
│ Klassifizierung │
│ Spalte.width / img_w │
│ >= 15% → broad │
│ < 15% → narrow │
└───────────┬───────────┘
┌───────────┴────────────────┐
│ │
Phase 1: Broad Phase 2: Narrow
(sequentiell) (parallel, max_workers=4)
│ │
Pro (row, col): Pro (row, col):
1. Words aus Full-Page 1. _ocr_cell_crop()
2. Filter conf >= 30 2. Isoliertes Zell-Bild
3. _words_to_reading_order 3. Upscale wenn noetig
4. _clean_cell_text() 4. _clean_cell_text_lite()
│ │
└───────────┬────────────────┘
Merge + Sortierung
(row_index, col_index)
Leere Zeilen entfernen
Ausgabe: cells[], columns_meta[]
```
### Post-Processing Pipeline (in `build_vocab_pipeline_streaming`)
| # | Schritt | Funktion | Beschreibung |
|---|---------|----------|--------------|
| 0a | Lautschrift-Fortsetzung | `_merge_phonetic_continuation_rows` | IPA-only Folgezeilen zusammenfuehren |
| 0b | Zeilen-Fortsetzung | `_merge_continuation_rows` | Zeilen mit Kleinbuchstaben-Anfang zusammenfuehren |
| 2 | Lautschrift-Fix | `_fix_phonetic_brackets` | OCR-Lautschrift mit Woerterbuch-IPA ersetzen |
| 3 | Komma-Split | `_split_comma_entries` | `break, broke, broken` → 3 Eintraege |
| 4 | Beispielsaetze | `_attach_example_sentences` | Beispielsatz-Zeilen an vorangehenden Eintrag haengen |
!!! info "Zeichenkorrektur in Schritt 6"
Die Zeichenverwirrungskorrektur (`|``I`, `1``I`, `8``B`) laeuft **nicht** in
Schritt 5, sondern als erstes in Schritt 6 (Korrektur), damit die Aenderungen im UI
sichtbar und rueckgaengig machbar sind.
---
## Schritt 8: Korrektur (Detail)
### Korrektur-Engine
Schritt 6 kombiniert drei Korrektur-Stufen, alle als SSE-Stream:
**Stufe 1 — Zeichenverwirrungskorrektur** (`_fix_character_confusion`):
| OCR-Fehler | Korrektur | Regel |
|------------|-----------|-------|
| `\|ch` | `Ich` | `\|` am Wortanfang vor Kleinbuchstaben → `I` |
| `\| want` | `I want` | Alleinstehendes `\|``I` |
| `8en` | `Ben` | `8` am Wortanfang vor `en``B` |
| `1 want` | `I want` | Alleinstehendes `1``I` (NICHT vor `.` oder `,`) |
| `1. Kreuz` | unveraendert | `1.` = Listennummer, wird **nicht** korrigiert |
**Stufe 2 — Regel-basierte Rechtschreibkorrektur** (`spell_review_entries_streaming`):
Nutzt `pyspellchecker` (MIT-Lizenz) mit EN+DE-Woerterbuch. Pro Token mit verdaechtigem Zeichen
(`0`, `1`, `5`, `6`, `8`, `|`) werden Kandidaten geprueft:
```python
_SPELL_SUBS = {
'0': ['O', 'o'], '1': ['l', 'I'], '5': ['S', 's'],
'6': ['G', 'g'], '8': ['B', 'b'], '|': ['I', 'l', '1'],
}
```
**Stufe 3 — Seitenzahl-Korrektur** (`page_ref`-Felder):
Korrigiert haeufige OCR-Fehler in Seitenverweisen (z.B. `p.5g``p.59`).
### Umgebungsvariablen
| Variable | Default | Beschreibung |
|----------|---------|--------------|
| `REVIEW_ENGINE` | `spell` | Korrektur-Engine: `spell` oder `llm` |
| `OLLAMA_REVIEW_MODEL` | `qwen3:0.6b` | Ollama-Modell (nur wenn `REVIEW_ENGINE=llm`) |
| `OLLAMA_REVIEW_BATCH_SIZE` | `20` | Eintraege pro LLM-Aufruf |
### SSE-Protokoll
```
POST /sessions/{id}/llm-review?stream=true
Events:
data: {"type": "meta", "total_entries": 96, "to_review": 80, "skipped": 16, "model": "spell"}
data: {"type": "batch", "changes": [...], "entries_reviewed": [0,1,2,...], "progress": {...}}
data: {"type": "complete", "duration_ms": 234}
data: {"type": "error", "detail": "..."}
Change-Format:
{"row_index": 5, "field": "english", "old": "| want", "new": "I want"}
```
---
## Schritt 8: Strukturerkennung (Detail)
Erkennt Boxen, Zonen, Farbregionen und grafische Elemente auf der Seite.
Laeuft **nach** der Worterkennung (Schritt 7), damit OCR-Wortpositionen
fuer die Unterscheidung von Text vs. Grafik zur Verfuegung stehen.
### Teilschritte
1. **Box-Erkennung** (`cv_box_detect.py`): Linien-Rahmen und farbige Hintergruende
2. **Zonen-Aufteilung** (`split_page_into_zones`): Seite in Box- und Content-Zonen aufteilen
3. **Farb-Analyse** (`cv_color_detect.py`): HSV-basierte Erkennung farbiger Textbereiche
4. **Grafik-Erkennung** (`cv_graphic_detect.py`): Nicht-Text-Grafiken identifizieren
### Grafik-Erkennung: Region-basierter Ansatz
Zwei Paesse trennen farbige Grafiken von farbigem Text und erkennen
schwarze Illustrationen:
**Pass 1 — Farbige Bildregionen:**
1. HSV-Saturation-Kanal extrahieren (Schwelle > 40)
- Schwarzer Text hat Saettigung ≈ 0 → unsichtbar auf diesem Kanal
2. Starke Dilation (25×25 Ellipse) verschmilzt nahe Farbpixel zu Regionen
3. Fuer jede Region: Wort-Ueberlappung pruefen
- \> 50 % Ueberlappung mit OCR-Woertern → farbiger Text → ueberspringen
- ≤ 50 % → farbige Grafik/Bild → behalten
4. Minimum 200 Farbpixel erforderlich (kein Rauschen)
5. Regionen > 50 % der Bildbreite oder -hoehe → Seitenumfassend → ueberspringen
**Pass 2 — Schwarze Illustrationen:**
1. Otsu-Binarisierung fuer Tinten-Maske
2. Ausschlusszonen: OCR-Woerter (5 px Padding) + erkannte Boxen (8 px Inset)
3. Farbige Pixel aus Pass 1 ebenfalls ausschliessen
4. Nur Konturen mit Flaeche > 5000 px und min(Breite, Hoehe) > 40 px
**Deduplizierung:** Ueberlappende Elemente (> 50 % IoU der kleineren
Bounding-Box) werden zusammengefasst. Ergebnis nach Flaeche absteigend
sortiert.
### Response-Format
```json
{
"boxes": [
{"x": 50, "y": 300, "w": 1100, "h": 200, "confidence": 0.85,
"border_thickness": 3, "bg_color_name": "blue", "bg_color_hex": "#2563eb"}
],
"zones": [
{"index": 0, "zone_type": "content", "x": 50, "y": 50, "w": 1100, "h": 250},
{"index": 1, "zone_type": "box", "x": 50, "y": 300, "w": 1100, "h": 200}
],
"graphics": [
{"x": 100, "y": 500, "w": 150, "h": 120, "area": 8500,
"shape": "image", "color_name": "red", "color_hex": "#dc2626",
"confidence": 0.72}
],
"color_pixel_counts": {"red": 1234, "blue": 5678},
"has_words": true,
"word_count": 96,
"duration_seconds": 0.45
}
```
### Grafik-Shape-Typen
| Shape | Quelle | Beschreibung |
|-------|--------|--------------|
| `image` | Pass 1 | Farbige Grafik/Bild (Ballons, Pfeile, Icons) |
| `illustration` | Pass 2 | Grosse schwarze Zeichnung/Illustration |
### Erkannte Farben
`red`, `orange`, `yellow`, `green`, `blue`, `purple`, `black`
— basierend auf dem Median-Hue der saturierten Pixel in der Region.
### Frontend-Anzeige
`StepStructureDetection.tsx` zeigt:
- Boxen-Liste mit Position, Hintergrundfarbe und Confidence
- Zonen-Uebersicht (Content vs. Box)
- Farb-Zusammenfassung (Pixel-Counts)
- Grafik-Liste mit Shape, Abmessungen, Farbe und Confidence
---
## Schritt 9: Rekonstruktion (Detail)
Drei Modi verfuegbar:
### Einfacher Modus
Das entzerrte Originalbild wird mit 30 % Opazitaet als Hintergrund
angezeigt, alle Grid-Zellen (auch leere!) werden als editierbare Textfelder darueber gelegt.
**Features:**
- Alle Zellen editierbar — auch leere Zellen (kein Filter mehr)
- Farbkodierung nach Spaltentyp (Blau=EN, Gruen=DE, Orange=Beispiel)
- Leere Pflichtfelder (EN/DE) rot gestrichelt markiert
- Undo/Redo (Ctrl+Z / Ctrl+Shift+Z)
- Tab-Navigation durch alle Zellen (inkl. leerer)
- Zoom 50200 %
- Per-Zell-Reset-Button bei geaenderten Zellen
### Overlay-Modus (neu in v4.2)
Ganzseitige Tabellenrekonstruktion mit **Pixel-basierter Wortpositionierung**.
Nur verfuegbar bei Parent-Sessions mit Sub-Sessions (Box-Bereiche).
**Funktionsweise:**
1. **Sub-Session-Merging:** Zellen aus Sub-Sessions werden koordinaten-konvertiert
und in die Parent-Session eingefuegt. Die Umrechnung laeuft ueber die Box-Zone:
```
parentCellX = boxXPct + (subCell.bbox_pct.x / 100) * boxWPct
parentCellY = boxYPct + (subCell.bbox_pct.y / 100) * boxHPct
```
2. **180°-Rotation:** Bei Parent-Sessions mit Boxen wird das Bild standardmaessig
180° gedreht, da der Scan haeufig kopfueber vorliegt. Die Pixel-Analyse
arbeitet auf dem rotierten Bild:
- Canvas: `ctx.translate(W, H); ctx.rotate(Math.PI)`
- Zell-Koordinaten: `(100 - x - w, 100 - y - h)` fuer rotiertes Space
- Cluster-Ruecktransformation: `start → cw-1-end`, danach `reverse()`
3. **Pixel-Wortpositionierung:** Der `usePixelWordPositions` Hook analysiert
dunkle Pixel per vertikaler Projektion, findet Wortgruppen-Cluster und
berechnet die exakte horizontale Position + Auto-Schriftgroesse.
**Layout:** 50/50 Grid (links Originalbild, rechts Rekonstruktion)
**Toolbar:**
- Schriftgroessen-Slider (30120%)
- Bold-Toggle
- 180°-Rotations-Toggle
- Speichern-Button
**Visuelle Elemente:**
- Spaltenlinien (aus `column_result.columns`)
- Zeilenlinien (aus `row_result.rows`)
- Box-Zonen-Markierung (blau, halbtransparent)
- Editierbare Inputs an Pixel-Positionen
### Shared Hook: `usePixelWordPositions`
Extrahierter Hook fuer die Pixel-basierte Wortpositionierung, genutzt in
StepLlmReview (Schritt 8) und StepReconstruction (Schritt 9).
```typescript
function usePixelWordPositions(
imageUrl: string,
cells: GridCell[],
active: boolean,
rotation: 0 | 180 = 0,
): Map<string, WordPosition[]>
```
**Algorithmus:**
1. Bild in offscreen Canvas laden (optional 180° gedreht)
2. Pro Zelle: `getImageData()` → vertikale Projektion (dunkle Pixel pro Spalte)
3. Cluster-Erkennung (Schwelle: 3% der Zellhoehe, Gap: 2% der Zellbreite)
4. Bei Rotation: Cluster zurueck ins Original-Koordinatensystem spiegeln
5. Text-Gruppen (split bei 3+ Leerzeichen) auf Cluster matchen
6. Auto-Schriftgroesse per `measureText()` + `fontRatio`
7. Mode-Normalisierung: Haeufigste `fontRatio` (gerundet auf 0.02) auf alle anwenden
**Rueckgabe:** `Map<cell_id, WordPosition[]>` mit `xPct`, `wPct`, `yPct`, `hPct`, `text`, `fontRatio`
!!! note "Per-Word Y-Positionierung (v4.3.1)"
`WordPosition` enthaelt seit v4.3.1 auch `yPct` und `hPct`. Dadurch rendert jedes
Wort an seiner tatsaechlichen vertikalen Position, statt alle Woerter einer Zelle
auf der Zell-Mitte zu stapeln. Bei Zellen ohne `word_boxes` (Fallback) werden
`yPct`/`hPct` aus `cell.bbox_pct` uebernommen.
### Fabric.js Editor
Erweiterter Canvas-Editor (`FabricReconstructionCanvas.tsx`):
- Drag & Drop fuer Zellen
- Freie Positionierung auf dem Canvas
- Export als PDF (reportlab) oder DOCX (python-docx)
```
POST /sessions/{id}/reconstruction
Body: {"cells": [{"cell_id": "r5_c2", "text": "corrected text"}]}
```
---
## Grid Editor (Detail)
Der Grid Editor baut aus den Kombi-Wortdaten (PaddleOCR + Tesseract) ein strukturiertes,
Excel-aehnliches Grid mit Zonen, Spalten, Zeilen und Zellen. Er ersetzt die manuelle
Rekonstruktion fuer Vokabelseiten mit komplexen Layouts (Bilder, Ueberschriften, IPA-Lautschrift).
**Dateien:**
| Datei | Beschreibung |
|-------|--------------|
| `grid_editor_api.py` | `_build_grid_core()` Pipeline, alle Steps |
| `grid_editor_helpers.py` | `_filter_footer_words()` → Seitenzahl-Extraktion, Footer-Filterung |
| `cv_syllable_detect.py` | Deutsche Silbentrennung mit IPA-Kompatibilitaet |
| `cv_ocr_engines.py` | IPA-Korrektur, Britfone-Woerterbuch, Garbled-IPA-Erkennung |
| `cv_vocab_types.py` | `PageZone` (mit `image_overlays`), `ColumnGeometry` |
| `tests/test_grid_editor_api.py` | 27 Tests |
### `_build_grid_core()` Pipeline
Die Grid-Erstellung laeuft in mehreren Steps:
```
Kombi-Wortdaten
├─ Zone Merging: Content-Zonen durch Bilder zusammenfuegen
│ → _merge_content_zones_across_boxes()
├─ Step 1: Zonen-Grid-Berechnung
│ → Spalten (Column Union), Zeilen, Zellen pro Zone
├─ Step 2: Header-Zeilen erkennen + mergen
├─ Step 3: Ghost-Filter
│ → _filter_border_ghosts(): Einzel-Char-Artefakte an Box-Raendern
├─ Step 4: Farb-Annotation
│ → detect_word_colors(): HSV-Farbanalyse aller word_boxes
├─ Step 4b2: Per-Cell Artifact Filter
│ → Einzel-Wort-Zellen mit ≤2 Zeichen und conf < 65 entfernen
├─ Step 4c: Oversized Word Box Removal
│ → word_boxes > 3x Median entfernen (Grafik-Artefakte)
├─ Step 4d2: Connector Column Normalization
│ → Dominante Kurzwoerter in schmalen Spalten normalisieren
├─ Step 5: Overlay-Wort-Filter
│ → Woerter innerhalb image_overlays entfernen
├─ Step 5a: Heading Detection by Color + Height
│ → _detect_heading_rows_by_color(): Farbige Ueberschriften erkennen
├─ Step 5b: Fix Unmatched Parentheses
├─ Step 5c: IPA Phonetic Correction
│ → fix_cell_phonetics(): Garbled OCR-IPA durch Britfone ersetzen
└─ Step 5d: IPA Continuation Detection
→ fix_ipa_continuation_cell(): Reine Klammer-Zellen als IPA-Fortsetzung
```
### Zone Merging Across Images
`_merge_content_zones_across_boxes()` erkennt das Muster `[content, box, content]` und
fuegt die Content-Zonen zu einer einzigen Zone zusammen. Die Box-Zonen werden als
`image_overlays` auf der gemergten Zone gespeichert.
```
Vorher: Zone 1 (content) → Zone 2 (box/Bild) → Zone 3 (content)
Nachher: Zone 1 (content, mit image_overlays=[Zone 2])
```
**PageZone.image_overlays:**
```json
[{"y": 120, "height": 85, "x": 50, "width": 400, "box": {...}}]
```
Box-Zonen die NICHT zwischen Content-Zonen liegen (z.B. "VOCABULARY" Header-Box)
bleiben als eigenstaendige Zone erhalten.
### Heading Detection by Color + Height
`_detect_heading_rows_by_color()` erkennt farbige Zwischenueberschriften
(z.B. "Unit 4: Bonnie Scotland") nach der Farb-Annotation:
1. **Farbkriterium:** ALLE word_boxes der Zeile haben `color_name != 'black'` (typisch: blau)
2. **Hoehenkriterium:** Mittlere Worthoehe > 1.2× Median aller Woerter in der Zone
Erkannte Headings werden zu einer Spanning-Cell zusammengefuegt (`col_type: 'heading'`).
### Ghost-Filter
`_filter_border_ghosts()` entfernt einzelne Zeichen (a-z, A-Z, 0-9) die nahe an
Box-Raendern erkannt werden — typische OCR-Artefakte von Rahmenlinien.
**Ausnahme:** Borderless Boxes (`border_thickness = 0`) ueberspringen den Ghost-Filter,
da dort keine Rahmen-Artefakte entstehen.
### IPA Phonetic Correction (Step 5c)
PaddleOCR erkennt IPA-Symbole als ASCII-Zeichen:
| IPA-Symbol | OCR liest |
|------------|-----------|
| `ʒ` | `3` |
| `ʃ` | `f` |
| `ə` | `9` |
| `ˈ` | `'` |
| `ɪ` | `i` oder `1` |
**`fix_cell_phonetics()`** ersetzt garbled OCR-IPA durch korrekte Lautschrift aus dem
Britfone-Woerterbuch (British English). Nur Zellen mit `col_type == 'column_en'` werden
verarbeitet.
**`_text_has_garbled_ipa()`** erkennt garbled IPA anhand von:
- Typischen OCR-IPA-Zeichen: `' 3 9 @ : ;`
- Bracket-Notation ohne echte IPA-Symbole: `[n, nn]`, `[1uedtX,1]`
### IPA Continuation Detection (Step 5d)
Vokabelseiten haben manchmal Zeilen, die nur Lautschrift enthalten (Fortsetzung
des Headwords der vorherigen Zeile). Diese werden von PaddleOCR als garbled Text erkannt.
**`fix_ipa_continuation_cell()`:**
1. Prueft ob die Zelle **komplett in Klammern** steht (`[...]`) — Zellen wie
"employee [im'ploi:]" werden NICHT ueberschrieben
2. Sucht das Headword in der Zeile darueber
3. Stripped Grammatik-Annotationen: `[sth.]`, `(adj.)` etc.
4. Schlaegt IPA im Britfone-Woerterbuch nach
5. Beruecksichtigt alle Wortteile (z.B. "close sth. down" → `[klˈəʊz dˈaʊn]`)
### Per-Cell Artifact Filter (Step 4b2)
Entfernt OCR-Rauschen auf Zellebene: Zellen mit genau einer `word_box`, maximal 2 Zeichen
und Confidence unter 65 werden als Artefakte klassifiziert und entfernt.
**Konstanten:**
| Parameter | Wert | Beschreibung |
|-----------|------|--------------|
| `_ARTIFACT_MAX_LEN` | 2 | Maximale Textlaenge fuer Artefakt-Verdacht |
| `_ARTIFACT_CONF_THRESHOLD` | 65 | Confidence-Schwelle (darunter = Artefakt) |
**Sicherheit:** Einzelne Zeichen mit hoher Confidence (z.B. rote `!`-Marker mit conf=98)
werden **nicht** entfernt, da ihre Confidence ueber dem Schwellwert liegt.
**Typische Artefakte:** `(as)` conf=55, `u)` conf=44 — OCR-Noise aus Seitenraendern
oder Schatten.
### Connector Column Normalization (Step 4d2)
Erkennt schmale Spalten mit einem dominanten Kurzwort (z.B. "oder", "and", "bzw.")
und normalisiert OCR-Fehler bei denen das dominante Wort mit Rauschen versehen wurde.
**Algorithmus:**
1. Pro Spalte: Zaehle Textvorkommen aller Zellen
2. Pruefe ob ein dominantes Wort existiert (≥ 60% der Zellen, max 10 Zeichen)
3. Fuer Zellen die mit dem dominanten Wort **beginnen** und max 2 Zeichen laenger sind:
Normalisiere auf das dominante Wort
**Beispiel:** Spalte mit "oder" in 80% der Zellen → `"oderb"` wird zu `"oder"` normalisiert.
### Compound Word IPA Decomposition (Step 5e)
Zusammengesetzte Woerter wie "schoolbag" oder "blackbird" haben oft keinen eigenen
IPA-Eintrag im Woerterbuch. Die Funktion `_decompose_compound()` zerlegt sie:
1. Probiere jede Teilungsposition (min. 3 Zeichen pro Teil)
2. Wenn beide Teile im Woerterbuch stehen → IPA verketten
3. Waehle die Teilung mit dem laengsten ersten Teil
**Beispiele:**
| Eingabe | Zerlegung | IPA |
|---------|-----------|-----|
| schoolbag | school + bag | skˈuːl + bæɡ |
| blackbird | black + bird | blæk + bˈɜːd |
| ice-cream | ice + cream | aɪs + kɹˈiːm |
### Trailing Garbled Fragment Removal (Step 5f)
Nach korrekt erkanntem IPA (z.B. `seat [sˈiːt]`) haengt OCR manchmal
eine garbled Kopie der IPA-Transkription an: `seat [sˈiːt] belt si:t belt`.
**`_strip_post_bracket_garbled()`** erkennt und entfernt diese:
1. Alles nach dem letzten `]` scannen
2. Woerter mit IPA-Markern (`:`, `ə`, `ɪ` etc.) → garbled, entfernen
3. Echte Woerter (Woerterbuch, Deutsch, Delimiter) → behalten
4. **Multi-Wort-Headword:** "belt" ist ein echtes Wort, aber wenn danach
garbled IPA kommt, wird nur "belt" behalten, der Rest entfernt
### Regression Framework (Step 5g)
Ground-Truth Sessions koennen als Referenz markiert werden. Nach jeder
Code-Aenderung vergleicht `POST /regression/run` die aktuelle Pipeline-Ausgabe
mit den gespeicherten Referenzen:
- **Strukturelle Diffs:** Zonen, Spalten, Zeilen (Anzahl-Aenderungen)
- **Zellen-Diffs:** Text-Aenderungen, fehlende/neue Zellen, col_type-Aenderungen
- **Persistenz:** Ergebnisse in `regression_runs` Tabelle fuer Trend-Analyse
- **Shell-Script:** `scripts/run-regression.sh` fuer CI-Integration
Admin-UI: [/ai/ocr-regression](https://macmini:3002/ai/ocr-regression)
### Ground Truth Review Workflow (Step 5h)
Admin-UI fuer effiziente Massenpruefung von Sessions:
- **Split-View:** Original-Bild links, erkannter Grid rechts
- **Confidence-Highlighting:** Niedrige Konfidenz rot hervorgehoben
- **Quick-Accept:** Korrekte Zeilen mit einem Klick bestaetigen
- **Inline-Edit:** Text direkt im Grid korrigieren
- **Session-Queue:** Automatisch naechste Session laden
- **Batch-Mark:** Mehrere Sessions gleichzeitig als Ground Truth markieren
Admin-UI: [/ai/ocr-ground-truth](https://macmini:3002/ai/ocr-ground-truth)
### Page Number Extraction
Die Footer-Filterung (`_filter_footer_words` in `grid_editor_helpers.py`) erkennt
Seitenzahlen in den untersten 5% des Bildes und gibt sie als Metadaten zurueck,
statt sie stillschweigend zu entfernen.
**Algorithmus:**
1. Woerter in den untersten 5% des Bildes identifizieren
2. Wenn ≤ 3 Woerter mit ≤ 10 Zeichen Gesamtlaenge: Als Seitenzahl extrahieren
3. Rueckgabe als `PageNumber`-Objekt: `{text, y_pct, number?}`
4. Ziffern werden separat als `number` (Integer) extrahiert
**Datentyp:**
```typescript
interface PageNumber {
text: string // Roh-OCR-Text (z.B. "u)233")
y_pct: number // Vertikale Position in Prozent
number?: number // Extrahierte Zahl (z.B. 233)
}
```
**Frontend-Anzeige:**
In der Summary-Leiste (GridEditor + StepGridReview) als Badge: `S. 233`.
Zeigt bevorzugt `page_number.number` (saubere Zahl), Fallback auf `page_number.text`.
**Zweck:** Spaetere Zusammenfuehrung aufeinanderfolgender Seiten im Kundenfrontend.
### Footer-Zeilen-Erkennung (Verbesserung)
Die Footer-Erkennung wurde um zwei Pruefungen erweitert, um falsch-positive
Footer-Markierungen bei Content-Zeilen zu verhindern:
| Pruefung | Bedingung | Grund |
|----------|-----------|-------|
| Komma-Check | `',' in text` → kein Footer | Content-Saetze enthalten Kommas, Seitenzahlen nicht |
| Laengen-Check | `len(text) > 20` → kein Footer | Seitenzahlen sind kurz, Content-Zeilen lang |
**Vorher:** `"Uhrzeit, Vergangenheit, Zukunft"` wurde als Footer markiert.
**Nachher:** Nur tatsaechliche Seitenzahlen (kurz, ohne Kommas) werden als Footer erkannt.
### Silben + IPA Kombination (Fix)
**Datei:** `cv_syllable_detect.py`
Wenn beide Modi (Silben:DE und IPA) aktiviert sind, blockierte der `_IPA_RE`-Guard
die Silbentrennung, weil programmatisch eingefuegte IPA-Klammern (z.B. `[bɪltʃøn]`)
IPA-Zeichen enthalten.
**Loesung:** Vor der IPA-Pruefung wird Bracket-Content entfernt:
```python
# Bracket-Content strippen, da programmatisch eingefuegt
text_no_brackets = re.sub(r'\[[^\]]*\]', '', text)
if _IPA_RE.search(text_no_brackets):
return text # Echte IPA im Fliesstext → keine Silbentrennung
```
So wird `"Bild·chen [bɪltʃøn]"` korrekt silbifiziert: Die Silbenpunkte bleiben erhalten,
und die IPA in Klammern wird nicht als Blockiergrund gewertet.
### `en_col_type` Erkennung
Die Erkennung der Englisch-Headword-Spalte nutzt **Bracket-IPA-Pattern-Count**
statt "laengster Durchschnittstext":
1. Fuer jede `column_*`-Spalte: Zaehle Zellen mit `[` im Text
2. Spalte mit den meisten Klammer-Mustern = `en_col_type`
3. **Fallback:** Laengster Durchschnittstext (wenn keine Klammern vorhanden)
Dies verhindert, dass Beispielsatz-Spalten (laenger aber ohne IPA) faelschlicherweise
als Headword-Spalte erkannt werden.
### Oversized Word Box Removal (Step 4c)
Entfernt `word_boxes` deren Flaeche > 3× der Median-Flaeche aller Woerter in der Zone.
Diese sind typischerweise Grafik-Artefakte (z.B. ein einzelnes "N" das eine ganze
Illustration abdeckt).
### Tests
```bash
cd klausur-service/backend && pytest tests/test_grid_editor_api.py -v # 27 Tests
```
| Klasse | Tests | Beschreibung |
|--------|-------|--------------|
| `TestZoneMerging` | 4 | Content-Zone-Merging ueber Box-Zonen |
| `TestHeadingDetection` | 3 | Farb- + Hoehenbasierte Heading-Erkennung |
| `TestGhostFilter` | 4 | Border-Ghost-Filterung inkl. Borderless |
| `TestOversizedWordBoxes` | 2 | Grafik-Artefakt-Entfernung |
| `TestGarbledIpaDetection` | 8 | Bracket-IPA, Continuation, en_col_type |
| `TestColumnUnion` | 3 | Spalten-Vereinigung ueber Zonen |
| `TestHeaderMerging` | 3 | Header-Zeilen zusammenfuegen |
---
## Wichtige Konstanten
| Konstante | Wert | Datei | Beschreibung |
|-----------|------|-------|--------------|
| `_NARROW_COL_THRESHOLD_PCT` | 15.0% | cv_vocab_pipeline.py | Schwelle breit/schmal fuer Hybrid-OCR |
| `_NARROW_THRESHOLD_PCT` | 10.0% | cv_vocab_pipeline.py | Schwelle fuer Spalten-Erweiterung |
| `_MIN_WORD_CONF` | 30 | cv_vocab_pipeline.py | Mindest-Confidence fuer OCR-Woerter |
| `_PAD` | 3px | cv_vocab_pipeline.py | Internes Padding bei Cell-Crop |
| `PDF_ZOOM` | 3.0 | cv_vocab_pipeline.py | PDF-Rendering (= 432 DPI) |
| `_MIN_WORD_MARGIN` | 4px | cv_vocab_pipeline.py | Sicherheitsabstand bei Spalten-Erweiterung |
---
## Datenbank-Schema
```sql
CREATE TABLE ocr_pipeline_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255),
filename VARCHAR(255),
status VARCHAR(50) DEFAULT 'active',
current_step INT DEFAULT 1,
-- Dokumenttyp-Erkennung
doc_type VARCHAR(50), -- 'vocab_table', 'generic_table', 'full_text'
doc_type_result JSONB, -- Vollstaendiges DetectionResult
-- Bilder (BYTEA)
original_png BYTEA,
deskewed_png BYTEA,
binarized_png BYTEA,
dewarped_png BYTEA,
-- Step-Results (JSONB)
deskew_result JSONB,
dewarp_result JSONB,
column_result JSONB,
row_result JSONB,
word_result JSONB, -- enthaelt vocab_entries, cells, llm_review
-- Ground Truth + Meta
ground_truth JSONB,
auto_shear_degrees REAL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
`word_result` JSONB-Struktur:
```json
{
"vocab_entries": [...],
"cells": [{"cell_id": "r0_c0", "text": "hello", "bbox_pct": {...}, "ocr_engine": "word_lookup", ...}],
"columns_used": [...],
"llm_review": {
"changes": [{"row_index": 5, "field": "english", "old": "...", "new": "..."}],
"model_used": "spell",
"duration_ms": 234
}
}
```
---
## Abhaengigkeiten
### Python (klausur-service)
| Paket | Version | Lizenz | Zweck |
|-------|---------|--------|-------|
| `pytesseract` | ≥0.3.10 | Apache-2.0 | Haupt-OCR (Schritt 35) |
| `opencv-python-headless` | ≥4.8.0 | Apache-2.0 | Bildverarbeitung, Projektionsprofile |
| `Pillow` | ≥10.0.0 | HPND (MIT-kompatibel) | Bildkonvertierung |
| `rapidocr` | latest | Apache-2.0 | Schnelles OCR (ARM64 via ONNX) |
| `onnxruntime` | latest | MIT | ONNX-Inferenz fuer RapidOCR |
| `pyspellchecker` | ≥0.8.1 | MIT | Regel-basierte OCR-Korrektur (Schritt 6) |
| `eng-to-ipa` | latest | MIT | IPA-Lautschrift-Lookup (Schritt 5) |
| `reportlab` | latest | BSD | PDF-Export (Schritt 7) |
| `python-docx` | ≥1.1.0 | MIT | DOCX-Export (Schritt 7) |
| `fabric` (JS) | ^6 | MIT | Canvas-Editor (Frontend) |
!!! info "pyspellchecker (neu seit 2026-03)"
`pyspellchecker` (MIT-Lizenz) ersetzt die LLM-basierte Korrektur als Standard-Engine.
EN+DE-Woerterbuch, ~134k Woerter. Kein Ollama noetig.
Umschaltbar via `REVIEW_ENGINE=llm` fuer den LLM-Pfad.
---
## Bekannte Einschraenkungen
| Problem | Ursache | Workaround |
|---------|---------|------------|
| Schraeg gedruckte Seiten | Deskew erkennt Text-Rotation, nicht Seiten-Rotation | Manueller Winkel |
| Sehr kleine Schrift (< 8pt) | Tesseract PSM 7 braucht min. Zeichengroesse | Vorher zoomen |
| Handgeschriebene Eintraege | Tesseract/RapidOCR sind fuer Druckschrift optimiert | TrOCR-Engine |
| Mehr als 5 Spalten | Projektionsprofil kann verschmelzen (Segmentierung hilft) | Manuelle Spalten |
| Farbige Marker (rot/blau) | HSV-Erkennung erzeugt False Positives | Manuell im Rekonstruktions-Editor |
| 15%-Schwelle nicht breit validiert | Nur an einem Arbeitsblatt-Typ getestet | Diverse Schulbuchseiten testen |
---
## Deployment
```bash
# 1. Git push
git push origin main
# 2. Mac Mini pull + build
ssh macmini "git -C /Users/benjaminadmin/Projekte/breakpilot-lehrer pull --no-rebase origin main"
# klausur-service (Backend)
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-lehrer/docker-compose.yml build klausur-service"
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-lehrer/docker-compose.yml up -d klausur-service"
# admin-lehrer (Frontend)
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-lehrer/docker-compose.yml build admin-lehrer"
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-lehrer/docker-compose.yml up -d admin-lehrer"
# 3. Testen unter:
# https://macmini:3002/ai/ocr-pipeline
```
!!! warning "Base-Image bei neuen Python-Paketen"
Wenn `requirements.txt` geaendert wird (z.B. neues Paket hinzugefuegt), muss zuerst
das Base-Image neu gebaut werden:
```bash
ssh macmini "/usr/local/bin/docker build -f /Users/benjaminadmin/Projekte/breakpilot-lehrer/klausur-service/Dockerfile.base \
-t klausur-base:latest /Users/benjaminadmin/Projekte/breakpilot-lehrer/klausur-service/"
```
---
## OCR Overlay — Alternative Pipelines
**URL:** https://macmini:3002/ai/ocr-overlay
Neben der vollen 10-Schritt-Pipeline gibt es die **OCR Overlay**-Seite mit
vereinfachten Pfaden fuer schnelle Ergebnisse. Alle drei Modi teilen die
gleichen Vorverarbeitungsschritte (Orient → Deskew → Dewarp → Crop).
### Modus-Uebersicht
| Modus | Schritte | Engine | Endpoint | Beschreibung |
|-------|----------|--------|----------|--------------|
| **Pipeline** | 7 | Tesseract | `/words` (SSE) | Volle Pipeline: Zeilen + Woerter + Overlay |
| **Paddle Direct** | 5 | PaddleOCR | `/paddle-direct` | PaddleOCR ersetzt Zeilen + Woerter + Overlay |
| **Kombi** | 5 | PaddleOCR + Tesseract | `/paddle-kombi` | Beide Engines, Ergebnisse gemittelt |
### Flussdiagramm
```
┌──────────────────────────────────────────────────────────────┐
│ GEMEINSAME VORVERARBEITUNG (alle 3 Modi) │
│ │
│ Schritt 1: Orientierung │
│ Schritt 2: Deskew │
│ Schritt 3: Dewarp │
│ Schritt 4: Crop │
└──────────────────┬────────────────────┬───────────────────────┘
│ │
┌───────────┼────────────────────┼────────────────┐
▼ ▼ ▼ ▼
PIPELINE PADDLE DIRECT KOMBI-MODUS
(7 Schritte) (5 Schritte) (5 Schritte)
│ │ │
Zeilen- PaddleOCR PaddleOCR
erkennung word_boxes + Tesseract
│ │ parallel
Woerter- build_grid_ │
erkennung from_words() _merge_paddle_
│ │ tesseract()
Overlay Overlay │
│ │ build_grid_
▼ ▼ from_words()
Ergebnis Ergebnis │
Overlay
Ergebnis
```
### Paddle Direct
PaddleOCR laeuft auf dem vorverarbeiteten Bild und erkennt Woerter direkt.
**Endpoint:** `POST /api/v1/ocr-pipeline/sessions/{id}/paddle-direct`
**Ablauf:**
1. Cropped/dewarped Bild laden (Prioritaet: cropped > dewarped > original)
2. `ocr_region_paddle(img_bgr, region=None)` aufrufen
3. `build_grid_from_words(word_dicts, img_w, img_h)` fuer Grid-Erstellung
4. Cells mit `ocr_engine="paddle_direct"` taggen
5. In DB speichern (`current_step=8`)
**Frontend:** `PaddleDirectStep.tsx` — wiederverwendbare Komponente mit konfigurierbaren Props.
### Kombi-Modus (PaddleOCR + Tesseract)
!!! info "Motivation"
PaddleOCR liefert gute Texterkennung, positioniert Woerter aber manchmal falsch
(z.B. `!Betonung` als ein Wort, Bullet Points nicht erkannt). Tesseract erkennt
Sonderzeichen besser und liefert feinere Word-Level-Boxen. Der Kombi-Modus
nutzt beide Engines und mittelt die Koordinaten.
**Endpoint:** `POST /api/v1/ocr-pipeline/sessions/{id}/paddle-kombi`
**Ablauf:**
1. Cropped/dewarped Bild laden
2. **Parallel** beide Engines aufrufen:
- `ocr_region_paddle(img_bgr, region=None)` → `paddle_words`
- `pytesseract.image_to_data(pil_img, lang='eng+deu', config='--psm 6 --oem 3')` → `tess_words`
3. **Merge:** `_merge_paddle_tesseract(paddle_words, tess_words)`
4. `build_grid_from_words(merged_words, img_w, img_h)` fuer Grid
5. Cells mit `ocr_engine="kombi"` taggen
6. In DB speichern
#### Merge-Algorithmus (v2: Row-Based Sequence Alignment)
!!! info "Rewrite (v4.3)"
Der Merge wurde von IoU-basiertem Matching auf **Row-Based Sequence Alignment** umgestellt.
Multi-Word Paddle-Boxen werden vor dem Merge in Einzelwoerter aufgeteilt
(`_split_paddle_multi_words`).
**Ablauf:**
1. **Row Grouping:** Woerter beider Engines nach Y-Position in Zeilen gruppieren (12px Toleranz)
2. **Row Matching:** Paddle- und Tesseract-Zeilen ueber vertikale Naehe zuordnen
3. **Sequence Alignment:** Innerhalb jeder gematchten Zeile links-nach-rechts durchlaufen:
- **Gleicher Text** oder **Substring-Match:** Zusammenfuehren (Paddle-Text, gemittelte Koordinaten)
- **Raeumlicher Overlap >= 50%:** Auch bei unterschiedlichem Text als Duplikat behandeln
- **Nur bei einer Engine:** Wort beibehalten (falls Confidence >= 30)
4. **Ungematchte Zeilen:** Paddle-Zeilen behalten, Tesseract-Zeilen nur mit Confidence >= 40
```mermaid
flowchart TD
A[Beide Engines] --> B[Row Grouping<br/>Y-Toleranz 12px]
B --> C[Row Matching<br/>vertikale Naehe]
C --> D{Gleicher Text<br/>oder Overlap >= 50%?}
D -->|Ja| E[Deduplizieren:<br/>Paddle-Text + gemittelte Coords]
D -->|Nein| F{Wort nur bei<br/>einer Engine?}
F -->|Ja| G[Beibehalten<br/>falls conf >= 30]
F -->|Nein| H[Beide behalten<br/>verschiedene Positionen]
```
**Koordinaten-Mittelung:**
```
merged_left = (paddle_left × paddle_conf + tess_left × tess_conf) / (paddle_conf + tess_conf)
```
Gleiches Prinzip fuer `top`, `width`, `height`. Der Text kommt immer von PaddleOCR (bessere Texterkennung).
**Raeumlicher Overlap-Check (v4.3.1):**
Wenn zwei Woerter >= 50% horizontal ueberlappen, werden sie als dasselbe physische Wort behandelt —
unabhaengig davon, ob die OCR-Texte unterschiedlich sind (z.B. "hello" vs "helo").
Dies verhindert, dass leicht unterschiedliche Erkennungen als separate Woerter uebereinander
im Overlay erscheinen.
#### Dateien
| Datei | Aenderung |
|-------|-----------|
| `ocr_pipeline_api.py` | `_split_paddle_multi_words()`, `_group_words_into_rows()`, `_merge_row_sequences()`, `_merge_paddle_tesseract()`, `/paddle-kombi` Endpoint |
| `admin-lehrer/.../ocr-overlay/types.ts` | `KOMBI_STEPS` Konstante |
| `admin-lehrer/.../ocr-overlay/useSlideWordPositions.ts` | Slide-Positionierung mit `yPct`/`hPct` |
| `admin-lehrer/.../ocr-overlay/usePixelWordPositions.ts` | Pixel-Cluster-Positionierung mit `yPct`/`hPct` |
| `admin-lehrer/.../ocr-overlay/OverlayReconstruction.tsx` | Rendering mit per-Word Y-Positionen |
| `admin-lehrer/.../PaddleDirectStep.tsx` | Wiederverwendbar mit `endpoint`/`engineKey` Props |
| `admin-lehrer/.../ocr-overlay/page.tsx` | 3er-Toggle: Pipeline / Paddle Direct / Kombi |
#### Tests
```bash
cd klausur-service/backend && pytest tests/test_paddle_kombi.py -v # 36 Tests
```
**Test-Klassen:**
| Klasse | Tests | Beschreibung |
|--------|-------|--------------|
| `TestSplitPaddleMultiWords` | 7 | Multi-Word-Box-Splitting |
| `TestGroupWordsIntoRows` | 5 | Y-Position Row Clustering |
| `TestMergeRowSequences` | 10 | Sequence Alignment innerhalb einer Zeile |
| `TestMergePaddleTesseract` | 8 | Vollstaendiger Merge mit Row-Matching |
| `TestMergeRealWorldRegression` | 1 | Regression mit Echtdaten |
| `TestSpatialOverlapDedup` | 4 | Raeumliche Overlap-Deduplizierung |
| `TestSplitThenMerge` | 1 | Split + Merge End-to-End |
| Testklasse | Tests | Beschreibung |
|------------|-------|--------------|
| `TestBoxIoU` | 6 | IoU-Berechnung: identisch, kein Overlap, teilweise, enthalten, Kante, Null-Flaeche |
| `TestMergePaddleTesseract` | 10 | Merge: Match-Averaging, kein Match, Low-Conf-Drop, leer, IoU-Schwelle, Text-Praeferenz, Zero-Conf |
| `TestMergePaddleTesseractBulletPoints` | 2 | Bullet-Points und Sonderzeichen von Tesseract |
---
## ONNX Backends und PP-DocLayout (Sprint 2)
### TrOCR ONNX Runtime
Ab Sprint 2 unterstuetzt die Pipeline **TrOCR mit ONNX Runtime** als Alternative zu PyTorch.
ONNX reduziert den RAM-Verbrauch von ~1.1 GB auf ~300 MB pro Modell und beschleunigt
die Inferenz um ~3x. Ideal fuer Hardware Tier 2 (8 GB RAM).
**Backend-Auswahl:** Umgebungsvariable `TROCR_BACKEND` (`auto` | `pytorch` | `onnx`).
Im `auto`-Modus wird ONNX bevorzugt, wenn exportierte Modelle vorhanden sind.
Vollstaendige Dokumentation: [TrOCR ONNX Runtime](TrOCR-ONNX.md)
### PP-DocLayout (Document Layout Analysis)
PP-DocLayout ersetzt die bisherige manuelle Zonen-Erkennung durch ein vortrainiertes
Layout-Analyse-Modell. Es erkennt automatisch:
- **Tabellen** (vocab_table, generic_table)
- **Ueberschriften** (title, section_header)
- **Bilder/Grafiken** (figure, illustration)
- **Textbloecke** (paragraph, list)
PP-DocLayout laeuft als ONNX-Modell (~15 MB) und benoetigt kein PyTorch.
Die Ergebnisse fliessen in Schritt 5 (Spaltenerkennung) und den Grid Editor ein.
---
## Aenderungshistorie
| Datum | Version | Aenderung |
|-------|---------|----------|
| 2026-03-26 | 5.2.0 | **OCR Kombi Pipeline:** Neuer modularer Nachfolger als 11-Schritt-Architektur unter `/ai/ocr-kombi`. Eigene Dokumentation: [OCR Kombi Pipeline](OCR-Kombi-Pipeline.md). Phase 1 (Grundgeruest + DB) implementiert: DB-Migration (`document_group_id`, `page_number`), Frontend-Orchestrator, 13 Step-Komponenten, Backend-Router mit Multi-Page-Upload. |
| 2026-03-26 | 5.1.0 | **Grid Quality & Metadata:** Per-Cell Artifact Filter (Step 4b2: ≤2 Zeichen + conf < 65), Connector Column Normalization (Step 4d2: dominante Kurzwoerter), Footer-Erkennung verbessert (Komma/Laengen-Check), Seitenzahl-Extraktion als Metadaten (`page_number` Feld im Grid-Result), Frontend-Anzeige in Summary-Leiste. Silben+IPA-Kombination gefixt (Bracket-Content vor IPA-Guard strippen). |
| 2026-03-23 | 5.0.0 | **Phase 1 Sprint 1:** Compound-IPA-Zerlegung (`_decompose_compound`), Trailing-Garbled-Fragment-Entfernung (Multi-Wort-Headwords), Regression Framework mit DB-Persistenz + History + Shell-Script, Ground-Truth Review Workflow UI, Page-Crop Determinismus verifiziert. Admin-Seiten: `/ai/ocr-regression`, `/ai/ocr-ground-truth`. |
| 2026-03-20 | 4.7.0 | Grid Editor: Zone Merging ueber Bilder (`image_overlays`), Heading Detection (Farbe + Hoehe), Ghost-Filter (borderless-aware), Oversized Word Box Removal, IPA Phonetic Correction (Britfone), IPA Continuation Detection, `en_col_type` via Bracket-Count. 27 Tests. |
| 2026-03-16 | 4.6.0 | Strukturerkennung (Schritt 8): Region-basierte Grafikerkennung (`cv_graphic_detect.py`) mit Zwei-Pass-Verfahren (Farbregionen + schwarze Illustrationen), Wort-Ueberlappungs-Filter, Box/Zonen/Farb-Analyse. Schritt laeuft nach Worterkennung. |
| 2026-03-12 | 4.5.0 | Kombi-Modus (PaddleOCR + Tesseract): Beide Engines laufen parallel, Koordinaten werden IoU-basiert gematcht und confidence-gewichtet gemittelt. Ungematchte Tesseract-Woerter (Bullets, Symbole) werden hinzugefuegt. 3er-Toggle in OCR Overlay. |
| 2026-03-12 | 4.4.0 | PaddleOCR Remote-Engine (`engine=paddle`): PP-OCRv5 Latin auf Hetzner x86_64. Neuer Microservice (`paddleocr-service/`), HTTP-Client (`paddleocr_remote.py`), Frontend-Dropdown-Option. Nutzt words_first Grid-Methode. |
| 2026-03-12 | 4.3.0 | Words-First Grid Builder (`cv_words_first.py`): Bottom-up-Algorithmus clustert Tesseract word_boxes direkt zu Spalten/Zeilen/Zellen. Neuer `grid_method` Parameter im `/words` Endpoint. Frontend-Toggle in StepWordRecognition. |
| 2026-03-10 | 4.2.0 | Rekonstruktion: Overlay-Modus mit Pixel-Wortpositionierung, 180°-Rotation, Sub-Session-Merging, usePixelWordPositions Hook, Box-Boundary-Schutz (box_ranges_inner) |
| 2026-03-05 | 3.1.0 | Spalten: Seiten-Segmentierung an Sub-Headern, Word-Coverage Fallback, Segment-gefilterte Validierung |
| 2026-03-05 | 3.0.1 | Dewarp: Feinabstimmung mit 7 Schiebereglern (3 Rotation + 4 Shear), Combined-Adjust-Endpoint |
| 2026-03-05 | 3.0.0 | Doku-Update: Dokumenttyp-Erkennung, Hybrid-Grid, Sub-Column-Detection, Pipeline-Pfade |
| 2026-03-04 | 2.2.0 | Dewarp: Vertikalkanten-Drift statt Textzeilen-Neigung, Schwellenwerte gesenkt |
| 2026-03-04 | 2.1.0 | Sub-Column-Detection, expand_narrow_columns, Fabric.js Editor, PDF/DOCX-Export |
| 2026-03-03 | 2.0.0 | Schritte 67 implementiert; Spell-Checker, Rekonstruktions-Canvas |
| 2026-03-03 | 1.5.0 | Spaltenerkennung: volle Bildbreite fuer initialen Scan, Phantom-Filter |
| 2026-03-03 | 1.4.0 | Zeilenerkennung: Artefakt-Zeilen entfernen + Luecken-Heilung |
| 2026-03-03 | 1.3.0 | Zeichenkorrektur: `1.`/`\|.` Listenpraefixe werden nicht zu `I.` |
| 2026-03-03 | 1.2.0 | LLM-Engine durch Spell-Checker ersetzt (REVIEW_ENGINE=spell) |
| 2026-02-28 | 1.0.0 | Schritt 5 (Worterkennung) implementiert |
| 2026-02-22 | 0.4.0 | Schritt 4 (Zeilenerkennung) implementiert |
| 2026-02-20 | 0.3.0 | Schritt 3 (Spaltenerkennung) mit Typ-Klassifikation |
| 2026-02-15 | 0.2.0 | Schritt 2 (Entzerrung/Dewarp) |
| 2026-02-12 | 0.1.0 | Schritt 1 (Begradigung/Deskew) + Session-Management |