This commit is contained in:
2026-06-15 17:46:45 +08:00
parent 2b779d0423
commit c33488d95c
3 changed files with 56 additions and 74 deletions
+11 -27
View File
@@ -1,10 +1,12 @@
# ============================================================================= # =============================================================================
# MinerU ROCm Docker Compose 配置 # MinerU ROCm Docker Compose 配置
# 原生 Linux + AMD GPU 环境 # 原生 Linux + AMD GPU 环境
#
# 架构:Gradio / API → nginx (8000) → worker0 (8001) / worker1 (8002)
# ============================================================================= # =============================================================================
services: services:
# --- WebUI 前端(纯前端,不本地处理,通过 Router → Worker 处理)--- # --- WebUI 前端(纯前端,通过 nginx 分发到 worker)---
gradio: gradio:
image: mineru-rocm:7.2.1 image: mineru-rocm:7.2.1
profiles: ["gradio"] profiles: ["gradio"]
@@ -16,7 +18,7 @@ services:
- "10002:7860" - "10002:7860"
environment: environment:
- GRADIO_SERVER_NAME=0.0.0.0 - GRADIO_SERVER_NAME=0.0.0.0
- MINERU_API_BASE=http://mineru-router:8000 - MINERU_API_BASE=http://mineru-nginx:8000
- MINERU_MODEL_SOURCE=${MINERU_MODEL_SOURCE:-huggingface} - MINERU_MODEL_SOURCE=${MINERU_MODEL_SOURCE:-huggingface}
- HF_HUB_CACHE=${HF_HUB_CACHE:-/opt/models/huggingface} - HF_HUB_CACHE=${HF_HUB_CACHE:-/opt/models/huggingface}
- MODELSCOPE_CACHE=${MODELSCOPE_CACHE:-/opt/models/modelscope} - MODELSCOPE_CACHE=${MODELSCOPE_CACHE:-/opt/models/modelscope}
@@ -30,9 +32,9 @@ services:
"/opt/mineru_venv/bin/python", "/opt/scripts/gradio_client.py", "/opt/mineru_venv/bin/python", "/opt/scripts/gradio_client.py",
] ]
depends_on: depends_on:
- router - nginx
# --- 双卡 Worker(GPU 算力,无 profile,始终可用)--- # --- 双卡 Worker(GPU 算力)---
worker0: worker0:
image: mineru-rocm:7.2.1 image: mineru-rocm:7.2.1
build: build:
@@ -94,32 +96,14 @@ services:
- ./scripts:/opt/scripts:ro - ./scripts:/opt/scripts:ro
command: ["mineru-api", "--host", "0.0.0.0", "--port", "8002", "--allow-public-http-client"] command: ["mineru-api", "--host", "0.0.0.0", "--port", "8002", "--allow-public-http-client"]
router: # --- Nginx 负载均衡(替换有 Bug 的 mineru-router)---
image: mineru-rocm:7.2.1 nginx:
container_name: mineru-router image: nginx:alpine
stdin_open: true container_name: mineru-nginx
tty: true
ipc: host
ports: ports:
- "8000:8000" - "8000:8000"
environment:
- MINERU_MODEL_SOURCE=${MINERU_MODEL_SOURCE:-huggingface}
- HF_HUB_CACHE=${HF_HUB_CACHE:-/opt/models/huggingface}
- MODELSCOPE_CACHE=${MODELSCOPE_CACHE:-/opt/models/modelscope}
volumes: volumes:
- ${INPUT_DIR:-./data/input}:/data/input:ro - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ${OUTPUT_DIR:-./data/output}:/data/output
- ${MODEL_DIR:-./data/models}:/opt/models
- ./scripts:/opt/scripts:ro
command:
[
"mineru-router",
"--api-urls",
"http://mineru-worker0:8001,http://mineru-worker1:8002",
"--host", "0.0.0.0",
"--port", "8000",
"--allow-public-http-client",
]
depends_on: depends_on:
- worker0 - worker0
- worker1 - worker1
+22
View File
@@ -0,0 +1,22 @@
upstream mineru_workers {
# 轮询分发
server mineru-worker0:8001;
server mineru-worker1:8002;
}
server {
listen 8000;
server_name _;
# 文件上传可能很大,调大限制
client_max_body_size 500m;
location / {
proxy_pass http://mineru_workers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 600s; # 长任务需要
proxy_send_timeout 600s;
}
}
+23 -47
View File
@@ -17,44 +17,40 @@ from pathlib import Path
import gradio as gr import gradio as gr
import httpx import httpx
API_BASE = os.environ.get("MINERU_API_BASE", "http://mineru-router:8000") API_BASE = os.environ.get("MINERU_API_BASE", "http://mineru-nginx:8000")
DEFAULT_BACKEND = os.environ.get("MINERU_BACKEND", "hybrid-auto-engine") DEFAULT_BACKEND = os.environ.get("MINERU_BACKEND", "hybrid-engine")
DEFAULT_LANG = os.environ.get("MINERU_LANG", "ch") DEFAULT_LANG = os.environ.get("MINERU_LANG", "ch")
POLL_INTERVAL = float(os.environ.get("MINERU_POLL_INTERVAL", "1.0")) POLL_INTERVAL = float(os.environ.get("MINERU_POLL_INTERVAL", "1.0"))
MAX_WAIT = float(os.environ.get("MINERU_MAX_WAIT", "600")) MAX_WAIT = float(os.environ.get("MINERU_MAX_WAIT", "600"))
async def discover_api(client: httpx.AsyncClient) -> str: async def discover_api(client: httpx.AsyncClient) -> str:
"""探测实际可用的 API 前缀: /api/v1/tasks 或 /tasks""" """探测实际可用的 API 前缀: /file_parse 端点"""
candidates = [ candidates = [
f"{API_BASE}/api/v1/health", f"{API_BASE}/openapi.json",
f"{API_BASE}/health",
f"{API_BASE}/docs", f"{API_BASE}/docs",
] ]
for url in candidates: for url in candidates:
try: try:
r = await client.get(url, timeout=5) r = await client.get(url, timeout=5)
if r.status_code < 500: if r.status_code == 200:
# FastAPI docs → /api/v1 ; plain health → / return "" # worker uses root-level endpoints like /file_parse
if "/api/v1" in str(r.url):
return "/api/v1/tasks"
return "/tasks"
except Exception: except Exception:
continue continue
# 默认新版 API return ""
return "/api/v1/tasks"
async def submit_task(client: httpx.AsyncClient, api_prefix: str, async def submit_task(client: httpx.AsyncClient, api_prefix: str,
file_path: str, file_name: str, file_path: str, file_name: str,
backend: str, lang: str) -> dict: backend: str, lang: str) -> dict:
"""提交解析任务""" """提交解析任务到 worker 的 /file_parse 端点"""
submit_url = f"{API_BASE}{api_prefix}/submit" submit_url = f"{API_BASE}/file_parse"
with open(file_path, "rb") as f: with open(file_path, "rb") as f:
files = {"file": (file_name, f, "application/pdf")} files = {"files": (file_name, f, "application/pdf")}
data = { data = {
"backend": backend, "backend": backend,
"lang": lang, "lang_list": lang,
"parse_method": "auto",
} }
r = await client.post(submit_url, files=files, data=data, timeout=30) r = await client.post(submit_url, files=files, data=data, timeout=30)
r.raise_for_status() r.raise_for_status()
@@ -64,7 +60,7 @@ async def submit_task(client: httpx.AsyncClient, api_prefix: str,
async def get_task_status(client: httpx.AsyncClient, api_prefix: str, async def get_task_status(client: httpx.AsyncClient, api_prefix: str,
task_id: str) -> dict: task_id: str) -> dict:
"""查询任务状态""" """查询任务状态"""
url = f"{API_BASE}{api_prefix}/{task_id}" url = f"{API_BASE}/tasks/{task_id}"
r = await client.get(url, timeout=10) r = await client.get(url, timeout=10)
r.raise_for_status() r.raise_for_status()
return r.json() return r.json()
@@ -72,24 +68,14 @@ async def get_task_status(client: httpx.AsyncClient, api_prefix: str,
async def download_result(client: httpx.AsyncClient, api_prefix: str, async def download_result(client: httpx.AsyncClient, api_prefix: str,
task_id: str, output_dir: str) -> str: task_id: str, output_dir: str) -> str:
"""下载任务结果。尝试多种 endpoint 格式""" """下载任务结果"""
candidates = [ url = f"{API_BASE}/tasks/{task_id}/result"
f"{api_prefix}/{task_id}/data", r = await client.get(url, timeout=60, follow_redirects=True)
f"{api_prefix}/{task_id}/result", r.raise_for_status()
] zip_path = os.path.join(output_dir, f"{task_id}.zip")
for suffix in candidates: with open(zip_path, "wb") as f:
url = f"{API_BASE}{suffix}" f.write(r.content)
try: return zip_path
r = await client.get(url, timeout=60, follow_redirects=True)
if r.status_code == 200:
# 下载 zip 文件
zip_path = os.path.join(output_dir, f"{task_id}.zip")
with open(zip_path, "wb") as f:
f.write(r.content)
return zip_path
except Exception:
continue
raise RuntimeError(f"Failed to download result for task {task_id}")
def extract_readme(zip_path: str, output_dir: str) -> str: def extract_readme(zip_path: str, output_dir: str) -> str:
@@ -127,18 +113,8 @@ async def process_pdf(file_obj, backend, lang, progress=gr.Progress()):
progress(0.15, desc="Submitting task...") progress(0.15, desc="Submitting task...")
try: try:
submit_resp = await submit_task(client, api_prefix, file_path, file_name, backend, lang) submit_resp = await submit_task(client, api_prefix, file_path, file_name, backend, lang)
except httpx.HTTPStatusError as e: except Exception as e:
# 尝试旧版 endpoint(不带 /submit 后缀) return f"Task submission failed: {e}", "", ""
alt_data = {"backend": backend, "lang": lang}
try:
alt_url = f"{API_BASE}{api_prefix}"
with open(file_path, "rb") as f:
files = {"file": (file_name, f, "application/pdf")}
r = await client.post(alt_url, files=files, data=alt_data, timeout=30)
r.raise_for_status()
submit_resp = r.json()
except Exception as e2:
return f"Task submission failed: {e}\nAlt attempt: {e2}", "", ""
task_id = submit_resp.get("task_id") or submit_resp.get("data", {}).get("task_id") task_id = submit_resp.get("task_id") or submit_resp.get("data", {}).get("task_id")
if not task_id: if not task_id: