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 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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<OcrBlockResult> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE ocr_jobs ADD COLUMN progress_message TEXT;
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -128,8 +128,30 @@ async function reviewToggle(blockId: string) {
|
||||
}
|
||||
|
||||
let ocrRunning = $state(false);
|
||||
let ocrProgressMessage = $state('');
|
||||
let ocrPollTimer = $state<ReturnType<typeof setInterval> | 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(() => {
|
||||
<p class="text-xs font-bold tracking-widest text-gray-400 uppercase">
|
||||
{m.ocr_progress_heading()}
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-ink-2">
|
||||
{translateOcrProgress(ocrProgressMessage)}
|
||||
</p>
|
||||
</div>
|
||||
{:else if panelMode === 'read'}
|
||||
<TranscriptionReadView
|
||||
|
||||
Reference in New Issue
Block a user