diff --git a/admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts b/admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts index ce4de3a..a0feca1 100644 --- a/admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts +++ b/admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts @@ -81,6 +81,7 @@ export interface ColumnResult { export interface ColumnGroundTruth { is_correct: boolean + corrected_columns?: PageRegion[] notes?: string } diff --git a/admin-lehrer/components/ocr-pipeline/ColumnControls.tsx b/admin-lehrer/components/ocr-pipeline/ColumnControls.tsx index df2d2ad..d6c44c8 100644 --- a/admin-lehrer/components/ocr-pipeline/ColumnControls.tsx +++ b/admin-lehrer/components/ocr-pipeline/ColumnControls.tsx @@ -1,15 +1,17 @@ 'use client' -import { useState } from 'react' +import { useState, useMemo } from 'react' import type { ColumnResult, ColumnGroundTruth, PageRegion } from '@/app/(admin)/ai/ocr-pipeline/types' interface ColumnControlsProps { columnResult: ColumnResult | null onRerun: () => void onManualMode: () => void + onGtMode: () => void onGroundTruth: (gt: ColumnGroundTruth) => void onNext: () => void isDetecting: boolean + savedGtColumns: PageRegion[] | null } const TYPE_COLORS: Record = { @@ -42,9 +44,95 @@ const METHOD_LABELS: Record = { position_fallback: 'Fallback', } -export function ColumnControls({ columnResult, onRerun, onManualMode, onGroundTruth, onNext, isDetecting }: ColumnControlsProps) { +interface DiffRow { + index: number + autoCol: PageRegion | null + gtCol: PageRegion | null + diffX: number | null + diffW: number | null + typeMismatch: boolean +} + +/** Match auto columns to GT columns by overlap on X-axis (IoU > 50%) */ +function computeDiff(autoCols: PageRegion[], gtCols: PageRegion[]): DiffRow[] { + const rows: DiffRow[] = [] + const usedGt = new Set() + const usedAuto = new Set() + + // Match auto → GT by best X-axis overlap + for (let ai = 0; ai < autoCols.length; ai++) { + const a = autoCols[ai] + let bestIdx = -1 + let bestIoU = 0 + + for (let gi = 0; gi < gtCols.length; gi++) { + if (usedGt.has(gi)) continue + const g = gtCols[gi] + const overlapStart = Math.max(a.x, g.x) + const overlapEnd = Math.min(a.x + a.width, g.x + g.width) + const overlap = Math.max(0, overlapEnd - overlapStart) + const union = (a.width + g.width) - overlap + const iou = union > 0 ? overlap / union : 0 + if (iou > bestIoU) { + bestIoU = iou + bestIdx = gi + } + } + + if (bestIdx >= 0 && bestIoU > 0.3) { + usedGt.add(bestIdx) + usedAuto.add(ai) + const g = gtCols[bestIdx] + rows.push({ + index: rows.length + 1, + autoCol: a, + gtCol: g, + diffX: g.x - a.x, + diffW: g.width - a.width, + typeMismatch: a.type !== g.type, + }) + } + } + + // Unmatched auto columns + for (let ai = 0; ai < autoCols.length; ai++) { + if (usedAuto.has(ai)) continue + rows.push({ + index: rows.length + 1, + autoCol: autoCols[ai], + gtCol: null, + diffX: null, + diffW: null, + typeMismatch: false, + }) + } + + // Unmatched GT columns + for (let gi = 0; gi < gtCols.length; gi++) { + if (usedGt.has(gi)) continue + rows.push({ + index: rows.length + 1, + autoCol: null, + gtCol: gtCols[gi], + diffX: null, + diffW: null, + typeMismatch: false, + }) + } + + return rows +} + +export function ColumnControls({ columnResult, onRerun, onManualMode, onGtMode, onGroundTruth, onNext, isDetecting, savedGtColumns }: ColumnControlsProps) { const [gtSaved, setGtSaved] = useState(false) + const diffRows = useMemo(() => { + if (!columnResult || !savedGtColumns) return null + const autoCols = columnResult.columns.filter(c => c.type.startsWith('column') || c.type === 'page_ref') + const gtCols = savedGtColumns.filter(c => c.type.startsWith('column') || c.type === 'page_ref') + return computeDiff(autoCols, gtCols) + }, [columnResult, savedGtColumns]) + if (!columnResult) return null const columns = columnResult.columns.filter((c: PageRegion) => c.type.startsWith('column') || c.type === 'page_ref') @@ -58,7 +146,7 @@ export function ColumnControls({ columnResult, onRerun, onManualMode, onGroundTr return (
{/* Summary */} -
+
{columns.length} Spalten erkannt {columnResult.duration_seconds > 0 && ( @@ -78,6 +166,12 @@ export function ColumnControls({ columnResult, onRerun, onManualMode, onGroundTr > Manuell markieren +
{/* Column list */} @@ -114,6 +208,82 @@ export function ColumnControls({ columnResult, onRerun, onManualMode, onGroundTr ))}
+ {/* Diff table (Auto vs GT) */} + {diffRows && diffRows.length > 0 && ( +
+
+ Vergleich: Auto vs Ground Truth +
+
+ + + + + + + + + + + + {diffRows.map((row) => ( + 20) || (row.diffW !== null && Math.abs(row.diffW) > 20) + ? 'bg-amber-50 dark:bg-amber-900/10' + : '' + } + > + + + + + + + ))} + +
#Auto (Typ, x, w)GT (Typ, x, w)Diff XDiff W
{row.index} + {row.autoCol ? ( + + + {TYPE_LABELS[row.autoCol.type] || row.autoCol.type} + + {' '}{row.autoCol.x}, {row.autoCol.width} + + ) : ( + fehlt + )} + + {row.gtCol ? ( + + + {TYPE_LABELS[row.gtCol.type] || row.gtCol.type} + + {' '}{row.gtCol.x}, {row.gtCol.width} + + ) : ( + fehlt + )} + + {row.diffX !== null ? ( + 20 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-500'}> + {row.diffX > 0 ? '+' : ''}{row.diffX} + + ) : '—'} + + {row.diffW !== null ? ( + 20 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-500'}> + {row.diffW > 0 ? '+' : ''}{row.diffW} + + ) : '—'} +
+
+
+ )} + {/* Ground Truth + Navigation */}
diff --git a/admin-lehrer/components/ocr-pipeline/ManualColumnEditor.tsx b/admin-lehrer/components/ocr-pipeline/ManualColumnEditor.tsx index 4994410..a490120 100644 --- a/admin-lehrer/components/ocr-pipeline/ManualColumnEditor.tsx +++ b/admin-lehrer/components/ocr-pipeline/ManualColumnEditor.tsx @@ -47,6 +47,10 @@ interface ManualColumnEditorProps { onApply: (columns: PageRegion[]) => void onCancel: () => void applying: boolean + mode?: 'manual' | 'ground-truth' + layout?: 'two-column' | 'stacked' + initialDividers?: number[] + initialColumnTypes?: ColumnTypeKey[] } export function ManualColumnEditor({ @@ -56,13 +60,19 @@ export function ManualColumnEditor({ onApply, onCancel, applying, + mode = 'manual', + layout = 'two-column', + initialDividers, + initialColumnTypes, }: ManualColumnEditorProps) { const containerRef = useRef(null) - const [dividers, setDividers] = useState([]) // xPercent values - const [columnTypes, setColumnTypes] = useState([]) + const [dividers, setDividers] = useState(initialDividers ?? []) + const [columnTypes, setColumnTypes] = useState(initialColumnTypes ?? []) const [dragging, setDragging] = useState(null) const [imageLoaded, setImageLoaded] = useState(false) + const isGT = mode === 'ground-truth' + // Sync columnTypes length when dividers change useEffect(() => { const numColumns = dividers.length + 1 @@ -187,8 +197,8 @@ export function ManualColumnEditor({ return (
- {/* Two-column layout: image (left) + controls (right) */} -
+ {/* Layout: image + controls */} +
{/* Left: Interactive image */}
@@ -328,7 +338,11 @@ export function ManualColumnEditor({ disabled={dividers.length === 0 || applying} className="w-full px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed" > - {applying ? 'Wird gespeichert...' : `${dividers.length + 1} Spalten uebernehmen`} + {applying + ? 'Wird gespeichert...' + : isGT + ? `${dividers.length + 1} Spalten als Ground Truth speichern` + : `${dividers.length + 1} Spalten uebernehmen`}