refactor(ocr): move person-name enrichment from OcrController into OcrTrainingService

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-17 20:18:21 +02:00
committed by marcel
parent 4f86011ffb
commit c3939e0f13
4 changed files with 47 additions and 23 deletions

View File

@@ -14,7 +14,6 @@ import org.raddatz.familienarchiv.service.OcrBatchService;
import org.raddatz.familienarchiv.service.OcrProgressService; import org.raddatz.familienarchiv.service.OcrProgressService;
import org.raddatz.familienarchiv.service.OcrService; import org.raddatz.familienarchiv.service.OcrService;
import org.raddatz.familienarchiv.service.OcrTrainingService; import org.raddatz.familienarchiv.service.OcrTrainingService;
import org.raddatz.familienarchiv.service.PersonService;
import org.raddatz.familienarchiv.service.SegmentationTrainingExportService; import org.raddatz.familienarchiv.service.SegmentationTrainingExportService;
import org.raddatz.familienarchiv.service.TrainingDataExportService; import org.raddatz.familienarchiv.service.TrainingDataExportService;
import org.raddatz.familienarchiv.service.UserService; import org.raddatz.familienarchiv.service.UserService;
@@ -41,7 +40,6 @@ public class OcrController {
private final OcrBatchService ocrBatchService; private final OcrBatchService ocrBatchService;
private final OcrProgressService ocrProgressService; private final OcrProgressService ocrProgressService;
private final UserService userService; private final UserService userService;
private final PersonService personService;
private final TrainingDataExportService trainingDataExportService; private final TrainingDataExportService trainingDataExportService;
private final SegmentationTrainingExportService segmentationTrainingExportService; private final SegmentationTrainingExportService segmentationTrainingExportService;
private final OcrTrainingService ocrTrainingService; private final OcrTrainingService ocrTrainingService;
@@ -136,18 +134,6 @@ public class OcrController {
public Map<String, Object> getTrainingInfo() { public Map<String, Object> getTrainingInfo() {
OcrTrainingService.TrainingInfoResponse info = ocrTrainingService.getTrainingInfo(); OcrTrainingService.TrainingInfoResponse info = ocrTrainingService.getTrainingInfo();
Map<String, String> personNames = new HashMap<>();
for (OcrTrainingRun run : info.runs()) {
if (run.getPersonId() != null && !personNames.containsKey(run.getPersonId().toString())) {
try {
personNames.put(run.getPersonId().toString(),
personService.getById(run.getPersonId()).getDisplayName());
} catch (Exception e) {
log.debug("Could not resolve display name for person {}: {}", run.getPersonId(), e.getMessage());
}
}
}
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("availableBlocks", info.availableBlocks()); result.put("availableBlocks", info.availableBlocks());
result.put("totalOcrBlocks", info.totalOcrBlocks()); result.put("totalOcrBlocks", info.totalOcrBlocks());
@@ -156,7 +142,7 @@ public class OcrController {
result.put("ocrServiceAvailable", info.ocrServiceAvailable()); result.put("ocrServiceAvailable", info.ocrServiceAvailable());
result.put("lastRun", info.lastRun() != null ? info.lastRun() : Map.of()); result.put("lastRun", info.lastRun() != null ? info.lastRun() : Map.of());
result.put("runs", info.runs()); result.put("runs", info.runs());
result.put("personNames", personNames); result.put("personNames", info.personNames());
return result; return result;
} }

View File

@@ -17,7 +17,9 @@ import org.springframework.transaction.support.TransactionTemplate;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@@ -34,6 +36,7 @@ public class OcrTrainingService {
private final OcrHealthClient ocrHealthClient; private final OcrHealthClient ocrHealthClient;
private final TranscriptionBlockRepository blockRepository; private final TranscriptionBlockRepository blockRepository;
private final TransactionTemplate txTemplate; private final TransactionTemplate txTemplate;
private final PersonService personService;
public record TrainingInfoResponse( public record TrainingInfoResponse(
int availableBlocks, int availableBlocks,
@@ -42,7 +45,8 @@ public class OcrTrainingService {
int availableSegBlocks, int availableSegBlocks,
boolean ocrServiceAvailable, boolean ocrServiceAvailable,
OcrTrainingRun lastRun, OcrTrainingRun lastRun,
List<OcrTrainingRun> runs List<OcrTrainingRun> runs,
Map<String, String> personNames
) {} ) {}
private void assertNoRunningTraining() { private void assertNoRunningTraining() {
@@ -198,6 +202,18 @@ public class OcrTrainingService {
List<OcrTrainingRun> recentRuns = trainingRunRepository.findTop20ByOrderByCreatedAtDesc(); List<OcrTrainingRun> recentRuns = trainingRunRepository.findTop20ByOrderByCreatedAtDesc();
OcrTrainingRun lastRun = recentRuns.isEmpty() ? null : recentRuns.get(0); OcrTrainingRun lastRun = recentRuns.isEmpty() ? null : recentRuns.get(0);
Map<String, String> personNames = new HashMap<>();
for (OcrTrainingRun run : recentRuns) {
if (run.getPersonId() != null && !personNames.containsKey(run.getPersonId().toString())) {
try {
personNames.put(run.getPersonId().toString(),
personService.getById(run.getPersonId()).getDisplayName());
} catch (Exception e) {
log.debug("Could not resolve display name for person {}: {}", run.getPersonId(), e.getMessage());
}
}
}
return new TrainingInfoResponse( return new TrainingInfoResponse(
eligibleBlocks.size(), eligibleBlocks.size(),
totalOcrBlocks, totalOcrBlocks,
@@ -205,7 +221,8 @@ public class OcrTrainingService {
availableSegBlocks, availableSegBlocks,
ocrHealthClient.isHealthy(), ocrHealthClient.isHealthy(),
lastRun, lastRun,
recentRuns recentRuns,
personNames
); );
} }

View File

@@ -23,6 +23,8 @@ import org.springframework.test.web.servlet.MockMvc;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
@@ -43,7 +45,6 @@ class OcrControllerTest {
@MockitoBean OcrBatchService ocrBatchService; @MockitoBean OcrBatchService ocrBatchService;
@MockitoBean OcrProgressService ocrProgressService; @MockitoBean OcrProgressService ocrProgressService;
@MockitoBean UserService userService; @MockitoBean UserService userService;
@MockitoBean PersonService personService;
@MockitoBean CustomUserDetailsService customUserDetailsService; @MockitoBean CustomUserDetailsService customUserDetailsService;
@MockitoBean TrainingDataExportService trainingDataExportService; @MockitoBean TrainingDataExportService trainingDataExportService;
@MockitoBean SegmentationTrainingExportService segmentationTrainingExportService; @MockitoBean SegmentationTrainingExportService segmentationTrainingExportService;
@@ -220,7 +221,7 @@ class OcrControllerTest {
@WithMockUser(authorities = "ADMIN") @WithMockUser(authorities = "ADMIN")
void getTrainingInfo_returns200_withInfo() throws Exception { void getTrainingInfo_returns200_withInfo() throws Exception {
OcrTrainingService.TrainingInfoResponse info = OcrTrainingService.TrainingInfoResponse info =
new OcrTrainingService.TrainingInfoResponse(5, 20, 2, 3, true, null, List.of()); new OcrTrainingService.TrainingInfoResponse(5, 20, 2, 3, true, null, List.of(), Map.of());
when(ocrTrainingService.getTrainingInfo()).thenReturn(info); when(ocrTrainingService.getTrainingInfo()).thenReturn(info);
mockMvc.perform(get("/api/ocr/training-info")) mockMvc.perform(get("/api/ocr/training-info"))
@@ -231,21 +232,38 @@ class OcrControllerTest {
@Test @Test
@WithMockUser(authorities = "ADMIN") @WithMockUser(authorities = "ADMIN")
void getTrainingInfo_returns200_and_omits_personName_when_resolution_throws() throws Exception { void getTrainingInfo_returns200_and_omits_personName_when_service_provides_empty_map() throws Exception {
UUID personId = UUID.randomUUID(); UUID personId = UUID.randomUUID();
OcrTrainingRun runWithPerson = OcrTrainingRun.builder() OcrTrainingRun runWithPerson = OcrTrainingRun.builder()
.id(UUID.randomUUID()).status(TrainingStatus.DONE) .id(UUID.randomUUID()).status(TrainingStatus.DONE)
.personId(personId).blockCount(5).documentCount(1).modelName("sender_x").build(); .personId(personId).blockCount(5).documentCount(1).modelName("sender_x").build();
OcrTrainingService.TrainingInfoResponse info = OcrTrainingService.TrainingInfoResponse info =
new OcrTrainingService.TrainingInfoResponse(5, 20, 2, 3, true, null, List.of(runWithPerson)); new OcrTrainingService.TrainingInfoResponse(5, 20, 2, 3, true, null,
List.of(runWithPerson), Map.of());
when(ocrTrainingService.getTrainingInfo()).thenReturn(info); when(ocrTrainingService.getTrainingInfo()).thenReturn(info);
when(personService.getById(personId)).thenThrow(new RuntimeException("DB error"));
mockMvc.perform(get("/api/ocr/training-info")) mockMvc.perform(get("/api/ocr/training-info"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.personNames").isEmpty()); .andExpect(jsonPath("$.personNames").isEmpty());
} }
@Test
@WithMockUser(authorities = "ADMIN")
void getTrainingInfo_includesPersonName_whenPersonIdResolves() throws Exception {
UUID personId = UUID.randomUUID();
OcrTrainingRun runWithPerson = OcrTrainingRun.builder()
.id(UUID.randomUUID()).status(TrainingStatus.DONE)
.personId(personId).blockCount(5).documentCount(1).modelName("sender_x").build();
OcrTrainingService.TrainingInfoResponse info =
new OcrTrainingService.TrainingInfoResponse(5, 20, 2, 3, true, null,
List.of(runWithPerson), Map.of(personId.toString(), "Max Mustermann"));
when(ocrTrainingService.getTrainingInfo()).thenReturn(info);
mockMvc.perform(get("/api/ocr/training-info"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.personNames." + personId).value("Max Mustermann"));
}
@Test @Test
@WithMockUser(authorities = "READ_ALL") @WithMockUser(authorities = "READ_ALL")
void getDocumentOcrStatus_returnsNone_whenNoOcrJobExists() throws Exception { void getDocumentOcrStatus_returnsNone_whenNoOcrJobExists() throws Exception {

View File

@@ -8,6 +8,7 @@ import org.raddatz.familienarchiv.model.TrainingStatus;
import org.raddatz.familienarchiv.model.TranscriptionBlock; import org.raddatz.familienarchiv.model.TranscriptionBlock;
import org.raddatz.familienarchiv.repository.OcrTrainingRunRepository; import org.raddatz.familienarchiv.repository.OcrTrainingRunRepository;
import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository; import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository;
import org.raddatz.familienarchiv.service.PersonService;
import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
@@ -31,6 +32,7 @@ class OcrTrainingServiceTest {
OcrHealthClient healthClient; OcrHealthClient healthClient;
TranscriptionBlockRepository blockRepository; TranscriptionBlockRepository blockRepository;
TransactionTemplate txTemplate; TransactionTemplate txTemplate;
PersonService personService;
OcrTrainingService service; OcrTrainingService service;
@BeforeEach @BeforeEach
@@ -42,6 +44,7 @@ class OcrTrainingServiceTest {
healthClient = mock(OcrHealthClient.class); healthClient = mock(OcrHealthClient.class);
blockRepository = mock(TranscriptionBlockRepository.class); blockRepository = mock(TranscriptionBlockRepository.class);
txTemplate = mock(TransactionTemplate.class); txTemplate = mock(TransactionTemplate.class);
personService = mock(PersonService.class);
// Execute transaction callbacks inline so unit tests run without a real DataSource // Execute transaction callbacks inline so unit tests run without a real DataSource
when(txTemplate.execute(any())).thenAnswer(inv -> { when(txTemplate.execute(any())).thenAnswer(inv -> {
@@ -49,7 +52,7 @@ class OcrTrainingServiceTest {
return callback.doInTransaction(null); return callback.doInTransaction(null);
}); });
service = new OcrTrainingService(runRepository, exportService, segExportService, ocrClient, healthClient, blockRepository, txTemplate); service = new OcrTrainingService(runRepository, exportService, segExportService, ocrClient, healthClient, blockRepository, txTemplate, personService);
when(blockRepository.count()).thenReturn(0L); when(blockRepository.count()).thenReturn(0L);
when(runRepository.findTop20ByOrderByCreatedAtDesc()).thenReturn(List.of()); when(runRepository.findTop20ByOrderByCreatedAtDesc()).thenReturn(List.of());