关于 add-dictation
add-dictation 是为现有应用添加 Grok Speech to Text 听写功能的技能。它指导通过服务器实现批量转录或中继流式转录,并涵盖音频采集、选项和冒烟测试。
主要功能
- 01选择批量或流式转录
- 02添加麦克风听写控件
- 03通过后端中继流式音频
- 04配置转录选项
- 05用冒烟测试检查实现
适用场景
- 01将口述内容写入应用编辑框
- 02说话时显示实时字幕
- 03转录上传或录制的音频
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ cursor/ plugins --skill add-dictation安装后在 Claude Code 中输入/add-dictation确认已加载。
详细介绍
添加语音听写
为现有应用添加 Grok 语音转文本功能:提供一个麦克风按钮,可将听写内容输入到编辑器中、生成实时字幕,或转录录制的音频。通过 /add-dictation、输入 Dictate,或明确表达“transcribe”(转录)意图来运行。Cursor 没有麦克风;请接入应用,而不是 IDE。
文档
- https://docs.x.ai/developers/model-capabilities/audio/speech-to-text
- 模型选择(请先阅读):https://docs.x.ai/developers/model-capabilities/audio/speech-to-text#model-selection
- 定价(仅引用文档):https://docs.x.ai/developers/pricing
模型
进行任何 STT 调用之前,请先阅读模型选择部分,并将列表中最新的模型作为 model 传入。不要臆造 ID,也不要复用此技能中过时的 ID。REST:将表单字段放在 file 之前。流式传输:使用查询参数。官方示例目前发送 grok-voice-transcribe-2.0。截至今日,列表中的模型为:grok-voice-transcribe-2.0(最佳)、grok-voice-transcribe-1.0(原始版本;省略 model 时的 API 默认值)。
选择路径
| 需求 | 路径 |
|---|---|
| 点击、说话、再点击,文本随即出现。上传文件。URL。 | 批量 POST https://api.x.ai/v1/stt(默认) |
| 说话时文本就会出现:字幕、长篇听写、按住说话 | 通过后端中继进行流式传输 wss://api.x.ai/v1/stt |
编辑器的麦克风按钮默认使用批量处理:一次请求,无需套接字,密钥不会离开服务器。只有当用户体验需要中间文本时,才使用流式传输。
身份验证
- 仅在服务端使用 Bearer
XAI_API_KEY。STT 文档未说明临时令牌流程,而且浏览器无法设置 WebSocket 标头,因此浏览器端的流式传输需通过后端中继。不要臆造令牌流程。 - 切勿将密钥放入客户端包中。不要在聊天中粘贴密钥。
步骤
-
梳理应用
- Composer 或输入组件、文本应插入的位置(插入到光标处还是替换文本)、服务端框架、包管理器。
- 麦克风图标用于听写。如果已安装
/add-voice,则保留其波形主按钮;在旁边添加麦克风作为次要幽灵按钮。 - 是否已有麦克风采集功能?如果运行过
/add-voice,其 PCM 采集数据可用于流式 STT;将其采样率作为sample_rate传入。16 kHz 是模型的原生采样率;其他受支持的采样率(8000、16000、22050、24000、44100、48000)会在服务端重采样。
-
批处理路径(默认)
- 客户端:
MediaRecorder→Blob→POST到你自己的路由。端点会自动检测容器(WAV、MP3、OGG、Opus、FLAC、AAC、MP4、M4A、MKV、WebM),因此直接发送MediaRecorder生成的内容即可。 - 服务端:以
multipart/form-data转发。选项字段放在前面,file放在最后;file后面的字段可能会被忽略。使用file或url,最大 500 MB。model使用文档中列出的最新版本。
- 客户端:
// server (any runtime with fetch + FormData)
export async function transcribe(blob: Blob, filename: string) {
const form = new FormData();
form.append("model", "grok-voice-transcribe-2.0"); // latest from docs; re-read model-selection
form.append("format", "true"); // written-form numbers/currency; requires language
form.append("language", "en");
// form.append("keyterm", "Acme"); // repeat per term, ≤100 terms × 50 chars
form.append("file", blob, filename); // last
const res = await fetch("https://api.x.ai/v1/stt", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.XAI_API_KEY}` },
body: form,
});
if (!res.ok) throw new Error(`STT ${res.status}`); // 400 bad input, 413 >500 MB, 429 back off, 502 url fetch failed, 503 retry
return (await res.json()) as {
text: string; language: string; duration: number;
words?: { text: string; start: number; end: number; speaker?: number }[];
channels?: { index: number; text: string; words: unknown[] }[];
};
}
// client
const mime = MediaRecorder.isTypeSupported("audio/webm;codecs=opus") ? "audio/webm;codecs=opus" : "audio/mp4";
const rec = new MediaRecorder(stream, { mimeType: mime });
const parts: BlobPart[] = [];
rec.ondataavailable = (e) => parts.push(e.data);
rec.onstop = async () => {
const fd = new FormData();
fd.append("file", new Blob(parts, { type: mime }), "dictation");
const { text } = await (await fetch("/api/dictation", { method: "POST", body: fd })).json();
insertAtCursor(text);
};
rec.start(); // second tap: rec.stop()
- 流式路径
- 中继:服务端保管密钥,升级浏览器套接字,将二进制帧和客户端控制消息向上转发,并将 JSON 事件向下转发。在服务端构建查询字符串。
model使用文档中列出的最新版本。
- 中继:服务端保管密钥,升级浏览器套接字,将二进制帧和客户端控制消息向上转发,并将 JSON 事件向下转发。在服务端构建查询字符串。
import { WebSocketServer, WebSocket } from "ws";
new WebSocketServer({ port: 8788 }).on("connection", (client) => {
const q = new URLSearchParams({ model: "grok-voice-transcribe-2.0", sample_rate: "16000", encoding: "pcm", interim_results: "true", language: "en" });
const up = new WebSocket(`wss://api.x.ai/v1/stt?${q}`, { headers: { Authorization: `Bearer ${process.env.XAI_API_KEY}` } });
up.on("message", (d) => client.send(d.toString())); // transcript.* and error events
client.on("message", (d, isBinary) => up.readyState === WebSocket.OPEN && up.send(d, { binary: isBinary })); // audio + finalize/audio.done
const end = () => { client.close(); up.close(); };
up.on("close", end); up.on("error", end); client.on("close", end);
});
- 浏览器采集:PCM16、小端序、单声道、16 kHz,100 ms 帧 = 3,200 字节,原始二进制,不使用 base64。发送前等待
transcript.created。MediaRecorder的输出是容器,不是原始帧;不要对其进行流式传输。
const ws = new WebSocket(relayUrl); ws.binaryType = "arraybuffer";
const ctx = new AudioContext({ sampleRate: 16000 }); // if ctx.sampleRate !== 16000, downsample in the worklet
await ctx.audioWorklet.addModule("/pcm16-worklet.js"); // Float32 → Int16LE, posts one 3,200-byte frame per 100 ms
const node = new AudioWorkletNode(ctx, "pcm16");
ctx.createMediaStreamSource(stream).connect(node);
let ready = false;
node.port.onmessage = (e) => ready && ws.readyState === WebSocket.OPEN && ws.send(e.data);
let committed = "", locked = "", live = "";
ws.addEventListener("message", (e) => {
const ev = JSON.parse(e.data);
if (ev.type === "transcript.created") ready = true;
else if (ev.type === "transcript.partial") {
if (ev.speech_final) { committed += ev.text + " "; locked = ""; live = ""; } // complete stitched utterance
else if (ev.is_final) { locked += ev.text + " "; live = ""; } // chunk final: text will not change
else live = ev.text; // interim: may change
render(committed + locked + live);
} else if (ev.type === "transcript.done") ws.close(); // after audio.done
else if (ev.type === "error") showError(ev.message); // most errors close the socket
});
// stop: ws.send(JSON.stringify({ type: "audio.done" }))
// push-to-talk release: ws.send(JSON.stringify({ type: "Finalize" })) then keep streaming (docs show both `finalize` and `Finalize`; the examples use `Finalize`)
- 选项(流式传输使用查询参数,批处理使用表单字段)
| 需求 | 设置 |
|---|---|
| 最新 STT | 阅读文档中的 #model-selection,并传入对应的 model。快照:grok-voice-transcribe-2.0。省略时 → grok-voice-transcribe-1.0。批处理:表单字段。流式传输:查询参数。 |
| 说话时显示文本 | interim_results=true |
“one hundred dollars” → $100 | 流式传输:language=en;批处理:format=true + language=en |
| 产品名称、术语 | 重复使用 keyterm= |
| 听写数字时避免句中截断 | smart_turn=0.7&smart_turn_timeout=3000 |
| 加快或放慢语句结束判定 | endpointing= 毫秒,默认值为 400 |
| 区分说话人(会议) | diarize=true → words[].speaker |
| 将 Agent 和客户放在不同声道 | multichannel=true&channels=2(仅限 PCM,不支持 Opus) |
| 保留“um”“uh” | filler_words=true(默认会移除) |
| 低带宽或移动设备 | encoding=opus,每帧恰好一个原始 Opus 数据包,省略 sample_rate |
| 批处理原始音频 | `audio_format=pcm |
| 安静环境或电话音频 | 降低 vad_threshold(流式传输默认值为 0.08,批处理默认值为 0.5) |
- Python 对应实现(仅当服务端使用 Python 时)
import os, requests
r = requests.post(
"https://api.x.ai/v1/stt",
headers={"Authorization": f"Bearer {os.environ['XAI_API_KEY']}"},
data=[("model", "grok-voice-transcribe-2.0"), ("format", "true"), ("language", "en")],
files={"file": ("dictation.webm", blob, "audio/webm")}, # requests sends data fields before files
)
r.raise_for_status(); text = r.json()["text"]
# streaming: websockets.connect(url, additional_headers={"Authorization": f"Bearer {key}"}); await ws.send(pcm_bytes)
- 冒烟测试
- 批处理:使用文档中的最新
model(目前为grok-voice-transcribe-2.0):curl -X POST https://api.x.ai/v1/stt -H "Authorization: Bearer $XAI_API_KEY" -F model=grok-voice-transcribe-2.0 -F language=en -F [email protected]→ 返回 200 和text。同样的调用加上-F format=true并省略language→ 返回 400。 - 流式传输:听写两句话,中间停顿。预期先收到中间文本,再收到最终文本;语句边界处不应有重复或消失的词语(如果词语消失,说明拼接后的
speech_final文本未包含各片段的最终文本:应追加,而不是替换locked)。audio.done→transcript.done,随后套接字关闭。 - 在客户端打包文件中搜索
XAI_API_KEY;它不应出现在其中。 - 根据日志使用
/debug-voice调试;将其钩子点替换为transcript.*事件。
- 批处理:使用文档中的最新
不在范围内
- 会回应语音的功能(
/add-voice)、朗读文本(/add-read-aloud) - 凭空编造 STT 令牌流程、端点、模型 ID 或文档中没有的事件名称
评论
支持 Markdown