语音系统

📑 目录

Claude Code 作为 Anthropic 推出的终端级 AI Agent 工具,除了传统的键盘输入方式外,还提供了一套完整的语音交互系统。本文将从源码层面深入解析其语音系统的整体架构、语音识别(STT)、语音合成(TTS)、语音模式切换以及性能优化策略。

一、语音系统架构概览

Claude Code 的语音系统采用分层架构设计,核心模块分布在 voice/services/voice/hooks/ 三个目录中,各司其职又紧密协作。

flowchart TD
    subgraph UI["UI 层"]
        A[useVoiceEnabled.ts] --> B[useVoice.ts]
        B --> C[voiceModeEnabled.ts]
    end
    subgraph Service["服务层"]
        B --> D[voice.ts]
        B --> E[voiceStreamSTT.ts]
    end
    subgraph Backend["后端"]
        E --> F[Anthropic voice_stream
WebSocket Endpoint] F --> G[Deepgram Nova 3 STT] end D --> H[本地音频捕获
macOS/Linux/Windows]

各模块职责划分:

模块文件路径大小职责
语音开关voice/voiceModeEnabled.ts~3KBGrowthBook 功能开关 + OAuth 认证校验
语音 Hookhooks/useVoice.ts~45KB按住说话(hold-to-talk)交互逻辑、焦点模式、音频可视化
语音使能 Hookhooks/useVoiceEnabled.ts~2KB用户设置 + 认证 + 功能开关的组合判断
音频录制services/voice.ts~17KB本地音频捕获、原生模块加载、降级方案
语音流转写services/voiceStreamSTT.ts~21KBWebSocket STT 客户端、协议解析、连接管理

从架构上看,useVoice.ts 是整个语音系统的 orchestrator(编排器),它协调本地录音与远程 STT 服务之间的数据流;voice.ts 负责与操作系统音频子系统交互;voiceStreamSTT.ts 则作为 Anthropic 语音流服务的客户端代理。

二、语音功能开关与认证体系

语音功能并非无条件可用,Claude Code 设计了一套多层 gate( gate 机制)来控制语音模式的启用状态。

2.1 GrowthBook 功能开关

voice/voiceModeEnabled.ts 第 14-22 行实现了 GrowthBook kill-switch 检查:

export function isVoiceGrowthBookEnabled(): boolean {
  return feature('VOICE_MODE')
    ? !getFeatureValue_CACHED_MAY_BE_STALE('tengu_amber_quartz_disabled', false)
    : false
}

这里的逻辑非常精巧:默认返回 false(未禁用),意味着即使 GrowthBook 缓存尚未初始化或磁盘缓存丢失,新安装的客户端也能立即使用语音功能,无需等待 GrowthBook 初始化完成。这种设计避免了"功能已就绪但因配置服务未加载而被禁用"的负面体验。

2.2 OAuth 认证校验

语音服务依赖 Anthropic 的 voice_stream 端点,该端点仅对 Claude.ai 的 OAuth 用户开放,不支持 API Key、Bedrock、Vertex 或 Foundry 等认证方式。voiceModeEnabled.ts 第 28-40 行的 hasVoiceAuth() 函数负责校验:

export function hasVoiceAuth(): boolean {
  if (!isAnthropicAuthEnabled()) {
    return false
  }
  const tokens = getClaudeAIOAuthTokens()
  return Boolean(tokens?.accessToken)
}

getClaudeAIOAuthTokens 是一个 memoized 函数,首次调用会触发 macOS security 命令行工具读取钥匙串(耗时约 20-50ms),后续调用均为缓存命中。memoize 在 token 刷新时(约每小时一次)自动清除缓存,因此每个刷新周期内仅有一次冷启动开销。

2.3 React 层的组合判断

hooks/useVoiceEnabled.ts 第 15-26 行将用户意图、认证状态和功能开关组合为最终的使能状态:

export function useVoiceEnabled(): boolean {
  const userIntent = useAppState(s => s.settings.voiceEnabled === true)
  const authVersion = useAppState(s => s.authVersion)
  const authed = useMemo(hasVoiceAuth, [authVersion])
  return userIntent && authed && isVoiceGrowthBookEnabled()
}

这里有一个性能优化细节:只有 authVersion 变化时才重新计算 hasVoiceAuth(),而 GrowthBook 的廉价缓存查询(cached-map lookup)保持在 memo 外部,确保 mid-session 的 kill-switch 切换能在下一次渲染时立即生效。

三、语音识别(STT):voiceStreamSTT.ts

services/voiceStreamSTT.ts 是整个语音系统中技术含量最高的模块之一,它实现了与 Anthropic voice_stream WebSocket 端点的完整交互协议。

3.1 WebSocket 连接建立

connectVoiceStream() 函数(第 76-185 行)负责建立 WebSocket 连接。源码中有一个值得注意的设计决策:为了避免 Cloudflare 的 TLS fingerprinting 拦截,Claude Code 选择连接 api.anthropic.com 而非 claude.ai

const wsBaseUrl =
  process.env.VOICE_STREAM_BASE_URL ||
  getOauthConfig()
    .BASE_API_URL.replace('https://', 'wss://')
    .replace('http://', 'ws://')

桌面端(Swift 版)仍使用 claude.ai,因为 URLSession 具有浏览器级别的 JA3 fingerprint,能通过 Cloudflare 的检测;而 CLI 客户端的 TLS fingerprint 会被识别为非浏览器,从而遭到挑战或拦截。

3.2 查询参数与 Deepgram Nova 3

连接 URL 携带了丰富的音频参数(第 137-155 行):

const params = new URLSearchParams({
  encoding: 'linear16',
  sample_rate: '16000',
  channels: '1',
  endpointing_ms: '300',
  utterance_end_ms: '1000',
  language: options?.language ?? 'en',
})

其中 linear16 表示 16-bit 线性 PCM,16000 Hz 单声道是 STT 模型的标准输入格式。此外,GrowthBook 的 tengu_cobalt_frost flag 控制是否启用 Deepgram Nova 3 模型:

const isNova3 = getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_frost', false)
if (isNova3) {
  params.set('use_conversation_engine', 'true')
  params.set('stt_provider', 'deepgram-nova3')
}

Nova 3 模型通过 conversation_engine 路由,允许客户端独立于服务端 GrowthBook 门控进行灰度发布。

3.3 语音流协议

voice_stream 的 wire protocol 采用 JSON 控制消息 + 二进制音频帧的混合模式:

sequenceDiagram
    participant C as Claude Code Client
    participant S as voice_stream Server
    C->>S: WebSocket Upgrade + Bearer Token
    S-->>C: open
    C->>S: {"type":"KeepAlive"}
    loop 录音期间
        C->>S: 
        S-->>C: {"type":"TranscriptText", "data":"hello"}
        S-->>C: {"type":"TranscriptText", "data":"hello world"}
    end
    C->>S: {"type":"CloseStream"}
    S-->>C: {"type":"TranscriptEndpoint"}
    S-->>C: close

消息类型包括:

  • KeepAlive:客户端每 8 秒发送一次,防止服务器 idle timeout(voiceStreamSTT.ts 第 23 行)
  • CloseStream:录音结束时发送,通知服务器停止接收音频并开始最终转写
  • TranscriptText:服务器返回的中间或最终结果文本
  • TranscriptEndpoint:表示一个 utterance(话语片段)的结束
  • TranscriptError:转写错误

3.4 finalize() 与超时策略

录音停止后,客户端需要等待服务器返回最终转写结果。finalize() 方法(第 230-280 行)设计了多层超时机制:

export const FINALIZE_TIMEOUTS_MS = {
  safety: 5_000,    // 最后保险:WebSocket 挂起时的最大等待
  noData: 1_500,    // CloseStream 后无数据到达时的快速退出
}

finalize() 的四种 resolution source 定义如下:

Source说明
post_closestream_endpointCloseStream 后收到 TranscriptEndpoint,正常完成
no_data_timeoutCloseStream 后 1.5 秒内无任何消息,静默丢弃
safety_timeout5 秒安全超时,兜底机制
ws_already_closedWebSocket 已关闭,无需等待

3.5 静默丢弃重试(Silent-Drop Replay)

useVoice.ts 第 130-135 行揭示了一个重要的鲁棒性设计:约 1% 的会话会遇到"sticky-broken CE pod"问题——服务器接收了音频但返回零转写。当 finalize()no_data_timeout 解决且检测到 hadAudioSignal=true 时,系统会自动将缓存的音频(上限约 2MB,对应 60 秒录音)在全新 WebSocket 连接上重放一次:

if (
  finalizeSource === 'no_data_timeout' &&
  hadAudioSignal &&
  wsConnected &&
  !silentDropRetriedRef.current &&
  fullAudioRef.current.length > 0
) {
  silentDropRetriedRef.current = true
  // ...replay on fresh connection
}

这一机制对用户完全透明,却显著提升了语音输入的可靠性。

四、音频录制:services/voice.ts

services/voice.ts 负责本地音频捕获,是连接操作系统音频子系统的桥梁。

4.1 原生音频模块与懒加载

Claude Code 使用 audio-capture-napi 作为原生音频模块,底层基于 Rust 的 cpal 库,支持 macOS(CoreAudio)、Linux(ALSA/PulseAudio)和 Windows。模块采用严格懒加载策略(第 20-33 行):

function loadAudioNapi(): Promise<AudioNapi> {
  audioNapiPromise ??= (async () => {
    const t0 = Date.now()
    const mod = await import('audio-capture-napi')
    mod.isNativeAudioAvailable()
    audioNapi = mod
    logForDebugging(`[voice] audio-capture-napi loaded in ${Date.now() - t0}ms`)
    return mod
  })()
  return audioNapiPromise
}

延迟加载的原因在于 dlopen 是同步阻塞操作:在 macOS 上可能阻塞事件循环 1 秒(热启动)到 8 秒(冷启动,如系统唤醒后)。如果预加载,会导致整个 CLI 启动卡顿,因此设计为首次按下语音键时才加载。

4.2 多平台降级策略

当原生模块不可用时,Claude Code 实现了完整的降级链(第 220-290 行):

平台优先级 1优先级 2优先级 3
macOScpal (CoreAudio)
Linuxcpal (ALSA)arecord (ALSA utils)SoX rec
Windowscpal (WASAPI)

对于 Linux,还有一个特殊处理:通过读取 /proc/asound/cards 检测 ALSA 声卡是否存在(第 130-145 行)。在 WSL1、Win10-WSL2 或 headless Linux 环境中,声卡文件为空或包含 "no soundcards",此时直接跳过 cpal 尝试,避免其向 stderr 打印无法捕获的错误信息。

4.3 麦克风权限探测

requestMicrophonePermission() 函数(第 235-250 行)通过实际录制一段音频并立即丢弃的方式探测权限,而非依赖不可靠的 TCC(Transparency, Consent, and Control)状态 API。这对于 ad-hoc 签名或跨架构二进制文件(如 x64-on-arm64)尤为重要。

4.4 录音参数

音频录制统一使用以下参数(第 35-40 行):

const RECORDING_SAMPLE_RATE = 16000
const RECORDING_CHANNELS = 1

16kHz 单声道 16-bit PCM 是语音识别的最佳输入格式,既保证了足够的话音频段信息(8kHz 即可覆盖语音频率),又控制了数据量(约 32KB/秒)。

五、语音模式:useVoice.ts

hooks/useVoice.ts 是语音交互的指挥中心,实现了两种交互模式:按住说话(Hold-to-Talk)焦点模式(Focus Mode)

5.1 按住说话模式

这是默认的交互方式:用户按住语音快捷键开始录音,松开即停止并提交转写。源码通过终端的 auto-repeat key 事件来检测"按住"状态(第 105-115 行):

const RELEASE_TIMEOUT_MS = 200
const REPEAT_FALLBACK_MS = 600
export const FIRST_PRESS_FALLBACK_MS = 2000
  • 终端 auto-repeat 通常每 30-80ms 触发一次
  • RELEASE_TIMEOUT_MS = 200ms:两次 key 事件间隔超过 200ms 视为释放
  • REPEAT_FALLBACK_MS = 600ms:如果用户短按(未触发 auto-repeat),600ms 后备启动释放检测
  • FIRST_PRESS_FALLBACK_MS = 2000ms:修饰键组合(modifier-combo)的首次按下,考虑到 macOS 最长 2 秒的初始 repeat delay

5.2 焦点模式

焦点模式面向"多 Claude 军团"工作流:当终端获得焦点时自动开始录音,失去焦点时停止(useVoice.ts 第 380-420 行)。此模式下,每个 final transcript 会立即注入到输入框中并继续录音,而非等待用户释放按键。

为了防止环境噪音导致无限录音,焦点模式引入了 5 秒静默超时(FOCUS_SILENCE_TIMEOUT_MS = 5_000)。当 5 秒内未检测到语音时,会话自动销毁;用户下一次按键可重新激活。

5.3 音频可视化

computeLevel() 函数(第 125-140 行)从 16-bit PCM 缓冲区计算 RMS(均方根)振幅并归一化为 0-1:

export function computeLevel(chunk: Buffer): number {
  const samples = chunk.length >> 1
  if (samples === 0) return 0
  let sumSq = 0
  for (let i = 0; i < chunk.length - 1; i += 2) {
    const sample = ((chunk[i]! | (chunk[i + 1]! << 8)) << 16) >> 16
    sumSq += sample * sample
  }
  const rms = Math.sqrt(sumSq / samples)
  const normalized = Math.min(rms / 2000, 1)
  return Math.sqrt(normalized)
}

这里采用 Math.sqrt(normalized) 曲线将较安静的电平展宽到更大的视觉范围内,使波形可视化能充分利用全部 16 个条形(AUDIO_LEVEL_BARS = 16)。

5.4 音频缓冲与 WebSocket 并行化

为了消除 WebSocket 连接建立(OAuth 刷新 + TLS 握手)带来的 1-2 秒延迟,startRecordingSession() 采用先录音、后连接策略(第 490-510 行):

const audioBuffer: Buffer[] = []
const started = await voiceModule.startRecording(
  (chunk: Buffer) => {
    const owned = Buffer.from(chunk)
    if (connectionRef.current) {
      connectionRef.current.send(owned)
    } else {
      audioBuffer.push(owned)
    }
  },
  // ...
)

音频先缓存在 audioBuffer 中,待 WebSocket onReady 触发后,将缓冲的音频合并切片发送(每片约 32KB,对应 1 秒音频),而非逐 chunk 发送。这减少了 WebSocket 帧数量,降低了双方的处理开销。

六、语言支持与规范化

useVoice.ts 第 30-100 行实现了语言名称到 BCP-47 代码的映射,支持 20 种语言:

const LANGUAGE_NAME_TO_CODE: Record<string, string> = {
  english: 'en', spanish: 'es', french: 'fr', japanese: 'ja',
  german: 'de', portuguese: 'pt', italian: 'it', korean: 'ko',
  hindi: 'hi', indonesian: 'id', russian: 'ru', polish: 'pl',
  turkish: 'tr', dutch: 'nl', ukrainian: 'uk', greek: 'el',
  czech: 'cs', danish: 'da', swedish: 'sv', norwegian: 'no',
}

normalizeLanguageForSTT() 函数执行四级回退:精确匹配 → 名称映射 → 基础子标签(如 zh-CNzh)→ 默认 en。关键约束是客户端发送的代码必须是服务端 GrowthBook speech_to_text_voice_stream_config 允许列表的子集,否则 WebSocket 会以 1008 "Unsupported language" 关闭连接。

七、性能与优化策略

7.1 音频缓冲策略

  • 录音缓冲:WebSocket 连接期间音频缓存在内存中,上限约 2MB(60 秒 @ 32KB/s)
  • 焦点模式优化:不缓存 full audio,避免 10 分钟会话产生 ~20MB 死数据
  • 切片发送:缓冲音频合并为 ~1 秒切片发送,减少 WS 帧数量

7.2 延迟控制

环节延迟来源优化手段
模块加载dlopen 阻塞 1-8s懒加载至首次按键
OAuth 刷新钥匙串读取 20-50msmemoize,每小时仅一次
WS 连接TLS + 握手 1-2s先录音后连接,音频缓冲
转写往返服务器处理 300ms-5snoData 超时 1.5s 快速退出

7.3 网络优化

  • KeepAlive:每 8 秒发送心跳,防止 NAT/代理超时
  • TLS 代理支持:通过 getWebSocketProxyAgent()getWebSocketProxyUrl() 支持企业代理环境
  • 重试机制:连接早期错误(pre-transcript)自动重试一次,250ms 退避避免同 pod 碰撞
  • sessionGen 防僵尸:每个会话递增 generation,防止慢连接的 WebSocket 回调污染新会话

八、总结

Claude Code 的语音系统是一个在可靠性、性能和用户体验之间精心平衡的工程范例:

  1. 多层 gate 设计:GrowthBook kill-switch + OAuth 认证 + 用户设置,既保证功能可控又避免负面体验
  2. 跨平台音频捕获:原生 cpal 模块 + arecord + SoX 的三级降级链,覆盖 macOS/Linux/Windows
  3. 智能缓冲策略:先录音后连接消除了 WS 握手延迟,切片发送降低协议开销
  4. 静默丢弃重试:自动检测并重放服务器端的偶发故障,用户无感知
  5. 焦点模式创新:让语音输入跟随终端焦点,支持多实例协作场景

从技术实现来看,这套系统的复杂度远超表面上的"按住说话转文字"。它在事件循环阻塞、内存管理、网络容错、跨平台兼容性等方面都做了大量工程投入,值得在构建同类 AI Agent 语音交互时参考借鉴。