From 971527a50ede722139b619e6fb63a8e22afcd8c2 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 12 Apr 2026 23:31:23 +0200 Subject: [PATCH] feat(ocr): show translated progress messages during OCR processing Backend sends progress codes (PREPARING, LOADING, ANALYZING, CREATING_BLOCKS:N, DONE:N, ERROR) via OcrJob.progressMessage. Frontend translates them via Paraglide (de/en/es) and displays below the spinner. - V27 migration: adds progress_message column to ocr_jobs - OcrAsyncRunner updates progress at each phase - Poll interval reduced to 2s for snappier updates Refs #226 Co-Authored-By: Claude Sonnet 4.6 --- .../raddatz/familienarchiv/model/OcrJob.java | 3 ++ .../service/OcrAsyncRunner.java | 18 ++++++++++-- .../V27__add_progress_message_to_ocr_jobs.sql | 1 + frontend/messages/de.json | 6 ++++ frontend/messages/en.json | 6 ++++ frontend/messages/es.json | 6 ++++ .../src/routes/documents/[id]/+page.svelte | 29 ++++++++++++++++++- 7 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V27__add_progress_message_to_ocr_jobs.sql diff --git a/backend/src/main/java/org/raddatz/familienarchiv/model/OcrJob.java b/backend/src/main/java/org/raddatz/familienarchiv/model/OcrJob.java index 81f205fe..076d3ef3 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/model/OcrJob.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/model/OcrJob.java @@ -47,6 +47,9 @@ public class OcrJob { @Builder.Default private int skippedCount = 0; + @Column(name = "progress_message") + private String progressMessage; + @Column(name = "created_by") private UUID createdBy; diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/OcrAsyncRunner.java b/backend/src/main/java/org/raddatz/familienarchiv/service/OcrAsyncRunner.java index 610c3e2d..1b773748 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/OcrAsyncRunner.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/OcrAsyncRunner.java @@ -37,7 +37,7 @@ public class OcrAsyncRunner { if (job == null) return; job.setStatus(OcrJobStatus.RUNNING); - ocrJobRepository.save(job); + updateProgress(job, "PREPARING"); OcrJobDocument jobDoc = ocrJobDocumentRepository.findByJobIdAndDocumentId(jobId, documentId) .orElse(null); @@ -49,9 +49,19 @@ public class OcrAsyncRunner { Document doc = documentService.getDocumentById(documentId); try { - processDocument(documentId, doc, userId); + updateProgress(job, "LOADING"); + clearExistingBlocks(documentId); + String pdfUrl = fileService.generatePresignedUrl(doc.getFilePath()); + + updateProgress(job, "ANALYZING"); + List blocks = ocrClient.extractBlocks(pdfUrl, doc.getScriptType()); + + updateProgress(job, "CREATING_BLOCKS:" + blocks.size()); + createTranscriptionBlocks(documentId, blocks, userId, doc.getFileHash()); + job.setStatus(OcrJobStatus.DONE); job.setProcessedDocuments(1); + updateProgress(job, "DONE:" + blocks.size()); if (jobDoc != null) { jobDoc.setStatus(OcrDocumentStatus.DONE); ocrJobDocumentRepository.save(jobDoc); @@ -60,13 +70,17 @@ public class OcrAsyncRunner { log.error("OCR processing failed for document {}", documentId, e); job.setStatus(OcrJobStatus.FAILED); job.setErrorCount(1); + updateProgress(job, "ERROR"); if (jobDoc != null) { jobDoc.setStatus(OcrDocumentStatus.FAILED); jobDoc.setErrorMessage(e.getMessage()); ocrJobDocumentRepository.save(jobDoc); } } + } + private void updateProgress(OcrJob job, String message) { + job.setProgressMessage(message); ocrJobRepository.save(job); } diff --git a/backend/src/main/resources/db/migration/V27__add_progress_message_to_ocr_jobs.sql b/backend/src/main/resources/db/migration/V27__add_progress_message_to_ocr_jobs.sql new file mode 100644 index 00000000..0b8ed4d2 --- /dev/null +++ b/backend/src/main/resources/db/migration/V27__add_progress_message_to_ocr_jobs.sql @@ -0,0 +1 @@ +ALTER TABLE ocr_jobs ADD COLUMN progress_message TEXT; diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 3eac5fd0..a4221f78 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -521,6 +521,12 @@ "ocr_error_retry": "Erneut versuchen", "ocr_batch_running": "OCR läuft · {processed} von {total} Dokumente abgeschlossen", "ocr_batch_done": "OCR abgeschlossen · {processed} erfolgreich · {errors} fehlgeschlagen", + "ocr_status_preparing": "Dokument wird vorbereitet…", + "ocr_status_loading": "Lade Modell und Dokument…", + "ocr_status_analyzing": "OCR-Analyse läuft — dies kann einige Minuten dauern…", + "ocr_status_creating_blocks": "{count} Textblöcke erkannt — erstelle Transkription…", + "ocr_status_done_blocks": "{count} Blöcke erstellt", + "ocr_status_error": "OCR fehlgeschlagen", "transcription_block_review": "Als geprüft markieren", "transcription_block_unreview": "Markierung aufheben", "transcription_reviewed_count": "{reviewed} von {total} geprüft" diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 9853b3d7..e9546eae 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -521,6 +521,12 @@ "ocr_error_retry": "Try again", "ocr_batch_running": "OCR running · {processed} of {total} documents complete", "ocr_batch_done": "OCR complete · {processed} successful · {errors} failed", + "ocr_status_preparing": "Preparing document…", + "ocr_status_loading": "Loading model and document…", + "ocr_status_analyzing": "OCR analysis running — this may take a few minutes…", + "ocr_status_creating_blocks": "{count} text blocks detected — creating transcription…", + "ocr_status_done_blocks": "{count} blocks created", + "ocr_status_error": "OCR failed", "transcription_block_review": "Mark as reviewed", "transcription_block_unreview": "Unmark as reviewed", "transcription_reviewed_count": "{reviewed} of {total} reviewed" diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 9062c2ed..ce03d8eb 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -521,6 +521,12 @@ "ocr_error_retry": "Intentar de nuevo", "ocr_batch_running": "OCR en curso · {processed} de {total} documentos completados", "ocr_batch_done": "OCR completado · {processed} exitosos · {errors} fallidos", + "ocr_status_preparing": "Preparando documento…", + "ocr_status_loading": "Cargando modelo y documento…", + "ocr_status_analyzing": "Análisis OCR en curso — esto puede tardar unos minutos…", + "ocr_status_creating_blocks": "{count} bloques de texto detectados — creando transcripción…", + "ocr_status_done_blocks": "{count} bloques creados", + "ocr_status_error": "OCR fallido", "transcription_block_review": "Marcar como revisado", "transcription_block_unreview": "Desmarcar como revisado", "transcription_reviewed_count": "{reviewed} de {total} revisados" diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index 23d6a232..b3f3a6bf 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -128,8 +128,30 @@ async function reviewToggle(blockId: string) { } let ocrRunning = $state(false); +let ocrProgressMessage = $state(''); let ocrPollTimer = $state | null>(null); +function translateOcrProgress(code: string): string { + if (!code) return m.ocr_progress_heading(); + const [key, param] = code.split(':'); + switch (key) { + case 'PREPARING': + return m.ocr_status_preparing(); + case 'LOADING': + return m.ocr_status_loading(); + case 'ANALYZING': + return m.ocr_status_analyzing(); + case 'CREATING_BLOCKS': + return m.ocr_status_creating_blocks({ count: param ?? '0' }); + case 'DONE': + return m.ocr_status_done_blocks({ count: param ?? '0' }); + case 'ERROR': + return m.ocr_status_error(); + default: + return code; + } +} + async function triggerOcr(scriptType: string) { ocrRunning = true; try { @@ -157,10 +179,12 @@ function pollOcrJob(jobId: string) { const res = await fetch(`/api/ocr/jobs/${jobId}`); if (!res.ok) return; const job = await res.json(); + ocrProgressMessage = job.progressMessage ?? ''; if (job.status === 'DONE' || job.status === 'FAILED') { if (ocrPollTimer) clearInterval(ocrPollTimer); ocrPollTimer = null; ocrRunning = false; + ocrProgressMessage = ''; await loadTranscriptionBlocks(); annotationReloadKey++; panelMode = transcriptionBlocks.length > 0 ? 'read' : 'edit'; @@ -168,7 +192,7 @@ function pollOcrJob(jobId: string) { } catch { // polling is best-effort } - }, 3000); + }, 2000); } async function createBlockFromDraw(rect: { @@ -399,6 +423,9 @@ onMount(() => {

{m.ocr_progress_heading()}

+

+ {translateOcrProgress(ocrProgressMessage)} +

{:else if panelMode === 'read'}