Files
breakpilot-lehrer/docs-src/services/klausur-service/OCR-Pipeline.md
Benjamin Admin a6069631cc
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 31s
CI / test-go-edu-search (push) Successful in 29s
CI / test-python-klausur (push) Failing after 2m7s
CI / test-python-agent-core (push) Successful in 21s
CI / test-nodejs-website (push) Successful in 21s
feat: PaddleOCR Remote-Engine (PP-OCRv5 Latin auf Hetzner x86_64)
PaddleOCR als neue engine=paddle Option in der OCR-Pipeline.
Microservice auf Hetzner (paddleocr-service/), async HTTP-Client
(paddleocr_remote.py), Frontend-Dropdown, automatisch words_first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 09:31:22 +01:00

1106 lines
49 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:** 4.3.0
**Status:** Produktiv (Schritte 110 implementiert)
**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)
├── cv_box_detect.py # Box-Erkennung + Zonen-Aufteilung
├── cv_words_first.py # Words-First Grid Builder (bottom-up)
├── page_crop.py # Content-basierter Crop-Algorithmus
├── ocr_pipeline_session_store.py # PostgreSQL Persistence
├── layout_reconstruction_service.py # Fabric.js JSON + PDF/DOCX Export
└── 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
└── 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
├── StepLlmReview.tsx # Schritt 8: 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: 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 9: 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 |
---
## 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 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`, `text`, `fontRatio`
### 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"}]}
```
---
## 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/"
```
---
## Aenderungshistorie
| Datum | Version | Aenderung |
|-------|---------|----------|
| 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 |