From c8fa8a013149c4ee1ccc52330d2670e9ae19cc51 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Fri, 24 Oct 2025 01:09:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0MinerU(v2.5)=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=EF=BC=8C=E4=BF=AE=E6=94=B9=E9=95=9C=E5=83=8F?= =?UTF-8?q?=E5=90=8D=E7=A7=B0=E5=92=8C=E5=90=8E=E7=AB=AF=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96OCR=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 10 +- docker/mineru.Dockerfile | 16 +- src/plugins/_ocr.py | 2 +- src/plugins/mineru.py | 333 +++++++++++++++++++-------------------- 4 files changed, 177 insertions(+), 184 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 104efb88..c5d830f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -207,23 +207,27 @@ services: networks: - app-network restart: unless-stopped + # lastest version: wget https://gcore.jsdelivr.net/gh/opendatalab/MinerU@master/docker/compose.yaml mineru: build: context: . dockerfile: docker/mineru.Dockerfile - image: mineru-sglang:latest + image: mineru-vllm:latest container_name: mineru profiles: - all ports: - 30000:30000 environment: - MINERU_MODEL_SOURCE: modelscope - entrypoint: mineru-sglang-server + MINERU_MODEL_SOURCE: local + entrypoint: mineru-vllm-server command: --host 0.0.0.0 --port 30000 + # parameters for vllm-engine + # --data-parallel-size 2 # If using multiple GPUs, increase throughput using vllm's multi-GPU parallel mode + # --gpu-memory-utilization 0.5 # If running on a single GPU and encountering VRAM shortage, reduce the KV cache size by this parameter, if VRAM issues persist, try lowering it further to `0.4` or below. ulimits: memlock: -1 stack: 67108864 diff --git a/docker/mineru.Dockerfile b/docker/mineru.Dockerfile index e369e2d0..c1a9d51a 100644 --- a/docker/mineru.Dockerfile +++ b/docker/mineru.Dockerfile @@ -1,9 +1,15 @@ -# Lastest version: wget https://gcore.jsdelivr.net/gh/opendatalab/MinerU@master/docker/china/Dockerfile +# Use DaoCloud mirrored vllm image for China region for gpu with Ampere architecture and above (Compute Capability>=8.0) +# Compute Capability version query (https://developer.nvidia.com/cuda-gpus) +FROM docker.m.daocloud.io/vllm/vllm-openai:v0.10.1.1 -# Use the official sglang image -FROM lmsysorg/sglang:v0.4.9.post3-cu126 -# For blackwell GPU, use the following line instead: -# FROM lmsysorg/sglang:v0.4.9.post3-cu128-b200 +# Use the official vllm image +# FROM vllm/vllm-openai:v0.10.1.1 + +# Use DaoCloud mirrored vllm image for China region for gpu with Turing architecture and below (Compute Capability<8.0) +# FROM docker.m.daocloud.io/vllm/vllm-openai:v0.10.2 + +# Use the official vllm image +# FROM vllm/vllm-openai:v0.10.2 # Install libgl for opencv support & Noto fonts for Chinese characters RUN apt-get update && \ diff --git a/src/plugins/_ocr.py b/src/plugins/_ocr.py index 8d51568b..41fffdab 100644 --- a/src/plugins/_ocr.py +++ b/src/plugins/_ocr.py @@ -270,7 +270,7 @@ class OCRPlugin: file_path_list = [file_path] output_dir = os.path.join(os.getcwd(), "tmp", "mineru_ocr") - text = parse_doc(file_path_list, output_dir, backend="vlm-sglang-client", server_url=mineru_ocr_uri)[0] + text = parse_doc(file_path_list, output_dir, backend="vlm-vllm-client", server_url=mineru_ocr_uri)[0] processing_time = time.time() - start_time log_ocr_request("mineru_ocr", file_path, True, processing_time) diff --git a/src/plugins/mineru.py b/src/plugins/mineru.py index 8187ec13..3b4749b2 100644 --- a/src/plugins/mineru.py +++ b/src/plugins/mineru.py @@ -4,18 +4,18 @@ import json import os from pathlib import Path -from mineru.backend.pipeline.model_json_to_middle_json import result_to_middle_json as pipeline_result_to_middle_json -from mineru.backend.pipeline.pipeline_analyze import doc_analyze as pipeline_doc_analyze -from mineru.backend.pipeline.pipeline_middle_json_mkcontent import union_make as pipeline_union_make -from mineru.backend.vlm.vlm_analyze import doc_analyze as vlm_doc_analyze -from mineru.backend.vlm.vlm_middle_json_mkcontent import union_make as vlm_union_make +from loguru import logger + from mineru.cli.common import convert_pdf_bytes_to_bytes_by_pypdfium2, prepare_env, read_fn from mineru.data.data_reader_writer import FileBasedDataWriter from mineru.utils.draw_bbox import draw_layout_bbox, draw_span_bbox from mineru.utils.enum_class import MakeMode -from tqdm import tqdm - -from src.utils.logging_config import logger +from mineru.backend.vlm.vlm_analyze import doc_analyze as vlm_doc_analyze +from mineru.backend.pipeline.pipeline_analyze import doc_analyze as pipeline_doc_analyze +from mineru.backend.pipeline.pipeline_middle_json_mkcontent import union_make as pipeline_union_make +from mineru.backend.pipeline.model_json_to_middle_json import result_to_middle_json as pipeline_result_to_middle_json +from mineru.backend.vlm.vlm_middle_json_mkcontent import union_make as vlm_union_make +from mineru.utils.guess_suffix_or_lang import guess_suffix_by_path def do_parse( @@ -25,9 +25,9 @@ def do_parse( p_lang_list: list[str], # List of languages for each PDF, default is 'ch' (Chinese) backend="pipeline", # The backend for parsing PDF, default is 'pipeline' parse_method="auto", # The method for parsing PDF, default is 'auto' - p_formula_enable=True, # Enable formula parsing - p_table_enable=True, # Enable table parsing - server_url=None, # Server URL for vlm-sglang-client backend + formula_enable=True, # Enable formula parsing + table_enable=True, # Enable table parsing + server_url=None, # Server URL for vlm-http-client backend f_draw_layout_bbox=True, # Whether to draw layout bounding boxes f_draw_span_bbox=True, # Whether to draw span bounding boxes f_dump_md=True, # Whether to dump markdown files @@ -38,22 +38,15 @@ def do_parse( f_make_md_mode=MakeMode.MM_MD, # The mode for making markdown content, default is MM_MD start_page_id=0, # Start page ID for parsing, default is 0 end_page_id=None, # End page ID for parsing, default is None (parse all pages until the end of the document) -) -> list[str]: +): + if backend == "pipeline": for idx, pdf_bytes in enumerate(pdf_bytes_list): new_pdf_bytes = convert_pdf_bytes_to_bytes_by_pypdfium2(pdf_bytes, start_page_id, end_page_id) pdf_bytes_list[idx] = new_pdf_bytes - result = pipeline_doc_analyze( - pdf_bytes_list, - p_lang_list, - parse_method=parse_method, - formula_enable=p_formula_enable, - table_enable=p_table_enable, - ) - infer_results, all_image_lists, all_pdf_docs, lang_list, ocr_enabled_list = result + infer_results, all_image_lists, all_pdf_docs, lang_list, ocr_enabled_list = pipeline_doc_analyze(pdf_bytes_list, p_lang_list, parse_method=parse_method, formula_enable=formula_enable,table_enable=table_enable) - md_results = [] for idx, model_list in enumerate(infer_results): model_json = copy.deepcopy(model_list) pdf_file_name = pdf_file_names[idx] @@ -64,193 +57,183 @@ def do_parse( pdf_doc = all_pdf_docs[idx] _lang = lang_list[idx] _ocr_enable = ocr_enabled_list[idx] - middle_json = pipeline_result_to_middle_json( - model_list, images_list, pdf_doc, image_writer, _lang, _ocr_enable, p_formula_enable - ) + middle_json = pipeline_result_to_middle_json(model_list, images_list, pdf_doc, image_writer, _lang, _ocr_enable, formula_enable) pdf_info = middle_json["pdf_info"] pdf_bytes = pdf_bytes_list[idx] - if f_draw_layout_bbox: - draw_layout_bbox(pdf_info, pdf_bytes, local_md_dir, f"{pdf_file_name}_layout.pdf") - - if f_draw_span_bbox: - draw_span_bbox(pdf_info, pdf_bytes, local_md_dir, f"{pdf_file_name}_span.pdf") - - if f_dump_orig_pdf: - md_writer.write( - f"{pdf_file_name}_origin.pdf", - pdf_bytes, - ) - - if f_dump_md: - image_dir = str(os.path.basename(local_image_dir)) - md_content_str = pipeline_union_make(pdf_info, f_make_md_mode, image_dir) - md_writer.write_string( - f"{pdf_file_name}.md", - md_content_str, - ) - md_results.append(md_content_str) - - if f_dump_content_list: - image_dir = str(os.path.basename(local_image_dir)) - content_list = pipeline_union_make(pdf_info, MakeMode.CONTENT_LIST, image_dir) - md_writer.write_string( - f"{pdf_file_name}_content_list.json", - json.dumps(content_list, ensure_ascii=False, indent=4), - ) - - if f_dump_middle_json: - md_writer.write_string( - f"{pdf_file_name}_middle.json", - json.dumps(middle_json, ensure_ascii=False, indent=4), - ) - - if f_dump_model_output: - md_writer.write_string( - f"{pdf_file_name}_model.json", - json.dumps(model_json, ensure_ascii=False, indent=4), - ) - - logger.info(f"local output dir is {local_md_dir}") - - return md_results - + _process_output( + pdf_info, pdf_bytes, pdf_file_name, local_md_dir, local_image_dir, + md_writer, f_draw_layout_bbox, f_draw_span_bbox, f_dump_orig_pdf, + f_dump_md, f_dump_content_list, f_dump_middle_json, f_dump_model_output, + f_make_md_mode, middle_json, model_json, is_pipeline=True + ) else: if backend.startswith("vlm-"): backend = backend[4:] f_draw_span_bbox = False parse_method = "vlm" - md_results = [] - - for idx, pdf_bytes in enumerate(tqdm(pdf_bytes_list, desc="Parsing documents bytes")): + for idx, pdf_bytes in enumerate(pdf_bytes_list): pdf_file_name = pdf_file_names[idx] pdf_bytes = convert_pdf_bytes_to_bytes_by_pypdfium2(pdf_bytes, start_page_id, end_page_id) local_image_dir, local_md_dir = prepare_env(output_dir, pdf_file_name, parse_method) image_writer, md_writer = FileBasedDataWriter(local_image_dir), FileBasedDataWriter(local_md_dir) - middle_json, infer_result = vlm_doc_analyze( - pdf_bytes, image_writer=image_writer, backend=backend, server_url=server_url - ) + middle_json, infer_result = vlm_doc_analyze(pdf_bytes, image_writer=image_writer, backend=backend, server_url=server_url) pdf_info = middle_json["pdf_info"] - if f_draw_layout_bbox: - draw_layout_bbox(pdf_info, pdf_bytes, local_md_dir, f"{pdf_file_name}_layout.pdf") + _process_output( + pdf_info, pdf_bytes, pdf_file_name, local_md_dir, local_image_dir, + md_writer, f_draw_layout_bbox, f_draw_span_bbox, f_dump_orig_pdf, + f_dump_md, f_dump_content_list, f_dump_middle_json, f_dump_model_output, + f_make_md_mode, middle_json, infer_result, is_pipeline=False + ) - if f_draw_span_bbox: - draw_span_bbox(pdf_info, pdf_bytes, local_md_dir, f"{pdf_file_name}_span.pdf") - if f_dump_orig_pdf: - md_writer.write( - f"{pdf_file_name}_origin.pdf", - pdf_bytes, - ) +def _process_output( + pdf_info, + pdf_bytes, + pdf_file_name, + local_md_dir, + local_image_dir, + md_writer, + f_draw_layout_bbox, + f_draw_span_bbox, + f_dump_orig_pdf, + f_dump_md, + f_dump_content_list, + f_dump_middle_json, + f_dump_model_output, + f_make_md_mode, + middle_json, + model_output=None, + is_pipeline=True +): + """处理输出文件""" + if f_draw_layout_bbox: + draw_layout_bbox(pdf_info, pdf_bytes, local_md_dir, f"{pdf_file_name}_layout.pdf") - if f_dump_md: - image_dir = str(os.path.basename(local_image_dir)) - md_content_str = vlm_union_make(pdf_info, f_make_md_mode, image_dir) - md_writer.write_string( - f"{pdf_file_name}.md", - md_content_str, - ) - md_results.append(md_content_str) + if f_draw_span_bbox: + draw_span_bbox(pdf_info, pdf_bytes, local_md_dir, f"{pdf_file_name}_span.pdf") - if f_dump_content_list: - image_dir = str(os.path.basename(local_image_dir)) - content_list = vlm_union_make(pdf_info, MakeMode.CONTENT_LIST, image_dir) - md_writer.write_string( - f"{pdf_file_name}_content_list.json", - json.dumps(content_list, ensure_ascii=False, indent=4), - ) + if f_dump_orig_pdf: + md_writer.write( + f"{pdf_file_name}_origin.pdf", + pdf_bytes, + ) - if f_dump_middle_json: - md_writer.write_string( - f"{pdf_file_name}_middle.json", - json.dumps(middle_json, ensure_ascii=False, indent=4), - ) + image_dir = str(os.path.basename(local_image_dir)) - if f_dump_model_output: - model_output = ("\n" + "-" * 50 + "\n").join(infer_result) - md_writer.write_string( - f"{pdf_file_name}_model_output.txt", - model_output, - ) + if f_dump_md: + make_func = pipeline_union_make if is_pipeline else vlm_union_make + md_content_str = make_func(pdf_info, f_make_md_mode, image_dir) + md_writer.write_string( + f"{pdf_file_name}.md", + md_content_str, + ) - logger.info(f"local output dir is {local_md_dir}") + if f_dump_content_list: + make_func = pipeline_union_make if is_pipeline else vlm_union_make + content_list = make_func(pdf_info, MakeMode.CONTENT_LIST, image_dir) + md_writer.write_string( + f"{pdf_file_name}_content_list.json", + json.dumps(content_list, ensure_ascii=False, indent=4), + ) - return md_results + if f_dump_middle_json: + md_writer.write_string( + f"{pdf_file_name}_middle.json", + json.dumps(middle_json, ensure_ascii=False, indent=4), + ) + + if f_dump_model_output: + md_writer.write_string( + f"{pdf_file_name}_model.json", + json.dumps(model_output, ensure_ascii=False, indent=4), + ) + + logger.info(f"local output dir is {local_md_dir}") def parse_doc( - path_list: list[Path], - output_dir, - lang="ch", - backend="pipeline", - method="auto", - server_url=None, - start_page_id=0, # Start page ID for parsing, default is 0 - end_page_id=None, # End page ID for parsing, default is None (parse all pages until the end of the document) -) -> list[str]: + path_list: list[Path], + output_dir, + lang="ch", + backend="pipeline", + method="auto", + server_url=None, + start_page_id=0, + end_page_id=None +): """ - Parameter description: - path_list: List of document paths to be parsed, can be PDF or image files. - output_dir: Output directory for storing parsing results. - lang: Language option, default is 'ch', - optional values include[ - 'ch', 'ch_server', 'ch_lite', 'en', 'korean', 'japan', 'chinese_cht', 'ta', 'te', 'ka' - ]。 - Input the languages in the pdf (if known) to improve OCR accuracy. Optional. - Adapted only for the case where the backend is set to "pipeline" - backend: the backend for parsing pdf: - pipeline: More general. - vlm-transformers: More general. - vlm-sglang-engine: Faster(engine). - vlm-sglang-client: Faster(client). - without method specified, pipeline will be used by default. - method: the method for parsing pdf: - auto: Automatically determine the method based on the file type. - txt: Use text extraction method. - ocr: Use OCR method for image-based PDFs. - Without method specified, 'auto' will be used by default. - Adapted only for the case where the backend is set to "pipeline". - server_url: When the backend is `sglang-client`, you need to specify the server_url, for example:`http://127.0.0.1:30000` + Parameter description: + path_list: List of document paths to be parsed, can be PDF or image files. + output_dir: Output directory for storing parsing results. + lang: Language option, default is 'ch', optional values include['ch', 'ch_server', 'ch_lite', 'en', 'korean', 'japan', 'chinese_cht', 'ta', 'te', 'ka']。 + Input the languages in the pdf (if known) to improve OCR accuracy. Optional. + Adapted only for the case where the backend is set to "pipeline" + backend: the backend for parsing pdf: + pipeline: More general. + vlm-transformers: More general. + vlm-vllm-engine: Faster(engine). + vlm-http-client: Faster(client). + without method specified, pipeline will be used by default. + method: the method for parsing pdf: + auto: Automatically determine the method based on the file type. + txt: Use text extraction method. + ocr: Use OCR method for image-based PDFs. + Without method specified, 'auto' will be used by default. + Adapted only for the case where the backend is set to "pipeline". + server_url: When the backend is `http-client`, you need to specify the server_url, for example:`http://127.0.0.1:30000` + start_page_id: Start page ID for parsing, default is 0 + end_page_id: End page ID for parsing, default is None (parse all pages until the end of the document) """ - file_name_list = [] - pdf_bytes_list = [] - lang_list = [] - for path in tqdm(path_list, desc="Parsing documents"): - file_name = str(Path(path).stem) - pdf_bytes = read_fn(path) - file_name_list.append(file_name) - pdf_bytes_list.append(pdf_bytes) - lang_list.append(lang) - - result = do_parse( - output_dir=output_dir, - pdf_file_names=file_name_list, - pdf_bytes_list=pdf_bytes_list, - p_lang_list=lang_list, - backend=backend, - parse_method=method, - server_url=server_url, - start_page_id=start_page_id, - end_page_id=end_page_id, - ) - return result if result else [""] + try: + file_name_list = [] + pdf_bytes_list = [] + lang_list = [] + for path in path_list: + file_name = str(Path(path).stem) + pdf_bytes = read_fn(path) + file_name_list.append(file_name) + pdf_bytes_list.append(pdf_bytes) + lang_list.append(lang) + do_parse( + output_dir=output_dir, + pdf_file_names=file_name_list, + pdf_bytes_list=pdf_bytes_list, + p_lang_list=lang_list, + backend=backend, + parse_method=method, + server_url=server_url, + start_page_id=start_page_id, + end_page_id=end_page_id + ) + except Exception as e: + logger.exception(e) -if __name__ == "__main__": - pdf_files_dir = "/home/zwj/workspace/projects/Yuxi-Know/test/struct_pdf" - output_dir = "/home/zwj/workspace/projects/Yuxi-Know/test/struct_pdf_output" - pdf_suffixes = [".pdf"] - image_suffixes = [".png", ".jpeg", ".jpg"] +if __name__ == '__main__': + # args + __dir__ = os.path.dirname(os.path.abspath(__file__)) + pdf_files_dir = os.path.join(__dir__, "pdfs") + output_dir = os.path.join(__dir__, "output") + pdf_suffixes = ["pdf"] + image_suffixes = ["png", "jpeg", "jp2", "webp", "gif", "bmp", "jpg"] doc_path_list = [] - for doc_path in Path(pdf_files_dir).glob("*"): - if doc_path.suffix in pdf_suffixes + image_suffixes: + for doc_path in Path(pdf_files_dir).glob('*'): + if guess_suffix_by_path(doc_path) in pdf_suffixes + image_suffixes: doc_path_list.append(doc_path) - parse_doc( - doc_path_list, output_dir, backend="vlm-sglang-client", server_url="http://172.19.13.5:30000" - ) # faster(client). + """如果您由于网络问题无法下载模型,可以设置环境变量MINERU_MODEL_SOURCE为modelscope使用免代理仓库下载模型""" + # os.environ['MINERU_MODEL_SOURCE'] = "modelscope" + + """Use pipeline mode if your environment does not support VLM""" + parse_doc(doc_path_list, output_dir, backend="pipeline") + + """To enable VLM mode, change the backend to 'vlm-xxx'""" + # parse_doc(doc_path_list, output_dir, backend="vlm-transformers") # more general. + # parse_doc(doc_path_list, output_dir, backend="vlm-vllm-engine") # faster(engine). + # parse_doc(doc_path_list, output_dir, backend="vlm-http-client", server_url="http://127.0.0.1:30000") # faster(client). \ No newline at end of file