内容
# voice-cc
用语音与 Claude Code 对话。仅限 macOS。
将语音作为 Claude Code CLI 周围的外设 — 添加语音输入/输出,而不放弃钩子、MCP、插件、代理、斜杠命令或会话状态。STT(语音转文本)本地运行(更快的 Whisper);TTS(文本转语音)默认通过 macOS 的 `say` 运行,可选使用 ElevenLabs 流媒体以获得人类级别的语音。
## 为什么不使用 cookbook 示例?
[ElevenLabs cookbook](https://github.com/anthropics/claude-cookbooks/tree/main/third_party/ElevenLabs) 和 [bidirectional_streaming_ai_voice](https://github.com/ccappetta/bidirectional_streaming_ai_voice) 用自定义聊天循环封装了 Anthropic API。您可以获得语音功能,但会失去 Claude Code — 没有工具使用,没有插件,没有钩子,没有会话,没有斜杠命令。
voice-cc 则相反:保留完整的 Claude Code 运行时,通过停止钩子(输出)和 Hammerspoon 推-to-谈(输入)将语音附加为 IO 外设。不会替换 Claude Code;语音是附加的。
## 工作原理
```
输入:PTT 键按住 → 麦克风 → 更快的 Whisper → cliclick → CC 提示
输出:CC 回应结束 → 停止钩子 → 过滤 → say 或 ElevenLabs → 扬声器
```
**标记驱动的说话**:通过 `CLAUDE.md` 规则指示 Claude(via `CLAUDE.md` 规则)用 `<say>...</say>` 标签包装其结束-turn 摘要。过滤器仅朗读该内容。如果缺少标记,则回退到最后一段的提取 — 因此它不会意外地保持沉默。
**中断**:按下 PTT 键会终止任何正在进行的 TTS,因此您可以插入到句子中间。
**回退链**:如果 ElevenLabs 失败(无密钥,配额用完,网络断开),脚本会自动回退到 macOS `say`,因此音频不会中断。
## 安装(仅限 macOS)
```bash
git clone https://github.com/LukeSmith25/voice-cc ~/Code/voice-cc
cd ~/Code/voice-cc
./install.sh
```
安装程序会将钩子脚本复制到 `~/.claude/voice-cc/hooks/`,将 Hammerspoon 配置复制到 `~/.hammerspoon/`,运行 `pip install`,并打印剩余的手动步骤(brew 软件包,OS 权限,settings.json 片段,可选的 ElevenLabs 配置)。
## 用法
| 操作 | 方法 |
|---|---|
| 说话 | 按住 **Ctrl+Option+Space**,说话,松开 |
| 听取 | 每个助理回应的 `<say>` 内容(或最后一段)都会大声朗读 |
| 中断 | 在 TTS 播放期间按 PTT 键 — 立即终止音频 |
| 切换 TTS | `/voice`(也:`/voice on`,`/voice off`,`/voice status`,`/voice stop`) |
| 禁用一个会话 | 在启动 `claude` 之前设置 `export VOICE_CC_ENABLED=0` |
| 禁用 PTT | Hammerspoon 菜单 → 禁用 Hammerspoon |
| 永久禁用 | 在 `~/.claude/settings.json` 中注释停止钩子条目 |
**切换机制**:`/voice off` 在 `~/.claude-data/voice-cc/disabled` 处写入一个标记文件。停止钩子在存在时短路。跨会话和 CC 重启生效。`/voice on` 删除该文件。环境变量是独立的,当您希望单个会话静音时很有用。
## ElevenLabs 升级
默认的 `say` 语音对于测试来说很好,但对于持续使用来说很机械。换成 ElevenLabs 以获得人类级别的流媒体 TTS:
```bash
export ELEVENLABS_API_KEY="sk_..." # https://elevenlabs.io/app/settings/api-keys
export VOICE_CC_TTS=elevenlabs
export VOICE_CC_VOICE_ID="nPczCjzI2devNBz1zQrb" # elevenlabs.io/voice-library 中的任意语音
```
免费层提供 10k 字符/月 — 足以评估。限制 API 密钥仅限 **Text to Speech** 范围。
## 语音模式标记规则
将此添加到您的全局 `~/.claude/CLAUDE.md` 中,以便 Claude 适当地包装摘要:
```markdown
## 语音模式(voice-cc)
- 用 `<say>...</say>` 标签包装您的结束-turn 摘要。标签出现在聊天中 — 这是故意的。
- 使摘要独立 — 没有对“此回应”或周围上下文的引用。
- 目标是一句话,至多两句。如果回应真的没有有用的口语摘要,请省略标记。
```
## 文档
- [`docs/spec.md`](docs/spec.md) — 具有每个门的二进制传递标准的阶段规格
- [`docs/implementation.md`](docs/implementation.md) — 具有完整代码的任务级指南
## 故障排除
| 症状 | 首先查看的地方 |
|---|---|
| 任何回应都没有音频 | `~/.claude-data/voice-cc/log/hook.log` |
| 说出的文本错误 | 运行 `probe.sh` 而不是 `on-stop.sh` 以转储有效载荷模式 |
| 热键无反应 | Hammerspoon 控制台(菜单 → 控制台) |
| 转录为空 | `~/.claude-data/voice-cc/log/stt.log` + 检查麦克风权限 |
| ElevenLabs 静默 | `tail ~/.claude-data/voice-cc/log/hook.log` 以查看回退通知 |
| `cliclick` 不会键入 | 未授予 cliclick 访问权限 |
## 架构选择
- **为什么使用停止钩子,而不是 stdout 尾部?** 停止钩子每次触发,并带有指向转录的干净 JSON 有效载荷。尾部 stdout 需要解析终端转义序列并猜测转折点。
- **为什么使用本地 Whisper,而不是云 STT?** 隐私 + 零每分钟成本 + ~200ms 预热延迟。云 STT(ElevenLabs Scribe,Deepgram)如果技术术语的准确性较差,可以替换。
- **为什么使用推-to-谈,而不是唤醒词?** 唤醒词会增加延迟,并在口述代码时错误触发。按住键匹配突发工程工作。
- **为什么使用 `<say>` 标记,而不是总是说话?** 代码、路径、表格、错误堆栈读起来很糟糕。标记让 Claude 选择可说话的摘要;如果忘记标记,则回退到最后一段以保持有用。
## 许可证
MIT。参见 [LICENSE](LICENSE)。
## 致谢
- 灵感来自 [ElevenLabs Claude cookbook](https://github.com/anthropics/claude-cookbooks/tree/main/third_party/ElevenLabs) 和 [bidirectional_streaming_ai_voice](https://github.com/ccappetta/bidirectional_streaming_ai_voice) — 虽然 voice-cc 采取了相反的架构方法。