add-voice

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Add Voice

添加语音功能

Add Grok Speech to Speech to an existing app. Run on
/add-voice
, typed Voice Mode, or clear “add Grok voice” intent.
为现有应用添加 Grok 语音转语音功能。在用户执行
/add-voice
、输入 Voice Mode 或明确表达“添加 Grok 语音”的意图时使用。

Goal

目标

Working duplex path: user-app mic in, audio out,
wss://api.x.ai/v1/realtime?model=grok-voice-latest
, 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。

Protocol first

优先基于协议实现

Language-agnostic event loop. TypeScript samples default. Short Python twins only where the client API differs (e.g.
ws
vs
websockets
).
与语言无关的事件循环。默认提供 TypeScript 示例。 仅在客户端 API 存在差异时(例如
ws
与
websockets
)提供精简的 Python 对应示例。

Docs

文档

Steps

步骤

  1. 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
      /v1/stt
      or
      /v1/tts
      only if the product still needs one-shot listen or speak outside the agent.
  2. Auth
    • Server: Bearer
      XAI_API_KEY
      .
    • Browser/mobile: backend
      POST https://api.x.ai/v1/realtime/client_secrets
      , client uses ephemeral token (Bearer or browser
      sec-websocket-protocol
      :
      xai-client-secret.<token>
      ).
    • Never put a long-lived key in client bundles. Do not paste keys in chat.
  3. Connect + session
    • URL:
      wss://api.x.ai/v1/realtime?model=grok-voice-latest
    • On open:
      session.update
      with
      voice
      (default
      eve
      ),
      instructions
      ,
      turn_detection: { type: "server_vad" }
      (or
      null
      for push-to-talk), PCM 24 kHz unless the app already standardizes elsewhere.
    • Set
      audio.input.transcription.model: "grok-transcribe"
      or no user transcript arrives (
      conversation.item.input_audio_transcription.updated
      is cumulative, not a delta).
    • Tools if needed:
      web_search
      ,
      x_search
      ,
      file_search
      ,
      mcp
      , custom
      function
      .
  4. Audio I/O (app-side)
    • One
      AudioContext
      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.
    • Mic → AudioWorklet in ~100 ms chunks →
      input_audio_buffer.append
      (or binary transport). Start WS and mic in parallel; buffer early audio, flush on open.
    • Play
      response.output_audio.delta
      immediately; schedule with a ~150 ms lead so chunks butt together. On
      input_audio_buffer.speech_started
      , stop everything queued (barge-in).
    • Transcript rows: create the user row on
      input_audio_buffer.committed
      (
      item_id
      ), fill it on
      …transcription.updated
      ; assistant text from
      response.output_audio_transcript.delta
      /
      .done
      , close the turn on
      response.done
      .
    • On function tools:
      function_call_output
      , finish playback, then
      response.create
      .
  5. Composer UI convention
    • One primary button, right side of the composer. Empty composer → waveform icon (stroked, e.g. Phosphor
      WaveformIcon weight="bold"
      ; never the
      fill
      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 (
      conversation.item.create
      +
      response.create
      ). Text reply streaming → stop square.
    • 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
      prefers-reduced-motion
      . No X button, no pulsing ring.
    • Status lives in the composer, not around it: the placeholder reads
      Connecting…
      /
      Listening…
      /
      Thinking…
      /
      Speaking…
      , plus an
      sr-only
      role="status"
      . No separate status row.
    • The microphone icon is reserved for dictation (
      /add-dictation
      ). Never use it for voice mode.
  6. 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
  }
});
  1. 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
  1. Instrument (before the first human test)
    • Run
      /debug-voice
      : it proposes a plan, then installs a dev-only log sink (
      POST /api/voice/log
      →
      .voice-logs/<sessionId>.ndjson
      , gitignored), a client logger with audio reduced to byte counts, and the session id in the UI, in the app's own language.
    • The same skill carries the fix loop and the symptom → log signature → fix table.
  2. Smoke
    • Text turn via
      conversation.item.create
      +
      response.create
      ; confirm audio or transcript events.
    • 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
      .
  1. 梳理应用现状
    • 技术栈:无、OpenAI Realtime、STT→LLM→TTS 级联、仅 TTS/STT。
    • 客户端类型:web / Node / iOS / Android / 服务端。
    • 如果已存在级联方案或 OpenAI Realtime:用下方的单双向循环替换(注意 URL、模型、语音、事件差异);仅当产品仍需要 Agent 之外的单次监听或朗读功能时,保留独立的
      /v1/stt
      或
      /v1/tts
      。
  2. 认证
    • 服务端:Bearer 方式使用
      XAI_API_KEY
      。
    • 浏览器/移动端:后端调用
      POST https://api.x.ai/v1/realtime/client_secrets
      ,客户端使用临时令牌(Bearer 方式或浏览器
      sec-websocket-protocol
      :
      xai-client-secret.<token>
      )。
    • 切勿将长期有效密钥放入客户端包中。不要在聊天中粘贴密钥。
  3. 连接与会话
    • URL:
      wss://api.x.ai/v1/realtime?model=grok-voice-latest
    • 连接建立时:发送
      session.update
      事件,配置
      voice
      (默认
      eve
      )、
      instructions
      、
      turn_detection: { type: "server_vad" }
      (按键说话模式则设为
      null
      ),音频格式默认 PCM 24 kHz,除非应用已有其他统一标准。
    • 设置
      audio.input.transcription.model: "grok-transcribe"
      ,否则不会返回用户转写结果(
      conversation.item.input_audio_transcription.updated
      是累积式的,而非增量式)。
    • 如需工具调用:支持
      web_search
      、
      x_search
      、
      file_search
      、
      mcp
      、自定义
      function
      。
  4. 音频输入输出(应用端)
    • 每个会话使用一个
      AudioContext
      进行采集和播放,需在用户手势触发的回调中创建(符合自动播放策略)。请求 24 kHz 采样率;如果浏览器返回其他采样率,发送前先重采样。
    • 麦克风 → AudioWorklet 以约 100 ms 的块为单位 →
      input_audio_buffer.append
      (或二进制传输)。并行启动 WebSocket 和麦克风;缓冲早期音频,连接建立后立即发送。
    • 立即播放
      response.output_audio.delta
      ;以约 150 ms 的提前量调度播放,确保音频块无缝衔接。收到
      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
      。
  5. 输入框 UI 规范
    • 一个主按钮,位于输入框右侧。输入框为空时显示波形图标(描边样式,例如 Phosphor 的
      WaveformIcon weight="bold"
      ;切勿使用
      fill
      权重,16 px 下会显示成一团),点击启动语音模式。输入框有文本时显示经典的发送箭头;在语音模式下,该文本会被加入实时会话(通过
      conversation.item.create
      +
      response.create
      )。文本回复流式输出时显示停止方形按钮。
    • 语音激活时,同一按钮显示动画波形(4 条竖条,宽约 3 px,间距 2 px,高约 16 px,最小缩放比例 0.4,确保在 28 px 按钮内清晰可见),点击结束会话。动画由阶段驱动:监听时慢,说话时快,连接/思考时更慢且略微变暗(透明度 ≥ 0.75)。遵循
      prefers-reduced-motion
      设置。不要使用 X 按钮,不要使用脉冲圆环效果。
    • 状态显示在输入框内部,而非外部:占位符显示
      连接中…
      /
      聆听中…
      /
      思考中…
      /
      说话中…
      ,同时添加一个
      sr-only
      的
      role="status"
      元素。不要单独设置状态行。
    • 麦克风图标预留给听写功能(
      /add-dictation
      ),切勿用于语音模式。
  6. 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
  }
});
  1. 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
  1. 埋点(首次人工测试前)
    • 运行
      /debug-voice
      :它会提出方案,然后安装仅用于开发的日志接收器(
      POST /api/voice/log
      →
      .voice-logs/<sessionId>.ndjson
      ,已加入 gitignore)、一个将音频简化为字节计数的客户端日志器,以及在 UI 中显示会话 ID,所有实现均使用应用自身的语言。
    • 该技能同时包含修复循环和“症状 → 日志特征 → 修复方案”对照表。
  2. 冒烟测试
    • 通过
      conversation.item.create
      +
      response.create
      发起文本轮次;确认收到音频或转写事件。
    • 确认客户端中没有长期有效密钥(在构建后的客户端包中 grep 环境变量名和
      client_secrets
      关键字)。
    • 将应用交给用户时建议佩戴耳机。使用外放时麦克风会拾取回复,模型会自己应答自己;这是回声问题,不是循环逻辑的 bug。
    • 使用
      /debug-voice
      迭代优化。

Out of scope

不适用范围

  • Speech-to-text only (
    /add-dictation
    ), speaking text (
    /add-read-aloud
    )
  • Image generation and text-only inference
  • Invented endpoints, events, or CLI flags
  • 仅语音转文本(
    /add-dictation
    )、文本朗读(
    /add-read-aloud
    )
  • 图像生成和纯文本推理
  • 虚构的端点、事件或 CLI 标志