diff --git a/admin-lehrer/app/(admin)/ai/ocr-kombi/page.tsx b/admin-lehrer/app/(admin)/ai/ocr-kombi/page.tsx index 9eafaee..a7e7b8d 100644 --- a/admin-lehrer/app/(admin)/ai/ocr-kombi/page.tsx +++ b/admin-lehrer/app/(admin)/ai/ocr-kombi/page.tsx @@ -40,9 +40,9 @@ function OcrKombiContent() { deleteSession, renameSession, updateCategory, - handleOrientationComplete, handleSessionChange, setSessionId, + setSessionName, setSubSessions, setParentSessionId, setIsGroundTruth, @@ -54,8 +54,9 @@ function OcrKombiContent() { return ( { + onUploaded={(sid, name) => { setSessionId(sid) + setSessionName(name) loadSessions() }} onNext={handleNext} @@ -65,7 +66,7 @@ function OcrKombiContent() { return ( handleNext()} onSessionList={() => { loadSessions(); handleNewSession() }} /> ) @@ -73,10 +74,19 @@ function OcrKombiContent() { return ( { + // If sub-sessions were created, switch to the first one + if (subSessions.length > 0) { + setSessionId(subSessions[0].id) + setSessionName(subSessions[0].name) + } + handleNext() + }} onSubSessionsCreated={(subs) => { setSubSessions(subs) if (sessionId) setParentSessionId(sessionId) + loadSessions() }} /> ) diff --git a/admin-lehrer/app/(admin)/ai/ocr-kombi/useKombiPipeline.ts b/admin-lehrer/app/(admin)/ai/ocr-kombi/useKombiPipeline.ts index ac2b0e6..2bea943 100644 --- a/admin-lehrer/app/(admin)/ai/ocr-kombi/useKombiPipeline.ts +++ b/admin-lehrer/app/(admin)/ai/ocr-kombi/useKombiPipeline.ts @@ -356,6 +356,7 @@ export function useKombiPipeline() { setSessionId, setSubSessions, setParentSessionId, + setSessionName, setIsGroundTruth, } } diff --git a/admin-lehrer/components/ocr-kombi/StepOrientation.tsx b/admin-lehrer/components/ocr-kombi/StepOrientation.tsx index e0455e4..09a3c04 100644 --- a/admin-lehrer/components/ocr-kombi/StepOrientation.tsx +++ b/admin-lehrer/components/ocr-kombi/StepOrientation.tsx @@ -4,17 +4,17 @@ import { StepOrientation as BaseStepOrientation } from '@/components/ocr-pipelin interface StepOrientationProps { sessionId: string | null - onNext: (sessionId: string) => void + onNext: () => void onSessionList: () => void } -/** Thin wrapper around the shared StepOrientation component */ +/** Thin wrapper — adapts the shared StepOrientation to the Kombi pipeline's simpler onNext() */ export function StepOrientation({ sessionId, onNext, onSessionList }: StepOrientationProps) { return ( onNext()} onSessionList={onSessionList} /> ) diff --git a/admin-lehrer/components/ocr-kombi/StepPageSplit.tsx b/admin-lehrer/components/ocr-kombi/StepPageSplit.tsx index 3e890e4..7899c7b 100644 --- a/admin-lehrer/components/ocr-kombi/StepPageSplit.tsx +++ b/admin-lehrer/components/ocr-kombi/StepPageSplit.tsx @@ -1,123 +1,201 @@ 'use client' -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import type { SubSession } from '@/app/(admin)/ai/ocr-pipeline/types' const KLAUSUR_API = '/klausur-api' +interface PageSplitResult { + multi_page: boolean + page_count?: number + page_splits?: { x: number; y: number; width: number; height: number; page_index: number }[] + sub_sessions?: { id: string; name: string; page_index: number }[] + used_original?: boolean + duration_seconds?: number +} + interface StepPageSplitProps { sessionId: string | null + sessionName: string onNext: () => void onSubSessionsCreated: (subs: SubSession[]) => void } -/** - * Step 3: Page split detection. - * Checks if the image is a double-page spread and offers to split it. - * If no split needed, auto-advances. - */ -export function StepPageSplit({ sessionId, onNext, onSubSessionsCreated }: StepPageSplitProps) { - const [checking, setChecking] = useState(false) - const [splitResult, setSplitResult] = useState<{ is_double_page: boolean; pages?: number } | null>(null) - const [splitting, setSplitting] = useState(false) +export function StepPageSplit({ sessionId, sessionName, onNext, onSubSessionsCreated }: StepPageSplitProps) { + const [detecting, setDetecting] = useState(false) + const [splitResult, setSplitResult] = useState(null) const [error, setError] = useState('') + const didDetect = useRef(false) + // Auto-detect page split when step opens useEffect(() => { - if (!sessionId) return - // Auto-check for page split - checkPageSplit() + if (!sessionId || didDetect.current) return + didDetect.current = true + detectPageSplit() // eslint-disable-next-line react-hooks/exhaustive-deps }, [sessionId]) - const checkPageSplit = async () => { + const detectPageSplit = async () => { if (!sessionId) return - setChecking(true) + setDetecting(true) setError('') try { - const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}`) - if (!res.ok) throw new Error('Session nicht gefunden') - const data = await res.json() - - // If sub-sessions already exist, this was already split - if (data.sub_sessions?.length > 0) { - onSubSessionsCreated(data.sub_sessions) - onNext() - return + // First check if sub-sessions already exist + const sessionRes = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}`) + if (sessionRes.ok) { + const sessionData = await sessionRes.json() + if (sessionData.sub_sessions?.length > 0) { + // Already split — show existing sub-sessions + const subs = sessionData.sub_sessions as { id: string; name: string; page_index?: number; box_index?: number; current_step?: number }[] + setSplitResult({ + multi_page: true, + page_count: subs.length, + sub_sessions: subs.map((s: { id: string; name: string; page_index?: number; box_index?: number }) => ({ + id: s.id, + name: s.name, + page_index: s.page_index ?? s.box_index ?? 0, + })), + }) + onSubSessionsCreated(subs.map((s: { id: string; name: string; page_index?: number; box_index?: number; current_step?: number }) => ({ + id: s.id, + name: s.name, + box_index: s.page_index ?? s.box_index ?? 0, + current_step: s.current_step ?? 2, + }))) + setDetecting(false) + return + } } - // Check aspect ratio to guess if double-page - // For now, just auto-advance (page-split detection happens in orientation step) - setSplitResult({ is_double_page: false }) - // Auto-advance if single page - onNext() - } catch (e) { - setError(e instanceof Error ? e.message : String(e)) - } finally { - setChecking(false) - } - } - - const handleSplit = async () => { - if (!sessionId) return - setSplitting(true) - setError('') - try { + // Run page-split detection const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/page-split`, { method: 'POST', }) if (!res.ok) { const data = await res.json().catch(() => ({})) - throw new Error(data.detail || 'Split fehlgeschlagen') + throw new Error(data.detail || 'Seitentrennung fehlgeschlagen') } - const data = await res.json() - if (data.sub_sessions?.length > 0) { - onSubSessionsCreated(data.sub_sessions) + const data: PageSplitResult = await res.json() + setSplitResult(data) + + if (data.multi_page && data.sub_sessions?.length) { + // Rename sub-sessions to "Title — S. 1", "Title — S. 2" + const baseName = sessionName || 'Dokument' + for (let i = 0; i < data.sub_sessions.length; i++) { + const sub = data.sub_sessions[i] + const newName = `${baseName} — S. ${i + 1}` + await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sub.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: newName }), + }).catch(() => {}) + sub.name = newName + } + + onSubSessionsCreated(data.sub_sessions.map(s => ({ + id: s.id, + name: s.name, + box_index: s.page_index, + current_step: 2, + }))) } - onNext() } catch (e) { setError(e instanceof Error ? e.message : String(e)) } finally { - setSplitting(false) + setDetecting(false) } } - if (checking) { - return
Pruefe Seitenformat...
- } + if (!sessionId) return null - if (splitResult?.is_double_page) { - return ( -
-

- Doppelseite erkannt -

-

- Das Bild scheint eine Doppelseite zu sein. Soll es in zwei Einzelseiten aufgeteilt werden? -

-
- - -
- {error &&
{error}
} -
- ) - } + const imageUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/oriented` return ( -
- Einzelseite erkannt — weiter zum naechsten Schritt. - {error &&
{error}
} +
+ {/* Image */} +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Orientiertes Bild { + // Fallback to non-oriented image + (e.target as HTMLImageElement).src = + `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image` + }} + /> +
+ + {/* Detection status */} + {detecting && ( +
+
+ Doppelseiten-Erkennung laeuft... +
+ )} + + {/* Detection result */} + {splitResult && !detecting && ( + splitResult.multi_page ? ( +
+
+ Doppelseite erkannt — {splitResult.page_count} Seiten getrennt +
+

+ Jede Seite wird als eigene Session weiterverarbeitet (eigene Begradigung, Entzerrung, etc.). + {splitResult.used_original && ' Trennung auf Originalbild, da Orientierung die Doppelseite gedreht hat.'} +

+
+ {splitResult.sub_sessions?.map(s => ( + + {s.name} + + ))} +
+ {splitResult.duration_seconds != null && ( +
{splitResult.duration_seconds.toFixed(1)}s
+ )} +
+ ) : ( +
+
+ Einzelseite — keine Trennung noetig +
+ {splitResult.duration_seconds != null && ( +
{splitResult.duration_seconds.toFixed(1)}s
+ )} +
+ ) + )} + + {/* Error */} + {error && ( +
+ {error} + +
+ )} + + {/* Next button — only show when detection is done */} + {(splitResult || error) && !detecting && ( +
+ +
+ )}
) } diff --git a/admin-lehrer/components/ocr-kombi/StepUpload.tsx b/admin-lehrer/components/ocr-kombi/StepUpload.tsx index 5c21aa4..356e9be 100644 --- a/admin-lehrer/components/ocr-kombi/StepUpload.tsx +++ b/admin-lehrer/components/ocr-kombi/StepUpload.tsx @@ -7,7 +7,7 @@ const KLAUSUR_API = '/klausur-api' interface StepUploadProps { sessionId: string | null - onUploaded: (sessionId: string) => void + onUploaded: (sessionId: string, name: string) => void onNext: () => void } @@ -71,7 +71,7 @@ export function StepUpload({ sessionId, onUploaded, onNext }: StepUploadProps) { }) } - onUploaded(sid) + onUploaded(sid, title.trim() || selectedFile.name) } catch (e) { setError(e instanceof Error ? e.message : String(e)) } finally {