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 28s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Failing after 1m59s
CI / test-python-agent-core (push) Successful in 17s
CI / test-nodejs-website (push) Successful in 18s
Zwei neue Wizard-Schritte vor Begradigung: - Step 1: Orientierungserkennung (0/90/180/270° via Tesseract OSD) - Step 2: Seitenrand-Erkennung und Zuschnitt (Scannerraender entfernen) Backend: - orientation_crop_api.py: POST /orientation, POST /crop, POST /crop/skip - page_crop.py: detect_and_crop_page() mit Format-Erkennung (A4/A5/Letter) - Session-Store: orientation_result, crop_result Felder - Pipeline nutzt zugeschnittenes Bild fuer Deskew/Dewarp Frontend: - StepOrientation.tsx: Upload + Auto-Orientierung + Vorher/Nachher - StepCrop.tsx: Auto-Crop + Format-Badge + Ueberspringen-Option - Pipeline-Stepper: 10 Schritte (war 8) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
210 lines
8.0 KiB
TypeScript
210 lines
8.0 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import type { DeskewResult, DeskewGroundTruth } from '@/app/(admin)/ai/ocr-pipeline/types'
|
|
|
|
interface DeskewControlsProps {
|
|
deskewResult: DeskewResult | null
|
|
showBinarized: boolean
|
|
onToggleBinarized: () => void
|
|
showGrid: boolean
|
|
onToggleGrid: () => void
|
|
onManualDeskew: (angle: number) => void
|
|
onGroundTruth: (gt: DeskewGroundTruth) => void
|
|
onNext: () => void
|
|
isApplying: boolean
|
|
}
|
|
|
|
const METHOD_LABELS: Record<string, string> = {
|
|
hough: 'Hough-Linien',
|
|
word_alignment: 'Wortausrichtung',
|
|
manual: 'Manuell',
|
|
}
|
|
|
|
export function DeskewControls({
|
|
deskewResult,
|
|
showBinarized,
|
|
onToggleBinarized,
|
|
showGrid,
|
|
onToggleGrid,
|
|
onManualDeskew,
|
|
onGroundTruth,
|
|
onNext,
|
|
isApplying,
|
|
}: DeskewControlsProps) {
|
|
const [manualAngle, setManualAngle] = useState(0)
|
|
const [gtFeedback, setGtFeedback] = useState<'correct' | 'incorrect' | null>(null)
|
|
const [gtNotes, setGtNotes] = useState('')
|
|
const [gtSaved, setGtSaved] = useState(false)
|
|
|
|
const handleGroundTruth = (isCorrect: boolean) => {
|
|
setGtFeedback(isCorrect ? 'correct' : 'incorrect')
|
|
if (isCorrect) {
|
|
onGroundTruth({ is_correct: true })
|
|
setGtSaved(true)
|
|
}
|
|
}
|
|
|
|
const handleGroundTruthIncorrect = () => {
|
|
onGroundTruth({
|
|
is_correct: false,
|
|
corrected_angle: manualAngle !== 0 ? manualAngle : undefined,
|
|
notes: gtNotes || undefined,
|
|
})
|
|
setGtSaved(true)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Results */}
|
|
{deskewResult && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
<div className="flex flex-wrap items-center gap-3 text-sm">
|
|
<div>
|
|
<span className="text-gray-500">Winkel:</span>{' '}
|
|
<span className="font-mono font-medium">{deskewResult.angle_applied}°</span>
|
|
</div>
|
|
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600" />
|
|
<div>
|
|
<span className="text-gray-500">Methode:</span>{' '}
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-300">
|
|
{METHOD_LABELS[deskewResult.method_used] || deskewResult.method_used}
|
|
</span>
|
|
</div>
|
|
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600" />
|
|
<div>
|
|
<span className="text-gray-500">Konfidenz:</span>{' '}
|
|
<span className="font-mono">{Math.round(deskewResult.confidence * 100)}%</span>
|
|
</div>
|
|
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600" />
|
|
<div className="text-gray-400 text-xs">
|
|
Hough: {deskewResult.angle_hough}° | WA: {deskewResult.angle_word_alignment}°
|
|
</div>
|
|
</div>
|
|
|
|
{/* Toggles */}
|
|
<div className="flex gap-3 mt-3">
|
|
<button
|
|
onClick={onToggleBinarized}
|
|
className={`text-xs px-3 py-1 rounded-full border transition-colors ${
|
|
showBinarized
|
|
? 'bg-teal-100 border-teal-300 text-teal-700 dark:bg-teal-900/40 dark:border-teal-600 dark:text-teal-300'
|
|
: 'border-gray-300 text-gray-500 dark:border-gray-600 dark:text-gray-400'
|
|
}`}
|
|
>
|
|
Binarisiert anzeigen
|
|
</button>
|
|
<button
|
|
onClick={onToggleGrid}
|
|
className={`text-xs px-3 py-1 rounded-full border transition-colors ${
|
|
showGrid
|
|
? 'bg-teal-100 border-teal-300 text-teal-700 dark:bg-teal-900/40 dark:border-teal-600 dark:text-teal-300'
|
|
: 'border-gray-300 text-gray-500 dark:border-gray-600 dark:text-gray-400'
|
|
}`}
|
|
>
|
|
Raster anzeigen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Manual angle */}
|
|
{deskewResult && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Manuelle Korrektur</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-xs text-gray-400 w-8 text-right">-5°</span>
|
|
<input
|
|
type="range"
|
|
min={-5}
|
|
max={5}
|
|
step={0.1}
|
|
value={manualAngle}
|
|
onChange={(e) => setManualAngle(parseFloat(e.target.value))}
|
|
className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-teal-500"
|
|
/>
|
|
<span className="text-xs text-gray-400 w-8">+5°</span>
|
|
<span className="font-mono text-sm w-14 text-right">{manualAngle.toFixed(1)}°</span>
|
|
<button
|
|
onClick={() => onManualDeskew(manualAngle)}
|
|
disabled={isApplying}
|
|
className="px-3 py-1.5 text-sm bg-teal-600 text-white rounded-md hover:bg-teal-700 disabled:opacity-50 transition-colors"
|
|
>
|
|
{isApplying ? '...' : 'Anwenden'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Ground Truth */}
|
|
{deskewResult && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Rotation korrekt?
|
|
</div>
|
|
<p className="text-xs text-gray-400 mb-2">Nur die Drehung bewerten — Woelbung/Verzerrung wird im naechsten Schritt korrigiert.</p>
|
|
{!gtSaved ? (
|
|
<div className="space-y-3">
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => handleGroundTruth(true)}
|
|
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
|
gtFeedback === 'correct'
|
|
? 'bg-green-100 text-green-700 ring-2 ring-green-400'
|
|
: 'bg-gray-100 text-gray-600 hover:bg-green-50 dark:bg-gray-700 dark:text-gray-300'
|
|
}`}
|
|
>
|
|
Ja
|
|
</button>
|
|
<button
|
|
onClick={() => handleGroundTruth(false)}
|
|
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
|
gtFeedback === 'incorrect'
|
|
? 'bg-red-100 text-red-700 ring-2 ring-red-400'
|
|
: 'bg-gray-100 text-gray-600 hover:bg-red-50 dark:bg-gray-700 dark:text-gray-300'
|
|
}`}
|
|
>
|
|
Nein
|
|
</button>
|
|
</div>
|
|
{gtFeedback === 'incorrect' && (
|
|
<div className="space-y-2">
|
|
<textarea
|
|
value={gtNotes}
|
|
onChange={(e) => setGtNotes(e.target.value)}
|
|
placeholder="Notizen zur Korrektur..."
|
|
className="w-full text-sm border border-gray-300 dark:border-gray-600 rounded-md p-2 bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200"
|
|
rows={2}
|
|
/>
|
|
<button
|
|
onClick={handleGroundTruthIncorrect}
|
|
className="text-sm px-3 py-1 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
|
|
>
|
|
Feedback speichern
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="text-sm text-green-600 dark:text-green-400">
|
|
Feedback gespeichert
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Next button */}
|
|
{deskewResult && (
|
|
<div className="flex justify-end">
|
|
<button
|
|
onClick={onNext}
|
|
className="px-6 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 font-medium transition-colors"
|
|
>
|
|
Uebernehmen & Weiter →
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|