Files
goodbuddy/docs/architecture/full-duplex-voice-design.md
T
mesalogo 43e1d162dc feat: add explicit managed model download sources
Managed speech and OCR downloads previously used catalog-specific source URLs without a global selection. Platform Features now lets users choose ModelScope by default or Hugging Face, while Main validates and freezes that source for each download.

Verified coverage remains explicit: downloads never mix artifacts or silently switch sources, and installed models plus ZIP imports stay source-independent.

Release note: 可在“设置 → 平台功能 → 通用设置”中选择 ModelScope 或 Hugging Face 作为后续语音输入与 OCR 模型下载源;缺少完整已验证文件或下载失败时不会静默换源。
2026-08-19 13:43:16 +08:00

41 KiB
Raw Blame History

GoodBuddy 全双工实时语音交互设计

文档信息

项目 内容
文档类型 跨功能技术与产品架构
状态 设计中
版本 0.1
日期 2026-08-19
适用产品 GoodBuddy 桌面端
目标平台 Windows、macOS、Linuxx64 与 arm64
相关基线 跨平台助手产品设计长期助手路线图统一界面设计系统

本文定义 GoodBuddy 中类似自然通话的全双工实时语音能力,包括本地与云端语音引擎、 音频平面、会话状态、打断语义、工具审批、数据留存、失败恢复、跨平台交付和验收指标。

本文所称“支持本地与云端”是指用户可以显式配置并选择不同语音引擎,不代表系统可以在 它们之间自动切换。GoodBuddy 不设计静默降级。


1. 摘要与核心决策

  1. 实时语音是独立的 VoiceSession,不把现有一次性语音听写改名后直接复用。
  2. 系统支持三种显式引擎:
    • 本地模块化全双工:本地流式 ASR、所选 Agent Runtime、本地流式 TTS。
    • 本地原生全双工:一个本地端到端语音模型同时听、想和说。
    • 云端原生全双工:通过供应商 Realtime/Live API 进行双向流式音频交互。
  3. 用户开始会话时冻结引擎、Provider、模型、版本、地域、数据位置、声音、能力和 Turn Detection 配置。会话过程中不得静默替换。
  4. 同一目标内允许有界重试、网络抖动恢复和语义等价的内部执行优化;任何会改变 Provider、模型、数据位置、成本、隐私、能力、质量或可感知延迟的替代路径都必须显式。
  5. 所选引擎不可用时,会话明确进入 blockedfailed,保留可恢复上下文,并提供 “重试当前引擎”或“结束后选择其他引擎”。不自动切换本地/云端,不退回听写、纯文本或 非全双工模式。
  6. Renderer 负责麦克风采集、回声消除、低延迟播放和即时打断;Main 负责凭据、会话 控制、Provider Adapter、工具权限、持久化和资源回收。
  7. 音频帧不进入普通 AgentEvent 和聊天消息持久化通道。默认只保存最终文本、会话状态和 有界诊断,不保存原始录音。
  8. 语音不能成为新的授权通道。Ask 继续只读,Execute 的工具调用继续经过现有审批控件。

2. 背景与当前基础

GoodBuddy 当前已经具备:

  • Renderer 中的麦克风入口、录音状态和取消操作。
  • getUserMedia 的单声道采集、回声消除和噪声抑制。
  • 将完整录音重采样为 16 kHz PCM 的能力。
  • 基于 sherpa-onnx 的本地离线识别、模型下载、ZIP 迁移、选择和删除。
  • Main 中受信任发送者校验、Zod IPC 输入校验、超时、取消和应用关闭回收。
  • AgentRuntime.run() 的流式文本、工具事件、审批、取消和会话持久化。
  • Renderer 中的流式聊天时间线、全局通知和可访问的输入控件。

当前链路仍是:

点击麦克风
→ 最多录音 20 秒
→ 停止并一次性发送完整 PCM
→ 本地离线转写
→ 把文本插入输入框
→ 用户再次确认发送

该链路适合听写,但不具备:

  • 连续流式识别和临时转写。
  • 同时采集与播放。
  • 自动轮次检测。
  • 助手语音输出。
  • 用户抢话和响应截断。
  • 音频队列、背压和时钟同步。
  • 实时语音 Provider 抽象。
  • 语音会话快照和诊断。

因此实时语音必须新增会话层,而不是在现有 SpeechTranscriptionService 后面简单追加 TTS。


3. 目标

3.1 用户目标

  • 用户可以像通话一样持续说话,不需要每轮点击开始和停止。
  • 助手可以边生成边说,并显示与实际播放进度一致的文本。
  • 用户开口时可以自然打断,助手在很短时间内停止出声并开始听取新内容。
  • 用户始终知道当前使用本地还是云端、具体引擎是什么、音频或转写文本会去哪里。
  • 本地或云端引擎失败时,用户能看到准确状态并决定下一步,不被系统暗中换模型。
  • 语音对话继续拥有文本聊天中的项目、知识库、角色、Ask/Execute、工具审批和历史能力。

3.2 产品目标

  • 在六个平台/架构目标上提供统一的上层会话契约。
  • 先以现有 sherpa-onnx 和 Agent Runtime 构建可跨平台交付的本地模块化引擎。
  • 允许云端 Provider 使用 WebRTC 或 WebSocket,但不把供应商协议泄漏到通用 UI。
  • 允许高性能设备安装本地原生全双工模型,但按真实能力检测决定是否可选。
  • 保持 Main-only 凭据、上下文隔离、沙箱、取消、超时、有界输出和关机回收。
  • 为延迟、打断、回声、音频中断、Provider 错误和成本提供可诊断指标。

4. 非目标

首期不包含:

  • 唤醒词、后台常驻监听或应用退出后的麦克风采集。
  • 根据网络、负载、价格或“智能判断”自动选择语音引擎。
  • 在一个会话内自动从云端切到本地,或从本地切到云端。
  • 从原生全双工自动退到 ASR → LLM → TTS,或反向切换。
  • 在实时语音失败后自动改成一次性听写、纯文本发送或系统 TTS。
  • 默认保存、上传或训练用户原始音频。
  • 声音克隆、未成年人声音模仿、电话呼入或多人会议。
  • 使用口头“同意”替代工具审批按钮或键盘确认。
  • 绕过当前 Agent Runtime 和权限边界的 Provider 直连工具、MCP 或 Connector。
  • 保证所有本地原生语音模型都能在 CPU 或全部六个发布目标上运行。

5. 术语与全双工范围

术语 定义
VoiceEngineProfile 用户保存的语音引擎配置,包含类型、Provider、模型、地域、声音和能力
VoiceSession 一次从用户显式开始到结束的连续实时语音会话
VoiceTurn 用户输入和助手响应形成的一次可持久化对话轮次
系统级全双工 麦克风在助手播放期间继续采集,用户可以随时打断
原生模型全双工 同一个模型联合处理持续输入、轮次判断和持续语音输出
模块化全双工 ASR、Agent Runtime 和 TTS 分离,但系统保持同时听说与可打断
临时文本 尚未确认的 ASR 或尚未实际播放的助手文本,不写入长期历史
已提交文本 用户轮次已确认,或助手对应音频已实际播放的文本
Barge-in 用户在助手说话时开口,触发立即静音、取消和上下文截断
引擎快照 会话开始时冻结的完整、无凭据配置及能力声明

“模块化”不等同于“回退”。当用户明确选择模块化本地引擎时,它就是该会话的唯一正式 执行路径。原生模型和模块化引擎之间没有隐式优先级。


6. 不静默降级产品契约

6.1 必须显式的变化

以下变化不得在活动会话中静默发生:

  • 本地与云端之间切换。
  • Provider、Endpoint、地域或账号切换。
  • 模型 ID、模型版本、量化档位或语音角色切换。
  • 原生全双工与模块化全双工之间切换。
  • ASR、LLM 或 TTS 组件切换。
  • 从音频输入改成仅文本输入,或从语音输出改成仅文本输出。
  • 禁用原本声明可用的工具、知识库、角色或 Execute 能力后继续运行。
  • 把原始音频改为上传,或改变云端数据地域和保留策略。
  • 采用明显更慢、更低质量或成本不同的路径。

6.2 可自动进行的恢复

以下操作可以自动执行,但必须保持同一引擎快照:

  • 同一连接内的丢包恢复、抖动缓冲和音频重排。
  • 同一 Provider、模型、地域和配置的有限重连。
  • 同一本地模型进程的有限重启。
  • 不改变语义、隐私、成本和已声明性能级别的算子或执行 Provider 优化。

恢复在用户可感知前完成时可不打断界面;持续超过 500 ms、导致音频停顿或创建新远端 会话时,必须显示“正在重新连接当前引擎”。所有恢复都进入有界诊断记录。

6.3 失败后的用户决策

恢复预算耗尽后:

  1. 停止采集上传和音频播放。
  2. 将临时文本标记为未提交,不伪装成完整轮次。
  3. 保存已提交文本和脱敏错误。
  4. 显示当前失败的引擎、影响和建议。
  5. 提供“重试当前引擎”和“结束语音会话”。
  6. 用户结束后可以显式选择其他引擎并开始新会话。

首期不提供自动 Failover 列表。未来即使允许用户预先配置替代引擎,也必须在切换前获得 明确确认,并在会话中持续显示新的活动引擎。

6.4 产品级适用范围

本契约不仅适用于语音。GoodBuddy 中 Provider、模型、Runtime、数据处理位置、工作模式、 权限范围和质量档位等影响隐私、成本或能力的用户选择,都不得被静默替换。

用户明确选择名为“自动”的策略时,系统可以在该策略事先声明的范围内选择,但实际结果和 任何能力退化必须可见、可诊断,不能把空结果或不完整结果表示为正常成功。


7. 用户体验

7.1 入口

现有麦克风入口继续表示“语音输入/听写”,转写进入可编辑输入框,不自动发送。

实时语音使用独立的“开始语音对话”入口,避免用户误以为点击一次听写会开启持续监听。 入口只在以下条件满足时可用:

  • 已选择并验证一个全双工语音引擎。
  • 当前平台满足该引擎能力要求。
  • 麦克风权限可申请。
  • 当前 Conversation 没有冲突的活动请求。
  • 当前工作模式和引擎能力兼容。

7.2 会话界面

活动会话显示一个持续可见的语音控制区:

  • 当前状态:准备中、正在听、用户说话、正在思考、助手说话、正在打断、等待审批、 正在重连、失败。
  • 本地/云端徽标、引擎名称和数据去向。
  • 实时用户转写和与播放同步的助手文本。
  • 麦克风静音、结束会话和必要的设备入口。
  • 输入音量与助手播放状态,但不得只用颜色表达。
  • 云端会话的使用量或成本提示入口。

“结束语音会话”是活动状态下的唯一主操作。波形和头像动效遵守 prefers-reduced-motion,关闭动效后仍使用文字和图标表达状态。

7.3 打断

助手说话期间检测到用户有效语音:

  1. 在 Renderer 立即对当前音频执行 20–40 ms 淡出。
  2. 清空尚未播放的音频队列。
  3. 向 Main 发送包含播放位置的 interrupt
  4. Main 取消当前 Agent/TTS 响应或向 Provider 发送截断事件。
  5. 尚未播放的助手文本保持临时状态并从会话上下文中移除。
  6. 输入状态切到用户说话,继续采集,不重新建立会话。

键盘点击“停止说话”与语音 Barge-in 使用相同取消和提交语义。

7.4 工具和审批

  • Ask 模式继续在 Runtime 边界保持只读。
  • Execute 模式的工具调用进入现有 Approval Broker。
  • 等待审批时暂停新的助手音频,可播放一次确定性的短提示,例如“需要你确认一个操作”。
  • 工具参数、风险、范围和确认操作使用现有可访问审批控件。
  • 麦克风中的“同意”“确认”或相似内容只作为普通用户文本,不构成授权。
  • 用户拒绝或取消后,结果作为结构化工具事件返回当前引擎,不私自换模型继续。

7.5 设置结构

长期设置结构使用一级“语音”分类,并以 PageTabs 组织:

  1. 实时对话:语音引擎列表、默认引擎、能力、数据位置、地域、声音和真实连接测试。
  2. 语音输入:现有本地 ASR 模型、一次性听写和麦克风设置。
  3. 语音输出:本地 TTS 模型、声音、语速和试听。

当前“模型连接”中的“语音输入”可在迁移阶段保留,之后移动现有模型管理组件时必须保存 已安装模型和选择,不创建第二份设置。

模型类型选择器当前已经包含四项,不增加第五个分段项来承载实时语音,以免违反 SegmentedControl 的 24 项约束。

语音引擎卡片必须持续显示:

  • 本地或云端。
  • Provider、模型和版本。
  • 支持的语言。
  • 系统级或原生模型全双工。
  • 是否支持工具、图像和当前 Ask/Execute 模式。
  • 所需硬件或云端地域。
  • 音频和文本的数据去向。
  • 安装、已验证、不可用或需要凭据状态。

实际生成能力只能通过一次真实、有界、由用户触发的会话测试确认。配置保存成功或只完成 握手不能证明麦克风输入、语音输出和打断均可工作。


8. 总体架构

┌──────────────────────── Renderer ────────────────────────┐
│ VoiceSession UI                                           │
│ getUserMedia → AudioWorklet Capture → Fast VAD            │
│ AudioWorklet Playback ← Jitter/Playback Queue              │
└────────────── control IPC ─────── media MessagePort ───────┘
                              │
┌────────────────────────── Main ────────────────────────────┐
│ VoiceSessionController                                     │
│ ├─ Session snapshot and state                              │
│ ├─ Turn coordinator and interruption                      │
│ ├─ Tool/approval bridge                                    │
│ ├─ Transcript/message persistence                         │
│ ├─ Credential and provider policy                         │
│ └─ VoiceEngineAdapter                                      │
│      ├─ LocalModularAdapter                                │
│      ├─ LocalNativeDuplexAdapter                           │
│      └─ CloudRealtimeAdapter                               │
└───────────────┬──────────────────────┬─────────────────────┘
                │                      │
        Local managed sidecar     Cloud Realtime API
        or bounded worker         WebRTC / WebSocket

8.1 Renderer 音频平面

Renderer 负责需要接近音频设备的低延迟操作:

  • 在用户操作后调用 getUserMedia
  • 请求单声道、回声消除、噪声抑制和受支持时的自动增益。
  • 使用 AudioWorklet,不继续扩展 ScriptProcessorNode
  • 将音频切成 10–20 ms 有序帧,并按引擎格式重采样。
  • 执行快速本地 VAD,用于 Barge-in,不独立提交最终轮次。
  • 维护有界播放和抖动缓冲,记录实际播放采样位置。
  • 在打断、设备变化、休眠或窗口销毁时立即静音和释放资源。

Renderer 不持有长期 API Key、不创建本地模型目录、不决定工具权限,也不持久化原始音频。

8.2 Preload 与 IPC

控制面使用显式、类型化的 preload 方法:

  • voice.getSnapshot()
  • voice.startSession(input)
  • voice.stopSession(sessionId)
  • voice.setMuted(input)
  • voice.interrupt(input)
  • voice.respondApproval(...) 继续复用现有审批接口
  • voice.onEvent(listener)

音频帧不使用逐帧 ipcRenderer.invoke、JSON 或 Base64。Main 通过 MessageChannelMain 向可信主 Frame 传递专用 MessagePort,使用可转移 ArrayBuffer 和严格的帧头。控制事件和媒体帧分别限速、限长和验证。

8.3 Main 控制面

VoiceSessionController 负责:

  • 每个窗口最多一个活动语音会话。
  • 解析并冻结 VoiceSessionSnapshot
  • 建立所选 Adapter,不执行自动 Adapter 选择。
  • 维护输入、输出和生命周期状态。
  • 将 Barge-in 传播到 Provider、Agent Runtime、TTS 和播放队列。
  • 桥接工具调用、审批、问题和取消。
  • 只提交已经确认或实际播放的文本。
  • 处理超时、重连预算、应用退出、系统休眠和窗口销毁。
  • 对错误和诊断执行脱敏与边界限制。

8.4 本地进程边界

轻量 ONNX 能力可以运行在受控 Worker。需要 Python、CUDA、Metal/MLX 或独立依赖树的 原生模型运行在 GoodBuddy 管理的 Sidecar

  • 只绑定 loopback,不监听外部网卡。
  • 使用随机端口和每次启动的短期认证值。
  • 环境变量使用最小 allowlist。
  • 不继承云端 Provider 密钥。
  • 模型路径由 Main 从受管目录解析,不接受任意相对路径。
  • 启动、健康检查、并发、输出、内存、超时和进程树有界。
  • 应用退出时终止完整进程树。

Sidecar 不因本地模型启动失败而自行连接云端。

8.5 云端连接边界

供应商支持 WebRTC 时优先使用其媒体传输、编解码和抖动能力:

  • Main 使用长期凭据创建受限、短时的会话描述或临时凭据。
  • Renderer 只接收当前会话需要的短期材料。
  • 工具和业务事件优先由 Main sideband 连接处理。
  • Provider 不支持 sideband 时,由 Main 拥有 WebSocket,并通过媒体 MessagePort 与 Renderer 交换音频。

长期凭据永不进入 Renderer、日志、诊断或会话快照。云端 Profile 必须固定可信 Endpoint、 地域和数据说明,不跟随重定向切换到未声明的主机。


9. 共享契约

建议新增 src/shared/voice-contracts.ts,核心结构如下:

type VoiceEngineKind =
  | 'local-modular'
  | 'local-native-duplex'
  | 'cloud-native-duplex'

type VoiceComponentRef = {
  providerId: string
  modelId: string
  modelVersion?: string
  endpoint?: string
  region?: string
  accountRef?: string
  credentialRef?: string
}

type VoiceEngineProfile = {
  id: string
  name: string
  kind: VoiceEngineKind
  locality: 'local' | 'cloud'
  voiceId: string
  components: {
    asr?: VoiceComponentRef
    tts?: VoiceComponentRef
    nativeDuplex?: VoiceComponentRef
  }
  dataPath: {
    audioDestination:
      | { kind: 'device' }
      | {
          kind: 'provider'
          providerId: string
          endpoint: string
          region?: string
        }
    transcriptDestination:
      | { kind: 'device' }
      | {
          kind: 'provider'
          providerId: string
          endpoint: string
          region?: string
        }
    retentionPolicyId?: string
  }
  capabilities: {
    nativeDuplex: boolean
    supportsTools: boolean
    supportsAsk: boolean
    supportsExecute: boolean
    inputLanguages: string[]
    outputLanguages: string[]
  }
}

type VoiceRuntimeSnapshot = {
  selection: Exclude<AgentRuntimeSelection, { provider: 'auto' }>
  profileRevision?: string
  configurationDigest: string
  workspacePath: string
}

type VoiceSessionSnapshot = {
  sessionId: string
  conversationId: string
  profile: VoiceEngineProfile
  profileRevision: string
  engineConfigurationDigest: string
  runtime?: VoiceRuntimeSnapshot
  workMode: 'ask' | 'execute'
  inputFormat: VoiceAudioFormat
  outputFormat: VoiceAudioFormat
  turnDetection: VoiceTurnDetectionConfig
  startedAt: string
}

credentialRefaccountRef 是不含凭据正文的稳定引用。Endpoint 写入 Profile 或快照前 必须规范化并删除用户名、密码、查询参数和 Fragment;供应商部署路径仍应保留,以便检测 Endpoint 是否发生变化。Profile 持久化时只引用 Main 加密设置,快照不包含长期或临时 Token。

模块化引擎分别记录 ASR 和 TTS 组件,原生引擎记录 nativeDuplex 组件;不能用一个 modelId 代表多组件链路。文本 Agent 使用独立 VoiceRuntimeSnapshot,记录已解析的明确 Runtime、模型 Profile 修订、配置摘要和工作区。dataPath 分别说明原始音频和转写文本 留在设备还是发送到哪个供应商。

9.1 Runtime Lease

语音会话不能在每轮请求时重新读取可变的全局 Runtime 设置。启动时必须:

  1. auto 解析为明确的 Runtime 和模型 Profile,并在会话界面显示实际结果。
  2. 根据已解析配置创建或取得一个不可变的 VoiceRuntimeLease
  3. Lease 在整个 Voice Session 内引用同一个 Runtime 实例和配置摘要。
  4. 全局设置变化只为新请求和新 Voice Session 创建 Runtime,不替换活动 Lease。
  5. 用户删除或修改活动 Profile 时,界面说明“下次语音会话生效”;当前 Lease 继续运行。
  6. 固定实例无法继续时,当前语音会话明确失败,不能取得新的全局 Runtime 继续。

现有 AgentRuntimeController 的可变 current Slot 会在 replace() 后中断活动请求,因此 不能直接作为长期 Voice Session Lease。实现前必须增加引用计数式 Pin/Lease,或由 SelectedRuntimeManager 为会话持有独立 Runtime Slot;会话结束后再 releaseConversation() 并释放 Lease。

9.2 事件

控制事件至少包括:

  • session-preparing
  • session-ready
  • session-reconnecting
  • input-speech-started
  • input-transcript-delta
  • input-transcript-committed
  • response-started
  • response-transcript-delta
  • response-audio-started
  • response-interrupted
  • response-completed
  • approval-required
  • tool-state
  • usage
  • error
  • session-ended

音频帧使用独立二进制协议,包含:

  • sessionId
  • generationId
  • sequence
  • timestampSamples
  • sampleRate
  • channels
  • encoding
  • payload

帧乱序、重复、跨会话或超过大小上限时直接拒绝,不尝试解释为其他格式。


10. 状态模型

全双工不能只用一个“正在听/正在说”枚举描述。会话使用三个正交状态:

Lifecycle:
idle → preparing → active ↔ reconnecting → ended
                         └──────────────→ failed

Input:
muted ↔ listening ↔ speech

Output:
idle → generating → playing → interrupting → idle

用户可见状态由三个状态组合得出。合法示例:

  • input=listening + output=playing:助手说话,同时继续监听。
  • input=speech + output=interrupting:用户抢话,助手正在停止。
  • lifecycle=reconnecting + input=muted + output=idle:当前引擎重连,停止上传。

awaiting-approval 是运行阻塞原因,不关闭会话;此时输入可以继续听取取消或补充文本, 但不能把口头内容解释成授权。


11. Turn、文本与播放提交

11.1 用户输入

  • 流式 ASR Delta 只用于界面。
  • Endpoint Detector 确认轮次后产生 committed transcript。
  • 空白、纯噪音和低置信度片段不创建用户消息。
  • 用户可在提交前通过键盘修正;修正结果而非原始猜测进入 Agent Runtime。

11.2 助手输出

模块化 TTS 可能落后于文本生成,因此助手文本分为:

  • generated:模型已生成,尚未安排播放。
  • queued:已生成音频,尚未播放。
  • played:对应音频已从播放时钟确认输出。

助手消息需要区分“用户可见历史”和“下一轮模型上下文”:

  • 所有已展示的有界文本和结构化内容都写入可见消息历史。
  • 可朗读文本记录 generatedqueuedplayed 边界;中断后的消息标记为 interrupted,并保留用户已经看见的内容及已播放边界。
  • 下一轮模型上下文只包含 played 可朗读文本,以及已经展示的 visual-only 内容。
  • 尚未播放的可朗读尾部即使曾临时显示,也不回送模型,并在历史中显示“未播完”状态。

代码块、表格、URL、引用和工具结果等不适合逐字朗读的内容使用 visual-only Block。它们 一旦完整展示即可进入可见历史和下一轮上下文,不受语音播放边界裁切。这样既不会丢失用户 已经看到的详细成果,也不会让模型误以为用户听到了被打断的语音尾部。

云端 Provider 支持会话截断时,Main 使用实际播放位置截断远端 Conversation Item 不支持时由 GoodBuddy 在下一轮上下文中只组装 played 和已展示的 visual-only 部分。 现有消息契约与上下文组装器需要增加对应 Block 状态,不能用删掉完整助手消息来模拟截断。

11.3 文本转语音规划

模块化引擎从流式文本中产生可取消的短语块:

  • 优先在中文标点、英文句界和自然从句边界提交。
  • 首个短语不等待完整回答,以降低首音频延迟。
  • URL、Markdown 标记、代码块、表格、引用编号和工具 JSON 不逐字符朗读。
  • 不能可靠口述的内容在界面展示,并使用确定性短提示说明“详细内容已显示在对话中”。
  • 不调用第二个未选择的模型生成“语音摘要”。

12. 引擎设计

12.1 本地模块化全双工

首个跨平台本地基线复用现有 sherpa-onnx

AudioWorklet
→ Silero/TEN VAD
→ sherpa-onnx OnlineRecognizer
→ selected AgentRuntime
→ deterministic speech text planner
→ sherpa-onnx TTS callback
→ AudioWorklet playback

现有 sherpa-onnx Node Addon 已提供在线识别、VAD、本地 TTS 和 TTS 音频回调。当前已安装 的 SenseVoice、Paraformer 和 Whisper 目录主要用于离线识别;实时模式需要独立的在线 模型目录和能力声明,不能把离线模型误标成流式模型。

“本地模块化”只保证音频采集、ASR 和 TTS 在本机。中间 Agent Runtime 是否本地取决于 用户明确选择的模型连接:

  • 连接到 loopback 本地模型时,完整链路可离线。
  • 连接到云端文本模型时,原始音频留在本地,但最终转写文本和 Agent 上下文会发送到 该模型。界面必须明确显示这一数据路径。

不得因当前文本 Runtime 不可用而替换为另一模型连接。

12.2 本地原生全双工

本地原生 Adapter 面向 MiniCPM-o、Moshi/PersonaPlex、BayLing-Duplex 等能够持续接收并 生成音频的模型。具体模型接入前必须逐个验证:

  • 中文和目标语言质量。
  • 真正的持续输入、Barge-in 和 Backchannel,而不只是流式输出。
  • 首音频延迟和长期运行内存。
  • Windows、macOS、Linux 及 x64/arm64 Runtime 可用性。
  • NVIDIA CUDA、Apple Silicon 或 CPU 的真实硬件要求。
  • 工具调用、系统指令、上下文长度和取消支持。
  • 模型、声音、训练数据与商业分发许可。
  • 权重下载、ZIP 迁移、校验和、磁盘占用和卸载。

本地原生模型不作为六平台默认能力。只有能力检测和一次真实会话测试通过后才允许选择。 缺少结构化工具能力的模型可以声明为 Ask-only;Execute 入口必须阻塞并说明原因,不能暗中 调用另一个文本模型补齐工具。

12.3 云端原生全双工

云端 Adapter 可以面向 OpenAI Realtime、Gemini Live、Qwen Realtime、Azure Voice Live 等正式配置。每个 Adapter 必须显式声明:

  • WebRTC 或 WebSocket 传输。
  • 输入输出音频格式。
  • VAD、Semantic Turn Detection 和手动提交能力。
  • 响应取消、音频截断和实际播放对齐能力。
  • 输入与输出转写能力。
  • 工具调用和 sideband 控制能力。
  • 会话时长、上下文、速率限制和费用。
  • 可用地域、数据处理与保留说明。

Provider 配置不使用泛化“OpenAI compatible”推断 Realtime 能力。普通 Chat Completions Endpoint 不能因为 URL 相似就被标记为实时语音。


13. 音频处理

13.1 采集

  • 浏览器设备通常以 44.1 或 48 kHz 采集,不能假定请求值就是实际值。
  • 使用 MediaStreamTrack.getSettings() 记录实际声道、采样率和回声消除状态。
  • AudioWorklet 以原始设备时钟采集,再按引擎要求转换为 16/24/48 kHz。
  • 默认单声道 Float32 内部格式,边界处转换为 PCM16、Opus 或 Provider 指定格式。
  • 每帧 10–20 ms,带序号和采样时间,不使用墙钟猜测播放位置。

13.2 回声与抢话

回声处理使用两层信号:

  1. Chromium AEC/NS/AGC 处理后的麦克风流。
  2. GoodBuddy 已知的播放活动、播放能量和 VAD 结果。

只有满足最短语音持续时间、能量和回声相关性条件时才触发 Barge-in。阈值必须可测试, 不能仅依赖一个 Provider 的 speech_started 事件。Provider 事件作为权威轮次信号之一, 本地快速 VAD 负责先静音。

13.3 播放与背压

  • 每个响应使用独立 generationId,旧响应帧不得进入新队列。
  • 播放队列按采样时钟排序,禁止无限积压。
  • 达到高水位时对上游施加背压;无法背压的 Provider 丢弃会话并报告协议错误,不能持续 增长内存。
  • 音频缺口使用短静音或 Provider 编解码恢复,不重复上一段语音。
  • 切换输出设备、设备丢失或系统休眠时暂停提交时钟,避免把未播放文本标记为已听到。

14. 数据与持久化

14.1 默认保存

  • Voice Session ID、Conversation ID 和时间。
  • 无凭据的引擎快照及其摘要哈希。
  • 最终用户文本、已展示的助手消息、visual-only Block、实际播放边界和中断状态。
  • 中断、失败、取消和完成状态。
  • 有界延迟、音频中断和用量指标。
  • 工具与审批事件继续进入现有任务和活动记录。

14.2 默认不保存

  • 原始麦克风音频。
  • Provider 返回但尚未播放的音频。
  • 临时 ASR Delta。
  • 长期或临时 API Key、Cookie、会话 Token。
  • Provider 原始错误正文和可能包含用户内容的网络帧。
  • 回声参考信号、设备唯一标识和完整声学特征。

未来若提供录音留存,必须是独立、默认关闭的功能,说明保存位置、期限、大小、导出和删除, 并与“改进模型”授权分离。

14.3 崩溃恢复

应用启动时将未结束的 Voice Session 标记为 interrupted。恢复文本 Conversation, 但不自动重新打开麦克风、不自动连接 Provider,也不重播未完成音频。


15. 错误、重连与资源回收

场景 行为
麦克风权限拒绝 阻塞启动,保留引擎选择,提供系统权限说明
输入/输出设备消失 立即静音或暂停,要求用户处理设备,不改用未选择设备
本地模型缺失或损坏 阻塞启动,进入模型管理,不连接云端
本地 Runtime 启动失败 在有界预算内重启同一 Runtime,之后明确失败
云端认证或地域错误 明确失败,保留配置,不尝试其他 Provider/地域
短暂断网 同一引擎有界重连,超过 500 ms 显示状态
Provider 限流或余额不足 结束生成并显示原因,不切本地模型
Agent Runtime 失败 终止当前轮次,允许重试同一 Runtime,不换连接
TTS 失败 当前轮次失败,不静默改成系统 TTS 或仅文本成功
工具等待审批 暂停响应,保留会话;拒绝后把结果返回当前引擎
应用退出/窗口销毁 取消请求、停止 Track、关闭 Port/PeerConnection、终止 Sidecar
系统休眠/锁屏 停止采集和上传;恢复后要求用户显式继续

每个会话必须有最大时长、最大连续无声时间、最大媒体队列、最大临时文本、最大重连次数和 最大诊断大小。取消优先于重连和重试。


16. 安全与隐私

  1. 只允许可信主窗口主 Frame 创建和控制 Voice Session。
  2. 麦克风权限只放行音频,不因实时语音放开视频。
  3. 任何音频采集都需要用户操作;活动期间持续显示应用内状态和系统麦克风指示。
  4. 云端会话在开始前显示 Provider、地域、发送内容和可能费用。
  5. API Key 只在 Main 的加密设置或受控环境变量中使用。
  6. 临时 Provider 凭据具有最短可行期限、最小能力和单会话作用域。
  7. Provider 工具调用必须回到 Main 的白名单、Schema、Ask/Execute 和审批边界。
  8. 本地 Sidecar 只监听 loopback,使用短期认证,不开放外部端口。
  9. 模型权重按受信任目录、固定来源、大小和 SHA-256 校验,导入 ZIP 防止路径穿越和压缩炸弹。
  10. 日志只记录状态、耗时、错误分类和匿名引擎 ID,不记录语音正文和音频。
  11. 窗口隐藏时若会话仍活动,托盘必须持续显示麦克风状态和停止入口;首期可以选择隐藏即 暂停,但不能隐藏后无提示继续采集。
  12. Voice Session 不扩大项目、知识库、文件、浏览器或桌面控制范围。

17. 性能与质量指标

17.1 交互指标

指标 目标
用户开口到本地 VAD 检出 P95 ≤ 100 ms
Barge-in 检出到扬声器静音 P95 ≤ 150 ms
播放队列常态深度 100400 ms
用户轮次结束到临时文本稳定 P50 ≤ 300 ms
用户轮次结束到首段助手音频 云端/原生引擎 P50 ≤ 800 ms;模块化引擎 P50 ≤ 1,200 ms
已提交文本与实际播放偏差 ≤ 100 ms 或一个最小短语块
连续 30 分钟会话 无未界定内存增长、重复播放或资源泄漏

本地指标必须注明测试硬件,不能把高端 GPU 结果宣传为 CPU 基线。未达到所选引擎声明的 实时系数时,能力检测应标记为不满足实时要求,而不是静默切到更小模型。

17.2 质量指标

  • 中文普通话、英文和中英混合词的 ASR 错误率。
  • 长停顿、语气词、短回答和自我修正的轮次准确率。
  • 扬声器回声、键盘声、音乐和旁人说话下的误打断率。
  • 真正用户抢话的漏检率和停止延迟。
  • TTS 首段延迟、断句、数字、日期、英文缩写和代码术语可懂度。
  • 中断后下一轮上下文不包含未播放内容。
  • Provider、模型、数据位置和能力从不发生未声明变化。

18. 测试策略

18.1 自动化

  • Voice Contract Schema、大小边界和迁移测试。
  • 三组正交状态及非法状态组合测试。
  • 有序、乱序、重复、迟到和跨 Session 音频帧测试。
  • Barge-in 对播放、Provider、Agent、TTS 和持久化的取消传播测试。
  • 临时文本、已提交文本和播放位置对齐测试。
  • 同一引擎重连预算与超时测试。
  • “禁止静默降级”矩阵测试:任何 Adapter、Provider、模型、地域或 Runtime 变化都必须失败。
  • Ask 只读和 Execute 审批测试。
  • 窗口销毁、应用退出、休眠和设备丢失的资源释放测试。
  • 不持久化音频、临时 Token 和 Provider 原始正文的数据库测试。

18.2 模拟与声学测试

建立确定性 Fake Voice Engine,能够注入:

  • 固定节奏的输入、文本和音频。
  • 网络抖动、丢包、重复和断开。
  • 超前文本、迟到音频和错误播放位置。
  • 用户抢话、回声、短噪音和长停顿。
  • 工具调用、审批、拒绝和取消。

真实声学测试使用预录双声道夹具,一路作为助手扬声器参考,一路作为用户麦克风输入。 不能只通过静态单段 WAV 验证全双工。

18.3 手动与外部调用

  • 六个发布目标分别验证麦克风权限、采集、播放、设备拔插和应用退出。
  • 本地模型在声明的最低硬件上完成 30 分钟稳定性和实时系数测试。
  • 云端 Provider 测试会产生外部调用和费用,只在明确授权的 gated 测试中运行。
  • 每个云端 Adapter 至少验证一次真实音频输入、真实音频输出、打断和工具审批。
  • 真实测试失败时不使用配置握手成功替代生成验证。

19. 跨平台交付

19.1 基线

  • 本地模块化引擎作为 Windows、macOS、Linux x64/arm64 的统一功能基线。
  • 在线 ASR、VAD 和轻量 TTS 权重不内置,继续使用按需下载和 ZIP 离线迁移。
  • GoodBuddy 托管模型的下载遵守 平台功能页签与模型下载源设计,使用用户显式选择的 ModelScope 或 Hugging Face,失败时不切换来源。
  • 云端 Adapter 在六个平台复用同一契约,并分别验证 Electron WebRTC/WebSocket 行为。
  • 本地原生引擎按 Adapter 声明平台与硬件,不伪装成全平台能力。

19.2 硬件能力等级

等级 目标
CPU 基线 本地模块化 ASR/TTS;文本 Runtime 可以本地或云端
Apple Silicon 可增加 MLX/Metal 本地原生 Adapter,必须单独验证
NVIDIA GPU 可增加 CUDA 本地原生 Adapter,按显存和驱动验证
不满足要求 引擎卡片显示不可用与原因,不自动选择其他引擎

安装包继续保持轻量。大模型权重、CUDA Runtime 和独立 Python 环境不得无条件加入全部 发布包。


20. 分阶段实施

阶段 0:契约与模拟器

  • 新增 Voice Contracts、状态机和 Fake Voice Engine。
  • 建立禁止静默降级测试矩阵。
  • 建立会话快照、事件和诊断结构。

阶段 1Renderer 音频平面

  • AudioWorklet 采集与播放。
  • 媒体 MessagePort、背压和播放时钟。
  • 快速 VAD、回声关联、Barge-in 和设备生命周期。
  • 实时语音控制区和可访问状态。

阶段 2:本地模块化基线

  • 在线 ASR 和 VAD 模型管理。
  • Agent Runtime 流式文本桥。
  • 本地 TTS 模型管理、短语规划、音频回调和取消。
  • 最终文本持久化与工具审批。
  • 六个平台/架构验证。

阶段 3:首个云端原生 Adapter

  • Main-only 凭据和引擎 Profile。
  • WebRTC 或 WebSocket 会话。
  • 转写、音频、截断、用量和 Provider 错误。
  • sideband 工具与审批。
  • 真实有费用的 gated 验证。

阶段 4:本地原生全双工 Adapter

  • 选择一个中文质量、许可和硬件要求已验证的模型。
  • 建立受管 Sidecar、能力检测和真实会话测试。
  • 验证原生 Barge-in、文本提交、工具能力和长期稳定性。

阶段 5:扩展与质量

  • 增加经过验证的云端和本地 Adapter。
  • 输出设备选择和企业语音策略。
  • 声学基准、延迟仪表盘和成本诊断。
  • 评估是否允许用户预配置仍需确认的显式替代策略。

21. 首个垂直切片

首个可合并实现应使用 Fake Voice Engine,不立即绑定某个云端 Provider:

  1. 用户显式开始会话。
  2. AudioWorklet 持续采集和播放模拟流。
  3. Fake Engine 产生临时转写、助手文本和音频。
  4. 用户开口触发 150 ms 内静音和响应取消。
  5. 持久化已提交用户文本、已展示助手内容、visual-only Block 和实际播放边界;下一轮 模型上下文只使用已播放文本与完整显示的 visual-only Block。
  6. 模拟工具审批时暂停语音,拒绝口头授权。
  7. 注入 Adapter 失败后明确结束,不切换任何引擎。
  8. 关闭窗口后所有 Track、Port、计时器和模拟任务归零。

该切片先验证最难改变的会话、音频、提交和安全契约,再分别接入本地和云端实现。


22. 验收标准

  • 用户可以明确选择本地模块化、本地原生或云端原生引擎,界面持续显示当前选择。
  • 会话快照冻结 Provider、模型、地域、声音、数据位置和能力。
  • 任何引擎、Provider、模型、地域、Runtime 或模式变化都不能在测试中静默发生。
  • 助手播放期间继续采集麦克风,用户可在 P95 150 ms 内打断。
  • 中断后未播放音频与文本不进入下一轮上下文。
  • Ask 和 Execute 在语音中与文本中使用同一权限和审批边界。
  • 语音口令不能批准工具。
  • 云端长期凭据不进入 Renderer,本地 Sidecar 不监听外部地址。
  • 默认数据库、日志和 Artifact 中没有原始音频。
  • 本地引擎失败不连接云端,云端引擎失败不启动本地模型。
  • 重连只针对同一引擎快照,并在可感知时显示状态。
  • 六个平台目标完成各自声明能力的真实采集、播放、取消和资源回收验证。
  • npm testnpm run typechecknpm run lint 和生产构建全部通过。

23. 参考

  • sherpa-onnx:本地在线/离线 ASR、VAD 与 TTS。
  • OpenAI Realtime:云端实时音频会话与 WebRTC/WebSocket。
  • Gemini Live API:云端双向实时音频与多模态会话。
  • Qwen Realtime:云端实时音视频输入与音频/文本输出。
  • MiniCPM-o:本地端到端多模态与全双工候选。
  • Moshi:本地原生全双工语音模型框架。
  • PersonaPlex:本地可控角色与声音的全双工候选。
  • AudioWorklet:Renderer 低延迟音频处理基础。