scan-exam-to-docx — 扫描试卷 → 空白卷 + 解析卷
把「手机拍的/扫描仪扫的」试卷 PDF,整理成老师能直接打印给学生做、并配套讲评的两份 Word。
适用范围:初中阶段(七至九年级)英语学科试卷,九大题型(单项填空 / 完形填空 / 阅读理解 / 补全对话 / 词汇运用 / 完成句子 / 任务型阅读 / 综合填空 / 书面表达)。小学与高中卷的版式、题型差异较大,不在覆盖范围内;其他学科需自行扩展 builder(见下文)。
硬性前置规则(违反必翻车)
- 先看
venv_path.txt(skill 目录下,由scripts/setup_env.py在部署成功时生成)。文件存在且路径有效 → 用里面的解释器调用所有脚本;不存在 → 先跑一次部署。不要向系统 Python 直接pip install。 - 页面方向必须人工目检,
preprocess_pages.py的自动判据(宽高比 / FFT 行投影)在双栏页和插图页上会误判,历史上三种自动判据全部失效过。正确做法见 Stage 1 的 probe 流程。 - bootstrap 的草稿禁止直接渲染。
bootstrap_curated.py只产出骨架,validate_curated.py会对此报 ERROR。 - 渲染前必跑
validate_curated.py,渲染后必跑selftest.py(若该管卷已有 golden 基线)。 - 渲染器不做任何文本猜测。
build_docx.py唯一职责是按 schema 排版;所有语义修正在 Stage 4 精校阶段完成。
首次部署(只需一次)
python <skill_dir>/scripts/setup_env.py # venv 落在 <skill_dir>/.venv
python <skill_dir>/scripts/setup_env.py --venv-dir "E:\ai\ocr_venv" # 放别处省 C 盘
约 300–400 MB(含 OCR 模型),联网一次,之后永久离线可用。看到 SETUP DONE 即可。
五段式流水线
① 预处理 PDF → 方向校正 → 对开拆分 → 裁边去歪斜 → page_map.json
② OCR 本地 RapidOCR(PP-OCRv6) → pNN.ocr.json
③ 版面还原 碎片合并成阅读顺序的行 → pNN.txt(人读对照底稿)
④ ★精校 逐页读图 + 对照 OCR 文本 → curated.json (唯一智能环节)
⑤ 渲染 curated.json → 空白试卷.docx + 答案解析.docx
> 中间产物一律落在工作区的 _work/<stage>/<paper_id>/,OCR 只跑一次并固化;后续改错只改 JSON,秒级重渲染,不重跑 OCR。
目录约定
<工作区>/
├── scripts/ # 本 skill 的 scripts 副本(可选,便于改)
├── _work/
│ ├── preprocessed/<paper_id>/pNN.jpg + page_map.json
│ ├── ocr/<paper_id>/pNN.ocr.json + summary.json
│ ├── layout/<paper_id>/pNN.lines.json + pNN.txt
│ └── curated/<paper_id>.json # ★ 精校唯一真源
└── output/<试卷名>/
├── <试卷名>_空白试卷.docx
└── <试卷名>_答案解析.docx
Stage 1 — 预处理
# 1) 出四方向候选图,逐个目检,确定这一册用的是哪个角度
$PY scripts/preprocess_pages.py --pdf "xxx.pdf" --outdir _work/preprocessed/<id> \
--dpi 300 --text-rotate probe
# 2) 用目检确认的角度全量跑(同一册只用这一个角度)
$PY scripts/preprocess_pages.py --pdf "xxx.pdf" --outdir _work/preprocessed/<id> \
--dpi 300 --text-rotate 270
验收:全部输出页 h > w;无扫描黑边;行尾字符完整;page_map.json 记录了 src_page / split / rotation 便于溯源。
> probe 模式会输出 probe_rot000/090/180/270.jpg,用 Read 工具逐张看,标题横排且位于顶部即为正确方向。
> --text-rotate auto 默认会硬退出(exit 2),这是故意的,不要绕过。 行投影 FFT 的幅值
> 对 180° 旋转不变,数学上就分不开 0° 与 180°,于是 family_ambiguous 几乎恒为 True;
> 实测某册 10 页里有 9 页被判成 90°(真实 0°),且四方向得分只差 0.02。判不准就停在这里,
> 比静默烧掉整册 OCR 便宜。确知风险仍要跑,加 --allow-ambiguous,警告会写进 page_map.json。
Stage 2 — 本地 OCR
$(cat venv_path.txt) scripts/ocr_local.py --images _work/preprocessed/<id> --outdir _work/ocr/<id>
输出 summary.json:每页行数、置信度中位数、低置信占比。验收参考:置信度中位数 ≥ 0.95,低置信(<0.85)行 ≤ 15%(不满足通常提示预处理没做好,而不是 OCR 的问题)。
识别模型可选(--model,默认 v6-small,检测器始终是 PP-OCRv6-small,只换识别器):
| preset | 实际模型 | 适用 |
|---|---|---|
v6-small |
PP-OCRv6/small/lang=ch | 默认。印刷体准确率与英文词间空格都最好 |
v5-server-ch |
PP-OCRv5/server/lang=ch | 只作交叉验证;英文明显退化且大量丢空格 |
v5-en-mobile |
PP-OCRv5/mobile/lang=en | 只认英文,中文会全毁(含中文的卷不能用) |
⚠ 两个实测结论(勿凭直觉替换):① PP-OCRv5 没有 en-server,server 只给 lang=ch;② v5 在整页比对中文本一致率仅 68–72%(v6 为基线),换全卷只会更差。v5 的唯一价值是在「被裁切/低置信的印刷碎片」上给第二个读数。
Stage 3 — 版面还原
$PY scripts/layout.py --ocr-dir _work/ocr/<id> --out _work/layout/<id>
产出 pNN.txt(人读版,精校时的对照底稿)与 pNN.lines.json(带坐标,给 bootstrap 用)。
Stage 3.5 — 手写排雷(★已作答卷必做,否则泄题)
学生手写的答案、勾选、批注会被 OCR 当成正文混进 pNN.txt。空白卷里出现任何手写内容都等于泄题。
三个工具配合,先定位、再放大、最后交叉验证:
# ① 定位:给每个 OCR 框打"疑似手写"分,只提示、绝不改动 OCR 结果
$PY scripts/ink_scan.py --ocr-dir _work/ocr/<id> --images _work/preprocessed/<id> \
--out _work/ink/<id>
# -> pNN.suspect.json(bbox+特征+理由)、pNN.marked.jpg(红框标注图)、suspect_report.md
# ② 放大:看不清的地方裁切放大(也可直接读 suspect 的裁剪图)
$PY scripts/zoom_crop.py --image _work/preprocessed/<id>/p07.jpg \
--bbox 0,2280,2113,3163 --scale 2 --out _work/zoom/p07.png
# ③ 交叉验证:对 suspect 框只跑"识别"(不重新检测),拿第二个模型的读数
$PY scripts/hw_crosscheck.py --ocr-dir _work/ocr/<id> --images _work/preprocessed/<id> \
--ink-dir _work/ink/<id> --out _work/hw/<id> \
--model v5-server-ch
# -> crosscheck.md(SAME/DIFF/EMPTY/DOUBT 四标签 + crops/*.png)
判读规则(硬性):
| ink_scan / crosscheck 结果 | 处理 |
|---|---|
| 确认是手写(答案字母、√×、圈划、下划线、笔记) | 一律不进空白卷;学生答案可写进该题 answer_note 作参考 |
| 确认是印刷体(SAME,且回图看不到笔迹) | 照录 |
| 手写压住印刷字 | 先 zoom_crop 放大;能推断 → {{还原\|原卷此处被手写涂盖,依上下文还原}};不能推断 → 留空并在 analysis 说明 |
| 手写框上各模型读数互相矛盾 | 以裁剪图目检为准,模型读数不作依据(handwriting OCR 本来就不可靠) |
⚠ 常见误判:正确的选项词(如 strong / others / height)在原文里合法出现,verify_output 会报"泄漏候选",
需逐条回图判读,不要直接删原文。
Stage 4 — 逐页精校(★决定成败)
这一步没法自动化,也不该自动化。 做法是:打开 _work/preprocessed/<id>/pNN.jpg 逐页读,对照同页的 pNN.txt,把正确的卷面内容写进 curated.json。
可以先跑 bootstrap 拿骨架(省转录量),但它只减免打字,每一条仍需回到图纸核对:
$PY scripts/bootstrap_curated.py --layout-dir _work/layout/<id> \
--paper-id <id> --out _work/curated
必须逐句核对的六类语义缺陷(正则修不出来,历史上全部出过):
| 缺陷 | 例子 |
|---|---|
| 行首空位丢失 | OCR 把行首 ______ 吞掉 |
| 选项粘连 | A. longB. beautifulC. narrow |
| 答句未分行 | 对话题的问句与答句挤在一行,且缺 — |
| 机械换行当段落 | 页边距换行被当成自然段,语篇支离破碎 |
| 答案混入题干 | 学生手写"Not yet"被识别进题干(严重泄题) |
| 题号误切 | 5.1. → 51,.57. → 57,March, 2020. → 第 20 题 |
留痕语法 —— 任何对识别结果的修改都必须写成:
"stem": ["{{听 Listen to|选项 A 残缺,依据图像还原}} your parents."]
渲染为:红色文本 + 一条 Word 真实批注(内容=理由)+ 文末《题目整理说明》表一行。三者同源,缺一不可。
> 参照 samples/reference_curated.json(一份 85 题的完整真卷,含 120 处红色标记与 60 条批注)。
Stage 5 — 校验与渲染
$PY scripts/validate_curated.py _work/curated/<id>.json # 不过渲染
$PY scripts/validate_curated.py templates/curated_template.json --allow-sparse # 样例/模板
$PY scripts/build_docx.py --curated _work/curated/<id>.json \
--outdir "output/<试卷名>" --name "<试卷名>"
$PY scripts/selftest.py # 跑 selftest_cases.json 里的全部 case
$PY scripts/verify_output.py --curated _work/curated/<id>.json \
--blank "output/<试卷名>/<试卷名>_空白试卷.docx" \
--answer "output/<试卷名>/<试卷名>_答案解析.docx"
$PY scripts/check_docs.py # 文档结构自检(改了任何 .md 必跑;没改也顺手跑,0.1 秒)
check_docs.py 查的是文档而不是卷子:表格列数与表头是否一致、单元格里有没有没转义的
竖线({{还原|理由}} 写进表格会把该行劈成两半)、表格有没有被空行打断成"带竖线的普通段落"。
这三类都不会让任何代码报错,只有成品文档坏掉,所以必须自己有一道闸门。
verify_output.py 管的是"只有渲染后才会暴露"的问题(插图数、补全对话选项框宽度、
空白卷泄题、解析卷大题是否升序、范文字数是否 80–140、是否残留【主要错误与订正】);
泄漏候选只打印,需回图人工判读 —— 完形选项与原文里合法出现同形词很常见。
其中选项框宽度这条是"渲染期才存在"的典型:宽度由 text_width_cm() 现算,输入侧
JSON 里看不到,所以质检侧必须用同一个度量复算 fit 再与 v:shape 实际宽度比对
(容差 0.02 cm)。别用"字符数 × 常数"近似,那正是历史上差 0.14 cm 就折行的成因。
两个易踩的边界:① 泄漏检查的"全文"必须包含 VML 框内文字(补全对话选项在
w:txbxContent 里,doc.paragraphs 取不到),清单超过 10 条会提示"另有 N 条",--all-leaks 看全量;
② 解析卷的速查表/详细解析按卷内 参考答案、详细解析 两个标题切分,不靠"大题号重复"猜,
否则某大题空着时顺序检查会空转通过;大题号解析支持到"九十九"(十一 曾经直接抛 TypeError)。
若目标文件被 Word 打开,Windows 会抛 PermissionError —— 此时输出到 output/<试卷名>_vN/,并明确告诉用户文件在哪个目录,不要静默失败。
核查强度:软宽一轮、硬宽多轮
排版改动按"宽度是软的还是硬的"分配核查预算,别一律反复渲染比对 PDF:
| 类型 | 有哪些 | 超出会怎样 | 核查强度 |
|---|---|---|---|
| 软宽 | 答案速查表主观题(_answer_flow_text 流式段落)、正文段落、解析文本 |
Word 断行引擎按 TEXT_W 自动折行,永不溢出纸面;失败模式只有"断在哪"的审美(末行孤条、行参差) |
一轮:selftest(golden 的 tables 计数立刻抓"退回表格")→ verify_output(顺序/框宽)→ 渲染后只看首页速查表那一屏 |
| 硬宽 | VML 选项框(box_fit_cm)、三张表列宽、draw_structure_fig 结构图 |
内容超限会顶出框外、单元格内折行、圈外压字 —— 内容级错误 | 多轮:validate 静态 → verify 复算实际宽度 → 渲染目检 |
为什么软宽能放心:text_width_cm() 的误差方向是偏大(ASCII 走真实 TNR 字体 metrics、CJK 记 1 em、弯引号略高估;docx 未设 w:kern 而估算无 kerning,实测差 ≤2%)。偏大的唯一后果是"提前换行、行尾留白";真正的错误形态——某条答案被折行切成两条——需要偏小才会发生。实测三份卷 + 两个样例,单条答案最宽 5.32 / 6.24 cm,仅占 TEXT_W 的 31–37%,离被迫跨行很远。
> 反向教训:不要为假想风险加检查。曾考虑在 validate 加"单条答案超宽 WARN"(阈值 85% TEXT_W),实测最大仅 37%,规则永不触发,只是给后续维护者留一处噪音。
>
> 但页数要当软信号记录:flow 装行失效时页数会明显上跳(练习一 9 → 8 页是正常收益)。渲染后把两个 docx 的页数写进交付说明,用户一眼能看出"这次是不是排版没生效"。
verify_output 对软宽段落没有、也不该有断言:docx 里不存行信息,断行点反推不出来,硬加只能复算一遍构造时同一条公式(恒真)。软宽靠目检,别指望脚本。
★ 长任务断点续跑(PROGRESS.md,防限流/中断后重来)
一份 10 页已作答卷的全流程要几十次工具调用,中途被限流(429)或会话中断很常见。
开跑就建 _work/PROGRESS.md,每完成一个子任务立刻回写——恢复时以磁盘为准,不靠记忆。
| 时机 | 写什么 |
|---|---|
| 任务开始(Stage 1 之前) | 把流程拆成带编号的子任务表(预处理 / OCR / 每大题精校 / 渲染 / 质检各一行),状态 TODO |
| 每个子任务完成 | 该行改 DONE + 一句话产出(文件名 / 题数 / 校验结果)+ 时间;末尾 NEXT: 写下一步 |
恢复流程(新会话第一件事):
Read _work/PROGRESS.md,找第一个非DONE的行;- 回磁盘核对:该行声称完成的产物是否真的存在且非空(
_work/curated/、_work/ocr/、output/);
声称没做的,不要因为"看起来像做过"就跳过; - 从第一个未完成项继续。不要重跑 OCR(一遍就是几分钟),OCR 产物在
_work/ocr/<id>/里可复用。
> 占用:PROGRESS.md 只有几 KB。真正吃盘的是 _work/zoom、_work/preprocessed、_work/ink
> 这些中间图(10 页卷约 40–50 MB)。交付确认后可整目录删除;_work/curated/ 与 output/ 必须保留
> (前者是内容真源,后者是交付物)。
排版规范(本技能默认输出,可直接承诺给用户)
| 项 | 规则 |
|---|---|
| 行距 | 全文 1.3 倍(含表格单元格) |
| 对齐 | 全文两端对齐;仅试卷标题与阅读 A/B/C 标号居中 |
| 题干 | left_indent = 0(贴左边线),后续行悬挂缩进 |
| 选项 | 整体缩进;短选项 AB 一行 / CD 一行(制表位对齐第二列);长选项每行一个。两列判定 = 字符数 ≤ SHORT_OPT 且实测宽度不越界(HANG+宽A+0.6 ≤ OPT_COL2、OPT_COL2+宽B ≤ TEXT_W),单看字符数会让 26 个 W 撞进第二列 |
| 完形填空 | 4 列无边框表:题号+A │ B │ C │ D,ABCD 强制同行;正文与表格之间不留空行 |
| 补全对话 | 选项放 VML 文本框,每选项一行;框宽按最长选项实测自动适配(字体度量 + 内边距,上限 TEXT_W),任何选项都不许折行;对话行不再画表 |
| 完成句子 | 中文提示与英文各占一行;英文空位统一两个 ______,相邻空位隔一个空格 |
| 任务型阅读 | 照原卷形态,不许改写:原卷是完成句子型(题干内含空位)就保留 ______、答案即空格内容;只有原卷本就是疑问句时才在题下加横线作答区。两种形态 builder 都支持(判据是题干是否含 ______) |
| 答案速查表 | 选择题保留分组表格(5 题一行、首列题号区间);主观题横向流式排(按实测宽度贪心装行、项间两个全角空格 ANS_SEP),不用表格——两列表格会给短答案留大片空白、又把长答案挤成折行,反而不易读 |
| 阅读分篇 | 正文前居中添加粗体 A/B/C,上篇题目与下篇正文之间留空行 |
| 同页保持 | 题干+选项用 keep_with_next 绑定,表格行用 cantSplit,不用塞空行凑 |
| 页码标识 | 第x页 共x页 / 班级: / 姓名: 一律删除;卷面完整性靠题号连续性判断 |
| 字体 | 英文 Times New Roman,中文宋体(必须同时写 w:eastAsia + w:hint) |
版面可调项(--config)
build_docx.py --config cfg.json 覆盖 CONFIGURABLE 里的常量:HANG / OPT_COL2 /
OPT_GAP_CM / SHORT_OPT / LINE / PAGE_W / TEXT_W / CLOZE_COL1 / ANS_LEAD_COL /
ASCII_FONT / EAST_FONT。所有派生尺寸都跟随 TEXT_W:页边距由
(PAGE_W - TEXT_W) / 2 反推,三张表的列宽、结构示意图默认宽、选项框上限都按
TEXT_W 收;VML 框内行距由 LINE 派生(不是写死的 312)。每次 apply_config
先复位到出厂默认再覆盖,同一进程连渲多份不会串味——selftest 的 config_tight
用例夹在两个 golden 用例之间守着这条。若改了 TEXT_W 而某张表仍是 17.0 cm,
就是这层收口被改坏了(历史缺陷:正文缩了、表没缩,溢出页边)。
题型与 schema
| kind | 大题示例 | section 额外字段 | item 字段 |
|---|---|---|---|
choice |
单项填空 | — | qid, stem[], options{ABCD}, answer, tag, analysis |
cloze |
完形填空 | passage[] |
同上 |
reading |
阅读理解 | groups[{label, blocks[{k:h/p,t}], items[]}] |
同上,题干为字符串 |
dialogue |
补全对话 | box[{k,t}], lines[] |
仅答答案用 qid, answer, tag, analysis |
vocab |
词汇运用 | — | qid, stem, answer, tag, analysis |
sentence |
完成句子 | — | qid, zh, en, blanks, answer, tag, analysis |
task |
任务型阅读 | passage[] |
qid, stem, answer, tag, analysis |
comprehensive |
综合填空 | passage[] |
qid, answer, tag, analysis(空位写在 passage 里) |
writing |
书面表达 | intro[], essay{title,opening}, answer_lines, samples[{level,words,text[],analysis[]}] |
无 item;samples 为解析卷范文(词数不含已给标题/开头句,范文一律零语法错误,档次差异靠句式与词汇难度体现) |
顶层字段:paper_id / title / code / info / sections[]。
完整字段级说明见 references/curated_schema.md;填空模板见 templates/curated_template.json。
通用 item 扩展字段:
subintro:挂在某个小题上的引导语(如 "(B) 根据提示信息完成句子…"),渲染在该题题号之前。大题内有多套说明时用它,别再全塞进intro[](否则 (B) 会管到不该管的题)。options_fig:选项为图形(段落结构示意图、图表类选择题)时的图片路径。原卷是图形的选项必须还原为图形,渲染为居中插图,中文转述只写进analysis(仅进解析卷),不得用文字转述代替。- 路径一律相对 curated 文件写(
assets/xx.png),build_docx.resolve_asset()与validate_curated.py用同一基准解析;绝对路径虽可用但不利于迁移。 - 重绘用
scripts/draw_structure_fig.py,并用scripts/check_fig.py证明"数字完整落在圆圈内、与圆环无接触"。
扩展新题型:在 build_docx.py 里写一个 build_xxx_section(doc, sec, cmt, recon),注册进 BUILDERS,并在 KIND_LABEL 加中文名即可。(writing/书面表达 已内置:空白卷出标题+开头句+横线区,解析卷出多档范文+逐条解析。范文解析建议含【档位依据】【内容要点对应】【用法积累】【可模仿句型】【注意要点】;其中【用法积累】要把低频词、派生词、固定搭配列出中文释义与可替换的简单表达。)
文件一览
CHANGELOG.md 逐版变更记录(名称/描述/版本这些"对外身份"的历史 + 每版要点)。
版本演进**只记在这里**,项目的 MEMORY.md 只留当前版本与指针
scripts/
setup_env.py 一键部署 venv(写 venv_path.txt)
preprocess_pages.py Stage 1 预处理:方向/拆分/裁边/去歪斜(含 probe 模式)
ocr_local.py Stage 2 本地 OCR,输出带坐标与置信度的 JSON
layout.py Stage 3 版面还原,输出 pNN.txt(人读底稿)
bootstrap_curated.py Stage 4a 可选:根据版面正则生成**草稿**骨架
validate_curated.py Stage 5a 渲染前校验(阻断语义/结构错误)
build_docx.py Stage 5b 渲染两份 docx(唯一排版出口)
分节顺序即分层:常量/配置 → 排版原语 → 批注注入
(`emit`/`segments`/`mark_*`/`Commenter`)→ 九种 builder
→ 两份文档编排 → 入口。`__all__` 列出被 validate /
verify / selftest 直接 import 的符号,重构时不得搬走
verify_output.py Stage 5c 成品质检:插图数/选项框宽度/泄题/大题顺序/范文字数
selftest.py 回归自检:重渲染并与 golden 逐字比对
(默认跑 samples/selftest_cases.json 里的全部 case。
snapshot 除段落/表格/批注计数外,还有三处最容易漏掉的键:
`boxes` = VML 文本框宽/高/框内文字(框内文字不在
doc.paragraphs 里)、`table_widths` = 每张表的列宽、
`comment_spans` = 每条批注范围实际包住的文字(为空即
F-06「锚点塌成零宽」回归,只数批注个数抓不到);
case 可带 `config`/`expect`:不落 golden,只断言
`--config` 真的改到了产物)
draw_structure_fig.py 重绘"段落结构"类图形选项(四种版式 + 圆点校验)
check_fig.py 像素级证明圆圈数字完整、无遮挡
ink_scan.py Stage 3.5 手写排雷:给 OCR 框打疑似手写分(只提示不改动)
zoom_crop.py 裁切放大任意区域(判读遮挡/残字)
hw_crosscheck.py 手写框交叉验证:只跑识别,输出 SAME/DIFF/EMPTY/DOUBT + 裁剪图
make_dist.py 打包 zip 并(可选)同步到工作区分发目录
(同步走 tmp+rename 原子替换;`--check` 真比对版本与逐文件内容)
check_docs.py 文档自检(三种"渲染才坏、代码不报"的结构缺陷):
`.md` 表格列数与表头不符(多出的列**渲染时整列丢弃**,
本文件的对策列曾被整列丢掉过);单元格内未转义的竖线
(`{{还原|…}}` 会把该行劈成两半,必须写 `\|`);
表格被空行打断成"带竖线的普通段落"。已进 Stage 5 命令块
templates/curated_template.json 九种题型的最小可渲染模板
(含 writing / subintro / options_fig 示范)
references/curated_schema.md 逐字段说明 + 红标语法
references/pitfalls.md 真实踩坑清单(★为造成返工的坑)+ 交付前质检项
references/review_protocol.md ★复盘协议:任务完成后如何把经验固化回 skill
samples/reference_curated.json 85 题真卷样例(answer/tag/analysis 齐备)
samples/golden/*.docx 该样例对应的标准输出,供 selftest 比对
samples/mini_curated.json 最小样例:覆盖 dialogue 长选项框(62 字符)/ writing /
subintro / options_fig —— 新能力的回归基线
samples/golden_mini/*.docx mini 对应的 golden(新能力的回归基线)
samples/selftest_cases.json selftest 的 case 清单(新增能力就加一条)
> 新增能力必须同时加 golden case:改完 build_docx.py 后把新用法写进
> samples/mini_curated.json,重生成 samples/golden_mini/,再跑 selftest.py。
分发 / 安装:整个 scan-exam-to-docx/ 目录是自包含的,直接复制到
<用户目录>/.workbuddy/skills/scan-exam-to-docx/ 即可被加载;改动后用同一份目录覆盖。
若 samples/golden 不需要随包分发,删掉即可 —— 只是 selftest.py 失去比对基线。
★ 复盘环节(收尾用,别提前做)
触发条件:用户明确表达「任务完成 / 结束 / 交付完成」意图之后才进入。在此之前(用户还在提修改意见时)不要复盘——那时沉淀的是中间态。
做法:先切 Ask 模式做只读核查并给出方案 → 询问用户意见 → 用户同意后切回 Agent 模式执行。完整清单见 references/review_protocol.md,核心四步:
- 盘点:本轮新增题型/字段、用户返工点、改过的代码、值得固化的临时脚本。
- 找冗余与漂移:真源 vs 分发副本 vs zip 是否同版;
SKILL.md版本号;文档自相矛盾(如"九大题型"对"八种模板");文档与代码是否一致;memory 是否与 skill 重复;新能力有没有 golden 覆盖。
这一步现在一条命令搞定:$PY scripts/make_dist.py --check --out-dir <dist> --sync <dist>/<skill>—— 会比对版本号 + 逐文件内容,非 0 退出即存在漂移(以前只能人肉diff -r)。 - 分级:P0 防返工(路径基准、成品质检、回归基线、副本同步)→ P1 防漂移(文档去重、模板补齐、校验补全、踩坑清单)→ P2 提效率(脚本工具化、代码抽公共函数、memory 瘦身)。
- 执行并回归:
validate三份 JSON →selftest.py全套 →verify_output.py;渲染语义改了必须先重生成 golden 再 selftest;最后make_dist.py打包同步、写 memory。
★ 能力不足时的外部求助协议(用户已确认的工作方式)
遇到本 skill 搞不定的问题时,不要硬扛、也不要擅自装东西,按这个顺序走:
- 先定位真痛点,别把"效果不好"直接当成"识别率不够"。
例:手写污染卷的真痛点是「判定哪段是手写」+「还原被盖住的印刷字」,属于视觉理解,不是文字识别——
所以任何 OCR(含宣称支持手写的云端服务)都解决不了,它只会把两种字混在一起吐回来。 - 再检索:
search_and_install_skills找可安装 skill、search_plugins找连接器/专家。
搜不到就如实说搜不到,不要编造。 - 必须先报批:在 plan 模式给出候选对比(能力/代价/风险)与推荐,经用户确认后才安装或绑定。
- 上传第三方须显式同意:只要方案涉及把用户试卷图片传到外部服务器,必须单独说明并取得同意;
默认优先离线本地方案。 - 专家只做单点审校,不接管流水线:把「答案 + 存疑题 + 还原清单」这类自包含小片段交给一位学科专家审一轮即可。
不要组建多专家并行跑流水线——版面图会被重复读 N 遍,token 成本翻倍,还要额外付结果裁决的成本。
> 判断依据:本流水线的 token 大头是 Stage 4「逐页读图 + 写 JSON」,上下文密集且不可并行,
> 所以拆给多个 agent 的 ROI 为负;而"内容质量审校"是离散、可外包的,适合单点请专家。
真题匹配(可选增强,不是必经环节)
若用户明确要求「把题目还原成真题原貌」,可尝试题库类 skill(如 tizhuang-question-bank)检索母题:
命中且一致 → 用真题原文修正识别错误
部分命中 → 以 OCR 文本为母版,真题只作参照
无命中 → 上下文还原 / 命题重构
止损规则:连续 3 次检索无果即停止,转上下文还原,不再消耗额度。
> 注意:很多地方卷/区统考卷命制时会改写甚至由生成式 AI 出题,题库零命中是常态,不是流程失败。请如实告知用户,不要为了"显得做过"而编造匹配结果。服务通常有并发限制(易返回 429/504),必须串行请求并缩短关键词。
能力边界(必须如实告知用户)
- 手写体识别很差。已作答的扫描卷会被手写/涂鸦污染,手写答案甚至会被识别进题干造成泄题,精校时必须逐题排雷。
- 插图、表格、听力音频不自动处理。插图题需人工补说明,听力按约定删除(默认行为)。
- AI 生成的答案必须人工复核。若卷内无答案页,所有答案均为推断所得,交付时要说明这一点。
- 「共 N 页」这类卷脚标注不作为完整性依据;缺页靠题型结构与题号连续性判断,缺口要主动告知用户。
- 单册 8–12 个版面,端到端约 3–8 分钟(不含人工精校,精校才是主要耗时)。
排错
| 现象 | 处理 |
|---|---|
PermissionError 写不了 docx |
目标被 Word 占用 → 换 _vN 目录并告知用户 |
| 页面内容是横的 | 方向没目检 → 回 Stage 1 跑 probe |
| 一行被从中间切开 | 中缝切线切到字 → 检查 page_map.json 的 split_x_ratio,确认用了 min-max 谷评分版本 |
| 行尾字符被裁 | 裁边阈值过高,应为 0.001×H 量级 |
| Word 打开提示"文档已修复" | OOXML 序列错:cantSplit 必须在 trHeight 之前 |
| 行距不是 1.3 | 表格单元格漏设 → 用 cell_para();不要用 id(p._p) 去重(lxml 代理回收后 id 会复用,导致漏设) |
pip install rapidocr_onnxruntime 失败 |
该包 2022 停更不支持新版 Python → 用 rapidocr 3.x,并额外装 onnxruntime |
| 批注注入失败 | 降级为红色上标编号 + 文末整理说明表,保证理由不丢 |