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 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" """
PaddleX 文档分析使用示例 PaddleX 文档分析使用示例
演示如何使用 analyze_document 函数分析文档 演示如何使用 analyze_document 函数分析文档
""" """
from paddlex_layout_parser import analyze_document from src.plugins.paddlex import analyze_document
import json import json
@ -14,7 +13,7 @@ def analyze_custom_file(file_path: str):
print(f"\n🔍 分析自定义文件: {file_path}") 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"]: if result["success"]:
return result return result
@ -27,6 +26,7 @@ if __name__ == "__main__":
# main() # main()
# 如果您想分析其他文件,可以取消注释下面的代码 # 如果您想分析其他文件,可以取消注释下面的代码
custom_file = "test/struct_pdf/P020241226617572090546.pdf" custom_file = "test/data/ocr_test/1706.03762v7_扫描版.pdf"
custom_file = "test/data/PixPin_2025-06-19_23-42-17.png" custom_file = "test/data/ocr_test/PixPin_2025-06-19_23-42-17.png"
print(analyze_custom_file(custom_file))
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=}") logger.debug(f"File to chunk for db_id {db_id}: {files} {params=}")
try: try:
processed_files = await knowledge_base.save_files_for_pending_indexing(db_id, files, params) 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: except Exception as e:
logger.error(f"Failed to process files for pending indexing: {e}, {traceback.format_exc()}") 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"} 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 from src.plugins import ocr
return ocr.process_pdf_mineru(file) return ocr.process_pdf_mineru(file)
elif opt_ocr == "paddlex_ocr":
from src.plugins import ocr
return ocr.process_pdf_paddlex(file)
else: else:
return pdfreader(file, params=params) return pdfreader(file, params=params)

View File

@ -159,11 +159,10 @@ class OCRPlugin:
:param pdf_path: PDF文件路径 :param pdf_path: PDF文件路径
:return: 提取的文本 :return: 提取的文本
""" """
import requests
from .mineru import parse_doc
mineru_ocr_uri = os.getenv("MINERU_OCR_URI", "http://localhost:30000") mineru_ocr_uri = os.getenv("MINERU_OCR_URI", "http://localhost:30000")
mineru_ocr_uri_health = f"{mineru_ocr_uri}/health" 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) health_check_response = requests.get(mineru_ocr_uri_health, timeout=5)
if health_check_response.status_code != 200: 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.") logger.debug(f"Mineru OCR result: {pdf_text[:50]}(...) total {len(pdf_text)} characters.")
return pdf_text 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): def get_state(task_id):
return GOLBAL_STATE.get(task_id, {}) return GOLBAL_STATE.get(task_id, {})

View File

@ -4,8 +4,8 @@ import json
import base64 import base64
import os import os
import time 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: def _process_file_input(self, file_input: str) -> str:
# 检查是否为本地文件路径 # 检查是否为本地文件路径
if os.path.exists(file_input): if os.path.exists(file_input):
print(f"📁 检测到本地文件: {file_input}") logger.info(f"📁 检测到本地文件: {file_input}")
print(f"📏 文件大小: {os.path.getsize(file_input) / 1024 / 1024:.2f} MB") logger.info(f"📏 文件大小: {os.path.getsize(file_input) / 1024 / 1024:.2f} MB")
try: try:
# 将本地文件编码为Base64 # 将本地文件编码为Base64
encoded_content = self.encode_file_to_base64(file_input) 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 return encoded_content
except Exception as e: except Exception as e:
print(f"❌ 文件编码失败: {e}") logger.error(f"❌ 文件编码失败: {e}")
raise raise
# 检查是否为URL # 检查是否为URL
elif file_input.startswith(('http://', 'https://')): elif file_input.startswith(('http://', 'https://')):
print(f"🌐 检测到URL: {file_input}") logger.info(f"🌐 检测到URL: {file_input}")
return file_input return file_input
# 否则假设为Base64编码内容 # 否则假设为Base64编码内容
else: else:
print(f"📝 假设为Base64编码内容长度: {len(file_input)} 字符") logger.info(f"📝 假设为Base64编码内容长度: {len(file_input)} 字符")
return file_input return file_input
def layout_parsing(self, def layout_parsing(self,
file_input: str, file_input: str,
file_type: Optional[int] = None, file_type: int | None = None,
use_textline_orientation: Optional[bool] = None, use_textline_orientation: bool | None = None,
use_seal_recognition: Optional[bool] = None, use_seal_recognition: bool | None = None,
use_table_recognition: Optional[bool] = None, use_table_recognition: bool | None = None,
use_formula_recognition: Optional[bool] = None, use_formula_recognition: bool | None = None,
use_chart_recognition: Optional[bool] = None, use_chart_recognition: bool | None = None,
use_region_detection: Optional[bool] = None, use_region_detection: bool | None = None,
layout_threshold: Optional[float] = None, layout_threshold: float | None = None,
layout_nms: Optional[bool] = None, layout_nms: bool | None = None,
use_doc_orientation_classify: Optional[bool] = True, use_doc_orientation_classify: bool = True,
use_doc_unwarping: Optional[bool] = False, use_doc_unwarping: bool | None = False,
use_wired_table_cells_trans_to_html: Optional[bool] = True, # 是否启用无有线表单元格检测结果直转HTML默认False启用则直接基于有线表单元格检测结果的几何关系构建HTML。 use_wired_table_cells_trans_to_html: bool = True, # 启用则直接基于有线表单元格检测结果的几何关系构建HTML。
**kwargs) -> Dict[str, Any]: **kwargs) -> dict[str, Any]:
""" """
调用版面解析APIhttps://paddlepaddle.github.io/PaddleX/latest/pipeline_usage/tutorials/ocr_pipelines/PP-StructureV3.html#22-python 调用版面解析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: if response.status_code == 200:
result = response.json() result = response.json()
print("✅ 请求成功!") logger.info("✅ 请求成功!")
return result return result
else: else:
print("❌ 请求失败!") logger.error("❌ 请求失败!")
try: try:
error_result = response.json() 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 return error_result
except: except Exception as e:
print(f"响应内容: {response.text}") logger.error(f"响应内容: {response.text}")
return {"error": response.text, "status_code": response.status_code} return {"error": f"{e}: {response.text}", "status_code": response.status_code}
except requests.exceptions.RequestException as e: 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)} return {"error": str(e)}
except Exception as e: except Exception as e:
print(f"❌ 其他异常: {e}") logger.error(f"❌ 其他异常: {e}")
return {"error": str(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 = { parsed_result = {
"success": True, "success": True,
@ -238,7 +240,7 @@ def _parse_recognition_result(api_result: Dict[str, Any], file_path: str) -> Dic
return parsed_result 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): 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() 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 "file_path": file_path
} }
print(f"📄 开始分析文档: {os.path.basename(file_path)}") logger.info(f"📄 开始分析文档: {os.path.basename(file_path)}")
print(f"📏 文件大小: {os.path.getsize(file_path) / 1024 / 1024:.2f} MB") logger.info(f"📏 文件大小: {os.path.getsize(file_path) / 1024 / 1024:.2f} MB")
print(f"📋 文件类型: {'PDF' if file_type == 0 else '图片'}") logger.info(f"📋 文件类型: {'PDF' if file_type == 0 else '图片'}")
try: try:
# 调用API进行识别 # 调用API进行识别
@ -291,3 +293,7 @@ def analyze_document(file_path: str) -> Dict[str, Any]:
"error": f"处理异常: {str(e)}", "error": f"处理异常: {str(e)}",
"file_path": file_path "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: 'disable', payload: { title: '不启用' } },
{ value: 'onnx_rapid_ocr', payload: { title: 'ONNX with RapidOCR' } }, { value: 'onnx_rapid_ocr', payload: { title: 'ONNX with RapidOCR' } },
{ value: 'mineru_ocr', payload: { title: 'MinerU OCR' } }, { value: 'mineru_ocr', payload: { title: 'MinerU OCR' } },
{ value: 'paddlex_ocr', payload: { title: 'Paddlex OCR' } },
]) ])
const use_rewrite_queryOptions = ref([ const use_rewrite_queryOptions = ref([
@ -811,7 +812,7 @@ const chunkFiles = () => {
.then(data => { .then(data => {
console.log('文件处理结果:', data) console.log('文件处理结果:', data)
if (data.status === 'success') { if (data.status === 'success') {
message.success(data.message || '文件已提交处理,请稍后在列表刷新查看状态'); message.info(data.message || '文件已提交处理,请稍后在列表刷新查看状态');
fileList.value = []; // fileList.value = []; //
addFilesModalVisible.value = false; // addFilesModalVisible.value = false; //
getDatabaseInfo(); // getDatabaseInfo(); //