mirror of
https://github.com/opendatalab/MinerU.git
synced 2026-09-19 09:30:53 +08:00
feat: add hybrid parsing effort parameter to API and CLI tools
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 兼容地址
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user