Direct a script for text-to-speech so it sounds like a real
TTS director: write for the ear, tag the beats, test by ear

Direct a script for text-to-speech so it sounds like a real, engaged human, not a narrator reading. Persian (Farsi) first, English supported.
--- name: tts-director description: Direct a script for text-to-speech so it sounds like a real, engaged human, not a narrator reading. Persian (Farsi) first, English supported. It rewrites the text for the ear, maps the beats of a short-form video (reel, TikTok, Short), and adds ElevenLabs v3/v4 audio tags only where they earn their place ([excited], [whispers], [laughs], [chuckles], [gasps], [sighs], [sarcastic]...). It checks for over-tagging with a free linter and A/B-tests risky lines on the real voice before a full run. It also covers model rules (eleven_v4 ignores speed and style; multilingual_v2, flash and turbo read tags aloud) and converts the result for other TTS engines (OpenAI, Azure, Google). Use for ANY script headed to ElevenLabs or another TTS, and whenever someone wants a voice that is more exciting, emotional, expressive, human or viral, or asks about tone, emotion, laughter, pauses or pacing in TTS: «لحن»، «احساس»، «هیجان»، «خنده»، «تگ»، «انسانیتر»، «گوینده»، «صداگذاری». --- # TTS director: write for the ear, tag the beats, test by ear A great voiceover is about 80% text and 20% tags. The text sets the rhythm, the register and where the energy goes. Tags only push a delivery the text has already set up. A script with a tag on every line sounds like an over-acted ad and loses the viewer. A script with none often sounds read. This skill does not replace your TTS pipeline. It prepares the text that goes into it. Scripts are stdlib Python 3.9+; ffmpeg/ffprobe are optional and give durations. ## Workflow Do the steps in order. The tools check them, so none can be skipped silently: - `tag_lint.py` gives an ERROR when a line has no beat label; - `tag_lint.py --check-approved` fails until the person has approved the exact text. Gate your TTS call on it. Before step 1, read the delivery reference for the script's language (`references/persian-delivery.md` or `references/english-delivery.md`) and `references/audio-tags.md`. The linter checks only part of them.
Overview
A great voiceover is about 80% text and 20% tags. The text sets the rhythm, the register and where the energy goes. Tags only push a delivery the text has already set up. A script with a tag on every line sounds like an over-acted ad and loses the viewer. A script with none often sounds read.
Manual install (npx, ZIP)
npx skills add aligh98/tts-director -g -a claude-codeConnects with Agent Alley MCP · agentalley.io/mcp
Files included
SKILL.md13.9 KB
Read SKILL.md here
aligh98/
Show the full text186 lines
TTS director: write for the ear, tag the beats, test by ear
A great voiceover is about 80% text and 20% tags. The text sets the rhythm, the register and where the energy goes. Tags only push a delivery the text has already set up. A script with a tag on every line sounds like an over-acted ad and loses the viewer. A script with none often sounds read.
This skill does not replace your TTS pipeline. It prepares the text that goes into it. Scripts are stdlib Python 3.9+; ffmpeg/ffprobe are optional and give durations.
Workflow
Do the steps in order. The tools check them, so none can be skipped silently:
tag_lint.pygives an ERROR when a line has no beat label;tag_lint.py --check-approvedfails until the person has approved the exact text. Gate your TTS call on it.
Before step 1, read the delivery reference for the script's language (references/persian-delivery.md or references/english-delivery.md) and references/audio-tags.md. The linter checks only part of them.
Beat map, inside the script. End every spoken line with « # ROLE ENERGY» (energy 1–5):
[excited] Wait, this ends Friday... and it's not coming back! # HOOK 5 [whispers] Here's the trick... # TIP 2The roles are:
- HOOK
- SETUP
- REVEAL
- ITEM (a list point)
- TIP (insider advice)
- JOKE or IRONY
- WARNING
- CTA
Plan the arc before touching words. See Viral arc. Strip the labels before the text goes to the TTS:
tag_lint.strip_beats(text)does it, or remove everything from « #» to the end of each line. For a script that is not a short-form video, pass--no-beats.Rewrite for the ear. Keep the meaning and every fact. Rewriting means:
- shorter sentences;
- the punch word moved to the end;
- the interjections a native speaker would actually use;
- punctuation that marks breaths.
For Persian, follow the alef rule in
persian-delivery.md: the voice reads every «ا» as a long «آ», and a short /a/ is a fatha only. Some words are spelled unlike they sound («بالاخره»، «حتی»، «فوقالعاده»; seereferences/fa-traps.tsv). Treat a respelling as a guess until a human confirms it by ear; mark confirmed rows in your lexicon with a note that starts with «✓».Tag pass. Tag only the beats the text alone cannot carry, within the Tag budget. For tag names and what each one is for, see
references/audio-tags.md.Lint (free):
python scripts/tag_lint.py SCRIPT --model eleven_v4 --tempo 1.15 [--lexicon words.tsv] [--ab-budget 160]It prints the beat map with estimated times, then checks:
- Beats and arc:
- a missing or invalid label (ERROR);
- a first line that is not a HOOK at energy 4–5 (ERROR);
- a TIP above energy 3;
- a flat arc;
- more than 14 s without a change of delivery;
- a low tag on a high beat, or a high tag on a low beat;
- a whisper or sigh not followed by a line that lifts the energy.
- Tags:
- over-tagging;
- laughs that are too many or too close together;
- a repeated tag;
- staging tags;
- SSML on v3/v4, and tags on models that read them aloud.
- Ear rewrite:
- written register («را»، «است»، «میشود»; uncontracted English);
- fewer than 2 interjections, or a repeated one;
- «!» on every line, and too many ellipses;
- long sentences;
- written laughter («هاها», "haha");
- stretched letters;
- an opening greeting.
- Persian readings:
- unconfirmed respellings that change the number of «ا/آ»;
{written|spoken}overrides that do the same;- trap words;
- tanwin.
Fix every ERROR. Keep a WARN only when you can say why. The A/B PLAN at the end lists the lines to test, in priority order, with their cost; see A/B priority.
- Beats and arc:
Approval. Show the person the full text with its beat map and the A/B plan. Generating audio costs them credits, and the wording is theirs to approve. After their OK:
python scripts/tag_lint.py SCRIPT --approveThis writes
SCRIPT.approved.json, the SHA-256 of the stripped file text.--approverefuses while there is an ERROR. Before every TTS call, runtag_lint.py SCRIPT --check-approved: exit 0 means approved, exit 3 means no stamp or a changed text. Any edit needs a new OK. A pipeline can also compare the hash itself:sha256(file_text.strip()). Choosing an A/B take does not approve the whole text.A/B the risky lines in the A/B plan's order. Check the cost first;
--dry-runsends nothing:python scripts/ab.py "VARIANT A" "VARIANT B" --voice VOICE_ID --stability 0.35 --stability 0.5 --prev "line before" --next "line after" --out probe/ab --dry-runRemove
--dry-runto generate. Every clip uses the same--seed, so the differences come from the text and the settings, not from chance. Each clip is transcribed:LEAKmeans a tag was read aloud as a word. Drop or rephrase that tag.- An event such as
[laughter]or[خنده]means the sound was performed. LISTEN FOR(Persian) names the words whose reading is unconfirmed.
Speech-to-text cannot judge excitement or vowel length, so let a human ear pick the winner. If the chosen take changes the text, lint and approve again.
Generate the full script with the chosen variants and settings, after
--check-approvedpasses. Then:- Set the pacing with
ffmpeg -af atempo=1.1(1.10–1.20 keeps pitch), not with a speed setting. If you have word timings, divide them by the same factor. - To fix one line, regenerate only that chunk with
previous_text/next_textand splice it in.
- Set the pacing with
Tag budget
Never over-tag. Every tag must help retention or make the voice more human. A tag that repeats what the text already says is noise.
Density: about 1 tag per 3–4 sentences, which is 4–8 tags in a 60–90 s reel. The linter errors above 0.5 tags per sentence and warns above 0.34.
Baseline energy is not a tag. The voice, a stability of 0.3–0.4 and punchy text set the overall energy. Do not put
[excited]on every line. Save it for 2–3 peaks: the hook, the big reveal and the CTA push.Laughs: 0 to 2 per reel, and never forced. Laugh only where a person would:
- after a real joke or irony;
- on a self-aware aside;
- for relief after a tense point.
Never laugh on numbers, dates, prices, the offer itself or the CTA keyword. Keep laughs 20 s or more apart. Pick the tag by size:
[chuckles]is the safe, light one;[laughs]is a clear laugh; use[laughs harder]for one big moment at most.Contrast beats intensity. Attention comes from change. Use one change of delivery every 8–12 s; this is the pattern interrupt. For example:
- a whisper after an energetic line;
- a pause before a reveal;
- a sigh before a pain point;
- a gasp before an unbelievable fact.
One or two tags at a spot, never three. Put the tag right before the words it colours. Its effect runs to about the end of that sentence.
Bring the energy back after
[whispers]or[sighs]. Do it on the next line with the text (a «!», a short punch) or with a delivery tag. Check it by ear.Keep clarity lines clean. Numbers, dates, prices, product names and the CTA keyword are said plainly. A warm tag may come before the CTA sentence, never inside the keyword.
Never tag staging or music (
[music],[standing]), and never tag sensitive topics such as grief, illness or tragedy.
A/B priority
Test credits are often limited. Spend them in this order; the linter's A/B PLAN lists the lines the same way, and --ab-budget N marks what fits:
- The hook. It decides the watch. Test two variants: a tag against none, or two wordings.
- The first use of each risky tag: whisper, sigh, laugh, sarcasm, gasp. A tag you have already tested on this voice goes lower.
- Unconfirmed readings (Persian): test the word alone, which costs about 15 characters.
- The lift after a low tag: the line after a whisper or sigh, with
--prevset to the low line. Does the voice come back up? - The CTA, unless the person already approved it word for word.
When a line does not fit, shorten the clip to the risky words alone; --prev/--next give the context and are not spoken. Never skip a level-1 or level-2 item to test a lower one. Tell the person what was not tested.
Viral arc
For a 60–90 s vertical video:
| Time | Beat | Energy | Typical device |
|---|---|---|---|
| 0–3 s | HOOK | 5 | Urgency or curiosity in the first words: an interjection, a bold claim, a question, [gasps] or [excited]. No greeting. |
| 3–10 s | SETUP | 4 | Why it matters now: a deadline, or what they lose if they skip it. Short and fast. |
| every 8–12 s | INTERRUPT | ± | A change of delivery: a whisper, a sigh, a chuckle, a «...» pause, a rhetorical question. |
| list | ITEM | 4 | A punchy numbered title, then one line of reason. Don't tag every item, and vary the openers. |
| ~70% | TIP | 3, intimate | [whispers] plus "here's the trick...", then back up. Lower energy here reads as a secret and lifts retention. |
| end | CTA | 4, warm | Clear and friendly, with the keyword said unhurried. A smile in the voice; one light tag at most. |
For long-form narration (an audiobook, an explainer), halve the tag budget and drop the pattern-interrupt rule. Let the text carry it.
Settings (ElevenLabs)
- eleven_v4 is ElevenLabs' recommended model. It reads only
stabilityandsimilarity_boost; its help page says Style and Speed are not available. A test on 2026-11-03 confirmed this: speed 0.8 and 1.2 gave clips of 6.32 s and 6.40 s. Set the pace with short sentences and fewer commas, thenatempo. - Stability: lower means more expressive delivery and closer tag-following, but takes vary more from one to the next.
- 0.3–0.4: hype reels.
- 0.5: neutral narration.
- Above 0.7: tags are followed weakly.
- Below about 0.25: takes can wobble, so A/B first.
- eleven_v3: the same tags work. The ElevenLabs UI names three stability presets: Creative (low), Natural (0.5) and Robust (high, follows tags weakly). If the API rejects a value, use 0.0, 0.5 or 1.0.
- No SSML on v3/v4.
<break>is not supported. Use «...» or «—» for pauses, «،»/«,» for a breath, and[short pause]when punctuation is not enough. - multilingual_v2, flash, turbo: these read
[tags]aloud, so strip every tag. Steer withstyle(0–1),speed(0.7–1.2) and<break time="0.5s"/>(3 s at most, and only a few). - Chunks: when you generate part of a script, send
previous_text/next_textso the energy and intonation join up. That context is not spoken. - Seed:
seedmakes sampling close to repeatable, though not guaranteed. Fix it for A/B tests.
Other TTS engines
Convert the directed script into whatever the engine accepts:
- OpenAI
gpt-4o-mini-tts: no inline tags, so remove them. Put the delivery into theinstructionsfield, for example: "energetic young narrator, fast, smiling, one short laugh after the joke". - Azure or Google SSML: use
<break>,<prosody rate pitch>and<emphasis>. Use Azure's<mstts:express-as style>only on voices that list that style; check the voice's style list first. - Prompt-driven engines (e.g. Gemini TTS): a natural-language style prompt plus the clean text.
Whatever the engine, the step 2 rewrite does most of the work.
Calibrate for a new voice
Tag behaviour and speaking rate depend on the voice. Once per voice:
- Run
tag_lint.py SCRIPT --calibrate AUDIO.mp3. It measures the voice's real characters per second from one finished take, for free. Pass that number as--cpsfrom then on. - A/B the tags you plan to use with
ab.py. Record what works in the tested-voices table inreferences/audio-tags.md.
Files
| Path | What |
|---|---|
references/persian-delivery.md |
Persian for the ear: colloquial register, interjections, punctuation as a score, emphasis without capitals, before/after examples |
references/english-delivery.md |
The same for English |
references/audio-tags.md |
Tag catalogue by job, tested-voices table, what never to tag |
references/fa-traps.tsv |
Persian words spelled unlike they sound (Arabic «ال», final «ی» said «ا»); the reading checks warn on them |
scripts/tag_lint.py |
Free checker: required beat labels and arc, ear-rewrite rules, tags, model errors, Persian readings, A/B plan, --approve / --check-approved, --calibrate |
scripts/ab.py |
Variants × stability on the real voice with a fixed seed, STT leak and event detection, LISTEN FOR, --dry-run, --lexicon |
scripts/reading.py |
Shared lexicon loading (✓ = confirmed by ear), respelling and the Persian reading checks |
examples/ |
A directed Persian and English reel script, with beat labels |
| 1 | |
| 2 | name tts-director |
| 3 | description Direct a script for text-to-speech so it sounds like a real, engaged human, not a narrator reading. Persian (Farsi) first, English supported. It rewrites the text for the ear, maps the beats of a short-form video (reel, TikTok, Short), and adds ElevenLabs v3/v4 audio tags only where they earn their place ([excited], [whispers], [laughs], [chuckles], [gasps], [sighs], [sarcastic]...). It checks for over-tagging with a free linter and A/B-tests risky lines on the real voice before a full run. It also covers model rules (eleven_v4 ignores speed and style; multilingual_v2, flash and turbo read tags aloud) and converts the result for other TTS engines (OpenAI, Azure, Google). Use for ANY script headed to ElevenLabs or another TTS, and whenever someone wants a voice that is more exciting, emotional, expressive, human or viral, or asks about tone, emotion, laughter, pauses or pacing in TTS: «لحن»، «احساس»، «هیجان»، «خنده»، «تگ»، «انسانیتر»، «گوینده»، «صداگذاری». |
| 4 | |
| 5 | |
| 6 | # TTS director: write for the ear, tag the beats, test by ear |
| 7 | |
| 8 | A great voiceover is about 80% text and 20% tags. The text sets the rhythm, the register and where the energy goes. Tags only push a delivery the text has already set up. A script with a tag on every line sounds like an over-acted ad and loses the viewer. A script with none often sounds read. |
| 9 | |
| 10 | This skill does not replace your TTS pipeline. It prepares the text that goes into it. Scripts are stdlib Python 3.9+; ffmpeg/ffprobe are optional and give durations. |
| 11 | |
| 12 | ## Workflow |
| 13 | |
| 14 | Do the steps in order. The tools check them, so none can be skipped silently: |
| 15 | `tag_lint.py` gives an ERROR when a line has no beat label; |
| 16 | `tag_lint.py --check-approved` fails until the person has approved the exact text. Gate your TTS call on it. |
| 17 | |
| 18 | Before step 1, read the delivery reference for the script's language (`references/persian-delivery.md` or `references/english-delivery.md`) and `references/audio-tags.md`. The linter checks only part of them. |
| 19 | |
| 20 | **Beat map, inside the script.** End every spoken line with « # ROLE ENERGY» (energy 1–5): |
| 21 | |
| 22 | [excited] Wait, this ends Friday... and it's not coming back! # HOOK 5 |
| 23 | [whispers] Here's the trick... # TIP 2 |
| 24 | |
| 25 | The roles are: |
| 26 | HOOK |
| 27 | SETUP |
| 28 | REVEAL |
| 29 | ITEM (a list point) |
| 30 | TIP (insider advice) |
| 31 | JOKE or IRONY |
| 32 | WARNING |
| 33 | CTA |
| 34 | |
| 35 | Plan the arc before touching words. See [Viral arc]. Strip the labels before the text goes to the TTS: `tag_lint.strip_beats(text)` does it, or remove everything from « #» to the end of each line. For a script that is not a short-form video, pass `--no-beats`. |
| 36 | **Rewrite for the ear.** Keep the meaning and every fact. Rewriting means: |
| 37 | shorter sentences; |
| 38 | the punch word moved to the end; |
| 39 | the interjections a native speaker would actually use; |
| 40 | punctuation that marks breaths. |
| 41 | |
| 42 | For Persian, follow the **alef rule** in `persian-delivery.md`: the voice reads every «ا» as a long «آ», and a short /a/ is a fatha only. Some words are spelled unlike they sound («بالاخره»، «حتی»، «فوقالعاده»; see `references/fa-traps.tsv`). Treat a respelling as a guess until a human confirms it by ear; mark confirmed rows in your lexicon with a note that starts with «✓». |
| 43 | **Tag pass.** Tag only the beats the text alone cannot carry, within the [Tag budget]. For tag names and what each one is for, see `references/audio-tags.md`. |
| 44 | **Lint (free):** |
| 45 | |
| 46 | python scripts/tag_lint.py SCRIPT --model eleven_v4 --tempo 1.15 [--lexicon words.tsv] [--ab-budget 160] |
| 47 | |
| 48 | It prints the beat map with estimated times, then checks: |
| 49 | **Beats and arc:** |
| 50 | a missing or invalid label (ERROR); |
| 51 | a first line that is not a HOOK at energy 4–5 (ERROR); |
| 52 | a TIP above energy 3; |
| 53 | a flat arc; |
| 54 | more than 14 s without a change of delivery; |
| 55 | a low tag on a high beat, or a high tag on a low beat; |
| 56 | a whisper or sigh not followed by a line that lifts the energy. |
| 57 | **Tags:** |
| 58 | over-tagging; |
| 59 | laughs that are too many or too close together; |
| 60 | a repeated tag; |
| 61 | staging tags; |
| 62 | SSML on v3/v4, and tags on models that read them aloud. |
| 63 | **Ear rewrite:** |
| 64 | written register («را»، «است»، «میشود»; uncontracted English); |
| 65 | fewer than 2 interjections, or a repeated one; |
| 66 | «!» on every line, and too many ellipses; |
| 67 | long sentences; |
| 68 | written laughter («هاها», "haha"); |
| 69 | stretched letters; |
| 70 | an opening greeting. |
| 71 | **Persian readings:** |
| 72 | unconfirmed respellings that change the number of «ا/آ»; |
| 73 | `{written|spoken}` overrides that do the same; |
| 74 | trap words; |
| 75 | tanwin. |
| 76 | |
| 77 | Fix every ERROR. Keep a WARN only when you can say why. The **A/B PLAN** at the end lists the lines to test, in priority order, with their cost; see [A/B priority]. |
| 78 | **Approval.** Show the person the full text with its beat map and the A/B plan. Generating audio costs them credits, and the wording is theirs to approve. After their OK: |
| 79 | |
| 80 | python scripts/tag_lint.py SCRIPT --approve |
| 81 | |
| 82 | This writes `SCRIPT.approved.json`, the SHA-256 of the stripped file text. `--approve` refuses while there is an ERROR. Before every TTS call, run `tag_lint.py SCRIPT --check-approved`: exit 0 means approved, exit 3 means no stamp or a changed text. Any edit needs a new OK. A pipeline can also compare the hash itself: `sha256(file_text.strip())`. Choosing an A/B take does not approve the whole text. |
| 83 | **A/B the risky lines** in the A/B plan's order. Check the cost first; `--dry-run` sends nothing: |
| 84 | |
| 85 | python scripts/ab.py "VARIANT A" "VARIANT B" --voice VOICE_ID --stability 0.35 --stability 0.5 --prev "line before" --next "line after" --out probe/ab --dry-run |
| 86 | |
| 87 | Remove `--dry-run` to generate. Every clip uses the same `--seed`, so the differences come from the text and the settings, not from chance. Each clip is transcribed: |
| 88 | `LEAK` means a tag was read aloud as a word. Drop or rephrase that tag. |
| 89 | An event such as `[laughter]` or `[خنده]` means the sound was performed. |
| 90 | `LISTEN FOR` (Persian) names the words whose reading is unconfirmed. |
| 91 | |
| 92 | Speech-to-text cannot judge excitement or vowel length, so let a human ear pick the winner. If the chosen take changes the text, lint and approve again. |
| 93 | **Generate the full script** with the chosen variants and settings, after `--check-approved` passes. Then: |
| 94 | Set the pacing with `ffmpeg -af atempo=1.1` (1.10–1.20 keeps pitch), not with a speed setting. If you have word timings, divide them by the same factor. |
| 95 | To fix one line, regenerate only that chunk with `previous_text`/`next_text` and splice it in. |
| 96 | |
| 97 | ## Tag budget |
| 98 | |
| 99 | **Never over-tag.** Every tag must help retention or make the voice more human. A tag that repeats what the text already says is noise. |
| 100 | |
| 101 | **Density:** about 1 tag per 3–4 sentences, which is 4–8 tags in a 60–90 s reel. The linter errors above 0.5 tags per sentence and warns above 0.34. |
| 102 | **Baseline energy is not a tag.** The voice, a stability of 0.3–0.4 and punchy text set the overall energy. Do not put `[excited]` on every line. Save it for 2–3 peaks: the hook, the big reveal and the CTA push. |
| 103 | **Laughs: 0 to 2 per reel, and never forced.** Laugh only where a person would: |
| 104 | after a real joke or irony; |
| 105 | on a self-aware aside; |
| 106 | for relief after a tense point. |
| 107 | |
| 108 | Never laugh on numbers, dates, prices, the offer itself or the CTA keyword. Keep laughs 20 s or more apart. Pick the tag by size: `[chuckles]` is the safe, light one; `[laughs]` is a clear laugh; use `[laughs harder]` for one big moment at most. |
| 109 | **Contrast beats intensity.** Attention comes from change. Use one change of delivery every 8–12 s; this is the pattern interrupt. For example: |
| 110 | a whisper after an energetic line; |
| 111 | a pause before a reveal; |
| 112 | a sigh before a pain point; |
| 113 | a gasp before an unbelievable fact. |
| 114 | **One or two tags at a spot, never three.** Put the tag right before the words it colours. Its effect runs to about the end of that sentence. |
| 115 | **Bring the energy back after `[whispers]` or `[sighs]`.** Do it on the next line with the text (a «!», a short punch) or with a delivery tag. Check it by ear. |
| 116 | **Keep clarity lines clean.** Numbers, dates, prices, product names and the CTA keyword are said plainly. A warm tag may come before the CTA sentence, never inside the keyword. |
| 117 | **Never tag staging or music** (`[music]`, `[standing]`), and never tag sensitive topics such as grief, illness or tragedy. |
| 118 | |
| 119 | ## A/B priority |
| 120 | |
| 121 | Test credits are often limited. Spend them in this order; the linter's A/B PLAN lists the lines the same way, and `--ab-budget N` marks what fits: |
| 122 | **The hook.** It decides the watch. Test two variants: a tag against none, or two wordings. |
| 123 | **The first use of each risky tag**: whisper, sigh, laugh, sarcasm, gasp. A tag you have already tested on this voice goes lower. |
| 124 | **Unconfirmed readings** (Persian): test the word alone, which costs about 15 characters. |
| 125 | **The lift after a low tag**: the line after a whisper or sigh, with `--prev` set to the low line. Does the voice come back up? |
| 126 | **The CTA**, unless the person already approved it word for word. |
| 127 | |
| 128 | When a line does not fit, shorten the clip to the risky words alone; `--prev`/`--next` give the context and are not spoken. Never skip a level-1 or level-2 item to test a lower one. Tell the person what was not tested. |
| 129 | |
| 130 | ## Viral arc |
| 131 | |
| 132 | For a 60–90 s vertical video: |
| 133 | |
| 134 | | Time | Beat | Energy | Typical device | |
| 135 | | --- | --- | --- | --- | |
| 136 | | 0–3 s | HOOK | 5 | Urgency or curiosity in the first words: an interjection, a bold claim, a question, `[gasps]` or `[excited]`. No greeting. | |
| 137 | | 3–10 s | SETUP | 4 | Why it matters now: a deadline, or what they lose if they skip it. Short and fast. | |
| 138 | | every 8–12 s | INTERRUPT | ± | A change of delivery: a whisper, a sigh, a chuckle, a «...» pause, a rhetorical question. | |
| 139 | | list | ITEM | 4 | A punchy numbered title, then one line of reason. Don't tag every item, and vary the openers. | |
| 140 | | ~70% | TIP | 3, intimate | `[whispers]` plus "here's the trick...", then back up. Lower energy here reads as a secret and lifts retention. | |
| 141 | | end | CTA | 4, warm | Clear and friendly, with the keyword said unhurried. A smile in the voice; one light tag at most. | |
| 142 | |
| 143 | For long-form narration (an audiobook, an explainer), halve the tag budget and drop the pattern-interrupt rule. Let the text carry it. |
| 144 | |
| 145 | ## Settings (ElevenLabs) |
| 146 | |
| 147 | **eleven_v4** is ElevenLabs' recommended model. It reads only `stability` and `similarity_boost`; its help page says **Style and Speed are not available**. A test on 2026-11-03 confirmed this: speed 0.8 and 1.2 gave clips of 6.32 s and 6.40 s. Set the pace with short sentences and fewer commas, then `atempo`. |
| 148 | **Stability:** lower means more expressive delivery and closer tag-following, but takes vary more from one to the next. |
| 149 | 0.3–0.4: hype reels. |
| 150 | 0.5: neutral narration. |
| 151 | Above 0.7: tags are followed weakly. |
| 152 | Below about 0.25: takes can wobble, so A/B first. |
| 153 | **eleven_v3:** the same tags work. The ElevenLabs UI names three stability presets: Creative (low), Natural (0.5) and Robust (high, follows tags weakly). If the API rejects a value, use 0.0, 0.5 or 1.0. |
| 154 | **No SSML on v3/v4.** `<break>` is not supported. Use «...» or «—» for pauses, «،»/«,» for a breath, and `[short pause]` when punctuation is not enough. |
| 155 | **multilingual_v2, flash, turbo:** these read `[tags]` aloud, so strip every tag. Steer with `style` (0–1), `speed` (0.7–1.2) and `<break time="0.5s"/>` (3 s at most, and only a few). |
| 156 | **Chunks:** when you generate part of a script, send `previous_text`/`next_text` so the energy and intonation join up. That context is not spoken. |
| 157 | **Seed:** `seed` makes sampling close to repeatable, though not guaranteed. Fix it for A/B tests. |
| 158 | |
| 159 | ## Other TTS engines |
| 160 | |
| 161 | Convert the directed script into whatever the engine accepts: |
| 162 | **OpenAI `gpt-4o-mini-tts`:** no inline tags, so remove them. Put the delivery into the `instructions` field, for example: "energetic young narrator, fast, smiling, one short laugh after the joke". |
| 163 | **Azure or Google SSML:** use `<break>`, `<prosody rate pitch>` and `<emphasis>`. Use Azure's `<mstts:express-as style>` only on voices that list that style; check the voice's style list first. |
| 164 | **Prompt-driven engines** (e.g. Gemini TTS): a natural-language style prompt plus the clean text. |
| 165 | |
| 166 | Whatever the engine, the step 2 rewrite does most of the work. |
| 167 | |
| 168 | ## Calibrate for a new voice |
| 169 | |
| 170 | Tag behaviour and speaking rate depend on the voice. Once per voice: |
| 171 | Run `tag_lint.py SCRIPT --calibrate AUDIO.mp3`. It measures the voice's real characters per second from one finished take, for free. Pass that number as `--cps` from then on. |
| 172 | A/B the tags you plan to use with `ab.py`. Record what works in the tested-voices table in `references/audio-tags.md`. |
| 173 | |
| 174 | ## Files |
| 175 | |
| 176 | | Path | What | |
| 177 | | --- | --- | |
| 178 | | `references/persian-delivery.md` | Persian for the ear: colloquial register, interjections, punctuation as a score, emphasis without capitals, before/after examples | |
| 179 | | `references/english-delivery.md` | The same for English | |
| 180 | | `references/audio-tags.md` | Tag catalogue by job, tested-voices table, what never to tag | |
| 181 | | `references/fa-traps.tsv` | Persian words spelled unlike they sound (Arabic «ال», final «ی» said «ا»); the reading checks warn on them | |
| 182 | | `scripts/tag_lint.py` | Free checker: required beat labels and arc, ear-rewrite rules, tags, model errors, Persian readings, A/B plan, `--approve` / `--check-approved`, `--calibrate` | |
| 183 | | `scripts/ab.py` | Variants × stability on the real voice with a fixed seed, STT leak and event detection, `LISTEN FOR`, `--dry-run`, `--lexicon` | |
| 184 | | `scripts/reading.py` | Shared lexicon loading (✓ = confirmed by ear), respelling and the Persian reading checks | |
| 185 | | `examples/` | A directed Persian and English reel script, with beat labels | |
| 186 |
Details
- License
- MIT
- Stars on the repo
- ★ 75
- Checked
- Source
- aligh98/tts-director ↗
- Creator
- aligh98 · 1 listing
Similar skills
See all →
SkillReverse engineer native, managed, Electron/JavaScriptREA
SkillBrainstorms ideas into designs before any creative workBrainstorming Ideas Into Designs
SkillRequires technical rigor and verificationCode Review Reception
SkillDiagnosis loop for hard bugs and performance regressionsDiagnosing bugs
SkillImplement the result of /to-spec and /to-tickets in codeImplement spec
SkillWriting a PR body