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:
parent
97ee5b0855
commit
eeb53f4774
@ -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))
|
||||||
|
|||||||
@ -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"}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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, {})
|
||||||
|
|
||||||
|
|||||||
@ -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]:
|
||||||
"""
|
"""
|
||||||
调用版面解析API:https://paddlepaddle.github.io/PaddleX/latest/pipeline_usage/tutorials/ocr_pipelines/PP-StructureV3.html#22-python
|
调用版面解析API:https://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)
|
||||||
BIN
test/data/ocr_test/PixPin_2025-06-19_23-42-17.png
Normal file
BIN
test/data/ocr_test/PixPin_2025-06-19_23-42-17.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@ -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(); // 刷新数据库信息以显示新文件及其状态
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user