资讯动态

Friend 桌面端 Push-to-Talk 移植实录:每轮口语语言自动检测与系统音频静音(Mac→Windows Ground-Truth 审计)

发布时间:2026/9/16 14:35:05 来源:尧图企业网站定制
Friend 桌面端 Push-to-Talk 移植实录每轮口语语言自动检测与系统音频静音Mac→Windows Ground-Truth 审计【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend导读FriendOmi桌面应用在将 macOS 上已调优的 Push-to-TalkPTT体验移植到 Windows 时会面临两个“行为契约”层面的硬骨头一是每轮口语语言的自动检测与回灌防止 STT 提供商把俄语误判成意大利语二是PTT 捕获期间对系统音频的静音/闪避防止正在播放的视频声音串入麦克风。本文以仓库中的 Mac-Windows 对等性审计文档track2-groundtruth/05-language-id-system-audio-mute.md为骨架逐条拆解 Mac 侧的精确算法与契约再对照 Windows 侧的移植建议与当前仓库已落地的实现给出可验证的源码级证据。读完你将掌握PTT 语言识别两阶段管线的门控逻辑、系统音频静音的幂等/恢复契约、以及 Windows 端“无本地 ASR 时如何做语言前馈”“无 COM 先例时如何用 C# 原生 helper 桥接 WASAPI”的具体工程取舍。一、背景为什么这份审计文档是移植的“ground truth”桌面版 PTT 是跨平台功能但 Mac 端经过了长时间线上调优Windows 端不能靠“看着像”来对齐。为此仓库维护了一份Mac 对等性审计mac-parity-auditground-truth 文档它把 Mac 端的两个 PTT 子功能——每轮口语语言自动检测TOPIC A与系统音频静音/闪避TOPIC B——的算法、门控、调用点、设置键全部以“精确契约”的形式记录下来作为 Windows 移植的验收基准并给出 Windows 可行路径TOPIC C 汇总设置项。审计文档的元信息本身就标明了证据来源Mac 侧读取于desktop/macos/Desktop/Sources/FloatingControlBar/下的PTTLanguageIdentifier.swift、SystemAudioMuteController.swift、RealtimeHubController.swift、RealtimeHubTools.swift、PushToTalkManager.swift、ShortcutSettings.swift以及ProactiveAssistants/Services/AssistantSettings.swiftWindows 侧读取于desktop/windows/src/renderer/src/lib/下的ptt/constants.ts、ptt/transport.ts、capture/systemAudio.ts、preferences.ts与desktop/windows/src/main/bar/keyState.ts。有意思的是审计文档写作时 Windows 端这些功能尚未落地而当前仓库中Windows 侧已经按文档建议实现了绝大部分voiceLanguages偏好、跨轮语言前馈、WASAPI 音频静音桥接均已存在。因此本文在介绍“文档建议”的同时会对照展示“仓库当前实际实现”这本身就是对这份 ground-truth 有效性的最好验证。二、TOPIC A每轮口语语言自动检测Per-turn spoken-language auto-detection2.1 Mac 侧的精确算法两阶段管线Mac 的核心实现位于 PTTLanguageIdentifier.swift。它是一个actor单例PTTLanguageIdentifier.shared且刻意与常驻的环境转录管线隔离它持有自己的AsrManager永远加载多语言 Parakeet v3模型AsrModels.downloadAndLoad(version: .v3)L44-47而不像环境转录管线那样可能是 v2/纯英文模型。这样做的原因在源码注释里写得很清楚——环境转录管线对en用户是 English-only 的语言检测必须用多语言模型。两阶段流程Stage 1 — 解码manager.transcribe(samples, decoderState:, language: nil)使用 Apple 端侧Parakeet v3 多语言 ASR把 16kHz 单声道 PCM 转成文本L96-109 的decodeText。模型通过loadedManager()惰性加载并缓存L39-62且提供显式的prewarm()入口L35-37由 hub 预热时调用避免第一次 PTT 时吃模型加载延迟。Stage 2 — 分类对解码文本运行 Apple 的NLLanguageRecognizerdominantLanguage(of:hints:)L138-150通过recognizer.languageHints做候选语言偏置——每个候选语言权重为0.3L142-146。一个重要的细节是 TDT 解码器对 OOV词表外音频会输出字面量unk标记decodeText会先剥掉它、再压缩连续空格、最后拒绝没有任何字母的文本L100-104保证进入语言分类的文本是干净的、可读的。2.2 门控规则Gatingidentify(pcm16k:candidates:clipSeconds:)L71-90实现了三层门控门控规则原因音频量下限拒绝 6,400个 int16 样本 0.4s 16kHz两个阶段都没有足够信号文本可用性剥离unk后必须含字母空文本无法分类、也不该上屏候选集过滤detectLanguage(of:candidates:)L128-132只在主语言属于候选集时返回结果否则返回nil“无匹配 → 让提供商自己决定”注意区分两个原语detectLanguage(of:candidates:)是带门控的调用主语言必须在候选集内才返回否则nil但转录文本无论判据如何都照样返回——它会被用作聊天气泡的兜底内容。dominantLanguage(of:hints:)是不带门控的底层原语返回NLLanguageRecognizer认为的主语言候选偏置只管权重、不做集合过滤。它被用于对提供商自己的转录做分类偏置的意义在于让“play Despacito”这种语码混用的短句仍能归类到用户的候选集注释见 L134-137。另外还有语言代码规范化Apple 的 NLLanguage 代码与 Omi 设置代码在少数地方不一致如挪威语nbvsnonormalizedBaseCode/nlCode(for:)双向映射L152-163。2.3 判定结果的三个调用点审计文档记录判定结果在 RealtimeHubController.swift 有三个使用点按住中途的早期提示early mid-hold hint约 1.5s 的 PTT 音频累积后earlyLIDBytes且用户配置了超过 1 个候选语言candidates.count 1时派生一个identify()任务。单语言用户直接跳过 LID——没有歧义可解。结果落到turnEarlyVerdictCode并用turnEpoch做键使被取代回合的过期判定被丢弃。关键点是它在按键尚未松开时就跑完PTT-up 时结果已经就绪。提交时提示提供商hint the provider at commitPTT-up 时若实时提供商支持setInputTranscriptionLanguage且候选语言多于一个则传入turnEarlyVerdictCode单候选语言直接传候选本身无需 LID多候选且早期判定为nil则传“仍然自动检测”。回合结束后校准转录reconcile提供商返回自己的转录后用无门控的dominantLanguage(of: hints: [])对提供商文本分类——此处刻意不对提供商文本施加候选偏置。若提供商转录为空或语言落在用户候选集之外providerMismatches则等待整段缓冲的本地解码fullLIDTask20s 超时若本地语言命中候选集localMatchesPreference就用本地转录语言替换提供商转录。这正是“语码混用处理”/交叉校验提供商自动检测常把短句误标文档记录的真实案例俄语被渲染成意大利语。防御性用途RealtimeHubTools.shouldRejectEscalationQueryForLanguage用无门控的dominantLanguage(hints: allowed)拒绝查询语言与用户语音语言不匹配的工具调用。2.4 候选语言来源AssistantSettingsMac 的候选语言来自 AssistantSettings.swiftL138-177三个字段构成完整契约voiceLanguages: [String]有序、主语言在前的用户配置语音助手语言存储在独立的 UserDefaults 键voiceAssistantLanguages与环境的transcriptionLanguage/transcriptionAutoDetect刻意解耦并有自己的通知voiceLanguagesDidChange因此修改它不会重启环境转录管线。为空时回退到[transcriptionLanguage]保证老用户行为不变。hasExplicitVoiceLanguages: Bool仅当用户显式设置了voiceLanguagesKey才为真。这是总开关系统指令语言钉住、whisper 提示、每轮 LID 全部被它门控——默认配置用户得到的就是今天的提供商自动检测绝无“强制只认英文”的行为。voiceBaseLanguages: [String]LID 真正消费的候选数组——基 ISO-639-1 码去掉区域后缀en-US→en、去重、保序除非hasExplicitVoiceLanguages否则为空。baseLanguageCode(_:)code.split(separator: -).first并小写。2.5 Windows 侧的移植路径审计建议 vs 仓库现状审计文档明确指出Windows没有端侧多语言 ASR也没有 NLLanguageRecognizer。端侧移植 Parakeet 需要 ONNX/DirectML 移植文本语言 ID 库franc/cld3风格的 n-gram 检测器是独立依赖且当时不在package.json中。因此文档给出的务实路径是——“用提供商自己的自动检测作用在返回的转录上把‘本地解码’替换成‘复用已返回的结果’”候选集门控在Preferences增加voiceLanguages: string[]语义与 Mac 完全一致空/未设置 功能惰性单条 直通无需检测仅 ≥2 条才触发任何 LID 逻辑。文本级检测因没有本地解码Mac 的“按住中途早期提示”阶段在 Windows 没有直接对应物若要近似可引入轻量 JS n-gram 库如franc-min对流式通道的 interim/部分转录文本transport.ts的onFinal回调而非 PCM 做检测并用“检测器 top-N 与候选集求交”来近似languageHints加权。校准阶段对 Mac 防的“俄语被渲染成意大利语”这类 bug 最重要转录返回后对文本做同样的检测语言落在候选集外即为 mismatch。(a) 若 STT 提供商自己返回检测语言字段Deepgram/Parakeet 后端常带优先透传而不是自研检测(b) 若没有该字段只能把 mismatch 信号前馈给下一次调用无法像 Mac 那样在同一回合内换出第二份本地转录——文档要求把这一行为差距记录为已知偏差而不是悄悄丢弃。前馈到下一次调用把最近检测/最近偏好的语言作为下一次batchTranscribeParams()的language参数和listenStart的language字段替代当前静态的getPreferences().language。这是即使不做本地解码也值得移植的核心行为按“上一句实际说了什么”来偏置提供商而不是固定设置。不做Mac 的按住中途早期提示优化——Windows 流式通道的onFinalinterim 段本来延迟就低于 Mac 的完整本地解码。仓库现状印证Windows 端当前已按此落地。preferences.ts 已定义voiceLanguages?: string[]注释与文档契约逐字对应“空/未定义 ⇒ 惰性PTT 用静态language非空 ⇒ 跨轮前馈……基础 ISO 639-1 码如[en, ru]”。transport.ts 实现了跨轮语言前馈lastDetectedLanguage模块级记忆、resolveTranscribeLanguage()只在候选集内且命中记忆时才替换静态偏好、rememberDetectedLanguage()只在语言属于候选集时才记录。其注释明确写着“刻意不移植 macOS 的PTTLanguageIdentifierWindows 没有端侧多语言模型回合内没有可对照校准的东西跨轮前馈是可实现的最大子集”。batchTranscribe把响应里的res.data?.language传给rememberDetectedLanguageL178-L201即直接采纳提供商返回的语言字段对应审计建议 3abatchTranscribeParams(resolveTranscribeLanguage(), ...)则把结果前馈进下一次批量调用。审计文档提到的“静态language”在批量通道已被resolveTranscribeLanguage()取代流式listenStart的language字段transport.ts L93目前仍是getPreferences().language这与文档“把语言前馈到流式通道的language字段”的建议之间仍存在一条待对齐的缝隙。对应地constants.ts顶部的注释constants.ts L1-3明确把该文件定义为“PTT 调优常量的单一事实来源值标注 (macOS) 的镜像自 Mac 已验证调优”。三、TOPIC BPTT 捕获期间的系统音频静音/闪避3.1 Mac 的精确契约Mac 实现位于 SystemAudioMuteController.swiftMainActor final class单例SystemAudioMuteController.shared。源码注释点明了设计动机“不是暂停媒体会停掉曲目而是让播放继续但静音这样按键一松开就无缝恢复只在实际有音频播放时才静音、绝不动用户自己已经静音的设备、监听结束时总是恢复。”总开关门控ShortcutSettings.swiftpttMuteSystemAudio: BoolUserDefaults 键shortcut_pttMuteSystemAudio默认trueUserDefaults.standard.object(forKey:) as? Bool ?? true在 设置 → Shortcuts 暴露为开关。PushToTalkManager.swift中每一次静音调用都被if ShortcutSettings.shared.pttMuteSystemAudio { ... }包裹按住式 PTT 开始、锁定式 PTT 开始两处而恢复调用无条件——不做设置检查因为恢复永远安全。muteForListening()的门控L39-61幂等guard mutedDevice nil—— 已持有静音则直接 no-op可安全重复调用guard let device defaultOutputDevice()—— 通过kAudioHardwarePropertyDefaultOutputDevice解析默认输出设备L91-102解析不到则放弃guard isDeviceRunningSomewhere(device)——“只在真有音频播放时才静音”读kAudioDevicePropertyDeviceIsRunningSomewhereL105-114没有任何进程在设备上做 I/O 就不动guard deviceIsMuted(device) ! true——“绝不动用户自己已静音的设备”设备静音属性已为 true 就保持原样之后restore()因为mutedDevice未设置而成为 no-op绝不会把用户自己的静音解除首选路径setMute(device, muted: true)写kAudioDevicePropertyMute先检查AudioObjectIsPropertySettable。成功后记录mutedDevice、usedVolumeFallback false回退路径设备没有可写的静音属性zeroVolume(device)—— 先读主音量元素kAudioObjectPropertyElementMain主元素不可写则按声道 [1,2]立体声逐个归零把原值存入restoreVolumes置usedVolumeFallback true。restore()L76-87未静音时调用也是安全 no-op若usedVolumeFallback逐个(channel, value)回放setVolume否则setMute(device, muted: false)无论底层调用成败最后都清空mutedDevice、restoreVolumes、usedVolumeFallback。恢复调用点PushToTalkManager.swiftperformTerminalCleanup()注释为“teardown取消/出错/清理时总是恢复音频绝不遗留静音”——无条件覆盖所有 PTT 结束路径点按锁定决策判定“听写结束”之后让音乐立即恢复RealtimeHubController.swift:2629回合完成/响应的路径里防御性地第二次恢复——在助手语音回复即将播放时恢复“即使捕获 teardown 的恢复被硬件延迟也要保证模型回复可闻”。即 Mac 在两个时点都恢复PTT 生命周期 teardown 处 回复即将可闻前因为无法保证前者一定先于硬件级恢复延迟落地。静音时机静音发生在PTT-DOWN按住式与锁定式开始捕获的瞬间而不是更晚的“确认有语音”之后恢复发生在 PTT-UP / teardown / cancel外加回复播放前的防御性恢复。就绪度自省defaultOutputReadiness()L64-73仅用于引导 UX“把音量调大”提示不参与静音/恢复生命周期我们持有时返回.audible不因自己造成的静音而让用户去调音量设备静音属性为 true 返回.muted所有声道可读音量 ≤ 0.001 返回.zeroVolume无默认设备或音量属性不可读返回.unavailable。3.2 Windows 现状确认与 PTT 无关审计文档专门确认了 capture/systemAudio.ts 是回路捕获loopback capture——用getDisplayMedia({video: true, audio: true})抓系统音频作为输入流用于会议转录功能拿到音频后立即丢弃视频轨见该文件 L23-24不是输出静音。仓库中没有现有代码静音或闪避默认渲染输出设备src/main也没有任何 COM/WASAPI 绑定——src/main/bar/keyState.ts是唯一的 koffi 先例且只是扁平的user32.dll函数调用GetAsyncKeyState、GetSystemMetrics没有 COM/vtable 接口。3.3 Windows WASAPI 实现方案审计建议审计文档给出的 API 级移植目标是一对 WASAPI 接口 一个枚举器作用在默认渲染终结点上解析默认输出设备MMDeviceEnumeratorCOM 对象CLSIDMMDeviceEnumeratorIIDIMMDeviceEnumeratorGetDefaultAudioEndpoint(eRender0, eConsole0)→IMMDevice。静音对应 Mac 的setMute/deviceIsMutedIMMDevice::Activate(IID_IAudioEndpointVolume)→IAudioEndpointVolume::GetMute(bool)/SetMute(bool, GUID)。Windows 渲染终结点可靠地暴露可写的主静音所以 Mac 需要的主静音不可写时的音量归零回退在 Windows 上大概率用不上但GetMasterVolumeLevelScalar/SetMasterVolumeLevelScalar是等价回退镜像zeroVolume/restoreVolumes。“确有音频播放”门控对应 Mac 的isDeviceRunningSomewhere同一IMMDevice上Activate(IID_IAudioMeterInformation)→IAudioMeterInformation::GetPeakValue峰值 ~0 即表示有输出在静音时点采样一次不持续轮询。更贴近“是否有活跃渲染会话”语义的替代是IAudioSessionManager2/IAudioSessionEnumerator检查GetState() AudioSessionStateActive但仪表峰值检查更简单、更贴近 Mac 的设备级单点检查。“用户已自己静音”门控设置前IAudioEndpointVolume::GetMute与 Mac 的deviceIsMuted检查相同。恢复SetMute(FALSE, GUID)—— 与 Macrestore()相同的无条件安全、幂等、teardown 必调契约。koffi 可行性评估koffi 3.x 可以通过把接口指针当void**、读 vtable同为void**、按索引取方法函数指针来调用 COM但要求手工布局IUnknown/IAudioEndpointVolume/IMMDevice/IMMDeviceEnumerator的 vtable 偏移必须对照 Windows SDK 头文件逐一核验一处转录错误会静默破坏调用、在 Node 里正确处理CoInitializeEx/CoUninitialize生命周期以及正确的 GUID/结构体封送SetMute的 event-context GUID 参数。这比现有keyState.ts先例扁平导出 DLL 函数、无接口/vtable 间接、无 COM 公寓生命周期高出一个复杂度量级。审计建议复用项目现有的C# 原生 helper 构建模式OCR/自动化 helper 经.NET SDK由scripts/build-ocr-helper.ps1、scripts/build-automation-helper.ps1构建。C# 的NAudio或 CLR 内建 COM interop 让IAudioEndpointVolume/IAudioMeterInformation变成几行惯用代码而不用手写 vtable 偏移。第三个 helperbuild-audio-mute-helper.ps1风格按与 OCR/自动化 helper 相同的方式调用由于静音必须发生在 PTT-down 且无可感知延迟长驻 helper 进程避免每次调用的 .NET 启动开销优于每次静音冷启动。3.4 仓库现状C# helper 方案已落地Windows 侧当前已经按审计建议实现了整套桥接主进程桥main/audio/systemAudioMute.ts 中的SystemAudioMuteBridge——长驻子进程、长度前缀帧协议、崩溃退避重连仿照自动化桥的形状。它warm()在应用启动时预热L246-248保证 PTT-down 不吃冷启动开销muteSystemAudio()/restoreSystemAudio()永不向调用方抛错helper 缺失/死亡被吞掉绝不阻塞 PTT。原生 helpermain/audio/helper/Program.cs 是一个 C# 控制台程序NAudio.CoreAudioApi实现三个 opcode1 MUTE、2 RESTORE、3 HELLO协议版本3L45。它的静音契约与 Mac 完全对齐注释逐条对应Mute()L121-177先检查device.AudioEndpointVolume.Mute用户自己已静音 → 返回user_muted跳过再用PeakOverWindow()在 ~150ms 窗口内采样AudioMeterInformation.MasterPeakValue最多 10 次、每次 15ms一旦超过阈值立即判定——L51-L60无播放则返回not_playing。注释解释了为什么不能单次采样MasterPeakValue只报告上一个设备周期的峰值恰好在渲染客户端填充缓冲的间隙采样会读到 0线上真实教训。Restore()L179-211支持{deviceId:...}迷途静音提示stranded-mute hint如果上一个 helper 进程在被杀前已静音而没恢复OS 静音是持久状态进程消失它仍在新 spawn 的 helper 可以只解开自己人设过的静音。三层“绝不遗留静音”防护请求循环的finallystdin EOF 父进程退出/崩溃先恢复再退、ProcessExit钩子、以及被TerminateProcess强杀时由主进程桥按记录的 deviceId 重放 RESTOREL27-L36。IPC 通道main/ipc/audioMute.ts 注册audio:muteSystemAudio/audio:restoreSystemAudio两个 fire-and-forget 的send通道刻意不用invoke避免慢 helper 拖住 PTT。渲染端策略层renderer/src/lib/ptt/systemAudioMute.ts 是纯策略接缝——muteEnabled()用getPreferences().pttMuteSystemAudio ! false未设置 ⇒ 开与 Mac 默认 true 对齐systemAudioActionFor(effect)把 PTT 状态机的startCapture映射为 mute、把startDrain松开和stopCapture取消/看门狗/卸载映射为 restoreapplyPttSystemAudio保证RESTORE 无条件、绝不因偏好设置而跳过。构建脚本desktop/windows/scripts/build-audio-helper.ps1 负责构建 helper exe桥接代码在 helper 二进制缺失时静默降级并打recordFallback事件config_incompletePTT 依旧可用只是不静音。值得一提的刻意偏差Windows 在 PTT-END 的确定性效果点恢复release → startDraincancel/watchdog/unmount → stopCapture这些点比 STT → LLM → TTS 往返早几秒不存在“自静音窗口”因此不需要Mac 的“回复播放前防御性第二次恢复”——渲染端策略文件注释明确要求不要补这个 hook见 ptt/systemAudioMute.ts L19-24。四、TOPIC C需要带过去的设置键与默认值审计文档用一张表总结了必须迁移的两个设置键Mac 键存储默认值Windows 等价项ShortcutSettings.pttMuteSystemAudioUserDefaultsshortcut_pttMuteSystemAudiotruePreferences.pttMuteSystemAudio?: boolean默认true调用点按?? true读取沿用该文件已有的vadGateEnabled?式可选字段回退模式AssistantSettings.voiceLanguagesUserDefaultsvoiceAssistantLanguages[]空 → 回退[transcriptionLanguage]Preferences.voiceLanguages?: string[]默认未设置/空Windows 目前没有独立的环境转录语言设置可回退——回退到现有单值language: string字段方式同 Mac 回退transcriptionLanguageAssistantSettings.hasExplicitVoiceLanguages派生非空检查—同法派生(preferences.voiceLanguages?.length ?? 0) 0—— 总开关为 false 时不跑任何 LID/检测逻辑AssistantSettings.voiceBaseLanguages派生—同法派生对voiceLanguages去重 去区域后缀code.split(-)[0].toLowerCase()仓库现状印证两个新字段都已进入 preferences.tsvoiceLanguages?: string[]L42注释完整复述了“空 ⇒ 惰性非空 ⇒ 跨轮前馈”的契约pttMuteSystemAudio?: booleanL108注释写明“macOS SystemAudioMuteController 对等——只有 MUTE 调用被此开关门控RESTORE 无条件所以按住中途关掉开关也绝不会让机器滞留静音”默认未定义 ⇒ 开读取模式遵循vadGateEnabled?先例L98即可选字段 ?? 默认值回退。文档结论是“这两个功能在 Mac 上没有其他承载性设置键”这保证了移植面是收敛的。五、总结值得带走的四个行为契约综合 TOPIC A/B/C这份 ground-truth 审计给出的是四条跨平台通用的行为契约与具体解码器、具体静音 API 无关候选集门控三态空 惰性、单候选 直通、多候选 才做检测——用一条voiceLanguages偏好即可完整移植即使没有本地解码器。前馈而非重建Windows 没有本地 ASR/NL 框架务实路径是优先透传 STT 后端自带的语言字段并把检测/偏好的语言前馈进下一次回合的language参数批量通道与流式通道而不是自研检测器。同回合双转录校准是已知差距Mac 能拿 Parakeet 本地转录换掉被误标的提供商转录Windows 单转录源下只能把 mismatch 前馈给下一次调用——这是要在文档里明示的偏差而不是悄悄丢弃的行为。静音契约的幂等与恢复安全只在确有播放时静音、绝不动用户自己静音的设备、恢复无条件且幂等、teardown 必恢复、外加“绝不遗留静音”的多层兜底stdin EOF 恢复、ProcessExit 钩子、强杀后按 deviceId 重放 RESTORE。Windows 用长驻 C# helperNAudio/WASAPI实现它比手写 koffi/vtable COM 调用低一个复杂度量级且静音不引入 PTT-down 延迟。如果你想继续深入Mac 侧算法读 PTTLanguageIdentifier.swift 与 SystemAudioMuteController.swiftWindows 侧桥接读 main/audio/systemAudioMute.ts 与 main/audio/helper/Program.cs偏好契约读 preferences.ts语言前馈读 ptt/transport.ts。【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价