ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Friend 桌面端「AI 回复语音朗读 + 抢话打断」实现剖析:Mac/Windows 双端 TTS 架构与 /v1/tts/synthesize 后端契约

Friend 桌面端「AI 回复语音朗读 + 抢话打断」实现剖析:Mac/Windows 双端 TTS 架构与 /v1/tts/synthesize 后端契约 Friend 桌面端「AI 回复语音朗读 抢话打断」实现剖析Mac/Windows 双端 TTS 架构与 /v1/tts/synthesize 后端契约【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend导读Friend 桌面客户端的浮窗Floating Control Bar会把 AI 回复用语音朗读出来并在用户开始新的 Push-to-TalkPTT输入时立刻打断正在播放的语音。这篇指南以仓库中desktop/windows/docs/mac-parity-audit/track2-groundtruth/01-tts-readaloud.md的 ground truth 记录为骨架完整还原 Mac 端从「流式分块 → 填充语 → OpenAI TTS → 系统语音降级」的完整播放流水线、interruptCurrentResponse()抢话机制、Windows 端speakText/playSystemVoice的对应契约以及两端共用的后端POST /v1/tts/synthesize接口规格。读完你可以掌握 Friend 双端 TTS 的每一处参数阈值、降级触发条件与平台差异并据此在 Windows 端做 Mac 对齐改造。一、背景从 Parity Audit 到 Ground Truth 文档该文档隶属于desktop/windows/docs/mac-parity-audit/track2-groundtruth/目录是「Mac 功能对齐审计」第二阶段Track 2的 ground truth 基线。它逐条核对三个 worktree 中的真实代码Mac tagv0.12.72、Windows worktreetrack2-voice-bar、后端track2-voice-bar/backend目的是为「Windows 是否缺少语音朗读/抢话能力」给出基于源码的事实而不是基于审计简报的猜测。文档开篇就纠正了一个重要前提审计简报声称Windows 没有路径朗读浮窗 Ask-AI 回复是过时/错误的——在当前 worktree 中一条完整的「PTT 按下 → 语音回复」闭环已经端到端接通详见本文第五章。真正缺失的只有两块浮窗里「打字提问」的回复默认不朗读且没有设置开关。二、Mac 端流式 TTS分块、填充语与降级Mac 端 TTS 的全部逻辑集中在 FloatingBarVoicePlaybackService.swift共 1433 行。它的核心设计是把一条长回复切成小块逐个合成播放第一块故意做小以快速出声后续块做大以减少生成音频片段之间的停顿感。2.1 分块长度阈值nextChunkBoundary两套长度常量定义在文件第 54-63 行常量最小值偏好值紧急值用途firstChunk*40120200首个分块刻意短小保证首块尽快合成并开始播放followupChunk*320520800后续分块更大减少音频片段数量、降低句子间可感知停顿nextChunkBoundary(in:isFinal:isFirstChunk:)只在!isFinal时被调用isFinal时直接返回text.endIndex一次性冲刷全部剩余文本。非终态下的完整决策链8 步为text.count minLength→ 尚无边界继续缓冲取preferredLength之前的切片若含./!/?/\n在该切片最后一个此类标点后切断否则若text.count preferredLength→ 继续等待更多文本否则取emergencyLength之前的切片在最后一个.!?\n后切断否则若text.count emergencyLength→ 继续等待否则在紧急切片的最后一个,;:\n后切断否则在紧急切片的最后一个空白处切断否则在emergencyLength处硬切。源码注释第 53-60 行点明了设计动机每个分块边界处都带有首尾静音因此分块越少长答案句子/段落之间的可感知停顿就越小——这正是首块 40-200 字符、后续块 320-800 字符的由来。2.2 流式入口updateStreamingResponseIfEnabled(_:isFinal:)该函数第 196 行起在浮窗发送流程的每次 SSE delta 到来时被调用行为如下当message.id变化时先resetPlaybackPipeline(clearMode: false, notifyPTTDrain: true)重置整条流水线再记录新的currentResponseID计算text cleanedPlaybackText(from: message)——对普通消息做空白折叠对discoveryCard/agentSpawn/agentCompletion使用 block 派生文本toolCall/thinkingblock 一律排除随后若Self.shouldSpeak(text)为 false 则直接退出该过滤器屏蔽通用报错文案如 Failed to get a response...以及带⚠️/warning:的文本见第 207-208 行当该回复的第一条非空文本到来时!hasStartedRealPlayback text.count 0第 238-246 行将hasStartedRealPlayback置 true、调用tracer?.begin(tts_start)同时fillerTask?.cancel()、audioPlayer?.stop()、speechSynthesizer.stopSpeaking(at: .immediate)——真实内容在这一刻抢占并终止填充语只把新增后缀text.dropFirst(streamedText.count)追加进bufferedText然后调用drainBufferedText(isFinal:mode:)循环调用nextChunkBoundary产出就绪分块经enqueueChunk送入播放队列。2.3 填充语Filler Phrase首块合成期间的垫话playFillerIfEnabled()第 139 行由FloatingControlBarWindow在查询发出的瞬间调用——此时 LLM 尚未产出任何文本。它从fillerPhrases第 80-89 行Let me check.、One moment.、Looking into it.、Let me see.、Checking now.、Hold on.、One sec.、Working on it.随机选一句并按播放模式处理.systemVoice模式直接用AVSpeechSynthesizer立刻朗读无网络往返.openAI模式走 OpenAI TTS 合成isFillerSynthesizing true解析完成后若hasStartedRealPlayback仍为 false真实内容尚未抢占则通过startPlayback播放该填充语。关键约束是填充语只可能填补首块到达之前的静默。因为第一个真实分块到达时会取消fillerTask并停止正在播放的填充音频speechSynthesizer.stopSpeaking、audioPlayer?.stop()二者永不重叠。2.4 OpenAI TTS → 系统语音降级Mac 端在三个位置触发降级统一走enqueueSystemSpeech(text)并经由recordSelectedVoiceFallback(to:reason:outcome:)→DesktopDiagnosticsManager.shared.recordFallback(...)上报遥测触发点位置reasonoutcome分块合成失败startSynthesisIfNeeded第 295-367 行startSynthesisIfNeededttsFallbackReason(for:)HTTP 429 →quotaCredentialHealthError.failureClass→auth/provider_429/provider_5xx.degradedAVAudioPlayer构造/启动失败startPlayback第 512-546 行startPlaybackenqueue_failed有兜底文本 →.degraded兜底文本也为空 →.exhausted第 538-544 行同时受forceTTSPlaybackFail/forceTTSPlaybackStartFalse两个 UserDefaults 测试钩子门控播放器非正常结束audioPlayerDidFinishPlaying第 586-603 行delegate 回调同降级路径.degradedenqueueSystemSpeech第 562 行使用AVSpeechUtterance参数为rate 0.47、pitchMultiplier 1.02、volume 1.0并按[Ava,Allison,Samantha,Karen,Moira]顺序搜索首选语音找不到则回退到AVSpeechSynthesisVoice(language: en-US)。三、Mac 端门控什么时候开口朗读是否触发由 ShortcutSettings.swift 控制这里存在一个容易误读的硬编码陷阱let floatingBarVoiceAnswersEnabled: Bool true第 371 行——硬编码常量注释明言Push-to-talk 回复总是会被朗读var hasAnyFloatingBarVoiceAnswersEnabled: Bool { true }第 509-511 行——同样硬编码为 true且并不从上面的常量或类型化开关派生。playFillerIfEnabled()/playResponseIfEnabled()/updateStreamingResponseIfEnabled()用的就是它第 92、144、149 行因此这三个方法永远不会短路——它们总是尝试开口Published var floatingBarTypedQuestionVoiceAnswersEnabled: Bool第 374 行UserDefaults keyshortcut_floatingBarTypedQuestionVoiceAnswersEnabled默认false第 556 行——这是用户可见的打字提问的回复也朗读设置开关func shouldSpeakFloatingBarResponse(forVoiceQuery:) - Bool { forVoiceQuery || floatingBarTypedQuestionVoiceAnswersEnabled }第 513-515 行——这才是真正的逐查询门控。它在FloatingControlBarWindow.swift第 2954、4298 行被读取为shouldPlayVoice该捕获值决定本回合的流式回调是否调用playFillerIfEnabled()/updateStreamingResponseIfEnabled()第 4301-4306、4332-4337、4428-4433 行。语音发起的查询barWindow.state.currentQueryFromVoice为 true即 PTT总是朗读打字查询仅在设置开启时朗读纯语音查询sendVoiceOnlyQuery第 4436 行甚至不检查shouldSpeakFloatingBarResponse——无条件调用interruptCurrentResponse()/playFillerIfEnabled()/updateStreamingResponseIfEnabled(...)第 4461-4463、4489、4514、4516、4518 行因为语音回合必然是语音发起的。另外两个用户可调项语速Published var voicePlaybackSpeed: FloatUserDefaults keyshortcut_voicePlaybackSpeed默认1.4可选档位[0.8, 1.0, 1.2, 1.4, 1.6, 2.0]第 394 行标签为 Slow/Normal/Fast/Faster/Very Fast/Maximum。在startPlayback中通过player.enableRate true; player.rate playbackRate生效——注意它只影响 OpenAI TTS 的AVAudioPlayer路径不影响系统语音降级路径那里固定rate 0.47音色Published var selectedVoiceIDUserDefaults keyshortcut_selectedVoiceID默认openAIShimmerVoiceID即openai:shimmer。精选列表第 440-489 行为 Onyx男声、Shimmer女声默认、Coral女声、Nova女声——每项是一个 OpenAI 声音 id 手写openAIInstructions语气字符串。尽管VoiceOption.Provider.localSystem枚举分支存在选择器中不提供本地系统语音选项。四、抢话Barge-ininterruptCurrentResponse()4.1 函数语义与陈旧租约守卫定义于 FloatingBarVoicePlaybackService.swift 第 485-503 行discardableResult func interruptCurrentResponse(leaseID: VoiceLeaseID? nil, armNextResponse: Bool false) - Bool若传入leaseID且与activePTTLease?.id不匹配则为 no-op陈旧租约守卫记日志并返回 false把当前回复若有标记为interruptedResponseID此后同一条 message id 的流式文本被静默吞掉updateStreamingResponseIfEnabled第 209-214 行设置streamedText、清空bufferedText绝不重新入队音频若当前尚无回复则武装shouldInterruptNextResponse让下一条回复被立即打断调用resetPlaybackPipeline(clearMode: false)第 622-659 行完整动作序列为bumpplaybackGeneration使所有在途合成/播放闭包因代际校验失效→ 取消playbackTask/fillerTask→ 清空全部队列synthesisQueue、audioQueue、streamedText、bufferedText→ 停止audioPlayer和speechSynthesizer→ 释放活动 PTT 租约VoiceOutputCoordinator.shared.release(lease)→ 关闭浮窗辉光setFloatingPillResponseGlow(false)。4.2 调用点文档核对出的全部调用点覆盖每一次新 PTT 按住以及所有与抢话相邻的边界PushToTalkManager 第 434 行startListening()每次新的 PTT 按住开始与第 469 行enterLockedListening()双击锁定进入——两者都是无条件调用且发生在麦克风采集开始之前第 282 行handleVoiceTurnEffect中的.stopPlayback效果来自语音回合状态机——租约变体仅当租约 id 匹配时停止FloatingControlBarWindow.swift第 4129、4277 行sendChatQuery/ 打字发送开始处、第 4461 行sendVoiceOnlyQuery开始处——每次新查询都会先打断正在播放的内容再进入填充语/流式路径RealtimeHubController.swift第 2019、3558 行——实时 hub 的回合边界也会打断浮窗 TTS确保两个音频源永不同时发声。4.3 辉光 /isVoiceResponseActive浮窗圆点的正在说话辉光由 FloatingControlBarState.swift 驱动Published var isVoiceResponseActive: Bool第 282 行didSet 在置 true 时清空isVoiceResponseWaiting与isThinking并调用updateVoiceResponseWatchdog()超时自动清除var isVoiceResponseGlowActive: Bool { isVoiceResponseActive || isVoiceResponseWaiting }第 301-303 行——这是实际的辉光驱动源clearVoiceResponseState()第 705-708 行将两者置 false。播放服务通过setFloatingPillResponseGlow(_ active:)第 661-668 行间接驱动有活动 PTT 租约时经VoiceTurnCoordinator.shared.send(.responseActiveChanged(turnID:active:))否则走setUnscopedResponseActive(active)VoiceTurnCoordinator.swift第 71 行据此将barState.isVoiceResponseActive置 true。因此辉光 ON发生在每次playFillerIfEnabled/updateStreamingResponseIfEnabled/speakOneShot/speakBackgroundAgentKickoff调用开头setFloatingPillResponseGlow(true)辉光 OFF由clearFloatingPillResponseGlowIfIdle()完成——它在每个分块结束或失败时被调用但只有!isSpeaking检查audioPlayer?.isPlaying、localSpeechActive、speechSynthesizer.isSpeaking、填充语/一次性/分块合成在途标志以及两条队列均为空见isSpeakinggetter 第 130-137 行时才会真正熄灭。五、Windows 端 TTSspeakText/playSystemVoice契约Windows 端逻辑集中在 voiceController.ts 与 tts.ts。5.1speakTextvoiceController.ts 第 672-722 行export async function speakText( text: string, voiceId: string DEFAULT_TTS_VOICE, leaseID: VoiceLeaseID | null null ): Promisevoid执行序列先解析音频源尝试synthesizeTts(text, voiceId)后端 TTS blob失败则记录record(tts-fallback, ...)、触发trackEvent(fallback_triggered, { component: voice_tts, from: openai_tts, to: system_voice, reason: provider_unavailable, outcome: degraded })并降级到playSystemVoice(text)播放前注入回声门记录window.omi?.captureCommand({ type: assistant-utterance, utteranceId: tts-${ttsSeq}, text })——与实时语音共用同一个回声门echo-gate契约驱动与实时会话相同的EchoGate播放前gate.playbackStarted(Date.now()); syncGate()play()resolve 后的finally中gate.playbackDrained(Date.now()); syncGate()长度上限MAX_TTS_CHARS 4096、axios 超时 45s 由tts.ts的synthesizeTts强制第 9-24 行——文本在客户端 POST 前被 trim/切片到 4096 字符。5.2playSystemVoicevoiceController.ts 第 448-511 行基于 Web Speech APISpeechSynthesisUtterance并内置两个 Chromium 防卡死机制resumePump每 10s 调用一次window.speechSynthesis.resume()规避长语句下 Chromium 静默停摆停摆会导致onend永不触发进而卡住 Promise、卡死回声门并通过useChat.speaking冻结浮窗圆点和 keepAlive 直到应用重启硬看门狗超时maxMs Math.min(120000, Math.max(8000, text.length * 100))——约 10 字符/秒下限 8s、上限 120s超时后window.speechSynthesis.cancel()并强制 resolveonerror将interrupted/canceled视为正常 resolve即抢话打断其余错误 reject。5.3 遥测字段voiceController.ts tts.ts AGENTS.md 契约voice_tts降级component: voice_tts、from: openai_tts、to: system_voice、reason: provider_unavailable、outcome: degraded——这是 TTS 路径降级的唯一发射点完全复用仓库共享的fallback_triggered契约component/from/to/reason/outcome 均为封闭枚举没有另造计数器独立的voice_echo_gate降级针对看门狗强制的回声门释放component: voice_echo_gate、from: gated、to: released、reason: watchdog_max_hold、outcome: degraded——不属于 TTS 专属但与speakText共用同一套门机制record(type, detail)模块级环形缓冲容量 200记录tts-fallback、tts-start、tts-end供实时 loop-check 测试工具使用——与 PostHog 的trackEvent相互独立。5.4 已端到端接通的「浮窗 Ask-AI → PTT → 语音回复」链路文档逐行核对了这条完整调用链全部位于当前 worktreeBarApp.tsx 第 153-175 行usePushToTalk({ onCommit: (text) sendFromBar(text, true), ... })——每次 PTT 按住释放提交都会以fromVoice: true调用sendFromBarsendFromBar第 154 行起window.omiBar.sendChat(text, fromVoice)→ IPCbar:sendChatpreload 与 main 进程的 bar 窗口处理器main 进程把消息转发回主窗口渲染进程由 ChatBridgeHost.tsx 通过window.omi.onBarChatSend接住并调用唯一共享的useChat().sendsendRef.currentuseChat.ts 的send(text, { fromVoice })第 1056 行起在成功路径上流式回复完整渲染且非空后调用maybeSpeak(assistantText, fromVoice)plan 执行分支与 plan 错误分支也会调用它。maybeSpeak第 209-210 行在fromVoice为 true 且文本非空时 fire-and-forget 调用speakText(text)并维护一个speaking引用计数布尔状态——ChatBridgeHost将其投影回浮窗作为status: speaking浮窗圆点的正在说话姿态就读这个状态catch 分支网络/流错误从不调用maybeSpeak——与 Mac绝不朗读僵尸/错误回复行为一致不过 Mac 在部分分支会用speakOneShot朗读特定面向用户的错误字符串FloatingControlBarWindow.swift第 4516-4518 行Windows 目前没有对应物。结论Windows 的 PTT 浮窗回复当前就会朗读路径是useChat.ts的maybeSpeak/speakText由从 PTT 提交一路穿透到这里的fromVoice: true驱动。六、文档记录的 Gap 与当前仓库中的实现演进文档明确记录了两处真实的差距而从当前仓库源码看这两处已经或正在被实现补齐Gap 1打字浮窗提问永不朗读、且无设置开关。文档核对时 BarApp.tsx 第 377 行的onSubmit{(text) sendFromBar(text, false)}硬编码fromVoice: false且 preferences.ts 中 grep 不到任何 voice/tts 偏好键。当前源码状态已经演进BarApp.tsx第 96-98 行新增const [typedVoice, setTypedVoice] useState(() !!getPreferences().floatingBarTypedVoiceEnabled)第 691 行变为onSubmit{(text) sendFromBar(text, typedVoice)}——打字提交不再硬编码 falsepreferences.ts第 109-113 行新增floatingBarTypedVoiceEnabled?: boolean注释明确它是 Macshortcut_floatingBarTypedQuestionVoiceAnswersEnabled的对齐默认关闭即未定义 关与 Mac 默认一致。也就是说文档所记的唯一可归属 Track-2 的钩子——在BarApp.tsx的调用点决定fromVoice传什么——已经在typedVoice偏好中实现。Gap 2Windows 没有填充语、没有分块/流式 TTS、没有 PTT 按住即抢话的入口。文档核对时speakText/voiceController.ts/usePushToTalk.ts中无 filler 短语、整条回复一次性合成而非流式逐块、无interruptCurrentResponse等价调用唯一打断路径是teardown()/stopCurrentTts且旧播放仅被孤儿化而非显式取消。当前源码状态已经演进可从源码结构确认ttsChunker.ts 已存在FIRST_CHUNK { min: 40, preferred: 120, emergency: 200 }与FOLLOW_CHUNK { min: 320, preferred: 520, emergency: 800 }nextChunkBoundary(text, isFirstChunk)是 MacnextChunkBoundary(in:isFinal:isFirstChunk:)以isFinal false的直译注释写明Port of macOS分块优先级完全一致句末标点. ! ? \n 从句标点, ; : \n 空白 紧急长度硬切voiceController.ts已新增runChunkedTts分块流水线播放块 N 的同时开始合成块 N1多块回复才启动 filler、synthChunk块级合成失败映射为系统语音降级并上报共享遥测、startFiller/cancelFillerFILLER_PHRASES随机选句首块真实音频到达即取消、resetTtsPipelinebumpttsGeneration abort 在途合成 取消 filler 停止当前播放被新回复、PTT 抢话、会话 teardown 共享以及interruptCurrentResponse(leaseID)——注释明确它经 IPC 接到 macOSPushToTalkManager.startListening → FloatingBarVoicePlaybackService.interruptCurrentResponse的同一位置且registerTtsStop(resetTtsPipeline)让实时语音通道在可听见时物理抢占级联 TTSusePushToTalk.ts 已定义onHoldStart回调第 68-72 行注释指明这就是每次新 PTT 按住开始的抢话缝macOS 恰在此点调用interruptCurrentResponse()第 476 行附近也有 hold-start 处抢话的对应逻辑。这些演进是否完全等同 Mac 的每次 hold-start 无条件interruptCurrentResponse()仍需以 Windows 端实际调用点为准但从上述源码可见文档记录的差距已在当前仓库中被系统性补齐Windows 端正逐步逼近 Mac 的语音体验基线。七、后端POST /v1/tts/synthesize契约文档还纠正了一个归属问题Windows 端经VITE_OMI_DESKTOP_API_BASE调用的并不是 Python 后端的/v2/tts/synthesize而是backend/routers/desktop_tts_updates.py中的/v1/tts/synthesize——这是 macOS 与 Windows 客户端共享的桌面 TTS 端点。7.1 请求、校验与鉴权方法POST /v1/tts/synthesize路由注册于第 298 行附近鉴权PaywalledAuthUser提取器Firebase 认证 付费墙检查——handler 内不读取也不检查任何平台头请求体{ text: string, voice_id: string, instructions?: string }TtsSynthesizeRequest第 33-36 行校验text去空白后必须非空char_countUnicode 标量计数必须 4096_MAX_TTS_CHARS否则 400 text is too longvoice_id必须是alloy, ash, ballad, coral, echo, fable, nova, onyx, sage, shimmer, verse, marin, cedar之一_is_allowed_openai_voice第 68-69 行否则 400 voice_id is not supported。7.2 密钥解析与限流BYOK 优先若请求头中带有激活的 BYOK OpenAI keybyok::get_byok_key_if_active直接使用且不施加服务端限流否则回退到服务器配置的state.config.openai_api_key未配置则 503 OpenAI TTS is not configured此时才施加服务端 key 限流服务端限流check_server_tts_rate_limit第 240-276 行Redis 支撑SERVER_TTS_BURST_PER_MINUTE 2060s 窗口与SERVER_TTS_DAILY_CHARS 50_000每日字符数任一超限返回 429若 Redis 未配置fails closed503 TTS rate limiting is unavailable——这与 Python 后端/v2/tts/synthesize的Redis 错误时 fails openstatus -1注释 TTS is best-effort策略形成鲜明对比。7.3 上游调用与响应上游OpenAIPOST https://api.openai.com/v1/audio/speechmodel: gpt-4o-mini-tts、response_format: mp3、Bearer 认证对瞬时状态码408, 425, 429, 500, 502, 503, 504, 529最多重试 3 次退避为300ms * attempt第 26、152-190 行非瞬时上游错误如 401/400立即返回TtsProxyError::Upstream(status, body)body 截断至 500 字符响应整体 blob、非流式——upstream.bytes().await完整缓冲 OpenAI 响应后Body::from(bytes)返回content-type: audio/mpeg状态 200错误形状JSON{ error: message }携带对应状态码——400空/过长文本、不支持的声音、503MissingApiKey或RateLimitUnavailable、429RateLimited消息为 TTS burst rate limit exceeded 或 TTS daily character limit exceeded、上游状态透传消息 OpenAI TTS request failed: {body}、502BadGateway到 OpenAI 的网络/传输失败。7.4 平台识别与/v2/tts/synthesize的区分平台识别无。Windows 与 macOS 命中的是完全相同的端点和契约服务端不做任何平台差异化Python 后端的/v2/tts/synthesizebackend/routers/tts.py是完全独立、不相关的路由ElevenLabs 支撑、请求形状为model_id/output_format/voice_settings、StreamingResponse流式返回、5000 字符上限、不同的限流策略——两个客户端当前都不为该功能调用它。文档特别指出这一点是为了避免审计简报中Python 后端托管/v1/tts/synthesize的错误假设。八、双端对齐要点速查维度macOSFloatingBarVoicePlaybackService / ShortcutSettingsWindowsvoiceController / ttsChunker / useChat分块策略流式增量切块首块 40/120/200后续 320/520/800一次性chunkTts切块非流式阈值与优先级与 Mac 逐字一致填充语fillerPhrases8 句首块到达即取消FILLER_PHRASESstartFiller多块回复才启用首块前取消主 TTS 引擎OpenAI TTSgpt-4o-mini-tts走/v1/tts/synthesize同一后端端点synthesizeTts系统语音降级AVSpeechSynthesizerrate 0.47 / pitch 1.02 / volume 1.0Web Speech API resumePump 看门狗约 10 字符/秒降级遥测recordFallbackquota / auth / provider_429 / provider_5xx / enqueue_failedfallback_triggeredvoice_ttsprovider_unavailabledegraded语音回复门控PTT 总是朗读打字需floatingBarTypedQuestionVoiceAnswersEnabled默认关fromVoice: true总是朗读打字由floatingBarTypedVoiceEnabled偏好控制默认关抢话每次 PTT hold-start 无条件interruptCurrentResponse()含陈旧租约守卫interruptCurrentResponse()resetTtsPipeline()usePushToTalk.onHoldStart抢话缝回声门语音输出与采集端通过捕获记录对齐assistant-utterancecaptureCommand EchoGateplaybackStarted/playbackDrained如需继续深入可依次阅读ground truth 原文、Mac 播放服务源码 FloatingBarVoicePlaybackService.swift、Windows 播放控制器 voiceController.ts、分块器 ttsChunker.ts 及其单测 ttsChunker.test.ts以及后端端点 desktop_tts_updates.py。【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表