feat: add Kombi-Vergleich mode for side-by-side Paddle vs RapidOCR comparison
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 33s
CI / test-go-edu-search (push) Successful in 26s
CI / test-python-klausur (push) Failing after 1m55s
CI / test-python-agent-core (push) Successful in 17s
CI / test-nodejs-website (push) Successful in 21s

Add /rapid-kombi backend endpoint using local RapidOCR + Tesseract merge,
KombiCompareStep component for parallel execution and side-by-side overlay,
and wordResultOverride prop on OverlayReconstruction for direct data injection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-14 07:59:06 +01:00
parent c2c082d4b4
commit a994ddee83
6 changed files with 504 additions and 35 deletions

View File

@@ -11,12 +11,13 @@ import { StepRowDetection } from '@/components/ocr-pipeline/StepRowDetection'
import { StepWordRecognition } from '@/components/ocr-pipeline/StepWordRecognition'
import { OverlayReconstruction } from '@/components/ocr-overlay/OverlayReconstruction'
import { PaddleDirectStep } from '@/components/ocr-overlay/PaddleDirectStep'
import { OVERLAY_PIPELINE_STEPS, PADDLE_DIRECT_STEPS, KOMBI_STEPS, DOCUMENT_CATEGORIES, dbStepToOverlayUi, type PipelineStep, type SessionListItem, type DocumentCategory } from './types'
import { KombiCompareStep } from '@/components/ocr-overlay/KombiCompareStep'
import { OVERLAY_PIPELINE_STEPS, PADDLE_DIRECT_STEPS, KOMBI_STEPS, KOMBI_COMPARE_STEPS, DOCUMENT_CATEGORIES, dbStepToOverlayUi, type PipelineStep, type SessionListItem, type DocumentCategory } from './types'
const KLAUSUR_API = '/klausur-api'
export default function OcrOverlayPage() {
const [mode, setMode] = useState<'pipeline' | 'paddle-direct' | 'kombi'>('pipeline')
const [mode, setMode] = useState<'pipeline' | 'paddle-direct' | 'kombi' | 'kombi-compare'>('pipeline')
const [currentStep, setCurrentStep] = useState(0)
const [sessionId, setSessionId] = useState<string | null>(null)
const [sessionName, setSessionName] = useState<string>('')
@@ -63,14 +64,15 @@ export default function OcrOverlayPage() {
setSessionName(data.name || data.filename || '')
setActiveCategory(data.document_category || undefined)
// Check if this session was processed with paddle_direct or kombi
// Check if this session was processed with paddle_direct, kombi, or rapid_kombi
const ocrEngine = data.word_result?.ocr_engine
const isPaddleDirect = ocrEngine === 'paddle_direct'
const isKombi = ocrEngine === 'kombi'
const isRapidKombi = ocrEngine === 'rapid_kombi'
if (isPaddleDirect || isKombi) {
const m = isKombi ? 'kombi' : 'paddle-direct'
const baseSteps = isKombi ? KOMBI_STEPS : PADDLE_DIRECT_STEPS
if (isPaddleDirect || isKombi || isRapidKombi) {
const m = isKombi ? 'kombi' : isPaddleDirect ? 'paddle-direct' : 'kombi-compare'
const baseSteps = isKombi ? KOMBI_STEPS : isRapidKombi ? KOMBI_COMPARE_STEPS : PADDLE_DIRECT_STEPS
setMode(m)
setSteps(
baseSteps.map((s, i) => ({
@@ -105,7 +107,7 @@ export default function OcrOverlayPage() {
if (sessionId === sid) {
setSessionId(null)
setCurrentStep(0)
const baseSteps = mode === 'kombi' ? KOMBI_STEPS : mode === 'paddle-direct' ? PADDLE_DIRECT_STEPS : OVERLAY_PIPELINE_STEPS
const baseSteps = mode === 'kombi' ? KOMBI_STEPS : mode === 'kombi-compare' ? KOMBI_COMPARE_STEPS : mode === 'paddle-direct' ? PADDLE_DIRECT_STEPS : OVERLAY_PIPELINE_STEPS
setSteps(baseSteps.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
}
} catch (e) {
@@ -162,7 +164,7 @@ export default function OcrOverlayPage() {
const handleNext = () => {
if (currentStep >= steps.length - 1) {
// Last step completed — return to session list
const baseSteps = mode === 'kombi' ? KOMBI_STEPS : mode === 'paddle-direct' ? PADDLE_DIRECT_STEPS : OVERLAY_PIPELINE_STEPS
const baseSteps = mode === 'kombi' ? KOMBI_STEPS : mode === 'kombi-compare' ? KOMBI_COMPARE_STEPS : mode === 'paddle-direct' ? PADDLE_DIRECT_STEPS : OVERLAY_PIPELINE_STEPS
setSteps(baseSteps.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
setCurrentStep(0)
setSessionId(null)
@@ -191,7 +193,7 @@ export default function OcrOverlayPage() {
setSessionId(null)
setSessionName('')
setCurrentStep(0)
const baseSteps = mode === 'kombi' ? KOMBI_STEPS : mode === 'paddle-direct' ? PADDLE_DIRECT_STEPS : OVERLAY_PIPELINE_STEPS
const baseSteps = mode === 'kombi' ? KOMBI_STEPS : mode === 'kombi-compare' ? KOMBI_COMPARE_STEPS : mode === 'paddle-direct' ? PADDLE_DIRECT_STEPS : OVERLAY_PIPELINE_STEPS
setSteps(baseSteps.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
}
@@ -230,7 +232,7 @@ export default function OcrOverlayPage() {
}, [sessionId, goToStep])
const renderStep = () => {
if (mode === 'paddle-direct' || mode === 'kombi') {
if (mode === 'paddle-direct' || mode === 'kombi' || mode === 'kombi-compare') {
switch (currentStep) {
case 0:
return <StepOrientation sessionId={sessionId} onNext={handleOrientationComplete} />
@@ -241,6 +243,9 @@ export default function OcrOverlayPage() {
case 3:
return <StepCrop sessionId={sessionId} onNext={handleNext} />
case 4:
if (mode === 'kombi-compare') {
return <KombiCompareStep sessionId={sessionId} onNext={handleNext} />
}
return mode === 'kombi' ? (
<PaddleDirectStep
sessionId={sessionId}
@@ -514,6 +519,22 @@ export default function OcrOverlayPage() {
>
Kombi (5 Schritte)
</button>
<button
onClick={() => {
if (mode === 'kombi-compare') return
setMode('kombi-compare')
setCurrentStep(0)
setSessionId(null)
setSteps(KOMBI_COMPARE_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
}}
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
mode === 'kombi-compare'
? 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 shadow-sm'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
Vergleich (5 Schritte)
</button>
</div>
<PipelineStepper

View File

@@ -72,6 +72,18 @@ export const KOMBI_STEPS: PipelineStep[] = [
{ id: 'kombi', name: 'Paddle + Tesseract', icon: '🔀', status: 'pending' },
]
/**
* 5-step pipeline for Kombi-Vergleich mode (Paddle-Kombi vs Rapid-Kombi side-by-side).
* Same preprocessing, then both kombi engines run in parallel and are shown side-by-side.
*/
export const KOMBI_COMPARE_STEPS: PipelineStep[] = [
{ id: 'orientation', name: 'Orientierung', icon: '🔄', status: 'pending' },
{ id: 'deskew', name: 'Begradigung', icon: '📐', status: 'pending' },
{ id: 'dewarp', name: 'Entzerrung', icon: '🔧', status: 'pending' },
{ id: 'crop', name: 'Zuschneiden', icon: '✂️', status: 'pending' },
{ id: 'kombi-compare', name: 'Kombi-Vergleich', icon: '⚖️', status: 'pending' },
]
/** Map from DB step to overlay UI step index */
export function dbStepToOverlayUi(dbStep: number): number {
// DB: 1=start, 2=orient, 3=deskew, 4=dewarp, 5=crop, 6=columns, 7=rows, 8=words, 9=recon, 10=gt