Install
$ agentstack add skill-ferrants-skill-tts-tts ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →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:
## 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.
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:
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-hdapplyspeedserver-side, so it's sent in the request.gpt-4o-mini-ttssilently ignores thespeedfield — 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:
--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.
wavandpcmcan'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
- Source: ferrants/skill-tts
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.