fix(ocr-compare): Replace Ollama call in grid analysis with heuristic from comparison results
Ollama crashes when two concurrent vision requests hit the 32B model (compare-ocr + analyze-grid). The grid analysis was redundantly calling Ollama again even though compare-ocr already extracted all vocabulary. - compare-ocr now saves vocabulary in session for reuse - analyze-grid builds grid from session data (no Ollama, instant response) - Grid button disabled until comparison results are available - Added export-to-editor functionality Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -139,6 +139,10 @@ export default function OCRComparePage() {
|
|||||||
const [currentBlockNumber, setCurrentBlockNumber] = useState(1)
|
const [currentBlockNumber, setCurrentBlockNumber] = useState(1)
|
||||||
const [blockReviewData, setBlockReviewData] = useState<Record<number, BlockReviewData>>({})
|
const [blockReviewData, setBlockReviewData] = useState<Record<number, BlockReviewData>>({})
|
||||||
|
|
||||||
|
// Export State
|
||||||
|
const [isExporting, setIsExporting] = useState(false)
|
||||||
|
const [exportSuccess, setExportSuccess] = useState(false)
|
||||||
|
|
||||||
const KLAUSUR_API = '/klausur-api'
|
const KLAUSUR_API = '/klausur-api'
|
||||||
|
|
||||||
// Load session history
|
// Load session history
|
||||||
@@ -535,6 +539,72 @@ export default function OCRComparePage() {
|
|||||||
}
|
}
|
||||||
}, [gridData])
|
}, [gridData])
|
||||||
|
|
||||||
|
// Export to Worksheet Editor
|
||||||
|
const handleExportToEditor = useCallback(async () => {
|
||||||
|
if (!gridData || !sessionId) return
|
||||||
|
|
||||||
|
setIsExporting(true)
|
||||||
|
setExportSuccess(false)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert grid cells (percent coordinates) to mm for A4
|
||||||
|
const A4_WIDTH_MM = 210
|
||||||
|
const A4_HEIGHT_MM = 297
|
||||||
|
|
||||||
|
const words = gridData.cells.flat()
|
||||||
|
.filter(cell => cell.status !== 'empty' && cell.text)
|
||||||
|
.map(cell => ({
|
||||||
|
text: cell.text,
|
||||||
|
x_mm: (cell.x / 100) * A4_WIDTH_MM,
|
||||||
|
y_mm: (cell.y / 100) * A4_HEIGHT_MM,
|
||||||
|
width_mm: (cell.width / 100) * A4_WIDTH_MM,
|
||||||
|
height_mm: (cell.height / 100) * A4_HEIGHT_MM,
|
||||||
|
column_type: cell.column_type || 'unknown',
|
||||||
|
logical_row: cell.row,
|
||||||
|
confidence: cell.confidence,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const detectedColumns = gridData.column_types.map((type, idx) => ({
|
||||||
|
column_type: type,
|
||||||
|
x_start_mm: (gridData.column_boundaries[idx] / 100) * A4_WIDTH_MM,
|
||||||
|
x_end_mm: (gridData.column_boundaries[idx + 1] / 100) * A4_WIDTH_MM,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const exportData = {
|
||||||
|
version: '1.0',
|
||||||
|
source: 'ocr-compare',
|
||||||
|
exported_at: new Date().toISOString(),
|
||||||
|
session_id: sessionId,
|
||||||
|
page_number: selectedPage + 1,
|
||||||
|
page_dimensions: {
|
||||||
|
width_mm: A4_WIDTH_MM,
|
||||||
|
height_mm: A4_HEIGHT_MM,
|
||||||
|
format: 'A4',
|
||||||
|
},
|
||||||
|
words,
|
||||||
|
detected_columns: detectedColumns,
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`${KLAUSUR_API}/api/v1/vocab/sessions/${sessionId}/ocr-export/${selectedPage + 1}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(exportData),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
setExportSuccess(true)
|
||||||
|
setTimeout(() => setExportSuccess(false), 3000)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Export failed:', e)
|
||||||
|
} finally {
|
||||||
|
setIsExporting(false)
|
||||||
|
}
|
||||||
|
}, [gridData, sessionId, selectedPage, KLAUSUR_API])
|
||||||
|
|
||||||
// Count non-empty blocks
|
// Count non-empty blocks
|
||||||
const nonEmptyBlockCount = useMemo(() => {
|
const nonEmptyBlockCount = useMemo(() => {
|
||||||
if (!gridData) return 0
|
if (!gridData) return 0
|
||||||
@@ -831,6 +901,35 @@ export default function OCRComparePage() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Export to Editor Button */}
|
||||||
|
<button
|
||||||
|
onClick={handleExportToEditor}
|
||||||
|
disabled={isExporting}
|
||||||
|
className={`w-full px-4 py-2 rounded-lg font-medium text-sm transition-colors ${
|
||||||
|
exportSuccess
|
||||||
|
? 'bg-green-100 text-green-700'
|
||||||
|
: 'bg-green-600 text-white hover:bg-green-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
{isExporting ? (
|
||||||
|
<svg className="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
) : exportSuccess ? (
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{exportSuccess ? 'Exportiert!' : 'Zum Editor exportieren'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1015,11 +1114,25 @@ export default function OCRComparePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{thumbnails[selectedPage] ? (
|
{thumbnails[selectedPage] ? (
|
||||||
<img
|
gridData && showGridOverlay ? (
|
||||||
src={thumbnails[selectedPage]}
|
<GridOverlay
|
||||||
alt={`Seite ${selectedPage + 1}`}
|
grid={gridData}
|
||||||
className={`rounded-lg border border-slate-200 ${isFullscreen ? 'max-h-[80vh] mx-auto' : 'w-full max-w-2xl mx-auto'}`}
|
imageUrl={thumbnails[selectedPage]}
|
||||||
/>
|
onCellClick={handleCellClick}
|
||||||
|
selectedCell={selectedCell}
|
||||||
|
showEmpty={false}
|
||||||
|
showNumbers={blockReviewMode}
|
||||||
|
showTextLabels={true}
|
||||||
|
highlightedBlockNumber={blockReviewMode ? currentBlockNumber : null}
|
||||||
|
className={`rounded-lg border border-slate-200 overflow-hidden ${isFullscreen ? 'max-h-[80vh] mx-auto' : 'w-full max-w-2xl mx-auto'}`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={thumbnails[selectedPage]}
|
||||||
|
alt={`Seite ${selectedPage + 1}`}
|
||||||
|
className={`rounded-lg border border-slate-200 ${isFullscreen ? 'max-h-[80vh] mx-auto' : 'w-full max-w-2xl mx-auto'}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="h-96 bg-slate-100 rounded-lg flex items-center justify-center text-slate-500">
|
<div className="h-96 bg-slate-100 rounded-lg flex items-center justify-center text-slate-500">
|
||||||
Kein Bild verfuegbar
|
Kein Bild verfuegbar
|
||||||
@@ -1116,6 +1229,7 @@ export default function OCRComparePage() {
|
|||||||
selectedCell={selectedCell}
|
selectedCell={selectedCell}
|
||||||
showEmpty={false}
|
showEmpty={false}
|
||||||
showNumbers={blockReviewMode}
|
showNumbers={blockReviewMode}
|
||||||
|
showTextLabels={!blockReviewMode}
|
||||||
highlightedBlockNumber={blockReviewMode ? currentBlockNumber : null}
|
highlightedBlockNumber={blockReviewMode ? currentBlockNumber : null}
|
||||||
className="rounded-lg border border-slate-200 overflow-hidden"
|
className="rounded-lg border border-slate-200 overflow-hidden"
|
||||||
/>
|
/>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user