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 28s
CI / test-python-klausur (push) Failing after 2m1s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 18s
Neues aufklappbares Panel unter Entzerrung mit individuellen Reglern:
- 3 Rotations-Regler (P1 Iterative, P2 Word-Alignment, P3 Textline)
- 4 Scherungs-Regler (A-D Methoden) mit Radio-Auswahl
- Kombinierte Vorschau und Ground-Truth-Speicherung
- Backend: POST /sessions/{id}/adjust-combined Endpoint
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
205 lines
6.6 KiB
TypeScript
205 lines
6.6 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)
|
|
|
|
// 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>
|
|
)
|
|
}
|