feat: Paddle Direct — 1-click OCR without deskew/dewarp/crop
Some checks failed
CI / go-lint (push) Has been cancelled
CI / python-lint (push) Has been cancelled
CI / nodejs-lint (push) Has been cancelled
CI / test-go-school (push) Has been cancelled
CI / test-go-edu-search (push) Has been cancelled
CI / test-python-klausur (push) Has been cancelled
CI / test-python-agent-core (push) Has been cancelled
CI / test-nodejs-website (push) Has been cancelled

New 2-step mode (Upload → PaddleOCR+Overlay) alongside the existing
7-step pipeline. Backend endpoint runs PaddleOCR on the original image
and clusters words into rows/cells directly. Frontend adds a mode
toggle and PaddleDirectStep component.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-12 16:41:55 +01:00
parent 06d63d18f9
commit 90c1efd9b0
4 changed files with 403 additions and 16 deletions

View File

@@ -0,0 +1,129 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { OverlayReconstruction } from './OverlayReconstruction'
const KLAUSUR_API = '/klausur-api'
type Phase = 'idle' | 'running' | 'overlay'
interface PaddleDirectStepProps {
sessionId: string | null
onNext: () => void
}
export function PaddleDirectStep({ sessionId, onNext }: PaddleDirectStepProps) {
const [phase, setPhase] = useState<Phase>('idle')
const [error, setError] = useState<string | null>(null)
const [stats, setStats] = useState<{ cells: number; rows: number; duration: number } | null>(null)
// Auto-detect: if session already has paddle_direct word_result → show overlay
useEffect(() => {
if (!sessionId) return
let cancelled = false
;(async () => {
try {
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}`)
if (!res.ok || cancelled) return
const data = await res.json()
if (data.word_result?.ocr_engine === 'paddle_direct') {
setPhase('overlay')
}
} catch {
// ignore
}
})()
return () => { cancelled = true }
}, [sessionId])
const runPaddleDirect = useCallback(async () => {
if (!sessionId) return
setPhase('running')
setError(null)
try {
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/paddle-direct`, {
method: 'POST',
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
throw new Error(data.detail || `HTTP ${res.status}`)
}
const data = await res.json()
setStats({
cells: data.summary?.total_cells || 0,
rows: data.grid_shape?.rows || 0,
duration: data.duration_seconds || 0,
})
setPhase('overlay')
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
setPhase('idle')
}
}, [sessionId])
if (!sessionId) {
return (
<div className="text-sm text-gray-400 py-8 text-center">
Bitte zuerst ein Bild hochladen.
</div>
)
}
if (phase === 'overlay') {
return (
<div className="space-y-3">
{stats && (
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
<span>{stats.cells} Woerter erkannt</span>
<span>{stats.rows} Zeilen</span>
<span>{stats.duration.toFixed(1)}s</span>
</div>
)}
<OverlayReconstruction sessionId={sessionId} onNext={onNext} />
</div>
)
}
return (
<div className="flex flex-col items-center justify-center py-16 space-y-6">
{phase === 'running' ? (
<>
<div className="w-10 h-10 border-4 border-teal-200 dark:border-teal-800 border-t-teal-600 dark:border-t-teal-400 rounded-full animate-spin" />
<div className="text-center space-y-1">
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
PaddleOCR laeuft...
</p>
<p className="text-xs text-gray-400">
Bild wird analysiert (ca. 5-30s)
</p>
</div>
</>
) : (
<>
<div className="text-center space-y-2">
<div className="text-4xl"></div>
<h3 className="text-lg font-medium text-gray-700 dark:text-gray-300">
Paddle Direct
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 max-w-md">
PaddleOCR erkennt alle Woerter direkt auf dem Originalbild ohne Begradigung, Entzerrung oder Zuschnitt.
</p>
</div>
{error && (
<div className="text-sm text-red-500 bg-red-50 dark:bg-red-900/20 px-4 py-2 rounded-lg">
{error}
</div>
)}
<button
onClick={runPaddleDirect}
className="px-6 py-2.5 bg-teal-600 text-white text-sm font-medium rounded-lg hover:bg-teal-700 transition-colors"
>
PaddleOCR starten
</button>
</>
)}
</div>
)
}