# Tts

> Convert written text into spoken audio using OpenAI's text-to-speech API (gpt-4o-mini-tts), saving an MP3 and optionally playing it on local speakers. Use this skill whenever the user wants to HEAR something rather than read it — 'read this to me,' 'say that out loud,' 'can I listen to this,' 'turn this into audio,' 'narrate this post,' 'make a voiceover,' 'text to speech,' 'TTS,' 'generate an au…

- **Type:** Skill
- **Install:** `agentstack add skill-ferrants-skill-tts-tts`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ferrants](https://agentstack.voostack.com/s/ferrants)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ferrants](https://github.com/ferrants)
- **Source:** https://github.com/ferrants/skill-tts/tree/main/skills/tts

## Install

```sh
agentstack add skill-ferrants-skill-tts-tts
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Text to Speech

Turn text into an audio file via OpenAI's `/v1/audio/speech` endpoint. The
bundled script handles credentials, the API's 4096-character request limit,
markdown cleanup, and playback.

## Decide two things before generating

Neither of these is worth a clarifying question in the normal case — the
phrasing almost always tells you, and stopping to ask "should I play it?"
defeats the point of asking for audio in the first place. Read the intent, act,
and mention what you assumed in one line afterward.

### 1. Play it now, or just save it?

Listening and producing are different jobs, and guessing wrong is annoying in
both directions — unwanted audio startles, and a silent file when someone
wanted to listen means they have to go hunt for it.

| Signal | Action |
|---|---|
| "read this to me," "read it aloud," "say that out loud," "can I hear this," "listen to this," "what does X say," "my eyes are tired," "I'm driving" | Pass `--play` |
| "generate an audio file," "make an mp3," "create a voiceover," "render the narration," "I need audio for the video," or any named output path or downstream pipeline | Save only, no `--play` |
| "read it to me and save it" | Both — `--play` plus a durable path |

When it's genuinely ambiguous, ask which the request is *about*: consuming the
content right now leans play; producing an artifact leans save. A temp path is
fine for listening; anything meant to be kept goes somewhere durable
(`~/Downloads` when unspecified) and gets sent with SendUserFile rather than
just having its path printed.

### 2. Verbatim, or adapted for the ear?

Default to adapting. Text written to be read scans badly out loud — tables,
bullet fragments, headings, code blocks, and bare URLs are visual furniture that
turn into noise when spoken. Unless the exact wording is the point, rewrite the
content into what a person would actually say, then narrate that.

Read it **verbatim** when:

- The user says so — "exactly," "verbatim," "word for word," "as written," "don't change anything."
- The wording *is* the deliverable — ad copy, a voiceover script, a quote, legal or medical text, anything feeding a video pipeline where the script was already approved.
- They're evaluating their own prose. "Read me my draft" usually means they want
  to hear how *their* sentences land; paraphrasing hands them your writing
  instead and defeats the purpose.

Even in verbatim mode, don't literally speak the furniture. Announce it and move
on — "there's a table here comparing the three plans" — rather than reciting
cells, pipes, or a 60-character URL.

## Adapting text for the ear

The goal is what a knowledgeable person would say if asked about this out loud,
not a transcription of the page.

- **Tables** → the takeaway, not the cells. "Three of the four cases passed" beats reading every row.
- **Bullets** → connected prose with real transitions. "First… then… the last one is…"
- **Headings** → spoken transitions ("On pricing —") or dropped entirely.
- **Code, paths, URLs, IDs** → describe or skip. "There's a troubleshooting section in the docs," not `docs/tshoot.md#stdin`.
- **Visual references** → reword. "See the diagram above" and "click here" mean nothing to a listener.
- **Long or nested sentences** → split. A listener can't re-read a clause they lost.
- **Acronyms and jargon** → expand on first use.
- Lead with the point, and make it self-contained; there's no scrolling back.

**Example.** This section of a README:

```markdown
## Test results
| Case | Result |
|---|---|
| Short text | pass |
| Long text | pass |
| stdin | fail — exit 1 |

See `docs/troubleshooting.md#stdin` for details.
```

...becomes this, spoken:

> On the test results — short text and long text both passed. The stdin case
> failed with exit code one, and there's a troubleshooting section in the docs
> that covers it.

Write adapted text to a temp `.txt` file and use `--input-file` rather than
cramming it into `--text`; long strings full of apostrophes and quotes are a
shell-escaping minefield. Then say in one line that you adapted it, and offer
verbatim if they'd rather — some people want the original and will say so.

## Usage

`scripts/speak.py` is pure stdlib — run it with plain `python3`, no venv or
install step. It reads the API key from `~/.openai_credentials` by default.

```bash
python3 /scripts/speak.py \
  --text "Today is a wonderful day to build something people love." \
  --output ~/Downloads/speech.mp3 \
  --play
```

Long documents work the same way — point it at a file, or pipe on stdin:

```bash
python3 /scripts/speak.py --input-file ./post.md --output ./post.mp3
git log --oneline -20 | python3 /scripts/speak.py --output ./log.mp3
```

### Options

| Flag | Description | Default |
|------|-------------|---------|
| `--text` / `--input-file` | Source text. `--input-file -` reads stdin; stdin is also used automatically when piped. | — |
| `--output` | Where to write the audio (required). Parent dirs are created. | — |
| `--voice` | See voices below. | `cedar` |
| `--instructions` | How to perform the read — tone, pacing, emotion, accent, pronunciation. `gpt-4o-mini-tts` only. | — |
| `--model` | `gpt-4o-mini-tts`, `tts-1` (lower latency, lower quality), `tts-1-hd`. | `gpt-4o-mini-tts` |
| `--format` | `mp3`, `opus`, `aac`, `flac`, `wav`, `pcm`. Inferred from the output extension. | from extension |
| `--speed` | 0.25–4.0. See speed below. | `1.2` |
| `--play` | Play on local speakers after saving (ffplay/mpv/cvlc). | off |
| `--strip-markdown` / `--no-strip-markdown` | Force markdown cleanup on or off. On by default for `.md` files. | auto |
| `--jobs` | Concurrent requests when the text needs multiple chunks. | `4` |
| `--credentials` | Path to the key file. | `~/.openai_credentials` |

### Voices

`alloy`, `ash`, `ballad`, `coral`, `echo`, `fable`, `nova`, `onyx`, `sage`,
`shimmer`, `verse`, `marin`, `cedar`.

`cedar` is the default and `marin` is the other top-tier voice; both only exist
on `gpt-4o-mini-tts`. The older `tts-1` models support the first nine only, and
ignore `--instructions` entirely. If the user asks for a specific voice, use it;
otherwise don't interrogate them about voice choice — narrate with `cedar` and
offer to try another if they want a different feel.

### Speed

The default is `1.2`. A 1.0x read sounds sluggish for narration, and 20% faster
lands close to natural speaking pace without sounding rushed. Drop to `1.0` when
the pacing itself matters — an approved voiceover script, anything timed against
video, or a deliberately slow delivery.

The script gets there two different ways, which is invisible in use but worth
knowing if you're debugging a duration:

- `tts-1` / `tts-1-hd` apply `speed` server-side, so it's sent in the request.
- `gpt-4o-mini-tts` **silently ignores** the `speed` field — it returns 1.0x
  audio and no error. The script detects this and re-times the result locally
  with ffmpeg's `atempo` filter, which preserves pitch (no chipmunk effect).

Two consequences: any `--speed` other than `1.0` needs ffmpeg on the default
model, and the resulting file *is* sped up rather than merely played faster —
so it stays correct when handed to a video pipeline or any other player. If
ffmpeg is missing the script warns and leaves the audio at 1.0x rather than
failing outright.

## Getting a good read

`--instructions` is where the quality is. The model follows direction about
delivery the way a voice actor would, and a bare read with no direction sounds
flat and generic. Describe voice, tone, delivery, and any tricky pronunciations:

```bash
--instructions "Voice: warm and grounded, like a documentary narrator.
Tone: calm and confident, never salesy.
Delivery: unhurried, with a beat of pause after each key idea.
Pronunciation: say 'n8n' as 'en-eight-en'."
```

Two things worth knowing when you write instructions:

- **Punctuation is prosody.** The model takes its pacing cues from the text
  itself, so commas, periods, and paragraph breaks shape the rhythm more
  reliably than asking for "slower" in the instructions.
- **Spell out what would be misread.** Acronyms, product names, URLs, and
  version numbers get mangled ("TTS" as a word, "v1.2" as "vee one point two").
  Either respell them phonetically in the input text or name the pronunciation
  in `--instructions`.

## Long text

The API rejects input over 4096 characters. The script splits anything longer
on paragraph, then sentence, then word boundaries, synthesizes the pieces
concurrently, and joins them with ffmpeg — so a 6,000-word article is a single
command and takes seconds, not minutes. Two consequences worth remembering:

- ffmpeg must be installed for multi-chunk text. Single-chunk text has no
  external dependencies at all.
- `wav` and `pcm` can't be concatenated this way, so long text needs a
  compressed format. The script stops with a clear error rather than producing
  a corrupt file.

Markdown is stripped automatically for `.md` input: code fences and images are
dropped rather than read aloud, and link text is kept while the URL is
discarded. Pass `--no-strip-markdown` if the syntax itself matters.

## Length

Say what it will cost before generating something long — a 20-minute narration
is a real amount of audio, and someone who asked to "hear the gist" of a
40-page doc wants a two-minute summary, not the whole thing read out. Roughly
150 words per minute is a good estimate. When the source is long and the
request was about understanding it rather than reproducing it, adapt down to
the substance and say how long the result runs.

## Disclosure

OpenAI's usage policies require telling end users that a TTS voice is
AI-generated, not human. That matters when the audio is published or shown to
someone else — ads, videos, podcasts, voicemail greetings. Mention this when
output is headed somewhere public; it's not a concern for the user listening to
their own drafts.

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ferrants](https://github.com/ferrants)
- **Source:** [ferrants/skill-tts](https://github.com/ferrants/skill-tts)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-ferrants-skill-tts-tts
- Seller: https://agentstack.voostack.com/s/ferrants
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
