Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 30s
CI / test-go-edu-search (push) Successful in 30s
CI / test-python-klausur (push) Failing after 2m28s
CI / test-python-agent-core (push) Successful in 17s
CI / test-nodejs-website (push) Successful in 18s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1752 lines
76 KiB
Markdown
1752 lines
76 KiB
Markdown
# OCR Pipeline - Schrittweise Seitenrekonstruktion
|
||
|
||
**Version:** 5.1.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<br/>pipeline = cell_first<br/>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<br/>pipeline = full_page<br/>skip: columns, rows"]
|
||
F -->|Ja| I["generic_table<br/>pipeline = cell_first<br/>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
|
||
├── grid_editor_helpers.py # Footer-Filterung, Seitenzahl-Extraktion
|
||
├── cv_ocr_engines.py # OCR-Engines, IPA-Korrektur, Britfone-Woerterbuch
|
||
├── cv_syllable_detect.py # Deutsche Silbentrennung (Silben:DE Modus)
|
||
├── cv_box_detect.py # Box-Erkennung + Zonen-Aufteilung
|
||
├── cv_graphic_detect.py # Grafik-/Bilderkennung (Region-basiert)
|
||
├── cv_color_detect.py # Farbtext-Erkennung (HSV-Analyse)
|
||
├── cv_words_first.py # Words-First Grid Builder (bottom-up)
|
||
├── cv_vocab_types.py # Datentypen: PageZone, ColumnGeometry, etc.
|
||
├── page_crop.py # Content-basierter Crop-Algorithmus
|
||
├── ocr_pipeline_session_store.py # PostgreSQL Persistence
|
||
├── layout_reconstruction_service.py # Fabric.js JSON + PDF/DOCX Export
|
||
├── tests/
|
||
│ └── test_grid_editor_api.py # 27 Tests fuer Grid Editor + IPA
|
||
└── migrations/
|
||
├── 002_ocr_pipeline_sessions.sql # Basis-Schema
|
||
├── 003_add_row_result.sql # Row-Result Spalte
|
||
└── 004_add_word_result.sql # Word-Result Spalte
|
||
|
||
admin-lehrer/
|
||
├── app/(admin)/ai/ocr-pipeline/
|
||
│ ├── page.tsx # Haupt-Page mit Session-Management
|
||
│ └── types.ts # TypeScript Interfaces
|
||
├── app/(admin)/ai/ocr-overlay/
|
||
│ ├── page.tsx # OCR Overlay: 3 Modi (Pipeline/Paddle/Kombi)
|
||
│ └── types.ts # OVERLAY_/PADDLE_DIRECT_/KOMBI_STEPS
|
||
├── components/ocr-overlay/
|
||
│ ├── PaddleDirectStep.tsx # Wiederverwendbar fuer Paddle Direct + Kombi
|
||
│ └── OverlayReconstruction.tsx # Overlay-Anzeige auf Bildhintergrund
|
||
└── components/ocr-pipeline/
|
||
├── PipelineStepper.tsx # Fortschritts-Stepper
|
||
├── StepOrientation.tsx # Schritt 1: Orientierung
|
||
├── StepDeskew.tsx # Schritt 2: Begradigung
|
||
├── StepDewarp.tsx # Schritt 3: Entzerrung
|
||
├── StepCrop.tsx # Schritt 4: Zuschneiden
|
||
├── StepColumnDetection.tsx # Schritt 5: Spaltenerkennung
|
||
├── StepRowDetection.tsx # Schritt 6: Zeilenerkennung
|
||
├── StepWordRecognition.tsx # Schritt 7: Worterkennung
|
||
├── StepStructureDetection.tsx # Schritt 8: Strukturerkennung
|
||
├── StepLlmReview.tsx # Schritt 9: Korrektur (SSE-Stream)
|
||
├── StepReconstruction.tsx # Schritt 9: Rekonstruktion (Canvas + Overlay)
|
||
├── usePixelWordPositions.ts # Shared Hook: Pixel-basierte Wortpositionierung
|
||
├── FabricReconstructionCanvas.tsx # Fabric.js Editor
|
||
└── StepGroundTruth.tsx # Schritt 10: Validierung
|
||
```
|
||
|
||
---
|
||
|
||
## API-Referenz
|
||
|
||
Alle Endpoints unter `/api/v1/ocr-pipeline/`.
|
||
|
||
### Sessions
|
||
|
||
| Methode | Pfad | Beschreibung |
|
||
|---------|------|--------------|
|
||
| `POST` | `/sessions` | Neue Session erstellen (Bild hochladen) |
|
||
| `GET` | `/sessions` | Alle Sessions auflisten |
|
||
| `GET` | `/sessions/{id}` | Session-Info mit allen Step-Results |
|
||
| `PUT` | `/sessions/{id}` | Session umbenennen |
|
||
| `DELETE` | `/sessions/{id}` | Session loeschen |
|
||
| `POST` | `/sessions/{id}/detect-type` | Dokumenttyp erkennen |
|
||
|
||
### Bilder
|
||
|
||
| Methode | Pfad | Beschreibung |
|
||
|---------|------|--------------|
|
||
| `GET` | `/sessions/{id}/image/original` | Originalbild |
|
||
| `GET` | `/sessions/{id}/image/oriented` | Orientiertes Bild |
|
||
| `GET` | `/sessions/{id}/image/deskewed` | Begradigtes Bild |
|
||
| `GET` | `/sessions/{id}/image/dewarped` | Entzerrtes Bild |
|
||
| `GET` | `/sessions/{id}/image/cropped` | Zugeschnittenes Bild |
|
||
| `GET` | `/sessions/{id}/image/binarized` | Binarisiertes Bild |
|
||
| `GET` | `/sessions/{id}/image/columns-overlay` | Spalten-Overlay |
|
||
| `GET` | `/sessions/{id}/image/rows-overlay` | Zeilen-Overlay |
|
||
| `GET` | `/sessions/{id}/image/words-overlay` | Wort-Grid-Overlay |
|
||
|
||
### Schritt 1: Orientierung
|
||
|
||
| Methode | Pfad | Beschreibung |
|
||
|---------|------|--------------|
|
||
| `POST` | `/sessions/{id}/orientation` | 90/180/270° Drehung erkennen und korrigieren |
|
||
|
||
### Schritt 2: Begradigung
|
||
|
||
| Methode | Pfad | Beschreibung |
|
||
|---------|------|--------------|
|
||
| `POST` | `/sessions/{id}/deskew` | Automatische Begradigung |
|
||
| `POST` | `/sessions/{id}/deskew/manual` | Manuelle Winkelkorrektur |
|
||
| `POST` | `/sessions/{id}/ground-truth/deskew` | Ground Truth speichern |
|
||
|
||
### Schritt 3: Entzerrung
|
||
|
||
| Methode | Pfad | Beschreibung |
|
||
|---------|------|--------------|
|
||
| `POST` | `/sessions/{id}/dewarp` | Automatische Entzerrung |
|
||
| `POST` | `/sessions/{id}/dewarp/manual` | Manueller Scherbungswinkel |
|
||
| `POST` | `/sessions/{id}/adjust-combined` | Kombinierte Rotation + Shear Feinabstimmung |
|
||
| `POST` | `/sessions/{id}/ground-truth/dewarp` | Ground Truth speichern |
|
||
|
||
### Schritt 4: Zuschneiden
|
||
|
||
| Methode | Pfad | Beschreibung |
|
||
|---------|------|--------------|
|
||
| `POST` | `/sessions/{id}/crop` | Automatischer Content-Crop |
|
||
| `POST` | `/sessions/{id}/crop/manual` | Manueller Crop (Prozent-Koordinaten) |
|
||
| `POST` | `/sessions/{id}/crop/skip` | Crop ueberspringen |
|
||
|
||
### Schritt 5: Spalten
|
||
|
||
| Methode | Pfad | Beschreibung |
|
||
|---------|------|--------------|
|
||
| `POST` | `/sessions/{id}/columns` | Automatische Spaltenerkennung |
|
||
| `POST` | `/sessions/{id}/columns/manual` | Manuelle Spalten-Definition |
|
||
| `POST` | `/sessions/{id}/ground-truth/columns` | Ground Truth speichern |
|
||
|
||
### Schritt 6: Zeilen
|
||
|
||
| Methode | Pfad | Beschreibung |
|
||
|---------|------|--------------|
|
||
| `POST` | `/sessions/{id}/rows` | Automatische Zeilenerkennung |
|
||
| `POST` | `/sessions/{id}/rows/manual` | Manuelle Zeilen-Definition |
|
||
| `POST` | `/sessions/{id}/ground-truth/rows` | Ground Truth speichern |
|
||
| `GET` | `/sessions/{id}/ground-truth/rows` | Ground Truth abrufen |
|
||
|
||
### Schritt 7: Worterkennung
|
||
|
||
| Methode | Pfad | Beschreibung |
|
||
|---------|------|--------------|
|
||
| `POST` | `/sessions/{id}/words` | Wort-Grid erstellen |
|
||
| `POST` | `/sessions/{id}/ground-truth/words` | Ground Truth speichern |
|
||
| `GET` | `/sessions/{id}/ground-truth/words` | Ground Truth abrufen |
|
||
|
||
**Query-Parameter fuer `/sessions/{id}/words`:**
|
||
|
||
| Parameter | Default | Beschreibung |
|
||
|-----------|---------|--------------|
|
||
| `engine` | `auto` | OCR-Engine: `auto`, `tesseract`, `rapid`, `paddle` |
|
||
| `pronunciation` | `british` | IPA-Woerterbuch: `british` oder `american` |
|
||
| `stream` | `false` | SSE-Streaming (nur bei `grid_method=v2`) |
|
||
| `skip_heal_gaps` | `false` | Zeilen-Luecken nicht heilen (Overlay-Modus) |
|
||
| `grid_method` | `v2` | Grid-Strategie: `v2` (top-down) oder `words_first` (bottom-up) |
|
||
|
||
### Schritt 8: Strukturerkennung
|
||
|
||
| Methode | Pfad | Beschreibung |
|
||
|---------|------|--------------|
|
||
| `POST` | `/sessions/{id}/detect-structure` | Boxen, Zonen, Farben und Grafiken erkennen |
|
||
| `GET` | `/sessions/{id}/image/structure-overlay` | Overlay mit allen Strukturelementen |
|
||
|
||
### Schritt 9: Korrektur
|
||
|
||
| Methode | Pfad | Beschreibung |
|
||
|---------|------|--------------|
|
||
| `POST` | `/sessions/{id}/llm-review?stream=true` | SSE-Stream Korrektur starten |
|
||
| `POST` | `/sessions/{id}/llm-review/apply` | Ausgewaehlte Korrekturen speichern |
|
||
|
||
### Schritt 10: Rekonstruktion
|
||
|
||
| Methode | Pfad | Beschreibung |
|
||
|---------|------|--------------|
|
||
| `POST` | `/sessions/{id}/reconstruction` | Zellaenderungen speichern |
|
||
| `GET` | `/sessions/{id}/reconstruction/fabric-json` | Fabric.js Canvas-Daten |
|
||
| `GET` | `/sessions/{id}/reconstruction/export/pdf` | PDF-Export (reportlab) |
|
||
| `GET` | `/sessions/{id}/reconstruction/export/docx` | DOCX-Export (python-docx) |
|
||
| `POST` | `/sessions/{id}/reconstruction/detect-images` | Bildbereiche per VLM erkennen |
|
||
| `POST` | `/sessions/{id}/reconstruction/generate-image` | Bild per mflux generieren |
|
||
| `POST` | `/sessions/{id}/reconstruction/validate` | Validierung speichern (Step 10) |
|
||
| `GET` | `/sessions/{id}/reconstruction/validation` | Validierungsdaten abrufen |
|
||
|
||
### Grid Editor (Excel-aehnlich)
|
||
|
||
| Methode | Pfad | Beschreibung |
|
||
|---------|------|--------------|
|
||
| `POST` | `/sessions/{id}/build-grid` | Strukturiertes Grid aus Kombi-Wortdaten erstellen |
|
||
| `POST` | `/sessions/{id}/save-grid` | Bearbeitetes Grid speichern |
|
||
| `GET` | `/sessions/{id}/grid-editor` | Grid-Editor-Daten abrufen |
|
||
|
||
---
|
||
|
||
## Schritt 4: Zuschneiden/Crop (Detail)
|
||
|
||
### Warum Crop nach Deskew/Dewarp?
|
||
|
||
In frueheren Versionen lief Crop als Schritt 2 (vor Deskew). Das fuehrte zu Problemen:
|
||
|
||
- **Schiefes Bild**: `boundingRect` einer schiefen Seite schliesst viel Scanner-Hintergrund ein
|
||
- **Buchscans**: Otsu-Binarisierung versagt bei weiss-auf-weiss (Seite auf weissem Scanner)
|
||
- **Buchruecken**: Gradueller Schatten-Uebergang wird nicht als Kante erkannt
|
||
|
||
**Loesung (v4.1):** Crop laeuft jetzt nach Dewarp — das Bild ist dann gerade.
|
||
|
||
### Algorithmus: Content-basierte 4-Kanten-Erkennung
|
||
|
||
Datei: `page_crop.py`
|
||
|
||
```
|
||
Input: Entzerrtes BGR-Bild
|
||
│
|
||
├─ Adaptive Threshold (Gauss, blockSize=51)
|
||
│ → binary (Text=255, Hintergrund=0)
|
||
│
|
||
├─ Linker Rand (Buchruecken-Schatten):
|
||
│ 1. Grauwert-Spaltenmittel in linken 25%
|
||
│ 2. Glaetten mit Boxcar-Kernel
|
||
│ 3. Transition hell→dunkel finden (> 60% des Helligkeitsbereichs)
|
||
│ 4. Fallback: Binaere Vertikal-Projektion
|
||
│
|
||
├─ Rechter Rand: Binaere Vertikal-Projektion (letzte Ink-Spalte)
|
||
│
|
||
├─ Oben/Unten: Binaere Horizontal-Projektion (erste/letzte Ink-Zeile)
|
||
│
|
||
├─ Rausch-Filter: Runs < 0.5% der Dimension ignorieren
|
||
│
|
||
├─ Sanity-Checks:
|
||
│ - Mindestens eine Kante > 2% Border
|
||
│ - Crop-Flaeche >= 40% des Originals
|
||
│
|
||
└─ Crop + konfigurierbarer Rand (default 1%)
|
||
```
|
||
|
||
### Vergleich alt vs. neu
|
||
|
||
| Eigenschaft | Alt (Otsu + Kontur) | Neu (Content-basiert) |
|
||
|-------------|--------------------|-----------------------|
|
||
| Binarisierung | Otsu (global) | Adaptive Threshold |
|
||
| Methode | Groesste Kontur → boundingRect | 4-Kanten Ink-Projektion |
|
||
| Buchruecken | Nicht erkannt | Schatten-Gradient-Erkennung |
|
||
| Weiss-auf-weiss | Versagt | Funktioniert (adaptive) |
|
||
| Format-Matching | A4/Letter erzwungen | Kein Format-Matching (Content-Bounds) |
|
||
| Position in Pipeline | Vor Deskew (Schritt 2) | Nach Dewarp (Schritt 4) |
|
||
|
||
---
|
||
|
||
## Schritt 3: Entzerrung/Dewarp (Detail)
|
||
|
||
### Algorithmus: Vertikalkanten-Drift
|
||
|
||
Die Dewarp-Erkennung misst die **vertikale Spaltenkippung** (dx/dy) statt Textzeilen-Neigung:
|
||
|
||
1. Woerter werden nach X-Position in vertikale Spaltencluster gruppiert
|
||
2. Pro Cluster: Lineare Regression `x = a*y + b` → `a = dx/dy = tan(shear_angle)`
|
||
3. Ensemble aus drei Methoden: Textzeilen (1.5× Gewicht), Projektionsprofil (2-Pass), Vertikalkanten
|
||
4. Qualitaetspruefung: Horizontale Projektionsvarianz vor/nach Korrektur
|
||
|
||
**Schwellenwerte:**
|
||
|
||
| Parameter | Wert | Beschreibung |
|
||
|-----------|------|--------------|
|
||
| Min. Korrekturwinkel | 0.08° | Unter 0.08° wird nicht korrigiert |
|
||
| Ensemble Min-Confidence | 0.35 | Mindest-Konfidenz fuer Korrektur |
|
||
| Quality-Gate Skip | < 0.5° | Kleine Korrekturen ueberspringen Quality-Gate |
|
||
|
||
### Feinabstimmung (Combined Adjust)
|
||
|
||
Der Endpoint `POST /sessions/{id}/adjust-combined` erlaubt die kombinierte Feinabstimmung von
|
||
Rotation und Shear in einem Schritt. Im Frontend stehen **7 Schieberegler** zur Verfuegung:
|
||
|
||
**Rotation (3 Paesse):**
|
||
|
||
| Slider | Bereich | Beschreibung |
|
||
|--------|---------|--------------|
|
||
| P1 Iterative | ±5° | Erster Deskew-Pass (Hough Lines) |
|
||
| P2 Word-Alignment | ±3° | Zweiter Pass (Wort-Ausrichtung) |
|
||
| P3 Textline | ±3° | Dritter Pass (Textzeilen-Regression) |
|
||
|
||
Die Summe aller drei ergibt den finalen Rotationswinkel.
|
||
|
||
**Shear (4 Methoden, Radio-Auswahl):**
|
||
|
||
| Slider | Bereich | Beschreibung |
|
||
|--------|---------|--------------|
|
||
| A: Textline Drift | ±5° | Textzeilen-Drift |
|
||
| B: Projection Profile | ±5° | 2-Pass Projektionsprofil |
|
||
| C: Vertical Edges | ±5° | Vertikalkanten-Analyse |
|
||
| D: Ensemble | ±5° | Gewichteter Ensemble-Wert |
|
||
|
||
Nur der per Radio-Button ausgewaehlte Shear-Wert wird verwendet.
|
||
|
||
```
|
||
POST /sessions/{id}/adjust-combined
|
||
Body: {"rotation_degrees": 1.23, "shear_degrees": -0.45}
|
||
Response: {"method_used": "manual_combined", "shear_degrees": -0.45, "dewarped_image_url": "..."}
|
||
```
|
||
|
||
---
|
||
|
||
## Schritt 5: Spaltenerkennung (Detail)
|
||
|
||
### Algorithmus: `detect_column_geometry()`
|
||
|
||
Mehrstufige Erkennung: Seite segmentieren, vertikale Projektionsprofile finden Luecken, Wort-Bounding-Boxes validieren.
|
||
|
||
```
|
||
Bild → Binarisierung → Seiten-Segmentierung → Vertikalprofil → Lueckenerkennung → Wort-Validierung → ColumnGeometry
|
||
```
|
||
|
||
**Wichtige Implementierungsdetails:**
|
||
|
||
- **Initialer Tesseract-Scan:** Laeuft auf der vollen Bildbreite `[left_x : w]` (nicht nur bis zur Content-Grenze `right_x`), damit Woerter am rechten Rand der letzten Spalte nicht uebersehen werden.
|
||
- **Letzte Spalte:** Wird immer bis zur vollen Bildbreite `w` ausgedehnt, nicht nur bis zur erkannten Content-Grenze.
|
||
- **Phantom-Spalten-Filter (Step 9):** Spalten mit Breite < 3 % der Content-Breite UND < 3 Woerter werden als Artefakte entfernt; die angrenzenden Spalten schliessen die Luecke.
|
||
- **Spaltenzuweisung:** Woerter werden anhand des groessten horizontalen Ueberlappungsbereichs einer Spalte zugeordnet.
|
||
|
||
### Seiten-Segmentierung an Sub-Headern
|
||
|
||
Farbige Zwischenueberschriften (z.B. „Unit 4: Bonnie Scotland" mit blauem Hintergrund)
|
||
erzeugen nach Binarisierung Tinte ueber die gesamte Seitenbreite. Diese Baender fuellen
|
||
Spaltenluecken im vertikalen Projektionsprofil auf und fuehren zu fragmentierten Spalten
|
||
(z.B. 11 statt 5).
|
||
|
||
**Loesung: Horizontale Gap-Segmentierung (Step 2b)**
|
||
|
||
1. **Horizontales Projektionsprofil** berechnen: Zeilensummen ueber den Content-Bereich
|
||
2. **Leere Zeilen** erkennen: Zeilen mit < 2% Tinten-Dichte (`H_GAP_THRESH = 0.02`)
|
||
3. **Gaps sammeln**: Zusammenhaengende leere Zeilen zu Gaps buendeln (Mindestlaenge: `max(5, h/200)`)
|
||
4. **Grosse Gaps identifizieren**: Gaps > 1.8× Median-Gap-Hoehe = Sub-Header-Trennungen
|
||
5. **Segmente bilden**: Seite an grossen Gaps aufteilen
|
||
6. **Groesstes Segment waehlen**: Das hoechste Segment wird fuer die vertikale Projektion verwendet
|
||
|
||
```
|
||
┌─────────────────────────────────┐
|
||
│ Header / Titel │ ─── grosser Gap ───
|
||
├─────────────────────────────────┤
|
||
│ EN │ DE │ Example │ Page │ ← Segment 1 (groesster)
|
||
│ ... │ ... │ ... │ ... │
|
||
├─────────────────────────────────┤
|
||
│ Unit 4: Bonnie Scotland │ ─── grosser Gap ───
|
||
├─────────────────────────────────┤
|
||
│ EN │ DE │ Example │ Page │ ← Segment 2
|
||
│ ... │ ... │ ... │ ... │
|
||
└─────────────────────────────────┘
|
||
```
|
||
|
||
**Segment-gefilterte Wort-Validierung:**
|
||
|
||
Die Wort-Validierung (Step 5) nutzt nur Tesseract-Woerter **innerhalb des gewaehlten Segments**.
|
||
Woerter aus Sub-Header-Bereichen (die die volle Breite einnehmen) werden so ausgeschlossen
|
||
und koennen die Spaltenluecken-Validierung nicht verfaelschen.
|
||
|
||
### Word-Coverage Gap Detection (Fallback)
|
||
|
||
Wenn die pixel-basierte Projektion keine ausreichenden Spaltenluecken findet
|
||
(z.B. bei Seiten mit Illustrationen, die Spaltenluecken teilweise verdecken),
|
||
greift ein Fallback auf Basis der Tesseract-Wort-Bounding-Boxes:
|
||
|
||
1. X-Achse in 2px-Bins aufteilen
|
||
2. Pro Bin zaehlen, wie viele Segment-Woerter ihn ueberdecken
|
||
3. Zusammenhaengende Bins mit 0 Woertern = Gap-Kandidaten
|
||
4. Nur Gaps im inneren 90%-Bereich beruecksichtigen (Raender ignorieren)
|
||
5. Gaps mit Mindestbreite (`max(8px, content_w * 0.5%)`) werden als Spaltenluecken akzeptiert
|
||
|
||
### Sub-Spalten-Erkennung: `_detect_sub_columns()`
|
||
|
||
Erkennt versteckte Sub-Spalten innerhalb breiter Spalten (z.B. Seitenzahl-Spalte links neben EN-Vokabeln).
|
||
|
||
**Algorithmus (Left-Edge Alignment Clustering):**
|
||
|
||
1. Fuer jede Spalte mit `width_ratio >= 0.15` und `word_count >= 5`:
|
||
2. Left-Edges aller Woerter mit `conf >= 30` sammeln
|
||
3. In Alignment-Bins clustern (8px Toleranz)
|
||
4. Linkester Bin mit >= 10% der Woerter = wahrer Spaltenanfang
|
||
5. Woerter links davon = Sub-Spalte, wenn >= 2 und < 35% Anteil
|
||
6. Neue ColumnGeometry-Objekte mit korrekten Indizes erzeugen
|
||
|
||
**Koordinatensystem:** Word `left`-Werte sind relativ zum Content-ROI (`left_x`), `ColumnGeometry.x` ist absolut. `left_x` wird als Parameter durchgereicht.
|
||
|
||
### Spalten-Erweiterung: `expand_narrow_columns()`
|
||
|
||
Laeuft **nach** `_detect_sub_columns()`. Erweitert sehr schmale Spalten (< 10% Content-Breite,
|
||
z.B. `page_ref`, `marker`) in den Weissraum zum Nachbar-Spalte hinein, aber nie ueber die
|
||
naechsten Woerter im Nachbarn hinaus (4px Sicherheitsabstand).
|
||
|
||
### Spaltentyp-Klassifikation: `classify_column_types()`
|
||
|
||
| Spaltentyp | Beschreibung | Erkennung |
|
||
|------------|--------------|-----------|
|
||
| `column_en` | Englische Vokabeln | EN-Funktionswoerter (the, a, is...) |
|
||
| `column_de` | Deutsche Uebersetzung | DE-Funktionswoerter (der, die, das...) |
|
||
| `column_example` | Beispielsaetze | Abkuerzungen, Grammatik-Marker |
|
||
| `page_ref` | Seitenzahlen | Schmal (< 20% Breite), wenige Woerter |
|
||
| `column_marker` | Dekorative Markierungen | Sehr schmal, spezielle Zeichen |
|
||
| `column_text` | Generischer Text | Fallback |
|
||
|
||
### Konfigurierbare Parameter
|
||
|
||
```python
|
||
# Mindestbreite fuer echte Spalten (automatisch: max(20px, 3% content_w))
|
||
min_real_col_w = max(20, int(content_w * 0.03))
|
||
```
|
||
|
||
---
|
||
|
||
## Schritt 6: Zeilenerkennung (Detail)
|
||
|
||
### Algorithmus: `detect_row_geometry()`
|
||
|
||
Horizontale Projektionsprofile finden Zeilen-Luecken; word-level Validierung verhindert Fehlschnitte.
|
||
|
||
**Zusaetzliche Post-Processing-Schritte:**
|
||
|
||
1. **Artefakt-Zeilen entfernen** (`_is_artifact_row`):
|
||
Zeilen, in denen alle erkannten Tokens nur 1 Zeichen lang sind (Scan-Schatten, leere Zeilen),
|
||
werden als Artefakte klassifiziert und aus dem Grid entfernt.
|
||
|
||
2. **Luecken-Heilung** (`_heal_row_gaps`):
|
||
Nach dem Entfernen leerer/Artefakt-Zeilen werden die verbleibenden Zeilen auf die Mitte
|
||
der entstehenden Luecke ausgedehnt, damit kein Zeileninhalt durch schrumpfende Grenzen
|
||
abgeschnitten wird.
|
||
|
||
3. **Box-Boundary-Schutz** (`box_ranges_inner`, neu in v4.2):
|
||
Bei Seiten mit Box-Zonen (Sub-Sessions) werden Zeilen am Box-Rand nicht faelschlich
|
||
ausgeschlossen. Das Problem: Die letzte Textzeile ueber einer Box ueberlappt haeufig
|
||
mit dem Box-Rahmen. Loesung: Die Exclusion-Zone wird um `max(border_thickness, 5px)`
|
||
geschrumpft, sodass nur Zeilen **innerhalb** der Box ausgeschlossen werden.
|
||
|
||
```python
|
||
def _is_artifact_row(row: RowGeometry) -> bool:
|
||
"""Zeile ist Artefakt wenn alle Tokens <= 1 Zeichen."""
|
||
if row.word_count == 0: return True
|
||
return all(len(w.get('text','').strip()) <= 1 for w in row.words)
|
||
|
||
def _heal_row_gaps(rows, top_bound, bottom_bound):
|
||
"""Verbleibende Zeilen auf Mitte der Luecken ausdehnen."""
|
||
...
|
||
```
|
||
|
||
### Box-Zonen und Content-Strips (Detail)
|
||
|
||
Seiten mit Box-Bereichen (z.B. Grammatik-Tipps, Uebungsboxen) werden in Zonen aufgeteilt:
|
||
|
||
```
|
||
┌──────────────────────────┐
|
||
│ Content Zone 0 (Zeilen) │ ← Vokabeltabelle oben
|
||
├──────────────────────────┤
|
||
│ ███ Box Zone (border) ███│ ← Sub-Session mit eigener OCR
|
||
├──────────────────────────┤
|
||
│ Content Zone 2 (Zeilen) │ ← Vokabeltabelle unten
|
||
└──────────────────────────┘
|
||
```
|
||
|
||
**Content-Strip-Verfahren** (`detect_rows` in `ocr_pipeline_api.py`):
|
||
|
||
1. Box-Zonen identifizieren, `box_ranges_inner` berechnen (geschrumpft um Border-Dicke)
|
||
2. Content-Strips = Seitenbereiche **ohne** Box-Inneres, vertikal gestapelt
|
||
3. Zeilenerkennung auf gestapeltem Bild, Y-Koordinaten zurueckgemappt
|
||
4. Wort-Filterung: Woerter in Box-Innerem werden ausgeschlossen
|
||
|
||
**Wichtig:** `box_ranges_inner` (nicht `box_ranges`) wird verwendet, damit
|
||
Zeilen am Box-Rand nicht abgeschnitten werden. Minimum 5px Margin.
|
||
|
||
---
|
||
|
||
## Schritt 7: Worterkennung (Detail)
|
||
|
||
Schritt 7 bietet zwei Grid-Strategien, auswaehlbar per `grid_method`-Parameter:
|
||
|
||
| Strategie | Parameter | Ansatz | Benoetigt Spalten/Zeilen? |
|
||
|-----------|-----------|--------|--------------------------|
|
||
| **Hybrid-Grid v2** | `grid_method=v2` (Default) | Top-down: Spalten → Zeilen → Zellen → OCR | Ja (Schritte 5+6) |
|
||
| **Words-First** | `grid_method=words_first` | Bottom-up: Woerter → Spalten clustern → Zeilen clustern → Zellen | Nein |
|
||
|
||
---
|
||
|
||
### Words-First Grid Builder: `build_grid_from_words()`
|
||
|
||
**Datei:** `cv_words_first.py`
|
||
|
||
Der Words-First Builder arbeitet bottom-up: Er nimmt die pixelgenauen `word_boxes` aus einem
|
||
Tesseract Full-Page-Lauf und clustert sie direkt zu Spalten und Zeilen — ohne die
|
||
vorherige Spalten-/Zeilenerkennung (Schritte 5+6) zu benoetigen.
|
||
|
||
#### Algorithmus
|
||
|
||
```
|
||
Eingabe: word_dicts (flat list), img_w, img_h
|
||
│
|
||
┌───────────┴───────────┐
|
||
│ 1. Confidence-Filter │
|
||
│ conf >= 30 │
|
||
│ Whitespace entf. │
|
||
└───────────┬───────────┘
|
||
│
|
||
┌───────────┴───────────┐
|
||
│ 2. _cluster_columns() │
|
||
│ X-Gap-Analyse │
|
||
│ Schwelle: median_h │
|
||
│ × 3 (min 3% Breite)│
|
||
└───────────┬───────────┘
|
||
│
|
||
┌───────────┴───────────┐
|
||
│ 3. _cluster_rows() │
|
||
│ Y-Proximity-Grupp. │
|
||
│ Toleranz: median_h │
|
||
│ / 2 │
|
||
└───────────┬───────────┘
|
||
│
|
||
┌───────────┴───────────┐
|
||
│ 4. _build_cells() │
|
||
│ Wort → (col, row) │
|
||
│ Text + bbox + conf │
|
||
│ word_boxes pro Zelle│
|
||
└───────────┬───────────┘
|
||
│
|
||
Ausgabe: cells[], columns_meta[]
|
||
(identisch zu build_cell_grid_v2)
|
||
```
|
||
|
||
#### Spalten-Clustering
|
||
|
||
1. Alle Woerter nach X-Mitte sortieren
|
||
2. Aufeinanderfolgende X-Gaps berechnen
|
||
3. Adaptiver Schwellwert: `median_word_height × 3` (min 3% Bildbreite)
|
||
4. Gaps > Schwellwert = Spaltengrenzen
|
||
5. Kein Gap gefunden → 1 Spalte (`column_text`)
|
||
6. Spaltentypen: `column_1`, `column_2`, ... (generisch, positionsbasiert)
|
||
|
||
#### Zeilen-Clustering
|
||
|
||
1. Woerter zu visuellen Zeilen gruppieren (Y-Toleranz: halbe Worthoehe)
|
||
2. Jede visuelle Zeile = eine Zeile im Grid
|
||
3. Sortiert von oben nach unten
|
||
|
||
#### Edge Cases
|
||
|
||
| Fall | Behandlung |
|
||
|------|------------|
|
||
| Einzelne Spalte (Fliesstext) | Kein X-Gap → 1 Spalte `column_text` |
|
||
| Keine Woerter erkannt | Leeres Ergebnis `([], [])` |
|
||
| Ueberschriften (grosse Schrift) | Eigene Zeile durch Y-Gap |
|
||
| Bilder/Grafiken | Keine Woerter → automatisch leerer Bereich |
|
||
| Schmale Spalten (Seitenzahlen) | Eigene Spalte durch X-Gap |
|
||
|
||
#### Vergleich v2 vs. Words-First
|
||
|
||
| Kriterium | v2 (Top-Down) | Words-First (Bottom-Up) |
|
||
|-----------|---------------|------------------------|
|
||
| **Abhaengigkeiten** | Spalten + Zeilen noetig | Nur Tesseract-Woerter |
|
||
| **Spaltentypen** | Semantisch (EN, DE, ...) | Positionsbasiert (1, 2, ...) |
|
||
| **OCR** | Hybrid (full-page + cell-crop) | Nur full-page Tesseract |
|
||
| **Robustheit** | Abhaengig von Spalten-/Zeilenerkennung | Direkt aus Wortpositionen |
|
||
| **Geschwindigkeit** | Langsamer (cell-crop pro Zelle) | Schneller (kein OCR-Lauf) |
|
||
| **Genauigkeit** | Besser bei schmalen Spalten | Besser bei ungewoehnlichen Layouts |
|
||
|
||
---
|
||
|
||
### Hybrid-Grid v2: `build_cell_grid_v2()`
|
||
|
||
Schritt 7 nutzt im Default eine **Hybrid-Strategie**: Breite Spalten verwenden die Full-Page-Tesseract-Woerter,
|
||
schmale Spalten werden isoliert per Cell-Crop OCR verarbeitet.
|
||
|
||
!!! success "Warum Hybrid?"
|
||
Full-Page OCR liefert gute Ergebnisse fuer breite Spalten (Saetze, IPA-Klammern, Interpunktion).
|
||
Aber bei schmalen Spalten (Seitenzahlen, Marker) „bluten" Woerter aus Nachbar-Spalten ein.
|
||
Cell-Crop isoliert jede Zelle und verhindert dieses Bleeding.
|
||
|
||
### Broad vs. Narrow — Die 15%-Schwelle
|
||
|
||
```python
|
||
_NARROW_COL_THRESHOLD_PCT = 15.0 # cv_vocab_pipeline.py
|
||
```
|
||
|
||
| Eigenschaft | Breite Spalten (>= 15%) | Schmale Spalten (< 15%) |
|
||
|-------------|------------------------|------------------------|
|
||
| **OCR-Quelle** | Full-Page Tesseract (vorher gelaufen) | Isolierter Cell-Crop |
|
||
| **Wort-Zuweisung** | `_assign_row_words_to_columns()` | Direktes Zell-OCR |
|
||
| **Confidence-Filter** | `conf >= 30` | `conf >= 30` |
|
||
| **Text-Bereinigung** | `_clean_cell_text()` (mittel) | `_clean_cell_text_lite()` (aggressiv) |
|
||
| **Neighbour-Bleeding** | Risiko vorhanden | Verhindert (isoliert) |
|
||
| **Parallelisierung** | Sequentiell | Parallel (`max_workers=4`) |
|
||
| **OCR-Engine Label** | `word_lookup` | `cell_crop_v2` |
|
||
| **Typische Spalten** | EN-Vokabeln, DE-Uebersetzung, Beispielsaetze | Seitenzahlen, Marker |
|
||
|
||
**Empirische Grundlage:** Typische breite Spalten liegen bei 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<string, WordPosition[]>
|
||
```
|
||
|
||
**Algorithmus:**
|
||
|
||
1. Bild in offscreen Canvas laden (optional 180° gedreht)
|
||
2. Pro Zelle: `getImageData()` → vertikale Projektion (dunkle Pixel pro Spalte)
|
||
3. Cluster-Erkennung (Schwelle: 3% der Zellhoehe, Gap: 2% der Zellbreite)
|
||
4. Bei Rotation: Cluster zurueck ins Original-Koordinatensystem spiegeln
|
||
5. Text-Gruppen (split bei 3+ Leerzeichen) auf Cluster matchen
|
||
6. Auto-Schriftgroesse per `measureText()` + `fontRatio`
|
||
7. Mode-Normalisierung: Haeufigste `fontRatio` (gerundet auf 0.02) auf alle anwenden
|
||
|
||
**Rueckgabe:** `Map<cell_id, WordPosition[]>` mit `xPct`, `wPct`, `yPct`, `hPct`, `text`, `fontRatio`
|
||
|
||
!!! note "Per-Word Y-Positionierung (v4.3.1)"
|
||
`WordPosition` enthaelt seit v4.3.1 auch `yPct` und `hPct`. Dadurch rendert jedes
|
||
Wort an seiner tatsaechlichen vertikalen Position, statt alle Woerter einer Zelle
|
||
auf der Zell-Mitte zu stapeln. Bei Zellen ohne `word_boxes` (Fallback) werden
|
||
`yPct`/`hPct` aus `cell.bbox_pct` uebernommen.
|
||
|
||
### Fabric.js Editor
|
||
|
||
Erweiterter Canvas-Editor (`FabricReconstructionCanvas.tsx`):
|
||
|
||
- Drag & Drop fuer Zellen
|
||
- Freie Positionierung auf dem Canvas
|
||
- Export als PDF (reportlab) oder DOCX (python-docx)
|
||
|
||
```
|
||
POST /sessions/{id}/reconstruction
|
||
Body: {"cells": [{"cell_id": "r5_c2", "text": "corrected text"}]}
|
||
```
|
||
|
||
---
|
||
|
||
## Grid Editor (Detail)
|
||
|
||
Der Grid Editor baut aus den Kombi-Wortdaten (PaddleOCR + Tesseract) ein strukturiertes,
|
||
Excel-aehnliches Grid mit Zonen, Spalten, Zeilen und Zellen. Er ersetzt die manuelle
|
||
Rekonstruktion fuer Vokabelseiten mit komplexen Layouts (Bilder, Ueberschriften, IPA-Lautschrift).
|
||
|
||
**Dateien:**
|
||
|
||
| Datei | Beschreibung |
|
||
|-------|--------------|
|
||
| `grid_editor_api.py` | `_build_grid_core()` Pipeline, alle Steps |
|
||
| `grid_editor_helpers.py` | `_filter_footer_words()` → Seitenzahl-Extraktion, Footer-Filterung |
|
||
| `cv_syllable_detect.py` | Deutsche Silbentrennung mit IPA-Kompatibilitaet |
|
||
| `cv_ocr_engines.py` | IPA-Korrektur, Britfone-Woerterbuch, Garbled-IPA-Erkennung |
|
||
| `cv_vocab_types.py` | `PageZone` (mit `image_overlays`), `ColumnGeometry` |
|
||
| `tests/test_grid_editor_api.py` | 27 Tests |
|
||
|
||
### `_build_grid_core()` Pipeline
|
||
|
||
Die Grid-Erstellung laeuft in mehreren Steps:
|
||
|
||
```
|
||
Kombi-Wortdaten
|
||
│
|
||
├─ Zone Merging: Content-Zonen durch Bilder zusammenfuegen
|
||
│ → _merge_content_zones_across_boxes()
|
||
│
|
||
├─ Step 1: Zonen-Grid-Berechnung
|
||
│ → Spalten (Column Union), Zeilen, Zellen pro Zone
|
||
│
|
||
├─ Step 2: Header-Zeilen erkennen + mergen
|
||
│
|
||
├─ Step 3: Ghost-Filter
|
||
│ → _filter_border_ghosts(): Einzel-Char-Artefakte an Box-Raendern
|
||
│
|
||
├─ Step 4: Farb-Annotation
|
||
│ → detect_word_colors(): HSV-Farbanalyse aller word_boxes
|
||
│
|
||
├─ Step 4b2: Per-Cell Artifact Filter
|
||
│ → Einzel-Wort-Zellen mit ≤2 Zeichen und conf < 65 entfernen
|
||
│
|
||
├─ Step 4c: Oversized Word Box Removal
|
||
│ → word_boxes > 3x Median entfernen (Grafik-Artefakte)
|
||
│
|
||
├─ Step 4d2: Connector Column Normalization
|
||
│ → Dominante Kurzwoerter in schmalen Spalten normalisieren
|
||
│
|
||
├─ Step 5: Overlay-Wort-Filter
|
||
│ → Woerter innerhalb image_overlays entfernen
|
||
│
|
||
├─ Step 5a: Heading Detection by Color + Height
|
||
│ → _detect_heading_rows_by_color(): Farbige Ueberschriften erkennen
|
||
│
|
||
├─ Step 5b: Fix Unmatched Parentheses
|
||
│
|
||
├─ Step 5c: IPA Phonetic Correction
|
||
│ → fix_cell_phonetics(): Garbled OCR-IPA durch Britfone ersetzen
|
||
│
|
||
└─ Step 5d: IPA Continuation Detection
|
||
→ fix_ipa_continuation_cell(): Reine Klammer-Zellen als IPA-Fortsetzung
|
||
```
|
||
|
||
### Zone Merging Across Images
|
||
|
||
`_merge_content_zones_across_boxes()` erkennt das Muster `[content, box, content]` und
|
||
fuegt die Content-Zonen zu einer einzigen Zone zusammen. Die Box-Zonen werden als
|
||
`image_overlays` auf der gemergten Zone gespeichert.
|
||
|
||
```
|
||
Vorher: Zone 1 (content) → Zone 2 (box/Bild) → Zone 3 (content)
|
||
Nachher: Zone 1 (content, mit image_overlays=[Zone 2])
|
||
```
|
||
|
||
**PageZone.image_overlays:**
|
||
|
||
```json
|
||
[{"y": 120, "height": 85, "x": 50, "width": 400, "box": {...}}]
|
||
```
|
||
|
||
Box-Zonen die NICHT zwischen Content-Zonen liegen (z.B. "VOCABULARY" Header-Box)
|
||
bleiben als eigenstaendige Zone erhalten.
|
||
|
||
### Heading Detection by Color + Height
|
||
|
||
`_detect_heading_rows_by_color()` erkennt farbige Zwischenueberschriften
|
||
(z.B. "Unit 4: Bonnie Scotland") nach der Farb-Annotation:
|
||
|
||
1. **Farbkriterium:** ALLE word_boxes der Zeile haben `color_name != 'black'` (typisch: blau)
|
||
2. **Hoehenkriterium:** Mittlere Worthoehe > 1.2× Median aller Woerter in der Zone
|
||
|
||
Erkannte Headings werden zu einer Spanning-Cell zusammengefuegt (`col_type: 'heading'`).
|
||
|
||
### Ghost-Filter
|
||
|
||
`_filter_border_ghosts()` entfernt einzelne Zeichen (a-z, A-Z, 0-9) die nahe an
|
||
Box-Raendern erkannt werden — typische OCR-Artefakte von Rahmenlinien.
|
||
|
||
**Ausnahme:** Borderless Boxes (`border_thickness = 0`) ueberspringen den Ghost-Filter,
|
||
da dort keine Rahmen-Artefakte entstehen.
|
||
|
||
### IPA Phonetic Correction (Step 5c)
|
||
|
||
PaddleOCR erkennt IPA-Symbole als ASCII-Zeichen:
|
||
|
||
| IPA-Symbol | OCR liest |
|
||
|------------|-----------|
|
||
| `ʒ` | `3` |
|
||
| `ʃ` | `f` |
|
||
| `ə` | `9` |
|
||
| `ˈ` | `'` |
|
||
| `ɪ` | `i` oder `1` |
|
||
|
||
**`fix_cell_phonetics()`** ersetzt garbled OCR-IPA durch korrekte Lautschrift aus dem
|
||
Britfone-Woerterbuch (British English). Nur Zellen mit `col_type == 'column_en'` werden
|
||
verarbeitet.
|
||
|
||
**`_text_has_garbled_ipa()`** erkennt garbled IPA anhand von:
|
||
|
||
- Typischen OCR-IPA-Zeichen: `' 3 9 @ : ;`
|
||
- Bracket-Notation ohne echte IPA-Symbole: `[n, nn]`, `[1uedtX,1]`
|
||
|
||
### IPA Continuation Detection (Step 5d)
|
||
|
||
Vokabelseiten haben manchmal Zeilen, die nur Lautschrift enthalten (Fortsetzung
|
||
des Headwords der vorherigen Zeile). Diese werden von PaddleOCR als garbled Text erkannt.
|
||
|
||
**`fix_ipa_continuation_cell()`:**
|
||
|
||
1. Prueft ob die Zelle **komplett in Klammern** steht (`[...]`) — Zellen wie
|
||
"employee [im'ploi:]" werden NICHT ueberschrieben
|
||
2. Sucht das Headword in der Zeile darueber
|
||
3. Stripped Grammatik-Annotationen: `[sth.]`, `(adj.)` etc.
|
||
4. Schlaegt IPA im Britfone-Woerterbuch nach
|
||
5. Beruecksichtigt alle Wortteile (z.B. "close sth. down" → `[klˈəʊz dˈaʊn]`)
|
||
|
||
### Per-Cell Artifact Filter (Step 4b2)
|
||
|
||
Entfernt OCR-Rauschen auf Zellebene: Zellen mit genau einer `word_box`, maximal 2 Zeichen
|
||
und Confidence unter 65 werden als Artefakte klassifiziert und entfernt.
|
||
|
||
**Konstanten:**
|
||
|
||
| Parameter | Wert | Beschreibung |
|
||
|-----------|------|--------------|
|
||
| `_ARTIFACT_MAX_LEN` | 2 | Maximale Textlaenge fuer Artefakt-Verdacht |
|
||
| `_ARTIFACT_CONF_THRESHOLD` | 65 | Confidence-Schwelle (darunter = Artefakt) |
|
||
|
||
**Sicherheit:** Einzelne Zeichen mit hoher Confidence (z.B. rote `!`-Marker mit conf=98)
|
||
werden **nicht** entfernt, da ihre Confidence ueber dem Schwellwert liegt.
|
||
|
||
**Typische Artefakte:** `(as)` conf=55, `u)` conf=44 — OCR-Noise aus Seitenraendern
|
||
oder Schatten.
|
||
|
||
### Connector Column Normalization (Step 4d2)
|
||
|
||
Erkennt schmale Spalten mit einem dominanten Kurzwort (z.B. "oder", "and", "bzw.")
|
||
und normalisiert OCR-Fehler bei denen das dominante Wort mit Rauschen versehen wurde.
|
||
|
||
**Algorithmus:**
|
||
|
||
1. Pro Spalte: Zaehle Textvorkommen aller Zellen
|
||
2. Pruefe ob ein dominantes Wort existiert (≥ 60% der Zellen, max 10 Zeichen)
|
||
3. Fuer Zellen die mit dem dominanten Wort **beginnen** und max 2 Zeichen laenger sind:
|
||
Normalisiere auf das dominante Wort
|
||
|
||
**Beispiel:** Spalte mit "oder" in 80% der Zellen → `"oderb"` wird zu `"oder"` normalisiert.
|
||
|
||
### Compound Word IPA Decomposition (Step 5e)
|
||
|
||
Zusammengesetzte Woerter wie "schoolbag" oder "blackbird" haben oft keinen eigenen
|
||
IPA-Eintrag im Woerterbuch. Die Funktion `_decompose_compound()` zerlegt sie:
|
||
|
||
1. Probiere jede Teilungsposition (min. 3 Zeichen pro Teil)
|
||
2. Wenn beide Teile im Woerterbuch stehen → IPA verketten
|
||
3. Waehle die Teilung mit dem laengsten ersten Teil
|
||
|
||
**Beispiele:**
|
||
|
||
| Eingabe | Zerlegung | IPA |
|
||
|---------|-----------|-----|
|
||
| schoolbag | school + bag | skˈuːl + bæɡ |
|
||
| blackbird | black + bird | blæk + bˈɜːd |
|
||
| ice-cream | ice + cream | aɪs + kɹˈiːm |
|
||
|
||
### Trailing Garbled Fragment Removal (Step 5f)
|
||
|
||
Nach korrekt erkanntem IPA (z.B. `seat [sˈiːt]`) haengt OCR manchmal
|
||
eine garbled Kopie der IPA-Transkription an: `seat [sˈiːt] belt si:t belt`.
|
||
|
||
**`_strip_post_bracket_garbled()`** erkennt und entfernt diese:
|
||
|
||
1. Alles nach dem letzten `]` scannen
|
||
2. Woerter mit IPA-Markern (`:`, `ə`, `ɪ` etc.) → garbled, entfernen
|
||
3. Echte Woerter (Woerterbuch, Deutsch, Delimiter) → behalten
|
||
4. **Multi-Wort-Headword:** "belt" ist ein echtes Wort, aber wenn danach
|
||
garbled IPA kommt, wird nur "belt" behalten, der Rest entfernt
|
||
|
||
### Regression Framework (Step 5g)
|
||
|
||
Ground-Truth Sessions koennen als Referenz markiert werden. Nach jeder
|
||
Code-Aenderung vergleicht `POST /regression/run` die aktuelle Pipeline-Ausgabe
|
||
mit den gespeicherten Referenzen:
|
||
|
||
- **Strukturelle Diffs:** Zonen, Spalten, Zeilen (Anzahl-Aenderungen)
|
||
- **Zellen-Diffs:** Text-Aenderungen, fehlende/neue Zellen, col_type-Aenderungen
|
||
- **Persistenz:** Ergebnisse in `regression_runs` Tabelle fuer Trend-Analyse
|
||
- **Shell-Script:** `scripts/run-regression.sh` fuer CI-Integration
|
||
|
||
Admin-UI: [/ai/ocr-regression](https://macmini:3002/ai/ocr-regression)
|
||
|
||
### Ground Truth Review Workflow (Step 5h)
|
||
|
||
Admin-UI fuer effiziente Massenpruefung von Sessions:
|
||
|
||
- **Split-View:** Original-Bild links, erkannter Grid rechts
|
||
- **Confidence-Highlighting:** Niedrige Konfidenz rot hervorgehoben
|
||
- **Quick-Accept:** Korrekte Zeilen mit einem Klick bestaetigen
|
||
- **Inline-Edit:** Text direkt im Grid korrigieren
|
||
- **Session-Queue:** Automatisch naechste Session laden
|
||
- **Batch-Mark:** Mehrere Sessions gleichzeitig als Ground Truth markieren
|
||
|
||
Admin-UI: [/ai/ocr-ground-truth](https://macmini:3002/ai/ocr-ground-truth)
|
||
|
||
### Page Number Extraction
|
||
|
||
Die Footer-Filterung (`_filter_footer_words` in `grid_editor_helpers.py`) erkennt
|
||
Seitenzahlen in den untersten 5% des Bildes und gibt sie als Metadaten zurueck,
|
||
statt sie stillschweigend zu entfernen.
|
||
|
||
**Algorithmus:**
|
||
|
||
1. Woerter in den untersten 5% des Bildes identifizieren
|
||
2. Wenn ≤ 3 Woerter mit ≤ 10 Zeichen Gesamtlaenge: Als Seitenzahl extrahieren
|
||
3. Rueckgabe als `PageNumber`-Objekt: `{text, y_pct, number?}`
|
||
4. Ziffern werden separat als `number` (Integer) extrahiert
|
||
|
||
**Datentyp:**
|
||
|
||
```typescript
|
||
interface PageNumber {
|
||
text: string // Roh-OCR-Text (z.B. "u)233")
|
||
y_pct: number // Vertikale Position in Prozent
|
||
number?: number // Extrahierte Zahl (z.B. 233)
|
||
}
|
||
```
|
||
|
||
**Frontend-Anzeige:**
|
||
|
||
In der Summary-Leiste (GridEditor + StepGridReview) als Badge: `S. 233`.
|
||
Zeigt bevorzugt `page_number.number` (saubere Zahl), Fallback auf `page_number.text`.
|
||
|
||
**Zweck:** Spaetere Zusammenfuehrung aufeinanderfolgender Seiten im Kundenfrontend.
|
||
|
||
### Footer-Zeilen-Erkennung (Verbesserung)
|
||
|
||
Die Footer-Erkennung wurde um zwei Pruefungen erweitert, um falsch-positive
|
||
Footer-Markierungen bei Content-Zeilen zu verhindern:
|
||
|
||
| Pruefung | Bedingung | Grund |
|
||
|----------|-----------|-------|
|
||
| Komma-Check | `',' in text` → kein Footer | Content-Saetze enthalten Kommas, Seitenzahlen nicht |
|
||
| Laengen-Check | `len(text) > 20` → kein Footer | Seitenzahlen sind kurz, Content-Zeilen lang |
|
||
|
||
**Vorher:** `"Uhrzeit, Vergangenheit, Zukunft"` wurde als Footer markiert.
|
||
**Nachher:** Nur tatsaechliche Seitenzahlen (kurz, ohne Kommas) werden als Footer erkannt.
|
||
|
||
### Silben + IPA Kombination (Fix)
|
||
|
||
**Datei:** `cv_syllable_detect.py`
|
||
|
||
Wenn beide Modi (Silben:DE und IPA) aktiviert sind, blockierte der `_IPA_RE`-Guard
|
||
die Silbentrennung, weil programmatisch eingefuegte IPA-Klammern (z.B. `[bɪltʃøn]`)
|
||
IPA-Zeichen enthalten.
|
||
|
||
**Loesung:** Vor der IPA-Pruefung wird Bracket-Content entfernt:
|
||
|
||
```python
|
||
# Bracket-Content strippen, da programmatisch eingefuegt
|
||
text_no_brackets = re.sub(r'\[[^\]]*\]', '', text)
|
||
if _IPA_RE.search(text_no_brackets):
|
||
return text # Echte IPA im Fliesstext → keine Silbentrennung
|
||
```
|
||
|
||
So wird `"Bild·chen [bɪltʃøn]"` korrekt silbifiziert: Die Silbenpunkte bleiben erhalten,
|
||
und die IPA in Klammern wird nicht als Blockiergrund gewertet.
|
||
|
||
### `en_col_type` Erkennung
|
||
|
||
Die Erkennung der Englisch-Headword-Spalte nutzt **Bracket-IPA-Pattern-Count**
|
||
statt "laengster Durchschnittstext":
|
||
|
||
1. Fuer jede `column_*`-Spalte: Zaehle Zellen mit `[` im Text
|
||
2. Spalte mit den meisten Klammer-Mustern = `en_col_type`
|
||
3. **Fallback:** Laengster Durchschnittstext (wenn keine Klammern vorhanden)
|
||
|
||
Dies verhindert, dass Beispielsatz-Spalten (laenger aber ohne IPA) faelschlicherweise
|
||
als Headword-Spalte erkannt werden.
|
||
|
||
### Oversized Word Box Removal (Step 4c)
|
||
|
||
Entfernt `word_boxes` deren Flaeche > 3× der Median-Flaeche aller Woerter in der Zone.
|
||
Diese sind typischerweise Grafik-Artefakte (z.B. ein einzelnes "N" das eine ganze
|
||
Illustration abdeckt).
|
||
|
||
### Tests
|
||
|
||
```bash
|
||
cd klausur-service/backend && pytest tests/test_grid_editor_api.py -v # 27 Tests
|
||
```
|
||
|
||
| Klasse | Tests | Beschreibung |
|
||
|--------|-------|--------------|
|
||
| `TestZoneMerging` | 4 | Content-Zone-Merging ueber Box-Zonen |
|
||
| `TestHeadingDetection` | 3 | Farb- + Hoehenbasierte Heading-Erkennung |
|
||
| `TestGhostFilter` | 4 | Border-Ghost-Filterung inkl. Borderless |
|
||
| `TestOversizedWordBoxes` | 2 | Grafik-Artefakt-Entfernung |
|
||
| `TestGarbledIpaDetection` | 8 | Bracket-IPA, Continuation, en_col_type |
|
||
| `TestColumnUnion` | 3 | Spalten-Vereinigung ueber Zonen |
|
||
| `TestHeaderMerging` | 3 | Header-Zeilen zusammenfuegen |
|
||
|
||
---
|
||
|
||
## Wichtige Konstanten
|
||
|
||
| Konstante | Wert | Datei | Beschreibung |
|
||
|-----------|------|-------|--------------|
|
||
| `_NARROW_COL_THRESHOLD_PCT` | 15.0% | cv_vocab_pipeline.py | Schwelle breit/schmal fuer Hybrid-OCR |
|
||
| `_NARROW_THRESHOLD_PCT` | 10.0% | cv_vocab_pipeline.py | Schwelle fuer Spalten-Erweiterung |
|
||
| `_MIN_WORD_CONF` | 30 | cv_vocab_pipeline.py | Mindest-Confidence fuer OCR-Woerter |
|
||
| `_PAD` | 3px | cv_vocab_pipeline.py | Internes Padding bei Cell-Crop |
|
||
| `PDF_ZOOM` | 3.0 | cv_vocab_pipeline.py | PDF-Rendering (= 432 DPI) |
|
||
| `_MIN_WORD_MARGIN` | 4px | cv_vocab_pipeline.py | Sicherheitsabstand bei Spalten-Erweiterung |
|
||
|
||
---
|
||
|
||
## Datenbank-Schema
|
||
|
||
```sql
|
||
CREATE TABLE ocr_pipeline_sessions (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
name VARCHAR(255),
|
||
filename VARCHAR(255),
|
||
status VARCHAR(50) DEFAULT 'active',
|
||
current_step INT DEFAULT 1,
|
||
|
||
-- Dokumenttyp-Erkennung
|
||
doc_type VARCHAR(50), -- 'vocab_table', 'generic_table', 'full_text'
|
||
doc_type_result JSONB, -- Vollstaendiges DetectionResult
|
||
|
||
-- Bilder (BYTEA)
|
||
original_png BYTEA,
|
||
deskewed_png BYTEA,
|
||
binarized_png BYTEA,
|
||
dewarped_png BYTEA,
|
||
|
||
-- Step-Results (JSONB)
|
||
deskew_result JSONB,
|
||
dewarp_result JSONB,
|
||
column_result JSONB,
|
||
row_result JSONB,
|
||
word_result JSONB, -- enthaelt vocab_entries, cells, llm_review
|
||
|
||
-- Ground Truth + Meta
|
||
ground_truth JSONB,
|
||
auto_shear_degrees REAL,
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
updated_at TIMESTAMP DEFAULT NOW()
|
||
);
|
||
```
|
||
|
||
`word_result` JSONB-Struktur:
|
||
|
||
```json
|
||
{
|
||
"vocab_entries": [...],
|
||
"cells": [{"cell_id": "r0_c0", "text": "hello", "bbox_pct": {...}, "ocr_engine": "word_lookup", ...}],
|
||
"columns_used": [...],
|
||
"llm_review": {
|
||
"changes": [{"row_index": 5, "field": "english", "old": "...", "new": "..."}],
|
||
"model_used": "spell",
|
||
"duration_ms": 234
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Abhaengigkeiten
|
||
|
||
### Python (klausur-service)
|
||
|
||
| Paket | Version | Lizenz | Zweck |
|
||
|-------|---------|--------|-------|
|
||
| `pytesseract` | ≥0.3.10 | Apache-2.0 | Haupt-OCR (Schritt 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<br/>Y-Toleranz 12px]
|
||
B --> C[Row Matching<br/>vertikale Naehe]
|
||
C --> D{Gleicher Text<br/>oder Overlap >= 50%?}
|
||
D -->|Ja| E[Deduplizieren:<br/>Paddle-Text + gemittelte Coords]
|
||
D -->|Nein| F{Wort nur bei<br/>einer Engine?}
|
||
F -->|Ja| G[Beibehalten<br/>falls conf >= 30]
|
||
F -->|Nein| H[Beide behalten<br/>verschiedene Positionen]
|
||
```
|
||
|
||
**Koordinaten-Mittelung:**
|
||
|
||
```
|
||
merged_left = (paddle_left × paddle_conf + tess_left × tess_conf) / (paddle_conf + tess_conf)
|
||
```
|
||
|
||
Gleiches Prinzip fuer `top`, `width`, `height`. Der Text kommt immer von PaddleOCR (bessere Texterkennung).
|
||
|
||
**Raeumlicher Overlap-Check (v4.3.1):**
|
||
|
||
Wenn zwei Woerter >= 50% horizontal ueberlappen, werden sie als dasselbe physische Wort behandelt —
|
||
unabhaengig davon, ob die OCR-Texte unterschiedlich sind (z.B. "hello" vs "helo").
|
||
Dies verhindert, dass leicht unterschiedliche Erkennungen als separate Woerter uebereinander
|
||
im Overlay erscheinen.
|
||
|
||
#### Dateien
|
||
|
||
| Datei | Aenderung |
|
||
|-------|-----------|
|
||
| `ocr_pipeline_api.py` | `_split_paddle_multi_words()`, `_group_words_into_rows()`, `_merge_row_sequences()`, `_merge_paddle_tesseract()`, `/paddle-kombi` Endpoint |
|
||
| `admin-lehrer/.../ocr-overlay/types.ts` | `KOMBI_STEPS` Konstante |
|
||
| `admin-lehrer/.../ocr-overlay/useSlideWordPositions.ts` | Slide-Positionierung mit `yPct`/`hPct` |
|
||
| `admin-lehrer/.../ocr-overlay/usePixelWordPositions.ts` | Pixel-Cluster-Positionierung mit `yPct`/`hPct` |
|
||
| `admin-lehrer/.../ocr-overlay/OverlayReconstruction.tsx` | Rendering mit per-Word Y-Positionen |
|
||
| `admin-lehrer/.../PaddleDirectStep.tsx` | Wiederverwendbar mit `endpoint`/`engineKey` Props |
|
||
| `admin-lehrer/.../ocr-overlay/page.tsx` | 3er-Toggle: Pipeline / Paddle Direct / Kombi |
|
||
|
||
#### Tests
|
||
|
||
```bash
|
||
cd klausur-service/backend && pytest tests/test_paddle_kombi.py -v # 36 Tests
|
||
```
|
||
|
||
**Test-Klassen:**
|
||
|
||
| Klasse | Tests | Beschreibung |
|
||
|--------|-------|--------------|
|
||
| `TestSplitPaddleMultiWords` | 7 | Multi-Word-Box-Splitting |
|
||
| `TestGroupWordsIntoRows` | 5 | Y-Position Row Clustering |
|
||
| `TestMergeRowSequences` | 10 | Sequence Alignment innerhalb einer Zeile |
|
||
| `TestMergePaddleTesseract` | 8 | Vollstaendiger Merge mit Row-Matching |
|
||
| `TestMergeRealWorldRegression` | 1 | Regression mit Echtdaten |
|
||
| `TestSpatialOverlapDedup` | 4 | Raeumliche Overlap-Deduplizierung |
|
||
| `TestSplitThenMerge` | 1 | Split + Merge End-to-End |
|
||
|
||
| Testklasse | Tests | Beschreibung |
|
||
|------------|-------|--------------|
|
||
| `TestBoxIoU` | 6 | IoU-Berechnung: identisch, kein Overlap, teilweise, enthalten, Kante, Null-Flaeche |
|
||
| `TestMergePaddleTesseract` | 10 | Merge: Match-Averaging, kein Match, Low-Conf-Drop, leer, IoU-Schwelle, Text-Praeferenz, Zero-Conf |
|
||
| `TestMergePaddleTesseractBulletPoints` | 2 | Bullet-Points und Sonderzeichen von Tesseract |
|
||
|
||
---
|
||
|
||
## ONNX Backends und PP-DocLayout (Sprint 2)
|
||
|
||
### TrOCR ONNX Runtime
|
||
|
||
Ab Sprint 2 unterstuetzt die Pipeline **TrOCR mit ONNX Runtime** als Alternative zu PyTorch.
|
||
ONNX reduziert den RAM-Verbrauch von ~1.1 GB auf ~300 MB pro Modell und beschleunigt
|
||
die Inferenz um ~3x. Ideal fuer Hardware Tier 2 (8 GB RAM).
|
||
|
||
**Backend-Auswahl:** Umgebungsvariable `TROCR_BACKEND` (`auto` | `pytorch` | `onnx`).
|
||
Im `auto`-Modus wird ONNX bevorzugt, wenn exportierte Modelle vorhanden sind.
|
||
|
||
Vollstaendige Dokumentation: [TrOCR ONNX Runtime](TrOCR-ONNX.md)
|
||
|
||
### PP-DocLayout (Document Layout Analysis)
|
||
|
||
PP-DocLayout ersetzt die bisherige manuelle Zonen-Erkennung durch ein vortrainiertes
|
||
Layout-Analyse-Modell. Es erkennt automatisch:
|
||
|
||
- **Tabellen** (vocab_table, generic_table)
|
||
- **Ueberschriften** (title, section_header)
|
||
- **Bilder/Grafiken** (figure, illustration)
|
||
- **Textbloecke** (paragraph, list)
|
||
|
||
PP-DocLayout laeuft als ONNX-Modell (~15 MB) und benoetigt kein PyTorch.
|
||
Die Ergebnisse fliessen in Schritt 5 (Spaltenerkennung) und den Grid Editor ein.
|
||
|
||
---
|
||
|
||
## Aenderungshistorie
|
||
|
||
| Datum | Version | Aenderung |
|
||
|-------|---------|----------|
|
||
| 2026-03-26 | 5.2.0 | **OCR Kombi Pipeline:** Neuer modularer Nachfolger als 11-Schritt-Architektur unter `/ai/ocr-kombi`. Eigene Dokumentation: [OCR Kombi Pipeline](OCR-Kombi-Pipeline.md). Phase 1 (Grundgeruest + DB) implementiert: DB-Migration (`document_group_id`, `page_number`), Frontend-Orchestrator, 13 Step-Komponenten, Backend-Router mit Multi-Page-Upload. |
|
||
| 2026-03-26 | 5.1.0 | **Grid Quality & Metadata:** Per-Cell Artifact Filter (Step 4b2: ≤2 Zeichen + conf < 65), Connector Column Normalization (Step 4d2: dominante Kurzwoerter), Footer-Erkennung verbessert (Komma/Laengen-Check), Seitenzahl-Extraktion als Metadaten (`page_number` Feld im Grid-Result), Frontend-Anzeige in Summary-Leiste. Silben+IPA-Kombination gefixt (Bracket-Content vor IPA-Guard strippen). |
|
||
| 2026-03-23 | 5.0.0 | **Phase 1 Sprint 1:** Compound-IPA-Zerlegung (`_decompose_compound`), Trailing-Garbled-Fragment-Entfernung (Multi-Wort-Headwords), Regression Framework mit DB-Persistenz + History + Shell-Script, Ground-Truth Review Workflow UI, Page-Crop Determinismus verifiziert. Admin-Seiten: `/ai/ocr-regression`, `/ai/ocr-ground-truth`. |
|
||
| 2026-03-20 | 4.7.0 | Grid Editor: Zone Merging ueber Bilder (`image_overlays`), Heading Detection (Farbe + Hoehe), Ghost-Filter (borderless-aware), Oversized Word Box Removal, IPA Phonetic Correction (Britfone), IPA Continuation Detection, `en_col_type` via Bracket-Count. 27 Tests. |
|
||
| 2026-03-16 | 4.6.0 | Strukturerkennung (Schritt 8): Region-basierte Grafikerkennung (`cv_graphic_detect.py`) mit Zwei-Pass-Verfahren (Farbregionen + schwarze Illustrationen), Wort-Ueberlappungs-Filter, Box/Zonen/Farb-Analyse. Schritt laeuft nach Worterkennung. |
|
||
| 2026-03-12 | 4.5.0 | Kombi-Modus (PaddleOCR + Tesseract): Beide Engines laufen parallel, Koordinaten werden IoU-basiert gematcht und confidence-gewichtet gemittelt. Ungematchte Tesseract-Woerter (Bullets, Symbole) werden hinzugefuegt. 3er-Toggle in OCR Overlay. |
|
||
| 2026-03-12 | 4.4.0 | PaddleOCR Remote-Engine (`engine=paddle`): PP-OCRv5 Latin auf Hetzner x86_64. Neuer Microservice (`paddleocr-service/`), HTTP-Client (`paddleocr_remote.py`), Frontend-Dropdown-Option. Nutzt words_first Grid-Methode. |
|
||
| 2026-03-12 | 4.3.0 | Words-First Grid Builder (`cv_words_first.py`): Bottom-up-Algorithmus clustert Tesseract word_boxes direkt zu Spalten/Zeilen/Zellen. Neuer `grid_method` Parameter im `/words` Endpoint. Frontend-Toggle in StepWordRecognition. |
|
||
| 2026-03-10 | 4.2.0 | Rekonstruktion: Overlay-Modus mit Pixel-Wortpositionierung, 180°-Rotation, Sub-Session-Merging, usePixelWordPositions Hook, Box-Boundary-Schutz (box_ranges_inner) |
|
||
| 2026-03-05 | 3.1.0 | Spalten: Seiten-Segmentierung an Sub-Headern, Word-Coverage Fallback, Segment-gefilterte Validierung |
|
||
| 2026-03-05 | 3.0.1 | Dewarp: Feinabstimmung mit 7 Schiebereglern (3 Rotation + 4 Shear), Combined-Adjust-Endpoint |
|
||
| 2026-03-05 | 3.0.0 | Doku-Update: Dokumenttyp-Erkennung, Hybrid-Grid, Sub-Column-Detection, Pipeline-Pfade |
|
||
| 2026-03-04 | 2.2.0 | Dewarp: Vertikalkanten-Drift statt Textzeilen-Neigung, Schwellenwerte gesenkt |
|
||
| 2026-03-04 | 2.1.0 | Sub-Column-Detection, expand_narrow_columns, Fabric.js Editor, PDF/DOCX-Export |
|
||
| 2026-03-03 | 2.0.0 | Schritte 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 |
|