mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-12 11:43:39 +08:00
## Summary
Migrate PaddleOCR integration from the deprecated synchronous HTTP API
to the new asynchronous Job API (`submit → poll → fetch`), aligning with
PaddleOCR 3.6.0+ architecture.
## Changes
### Python (`deepdoc/parser/paddleocr_parser.py`)
- Replace synchronous `requests.post()` with async Job API flow (submit
→ poll → fetch)
- Authentication: `token {token}` → `Bearer {token}`
- File transfer: base64 JSON body → multipart file upload
- Polling: exponential backoff (initial 3s, ×1.5, max 15s, timeout
controlled by `request_timeout`)
- Result: fetch full JSONL from result URL, preserving `prunedResult`
with bbox info for crop functionality
- Rename `api_url` → `base_url` (backward compatible: `api_url` still
accepted as fallback)
### Python (`rag/llm/ocr_model.py`)
- Prefer `paddleocr_base_url` / `PADDLEOCR_BASE_URL`, fallback to
`paddleocr_api_url` / `PADDLEOCR_API_URL`
### Go (`internal/entity/models/paddleocr.go`)
- Add `Client-Platform: ragflow` header to submit and poll requests
- Change polling from fixed 3s to exponential backoff (initial 3s, ×1.5,
max 15s)
### Python (`common/constants.py`)
- Add `PADDLEOCR_BASE_URL` to env keys and default config
## Backward Compatibility
- Old env var `PADDLEOCR_API_URL` still works (used as fallback)
- Frontend field `paddleocr_api_url` still works (backend reads it as
fallback)
- No user-facing configuration changes required for existing setups
## Why not use the `paddleocr` SDK package directly?
RAGFlow's `_transfer_to_sections()` relies on `prunedResult` (containing
`block_bbox`, `block_label`, `parsing_res_list`) from the raw API
response for PDF crop functionality. The SDK's public `parse_document()`
API only returns `DocParsingResult` with `markdown_text`, discarding the
bbox data. Therefore we implement the async Job API flow directly via
HTTP, following the same logic as the SDK internally.
206 lines
8.4 KiB
Python
206 lines
8.4 KiB
Python
#
|
|
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
import json
|
|
import logging
|
|
import os
|
|
from typing import Any, Optional
|
|
|
|
from deepdoc.parser.mineru_parser import MinerUParser
|
|
from deepdoc.parser.opendataloader_parser import OpenDataLoaderParser
|
|
from deepdoc.parser.paddleocr_parser import PaddleOCRParser
|
|
|
|
|
|
class Base:
|
|
def __init__(self, key: str | dict, model_name: str, **kwargs):
|
|
self.model_name = model_name
|
|
|
|
def parse_pdf(self, filepath: str, binary=None, **kwargs) -> tuple[Any, Any]:
|
|
raise NotImplementedError("Please implement parse_pdf!")
|
|
|
|
|
|
class MinerUOcrModel(Base, MinerUParser):
|
|
_FACTORY_NAME = "MinerU"
|
|
|
|
def __init__(self, key: str | dict, model_name: str, **kwargs):
|
|
Base.__init__(self, key, model_name, **kwargs)
|
|
raw_config = {}
|
|
if key:
|
|
try:
|
|
raw_config = json.loads(key)
|
|
except Exception:
|
|
raw_config = {}
|
|
|
|
# nested {"api_key": {...}} from UI
|
|
# flat {"MINERU_*": "..."} payload auto-provisioned from env vars
|
|
config = raw_config.get("api_key", raw_config)
|
|
if not isinstance(config, dict):
|
|
config = {}
|
|
|
|
def _resolve_config(key: str, env_key: str, default=""):
|
|
# lower-case keys (UI), upper-case MINERU_* (env auto-provision), env vars
|
|
return config.get(key, config.get(env_key, os.environ.get(env_key, default)))
|
|
|
|
self.mineru_api = _resolve_config("mineru_apiserver", "MINERU_APISERVER", "")
|
|
self.mineru_output_dir = _resolve_config("mineru_output_dir", "MINERU_OUTPUT_DIR", "")
|
|
self.mineru_backend = _resolve_config("mineru_backend", "MINERU_BACKEND", "pipeline")
|
|
self.mineru_server_url = _resolve_config("mineru_server_url", "MINERU_SERVER_URL", "")
|
|
self.mineru_delete_output = bool(int(_resolve_config("mineru_delete_output", "MINERU_DELETE_OUTPUT", 1)))
|
|
|
|
# Redact sensitive config keys before logging
|
|
redacted_config = {}
|
|
for k, v in config.items():
|
|
if any(sensitive_word in k.lower() for sensitive_word in ("key", "password", "token", "secret")):
|
|
redacted_config[k] = "[REDACTED]"
|
|
else:
|
|
redacted_config[k] = v
|
|
logging.info(f"Parsed MinerU config (sensitive fields redacted): {redacted_config}")
|
|
|
|
MinerUParser.__init__(self, mineru_api=self.mineru_api, mineru_server_url=self.mineru_server_url)
|
|
|
|
def check_available(self, backend: Optional[str] = None, server_url: Optional[str] = None) -> tuple[bool, str]:
|
|
backend = backend or self.mineru_backend
|
|
server_url = server_url or self.mineru_server_url
|
|
return self.check_installation(backend=backend, server_url=server_url)
|
|
|
|
def parse_pdf(self, filepath: str, binary=None, callback=None, parse_method: str = "raw", **kwargs):
|
|
ok, reason = self.check_available()
|
|
if not ok:
|
|
raise RuntimeError(f"MinerU server not accessible: {reason}")
|
|
|
|
sections, tables = MinerUParser.parse_pdf(
|
|
self,
|
|
filepath=filepath,
|
|
binary=binary,
|
|
callback=callback,
|
|
output_dir=self.mineru_output_dir,
|
|
backend=self.mineru_backend,
|
|
server_url=self.mineru_server_url,
|
|
delete_output=self.mineru_delete_output,
|
|
parse_method=parse_method,
|
|
**kwargs,
|
|
)
|
|
return sections, tables
|
|
|
|
|
|
class PaddleOCROcrModel(Base, PaddleOCRParser):
|
|
_FACTORY_NAME = "PaddleOCR"
|
|
|
|
def __init__(self, key: str | dict, model_name: str, **kwargs):
|
|
Base.__init__(self, key, model_name, **kwargs)
|
|
raw_config = {}
|
|
if key:
|
|
try:
|
|
raw_config = json.loads(key)
|
|
except Exception:
|
|
raw_config = {}
|
|
|
|
# nested {"api_key": {...}} from UI
|
|
# flat {"PADDLEOCR_*": "..."} payload auto-provisioned from env vars
|
|
config = raw_config.get("api_key", raw_config)
|
|
if not isinstance(config, dict):
|
|
config = {}
|
|
|
|
def _resolve_config(key: str, env_key: str, default=""):
|
|
# lower-case keys (UI), upper-case PADDLEOCR_* (env auto-provision), env vars
|
|
return config.get(key, config.get(env_key, os.environ.get(env_key, default)))
|
|
|
|
self.paddleocr_base_url = _resolve_config("paddleocr_base_url", "PADDLEOCR_BASE_URL", "") or _resolve_config("paddleocr_api_url", "PADDLEOCR_API_URL", "")
|
|
self.paddleocr_algorithm = _resolve_config("paddleocr_algorithm", "PADDLEOCR_ALGORITHM", "PaddleOCR-VL")
|
|
self.paddleocr_access_token = _resolve_config("paddleocr_access_token", "PADDLEOCR_ACCESS_TOKEN", None)
|
|
|
|
# Redact sensitive config keys before logging
|
|
redacted_config = {}
|
|
for k, v in config.items():
|
|
if any(sensitive_word in k.lower() for sensitive_word in ("key", "password", "token", "secret")):
|
|
redacted_config[k] = "[REDACTED]"
|
|
else:
|
|
redacted_config[k] = v
|
|
logging.info(f"Parsed PaddleOCR config (sensitive fields redacted): {redacted_config}")
|
|
|
|
PaddleOCRParser.__init__(
|
|
self,
|
|
base_url=self.paddleocr_base_url or None,
|
|
access_token=self.paddleocr_access_token,
|
|
algorithm=self.paddleocr_algorithm,
|
|
)
|
|
|
|
def check_available(self) -> tuple[bool, str]:
|
|
return self.check_installation()
|
|
|
|
def parse_pdf(self, filepath: str, binary=None, callback=None, parse_method: str = "raw", **kwargs):
|
|
ok, reason = self.check_available()
|
|
if not ok:
|
|
raise RuntimeError(f"PaddleOCR server not accessible: {reason}")
|
|
|
|
sections, tables = PaddleOCRParser.parse_pdf(self, filepath=filepath, binary=binary, callback=callback, parse_method=parse_method, **kwargs)
|
|
return sections, tables
|
|
|
|
|
|
class OpenDataLoaderOcrModel(Base, OpenDataLoaderParser):
|
|
_FACTORY_NAME = "OpenDataLoader"
|
|
|
|
def __init__(self, key: str | dict, model_name: str, **kwargs):
|
|
Base.__init__(self, key, model_name, **kwargs)
|
|
raw_config = {}
|
|
if key:
|
|
try:
|
|
raw_config = json.loads(key)
|
|
except Exception:
|
|
raw_config = {}
|
|
|
|
config = raw_config.get("api_key", raw_config)
|
|
if not isinstance(config, dict):
|
|
config = {}
|
|
|
|
def _resolve_config(key: str, env_key: str, default=""):
|
|
return config.get(key, config.get(env_key, os.environ.get(env_key, default)))
|
|
|
|
redacted_config = {}
|
|
for k, v in config.items():
|
|
if any(s in k.lower() for s in ("key", "password", "token", "secret")):
|
|
redacted_config[k] = "[REDACTED]"
|
|
else:
|
|
redacted_config[k] = v
|
|
logging.info(f"Parsed OpenDataLoader config (sensitive fields redacted): {redacted_config}")
|
|
|
|
OpenDataLoaderParser.__init__(self)
|
|
self.api_url = _resolve_config("opendataloader_apiserver", "OPENDATALOADER_APISERVER", "").rstrip("/")
|
|
self.api_key = _resolve_config("opendataloader_api_key", "OPENDATALOADER_API_KEY", "").strip()
|
|
timeout_val = _resolve_config("opendataloader_timeout", "OPENDATALOADER_TIMEOUT", "600") or "600"
|
|
try:
|
|
self.timeout = int(timeout_val)
|
|
except (TypeError, ValueError):
|
|
self.timeout = 600
|
|
|
|
def check_available(self) -> tuple[bool, str]:
|
|
ok = self.check_installation()
|
|
return ok, "" if ok else "OpenDataLoader service not reachable"
|
|
|
|
def parse_pdf(self, filepath: str, binary=None, callback=None, parse_method: str = "raw", **kwargs):
|
|
ok, reason = self.check_available()
|
|
if not ok:
|
|
raise RuntimeError(f"OpenDataLoader service not accessible: {reason}")
|
|
|
|
sections, tables = OpenDataLoaderParser.parse_pdf(
|
|
self,
|
|
filepath=filepath,
|
|
binary=binary,
|
|
callback=callback,
|
|
parse_method=parse_method,
|
|
**kwargs,
|
|
)
|
|
return sections, tables
|