Skip to content

Latest commit

 

History

History
121 lines (85 loc) · 12.2 KB

File metadata and controls

121 lines (85 loc) · 12.2 KB

ADR-013: 实时会话引擎预热与播放暂停驱动(预备线程 + 共享暂停标志)

状态

已接受(2026-08-19 用户需求评审:课堂助手三项优化——停止即时/播放暂停驱动/引擎预热)

日期

2026-08-19

背景

课堂助手实时捕获存在三处体验缺口:

  1. 初始化时机:StreamingAsrEngine::load(SenseVoice + 标点恢复,数秒)在点"开始"后的会话线程内执行,启动延迟全部落在用户点击之后。
  2. 暂停语义:REQ-125 已能检测视频播放/暂停(暂停图标颜色统计,落 PlayerBehavior 事件),但只落库不驱动捕获——视频暂停时应用仍持续采集/OCR。
  3. 停止响应:停止后 drain 宽限 8s 内捕获线程持续喂块,队列永不空,stop_active 5s 有界等待必然超时(detach 日志常态出现)。

约束:StreamingAsrEngine 含 FFI 句柄(非 Send),不能跨线程移交;同时期 A1 硬暂停(WASAPI 端点 Stop/Start + 时间戳补偿)已落地共享 SessionPause 机制。

决策

  1. 引擎预热 = 预备线程 + 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(加载完即退出)。
  2. 播放暂停驱动捕获 = 复用 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 与停止路径共用,防实现漂移;时间戳统一补偿暂停时长)。
  3. 停止即时:主循环观察到停止标志时先 audio.stop()(捕获线程 ≤100ms 退出 → channel 断开),再 drain 残留块(通常 <1s),Disconnected 后退出;8s 宽限保留为理论兜底。stop_live_session 补 spawn_blocking(此前异步 command 直接阻塞运行时 5s)。
  4. A1 补漏:SessionPause 按会话复位(reset())——暂停标志/补偿时长跨会话残留会让新会话起始即暂停、时间戳偏移。

备选方案

方案 A:预备线程 + channel 交接(选定)

  • 优点:引擎不跨线程(规避非 Send 约束);预热与开始解耦(模型加载移出点击路径);TTL/释放命令双保险控内存;失败回退内联零回归。
  • 缺点:预热线程 park 期间占用内存(~数百 MB);交接路径需处理加载中/失败竞态(有界等待 + 回退 + send 失败取回参数)。
  • 适用场景:引擎加载耗时且生命周期与 UI 流程解耦的场景。

方案 B:预热结果放共享槽、会话线程自取

  • 缺点:引擎非 Send 无法存入跨线程共享槽(结构上不可行)。
  • 适用场景:无。

方案 C:自动暂停独立实现(自有标志 + 循环内丢块)

  • 优点:与 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(相关文件行数登记)

WAV 轴与会话轴的对齐(2026-09-13 加注,批 6 T23 / R5.5-b)

上文一字未改;本节是新增的运行时源码级探针结论与修复方案(出处:.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 本环境不可达 ⇒ 未验证。

批 6 收口复核(T35,2026-09-13 —— 上节原文一字未改)

性质:对上面「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 文案不得说「这条录音没有对齐」。