Spaces:
Running
Running
[Admin maintenance] Migrate grant to ZeroGPU
#13
by multimodalart HF Staff - opened
- README.md +1 -1
- app.py +295 -39
- requirements.txt +2 -2
README.md
CHANGED
|
@@ -4,7 +4,7 @@ emoji: 📚
|
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 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 |
-
|
|
|
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import os
|
| 4 |
-
import
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
try:
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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[
|
| 2 |
-
gradio-pdf
|
|
|
|
| 1 |
+
mineru[vlm,core]>=3.1.0
|
| 2 |
+
gradio-pdf==0.0.22
|