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 |
入力欄のマイクボタンには、バッチがデフォルトの方法です。リクエストは 1 回で、ソケットは不要です。キーがサーバーの外に出ることもありません。途中のテキスト表示が必要な UX の場合に限り、ストリーミングを使用してください。
認証
- 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 |
| エージェントと顧客を別々のチャンネルにする | multichannel=true&channels=2(Opus ではなく PCM のみ) |
| 「um」、「uh」を保持する | filler_words=true(デフォルトでは削除されます) |
| 低帯域幅またはモバイル | encoding=opus、各フレームに生の Opus パケットを正確に 1 つ指定し、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]→textを含む 200。-F format=trueを追加し、languageを指定せずに同じ呼び出しをすると 400 になります。 - ストリーミング:文の間に間を置いて、2 文をディクテーションします。中間テキストの後に確定テキストが出ることを確認します。発話の境界で単語が重複したり消えたりしないことを確認します(単語が消える場合、結合された
speech_finalテキストにチャンクの確定結果が含まれていません。lockedを置き換えるのではなく、追加してください)。audio.done→transcript.doneの後、ソケットが閉じることを確認します。 - クライアントのバンドルで
XAI_API_KEYを検索し、含まれていないことを確認します。 /debug-voiceを使ってログからデバッグします。フックポイントをtranscript.*イベントに差し替えます。
- バッチ:ドキュメントに記載されている最新の
対象外
- 音声で応答する機能(
/add-voice)、テキストを読み上げる機能(/add-read-aloud) - ドキュメントに記載されていない STT トークンフロー、エンドポイント、モデル ID、イベント名を考案すること