add-voice について
add-voice は、既存アプリに Grok Speech to Speech を組み込むスキルです。双方向音声機能を開発する人向けに、リアルタイム WebSocket セッション、クライアント側の音声処理、安全な認証方法を説明します。
主な機能
- 01リアルタイム双方向音声セッション
- 02エフェメラルトークンによるクライアント保護
- 03マイク音声の取得と送信
- 04音声応答の受信と再生
- 05関数ツール呼び出しへの対応
活用シーン
- 01アプリにボイスモードを追加
- 02チャット UI に音声対話を接続
- 03既存の音声カスケードを置き換え
- 04リアルタイム文字起こしで会話を表示
MCP 対応
セッションで `mcp` ツールを設定する方法を説明しています。`web_search`、`x_search`、`file_search`、カスタム関数にも対応します。
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ cursor/ plugins --skill add-voiceインストール後、Claude Code で/add-voiceを入力して読み込まれたか確認します。
詳細
音声を追加する
既存のアプリに Grok Speech to Speech を追加します。/add-voice を実行するか、Voice Mode と入力するか、「add Grok voice」と明確に指定してください。
目標
双方向の音声経路を構築します。ユーザーアプリのマイク入力と音声出力を、wss://api.x.ai/v1/realtime?model=grok-voice-latest と安全な認証でつなぎます。Cursor にはネイティブのマイク機能がないため、IDE ではなくアプリ(またはサンプルクライアント)を接続してください。
プロトコルを優先
言語に依存しないイベントループです。**TypeScript のサンプルを標準とします。**クライアント API が異なる場合(例:ws と websockets)に限り、短い Python 版も用意します。
ドキュメント
- 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(STS は約 $0.08/分、テキスト項目は $0.004/件、セッションの最大時間は 120 分)
手順
-
アプリの構成を確認する
- スタック:なし、OpenAI Realtime、STT→LLM→TTS のカスケード、TTS/STT のみ。
- クライアント:Web/Node/iOS/Android/サーバー。
- カスケードまたは OpenAI Realtime がある場合は、以下の単一の双方向ループに置き換えます(URL、モデル、音声、イベントの差分)。エージェント以外で単発の聞き取りや発話が引き続き必要な場合に限り、単独の
/v1/sttまたは/v1/ttsを残してください。
-
認証
- サーバー:Bearer
XAI_API_KEY。 - ブラウザー/モバイル:バックエンドから
POST https://api.x.ai/v1/realtime/client_secretsを呼び出し、クライアントではエフェメラルトークンを使用します(Bearer またはブラウザーのsec-websocket-protocol:xai-client-secret.<token>)。 - 有効期間の長いキーをクライアントバンドルに含めないでください。キーをチャットに貼り付けないでください。
- サーバー:Bearer
-
接続とセッション
- URL:
wss://api.x.ai/v1/realtime?model=grok-voice-latest - 接続時:
voice(デフォルトはeve)、instructions、turn_detection: { type: "server_vad" }(またはプッシュ・トゥ・トークの場合はnull)を指定してsession.updateを送信します。アプリですでに別のサンプリングレートを標準化している場合を除き、PCM は 24 kHz を使用します。 audio.input.transcription.model: "grok-transcribe"を設定してください。設定しないとユーザーのトランスクリプトが届きません(conversation.item.input_audio_transcription.updatedは差分ではなく累積です)。- 必要に応じてツールを設定します:
web_search、x_search、file_search、mcp、カスタムのfunction。
- URL:
-
音声 I/O(アプリ側)
- 収音と再生には、セッションごとに 1 つの
AudioContextを使用し、ユーザー操作中に作成します(自動再生ポリシーに対応するため)。24 kHz を要求し、ブラウザーから別のサンプリングレートが指定された場合は、送信前にリサンプリングします。 - マイク → 約 100 ms のチャンクで AudioWorklet →
input_audio_buffer.append(またはバイナリ転送)。WS とマイクを並行して開始し、先行して届いた音声をバッファーに入れて、接続時にフラッシュします。 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を送信します。
- 収音と再生には、セッションごとに 1 つの
-
Composer UI の規約
- プライマリボタンは 1 つ、composer の右側に配置します。composer が空のとき → waveform アイコン(ストローク、例: Phosphor
WaveformIcon weight="bold"。fillウェイトは 16 px では塊のように描画されるため、決して使用しないでください)で、音声モードを開始します。テキストが入力されているとき → 従来の send 矢印。音声モードではそのテキストがライブセッションに送られます(conversation.item.create+response.create)。テキスト返信のストリーミング中 → 停止スクエア。 - 音声がライブの間、同じボタンは アニメーションする波形(4 本のバー、幅約 3 px、間隔 2 px、高さ約 16 px、最小スケール 0.4 で 28 px のボタン内でも判読可能)を表示し、クリックでセッションを終了します。アニメーションはフェーズによって変化します。リスニングは遅く、スピーキングは速く、接続中/思考中はより遅く、やや暗く(不透明度 ≥ 0.75)。
prefers-reduced-motionを尊重してください。X ボタンやパルスするリングは使用しません。 - ステータスは composer の中に、その周囲ではなく 表示します。プレースホルダーは
Connecting…/Listening…/Thinking…/Speaking…と表示し、さらにsr-onlyのrole="status"を付けます。独立したステータス行は設けません。 - マイクアイコンはディクテーション専用(
/add-dictation)です。音声モードには決して使用しないでください。
- プライマリボタンは 1 つ、composer の右側に配置します。composer が空のとき → waveform アイコン(ストローク、例: Phosphor
-
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 の場合のみ)
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→.voice-logs/<sessionId>.ndjson、gitignore 対象)、音声をバイト数のみに削減するクライアントロガー、および UI 内のセッション ID を、アプリ自身の言語で導入します。- 同じスキルに修正ループと、症状 → ログのシグネチャ → 修正の表が含まれています。
-
スモークテスト
conversation.item.create+response.createによるテキストターン。音声またはトランスクリプトのイベントを確認します。- クライアントに長期間有効なキーが存在しないことを確認します(ビルド済みクライアントバンドルで環境変数名と
client_secretsを grep します)。 - ユーザーには ヘッドフォン を付けてアプリを渡してください。スピーカーの場合、マイクが返信を拾い、モデルが自分自身に応答します。これはエコーであり、ループのバグではありません。
/debug-voiceで反復します。
対象外
- 音声認識のみ(
/add-dictation)、テキストの読み上げ(/add-read-aloud) - 画像生成およびテキストのみの推論
- 実在しないエンドポイント、イベント、CLI フラグ
コメント
Markdown 対応