create-ccc-tutor 0.1.0 → 0.3.0
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/README.md +14 -1
- package/bin/cli.js +70 -9
- package/package.json +1 -1
- package/template/.claude/commands/exam.md +13 -0
- package/template/.claude/commands/slide.md +24 -5
- package/template/.claude-plugin/plugin.json +13 -26
- package/template/.codex/skills/exam/SKILL.md +13 -0
- package/template/.codex/skills/slide/SKILL.md +27 -3
- package/template/.harness/scripts/pdf-rag.sh +40 -0
- package/template/.harness/scripts/pdf_rag.py +485 -0
- package/template/.harness/scripts/requirements-pdf.txt +6 -0
- package/template/.harness/scripts/tests/test_pdf_rag.py +228 -0
- package/template/.harness/state/install.json +1 -1
- package/template/constitution.md +1 -1
- package/template/course/README.md +1 -1
- package/template/docs/features/pdf-vision-implementation.md +109 -0
- package/template/docs/features/pdf-vision.md +226 -0
- package/template/docs/features/slide-query-implementation.md +2 -2
- package/template/docs/features/slide-query.md +2 -0
- package/template/gitignore +4 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""pdf-vision 引擎测试。覆盖 spec 中脚本层可测的 [Required automated test] 场景。
|
|
2
|
+
|
|
3
|
+
不依赖大模型下载:默认用关键词模式(PDF_RAG_FORCE_KEYWORD=1)。跨语言语义检索
|
|
4
|
+
单测在 fastembed 可用时才跑(importorskip)。需要 pymupdf 生成 fixture PDF。
|
|
5
|
+
"""
|
|
6
|
+
import importlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
14
|
+
pymupdf = pytest.importorskip("pymupdf") # 生成 fixture 需要;缺失则整文件跳过
|
|
15
|
+
|
|
16
|
+
import pdf_rag # noqa: E402
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture(autouse=True)
|
|
20
|
+
def isolate(tmp_path, monkeypatch):
|
|
21
|
+
"""每个测试隔离缓存/课件目录、强制关键词模式、重置嵌入状态。"""
|
|
22
|
+
monkeypatch.setattr(pdf_rag, "CACHE_ROOT", str(tmp_path / "cache"))
|
|
23
|
+
monkeypatch.setattr(pdf_rag, "COURSE_ROOT", str(tmp_path / "course"))
|
|
24
|
+
monkeypatch.setenv("PDF_RAG_FORCE_KEYWORD", "1")
|
|
25
|
+
monkeypatch.setenv("PDF_RAG_DISABLE_OCR", "1")
|
|
26
|
+
pdf_rag._embed_state.update({"tried": False, "model": None, "lib_version": "none",
|
|
27
|
+
"mode": "keyword"})
|
|
28
|
+
yield
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _make_pdf(path, pages_text):
|
|
32
|
+
"""pages_text: list[str|None];None=空白页(无文字、无图)。"""
|
|
33
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
34
|
+
doc = pymupdf.open()
|
|
35
|
+
for txt in pages_text:
|
|
36
|
+
pg = doc.new_page()
|
|
37
|
+
if txt:
|
|
38
|
+
pg.insert_text((72, 72), txt, fontsize=14)
|
|
39
|
+
doc.save(path)
|
|
40
|
+
doc.close()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _slide(subject):
|
|
44
|
+
d = pdf_rag.slide_dir(subject)
|
|
45
|
+
os.makedirs(d, exist_ok=True)
|
|
46
|
+
return d
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
SUBJ = "TEST-101"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ── spec 3.12:文件夹 reconcile(增 / 删 / 未变)──────────────────────────────
|
|
53
|
+
def test_reconcile_add_remove_unchanged():
|
|
54
|
+
d = _slide(SUBJ)
|
|
55
|
+
_make_pdf(os.path.join(d, "Lecture 1 Intro.pdf"),
|
|
56
|
+
["hypothesis testing type one error alpha"])
|
|
57
|
+
r1 = pdf_rag.reconcile(SUBJ, pdf_rag.DEFAULT_DPI)
|
|
58
|
+
assert "Lecture 1 Intro.pdf" in r1["added"]
|
|
59
|
+
|
|
60
|
+
r2 = pdf_rag.reconcile(SUBJ, pdf_rag.DEFAULT_DPI) # 没动 → unchanged
|
|
61
|
+
assert "Lecture 1 Intro.pdf" in r2["unchanged"]
|
|
62
|
+
assert r2["added"] == [] and r2["removed"] == []
|
|
63
|
+
|
|
64
|
+
_make_pdf(os.path.join(d, "Lecture 2 RV.pdf"), ["random variable distribution"])
|
|
65
|
+
r3 = pdf_rag.reconcile(SUBJ, pdf_rag.DEFAULT_DPI) # 新增 → added
|
|
66
|
+
assert "Lecture 2 RV.pdf" in r3["added"]
|
|
67
|
+
|
|
68
|
+
os.remove(os.path.join(d, "Lecture 1 Intro.pdf"))
|
|
69
|
+
r4 = pdf_rag.reconcile(SUBJ, pdf_rag.DEFAULT_DPI) # 删除 → removed
|
|
70
|
+
assert "Lecture 1 Intro.pdf" in r4["removed"]
|
|
71
|
+
out = pdf_rag.query(SUBJ, "type one error alpha", 5, pdf_rag.DEFAULT_DPI)
|
|
72
|
+
files = {r["source_file_exact"] for r in out["results"]}
|
|
73
|
+
assert "Lecture 1 Intro.pdf" not in files # 删了不再被引用(忠实性)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ── spec 3.5:内容哈希失效 ────────────────────────────────────────────────────
|
|
77
|
+
def test_cache_invalidation_on_content_change():
|
|
78
|
+
d = _slide(SUBJ)
|
|
79
|
+
p = os.path.join(d, "Lecture 3.pdf")
|
|
80
|
+
_make_pdf(p, ["alpha beta gamma original content"])
|
|
81
|
+
pdf_rag.reconcile(SUBJ, pdf_rag.DEFAULT_DPI)
|
|
82
|
+
_make_pdf(p, ["totally different replaced content delta"]) # 同名换内容
|
|
83
|
+
r = pdf_rag.reconcile(SUBJ, pdf_rag.DEFAULT_DPI)
|
|
84
|
+
assert "Lecture 3.pdf" in r["changed"]
|
|
85
|
+
out = pdf_rag.query(SUBJ, "delta replaced content", 3, pdf_rag.DEFAULT_DPI)
|
|
86
|
+
assert not out["miss"] # 命中的是新内容
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ── spec 3.5 红线:处理指纹失效(改 dpi 等)────────────────────────────────────
|
|
90
|
+
def test_cache_invalidation_on_fingerprint_change():
|
|
91
|
+
d = _slide(SUBJ)
|
|
92
|
+
_make_pdf(os.path.join(d, "Lecture 4.pdf"), ["confidence interval"])
|
|
93
|
+
pdf_rag.reconcile(SUBJ, 150)
|
|
94
|
+
m1 = pdf_rag.load_manifest(SUBJ)
|
|
95
|
+
assert m1["fingerprint"]["render_dpi"] == 150
|
|
96
|
+
r = pdf_rag.reconcile(SUBJ, 220) # dpi 变 → 整体重建
|
|
97
|
+
m2 = pdf_rag.load_manifest(SUBJ)
|
|
98
|
+
assert m2["fingerprint"]["render_dpi"] == 220
|
|
99
|
+
assert "Lecture 4.pdf" in r["added"] # 旧档作废,等同重新建
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ── spec 3.6:检索未命中不勉强答 ──────────────────────────────────────────────
|
|
103
|
+
def test_retrieval_miss():
|
|
104
|
+
d = _slide(SUBJ)
|
|
105
|
+
_make_pdf(os.path.join(d, "Lecture 5.pdf"), ["regression slope intercept"])
|
|
106
|
+
pdf_rag.reconcile(SUBJ, pdf_rag.DEFAULT_DPI)
|
|
107
|
+
out = pdf_rag.query(SUBJ, "完全无关的问题zzzqqq", 5, pdf_rag.DEFAULT_DPI)
|
|
108
|
+
assert out["miss"] is True
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ── spec 3.2:嵌入不可用降级为关键词,文字问题仍可答 ──────────────────────────
|
|
112
|
+
def test_keyword_degradation():
|
|
113
|
+
d = _slide(SUBJ)
|
|
114
|
+
_make_pdf(os.path.join(d, "Lecture 6.pdf"), ["paired sample t test design"])
|
|
115
|
+
out = pdf_rag.query(SUBJ, "paired sample design", 5, pdf_rag.DEFAULT_DPI)
|
|
116
|
+
assert out["mode"] == "keyword"
|
|
117
|
+
assert not out["miss"]
|
|
118
|
+
assert out["results"][0]["source_file_exact"] == "Lecture 6.pdf"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ── spec 3.10:部分可读 —— 空白/纯图页标 visual_only,不连累整份文件 ───────────
|
|
122
|
+
def test_partial_readable_page_level():
|
|
123
|
+
d = _slide(SUBJ)
|
|
124
|
+
_make_pdf(os.path.join(d, "Lecture 7.pdf"),
|
|
125
|
+
["Page one has real text about variance and standard deviation "
|
|
126
|
+
"in statistics, well above the scanned-page threshold.", None]) # 第2页空白
|
|
127
|
+
pdf_rag.reconcile(SUBJ, pdf_rag.DEFAULT_DPI)
|
|
128
|
+
m = pdf_rag.load_manifest(SUBJ)
|
|
129
|
+
pages = m["files"]["Lecture 7.pdf"]["pages"]
|
|
130
|
+
assert m["files"]["Lecture 7.pdf"]["indexed_complete"] is True # 整份没被丢
|
|
131
|
+
assert pages[0]["visual_only"] is False
|
|
132
|
+
assert pages[1]["visual_only"] is True # 空白页单独标
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ── spec 3.4:建档中断(incomplete)不被检索使用 ──────────────────────────────
|
|
136
|
+
def test_interrupted_index_not_used():
|
|
137
|
+
d = _slide(SUBJ)
|
|
138
|
+
_make_pdf(os.path.join(d, "Lecture 8.pdf"), ["interrupted indexing content xyz"])
|
|
139
|
+
pdf_rag.reconcile(SUBJ, pdf_rag.DEFAULT_DPI)
|
|
140
|
+
m = pdf_rag.load_manifest(SUBJ)
|
|
141
|
+
m["files"]["Lecture 8.pdf"]["indexed_complete"] = False # 模拟中断
|
|
142
|
+
pdf_rag._atomic_write_json(pdf_rag.subject_paths(SUBJ)["manifest"], m)
|
|
143
|
+
pdf_rag._rebuild_vectors(SUBJ, m)
|
|
144
|
+
with open(pdf_rag.subject_paths(SUBJ)["chunks"], encoding="utf-8") as f:
|
|
145
|
+
chunks = json.load(f)
|
|
146
|
+
assert all(c["file"] != "Lecture 8.pdf" for c in chunks) # 半截档不入检索
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── spec 3.7:多版本出处精确 ──────────────────────────────────────────────────
|
|
150
|
+
def test_exact_file_provenance():
|
|
151
|
+
d = _slide(SUBJ)
|
|
152
|
+
_make_pdf(os.path.join(d, "Lecture11 Regression WK.pdf"), ["regression WK version"])
|
|
153
|
+
_make_pdf(os.path.join(d, "Lecture11 Regression TW.pdf"), ["regression TW version"])
|
|
154
|
+
pdf_rag.reconcile(SUBJ, pdf_rag.DEFAULT_DPI)
|
|
155
|
+
out = pdf_rag.query(SUBJ, "regression TW version", 5, pdf_rag.DEFAULT_DPI)
|
|
156
|
+
assert out["results"][0]["source_file_exact"] == "Lecture11 Regression TW.pdf"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ── spec 3.5:渲染 PNG 随内容变化被清除(不返回旧图)────────────────────────
|
|
160
|
+
def test_render_cache_cleared_on_content_change():
|
|
161
|
+
d = _slide(SUBJ)
|
|
162
|
+
p = os.path.join(d, "Lecture 10.pdf")
|
|
163
|
+
_make_pdf(p, ["original page text long enough to index properly here xyz"])
|
|
164
|
+
pdf_rag.reconcile(SUBJ, 150)
|
|
165
|
+
base = pdf_rag.subject_paths(SUBJ)["base"]
|
|
166
|
+
slug = pdf_rag.load_manifest(SUBJ)["files"]["Lecture 10.pdf"]["slug"]
|
|
167
|
+
png = pdf_rag.render_page(p, 1, 150, os.path.join(base, slug))
|
|
168
|
+
assert png and os.path.exists(png)
|
|
169
|
+
_make_pdf(p, ["completely different replaced page content here now abcdef"])
|
|
170
|
+
pdf_rag.reconcile(SUBJ, 150)
|
|
171
|
+
assert not os.path.exists(png) # 旧 PNG 已随重建被清除
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ── spec 3.5:处理指纹变化清掉整个科目缓存(旧 PNG 不残留)──────────────────
|
|
175
|
+
def test_fingerprint_change_clears_cache():
|
|
176
|
+
d = _slide(SUBJ)
|
|
177
|
+
p = os.path.join(d, "Lecture 12.pdf")
|
|
178
|
+
_make_pdf(p, ["confidence interval estimation content long enough here"])
|
|
179
|
+
pdf_rag.reconcile(SUBJ, 150)
|
|
180
|
+
base = pdf_rag.subject_paths(SUBJ)["base"]
|
|
181
|
+
slug = pdf_rag.load_manifest(SUBJ)["files"]["Lecture 12.pdf"]["slug"]
|
|
182
|
+
png = pdf_rag.render_page(p, 1, 150, os.path.join(base, slug))
|
|
183
|
+
assert os.path.exists(png)
|
|
184
|
+
pdf_rag.reconcile(SUBJ, 240) # dpi 变 → 整体清缓存
|
|
185
|
+
assert not os.path.exists(png)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ── spec 3.6:关键词降级下常见词不致误判命中 ──────────────────────────────────
|
|
189
|
+
def test_keyword_stopwords_no_false_hit():
|
|
190
|
+
d = _slide(SUBJ)
|
|
191
|
+
_make_pdf(os.path.join(d, "Lecture 13.pdf"),
|
|
192
|
+
["The regression line is what models the data in this lecture."])
|
|
193
|
+
# 问句只由停用词 + 与课件无关的内容词组成 → 不应命中
|
|
194
|
+
out = pdf_rag.query(SUBJ, "what is the area of a triangle", 5, pdf_rag.DEFAULT_DPI)
|
|
195
|
+
assert out["miss"] is True
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ── spec 3.7:slug 碰撞隔离(不同文件名同 slug 不互相串/误删)──────────────────
|
|
199
|
+
def test_slug_collision_isolation():
|
|
200
|
+
d = _slide(SUBJ)
|
|
201
|
+
_make_pdf(os.path.join(d, "Lecture 1.pdf"),
|
|
202
|
+
["alpha content about variance dispersion measure one here"])
|
|
203
|
+
_make_pdf(os.path.join(d, "Lecture_1.pdf"),
|
|
204
|
+
["beta content about regression slope coefficient two here"])
|
|
205
|
+
pdf_rag.reconcile(SUBJ, pdf_rag.DEFAULT_DPI)
|
|
206
|
+
m = pdf_rag.load_manifest(SUBJ)
|
|
207
|
+
assert m["files"]["Lecture 1.pdf"]["slug"] != m["files"]["Lecture_1.pdf"]["slug"]
|
|
208
|
+
os.remove(os.path.join(d, "Lecture 1.pdf")) # 删一个不应影响另一个
|
|
209
|
+
pdf_rag.reconcile(SUBJ, pdf_rag.DEFAULT_DPI)
|
|
210
|
+
out = pdf_rag.query(SUBJ, "regression slope coefficient", 5, pdf_rag.DEFAULT_DPI)
|
|
211
|
+
assert not out["miss"]
|
|
212
|
+
assert out["results"][0]["source_file_exact"] == "Lecture_1.pdf"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ── spec 3.11:中文问、英文课件 —— 跨语言语义检索(需 fastembed,否则跳过)────
|
|
216
|
+
def test_cross_lingual_retrieval(monkeypatch):
|
|
217
|
+
pytest.importorskip("fastembed")
|
|
218
|
+
monkeypatch.delenv("PDF_RAG_FORCE_KEYWORD", raising=False)
|
|
219
|
+
pdf_rag._embed_state.update({"tried": False, "model": None, "mode": "keyword"})
|
|
220
|
+
d = _slide(SUBJ)
|
|
221
|
+
_make_pdf(os.path.join(d, "Lecture 9.pdf"),
|
|
222
|
+
["The Type I error is rejecting a true null hypothesis."])
|
|
223
|
+
pdf_rag.reconcile(SUBJ, pdf_rag.DEFAULT_DPI)
|
|
224
|
+
out = pdf_rag.query(SUBJ, "第一类错误是什么", 5, pdf_rag.DEFAULT_DPI)
|
|
225
|
+
if out["mode"] != "semantic":
|
|
226
|
+
pytest.skip("embedding model unavailable offline")
|
|
227
|
+
assert out["results"][0]["source_file_exact"] == "Lecture 9.pdf"
|
|
228
|
+
assert not out["miss"]
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"spec_dir": "docs/features/",
|
|
31
31
|
"implementation_dir": "docs/features/",
|
|
32
32
|
|
|
33
|
-
"project_audience": "
|
|
33
|
+
"project_audience": "使用本工具复习课程的学生",
|
|
34
34
|
"project_non_goals": ["不替代课件本身", "不回答课件之外或无依据的问题", "不编造课件中不存在的内容"],
|
|
35
35
|
"project_compliance": "none",
|
|
36
36
|
"project_performance_floor": "暂无正式性能底线",
|
package/template/constitution.md
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# 课件读图与快速查找 — Implementation Notes
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/features/pdf-vision.md (canonical, CEO domain)
|
|
4
|
+
**This file:** technical detail, manager domain.
|
|
5
|
+
|
|
6
|
+
## 0. 设计要旨
|
|
7
|
+
|
|
8
|
+
本项目无后端、无应用服务进程——`/slide`、`/exam` 是 prompt 驱动的 AI 技能(Claude 版 `.claude/commands/slide.md`、Codex 版 `.codex/skills/slide/SKILL.md`)。因此「RAG / 视觉」不是一个常驻服务,而是**一套模型无关、可被 CLI 调用、产物落在磁盘的预处理 + 检索工具**,两个模型各自的技能去调它、再把结果套用统一的「出处 + 置信度」规则。
|
|
9
|
+
|
|
10
|
+
视觉模型本身就是 CV 引擎:不自建区域分类器 / 网格检测 / 多策略表格管线(那是过度工程,已在 Stage 1 砍掉)。渲染整页交给模型读,模型自行聚焦区域。
|
|
11
|
+
|
|
12
|
+
## 1. 组件总览
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
预处理(每个 PDF 一次,按内容哈希+处理指纹缓存)— 纯脚本,无需视觉模型(混合检索,见 §3 决定 B):
|
|
16
|
+
抽每页文字(文字过少且有 OCR → OCR 取文字)──┐
|
|
17
|
+
探测每页是否含图(PyMuPDF get_images)──────┤→ 切块 + 嵌入(页文字,多语言模型)
|
|
18
|
+
渲染每页 PNG(按需,回答阶段看图用)─────────┤→ 落盘:向量 + 元数据(pdf,page,visual_flag,visual_only,source_file_exact)
|
|
19
|
+
|
|
20
|
+
查询(每次提问):
|
|
21
|
+
embed(问题) → cosine top-K → [(pdf,page,type,score)...]
|
|
22
|
+
→ 加载命中页的缓存文字
|
|
23
|
+
→ 命中页含 visual_flag 或问题涉及图 → 加载/按需渲染该页 PNG
|
|
24
|
+
→ 组织带出处+置信度回答 → 回答前自检
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 检索策略决定(Stage 4 Tech Lead 决定:混合检索)
|
|
28
|
+
|
|
29
|
+
「图/表如何变得可被语义检索」有两条路:
|
|
30
|
+
- **A. 建档时让视觉模型给每张图写文字描述再嵌入** —— 检索精度最高,但建档时必须调用模型,慢且需交互;本项目无 API key、模型是交互式 CLI,建档批量跑不现实。
|
|
31
|
+
- **B. 混合检索(采用)** —— 建档**纯脚本无需模型**:嵌入每页文字(含图注/周边文字)+ 标记该页是否含图(PyMuPDF `page.get_images()` 探测)。查询命中含图页时,由**回答阶段**的视觉模型(Claude 原生 Read / Codex `-i` PNG)现场看图读取。图凭其页文字/图注被检索到,真正读图发生在回答时。
|
|
32
|
+
|
|
33
|
+
决定 B。理由:建档无模型依赖、快、可批量;仍满足「两侧模型都看图」与「完整语义检索(文字向量)」。**视觉描述回填缓存**(首次看图后把模型描述写回索引以提升后续检索)列为**后续增强**,v1 不做。这是对 CEO「完整 embedding RAG」决定的具体实现路径,不改变 CEO spec 的任何用户可见行为。
|
|
34
|
+
|
|
35
|
+
## 2. 渲染层
|
|
36
|
+
|
|
37
|
+
- 工具:**PyMuPDF (`pip install pymupdf`,import 名 `fitz`)**。纯 Python、不依赖系统 poppler(环境里 pdftoppm/pdftocairo/magick/convert 全部缺失,已确认)。
|
|
38
|
+
- 渲染:`page.get_pixmap(dpi=<render_dpi>)` → PNG。
|
|
39
|
+
- 缓存 key:`pdf_path + file_sha256 + page_number + render_dpi`。
|
|
40
|
+
- 缓存目录:`.cache/pdf-vision/<subject>/<pdf-slug>/page-NNN.png`(+ `.text.txt`、`.visual.json`)。
|
|
41
|
+
- 按需渲染:查询命中页才渲染,不预渲染全部。
|
|
42
|
+
|
|
43
|
+
## 3. 多模态索引层(embedding RAG)
|
|
44
|
+
|
|
45
|
+
- **嵌入模型**:默认本地 `fastembed`(ONNX 运行时,无 torch、无 API key)。**必须用多语言模型**——本项目中文问、英文课件,纯英文模型跨语言检索失效(Codex Stage4 阻断项1)。选 `intfloat/multilingual-e5-small`(384 维,fastembed 支持,~400–470MB,中英可用)。e5 系列需前缀约定:文档块嵌 `"passage: <text>"`、查询嵌 `"query: <question>"`,否则召回质量下降。首次运行联网拉模型一次,之后离线。CEO 可 veto 改联网 API。
|
|
46
|
+
- **扫描/纯图页可检索性**(Codex Stage4 阻断项1,spec §3.1/§3.11):建档时若某页可抽取文字过少(疑似扫描/纯图),尝试对渲染出的 PNG 做 OCR(PyMuPDF `page.get_textpage_ocr()` 需系统装 Tesseract)取文字再嵌入;OCR 不可用时,该页以「文件名/讲次标题 + 相邻页文字」入索引并标 `visual_only=true`,当该讲次为命中主题时一并呈现给模型现看。完全无 OCR 的纯扫描件检索较弱,属已知限制,回答时如实说明(§3.2 诚实原则)。
|
|
47
|
+
- **向量存储**:磁盘 numpy `.npy`(向量矩阵)+ 并列 JSON(元数据)。语料规模十几个 PDF / 几千 chunk,无需 Chroma/FAISS 等重型向量库。检索 = 内存内 cosine top-K。
|
|
48
|
+
- **切块**:页级为主,长页按段再切;每 chunk 元数据 `{pdf, page, type:text|figure|table|chart, visual_flag, source_file_exact}`。
|
|
49
|
+
- **视觉可检索性(混合检索,见上文决定 B)**:建档只嵌入页文字(含图注/周边文字)+ 标 `visual_flag`;图凭页文字被检索到,回答阶段由视觉模型现看 PNG 读取。视觉描述回填缓存为后续增强,v1 不做。
|
|
50
|
+
- **降级**:fastembed 不可用(下载失败 / 安装失败)→ 退回关键词检索(对缓存文字做 grep/BM25 级匹配),并向用户明示「语义检索未启用」。
|
|
51
|
+
|
|
52
|
+
## 4. 两侧模型如何调用(模型无关接口)
|
|
53
|
+
|
|
54
|
+
- 共享脚本:核心 `.harness/scripts/pdf_rag.py`(Python,下划线便于 pytest import)+ 包装 `.harness/scripts/pdf-rag.sh`。子命令:
|
|
55
|
+
- `index --subject <s>`:建/更新档(幂等,按哈希跳过已建)。
|
|
56
|
+
- `query --subject <s> "<question>" --k <K>`:返回 top-K `[(pdf,page,type,score,text_path,png_path?)]`(JSON)。
|
|
57
|
+
- `render --pdf <f> --page <n>`:按需渲染并返回 PNG 路径。
|
|
58
|
+
- **Claude 端**:`/slide`、`/exam` 技能调 `query` 拿命中页 → 用 Read 工具读 PDF 对应页(Claude 的 Read 对 PDF 是视觉渲染,已能看图)→ 套出处/置信度规则。
|
|
59
|
+
- **Codex 端**:技能调 `query` → 对含图页调 `render` 得 PNG → `codex` 以 `-i <png>` 喂图(已确认 codex-cli 0.130.0 支持 `-i/--image`)→ 套同一套规则。
|
|
60
|
+
|
|
61
|
+
## 5. 原子性 / 缓存正确性(对应 spec 3.4 / 3.5)
|
|
62
|
+
|
|
63
|
+
- 建档写 `*.tmp` 再 `rename`,避免半截索引被读(spec 3.4)。
|
|
64
|
+
- 缓存有效性 = **课件内容 + 生成方式都未变**。每个 PDF 缓存记 `{file_sha256, indexed_complete:bool, render_dpi, embed_model_id, embed_model_version, vision_prompt_version, pipeline_schema_version}`;查询前比对**全部这些字段**,任一不一致即失效重建(spec 3.5 红线)。仅比对 `file_sha256` 不够——换嵌入模型 / 改看图 prompt / 改渲染 dpi 都会让旧向量与旧图文描述变成「过期但 PDF 未变」的脏数据。
|
|
65
|
+
- 中断恢复:`indexed_complete=false` 的条目下次重建。
|
|
66
|
+
- **每次查询前的文件夹核对(spec 2.4 / 3.12)**:`query` 启动时先对该科目 slide 文件夹做**轻量 reconcile**——列当前 PDF 集合,每个文件先用 `(filename, size, mtime)` 快速比对索引 manifest:
|
|
67
|
+
- 索引有、文件夹没了 → 删除该文件的全部向量+元数据条目(绝不再引用已删除来源)。
|
|
68
|
+
- 文件夹有、索引没有 → 新建该文件的档。
|
|
69
|
+
- `(size|mtime)` 变了 → 再算 `file_sha256` 确认,变了才重建(避免无谓重读)。
|
|
70
|
+
- 全部未变 → 直接用现成档。
|
|
71
|
+
reconcile 只在「真有增删改」时触发读 PDF / 嵌入;否则是纯目录列举,毫秒级,不损速度。
|
|
72
|
+
- **页级粒度**(spec 3.10):缓存与纳入判断按页记录 `{page, text_ok, render_ok, vision_ok}`;坏页单独标注,不连累整份文件。
|
|
73
|
+
- **图答新鲜度**(spec 2.2 / 3.3 advisory):回答涉及图时,answer-time 校验对应 PNG 可重新打开;不可打开则明示「该页图无法复核」,不得拿缓存 `visual.json` 描述当现读视觉证据。
|
|
74
|
+
- **降级分级**(spec 3.2 红线):嵌入工具不可用→关键词检索仅服务文字类问题;渲染/看图能力不可用→视觉类问题直接返回「看图能力不可用、无法作答」,禁止纯文字降级冒充。
|
|
75
|
+
|
|
76
|
+
## 6. 功能需求(EARS notation)
|
|
77
|
+
|
|
78
|
+
- **WHEN** 用户对某科目首次提问 **THE SYSTEM SHALL** 对该科目未建档或哈希已变的 PDF 执行建档(抽文字 / 文字过少则 OCR + 探测含图页 + 嵌入页文字,纯脚本无 index-time 视觉描述),再回答;看图发生在回答阶段。
|
|
79
|
+
- **WHEN** 一个 PDF 的内容哈希与缓存记录不一致 **THE SYSTEM SHALL** 失效旧缓存并重建该 PDF 的档。
|
|
80
|
+
- **WHEN** 查询的 top-K 最高相似度低于阈值 **THE SYSTEM SHALL** 视为「课件未命中」,走「先问再补充」流程,**SHALL NOT** 将低相关结果当权威答案。
|
|
81
|
+
- **WHEN** 某页缺文字层 **THE SYSTEM SHALL** 渲染该页为图并以视觉方式读取;**IF** 连图也不可读 **THEN THE SYSTEM SHALL** 报告该文件未纳入并用其余课件继续。
|
|
82
|
+
- **WHEN** 嵌入工具不可用 **THE SYSTEM SHALL** 降级为关键词检索并明示用户语义检索未启用。
|
|
83
|
+
- **WHEN** 对图表数值的精确值无直接标注 **THE SYSTEM SHALL** 将该数值标注为估算 / 看不清,**SHALL NOT** 输出伪精确读数。
|
|
84
|
+
- **WHEN** 文字层与视觉读取冲突 **THE SYSTEM SHALL** 并列两者各自出处并指出不一致,**SHALL NOT** 自行裁决。
|
|
85
|
+
- **WHEN** 同一讲存在多版本文件 **THE SYSTEM SHALL** 在出处中点名确切文件名。
|
|
86
|
+
|
|
87
|
+
## 7. External dependencies
|
|
88
|
+
|
|
89
|
+
- `pymupdf`(PDF→PNG 渲染 + 含图探测 + 可选 OCR;纯 Python)
|
|
90
|
+
- `fastembed`(本地嵌入,ONNX;多语言 `intfloat/multilingual-e5-small`,首次下载 ~400–470MB)—— 或可替换为联网 embedding API(需 key)
|
|
91
|
+
- `numpy`(向量存储与 cosine 计算)
|
|
92
|
+
- 既有:`pdftotext` / `pypdf`(文字层抽取,保留)
|
|
93
|
+
- 可选系统依赖:Tesseract(扫描/纯图页 OCR;缺失则按 §3 兜底降级)
|
|
94
|
+
- `codex` CLI ≥ 0.130.0 `-i/--image`(Codex 看图通道,已确认)
|
|
95
|
+
|
|
96
|
+
## 8. File locations(规划)
|
|
97
|
+
|
|
98
|
+
- `.harness/scripts/pdf_rag.py` + `.harness/scripts/pdf-rag.sh`(共享检索/建档/渲染 CLI)
|
|
99
|
+
- `.cache/pdf-vision/`(缓存产物,应进 `.gitignore`)
|
|
100
|
+
- 改动:`.claude/commands/slide.md`、`.claude/commands/exam.md`、`.codex/skills/slide/SKILL.md`、`.codex/skills/exam/SKILL.md`(接入 query/render + 出处/置信度规则)
|
|
101
|
+
- 同步:`docs/features/slide-query.md` 及其 implementation(视觉/检索规则并入既有铁律)
|
|
102
|
+
|
|
103
|
+
## 9. Boundary contracts
|
|
104
|
+
|
|
105
|
+
- 与既有 `/slide`、`/exam` 的「只依据课件、找不到先问、每条标出处」铁律完全兼容:本功能只增强「找得更快 + 看得见图」,不放松任何忠实性约束。检索未命中 → 复用现有「情况 E」流程。
|
|
106
|
+
|
|
107
|
+
## 10. Scenario → automated test map
|
|
108
|
+
|
|
109
|
+
(Stage 6 填入;此处先留空 stub)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Feature: 课件读图与快速查找(PDF 看图 + 加速查询)
|
|
2
|
+
|
|
3
|
+
## Status: FINALIZED 2026-06-13
|
|
4
|
+
|
|
5
|
+
## 1. What this feature is for
|
|
6
|
+
|
|
7
|
+
学生用本工具复习时,提一个问题,工具要又快又准地从课件里找到答案——不只读课件上的文字,还要能看懂课件里的图片、示意图、表格、柱状图、饼图,并且每条结论都标明来自哪个课件、第几页、有多确定。**Claude 和 Codex 两种助手都要具备这个能力。** 同时,提问时不再每次把所有课件从头读一遍,而是先快速定位到相关的几页,所以反应更快。
|
|
8
|
+
|
|
9
|
+
## 2. Happy path
|
|
10
|
+
|
|
11
|
+
### 2.1 问一个涉及文字的问题
|
|
12
|
+
|
|
13
|
+
用户提问 → 工具快速定位到最相关的课件页 → 读这几页内容 → 回答,并标注 `(Lecture N, 第M页)`。
|
|
14
|
+
|
|
15
|
+
### 2.2 问一个涉及图 / 表的问题
|
|
16
|
+
|
|
17
|
+
用户问「某张图说明什么」「某个表里的数值是多少」「哪个柱子更高」 → 工具找到对应页 → 看那页的图 / 表 → 回答内容、标注出处,并说明数值是**精确读到的**还是**估算的**。
|
|
18
|
+
|
|
19
|
+
回答涉及图 / 表时,必须绑定到**具体哪一页的图**;如果回答时那一页的图已经无法重新打开 / 无法复核,要明说「该页图当前无法复核」,绝不拿之前缓存下来的图文描述当成「刚刚看到的」来回答。
|
|
20
|
+
|
|
21
|
+
### 2.3 第一次使用某科目
|
|
22
|
+
|
|
23
|
+
第一次对一个科目提问时,工具先把该科目课件整理建档(读文字、看图、做索引)一次;之后再提问就直接用建好的档,很快。课件没变就不会重复建档。
|
|
24
|
+
|
|
25
|
+
### 2.4 每次提问前快速核对课件文件夹
|
|
26
|
+
|
|
27
|
+
每次提问前,工具会**快速看一眼**该科目课件文件夹(只看有哪些文件、有没有变动——看文件名 / 大小 / 修改时间,**不重新读 PDF 内容**):
|
|
28
|
+
- 有**新增**的课件 → 补建它的档
|
|
29
|
+
- 有被**删除**的课件 → 把它从档里移除(之后绝不再引用一个已经不在文件夹里的文件)
|
|
30
|
+
- 有**内容改动**的课件 → 重建那一份(见 3.5)
|
|
31
|
+
- 文件夹没变动 → 直接用现成的档(所以仍然快)
|
|
32
|
+
|
|
33
|
+
## 3. Edge-case behavior
|
|
34
|
+
|
|
35
|
+
### 3.1 课件读不出文字(扫描件 / 加密 / 损坏)
|
|
36
|
+
|
|
37
|
+
#### Behavior (CEO sign-off)
|
|
38
|
+
- 当某个课件抽不出文字层
|
|
39
|
+
- 工具不再直接跳过,而是**把该页当成图片来看**,用看图能力读取内容
|
|
40
|
+
- 只有当连图也读不出(严重失真 / 加密无法打开)时,才告诉用户「文件 xxx 读不出、本次未纳入」,用其余课件继续
|
|
41
|
+
|
|
42
|
+
#### Classification
|
|
43
|
+
[Required automated test]
|
|
44
|
+
|
|
45
|
+
#### Smoke test procedure
|
|
46
|
+
**Reproduce:** 放一个扫描版(纯图片)PDF 进 slide 文件夹,问一个该 PDF 里图上才有的内容。
|
|
47
|
+
**Pass criteria:** 工具能读出图上的内容并标出处,而不是说「读不出、未纳入」。
|
|
48
|
+
**Failure signals:** 直接跳过该文件 / 编造内容。
|
|
49
|
+
|
|
50
|
+
### 3.2 首次建档需要联网下载,却下载失败
|
|
51
|
+
|
|
52
|
+
#### Behavior (CEO sign-off)
|
|
53
|
+
- 当首次整理课件需要的工具下载失败(无网 / 中断)
|
|
54
|
+
- 工具**自动退回到「按关键词查找」**,并明确告诉用户「语义检索未启用,已用关键词查找代替」
|
|
55
|
+
- 整个功能不崩溃,**对纯文字能回答的问题**仍能基于课件回答
|
|
56
|
+
- **但关键词查找只对文字类问题有效**:如果用户问的是「这张图说明什么」这种**必须看图才能回答**的问题,而看图能力此刻不可用,工具必须明说「看图能力当前不可用,这个需要看图才能回答的问题无法从课件作答」,**绝不用纯文字答案冒充已经回答了图的问题**
|
|
57
|
+
|
|
58
|
+
#### Classification
|
|
59
|
+
[Required automated test](命中项目核心:忠实性)
|
|
60
|
+
|
|
61
|
+
#### Smoke test procedure
|
|
62
|
+
**Reproduce:** 断网后第一次对新科目提问(先问一个文字问题,再问一个只能看图回答的问题)。
|
|
63
|
+
**Pass criteria:** 文字问题提示已降级为关键词查找仍能基于课件回答;看图问题明确说明「看图能力不可用、无法作答」,而不是给个纯文字答案假装答了。
|
|
64
|
+
**Failure signals:** 报错卡死;或对看图问题给出纯文字答案却不说明图没看到。
|
|
65
|
+
|
|
66
|
+
### 3.3 某页的图没看成功
|
|
67
|
+
|
|
68
|
+
#### Behavior (CEO sign-off)
|
|
69
|
+
- 当某一页的看图步骤失败
|
|
70
|
+
- 该页**退回只用文字**回答,并在涉及该页图的部分标「该页图未读出」
|
|
71
|
+
- 不假装看到了图
|
|
72
|
+
|
|
73
|
+
#### Classification
|
|
74
|
+
[Smoke test only](属 AI 回答措辞,人工验收)
|
|
75
|
+
|
|
76
|
+
### 3.4 建档过程被中途打断
|
|
77
|
+
|
|
78
|
+
#### Behavior (CEO sign-off)
|
|
79
|
+
- 当整理课件建档的过程被中断(关窗 / 中止)
|
|
80
|
+
- 下次提问时,工具识别出该课件**没建完**,重新建它
|
|
81
|
+
- **绝不使用残缺的档**来回答
|
|
82
|
+
|
|
83
|
+
#### Classification
|
|
84
|
+
[Required automated test]
|
|
85
|
+
|
|
86
|
+
### 3.5 课件被替换或改动,但文件名没变(最关键)
|
|
87
|
+
|
|
88
|
+
#### Behavior (CEO sign-off)
|
|
89
|
+
- 当一个课件文件内容变了(被新版本覆盖),即使文件名一样
|
|
90
|
+
- 工具能**察觉内容变了**,自动重新建档
|
|
91
|
+
- **绝不拿旧档回答新内容**
|
|
92
|
+
- 失效判断不只看课件内容:当**生成这份档所用的方式变了**(例如换了整理工具 / 换了看图方式 / 改了页面转图设置),即使课件文件一个字没改,旧档也算过期、必须重建。换句话说,旧档只有在「**课件内容 + 当初生成它用的工具和设置都没变**」时才算有效,否则不能拿来当当前课件证据
|
|
93
|
+
|
|
94
|
+
#### Classification
|
|
95
|
+
[Required automated test]
|
|
96
|
+
|
|
97
|
+
#### Smoke test procedure
|
|
98
|
+
**Reproduce:** 对某科目提问一次(建好档)→ 用另一份内容不同的 PDF 覆盖同名文件 → 再问同一处内容。
|
|
99
|
+
**Pass criteria:** 第二次回答反映的是新内容。
|
|
100
|
+
**Failure signals:** 第二次仍答旧内容。
|
|
101
|
+
|
|
102
|
+
### 3.6 没找到相关内容 / 相关度过低
|
|
103
|
+
|
|
104
|
+
#### Behavior (CEO sign-off)
|
|
105
|
+
- 当查找没有命中足够相关的课件内容
|
|
106
|
+
- **不许把勉强沾边的结果当权威答案**
|
|
107
|
+
- 落回现有铁律:先明确说「课件里没有这个内容」,再问用户要不要用课件外知识补充,用户同意后才补充并整段标注外部来源
|
|
108
|
+
|
|
109
|
+
#### Classification
|
|
110
|
+
[Required automated test](命中项目核心:忠实性)
|
|
111
|
+
|
|
112
|
+
### 3.7 同一讲有多个版本文件
|
|
113
|
+
|
|
114
|
+
#### Behavior (CEO sign-off)
|
|
115
|
+
- 当同一讲存在多个版本(例如 `Lecture11_..._WK`、`..._WK_L2`、`..._TW`)
|
|
116
|
+
- 出处必须**点名确切的文件**,不能只说「Lecture 11」含糊带过
|
|
117
|
+
|
|
118
|
+
#### Classification
|
|
119
|
+
[Required automated test]
|
|
120
|
+
|
|
121
|
+
### 3.8 问图里没有标注的精确数字
|
|
122
|
+
|
|
123
|
+
#### Behavior (CEO sign-off)
|
|
124
|
+
- 当用户问一个图表里没有直接标注数字的精确值
|
|
125
|
+
- 工具必须标明这是**估算 / 看不清**,绝不假装是精确读数
|
|
126
|
+
- 能判断相对大小(更高 / 更低 / 大约)时就只给相对判断
|
|
127
|
+
|
|
128
|
+
#### Classification
|
|
129
|
+
[Smoke test only](属 AI 回答措辞,人工验收;命中忠实性,须在冒烟必测)
|
|
130
|
+
|
|
131
|
+
#### Smoke test procedure
|
|
132
|
+
**Reproduce:** 问一张没有数字标签的柱状图「A 的值是多少」。
|
|
133
|
+
**Pass criteria:** 回答说明这是按刻度估算 / 或只给相对大小,并标「非精确读数」。
|
|
134
|
+
**Failure signals:** 给出一个看似精确的数字却不标估算。
|
|
135
|
+
|
|
136
|
+
### 3.9 文字内容与图表内容冲突
|
|
137
|
+
|
|
138
|
+
#### Behavior (CEO sign-off)
|
|
139
|
+
- 当课件文字层说的和图 / 表里读到的不一致
|
|
140
|
+
- **两个都列出来、各标出处**,明确指出「两处不一致」
|
|
141
|
+
- 不替课件裁决哪个对
|
|
142
|
+
|
|
143
|
+
#### Classification
|
|
144
|
+
[Smoke test only](属 AI 回答措辞,人工验收;命中忠实性,须在冒烟必测)
|
|
145
|
+
|
|
146
|
+
### 3.10 同一份 PDF 里部分页能读、部分页坏
|
|
147
|
+
|
|
148
|
+
#### Behavior (CEO sign-off)
|
|
149
|
+
- 当一份课件里有的页能正常读(文字 / 图都行)、有的页坏(读不出文字也渲染不出图)
|
|
150
|
+
- 工具**按页处理**:能读的页正常纳入,坏的页单独标「第 X 页未读出」
|
|
151
|
+
- 一页坏**不连累整份文件**——既不能因为一页坏就丢掉整份课件,也不能因为整份大体能读就把坏页的内容当成已读
|
|
152
|
+
|
|
153
|
+
#### Classification
|
|
154
|
+
[Required automated test]
|
|
155
|
+
|
|
156
|
+
### 3.11 中文提问、英文课件(中英混合)
|
|
157
|
+
|
|
158
|
+
#### Behavior (CEO sign-off)
|
|
159
|
+
- 当用户用中文提问,而课件是英文(本项目正是这种情况:中文问 + 英文 ECON 课件)
|
|
160
|
+
- 查找和出处标注必须**跨中英文都管用**,能凭中文问题找到对应英文课件内容
|
|
161
|
+
- **不确定的地方仍要标不确定**,绝不因为做了中英转换就把「不确定」说成「确定」
|
|
162
|
+
|
|
163
|
+
#### Classification
|
|
164
|
+
[Required automated test]
|
|
165
|
+
|
|
166
|
+
### 3.12 课件文件夹里增加或删除了文件
|
|
167
|
+
|
|
168
|
+
#### Behavior (CEO sign-off)
|
|
169
|
+
- 当用户往科目文件夹里**新放了一个 PDF** → 下次提问前的快速核对会发现它,补建它的档,使它能被检索到
|
|
170
|
+
- 当用户**删掉了一个 PDF** → 下次提问前的快速核对会发现它没了,把它从档里移除;**之后绝不再把已删除的文件当作课件来源引用**
|
|
171
|
+
- 这个核对每次提问前都做,但只做轻量比对(文件名 / 大小 / 修改时间),没变动就不重读内容、不影响速度
|
|
172
|
+
|
|
173
|
+
#### Classification
|
|
174
|
+
[Required automated test](命中项目核心:忠实性——不得引用已不存在的来源)
|
|
175
|
+
|
|
176
|
+
#### Smoke test procedure
|
|
177
|
+
**Reproduce:** 对某科目建好档并问过一次 → 删掉其中一个 PDF → 再问一个只有那个 PDF 才答得出的问题;另外再放进一个新 PDF → 问新 PDF 里的内容。
|
|
178
|
+
**Pass criteria:** 删掉的那个文件不再被引用(答"课件里没有");新放的文件能被找到并引用。
|
|
179
|
+
**Failure signals:** 仍引用已删除文件的内容;或找不到新加入的文件。
|
|
180
|
+
|
|
181
|
+
### 3.13 科目下没有任何课件
|
|
182
|
+
|
|
183
|
+
#### Behavior (CEO sign-off)
|
|
184
|
+
- 当科目的 slide 文件夹是空的
|
|
185
|
+
- 提示用户先把 PDF 放进去,然后停止
|
|
186
|
+
|
|
187
|
+
#### Classification
|
|
188
|
+
[Smoke test only]
|
|
189
|
+
|
|
190
|
+
## 4. Who can use this
|
|
191
|
+
|
|
192
|
+
- 本地学习工具,无登录、无账号
|
|
193
|
+
- 使用者即本人,无访问限制
|
|
194
|
+
- 不依赖任何外部授权
|
|
195
|
+
|
|
196
|
+
## 5. External dependencies (plain language)
|
|
197
|
+
|
|
198
|
+
- 需要「把课件页面转成图片」的能力(本地软件,一次性安装)
|
|
199
|
+
- 需要「把文字变成可比较相似度」的整理工具(本地、支持中文问英文课件的多语言版本;首次运行联网下载一次、约几百 MB,之后离线)
|
|
200
|
+
- (可选)一个「认图上文字」的能力,用来让扫描版/图片页也能被搜到;没有它时这类页靠所在讲次被找到、并在回答时看图(属较弱检索,会如实说明)
|
|
201
|
+
- 当「把文字变成可比较相似度」的工具不可用时,退化为「只按关键词查找」回答**文字类问题**,功能不挂
|
|
202
|
+
- 但当「把课件页面转成图片 / 看图」的能力不可用时,**只能看图回答的问题不做纯文字降级**——明确告诉用户这个问题此刻无法从课件作答(见 3.2)
|
|
203
|
+
|
|
204
|
+
## 6. Deferred / unresolved
|
|
205
|
+
|
|
206
|
+
- 整理工具用本地还是联网在线服务:当前默认**本地**(免密钥、离线);CEO 可改成联网在线服务(需要你提供一个访问密钥)
|
|
207
|
+
- 检索质量调优、是否需要更细的切块策略:等课件量变大后再评估
|
|
208
|
+
|
|
209
|
+
## 7. Out of scope
|
|
210
|
+
|
|
211
|
+
- 课件之外的内容生成
|
|
212
|
+
- 修复课件本身的错误或低清图
|
|
213
|
+
- 在没有刻度 / 缺图例的情况下硬给精确数值
|
|
214
|
+
- 嵌入式语义检索的进一步性能优化(属未来独立工作)
|
|
215
|
+
|
|
216
|
+
## 8. Decision history
|
|
217
|
+
|
|
218
|
+
- 2026-06-13 — CEO 选择「完整语义检索(按意思找,不只按字面关键词)」,覆盖 Tech Lead「先用轻量关键词索引」的简化建议。理由:CEO 希望检索质量一步到位。Tech Lead 记录:对当前十几个 PDF 的规模这是偏重的方案,但 CEO 拍板优先检索质量(宪法 §3,STRONG 原则「简单优先」被 CEO 带理由覆盖)。
|
|
219
|
+
- 2026-06-13 — 整理工具默认本地(免密钥、离线),Tech Lead 内部技术决定,理由:项目无后端、未配在线服务密钥。CEO 可 veto 改联网。
|
|
220
|
+
- 2026-06-13 — Phase 1 范围 = 看图看表 + 缓存检索地基一起做(同一套基础设施,分开做等于重复铺地基)。
|
|
221
|
+
- 2026-06-13 — 课件读不出文字时由「跳过」改为「当图片看」,理由:满足「让两个模型都看懂图」的核心目标。
|
|
222
|
+
- 2026-06-13 — Codex 审查(Stage 1 第一轮,FAIL/风险9)后采纳全部 5 项:①看图能力不可用时禁止纯文字降级冒充作答(红线)②缓存失效须同时考虑「读法/工具/设置变更」非仅课件内容(红线)③部分可读 PDF 按页处理 ④图答须绑定页、无法复核时明示 ⑤中英混合检索与不确定标注。理由:均属忠实性约束,两条红线按宪法§3不可否决。
|
|
223
|
+
- 2026-06-13 — CEO 提出:每次提问前应快速核对课件文件夹,新增文件补建、删除文件移除(不只处理「同名内容改动」)。新增 §2.4 与边界场景 §3.12。理由:避免引用已删除来源(忠实性),并让新课件即时可用;核对为轻量比对(名/大小/时间),不损速度。
|
|
224
|
+
- 2026-06-13 — Codex 审查计划(Stage 4 第一轮 FAIL/风险9)后修订:①嵌入模型由纯英文改为**多语言版**(解决中文问英文课件的检索;下载体积增至约几百 MB,§5 已更新)②扫描/纯图页增加可选 OCR 检索路径 + 含图标记兜底(§3.1/§5)③测试覆盖补齐 3.1、3.11(脚本层可测)④将 3.3、3.8、3.9 由 [Required automated test] **改为 [Smoke test only]**——理由:三者属「AI 回答措辞」(图未读出标注、估算标注、文图冲突并列),真自动化需模型在测试回路、本环境不可靠;改由 skill 提示词强制 + 回答前自检 + CEO 冒烟必测保障。Tech Lead 建议、CEO 可否决恢复为自动测试。
|
|
225
|
+
|
|
226
|
+
## 9. (Audit mode only — leave empty in new-feature mode) Code vs spec delta
|
|
@@ -15,7 +15,7 @@ No build step, no runtime, no dependencies beyond Claude Code / Codex itself.
|
|
|
15
15
|
|
|
16
16
|
## Multi-subject architecture (2026-06-10)
|
|
17
17
|
|
|
18
|
-
- Materials live under `course/<subject>/slide/`; problem files under `course/<subject>/exam/` (sibling feature `/exam`). One folder per subject (e.g. `
|
|
18
|
+
- Materials live under `course/<subject>/slide/`; problem files under `course/<subject>/exam/` (sibling feature `/exam`). One folder per subject (e.g. `ECON-10005`, `COMP-5990`).
|
|
19
19
|
- **Subject resolution** (same logic in slide + exam, Claude + Codex):
|
|
20
20
|
1. `ls -d course/*/` to enumerate subjects.
|
|
21
21
|
2. Read current session subject from `.harness/state/current-subject.txt` (may not exist).
|
|
@@ -50,7 +50,7 @@ Replaces the old "never supplement" red line. When the answer is absent from the
|
|
|
50
50
|
|
|
51
51
|
- Source folder: `course/<subject>/slide/`. Claude renders PDFs with the Read tool (`pages` param). Codex has no built-in PDF rendering — it extracts text via `pdftotext -layout` or `pypdf` in shell.
|
|
52
52
|
- Reading: Claude Code's Read tool can render PDF pages. Per-request page limits apply (the tool reads up to ~20 pages/request and requires an explicit page range for large PDFs).
|
|
53
|
-
- ~10 PDFs × 1.5–3.5MB each. Reading all pages of all PDFs for every query is potentially heavy. **
|
|
53
|
+
- ~10 PDFs × 1.5–3.5MB each. Reading all pages of all PDFs for every query is potentially heavy. **This deferred decision is now RESOLVED by the `pdf-vision` feature (2026-06-13):** a shared `.harness/scripts/pdf-rag.sh` builds a per-subject embedding index (cached by content hash + processing fingerprint) and answers `query` with the relevant pages, so the assistant no longer scans all PDFs per question. Visual pages are read by the host model at answer time (Claude native Read / Codex `codex exec -i <png>`). See `docs/features/pdf-vision.md` (CEO spec) and `docs/features/pdf-vision-implementation.md` (mechanism). The `/slide` and `/exam` command/skill files now call `pdf-rag.sh query` first, with the old `pdftotext`/Read path retained as a fallback when the engine is unavailable.
|
|
54
54
|
|
|
55
55
|
## Citation derivation
|
|
56
56
|
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
下文 1–9 节里凡是写“课件文件夹 / 课件/”的,均指当前科目的 `course/<科目>/slide/`。
|
|
14
14
|
|
|
15
|
+
**视觉与快速检索(2026-06-13,见姊妹功能 `pdf-vision`)**:本功能的「读图/表/图表」能力与「按意思快速定位相关页、不再每次翻遍所有课件」的加速,由 `docs/features/pdf-vision.md` 治理(Claude 与 Codex 两侧都具备)。既有的忠实性铁律(只依据课件、找不到先问再补充、每条标出处、绝不编造)完全不变;pdf-vision 只增强「找得更快 + 看得见图」,并对视觉结论同样要求出处、估算/看不清标注。
|
|
16
|
+
|
|
15
17
|
## 1. What this feature is for
|
|
16
18
|
|
|
17
19
|
给正在复习课程的同学用的一个命令。用户在课程文件夹里启动助手后,先确定科目,再输入 `/slide` 加上自己的问题,助手只翻阅该科目 `course/<科目>/slide/` 里的课程材料来回答,并且每次回答都标明出处(第几讲、第几页)。它的价值在于:复习时能快速从一堆课件里找到答案,而且每个说法都能追溯到原始课件,绝不凭空编造——让用户可以放心地信任答案。课件查不到时,先如实告知再按需补充。
|