From a3e2a7f994afe9c78d485298fac16d75489d6406 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 18 Mar 2026 14:49:02 +0100 Subject: [PATCH] Add GT button to OCR overlay, prominent category picker, track pipeline - Ground Truth button on last step of Pipeline/Kombi modes in ocr-overlay - Prominent category picker in active session info bar (pulses when unset) - GT badge shown when session has ground truth reference - Backend: auto-detect pipeline from ocr_engine, store in GT snapshot - Pipeline info shown in GT session list and regression reports - Also pass pipeline param from ocr-pipeline StepGroundTruth Co-Authored-By: Claude Opus 4.6 --- .../app/(admin)/ai/ocr-overlay/page.tsx | 99 +++++++++++++++++-- .../ocr-pipeline/StepGroundTruth.tsx | 2 +- .../backend/ocr_pipeline_regression.py | 31 +++++- 3 files changed, 120 insertions(+), 12 deletions(-) diff --git a/admin-lehrer/app/(admin)/ai/ocr-overlay/page.tsx b/admin-lehrer/app/(admin)/ai/ocr-overlay/page.tsx index 8e03dbe..d156af8 100644 --- a/admin-lehrer/app/(admin)/ai/ocr-overlay/page.tsx +++ b/admin-lehrer/app/(admin)/ai/ocr-overlay/page.tsx @@ -28,6 +28,10 @@ export default function OcrOverlayPage() { const [editNameValue, setEditNameValue] = useState('') const [editingCategory, setEditingCategory] = useState(null) const [activeCategory, setActiveCategory] = useState(undefined) + const [editingActiveCategory, setEditingActiveCategory] = useState(false) + const [isGroundTruth, setIsGroundTruth] = useState(false) + const [gtSaving, setGtSaving] = useState(false) + const [gtMessage, setGtMessage] = useState('') const [steps, setSteps] = useState( OVERLAY_PIPELINE_STEPS.map((s, i) => ({ ...s, @@ -64,6 +68,8 @@ export default function OcrOverlayPage() { setSessionId(sid) setSessionName(data.name || data.filename || '') setActiveCategory(data.document_category || undefined) + setIsGroundTruth(!!data.ground_truth?.build_grid_reference) + setGtMessage('') // Check if this session was processed with paddle_direct, kombi, or rapid_kombi const ocrEngine = data.word_result?.ocr_engine @@ -239,6 +245,33 @@ export default function OcrOverlayPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [sessionId, goToStep]) + const handleMarkGroundTruth = async () => { + if (!sessionId) return + setGtSaving(true) + setGtMessage('') + try { + const resp = await fetch( + `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/mark-ground-truth?pipeline=${mode}`, + { method: 'POST' } + ) + if (!resp.ok) { + const body = await resp.text().catch(() => '') + throw new Error(`Ground Truth fehlgeschlagen (${resp.status}): ${body}`) + } + const data = await resp.json() + setIsGroundTruth(true) + setGtMessage(`Ground Truth gespeichert (${data.cells_saved} Zellen)`) + setTimeout(() => setGtMessage(''), 5000) + } catch (e) { + setGtMessage(e instanceof Error ? e.message : String(e)) + } finally { + setGtSaving(false) + } + } + + const isLastStep = currentStep === steps.length - 1 + const showGtButton = isLastStep && sessionId != null + const renderStep = () => { if (mode === 'paddle-direct' || mode === 'kombi') { switch (currentStep) { @@ -472,14 +505,48 @@ export default function OcrOverlayPage() { )} - {/* Active session info */} + {/* Active session info + category picker */} {sessionId && sessionName && ( -
+
Aktive Session: {sessionName} - {activeCategory && (() => { - const cat = DOCUMENT_CATEGORIES.find(c => c.value === activeCategory) - return cat ? {cat.icon} {cat.label} : null - })()} + + {isGroundTruth && ( + + GT + + )} + {editingActiveCategory && ( +
+ {DOCUMENT_CATEGORIES.map((cat) => ( + + ))} +
+ )}
)} @@ -543,6 +610,26 @@ export default function OcrOverlayPage() { />
{renderStep()}
+ + {/* Ground Truth button bar — visible on last step */} + {showGtButton && ( +
+
+ {gtMessage && ( + + {gtMessage} + + )} +
+ +
+ )}
) } diff --git a/admin-lehrer/components/ocr-pipeline/StepGroundTruth.tsx b/admin-lehrer/components/ocr-pipeline/StepGroundTruth.tsx index e2697bf..224047b 100644 --- a/admin-lehrer/components/ocr-pipeline/StepGroundTruth.tsx +++ b/admin-lehrer/components/ocr-pipeline/StepGroundTruth.tsx @@ -210,7 +210,7 @@ export function StepGroundTruth({ sessionId, onNext }: StepGroundTruthProps) { setGtMessage('') try { const resp = await fetch( - `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/mark-ground-truth`, + `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/mark-ground-truth?pipeline=ocr-pipeline`, { method: 'POST' } ) if (!resp.ok) { diff --git a/klausur-service/backend/ocr_pipeline_regression.py b/klausur-service/backend/ocr_pipeline_regression.py index 015df26..47f206f 100644 --- a/klausur-service/backend/ocr_pipeline_regression.py +++ b/klausur-service/backend/ocr_pipeline_regression.py @@ -12,7 +12,7 @@ import logging from datetime import datetime, timezone from typing import Any, Dict, List, Optional -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, HTTPException, Query from grid_editor_api import _build_grid_core from ocr_pipeline_session_store import ( @@ -49,7 +49,10 @@ def _extract_cells_for_comparison(grid_result: dict) -> List[Dict[str, Any]]: return cells -def _build_reference_snapshot(grid_result: dict) -> dict: +def _build_reference_snapshot( + grid_result: dict, + pipeline: Optional[str] = None, +) -> dict: """Build a ground-truth reference snapshot from a grid_editor_result.""" cells = _extract_cells_for_comparison(grid_result) @@ -57,9 +60,10 @@ def _build_reference_snapshot(grid_result: dict) -> dict: total_columns = sum(len(z.get("columns", [])) for z in grid_result.get("zones", [])) total_rows = sum(len(z.get("rows", [])) for z in grid_result.get("zones", [])) - return { + snapshot = { "saved_at": datetime.now(timezone.utc).isoformat(), "version": 1, + "pipeline": pipeline, "summary": { "total_zones": total_zones, "total_columns": total_columns, @@ -68,6 +72,7 @@ def _build_reference_snapshot(grid_result: dict) -> dict: }, "cells": cells, } + return snapshot def compare_grids(reference: dict, current: dict) -> dict: @@ -160,7 +165,10 @@ def compare_grids(reference: dict, current: dict) -> dict: # --------------------------------------------------------------------------- @router.post("/sessions/{session_id}/mark-ground-truth") -async def mark_ground_truth(session_id: str): +async def mark_ground_truth( + session_id: str, + pipeline: Optional[str] = Query(None, description="Pipeline used: kombi, pipeline, paddle-direct"), +): """Save the current build-grid result as ground-truth reference.""" session = await get_session_db(session_id) if not session: @@ -173,7 +181,18 @@ async def mark_ground_truth(session_id: str): detail="No grid_editor_result found. Run build-grid first.", ) - reference = _build_reference_snapshot(grid_result) + # Auto-detect pipeline from word_result if not provided + if not pipeline: + wr = session.get("word_result") or {} + engine = wr.get("ocr_engine", "") + if engine in ("kombi", "rapid_kombi"): + pipeline = "kombi" + elif engine == "paddle_direct": + pipeline = "paddle-direct" + else: + pipeline = "pipeline" + + reference = _build_reference_snapshot(grid_result, pipeline=pipeline) # Merge into existing ground_truth JSONB gt = session.get("ground_truth") or {} @@ -224,6 +243,8 @@ async def list_ground_truth_sessions(): "session_id": s["id"], "name": s.get("name", ""), "filename": s.get("filename", ""), + "document_category": s.get("document_category"), + "pipeline": ref.get("pipeline"), "saved_at": ref.get("saved_at"), "summary": ref.get("summary", {}), })