# OCR Pipeline - Schrittweise Seitenrekonstruktion **Version:** 3.0.0 **Status:** Produktiv (Schritte 1–8 implementiert) **URL:** https://macmini:3002/ai/ocr-pipeline ## Uebersicht Die OCR Pipeline zerlegt den OCR-Prozess in **8 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 | Begradigung (Deskew) | Scan begradigen (Hough Lines + Word Alignment) | Implementiert | | 2 | Entzerrung (Dewarp) | Buchwoelbung entzerren (Vertikalkanten-Analyse) | Implementiert | | 3 | Spaltenerkennung | Unsichtbare Spalten finden (Projektionsprofile + Wortvalidierung) | Implementiert | | 4 | Zeilenerkennung | Horizontale Zeilen + Kopf-/Fusszeilen-Klassifikation + Luecken-Heilung | Implementiert | | 5 | Worterkennung | Hybrid-Grid: Breite Spalten full-page, schmale cell-crop | Implementiert | | 6 | Korrektur | Zeichenverwirrung + regel-basierte Rechtschreibkorrektur (SSE-Stream) | Implementiert | | 7 | Rekonstruktion | Interaktive Zellenbearbeitung auf Bildhintergrund (Fabric.js) | Implementiert | | 8 | Validierung | Ground-Truth-Vergleich und Qualitaetspruefung | Implementiert | --- ## Dokumenttyp-Erkennung und Pipeline-Pfade ### Automatische Weiche: `detect_document_type()` Nicht jedes Dokument durchlaeuft denselben Pfad. Nach den gemeinsamen Vorverarbeitungsschritten (Deskew, Dewarp, Binarisierung) 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) │ │ │ │ Stage 1: Render (432 DPI, 3× Zoom) │ │ Stage 2: Deskew (Hough Lines + Ensemble) │ │ Stage 3: Dewarp (Vertikalkanten-Drift, Ensemble Shear) │ │ Stage 4: Dual-Bild (ocr_img = binarisiert, layout_img = CLAHE) │ └─────────────────────────────────────┬───────────────────────────────┘ │ detect_document_type() │ ┌─────────────────┴──────────────────┐ ▼ ▼ FULL-TEXT PFAD CELL-FIRST PFAD (pipeline='full_page') (pipeline='cell_first') │ │ Keine Spalten/Zeilen Spaltenerkennung analyze_layout_by_words() detect_column_geometry() Lese-Reihenfolge _detect_sub_columns() │ expand_narrow_columns() │ Zeilenerkennung │ detect_row_geometry() │ │ │ 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 6: Korrektur (Spell) Schritt 7: Rekonstruktion Schritt 8: 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 (alle Endpoints) ├── 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 ├── StepDeskew.tsx # Schritt 1: Begradigung ├── StepDewarp.tsx # Schritt 2: Entzerrung ├── StepColumnDetection.tsx # Schritt 3: Spaltenerkennung ├── StepRowDetection.tsx # Schritt 4: Zeilenerkennung ├── StepWordRecognition.tsx # Schritt 5: Worterkennung ├── StepLlmReview.tsx # Schritt 6: Korrektur (SSE-Stream) ├── StepReconstruction.tsx # Schritt 7: Rekonstruktion (Canvas) ├── FabricReconstructionCanvas.tsx # Fabric.js Editor └── StepGroundTruth.tsx # Schritt 8: 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/deskewed` | Begradigtes Bild | | `GET` | `/sessions/{id}/image/dewarped` | Entzerrtes 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: 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 2: Entzerrung | Methode | Pfad | Beschreibung | |---------|------|--------------| | `POST` | `/sessions/{id}/dewarp` | Automatische Entzerrung | | `POST` | `/sessions/{id}/dewarp/manual` | Manueller Scherbungswinkel | | `POST` | `/sessions/{id}/ground-truth/dewarp` | Ground Truth speichern | ### Schritt 3: 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 4: 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 5: Worterkennung | Methode | Pfad | Beschreibung | |---------|------|--------------| | `POST` | `/sessions/{id}/words` | Wort-Grid aus Spalten x Zeilen erstellen | | `POST` | `/sessions/{id}/ground-truth/words` | Ground Truth speichern | | `GET` | `/sessions/{id}/ground-truth/words` | Ground Truth abrufen | ### Schritt 6: 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 7: 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 8) | | `GET` | `/sessions/{id}/reconstruction/validation` | Validierungsdaten abrufen | --- ## Schritt 2: 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 | --- ## Schritt 3: Spaltenerkennung (Detail) ### Algorithmus: `detect_column_geometry()` Zweistufige Erkennung: vertikale Projektionsprofile finden Luecken, Wort-Bounding-Boxes validieren. ``` Bild → Binarisierung → 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. ### 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 4: 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. ```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.""" ... ``` --- ## Schritt 5: Worterkennung — Hybrid-Grid (Detail) ### Algorithmus: `build_cell_grid_v2()` Schritt 5 nutzt 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) 5. **Fallback:** Bei leerem Ergebnis → PSM 7 (Einzelzeile) statt PSM 6 6. **Bereinigung:** `_clean_cell_text_lite()` (aggressives Noise-Filtering) ### 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 6: 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 7: Rekonstruktion (Detail) Zwei 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 ### 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 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 4 Spalten | Projektionsprofil kann verschmelzen | 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-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 |