[Admin maintenance] Migrate grant to ZeroGPU

#13
by multimodalart HF Staff - opened
Files changed (3) hide show
  1. README.md +1 -1
  2. app.py +295 -39
  3. requirements.txt +2 -2
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 📚
4
  colorFrom: purple
5
  colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.8.0
8
  app_file: app.py
9
  pinned: true
10
  license: apache-2.0
 
4
  colorFrom: purple
5
  colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: true
10
  license: apache-2.0
app.py CHANGED
@@ -1,42 +1,298 @@
1
- # Copyright (c) Opendatalab. All rights reserved.
 
2
 
 
 
 
 
 
 
 
 
3
  import os
4
- import json
5
- from loguru import logger
6
-
7
-
8
- if __name__ == '__main__':
9
- os.system('pip uninstall -y mineru')
10
- os.system('pip install git+https://github.com/myhloli/Magic-PDF.git@dev')
11
- os.system('pip install -U mineru_vl_utils')
12
- os.system('pip uninstall -y onnxruntime')
13
- os.system('pip install onnxruntime-gpu==1.23.2')
14
- os.system('mineru-models-download -s huggingface -m all')
15
- # os.system('mineru-models-download -s modelscope -m all')
16
- os.environ['MINERU_MODEL_SOURCE'] = "local"
17
- os.environ['GRADIO_SSR_MODE'] = "false"
18
- os.environ['MINERU_PDF_RENDER_TIMEOUT'] = "10"
 
 
 
 
 
19
  try:
20
- home_dir = os.path.expanduser('~')
21
- config_file = os.path.join(home_dir, 'mineru.json')
22
- with open(config_file, 'r+') as file:
23
- config = json.load(file)
24
-
25
- delimiters = {
26
- 'display': {'left': '\\[', 'right': '\\]'},
27
- 'inline': {'left': '\\(', 'right': '\\)'}
28
- }
29
-
30
- config['latex-delimiter-config'] = delimiters
31
-
32
- if os.getenv('apikey'):
33
- config['llm-aided-config']['title_aided']['api_key'] = os.getenv('apikey')
34
- config['llm-aided-config']['title_aided']['enable'] = True
35
- config['llm-aided-config']['title_aided']['model'] = "qwen3.5-flash"
36
-
37
- file.seek(0) # 将文件指针移回文件开始位置
38
- file.truncate() # 截断文件,清除原有内容
39
- json.dump(config, file, indent=4) # 写入新内容
40
- except Exception as e:
41
- logger.exception(e)
42
- os.system('mineru-gradio --enable-api false --max-convert-pages 20 --latex-delimiters-type b --gpu-memory-utilization 0.5 --enable-vlm-preload true')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MinerU on ZeroGPU.
3
 
4
+ We keep mineru's official Gradio UI intact (i18n, status panel, PDF viewer,
5
+ examples, locale, latex delimiters) by importing `mineru.cli.gradio_app`
6
+ and invoking its Click command in-process.
7
+
8
+ But we replace the local mineru-api FastAPI subprocess + HTTP round-trip with
9
+ a direct in-process call to `aio_do_parse`, wrapped in @spaces.GPU so it runs
10
+ on ZeroGPU. The model is preloaded at module scope into the snapshot.
11
+ """
12
  import os
13
+ import sys
14
+ import time
15
+ import uuid
16
+ import shutil
17
+ import asyncio
18
+ import zipfile
19
+ import subprocess
20
+ from pathlib import Path
21
+ from typing import Callable
22
+
23
+ # Set before any mineru import
24
+ os.environ.setdefault("MINERU_MODEL_SOURCE", "huggingface")
25
+ os.environ.setdefault("MINERU_VLM_FORMULA_ENABLE", "true")
26
+ os.environ.setdefault("MINERU_VLM_TABLE_ENABLE", "true")
27
+
28
+ # CUDA hook before any torch / cuda touch
29
+ import spaces # noqa: E402
30
+
31
+
32
+ def _ensure_mineru():
33
  try:
34
+ import mineru # noqa: F401
35
+ return
36
+ except ImportError:
37
+ pass
38
+ subprocess.check_call(
39
+ [sys.executable, "-m", "pip", "install", "-q",
40
+ "mineru[vlm,core]>=3.1.0", "gradio-pdf>=0.0.22"],
41
+ )
42
+
43
+ _ensure_mineru()
44
+
45
+ # Download all weights (idempotent if already present)
46
+ print("Downloading MinerU model weights...")
47
+ subprocess.check_call(["mineru-models-download", "-s", "huggingface", "-m", "all"])
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Preload the transformers backend into the ZeroGPU snapshot
51
+ # ---------------------------------------------------------------------------
52
+ from mineru.backend.vlm.vlm_analyze import ModelSingleton
53
+ from mineru.cli.common import aio_do_parse
54
+
55
+ print("Preloading MinerU VLM (transformers backend) into ZeroGPU snapshot...")
56
+ _t0 = time.time()
57
+ ModelSingleton().get_model(
58
+ backend="transformers",
59
+ model_path=None,
60
+ server_url=None,
61
+ )
62
+ print(f"Preloaded in {time.time() - _t0:.1f}s")
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Monkey-patch mineru-gradio: skip the local mineru-api subprocess entirely
67
+ # and route the inference call directly to aio_do_parse, wrapped in @spaces.GPU.
68
+ # ---------------------------------------------------------------------------
69
+ # Patch pdf_image_tools to use a thread pool instead of process pool —
70
+ # we're inside a daemon @spaces.GPU worker which can't spawn child processes.
71
+ from mineru.utils import pdf_image_tools as _pit
72
+ from concurrent.futures import ThreadPoolExecutor
73
+
74
+ def _thread_pdf_render_executor(max_workers=None):
75
+ return ThreadPoolExecutor(max_workers=max_workers or 4)
76
+ _pit._create_pdf_render_executor = _thread_pdf_render_executor
77
+ _pit._pdf_render_executor = ThreadPoolExecutor(max_workers=4)
78
+ _pit._get_pdf_render_executor = lambda: _pit._pdf_render_executor
79
+
80
+ from mineru.cli import gradio_app as mineru_gradio
81
+ from mineru.cli.gradio_app import (
82
+ STATUS_PREPARING_REQUEST,
83
+ STATUS_CHECKING_SERVER,
84
+ STATUS_SUBMITTING_TASK,
85
+ STATUS_QUEUED_ON_SERVER,
86
+ STATUS_PROCESSING_ON_SERVER,
87
+ STATUS_DOWNLOADING_RESULT,
88
+ STATUS_PROCESSING_OUTPUT,
89
+ STATUS_COMPLETED,
90
+ create_gradio_run_paths,
91
+ resolve_parse_method,
92
+ resolve_parse_dir,
93
+ replace_image_with_gradio_file_urls,
94
+ maybe_generate_local_preview,
95
+ compress_directory_to_zip,
96
+ office_suffixes,
97
+ normalize_language,
98
+ build_gradio_upload_name,
99
+ )
100
+ from mineru.cli import api_client as _api_client
101
+
102
+
103
+ from mineru.cli.common import do_parse as _sync_do_parse
104
+
105
+ @spaces.GPU(duration=600)
106
+ def _gpu_do_parse_sync(
107
+ output_dir,
108
+ pdf_file_names,
109
+ pdf_bytes_list,
110
+ p_lang_list,
111
+ backend,
112
+ formula_enable,
113
+ table_enable,
114
+ end_page_id,
115
+ f_dump_md,
116
+ f_dump_content_list,
117
+ f_dump_orig_pdf,
118
+ f_dump_middle_json,
119
+ f_dump_model_output,
120
+ image_analysis,
121
+ ):
122
+ # Use the SYNC do_parse — aio_do_parse pulls in ProcessPoolExecutor for PDF
123
+ # rendering, and ZeroGPU's daemon worker can't spawn children.
124
+ return _sync_do_parse(
125
+ output_dir=output_dir,
126
+ pdf_file_names=pdf_file_names,
127
+ pdf_bytes_list=pdf_bytes_list,
128
+ p_lang_list=p_lang_list,
129
+ backend=backend,
130
+ formula_enable=formula_enable,
131
+ table_enable=table_enable,
132
+ end_page_id=end_page_id,
133
+ f_draw_layout_bbox=True, # produce the layout-annotated PDF for the doc-preview pane
134
+ f_draw_span_bbox=False,
135
+ f_dump_md=f_dump_md,
136
+ f_dump_content_list=f_dump_content_list,
137
+ f_dump_orig_pdf=f_dump_orig_pdf,
138
+ f_dump_middle_json=f_dump_middle_json,
139
+ f_dump_model_output=f_dump_model_output,
140
+ image_analysis=image_analysis,
141
+ )
142
+
143
+
144
+ async def _run_to_markdown_job_local(
145
+ file_path,
146
+ end_pages=10,
147
+ is_ocr=False,
148
+ formula_enable=True,
149
+ table_enable=True,
150
+ image_analysis=True,
151
+ language="ch",
152
+ backend="vlm-transformers",
153
+ url=None,
154
+ api_url=None,
155
+ status_callback: Callable[[str], None] | None = None,
156
+ ):
157
+ """In-process replacement for mineru-gradio's _run_to_markdown_job.
158
+
159
+ Original calls a local mineru-api over HTTP. We call aio_do_parse directly
160
+ inside a @spaces.GPU window so ZeroGPU treats this as a single GPU task."""
161
+
162
+ def emit(msg: str) -> None:
163
+ if status_callback is not None:
164
+ status_callback(msg)
165
+
166
+ if file_path is None:
167
+ return "", "", None, None
168
+
169
+ # Force the transformers backend regardless of what the UI dropdown set,
170
+ # since we only support that path on ZeroGPU.
171
+ if backend == "pipeline":
172
+ backend = "pipeline"
173
+ elif backend.startswith("vlm-") or backend == "auto":
174
+ backend = "vlm-transformers"
175
+
176
+ normalized_language = normalize_language(language)
177
+ file_path = str(file_path)
178
+ file_suffix = Path(file_path).suffix.lower().lstrip(".")
179
+ parse_method = resolve_parse_method(file_path, is_ocr, backend)
180
+ run_root, extract_root, archive_zip_path = create_gradio_run_paths(file_path)
181
+ run_root.mkdir(parents=True, exist_ok=True)
182
+ extract_root.mkdir(parents=True, exist_ok=True)
183
+
184
+ upload_name = build_gradio_upload_name(file_path)
185
+ file_stem = Path(upload_name).stem
186
+
187
+ task_id = uuid.uuid4().hex[:12]
188
+ emit(STATUS_PREPARING_REQUEST)
189
+ emit(STATUS_CHECKING_SERVER)
190
+ emit(STATUS_SUBMITTING_TASK)
191
+ emit(f"Task submitted: task_id={task_id}")
192
+ # Local execution skips the server queue; advance the UI past it
193
+ emit(STATUS_QUEUED_ON_SERVER)
194
+ emit(STATUS_PROCESSING_ON_SERVER)
195
+
196
+ # Load the file bytes
197
+ from mineru.cli.common import read_fn
198
+ pdf_bytes = read_fn(Path(file_path))
199
+
200
+ # Run the parser inside a single @spaces.GPU window
201
+ end_page_id = (int(end_pages) - 1) if end_pages else None
202
+ # Inference happens inside _gpu_do_parse_sync, which is decorated.
203
+ # Run it in a thread so we don't block the asyncio loop.
204
+ await asyncio.to_thread(
205
+ _gpu_do_parse_sync,
206
+ output_dir=str(extract_root),
207
+ pdf_file_names=[file_stem],
208
+ pdf_bytes_list=[pdf_bytes],
209
+ p_lang_list=[normalized_language],
210
+ backend=backend,
211
+ formula_enable=bool(formula_enable),
212
+ table_enable=bool(table_enable),
213
+ end_page_id=end_page_id,
214
+ f_dump_md=True,
215
+ f_dump_content_list=True,
216
+ f_dump_orig_pdf=True,
217
+ f_dump_middle_json=True,
218
+ f_dump_model_output=True,
219
+ image_analysis=bool(image_analysis),
220
+ )
221
+
222
+ file_name = file_stem
223
+ local_md_dir = resolve_parse_dir(
224
+ extract_root,
225
+ file_name,
226
+ backend,
227
+ parse_method,
228
+ allow_office_fallback=True,
229
+ )
230
+ preview_pdf_path = maybe_generate_local_preview(
231
+ extract_root=extract_root,
232
+ file_name=file_name,
233
+ file_suffix=file_suffix,
234
+ backend=backend,
235
+ parse_method=parse_method,
236
+ )
237
+
238
+ emit(STATUS_DOWNLOADING_RESULT)
239
+ emit(STATUS_PROCESSING_OUTPUT)
240
+
241
+ # Zip the per-document output dir (same shape as what mineru-api ships back)
242
+ if compress_directory_to_zip(local_md_dir, archive_zip_path) != 0:
243
+ # Fallback: zip with stdlib
244
+ with zipfile.ZipFile(archive_zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
245
+ for p in Path(local_md_dir).rglob("*"):
246
+ if p.is_file():
247
+ zf.write(p, p.relative_to(local_md_dir))
248
+
249
+ md_path = Path(local_md_dir) / f"{file_name}.md"
250
+ if not md_path.exists():
251
+ # office_suffixes may have produced a different name
252
+ candidates = list(Path(local_md_dir).glob("*.md"))
253
+ if candidates:
254
+ md_path = candidates[0]
255
+ txt_content = md_path.read_text(encoding="utf-8", errors="replace") if md_path.exists() else ""
256
+ md_content = replace_image_with_gradio_file_urls(txt_content, str(local_md_dir))
257
+
258
+ if file_suffix in office_suffixes:
259
+ preview_pdf_path = None
260
+
261
+ emit(STATUS_COMPLETED)
262
+ return md_content, txt_content, str(archive_zip_path), preview_pdf_path
263
+
264
+
265
+ # Patch the original mineru-gradio job runner
266
+ mineru_gradio._run_to_markdown_job = _run_to_markdown_job_local
267
+
268
+ # Disable the local-API startup hook
269
+ mineru_gradio.maybe_prepare_local_api_for_gradio_startup = lambda **kw: None
270
+ async def _noop_ensure(*a, **k):
271
+ return None
272
+ mineru_gradio.ensure_local_api_ready_for_gradio_startup = _noop_ensure
273
+
274
+ # Replace resolve_server_health to return a stub: avoids the upstream HTTP probe
275
+ class _StubServerHealth:
276
+ max_concurrent_requests = 1
277
+ base_url = "http://disabled/"
278
+
279
+ async def _resolve_server_health_stub(http_client, api_url):
280
+ return _StubServerHealth()
281
+ mineru_gradio.resolve_server_health = _resolve_server_health_stub
282
+ mineru_gradio.resolve_gradio_max_concurrent_requests = lambda api_url, server_health: 1
283
+
284
+
285
+ # ---------------------------------------------------------------------------
286
+ # Launch mineru-gradio in-process with our patches active
287
+ # ---------------------------------------------------------------------------
288
+ sys.argv = [
289
+ "mineru-gradio",
290
+ "--enable-api", "false",
291
+ "--enable-http-client", "false",
292
+ "--enable-vlm-preload", "false", # already preloaded above
293
+ "--max-convert-pages", "25",
294
+ "--latex-delimiters-type", "b",
295
+ ]
296
+
297
+ from mineru.cli.gradio_app import main as _mineru_main
298
+ _mineru_main(standalone_mode=False)
requirements.txt CHANGED
@@ -1,2 +1,2 @@
1
- mineru[all]>=3.0.5
2
- gradio-pdf>=0.0.22
 
1
+ mineru[vlm,core]>=3.1.0
2
+ gradio-pdf==0.0.22