feat: OCR pipeline with NDJSON streaming and real-time progress (#226, #227, #231) #229

Merged
marcel merged 74 commits from feat/issue-226-227-ocr-pipeline-polygon into main 2026-04-13 12:39:04 +02:00
2 changed files with 69 additions and 0 deletions
Showing only changes of commit c19c41f812 - Show all commits

View File

@@ -48,6 +48,26 @@ public class AnnotationService {
return annotationRepository.save(annotation);
}
@Transactional
public DocumentAnnotation createOcrAnnotation(UUID documentId, CreateAnnotationDTO dto,
UUID userId, String fileHash,
List<List<Double>> polygon) {
DocumentAnnotation annotation = DocumentAnnotation.builder()
.documentId(documentId)
.pageNumber(dto.getPageNumber())
.x(dto.getX())
.y(dto.getY())
.width(dto.getWidth())
.height(dto.getHeight())
.color(dto.getColor())
.fileHash(fileHash)
.createdBy(userId)
.polygon(polygon)
.build();
return annotationRepository.save(annotation);
}
@Transactional
public void deleteAnnotation(UUID documentId, UUID annotationId, UUID userId) {
DocumentAnnotation annotation = annotationRepository

View File

@@ -260,6 +260,55 @@ class AnnotationServiceTest {
verify(annotationRepository).save(any());
}
// ─── createOcrAnnotation ──────────────────────────────────────────────────
@Test
void createOcrAnnotation_skipsOverlapCheck_andSavesWithPolygon() {
UUID docId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.8, 0.04, "#00C7B1");
List<List<Double>> polygon = List.of(
List.of(0.1, 0.1), List.of(0.9, 0.11),
List.of(0.89, 0.14), List.of(0.11, 0.13));
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
DocumentAnnotation result = annotationService.createOcrAnnotation(
docId, dto, userId, "filehash", polygon);
assertThat(result.getPolygon()).isEqualTo(polygon);
assertThat(result.getDocumentId()).isEqualTo(docId);
verify(annotationRepository).save(any());
verify(annotationRepository, never()).findByDocumentIdAndPageNumber(any(), any(int.class));
}
@Test
void createOcrAnnotation_savesWithNullPolygon_whenPolygonNotProvided() {
UUID docId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.8, 0.04, "#00C7B1");
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
DocumentAnnotation result = annotationService.createOcrAnnotation(
docId, dto, userId, "filehash", null);
assertThat(result.getPolygon()).isNull();
verify(annotationRepository).save(any());
}
@Test
void createOcrAnnotation_doesNotCheckOverlap_evenWhenOverlappingAnnotationExists() {
UUID docId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.3, 0.3, "#00C7B1");
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
annotationService.createOcrAnnotation(docId, dto, userId, "hash", null);
verify(annotationRepository, never()).findByDocumentIdAndPageNumber(any(), any(int.class));
}
// ─── overlaps — partial overlap cases ────────────────────────────────────
@Test
void createAnnotation_noConflict_whenAnnotationIsAbove() {
// x ranges overlap, y ranges don't — existing is ABOVE the new annotation