已接受(2026-08-19 用户需求评审:课堂助手三项优化——停止即时/播放暂停驱动/引擎预热)
2026-08-19
课堂助手实时捕获存在三处体验缺口:
- 初始化时机:
StreamingAsrEngine::load(SenseVoice + 标点恢复,数秒)在点"开始"后的会话线程内执行,启动延迟全部落在用户点击之后。 - 暂停语义:REQ-125 已能检测视频播放/暂停(暂停图标颜色统计,落 PlayerBehavior 事件),但只落库不驱动捕获——视频暂停时应用仍持续采集/OCR。
- 停止响应:停止后 drain 宽限 8s 内捕获线程持续喂块,队列永不空,
stop_active5s 有界等待必然超时(detach 日志常态出现)。
约束:StreamingAsrEngine 含 FFI 句柄(非 Send),不能跨线程移交;同时期 A1 硬暂停(WASAPI 端点 Stop/Start + 时间戳补偿)已落地共享 SessionPause 机制。
- 引擎预热 = 预备线程 + channel 交接(不移动引擎):
- 新命令
prepare_live_session(进课堂助手页即调用)起"预备线程":加载引擎 → 置Ready→ park 在原线程等 Start/Cancel(500ms 轮询,15min TTL)。 start_live_session优先交接:有界等待就绪 ≤5s(防双引擎内存翻倍)→ channel 发 Start 消息(参数/会话 id/停止标志/暂停状态)→ 预备线程就地转为会话线程,开始毫秒级;加载失败/等待超时/交接失败 → 取消预备线程并回退内联加载(现状路径,start 永不因预热缺席失败)。release_live_prepare(离开页面调用)有界 join ≤1s;引擎加载中不可中断时 detach(加载完即退出)。
- 新命令
- 播放暂停驱动捕获 = 复用 A1 共享暂停标志:
- 屏幕 worker 的 REQ-125 检测在检测到暂停时置
SessionPause.paused(首检基线同置,不写假事件)——音频捕获线程/主循环沿既有 A1 边沿执行真实暂停(端点 Stop、不喂 ASR、不写 WAV、时间戳补偿)。 - 自动暂停期间 worker 进入轻量轮询(区别于手动暂停的全冻结):1s 一拍仅取帧刷新
latest_frame(恢复检测信号源)+ 5s 一拍检测;检测到恢复 → 清标志 + 落 Play 事件,链路沿边沿自动恢复。 - 暂停边沿的断句隔离由"喂 100ms 静音"(A1 原方案,不足以触发 sherpa 端点规则)改为 flush 尾句落库 + 引擎
reset()(flush_tail_and_persist 与停止路径共用,防实现漂移;时间戳统一补偿暂停时长)。
- 屏幕 worker 的 REQ-125 检测在检测到暂停时置
- 停止即时:主循环观察到停止标志时先
audio.stop()(捕获线程 ≤100ms 退出 → channel 断开),再 drain 残留块(通常 <1s),Disconnected后退出;8s 宽限保留为理论兜底。stop_live_session补spawn_blocking(此前异步 command 直接阻塞运行时 5s)。 - A1 补漏:
SessionPause按会话复位(reset())——暂停标志/补偿时长跨会话残留会让新会话起始即暂停、时间戳偏移。
- 优点:引擎不跨线程(规避非 Send 约束);预热与开始解耦(模型加载移出点击路径);TTL/释放命令双保险控内存;失败回退内联零回归。
- 缺点:预热线程 park 期间占用内存(~数百 MB);交接路径需处理加载中/失败竞态(有界等待 + 回退 + send 失败取回参数)。
- 适用场景:引擎加载耗时且生命周期与 UI 流程解耦的场景。
- 缺点:引擎非 Send 无法存入跨线程共享槽(结构上不可行)。
- 适用场景:无。
- 优点:与 A1 硬暂停解耦。
- 缺点:重复实现暂停机制(WAV 间隙/时间戳补偿/事件三处漂移);A1 已有真实端点停采+补偿,叠加自实现违背"复用存量"原则。
- 适用场景:无(A1 机制已存在,直接驱动更优)。
- 引擎非 Send 是硬约束——方案 A 是唯一能"提前加载 + 线程内使用"的结构。
- 播放暂停自动化的成本集中在检测端(worker 轻量轮询),驱动端完全复用 A1 已验证的硬暂停(端点 Stop/Start 无重连风险、时间戳补偿无跳跃)。
- 停止即时化改动最小(一处
audio.stop()前置),同时消除 detach 常态日志并修复 stop 阻塞运行时线程的问题。
- 点"开始"到录制开始从数秒降至毫秒级(预热就绪时)。
- 视频暂停自动停采(省 OCR/磁盘/静音音频),恢复自动续采,时间轴无跳跃。
- 停止响应 <1s,detach 日志仅真卡死时出现;暂停前后句子不连句。
- 预热引擎常驻内存(~数百 MB),离开页面/TTL 才释放——受 15min TTL 与页面卸载释放约束。
- 自动暂停依赖暂停图标可见(窗口前台、播放器显示图标);遮挡/最小化可能漏检或误判恢复(沿用 REQ-125 保守检测的既有局限)。
- 预热后词表变更在首个端点时生效(现状语义,无回归)。
- 交接竞态(预备线程在 send 前退出):已用 SendError 取回参数回退内联 + 防御性报错兜底。
- 手动恢复时视频仍暂停 → worker 兜底重新自动暂停(语义:捕获跟随视频状态;用户可再次手动继续,5s 内复判)。
- 暂停边沿 flush 含 SenseVoice 重打分(有界 3s)——暂停/停止时主循环短暂阻塞,捕获已停无积压,可接受。
cargo test全绿(936 例);cargo check --all-targets无新增告警。- 前端
npm run build(tsc + vite)通过。 - 验收路径(真机):① 预热就绪后点开始 →
live:status recording毫秒级;② 视频暂停 → 5s 内live:paused+ 端点停采 + 屏幕零分析;恢复 → 自动续采、时间轴连续;③ 停止 → <1.5s 返回、无 detach 日志。 - 手动暂停/自动暂停共用同一标志与事件,前端按钮与徽标零改动复用。
- ADR-007: live-session-lifecycle(会话线程编排;本 ADR 在其上加预备交接)
- ADR-008: 会话纪元统一(A1 时间戳;预备交接的纪元在移交后创建,无偏移)
- ADR-012: 流式 ASR 质量修复(F1-2 flush 重打分兜底——flush_tail_and_persist 复用)
- 2026-08 A1 硬暂停设计(docs/archive/2026-08-19/brainstorming-classroom-homepage-controls.md)
docs/archive/2026-08-19/2026-08-19-classroom-live-optimizations-design.md(本批设计规格,[ ] 已归档 2026-08-19)docs/standards/line-limit-exemptions.md(相关文件行数登记)
上文一字未改;本节是新增的运行时源码级探针结论与修复方案(出处:
.superpowers/sdd/2026-09-12-frontend-redesign-batch6-motion/probe-audio-runtime.md② 与同目录rulings.md§五 R5.5-b)。
问题:上文第 2 条的「不写 WAV」在暂停期成立(端点 Stop + 捕获循环 continue),但暂停本身不产生偏移(端点停采 ↔ 时间戳冻结严格抵消)。真正的偏移来自「写块路径纯追加」:WAV 轴 = 已写样本数 ÷ 16000,而会话轴 = 会话纪元 − 累计暂停。偏差清单(量级由源码常量推导,非真机实测):
| # | 偏差源 | 方向 | 量级 |
|---|---|---|---|
| D1 | WAV 的 0 点 = 第一个被捕获样本,会话纪元更早 | WAV 落后 | 内联启动路径秒级 / 预热路径 ≈数十 ms |
| D2 | 静默窗根本不产包(GetNextPacketSize == 0 ⇒ sleep + continue) |
WAV 落后 | = 没有声音的总时长,上不封界 |
| D3 | 每次恢复丢未满 200 ms 的残块 | WAV 落后 | ≤199.9 ms / 次 |
| D4 | 恢复时丢端点积压包 | WAV 落后 | ≈10–100 ms / 次 |
| D5 | 停止时尾块丢弃(ChunkAccumulator::flush 无调用点) |
只影响末尾 | ≤199.9 ms |
| D6 | 逐包重采样 floor 余数不回带 |
WAV 落后(累积) | 48 kHz 整除包 = 0;44.1 kHz 未实测 |
修复(批 6 T23):写块按 AudioChunk.timestamp_ms 与「上块末端的会话时刻」之差补等长静音(PCM16 全 0)⇒ WAV 轴 ≡ 会话轴,一次收掉 D1/D2(D3–D6 逐字登记为残余)。timestamp_ms 已含暂停补偿(capture/audio_loopback.rs 逐字 epoch.elapsed() - total_paused_ms,暂停时长在恢复成功时才累加)⇒ 补静音基准与 segments[].start_ms 同轴,无需另立基准;补静音上限 10 分钟(超大空档宁可不对齐,也不写巨量静音)。
自证量与失效安全:会话 finalize 时写 {id}.wav.meta.json(键 version / aligned / firstTsMs / samplesWritten)。aligned 是自证量、禁止恒 true:时间戳缺失/为负/回退/超大空档之一 ⇒ 永久置 false 并退回纯追加(不补静音、样本一个不丢、不阻断会话主链路)。本修复之前录的 WAV 没有 sidecar ⇒ aligned = false —— 历史录音不对齐、无时间基准 ⇒ UI 对无基准的录音不得假装精确(只能近似定位或只读降级)。
AGENTS.md §10 额外审查记录(live_session*.rs 属隐私敏感面):① 本次改动只加静音填充、不改采集语义(capture/ 零改动);② 不新增系统调用、不改变文件位置与权限(仍 {data_dir}/session-audio/);③ 不新增依赖;④ 不影响暂停语义(暂停期 write_chunk 完全不被调用 ⇒ 补静音逻辑不执行);⑤ 回滚 = 还原 1 个签名 + 1 个调用点 + 删 2 个新文件。真机播放/seek 本环境不可达 ⇒ 未验证。
性质:对上面「WAV 轴与会话轴的对齐」节的最终读数复核(不新增决策、不改上文)。出处:
.superpowers/sdd/2026-09-12-frontend-redesign-batch6-motion/task-34-report.md。
- TODO 全部结清:① 补静音纯函数
audio_align.rs+audio_align_tests.rs(T23 的375e5058)· ② 写块接线audio_store.rs的write_chunk(timestamp_ms)+finalize写 sidecar(911b6190)· ③ 唯一调用点live_session_loop.rs· ④audio_store_tests.rs的既有调用点改签名(只改调用形态、不改期望值)· ⑤audio_store.rs:3的@ai-context就地更正 · ⑥ 本节(T23 加注)。 - 常量终值:
MAX_GAP_MS= 10 分钟(600000 ms)(超限 ⇒None+aligned = false,不补);sidecar 键逐字冻结 =version/aligned/firstTsMs/samplesWritten({id}.wav.meta.json)。 - 门禁读数(T34 终态):🔴
cargo test --test app_lib_tests真跑 ——running 2335 tests→2329 passed; 0 failed; 6 ignored,exit 0;Rust 侧用例数与批 3 基线持平(Δ 0);cargo clippy --all-targets:error 0 · lib warnings 15 = 基线。前端 registry 313 / 313 / 0(session_audio_path= 本批唯一新增 IPC)。 - 判据落点:
audio_align_tests.rs(空档补静音的构造性正确、时间戳缺失 / 非单调 ⇒ 纯追加 +aligned = false且样本一个不丢、批量 ≡ 增量对拍)·audio_store_tests.rs·commands_audio_tests.rs(无音频 ⇒None/ 非法 id ⇒Err/ 路径越界 ⇒Err)·types_contract_tests.rs的assert_wire!。 - 🔴 未验证(诚实单列,不得读成「已验证」):真实媒体播放与 seek 本环境不可达(jsdom 无媒体栈 · headless Edge 不说
asset:协议 · 真机/WebView2 用户已裁决跳过)⇒ 不得出现「播放已可用」「seek 已验证」类表述;本节的播放头只能给属性契约(<audio src>/preload/onTimeUpdate)与纯函数(ms → 位置)两级判据。aligned的语义仍然是「不能保证对齐」(历史录音无法判定),UI 文案不得说「这条录音没有对齐」。