talkback — 用听的,别用看的 是做什么的?
从任何项目目录朗读文本,保存音频,绝不会静默消耗计量配额。
默认按需工作。 它只在被要求时朗读 — 用户说“解释一下”,或直接运行脚本。它不会主动叙述,直到有人开启 对话模式,且对话模式一次只属于一个会话。
使用方法
S=~/.claude/skills/talkback/scripts
$S/talkback.py "以下是改动内容和原因。" # ElevenLabs(默认)
$S/talkback.py --fast "无关紧要的一句话。" # 本地,即时,免费
$S/talkback.py --quota # 查看剩余配额
$S/talkback.py --voices # 列出可用语音
$S/talkback.py --dry-run "..." # 预览要朗读的文本,不合成
$S/talkback.py --help # 所有标志
$S/talkback-hook.py --on # 开启对话模式,仅当前会话
$S/talkback-hook.py --off # 停止朗读
$S/talkback-hook.py --status # 查看谁在朗读,钩子是否已注册
$S/talkback-hook.py --outputs # 查看本机的音频输出设备
文本也可以通过管道输入:git log -1 --format=%B | $S/talkback.py。
代理应如何使用
这里包含两种不同的功能。一次性朗读 是你传递给 talkback.py 的精心编写的散文。对话模式 是通过 talkback-hook.py 切换的持续设置;你无需编写其文本,钩子会读取当前回合的内容。
一次性朗读
当用户要求听到解释时,为听觉编写,然后朗读它。 不要将原始 Markdown 或 diff 直接传递给工具。编写两到五句简洁的口语散文 — 改了哪些内容、为什么改、对用户意味着什么 — 然后传递。听众无法回滚,所以先给出结论。
默认使用优质语音 — 计划完全可以承受。当文本无关紧要或希望零网络延迟时,使用 --fast。如果发生回退,说明使用了哪个后端。
驱动对话模式
用户说的话对应一条命令。直接运行它;不要手动叙述改动内容,因为钩子会朗读你正在写的回合。
| 用户说 | 运行 |
|---|---|
| “开启对话模式”、“继续朗读”、“叙述这个会话” | talkback-hook.py --on |
| “停止朗读”、“静音”、“安静” | talkback-hook.py --off |
| “只说重点”、“少点细节” | talkback-hook.py --on brief |
| “只告诉我重要的部分” | talkback-hook.py --on marker |
| “用我的 AirPods”、“在 X 上播放” | talkback-hook.py --outputs,然后 --on --output "<名称>" |
| “用便宜的语音”、“停止消耗配额” | talkback-hook.py --on --backend say |
| “它开启了吗?”、“为什么听不到声音?” | talkback-hook.py --status |
| “有其他东西在朗读吗?” | talkback-hook.py --sessions |
| “让所有东西都安静”(所有会话) | talkback-hook.py --off --all |
在告诉用户它已生效前,检查三件事:
--status报告钩子是否已注册。 如果没有,运行--install并告诉他们需要重启。- 如果
--status警告钩子是在此会话开始后注册的,对话模式已开启但不会产生声音。告知用户 — 不要让他们在沉默中发现。 - 对话模式不会持续到会话之外。 重启或
/clear后它将再次关闭,需要再次--on。这是有意为之,不是缺陷;提前说一次,而不是让他们重新询问。
在 marker 模式下 — 以及任何时候当你希望回读内容是一个为听觉编写的摘要而非当前回合本身时 — 在消息末尾添加一个标记:
<!-- talkback: 重构了认证层,三个调用点,测试全部通过。 -->
后端
| 后端 | 成本 | 质量 | 备注 |
|---|---|---|---|
elevenlabs(默认) | 计量 | 最佳 | 受配额保护,自动回退到 say |
say(--fast) | 免费,无限 | 一般 | macOS 原生,~即时,无网络 |
omnivoice | 免费,无限 | 良好 | 本地 + 私有;需要启动后端。未验证 — 见下文 |
降级链
elevenlabs → omnivoice → say,最佳优先。如果一个环节无法承担任务 — 缺少密钥、配额已用至保留、本地服务器宕机、合成错误 — 会交给下一个环节,因此语音会降级,而不是音频缺失。TALKBACK_CHAIN 可重新排序。
明确指定一个环节后,降级链从此处开始并仅向下降级:--fast 表示“立即本地”,绝不会回升到计量后端。每次降级都会在 stderr 上打印原因,并记录所选后端到日志中,因此降级运行绝不会默默降级。
--strict 将任何降级转换为硬错误(退出码 1),适用于必须不允许默默降级的脚本。
配额
账户为 Creator 版:每月 130,958 个字符。一个两分钟的口语解释大约 1500 个字符,因此一个月大约可以朗读 87 次 — 足够将优质语音作为默认选项,而不是奢侈品。
在使用时实时验证,不要依赖记忆 — talkback.py --quota 实时读取。配额方案已经更改过一次,文档中的数字在计划变动时立即过时。
在合成前,工具会读取实时配额并保留 250 个字符的储备,因此一个长解释绝不会耗尽所有余额。如果请求超出配额,它会在 stderr 上发出警告并使用 say。
凭证按以下顺序解析:$ELEVENLABS_API_KEY → ~/.elevenlabs/api_key(由 elevenlabs auth login 写入)→ ~/broomva/.env.local 中的 ELEVENLABS_API_KEY。此机器上有两个不同的密钥,但它们解析到同一个账户,因此检查一个就是检查两个。
Creator 版还解锁了即时和专业语音克隆(30 个语音槽,1 个专业)。--voices 列出账户当前可用的语音。
@elevenlabs/cli包不在运行时使用,也无法做到这一点 — 其全部功能是auth · agents · tools · tests · components,用于管理托管的 ConvAI 代理项目。它没有合成命令。CLI 在此处仅对auth login有用,它会写入密钥文件。
OmniVoice 层未验证
omnivoice 后端是按照文档中的形状实现的,但从未实际测试过 — 本地后端在发布时处于关闭状态。它会优雅地降级(回退到 say,或在 --strict 下失败)。要启动它,请参阅 omnivoice 技能;仓库已位于 ~/broomva/external/OmniVoice-Studio。
口语文本处理
代理的散文并不是为了被听到而编写的,因此文本会先经过处理:
- 代码块 →
(code omitted);URL →(link) - 标题和项目符号变为句子断点,因此列表不会连在一起
- 深层路径缩短为基名(
src/lib/engine.py→engine.py);传递--keep-paths可听到完整路径 - Markdown 强调仅在其界定范围时被移除 —
backend_chain内的裸下划线会被保留,因为删除它会将一个用户可能搜索的符号变成无法搜索的符号
保存的音频
每次朗读都会保存到 ~/.talkback/audio/ 目录,格式为 MP3(如果存在 ffmpeg 则通过其转换),文件名包含时间戳和简洁的 slug,并追加到 ~/.talkback/ledger.jsonl。使用 --no-save 可丢弃,使用 --out-dir 可重定向。
对话模式 — 实时叙述会话
对话模式使代理在每个回合结束时朗读一段简短的回读,只要会话持续。它默认关闭,需要手动开启,并且属于一个会话。
$S/talkback-hook.py --on # 完整详细,每个回合,当前会话
$S/talkback-hook.py --on brief # 仅开头几行
$S/talkback-hook.py --on marker # 仅包含标记的回合
$S/talkback-hook.py --on --backend elevenlabs # 优质语音,计量
$S/talkback-hook.py --on --output "AirPods Pro" # 此会话使用的扬声器
$S/talkback-hook.py --off # 停止朗读(此会话)
$S/talkback-hook.py --off --all # 全局关闭,所有地方
$S/talkback-hook.py --status # 模式、后端、谁在朗读
$S/talkback-hook.py --sessions # 每个当前正在朗读的会话
为什么按会话作用域
Stop 钩子是全局且永久注册的 — 一个在会话期间无法切换的开关不能称为开关。因此钩子在每个会话中都会触发,而标志才是作用域所在:对话模式是会话的属性,而不是机器的属性。
标志是 ~/.talkback/sessions/<session-id>;从未选择加入的会话没有此标志,因此钩子退出 0 且不发出任何声音。这就是为什么六个工作树中的六个并行代理保持静默,而您正在观看的那个会话会说话。0.3.0 之前的设计使用单个机器范围的标志,这就是为什么它从未被开启的原因:开启它会使盒子上的每个代理都能同时发声。
会话 ID 从 CLAUDE_CODE_SESSION_ID 或 CLAUDE_SESSION_ID 读取;--session <id> 可显式设置。当两者都不存在时,命令会打印 pass --session <id> 并以退出码 2 退出,而不是猜测 — 它曾经会回退到工作目录中最新的转录本,这意味着如果同一目录中有两个会话,--on 会启用最近被访问的会话,而不是请求的那个。
在钩子侧,payload 的 session_id 是权威的:当它存在时,它是门户授权的唯一密钥。转录文件名前缀仅在缺少该字段时使用。两者曾经同时被接受,这允许一个指定会话 A 和会话 B 转录本的 payload 解析 B 的 opt-in 并在 A 中说话。
--session <id> 定位另一个会话是故意的,并且不是安全边界。 会话 A 运行 talkback-hook.py --session B --off 可以在没有 B 同意的情况下启用或禁用 B 的对话模式 — 没有所有权检查将标志与会话绑定。这是一个功能,保留下来而不是撤销:盒子上的每个会话都以相同的 OS 用户身份运行,因此任何会话都可以直接在 ~/.talkback/sessions/ 下写入另一个会话的标志文件。CLI 内部的所属关系检查只能阻止通过标志的礼貌路径,而不能阻止直接写入 — 同一主体的边界无法阻止共享该主体的写入者。标志作用域限制音频,因此并行代理默认保持静默;它不限制控制,并且此处没有任何内容应被解读为这样。
详细级别
| 模式 | 朗读内容 |
|---|---|
full(会话默认) | 每个回合的完整内容 — 如果代理写了标记,则从标记开始 |
brief | 超过 TALKBACK_HOOK_MIN_CHARS(80)的回合的前 TALKBACK_HOOK_CHARS(320)个字符;当存在标记时标记优先 |
marker | 仅包含 <!-- talkback: … --> 标记的回合,且仅朗读标记文本 |
off | 从不朗读 — 由 --off 写入,当全局标志会重新启用该会话时使用 |
full 是默认模式,因为回读的目的是走开然后回来知道发生了什么。截取的摘要是答案的预览,而不是答案 — 您仍然需要阅读屏幕,而这正是对话模式旨在避免的。被丢弃的仅是无法听到的内容(代码块、URL、深层路径 — 参见口语文本处理),而不是仅仅很长内容。full 也没有长度下限:"完成,测试通过" 是您离开时想要的结果,而不是噪音。
brief 和 marker 适用于长时间无人值守的流程,您希望了解进度的大致情况,而不是完整转录。
TALKBACK_FULL_MAX_CHARS 为 full 设置了上限,默认关闭 — 上限会将 full 在恰好值得完整朗读的回合处变回 brief。
回读使用整个后端降级链 — 先 ElevenLabs,仅当某个环节不可用时才降级。因此,每个回合的完整详细会消耗实际配额:一个 1500 字符的回合约占每月 Creator 配额的 1%,因此一个长时间的会话将达到保留值,此时它会继续在下一环节朗读,而不是静默。--on --backend say 将其锁定在低端,--on brief 缩短内容。
当一次回读与下一次重叠时
一次完整的回读可能仍在播放,而下一个回合已经结束。TALKBACK_ON_OVERLAP 决定如何处理:
| 值 | 行为 |
|---|---|
interrupt(默认) | 新的回读打断旧的 — 最新的状态才是值得听的 |
queue | 它们按顺序播放,因此一个长详细回读永远不会被截断;您会落后但能听到所有内容 |
queue 适用于您打算完整回听无人值守流程的情况;interrupt 适用于您坐在机器前并希望获取最新内容的情况。
标记
要选择单个回合 — 在 marker 模式下,或朗读一个为听觉编写的摘要而不是消息的开头几行 — 在消息末尾添加:
<!-- talkback: 重构了认证层,三个调用点,测试全部通过。 -->
在 full 模式下,标记成为标题:它首先被朗读,然后是背后的完整回合,因此回读以结论开头而不丢失细节。在 brief 模式下,标记替换摘录,而没有标记的回合会回退到在句子边界处截断的开头句子。
使用哪个扬声器
音频来自运行 Claude Code 的机器。您从手机、平板或另一台笔记本电脑驱动的会话仍然在主机上发声,因此重要的杠杆是选择主机的哪个输出 — 与 Mac 配对的 AirPods、AirPlay 扬声器、显示器、内置扬声器。
$S/talkback-hook.py --outputs # 查看本机可以播放的设备
$S/talkback-hook.py --on --output "AirPods Pro"
$S/talkback.py -d "MacBook Pro 扬声器" "在选定设备上的一句话"
输出按会话存储,因此…