Phase 4C:接入无执行权的声纹 Shadow Runtime - #2408
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (3)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Walkthrough新增 CAM++ 说话人模型资产管理、ONNX 嵌入推理和异步 speaker shadow 运行时,并将其接入 ASR 检测、独立打包、离线验证、评估脚本及测试体系,覆盖 Linux、Unix 与 Windows 构建流程喵。 ChangesCAM++ 说话人资产与构建交付
模型推理与运行时集成
验证与评估
Estimated code review effort: 5 (Critical) | ~120 minutes Suggested labels: Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
afa0884 to
968566c
Compare
5d382a2 to
abb7b18
Compare
968566c to
83f0e4e
Compare
|
已按收敛方案完成本轮修复,并把 #2408 精确 rebase 到 #2398 本轮三个提交:
验证结果:
边界未变化:默认关闭、无生产 backend、无 enrollment、无 UI/telemetry、无硬拦截;不修改 Core 生命周期、Provider/Registry、RNNoise/Silero 参数、SmartTurn 模型或任何 fallback。PR 继续保持 Draft,等本次 force-push 后的 CI/review 对精确堆叠组合重新确认。 |
abb7b18 to
4240984
Compare
83f0e4e to
d381f4d
Compare
|
本次已将 Phase 4C 重叠到收敛后的 #2398: |
|
已沿堆叠链检查 #2396 的两个 P2 修复( #2408 仅涉及 Speaker Shadow runtime/integration/tests,没有修改 堆叠顺序仍为 #2396 → #2398 → #2408:待 #2398 吸收新的 4A 基线后,#2408 再跟随新的 4B head 做精确 rebase 和组合回归即可;不向 Shadow runtime、Core、Provider 或 Electron 原生层扩散本次修复。 |
|
CAM++ real-model Shadow implementation has been pushed at 集中 review 请重点检查以下边界:
验证证据:
PR 继续保持 Draft。当前远端 CI 未运行的原因是仓库 Actions 只监听 |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
.github/workflows/build-desktop.yml (1)
640-646: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winmacOS 这里别再写死
dist/Xiao8喵
vad_models和speaker_models的离线校验都改成$NEKO_NUITKA_RUNTIME_DIR/data/...喵。macOS 分支里RUNTIME_DIR实际是dist/Xiao8/projectneko_server.app/Contents/MacOS,继续写死dist/Xiao8/data/...会在 app bundle 下找错目录,离线检查会误报缺失喵。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/build-desktop.yml around lines 640 - 646, Update the macOS asset paths in the voice evaluation commands invoking prepare_voice_turn_assets.py and prepare_speaker_model.py to use $NEKO_NUITKA_RUNTIME_DIR/data/vad_models and $NEKO_NUITKA_RUNTIME_DIR/data/speaker_models instead of the hardcoded dist/Xiao8 prefix, while preserving the existing offline validation and missing-status handling.
🧹 Nitpick comments (3)
main_logic/asr_client/campplus.py (2)
706-724: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win静默失败缺少日志,调试会摸黑喵。
CampPlusSpeakerShadowFactory.__call__()捕获资产校验/模型契约异常后直接返回None,没有任何日志记录(本文件也没有引入logging)。作为默认关闭、失败即优雅降级的观察型功能,这个方向是对的,但生产环境如果因为路径错误/资产损坏而始终拿不到 shadow runtime,排查起来会很麻烦——除非调用方(如 detector_runtime.py)自己记录了原因喵。建议在
except分支加一条 debug/warning 级别日志,说明具体失败原因(不影响返回值,也不影响"零副作用"的设计)。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@main_logic/asr_client/campplus.py` around lines 706 - 724, Update CampPlusSpeakerShadowFactory.__call__() to log the caught CampPlusAssetError, RuntimeError, or ValueError with the exception details before returning None; add or reuse the module’s logging facility without changing the existing graceful-degradation behavior or return value.
315-376: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win每次重新
load()都会对 28MB 模型重新算一遍完整 SHA-256 喵。
load()里validate_assets()每次都会触发resolve_verified_campplus_asset做一次全量 SHA-256(约 28MB)。结合 idle-unload 设计(模型闲置后会unload(),下次需要时再load()),只要这个 shadow 组件间歇性工作,就会反复对同一份没变过的文件做重复哈希计算——虽然频率不高,但对一个"观察即可、不该抢资源"的后台组件来说,这点 CPU/IO 抖动本可以省掉喵。可以考虑在同一进程内缓存"已验证过且文件 mtime/size 未变"的结果,跳过后续重复的全量哈希(仍保留首次加载时的完整校验)。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@main_logic/asr_client/campplus.py` around lines 315 - 376, Update CampPlus asset validation around validate_assets and load so the same process caches a successfully verified asset keyed by its current file mtime and size, allowing subsequent loads after unload to skip the full SHA-256 when those metadata values are unchanged. Preserve the initial complete verification and invalidate or revalidate the cache whenever the asset file changes.specs/launcher.spec (1)
68-70: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value文件名判断硬编码,未来改名容易漏更新喵。
speaker_model_assets_present直接硬编码campplus-zh-en-advanced.onnx,而没有像其他资产一样只判断目录存在,也没有从data/speaker_models/manifest.json读取filename字段。目前和CAMPPLUS_FILENAME完全一致,暂时没问题,但如果模型换代改名,这里可能被遗忘同步喵(本猫娘只是提醒一下,不是非改不可啦)。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@specs/launcher.spec` around lines 68 - 70, 更新 specs/launcher.spec 中 speaker_model_assets_present 的判断,避免直接硬编码 campplus-zh-en-advanced.onnx;改为复用 data/speaker_models/manifest.json 的 filename 字段或现有模型文件名配置,并保持对 speaker_models 资产存在性的判断行为。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In @.github/workflows/build-desktop.yml:
- Around line 640-646: Update the macOS asset paths in the voice evaluation
commands invoking prepare_voice_turn_assets.py and prepare_speaker_model.py to
use $NEKO_NUITKA_RUNTIME_DIR/data/vad_models and
$NEKO_NUITKA_RUNTIME_DIR/data/speaker_models instead of the hardcoded dist/Xiao8
prefix, while preserving the existing offline validation and missing-status
handling.
---
Nitpick comments:
In `@main_logic/asr_client/campplus.py`:
- Around line 706-724: Update CampPlusSpeakerShadowFactory.__call__() to log the
caught CampPlusAssetError, RuntimeError, or ValueError with the exception
details before returning None; add or reuse the module’s logging facility
without changing the existing graceful-degradation behavior or return value.
- Around line 315-376: Update CampPlus asset validation around validate_assets
and load so the same process caches a successfully verified asset keyed by its
current file mtime and size, allowing subsequent loads after unload to skip the
full SHA-256 when those metadata values are unchanged. Preserve the initial
complete verification and invalidate or revalidate the cache whenever the asset
file changes.
In `@specs/launcher.spec`:
- Around line 68-70: 更新 specs/launcher.spec 中 speaker_model_assets_present
的判断,避免直接硬编码 campplus-zh-en-advanced.onnx;改为复用 data/speaker_models/manifest.json
的 filename 字段或现有模型文件名配置,并保持对 speaker_models 资产存在性的判断行为。
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 8c90c71f-9b7e-47fa-8d88-f367c6b39e31
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (19)
.github/workflows/build-desktop-linux.yml.github/workflows/build-desktop.yml.gitignoredata/speaker_models/THIRD_PARTY_NOTICES.mddata/speaker_models/manifest.jsonmain_logic/asr_client/campplus.pymain_logic/asr_client/detector_runtime.pymain_logic/asr_client/runtime.pymain_logic/asr_client/speaker_shadow.pypyproject.tomlspecs/launcher.spectests/unit/test_asr_detector_runtime.pytests/unit/test_asr_speaker_shadow.pytests/unit/test_campplus_real_model.pytests/unit/test_campplus_speaker_model.pytests/unit/test_core_independent_asr.pytests/unit/voice_turn/test_build_contracts.pytools/voice_eval/evaluate_campplus_shadow.pytools/voice_eval/prepare_speaker_model.py
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
Project-N-E-K-O/N.E.K.O.-PC(manual)
|
补充验收记录(head
边界保持不变:默认关闭、无 profile 零加载、仅 Shadow observation;不接硬拦截、Provider/Omni、UI 或持久化,不改变 Provider PCM 字节、顺序或生命周期。 远端 CI 尚未运行并非测试失败:仓库 PR Actions 只监听 |
|
已按备忘录独立核对并处理本轮 review,修复提交:
验证:
边界未变化:默认关闭、无 profile 零加载、仅 Shadow observation;不接硬拦截、Provider/Omni、UI 或持久化。 |
a264377 to
fec62d3
Compare
Stacked rebase / Soniox 边界验证本 PR 已从旧 #2398 head 复核结果:
验证:完整堆叠回归 515 passed / 3 skipped, |
4f48780 to
b9e4098
Compare
fec62d3 to
20dafe9
Compare
真实麦克风人工实测补充(2026-07-22)在 Windows 上通过临时、仅内存的开发测试入口完成了一组 CAM++ 实录验证:
同一份 owner profile、同一设备和同一环境下的独立候选结果:
本次同 profile 分数间隔为 0.5092。另一次已释放的 owner profile 下,非 owner 的 4 次独立候选结果为 当前结论这组结果证明:真实 CAM++ 链路能够对内存 enrollment 建立 192 维 reference profile,并在相同麦克风、相同环境、同一 profile 下对 owner 与非 owner 产生明显分离的真实 cosine similarity;五个候选阈值的 这不是准确率统计,也不用于确定生产阈值。样本规模仍不足以覆盖跨设备、远场、噪声、多人混说、录音回放、声音克隆或 native Omni 路径,因此本 PR 的结论仍严格限定为:真实 CAM++ 可用于无执行权的 speaker Shadow 观察,不启用硬拦截。 |
21e1212 to
01345cf
Compare
|
本 PR 已 rebase 到包含 TTL 与 Detector binding 修复的 #2398 最新 head Speaker Shadow 没有复制或拥有 transport TTL、binding cleanup、Provider selection、endpoint、fallback、retry 或 recovery 逻辑;这些行为全部继承 Phase 4B 基线。边界扫描确认 Shadow 实现对相关策略字段的引用为 0,相对新 #2398 base 的重复修复 diff 为 0;rebase 前后 9 个 Shadow commit 均为 patch-equivalent。 组合回归覆盖 5 秒 PREWARMING、Provider policy WARM_IDLE、100 轮 binding 容量、Speaker Shadow、CAM++、DetectorRuntime、Core 和 build contracts:470 项通过,3 项因真实模型资产未准备而条件性跳过; Shadow 结果仍不能影响 transport 保温、candidate completion 释放、Soniox retry/blocked 状态或 Provider endpointing。 |
P0 / P1 收敛记录(
|
|
本 PR 已 rebase 到包含 DRAINING successor overflow / Provider final fence 修复的 #2398 最新 head Speaker Shadow 没有复制或拥有 discard watermark、successor discard、final acceptance、transport TTL、binding cleanup、Provider selection、endpoint 或 recovery 逻辑;相关行为全部继承 Phase 4B 基线。唯一 rebase 冲突位于 相对新 #2398 base 的 diff 仍为 20 个 Speaker Shadow/CAM++ 预期文件;没有新增或覆盖 |
758e4a9 to
a4899c2
Compare
3a1e124 to
b35c325
Compare
Restack 完成回执本 PR 已完成 restack:
9 个 speaker-shadow 提交已按原顺序重放到新版 #2398。冲突收敛遵循当前架构:
验证结果:
上游 |
真实综合链路复核结论(CAM++ / Provider / SmartTurn)本轮在未改变 #2408 Draft 状态、未启用任何拦截权的前提下,对真实麦克风结果和固定输入 A/B 结果进行了复核。 真实麦克风结果
这证明当前内存 owner profile 与候选音频能够完成真实 CAM++ embedding 和 cosine 评分; Provider PCM 隔离真实综合链路中,baseline 与 shadow 均为
两侧 callback chunk 数分别为 234 / 226,但这只是相同连续 PCM 字节流的异步调用分片不同。后续使用完全相同、只预处理一次的 immutable SmartTurn 复核原始单次结果中的 baseline 真实录音 PCM 已按隐私边界释放,因此不能对该原始样本逐字节重放;上述结论来自固定预处理输入的可重复隔离实验。 新发现:指标计数缺陷成功评分的候选当前可能同时被计入:
原因是评分后候选 buffer 已释放,后续 finish marker 再处理时被误判为“音频不足”。该问题只影响 telemetry 计数,不影响 similarity、Provider PCM、SmartTurn、ASR 或候选评分,但应在最终验收前修复,并补充“成功评分不得同时计入 insufficient”的回归测试。 此外,真实运行中的 当前边界结论现有证据支持:真实 CAM++ 已可作为 default-off、无执行权的 Speaker Shadow 观察链路运行,并且固定输入复测未发现它改变 Provider PCM、SmartTurn seal 或生命周期。 仍不能据此宣称面向普通用户的声纹安全功能或生产硬拦截已经完成:本 PR 没有产品 enrollment 身份体系、阈值校准、误接收/误拒绝统计、回放/克隆防御、native Omni 覆盖或任何音频执行权。#2408 继续保持 Draft;下一步先修复指标计数与测试判定,再按既定门禁完成前置合并后的 Windows/Linux 发布验收。 |
#2408 工程收敛回执(factory / 指标 / 固定输入隔离)当前远端状态:
本轮严格只补齐 #2408 原有范围内的三个缺口,并保持为三个可独立回滚的小提交:
1. 正式 factory bridge正式组合关系现在为: Core factory 默认值为 2. insufficient 指标修复旧状态中,候选达到评分门槛后 buffer 已移交评分线程,finish marker 随后因找不到 buffer 又把该候选记为 insufficient。10,000 candidate 压力用例会错误得到 现在 finalized tombstone 明确保存
finish marker 保持幂等;reset/close 不制造额外 insufficient;10,000 candidate 压力测试仍保持有界。 3. 固定输入 ABBA 隔离合同测试只把原始 48 kHz PCM 通过 四轮均满足:
callback chunk 数不再作为隔离失败条件:连续 PCM 相同并不要求异步 callback 分片数量相同。 验证结果
真实模型资源复核:
GitNexus 相对 #2398 compare 为 MEDIUM,21 个预期文件;影响仅为 Independent ASR start/close/lifecycle/status 流。最终 diff 未触及 Provider worker/registry、transport、 尚未宣称完成的门槛Windows 本机不能替代 Linux 完整安装产物验证。远端 Windows/Linux CI 仍需确认正式产物包含 ONNX、manifest、notice,并能断网加载;#2398 合并后还要把纯 #2408 提交重叠到最终 因此当前结论仍然是:真实 CAM++ Speaker Shadow 的工程基础已收干净;它默认关闭、无 profile 零工作、只有 observation,没有生产阈值、产品 enrollment、攻击防御、Omni 覆盖或音频拦截权。 |
|
阶段衔接说明(2026-07-23) #2408 原定工程范围已经收敛:
后续“生产基础链路”没有继续塞进 #2408,已按文件归属和审查边界拆到 stacked Draft #2450: #2450 只建立: 内存 profile → Core 正式内部入口 → Independent ASR candidate → CAM++ observation → revision/stale/close 清理。 它不反向扩大 #2408 的声明。以下内容仍明确不属于 #2408:生产阈值、声纹硬拦截、产品 enrollment UI/API、持久化、native Omni、回放/AI 克隆防御、多说话人处理,以及 Provider PCM 的修改、延迟、缓存或重放。 因此 #2408 的最终边界仍然是:证明真实 CAM++ 能在 N.E.K.O 的 Independent ASR 候选旁路中安全观察并产生 similarity,但不决定音频是否继续传递。 |
33e6dfb to
bd87b6e
Compare
bd87b6e to
69d8d5e
Compare
|
已完成本轮 rebase 并解决冲突:
下一步继续处理 #2450。 |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 69d8d5e41f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| item_id = event.get("item_id") | ||
| if isinstance(item_id, str): | ||
| item_keys.pop(item_id, None) | ||
| await _emit_error( |
There was a problem hiding this comment.
Ignore failed OpenAI events for unknown items
When OpenAI sends conversation.item.input_audio_transcription.failed for an item_id that was already cleared or belongs to an older route epoch, this path still emits ASR_OPENAI_TRANSCRIPTION_FAILED using the current last_generation and tears down the worker. The completed/delta handler already ignores unknown item ids for exactly this stale-event case, so the failed handler should only fail the route when item_keys.pop(...) actually finds a live item.
Useful? React with 👍 / 👎.
| if SpeechActivityEvent.CANDIDATE_PAUSE in events: | ||
| self._finish_speaker_shadow_candidate(expected=candidate) |
There was a problem hiding this comment.
Finish provider shadow candidates on provider endpoints
Provider-endpointed routes seal a turn from the provider callback via seal_provider_candidate(), which can happen before local Silero emits CANDIDATE_PAUSE; in that case this is the only finish condition, so _speaker_shadow_state stays CONFIRMED and the next speech frames reuse the previous provider_pause key. That merges separate provider turns in the shadow scorer and corrupts the would-block observations, so the shadow candidate needs to be finished when the provider endpoint seals the turn as well.
Useful? React with 👍 / 👎.
结果
would_block。would_block只用于观察和指标,绝不修改、延迟、丢弃或重排 Provider 音频。IndependentAsrRuntime,不再需要测试直接修改嵌套私有字段。固定模型身份
iic/speech_campplus_sv_zh_en_16k-common_advancedv1.0.0k2-fsa/sherpa-onnx28,281,164 bytesaa3cfc16963a10586a9393f5035d6d6b57e98d358b347f80c2a30bf4f00ceba2float32[N,T,80]→float32[N,192]权重不进入 Git。Windows/Linux 正式构建从 manifest 固定 URL 获取,先校验大小与 SHA,再打入安装产物;运行时不联网,也不接受用户缓存目录中的同名权重。
实现边界
CampPlusEmbeddingModel:独立单线程 ONNX CPU session;不修改共享OnnxModelRuntime。CampPlusSpeakerProfile:只保存 defensive-copy 的 192 维 L2 reference embedding、profile revision 和模型身份;关闭时尽力覆盖数组。CampPlusSpeakerShadowBackend:只做 PCM → embedding → dot product。None的_speaker_shadow_factory;IndependentAsrRuntime只接收并转交 factory;factory/模型失败均 fail-open 到无 Shadow。sufficient / insufficient / dropped终态;成功评分或推理失败不再误计为 insufficient。本轮工程收敛
本轮新增三个可独立回滚的提交:
fix(asr): bridge speaker shadow factory through core runtimefix(voice): keep scored shadow candidates out of insufficient metricstest(asr): replay immutable frames for shadow isolation固定输入隔离测试只预处理一次原始 48 kHz PCM,得到同一个不可变
ProcessedVoiceFrame,再按baseline → shadow → shadow → baseline四轮重放。四轮连续 Provider PCM、拼接 SHA256、字节顺序、SmartTurn seal、lifecycle history、Provider final、Detector 终态以及 pending queue/buffer 清理状态完全一致;两次 Shadow similarity 均为0.75。callback chunk 数不作为隔离合同,因为相同连续 PCM 可能被异步调度切成不同 callback 分片。回归报告 / Regression Report
None;无 profile/无 factory 时零模型工作;factory 异常不影响 Independent ASR;Shadow 高低分均不改变 Provider PCM、endpoint authority 或生命周期。3 passed;CAM++ / Shadow / Detector / Core / VoiceTurn / build-contract 聚焦矩阵284 passed, 2 skipped(2 项为与本变更无关的可选 VoiceTurn 真实资产);全部 ASR 回归455 passed;Core 回归176 passed。Ruff、py_compile、async-blocking、Windows/Linux workflow YAML 与git diff --check均通过。10.46 ms、P9511.30 ms;ONNX session RSS 增量50,044,928 bytes,均低于50 ms / 60 MB停止线。真实模型输出 192 维、finite、L2≈1;frontend parity 门槛继续通过。明确不做
AsrAudioDispatcher、RNNoise、Silero、SmartTurn 算法或生命周期状态机。Ready 状态
在前置 PR #2398 真正合入最终目标分支前,本 PR始终保持 Draft。前置合并后,才把纯 #2408 提交重叠到最新
main,执行最终 Windows/Linux 发布构建、完整 CI、GitNexus 和人工 review;所有硬门槛通过后才转 Ready。当前准确表述:N.E.K.O 已能用真实 CAM++ 在 default-off、无执行权的 Shadow 中观察 Independent ASR 候选说话人;该观察不能决定音频是否继续。