这是系列文章的第二篇,重点介绍原生文字健康检测、块级文字对齐、递归序列化、跨页表格、图片资产以及人工校正治理。

公开说明:所有网络地址、账号、Token、密钥、源文档名称、内部哈希和个人路径均已删除或泛化。

系列目录

  1. 解析架构与质量基线
  2. 块级融合与人工质量验收
  3. Chunk、Embedding 与混合检索
  4. 服务部署、增量发布与安全运维

1. 为什么必须增加融合层

MinerU 能提供版面、阅读顺序、表格、图片和 bbox,但它的视觉正文并不总是适合作为最终正文。

实际测试发现,在健康原生文字页中,VLM 可能:

  • 删除标准号和技术对象;
  • 将一组英文缩写变成 /;
  • 丢失代码标识符;
  • 改变表格 caption;
  • 对看似重复的技术文本进行“概括”。

另一方面,原生文字层也可能损坏:肉眼看到的是正常中文,直接提取却只剩标点、符号和无意义短串。

所以最终采用统一融合流水线:

MinerU:结构、顺序、bbox、表格、图片、页面映射
PDF native:健康页面的正文真值
MinerU VLM:损坏文字层、扫描页和视觉页的正文
Fusion:按 block 对齐并输出统一 Schema

2. 原生文字健康检测

早期规则只检查:

  • 控制字符;
  • Unicode 私用区;
  • 替换字符;
  • Gxx 类伪字符。

这不够。有些损坏文字完全由合法 ASCII 字符组成,例如大量 !、#、$ 和短串。编码层面没有异常,语义层面却不可读。

最终加入以下指标:

  • CJK 字符比例;
  • 可读英文单词比例;
  • ASCII 符号异常比例;
  • 标点/符号熵;
  • 私用区字符比例;
  • 替换字符比例;
  • Gxx 伪字符比例;
  • 英文异常逐字空格;
  • native 与 VLM 的脚本分布差异;
  • 页面可见内容覆盖率;
  • 页类型分类。

2.1 native 与 VLM 对照

对于中文可见文字页,如果出现:

  • native 几乎无中文;
  • native 主要由符号和无意义短串组成;
  • VLM 有大量正常中文;
  • 两者脚本分布差异巨大;

则必须将 native 判定为损坏文字层。

这种判断不能只靠一个字符数阈值。图像页的错误 caption 可能超过十个字符,但仍完全不可读。

2.2 页面分类

建议为页面增加显式类型:

TEXT_PAGE
TABLE_PAGE
FIGURE_PAGE
VISUAL_ONLY_PAGE
OCR_PAGE
MIXED_PAGE

FIGURE_PAGE 和 VISUAL_ONLY_PAGE 需要优先检查图片 caption 与视觉覆盖率,不能仅因 native 字符数非零就判为健康。

2.3 决策逻辑

if native_text_health == HEALTHY:
    authoritative_source = native_pdf
else:
    authoritative_source = mineru_hybrid_ocr

这只是页面级决策。真正输出正式正文前,还需要完成 block 级对齐。

3. 什么才是真正的块级融合

把整页 native 正文放在 body_text,再附上一组使用 VLM 错误文字的 bbox,不是块级融合。

正确流程:

  1. 每份 PDF 打开一次;
  2. 每页 PdfPage 和 TextPage 构建一次;
  3. 提取带坐标的 native spans;
  4. 读取 MinerU block、类型、order 和 bbox;
  5. 使用几何重叠、顺序、脚本和文本特征进行匹配;
  6. 健康页 block 使用对齐后的 native 文字;
  7. OCR 页 block 使用 MinerU Hybrid High 结果;
  8. 无法对齐时显式标记 UNRESOLVED;
  9. 任意 UNRESOLVED 阻断后续 Chunk。

性能上必须缓存同页对象。同一页所有 block 共用一次 native 文字提取,页面处理完再释放;不能为每个 block 重新打开 PDF。

4. 统一 Block Schema

正式 Schema 可设计为:

{
  "block_id": "stable-block-id",
  "document_id": "stable-document-id",
  "original_page": 32,
  "order": 17,
  "type": "text",
  "bbox": [72.1, 118.4, 523.8, 166.2],
  "bbox_coordinate_space": "pdf_page",
  "bbox_origin": "top_left",
  "page_width": 595.28,
  "page_height": 841.89,
  "bbox_unit": "pt",
  "text_source": "native_pdf",
  "text_alignment_status": "ALIGNED",
  "raw_text": "原始提取文字",
  "normalized_text": "确定性修复后的文字",
  "search_text": "面向检索的文字",
  "normalization_operations": [],
  "search_normalization_operations": [],
  "dictionary_suggestions": [],
  "requires_review": false,
  "structure_source": "mineru",
  "table_html": null,
  "asset_path": null,
  "source_sha256": "<source-pdf-sha256>"
}

4.1 坐标元数据

仅保存 [x0, y0, x1, y1] 不够。每个 block、表格片段和图片都应保存:

  • bbox_coordinate_space
  • bbox_origin
  • page_width
  • page_height
  • bbox_unit

否则迁移到另一个渲染器后,很难判断 bbox 使用 PDF point、像素还是归一化坐标,原点在左上还是左下。

4.2 资产路径

正式 asset_path 必须是相对于文档输出根目录的 POSIX 路径:

assets/structured-crops/page-0037-figure-001.png

不能把某台 Windows 工作站的绝对路径写进迁移主字段。运行时可以额外提供 runtime_absolute_path,但它不能成为 Canonical 的正式定位方式。

5. 递归序列化 MinerU 内容

一个很隐蔽的问题来自 content_list_v2 的序列化器。如果只读取浅层字段:

text
table_body
caption
footnote

那么标题、列表、公式和嵌套内容会被静默遗漏。

递归序列化器至少支持:

  • title_content
  • paragraph_content
  • list_items
  • item_content
  • equation_inline
  • table_caption
  • table_footnote
  • image_caption
  • image_footnote
  • 普通 text
  • 嵌套 list 与 dict

推荐将遍历逻辑写成字段白名单加通用递归,而不是不断给顶层 if 打补丁。

还应增加确定性门禁:MinerU 识别出的 list_items 数量必须等于最终序列化数量。

VLM 原始序列化内容可以保留用于审计,但应明确标记:

non_authoritative_vlm_audit

否则人工审核者容易误以为其中的错误文字就是最终融合正文。

6. 跨页表格的正确模型

跨页表格必须同时保存两种资产。

6.1 logical_table

表示一张完整逻辑表:

  • 稳定 logical_table_id;
  • 正式 caption;
  • 合并后的完整结构;
  • page fragment ID 列表;
  • 唯一片段数;
  • 合并后行数。

6.2 page_table_fragment

表示每个原始页面自己的真实表格片段。它应直接来源于 MinerU middle.json 中该页面的 preproc_blocks,而不是从合并表反推。

{
  "fragment_id": "stable-fragment-id",
  "logical_table_id": "stable-table-id",
  "original_page": 8,
  "order": 3,
  "bbox": [60.0, 100.0, 540.0, 780.0],
  "bbox_coordinate_space": "pdf_page",
  "bbox_origin": "top_left",
  "page_width": 595.28,
  "page_height": 841.89,
  "bbox_unit": "pt",
  "fragment_index": 2,
  "fragment_count": 3,
  "continuation": true,
  "table_html": "<table>...</table>"
}

7. 表格双向关联门禁

必须同时满足:

  1. 每个 fragment 恰好被一个活动 logical table 引用;
  2. logical table 引用的 fragment,其 logical_table_id 反向一致;
  3. 同一 fragment 不得出现在多个 logical table 中;
  4. fragment_count 等于唯一 fragment 数量;
  5. fragment_index 在 1..fragment_count 内连续且不重复;
  6. 同一表的片段按原始页码和页内 order 排序;
  7. logical table 行数与各页片段合并去重后的行数一致;
  8. fragment 必须具有真实 original_page 和 bbox。

如果两个 logical table 同时引用同一组 fragment,而 fragment 又只反向指向其中一个表,JSON 仍然合法,但所有权关系已经损坏。这类问题必须阻断,而不是自动挑一个表继续。

7.1 caption 的权威来源

健康 native 页的表格 caption 也必须通过 native bbox 对齐获得。否则 VLM 可能把:

表 A.1

变成:

表 A1

这种小数点丢失会直接影响图表引用检索。

8. 图片资产治理

图片处理遵循:

  • 原始裁剪图是主资产;
  • Mermaid 或图片描述只是辅助;
  • 不允许 Mermaid 替代原图;
  • 每个图对象保留图题、原始页码、bbox、相对路径和文件 SHA-256;
  • 图片对象数量与真实文件数量一致;
  • 图题使用可信文字源;
  • 服务器只保留必要结构化裁剪,不保留重复整页图。

图像页尤其容易出现“native 字符数大于阈值,但内容实际是乱码”的误判。因此图题也必须经过文字健康检查。

9. 三层文字设计

9.1 raw_text

原始提取结果,永久不改。它是审计、回放和后续改进算法的依据。

9.2 normalized_text

只进行确定性修复,例如:

  • 10. 3. 1 -> 10.3.1;
  • 标准号内部确定的异常空格;
  • 可以由上下文和格式规则唯一确定的符号。

所有修改写入 normalization_operations。

9.3 search_text

用于 BM25 和 Embedding,可额外进行:

  • 中文异常逐字空格修复;
  • OCR 产生的 LaTeX \sim -> ~;
  • 标准号检索格式统一;
  • 不改变语义的空白和标点处理。

所有修改写入 search_normalization_operations。

10. 不确定术语只能建议,不能覆盖

如果 OCR 将一个技术词识别成近形字,而系统只有“疑似正确”的判断,应保留原文:

{
  "raw_text": "OCR 原始结果",
  "normalized_text": "OCR 原始结果",
  "dictionary_suggestions": [
    {
      "candidate": "疑似正确词",
      "reason": "domain_dictionary_match"
    }
  ],
  "requires_review": true
}

字典建议服务于人工审核,不能借“自动纠错”之名覆盖 OCR 证据。

11. 自动质量门禁

融合完成后至少检查:

  • 原始 PDF SHA-256 不变;
  • 页面映射与原 PDF 一致;
  • JSON 严格解析,无 NaN/Infinity;
  • UNRESOLVED block 为 0;
  • 可见正文或表格页不能同时正文为空且无表格片段;
  • OCR 页正文达到最低覆盖率;
  • native/VLM 脚本差异没有触发误信任;
  • 标准号、年份和技术标识召回;
  • 条款号、图号和表号格式;
  • 阅读顺序;
  • 跨页表格首列或行号连续性;
  • 图片对象与文件一一对应;
  • list item 识别数与序列化数一致;
  • logical table 与 fragment 双向一致;
  • bbox 坐标元数据完整;
  • 所有正式资产路径均为相对路径。

不能只因为文件存在、JSON 可解析就输出 PASS。

12. 人工 QC 检查什么

自动检查后,人工逐页对照原图,重点检查:

  • 标准号和年份;
  • 条款编号;
  • 英文缩写和代码标识;
  • 数字字段;
  • 0/O、1/I/l、5/S、8/B;
  • 表题、表头和数字单元格;
  • 跨页表格连续性;
  • 图题和真实图片;
  • 代码块的大小写、标签号、括号和跨页闭合关系。

每个待核对项都包含:

路线 / 文档 / 原始页码 / 输出文件 / 提取文本
人工结果:PASS | FAIL | NOT_CHECKED
备注

自动状态与人工状态分离:

PASS_PENDING_HUMAN_QC
PASS_WITH_REVIEW_FLAGS
BLOCKED

自动结构检查通过,只代表可以进入人工复核,不代表 OCR 内容已经正确。

13. Pilot 修复了什么

全量前先运行约 100 页 Pilot。它暴露了两个会污染全库的类别:

NATIVE_TEXT_HEALTH_FALSE_POSITIVE
LOGICAL_TABLE_FRAGMENT_OWNERSHIP_INVALID

前者导致正常中文页面使用乱码 native 层,后者导致两个逻辑表争用同一组页级 fragment。

修复后重新构建融合层,不重跑已存在的 MinerU 模型产物,并对全部 Pilot 页面重新执行门禁。只有自动检查和人工抽查均通过后,才进入 2,999 页全量。

14. 全量分批与断点恢复

全量按小批次处理,每次持久化:

  • 当前批次;
  • 已完成文档;
  • 失败文档;
  • 每个文档的重试次数;
  • 输入来源哈希;
  • 输出 Manifest 哈希;
  • 文档 QC 状态。

最终 41 份文档分 14 批完成,未留下失败文档。中途中断时只继续未完成文档,不重跑成功结果。

15. 人工校正包

少数技术代码页即使整体通过,仍可能出现确定性序列化错误。正确校正流程是:

  1. 固定异常 block_id;
  2. 保存异常 raw 文本;
  3. 保存原始页证据;
  4. 创建 proposed correction;
  5. 逐行人工核对;
  6. 保存文件清单和内部 SHA-256;
  7. 审核 PASS 后只替换目标 block 的正式文本;
  8. 保留 raw 文本和校正记录;
  9. 重跑块级、页级、严格 JSON 和全量门禁。

校正包可以包含:

correction-request.json
original-page.png
original-abnormal-text.txt
proposed-correction.txt
human-review.json
package-manifest.json
SHA256SUMS

不要直接打开 Canonical JSON 手工改字。没有证据、范围和哈希的修改无法可靠重放。

16. 本阶段完成标准

  • 所有健康页使用 native block 对齐文本
  • 所有 OCR 页同时保留 raw 和 normalized/search 文本
  • UNRESOLVED block 为 0
  • 表格 logical/fragment 关系双向一致
  • 每个图片对象有真实资产
  • 所有 bbox 坐标元数据完整
  • 资产路径可迁移
  • 自动门禁通过
  • 人工抽查通过或明确保留 review flag
  • Canonical 输入和输出哈希冻结

当 Canonical 通过这些门禁后,才允许进入 Chunk 和 Embedding。下一篇将介绍结构感知切分、Qwen3 Embedding、PostgreSQL、HNSW、BM25、RRF 与 Reranker 的完整构建过程。