# podcast@0.1

**Status:** normative. **Owns diagnostic codes:** `MDTO1200`–`MDTO1299`.
**Heading axis:** production sections. **Item model:** none; dialogue turns are not tasks.

**Companion skill:** [Authoring and editorial guidance](https://markdownto.ai/specs/podcast.skill.md) (non-normative).

---

## 1. Purpose

`podcast@0.1` describes a portable two-speaker dialogue manuscript for native multi-speaker text-to-speech, with stable speaker identity, explicit production direction, and reproducible audio artifacts.

It exists separately from `narrate@0.1`: narration has one document voice and chapters of readable
prose; a podcast has two named voices and an ordered dialogue-turn grammar. An implementation MUST
NOT emulate this spec by reading speaker labels with one voice or by synthesizing each speaker alone.

---

## 2. Grammar

### 2.1 Envelope and speakers

```markdown
---
markdownto: podcast@0.1
title: The durable-agent show
id: durable-agent-show-01
speakers:
  - id: host
    name: HOST 1
    voice: Orus
  - id: cohost
    name: HOST 2
    voice: Aoede
style: Warm, curious, educational; explain jargon before using it.
language: en-US
pronunciations:
  AgentsFS: agents F S
---
```

`speakers` is required and contains exactly two mappings. Each mapping has exactly the semantic
fields `id`, `name`, and `voice`. `id` is a stable, lowercase manuscript identifier matching
`[a-z][a-z0-9_-]{0,31}`. `name` is the 1–64 character label sent to Gemini and shown to readers; it
contains no colon or line break. `voice` is one of Gemini's 30 published prebuilt voice names,
matched case-insensitively and canonicalized in the IR. IDs and names MUST be mutually unique under
case-insensitive comparison, and the two voices MUST differ. These are mdto restrictions that make
speaker resolution and audible identity deterministic.

The published voices are: Zephyr, Puck, Charon, Kore, Fenrir, Leda, Orus, Aoede, Callirrhoe,
Autonoe, Enceladus, Iapetus, Umbriel, Algieba, Despina, Erinome, Algenib, Rasalgethi, Laomedeia,
Achernar, Alnilam, Schedar, Gacrux, Pulcherrima, Achird, Zubenelgenubi, Vindemiatrix, Sadachbia,
Sadaltager, and Sulafat.

`style` is optional non-empty natural-language direction. `language` is an optional upstream
Gemini TTS language code: `ar-EG`, `bn-BD`, `nl-NL`, `en-IN`, `en-US`, `fr-FR`, `de-DE`, `hi-IN`,
`id-ID`, `it-IT`, `ja-JP`, `ko-KR`, `mr-IN`, `pl-PL`, `pt-BR`, `ro-RO`, `ru-RU`, `es-US`, `ta-IN`,
`te-IN`, `th-TH`, `tr-TR`, `uk-UA`, or `vi-VN`.
`pronunciations` has the same deterministic whole-word, longest-first substitution semantics as
narrate, but owns its diagnostics here. Wrong top-level YAML types are shared `MDTO007`.

### 2.2 Sections and turns

One optional leading `#` is a document title. Every `##` begins a production section; headings are
never spoken. Dialogue before the first `##` forms one implicit section. Any other heading is
`MDTO1210`. Sections are the generation, cache, resumption, and numbered output axis; an
implementation MAY split an oversized section at turn boundaries into smaller synthesis segments,
but MUST preserve order and speaker identity.

A dialogue turn is a paragraph or physical line beginning with a speaker's ID or display name and a
colon. The label MAY be bold. These forms are equivalent:

```markdown
host: Welcome back.
HOST 1: Welcome back.
**HOST 1:** Welcome back.
```

Continuation lines belong to the preceding turn. A new recognized label opens the next turn. An
unknown label is `MDTO1212`; an empty turn is `MDTO1213`; prose with no resolvable label is
`MDTO1211`. Thus ordinary prose is never silently assigned to a speaker. A conforming production
contains at least one turn (`MDTO1230`).

### 2.3 Pauses and production notes

A standalone `[pause:: 750ms]` or `[pause:: 2s]` inserts exact local silence between native
multi-speaker segments. It is greater than zero and at most ten minutes (`MDTO1214`). No other
silence is implied.

A blockquote beginning `> [production:: note]` is preserved and rendered but never sent to TTS. An
ordinary or empty blockquote is `MDTO1215`. Lists, code, tables, HTML, images, and unlabeled
paragraphs are ambiguous body content (`MDTO1211`), not dialogue.

### 2.4 Native Gemini contract and mdto choices

The upstream Gemini Developer API accepts text input and returns audio only. Its
`speechConfig.voiceConfig` and `speechConfig.multiSpeakerVoiceConfig` are mutually exclusive. A
multi-speaker request contains `speakerVoiceConfigs`, with at most two entries; each `speaker` MUST
match the label in the prompt and maps to a `voiceConfig.prebuiltVoiceConfig.voiceName`. Gemini 3.1
Flash TTS Preview, Gemini 2.5 Flash Preview TTS, and Gemini 2.5 Pro Preview TTS publish single- and
multi-speaker support. The API is preview; only Gemini 3.1 Flash TTS Preview publishes streaming,
and longer productions can drift in quality, so smaller chunks are recommended. `generateContent`
returns base64 `inlineData`; the documented examples receive mono signed 16-bit PCM at 24 kHz and
wrap it as WAV locally. The TTS guide publishes a 32k-token session context, while the current
[Gemini 2.5 Pro Preview TTS model card](https://ai.google.dev/gemini-api/docs/models/gemini-2.5-pro-preview-tts)
lists 8,192 input tokens and 16,384 output tokens for that model. An implementation MUST honor the
selected model's current service limit rather than treating either number as portable manuscript
syntax.

Those are upstream facts. The following are mdto choices: exactly two speakers; stable IDs distinct
from prompt labels; distinct voices; section/turn syntax; deterministic pronunciation; explicit
local pauses; production notes; bounded section-segment concurrency; content-addressed caching; MP3
artifacts; and atomic ordered joining. Authoritative upstream references are the
[Gemini TTS guide](https://ai.google.dev/gemini-api/docs/speech-generation) and
[`generateContent` API schema](https://ai.google.dev/api/generate-content).

### 2.5 Diagnostics

| Code | Severity | Meaning |
|---|---|---|
| `MDTO1200` | error | The file does not declare exactly two valid speakers. |
| `MDTO1201` | error | A speaker declaration is not a mapping of string `id`, `name`, and `voice`. |
| `MDTO1202` | error | A speaker ID is duplicated. |
| `MDTO1203` | error | A speaker ID/display label collides with another label. |
| `MDTO1204` | error | Both speakers map to the same voice. |
| `MDTO1205` | error | A speaker ID or display name is malformed. |
| `MDTO1206` | error | A voice is not a published Gemini prebuilt voice. |
| `MDTO1207` | error | `style` is empty. |
| `MDTO1208` | error | `language` is not a BCP-47 tag. |
| `MDTO1209` | error | A pronunciation is empty, non-string, or duplicated case-insensitively. |
| `MDTO1210` | error | A heading level or section title is invalid. |
| `MDTO1211` | error | Body content is not an unambiguous dialogue turn, pause, or production note. |
| `MDTO1212` | error | A turn names an unknown speaker. |
| `MDTO1213` | error | A dialogue turn is empty. |
| `MDTO1214` | error | A pause is malformed or out of range. |
| `MDTO1215` | error | A production note is malformed or empty. |
| `MDTO1230` | error | The production has no dialogue turns. |

`MDTO1211` recovery preserves ambiguous content but creates no turn. Invalid speaker declarations
are omitted from the typed speaker array but remain verbatim in `frontmatter`. Invalid pauses and
notes create no event. No error has a `--force` path to paid production.

### 2.6 Production, cache, receipt, and Hub rendering

Before spending, production resolves both voices, verifies that the provider/model advertises native
multi-speaker support, validates every segment against provider limits, prints an estimate, and
requires `--yes`. Cache misses alone occupy workers. Each cache key includes `podcast@0.1`, provider,
model, language, style, both ordered `{id,name,voice}` mappings, and every ordered turn/pause in the
segment. A changed voice invalidates every affected segment; a changed turn invalidates only its
segment. Completed segments are resumable after failure.

Segments may finish out of order. Numbered files, receipt entries, and the final MP3 MUST be in
manuscript order. The final join is atomic and absent after partial failure; a stale joined file is
removed before generation begins. Receipts distinguish generated/cached counts for this invocation,
record per-segment cache status and measured timing, and never invent cost.

The Hub read-only view MUST show the speaker legend, style/language direction, ordered sections,
color- or shape-distinct dialogue turns, pauses, non-narrated notes, diagnostics, and artifact status.
It MUST escape all source content and MUST NOT load remote resources. Production is a separate
authenticated action; rendering a manuscript never sends its text to a provider.

---

## 3. Rationale

Native multi-speaker synthesis receives a conversation, not a bag of independent utterances. Keeping
both voices in every request lets the model coordinate rhythm and turn-taking and prevents a tool
from pretending that two unrelated single-voice clips are the same feature. Stable IDs survive a
display-name rewrite; exact display labels satisfy Gemini's prompt/config matching rule. Explicit
section boundaries give authors control over quality, caching, cost, and recovery without exposing a
provider token limit in the manuscript grammar.
The reference implementation's 6,000-character segment target is a conservative quality boundary,
not an upstream maximum.

This spec does not use or redefine the shared item model: a dialogue turn has no checkbox, task
state, owner, or mutation identity. Its order is manuscript meaning, not workflow state.

---

## 4. Examples

- [`fixtures/valid/conversation.md`](fixtures/valid/conversation.md) — `HOST 1` / bold `HOST 2` compatibility.
- [`fixtures/valid/stable-ids.md`](fixtures/valid/stable-ids.md) — stable IDs used as body labels.
- [`fixtures/valid/pauses-notes.md`](fixtures/valid/pauses-notes.md) — sections, exact pause, non-spoken note.
- [`fixtures/valid/direction.md`](fixtures/valid/direction.md) — style, language, pronunciation.
- [`fixtures/invalid/speaker-errors.md`](fixtures/invalid/speaker-errors.md) — declaration diagnostics.
- [`fixtures/invalid/dialogue-errors.md`](fixtures/invalid/dialogue-errors.md) — refuses ambiguous production text.

---

## 5. Verb reference

`podcast@0.1` owns the vocabulary **estimate, produce, preview, voices**. Every verb is read-only with
respect to the Markdown source; artifacts are written beside it.

### Common flags

`--file <path>` selects the manuscript; `--provider auto|gemini|hub|mock` and `--model <name>`
select the native TTS boundary; `--json` emits the stable machine-readable form. Paid production
also accepts `--jobs <N>`, refuses above the provider maximum, and requires `--yes` after printing
its estimate. There is no `--force`: a non-conforming paid manuscript always refuses.

| Verb | Signature | Source edit |
|---|---|---|
| `estimate` | `mdto podcast estimate [--file f] [--provider p] [--model m] [--jobs N]` | None. Reports turns, duration, cache disposition, workers, and native capability without network or spend. |
| `produce` | `mdto podcast produce [--file f] [--out DIR] [--section N] [--jobs N] [--yes]` | None. Writes ordered MP3 artifacts and a receipt beside the source after capability-checking and acknowledgement. |
| `preview` | `mdto podcast preview --section N [--file f] [--provider p] [--model m]` | None. Writes one temporary section audition through the same native/cached path. |
| `voices` | `mdto podcast voices [--provider p] [--model m]` | None. Reads no manuscript; lists voices and native multi-speaker capability. |

`--provider mock` is an explicitly synthetic offline test path. It may exercise parsing, caching,
ordering, and receipts, but its output is not speech. A provider that cannot perform one native
multi-speaker request MUST refuse before generation; it MUST NOT fall back to alternating
single-speaker synthesis.
