/add-read-aloud
Use when the user runs /add-read-aloud or wants the app to speak text with Grok text-to-speech: read-aloud button on assistant replies, auto-speak, TTS, voice output, narration, IVR prompts, speech tags, voice_id. For a two-way voice agent use /add-voice. For speech-to-text use
Claude Code
$ npx -y skills add cursor/plugins --skill add-read-aloud --agent claude-code
Codex
$ npx skills add cursor/plugins --skill add-read-aloud -a codex -y
Cursor
$ npx skills add cursor/plugins --skill add-read-aloud -a cursor -y
Gemini CLI
$ npx skills add cursor/plugins --skill add-read-aloud -a gemini-cli -y
GitHub Copilot
$ npx skills add cursor/plugins --skill add-read-aloud -a github-copilot -y
OpenCode
$ npx skills add cursor/plugins --skill add-read-aloud -a opencode -y
Windsurf
$ npx skills add cursor/plugins --skill add-read-aloud -a windsurf -y
SKILL.md
add-read-aloud.SKILL.mdname: add-read-aloud
description: >-
Use when the user runs /add-read-aloud or wants the app to speak text with
Grok text-to-speech: read-aloud button on assistant replies, auto-speak, TTS,
voice output, narration, IVR prompts, speech tags, voice_id. For a two-way
voice agent use /add-voice. For speech-to-text use /add-dictation.
Add Read Aloud
Add Grok Text to Speech to an existing app: a speaker button on assistant replies, auto-speak, or narration of any text. Run on `/add-read-aloud`, typed **Read aloud**, or clear “speak this” / “TTS” intent. Cursor has no speaker; wire the **app**, not the IDE.
Docs
- https://docs.x.ai/developers/model-capabilities/audio/text-to-speech
- API reference: https://docs.x.ai/developers/rest-api-reference/inference/voice
- Custom voices: https://docs.x.ai/developers/model-capabilities/audio/custom-voices
- Pricing (cite docs only): https://docs.x.ai/developers/pricing
Pick the path
| Need | Path | | --- | --- | | Tap speaker, hear the finished reply. Narrate a page. Generate a file. | **Batch** `POST https://api.x.ai/v1/tts` (default) | | Audio starts while the LLM is still streaming; barge-in; texts over 15,000 chars | **Streaming** `wss://api.x.ai/v1/tts` through a backend relay |
Batch is the default for a read-aloud button: one request, one MP3, cacheable, the key never leaves the server. Go streaming only when the UX needs audio before the text is complete. `POST /v1/tts` has no documented streaming flag; do not invent one.
Auth
- Bearer `XAI_API_KEY`, server side only. The TTS docs document no ephemeral-token flow, and browsers cannot set WebSocket headers, so browser streaming goes through your backend relay.
- Never put the key in a client bundle. Do not paste keys in chat.
Steps
1. **Map the app**
- Where assistant messages render, where per-message actions live (copy, regenerate), how the reply stream ends, server framework, package manager.
- The **speaker icon belongs to read aloud**. Waveform is voice mode (`/add-voice`), microphone is dictation (`/add-dictation`). Put a ghost speaker button in the message action row; loading shows a spinner, playing shows a stop square. One utterance at a time: starting a new one stops the current one.
- Align the action row to the reply’s **text edge**, not the button’s box: an icon button centres its glyph, so if the assistant bubble has no padding pull the row left by that inset (e.g. `-ml-[5px]` for a 12–14 px icon in a 24 px button). Measure in the browser; `getBoundingClientRect` on the `<p>` and the `<svg>` should share a left edge.
- Render the button only once the reply has **finished streaming**; on a live message it would speak a partial reply.
- With many messages on screen, keep player state (active message id, `loading | playing`, last error) in one shared store (`useSyncExternalStore`, a signal, whatever the app uses) so every button reflects it and errors can surface in the app's existing status area. A per-button `let current` is not enough.
- Auto-speak: opt-in toggle, off by default, and only after a user gesture on the page (autoplay policy). Never auto-speak on load.
- If `/add-voice` is installed, its `AudioContext` and PCM player can play streaming TTS; do not add a second audio graph.
2. **Prepare the text**
- Speak prose, not markup. Strip markdown: headings → text, `**bold**` → text, links → link text, inline code → the code, fenced blocks → `[pause] Code block omitted.`, tables → one sentence per row or omit. Keep punctuation; it drives pacing.
- Neutralise speech tags that arrive inside the reply (`[laugh]`, `<whisper>`…) so the model’s text cannot steer delivery. Strip only the **documented tag names** (list in step 5), not every bracket: `[1]` citations and `[note]` must survive.
- Batch limit is **15,000 characters per request**. Split longer text on paragraph, then sentence, then word boundaries and play the parts in order; fetch part N+1 while N plays or there is a silent gap at every boundary. Or use streaming.
- Cache by `hash(text + voice_id + language + speed)`; the same reply is often replayed.
3. **Batch path (default)**
- Server: your route takes `{ text, voice_id?, language? }`, validates the shape of each (`voice_id` `^[a-z0-9-]{1,64}$`, `language` BCP-47 or `auto`), forwards JSON, streams the body back with the upstream `Content-Type` and `Cache-Control: no-store`. Map upstream 404 to “unknown voice” so the client gets a readable error. Default output is MP3 at 24 kHz / 128 kbps, playable everywhere in the browser.
- `language`: default to `"auto"` for a chat app, where replies follow the user’s language; pin `"en"` etc. only for fixed-language products.
// server (any runtime with fetch)
export async function speak(text: string, voice_id = "eve", language = "auto") {
if (!text.trim() || text.length > 15_000) throw new Error("TTS text must be 1–15,000 chars");
const res = await fetch("https://api.x.ai/v1/tts", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.XAI_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
text,
voice_id,
language, // required: `auto` or BCP-47 (`en`, `pt-BR`); omitting it → 422
// output_format: { codec: "mp3", sample_rate: 24000, bit_rate: 128000 }, // default
// speed: 1.0, // 0.7–1.5
// text_normalization: true, // "$5" → "five dollars"
// replace: { nginx: "/ˈɛndʒɪn ˈɛks/" },
}),
});
if (!res.ok) throw new Error(`TTS ${res.status}`); // 400 bad text/format, 401 key, 404 unknown voice_id, 422 missing required field (e.g. language), 429/500/503 back off and retry
return new Response(res.body, { headers: { "Content-Type": res.headers.get("content-type") ?? "audio/mpeg" } });
}// client
let current: HTMLAudioElement | null = null;
async function readAloud(text: string, voiceId = "eve") {
currentRead more
name: add-read-aloud description: >- Use when the user runs /add-read-aloud or wants the app to speak text with Grok text-to-speech: read-aloud button on assistant replies, auto-speak, TTS, voice output, narration, IVR prompts, speech tags, voice_id. For a two-way voice agent use /add-voice. For speech-to-text use /add-dictation.
Add Read Aloud
Add Grok Text to Speech to an existing app: a speaker button on assistant replies, auto-speak, or narration of any text. Run on `/add-read-aloud`, typed **Read aloud**, or clear “speak this” / “TTS” intent. Cursor has no speaker; wire the **app**, not the IDE.
Docs
- https://docs.x.ai/developers/model-capabilities/audio/text-to-speech
- API reference: https://docs.x.ai/developers/rest-api-reference/inference/voice
- Custom voices: https://docs.x.ai/developers/model-capabilities/audio/custom-voices
- Pricing (cite docs only): https://docs.x.ai/developers/pricing
Pick the path
| Need | Path | | --- | --- | | Tap speaker, hear the finished reply. Narrate a page. Generate a file. | **Batch** `POST https://api.x.ai/v1/tts` (default) | | Audio starts while the LLM is still streaming; barge-in; texts over 15,000 chars | **Streaming** `wss://api.x.ai/v1/tts` through a backend relay |
Batch is the default for a read-aloud button: one request, one MP3, cacheable, the key never leaves the server. Go streaming only when the UX needs audio before the text is complete. `POST /v1/tts` has no documented streaming flag; do not invent one.
Auth
- Bearer `XAI_API_KEY`, server side only. The TTS docs document no ephemeral-token flow, and browsers cannot set WebSocket headers, so browser streaming goes through your backend relay.
- Never put the key in a client bundle. Do not paste keys in chat.
Steps
1. **Map the app**
- Where assistant messages render, where per-message actions live (copy, regenerate), how the reply stream ends, server framework, package manager.
- The **speaker icon belongs to read aloud**. Waveform is voice mode (`/add-voice`), microphone is dictation (`/add-dictation`). Put a ghost speaker button in the message action row; loading shows a spinner, playing shows a stop square. One utterance at a time: starting a new one stops the current one.
- Align the action row to the reply’s **text edge**, not the button’s box: an icon button centres its glyph, so if the assistant bubble has no padding pull the row left by that inset (e.g. `-ml-[5px]` for a 12–14 px icon in a 24 px button). Measure in the browser; `getBoundingClientRect` on the `<p>` and the `<svg>` should share a left edge.
- Render the button only once the reply has **finished streaming**; on a live message it would speak a partial reply.
- With many messages on screen, keep player state (active message id, `loading | playing`, last error) in one shared store (`useSyncExternalStore`, a signal, whatever the app uses) so every button reflects it and errors can surface in the app's existing status area. A per-button `let current` is not enough.
- Auto-speak: opt-in toggle, off by default, and only after a user gesture on the page (autoplay policy). Never auto-speak on load.
- If `/add-voice` is installed, its `AudioContext` and PCM player can play streaming TTS; do not add a second audio graph.
2. **Prepare the text**
- Speak prose, not markup. Strip markdown: headings → text, `**bold**` → text, links → link text, inline code → the code, fenced blocks → `[pause] Code block omitted.`, tables → one sentence per row or omit. Keep punctuation; it drives pacing.
- Neutralise speech tags that arrive inside the reply (`[laugh]`, `<whisper>`…) so the model’s text cannot steer delivery. Strip only the **documented tag names** (list in step 5), not every bracket: `[1]` citations and `[note]` must survive.
- Batch limit is **15,000 characters per request**. Split longer text on paragraph, then sentence, then word boundaries and play the parts in order; fetch part N+1 while N plays or there is a silent gap at every boundary. Or use streaming.
- Cache by `hash(text + voice_id + language + speed)`; the same reply is often replayed.
3. **Batch path (default)**
- Server: your route takes `{ text, voice_id?, language? }`, validates the shape of each (`voice_id` `^[a-z0-9-]{1,64}$`, `language` BCP-47 or `auto`), forwards JSON, streams the body back with the upstream `Content-Type` and `Cache-Control: no-store`. Map upstream 404 to “unknown voice” so the client gets a readable error. Default output is MP3 at 24 kHz / 128 kbps, playable everywhere in the browser.
- `language`: default to `"auto"` for a chat app, where replies follow the user’s language; pin `"en"` etc. only for fixed-language products.
// server (any runtime with fetch)
export async function speak(text: string, voice_id = "eve", language = "auto") {
if (!text.trim() || text.length > 15_000) throw new Error("TTS text must be 1–15,000 chars");
const res = await fetch("https://api.x.ai/v1/tts", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.XAI_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
text,
voice_id,
language, // required: `auto` or BCP-47 (`en`, `pt-BR`); omitting it → 422
// output_format: { codec: "mp3", sample_rate: 24000, bit_rate: 128000 }, // default
// speed: 1.0, // 0.7–1.5
// text_normalization: true, // "$5" → "five dollars"
// replace: { nginx: "/ˈɛndʒɪn ˈɛks/" },
}),
});
if (!res.ok) throw new Error(`TTS ${res.status}`); // 400 bad text/format, 401 key, 404 unknown voice_id, 422 missing required field (e.g. language), 429/500/503 back off and retry
return new Response(res.body, { headers: { "Content-Type": res.headers.get("content-type") ?? "audio/mpeg" } });
}// client
let current: HTMLAudioElement | null = null;
async function readAloud(text: string, voiceId = "eve") {
currentOfficial Cursor plugins for popular developer tools, frameworks, and SaaS products. Each plugin is a standalone directory at the repository root with its own .cursor-plugin/plugin.json manifest.
More skills from other plugins.
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
ask-matt
Ask which skill or flow fits your situation. A router over the skills in this repo.
Questions about add-read-aloud.
- Which coding agents can use add-read-aloud?
- add-read-aloud installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

