microsoft/generating-podcast-script
Use this skill whenever the user asks to write, generate, or create a podcast script or podcast episode — from a topic, or from source material such as a news digest, newsletter, email review, or set of articles — and optionally convert it to audio with Azure Text-to-Speech. Handles the initial request and every follow-up refinement (source, topic, length, cast, narration) in the same task.
npx skills add https://github.com/microsoft/cat-agent-skills --skill generating-podcast-script
Produces a two-host, NotebookLM-style conversational episode: a readable script
plus a multi-voice SSML document ready for Azure Text-to-Speech.
Ask only for what is missing. Do not ask for optional fields that have safe
defaults.
| Input | Required | Default |
|---|---|---|
| source or topic | Yes | — (source material such as a digest/newsletter/articles, or a plain topic) |
| duration | No | medium — short ≈ 3 min / ~450 words, medium ≈ 6 min / ~900 words, long ≈ 12 min / ~1,800 words |
| language | No | English |
| cast | No | Two hosts, NOVA and MILES (see Step 4) |
| generate_audio | No | Ask after the script is ready |
All word targets assume ~150 spoken words per minute. Stay within 10 percent of
target.
Derive a lowercase-hyphenated <slug> from the source or topic (max 6 words,
ASCII only).
Skip this step when the user gave a bare topic with no source material.
Extract every distinct article or item. For each, capture:
Merge duplicates covering the same event. Discard boilerplate, footers,
disclaimers, legal notices, unsubscribe text, and image captions.
Rank items by newsworthiness and reader impact. Keep the top 4 to 6 for full
treatment (fewer for short, more for long). Group the remainder into one
fast rapid fire segment. If the material has a dominant theme, lead with it
and thread it through the episode.
Two hosts, always the same personalities, always distinct voices:
en-US-AvaMultilingualNeural. Lead host. Warm, curious,quick. Drives the agenda, asks the question the listener is thinking, reacts
out loud, reframes jargon into plain language. Slightly faster cadence.
en-US-AndrewMultilingualNeural. Analyst. Calm, dry,precise. Supplies context, numbers, caveats, second-order implications.
Slightly slower, lower pitch. Occasionally pushes back on Nova.
Neither host is a narrator. They talk to each other, not to the microphone.
tension. No "welcome to the podcast", no channel branding, no music cues.
hook → the facts → why it matters → a short exchange of interpretation →
handoff line into the next story. Vary who leads each segment.
tempo.
then a short human sign-off. Alternate whether Nova or Miles ends.
This is what makes it sound conversational rather than read-aloud.
turns with the other host interjecting.
up", "here's the part I didn't expect", "yeah, and that's the thing", "hmm".
Roughly one marker every 4 to 5 turns. Never let it become a tic.
explain.
people, companies, or politics are not.
reporting".
report is careful to call that unconfirmed".
emojis, no bullet points, and no URLs in the spoken text.
Applies to every word that will be spoken.
2026, "three point two billion dollars" not $3.2B, "about fifteen
percent" not ~15%.
<say-as interpret-as="characters">API</say-as>.<sub alias="phonetic spelling">Name</sub>.<lang xml:lang="fr-FR">...</lang>.
& → &, < → <,> → >.
Write the human-readable transcript to:
/app/created/<slug>_Podcast_Script.txt
This file — and only this file — may carry NOVA: / MILES: speaker labels so
a person can follow along. It contains no stage directions and no markdown. The
spoken text itself must already satisfy Steps 6 and 7 so it can be lifted into
SSML unchanged.
Produce the SSML document and nothing else — no preamble, no explanation, no
code fences, no trailing notes inside the artifact. Write it verbatim to:
/app/created/<slug>_Podcast.ssml
Rules:
<speak> element with version="1.0",xmlns="http://www.w3.org/2001/10/synthesis",
xmlns:mstts="http://www.w3.org/2001/mstts", xml:lang="en-US".
<voice> element per conversational turn. Alternate speakers. Never putboth hosts inside one <voice> element.
<prosody> so it never sounds flat. Baselines: Novarate="+6%" pitch="+2%", Miles rate="-2%" pitch="-4%". Nudge per line to
match the emotion of the sentence.
<mstts:express-as style="..."> where the voice supports it. Preferredstyles: chat for banter, friendly for explanation,
narration-professional for the factual core of a story, excited sparingly
for the cold open. An unsupported style is ignored by the service, so keep
styles optional — never structural.
<break time="250ms"/> between turns within a segment,<break time="700ms"/> between segments, <break time="400ms"/> before a
punchline or a pivot. Never exceed 900ms.
<break> must sit inside a <voice> element. A <break> placedbetween <voice> elements — as a direct child of <speak> — is invalid in a
multi-voice document and will fail synthesis. Two <voice> elements may sit
directly next to each other with nothing between them. To pause *between*
turns, put the break at the end of the preceding turn's text, inside that
turn's <prosody>.
<emphasis level="moderate"> on at most one or two key terms per segment.<speak version="1.0"
xmlns="http://www.w3.org/2001/10/synthesis"
xmlns:mstts="http://www.w3.org/2001/mstts"
xml:lang="en-US">
<voice name="en-US-AvaMultilingualNeural">
<mstts:express-as style="excited">
<prosody rate="+8%" pitch="+3%">Okay, so the number that stopped me cold
this morning was forty percent. <break time="300ms"/> Forty percent, in one
quarter. <break time="250ms"/></prosody>
</mstts:express-as>
</voice>
<voice name="en-US-AndrewMultilingualNeural">
<mstts:express-as style="chat">
<prosody rate="-2%" pitch="-4%">Right, and the part everyone's skipping is
that it's off a very small base. <break time="250ms"/> Context matters
here. <break time="700ms"/></prosody>
</mstts:express-as>
</voice>
</speak>
Note the trailing <break> closing each turn: the 250ms is the gap before the
next turn, the 700ms is the longer gap before the next segment. Nothing sits
between the two <voice> elements.
Show the user a table summarising each segment (title, one-line description,
approximate spoken duration), print both file paths, and ask:
> Would you like me to convert this to an audio file?
Only if they say yes:
ConverttexttospeechwithSSML tool is available on the agent. Ifit is not, tell the user how to add it and stop.
ConverttexttospeechwithSSML with the SSML document from Step 9 andoutputFormat: riff-24khz-16bit-mono-pcm.
/app/created/<slug>_Podcast.wav.If the document is too large for a single call, split it at a segment boundary and synthesize each part separately.
When stitching, do NOT concatenate decoded RIFF/WAV bytes; instead, append the audio at the PCM-frame level (e.g., via Python’s wave module) and write a single valid /app/created/<slug>_Podcast.wav with one header.
import base64
with open('<tool_output_file>', 'r') as f:
content = f.read().strip()
audio_bytes = base64.b64decode(content)
with open('/app/created/<slug>_Podcast.wav', 'wb') as f:
f.write(audio_bytes)
Always end with:
| Item | Details |
|---|---|
| Script file | /app/created/<slug>_Podcast_Script.txt |
| SSML file | /app/created/<slug>_Podcast.ssml |
| Word count / estimated duration | actual vs. target at ~150 wpm |
| Items covered | full segments + rapid-fire count |
| Voices | en-US-AvaMultilingualNeural (Nova), en-US-AndrewMultilingualNeural (Miles) |
| Audio file | /app/created/<slug>_Podcast.wav *(only if audio generated)* |
Take microsoft/generating-podcast-script from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.