x
This commit is contained in:
+11
-27
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user