feat: add hybrid parsing effort parameter to API and CLI tools

This commit is contained in:
myhloli
2026-06-09 15:01:20 +08:00
parent 3136764443
commit 39b1912dda
13 changed files with 163 additions and 83 deletions
+7
View File
@@ -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,
+3 -2
View File
@@ -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
+3 -2
View File
@@ -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 兼容地址
+42 -42
View File
@@ -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
@@ -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(
+5
View File
@@ -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)
+3
View File
@@ -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(),
+25 -4
View File
@@ -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,
+12 -6
View File
@@ -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
+36 -3
View File
@@ -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,
+14 -16
View File
@@ -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,
)
+4
View File
@@ -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,
+1 -1
View File
@@ -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