feat(ocr): add Python OCR microservice, RestClientOcrClient, Docker Compose
Python microservice (ocr-service/): - FastAPI app with /ocr and /health endpoints - Surya engine: transformer-based OCR for typewritten/modern handwriting - Kraken engine: historical HTR for Kurrent/Suetterlin with pure-Python polygon-to-quad approximation (gift wrapping + rotating calipers) - Eager model loading at startup via lifespan context manager - PDF download via httpx, page rendering via pypdfium2 at 300 DPI Java RestClientOcrClient: - Implements OcrClient + OcrHealthClient interfaces - Calls Python service via Spring RestClient - Health check with graceful fallback Docker Compose: - New ocr-service container (mem_limit 6g, no host ports) - Health check with start_period 60s for model loading - ocr_models volume for Kraken model files - Backend depends on ocr-service health Refs #226, #227 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
package org.raddatz.familienarchiv.service;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.raddatz.familienarchiv.model.ScriptType;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class RestClientOcrClient implements OcrClient, OcrHealthClient {
|
||||
|
||||
private final RestClient restClient;
|
||||
|
||||
public RestClientOcrClient(@Value("${app.ocr.base-url:http://ocr-service:8000}") String baseUrl) {
|
||||
this.restClient = RestClient.builder().baseUrl(baseUrl).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OcrBlockResult> extractBlocks(String pdfUrl, ScriptType scriptType) {
|
||||
Map<String, String> body = Map.of(
|
||||
"pdfUrl", pdfUrl,
|
||||
"scriptType", scriptType.name(),
|
||||
"language", "de");
|
||||
|
||||
List<OcrBlockJson> response = restClient.post()
|
||||
.uri("/ocr")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(body)
|
||||
.retrieve()
|
||||
.body(new ParameterizedTypeReference<>() {});
|
||||
|
||||
if (response == null) return List.of();
|
||||
|
||||
return response.stream()
|
||||
.map(OcrBlockJson::toResult)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHealthy() {
|
||||
try {
|
||||
restClient.get()
|
||||
.uri("/health")
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.warn("OCR service health check failed: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
record OcrBlockJson(
|
||||
@JsonProperty("pageNumber") int pageNumber,
|
||||
double x,
|
||||
double y,
|
||||
double width,
|
||||
double height,
|
||||
List<List<Double>> polygon,
|
||||
String text
|
||||
) {
|
||||
OcrBlockResult toResult() {
|
||||
return new OcrBlockResult(pageNumber, x, y, width, height, polygon, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user