Sfoglia il codice sorgente

feat: 实现基于 YAML/JSON 的统一配置文件系统

- 移除对环境变量和 .env 的直接依赖,统一通过 config.py 管理配置
- 新增 config_loader.py 支持类型安全的配置加载
- 重构所有相关模块以使用统一配置系统
- 添加详细配置文档 CONFIG.md 和测试脚本 test_config.py

Co-authored-by: Cursor <cursoragent@cursor.com>
何文松 2 settimane fa
parent
commit
080d9e4463

+ 8 - 0
pdf_converter_v2/.gitignore

@@ -75,6 +75,14 @@ logs/
 *.local.yml
 .env.*
 
+# Production config (should not be committed)
+config.prod.json
+config.prod.yaml
+config.prod.yml
+config.production.json
+config.production.yaml
+config.production.yml
+
 # Editor / IDE
 .vscode/
 .idea/

+ 243 - 0
pdf_converter_v2/CONFIG.md

@@ -0,0 +1,243 @@
+# 配置文件说明
+
+## 概述
+
+PDF Converter v2 支持使用配置文件来管理所有配置项,**不再依赖环境变量或 `.env` 文件**。
+
+## 配置文件格式
+
+支持以下三种格式:
+
+1. **YAML 格式**(推荐):`config.yaml` 或 `config.yml`
+2. **JSON 格式**:`config.json`
+
+配置文件应放置在 `pdf_converter_v2` 目录下。
+
+## 配置文件查找顺序
+
+程序会按以下顺序自动查找配置文件:
+
+1. `pdf_converter_v2/config.yaml`
+2. `pdf_converter_v2/config.yml`
+3. `pdf_converter_v2/config.json`
+
+找到第一个存在的配置文件后,将使用该文件的配置。
+
+## 使用方法
+
+### 方法 1:使用 YAML 格式(推荐)
+
+1. 将 `config.yaml` 复制并根据需要修改配置项
+2. 所有配置项都是可选的,未指定的项将使用默认值
+
+```bash
+# 查看默认配置文件
+cat pdf_converter_v2/config.yaml
+```
+
+### 方法 2:使用 JSON 格式
+
+1. 复制 `config.json.example` 为 `config.json`
+2. 根据需要修改配置项
+
+```bash
+cp pdf_converter_v2/config.json.example pdf_converter_v2/config.json
+```
+
+### 方法 3:指定自定义配置文件(程序中)
+
+在 Python 代码中指定配置文件路径:
+
+```python
+from pdf_converter_v2.config_loader import reload_config
+
+# 重新加载指定的配置文件
+reload_config("/path/to/your/config.yaml")
+```
+
+## 配置项说明
+
+### 设备环境配置
+
+| 配置项 | 类型 | 默认值 | 说明 |
+|--------|------|--------|------|
+| `device_kind` | string | 自动检测 | 设备类型:`nvi`(NVIDIA GPU)/ `npu`(华为昇腾)/ `cpu` |
+
+### 模型配置
+
+| 配置项 | 类型 | 默认值 | 说明 |
+|--------|------|--------|------|
+| `default_model_name` | string | `OpenDataLab/MinerU2.5-2509-1.2B` | 默认模型名称 |
+| `default_gpu_memory_utilization` | float | `0.9` | GPU 内存利用率(0.0-1.0) |
+| `default_dpi` | int | `200` | DPI 设置 |
+| `default_max_pages` | int | `10` | 最大页数限制 |
+
+### API 配置
+
+| 配置项 | 类型 | 默认值 | 说明 |
+|--------|------|--------|------|
+| `api_url` | string | `http://127.0.0.1:5282` | MinerU API 服务地址 |
+| `backend` | string | `vlm-vllm-async-engine` | 处理后端:`vlm-vllm-async-engine` / `pipeline` |
+| `parse_method` | string | `auto` | 解析方法:`auto` / `txt` / `ocr` |
+| `start_page_id` | int | `0` | 起始页ID(从0开始) |
+| `end_page_id` | int | `99999` | 结束页ID |
+| `language` | string | `ch` | 识别语言:`ch` / `en` |
+| `server_url` | string | `string` | 服务器URL |
+
+### 返回格式配置
+
+| 配置项 | 类型 | 默认值 | 说明 |
+|--------|------|--------|------|
+| `response_format_zip` | bool | `true` | 是否返回 ZIP 格式 |
+| `return_middle_json` | bool | `false` | 是否返回中间 JSON |
+| `return_model_output` | bool | `true` | 是否返回模型输出 |
+| `return_md` | bool | `true` | 是否返回 Markdown |
+| `return_images` | bool | `false` | 是否返回图片 |
+| `return_content_list` | bool | `false` | 是否返回内容列表 |
+
+### 日志配置
+
+| 配置项 | 类型 | 默认值 | 说明 |
+|--------|------|--------|------|
+| `log_dir` | string | `./logs` | 日志目录 |
+| `log_level` | string | `INFO` | 日志级别:`DEBUG` / `INFO` / `WARNING` / `ERROR` |
+| `log_to_file` | bool | `true` | 是否记录到文件 |
+| `log_to_console` | bool | `true` | 是否输出到控制台 |
+
+## 配置示例
+
+### YAML 格式示例
+
+```yaml
+# 修改 API 地址
+api_url: "http://192.168.1.100:5282"
+
+# 修改语言为英文
+language: "en"
+
+# 启用图片返回
+return_images: true
+
+# 修改日志级别
+log_level: "DEBUG"
+```
+
+### JSON 格式示例
+
+```json
+{
+  "api_url": "http://192.168.1.100:5282",
+  "language": "en",
+  "return_images": true,
+  "log_level": "DEBUG"
+}
+```
+
+## 依赖说明
+
+### YAML 格式支持
+
+如果使用 YAML 格式的配置文件,需要安装 PyYAML:
+
+```bash
+pip install pyyaml
+```
+
+### JSON 格式支持
+
+JSON 格式使用 Python 标准库,无需额外安装依赖。
+
+## 配置优先级
+
+1. 程序会按照查找顺序使用第一个找到的配置文件
+2. 配置文件中未指定的项将使用默认值
+3. 所有配置项都是可选的,可以只配置需要修改的项
+
+## 注意事项
+
+1. **完全移除了环境变量依赖**:不再读取任何环境变量
+2. **配置文件优先**:所有配置都从配置文件读取
+3. **向后兼容**:如果没有配置文件,将使用合理的默认值
+4. **类型安全**:配置加载器会自动进行类型转换和验证
+
+## 示例:快速开始
+
+1. 复制默认配置文件:
+```bash
+cd pdf_converter_v2
+# YAML格式(已存在)
+# 或者使用 JSON 格式
+cp config.json.example config.json
+```
+
+2. 修改配置(例如修改 API 地址):
+```bash
+# 编辑 config.yaml
+vim config.yaml
+
+# 或编辑 config.json
+vim config.json
+```
+
+3. 启动服务:
+```bash
+python -m pdf_converter_v2.api_server
+# 或
+uvicorn pdf_converter_v2.api.main:app --host 0.0.0.0 --port 8000
+```
+
+配置会自动加载,无需任何额外操作!
+
+## 故障排除
+
+### 问题:配置文件未生效
+
+**解决方法**:
+1. 确认配置文件路径正确(在 `pdf_converter_v2/` 目录下)
+2. 检查配置文件格式是否正确(YAML 或 JSON)
+3. 查看日志输出,确认配置文件是否被正确加载
+
+### 问题:YAML 文件报错
+
+**解决方法**:
+1. 确保已安装 PyYAML:`pip install pyyaml`
+2. 检查 YAML 语法是否正确(缩进、冒号、引号等)
+3. 可以使用在线 YAML 验证工具检查语法
+
+### 问题:想要使用多个配置文件
+
+**解决方法**:
+可以通过程序代码指定不同的配置文件:
+
+```python
+from pdf_converter_v2.config_loader import reload_config
+
+# 开发环境
+reload_config("config.dev.yaml")
+
+# 生产环境
+reload_config("config.prod.yaml")
+```
+
+## 迁移指南
+
+### 从环境变量迁移到配置文件
+
+如果之前使用环境变量,现在迁移到配置文件很简单:
+
+1. **创建配置文件**:复制 `config.yaml` 或 `config.json.example`
+2. **映射环境变量**:将环境变量映射到配置文件项
+
+例如:
+
+| 旧环境变量 | 新配置项 |
+|-----------|---------|
+| `API_URL` | `api_url` |
+| `BACKEND` | `backend` |
+| `LANGUAGE` | `language` |
+| `LOG_LEVEL` | `log_level` |
+
+3. **删除 `.env` 文件**(如果有)
+4. **重启服务**
+
+完成!现在配置完全由配置文件管理。

+ 41 - 0
pdf_converter_v2/README.md

@@ -16,6 +16,47 @@ v2版本通过调用新的API接口(`http://127.0.0.1:5282/file_parse`)进
 6. **部署优化**: 支持命令行参数和systemd服务部署
 7. **性能优化**: 使用外部API接口,转换速度更快
 8. **保持兼容**: 复用v1的json解析逻辑,保持输出格式一致
+9. **配置文件**: 使用 YAML/JSON 配置文件,不再依赖环境变量
+
+## 配置说明
+
+v2 版本使用配置文件(YAML 或 JSON 格式)管理所有配置,**不再使用环境变量或 .env 文件**。
+
+### 快速配置
+
+1. **查看默认配置**:
+   ```bash
+   cat pdf_converter_v2/config.yaml
+   ```
+
+2. **修改配置**(可选):
+   ```bash
+   vim pdf_converter_v2/config.yaml
+   ```
+
+3. **测试配置**:
+   ```bash
+   python pdf_converter_v2/test_config.py
+   ```
+
+### 常用配置项
+
+```yaml
+# API 配置
+api_url: "http://127.0.0.1:5282"
+backend: "vlm-vllm-async-engine"
+language: "ch"
+
+# 日志配置
+log_level: "INFO"
+log_dir: "./logs"
+```
+
+### 详细文档
+
+- 📖 [配置文件快速入门](QUICKSTART_CONFIG.md)
+- 📋 [完整配置说明](CONFIG.md)
+- 🔄 [从环境变量迁移](MIGRATION.md)
 
 ## 使用方法
 

+ 21 - 18
pdf_converter_v2/api/main.py

@@ -27,34 +27,37 @@ from ..processor.converter import convert_to_markdown, convert_pdf_to_markdown_o
 from ..utils.logging_config import get_logger
 from ..utils.pdf_watermark_remover import remove_watermark_from_pdf, crop_header_footer_from_pdf
 
-# 尝试导入配置,如果不存在则使用默认值
+# 尝试导入配置
 try:
     from ..config import (
         DEFAULT_MODEL_NAME, DEFAULT_GPU_MEMORY_UTILIZATION, DEFAULT_DPI, DEFAULT_MAX_PAGES,
         DEFAULT_API_URL, DEFAULT_BACKEND, DEFAULT_PARSE_METHOD, DEFAULT_START_PAGE_ID,
         DEFAULT_END_PAGE_ID, DEFAULT_LANGUAGE, DEFAULT_RESPONSE_FORMAT_ZIP,
         DEFAULT_RETURN_MIDDLE_JSON, DEFAULT_RETURN_MODEL_OUTPUT, DEFAULT_RETURN_MD,
-        DEFAULT_RETURN_IMAGES, DEFAULT_RETURN_CONTENT_LIST, DEFAULT_SERVER_URL
+        DEFAULT_RETURN_IMAGES, DEFAULT_RETURN_CONTENT_LIST, DEFAULT_SERVER_URL,
+        LOG_DIR, LOG_LEVEL
     )
 except ImportError:
-    # 如果配置不存在,使用默认值
+    # 如果导入失败,使用硬编码的默认值(不推荐,正常情况下应该能导入)
     DEFAULT_MODEL_NAME = "OpenDataLab/MinerU2.5-2509-1.2B"
     DEFAULT_GPU_MEMORY_UTILIZATION = 0.9
     DEFAULT_DPI = 200
     DEFAULT_MAX_PAGES = 10
-    DEFAULT_API_URL = os.getenv("API_URL", "http://127.0.0.1:5282")
-    DEFAULT_BACKEND = os.getenv("BACKEND", "pipeline")
-    DEFAULT_PARSE_METHOD = os.getenv("PARSE_METHOD", "auto")
-    DEFAULT_START_PAGE_ID = int(os.getenv("START_PAGE_ID", "0"))
-    DEFAULT_END_PAGE_ID = int(os.getenv("END_PAGE_ID", "99999"))
-    DEFAULT_LANGUAGE = os.getenv("LANGUAGE", "ch")
-    DEFAULT_RESPONSE_FORMAT_ZIP = os.getenv("RESPONSE_FORMAT_ZIP", "true").lower() == "true"
-    DEFAULT_RETURN_MIDDLE_JSON = os.getenv("RETURN_MIDDLE_JSON", "false").lower() == "true"
-    DEFAULT_RETURN_MODEL_OUTPUT = os.getenv("RETURN_MODEL_OUTPUT", "true").lower() == "true"
-    DEFAULT_RETURN_MD = os.getenv("RETURN_MD", "true").lower() == "true"
-    DEFAULT_RETURN_IMAGES = os.getenv("RETURN_IMAGES", "true").lower() == "true"  # 默认启用,以便PaddleOCR备用解析可以使用
-    DEFAULT_RETURN_CONTENT_LIST = os.getenv("RETURN_CONTENT_LIST", "false").lower() == "true"
-    DEFAULT_SERVER_URL = os.getenv("SERVER_URL", "string")
+    DEFAULT_API_URL = "http://127.0.0.1:5282"
+    DEFAULT_BACKEND = "vlm-vllm-async-engine"
+    DEFAULT_PARSE_METHOD = "auto"
+    DEFAULT_START_PAGE_ID = 0
+    DEFAULT_END_PAGE_ID = 99999
+    DEFAULT_LANGUAGE = "ch"
+    DEFAULT_RESPONSE_FORMAT_ZIP = True
+    DEFAULT_RETURN_MIDDLE_JSON = False
+    DEFAULT_RETURN_MODEL_OUTPUT = True
+    DEFAULT_RETURN_MD = True
+    DEFAULT_RETURN_IMAGES = True
+    DEFAULT_RETURN_CONTENT_LIST = False
+    DEFAULT_SERVER_URL = "string"
+    LOG_DIR = "./logs"
+    LOG_LEVEL = "INFO"
 
 # 初始化日志
 # v2 使用简化的日志配置,从 v1 复用或使用 loguru
@@ -67,8 +70,8 @@ try:
         sys.path.insert(0, str(v1_path.parent))
     from pdf_converter.utils.logging_config import init_logging
     init_logging(
-        log_dir=os.getenv("PDF_CONVERTER_LOG_DIR", "./logs"),
-        log_level=os.getenv("LOG_LEVEL", "INFO"),
+        log_dir=LOG_DIR,
+        log_level=LOG_LEVEL,
         log_to_file=True,
         log_to_console=True
     )

+ 8 - 6
pdf_converter_v2/api_server.py

@@ -21,10 +21,12 @@ import uvicorn
 # 支持相对导入和绝对导入
 try:
     from pdf_converter_v2.api.main import app
+    from pdf_converter_v2.config import API_HOST, API_PORT, LOG_LEVEL
 except ImportError:
     # 如果绝对导入失败,尝试相对导入
     sys.path.insert(0, str(current_dir.parent))
     from pdf_converter_v2.api.main import app
+    from pdf_converter_v2.config import API_HOST, API_PORT, LOG_LEVEL
 
 
 def parse_args():
@@ -51,23 +53,23 @@ def parse_args():
     parser.add_argument(
         "--host",
         type=str,
-        default=os.getenv("API_HOST", "0.0.0.0"),
-        help="服务器监听地址 (默认: 0.0.0.0,可通过环境变量 API_HOST 设置)"
+        default=API_HOST,
+        help=f"服务器监听地址 (默认: {API_HOST})"
     )
     
     parser.add_argument(
         "--port",
         type=int,
-        default=int(os.getenv("API_PORT", "4214")),
-        help="服务器监听端口 (默认: 4214,可通过环境变量 API_PORT 设置)"
+        default=API_PORT,
+        help=f"服务器监听端口 (默认: {API_PORT})"
     )
     
     parser.add_argument(
         "--log-level",
         type=str,
-        default=os.getenv("LOG_LEVEL", "info"),
+        default=LOG_LEVEL.lower(),
         choices=["critical", "error", "warning", "info", "debug", "trace"],
-        help="日志级别 (默认: info,可通过环境变量 LOG_LEVEL 设置)"
+        help=f"日志级别 (默认: {LOG_LEVEL.lower()})"
     )
     
     parser.add_argument(

+ 31 - 0
pdf_converter_v2/config.json.example

@@ -0,0 +1,31 @@
+{
+  "_comment": "PDF Converter v2 配置文件 (JSON格式)",
+  "_note": "所有配置项均为可选,未指定时将使用默认值",
+  
+  "device_kind": "",
+  
+  "default_model_name": "OpenDataLab/MinerU2.5-2509-1.2B",
+  "default_gpu_memory_utilization": 0.9,
+  "default_dpi": 200,
+  "default_max_pages": 10,
+  
+  "api_url": "http://127.0.0.1:5282",
+  "backend": "vlm-vllm-async-engine",
+  "parse_method": "auto",
+  "start_page_id": 0,
+  "end_page_id": 99999,
+  "language": "ch",
+  "server_url": "string",
+  
+  "response_format_zip": true,
+  "return_middle_json": false,
+  "return_model_output": true,
+  "return_md": true,
+  "return_images": false,
+  "return_content_list": false,
+  
+  "log_dir": "./logs",
+  "log_level": "INFO",
+  "log_to_file": true,
+  "log_to_console": true
+}

+ 50 - 23
pdf_converter_v2/config.py

@@ -1,34 +1,61 @@
 # Copyright (c) Opendatalab. All rights reserved.
 
 """
-配置文件 v2
+配置文件 v2 - 从配置文件读取配置(不使用环境变量)
 """
 
-import os
-
-# 设备环境:nvi(NVIDIA GPU)/ npu(华为昇腾 NPU)/ cpu,用于按环境选择 VLLM_PLUGINS、PADDLE_OCR_DEVICE 等
+from .config_loader import get_config_loader
 from .utils.device_env import detect_device_kind
 
-DEVICE_KIND = os.getenv("PDF_CONVERTER_DEVICE_KIND") or detect_device_kind()
+# 加载配置文件
+_config = get_config_loader()
+
+# 设备环境:nvi(NVIDIA GPU)/ npu(华为昇腾 NPU)/ cpu,用于按环境选择 VLLM_PLUGINS、PADDLE_OCR_DEVICE 等
+# 优先从配置文件读取,如果未配置则自动检测
+_device_kind_from_config = _config.get_str("device_kind", "")
+DEVICE_KIND = _device_kind_from_config if _device_kind_from_config else detect_device_kind()
 
-# 默认模型配置(与 v1 保持一致)
-DEFAULT_MODEL_NAME = "OpenDataLab/MinerU2.5-2509-1.2B"
-DEFAULT_GPU_MEMORY_UTILIZATION = 0.9
-DEFAULT_DPI = 200
-DEFAULT_MAX_PAGES = 10
+# 默认模型配置
+DEFAULT_MODEL_NAME = _config.get_str("default_model_name", "OpenDataLab/MinerU2.5-2509-1.2B")
+DEFAULT_GPU_MEMORY_UTILIZATION = _config.get_float("default_gpu_memory_utilization", 0.9)
+DEFAULT_DPI = _config.get_int("default_dpi", 200)
+DEFAULT_MAX_PAGES = _config.get_int("default_max_pages", 10)
 
 # v2 特有配置(外部API相关)
-DEFAULT_API_URL = os.getenv("API_URL", "http://127.0.0.1:5282")
-DEFAULT_BACKEND = os.getenv("BACKEND", "vlm-vllm-async-engine")
-DEFAULT_PARSE_METHOD = os.getenv("PARSE_METHOD", "auto")
-DEFAULT_START_PAGE_ID = int(os.getenv("START_PAGE_ID", "0"))
-DEFAULT_END_PAGE_ID = int(os.getenv("END_PAGE_ID", "99999"))
-DEFAULT_LANGUAGE = os.getenv("LANGUAGE", "ch")
-DEFAULT_RESPONSE_FORMAT_ZIP = os.getenv("RESPONSE_FORMAT_ZIP", "true").lower() == "true"
-DEFAULT_RETURN_MIDDLE_JSON = os.getenv("RETURN_MIDDLE_JSON", "false").lower() == "true"
-DEFAULT_RETURN_MODEL_OUTPUT = os.getenv("RETURN_MODEL_OUTPUT", "true").lower() == "true"
-DEFAULT_RETURN_MD = os.getenv("RETURN_MD", "true").lower() == "true"
-DEFAULT_RETURN_IMAGES = os.getenv("RETURN_IMAGES", "false").lower() == "true"
-DEFAULT_RETURN_CONTENT_LIST = os.getenv("RETURN_CONTENT_LIST", "false").lower() == "true"
-DEFAULT_SERVER_URL = os.getenv("SERVER_URL", "string")
+DEFAULT_API_URL = _config.get_str("api_url", "http://127.0.0.1:5282")
+DEFAULT_BACKEND = _config.get_str("backend", "vlm-vllm-async-engine")
+DEFAULT_PARSE_METHOD = _config.get_str("parse_method", "auto")
+DEFAULT_START_PAGE_ID = _config.get_int("start_page_id", 0)
+DEFAULT_END_PAGE_ID = _config.get_int("end_page_id", 99999)
+DEFAULT_LANGUAGE = _config.get_str("language", "ch")
+DEFAULT_RESPONSE_FORMAT_ZIP = _config.get_bool("response_format_zip", True)
+DEFAULT_RETURN_MIDDLE_JSON = _config.get_bool("return_middle_json", False)
+DEFAULT_RETURN_MODEL_OUTPUT = _config.get_bool("return_model_output", True)
+DEFAULT_RETURN_MD = _config.get_bool("return_md", True)
+DEFAULT_RETURN_IMAGES = _config.get_bool("return_images", False)
+DEFAULT_RETURN_CONTENT_LIST = _config.get_bool("return_content_list", False)
+DEFAULT_SERVER_URL = _config.get_str("server_url", "string")
+
+# API 服务启动配置
+API_HOST = _config.get_str("api_host", "0.0.0.0")
+API_PORT = _config.get_int("api_port", 4214)
+
+# MinerU 服务管理配置
+MINERU_API_HOST = _config.get_str("mineru_api_host", "127.0.0.1")
+MINERU_API_PORT = _config.get_int("mineru_api_port", 5282)
+MINERU_IDLE_TIMEOUT = _config.get_int("mineru_idle_timeout", 60)
+MINERU_CHECK_INTERVAL = _config.get_int("mineru_check_interval", 60)
+MINERU_START_TIMEOUT = _config.get_int("mineru_start_timeout", 120)
+
+# PaddleOCR 配置
+PADDLEOCR_CMD = _config.get_str("paddleocr_cmd", "paddleocr")
+PADDLE_OCR_DEVICE = _config.get_str("paddle_ocr_device", "")
+PADDLE_OCR_DEVICES = _config.get_str("paddle_ocr_devices", "")
+PADDLE_DOC_PARSER_CMD = _config.get_str("paddle_doc_parser_cmd", "paddleocr")
+
+# 日志配置(可选)
+LOG_DIR = _config.get_str("log_dir", "./logs")
+LOG_LEVEL = _config.get_str("log_level", "INFO")
+LOG_TO_FILE = _config.get_bool("log_to_file", True)
+LOG_TO_CONSOLE = _config.get_bool("log_to_console", True)
 

+ 127 - 0
pdf_converter_v2/config.yaml

@@ -0,0 +1,127 @@
+# PDF Converter v2 配置文件
+# 所有配置项均为可选,未指定时将使用默认值
+
+# =============================================================================
+# 设备环境配置
+# =============================================================================
+# 设备类型:nvi(NVIDIA GPU)/ npu(华为昇腾 NPU)/ cpu
+# 留空则自动检测
+device_kind: ""
+
+# =============================================================================
+# 默认模型配置
+# =============================================================================
+# 默认模型名称
+default_model_name: "OpenDataLab/MinerU2.5-2509-1.2B"
+
+# GPU 内存利用率(0.0-1.0)
+default_gpu_memory_utilization: 0.9
+
+# DPI 设置
+default_dpi: 200
+
+# 最大页数限制
+default_max_pages: 10
+
+# =============================================================================
+# API 配置
+# =============================================================================
+# MinerU API 服务地址
+api_url: "http://127.0.0.1:5282"
+
+# 处理后端:vlm-vllm-async-engine / pipeline
+backend: "vlm-vllm-async-engine"
+
+# 解析方法:auto / txt / ocr
+parse_method: "auto"
+
+# 起始页ID(从0开始)
+start_page_id: 0
+
+# 结束页ID
+end_page_id: 99999
+
+# 识别语言:ch / en
+language: "ch"
+
+# 服务器URL
+server_url: "string"
+
+# =============================================================================
+# API 服务启动配置
+# =============================================================================
+# API 服务监听地址
+api_host: "0.0.0.0"
+
+# API 服务监听端口
+api_port: 4214
+
+# =============================================================================
+# MinerU 服务管理配置
+# =============================================================================
+# MinerU API 内部地址(用于健康检查)
+mineru_api_host: "127.0.0.1"
+
+# MinerU API 内部端口
+mineru_api_port: 5282
+
+# 空闲超时时间(秒),超过此时间无任务则停止服务
+mineru_idle_timeout: 60
+
+# 检查间隔(秒)
+mineru_check_interval: 60
+
+# 服务启动等待超时(秒)
+mineru_start_timeout: 120
+
+# =============================================================================
+# PaddleOCR 配置
+# =============================================================================
+# PaddleOCR 可执行命令或路径
+paddleocr_cmd: "paddleocr"
+
+# PaddleOCR 推理设备 (例如 "npu:0", "cuda:0", "cpu")
+# 留空则根据环境自动选择
+paddle_ocr_device: ""
+
+# PaddleOCR 多卡推理设备 (例如 "npu:0,npu:1")
+paddle_ocr_devices: ""
+
+# PaddleOCR 文档解析命令
+paddle_doc_parser_cmd: "paddleocr"
+
+# =============================================================================
+# 返回格式配置
+# =============================================================================
+# 是否返回ZIP格式
+response_format_zip: true
+
+# 是否返回中间JSON
+return_middle_json: false
+
+# 是否返回模型输出
+return_model_output: true
+
+# 是否返回Markdown
+return_md: true
+
+# 是否返回图片
+return_images: false
+
+# 是否返回内容列表
+return_content_list: false
+
+# =============================================================================
+# 日志配置(可选)
+# =============================================================================
+# 日志目录
+log_dir: "./logs"
+
+# 日志级别:DEBUG / INFO / WARNING / ERROR
+log_level: "INFO"
+
+# 是否记录到文件
+log_to_file: true
+
+# 是否输出到控制台
+log_to_console: true

+ 167 - 0
pdf_converter_v2/config_loader.py

@@ -0,0 +1,167 @@
+# Copyright (c) Opendatalab. All rights reserved.
+
+"""
+配置文件加载器
+支持从 YAML 或 JSON 配置文件读取配置
+"""
+
+import os
+import json
+from pathlib import Path
+from typing import Any, Dict, Optional
+
+
+class ConfigLoader:
+    """配置文件加载器"""
+    
+    def __init__(self, config_file: Optional[str] = None):
+        """
+        初始化配置加载器
+        
+        Args:
+            config_file: 配置文件路径,支持 .yaml / .yml / .json 格式
+                        如果为 None,将按以下顺序查找:
+                        1. 当前目录下的 config.yaml
+                        2. 当前目录下的 config.yml
+                        3. 当前目录下的 config.json
+        """
+        self.config_file = config_file
+        self.config: Dict[str, Any] = {}
+        self._load_config()
+    
+    def _find_config_file(self) -> Optional[str]:
+        """查找配置文件"""
+        # 获取当前模块所在目录
+        current_dir = Path(__file__).parent
+        
+        # 按优先级查找配置文件
+        candidates = [
+            current_dir / "config.yaml",
+            current_dir / "config.yml",
+            current_dir / "config.json",
+        ]
+        
+        for candidate in candidates:
+            if candidate.exists():
+                return str(candidate)
+        
+        return None
+    
+    def _load_yaml(self, file_path: str) -> Dict[str, Any]:
+        """加载 YAML 配置文件"""
+        try:
+            import yaml
+            with open(file_path, 'r', encoding='utf-8') as f:
+                return yaml.safe_load(f) or {}
+        except ImportError:
+            raise ImportError(
+                "需要安装 PyYAML 才能读取 YAML 配置文件。"
+                "请运行: pip install pyyaml"
+            )
+        except Exception as e:
+            raise RuntimeError(f"加载 YAML 配置文件失败: {e}")
+    
+    def _load_json(self, file_path: str) -> Dict[str, Any]:
+        """加载 JSON 配置文件"""
+        try:
+            with open(file_path, 'r', encoding='utf-8') as f:
+                return json.load(f)
+        except Exception as e:
+            raise RuntimeError(f"加载 JSON 配置文件失败: {e}")
+    
+    def _load_config(self):
+        """加载配置文件"""
+        # 确定配置文件路径
+        config_path = self.config_file
+        if not config_path:
+            config_path = self._find_config_file()
+        
+        if not config_path:
+            # 没有找到配置文件,使用空配置(将使用默认值)
+            return
+        
+        if not os.path.exists(config_path):
+            raise FileNotFoundError(f"配置文件不存在: {config_path}")
+        
+        # 根据文件扩展名加载配置
+        ext = Path(config_path).suffix.lower()
+        if ext in ['.yaml', '.yml']:
+            self.config = self._load_yaml(config_path)
+        elif ext == '.json':
+            self.config = self._load_json(config_path)
+        else:
+            raise ValueError(f"不支持的配置文件格式: {ext},仅支持 .yaml, .yml, .json")
+    
+    def get(self, key: str, default: Any = None) -> Any:
+        """
+        获取配置项
+        
+        Args:
+            key: 配置项的键名
+            default: 默认值(如果配置项不存在)
+        
+        Returns:
+            配置项的值,如果不存在则返回默认值
+        """
+        return self.config.get(key, default)
+    
+    def get_int(self, key: str, default: int = 0) -> int:
+        """获取整数类型的配置项"""
+        value = self.get(key, default)
+        try:
+            return int(value)
+        except (ValueError, TypeError):
+            return default
+    
+    def get_float(self, key: str, default: float = 0.0) -> float:
+        """获取浮点数类型的配置项"""
+        value = self.get(key, default)
+        try:
+            return float(value)
+        except (ValueError, TypeError):
+            return default
+    
+    def get_bool(self, key: str, default: bool = False) -> bool:
+        """获取布尔类型的配置项"""
+        value = self.get(key, default)
+        if isinstance(value, bool):
+            return value
+        if isinstance(value, str):
+            return value.lower() in ['true', 'yes', '1', 'on']
+        return bool(value)
+    
+    def get_str(self, key: str, default: str = "") -> str:
+        """获取字符串类型的配置项"""
+        value = self.get(key, default)
+        return str(value) if value is not None else default
+
+
+# 创建全局配置加载器实例
+_config_loader: Optional[ConfigLoader] = None
+
+
+def get_config_loader(config_file: Optional[str] = None) -> ConfigLoader:
+    """
+    获取全局配置加载器实例
+    
+    Args:
+        config_file: 配置文件路径(可选)
+    
+    Returns:
+        ConfigLoader 实例
+    """
+    global _config_loader
+    if _config_loader is None:
+        _config_loader = ConfigLoader(config_file)
+    return _config_loader
+
+
+def reload_config(config_file: Optional[str] = None):
+    """
+    重新加载配置文件
+    
+    Args:
+        config_file: 配置文件路径(可选)
+    """
+    global _config_loader
+    _config_loader = ConfigLoader(config_file)

+ 2 - 1
pdf_converter_v2/processor/converter.py

@@ -26,9 +26,10 @@ from ..utils.paddleocr_fallback import (
     _paddle_ocr_device_args,
     _get_paddleocr_subprocess_env,
 )
+from ..config import PADDLE_DOC_PARSER_CMD
 
 logger = get_logger("pdf_converter_v2.processor")
-PADDLE_CMD = os.getenv("PADDLE_DOC_PARSER_CMD", "paddleocr")
+PADDLE_CMD = PADDLE_DOC_PARSER_CMD
 
 
 async def _run_paddle_doc_parser(cmd: Sequence[str]) -> tuple[int, str, str]:

+ 3 - 0
pdf_converter_v2/requirements.txt

@@ -30,5 +30,8 @@ openpyxl>=3.0.0         # Excel 文件读写
 # ========== 日志 ==========
 loguru>=0.7.0
 
+# ========== 配置文件 ==========
+pyyaml>=6.0.0          # YAML 格式配置文件支持(可选,JSON 格式无需此依赖)
+
 # ========== HTTP 客户端(测试用) ==========
 requests>=2.28.0        # test_api.py 调用接口时需要

+ 103 - 0
pdf_converter_v2/test_config.py

@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+# Copyright (c) Opendatalab. All rights reserved.
+
+"""
+配置文件测试脚本
+验证配置文件是否正确加载
+"""
+
+import sys
+from pathlib import Path
+
+# 添加父目录到路径
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+from pdf_converter_v2.config import (
+    DEVICE_KIND,
+    DEFAULT_MODEL_NAME,
+    DEFAULT_GPU_MEMORY_UTILIZATION,
+    DEFAULT_DPI,
+    DEFAULT_MAX_PAGES,
+    DEFAULT_API_URL,
+    DEFAULT_BACKEND,
+    DEFAULT_PARSE_METHOD,
+    DEFAULT_START_PAGE_ID,
+    DEFAULT_END_PAGE_ID,
+    DEFAULT_LANGUAGE,
+    DEFAULT_RESPONSE_FORMAT_ZIP,
+    DEFAULT_RETURN_MIDDLE_JSON,
+    DEFAULT_RETURN_MODEL_OUTPUT,
+    DEFAULT_RETURN_MD,
+    DEFAULT_RETURN_IMAGES,
+    DEFAULT_RETURN_CONTENT_LIST,
+    DEFAULT_SERVER_URL,
+    LOG_DIR,
+    LOG_LEVEL,
+    LOG_TO_FILE,
+    LOG_TO_CONSOLE,
+)
+
+
+def print_config():
+    """打印所有配置项"""
+    print("=" * 80)
+    print("PDF Converter v2 - 配置信息")
+    print("=" * 80)
+    print()
+    
+    print("【设备环境配置】")
+    print(f"  设备类型: {DEVICE_KIND}")
+    print()
+    
+    print("【模型配置】")
+    print(f"  模型名称: {DEFAULT_MODEL_NAME}")
+    print(f"  GPU 内存利用率: {DEFAULT_GPU_MEMORY_UTILIZATION}")
+    print(f"  DPI: {DEFAULT_DPI}")
+    print(f"  最大页数: {DEFAULT_MAX_PAGES}")
+    print()
+    
+    print("【API 配置】")
+    print(f"  API URL: {DEFAULT_API_URL}")
+    print(f"  后端: {DEFAULT_BACKEND}")
+    print(f"  解析方法: {DEFAULT_PARSE_METHOD}")
+    print(f"  起始页ID: {DEFAULT_START_PAGE_ID}")
+    print(f"  结束页ID: {DEFAULT_END_PAGE_ID}")
+    print(f"  语言: {DEFAULT_LANGUAGE}")
+    print(f"  服务器URL: {DEFAULT_SERVER_URL}")
+    print()
+    
+    print("【返回格式配置】")
+    print(f"  返回ZIP格式: {DEFAULT_RESPONSE_FORMAT_ZIP}")
+    print(f"  返回中间JSON: {DEFAULT_RETURN_MIDDLE_JSON}")
+    print(f"  返回模型输出: {DEFAULT_RETURN_MODEL_OUTPUT}")
+    print(f"  返回Markdown: {DEFAULT_RETURN_MD}")
+    print(f"  返回图片: {DEFAULT_RETURN_IMAGES}")
+    print(f"  返回内容列表: {DEFAULT_RETURN_CONTENT_LIST}")
+    print()
+    
+    print("【日志配置】")
+    print(f"  日志目录: {LOG_DIR}")
+    print(f"  日志级别: {LOG_LEVEL}")
+    print(f"  记录到文件: {LOG_TO_FILE}")
+    print(f"  输出到控制台: {LOG_TO_CONSOLE}")
+    print()
+    
+    print("=" * 80)
+    print("✅ 配置加载成功!")
+    print("=" * 80)
+
+
+def main():
+    """主函数"""
+    try:
+        print_config()
+        return 0
+    except Exception as e:
+        print(f"❌ 配置加载失败: {e}", file=sys.stderr)
+        import traceback
+        traceback.print_exc()
+        return 1
+
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 12 - 5
pdf_converter_v2/utils/mineru_service_manager.py

@@ -19,6 +19,13 @@ from typing import Optional
 from datetime import datetime
 
 from .logging_config import get_logger
+from ..config import (
+    MINERU_API_HOST as _MINERU_API_HOST,
+    MINERU_API_PORT as _MINERU_API_PORT,
+    MINERU_IDLE_TIMEOUT as _MINERU_IDLE_TIMEOUT,
+    MINERU_CHECK_INTERVAL as _MINERU_CHECK_INTERVAL,
+    MINERU_START_TIMEOUT as _MINERU_START_TIMEOUT,
+)
 
 logger = get_logger("pdf_converter_v2.mineru_manager")
 
@@ -26,17 +33,17 @@ logger = get_logger("pdf_converter_v2.mineru_manager")
 MINERU_SERVICE_NAME = "mineru-api.service"
 
 # MinerU API 地址和端口(用于健康检查)
-MINERU_API_HOST = os.getenv("MINERU_API_HOST", "127.0.0.1")
-MINERU_API_PORT = int(os.getenv("MINERU_API_PORT", "5282"))
+MINERU_API_HOST = _MINERU_API_HOST
+MINERU_API_PORT = _MINERU_API_PORT
 
 # 空闲超时时间(秒),超过此时间无任务则停止服务
-IDLE_TIMEOUT_SECONDS = int(os.getenv("MINERU_IDLE_TIMEOUT", "60"))  # 默认 1 分钟
+IDLE_TIMEOUT_SECONDS = _MINERU_IDLE_TIMEOUT
 
 # 检查间隔(秒)
-CHECK_INTERVAL_SECONDS = int(os.getenv("MINERU_CHECK_INTERVAL", "60"))  # 默认 1 分钟
+CHECK_INTERVAL_SECONDS = _MINERU_CHECK_INTERVAL
 
 # 服务启动等待超时(秒)
-SERVICE_START_TIMEOUT = int(os.getenv("MINERU_START_TIMEOUT", "120"))  # 默认 2 分钟
+SERVICE_START_TIMEOUT = _MINERU_START_TIMEOUT
 
 
 class MinerUServiceManager:

+ 5 - 4
pdf_converter_v2/utils/mineru_url_selector.py

@@ -5,19 +5,20 @@ MinerU API 多实例 URL 轮询
 当 API_URL 配置为逗号分隔的多个地址时(多卡多实例),按请求轮询使用。
 """
 
-import os
 import threading
 from typing import List
 
-_DEFAULT_SINGLE = "http://127.0.0.1:5282"
+from ..config import DEFAULT_API_URL
+
+_DEFAULT_SINGLE = DEFAULT_API_URL
 _URL_LIST: List[str] = []
 _URL_INDEX: int = 0
 _URL_LOCK = threading.Lock()
 
 
 def _parse_api_url_list() -> List[str]:
-    """从环境变量 API_URL 解析 URL 列表(逗号分隔,去空格)。"""
-    raw = os.getenv("API_URL", _DEFAULT_SINGLE).strip()
+    """从配置解析 URL 列表(逗号分隔,去空格)。"""
+    raw = DEFAULT_API_URL.strip()
     if not raw:
         return [_DEFAULT_SINGLE]
     return [u.strip() for u in raw.split(",") if u.strip()] or [_DEFAULT_SINGLE]

+ 9 - 4
pdf_converter_v2/utils/paddleocr_fallback.py

@@ -15,6 +15,11 @@ import ast
 import re
 
 from ..utils.logging_config import get_logger
+from ..config import (
+    PADDLEOCR_CMD as _PADDLEOCR_CMD,
+    PADDLE_OCR_DEVICE as _PADDLE_OCR_DEVICE,
+    PADDLE_OCR_DEVICES as _PADDLE_OCR_DEVICES_CONFIG,
+)
 
 logger = get_logger("pdf_converter_v2.utils.paddleocr")
 
@@ -43,8 +48,8 @@ except ImportError:
 def _get_paddleocr_executable() -> str:
     """返回 paddleocr 可执行文件路径或命令名,供 subprocess 使用。
     当以 systemd 等方式运行时 PATH 可能不包含 venv/bin,故优先使用当前 Python 同目录下的 paddleocr。
-    可通过环境变量 PADDLEOCR_CMD 显式指定(完整路径或命令名)。"""
-    cmd = os.getenv("PADDLEOCR_CMD", "").strip()
+    可通过配置 PADDLEOCR_CMD 显式指定(完整路径或命令名)。"""
+    cmd = _PADDLEOCR_CMD.strip()
     if cmd:
         return cmd
     # 与当前 Python 同目录(venv/bin)下的 paddleocr
@@ -70,11 +75,11 @@ def _get_paddle_ocr_devices() -> List[str]:
     with _PADDLE_OCR_DEVICE_LOCK:
         if _PADDLE_OCR_DEVICES:
             return _PADDLE_OCR_DEVICES
-        multi = os.getenv("PADDLE_OCR_DEVICES", "").strip()
+        multi = _PADDLE_OCR_DEVICES_CONFIG.strip()
         if multi:
             _PADDLE_OCR_DEVICES[:] = [d.strip() for d in multi.split(",") if d.strip()]
         if not _PADDLE_OCR_DEVICES:
-            single = os.getenv("PADDLE_OCR_DEVICE", "").strip()
+            single = _PADDLE_OCR_DEVICE.strip()
             if not single:
                 from .device_env import is_npu
                 if is_npu():