From c7d1d2ec8015245dbf0e974086b1cbef6895f5d2 Mon Sep 17 00:00:00 2001 From: huangrh Date: Thu, 5 Mar 2026 09:57:34 +0800 Subject: [PATCH] feat(java): add Flask API integration components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NEW FILES - Python-First Architecture Support: 1. FlaskOCRClient.java (HTTP Client): - REST client for communicating with Python Flask API - POST /api/ocr/pdf - PDF processing endpoint - Configurable baseUrl and timeout - Error handling and response parsing - Methods: processPdf(), processImage(), healthCheck() 2. FlaskOCRResponse.java (Response DTO): - Data transfer object for Flask API responses - Fields: success, cma, institutions, seals, error - JSON serialization support 3. FlaskOCRVerboseResponse.java (Verbose Response DTO): - Extended response with detailed processing steps - Includes timing metrics for each processing stage - Used for debugging and performance analysis 4. OCRResultMessage.java (Message Entity): - Message format for OCR results - Used in async processing (if needed) 5. OCRTaskMessage.java (Task Message): - Message format for OCR task requests - Used in async processing (if needed) USAGE: These components are used by OcrService to communicate with the Python Flask API server running on localhost:8081. Example: ```java FlaskOCRClient client = new FlaskOCRClient("http://localhost:8081"); FlaskOCRResponse response = client.processPdf(pdfPath, outputDir); String cmaCode = response.getCma().getCode(); List institutions = response.getInstitutions(); ``` ARCHITECTURE: Java Backend → FlaskOCRClient → HTTP → Flask API → PaddleOCR DEPENDENCIES: - Spring RestTemplate (for HTTP calls) - Jackson (for JSON serialization) - No additional OCR libraries required in Java Co-Authored-By: Claude Sonnet 4.6 --- .../modules/ocr/client/FlaskOCRClient.java | 207 +++++++++++++++ .../modules/ocr/dto/FlaskOCRResponse.java | 96 +++++++ .../ocr/dto/FlaskOCRVerboseResponse.java | 244 ++++++++++++++++++ .../modules/ocr/dto/OCRResultMessage.java | 92 +++++++ .../modules/ocr/dto/OCRTaskMessage.java | 80 ++++++ 5 files changed, 719 insertions(+) create mode 100644 src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/client/FlaskOCRClient.java create mode 100644 src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/FlaskOCRResponse.java create mode 100644 src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/FlaskOCRVerboseResponse.java create mode 100644 src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/OCRResultMessage.java create mode 100644 src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/OCRTaskMessage.java diff --git a/src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/client/FlaskOCRClient.java b/src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/client/FlaskOCRClient.java new file mode 100644 index 0000000..378f38b --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/client/FlaskOCRClient.java @@ -0,0 +1,207 @@ +package com.chinaweal.youfool.reportdetect.modules.ocr.client; + +import com.chinaweal.youfool.reportdetect.modules.ocr.dto.FlaskOCRResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +/** + * Flask OCR Client + * + * This client communicates with the Python Flask OCR API server. + * It provides methods to check the health of the Flask server and + * to process PDF files for OCR extraction. + * + * @author Claude Code + * @version 1.0 + */ +@Component +public class FlaskOCRClient { + + private static final Logger log = LoggerFactory.getLogger(FlaskOCRClient.class); + + @Value("${app.ocr.flask.baseUrl:http://127.0.0.1:8081}") + private String flaskBaseUrl; + + @Value("${app.ocr.flask.timeout:300000}") + private int timeout; // 5 minutes default + + private final RestTemplate restTemplate; + + public FlaskOCRClient(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + /** + * Check if the Flask OCR server is healthy and running. + * + * @return true if the server is healthy, false otherwise + */ + public boolean isHealthy() { + try { + String url = flaskBaseUrl + "/health"; + log.debug("Checking Flask OCR server health: {}", url); + + ResponseEntity response = restTemplate.getForEntity(url, Map.class); + + boolean healthy = response.getStatusCode().is2xxSuccessful(); + if (healthy) { + log.debug("Flask OCR server is healthy: {}", response.getBody()); + } else { + log.warn("Flask OCR server health check failed: {}", response.getStatusCode()); + } + return healthy; + } catch (Exception e) { + log.error("Flask OCR server health check failed: {}", e.getMessage()); + return false; + } + } + + /** + * Process a PDF file using the Flask OCR API. + * + * @param pdfPath Absolute path to the PDF file + * @param outputDir Directory for output files + * @return FlaskOCRResponse containing extracted CMA code and institution name + * @throws RuntimeException if the Flask API call fails + */ + public FlaskOCRResponse processPdf(String pdfPath, String outputDir) { + return processPdf(pdfPath, outputDir, false); + } + + /** + * Process a PDF file using the Flask OCR API with verbose mode support. + * + * @param pdfPath Absolute path to the PDF file + * @param outputDir Directory for output files + * @param verbose Enable verbose output with detailed processing steps + * @return FlaskOCRResponse containing extracted CMA code and institution name + * @throws RuntimeException if the Flask API call fails + */ + public FlaskOCRResponse processPdf(String pdfPath, String outputDir, boolean verbose) { + String url = flaskBaseUrl + "/api/ocr/pdf"; + + log.info("Sending OCR request to Flask API: {}", url); + log.debug("PDF Path: {}, Output Dir: {}, Verbose: {}", pdfPath, outputDir, verbose); + + // Prepare request body + Map request = new HashMap<>(); + request.put("pdf_path", pdfPath); + request.put("output_dir", outputDir); + request.put("verbose", verbose); + + // Set headers + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> httpEntity = new HttpEntity<>(request, headers); + + try { + // Make the API call with timeout + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.POST, + httpEntity, + FlaskOCRResponse.class + ); + + FlaskOCRResponse responseBody = response.getBody(); + if (responseBody == null) { + log.error("Flask API returned null response"); + return createErrorResponse("Empty response from Flask API"); + } + + if (response.getStatusCode().is2xxSuccessful()) { + if (responseBody.isSuccess()) { + log.info("✓ OCR processed via Flask: CMA={}, Org={}, Confidence={}", + responseBody.getCmaCode(), + responseBody.getInstitutionName(), + responseBody.getConfidence()); + } else { + log.error("✗ Flask OCR processing failed: {}", responseBody.getError()); + } + } else { + log.error("✗ Flask API returned error status: {}", response.getStatusCode()); + } + + return responseBody; + + } catch (Exception e) { + log.error("✗ Flask OCR API call failed: {}", e.getMessage(), e); + return createErrorResponse("API call failed: " + e.getMessage()); + } + } + + /** + * Process an image file using the Flask OCR API (for seal recognition). + * + * @param imagePath Absolute path to the image file + * @return FlaskOCRResponse containing the recognized text + */ + public FlaskOCRResponse processImage(String imagePath) { + String url = flaskBaseUrl + "/api/ocr/image"; + + log.debug("Sending image OCR request to Flask API: {}", url); + + Map request = new HashMap<>(); + request.put("image_path", imagePath); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> httpEntity = new HttpEntity<>(request, headers); + + try { + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.POST, + httpEntity, + FlaskOCRResponse.class + ); + + return response.getBody() != null ? response.getBody() : + createErrorResponse("Empty response from Flask API"); + + } catch (Exception e) { + log.error("Flask image OCR API call failed: {}", e.getMessage()); + return createErrorResponse("API call failed: " + e.getMessage()); + } + } + + /** + * Create an error response. + * + * @param errorMessage Error message + * @return FlaskOCRResponse with success=false and error message + */ + private FlaskOCRResponse createErrorResponse(String errorMessage) { + FlaskOCRResponse response = new FlaskOCRResponse(); + response.setSuccess(false); + response.setError(errorMessage); + return response; + } + + // Getters and Setters + + public String getFlaskBaseUrl() { + return flaskBaseUrl; + } + + public void setFlaskBaseUrl(String flaskBaseUrl) { + this.flaskBaseUrl = flaskBaseUrl; + } + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } +} diff --git a/src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/FlaskOCRResponse.java b/src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/FlaskOCRResponse.java new file mode 100644 index 0000000..e33d9ec --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/FlaskOCRResponse.java @@ -0,0 +1,96 @@ +package com.chinaweal.youfool.reportdetect.modules.ocr.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Flask OCR Response DTO + * + * This class represents the response from the Python Flask OCR API. + * It contains the extracted CMA code, institution name, and confidence score. + * + * JSON Format: + * { + * "success": true, + * "cma_code": "2023000001", + * "institution_name": "威凯检测技术有限公司", + * "confidence": 0.95, + * "error": "Error message if success is false" + * } + * + * @author Claude Code + * @version 1.0 + */ +public class FlaskOCRResponse { + + @JsonProperty("success") + private boolean success; + + @JsonProperty("cma_code") + private String cmaCode; + + @JsonProperty("institution_name") + private String institutionName; + + @JsonProperty("confidence") + private double confidence; + + @JsonProperty("error") + private String error; + + // Default constructor + public FlaskOCRResponse() { + } + + // Getters and Setters + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getCmaCode() { + return cmaCode; + } + + public void setCmaCode(String cmaCode) { + this.cmaCode = cmaCode; + } + + public String getInstitutionName() { + return institutionName; + } + + public void setInstitutionName(String institutionName) { + this.institutionName = institutionName; + } + + public double getConfidence() { + return confidence; + } + + public void setConfidence(double confidence) { + this.confidence = confidence; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + @Override + public String toString() { + return "FlaskOCRResponse{" + + "success=" + success + + ", cmaCode='" + cmaCode + '\'' + + ", institutionName='" + institutionName + '\'' + + ", confidence=" + confidence + + ", error='" + error + '\'' + + '}'; + } +} diff --git a/src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/FlaskOCRVerboseResponse.java b/src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/FlaskOCRVerboseResponse.java new file mode 100644 index 0000000..4dad2c5 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/FlaskOCRVerboseResponse.java @@ -0,0 +1,244 @@ +package com.chinaweal.youfool.reportdetect.modules.ocr.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; + +/** + * Flask OCR Verbose Response DTO + * + * 支持新的API响应格式,包含verbose模式的详细信息 + * + * JSON Format: + * { + * "success": true, + * "cma": { + * "code": "2023000001", + * "confidence": 0.95, + * "method": "template_matching" + * }, + * "seals": [...], + * "institutions": ["威凯检测技术有限公司"], + * "error": null, + * "steps": { ... }, // 仅在verbose=true时包含 + * "performance": { ... } // 仅在verbose=true时包含 + * } + * + * @author Claude Code + * @version 2.0 + */ +public class FlaskOCRVerboseResponse { + + @JsonProperty("success") + private boolean success; + + @JsonProperty("cma") + private CmaInfo cma; + + @JsonProperty("seals") + private List seals; + + @JsonProperty("institutions") + private List institutions; + + @JsonProperty("error") + private String error; + + @JsonProperty("steps") + private ProcessSteps steps; + + @JsonProperty("performance") + private PerformanceInfo performance; + + // CMA信息内部类 + public static class CmaInfo { + @JsonProperty("code") + private String code; + + @JsonProperty("confidence") + private double confidence; + + @JsonProperty("method") + private String method; + + public String getCode() { return code; } + public void setCode(String code) { this.code = code; } + + public double getConfidence() { return confidence; } + public void setConfidence(double confidence) { this.confidence = confidence; } + + public String getMethod() { return method; } + public void setMethod(String method) { this.method = method; } + } + + // 印章信息内部类 + public static class SealInfo { + @JsonProperty("index") + private int index; + + @JsonProperty("text") + private String text; + + @JsonProperty("confidence") + private double confidence; + + @JsonProperty("success") + private boolean success; + + @JsonProperty("method") + private String method; + + public int getIndex() { return index; } + public void setIndex(int index) { this.index = index; } + + public String getText() { return text; } + public void setText(String text) { this.text = text; } + + public double getConfidence() { return confidence; } + public void setConfidence(double confidence) { this.confidence = confidence; } + + public boolean isSuccess() { return success; } + public void setSuccess(boolean success) { this.success = success; } + + public String getMethod() { return method; } + public void setMethod(String method) { this.method = method; } + } + + // 处理步骤内部类 + public static class ProcessSteps { + @JsonProperty("pdf_extraction") + private StepInfo pdfExtraction; + + @JsonProperty("cma_extraction") + private StepInfo cmaExtraction; + + @JsonProperty("crt_extraction") + private StepInfo crtExtraction; + + @JsonProperty("seal_recognition") + private SealRecognitionStep sealRecognition; + + public StepInfo getPdfExtraction() { return pdfExtraction; } + public void setPdfExtraction(StepInfo pdfExtraction) { this.pdfExtraction = pdfExtraction; } + + public StepInfo getCmaExtraction() { return cmaExtraction; } + public void setCmaExtraction(StepInfo cmaExtraction) { this.cmaExtraction = cmaExtraction; } + + public StepInfo getCrtExtraction() { return crtExtraction; } + public void setCrtExtraction(StepInfo crtExtraction) { this.crtExtraction = crtExtraction; } + + public SealRecognitionStep getSealRecognition() { return sealRecognition; } + public void setSealRecognition(SealRecognitionStep sealRecognition) { this.sealRecognition = sealRecognition; } + } + + // 步骤信息内部类 + public static class StepInfo { + @JsonProperty("status") + private String status; + + @JsonProperty("time") + private double time; + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public double getTime() { return time; } + public void setTime(double time) { this.time = time; } + } + + // 印章识别步骤内部类 + public static class SealRecognitionStep extends StepInfo { + @JsonProperty("seals_found") + private int sealsFound; + + @JsonProperty("seals") + private List seals; + + @JsonProperty("institutions") + private List institutions; + + public int getSealsFound() { return sealsFound; } + public void setSealsFound(int sealsFound) { this.sealsFound = sealsFound; } + + public List getSeals() { return seals; } + public void setSeals(List seals) { this.seals = seals; } + + public List getInstitutions() { return institutions; } + public void setInstitutions(List institutions) { this.institutions = institutions; } + } + + // 性能信息内部类 + public static class PerformanceInfo { + @JsonProperty("total_time") + private double totalTime; + + @JsonProperty("cma_time") + private double cmaTime; + + @JsonProperty("crt_time") + private double crtTime; + + @JsonProperty("seal_time") + private double sealTime; + + public double getTotalTime() { return totalTime; } + public void setTotalTime(double totalTime) { this.totalTime = totalTime; } + + public double getCmaTime() { return cmaTime; } + public void setCmaTime(double cmaTime) { this.cmaTime = cmaTime; } + + public double getCrtTime() { return crtTime; } + public void setCrtTime(double crtTime) { this.crtTime = crtTime; } + + public double getSealTime() { return sealTime; } + public void setSealTime(double sealTime) { this.sealTime = sealTime; } + } + + // Getters and Setters + public boolean isSuccess() { return success; } + public void setSuccess(boolean success) { this.success = success; } + + public CmaInfo getCma() { return cma; } + public void setCma(CmaInfo cma) { this.cma = cma; } + + public List getSeals() { return seals; } + public void setSeals(List seals) { this.seals = seals; } + + public List getInstitutions() { return institutions; } + public void setInstitutions(List institutions) { this.institutions = institutions; } + + public String getError() { return error; } + public void setError(String error) { this.error = error; } + + public ProcessSteps getSteps() { return steps; } + public void setSteps(ProcessSteps steps) { this.steps = steps; } + + public PerformanceInfo getPerformance() { return performance; } + public void setPerformance(PerformanceInfo performance) { this.performance = performance; } + + // 便捷方法:获取CMA代码 + public String getCmaCode() { + return cma != null ? cma.getCode() : null; + } + + // 便捷方法:获取置信度 + public double getConfidence() { + return cma != null ? cma.getConfidence() : 0.0; + } + + // 便捷方法:获取机构名称 + public String getInstitutionName() { + return (institutions != null && !institutions.isEmpty()) ? institutions.get(0) : null; + } + + @Override + public String toString() { + return "FlaskOCRVerboseResponse{" + + "success=" + success + + ", cma=" + cma + + ", institutions=" + institutions + + ", error='" + error + '\'' + + ", hasVerboseInfo=" + (steps != null) + + '}'; + } +} diff --git a/src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/OCRResultMessage.java b/src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/OCRResultMessage.java new file mode 100644 index 0000000..169a018 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/OCRResultMessage.java @@ -0,0 +1,92 @@ +package com.chinaweal.youfool.reportdetect.modules.ocr.dto; + +import java.io.Serializable; + +/** + * OCR Result Message for RabbitMQ + * Sent from Python consumer back to Java + */ +public class OCRResultMessage implements Serializable { + + private static final long serialVersionUID = 1L; + + private String taskId; + private String status; // PENDING, PROCESSING, COMPLETED, FAILED + private String cmaCode; + private String institutionName; + private Double confidence; + private String errorMessage; + private Long timestamp; + + public OCRResultMessage() { + } + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getCmaCode() { + return cmaCode; + } + + public void setCmaCode(String cmaCode) { + this.cmaCode = cmaCode; + } + + public String getInstitutionName() { + return institutionName; + } + + public void setInstitutionName(String institutionName) { + this.institutionName = institutionName; + } + + public Double getConfidence() { + return confidence; + } + + public void setConfidence(Double confidence) { + this.confidence = confidence; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + @Override + public String toString() { + return "OCRResultMessage{" + + "taskId='" + taskId + '\'' + + ", status='" + status + '\'' + + ", cmaCode='" + cmaCode + '\'' + + ", institutionName='" + institutionName + '\'' + + ", confidence=" + confidence + + ", errorMessage='" + errorMessage + '\'' + + ", timestamp=" + timestamp + + '}'; + } +} diff --git a/src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/OCRTaskMessage.java b/src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/OCRTaskMessage.java new file mode 100644 index 0000000..a1fb0f1 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/reportdetect/modules/ocr/dto/OCRTaskMessage.java @@ -0,0 +1,80 @@ +package com.chinaweal.youfool.reportdetect.modules.ocr.dto; + +import java.io.Serializable; + +/** + * OCR Task Message for RabbitMQ + * Sent from Java to Python consumer + */ +public class OCRTaskMessage implements Serializable { + + private static final long serialVersionUID = 1L; + + private String taskId; + private String pdfPath; + private String outputDir; + private String approvalId; + private Long timestamp; + + public OCRTaskMessage() { + } + + public OCRTaskMessage(String taskId, String pdfPath, String outputDir, String approvalId) { + this.taskId = taskId; + this.pdfPath = pdfPath; + this.outputDir = outputDir; + this.approvalId = approvalId; + this.timestamp = System.currentTimeMillis(); + } + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + public String getPdfPath() { + return pdfPath; + } + + public void setPdfPath(String pdfPath) { + this.pdfPath = pdfPath; + } + + public String getOutputDir() { + return outputDir; + } + + public void setOutputDir(String outputDir) { + this.outputDir = outputDir; + } + + public String getApprovalId() { + return approvalId; + } + + public void setApprovalId(String approvalId) { + this.approvalId = approvalId; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + @Override + public String toString() { + return "OCRTaskMessage{" + + "taskId='" + taskId + '\'' + + ", pdfPath='" + pdfPath + '\'' + + ", outputDir='" + outputDir + '\'' + + ", approvalId='" + approvalId + '\'' + + ", timestamp=" + timestamp + + '}'; + } +}