| name | agent-ttp |
| description | Use when turning long source material (articles, papers, notes, documents) into a listenable podcast-style audio file. Guides the agent to rewrite the source into a listening-first YAML script, validate it, and render it to MP3 with the agent-ttp CLI. Do NOT use for raw, mechanical text-to-speech of unedited text. |
agent-ttp: author a podcast script, then render it
agent-ttp splits the work in two:
- You (the agent) are the writer/producer. You read raw source material and rewrite it into a listening-first script.
- The CLI is the renderer/compiler. It deterministically turns your script into audio. It does not understand the source — it only renders what you write.
The two kinds of chunking (keep them separate)
- Semantic chunking — your job. Break the meaning into coherent spoken segments with clear intent and natural transitions. This is editorial work.
- Technical chunking — the CLI's job. If a segment exceeds the model's input limit, the CLI splits it on sentence boundaries and stitches the audio back. You never do this.
Workflow
When the user hands you a file, treat it as source material to rewrite (unless it is already a valid script — then just validate and render).
- Read the source in full.
- Rewrite into spoken prose: drop the original paragraph structure, headings, lists, tables, and citations.
- Segment into coherent blocks — one spoken beat with a single intent each.
- Add delivery metadata:
speaker, intent, pause_after_ms, and per-block instructions where delivery should change.
- Write the script as a YAML file (schema below).
- Validate — fix every error, review warnings.
- Render, then return the output path to the user.
Writing guidance
Structure the episode — don't just transcribe.
- Hook (one segment,
intent: hook): the most interesting idea or question, in plain language, before any setup.
- Orient (one or two segments): why this matters and what the listener will get.
- Body: one coherent beat per segment, ordered so each builds on the last.
- Close: a brief recap or takeaway, not an abrupt stop.
Length and pacing.
- Aim for segments of roughly 30–90 seconds — a few hundred words in English, or ~300–900 Chinese characters.
validate warns past ~1200 characters; treat that as "split this beat."
- Use
pause_after_ms to breathe: ~300–500 ms between beats, ~700–1000 ms at a topic shift or before a punchline. Don't pause after every sentence — the model paces within a segment.
Write for the ear, not the eye.
- No markdown, tables, code fences, bare URLs, or "(see figure 3)".
validate warns on these; fix by rewriting as spoken prose.
- Normalize anything that reads badly aloud. Say it the way you'd speak it: numbers and units ("ninety-five percent", not "95%"), symbols (
%, &, /, $, →), and dates ("twenty twenty-four"). Expand acronyms on first use. Don't make the model guess e.g., Q3, or $4.2B.
- Turn structure into speech: a list becomes "three things — first… second… and finally…"; a table becomes a sentence comparing the rows that matter.
- Every segment carries information or moves the thread forward — cut filler.
Voices and delivery.
- For a two-person dialogue, define two voices and alternate
speaker across segments. The block list is the conversation; pauses give turn-taking.
- Use
instructions to steer delivery (tone, pace, a rising question) on segments that need it. To slow a passage down, say so there ("speak slowly and clearly") — there is no separate speed knob.
Script schema (YAML only)
title: "Episode title"
language: "zh-CN"
style: "calm, dense, explanatory"
model: "gpt-4o-mini-tts-2025-12-15"
max_chars: 2000
voices:
host:
voice: cedar
instructions: "Calm, knowledge-focused Mandarin."
guest:
voice: marin
instructions: "Thoughtful podcast co-host."
segments:
- id: intro
speaker: host
intent: hook
pause_after_ms: 700
text: >
Spoken prose for this block.
- id: example
speaker: host
language: "es"
instructions: "Ask as a genuine, curious question."
text: >
...
Parameter cascade (most-specific wins): a segment's model / instructions / language override the voice's, which override the script-level defaults. speaker binds the segment to a voice.
language is per-segment. The API has no language parameter, so the resolved language is carried as a "Speak in …" clause appended to the delivery instructions — applied even when you also give explicit instructions. Common BCP-47 tags are rendered as natural-language names the model follows more reliably (zh-CN → "Speak in Mandarin Chinese.", en-US → "Speak in American English."); an unmapped tag falls back to "Speak in <tag>." Just write the tag — the CLI handles the phrasing. This is what makes language-learning episodes work: an English explanation block and a Spanish example block render each in its own language (see examples/bilingual-language-learning.yaml).
CLI reference
The published package is @spicadust/agent-ttp, so invoke it with the scoped name: npx @spicadust/agent-ttp <cmd>. (The first run downloads it; thereafter npx uses its cache.) Inside this repo you can instead run npx tsx src/cli.ts <cmd>.
npx @spicadust/agent-ttp validate script.yaml
npx @spicadust/agent-ttp validate script.yaml --json
npx @spicadust/agent-ttp render script.yaml -o episode.mp3
npx @spicadust/agent-ttp render script.yaml -o episode.wav
npx @spicadust/agent-ttp api-key set
npx @spicadust/agent-ttp api-key status
npx @spicadust/agent-ttp api-key unset
Output format follows the -o extension: .mp3 (default) or .wav. Worked examples live in examples/: script.yaml (two-voice dialogue), en-article-briefing.yaml (single narrator), zh-paper-summary.yaml (Mandarin explainer), and bilingual-language-learning.yaml (per-segment language override).
When something's missing
-
No Node / npx: the CLI needs Node ≥ 20. If it isn't installed, tell the user to install it — don't improvise a workaround. (Inside this repo you can instead run npx tsx src/cli.ts <cmd>.)
-
No OpenAI API key: only render needs one — validate always runs key-free and its report shows api key: MISSING. When no key is found, render fails before any network call with:
No OpenAI API key found. Run agent-ttp api-key set, set OPENAI_API_KEY, or add OPENAI_API_KEY to .env.
The key is resolved in this order, first match wins:
--api-key sk-... flag on render
OPENAI_API_KEY environment variable
OPENAI_API_KEY in a .env in the current directory
- stored config via
npx @spicadust/agent-ttp api-key set (~/.agent-ttp/config.json)
Never invent, hardcode, or guess a key, and don't pass --api-key with a placeholder. Ask the user to provide one through any of the above, confirm with api-key status, then re-run render. A validate-clean script needs no rework once the key is in place.
-
A render interrupted partway re-uses already-synthesized segments from the cache on the next run, so retries are cheap.