diff --git a/demo/demo.py b/demo/demo.py index e83d05a3..69bcebf1 100644 --- a/demo/demo.py +++ b/demo/demo.py @@ -51,10 +51,12 @@ def build_form_data( start_page_id: int, end_page_id: int | None, image_analysis: bool = True, + effort: str = "medium", ) -> dict[str, str | list[str]]: return _api_client.build_parse_request_form_data( lang_list=[language], backend=backend, + effort=effort, parse_method=parse_method, formula_enable=formula_enable, table_enable=table_enable, @@ -101,6 +103,7 @@ async def run_demo( formula_enable: bool = True, table_enable: bool = True, image_analysis: bool = True, + effort: str = "medium", server_url: str | None = None, start_page_id: int = 0, end_page_id: int | None = None, @@ -121,6 +124,7 @@ async def run_demo( formula_enable=formula_enable, table_enable=table_enable, image_analysis=image_analysis, + effort=effort, server_url=server_url, start_page_id=start_page_id, end_page_id=end_page_id, @@ -218,6 +222,8 @@ def main() -> None: # "vlm-http-client" -> remote OpenAI-compatible VLM server # "hybrid-http-client" -> remote OpenAI-compatible hybrid server backend = "hybrid-engine" + # Hybrid parsing effort. "medium" is faster; "high" keeps the high-effort hybrid behavior. + effort = "medium" # Available options: # "auto" -> let MinerU choose between text extraction and OCR # "txt" -> force text extraction @@ -247,6 +253,7 @@ def main() -> None: output_dir=output_dir, api_url=api_url, backend=backend, + effort=effort, parse_method=parse_method, language=language, formula_enable=formula_enable, diff --git a/docs/en/usage/cli_tools.md b/docs/en/usage/cli_tools.md index 25c30d03..440bfaf6 100644 --- a/docs/en/usage/cli_tools.md +++ b/docs/en/usage/cli_tools.md @@ -12,8 +12,9 @@ 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|vlm-engine|hybrid-engine|hybrid-flash-engine|vlm-http-client|hybrid-http-client|hybrid-flash-http-client] - Parsing backend (default: hybrid-flash-engine) + -b, --backend [pipeline|vlm-engine|hybrid-engine|vlm-http-client|hybrid-http-client] + Parsing backend (default: hybrid-engine) + --effort [medium|high] Hybrid parsing effort (default: medium) -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 52f52899..64c1124b 100644 --- a/docs/zh/usage/cli_tools.md +++ b/docs/zh/usage/cli_tools.md @@ -12,8 +12,9 @@ Options: -o, --output PATH 输出目录(必填) --api-url TEXT MinerU FastAPI 服务地址;不传时自动拉起本地临时 mineru-api -m, --method [auto|txt|ocr] 解析方法:auto(默认)、txt、ocr(仅用于 pipeline 与 hybrid* 后端) - -b, --backend [pipeline|vlm-engine|hybrid-engine|hybrid-flash-engine|vlm-http-client|hybrid-http-client|hybrid-flash-http-client] - 解析后端(默认为 hybrid-flash-engine) + -b, --backend [pipeline|vlm-engine|hybrid-engine|vlm-http-client|hybrid-http-client] + 解析后端(默认为 hybrid-engine) + --effort [medium|high] Hybrid 解析强度(默认:medium) -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/backend/hybrid/hybrid_analyze.py b/mineru/backend/hybrid/hybrid_analyze.py index 923feafc..486f8e43 100644 --- a/mineru/backend/hybrid/hybrid_analyze.py +++ b/mineru/backend/hybrid/hybrid_analyze.py @@ -26,7 +26,7 @@ from mineru.backend.pipeline.model_init import ( 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.utils.formula_number import optimize_medium_formula_number_blocks from mineru.backend.vlm.vlm_analyze import ( ModelSingleton, aio_predictor_execution_guard, @@ -61,9 +61,9 @@ 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"} +HYBRID_ANALYZE_EFFORTS = {"medium", "high"} INLINE_FORMULA_CONTAINER_LABELS = {"table", "image", "chart", "display_formula"} -FLASH_LAYOUT_LABEL_TO_VLM_TYPE = { +MEDIUM_EFFORT_LAYOUT_LABEL_TO_VLM_TYPE = { "abstract": BlockType.TEXT, "algorithm": BlockType.CODE, "aside_text": BlockType.ASIDE_TEXT, @@ -90,19 +90,19 @@ FLASH_LAYOUT_LABEL_TO_VLM_TYPE = { } -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 _validate_parse_effort(effort: str = "medium") -> str: + """校验 Hybrid effort,避免静默走错解析强度分支。""" + if effort not in HYBRID_ANALYZE_EFFORTS: + raise ValueError('effort must be "medium" or "high"') + return effort -def _vlm_type_for_flash_layout_label(label: str | None) -> str | None: +def _vlm_type_for_medium_layout_label(label: str | None) -> str | None: """将 pipeline layout 标签映射为 mineru-vl-utils 支持的 VLM 抽取类型。""" - return FLASH_LAYOUT_LABEL_TO_VLM_TYPE.get(label) + return MEDIUM_EFFORT_LAYOUT_LABEL_TO_VLM_TYPE.get(label) -def _apply_flash_visual_sub_type(block, label: str | None): +def _apply_medium_visual_sub_type(block, label: str | None): """为视觉块补充下游需要透传的子类型。""" if label == "seal": block["sub_type"] = "seal" @@ -364,7 +364,7 @@ def _layout_det_bbox_to_pixel(layout_det, page_width, page_height): return [x0, y0, x1, y1] -def _normalize_flash_vlm_angle(angle): +def _normalize_medium_vlm_angle(angle): """将pipeline方向标签转换为mineru-vl-utils接受的整数角度。""" try: normalized_angle = int(angle) @@ -375,12 +375,12 @@ def _normalize_flash_vlm_angle(angle): return 0 -def _build_flash_vlm_layout_blocks(layout_dets, page_width, page_height): +def _build_medium_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) + vlm_type = _vlm_type_for_medium_layout_label(label) if vlm_type is None: continue bbox = _layout_det_bbox_to_unit(layout_det, page_width, page_height) @@ -390,24 +390,24 @@ def _build_flash_vlm_layout_blocks(layout_dets, page_width, page_height): block = ContentBlock( vlm_type, bbox, - angle=_normalize_flash_vlm_angle(layout_det.get("angle", 0)), + angle=_normalize_medium_vlm_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}") + logger.warning(f"Skip invalid Hybrid medium effort VLM block: {layout_det}, error: {exc}") continue - _apply_flash_visual_sub_type(block, label) + _apply_medium_visual_sub_type(block, label) blocks.append(block) return blocks -def _apply_flash_table_orientation_labels( +def _apply_medium_table_orientation_labels( images_pil_list, images_layout_res, hybrid_pipeline_model, batch_ratio: int = 1, ): - """复用pipeline表格方向分类,为Hybrid flash的table layout写入VLM旋转角度。""" + """复用pipeline表格方向分类,为Hybrid medium effort 的 table layout 写入VLM旋转角度。""" table_inputs = [] table_layout_refs = [] for pil_img, layout_res in zip(images_pil_list, images_layout_res): @@ -422,7 +422,7 @@ def _apply_flash_table_orientation_labels( 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}" + f"Skip Hybrid medium effort table orientation crop: {layout_det}, error: {exc}" ) continue table_inputs.append({"table_img": table_img}) @@ -447,7 +447,7 @@ def _apply_flash_table_orientation_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" + f"Hybrid medium effort table orientation classification failed: {exc}, using original table images" ) @@ -932,7 +932,7 @@ def get_batch_ratio(device): logger.info(f"hybrid batch ratio (from env): {batch_ratio}") return batch_ratio except ValueError as e: - logger.warning(f"Invalid MINERU_HYBRID_BATCH_RATIO value: {env_val}, switching to auto mode. Error: {e}") + logger.warning(f"Invalid MINERU_HYBRID_BATCH_RATIO value: {env_val}, switching to auto ratio. Error: {e}") # 2. 根据显存自动推断 """ @@ -990,10 +990,10 @@ def doc_analyze( model_path: str | None = None, server_url: str | None = None, image_analysis: bool = True, - mode: str = "pro", + effort: str = "medium", **kwargs, ): - mode = _validate_hybrid_mode(mode) + effort = _validate_parse_effort(effort) client_side_output_generation = bool( kwargs.pop("client_side_output_generation", False) ) @@ -1009,7 +1009,7 @@ def doc_analyze( middle_json = init_middle_json( _ocr_enable, _vlm_ocr_enable, - hybrid_mode=mode, + effort=effort, ) model_list = [] doc_closed = False @@ -1058,15 +1058,15 @@ def doc_analyze( batch_ratio, _vlm_ocr_enable, ) - if mode == "flash": - _apply_flash_table_orientation_labels( + if effort == "medium": + _apply_medium_table_orientation_labels( images_pil_list, images_layout_res, hybrid_pipeline_model, batch_ratio=batch_ratio, ) vlm_blocks_list = [ - _build_flash_vlm_layout_blocks( + _build_medium_vlm_layout_blocks( page_layout_res, pil_img.width, pil_img.height, @@ -1080,7 +1080,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) + optimize_medium_formula_number_blocks(window_model_list) if _vlm_ocr_enable: _apply_vlm_ocr_det_sidecars_for_window( images_pil_list, @@ -1099,7 +1099,7 @@ def doc_analyze( images_layout_res=images_layout_res, hybrid_pipeline_model=hybrid_pipeline_model, ) - elif mode == "pro": + elif effort == "high": if _vlm_ocr_enable: with predictor_execution_guard(predictor): window_model_list = predictor.batch_two_step_extract( @@ -1130,7 +1130,7 @@ def doc_analyze( hybrid_pipeline_model=hybrid_pipeline_model, ) else: - raise ValueError(f"Unsupported hybrid mode: {mode}") + raise ValueError(f"Unsupported hybrid effort: {effort}") _apply_layout_title_split( window_model_list, @@ -1184,7 +1184,7 @@ def doc_analyze( hybrid_pipeline_model, _ocr_enable, _vlm_ocr_enable, - hybrid_mode=mode, + effort=effort, ) close_pdfium_document(pdf_doc) doc_closed = True @@ -1206,10 +1206,10 @@ async def aio_doc_analyze( model_path: str | None = None, server_url: str | None = None, image_analysis: bool = True, - mode: str = "pro", + effort: str = "medium", **kwargs, ): - mode = _validate_hybrid_mode(mode) + effort = _validate_parse_effort(effort) client_side_output_generation = bool( kwargs.pop("client_side_output_generation", False) ) @@ -1225,7 +1225,7 @@ async def aio_doc_analyze( middle_json = init_middle_json( _ocr_enable, _vlm_ocr_enable, - hybrid_mode=mode, + effort=effort, ) model_list = [] doc_closed = False @@ -1274,16 +1274,16 @@ async def aio_doc_analyze( batch_ratio, _vlm_ocr_enable, ) - if mode == "flash": + if effort == "medium": await asyncio.to_thread( - _apply_flash_table_orientation_labels, + _apply_medium_table_orientation_labels, images_pil_list, images_layout_res, hybrid_pipeline_model, batch_ratio, ) vlm_blocks_list = [ - _build_flash_vlm_layout_blocks( + _build_medium_vlm_layout_blocks( page_layout_res, pil_img.width, pil_img.height, @@ -1297,7 +1297,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) + optimize_medium_formula_number_blocks(window_model_list) if _vlm_ocr_enable: await asyncio.to_thread( _apply_vlm_ocr_det_sidecars_for_window, @@ -1318,7 +1318,7 @@ async def aio_doc_analyze( images_layout_res=images_layout_res, hybrid_pipeline_model=hybrid_pipeline_model, ) - elif mode == "pro": + elif effort == "high": if _vlm_ocr_enable: async with aio_predictor_execution_guard(predictor): window_model_list = await predictor.aio_batch_two_step_extract( @@ -1351,7 +1351,7 @@ async def aio_doc_analyze( hybrid_pipeline_model=hybrid_pipeline_model, ) else: - raise ValueError(f"Unsupported hybrid mode: {mode}") + raise ValueError(f"Unsupported hybrid effort: {effort}") await asyncio.to_thread( _apply_layout_title_split, @@ -1408,7 +1408,7 @@ async def aio_doc_analyze( hybrid_pipeline_model, _ocr_enable, _vlm_ocr_enable, - hybrid_mode=mode, + effort=effort, ) 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 f6da7d4b..d9264e41 100644 --- a/mineru/backend/hybrid/hybrid_model_output_to_middle_json.py +++ b/mineru/backend/hybrid/hybrid_model_output_to_middle_json.py @@ -180,11 +180,12 @@ def _normalize_split_title_blocks(pdf_info_list): block["level"] = title_level -def init_middle_json(_ocr_enable, _vlm_ocr_enable, hybrid_mode="pro"): +def init_middle_json(_ocr_enable, _vlm_ocr_enable, effort="medium"): + """初始化 Hybrid middle json,使用公开 effort 元数据描述解析强度。""" return { "pdf_info": [], "_backend": "hybrid", - "_hybrid_mode": hybrid_mode, + "_effort": effort, "_ocr_enable": _ocr_enable, "_vlm_ocr_enable": _vlm_ocr_enable, "_version_name": __version__ @@ -261,13 +262,13 @@ def apply_server_side_postprocess( _apply_post_ocr(pdf_info_list, hybrid_pipeline_model) -def finalize_middle_json_from_preproc(pdf_info_list, hybrid_mode="pro"): +def finalize_middle_json_from_preproc(pdf_info_list, effort="medium"): """从 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", + auto_merge_vertical_by_det=effort == "medium", ) table_enable = get_table_enable(os.getenv('MINERU_VLM_TABLE_ENABLE', 'True').lower() == 'true') @@ -284,16 +285,16 @@ def finalize_middle_json( hybrid_pipeline_model, _ocr_enable, _vlm_ocr_enable, - hybrid_mode="pro", + effort="medium", ): - """保持旧入口语义:服务端先做必要 post-OCR,再执行完整 finalize。""" + """服务端先做必要 post-OCR,再按公开 effort 执行完整 finalize。""" apply_server_side_postprocess( pdf_info_list, hybrid_pipeline_model, _ocr_enable, _vlm_ocr_enable, ) - finalize_middle_json_from_preproc(pdf_info_list, hybrid_mode=hybrid_mode) + finalize_middle_json_from_preproc(pdf_info_list, effort=effort) def result_to_middle_json( diff --git a/mineru/backend/utils/formula_number.py b/mineru/backend/utils/formula_number.py index 3a29efae..ab22ce2a 100644 --- a/mineru/backend/utils/formula_number.py +++ b/mineru/backend/utils/formula_number.py @@ -159,3 +159,8 @@ def optimize_flash_formula_number_blocks(model_list: Iterable[list[Block]]) -> N _downgrade_formula_number_to_text, ) page_model_list[:] = optimized_blocks + + +def optimize_medium_formula_number_blocks(model_list: Iterable[list[Block]]) -> None: + """按 Hybrid medium effort 规则处理 VLM 公式编号块。""" + optimize_flash_formula_number_blocks(model_list) diff --git a/mineru/cli/api_client.py b/mineru/cli/api_client.py index 9beada1f..8de6b608 100644 --- a/mineru/cli/api_client.py +++ b/mineru/cli/api_client.py @@ -23,6 +23,7 @@ import click import httpx from loguru import logger +from mineru.cli.backend_options import DEFAULT_HYBRID_EFFORT from mineru.cli.api_protocol import ( API_PROTOCOL_VERSION, DEFAULT_MAX_CONCURRENT_REQUESTS, @@ -811,6 +812,7 @@ def build_parse_request_form_data( start_page_id: int, end_page_id: Optional[int], *, + effort: str = DEFAULT_HYBRID_EFFORT, image_analysis: bool = True, return_md: bool, return_middle_json: bool, @@ -825,6 +827,7 @@ def build_parse_request_form_data( data: dict[str, str | list[str]] = { "lang_list": effective_lang_list, "backend": backend, + "effort": effort, "parse_method": parse_method, "formula_enable": str(formula_enable).lower(), "table_enable": str(table_enable).lower(), diff --git a/mineru/cli/api_request.py b/mineru/cli/api_request.py index ddc82b8f..4a1b543f 100644 --- a/mineru/cli/api_request.py +++ b/mineru/cli/api_request.py @@ -7,7 +7,10 @@ from fastapi import File, Form, HTTPException, Request, UploadFile from mineru.cli.backend_options import ( BACKEND_SCHEMA_EXTRA, DEFAULT_BACKEND, + DEFAULT_HYBRID_EFFORT, + HYBRID_EFFORT_SCHEMA_EXTRA, validate_backend as validate_public_backend, + validate_effort as validate_public_effort, ) from mineru.cli.public_http_client_policy import validate_public_http_client_request @@ -26,6 +29,7 @@ class ParseRequestOptions: files: list[UploadFile] lang_list: list[str] backend: str + effort: str parse_method: str formula_enable: bool table_enable: bool @@ -64,6 +68,14 @@ def validate_parse_backend(backend: str) -> str: raise HTTPException(status_code=400, detail=str(exc)) from exc +def validate_parse_effort(effort: str) -> str: + """校验公开 API 允许的 hybrid effort,避免非法值进入解析链路。""" + try: + return validate_public_effort(effort) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + async def parse_request_form( request: Request, files: Annotated[ @@ -104,13 +116,20 @@ async def parse_request_form( - pipeline: More general, supports multiple languages, hallucination-free. - 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: 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: Hybrid flash mode via remote computing power(client suitable for openai-compatible servers), supports multiple languages.""", +- hybrid-engine: Hybrid parsing via local computing power, supports multiple languages. Use effort to switch medium/high behavior. +- hybrid-http-client: Hybrid parsing via remote computing power but requires a little local computing power(client suitable for openai-compatible servers), supports multiple languages. Use effort to switch medium/high behavior.""", json_schema_extra=BACKEND_SCHEMA_EXTRA, ), ] = DEFAULT_BACKEND, + effort: Annotated[ + str, + Form( + description="""(Adapted only for hybrid backend) Hybrid parsing effort: +- medium: Fast hybrid parsing, equivalent to the previous fast hybrid behavior. +- high: High-effort hybrid parsing, equivalent to the previous hybrid behavior.""", + json_schema_extra=HYBRID_EFFORT_SCHEMA_EXTRA, + ), + ] = DEFAULT_HYBRID_EFFORT, parse_method: Annotated[ str, Form( @@ -192,6 +211,7 @@ async def parse_request_form( ) -> ParseRequestOptions: """解析 API/Router 共用的 multipart 表单,并保持 Swagger 参数同源。""" backend = validate_parse_backend(backend) + effort = validate_parse_effort(effort) validate_public_http_client_request( public_bind_exposed=bool( getattr(request.app.state, "public_bind_exposed", False) @@ -214,6 +234,7 @@ async def parse_request_form( files=files, lang_list=lang_list, backend=backend, + effort=effort, parse_method=validate_parse_method(parse_method), formula_enable=formula_enable, table_enable=table_enable, diff --git a/mineru/cli/backend_options.py b/mineru/cli/backend_options.py index 5796d562..7ec381c1 100644 --- a/mineru/cli/backend_options.py +++ b/mineru/cli/backend_options.py @@ -3,30 +3,28 @@ 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_HYBRID_EFFORT = "medium" +HYBRID_EFFORT_CHOICES = ("medium", "high") -DEFAULT_BACKEND = BACKEND_HYBRID_FLASH_ENGINE +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)} +HYBRID_EFFORT_SCHEMA_EXTRA = {"enum": list(HYBRID_EFFORT_CHOICES)} LEGACY_BACKEND_ALIASES = { "vlm-auto-engine": BACKEND_VLM_ENGINE, "hybrid-auto-engine": BACKEND_HYBRID_ENGINE, - "hybrid-flash-auto-engine": BACKEND_HYBRID_FLASH_ENGINE, } @@ -50,3 +48,11 @@ def normalize_backend(backend: str) -> str: def validate_backend(backend: str) -> str: """校验公开入口允许的 backend 名称,并返回规范后的后端名称。""" return normalize_backend(backend) + + +def validate_effort(effort: str) -> str: + """校验公开 hybrid effort 参数,并返回规范后的 effort 名称。""" + if effort not in HYBRID_EFFORT_CHOICES: + allowed_values = ", ".join(HYBRID_EFFORT_CHOICES) + raise ValueError(f"Invalid effort. Allowed values: {allowed_values}") + return effort diff --git a/mineru/cli/client.py b/mineru/cli/client.py index 02dcb1bd..06a36d54 100644 --- a/mineru/cli/client.py +++ b/mineru/cli/client.py @@ -20,8 +20,11 @@ from mineru.cli.api_protocol import ( ) from mineru.cli.backend_options import ( DEFAULT_BACKEND, + DEFAULT_HYBRID_EFFORT, + HYBRID_EFFORT_CHOICES, PUBLIC_BACKEND_CHOICES, normalize_backend, + validate_effort, ) from mineru.utils.config_reader import ( get_max_concurrent_requests as read_max_concurrent_requests, @@ -102,6 +105,18 @@ def normalize_backend_option( raise click.BadParameter(str(exc), ctx=ctx, param=param) from exc +def normalize_effort_option( + ctx: click.Context, + param: click.Parameter, + value: str, +) -> str: + """将 CLI 输入的 hybrid effort 参数规范为当前公开名称。""" + try: + return validate_effort(value) + except ValueError as exc: + raise click.BadParameter(str(exc), ctx=ctx, param=param) from exc + + @dataclass(frozen=True) class TaskFailure: task_index: int @@ -647,6 +662,7 @@ def build_request_form_data( end_page_id: Optional[int], image_analysis: bool = True, client_side_output_generation: bool = False, + effort: str = DEFAULT_HYBRID_EFFORT, ) -> dict[str, str | list[str]]: # 开启客户端输出生成时,只关闭客户端会重建的最终产物。 return_md = not client_side_output_generation @@ -654,6 +670,7 @@ def build_request_form_data( return _api_client.build_parse_request_form_data( lang_list=[lang], backend=backend, + effort=effort, parse_method=method, formula_enable=formula_enable, table_enable=table_enable, @@ -891,6 +908,7 @@ async def run_orchestrated_cli( table_enable: bool, image_analysis: bool = True, client_side_output_generation: bool = False, + effort: str = DEFAULT_HYBRID_EFFORT, extra_cli_args: tuple[str, ...] = (), ) -> None: if start_page_id < 0: @@ -962,6 +980,7 @@ async def run_orchestrated_cli( start_page_id=start_page_id, end_page_id=end_page_id, client_side_output_generation=client_side_output_generation, + effort=effort, ) visualization_context = create_visualization_context() failures = await execute_planned_tasks( @@ -1056,10 +1075,22 @@ 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: 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 backend specified, hybrid-flash-engine will be used by default.""", + Without backend specified, hybrid-engine will be used by default.""", +) +@click.option( + "--effort", + "effort", + type=str, + default=DEFAULT_HYBRID_EFFORT, + callback=normalize_effort_option, + metavar="[" + "|".join(HYBRID_EFFORT_CHOICES) + "]", + help="""\b + Hybrid parsing effort: + medium: Fast hybrid parsing, equivalent to the previous fast hybrid behavior. + high: High-effort hybrid parsing, equivalent to the previous hybrid behavior. + Without effort specified, medium will be used by default. + Adapted only for the case where the backend is set to 'hybrid-*'.""", ) @click.option( "-l", @@ -1159,6 +1190,7 @@ def main( api_url: Optional[str], method: str, backend: str, + effort: str, lang: str, server_url: Optional[str], start_page_id: int, @@ -1174,6 +1206,7 @@ def main( output_dir=output_dir, method=method, backend=backend, + effort=effort, lang=lang, server_url=server_url, api_url=api_url, diff --git a/mineru/cli/common.py b/mineru/cli/common.py index e95496bf..2f78c5e3 100644 --- a/mineru/cli/common.py +++ b/mineru/cli/common.py @@ -10,7 +10,11 @@ from typing import Sequence from loguru import logger -from mineru.cli.backend_options import normalize_backend +from mineru.cli.backend_options import ( + DEFAULT_HYBRID_EFFORT, + normalize_backend, + validate_effort, +) 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 @@ -70,7 +74,7 @@ def ensure_backend_dependencies(backend: str) -> None: def _load_hybrid_analyze_entrypoint(entrypoint_name: str, backend: str): - """加载统一 hybrid analyze 入口,flash/pro 由调用方通过 mode 控制。""" + """加载统一 hybrid analyze 入口,解析强度由公开 effort 参数控制。""" ensure_backend_dependencies(backend) module_name = "mineru.backend.hybrid.hybrid_analyze" try: @@ -518,7 +522,7 @@ def _process_hybrid( f_dump_content_list, f_make_md_mode, server_url=None, - mode="pro", + effort=DEFAULT_HYBRID_EFFORT, **kwargs, ): hybrid_doc_analyze = _load_hybrid_analyze_entrypoint( @@ -542,7 +546,7 @@ def _process_hybrid( language=lang, inline_formula_enable=inline_formula_enable, server_url=server_url, - mode=mode, + effort=validate_effort(effort), **kwargs, ) @@ -576,7 +580,7 @@ async def _async_process_hybrid( f_dump_content_list, f_make_md_mode, server_url=None, - mode="pro", + effort=DEFAULT_HYBRID_EFFORT, **kwargs, ): aio_hybrid_doc_analyze = _load_hybrid_analyze_entrypoint( @@ -600,7 +604,7 @@ async def _async_process_hybrid( language=lang, inline_formula_enable=inline_formula_enable, server_url=server_url, - mode=mode, + effort=validate_effort(effort), **kwargs, ) @@ -689,6 +693,7 @@ def do_parse( end_page_id=None, image_analysis=True, client_side_output_generation=False, + effort=DEFAULT_HYBRID_EFFORT, **kwargs, ): backend = normalize_backend(backend) @@ -742,10 +747,6 @@ def do_parse( elif backend.startswith("hybrid-"): ensure_backend_dependencies(backend) backend = backend[7:] - mode = "flash" if backend.startswith("flash-") else "pro" - - if mode == "flash": - backend = backend[6:] if backend == "engine": backend = get_vlm_engine(inference_engine='auto', is_async=False) @@ -757,7 +758,7 @@ def do_parse( 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, + server_url, effort=effort, image_analysis=image_analysis, client_side_output_generation=client_side_output_generation, **kwargs, ) @@ -784,6 +785,7 @@ async def aio_do_parse( end_page_id=None, image_analysis=True, client_side_output_generation=False, + effort=DEFAULT_HYBRID_EFFORT, **kwargs, ): backend = normalize_backend(backend) @@ -840,10 +842,6 @@ async def aio_do_parse( elif backend.startswith("hybrid-"): ensure_backend_dependencies(backend) backend = backend[7:] - mode = "flash" if backend.startswith("flash-") else "pro" - - if mode == "flash": - backend = backend[6:] if backend == "engine": backend = get_vlm_engine(inference_engine='auto', is_async=True) @@ -855,7 +853,7 @@ async def aio_do_parse( 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, + server_url, effort=effort, image_analysis=image_analysis, client_side_output_generation=client_side_output_generation, **kwargs, ) diff --git a/mineru/cli/fast_api.py b/mineru/cli/fast_api.py index 2dc09d44..3a95941a 100644 --- a/mineru/cli/fast_api.py +++ b/mineru/cli/fast_api.py @@ -54,6 +54,7 @@ from mineru.cli.api_protocol import ( DEFAULT_MAX_CONCURRENT_REQUESTS, DEFAULT_PROCESSING_WINDOW_SIZE, ) +from mineru.cli.backend_options import DEFAULT_HYBRID_EFFORT from mineru.cli.vlm_preload import ( maybe_preload_vlm_model, split_service_and_model_config, @@ -145,6 +146,7 @@ class AsyncParseTask: file_names: list[str] created_at: str output_dir: str + effort: str parse_method: str lang_list: list[str] formula_enable: bool @@ -834,6 +836,7 @@ async def run_parse_job( p_lang_list=list(actual_lang_list), backend=request_options.backend, parse_method=request_options.parse_method, + effort=getattr(request_options, "effort", DEFAULT_HYBRID_EFFORT), formula_enable=request_options.formula_enable, table_enable=request_options.table_enable, image_analysis=request_options.image_analysis, @@ -890,6 +893,7 @@ async def create_async_parse_task( file_names=file_names, created_at=utc_now_iso(), output_dir=task_output_dir, + effort=request_options.effort, parse_method=request_options.parse_method, lang_list=request_options.lang_list, formula_enable=request_options.formula_enable, diff --git a/mineru/utils/title_level_postprocess.py b/mineru/utils/title_level_postprocess.py index 634c8925..4f8d769e 100644 --- a/mineru/utils/title_level_postprocess.py +++ b/mineru/utils/title_level_postprocess.py @@ -74,7 +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"), + effort=middle_json.get("_effort", "medium"), ) return middle_json