PaddleOCR MCP Server
by pewee-live
README.md
# PaddleOCR MCP Server
将 [PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR)(PP-OCRv5)封装为标准的
[MCP (Model Context Protocol)](https://modelcontextprotocol.io) 服务,支持
**中文、英文、日文、韩文**的文本识别,可被 Claude Desktop、Cursor、Codex 等
任意 MCP 客户端调用,也可作为 Docker 镜像通过网络服务对外提供 OCR 能力。
除纯文本识别外,还集成 **PP-Structure 版面分析**:对含表格/标题/段落的表单或
文档,可自动还原版面结构,把图片、PDF 或办公文档(PPT/Word/Excel)中的表格直接转换为 HTML 与 Markdown 表格代码
(保留行列结构),并返回每个文本区块的坐标框(Bounding Box)。PDF 与多页文档按页逐张处理。
提供两种运行方式,均支持 HTTP(`streamable-http`)传输:
- **Docker**(推荐):镜像内置四种语言模型,构建后可完全离线运行。
- **Python 本地**:首次运行自动下载模型,之后同样可离线。
## 功能特性
- 基于官方 MCP Python SDK(FastMCP),标准 `stdio` 与 `streamable-http` 双传输。
- 每种语言独立缓存 OCR 引擎实例,首次调用懒加载、后续秒级响应。
- 图片、PDF、办公文档输入均支持本地路径、`http(s)` URL、`data:` URI、裸 base64 字符串。
- **原生 PDF 支持**:PDF 交给 PaddleOCR/PaddleX 按页逐张处理(依赖 PyMuPDF),
多页结果自动合并,每个文本行/区块/表格都带 `page` 页码;`recognize_layout`
额外返回 `page_count` 总页数。
- **办公文档支持**:Word(`doc`/`docx`)、PPT(`ppt`/`pptx`)、Excel(`xls`/`xlsx`)及
ODF 格式(`odt`/`odp`/`ods`)先由内置的 LibreOffice headless 转成 PDF,再走 PDF 管道。
办公文档需通过本地路径或带扩展名的 URL 提供(裸 base64 不支持,因 ZIP 容器无法靠魔数嗅探)。
- 返回每行文本、**文本框坐标(Bounding Box)** 与置信度。
- **版面分析(`recognize_layout`)**:基于 PP-Structure 还原标题/段落/表格/图片
等版面区块,表格输出 HTML 与 Markdown(保留行列),并按阅读顺序返回各区块坐标。
- Docker 镜像内置全部四种语言模型,**构建后可完全离线运行**。
- **异步模式(`async_mode=True`)**:大图/慢图识别可设为异步,立即返回 `job_id`,
后台执行 OCR,通过 `get_job_result` 轮询结果。避免长耗时任务触发客户端超时
(如 Dify 默认 5 分钟)。
## 支持的语言
| 代码 | 语言 | 说明 |
|------|------|------|
| `ch` | 中文 + 英文 | 默认值,PP-OCRv5,适合中英混排 |
| `en` | 英文 | 拉丁文字 |
| `japan` | 日文 | 日语 |
| `korean` | 韩文 | 韩语 |
同时接受常用别名:`zh` / `zh-cn` / `chinese` / `cn`(中文),`english`(英文),
`ja` / `japanese`(日文),`ko` / `kr` / `hangul`(韩文)。
## Quickstart
两种方式都默认以 HTTP 服务启动,监听 `0.0.0.0:8000`,MCP 端点为
`http://<主机IP>:8000/mcp`。
### 方式一:Docker(推荐)
镜像构建时会预下载四种语言模型,容器启动即可调用、无需联网。
#### 从 Docker Hub 拉取预构建镜像
仓库提供手动触发的 [构建 workflow](.github/workflows/dockerhub-publish.yml),会在原生 runner 上并行构建 amd64 与 arm64 双架构镜像,合并为单个多架构 manifest 后发布到 Docker Hub。直接拉取即可,免去本地构建:
```bash
# 拉取镜像(自动匹配当前主机的 amd64 / arm64 架构)
docker pull pewee-live/ocr-mcp:latest
# 启动服务(仅本机访问)
docker run -d --name ocr-mcp -p 8000:8000 pewee-live/ocr-mcp:latest
```
> `pewee-live` 为示例命名空间,请替换为你的 Docker Hub 用户名(需与仓库 Secrets 中的 `DOCKERHUB_USERNAME` 一致)。
> 触发方式:GitHub 仓库 → Actions → 选择 **Build & Push to Docker Hub** → Run workflow,按需填写镜像名、标签与是否同步打 `:latest`。首次使用前需在 Secrets 中配置 `DOCKERHUB_USERNAME` 与 `DOCKERHUB_TOKEN`(Access Token,非密码)。
#### 本地构建镜像
```bash
# 构建镜像(首次会下载 paddlepaddle 与四种语言模型,约需数分钟)
docker build -t ocr-mcp:latest .
# 启动服务(仅本机访问)
docker run -d --name ocr-mcp -p 8000:8000 ocr-mcp:latest
# 如果要让局域网/其他机器通过 IP 访问,需要放开 Host 白名单:
docker run -d --name ocr-mcp -p 8000:8000 \
-e MCP_ALLOWED_HOSTS="192.168.7.49:*,localhost:*" \
-e MCP_ALLOWED_ORIGINS="http://192.168.7.49:*,http://localhost:*" \
ocr-mcp:latest
# 查看日志
docker logs -f ocr-mcp
```
启动后,MCP 端点为 `http://<容器所在主机IP>:8000/mcp`。
### 方式二:Python 本地运行
```bash
# 安装依赖
pip install -r requirements.txt
# 以 HTTP 服务启动(仅本机访问)
MCP_TRANSPORT=streamable-http python -m ocr_mcp
# 监听所有网卡,供局域网访问(并把本机 IP 加入白名单)
MCP_TRANSPORT=streamable-http \
MCP_HOST=0.0.0.0 \
MCP_PORT=8000 \
MCP_ALLOWED_HOSTS="192.168.7.49:*,localhost:*" \
MCP_ALLOWED_ORIGINS="http://192.168.7.49:*,http://localhost:*" \
python -m ocr_mcp
```
> Windows PowerShell 设置环境变量用 `$env:MCP_TRANSPORT="streamable-http"` 等,
> 或写成一行:`$env:MCP_TRANSPORT='streamable-http'; python -m ocr_mcp`。
首次运行时会自动从 Hugging Face 下载 PP-OCRv5 模型到本地缓存目录
(`~/.paddlex/official_models`),之后可离线使用。
### 切换模型:server(高精度)vs mobile(快速)
本项目内置两套 PP-OCRv5 模型,Docker 镜像构建时已**同时预下载**,运行时通过 tool 的 `model` 参数切换,**无需重启或改环境变量**。两个引擎各自独立缓存,同一个服务实例可同时服务两种,互不干扰:
| `model` 值 | 检测模型 | 识别模型 | 特点 |
|------------|---------|---------|------|
| `mobile`(默认) | `PP-OCRv5_mobile_det` | `PP-OCRv5_mobile_rec` | 快 3-5 倍,适合运单等清晰印刷体 |
| `server` | `PP-OCRv5_server_det` | `PP-OCRv5_server_rec` | 精度最高、最慢,适合手写/模糊/复杂场景 |
调用 `recognize_text` 或 `recognize_layout` 时传入即可。切换后**首次用某变体需将模型加载进内存(数秒)**,之后命中缓存秒级响应:
```json
// 清晰印刷体运单 — 用 mobile(默认,快)
{"image": "https://.../waybill.pdf", "language": "ch", "model": "mobile"}
// 手写签收回执 / 模糊单据 — 用 server(准)
{"image": "https://.../receipt.pdf", "language": "ch", "model": "server"}
```
> 对 `recognize_layout`,`model` 控制的是 PP-StructureV3 内部 OCR 子模型(det/rec);版面与表格模型(PP-DocLayout、RT-DETR、SLANeXt)无 mobile 变体,两种 `model` 下相同。因此 mobile 在版面分析上的提速幅度小于纯文本 OCR(约 2-3 倍,而非 3-5 倍)。
>
> 旧的 `OCR_DET_MODEL` / `OCR_REC_MODEL` 环境变量已移除。模型选择统一由 `model` 参数控制。
## 重要:跨机访问与 Host 白名单
MCP SDK 默认开启 **DNS 重绑定防护**,只放行本机地址:
`127.0.0.1`、`localhost`、`[::1]`。
因此,**只要你是通过 IP、域名或另一台机器访问**(例如
`http://192.168.7.49:8000/mcp`),就**必须**把该访问地址加入白名单,
否则请求会被拒绝并返回:
```
Invalid Host header (HTTP 421)
```
这种情况下 MCP 客户端连不上服务,工具也不会出现在客户端里(表现为客户端
改用自带模型去"识别",而不是调用本 MCP)。
配置方法(逗号分隔,`*` 表示端口通配):
| 环境变量 | 示例 | 说明 |
|----------|------|------|
| `MCP_ALLOWED_HOSTS` | `192.168.7.49:*,localhost:*` | 允许的 Host 头 |
| `MCP_ALLOWED_ORIGINS` | `http://192.168.7.49:*,http://localhost:*` | 允许的 Origin 头(浏览器/带 Origin 的客户端) |
> 只在本机访问(用 `localhost`)时无需配置。
## 验证服务是否正常
MCP 采用 `streamable-http` 传输,是有状态的协议:必须先 `initialize` 握手拿到会话 ID,再发 `notifications/initialized`,之后才能调用工具。下面提供两种验证方式。
### 方式一:curl 快速握手
用 curl 做 MCP 握手,确认服务可达:
```bash
# 成功:返回 200 + 一段 JSON/SSE 流,包含 serverInfo
curl -i -X POST http://192.168.7.49:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
# 如果返回 "Invalid Host header"(421),说明该访问地址没加入白名单
```
### 方式二:Postman 端到端(base64)
仓库提供了可直接导入的 Postman Collection:[tests/ocr-mcp.postman_collection.json](tests/ocr-mcp.postman_collection.json),内含三个按顺序的请求,会话 ID 自动传递,无需手动复制。
**第 0 步:准备 base64 图片**
Postman 无法直接读取本地文件,需先把图片转成 data URI。任选一种(PDF 同理,把
`image/pdf` 作为 MIME 即可):
```powershell
# PowerShell(Windows):输出一整行 data URI,复制备用
$b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes("sample_data\zh.png"))
"data:image/png;base64,$b64"
```
```bash
# Python(跨平台)
python -c "import base64; b=base64.b64encode(open('sample_data/zh.png','rb').read()).decode(); print('data:image/png;base64,'+b)"
```
**第 1 步:导入并配置**
1. Postman → Import → 选择 `tests/ocr-mcp.postman_collection.json`。
2. 点击 Collection 名 → Variables,设置:
- `base_url`:你的服务地址(如 `http://192.168.137.94:8000`)
- `base64_image`:上一步复制的 data URI
- `session_id`:留空(会自动填充)
**第 2 步:按顺序发送三个请求**
| 顺序 | 请求 | 作用 | 预期 |
|------|------|------|------|
| 1 | `1. Initialize` | 握手,获取会话 ID | `200`,响应头含 `mcp-session-id`,脚本自动存入变量 |
| 2 | `2. Notifications/initialized` | 通知初始化完成 | `202` |
| 3 | `3. recognize_text (base64)` | 用 base64 图片做 OCR | `200`,Body(SSE)里含识别文本 |
> 三个请求都是 `POST {{base_url}}/mcp`,必须带这两个头:
> `Content-Type: application/json`、`Accept: application/json, text/event-stream`。
> 第 2、3 个请求还要带 `mcp-session-id: {{session_id}}`。
> Collection 已配好这些,直接 Send 即可。
**第 3 步:查看结果**
第 3 个请求的响应是 SSE 流,Postman 会以 `EventStream` 形式展示,其中 `data:` 后的 JSON 结构为:
```json
{
"jsonrpc": "2.0", "id": 2,
"result": {
"content": [{"type": "text", "text": "{\"language\":\"ch\",\"count\":2,\"text\":\"你好世界\nHello OCR\"}"}],
"isError": false
}
}
```
即 OCR 成功。若 `isError` 为 `true`,看 `content` 里的错误信息(例如跨机访问时本机路径无效,需改用 data URI 或服务端可达的 URL)。
### 其他:stdio 端到端测试
```bash
python tests/test_mcp_client.py
```
## 接入 MCP 客户端
### Codex
编辑 Codex 配置文件(Windows:`~/.codex/config.toml`,即
`%USERPROFILE%\.codex\config.toml`):
```toml
[mcp_servers.ocr]
enabled = true
url = "http://192.168.7.49:8000/mcp"
```
保存后重启 Codex,即可在对话中让其"识别这张图片里的文字"。
### Claude Desktop
编辑配置文件(macOS:`~/Library/Application Support/Claude/claude_desktop_config.json`,
Windows:`%APPDATA%\Claude\claude_desktop_config.json`):
```json
{
"mcpServers": {
"paddleocr": {
"url": "http://192.168.7.49:8000/mcp",
"type": "http"
}
}
}
```
### Cursor
在 Cursor 设置 → MCP 中添加 HTTP 类型的 MCP Server,URL 填
`http://192.168.7.49:8000/mcp`。
## 提供的 MCP 工具
### `recognize_text`
对图片、PDF 或办公文档执行 OCR 文本识别。
| 参数 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `image` | string | 必填 | 本地文件路径、`http(s)` URL、`data:image/...;base64,...` 或裸 base64;支持图片、PDF 与办公文档(doc/ppt/xls 等) |
| `language` | string | `"ch"` | 语言代码:`ch` / `en` / `japan` / `korean` |
| `detail` | bool | `false` | 坐标框 `box` 始终返回;为 `true` 时额外返回置信度 `confidence` |
| `min_confidence` | float | `0.0` | 过滤低于该置信度的行(0-1,0 表示保留全部)|
| `model` | string | `"mobile"` | 模型变体:`mobile`(快,适合清晰印刷体)/ `server`(准,适合手写/模糊)|
| `async_mode` | bool | `false` | 异步模式:立即返回 `job_id`,后台执行 OCR,用 `get_job_result` 轮询结果 |
返回示例:
```json
{
"language": "ch",
"count": 2,
"recognized_text": "你好世界\nHello OCR",
"lines": [
{"text": "你好世界", "box": [[25, 58], [300, 58], [300, 114], [25, 114]], "page": 0},
{"text": "Hello OCR", "box": [[40, 120], [220, 120], [220, 154], [40, 154]], "page": 0}
]
}
```
异步模式返回(`async_mode=true`):
```json
{
"job_id": "a3f2b1c9d0e4",
"status": "running",
"message": "Job submitted. Call get_job_result with job_id to retrieve the result."
}
```
| 异步返回字段 | 类型 | 说明 |
|-------------|------|------|
| `job_id` | string | 任务 ID,用于 `get_job_result` 轮询 |
| `status` | string | 固定为 `"running"`(任务已提交,后台执行中) |
| `message` | string | 提示信息 |
> 异步模式下 OCR 结果不会直接返回,需用 `job_id` 调 `get_job_result` 轮询;
> 完成后 `get_job_result` 的 `result` 字段结构同上面同步返回的示例。
> 每行 `lines` 始终返回,且自带坐标框 `box` 与 `page` 页码(从 0 开始);`detail=true` 时每行额外带 `confidence`。
> 顶层键为 `recognized_text`(非 `text`,因 Dify 保留 `text` 作为工作流变量名)。
> `box` 是四点角框 `[[x1,y1],[x2,y1],[x2,y2],[x1,y2]]`(顺时针、像素坐标,来自检测多边形);
> 与 `recognize_layout` 的 `box`(`[x1,y1,x2,y2]` 四值轴对齐框)格式不同。
>
> **多页 PDF**:PDF 按页逐张识别,各行带 `page` 页码(0 起),`box` 坐标基于其所在页;
> `recognized_text` 把所有页的文本按顺序换行拼接。
### `recognize_layout`
对图片、PDF 或办公文档执行**版面分析**(基于 PP-Structure),还原表格/标题/段落等结构。
与 `recognize_text`(扁平文本行)不同,它把页面切分为多个版面区块
(`title`/`text`/`table`/`figure`/`formula`/...),识别表格并转为 HTML 与 Markdown
(保留行列结构),同时重建阅读顺序,每个区块都带坐标框 `box`。
| 参数 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `image` | string | 必填 | 本地文件路径、`http(s)` URL、`data:image/...;base64,...` 或裸 base64;支持图片、PDF 与办公文档(doc/ppt/xls 等) |
| `language` | string | `"ch"` | 语言代码:`ch` / `en` / `japan` / `korean` |
| `output` | string | `"markdown"` | 顶层便捷内容:`markdown`(整页 Markdown,表格保留)或 `text`(仅扁平文本)。结构化数据(`regions`/`tables`)始终返回 |
| `flat` | bool | `true` | 输出形态开关,见下方「返回形态」:**Dify 用 `true`(默认)**,把 `regions`/`tables` 序列化为 JSON 字符串;Codex / Claude / 原始 MCP 用 `false` 取回嵌套 dict |
| `model` | string | `"mobile"` | 模型变体:`mobile`(快,适合清晰印刷体)/ `server`(准,适合手写/模糊)。控制 PP-StructureV3 内部 OCR 子模型 |
| `async_mode` | bool | `false` | 异步模式:立即返回 `job_id`,后台执行版面分析,用 `get_job_result` 轮询结果 |
返回字段:
返回形态(`flat` 参数决定):
| `flat` | 适用 | 结构化数据键 | 类型 |
|-------|------|------------|------|
| `true`(默认) | **Dify** | `regions_json` / `tables_json` | JSON 字符串(Dify 工作流变量只接受基本类型,嵌套 dict 会报 `Only basic types and lists are allowed`)|
| `false` | Codex / Claude / 原始 MCP | `regions` / `tables` | 嵌套 dict / list |
- `regions`:版面区块列表,每项含 `{type, text, box, page}`,表格区块额外含 `{html, markdown}`。
- `tables`:识别到的表格列表,每项含 `{html, markdown, box, cell_count, page}`。
- `markdown`:整页 Markdown 渲染结果,表格以 Markdown 表格语法保留行列结构;多页 PDF 时各页之间以 `---` 分隔。
- `region_count` / `table_count`:区块数与表格数;`width`/`height`:页面像素尺寸。
- `page_count`:页数(单张图片为 `1`,PDF 为实际页数)。
返回示例(含一张 2x2 表格的表单):
```json
{
"language": "ch",
"region_count": 2,
"table_count": 1,
"regions": [
{"type": "title", "text": "员工信息表", "box": [40, 20, 410, 70], "page": 0},
{
"type": "table", "box": [30, 90, 430, 260],
"text": "<table><tr><td>姓名</td><td>年龄</td></tr>...</table>",
"html": "<table><tr><td>姓名</td><td>年龄</td></tr><tr><td>张三</td><td>30</td></tr></table>",
"markdown": "| 姓名 | 年龄 |\n| --- | --- |\n| 张三 | 30 |"
}
],
"tables": [
{"html": "<table>...</table>", "markdown": "| 姓名 | 年龄 |\n| --- | --- |\n| 张三 | 30 |", "box": [30, 90, 430, 260], "cell_count": 4, "page": 0}
],
"markdown": "# 员工信息表\n\n| 姓名 | 年龄 |\n| --- | --- |\n| 张三 | 30 |"
}
```
异步模式返回(`async_mode=true`):
```json
{
"job_id": "a3f2b1c9d0e4",
"status": "running",
"message": "Job submitted. Call get_job_result with job_id to retrieve the result."
}
```
| 异步返回字段 | 类型 | 说明 |
|-------------|------|------|
| `job_id` | string | 任务 ID,用于 `get_job_result` 轮询 |
| `status` | string | 固定为 `"running"`(任务已提交,后台执行中) |
| `message` | string | 提示信息 |
> 异步模式下版面分析结果不会直接返回,需用 `job_id` 调 `get_job_result` 轮询;
> 完成后 `get_job_result` 的 `result` 字段结构同上面同步返回的示例
> (含 `regions`/`tables`/`markdown`/`region_count`/`table_count` 等全部字段)。
> 上例为单页(图片或单页 PDF),故顶层 `page_count` 省略(为 `1`)。多页 PDF 时
> 每个 region/table 都带其所在页的 `page` 索引(0 起),并额外返回顶层 `page_count`。
>
> `box` 为 `[x1, y1, x2, y2]`(左上/右下,像素坐标,基于其所在页)。表格的 `markdown` 由服务端
> 把识别出的 HTML 表格转换而来,`colspan`/`rowspan` 会展开为重复单元格以保持网格。
> **引擎与子 pipeline**:`recognize_layout` 默认启用表格/版面/文字识别。公式/图表/印章
> 识别**仅在原生 paddle 引擎**下可用(即 `OCR_ENGINE` 未强制 onnx、x86_64 环境)——paddlex
> 的这三类模型没有 onnx 包,在 `onnxruntime` 引擎下(aarch64 默认 / `OCR_ENGINE=onnxruntime`)
> 会被自动关闭(否则加载即报 `does not provide a 'onnx' package`)。设 `OCR_LAYOUT_FULL=1` 强制开启(仅 paddle 引擎有意义)。
### `list_supported_languages`
返回本服务支持的语言代码与说明,无需参数。
### `get_job_result`
轮询异步 OCR 任务的进度和结果。当 `recognize_text` 或 `recognize_layout` 以
`async_mode=True` 调用时,会立即返回一个 `job_id`,后台执行 OCR。用此工具传入
`job_id` 查询状态和结果。
| 参数 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `job_id` | string | 必填 | 异步任务返回的 `job_id` |
返回示例:
```json
// 运行中
{"job_id": "a3f2b1c9d0e4", "status": "running", "elapsed_seconds": 45.2}
// 完成(结果在 result 字段,结构同 recognize_text/recognize_layout 同步返回)
{"job_id": "a3f2b1c9d0e4", "status": "done", "result": {...}, "elapsed_seconds": 448.0}
// 失败
{"job_id": "a3f2b1c9d0e4", "status": "failed", "error": "OCR inference failed: ...", "elapsed_seconds": 120.3}
// 任务已过期(完成后超过 TTL)
{"error": "Job 'a3f2b1c9d0e4' not found. It may have expired ..."}
```
> **任务存储**:完成任务在内存中保留 1 小时(`OCR_JOB_DONE_TTL`)后自动清除;
> 运行中任务最长存活 2 小时(`OCR_JOB_RUNNING_TTL`);超过容量上限(`OCR_JOB_MAX_ENTRIES`)
> 时按 LRU 淘汰最久未访问的已完成任务。
#### Dify 异步工作流示例
由于 Dify 的 MCP 客户端有 5 分钟超时,大图识别(可能超过 5 分钟)需要用异步模式。
在 Dify 工作流中:
1. **提交任务**:`recognize_layout` 节点设 `async_mode=true`,返回 `job_id`
2. **循环轮询**:用循环节点调 `get_job_result`,传入 `job_id`
- `status=running` → 等 30 秒再轮询
- `status=done` → 退出循环,提取 `result`
- `status=failed` → 退出循环,处理错误
每次 MCP 调用都在 1 秒内返回,不会触发 Dify 的 5 分钟超时。
## 环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `MCP_TRANSPORT` | `stdio` | 传输方式:`stdio` 或 `streamable-http` |
| `MCP_HOST` | `0.0.0.0` | HTTP 模式监听地址 |
| `MCP_PORT` | `8000` | HTTP 模式监听端口 |
| `MCP_ALLOWED_HOSTS` | (仅 loopback) | 允许的 Host 头(逗号分隔,跨机访问必填) |
| `MCP_ALLOWED_ORIGINS` | (仅 loopback) | 允许的 Origin 头(逗号分隔) |
OCR 模型与推理相关:
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `OCR_VERSION` | `PP-OCRv5` | PaddleOCR 模型版本。paddleocr >=3.7 默认 v6 会在 CPU 段错误,故锁回 v5。可选 `PP-OCRv4`/`PP-OCRv6` |
| `OCR_PIR` | `0` | 设为 `1` 时不关闭 paddle 的 PIR 执行器(默认关闭以规避 CPU 段错误) |
| `OCR_ENGINE` | 自动 | 推理引擎。aarch64(树莓派等)自动用 `onnxruntime` 规避 native 段错误;x86 用 `paddle`。可强制 `onnxruntime`/`paddle` |
| `OCR_DEVICE` | `cpu` | 推理设备:`cpu` / `gpu` / `gpu:0`。用 `gpu` 需 GPU 版 paddlepaddle + NVIDIA 驱动 |
| `OCR_MAX_IMAGE_SIDE` | `2880` | OCR 前将图片长边缩放到此值以下(像素)。调小可显著加速,但过小会丢失小字 |
| `OCR_JOB_DONE_TTL` | `3600` | 异步任务完成后在内存中保留的秒数(过期自动清除)|
| `OCR_JOB_RUNNING_TTL` | `7200` | 异步任务运行中最长存活秒数(防止僵死任务永久占内存)|
| `OCR_JOB_MAX_ENTRIES` | `200` | 异步任务存储上限,超限时按 LRU 淘汰最久未访问的已完成任务 |
Docker 镜像默认设置 `MCP_TRANSPORT=streamable-http`。
## 本地 stdio 模式
适用于 Claude Desktop、Cursor 等通过子进程接入的客户端。
```bash
# 不设置 MCP_TRANSPORT 即为 stdio
python -m ocr_mcp
```
Claude Desktop 配置(stdio):
```json
{
"mcpServers": {
"paddleocr": {
"command": "python",
"args": ["-m", "ocr_mcp"],
"cwd": "C:/develop/pythonws/ocr-server",
"env": { "MCP_TRANSPORT": "stdio" }
}
}
}
```
## 构建与测试
```bash
# 生成四种语言的测试图片(需要本机有对应字体)
python tests/make_test_image.py
# 在 Docker 容器内直接验证 OCR 引擎(中/英/日/韩)
docker run --rm \
-v "${PWD}/tests:/work/tests" \
-v "${PWD}/sample_data:/work/sample_data" \
-e PYTHONPATH=/app -w /app \
--entrypoint python ocr-mcp:latest /work/tests/test_engine.py
# 端到端 MCP 客户端测试(stdio)
python tests/test_mcp_client.py
```
## 项目结构
```text
ocr-server/
├── ocr_mcp/
│ ├── __init__.py # 包入口
│ ├── __main__.py # python -m ocr_mcp 入口
│ ├── ocr_engine.py # PaddleOCR 引擎封装与结果归一化
│ ├── server.py # FastMCP 服务与工具定义
│ ├── layout_engine.py # PP-Structure 版面/表格分析与 HTML→Markdown 转换
│ ├── job_store.py # 异步任务内存存储(TTL + LRU 淘汰)
│ └── warmup.py # 模型预下载脚本(Docker 构建期调用)
├── tests/
│ ├── make_test_image.py # 生成四语言测试图
│ ├── test_engine.py # OCR 引擎直跑验证
│ └── test_mcp_client.py # stdio MCP 端到端验证
├── Dockerfile # 内置模型的 Docker 镜像
├── requirements.txt # 运行时依赖
└── pyproject.toml # 包元数据与入口命令
```
## 实现说明
- **模型版本锁定为 PP-OCRv5**:paddleocr >=3.7 在只传 `lang`、不传 `ocr_version` 时会
默认选用 **PP-OCRv6**(`PP-OCRv6_medium_det`/`rec`),而 PP-OCRv6 在 paddlepaddle 3.x
CPU 上会触发原生段错误(`ConvertPirAttribute2RuntimeAttribute`),连 `enable_mkldnn=False`
也无法避免,直接导致进程崩溃、服务停止。本项目通过 `ocr_version="PP-OCRv5"` 锁定到验证可用的
v5 模型(中/英/日/韩均有对应模型)。可用环境变量 `OCR_VERSION` 覆盖(如 `PP-OCRv4`)。
- **关闭 PIR 执行器**:在导入 paddle 前,默认设置 `FLAGS_enable_pir_in_executor=0`、
`FLAGS_enable_pir_api=0`,作为 CPU 段错误的兜底防御。需要恢复新 IR 执行器时设 `OCR_PIR=1`。
- **CPU 推理**:`device="cpu"` + `enable_mkldnn=False`,规避 OneDNN 相关问题。如需 GPU,
可在 [ocr_engine.py](ocr_mcp/ocr_engine.py) 中将 `device="cpu"` 改为 `"gpu"` 并移除
`enable_mkldnn=False`,同时使用 GPU 版 paddlepaddle。
- **模型变体(server / mobile 两套预热)**:镜像构建时通过 prefetch 脚本同时预下载 **server**
(高精度、慢)与 **mobile**(快 3-5 倍,适合运单等清晰印刷体)两套模型,运行时通过 tool 的
`model` 参数(默认 `mobile`)切换,两个引擎各自独立缓存、无需重启或改环境变量。
- **GPU 加速**:设置 `OCR_DEVICE=gpu` 即可走 GPU 推理(需 GPU 版 `paddlepaddle-gpu` +
NVIDIA 驱动 + `nvidia-container-toolkit`,`docker run --gpus all`)。aarch64(树莓派等)无
NVIDIA GPU,只能 CPU。
- **ARM64(aarch64)用 ONNX Runtime**:paddlepaddle 3.x 的 aarch64 预编译包在推理时存在
原生空指针段错误(native kernel,无环境变量可绕)。本项目在检测到 aarch64(树莓派、Graviton、
Apple Silicon Docker 等)时自动改用 `engine='onnxruntime'`,完全绕开 paddle native kernel。
需要安装 `onnxruntime` 与 `paddle2onnx`(已加入 requirements)。可用 `OCR_ENGINE` 覆盖。
- **模型缓存**:镜像构建时通过 `python -m ocr_mcp.warmup` 预初始化四种语言的引擎,
模型权重写入 `/home/app/.paddlex/official_models`,使运行期不再依赖网络。
- **线程安全**:OCR 推理通过 `anyio.to_thread.run_sync` 在工作线程中执行,避免阻塞
MCP 事件循环;引擎实例以语言为键缓存,带双重检查锁。
- **PDF 支持**:PDF 直接交给 PaddleOCR/PaddleX 的 `predict()` 原生按页处理(内部用
PyMuPDF 解码各页),不预先栅格化。本服务仅负责:把 URL/base64 形式的 PDF 落成临时文件
再透传路径(URL/base64 无法被引擎当字节消费),并在推理后清理临时文件;本地 PDF 路径则
原样透传。多页结果按页合并,每行/区块/表格带 `page`,`recognize_layout` 额外返回 `page_count`,
多页 Markdown 以 `---` 分页。
- **办公文档支持**:Word/PPT/Excel(`doc`/`docx`/`ppt`/`pptx`/`xls`/`xlsx`/`odt`/`odp`/`ods`)
先由内置 LibreOffice headless 转成 PDF,再走上述 PDF 管道。每次调用用独立的
`UserInstallation` profile 目录,避免并发 `soffice` 实例争用共享锁;转换产物作为临时 PDF
追踪并由 `release_input` 清理。镜像需安装 LibreOffice + `fonts-noto-cjk`(无中文字体会渲染成豆腐块,
OCR 出来是乱码)。办公文档仅支持本地路径与带扩展名的 URL(裸 base64 不支持,因 OOZIP 容器无法靠魔数嗅探)。
## 依赖版本
- paddleocr `3.7.0` + paddlepaddle `3.2.2`(3.2.2 是最高同时有 amd64/aarch64 wheel 的版本)
onnxruntime + paddle2onnx(ORT 引擎 / aarch64 推理所需)
pydantic `2.13.4` + mcp `1.28.1`(版本锁定,避免 pip 回溯解析失败)
Python `>= 3.10`(镜像基于 `python:3.11-slim`)
## License
MIT
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues