ソースを参照

feat(新增mineru_vl_utils运行时补丁): 新增对PaddleOCR-VL的OTSL转换补丁,修复表格首格缺失前导结构token的问题,确保输出HTML中完整保留文本,提升文档解析的准确性与可靠性。同时在MinerUVLRecognizer初始化中应用该补丁,确保兼容性。

zhch158_admin 1 ヶ月 前
コミット
4e44a6c829

+ 92 - 0
ocr_tools/universal_doc_parser/models/adapters/_mineru_vl_patches.py

@@ -0,0 +1,92 @@
+"""mineru_vl_utils 运行时补丁集合。
+
+集中存放对第三方库 ``mineru_vl_utils`` 的运行时修补(monkey-patch),
+目的是在**不修改第三方源码**的前提下修复其在 PaddleOCR-VL 模型上的兼容性问题,
+并保证补丁随本仓库一起版本化、可随时开关、升级第三方库后不会丢失。
+
+当前包含的补丁:
+
+1. ``patch_convert_otsl_to_html``
+   修复 PaddleOCR-VL 输出的 OTSL「整表首个单元格缺少前导 ``<fcel>`` token」
+   导致 ``otsl_parse_texts`` 文本错位、所有单元格文字丢失的问题。
+
+统一通过 :func:`apply_once` 应用,幂等且仅在首次调用时生效。
+"""
+
+from __future__ import annotations
+
+from loguru import logger
+
+# OTSL 结构 token;与 mineru_vl_utils.post_process 内部定义保持一致
+_OTSL_STRUCT_TOKENS = ("<nl>", "<fcel>", "<ecel>", "<lcel>", "<ucel>", "<xcel>")
+
+_applied = False
+
+
+def _make_otsl_normalizer(orig_convert):
+    """生成一个在调用原始 convert_otsl_to_html 前先归一化 OTSL 的包装函数。"""
+
+    def _normalize_then_convert(otsl_content):
+        if isinstance(otsl_content, str):
+            stripped = otsl_content.lstrip()
+            # 整表首格缺少前导结构 token(如 PaddleOCR-VL)时补 <fcel>,
+            # 否则 otsl_parse_texts 的 text_idx 会永久错位,导致全部单元格文字丢失。
+            if (
+                stripped
+                and not stripped.startswith("<table")
+                and not stripped.startswith(_OTSL_STRUCT_TOKENS)
+            ):
+                otsl_content = "<fcel>" + stripped
+        return orig_convert(otsl_content)
+
+    # 记录原始函数,便于排查与还原
+    _normalize_then_convert.__wrapped__ = orig_convert
+    return _normalize_then_convert
+
+
+def _patch_convert_otsl_to_html():
+    """替换 post_process 命名空间中的 convert_otsl_to_html。
+
+    mineru_vl_utils.post_process.__init__ 通过
+    ``from .otsl2html import convert_otsl_to_html`` 导入该函数,
+    其内部 simple_process / _convert_pure_table_content_to_html 在调用时
+    按 post_process 模块全局名查找,因此覆盖该命名空间即可拦截全部内部调用。
+    """
+    import mineru_vl_utils.post_process as pp
+    from mineru_vl_utils.post_process import otsl2html
+
+    orig = getattr(otsl2html, "convert_otsl_to_html", None)
+    if orig is None:
+        # 上游接口变更时大声报错,避免补丁静默失效后又开始丢字
+        raise RuntimeError(
+            "mineru_vl_utils 接口已变更:找不到 otsl2html.convert_otsl_to_html,"
+            "请检查第三方库版本并更新补丁。"
+        )
+
+    wrapped = _make_otsl_normalizer(orig)
+    # 关键:post_process 内部调用按此命名空间查找
+    pp.convert_otsl_to_html = wrapped
+    # 兜底:若有代码直接 import otsl2html.convert_otsl_to_html
+    otsl2html.convert_otsl_to_html = wrapped
+
+
+def apply_once() -> bool:
+    """应用全部 mineru_vl_utils 运行时补丁,幂等。
+
+    应在任何 ``content_extract`` / ``batch_content_extract`` 调用之前执行一次
+    (通常放在 VL 识别器/检测器的 ``initialize()`` 内、获取模型之前)。
+
+    Returns:
+        bool: 本次调用是否真正应用了补丁(首次为 True,后续为 False)。
+    """
+    global _applied
+    if _applied:
+        return False
+    try:
+        _patch_convert_otsl_to_html()
+        _applied = True
+        logger.info("已应用 mineru_vl_utils 补丁:OTSL 整表首格 <fcel> 归一化")
+        return True
+    except Exception as e:  # 补丁失败不应阻断主流程,但需明确告警
+        logger.error(f"应用 mineru_vl_utils 补丁失败:{e}")
+        raise

+ 11 - 0
ocr_tools/universal_doc_parser/models/adapters/mineru_adapter.py

@@ -361,6 +361,17 @@ class MinerUVLRecognizer(BaseVLRecognizer):
         # 🔧 添加图片尺寸限制配置
         self.max_image_size = config.get('max_image_size', 1568)  # VLM 模型的最大尺寸
         self.resize_mode = config.get('resize_mode', 'max')  # 'max' or 'fixed'
+
+        # 应用 mineru_vl_utils 运行时补丁(修复 PaddleOCR-VL OTSL 首格 <fcel> 缺失导致表格文字丢失)
+        # 放在 __init__ 中,可同时覆盖 mineru 与 paddle 两条路径:
+        # PaddleVLRecognizer 重写了 initialize() 但其 __init__ 会经 super().__init__ 到达这里。
+        # 补丁仅替换 mineru_vl_utils.post_process 内函数,无需模型已加载,且幂等。
+        try:
+            from ._mineru_vl_patches import apply_once as _apply_mineru_vl_patches
+            _apply_mineru_vl_patches()
+        except Exception as e:
+            # 补丁失败不应阻断识别器创建,退回默认行为,但需明确告警
+            logger.warning(f"应用 mineru_vl_utils 补丁失败(退回默认行为,表格可能丢字): {e}")
         
     def initialize(self):
         """初始化VL模型"""