feat: 添加 PaddleX OCR 支持及示例更新

- 在 example_usage.py 中更新分析文档的示例,使用新的 PaddleX OCR 服务。
- 在 data_router.py 中优化文件处理反馈信息,增加处理失败文件的统计。
- 在 indexing.py 中新增对 PaddleX OCR 的支持。
- 在 _ocr.py 中添加 process_pdf_paddlex 方法,集成 PaddleX OCR 处理逻辑。
- 新增 paddlex.py 文件,封装 PaddleX 版面解析服务的 API 调用。
- 更新前端 DataBaseInfoView.vue,增加 PaddleX OCR 选项。
- 新增测试文件 PixPin_2025-06-19_23-42-17.png 以支持 OCR 测试。
This commit is contained in:
Wenjie Zhang 2025-06-23 10:09:51 +08:00
parent 97ee5b0855
commit eeb53f4774
7 changed files with 80 additions and 46 deletions

View File

@ -1,11 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PaddleX 文档分析使用示例
演示如何使用 analyze_document 函数分析文档
"""
from paddlex_layout_parser import analyze_document
from src.plugins.paddlex import analyze_document
import json
@ -14,7 +13,7 @@ def analyze_custom_file(file_path: str):
print(f"\n🔍 分析自定义文件: {file_path}")
result = analyze_document(file_path=file_path)
result = analyze_document(file_path=file_path, base_url="http://172.19.13.5:8080")
if result["success"]:
return result
@ -27,6 +26,7 @@ if __name__ == "__main__":
# main()
# 如果您想分析其他文件,可以取消注释下面的代码
custom_file = "test/struct_pdf/P020241226617572090546.pdf"
custom_file = "test/data/PixPin_2025-06-19_23-42-17.png"
print(analyze_custom_file(custom_file))
custom_file = "test/data/ocr_test/1706.03762v7_扫描版.pdf"
custom_file = "test/data/ocr_test/PixPin_2025-06-19_23-42-17.png"
print(analyze_custom_file(custom_file))

View File

@ -56,7 +56,9 @@ async def file_to_chunk(db_id: str = Body(...), files: list[str] = Body(...), pa
logger.debug(f"File to chunk for db_id {db_id}: {files} {params=}")
try:
processed_files = await knowledge_base.save_files_for_pending_indexing(db_id, files, params)
return {"message": "Files processed and pending indexing", "files": processed_files, "status": "success"}
processed_failed_count = len([_p['status'] == 'failed' for _p in processed_files])
processed_info = f"Processed {len(processed_files)} files for pending indexing, {processed_failed_count} files failed"
return {"message": processed_info, "files": processed_files, "status": "success"}
except Exception as e:
logger.error(f"Failed to process files for pending indexing: {e}, {traceback.format_exc()}")
return {"message": f"Failed to process files for pending indexing: {e}", "status": "failed"}

View File

@ -133,6 +133,10 @@ def parse_pdf(file, params=None):
from src.plugins import ocr
return ocr.process_pdf_mineru(file)
elif opt_ocr == "paddlex_ocr":
from src.plugins import ocr
return ocr.process_pdf_paddlex(file)
else:
return pdfreader(file, params=params)

View File

@ -159,11 +159,10 @@ class OCRPlugin:
:param pdf_path: PDF文件路径
:return: 提取的文本
"""
import requests
from .mineru import parse_doc
mineru_ocr_uri = os.getenv("MINERU_OCR_URI", "http://localhost:30000")
mineru_ocr_uri_health = f"{mineru_ocr_uri}/health"
import requests
import json
from .mineru import parse_doc
health_check_response = requests.get(mineru_ocr_uri_health, timeout=5)
if health_check_response.status_code != 200:
@ -180,6 +179,28 @@ class OCRPlugin:
logger.debug(f"Mineru OCR result: {pdf_text[:50]}(...) total {len(pdf_text)} characters.")
return pdf_text
def process_pdf_paddlex(self, pdf_path):
"""
使用Paddlex OCR处理PDF文件
:param pdf_path: PDF文件路径
:return: 提取的文本
"""
from .paddlex import analyze_document, check_paddlex_health
paddlex_uri = os.getenv("PADDLEX_URI", "http://localhost:8080")
health_check_response = check_paddlex_health(paddlex_uri)
if not health_check_response.ok:
logger.error(f"Paddlex OCR service health check failed with {paddlex_uri}: {health_check_response.json()}")
raise RuntimeError("Paddlex OCR service health check failed. Please check the log use `docker logs paddlex`")
result = analyze_document(pdf_path, base_url=paddlex_uri)
if not result["success"]:
logger.error(f"Paddlex OCR failed: {result['error']}")
return ""
return result["full_text"]
def get_state(task_id):
return GOLBAL_STATE.get(task_id, {})

View File

@ -4,8 +4,8 @@ import json
import base64
import os
import time
from typing import Optional, Dict, Any
from typing import Optional, Any
from src.utils import logger
@ -24,43 +24,43 @@ class PaddleXLayoutParser:
def _process_file_input(self, file_input: str) -> str:
# 检查是否为本地文件路径
if os.path.exists(file_input):
print(f"📁 检测到本地文件: {file_input}")
print(f"📏 文件大小: {os.path.getsize(file_input) / 1024 / 1024:.2f} MB")
logger.info(f"📁 检测到本地文件: {file_input}")
logger.info(f"📏 文件大小: {os.path.getsize(file_input) / 1024 / 1024:.2f} MB")
try:
# 将本地文件编码为Base64
encoded_content = self.encode_file_to_base64(file_input)
print(f"✅ 文件已编码为Base64长度: {len(encoded_content)} 字符")
logger.info(f"✅ 文件已编码为Base64长度: {len(encoded_content)} 字符")
return encoded_content
except Exception as e:
print(f"❌ 文件编码失败: {e}")
logger.error(f"❌ 文件编码失败: {e}")
raise
# 检查是否为URL
elif file_input.startswith(('http://', 'https://')):
print(f"🌐 检测到URL: {file_input}")
logger.info(f"🌐 检测到URL: {file_input}")
return file_input
# 否则假设为Base64编码内容
else:
print(f"📝 假设为Base64编码内容长度: {len(file_input)} 字符")
logger.info(f"📝 假设为Base64编码内容长度: {len(file_input)} 字符")
return file_input
def layout_parsing(self,
file_input: str,
file_type: Optional[int] = None,
use_textline_orientation: Optional[bool] = None,
use_seal_recognition: Optional[bool] = None,
use_table_recognition: Optional[bool] = None,
use_formula_recognition: Optional[bool] = None,
use_chart_recognition: Optional[bool] = None,
use_region_detection: Optional[bool] = None,
layout_threshold: Optional[float] = None,
layout_nms: Optional[bool] = None,
use_doc_orientation_classify: Optional[bool] = True,
use_doc_unwarping: Optional[bool] = False,
use_wired_table_cells_trans_to_html: Optional[bool] = True, # 是否启用无有线表单元格检测结果直转HTML默认False启用则直接基于有线表单元格检测结果的几何关系构建HTML。
**kwargs) -> Dict[str, Any]:
file_type: int | None = None,
use_textline_orientation: bool | None = None,
use_seal_recognition: bool | None = None,
use_table_recognition: bool | None = None,
use_formula_recognition: bool | None = None,
use_chart_recognition: bool | None = None,
use_region_detection: bool | None = None,
layout_threshold: float | None = None,
layout_nms: bool | None = None,
use_doc_orientation_classify: bool = True,
use_doc_unwarping: bool | None = False,
use_wired_table_cells_trans_to_html: bool = True, # 启用则直接基于有线表单元格检测结果的几何关系构建HTML。
**kwargs) -> dict[str, Any]:
"""
调用版面解析APIhttps://paddlepaddle.github.io/PaddleX/latest/pipeline_usage/tutorials/ocr_pipelines/PP-StructureV3.html#22-python
"""
@ -104,28 +104,30 @@ class PaddleXLayoutParser:
if response.status_code == 200:
result = response.json()
print("✅ 请求成功!")
logger.info("✅ 请求成功!")
return result
else:
print("❌ 请求失败!")
logger.error("❌ 请求失败!")
try:
error_result = response.json()
print(f"错误信息: {json.dumps(error_result, indent=2, ensure_ascii=False)}")
logger.error(f"错误信息: {json.dumps(error_result, indent=2, ensure_ascii=False)}")
return error_result
except:
print(f"响应内容: {response.text}")
return {"error": response.text, "status_code": response.status_code}
except Exception as e:
logger.error(f"响应内容: {response.text}")
return {"error": f"{e}: {response.text}", "status_code": response.status_code}
except requests.exceptions.RequestException as e:
print(f"❌ 网络请求异常: {e}")
health_check_response = requests.get(f"{self.base_url}/health", timeout=5)
logger.error(f"❌ 网络请求异常: {e}: {health_check_response.json()}")
return {"error": str(e)}
except Exception as e:
print(f"❌ 其他异常: {e}")
logger.error(f"❌ 其他异常: {e}")
return {"error": str(e)}
def _parse_recognition_result(api_result: Dict[str, Any], file_path: str) -> Dict[str, Any]:
def _parse_recognition_result(api_result: dict[str, Any], file_path: str) -> dict[str, Any]:
# 基本信息
parsed_result = {
"success": True,
@ -238,7 +240,7 @@ def _parse_recognition_result(api_result: Dict[str, Any], file_path: str) -> Dic
return parsed_result
def analyze_document(file_path: str) -> Dict[str, Any]:
def analyze_document(file_path: str, base_url: str = "http://localhost:8080") -> dict[str, Any]:
# 检查文件是否存在
if not os.path.exists(file_path):
@ -249,7 +251,7 @@ def analyze_document(file_path: str) -> Dict[str, Any]:
}
# 初始化客户端
client = PaddleXLayoutParser()
client = PaddleXLayoutParser(base_url=base_url)
# 判断文件类型
file_ext = os.path.splitext(file_path)[1].lower()
@ -264,9 +266,9 @@ def analyze_document(file_path: str) -> Dict[str, Any]:
"file_path": file_path
}
print(f"📄 开始分析文档: {os.path.basename(file_path)}")
print(f"📏 文件大小: {os.path.getsize(file_path) / 1024 / 1024:.2f} MB")
print(f"📋 文件类型: {'PDF' if file_type == 0 else '图片'}")
logger.info(f"📄 开始分析文档: {os.path.basename(file_path)}")
logger.info(f"📏 文件大小: {os.path.getsize(file_path) / 1024 / 1024:.2f} MB")
logger.info(f"📋 文件类型: {'PDF' if file_type == 0 else '图片'}")
try:
# 调用API进行识别
@ -291,3 +293,7 @@ def analyze_document(file_path: str) -> Dict[str, Any]:
"error": f"处理异常: {str(e)}",
"file_path": file_path
}
def check_paddlex_health(base_url: str = "http://localhost:8080") -> bool:
return requests.get(f"{base_url}/health", timeout=5)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -445,6 +445,7 @@ const enable_ocr_options = ref([
{ value: 'disable', payload: { title: '不启用' } },
{ value: 'onnx_rapid_ocr', payload: { title: 'ONNX with RapidOCR' } },
{ value: 'mineru_ocr', payload: { title: 'MinerU OCR' } },
{ value: 'paddlex_ocr', payload: { title: 'Paddlex OCR' } },
])
const use_rewrite_queryOptions = ref([
@ -811,7 +812,7 @@ const chunkFiles = () => {
.then(data => {
console.log('文件处理结果:', data)
if (data.status === 'success') {
message.success(data.message || '文件已提交处理,请稍后在列表刷新查看状态');
message.info(data.message || '文件已提交处理,请稍后在列表刷新查看状态');
fileList.value = []; //
addFilesModalVisible.value = false; //
getDatabaseInfo(); //