feat(ocr-pipeline): ground-truth comparison tool for column detection
Side-by-side view: auto result (readonly) vs GT editor where teacher draws correct columns. Diff table shows Auto vs GT with IoU matching. GT data persisted per session for algorithm tuning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,7 @@ export interface ColumnResult {
|
||||
|
||||
export interface ColumnGroundTruth {
|
||||
is_correct: boolean
|
||||
corrected_columns?: PageRegion[]
|
||||
notes?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, string> = {
|
||||
@@ -42,9 +44,95 @@ const METHOD_LABELS: Record<string, string> = {
|
||||
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<number>()
|
||||
const usedAuto = new Set<number>()
|
||||
|
||||
// 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 (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 space-y-4">
|
||||
{/* Summary */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span className="font-medium text-gray-800 dark:text-gray-200">{columns.length} Spalten</span> erkannt
|
||||
{columnResult.duration_seconds > 0 && (
|
||||
@@ -78,6 +166,12 @@ export function ColumnControls({ columnResult, onRerun, onManualMode, onGroundTr
|
||||
>
|
||||
Manuell markieren
|
||||
</button>
|
||||
<button
|
||||
onClick={onGtMode}
|
||||
className="text-xs px-2 py-1 bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400 rounded hover:bg-amber-200 dark:hover:bg-amber-900/50 transition-colors"
|
||||
>
|
||||
{savedGtColumns ? 'Ground Truth bearbeiten' : 'Ground Truth eintragen'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Column list */}
|
||||
@@ -114,6 +208,82 @@ export function ColumnControls({ columnResult, onRerun, onManualMode, onGroundTr
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Diff table (Auto vs GT) */}
|
||||
{diffRows && diffRows.length > 0 && (
|
||||
<div className="border-t border-gray-100 dark:border-gray-700 pt-3">
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
Vergleich: Auto vs Ground Truth
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-gray-500 dark:text-gray-400 border-b border-gray-100 dark:border-gray-700">
|
||||
<th className="text-left py-1 pr-2">#</th>
|
||||
<th className="text-left py-1 pr-2">Auto (Typ, x, w)</th>
|
||||
<th className="text-left py-1 pr-2">GT (Typ, x, w)</th>
|
||||
<th className="text-right py-1 pr-2">Diff X</th>
|
||||
<th className="text-right py-1">Diff W</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{diffRows.map((row) => (
|
||||
<tr
|
||||
key={row.index}
|
||||
className={
|
||||
!row.autoCol || !row.gtCol || row.typeMismatch
|
||||
? 'bg-red-50 dark:bg-red-900/10'
|
||||
: (row.diffX !== null && Math.abs(row.diffX) > 20) || (row.diffW !== null && Math.abs(row.diffW) > 20)
|
||||
? 'bg-amber-50 dark:bg-amber-900/10'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<td className="py-1 pr-2 font-mono text-gray-400">{row.index}</td>
|
||||
<td className="py-1 pr-2 font-mono">
|
||||
{row.autoCol ? (
|
||||
<span>
|
||||
<span className={`inline-block px-1 rounded ${TYPE_COLORS[row.autoCol.type] || ''}`}>
|
||||
{TYPE_LABELS[row.autoCol.type] || row.autoCol.type}
|
||||
</span>
|
||||
{' '}{row.autoCol.x}, {row.autoCol.width}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-red-400">fehlt</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-1 pr-2 font-mono">
|
||||
{row.gtCol ? (
|
||||
<span>
|
||||
<span className={`inline-block px-1 rounded ${TYPE_COLORS[row.gtCol.type] || ''}`}>
|
||||
{TYPE_LABELS[row.gtCol.type] || row.gtCol.type}
|
||||
</span>
|
||||
{' '}{row.gtCol.x}, {row.gtCol.width}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-red-400">fehlt</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-1 pr-2 text-right font-mono">
|
||||
{row.diffX !== null ? (
|
||||
<span className={Math.abs(row.diffX) > 20 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-500'}>
|
||||
{row.diffX > 0 ? '+' : ''}{row.diffX}
|
||||
</span>
|
||||
) : '—'}
|
||||
</td>
|
||||
<td className="py-1 text-right font-mono">
|
||||
{row.diffW !== null ? (
|
||||
<span className={Math.abs(row.diffW) > 20 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-500'}>
|
||||
{row.diffW > 0 ? '+' : ''}{row.diffW}
|
||||
</span>
|
||||
) : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Ground Truth + Navigation */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-gray-100 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -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<HTMLDivElement>(null)
|
||||
const [dividers, setDividers] = useState<number[]>([]) // xPercent values
|
||||
const [columnTypes, setColumnTypes] = useState<ColumnTypeKey[]>([])
|
||||
const [dividers, setDividers] = useState<number[]>(initialDividers ?? [])
|
||||
const [columnTypes, setColumnTypes] = useState<ColumnTypeKey[]>(initialColumnTypes ?? [])
|
||||
const [dragging, setDragging] = useState<number | null>(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 (
|
||||
<div className="space-y-4">
|
||||
{/* Two-column layout: image (left) + controls (right) */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Layout: image + controls */}
|
||||
<div className={layout === 'stacked' ? 'space-y-4' : 'grid grid-cols-2 gap-4'}>
|
||||
{/* Left: Interactive image */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
@@ -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`}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDividers([])}
|
||||
|
||||
@@ -4,21 +4,44 @@ import { useCallback, useEffect, useState } from 'react'
|
||||
import type { ColumnResult, ColumnGroundTruth, PageRegion } from '@/app/(admin)/ai/ocr-pipeline/types'
|
||||
import { ColumnControls } from './ColumnControls'
|
||||
import { ManualColumnEditor } from './ManualColumnEditor'
|
||||
import type { ColumnTypeKey } from '@/app/(admin)/ai/ocr-pipeline/types'
|
||||
|
||||
const KLAUSUR_API = '/klausur-api'
|
||||
|
||||
type ViewMode = 'normal' | 'ground-truth' | 'manual'
|
||||
|
||||
interface StepColumnDetectionProps {
|
||||
sessionId: string | null
|
||||
onNext: () => void
|
||||
}
|
||||
|
||||
/** Convert PageRegion[] to divider percentages + column types for ManualColumnEditor */
|
||||
function columnsToEditorState(
|
||||
columns: PageRegion[],
|
||||
imageWidth: number
|
||||
): { dividers: number[]; columnTypes: ColumnTypeKey[] } {
|
||||
if (!columns.length || !imageWidth) return { dividers: [], columnTypes: [] }
|
||||
|
||||
const sorted = [...columns].sort((a, b) => a.x - b.x)
|
||||
const dividers: number[] = []
|
||||
const columnTypes: ColumnTypeKey[] = sorted.map(c => c.type)
|
||||
|
||||
for (let i = 1; i < sorted.length; i++) {
|
||||
const xPct = (sorted[i].x / imageWidth) * 100
|
||||
dividers.push(xPct)
|
||||
}
|
||||
|
||||
return { dividers, columnTypes }
|
||||
}
|
||||
|
||||
export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionProps) {
|
||||
const [columnResult, setColumnResult] = useState<ColumnResult | null>(null)
|
||||
const [detecting, setDetecting] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [manualMode, setManualMode] = useState(false)
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('normal')
|
||||
const [applying, setApplying] = useState(false)
|
||||
const [imageDimensions, setImageDimensions] = useState<{ width: number; height: number } | null>(null)
|
||||
const [savedGtColumns, setSavedGtColumns] = useState<PageRegion[] | null>(null)
|
||||
|
||||
// Fetch session info (image dimensions) + check for cached column result
|
||||
useEffect(() => {
|
||||
@@ -49,6 +72,24 @@ export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionPr
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sessionId])
|
||||
|
||||
// Load saved GT if exists
|
||||
useEffect(() => {
|
||||
if (!sessionId) return
|
||||
const fetchGt = async () => {
|
||||
try {
|
||||
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/ground-truth/columns`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const corrected = data.columns_gt?.corrected_columns
|
||||
if (corrected) setSavedGtColumns(corrected)
|
||||
}
|
||||
} catch {
|
||||
// No saved GT - that's fine
|
||||
}
|
||||
}
|
||||
fetchGt()
|
||||
}, [sessionId])
|
||||
|
||||
const runAutoDetection = useCallback(async () => {
|
||||
if (!sessionId) return
|
||||
setDetecting(true)
|
||||
@@ -106,7 +147,30 @@ export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionPr
|
||||
columns: data.columns,
|
||||
duration_seconds: data.duration_seconds ?? 0,
|
||||
})
|
||||
setManualMode(false)
|
||||
setViewMode('normal')
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Fehler beim Speichern')
|
||||
} finally {
|
||||
setApplying(false)
|
||||
}
|
||||
}, [sessionId])
|
||||
|
||||
const handleGtApply = useCallback(async (columns: PageRegion[]) => {
|
||||
if (!sessionId) return
|
||||
setApplying(true)
|
||||
setError(null)
|
||||
try {
|
||||
const gt: ColumnGroundTruth = {
|
||||
is_correct: false,
|
||||
corrected_columns: columns,
|
||||
}
|
||||
await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/ground-truth/columns`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(gt),
|
||||
})
|
||||
setSavedGtColumns(columns)
|
||||
setViewMode('normal')
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Fehler beim Speichern')
|
||||
} finally {
|
||||
@@ -131,6 +195,11 @@ export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionPr
|
||||
const dewarpedUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/dewarped`
|
||||
const overlayUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/columns-overlay`
|
||||
|
||||
// Pre-compute editor state from saved GT or auto columns for GT mode
|
||||
const gtInitial = savedGtColumns
|
||||
? columnsToEditorState(savedGtColumns, imageDimensions?.width ?? 1000)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Loading indicator */}
|
||||
@@ -141,18 +210,77 @@ export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionPr
|
||||
</div>
|
||||
)}
|
||||
|
||||
{manualMode ? (
|
||||
/* Manual column editor */
|
||||
{viewMode === 'manual' ? (
|
||||
/* Manual column editor - overwrites column_result */
|
||||
<ManualColumnEditor
|
||||
imageUrl={dewarpedUrl}
|
||||
imageWidth={imageDimensions?.width ?? 1000}
|
||||
imageHeight={imageDimensions?.height ?? 1400}
|
||||
onApply={handleManualApply}
|
||||
onCancel={() => setManualMode(false)}
|
||||
onCancel={() => setViewMode('normal')}
|
||||
applying={applying}
|
||||
mode="manual"
|
||||
/>
|
||||
) : viewMode === 'ground-truth' ? (
|
||||
/* GT mode: auto result (left, readonly) + GT editor (right) */
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Left: Auto result (readonly overlay) */}
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||
Auto-Ergebnis (readonly)
|
||||
</div>
|
||||
<div className="border rounded-lg overflow-hidden dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
||||
{columnResult ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={`${overlayUrl}?t=${Date.now()}`}
|
||||
alt="Auto Spalten-Overlay"
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
) : (
|
||||
<div className="aspect-[3/4] flex items-center justify-center text-gray-400 text-sm">
|
||||
Keine Auto-Daten
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Auto column list */}
|
||||
{columnResult && (
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
Auto: {columnResult.columns.length} Spalten
|
||||
</div>
|
||||
{columnResult.columns
|
||||
.filter(c => c.type.startsWith('column') || c.type === 'page_ref')
|
||||
.map((col, i) => (
|
||||
<div key={i} className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
||||
{i + 1}. {col.type} x={col.x} w={col.width}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: GT editor */}
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||
Ground Truth Editor
|
||||
</div>
|
||||
<ManualColumnEditor
|
||||
imageUrl={dewarpedUrl}
|
||||
imageWidth={imageDimensions?.width ?? 1000}
|
||||
imageHeight={imageDimensions?.height ?? 1400}
|
||||
onApply={handleGtApply}
|
||||
onCancel={() => setViewMode('normal')}
|
||||
applying={applying}
|
||||
mode="ground-truth"
|
||||
layout="stacked"
|
||||
initialDividers={gtInitial?.dividers}
|
||||
initialColumnTypes={gtInitial?.columnTypes}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Image comparison: overlay (left) vs clean (right) */
|
||||
/* Normal mode: overlay (left) vs clean (right) */
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||
@@ -190,14 +318,16 @@ export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionPr
|
||||
)}
|
||||
|
||||
{/* Controls */}
|
||||
{!manualMode && (
|
||||
{viewMode === 'normal' && (
|
||||
<ColumnControls
|
||||
columnResult={columnResult}
|
||||
onRerun={handleRerun}
|
||||
onManualMode={() => setManualMode(true)}
|
||||
onManualMode={() => setViewMode('manual')}
|
||||
onGtMode={() => setViewMode('ground-truth')}
|
||||
onGroundTruth={handleGroundTruth}
|
||||
onNext={onNext}
|
||||
isDetecting={detecting}
|
||||
savedGtColumns={savedGtColumns}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -135,6 +135,7 @@ class ManualColumnsRequest(BaseModel):
|
||||
|
||||
class ColumnGroundTruthRequest(BaseModel):
|
||||
is_correct: bool
|
||||
corrected_columns: Optional[List[Dict[str, Any]]] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
@@ -711,6 +712,7 @@ async def save_column_ground_truth(session_id: str, req: ColumnGroundTruthReques
|
||||
ground_truth = session.get("ground_truth") or {}
|
||||
gt = {
|
||||
"is_correct": req.is_correct,
|
||||
"corrected_columns": req.corrected_columns,
|
||||
"notes": req.notes,
|
||||
"saved_at": datetime.utcnow().isoformat(),
|
||||
"column_result": session.get("column_result"),
|
||||
@@ -725,6 +727,25 @@ async def save_column_ground_truth(session_id: str, req: ColumnGroundTruthReques
|
||||
return {"session_id": session_id, "ground_truth": gt}
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}/ground-truth/columns")
|
||||
async def get_column_ground_truth(session_id: str):
|
||||
"""Retrieve saved ground truth for column detection, including auto vs GT diff."""
|
||||
session = await get_session_db(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
|
||||
|
||||
ground_truth = session.get("ground_truth") or {}
|
||||
columns_gt = ground_truth.get("columns")
|
||||
if not columns_gt:
|
||||
raise HTTPException(status_code=404, detail="No column ground truth saved")
|
||||
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"columns_gt": columns_gt,
|
||||
"columns_auto": session.get("column_result"),
|
||||
}
|
||||
|
||||
|
||||
async def _get_columns_overlay(session_id: str) -> Response:
|
||||
"""Generate dewarped image with column borders drawn on it."""
|
||||
session = await get_session_db(session_id)
|
||||
|
||||
Reference in New Issue
Block a user