# OCR Pipeline - Schrittweise Seitenrekonstruktion

**Version:** 2.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 Vokabelseiten
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 | Grid aus Spalten x Zeilen, OCR pro Zelle, Post-Processing | Implementiert |
| 6 | Korrektur | Zeichenverwirrung + regel-basierte Rechtschreibkorrektur (SSE-Stream) | Implementiert |
| 7 | Rekonstruktion | Interaktive Zellenbearbeitung auf Bildhintergrund | Implementiert |
| 8 | Validierung | Ground-Truth-Vergleich und Qualitaetspruefung | Implementiert |

---

## 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/
├── ocr_pipeline_api.py              # FastAPI Router (alle Endpoints)
├── ocr_pipeline_session_store.py    # PostgreSQL Persistence
├── cv_vocab_pipeline.py             # Computer Vision + NLP Algorithmen
└── 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)
    └── 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 |

### 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 |

---

## 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.

### 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 (Detail)

### Algorithmus: `build_cell_grid()`

Schritt 5 nutzt die Ergebnisse von Schritt 3 (Spalten) und Schritt 4 (Zeilen), um ein Grid
zu erstellen und jede Zelle per OCR auszulesen.

```
Spalten (Step 3):    column_en  |  column_de  |  column_example
                     ───────────┼─────────────┼────────────────
Zeilen (Step 4):  R0 │  hello   │  hallo      │  Hello, World!
                  R1 │  world   │  Welt       │  The whole world
                  R2 │  book    │  Buch       │  Read a book
                     ───────────┼─────────────┼────────────────
```

**Ablauf:**

1. **Initialer Scan:** Ganzes Bild einmal per Tesseract/RapidOCR → alle Wort-Bboxes
2. **Zuweisung:** Jedes Wort der Spalte mit groesstem horizontalem Ueberlapp zuordnen
3. **Zell-OCR Fallback:** Leere Zellen bekommen eigenen Crop + erneuten OCR-Aufruf (PSM 6/7)
4. **Batch-Spalten-OCR:** Bei vielen leeren Zellen in einer Spalte: gesamte Spalte einmal OCR-en
5. **Post-Processing:** Continuation-Rows zusammenfuehren, Lautschrift erkennen, Komma-Eintraege splitten

### 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 zwei Korrektur-Stufen, beide 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'],
}
```

Logik: Kandidaten werden durch Woerterbuch-Lookup validiert. Strukturregel: Verdaechtiges
Zeichen an Position 0 + Rest klein → erstes Substitut (z.B. `8en` → `Ben`).

### 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)

Interaktiver Canvas-Editor: 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

```
POST /sessions/{id}/reconstruction
Body: {"cells": [{"cell_id": "r5_c2", "text": "corrected text"}]}
```

---

## 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,

    -- 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": {...}, ...}],
  "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) |

!!! info "pyspellchecker (neu seit 2026-03)"
    `pyspellchecker` (MIT-Lizenz) ersetzt die LLM-basierte Korrektur als Standard-Engine.
    EN+DE-Woerterbuch, ~134k Woerter. Kein Ollama notig.
    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 (geplant) |
| Mehr als 4 Spalten | Projektionsprofil kann verschmelzen | Manuelle Spalten |

---

## Deployment

```bash
# 1. Git push
git push origin main

# 2. Mac Mini pull + build
ssh macmini "cd /Users/benjaminadmin/Projekte/breakpilot-lehrer && git pull --no-rebase origin main"

# klausur-service (Backend) — bei requirements.txt Aenderungen: klausur-base neu bauen
ssh macmini "cd /Users/benjaminadmin/Projekte/breakpilot-lehrer && \
  /usr/local/bin/docker compose build klausur-service && \
  /usr/local/bin/docker compose up -d klausur-service"

# admin-lehrer (Frontend)
ssh macmini "cd /Users/benjaminadmin/Projekte/breakpilot-lehrer && \
  /usr/local/bin/docker compose build admin-lehrer && \
  /usr/local/bin/docker compose 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 "cd ~/Projekte/breakpilot-lehrer && \
      /usr/local/bin/docker build -f klausur-service/Dockerfile.base \
      -t klausur-base:latest klausur-service/"
    ```

---

## Aenderungshistorie

| Datum | Version | Aenderung |
|-------|---------|----------|
| 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 |
