simpleEval · run_school.sh 主链路

工作流全景:每一步在做什么、凭什么参数做

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 前置合规闸——有确证发现的课在材料构建之前冻结分流人审,不进评分线, 该课全部条目被人裁"错"才放行。

必做 · 不受开关控制 分流闸 · 判定后路由 默认关闭 · 需显式开启 写入 adjudications.jsonl
姊妹页:《一堂课的全生命周期》——用真实课程逐站走一遍本页描述的管线, 每站附真实产出物摘录与抽帧画面(名词表也在那边)。正文里带虚线下划线 的名词可以鼠标悬停(或点按)弹出名词表里的解释,不用来回翻页对照。
目录
  1. 总览流程图
  2. 筛选与批次入口(管线外)
  3. P1 抽音频
  4. P2 ASR + 说话人分离
  5. P6.5 转写可靠性分流
  6. P6.6 人工裁决自动回纳
  7. P6.8 教师言语筛查(前置合规闸)
  8. P7 课时级转写集合
  9. P8.5 时长占比 + KFA前置核验 + 两类标注
  10. P8 直评出分(codex 单轨 K=3)
  11. P8.6 ASR转写归因核查
  12. P8.7 高分歧自动 KFA 核验
  13. P8.8 终局台账 run_manifest
  14. adjudications.jsonl 队列架构
  15. 图形化人审页面
  16. 已知限制
◆

总览流程图

粗色块=不受开关控制的必做环节;虚线块=默认关闭的诊断分支;菱形=分流判定; 红色柱=人工待裁决队列。同一门课两个课时各自独立走完整条链,互不拼接。

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
三个独立工作流:筛选(虚线白框)在管线之前把不合格文件分拣掉, 批次生成是唯一入口(每个课程组合一个 run 目录),评价管线各阶段支持 --from/--only/--list 阶段选择,收尾必刷 P8.8 终局台账。红色柱是全流程唯一一份人审队列——七个生产者 (P1时长差 / P6.5低置信 / P6.8言语筛查冻结 / P8.5a1三类过半 / P8.5a2核验卡 / P8.6归因指控 / P8.7高分歧核验卡), 一个消费入口(图形化人审页面,也可用命令行 adjudicate.py)。 P3~P6 课程级诊断分支已整体移出本仓库(需要去 tjlg_xsx_2025 跑)。
00

筛选与批次入口(管线外的两个独立工作流)

screen_videos.py ① 批量筛选:不合格文件进管线前分拣掉 独立工作流

2026-08-12 拍板:不合格检测(原 P0 健康体检 + 原 P1.5 数字静音)从单校管线挪出, 成为独立"分拣"工作流。三道判据全部沿用既有实证,不新定阈值。 产出 qualified.txt(直接喂批次生成)+ disqualified.txt(带原因)。

参数取值逻辑
ffprobe 体检可读 + 有音轨读不出/无音轨的文件无法评价
--min-dur1800 秒半堂课不评(build_session_manifest 统一口径)
--zero-threshold10.0%某秒采样全零=录制中断(zeroscan 生产定稿);"近静音"音量判据已被1471门实测证伪,不做
--jobs / --skip-zero-scan4 / 关并发;跳过静音扫描可加速但漏录制中断类问题
make_batch.py ② 批次生成:唯一入口,换课程组合就换一个 run 唯一入口

三种来源产出同一种 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 密钥。

01

P1 抽音频

batch_audio.py 视频(只读区)→ 16k mono mp3 必做

只读纪律:输入路径必须以 /data/ro/ 开头,脚本自己断言,不靠自觉—— 原始区是 CIFS rw 挂载,历史上有写坏风险。瓶颈在 NAS 读带宽不在 CPU, 并发给 4 就够。断点续跑:目标文件已存在且 >10KB 就跳过。

抽完顺手核对时长:抽出音频与源视频时长差 >2 秒暗示解码可能丢了一段。 这条 2026-08-11 才接入人审队列——此前只打印 WARN 不拦截也不留痕。 不改变现有行为:文件仍照常产出、照常往下游走,只是多一条待裁决记录, 不会让这门课凭空从批次里消失(FAIL 才会让整体非零退出,是硬故障,不进这条"存疑"队列)。

参数取值逻辑
--jobs4并发抽取;瓶颈在 NAS 带宽不在 CPU
ffmpeg 编码-ac 1 -ar 16000 -b:a 48k -f mp3单声道 16kHz 48kbps,ASR 输入规格
断点判据size > 10240 字节已存在且非空才跳过,避免半成品被误认已完成
时长差阈值> 2 秒 → WARNWARN 入队仅记录;ffmpeg 非零退出或 ffprobe 读不出时长才是 FAIL(硬故障)
写盘方式先写 .part 再 os.replace防止中途失败留下半成品被当作已完成文件
02

P2 ASR + 说话人分离

transcribe_diar.py fun-asr 批量转写,diarization 开启 必做

本环节不做"提交前排除"——不合格文件(读不出/无音轨/过短/数字静音)由管线外的 筛选工作流在批次生成前分拣掉,manifest 里有什么就转什么。 --work 2026-08-12 起改为必填(原默认指向另一仓库部署路径, 裸调会静默读写别的工作目录)。

按 result.file_url 回映射 sid,绝不按下标——批量返回顺序 ≠ 提交顺序, 这是最容易踩的坑之一(脚本自带 --shuffle 用于主动验证这条不会串课, 归一化后 URL 冲突或同一 sid 被映射两次都会直接中止,不静默继续)。 热词按词读表,不是全表统一权重——weight=5 全表盲铺会把"五育/五通/五史"这类词 强插进正常语句,那是污染不是纠正。

参数取值逻辑
modelfun-asrDashScope 异步转写
--chunk4每批提交的课时数
diarization_enabledTrue说话人分离——学生发言实质度/互动真实性的硬依赖
disfluency_removal_enabledFalse保留口语原貌,不做顺滑处理
--hotwords共享路径 asr_hotwords_calibrated.json与 tjlg_xsx_2025 刻意共享,不拷贝进 simpleEval 自己目录(避免"哪份最新"漂移风险)
热词 weight主体 5,≤3字短词降 4逐词读表;短词过高权重易过度偏置
产出asr_raw/{pid}.json + diar.jsonasr.json/ts.json/asr_ts/ 2026-08-14 审计停产——仓库内零消费者,声称的下游(class_span 等)是已移除分支;需要时从 asr_raw/ 重新派生
P3~P6 课程级诊断分支已整体移出本仓库(2026-08-12)——课时定序/去重、 置信度聚合、课型普查、抽帧信息卡的脚本从来没拷进 simpleEval,原来的调用段和 ENABLE_* 开关是"一开就崩"的陷阱,已连同开关一起删除。需要那些课程级诊断, 去 tjlg_xsx_2025 仓库跑。
03

P6.5 转写可靠性分流

conf_route.py 听不懂的课不能悄悄流进打分材料 必做 · 唯一写死不给开关的分流闸

依据 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 门"的覆辙,但仍是个固定阈值,没有针对本语料重新标定, 是用户已知情接受的简化。

2026-08-12 局部塌陷检测已拆除:曾新增过 60 秒滑窗局部塌陷检测 (整课median达标但某窗口置信度骤降仍路由人审)+ 剪音频片段可听化 + prep_score_inputs.py 的 G-4 软带护栏,真机验证过真实案例 (某课整课 median=0.968 轻松过闸,但 480 秒起有 60 秒窗口 median 仅 0.236, 652 个词 confidence<0.4——局部塌陷是真实现象,不是假设)。 用户最终拍板:整套加入生产链路的复杂度收益比不够,只保留整课单一闸。 中间地带(过了硬闸但局部质量一般)不再有专门护栏,是明确接受的简化,不是遗漏。
参数取值逻辑
--threshold0.75整课 conf_median 闸;< 阈值路由人审,不自动排除,等人工裁决
判据来源asr_raw/{pid}.json 逐字 confidence取全部带 confidence 字段的词,算中位数
数据缺失处理asr_raw 缺失/无 confidence → eligible=false与"达标/不达标"分开统计,不混为一谈
--window-threshold 等 7 个参数已删除2026-08-12 局部塌陷检测整套拆除,见上方说明
04

P6.6 人工裁决自动回纳

apply_adjudication_overrides.py 人工把 AI 判断否掉后,自动把排除撤回去 必做 · 全流程仅跑一次

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 的条目直接跳过可安全重复调用
asr_blame 裁决消费——两级(2026-08-13 新增;2026-08-14 审计修正为只写 sidecar): 轻量级:claim_ref=asr_blame 判"错"或"拿不准" → 写 eval/data/claim_overrides.json sidecar,对外报告读它过滤被否决指控—— 分数不变,成本为零(不重跑)。⚠️ 原先还往 eval_codex.json 写 blame_overridden 标记——审计发现该标记必被 P8 开头"从 openclaw 拉缓存" 覆盖,寿命只有几分钟,最终文件里从来不会有它(与 P8.5S 同形状的错位:打分产物的 裁决消费挂在了打分前的 P6.6),已删;报表一律读 sidecar。 彻底重评:需要真正干净的分数时,把人工核验的具体发现写进 P7 的 verified-caveats,让 invalidate_stale_cache 带动那门课重新打分——这才是真正抹掉冤枉扣分的做法,轻量级标记不解决"旧分数 本身仍隐含冤枉扣分"这个问题。两级都已在真实数据上验证过。
05

P6.8 教师言语筛查·前置合规闸(2026-08-14 二次拍板)

覆盖范围用户逐项拍板:红线(价值导向/立场, severity 单独标记)/ 跑题 / 身份不符言语(重点)/ 性质不符内容。处置口径当日两次演进:首版"P8.5S 只推人审不拦截"被 二次拍板取代——筛查前移到材料构建(P7)之前,有确证发现的课 冻结分流人审,不进评分线的任何后续环节(不建材料、不进 span 分析/KFA、不打分)。 放行铁律:该课全部 speech_ 条目非阻塞(人裁"错",或 二次放行——2026-08-14 拍板:verdict="对"问题在案时,人可在人审页 显式裁定"准予回到评审队列",/api/release 写 entry.release=true+必填理由,发现记录 不动)才翻回 eligible=true;"对"未放行/"拿不准"/未裁维持冻结。条目带时间点, 人审页面可一键跳转原视频对应时刻核听。概念/事实类指控(专名提法、概念混淆、史实数据) 不在本模块,走既有 P8.6 asr_blame 通道。

speech_screen.py

为什么独立模块(用户追问后拍板):任务性质与 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 最容易伪造满足的一道;人裁"错"回纳 放行后下轮自然补筛。

防误报四层确定性证据闸(合规线实证"LLM 找茬先验用 prompt 压不住", 故不依赖模型自觉):① 引文逐字防伪(quote 在声称区间内滑窗命中,编造即丢); ② 播片段排除(prompt+代码双层);③ 跑题/性质不符 ≥120 秒时长闸(时间戳核算, 实测精准拦下一个 119 秒边界案例);④ 引文句 conf<0.85 自动标 "⚠️疑转写失真请先核听"——粗口/怪话可能是 ASR 伪造的,人审必须核听,这正是 视频跳转按钮存在的理由。prompt 排除清单:转述/引用批判靶子(看教师自己的结论 立场,不看被引述内容)、教学性反讽设问、口误自纠、乱码文本。
首轮验证(2026-08-14):合成正例(真实 diar 注入温和测试语句) 3/3 命中入队且红线单独标记,视频 span 内注入被排除,三条代码闸单测全过; 12 门真实课负例回归:b_tjjt 0 条,b_tjpu10 4 条全为"身份不符"(无红线误报, 含 conf 0.65 自动警示案例),人审负担 0.33 条/课时。prompt 已按首个人审判例收紧 ("转述中逐字复读的不当称谓属被引述内容,不报")。
前置闸真机回归(b_tjpu10,2026-08-14):T08-20250320 两课时因人审 确认"对"的发现(透露单位内部考核排名 / 粗俗用语)被冻结,退出评分线,eval_codex 不再含其分数;T10/T16 两条人裁"错"(ASR 误转、转述美方言论)正常放行参评。 放行(全部条目"错")/维持冻结(对/拿不准)/幂等三条路径单测通过; speech_screen 自查队列与 P6.6 回纳两处同语义、无顺序依赖。
参数取值说明
ENABLE_SPEECH_SCREEN1(默认开)独立开关,0=整体关停本模块(连同前置闸一起关)
模型qwen3.7-max K=1与 segment_ratio 同模型,同一份带时间戳 diar 输入
SPAN_KINDS_MIN_SEC跑题/性质不符 120s成段类时长闸;红线/身份不符为句级不设时长闸
LOW_CONF0.85引文句 conf 低于此值附"疑转写失真"提示
claim_refspeech_<kind>_<锚句号>重跑稳定,队列按 sid+claim_ref 去重
分流闸文件eval/data/speech_screen_route.json{pid: {eligible, findings, kinds, pending_refs}};P7 --extra-route 消费,segment_ratio/prep_score_inputs 亦读它跳过/清残留
06

P7 课时级转写集合

build_period_asr.py 拼全文,双重分流闸取并集排除 必做

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
人工核验转写存疑提示(2026-08-13 新增,不占护栏编号,G-3 拆除后仍保留): --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 成本。
07

P8.5 时长占比 + KFA前置核验 + 两类标注

segment_ratio.py → … → prep_score_inputs.py(材料唯一出口)→ annotate_spans.py 要回答"占了多少",不是"有没有" 可插拔,两个开关独立

教师顺口一句"上节课我们讲了 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 非主讲占比闸而非视觉判断——分享的学生可能不入镜,"用视觉核验证明学生展示是错的"这个 方法论本身在最早试点就踩过坑。

播片段"教师收束"conf 降级——2026-08-14 二审拆除:曾在终点句 conf<0.70 时把 end_basis 从"教师收束"强制降为"推断"(2026-08-13 加,n=2 标定)。 拆除依据:end_basis 全仓唯一消费者是核验卡展示(视频播放因不计入自动占比, 降级对任何口径零影响);它防的"乱码收束语误导人"已被终点帧带+卡片失真警示覆盖。 模型自报的 end_basis 原样保留;帧带的 conf 恢复辅助线是另一机制未动。
参数取值逻辑
--modelqwen3.7-maxplus 单遍召回仅 51%(整段漏看),max 100% 且 11/12 完全可复现,K=1 即可不必 K=3
--workers3DashScope 并发>3 必 429
MIN_SENT / MIN_SEC3 句 / 60 秒最短区间闸
STUDENT_NONMAIN_MIN_RATIO0.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_tokens8000防模型间歇性冗长丢样本,不算"修复",真正对症的是 K=3 取中位(此处 K=1)
P8.5a2 span级KFA前置核验(2026-08-12 新增,ENABLE_KFA_SPANS 默认开): 过闸待标注的 span 先跑 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极少,多数课零开销。
视频播放终点帧带 + 人裁终点升格(2026-08-13/14 新增): end±5s 只能回答"声称终点处还在放吗"这个是非题——终点报偏几分钟时人只知道 "错了"不知道错多远。视频播放 span 的核验卡额外抽终点帧带 (声称终点±2分钟、每15秒一帧共17帧)+ 逐帧标就近句 ASR conf(<0.70≈视频 原声),conf 连续两句恢复到 0.90+ 处紫框标建议终点(辅助线,人裁决为准); 人审页面点选"视频已结束的第一帧"产出 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 移植成功零人工成本。
P8.5a3 视频原声反哺——2026-08-14 随 G-3 拆除移除: 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 抓出来)。

08

P8 直评出分

codex_score.py + run_api_tracks.py codex 主轨必跑,其余模型声明式可插拔 codex 必做 · qwen 等轨可插拔

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 均值)。

codex 打分缓存坑(2026-08-11 端到端实测踩过;2026-08-14 机制改造):真正的状态文件在 openclaw 本地,续跑逻辑只看 K 计数、看不出内容变了。每次先把 openclaw 上现有缓存拉回 本地,过一遍 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-sol3jobs=4zhiping_system_D6.txt + schema_D6.json含 main_weakness(诊断用短板归因)。三类过半判断曾短暂放此处(nonteach_overload 字段),2026-08-14 当天二次拍板撤回——评分材料无时间戳按篇幅估有盲区、评分 prompt 须单一职责;判定移至 P8.5a(有时间戳的环节),prompt 已 git 精确恢复原版
qwen(可插拔,当前禁用)qwen3.7-max3workers=3zhiping_system_D6_qwen.txt无 main_weakness——v4 6选项含"无明显短板"逃逸项,qwen 用了25%次数解释71%分数方差;v5 5选项强制后 97%课程坍缩单一标签"平铺罗列",两版均验证失败,故拿掉
参数取值逻辑
--effort(codex)mediumcodex reasoning effort;三档已验证过一致性
--sandbox(codex)read-onlycodex 执行环境只读,不允许改动
timeout(codex)900 秒单次调用超时
enable_thinking(qwen)false关闭思考链,控制成本与延迟
X-DashScope-DataInspection(qwen){"input":"disable","output":"disable"}思政 ASR 内容会触发绿网审核 400 data_inspection_failed,须显式关闭
SPREAD_WARN10(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 转写失误防偏失设计的第二层;独立判断,不参考任何其他课程或 模型的分数。

09

P8.6 ASR转写归因核查

指纹去重(2026-08-14 简化批次):claim_ref 从固定 "asr_blame"(一 sid 一条) 改为 asr_blame_<指控文本sha256前8>——旧口径下重评后模型提出内容 不同的新指控会被旧条目挡住不入队(实测发生过),现在新指控重新入队、旧裁决只盖 旧指控;存量旧式条目按其 ai_said 算等价指纹兼容去重,模型重提相同指控不重复入队。
asr_blame_check.py 教师错误类指控一律推人审,附富证据卡 必做 · 2026-08-12 新增

要解决的问题:ASR 偶有转写不准,模型据此指控教师"概念错误/说错/史实错误"并压分—— 但那可能是 ASR 转错了字,不是教师说错了话。codex CLI 拿不到思考过程, reason/main_weakness/evidence_quotes 就是可观察的 "思考痕迹",本环节扫描它们。

口径A(有指控就推)是8校190门实测拍板的,不要改回conf二级闸: 触发率仅 7.4%(14/190),人审负担小;被指控课均分 59.8 vs 无指控课 64.7 (压分 -4.9 分真实存在)。conf 闸被数据否决——同音误转(国体/政体、国情/国企) 恰恰是高置信度转写错误,实测被指控课证据片段 conf 中位数 0.976, 最重要的同音案例 conf 全在 0.94+,conf 闸会系统性漏掉它们;指控里的原话短语仅 4/20 能逐字定位回转写(模型常转述),定位失败也不能当"编造"判据(但值得人看—— 实测有"汉谟拉比法典推动罗马帝国全盛"这种转写里找不到原话的指控,疑似模型幻觉)。

富证据卡:定位/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纯确定性检测+定位,零边际成本
10

P8.7 高分歧课时自动 KFA 核验

auto_kfa_spread.py 模型自己都判不准的课,自动抽帧核实 必做 · 不受开关控制

背景: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-threshold10(2026-08-13 用户拍板下调)与 codex_score.py 的 SPREAD_WARN 保持一致;实测依据见该表
--run-idx0(K=3 第 0 轮)核验目的是"证据是否存在",不需要每轮都核
覆盖范围只读 eval_codex.jsonqwen 轨不在覆盖范围内(已知限制,见文末)
帧位4 帧硬编码(起点+2s / 中点 / 终点-5s / 终点+5s,2026-08-13从3帧改;--frames-per-span 参数已失效不再读取)<6 秒的区间只抽 1 帧;终点前后各一帧便于对比边界切没切准
--context-sents默认 5画面信息不足时向前回查几句 diar 原文找文本佐证
11

P8.8 终局台账 run_manifest(2026-08-18 新增)

审计发现的唯一业务级缺口:管线跑完后没有任何文件回答"谁出了分、 谁被哪道闸挡了、谁在等人"——b_tjpu10 批次 10 课时只有 8 门进 eval_codex.json,被 合规闸冻结的 2 门从结果里彻底消失,要知道去向得同时读两个文件。1000 门规模下静默丢 20% 而交付方不知道,是交付事故不是 bug。

run_manifest.py 纯只读派生 · 每次跑完都刷新(含 --only 局部重跑)· 失败不毁批

每课时一行 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-18 生产回归):台账直接点名参评池 8→6 的原因 ——全量重筛(存量条目补 prompt_sha 触发)新抓 2 条边界发现(T10-P1 教师调侃学生 转卖课本 / T15-P0 疑似"特别讨厌"贬损语,模型自标可能 ASR 失真),两门新冻结待人裁。 这不是回归失败,是筛查非确定性的真实暴露:单遍筛查召回不完整, 重筛会抓到首轮漏掉的边界案例(宁多审勿漏审侧)。终态:10 课时 = 6 scored + 4 frozen_speech,阻塞性待裁 4 条。
◆

adjudications.jsonl 队列架构

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
三类分流裁决有自动回纳效果:conf_route/nonteach_overload 单条"错"即翻回 eligible=true;speech_* 前置合规闸须该课全部条目均"错"或已二次 放行(release,2026-08-14 拍板:问题在案准予参评)才放行("对"未放行/"拿不准" 维持冻结)。asr_blame 裁决有两级 消费:sidecar 过滤零成本但分数不变(2026-08-14 起只写 claim_overrides.json,不再写 eval_codex——那个标记必被 P8 拉缓存覆盖),需要真正干净的分数得走 verified_asr_caveats 触发彻底重评(真机验证:+7.3/+4.7分,指控不复发)。span/quote 类核验卡裁决留痕供人处理, 不自动化。
条目内容身份与 superseded 终态(2026-08-18 修缮批,H2):审计在 b_tjpu10 实测发现队列 19 条里 5 条已是"僵尸"——重析后内容变了,已裁完的旧条目在 人审页显示的却是被覆盖后的另一段证据。修缮后:① span/quote 类 claim_ref 带引文指纹, 核验卡与帧图文件名同指纹(重析写新文件,旧证据永不被覆盖,过期条目 仍能回看自己那张卡);② 生产方每轮做失配检测,内容对不上当前产物的旧条目自动标 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 条原样保留。
◆

图形化人审页面

review_server.py + review_ui.html 填补"KFA核验卡/队列无可视化前端"的已知限制 2026-08-12 新增,内网 :8130

纯 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 敲命令。

span核验卡三层子票(2026-08-13 新增):单票"对/错"会把分类/边界/ 引文三层信息压扁——首批人审两条"错"票实为"分类对、边界对、只是引文文本因播片段 ASR失真而错"。现在 span 类卡片拆成分类(这段真的是这个行为吗)/ 边界(起止时间准吗,对照 end±5s 两帧)/引文 (卡面引文是ASR原文仅作定位,播片段常失真,请以画面为准)三问各自投票, sub_verdicts 随裁决一起持久化进队列条目。总 verdict 推导规则 2026-08-14 二审拍板修订:只由"分类"决定——旧规则"任一错→错"会让 引文失真把总票拖成错(T16-P0/span0 实案:分类对/边界对/引文错却存成顶层"错"), 恰恰违背拆三层票的初衷;且总票/引文子票本无自动化消费者(终点升格只读分类+边界), 顶层"错"纯粹误导人工回看与未来报表。边界/引文判断完整保留在 sub_verdicts。 边界信息回收(同批拍板):分类子票="错" → P6.6 写 human_kind_rejected → annotate_spans 撤销该 span 的材料标注;路径A人裁终点 → annotate_spans 把结束标记移到人裁终点所在句——人工提交的每一层信息都有消费者。
视频播放卡片的终点帧带交互(2026-08-13/14 新增):横向滚动 时间带(±2分钟/15秒一帧),橙框=声称终点、紫框=conf恢复建议终点;图片点击 放大、每帧下"已结束⏹"小按钮点选(两个手势不打架),另有"末帧仍在播放"兜底。 选择作为 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 脚本规避。 没有身份校验(内网工具,与项目其他内部看板同一惯例)。

◆

已知限制(诚实列出,不是遗漏)