From 6ea82ff0d5541ae3b97ea3444083e976aa76ae50 Mon Sep 17 00:00:00 2001 From: myhloli Date: Fri, 8 May 2026 17:04:50 +0800 Subject: [PATCH 01/22] feat: add hybrid-flash analyze functionality with formula recognition scope --- mineru/backend/hybrid_flash/__init__.py | 1 + .../hybrid_flash/hybrid_flash_analyze.py | 479 ++++++++++++++++++ mineru/backend/pipeline/batch_analyze.py | 10 +- mineru/backend/pipeline/pipeline_analyze.py | 12 +- 4 files changed, 499 insertions(+), 3 deletions(-) create mode 100644 mineru/backend/hybrid_flash/__init__.py create mode 100644 mineru/backend/hybrid_flash/hybrid_flash_analyze.py diff --git a/mineru/backend/hybrid_flash/__init__.py b/mineru/backend/hybrid_flash/__init__.py new file mode 100644 index 00000000..1e17167c --- /dev/null +++ b/mineru/backend/hybrid_flash/__init__.py @@ -0,0 +1 @@ +# Copyright (c) Opendatalab. All rights reserved. diff --git a/mineru/backend/hybrid_flash/hybrid_flash_analyze.py b/mineru/backend/hybrid_flash/hybrid_flash_analyze.py new file mode 100644 index 00000000..d56ab108 --- /dev/null +++ b/mineru/backend/hybrid_flash/hybrid_flash_analyze.py @@ -0,0 +1,479 @@ +# Copyright (c) Opendatalab. All rights reserved. +import os +import time + +import pypdfium2 as pdfium +from loguru import logger +from mineru_vl_utils.structs import ContentBlock +from tqdm import tqdm + +from mineru.backend.utils.runtime_utils import exclude_progress_bar_idle_time +from mineru.backend.vlm.vlm_analyze import ( + ModelSingleton, + _get_model_async, + _maybe_enable_serial_execution, + aio_predictor_execution_guard, + predictor_execution_guard, +) +from mineru.data.data_reader_writer import DataWriter +from mineru.utils.config_reader import get_device, get_processing_window_size +from mineru.utils.enum_class import ImageType +from mineru.utils.model_utils import clean_memory +from mineru.utils.pdf_classify import classify +from mineru.utils.pdf_image_tools import ( + aio_load_images_from_pdf_bytes_range, + load_images_from_pdf_doc, +) +from mineru.utils.pdfium_guard import ( + close_pdfium_document, + get_pdfium_document_page_count, + open_pdfium_document, +) +from mineru.version import __version__ + + +VLM_VISUAL_LABELS = {"image", "chart"} +VLM_TEXT_LABEL_TO_TYPE = { + "abstract": "text", + "algorithm": "code", + "aside_text": "aside_text", + "content": "text", + "doc_title": "title", + "footer": "footer", + "footnote": "page_footnote", + "formula_number": "text", + "header": "header", + "number": "page_number", + "paragraph_title": "title", + "reference_content": "ref_text", + "text": "text", + "vertical_text": "text", + "vision_footnote": "page_footnote", +} + + +def batch_image_analyze(*args, **kwargs): + """懒加载pipeline analyze,避免导入hybrid-flash模块时提前要求torch环境。""" + from mineru.backend.pipeline.pipeline_analyze import batch_image_analyze as pipeline_batch_image_analyze + + return pipeline_batch_image_analyze(*args, **kwargs) + + +def _get_ocr_enable(pdf_bytes, parse_method: str) -> bool: + """根据parse_method解析OCR开关,保持和pipeline/hybrid入口一致。""" + if parse_method == "auto": + return classify(pdf_bytes) == "ocr" + if parse_method == "ocr": + return True + return False + + +def _should_enable_vlm_ocr(ocr_enable: bool, language: str, inline_formula_enable: bool) -> bool: + """判断是否让VLM抽取文本内容,OCR-det本身不受该开关影响。""" + force_enable = os.getenv("MINERU_FORCE_VLM_OCR_ENABLE", "0").lower() in ("1", "true", "yes") + if force_enable: + return True + + force_pipeline = os.getenv("MINERU_HYBRID_FORCE_PIPELINE_ENABLE", "0").lower() in ("1", "true", "yes") + return ( + ocr_enable + and language in ["ch", "en"] + and inline_formula_enable + and not force_pipeline + ) + + +def _close_images(images_list): + """关闭窗口内PIL图片,避免长文档处理时文件句柄或内存累积。""" + for image_dict in images_list or []: + pil_img = image_dict.get("img_pil") + if pil_img is not None: + try: + pil_img.close() + except Exception: + pass + + +def _normalize_bbox_to_unit(bbox, page_width: int, page_height: int) -> list[float] | None: + """把pipeline像素bbox转换为mineru-vl-utils需要的归一化bbox。""" + if bbox is None or len(bbox) != 4: + return None + x0, y0, x1, y1 = [float(v) for v in bbox] + if 0.0 <= x0 <= 1.0 and 0.0 <= y0 <= 1.0 and 0.0 <= x1 <= 1.0 and 0.0 <= y1 <= 1.0: + normalized_bbox = [x0, y0, x1, y1] + else: + normalized_bbox = [ + x0 / page_width, + y0 / page_height, + x1 / page_width, + y1 / page_height, + ] + normalized_bbox = [round(min(max(v, 0.0), 1.0), 6) for v in normalized_bbox] + if normalized_bbox[0] >= normalized_bbox[2] or normalized_bbox[1] >= normalized_bbox[3]: + return None + return normalized_bbox + + +def _vlm_type_for_layout_det(layout_det: dict, vlm_ocr_enable: bool, table_enable: bool, image_analysis: bool) -> str | None: + """把pipeline layout label映射为VLM内容抽取类型,未命中则跳过。""" + label = layout_det.get("label") + if label == "table": + return "table" if table_enable else None + if label == "display_formula": + return "equation" + if label in VLM_VISUAL_LABELS: + return label if image_analysis else None + if vlm_ocr_enable: + return VLM_TEXT_LABEL_TO_TYPE.get(label) + return None + + +def _build_vlm_layout_blocks( + layout_dets: list[dict], + page_width: int, + page_height: int, + vlm_ocr_enable: bool, + table_enable: bool, + image_analysis: bool, +) -> list[ContentBlock]: + """从pipeline layout结果构造VLM sidecar输入,并保留回填用索引。""" + blocks = [] + for position, layout_det in enumerate(layout_dets): + vlm_type = _vlm_type_for_layout_det( + layout_det, + vlm_ocr_enable=vlm_ocr_enable, + table_enable=table_enable, + image_analysis=image_analysis, + ) + if vlm_type is None: + continue + bbox = _normalize_bbox_to_unit(layout_det.get("bbox"), page_width, page_height) + if bbox is None: + continue + try: + block = ContentBlock( + vlm_type, + bbox, + angle=layout_det.get("angle", 0), + content=layout_det.get("content"), + ) + except AssertionError as exc: + logger.warning(f"Skip invalid hybrid-flash VLM block: {layout_det}, error: {exc}") + continue + block["_layout_det_index"] = layout_det.get("index", position) + block["_layout_det_position"] = position + block["_layout_det_label"] = layout_det.get("label") + blocks.append(block) + return blocks + + +def _build_not_extract_list(vlm_ocr_enable: bool) -> list[str] | None: + """非VLM-OCR模式下显式跳过文本抽取,保持hybrid文本边界。""" + if vlm_ocr_enable: + return None + return ["text"] + + +def _strip_display_formula_delimiters(content: str) -> str: + """去除VLM公式结果外层display delimiters,便于回填pipeline latex字段。""" + stripped = content.strip() + if stripped.startswith("\\[") and stripped.endswith("\\]"): + stripped = stripped[2:-2].strip() + return stripped + + +def _merge_vlm_sidecar_result(layout_dets: list[dict], sidecar_blocks) -> None: + """把VLM sidecar结果回填到pipeline layout_dets,保持原始label不变。""" + by_index = { + layout_det.get("index", position): layout_det + for position, layout_det in enumerate(layout_dets) + } + for block in sidecar_blocks or []: + layout_det = by_index.get(block.get("_layout_det_index")) + if layout_det is None: + position = block.get("_layout_det_position") + if isinstance(position, int) and 0 <= position < len(layout_dets): + layout_det = layout_dets[position] + if layout_det is None: + continue + + block_type = block.get("type") + block_content = block.get("content") + label = layout_det.get("label") + + if label in VLM_VISUAL_LABELS: + if block_content is not None: + layout_det["content"] = block_content + if "sub_type" in block: + layout_det["sub_type"] = block["sub_type"] + continue + + if label == "table" or block_type == "table": + if block_content is not None: + layout_det["html"] = block_content + if "cell_merge" in block: + layout_det["cell_merge"] = block["cell_merge"] + continue + + if label == "display_formula" or block_type == "equation": + if block_content: + layout_det["latex"] = _strip_display_formula_delimiters(block_content) + continue + + if block_type in VLM_TEXT_LABEL_TO_TYPE.values() and block_content is not None: + layout_det["text"] = block_content + layout_det["content"] = block_content + + +def _build_page_model_info(page_layout_dets: list[dict], page_index: int, pil_img) -> dict: + """按pipeline model_list格式包装单页analyze结果。""" + return { + "layout_dets": page_layout_dets, + "page_info": { + "page_no": page_index, + "width": pil_img.width, + "height": pil_img.height, + }, + } + + +def _build_analyze_meta(ocr_enable: bool, vlm_ocr_enable: bool) -> dict: + """构造第一阶段analyze元信息,供后续阶段判断backend分支和OCR策略。""" + return { + "_backend": "hybrid-flash", + "_ocr_enable": ocr_enable, + "_vlm_ocr_enable": vlm_ocr_enable, + "_version_name": __version__, + } + + +def _get_device_for_cleanup(): + """获取清理显存用device;测试或轻量环境缺少torch时退回CPU。""" + try: + return get_device() + except NameError: + return "cpu" + + +def _ensure_external_layout_api(predictor) -> None: + """确认mineru-vl-utils已提供外部layout抽取接口,避免静默走错VLM layout路径。""" + if not hasattr(predictor, "batch_extract_with_layout"): + raise AttributeError( + "hybrid-flash requires mineru-vl-utils with `MinerUClient.batch_extract_with_layout` support" + ) + + +def doc_analyze( + pdf_bytes, + image_writer: DataWriter | None = None, + predictor=None, + backend="transformers", + parse_method: str = "auto", + language: str = "ch", + inline_formula_enable: bool = True, + table_enable: bool = True, + model_path: str | None = None, + server_url: str | None = None, + image_analysis: bool = True, + **kwargs, +): + """hybrid-flash第一阶段analyze:返回pipeline形态model_list和backend元信息。""" + if predictor is None: + predictor = ModelSingleton().get_model(backend, model_path, server_url, **kwargs) + predictor = _maybe_enable_serial_execution(predictor, backend) + _ensure_external_layout_api(predictor) + + device = _get_device_for_cleanup() + ocr_enable = _get_ocr_enable(pdf_bytes, parse_method=parse_method) + vlm_ocr_enable = _should_enable_vlm_ocr(ocr_enable, language, inline_formula_enable) + pdf_doc = open_pdfium_document(pdfium.PdfDocument, pdf_bytes) + doc_closed = False + model_list = [] + try: + page_count = get_pdfium_document_page_count(pdf_doc) + configured_window_size = get_processing_window_size(default=64) + effective_window_size = min(page_count, configured_window_size) if page_count else 0 + total_windows = ( + (page_count + effective_window_size - 1) // effective_window_size + if effective_window_size + else 0 + ) + logger.info( + f"Hybrid-flash analyze window run. page_count={page_count}, " + f"window_size={configured_window_size}, total_windows={total_windows}" + ) + + infer_start = time.time() + progress_bar = None + last_append_end_time = None + try: + for window_index, window_start in enumerate(range(0, page_count, effective_window_size or 1)): + window_end = min(page_count - 1, window_start + effective_window_size - 1) + images_list = load_images_from_pdf_doc( + pdf_doc, + start_page_id=window_start, + end_page_id=window_end, + image_type=ImageType.PIL, + pdf_bytes=pdf_bytes, + ) + try: + images_pil_list = [image_dict["img_pil"] for image_dict in images_list] + logger.info( + f"Hybrid-flash analyze window {window_index + 1}/{total_windows}: " + f"pages {window_start + 1}-{window_end + 1}/{page_count} " + f"({len(images_pil_list)} pages)" + ) + pipeline_inputs = [ + (pil_img, ocr_enable, language) + for pil_img in images_pil_list + ] + page_layout_dets_list = batch_image_analyze( + pipeline_inputs, + formula_enable=inline_formula_enable, + table_enable=False, + formula_recognition_scope="inline_only", + ) + vlm_blocks_list = [ + _build_vlm_layout_blocks( + page_layout_dets, + pil_img.width, + pil_img.height, + vlm_ocr_enable=vlm_ocr_enable, + table_enable=table_enable, + image_analysis=image_analysis, + ) + for page_layout_dets, pil_img in zip(page_layout_dets_list, images_pil_list) + ] + if any(vlm_blocks_list): + with predictor_execution_guard(predictor): + sidecar_results = predictor.batch_extract_with_layout( + images_pil_list, + vlm_blocks_list, + not_extract_list=_build_not_extract_list(vlm_ocr_enable), + image_analysis=image_analysis, + ) + for page_layout_dets, sidecar_blocks in zip(page_layout_dets_list, sidecar_results): + _merge_vlm_sidecar_result(page_layout_dets, sidecar_blocks) + + if progress_bar is None: + progress_bar = tqdm(total=page_count, desc="Processing pages") + else: + exclude_progress_bar_idle_time( + progress_bar, + last_append_end_time, + now=time.time(), + ) + + for offset, (page_layout_dets, pil_img) in enumerate(zip(page_layout_dets_list, images_pil_list)): + page_index = window_start + offset + model_list.append(_build_page_model_info(page_layout_dets, page_index, pil_img)) + if progress_bar is not None: + progress_bar.update(1) + last_append_end_time = time.time() + finally: + _close_images(images_list) + finally: + if progress_bar is not None: + progress_bar.close() + + infer_time = round(time.time() - infer_start, 2) + if infer_time > 0 and page_count > 0: + logger.debug( + f"hybrid-flash analyze finished, cost: {infer_time}, " + f"speed: {round(len(model_list) / infer_time, 3)} page/s" + ) + close_pdfium_document(pdf_doc) + doc_closed = True + clean_memory(device) + return model_list, _build_analyze_meta(ocr_enable, vlm_ocr_enable) + finally: + if not doc_closed: + close_pdfium_document(pdf_doc) + + +async def aio_doc_analyze( + pdf_bytes, + image_writer: DataWriter | None = None, + predictor=None, + backend="transformers", + parse_method: str = "auto", + language: str = "ch", + inline_formula_enable: bool = True, + table_enable: bool = True, + model_path: str | None = None, + server_url: str | None = None, + image_analysis: bool = True, + **kwargs, +): + """异步hybrid-flash analyze入口,返回pipeline形态model_list和backend元信息。""" + if predictor is None: + predictor = await _get_model_async(backend, model_path, server_url, **kwargs) + predictor = _maybe_enable_serial_execution(predictor, backend) + if not hasattr(predictor, "aio_batch_extract_with_layout"): + raise AttributeError( + "hybrid-flash requires mineru-vl-utils with `MinerUClient.aio_batch_extract_with_layout` support" + ) + + device = _get_device_for_cleanup() + ocr_enable = _get_ocr_enable(pdf_bytes, parse_method=parse_method) + vlm_ocr_enable = _should_enable_vlm_ocr(ocr_enable, language, inline_formula_enable) + pdf_doc = open_pdfium_document(pdfium.PdfDocument, pdf_bytes) + doc_closed = False + model_list = [] + try: + page_count = get_pdfium_document_page_count(pdf_doc) + configured_window_size = get_processing_window_size(default=64) + effective_window_size = min(page_count, configured_window_size) if page_count else 0 + + for window_start in range(0, page_count, effective_window_size or 1): + window_end = min(page_count - 1, window_start + effective_window_size - 1) + images_list = await aio_load_images_from_pdf_bytes_range( + pdf_bytes, + start_page_id=window_start, + end_page_id=window_end, + image_type=ImageType.PIL, + ) + try: + images_pil_list = [image_dict["img_pil"] for image_dict in images_list] + pipeline_inputs = [ + (pil_img, ocr_enable, language) + for pil_img in images_pil_list + ] + page_layout_dets_list = batch_image_analyze( + pipeline_inputs, + formula_enable=inline_formula_enable, + table_enable=False, + formula_recognition_scope="inline_only", + ) + vlm_blocks_list = [ + _build_vlm_layout_blocks( + page_layout_dets, + pil_img.width, + pil_img.height, + vlm_ocr_enable=vlm_ocr_enable, + table_enable=table_enable, + image_analysis=image_analysis, + ) + for page_layout_dets, pil_img in zip(page_layout_dets_list, images_pil_list) + ] + if any(vlm_blocks_list): + async with aio_predictor_execution_guard(predictor): + sidecar_results = await predictor.aio_batch_extract_with_layout( + images_pil_list, + vlm_blocks_list, + not_extract_list=_build_not_extract_list(vlm_ocr_enable), + image_analysis=image_analysis, + ) + for page_layout_dets, sidecar_blocks in zip(page_layout_dets_list, sidecar_results): + _merge_vlm_sidecar_result(page_layout_dets, sidecar_blocks) + for offset, (page_layout_dets, pil_img) in enumerate(zip(page_layout_dets_list, images_pil_list)): + model_list.append(_build_page_model_info(page_layout_dets, window_start + offset, pil_img)) + finally: + _close_images(images_list) + close_pdfium_document(pdf_doc) + doc_closed = True + clean_memory(device) + return model_list, _build_analyze_meta(ocr_enable, vlm_ocr_enable) + finally: + if not doc_closed: + close_pdfium_document(pdf_doc) diff --git a/mineru/backend/pipeline/batch_analyze.py b/mineru/backend/pipeline/batch_analyze.py index 1d88eb42..2d6a0637 100644 --- a/mineru/backend/pipeline/batch_analyze.py +++ b/mineru/backend/pipeline/batch_analyze.py @@ -44,6 +44,7 @@ class BatchAnalyze: table_ori_cls_batch_enabled: bool | None = None, text_ocr_det_batch_enabled: bool | None = None, mask_inline_formula_for_ocr_det: bool = True, + formula_recognition_scope: str = "all", ): self.batch_ratio = batch_ratio self.formula_enable = get_formula_enable(formula_enable) @@ -59,6 +60,10 @@ class BatchAnalyze: self.mask_inline_formula_for_ocr_det = ( get_ocr_det_mask_inline_formula_enable(mask_inline_formula_for_ocr_det) ) + if formula_recognition_scope not in {"all", "inline_only"}: + raise ValueError(f"Unsupported formula_recognition_scope: {formula_recognition_scope}") + # 控制公式识别范围,默认保持pipeline原行为;hybrid-flash只让pipeline处理行内公式。 + self.formula_recognition_scope = formula_recognition_scope @staticmethod def _apply_mask_boxes_to_image( @@ -327,11 +332,14 @@ class BatchAnalyze: clean_vram(self.model.device, vram_threshold=8) if self.formula_enable: + formula_labels = ["display_formula", "inline_formula"] + if self.formula_recognition_scope == "inline_only": + formula_labels = ["inline_formula"] images_mfd_res = [] for layout_res in images_layout_res: page_formula_res = [] for res in layout_res: - if res.get("label") in ["display_formula", "inline_formula"]: + if res.get("label") in formula_labels: res.setdefault("latex", "") page_formula_res.append(res) images_mfd_res.append(page_formula_res) diff --git a/mineru/backend/pipeline/pipeline_analyze.py b/mineru/backend/pipeline/pipeline_analyze.py index aa309145..5589ace7 100644 --- a/mineru/backend/pipeline/pipeline_analyze.py +++ b/mineru/backend/pipeline/pipeline_analyze.py @@ -295,7 +295,8 @@ def doc_analyze_streaming( def batch_image_analyze( images_with_extra_info: List[Tuple[Image.Image, bool, str]], formula_enable=True, - table_enable=True): + table_enable=True, + formula_recognition_scope="all"): from .batch_analyze import BatchAnalyze @@ -340,7 +341,14 @@ def batch_image_analyze( os.environ["TORCH_CUDNN_V8_API_DISABLED"] = "1" enable_ocr_det_batch = True - batch_model = BatchAnalyze(model_manager, batch_ratio, formula_enable, table_enable, enable_ocr_det_batch) + batch_model = BatchAnalyze( + model_manager, + batch_ratio, + formula_enable, + table_enable, + enable_ocr_det_batch, + formula_recognition_scope=formula_recognition_scope, + ) results = batch_model(images_with_extra_info) clean_memory(get_device()) From ce44a62c021b12eea7183280207d5fa9a15be8ae Mon Sep 17 00:00:00 2001 From: myhloli Date: Thu, 4 Jun 2026 18:17:00 +0800 Subject: [PATCH 02/22] feat: add inline formula detection inputs for VLM-OCR processing --- mineru/backend/hybrid/hybrid_analyze.py | 30 ++++++++++++++++++--- mineru/backend/hybrid/hybrid_magic_model.py | 2 +- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/mineru/backend/hybrid/hybrid_analyze.py b/mineru/backend/hybrid/hybrid_analyze.py index 095aa2d4..89679ed2 100644 --- a/mineru/backend/hybrid/hybrid_analyze.py +++ b/mineru/backend/hybrid/hybrid_analyze.py @@ -326,6 +326,28 @@ def _build_formula_mask_inputs(images_layout_res): return page_formula_masks +def _build_inline_formula_det_inputs(images_layout_res): + """从 layout 检测结果提取行内公式框,供 VLM-OCR 作为 OCR det hint 使用。""" + inline_formula_inputs = [] + for layout_res in images_layout_res: + page_inline_formula_inputs = [] + for res in layout_res: + if res.get('label') != 'inline_formula': + continue + bbox = _formula_item_to_pixel_bbox(res) + if bbox is None: + continue + page_inline_formula_inputs.append( + { + "bbox": bbox, + "score": float(res.get('score', 0.0)), + "latex": "", + } + ) + inline_formula_inputs.append(page_inline_formula_inputs) + return inline_formula_inputs + + def _normalize_page_size(page_image): """从PIL或numpy图像中读取页面宽高,供归一化bbox还原为像素bbox。""" if hasattr(page_image, "size"): @@ -570,20 +592,22 @@ def _apply_layout_title_split_for_window( images_pil_list, batch_ratio, ) + formula_mask_inputs = _build_formula_mask_inputs(images_layout_res) + inline_formula_list = _build_inline_formula_det_inputs(images_layout_res) np_images = [np.asarray(pil_image).copy() for pil_image in images_pil_list] ocr_res_list = ocr_det( hybrid_pipeline_model, np_images, model_list, - _build_formula_mask_inputs(images_layout_res), + formula_mask_inputs, False, batch_ratio=batch_ratio, fill_text=False, ) - _normalize_bbox([[] for _ in images_pil_list], ocr_res_list, images_pil_list) + _normalize_bbox(inline_formula_list, ocr_res_list, images_pil_list) model_list[:] = _merge_page_sidecar_items( model_list, - [[] for _ in images_pil_list], + inline_formula_list, ocr_res_list, keep_ocr_text=False, ) diff --git a/mineru/backend/hybrid/hybrid_magic_model.py b/mineru/backend/hybrid/hybrid_magic_model.py index 21555ec2..a2a19629 100644 --- a/mineru/backend/hybrid/hybrid_magic_model.py +++ b/mineru/backend/hybrid/hybrid_magic_model.py @@ -74,7 +74,7 @@ class MagicModel: for inline_formula in self.page_inline_formula: inline_formula["bbox"] = list(self.cal_real_bbox(inline_formula["bbox"])) inline_formula_latex = inline_formula.pop("latex", "") - if inline_formula_latex: + if inline_formula_latex or _vlm_ocr_enable: page_text_inline_formula_spans.append( { "bbox": inline_formula["bbox"], From b97814b69f99bdc366363b3920cd5e365568a2d7 Mon Sep 17 00:00:00 2001 From: myhloli Date: Fri, 5 Jun 2026 00:05:28 +0800 Subject: [PATCH 03/22] feat: enhance OCR and formula recognition configuration with new flags --- .../hybrid_flash/hybrid_flash_analyze.py | 46 +++++++----- mineru/backend/pipeline/batch_analyze.py | 73 ++++++++++++------- mineru/backend/pipeline/pipeline_analyze.py | 6 +- 3 files changed, 79 insertions(+), 46 deletions(-) diff --git a/mineru/backend/hybrid_flash/hybrid_flash_analyze.py b/mineru/backend/hybrid_flash/hybrid_flash_analyze.py index d56ab108..0bb96822 100644 --- a/mineru/backend/hybrid_flash/hybrid_flash_analyze.py +++ b/mineru/backend/hybrid_flash/hybrid_flash_analyze.py @@ -32,7 +32,10 @@ from mineru.utils.pdfium_guard import ( from mineru.version import __version__ -VLM_VISUAL_LABELS = {"image", "chart"} +VLM_VISUAL_LABELS = {"image", "chart", "seal"} +VLM_VISUAL_TYPE_BY_LABEL = { + "seal": "image", +} VLM_TEXT_LABEL_TO_TYPE = { "abstract": "text", "algorithm": "code", @@ -122,7 +125,7 @@ def _vlm_type_for_layout_det(layout_det: dict, vlm_ocr_enable: bool, table_enabl if label == "display_formula": return "equation" if label in VLM_VISUAL_LABELS: - return label if image_analysis else None + return VLM_VISUAL_TYPE_BY_LABEL.get(label, label) if image_analysis else None if vlm_ocr_enable: return VLM_TEXT_LABEL_TO_TYPE.get(label) return None @@ -201,6 +204,13 @@ def _merge_vlm_sidecar_result(layout_dets: list[dict], sidecar_blocks) -> None: block_content = block.get("content") label = layout_det.get("label") + if label == "seal": + if block_content is not None: + layout_det["text"] = block_content + layout_det["content"] = block_content + layout_det["sub_type"] = "seal" + continue + if label in VLM_VISUAL_LABELS: if block_content is not None: layout_det["content"] = block_content @@ -247,6 +257,19 @@ def _build_analyze_meta(ocr_enable: bool, vlm_ocr_enable: bool) -> dict: } +def _build_pipeline_batch_options(vlm_ocr_enable: bool) -> dict: + """构造 hybrid-flash 调用 pipeline batch 时的识别策略。""" + if vlm_ocr_enable: + return { + "formula_recognition_scope": "none", + "ocr_rec_enable": False, + } + return { + "formula_recognition_scope": "inline_only", + "ocr_rec_enable": True, + } + + def _get_device_for_cleanup(): """获取清理显存用device;测试或轻量环境缺少torch时退回CPU。""" try: @@ -255,14 +278,6 @@ def _get_device_for_cleanup(): return "cpu" -def _ensure_external_layout_api(predictor) -> None: - """确认mineru-vl-utils已提供外部layout抽取接口,避免静默走错VLM layout路径。""" - if not hasattr(predictor, "batch_extract_with_layout"): - raise AttributeError( - "hybrid-flash requires mineru-vl-utils with `MinerUClient.batch_extract_with_layout` support" - ) - - def doc_analyze( pdf_bytes, image_writer: DataWriter | None = None, @@ -281,7 +296,6 @@ def doc_analyze( if predictor is None: predictor = ModelSingleton().get_model(backend, model_path, server_url, **kwargs) predictor = _maybe_enable_serial_execution(predictor, backend) - _ensure_external_layout_api(predictor) device = _get_device_for_cleanup() ocr_enable = _get_ocr_enable(pdf_bytes, parse_method=parse_method) @@ -331,7 +345,8 @@ def doc_analyze( pipeline_inputs, formula_enable=inline_formula_enable, table_enable=False, - formula_recognition_scope="inline_only", + seal_ocr_rec_enable=False, + **_build_pipeline_batch_options(vlm_ocr_enable), ) vlm_blocks_list = [ _build_vlm_layout_blocks( @@ -409,10 +424,6 @@ async def aio_doc_analyze( if predictor is None: predictor = await _get_model_async(backend, model_path, server_url, **kwargs) predictor = _maybe_enable_serial_execution(predictor, backend) - if not hasattr(predictor, "aio_batch_extract_with_layout"): - raise AttributeError( - "hybrid-flash requires mineru-vl-utils with `MinerUClient.aio_batch_extract_with_layout` support" - ) device = _get_device_for_cleanup() ocr_enable = _get_ocr_enable(pdf_bytes, parse_method=parse_method) @@ -443,7 +454,8 @@ async def aio_doc_analyze( pipeline_inputs, formula_enable=inline_formula_enable, table_enable=False, - formula_recognition_scope="inline_only", + seal_ocr_rec_enable=False, + **_build_pipeline_batch_options(vlm_ocr_enable), ) vlm_blocks_list = [ _build_vlm_layout_blocks( diff --git a/mineru/backend/pipeline/batch_analyze.py b/mineru/backend/pipeline/batch_analyze.py index 737dbbda..7cd6ddef 100644 --- a/mineru/backend/pipeline/batch_analyze.py +++ b/mineru/backend/pipeline/batch_analyze.py @@ -58,11 +58,15 @@ class BatchAnalyze: text_ocr_det_batch_enabled: bool | None = None, mask_inline_formula_for_ocr_det: bool = True, formula_recognition_scope: str = "all", + ocr_rec_enable: bool = True, + seal_ocr_rec_enable: bool = True, ): self.batch_ratio = batch_ratio self.formula_enable = get_formula_enable(formula_enable) self.table_enable = get_table_enable(table_enable) self.model_manager = model_manager + self.ocr_rec_enable = ocr_rec_enable + self.seal_ocr_rec_enable = seal_ocr_rec_enable self.enable_ocr_det_batch = enable_ocr_det_batch self.table_ori_cls_batch_enabled = ( enable_ocr_det_batch if table_ori_cls_batch_enabled is None else table_ori_cls_batch_enabled @@ -73,9 +77,9 @@ class BatchAnalyze: self.mask_inline_formula_for_ocr_det = ( get_ocr_det_mask_inline_formula_enable(mask_inline_formula_for_ocr_det) ) - if formula_recognition_scope not in {"all", "inline_only"}: + if formula_recognition_scope not in {"all", "inline_only", "none"}: raise ValueError(f"Unsupported formula_recognition_scope: {formula_recognition_scope}") - # 控制公式识别范围,默认保持pipeline原行为;hybrid-flash只让pipeline处理行内公式。 + # 控制公式识别范围,默认保持pipeline原行为;hybrid-flash可只保留公式det而跳过MFR。 self.formula_recognition_scope = formula_recognition_scope @staticmethod @@ -264,6 +268,9 @@ class BatchAnalyze: return match.expand(replacement) return text + def _formula_recognition_enabled(self) -> bool: + return self.formula_enable and self.formula_recognition_scope != "none" + @classmethod def _extract_table_inline_objects( cls, @@ -362,7 +369,7 @@ class BatchAnalyze: self.model = self.model_manager.get_model( lang=None, - formula_enable=self.formula_enable, + formula_enable=self._formula_recognition_enabled(), table_enable=self.table_enable, ) atom_model_manager = AtomModelSingleton() @@ -381,35 +388,42 @@ class BatchAnalyze: clean_vram(self.model.device, vram_threshold=8) if self.formula_enable: - formula_labels = ["display_formula", "inline_formula"] + all_formula_labels = ["display_formula", "inline_formula"] + formula_labels = all_formula_labels if self.formula_recognition_scope == "inline_only": formula_labels = ["inline_formula"] images_mfd_res = [] for layout_res in images_layout_res: page_formula_res = [] for res in layout_res: - if res.get("label") in formula_labels: + if res.get("label") in all_formula_labels: res.setdefault("latex", "") + if res.get("label") in formula_labels: page_formula_res.append(res) images_mfd_res.append(page_formula_res) - # 公式识别 - images_formula_list = run_mfr_inference( - self.model.mfr_model.batch_predict, - images_mfd_res, - np_images, - batch_size=self.batch_ratio * MFR_BASE_BATCH_SIZE, - ) - mfr_count = 0 - for image_index in range(len(np_images)): - mfr_count += len(images_formula_list[image_index]) - for formula_res, formula_with_latex in zip( - images_mfd_res[image_index], images_formula_list[image_index] - ): - formula_res["latex"] = formula_with_latex.get("latex", "") + if self.formula_recognition_scope != "none": + # 公式识别 + images_formula_list = run_mfr_inference( + self.model.mfr_model.batch_predict, + images_mfd_res, + np_images, + batch_size=self.batch_ratio * MFR_BASE_BATCH_SIZE, + ) + mfr_count = 0 + for image_index in range(len(np_images)): + mfr_count += len(images_formula_list[image_index]) + for formula_res, formula_with_latex in zip( + images_mfd_res[image_index], images_formula_list[image_index] + ): + formula_res["latex"] = formula_with_latex.get("latex", "") - # 清理显存 - clean_vram(self.model.device, vram_threshold=8) + # 清理显存 + clean_vram(self.model.device, vram_threshold=8) + else: + for page_formula_res in images_mfd_res: + for formula_res in page_formula_res: + formula_res["latex"] = "" else: for layout_res in images_layout_res: @@ -418,6 +432,8 @@ class BatchAnalyze: + ocr_should_recognize_text = bool(self.ocr_rec_enable) + ocr_res_list_all_page = [] table_res_list_all_page = [] for index in range(len(np_images)): @@ -768,7 +784,7 @@ class BatchAnalyze: ocr_result_list = get_ocr_result_list( ocr_res, useful_list, - ocr_res_list_dict['ocr_enable'], + ocr_res_list_dict['ocr_enable'] and ocr_should_recognize_text, bgr_image, _lang, ) @@ -812,7 +828,7 @@ class BatchAnalyze: ocr_result_list = get_ocr_result_list( ocr_res, useful_list, - ocr_res_list_dict['ocr_enable'], + ocr_res_list_dict['ocr_enable'] and ocr_should_recognize_text, bgr_image, _lang, ) @@ -901,10 +917,11 @@ class BatchAnalyze: total_processed += len(img_crop_list) seal_ocr_items = [] - for ocr_res_list_dict in ocr_res_list_all_page: - for layout_res_item in ocr_res_list_dict['layout_res']: - if layout_res_item.get("label") == "seal": - seal_ocr_items.append((ocr_res_list_dict, layout_res_item)) + if self.seal_ocr_rec_enable: + for ocr_res_list_dict in ocr_res_list_all_page: + for layout_res_item in ocr_res_list_dict['layout_res']: + if layout_res_item.get("label") == "seal": + seal_ocr_items.append((ocr_res_list_dict, layout_res_item)) seal_ocr_model = None for ocr_res_list_dict, layout_res_item in tqdm(seal_ocr_items, desc="Seal Predict"): @@ -952,7 +969,7 @@ class BatchAnalyze: for ocr_res_list_dict in ocr_res_list_all_page: self._prune_empty_ocr_text_blocks( ocr_res_list_dict["layout_res"], - ocr_res_list_dict["ocr_enable"], + ocr_res_list_dict["ocr_enable"] and self.ocr_rec_enable, ) return images_layout_res diff --git a/mineru/backend/pipeline/pipeline_analyze.py b/mineru/backend/pipeline/pipeline_analyze.py index 3310147a..e8670d18 100644 --- a/mineru/backend/pipeline/pipeline_analyze.py +++ b/mineru/backend/pipeline/pipeline_analyze.py @@ -332,7 +332,9 @@ def batch_image_analyze( images_with_extra_info: List[Tuple[Image.Image, bool, str]], formula_enable=True, table_enable=True, - formula_recognition_scope="all"): + formula_recognition_scope="all", + ocr_rec_enable=True, + seal_ocr_rec_enable=True): from .batch_analyze import BatchAnalyze @@ -384,6 +386,8 @@ def batch_image_analyze( table_enable, enable_ocr_det_batch, formula_recognition_scope=formula_recognition_scope, + ocr_rec_enable=ocr_rec_enable, + seal_ocr_rec_enable=seal_ocr_rec_enable, ) results = batch_model(images_with_extra_info) From ae5418db7c81f72f9bffbd9b884b16ce4361c6e1 Mon Sep 17 00:00:00 2001 From: myhloli Date: Fri, 5 Jun 2026 11:33:37 +0800 Subject: [PATCH 04/22] feat: update VLM label mappings and enhance layout detection logging --- mineru/backend/hybrid_flash/hybrid_flash_analyze.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/mineru/backend/hybrid_flash/hybrid_flash_analyze.py b/mineru/backend/hybrid_flash/hybrid_flash_analyze.py index 0bb96822..a11f8cf3 100644 --- a/mineru/backend/hybrid_flash/hybrid_flash_analyze.py +++ b/mineru/backend/hybrid_flash/hybrid_flash_analyze.py @@ -33,9 +33,6 @@ from mineru.version import __version__ VLM_VISUAL_LABELS = {"image", "chart", "seal"} -VLM_VISUAL_TYPE_BY_LABEL = { - "seal": "image", -} VLM_TEXT_LABEL_TO_TYPE = { "abstract": "text", "algorithm": "code", @@ -43,15 +40,18 @@ VLM_TEXT_LABEL_TO_TYPE = { "content": "text", "doc_title": "title", "footer": "footer", + "footer_image": "footer", "footnote": "page_footnote", "formula_number": "text", "header": "header", + "header_image": "header", "number": "page_number", "paragraph_title": "title", "reference_content": "ref_text", "text": "text", "vertical_text": "text", - "vision_footnote": "page_footnote", + "figure_title": "image_caption", + "vision_footnote": "image_footnote", } @@ -120,12 +120,15 @@ def _normalize_bbox_to_unit(bbox, page_width: int, page_height: int) -> list[flo def _vlm_type_for_layout_det(layout_det: dict, vlm_ocr_enable: bool, table_enable: bool, image_analysis: bool) -> str | None: """把pipeline layout label映射为VLM内容抽取类型,未命中则跳过。""" label = layout_det.get("label") + if label is None: + logger.warning("Layout detection result missing label: %s", layout_det) + return None if label == "table": return "table" if table_enable else None if label == "display_formula": return "equation" if label in VLM_VISUAL_LABELS: - return VLM_VISUAL_TYPE_BY_LABEL.get(label, label) if image_analysis else None + return "image" if image_analysis else None if vlm_ocr_enable: return VLM_TEXT_LABEL_TO_TYPE.get(label) return None From 7b3b655721ad4fd3a881daf62bc1e94bf6432f91 Mon Sep 17 00:00:00 2001 From: myhloli Date: Fri, 5 Jun 2026 16:08:37 +0800 Subject: [PATCH 05/22] feat: add hybrid-flash engine and client options for enhanced multi-language support --- mineru/backend/hybrid_flash/hybrid_flash_analyze.py | 5 +++-- mineru/cli/api_request.py | 4 +++- mineru/cli/client.py | 4 ++++ mineru/cli/gradio_app.py | 2 ++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/mineru/backend/hybrid_flash/hybrid_flash_analyze.py b/mineru/backend/hybrid_flash/hybrid_flash_analyze.py index a11f8cf3..212a7c24 100644 --- a/mineru/backend/hybrid_flash/hybrid_flash_analyze.py +++ b/mineru/backend/hybrid_flash/hybrid_flash_analyze.py @@ -4,6 +4,7 @@ import time import pypdfium2 as pdfium from loguru import logger +from mineru_vl_utils import MinerUClient from mineru_vl_utils.structs import ContentBlock from tqdm import tqdm @@ -284,7 +285,7 @@ def _get_device_for_cleanup(): def doc_analyze( pdf_bytes, image_writer: DataWriter | None = None, - predictor=None, + predictor: MinerUClient | None = None, backend="transformers", parse_method: str = "auto", language: str = "ch", @@ -412,7 +413,7 @@ def doc_analyze( async def aio_doc_analyze( pdf_bytes, image_writer: DataWriter | None = None, - predictor=None, + predictor: MinerUClient | None = None, backend="transformers", parse_method: str = "auto", language: str = "ch", diff --git a/mineru/cli/api_request.py b/mineru/cli/api_request.py index a90cb8d1..4e640a31 100644 --- a/mineru/cli/api_request.py +++ b/mineru/cli/api_request.py @@ -92,7 +92,9 @@ async def parse_request_form( - vlm-auto-engine: High accuracy via local computing power, supports Chinese and English documents only. - vlm-http-client: High accuracy via remote computing power(client suitable for openai-compatible servers), supports Chinese and English documents only. - hybrid-auto-engine: Next-generation high accuracy solution via local computing power, supports multiple languages. -- hybrid-http-client: High accuracy via remote computing power but requires a little local computing power(client suitable for openai-compatible servers), supports multiple languages.""", +- hybrid-flash-auto-engine: Experimental hybrid-flash analyze path via local computing power, supports multiple languages. +- hybrid-http-client: High accuracy via remote computing power but requires a little local computing power(client suitable for openai-compatible servers), supports multiple languages. +- hybrid-flash-http-client: Experimental hybrid-flash analyze path via remote computing power(client suitable for openai-compatible servers), supports multiple languages.""", ), ] = "hybrid-auto-engine", parse_method: Annotated[ diff --git a/mineru/cli/client.py b/mineru/cli/client.py index c00c68d5..05021446 100644 --- a/mineru/cli/client.py +++ b/mineru/cli/client.py @@ -1034,8 +1034,10 @@ async def run_orchestrated_cli( "pipeline", "vlm-http-client", "hybrid-http-client", + "hybrid-flash-http-client", "vlm-auto-engine", "hybrid-auto-engine", + "hybrid-flash-auto-engine", ] ), default="hybrid-auto-engine", @@ -1045,7 +1047,9 @@ async def run_orchestrated_cli( vlm-auto-engine: High accuracy via local computing power. vlm-http-client: High accuracy via remote computing power(client suitable for openai-compatible servers). hybrid-auto-engine: Next-generation high accuracy solution via local computing power. + hybrid-flash-auto-engine: Experimental hybrid-flash analyze path via local computing power. hybrid-http-client: High accuracy but requires a little local computing power(client suitable for openai-compatible servers). + hybrid-flash-http-client: Experimental hybrid-flash analyze path via remote computing power(client suitable for openai-compatible servers). Without method specified, hybrid-auto-engine will be used by default.""", ) @click.option( diff --git a/mineru/cli/gradio_app.py b/mineru/cli/gradio_app.py index e305d69f..04c4b068 100644 --- a/mineru/cli/gradio_app.py +++ b/mineru/cli/gradio_app.py @@ -228,10 +228,12 @@ BACKEND_CHOICE_DEFINITIONS = [ "pipeline", "vlm-auto-engine", "hybrid-auto-engine", + "hybrid-flash-auto-engine", ] HTTP_CLIENT_BACKEND_CHOICE_DEFINITIONS = [ "vlm-http-client", "hybrid-http-client", + "hybrid-flash-http-client", ] STATUS_STEP_DEFINITIONS = [ ("status_step_prepare", STATUS_PREPARING_REQUEST), From c48076afd1b218d363f18b2bc172bd1c0123d8db Mon Sep 17 00:00:00 2001 From: myhloli Date: Fri, 5 Jun 2026 17:46:28 +0800 Subject: [PATCH 06/22] feat: update parsing backend references from '-auto-engine' to '-engine' across documentation --- README.md | 2 +- README_zh-CN.md | 2 +- docs/en/quick_start/index.md | 2 +- docs/en/usage/cli_tools.md | 4 ++-- docs/zh/quick_start/index.md | 2 +- docs/zh/usage/acceleration_cards/Ascend.md | 6 ++--- docs/zh/usage/acceleration_cards/Biren.md | 6 ++--- docs/zh/usage/acceleration_cards/Cambricon.md | 6 ++--- docs/zh/usage/acceleration_cards/Enflame.md | 6 ++--- docs/zh/usage/acceleration_cards/Hygon.md | 6 ++--- .../usage/acceleration_cards/IluvatarCorex.md | 6 ++--- docs/zh/usage/acceleration_cards/Kunlunxin.md | 6 ++--- docs/zh/usage/acceleration_cards/METAX.md | 6 ++--- .../usage/acceleration_cards/MooreThreads.md | 6 ++--- docs/zh/usage/acceleration_cards/THead.md | 6 ++--- docs/zh/usage/acceleration_cards/Tecorigin.md | 6 ++--- docs/zh/usage/acceleration_cards/VastAI.md | 22 +++++++++---------- docs/zh/usage/cli_tools.md | 4 ++-- 18 files changed, 52 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 0423b732..47b776cb 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ A WebUI developed based on Gradio, with a simple interface and only core parsing Parsing Backend pipeline - *-auto-engine + *-engine *-http-client diff --git a/README_zh-CN.md b/README_zh-CN.md index d6e33077..7ee80344 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -183,7 +183,7 @@ https://github.com/user-attachments/assets/4bea02c9-6d54-4cd6-97ed-dff14340982c 解析后端 pipeline - *-auto-engine + *-engine *-http-client diff --git a/docs/en/quick_start/index.md b/docs/en/quick_start/index.md index fbf76fae..b4234a1a 100644 --- a/docs/en/quick_start/index.md +++ b/docs/en/quick_start/index.md @@ -33,7 +33,7 @@ A WebUI developed based on Gradio, with a simple interface and only core parsing Parsing Backend pipeline - *-auto-engine + *-engine *-http-client diff --git a/docs/en/usage/cli_tools.md b/docs/en/usage/cli_tools.md index 01d93c1f..1c254c8f 100644 --- a/docs/en/usage/cli_tools.md +++ b/docs/en/usage/cli_tools.md @@ -12,8 +12,8 @@ Options: -o, --output PATH Output directory (required) --api-url TEXT MinerU FastAPI base URL; if omitted, `mineru` starts a temporary local `mineru-api` -m, --method [auto|txt|ocr] Parsing method: auto (default), txt, ocr (pipeline and hybrid* backend only) - -b, --backend [pipeline|hybrid-auto-engine|hybrid-http-client|vlm-auto-engine|vlm-http-client] - Parsing backend (default: hybrid-auto-engine) + -b, --backend [pipeline|hybrid-engine|hybrid-http-client|vlm-engine|vlm-http-client] + Parsing backend (default: hybrid-engine) -l, --lang [ch|ch_server|ch_lite|en|korean|japan|chinese_cht|ta|te|ka|th|el|latin|arabic|east_slavic|cyrillic|devanagari] Specify document language (improves OCR accuracy, pipeline and hybrid* backend only) -u, --url TEXT OpenAI-compatible backend URL passed through to the server when using http-client diff --git a/docs/zh/quick_start/index.md b/docs/zh/quick_start/index.md index eb9792a8..04fea421 100644 --- a/docs/zh/quick_start/index.md +++ b/docs/zh/quick_start/index.md @@ -33,7 +33,7 @@ 解析后端 pipeline - *-auto-engine + *-engine *-http-client diff --git a/docs/zh/usage/acceleration_cards/Ascend.md b/docs/zh/usage/acceleration_cards/Ascend.md index 7aa643f7..ccfd25a0 100644 --- a/docs/zh/usage/acceleration_cards/Ascend.md +++ b/docs/zh/usage/acceleration_cards/Ascend.md @@ -116,7 +116,7 @@ docker run -u root --name mineru_docker --privileged=true \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 🟢 @@ -132,7 +132,7 @@ docker run -u root --name mineru_docker --privileged=true \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 🟢 @@ -148,7 +148,7 @@ docker run -u root --name mineru_docker --privileged=true \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 🟢 diff --git a/docs/zh/usage/acceleration_cards/Biren.md b/docs/zh/usage/acceleration_cards/Biren.md index acf9a2cc..154d2d94 100644 --- a/docs/zh/usage/acceleration_cards/Biren.md +++ b/docs/zh/usage/acceleration_cards/Biren.md @@ -57,7 +57,7 @@ docker run -it --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 @@ -70,7 +70,7 @@ docker run -it --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 @@ -83,7 +83,7 @@ docker run -it --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 diff --git a/docs/zh/usage/acceleration_cards/Cambricon.md b/docs/zh/usage/acceleration_cards/Cambricon.md index fc8a9af4..83943a03 100644 --- a/docs/zh/usage/acceleration_cards/Cambricon.md +++ b/docs/zh/usage/acceleration_cards/Cambricon.md @@ -95,7 +95,7 @@ docker run --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟡 🟡 @@ -111,7 +111,7 @@ docker run --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🔴 🟢 @@ -127,7 +127,7 @@ docker run --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🔴 🟢 diff --git a/docs/zh/usage/acceleration_cards/Enflame.md b/docs/zh/usage/acceleration_cards/Enflame.md index 19f66272..0ee1cbaa 100644 --- a/docs/zh/usage/acceleration_cards/Enflame.md +++ b/docs/zh/usage/acceleration_cards/Enflame.md @@ -55,7 +55,7 @@ docker run -u root --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 @@ -68,7 +68,7 @@ docker run -u root --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 @@ -81,7 +81,7 @@ docker run -u root --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 diff --git a/docs/zh/usage/acceleration_cards/Hygon.md b/docs/zh/usage/acceleration_cards/Hygon.md index faaada2a..d6ad7947 100644 --- a/docs/zh/usage/acceleration_cards/Hygon.md +++ b/docs/zh/usage/acceleration_cards/Hygon.md @@ -62,7 +62,7 @@ docker run -u root --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 @@ -75,7 +75,7 @@ docker run -u root --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 @@ -88,7 +88,7 @@ docker run -u root --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 diff --git a/docs/zh/usage/acceleration_cards/IluvatarCorex.md b/docs/zh/usage/acceleration_cards/IluvatarCorex.md index f1ef7c71..8022461d 100644 --- a/docs/zh/usage/acceleration_cards/IluvatarCorex.md +++ b/docs/zh/usage/acceleration_cards/IluvatarCorex.md @@ -69,7 +69,7 @@ docker run --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 @@ -82,7 +82,7 @@ docker run --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 @@ -95,7 +95,7 @@ docker run --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 diff --git a/docs/zh/usage/acceleration_cards/Kunlunxin.md b/docs/zh/usage/acceleration_cards/Kunlunxin.md index bb54cbdd..48576492 100644 --- a/docs/zh/usage/acceleration_cards/Kunlunxin.md +++ b/docs/zh/usage/acceleration_cards/Kunlunxin.md @@ -69,7 +69,7 @@ docker run -u root --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 @@ -82,7 +82,7 @@ docker run -u root --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 @@ -95,7 +95,7 @@ docker run -u root --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 diff --git a/docs/zh/usage/acceleration_cards/METAX.md b/docs/zh/usage/acceleration_cards/METAX.md index 8056d10b..ceef098a 100644 --- a/docs/zh/usage/acceleration_cards/METAX.md +++ b/docs/zh/usage/acceleration_cards/METAX.md @@ -88,7 +88,7 @@ docker run --ipc host \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 🟢 @@ -104,7 +104,7 @@ docker run --ipc host \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 🟢 @@ -120,7 +120,7 @@ docker run --ipc host \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 🟢 diff --git a/docs/zh/usage/acceleration_cards/MooreThreads.md b/docs/zh/usage/acceleration_cards/MooreThreads.md index 1015a637..31dbc8e8 100644 --- a/docs/zh/usage/acceleration_cards/MooreThreads.md +++ b/docs/zh/usage/acceleration_cards/MooreThreads.md @@ -63,7 +63,7 @@ docker run -u root --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 @@ -76,7 +76,7 @@ docker run -u root --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🔴 @@ -89,7 +89,7 @@ docker run -u root --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🔴 diff --git a/docs/zh/usage/acceleration_cards/THead.md b/docs/zh/usage/acceleration_cards/THead.md index 98e9dee0..677dde27 100644 --- a/docs/zh/usage/acceleration_cards/THead.md +++ b/docs/zh/usage/acceleration_cards/THead.md @@ -79,7 +79,7 @@ docker run --privileged=true \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 🟢 @@ -95,7 +95,7 @@ docker run --privileged=true \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 🟢 @@ -111,7 +111,7 @@ docker run --privileged=true \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 🟢 diff --git a/docs/zh/usage/acceleration_cards/Tecorigin.md b/docs/zh/usage/acceleration_cards/Tecorigin.md index 07180dce..3c44409e 100644 --- a/docs/zh/usage/acceleration_cards/Tecorigin.md +++ b/docs/zh/usage/acceleration_cards/Tecorigin.md @@ -65,7 +65,7 @@ docker run -dit --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 @@ -78,7 +78,7 @@ docker run -dit --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 @@ -91,7 +91,7 @@ docker run -dit --name mineru_docker \ 🟢 - <vlm/hybrid>-auto-engine + <vlm/hybrid>-engine 🟢 diff --git a/docs/zh/usage/acceleration_cards/VastAI.md b/docs/zh/usage/acceleration_cards/VastAI.md index 5698763d..a1938670 100644 --- a/docs/zh/usage/acceleration_cards/VastAI.md +++ b/docs/zh/usage/acceleration_cards/VastAI.md @@ -72,7 +72,7 @@ ## 4. MinerU功能 > [!NOTE] -> - `VastAI`加速卡仅支持使用`vlm-auto-engine`和`vlm-http-client`形式进行`VLM`模型推理加速 +> - `VastAI`加速卡仅支持使用`vlm-engine`和`vlm-http-client`形式进行`VLM`模型推理加速 - 进入容器 ```bash @@ -83,15 +83,15 @@ - 模型准备,参考官方介绍:[model_source.md](https://github.com/opendatalab/MinerU/blob/master/docs/zh/usage/model_source.md) - - 方式一:`vlm-auto-engine` + - 方式一:`vlm-engine` ```bash export MINERU_MODEL_SOURCE=modelscope - # step1, 以`vlm-auto-engine`方式启动MinerU解析任务 + # step1, 以`vlm-engine`方式启动MinerU解析任务 mineru -p image.png \ -o ./output \ - -b vlm-auto-engine \ + -b vlm-engine \ --http-timeout 1200 \ --tensor-parallel-size 2 \ --enforce_eager \ @@ -147,11 +147,11 @@ 🔴 - hybrid-auto-engine + hybrid-engine 🔴 - vlm-auto-engine + vlm-engine 🟢 @@ -168,11 +168,11 @@ 🔴 - hybrid-auto-engine + hybrid-engine 🔴 - vlm-auto-engine + vlm-engine 🟢 @@ -189,11 +189,11 @@ 🔴 - hybrid-auto-engine + hybrid-engine 🔴 - vlm-auto-engine + vlm-engine 🟢 @@ -212,4 +212,4 @@ > - 🟢: 支持,运行较稳定,精度与NVIDIA GPU基本一致 > - 🟡: 支持但较不稳定,在某些场景下可能出现异常,或精度存在一定差异 > - 🔴: 不支持,无法运行,或精度存在较大差异 -> - `vlm-auto-engine`:VastAI仅支持vLLM后端 \ No newline at end of file +> - `vlm-engine`:VastAI仅支持vLLM后端 \ No newline at end of file diff --git a/docs/zh/usage/cli_tools.md b/docs/zh/usage/cli_tools.md index e99d780b..0c894990 100644 --- a/docs/zh/usage/cli_tools.md +++ b/docs/zh/usage/cli_tools.md @@ -12,8 +12,8 @@ Options: -o, --output PATH 输出目录(必填) --api-url TEXT MinerU FastAPI 服务地址;不传时自动拉起本地临时 mineru-api -m, --method [auto|txt|ocr] 解析方法:auto(默认)、txt、ocr(仅用于 pipeline 与 hybrid* 后端) - -b, --backend [pipeline|hybrid-auto-engine|hybrid-http-client|vlm-auto-engine|vlm-http-client] - 解析后端(默认为 hybrid-auto-engine) + -b, --backend [pipeline|hybrid-engine|hybrid-http-client|vlm-engine|vlm-http-client] + 解析后端(默认为 hybrid-engine) -l, --lang [ch|ch_server|ch_lite|en|korean|japan|chinese_cht|ta|te|ka|th|el|latin|arabic|east_slavic|cyrillic|devanagari] 指定文档语言(可提升 OCR 准确率,仅用于 pipeline 与 hybrid* 后端) -u, --url TEXT 当使用 http-client 时,传给服务端后端的 OpenAI 兼容地址 From 742ac25101aabc12baab5b6fa62c6d72e46b8b43 Mon Sep 17 00:00:00 2001 From: myhloli Date: Fri, 5 Jun 2026 17:48:44 +0800 Subject: [PATCH 07/22] feat: implement backend validation and update backend options for parsing --- demo/demo.py | 8 +++--- mineru/cli/api_request.py | 23 +++++++++++++--- mineru/cli/backend_options.py | 52 +++++++++++++++++++++++++++++++++++ mineru/cli/client.py | 41 ++++++++++++++++----------- mineru/cli/gradio_app.py | 20 ++++++-------- 5 files changed, 108 insertions(+), 36 deletions(-) create mode 100644 mineru/cli/backend_options.py diff --git a/demo/demo.py b/demo/demo.py index f1c48a76..e83d05a3 100644 --- a/demo/demo.py +++ b/demo/demo.py @@ -95,7 +95,7 @@ async def run_demo( output_dir: str | Path, *, api_url: str | None = None, - backend: str = "hybrid-auto-engine", + backend: str = "hybrid-engine", parse_method: str = "auto", language: str = "ch", formula_enable: bool = True, @@ -212,12 +212,12 @@ def main() -> None: api_url = None # Available examples: - # "hybrid-auto-engine" -> local hybrid parsing, recommended default + # "hybrid-engine" -> local hybrid parsing, recommended default # "pipeline" -> more general OCR/text pipeline - # "vlm-auto-engine" -> local VLM parsing + # "vlm-engine" -> local VLM parsing # "vlm-http-client" -> remote OpenAI-compatible VLM server # "hybrid-http-client" -> remote OpenAI-compatible hybrid server - backend = "hybrid-auto-engine" + backend = "hybrid-engine" # Available options: # "auto" -> let MinerU choose between text extraction and OCR # "txt" -> force text extraction diff --git a/mineru/cli/api_request.py b/mineru/cli/api_request.py index 4e640a31..56780fe9 100644 --- a/mineru/cli/api_request.py +++ b/mineru/cli/api_request.py @@ -4,6 +4,11 @@ from typing import Annotated, Optional from fastapi import File, Form, HTTPException, Request, UploadFile +from mineru.cli.backend_options import ( + BACKEND_SCHEMA_EXTRA, + DEFAULT_BACKEND, + validate_backend as validate_public_backend, +) from mineru.cli.public_http_client_policy import validate_public_http_client_request ALLOWED_PARSE_METHODS = {"auto", "txt", "ocr"} @@ -51,6 +56,14 @@ def validate_parse_method(parse_method: str) -> str: return parse_method +def validate_parse_backend(backend: str) -> str: + """校验公开 API 允许的解析后端,避免旧入口名进入下游执行链路。""" + try: + return validate_public_backend(backend) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + async def parse_request_form( request: Request, files: Annotated[ @@ -89,14 +102,15 @@ async def parse_request_form( Form( description="""The backend for parsing: - pipeline: More general, supports multiple languages, hallucination-free. -- vlm-auto-engine: High accuracy via local computing power, supports Chinese and English documents only. +- vlm-engine: High accuracy via local computing power, supports Chinese and English documents only. - vlm-http-client: High accuracy via remote computing power(client suitable for openai-compatible servers), supports Chinese and English documents only. -- hybrid-auto-engine: Next-generation high accuracy solution via local computing power, supports multiple languages. -- hybrid-flash-auto-engine: Experimental hybrid-flash analyze path via local computing power, supports multiple languages. +- hybrid-engine: Next-generation high accuracy solution via local computing power, supports multiple languages. +- hybrid-flash-engine: Experimental hybrid-flash analyze path via local computing power, supports multiple languages. - hybrid-http-client: High accuracy via remote computing power but requires a little local computing power(client suitable for openai-compatible servers), supports multiple languages. - hybrid-flash-http-client: Experimental hybrid-flash analyze path via remote computing power(client suitable for openai-compatible servers), supports multiple languages.""", + json_schema_extra=BACKEND_SCHEMA_EXTRA, ), - ] = "hybrid-auto-engine", + ] = DEFAULT_BACKEND, parse_method: Annotated[ str, Form( @@ -177,6 +191,7 @@ async def parse_request_form( ] = 99999, ) -> ParseRequestOptions: """解析 API/Router 共用的 multipart 表单,并保持 Swagger 参数同源。""" + backend = validate_parse_backend(backend) validate_public_http_client_request( public_bind_exposed=bool( getattr(request.app.state, "public_bind_exposed", False) diff --git a/mineru/cli/backend_options.py b/mineru/cli/backend_options.py new file mode 100644 index 00000000..23f2d67e --- /dev/null +++ b/mineru/cli/backend_options.py @@ -0,0 +1,52 @@ +# Copyright (c) Opendatalab. All rights reserved. + +BACKEND_PIPELINE = "pipeline" +BACKEND_VLM_ENGINE = "vlm-engine" +BACKEND_HYBRID_ENGINE = "hybrid-engine" +BACKEND_HYBRID_FLASH_ENGINE = "hybrid-flash-engine" +BACKEND_VLM_HTTP_CLIENT = "vlm-http-client" +BACKEND_HYBRID_HTTP_CLIENT = "hybrid-http-client" +BACKEND_HYBRID_FLASH_HTTP_CLIENT = "hybrid-flash-http-client" + +DEFAULT_BACKEND = BACKEND_HYBRID_ENGINE + +LOCAL_BACKEND_CHOICES = ( + BACKEND_PIPELINE, + BACKEND_VLM_ENGINE, + BACKEND_HYBRID_ENGINE, + BACKEND_HYBRID_FLASH_ENGINE, +) +HTTP_CLIENT_BACKEND_CHOICES = ( + BACKEND_VLM_HTTP_CLIENT, + BACKEND_HYBRID_HTTP_CLIENT, + BACKEND_HYBRID_FLASH_HTTP_CLIENT, +) +PUBLIC_BACKEND_CHOICES = LOCAL_BACKEND_CHOICES + HTTP_CLIENT_BACKEND_CHOICES +BACKEND_SCHEMA_EXTRA = {"enum": list(PUBLIC_BACKEND_CHOICES)} +LEGACY_BACKEND_ALIASES = { + "vlm-auto-engine": BACKEND_VLM_ENGINE, + "hybrid-auto-engine": BACKEND_HYBRID_ENGINE, + "hybrid-flash-auto-engine": BACKEND_HYBRID_FLASH_ENGINE, +} + + +def get_backend_choices(include_http_client: bool = True) -> list[str]: + """按入口配置返回公开 backend 选项,避免各入口重复维护字符串列表。""" + choices = list(LOCAL_BACKEND_CHOICES) + if include_http_client: + choices.extend(HTTP_CLIENT_BACKEND_CHOICES) + return choices + + +def normalize_backend(backend: str) -> str: + """将旧 backend 别名规范为当前公开名称,并校验最终名称是否合法。""" + normalized_backend = LEGACY_BACKEND_ALIASES.get(backend, backend) + if normalized_backend not in PUBLIC_BACKEND_CHOICES: + allowed_values = ", ".join(PUBLIC_BACKEND_CHOICES) + raise ValueError(f"Invalid backend. Allowed values: {allowed_values}") + return normalized_backend + + +def validate_backend(backend: str) -> str: + """校验公开入口允许的 backend 名称,并返回规范后的后端名称。""" + return normalize_backend(backend) diff --git a/mineru/cli/client.py b/mineru/cli/client.py index 05021446..4659d153 100644 --- a/mineru/cli/client.py +++ b/mineru/cli/client.py @@ -18,6 +18,11 @@ from mineru.cli.api_protocol import ( DEFAULT_MAX_CONCURRENT_REQUESTS, DEFAULT_PROCESSING_WINDOW_SIZE, ) +from mineru.cli.backend_options import ( + DEFAULT_BACKEND, + PUBLIC_BACKEND_CHOICES, + normalize_backend, +) from mineru.utils.config_reader import ( get_max_concurrent_requests as read_max_concurrent_requests, ) @@ -85,6 +90,18 @@ SubmitResponse = _api_client.SubmitResponse LocalAPIServer = _api_client.LocalAPIServer +def normalize_backend_option( + ctx: click.Context, + param: click.Parameter, + value: str, +) -> str: + """将 CLI 输入的旧 backend 名称规范为当前公开名称。""" + try: + return normalize_backend(value) + except ValueError as exc: + raise click.BadParameter(str(exc), ctx=ctx, param=param) from exc + + @dataclass(frozen=True) class TaskFailure: task_index: int @@ -1029,28 +1046,20 @@ async def run_orchestrated_cli( "-b", "--backend", "backend", - type=click.Choice( - [ - "pipeline", - "vlm-http-client", - "hybrid-http-client", - "hybrid-flash-http-client", - "vlm-auto-engine", - "hybrid-auto-engine", - "hybrid-flash-auto-engine", - ] - ), - default="hybrid-auto-engine", + type=str, + default=DEFAULT_BACKEND, + callback=normalize_backend_option, + metavar="[" + "|".join(PUBLIC_BACKEND_CHOICES) + "]", help="""\b the backend for parsing pdf: pipeline: More general. - vlm-auto-engine: High accuracy via local computing power. + vlm-engine: High accuracy via local computing power. vlm-http-client: High accuracy via remote computing power(client suitable for openai-compatible servers). - hybrid-auto-engine: Next-generation high accuracy solution via local computing power. - hybrid-flash-auto-engine: Experimental hybrid-flash analyze path via local computing power. + hybrid-engine: Next-generation high accuracy solution via local computing power. + hybrid-flash-engine: Experimental hybrid-flash analyze path via local computing power. hybrid-http-client: High accuracy but requires a little local computing power(client suitable for openai-compatible servers). hybrid-flash-http-client: Experimental hybrid-flash analyze path via remote computing power(client suitable for openai-compatible servers). - Without method specified, hybrid-auto-engine will be used by default.""", + Without method specified, hybrid-engine will be used by default.""", ) @click.option( "-l", diff --git a/mineru/cli/gradio_app.py b/mineru/cli/gradio_app.py index 04c4b068..aa73b759 100644 --- a/mineru/cli/gradio_app.py +++ b/mineru/cli/gradio_app.py @@ -38,6 +38,11 @@ from mineru.cli.common import ( read_fn, ) from mineru.cli import api_client as _api_client +from mineru.cli.backend_options import ( + DEFAULT_BACKEND, + HTTP_CLIENT_BACKEND_CHOICES, + LOCAL_BACKEND_CHOICES, +) from mineru.cli.client_side_output import regenerate_client_side_outputs from mineru.cli.output_paths import resolve_parse_dir from mineru.cli.vlm_preload import resolve_gradio_local_api_cli_args @@ -224,17 +229,8 @@ STATUS_QUEUED_ON_SERVER = "Queued on server" STATUS_PROCESSING_ON_SERVER = "Processing on server" STATUS_QUEUED_LOCALLY_PREFIX = "Queued locally:" -BACKEND_CHOICE_DEFINITIONS = [ - "pipeline", - "vlm-auto-engine", - "hybrid-auto-engine", - "hybrid-flash-auto-engine", -] -HTTP_CLIENT_BACKEND_CHOICE_DEFINITIONS = [ - "vlm-http-client", - "hybrid-http-client", - "hybrid-flash-http-client", -] +BACKEND_CHOICE_DEFINITIONS = list(LOCAL_BACKEND_CHOICES) +HTTP_CLIENT_BACKEND_CHOICE_DEFINITIONS = list(HTTP_CLIENT_BACKEND_CHOICES) STATUS_STEP_DEFINITIONS = [ ("status_step_prepare", STATUS_PREPARING_REQUEST), ("status_step_check", STATUS_CHECKING_SERVER), @@ -1778,7 +1774,7 @@ def main(ctx, file_types=suffixes, elem_classes=["mineru-upload-file"], ) - preferred_option = "hybrid-auto-engine" + preferred_option = DEFAULT_BACKEND backend = gr.Dropdown( build_backend_choices(http_client_enable, i18n), label=i18n("backend"), From d4e4ad67d5e26239cd4aa606dbc06e4ce43e4d52 Mon Sep 17 00:00:00 2001 From: myhloli Date: Fri, 5 Jun 2026 19:34:19 +0800 Subject: [PATCH 08/22] feat: add hybrid-flash processing functions for synchronous and asynchronous analyze stages --- .../hybrid_flash/hybrid_flash_analyze.py | 4 + mineru/cli/common.py | 188 +++++++++++++++--- 2 files changed, 162 insertions(+), 30 deletions(-) diff --git a/mineru/backend/hybrid_flash/hybrid_flash_analyze.py b/mineru/backend/hybrid_flash/hybrid_flash_analyze.py index 212a7c24..3710c606 100644 --- a/mineru/backend/hybrid_flash/hybrid_flash_analyze.py +++ b/mineru/backend/hybrid_flash/hybrid_flash_analyze.py @@ -297,6 +297,8 @@ def doc_analyze( **kwargs, ): """hybrid-flash第一阶段analyze:返回pipeline形态model_list和backend元信息。""" + # hybrid-flash 当前只执行 analyze,客户端输出开关属于最终输出阶段,不能下传到模型初始化。 + kwargs.pop("client_side_output_generation", None) if predictor is None: predictor = ModelSingleton().get_model(backend, model_path, server_url, **kwargs) predictor = _maybe_enable_serial_execution(predictor, backend) @@ -425,6 +427,8 @@ async def aio_doc_analyze( **kwargs, ): """异步hybrid-flash analyze入口,返回pipeline形态model_list和backend元信息。""" + # hybrid-flash 当前只执行 analyze,客户端输出开关属于最终输出阶段,不能下传到模型初始化。 + kwargs.pop("client_side_output_generation", None) if predictor is None: predictor = await _get_model_async(backend, model_path, server_url, **kwargs) predictor = _maybe_enable_serial_execution(predictor, backend) diff --git a/mineru/cli/common.py b/mineru/cli/common.py index f029dd76..130c281a 100644 --- a/mineru/cli/common.py +++ b/mineru/cli/common.py @@ -10,6 +10,7 @@ from typing import Sequence from loguru import logger +from mineru.cli.backend_options import normalize_backend from mineru.data.data_reader_writer import FileBasedDataWriter from mineru.utils.draw_bbox import draw_layout_bbox, draw_span_bbox from mineru.utils.engine_utils import get_vlm_engine @@ -69,9 +70,15 @@ def ensure_backend_dependencies(backend: str) -> None: def _load_hybrid_analyze_entrypoint(entrypoint_name: str, backend: str): + """按 hybrid 后端家族加载普通 hybrid 或 hybrid-flash 的 analyze 入口。""" ensure_backend_dependencies(backend) + module_name = ( + "mineru.backend.hybrid_flash.hybrid_flash_analyze" + if backend.startswith("hybrid-flash-") + else "mineru.backend.hybrid.hybrid_analyze" + ) try: - hybrid_analyze = importlib.import_module("mineru.backend.hybrid.hybrid_analyze") + hybrid_analyze = importlib.import_module(module_name) except (ImportError, ModuleNotFoundError) as exc: raise HybridDependencyError( build_hybrid_dependency_error_message(backend) @@ -498,6 +505,112 @@ def _process_vlm( ) +def _process_hybrid_flash( + output_dir, + pdf_file_names, + pdf_bytes_list, + h_lang_list, + parse_method, + inline_formula_enable, + backend, + f_draw_layout_bbox, + f_draw_span_bbox, + f_dump_md, + f_dump_middle_json, + f_dump_model_output, + f_dump_orig_pdf, + f_dump_content_list, + f_make_md_mode, + table_enable, + server_url=None, + image_analysis=True, + **kwargs, +): + """同步运行 hybrid-flash analyze 阶段,当前只产出 analyze 调试结果。""" + hybrid_flash_doc_analyze = _load_hybrid_analyze_entrypoint( + "doc_analyze", + f"hybrid-flash-{backend}", + ) + + if not backend.endswith("client"): + server_url = None + + for idx, (pdf_bytes, lang) in enumerate(zip(pdf_bytes_list, h_lang_list)): + pdf_file_name = pdf_file_names[idx] + local_image_dir, _local_md_dir = prepare_env( + output_dir, + pdf_file_name, + f"hybrid_{parse_method}", + ) + image_writer = FileBasedDataWriter(local_image_dir) + + hybrid_flash_doc_analyze( + pdf_bytes=pdf_bytes, + image_writer=image_writer, + backend=backend, + parse_method=parse_method, + language=lang, + inline_formula_enable=inline_formula_enable, + table_enable=table_enable, + server_url=server_url, + image_analysis=image_analysis, + **kwargs, + ) + + +async def _async_process_hybrid_flash( + output_dir, + pdf_file_names, + pdf_bytes_list, + h_lang_list, + parse_method, + inline_formula_enable, + backend, + f_draw_layout_bbox, + f_draw_span_bbox, + f_dump_md, + f_dump_middle_json, + f_dump_model_output, + f_dump_orig_pdf, + f_dump_content_list, + f_make_md_mode, + table_enable, + server_url=None, + image_analysis=True, + **kwargs, +): + """异步运行 hybrid-flash analyze 阶段,当前只产出 analyze 调试结果。""" + aio_hybrid_flash_doc_analyze = _load_hybrid_analyze_entrypoint( + "aio_doc_analyze", + f"hybrid-flash-{backend}", + ) + + if not backend.endswith("client"): + server_url = None + + for idx, (pdf_bytes, lang) in enumerate(zip(pdf_bytes_list, h_lang_list)): + pdf_file_name = pdf_file_names[idx] + local_image_dir, _local_md_dir = prepare_env( + output_dir, + pdf_file_name, + f"hybrid_{parse_method}", + ) + image_writer = FileBasedDataWriter(local_image_dir) + + await aio_hybrid_flash_doc_analyze( + pdf_bytes=pdf_bytes, + image_writer=image_writer, + backend=backend, + parse_method=parse_method, + language=lang, + inline_formula_enable=inline_formula_enable, + table_enable=table_enable, + server_url=server_url, + image_analysis=image_analysis, + **kwargs, + ) + + def _process_hybrid( output_dir, pdf_file_names, @@ -684,6 +797,7 @@ def do_parse( client_side_output_generation=False, **kwargs, ): + backend = normalize_backend(backend) need_remove_index = _process_office_doc( output_dir, pdf_file_names=pdf_file_names, @@ -718,10 +832,7 @@ def do_parse( if backend.startswith("vlm-"): backend = backend[4:] - if backend == "vllm-async-engine": - raise Exception("vlm-vllm-async-engine backend is not supported in sync mode, please use vlm-vllm-engine backend") - - if backend == "auto-engine": + if backend == "engine": backend = get_vlm_engine(inference_engine='auto', is_async=False) os.environ['MINERU_VLM_FORMULA_ENABLE'] = str(formula_enable) @@ -737,24 +848,33 @@ def do_parse( elif backend.startswith("hybrid-"): ensure_backend_dependencies(backend) backend = backend[7:] + is_flash = backend.startswith("flash-") - if backend == "vllm-async-engine": - raise Exception( - "hybrid-vllm-async-engine backend is not supported in sync mode, please use hybrid-vllm-engine backend") + if is_flash: + backend = backend[6:] - if backend == "auto-engine": + if backend == "engine": backend = get_vlm_engine(inference_engine='auto', is_async=False) os.environ['MINERU_VLM_TABLE_ENABLE'] = str(table_enable) os.environ['MINERU_VLM_FORMULA_ENABLE'] = "true" - _process_hybrid( - output_dir, pdf_file_names, pdf_bytes_list, p_lang_list, parse_method, formula_enable, backend, - f_draw_layout_bbox, f_draw_span_bbox, f_dump_md, f_dump_middle_json, - f_dump_model_output, f_dump_orig_pdf, f_dump_content_list, f_make_md_mode, - server_url, image_analysis=image_analysis, - client_side_output_generation=client_side_output_generation, **kwargs, - ) + if is_flash: + _process_hybrid_flash( + output_dir, pdf_file_names, pdf_bytes_list, p_lang_list, parse_method, formula_enable, backend, + f_draw_layout_bbox, f_draw_span_bbox, f_dump_md, f_dump_middle_json, + f_dump_model_output, f_dump_orig_pdf, f_dump_content_list, f_make_md_mode, + table_enable, server_url, image_analysis=image_analysis, + client_side_output_generation=client_side_output_generation, **kwargs, + ) + else: + _process_hybrid( + output_dir, pdf_file_names, pdf_bytes_list, p_lang_list, parse_method, formula_enable, backend, + f_draw_layout_bbox, f_draw_span_bbox, f_dump_md, f_dump_middle_json, + f_dump_model_output, f_dump_orig_pdf, f_dump_content_list, f_make_md_mode, + server_url, image_analysis=image_analysis, + client_side_output_generation=client_side_output_generation, **kwargs, + ) async def aio_do_parse( @@ -781,6 +901,7 @@ async def aio_do_parse( client_side_output_generation=False, **kwargs, ): + backend = normalize_backend(backend) # Office 解析是同步且可能耗时的操作,异步入口需要放到线程中避免阻塞事件循环。 need_remove_index = await asyncio.to_thread( _process_office_doc, @@ -818,10 +939,7 @@ async def aio_do_parse( if backend.startswith("vlm-"): backend = backend[4:] - if backend == "vllm-engine": - raise Exception("vlm-vllm-engine backend is not supported in async mode, please use vlm-vllm-async-engine backend") - - if backend == "auto-engine": + if backend == "engine": backend = get_vlm_engine(inference_engine='auto', is_async=True) os.environ['MINERU_VLM_FORMULA_ENABLE'] = str(formula_enable) @@ -837,23 +955,33 @@ async def aio_do_parse( elif backend.startswith("hybrid-"): ensure_backend_dependencies(backend) backend = backend[7:] + is_flash = backend.startswith("flash-") - if backend == "vllm-engine": - raise Exception("hybrid-vllm-engine backend is not supported in async mode, please use hybrid-vllm-async-engine backend") + if is_flash: + backend = backend[6:] - if backend == "auto-engine": + if backend == "engine": backend = get_vlm_engine(inference_engine='auto', is_async=True) os.environ['MINERU_VLM_TABLE_ENABLE'] = str(table_enable) os.environ['MINERU_VLM_FORMULA_ENABLE'] = "true" - await _async_process_hybrid( - output_dir, pdf_file_names, pdf_bytes_list, p_lang_list, parse_method, formula_enable, backend, - f_draw_layout_bbox, f_draw_span_bbox, f_dump_md, f_dump_middle_json, - f_dump_model_output, f_dump_orig_pdf, f_dump_content_list, f_make_md_mode, - server_url, image_analysis=image_analysis, - client_side_output_generation=client_side_output_generation, **kwargs, - ) + if is_flash: + await _async_process_hybrid_flash( + output_dir, pdf_file_names, pdf_bytes_list, p_lang_list, parse_method, formula_enable, backend, + f_draw_layout_bbox, f_draw_span_bbox, f_dump_md, f_dump_middle_json, + f_dump_model_output, f_dump_orig_pdf, f_dump_content_list, f_make_md_mode, + table_enable, server_url, image_analysis=image_analysis, + client_side_output_generation=client_side_output_generation, **kwargs, + ) + else: + await _async_process_hybrid( + output_dir, pdf_file_names, pdf_bytes_list, p_lang_list, parse_method, formula_enable, backend, + f_draw_layout_bbox, f_draw_span_bbox, f_dump_md, f_dump_middle_json, + f_dump_model_output, f_dump_orig_pdf, f_dump_content_list, f_make_md_mode, + server_url, image_analysis=image_analysis, + client_side_output_generation=client_side_output_generation, **kwargs, + ) if __name__ == "__main__": From ca5424387bad43bbb6956cd5bbc76739301397e1 Mon Sep 17 00:00:00 2001 From: myhloli Date: Fri, 5 Jun 2026 22:43:03 +0800 Subject: [PATCH 09/22] feat: enhance hybrid analysis with flash mode support and layout processing --- mineru/backend/hybrid/hybrid_analyze.py | 278 +++++++++++++++++++----- 1 file changed, 223 insertions(+), 55 deletions(-) diff --git a/mineru/backend/hybrid/hybrid_analyze.py b/mineru/backend/hybrid/hybrid_analyze.py index 89679ed2..46cf05ed 100644 --- a/mineru/backend/hybrid/hybrid_analyze.py +++ b/mineru/backend/hybrid/hybrid_analyze.py @@ -9,7 +9,7 @@ import numpy as np import pypdfium2 as pdfium from loguru import logger from mineru_vl_utils import MinerUClient -from mineru_vl_utils.structs import BlockType +from mineru_vl_utils.structs import BlockType, ContentBlock from tqdm import tqdm from mineru.backend.hybrid.hybrid_model_output_to_middle_json import ( @@ -59,6 +59,44 @@ LAYOUT_TITLE_SPLIT_OVERLAP_THRESHOLD = 0.8 not_extract_list = [item.value for item in NotExtractType] HYBRID_OCR_DET_TEXT_TYPES = set(not_extract_list) +HYBRID_ANALYZE_MODES = {"pro", "flash"} +FLASH_LAYOUT_VISUAL_LABELS = {"image", "chart", "seal"} +FLASH_LAYOUT_LABEL_TO_VLM_TYPE = { + "abstract": BlockType.TEXT, + "algorithm": BlockType.CODE, + "aside_text": BlockType.ASIDE_TEXT, + "content": BlockType.TEXT, + "doc_title": BlockType.TITLE, + "footer": BlockType.FOOTER, + "footer_image": BlockType.FOOTER, + "footnote": BlockType.PAGE_FOOTNOTE, + "formula_number": BlockType.TEXT, + "header": BlockType.HEADER, + "header_image": BlockType.HEADER, + "number": BlockType.PAGE_NUMBER, + "paragraph_title": BlockType.TITLE, + "reference_content": BlockType.REF_TEXT, + "text": BlockType.TEXT, + "vertical_text": BlockType.TEXT, + "figure_title": BlockType.IMAGE_CAPTION, + "vision_footnote": BlockType.IMAGE_FOOTNOTE, + "table": BlockType.TABLE, + "display_formula": BlockType.EQUATION, +} + + +def _validate_hybrid_mode(mode: str) -> str: + """校验 Hybrid 运行模式,避免静默走错解析分支。""" + if mode not in HYBRID_ANALYZE_MODES: + raise ValueError('mode must be "pro" or "flash"') + return mode + + +def _vlm_type_for_flash_layout_label(label: str | None) -> str | None: + """将 pipeline layout 标签映射为 mineru-vl-utils 支持的 VLM 抽取类型。""" + if label in FLASH_LAYOUT_VISUAL_LABELS: + return BlockType.IMAGE + return FLASH_LAYOUT_LABEL_TO_VLM_TYPE.get(label) def _is_hybrid_ocr_det_candidate(block): @@ -281,6 +319,42 @@ def normalize_bbox_to_unit(item, page_width, page_height): return True +def _layout_det_bbox_to_unit(layout_det, page_width, page_height): + """复制并归一化 layout bbox,避免构造 VLM 输入时改动 pipeline 原始结果。""" + bbox = layout_det.get("bbox") + if bbox is None or len(bbox) != 4: + return None + bbox_item = {"bbox": list(bbox)} + if not normalize_bbox_to_unit(bbox_item, page_width, page_height): + return None + return bbox_item["bbox"] + + +def _build_flash_vlm_layout_blocks(layout_dets, page_width, page_height): + """用 pipeline layout 构造 VLM 外部 layout 输入,跳过 VLM 自身 layout 解析。""" + blocks = [] + for layout_det in layout_dets or []: + label = layout_det.get("label") + vlm_type = _vlm_type_for_flash_layout_label(label) + if vlm_type is None: + continue + bbox = _layout_det_bbox_to_unit(layout_det, page_width, page_height) + if bbox is None: + continue + try: + block = ContentBlock( + vlm_type, + bbox, + angle=layout_det.get("angle", 0), + content=layout_det.get("content"), + ) + except AssertionError as exc: + logger.warning(f"Skip invalid Hybrid flash VLM block: {layout_det}, error: {exc}") + continue + blocks.append(block) + return blocks + + def _formula_item_to_pixel_bbox(item): bbox = item.get('bbox') if bbox is not None and len(bbox) == 4: @@ -294,7 +368,7 @@ def _build_inline_formula_inputs(images_layout_res): for layout_res in images_layout_res: page_inline_formula_inputs = [] for res in layout_res: - if res.get('label') not in ['inline_formula', 'display_formula']: + if res.get('label') != 'inline_formula': continue bbox = res.get('bbox') if bbox is None or len(bbox) != 4: @@ -433,13 +507,36 @@ def _predict_layout_for_title_split( ) +def _predict_layout_for_window( + images_pil_list, + language, + inline_formula_enable, + batch_ratio, + vlm_ocr_enable, +): + """为单个处理窗口执行一次 pipeline layout,并返回可复用的小模型实例。""" + hybrid_model_singleton = HybridModelSingleton() + hybrid_pipeline_model = hybrid_model_singleton.get_model( + lang=language, + formula_enable=inline_formula_enable and not vlm_ocr_enable, + ) + images_layout_res = _predict_layout_for_title_split( + hybrid_pipeline_model, + images_pil_list, + batch_ratio, + ) + return images_layout_res, hybrid_pipeline_model + + def _process_ocr_and_formulas( images_pil_list, model_list, - language, inline_formula_enable, _ocr_enable, batch_ratio: int = 1, + *, + images_layout_res, + hybrid_pipeline_model, ): """处理OCR和公式识别""" @@ -450,21 +547,6 @@ def _process_ocr_and_formulas( # 将PIL图片转换为numpy数组 np_images = [np.asarray(pil_image).copy() for pil_image in images_pil_list] - # 获取混合模型实例 - hybrid_model_singleton = HybridModelSingleton() - hybrid_pipeline_model = hybrid_model_singleton.get_model( - lang=language, - formula_enable=inline_formula_enable, - ) - - # 在进行`行内`公式检测和识别前,先将图像中的图片、表格、`行间`公式区域mask掉 - layout_images = mask_image_regions(np_images, model_list) if inline_formula_enable else np_images - images_layout_res = _predict_layout_for_title_split( - hybrid_pipeline_model, - layout_images, - batch_ratio, - ) - if inline_formula_enable: images_mfd_res = _build_inline_formula_inputs(images_layout_res) # 公式识别 @@ -560,38 +642,24 @@ def _process_ocr_and_formulas( if need_ocr_res in page_ocr_res_list: page_ocr_res_list.remove(need_ocr_res) - _apply_layout_title_split( - model_list, - images_layout_res, - [_normalize_page_size(image) for image in images_pil_list], - ) - _normalize_bbox(inline_formula_list, ocr_res_list, images_pil_list) merged_model_list = _merge_page_sidecar_items( model_list, inline_formula_list, ocr_res_list, ) - return merged_model_list, hybrid_pipeline_model + return merged_model_list -def _apply_layout_title_split_for_window( +def _apply_vlm_ocr_det_sidecars_for_window( images_pil_list, model_list, - language, batch_ratio, + *, + images_layout_res, + hybrid_pipeline_model, ): - """为VLM-OCR路径补跑layout小模型,先基于VLM原始title做OCR det,再拆分标题。""" - hybrid_model_singleton = HybridModelSingleton() - hybrid_pipeline_model = hybrid_model_singleton.get_model( - lang=language, - formula_enable=False, - ) - images_layout_res = _predict_layout_for_title_split( - hybrid_pipeline_model, - images_pil_list, - batch_ratio, - ) + """为VLM-OCR路径追加OCR det空文本行和行内公式框sidecar。""" formula_mask_inputs = _build_formula_mask_inputs(images_layout_res) inline_formula_list = _build_inline_formula_det_inputs(images_layout_res) np_images = [np.asarray(pil_image).copy() for pil_image in images_pil_list] @@ -611,12 +679,6 @@ def _apply_layout_title_split_for_window( ocr_res_list, keep_ocr_text=False, ) - _apply_layout_title_split( - model_list, - images_layout_res, - [_normalize_page_size(image) for image in images_pil_list], - ) - return hybrid_pipeline_model def _normalize_bbox( @@ -764,8 +826,10 @@ def doc_analyze( model_path: str | None = None, server_url: str | None = None, image_analysis: bool = True, + mode: str = "pro", **kwargs, ): + mode = _validate_hybrid_mode(mode) client_side_output_generation = bool( kwargs.pop("client_side_output_generation", False) ) @@ -813,22 +877,65 @@ def doc_analyze( ) try: images_pil_list = [image_dict["img_pil"] for image_dict in images_list] + page_sizes = [_normalize_page_size(image) for image in images_pil_list] logger.info( f'Hybrid processing window {window_index + 1}/{total_windows}: ' f'pages {window_start + 1}-{window_end + 1}/{page_count} ' f'({len(images_pil_list)} pages)' ) - if _vlm_ocr_enable: + images_layout_res, hybrid_pipeline_model = _predict_layout_for_window( + images_pil_list, + language, + inline_formula_enable, + batch_ratio, + _vlm_ocr_enable, + ) + if mode == "flash": + vlm_blocks_list = [ + _build_flash_vlm_layout_blocks( + page_layout_res, + pil_img.width, + pil_img.height, + ) + for page_layout_res, pil_img in zip(images_layout_res, images_pil_list) + ] + with predictor_execution_guard(predictor): + window_model_list = predictor.batch_extract_with_layout( + images_pil_list, + vlm_blocks_list, + not_extract_list=None if _vlm_ocr_enable else not_extract_list, + image_analysis=image_analysis, + ) + if _vlm_ocr_enable: + _apply_vlm_ocr_det_sidecars_for_window( + images_pil_list, + window_model_list, + batch_ratio, + images_layout_res=images_layout_res, + hybrid_pipeline_model=hybrid_pipeline_model, + ) + else: + window_model_list = _process_ocr_and_formulas( + images_pil_list, + window_model_list, + inline_formula_enable, + _ocr_enable, + batch_ratio=batch_ratio, + images_layout_res=images_layout_res, + hybrid_pipeline_model=hybrid_pipeline_model, + ) + elif _vlm_ocr_enable: with predictor_execution_guard(predictor): window_model_list = predictor.batch_two_step_extract( images=images_pil_list, image_analysis=image_analysis, ) - hybrid_pipeline_model = _apply_layout_title_split_for_window( + _apply_vlm_ocr_det_sidecars_for_window( images_pil_list, window_model_list, - language, batch_ratio, + images_layout_res=images_layout_res, + hybrid_pipeline_model=hybrid_pipeline_model, ) else: with predictor_execution_guard(predictor): @@ -837,15 +944,21 @@ def doc_analyze( not_extract_list=not_extract_list, image_analysis=image_analysis, ) - window_model_list, hybrid_pipeline_model = _process_ocr_and_formulas( + window_model_list = _process_ocr_and_formulas( images_pil_list, window_model_list, - language, inline_formula_enable, _ocr_enable, batch_ratio=batch_ratio, + images_layout_res=images_layout_res, + hybrid_pipeline_model=hybrid_pipeline_model, ) + _apply_layout_title_split( + window_model_list, + images_layout_res, + page_sizes, + ) model_list.extend(window_model_list) if progress_bar is None: progress_bar = tqdm(total=page_count, desc="Processing pages") @@ -914,8 +1027,10 @@ async def aio_doc_analyze( model_path: str | None = None, server_url: str | None = None, image_analysis: bool = True, + mode: str = "pro", **kwargs, ): + mode = _validate_hybrid_mode(mode) client_side_output_generation = bool( kwargs.pop("client_side_output_generation", False) ) @@ -962,23 +1077,69 @@ async def aio_doc_analyze( ) try: images_pil_list = [image_dict["img_pil"] for image_dict in images_list] + page_sizes = [_normalize_page_size(image) for image in images_pil_list] logger.info( f'Hybrid processing window {window_index + 1}/{total_windows}: ' f'pages {window_start + 1}-{window_end + 1}/{page_count} ' f'({len(images_pil_list)} pages)' ) - if _vlm_ocr_enable: + images_layout_res, hybrid_pipeline_model = await asyncio.to_thread( + _predict_layout_for_window, + images_pil_list, + language, + inline_formula_enable, + batch_ratio, + _vlm_ocr_enable, + ) + if mode == "flash": + vlm_blocks_list = [ + _build_flash_vlm_layout_blocks( + page_layout_res, + pil_img.width, + pil_img.height, + ) + for page_layout_res, pil_img in zip(images_layout_res, images_pil_list) + ] + async with aio_predictor_execution_guard(predictor): + window_model_list = await predictor.aio_batch_extract_with_layout( + images_pil_list, + vlm_blocks_list, + not_extract_list=None if _vlm_ocr_enable else not_extract_list, + image_analysis=image_analysis, + ) + if _vlm_ocr_enable: + await asyncio.to_thread( + _apply_vlm_ocr_det_sidecars_for_window, + images_pil_list, + window_model_list, + batch_ratio, + images_layout_res=images_layout_res, + hybrid_pipeline_model=hybrid_pipeline_model, + ) + else: + window_model_list = await asyncio.to_thread( + _process_ocr_and_formulas, + images_pil_list, + window_model_list, + inline_formula_enable, + _ocr_enable, + batch_ratio=batch_ratio, + images_layout_res=images_layout_res, + hybrid_pipeline_model=hybrid_pipeline_model, + ) + elif _vlm_ocr_enable: async with aio_predictor_execution_guard(predictor): window_model_list = await predictor.aio_batch_two_step_extract( images=images_pil_list, image_analysis=image_analysis, ) - hybrid_pipeline_model = await asyncio.to_thread( - _apply_layout_title_split_for_window, + await asyncio.to_thread( + _apply_vlm_ocr_det_sidecars_for_window, images_pil_list, window_model_list, - language, batch_ratio, + images_layout_res=images_layout_res, + hybrid_pipeline_model=hybrid_pipeline_model, ) else: async with aio_predictor_execution_guard(predictor): @@ -987,16 +1148,23 @@ async def aio_doc_analyze( not_extract_list=not_extract_list, image_analysis=image_analysis, ) - window_model_list, hybrid_pipeline_model = await asyncio.to_thread( + window_model_list = await asyncio.to_thread( _process_ocr_and_formulas, images_pil_list, window_model_list, - language, inline_formula_enable, _ocr_enable, batch_ratio=batch_ratio, + images_layout_res=images_layout_res, + hybrid_pipeline_model=hybrid_pipeline_model, ) + await asyncio.to_thread( + _apply_layout_title_split, + window_model_list, + images_layout_res, + page_sizes, + ) model_list.extend(window_model_list) if progress_bar is None: progress_bar = tqdm(total=page_count, desc="Processing pages") From d62b51bc34f560b4a464f2d92f580ffa43125e0e Mon Sep 17 00:00:00 2001 From: myhloli Date: Fri, 5 Jun 2026 23:00:20 +0800 Subject: [PATCH 10/22] feat: update hybrid-flash engine descriptions and refactor processing functions for mode control --- mineru/backend/hybrid_flash/__init__.py | 1 - .../hybrid_flash/hybrid_flash_analyze.py | 499 ------------------ mineru/cli/api_request.py | 4 +- mineru/cli/client.py | 4 +- mineru/cli/common.py | 172 +----- 5 files changed, 28 insertions(+), 652 deletions(-) delete mode 100644 mineru/backend/hybrid_flash/__init__.py delete mode 100644 mineru/backend/hybrid_flash/hybrid_flash_analyze.py diff --git a/mineru/backend/hybrid_flash/__init__.py b/mineru/backend/hybrid_flash/__init__.py deleted file mode 100644 index 1e17167c..00000000 --- a/mineru/backend/hybrid_flash/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Copyright (c) Opendatalab. All rights reserved. diff --git a/mineru/backend/hybrid_flash/hybrid_flash_analyze.py b/mineru/backend/hybrid_flash/hybrid_flash_analyze.py deleted file mode 100644 index 3710c606..00000000 --- a/mineru/backend/hybrid_flash/hybrid_flash_analyze.py +++ /dev/null @@ -1,499 +0,0 @@ -# Copyright (c) Opendatalab. All rights reserved. -import os -import time - -import pypdfium2 as pdfium -from loguru import logger -from mineru_vl_utils import MinerUClient -from mineru_vl_utils.structs import ContentBlock -from tqdm import tqdm - -from mineru.backend.utils.runtime_utils import exclude_progress_bar_idle_time -from mineru.backend.vlm.vlm_analyze import ( - ModelSingleton, - _get_model_async, - _maybe_enable_serial_execution, - aio_predictor_execution_guard, - predictor_execution_guard, -) -from mineru.data.data_reader_writer import DataWriter -from mineru.utils.config_reader import get_device, get_processing_window_size -from mineru.utils.enum_class import ImageType -from mineru.utils.model_utils import clean_memory -from mineru.utils.pdf_classify import classify -from mineru.utils.pdf_image_tools import ( - aio_load_images_from_pdf_bytes_range, - load_images_from_pdf_doc, -) -from mineru.utils.pdfium_guard import ( - close_pdfium_document, - get_pdfium_document_page_count, - open_pdfium_document, -) -from mineru.version import __version__ - - -VLM_VISUAL_LABELS = {"image", "chart", "seal"} -VLM_TEXT_LABEL_TO_TYPE = { - "abstract": "text", - "algorithm": "code", - "aside_text": "aside_text", - "content": "text", - "doc_title": "title", - "footer": "footer", - "footer_image": "footer", - "footnote": "page_footnote", - "formula_number": "text", - "header": "header", - "header_image": "header", - "number": "page_number", - "paragraph_title": "title", - "reference_content": "ref_text", - "text": "text", - "vertical_text": "text", - "figure_title": "image_caption", - "vision_footnote": "image_footnote", -} - - -def batch_image_analyze(*args, **kwargs): - """懒加载pipeline analyze,避免导入hybrid-flash模块时提前要求torch环境。""" - from mineru.backend.pipeline.pipeline_analyze import batch_image_analyze as pipeline_batch_image_analyze - - return pipeline_batch_image_analyze(*args, **kwargs) - - -def _get_ocr_enable(pdf_bytes, parse_method: str) -> bool: - """根据parse_method解析OCR开关,保持和pipeline/hybrid入口一致。""" - if parse_method == "auto": - return classify(pdf_bytes) == "ocr" - if parse_method == "ocr": - return True - return False - - -def _should_enable_vlm_ocr(ocr_enable: bool, language: str, inline_formula_enable: bool) -> bool: - """判断是否让VLM抽取文本内容,OCR-det本身不受该开关影响。""" - force_enable = os.getenv("MINERU_FORCE_VLM_OCR_ENABLE", "0").lower() in ("1", "true", "yes") - if force_enable: - return True - - force_pipeline = os.getenv("MINERU_HYBRID_FORCE_PIPELINE_ENABLE", "0").lower() in ("1", "true", "yes") - return ( - ocr_enable - and language in ["ch", "en"] - and inline_formula_enable - and not force_pipeline - ) - - -def _close_images(images_list): - """关闭窗口内PIL图片,避免长文档处理时文件句柄或内存累积。""" - for image_dict in images_list or []: - pil_img = image_dict.get("img_pil") - if pil_img is not None: - try: - pil_img.close() - except Exception: - pass - - -def _normalize_bbox_to_unit(bbox, page_width: int, page_height: int) -> list[float] | None: - """把pipeline像素bbox转换为mineru-vl-utils需要的归一化bbox。""" - if bbox is None or len(bbox) != 4: - return None - x0, y0, x1, y1 = [float(v) for v in bbox] - if 0.0 <= x0 <= 1.0 and 0.0 <= y0 <= 1.0 and 0.0 <= x1 <= 1.0 and 0.0 <= y1 <= 1.0: - normalized_bbox = [x0, y0, x1, y1] - else: - normalized_bbox = [ - x0 / page_width, - y0 / page_height, - x1 / page_width, - y1 / page_height, - ] - normalized_bbox = [round(min(max(v, 0.0), 1.0), 6) for v in normalized_bbox] - if normalized_bbox[0] >= normalized_bbox[2] or normalized_bbox[1] >= normalized_bbox[3]: - return None - return normalized_bbox - - -def _vlm_type_for_layout_det(layout_det: dict, vlm_ocr_enable: bool, table_enable: bool, image_analysis: bool) -> str | None: - """把pipeline layout label映射为VLM内容抽取类型,未命中则跳过。""" - label = layout_det.get("label") - if label is None: - logger.warning("Layout detection result missing label: %s", layout_det) - return None - if label == "table": - return "table" if table_enable else None - if label == "display_formula": - return "equation" - if label in VLM_VISUAL_LABELS: - return "image" if image_analysis else None - if vlm_ocr_enable: - return VLM_TEXT_LABEL_TO_TYPE.get(label) - return None - - -def _build_vlm_layout_blocks( - layout_dets: list[dict], - page_width: int, - page_height: int, - vlm_ocr_enable: bool, - table_enable: bool, - image_analysis: bool, -) -> list[ContentBlock]: - """从pipeline layout结果构造VLM sidecar输入,并保留回填用索引。""" - blocks = [] - for position, layout_det in enumerate(layout_dets): - vlm_type = _vlm_type_for_layout_det( - layout_det, - vlm_ocr_enable=vlm_ocr_enable, - table_enable=table_enable, - image_analysis=image_analysis, - ) - if vlm_type is None: - continue - bbox = _normalize_bbox_to_unit(layout_det.get("bbox"), page_width, page_height) - if bbox is None: - continue - try: - block = ContentBlock( - vlm_type, - bbox, - angle=layout_det.get("angle", 0), - content=layout_det.get("content"), - ) - except AssertionError as exc: - logger.warning(f"Skip invalid hybrid-flash VLM block: {layout_det}, error: {exc}") - continue - block["_layout_det_index"] = layout_det.get("index", position) - block["_layout_det_position"] = position - block["_layout_det_label"] = layout_det.get("label") - blocks.append(block) - return blocks - - -def _build_not_extract_list(vlm_ocr_enable: bool) -> list[str] | None: - """非VLM-OCR模式下显式跳过文本抽取,保持hybrid文本边界。""" - if vlm_ocr_enable: - return None - return ["text"] - - -def _strip_display_formula_delimiters(content: str) -> str: - """去除VLM公式结果外层display delimiters,便于回填pipeline latex字段。""" - stripped = content.strip() - if stripped.startswith("\\[") and stripped.endswith("\\]"): - stripped = stripped[2:-2].strip() - return stripped - - -def _merge_vlm_sidecar_result(layout_dets: list[dict], sidecar_blocks) -> None: - """把VLM sidecar结果回填到pipeline layout_dets,保持原始label不变。""" - by_index = { - layout_det.get("index", position): layout_det - for position, layout_det in enumerate(layout_dets) - } - for block in sidecar_blocks or []: - layout_det = by_index.get(block.get("_layout_det_index")) - if layout_det is None: - position = block.get("_layout_det_position") - if isinstance(position, int) and 0 <= position < len(layout_dets): - layout_det = layout_dets[position] - if layout_det is None: - continue - - block_type = block.get("type") - block_content = block.get("content") - label = layout_det.get("label") - - if label == "seal": - if block_content is not None: - layout_det["text"] = block_content - layout_det["content"] = block_content - layout_det["sub_type"] = "seal" - continue - - if label in VLM_VISUAL_LABELS: - if block_content is not None: - layout_det["content"] = block_content - if "sub_type" in block: - layout_det["sub_type"] = block["sub_type"] - continue - - if label == "table" or block_type == "table": - if block_content is not None: - layout_det["html"] = block_content - if "cell_merge" in block: - layout_det["cell_merge"] = block["cell_merge"] - continue - - if label == "display_formula" or block_type == "equation": - if block_content: - layout_det["latex"] = _strip_display_formula_delimiters(block_content) - continue - - if block_type in VLM_TEXT_LABEL_TO_TYPE.values() and block_content is not None: - layout_det["text"] = block_content - layout_det["content"] = block_content - - -def _build_page_model_info(page_layout_dets: list[dict], page_index: int, pil_img) -> dict: - """按pipeline model_list格式包装单页analyze结果。""" - return { - "layout_dets": page_layout_dets, - "page_info": { - "page_no": page_index, - "width": pil_img.width, - "height": pil_img.height, - }, - } - - -def _build_analyze_meta(ocr_enable: bool, vlm_ocr_enable: bool) -> dict: - """构造第一阶段analyze元信息,供后续阶段判断backend分支和OCR策略。""" - return { - "_backend": "hybrid-flash", - "_ocr_enable": ocr_enable, - "_vlm_ocr_enable": vlm_ocr_enable, - "_version_name": __version__, - } - - -def _build_pipeline_batch_options(vlm_ocr_enable: bool) -> dict: - """构造 hybrid-flash 调用 pipeline batch 时的识别策略。""" - if vlm_ocr_enable: - return { - "formula_recognition_scope": "none", - "ocr_rec_enable": False, - } - return { - "formula_recognition_scope": "inline_only", - "ocr_rec_enable": True, - } - - -def _get_device_for_cleanup(): - """获取清理显存用device;测试或轻量环境缺少torch时退回CPU。""" - try: - return get_device() - except NameError: - return "cpu" - - -def doc_analyze( - pdf_bytes, - image_writer: DataWriter | None = None, - predictor: MinerUClient | None = None, - backend="transformers", - parse_method: str = "auto", - language: str = "ch", - inline_formula_enable: bool = True, - table_enable: bool = True, - model_path: str | None = None, - server_url: str | None = None, - image_analysis: bool = True, - **kwargs, -): - """hybrid-flash第一阶段analyze:返回pipeline形态model_list和backend元信息。""" - # hybrid-flash 当前只执行 analyze,客户端输出开关属于最终输出阶段,不能下传到模型初始化。 - kwargs.pop("client_side_output_generation", None) - if predictor is None: - predictor = ModelSingleton().get_model(backend, model_path, server_url, **kwargs) - predictor = _maybe_enable_serial_execution(predictor, backend) - - device = _get_device_for_cleanup() - ocr_enable = _get_ocr_enable(pdf_bytes, parse_method=parse_method) - vlm_ocr_enable = _should_enable_vlm_ocr(ocr_enable, language, inline_formula_enable) - pdf_doc = open_pdfium_document(pdfium.PdfDocument, pdf_bytes) - doc_closed = False - model_list = [] - try: - page_count = get_pdfium_document_page_count(pdf_doc) - configured_window_size = get_processing_window_size(default=64) - effective_window_size = min(page_count, configured_window_size) if page_count else 0 - total_windows = ( - (page_count + effective_window_size - 1) // effective_window_size - if effective_window_size - else 0 - ) - logger.info( - f"Hybrid-flash analyze window run. page_count={page_count}, " - f"window_size={configured_window_size}, total_windows={total_windows}" - ) - - infer_start = time.time() - progress_bar = None - last_append_end_time = None - try: - for window_index, window_start in enumerate(range(0, page_count, effective_window_size or 1)): - window_end = min(page_count - 1, window_start + effective_window_size - 1) - images_list = load_images_from_pdf_doc( - pdf_doc, - start_page_id=window_start, - end_page_id=window_end, - image_type=ImageType.PIL, - pdf_bytes=pdf_bytes, - ) - try: - images_pil_list = [image_dict["img_pil"] for image_dict in images_list] - logger.info( - f"Hybrid-flash analyze window {window_index + 1}/{total_windows}: " - f"pages {window_start + 1}-{window_end + 1}/{page_count} " - f"({len(images_pil_list)} pages)" - ) - pipeline_inputs = [ - (pil_img, ocr_enable, language) - for pil_img in images_pil_list - ] - page_layout_dets_list = batch_image_analyze( - pipeline_inputs, - formula_enable=inline_formula_enable, - table_enable=False, - seal_ocr_rec_enable=False, - **_build_pipeline_batch_options(vlm_ocr_enable), - ) - vlm_blocks_list = [ - _build_vlm_layout_blocks( - page_layout_dets, - pil_img.width, - pil_img.height, - vlm_ocr_enable=vlm_ocr_enable, - table_enable=table_enable, - image_analysis=image_analysis, - ) - for page_layout_dets, pil_img in zip(page_layout_dets_list, images_pil_list) - ] - if any(vlm_blocks_list): - with predictor_execution_guard(predictor): - sidecar_results = predictor.batch_extract_with_layout( - images_pil_list, - vlm_blocks_list, - not_extract_list=_build_not_extract_list(vlm_ocr_enable), - image_analysis=image_analysis, - ) - for page_layout_dets, sidecar_blocks in zip(page_layout_dets_list, sidecar_results): - _merge_vlm_sidecar_result(page_layout_dets, sidecar_blocks) - - if progress_bar is None: - progress_bar = tqdm(total=page_count, desc="Processing pages") - else: - exclude_progress_bar_idle_time( - progress_bar, - last_append_end_time, - now=time.time(), - ) - - for offset, (page_layout_dets, pil_img) in enumerate(zip(page_layout_dets_list, images_pil_list)): - page_index = window_start + offset - model_list.append(_build_page_model_info(page_layout_dets, page_index, pil_img)) - if progress_bar is not None: - progress_bar.update(1) - last_append_end_time = time.time() - finally: - _close_images(images_list) - finally: - if progress_bar is not None: - progress_bar.close() - - infer_time = round(time.time() - infer_start, 2) - if infer_time > 0 and page_count > 0: - logger.debug( - f"hybrid-flash analyze finished, cost: {infer_time}, " - f"speed: {round(len(model_list) / infer_time, 3)} page/s" - ) - close_pdfium_document(pdf_doc) - doc_closed = True - clean_memory(device) - return model_list, _build_analyze_meta(ocr_enable, vlm_ocr_enable) - finally: - if not doc_closed: - close_pdfium_document(pdf_doc) - - -async def aio_doc_analyze( - pdf_bytes, - image_writer: DataWriter | None = None, - predictor: MinerUClient | None = None, - backend="transformers", - parse_method: str = "auto", - language: str = "ch", - inline_formula_enable: bool = True, - table_enable: bool = True, - model_path: str | None = None, - server_url: str | None = None, - image_analysis: bool = True, - **kwargs, -): - """异步hybrid-flash analyze入口,返回pipeline形态model_list和backend元信息。""" - # hybrid-flash 当前只执行 analyze,客户端输出开关属于最终输出阶段,不能下传到模型初始化。 - kwargs.pop("client_side_output_generation", None) - if predictor is None: - predictor = await _get_model_async(backend, model_path, server_url, **kwargs) - predictor = _maybe_enable_serial_execution(predictor, backend) - - device = _get_device_for_cleanup() - ocr_enable = _get_ocr_enable(pdf_bytes, parse_method=parse_method) - vlm_ocr_enable = _should_enable_vlm_ocr(ocr_enable, language, inline_formula_enable) - pdf_doc = open_pdfium_document(pdfium.PdfDocument, pdf_bytes) - doc_closed = False - model_list = [] - try: - page_count = get_pdfium_document_page_count(pdf_doc) - configured_window_size = get_processing_window_size(default=64) - effective_window_size = min(page_count, configured_window_size) if page_count else 0 - - for window_start in range(0, page_count, effective_window_size or 1): - window_end = min(page_count - 1, window_start + effective_window_size - 1) - images_list = await aio_load_images_from_pdf_bytes_range( - pdf_bytes, - start_page_id=window_start, - end_page_id=window_end, - image_type=ImageType.PIL, - ) - try: - images_pil_list = [image_dict["img_pil"] for image_dict in images_list] - pipeline_inputs = [ - (pil_img, ocr_enable, language) - for pil_img in images_pil_list - ] - page_layout_dets_list = batch_image_analyze( - pipeline_inputs, - formula_enable=inline_formula_enable, - table_enable=False, - seal_ocr_rec_enable=False, - **_build_pipeline_batch_options(vlm_ocr_enable), - ) - vlm_blocks_list = [ - _build_vlm_layout_blocks( - page_layout_dets, - pil_img.width, - pil_img.height, - vlm_ocr_enable=vlm_ocr_enable, - table_enable=table_enable, - image_analysis=image_analysis, - ) - for page_layout_dets, pil_img in zip(page_layout_dets_list, images_pil_list) - ] - if any(vlm_blocks_list): - async with aio_predictor_execution_guard(predictor): - sidecar_results = await predictor.aio_batch_extract_with_layout( - images_pil_list, - vlm_blocks_list, - not_extract_list=_build_not_extract_list(vlm_ocr_enable), - image_analysis=image_analysis, - ) - for page_layout_dets, sidecar_blocks in zip(page_layout_dets_list, sidecar_results): - _merge_vlm_sidecar_result(page_layout_dets, sidecar_blocks) - for offset, (page_layout_dets, pil_img) in enumerate(zip(page_layout_dets_list, images_pil_list)): - model_list.append(_build_page_model_info(page_layout_dets, window_start + offset, pil_img)) - finally: - _close_images(images_list) - close_pdfium_document(pdf_doc) - doc_closed = True - clean_memory(device) - return model_list, _build_analyze_meta(ocr_enable, vlm_ocr_enable) - finally: - if not doc_closed: - close_pdfium_document(pdf_doc) diff --git a/mineru/cli/api_request.py b/mineru/cli/api_request.py index 56780fe9..ddc82b8f 100644 --- a/mineru/cli/api_request.py +++ b/mineru/cli/api_request.py @@ -105,9 +105,9 @@ async def parse_request_form( - vlm-engine: High accuracy via local computing power, supports Chinese and English documents only. - vlm-http-client: High accuracy via remote computing power(client suitable for openai-compatible servers), supports Chinese and English documents only. - hybrid-engine: Next-generation high accuracy solution via local computing power, supports multiple languages. -- hybrid-flash-engine: Experimental hybrid-flash analyze path via local computing power, supports multiple languages. +- hybrid-flash-engine: Hybrid flash mode via local computing power, supports multiple languages. - hybrid-http-client: High accuracy via remote computing power but requires a little local computing power(client suitable for openai-compatible servers), supports multiple languages. -- hybrid-flash-http-client: Experimental hybrid-flash analyze path via remote computing power(client suitable for openai-compatible servers), supports multiple languages.""", +- hybrid-flash-http-client: Hybrid flash mode via remote computing power(client suitable for openai-compatible servers), supports multiple languages.""", json_schema_extra=BACKEND_SCHEMA_EXTRA, ), ] = DEFAULT_BACKEND, diff --git a/mineru/cli/client.py b/mineru/cli/client.py index 4659d153..1beac233 100644 --- a/mineru/cli/client.py +++ b/mineru/cli/client.py @@ -1056,9 +1056,9 @@ async def run_orchestrated_cli( vlm-engine: High accuracy via local computing power. vlm-http-client: High accuracy via remote computing power(client suitable for openai-compatible servers). hybrid-engine: Next-generation high accuracy solution via local computing power. - hybrid-flash-engine: Experimental hybrid-flash analyze path via local computing power. + hybrid-flash-engine: Hybrid flash mode via local computing power. hybrid-http-client: High accuracy but requires a little local computing power(client suitable for openai-compatible servers). - hybrid-flash-http-client: Experimental hybrid-flash analyze path via remote computing power(client suitable for openai-compatible servers). + hybrid-flash-http-client: Hybrid flash mode via remote computing power(client suitable for openai-compatible servers). Without method specified, hybrid-engine will be used by default.""", ) @click.option( diff --git a/mineru/cli/common.py b/mineru/cli/common.py index 130c281a..e95496bf 100644 --- a/mineru/cli/common.py +++ b/mineru/cli/common.py @@ -70,13 +70,9 @@ def ensure_backend_dependencies(backend: str) -> None: def _load_hybrid_analyze_entrypoint(entrypoint_name: str, backend: str): - """按 hybrid 后端家族加载普通 hybrid 或 hybrid-flash 的 analyze 入口。""" + """加载统一 hybrid analyze 入口,flash/pro 由调用方通过 mode 控制。""" ensure_backend_dependencies(backend) - module_name = ( - "mineru.backend.hybrid_flash.hybrid_flash_analyze" - if backend.startswith("hybrid-flash-") - else "mineru.backend.hybrid.hybrid_analyze" - ) + module_name = "mineru.backend.hybrid.hybrid_analyze" try: hybrid_analyze = importlib.import_module(module_name) except (ImportError, ModuleNotFoundError) as exc: @@ -505,112 +501,6 @@ def _process_vlm( ) -def _process_hybrid_flash( - output_dir, - pdf_file_names, - pdf_bytes_list, - h_lang_list, - parse_method, - inline_formula_enable, - backend, - f_draw_layout_bbox, - f_draw_span_bbox, - f_dump_md, - f_dump_middle_json, - f_dump_model_output, - f_dump_orig_pdf, - f_dump_content_list, - f_make_md_mode, - table_enable, - server_url=None, - image_analysis=True, - **kwargs, -): - """同步运行 hybrid-flash analyze 阶段,当前只产出 analyze 调试结果。""" - hybrid_flash_doc_analyze = _load_hybrid_analyze_entrypoint( - "doc_analyze", - f"hybrid-flash-{backend}", - ) - - if not backend.endswith("client"): - server_url = None - - for idx, (pdf_bytes, lang) in enumerate(zip(pdf_bytes_list, h_lang_list)): - pdf_file_name = pdf_file_names[idx] - local_image_dir, _local_md_dir = prepare_env( - output_dir, - pdf_file_name, - f"hybrid_{parse_method}", - ) - image_writer = FileBasedDataWriter(local_image_dir) - - hybrid_flash_doc_analyze( - pdf_bytes=pdf_bytes, - image_writer=image_writer, - backend=backend, - parse_method=parse_method, - language=lang, - inline_formula_enable=inline_formula_enable, - table_enable=table_enable, - server_url=server_url, - image_analysis=image_analysis, - **kwargs, - ) - - -async def _async_process_hybrid_flash( - output_dir, - pdf_file_names, - pdf_bytes_list, - h_lang_list, - parse_method, - inline_formula_enable, - backend, - f_draw_layout_bbox, - f_draw_span_bbox, - f_dump_md, - f_dump_middle_json, - f_dump_model_output, - f_dump_orig_pdf, - f_dump_content_list, - f_make_md_mode, - table_enable, - server_url=None, - image_analysis=True, - **kwargs, -): - """异步运行 hybrid-flash analyze 阶段,当前只产出 analyze 调试结果。""" - aio_hybrid_flash_doc_analyze = _load_hybrid_analyze_entrypoint( - "aio_doc_analyze", - f"hybrid-flash-{backend}", - ) - - if not backend.endswith("client"): - server_url = None - - for idx, (pdf_bytes, lang) in enumerate(zip(pdf_bytes_list, h_lang_list)): - pdf_file_name = pdf_file_names[idx] - local_image_dir, _local_md_dir = prepare_env( - output_dir, - pdf_file_name, - f"hybrid_{parse_method}", - ) - image_writer = FileBasedDataWriter(local_image_dir) - - await aio_hybrid_flash_doc_analyze( - pdf_bytes=pdf_bytes, - image_writer=image_writer, - backend=backend, - parse_method=parse_method, - language=lang, - inline_formula_enable=inline_formula_enable, - table_enable=table_enable, - server_url=server_url, - image_analysis=image_analysis, - **kwargs, - ) - - def _process_hybrid( output_dir, pdf_file_names, @@ -628,6 +518,7 @@ def _process_hybrid( f_dump_content_list, f_make_md_mode, server_url=None, + mode="pro", **kwargs, ): hybrid_doc_analyze = _load_hybrid_analyze_entrypoint( @@ -651,6 +542,7 @@ def _process_hybrid( language=lang, inline_formula_enable=inline_formula_enable, server_url=server_url, + mode=mode, **kwargs, ) @@ -684,6 +576,7 @@ async def _async_process_hybrid( f_dump_content_list, f_make_md_mode, server_url=None, + mode="pro", **kwargs, ): aio_hybrid_doc_analyze = _load_hybrid_analyze_entrypoint( @@ -707,6 +600,7 @@ async def _async_process_hybrid( language=lang, inline_formula_enable=inline_formula_enable, server_url=server_url, + mode=mode, **kwargs, ) @@ -848,9 +742,9 @@ def do_parse( elif backend.startswith("hybrid-"): ensure_backend_dependencies(backend) backend = backend[7:] - is_flash = backend.startswith("flash-") + mode = "flash" if backend.startswith("flash-") else "pro" - if is_flash: + if mode == "flash": backend = backend[6:] if backend == "engine": @@ -859,22 +753,13 @@ def do_parse( os.environ['MINERU_VLM_TABLE_ENABLE'] = str(table_enable) os.environ['MINERU_VLM_FORMULA_ENABLE'] = "true" - if is_flash: - _process_hybrid_flash( - output_dir, pdf_file_names, pdf_bytes_list, p_lang_list, parse_method, formula_enable, backend, - f_draw_layout_bbox, f_draw_span_bbox, f_dump_md, f_dump_middle_json, - f_dump_model_output, f_dump_orig_pdf, f_dump_content_list, f_make_md_mode, - table_enable, server_url, image_analysis=image_analysis, - client_side_output_generation=client_side_output_generation, **kwargs, - ) - else: - _process_hybrid( - output_dir, pdf_file_names, pdf_bytes_list, p_lang_list, parse_method, formula_enable, backend, - f_draw_layout_bbox, f_draw_span_bbox, f_dump_md, f_dump_middle_json, - f_dump_model_output, f_dump_orig_pdf, f_dump_content_list, f_make_md_mode, - server_url, image_analysis=image_analysis, - client_side_output_generation=client_side_output_generation, **kwargs, - ) + _process_hybrid( + output_dir, pdf_file_names, pdf_bytes_list, p_lang_list, parse_method, formula_enable, backend, + f_draw_layout_bbox, f_draw_span_bbox, f_dump_md, f_dump_middle_json, + f_dump_model_output, f_dump_orig_pdf, f_dump_content_list, f_make_md_mode, + server_url, mode=mode, image_analysis=image_analysis, + client_side_output_generation=client_side_output_generation, **kwargs, + ) async def aio_do_parse( @@ -955,9 +840,9 @@ async def aio_do_parse( elif backend.startswith("hybrid-"): ensure_backend_dependencies(backend) backend = backend[7:] - is_flash = backend.startswith("flash-") + mode = "flash" if backend.startswith("flash-") else "pro" - if is_flash: + if mode == "flash": backend = backend[6:] if backend == "engine": @@ -966,22 +851,13 @@ async def aio_do_parse( os.environ['MINERU_VLM_TABLE_ENABLE'] = str(table_enable) os.environ['MINERU_VLM_FORMULA_ENABLE'] = "true" - if is_flash: - await _async_process_hybrid_flash( - output_dir, pdf_file_names, pdf_bytes_list, p_lang_list, parse_method, formula_enable, backend, - f_draw_layout_bbox, f_draw_span_bbox, f_dump_md, f_dump_middle_json, - f_dump_model_output, f_dump_orig_pdf, f_dump_content_list, f_make_md_mode, - table_enable, server_url, image_analysis=image_analysis, - client_side_output_generation=client_side_output_generation, **kwargs, - ) - else: - await _async_process_hybrid( - output_dir, pdf_file_names, pdf_bytes_list, p_lang_list, parse_method, formula_enable, backend, - f_draw_layout_bbox, f_draw_span_bbox, f_dump_md, f_dump_middle_json, - f_dump_model_output, f_dump_orig_pdf, f_dump_content_list, f_make_md_mode, - server_url, image_analysis=image_analysis, - client_side_output_generation=client_side_output_generation, **kwargs, - ) + await _async_process_hybrid( + output_dir, pdf_file_names, pdf_bytes_list, p_lang_list, parse_method, formula_enable, backend, + f_draw_layout_bbox, f_draw_span_bbox, f_dump_md, f_dump_middle_json, + f_dump_model_output, f_dump_orig_pdf, f_dump_content_list, f_make_md_mode, + server_url, mode=mode, image_analysis=image_analysis, + client_side_output_generation=client_side_output_generation, **kwargs, + ) if __name__ == "__main__": From 98abd2f948335fb941b99d113ab2bfbbea22ec19 Mon Sep 17 00:00:00 2001 From: myhloli Date: Fri, 5 Jun 2026 23:41:25 +0800 Subject: [PATCH 11/22] feat: refactor batch analysis to simplify formula recognition and OCR settings --- mineru/backend/pipeline/batch_analyze.py | 77 +++++++-------------- mineru/backend/pipeline/pipeline_analyze.py | 8 +-- 2 files changed, 26 insertions(+), 59 deletions(-) diff --git a/mineru/backend/pipeline/batch_analyze.py b/mineru/backend/pipeline/batch_analyze.py index 7cd6ddef..ca18fd07 100644 --- a/mineru/backend/pipeline/batch_analyze.py +++ b/mineru/backend/pipeline/batch_analyze.py @@ -57,16 +57,11 @@ class BatchAnalyze: table_ori_cls_batch_enabled: bool | None = None, text_ocr_det_batch_enabled: bool | None = None, mask_inline_formula_for_ocr_det: bool = True, - formula_recognition_scope: str = "all", - ocr_rec_enable: bool = True, - seal_ocr_rec_enable: bool = True, ): self.batch_ratio = batch_ratio self.formula_enable = get_formula_enable(formula_enable) self.table_enable = get_table_enable(table_enable) self.model_manager = model_manager - self.ocr_rec_enable = ocr_rec_enable - self.seal_ocr_rec_enable = seal_ocr_rec_enable self.enable_ocr_det_batch = enable_ocr_det_batch self.table_ori_cls_batch_enabled = ( enable_ocr_det_batch if table_ori_cls_batch_enabled is None else table_ori_cls_batch_enabled @@ -77,10 +72,6 @@ class BatchAnalyze: self.mask_inline_formula_for_ocr_det = ( get_ocr_det_mask_inline_formula_enable(mask_inline_formula_for_ocr_det) ) - if formula_recognition_scope not in {"all", "inline_only", "none"}: - raise ValueError(f"Unsupported formula_recognition_scope: {formula_recognition_scope}") - # 控制公式识别范围,默认保持pipeline原行为;hybrid-flash可只保留公式det而跳过MFR。 - self.formula_recognition_scope = formula_recognition_scope @staticmethod def _apply_mask_boxes_to_image( @@ -268,9 +259,6 @@ class BatchAnalyze: return match.expand(replacement) return text - def _formula_recognition_enabled(self) -> bool: - return self.formula_enable and self.formula_recognition_scope != "none" - @classmethod def _extract_table_inline_objects( cls, @@ -369,7 +357,7 @@ class BatchAnalyze: self.model = self.model_manager.get_model( lang=None, - formula_enable=self._formula_recognition_enabled(), + formula_enable=self.formula_enable, table_enable=self.table_enable, ) atom_model_manager = AtomModelSingleton() @@ -388,52 +376,38 @@ class BatchAnalyze: clean_vram(self.model.device, vram_threshold=8) if self.formula_enable: - all_formula_labels = ["display_formula", "inline_formula"] - formula_labels = all_formula_labels - if self.formula_recognition_scope == "inline_only": - formula_labels = ["inline_formula"] images_mfd_res = [] for layout_res in images_layout_res: page_formula_res = [] for res in layout_res: - if res.get("label") in all_formula_labels: + if res.get("label") in ["display_formula", "inline_formula"]: res.setdefault("latex", "") - if res.get("label") in formula_labels: page_formula_res.append(res) images_mfd_res.append(page_formula_res) - if self.formula_recognition_scope != "none": - # 公式识别 - images_formula_list = run_mfr_inference( - self.model.mfr_model.batch_predict, - images_mfd_res, - np_images, - batch_size=self.batch_ratio * MFR_BASE_BATCH_SIZE, - ) - mfr_count = 0 - for image_index in range(len(np_images)): - mfr_count += len(images_formula_list[image_index]) - for formula_res, formula_with_latex in zip( - images_mfd_res[image_index], images_formula_list[image_index] - ): - formula_res["latex"] = formula_with_latex.get("latex", "") + # 公式识别 + images_formula_list = run_mfr_inference( + self.model.mfr_model.batch_predict, + images_mfd_res, + np_images, + batch_size=self.batch_ratio * MFR_BASE_BATCH_SIZE, + ) + mfr_count = 0 + for image_index in range(len(np_images)): + mfr_count += len(images_formula_list[image_index]) + for formula_res, formula_with_latex in zip( + images_mfd_res[image_index], images_formula_list[image_index] + ): + formula_res["latex"] = formula_with_latex.get("latex", "") - # 清理显存 - clean_vram(self.model.device, vram_threshold=8) - else: - for page_formula_res in images_mfd_res: - for formula_res in page_formula_res: - formula_res["latex"] = "" + # 清理显存 + clean_vram(self.model.device, vram_threshold=8) else: for layout_res in images_layout_res: # 移除所有的"inline_formula" layout_res[:] = [res for res in layout_res if res.get("label") != "inline_formula"] - - - ocr_should_recognize_text = bool(self.ocr_rec_enable) - ocr_res_list_all_page = [] table_res_list_all_page = [] for index in range(len(np_images)): @@ -784,7 +758,7 @@ class BatchAnalyze: ocr_result_list = get_ocr_result_list( ocr_res, useful_list, - ocr_res_list_dict['ocr_enable'] and ocr_should_recognize_text, + ocr_res_list_dict['ocr_enable'], bgr_image, _lang, ) @@ -828,7 +802,7 @@ class BatchAnalyze: ocr_result_list = get_ocr_result_list( ocr_res, useful_list, - ocr_res_list_dict['ocr_enable'] and ocr_should_recognize_text, + ocr_res_list_dict['ocr_enable'], bgr_image, _lang, ) @@ -917,11 +891,10 @@ class BatchAnalyze: total_processed += len(img_crop_list) seal_ocr_items = [] - if self.seal_ocr_rec_enable: - for ocr_res_list_dict in ocr_res_list_all_page: - for layout_res_item in ocr_res_list_dict['layout_res']: - if layout_res_item.get("label") == "seal": - seal_ocr_items.append((ocr_res_list_dict, layout_res_item)) + for ocr_res_list_dict in ocr_res_list_all_page: + for layout_res_item in ocr_res_list_dict['layout_res']: + if layout_res_item.get("label") == "seal": + seal_ocr_items.append((ocr_res_list_dict, layout_res_item)) seal_ocr_model = None for ocr_res_list_dict, layout_res_item in tqdm(seal_ocr_items, desc="Seal Predict"): @@ -969,7 +942,7 @@ class BatchAnalyze: for ocr_res_list_dict in ocr_res_list_all_page: self._prune_empty_ocr_text_blocks( ocr_res_list_dict["layout_res"], - ocr_res_list_dict["ocr_enable"] and self.ocr_rec_enable, + ocr_res_list_dict["ocr_enable"], ) return images_layout_res diff --git a/mineru/backend/pipeline/pipeline_analyze.py b/mineru/backend/pipeline/pipeline_analyze.py index e8670d18..b288712b 100644 --- a/mineru/backend/pipeline/pipeline_analyze.py +++ b/mineru/backend/pipeline/pipeline_analyze.py @@ -331,10 +331,7 @@ def doc_analyze_streaming( def batch_image_analyze( images_with_extra_info: List[Tuple[Image.Image, bool, str]], formula_enable=True, - table_enable=True, - formula_recognition_scope="all", - ocr_rec_enable=True, - seal_ocr_rec_enable=True): + table_enable=True): from .batch_analyze import BatchAnalyze @@ -385,9 +382,6 @@ def batch_image_analyze( formula_enable, table_enable, enable_ocr_det_batch, - formula_recognition_scope=formula_recognition_scope, - ocr_rec_enable=ocr_rec_enable, - seal_ocr_rec_enable=seal_ocr_rec_enable, ) results = batch_model(images_with_extra_info) From 41e100267f60961aaf8ca80bf232c20364adb37e Mon Sep 17 00:00:00 2001 From: myhloli Date: Sat, 6 Jun 2026 01:22:46 +0800 Subject: [PATCH 12/22] feat: implement formula number processing functions and optimize hybrid flash layout handling --- mineru/backend/hybrid/hybrid_analyze.py | 106 +++++++++++- .../pipeline/model_json_to_middle_json.py | 68 +------- .../backend/pipeline/pipeline_magic_model.py | 15 +- mineru/backend/utils/formula_number.py | 154 ++++++++++++++++++ pyproject.toml | 2 +- 5 files changed, 263 insertions(+), 82 deletions(-) create mode 100644 mineru/backend/utils/formula_number.py diff --git a/mineru/backend/hybrid/hybrid_analyze.py b/mineru/backend/hybrid/hybrid_analyze.py index 46cf05ed..aaa548d0 100644 --- a/mineru/backend/hybrid/hybrid_analyze.py +++ b/mineru/backend/hybrid/hybrid_analyze.py @@ -25,6 +25,8 @@ from mineru.backend.pipeline.model_init import ( run_mfr_inference, run_ocr_inference, ) +from mineru.backend.pipeline.model_list import AtomicModel +from mineru.backend.utils.formula_number import optimize_flash_formula_number_blocks from mineru.backend.vlm.vlm_analyze import ( ModelSingleton, aio_predictor_execution_guard, @@ -70,7 +72,7 @@ FLASH_LAYOUT_LABEL_TO_VLM_TYPE = { "footer": BlockType.FOOTER, "footer_image": BlockType.FOOTER, "footnote": BlockType.PAGE_FOOTNOTE, - "formula_number": BlockType.TEXT, + "formula_number": BlockType.FORMULA_NUMBER, "header": BlockType.HEADER, "header_image": BlockType.HEADER, "number": BlockType.PAGE_NUMBER, @@ -330,6 +332,42 @@ def _layout_det_bbox_to_unit(layout_det, page_width, page_height): return bbox_item["bbox"] +def _layout_det_bbox_to_pixel(layout_det, page_width, page_height): + """将layout bbox转换为页面像素坐标,兼容归一化和像素两种输入。""" + bbox = layout_det.get("bbox") + if bbox is None or len(bbox) != 4: + return None + + x0, y0, x1, y1 = [float(v) for v in bbox] + if ( + 0.0 <= x0 <= 1.0 + and 0.0 <= y0 <= 1.0 + and 0.0 <= x1 <= 1.0 + and 0.0 <= y1 <= 1.0 + ): + x0, x1 = x0 * page_width, x1 * page_width + y0, y1 = y0 * page_height, y1 * page_height + + x0 = max(0, min(page_width, x0)) + y0 = max(0, min(page_height, y0)) + x1 = max(0, min(page_width, x1)) + y1 = max(0, min(page_height, y1)) + if x1 <= x0 or y1 <= y0: + return None + return [x0, y0, x1, y1] + + +def _normalize_flash_vlm_angle(angle): + """将pipeline方向标签转换为mineru-vl-utils接受的整数角度。""" + try: + normalized_angle = int(angle) + except (TypeError, ValueError): + return 0 + if normalized_angle in {0, 90, 180, 270}: + return normalized_angle + return 0 + + def _build_flash_vlm_layout_blocks(layout_dets, page_width, page_height): """用 pipeline layout 构造 VLM 外部 layout 输入,跳过 VLM 自身 layout 解析。""" blocks = [] @@ -345,7 +383,7 @@ def _build_flash_vlm_layout_blocks(layout_dets, page_width, page_height): block = ContentBlock( vlm_type, bbox, - angle=layout_det.get("angle", 0), + angle=_normalize_flash_vlm_angle(layout_det.get("angle", 0)), content=layout_det.get("content"), ) except AssertionError as exc: @@ -355,6 +393,55 @@ def _build_flash_vlm_layout_blocks(layout_dets, page_width, page_height): return blocks +def _apply_flash_table_orientation_labels( + images_pil_list, + images_layout_res, + hybrid_pipeline_model, + batch_ratio: int = 1, +): + """复用pipeline表格方向分类,为Hybrid flash的table layout写入VLM旋转角度。""" + table_inputs = [] + table_layout_refs = [] + for pil_img, layout_res in zip(images_pil_list, images_layout_res): + page_width, page_height = pil_img.size + for layout_det in layout_res or []: + if layout_det.get("label") != "table": + continue + pixel_bbox = _layout_det_bbox_to_pixel(layout_det, page_width, page_height) + if pixel_bbox is None: + continue + try: + table_img, _ = crop_img({"bbox": pixel_bbox}, pil_img) + except Exception as exc: + logger.warning( + f"Skip Hybrid flash table orientation crop: {layout_det}, error: {exc}" + ) + continue + table_inputs.append({"table_img": table_img}) + table_layout_refs.append(layout_det) + + if not table_inputs: + return + + try: + table_orientation_cls_model = hybrid_pipeline_model.atom_model_manager.get_atom_model( + atom_model_name=AtomicModel.TableOrientationCls, + lang=getattr(hybrid_pipeline_model, "lang", None), + ) + rotate_labels = table_orientation_cls_model.batch_predict( + table_inputs, + det_batch_size=max(1, batch_ratio * OCR_DET_BASE_BATCH_SIZE), + ) + if len(rotate_labels) != len(table_layout_refs): + raise ValueError("Table orientation prediction result count mismatch") + for layout_det, rotate_label in zip(table_layout_refs, rotate_labels): + layout_det["angle"] = str(rotate_label or "0") + except Exception as exc: + logger.warning( + f"Hybrid flash table orientation classification failed: {exc}, using original table images" + ) + + def _formula_item_to_pixel_bbox(item): bbox = item.get('bbox') if bbox is not None and len(bbox) == 4: @@ -891,6 +978,12 @@ def doc_analyze( _vlm_ocr_enable, ) if mode == "flash": + _apply_flash_table_orientation_labels( + images_pil_list, + images_layout_res, + hybrid_pipeline_model, + batch_ratio=batch_ratio, + ) vlm_blocks_list = [ _build_flash_vlm_layout_blocks( page_layout_res, @@ -906,6 +999,7 @@ def doc_analyze( not_extract_list=None if _vlm_ocr_enable else not_extract_list, image_analysis=image_analysis, ) + optimize_flash_formula_number_blocks(window_model_list) if _vlm_ocr_enable: _apply_vlm_ocr_det_sidecars_for_window( images_pil_list, @@ -1092,6 +1186,13 @@ async def aio_doc_analyze( _vlm_ocr_enable, ) if mode == "flash": + await asyncio.to_thread( + _apply_flash_table_orientation_labels, + images_pil_list, + images_layout_res, + hybrid_pipeline_model, + batch_ratio, + ) vlm_blocks_list = [ _build_flash_vlm_layout_blocks( page_layout_res, @@ -1107,6 +1208,7 @@ async def aio_doc_analyze( not_extract_list=None if _vlm_ocr_enable else not_extract_list, image_analysis=image_analysis, ) + optimize_flash_formula_number_blocks(window_model_list) if _vlm_ocr_enable: await asyncio.to_thread( _apply_vlm_ocr_det_sidecars_for_window, diff --git a/mineru/backend/pipeline/model_json_to_middle_json.py b/mineru/backend/pipeline/model_json_to_middle_json.py index 5c4cc9b9..dcd66c40 100644 --- a/mineru/backend/pipeline/model_json_to_middle_json.py +++ b/mineru/backend/pipeline/model_json_to_middle_json.py @@ -4,13 +4,13 @@ import copy from tqdm import tqdm from mineru.backend.utils.html_image_utils import replace_inline_table_images +from mineru.backend.utils.formula_number import optimize_formula_number_blocks from mineru.backend.utils.runtime_utils import cross_page_table_merge from mineru.backend.pipeline.model_init import ( AtomModelSingleton, run_ocr_inference, ) from mineru.backend.pipeline.para_split import para_split -from mineru.utils.char_utils import full_to_half from mineru.utils.cut_image import cut_image_and_table from mineru.utils.enum_class import ContentType, BlockType from mineru.utils.title_level_postprocess import apply_title_leveling_to_pdf_info @@ -136,15 +136,6 @@ def append_batch_results_to_middle_json( ) -def _extract_text_from_block(block): - text_parts = [] - for line in block.get("lines", []): - for span in line.get("spans", []): - if span.get("type") == ContentType.TEXT: - text_parts.append(span.get("content", "")) - return "".join(text_parts).strip() - - def _iter_block_spans(block): for line in block.get("lines", []): for span in line.get("spans", []): @@ -154,61 +145,6 @@ def _iter_block_spans(block): yield from _iter_block_spans(sub_block) -def _normalize_formula_tag_content(tag_content): - tag_content = full_to_half(tag_content.strip()) - if tag_content.startswith("("): - tag_content = tag_content[1:].strip() - if tag_content.endswith(")"): - tag_content = tag_content[:-1].strip() - return tag_content - - -def _get_interline_equation_span(block): - for line in block.get("lines", []): - for span in line.get("spans", []): - if span.get("type") == ContentType.INTERLINE_EQUATION: - return span - return None - - -def _append_formula_number_tag(equation_block, formula_number_block): - equation_span = _get_interline_equation_span(equation_block) - tag_content = _normalize_formula_tag_content(_extract_text_from_block(formula_number_block)) - if equation_span is not None: - formula = equation_span.get("content", "") - equation_span["content"] = f"{formula}\\tag{{{tag_content}}}" - - -def _optimize_formula_number_blocks(pdf_info_list): - for page_info in pdf_info_list: - optimized_blocks = [] - blocks = page_info.get("preproc_blocks", []) - for index, block in enumerate(blocks): - if block.get("type") != BlockType.FORMULA_NUMBER: - optimized_blocks.append(block) - continue - - prev_block = blocks[index - 1] if index > 0 else None - if prev_block and prev_block.get("type") == BlockType.INTERLINE_EQUATION: - _append_formula_number_tag(prev_block, block) - continue - - next_block = blocks[index + 1] if index + 1 < len(blocks) else None - next_next_block = blocks[index + 2] if index + 2 < len(blocks) else None - if ( - next_block - and next_block.get("type") == BlockType.INTERLINE_EQUATION - and (next_next_block is None or next_next_block.get("type") != BlockType.FORMULA_NUMBER) - ): - _append_formula_number_tag(next_block, block) - continue - - block["type"] = BlockType.TEXT - optimized_blocks.append(block) - - page_info["preproc_blocks"] = optimized_blocks - - def _apply_post_ocr(pdf_info_list, lang=None): need_ocr_list = [] img_crop_list = [] @@ -279,7 +215,7 @@ def apply_server_side_postprocess(pdf_info_list, lang=None): def finalize_middle_json_from_preproc(pdf_info_list): """从 preproc_blocks 执行确定性 finalize,供服务端完整路径和客户端复用。""" - _optimize_formula_number_blocks(pdf_info_list) + optimize_formula_number_blocks(pdf_info_list) para_split(pdf_info_list) cross_page_table_merge(pdf_info_list) apply_title_leveling_to_pdf_info(pdf_info_list) diff --git a/mineru/backend/pipeline/pipeline_magic_model.py b/mineru/backend/pipeline/pipeline_magic_model.py index fa82e223..30f43b05 100644 --- a/mineru/backend/pipeline/pipeline_magic_model.py +++ b/mineru/backend/pipeline/pipeline_magic_model.py @@ -1,10 +1,7 @@ # Copyright (c) Opendatalab. All rights reserved. from mineru.backend.pipeline.para_split import ListLineTag from mineru.backend.pipeline.pipeline_middle_json_mkcontent import _merge_para_text -from mineru.utils.boxbase import ( - calculate_overlap_area_2_minbox_area_ratio, - calculate_overlap_area_in_bbox1_area_ratio, -) +from mineru.backend.utils.formula_number import formula_number_max_overlap_ratio from mineru.utils.enum_class import ContentType, BlockType from mineru.utils.guess_suffix_or_lang import guess_language_by_text from mineru.utils.span_block_fix import merge_spans_to_vertical_line, vertical_line_sort_spans_from_top_to_bottom, \ @@ -286,7 +283,7 @@ class MagicModel: if block["type"] == BlockType.FORMULA_NUMBER: block_spans = span_matcher.collect_for_block( block["bbox"], - overlap_ratio_getter=self.__formula_number_overlap_ratio, + overlap_ratio_getter=formula_number_max_overlap_ratio, ) else: block_spans = span_matcher.collect_for_block(block["bbox"]) @@ -295,14 +292,6 @@ class MagicModel: block = self.__fix_text_block(block) self.page_text_inline_formula_spans = span_matcher.remaining_spans() - @staticmethod - def __formula_number_overlap_ratio(span, block_bbox): - """公式编号框较窄时,沿用最小框重叠比例提高回填召回。""" - return max( - calculate_overlap_area_in_bbox1_area_ratio(span['bbox'], block_bbox), - calculate_overlap_area_2_minbox_area_ratio(span['bbox'], block_bbox), - ) - def __fix_axis(self): need_remove_list = [] layout_dets = self.__page_model_info["layout_dets"] diff --git a/mineru/backend/utils/formula_number.py b/mineru/backend/utils/formula_number.py new file mode 100644 index 00000000..2245c3a1 --- /dev/null +++ b/mineru/backend/utils/formula_number.py @@ -0,0 +1,154 @@ +# Copyright (c) Opendatalab. All rights reserved. +from collections.abc import Callable, Iterable, Sequence +from typing import Any + +from mineru.utils.boxbase import ( + calculate_overlap_area_2_minbox_area_ratio, + calculate_overlap_area_in_bbox1_area_ratio, +) +from mineru.utils.char_utils import full_to_half +from mineru.utils.enum_class import BlockType, ContentType + +Block = dict[str, Any] + + +def formula_number_max_overlap_ratio(span: Block, block_bbox: Sequence[float]) -> float: + """取公式编号span与block的两种重叠比例最大值,兼容block窄于span的情况。""" + return max( + calculate_overlap_area_in_bbox1_area_ratio(span["bbox"], block_bbox), + calculate_overlap_area_2_minbox_area_ratio(span["bbox"], block_bbox), + ) + + +def extract_formula_number_text(block: Block) -> str: + """从公式编号块中提取文本,优先使用VLM直接返回的content。""" + content = block.get("content") + if isinstance(content, str) and content.strip(): + return content.strip() + + text_parts = [] + for line in block.get("lines", []): + for span in line.get("spans", []): + if span.get("type") == ContentType.TEXT: + text_parts.append(span.get("content", "")) + return "".join(text_parts).strip() + + +def normalize_formula_tag_content(tag_content: str) -> str: + """归一化公式编号文本,去掉外层括号并转换全角字符。""" + tag_content = full_to_half(tag_content.strip()) + if tag_content.startswith("("): + tag_content = tag_content[1:].strip() + if tag_content.endswith(")"): + tag_content = tag_content[:-1].strip() + return tag_content + + +def build_tagged_formula_content( + formula_content: str, + formula_number_block: Block, +) -> str: + """将公式正文和公式编号合成带LaTeX tag的公式内容。""" + tag_content = normalize_formula_tag_content( + extract_formula_number_text(formula_number_block) + ) + return f"{formula_content}\\tag{{{tag_content}}}" + + +def get_interline_equation_span(block: Block) -> Block | None: + """查找行间公式块中的公式span。""" + for line in block.get("lines", []): + for span in line.get("spans", []): + if span.get("type") == ContentType.INTERLINE_EQUATION: + return span + return None + + +def append_formula_number_tag( + equation_block: Block, + formula_number_block: Block, +) -> None: + """将公式编号写入pipeline middle-json行间公式span。""" + equation_span = get_interline_equation_span(equation_block) + if equation_span is not None: + equation_span["content"] = build_tagged_formula_content( + equation_span.get("content", ""), + formula_number_block, + ) + + +def _optimize_formula_number_sequence( + blocks: Sequence[Block], + is_formula_number: Callable[[Block], bool], + is_equation: Callable[[Block], bool], + append_tag: Callable[[Block, Block], None], + downgrade_block: Callable[[Block], None], +) -> list[Block]: + """按统一相邻规则优化公式编号序列,调用方负责适配不同block结构。""" + optimized_blocks = [] + for index, block in enumerate(blocks): + if not is_formula_number(block): + optimized_blocks.append(block) + continue + + prev_block = blocks[index - 1] if index > 0 else None + if prev_block and is_equation(prev_block): + append_tag(prev_block, block) + continue + + next_block = blocks[index + 1] if index + 1 < len(blocks) else None + next_next_block = blocks[index + 2] if index + 2 < len(blocks) else None + if ( + next_block + and is_equation(next_block) + and (next_next_block is None or not is_formula_number(next_next_block)) + ): + append_tag(next_block, block) + continue + + downgrade_block(block) + optimized_blocks.append(block) + + return optimized_blocks + + +def _downgrade_formula_number_to_text(block: Block) -> None: + """将未匹配公式编号降级为普通文本块。""" + block["type"] = BlockType.TEXT + + +def _append_flash_formula_number_tag( + equation_block: Block, + formula_number_block: Block, +) -> None: + """将公式编号写入Hybrid flash的VLM行间公式内容。""" + equation_block["content"] = build_tagged_formula_content( + equation_block.get("content", ""), + formula_number_block, + ) + + +def optimize_formula_number_blocks(pdf_info_list: Iterable[Block]) -> None: + """按pipeline规则合并公式编号块,未匹配的编号降级为普通文本。""" + for page_info in pdf_info_list: + blocks = page_info.get("preproc_blocks", []) + page_info["preproc_blocks"] = _optimize_formula_number_sequence( + blocks, + lambda block: block.get("type") == BlockType.FORMULA_NUMBER, + lambda block: block.get("type") == BlockType.INTERLINE_EQUATION, + append_formula_number_tag, + _downgrade_formula_number_to_text, + ) + + +def optimize_flash_formula_number_blocks(model_list: Iterable[list[Block]]) -> None: + """按统一相邻规则处理Hybrid flash的VLM公式编号块。""" + for page_model_list in model_list: + optimized_blocks = _optimize_formula_number_sequence( + page_model_list or [], + lambda block: block.get("type") == BlockType.FORMULA_NUMBER, + lambda block: block.get("type") == BlockType.EQUATION, + _append_flash_formula_number_tag, + _downgrade_formula_number_to_text, + ) + page_model_list[:] = optimized_blocks diff --git a/pyproject.toml b/pyproject.toml index ed6fd3e0..79cc4dd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "openai>=1.70.0,<3", "beautifulsoup4>=4.13.5,<5", "magika>=0.6.2,<1.1.0", - "mineru-vl-utils>=1.0.2,<2", + "mineru-vl-utils>=1.0.4,<2", "python-docx>=1.2.0,<2", 'pypptx-with-oxml>=1.0.3,<2', "mammoth>=1.11.0,<2", From dfd7306c2f474ed5d51dd5e08437119b90e6c275 Mon Sep 17 00:00:00 2001 From: myhloli Date: Sat, 6 Jun 2026 01:37:18 +0800 Subject: [PATCH 13/22] feat: add normalization function for formula content to clean display separators --- mineru/backend/utils/formula_number.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mineru/backend/utils/formula_number.py b/mineru/backend/utils/formula_number.py index 2245c3a1..3a29efae 100644 --- a/mineru/backend/utils/formula_number.py +++ b/mineru/backend/utils/formula_number.py @@ -8,6 +8,7 @@ from mineru.utils.boxbase import ( ) from mineru.utils.char_utils import full_to_half from mineru.utils.enum_class import BlockType, ContentType +from mineru.utils.visual_magic_model_utils import isolated_formula_clean Block = dict[str, Any] @@ -44,11 +45,17 @@ def normalize_formula_tag_content(tag_content: str) -> str: return tag_content +def _normalize_formula_content_for_tag(formula_content: str) -> str: + """归一化待合并编号的公式正文,去掉VLM可能携带的展示公式分隔符。""" + return isolated_formula_clean(formula_content or "") + + def build_tagged_formula_content( formula_content: str, formula_number_block: Block, ) -> str: """将公式正文和公式编号合成带LaTeX tag的公式内容。""" + formula_content = _normalize_formula_content_for_tag(formula_content) tag_content = normalize_formula_tag_content( extract_formula_number_text(formula_number_block) ) From 096028417c749b5a1fd77e9e0e206a7bdc8d034d Mon Sep 17 00:00:00 2001 From: myhloli Date: Sat, 6 Jun 2026 02:17:37 +0800 Subject: [PATCH 14/22] feat: update image collection logic to support all table orientations and simplify aspect ratio filtering --- .../model/table/cls/mineru_table_ori_cls.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/mineru/model/table/cls/mineru_table_ori_cls.py b/mineru/model/table/cls/mineru_table_ori_cls.py index b7002ec9..2207fd16 100644 --- a/mineru/model/table/cls/mineru_table_ori_cls.py +++ b/mineru/model/table/cls/mineru_table_ori_cls.py @@ -233,13 +233,24 @@ class MineruTableOrientationClsModel: imgs: List[Dict], resolution_group_stride: int, ) -> Dict[tuple[int, int], list[Dict]]: - """按归一化分辨率收集竖版表格,横版表格默认保持 0 度跳过后续 OCR。""" + """兼容旧私有入口,实际收集逻辑已不再按表格宽高比过滤。""" + return cls._collect_orientation_image_groups( + imgs, + resolution_group_stride, + ) + + @classmethod + def _collect_orientation_image_groups( + cls, + imgs: List[Dict], + resolution_group_stride: int, + ) -> Dict[tuple[int, int], list[Dict]]: + """按归一化分辨率收集所有有效表格图,旋转判断交给 OCR det/rec 评分。""" resolution_groups = defaultdict(list) for index, img in enumerate(imgs): bgr_img = cls._to_bgr_table_image(img) img_height, img_width = bgr_img.shape[:2] - img_aspect_ratio = img_height / img_width if img_width > 0 else 1.0 - if img_aspect_ratio <= 1.2: + if img_height <= 0 or img_width <= 0: continue group_key = ( @@ -281,7 +292,7 @@ class MineruTableOrientationClsModel: det_batch_size: int, resolution_group_stride: int, ) -> list[Dict]: - """对竖版表格批量做 OCR det,并筛选需要进入多角度评分的候选。""" + """对表格批量做 OCR det,并筛选需要进入多角度评分的候选。""" rotated_imgs = [] for _group_key, group_imgs in resolution_groups.items(): batch_images = self._pad_group_images(group_imgs, resolution_group_stride) @@ -353,7 +364,7 @@ class MineruTableOrientationClsModel: """ RESOLUTION_GROUP_STRIDE = 128 rotate_labels = ["0"] * len(imgs) - resolution_groups = self._collect_portrait_image_groups( + resolution_groups = self._collect_orientation_image_groups( imgs, RESOLUTION_GROUP_STRIDE, ) From 69ec1e320eee32d2bf579e499b95a87fce3016f4 Mon Sep 17 00:00:00 2001 From: myhloli Date: Sat, 6 Jun 2026 02:48:15 +0800 Subject: [PATCH 15/22] feat: enhance progress tracking in batch analysis and OCR processes with tqdm integration --- mineru/backend/hybrid/hybrid_analyze.py | 1 + mineru/backend/pipeline/batch_analyze.py | 1 + mineru/model/ocr/pytorch_paddle.py | 8 +- .../model/table/cls/mineru_table_ori_cls.py | 103 ++++++++++++++++-- mineru/model/utils/tools/infer/predict_rec.py | 21 +++- 5 files changed, 118 insertions(+), 16 deletions(-) diff --git a/mineru/backend/hybrid/hybrid_analyze.py b/mineru/backend/hybrid/hybrid_analyze.py index aaa548d0..9e364288 100644 --- a/mineru/backend/hybrid/hybrid_analyze.py +++ b/mineru/backend/hybrid/hybrid_analyze.py @@ -431,6 +431,7 @@ def _apply_flash_table_orientation_labels( rotate_labels = table_orientation_cls_model.batch_predict( table_inputs, det_batch_size=max(1, batch_ratio * OCR_DET_BASE_BATCH_SIZE), + tqdm_enable=True, ) if len(rotate_labels) != len(table_layout_refs): raise ValueError("Table orientation prediction result count mismatch") diff --git a/mineru/backend/pipeline/batch_analyze.py b/mineru/backend/pipeline/batch_analyze.py index ca18fd07..bc60237d 100644 --- a/mineru/backend/pipeline/batch_analyze.py +++ b/mineru/backend/pipeline/batch_analyze.py @@ -472,6 +472,7 @@ class BatchAnalyze: rotate_labels = table_orientation_cls_model.batch_predict( table_res_list_all_page, det_batch_size=self.batch_ratio * OCR_DET_BASE_BATCH_SIZE, + tqdm_enable=True, ) if len(rotate_labels) != len(table_res_list_all_page): raise ValueError( diff --git a/mineru/model/ocr/pytorch_paddle.py b/mineru/model/ocr/pytorch_paddle.py index 08db11ed..48743bfe 100644 --- a/mineru/model/ocr/pytorch_paddle.py +++ b/mineru/model/ocr/pytorch_paddle.py @@ -289,6 +289,7 @@ class PytorchPaddleOCR(TextSystem): mfd_res=None, tqdm_enable=False, tqdm_desc="OCR-rec Predict", + tqdm_progress_bar=None, ): assert isinstance(img, (np.ndarray, list, str, bytes)) if isinstance(img, list) and det == True: @@ -338,7 +339,12 @@ class PytorchPaddleOCR(TextSystem): if not isinstance(img, list): img = preprocess_image(img) img = [img] - rec_res, elapse = self.text_recognizer(img, tqdm_enable=tqdm_enable, tqdm_desc=tqdm_desc) + rec_res, elapse = self.text_recognizer( + img, + tqdm_enable=tqdm_enable, + tqdm_desc=tqdm_desc, + tqdm_progress_bar=tqdm_progress_bar, + ) # logger.debug("rec_res num : {}, elapsed : {}".format(len(rec_res), elapse)) ocr_res.append(rec_res) return ocr_res diff --git a/mineru/model/table/cls/mineru_table_ori_cls.py b/mineru/model/table/cls/mineru_table_ori_cls.py index 2207fd16..e31da4fb 100644 --- a/mineru/model/table/cls/mineru_table_ori_cls.py +++ b/mineru/model/table/cls/mineru_table_ori_cls.py @@ -5,6 +5,7 @@ from collections import defaultdict from typing import List, Dict import cv2 import numpy as np +from tqdm import tqdm # 旋转候选门控回到旧规则,先尽量召回疑似旋转表,再由 OCR rec 评分决定最终角度。 @@ -227,6 +228,26 @@ class MineruTableOrientationClsModel: return self._select_rotation_label_by_scores(score_by_label) + @staticmethod + def _set_progress_description(progress_bar, desc: str): + """切换复用进度条的阶段描述,并兼容测试替身和 tqdm 对象。""" + if progress_bar is None: + return + if hasattr(progress_bar, "set_description"): + progress_bar.set_description(desc) + else: + progress_bar.desc = desc + + @staticmethod + def _extend_progress_total(progress_bar, count: int): + """按新增工作量动态扩展总进度,保证最终 total 覆盖 det/score/rec。""" + if progress_bar is None or count <= 0: + return + current_total = progress_bar.total if progress_bar.total is not None else progress_bar.n + progress_bar.total = current_total + count + if hasattr(progress_bar, "refresh"): + progress_bar.refresh() + @classmethod def _collect_portrait_image_groups( cls, @@ -291,6 +312,7 @@ class MineruTableOrientationClsModel: resolution_groups: Dict[tuple[int, int], list[Dict]], det_batch_size: int, resolution_group_stride: int, + progress_bar=None, ) -> list[Dict]: """对表格批量做 OCR det,并筛选需要进入多角度评分的候选。""" rotated_imgs = [] @@ -304,11 +326,14 @@ class MineruTableOrientationClsModel: for img_info, (dt_boxes, _elapse) in zip(group_imgs, batch_results): if self._is_rotation_candidate_by_det_boxes(dt_boxes): rotated_imgs.append(img_info) + if progress_bar is not None: + progress_bar.update(len(group_imgs)) return rotated_imgs def _build_score_tasks_for_candidates( self, rotated_imgs: list[Dict], + progress_bar=None, ) -> tuple[list[tuple[Dict, list[Dict]]], list[np.ndarray]]: """为所有旋转候选构造三角度评分任务,并汇总成一次 OCR rec 输入。""" img_score_tasks = [] @@ -322,9 +347,17 @@ class MineruTableOrientationClsModel: task["crop_start"] = crop_start task["crop_end"] = crop_end img_score_tasks.append((img_info, tasks)) + if progress_bar is not None: + progress_bar.update(1) return img_score_tasks, all_crop_imgs - def _recognize_orientation_crops(self, all_crop_imgs: list[np.ndarray]) -> list: + def _recognize_orientation_crops( + self, + all_crop_imgs: list[np.ndarray], + tqdm_enable: bool = False, + tqdm_desc: str = "Table orientation", + tqdm_progress_bar=None, + ) -> list: """对所有候选角度 crop 合并执行 OCR rec,返回可按 slice 回填的结果。""" if not all_crop_imgs: return [] @@ -333,19 +366,38 @@ class MineruTableOrientationClsModel: all_crop_imgs, det=False, rec=True, + tqdm_enable=tqdm_enable and tqdm_progress_bar is None, + tqdm_desc=f"{tqdm_desc} rec", + tqdm_progress_bar=tqdm_progress_bar, ) return rec_ocr_res[0] if rec_ocr_res else [] - def _score_rotation_candidates(self, rotated_imgs: list[Dict]) -> Dict[int, str]: + def _score_rotation_candidates( + self, + rotated_imgs: list[Dict], + tqdm_enable: bool = False, + tqdm_desc: str = "Table orientation", + progress_bar=None, + ) -> Dict[int, str]: """批量评分旋转候选,并返回原始表格下标到最终角度标签的映射。""" if not rotated_imgs: return {} label_by_index = {} img_score_tasks, all_crop_imgs = self._build_score_tasks_for_candidates( - rotated_imgs + rotated_imgs, + progress_bar=progress_bar, + ) + self._extend_progress_total(progress_bar, len(all_crop_imgs)) + self._set_progress_description(progress_bar, f"{tqdm_desc} rec") + if progress_bar is not None and hasattr(progress_bar, "refresh"): + progress_bar.refresh() + rec_res = self._recognize_orientation_crops( + all_crop_imgs, + tqdm_enable=tqdm_enable, + tqdm_desc=tqdm_desc, + tqdm_progress_bar=progress_bar, ) - rec_res = self._recognize_orientation_crops(all_crop_imgs) for img_info, tasks in img_score_tasks: score_by_label = self._score_orientation_tasks_with_rec(tasks, rec_res) @@ -358,6 +410,8 @@ class MineruTableOrientationClsModel: self, imgs: List[Dict], det_batch_size: int, + tqdm_enable: bool = False, + tqdm_desc: str = "Table orientation", ) -> List[str]: """ 批量预测传入表格图片的旋转角度,只返回角度,不修改输入图片。 @@ -368,13 +422,38 @@ class MineruTableOrientationClsModel: imgs, RESOLUTION_GROUP_STRIDE, ) - rotated_imgs = self._detect_rotation_candidates( - resolution_groups, - det_batch_size, - RESOLUTION_GROUP_STRIDE, - ) - label_by_index = self._score_rotation_candidates(rotated_imgs) - for index, label in label_by_index.items(): - rotate_labels[index] = label + total_images = sum(len(group_imgs) for group_imgs in resolution_groups.values()) + progress_bar = None + if tqdm_enable: + progress_bar = tqdm( + total=total_images, + desc=f"{tqdm_desc} det", + leave=True, + ) + try: + rotated_imgs = self._detect_rotation_candidates( + resolution_groups, + det_batch_size, + RESOLUTION_GROUP_STRIDE, + progress_bar=progress_bar, + ) + self._extend_progress_total(progress_bar, len(rotated_imgs)) + self._set_progress_description(progress_bar, f"{tqdm_desc} score") + if progress_bar is not None and hasattr(progress_bar, "refresh"): + progress_bar.refresh() + label_by_index = self._score_rotation_candidates( + rotated_imgs, + tqdm_enable=tqdm_enable, + tqdm_desc=tqdm_desc, + progress_bar=progress_bar, + ) + for index, label in label_by_index.items(): + rotate_labels[index] = label + finally: + self._set_progress_description(progress_bar, tqdm_desc) + if progress_bar is not None and hasattr(progress_bar, "refresh"): + progress_bar.refresh() + if progress_bar is not None: + progress_bar.close() return rotate_labels diff --git a/mineru/model/utils/tools/infer/predict_rec.py b/mineru/model/utils/tools/infer/predict_rec.py index 8bcd8555..8ace3943 100644 --- a/mineru/model/utils/tools/infer/predict_rec.py +++ b/mineru/model/utils/tools/infer/predict_rec.py @@ -288,7 +288,13 @@ class TextRecognizer(BaseOCRV20): return img - def __call__(self, img_list, tqdm_enable=False, tqdm_desc="OCR-rec Predict"): + def __call__( + self, + img_list, + tqdm_enable=False, + tqdm_desc="OCR-rec Predict", + tqdm_progress_bar=None, + ): img_num = len(img_list) # Calculate the aspect ratio of all text bars width_list = [] @@ -301,8 +307,14 @@ class TextRecognizer(BaseOCRV20): rec_res = [['', 0.0]] * img_num batch_num = self.rec_batch_num elapse = 0 - # for beg_img_no in range(0, img_num, batch_num): - with tqdm(total=img_num, desc=tqdm_desc, disable=not tqdm_enable) as pbar: + # tqdm_progress_bar 由上层复用时,不再创建内部 OCR-rec 进度条。 + pbar = tqdm_progress_bar + should_close_pbar = False + if pbar is None: + pbar = tqdm(total=img_num, desc=tqdm_desc, disable=not tqdm_enable) + should_close_pbar = True + + try: index = 0 for beg_img_no in range(0, img_num, batch_num): end_img_no = min(img_num, beg_img_no + batch_num) @@ -428,6 +440,9 @@ class TextRecognizer(BaseOCRV20): current_batch_size = min(batch_num, img_num - index * batch_num) index += 1 pbar.update(current_batch_size) + finally: + if should_close_pbar: + pbar.close() # Fix NaN values in recognition results for i in range(len(rec_res)): From 743a27d29d1a14556b5a519dfe8eadeb12cfc900 Mon Sep 17 00:00:00 2001 From: myhloli Date: Sat, 6 Jun 2026 04:01:02 +0800 Subject: [PATCH 16/22] feat: add hybrid mode support to middle JSON initialization and processing functions --- mineru/backend/hybrid/hybrid_analyze.py | 14 +++- .../hybrid_model_output_to_middle_json.py | 9 +- mineru/backend/utils/para_block_utils.py | 83 ++++++++++++++++++- mineru/utils/title_level_postprocess.py | 1 + 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/mineru/backend/hybrid/hybrid_analyze.py b/mineru/backend/hybrid/hybrid_analyze.py index 9e364288..ee2e8565 100644 --- a/mineru/backend/hybrid/hybrid_analyze.py +++ b/mineru/backend/hybrid/hybrid_analyze.py @@ -930,7 +930,11 @@ def doc_analyze( _vlm_ocr_enable = _should_enable_vlm_ocr(_ocr_enable, language, inline_formula_enable) pdf_doc = open_pdfium_document(pdfium.PdfDocument, pdf_bytes) - middle_json = init_middle_json(_ocr_enable, _vlm_ocr_enable) + middle_json = init_middle_json( + _ocr_enable, + _vlm_ocr_enable, + hybrid_mode=mode, + ) model_list = [] doc_closed = False hybrid_pipeline_model = None @@ -1101,6 +1105,7 @@ def doc_analyze( hybrid_pipeline_model, _ocr_enable, _vlm_ocr_enable, + hybrid_mode=mode, ) close_pdfium_document(pdf_doc) doc_closed = True @@ -1138,7 +1143,11 @@ async def aio_doc_analyze( _vlm_ocr_enable = _should_enable_vlm_ocr(_ocr_enable, language, inline_formula_enable) pdf_doc = open_pdfium_document(pdfium.PdfDocument, pdf_bytes) - middle_json = init_middle_json(_ocr_enable, _vlm_ocr_enable) + middle_json = init_middle_json( + _ocr_enable, + _vlm_ocr_enable, + hybrid_mode=mode, + ) model_list = [] doc_closed = False hybrid_pipeline_model = None @@ -1317,6 +1326,7 @@ async def aio_doc_analyze( hybrid_pipeline_model, _ocr_enable, _vlm_ocr_enable, + hybrid_mode=mode, ) close_pdfium_document(pdf_doc) doc_closed = True diff --git a/mineru/backend/hybrid/hybrid_model_output_to_middle_json.py b/mineru/backend/hybrid/hybrid_model_output_to_middle_json.py index 06821c64..f6da7d4b 100644 --- a/mineru/backend/hybrid/hybrid_model_output_to_middle_json.py +++ b/mineru/backend/hybrid/hybrid_model_output_to_middle_json.py @@ -180,10 +180,11 @@ def _normalize_split_title_blocks(pdf_info_list): block["level"] = title_level -def init_middle_json(_ocr_enable, _vlm_ocr_enable): +def init_middle_json(_ocr_enable, _vlm_ocr_enable, hybrid_mode="pro"): return { "pdf_info": [], "_backend": "hybrid", + "_hybrid_mode": hybrid_mode, "_ocr_enable": _ocr_enable, "_vlm_ocr_enable": _vlm_ocr_enable, "_version_name": __version__ @@ -260,12 +261,13 @@ def apply_server_side_postprocess( _apply_post_ocr(pdf_info_list, hybrid_pipeline_model) -def finalize_middle_json_from_preproc(pdf_info_list): +def finalize_middle_json_from_preproc(pdf_info_list, hybrid_mode="pro"): """从 Hybrid preproc_blocks 执行完整 finalize,供服务端完整路径和客户端复用。""" build_para_blocks_from_preproc(pdf_info_list) merge_para_text_blocks( pdf_info_list, auto_merge_by_det=True, + auto_merge_vertical_by_det=hybrid_mode == "flash", ) table_enable = get_table_enable(os.getenv('MINERU_VLM_TABLE_ENABLE', 'True').lower() == 'true') @@ -282,6 +284,7 @@ def finalize_middle_json( hybrid_pipeline_model, _ocr_enable, _vlm_ocr_enable, + hybrid_mode="pro", ): """保持旧入口语义:服务端先做必要 post-OCR,再执行完整 finalize。""" apply_server_side_postprocess( @@ -290,7 +293,7 @@ def finalize_middle_json( _ocr_enable, _vlm_ocr_enable, ) - finalize_middle_json_from_preproc(pdf_info_list) + finalize_middle_json_from_preproc(pdf_info_list, hybrid_mode=hybrid_mode) def result_to_middle_json( diff --git a/mineru/backend/utils/para_block_utils.py b/mineru/backend/utils/para_block_utils.py index d66f2dac..315ce3e6 100644 --- a/mineru/backend/utils/para_block_utils.py +++ b/mineru/backend/utils/para_block_utils.py @@ -2,6 +2,7 @@ import copy from mineru.utils.enum_class import BlockType, SplitFlag +from mineru.utils.span_block_fix import is_vertical_text_block_by_spans LINE_STOP_FLAG = ('.', '!', '?', '。', '!', '?', ')', ')', '"', '”', ':', ':', ';', ';') @@ -43,7 +44,11 @@ def build_para_blocks_from_preproc(pdf_info_list): page_info["para_blocks"] = copy.deepcopy(page_info.get("preproc_blocks", [])) -def merge_para_text_blocks(pdf_info_list, auto_merge_by_det=False): +def merge_para_text_blocks( + pdf_info_list, + auto_merge_by_det=False, + auto_merge_vertical_by_det=False, +): ordered_blocks = [] for page_info in pdf_info_list: page_idx = page_info.get("page_idx") @@ -62,6 +67,7 @@ def merge_para_text_blocks(pdf_info_list, auto_merge_by_det=False): current_page_idx, current_block, auto_merge_by_det, + auto_merge_vertical_by_det, ) elif current_type == BlockType.LIST: if not current_block.get("blocks"): @@ -79,6 +85,7 @@ def _merge_current_text_block( current_page_idx, current_block, auto_merge_by_det, + auto_merge_vertical_by_det, ): """处理当前 text block 的 merge_prev 候选合并和 Hybrid det 自动合并。""" previous_block = None @@ -107,6 +114,7 @@ def _merge_current_text_block( if not can_auto_merge_text_blocks( current_block, previous_text_block, + allow_vertical_blocks=auto_merge_vertical_by_det, ): previous_block = None @@ -148,6 +156,7 @@ def can_auto_merge_text_blocks( current_block, previous_block, allow_single_line_blocks=False, + allow_vertical_blocks=False, ): """按段落首尾文本和行几何规则判断 text 是否可合并。""" current_lines = current_block.get("lines", []) @@ -162,6 +171,20 @@ def can_auto_merge_text_blocks( ): return False + if ( + allow_vertical_blocks + and _is_vertical_text_block_by_lines(current_metric_lines) + and _is_vertical_text_block_by_lines(previous_metric_lines) + ): + return _can_auto_merge_vertical_text_blocks( + current_block, + previous_block, + current_lines, + previous_lines, + current_metric_lines, + previous_metric_lines, + ) + first_metric_line = current_metric_lines[0] last_metric_line = previous_metric_lines[-1] first_line_height = _line_height(first_metric_line) @@ -339,6 +362,14 @@ def _line_height(line): return bbox[3] - bbox[1] +def _line_width(line): + """计算行或纵排列的宽度,供纵排几何规则使用。""" + bbox = line.get("bbox") + if not bbox: + return 0 + return bbox[2] - bbox[0] + + def _build_bbox_fs(block, lines): if lines: return [ @@ -379,6 +410,56 @@ def _has_mergeable_block_bbox_relation(current_block, previous_block): return current_block["bbox"][1] < previous_block["bbox"][3] +def _is_vertical_text_block_by_lines(lines): + """使用当前几何行中的 spans 判断文本块是否为纵排。""" + spans = [ + span + for line in lines + for span in line.get("spans", []) + ] + return is_vertical_text_block_by_spans(spans) + + +def _can_auto_merge_vertical_text_blocks( + current_block, + previous_block, + current_lines, + previous_lines, + current_metric_lines, + previous_metric_lines, +): + """复刻 pipeline 纵排文本块合并规则,几何优先使用 OCR det 行。""" + first_metric_line = current_metric_lines[0] + last_metric_line = previous_metric_lines[-1] + first_line_width = _line_width(first_metric_line) + last_line_width = _line_width(last_metric_line) + if first_line_width <= 0 or last_line_width <= 0: + return False + + current_bbox_fs = _build_bbox_fs(current_block, current_metric_lines) + previous_bbox_fs = _build_bbox_fs(previous_block, previous_metric_lines) + if abs(current_bbox_fs[1] - first_metric_line["bbox"][1]) >= first_line_width / 2: + return False + if abs(previous_bbox_fs[3] - last_metric_line["bbox"][3]) >= last_line_width: + return False + + first_content = _first_non_empty_content(current_lines) + last_content = _last_non_empty_content(previous_lines) + if not first_content or not last_content: + return False + if last_content.endswith(LINE_STOP_FLAG): + return False + if first_content[0].isdigit() or first_content[0].isupper(): + return False + + current_metric_height = current_bbox_fs[3] - current_bbox_fs[1] + previous_metric_height = previous_bbox_fs[3] - previous_bbox_fs[1] + min_metric_height = min(current_metric_height, previous_metric_height) + if min_metric_height <= 0: + return False + return abs(current_metric_height - previous_metric_height) < min_metric_height + + def _cleanup_block_internal_metadata(block): """递归清理只供 finalize 内部流程使用的临时字段。""" for metadata_key in INTERNAL_BLOCK_METADATA_KEYS: diff --git a/mineru/utils/title_level_postprocess.py b/mineru/utils/title_level_postprocess.py index 76a14dd7..634c8925 100644 --- a/mineru/utils/title_level_postprocess.py +++ b/mineru/utils/title_level_postprocess.py @@ -74,6 +74,7 @@ def finalize_client_side_middle_json(middle_json: dict[str, Any]) -> dict[str, A finalize_middle_json_from_preproc( pdf_info, + hybrid_mode=middle_json.get("_hybrid_mode", "pro"), ) return middle_json From a3bd9d02236456fa5d701ae3c6e6088d9d59e8a1 Mon Sep 17 00:00:00 2001 From: myhloli Date: Sat, 6 Jun 2026 04:03:11 +0800 Subject: [PATCH 17/22] feat: update default backend to hybrid flash engine in backend options --- mineru/cli/backend_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mineru/cli/backend_options.py b/mineru/cli/backend_options.py index 23f2d67e..5796d562 100644 --- a/mineru/cli/backend_options.py +++ b/mineru/cli/backend_options.py @@ -8,7 +8,7 @@ BACKEND_VLM_HTTP_CLIENT = "vlm-http-client" BACKEND_HYBRID_HTTP_CLIENT = "hybrid-http-client" BACKEND_HYBRID_FLASH_HTTP_CLIENT = "hybrid-flash-http-client" -DEFAULT_BACKEND = BACKEND_HYBRID_ENGINE +DEFAULT_BACKEND = BACKEND_HYBRID_FLASH_ENGINE LOCAL_BACKEND_CHOICES = ( BACKEND_PIPELINE, From 3844f1c93d738c5c13526d561dcfc9b0dc4cbf62 Mon Sep 17 00:00:00 2001 From: myhloli Date: Sat, 6 Jun 2026 04:13:34 +0800 Subject: [PATCH 18/22] feat: add backend info selection logic for hybrid and hybrid-flash options --- mineru/cli/gradio_app.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/mineru/cli/gradio_app.py b/mineru/cli/gradio_app.py index aa73b759..33229505 100644 --- a/mineru/cli/gradio_app.py +++ b/mineru/cli/gradio_app.py @@ -338,6 +338,21 @@ def is_http_client_backend(backend_choice): return isinstance(backend_choice, str) and backend_choice.endswith("-http-client") +def select_backend_info_key(backend_choice): + """根据解析后端选择说明文案的 i18n key,保证 flash 与普通 hybrid 可分别描述。""" + if not isinstance(backend_choice, str): + return "backend_info_default" + if backend_choice.startswith("vlm"): + return "backend_info_vlm" + if backend_choice == "pipeline": + return "backend_info_pipeline" + if backend_choice.startswith("hybrid-flash"): + return "backend_info_hybrid_flash" + if backend_choice.startswith("hybrid"): + return "backend_info_hybrid" + return "backend_info_default" + + def resolve_status_step_index(status_lines): """根据现有状态日志推断步骤面板中当前应高亮的步骤索引。""" if not status_lines: @@ -1594,6 +1609,7 @@ def main(ctx, "backend_info_vlm": "High-precision parsing via VLM, supports Chinese and English documents only.", "backend_info_pipeline": "Traditional Multi-model pipeline parsing, supports multiple languages, hallucination-free.", "backend_info_hybrid": "High-precision hybrid parsing, supports multiple languages.", + "backend_info_hybrid_flash": "Fast, high-precision hybrid parsing, supports multiple languages.", "backend_info_default": "Select the backend engine for document parsing.", }, zh={ @@ -1662,6 +1678,7 @@ def main(ctx, "backend_info_vlm": "多模态大模型高精度解析,仅支持中英文文档。", "backend_info_pipeline": "传统多模型管道解析,支持多语言,无幻觉。", "backend_info_hybrid": "高精度混合解析,支持多语言。", + "backend_info_hybrid_flash": "高精度快速混合解析,支持多语言。", "backend_info_default": "选择文档解析的后端引擎。", }, ) @@ -1688,14 +1705,7 @@ def main(ctx, return "" def get_backend_info(backend_choice): - if backend_choice.startswith("vlm"): - return i18n("backend_info_vlm") - elif backend_choice == "pipeline": - return i18n("backend_info_pipeline") - elif backend_choice.startswith("hybrid"): - return i18n("backend_info_hybrid") - else: - return i18n("backend_info_default") + return i18n(select_backend_info_key(backend_choice)) # 更新界面函数 def update_interface(backend_choice): From 248e5e82786a2142b05edc69b6fed6746d4108f5 Mon Sep 17 00:00:00 2001 From: myhloli Date: Sat, 6 Jun 2026 04:41:20 +0800 Subject: [PATCH 19/22] feat: implement filtering of inline formulas within specified visual containers --- mineru/backend/hybrid/hybrid_analyze.py | 69 +++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/mineru/backend/hybrid/hybrid_analyze.py b/mineru/backend/hybrid/hybrid_analyze.py index ee2e8565..5ad1fd72 100644 --- a/mineru/backend/hybrid/hybrid_analyze.py +++ b/mineru/backend/hybrid/hybrid_analyze.py @@ -63,6 +63,7 @@ not_extract_list = [item.value for item in NotExtractType] HYBRID_OCR_DET_TEXT_TYPES = set(not_extract_list) HYBRID_ANALYZE_MODES = {"pro", "flash"} FLASH_LAYOUT_VISUAL_LABELS = {"image", "chart", "seal"} +INLINE_FORMULA_CONTAINER_LABELS = {"table", "image", "chart", "display_formula"} FLASH_LAYOUT_LABEL_TO_VLM_TYPE = { "abstract": BlockType.TEXT, "algorithm": BlockType.CODE, @@ -451,6 +452,73 @@ def _formula_item_to_pixel_bbox(item): return None +def _layout_item_to_float_bbox(item): + """校验并读取layout检测框,异常或无效bbox返回None。""" + bbox = item.get("bbox") + if bbox is None or len(bbox) != 4: + return None + + try: + x0, y0, x1, y1 = [float(v) for v in bbox] + except (TypeError, ValueError): + return None + + if x1 < x0 or y1 < y0: + return None + + return [x0, y0, x1, y1] + + +def _bbox_center_point(bbox): + """计算bbox中心点,用于判断行内公式是否落入视觉容器。""" + return (float(bbox[0] + bbox[2]) / 2.0, float(bbox[1] + bbox[3]) / 2.0) + + +def _is_point_inside_bbox(point, bbox): + """判断点是否位于bbox内部,边界点按内部处理。""" + x, y = point + return bbox[0] <= x <= bbox[2] and bbox[1] <= y <= bbox[3] + + +def _is_inline_formula_inside_container(inline_formula_bbox, container_bboxes): + """判断行内公式中心点是否落入任一视觉/行间公式容器。""" + inline_formula_center = _bbox_center_point(inline_formula_bbox) + return any( + _is_point_inside_bbox(inline_formula_center, container_bbox) + for container_bbox in container_bboxes + ) + + +def _filter_inline_formulas_inside_containers(images_layout_res): + """原地移除位于table/image/chart/display_formula内的行内公式。""" + for layout_res in images_layout_res: + container_bboxes = [] + for res in layout_res: + if res.get("label") not in INLINE_FORMULA_CONTAINER_LABELS: + continue + bbox = _layout_item_to_float_bbox(res) + if bbox is not None: + container_bboxes.append(bbox) + + if not container_bboxes: + continue + + kept_layout_res = [] + for res in layout_res: + if res.get("label") != "inline_formula": + kept_layout_res.append(res) + continue + + inline_formula_bbox = _layout_item_to_float_bbox(res) + if inline_formula_bbox is None or not _is_inline_formula_inside_container( + inline_formula_bbox, + container_bboxes, + ): + kept_layout_res.append(res) + + layout_res[:] = kept_layout_res + + def _build_inline_formula_inputs(images_layout_res): inline_formula_inputs = [] for layout_res in images_layout_res: @@ -613,6 +681,7 @@ def _predict_layout_for_window( images_pil_list, batch_ratio, ) + _filter_inline_formulas_inside_containers(images_layout_res) return images_layout_res, hybrid_pipeline_model From 7424b889906b9cd6c5865ab50b7ee78fc4f5316d Mon Sep 17 00:00:00 2001 From: myhloli Date: Sat, 6 Jun 2026 05:10:33 +0800 Subject: [PATCH 20/22] feat: enhance VLM layout processing with image analysis toggle and sub-type handling --- mineru/backend/hybrid/hybrid_analyze.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/mineru/backend/hybrid/hybrid_analyze.py b/mineru/backend/hybrid/hybrid_analyze.py index 5ad1fd72..97cc5566 100644 --- a/mineru/backend/hybrid/hybrid_analyze.py +++ b/mineru/backend/hybrid/hybrid_analyze.py @@ -95,13 +95,23 @@ def _validate_hybrid_mode(mode: str) -> str: return mode -def _vlm_type_for_flash_layout_label(label: str | None) -> str | None: - """将 pipeline layout 标签映射为 mineru-vl-utils 支持的 VLM 抽取类型。""" +def _vlm_type_for_flash_layout_label(label: str | None, image_analysis: bool = True) -> str | None: + """将 pipeline layout 标签按 image_analysis 开关映射为 VLM 抽取类型。""" if label in FLASH_LAYOUT_VISUAL_LABELS: + if not image_analysis and label == "chart": + return BlockType.CHART return BlockType.IMAGE return FLASH_LAYOUT_LABEL_TO_VLM_TYPE.get(label) +def _apply_flash_visual_sub_type(block, label: str | None, image_analysis: bool): + """为不开启 image analysis 的视觉块补充下游需要透传的子类型。""" + if image_analysis: + return + if label == "seal": + block["sub_type"] = "seal" + + def _is_hybrid_ocr_det_candidate(block): """判断 Hybrid 文本类块是否需要 OCR det 生成行级视觉信息。""" return (block.get("type") or block.get("label")) in HYBRID_OCR_DET_TEXT_TYPES @@ -369,12 +379,12 @@ def _normalize_flash_vlm_angle(angle): return 0 -def _build_flash_vlm_layout_blocks(layout_dets, page_width, page_height): +def _build_flash_vlm_layout_blocks(layout_dets, page_width, page_height, image_analysis=True): """用 pipeline layout 构造 VLM 外部 layout 输入,跳过 VLM 自身 layout 解析。""" blocks = [] for layout_det in layout_dets or []: label = layout_det.get("label") - vlm_type = _vlm_type_for_flash_layout_label(label) + vlm_type = _vlm_type_for_flash_layout_label(label, image_analysis) if vlm_type is None: continue bbox = _layout_det_bbox_to_unit(layout_det, page_width, page_height) @@ -390,6 +400,7 @@ def _build_flash_vlm_layout_blocks(layout_dets, page_width, page_height): except AssertionError as exc: logger.warning(f"Skip invalid Hybrid flash VLM block: {layout_det}, error: {exc}") continue + _apply_flash_visual_sub_type(block, label, image_analysis) blocks.append(block) return blocks @@ -1063,6 +1074,7 @@ def doc_analyze( page_layout_res, pil_img.width, pil_img.height, + image_analysis=image_analysis, ) for page_layout_res, pil_img in zip(images_layout_res, images_pil_list) ] @@ -1277,6 +1289,7 @@ async def aio_doc_analyze( page_layout_res, pil_img.width, pil_img.height, + image_analysis=image_analysis, ) for page_layout_res, pil_img in zip(images_layout_res, images_pil_list) ] From c85b0dcbaf68fd2f0dd1b4e30a1b9325ef15d13a Mon Sep 17 00:00:00 2001 From: myhloli Date: Sat, 6 Jun 2026 12:20:32 +0800 Subject: [PATCH 21/22] feat: simplify flash layout processing by removing image analysis toggle from relevant functions --- mineru/backend/hybrid/hybrid_analyze.py | 26 ++++++++++--------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/mineru/backend/hybrid/hybrid_analyze.py b/mineru/backend/hybrid/hybrid_analyze.py index 97cc5566..0a1213c0 100644 --- a/mineru/backend/hybrid/hybrid_analyze.py +++ b/mineru/backend/hybrid/hybrid_analyze.py @@ -62,7 +62,6 @@ LAYOUT_TITLE_SPLIT_OVERLAP_THRESHOLD = 0.8 not_extract_list = [item.value for item in NotExtractType] HYBRID_OCR_DET_TEXT_TYPES = set(not_extract_list) HYBRID_ANALYZE_MODES = {"pro", "flash"} -FLASH_LAYOUT_VISUAL_LABELS = {"image", "chart", "seal"} INLINE_FORMULA_CONTAINER_LABELS = {"table", "image", "chart", "display_formula"} FLASH_LAYOUT_LABEL_TO_VLM_TYPE = { "abstract": BlockType.TEXT, @@ -83,6 +82,9 @@ FLASH_LAYOUT_LABEL_TO_VLM_TYPE = { "vertical_text": BlockType.TEXT, "figure_title": BlockType.IMAGE_CAPTION, "vision_footnote": BlockType.IMAGE_FOOTNOTE, + "image": BlockType.IMAGE, + "chart": BlockType.CHART, + "seal": BlockType.IMAGE, "table": BlockType.TABLE, "display_formula": BlockType.EQUATION, } @@ -95,19 +97,13 @@ def _validate_hybrid_mode(mode: str) -> str: return mode -def _vlm_type_for_flash_layout_label(label: str | None, image_analysis: bool = True) -> str | None: - """将 pipeline layout 标签按 image_analysis 开关映射为 VLM 抽取类型。""" - if label in FLASH_LAYOUT_VISUAL_LABELS: - if not image_analysis and label == "chart": - return BlockType.CHART - return BlockType.IMAGE +def _vlm_type_for_flash_layout_label(label: str | None) -> str | None: + """将 pipeline layout 标签映射为 mineru-vl-utils 支持的 VLM 抽取类型。""" return FLASH_LAYOUT_LABEL_TO_VLM_TYPE.get(label) -def _apply_flash_visual_sub_type(block, label: str | None, image_analysis: bool): - """为不开启 image analysis 的视觉块补充下游需要透传的子类型。""" - if image_analysis: - return +def _apply_flash_visual_sub_type(block, label: str | None): + """为视觉块补充下游需要透传的子类型。""" if label == "seal": block["sub_type"] = "seal" @@ -379,12 +375,12 @@ def _normalize_flash_vlm_angle(angle): return 0 -def _build_flash_vlm_layout_blocks(layout_dets, page_width, page_height, image_analysis=True): +def _build_flash_vlm_layout_blocks(layout_dets, page_width, page_height): """用 pipeline layout 构造 VLM 外部 layout 输入,跳过 VLM 自身 layout 解析。""" blocks = [] for layout_det in layout_dets or []: label = layout_det.get("label") - vlm_type = _vlm_type_for_flash_layout_label(label, image_analysis) + vlm_type = _vlm_type_for_flash_layout_label(label) if vlm_type is None: continue bbox = _layout_det_bbox_to_unit(layout_det, page_width, page_height) @@ -400,7 +396,7 @@ def _build_flash_vlm_layout_blocks(layout_dets, page_width, page_height, image_a except AssertionError as exc: logger.warning(f"Skip invalid Hybrid flash VLM block: {layout_det}, error: {exc}") continue - _apply_flash_visual_sub_type(block, label, image_analysis) + _apply_flash_visual_sub_type(block, label) blocks.append(block) return blocks @@ -1074,7 +1070,6 @@ def doc_analyze( page_layout_res, pil_img.width, pil_img.height, - image_analysis=image_analysis, ) for page_layout_res, pil_img in zip(images_layout_res, images_pil_list) ] @@ -1289,7 +1284,6 @@ async def aio_doc_analyze( page_layout_res, pil_img.width, pil_img.height, - image_analysis=image_analysis, ) for page_layout_res, pil_img in zip(images_layout_res, images_pil_list) ] From 8809ce007a4c00cb66aceedf350118e35bdb2b37 Mon Sep 17 00:00:00 2001 From: myhloli Date: Sat, 6 Jun 2026 12:39:47 +0800 Subject: [PATCH 22/22] feat: update backend options in CLI tools to include hybrid-flash-engine as default --- docs/en/usage/cli_tools.md | 4 ++-- docs/zh/usage/cli_tools.md | 4 ++-- mineru/cli/client.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/en/usage/cli_tools.md b/docs/en/usage/cli_tools.md index 1c254c8f..25c30d03 100644 --- a/docs/en/usage/cli_tools.md +++ b/docs/en/usage/cli_tools.md @@ -12,8 +12,8 @@ Options: -o, --output PATH Output directory (required) --api-url TEXT MinerU FastAPI base URL; if omitted, `mineru` starts a temporary local `mineru-api` -m, --method [auto|txt|ocr] Parsing method: auto (default), txt, ocr (pipeline and hybrid* backend only) - -b, --backend [pipeline|hybrid-engine|hybrid-http-client|vlm-engine|vlm-http-client] - Parsing backend (default: hybrid-engine) + -b, --backend [pipeline|vlm-engine|hybrid-engine|hybrid-flash-engine|vlm-http-client|hybrid-http-client|hybrid-flash-http-client] + Parsing backend (default: hybrid-flash-engine) -l, --lang [ch|ch_server|ch_lite|en|korean|japan|chinese_cht|ta|te|ka|th|el|latin|arabic|east_slavic|cyrillic|devanagari] Specify document language (improves OCR accuracy, pipeline and hybrid* backend only) -u, --url TEXT OpenAI-compatible backend URL passed through to the server when using http-client diff --git a/docs/zh/usage/cli_tools.md b/docs/zh/usage/cli_tools.md index 0c894990..52f52899 100644 --- a/docs/zh/usage/cli_tools.md +++ b/docs/zh/usage/cli_tools.md @@ -12,8 +12,8 @@ Options: -o, --output PATH 输出目录(必填) --api-url TEXT MinerU FastAPI 服务地址;不传时自动拉起本地临时 mineru-api -m, --method [auto|txt|ocr] 解析方法:auto(默认)、txt、ocr(仅用于 pipeline 与 hybrid* 后端) - -b, --backend [pipeline|hybrid-engine|hybrid-http-client|vlm-engine|vlm-http-client] - 解析后端(默认为 hybrid-engine) + -b, --backend [pipeline|vlm-engine|hybrid-engine|hybrid-flash-engine|vlm-http-client|hybrid-http-client|hybrid-flash-http-client] + 解析后端(默认为 hybrid-flash-engine) -l, --lang [ch|ch_server|ch_lite|en|korean|japan|chinese_cht|ta|te|ka|th|el|latin|arabic|east_slavic|cyrillic|devanagari] 指定文档语言(可提升 OCR 准确率,仅用于 pipeline 与 hybrid* 后端) -u, --url TEXT 当使用 http-client 时,传给服务端后端的 OpenAI 兼容地址 diff --git a/mineru/cli/client.py b/mineru/cli/client.py index 1beac233..02dcb1bd 100644 --- a/mineru/cli/client.py +++ b/mineru/cli/client.py @@ -1059,7 +1059,7 @@ async def run_orchestrated_cli( hybrid-flash-engine: Hybrid flash mode via local computing power. hybrid-http-client: High accuracy but requires a little local computing power(client suitable for openai-compatible servers). hybrid-flash-http-client: Hybrid flash mode via remote computing power(client suitable for openai-compatible servers). - Without method specified, hybrid-engine will be used by default.""", + Without backend specified, hybrid-flash-engine will be used by default.""", ) @click.option( "-l",