feat(ocr): wire SenderModelService into OcrAsyncRunner; stage missing foundational files

OcrAsyncRunner now passes the per-sender model path to streamBlocks for
HANDWRITING_KURRENT documents. processDocument replaced extractBlocks
with streamBlocks + AtomicReference, removing the unchecked raw-array
pattern.

Also stages all previously uncommitted foundational files for this
feature: SenderModel entity, SenderModelRepository, Flyway migrations
V40/V41, updated OcrClient/RestClientOcrClient streaming API,
TrainingDataExportService.exportForSender, TranscriptionService Kurrent
hook, application.yaml OCR config, and frontend i18n/test additions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-17 19:27:02 +02:00
committed by marcel
parent a8bd2606a0
commit a296ad527e
19 changed files with 252 additions and 29 deletions

View File

@@ -561,6 +561,11 @@
"transcription_block_segmentation_only": "Nur Segmentierung",
"training_chip_kurrent": "Kurrent-Erkennung",
"training_chip_segmentation": "Segmentierung",
"training_col_type": "Typ",
"training_type_base": "Basis",
"training_type_personalized": "Personalisiert",
"training_col_person": "Absender",
"training_status_queued": "Warteschlange",
"mission_control_heading": "Was braucht Aufmerksamkeit?",
"mission_control_segmentation_heading": "Text markieren",
"mission_control_segmentation_description": "Textbereiche markieren — keine Vorkenntnisse nötig",

View File

@@ -561,6 +561,11 @@
"transcription_block_segmentation_only": "Segmentation only",
"training_chip_kurrent": "Kurrent recognition",
"training_chip_segmentation": "Segmentation",
"training_col_type": "Type",
"training_type_base": "Base",
"training_type_personalized": "Personalized",
"training_col_person": "Sender",
"training_status_queued": "Queued",
"mission_control_heading": "What needs attention?",
"mission_control_segmentation_heading": "Mark text",
"mission_control_segmentation_description": "Mark text areas — no prior knowledge needed",

View File

@@ -561,6 +561,11 @@
"transcription_block_segmentation_only": "Solo segmentación",
"training_chip_kurrent": "Reconocimiento Kurrent",
"training_chip_segmentation": "Segmentación",
"training_col_type": "Tipo",
"training_type_base": "Base",
"training_type_personalized": "Personalizado",
"training_col_person": "Remitente",
"training_status_queued": "En cola",
"mission_control_heading": "¿Qué necesita atención?",
"mission_control_segmentation_heading": "Marcar texto",
"mission_control_segmentation_description": "Marcar áreas de texto — sin conocimientos previos",

View File

@@ -50,3 +50,26 @@ describe('TrainingHistory — expand/collapse', () => {
.not.toBeInTheDocument();
});
});
describe('TrainingHistory — type and person columns', () => {
it('shows "Basis" for runs without personId', async () => {
render(TrainingHistory, { runs: [makeRun(0)] });
await expect.element(page.getByText(/Basis/i)).toBeInTheDocument();
});
it('shows "Personalisiert" for runs with personId', async () => {
const run = { ...makeRun(0), personId: 'person-1' };
render(TrainingHistory, { runs: [run], personNames: { 'person-1': 'Karl Müller' } });
await expect.element(page.getByText(/Personalisiert/i)).toBeInTheDocument();
});
it('shows person name from personNames for sender runs', async () => {
const run = { ...makeRun(0), personId: 'person-1' };
render(TrainingHistory, { runs: [run], personNames: { 'person-1': 'Karl Müller' } });
await expect.element(page.getByText(/Personalisiert/i)).toBeInTheDocument();
await expect.element(page.getByText('Karl Müller')).toBeInTheDocument();
});
});