跳到主要内容

hybrid 混合后端与 Office 直解

30 秒导读: MinerU 有两条"非主流"的解析支线。hybrid 让传统 pipeline 的小模型 (layout/公式/OCR)和 vlm 的大模型同台协作,再用一个 effort 旋钮在"更快"和"更准" 之间挑;Office 直解.docx/.pptx/.xlsx 根本不渲染成图,直接读 OOXML 结构解出内容。 两条支线走法迥异,但终点相同——都产出和主线一样的 middle_json,后面的 Markdown 渲染 完全复用。

本章讲这两条支线为什么存在、各自怎么走、又如何殊途同归。读前建议先看 02-pipeline-backend.md(小模型流水线)、03-vlm-backend.md (VLM 端到端)和 04-middle-json-and-markdown.md(统一中间表示)。


1. 先建立直觉:为什么要有这两条支线

前两章给了两种极端:

  • pipeline —— 一堆专用小模型串成流水线(layout 检测 → 公式识别 → OCR),快、可控,但每个 环节都可能出错,拼装规则复杂。
  • vlm —— 一个视觉大模型端到端吐出结构化块,省心、鲁棒,但推理慢、成本高。

这留下两个没被覆盖的现实需求:

现实需求主线为什么不够这条支线怎么补
想要 VLM 的语义理解,又不想全靠它扛精度和速度纯 vlm 慢;纯 pipeline 拼装脆hybrid:小模型做它擅长的(layout/公式/OCR det),VLM 做它擅长的(读内容),各取所长
输入本来就是 Office 文件(Word/PPT/Excel).docx 先转成 PDF 再渲染成图片再识别,是"把已经结构化的东西打碎再拼回来"Office 直解:直接读 OOXML(Office 的 XML 骨架),文字/表格/公式本就在那儿,原样取出

一句话记住两条支线的"性格":

  • hybrid = 让两代模型合作,并给你一个精度/速度旋钮(effort)。
  • Office 直解 = 有结构就别打碎;绕开"渲染成图 + 视觉识别"这一整套。

2. 顶层全景:两条支线如何汇入同一条河

先看大盘。三条后端最终都灌进同一个 middle_json,再由统一的 mkcontent 出 Markdown/JSON。

怎么读这张图:从左边的输入类型出发,看它走哪条支线、经过谁,箭头汇聚处就是"收敛点"。

输入 解析支线(本章两条加粗) 收敛
─────── ───────────────────────── ──────

PDF ──┬─► pipeline ──► 小模型流水线 ───────────────┐
│ (第2章) │
│ │
├─► vlm ─────► 视觉大模型端到端 ──────────────┤
│ (第3章) │
│ ├─► middle_json ─► mkcontent ─► .md / .json
└─► 【hybrid】► 小模型(layout/公式/OCR det) │ (第4章统一中间表示) (第4章)
+ VLM(读内容) ──────────────┤

DOCX ─┐ │
PPTX ─┼─► 【Office 直解】► 读 OOXML → 结构化块 ───────┘
XLSX ─┘ (不渲染成图、不过视觉模型)

部件一句话职责:

部件干什么在哪个文件
_process_office_doc按后缀把 Office 文件分派到 docx/pptx/xlsx 直解mineru/cli/common.py:618
office_{docx,pptx,xlsx}_analyze读文件字节 → OOXML 转换器 → result_to_middle_jsonmineru/backend/office/*_analyze.py
hybrid doc_analyze分窗口跑 layout 小模型 + VLM,按 effort 分支mineru/backend/hybrid/hybrid_analyze.py:889
HybridModelSingleton复用 pipeline 的 layout/mfr/ocr 小模型实例mineru/backend/pipeline/model_init.py
MinerUClient(predictor)vlm 的视觉大模型客户端mineru_vl_utils(外部库)
两个 MagicModel各自把本支线的原始结果整理成 middle_json 的块hybrid / office *_magic_model.py

主线走一遍(高层):

  1. 入口分派(do_parse)先看输入是不是 Office 后缀——是就走直解、并从 PDF 列表里剔除; 剩下的 PDF 再按 backend 走 pipeline / vlm / hybrid。
  2. hybrid:按页窗口,先用小模型出 layout,再交给 VLM 读内容,按 effort 决定谁主导。
  3. Office:直接把 OOXML 转成"块列表",不碰任何视觉模型。
  4. 收敛:两者都调各自的 result_to_middle_json / finalize_middle_json,产出同构 middle_json

3. hybrid 支线:让两代模型合作

3.1 它要解决的小问题

VLM 强在"读懂一块内容是什么",但让它自己也去做版面切分、精确定位、逐行文本框检测既慢又 未必更准——这些恰恰是 pipeline 小模型的强项。hybrid 的思路是:分工

  • 小模型(来自 pipeline):出 layout 版面框、行内公式框(mfr)、OCR 文本行检测(det)。
  • 大模型(来自 vlm):读每块的实际内容(文字、表格 HTML、图表)。

3.2 复用 pipeline 小模型 + vlm 客户端

hybrid 不重新实现小模型,而是直接 import pipeline 的那几个:

真实实现见 mineru/backend/hybrid/hybrid_analyze.py:21-27:

from mineru.backend.pipeline.model_init import (
HybridModelSingleton, # 复用 pipeline 的小模型单例
run_layout_inference, # 版面检测
run_mfr_inference, # 行内公式识别
run_ocr_inference, # OCR(此支线只用 det,不做 rec)
)

VLM 侧则复用 vlm 后端的客户端与执行守卫(hybrid_analyze.py:30-36),核心是 MinerUClient 类型的 predictor(见 doc_analyze 签名 hybrid_analyze.py:889)。

小模型的取用走 _predict_layout_for_window(hybrid_analyze.py:663):它 HybridModelSingleton().get_model(...) 拿到复用实例,只跑 layout(OCR 模式下文本交给 VLM,连公式模型都不开——见 :671 注释), 然后 _filter_inline_formulas_inside_containers 把落在表格/图/图表里的行内公式先滤掉。

3.3 先判"这份 PDF 要不要 OCR"

进入解析前,hybrid 先分类这份 PDF 是"文字型"还是"扫描型",决定文本从哪来:

真实实现 ocr_classify(hybrid_analyze.py:140):

def ocr_classify(pdf_bytes, parse_method='auto') -> bool:
_ocr_enable = False
if parse_method == 'auto':
if classify(pdf_bytes) == 'ocr': # 自动判定:像扫描件就开 OCR
_ocr_enable = True
elif parse_method == 'ocr':
_ocr_enable = True # 强制 OCR
return _ocr_enable

这个 _ocr_enable 布尔值贯穿全程,决定文本原生抽取 vs OCR 识别两条内部走法:

  • 非 OCR(文字型 PDF):文字本就嵌在 PDF 里,直接抽原生文本层(见 3.6 的 txt_spans_extract); 小模型只需 OCR det(检测文本行位置)当作 sidecar 提示。
  • OCR(扫描件):PDF 里没有文字层,内容全靠 VLM 读;小模型的 OCR det 只负责给出空文本行框 作为版面提示。

3.4 OCR det 只做"检测",不做"识别"

关键设计:在 hybrid 里,pipeline 的 OCR 模型只跑检测(det)、不跑识别(rec)——因为文字 要么来自 PDF 原生文本层,要么来自 VLM。ocr_det(hybrid_analyze.py:150)对每个文本类块裁图、 按需遮蔽公式区域,再调 run_ocr_inference(..., rec=False)(hybrid_analyze.py:205)只拿到文本行框。

_is_hybrid_ocr_det_candidate(hybrid_analyze.py:135)决定哪些块需要走 det:非 OCR 路径用 HYBRID_OCR_DET_TEXT_TYPES(不可抽取类型),VLM-OCR 路径改用 HYBRID_VLM_OCR_DET_TEXT_TYPES (text/title 等,hybrid_analyze.py:75)。检测出的行会被打包成 ocr_text sidecar 塞回结果 (_build_ocr_text_model_item,hybrid_analyze.py:800;VLM-OCR 路径可只留空文本行提示)。

3.5 effort:精度/速度旋钮

hybrid 的灵魂是一个 effort 参数,只接受 "medium""high"。它先被校验:

真实实现 _validate_parse_effort(hybrid_analyze.py:110):

HYBRID_ANALYZE_EFFORTS = {"medium", "high"}

def _validate_parse_effort(effort="medium"):
if effort not in HYBRID_ANALYZE_EFFORTS:
raise ValueError('effort must be "medium" or "high"') # 不静默走错分支
return effort

紧接着一条重要约束:medium 强制关闭图片分析,以保住"快"这条路。

真实实现 _resolve_effective_image_analysis(hybrid_analyze.py:117):

def _resolve_effective_image_analysis(effort, image_analysis):
if effort == "medium":
return False # medium 不支持 image_analysis,强制关掉
return image_analysis

两档的本质区别在于VLM 是否自己做版面:

effortVLM 调用layout 谁出image_analysis取舍
mediumbatch_extract_with_layoutpipeline 小模型出好 layout 喂给 VLM强制关闭更快:VLM 跳过版面解析,只读内容
highbatch_two_step_extractVLM 自己做两步(先版面后内容)可开更准:VLM 全权把控,含图片分析
  • medium:先跑小模型 layout,再用 _build_medium_vlm_layout_blocks(hybrid_analyze.py:368) 把 pipeline 的 layout 标签映射成 VLM 认识的类型(映射表 MEDIUM_EFFORT_LAYOUT_LABEL_TO_VLM_TYPE, :83),作为外部 layout 喂给 predictor.batch_extract_with_layout(...)(hybrid_analyze.py:981)。 VLM 省掉了自己切版面的开销。
  • high:直接 predictor.batch_two_step_extract(...)(hybrid_analyze.py:1008),VLM 自己先版面后 内容;此时 image_analysis 可透传,精度更高但更慢。

这套分支在 doc_analyze(hybrid_analyze.py:965-1035,if effort == "medium" / elif "high")和其异步 孪生 aio_doc_analyze(:1173-1248)里各写一遍,逻辑对称。

3.6 坐标归一 + 文本原生抽取

hybrid 把小模型(像素坐标)和 VLM(归一化 [0,1] 坐标)两套坐标系统一到 [0,1]

真实实现 normalize_bbox_to_unit(hybrid_analyze.py:296):

def normalize_bbox_to_unit(item, page_width, page_height):
bbox = item.get('bbox')
...
x0, y0, x1, y1 = [float(v) for v in bbox]
if 0.0 <= x0 <= 1.0 and ... and 0.0 <= y1 <= 1.0:
normalized_bbox = [x0, y0, x1, y1] # 已经是 [0,1],原样
else:
normalized_bbox = [x0/page_width, y0/page_height, # 像素 → 归一化
x1/page_width, y1/page_height]
item['bbox'] = [round(min(max(v, 0), 1), 3) for v in normalized_bbox] # 裁进 [0,1] 并保 3 位
return True

它兼容两种输入:已在 [0,1] 就原样,是像素就除以页宽高。归一化后,_normalize_bbox (hybrid_analyze.py:772)对整页公式和 OCR 结果批量走一遍,坐标系彻底对齐。

文本从哪来_ocr_enable 决定,收敛点在 hybrid 的 MagicModel。非 OCR 模式下,它对文本 span 调 txt_spans_extract(hybrid_magic_model.py:93)从 PDF 原生文本层抽字;OCR 模式下则 把 3.4 生成的 ocr_res sidecar 当作文本来源(hybrid_magic_model.py:81-90)。无论哪条,最后都 落进同一套 blocks_to_page_info(hybrid_model_output_to_middle_json.py:52)→ finalize_middle_json, 产出标准 middle_json

3.7 两个配套文件

  • hybrid_magic_model.py —— hybrid 版的 MagicModel,构造函数 (hybrid_magic_model.py:45)接收 page_model_list + page + _ocr_enable,先 _split_page_model_list(:401)把主块、行内公式、OCR sidecar 拆开,再按块类型整理成 image/table/chart/title/text 等块。它是"把 hybrid 混合出来的原始结果对齐成 middle_json 块"的地方。
  • hybrid_model_output_to_middle_json.py —— 提供 init_middle_json / append_page_model_list_to_middle_json / finalize_middle_json(hybrid_analyze.py:15-20 导入),把逐窗口的结果累积、后处理(跨页表格合并、 标题分级等)成最终 middle_json

4. Office 直解支线:有结构就别打碎

4.1 它要解决的小问题

.docx/.pptx/.xlsx 本质是 OOXML(Office Open XML)——一个 zip 包,里面标题就是标题、 表格就是表格、公式是 OMML(Office 的数学标记)。把它转 PDF、渲染成图、再让视觉模型"看图识字" 是信息的先破坏后重建。Office 直解的思路:直接读 OOXML,该是什么就取什么

4.2 按后缀分派

入口 _process_office_doc(mineru/cli/common.py:618)在正式解析前先"劫走"Office 文件。

后缀常量在 common.py:44-47:

docx_suffixes = ["docx"]
pptx_suffixes = ["pptx"]
xlsx_suffixes = ["xlsx"]
office_suffixes = docx_suffixes + pptx_suffixes + xlsx_suffixes

分派逻辑(common.py:632-652):

file_suffix = guess_suffix_by_bytes(file_bytes) # 按字节猜后缀,不信文件名
if file_suffix in office_suffixes:
need_remove_index.append(i) # 记下来,稍后从 PDF 列表剔除
if file_suffix in docx_suffixes: office_analyze = office_docx_analyze
elif file_suffix in pptx_suffixes: office_analyze = office_pptx_analyze
elif file_suffix in xlsx_suffixes: office_analyze = office_xlsx_analyze
middle_json, infer_result = office_analyze(file_bytes, image_writer=image_writer)

do_parse 在跑真正的 pipeline/vlm/hybrid 之前先调它,然后按返回的 need_remove_index 把 Office 文件从后续 PDF 列表里删掉(common.py:705-708),互不干扰。 异步入口 aio_do_parse 因为 Office 解析是同步且可能耗时,特意 asyncio.to_thread 包起来跑(common.py:786-788)避免阻塞事件循环。

4.3 三个 analyze:同一套骨架

三个 office_*_analyze 长得几乎一样,都是"读字节 → OOXML 转换器 → result_to_middle_json"。

office_docx_analyze(mineru/backend/office/docx_analyze.py:11)为例:

def office_docx_analyze(file_bytes, image_writer=None):
file_stream = BytesIO(file_bytes)
results = convert_binary(file_stream) # OOXML → 块列表(每页/每幻灯片一组)
middle_json = result_to_middle_json(results, image_writer) # → 标准 middle_json
return middle_json, results

pptx_analyze.py:11xlsx_analyze.py:11 与之逐行同构,只是 convert_binary 指向各自的 转换器。三者背后用的都是成熟的 OOXML 库,而非视觉模型:

后端转换器用的库怎么解
docxmineru/model/docx/docx_converter.pypython-docx + mammoth + lxml遍历段落/表格,OMML 公式经 oMath2Latex 转 LaTeX
pptxmineru/model/pptx/pptx_converter.pypython-pptx + lxml逐幻灯片取 shape,用 xycut 排版面阅读序
xlsxmineru/model/xlsx/xlsx_converter.pyopenpyxl + zipfile + ElementTree逐 sheet 读单元格,合并格/富文本 → HTML 表格

(库依赖分别见各 converter 顶部 import,如 docx_converter.py:8-22pptx_converter.py:8-25xlsx_converter.py:13-18。)

4.4 收敛:office 的 MagicModel 与 middle_json

三条都汇到 result_to_middle_json(mineru/backend/office/model_output_to_middle_json.py:126):

def result_to_middle_json(model_output_blocks_list, image_writer):
middle_json = {"pdf_info": [], "_backend": "office", "_version_name": __version__}
for index, page_blocks in enumerate(model_output_blocks_list):
page_info = blocks_to_page_info(page_blocks, image_writer, index) # 每"页"一份
middle_json["pdf_info"].append(page_info)
... # 章节编号(section_number)、目录锚点关联
return middle_json

每页交给 blocks_to_page_info(:11),内部实例化 office 版 MagicModel (mineru/backend/office/office_magic_model.py:13),把原始块整理成 image/table/chart/title/ text/list/index 等标准块,再按 index 排序。这里几个 Office 特有的巧处:

  • caption 归属:classify_caption_blocks(office_magic_model.py:737)按"表/图/图表前缀 + 相邻性" 把说明文字挂到正确母块(/table→table_caption,/fig→image_caption)。
  • 表格 HTML 清洗:clean_table_html(:503)只留 colspan/rowspan、图片、清洗过的 a.href,_sanitize_table_hyperlink_href(:486)拦掉 javascript:/data: 等危险协议。
  • 富文本 span:parse_text_block_spans(:227)解析 <eq>(行内公式)、<hyperlink>、 带 style<text>,还原加粗/斜体/上下标等样式。
  • 章节编号:result_to_middle_json(:132-169)用一组 section_counters 给带编号样式的标题 自动生成 1.2.1 这样的 section_number

最终这份 middle_json"_backend": "office",但结构与 pipeline/vlm/hybrid 完全同构, 因此 _process_output(common.py:283)只需按后缀选 office_union_make 就能复用第 4 章的 mkcontent 出 Markdown——见 office_middle_json_mkcontent.py(它只是从 mkcontent/inline_renderer.pymkcontent/output_builders.py re-export union_make 等函数,office_middle_json_mkcontent.py:1-36)。


5. 巧妙之处(可借鉴的技术)

  • effort 用"谁来做 layout"当旋钮,而不是模糊的"质量档位":medium 让小模型出 layout 省 VLM 的活、 high 让 VLM 全权两步走。语义清晰、可校验(_validate_parse_effort:110),且顺手绑定"medium 必关 image_analysis"这条硬约束(_resolve_effective_image_analysis:117)保证快路径不被拖慢。
  • OCR 只做 det 不做 rec:文本另有来源(PDF 原生层或 VLM)时,让 OCR 模型只干"找框"这件它最 擅长的事,run_ocr_inference(..., rec=False)(hybrid_analyze.py:205),省掉识别开销又保留版面信息。
  • 坐标系兼容判断:normalize_bbox_to_unit:296 用"值是否已在 [0,1]"来判定是归一化还是像素坐标, 一个函数吃两种输入,免去调用方各处传标志位。
  • 有结构就别打碎:Office 直解直接吃 OOXML,guess_suffix_by_bytes 按字节而非文件名判类型, 从入口就把 Office 文件与 PDF 分流(common.py:632-652),两条路互不污染。
  • 表格超链接协议白名单:_sanitize_table_hyperlink_href:486 在中间层就拦掉 javascript:/data:, 安全前移到解析阶段而非渲染阶段。

6. 边界与局限

  • hybrid effort 只有两档:medium/high,传别的直接 ValueError(hybrid_analyze.py:113); 没有"low"这种更快档。
  • medium 用不了 image_analysis:想要图片分析必须上 high(_resolve_effective_image_analysis:119)。
  • hybrid 依赖两套模型都在场:既要 pipeline 小模型,又要 VLM predictor,资源占用高于任一单后端; MINERU_HYBRID_BATCH_RATIO 和显存自动推断(get_batch_ratio:835)是为此做的显存权衡。
  • Office 直解不过视觉模型:纯靠 OOXML 结构——若文档里嵌的是扫描图片而非文字,直解取不到 文字(那部分只能当图片透传),不会像 pipeline/vlm 那样对图片做 OCR。
  • Office 只认三种后缀:docx/pptx/xlsx(common.py:44-47);旧式 .doc/.ppt/.xls 或其他格式 不在此列,_process_office_doc 里遇到未知 office 后缀会 raise ValueError(common.py:647)。

7. 横向对比

同 shelf 的 MinerU 内部三后端对照(本章两条 vs 前两章主线):

维度pipeline(02)vlm(03)hybrid(本章)office 直解(本章)
谁读内容小模型 OCRVLM小模型定位 + VLM 读OOXML 库
是否渲染成图是(PDF)
速度/精度旋钮effort medium/high无(直解即最快)
输入PDF/图片PDF/图片PDF/图片docx/pptx/xlsx
输出middle_jsonmiddle_jsonmiddle_jsonmiddle_json

四条支线的共同收敛点middle_json + mkcontent(见 04-middle-json-and-markdown.md), 这正是 MinerU 能"多后端、一套输出"的关键抽象。


8. 代码地图(导航索引)

主题文件路径符号名
hybrid 主流程(同步)mineru/backend/hybrid/hybrid_analyze.pydoc_analyze
hybrid 主流程(异步)mineru/backend/hybrid/hybrid_analyze.pyaio_doc_analyze
effort 校验mineru/backend/hybrid/hybrid_analyze.py_validate_parse_effort
medium 强制关图片分析mineru/backend/hybrid/hybrid_analyze.py_resolve_effective_image_analysis
OCR 需求判定mineru/backend/hybrid/hybrid_analyze.pyocr_classify
OCR det(只检测不识别)mineru/backend/hybrid/hybrid_analyze.pyocr_det
坐标归一到 [0,1]mineru/backend/hybrid/hybrid_analyze.pynormalize_bbox_to_unit
medium 复用小模型 layoutmineru/backend/hybrid/hybrid_analyze.py_build_medium_vlm_layout_blocks
复用 pipeline 小模型mineru/backend/pipeline/model_init.pyHybridModelSingleton, run_layout_inference, run_mfr_inference, run_ocr_inference
hybrid 块整理mineru/backend/hybrid/hybrid_magic_model.pyMagicModel, _split_page_model_list
hybrid 文本原生抽取mineru/backend/hybrid/hybrid_magic_model.pytxt_spans_extract(调用点)
hybrid 累积/收敛 middle_jsonmineru/backend/hybrid/hybrid_model_output_to_middle_json.pyfinalize_middle_json, append_page_model_list_to_middle_json
Office 后缀分派mineru/cli/common.py_process_office_doc, office_suffixes
docx/pptx/xlsx 直解入口mineru/backend/office/{docx,pptx,xlsx}_analyze.pyoffice_docx_analyze, office_pptx_analyze, office_xlsx_analyze
Office → middle_jsonmineru/backend/office/model_output_to_middle_json.pyresult_to_middle_json, blocks_to_page_info
Office 块整理 / caption / 表格清洗mineru/backend/office/office_magic_model.pyMagicModel, classify_caption_blocks, clean_table_html, parse_text_block_spans
Office mkcontent re-exportmineru/backend/office/office_middle_json_mkcontent.pyunion_make
OOXML 转换器mineru/model/{docx,pptx,xlsx}/*_converter.pyDocxConverter(docx), convert_binary(各 main)