multi-ocr-py 0.1.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/PKG-INFO +17 -17
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/README.md +16 -16
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-15-cli-args-design.md +2 -4
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-15-concurrency-design.md +2 -2
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-15-engine-refactor-design.md +1 -3
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-15-liteparse-engine-design.md +1 -2
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-15-model-shorthand-design.md +1 -9
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-15-pdf-progress-design.md +2 -2
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-16-src-package-restructure-design.md +0 -1
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/pyproject.toml +4 -1
- multi_ocr_py-0.2.2/release.sh +36 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/src/multi_ocr/__init__.py +2 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/src/multi_ocr/batch.py +2 -2
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/src/multi_ocr/cli.py +0 -3
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/src/multi_ocr/engines/__init__.py +4 -5
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/src/multi_ocr/engines/base.py +5 -1
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/tests/test_engines.py +0 -9
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/uv.lock +2 -2
- multi_ocr_py-0.1.0/src/multi_ocr/engines/dashscope.py +0 -85
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/.gitignore +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/.python-version +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/LICENSE +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/liteparse/Markdown-Output.md +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/liteparse/library-use.md +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/liteparse/ocr-config.md +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-15-batch-pdf-concurrency-design.md +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-15-batch-processing-design.md +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-15-multi-ocr-design.md +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-15-ollama-engine-design.md +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/src/multi_ocr/engines/liteparse.py +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/src/multi_ocr/engines/ollama.py +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/src/multi_ocr/engines/siliconflow.py +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/src/multi_ocr/pdf_utils.py +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/src/multi_ocr/single.py +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/tests/1.jpg +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/tests/2.pdf +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/tests/conftest.py +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/tests/test_batch.py +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/tests/test_pdf_utils.py +0 -0
- {multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/tests/test_single.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multi-ocr-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: 多引擎 OCR 工具包:将 PDF 和图片转换为 Markdown,支持 CLI 全局安装和 SDK 依赖两种使用方式
|
|
5
5
|
Project-URL: Repository, https://github.com/BlackBoxRecorder/multi-ocr
|
|
6
6
|
Author-email: Yinnan <im.yinnan@outlook.com>
|
|
@@ -33,30 +33,32 @@ Description-Content-Type: text/markdown
|
|
|
33
33
|
|
|
34
34
|
## 支持的引擎
|
|
35
35
|
|
|
36
|
-
| 引擎 | 说明 | 类型 |
|
|
37
|
-
|
|
38
|
-
| SiliconFlow + DeepSeek-OCR | 云端 OCR,精度高 | API |
|
|
39
|
-
| SiliconFlow + PaddleOCR-VL-1.5 | 云端 OCR,轻量快速 | API |
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
36
|
+
| 引擎 | 说明 | 类型 | 场景 | 费用 |
|
|
37
|
+
|---|---|---|---|---|
|
|
38
|
+
| SiliconFlow + DeepSeek-OCR | 云端 OCR,精度高 | API | 图片+PDF | 免费 |
|
|
39
|
+
| SiliconFlow + PaddleOCR-VL-1.5 | 云端 OCR,轻量快速 | API | 图片+PDF | 免费 |
|
|
40
|
+
| LiteParse | 本地 PDF 解析,无需联网 | 本地 | PDF | 免费 |
|
|
41
|
+
| Ollama + DeepSeek-OCR | 本地部署 OCR | 本地 | 图片+PDF | 免费 |
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
> 本地运行 ollama 参考:https://ollama.com/library/deepseek-ocr
|
|
43
45
|
|
|
44
46
|
## 安装
|
|
45
47
|
|
|
46
48
|
### CLI 全局安装
|
|
47
49
|
|
|
48
50
|
```bash
|
|
49
|
-
uv tool install multi-ocr
|
|
51
|
+
uv tool install multi-ocr-py
|
|
50
52
|
# 或
|
|
51
|
-
pip install multi-ocr
|
|
53
|
+
pip install multi-ocr-py
|
|
52
54
|
```
|
|
53
55
|
|
|
54
56
|
### SDK 依赖安装
|
|
55
57
|
|
|
56
58
|
```bash
|
|
57
|
-
pip install multi-ocr
|
|
59
|
+
pip install multi-ocr-py
|
|
58
60
|
# 或
|
|
59
|
-
uv add multi-ocr
|
|
61
|
+
uv add multi-ocr-py
|
|
60
62
|
```
|
|
61
63
|
|
|
62
64
|
## CLI 使用
|
|
@@ -66,13 +68,13 @@ uv add multi-ocr
|
|
|
66
68
|
multi-ocr document.pdf
|
|
67
69
|
|
|
68
70
|
# 指定引擎和页码范围
|
|
69
|
-
multi-ocr document.pdf --model
|
|
71
|
+
multi-ocr document.pdf --model liteparse --pages 1-5
|
|
70
72
|
|
|
71
|
-
#
|
|
73
|
+
# 批量处理目录,4个并发
|
|
72
74
|
multi-ocr ./scans/ --model silicon-deepseek-ocr -j 4
|
|
73
75
|
|
|
74
76
|
# 查看帮助
|
|
75
|
-
multi-ocr
|
|
77
|
+
multi-ocr -h
|
|
76
78
|
```
|
|
77
79
|
|
|
78
80
|
### 环境变量
|
|
@@ -80,10 +82,8 @@ multi-ocr --help
|
|
|
80
82
|
| 引擎 | 环境变量 |
|
|
81
83
|
|---|---|
|
|
82
84
|
| SiliconFlow | `SILICONFLOW_API_KEY` |
|
|
83
|
-
| DashScope | `DASHSCOPE_API_KEY` |
|
|
84
85
|
| Ollama | `OLLAMA_BASE_URL`(可选,默认 http://127.0.0.1:11434)|
|
|
85
86
|
|
|
86
|
-
也可通过 `--api-key` 参数直接传入。
|
|
87
87
|
|
|
88
88
|
## SDK 使用
|
|
89
89
|
|
|
@@ -7,30 +7,32 @@
|
|
|
7
7
|
|
|
8
8
|
## 支持的引擎
|
|
9
9
|
|
|
10
|
-
| 引擎 | 说明 | 类型 |
|
|
11
|
-
|
|
12
|
-
| SiliconFlow + DeepSeek-OCR | 云端 OCR,精度高 | API |
|
|
13
|
-
| SiliconFlow + PaddleOCR-VL-1.5 | 云端 OCR,轻量快速 | API |
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
10
|
+
| 引擎 | 说明 | 类型 | 场景 | 费用 |
|
|
11
|
+
|---|---|---|---|---|
|
|
12
|
+
| SiliconFlow + DeepSeek-OCR | 云端 OCR,精度高 | API | 图片+PDF | 免费 |
|
|
13
|
+
| SiliconFlow + PaddleOCR-VL-1.5 | 云端 OCR,轻量快速 | API | 图片+PDF | 免费 |
|
|
14
|
+
| LiteParse | 本地 PDF 解析,无需联网 | 本地 | PDF | 免费 |
|
|
15
|
+
| Ollama + DeepSeek-OCR | 本地部署 OCR | 本地 | 图片+PDF | 免费 |
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
> 本地运行 ollama 参考:https://ollama.com/library/deepseek-ocr
|
|
17
19
|
|
|
18
20
|
## 安装
|
|
19
21
|
|
|
20
22
|
### CLI 全局安装
|
|
21
23
|
|
|
22
24
|
```bash
|
|
23
|
-
uv tool install multi-ocr
|
|
25
|
+
uv tool install multi-ocr-py
|
|
24
26
|
# 或
|
|
25
|
-
pip install multi-ocr
|
|
27
|
+
pip install multi-ocr-py
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
### SDK 依赖安装
|
|
29
31
|
|
|
30
32
|
```bash
|
|
31
|
-
pip install multi-ocr
|
|
33
|
+
pip install multi-ocr-py
|
|
32
34
|
# 或
|
|
33
|
-
uv add multi-ocr
|
|
35
|
+
uv add multi-ocr-py
|
|
34
36
|
```
|
|
35
37
|
|
|
36
38
|
## CLI 使用
|
|
@@ -40,13 +42,13 @@ uv add multi-ocr
|
|
|
40
42
|
multi-ocr document.pdf
|
|
41
43
|
|
|
42
44
|
# 指定引擎和页码范围
|
|
43
|
-
multi-ocr document.pdf --model
|
|
45
|
+
multi-ocr document.pdf --model liteparse --pages 1-5
|
|
44
46
|
|
|
45
|
-
#
|
|
47
|
+
# 批量处理目录,4个并发
|
|
46
48
|
multi-ocr ./scans/ --model silicon-deepseek-ocr -j 4
|
|
47
49
|
|
|
48
50
|
# 查看帮助
|
|
49
|
-
multi-ocr
|
|
51
|
+
multi-ocr -h
|
|
50
52
|
```
|
|
51
53
|
|
|
52
54
|
### 环境变量
|
|
@@ -54,10 +56,8 @@ multi-ocr --help
|
|
|
54
56
|
| 引擎 | 环境变量 |
|
|
55
57
|
|---|---|
|
|
56
58
|
| SiliconFlow | `SILICONFLOW_API_KEY` |
|
|
57
|
-
| DashScope | `DASHSCOPE_API_KEY` |
|
|
58
59
|
| Ollama | `OLLAMA_BASE_URL`(可选,默认 http://127.0.0.1:11434)|
|
|
59
60
|
|
|
60
|
-
也可通过 `--api-key` 参数直接传入。
|
|
61
61
|
|
|
62
62
|
## SDK 使用
|
|
63
63
|
|
{multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-15-cli-args-design.md
RENAMED
|
@@ -49,19 +49,18 @@ python main.py <path> [--model MODEL] [--pages PAGES] [--api-key KEY] [--ollama-
|
|
|
49
49
|
|------|---------------------------|---------------------------|
|
|
50
50
|
| liteparse | ❌ 不支持 | ✅ 支持 |
|
|
51
51
|
| siliconflow | ✅ 支持 | ❌ 不支持 |
|
|
52
|
-
| dashscope | ✅ 支持 | ❌ 不支持 |
|
|
53
52
|
| ollama | ✅ 支持 | ❌ 不支持 |
|
|
54
53
|
|
|
55
54
|
### 单文件模式下的自动转换
|
|
56
55
|
|
|
57
|
-
| 输入类型 | liteparse | 其他引擎 (siliconflow/
|
|
56
|
+
| 输入类型 | liteparse | 其他引擎 (siliconflow/ollama) |
|
|
58
57
|
|---------|----------|---------------------------------------|
|
|
59
58
|
| 图片 | 转为单页 PDF → `parse_pdf()` | 直接 `parse_image()` |
|
|
60
59
|
| PDF | 直接 `parse_pdf()` | 拆为图片 → `parse_image()` 逐页识别 |
|
|
61
60
|
|
|
62
61
|
### 批量模式下的自动转换
|
|
63
62
|
|
|
64
|
-
| 输入类型 | liteparse | 其他引擎 (siliconflow/
|
|
63
|
+
| 输入类型 | liteparse | 其他引擎 (siliconflow/ollama) |
|
|
65
64
|
|---------|----------|---------------------------------------|
|
|
66
65
|
| 图片目录 | 合并为 PDF → `parse_pdf()` | 逐张 `parse_image()` → 合并输出 |
|
|
67
66
|
| PDF 目录 | 逐文件 `parse_pdf()` | 逐文件拆为图片 → `parse_image()` 逐页识别 |
|
|
@@ -128,7 +127,6 @@ python main.py <path> [--model MODEL] [--pages PAGES] [--api-key KEY] [--ollama-
|
|
|
128
127
|
| `ocr.py` | `recognize()` → `parse_image()` 调用更新 |
|
|
129
128
|
| `engines/base.py` | `recognize()` 重命名为 `parse_image()` |
|
|
130
129
|
| `engines/siliconflow.py` | `recognize()` 重命名为 `parse_image()` |
|
|
131
|
-
| `engines/dashscope.py` | `recognize()` 重命名为 `parse_image()` |
|
|
132
130
|
| `engines/ollama.py` | `recognize()` 重命名为 `parse_image()` |
|
|
133
131
|
| `engines/liteparse.py` | `recognize()` → `parse_image()` 或抛出 NotImplementedError(如有实现) |
|
|
134
132
|
| `tests/test_batch.py` | 新增混合目录报错测试;更新对应测试用例;`recognize()` → `parse_image()` |
|
{multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-15-concurrency-design.md
RENAMED
|
@@ -67,7 +67,7 @@ with ThreadPoolExecutor(max_workers=concurrency) as executor:
|
|
|
67
67
|
|
|
68
68
|
### 引擎线程安全
|
|
69
69
|
|
|
70
|
-
- API 引擎(SiliconFlow、
|
|
70
|
+
- API 引擎(SiliconFlow、Ollama):HTTP 客户端 `OpenAI` 实例本身是线程安全的,多个线程共享同一引擎实例并发调用 `parse_image()` 没有问题
|
|
71
71
|
- LiteParse:该引擎覆盖了 `parse_pdf()`,不走默认逐页实现,因此不受并发改动影响
|
|
72
72
|
|
|
73
73
|
### 特殊情况处理
|
|
@@ -117,7 +117,7 @@ python main.py ./images
|
|
|
117
117
|
| `engines/base.py` | `OCREngine.parse_pdf()` | `concurrency: int = 1` |
|
|
118
118
|
| `main.py` | `main()` | 解析 `-j` 参数并向下传递 |
|
|
119
119
|
|
|
120
|
-
引擎子类(SiliconFlow、
|
|
120
|
+
引擎子类(SiliconFlow、Ollama)**无需修改**,仅有 LiteParse 覆盖了 `parse_pdf()` 签名,需补上 `concurrency` 参数以保持兼容(内部忽略该参数)。
|
|
121
121
|
|
|
122
122
|
## Testing
|
|
123
123
|
|
|
@@ -38,7 +38,6 @@ multi-ocr/
|
|
|
38
38
|
│ ├── __init__.py # 注册表 + get_engine()
|
|
39
39
|
│ ├── base.py # OCREngine(parse_image + parse_pdf 均为 abstract)
|
|
40
40
|
│ ├── siliconflow.py # + parse_pdf
|
|
41
|
-
│ ├── dashscope.py # + parse_pdf
|
|
42
41
|
│ ├── ollama.py # + parse_pdf
|
|
43
42
|
│ └── liteparse.py # 不变
|
|
44
43
|
└── tests/
|
|
@@ -66,7 +65,7 @@ class OCREngine(ABC):
|
|
|
66
65
|
|
|
67
66
|
## 各引擎实现
|
|
68
67
|
|
|
69
|
-
### siliconflow /
|
|
68
|
+
### siliconflow / ollama
|
|
70
69
|
|
|
71
70
|
`parse_image` 不变。`parse_pdf` 统一模式:
|
|
72
71
|
|
|
@@ -197,7 +196,6 @@ def ocr_file(
|
|
|
197
196
|
|------|----------|
|
|
198
197
|
| `engines/base.py` | 修改 — `parse_pdf` 改为 abstractmethod |
|
|
199
198
|
| `engines/siliconflow.py` | 修改 — 新增 `parse_pdf` |
|
|
200
|
-
| `engines/dashscope.py` | 修改 — 新增 `parse_pdf` |
|
|
201
199
|
| `engines/ollama.py` | 修改 — 新增 `parse_pdf` |
|
|
202
200
|
| `engines/liteparse.py` | 不变 |
|
|
203
201
|
| `pdf_utils.py` | 修改 — 新增 `split_pdf` |
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
新增 LiteParse 作为本地 PDF 解析引擎。LiteParse 是一个基于 Rust 的开源文档解析库,Python 包 `liteparse` 可直接调用,无需 API Key、无需联网,能直接解析 PDF 并输出 markdown。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
与现有云端引擎(如 SiliconFlow)不同,LiteParse **仅处理 PDF**,不处理图片。同时,本次统一所有引擎的输出格式为 markdown,保存扩展名改为 `.md`。
|
|
8
8
|
|
|
9
9
|
## 引擎接口扩展
|
|
10
10
|
|
|
@@ -58,7 +58,6 @@ class LiteParseEngine(OCREngine):
|
|
|
58
58
|
|
|
59
59
|
```python
|
|
60
60
|
_registry: dict[str, type[OCREngine]] = {
|
|
61
|
-
"dashscope": DashScopeEngine,
|
|
62
61
|
"siliconflow": SiliconFlowEngine,
|
|
63
62
|
"liteparse": LiteParseEngine,
|
|
64
63
|
}
|
|
@@ -10,14 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
python main.py input.pdf --provider siliconflow --model deepseek-ai/DeepSeek-OCR
|
|
13
|
-
python main.py input.pdf --provider dashscope --model qwen3.5-ocr
|
|
14
13
|
```
|
|
15
14
|
|
|
16
15
|
改为缩写后:
|
|
17
16
|
|
|
18
17
|
```bash
|
|
19
18
|
python main.py input.pdf --model silicon-deepseek-ocr
|
|
20
|
-
python main.py input.pdf --model dashscope-qwen-ocr
|
|
21
19
|
```
|
|
22
20
|
|
|
23
21
|
## MODEL_MAP 定义
|
|
@@ -28,8 +26,6 @@ python main.py input.pdf --model dashscope-qwen-ocr
|
|
|
28
26
|
MODEL_MAP: dict[str, tuple[str, str, str]] = {
|
|
29
27
|
"silicon-deepseek-ocr": ("siliconflow", "deepseek-ai/DeepSeek-OCR", "SiliconFlow + DeepSeek-OCR"),
|
|
30
28
|
"silicon-paddle-ocr": ("siliconflow", "PaddlePaddle/PaddleOCR-VL-1.5", "SiliconFlow + PaddleOCR-VL-1.5"),
|
|
31
|
-
"dashscope-qwen-ocr": ("dashscope", "qwen3.5-ocr", "DashScope + Qwen-OCR"),
|
|
32
|
-
"dashscope-qwen-vl-ocr": ("dashscope", "qwen-vl-ocr", "DashScope + Qwen-VL-OCR"),
|
|
33
29
|
}
|
|
34
30
|
```
|
|
35
31
|
|
|
@@ -64,16 +60,13 @@ DEFAULT_MODEL = "silicon-deepseek-ocr"
|
|
|
64
60
|
可用模型:
|
|
65
61
|
silicon-deepseek-ocr → SiliconFlow + DeepSeek-OCR
|
|
66
62
|
silicon-paddle-ocr → SiliconFlow + PaddleOCR-VL-1.5
|
|
67
|
-
dashscope-qwen-ocr → DashScope + Qwen-OCR
|
|
68
|
-
dashscope-qwen-vl-ocr → DashScope + Qwen-VL-OCR
|
|
69
63
|
```
|
|
70
64
|
|
|
71
65
|
`--api-key` 的 help 提示用户根据模型配置对应环境变量:
|
|
72
66
|
|
|
73
67
|
```
|
|
74
68
|
--api-key API_KEY API Key(根据 --model 自动读取对应环境变量:
|
|
75
|
-
|
|
76
|
-
dashscope 模型 → DASHSCOPE_API_KEY)
|
|
69
|
+
siliconflow 模型 → SILICONFLOW_API_KEY)
|
|
77
70
|
```
|
|
78
71
|
|
|
79
72
|
## 主流程变更(`main.py`)
|
|
@@ -106,7 +99,6 @@ ocr_file(...) → 执行 OCR
|
|
|
106
99
|
| 场景 | 预期 |
|
|
107
100
|
|------|------|
|
|
108
101
|
| `--model silicon-deepseek-ocr` | 正确推导 provider=SiliconFlow,使用 DeepSeek-OCR |
|
|
109
|
-
| `--model dashscope-qwen-ocr` | 正确推导 provider=DashScope,使用 qwen3.5-ocr |
|
|
110
102
|
| `--model invalid-model` | 报错退出,列出所有可用缩写 |
|
|
111
103
|
| 不传 `--model` | 使用默认值 `silicon-deepseek-ocr` |
|
|
112
104
|
| 未设置对应环境变量 | 提示设置对应环境变量(如 SILICONFLOW_API_KEY) |
|
{multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-15-pdf-progress-design.md
RENAMED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## 背景
|
|
8
8
|
|
|
9
|
-
当前 `ocr_file()` 对 PDF 逐页调用 OCR API 时没有任何进度提示。批量模式仅有文件级 tqdm,不显示页级进度。三个引擎(SiliconFlow /
|
|
9
|
+
当前 `ocr_file()` 对 PDF 逐页调用 OCR API 时没有任何进度提示。批量模式仅有文件级 tqdm,不显示页级进度。三个引擎(SiliconFlow / Ollama)各自实现了完全相同的 `parse_pdf()` 逐页迭代逻辑。
|
|
10
10
|
|
|
11
11
|
## 各模式进度行为
|
|
12
12
|
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
- 新增 `from typing import Callable`、`import shutil`、`from pdf_utils import split_pdf`
|
|
29
29
|
- 方法体:`split_pdf()` → 逐页 `parse_image()` → 每页调用 `progress_callback()` → 合并结果
|
|
30
30
|
|
|
31
|
-
### 2. `engines/siliconflow.py` / `
|
|
31
|
+
### 2. `engines/siliconflow.py` / `ollama.py` — 删除重复代码
|
|
32
32
|
|
|
33
33
|
- 删除各自的 `parse_pdf()` 方法(每个约 12 行)
|
|
34
34
|
- 删除不再需要的 `import shutil` 和 `from pdf_utils import split_pdf`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "multi-ocr-py"
|
|
3
|
-
|
|
3
|
+
dynamic = ["version"]
|
|
4
4
|
description = "多引擎 OCR 工具包:将 PDF 和图片转换为 Markdown,支持 CLI 全局安装和 SDK 依赖两种使用方式"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -42,6 +42,9 @@ build-backend = "hatchling.build"
|
|
|
42
42
|
[tool.hatch.build.targets.wheel]
|
|
43
43
|
packages = ["src/multi_ocr"]
|
|
44
44
|
|
|
45
|
+
[tool.hatch.version]
|
|
46
|
+
path = "src/multi_ocr/__init__.py"
|
|
47
|
+
|
|
45
48
|
[tool.pytest.ini_options]
|
|
46
49
|
pythonpath = ["src"]
|
|
47
50
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
PART="${1:-minor}"
|
|
5
|
+
|
|
6
|
+
echo "==> multi-ocr 发布流程开始"
|
|
7
|
+
|
|
8
|
+
# 1. 检查 git 状态
|
|
9
|
+
if [ -n "$(git status --porcelain)" ]; then
|
|
10
|
+
echo "==> 错误: 工作区有未提交的变更"
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# 2. 升级版本号
|
|
15
|
+
echo "==> 升级版本 ($PART)"
|
|
16
|
+
hatch version "$PART"
|
|
17
|
+
NEW_VERSION=$(hatch version)
|
|
18
|
+
echo " 新版本: $NEW_VERSION"
|
|
19
|
+
|
|
20
|
+
# 3. 构建
|
|
21
|
+
echo "==> 构建包"
|
|
22
|
+
hatch build
|
|
23
|
+
|
|
24
|
+
# 4. 发布到 PyPI(使用 UV_PUBLISH_TOKEN / UV_PUBLISH_PASSWORD)
|
|
25
|
+
echo "==> 发布到 PyPI"
|
|
26
|
+
uv publish
|
|
27
|
+
|
|
28
|
+
# 5. Git 提交 + Tag
|
|
29
|
+
echo "==> Git 提交 & 打标签"
|
|
30
|
+
git add -A
|
|
31
|
+
git commit -m "chore: release v${NEW_VERSION}"
|
|
32
|
+
git tag "v${NEW_VERSION}"
|
|
33
|
+
git push
|
|
34
|
+
git push --tags
|
|
35
|
+
|
|
36
|
+
echo "==> v${NEW_VERSION} 发布完成!"
|
|
@@ -65,7 +65,7 @@ def _batch_images(
|
|
|
65
65
|
):
|
|
66
66
|
idx = future_to_idx[future]
|
|
67
67
|
results[idx] = future.result()
|
|
68
|
-
merged =
|
|
68
|
+
merged = engine.merge_results([r for r in results if r is not None])
|
|
69
69
|
else:
|
|
70
70
|
results = []
|
|
71
71
|
for img_path in tqdm(
|
|
@@ -73,7 +73,7 @@ def _batch_images(
|
|
|
73
73
|
):
|
|
74
74
|
text = engine.parse_image(img_path)
|
|
75
75
|
results.append(text)
|
|
76
|
-
merged =
|
|
76
|
+
merged = engine.merge_results(results)
|
|
77
77
|
|
|
78
78
|
output_path = input_dir.with_suffix(".md")
|
|
79
79
|
output_path.write_text(merged, encoding="utf-8")
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
支持多种 OCR 引擎:
|
|
4
4
|
- SiliconFlow(DeepSeek-OCR / PaddleOCR-VL-1.5)
|
|
5
|
-
- DashScope(Qwen-VL-OCR)
|
|
6
5
|
- LiteParse(本地 PDF 解析)
|
|
7
6
|
- Ollama(本地 DeepSeek-OCR)
|
|
8
7
|
|
|
@@ -37,7 +36,6 @@ MODEL_MAP: dict[str, tuple[str, str, str]] = {
|
|
|
37
36
|
"PaddlePaddle/PaddleOCR-VL-1.5",
|
|
38
37
|
"SiliconFlow + PaddleOCR-VL-1.5",
|
|
39
38
|
),
|
|
40
|
-
"dashscope-qwen-vl-ocr": ("dashscope", "qwen-vl-ocr", "DashScope + Qwen-VL-OCR"),
|
|
41
39
|
"liteparse": ("liteparse", "", "LiteParse (本地 PDF 解析)"),
|
|
42
40
|
"ollama-deepseek-ocr": (
|
|
43
41
|
"ollama",
|
|
@@ -49,7 +47,6 @@ MODEL_MAP: dict[str, tuple[str, str, str]] = {
|
|
|
49
47
|
# provider -> 环境变量名
|
|
50
48
|
_PROVIDER_ENV: dict[str, str] = {
|
|
51
49
|
"siliconflow": "SILICONFLOW_API_KEY",
|
|
52
|
-
"dashscope": "DASHSCOPE_API_KEY",
|
|
53
50
|
# ollama / liteparse 为本地引擎,不需要 API Key
|
|
54
51
|
}
|
|
55
52
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from multi_ocr.engines.base import OCREngine
|
|
2
|
-
from multi_ocr.engines.dashscope import DashScopeEngine
|
|
3
2
|
from multi_ocr.engines.ollama import OllamaEngine
|
|
4
3
|
from multi_ocr.engines.siliconflow import SiliconFlowEngine
|
|
5
4
|
|
|
@@ -14,7 +13,6 @@ except ImportError:
|
|
|
14
13
|
# 引擎注册表:provider 名 -> Engine 类
|
|
15
14
|
# 添加新提供商时只需在此注册
|
|
16
15
|
_registry: dict[str, type[OCREngine]] = {
|
|
17
|
-
"dashscope": DashScopeEngine,
|
|
18
16
|
"ollama": OllamaEngine,
|
|
19
17
|
"siliconflow": SiliconFlowEngine,
|
|
20
18
|
}
|
|
@@ -24,7 +22,7 @@ if _HAS_LITEPARSE:
|
|
|
24
22
|
|
|
25
23
|
|
|
26
24
|
def get_engine(
|
|
27
|
-
provider: str, model: str, api_key: str, base_url: str | None = None
|
|
25
|
+
provider: str, model: str, api_key: str, base_url: str | None = None, **extra_kwargs
|
|
28
26
|
) -> OCREngine:
|
|
29
27
|
"""工厂函数:按 provider 名查找并实例化 OCR 引擎。
|
|
30
28
|
|
|
@@ -44,6 +42,7 @@ def get_engine(
|
|
|
44
42
|
if engine_cls is None:
|
|
45
43
|
available = ", ".join(_registry.keys())
|
|
46
44
|
raise ValueError(f"未知的 OCR 提供商: {provider},当前可用: {available}")
|
|
45
|
+
kwargs: dict = dict(extra_kwargs)
|
|
47
46
|
if base_url is not None:
|
|
48
|
-
|
|
49
|
-
return engine_cls(model=model, api_key=api_key)
|
|
47
|
+
kwargs["base_url"] = base_url
|
|
48
|
+
return engine_cls(model=model, api_key=api_key, **kwargs)
|
|
@@ -10,6 +10,10 @@ from multi_ocr.pdf_utils import split_pdf
|
|
|
10
10
|
class OCREngine(ABC):
|
|
11
11
|
"""OCR 引擎抽象基类。所有 OCR 引擎必须实现此接口。"""
|
|
12
12
|
|
|
13
|
+
def merge_results(self, results: list[str]) -> str:
|
|
14
|
+
"""合并多条返回结果。默认用双换行分隔,JSON 引擎可覆写为数组格式。"""
|
|
15
|
+
return "\n\n".join(results)
|
|
16
|
+
|
|
13
17
|
@abstractmethod
|
|
14
18
|
def parse_image(self, image_path: Path) -> str:
|
|
15
19
|
"""对单张图片进行 OCR,返回 markdown 文本。
|
|
@@ -56,7 +60,7 @@ class OCREngine(ABC):
|
|
|
56
60
|
results.append(f"--- 第 {label} 页 ---\n{text}")
|
|
57
61
|
if progress_callback:
|
|
58
62
|
progress_callback()
|
|
59
|
-
return
|
|
63
|
+
return self.merge_results(results)
|
|
60
64
|
finally:
|
|
61
65
|
if image_paths:
|
|
62
66
|
shutil.rmtree(image_paths[0].parent, ignore_errors=True)
|
|
@@ -19,15 +19,6 @@ class TestGetEngine:
|
|
|
19
19
|
assert isinstance(engine, SiliconFlowEngine)
|
|
20
20
|
assert engine._model == "test-model"
|
|
21
21
|
|
|
22
|
-
def test_known_provider_dashscope(self) -> None:
|
|
23
|
-
engine = get_engine(
|
|
24
|
-
provider="dashscope", model="qwen-vl-ocr", api_key="test-key"
|
|
25
|
-
)
|
|
26
|
-
from multi_ocr.engines.dashscope import DashScopeEngine
|
|
27
|
-
|
|
28
|
-
assert isinstance(engine, DashScopeEngine)
|
|
29
|
-
assert engine._model == "qwen-vl-ocr"
|
|
30
|
-
|
|
31
22
|
def test_unknown_provider(self) -> None:
|
|
32
23
|
with pytest.raises(ValueError, match="未知的 OCR 提供商"):
|
|
33
24
|
get_engine(provider="nonexistent", model="m", api_key="k")
|
|
@@ -233,9 +233,9 @@ wheels = [
|
|
|
233
233
|
]
|
|
234
234
|
|
|
235
235
|
[[package]]
|
|
236
|
-
name = "multi-ocr"
|
|
236
|
+
name = "multi-ocr-py"
|
|
237
237
|
version = "0.1.0"
|
|
238
|
-
source = {
|
|
238
|
+
source = { editable = "." }
|
|
239
239
|
dependencies = [
|
|
240
240
|
{ name = "liteparse" },
|
|
241
241
|
{ name = "ollama" },
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import re
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
from openai import OpenAI
|
|
6
|
-
|
|
7
|
-
from multi_ocr.engines.base import OCREngine
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class DashScopeEngine(OCREngine):
|
|
11
|
-
"""DashScope (阿里云百炼) OCR 引擎,兼容 OpenAI SDK。
|
|
12
|
-
|
|
13
|
-
支持 Qwen-VL-OCR 模型,通过 OpenAI 兼容接口调用。
|
|
14
|
-
参考文档:https://help.aliyun.com/zh/model-studio/qwen-ocr
|
|
15
|
-
|
|
16
|
-
默认为通用 OCR 场景,如需使用内置任务(高精识别、表格解析、信息抽取等),
|
|
17
|
-
可参照文档在 prompt 中传入对应指令。
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
21
|
-
|
|
22
|
-
# 模型名称 -> 提示词映射(按前缀匹配)
|
|
23
|
-
_PROMPT_MAP: dict[str, str] = {
|
|
24
|
-
"qwen-vl-ocr": "OCR this image and output in markdown format.",
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
_FALLBACK_PROMPT = "OCR this image and output in markdown format."
|
|
28
|
-
|
|
29
|
-
def __init__(self, model: str, api_key: str, base_url: str | None = None) -> None:
|
|
30
|
-
self._model = model
|
|
31
|
-
self._client = OpenAI(
|
|
32
|
-
api_key=api_key,
|
|
33
|
-
base_url=base_url or self.BASE_URL,
|
|
34
|
-
timeout=120.0,
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
def _get_prompt(self) -> str:
|
|
38
|
-
"""根据模型名称匹配对应的提示词,未匹配则使用默认提示词。"""
|
|
39
|
-
for prefix, prompt in self._PROMPT_MAP.items():
|
|
40
|
-
if prefix in self._model:
|
|
41
|
-
return prompt
|
|
42
|
-
return self._FALLBACK_PROMPT
|
|
43
|
-
|
|
44
|
-
def parse_image(self, image_path: Path) -> str:
|
|
45
|
-
with open(image_path, "rb") as f:
|
|
46
|
-
image_data = base64.b64encode(f.read()).decode("utf-8")
|
|
47
|
-
|
|
48
|
-
# 根据扩展名确定 MIME 类型
|
|
49
|
-
suffix = image_path.suffix.lower()
|
|
50
|
-
mime_map = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg"}
|
|
51
|
-
mime_type = mime_map.get(suffix, "image/png")
|
|
52
|
-
|
|
53
|
-
data_url = f"data:{mime_type};base64,{image_data}"
|
|
54
|
-
|
|
55
|
-
prompt = self._get_prompt()
|
|
56
|
-
|
|
57
|
-
response = self._client.chat.completions.create(
|
|
58
|
-
model=self._model,
|
|
59
|
-
messages=[
|
|
60
|
-
{
|
|
61
|
-
"role": "user",
|
|
62
|
-
"content": [
|
|
63
|
-
{
|
|
64
|
-
"type": "image_url",
|
|
65
|
-
"image_url": {"url": data_url},
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
"type": "text",
|
|
69
|
-
"text": prompt,
|
|
70
|
-
},
|
|
71
|
-
],
|
|
72
|
-
}
|
|
73
|
-
],
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
content = response.choices[0].message.content
|
|
77
|
-
if not content:
|
|
78
|
-
return ""
|
|
79
|
-
# 清理模型可能输出的特殊标记
|
|
80
|
-
content = re.sub(r"<\|/?ref\|>", "", content)
|
|
81
|
-
content = re.sub(r"<\|/?det\|>", "", content)
|
|
82
|
-
# 剥离 markdown 代码块包裹(如 ```markdown ... ```)
|
|
83
|
-
content = re.sub(r"^```(?:markdown)?\s*\n", "", content, count=1)
|
|
84
|
-
content = re.sub(r"\n```\s*$", "", content)
|
|
85
|
-
return content.strip()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-15-multi-ocr-design.md
RENAMED
|
File without changes
|
{multi_ocr_py-0.1.0 → multi_ocr_py-0.2.2}/docs/superpowers/specs/2026-07-15-ollama-engine-design.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|