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 25s
CI / test-go-edu-search (push) Successful in 25s
CI / test-python-klausur (push) Failing after 1m53s
CI / test-python-agent-core (push) Successful in 15s
CI / test-nodejs-website (push) Successful in 17s
Step components (Deskew, Dewarp, Crop, Orientation) had local state guards that prevented reloading when sessionId changed via sub-session tab clicks. Added useEffect reset hooks that clear all local state when sessionId changes, allowing the component to properly reload the new session's data. Also renamed "Box N" to "Seite N" in BoxSessionTabs per user feedback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
212 lines
6.8 KiB
TypeScript
212 lines
6.8 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useState } from 'react'
|
|
import type { DeskewResult, DewarpResult, DewarpGroundTruth } from '@/app/(admin)/ai/ocr-pipeline/types'
|
|
import { DewarpControls } from './DewarpControls'
|
|
import { ImageCompareView } from './ImageCompareView'
|
|
|
|
const KLAUSUR_API = '/klausur-api'
|
|
|
|
interface StepDewarpProps {
|
|
sessionId: string | null
|
|
onNext: () => void
|
|
}
|
|
|
|
export function StepDewarp({ sessionId, onNext }: StepDewarpProps) {
|
|
const [dewarpResult, setDewarpResult] = useState<DewarpResult | null>(null)
|
|
const [deskewResult, setDeskewResult] = useState<DeskewResult | null>(null)
|
|
const [dewarping, setDewarping] = useState(false)
|
|
const [applying, setApplying] = useState(false)
|
|
const [showGrid, setShowGrid] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
// Reset state when sessionId changes (e.g. switching sub-sessions)
|
|
useEffect(() => {
|
|
setDewarpResult(null)
|
|
setDeskewResult(null)
|
|
setError(null)
|
|
}, [sessionId])
|
|
|
|
// Load session info to get deskew_result (for fine-tuning init values)
|
|
useEffect(() => {
|
|
if (!sessionId) return
|
|
const loadSession = async () => {
|
|
try {
|
|
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}`)
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
if (data.deskew_result) {
|
|
setDeskewResult(data.deskew_result)
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load session info:', e)
|
|
}
|
|
}
|
|
loadSession()
|
|
}, [sessionId])
|
|
|
|
// Auto-trigger dewarp when component mounts with a sessionId
|
|
useEffect(() => {
|
|
if (!sessionId || dewarpResult) return
|
|
|
|
const runDewarp = async () => {
|
|
setDewarping(true)
|
|
setError(null)
|
|
try {
|
|
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/dewarp`, {
|
|
method: 'POST',
|
|
})
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ detail: res.statusText }))
|
|
throw new Error(err.detail || 'Entzerrung fehlgeschlagen')
|
|
}
|
|
const data: DewarpResult = await res.json()
|
|
data.dewarped_image_url = `${KLAUSUR_API}${data.dewarped_image_url}`
|
|
setDewarpResult(data)
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
|
} finally {
|
|
setDewarping(false)
|
|
}
|
|
}
|
|
|
|
runDewarp()
|
|
}, [sessionId, dewarpResult])
|
|
|
|
const handleManualDewarp = useCallback(async (shearDegrees: number) => {
|
|
if (!sessionId) return
|
|
setApplying(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/dewarp/manual`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ shear_degrees: shearDegrees }),
|
|
})
|
|
if (!res.ok) throw new Error('Manuelle Entzerrung fehlgeschlagen')
|
|
|
|
const data = await res.json()
|
|
setDewarpResult((prev) =>
|
|
prev
|
|
? {
|
|
...prev,
|
|
method_used: data.method_used,
|
|
shear_degrees: data.shear_degrees,
|
|
dewarped_image_url: `${KLAUSUR_API}${data.dewarped_image_url}?t=${Date.now()}`,
|
|
}
|
|
: null,
|
|
)
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Fehler')
|
|
} finally {
|
|
setApplying(false)
|
|
}
|
|
}, [sessionId])
|
|
|
|
const handleCombinedAdjust = useCallback(async (rotationDegrees: number, shearDegrees: number) => {
|
|
if (!sessionId) return
|
|
setApplying(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/adjust-combined`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ rotation_degrees: rotationDegrees, shear_degrees: shearDegrees }),
|
|
})
|
|
if (!res.ok) throw new Error('Kombinierte Anpassung fehlgeschlagen')
|
|
|
|
const data = await res.json()
|
|
setDewarpResult((prev) =>
|
|
prev
|
|
? {
|
|
...prev,
|
|
method_used: data.method_used,
|
|
shear_degrees: data.shear_degrees,
|
|
dewarped_image_url: `${KLAUSUR_API}${data.dewarped_image_url}?t=${Date.now()}`,
|
|
}
|
|
: null,
|
|
)
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Fehler')
|
|
} finally {
|
|
setApplying(false)
|
|
}
|
|
}, [sessionId])
|
|
|
|
const handleGroundTruth = useCallback(async (gt: DewarpGroundTruth) => {
|
|
if (!sessionId) return
|
|
try {
|
|
await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/ground-truth/dewarp`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(gt),
|
|
})
|
|
} catch (e) {
|
|
console.error('Ground truth save failed:', e)
|
|
}
|
|
}, [sessionId])
|
|
|
|
if (!sessionId) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
<div className="text-5xl mb-4">🔧</div>
|
|
<h3 className="text-lg font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Schritt 2: Entzerrung (Dewarp)
|
|
</h3>
|
|
<p className="text-gray-500 dark:text-gray-400 max-w-md">
|
|
Bitte zuerst Schritt 1 (Begradigung) abschliessen.
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const deskewedUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/deskewed`
|
|
const dewarpedUrl = dewarpResult?.dewarped_image_url ?? null
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Loading indicator */}
|
|
{dewarping && (
|
|
<div className="flex items-center gap-2 text-teal-600 dark:text-teal-400 text-sm">
|
|
<div className="animate-spin w-4 h-4 border-2 border-teal-500 border-t-transparent rounded-full" />
|
|
Entzerrung laeuft (beide Methoden)...
|
|
</div>
|
|
)}
|
|
|
|
{/* Image comparison: deskewed (left) vs dewarped (right) */}
|
|
<ImageCompareView
|
|
originalUrl={deskewedUrl}
|
|
deskewedUrl={dewarpedUrl}
|
|
showGrid={showGrid}
|
|
showGridLeft={showGrid}
|
|
showBinarized={false}
|
|
binarizedUrl={null}
|
|
leftLabel={`Begradigt (nach Deskew)${showGrid ? ' + Raster' : ''}`}
|
|
rightLabel={`Entzerrt${showGrid ? ' + Raster (mm)' : ''}`}
|
|
/>
|
|
|
|
{/* Controls */}
|
|
<DewarpControls
|
|
dewarpResult={dewarpResult}
|
|
deskewResult={deskewResult}
|
|
showGrid={showGrid}
|
|
onToggleGrid={() => setShowGrid((v) => !v)}
|
|
onManualDewarp={handleManualDewarp}
|
|
onCombinedAdjust={handleCombinedAdjust}
|
|
onGroundTruth={handleGroundTruth}
|
|
onNext={onNext}
|
|
isApplying={applying}
|
|
/>
|
|
|
|
{error && (
|
|
<div className="p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|