From d552fd8b6b1441065c83c2c73f4a9a97a300506b Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 26 Feb 2026 15:38:08 +0100 Subject: [PATCH] feat: OCR Pipeline mit 6-Schritt-Wizard fuer Seitenrekonstruktion Neue Route /ai/ocr-pipeline mit schrittweiser Begradigung (Deskew), Raster-Overlay und Ground Truth. Schritte 2-6 als Platzhalter. Co-Authored-By: Claude Opus 4.6 --- .../app/(admin)/ai/ocr-pipeline/page.tsx | 83 +++++ .../app/(admin)/ai/ocr-pipeline/types.ts | 43 +++ .../ocr-pipeline/DeskewControls.tsx | 208 ++++++++++++ .../ocr-pipeline/ImageCompareView.tsx | 134 ++++++++ .../ocr-pipeline/PipelineStepper.tsx | 53 +++ .../ocr-pipeline/StepColumnDetection.tsx | 19 ++ .../ocr-pipeline/StepCoordinates.tsx | 19 ++ .../components/ocr-pipeline/StepDeskew.tsx | 222 +++++++++++++ .../ocr-pipeline/StepGroundTruth.tsx | 19 ++ .../ocr-pipeline/StepReconstruction.tsx | 19 ++ .../ocr-pipeline/StepWordRecognition.tsx | 19 ++ admin-lehrer/lib/navigation.ts | 9 + klausur-service/backend/main.py | 2 + klausur-service/backend/ocr_pipeline_api.py | 301 ++++++++++++++++++ 14 files changed, 1150 insertions(+) create mode 100644 admin-lehrer/app/(admin)/ai/ocr-pipeline/page.tsx create mode 100644 admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts create mode 100644 admin-lehrer/components/ocr-pipeline/DeskewControls.tsx create mode 100644 admin-lehrer/components/ocr-pipeline/ImageCompareView.tsx create mode 100644 admin-lehrer/components/ocr-pipeline/PipelineStepper.tsx create mode 100644 admin-lehrer/components/ocr-pipeline/StepColumnDetection.tsx create mode 100644 admin-lehrer/components/ocr-pipeline/StepCoordinates.tsx create mode 100644 admin-lehrer/components/ocr-pipeline/StepDeskew.tsx create mode 100644 admin-lehrer/components/ocr-pipeline/StepGroundTruth.tsx create mode 100644 admin-lehrer/components/ocr-pipeline/StepReconstruction.tsx create mode 100644 admin-lehrer/components/ocr-pipeline/StepWordRecognition.tsx create mode 100644 klausur-service/backend/ocr_pipeline_api.py diff --git a/admin-lehrer/app/(admin)/ai/ocr-pipeline/page.tsx b/admin-lehrer/app/(admin)/ai/ocr-pipeline/page.tsx new file mode 100644 index 0000000..ae628ce --- /dev/null +++ b/admin-lehrer/app/(admin)/ai/ocr-pipeline/page.tsx @@ -0,0 +1,83 @@ +'use client' + +import { useState } from 'react' +import { PagePurpose } from '@/components/common/PagePurpose' +import { PipelineStepper } from '@/components/ocr-pipeline/PipelineStepper' +import { StepDeskew } from '@/components/ocr-pipeline/StepDeskew' +import { StepColumnDetection } from '@/components/ocr-pipeline/StepColumnDetection' +import { StepWordRecognition } from '@/components/ocr-pipeline/StepWordRecognition' +import { StepCoordinates } from '@/components/ocr-pipeline/StepCoordinates' +import { StepReconstruction } from '@/components/ocr-pipeline/StepReconstruction' +import { StepGroundTruth } from '@/components/ocr-pipeline/StepGroundTruth' +import { PIPELINE_STEPS, type PipelineStep } from './types' + +export default function OcrPipelinePage() { + const [currentStep, setCurrentStep] = useState(0) + const [steps, setSteps] = useState( + PIPELINE_STEPS.map((s, i) => ({ + ...s, + status: i === 0 ? 'active' : 'pending', + })), + ) + + const handleStepClick = (index: number) => { + if (index <= currentStep || steps[index].status === 'completed') { + setCurrentStep(index) + } + } + + const handleNext = () => { + if (currentStep < steps.length - 1) { + setSteps((prev) => + prev.map((s, i) => { + if (i === currentStep) return { ...s, status: 'completed' } + if (i === currentStep + 1) return { ...s, status: 'active' } + return s + }), + ) + setCurrentStep((prev) => prev + 1) + } + } + + const renderStep = () => { + switch (currentStep) { + case 0: + return + case 1: + return + case 2: + return + case 3: + return + case 4: + return + case 5: + return + default: + return null + } + } + + return ( +
+ + + + +
{renderStep()}
+
+ ) +} diff --git a/admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts b/admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts new file mode 100644 index 0000000..cd39700 --- /dev/null +++ b/admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts @@ -0,0 +1,43 @@ +export type PipelineStepStatus = 'pending' | 'active' | 'completed' | 'failed' + +export interface PipelineStep { + id: string + name: string + icon: string + status: PipelineStepStatus +} + +export interface SessionInfo { + session_id: string + filename: string + image_width: number + image_height: number + original_image_url: string +} + +export interface DeskewResult { + session_id: string + angle_hough: number + angle_word_alignment: number + angle_applied: number + method_used: 'hough' | 'word_alignment' | 'manual' + confidence: number + duration_seconds: number + deskewed_image_url: string + binarized_image_url: string +} + +export interface DeskewGroundTruth { + is_correct: boolean + corrected_angle?: number + notes?: string +} + +export const PIPELINE_STEPS: PipelineStep[] = [ + { id: 'deskew', name: 'Begradigung', icon: '📐', status: 'pending' }, + { id: 'columns', name: 'Spalten', icon: '📊', status: 'pending' }, + { id: 'words', name: 'Woerter', icon: '🔤', status: 'pending' }, + { id: 'coordinates', name: 'Koordinaten', icon: '📍', status: 'pending' }, + { id: 'reconstruction', name: 'Rekonstruktion', icon: '🏗️', status: 'pending' }, + { id: 'ground-truth', name: 'Validierung', icon: '✅', status: 'pending' }, +] diff --git a/admin-lehrer/components/ocr-pipeline/DeskewControls.tsx b/admin-lehrer/components/ocr-pipeline/DeskewControls.tsx new file mode 100644 index 0000000..3370e2c --- /dev/null +++ b/admin-lehrer/components/ocr-pipeline/DeskewControls.tsx @@ -0,0 +1,208 @@ +'use client' + +import { useState } from 'react' +import type { DeskewResult, DeskewGroundTruth } from '@/app/(admin)/ai/ocr-pipeline/types' + +interface DeskewControlsProps { + deskewResult: DeskewResult | null + showBinarized: boolean + onToggleBinarized: () => void + showGrid: boolean + onToggleGrid: () => void + onManualDeskew: (angle: number) => void + onGroundTruth: (gt: DeskewGroundTruth) => void + onNext: () => void + isApplying: boolean +} + +const METHOD_LABELS: Record = { + hough: 'Hough-Linien', + word_alignment: 'Wortausrichtung', + manual: 'Manuell', +} + +export function DeskewControls({ + deskewResult, + showBinarized, + onToggleBinarized, + showGrid, + onToggleGrid, + onManualDeskew, + onGroundTruth, + onNext, + isApplying, +}: DeskewControlsProps) { + const [manualAngle, setManualAngle] = useState(0) + const [gtFeedback, setGtFeedback] = useState<'correct' | 'incorrect' | null>(null) + const [gtNotes, setGtNotes] = useState('') + const [gtSaved, setGtSaved] = useState(false) + + const handleGroundTruth = (isCorrect: boolean) => { + setGtFeedback(isCorrect ? 'correct' : 'incorrect') + if (isCorrect) { + onGroundTruth({ is_correct: true }) + setGtSaved(true) + } + } + + const handleGroundTruthIncorrect = () => { + onGroundTruth({ + is_correct: false, + corrected_angle: manualAngle !== 0 ? manualAngle : undefined, + notes: gtNotes || undefined, + }) + setGtSaved(true) + } + + return ( +
+ {/* Results */} + {deskewResult && ( +
+
+
+ Winkel:{' '} + {deskewResult.angle_applied}° +
+
+
+ Methode:{' '} + + {METHOD_LABELS[deskewResult.method_used] || deskewResult.method_used} + +
+
+
+ Konfidenz:{' '} + {Math.round(deskewResult.confidence * 100)}% +
+
+
+ Hough: {deskewResult.angle_hough}° | WA: {deskewResult.angle_word_alignment}° +
+
+ + {/* Toggles */} +
+ + +
+
+ )} + + {/* Manual angle */} + {deskewResult && ( +
+
Manuelle Korrektur
+
+ -5° + setManualAngle(parseFloat(e.target.value))} + className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-teal-500" + /> + +5° + {manualAngle.toFixed(1)}° + +
+
+ )} + + {/* Ground Truth */} + {deskewResult && ( +
+
+ Korrekt ausgerichtet? +
+ {!gtSaved ? ( +
+
+ + +
+ {gtFeedback === 'incorrect' && ( +
+