2026-06-09 23:15:17 +08:00
|
|
|
|
---
|
|
|
|
|
|
tags: [voice, stt, 语音输入, push-to-talk, doubao, anthropic]
|
|
|
|
|
|
create time: 2026-06-09 22:30
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-08 23:08:57 +08:00
|
|
|
|
# VOICE_MODE — 语音输入
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
## 概述
|
|
|
|
|
|
|
|
|
|
|
|
VOICE_MODE 实现"按键说话"(Push-to-Talk)语音输入。用户按住空格键录音,音频流式传输到 STT 后端,实时转录显示在终端中。支持 Anthropic STT 和豆包 ASR 两个后端。
|
|
|
|
|
|
|
|
|
|
|
|
> [!info]
|
2026-06-08 23:08:57 +08:00
|
|
|
|
> Feature Flag: `FEATURE_VOICE_MODE=1`
|
|
|
|
|
|
> 实现状态:完整可用(双后端:Anthropic OAuth / 豆包 ASR)
|
|
|
|
|
|
> 引用数:46
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
## 正文
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
### 核心特性
|
|
|
|
|
|
|
|
|
|
|
|
- **Push-to-Talk**:长按空格键录音,释放后自动发送
|
|
|
|
|
|
- **流式转录**:录音过程中实时显示中间转录结果
|
|
|
|
|
|
- **无缝集成**:转录文本直接作为用户消息提交到对话
|
|
|
|
|
|
- **双后端切换**:通过 `/voice` 命令参数选择 STT 后端,持久化到 settings.json
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
### 用户交互
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
| 操作 | 行为 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| 长按空格 | 开始录音,显示录音状态 |
|
|
|
|
|
|
| 释放空格 | 停止录音,转录结果自动提交 |
|
|
|
|
|
|
| `/voice` | 切换语音模式开关(默认使用 Anthropic 后端) |
|
|
|
|
|
|
| `/voice doubao` | 启用语音模式并使用豆包 ASR 后端 |
|
|
|
|
|
|
| `/voice anthropic` | 切换回 Anthropic STT 后端 |
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
#### UI 反馈
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
- **录音指示器**:录音时显示红色/脉冲动画
|
|
|
|
|
|
- **中间转录**:录音过程中显示 STT 实时识别文本
|
|
|
|
|
|
- **最终转录**:完成后替换中间结果
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
### 实现架构
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
#### 门控逻辑
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
文件:`src/voice/voiceModeEnabled.ts`
|
|
|
|
|
|
|
|
|
|
|
|
两层检查函数:
|
|
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
|
// Anthropic 后端(需要 OAuth)
|
|
|
|
|
|
isVoiceModeEnabled() = hasVoiceAuth() && isVoiceGrowthBookEnabled()
|
|
|
|
|
|
|
|
|
|
|
|
// 豆包后端 / 通用可用性检查(不需要 OAuth)
|
|
|
|
|
|
isVoiceAvailable() = isVoiceGrowthBookEnabled()
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
四层门控:
|
|
|
|
|
|
|
2026-06-08 23:08:57 +08:00
|
|
|
|
1. **Feature Flag**:`feature('VOICE_MODE')` — 编译时/运行时开关
|
2026-06-09 23:15:17 +08:00
|
|
|
|
2. **GrowthBook Kill-Switch**:`!getFeatureValue_CACHED_MAY_BE_STALE('tengu_amber_quartz_disabled', false)` — 紧急关闭开关
|
|
|
|
|
|
3. **Auth 检查(仅 Anthropic)**:`hasVoiceAuth()` — 需要 Anthropic OAuth token
|
|
|
|
|
|
4. **Provider 检查**:`voiceProvider` 设置决定使用哪个后端
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
#### 核心模块
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
| 模块 | 职责 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| `src/voice/voiceModeEnabled.ts` | Feature flag + GrowthBook + Auth 三层门控 |
|
|
|
|
|
|
| `src/hooks/useVoice.ts` | React hook 管理录音状态和后端连接 |
|
|
|
|
|
|
| `src/services/voiceStreamSTT.ts` | Anthropic WebSocket 流式 STT |
|
|
|
|
|
|
| `src/services/doubaoSTT.ts` | 豆包 ASR 适配器(AsyncGenerator → VoiceStreamConnection) |
|
|
|
|
|
|
| `src/commands/voice/voice.ts` | `/voice` 命令实现,处理后端选择和持久化 |
|
|
|
|
|
|
| `src/hooks/useVoiceEnabled.ts` | 语音启用状态 hook,根据 provider 决定是否跳过 OAuth |
|
|
|
|
|
|
| `src/utils/settings/types.ts` | `voiceProvider: 'anthropic' | 'doubao'` 设置类型定义 |
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
#### 数据流
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
##### Anthropic 后端
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
```mermaid
|
|
|
|
|
|
graph TD
|
|
|
|
|
|
A["用户按下空格键"] --> B["useVoice hook 激活"]
|
|
|
|
|
|
B --> C["macOS 原生音频 / SoX 开始录音"]
|
|
|
|
|
|
C --> D["WebSocket 连接到 Anthropic STT 端点"]
|
|
|
|
|
|
D --> E["中间转录结果 → 实时显示"]
|
|
|
|
|
|
D --> F["用户释放空格键"]
|
|
|
|
|
|
F --> G["停止录音,等待最终转录"]
|
|
|
|
|
|
G --> H["转录文本 → 插入输入框 → 自动提交"]
|
2026-06-08 23:08:57 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
##### 豆包 ASR 后端
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
graph TD
|
|
|
|
|
|
A["用户按下空格键"] --> B["useVoice hook 激活 voiceProvider=doubao"]
|
|
|
|
|
|
B --> C["macOS 原生音频 / SoX 开始录音"]
|
|
|
|
|
|
C --> D["connectDoubaoStream 创建 AudioChunkQueue + VoiceStreamConnection"]
|
|
|
|
|
|
D --> E["onReady 立即触发(无需等待握手)"]
|
|
|
|
|
|
E --> F["音频数据通过 AudioChunkQueue 传入 transcribeRealtime"]
|
|
|
|
|
|
F --> G["INTERIM_RESULT → 实时显示中间转录"]
|
|
|
|
|
|
F --> H["FINAL_RESULT → 显示最终转录"]
|
|
|
|
|
|
H --> I["用户释放空格键"]
|
|
|
|
|
|
I --> J["finalize 立即返回"]
|
|
|
|
|
|
J --> K["转录文本 → 插入输入框 → 自动提交"]
|
2026-06-08 23:08:57 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
#### 音频录制
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
支持两种音频后端(两个 STT 后端共享):
|
2026-06-09 23:15:17 +08:00
|
|
|
|
|
2026-06-08 23:08:57 +08:00
|
|
|
|
- **macOS 原生音频**:优先使用,低延迟
|
|
|
|
|
|
- **SoX(Sound eXchange)**:回退方案,跨平台
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
#### 豆包 ASR 适配器设计
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
文件:`src/services/doubaoSTT.ts`
|
|
|
|
|
|
|
|
|
|
|
|
豆包后端使用适配器模式,将 `doubaoime-asr` 的 AsyncGenerator 协议桥接到 `VoiceStreamConnection` 接口:
|
|
|
|
|
|
|
|
|
|
|
|
**AudioChunkQueue** — push 式异步队列:
|
2026-06-09 23:15:17 +08:00
|
|
|
|
|
2026-06-08 23:08:57 +08:00
|
|
|
|
- 实现 `AsyncIterable<Uint8Array>` 接口
|
|
|
|
|
|
- `push(chunk)` 将音频数据入队,`push(null)` 发送结束信号
|
|
|
|
|
|
|
|
|
|
|
|
**connectDoubaoStream()** — 连接入口:
|
2026-06-09 23:15:17 +08:00
|
|
|
|
|
2026-06-08 23:08:57 +08:00
|
|
|
|
- 动态导入 `doubaoime-asr`(optionalDependencies)
|
|
|
|
|
|
- 从 `~/.claude/tts/doubao/credentials.json` 加载凭证
|
|
|
|
|
|
- 立即触发 `onReady`(避免与 useVoice 的音频缓冲死锁)
|
|
|
|
|
|
- `finalize()` 立即返回(豆包在录音过程中已返回结果)
|
|
|
|
|
|
|
|
|
|
|
|
**响应类型映射**:
|
|
|
|
|
|
|
|
|
|
|
|
| doubaoime-asr ResponseType | 回调映射 |
|
|
|
|
|
|
|----------------------------|----------|
|
|
|
|
|
|
| SESSION_STARTED | 日志记录 |
|
|
|
|
|
|
| VAD_START | 日志记录 |
|
|
|
|
|
|
| INTERIM_RESULT | `onTranscript(text, false)` |
|
|
|
|
|
|
| FINAL_RESULT | `onTranscript(text, true)` |
|
|
|
|
|
|
| ERROR | `onError(errorMsg)` |
|
|
|
|
|
|
| SESSION_FINISHED | 日志记录 |
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
### 关键设计决策
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
1. **双后端共存**:豆包后端作为独立适配器与 Anthropic 后端并存,通过 `voiceProvider` 设置切换
|
2026-06-08 23:08:57 +08:00
|
|
|
|
2. **设置持久化**:`voiceProvider` 存储在 `settings.json`,通过 `/voice` 命令修改,跨会话生效
|
|
|
|
|
|
3. **OAuth 独占(Anthropic)**:Anthropic 后端使用 `voice_stream` 端点(claude.ai),仅 OAuth 用户可用
|
2026-06-09 23:15:17 +08:00
|
|
|
|
4. **豆包无需 OAuth**:豆包后端使用独立凭证文件,不依赖 Anthropic 认证
|
2026-06-08 23:08:57 +08:00
|
|
|
|
5. **GrowthBook 负向门控**:`tengu_amber_quartz_disabled` 默认 `false`,新安装自动可用
|
2026-06-09 23:15:17 +08:00
|
|
|
|
6. **onReady 立即触发**:避免与 useVoice 音频缓冲的时序死锁
|
|
|
|
|
|
7. **finalize() 立即返回**:豆包在录音过程中已返回所有结果,用户抬手时无需等待
|
|
|
|
|
|
8. **乐观可用性检查**:`isDoubaoAvailableSync()` 首次调用返回 `true`,实际导入错误在 `connectDoubaoStream` 中处理
|
2026-06-08 23:08:57 +08:00
|
|
|
|
9. **optionalDependencies**:`doubaoime-asr` 作为可选依赖,安装失败不影响 Anthropic 后端
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
### 使用方式
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 启用 feature
|
|
|
|
|
|
FEATURE_VOICE_MODE=1 bun run dev
|
|
|
|
|
|
|
|
|
|
|
|
# 在 REPL 中使用 Anthropic 后端
|
|
|
|
|
|
# 1. 确保已通过 OAuth 登录(claude.ai 订阅)
|
|
|
|
|
|
# 2. 输入 /voice 启用
|
|
|
|
|
|
# 3. 按住空格键说话
|
|
|
|
|
|
# 4. 释放空格键等待转录
|
|
|
|
|
|
|
|
|
|
|
|
# 在 REPL 中使用豆包 ASR 后端
|
|
|
|
|
|
# 1. 确保 doubaoime-asr 已安装(bun add doubaoime-asr)
|
|
|
|
|
|
# 2. 配置凭证文件:~/.claude/tts/doubao/credentials.json
|
|
|
|
|
|
# 3. 输入 /voice doubao 启用
|
|
|
|
|
|
# 4. 按住空格键说话
|
|
|
|
|
|
# 5. 释放空格键,转录结果即刻显示
|
|
|
|
|
|
|
|
|
|
|
|
# 切换后端
|
|
|
|
|
|
/voice doubao # 切换到豆包 ASR
|
|
|
|
|
|
/voice anthropic # 切换回 Anthropic STT
|
|
|
|
|
|
/voice # 关闭语音模式
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
#### 豆包凭证配置
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
凭证文件路径:`~/.claude/tts/doubao/credentials.json`
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"deviceId": "...",
|
|
|
|
|
|
"installId": "...",
|
|
|
|
|
|
"cdid": "...",
|
|
|
|
|
|
"openudid": "...",
|
|
|
|
|
|
"clientudid": "...",
|
|
|
|
|
|
"token": "..."
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
### 外部依赖
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
| 依赖 | 说明 | 适用后端 |
|
|
|
|
|
|
|------|------|----------|
|
|
|
|
|
|
| Anthropic OAuth | claude.ai 订阅登录,非 API key | Anthropic |
|
|
|
|
|
|
| GrowthBook | `tengu_amber_quartz_disabled` 紧急关闭 | 通用 |
|
|
|
|
|
|
| macOS 原生音频 或 SoX | 音频录制 | 通用 |
|
|
|
|
|
|
| Nova 3 STT | Anthropic 语音转文本模型 | Anthropic |
|
|
|
|
|
|
| doubaoime-asr | 豆包 ASR SDK(optionalDependencies) | 豆包 |
|
|
|
|
|
|
| 凭证文件 | `~/.claude/tts/doubao/credentials.json` | 豆包 |
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
### 文件索引
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
| 文件 | 职责 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| `src/voice/voiceModeEnabled.ts` | 三层门控逻辑 + `isVoiceAvailable()` |
|
|
|
|
|
|
| `src/hooks/useVoice.ts` | React hook(录音状态 + 后端选择 + 连接管理) |
|
|
|
|
|
|
| `src/hooks/useVoiceEnabled.ts` | 语音启用状态 hook(按 provider 决定 OAuth 检查) |
|
|
|
|
|
|
| `src/services/voiceStreamSTT.ts` | Anthropic STT WebSocket 流式传输 |
|
|
|
|
|
|
| `src/services/doubaoSTT.ts` | 豆包 ASR 适配器(AudioChunkQueue + connectDoubaoStream) |
|
|
|
|
|
|
| `src/commands/voice/voice.ts` | `/voice` 命令(开关 + 后端选择) |
|
|
|
|
|
|
| `src/commands/voice/index.ts` | 命令注册(去除 availability 限制) |
|
|
|
|
|
|
| `src/utils/settings/types.ts` | `voiceProvider` 类型定义 |
|
2026-06-09 23:15:17 +08:00
|
|
|
|
|
|
|
|
|
|
## 关联笔记
|
|
|
|
|
|
|
|
|
|
|
|
- [[all-features-guide]]
|