Repository navigation
Conversation
端点环回(getDisplayMedia)在系统主音量之后截取全系统混音,导致两类 问题:跨应用杂音混入课堂转写(ASR 幻觉的可能成因之一),以及音量 0/ 设备错配时采到全零。引入 Windows 10 2004+ 进程环回作为互补音频源。 Phase 1 spike 全部硬验收通过(实测数据见 README 与 ADR): - 杂音隔离:同时刻发声进程 RMS 0.049 / 静默进程 RMS 0.000000 - Chromium 进程树覆盖(网课成败点):Electron 声源 RMS 0.055, 证明能采到 audio service 子进程的声音 - 系统静音下峰值与正常音量完全一致(连衰减都没有) - 16kHz mono float32 被直接接受,无需实现重采样 - Chromium 顶层窗口归属 browser process,窗口 PID 即进程树根 ADR-001 记录决策:双源互补而非替换——端点环回是设备视角(永不漏采但 脏),进程环回是应用视角(干净但可能漏采);虚拟声卡方案因需内核驱动 签名与用户安装摩擦被否决。 本模块独立于 client 主构建(未接入 package.json / electron-builder), 对已发布版本零影响。
为 ADR-001 的双源互补铺路:audioCapture 从'直接驱动渲染进程采集'改为 '按策略选源 + 降级 + 统一补时间戳'的编排器,采集实现下沉到 Provider。 - src/lib/capture/audioSourceStrategy.ts:源类型与选源策略纯函数 (主/渲染进程共用——useAudioRecovery 后续需按源分支提示文案),12 例单测 - electron/audio/audioSourceProvider.ts:Provider 接口与音频块契约 - electron/audio/endpointLoopbackProvider.ts:现有端点环回逻辑原样迁入 - audioCapture.ts:编排器,公开 API(start/stop/dispose/handleRendererChunk /isCapturing/config)保持不变,mediaCaptureHandlers 零改动 Phase 0 阶段能力探测恒为'进程环回不可用',选源结果必为端点环回, 行为与重构前完全一致。
Phase 2 前置:同步阻塞采集无法用于真实链路,新增生产采集入口。 - capture_session:WASAPI 激活/初始化/读包原语(pimpl 隐藏 COM),供同步与流式 两路复用;含 isProcessLoopbackSupported 能力探测(真实尝试激活而非查版本号, 避免兼容性设置伪造版本号导致误判) - streaming_capture:采集线程按 chunkDurationMs 聚块回调,Stop 阻塞等待线程退出 以保证无回调泄漏 - addon:新增 startCapture/stopCapture/isProcessLoopbackSupported,块经 ThreadSafeFunction 投递到 JS 线程并拷贝为 ArrayBuffer - loopback_capture 精简为复用 CaptureSession(去重 WASAPI 样板代码) 实测 spike-streaming:5 个 1s 块周期准确、样本数 16000 一致、元数据正确、 stopCapture 后零泄漏。
ADR-001 Phase 2:原生进程环回接入编排器,特性开关默认关闭,行为与 Phase 0 一致;开启方式为环境变量 ENTROPY_PROCESS_LOOPBACK=1。 - processAudioNative:原生模块加载器。addon 视为可选依赖,未编译/非 Windows/ 版本过低均优雅降级为'能力不可用',加载失败不冒泡到启动路径;能力探测与 加载结果缓存到进程生命周期 - processLoopbackProvider:窗口源 ID 解析 HWND → 匹配原生枚举取进程树根 PID; 采集在主进程完成故不实现 handleRendererChunk - audioCapture:接入能力探测与 flag;新增运行时降级——采集开始后才发生的 进程环回故障(如目标进程退出)无缝切回端点环回 - 构建链路:native:install / native:build 脚本(electron-rebuild 针对 Electron ABI 编译)、electron-builder files 纳入 .node 产物(已有 asarUnpack 解包)、CI 增加编译步骤(continue-on-error + 产物检查告警, 编译失败不阻断发布) 验证:client 473 tests、lint 0 errors、渲染与主进程 tsc 均 0; electron-rebuild 针对 Electron ABI 重建通过。 未纳入本阶段:useAudioRecovery 按源分支提示文案(flag 关闭时现有文案正确), 留待 Phase 3 灰度开启时一并处理。
ADR-001 Phase 3:进程环回从灰度转默认启用,并补齐用户可控与可归因链路。 - flag 反转为默认开启,ENTROPY_PROCESS_LOOPBACK=0 保留为应急总闸 (现场排障可不重新发版即关闭) - audioSourcePreference:偏好持久化(localStorage),读写全程静默降级, 绝不因偏好读取失败阻断采集启动;6 例单测覆盖非法值与存储异常 - 设置页新增「课堂音频采集」区块:自动 / 仅目标窗口声音 / 系统全部声音, 各选项标注取舍(干净但可能漏采 vs 不漏采但含杂音) - audio_capture_start 双向打通:入参接收 preference(主进程读不到 localStorage),返回值回传 sourceKind + sourceReason - useAudioRecovery 按源分支:对进程环回不再提示'检查系统默认输出设备' (该源不受输出设备与系统音量影响,属误导),改为提示确认目标窗口在播放; devicechange 自动重启仅端点环回保留 - 生效源经 useClassroomCapture 暴露,供 UI 展示与内测归因 验证:client 479 tests、lint 0 errors、渲染与主进程 tsc 均 0。
Owner
Author
|
🎉 This PR is included in version 0.30.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Aparencia
added a commit
that referenced
this pull request
Aug 4, 2026
- #5: Gemini generate_stream iteration moved into thread pool via asyncio.to_thread, unblocking the event loop during streaming - #6: providers return real input/output token split; fallback cost tracking prefers it and falls back to 60/40 estimate instead of the misleading 50/50 split (output priced 2-3x higher) - #7: balance queries use Key pool primary key (plural env vars), DeepSeek balance no longer silently skipped - #8: JWKS fetch distinguishes network failures (stale cache reuse with 60s retry backoff) from HTTP/key errors (fail-closed), preventing whole-site 401 on transient Supabase hiccups - #9: import_concept registered in TIMEOUT_CONFIG/RATE_LIMITS plus startup validation warning for unregistered features - #10: error_pattern cache key includes user_id and hashes full content, preventing cross-user result reuse
Aparencia
added a commit
that referenced
this pull request
Sep 13, 2026
控制方裁决(未决项①成立,派单缺陷在控制方):前一条把 `containerGroups` 当候选集
⇒ 移组后碎片会被塞进「笔记容器组」,而碎片的自动归组走的是 feed ⇒ 静默的语义错位。
Rust 侧决定性证据(本单元独立复核,逐行读过):
- `commands_groups.rs:36` 逐字「列出笔记组(含组内笔记数;terrain 可选过滤 container/feed)」;
`:43-46` 白名单校验 `if t != "container" && t != "feed" { return Err(...) }` ⇒ feed 是一等地形。
- `commands_fragments.rs:344-366` `resolve_feed_topic_group`:碎片自动归组建的就是 feed 组
(`:355 terrain: "feed".to_string()`、`:349 find_topic_group(domain_tag, "feed")`、
`:361` 理由逐字「碎片 DomainTag 自动归组」)。
- 反向对照:`FeedFragmentList.tsx:109` 的「目标=笔记容器组」是 `promote_fragment_to_note`
(升为笔记)的去向语义 —— 另一件事,不能当本命令的先例(该注释**未改动**)。
返工内容:
1. feed 清单加载搬进 `FragmentGroupAction.tsx` 自己:
`invoke<NoteGroup[]>("list_note_groups", { terrain: "feed" })`(后端真源过滤)
+ 客户端再收一次口径 `list.filter((g) => g.terrain === "feed")`(防口径漂移混入容器组)。
2. 父层去掉 `groups` / `onNeedGroups` 两个 prop ⇒ `FeedFragmentList.tsx` **300 → 299**(净变化 −1)。
3. 头注里「本件不自建第二套加载器…两套加载器就是两套口径」那段改写:该理由只在**同一 terrain**
上成立;这里是**两个不同 terrain 的两个不同查询**(container=笔记去向 / feed=碎片归组),
不构成「两套口径」。改写后逐字写出上面三条 Rust 证据(含行号)。
4. 清单加载失败也走父层 `setErr`(可见行,不静默);`groups` 留 null ⇒ 可再点一次重试。
5. 「当前组」标签:组名查不到时**如实报 id**(`当前组 #N`),不许把「碎片还在容器组里」谎报成
「当前未归组」。
判据(新增 V7–V9):
- `FeedFragmentList.test.tsx` 9 用例(件数不变,断言加强):
V7 正控 = feed 组 11 出现在候选 + 查询载荷逐字 `{ terrain: "feed" }`;
🔴 V7 负控 = fixture 的 `terrain: "container"` 组(id 9)**不得出现**在候选里
(桩在 feed 查询上**故意多回一个容器组**,把客户端第二次收口的牙摆出来);
另有「当前组 #9 / 当前未归组」两条如实性断言。
- `countLines()`:`FeedFragmentList.tsx` **299** · `FragmentGroupAction.tsx` **155** ·
`FeedFragmentList.test.tsx` **299** ⇒ 全部 ≤300。
- 冻结键持平:宿主 nativeButton 6、边框 5、越界圆角 0、阴影 0、弱化灰 1、字号越界 8、三红 1、
`<Surface>` 1;新件六棘轮全 0、原生 button 0。
门禁(串行):line-limits --full exit 0(0 / 121 / 121)· docs-check exit 0(扫描 281 /
检查 181 + ✅ docs-check 通过)· check-command-registry exit 0(定义 311 / 注册 311 / 重复 0)·
tsc --noEmit exit 0 · 守卫族 12 件 122 用例全绿 · 落点两件 29/29 绿。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
课堂助手音频采集原先只有端点环回(Chromium getDisplayMedia),在系统主音量之后截取设备最终混音,两个原理性缺陷:
决策
见 ADR-001(本项目首个 ADR):引入 Windows 10 2004+ 的进程环回(混音前按进程树截取),与端点环回双源互补而非替换——前者干净但可能漏采,后者不漏采但脏;虚拟声卡方案因需内核驱动签名与用户安装摩擦被否决。
实测验收(Phase 1)
实施
下游零改动:AudioChunkData 契约未变,VAD / ASR / 幻觉过滤 / 交叉融合全部复用。
安全设计
进程环回全程是可选增强:原生模块未编译、非 Windows、系统版本过低、CI 编译失败(continue-on-error + 告警)均只降级为端点环回,不影响应用启动与发布流水线。另有 ENTROPY_PROCESS_LOOPBACK=0 应急总闸,现场排障可不发版关闭。
验证
打包产物首次包含原生模块,建议实机验证:目标窗口采集生效(日志 [ProcessLoopback] 开始捕获\)、其他应用提示音不进转写、采集中关闭目标窗口触发运行时降级。
已知空白:混音器内「每应用音量」是否衰减进程环回尚未实测(主音量已确认无影响)。