# OCR Pipeline - Schrittweise Seitenrekonstruktion **Version:** 5.0.0 **Status:** Produktiv (Schritte 1–10 + 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
pipeline = cell_first
confidence 0.7–0.95"] D -->|Nein| F{Zeilen-Gaps >= 3?} C -->|Nein| G{Interne Spalten-Gaps >= 1?} G -->|Ja| F G -->|Nein| H["full_text
pipeline = full_page
skip: columns, rows"] F -->|Ja| I["generic_table
pipeline = cell_first
confidence 0.5–0.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 ├── cv_ocr_engines.py # OCR-Engines, IPA-Korrektur, Britfone-Woerterbuch ├── 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 20–40% Bildbreite, typische schmale bei 3–12%. Die 15%-Grenze trennt diese Gruppen sauber. !!! note "Offener Punkt: Schwellen-Validierung" Die 15%-Schwelle wurde an Vokabeltabellen mit 3–5 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 50–200 % - 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 (30–120%) - 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 ``` **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` 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 | | `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 4c: Oversized Word Box Removal │ → word_boxes > 3x Median entfernen (Grafik-Artefakte) │ ├─ 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]`) ### 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) ### `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 3–5) | | `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
Y-Toleranz 12px] B --> C[Row Matching
vertikale Naehe] C --> D{Gleicher Text
oder Overlap >= 50%?} D -->|Ja| E[Deduplizieren:
Paddle-Text + gemittelte Coords] D -->|Nein| F{Wort nur bei
einer Engine?} F -->|Ja| G[Beibehalten
falls conf >= 30] F -->|Nein| H[Beide behalten
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-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 6–7 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 |