pdf-craft-service 0.1.0
A RESTful API service for PDF operations using FastAPI
Requires Python: <3.13,>=3.11

# PDF Craft Service

基于 FastAPI 的 PDF 处理服务，提供 OCR（光学字符识别）和格式转换（EPUB、Markdown）功能。

## 项目概述

本项目包含两个微服务：

### 1. pdf-ocr
**需要 CUDA 支持的 OCR 服务**

- 使用 AI 模型对 PDF 文件进行 OCR 识别
- **必须运行在 CUDA GPU 环境**（专为阿里云函数计算 FC 设计）
- **无法在 macOS 上运行** - 需要 Linux + NVIDIA GPU
- 开发建议：直接部署到阿里云 FC 测试效果
- 如果一定要本地测试，需要准备一台有 CUDA 的设备

### 2. pdf-transform
**纯 CPU 的 PDF 转换服务**

- 将 PDF 转换为 EPUB 或 Markdown 格式
- **可以在 macOS 上运行** - 不需要 GPU
- 内部调用 OCR 服务进行 PDF 处理
- 适合本地开发和测试

### 3. 根目录开发环境
**VSCode Python 支持**

- 根目录的 `pyproject.toml` 仅用于 IDE 集成
- 运行 `poetry install` 让 VSCode 的 Python 能读到依赖，不报错
- 自动使用纯 CPU 版本依赖（不包含 CUDA）
- 同时也支持本地运行 pdf-transform

## 快速开始

### 环境要求

- Python 3.11+
- Poetry（依赖管理工具）
- pdf-ocr 需要：Linux + NVIDIA GPU + CUDA 支持
- pdf-transform 需要：任意系统（macOS、Linux、Windows）

### 安装

#### 开发环境（macOS/Linux/Windows）

在根目录安装，用于 VSCode 支持：

```bash
poetry install
```

这会安装：
- CPU 版本的 PyTorch（来自 PyPI）
- FastAPI 及相关依赖
- 开发工具（pytest、httpx、pylint）
- 让 VSCode 的 IntelliSense 正常工作

#### 使用 Conda（可选）

如果你更喜欢用 Conda 管理环境：

```bash
# 创建 Conda 环境
conda create -p ./.conda python=3.11.14
conda activate ./.conda

# 安装依赖
poetry install
```

验证安装：

```bash
python -c "import torch; import pdf_craft; print(f'torch: {torch.__version__}'); print('pdf_craft: OK')"
```

#### 生产环境（CUDA 环境）

在 GPU 机器上安装 pdf-ocr 服务：

```bash
cd pdf-ocr
poetry install
```

这会从 pytorch-cu121 源安装支持 CUDA 的 PyTorch。

## 环境变量

### 通用环境变量（pdf-ocr 和 pdf-transform 共享）

| 环境变量 | 是否必需 | 默认值 | 说明 |
|---------|---------|--------|------|
| `PORT` | 否 | `80` | 服务监听端口，必须是有效的整数 |
| `VERSION` | 否 | `"dev"` | 服务版本号，显示在 API 文档中 |
| `SECRET_KEY` | 否 | 无 | 鉴权密钥，设置后会启用 Authorization header 验证 |
| `LOG_LEVEL` | 否 | `"INFO"` | 日志级别，可选值：`"DEBUG"`、`"INFO"`、`"WARNING"`、`"ERROR"`、`"CRITICAL"`。设置为 `"DEBUG"` 可查看详细的调试日志 |
| `LOG_STORAGE_PATH` | 否 | 无 | 日志文件存储目录，若不设置则只输出到控制台 |
| `WORKERS_COUNT` | 否 | `1` | Gunicorn 工作进程数量。对于 FastAPI 异步应用，建议值为 **CPU 核心数**（如 1 vCPU → 1 worker，2 vCPU → 2 worker，4 vCPU → 4 worker）。每个 worker 运行独立的事件循环处理并发请求。pdf-ocr 建议保持较小值避免 GPU 资源竞争。 |
| `WORKER_TIMEOUT` | 否 | `86400` | Gunicorn worker 超时时间（秒），默认 86400 秒（24 小时）。如果 worker 处理请求超过此时间会被强制重启。处理大文件时可能需要增加此值。 |

### pdf-transform 服务独有环境变量

#### OCR 处理方式配置（二选一）

**方式 1: 使用自建 OCR 服务**（推荐用于生产环境）

| 环境变量 | 是否必需 | 默认值 | 说明 |
|---------|---------|--------|------|
| `OCR_HOST` | **条件必需** | 无 | pdf-ocr 服务的 URL，必须是有效的 HTTP/HTTPS URL（如：`https://example.com`）。与 `VENDOR_API_KEY` 二选一 |

**方式 2: 使用第三方 Vendor API**（例如 SiliconFlow）

| 环境变量 | 是否必需 | 默认值 | 说明 |
|---------|---------|--------|------|
| `VENDOR_API_KEY` | **条件必需** | 无 | 第三方 Vendor API 密钥。与 `OCR_HOST` 二选一，二者至少设置一个 |
| `VENDOR_URL` | 否 | 无 | Vendor API 的服务 URL（可选字符串，如 `https://api.siliconflow.cn/v1`）。若不设置则使用默认 Vendor 服务地址 |
| `VENDOR_MODEL_LV0` | 否 | `"deepseek-ocr"` | Vendor API 的基础模型名称（Level 0），用于默认请求 |
| `VENDOR_MODEL_LV1` | 否 | `"deepseek-ocr"` | Vendor API 的标准模型名称（Level 1），当使用 OOMOL API 时生效 |
| `VENDOR_MODEL_LV2` | 否 | `"deepseek-ocr"` | Vendor API 的高并发模型名称（Level 2），当并发量达到 `VENDOR_L2_CONCURRENCY` 阈值时自动切换 |
| `VENDOR_L2_CONCURRENCY` | 否 | 无 | Level 2 模型切换的并发阈值（可选整数）。当全局并发请求数超过此值时，自动从 LV1 切换到 LV2 模型 |
| `VENDOR_TEMPERATURE` | 否 | `None` | Vendor API 的 temperature 参数，控制生成的随机性（可选浮点数，如 `0.95`） |
| `VENDOR_TOP_P` | 否 | `None` | Vendor API 的 top_p 参数，控制采样概率（可选浮点数，如 `0.95`） |

**注意：** `OCR_HOST` 和 `VENDOR_API_KEY` 必须至少设置一个：
- 如果设置了 `OCR_HOST`，将使用自建 OCR 服务
- 如果只设置了 `VENDOR_API_KEY`，将使用第三方 Vendor API（当前支持 SiliconFlow）
- 如果两者都未设置，服务将无法启动

#### Redis 配置

| 环境变量 | 是否必需 | 默认值 | 说明 |
|---------|---------|--------|------|
| `REDIS_DOMAIN` | **是** | 无 | Redis 服务器域名或 IP 地址 |
| `REDIS_USERNAME` | **是** | 无 | Redis 用户名 |
| `REDIS_PASSWORD` | **是** | 无 | Redis 密码 |
| `REDIS_PORT` | **是** | 无 | Redis 端口，必须是有效的整数 |
| `REDIS_DB` | **是** | 无 | Redis 数据库编号，必须是有效的整数 |
| `REDIS_EXPIRE_SECONDS` | **是** | 无 | Redis 键过期时间（秒），必须是有效的整数 |

## 开发指南

详细的开发指南请参阅 [DEVELOPMENT.md](docs/DEVELOPMENT.md)。

### 配置环境变量

1. **复制环境变量模板：**
   ```bash
   cp .env.template .env
   ```

编辑 `.env` 文件，填入实际配置。

### 本地运行 pdf-transform

```bash
python app.py
```

若 terminal 无法正常读取 `.env` 文件，可执行如下命令强制指定

```bash
set -a && source .env && set +a && python app.py
```

服务启动后可访问：
- API：http://localhost:3000
- API 文档：http://localhost:3000/docs

### 运行 pdf-ocr（需要 CUDA）

**注意：** 此服务需要 CUDA GPU，通常在阿里云 FC 中运行。

```bash
# 从根目录启动（会自动加载 .env 文件）
set -a && source .env && set +a && python app.py
```

本地测试需要：
- Linux 操作系统
- NVIDIA GPU + CUDA 12.1+
- 正确安装的 GPU 驱动

### 运行测试

从根目录执行：

```bash
poetry run pytest
```

## API 接口文档

### pdf-ocr 服务

#### `GET /`
健康检查和服务信息。

**响应：**
```json
{
  "service": "PDF Craft OCR Service",
  "version": "0.1.0",
  "status": "running"
}
```

**示例：**
```bash
curl https://your-ocr-service.com/
```

#### `GET /health`
简单的健康检查端点。

**响应：**
```json
{
  "status": "healthy"
}
```

**示例：**
```bash
curl https://your-ocr-service.com/health
```

#### `POST /initialize`
初始化 OCR 模型。该端点用于预加载 OCR 模型到内存中，加快后续 OCR 请求的处理速度。通常在服务启动后调用一次。

**响应：**
```json
{
  "status": "initialized",
  "cost_ms": 1234.56
}
```

**示例：**
```bash
curl -X POST https://your-ocr-service.com/initialize
```

#### `GET /pre-stop`
预停止端点。用于优雅关闭服务，释放 GPU 设备资源。通常在容器或服务即将停止前调用。

**响应：**
```json
{
  "status": "disposed"
}
```

**示例：**
```bash
curl https://your-ocr-service.com/pre-stop
```

#### `POST /ocr`
对 PDF 文件执行 OCR，返回流式响应。

**请求：**
- Content-Type: `multipart/form-data`
- **Headers：**
  - `TaskID`（必需）：任务唯一标识符，用于追踪任务进度和日志
- **参数：**
  - `file`（必需）：要处理的 PDF 文件
  - `model`（可选）：OCR 模型，可选值：`"tiny"`、`"small"`、`"base"`、`"large"`、`"gundam"`，默认 `"gundam"`
  - `max_tokens`（可选）：最大输入 tokens，默认 `0`（无限制）
  - `max_output_tokens`（可选）：最大输出 tokens，默认 `0`（无限制）
  - `includes_cover`（可选）：是否处理封面，默认 `true`
  - `includes_footnotes`（可选）：是否处理脚注，默认 `true`
  - `ignore_pdf_errors`（可选）：是否忽略 PDF 解析错误，默认 `false`。设为 `true` 时即使 PDF 文件存在错误也会尝试继续处理
  - `ignore_ocr_errors`（可选）：是否忽略 OCR 识别错误，默认 `false`。设为 `true` 时即使某些页面 OCR 失败也会继续处理其他页面
  - `max_waiting_queue_size`（可选）：最大等待队列大小，默认 `-1`（无限制）。当队列已满时会返回 429 错误
  - `max_waiting_time_seconds`（可选）：最大等待时间（秒），默认 `-1`（无限制）。超过等待时间会返回 408 错误

**响应：**
- Content-Type: `text/event-stream`（服务器发送事件）
- 流式 JSON 事件（每行一个，带 "data: " 前缀）

**事件类型：**

1. **封面事件**（如果 includes_cover=true）
```json
{
  "kind": "cover",
  "content": "<base64编码的图片>"
}
```

2. **页面处理中事件**
```json
{
  "kind": "processing",
  "page_index": 0
}
```

3. **页面已处理事件**
```json
{
  "kind": "processed",
  "page_index": 0,
  "cost": 1234,
  "input_tokens": 5000,
  "output_tokens": 1000
}
```

4. **页面事件**（每个文件会拆成多个片段）
```json
{
  "kind": "page",
  "page_index": 0,
  "content": "<base64编码的XML片段>"
}
```

5. **资源事件**（图片/资源，会被拆分成多个片段）
```json
{
  "kind": "asset",
  "hash": "abc123",
  "content": "<base64编码的图片片段>"
}
```

6. **完成事件**
```json
{"kind": "complete"}
```

7. **错误事件**
```json
{
  "kind": "error",
  "message": "错误描述"
}
```

**错误码：**
- `400`：无效文件（不是 PDF）
- `408`：等待超时（超过 `max_waiting_time_seconds` 限制）
- `429`：队列已满（超过 `max_waiting_queue_size` 限制）
- `500`：服务器错误

**示例：**
```bash
# 基本调用
curl -X POST https://your-ocr-service.com/ocr \
  -H "TaskID: unique-task-id-12345" \
  -F "file=@document.pdf"

# 使用所有参数
curl -X POST https://your-ocr-service.com/ocr \
  -H "TaskID: unique-task-id-12345" \
  -F "file=@document.pdf" \
  -F "model=gundam" \
  -F "max_tokens=10000" \
  -F "max_output_tokens=5000" \
  -F "includes_cover=true" \
  -F "includes_footnotes=false" \
  -F "ignore_pdf_errors=false" \
  -F "ignore_ocr_errors=false" \
  -F "max_waiting_queue_size=100" \
  -F "max_waiting_time_seconds=300"
```

---

### pdf-transform 服务

#### `GET /`
健康检查和服务信息。

**响应：**
```json
{
  "service": "PDF Craft Transform Service",
  "version": "0.1.0",
  "status": "running"
}
```

**示例：**
```bash
curl https://your-transform-service.com/
```

#### `GET /health`
简单的健康检查端点。

**响应：**
```json
{
  "status": "healthy"
}
```

**示例：**
```bash
curl https://your-transform-service.com/health
```

#### `GET /pre-stop`
预停止端点。用于优雅关闭服务，释放并发控制资源。通常在容器或服务即将停止前调用。

**响应：**
```json
{
  "status": "disposed"
}
```

**示例：**
```bash
curl https://your-transform-service.com/pre-stop
```

#### `POST /transform/epub`
将 PDF 转换为 EPUB（电子书）格式。

**请求：**
- Content-Type: `multipart/form-data`
- **Headers：**
  - `TaskID`（必需）：任务唯一标识符，用于追踪任务进度和日志
  - `Authorization`（条件必需）：Bearer token 格式的鉴权令牌。仅当服务端设置了 `SECRET_KEY` 环境变量时需要提供，格式为 `Bearer <your-secret-key>`
- **参数：**
  - `origin_url`（必需）：要转换的 PDF 文件的下载 URL
  - `target_url`（必需）：转换完成后的 EPUB 文件上传 URL
  - `model`（可选）：OCR 模型，可选值：`"tiny"`、`"small"`、`"base"`、`"large"`、`"gundam"`，默认 `"gundam"`
  - `includes_footnotes`（可选）：是否处理脚注，默认 `true`
  - `ignore_pdf_errors`（可选）：是否忽略 PDF 解析错误，默认 `false`。设为 `true` 时即使 PDF 文件存在错误也会尝试继续处理
  - `ignore_ocr_errors`（可选）：是否忽略 OCR 识别错误，默认 `false`。设为 `true` 时即使某些页面 OCR 失败也会继续处理其他页面
  - `inline_latex`（可选）：是否使用内联 LaTeX 格式输出数学公式，默认 `false`。设为 `true` 时数学公式将以 LaTeX 格式内联在文本中
  - `ocr_workers`（可选）：OCR 并发处理的 worker 数量，默认 `1`。增加此值可以提高处理速度，但会增加对 OCR 服务的并发请求压力
  - `oomol_api_key`（可选）：OOMOL API 密钥。配置此字段后，服务将直接连接 OOMOL 平台（https://llm.oomol.com）进行 OCR 处理并走计费系统。如果不配置此字段，服务将优先使用环境变量 `OCR_HOST`（自建 OCR 服务）或 `VENDOR_API_KEY`（硅基流动等第三方服务），且不会产生 OOMOL 平台费用

**响应：**
- Content-Type: `application/json`
- Body：
```json
{
  "Content-Type": "application/epub+zip",
  "URL": "<上传的目标URL>"
}
```

**错误码：**
- `400`：无效 URL 或参数
- `401`：鉴权失败（需要设置正确的 Authorization header）
- `500`：服务器错误

**示例：**
```bash
# 基本调用（需要 TaskID 和 Authorization）
curl -X POST https://your-transform-service.com/transform/epub \
  -H "TaskID: unique-task-id-12345" \
  -H "Authorization: Bearer your-secret-key" \
  -F "origin_url=https://example.com/document.pdf" \
  -F "target_url=https://example.com/upload/output.epub"

# 带所有参数
curl -X POST https://your-transform-service.com/transform/epub \
  -H "TaskID: unique-task-id-12345" \
  -H "Authorization: Bearer your-secret-key" \
  -F "origin_url=https://example.com/document.pdf" \
  -F "target_url=https://example.com/upload/output.epub" \
  -F "model=gundam" \
  -F "includes_footnotes=false" \
  -F "ignore_pdf_errors=false" \
  -F "ignore_ocr_errors=false" \
  -F "inline_latex=true" \
  -F "ocr_workers=2"

# 使用 OOMOL API 进行处理（走计费系统）
curl -X POST https://your-transform-service.com/transform/epub \
  -H "TaskID: unique-task-id-12345" \
  -H "Authorization: Bearer your-secret-key" \
  -F "origin_url=https://example.com/document.pdf" \
  -F "target_url=https://example.com/upload/output.epub" \
  -F "oomol_api_key=your-oomol-api-key"
```

#### `POST /transform/markdown`
将 PDF 转换为 Markdown 格式（带图片），输出为 ZIP 压缩包。

**请求：**
- Content-Type: `multipart/form-data`
- **Headers：**
  - `TaskID`（必需）：任务唯一标识符，用于追踪任务进度和日志
  - `Authorization`（条件必需）：Bearer token 格式的鉴权令牌。仅当服务端设置了 `SECRET_KEY` 环境变量时需要提供，格式为 `Bearer <your-secret-key>`
- **参数：**
  - `origin_url`（必需）：要转换的 PDF 文件的下载 URL
  - `target_url`（必需）：转换完成后的 ZIP 文件上传 URL
  - `origin_name`（可选）：原始 PDF 文件名（不含扩展名），用于生成 Markdown 文件名。若不传则自动从 `origin_url` 提取文件名。
  - `model`（可选）：OCR 模型，可选值：`"tiny"`、`"small"`、`"base"`、`"large"`、`"gundam"`，默认 `"gundam"`
  - `includes_footnotes`（可选）：是否处理脚注，默认 `true`
  - `ignore_pdf_errors`（可选）：是否忽略 PDF 解析错误，默认 `false`。设为 `true` 时即使 PDF 文件存在错误也会尝试继续处理
  - `ignore_ocr_errors`（可选）：是否忽略 OCR 识别错误，默认 `false`。设为 `true` 时即使某些页面 OCR 失败也会继续处理其他页面
  - `inline_latex`（可选）：是否使用内联 LaTeX 格式输出数学公式，默认 `true`。设为 `true` 时数学公式将以 LaTeX 格式内联在文本中。
  - `ocr_workers`（可选）：OCR 并发处理的 worker 数量，默认 `1`。增加此值可以提高处理速度，但会增加对 OCR 服务的并发请求压力
  - `oomol_api_key`（可选）：OOMOL API 密钥。配置此字段后，服务将直接连接 OOMOL 平台（https://llm.oomol.com）进行 OCR 处理并走计费系统。如果不配置此字段，服务将优先使用环境变量 `OCR_HOST`（自建 OCR 服务）或 `VENDOR_API_KEY`（硅基流动等第三方服务），且不会产生 OOMOL 平台费用

**响应：**
- Content-Type: `application/json`
- Body：
```json
{
  "Content-Type": "application/zip",
  "URL": "<上传的目标URL>"
}
```

**ZIP 压缩包内容：**
- `{原文件名}.md`（根目录）
- `images/` 文件夹，包含提取的图片（如果有）

**错误码：**
- `400`：无效 URL 或参数
- `401`：鉴权失败（需要设置正确的 Authorization header）
- `500`：服务器错误

**示例：**
```bash
# 基本调用（需要 TaskID 和 Authorization）
curl -X POST https://your-transform-service.com/transform/markdown \
  -H "TaskID: unique-task-id-12345" \
  -H "Authorization: Bearer your-secret-key" \
  -F "origin_url=https://example.com/document.pdf" \
  -F "target_url=https://example.com/upload/output.zip"

# 带所有参数
curl -X POST https://your-transform-service.com/transform/markdown \
  -H "TaskID: unique-task-id-12345" \
  -H "Authorization: Bearer your-secret-key" \
  -F "origin_url=https://example.com/document.pdf" \
  -F "target_url=https://example.com/upload/output.zip" \
  -F "model=gundam" \
  -F "includes_footnotes=false" \
  -F "ignore_pdf_errors=false" \
  -F "ignore_ocr_errors=false" \
  -F "inline_latex=true" \
  -F "ocr_workers=2"

# 使用 OOMOL API 进行处理（走计费系统）
curl -X POST https://your-transform-service.com/transform/markdown \
  -H "TaskID: unique-task-id-12345" \
  -H "Authorization: Bearer your-secret-key" \
  -F "origin_url=https://example.com/document.pdf" \
  -F "target_url=https://example.com/upload/output.zip" \
  -F "oomol_api_key=your-oomol-api-key"
```

## Task 进度汇报和用量统计

pdf-transform 服务会将任务的实时进度和用量统计信息上报到 Redis，客户端可以通过查询 Redis 获取任务的实时状态。详细实现参见 [task_status.py](pdf-transform/src/task_status.py)。

### Redis 数据结构

#### 1. 任务总体状态（Hash）

**Key:** `pdf-craft:{task_id}`
**类型:** Hash

| 字段名 | 数据类型 | 说明 | 更新时机 |
|--------|---------|------|---------|
| `status` | String | 任务状态，可选值：`pending`（阻塞）、`reading`（读取中）、`ocr`（OCR处理中）、`generating`（生成中）、`completed`（已完成）、`failed`（失败）、`aborted`（已中止） | 调用 `report_status()` 时更新；任务完成或异常时自动更新 |
| `total_pages` | Integer | PDF 总页数 | 调用 `report_pages()` 时初始化 |
| `completed_pages` | Integer | 已完成页数 | 调用 `report_page_completed()` 时原子性累加（使用 Redis HINCRBY） |
| `input_tokens` | Integer | 累计输入 token 数 | 调用 `report_page_completed()` 时原子性累加（使用 Redis HINCRBY） |
| `output_tokens` | Integer | 累计输出 token 数 | 调用 `report_page_completed()` 时原子性累加（使用 Redis HINCRBY） |

**过期时间:** `REDIS_EXPIRE_SECONDS`（每次更新时重置）

**示例查询:**
```bash
# 获取任务状态
redis-cli HGET "pdf-craft:{task_id}" status

# 获取所有字段
redis-cli HGETALL "pdf-craft:{task_id}"
```

#### 2. 页面详细状态（List）

**Key:** `pdf-craft:{task_id}:pages`
**类型:** List（每个元素是 JSON 字符串）

**元素结构（JSON）:**
```json
{
  "status": "pending | processing | completed",
  "cost_time_ms": 1234,
  "input_tokens": 5000,
  "output_tokens": 1000
}
```

| 字段名 | 数据类型 | 说明 | 更新时机 |
|--------|---------|------|---------|
| `status` | String | 页面状态：`pending`（待处理）、`processing`（处理中）、`completed`（已完成） | 调用 `report_page_status()` 时更新 |
| `cost_time_ms` | Integer | 该页处理耗时（毫秒） | 页面处理完成时记录 |
| `input_tokens` | Integer | 该页输入 token 数 | 页面处理完成时记录 |
| `output_tokens` | Integer | 该页输出 token 数 | 页面处理完成时记录 |

**过期时间:** `REDIS_EXPIRE_SECONDS`（每次更新时重置）

**初始化时机:** 调用 `report_pages(total_pages)` 时创建，所有页面初始状态为 `pending`

**示例查询:**
```bash
# 获取所有页面状态
redis-cli LRANGE "pdf-craft:{task_id}:pages" 0 -1

# 获取第一页状态（index 0）
redis-cli LINDEX "pdf-craft:{task_id}:pages" 0

# 获取总页数
redis-cli LLEN "pdf-craft:{task_id}:pages"
```

### 使用示例

**Python 查询示例:**
```python
import redis
import json

r = redis.Redis(host='your-redis-host', decode_responses=True)
task_id = 'your-task-id'

# 获取任务总体状态
task_info = r.hgetall(f"pdf-craft:{task_id}")
print(f"Status: {task_info.get('status')}")
print(f"Total Pages: {task_info.get('total_pages', 0)}")
print(f"Completed Pages: {task_info.get('completed_pages', 0)}")
print(f"Total Input Tokens: {task_info.get('input_tokens', 0)}")
print(f"Total Output Tokens: {task_info.get('output_tokens', 0)}")

# 获取页面详细状态
pages = r.lrange(f"pdf-craft:{task_id}:pages", 0, -1)
for i, page_json in enumerate(pages):
    page = json.loads(page_json)
    print(f"Page {i+1}: {page['status']}, Cost: {page['cost_time_ms']}ms")
```

**监控进度示例:**
```python
import time

def monitor_task(task_id):
    while True:
        task_info = r.hgetall(f"pdf-craft:{task_id}")
        status = task_info.get('status')
        if status in ['completed', 'failed', 'aborted']:
            print(f"Task {status}")
            break

        # 从 Hash 字段直接获取进度（更高效）
        completed = int(task_info.get('completed_pages', 0))
        total = int(task_info.get('total_pages', 0))
        print(f"Progress: {completed}/{total} pages, Status: {status}")

        time.sleep(1)
```

## 部署

容器化部署文档请参阅 [DEPLOYMENT.md](docs/DEPLOYMENT.md)。

### 阿里云函数计算（pdf-ocr）

1. 构建包含 CUDA 依赖的部署包
2. 上传到阿里云 FC
3. 配置 GPU 实例类型（vCPU 范围：0.05~16 核，详见 [FC 实例规格](https://help.aliyun.com/zh/functioncompute/fc-2-0/user-guide/instance-types-and-instance-modes)）
4. 设置环境变量：
   - `PORT`：服务端口（默认 80）
   - `VERSION`：服务版本（默认 "dev"）
   - `WORKERS_COUNT`：工作进程数（建议与 vCPU 核心数相同，如 1 vCPU 设为 1，2 vCPU 设为 2）
   - `SECRET_KEY`：鉴权密钥（可选）
5. 获取公网 URL 用于 OCR_HOST

### Docker 部署（pdf-transform）

```bash
# 构建镜像
docker build -f pdf-transform/Dockerfile -t pdf-transform .

# 运行容器（单核 CPU）
docker run -p 3000:3000 \
  -e PORT=3000 \
  -e WORKERS_COUNT=1 \
  -e OCR_HOST=http://你的fc地址.com \
  pdf-transform

# 运行容器（4 核 CPU）
docker run -p 3000:3000 \
  -e PORT=3000 \
  -e WORKERS_COUNT=4 \
  -e OCR_HOST=http://你的fc地址.com \
  pdf-transform
```

## 日志分析

### 分析 OCR 处理日志

项目提供了日志分析脚本，可以对 pdf-transform 服务的日志文件进行详细分析，生成包含性能统计和处理详情的报告。

#### 使用方法

```bash
# 自动分析 ./logs 目录下最新的日志文件
python3 scripts/analyze_log.py

# 或指定日志文件路径
python3 scripts/analyze_log.py <日志文件路径>
```

#### 示例

```bash
# 自动检测并分析最新日志
python3 scripts/analyze_log.py

# 输出
# No log file specified. Using latest log file: logs/2025-11-21-12-35-16.log
# Analyzing log file: logs/2025-11-21-12-35-16.log
# Generating report: report.md
# Analysis complete!
# Report saved to: report.md

# 分析指定日志文件
python3 scripts/analyze_log.py logs/2025-11-21-12-35-16.log
```

#### 生成的报告内容

脚本会在项目根目录生成 `report.md` 文件，包含以下统计信息（按 TaskID 分组）：

##### 1. 基本信息
- PDF 总页数
- OCR Workers 并发数

##### 2. 实际处理时长
- **并发处理总时长**：从第一条日志到最后一条日志的实际时间
- **累计处理时长（假设不并发）**：所有页面处理时间的累加值
- **平均每页实际耗时**：并发处理总时长除以已处理页数

##### 3. 页面处理统计
- **平均时长**：每页 OCR 处理的平均时间
- **中位时长**：中位数时长
- **方差**：时长方差

##### 4. OCR 连接建立统计
- **最大建连时长**
- **中位建连时长**
- **建连次数**

##### 5. 每页转换详情
- 详细的页码、耗时、开始时间偏移表格

#### 报告示例

```markdown
## TaskID: aasdafjksdgjsdfkjsdhkhfjsjdtaskId22

### 基本信息

- **总页数**: 373
- **OCR Workers 并发数**: 2

### 实际处理时长

- **并发处理总时长**: 1315.68 秒
- **累计处理时长(假设不并发)**: 1309.46 秒
- **平均每页实际耗时**: 10.53 秒

### 页面处理统计

- **平均时长**: 10.48 秒
- **中位时长**: 10.74 秒
- **方差**: 2.80

### OCR 连接建立统计

- **最大建连时长**: 68.96 秒
- **中位建连时长**: 0.41 秒
- **建连次数**: 21

### 每页转换详情

| 页码 | 耗时(秒) | 开始时间偏移(秒) |
|------|----------|------------------|
| 1 | 8.12 | 78.69 |
| 2 | 4.12 | 82.43 |
| 3 | 17.89 | 100.34 |
...
```

## 依赖管理

### 版本同步

三个 `pyproject.toml` 文件都依赖 `pdf-craft`，需要保持版本一致：

**检查版本一致性：**
```bash
python scripts/check_pdf_craft_version.py
```

**从根目录同步版本：**
```bash
# 1. 更新根目录 pyproject.toml 中的版本
# 2. 运行同步脚本
python scripts/sync_pdf_craft_version.py
# 3. 提交所有变更的文件
```

## 许可证

详见 LICENSE 文件。

## 贡献

1. Fork 本仓库
2. 创建功能分支
3. 进行修改
4. 运行测试：`poetry run pytest`
5. 检查代码风格：`poetry run pylint`
6. 提交 Pull Request

## 支持

如有问题，请在项目仓库提交 issue。