add-voice
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAdd Voice
添加语音功能
Add Grok Speech to Speech to an existing app. Run on , typed Voice Mode, or clear “add Grok voice” intent.
/add-voice为现有应用添加 Grok 语音转语音功能。在用户执行 、输入 Voice Mode 或明确表达“添加 Grok 语音”的意图时使用。
/add-voiceGoal
目标
Working duplex path: user-app mic in, audio out, , safe auth. Cursor has no native mic; wire the app (or a sample client), not the IDE.
wss://api.x.ai/v1/realtime?model=grok-voice-latest实现可用的双向通路:用户端应用麦克风输入、音频输出,连接 ,具备安全认证。Cursor 没有原生麦克风支持;需对接应用(或示例客户端),而非 IDE。
wss://api.x.ai/v1/realtime?model=grok-voice-latestProtocol first
优先基于协议实现
Language-agnostic event loop. TypeScript samples default. Short Python twins only where the client API differs (e.g. vs ).
wswebsockets与语言无关的事件循环。默认提供 TypeScript 示例。 仅在客户端 API 存在差异时(例如 与 )提供精简的 Python 对应示例。
wswebsocketsDocs
文档
- https://docs.x.ai/developers/model-capabilities/audio/speech-to-speech
- https://docs.x.ai/developers/model-capabilities/audio/ephemeral-tokens
- Pricing (cite docs only): https://docs.x.ai/developers/pricing (~$0.08/min STS + $0.004/text item; max session 120 min)
- https://docs.x.ai/developers/model-capabilities/audio/speech-to-speech
- https://docs.x.ai/developers/model-capabilities/audio/ephemeral-tokens
- 定价(仅引用文档):https://docs.x.ai/developers/pricing (约 0.08 美元/分钟 STS + 0.004 美元/文本项;单会话最长 120 分钟)
Steps
步骤
-
Map the app
- Stack: none, OpenAI Realtime, STT→LLM→TTS cascade, TTS/STT only.
- Client: web / Node / iOS / Android / server.
- If a cascade or OpenAI Realtime exists: replace it with the single duplex loop below (URL, model, voice, event diffs); keep standalone or
/v1/sttonly if the product still needs one-shot listen or speak outside the agent./v1/tts
-
Auth
- Server: Bearer .
XAI_API_KEY - Browser/mobile: backend , client uses ephemeral token (Bearer or browser
POST https://api.x.ai/v1/realtime/client_secrets:sec-websocket-protocol).xai-client-secret.<token> - Never put a long-lived key in client bundles. Do not paste keys in chat.
- Server: Bearer
-
Connect + session
- URL:
wss://api.x.ai/v1/realtime?model=grok-voice-latest - On open: with
session.update(defaultvoice),eve,instructions(orturn_detection: { type: "server_vad" }for push-to-talk), PCM 24 kHz unless the app already standardizes elsewhere.null - Set or no user transcript arrives (
audio.input.transcription.model: "grok-transcribe"is cumulative, not a delta).conversation.item.input_audio_transcription.updated - Tools if needed: ,
web_search,x_search,file_search, custommcp.function
- URL:
-
Audio I/O (app-side)
- One per session for capture and playback, created inside the user gesture (autoplay policy). Ask for 24 kHz; if the browser gives another rate, resample before sending.
AudioContext - Mic → AudioWorklet in ~100 ms chunks → (or binary transport). Start WS and mic in parallel; buffer early audio, flush on open.
input_audio_buffer.append - Play immediately; schedule with a ~150 ms lead so chunks butt together. On
response.output_audio.delta, stop everything queued (barge-in).input_audio_buffer.speech_started - Transcript rows: create the user row on (
input_audio_buffer.committed), fill it onitem_id; assistant text from…transcription.updated/response.output_audio_transcript.delta, close the turn on.done.response.done - On function tools: , finish playback, then
function_call_output.response.create
- One
-
Composer UI convention
- One primary button, right side of the composer. Empty composer → waveform icon (stroked, e.g. Phosphor ; never the
WaveformIcon weight="bold"weight, which renders as a blob at 16 px), starts voice mode. Any text present → classic send arrow; in voice mode that text goes into the live session (fill+conversation.item.create). Text reply streaming → stop square.response.create - While voice is live the same button shows an animated waveform (4 bars, ~3 px wide, 2 px gap, ~16 px tall, min scale 0.4 so they stay legible in a 28 px button) and ends the session on click. Phase drives the animation: listening slow, speaking fast, connecting/thinking slower and slightly dimmed (opacity ≥ 0.75). Honor . No X button, no pulsing ring.
prefers-reduced-motion - Status lives in the composer, not around it: the placeholder reads /
Connecting…/Listening…/Thinking…, plus anSpeaking…sr-only. No separate status row.role="status" - The microphone icon is reserved for dictation (). Never use it for voice mode.
/add-dictation
- One primary button, right side of the composer. Empty composer → waveform icon (stroked, e.g. Phosphor
-
TS skeleton (default)
ts
const url = "wss://api.x.ai/v1/realtime?model=grok-voice-latest";
// Node: pass Authorization header. Browser: use xai-client-secret.<token> protocol.
const ws = new WebSocket(url /* , { headers: { Authorization: `Bearer ${token}` } } */);
ws.addEventListener("open", () => {
ws.send(JSON.stringify({
type: "session.update",
session: {
voice: "eve",
instructions: "You are a helpful voice agent.",
turn_detection: { type: "server_vad" },
},
}));
});
ws.addEventListener("message", (ev) => {
const event = JSON.parse(String(ev.data));
if (event.type === "response.output_audio.delta") {
// decode base64 PCM and play
}
});- Python twin (only if the app is Python)
python
import json, os, websockets
url = "wss://api.x.ai/v1/realtime?model=grok-voice-latest"
headers = {"Authorization": f"Bearer {os.environ['XAI_API_KEY']}"}
async with websockets.connect(url, additional_headers=headers) as ws:
await ws.send(json.dumps({
"type": "session.update",
"session": {
"voice": "eve",
"instructions": "You are a helpful voice agent.",
"turn_detection": {"type": "server_vad"},
},
}))
async for raw in ws:
event = json.loads(raw)
if event.get("type") == "response.output_audio.delta":
pass # decode and play-
Instrument (before the first human test)
- Run : it proposes a plan, then installs a dev-only log sink (
/debug-voice→POST /api/voice/log, gitignored), a client logger with audio reduced to byte counts, and the session id in the UI, in the app's own language..voice-logs/<sessionId>.ndjson - The same skill carries the fix loop and the symptom → log signature → fix table.
- Run
-
Smoke
- Text turn via +
conversation.item.create; confirm audio or transcript events.response.create - Confirm no long-lived key in client (grep the built client bundle for the env name and ).
client_secrets - Hand the app to the user with headphones. On speakers the mic hears the reply and the model answers itself; that is echo, not a bug in the loop.
- Iterate with .
/debug-voice
- Text turn via
-
梳理应用现状
- 技术栈:无、OpenAI Realtime、STT→LLM→TTS 级联、仅 TTS/STT。
- 客户端类型:web / Node / iOS / Android / 服务端。
- 如果已存在级联方案或 OpenAI Realtime:用下方的单双向循环替换(注意 URL、模型、语音、事件差异);仅当产品仍需要 Agent 之外的单次监听或朗读功能时,保留独立的 或
/v1/stt。/v1/tts
-
认证
- 服务端:Bearer 方式使用 。
XAI_API_KEY - 浏览器/移动端:后端调用 ,客户端使用临时令牌(Bearer 方式或浏览器
POST https://api.x.ai/v1/realtime/client_secrets:sec-websocket-protocol)。xai-client-secret.<token> - 切勿将长期有效密钥放入客户端包中。不要在聊天中粘贴密钥。
- 服务端:Bearer 方式使用
-
连接与会话
- URL:
wss://api.x.ai/v1/realtime?model=grok-voice-latest - 连接建立时:发送 事件,配置
session.update(默认voice)、eve、instructions(按键说话模式则设为turn_detection: { type: "server_vad" }),音频格式默认 PCM 24 kHz,除非应用已有其他统一标准。null - 设置 ,否则不会返回用户转写结果(
audio.input.transcription.model: "grok-transcribe"是累积式的,而非增量式)。conversation.item.input_audio_transcription.updated - 如需工具调用:支持 、
web_search、x_search、file_search、自定义mcp。function
- URL:
-
音频输入输出(应用端)
- 每个会话使用一个 进行采集和播放,需在用户手势触发的回调中创建(符合自动播放策略)。请求 24 kHz 采样率;如果浏览器返回其他采样率,发送前先重采样。
AudioContext - 麦克风 → AudioWorklet 以约 100 ms 的块为单位 → (或二进制传输)。并行启动 WebSocket 和麦克风;缓冲早期音频,连接建立后立即发送。
input_audio_buffer.append - 立即播放 ;以约 150 ms 的提前量调度播放,确保音频块无缝衔接。收到
response.output_audio.delta时,停止所有排队的播放(支持打断)。input_audio_buffer.speech_started - 转写行逻辑:在 事件(携带
input_audio_buffer.committed)时创建用户消息行,在item_id事件时填充内容;助手文本来自…transcription.updated/response.output_audio_transcript.delta,在.done事件时结束本轮对话。response.done - 函数工具调用:返回 ,等待播放完成后,再发送
function_call_output。response.create
- 每个会话使用一个
-
输入框 UI 规范
- 一个主按钮,位于输入框右侧。输入框为空时显示波形图标(描边样式,例如 Phosphor 的 ;切勿使用
WaveformIcon weight="bold"权重,16 px 下会显示成一团),点击启动语音模式。输入框有文本时显示经典的发送箭头;在语音模式下,该文本会被加入实时会话(通过fill+conversation.item.create)。文本回复流式输出时显示停止方形按钮。response.create - 语音激活时,同一按钮显示动画波形(4 条竖条,宽约 3 px,间距 2 px,高约 16 px,最小缩放比例 0.4,确保在 28 px 按钮内清晰可见),点击结束会话。动画由阶段驱动:监听时慢,说话时快,连接/思考时更慢且略微变暗(透明度 ≥ 0.75)。遵循 设置。不要使用 X 按钮,不要使用脉冲圆环效果。
prefers-reduced-motion - 状态显示在输入框内部,而非外部:占位符显示 /
连接中…/聆听中…/思考中…,同时添加一个说话中…的sr-only元素。不要单独设置状态行。role="status" - 麦克风图标预留给听写功能(),切勿用于语音模式。
/add-dictation
- 一个主按钮,位于输入框右侧。输入框为空时显示波形图标(描边样式,例如 Phosphor 的
-
TypeScript 骨架(默认)
ts
const url = "wss://api.x.ai/v1/realtime?model=grok-voice-latest";
// Node: pass Authorization header. Browser: use xai-client-secret.<token> protocol.
const ws = new WebSocket(url /* , { headers: { Authorization: `Bearer ${token}` } } */);
ws.addEventListener("open", () => {
ws.send(JSON.stringify({
type: "session.update",
session: {
voice: "eve",
instructions: "You are a helpful voice agent.",
turn_detection: { type: "server_vad" },
},
}));
});
ws.addEventListener("message", (ev) => {
const event = JSON.parse(String(ev.data));
if (event.type === "response.output_audio.delta") {
// decode base64 PCM and play
}
});- Python 对应示例(仅当应用为 Python 技术栈时使用)
python
import json, os, websockets
url = "wss://api.x.ai/v1/realtime?model=grok-voice-latest"
headers = {"Authorization": f"Bearer {os.environ['XAI_API_KEY']}"}
async with websockets.connect(url, additional_headers=headers) as ws:
await ws.send(json.dumps({
"type": "session.update",
"session": {
"voice": "eve",
"instructions": "You are a helpful voice agent.",
"turn_detection": {"type": "server_vad"},
},
}))
async for raw in ws:
event = json.loads(raw)
if event.get("type") == "response.output_audio.delta":
pass # decode and play-
埋点(首次人工测试前)
- 运行 :它会提出方案,然后安装仅用于开发的日志接收器(
/debug-voice→POST /api/voice/log,已加入 gitignore)、一个将音频简化为字节计数的客户端日志器,以及在 UI 中显示会话 ID,所有实现均使用应用自身的语言。.voice-logs/<sessionId>.ndjson - 该技能同时包含修复循环和“症状 → 日志特征 → 修复方案”对照表。
- 运行
-
冒烟测试
- 通过 +
conversation.item.create发起文本轮次;确认收到音频或转写事件。response.create - 确认客户端中没有长期有效密钥(在构建后的客户端包中 grep 环境变量名和 关键字)。
client_secrets - 将应用交给用户时建议佩戴耳机。使用外放时麦克风会拾取回复,模型会自己应答自己;这是回声问题,不是循环逻辑的 bug。
- 使用 迭代优化。
/debug-voice
- 通过
Out of scope
不适用范围
- Speech-to-text only (), speaking text (
/add-dictation)/add-read-aloud - Image generation and text-only inference
- Invented endpoints, events, or CLI flags
- 仅语音转文本()、文本朗读(
/add-dictation)/add-read-aloud - 图像生成和纯文本推理
- 虚构的端点、事件或 CLI 标志