feat(java): add Flask API integration components

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<String> 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 <noreply@anthropic.com>
This commit is contained in:
黄仁欢 2026-03-05 09:57:34 +08:00
parent ae9ed3128f
commit c7d1d2ec80
5 changed files with 719 additions and 0 deletions

View File

@ -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<Map> 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<String, Object> 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<Map<String, Object>> httpEntity = new HttpEntity<>(request, headers);
try {
// Make the API call with timeout
ResponseEntity<FlaskOCRResponse> 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<String, String> request = new HashMap<>();
request.put("image_path", imagePath);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, String>> httpEntity = new HttpEntity<>(request, headers);
try {
ResponseEntity<FlaskOCRResponse> 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;
}
}

View File

@ -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 + '\'' +
'}';
}
}

View File

@ -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<SealInfo> seals;
@JsonProperty("institutions")
private List<String> 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<SealInfo> seals;
@JsonProperty("institutions")
private List<String> institutions;
public int getSealsFound() { return sealsFound; }
public void setSealsFound(int sealsFound) { this.sealsFound = sealsFound; }
public List<SealInfo> getSeals() { return seals; }
public void setSeals(List<SealInfo> seals) { this.seals = seals; }
public List<String> getInstitutions() { return institutions; }
public void setInstitutions(List<String> 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<SealInfo> getSeals() { return seals; }
public void setSeals(List<SealInfo> seals) { this.seals = seals; }
public List<String> getInstitutions() { return institutions; }
public void setInstitutions(List<String> 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) +
'}';
}
}

View File

@ -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 +
'}';
}
}

View File

@ -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 +
'}';
}
}