# OCR-Labeling System Spezifikation

**Version:** 1.1.0
**Datum:** 2026-01-23
**Status:** In Produktion (Mac Mini)

## Übersicht

Das OCR-Labeling System ermöglicht das Erstellen von Trainingsdaten für Handschrift-OCR-Modelle aus eingescannten Klausuren. Es unterstützt folgende OCR-Modelle:

| Modell | Beschreibung | Geschwindigkeit | Empfohlen für |
|--------|--------------|-----------------|---------------|
| **llama3.2-vision:11b** | Vision-LLM (Standard) | Langsam | Handschrift, beste Qualität |
| **TrOCR** | Microsoft Transformer | Schnell | Gedruckter Text |
| **PaddleOCR + LLM** | Hybrid-Ansatz (NEU) | Sehr schnell (4x) | Gemischte Dokumente |
| **Donut** | Document Understanding (NEU) | Mittel | Tabellen, Formulare |
| **qwen2.5:14b** | Korrektur-LLM | - | Klausurbewertung |

### Neue OCR-Optionen (v1.1.0)

#### PaddleOCR + LLM (Empfohlen für Geschwindigkeit)

PaddleOCR ist ein zweistufiger Ansatz:
1. **PaddleOCR** - Schnelle, präzise Texterkennung mit Bounding-Boxes
2. **qwen2.5:14b** - Semantische Strukturierung des erkannten Texts

**Vorteile:**
- 4x schneller als Vision-LLM (~7-15 Sek vs 30-60 Sek pro Seite)
- Höhere Genauigkeit bei gedrucktem Text (95-99%)
- Weniger Halluzinationen (LLM korrigiert nur, erfindet nicht)
- Position-basierte Spaltenerkennung möglich

**Dateien:**
- `/klausur-service/backend/hybrid_vocab_extractor.py` - PaddleOCR Integration

#### Donut (Document Understanding Transformer)

Donut ist speziell für strukturierte Dokumente optimiert:
- Tabellen und Formulare
- Rechnungen und Quittungen
- Multi-Spalten-Layouts

**Dateien:**
- `/klausur-service/backend/services/donut_ocr_service.py` - Donut Service

## Architektur

```
┌──────────────────────────────────────────────────────────────────────────┐
│                           OCR-Labeling System                             │
├──────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   ┌─────────────┐    ┌─────────────────┐    ┌────────────────────────┐  │
│   │   Frontend  │◄──►│  Klausur-Service │◄──►│    PostgreSQL          │  │
│   │  (Next.js)  │    │   (FastAPI)      │    │  - ocr_labeling_sessions│  │
│   │  Port 3000  │    │   Port 8086      │    │  - ocr_labeling_items   │  │
│   └─────────────┘    └────────┬─────────┘    │  - ocr_training_samples │  │
│                               │              └────────────────────────┘  │
│                               │                                          │
│                    ┌──────────┼──────────┐                              │
│                    ▼          ▼          ▼                              │
│            ┌───────────┐ ┌─────────┐ ┌───────────────┐                  │
│            │   MinIO   │ │ Ollama  │ │ Export Service │                  │
│            │  (Images) │ │ (OCR)   │ │ (Training)     │                  │
│            │ Port 9000 │ │ :11434  │ │                │                  │
│            └───────────┘ └─────────┘ └───────────────┘                  │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘
```

## Datenmodell

### PostgreSQL Tabellen

```sql
-- Labeling Sessions (gruppiert zusammengehörige Bilder)
CREATE TABLE ocr_labeling_sessions (
    id VARCHAR(36) PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    source_type VARCHAR(50) NOT NULL,  -- 'klausur', 'handwriting_sample', 'scan'
    description TEXT,
    ocr_model VARCHAR(100),             -- z.B. 'llama3.2-vision:11b'
    total_items INTEGER DEFAULT 0,
    labeled_items INTEGER DEFAULT 0,
    confirmed_items INTEGER DEFAULT 0,
    corrected_items INTEGER DEFAULT 0,
    skipped_items INTEGER DEFAULT 0,
    teacher_id VARCHAR(100),
    created_at TIMESTAMP DEFAULT NOW()
);

-- Einzelne Labeling Items (Bild + OCR + Ground Truth)
CREATE TABLE ocr_labeling_items (
    id VARCHAR(36) PRIMARY KEY,
    session_id VARCHAR(36) REFERENCES ocr_labeling_sessions(id),
    image_path TEXT NOT NULL,           -- MinIO Pfad oder lokaler Pfad
    image_hash VARCHAR(64),             -- SHA256 für Deduplizierung
    ocr_text TEXT,                      -- Von LLM erkannter Text
    ocr_confidence FLOAT,               -- Konfidenz (0-1)
    ocr_model VARCHAR(100),
    ground_truth TEXT,                  -- Korrigierter/bestätigter Text
    status VARCHAR(20) DEFAULT 'pending', -- pending/confirmed/corrected/skipped
    labeled_by VARCHAR(100),
    labeled_at TIMESTAMP,
    label_time_seconds INTEGER,
    metadata JSONB,
    created_at TIMESTAMP DEFAULT NOW()
);

-- Exportierte Training Samples
CREATE TABLE ocr_training_samples (
    id VARCHAR(36) PRIMARY KEY,
    item_id VARCHAR(36) REFERENCES ocr_labeling_items(id),
    image_path TEXT NOT NULL,
    ground_truth TEXT NOT NULL,
    export_format VARCHAR(50) NOT NULL,  -- 'generic', 'trocr', 'llama_vision'
    exported_at TIMESTAMP DEFAULT NOW(),
    training_batch VARCHAR(100),
    used_in_training BOOLEAN DEFAULT FALSE
);
```

## API Referenz

Base URL: `http://macmini:8086/api/v1/ocr-label`

### Sessions

#### POST /sessions
Neue Labeling-Session erstellen.

**Request:**
```json
{
  "name": "Klausur Deutsch 12a Q1",
  "source_type": "klausur",
  "description": "Gedichtanalyse Expressionismus",
  "ocr_model": "llama3.2-vision:11b"
}
```

**Response:**
```json
{
  "id": "abc-123-def",
  "name": "Klausur Deutsch 12a Q1",
  "source_type": "klausur",
  "total_items": 0,
  "labeled_items": 0,
  "created_at": "2026-01-21T10:30:00Z"
}
```

#### GET /sessions
Sessions auflisten.

**Query Parameter:**
- `limit` (int, default: 50) - Maximale Anzahl

#### GET /sessions/{session_id}
Einzelne Session abrufen.

### Upload

#### POST /sessions/{session_id}/upload
Bilder zu einer Session hochladen.

**Request:** Multipart Form Data
- `files` (File[]) - PNG/JPG/PDF Dateien
- `run_ocr` (bool, default: true) - OCR direkt ausführen
- `metadata` (JSON string) - Optional: Metadaten

**Response:**
```json
{
  "session_id": "abc-123-def",
  "uploaded_count": 5,
  "items": [
    {
      "id": "item-1",
      "filename": "scan_001.png",
      "image_path": "ocr-labeling/abc-123/item-1.png",
      "ocr_text": "Die Lösung der Aufgabe...",
      "ocr_confidence": 0.87,
      "status": "pending"
    }
  ]
}
```

### Labeling Queue

#### GET /queue
Nächste zu labelnde Items abrufen.

**Query Parameter:**
- `session_id` (str, optional) - Nach Session filtern
- `status` (str, default: "pending") - Status-Filter
- `limit` (int, default: 10) - Maximale Anzahl

**Response:**
```json
[
  {
    "id": "item-456",
    "session_id": "abc-123",
    "session_name": "Klausur Deutsch",
    "image_path": "/app/ocr-labeling/abc-123/item-456.png",
    "image_url": "/api/v1/ocr-label/images/abc-123/item-456.png",
    "ocr_text": "Erkannter Text...",
    "ocr_confidence": 0.87,
    "ground_truth": null,
    "status": "pending",
    "metadata": {"page": 1}
  }
]
```

### Labeling Actions

#### POST /confirm
OCR-Text als korrekt bestätigen.

**Request:**
```json
{
  "item_id": "item-456",
  "label_time_seconds": 5
}
```

**Effect:** `ground_truth = ocr_text`, `status = 'confirmed'`

#### POST /correct
Ground Truth korrigieren.

**Request:**
```json
{
  "item_id": "item-456",
  "ground_truth": "Korrigierter Text hier",
  "label_time_seconds": 15
}
```

**Effect:** `ground_truth = <input>`, `status = 'corrected'`

#### POST /skip
Item überspringen (unbrauchbar).

**Request:**
```json
{
  "item_id": "item-456"
}
```

**Effect:** `status = 'skipped'` (wird nicht exportiert)

### Statistiken

#### GET /stats
Labeling-Statistiken abrufen.

**Query Parameter:**
- `session_id` (str, optional) - Für Session-spezifische Stats

**Response:**
```json
{
  "total_items": 100,
  "labeled_items": 75,
  "confirmed_items": 60,
  "corrected_items": 15,
  "pending_items": 25,
  "accuracy_rate": 0.80,
  "avg_label_time_seconds": 8.5
}
```

### Training Export

#### POST /export
Trainingsdaten exportieren.

**Request:**
```json
{
  "export_format": "trocr",
  "session_id": "abc-123",
  "batch_id": "batch_20260121"
}
```

**Export Formate:**

| Format | Beschreibung | Output |
|--------|--------------|--------|
| `generic` | Allgemeines JSONL | `{"id", "image_path", "ground_truth", ...}` |
| `trocr` | Microsoft TrOCR | `{"file_name", "text", "id"}` |
| `llama_vision` | Llama 3.2 Vision | OpenAI-style Messages mit image_url |

**Response:**
```json
{
  "export_format": "trocr",
  "batch_id": "batch_20260121",
  "exported_count": 75,
  "export_path": "/app/ocr-exports/trocr/batch_20260121",
  "manifest_path": "/app/ocr-exports/trocr/batch_20260121/manifest.json",
  "samples": [...]
}
```

#### GET /exports
Verfügbare Exports auflisten.

**Query Parameter:**
- `export_format` (str, optional) - Nach Format filtern

## Export Formate im Detail

### TrOCR Format

```
batch_20260121/
├── manifest.json
├── train.jsonl
└── images/
    ├── item-1.png
    └── item-2.png
```

**train.jsonl:**
```jsonl
{"file_name": "images/item-1.png", "text": "Ground truth text", "id": "item-1"}
{"file_name": "images/item-2.png", "text": "Another text", "id": "item-2"}
```

### Llama Vision Format

```jsonl
{
  "id": "item-1",
  "messages": [
    {"role": "system", "content": "Du bist ein OCR-Experte für deutsche Handschrift..."},
    {"role": "user", "content": [
      {"type": "image_url", "image_url": {"url": "images/item-1.png"}},
      {"type": "text", "text": "Lies den handgeschriebenen Text in diesem Bild."}
    ]},
    {"role": "assistant", "content": "Ground truth text"}
  ]
}
```

### Generic Format

```jsonl
{
  "id": "item-1",
  "image_path": "images/item-1.png",
  "ground_truth": "Ground truth text",
  "ocr_text": "OCR recognized text",
  "ocr_confidence": 0.87,
  "metadata": {"page": 1, "session": "Deutsch 12a"}
}
```

## Frontend Integration

Die OCR-Labeling UI ist unter `/admin/ocr-labeling` verfügbar.

### Keyboard Shortcuts

| Taste | Aktion |
|-------|--------|
| `Enter` | Bestätigen (OCR korrekt) |
| `Tab` | Ins Korrekturfeld springen |
| `Escape` | Überspringen |
| `←` / `→` | Navigation (Prev/Next) |

### Workflow

1. **Session erstellen** - Name, Typ, OCR-Modell wählen
2. **Bilder hochladen** - Drag & Drop oder File-Browser
3. **Labeling durchführen** - Bild + OCR-Text vergleichen
   - Korrekt → Bestätigen (Enter)
   - Falsch → Korrigieren + Speichern
   - Unbrauchbar → Überspringen
4. **Export** - Format wählen (TrOCR, Llama Vision, Generic)
5. **Training starten** - Export-Ordner für Fine-Tuning nutzen

## Umgebungsvariablen

```bash
# PostgreSQL
DATABASE_URL=postgres://user:pass@postgres:5432/breakpilot_db

# MinIO (S3-kompatibel)
MINIO_ENDPOINT=minio:9000
MINIO_ACCESS_KEY=breakpilot
MINIO_SECRET_KEY=breakpilot123
MINIO_BUCKET=breakpilot-rag
MINIO_SECURE=false

# Ollama (Vision-LLM)
OLLAMA_BASE_URL=http://host.docker.internal:11434
OLLAMA_VISION_MODEL=llama3.2-vision:11b
OLLAMA_CORRECTION_MODEL=qwen2.5:14b

# Export
OCR_EXPORT_PATH=/app/ocr-exports
OCR_STORAGE_PATH=/app/ocr-labeling
```

## Sicherheit & Datenschutz

- **100% Lokale Verarbeitung** - Alle Daten bleiben auf dem Mac Mini
- **Keine Cloud-Uploads** - Ollama läuft vollständig offline
- **DSGVO-konform** - Keine Schülerdaten verlassen das Schulnetzwerk
- **Deduplizierung** - SHA256-Hash verhindert doppelte Bilder

## Dateien

| Datei | Beschreibung |
|-------|--------------|
| `klausur-service/backend/ocr_labeling_api.py` | FastAPI Router mit OCR Model Dispatcher |
| `klausur-service/backend/training_export_service.py` | Export-Service für TrOCR/Llama |
| `klausur-service/backend/metrics_db.py` | PostgreSQL CRUD Funktionen |
| `klausur-service/backend/minio_storage.py` | MinIO OCR-Image Storage |
| `klausur-service/backend/hybrid_vocab_extractor.py` | PaddleOCR Integration |
| `klausur-service/backend/services/donut_ocr_service.py` | Donut OCR Service (NEU) |
| `klausur-service/backend/services/trocr_service.py` | TrOCR Service (NEU) |
| `website/app/admin/ocr-labeling/page.tsx` | Frontend UI mit Model-Auswahl |
| `website/app/admin/ocr-labeling/types.ts` | TypeScript Interfaces inkl. OCRModel Type |

## Tests

```bash
# Backend-Tests ausführen
cd klausur-service/backend
pytest tests/test_ocr_labeling.py -v

# Mit Coverage
pytest tests/test_ocr_labeling.py --cov=. --cov-report=html
```
