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.
Files changed (46) hide show
  1. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/PKG-INFO +67 -4
  2. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/README.md +63 -3
  3. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/__init__.py +1 -1
  4. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/config/kit_config.py +21 -0
  5. sdpy_kit-0.2.0/kit/marker_kit.py +548 -0
  6. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/pyproject.toml +3 -0
  7. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/.gitignore +0 -0
  8. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/__init__.py +0 -0
  9. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/base_service.py +0 -0
  10. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/chat_service.py +0 -0
  11. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/embedding_service.py +0 -0
  12. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/exceptions.py +0 -0
  13. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/failover.py +0 -0
  14. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/image_service.py +0 -0
  15. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/model_builder.py +0 -0
  16. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/model_client.py +0 -0
  17. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/pool.py +0 -0
  18. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/profiles.py +0 -0
  19. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/rerank_service.py +0 -0
  20. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/runner.py +0 -0
  21. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/ai/types.py +0 -0
  22. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/anydoc_kit.py +0 -0
  23. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/common_kit.py +0 -0
  24. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/config/__init__.py +0 -0
  25. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/config/log_config.py +0 -0
  26. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/__init__.py +0 -0
  27. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/base_db_kit.py +0 -0
  28. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/crud.py +0 -0
  29. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/engine.py +0 -0
  30. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/exceptions.py +0 -0
  31. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/models/__init__.py +0 -0
  32. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/models/base.py +0 -0
  33. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/mysql_kit.py +0 -0
  34. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/pg_kit.py +0 -0
  35. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/query.py +0 -0
  36. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/session.py +0 -0
  37. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/sqlite_kit.py +0 -0
  38. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/db/transaction.py +0 -0
  39. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/docx_kit.py +0 -0
  40. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/file_kit.py +0 -0
  41. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/mcp_kit.py +0 -0
  42. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/md2doc_kit.py +0 -0
  43. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/mineru_kit.py +0 -0
  44. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/redis_kit.py +0 -0
  45. {sdpy_kit-0.1.14 → sdpy_kit-0.2.0}/kit/wx_article_kit.py +0 -0
  46. {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.1.14
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`,由 anydoc 自己回落读取 |
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_*` | `get_settings()` 带 `lru_cache`,进程内首次读取后固定 → **重启进程**(或调用 `AI.reload_config()`,它会顺带清空该缓存) |
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`,由 anydoc 自己回落读取 |
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_*` | `get_settings()` 带 `lru_cache`,进程内首次读取后固定 → **重启进程**(或调用 `AI.reload_config()`,它会顺带清空该缓存) |
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]"`
@@ -23,7 +23,7 @@ Author: Clark Chang
23
23
  数据库访问通过 kit.db 子包提供(基于 SQLModel)。
24
24
  """
25
25
 
26
- __version__ = "0.1.14"
26
+ __version__ = "0.2.0"
27
27
 
28
28
  from kit.db import (
29
29
  MYSQL,
@@ -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