11# VOICE_MODE — 语音输入
22
33> Feature Flag: ` FEATURE_VOICE_MODE=1 `
4- > 实现状态:完整可用(需要 Anthropic OAuth)
4+ > 实现状态:完整可用(双后端: Anthropic OAuth / 豆包 ASR )
55> 引用数:46
66
77## 一、功能概述
88
9- VOICE_MODE 实现"按键说话"(Push-to-Talk)语音输入。用户按住空格键录音,音频通过 WebSocket 流式传输到 Anthropic STT 端点(Nova 3),实时转录显示在终端中。
9+ VOICE_MODE 实现"按键说话"(Push-to-Talk)语音输入。用户按住空格键录音,音频流式传输到 STT 后端,实时转录显示在终端中。支持两个后端:
10+
11+ - ** Anthropic STT(默认)** :通过 WebSocket 流式传输到 Nova 3 端点,需要 Anthropic OAuth
12+ - ** 豆包 ASR(Doubao)** :通过 ` doubaoime-asr ` 包的 AsyncGenerator 协议流式识别,使用独立凭证文件,无需 Anthropic OAuth
1013
1114### 核心特性
1215
1316- ** Push-to-Talk** :长按空格键录音,释放后自动发送
1417- ** 流式转录** :录音过程中实时显示中间转录结果
1518- ** 无缝集成** :转录文本直接作为用户消息提交到对话
19+ - ** 双后端切换** :通过 ` /voice ` 命令参数选择 STT 后端,持久化到 settings.json
1620
1721## 二、用户交互
1822
1923| 操作 | 行为 |
2024| ------| ------|
2125| 长按空格 | 开始录音,显示录音状态 |
22- | 释放空格 | 停止录音,等待最终转录 |
23- | 转录完成 | 自动插入到输入框并提交 |
24- | ` /voice ` 命令 | 切换语音模式开关 |
26+ | 释放空格 | 停止录音,转录结果自动提交 |
27+ | ` /voice ` | 切换语音模式开关(默认使用 Anthropic 后端) |
28+ | ` /voice doubao ` | 启用语音模式并使用豆包 ASR 后端 |
29+ | ` /voice anthropic ` | 切换回 Anthropic STT 后端 |
2530
2631### UI 反馈
2732
@@ -35,26 +40,37 @@ VOICE_MODE 实现"按键说话"(Push-to-Talk)语音输入。用户按住空
3540
3641文件:` src/voice/voiceModeEnabled.ts `
3742
38- 三层检查 :
43+ 两层检查函数 :
3944
4045``` ts
46+ // Anthropic 后端(需要 OAuth)
4147isVoiceModeEnabled () = hasVoiceAuth () && isVoiceGrowthBookEnabled ()
48+
49+ // 豆包后端 / 通用可用性检查(不需要 OAuth)
50+ isVoiceAvailable () = isVoiceGrowthBookEnabled ()
4251```
4352
44531 . ** Feature Flag** :` feature('VOICE_MODE') ` — 编译时/运行时开关
45542 . ** GrowthBook Kill-Switch** :` !getFeatureValue_CACHED_MAY_BE_STALE('tengu_amber_quartz_disabled', false) ` — 紧急关闭开关(默认 false = 未禁用)
46- 3 . ** Auth 检查** :` hasVoiceAuth() ` — 需要 Anthropic OAuth token(非 API key)
55+ 3 . ** Auth 检查(仅 Anthropic)** :` hasVoiceAuth() ` — 需要 Anthropic OAuth token(非 API key)
56+ 4 . ** Provider 检查** :` voiceProvider ` 设置决定使用哪个后端,豆包后端跳过 OAuth 检查
4757
4858### 3.2 核心模块
4959
5060| 模块 | 职责 |
5161| ------| ------|
5262| ` src/voice/voiceModeEnabled.ts ` | Feature flag + GrowthBook + Auth 三层门控 |
53- | ` src/hooks/useVoice.ts ` | React hook 管理录音状态和 WebSocket 连接 |
54- | ` src/services/voiceStreamSTT.ts ` | WebSocket 流式传输到 Anthropic STT |
63+ | ` src/hooks/useVoice.ts ` | React hook 管理录音状态和后端连接 |
64+ | ` src/services/voiceStreamSTT.ts ` | Anthropic WebSocket 流式 STT |
65+ | ` src/services/doubaoSTT.ts ` | 豆包 ASR 适配器(AsyncGenerator → VoiceStreamConnection) |
66+ | ` src/commands/voice/voice.ts ` | ` /voice ` 命令实现,处理后端选择和持久化 |
67+ | ` src/hooks/useVoiceEnabled.ts ` | 语音启用状态 hook,根据 provider 决定是否跳过 OAuth |
68+ | ` src/utils/settings/types.ts ` | `voiceProvider: 'anthropic' | 'doubao'` 设置类型定义 |
5569
5670### 3.3 数据流
5771
72+ #### Anthropic 后端
73+
5874```
5975用户按下空格键
6076 │
@@ -79,47 +95,169 @@ WebSocket 连接到 Anthropic STT 端点
7995转录文本 → 插入输入框 → 自动提交
8096```
8197
98+ #### 豆包 ASR 后端
99+
100+ ```
101+ 用户按下空格键
102+ │
103+ ▼
104+ useVoice hook 激活(检测到 voiceProvider === 'doubao')
105+ │
106+ ▼
107+ macOS 原生音频 / SoX 开始录音
108+ │
109+ ▼
110+ connectDoubaoStream() 创建 AudioChunkQueue + VoiceStreamConnection
111+ │
112+ ├──→ onReady 立即触发(无需等待握手)
113+ │
114+ ▼
115+ 音频数据通过 AudioChunkQueue 传入 transcribeRealtime()
116+ │
117+ ├──→ INTERIM_RESULT → 实时显示中间转录
118+ ├──→ FINAL_RESULT → 显示最终转录
119+ │
120+ ▼
121+ 用户释放空格键
122+ │
123+ ▼
124+ finalize() 立即返回(豆包在录音过程中已返回结果,无需等待)
125+ │
126+ ▼
127+ 转录文本 → 插入输入框 → 自动提交
128+ ```
129+
82130### 3.4 音频录制
83131
84- 支持两种音频后端:
132+ 支持两种音频后端(两个 STT 后端共享) :
85133- ** macOS 原生音频** :优先使用,低延迟
86134- ** SoX(Sound eXchange)** :回退方案,跨平台
87135
88- 音频流通过 WebSocket 发送到 Anthropic 的 Nova 3 STT 模型。
136+ ### 3.5 豆包 ASR 适配器设计
137+
138+ 文件:` src/services/doubaoSTT.ts `
139+
140+ 豆包后端使用适配器模式,将 ` doubaoime-asr ` 的 AsyncGenerator 协议桥接到 ` VoiceStreamConnection ` 接口:
141+
142+ ** AudioChunkQueue** — push 式异步队列:
143+ - 实现 ` AsyncIterable<Uint8Array> ` 接口
144+ - ` push(chunk) ` 将音频数据入队,` push(null) ` 发送结束信号
145+ - 内部维护等待者(waiting)和缓冲队列(chunks)两个状态
146+
147+ ** connectDoubaoStream()** — 连接入口:
148+ - 动态导入 ` doubaoime-asr ` (optionalDependencies)
149+ - 从 ` ~/.claude/tts/doubao/credentials.json ` 加载凭证
150+ - 创建 AudioChunkQueue 和 VoiceStreamConnection
151+ - 立即触发 ` onReady ` (避免与 useVoice 的音频缓冲死锁)
152+ - ` finalize() ` 立即返回(豆包在录音过程中已返回结果)
153+ - 后台 async IIFE 消费 ` transcribeRealtime ` generator,映射响应类型到回调
154+
155+ ** 响应类型映射** :
156+
157+ | doubaoime-asr ResponseType | 回调映射 |
158+ | ----------------------------| ----------|
159+ | SESSION_STARTED | 日志记录 |
160+ | VAD_START | 日志记录 |
161+ | INTERIM_RESULT | ` onTranscript(text, false) ` |
162+ | FINAL_RESULT | ` onTranscript(text, true) ` |
163+ | ERROR | ` onError(errorMsg) ` |
164+ | SESSION_FINISHED | 日志记录 |
165+
166+ ### 3.6 后端选择逻辑
167+
168+ 文件:` src/hooks/useVoice.ts `
169+
170+ ``` ts
171+ // 判断当前 provider
172+ isDoubaoProvider () → 读取 settings .voiceProvider
173+
174+ // handleKeyEvent 中的可用性检查
175+ const sttAvailable = isDoubaoProvider ()
176+ ? isDoubaoAvailableSync () // 乐观检查(首次返回 true)
177+ : isVoiceStreamAvailable () // Anthropic WebSocket 检查
178+
179+ // attemptConnect 中的连接函数选择
180+ const connectFn = isDoubaoProvider ()
181+ ? connectDoubaoStream
182+ : connectVoiceStream
183+ ```
184+
185+ 豆包后端的特殊处理:
186+ - 跳过 ` getVoiceKeyterms() ` 调用(豆包无需关键词提示)
187+ - 跳过 Focus Mode(` if (!enabled || !focusMode || isDoubaoProvider()) ` )
89188
90189## 四、关键设计决策
91190
92- 1 . ** OAuth 独占** :语音模式使用 ` voice_stream ` 端点(claude.ai),仅 Anthropic OAuth 用户可用。API key、Bedrock、Vertex 用户无法使用
93- 2 . ** GrowthBook 负向门控** :` tengu_amber_quartz_disabled ` 默认 ` false ` ,新安装自动可用(无需等 GrowthBook 初始化)
94- 3 . ** Keychain 缓存** :` getClaudeAIOAuthTokens() ` 首次调用访问 macOS keychain(~ 20-50ms),后续缓存命中
95- 4 . ** 独立于主 feature flag** :` isVoiceGrowthBookEnabled() ` 在 feature flag 关闭时短路返回 ` false ` ,不触发任何模块加载
191+ 1 . ** 双后端共存** :豆包后端作为独立适配器与 Anthropic 后端并存,不替换原有流程,通过 ` voiceProvider ` 设置切换
192+ 2 . ** 设置持久化** :` voiceProvider ` 存储在 ` settings.json ` ,通过 ` /voice ` 命令修改,跨会话生效
193+ 3 . ** OAuth 独占(Anthropic)** :Anthropic 后端使用 ` voice_stream ` 端点(claude.ai),仅 OAuth 用户可用
194+ 4 . ** 豆包无需 OAuth** :豆包后端使用独立凭证文件,不依赖 Anthropic 认证,通过 ` isVoiceAvailable() ` 放宽门控
195+ 5 . ** GrowthBook 负向门控** :` tengu_amber_quartz_disabled ` 默认 ` false ` ,新安装自动可用
196+ 6 . ** onReady 立即触发** :豆包后端在连接建立后立即触发 ` onReady ` ,避免与 useVoice 音频缓冲的时序死锁(Anthropic 需要等待 WebSocket 握手)
197+ 7 . ** finalize() 立即返回** :豆包在录音过程中已返回所有结果,用户抬手时无需等待处理
198+ 8 . ** 乐观可用性检查** :` isDoubaoAvailableSync() ` 在首次调用时返回 ` true ` ,实际导入错误在 ` connectDoubaoStream ` 中处理
199+ 9 . ** optionalDependencies** :` doubaoime-asr ` 作为可选依赖,安装失败不影响 Anthropic 后端
96200
97201## 五、使用方式
98202
99203``` bash
100204# 启用 feature
101205FEATURE_VOICE_MODE=1 bun run dev
102206
103- # 在 REPL 中使用
207+ # 在 REPL 中使用 Anthropic 后端
104208# 1. 确保已通过 OAuth 登录(claude.ai 订阅)
105- # 2. 按住空格键说话
106- # 3. 释放空格键等待转录
107- # 4. 或使用 /voice 命令切换开关
209+ # 2. 输入 /voice 启用
210+ # 3. 按住空格键说话
211+ # 4. 释放空格键等待转录
212+
213+ # 在 REPL 中使用豆包 ASR 后端
214+ # 1. 确保 doubaoime-asr 已安装(bun add doubaoime-asr)
215+ # 2. 配置凭证文件:~/.claude/tts/doubao/credentials.json
216+ # 3. 输入 /voice doubao 启用
217+ # 4. 按住空格键说话
218+ # 5. 释放空格键,转录结果即刻显示
219+
220+ # 切换后端
221+ /voice doubao # 切换到豆包 ASR
222+ /voice anthropic # 切换回 Anthropic STT
223+ /voice # 关闭语音模式
224+ ```
225+
226+ ### 豆包凭证配置
227+
228+ 凭证文件路径:` ~/.claude/tts/doubao/credentials.json `
229+
230+ ``` json
231+ {
232+ "deviceId" : " ..." ,
233+ "installId" : " ..." ,
234+ "cdid" : " ..." ,
235+ "openudid" : " ..." ,
236+ "clientudid" : " ..." ,
237+ "token" : " ..."
238+ }
108239```
109240
110241## 六、外部依赖
111242
112- | 依赖 | 说明 |
113- | ------| ------|
114- | Anthropic OAuth | claude.ai 订阅登录,非 API key |
115- | GrowthBook | ` tengu_amber_quartz_disabled ` 紧急关闭 |
116- | macOS 原生音频 或 SoX | 音频录制 |
117- | Nova 3 STT | 语音转文本模型 |
243+ | 依赖 | 说明 | 适用后端 |
244+ | ------| ------| ----------|
245+ | Anthropic OAuth | claude.ai 订阅登录,非 API key | Anthropic |
246+ | GrowthBook | ` tengu_amber_quartz_disabled ` 紧急关闭 | 通用 |
247+ | macOS 原生音频 或 SoX | 音频录制 | 通用 |
248+ | Nova 3 STT | Anthropic 语音转文本模型 | Anthropic |
249+ | doubaoime-asr | 豆包 ASR SDK(optionalDependencies) | 豆包 |
250+ | 凭证文件 | ` ~/.claude/tts/doubao/credentials.json ` | 豆包 |
118251
119252## 七、文件索引
120253
121- | 文件 | 行数 | 职责 |
122- | ------| ------| ------|
123- | ` src/voice/voiceModeEnabled.ts ` | 54 | 三层门控逻辑 |
124- | ` src/hooks/useVoice.ts ` | — | React hook(录音状态 + WebSocket) |
125- | ` src/services/voiceStreamSTT.ts ` | — | STT WebSocket 流式传输 |
254+ | 文件 | 职责 |
255+ | ------| ------|
256+ | ` src/voice/voiceModeEnabled.ts ` | 三层门控逻辑 + ` isVoiceAvailable() ` |
257+ | ` src/hooks/useVoice.ts ` | React hook(录音状态 + 后端选择 + 连接管理) |
258+ | ` src/hooks/useVoiceEnabled.ts ` | 语音启用状态 hook(按 provider 决定 OAuth 检查) |
259+ | ` src/services/voiceStreamSTT.ts ` | Anthropic STT WebSocket 流式传输 |
260+ | ` src/services/doubaoSTT.ts ` | 豆包 ASR 适配器(AudioChunkQueue + connectDoubaoStream) |
261+ | ` src/commands/voice/voice.ts ` | ` /voice ` 命令(开关 + 后端选择) |
262+ | ` src/commands/voice/index.ts ` | 命令注册(去除 availability 限制) |
263+ | ` src/utils/settings/types.ts ` | ` voiceProvider ` 类型定义 |
0 commit comments