Claude Code 源码分析第 27 课 · 第 06
第 27 课

语音系统

一键通、WebSocket STT、多后端音频捕获和 Claude Code 中的弹性模式。

1. 语音系统的作用

Claude Code 提供一键通语音输入管道,可将口语语音转换为提示文本。 按住一个键,说话,松开——文字记录准确地落在光标所在的提示输入中。 该系统受到两个独立守卫的控制:GrowthBook 功能标志(VOICE_MODE) 和 Anthropic OAuth 标记。 单独两者都不够。

高阶流量
  ┌──────────────┐  keypress   ┌───────────────────┐  PCM chunks   ┌──────────────────────┐
  │ useVoiceInt- │ ──────────► │   useVoice.ts      │ ────────────► │  voiceStreamSTT.ts   │
  │ egration.tsx │             │  (hold detection)  │               │  (WebSocket STT)     │
  └──────────────┘             └────────┬──────────┘               └──────────┬───────────┘
                                        │ startRecording()                     │ TranscriptText
                               ┌────────▼──────────┐               ┌──────────▼───────────┐
                               │   voice.ts         │               │ Anthropic voice_stream│
                               │  (audio backend)   │               │  /api/ws/speech_to_  │
                               │  NAPI / arecord    │               │   text/voice_stream  │
                               │       / SoX        │               └──────────────────────┘
                               └───────────────────┘
  

四个关键文件:

  • voice/voiceModeEnabled.ts — auth + 终止开关检查
  • services/voice.ts — 音频录制后端(NAPI、arecord、SoX)
  • services/voiceStreamSTT.ts — Anthropic 的 STT 端点的 WebSocket 客户端
  • hooks/useVoice.ts — React hook 连接音频 → WS → 文字记录
  • hooks/useVoiceIntegration.tsx — 提示输入集成、保持阈值、临时渲染

2. 功能门控和身份验证

voice/ voiceModeEnabled.ts
commands/voice/ voice.ts

三个功能构成了一个分层门:

isVoiceGrowthBookEnabled() — 终止开关
export function isVoiceGrowthBookEnabled(): boolean {
  // feature('VOICE_MODE') is a compile-time constant (Bun bundler)
  // Dead code is eliminated in non-ANT builds — keeps binary size down.
  return feature('VOICE_MODE')
    ? !getFeatureValue_CACHED_MAY_BE_STALE('tengu_amber_quartz_disabled', false)
    : false
}

成长书标志 tengu_amber_quartz_disabled 默认为 false (未禁用)。 丢失或陈旧的磁盘缓存读取为“未终止”——因此全新安装可以立即运行。 将旗帜翻转至 true 紧急情况 - 在 GrowthBook 的下一个缓存刷新周期内禁用所有用户的语音。

hasVoiceAuth() — OAuth 令牌检查
export function hasVoiceAuth(): boolean {
  if (!isAnthropicAuthEnabled()) return false
  const tokens = getClaudeAIOAuthTokens()
  return Boolean(tokens?.accessToken)
}

getClaudeAIOAuthTokens() spawns security 在 macOS 上(大约 20-50 毫秒冷,之后缓存)。 记忆会在令牌刷新时清除(大约每小时一次),因此预计每个会话都会出现一次冷生成。 API 密钥、Bedrock、Vertex 和 Foundry 均在此处返回 false — voice_stream 仅可通过 Claude.ai OAuth 获取。

/voice 命令飞行前检查

启用语音在写入之前运行五次连续检查 voiceEnabled: true 到设置:

  1. isVoiceModeEnabled() — 授权 + GB 门。
  2. checkRecordingAvailability() - 拒绝远程环境(CLAUDE_CODE_REMOTE、Homespace)立即; 然后探测音频后端链。
  3. isVoiceStreamAvailable() — 重新检查 OAuth 令牌的新鲜度。
  4. checkVoiceDependencies() — 验证至少一种录音工具可用。
  5. requestMicrophonePermission() — 现在触发 OS TCC 对话框 (macOS),而不是在第一次按键时触发。 提供针对特定平台的拒绝指南。
// Fires TCC dialog early — better than a surprise on first hold-to-talk
if (!(await requestMicrophonePermission())) {
  const guidance = process.platform === 'darwin'
    ? 'System Settings → Privacy & Security → Microphone'
    : 'your system\'s audio settings'
  return { type: 'text', value: `Microphone access denied. Go to ${guidance}.` }
}

3. 录音后端

services/ voice.ts

记录层呈现单一 startRecording(onData, onEnd, options?) 三个可能的后端的接口,按优先顺序尝试:

Backend Platforms Notes Status
audio-capture-napi macOS、Linux(存在 ALSA 卡)、Windows 通过 cpal + CoreAudio/AudioUnit 进行进程内。 dlopen 在 macOS 上阻止约 1 秒热,约 8 秒冷 — 仅在第一次语音按键时延迟加载。 Primary
arecord 仅限 Linux ALSA 用户空间实用程序。 通过 150 毫秒竞赛进行探测:如果仍然存在 = 设备打开正常。 通过 PulseAudio RDP 管道处理 WSL2+WSLg (Win11); 在 WSL1 / Win10-WSL2 上失败。 后备 1
SoX(推荐) macOS、Linux 外部工艺管道原始 PCM。 需要 --buffer 1024 以防止内部缓冲延迟。 内置静音检测; arecord 没有。 后备2
Windows(非母语) 仅限 Windows Windows 上没有子进程回退。 需要本机模块。 无后备方案
为什么 arecord 需要运行时探测,而不仅仅是 PATH 检查

在 WSL1 和无头 Linux 上, arecord 已安装但是 open() 由于没有 ALSA 卡和 PulseAudio 服务器,因此立即失败。 hasCommand('arecord') returns true 在所有这些情况下。 探头通过产卵来工作 arecord 使用与真实录制会话相同的参数并使用 150 毫秒的计时器:

const timer = setTimeout((child, resolve) => {
  child.kill('SIGTERM')
  resolve({ ok: true, stderr: '' })   // still alive = opened OK
}, 150, child, resolve)

child.once('close', code => {
  clearTimeout(timer)
  resolve({ ok: code === 0, stderr: stderr.trim() }) // exited early = failed
})

结果被记忆 - 音频设备可用性不会在会话中改变,并且这在每次语音按键时运行 checkRecordingAvailability().

SoX 争论细节和沉默检测
const args = [
  '-q',               // quiet: no progress output
  '--buffer', '1024', // flush in small chunks; without this SoX buffers seconds
  '-t', 'raw',         // raw PCM, no WAV header
  '-r', '16000',      // 16kHz sample rate (matches STT endpoint requirement)
  '-e', 'signed',     // signed PCM
  '-b', '16',         // 16-bit depth
  '-c', '1',          // mono
  '-',                // write to stdout
]
// Silence detection (only when NOT in push-to-talk mode)
if (useSilenceDetection) {
  args.push('silence', '1', '0.1', '3%', '1', '2.0', '3%')
  // ↑ stop after 2 seconds of audio below 3% threshold
}

一键通通行证 { silenceDetection: false } — 用户控制启动和停止。 本机 NAPI 模块也会忽略其内置的 onEnd 在一键通模式下。

4.WebSocket STT协议

services/ voiceStreamSTT.ts

STT 客户端连接到 wss://api.anthropic.com/api/ws/speech_to_text/voice_stream。 它使用与 Claude Code 的其余部分相同的 OAuth 不记名令牌。 URL 包含配置 STT 会话的查询参数:

const params = new URLSearchParams({
  encoding:        'linear16',   // 16-bit signed PCM
  sample_rate:     '16000',
  channels:        '1',          // mono
  endpointing_ms:  '300',        // endpoint detection window
  utterance_end_ms:'1000',
  language:        options?.language ?? 'en',
})

有线协议消息

KeepAlive
JSON 控制。 打开时立即发送(防止服务器在音频开始前关闭),然后每 8 秒发送一次。
二进制帧
来自录音后端的原始 PCM 音频块。 复制通过 Buffer.from() 以防止 NAPI 共享 ArrayBuffer 竞争。
CloseStream
JSON 控制。 发出音频结束信号。 发送至 setTimeout(0) 首先刷新任何排队的 NAPI 回调。
TranscriptText
临时转录块。 可能会被后续消息修改(更短或更长)。 发送给调用者为 isFinal=false.
TranscriptEndpoint
信号言语边界。 促销最后一个 TranscriptText to isFinal=true。 后 CloseStream, 解决 finalize() 快(~300 毫秒)。
TranscriptError
STT 错误(例如,不受支持的语言以代码 1008 结束)。 转接至呼叫者 onError.
为什么选择 api.anthropic.com 而不是 claude.ai

claude.ai Cloudflare 区域使用 TLS 指纹并阻止非浏览器客户端(JA3 指纹不匹配)。 这 api.anthropic.com 侦听器公开具有相同 OAuth 身份验证的相同 private-api pod,但位于不强制执行浏览器级 TLS 指纹识别的 CF 区域。 桌面听写仍然使用 claude.ai 因为 Swift 的 URLSession 具有浏览器级 JA3 指纹并通过了挑战。

// Override via env var for testing/staging
const wsBaseUrl =
  process.env.VOICE_STREAM_BASE_URL ||
  getOauthConfig().BASE_API_URL
    .replace('https://', 'wss://')
    .replace('http://', 'ws://')
Finalize() — 四个分辨率触发器

finalize() 返回一个 Promise<FinalizeSource> 通过四个路径中最先触发的一个来解决:

SourceCondition典型延迟
post_closestream_endpointCloseStream 发送后 TranscriptEndpoint 到达约 300 毫秒
no_data_timeoutCloseStream(1.5 秒)后没有到达 TranscriptText1.5秒
ws_closeWebSocket 关闭事件触发3-5秒
safety_timeout最后手段上限5秒

The no_data_timeout 路径是 无声签名 — 如果它触发 hadAudioSignal=true,会话遇到了一个已知的服务器错误(粘性 CE pod 返回零转录本,约 1% 的会话)。

5. 保持通话机制和保持阈值

hooks/ useVoiceIntegration.tsx

终端按键事件以流的形式到达:首次按下时发生一个事件,然后在按住时每 30-80 毫秒自动重复一次事件。 终端中没有“keyup”事件。 系统通过事件之间的时间间隔重建保持。

保持阈值问题: 像 Space 这样的裸字符绑定可以是普通键入的空格或保留的开始。 在激活语音之前要求快速连续按下 N 次,可以防止正常打字期间的意外触发。
// In useVoiceIntegration.tsx
const RAPID_KEY_GAP_MS = 120   // auto-repeat fires every 30-80ms; 120ms covers jitter
const HOLD_THRESHOLD = 5       // 5 rapid presses required before activating voice
const WARMUP_THRESHOLD = 2     // show "keep holding…" feedback at press #2

修饰符组合绑定(例如 Ctrl+Space)在第一次按下时激活 - 无需预热,因为修饰符组合显然是有意为之的。

stripTrailing() — 清理泄漏的空间

当用户按住 Space 进行热身时,一些空格字符会泄漏到之前的文本输入中 stopImmediatePropagation() 生效(不保证监听器注册顺序)。 stripTrailing() 精确删除泄漏计数,而不触及边界处预先存在的空间:

// Strip exactly `maxStrip` trailing `char` chars, leaving `floor` behind
const stripTrailing = (maxStrip, { char = ' ', anchor = false, floor = 0 } = {}) => {
  // Also counts full-width spaces (U+3000) for CJK IME compatibility
  const scan = char === ' '
    ? normalizeFullWidthSpace(beforeCursor)
    : beforeCursor
  // ...
  if (anchor) {
    voicePrefixRef.current = stripped         // save text before cursor
    voiceSuffixRef.current = afterCursor      // save text after cursor
  }
}

When anchor=true,该调用还捕获临时转录本注入的光标位置。 在前缀和后缀之间插入的间隙确保波形光标位于间隙上而不是第一个后缀字母上。

释放检测:RELEASE_TIMEOUT_MS 和 REPEAT_FALLBACK_MS
// In useVoice.ts
const RELEASE_TIMEOUT_MS = 200    // gap that signals key release (auto-repeat is 30-80ms)
const REPEAT_FALLBACK_MS = 600    // arm release timer if no auto-repeat seen yet
const FIRST_PRESS_FALLBACK_MS = 2000 // modifier combos: OS initial repeat delay up to ~2s

当 600 毫秒内没有第二次按键到达时,回退计时器将启动释放检测。 对于修饰符组合,调用者通过 2000 毫秒来覆盖较长的操作系统初始重复延迟(macOS 滑块位于“长”= 自动重复开始前约 2 秒)。

6. 会话状态机

每个记录会话都会经历由以下管理的三个状态 useVoice:

State
idle
没有活动录音。 WS 关闭。 提示输入正常。
State
recording
音频捕获。 PCM 流式传输到 WS。 提示中显示临时记录。
State
processing
钥匙已释放。 音频停止了。 等待 Finalize() 和最终成绩单。
关键实施细节: updateState('recording') 被称为 在任何等待之前同步 in startRecordingSession(). useVoiceIntegration reads voiceState 立即从商店购买 void startRecordingSession() 门泄漏的空间按键是否应该被吞掉。 如果首先运行等待,守卫会看到陈旧的 'idle' 并让空间泄漏。
完整的会话生命周期时间表
达到保持阈值(按#5)
带anchor=true的stripTrailing捕获前缀/后缀。 状态→同步录音。 sessionGenRef++。
checkRecordingAvailability() 等待
探测后端链(第一次调用后记忆)。
startRecording() + connectVoiceStream() — 并行
音频捕获立即开始。 PCM 块缓冲在 audioBuffer[] 中,直到 WS 打开。 在 WS 连接之前收集的关键术语(git 分支、最近的文件)。
onReady 触发(WS 打开)
audioBuffer 刷新到 WS。 后续块直接发送。 KeepAlive 开始(8 秒间隔)。
文字消息到达
光标位置提示中显示临时内容。 自动最终确定的非累积新分段(仅限旧版 Deepgram)。
按键释放(间隙 > 200 毫秒)
停止录音()。 状态→处理。 调用了finalize()。 在等待之前捕获的recordingDurationMs。
Finalize() 解决
通常通过 TranscriptEndpoint (~300 ms) 或 no_data_timeout (1.5 s)。
onTranscript(text) 调用
在光标锚点处注入最终转录本。 状态→空闲。

7.静默回放

hooks/ useVoice.ts

大约 1% 的会话遇到了服务器端错误(会话粘性 CE pod 接受音频但返回零记录)。 症状: finalize() 通过解决 no_data_timeout 尽管是真实的言语。 客户端检测到此模式并在新的 WebSocket 上重播完整的音频缓冲区一次。

Silent-drop检测条件和重放代码

所有六个条件必须满足才能触发重播:

  1. finalizeSource === 'no_data_timeout'
  2. hadAudioSignal === true (检测到重要的麦克风信号)
  3. wsConnected === true (WS确实打开——后端收到音频)
  4. !focusTriggered (不是焦点模式会话)
  5. accumulatedRef.current.trim() === '' (没有积累部分成绩单)
  6. !silentDropRetriedRef.current (每个会话仅重播一次)
if (finalizeSource === 'no_data_timeout' && hadAudioSignal && wsConnected
    && !focusTriggered && focusFlushedChars === 0
    && accumulatedRef.current.trim() === ''
    && !silentDropRetriedRef.current
    && fullAudioRef.current.length > 0) {
  silentDropRetriedRef.current = true
  await sleep(250)          // backoff to clear rapid-reconnect same-pod race
  if (isStale()) return

  // Replay full buffer in 32 KB slices on a fresh connection
  const SLICE = 32_000
  for (const chunk of replayBuffer) {
    // ... batch into SLICE-sized sends ...
    conn.send(Buffer.concat(slice))
  }
  await conn.finalize()
}

音频缓冲区是有界的: fullAudioRef.current 在焦点模式下跳过缓冲(会话可能持续几分钟,缓冲区可能达到约 20 MB)。 32 KB 切片大小将小 NAPI 块批处理为合理的 WS 帧大小,而不会超出 WS 消息限制。

8. 对焦模式

焦点模式是一种“多方军队”工作流程:当终端窗口获得焦点时开始录制,当失去焦点时停止录制。 转录块会立即刷新(而不是累积),因此长时间会话中的连续听写可以保持响应。

对焦模式与保持通话的区别
BehaviorHold-to-talk对焦模式
Trigger按键保持终端焦点增益
停止触发按键释放(间隙 > 200 毫秒)终端焦点丢失
成绩单交付累积,停止注入每一次决赛都会立即泛红; 锚先进
静音超时None5 秒(FOCUS_SILENCE_TIMEOUT_MS) — 拆除会话以释放 WS
无声重放Yes否(选通 !focusTriggered)
音频缓冲区保留完整缓冲区以供重播跳过(长时间训练中的自重)
// Arms / resets the silence timer after each flushed transcript
function armFocusSilenceTimer(): void {
  if (focusSilenceTimerRef.current) clearTimeout(focusSilenceTimerRef.current)
  focusSilenceTimerRef.current = setTimeout(() => {
    if (stateRef.current === 'recording' && focusTriggeredRef.current) {
      silenceTimedOutRef.current = true
      finishRecording()           // tears down WS gracefully
    }
  }, FOCUS_SILENCE_TIMEOUT_MS)   // 5000 ms
}

9. 语言规范化和关键术语

hooks/ useVoice.ts   services/ voiceKeyterms.ts

语言规范化

normalizeLanguageForSTT() 映射用户的 settings.language 字符串(可能是“日语”、“日本语”、“ja-JP”等)到硬编码白名单中的 BCP-47 代码,该白名单是服务器的子集 speech_to_text_voice_stream_config GrowthBook 许可名单。 发送不受支持的代码会关闭 WebSocket,并显示代码 1008“不支持的语言”。

// Falls back to 'en' with a fellBackFrom warning if language is unsupported
export function normalizeLanguageForSTT(language): { code: string, fellBackFrom?: string } {
  if (!language) return { code: 'en' }
  const lower = language.toLowerCase().trim()
  if (SUPPORTED_LANGUAGE_CODES.has(lower)) return { code: lower }
  const fromName = LANGUAGE_NAME_TO_CODE[lower]   // e.g. "japanese" → "ja"
  if (fromName) return { code: fromName }
  const base = lower.split('-')[0]              // "ja-JP" → "ja"
  if (SUPPORTED_LANGUAGE_CODES.has(base)) return { code: base }
  return { code: 'en', fellBackFrom: language }
}

关键术语(STT 提升)

The getVoiceKeyterms() 函数构建最多 50 个特定于域的术语的列表,发送为 keyterms 查询参数。 STT 后端应用增强功能,以便正确识别“MCP”、“OAuth”、“TypeScript”和特定于项目的词汇。

关键术语来源和标识符分割

关键词来自三个来源,并合并到一个去重复的集中:

  1. 全局硬编码术语:MCP、符号链接、grep、正则表达式、localhost、TypeScript、OAuth、webhook、gRPC、dotfiles、子代理、工作树。 (Claude 和 Anthropic 已经是服务器上的基本关键字。)
  2. 项目根基名: 例如, claude-code 作为一个整体添加。
  3. Git 分支词: feat/voice-keyterms → [“壮举”、“声音”、“关键术语”]。
export function splitIdentifier(name: string): string[] {
  return name
    .replace(/([a-z])([A-Z])/g, '$1 $2')  // camelCase → camel Case
    .split(/[-_./\s]+/)                    // split on separators
    .filter(w => w.length > 2 && w.length <= 20) // discard noise
}

10. 音频电平可视化和RMS计算

录音时,提示输入显示 16 条波形。 每个新的 PCM 块通过计算原始 16 位签名 PCM 缓冲区的 RMS 幅度来更新最右边的条。

const AUDIO_LEVEL_BARS = 16

export function computeLevel(chunk: Buffer): number {
  const samples = chunk.length >> 1   // 16-bit = 2 bytes per sample
  if (samples === 0) return 0
  let sumSq = 0
  for (let i = 0; i < chunk.length - 1; i += 2) {
    // Read 16-bit signed little-endian sample
    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)   // sqrt curve spreads quieter levels visually
}
sqrt 曲线是故意的。 线性标度将大部分语音能量压缩到视觉范围的前 20% - 除了响亮的峰值之外,波形看起来很平坦。 服用 sqrt(normalized) 将更安静的级别 (0.0–0.5) 分布在更多条上,使可视化响应正常的对话语音。

11.React上下文层

context/ voice.tsx   hooks/ useVoiceEnabled.ts

语音状态存储在自定义的 Store<VoiceState> (不是 React 状态)保存在上下文中。 这使得 useSyncExternalStore基于 - 的订阅,仅在选定切片更改时重新渲染。

export type VoiceState = {
  voiceState:             'idle' | 'recording' | 'processing'
  voiceError:             string | null
  voiceInterimTranscript: string   // live preview text shown in prompt
  voiceAudioLevels:       number[] // 16 bars, 0–1 normalized
  voiceWarmingUp:         boolean  // show "keep holding…" hint
}
useVoiceEnabled — 为什么 auth 在 authVersion 上被记忆
export function useVoiceEnabled(): boolean {
  const userIntent   = useAppState(s => s.settings.voiceEnabled === true)
  const authVersion  = useAppState(s => s.authVersion)
  // authVersion bumps on /login only.
  // getClaudeAIOAuthTokens() spawns `security` (~60ms cold) — can't call on every render.
  const authed = useMemo(hasVoiceAuth, [authVersion])
  return userIntent && authed && isVoiceGrowthBookEnabled()
}

The isVoiceGrowthBookEnabled() 调用保留在备忘录之外,因此会话中的终止开关翻转会在下一次渲染时生效,而无需等待登录事件。

12. 早期错误重试

CE 代理可以拒绝快速重新连接(~1/N_pods 相同 Pod 冲突),并且 Deepgram 的上游可能在其自己的拆卸窗口期间失败。 这些在任何成绩单到达之前都会表现为错误。 系统会以 250 毫秒的退避时间重试一次。

// Only retry if: not fatal (4xx), no transcript seen yet, still recording
if (!opts?.fatal && !sawTranscript && stateRef.current === 'recording') {
  if (!retryUsedRef.current) {
    retryUsedRef.current = true
    connectionRef.current = null       // null → audio re-buffers until new onReady
    attemptGenRef.current++             // stale conn's trailing close is ignored
    setTimeout(() => {
      if (stateRef.current === 'recording') attemptConnect(keyterms)
    }, 250)
    return
  }
}
// Fatal errors (4xx) surface the message to the user
致命错误与瞬时错误: 标记了 4xx HTTP 升级拒绝(Cloudflare 机器人挑战、身份验证拒绝) fatal: trueunexpected-response 处理程序。 致命错误永远不会重试——相同的请求将得到相同的拒绝。

要点

  1. 语音是双门控的。 成长手册 VOICE_MODE 需要功能标志(编译时死代码消除)和 Anthropic OAuth 标记。 终止开关默认为“未终止”,因此全新安装会立即生效。 API 密钥、Bedrock、Vertex 和 Foundry 被设计排除在外。
  2. 后端后备链与可用性检查链相匹配。 startRecording() and checkRecordingAvailability() 遵循相同的 NAPI → arecord → SoX 优先顺序。 已记下的 probeArecord() 结果确保,如果可用性检查失败到 SoX(损坏的记录),录音调用也会失败。
  3. 音频在 WebSocket 打开之前开始。 PCM 块缓冲在 audioBuffer[] until onReady 火灾。 这消除了从用户感知到的录制开始起的 1–2 秒 OAuth+WS 连接延迟。
  4. 在任何等待之前,状态同步转换为“记录”。 此转换之前的任何异步工作都会让保持检测代码看到陈旧的“空闲”,并允许自动重复关键字符泄漏到文本输入中。
  5. 保持阈值(快速按下 5 次)可防止意外激活裸字符绑定。 修饰符组合完全绕过了门槛。 stripTrailing() 清理泄漏的字符而不干扰光标边界处预先存在的内容。
  6. 无提示重放是针对服务器端错误的客户端解决方法。 完整的音频缓冲区是专门为这一一次性重放而保留的。 焦点模式会跳过缓冲,以避免长时间会话中的多 MB 累积。
  7. finalize() 有四个具有不同延迟的分辨率触发器。 快速路径(post_closestream_endpoint,~300 ms)是正常情况。 no_data_timeout (1.5 s)是无声跌落检测器。 始终捕捉 recordingDurationMs 之前 finalize() wait 可防止 WebSocket 拆卸时间夸大指标。
  8. 焦点模式在结构上与保持通话不同。 它禁用音频缓冲,用立即刷新代替累积,并使用 5 秒静音计时器而不是按键释放作为停止触发器。

测验

1. 为什么会这样 updateState('recording') 在任何之前同步运行 await in startRecordingSession()?
  • A 使动画帧立即更新
  • B Because useVoiceIntegration reads voiceState 之后同步 void startRecordingSession() 决定是否吞掉自动重复空格
  • C 防止释放计时器在录制开始之前触发
  • D 因为React的并发模式需要在异步边界之前更新状态
如果先运行任何等待, useVoiceIntegration的保持检测代码将变得陈旧 'idle' 并且无法吞掉自动重复空格,导致空格泄漏到提示输入中。
2. 在无头 Linux 上同时使用两者 arecord and sox 安装后,什么决定实际使用哪个后端?
  • A 以 PATH 中最先安装的为准
  • B 编译时常量
  • C 运行时 probeArecord() 结果 — 如果 arecord 无法打开设备(在 150 毫秒之前退出),则会落入 SoX
  • D 用户的 audioBackend 设置在 settings.json
hasCommand('arecord') 只检查路径。 在无头 Linux 上, arecord 存在但是 open() 如果没有 ALSA 卡,就会立即失败。 150 ms 竞赛检测到这一点:如果 arecord 在计时器触发之前退出, probe.ok = false 并用SoX代替。 该决定会在会议中被记住。
3. 什么是 no_data_timeout 分辨率来源 finalize() 旨在检测?
  • A 用户按住钥匙但没有说话
  • B WebSocket 升级被 Cloudflare 拒绝
  • C 会话粘性 CE pod 接受音频但返回零转录本(静默删除错误)
  • D OAuth 令牌在会话中过期
When no_data_timeout 火灾与 hadAudioSignal=true and wsConnected=true,这意味着音频到达后端,但没有返回任何文字记录——这是 ~1% 静默丢失错误的签名。 然后,客户端在新的 WebSocket 连接上重播完整的音频缓冲区。
4. 为什么会这样 connectVoiceStream() target api.anthropic.com 而不是 claude.ai?
  • A 更低延迟的路由
  • B claude.ai 上的不同身份验证方案
  • C claude.ai Cloudflare 区域会阻止非浏览器 TLS 指纹; api.anthropic.com 公开了相同的 pod,没有该限制
  • D voice_stream 端点在 claude.ai 上不可用
claude.ai 区域上的 Cloudflare TLS 指纹识别 (JA3) 挑战 Node.js / Bun WebSocket 连接。 api.anthropic.com 侦听器公开具有相同 OAuth Bearer auth 的相同 private-api pod,但位于不强制执行浏览器级指纹的 CF 区域。
5. 为什么音频缓冲区已满(fullAudioRef)在焦点模式下跳过?
  • A 焦点模式使用不支持重播的不同 STT 提供程序
  • B 焦点模式会话可能会持续几分钟,使得缓冲区可能超过 20 MB; 并且重播已开启 !focusTriggered anyway
  • C NAPI 后端不支持焦点模式下的缓冲
  • D Focus 模式总是在 CloseStream 之前获取 TranscriptEndpoint
在 32 KB/s PCM 下,10 分钟的焦点会话将累积约 20 MB。 由于重播是明确门控的 !focusTriggered,缓冲区在焦点模式下没有任何作用,并且浪费内存。
6. 当发生什么情况时 normalizeLanguageForSTT() 收到“斯瓦希里语”等不受支持的语言?
  • A 语音模式被禁用并且用户看到错误
  • B WebSocket 连接但关闭,代码为 1008
  • C 它回落到 'en' 和集 fellBackFrom: 'Swahili',这会在 /voice 切换响应
  • D 它将原始字符串“Swahili”发送到 STT 端点
该函数返回 { code: 'en', fellBackFrom: 'Swahili' }。 这 /voice 命令处理程序检查 stt.fellBackFrom 并在启用确认消息中附加一条注释,例如“斯瓦希里语不是受支持的听写语言;使用英语”。