2026-08-12 独立审计重构后,simpleEval 是三个独立工作流: ① 批量筛选(screen_videos.py,管线之前分拣不合格文件)→ ② 批次生成(make_batch.py, 唯一入口,换课程组合就换一个 run)→ ③ 评价管线(run_school.sh,10 个阶段, 支持 --from/--only/--list 阶段选择)。下面先给流程图,再逐环节讲逻辑 (为什么这么设计、判据从哪来)配参数(阈值、模型、并发、开关取值)。
本轮重构新增:P8.6 ASR转写归因核查(教师错误类指控推 人审+富证据卡,8校190门实测定稿);P8.5a2 span级KFA前置核验(可核验才准写入打分 材料);裁决队列统一为一份,并配了一个真正能操作的 图形化人审页面(span 核验卡拆分类/边界/引文三层子票)。
2026-08-14 两项拍板:① G-3 护栏拆除—— 十门 K=3 双臂 A/B 实测其防护效果已被 D6 prompt 无条件保护措辞兜住,材料自动护栏 归零,substance 链(P8.5a3 视频原声反哺)连带移除;② 教师言语筛查前移为 P6.8 前置合规闸——有确证发现的课在材料构建之前冻结分流人审,不进评分线, 该课全部条目被人裁"错"才放行。
粗色块=不受开关控制的必做环节;虚线块=默认关闭的诊断分支;菱形=分流判定; 红色柱=人工待裁决队列。同一门课两个课时各自独立走完整条链,互不拼接。
flowchart TD
classDef always fill:#dfeae7,stroke:#2f6f6a,color:#173c39,stroke-width:1.6px
classDef optional fill:#e6e8e2,stroke:#8b8f86,color:#4b544f,stroke-width:1px,stroke-dasharray:4 3
classDef gate fill:#f5e6cd,stroke:#b8792a,color:#5c3d13,stroke-width:1.6px
classDef queue fill:#f4dcd8,stroke:#a8433a,color:#5c2019,stroke-width:1.6px
classDef ext fill:#ffffff,stroke:#2f6f6a,color:#173c39,stroke-width:1px,stroke-dasharray:6 3
SCR["① screen_videos.py 批量筛选
ffprobe体检 · 时长≥1800s · 数字静音<10%
→ qualified.txt / disqualified.txt"]:::ext
MB["② make_batch.py 批次入口
全语料抽样 / 显式清单 / 筛选产出
→ runs/<run名>/batch.json"]:::ext
P1["P1 抽音频
batch_audio.py --jobs 4
16k/mono/48kbps mp3"]:::always
Q[("adjudications.jsonl
唯一队列,$W/ 根下")]:::queue
P2["P2 ASR+说话人分离
transcribe_diar.py --chunk 4
diarization_enabled=True"]:::always
P65{"P6.5 转写可靠性分流
逐字conf中位数 ≥ 0.75?"}:::gate
P66["P6.6 人工裁决回纳
verdict=错 → eligible=true"]:::always
SPX{"P6.8 教师言语筛查·前置合规闸(默认开)
speech_screen.py 红线/跑题/身份不符/性质不符
有确证发现?(附at_sec视频跳转)"}:::gate
P7["P7 课时级转写集合
build_period_asr
评价单元=pid(拍板终稿)"]:::always
P85A["P8.5a 两类span+三类时间段
segment_ratio.py qwen3.7-max · prompt v6
模型报区间·代码按时间戳算分钟"]:::optional
OVL{"P8.5a1 三类计算时长
合计 > 全课50%?
overload_route.py"}:::gate
KFS{"P8.5a2 span级KFA前置核验
能抽帧出核验卡吗?"}:::gate
P85B["P8.5p 材料唯一出口 prep_score_inputs
(自动护栏0条,G-3已拆,无条件必跑)
→ P8.5b annotate_spans 标注写回"]:::always
DEG["退化为有无标签
留 segment_ratio.json 供报表
不写入打分材料"]:::optional
CACHE["P8前置手续:拉openclaw缓存
→ invalidate(条目input_sha/prompt_sha纯比对
·缺材料条目自动摘除)→ rsync --delete 推材料"]:::always
P8["P8 codex 单轨直评
gpt-5.6-sol · K=3独立采样取均值
api_tracks 可插拔(默认全禁用不spawn)"]:::always
P86{"P8.6 ASR转写归因核查
reason/main_weakness 有教师错误类指控?"}:::gate
P87{"P8.7 K=3极差 > 10?
auto_kfa_spread.py"}:::gate
KFA["kfa_verify.py 抽帧核验
evidence_quotes 定位→核验卡"]:::always
DONE["eval_codex.json 定稿"]:::always
MAN["P8.8 终局台账 run_manifest.py
每课时 final_state:谁出分/谁被闸挡/谁在等人
每次跑完都刷新(含 --only 局部重跑)"]:::always
RV["图形化人审页面
review_server.py :8130
span三层子票+回纳按钮"]:::ext
SCR -->|"合格清单"| MB --> P1
P1 -->|"时长差>2s 仅记录"| Q
P1 --> P2 --> P65
P65 -->|"≥ 0.75"| SPX
P65 -->|"< 0.75 排除"| Q
Q --> P66 --> SPX
SPX -->|"有→冻结:不建材料不评分
eligible=false 入队(红线severity单独标记)
全部条目'错'或二次放行才恢复"| Q
SPX -->|"无发现 / 人裁放行"| P7
P7 --> P85A --> OVL
OVL -->|"≤50%"| KFS
OVL -->|">50% 拦截:不进材料不评分
eligible=false 入队等人工确认"| Q
KFS -->|"核验卡产出→入队"| Q
KFS -->|"可核验"| P85B
KFS -->|"核验不了"| DEG
DEG -->|"课时继续参评,仅该span不写标注"| P85B
P85B --> CACHE --> P8 --> P86
P86 -->|"有→富证据卡入队(口径A)"| Q
P86 --> P87
P87 -->|"是→抽帧"| KFA --> Q
P87 -->|"否"| DONE
KFA --> DONE
DONE --> MAN
Q -.->|"阻塞性待裁统计"| MAN
Q <-.->|"查看+裁决(写回)"| RV
2026-08-12 拍板:不合格检测(原 P0 健康体检 + 原 P1.5 数字静音)从单校管线挪出,
成为独立"分拣"工作流。三道判据全部沿用既有实证,不新定阈值。
产出 qualified.txt(直接喂批次生成)+ disqualified.txt(带原因)。
| 参数 | 取值 | 逻辑 |
|---|---|---|
| ffprobe 体检 | 可读 + 有音轨 | 读不出/无音轨的文件无法评价 |
| --min-dur | 1800 秒 | 半堂课不评(build_session_manifest 统一口径) |
| --zero-threshold | 10.0% | 某秒采样全零=录制中断(zeroscan 生产定稿);"近静音"音量判据已被1471门实测证伪,不做 |
| --jobs / --skip-zero-scan | 4 / 关 | 并发;跳过静音扫描可加速但漏录制中断类问题 |
三种来源产出同一种 batch.json:全语料抽样
(--from-sessions,教师注册表冻结、seed=42 可复现)、显式清单
(--from-list,每行一个路径;同课两课时用"组标签+TAB+路径"相邻行)、
筛选产出(qualified.txt 直接喂)。已存在的 batch.json 默认拒绝覆盖
(批次组成难以撤销),确认要换加 --force。
自动把 /data/files/ 映射成 /data/ro/ 只读路径(防误写原始数据的
内核级保险,batch_audio.py 强制断言)。
评价管线入口随之简化:bash run_school.sh <run名>,
支持 --from P7(从某阶段跑到尾)/ --only P2,P8.5(只跑指定阶段)/
--list(看阶段清单);各阶段自声明所需环境变量,调试单阶段不用全量 export 密钥。
只读纪律:输入路径必须以 /data/ro/ 开头,脚本自己断言,不靠自觉——
原始区是 CIFS rw 挂载,历史上有写坏风险。瓶颈在 NAS 读带宽不在 CPU,
并发给 4 就够。断点续跑:目标文件已存在且 >10KB 就跳过。
抽完顺手核对时长:抽出音频与源视频时长差 >2 秒暗示解码可能丢了一段。 这条 2026-08-11 才接入人审队列——此前只打印 WARN 不拦截也不留痕。 不改变现有行为:文件仍照常产出、照常往下游走,只是多一条待裁决记录, 不会让这门课凭空从批次里消失(FAIL 才会让整体非零退出,是硬故障,不进这条"存疑"队列)。
| 参数 | 取值 | 逻辑 |
|---|---|---|
| --jobs | 4 | 并发抽取;瓶颈在 NAS 带宽不在 CPU |
| ffmpeg 编码 | -ac 1 -ar 16000 -b:a 48k -f mp3 | 单声道 16kHz 48kbps,ASR 输入规格 |
| 断点判据 | size > 10240 字节 | 已存在且非空才跳过,避免半成品被误认已完成 |
| 时长差阈值 | > 2 秒 → WARN | WARN 入队仅记录;ffmpeg 非零退出或 ffprobe 读不出时长才是 FAIL(硬故障) |
| 写盘方式 | 先写 .part 再 os.replace | 防止中途失败留下半成品被当作已完成文件 |
本环节不做"提交前排除"——不合格文件(读不出/无音轨/过短/数字静音)由管线外的
筛选工作流在批次生成前分拣掉,manifest 里有什么就转什么。
--work 2026-08-12 起改为必填(原默认指向另一仓库部署路径,
裸调会静默读写别的工作目录)。
按 result.file_url 回映射 sid,绝不按下标——批量返回顺序 ≠ 提交顺序,
这是最容易踩的坑之一(脚本自带 --shuffle 用于主动验证这条不会串课,
归一化后 URL 冲突或同一 sid 被映射两次都会直接中止,不静默继续)。
热词按词读表,不是全表统一权重——weight=5 全表盲铺会把"五育/五通/五史"这类词
强插进正常语句,那是污染不是纠正。
| 参数 | 取值 | 逻辑 |
|---|---|---|
| model | fun-asr | DashScope 异步转写 |
| --chunk | 4 | 每批提交的课时数 |
| diarization_enabled | True | 说话人分离——学生发言实质度/互动真实性的硬依赖 |
| disfluency_removal_enabled | False | 保留口语原貌,不做顺滑处理 |
| --hotwords | 共享路径 asr_hotwords_calibrated.json | 与 tjlg_xsx_2025 刻意共享,不拷贝进 simpleEval 自己目录(避免"哪份最新"漂移风险) |
| 热词 weight | 主体 5,≤3字短词降 4 | 逐词读表;短词过高权重易过度偏置 |
| 产出 | asr_raw/{pid}.json + diar.json | asr.json/ts.json/asr_ts/ 2026-08-14 审计停产——仓库内零消费者,声称的下游(class_span 等)是已移除分支;需要时从 asr_raw/ 重新派生 |
依据 docs/ASR置信度_可用性评估.md(606语料28门+2025录课40门人工金标验证):
逐字置信度 AUC=0.954,零边际成本,14/14 重度课零漏检——但跨语料绝对阈值不成立:
606 标定的 0.9770 套到 2025 录课语料,40 门里错标 23 门,而人工共识是 0 门重度。
2026-08-11 用户拍板阈值 0.75:明显低于 606(0.977) 和 2025 录课(0.974)
两个语料的中位数,是相对保守/宽松的闸——只拦真正严重的转写崩坏,不重蹈"0.977 套到别的
语料错标 23 门"的覆辙,但仍是个固定阈值,没有针对本语料重新标定,
是用户已知情接受的简化。
prep_score_inputs.py 的 G-4 软带护栏,真机验证过真实案例
(某课整课 median=0.968 轻松过闸,但 480 秒起有 60 秒窗口 median 仅 0.236,
652 个词 confidence<0.4——局部塌陷是真实现象,不是假设)。
用户最终拍板:整套加入生产链路的复杂度收益比不够,只保留整课单一闸。
中间地带(过了硬闸但局部质量一般)不再有专门护栏,是明确接受的简化,不是遗漏。
| 参数 | 取值 | 逻辑 |
|---|---|---|
| --threshold | 0.75 | 整课 conf_median 闸;< 阈值路由人审,不自动排除,等人工裁决 |
| 判据来源 | asr_raw/{pid}.json 逐字 confidence | 取全部带 confidence 字段的词,算中位数 |
| 数据缺失处理 | asr_raw 缺失/无 confidence → eligible=false | 与"达标/不达标"分开统计,不混为一谈 |
| --window-threshold 等 7 个参数 | 已删除 | 2026-08-12 局部塌陷检测整套拆除,见上方说明 |
2026-08-11 审计发现的真实缺口:此前人工在 adjudications.jsonl 里把某条
分流判断裁成"错"(AI 误判、这门课其实能用)之后,没有任何自动机制
把它重新纳入打分——会一直被排除,直到有人手工重跑对应的分流脚本。这个脚本补上这一环。
只有明确 verdict="错" 才回纳——"对"(AI 判断确实成立)和"拿不准"
都维持原排除状态,不是"没被否定就默认放行"。2026-08-12 起全流程只跑这一次:
原来还有一次更早的回纳(旧编号 P1.6),存在的唯一理由是赶在 P2 提交 ASR 前把
P1.5 数字静音的排除撤回去;P1.5 删除后这个理由不复存在,现在只剩这一次,位置在
P6.5 分流之后、P7 之前。zero_silence_route 处理分支 2026-08-14 已删
(其 producer 早已随数字静音判据移交 screen_videos 前置筛选,留着只是无源死键)。
| 参数/字段 | 取值 | 逻辑 |
|---|---|---|
| 回纳触发条件 | verdict == "错" | 严格相等;null/"对"/"拿不准" 都不触发 |
| 处理范围 | claim_ref ∈ {conf_route, nonteach_overload}:单条"错"即翻回;speech_*:该课全部条目均"错"才放行(另一套语义);asr_blame*:"错/拿不准"写 claim_overrides.json sidecar | 其余 claim_ref(audio_duration_mismatch、KFA span/quote)留痕不自动处理;span 类另有人裁终点升格消费 |
| 回写方式 | eligible=true + overridden_by_adjudication=true | 加字段不改原始判据——conf_median 等原始数据保留,只是不再据此排除 |
| 幂等性 | 已 eligible 的条目直接跳过 | 可安全重复调用 |
eval/data/claim_overrides.json sidecar,对外报告读它过滤被否决指控——
分数不变,成本为零(不重跑)。⚠️ 原先还往 eval_codex.json 写
blame_overridden 标记——审计发现该标记必被 P8 开头"从 openclaw 拉缓存"
覆盖,寿命只有几分钟,最终文件里从来不会有它(与 P8.5S 同形状的错位:打分产物的
裁决消费挂在了打分前的 P6.6),已删;报表一律读 sidecar。
彻底重评:需要真正干净的分数时,把人工核验的具体发现写进
P7 的 verified-caveats,让 invalidate_stale_cache
带动那门课重新打分——这才是真正抹掉冤枉扣分的做法,轻量级标记不解决"旧分数
本身仍隐含冤枉扣分"这个问题。两级都已在真实数据上验证过。
覆盖范围用户逐项拍板:红线(价值导向/立场, severity 单独标记)/ 跑题 / 身份不符言语(重点)/ 性质不符内容。处置口径当日两次演进:首版"P8.5S 只推人审不拦截"被 二次拍板取代——筛查前移到材料构建(P7)之前,有确证发现的课 冻结分流人审,不进评分线的任何后续环节(不建材料、不进 span 分析/KFA、不打分)。 放行铁律:该课全部 speech_ 条目非阻塞(人裁"错",或 二次放行——2026-08-14 拍板:verdict="对"问题在案时,人可在人审页 显式裁定"准予回到评审队列",/api/release 写 entry.release=true+必填理由,发现记录 不动)才翻回 eligible=true;"对"未放行/"拿不准"/未裁维持冻结。条目带时间点, 人审页面可一键跳转原视频对应时刻核听。概念/事实类指控(专名提法、概念混淆、史实数据) 不在本模块,走既有 P8.6 asr_blame 通道。
为什么独立模块(用户追问后拍板):任务性质与 segment_ratio 的结构
标注不同,是内容风险筛查——任务混载实证有害(time_budget 混在 span 任务里时模型不做
算术);筛查 prompt 的误报率需要独立调校/回滚,独立开关 ENABLE_SPEECH_SCREEN
+ 独立阶段可单跑可关停。产出 speech_screen_route.json 分流闸文件
(P7 --extra-route 消费)+ 写唯一人审队列。前置的代价:首轮跑在 segment_ratio 之前,
没有视频播放 span 可做播片段原声的确定性排除(纪录片里的批判性引述不能归教师)——
靠 prompt 排除规则(含人审判例)+ 人审视频跳转核听兜底,误报成本=一次人审;
重跑轮次已有 span 文件时确定性排除自动恢复。筛查出错(API 故障)的课不冻结
(fail-open)——冻结必须以确证发现为据;2026-08-18 修缮批起 error 不再算"已完成":
失败课下轮自动重试,run 结束醒目汇总"筛查失败 N 门"(fail-open 参评必须可见,此前一次瞬时
429 会让课永久跳过合规筛查且无告警)。条目带 prompt_sha,换筛查 prompt 自动全量
重筛(与 P8 失效机制对称化);模型独立配置 SPEECH_SCREEN_MODEL(此前误共用
SEGRATIO_MODEL,与"筛查可独立调校/回滚"的立项要求矛盾)。conf_route 排除课不筛查
(2026-08-14 审计补):整课转写不可靠时判"红线/身份不符"既浪费调用、更可能凭乱码把课
钉进合规冻结轨——"引文逐字命中"闸恰恰是乱码 ASR 最容易伪造满足的一道;人裁"错"回纳
放行后下轮自然补筛。
| 参数 | 取值 | 说明 |
|---|---|---|
| ENABLE_SPEECH_SCREEN | 1(默认开) | 独立开关,0=整体关停本模块(连同前置闸一起关) |
| 模型 | qwen3.7-max K=1 | 与 segment_ratio 同模型,同一份带时间戳 diar 输入 |
| SPAN_KINDS_MIN_SEC | 跑题/性质不符 120s | 成段类时长闸;红线/身份不符为句级不设时长闸 |
| LOW_CONF | 0.85 | 引文句 conf 低于此值附"疑转写失真"提示 |
| claim_ref | speech_<kind>_<锚句号> | 重跑稳定,队列按 sid+claim_ref 去重 |
| 分流闸文件 | eval/data/speech_screen_route.json | {pid: {eligible, findings, kinds, pending_refs}};P7 --extra-route 消费,segment_ratio/prep_score_inputs 亦读它跳过/清残留 |
build_period_asr.py 直接从 diar.json(课时级)产出
asr_scoreable.json,不经过 P3 的课时定序/拼接——
评价单元 = 视频文件(pid),不做任何形式的两课时合并——2026-08-14 用户拍板终稿口径(不再是临时取舍或开放问题)。
路由文件(conf_route + P6.8 的 speech_screen_route)取并集排除,任一判
eligible=false 就排除;2026-08-12 起不再传 zero_silence_route
(P1.5 已删,该文件不再产出)。substance.json 已于 2026-08-14 随 G-3 拆除停产
(它的唯一消费链就是 G-3;学生展示 20% 闸直接读 diar,不经它)。
prep_score_inputs.py(2026-08-14 起在 P8.5p 作为材料唯一出口调用
——此前 P7 也产一次、一条链写三次材料,P7 那次是纯被覆盖的中间态;现在
"材料只有一个产出点",P7 只产 asr_scoreable.json)是"视觉/录制信号变成打分杠杆"的唯一可能入口,
铁律写死在文件头:只注入【防止无中生有扣分】的句子,绝不注入任何可被当作加分/减分
依据的描述;禁止注入念稿嫌疑、视频放映、课堂纪律、机位、PPT可读
——DEV 实证念稿提示会被模型当成 −15 分的打分杠杆;护栏总数 ≤5
(v2 的 8 条护栏在 DEV 上全面劣于 v1 的 5 条,规则稀释效应)。生成后自检材料头部
不得混入 BANNED 词表,命中即 sys.exit(2)。
| 护栏 | 触发条件 | 防止的错误 | 状态 |
|---|---|---|---|
| G-1 | 课型=网课/录播 | 现场无学生时因缺互动扣分 | 2026-08-12 拆除(结构性从未触发,课型判定已不产出) |
| G-2 | 学生入镜=无 | 画面证据不足时因"文本无学生发言"扣分 | 同上,2026-08-12 拆除 |
| G-3 | 曾为 diar 非主讲占比 ≤ 0.03 | 仅凭"转写里零学生发言"直接判缺乏互动 | 2026-08-14 拆除——十门 K=3 双臂 A/B 实测其两个防护效果(互动状态误判、评语"缺互动"扣分泄漏)已被 D6 prompt 无条件保护措辞兜住:分差全在噪声带(-2.7~+2.0)、互动状态 0 翻转、评语泄漏 0 处。substance.json/refine_substance 链连带停产 |
| G-4 | 曾为 worst_window_conf < 0.65 | 局部转写质量偏低的定位提示 | 2026-08-12 随局部塌陷检测一并拆除 |
自动护栏现为 0 条。G-3 拆而不失防护的机理:它防的错 (模型数出"零条学生发言"→判"缺乏真实互动"→扣分)在 D6 prompt 里有三处无条件 保护——interaction_status 字段说明"学生的声音有没有被话筒收录,不影响本字段"、 "不要因为听不到学生就判仅确认式假问"、取向第 3 条"缺互动不构成扣分理由,也不得写进短板"。 条件注入(占比≤3%才提示)能做到的,无条件措辞同样做到了,还少一层检测机器。 遗留一个方向性弱信号("仅确认式假问"组均值 -1.5 vs "有真实往返"组 -0.2,n=10 不显著) 留待大样本批次复核,详见 docs/decisions/2026-08-14_G3拆除与合规前置.md。
| 参数 | 取值 | 逻辑 |
|---|---|---|
| 护栏硬上限 | ≤ 5 条 | 已被 DEV 实证:规则越多不是越准,是稀释(当前实际 0 条) |
| BANNED 自检词表 | 念稿/念PPT/念书/视频放映/放映/课堂纪律/趴睡/玩手机/机位/PPT可读/切镜/水印 | 材料头部命中任一即判自检失败,退出码 2 |
--verified-caveats 指向 eval/data/verified_asr_caveats.json
({pid: 提示句}),有则追加到防冤枉句块末尾。与已拆除的 G-3/G-4 的关键区别:
那些是算法自动检测触发;这条必须逐条人工核验后手写,
不是自动机制,文件不存在就静默跳过。这是已拆除的 G-4(自动检测局部转写质量存疑,
因"猜测性、测不准"被拆)的替代路径——不自动猜哪里可能转错,只在
P8.6 队列的某条指控被人工确认"确是ASR责任"之后,把核验到的
具体发现(涉及术语/大致时间点)转达给下一轮评分,防止模型读到同一段烂转写
再次得出同样的错误结论。真机效果(b_tjpu10 两门课):写入提示后重评,
"概念混淆"指控在新三轮评语里完全没有复发,分数 +7.3/+4.7 分,
P8.6 复检该批全部 10 门"检出 0 门"。⚠️ 措辞每次变化都会改写入课时的材料哈希,
触发 invalidate_stale_cache 重评,是预期行为但有真实 API 成本。
教师顺口一句"上节课我们讲了 X"是教学衔接,讲了 20 分钟才是复习课——两者不可仅凭
"有无"区分,必须算时长。参评池 = asr_scoreable.json 键集(2026-08-14
审计修复——此前池是 diar 全集,被 conf 闸排除的课照样花 qwen 全文调用做 span 分析,
产出还级联到 KFA 抽帧与人审队列,而那些课永远进不了材料;现在直接吃 P7 算好的
conf∪speech 并集结论,auto_kfa_spans 同款)。做法:整份 diar 转写(带
speaker_id/时间戳/文本)喂给模型,
不抽样,让模型只标语义边界(from/to 句号),
时长由确定性代码算,不让模型自己算。
三道防线,都不靠模型自觉:① 区间号必须合法(在范围内、 from≤to),越界丢弃;② 证据原句必须能在该区间内逐字比对找到,找不到丢弃并计入 fabricated;③ 最短区间闸——连续 <3 句或 <60 秒的区间一律丢弃,防"顺口一句" 被标成环节。
2026-08-14 五类→两类(用户拍板):复习/行政事务/课堂测试不再单独识别
——三类都是教师本人在讲、转写忠实,评分模型读全文自己看得懂,不存在需要修正的信息
不对称;实证收益率也极低(复习全库 104 个 span 仅 1 过闸、行政 49 个仅 1 过闸,
b_tjpu10 三类检出 14 个全部被丢弃——整套识别机器基本在生产待丢弃噪声+人审负担)。
三类只保留时间段报告(2026-08-14 二次拍板撤出评分 prompt 移到本环节——
本环节通读的正是带时间戳+说话人的完整 diar;2026-08-18 prompt v6 再收紧:
模型连分钟数也不报了,只报句号区间 time_budget_spans,
分钟数由代码按时间戳算(同类多段合并去重叠),无合法区间按 0 处理——
宁漏勿冤。收紧动机:v5 模型自报分钟数在 10 门实测里给的是 1,1,1,1,1,1,1,1,2,0,
像敷衍填空不像估算,而这个零证据数字是唯一能拦评分的闸的全部输入;v6 起它与 span
同款可校验,模型原报区间落盘供人审溯源。合成正例的老教训仍然成立:让模型直接下"过半"
结论,62% 复习被判"无"(模型不做算术)。判过半 → overload_route.py
(P8.5a1) 写 eligible=false + 推人审队列(claim_ref=nonteach_overload),
暂不推进后续环节(不生成打分材料、不抽帧、不评分),人裁"错"经
回纳恢复参评——与 conf_route 同形状的 路由→人审→回纳。
留下的两类恰是转写会误导评分的:视频播放(原声冒充教师)、学生展示
(学生话不该按教师讲授评价)。学生展示仍用 diar
非主讲占比闸而非视觉判断——分享的学生可能不入镜,"用视觉核验证明学生展示是错的"这个
方法论本身在最早试点就踩过坑。
| 参数 | 取值 | 逻辑 |
|---|---|---|
| --model | qwen3.7-max | plus 单遍召回仅 51%(整段漏看),max 100% 且 11/12 完全可复现,K=1 即可不必 K=3 |
| --workers | 3 | DashScope 并发>3 必 429 |
| MIN_SENT / MIN_SEC | 3 句 / 60 秒 | 最短区间闸 |
| STUDENT_NONMAIN_MIN_RATIO | 0.20 | 学生展示 diar 非主讲占比闸;0%~37% 间有清晰断层,取 20% 分界;2026-08-13 起先剔除与已识别视频播放span重叠的非主讲时长再过闸(见下方P8.5a3) |
| CUE_KINDS | {视频播放} | v5 起 announce 闸放宽(2026-08-14 拍板):有宣布原话照抄=最强证据;无宣布也可标但须 ≥2 条独立线索写进 no_announce_evidence 自证(v4 时代 386 秒"教师念马克思原文"误标教训转化为 prompt 排除项);编造零容忍——宣布语对不上全文整段作废,留空不丢分。无宣布段标 announce_verified=false,KFA 卡带警示供人审重点核对。放宽动机:宣布语召回仅 33%,约 2/3 真实播放段此前根本进不了 spans |
| SHELVED | {视频播放} | 时长口径("暂缓"概念 2026-08-14 退役,如实新口径):人裁终点优先;无人裁不计入自动占比——nonteach_ratio 不含它,升格走 nonteach_ratio_with_video 另出一个数。video_announced 布尔仍只认有已验证宣布语的段 |
| max_tokens | 8000 | 防模型间歇性冗长丢样本,不算"修复",真正对症的是 K=3 取中位(此处 K=1) |
auto_kfa_spans.py → kfa_verify.py
抽帧(起点+2s / 中点 / 终点-5s / 终点+5s,2026-08-13 从3帧改4帧)
+ 回查前文语境 + 引导语线索,产出核验卡入人审队列。KFA可核验是写入打分
材料的前置条件——核验不了(视频缺失/时间戳缺失)的 span 退化为"有无"标签
留在 segment_ratio.json 供报表,不进打分材料(用户拍板,符合防打分杠杆铁律)。
末帧改成终点前后各一帧:原来单独一帧取在终点-2s,正好卡在最难
判断的边界上、信息量最低(首批人审实证:末帧看不出视频结没结束);end±5s
对比着看,若 end+5s 仍是片内画面=边界切早了,已是教室画面=边界对。核验卡新增
frame_tags 字段标出每帧含义。成本:只对过闸span抽帧,历史数据里
过闸span极少,多数课零开销。
end_pick(真实终点夹在哪
两帧之间,精度15秒)。升格口径(2026-08-14 拍板):视频播放
时长默认不计入自动占比(旧称"暂缓类",概念 2026-08-14 退役;宣布语召回33%、模型终点不可靠),但人裁终点是人确认过的
事实——"回纳生效"时写回 segment_ratio.json(sec_human +
nonteach_ratio_with_video + 重写显示 label,原始 sec/nonteach_ratio
留痕不动)。两条升格路径,闸都是分类子票=对:A 显式点选
(end_pick 双边界,时长=两帧中点−起点,终点被纠正);B 背书
(边界子票=对且未点帧带——人对照 end±5s+帧带确认模型终点没切错,时长直接用
被背书的模型值;首轮回收实测 3 条 span 属此类,不升格等于白审,2026-08-14 补)。
两者都有时 A 优先;单边界(早于帧带首帧/帧带外仍在播放)不升格,扩窗重抽后
再裁。占比统计升格之外,路径A的人裁终点还回收进材料标注(annotate_spans 把
结束标记移到人裁终点所在句,2026-08-14 二审)。首轮真实回收:4 span 全升格——
T10-P0 端点纠正 466→503s/占比18.6%(人裁 774s 与 conf 建议 795s 及当时的
conf 降级信号三方向互证:声称终点报早约38秒),T16-P0 背书 378s/14.1%、
T16-P1 背书 250s/9.2%。⚠️ 重析防护(2026-08-14):--sids 强制重析后 span
内容可能整个换掉——apply 套用升格/分类否决前先比对核验卡引文与当前 span,
对不上即拒绝按下标盲套(WARN 须重新人审);引文相同的人裁字段由
carry_human_fields 按内容身份自动移植保留。实战:T16-P0 v5 重析后三条陈旧
裁决被闸精准拦截,T16-P1 移植成功零人工成本。
refine_substance.py 原本把 KFA 确认的视频播放句从说话人占比里剔除
重算,专为修正"视频原声灌高非主讲占比→G-3 被跳过"(实测 T16-P0 18.2% 全是央视
原声)。G-3 拆除后该链条失去唯一消费者,整体删除。学生展示 20% 闸不受
影响——它的视频区间重叠剔除在 segment_ratio.py 内完成(nonmain_sec
剔除后重过闸,防纪录片解说冒充学生发言),从来不经 substance。
annotate_spans.py --kfa-verdicts(2026-08-14 从 build_abc_variants.py 收敛更名——A/B/C 消融机器随五类→两类后 B==C 已无区分度,整体拆除)用同款证据闸+KFA核验闸,
只标两类(学生展示/视频播放),直接覆盖 score_inputs/{pid}.txt——必须在
prep_score_inputs.py 之后跑,顺序颠倒会导致标注被下一次未标注输出覆盖。
证据闸阈值在 annotate_spans.py 与 auto_kfa_spans.py 两处必须同步改(闸判据漂移会被
运行时 WARN 抓出来)。
codex 主轨不受 api_tracks.json 控制,是 run_school.sh 里
写死必跑的部分(经 openclaw,CLI 而非 API Key)。其余模型(qwen/deepseek/…)走声明式
api_tracks.json——加一个新模型只需要加一条配置,不用碰任何 .py 代码;
当前配置里 qwen(qwen3.7-max)与 deepseek(DashScope 托管 deepseek-v4-flash,
与 qwen 用同一个 EVAL_API_KEY,不用另办 key)两条轨都已配好、默认 enabled:false
——要开第二/第三评价轨,把对应 enabled 改 true 即可,零代码改动。铁律不变:跨模型
只比秩不融合,codex 主轨出档,其余轨只作对照/备份。2026-08-12 起无启用轨时不再
空 spawn 后台进程;同日修掉 run_api_tracks.py 遍历中删元素跳轨的 bug。
K 次采样必须是相互独立的会话(每次新 exec,不 resume),否则自一致
采样失去意义。两轨读的是同一份 score_inputs/,
跨模型绝对分不可比、只比秩——不做平均、不做融合(既有实证:Fusion 不胜 panel 均值)。
invalidate_stale_cache.py 摘掉失效条目再推回去。
2026-08-14 改造:失效判据从"侧车哈希文件"改为 eval 条目自带
input_sha/prompt_sha(打分脚本写回时一并写入)——侧车是会与分数脱节的第二状态源
(实测:字节相同材料的合法分数被误判失效,白花 24 次调用),且不随 rsync 走、换机即全批
作废。现在分数与"打这分用的材料+prompt"永远同一份状态;材料文件被上游闸清掉的课时条目也
在这一步自动摘除;存量无哈希条目 grandfather 保留。API 轨同步补上了 prompt 变更失效
(此前 run_api_tracks 没传 prompt 文件,prompt 变了 API 轨不失效)。
2026-08-18 M5 补全:prompt_sha 配方纳入 EXTRA + model + effort
——EXTRA 十行评分纪律改了会真的改变评分行为,此前却不失效任何缓存;换模型/档位同理。
条目记 model/effort 留痕;调用方(run_school / run_api_tracks)必传
--model/--effort,缺了会退化为旧配方比对 → 每轮把全批判失效(持续重评而非一次性)。
上线当天全批失效一次是预期成本,趁 b_tjpu10 小批做掉(6 门 18 次)。
| 轨道 | 模型 | K | 并发 | prompt | 字段差异 |
|---|---|---|---|---|---|
| codex(主轨,必做) | gpt-5.6-sol | 3 | jobs=4 | zhiping_system_D6.txt + schema_D6.json | 含 main_weakness(诊断用短板归因)。三类过半判断曾短暂放此处(nonteach_overload 字段),2026-08-14 当天二次拍板撤回——评分材料无时间戳按篇幅估有盲区、评分 prompt 须单一职责;判定移至 P8.5a(有时间戳的环节),prompt 已 git 精确恢复原版 |
| qwen(可插拔,当前禁用) | qwen3.7-max | 3 | workers=3 | zhiping_system_D6_qwen.txt | 无 main_weakness——v4 6选项含"无明显短板"逃逸项,qwen 用了25%次数解释71%分数方差;v5 5选项强制后 97%课程坍缩单一标签"平铺罗列",两版均验证失败,故拿掉 |
| 参数 | 取值 | 逻辑 |
|---|---|---|
| --effort(codex) | medium | codex reasoning effort;三档已验证过一致性 |
| --sandbox(codex) | read-only | codex 执行环境只读,不允许改动 |
| timeout(codex) | 900 秒 | 单次调用超时 |
| enable_thinking(qwen) | false | 关闭思考链,控制成本与延迟 |
| X-DashScope-DataInspection(qwen) | {"input":"disable","output":"disable"} | 思政 ASR 内容会触发绿网审核 400 data_inspection_failed,须显式关闭 |
| SPREAD_WARN | 10(2026-08-13 从16下调) | 200门实测分布 p50=5/p90=10/p95=11/max=17;>10 触发率 7%,正常噪声期望极差≈5.6,超10已出p90,值得便宜的KFA核验兜底 |
| EXTRA 固定任务说明 | 见下 | 不分护栏配额,两轨 prompt 都注入的强制要求 |
EXTRA 固定四条(拼在 prompt 末尾,两轨都有,不占用 ≤5 护栏配额): 必须用满 0–100 量程、敢打低分,不因是正式录课默认高分;确认式假问(是不是/对不对/好不好/ 明白吗)不算互动,不得据此加分;ASR 音近误转(同音错别字)音对即正确,不得因转写 错字扣分——这正是 ASR 转写失误防偏失设计的第二层;独立判断,不参考任何其他课程或 模型的分数。
asr_blame_<指控文本sha256前8>——旧口径下重评后模型提出内容
不同的新指控会被旧条目挡住不入队(实测发生过),现在新指控重新入队、旧裁决只盖
旧指控;存量旧式条目按其 ai_said 算等价指纹兼容去重,模型重提相同指控不重复入队。
要解决的问题:ASR 偶有转写不准,模型据此指控教师"概念错误/说错/史实错误"并压分——
但那可能是 ASR 转错了字,不是教师说错了话。codex CLI 拿不到思考过程,
reason/main_weakness/evidence_quotes 就是可观察的
"思考痕迹",本环节扫描它们。
富证据卡:定位/conf 不做闸、只做证据附件。入队条目自动带:指控句原文、 K=3 中出现次数(一致性信号)、指控短语定位@时间戳 + 转写上下文 + conf(能定位的部分)。 真机样例:「土地收归国有」@4:33 conf=0.978 + 前后25字上下文——人对照几秒即知 是教师真说了还是 ASR 转错。已知残余误报:正则分不出"围绕'国进民退'误读组织学生辨析" 这种表扬句(语义级反转),触发率低,人工看一眼即排除,不值得上 LLM 判别层。
| 参数/机制 | 取值 | 逻辑 |
|---|---|---|
| 指控模式 | 13 组正则(口误/说错/概念错误/史实错误/术语误用…) | 改动前须在存量数据重跑触发率,不凭感觉增删 |
| 否定前置守卫 | 无/没有/未见/避免… 前缀4字内 | "无知识性错误"是表扬不是指控 |
| 短语抽取 | 指控句内引号文本(2-40字) | 定位用全短语精确匹配,不做模糊——模糊会稀释"定位到了"的可信度 |
| 入队粒度 | 一条指控一条(claim_ref=asr_blame_<指控文本sha256前8>,2026-08-14 指纹化) | 多条指控合并;按 sid+claim_ref 判重可重复跑 |
| 新增LLM调用 | 0 | 纯确定性检测+定位,零边际成本 |
背景:codex_score.py(P8)K=3 极差超阈值此前只打印一行 WARN,KFA
(kfa_verify.py --claim-source eval_out)本来就是为核验"模型自己都判不准"
的课设计的,但两者从没接过线。2026-08-11 用户拍板补上这条闭环。命中数少时(多数课
K=3 都很稳)开销接近零,只在真有分歧的课时才会真的去抽帧。
kfa_verify.py 对每条 evidence_quotes 都尝试
定位,全部定位不到时会正常退出(returncode==0)但产出 0 张核验卡——从外部看跟
"这门课根本没被标记、不需要核验"没有任何区别。脚本显式区分三种结局并大声报出来:
真核验到(有卡产出)/触发但核验空(0 张卡,evidence_quotes
全部定位不到,需要人工另想办法核实)/执行失败(非零 exit)。
核验机制:quote_match.locate_quote() 把 D6 直评给出的 evidence_quotes
定位到 diar 句子(引文没有显式时间戳,要先找到最可能出自哪一句作为锚点),再抽帧、
回查上下文。诚实边界:evidence_quotes 是模型给"这次整体判断"的举证,不是给
main_weakness/interaction_status 各自单独标注的证据——核验卡里如实写这个限制,
不假装有精确对应关系。
| 参数 | 取值 | 逻辑 |
|---|---|---|
| --spread-threshold | 10(2026-08-13 用户拍板下调) | 与 codex_score.py 的 SPREAD_WARN 保持一致;实测依据见该表 |
| --run-idx | 0(K=3 第 0 轮) | 核验目的是"证据是否存在",不需要每轮都核 |
| 覆盖范围 | 只读 eval_codex.json | qwen 轨不在覆盖范围内(已知限制,见文末) |
| 帧位 | 4 帧硬编码(起点+2s / 中点 / 终点-5s / 终点+5s,2026-08-13从3帧改;--frames-per-span 参数已失效不再读取) | <6 秒的区间只抽 1 帧;终点前后各一帧便于对比边界切没切准 |
| --context-sents | 默认 5 | 画面信息不足时向前回查几句 diar 原文找文本佐证 |
审计发现的唯一业务级缺口:管线跑完后没有任何文件回答"谁出了分、 谁被哪道闸挡了、谁在等人"——b_tjpu10 批次 10 课时只有 8 门进 eval_codex.json,被 合规闸冻结的 2 门从结果里彻底消失,要知道去向得同时读两个文件。1000 门规模下静默丢 20% 而交付方不知道,是交付事故不是 bug。
每课时一行 final_state:
scored / excluded_conf / frozen_speech / blocked_overload / pending_human /
asr_failed / unknown(unknown 出现即打 WARN——那本身就是管线账目不平的信号)。
随行字段:命中的闸及依据摘要、队列统计(open/resolved,阻塞性待裁单列
——只有 conf/speech/overload 类未裁才算阻塞,span/quote/blame 未裁不算)、K=3 分数与
极差、K 轮主短板众数、占比账本挂载(此前 nonteach_ratio 系列全库零消费者,manifest
是唯一出口)、speech 筛查失败警示(fail-open 参评的课必须可见)。它同时是
claim_overrides.json 的第一个真实消费者:被人审否决的指控
在台账主短板栏标"⚠指控被否",此前 3 条真实裁决躺在 sidecar 里没有任何代码读过。
它还取代了 run_school 收尾那句 grep 待裁告警(旧口径把非阻塞条目和 superseded 僵尸
都算进"待裁",虚高)。
2026-08-12 起全流程只有一份队列($W/adjudications.jsonl, 此前分裂在 $W/ 和 $W/eval/ 两处要看两个文件)。七个生产者把"存疑但机器不敢下结论"的 判断入队;设计原则:机器判不了的(同音误转真伪、span内容属实性)不上更多AI层, 给人富证据卡几秒裁决。
flowchart LR
A1["P1 batch_audio
claim_ref=audio_duration_mismatch"] --> Q
A3["P6.5 conf_route
claim_ref=conf_route"] --> Q
A8["P6.8 speech_screen 前置合规闸
claim_ref=speech_*(言语筛查·冻结待裁·附at_sec)"] --> Q
A7["P8.5a1 overload_route
claim_ref=nonteach_overload(三类过半拦截)"] --> Q
A5["P8.5a2 auto_kfa_spans
claim_ref=spanN_指纹(核验卡+三层子票
·重析后新内容可重新入队)"] --> Q
A6["P8.6 asr_blame_check
claim_ref=asr_blame_<sha8>(富证据卡·指纹粒度)"] --> Q
A4["P8.7 auto_kfa_spread
claim_ref=quoteN_指纹(核验卡,2026-08-18起)"] --> Q
Q[("$W/adjudications.jsonl
{sid,claim_ref,target,ai_said,
evidence_summary,verdict,why,
sub_verdicts?}")]:::q
Q --> H["人工:图形化人审页面 :8130
或命令行 adjudicate.py"]
H -->|"conf_route / nonteach_overload:
verdict=错"| OV["apply_adjudication_overrides.py
P6.6 自动回纳"]
H -->|"speech_*: 该课全部条目
都是'错'才放行"| OV
OV -->|"eligible=true
overridden_by_adjudication=true"| R1["conf_route.json /
overload_route.json /
speech_screen_route.json"]
H -->|"asr_blame: verdict=错/拿不准"| BO["同脚本·写 sidecar
claim_overrides.json"]
BO -->|"对外报告过滤"| R2["claim_overrides.json
(不写 eval_codex,2026-08-14 修)"]
H -->|"asr_blame裁决后如需彻底重评"| VC["人工手写
verified_asr_caveats.json"]
VC -->|"P7重新注入+invalidate重评"| R3["eval_codex.json
干净的新分数"]
H -->|"其余(span/quote核验卡)"| N["留痕供人处理
(改进闸值等由人决定,不自动化)"]
classDef q fill:#f4dcd8,stroke:#a8433a,color:#5c2019,stroke-width:1.6px
superseded——已裁的裁决历史保留但不再生效,未裁的从 pending 消失;
③ conf_route 条目记 conf_median + asr_fp(转写文件指纹)——重转 ASR 后
旧"错"裁决自动过期重开,不再永久钉死放行;④ (sid, claim_ref) 不再唯一,
全部消费方(回纳/人审页/adjudicate/各阶段自查/终局台账)统一取未过期那条
(queue_lib.live_entries)。人审页默认隐藏过期条目(勾选"显示已过期"可见,⌛标记禁投票),
并且每张卡写明本票后果(M4:恢复参评 / 撤销标注不影响参评 / 仅留档),
消除"对/错"两个字在不同队列类型下四套语义的误解。首轮生产回归:8 条僵尸正确回收
(4 条旧式无指纹 + 3 条 v5 指纹随 v6 重析再变 + 1 条过期指控),已裁 9 条原样保留。
纯 stdlib 写的 HTTP 服务(opencode 上没装 flask),单文件起服务,浏览器直接打开
http://<opencode>:8130/。按 run 切换队列,待裁决/已裁决/全部三态
筛选;span/quote 类条目自动读 kfa_card_path 拼出完整证据卡(含图片
/media?path= 代理 URL,路径强制校验落在 runs-dir 下防穿越);其余类型
直接显示 ai_said/evidence_summary 文本。课程编号旁标注
源视频文件名(从 batch.json 反查)。裁决写回同一份
adjudications.jsonl,跟命令行 adjudicate.py 走同一套字段,
两种方式可以混用。"回纳生效"按钮直接调用 apply_adjudication_overrides.py,
不用再单独 ssh 敲命令。
sub_verdicts 随裁决一起持久化进队列条目。总 verdict 推导规则
2026-08-14 二审拍板修订:只由"分类"决定——旧规则"任一错→错"会让
引文失真把总票拖成错(T16-P0/span0 实案:分类对/边界对/引文错却存成顶层"错"),
恰恰违背拆三层票的初衷;且总票/引文子票本无自动化消费者(终点升格只读分类+边界),
顶层"错"纯粹误导人工回看与未来报表。边界/引文判断完整保留在 sub_verdicts。
边界信息回收(同批拍板):分类子票="错" → P6.6 写
human_kind_rejected → annotate_spans 撤销该 span 的材料标注;路径A人裁终点 →
annotate_spans 把结束标记移到人裁终点所在句——人工提交的每一层信息都有消费者。
end_pick 随三层子票一起写回队列。视频跳转核听
(2026-08-14):带 at_sec 的条目(教师言语筛查、asr_blame 归因卡——后者 2026-08-14 首轮人审"信息量不够,应提供原始视频"反馈后补上,取首个概念定位时刻,全未定位兜底 0:00)渲染
"▶ 跳到原视频 mm:ss 核听"按钮,点击惰性创建播放器并定位到时间点前 5 秒起播;
红线条目红底白字单独标记;"回纳生效"时人裁终点
升格计入占比(见 P8.5 节升格口径)。顺带修掉一个真 bug:帧文件名带
+(end+5s/strip+15s)在 query 里被解码成空格致 /media 403,
frame_urls 改为 quote() 编码。
| 接口 | 作用 | 安全边界 |
|---|---|---|
| GET /api/runs | 扫描 runs-dir 下所有批次的待裁决/总数 | — |
| GET /api/queue | 某批次队列,自动附KFA卡片+视频文件名 | — |
| GET /media | 图片流式代理 | path 必须落在 runs-dir 下,否则 403 |
| GET /video | 原视频流式播放(HTTP Range,拖动/定点起播) | 路径由服务端按 batch.json 解析 pid,不接受客户端原始路径 |
| POST /api/verdict | 写回裁决(含可选 sub_verdicts / end_pick) | 写锁串行化防并发覆盖;run 名校验防路径穿越 |
| POST /api/apply-overrides | 触发回纳脚本(eligible翻转+blame标记+人裁终点升格) | — |
运维提醒:重启服务不要在 ssh 内联命令里直接 pkill -f review_server.py——
该模式字符串会出现在 ssh 会话自身的 cmdline 里(含日志重定向路径),连自己一起
杀掉、静默失败。项目里固化成独立的 restart_review.sh 脚本规避。
没有身份校验(内网工具,与项目其他内部看板同一惯例)。