mcpField
D
debug-voice

debug-voice

github.com/cursor开发14.5k324 次安装今天更新
代码仓库

关于 debug-voice

用于为语音会话添加日志并依据日志排查问题的开发技能,适用于不同技术栈的应用。它规定先提交变更计划,再记录经过脱敏的事件与音频统计,供测试后定位故障。

主要功能

  1. 01先制定并确认变更计划
  2. 02记录语音事件并对数据脱敏
  3. 03汇总音频统计与会话日志
  4. 04依据日志特征逐项验证修复

适用场景

  1. 01排查语音会话无声或断连
  2. 02分析播放卡顿与首字延迟
  3. 03根据真实测试日志验证修复

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/cursor/plugins --skill debug-voice

安装后在 Claude Code 中输入/debug-voice确认已加载。

详细介绍

调试语音

让语音会话在事后清晰可读,再根据证据修复问题。Agent 听不到应用;日志是它的耳朵,用户是它的评判者。不录音频,不记录令牌,绝不用于生产环境。

适用于任何技术栈。管线遵循一份简明契约(如下);使用应用现有技术实现即可。

工作流

  1. 梳理应用(只读)。
  2. 规划:写出变更清单,展示给用户,然后停下。用户确认前不得编辑任何内容。
  3. 使用应用的语言、框架和惯例安装已确认的组件。
  4. 验证日志接收端,然后把应用交给用户。
  5. 根据日志进入修复循环。

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 对齐
tsepoch 毫秒epoch 毫秒
kindstart、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.doneaudio.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. 修复循环

  1. 如果应用还没有 .voice-logs 流程,先添加埋点(步骤 1–4)。
  2. 用户在实际应用中测试并描述此次运行。
  3. 阅读日志,检查故障发生时刻 t 附近的记录。
  4. 匹配症状 → 特征 → 修复方式(见下表)。如果没有匹配的特征,先添加日志记录,再次测试,然后修复。
  5. 只修复一项,再次测试,并确认新日志中已不再出现该特征。
  6. 记录下来:在“已通过会话确认”下追加一行已确认的问题。如果修复改变了语音功能的构建方式,也要更新 /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 中确认。

评论

支持 Markdown