Skip to content

Narration scripts

The narration script is the contract between script producers and the renderer. An agent writes one by hand for research reports; anything else goes through the deterministic normalizer. The renderer never knows which producer wrote the script.

The v1 contract

A script is Markdown with a YAML frontmatter block, then a body of ## chapters and plain paragraphs:

---
narration: 1 # required schema marker
title: One Updates Tab # required; episode title (ID3 TIT2, feed item title)
source: research:202609/admin_center_updates_tab_unification.md # optional
source_blob: 3f9c2e1... # optional `git hash-object` of the source
date: 2026-09-14 # optional source date, spoken in the intro
author: Ryan Lopopolo # optional article author
site: OpenAI # optional article publication
kind: research # research | document | article (default: document)
edition: brief # full | brief | digest | verbatim
producer: agent # agent | deterministic
target_minutes: 4 # optional
cover: custom_artwork.png # optional explicitly chosen artwork, relative to the script file
---

## The question

Plain spoken prose. Paragraphs are separated by blank lines...

Only ## headings are allowed. Every ## becomes an ID3 chapter and a synthesis boundary. Text before the first ## is a structural error.

Article scripts use kind: article, the canonical URL in source, and may include author and site. Those fields are read and written by the script parser and used in the spoken intro and generated cover. See Web articles. AI-written URL scripts use producer: agent; their manifest records the model, model version, prompt version, and lint-repair attempt count.

The renderer adds the spoken intro (AI disclosure) and outro, so scripts must not. Agent scripts are named <stem>_narration.md, where <stem> is the report stem with any trailing __final removed.

cover is optional explicitly chosen artwork, relative to the script file. Omit it to use the generated title card. Research audio renders with sase-listen render <script> --generated-cover, which generates the title card and ignores cover frontmatter and any sibling <stem>_infographic.png.

Edition budgets at 150 words per minute: full is at most 2,400 words (about 16 minutes), brief is about 600 words, digest is about 250 words per item, and verbatim has no budget. Lint warns above 115% of the budget or below 50%.

Deterministic scripts from Markdown

script normalizes any Markdown file into a lint-clean edition: verbatim, producer: deterministic script, with an omissions report:

sase-listen script notes.md -o notes_narration.md
sase-listen script notes.md --json

The normalizer keeps the title (frontmatter, else the first H1, else the filename), turns H2 headings into chapters, H3-H6 into spoken sentences, list items into sentences, links into anchor text, useful images into "Figure: ..." sentences, and small tables into row-by-row speech. Fences, Mermaid diagrams, math, HTML comments, footnotes, Sources/References sections, and large tables are dropped and recorded in the omissions report. Inline code drops paths, file:line cites, SHAs, URLs, and SASE refs; snake_case becomes "snake case", CamelCase splits, single keys become "the X key", and §6 becomes "section 6".

Lint

lint validates a script against the contract and listenability rules. Each finding carries an id, severity, line:col, and fix hint:

sase-listen lint episode_narration.md --source report.md
sase-listen lint episode_narration.md --strict --json

Structural errors cover frontmatter, missing chapters, empty chapters, non-## headings, and preamble text. Residue errors cover list markers, tables, fences, backticks, link syntax, HTML, and emphasis. Warnings cover URLs, paths, file:line, SHAs, refs, §, symbols (→ ≈ × ≥ ≤ ±), long chapters/paragraphs/sentences, and edition budgets. --source adds number fidelity: every number in the script must appear in the source. A source number may appear as digits or spelled out in words: "thirteen" matches 13, and "fifty percent" matches 50%.

lint exits 1 on errors, or on warnings with --strict. render refuses structural errors, and cleans residue with warnings.

Authoring guide

guide prints the packaged rules agents write by:

sase-listen guide                  # brief by default
sase-listen guide --edition full   # full-length edition

The final step of every hand-written script is sase-listen lint <script> --source <report>, repeated until clean.