Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ dist/
*.sqlite3-shm
*.sqlite3-wal
*.db
*.jpg
*.jpeg
*.JPG
*.JPEG
*.png
*.PNG
*.heic
*.HEIC
*.pdf
*.PDF
local-data/
data/inbox/*
!data/inbox/.gitkeep
output/*
Expand Down
13 changes: 10 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

## 用户发来错题照片时

1. 用原生图片理解能力逐张查看。多题同页必须拆成多条记录;连续多图属于同一题时可合并理解,但每条记录保留来源页。收录范围取“明显错题/订正题”与“题号被圈出的题”的并集;题号被圈时即使未看到红笔订正也必须入库。
2. 忠实提取题干、选项、学生错误答案、正确答案和批注。所有公式写成 LaTeX:行内 `$...$`,独立公式 `$$...$$`。
1. 用原生图片理解能力逐张查看。多题同页必须拆成多条记录;连续多图属于同一题时可合并理解,但每条记录保留来源页。收录范围取“明显错题/订正题”与“题号被圈出的题”的并集;题号被圈时即使未看到红笔订正也必须入库。答案位置空白但旁边有明确对钩,且题号未圈、没有红笔订正或其他错误标记时,表示该题已经掌握,必须排除;禁止仅因没有手写答案就把它识别成错题。
2. 忠实提取题干、选项、学生错误答案、正确答案和批注。`wrong_answer` 只填写学生最终写出的具体答案;答题区为空白、只有思路但没有最终答案,或只有红笔写出的答案/订正但看不到可确认的原答案时统一写 `不会`,禁止写“待确认(见原图红笔答案或订正)”“思路错误”或作答过程描述。能看清原来的具体错误答案时忠实记录,作答过程错误只写入 `error_reason`。有答案或解析 PDF 时,`correct_answer` 必须写 PDF 给出的明确最终答案或结论,禁止写“见解析”“见标准解析”“待确认”等占位文本;`analysis` 必须忠实转写对应 PDF 原解,禁止沿用概括版或自行编写。若 PDF 对应关系或文字无法可靠确认,两个字段留空并停止自动确认,交由人工复核。选择题的 `correct_answer` 以 `A`、`B`、`C`、`D` 等裸字母开头,不写成 `(A)` 或 `(A)`;公式内部括号照常保留。所有公式写成 LaTeX:行内 `$...$`,独立公式 `$$...$$`。独立公式与前后正文之间只换一行,不留空白行;连续独立公式之间也不留空白行。
3. 科目只能是 `数学`、`英语`、`408`、`政治`。使用下方标准板块;看不清或不能确定时写 `待确认`,不能臆造。
4. 原图没有解析时,补出步骤完整、可独立理解的解析;指出具体错因,给出短知识点标签。
4. 有答案/标准解析照片或 PDF 时,`analysis` 必须按原解顺序忠实转写方法、计算和结论,禁止概括改写或用自己解法替代。只有原图确实没有解析时才补出步骤完整、可独立理解的解析,并标明“【补充解析】”。若标准解析看不清、缺页或对应不确定,停止自行解答,保留解析图、置为低置信度待复核。`error_reason` 只能根据照片中可见的学生解答过程填写具体错误;没有解答过程、只有题号被圈、答案被订正、未作答或只有标准解析时必须留空,禁止写“题号被圈或答案被红笔订正,需要对照标准解析复盘”等占位话术。黑笔圈题按用户约定保留“再做一次”。给出短知识点标签。
5. 按 `docs/extraction-example.json` 生成临时 JSON,用 Pydantic 校验并导入:

```bash
Expand All @@ -18,6 +18,12 @@
7. 自动识别结果默认 `needs_review: true`;只有逐字、公式、答案和解析都经过核对后才能设为 `false`。
8. 导入展示图片时使用 `normalize_display_image`:先应用 EXIF,再用 Tesseract OSD 校正文字方向;始终保留用户原始图片。复核界面左侧是校正后的派生副本,手动旋转仅用于兜底。

## 用户凭线索找旧题时

- 先运行 `.venv/bin/cuoti search "用户描述" --subject 数学 --limit 8`。检索会把中文拆词、数学写法归一并给候选题排序;`--json` 便于程序读取。不要只用完整句子的 SQL `LIKE`,那会漏掉题干与解析分别命中的线索。
- 候选结果只是定位线索,回答前须核对题目原图和标准解析图;题库文字含识别错误时,以原图为准。用户提供明确公式时,应要求候选题实际命中该公式;无命中就说明未找到,再查未入库照片或教材,不要把书中未收录的题误称为用户错题。
- 命令只读本机四科数据库,不上传个人数据;如确需建立索引或引入模型,先评估本机内存和隐私边界。

## 标准板块

- 数学:高等数学、线性代数、概率论与数理统计;`chapter` 写更细章节,`section` 写这三个板块之一。
Expand All @@ -37,6 +43,7 @@
- PDF 修改后必须导出两种版本,用 `pdftoppm` 转成 PNG 并目视检查公式、中文、图片、分页和留白。
- 每次改动运行 `./.venv/bin/pytest`;涉及界面时再做真实浏览器检查。
- 运行时产物只放 `data/inbox`、`tmp`、`output` 或各科 `错题_auto`,不要散落到源码目录。
- GitHub 只允许同步通用代码、通用文档和测试。个人错题数据及其元信息一律留在本机,包括照片、题目/答案内容、批次日期、原始文件名、题号清单、数据库记录 ID、JSON、Markdown 镜像、备份和导出文件;提交或推送前必须检查暂存区和 Git 跟踪文件,发现这些内容立即停止。

## 当前架构状态

Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- 原图对照复核、分类建议、公式与代码块渲染
- 后台生成纯题重做版和完整错题本版 PDF
- 原始照片与校正后的展示副本分离保存
- 本地中文线索检索:按题目描述查找候选错题,支持公式相关词与少量 OCR 字符误差

## 运行要求

Expand All @@ -34,11 +35,13 @@ cd cuoti-auto
./scripts/setup.sh
```

安装脚本会创建 `.venv`、安装 Python 与 npm 依赖、初始化四科数据库,并在 macOS 桌面创建“打开错题本.command”快捷方式。之后也可以手动启动:
安装脚本会创建 `.venv`、安装 Python 与 npm 依赖、初始化四科数据库,并在 macOS 桌面创建“打开错题本.command”快捷方式。macOS 上同时会安装用户级 LaunchAgent:登录后自动启动服务并打开一次浏览器,服务异常退出后会自动拉起。也可以手动管理:

```bash
.venv/bin/cuoti doctor
.venv/bin/cuoti serve
.venv/bin/cuoti service status
.venv/bin/cuoti service install
.venv/bin/cuoti service uninstall
```

浏览器会打开 <http://127.0.0.1:8765>。
Expand Down Expand Up @@ -91,6 +94,14 @@ export OPENAI_API_KEY="你的 API Key"
.venv/bin/cuoti import-json tmp/codex_batch.json --source "/绝对路径/原图.jpg"
```

### 凭描述找旧题

```bash
.venv/bin/cuoti search "微分方程 积分 平方" --subject 数学 --limit 8
```

检索结果按线索匹配程度排序;它帮助定位候选,最终仍需打开原图核对。

## PDF 导出

网页顶部“PDF 导出”浮窗会在后台创建任务并显示进度。也可以使用命令行:
Expand Down
24 changes: 24 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,30 @@

用户要求每科在对应桌面目录独立建库。网页查询时依次读取四个小库后合并,因此既满足物理隔离,也保留统一筛选。数据库启用 WAL 和外键;`source_hash + question_text` 唯一约束用于阻止同一照片同一题重复入库。

## 选项标签不变量

`questions.options_json` 只保存选项正文,不保存 `A.`、`B.` 等序号。导入、复核保存和数据库更新通过 `normalize_options` 去除与当前位置相符的历史前缀;网页、实时预览、Markdown 和 PDF 在渲染时通过 `option_label` 统一补回标签。空字符串会作为缺失选项的占位保留,渲染时隐藏但不改变后续字母;人工编辑中的普通空行则由 `parse_options_text` 忽略。这样可避免 `A. A. ...` 重复、选项错位,并保证所有出口编号一致。

`questions.correct_answer` 中位于字段开头的选择题字母只保存裸标签,例如 `C` 或 `C $O(n)$`,不保存 `(C)`、`(C)`。导入模型和数据库更新统一通过 `normalize_correct_answer` 清理开头标签;规则锚定字段开头,因此不会改动公式或正文内部的括号。

## 复核图片角色

`images.image_role` 区分 `question`(题目)、`work`(学生作答/订正)、`solution`(答案/标准解析)和待细分的 `supplement`。复核页将题目与作答照片放在前组、答案解析照片放在后组并提供快捷跳转;答案解析图永远不进入纯题 PDF 的可选图片列表。新增附图必须通过 `SubjectStore.add_image` 明确写入角色。

`analysis` 的内容来源遵循严格优先级:已关联的标准解析页 > 无标准解析时的补充解析。有标准解析页时只做忠实转写,不允许用人工摘要或模型自解替换书中的方法。无法确定页面对应或公式时,保留解析图并降为低置信度待复核,不得生成看似已核对的文字。

## 行间公式间距不变量

结构化富文本字段和选项在 Pydantic 导入、数据库更新及网页/PDF 渲染入口统一经过 `normalize_rich_text_spacing`。独立公式 `$$...$$` 与前后正文之间只保留一个换行,不能出现空白行;连续独立公式同样紧邻。Markdown 代码围栏内部不执行该规则,避免改变示例程序的原始格式。历史数据迁移脚本位于 `tmp/normalize_display_math_spacing.py`。

## 错因证据不变量

`questions.error_reason` 只记录能从学生解答过程确认的具体错误。题号圈选、红笔订正、未作答和标准解析只能决定收录或辅助讲解,不能据此推断错因;没有作答过程时字段保持空字符串。识别模型、结构化导入和复核保存通过 `normalize_error_reason` 清除已知的泛化占位话术,复核输入框保持为空。“再做一次”是用户指定的黑笔圈题复习标记,不属于待核对占位话术,予以保留。

## 错误答案不变量

`questions.wrong_answer` 只保存学生最终写出的具体答案,不保存“待确认(见原图红笔答案或订正)”“思路错误”等状态性占位话术,也不保存作答过程描述。识别到答题区为空白、只有思路未形成最终答案,或只有红笔答案/订正而无法确认学生原答案时统一存为“不会”;能看清具体原错误答案时仍忠实保存。作答过程错误写入 `error_reason`。结构化导入和复核保存统一通过 `normalize_wrong_answer` 执行基础规则。

## 可扩展点

新增 OCR 供应商时返回 `ExtractionBatch` 并加入 `ingest_path` 的适配器表即可。不要改变核心表来适配供应商。若以后增加复习算法,应写入 `attempts`,不要覆盖历史答案。
4 changes: 4 additions & 0 deletions docs/OPEN_SOURCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
- [Tesseract OCR](https://github.com/tesseract-ocr/tesseract):Apache-2.0;本机已有中英文语言包,作为无需下载模型的保底识别器。
- [KaTeX](https://github.com/KaTeX/KaTeX):MIT;本地离线渲染 LaTeX,用于网页与 PDF,避免公式依赖在线 CDN。
- [FastAPI](https://github.com/fastapi/fastapi):MIT;提供本地筛选、编辑和上传接口。
- [jieba](https://github.com/fxsjy/jieba):MIT;本地中文搜索模式分词,用于命令行线索检索。
- [RapidFuzz](https://github.com/rapidfuzz/RapidFuzz):MIT;本地容错匹配,用于 OCR 拼写或字符偏差的候选排序。
- [WeasyPrint](https://github.com/Kozea/WeasyPrint):BSD-3-Clause;把紧凑双栏 HTML 排版为 PDF。

检索方案评估:[SQLite FTS5](https://www.sqlite.org/fts5.html) 的 trigram 索引适合日后题量明显增大时加速中文片段查询;[sqlite-vec](https://github.com/asg017/sqlite-vec) 可在 SQLite 内存放向量,但还需要本地嵌入模型,当前不启用。现阶段题量下使用 jieba 和 RapidFuzz 对现有四库直接排序,避免多一份待同步索引和常驻模型。

选择原则:默认安装保持轻量;重型 OCR 通过适配器接入;识别结果统一进入同一 Pydantic schema;自动化结果必须可编辑和可追溯到原图。
31 changes: 30 additions & 1 deletion docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,41 @@ cd /path/to/cuoti-auto

如果端口被占用,可临时使用 `CUOTI_PORT=8877 .venv/bin/cuoti serve`。

### macOS 登录自启与常驻

```bash
.venv/bin/cuoti service install
.venv/bin/cuoti service status
.venv/bin/cuoti service uninstall
```

`install` 会在 `~/Library/LaunchAgents/` 安装两个用户级任务:`com.nemoyu.cuoti-auto` 常驻检查 Web 服务并在异常退出后自动拉起;`com.nemoyu.cuoti-auto.open` 每次登录只等待服务就绪并打开一次浏览器。由于 macOS 会禁止 launchd 直接读取桌面下的项目和数据库,监视器在需要时会通过 Terminal 的桌面访问权限启动后台 supervisor,无需给 Python 开启“完全磁盘访问”。日志保存在 `output/logs/`,重启服务不会反复弹出新标签页。

## 对比复核

- 首页点“开始对比复核”,左侧查看原始照片,右侧直接修改题干、选项、答案、解析和分类。
- 点“仅保存”会继续停留在当前题;点“确认无误”会将状态改为“已复核”并进入下一题。
- 误收题可点“删除本题”并二次确认。记录会从 SQLite、Markdown 和后续 PDF 中移除;删除前的 JSON 与派生图片会移到本科 `错题_auto/backups/`,原始照片不受影响。
- 展示副本在入库时会先应用 EXIF 方向,再用 Tesseract OSD 检测文字方向。原始照片不会被改动;复核页的左右旋转按钮是人工兜底。

## 按描述查找旧题

```bash
.venv/bin/cuoti search "微分方程 积分 平方" --subject 数学 --limit 8
.venv/bin/cuoti search "链表删除复杂度" --subject 408 --json
```

检索在本机执行:jieba 把中文描述切成线索,RapidFuzz 对 OCR 小错误做模糊匹配,常见数学词与 LaTeX 写法会一起参与排序。结果只列候选题,不自动改动原题或复核状态;先打开 Markdown、原图和解析图确认,再向用户报告。当前四科数据库规模小,无需生成向量或运行常驻检索服务。网页筛选框仍用于精确子串筛选。
当描述含有较长的明确公式片段(例如 `b+a/x+x`)时,该片段必须在候选记录中命中;若文字库没有收录对应公式,命令返回空结果,需再核对原图或请用户提供批次,而不能仅按宽泛知识点报告一个题号。

## 科目分页与 PDF 导出

- 四科使用独立页面:`/subject/math`、`/subject/english`、`/subject/cs408`、`/subject/politics`。科目通过顶部页签切换,筛选表单不再包含科目下拉框。
- 四科使用独立页面:`/subject/math`、`/subject/english`、`/subject/cs408`、`/subject/politics`。科目通过顶部页签切换,筛选表单不再包含科目下拉框。图片上传统一使用独立的 `/import` 页面,科目页不内嵌上传表单;导入完成后停留在导入页并提供对应科目与复核入口。
- 题库卡片、待复核队列和 PDF 导出共用统一顺序:科目 → 章节 → 小节/板块 → 题号。数字按自然数比较,因此第 2 章排在第 10 章之前;`待确认`内容放在末尾。
- 题库桌面端固定每行两题,选项横向排列且不叠加浏览器自动序号。点击题目后先显示题干、答案和解析,原图与编辑表单默认折叠在页底。
- 题号在网页、复核、Markdown 和 PDF 中同时显示建档日期。Markdown 三反引号代码块会在网页和 PDF 中渲染为独立代码区域;解答、应用、计算、证明等长题在 PDF 中独占一列。
- PDF 由单线程后台队列生成,不占用页面请求。顶部导航栏的“PDF 导出”浮窗轮询 `/api/exports/{job_id}`,用圆环显示进度、完成后显示对勾并提供下载。
- 公式密集的大批次会每 24 题启动一个独立 WeasyPrint 子进程,串行渲染后合并为一个 PDF;子进程结束即释放内存,避免数百道公式题使 Web 服务常驻数 GB 排版缓存。
- 完整错题本版只输出结构化题目、错误答案、正确答案、解析、错因和知识点,不带原始拍照页;纯题版只会带复核页中人工勾选的无答案题图。
- 任务记录保存在当前服务进程内,服务重启后旧任务进度会失效,但已生成的 PDF 仍保留在 `output/pdf/`。

Expand All @@ -49,6 +70,7 @@ cd /path/to/cuoti-auto
- 一页 1-3 题最利于题目切分;连续过程要按页码顺序命名。
- 批改符号、错误答案和正确答案都要入镜。反光严重或焦外的照片先重拍。
- 入库时同时查找订正痕迹和题号圈选:题号被圈出的题一律收录,与其是否能看清原错答无关。
- 答案位置为空但有明确对钩,且题号未圈、没有红笔订正或其他错误标记时,按“已经掌握”排除;空白本身不能作为错题证据。
- 自动结果进入“待复核”;详情页复核后改为“已复核”。
- 原始照片可能含答案,默认不进入纯题 PDF。几何图、材料图等无答案插图可在详情页勾选“纯题 PDF 图片”。

Expand All @@ -63,6 +85,13 @@ cd /path/to/cuoti-auto

停止写入后复制四个 `wrong_questions.sqlite3` 以及 `assets/`、`markdown/` 即可完整恢复。WAL 模式运行中备份时优先用 SQLite 在线备份 API,不要只复制主库而漏掉 `-wal`。

## 本地批次记录与隐私边界

- 批次 JSON、图片映射、审计结果和一次性修复脚本只保存在被 Git 忽略的 `tmp/` 中;题目照片、数据库、Markdown、备份和导出文件只保存在各科本地 `错题_auto` 或运行时目录中。
- 写入前使用 SQLite 在线备份。解析照片的 `image_role` 是 `solution`,不会进入纯题 PDF。
- 挂接 `solution` 图片不等于文字解析已核对。批量导入时必须逐题按标准解析页忠实转写 `analysis`;若页面缺失、对应不确定或公式无法辨认,应留空、降低置信度并交由人工复核,禁止改用自写摘要。
- GitHub 只同步通用程序、通用文档与测试。不得提交或推送批次日期、原始文件名、题号清单、记录 ID、题目/答案映射、个人路径、照片、数据库、Markdown 镜像或导出文件。

## 常见问题

- 网页公式还是 `$...$`:运行 `npm install`,确认 `/vendor/katex/katex.min.js` 能访问。
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,20 @@ classifiers = [
]
dependencies = [
"fastapi>=0.116,<1",
"jieba>=0.42,<1",
"jinja2>=3.1,<4",
"markdown>=3.8,<4",
"openai>=1.99,<3",
"pillow>=11,<13",
"pydantic>=2.11,<3",
"python-multipart>=0.0.20,<1",
"rapidfuzz>=3.14,<4",
"uvicorn>=0.35,<1",
"weasyprint>=66,<70",
]

[project.optional-dependencies]
dev = ["httpx>=0.28,<1", "pytest>=8.4,<10"]
dev = ["httpx>=0.28,<1", "pypdf>=6,<7", "pytest>=8.4,<10"]

[project.scripts]
cuoti = "cuoti.cli:main"
Expand Down
Loading
Loading