dsh-convert-core 0.1.0-alpha.1
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.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/bin/media-to-md.mjs +168 -0
- package/bin/tita-convert.mjs +85 -0
- package/docs/media-to-md.md +143 -0
- package/lib/adapters/anydoc.d.ts +73 -0
- package/lib/adapters/anydoc.d.ts.map +1 -0
- package/lib/adapters/anydoc.js +295 -0
- package/lib/adapters/anydoc.js.map +1 -0
- package/lib/adapters/iddoc.d.ts +28 -0
- package/lib/adapters/iddoc.d.ts.map +1 -0
- package/lib/adapters/iddoc.js +153 -0
- package/lib/adapters/iddoc.js.map +1 -0
- package/lib/adapters/index.d.ts +41 -0
- package/lib/adapters/index.d.ts.map +1 -0
- package/lib/adapters/index.js +115 -0
- package/lib/adapters/index.js.map +1 -0
- package/lib/adapters/scanned.d.ts +28 -0
- package/lib/adapters/scanned.d.ts.map +1 -0
- package/lib/adapters/scanned.js +121 -0
- package/lib/adapters/scanned.js.map +1 -0
- package/lib/adapters/text.d.ts +29 -0
- package/lib/adapters/text.d.ts.map +1 -0
- package/lib/adapters/text.js +135 -0
- package/lib/adapters/text.js.map +1 -0
- package/lib/adapters/types.d.ts +65 -0
- package/lib/adapters/types.d.ts.map +1 -0
- package/lib/adapters/types.js +11 -0
- package/lib/adapters/types.js.map +1 -0
- package/lib/contract.d.ts +185 -0
- package/lib/contract.d.ts.map +1 -0
- package/lib/contract.js +59 -0
- package/lib/contract.js.map +1 -0
- package/lib/delivery.d.ts +36 -0
- package/lib/delivery.d.ts.map +1 -0
- package/lib/delivery.js +97 -0
- package/lib/delivery.js.map +1 -0
- package/lib/http.d.ts +34 -0
- package/lib/http.d.ts.map +1 -0
- package/lib/http.js +372 -0
- package/lib/http.js.map +1 -0
- package/lib/markdown.d.ts +32 -0
- package/lib/markdown.d.ts.map +1 -0
- package/lib/markdown.js +145 -0
- package/lib/markdown.js.map +1 -0
- package/lib/media/binaries.d.ts +26 -0
- package/lib/media/binaries.d.ts.map +1 -0
- package/lib/media/binaries.js +60 -0
- package/lib/media/binaries.js.map +1 -0
- package/lib/media/convert.d.ts +19 -0
- package/lib/media/convert.d.ts.map +1 -0
- package/lib/media/convert.js +210 -0
- package/lib/media/convert.js.map +1 -0
- package/lib/media/index.d.ts +27 -0
- package/lib/media/index.d.ts.map +1 -0
- package/lib/media/index.js +27 -0
- package/lib/media/index.js.map +1 -0
- package/lib/media/markdown.d.ts +40 -0
- package/lib/media/markdown.d.ts.map +1 -0
- package/lib/media/markdown.js +89 -0
- package/lib/media/markdown.js.map +1 -0
- package/lib/media/media.d.ts +45 -0
- package/lib/media/media.d.ts.map +1 -0
- package/lib/media/media.js +167 -0
- package/lib/media/media.js.map +1 -0
- package/lib/media/models.d.ts +31 -0
- package/lib/media/models.d.ts.map +1 -0
- package/lib/media/models.js +87 -0
- package/lib/media/models.js.map +1 -0
- package/lib/media/scanned.d.ts +18 -0
- package/lib/media/scanned.d.ts.map +1 -0
- package/lib/media/scanned.js +179 -0
- package/lib/media/scanned.js.map +1 -0
- package/lib/media/types.d.ts +154 -0
- package/lib/media/types.d.ts.map +1 -0
- package/lib/media/types.js +23 -0
- package/lib/media/types.js.map +1 -0
- package/lib/server.d.ts +35 -0
- package/lib/server.d.ts.map +1 -0
- package/lib/server.js +64 -0
- package/lib/server.js.map +1 -0
- package/lib/service.d.ts +170 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +562 -0
- package/lib/service.js.map +1 -0
- package/package.json +59 -0
- package/scripts/asr.py +55 -0
- package/scripts/ocr.py +40 -0
- package/scripts/pdf_pages.py +49 -0
- package/scripts/vendor.mjs +76 -0
- package/src/adapters/anydoc.ts +356 -0
- package/src/adapters/iddoc.ts +171 -0
- package/src/adapters/index.ts +147 -0
- package/src/adapters/scanned.ts +139 -0
- package/src/adapters/text.ts +146 -0
- package/src/adapters/types.ts +76 -0
- package/src/contract.ts +230 -0
- package/src/delivery.ts +124 -0
- package/src/http.ts +394 -0
- package/src/markdown.ts +141 -0
- package/src/media/binaries.ts +67 -0
- package/src/media/convert.ts +232 -0
- package/src/media/index.ts +43 -0
- package/src/media/markdown.ts +105 -0
- package/src/media/media.ts +204 -0
- package/src/media/models.ts +124 -0
- package/src/media/scanned.ts +200 -0
- package/src/media/types.ts +171 -0
- package/src/server.ts +85 -0
- package/src/service.ts +639 -0
- package/web/app.js +382 -0
- package/web/index.html +217 -0
- package/web/styles.css +263 -0
- package/web/vendor/icons.js +25 -0
- package/web/vendor/vue.global.prod.js +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NoneFire
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# dsh-convert-core
|
|
2
|
+
|
|
3
|
+
文档转换**内核**:把 Office / OpenDocument / RTF / EPUB / CSV / 文本层 PDF 转成
|
|
4
|
+
**知识就绪 Markdown + 本地图片**,自带 HTTP API 和一个 Vue 3(Options API)可视化控制台。
|
|
5
|
+
它可以**独立运行**,不依赖 DSH。
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm --filter dsh-convert-core build
|
|
9
|
+
node packages/dsh-convert-core/bin/tita-convert.mjs --port 8787
|
|
10
|
+
# → http://127.0.0.1:8787/
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
DSH 侧只是它的再封装,见
|
|
14
|
+
[`plugins/dsh-data-domain-knowledge/src/convert`](../../plugins/dsh-data-domain-knowledge/src/convert/host.ts)
|
|
15
|
+
(0018 起转换宿主是 knowledge 插件的子模块,内核仍是本包,并以 `optionalDependencies`
|
|
16
|
+
的形式被懒加载)。
|
|
17
|
+
|
|
18
|
+
## 引擎:@firecrawl/anydoc
|
|
19
|
+
|
|
20
|
+
解析全部交给 [`@firecrawl/anydoc`](https://github.com/firecrawl/anydoc)(MIT,Rust 内核,
|
|
21
|
+
官方 Node/Python/WASM binding,本机用 `@firecrawl/anydoc-darwin-arm64` 预编译二进制)。
|
|
22
|
+
|
|
23
|
+
- **一个依赖覆盖 14 种格式**:doc/docx/docm、ppt/pps/pot/pptx/pptm/ppsx/ppsm、
|
|
24
|
+
xls/xlsx/xlsm/xlsb、odt/ods/odp、rtf、epub、csv、pdf
|
|
25
|
+
- **按内容识别格式**(PDF 头、RTF 开组、OLE 流名、ZIP mimetype),改错扩展名也能转
|
|
26
|
+
- **没有 Python、没有 torch、没有模型下载、没有外部二进制**,中位数 4.4ms/文档
|
|
27
|
+
- 结构保真:标题层级、`w:numPr` 列表、合并单元格表格、脚注、公式转 LaTeX
|
|
28
|
+
- **错误码精确**:`unsupported / needsOcr / malformed / encrypted / resourceLimit / missingPart / io`,
|
|
29
|
+
内核原样透出(前缀 `ANYDOC_`)并附上可操作建议
|
|
30
|
+
|
|
31
|
+
### 唯一的胶水:内嵌图片
|
|
32
|
+
|
|
33
|
+
Markdown 无法内嵌字节,所以 anydoc 把内嵌图片渲染成它的 **alt 文本**,字节留在
|
|
34
|
+
`document.assets` 上(带 `mediaType` 和 `originPart`)。而知识召回**只认本地相对路径的图片**,
|
|
35
|
+
所以 `src/adapters/anydoc.ts` 做了三件事:
|
|
36
|
+
|
|
37
|
+
1. 遍历 `document.blocks` 找 `{ kind: 'image', source: { kind: 'asset', assetId } }`,按文档顺序取出图片;
|
|
38
|
+
2. 按内容哈希写成 `assets/img-<sha1_12>.<ext>`;
|
|
39
|
+
3. 把正文里那个 alt 行换成 ``;**找不到唯一占位时追加到文末
|
|
40
|
+
`## 图片` 小节并记 warning**,而不是猜位置。
|
|
41
|
+
|
|
42
|
+
非 `image/*` 的内嵌对象不落地,但计数并写 warning,不静默丢弃。
|
|
43
|
+
|
|
44
|
+
## 分层约定
|
|
45
|
+
|
|
46
|
+
| 层 | 负责 | 不负责 |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| 内核(本包) | 解析、任务队列、进度、取消、交付目录、控制台 | 知识库、DSH 工具、用户审批 |
|
|
49
|
+
| DSH 插件 | 设置、Agent 工具、`/convert` 挂载、入库接缝 | 任何解析逻辑 |
|
|
50
|
+
|
|
51
|
+
内核提供两种集成形态,同一份实现:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const running = await startConvertServer({ port: 8787 }) // 独立运行
|
|
55
|
+
|
|
56
|
+
const service = await createConvertService({ rootDir }) // 嵌入宿主
|
|
57
|
+
const handler = createRequestHandler({ service, basePath: '/convert' })
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 产物契约
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
convert-out/<taskId>/
|
|
64
|
+
index.md front matter + 正文,图片引用为 assets/img-<sha1_12>.<ext>
|
|
65
|
+
assets/ 内容寻址的本地图片
|
|
66
|
+
manifest.json 来源、引擎与格式、warnings、content_hash、ingest 负载
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
front matter 与知识录入(0013)对齐:`title / created_at / updated_at` 必填,
|
|
70
|
+
`external_id / source_format / content_hash / parser / source_path /
|
|
71
|
+
source_type_source / source_type_candidates` 由内核如实填写。
|
|
72
|
+
|
|
73
|
+
**内核不决定 `source_type`**。它只给出候选(`source_type_candidates`),并在
|
|
74
|
+
`manifest.ingest.sourceTypeRequired` 上写明"还没人分类"。这是 0013 的边界。
|
|
75
|
+
|
|
76
|
+
## 范围
|
|
77
|
+
|
|
78
|
+
文档类**只做文件类**;网页链接仍在
|
|
79
|
+
[`issues/0017-网页与视频转换能力.md`](../../issues/0017-网页与视频转换能力.md)。
|
|
80
|
+
**本地音视频与图片(`iddoc`)、扫描件轻量 OCR(`scanned`)已落地**;版面/表格/公式级的
|
|
81
|
+
高质量扫描档(docling / MinerU)仍是 0017 的可选档。
|
|
82
|
+
|
|
83
|
+
`needsOcr` 是文档线划在哪里的地方:扫描版 PDF 在默认路径上会**明确失败**,并把错误建议指向
|
|
84
|
+
`scanned` 档(或 `SCANNED_OCR=1`),而不是悄悄改成云端 OCR。特别地,**不使用 anydoc 的
|
|
85
|
+
`ocr: 'hosted'`** —— 它会把整份文档发往 Firecrawl Parse 云端,与 0015 的本地优先原则冲突。
|
|
86
|
+
|
|
87
|
+
## 适配器
|
|
88
|
+
|
|
89
|
+
| 输入 | 适配器 | 说明 |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| 14 种文档格式(含文本层 PDF) | `anydoc` | 见上;内嵌图片落地到 `assets/` |
|
|
92
|
+
| 音视频(`.mp4 .mov .mkv .webm …`、`.mp3 .m4a .wav …`)与图片(`.png .jpg .webp …`) | `iddoc` | 能力来自内核自带的本地媒体管线 [`src/media/`](docs/media-to-md.md):**字幕优先**,无字幕时才本地转写(默认**不**下载模型,`IDDOC_ASR=1` 才允许);图片默认只收录不做 OCR;视频关键帧(均匀采样 + 场景变化 + 内容去重)落地到 `assets/` |
|
|
93
|
+
| 扫描件 / 纯图片页 PDF | `scanned` | 同一包的 `convertScannedDocument()`:`pypdfium2` 逐页渲染 + 本地 `RapidOCR` 识别,`page-0001.png …` 落地为 assets。**显式 opt-in**:默认 `.pdf` 仍走 `anydoc`,只有 `adapter: "scanned"` 或 `SCANNED_OCR=1` 才走本档;不做版面还原 |
|
|
94
|
+
| `.md .markdown .txt .json .tsv .yml .log .adoc .rst .org .tex` | `text` | 直通 + 契约归一化(重写 front matter) |
|
|
95
|
+
|
|
96
|
+
未登记扩展名只有在**字节确实是文本**(无 NUL、UTF-8 可解码)时才走 `text`,
|
|
97
|
+
否则返回 `FORMAT_UNSUPPORTED` 并列出支持的格式。
|
|
98
|
+
|
|
99
|
+
`iddoc` 与 `scanned` 是**需要外部依赖**的两个适配器:`iddoc` 要 `ffmpeg`/`ffprobe`
|
|
100
|
+
(转写/OCR 档还要 `uvx`),`scanned` 只要 `uvx`。缺依赖时按既有约定返回
|
|
101
|
+
`DEPENDENCY_MISSING` + 安装命令,能力上报里如实写 `available: false`,不静默降级。
|
|
102
|
+
|
|
103
|
+
## API
|
|
104
|
+
|
|
105
|
+
| 方法 | 路径 | 说明 |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| `GET` | `/api/health` | 服务与交付根目录 |
|
|
108
|
+
| `GET` | `/api/capabilities` | 适配器可用性、引擎版本、支持的扩展名 |
|
|
109
|
+
| `POST` | `/api/tasks` | 受理任务:JSON(`text` / `local_path`)、原始字节、或 `multipart/form-data` |
|
|
110
|
+
| `GET` | `/api/tasks` | 任务列表 |
|
|
111
|
+
| `GET` | `/api/tasks/:id` | 任务详情(含 `ingest` 负载) |
|
|
112
|
+
| `POST` | `/api/tasks/:id/cancel` | 取消(删除半成品交付) |
|
|
113
|
+
| `POST` | `/api/tasks/:id/retry` | 重试(复用已落盘的输入) |
|
|
114
|
+
| `GET` | `/api/tasks/:id/markdown` | 交付的 `index.md` |
|
|
115
|
+
| `GET` | `/api/tasks/:id/files/<rel>` | 交付目录内任意文件(拒绝路径穿越) |
|
|
116
|
+
| `GET` | `/api/events` | SSE 任务事件流 |
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# 内联文本
|
|
120
|
+
curl -X POST localhost:8787/api/tasks -H 'content-type: application/json' \
|
|
121
|
+
-d '{"kind":"text","title":"标题","text":"# 内容"}'
|
|
122
|
+
|
|
123
|
+
# 本机文件(内核负责读取、限流、清理)
|
|
124
|
+
curl -X POST localhost:8787/api/tasks -H 'content-type: application/json' \
|
|
125
|
+
-d '{"kind":"file","local_path":"/abs/path/spec.docx"}'
|
|
126
|
+
|
|
127
|
+
# 上传
|
|
128
|
+
curl -X POST "localhost:8787/api/tasks?kind=file" -F "file=@spec.docx"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 控制台
|
|
132
|
+
|
|
133
|
+
`web/index.html` + `web/app.js`,**Vue 3 Options API、零构建**:`vue.global.prod.js` 与
|
|
134
|
+
lucide 图标集由 `scripts/vendor.mjs` 在构建时从 npm 包拷成静态资源(图标以纯 SVG map
|
|
135
|
+
注入,避免 `createIcons()` 在 Vue 背后替换 DOM 节点)。
|
|
136
|
+
|
|
137
|
+
之所以不用 SPA 工具链:内核必须能被 `node bin/tita-convert.mjs` 直接拉起,并且
|
|
138
|
+
DSH 插件要把它当静态页原样挂载。代价是没有 shadcn/Tailwind;DSH 侧的 React 面板
|
|
139
|
+
用 `@tita-data-agent/ui` 的 shadcn 封装(见 AGENTS.md 的组件约定)。
|
|
140
|
+
|
|
141
|
+
## 设计取舍
|
|
142
|
+
|
|
143
|
+
- **本地优先**:全链路在本机完成,不调用任何云端解析;引擎没有网络代码。
|
|
144
|
+
- **图片必须落地**:见上「唯一的胶水」。
|
|
145
|
+
- **可取消**:任务用 `AbortController`,取消后删除半成品交付目录。
|
|
146
|
+
- **进程重启可恢复**:任务状态落 `tasks.json`;重启时把 `running/queued` 标为
|
|
147
|
+
`INTERRUPTED_BY_RESTART` 并保留已落盘的输入,重试不必重来。
|
|
148
|
+
- **不伪造成功**:引擎的错误码原样透出,`needsOcr` / `encrypted` / `resourceLimit`
|
|
149
|
+
各自带建议,没有「转成功但内容为空」的分支。
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* media-to-md —— 命令行入口。
|
|
4
|
+
*
|
|
5
|
+
* 与库同一份实现:CLI 只负责参数解析、把产物写盘、以及把错误(含 suggestion)说人话。
|
|
6
|
+
*
|
|
7
|
+
* 用法:
|
|
8
|
+
* media-to-md <file> [--out <dir>] [--mode auto|subtitle-only|transcribe|frames-only]
|
|
9
|
+
* [--model small|large-v3-turbo|/path/to/model] [--lang zh]
|
|
10
|
+
* [--samples 12] [--max-frames 40] [--scene 0.3]
|
|
11
|
+
* [--allow-model-download] [--ocr-frames] [--image-ocr] [--title "..."]
|
|
12
|
+
* [--uvx <path>] [--python 3.12] [--keep-scratch] [--print]
|
|
13
|
+
*/
|
|
14
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
15
|
+
import { basename, extname, join, resolve } from 'node:path'
|
|
16
|
+
import { convertMedia, convertScannedDocument, MediaToMarkdownError } from '../lib/media/index.js'
|
|
17
|
+
|
|
18
|
+
function parseArgs(argv) {
|
|
19
|
+
const options = { mode: 'auto', samples: 12, maxFrames: 40, scene: 0.3 }
|
|
20
|
+
const takesValue = new Set(['--out', '--mode', '--model', '--lang', '--samples', '--max-frames', '--scene', '--title', '--uvx', '--python', '--dpi', '--max-pages'])
|
|
21
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
22
|
+
const arg = argv[i]
|
|
23
|
+
if (arg === '--allow-model-download') options.allowModelDownload = true
|
|
24
|
+
else if (arg === '--ocr-frames') options.ocrFrames = true
|
|
25
|
+
else if (arg === '--image-ocr') options.imageOcr = true
|
|
26
|
+
else if (arg === '--scanned-pdf') options.scannedPdf = true
|
|
27
|
+
else if (arg === '--no-page-images') options.noPageImages = true
|
|
28
|
+
else if (arg === '--keep-scratch') options.keepScratch = true
|
|
29
|
+
else if (arg === '--print') options.print = true
|
|
30
|
+
else if (arg === '--help' || arg === '-h') options.help = true
|
|
31
|
+
else if (takesValue.has(arg)) {
|
|
32
|
+
const value = argv[i + 1]
|
|
33
|
+
if (value === undefined) throw new Error(`${arg} 需要一个值`)
|
|
34
|
+
i += 1
|
|
35
|
+
options[arg.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = value
|
|
36
|
+
} else if (arg.startsWith('-')) throw new Error(`未知参数:${arg}`)
|
|
37
|
+
else if (!options.file) options.file = arg
|
|
38
|
+
else throw new Error(`多余的位置参数:${arg}`)
|
|
39
|
+
}
|
|
40
|
+
return options
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function usage() {
|
|
44
|
+
return [
|
|
45
|
+
'media-to-md —— 本地音视频/图片 → Markdown + 图片(不调用云端或大模型)',
|
|
46
|
+
'',
|
|
47
|
+
'用法: media-to-md <file> [选项]',
|
|
48
|
+
'',
|
|
49
|
+
'选项:',
|
|
50
|
+
' --out <dir> 产物目录(默认 <文件名>.md-out/)',
|
|
51
|
+
' --mode <档位> auto | subtitle-only | transcribe | frames-only(默认 auto)',
|
|
52
|
+
' --model <name|path> whisper 模型(默认 small;中文建议 large-v3-turbo;也可是本地模型目录)',
|
|
53
|
+
' --lang <code> 语言(默认自动检测;中文建议 zh)',
|
|
54
|
+
' --allow-model-download 允许在无字幕时下载转写模型(默认不允许,会直接报错并告知体积)',
|
|
55
|
+
' --samples <n> 均匀采样帧数(默认 12)',
|
|
56
|
+
' --max-frames <n> 关键帧上限(默认 40)',
|
|
57
|
+
' --scene <0..1> 场景切换阈值(默认 0.3)',
|
|
58
|
+
' --ocr-frames 对关键帧做画面文字 OCR(默认关闭)',
|
|
59
|
+
' --image-ocr 对图片输入做 OCR(默认关闭,图片直接收录)',
|
|
60
|
+
' --scanned-pdf 走扫描档:纯图片页 PDF → 逐页本地 OCR(默认关闭)',
|
|
61
|
+
' --dpi <n> 扫描档渲染 DPI(默认 150)',
|
|
62
|
+
' --max-pages <n> 扫描档最多处理页数(默认 50)',
|
|
63
|
+
' --no-page-images 扫描档不落地页图,只保留文字',
|
|
64
|
+
' --title <text> 产物标题(默认文件名)',
|
|
65
|
+
' --uvx <path> / --python <v> 自定义 uvx 与 Python 版本',
|
|
66
|
+
' --keep-scratch 保留中间目录(排障)',
|
|
67
|
+
' --print 同时把 Markdown 打到 stdout',
|
|
68
|
+
].join('\n')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function main() {
|
|
72
|
+
const options = parseArgs(process.argv.slice(2))
|
|
73
|
+
if (options.help || !options.file) {
|
|
74
|
+
console.log(usage())
|
|
75
|
+
process.exit(options.file ? 0 : 2)
|
|
76
|
+
}
|
|
77
|
+
const file = resolve(options.file)
|
|
78
|
+
const title = options.title || basename(file, extname(file))
|
|
79
|
+
const outDir = resolve(options.out || join(process.cwd(), `${title}.md-out`))
|
|
80
|
+
|
|
81
|
+
const onProgress = progress => {
|
|
82
|
+
const percent = progress.progress === null ? '' : ` ${Math.round(progress.progress * 100)}%`
|
|
83
|
+
console.error(`[${progress.stage}]${percent} ${progress.message ?? ''}`.trimEnd())
|
|
84
|
+
}
|
|
85
|
+
const tools = options.uvx || options.python
|
|
86
|
+
? { ...(options.uvx ? { uvxPath: options.uvx } : {}), ...(options.python ? { pythonVersion: options.python } : {}) }
|
|
87
|
+
: undefined
|
|
88
|
+
|
|
89
|
+
const result = options.scannedPdf
|
|
90
|
+
? await convertScannedDocument({
|
|
91
|
+
file,
|
|
92
|
+
title,
|
|
93
|
+
maxPages: Number(options.maxPages ?? 50),
|
|
94
|
+
dpi: Number(options.dpi ?? 150),
|
|
95
|
+
pageImages: options.noPageImages !== true,
|
|
96
|
+
...(tools ? { tools } : {}),
|
|
97
|
+
...(options.keepScratch ? { keepScratch: true } : {}),
|
|
98
|
+
onProgress,
|
|
99
|
+
})
|
|
100
|
+
: await convertMedia({
|
|
101
|
+
file,
|
|
102
|
+
title,
|
|
103
|
+
mode: options.mode,
|
|
104
|
+
asr: {
|
|
105
|
+
model: options.model ?? 'small',
|
|
106
|
+
...(options.lang ? { language: options.lang } : {}),
|
|
107
|
+
allowModelDownload: options.allowModelDownload === true,
|
|
108
|
+
},
|
|
109
|
+
frames: { samples: Number(options.samples), maxFrames: Number(options.maxFrames), sceneThreshold: Number(options.scene) },
|
|
110
|
+
...(tools ? { tools } : {}),
|
|
111
|
+
...(options.imageOcr ? { imageOcr: true } : {}),
|
|
112
|
+
...(options.ocrFrames ? { ocrFrames: true } : {}),
|
|
113
|
+
...(options.keepScratch ? { keepScratch: true } : {}),
|
|
114
|
+
onProgress,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
await mkdir(join(outDir, 'assets'), { recursive: true })
|
|
118
|
+
for (const asset of result.assets) {
|
|
119
|
+
await writeFile(join(outDir, asset.rel), asset.data)
|
|
120
|
+
}
|
|
121
|
+
const frontMatter = [
|
|
122
|
+
'---',
|
|
123
|
+
`title: ${JSON.stringify(result.title)}`,
|
|
124
|
+
`source_format: ${result.meta.sourceFormat}`,
|
|
125
|
+
`source_file: ${file}`,
|
|
126
|
+
`duration: ${result.meta.durationSeconds.toFixed(2)}`,
|
|
127
|
+
`transcript_source: ${result.meta.transcriptSource}`,
|
|
128
|
+
`parser: media-to-md@0.1.0/${result.meta.sourceFormat}`,
|
|
129
|
+
`external_id: ${result.externalId}`,
|
|
130
|
+
`generated_at: ${new Date().toISOString()}`,
|
|
131
|
+
'---',
|
|
132
|
+
'',
|
|
133
|
+
].join('\n')
|
|
134
|
+
await writeFile(join(outDir, 'index.md'), `${frontMatter}${result.markdown}\n`, 'utf8')
|
|
135
|
+
if (result.transcript.cues.length > 0) {
|
|
136
|
+
const stamp = (seconds) => {
|
|
137
|
+
const value = Number.isFinite(seconds) ? Math.max(0, seconds) : 0
|
|
138
|
+
const total = Math.floor(value)
|
|
139
|
+
const ms = Math.floor((value % 1) * 1000)
|
|
140
|
+
const hh = String(Math.floor(total / 3600)).padStart(2, '0')
|
|
141
|
+
const mm = String(Math.floor((total % 3600) / 60)).padStart(2, '0')
|
|
142
|
+
const ss = String(total % 60).padStart(2, '0')
|
|
143
|
+
return `${hh}:${mm}:${ss},${String(ms).padStart(3, '0')}`
|
|
144
|
+
}
|
|
145
|
+
const srt = result.transcript.cues
|
|
146
|
+
.map((cue, index) => {
|
|
147
|
+
const next = result.transcript.cues[index + 1]
|
|
148
|
+
const end = cue.endSeconds ?? next?.seconds ?? cue.seconds + 2
|
|
149
|
+
return `${index + 1}\n${stamp(cue.seconds)} --> ${stamp(end)}\n${cue.text}\n`
|
|
150
|
+
})
|
|
151
|
+
.join('\n')
|
|
152
|
+
await writeFile(join(outDir, 'transcript.srt'), srt, 'utf8')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(`完成 → ${outDir}`)
|
|
156
|
+
console.log(` index.md assets/${result.assets.length} 个 warnings: ${result.warnings.map(w => w.code).join(', ') || '(无)'}`)
|
|
157
|
+
if (options.print) console.log(`\n${result.markdown}`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
main().catch(error => {
|
|
161
|
+
if (error instanceof MediaToMarkdownError) {
|
|
162
|
+
console.error(`\n失败 [${error.code}] ${error.message}`)
|
|
163
|
+
if (error.suggestion) console.error(`建议:${error.suggestion}`)
|
|
164
|
+
} else {
|
|
165
|
+
console.error(`\n失败:${error instanceof Error ? error.message : String(error)}`)
|
|
166
|
+
}
|
|
167
|
+
process.exit(1)
|
|
168
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Standalone kernel launcher.
|
|
4
|
+
*
|
|
5
|
+
* tita-convert --port 8787 --root ~/.tita-data-agent/convert
|
|
6
|
+
*
|
|
7
|
+
* This is the "kernel first" half of the design: the console it serves is the
|
|
8
|
+
* same page the DSH plugin embeds, and it works with DSH not installed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { startConvertServer } from '../lib/server.js'
|
|
12
|
+
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const options = {}
|
|
15
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
16
|
+
const token = argv[index]
|
|
17
|
+
const [flag, inline] = token.includes('=') ? token.split(/=(.*)/s, 2) : [token, undefined]
|
|
18
|
+
const next = () => inline ?? argv[++index]
|
|
19
|
+
switch (flag) {
|
|
20
|
+
case '--port': case '-p': options.port = Number(next()); break
|
|
21
|
+
case '--host': options.host = next(); break
|
|
22
|
+
case '--root': options.rootDir = next(); break
|
|
23
|
+
case '--base-path': options.basePath = next(); break
|
|
24
|
+
case '--api-key': options.apiKey = next(); break
|
|
25
|
+
case '--web-root': options.webRoot = next(); break
|
|
26
|
+
case '--max-concurrent': options.maxConcurrent = Number(next()); break
|
|
27
|
+
case '--docling': options.doclingBaseUrl = next(); break
|
|
28
|
+
case '--prefer-docling': options.preferDocling = true; break
|
|
29
|
+
case '--help': case '-h': options.help = true; break
|
|
30
|
+
case '--version': case '-v': options.version = true; break
|
|
31
|
+
default:
|
|
32
|
+
if (flag?.startsWith('-')) {
|
|
33
|
+
console.error(`未知参数:${flag}`)
|
|
34
|
+
options.help = true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return options
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const HELP = `tita-convert — 多格式转换内核(独立运行)
|
|
42
|
+
|
|
43
|
+
用法
|
|
44
|
+
tita-convert [选项]
|
|
45
|
+
|
|
46
|
+
选项
|
|
47
|
+
-p, --port <n> 监听端口(默认 8787)
|
|
48
|
+
--host <host> 监听地址(默认 127.0.0.1,只对本机开放)
|
|
49
|
+
--root <dir> 任务与交付根目录(默认 ~/.tita-data-agent/convert)
|
|
50
|
+
--base-path <path> 挂载前缀(默认空,即根路径)
|
|
51
|
+
--api-key <key> 要求 X-Api-Key 头
|
|
52
|
+
--max-concurrent <n> 并发转换数(默认 2)
|
|
53
|
+
--docling <url> docling-serve 地址,启用高精度档
|
|
54
|
+
--prefer-docling 有 docling 地址时优先使用它
|
|
55
|
+
-h, --help 显示帮助
|
|
56
|
+
-v, --version 显示版本
|
|
57
|
+
|
|
58
|
+
控制台
|
|
59
|
+
启动后打开 http://127.0.0.1:<port>/ 即可看到 Vue3 转换面板。
|
|
60
|
+
`
|
|
61
|
+
|
|
62
|
+
const options = parseArgs(process.argv.slice(2))
|
|
63
|
+
if (options.help) {
|
|
64
|
+
console.log(HELP)
|
|
65
|
+
process.exit(0)
|
|
66
|
+
}
|
|
67
|
+
if (options.version) {
|
|
68
|
+
const { readFile } = await import('node:fs/promises')
|
|
69
|
+
const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'))
|
|
70
|
+
console.log(pkg.version)
|
|
71
|
+
process.exit(0)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const running = await startConvertServer(options)
|
|
75
|
+
console.log(`Tita 转换内核已启动:${running.url}`)
|
|
76
|
+
console.log(` 控制台 ${running.url}/`)
|
|
77
|
+
console.log(` API ${running.url}/api/health`)
|
|
78
|
+
console.log(` 交付目录 ${running.service.outDir}`)
|
|
79
|
+
|
|
80
|
+
const shutdown = async () => {
|
|
81
|
+
await running.close()
|
|
82
|
+
process.exit(0)
|
|
83
|
+
}
|
|
84
|
+
process.on('SIGINT', () => { void shutdown() })
|
|
85
|
+
process.on('SIGTERM', () => { void shutdown() })
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# 本地媒体管线(`src/media/`)
|
|
2
|
+
|
|
3
|
+
把**本地**音视频 / 图片 / 扫描件 PDF 转成「知识就绪 Markdown + 本地图片」。**纯本地:不调用云端 API、不调用大模型。**
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
<file> ──ffmpeg──→ 关键帧(场景变化 + 均匀采样 + 内容去重)+ 音轨/字幕轨
|
|
7
|
+
├─ 有字幕 ─→ 字幕轨/旁挂 .srt ─────────────────┐
|
|
8
|
+
└─ 无字幕 ─→ faster-whisper 本地转写(可选档) ──┤
|
|
9
|
+
图片 ─────→ 直接收录(默认不 OCR) ──────────────┤
|
|
10
|
+
扫描件 PDF ──────→ pypdfium2 逐页渲染 + RapidOCR 逐页识别 ┴─→ Markdown + assets/
|
|
11
|
+
(轻量档:见 `convertScannedDocument()`,不出网、不做版面还原)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 它不涉及大模型 —— 但需要几个**专用小模型**
|
|
15
|
+
|
|
16
|
+
| 环节 | 用的是什么 | 是不是大模型 |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| 抽帧 / 抽轨 / 拆音频 | `ffmpeg`、`ffprobe` | 不是,纯信号处理 |
|
|
19
|
+
| PDF 逐页渲染 | `pypdfium2`(自带 pdfium,无需系统 poppler) | 不是,纯渲染 |
|
|
20
|
+
| 语音 → 文字 | `faster-whisper`(whisper 模型) | **不是 LLM**。语音识别专用模型,本地 CPU;tiny 75MB / small 460MB / large-v3-turbo ≈1.5GB |
|
|
21
|
+
| 画面文字 / 扫描页文字 → 文字 | `RapidOCR`(ONNX) | **不是 LLM**。OCR 小模型,几十 MB |
|
|
22
|
+
| 拼装 Markdown | 本包模板 | 纯规则 |
|
|
23
|
+
|
|
24
|
+
模型只在首次使用时下载一次,之后完全离线。**是否允许下载由调用方显式决定**(`asr.allowModelDownload`,默认 `false`)—— 没字幕又没允许下载时,直接抛 `ASR_MODEL_REQUIRED` 并写清"要下多大"。
|
|
25
|
+
|
|
26
|
+
## 安装与构建
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm --filter dsh-convert-core build
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
依赖:系统 `ffmpeg`/`ffprobe`(必需);`uvx`(只有转写 / OCR 档需要,`uv` 自带)。
|
|
33
|
+
|
|
34
|
+
## 库用法
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// 内核内:从 ../media/index.js 导入。单独用也可以走 convert-core 的 ./media 子路径导出。
|
|
38
|
+
import { convertMedia } from 'dsh-convert-core/media'
|
|
39
|
+
|
|
40
|
+
const result = await convertMedia({
|
|
41
|
+
file: '/abs/path/clip.mp4',
|
|
42
|
+
mode: 'auto', // auto | subtitle-only | transcribe | frames-only
|
|
43
|
+
asr: { model: 'large-v3-turbo', language: 'zh', allowModelDownload: true },
|
|
44
|
+
frames: { samples: 12, maxFrames: 40, sceneThreshold: 0.3 },
|
|
45
|
+
ocrFrames: false, // 关键帧画面文字(默认关)
|
|
46
|
+
imageOcr: false, // 图片 OCR(默认关:图片直接收录)
|
|
47
|
+
onProgress: p => console.log(p.stage, p.progress),
|
|
48
|
+
})
|
|
49
|
+
// result.markdown Markdown 正文(不含 front matter)
|
|
50
|
+
// result.assets [{ rel: 'assets/frame-0001.jpg', data: Buffer, contentType }]
|
|
51
|
+
// result.meta { sourceFormat, durationSeconds, transcriptSource, … }
|
|
52
|
+
// result.warnings [{ code, message }]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
档位规则:
|
|
56
|
+
|
|
57
|
+
| mode | 字幕 | 转写 | 说明 |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| `auto`(默认) | 优先 | 仅当允许下载模型 | 有字幕就绝不碰模型 |
|
|
60
|
+
| `subtitle-only` | 只用 | 永不 | 没字幕就是"没有逐字稿",不报错 |
|
|
61
|
+
| `transcribe` | 跳过 | 强制 | 必须允许下载模型 |
|
|
62
|
+
| `frames-only` | 跳过 | 跳过 | 只要关键帧 |
|
|
63
|
+
|
|
64
|
+
## 扫描件轻量档:`convertScannedDocument()`
|
|
65
|
+
|
|
66
|
+
纯图片页 PDF(扫描件)走这条路 —— 它解决的**唯一**问题是"图片页里的字读不出来"。
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { convertScannedDocument } from 'dsh-convert-core/media'
|
|
70
|
+
|
|
71
|
+
const result = await convertScannedDocument({
|
|
72
|
+
file: '/abs/scan.pdf',
|
|
73
|
+
dpi: 150, // 渲染 DPI(默认 150)
|
|
74
|
+
maxPages: 50, // 上限,超出写 warning,不静默截断
|
|
75
|
+
pageImages: true, // 逐页图片落地为 assets/page-NNNN.png
|
|
76
|
+
})
|
|
77
|
+
// result.pages → [{ index, rel, width, height, text, ocrLines }]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**它不做/不承诺什么**(这是刻意的):
|
|
81
|
+
|
|
82
|
+
- **不做版面还原**:输出是"第 N 页 + 该页图片 + 该页文字",顺序 = 页序;多栏、表格、公式会乱;
|
|
83
|
+
- **不引入 docling / PaddleOCR 框架 / MinerU**:那是 `issues/0017` 的"可选高质量档"(几 GB ~ 20 GB 依赖);
|
|
84
|
+
- **不联网**:只有两个本地组件 —— `pypdfium2`(渲染)与 `RapidOCR`(识别);
|
|
85
|
+
- **不判断该不该用**:是"这份 PDF 没有文本层"才走这里;默认路径应当先让文档引擎试文本层。
|
|
86
|
+
|
|
87
|
+
## CLI
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# 音视频:带中文转写
|
|
91
|
+
node packages/dsh-convert-core/bin/media-to-md.mjs clip.mp4 --lang zh --model large-v3-turbo --allow-model-download
|
|
92
|
+
# → clip.md-out/{index.md, assets/, transcript.srt}
|
|
93
|
+
|
|
94
|
+
# 扫描件:逐页 OCR(无需任何系统依赖)
|
|
95
|
+
node packages/dsh-convert-core/bin/media-to-md.mjs scan.pdf --scanned-pdf --dpi 200
|
|
96
|
+
# → scan.md-out/{index.md, assets/page-0001.png …}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
扫描档相关参数:`--scanned-pdf`、`--dpi <n>`、`--max-pages <n>`、`--no-page-images`。
|
|
100
|
+
|
|
101
|
+
## 设计取舍
|
|
102
|
+
|
|
103
|
+
- **抽帧 = 均匀采样 + 场景变化,两路合并再按内容 sha1 去重。** 只靠 `scene` 会漏:口播/图文视频几乎没有硬切(实测:3 段画面的测试片,阈值降到 0.1 也只出 1 帧)。
|
|
104
|
+
- **字幕优先。** 有字幕轨/旁挂 `.srt` 时完全不下载转写模型。
|
|
105
|
+
- **图片默认不 OCR。** 图片直接收录、靠多模态召回(0013 的决定);需要文字时显式开 `imageOcr`。
|
|
106
|
+
- **扫描件只做轻量档,并且要求显式 opt-in。** 先用重的方案会让人误以为"必须装 20 GB 才能读扫描件"。
|
|
107
|
+
- **PDF 渲染用 pypdfium2 而不是 poppler。** `pdftoppm` 是外部二进制,不是每台机器都有(开发机就没有);pypdfium2 是 pip wheel,自带 pdfium,`uvx` 按需拉起。
|
|
108
|
+
- **产物只给正文和 assets。** front matter、写盘、入库都是宿主的事(转换内核写 manifest,CLI 写自己的 front matter),这个包不认识任何宿主。
|
|
109
|
+
- **`externalId` = 文件 sha256**,给宿主做幂等。
|
|
110
|
+
|
|
111
|
+
## 环境变量
|
|
112
|
+
|
|
113
|
+
| 变量 | 用途 |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| `MEDIA_TO_MD_UVX` | uvx 可执行文件(等价 `tools.uvxPath`) |
|
|
116
|
+
| `MEDIA_TO_MD_PYTHON` | uvx 使用的 Python 版本(默认 `3.12`) |
|
|
117
|
+
| `HF_ENDPOINT` | HuggingFace 镜像,国内建议 `https://hf-mirror.com` |
|
|
118
|
+
|
|
119
|
+
## 排障
|
|
120
|
+
|
|
121
|
+
- **`Could not create temporary file` / uv 缓存不可写**:把缓存重定向到可写目录:
|
|
122
|
+
`UV_CACHE_DIR`、`UV_PYTHON_INSTALL_DIR`、`UV_TOOL_DIR`、`UV_TOOL_BIN_DIR`、`XDG_CACHE_HOME`。
|
|
123
|
+
- **模型下载失败(SSL / Server disconnected)**:Python 侧走 HuggingFace 下载不稳定时,用 curl 预先下好模型目录再把 `asr.model` 指向它:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
DIR=~/.cache/media-to-md/faster-whisper-small && mkdir -p "$DIR"
|
|
127
|
+
for f in config.json tokenizer.json vocabulary.txt model.bin; do
|
|
128
|
+
curl -L --retry 3 -o "$DIR/$f" "https://hf-mirror.com/Systran/faster-whisper-small/resolve/main/$f"
|
|
129
|
+
done
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- **转写为空**:确认有音轨;`tiny`/`base` 对中文很差,换 `small` 以上。
|
|
133
|
+
|
|
134
|
+
## 消费者
|
|
135
|
+
|
|
136
|
+
- `dsh-convert-core` 的 `iddoc` adapter(issue 0017 的 video 档):本包负责能力,内核负责契约与产物目录。
|
|
137
|
+
- CLI / 未来的桌面端。
|
|
138
|
+
|
|
139
|
+
## 限制
|
|
140
|
+
|
|
141
|
+
- 长音频未做 VAD 分片与取消后的分片保留(子进程会随 `AbortSignal` 结束)。
|
|
142
|
+
- 说话人分离未接。
|
|
143
|
+
- 网页链接与扫描件 OCR 不在本包范围(见 `issues/0017` 的 `web.ts` / `docling.ts`)。
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The document engine: `@firecrawl/anydoc` (MIT, Rust, prebuilt Node binding).
|
|
3
|
+
*
|
|
4
|
+
* One adapter replaces what used to be five (pdfjs text-layer reader, mammoth,
|
|
5
|
+
* the high-quality docling tier, plus the format routing between them). anydoc
|
|
6
|
+
* covers doc/docx/docm, ppt/pps/pot/pptx/pptm/ppsx/ppsm, xls/xlsx/xlsm/xlsb,
|
|
7
|
+
* odt/ods/odp, rtf, epub, csv and text-layer pdf, detects the format from the
|
|
8
|
+
* bytes, and needs no Python, no models and no external binary.
|
|
9
|
+
*
|
|
10
|
+
* The one thing it cannot do for us is the 0013 image contract: Markdown has no
|
|
11
|
+
* way to embed bytes, so an embedded image renders as its alt text while the
|
|
12
|
+
* bytes stay on `document.assets`. Retrieval only reads local relative image
|
|
13
|
+
* paths, so this adapter re-materialises every embedded image into `assets/`
|
|
14
|
+
* and puts a real `` reference back where the document had it.
|
|
15
|
+
*/
|
|
16
|
+
import { type Adapter, type AdapterAvailability } from './types.js';
|
|
17
|
+
export declare const ANYDOC_EXTENSIONS: string[];
|
|
18
|
+
interface AnyDocAsset {
|
|
19
|
+
id?: number;
|
|
20
|
+
mediaType?: string;
|
|
21
|
+
originPart?: string;
|
|
22
|
+
data?: Uint8Array;
|
|
23
|
+
}
|
|
24
|
+
interface AnyDocInline {
|
|
25
|
+
kind: string;
|
|
26
|
+
alt?: string;
|
|
27
|
+
source?: {
|
|
28
|
+
kind?: string;
|
|
29
|
+
assetId?: number;
|
|
30
|
+
url?: string;
|
|
31
|
+
};
|
|
32
|
+
text?: string;
|
|
33
|
+
}
|
|
34
|
+
interface AnyDocBlock {
|
|
35
|
+
kind: string;
|
|
36
|
+
content?: AnyDocInline[];
|
|
37
|
+
}
|
|
38
|
+
interface AnyDocDocument {
|
|
39
|
+
blocks?: AnyDocBlock[];
|
|
40
|
+
notes?: unknown[];
|
|
41
|
+
assets?: AnyDocAsset[];
|
|
42
|
+
}
|
|
43
|
+
interface AnyDocModule {
|
|
44
|
+
toDocument(bytes: Uint8Array): Promise<AnyDocDocument>;
|
|
45
|
+
toMarkdown(path: string, options?: Record<string, unknown>): Promise<string>;
|
|
46
|
+
toMarkdownBytes(bytes: Uint8Array, format?: string): Promise<string>;
|
|
47
|
+
formatFromBytes(bytes: Uint8Array): string | null;
|
|
48
|
+
formatFromPath(path: string): string | null;
|
|
49
|
+
}
|
|
50
|
+
/** Load the binding once; the platform binary ships as an optional dependency. */
|
|
51
|
+
export declare function loadAnydoc(): Promise<AnyDocModule>;
|
|
52
|
+
export declare function anydocAvailability(): Promise<AdapterAvailability>;
|
|
53
|
+
export interface ImagePlacement {
|
|
54
|
+
markdown: string;
|
|
55
|
+
placed: number;
|
|
56
|
+
appended: number;
|
|
57
|
+
warnings: string[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Put real image references back into the Markdown.
|
|
61
|
+
*
|
|
62
|
+
* anydoc renders an embedded image as its alt text, so placement is a match on
|
|
63
|
+
* that text. A standalone-line match is used only when it is unambiguous;
|
|
64
|
+
* anything that cannot be located is appended under a `## 图片` heading rather
|
|
65
|
+
* than guessed at, and the count is reported.
|
|
66
|
+
*/
|
|
67
|
+
export declare function placeImageReferences(markdown: string, images: Array<{
|
|
68
|
+
rel: string;
|
|
69
|
+
alt: string;
|
|
70
|
+
}>): ImagePlacement;
|
|
71
|
+
export declare const anydocAdapter: Adapter;
|
|
72
|
+
export {};
|
|
73
|
+
//# sourceMappingURL=anydoc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anydoc.d.ts","sourceRoot":"","sources":["../../src/adapters/anydoc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH,OAAO,EAAgB,KAAK,OAAO,EAAE,KAAK,mBAAmB,EAA+D,MAAM,YAAY,CAAA;AAE9I,eAAO,MAAM,iBAAiB,UAW7B,CAAA;AAED,UAAU,WAAW;IACnB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,UAAU,CAAA;CAClB;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,YAAY,EAAE,CAAA;CACzB;AAED,UAAU,cAAc;IACtB,MAAM,CAAC,EAAE,WAAW,EAAE,CAAA;IACtB,KAAK,CAAC,EAAE,OAAO,EAAE,CAAA;IACjB,MAAM,CAAC,EAAE,WAAW,EAAE,CAAA;CACvB;AAED,UAAU,YAAY;IACpB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IACtD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC5E,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACpE,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAAA;IACjD,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAC5C;AAmBD,kFAAkF;AAClF,wBAAsB,UAAU,IAAI,OAAO,CAAC,YAAY,CAAC,CAqBxD;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAWvE;AA0BD,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,cAAc,CAiClH;AAwJD,eAAO,MAAM,aAAa,EAAE,OAQ3B,CAAA"}
|