sdpy-kit 0.1.14__tar.gz → 0.2.0__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.
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/PKG-INFO +67 -4
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/README.md +63 -3
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/__init__.py +1 -1
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/config/kit_config.py +21 -0
- sdpy_kit-0.2.0/kit/marker_kit.py +548 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/pyproject.toml +3 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/.gitignore +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/__init__.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/base_service.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/chat_service.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/embedding_service.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/exceptions.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/failover.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/image_service.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/model_builder.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/model_client.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/pool.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/profiles.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/rerank_service.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/runner.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/types.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/anydoc_kit.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/common_kit.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/config/__init__.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/config/log_config.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/__init__.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/base_db_kit.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/crud.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/engine.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/exceptions.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/models/__init__.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/models/base.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/mysql_kit.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/pg_kit.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/query.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/session.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/sqlite_kit.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/transaction.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/docx_kit.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/file_kit.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/mcp_kit.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/md2doc_kit.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/mineru_kit.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/redis_kit.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/wx_article_kit.py +0 -0
- {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/zvec_kit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sdpy-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: SDPY Kit——统一数据库访问层(PG/MySQL/SQLite)与 AI/MCP/Redis/文件/文档/向量工具集
|
|
5
5
|
Author-email: Clark <changhongyuan@126.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -30,6 +30,7 @@ Requires-Dist: fastmcp>=4.0.5; extra == 'all'
|
|
|
30
30
|
Requires-Dist: firecrawl-anydoc>=0.2.4; extra == 'all'
|
|
31
31
|
Requires-Dist: httpx>=0.28.1; extra == 'all'
|
|
32
32
|
Requires-Dist: markdown-it-py>=4.2.0; extra == 'all'
|
|
33
|
+
Requires-Dist: marker-pdf[full]>=2.0.0; extra == 'all'
|
|
33
34
|
Requires-Dist: numpy>=2.5.3; extra == 'all'
|
|
34
35
|
Requires-Dist: pikepdf>=10.13.0; extra == 'all'
|
|
35
36
|
Requires-Dist: pimd[pdfa]>=2.2.5; extra == 'all'
|
|
@@ -45,6 +46,8 @@ Requires-Dist: firecrawl-anydoc>=0.2.4; extra == 'anydoc'
|
|
|
45
46
|
Requires-Dist: pikepdf>=10.13.0; extra == 'anydoc'
|
|
46
47
|
Provides-Extra: docx
|
|
47
48
|
Requires-Dist: docxtpl>=0.20.2; extra == 'docx'
|
|
49
|
+
Provides-Extra: marker
|
|
50
|
+
Requires-Dist: marker-pdf[full]>=2.0.0; extra == 'marker'
|
|
48
51
|
Provides-Extra: mcp
|
|
49
52
|
Requires-Dist: fastmcp>=4.0.5; extra == 'mcp'
|
|
50
53
|
Provides-Extra: md2doc
|
|
@@ -72,7 +75,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
|
|
|
72
75
|
- **统一数据库访问层**:PostgreSQL / MySQL / SQLite,三个 Kit API 完全一致,切换数据库只改一行 import;
|
|
73
76
|
- **AI 多模型服务池**:chat / embedding / rerank / image 四类能力,多厂商候选链 + 故障自动转移;
|
|
74
77
|
- **MCP 客户端**:streamable-http / sse 双传输,档案驱动多服务器;
|
|
75
|
-
- **工具集**:Redis / 文件 / 雪花 ID / 文档互转 / Word 模板填充 / MinerU 解析 / 向量库 / 微信公众号文章抓取存储。
|
|
78
|
+
- **工具集**:Redis / 文件 / 雪花 ID / 文档互转 / Word 模板填充 / MinerU 解析 / Marker 本地解析(md + images) / 向量库 / 微信公众号文章抓取存储。
|
|
76
79
|
|
|
77
80
|
> 要求 Python 3.13+
|
|
78
81
|
|
|
@@ -91,6 +94,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
|
|
|
91
94
|
| `kit.docx_kit` | `DocxKit` | Word 模板动态填充(章节 / 列表 / 表格) | `docx` |
|
|
92
95
|
| `kit.md2doc_kit` | `Md2DocKit` | Markdown → Word / PDF / HTML | `md2doc` |
|
|
93
96
|
| `kit.mineru_kit` | `MinerUKit` | MinerU 文档解析(精准 + Agent 轻量) | `mineru` |
|
|
97
|
+
| `kit.marker_kit` | `MarkerKit` | 本地文档解析(PDF / 图片 / Office → Markdown + Images) | `marker` |
|
|
94
98
|
| `kit.zvec_kit` | `ZvecKit` | zvec 本地向量库 | `zvec` |
|
|
95
99
|
| `kit.wx_article_kit` | `WxArticleKit` | 微信公众号文章 → Markdown + 图片转存 | 无 |
|
|
96
100
|
|
|
@@ -117,6 +121,7 @@ uv add sdpy-kit
|
|
|
117
121
|
| `anydoc` | AnyDocKit 文档转 Markdown(firecrawl-anydoc + pikepdf) | `pip install "sdpy-kit[anydoc]"` |
|
|
118
122
|
| `docx` | DocxKit 模板动态填充(docxtpl) | `pip install "sdpy-kit[docx]"` |
|
|
119
123
|
| `mineru` | MinerUKit 解析 API(精准解析 + Agent 轻量解析) | `pip install "sdpy-kit[mineru]"` |
|
|
124
|
+
| `marker` | MarkerKit 本地文档解析(marker-pdf[full] → Markdown + Images,依赖体积大) | `pip install "sdpy-kit[marker]"` |
|
|
120
125
|
| `all` | 全部能力 | `pip install "sdpy-kit[all]"` |
|
|
121
126
|
|
|
122
127
|
## 配置(.env)
|
|
@@ -161,6 +166,12 @@ SQLITE_DB_PATH=./data/sqlite.db
|
|
|
161
166
|
|
|
162
167
|
# MinerU(仅精准解析需要 Key,Agent 轻量解析免登录)
|
|
163
168
|
# MINERU_API_KEY=
|
|
169
|
+
|
|
170
|
+
# Marker(本地解析,可选依赖 sdpy-kit[marker])
|
|
171
|
+
# MARKER_MODE=fast # None=按设备自动(GPU→balanced / CPU、MPS→fast)
|
|
172
|
+
# MARKER_HF_ENDPOINT=https://hf-mirror.com
|
|
173
|
+
# MARKER_HF_DISABLE_XET=true
|
|
174
|
+
# MARKER_LLAMA_CPP_BINARY=
|
|
164
175
|
```
|
|
165
176
|
|
|
166
177
|
### 环境变量 ↔ 能力对照表
|
|
@@ -247,6 +258,17 @@ SQLITE_DB_PATH=./data/sqlite.db
|
|
|
247
258
|
| `MINERU_POLL_INTERVAL` | `2.0`(≥0.5) | 任务轮询间隔 |
|
|
248
259
|
| `MINERU_MAX_WAIT` | `600`(≥10) | 轮询总超时,超时抛 `MinerUError` |
|
|
249
260
|
|
|
261
|
+
**Marker** → `kit.marker_kit`
|
|
262
|
+
|
|
263
|
+
| 环境变量 | 默认值 | 作用 |
|
|
264
|
+
|----------|--------|------|
|
|
265
|
+
| `MARKER_MODE` | 无(不写入 config) | 解析模式 `balanced` / `fast`;不配置则由 marker 按设备自动选择(GPU→balanced,CPU、MPS→fast) |
|
|
266
|
+
| `MARKER_HF_ENDPOINT` | `https://hf-mirror.com` | 模型权重下载入口(国内镜像);置空则用 HuggingFace 官方源 |
|
|
267
|
+
| `MARKER_HF_DISABLE_XET` | `true` | 禁用 HF Xet 存储后端(国内镜像不支持,开启会导致权重下载挂起) |
|
|
268
|
+
| `MARKER_LLAMA_CPP_BINARY` | 无 | `llama-server` 路径(非 NVIDIA 平台完整 VLM 能力);未设置时沿用系统 `LLAMA_CPP_BINARY` |
|
|
269
|
+
|
|
270
|
+
> 这四项由 kit 在**导入 marker 之前**经 `os.environ.setdefault` 应用:仅当用户未显式设置对应环境变量时生效,不覆盖显式配置。
|
|
271
|
+
|
|
250
272
|
**云 OCR**(任何 `ocr="hosted"` 的转换)→ `kit.anydoc_kit`
|
|
251
273
|
|
|
252
274
|
| 环境变量 | 默认值 | 作用 |
|
|
@@ -261,7 +283,7 @@ SQLITE_DB_PATH=./data/sqlite.db
|
|
|
261
283
|
| ① Settings 变量 | 固定,kit 定义 | 上表中除 `KIT_AI_CHAIN_TIMEOUT` 与密钥类之外的全部 | pydantic-settings 读 `.env` 与真实环境变量(后者优先),带类型与范围校验,缺失用默认值 |
|
|
262
284
|
| ② 直读变量 | 固定,kit 代码直接读 `os.environ` | `KIT_AI_CHAIN_TIMEOUT` | 无类型校验,非法值告警后回退 |
|
|
263
285
|
| ③ 档案声明变量 | **动态**,名字写在 JSON 里 | AI 的 `api_key`、MCP 的 `token_env` | **名字进 JSON、值进 `.env`**——所以档案文件可以安全入库,密钥留在环境里 |
|
|
264
|
-
| ④ 第三方库变量 | 由依赖库定义 | `FIRECRAWL_API_KEY`、`FIRECRAWL_API_URL` | kit 只透传 `None
|
|
286
|
+
| ④ 第三方库变量 | 由依赖库定义 | `FIRECRAWL_API_KEY`、`FIRECRAWL_API_URL`(anydoc);`HF_ENDPOINT` / `HF_HUB_DISABLE_XET` / `LLAMA_CPP_BINARY`(marker) | anydoc:kit 只透传 `None`,由其自行回落;marker:kit 在导入前按 `MARKER_*` 配置做 `os.environ.setdefault`(不覆盖显式设置) |
|
|
265
287
|
|
|
266
288
|
### 密钥解析优先级
|
|
267
289
|
|
|
@@ -277,7 +299,8 @@ SQLITE_DB_PATH=./data/sqlite.db
|
|
|
277
299
|
|
|
278
300
|
| 改动内容 | 生效时机 |
|
|
279
301
|
|----------|----------|
|
|
280
|
-
| `LOG_*`、`DB_*`、`MYSQL_*`、`REDIS_*`、`FILE_STORAGE_ROOT`、`ZVEC_*`、`MINERU_
|
|
302
|
+
| `LOG_*`、`DB_*`、`MYSQL_*`、`REDIS_*`、`FILE_STORAGE_ROOT`、`ZVEC_*`、`MINERU_*`、`MARKER_MODE` | `get_settings()` 带 `lru_cache`,进程内首次读取后固定 → **重启进程**(或调用 `AI.reload_config()`,它会顺带清空该缓存) |
|
|
303
|
+
| `MARKER_HF_ENDPOINT` / `MARKER_HF_DISABLE_XET` / `MARKER_LLAMA_CPP_BINARY` | 仅在 `import kit.marker_kit` 时经 `os.environ.setdefault` 应用 → 改动需**重启进程**重新导入模块 |
|
|
281
304
|
| AI 密钥写在 `.env` 里 | `.env` 只在加载档案时注入一次 → 重启,或 `AI.reload_config(reload_file=True)` |
|
|
282
305
|
| AI 密钥由自己写入 `os.environ` | `AI.reload_config()`(默认 `reload_file=False`,只重读 `os.environ`) |
|
|
283
306
|
| `ai_models.json`(增删档案、换 model) | `AI.reload_config(reload_file=True)` |
|
|
@@ -810,6 +833,46 @@ if r.state == "done":
|
|
|
810
833
|
|
|
811
834
|
---
|
|
812
835
|
|
|
836
|
+
## kit.marker_kit —— 本地文档解析(Markdown + Images)
|
|
837
|
+
|
|
838
|
+
安装:`pip install "sdpy-kit[marker]"`(依赖 `marker-pdf[full]`,含 torch 等 80+ 包,体积较大)
|
|
839
|
+
|
|
840
|
+
```python
|
|
841
|
+
from kit.marker_kit import MarkerKit
|
|
842
|
+
|
|
843
|
+
# 落盘 md + images(推荐):源文件同目录下建同名子目录
|
|
844
|
+
md_path = MarkerKit.convert_to_dir("report.pdf") # -> report/report.md + 图片
|
|
845
|
+
md_path = MarkerKit.convert_to_dir("report.pdf", "out/doc") # 指定产物目录
|
|
846
|
+
|
|
847
|
+
# 只取 Markdown 文本(图片不落盘)
|
|
848
|
+
md = MarkerKit.convert_to_md("report.pdf")
|
|
849
|
+
|
|
850
|
+
# 安全转换(不抛异常,返回结果对象)
|
|
851
|
+
result = MarkerKit.convert("report.pdf", "out/doc")
|
|
852
|
+
if result.success:
|
|
853
|
+
print(result.md_path, len(result.image_paths), result.page_count)
|
|
854
|
+
|
|
855
|
+
# 跳过 VLM(无需 llama.cpp)
|
|
856
|
+
md = MarkerKit.convert_to_md("scan.pdf", disable_ocr=True)
|
|
857
|
+
|
|
858
|
+
MarkerKit.shutdown() # 释放模型与推理服务(幂等)
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
| 方法 | 说明 |
|
|
862
|
+
|------|------|
|
|
863
|
+
| `is_supported(file_path) -> bool` | 扩展名白名单(pdf / 图片 / docx / pptx / xlsx / html / epub) |
|
|
864
|
+
| `convert_to_md(file_path, *, mode=None, force_ocr=None, disable_ocr=None, page_range=None) -> str` | 解析并返回 Markdown 文本(图片不落盘) |
|
|
865
|
+
| `convert_to_dir(src_path, output_dir=None, *, mode=None, force_ocr=None, disable_ocr=None, page_range=None, extract_images=True) -> str` | 解析并落盘 md + images,返回 md 绝对路径 |
|
|
866
|
+
| `convert(src_path, output_dir=None, *, ...) -> MarkerResult` | 安全版:失败封装为 `MarkerResult`,不抛异常 |
|
|
867
|
+
| `shutdown() -> None` | 释放模型与共享推理服务(幂等) |
|
|
868
|
+
|
|
869
|
+
> 产物:`<output_dir>/<源文件名>.md` + 图片文件(md 内相对路径引用),**不生成** `_meta.json`。
|
|
870
|
+
> 结果类型:`MarkerResult`(`success` / `markdown` / `file_path` / `output_dir` / `md_path` / `image_paths` / `page_count` / `error`);异常:`MarkerError`。
|
|
871
|
+
> `mode` 缺省时不写入配置,交由 marker 按设备自动选择;首次使用会下载模型权重(默认走 `MARKER_HF_ENDPOINT` 镜像)。
|
|
872
|
+
> 非 NVIDIA 平台完整 VLM 能力(公式 / 扫描页 OCR)需 llama.cpp 的 `llama-server`,否则用 `disable_ocr=True` 跳过。
|
|
873
|
+
|
|
874
|
+
---
|
|
875
|
+
|
|
813
876
|
## kit.zvec_kit —— 本地向量库
|
|
814
877
|
|
|
815
878
|
安装:`pip install "sdpy-kit[zvec]"`
|
|
@@ -5,7 +5,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
|
|
|
5
5
|
- **统一数据库访问层**:PostgreSQL / MySQL / SQLite,三个 Kit API 完全一致,切换数据库只改一行 import;
|
|
6
6
|
- **AI 多模型服务池**:chat / embedding / rerank / image 四类能力,多厂商候选链 + 故障自动转移;
|
|
7
7
|
- **MCP 客户端**:streamable-http / sse 双传输,档案驱动多服务器;
|
|
8
|
-
- **工具集**:Redis / 文件 / 雪花 ID / 文档互转 / Word 模板填充 / MinerU 解析 / 向量库 / 微信公众号文章抓取存储。
|
|
8
|
+
- **工具集**:Redis / 文件 / 雪花 ID / 文档互转 / Word 模板填充 / MinerU 解析 / Marker 本地解析(md + images) / 向量库 / 微信公众号文章抓取存储。
|
|
9
9
|
|
|
10
10
|
> 要求 Python 3.13+
|
|
11
11
|
|
|
@@ -24,6 +24,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
|
|
|
24
24
|
| `kit.docx_kit` | `DocxKit` | Word 模板动态填充(章节 / 列表 / 表格) | `docx` |
|
|
25
25
|
| `kit.md2doc_kit` | `Md2DocKit` | Markdown → Word / PDF / HTML | `md2doc` |
|
|
26
26
|
| `kit.mineru_kit` | `MinerUKit` | MinerU 文档解析(精准 + Agent 轻量) | `mineru` |
|
|
27
|
+
| `kit.marker_kit` | `MarkerKit` | 本地文档解析(PDF / 图片 / Office → Markdown + Images) | `marker` |
|
|
27
28
|
| `kit.zvec_kit` | `ZvecKit` | zvec 本地向量库 | `zvec` |
|
|
28
29
|
| `kit.wx_article_kit` | `WxArticleKit` | 微信公众号文章 → Markdown + 图片转存 | 无 |
|
|
29
30
|
|
|
@@ -50,6 +51,7 @@ uv add sdpy-kit
|
|
|
50
51
|
| `anydoc` | AnyDocKit 文档转 Markdown(firecrawl-anydoc + pikepdf) | `pip install "sdpy-kit[anydoc]"` |
|
|
51
52
|
| `docx` | DocxKit 模板动态填充(docxtpl) | `pip install "sdpy-kit[docx]"` |
|
|
52
53
|
| `mineru` | MinerUKit 解析 API(精准解析 + Agent 轻量解析) | `pip install "sdpy-kit[mineru]"` |
|
|
54
|
+
| `marker` | MarkerKit 本地文档解析(marker-pdf[full] → Markdown + Images,依赖体积大) | `pip install "sdpy-kit[marker]"` |
|
|
53
55
|
| `all` | 全部能力 | `pip install "sdpy-kit[all]"` |
|
|
54
56
|
|
|
55
57
|
## 配置(.env)
|
|
@@ -94,6 +96,12 @@ SQLITE_DB_PATH=./data/sqlite.db
|
|
|
94
96
|
|
|
95
97
|
# MinerU(仅精准解析需要 Key,Agent 轻量解析免登录)
|
|
96
98
|
# MINERU_API_KEY=
|
|
99
|
+
|
|
100
|
+
# Marker(本地解析,可选依赖 sdpy-kit[marker])
|
|
101
|
+
# MARKER_MODE=fast # None=按设备自动(GPU→balanced / CPU、MPS→fast)
|
|
102
|
+
# MARKER_HF_ENDPOINT=https://hf-mirror.com
|
|
103
|
+
# MARKER_HF_DISABLE_XET=true
|
|
104
|
+
# MARKER_LLAMA_CPP_BINARY=
|
|
97
105
|
```
|
|
98
106
|
|
|
99
107
|
### 环境变量 ↔ 能力对照表
|
|
@@ -180,6 +188,17 @@ SQLITE_DB_PATH=./data/sqlite.db
|
|
|
180
188
|
| `MINERU_POLL_INTERVAL` | `2.0`(≥0.5) | 任务轮询间隔 |
|
|
181
189
|
| `MINERU_MAX_WAIT` | `600`(≥10) | 轮询总超时,超时抛 `MinerUError` |
|
|
182
190
|
|
|
191
|
+
**Marker** → `kit.marker_kit`
|
|
192
|
+
|
|
193
|
+
| 环境变量 | 默认值 | 作用 |
|
|
194
|
+
|----------|--------|------|
|
|
195
|
+
| `MARKER_MODE` | 无(不写入 config) | 解析模式 `balanced` / `fast`;不配置则由 marker 按设备自动选择(GPU→balanced,CPU、MPS→fast) |
|
|
196
|
+
| `MARKER_HF_ENDPOINT` | `https://hf-mirror.com` | 模型权重下载入口(国内镜像);置空则用 HuggingFace 官方源 |
|
|
197
|
+
| `MARKER_HF_DISABLE_XET` | `true` | 禁用 HF Xet 存储后端(国内镜像不支持,开启会导致权重下载挂起) |
|
|
198
|
+
| `MARKER_LLAMA_CPP_BINARY` | 无 | `llama-server` 路径(非 NVIDIA 平台完整 VLM 能力);未设置时沿用系统 `LLAMA_CPP_BINARY` |
|
|
199
|
+
|
|
200
|
+
> 这四项由 kit 在**导入 marker 之前**经 `os.environ.setdefault` 应用:仅当用户未显式设置对应环境变量时生效,不覆盖显式配置。
|
|
201
|
+
|
|
183
202
|
**云 OCR**(任何 `ocr="hosted"` 的转换)→ `kit.anydoc_kit`
|
|
184
203
|
|
|
185
204
|
| 环境变量 | 默认值 | 作用 |
|
|
@@ -194,7 +213,7 @@ SQLITE_DB_PATH=./data/sqlite.db
|
|
|
194
213
|
| ① Settings 变量 | 固定,kit 定义 | 上表中除 `KIT_AI_CHAIN_TIMEOUT` 与密钥类之外的全部 | pydantic-settings 读 `.env` 与真实环境变量(后者优先),带类型与范围校验,缺失用默认值 |
|
|
195
214
|
| ② 直读变量 | 固定,kit 代码直接读 `os.environ` | `KIT_AI_CHAIN_TIMEOUT` | 无类型校验,非法值告警后回退 |
|
|
196
215
|
| ③ 档案声明变量 | **动态**,名字写在 JSON 里 | AI 的 `api_key`、MCP 的 `token_env` | **名字进 JSON、值进 `.env`**——所以档案文件可以安全入库,密钥留在环境里 |
|
|
197
|
-
| ④ 第三方库变量 | 由依赖库定义 | `FIRECRAWL_API_KEY`、`FIRECRAWL_API_URL` | kit 只透传 `None
|
|
216
|
+
| ④ 第三方库变量 | 由依赖库定义 | `FIRECRAWL_API_KEY`、`FIRECRAWL_API_URL`(anydoc);`HF_ENDPOINT` / `HF_HUB_DISABLE_XET` / `LLAMA_CPP_BINARY`(marker) | anydoc:kit 只透传 `None`,由其自行回落;marker:kit 在导入前按 `MARKER_*` 配置做 `os.environ.setdefault`(不覆盖显式设置) |
|
|
198
217
|
|
|
199
218
|
### 密钥解析优先级
|
|
200
219
|
|
|
@@ -210,7 +229,8 @@ SQLITE_DB_PATH=./data/sqlite.db
|
|
|
210
229
|
|
|
211
230
|
| 改动内容 | 生效时机 |
|
|
212
231
|
|----------|----------|
|
|
213
|
-
| `LOG_*`、`DB_*`、`MYSQL_*`、`REDIS_*`、`FILE_STORAGE_ROOT`、`ZVEC_*`、`MINERU_
|
|
232
|
+
| `LOG_*`、`DB_*`、`MYSQL_*`、`REDIS_*`、`FILE_STORAGE_ROOT`、`ZVEC_*`、`MINERU_*`、`MARKER_MODE` | `get_settings()` 带 `lru_cache`,进程内首次读取后固定 → **重启进程**(或调用 `AI.reload_config()`,它会顺带清空该缓存) |
|
|
233
|
+
| `MARKER_HF_ENDPOINT` / `MARKER_HF_DISABLE_XET` / `MARKER_LLAMA_CPP_BINARY` | 仅在 `import kit.marker_kit` 时经 `os.environ.setdefault` 应用 → 改动需**重启进程**重新导入模块 |
|
|
214
234
|
| AI 密钥写在 `.env` 里 | `.env` 只在加载档案时注入一次 → 重启,或 `AI.reload_config(reload_file=True)` |
|
|
215
235
|
| AI 密钥由自己写入 `os.environ` | `AI.reload_config()`(默认 `reload_file=False`,只重读 `os.environ`) |
|
|
216
236
|
| `ai_models.json`(增删档案、换 model) | `AI.reload_config(reload_file=True)` |
|
|
@@ -743,6 +763,46 @@ if r.state == "done":
|
|
|
743
763
|
|
|
744
764
|
---
|
|
745
765
|
|
|
766
|
+
## kit.marker_kit —— 本地文档解析(Markdown + Images)
|
|
767
|
+
|
|
768
|
+
安装:`pip install "sdpy-kit[marker]"`(依赖 `marker-pdf[full]`,含 torch 等 80+ 包,体积较大)
|
|
769
|
+
|
|
770
|
+
```python
|
|
771
|
+
from kit.marker_kit import MarkerKit
|
|
772
|
+
|
|
773
|
+
# 落盘 md + images(推荐):源文件同目录下建同名子目录
|
|
774
|
+
md_path = MarkerKit.convert_to_dir("report.pdf") # -> report/report.md + 图片
|
|
775
|
+
md_path = MarkerKit.convert_to_dir("report.pdf", "out/doc") # 指定产物目录
|
|
776
|
+
|
|
777
|
+
# 只取 Markdown 文本(图片不落盘)
|
|
778
|
+
md = MarkerKit.convert_to_md("report.pdf")
|
|
779
|
+
|
|
780
|
+
# 安全转换(不抛异常,返回结果对象)
|
|
781
|
+
result = MarkerKit.convert("report.pdf", "out/doc")
|
|
782
|
+
if result.success:
|
|
783
|
+
print(result.md_path, len(result.image_paths), result.page_count)
|
|
784
|
+
|
|
785
|
+
# 跳过 VLM(无需 llama.cpp)
|
|
786
|
+
md = MarkerKit.convert_to_md("scan.pdf", disable_ocr=True)
|
|
787
|
+
|
|
788
|
+
MarkerKit.shutdown() # 释放模型与推理服务(幂等)
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
| 方法 | 说明 |
|
|
792
|
+
|------|------|
|
|
793
|
+
| `is_supported(file_path) -> bool` | 扩展名白名单(pdf / 图片 / docx / pptx / xlsx / html / epub) |
|
|
794
|
+
| `convert_to_md(file_path, *, mode=None, force_ocr=None, disable_ocr=None, page_range=None) -> str` | 解析并返回 Markdown 文本(图片不落盘) |
|
|
795
|
+
| `convert_to_dir(src_path, output_dir=None, *, mode=None, force_ocr=None, disable_ocr=None, page_range=None, extract_images=True) -> str` | 解析并落盘 md + images,返回 md 绝对路径 |
|
|
796
|
+
| `convert(src_path, output_dir=None, *, ...) -> MarkerResult` | 安全版:失败封装为 `MarkerResult`,不抛异常 |
|
|
797
|
+
| `shutdown() -> None` | 释放模型与共享推理服务(幂等) |
|
|
798
|
+
|
|
799
|
+
> 产物:`<output_dir>/<源文件名>.md` + 图片文件(md 内相对路径引用),**不生成** `_meta.json`。
|
|
800
|
+
> 结果类型:`MarkerResult`(`success` / `markdown` / `file_path` / `output_dir` / `md_path` / `image_paths` / `page_count` / `error`);异常:`MarkerError`。
|
|
801
|
+
> `mode` 缺省时不写入配置,交由 marker 按设备自动选择;首次使用会下载模型权重(默认走 `MARKER_HF_ENDPOINT` 镜像)。
|
|
802
|
+
> 非 NVIDIA 平台完整 VLM 能力(公式 / 扫描页 OCR)需 llama.cpp 的 `llama-server`,否则用 `disable_ocr=True` 跳过。
|
|
803
|
+
|
|
804
|
+
---
|
|
805
|
+
|
|
746
806
|
## kit.zvec_kit —— 本地向量库
|
|
747
807
|
|
|
748
808
|
安装:`pip install "sdpy-kit[zvec]"`
|
|
@@ -121,6 +121,27 @@ class Settings(BaseSettings):
|
|
|
121
121
|
default=600, ge=10, description="任务轮询总超时(秒),超时抛 MinerUError"
|
|
122
122
|
)
|
|
123
123
|
|
|
124
|
+
# Marker 本地文档解析配置(marker-pdf,PDF→Markdown+Images,模型本地运行)
|
|
125
|
+
MARKER_MODE: str | None = Field(
|
|
126
|
+
default=None,
|
|
127
|
+
description=(
|
|
128
|
+
"解析模式:None=按设备自动(GPU→balanced,CPU/MPS→fast),"
|
|
129
|
+
"可设 balanced/fast"
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
MARKER_HF_ENDPOINT: str = Field(
|
|
133
|
+
default="https://hf-mirror.com",
|
|
134
|
+
description="模型权重下载入口(默认国内镜像),置空则使用 HuggingFace 官方源",
|
|
135
|
+
)
|
|
136
|
+
MARKER_HF_DISABLE_XET: bool = Field(
|
|
137
|
+
default=True,
|
|
138
|
+
description="禁用 HF Xet 存储后端(国内镜像不支持,开启会导致权重下载挂起)",
|
|
139
|
+
)
|
|
140
|
+
MARKER_LLAMA_CPP_BINARY: str | None = Field(
|
|
141
|
+
default=None,
|
|
142
|
+
description="llama-server 可执行文件路径(None 时沿用系统 LLAMA_CPP_BINARY)",
|
|
143
|
+
)
|
|
144
|
+
|
|
124
145
|
class Config:
|
|
125
146
|
env_file = ".env"
|
|
126
147
|
env_file_encoding = "utf-8"
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Project: sdpy
|
|
17
|
+
Author: Clark Chang
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# date: 2026-10-09
|
|
21
|
+
"""Marker 本地文档解析工具类,将 PDF 等文档转为 Markdown + Images
|
|
22
|
+
|
|
23
|
+
本模块基于 marker-pdf(Marker v2.x,https://github.com/datalab-to/marker),
|
|
24
|
+
在本地解析 PDF / 图片 / Office / HTML / EPUB,输出 GitHub-Flavored Markdown
|
|
25
|
+
并提取图片,产物形态对齐 kit.mineru_kit 的「md + images」。
|
|
26
|
+
|
|
27
|
+
与 kit.anydoc_kit 的分工:
|
|
28
|
+
- AnyDocKit:轻量文本转换(firecrawl-anydoc),只产出 Markdown 文本;
|
|
29
|
+
- MarkerKit:本地模型解析,产出 md + 图片,质量更高但依赖重(torch 等)。
|
|
30
|
+
|
|
31
|
+
设计要点:
|
|
32
|
+
- 按需加载:依赖 marker-pdf,缺失时导入报错并提示
|
|
33
|
+
`pip install sdpy-kit[marker]`(可选依赖,不进入基础安装)。
|
|
34
|
+
- 模型进程内单例:create_model_dict 开销极大(首次还要下载权重),
|
|
35
|
+
由类级缓存 + 线程锁保护;转换过程串行化,避免并发不稳定。
|
|
36
|
+
- 导入 marker 之前先应用 HuggingFace 环境(镜像入口、禁用 Xet),
|
|
37
|
+
否则国内网络下模型权重下载会超时或挂起。
|
|
38
|
+
- 低层 convert_to_md / convert_to_dir 直接抛出异常(便于精确定位);
|
|
39
|
+
高层 convert 返回 MarkerResult 结果对象,便于批量处理。
|
|
40
|
+
- 产物只落盘 md + images,不写 marker 默认的 _meta.json。
|
|
41
|
+
- 日志消息不使用 emoji:Windows 默认 GBK 控制台无法编码 emoji,
|
|
42
|
+
会使 logging 抛出 UnicodeEncodeError。
|
|
43
|
+
|
|
44
|
+
使用前准备(非 NVIDIA 平台):
|
|
45
|
+
- 首次使用会自动下载模型权重(默认走 MARKER_HF_ENDPOINT 镜像);
|
|
46
|
+
- 完整 VLM 能力(公式/扫描页 OCR)需 llama.cpp 的 llama-server,
|
|
47
|
+
可通过 MARKER_LLAMA_CPP_BINARY 指定;否则用 disable_ocr=True 跳过 VLM。
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
import os
|
|
51
|
+
import threading
|
|
52
|
+
from dataclasses import dataclass
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
from typing import Literal
|
|
55
|
+
|
|
56
|
+
from kit.config.kit_config import get_settings
|
|
57
|
+
from kit.config.log_config import get_default_logger
|
|
58
|
+
|
|
59
|
+
logger = get_default_logger(__name__)
|
|
60
|
+
|
|
61
|
+
_settings = get_settings()
|
|
62
|
+
|
|
63
|
+
# HuggingFace 相关环境必须在导入 marker / huggingface_hub 之前设置,否则不生效。
|
|
64
|
+
# 统一使用 setdefault:仅当用户未显式设置环境变量时生效,不覆盖显式配置。
|
|
65
|
+
if _settings.MARKER_HF_ENDPOINT:
|
|
66
|
+
os.environ.setdefault("HF_ENDPOINT", _settings.MARKER_HF_ENDPOINT)
|
|
67
|
+
if _settings.MARKER_HF_DISABLE_XET:
|
|
68
|
+
os.environ.setdefault("HF_HUB_DISABLE_XET", "1")
|
|
69
|
+
if _settings.MARKER_LLAMA_CPP_BINARY:
|
|
70
|
+
os.environ.setdefault("LLAMA_CPP_BINARY", _settings.MARKER_LLAMA_CPP_BINARY)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
from marker.converters.pdf import PdfConverter
|
|
74
|
+
from marker.models import create_model_dict, shutdown_models
|
|
75
|
+
from marker.output import text_from_rendered
|
|
76
|
+
except ImportError as e:
|
|
77
|
+
raise ImportError(
|
|
78
|
+
"marker_kit 需要可选依赖,请安装:pip install sdpy-kit[marker]"
|
|
79
|
+
) from e
|
|
80
|
+
|
|
81
|
+
# kit 维护的扩展名白名单(本地快速校验与错误提示)。
|
|
82
|
+
# 注意:这是 marker 实际支持面的常用子集;marker-pdf[full] 另覆盖 epub/html 等,
|
|
83
|
+
# 未列入的扩展名请在确认 marker 支持后按需增补。
|
|
84
|
+
_SUPPORTED_EXTENSIONS: frozenset[str] = frozenset(
|
|
85
|
+
{
|
|
86
|
+
# PDF
|
|
87
|
+
".pdf",
|
|
88
|
+
# 图片
|
|
89
|
+
".png",
|
|
90
|
+
".jpg",
|
|
91
|
+
".jpeg",
|
|
92
|
+
".webp",
|
|
93
|
+
".gif",
|
|
94
|
+
".bmp",
|
|
95
|
+
# Office(marker-pdf[full]:mammoth / python-pptx / openpyxl)
|
|
96
|
+
".docx",
|
|
97
|
+
".pptx",
|
|
98
|
+
".xlsx",
|
|
99
|
+
# 网页 / 电子书(marker-pdf[full]:weasyprint / ebooklib)
|
|
100
|
+
".html",
|
|
101
|
+
".epub",
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# 支持的解析模式(marker 仅接受这两个取值)
|
|
106
|
+
_MODE_CHOICES: frozenset[str] = frozenset({"balanced", "fast"})
|
|
107
|
+
|
|
108
|
+
# 类型别名:解析模式
|
|
109
|
+
_ModeLiteral = Literal["balanced", "fast"]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class MarkerError(Exception):
|
|
113
|
+
"""Marker 解析异常(模型加载失败 / 文档转换失败 / 产物落盘失败)"""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class MarkerResult:
|
|
118
|
+
"""文档解析结果对象
|
|
119
|
+
|
|
120
|
+
由 MarkerKit.convert 返回,将成功与失败信息统一封装,调用方无需 try/except。
|
|
121
|
+
|
|
122
|
+
Attributes:
|
|
123
|
+
success: 解析是否成功
|
|
124
|
+
markdown: 解析出的 Markdown 文本,失败时为空字符串
|
|
125
|
+
file_path: 源文件绝对路径
|
|
126
|
+
output_dir: 产物目录绝对路径;未落盘时为 None
|
|
127
|
+
md_path: 落盘的 Markdown 文件绝对路径;未落盘时为 None
|
|
128
|
+
image_paths: 落盘的图片文件绝对路径元组
|
|
129
|
+
page_count: 文档页数,未知为 None
|
|
130
|
+
error: 失败原因描述(异常类型与信息),成功时为 None
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
success: bool
|
|
134
|
+
markdown: str
|
|
135
|
+
file_path: str
|
|
136
|
+
output_dir: str | None = None
|
|
137
|
+
md_path: str | None = None
|
|
138
|
+
image_paths: tuple[str, ...] = ()
|
|
139
|
+
page_count: int | None = None
|
|
140
|
+
error: str | None = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class MarkerKit:
|
|
144
|
+
"""Marker 本地文档解析工具类,将文档转为 Markdown + Images
|
|
145
|
+
|
|
146
|
+
支持 PDF、图片、Office(docx/pptx/xlsx)、HTML、EPUB,产物形态对齐
|
|
147
|
+
kit.mineru_kit(md + 图片落盘于同一目录,md 以相对路径引用图片)。
|
|
148
|
+
|
|
149
|
+
使用前安装可选依赖::
|
|
150
|
+
|
|
151
|
+
pip install sdpy-kit[marker]
|
|
152
|
+
|
|
153
|
+
使用方式::
|
|
154
|
+
|
|
155
|
+
# 1. 落盘 md + images(推荐,返回 Markdown 文件绝对路径)
|
|
156
|
+
md_path = MarkerKit.convert_to_dir("report.pdf") # 同目录下 report/
|
|
157
|
+
md_path = MarkerKit.convert_to_dir("report.pdf", "out/doc") # 指定产物目录
|
|
158
|
+
|
|
159
|
+
# 2. 只取 Markdown 文本(图片不落盘)
|
|
160
|
+
md = MarkerKit.convert_to_md("report.pdf")
|
|
161
|
+
|
|
162
|
+
# 3. 安全转换(不抛异常,返回结果对象)
|
|
163
|
+
result = MarkerKit.convert("report.pdf", "out/doc")
|
|
164
|
+
if result.success:
|
|
165
|
+
print(result.md_path, result.image_paths)
|
|
166
|
+
|
|
167
|
+
# 4. 纯文本层抽取(跳过 VLM,无需 llama.cpp)
|
|
168
|
+
md = MarkerKit.convert_to_md("scan.pdf", disable_ocr=True)
|
|
169
|
+
|
|
170
|
+
# 5. 进程退出前释放模型
|
|
171
|
+
MarkerKit.shutdown()
|
|
172
|
+
|
|
173
|
+
注意:
|
|
174
|
+
- 默认模式由 MARKER_MODE 决定,未配置时不写 mode,交由 marker 按设备自动
|
|
175
|
+
选择(CUDA→balanced,CPU/MPS→fast);可用 mode 参数显式覆盖。
|
|
176
|
+
- 非 NVIDIA 平台完整 VLM 能力需 llama.cpp 的 llama-server,缺失时可通过
|
|
177
|
+
disable_ocr=True 跳过(公式与扫描页不做 OCR)。
|
|
178
|
+
- 建议串行调用;模型与转换过程由内部锁保护,不做多线程并发转换。
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
SUPPORTED_EXTENSIONS: frozenset[str] = _SUPPORTED_EXTENSIONS
|
|
182
|
+
|
|
183
|
+
# 进程内模型/转换器单例(create_model_dict 开销极大,只建一次)
|
|
184
|
+
_lock: threading.Lock = threading.Lock()
|
|
185
|
+
_converter: PdfConverter | None = None
|
|
186
|
+
_model_dict: dict | None = None
|
|
187
|
+
_config_signature: tuple[str, ...] | None = None
|
|
188
|
+
|
|
189
|
+
# ------------------------------------------------------------------
|
|
190
|
+
# 校验与内部工具
|
|
191
|
+
# ------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def is_supported(file_path: str | Path) -> bool:
|
|
195
|
+
"""检查文件格式是否受支持(依据扩展名)
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
file_path: 文件路径或文件名
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
bool: 扩展名在白名单内返回 True
|
|
202
|
+
|
|
203
|
+
Example:
|
|
204
|
+
>>> MarkerKit.is_supported("report.pdf")
|
|
205
|
+
True
|
|
206
|
+
>>> MarkerKit.is_supported("report.xyz")
|
|
207
|
+
False
|
|
208
|
+
"""
|
|
209
|
+
return Path(file_path).suffix.lower() in MarkerKit.SUPPORTED_EXTENSIONS
|
|
210
|
+
|
|
211
|
+
@staticmethod
|
|
212
|
+
def _validate_path(file_path: str | Path) -> Path:
|
|
213
|
+
"""校验待解析文件(存在 / 是文件 / 扩展名受支持),返回绝对路径
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
FileNotFoundError: 文件不存在
|
|
217
|
+
ValueError: 路径不是常规文件,或扩展名不受支持
|
|
218
|
+
"""
|
|
219
|
+
path = Path(file_path).resolve()
|
|
220
|
+
if not path.exists():
|
|
221
|
+
raise FileNotFoundError(f"文件不存在: {path}")
|
|
222
|
+
if not path.is_file():
|
|
223
|
+
raise ValueError(f"路径不是有效文件: {path}")
|
|
224
|
+
if not MarkerKit.is_supported(path):
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"不支持的文件格式: {path.suffix or '<无扩展名>'},"
|
|
227
|
+
f"支持的格式: {', '.join(sorted(MarkerKit.SUPPORTED_EXTENSIONS))}"
|
|
228
|
+
)
|
|
229
|
+
return path
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def _build_config(
|
|
233
|
+
*,
|
|
234
|
+
mode: _ModeLiteral | None,
|
|
235
|
+
force_ocr: bool | None,
|
|
236
|
+
disable_ocr: bool | None,
|
|
237
|
+
page_range: str | None,
|
|
238
|
+
) -> dict[str, object]:
|
|
239
|
+
"""组装 Marker 解析配置(仅保留显式指定的项)
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
mode: 解析模式;None 时回落到 MARKER_MODE 配置
|
|
243
|
+
force_ocr: 是否强制全页 OCR;None 表示不指定(用 marker 默认)
|
|
244
|
+
disable_ocr: 是否禁用全部 VLM 调用;None 表示不指定
|
|
245
|
+
page_range: 页码范围(marker parse_range_str 口径,如 "0-5,7")
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
dict[str, object]: 传给 PdfConverter 的 config 字典
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
ValueError: force_ocr 与 disable_ocr 同时为真,或 mode 取值非法
|
|
252
|
+
"""
|
|
253
|
+
if force_ocr and disable_ocr:
|
|
254
|
+
raise ValueError("force_ocr 与 disable_ocr 不能同时为真")
|
|
255
|
+
resolved_mode = mode if mode is not None else _settings.MARKER_MODE
|
|
256
|
+
if resolved_mode is not None and resolved_mode not in _MODE_CHOICES:
|
|
257
|
+
raise ValueError(
|
|
258
|
+
f"不支持的解析模式: {resolved_mode},可选 "
|
|
259
|
+
f"{', '.join(sorted(_MODE_CHOICES))}"
|
|
260
|
+
)
|
|
261
|
+
config: dict[str, object] = {}
|
|
262
|
+
if resolved_mode is not None:
|
|
263
|
+
config["mode"] = resolved_mode
|
|
264
|
+
if force_ocr is not None:
|
|
265
|
+
config["force_ocr"] = force_ocr
|
|
266
|
+
if disable_ocr is not None:
|
|
267
|
+
config["disable_ocr"] = disable_ocr
|
|
268
|
+
if page_range is not None:
|
|
269
|
+
config["page_range"] = page_range
|
|
270
|
+
return config
|
|
271
|
+
|
|
272
|
+
@classmethod
|
|
273
|
+
def _ensure_converter(cls, config: dict[str, object]) -> PdfConverter:
|
|
274
|
+
"""获取(必要时构建)进程内共享的 PdfConverter(调用方需持锁)
|
|
275
|
+
|
|
276
|
+
模型字典只加载一次;当 config 变化(如切换 mode)时仅重建转换器,
|
|
277
|
+
复用已加载的模型。
|
|
278
|
+
"""
|
|
279
|
+
if cls._model_dict is None:
|
|
280
|
+
logger.info("首次加载 Marker 模型(首次会下载权重,耗时较长)...")
|
|
281
|
+
cls._model_dict = create_model_dict()
|
|
282
|
+
logger.info("Marker 模型加载完成")
|
|
283
|
+
signature = tuple(f"{k}={config[k]}" for k in sorted(config))
|
|
284
|
+
if cls._converter is None or cls._config_signature != signature:
|
|
285
|
+
cls._converter = PdfConverter(
|
|
286
|
+
artifact_dict=cls._model_dict, config=dict(config)
|
|
287
|
+
)
|
|
288
|
+
cls._config_signature = signature
|
|
289
|
+
return cls._converter
|
|
290
|
+
|
|
291
|
+
@classmethod
|
|
292
|
+
def _render(
|
|
293
|
+
cls, path: Path, config: dict[str, object]
|
|
294
|
+
) -> tuple[str, dict, int | None]:
|
|
295
|
+
"""在锁内执行一次解析,返回 (markdown, images, page_count)
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
path: 已校验的源文件绝对路径
|
|
299
|
+
config: 解析配置
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
tuple[str, dict, int | None]: Markdown 文本、图片字典、页数
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
MarkerError: 模型加载或解析过程失败
|
|
306
|
+
"""
|
|
307
|
+
with cls._lock:
|
|
308
|
+
try:
|
|
309
|
+
converter = cls._ensure_converter(config)
|
|
310
|
+
rendered = converter(str(path))
|
|
311
|
+
text, _ext, images = text_from_rendered(rendered)
|
|
312
|
+
page_count = converter.page_count
|
|
313
|
+
except Exception as e: # noqa: BLE001 - marker 无统一异常基类
|
|
314
|
+
raise MarkerError(f"Marker 解析失败: {type(e).__name__}: {e}") from e
|
|
315
|
+
return text, images, page_count
|
|
316
|
+
|
|
317
|
+
@staticmethod
|
|
318
|
+
def _resolve_out_dir(src: Path, output_dir: str | Path | None) -> Path:
|
|
319
|
+
"""解析产物目录:缺省为源文件同目录下与文件名同名的子目录"""
|
|
320
|
+
if output_dir is None:
|
|
321
|
+
return (src.parent / src.stem).resolve()
|
|
322
|
+
return Path(output_dir).resolve()
|
|
323
|
+
|
|
324
|
+
@staticmethod
|
|
325
|
+
def _write_outputs(
|
|
326
|
+
src: Path,
|
|
327
|
+
out_dir: Path,
|
|
328
|
+
markdown: str,
|
|
329
|
+
images: dict,
|
|
330
|
+
*,
|
|
331
|
+
extract_images: bool,
|
|
332
|
+
) -> tuple[str, tuple[str, ...]]:
|
|
333
|
+
"""落盘 Markdown 与图片,返回 (md 路径, 图片路径元组)
|
|
334
|
+
|
|
335
|
+
图片名取 basename 防路径穿越;统一转 RGB 后再存(RGBA 存 JPEG 会失败)。
|
|
336
|
+
"""
|
|
337
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
338
|
+
md_path = out_dir / f"{src.stem}.md"
|
|
339
|
+
md_path.write_text(markdown, encoding="utf-8")
|
|
340
|
+
|
|
341
|
+
image_paths: list[str] = []
|
|
342
|
+
if extract_images:
|
|
343
|
+
for name, image in images.items():
|
|
344
|
+
target = out_dir / Path(name).name
|
|
345
|
+
image.convert("RGB").save(target)
|
|
346
|
+
image_paths.append(str(target))
|
|
347
|
+
return str(md_path), tuple(image_paths)
|
|
348
|
+
|
|
349
|
+
# ------------------------------------------------------------------
|
|
350
|
+
# 公开 API
|
|
351
|
+
# ------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
@classmethod
|
|
354
|
+
def convert_to_md(
|
|
355
|
+
cls,
|
|
356
|
+
file_path: str | Path,
|
|
357
|
+
*,
|
|
358
|
+
mode: _ModeLiteral | None = None,
|
|
359
|
+
force_ocr: bool | None = None,
|
|
360
|
+
disable_ocr: bool | None = None,
|
|
361
|
+
page_range: str | None = None,
|
|
362
|
+
) -> str:
|
|
363
|
+
"""将文档解析为 Markdown 文本(图片不落盘)
|
|
364
|
+
|
|
365
|
+
仅返回 Markdown 字符串;由于图片未落盘,md 内的图片相对引用会失效。
|
|
366
|
+
需要完整产物请使用 convert_to_dir。
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
file_path: 文档文件路径(支持 str 或 Path)
|
|
370
|
+
mode: 解析模式,None 时用 MARKER_MODE 配置,再 None 则交给 marker 自动
|
|
371
|
+
force_ocr: 是否强制全页 OCR;None 表示不指定
|
|
372
|
+
disable_ocr: 是否禁用全部 VLM 调用(无需 llama.cpp);None 表示不指定
|
|
373
|
+
page_range: 页码范围,如 "0-5,7"
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
str: 解析出的 Markdown 文本
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
FileNotFoundError: 文件不存在
|
|
380
|
+
ValueError: 路径非法 / 扩展名不受支持 / 参数冲突
|
|
381
|
+
MarkerError: 模型加载或解析失败
|
|
382
|
+
|
|
383
|
+
Example:
|
|
384
|
+
>>> md = MarkerKit.convert_to_md("report.pdf")
|
|
385
|
+
>>> md = MarkerKit.convert_to_md("scan.pdf", disable_ocr=True)
|
|
386
|
+
"""
|
|
387
|
+
path = cls._validate_path(file_path)
|
|
388
|
+
config = cls._build_config(
|
|
389
|
+
mode=mode,
|
|
390
|
+
force_ocr=force_ocr,
|
|
391
|
+
disable_ocr=disable_ocr,
|
|
392
|
+
page_range=page_range,
|
|
393
|
+
)
|
|
394
|
+
logger.info(f"开始 Marker 解析: {path.name}")
|
|
395
|
+
text, _images, _pages = cls._render(path, config)
|
|
396
|
+
logger.info(f"Marker 解析成功: {path.name}({len(text)} 字符)")
|
|
397
|
+
return text
|
|
398
|
+
|
|
399
|
+
@classmethod
|
|
400
|
+
def convert_to_dir(
|
|
401
|
+
cls,
|
|
402
|
+
src_path: str | Path,
|
|
403
|
+
output_dir: str | Path | None = None,
|
|
404
|
+
*,
|
|
405
|
+
mode: _ModeLiteral | None = None,
|
|
406
|
+
force_ocr: bool | None = None,
|
|
407
|
+
disable_ocr: bool | None = None,
|
|
408
|
+
page_range: str | None = None,
|
|
409
|
+
extract_images: bool = True,
|
|
410
|
+
) -> str:
|
|
411
|
+
"""解析文档并将 Markdown + 图片落盘到目录
|
|
412
|
+
|
|
413
|
+
产物写入 output_dir:`<源文件名>.md` 与若干图片文件(md 以相对路径引用)。
|
|
414
|
+
不生成 marker 默认的 `_meta.json`。
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
src_path: 源文档路径
|
|
418
|
+
output_dir: 产物目录;None 时使用源文件同目录下与文件名同名的子目录
|
|
419
|
+
mode: 解析模式,None 时用 MARKER_MODE 配置,再 None 则交给 marker 自动
|
|
420
|
+
force_ocr: 是否强制全页 OCR;None 表示不指定
|
|
421
|
+
disable_ocr: 是否禁用全部 VLM 调用(无需 llama.cpp);None 表示不指定
|
|
422
|
+
page_range: 页码范围,如 "0-5,7"
|
|
423
|
+
extract_images: 是否落盘图片(False 时仅写 md)
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
str: 落盘的 Markdown 文件绝对路径
|
|
427
|
+
|
|
428
|
+
Raises:
|
|
429
|
+
FileNotFoundError: 源文件不存在
|
|
430
|
+
ValueError: 路径非法 / 扩展名不受支持 / 参数冲突
|
|
431
|
+
MarkerError: 模型加载或解析失败
|
|
432
|
+
OSError: 产物写入失败
|
|
433
|
+
|
|
434
|
+
Example:
|
|
435
|
+
>>> MarkerKit.convert_to_dir("report.pdf") # 同目录 report/
|
|
436
|
+
>>> MarkerKit.convert_to_dir("report.pdf", "out/doc") # 指定目录
|
|
437
|
+
"""
|
|
438
|
+
src = cls._validate_path(src_path)
|
|
439
|
+
out_dir = cls._resolve_out_dir(src, output_dir)
|
|
440
|
+
config = cls._build_config(
|
|
441
|
+
mode=mode,
|
|
442
|
+
force_ocr=force_ocr,
|
|
443
|
+
disable_ocr=disable_ocr,
|
|
444
|
+
page_range=page_range,
|
|
445
|
+
)
|
|
446
|
+
if not extract_images:
|
|
447
|
+
config["extract_images"] = False
|
|
448
|
+
|
|
449
|
+
logger.info(f"开始 Marker 解析: {src.name} -> {out_dir}")
|
|
450
|
+
text, images, _pages = cls._render(src, config)
|
|
451
|
+
md_path, image_paths = cls._write_outputs(
|
|
452
|
+
src, out_dir, text, images, extract_images=extract_images
|
|
453
|
+
)
|
|
454
|
+
logger.info(
|
|
455
|
+
f"Marker 解析成功: {src.name}({len(text)} 字符,"
|
|
456
|
+
f"{len(image_paths)} 图)-> {md_path}"
|
|
457
|
+
)
|
|
458
|
+
return md_path
|
|
459
|
+
|
|
460
|
+
@classmethod
|
|
461
|
+
def convert(
|
|
462
|
+
cls,
|
|
463
|
+
src_path: str | Path,
|
|
464
|
+
output_dir: str | Path | None = None,
|
|
465
|
+
*,
|
|
466
|
+
mode: _ModeLiteral | None = None,
|
|
467
|
+
force_ocr: bool | None = None,
|
|
468
|
+
disable_ocr: bool | None = None,
|
|
469
|
+
page_range: str | None = None,
|
|
470
|
+
extract_images: bool = True,
|
|
471
|
+
) -> MarkerResult:
|
|
472
|
+
"""安全转换文档为 Markdown + Images,返回包含状态的结果对象
|
|
473
|
+
|
|
474
|
+
与 convert_to_dir 不同,本方法不抛出转换异常,而是将成功/失败信息封装到
|
|
475
|
+
MarkerResult 中,便于批量处理或状态检查。产物落盘规则同 convert_to_dir。
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
src_path: 源文档路径
|
|
479
|
+
output_dir: 产物目录;None 时使用源文件同目录下与文件名同名的子目录
|
|
480
|
+
mode: 解析模式,None 时用 MARKER_MODE 配置,再 None 则交给 marker 自动
|
|
481
|
+
force_ocr: 是否强制全页 OCR;None 表示不指定
|
|
482
|
+
disable_ocr: 是否禁用全部 VLM 调用;None 表示不指定
|
|
483
|
+
page_range: 页码范围,如 "0-5,7"
|
|
484
|
+
extract_images: 是否落盘图片
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
MarkerResult: 解析结果(含 markdown / md_path / image_paths / error)
|
|
488
|
+
|
|
489
|
+
Example:
|
|
490
|
+
>>> result = MarkerKit.convert("report.pdf", "out/doc")
|
|
491
|
+
>>> if result.success:
|
|
492
|
+
... print(result.md_path, len(result.image_paths))
|
|
493
|
+
... else:
|
|
494
|
+
... print(f"解析失败: {result.error}")
|
|
495
|
+
"""
|
|
496
|
+
src = Path(src_path).resolve()
|
|
497
|
+
out_dir = cls._resolve_out_dir(src, output_dir)
|
|
498
|
+
try:
|
|
499
|
+
config = cls._build_config(
|
|
500
|
+
mode=mode,
|
|
501
|
+
force_ocr=force_ocr,
|
|
502
|
+
disable_ocr=disable_ocr,
|
|
503
|
+
page_range=page_range,
|
|
504
|
+
)
|
|
505
|
+
path = cls._validate_path(src)
|
|
506
|
+
if not extract_images:
|
|
507
|
+
config["extract_images"] = False
|
|
508
|
+
text, images, page_count = cls._render(path, config)
|
|
509
|
+
md_path, image_paths = cls._write_outputs(
|
|
510
|
+
path, out_dir, text, images, extract_images=extract_images
|
|
511
|
+
)
|
|
512
|
+
except Exception as e: # noqa: BLE001 - 安全版统一封装为结果对象
|
|
513
|
+
logger.warning(f"Marker 解析失败: {src.name}({type(e).__name__}): {e}")
|
|
514
|
+
return MarkerResult(
|
|
515
|
+
success=False,
|
|
516
|
+
markdown="",
|
|
517
|
+
file_path=str(src),
|
|
518
|
+
output_dir=str(out_dir),
|
|
519
|
+
error=f"{type(e).__name__}: {e}",
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
logger.info(
|
|
523
|
+
f"Marker 解析成功: {path.name}({len(text)} 字符,"
|
|
524
|
+
f"{len(image_paths)} 图)-> {md_path}"
|
|
525
|
+
)
|
|
526
|
+
return MarkerResult(
|
|
527
|
+
success=True,
|
|
528
|
+
markdown=text,
|
|
529
|
+
file_path=str(path),
|
|
530
|
+
output_dir=str(out_dir),
|
|
531
|
+
md_path=md_path,
|
|
532
|
+
image_paths=image_paths,
|
|
533
|
+
page_count=page_count,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
@classmethod
|
|
537
|
+
def shutdown(cls) -> None:
|
|
538
|
+
"""释放 Marker 模型与共享推理服务(幂等,可重复调用)
|
|
539
|
+
|
|
540
|
+
仅在本进程实际加载过模型时才有实际动作;释放后再次调用解析方法会重新加载。
|
|
541
|
+
"""
|
|
542
|
+
with cls._lock:
|
|
543
|
+
if cls._model_dict is not None:
|
|
544
|
+
shutdown_models(cls._model_dict)
|
|
545
|
+
cls._model_dict = None
|
|
546
|
+
cls._converter = None
|
|
547
|
+
cls._config_signature = None
|
|
548
|
+
logger.info("Marker 模型已释放")
|
|
@@ -51,6 +51,7 @@ dynamic = ["version"]
|
|
|
51
51
|
# anydoc → anydoc_kit(firecrawl-anydoc + pikepdf,显式声明)
|
|
52
52
|
# docx → docx_kit(docxtpl + python-docx,模板动态填充)
|
|
53
53
|
# mineru → mineru_kit(requests)
|
|
54
|
+
# marker → marker_kit(marker-pdf[full],本地模型解析 PDF→Markdown+Images)
|
|
54
55
|
# 基础版 = config/log + kit.db 核心,SQLite 常规操作用标准库 sqlite3,零额外依赖。
|
|
55
56
|
[project.optional-dependencies]
|
|
56
57
|
pg = ["psycopg2-binary>=2.9.13"]
|
|
@@ -76,6 +77,7 @@ anydoc = [
|
|
|
76
77
|
]
|
|
77
78
|
docx = ["docxtpl>=0.20.2"]
|
|
78
79
|
mineru = ["requests>=2.34.2"]
|
|
80
|
+
marker = ["marker-pdf[full]>=2.0.0"]
|
|
79
81
|
all = [
|
|
80
82
|
"psycopg2-binary>=2.9.13",
|
|
81
83
|
"pymysql>=1.2.3",
|
|
@@ -92,6 +94,7 @@ all = [
|
|
|
92
94
|
"pikepdf>=10.13.0",
|
|
93
95
|
"docxtpl>=0.20.2",
|
|
94
96
|
"requests>=2.34.2",
|
|
97
|
+
"marker-pdf[full]>=2.0.0",
|
|
95
98
|
]
|
|
96
99
|
[tool.hatch.version]
|
|
97
100
|
path = "kit/__init__.py"
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|