File size: 9,980 Bytes
eeeb2b6 96f34e3 eeeb2b6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 | # 语音系统
## 概述
Codev 的语音系统支持**语音转文字(STT, Speech-to-Text)** 和 **文字转语音(TTS, Text-to-Speech)** 功能。语音录制使用底层音频捕获库(cpal),含有 SoX 和 arecord 回退方案。语音识别支持多个提供商,包括云端 API 和本地模型;语音合成同样支持多个引擎,涵盖 Edge TTS 和 DashScope TTS。
> 注意:部分功能受构建特性门控(feature gate)限制,仅在 ant-internal 构建中可用。
---
## 语音录制(Voice Recording)
核心文件:`src/services/voice.ts`
录音模块使用分层策略:
| 层级 | 方案 | 适用平台 |
|------|------|----------|
| 原生 | `audio-capture-napi`(cpal) | macOS, Linux, Windows |
| 回退 1 | SoX `rec` | Linux (有 SoX) |
| 回退 2 | `arecord` (ALSA) | Linux (有 ALSA) |
关键特征:
- **懒加载原生模块**:`loadAudioNapi()` 在首次按键时异步加载 `audio-capture-napi`,避免启动时阻塞(dlopen 可能耗时 1-8 秒)。
- **静音检测**:使用 SoX 或 arecord 时启用静音检测(阈值 3%,持续时间 2 秒),自动结束录音。
- **录音常量**:采样率 16000 Hz,单声道,16-bit PCM。
- **依赖检查**:`checkVoiceDependencies()` 探测可用性,显示每个方案的可用状态。
---
## STT 提供商(Speech-to-Text)
### 抽象接口
文件:`src/services/voice/providers.ts`
```typescript
interface TranscriptionProvider {
name: string
transcribe(wavPath: string, language?: string): Promise<TranscriptionResult>
}
```
所有 STT 提供商实现此接口,`LocalWhisperSTT` 和 `DoubaoSTTProvider` 是内置实现。
---
### 1. Groq Whisper(云端)
文件:`src/services/voice/groqSTT.ts`
通过官方 `groq-sdk` npm 包调用 Groq LPU API,使用 Whisper 模型。
**模型回退**:
1. 优先使用 `whisper-large-v3`
2. 遇到 429(限速)或 5xx(服务端错误)时自动回退到 `whisper-large-v3-turbo`
3. 其他错误(4xx)直接抛出,不重试
**API 密钥解析**(优先级从高到低):
1. 显式传入的 `apiKey` 参数
2. `friend.json` 中的 `groqApiKey` 配置
3. 环境变量 `GROQ_API_KEY`
4. `~/.claude/settings.json` 中的 `env.groqApiKey`
**核心流程**:
- `connectGroqStream()` 返回 `VoiceStreamConnection` 接口
- 接收 PCM 音频块并缓冲
- `finalize()` 时将 PCM 转换为 WAV(`pcmToWav()`,16-bit 单声道,16000 Hz)
- 通过 `File` API 上传 WAV 到 Groq
---
### 2. Local Whisper(本地模型)
文件:`src/services/voice/whisperSTT.ts`
基于 Python `openai-whisper` 的本地部署方案。
**架构**:
- 启动一个长期运行的 Python 子进程(`whisper_server.py`)
- 通过 stdin/stdout 的 JSON 行协议通信
- 支持预加载模型(`preloadWhisperModel()`)
**通信协议**:
- `{"type":"load","model":"small"}` —— 加载模型
- `{"type":"transcribe","wav":"/path/to/audio.wav","language":"en"}` —— 转写
- 服务端以 `{"type":"result","text":"...","language":"..."}` 或 `{"type":"error","message":"..."}` 响应
**进程管理**:
- 进程崩溃后自动重启
- 30 秒超时保护
- 临时文件自动清理
**可用性检查**:使用 Python `importlib.util.find_spec("whisper")` 探测 whisper 模块是否可导入,避免加载 PyTorch 的耗时。
---
### 3. Anthropic Voice Stream(WebSocket)
文件:`src/services/voiceStreamSTT.ts`
通过 Anthropic 的 voice_stream WebSocket 端点传输语音,仅在 ant-internal 构建中可用(由 `feature('VOICE_MODE')` 门控)。
**WebSocket 协议**:
- 端点:`wss://api.anthropic.com/api/ws/speech_to_text/voice_stream`
- 认证:OAuth Bearer Token(与 Claude Code 共享凭证)
- 消息类型:
- `KeepAlive` —— 每 8 秒发送保持连接
- `CloseStream` —— 结束流
- 服务端推送 `TranscriptText`、`TranscriptEndpoint`、`TranscriptError`
**连接生命周期**:
1. `connectVoiceStream()` 建立 WebSocket 连接
2. `send(audioChunk)` 发送二进制音频帧
3. `finalize()` 发送 `CloseStream`,等待服务端返回 `TranscriptEndpoint`
4. `FinalizeSource` 枚举标识解析路径:`post_closestream_endpoint`、`no_data_timeout`、`safety_timeout`、`ws_close`、`ws_already_closed`
**Deepgram Nova 3 门控**:通过 GrowthBook 特性标记 `deepgram_nova_3_gate` 控制是否使用 Deepgram Nova 3 模型。
**Voice Keyterms**:通过查询参数传递关键词列表,提高领域术语的识别准确率。
---
### 4. Doubao(豆包 STT)
文件:`src/services/voice/doubaoSTT.ts`
此文件是一个自动生成的存根(stub),对应 ant-internal 的 `feature()` 门控模块。外部构建中所有代码路径在 DCE(死代码消除)后不会实际执行。
存根使用 JavaScript `Proxy` 将任何属性访问、函数调用、构造操作映射到无操作(noop)处理器。导出 `connectDoubaoStream`、`normalizeLanguageForSTT` 等函数作为占位符。
`DoubaoSTTProvider`(在 `providers.ts` 中)封装了此存根的调用逻辑,通过动态导入(`import('./doubaoSTT.js')`)在运行时解析。
---
## TTS 提供商(Text-to-Speech)
### 抽象接口
文件:`src/services/voice/providers.ts`
```typescript
interface TTSProvider {
name: string
synthesize(text: string): Promise<SynthesisResult>
}
```
`EdgeTTSProvider` 和 `CommandTTSProvider` 是内置实现。
---
### 1. Edge TTS(微软神经网络语音)
文件:`src/services/voice/providers.ts`(类 `EdgeTTSProvider`)
**实现路径一(Provider 接口)**:
- 直接调用 `edge-tts` 命令行工具
- 使用 Node.js `child_process.spawn` 执行子进程
- 通过 `--voice`、`--text`、`--write-media` 参数控制输出
- 默认语音:`en-US-AriaNeural`
**实现路径二(独立函数)**:
文件:`src/services/voice/edgeTTS.ts`(`speakWithEdgeTTS()`)
- 调用 `scripts/speak.py` Python 脚本
- 依赖 `.venv/bin/python` 或系统 Python
- 返回标准化的 `TTSResult` 接口
**实现路径三(Friend 模块)**:
文件:`src/friend/tts.ts`(`edgeTts()`)
- 使用 `node-edge-tts` npm 包(Node.js 原生实现,无需 Python)
- 默认语音:`zh-CN-XiaoxiaoNeural`(中文语音)
- 输出为 MP3 文件
**播放功能**(`src/services/voice/edgeTTS.ts` `playAudioFile()`):
| 平台 | 播放器 | 说明 |
|------|--------|------|
| macOS | `afplay` | 原生 |
| Linux | `ffplay` | 先 `pkill` 已有进程,再启动新进程 |
| Windows | `start` | 系统默认播放器 |
---
### 2. Qwen DashScope TTS(通义千问语音合成)
文件:`src/friend/tts.ts`(`qwenTts()`)
调用阿里云 DashScope API 进行语音合成。
**API 信息**:
- 国内端点:`https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation`
- 国际端点:`https://dashscope-intl.aliyuncs.com/...`
- 默认模型:`qwen3-tts-flash`
- 默认语音:`Cherry`
- 认证:Bearer Token(`apiKey`)
**参数**:
- `voice`:语音角色(如 Cherry, Polly 等)
- `model`:模型版本
- `language`:语言(`zh` 或 `en`),决定端点选择和 `language_type`
**流程**:
1. POST 请求获取合成音频的 URL
2. 从 URL 下载音频数据
3. 保存为 WAV 临时文件
4. 支持 30 秒超时保护
**音频文件注册**:`registerAudioFile()` 将文件路径注册到内存映射,5 分钟后自动过期,用于跨模块引用。
---
### 3. Command TTS(命令模板回退)
文件:`src/services/voice/providers.ts`(类 `CommandTTSProvider`)
通用 shell 命令 TTS,支持模板占位符:
- `{input}` / `{input_path}` —— 输入文本文件路径
- `{output_path}` —— 输出音频文件路径
适用于调用任意外部 TTS 命令行工具。
---
## Voice Stream 协议
文件:`src/services/voiceStreamSTT.ts`
Voice Stream 是 Anthropic 的 WebSocket 协议,用于实时语音识别。
### 消息类型
| 方向 | 类型 | 说明 |
|------|------|------|
| 客户端 → 服务端 | `KeepAlive` | 心跳,每 8 秒 |
| 客户端 → 服务端 | `CloseStream` | 结束音频流 |
| 客户端 → 服务端 | 二进制帧 | PCM 音频数据 |
| 服务端 → 客户端 | `TranscriptText` | 转写文本片段 |
| 服务端 → 客户端 | `TranscriptEndpoint` | 转写结束标记 |
| 服务端 → 客户端 | `TranscriptError` | 错误信息 |
### FinalizeSource 枚举
`finalize()` 方法的解析路径:
| 值 | 说明 |
|----|------|
| `post_closestream_endpoint` | 正常流程:发送 CloseStream 后收到 TranscriptEndpoint |
| `no_data_timeout` | 发送 CloseStream 后 1.5 秒无响应 |
| `safety_timeout` | WebSocket 挂起超过 5 秒 |
| `ws_close` | WebSocket 连接关闭 |
| `ws_already_closed` | 已关闭的连接被重复调用 |
### Keyterms(关键词)
通过 WebSocket 查询参数 `keyterms` 传递关键词列表,格式为逗号分隔的 URL 编码值。关键词可提高模型对特定术语的识别准确率。
---
## Voice Mode(语音模式)
语音模式是 Push-to-Talk(按住说话)的实现:
1. **开始录音**:用户按下语音快捷键
2. **音频采集**:底层 cpal 或回退方案开始采集 16kHz 单声道 PCM 音频
3. **音频传输**:音频块通过 Voice Stream WebSocket 实时发送
4. **释放停止**:用户松开快捷键,发送 CloseStream
5. **等待转写**:接收服务端返回的 TranscriptText 和 TranscriptEndpoint
6. **提交文本**:转写文本进入对话输入流
当使用本地 Whisper 时,流程类似但使用子进程通信而非 WebSocket。
---
## 提供商注册与选择
文件:`src/services/voice/providers.ts`
系统通过 `TranscriptionProvider` 和 `TTSProvider` 接口实现多提供商支持。每个提供商有自己的名称(`name` 属性)和实现逻辑。选择策略在调用方(如 `useVoice` hook)中决定,根据可用性和用户配置选择合适的提供商。
|