关于 debug-voice
用于为语音会话添加日志并依据日志排查问题的开发技能,适用于不同技术栈的应用。它规定先提交变更计划,再记录经过脱敏的事件与音频统计,供测试后定位故障。
主要功能
- 01先制定并确认变更计划
- 02记录语音事件并对数据脱敏
- 03汇总音频统计与会话日志
- 04依据日志特征逐项验证修复
适用场景
- 01排查语音会话无声或断连
- 02分析播放卡顿与首字延迟
- 03根据真实测试日志验证修复
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cursor/ plugins --skill debug-voice安装后在 Claude Code 中输入/debug-voice确认已加载。
详细介绍
调试语音
让语音会话在事后清晰可读,再根据证据修复问题。Agent 听不到应用;日志是它的耳朵,用户是它的评判者。不录音频,不记录令牌,绝不用于生产环境。
适用于任何技术栈。管线遵循一份简明契约(如下);使用应用现有技术实现即可。
工作流
- 梳理应用(只读)。
- 规划:写出变更清单,展示给用户,然后停下。用户确认前不得编辑任何内容。
- 使用应用的语言、框架和惯例安装已确认的组件。
- 验证日志接收端,然后把应用交给用户。
- 根据日志进入修复循环。
1. 梳理
找到以下内容,并记下路径:
- 语音客户端:接收和发送实时事件、采集麦克风输入、播放音频、获取令牌的位置。
- 服务端:使用的框架、路由声明方式、共享服务端代码的位置、读取环境变量的方式,以及此处“生产环境”的定义。
- 惯例:使用的语言、模块系统、格式化工具,以及脚本或任务所在位置(
package.json、Makefile、pyproject、justfile)、.gitignore。 - 客户端类型:浏览器、移动端、桌面端、CLI。非浏览器客户端仍然发送相同的 JSON;单进程应用可以跳过 HTTP,直接追加到文件。
- 处理音频增量数据的位置。必须统计数量,绝不能记录内容。
2. 规划,然后停下
填写以下内容,使用实际路径和应用所用语言,发布后等待用户同意或提供删减后的清单。确认之前不要编辑文件。
## Debug voice: plan
Add
- <path>: client logger (batch, redact, flush) in <language>
- <path>: dev-only sink `POST /api/voice/log` → `.voice-logs/<sessionId>.ndjson`
- <path> (optional): summary command `<cmd>`; otherwise read the NDJSON with jq
Modify
- <voice client file>: hook points start, token, mic, env, ws.*, client/server events,
audio.in (2 s windows), audio.out.first, audio.out, play.stop, stop
- <token route>: append `server.token { ok, status, ms, upstream }` (never the token)
- <UI file>: session id in the voice status line and in voice error messages
- .gitignore: `/.voice-logs`
- <scripts file>: a `voice:logs` task (only if the summary command is wanted)
Logged: event names and non-audio fields, timings, byte counts, mic RMS.
Never: tokens, API keys, raw audio, strings over 400 chars.
Off in production unless `VOICE_LOG=1`.
Reply "go", or strike lines you do not want.
3. 安装:契约
与应用保持一致。使用与周边代码相同的语言、路由风格和格式化工具。按照以下契约编写组件;不要为日志引入第二种语言或工具链。
会话 ID:UUID 中的 8 个小写十六进制字符。日志接收端接受 ^[a-z0-9]{4,64}$;该 ID 将用作文件名。
条目(每行一个 JSON 对象):
| 字段 | 客户端 | 服务端 |
|---|---|---|
t | 自日志记录器启动以来的毫秒数 | 不存在;读取方根据 ts 对齐 |
ts | epoch 毫秒 | epoch 毫秒 |
kind | start、server、client、error、audio.in,…… | server.token,…… |
src | 不存在 | "server" |
| 其余字段 | 钩子字段,已脱敏 | 钩子字段 |
脱敏,在客户端缓冲之前执行:对于音频事件类型(response.output_audio.delta、response.audio.delta、input_audio_buffer.append),将 delta / audio 替换为 bytes = 解码后的 base64 长度;超过 400 个字符的字符串截断为 400 个字符 + …[N chars];深度超过 4 的对象替换为 "[depth]";超过 50 项的数组截断。
客户端日志记录器:缓冲条目;每 1 秒或达到 200 条时,以 POST <sink> {"sessionId","entries":[…]} 刷新;停止时使用 keepalive(或平台上具有“跳转后继续”效果的等效机制)刷新;忽略所有传输错误,日志记录绝不能向语音路径抛出异常。在开发环境中,也将条目输出到控制台。
日志接收端:POST /api/voice/log,JSON 请求体。除非处于开发环境或 VOICE_LOG=1,否则返回 404。如果 sessionId 不符合正则表达式,或 entries 不是数组,则返回 400。每个请求最多追加 500 条条目,丢弃超过 16,000 个字符的行,写入 .voice-logs/<sessionId>.ndjson,并创建目录。返回 204。
适用于任何服务端的伪代码:
handle POST /api/voice/log:
if production and VOICE_LOG != "1": return 404
body = parse json or return 400
if not regex(body.sessionId) or not list(body.entries): return 400
mkdir .voice-logs; append join(json(e) for e in body.entries[:500] if len < 16000) to .voice-logs/{sessionId}.ndjson
return 204
客户端日志记录器的伪代码:
logger(sessionId, sink):
buffer = []; started = now()
log(kind, data): buffer.push({ ...redact(data), t: now() - started, ts: epoch_ms(), kind }); schedule flush (1 s timer, or immediately at 200 entries)
server(event, extra): log("server", { ...redact_event(event), ...extra }) # never per audio delta
client(event): log("client", redact_event(event)) # never per audio chunk
error(where, err, extra): log("error", { where, name, message, ...extra })
flush(final=false): POST sink {"sessionId","entries": buffer}; buffer = []; ignore all errors; keepalive when final
close(): flush(final=true)
Hook 点
kind 和字段;所有语言中的结构都相同。
| 时机 | kind 和字段 |
|---|---|
| 会话开始 | start { url, target_rate } |
| 获取令牌/失败 | token.ok { ms } / error { where: "token", name, message, ms } |
| 麦克风已授权/被拒绝 | mic.ok { ms, label, settings } / error { where: "mic", … } |
| 音频图就绪 | env { ua, mic_rate, mic_state, play_rate, play_state, capture_frames, target_rate } |
| 套接字 | ws.connecting、ws.open { ms }、ws.error、ws.close { code, reason, wasClean, by } |
| 每个已发送事件,音频块除外 | client { …redacted event } |
| 每个已接收事件,音频增量除外 | server { …redacted event, phase } |
| 阶段变更(去重) | phase { phase } |
| 麦克风块,每 2 s 聚合一次 | audio.in { chunks, bytes, rms_max, rms_avg, pending, mic_state, phase } |
| 打开时发送预打开缓冲区 | audio.flush { chunks } |
| 响应的第一个音频增量 | audio.out.first { response_id, bytes, since_response_created_ms, since_speech_stopped_ms, play_state } |
response.done | audio.out { response_id, status, deltas, bytes, audio_ms, wall_ms, max_gap_ms, queued_ms, underruns, drain_ms_max } |
| 插话中断播放 | play.stop { reason, dropped_ms } |
| 用户停止 | stop { by: "client", phase },然后刷新 |
| 令牌路由(服务端) | server.token { ok, status, ms, upstream } |
在消息处理器中:如果事件是音频增量,就统计字节数和间隔并播放;否则将其作为带有当前阶段的 server 记录,然后执行现有处理逻辑。从 input_audio_buffer.speech_stopped 保存 speechStoppedT,从 response.created 保存 createdT;在第一个增量处报告这两个时间差。在播放器中,如果响应播放期间下一个计划播放时间已经过去,就统计一次欠载;跟踪最长排空时间,并在 response.created 时重置。
UI:在语音状态行显示 ID(Listening · session ab12cd34),并在语音错误后附加 (voice session <id>),以便用户能指出此次运行。
4. 验证
curl -s -o /dev/null -w "%{http_code}\n" -X POST localhost:<port>/api/voice/log \
-H 'Content-Type: application/json' \
-d '{"sessionId":"smoke001","entries":[{"t":0,"ts":0,"kind":"start"}]}' # 204
curl -s -o /dev/null -w "%{http_code}\n" -X POST localhost:<port>/api/voice/log \
-H 'Content-Type: application/json' -d '{"sessionId":"../x","entries":[]}' # 400
cat .voice-logs/smoke001.ndjson && rm .voice-logs/smoke001.ndjson
然后交接:用户在实际应用中测试,并报告他们说了什么、听到了什么、何时出错,以及会话 ID。
阅读日志
可使用以下任一方式;都不需要应用的工具链:
f=.voice-logs/<id>.ndjson
jq -r 'select(.kind|IN("start","token.ok","mic.ok","env","ws.open","ws.close","server.token","stop","error")) | "\(.t // .ts)ms \(.kind) \(.type // "") \(.message // "")"' $f # milestones and errors
jq -r 'select(.kind=="server") | .type' $f | sort | uniq -c | sort -rn # server event counts
jq -c 'select(.kind|IN("audio.out.first","audio.out"))' $f # per-turn latency, gaps, underruns
jq -c 'select(.kind=="audio.in")' $f # mic windows, rms
也可以读取文件;文件按时间顺序每行一个对象。服务端条目只有 ts;使用 start 条目的 ts 将其与客户端时钟对齐。如果用户想要摘要命令,就用应用所用的语言编写一个命令,输出:关键节点、服务端事件计数、每轮 audio.out 对应的一行及其 audio.out.first 延迟、麦克风窗口总计、错误和关闭事件,以及最后 25 条记录。
5. 修复循环
- 如果应用还没有
.voice-logs流程,先添加埋点(步骤 1–4)。 - 用户在实际应用中测试并描述此次运行。
- 阅读日志,检查故障发生时刻
t附近的记录。 - 匹配症状 → 特征 → 修复方式(见下表)。如果没有匹配的特征,先添加日志记录,再次测试,然后修复。
- 只修复一项,再次测试,并确认新日志中已不再出现该特征。
- 记录下来:在“已通过会话确认”下追加一行已确认的问题。如果修复改变了语音功能的构建方式,也要更新
/add-voice。
症状 → 日志特征 → 修复(初始条目)
| 症状(用户) | 特征(日志) | 修复 |
|---|---|---|
| 没有声音,但出现了转录文本 | env.play_state 或 audio.out.first.play_state = suspended | 在用户手势触发时创建并恢复播放音频上下文;每个会话使用一个上下文,而不是每轮一个 |
| 助手打断自己 | speech_started,且 phase=speaking;audio.in.rms_max 仅在播放期间升高 | 回声。戴上耳机确认(如果不再发生,就是回声)。保持回声消除开启、降低扬声器音量,或在 speaking 时阻止麦克风发送 |
| 卡顿、断续 | audio.out.underruns > 0,drain_ms_max 偏高,max_gap_ms 远大于分块长度 | 首个分块播放前预留少量提前量(150–250 ms);不要每轮都重建音频上下文 |
| 有杂音、音调或速度不对 | session.update 速率 ≠ 缓冲区速率;bytes 异常 | 全程使用同一速率(audio.input/output.format.rate、播放器缓冲区);确保字节对齐 |
| 一直无法连接,或立即断开 | session.updated 之前出现 ws.close;server.token.ok=false | 每次点击都生成一个令牌(300 s),协议为 xai-client-secret.<token>,URL 中包含 model;读取 server.token.status 和 upstream |
| 麦克风没有反应 | 每个时间窗口中 audio.in.rms_max ≈ 0;mic.ok.label 不符合预期 | 设备错误或操作系统权限问题;检查 mic.ok.settings、label、mic_state |
| 没有用户转录文本 | 没有 conversation.item.input_audio_transcription.updated | 在 session.update 中设置 audio.input.transcription.model: "grok-transcribe" |
| 首个词出现得慢 | audio.out.first.since_speech_stopped_ms 偏高 | 尝试设置 reasoning.effort: "none";缩短 instructions;检查 token.ok.ms 和 ws.open.ms,了解连接耗时 |
| 回复后才出现用户文本 | ...transcription.updated 的 t > response.created 的 t | 在 input_audio_buffer.committed 时创建用户行(item_id),并在 updated 时填入内容 |
| 开头几个词被截断 | ws.open.ms 较大,且 audio.flush.chunks 达到缓冲区上限 | 在 WebSocket 连接前启动麦克风,提前缓冲音频,并提高连接前缓冲上限 |
会话中确认的问题
修复经验证有效后,追加到新日志中。格式:YYYY-MM-DD · 症状 · 特征 · 修复 · 文件。
规则
- 先制定计划;用户未就更改列表达成一致前,不得修改。
- 不记录令牌、密钥或原始音频。音频只记录字节数。
- 仅用于开发环境。生产环境中,除非
VOICE_LOG=1,否则接收端会返回 404。.voice-logs/已加入 Git 忽略列表。 - 日志记录绝不会向语音处理路径抛出异常。
- 汇总记录音频;绝不逐块记录。
- 每次重新测试只做一项更改,以便从日志中判断哪项修复生效。
- 不要编造 xAI 事件名称;添加特征前,请先在 https://docs.x.ai/developers/model-capabilities/audio/speech-to-speech 中确认。