Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/en/guides/01-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,36 @@ Use `github_domains`, `gitlab_domains`, or `azure_devops_domains` when the host
needs those platform-specific URL semantics. Add other Git hosts to
`code_hosting_domains`.

### pdf

PDF parsing configuration. Three strategies are supported: `local` (local pdfplumber), `mineru` (remote MinerU API), and `auto` (try local first, fall back to MinerU).

```json
{
"pdf": {
"strategy": "auto",
"mineru_endpoint": "http://127.0.0.1:8000",
"mineru_api_key": "your-api-key",

@Tsan1024 Tsan1024 Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里方便提供下使用的是哪个版本的开源镜像服务吗?MinerU 3.4.4 提到的鉴权是MinerU -> vLLM 的下游鉴权,这里的api-key 是vllm部分的输入?

@zonas0574 zonas0574 Aug 13, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

对是3.4.4,但这个其实目前没有用,我看之前在我也没删就保留,要不我先删了 = =

@Tsan1024 Tsan1024 Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

对是3.4.4,但这个其实目前没有用,我看之前在我也没删就保留,要不我先删了 = =

之前的应该是有开发者支持了一版调用mineru官方api的方式,里面会有apikey存在。可以看看如何兼容官方api和自运营api方式哈,比如加个字段区分?

@Tsan1024 Tsan1024 Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当 PDF 解析策略为 mineru 时,MinerU 是必需依赖;当策略为 auto 且配置了
mineru_endpoint 时,它也承担本地解析失败后的回退职责。目前在实际解析 PDF 时才
调用 /file_parse,因此 endpoint 配置错误、服务未启动或协议不兼容可能会延后到运
行期才发现。

这里是否可以考虑在 OpenViking 初始化完成前增加一个 MinerU 启动预检?例如:

  • 对 {mineru_endpoint}/health 做一次短时轮询。
  • 收到 HTTP 200、合法 JSON 且包含 protocol_version 时视为就绪。
  • strategy="mineru",或 strategy="auto" 且配置了 mineru_endpoint 时执行。
  • strategy="local",以及未配置 endpoint 的 auto 模式可跳过。
  • 若预检超时,启动时给出包含 endpoint、最后错误和修复建议的提示。

这样可以更早发现依赖配置问题,也避免首个 PDF 导入任务才暴露错误

示例

  import asyncio
  import time

  import httpx


  async def wait_for_mineru_ready(endpoint: str, timeout: float = 5.0) -> None:
      health_url = f"{endpoint.rstrip('/')}/health"
      deadline = time.monotonic() + timeout
      last_error = "no response"

      async with httpx.AsyncClient(timeout=0.5) as client:
          while time.monotonic() < deadline:
              try:
                  response = await client.get(health_url)
                  payload = response.json()

                  if (
                      response.status_code == 200
                      and isinstance(payload, dict)
                      and payload.get("protocol_version")
                  ):
                      return

                  last_error = (
                      f"unexpected response: HTTP {response.status_code}, "
                      f"protocol_version={payload.get('protocol_version')!r}"
                  )
              except (httpx.HTTPError, ValueError) as exc:
                  last_error = str(exc)

              await asyncio.sleep(0.2)

      raise RuntimeError(
          f"MinerU startup preflight failed for {health_url} after {timeout}s. "
          f"Last error: {last_error}. "
          "Start a compatible mineru-api service or correct
          pdf.mineru_endpoint."
      )

在服务初始化流程中、设置 _initialized = True 前调用:

  pdf_config = self._config.pdf

  should_preflight_mineru = (
      pdf_config.strategy == "mineru"
      or (
          pdf_config.strategy == "auto"
          and pdf_config.mineru_endpoint is not None
      )
  )

  if should_preflight_mineru and pdf_config.mineru_endpoint:
      await wait_for_mineru_ready(pdf_config.mineru_endpoint)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

对是3.4.4,但这个其实目前没有用,我看之前在我也没删就保留,要不我先删了 = =

之前的应该是有开发者支持了一版调用mineru官方api的方式,里面会有apikey存在。可以看看如何兼容官方api和自运营api方式哈,比如加个字段区分?

官方我看不支持file_parse,只有异步提交任务,然后查询任务,和现在方式不是特别契合,不知道是不是可以和知识库解析一样变成队列

"mineru_timeout": 300.0,
"mineru_bodys": {
"backend": "hybrid-auto-engine",
"lang_list": ["ch"],
"parse_method": "auto"
}
}
}
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `strategy` | str | Parsing strategy: `local` / `mineru` / `auto` (default `auto`) |
| `mineru_endpoint` | str | MinerU API **base URL** (e.g. `http://127.0.0.1:8000`) |
| `mineru_api_key` | str | MinerU API authentication key (optional) |
| `mineru_timeout` | float | Request timeout in seconds (default `300.0`) |
| `mineru_bodys` | dict | MinerU API multipart form fields |

**MinerU protocol**: a synchronous `POST {mineru_endpoint}/file_parse` request with the PDF as the multipart `files` field; form parameters are passed through from `mineru_bodys`.

### rerank

Reranking model for search result refinement. Supports VikingDB (Volcengine), Cohere, and OpenAI-compatible APIs.
Expand Down
30 changes: 30 additions & 0 deletions docs/zh/guides/01-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,36 @@ ollama pull guoxuter/ov_intent_analysis_sft:v7_q8
需要 GitHub、GitLab 或 Azure DevOps 专属 URL 语义时,应配置到对应的平台字段;
其他 Git 主机统一添加到 `code_hosting_domains`。

### pdf

PDF 解析配置。支持三种策略:`local`(本地 pdfplumber)、`mineru`(远程 MinerU API)、`auto`(先本地、失败回退 MinerU)。

```json
{
"pdf": {
"strategy": "auto",
"mineru_endpoint": "http://127.0.0.1:8000",
"mineru_api_key": "your-api-key",
"mineru_timeout": 300.0,
"mineru_bodys": {
"backend": "hybrid-auto-engine",
"lang_list": ["ch"],
"parse_method": "auto"
}
}
}
```

| 参数 | 类型 | 说明 |
|------|------|------|
| `strategy` | str | 解析策略:`local` / `mineru` / `auto`(默认 `auto`) |
| `mineru_endpoint` | str | MinerU API **base URL**(如 `http://127.0.0.1:8000`) |
| `mineru_api_key` | str | MinerU API 认证密钥(可选) |
| `mineru_timeout` | float | 请求超时秒数(默认 `300.0`) |
| `mineru_bodys` | dict | MinerU API multipart form 参数 |

**MinerU 协议**:同步调用 `POST {mineru_endpoint}/file_parse`,multipart 文件字段为 `files`,form 参数由 `mineru_bodys` 透传。

### rerank

用于搜索结果精排的 Rerank 模型。支持 VikingDB (火山引擎)、Cohere 和 OpenAI 兼容接口。
Expand Down
7 changes: 6 additions & 1 deletion examples/ov.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,14 @@
"max_section_size": 4000,
"section_size_flexibility": 0.3,
"max_section_chars": 6000,
"mineru_endpoint": "https://mineru.example.com/api/v1",
"mineru_endpoint": "http://127.0.0.1:8000",
"mineru_api_key": "{your-mineru-api-key}",
"mineru_timeout": 300.0,
"mineru_bodys": {
"backend": "hybrid-auto-engine",
"lang_list": ["ch"],
"parse_method": "auto"
},
},
"code": {
"github_raw_domain": "raw.githubusercontent.com",
Expand Down
111 changes: 86 additions & 25 deletions openviking/parse/parsers/pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""

import asyncio
import base64
import hashlib
import io
import re
Expand All @@ -29,6 +30,7 @@
lazy_import,
)
from openviking.parse.parsers.base_parser import BaseParser
from openviking.utils.time_utils import parse_iso_datetime
from openviking_cli.utils import get_logger
from openviking_cli.utils.config.parser_config import PDFConfig

Expand Down Expand Up @@ -57,7 +59,7 @@ class PDFParser(BaseParser):
>>> # Remote API parsing
>>> config = PDFConfig(
... strategy="mineru",
... mineru_endpoint="https://api.example.com/convert",
... mineru_endpoint="http://127.0.0.1:8000",
... mineru_api_key="key"
... )
>>> parser = PDFParser(config)
Expand Down Expand Up @@ -682,27 +684,38 @@ def _extract_image_from_page(self, page, img_info: dict) -> Optional[bytes]:
async def _convert_mineru(
self,
pdf_path: Path,
storage=None,
resource_name: Optional[str] = None,
) -> tuple[str, Dict[str, Any]]:
"""
Convert PDF to Markdown using MinerU API.
Convert PDF to Markdown using the MinerU /file_parse API.

Args:
pdf_path: Path to PDF file
resource_name: Optional resource name (unused in MinerU conversion)
storage: Media storage used to persist extracted images; defaults to
the configured storage if None
resource_name: Resource name under which extracted images are saved;
defaults to the PDF stem

Returns:
Tuple of (markdown_content, metadata)
Tuple of (markdown_content, metadata) where metadata includes
strategy, endpoint, api_version, backend, task_id, processing_time
(seconds) and images_saved

Raises:
ImportError: If httpx not installed
Exception: If API call fails
ValueError: If MinerU endpoint is not configured, or the task does
not complete
Exception: If the API call fails
"""
httpx = lazy_import("httpx")

if not self.config.mineru_endpoint:
raise ValueError("MinerU endpoint not configured")

# mineru_endpoint is a base URL; append /file_parse unless it already ends with it
base = self.config.mineru_endpoint.rstrip("/")
url = base if base.endswith("/file_parse") else f"{base}/file_parse"

meta = {
"strategy": "mineru",
"endpoint": self.config.mineru_endpoint,
Expand All @@ -713,44 +726,92 @@ async def _convert_mineru(
async with httpx.AsyncClient(timeout=self.config.mineru_timeout) as client:
# Prepare file upload
with open(pdf_path, "rb") as f:
files = {"file": (pdf_path.name, f, "application/pdf")}
files = {"files": (pdf_path.name, f, "application/pdf")}

# Prepare headers
headers = {}
if self.config.mineru_api_key:
headers["Authorization"] = f"Bearer {self.config.mineru_api_key}"

# Prepare request params
params = self.config.mineru_params or {}
# Prepare Form fields
data: Dict[str, Any] = dict(self.config.mineru_bodys or {})

# MinerU must return extracted images for the markdown refs.
data["return_images"] = True

# Make API request
logger.info(f"Calling MinerU API: {self.config.mineru_endpoint}")
logger.info(f"Calling MinerU API: {url}")
response = await client.post(
self.config.mineru_endpoint,
url,
files=files,
data=data,
headers=headers,
params=params,
)
response.raise_for_status()

# Parse response
result = response.json()
markdown_content = result.get("markdown", "")
# Parse response
result = response.json()
if result.get("status") != "completed":
raise ValueError(f"MinerU task not completed: {result.get('status')}")

results = result.get("results") or {}
file_result = results.get(pdf_path.name) or next(iter(results.values()), {})
markdown_content = file_result.get("md_content") or ""

# Extract metadata from response
meta["api_version"] = result.get("version")
meta["backend"] = result.get("backend")
meta["task_id"] = result.get("task_id")
started_at, completed_at = result.get("started_at"), result.get("completed_at")
if started_at and completed_at:
meta["processing_time"] = (
parse_iso_datetime(completed_at) - parse_iso_datetime(started_at)
).total_seconds()

if not markdown_content:
logger.warning(f"MinerU returned empty content for {pdf_path}")
return markdown_content, meta

# Extract metadata from response
meta["api_version"] = result.get("version")
meta["processing_time"] = result.get("processing_time")
meta["total_pages"] = result.get("total_pages")
if storage is None:
from openviking_cli.utils.storage import get_storage

if not markdown_content:
logger.warning(f"MinerU returned empty content for {pdf_path}")
storage = get_storage()

logger.info(
f"MinerU conversion: {meta.get('total_pages', '?')} pages → "
f"{len(markdown_content)} chars"
if resource_name is None:
resource_name = pdf_path.stem

# MinerU embeds images as base64 data-URLs, referenced from markdown
# as `images/<filename>`; save them into the media store and rewrite
# the references to the stored relative paths.
repl: Dict[str, str] = {}
media_dir = storage.media_dir
for img_name, data_url in (file_result.get("images") or {}).items():
try:
# data URL form: "data:image/jpeg;base64,<b64>"
image_bytes = base64.b64decode(data_url.split(",", 1)[-1])
img_path = Path(img_name)
image_path = storage.save_image(
resource_name,
image_bytes,
filename=img_path.stem,
extension=img_path.suffix or ".png",
)
repl[f"images/{img_name}"] = image_path.relative_to(media_dir).as_posix()
except Exception as img_err:
logger.warning(f"Failed to save MinerU image {img_name}: {img_err}")

if repl:
# Single pass over the markdown replaces every known reference;
# unknown ``images/...`` text is left untouched.
ref_pattern = re.compile(
"|".join(re.escape(ref) for ref in sorted(repl, key=len, reverse=True))
)
markdown_content = ref_pattern.sub(lambda m: repl[m.group(0)], markdown_content)
meta["images_saved"] = len(repl)

return markdown_content, meta
logger.info(f"MinerU conversion: {len(markdown_content)} chars")

return markdown_content, meta

except Exception as e:
logger.error(f"MinerU API call failed: {e}")
Expand Down
4 changes: 2 additions & 2 deletions openviking_cli/utils/config/parser_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ class PDFConfig(ParserConfig):
mineru_endpoint: MinerU API endpoint URL
mineru_api_key: MinerU API authentication key
mineru_timeout: MinerU request timeout in seconds
mineru_params: Additional MinerU API parameters
mineru_bodys: Additional MinerU API multipart form fields
"""

strategy: str = "auto" # "local" | "mineru" | "auto"
Expand All @@ -159,7 +159,7 @@ class PDFConfig(ParserConfig):
mineru_endpoint: Optional[str] = None # API endpoint URL
mineru_api_key: Optional[str] = None # API authentication key
mineru_timeout: float = 300.0 # Request timeout in seconds (5 minutes)
mineru_params: Optional[dict] = None # Additional API parameters
mineru_bodys: Optional[dict] = None # Additional API multipart form fields

# Heading detection configuration
heading_detection: str = "auto" # "bookmarks" | "font" | "auto" | "none"
Expand Down