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 27s
CI / test-go-edu-search (push) Successful in 26s
CI / test-python-klausur (push) Failing after 1m53s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 16s
Since ocr_region_paddle() now runs RapidOCR locally (same PP-OCRv5 models), the "PaddleOCR (Hetzner)" labels were misleading. Renamed to "PP-OCRv5 (lokal)". Removed the Kombi-Vergleich tab since both sides would produce identical results. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
154 lines
4.6 KiB
TypeScript
154 lines
4.6 KiB
TypeScript
'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
|
|
/** Backend endpoint suffix, default: 'paddle-direct' */
|
|
endpoint?: string
|
|
/** Title shown in idle state */
|
|
title?: string
|
|
/** Description shown in idle state */
|
|
description?: string
|
|
/** Icon shown in idle state */
|
|
icon?: string
|
|
/** Button label */
|
|
buttonLabel?: string
|
|
/** Running label */
|
|
runningLabel?: string
|
|
/** OCR engine key to check for auto-detect */
|
|
engineKey?: string
|
|
}
|
|
|
|
export function PaddleDirectStep({
|
|
sessionId,
|
|
onNext,
|
|
endpoint = 'paddle-direct',
|
|
title = 'PP-OCRv5 Direct',
|
|
description = 'PP-OCRv5 (lokal via RapidOCR) erkennt alle Woerter direkt auf dem Originalbild — ohne Begradigung, Entzerrung oder Zuschnitt.',
|
|
icon = '⚡',
|
|
buttonLabel = 'PP-OCRv5 starten',
|
|
runningLabel = 'PP-OCRv5 laeuft...',
|
|
engineKey = 'paddle_direct',
|
|
}: 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 matching 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 === engineKey) {
|
|
setPhase('overlay')
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
})()
|
|
return () => { cancelled = true }
|
|
}, [sessionId, engineKey])
|
|
|
|
const runOcr = useCallback(async () => {
|
|
if (!sessionId) return
|
|
setPhase('running')
|
|
setError(null)
|
|
try {
|
|
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/${endpoint}`, {
|
|
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, endpoint])
|
|
|
|
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">
|
|
{runningLabel}
|
|
</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">{icon}</div>
|
|
<h3 className="text-lg font-medium text-gray-700 dark:text-gray-300">
|
|
{title}
|
|
</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 max-w-md">
|
|
{description}
|
|
</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={runOcr}
|
|
className="px-6 py-2.5 bg-teal-600 text-white text-sm font-medium rounded-lg hover:bg-teal-700 transition-colors"
|
|
>
|
|
{buttonLabel}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|