Skip to content

Commit b264a23

Browse files
committed
feat: add document tree parsing and auto chunking
- Implement hierarchical document tree (HeadingNode, DocumentTree) - Add automatic chunk level detection based on token distribution - Support parent heading chain context in prompts - Add XDG config directory fallback (~/.config/doc2anki) - New CLI options: --chunk-level, --include-parent-chain - Fix template packaging for distribution
1 parent 1d15607 commit b264a23

22 files changed

Lines changed: 2635 additions & 54 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,8 @@ ai_providers.toml
2828
![Rr][Ee][Aa][Dd][Mm][Ee]*.md
2929
![Rr][Ee][Aa][Dd][Mm][Ee]*.org
3030

31+
# keep docs/future planning files
32+
!docs/**/*.md
33+
3134
# remove output files
3235
*.apkg

README.md

Lines changed: 113 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,37 @@ doc2anki 将知识库文档转换为 Anki 学习卡片。
1313

1414
## 安装
1515

16+
### 全局安装 (推荐)
17+
18+
使用 pipx 或 uv 全局安装后,可在任意位置运行:
19+
20+
```sh
21+
# 使用 pipx
22+
pipx install doc2anki
23+
24+
# 或使用 uv
25+
uv tool install doc2anki
26+
```
27+
28+
### 开发环境
29+
1630
```sh
31+
git clone https://github.com/your-repo/doc2anki
32+
cd doc2anki
1733
uv sync
1834
```
1935

2036
## 配置
2137

22-
`config/ai_providers.toml` 文件中配置语言模型提供商:
38+
### 配置文件位置
39+
40+
doc2anki 按以下顺序查找配置文件:
41+
42+
1. 命令行指定的路径 (`--config`)
43+
2. 当前目录: `./config/ai_providers.toml`
44+
3. 用户配置目录: `~/.config/doc2anki/ai_providers.toml`
45+
46+
### 配置格式
2347

2448
```toml
2549
[provider_name]
@@ -31,16 +55,20 @@ default_model = "model-name"
3155
```
3256

3357
支持三种认证方式:
34-
- `direct`: 凭据直接写在配置文件中
35-
- `env`: 从环境变量读取
36-
- `dotenv`: 从 .env 文件加载
58+
59+
| 认证类型 | api_key 含义 | 示例 |
60+
|---------|-------------|------|
61+
| `direct` | API 密钥本身 | `api_key = "sk-xxx..."` |
62+
| `env` | 环境变量名 | `api_key = "OPENAI_API_KEY"` |
63+
| `dotenv` | .env 文件中的键名 | `api_key = "API_KEY"` |
3764

3865
## 使用
3966

4067
### 查看可用的模型提供商
4168

4269
```sh
4370
doc2anki list
71+
doc2anki list --all # 包含已禁用的提供商
4472
```
4573

4674
### 验证配置
@@ -53,7 +81,7 @@ doc2anki validate -p provider_name
5381
### 生成卡片
5482

5583
```sh
56-
doc2anki generate input.md -p provider_name -o output.apkg
84+
doc2anki generate input.md -p provider_name
5785
```
5886

5987
处理整个目录:
@@ -62,12 +90,68 @@ doc2anki generate input.md -p provider_name -o output.apkg
6290
doc2anki generate docs/ -p provider_name -o knowledge.apkg
6391
```
6492

65-
常用选项:
66-
- `--max-tokens`: 每个文本块的最大 token 数量 (默认 3000)
67-
- `--deck-depth`: 从文件路径生成卡片组层级的深度 (默认 2)
68-
- `--extra-tags`: 添加额外标签,用逗号分隔
69-
- `--dry-run`: 仅解析和分块,不调用语言模型
70-
- `--verbose`: 显示详细输出
93+
### 命令行选项
94+
95+
**基本选项:**
96+
97+
| 选项 | 默认值 | 说明 |
98+
|-----|-------|------|
99+
| `-o, --output` | `outputs/output.apkg` | 输出文件路径 |
100+
| `-p, --provider` | (必需) | AI 提供商名称 |
101+
| `-c, --config` | (自动查找) | 配置文件路径 |
102+
| `--dry-run` | false | 仅解析分块,不调用 LLM |
103+
| `--verbose` | false | 显示详细输出 |
104+
105+
**分块控制:**
106+
107+
| 选项 | 默认值 | 说明 |
108+
|-----|-------|------|
109+
| `--chunk-level` | 自动检测 | 按指定标题级别分块 (1-6) |
110+
| `--max-tokens` | 3000 | 每个块的最大 token 数量 |
111+
| `--include-parent-chain` | true | 在提示词中包含标题层级路径 |
112+
113+
**卡片组织:**
114+
115+
| 选项 | 默认值 | 说明 |
116+
|-----|-------|------|
117+
| `--deck-depth` | 2 | 从文件路径生成卡组层级的深度 |
118+
| `--extra-tags` | (无) | 额外标签,逗号分隔 |
119+
120+
## 分块策略
121+
122+
### 自动检测
123+
124+
默认情况下,doc2anki 自动检测最佳分块级别:
125+
126+
1. 遍历各标题级别 (1-6)
127+
2. 计算每个级别的平均块大小和方差
128+
3. 选择满足以下条件的级别:
129+
- 至少产生 2 个块
130+
- 平均块大小在 500-2400 tokens 之间
131+
- 块大小分布均匀(标准差 < 平均值的 50%)
132+
133+
### 手动指定
134+
135+
对于特殊文档结构,可手动指定分块级别:
136+
137+
```sh
138+
# 按二级标题分块
139+
doc2anki generate input.md -p provider --chunk-level 2
140+
141+
# 按三级标题分块,更细粒度
142+
doc2anki generate input.md -p provider --chunk-level 3
143+
```
144+
145+
### 标题层级上下文
146+
147+
启用 `--include-parent-chain` (默认) 时,每个块会包含其在文档中的位置:
148+
149+
```
150+
## 内容位置
151+
当前内容在文档中的位置:网络基础 > TCP/IP > 三次握手
152+
```
153+
154+
这帮助 LLM 理解当前内容的上下文,生成更准确的卡片。
71155

72156
## 文档格式
73157

@@ -101,6 +185,24 @@ Org-mode 格式:
101185
- 卡片组: `computing::network` (深度为 2)
102186
- 标签: `computing`, `network`, `tcp_ip`
103187

188+
## 项目结构
189+
190+
```
191+
src/doc2anki/
192+
├── cli.py # 命令行接口
193+
├── config/ # 配置加载
194+
├── parser/ # 文档解析
195+
│ ├── tree.py # AST 数据结构
196+
│ ├── markdown.py # Markdown 解析
197+
│ └── orgmode.py # Org-mode 解析
198+
├── pipeline/ # 处理管道
199+
│ ├── classifier.py # 块类型分类
200+
│ ├── context.py # 上下文管理
201+
│ └── processor.py # 处理流程
202+
├── llm/ # LLM 调用
203+
└── output/ # APKG 生成
204+
```
205+
104206
## 许可证
105207

106208
MIT License

docs/architecture.md

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# 系统架构
2+
3+
## 数据流概览
4+
5+
```
6+
输入文档 (.md/.org)
7+
8+
9+
┌─────────────┐
10+
│ Parser │ 解析文档,提取全局上下文
11+
└─────────────┘
12+
13+
14+
┌─────────────┐
15+
│ Tree Builder│ 构建文档 AST (HeadingNode 树)
16+
└─────────────┘
17+
18+
19+
┌─────────────┐
20+
│ Pipeline │ 分块、分类、上下文管理
21+
└─────────────┘
22+
23+
24+
┌─────────────┐
25+
│ LLM Client │ 调用 AI 生成卡片
26+
└─────────────┘
27+
28+
29+
┌─────────────┐
30+
│ Output │ 生成 Anki .apkg 文件
31+
└─────────────┘
32+
```
33+
34+
## 模块职责
35+
36+
### Parser 模块 (`src/doc2anki/parser/`)
37+
38+
负责解析 Markdown 和 Org-mode 文档。
39+
40+
**核心组件:**
41+
42+
| 文件 | 职责 |
43+
|-----|------|
44+
| `base.py` | 定义 `ParseResult``BaseParser` 接口 |
45+
| `markdown.py` | Markdown 解析,提取 ` ```context`|
46+
| `orgmode.py` | Org-mode 解析,提取 `#+BEGIN_CONTEXT`|
47+
| `tree.py` | AST 数据结构:`HeadingNode`, `DocumentTree` |
48+
| `chunker.py` | Token 感知的分块逻辑 |
49+
50+
**AST 结构:**
51+
52+
```python
53+
@dataclass
54+
class HeadingNode:
55+
level: int # 标题级别 (1-6)
56+
title: str # 标题文本
57+
content: str # 该标题下的内容(不含子节点)
58+
children: list[HeadingNode]
59+
60+
@property
61+
def full_content(self) -> str:
62+
"""包含所有子节点的完整内容"""
63+
64+
@property
65+
def path(self) -> list[str]:
66+
"""标题层级路径: ["父标题", "当前标题"]"""
67+
68+
@dataclass
69+
class DocumentTree:
70+
children: list[HeadingNode] # 顶级标题
71+
preamble: str # 第一个标题前的内容
72+
73+
def get_nodes_at_level(self, level: int) -> list[HeadingNode]:
74+
"""获取指定级别的所有节点"""
75+
```
76+
77+
### Pipeline 模块 (`src/doc2anki/pipeline/`)
78+
79+
处理分块、分类和上下文管理。
80+
81+
**核心组件:**
82+
83+
| 文件 | 职责 |
84+
|-----|------|
85+
| `classifier.py` | 定义 `ChunkType` 枚举和 `ClassifiedNode` |
86+
| `context.py` | 定义 `ChunkWithContext` 数据结构 |
87+
| `processor.py` | 处理流程和自动检测算法 |
88+
89+
**块类型分类 (2x2 矩阵):**
90+
91+
```
92+
│ 加入上下文 │ 不加入上下文
93+
────────────────┼───────────┼──────────────
94+
生成卡片 │ FULL │ CARD_ONLY ← 默认
95+
不生成卡片 │ CONTEXT_ONLY │ SKIP
96+
```
97+
98+
| 类型 | 生成卡片 | 加入上下文 | 用途 |
99+
|------|---------|-----------|------|
100+
| `FULL` | Yes | Yes | 基础概念、定义、公理 |
101+
| `CARD_ONLY` | Yes | No | 独立知识点 (v1 默认) |
102+
| `CONTEXT_ONLY` | No | Yes | 背景铺垫、历史动机 |
103+
| `SKIP` | No | No | 可跳过的内容 |
104+
105+
**自动检测算法:**
106+
107+
```python
108+
def auto_detect_level(tree: DocumentTree, max_tokens: int) -> int:
109+
"""
110+
纯本地启发式算法 - 零 API 成本
111+
112+
策略:
113+
1. 遍历各标题级别 (1-6)
114+
2. 计算每个级别的节点数和平均 token 数
115+
3. 检查方差 - 如果过高,继续深入
116+
4. 选择满足条件的级别:
117+
- 至少 2 个块
118+
- 平均块大小在 500-2400 tokens
119+
- 分布均匀(标准差 < 平均值的 50%)
120+
"""
121+
```
122+
123+
### LLM 模块 (`src/doc2anki/llm/`)
124+
125+
处理与 AI 服务的交互。
126+
127+
**核心组件:**
128+
129+
| 文件 | 职责 |
130+
|-----|------|
131+
| `client.py` | OpenAI 兼容客户端,重试逻辑 |
132+
| `prompt.py` | Jinja2 模板渲染 |
133+
| `extractor.py` | 从响应中提取 JSON |
134+
135+
**模板加载:**
136+
137+
使用 `importlib.resources` 从包内加载模板,支持 pip 安装后使用:
138+
139+
```python
140+
class PackageLoader(BaseLoader):
141+
"""从 Python 包资源加载模板"""
142+
def get_source(self, environment, template):
143+
files = importlib.resources.files(self.package)
144+
source = (files / template).read_text(encoding="utf-8")
145+
return source, template, lambda: True
146+
```
147+
148+
### Config 模块 (`src/doc2anki/config/`)
149+
150+
管理配置加载和验证。
151+
152+
**配置解析链:**
153+
154+
1. 命令行 `--config` 参数
155+
2. `./config/ai_providers.toml`
156+
3. `~/.config/doc2anki/ai_providers.toml`
157+
158+
**认证类型:**
159+
160+
| 类型 | `api_key` 含义 |
161+
|------|---------------|
162+
| `direct` | API 密钥本身 |
163+
| `env` | 环境变量名 |
164+
| `dotenv` | .env 文件中的键名 |
165+
166+
### Output 模块 (`src/doc2anki/output/`)
167+
168+
生成 Anki 包文件。
169+
170+
- 使用 `genanki` 库创建 .apkg 文件
171+
- 支持基础卡片 (Q&A) 和填空卡片 (Cloze)
172+
- 根据文件路径自动生成卡组层级和标签
173+
174+
## 关于上下文累积的成本警告
175+
176+
`FULL``CONTEXT_ONLY` 类型会将内容追加到后续 API 调用的上下文中。
177+
178+
**风险:**
179+
180+
- **Token 成本爆炸**: N 个块的总消耗从 O(N) 变成 O(N²)
181+
- **效果劣化**: 上下文越长,LLM 对当前内容的注意力越分散
182+
- **长文档不可用**: 超过十几个块就会撞上 context window 上限
183+
184+
**设计决策:**
185+
186+
- v1 默认所有块为 `CARD_ONLY`(独立处理,无累积)
187+
- `FULL`/`CONTEXT_ONLY` 仅在未来的 interactive 模式下由用户显式选择
188+
- 详见 [future/interactive.md](future/interactive.md)

0 commit comments

Comments
 (0)