> built 2026-10-14 12:40 UTC from 40cdebd (main) · braidio 0.0.71. Details: build_info.json

# index.html.md

<!-- generated by epythet -->

# braidio

**Weave *commentary* — solo, duo, panel, debate, interview, documentary — with
source clips into a production.**

`braidio` braids the strands of a piece that talks *about* something (a song, a
book, a film, an event) into one rendered production. The **talk is the spine** —
a single presenter, two hosts, a panel, a narrator — and **source clips and
narration bridges are the “illustration” layers** woven onto it. Audio today,
with visual support to follow (it braids A/V — *braid-io*, and it sounds like
*radio*).

Two things braidio gives you:

1. **Ready-made format templates** under the standard, industry names people
   already recognize — *Deep Dive*, *Song Exploder-style*, *Panel*, *Debate*,
   *Documentary VO* — each a high-quality bundle of defaults you can render in one
   call.
2. **Full parametrization** underneath, so any style is expressible: a `Script`
   of beats, cast via a `ConversationCast`, tuned by `WeaveConfig` + `Delivery`,
   with per-beat voice/delivery overrides.

> `braidio` was extracted from the
> [Hamilton lyrics-podcast](https://github.com/thorwhalen/Hamilton) (its first
> application) into a reusable engine. It is now live infrastructure: released
> on PyPI, served as a multi-tool MCP connector, and imported in production by
> [`reelee`](https://github.com/thorwhalen/reelee). Treat a signature change
> here as having downstream blast radius.

## The model

A production is a **`Script`** — an ordered list of **beats**:

| Beat                                          | What it is                                                                                                                                                                               |
|-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `Narration(text, voice?, voice_settings?, …)` | one voice reading — a narrator **or** a solo presenter. `voice` / `voice_settings` override per beat, so one timeline can carry a lively **presenter** *and* a graver **book-narrator**. |
| `Dialogue(turns=[(role, text)])`              | a multi-speaker exchange, synthesized in **one pass** (so it sounds like people *talking to each other*, not alternating monologues). A `ConversationCast` maps roles → voices.          |
| `SegmentBeat(reference)`                      | a span of source media to cut and weave in (resolved by a pluggable `SegmentSource` — quote → `[start, end]`). `spotlight=True` drops the music bed out under it.                        |
| `SceneBreak(label?)`                          | a structural boundary — “a new section starts here”. Renders as a short musical **sting** (your asset) or a beat of silence; costs nothing.                                              |

`WeaveConfig` holds every editing knob (casting, turns, pacing, clip pre/post-roll,
duck, crossfades, loudness). `Delivery` presets bundle the TTS model + voice
settings (e.g. `V2_PRESENTER` lively vs `V2_NARRATOR` grave). The renderer
loudness-normalizes every part, tucks clips under the speech (speech stays
dominant), and masters the result.

## Ready-made formats

Pick a standard format and render it — the template supplies the cast, voices,
delivery and mix defaults:

```python
from braidio import DEEP_DIVE, render_format

render_format(DEEP_DIVE, script, source=my_source, out_path="episode.mp3")
```

| Preset id                | Standard name                                                       | Shape                                                                                   |
|--------------------------|---------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
| `solo_explainer`         | Solo-Presenter Explainer (video/audio essay, close reading)         | one presenter + exhibits                                                                |
| `deep_dive`              | Two-Host Conversation ( *“Deep Dive”*, Switched on Pop, NotebookLM) | two hosts; one teaches, one probes                                                      |
| `interview`              | Interview (host + guest)                                            | Q→A, guest is the center                                                                |
| `interview_host_removed` | *Song Exploder*-style (host removed)                                | guest monologue; each claim illustrated by its isolated stem; full artifact at the tail |
| `panel`                  | Panel / Roundtable (Pop Culture Happy Hour)                         | moderator routes distinct voices                                                        |
| `debate`                 | Debate (Oxford-style)                                               | proposition / opposition / moderator, phased                                            |
| `documentary_vo`         | Documentary Voice-Over ( *“Voice of God”*, This American Life)      | narrator on top; interviews + clips + actuality beneath                                 |

**Narration bridges** and **source clips** are optional *illustration layers*
usable with any format. The taxonomy, weaving grammar, exemplar recipes and the
full template specs are in
[`misc/docs/research/commentary-formats-and-styles.md`]().

*Render support note:* the cast, per-role voices, narration deliveries, loudness
master, **per-clip placement** — `SegmentBeat(placement="before" | "under" | "after")`, where `under` lays the clip concurrently beneath the talk, ducked —
a **music bed** (`MusicBed`, an app-supplied instrumental laid under the whole
production) and the **structural music** are all applied. Structure is a
`SceneBreak` beat in the Script — “a new section starts here” — which renders as
a short **sting** when you pass `render_format(..., sting_asset=…)` (or a beat of
silence when you don’t), and `SegmentBeat(spotlight=True)` for
**fade-to-spotlight**: the bed drops out before that exhibit and resumes after
it. Each format’s `structure` (`MusicStructure`) sets the defaults — whether a
break stings, whether every clip is spotlit — and a beat overrides them. As with
the bed, braidio ships no music: the sting is your asset. Both render paths carry
this: the graph path takes the same `structure=` / `bed=` (or a `fmt=`, whose
declared structure it uses) on `weave_project`, records the decision as a
`production-structure/v1` node, and re-renders only the episode when you swap the
sting. Calling `weave_project` again on the same project **re-ingests**: beats are
matched by position, unchanged ones reuse their renders, and a changed config,
structure, rights profile or dialogue cast is rewritten under its existing node so
everything derived from it reads stale through provenance — the only correct way
to change a profile after the first weave. Every beat type takes the graph path,
`Dialogue` included: an exchange is one `dialogue-beat/v1` rendered in one
Text-to-Dialogue pass, and the cast (`cast=`, else the format’s, else the
default) is one `dialogue-cast/v1` node every dialogue render derives from — so
recasting re-renders the exchanges and nothing else. Design history in
[braidio#25](https://github.com/thorwhalen/braidio/issues/25),
[braidio#39](https://github.com/thorwhalen/braidio/issues/39),
[braidio#46](https://github.com/thorwhalen/braidio/issues/46) and
[braidio#51](https://github.com/thorwhalen/braidio/issues/51).

## Parametrize anything

The templates are just good defaults over the primitives — compose directly for
full control:

```python
from braidio import Script, Dialogue, Narration, SegmentBeat, WeaveConfig
from braidio.conversation import ConversationCast, JESSICA, CHRIS
from braidio import V2_NARRATOR, render_production

script = Script(
    title="…",
    id_slug="01",
    beats=[
        Dialogue(
            (("A", "First thing that gets me…"), ("B", "right — but isn't that…"))
        ),
        SegmentBeat("the lyric being discussed", label="hook"),
        Narration(
            "Here's how the book tells it —", voice_settings=V2_NARRATOR.voice_settings
        ),  # graver, per-beat
    ],
)
render_production(
    script,
    source=my_source,
    cast=ConversationCast(roles={"A": JESSICA, "B": CHRIS}),
    config=WeaveConfig(),
    out_path="out.mp3",
)
```

### Cost tracking

ElevenLabs TTS is braidio’s only spend (everything else is local ffmpeg). `braidio.estimate_cost(script)` previews a production’s cost before you pay for synthesis, and real renders attribute a per-character cost onto their artifacts. Dollars are a **rate estimate** — set your ElevenLabs plan’s rate for exact figures:

| Env var                        | Meaning                                                                                                                                    |
|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| `BRAIDIO_TTS_USD_PER_1K_CHARS` | USD per 1000 characters. Unset → a conservative default; `none` → mark spend *unpriced* (character counts still reported, dollars `None`). |
| `BRAIDIO_TTS_VOICE`            | Default ElevenLabs voice id for narration.                                                                                                 |
| `ELEVENLABS_API_KEY`           | The key synthesis bills when a render is given no `api_key=`.                                                                              |

Every render entry point — `narrate`, `render_dialogue`, `render_production`, `render_format` and the graph driver `weave_project` — takes an explicit `api_key=` for a caller’s own ElevenLabs key; `None` (the default) resolves from the environment. On the graph path the key reaches the two synthesis transforms through nw’s `execute(secrets=)` seam and is never persisted: not in a node, provenance, a cache key or a usage ledger (thorwhalen/braidio#58). The MCP server reads it from the `X-Elevenlabs-Key` request header, never from a tool argument.

## What it deliberately does *not* do

braidio is a thin orchestration layer. It **delegates**:

| Concern                                                       | Owner                                                                           |
|---------------------------------------------------------------|---------------------------------------------------------------------------------|
| Content acquisition (Genius, audiobooks, news…)               | the consuming app, via `SegmentSource` adapters                                 |
| Linked-artifact graph / content-addressed media               | [`lacing`](https://github.com/thorwhalen/lacing)                                |
| Project workflow, provenance, plan/execute, partial re-render | [`nw`](https://github.com/thorwhalen/nw)                                        |
| Video render + visual support                                 | [`reelee`](https://github.com/thorwhalen/reelee) — except the stills case below |
| Pan/zoom motion over a still                                  | [`burns`](https://github.com/thorwhalen/burns)                                  |
| Raw audio DSP + TTS (crop, concat, duck, loudnorm, synth)     | [`mixing`](https://github.com/thorwhalen/mixing) / `falaw`                      |

braidio orchestrates these; it never reimplements the DSP or the graph.

## Video: a commentary episode you can watch

`braidio[video]` turns a rendered episode into a **Ken Burns film over still
images** — the one video case that stays here rather than going to `reelee`,
because reelee imports braidio and the cuts have to land on braidio’s own beat
timeline. `burns` owns the motion; braidio owns only where the picture changes.

```python
import braidio
from braidio.video import plan_spans, assign_stills, render_video

path, timeline = braidio.render_format(
    fmt, script, source=src, out_path="ep.mp3", return_timeline=True
)
panels = assign_stills(plan_spans(timeline), ["a.jpg", "b.jpg", "c.jpg"])
render_video(panels, audio_path="ep.mp3", out_path="ep.mp4")
```

`plan_spans` cuts on beat boundaries — splitting a long passage so no photograph
stalls on screen, merging a short beat so none flickers. Each `Span` carries the
beat’s `label` and `kind`, because *which* picture belongs over a sentence needs
to know what the sentence says; `assign_stills` is the mechanical default for when
the images are interchangeable texture. Motion is content-aware by default, so a
slow push stays on the subject. braidio fetches **no images** — supply them, as
with `SegmentSource`.

Subtitles need no ASR: you authored the words and the timeline says when they
play.

```python
Path("ep.srt").write_text(braidio.captions_for(script, timeline, max_chars=42))
```

`braidio.captions` is part of the core (pure, no extra dependencies);
For a production you will **reopen and re-edit** — swap a still, change a
move, re-render — the picture track lives in the project graph instead: four
`lacing` bodies (`still/v1`, `video-panel/v1`, `video-cut/v1`,
`label-track/v1`) and three free transforms on the `commentary_weave` genre
(`video_panels.plan`, `video_cut.render`, `video_cut.finish`). The episode
persists its timeline, a panel pins its still to a span of the audio with an
authored camera move, and a label edit re-runs the cheap text pass, not the
frames. The `braidio-commentary-video` skill has the walkthrough.

### Where a commentary project lives

A `commentary_weave` project is created by braidio’s registered `nw` project
factory, and **where it lands is the caller’s decision, not braidio’s** (nw#84).
Called with no placement — braidio’s own connector — it goes in braidio’s per-user
workspace under `BRAIDIO_DATA_HOME`, exactly as before. Called with a
`projects_dir`, it is created there instead, which is how a *host* that will serve
the project (reelee) puts it beside that caller’s other projects rather than under
braidio’s data home, where the host’s router and lister would never find it.

`braidio.project.create_project_at(projects_dir, project_id)` is the host-placed
create on its own. It is deliberately outside `braidio.mcp`, so placing a project
never pulls the `[mcp]` extra.

`braidio.video`’s dependencies are imported inside the functions that use them, so
`import braidio.video` works on a bare install and the planners stay usable —
`braidio.HAS_VIDEO` reports whether the render path is available.

## Agent skills

braidio ships skills that install with it, so an agent host can use them without
cloning the repo:

| Skill                      | For                                                                  |
|----------------------------|----------------------------------------------------------------------|
| `braidio`                  | authoring and rendering a commentary production (audio)              |
| `braidio-commentary-video` | the video pipeline above — panels, stills, captions, credits, rights |
```bash
ln -s "$(python -c 'import braidio; print(braidio.skills_dir())')/braidio" ~/.claude/skills/braidio
```

## Ecosystem

`braidio` is a **production kind** on top of `nw` — a reusable definition of
“commentary that weaves talk with extracted media” — the way a music video is
another production kind. It sits on `lacing` (graph) + `nw` (workflow /
provenance) + `mixing`/`falaw` (audio/TTS), and will use `reelee` for video. The
optional nw-app layer (`braidio.HAS_GRAPH` / `HAS_NW`) records render choices as
provenance so a change re-renders only the affected parts.

## Install

```bash
pip install braidio
```

ElevenLabs credentials are needed for TTS; `ffmpeg` must be on PATH for
everything else.

## License

MIT

<p class="epythet-aggregates">This documentation as a single file: <a href="braidio.md">braidio.md</a> (Markdown, for agents).</p>


# _autosummary/braidio.captions.html.md

# braidio.captions

Subtitles for a rendered production, built from the script it was rendered from.

A braidio render is one of the rare cases where the caption track needs no speech
recognition: the narration text is *authored*, and
[`render_production()`](_autosummary/braidio.render.html.md#braidio.render.render_production) will hand back a
[`TimelineBreakdown`](_autosummary/braidio.timeline.html.md#braidio.timeline.TimelineBreakdown) saying exactly when each beat plays.
Transcribing the mix back into text would pay an ASR pass to recover something
that was never lost – and do it worse (a measured pass on braidio’s own output
turned “purse sets up hurts” into “Perse sets up herts”).

So [`captions_for()`](_autosummary/braidio.captions.html.md#braidio.captions.captions_for) joins the two: beat text from the `Script`, beat timing
from the `TimelineBreakdown`. Narration and dialogue beats are split into
sentence-sized cues timed *within* their beat in proportion to length; a
`SegmentBeat` becomes one cue carrying its reference, marked as lyric/quote.

Pure and dependency-free – part of the functional core, importable with nothing
installed beyond braidio itself.

### Examples

```pycon
>>> from braidio.script import Narration, Script
>>> from braidio.timeline import build_timeline
>>> script = Script(title="t", id_slug="01", beats=[Narration("One. Two.")])
>>> tl = build_timeline(kinds=["narration"], durations=[4.0])
>>> print(captions_for(script, tl))
1
00:00:00,000 --> 00:00:02,000
One.

2
00:00:02,000 --> 00:00:04,000
Two.
```

### Functions

| [`captions_for`](_autosummary/braidio.captions.html.md#braidio.captions.captions_for)(script, timeline, \*[, max_chars])   | The SRT document for `script` as laid out by `timeline`.   |
|----------------------------------------------------------------------------------------------------|------------------------------------------------------------|
| [`cues_for`](_autosummary/braidio.captions.html.md#braidio.captions.cues_for)(script, timeline, \*[, max_chars])       | Subtitle cues for `script` as laid out by `timeline`.      |
| [`format_srt`](_autosummary/braidio.captions.html.md#braidio.captions.format_srt)(cues)                                  | Render `cues` as an SRT document.                          |

### Classes

| [`Cue`](_autosummary/braidio.captions.html.md#braidio.captions.Cue)(start_s, end_s, text)   | One subtitle: `[start_s, end_s)` and the text shown.   |
|------------------------------------------------------------------------------|--------------------------------------------------------|

### *class* braidio.captions.Cue(start_s, end_s, text)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One subtitle: `[start_s, end_s)` and the text shown.

### braidio.captions.captions_for(script, timeline, , max_chars=0)

The SRT document for `script` as laid out by `timeline`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### braidio.captions.cues_for(script, timeline, , max_chars=0)

Subtitle cues for `script` as laid out by `timeline`.

Beats are matched by index, so a timeline built from the same script lines up
even when some beats were dropped by a rights profile (a dropped beat simply
has no span and contributes no cue).

Cues never overlap. That matters because the weave *crossfades* consecutive
beats, so a beat’s `start` sits slightly before the previous beat’s end;
left alone that produces subtitles that fight each other. Each cue is clamped
to begin where the previous one finished.

* **Parameters:**
  * **script** – the [`Script`](_autosummary/braidio.script.html.md#braidio.script.Script) that was rendered.
  * **timeline** – the [`TimelineBreakdown`](_autosummary/braidio.timeline.html.md#braidio.timeline.TimelineBreakdown) from
    `render_production(..., return_timeline=True)`.
  * **max_chars** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – if > 0, split sentences longer than this at whitespace, so no
    single cue overflows a player’s two lines. 0 leaves sentences whole.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Cue`](_autosummary/braidio.captions.html.md#braidio.captions.Cue)]
* **Returns:**
  Cues in playback order.

### braidio.captions.format_srt(cues)

Render `cues` as an SRT document.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/braidio.compose.html.md

# braidio.compose

Config-driven narration composition (#20) — the reusable entrypoint.

Turn a list of narration segments + a `WeaveConfig` into audio. Dispatches
single-voice and multi-voice uniformly (single = a pool of one), reading *every*
knob from the config. This is the seed of the reusable weave engine (#18/#19):
it has no Hamilton-specifics — it takes plain text segments and a config.

Clip weaving (padded extraction + ducking) is composed separately (#21); this
module renders the narration track.

### Functions

| [`compose_narration`](_autosummary/braidio.compose.html.md#braidio.compose.compose_narration)(segments, config, \*, out_path)   | Render `segments` under `config` → `out_path`.   |
|------------------------------------------------------------------------------------------------------|--------------------------------------------------|

### braidio.compose.compose_narration(segments, config, , out_path, api_key=None, work_dir='data/tts/compose')

Render `segments` under `config` → `out_path`.

Single-voice and multi-voice go through the same turn-based path (a single
voice is just a one-voice pool). Returns the `(voice, turn_text)`
assignment for reporting / provenance.

`api_key` is an optional per-request ElevenLabs key threaded to
[`braidio.multivoice.render_multivoice()`](_autosummary/braidio.multivoice.html.md#braidio.multivoice.render_multivoice) (and thence every synthesized
turn); `None` (default) keeps the `$ELEVENLABS_API_KEY` fallback.

Note `config.segmentation_unit` is **not** read here: this function is
handed `segments` already cut, so the unit was chosen upstream (usually
[`braidio.script.narration_segments()`](_autosummary/braidio.script.html.md#braidio.script.narration_segments)). It is the
[`braidio.render.render_production()`](_autosummary/braidio.render.html.md#braidio.render.render_production) path that cuts a beat itself, and
there the field is the switch that turns pacing on — see
[`braidio.pacing`](_autosummary/braidio.pacing.html.md#module-braidio.pacing).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Voice`](_autosummary/braidio.multivoice.html.md#braidio.multivoice.Voice), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]


# _autosummary/braidio.conversation.html.md

# braidio.conversation

Conversational register: render an exchange as people *talking to each other*.

The second delivery register beside narration (braidio#1). A
[`ConversationCast`](_autosummary/braidio.conversation.html.md#braidio.conversation.ConversationCast) maps role labels (`"A"`/`"B"`) to contrasting
**conversational** ElevenLabs voices. [`render_dialogue()`](_autosummary/braidio.conversation.html.md#braidio.conversation.render_dialogue) synthesizes the
whole exchange in **one pass** via [`braidio.tts.text_to_dialogue()`](_autosummary/braidio.tts.html.md#braidio.tts.text_to_dialogue) (eleven_v3)
so prosody is conditioned across turns — the key to not sounding narrated.
[`render_turns_sequential()`](_autosummary/braidio.conversation.html.md#braidio.conversation.render_turns_sequential) is the per-line baseline (each turn synthesized
alone, then concatenated) used for A/B comparison.

Casting note: use conversational-labelled voices, not narrator voices; loosen
settings so v3 audio tags fire. The scripted exchange itself must carry the
disfluency (backchannels, interruptions, fragments) — the model won’t invent it.

### Functions

| [`render_dialogue`](_autosummary/braidio.conversation.html.md#braidio.conversation.render_dialogue)(turns[, cast, api_key, ...])   | One-pass render of `turns` (`[(role, text), …]`) via Text-to-Dialogue.                              |
|-------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
| [`render_turns_sequential`](_autosummary/braidio.conversation.html.md#braidio.conversation.render_turns_sequential)(turns[, cast, ...])    | Per-line baseline: synthesize each turn alone (its role's voice) and concatenate with `gap_s` gaps. |

### Classes

| [`ConversationCast`](_autosummary/braidio.conversation.html.md#braidio.conversation.ConversationCast)([roles, model_id, settings])   | Role → voice mapping + model/settings for a conversational exchange.   |
|--------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|

### *class* braidio.conversation.ConversationCast(roles=<factory>, model_id='eleven_v3', settings=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Role → voice mapping + model/settings for a conversational exchange.

Default cast: **Jessica** (playful/bright, F) + **Chris** (charming/
down-to-earth, M) — snappier than the “relaxed” voices. `stability=0.45`
is the research sweet spot for a lively but coherent read (never 1.0/Robust,
which mutes v3 tags; ~0.1 is too unstable). See braidio#1 + the pacing doc.

### braidio.conversation.render_dialogue(turns, cast=ConversationCast(roles={'A': 'cgSgspJ2msm6clMCkdW9', 'B': 'iP95p4xoKVk53GoZ742B'}, model_id='eleven_v3', settings={'stability': 0.45}), , api_key=None, out_path, output_format='mp3_44100_128', seed=None, cache=True, refresh=False, tighten_gaps_s=0.6, return_cache_status=False)

One-pass render of `turns` (`[(role, text), …]`) via Text-to-Dialogue.

Cached by default (see [`braidio.tts.text_to_dialogue()`](_autosummary/braidio.tts.html.md#braidio.tts.text_to_dialogue)): an unchanged
exchange renders instantly on re-run. `refresh=True` re-rolls the take.

`api_key` is an optional per-request ElevenLabs key threaded to
[`braidio.tts.text_to_dialogue()`](_autosummary/braidio.tts.html.md#braidio.tts.text_to_dialogue); `None` (default) keeps the
`$ELEVENLABS_API_KEY` fallback.

`tighten_gaps_s` (>0) applies the “R-tight” pass: ffmpeg trims only the
over-long dead gaps (silences ≥ this many seconds) that make v3 dialogue
feel draggy, while leaving natural short pauses. Set 0 to keep the raw take.

`return_cache_status`: when `True`, return `(path, was_cached)` where
`was_cached` is `True` iff the take came from the on-disk cache (no
ElevenLabs call = $0 real spend) — what lets the graph path attribute real
cost (braidio#8). Default `False` keeps the `Path` return.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`bool`](https://docs.python.org/3/builtins/functions.html#bool)]

### braidio.conversation.render_turns_sequential(turns, cast=ConversationCast(roles={'A': 'cgSgspJ2msm6clMCkdW9', 'B': 'iP95p4xoKVk53GoZ742B'}, model_id='eleven_v3', settings={'stability': 0.45}), , api_key=None, out_path, work_dir='data/tts/conversation', voice_settings=None, gap_s=0.25)

Per-line baseline: synthesize each turn alone (its role’s voice) and
concatenate with `gap_s` gaps. Prosody is NOT shared across turns — this is
what tends to sound like alternating monologues (the thing to beat).

`api_key` is threaded to [`braidio.tts.narrate()`](_autosummary/braidio.tts.html.md#braidio.tts.narrate); `None` (default)
keeps the `$ELEVENLABS_API_KEY` fallback.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/braidio.cost.html.md

# braidio.cost

Cost model for braidio’s paid operations (ElevenLabs TTS).

braidio’s only spend is ElevenLabs synthesis — [`braidio.narrate()`](_autosummary/braidio.html.md#braidio.narrate) /
`braidio.text_to_dialogue()` and the renderers built on them — billed by
ElevenLabs **per character** of submitted text. Everything else (ffmpeg
extraction, weaving, loudness normalization) is local and free.

This module turns text into a USD figure so renders can attribute a cost onto
`lacing.Artifact.cost_usd` and a free [`estimate_cost()`](_autosummary/braidio.cost.html.md#braidio.cost.estimate_cost) can preview a whole
[`braidio.Script`](_autosummary/braidio.html.md#braidio.Script) before any synthesis is paid for.

**The figure is an ESTIMATE**, priced at a configurable per-1000-character rate
(env [`RATE_ENV_VAR`](_autosummary/braidio.cost.html.md#braidio.cost.RATE_ENV_VAR), the per-model [`MODEL_USD_PER_1K_CHARS`](_autosummary/braidio.cost.html.md#braidio.cost.MODEL_USD_PER_1K_CHARS) table, or
[`DEFAULT_USD_PER_1K_CHARS`](_autosummary/braidio.cost.html.md#braidio.cost.DEFAULT_USD_PER_1K_CHARS)). It equals what ElevenLabs bills for a *live*
synthesis at that rate — not the provider’s exact invoice. braidio does not yet
see the lower-level `mixing` on-disk cache, so a render served from that cache
still reports its rate estimate rather than `$0` (a safe over-estimate for a
spend ledger; see thorwhalen/braidio#8 to make it exact).

This differs from `falaw` (which has no default rate and returns `None`
whenever a real price is unknown): braidio deliberately supplies a *default* so a
ledger has a number out of the box — while still returning `None` (*unpriced*,
never a fake `0.0` for real text) when the rate is explicitly disabled or
invalid. Characters are always counted exactly, so a dollar figure can be
recomputed if the rate changes.

### Module Attributes

| [`RATE_ENV_VAR`](_autosummary/braidio.cost.html.md#braidio.cost.RATE_ENV_VAR)             | Env var overriding the USD-per-1000-characters rate (global default for models not in [`MODEL_USD_PER_1K_CHARS`](_autosummary/braidio.cost.html.md#braidio.cost.MODEL_USD_PER_1K_CHARS)).   |
|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`DEFAULT_USD_PER_1K_CHARS`](_autosummary/braidio.cost.html.md#braidio.cost.DEFAULT_USD_PER_1K_CHARS) | Default USD per 1000 characters when nothing else is configured.                                                                                                  |
| [`MODEL_USD_PER_1K_CHARS`](_autosummary/braidio.cost.html.md#braidio.cost.MODEL_USD_PER_1K_CHARS)   | Confirmed per-model rate overrides (USD per 1000 chars).                                                                                                          |

### Functions

| [`billable_chars`](_autosummary/braidio.cost.html.md#braidio.cost.billable_chars)(text)                  | Characters ElevenLabs bills for `text` (the whole submitted string).                                                            |
|----------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|
| [`estimate_cost`](_autosummary/braidio.cost.html.md#braidio.cost.estimate_cost)(source, \*[, model_id]) | Estimate ElevenLabs spend for a [`braidio.Script`](_autosummary/braidio.html.md#braidio.Script) or a raw string. |
| [`tts_cost_usd`](_autosummary/braidio.cost.html.md#braidio.cost.tts_cost_usd)(text, \*[, model_id])    | Estimated USD to synthesize `text`; `0.0` for empty, `None` if unpriced.                                                        |
| [`usd_per_1k_chars`](_autosummary/braidio.cost.html.md#braidio.cost.usd_per_1k_chars)([model_id])          | Resolved USD-per-1000-characters rate (most specific source first).                                                             |

### Classes

| [`CostLine`](_autosummary/braidio.cost.html.md#braidio.cost.CostLine)(label, kind, characters, usd, model_id)   | One billable line of an estimate (a narration beat or a dialogue beat).     |
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`CostRollup`](_autosummary/braidio.cost.html.md#braidio.cost.CostRollup)(characters, usd, unpriced[, lines])     | A production's estimated TTS spend, exact on characters, honest on dollars. |

### *class* braidio.cost.CostLine(label, kind, characters, usd, model_id)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One billable line of an estimate (a narration beat or a dialogue beat).

Named to echo `falaw.CostLine` (a line within a rollup) rather than
`falaw.CostEstimate` (which means a per-call price spec) — so the vocabulary
is consistent across the federation.

### *class* braidio.cost.CostRollup(characters, usd, unpriced, lines=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A production’s estimated TTS spend, exact on characters, honest on dollars.

`usd` is the sum of the *priced* lines; `unpriced` is `True` when some
billable text had no configured rate (so `usd` is a lower bound). No billable
lines (e.g. an all-segment script) gives `characters=0, usd=0.0,
unpriced=False`. Named `CostRollup` to match `falaw.CostRollup`.

#### *property* summary *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

One-line human summary (handy for a CLI/MCP preview).

### braidio.cost.DEFAULT_USD_PER_1K_CHARS *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.3*

Default USD per 1000 characters when nothing else is configured. Approximate:
ElevenLabs bills per credit and the $/credit depends on your plan, so set
[`RATE_ENV_VAR`](_autosummary/braidio.cost.html.md#braidio.cost.RATE_ENV_VAR) to your plan’s real rate. Kept slightly conservative so a
budget over- rather than under-estimates spend.

### braidio.cost.MODEL_USD_PER_1K_CHARS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [float](https://docs.python.org/3/builtins/functions.html#float)]* *= {}*

Confirmed per-model rate overrides (USD per 1000 chars). A model listed here
is priced at its own rate — winning over the env override and the default —
because a confirmed price is more accurate than a global guess. Empty until
ElevenLabs per-model rates are confirmed (e.g. `eleven_v3` dialogue vs
`eleven_multilingual_v2`).

### braidio.cost.RATE_ENV_VAR *= 'BRAIDIO_TTS_USD_PER_1K_CHARS'*

Env var overriding the USD-per-1000-characters rate (global default for models
not in [`MODEL_USD_PER_1K_CHARS`](_autosummary/braidio.cost.html.md#braidio.cost.MODEL_USD_PER_1K_CHARS)). Set it to your ElevenLabs plan’s real
rate for exact ledgers; set it to `none` to mark spend as *unpriced*.

### braidio.cost.billable_chars(text)

Characters ElevenLabs bills for `text` (the whole submitted string).

ElevenLabs charges for everything sent — including `eleven_v3` audio tags
like `[excited]` — so this is just `len(text)`. A named function keeps the
billing definition in one place if it ever needs to change.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> billable_chars("hello")
5
>>> billable_chars(None)
0
```

### braidio.cost.estimate_cost(source, , model_id=None)

Estimate ElevenLabs spend for a [`braidio.Script`](_autosummary/braidio.html.md#braidio.Script) or a raw string.

Free/local work (segment extraction, weaving) contributes nothing. The returned
[`CostRollup`](_autosummary/braidio.cost.html.md#braidio.cost.CostRollup) reports exact characters and an honest dollar sum (a lower
bound when some text is unpriced; see [`usd_per_1k_chars()`](_autosummary/braidio.cost.html.md#braidio.cost.usd_per_1k_chars)).

* **Return type:**
  [`CostRollup`](_autosummary/braidio.cost.html.md#braidio.cost.CostRollup)

```pycon
>>> from braidio import Script, Narration, SegmentBeat
>>> s = Script(title="x", id_slug="01", beats=[
...     Narration(text="a" * 500), SegmentBeat(reference="clip:1")])
>>> estimate_cost(s).characters  # only the narration counts; the clip is free
500
```

### braidio.cost.tts_cost_usd(text, , model_id=None)

Estimated USD to synthesize `text`; `0.0` for empty, `None` if unpriced.

Empty text is genuinely free (`0.0`) regardless of the rate; non-empty text
is `None` only when [`usd_per_1k_chars()`](_autosummary/braidio.cost.html.md#braidio.cost.usd_per_1k_chars) is unpriced.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> tts_cost_usd("")
0.0
```

### braidio.cost.usd_per_1k_chars(model_id=None)

Resolved USD-per-1000-characters rate (most specific source first).

Resolution: a confirmed per-model rate in [`MODEL_USD_PER_1K_CHARS`](_autosummary/braidio.cost.html.md#braidio.cost.MODEL_USD_PER_1K_CHARS) →
the env override [`RATE_ENV_VAR`](_autosummary/braidio.cost.html.md#braidio.cost.RATE_ENV_VAR) → [`DEFAULT_USD_PER_1K_CHARS`](_autosummary/braidio.cost.html.md#braidio.cost.DEFAULT_USD_PER_1K_CHARS).
Returns `None` (*unpriced*, not free) when the env override is explicitly
disabled (one of `_UNPRICED_SENTINELS`) or is not a finite, non-negative
number — a bad rate must never silently become a dishonest negative/NaN spend.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]


# _autosummary/braidio.defaults.html.md

# braidio.defaults

User-overridable, persisted defaults for how braidio renders a voice.

**Why this module exists.** The package default used to be
`eleven_multilingual_v2`, a model that cannot render inline `[audio tags]`
*at all*. Since tag density is the main lever on whether a read sounds alive
(measured: plain text 70.8 Hz of pitch range, densely tagged 124.4 Hz, while
`stability` across its whole range moves it only 85 -> 72 Hz), the old default
capped every production at the flat end before an author had written a word. One
listener’s verdict on such a render was “somniferous”.

So the package default is now the expressive one. But a default nobody can
change is just a different imposition, hence the resolution order:

> explicit argument  >  environment variable  >  user config file  >  package default

The config file is JSON at `$BRAIDIO_CONFIG`, else
`$XDG_CONFIG_HOME/braidio/config.json`, else `~/.config/braidio/config.json`:

```json
{
  "delivery": "v3-presenter",
  "voice_id": "iP95p4xoKVk53GoZ742B",
  "voice_settings": {"stability": 0.0, "use_speaker_boost": true}
}
```

Every key is optional; what you leave out keeps the package default. Nothing
here touches the network, and a malformed file is reported once and then ignored
rather than taking a render down with it.

### Module Attributes

| [`PACKAGE_DEFAULT_DELIVERY`](_autosummary/braidio.defaults.html.md#braidio.defaults.PACKAGE_DEFAULT_DELIVERY)   | What braidio uses when nothing else says otherwise.   |
|-----------------------------------------------------------------------------|-------------------------------------------------------|

### Functions

| [`config_path`](_autosummary/braidio.defaults.html.md#braidio.defaults.config_path)()                | Where the user's persisted defaults live (the file need not exist).   |
|-------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| [`user_config`](_autosummary/braidio.defaults.html.md#braidio.defaults.user_config)()                | The user's persisted defaults, or `{}`.                               |
| [`default_delivery`](_autosummary/braidio.defaults.html.md#braidio.defaults.default_delivery)([explicit]) | Resolve the delivery to render with.                                  |
| [`default_voice_id`](_autosummary/braidio.defaults.html.md#braidio.defaults.default_voice_id)([explicit]) | Resolve the narration voice id, or `None` to let the caller decide.   |
| [`default_voice_settings`](_autosummary/braidio.defaults.html.md#braidio.defaults.default_voice_settings)()     | The resolved delivery's voice settings, as a fresh dict.              |
| [`describe_defaults`](_autosummary/braidio.defaults.html.md#braidio.defaults.describe_defaults)()          | What is in force, and where each part came from.                      |

### braidio.defaults.PACKAGE_DEFAULT_DELIVERY *: [Delivery](_autosummary/braidio.delivery.html.md#braidio.delivery.Delivery)* *= Delivery(name='v3-presenter', model_id='eleven_v3', voice_settings={'stability': 0.0, 'use_speaker_boost': True}, supports_audio_tags=True, supports_speed=False, note='Host/presenter commentary on v3 -- the expressive end. The narration-spine default (solo_explainer). Colour comes from tag density in the script; this setting just gets out of its way.')*

What braidio uses when nothing else says otherwise. `eleven_v3` so inline
`[audio tags]` fire, at the expressive end of `stability`.

### braidio.defaults.config_path()

Where the user’s persisted defaults live (the file need not exist).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### braidio.defaults.default_delivery(explicit=None)

Resolve the delivery to render with.

* **Parameters:**
  **explicit** ([`Delivery`](_autosummary/braidio.delivery.html.md#braidio.delivery.Delivery) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – a [`Delivery`](_autosummary/braidio.delivery.html.md#braidio.delivery.Delivery), or the name of one
  (see `braidio.delivery.DELIVERIES`). Wins over everything.
* **Return type:**
  [`Delivery`](_autosummary/braidio.delivery.html.md#braidio.delivery.Delivery)

### Examples

```pycon
>>> default_delivery(V3_PRESENTER) is V3_PRESENTER
True
>>> default_delivery("v3-narrator").name
'v3-narrator'
```

### braidio.defaults.default_voice_id(explicit=None)

Resolve the narration voice id, or `None` to let the caller decide.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### braidio.defaults.default_voice_settings()

The resolved delivery’s voice settings, as a fresh dict.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### braidio.defaults.describe_defaults()

What is in force, and where each part came from.

Worth printing when a render does not sound the way somebody expected: the
commonest cause is a config file they forgot they wrote.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### braidio.defaults.user_config()

The user’s persisted defaults, or `{}`.

A missing file is normal. A malformed one warns *once* per path and is then
treated as absent.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]


# _autosummary/braidio.delivery.html.md

# braidio.delivery

Narration *delivery* presets — model + voice settings (issue #10, expressiveness).

A [`Delivery`](_autosummary/braidio.delivery.html.md#braidio.delivery.Delivery) bundles the ElevenLabs `model_id` and `voice_settings`
that shape how expressive vs flat a narration reads. The renderer takes one so
we can A/B the same script under different deliveries and pick what has the most
relief without editing the script.

Presets here are a starting point tuned from `docs/research/expressive-tts-
narration.md`; refine as we learn what sounds best. The key monotony levers:
lower `stability` and raise `style` add variation (at some cost in
consistency); `eleven_v3` adds inline audio tags for real expressive control.

### Classes

| [`Delivery`](_autosummary/braidio.delivery.html.md#braidio.delivery.Delivery)(name, model_id[, voice_settings, ...])   | A named narration delivery: which model + voice settings to synthesize with.   |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|

### *class* braidio.delivery.Delivery(name, model_id, voice_settings=<factory>, supports_audio_tags=False, supports_speed=True, note='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A named narration delivery: which model + voice settings to synthesize with.

#### supports_speed *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

Whether the model honors `voice_settings["speed"]`. \*\*False for eleven
v3\*\*, which has no speed control at all (“Speed is not available for the
Eleven v3 model”), so sending one there is undefined. The paced narration
path ([`braidio.pacing`](_autosummary/braidio.pacing.html.md#module-braidio.pacing)) reads this: on a speedless model it plans no
speed and varies tempo through real inter-turn silence, punctuation and
audio tags instead.


# _autosummary/braidio.formats.html.md

# braidio.formats

Ready-made **format templates** — high-quality presets under standard names.

braidio parametrizes *any* commentary style (a [`Script`](_autosummary/braidio.script.html.md#braidio.script.Script) of
`Narration` / `Dialogue` / `SegmentBeat` beats, cast via
[`ConversationCast`](_autosummary/braidio.conversation.html.md#braidio.conversation.ConversationCast), tuned by
[`WeaveConfig`](_autosummary/braidio.weave_config.html.md#braidio.weave_config.WeaveConfig) and [`Delivery`](_autosummary/braidio.delivery.html.md#braidio.delivery.Delivery)).
This module ships the *ready-made* end of that: a small set of named
[`Format`](_autosummary/braidio.formats.html.md#braidio.formats.Format) presets that bundle good defaults for the recurring, industry-named
ways people comment on an artifact — so `render_format(DEEP_DIVE, script, …)`
just works, and advanced users still compose the primitives directly.

The taxonomy + recipes are in
`misc/docs/research/commentary-formats-and-styles.md`. The organizing rule:
\*\*the talk is the spine; narration bridges and source clips are optional
“illustration” layers\*\* attached to it.

What a [`Format`](_autosummary/braidio.formats.html.md#braidio.formats.Format) drives at render time: the dialogue **cast**
(role→voice), the **narration voice + delivery**, the \*\*clip weave/duck +
loudness\*\* (`WeaveConfig`), and the **structural music**
([`MusicStructure`](_autosummary/braidio.structure.html.md#braidio.structure.MusicStructure)) — whether a
`SceneBreak` beat plays a sting, and whether the bed drops out under exhibits
(fade-to-spotlight). Per-clip placement renders too — set it on each
`SegmentBeat(placement=…)`; `Format.clip_placement` is the recommended
*default* for the format. Music is app-supplied: a **music bed** renders when you
pass a `bed_asset` to [`render_format()`](_autosummary/braidio.formats.html.md#braidio.formats.render_format) (the format’s `music_bed`
*intensity* picks the gain) and scene **stings** play when you pass a
`sting_asset` (without one a scene break is a beat of silence). Fields tagged
 *(authoring)* — `roles`, `scripting` — are conventions for whoever writes
(or generates) the `Script`. Preset ids mirror the standard names so a UI can
label them (“Deep Dive”, “Song Exploder-style”). The structural-music design
history is braidio#25.

### Functions

| [`describe_asset_application`](_autosummary/braidio.formats.html.md#braidio.formats.describe_asset_application)(fmt, script, \*[, ...])   | Which of the supplied `bed_asset` / `sting_asset` this format will actually render, and why not otherwise (braidio#43).           |
|-------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| [`render_format`](_autosummary/braidio.formats.html.md#braidio.formats.render_format)(fmt, script, \*, source[, ...])        | Render `script` under `fmt`'s defaults; `overrides` win over them.                                                                |
| [`sting_would_play`](_autosummary/braidio.formats.html.md#braidio.formats.sting_would_play)(structure, script, sting_asset)     | Whether some `SceneBreak` in `script` resolves to marker `"sting"` under `structure` once `sting_asset` is wired in as the sting. |

### Classes

| [`Format`](_autosummary/braidio.formats.html.md#braidio.formats.Format)(id, name, summary[, aka, cast, ...])   | A named commentary-format preset: a bundle of high-quality defaults.   |
|------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|

### *class* braidio.formats.Format(id, name, summary, aka=(), cast=None, narration_voice=None, narration_delivery=Delivery(name='v2-presenter', model_id='eleven_multilingual_v2', voice_settings={'stability': 0.35, 'similarity_boost': 0.75, 'style': 0.35, 'use_speaker_boost': True, 'speed': 0.98}, supports_audio_tags=False, supports_speed=True, note='Host/presenter commentary — lively (== v2-tuned), for the spine voice.'), weave=<factory>, structure=<factory>, roles=<factory>, clip_placement='before', music_bed='light', scripting='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A named commentary-format preset: a bundle of high-quality defaults.

Rendered fields drive [`render_format()`](_autosummary/braidio.formats.html.md#braidio.formats.render_format) → [`braidio.render.render_production()`](_autosummary/braidio.render.html.md#braidio.render.render_production).
Authoring fields document how to write a `Script` for this format;
`clip_placement` is the recommended per-beat default and `music_bed` the
bed intensity applied when a `bed_asset` is supplied.

#### render(script, , source, out_path=None, profile=Profile.PERSONAL, \*\*overrides)

Render `script` with this format’s defaults (see [`render_format()`](_autosummary/braidio.formats.html.md#braidio.formats.render_format)).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### braidio.formats.describe_asset_application(fmt, script, , bed_asset=None, sting_asset=None)

Which of the supplied `bed_asset` / `sting_asset` this format will
actually render, and why not otherwise (braidio#43).

Pure and pre-render — safe to call before paying for anything. A key stays
`None` when its asset wasn’t supplied. `bed_asset`’s fate is fixed by
`fmt.music_bed` alone: `"none"` never renders a bed, and
[`render_format()`](_autosummary/braidio.formats.html.md#braidio.formats.render_format) refuses `bed_asset` there rather than spend on one
that would be dropped, so `bed_applied` is `False` only via that refusal
path, never in a result you got back from a successful render.
`sting_asset`’s fate additionally depends on `script`: a scene break’s
marker can override the format’s default, so a sting can still legitimately
go unused in one script and play in another under the same format — that
case is reported here, not refused.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`bool`](https://docs.python.org/3/builtins/functions.html#bool) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]

### braidio.formats.render_format(fmt, script, , source, out_path=None, profile=Profile.PERSONAL, bed_asset=None, sting_asset=None, \*\*overrides)

Render `script` under `fmt`’s defaults; `overrides` win over them.

Wires the format’s `cast` / `narration_voice` / `narration_delivery` /
`weave` / `structure` into [`braidio.render.render_production()`](_autosummary/braidio.render.html.md#braidio.render.render_production). Any
beat may still override voice/settings per-beat (e.g. a graver book-narrator
inside an otherwise lively presenter piece — pass `V2_NARRATOR.voice_settings`
on that `Narration` beat).

`bed_asset` (a path to an app-supplied instrumental) adds a music bed at the
gain implied by `fmt.music_bed`; pass `music_bed=MusicBed(...)` in
`overrides` for full control. A format whose `music_bed` is `"none"`
(e.g. `SONG_EXPLODER`) never renders a bed at all, so `bed_asset` there
raises `ValueError` *before* any rendering — the caller would otherwise pay
for a bed the format silently drops (braidio#43) — unless `overrides`
itself supplies `music_bed=`, which always wins and makes the refusal moot.
`sting_asset` (a path to an app-supplied short marker) is what a
`SceneBreak` plays under the format’s `structure`; pass
`structure=MusicStructure(...)` in `overrides` for full control. Unlike
the bed, a format’s `scene_marker` is only the *default* — an individual
`SceneBreak.marker` override can still play the sting even under a
`"none"` default — so a sting that ends up unused is not refused, only
reported (see [`describe_asset_application()`](_autosummary/braidio.formats.html.md#braidio.formats.describe_asset_application)).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### braidio.formats.sting_would_play(structure, script, sting_asset)

Whether some `SceneBreak` in `script` resolves to marker `"sting"`
under `structure` once `sting_asset` is wired in as the sting.

Takes a bare `MusicStructure` (not a [`Format`](_autosummary/braidio.formats.html.md#braidio.formats.Format)) so callers with
no format at all — e.g. the graph path’s `_graph_structure` with no
`format_id` — can still ask against the ad hoc default structure they
build in that case (braidio#53).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)


# _autosummary/braidio.html.md

# braidio

braidio — weave narration with extracted media segments into productions.

Braid two kinds of strand into one production: authored **narration** (TTS —
single voice or a cycled pool) and extracted **segments** of source media (song
clips, audiobook passages, news, SFX). The result renders to an audiovisual
object.

This is the **functional core** (pure Python over files + numbers; deps:
`mixing`, `elevenlabs`, and `ffmpeg` on PATH). An optional nw-app layer
(graph bodies, transforms, provenance / partial re-render) will be added on top
and imported only when `nw` is available — `import braidio` never requires
it.

Extracted from the Hamilton lyrics-podcast; the extraction is designed in
Hamilton#18 (the epic) and Hamilton#28 (placement + naming). Those numbers are
in the *Hamilton* repo — braidio has its own #18 about something else.

### Functions

| [`skills_dir`](_autosummary/braidio.html.md#braidio.skills_dir)()                                       | Path to the agent skills that ship with braidio.                                                                        |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
| [`captions_for`](_autosummary/braidio.html.md#braidio.captions_for)(script, timeline, \*[, max_chars])    | The SRT document for `script` as laid out by `timeline`.                                                                |
| [`cues_for`](_autosummary/braidio.html.md#braidio.cues_for)(script, timeline, \*[, max_chars])        | Subtitle cues for `script` as laid out by `timeline`.                                                                   |
| [`format_srt`](_autosummary/braidio.html.md#braidio.format_srt)(cues)                                   | Render `cues` as an SRT document.                                                                                       |
| [`narration_segments`](_autosummary/braidio.html.md#braidio.narration_segments)(script)                         | All narration (default text) of a script, as sentence-level segments.                                                   |
| [`plan_production`](_autosummary/braidio.html.md#braidio.plan_production)(script, profile, \*[, ...])        | Filter `script` into the beats renderable under `profile`.                                                              |
| [`clip_plays_under`](_autosummary/braidio.html.md#braidio.clip_plays_under)(profile, rights[, publishable])   | Whether a segment with `rights` plays as audio under `profile`.                                                         |
| [`rights_are_publishable`](_autosummary/braidio.html.md#braidio.rights_are_publishable)(rights[, publishable])      | The publishable test at the level of a bare `rights` string.                                                            |
| [`find_verbatim_text`](_autosummary/braidio.html.md#braidio.find_verbatim_text)(text, forbidden, \*[, ...])     | Forbidden lines that appear (near-)verbatim in `text`.                                                                  |
| [`content_violations`](_autosummary/braidio.html.md#braidio.content_violations)(plan, forbidden, \*[, ...])     | Rights violations in a *published* plan (empty list = clean).                                                           |
| [`segment_is_publishable`](_autosummary/braidio.html.md#braidio.segment_is_publishable)(beat[, publishable])        | Whether `beat`'s audio may play in the published cut.                                                                   |
| [`render_production`](_autosummary/braidio.html.md#braidio.render_production)(script, \*, source[, ...])       | Render `script` under `profile` → a single audio file.                                                                  |
| [`find_segment`](_autosummary/braidio.html.md#braidio.find_segment)(lines, quote, \*[, max_span, ...])    | Best contiguous run of timed lines matching `quote`, or `None`.                                                         |
| [`load_timing`](_autosummary/braidio.html.md#braidio.load_timing)(path)                                  | Load a `{lines: [{index,start_s,end_s,text}]}` JSON into <br/><br/>```<br/>`<br/>```<br/><br/>TimedLine\`s.             |
| [`cut_quote`](_autosummary/braidio.html.md#braidio.cut_quote)(audio_path, lines, quote, out_path, \*)  | Resolve `quote` → segment and cut it from `audio_path` (pad + fades).                                                   |
| [`narrate`](_autosummary/braidio.html.md#braidio.narrate)(text, out_path, \*[, api_key, ...])        | Synthesize `text` to `out_path` (mp3).                                                                                  |
| [`resolve_voice_id`](_autosummary/braidio.html.md#braidio.resolve_voice_id)([voice_id])                       | Voice id from arg → `VOICE_ENV_VAR` env → default.                                                                      |
| [`estimate_cost`](_autosummary/braidio.html.md#braidio.estimate_cost)(source, \*[, model_id])              | Estimate ElevenLabs spend for a [`braidio.Script`](_autosummary/braidio.html.md#braidio.Script) or a raw string.        |
| [`tts_cost_usd`](_autosummary/braidio.html.md#braidio.tts_cost_usd)(text, \*[, model_id])                 | Estimated USD to synthesize `text`; `0.0` for empty, `None` if unpriced.                                                |
| [`billable_chars`](_autosummary/braidio.html.md#braidio.billable_chars)(text)                               | Characters ElevenLabs bills for `text` (the whole submitted string).                                                    |
| [`usd_per_1k_chars`](_autosummary/braidio.html.md#braidio.usd_per_1k_chars)([model_id])                       | Resolved USD-per-1000-characters rate (most specific source first).                                                     |
| `strip_markup`(text)                                                                                |                                                                                                                         |
| [`split_segments`](_autosummary/braidio.html.md#braidio.split_segments)(text)                               | Split narration into sentence-level segments (markup removed).                                                          |
| [`assign_voices`](_autosummary/braidio.html.md#braidio.assign_voices)(n, pool, \*[, seed, avoid_repeats])  | Assign a voice to each of `n` turns — random, seeded, no immediate repeats (so it isn't a rigid round-robin).           |
| [`group_turns`](_autosummary/braidio.html.md#braidio.group_turns)(segments, \*[, min_turn, ...])         | Group consecutive segments into *turns* of `min_turn..max_turn` segments.                                               |
| [`render_multivoice`](_autosummary/braidio.html.md#braidio.render_multivoice)(segments, pool, \*, out_path)    | Render `segments` cycling `pool` → `out_path`.                                                                          |
| [`bed_for_intensity`](_autosummary/braidio.html.md#braidio.bed_for_intensity)(asset_path, intensity, ...)      | Build a [`MusicBed`](_autosummary/braidio.html.md#braidio.MusicBed) at the gain for a Format `music_bed` intensity.       |
| [`render_format`](_autosummary/braidio.html.md#braidio.render_format)(fmt, script, \*, source[, ...])      | Render `script` under `fmt`'s defaults; `overrides` win over them.                                                      |
| [`describe_asset_application`](_autosummary/braidio.html.md#braidio.describe_asset_application)(fmt, script, \*[, ...]) | Which of the supplied `bed_asset` / `sting_asset` this format will actually render, and why not otherwise (braidio#43). |
| [`compose_narration`](_autosummary/braidio.html.md#braidio.compose_narration)(segments, config, \*, out_path)  | Render `segments` under `config` → `out_path`.                                                                          |
| [`extract_padded`](_autosummary/braidio.html.md#braidio.extract_padded)(asset_path, start_s, end_s, ...)    | Extract `[start_s-pre_roll, end_s+post_roll]` with in/out fades.                                                        |
| [`weave_timeline`](_autosummary/braidio.html.md#braidio.weave_timeline)(items, out_path, \*[, ...])         | Place items on a timeline and mix.                                                                                      |
| [`layout_starts`](_autosummary/braidio.html.md#braidio.layout_starts)(kinds, durs, \*, ...)                | Start offset (s) of each part (all sequential).                                                                         |
| `duration_s`(path)                                                                                  |                                                                                                                         |
| [`build_timeline`](_autosummary/braidio.html.md#braidio.build_timeline)(\*, kinds, durations[, ...])        | Assemble a [`TimelineBreakdown`](_autosummary/braidio.html.md#braidio.TimelineBreakdown) from per-beat render data (pure).         |
| [`render_settings`](_autosummary/braidio.html.md#braidio.render_settings)(\*, config, crossfade_s, ...)      | The record of *how* a production was rendered, as plain JSON types.                                                     |
| [`clean_ocr`](_autosummary/braidio.html.md#braidio.clean_ocr)(text, \*[, collapse_whitespace])         | Normalize OCR/PDF-extracted text for clean narration.                                                                   |
| [`strip_speaker_labels`](_autosummary/braidio.html.md#braidio.strip_speaker_labels)(text)                         | Remove a leading speaker-label prefix (e.g. `"Chris: "`) if present.                                                    |
| [`config_path`](_autosummary/braidio.html.md#braidio.config_path)()                                      | Where the user's persisted defaults live (the file need not exist).                                                     |
| [`default_delivery`](_autosummary/braidio.html.md#braidio.default_delivery)([explicit])                       | Resolve the delivery to render with.                                                                                    |
| [`default_voice_id`](_autosummary/braidio.html.md#braidio.default_voice_id)([explicit])                       | Resolve the narration voice id, or `None` to let the caller decide.                                                     |
| [`default_voice_settings`](_autosummary/braidio.html.md#braidio.default_voice_settings)()                           | The resolved delivery's voice settings, as a fresh dict.                                                                |
| [`describe_defaults`](_autosummary/braidio.html.md#braidio.describe_defaults)()                                | What is in force, and where each part came from.                                                                        |
| [`user_config`](_autosummary/braidio.html.md#braidio.user_config)()                                      | The user's persisted defaults, or `{}`.                                                                                 |
| [`audit_platitudes`](_autosummary/braidio.html.md#braidio.audit_platitudes)(text)                             | Return every [`Finding`](_autosummary/braidio.html.md#braidio.Finding) in `text`, in document order.                     |
| [`audit_expressiveness`](_autosummary/braidio.html.md#braidio.audit_expressiveness)(text)                         | Human-readable complaints about a script's written-in performance.                                                      |
| [`audio_tag_rate`](_autosummary/braidio.html.md#braidio.audio_tag_rate)(text, \*[, per])                    | Inline audio tags per `per` words — the expressiveness dial.                                                            |
| [`audio_tags`](_autosummary/braidio.html.md#braidio.audio_tags)(text)                                   | Every inline `[audio tag]` in `text`, in order.                                                                         |
| [`platitude_rate`](_autosummary/braidio.html.md#braidio.platitude_rate)(text, \*[, per])                    | Flagged hits per `per` words (default 1000).                                                                            |

### Classes

| [`WeaveKind`](_autosummary/braidio.html.md#braidio.WeaveKind)(\*values)                               | A braidio production kind.                                                                                         |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
| [`Cue`](_autosummary/braidio.html.md#braidio.Cue)(start_s, end_s, text)                         | One subtitle: `[start_s, end_s)` and the text shown.                                                               |
| [`Script`](_autosummary/braidio.html.md#braidio.Script)(title, id_slug[, beats])                   | An ordered production script.                                                                                      |
| [`Narration`](_autosummary/braidio.html.md#braidio.Narration)(text[, style, published_text, ...])     | A spoken narration beat (authored, synthesized by TTS).                                                            |
| [`SegmentBeat`](_autosummary/braidio.html.md#braidio.SegmentBeat)(reference[, label, rights, ...])      | A span of source media to weave in, addressed by an opaque `reference`.                                            |
| [`Dialogue`](_autosummary/braidio.html.md#braidio.Dialogue)(turns[, label])                          | A multi-speaker commentary exchange (the conversational register).                                                 |
| [`SceneBreak`](_autosummary/braidio.html.md#braidio.SceneBreak)([label, marker])                       | A structural boundary between sections — the "new scene" beat.                                                     |
| [`Sting`](_autosummary/braidio.html.md#braidio.Sting)(asset_path[, gain_db, max_len_s, ...])      | A short musical marker played at a scene break.                                                                    |
| [`MusicStructure`](_autosummary/braidio.html.md#braidio.MusicStructure)([sting, scene_marker, ...])        | How a production marks its structure with music — the render seam.                                                 |
| [`Profile`](_autosummary/braidio.html.md#braidio.Profile)(\*values)                                 | Which projection of the production we render.                                                                      |
| [`RightsPolicy`](_autosummary/braidio.html.md#braidio.RightsPolicy)([forbidden_texts, ...])              | Injected rights configuration for the published profile.                                                           |
| [`RenderPlan`](_autosummary/braidio.html.md#braidio.RenderPlan)(profile[, beats, dropped, ...])        |                                                                                                                    |
| [`PlannedBeat`](_autosummary/braidio.html.md#braidio.PlannedBeat)(kind, content, from_index[, ...])     | A beat resolved for a profile — what the renderer actually plays.                                                  |
| [`SegmentSource`](_autosummary/braidio.html.md#braidio.SegmentSource)(\*args, \*\*kwargs)                 | Resolve an opaque `reference` to a [`ResolvedSegment`](_autosummary/braidio.html.md#braidio.ResolvedSegment) (or None).     |
| [`ResolvedSegment`](_autosummary/braidio.html.md#braidio.ResolvedSegment)(asset_path, start_s, end_s)       | A cuttable span of a source asset — what a [`SegmentSource`](_autosummary/braidio.html.md#braidio.SegmentSource) returns. |
| [`Segment`](_autosummary/braidio.html.md#braidio.Segment)(start_s, end_s, score, line_start, ...)   | A resolved window for a reference, with the matched line span.                                                     |
| [`TimedLine`](_autosummary/braidio.html.md#braidio.TimedLine)(index, start_s, end_s, text)            | A source line with a `[start_s, end_s)` window (end may be None = tail).                                           |
| [`NamespacedSegmentSource`](_autosummary/braidio.html.md#braidio.NamespacedSegmentSource)(sources, \*[, sep, ...])  | A [`SegmentSource`](_autosummary/braidio.html.md#braidio.SegmentSource) that routes prefixed references to sub-sources.   |
| [`TimedLineSegmentSource`](_autosummary/braidio.html.md#braidio.TimedLineSegmentSource)(\*, lines, asset_path)     | A [`SegmentSource`](_autosummary/braidio.html.md#braidio.SegmentSource) over time-aligned lines + one source asset.       |
| [`CostRollup`](_autosummary/braidio.html.md#braidio.CostRollup)(characters, usd, unpriced[, lines])    | A production's estimated TTS spend, exact on characters, honest on dollars.                                        |
| [`CostLine`](_autosummary/braidio.html.md#braidio.CostLine)(label, kind, characters, usd, model_id)  | One billable line of an estimate (a narration beat or a dialogue beat).                                            |
| [`Delivery`](_autosummary/braidio.html.md#braidio.Delivery)(name, model_id[, voice_settings, ...])   | A named narration delivery: which model + voice settings to synthesize with.                                       |
| [`Voice`](_autosummary/braidio.html.md#braidio.Voice)(id, name, gender[, accent, note])           | A pooled narration voice.                                                                                          |
| [`WeaveConfig`](_autosummary/braidio.html.md#braidio.WeaveConfig)([voices, pool_label, ...])            | All editing choices for a narration+segment weave.                                                                 |
| [`MusicBed`](_autosummary/braidio.html.md#braidio.MusicBed)(asset_path[, gain_db, fade_in_s, ...])   | An instrumental underscore spanning the production, mixed under the talk.                                          |
| [`Format`](_autosummary/braidio.html.md#braidio.Format)(id, name, summary[, aka, cast, ...])       | A named commentary-format preset: a bundle of high-quality defaults.                                               |
| [`TimelineItem`](_autosummary/braidio.html.md#braidio.TimelineItem)(kind, path[, placement, ...])        | One part on the weave timeline.                                                                                    |
| [`BeatSpan`](_autosummary/braidio.html.md#braidio.BeatSpan)(index, kind[, label, source_start, ...]) | One beat's place on the timeline.                                                                                  |
| [`TimelineBreakdown`](_autosummary/braidio.html.md#braidio.TimelineBreakdown)([beats, title, settings])       | The ordered beats of a production, with per-kind totals and an HTML view.                                          |
| [`Finding`](_autosummary/braidio.html.md#braidio.Finding)(pattern, match, start)                    | One flagged platitude.                                                                                             |

### Exceptions

| [`RightsViolation`](_autosummary/braidio.html.md#braidio.RightsViolation)   | A render would play source audio the profile it claims forbids.   |
|--------------------------------------------------------------------|-------------------------------------------------------------------|

### *class* braidio.BeatSpan(index, kind, label='', source_start=None, source_end=None, duration=0.0, start=0.0, placement=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One beat’s place on the timeline.

### *class* braidio.CostLine(label, kind, characters, usd, model_id)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One billable line of an estimate (a narration beat or a dialogue beat).

Named to echo `falaw.CostLine` (a line within a rollup) rather than
`falaw.CostEstimate` (which means a per-call price spec) — so the vocabulary
is consistent across the federation.

### *class* braidio.CostRollup(characters, usd, unpriced, lines=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A production’s estimated TTS spend, exact on characters, honest on dollars.

`usd` is the sum of the *priced* lines; `unpriced` is `True` when some
billable text had no configured rate (so `usd` is a lower bound). No billable
lines (e.g. an all-segment script) gives `characters=0, usd=0.0,
unpriced=False`. Named `CostRollup` to match `falaw.CostRollup`.

#### *property* summary *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

One-line human summary (handy for a CLI/MCP preview).

### *class* braidio.Cue(start_s, end_s, text)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One subtitle: `[start_s, end_s)` and the text shown.

### *class* braidio.Delivery(name, model_id, voice_settings=<factory>, supports_audio_tags=False, supports_speed=True, note='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A named narration delivery: which model + voice settings to synthesize with.

#### supports_speed *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

Whether the model honors `voice_settings["speed"]`. \*\*False for eleven
v3\*\*, which has no speed control at all (“Speed is not available for the
Eleven v3 model”), so sending one there is undefined. The paced narration
path ([`braidio.pacing`](_autosummary/braidio.pacing.html.md#module-braidio.pacing)) reads this: on a speedless model it plans no
speed and varies tempo through real inter-turn silence, punctuation and
audio tags instead.

### *class* braidio.Dialogue(turns, label='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A multi-speaker commentary exchange (the conversational register).

`turns` is an ordered tuple of `(role, text)` pairs (roles like
`"A"`/`"B"` map to voices via a [`ConversationCast`](_autosummary/braidio.conversation.html.md#braidio.conversation.ConversationCast)
at render time). Rendered in ONE pass via Text-to-Dialogue so it sounds like
people talking to each other. This is our own commentary → always publishable
(its text is still scanned for forbidden verbatim quotes in the published cut).

### *class* braidio.Finding(pattern, match, start)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One flagged platitude.

### *class* braidio.Format(id, name, summary, aka=(), cast=None, narration_voice=None, narration_delivery=Delivery(name='v2-presenter', model_id='eleven_multilingual_v2', voice_settings={'stability': 0.35, 'similarity_boost': 0.75, 'style': 0.35, 'use_speaker_boost': True, 'speed': 0.98}, supports_audio_tags=False, supports_speed=True, note='Host/presenter commentary — lively (== v2-tuned), for the spine voice.'), weave=<factory>, structure=<factory>, roles=<factory>, clip_placement='before', music_bed='light', scripting='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A named commentary-format preset: a bundle of high-quality defaults.

Rendered fields drive [`render_format()`](_autosummary/braidio.html.md#braidio.render_format) → [`braidio.render.render_production()`](_autosummary/braidio.render.html.md#braidio.render.render_production).
Authoring fields document how to write a `Script` for this format;
`clip_placement` is the recommended per-beat default and `music_bed` the
bed intensity applied when a `bed_asset` is supplied.

#### render(script, , source, out_path=None, profile=Profile.PERSONAL, \*\*overrides)

Render `script` with this format’s defaults (see [`render_format()`](_autosummary/braidio.html.md#braidio.render_format)).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### *class* braidio.MusicBed(asset_path, gain_db=-22.0, fade_in_s=1.5, fade_out_s=2.0, lead_in_s=2.0, start_s=0.0, loop=True, spotlight_fade_s=0.6)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

An instrumental underscore spanning the production, mixed under the talk.

`asset_path` is an app-supplied instrumental. `gain_db` sets how far under
the voice it sits; `lead_in_s` posts the bed *after* speech onset so its
entrance feels motivated; `start_s` is the in-point into the asset;
`loop` repeats the asset to cover a timeline longer than it.
`spotlight_fade_s` is how fast the bed drops out before a spotlit exhibit
(it resumes after the exhibit over `fade_in_s`).

### *class* braidio.MusicStructure(sting=None, scene_marker='sting', spotlight_clips=False, pause_s=0.8)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

How a production marks its structure with music — the render seam.

`scene_marker` is the default for a [`SceneBreak`](_autosummary/braidio.script.html.md#braidio.script.SceneBreak)
whose `marker` is `None` (`"sting"` / `"none"`). `spotlight_clips`
is the default for a [`SegmentBeat`](_autosummary/braidio.script.html.md#braidio.script.SegmentBeat) whose
`spotlight` is `None` — `True` drops the bed under every exhibit.
`sting` is the production’s sting asset; with none supplied a
`"sting"`-marked break falls back to `pause_s` of silence.

The all-defaults value is inert for a script with no scene breaks and no
spotlight-marked clips: a format that declares no structure renders exactly
as it did without this layer.

#### marker_for(beat)

The marker a scene break renders with (its override, else the default).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### marker_of(marker)

The marker a break with this per-beat override renders with.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### plays_sting(beat)

Whether `beat` plays the sting (marked `"sting"` *and* one is supplied).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### plays_sting_of(marker)

Whether this per-beat marker plays the sting (and one is supplied).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### spotlight_for(beat)

Whether `beat` is spotlit (its override, else the format default).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### spotlight_of(spotlight)

Whether this per-beat override is spotlit (`None` = the format default).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### *class* braidio.NamespacedSegmentSource(sources, , sep=':', default=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A [`SegmentSource`](_autosummary/braidio.html.md#braidio.SegmentSource) that routes prefixed references to sub-sources.

A production that cuts between *several* recordings of the same work — two
takes of one song, a studio master and a live version, an audiobook read in
two languages — hits a wall: `render_production(..., source=...)` takes one
[`SegmentSource`](_autosummary/braidio.html.md#braidio.SegmentSource), and a reference like a lyric line matches equally
well in every recording. Which asset a quote should come from is a
*production* decision, not something a token matcher can infer.

This routes on an explicit prefix, so the script says which recording it
means:

```default
source = NamespacedSegmentSource(
    {
        "1966": TimedLineSegmentSource(lines=studio, asset_path="studio.mp3"),
        "1981": TimedLineSegmentSource(lines=live, asset_path="live.mp3"),
    }
)
SegmentBeat("1981: and in the naked light I saw")  # → the live master
```

* **Parameters:**
  * **sources** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource)]) – Namespace → sub-source. Namespaces are matched
    case-insensitively and with surrounding whitespace stripped.
  * **sep** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Separator between the namespace and the reference body.
  * **default** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Namespace used for an *unprefixed* reference. `None` (the
    default) makes an unprefixed reference an error.
* **Raises:**
  [**KeyError**](https://docs.python.org/3/builtins/exceptions.html#KeyError) – on an unknown namespace, or an unprefixed reference with no
      `default`. This is deliberate: silently falling through to the
      wrong recording would ship one performance under commentary that
      describes another — the failure a listener cannot detect and the
      author cannot see in a diff.

#### *property* namespaces *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

The namespaces this source routes, in insertion order.

#### split(reference)

Split `reference` into `(namespace, body)`, applying the default.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### *class* braidio.Narration(text, style=None, published_text=None, lead_gap_s=0.0, voice=None, voice_settings=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A spoken narration beat (authored, synthesized by TTS).

`published_text` is an optional rights-safe rewrite the published profile
uses when the default `text` quotes forbidden (e.g. copyrighted) content;
leave it `None` when the narration is already clean.

`lead_gap_s` prepends a beat of silence — breathing room before a
register change (e.g. a book-read entering after a conversation), so it
doesn’t feel glued to the previous speaker.

`voice` and `voice_settings` are per-beat overrides so one timeline can
carry contrasting roles — e.g. a lively *presenter* and a graver \*book
narrator\* (different voice, and/or a different delivery preset’s
`voice_settings`). Both fall back to the render’s production-level defaults
when `None`.

### *class* braidio.PlannedBeat(kind, content, from_index, note='', turns=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A beat resolved for a profile — what the renderer actually plays.

`kind` is `"narration"` (synthesize `content`), `"clip"` (resolve
`content` as a segment reference and cut audio), `"dialogue"` (synthesize
`turns`) or `"scene_break"` (a structural boundary — no content; the
renderer marks it with music). `from_index` points at the source beat;
`note` records any substitution/drop reasoning.

### *class* braidio.Profile(\*values)

Bases: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

Which projection of the production we render.

### *class* braidio.RenderPlan(profile, beats=<factory>, dropped=<factory>, substituted=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

### *class* braidio.ResolvedSegment(asset_path, start_s, end_s, score=1.0, matched_text='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A cuttable span of a source asset — what a [`SegmentSource`](_autosummary/braidio.html.md#braidio.SegmentSource) returns.

### *class* braidio.RightsPolicy(forbidden_texts=<function RightsPolicy.<lambda>>, publishable_clip_rights=frozenset({'public-domain'}))

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Injected rights configuration for the published profile.

`forbidden_texts` yields the strings that must not appear verbatim in
published narration (e.g. copyrighted lyric lines). `publishable_clip_rights`
is the set of segment `rights` values allowed in the published cut.

### *exception* braidio.RightsViolation

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A render would play source audio the profile it claims forbids.

Raised where a rights decision is *verified* rather than made — today by the
graph path’s episode transform, which checks the members it is about to
weave against the profile it is about to stamp on them. A `ValueError`
subclass so existing callers keep catching it, typed so a caller that cares
can tell a rights refusal from a malformed input.

### *class* braidio.SceneBreak(label='', marker=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A structural boundary between sections — the “new scene” beat.

Audio has no visual white space, so a boundary has to be *heard*: the
renderer marks it with a sting (a short musical marker) when the production
supplies one and the resolved `marker` is `"sting"`, and otherwise with
a beat of silence. This is the beat the format templates’ structure — a
debate’s open / rebuttal / close, a panel’s rounds, a documentary’s acts —
is expressed with; the talk on either side is unchanged.

`marker` overrides the format default per break (`None` = defer to
`braidio.structure.MusicStructure.scene_marker`). `label` names the
section that starts here (e.g. `"rebuttal"`) for the timeline breakdown.
A scene break synthesizes nothing, so it costs nothing.

### *class* braidio.Script(title, id_slug, beats=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

An ordered production script.

### *class* braidio.Segment(start_s, end_s, score, line_start, line_end, matched_text)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A resolved window for a reference, with the matched line span.

### *class* braidio.SegmentBeat(reference, label='', rights='owned-local', published_substitute=None, placement='before', spotlight=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A span of source media to weave in, addressed by an opaque `reference`.

The renderer resolves `reference` → `[start,end)` via a
[`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource) and cuts it. `rights` (e.g.
`owned-local` / `copyrighted` / `public-domain`) drives the render
profile; `published_substitute` is a transformative narration the
published profile swaps in when the segment’s audio can’t be used.

`placement` is how the clip sits against the talk (the weaving grammar):
`"before"` (default) and `"after"` play the clip **clean** in its own
slot — the set-up→clip and clip→payoff patterns, distinguished by where you
order the beat relative to the talk. `"under"` plays the clip
**concurrently beneath the following talk beat**, ducked by `duck_db` —
the “host talks over the clip” technique (place the clip *immediately before*
the talk it should sit under).

`spotlight` is the fade-to-spotlight override: `True` drops the music
bed out before this clip so it lands in silence rather than competing with
underscore (the bed resumes after it); `False` keeps the bed under it.
`None` (default) defers to the format’s
`braidio.structure.MusicStructure.spotlight_clips`. Without a music
bed the flag is inert.

### *class* braidio.SegmentSource(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Resolve an opaque `reference` to a [`ResolvedSegment`](_autosummary/braidio.html.md#braidio.ResolvedSegment) (or None).

### *class* braidio.Sting(asset_path, gain_db=-6.0, max_len_s=3.0, fade_out_s=0.5, gap_after_s=0.3)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A short musical marker played at a scene break.

`asset_path` is an app-supplied sound (a hit, a riser, a few notes of the
theme). It is trimmed to `max_len_s` with a `fade_out_s` tail, levelled
to the production’s loudness target, sat `gain_db` under the voice, and
followed by `gap_after_s` of breathing room before the talk resumes.

### *class* braidio.TimedLine(index, start_s, end_s, text)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A source line with a `[start_s, end_s)` window (end may be None = tail).

### *class* braidio.TimedLineSegmentSource(, lines, asset_path, song_end_s=None, min_score=0.5)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A [`SegmentSource`](_autosummary/braidio.html.md#braidio.SegmentSource) over time-aligned lines + one source asset.

Binds the generic [`find_segment()`](_autosummary/braidio.html.md#braidio.find_segment) matcher to a concrete asset so the
weave engine can `resolve(reference) -> ResolvedSegment`.

### *class* braidio.TimelineBreakdown(beats=<factory>, title='', settings=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The ordered beats of a production, with per-kind totals and an HTML view.

`settings` is the **render record**: the settings that actually produced
these timings (see [`render_settings()`](_autosummary/braidio.html.md#braidio.render_settings)), or `None` for a breakdown
built by hand. It exists because a render stopped being reproducible from
the script alone the moment `WeaveConfig.segmentation_unit` became a real
switch (braidio#63): the same script now renders to different durations
under different pacing knobs, so a persisted breakdown whose settings are
unknown cannot be re-derived, and any downstream data keyed to its timings
(panel cues, captions) silently stops matching a later re-render. Recording
the settings next to the timings closes that hole on the no-graph fast path,
where the nw/lacing provenance layer is not in play at all.

It is always plain JSON types, so a consumer that already persists the
breakdown gets the record for free.

#### *property* duration *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Total timeline length (s) — the max beat end, accounting for overlaps.

#### *classmethod* from_dict(d)

Rebuild a breakdown from `to_dict()` output (the persisted form).

`totals` and `duration` are derived, so they are recomputed rather
than read back — the beats are the record.

* **Return type:**
  [`TimelineBreakdown`](_autosummary/braidio.timeline.html.md#braidio.timeline.TimelineBreakdown)

```pycon
>>> tl = build_timeline(kinds=["narration", "clip"], durations=[4.0, 2.0],
...                     labels=["a", "b"], source_spans=[None, (1.0, 3.0)])
>>> TimelineBreakdown.from_dict(tl.to_dict()) == tl
True
```

#### shares()

Fraction of spoken+clip time per `kind` (sums to 1).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

#### to_html(title=None, subtitle='')

A self-contained HTML view: totals bar, walking-order timeline, table.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### totals()

Seconds spent per `kind` (insertion-ordered by first appearance).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

### *class* braidio.TimelineItem(kind, path, placement='sequential', duck_db=0.0, spotlight=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One part on the weave timeline.

`placement` `"sequential"` (default — narration, and clean `before` /
`after` clips) lays the part in its own slot. `"under"` overlays the part
beneath the *following* sequential part (it does not consume its own slot),
attenuated by `duck_db` — a ducked underlay (a clip talked over, or later a
music bed).

`spotlight` marks the part the music bed drops out for (fade-to-spotlight):
the bed is silent over this part’s span and resumes after it. Inert without
a bed.

### *class* braidio.Voice(id, name, gender, accent='', note='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A pooled narration voice.

### *class* braidio.WeaveConfig(voices=('JBFqnCBsd6RMkjVDRZzb', ), pool_label='single', voice_seed=7, avoid_immediate_repeat=True, model_id='eleven_multilingual_v2', voice_settings=<factory>, segmentation_unit='beat', min_turn=2, max_turn=4, speed_base=1.0, speed_jitter=0.04, crossfade_s=0.12, gap_turn_s=0.0, overlap_turn_s=0.0, clip_pre_roll_s=0.4, clip_post_roll_s=0.3, clip_fade_in_s=0.5, clip_fade_out_s=0.8, clip_min_len_s=2.2, clip_edge_overlap_s=0.5, duck_db=-15.0, target_lufs=-16.0, true_peak_dbtp=-1.0, sample_rate=44100)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

All editing choices for a narration+segment weave. Frozen + serializable.

#### *property* paces_narration *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether a render cuts each narration beat into separately-spoken turns.

`False` (the default, `segmentation_unit="beat"`) is one TTS call per
beat — no intra-beat gaps, no per-turn speed, so `gap_turn_s` and
`speed_jitter` do nothing on the [`braidio.render.render_production()`](_autosummary/braidio.render.html.md#braidio.render.render_production)
path. Opt in by setting `segmentation_unit` to a smaller unit.

```pycon
>>> WeaveConfig().paces_narration
False
>>> WeaveConfig(segmentation_unit="sentence").paces_narration
True
```

#### to_dict()

Stable serialization for a `render-config` provenance node (#26).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

#### with_(\*\*changes)

Return a copy with fields overridden (e.g. `cfg.with_(min_turn=1)`).

* **Return type:**
  [`WeaveConfig`](_autosummary/braidio.weave_config.html.md#braidio.weave_config.WeaveConfig)

### *class* braidio.WeaveKind(\*values)

Bases: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

A braidio production kind. `COMMENTARY_WEAVE` = narration woven with
extracted media segments (audio now, video later).

### braidio.assign_voices(n, pool, , seed=0, avoid_repeats=True)

Assign a voice to each of `n` turns — random, seeded, no immediate
repeats (so it isn’t a rigid round-robin).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Voice`](_autosummary/braidio.multivoice.html.md#braidio.multivoice.Voice)]

### braidio.audio_tag_rate(text, , per=100)

Inline audio tags per `per` words — the expressiveness dial.

Only meaningful on a delivery whose model renders tags at all
(`braidio.delivery.Delivery.supports_audio_tags`); on
`eleven_multilingual_v2` the tags are inert text and this number is a lie.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### braidio.audio_tags(text)

Every inline `[audio tag]` in `text`, in order.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### braidio.audit_expressiveness(text)

Human-readable complaints about a script’s written-in performance.

Returns an empty list when the script sits in the target band. This is the
gate [`audit_platitudes()`](_autosummary/braidio.html.md#braidio.audit_platitudes) is not: platitudes catch recycled *phrases*,
this catches a script that will be read flatly however good the words are.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### braidio.audit_platitudes(text)

Return every [`Finding`](_autosummary/braidio.html.md#braidio.Finding) in `text`, in document order.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/braidio.style.html.md#braidio.style.Finding)]

### braidio.bed_for_intensity(asset_path, intensity, \*\*overrides)

Build a [`MusicBed`](_autosummary/braidio.html.md#braidio.MusicBed) at the gain for a Format `music_bed` intensity.

Returns `None` for `"none"` (or an unknown intensity), so callers can do
`bed = bed_for_intensity(asset, fmt.music_bed)` and skip when falsy.

* **Return type:**
  [`MusicBed`](_autosummary/braidio.music.html.md#braidio.music.MusicBed) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### braidio.billable_chars(text)

Characters ElevenLabs bills for `text` (the whole submitted string).

ElevenLabs charges for everything sent — including `eleven_v3` audio tags
like `[excited]` — so this is just `len(text)`. A named function keeps the
billing definition in one place if it ever needs to change.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> billable_chars("hello")
5
>>> billable_chars(None)
0
```

### braidio.build_timeline(, kinds, durations, placements=None, labels=None, source_spans=None, clip_edge_overlap_s=0.5, narration_crossfade_s=0.12, title='', settings=None)

Assemble a [`TimelineBreakdown`](_autosummary/braidio.html.md#braidio.TimelineBreakdown) from per-beat render data (pure).

`kinds` are the aggregation labels (any string; `"clip"` is the only one
the layout treats specially — as an overlapping segment). `placements` is
the per-beat `"sequential"`/`"under"` used by the weave; offsets are
computed with the same [`layout_placed()`](_autosummary/braidio.weave.html.md#braidio.weave.layout_placed) the renderer uses.

`settings` is the optional render record ([`render_settings()`](_autosummary/braidio.html.md#braidio.render_settings)) — what
produced these durations. It stays optional so building a timeline by hand
(tests, a hand-cut episode, the captions/video doctests) needs nothing
extra; a real render always passes one.

* **Return type:**
  [`TimelineBreakdown`](_autosummary/braidio.timeline.html.md#braidio.timeline.TimelineBreakdown)

### braidio.captions_for(script, timeline, , max_chars=0)

The SRT document for `script` as laid out by `timeline`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### braidio.clean_ocr(text, , collapse_whitespace=True)

Normalize OCR/PDF-extracted text for clean narration.

Expands ligatures, removes soft-hyphens (used at scan line-breaks), turns a
doubled hyphen `--` into an em-dash (so TTS phrases it as a pause), and
(by default) collapses runs of whitespace to single spaces.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### braidio.clip_plays_under(profile, rights, publishable=frozenset({'public-domain'}))

Whether a segment with `rights` plays as audio under `profile`.

The whole clip-routing rule, in one place: `PERSONAL` plays everything,
`PUBLISHED` plays only publishable rights. [`plan_production()`](_autosummary/braidio.html.md#braidio.plan_production) asks it
when it filters a script, and the graph’s episode transform re-asks it at
weave time to verify that the members it inherited still match the profile
the production now declares — one rule, asked twice, never copied.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### braidio.compose_narration(segments, config, , out_path, api_key=None, work_dir='data/tts/compose')

Render `segments` under `config` → `out_path`.

Single-voice and multi-voice go through the same turn-based path (a single
voice is just a one-voice pool). Returns the `(voice, turn_text)`
assignment for reporting / provenance.

`api_key` is an optional per-request ElevenLabs key threaded to
[`braidio.multivoice.render_multivoice()`](_autosummary/braidio.multivoice.html.md#braidio.multivoice.render_multivoice) (and thence every synthesized
turn); `None` (default) keeps the `$ELEVENLABS_API_KEY` fallback.

Note `config.segmentation_unit` is **not** read here: this function is
handed `segments` already cut, so the unit was chosen upstream (usually
[`braidio.script.narration_segments()`](_autosummary/braidio.script.html.md#braidio.script.narration_segments)). It is the
[`braidio.render.render_production()`](_autosummary/braidio.render.html.md#braidio.render.render_production) path that cuts a beat itself, and
there the field is the switch that turns pacing on — see
[`braidio.pacing`](_autosummary/braidio.pacing.html.md#module-braidio.pacing).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Voice`](_autosummary/braidio.multivoice.html.md#braidio.multivoice.Voice), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### braidio.config_path()

Where the user’s persisted defaults live (the file need not exist).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### braidio.content_violations(plan, forbidden, , min_words=5)

Rights violations in a *published* plan (empty list = clean).

Fails if any planned beat plays non-publishable segment audio, or any
narration beat contains forbidden verbatim text.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### braidio.cues_for(script, timeline, , max_chars=0)

Subtitle cues for `script` as laid out by `timeline`.

Beats are matched by index, so a timeline built from the same script lines up
even when some beats were dropped by a rights profile (a dropped beat simply
has no span and contributes no cue).

Cues never overlap. That matters because the weave *crossfades* consecutive
beats, so a beat’s `start` sits slightly before the previous beat’s end;
left alone that produces subtitles that fight each other. Each cue is clamped
to begin where the previous one finished.

* **Parameters:**
  * **script** – the [`Script`](_autosummary/braidio.script.html.md#braidio.script.Script) that was rendered.
  * **timeline** – the [`TimelineBreakdown`](_autosummary/braidio.timeline.html.md#braidio.timeline.TimelineBreakdown) from
    `render_production(..., return_timeline=True)`.
  * **max_chars** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – if > 0, split sentences longer than this at whitespace, so no
    single cue overflows a player’s two lines. 0 leaves sentences whole.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Cue`](_autosummary/braidio.captions.html.md#braidio.captions.Cue)]
* **Returns:**
  Cues in playback order.

### braidio.cut_quote(audio_path, lines, quote, out_path, , pad_pre_s=0.15, pad_post_s=0.35, fade_s=0.04, min_score=0.5, song_end_s=None)

Resolve `quote` → segment and cut it from `audio_path` (pad + fades).

Convenience combining [`find_segment()`](_autosummary/braidio.html.md#braidio.find_segment) + an ffmpeg cut. Returns the
resolved [`Segment`](_autosummary/braidio.html.md#braidio.Segment) (raises `LookupError` if unmatched). New code
should prefer a [`SegmentSource`](_autosummary/braidio.html.md#braidio.SegmentSource) + [`braidio.weave.extract_padded()`](_autosummary/braidio.weave.html.md#braidio.weave.extract_padded).

* **Return type:**
  [`Segment`](_autosummary/braidio.sources.html.md#braidio.sources.Segment)

### braidio.default_delivery(explicit=None)

Resolve the delivery to render with.

* **Parameters:**
  **explicit** ([`Delivery`](_autosummary/braidio.delivery.html.md#braidio.delivery.Delivery) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – a [`Delivery`](_autosummary/braidio.delivery.html.md#braidio.delivery.Delivery), or the name of one
  (see `braidio.delivery.DELIVERIES`). Wins over everything.
* **Return type:**
  [`Delivery`](_autosummary/braidio.delivery.html.md#braidio.delivery.Delivery)

### Examples

```pycon
>>> default_delivery(V3_PRESENTER) is V3_PRESENTER
True
>>> default_delivery("v3-narrator").name
'v3-narrator'
```

### braidio.default_voice_id(explicit=None)

Resolve the narration voice id, or `None` to let the caller decide.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### braidio.default_voice_settings()

The resolved delivery’s voice settings, as a fresh dict.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### braidio.describe_asset_application(fmt, script, , bed_asset=None, sting_asset=None)

Which of the supplied `bed_asset` / `sting_asset` this format will
actually render, and why not otherwise (braidio#43).

Pure and pre-render — safe to call before paying for anything. A key stays
`None` when its asset wasn’t supplied. `bed_asset`’s fate is fixed by
`fmt.music_bed` alone: `"none"` never renders a bed, and
[`render_format()`](_autosummary/braidio.html.md#braidio.render_format) refuses `bed_asset` there rather than spend on one
that would be dropped, so `bed_applied` is `False` only via that refusal
path, never in a result you got back from a successful render.
`sting_asset`’s fate additionally depends on `script`: a scene break’s
marker can override the format’s default, so a sting can still legitimately
go unused in one script and play in another under the same format — that
case is reported here, not refused.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`bool`](https://docs.python.org/3/builtins/functions.html#bool) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]

### braidio.describe_defaults()

What is in force, and where each part came from.

Worth printing when a render does not sound the way somebody expected: the
commonest cause is a config file they forgot they wrote.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### braidio.estimate_cost(source, , model_id=None)

Estimate ElevenLabs spend for a [`braidio.Script`](_autosummary/braidio.html.md#braidio.Script) or a raw string.

Free/local work (segment extraction, weaving) contributes nothing. The returned
[`CostRollup`](_autosummary/braidio.html.md#braidio.CostRollup) reports exact characters and an honest dollar sum (a lower
bound when some text is unpriced; see [`usd_per_1k_chars()`](_autosummary/braidio.html.md#braidio.usd_per_1k_chars)).

* **Return type:**
  [`CostRollup`](_autosummary/braidio.cost.html.md#braidio.cost.CostRollup)

```pycon
>>> from braidio import Script, Narration, SegmentBeat
>>> s = Script(title="x", id_slug="01", beats=[
...     Narration(text="a" * 500), SegmentBeat(reference="clip:1")])
>>> estimate_cost(s).characters  # only the narration counts; the clip is free
500
```

### braidio.extract_padded(asset_path, start_s, end_s, out_path, , pre_roll_s=0.4, post_roll_s=0.3, fade_in_s=0.5, fade_out_s=0.8, min_len_s=2.2)

Extract `[start_s-pre_roll, end_s+post_roll]` with in/out fades.

The target words sit in the middle; the padded, faded head/tail are the
parts that overlap (tuck under) neighbouring narration in the weave.

Two guards keep **short** clips from sounding like they just swell in and
out (no steady body):

- the tail is extended so the clip is at least `min_len_s` long, giving the
  fade somewhere to breathe;
- the fades are **adaptive** — capped to a fraction of the clip so they never
  swallow it (a 1.5 s clip gets ~0.3 s fades, not 0.5 s + 0.8 s).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### braidio.find_segment(lines, quote, , max_span=12, min_score=0.5, song_end_s=None)

Best contiguous run of timed lines matching `quote`, or `None`.

Scores every run `lines[i..j]` (up to `max_span` lines) by token F1
against the reference’s tokens and returns the highest-scoring run clearing
`min_score`. Handles single-line, sub-line, and multi-line references.

* **Return type:**
  [`Segment`](_autosummary/braidio.sources.html.md#braidio.sources.Segment) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### braidio.find_verbatim_text(text, forbidden, , min_words=5)

Forbidden lines that appear (near-)verbatim in `text`.

A line counts as leaked if `text` shares a run of `min_words` consecutive
words with it (case-insensitive, word-level). Single words and short common
phrases don’t trip it — only substantial verbatim quoting.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### braidio.format_srt(cues)

Render `cues` as an SRT document.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### braidio.group_turns(segments, , min_turn=1, max_turn=1, seed=0)

Group consecutive segments into *turns* of `min_turn..max_turn` segments.

A turn is what one voice speaks before the next takes over. Bigger turns =
each speaker talks longer (fewer switches). Each turn’s segments are joined
into one utterance so prosody is continuous within a speaker. Turn sizes are
seeded-random within the range.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### braidio.layout_starts(kinds, durs, , clip_edge_overlap_s, narration_crossfade_s)

Start offset (s) of each part (all sequential). Thin wrapper over
`layout_placed()` — kept for callers that don’t use placement.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

### braidio.load_timing(path)

Load a `{lines: [{index,start_s,end_s,text}]}` JSON into 

```
`
```

TimedLine\`s.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`TimedLine`](_autosummary/braidio.sources.html.md#braidio.sources.TimedLine)]

### braidio.narrate(text, out_path, , api_key=None, voice_id=None, model_id='eleven_v3', voice_settings=None, output_format='mp3_44100_128', refresh=False, return_cache_status=False)

Synthesize `text` to `out_path` (mp3). Returns the path.

Caching is handled by `mixing.text_to_speech` (keyed on text+voice+model);
pass `refresh=True` to regenerate.

`api_key` is an optional per-request ElevenLabs key: when given it wins
over the environment; when `None` (default) resolution falls back to
`$ELEVENLABS_API_KEY` (unchanged behavior). This is what lets a caller
thread a per-user BYO key without touching the process environment.

`return_cache_status`: when `True`, return `(path, was_cached)` where
`was_cached` is `True` iff mixing served the audio from its on-disk cache
(no ElevenLabs call = $0 real spend). Lets the caller attribute real cost
(braidio#8). Default `False` keeps the `Path` return.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`bool`](https://docs.python.org/3/builtins/functions.html#bool)]

### braidio.narration_segments(script)

All narration (default text) of a script, as sentence-level segments.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### braidio.plan_production(script, profile, , publishable_clip_rights=frozenset({'public-domain'}))

Filter `script` into the beats renderable under `profile`.

* **Return type:**
  [`RenderPlan`](_autosummary/braidio.rights.html.md#braidio.rights.RenderPlan)

### braidio.platitude_rate(text, , per=1000)

Flagged hits per `per` words (default 1000). 0.0 for empty text.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### braidio.render_format(fmt, script, , source, out_path=None, profile=Profile.PERSONAL, bed_asset=None, sting_asset=None, \*\*overrides)

Render `script` under `fmt`’s defaults; `overrides` win over them.

Wires the format’s `cast` / `narration_voice` / `narration_delivery` /
`weave` / `structure` into [`braidio.render.render_production()`](_autosummary/braidio.render.html.md#braidio.render.render_production). Any
beat may still override voice/settings per-beat (e.g. a graver book-narrator
inside an otherwise lively presenter piece — pass `V2_NARRATOR.voice_settings`
on that `Narration` beat).

`bed_asset` (a path to an app-supplied instrumental) adds a music bed at the
gain implied by `fmt.music_bed`; pass `music_bed=MusicBed(...)` in
`overrides` for full control. A format whose `music_bed` is `"none"`
(e.g. `SONG_EXPLODER`) never renders a bed at all, so `bed_asset` there
raises `ValueError` *before* any rendering — the caller would otherwise pay
for a bed the format silently drops (braidio#43) — unless `overrides`
itself supplies `music_bed=`, which always wins and makes the refusal moot.
`sting_asset` (a path to an app-supplied short marker) is what a
`SceneBreak` plays under the format’s `structure`; pass
`structure=MusicStructure(...)` in `overrides` for full control. Unlike
the bed, a format’s `scene_marker` is only the *default* — an individual
`SceneBreak.marker` override can still play the sting even under a
`"none"` default — so a sting that ends up unused is not refused, only
reported (see [`describe_asset_application()`](_autosummary/braidio.html.md#braidio.describe_asset_application)).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### braidio.render_multivoice(segments, pool, , out_path, api_key=None, work_dir='data/tts/multivoice', seed=7, min_turn=2, max_turn=4, avoid_immediate_repeat=True, model_id='eleven_multilingual_v2', base_settings=None, speed_base=1.0, speed_jitter=0.04, crossfade_s=0.1, gap_s=0.0, target_lufs=-16.0)

Render `segments` cycling `pool` → `out_path`.

Segments are first grouped into *turns* of `min_turn..max_turn` segments
(bigger = each voice talks longer). One voice per turn, no immediate repeat,
with a jittered speed even within a speaker. `gap_s` inserts silence
between turns (0 = none). Returns `[(voice, turn_text), …]` for reporting.

`api_key` is an optional per-request ElevenLabs key threaded to every
[`braidio.tts.narrate()`](_autosummary/braidio.tts.html.md#braidio.tts.narrate) call; `None` (default) keeps the
`$ELEVENLABS_API_KEY` fallback.

#### NOTE
overlapping/interrupting speakers and clip ducking are separate,
upcoming parameters (tracked as issues) — this renders turns sequentially.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Voice`](_autosummary/braidio.multivoice.html.md#braidio.multivoice.Voice), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### braidio.render_production(script, , source, api_key=None, config=None, profile=Profile.PERSONAL, rights=None, delivery=None, cast=ConversationCast(roles={'A': 'cgSgspJ2msm6clMCkdW9', 'B': 'iP95p4xoKVk53GoZ742B'}, model_id='eleven_v3', settings={'stability': 0.45}), out_path=None, voice_id=None, crossfade_s=0.12, normalize=True, music_bed=None, structure=None, end_fade_s=0.35, end_silence_s=0.7, return_timeline=False, tts_dir='data/tts', clips_dir='data/clips', episodes_dir='data/episodes')

Render `script` under `profile` → a single audio file. Returns the path.

With `return_timeline=True` returns `(path, TimelineBreakdown)` instead —
the render records what it spent time on (per-beat kind, source interval,
duration, and offset) rather than leaving it to be reconstructed afterward,
plus, in `TimelineBreakdown.settings`, the settings that produced those
timings (see [`braidio.timeline.render_settings()`](_autosummary/braidio.timeline.html.md#braidio.timeline.render_settings)). Recording them is
what keeps a render reproducible: the pacing knobs below mean one script has
many possible cuts, so a persisted breakdown has to say which one it is.

Segment beats are resolved through `source` (a [`SegmentSource`](_autosummary/braidio.html.md#braidio.SegmentSource)).
When `config` has `clip_edge_overlap_s > 0`, a clip is `placement="under"`,
or a `music_bed` is given, the parts are woven on a timeline; otherwise they
are concatenated. `rights` (if given) sets which segment rights are
publishable. `music_bed` lays an instrumental underscore under the whole
production (see [`braidio.music.MusicBed`](_autosummary/braidio.music.html.md#braidio.music.MusicBed)).

`structure` (a [`braidio.structure.MusicStructure`](_autosummary/braidio.structure.html.md#braidio.structure.MusicStructure)) is how the
production marks its structure with music: a scene-break beat plays its
`sting` (or a pause when there is none / the break is marked `"none"`),
and a spotlit segment beat drops the bed out for its duration. `None`
uses the inert defaults — no sting asset, no clip spotlit by default — so a
script without scene breaks or spotlight flags renders exactly as before.

**Pacing inside a narration beat** is a `config` choice. By default
(`WeaveConfig.segmentation_unit == "beat"`) a beat is one TTS call — one
prosodic arc, no silence anywhere in it. Set a smaller
`segmentation_unit` and the beat is cut into turns of
`min_turn..max_turn` units, each synthesized separately with a jittered
`speed_base ± speed_jitter` (where the model has a speed knob) and
followed by `gap_turn_s` scaled to how strongly the boundary closes —
see [`braidio.pacing`](_autosummary/braidio.pacing.html.md#module-braidio.pacing). Those four knobs do **nothing** here at the
default unit, which is the historical behavior kept byte-identical.

`api_key` is an optional per-request ElevenLabs key threaded to every
synthesized beat — both narration ([`braidio.tts.narrate()`](_autosummary/braidio.tts.html.md#braidio.tts.narrate)) and dialogue
([`braidio.conversation.render_dialogue()`](_autosummary/braidio.conversation.html.md#braidio.conversation.render_dialogue)). When `None` (default) each
synthesizer falls back to `$ELEVENLABS_API_KEY` (unchanged behavior); an
explicit key lets a caller (e.g. a per-user BYO-key request) override the
environment without mutating it. Segment beats never call ElevenLabs, so the
key does not touch them.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`object`](https://docs.python.org/3/builtins/functions.html#object)]

### braidio.render_settings(, config, crossfade_s, clip_edge_overlap_s, target_lufs, duck_db, delivery=None, profile=None, normalize=True)

The record of *how* a production was rendered, as plain JSON types.

Why it exists: since braidio#63 the pacing knobs are real, so two renders of
one script can differ by minutes — and until this, nothing on disk said which
settings produced which file. This is the cheap, no-graph answer: the
renderer hands it to the [`TimelineBreakdown`](_autosummary/braidio.html.md#braidio.TimelineBreakdown) it already returns, so a
consumer that persists the breakdown persists the settings with it.

The whole resolved [`WeaveConfig`](_autosummary/braidio.weave_config.html.md#braidio.weave_config.WeaveConfig) goes in under
`"weave"` rather than a hand-picked subset — adding a knob then keeps the
record correct for free, where a curated list would quietly drift.
`"resolved"` carries the values the render *actually used*, which are not
always the config’s: a caller may pass no config at all (`"weave"` is then
`None`, and the pacing knobs were never consulted — each beat was one TTS
call), the concat path resolves its crossfade from either the config or the
`crossfade_s` argument, and `normalize` is a render argument rather than
a weave choice.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> from braidio.delivery import V2_TUNED
>>> from braidio.weave_config import WeaveConfig
>>> rec = render_settings(
...     config=WeaveConfig(segmentation_unit="sentence", gap_turn_s=0.28),
...     crossfade_s=0.12, clip_edge_overlap_s=0.5,
...     target_lufs=-16.0, duck_db=-15.0,
...     delivery=V2_TUNED, profile="published",
... )
>>> rec["weave"]["gap_turn_s"], rec["resolved"]["paces_narration"]
(0.28, True)
>>> rec["delivery"]["model_id"], rec["profile"]
('eleven_multilingual_v2', 'published')
```

### braidio.resolve_voice_id(voice_id=None)

Voice id from arg → `VOICE_ENV_VAR` env → default.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### braidio.rights_are_publishable(rights, publishable=frozenset({'public-domain'}))

The publishable test at the level of a bare `rights` string.

The graph records rights on nodes, not on `SegmentBeat`s, so the check
has to be askable without a beat in hand — but it must stay the *same*
check. [`segment_is_publishable()`](_autosummary/braidio.html.md#braidio.segment_is_publishable) is this function with a beat
unwrapped, never a parallel rule.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### braidio.segment_is_publishable(beat, publishable=frozenset({'public-domain'}))

Whether `beat`’s audio may play in the published cut.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### braidio.skills_dir()

Path to the agent skills that ship with braidio.

They install with the package, so an agent host can be pointed at them
without cloning the repo:

```default
ln -s "$(python -c 'import braidio; print(braidio.skills_dir())')/braidio" \
      ~/.claude/skills/braidio
```

### braidio.split_segments(text)

Split narration into sentence-level segments (markup removed).

Splits on sentence-final `.?!` (not the `…` used for in-thought pacing),
so connected clauses stay with one speaker.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### braidio.strip_speaker_labels(text)

Remove a leading speaker-label prefix (e.g. `"Chris: "`) if present.

Only strips a single leading `Word:` / `Host:` style label so it isn’t
read aloud; leaves colons that are part of the sentence untouched.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### braidio.tts_cost_usd(text, , model_id=None)

Estimated USD to synthesize `text`; `0.0` for empty, `None` if unpriced.

Empty text is genuinely free (`0.0`) regardless of the rate; non-empty text
is `None` only when [`usd_per_1k_chars()`](_autosummary/braidio.html.md#braidio.usd_per_1k_chars) is unpriced.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> tts_cost_usd("")
0.0
```

### braidio.usd_per_1k_chars(model_id=None)

Resolved USD-per-1000-characters rate (most specific source first).

Resolution: a confirmed per-model rate in `MODEL_USD_PER_1K_CHARS` →
the env override `RATE_ENV_VAR` → `DEFAULT_USD_PER_1K_CHARS`.
Returns `None` (*unpriced*, not free) when the env override is explicitly
disabled (one of `_UNPRICED_SENTINELS`) or is not a finite, non-negative
number — a bad rate must never silently become a dishonest negative/NaN spend.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

### braidio.user_config()

The user’s persisted defaults, or `{}`.

A missing file is normal. A malformed one warns *once* per path and is then
treated as absent.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### braidio.weave_timeline(items, out_path, , clip_edge_overlap_s=0.5, narration_crossfade_s=0.12, target_lufs=-16.0, true_peak=-1.0, sample_rate=44100, bed=None)

Place items on a timeline and mix. Clips overlap neighbours by
`clip_edge_overlap_s` (their faded edges tuck under narration); narration
parts butt-join with a small crossfade. Returns `out_path`.

`bed` (a [`MusicBed`](_autosummary/braidio.music.html.md#braidio.music.MusicBed)) lays an instrumental underscore
under the whole production: it’s rendered to cover the timeline, attenuated,
and mixed in posted by `bed.lead_in_s`. Items marked `spotlight` open a
gap in it (the bed is rendered as the regions around them — see
[`braidio.music.bed_regions()`](_autosummary/braidio.music.html.md#braidio.music.bed_regions)); with none marked the bed is the single
whole-span file. Falls back to a plain concat feel when
`clip_edge_overlap_s == 0` and there’s nothing to overlay.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### Modules

| [`captions`](_autosummary/braidio.captions.html.md#module-braidio.captions)         | Subtitles for a rendered production, built from the script it was rendered from.                           |
|-------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|
| [`compose`](_autosummary/braidio.compose.html.md#module-braidio.compose)           | Config-driven narration composition (#20) — the reusable entrypoint.                                       |
| [`conversation`](_autosummary/braidio.conversation.html.md#module-braidio.conversation) | Conversational register: render an exchange as people *talking to each other*.                             |
| [`cost`](_autosummary/braidio.cost.html.md#module-braidio.cost)                 | Cost model for braidio's paid operations (ElevenLabs TTS).                                                 |
| [`defaults`](_autosummary/braidio.defaults.html.md#module-braidio.defaults)         | User-overridable, persisted defaults for how braidio renders a voice.                                      |
| [`delivery`](_autosummary/braidio.delivery.html.md#module-braidio.delivery)         | Narration *delivery* presets — model + voice settings (issue #10, expressiveness).                         |
| [`formats`](_autosummary/braidio.formats.html.md#module-braidio.formats)           | Ready-made **format templates** — high-quality presets under standard names.                               |
| [`kinds`](_autosummary/braidio.kinds.html.md#module-braidio.kinds)               | Production kinds braidio defines.                                                                          |
| [`multivoice`](_autosummary/braidio.multivoice.html.md#module-braidio.multivoice)     | Multi-voice narration: cycle a pool of voices across segments (issue #10).                                 |
| [`music`](_autosummary/braidio.music.html.md#module-braidio.music)               | Music bed — an instrumental underscore laid under the whole production, ducked.                            |
| [`pacing`](_autosummary/braidio.pacing.html.md#module-braidio.pacing)             | Intra-beat narration pacing — how one narration beat becomes spoken *turns*.                               |
| [`relevance`](_autosummary/braidio.relevance.html.md#module-braidio.relevance)       | How well a still relates to the words spoken over it — the scorer seam.                                    |
| [`render`](_autosummary/braidio.render.html.md#module-braidio.render)             | Render a [`Script`](_autosummary/braidio.script.html.md#braidio.script.Script) into an audio file. |
| [`rights`](_autosummary/braidio.rights.html.md#module-braidio.rights)             | Render profiles: enforce personal-vs-published rights as data.                                             |
| [`script`](_autosummary/braidio.script.html.md#module-braidio.script)             | Composition model — the ordered beats a render walks.                                                      |
| [`sources`](_autosummary/braidio.sources.html.md#module-braidio.sources)           | Segment sources: resolve a *reference* to a cuttable `[start, end]` window.                                |
| [`structure`](_autosummary/braidio.structure.html.md#module-braidio.structure)       | Structural music — stings at scene breaks, fade-to-spotlight on exhibits.                                  |
| [`style`](_autosummary/braidio.style.html.md#module-braidio.style)               | Style audit — flag the recycled rhetorical tics in commentary text.                                        |
| [`textprep`](_autosummary/braidio.textprep.html.md#module-braidio.textprep)         | Text preparation for scripts — clean OCR'd source, tidy authored lines.                                    |
| [`timeline`](_autosummary/braidio.timeline.html.md#module-braidio.timeline)         | Timeline breakdown — what a production spends its time on, and in what order.                              |
| [`tts`](_autosummary/braidio.tts.html.md#module-braidio.tts)                   | ElevenLabs narration synthesis.                                                                            |
| [`video`](_autosummary/braidio.video.html.md#module-braidio.video)               | Turn a rendered production into a Ken Burns film over still images.                                        |
| [`weave`](_autosummary/braidio.weave.html.md#module-braidio.weave)               | Weave narration + audio clips on a timeline (#21) — the reusable mix engine.                               |
| [`weave_config`](_autosummary/braidio.weave_config.html.md#module-braidio.weave_config) | WeaveConfig — every editing choice for weaving narration + segments (#20).                                 |


# _autosummary/braidio.kinds.html.md

# braidio.kinds

Production kinds braidio defines. Pure (no optional deps).

### Classes

| [`WeaveKind`](_autosummary/braidio.kinds.html.md#braidio.kinds.WeaveKind)(\*values)   | A braidio production kind.   |
|------------------------------------------------------------------------|------------------------------|

### *class* braidio.kinds.WeaveKind(\*values)

Bases: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

A braidio production kind. `COMMENTARY_WEAVE` = narration woven with
extracted media segments (audio now, video later).


# _autosummary/braidio.multivoice.html.md

# braidio.multivoice

Multi-voice narration: cycle a pool of voices across segments (issue #10).

When one voice reads flat, variety across voices is a stronger lever than any
single-voice setting. This renders narration by splitting it into segments
(sentences) and assigning each a voice drawn from a pool — randomly (seeded, so
it’s reproducible) and avoiding immediate repeats so it doesn’t feel like a
rigid round-robin. Speed is jittered slightly per segment, even within one
speaker, to break regularity.

Two curated pools of **premade** ElevenLabs voices (all production quality,
deliberately less familiar than “George”, 2M/2F in `POOL_4`, wider in
`POOL_MANY`). For a truly large “many people / interviews” pool, the
shared Voice Library (thousands) can be tapped, but quality varies there, so
those need curation — the premade pools below are the safe default.

### Functions

| [`assign_voices`](_autosummary/braidio.multivoice.html.md#braidio.multivoice.assign_voices)(n, pool, \*[, seed, avoid_repeats])   | Assign a voice to each of `n` turns — random, seeded, no immediate repeats (so it isn't a rigid round-robin).   |
|------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
| [`group_turns`](_autosummary/braidio.multivoice.html.md#braidio.multivoice.group_turns)(segments, \*[, min_turn, ...])          | Group consecutive segments into *turns* of `min_turn..max_turn` segments.                                       |
| [`render_multivoice`](_autosummary/braidio.multivoice.html.md#braidio.multivoice.render_multivoice)(segments, pool, \*, out_path)     | Render `segments` cycling `pool` → `out_path`.                                                                  |
| [`split_segments`](_autosummary/braidio.multivoice.html.md#braidio.multivoice.split_segments)(text)                                | Split narration into sentence-level segments (markup removed).                                                  |
| `strip_markup`(text)                                                                                 |                                                                                                                 |

### Classes

| [`Voice`](_autosummary/braidio.multivoice.html.md#braidio.multivoice.Voice)(id, name, gender[, accent, note])   | A pooled narration voice.   |
|--------------------------------------------------------------------------------------------|-----------------------------|

### *class* braidio.multivoice.Voice(id, name, gender, accent='', note='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A pooled narration voice.

### braidio.multivoice.assign_voices(n, pool, , seed=0, avoid_repeats=True)

Assign a voice to each of `n` turns — random, seeded, no immediate
repeats (so it isn’t a rigid round-robin).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Voice`](_autosummary/braidio.multivoice.html.md#braidio.multivoice.Voice)]

### braidio.multivoice.group_turns(segments, , min_turn=1, max_turn=1, seed=0)

Group consecutive segments into *turns* of `min_turn..max_turn` segments.

A turn is what one voice speaks before the next takes over. Bigger turns =
each speaker talks longer (fewer switches). Each turn’s segments are joined
into one utterance so prosody is continuous within a speaker. Turn sizes are
seeded-random within the range.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### braidio.multivoice.render_multivoice(segments, pool, , out_path, api_key=None, work_dir='data/tts/multivoice', seed=7, min_turn=2, max_turn=4, avoid_immediate_repeat=True, model_id='eleven_multilingual_v2', base_settings=None, speed_base=1.0, speed_jitter=0.04, crossfade_s=0.1, gap_s=0.0, target_lufs=-16.0)

Render `segments` cycling `pool` → `out_path`.

Segments are first grouped into *turns* of `min_turn..max_turn` segments
(bigger = each voice talks longer). One voice per turn, no immediate repeat,
with a jittered speed even within a speaker. `gap_s` inserts silence
between turns (0 = none). Returns `[(voice, turn_text), …]` for reporting.

`api_key` is an optional per-request ElevenLabs key threaded to every
[`braidio.tts.narrate()`](_autosummary/braidio.tts.html.md#braidio.tts.narrate) call; `None` (default) keeps the
`$ELEVENLABS_API_KEY` fallback.

#### NOTE
overlapping/interrupting speakers and clip ducking are separate,
upcoming parameters (tracked as issues) — this renders turns sequentially.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Voice`](_autosummary/braidio.multivoice.html.md#braidio.multivoice.Voice), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### braidio.multivoice.split_segments(text)

Split narration into sentence-level segments (markup removed).

Splits on sentence-final `.?!` (not the `…` used for in-thought pacing),
so connected clauses stay with one speaker.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]


# _autosummary/braidio.music.html.md

# braidio.music

Music bed — an instrumental underscore laid under the whole production, ducked.

The bed is the spanning generalization of a per-clip `under` placement: instead
of one clip beneath one talk beat, an app-supplied **instrumental** asset runs
beneath the entire timeline at reduced gain, entering after speech onset and
fading at the ends (the conventions from
`docs/research/commentary-formats-and-styles.md`: bed 10–15 dB under voice,
~1.5s fade-in, *posted* after the start; beds must be instrumental).

braidio ships **no music** — the caller supplies the asset (a licensed/owned
instrumental). A [`Format`](_autosummary/braidio.formats.html.md#braidio.formats.Format)’s `music_bed` *intensity*
(continuous / light / sparse / none) picks a sensible gain via
`BED_GAIN_BY_INTENSITY`; `render_format()` builds the bed for you when
given a `bed_asset`.

**Fade-to-spotlight** ([`braidio.structure`](_autosummary/braidio.structure.html.md#module-braidio.structure)) is expressed here as bed
*regions*: [`bed_regions()`](_autosummary/braidio.music.html.md#braidio.music.bed_regions) cuts the bed’s span into the stretches around the
spotlit windows, each with its own fades, and [`prepare_bed_regions()`](_autosummary/braidio.music.html.md#braidio.music.prepare_bed_regions) renders
them so the weave mixes each in at its own start. With no spotlit window the bed
is one region rendered by [`prepare_bed()`](_autosummary/braidio.music.html.md#braidio.music.prepare_bed) — the unchanged single-file path.

### Functions

| [`bed_for_intensity`](_autosummary/braidio.music.html.md#braidio.music.bed_for_intensity)(asset_path, intensity, ...)   | Build a [`MusicBed`](_autosummary/braidio.music.html.md#braidio.music.MusicBed) at the gain for a Format `music_bed` intensity.   |
|--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|
| [`bed_regions`](_autosummary/braidio.music.html.md#braidio.music.bed_regions)(bed, total_s, spotlights)           | Cut the bed's span `[lead_in_s, total_s]` around the `spotlights`.                                                  |
| [`prepare_bed`](_autosummary/braidio.music.html.md#braidio.music.prepare_bed)(bed, target_s, out_path, \*[, ...]) | Render `bed` to a ready-to-mix underscore of length `target_s - lead_in_s`.                                         |
| [`prepare_bed_regions`](_autosummary/braidio.music.html.md#braidio.music.prepare_bed_regions)(bed, regions, out_dir, ...) | Render each [`BedRegion`](_autosummary/braidio.music.html.md#braidio.music.BedRegion) to `out_dir/<stem>-<k>.mp3`.                 |

### Classes

| [`BedRegion`](_autosummary/braidio.music.html.md#braidio.music.BedRegion)(start_s, end_s, fade_in_s, fade_out_s)   | One stretch of the timeline the bed plays over, with its own fades.       |
|-----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| [`MusicBed`](_autosummary/braidio.music.html.md#braidio.music.MusicBed)(asset_path[, gain_db, fade_in_s, ...])    | An instrumental underscore spanning the production, mixed under the talk. |

### *class* braidio.music.BedRegion(start_s, end_s, fade_in_s, fade_out_s)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One stretch of the timeline the bed plays over, with its own fades.

`start_s` / `end_s` are timeline offsets (the bed’s own `lead_in_s` is
already applied). The bed keeps its place in the asset across a gap: a
region seeks into the asset to where the music *would* have been.

### *class* braidio.music.MusicBed(asset_path, gain_db=-22.0, fade_in_s=1.5, fade_out_s=2.0, lead_in_s=2.0, start_s=0.0, loop=True, spotlight_fade_s=0.6)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

An instrumental underscore spanning the production, mixed under the talk.

`asset_path` is an app-supplied instrumental. `gain_db` sets how far under
the voice it sits; `lead_in_s` posts the bed *after* speech onset so its
entrance feels motivated; `start_s` is the in-point into the asset;
`loop` repeats the asset to cover a timeline longer than it.
`spotlight_fade_s` is how fast the bed drops out before a spotlit exhibit
(it resumes after the exhibit over `fade_in_s`).

### braidio.music.bed_for_intensity(asset_path, intensity, \*\*overrides)

Build a [`MusicBed`](_autosummary/braidio.music.html.md#braidio.music.MusicBed) at the gain for a Format `music_bed` intensity.

Returns `None` for `"none"` (or an unknown intensity), so callers can do
`bed = bed_for_intensity(asset, fmt.music_bed)` and skip when falsy.

* **Return type:**
  [`MusicBed`](_autosummary/braidio.music.html.md#braidio.music.MusicBed) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### braidio.music.bed_regions(bed, total_s, spotlights)

Cut the bed’s span `[lead_in_s, total_s]` around the `spotlights`.

Pure function. Each spotlight `(start, end)` (timeline seconds) opens a
gap: the bed fades *out* over `bed.spotlight_fade_s` to be silent by
`start`, and fades back *in* over `bed.fade_in_s` from `end`. The first
region keeps the bed’s own fade-in and the last its fade-out; a region that
would be empty (a spotlight at the very top, two spotlights back to back) is
dropped. With no spotlights the result is the single whole-span region.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`BedRegion`](_autosummary/braidio.music.html.md#braidio.music.BedRegion)]

### braidio.music.prepare_bed(bed, target_s, out_path, , sample_rate=44100)

Render `bed` to a ready-to-mix underscore of length `target_s - lead_in_s`.

Seeks to `bed.start_s` (looping if needed), trims to the covered length, and
bakes in fades + `gain_db` + stereo. The caller mixes the result delayed by
`bed.lead_in_s`. Returns `out_path`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### braidio.music.prepare_bed_regions(bed, regions, out_dir, , stem, sample_rate=44100)

Render each [`BedRegion`](_autosummary/braidio.music.html.md#braidio.music.BedRegion) to `out_dir/<stem>-<k>.mp3`.

Returns `[(path, start_s), …]` — the caller mixes each file in delayed by
its `start_s`. A region seeks into the asset to where the bed would have
been had it played through (so the music resumes in place after a
spotlight), wrapping around the asset’s length when the bed loops. A
non-looping bed that has already run out by a region’s start has nothing
to play there: that region is dropped (seeking past the end would yield an
empty file the mix can’t open).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`float`](https://docs.python.org/3/builtins/functions.html#float)]]


# _autosummary/braidio.pacing.html.md

# braidio.pacing

Intra-beat narration pacing — how one narration beat becomes spoken *turns*.

A narration beat used to be exactly one TTS call: one prosodic arc, one speed,
and no silence anywhere inside it. That is the mechanical read — a human varies
tempo within a paragraph and pauses \*proportionally to how strongly a boundary
closes\* (a paragraph break is not a comma).

This module is the pure, deterministic planner for that. It takes the beat’s
text and the pacing knobs off a [`WeaveConfig`](_autosummary/braidio.weave_config.html.md#braidio.weave_config.WeaveConfig) and
returns a list of [`NarrationTurn`](_autosummary/braidio.pacing.html.md#braidio.pacing.NarrationTurn) — what to synthesize, how fast, and how
much silence to leave after it. No audio, no API, no I/O: the renderer
([`braidio.render.render_production()`](_autosummary/braidio.render.html.md#braidio.render.render_production)) executes the plan.

Two research findings drive the design
(`misc/docs/research/notebooklm-and-conversational-pacing.md`):

- **Boundary-proportional pauses.** Punctuation is the engine’s pause
  instruction, and a pause is only human when the boundary that earns it is
  strong. [`BOUNDARIES`](_autosummary/braidio.pacing.html.md#braidio.pacing.BOUNDARIES) is that table.
- **Final lengthening.** The robotic signature is *pause without lengthening* —
  a clipped last word followed by dead air. So each boundary carries a
  `speed_scale` as well as a `gap_scale`, and they stay correlated.

The `unit="beat"` plan is empty by construction: that is the historical
one-call-per-beat behavior, and it is the default, so nothing paces unless a
`WeaveConfig` asks for it.

```pycon
>>> turns = plan_turns("One. Two. Three.", unit="sentence", min_turn=1, max_turn=1)
>>> [t.text for t in turns]
['One.', 'Two.', 'Three.']
>>> plan_turns("One. Two.", unit="beat")
[]
```

### Module Attributes

| [`SEGMENTATION_UNITS`](_autosummary/braidio.pacing.html.md#braidio.pacing.SEGMENTATION_UNITS)   | How a beat's text is cut into the units a turn is built from.                                                                               |
|-----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| [`SPEED_RANGE`](_autosummary/braidio.pacing.html.md#braidio.pacing.SPEED_RANGE)          | ElevenLabs clamps `voice_settings.speed` to this range in practice; a jittered speed outside it is silently useless, so the planner clamps. |
| [`BOUNDARIES`](_autosummary/braidio.pacing.html.md#braidio.pacing.BOUNDARIES)           | The boundary table.                                                                                                                         |

### Functions

| [`classify_boundary`](_autosummary/braidio.pacing.html.md#braidio.pacing.classify_boundary)(text)                     | Name the boundary implied by `text`'s trailing punctuation.           |
|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| [`plan_turns`](_autosummary/braidio.pacing.html.md#braidio.pacing.plan_turns)(text, \*[, unit, min_turn, ...]) | Plan how to speak one narration beat.                                 |
| [`planned_gap_total_s`](_autosummary/braidio.pacing.html.md#braidio.pacing.planned_gap_total_s)(turns)                  | Total silence a plan inserts inside the beat (for reporting / tests). |
| [`split_paragraphs`](_autosummary/braidio.pacing.html.md#braidio.pacing.split_paragraphs)(text)                      | Split `text` on blank lines, collapsing runs of spaces within each.   |
| [`split_units`](_autosummary/braidio.pacing.html.md#braidio.pacing.split_units)(text, \*[, unit])               | Cut `text` into `unit`-sized pieces, grouped by paragraph.            |

### Classes

| [`Boundary`](_autosummary/braidio.pacing.html.md#braidio.pacing.Boundary)(name, gap_scale, speed_scale)         | How strongly a unit's trailing punctuation closes.            |
|-------------------------------------------------------------------------------------------------|---------------------------------------------------------------|
| [`NarrationTurn`](_autosummary/braidio.pacing.html.md#braidio.pacing.NarrationTurn)(text[, gap_after_s, speed, ...]) | One synthesizable chunk of a narration beat, with its pacing. |

### braidio.pacing.BOUNDARIES *: [Mapping](https://docs.python.org/3/library/typing.html#typing.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Boundary](_autosummary/braidio.pacing.html.md#braidio.pacing.Boundary)]* *= {'clause': Boundary(name='clause', gap_scale=0.5, speed_scale=0.99), 'ellipsis': Boundary(name='ellipsis', gap_scale=1.4, speed_scale=0.97), 'flowing': Boundary(name='flowing', gap_scale=0.35, speed_scale=1.0), 'none': Boundary(name='none', gap_scale=0.25, speed_scale=1.0), 'paragraph': Boundary(name='paragraph', gap_scale=2.2, speed_scale=0.96), 'sentence': Boundary(name='sentence', gap_scale=1.0, speed_scale=0.98)}*

The boundary table. Scales are relative to `"sentence"` (== the configured
`gap_turn_s`), from the punctuation→break-strength table in
`misc/docs/research/notebooklm-and-conversational-pacing.md`.

### *class* braidio.pacing.Boundary(name, gap_scale, speed_scale)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

How strongly a unit’s trailing punctuation closes.

`gap_scale` multiplies the base gap (`WeaveConfig.gap_turn_s`) and
`speed_scale` multiplies the turn’s speed — *final lengthening*, the thing
that keeps a pause from sounding like a dropout. The two are deliberately
correlated: a stronger boundary gets both a longer silence and a slower
approach to it.

### *class* braidio.pacing.NarrationTurn(text, gap_after_s=0.0, speed=None, boundary='sentence')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One synthesizable chunk of a narration beat, with its pacing.

`speed` is `None` when the delivery’s model has no speed control (eleven
v3) — the renderer then leaves `voice_settings["speed"]` alone rather than
sending a parameter the model is documented not to honor.

### braidio.pacing.SEGMENTATION_UNITS *= ('beat', 'paragraph', 'sentence', 'clause')*

How a beat’s text is cut into the units a turn is built from. `"beat"`
means “don’t cut at all” — one TTS call per beat, the historical behavior.

### braidio.pacing.SPEED_RANGE *= (0.7, 1.2)*

ElevenLabs clamps `voice_settings.speed` to this range in practice; a
jittered speed outside it is silently useless, so the planner clamps.

### braidio.pacing.classify_boundary(text)

Name the boundary implied by `text`’s trailing punctuation.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> classify_boundary("So that's the charge sheet.")
'sentence'
>>> classify_boundary('He said "no," and left,')
'clause'
>>> classify_boundary("and then —")
'flowing'
>>> classify_boundary("well…")
'ellipsis'
>>> classify_boundary("no punctuation here")
'none'
```

### braidio.pacing.plan_turns(text, , unit='sentence', min_turn=1, max_turn=3, seed=7, gap_s=0.0, speed_base=1.0, speed_jitter=0.0, gap_scale_cap_s=3.0)

Plan how to speak one narration beat. Pure and deterministic in `seed`.

Returns `[]` for `unit="beat"` (and for empty text) — the caller’s signal
to keep the historical single-call path, byte for byte. Otherwise each
returned [`NarrationTurn`](_autosummary/braidio.pacing.html.md#braidio.pacing.NarrationTurn) is one TTS call, with the silence to append
after it and the speed to read it at.

`gap_s` is the *sentence-boundary* gap; every other boundary scales off it
via [`BOUNDARIES`](_autosummary/braidio.pacing.html.md#braidio.pacing.BOUNDARIES), capped at `gap_scale_cap_s` (long silences read as
a dropout, and the engines are unstable past ~3 s). The final turn of the
beat never gets a trailing gap — the beat boundary belongs to the renderer’s
crossfade and to the next beat’s `Narration.lead_gap_s`.

`speed_base=None` plans no speed at all (eleven v3, which has no speed
control); a float centers a per-turn jitter of `±speed_jitter` on it,
scaled by the boundary’s final lengthening and clamped to [`SPEED_RANGE`](_autosummary/braidio.pacing.html.md#braidio.pacing.SPEED_RANGE).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`NarrationTurn`](_autosummary/braidio.pacing.html.md#braidio.pacing.NarrationTurn)]

```pycon
>>> turns = plan_turns("One. Two. Three. Four.", min_turn=2, max_turn=2, gap_s=0.3)
>>> [(t.text, t.gap_after_s) for t in turns]
[('One. Two.', 0.3), ('Three. Four.', 0.0)]
>>> plan_turns("A.\n\nB.", gap_s=0.2)[0].gap_after_s  # paragraph > sentence
0.44
```

### braidio.pacing.planned_gap_total_s(turns)

Total silence a plan inserts inside the beat (for reporting / tests).

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> planned_gap_total_s(plan_turns("A. B. C.", min_turn=1, max_turn=1, gap_s=0.3))
0.6
```

### braidio.pacing.split_paragraphs(text)

Split `text` on blank lines, collapsing runs of spaces within each.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### braidio.pacing.split_units(text, , unit='sentence')

Cut `text` into `unit`-sized pieces, grouped by paragraph.

Returns a list of paragraphs, each a list of units, so a caller can keep
turns from straddling a paragraph break (which would swallow the strongest
pause in the beat). `unit="beat"` returns one paragraph of one unit.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

```pycon
>>> split_units("A one. A two.\n\nB one.", unit="sentence")
[['A one.', 'A two.'], ['B one.']]
>>> split_units("Yes, really. No.", unit="clause")
[['Yes,', 'really.', 'No.']]
```


# _autosummary/braidio.relevance.html.md

# braidio.relevance

How well a still relates to the words spoken over it — the scorer seam.

A picture track used to be placed with no recorded reason and no check, so an
off-topic still could not be explained, caught or tuned (thorwhalen/braidio#85:
a Dylan portrait over a sentence that no longer mentioned Dylan, a Beach Boys
frame under a tag naming a different concert). `video_panels.plan` now scores
every placement against its `anchor_text` through a **scorer**, and this
module is where scorers live.

A scorer has the shape of `illustration`’s reranking `Scorer` —
`(query, items) -> scores` — with the anchor text as the query and still
*bodies* (plain dicts) as the items, one score in `[0, 1]` per still. Batch
by design, so a model-backed scorer (illustration’s SigLIP rerank, its VLM
judge) embeds the text once per span, not once per candidate.

The default, [`lexical_relevance()`](_autosummary/braidio.relevance.html.md#braidio.relevance.lexical_relevance), needs nothing installed: the share of a
still’s naming words (`subject`, `title`, `key`) that the anchor text
actually says. It is deliberately literal — it cannot see that a studio console
suits “Tom Wilson booked a rhythm section” — so a low score means \*the words do
not name this picture\*, which is the question a disclaimer or a decorative role
has to answer, not a verdict that the picture is bad.

### Examples

```pycon
>>> dylan = {"key": "dylan-1965", "subject": "Bob Dylan, 1965"}
>>> said = "the same day as the sessions for Bob Dylan's Like a Rolling Stone"
>>> round(lexical_relevance(said, [dylan])[0], 2)
0.67
>>> lexical_relevance("overdubbed electric guitar onto the tape", [dylan])
[0.0]
>>> resolve_scorer("lexical") is lexical_relevance
True
```

### Module Attributes

| [`RelevanceScorer`](_autosummary/braidio.relevance.html.md#braidio.relevance.RelevanceScorer)   | `(anchor_text, still_bodies) -> one score in [0, 1] per still`.   |
|--------------------------------------------------------------------|-------------------------------------------------------------------|
| [`RELEVANCE_SCORERS`](_autosummary/braidio.relevance.html.md#braidio.relevance.RELEVANCE_SCORERS) | Registered scorers by id.                                         |

### Functions

| [`lexical_relevance`](_autosummary/braidio.relevance.html.md#braidio.relevance.lexical_relevance)(anchor_text, stills)   | Per still: the best share of a naming field's words that `anchor_text` says.   |
|-------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`register_relevance_scorer`](_autosummary/braidio.relevance.html.md#braidio.relevance.register_relevance_scorer)(name, scorer)  | Make `scorer` addressable by `name` (e.g. an illustration rerank adapter).     |
| [`resolve_scorer`](_autosummary/braidio.relevance.html.md#braidio.relevance.resolve_scorer)(spec)                     | A scorer from a registered name, a callable, or `None` (the default).          |
| [`scorer_id`](_autosummary/braidio.relevance.html.md#braidio.relevance.scorer_id)(spec)                          | The id a panel records for `spec`: its registered name, else its qualname.     |

### braidio.relevance.RELEVANCE_SCORERS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[[Mapping](https://docs.python.org/3/library/typing.html#typing.Mapping)]], [Sequence](https://docs.python.org/3/library/typing.html#typing.Sequence)[[float](https://docs.python.org/3/builtins/functions.html#float)]]]* *= {'lexical': <function lexical_relevance>}*

Registered scorers by id. The id is what a panel records in `scorer`.

### braidio.relevance.RelevanceScorer

`(anchor_text, still_bodies) -> one score in [0, 1] per still`.

alias of `Callable`[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)[[`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)]], [`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]]

### braidio.relevance.lexical_relevance(anchor_text, stills)

Per still: the best share of a naming field’s words that `anchor_text` says.

The maximum over `NAMING_FIELDS`, so a long Commons title does not
dilute a short, exact subject.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> beach = {"key": "central-park-decay",
...          "subject": "The Beach Boys in Central Park, 1971",
...          "title": "Beach Boys Good Vibrations from Central Park 1971"}
>>> lexical_relevance("half a million people all getting the joke", [beach])
[0.0]
```

### braidio.relevance.register_relevance_scorer(name, scorer)

Make `scorer` addressable by `name` (e.g. an illustration rerank adapter).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> register_relevance_scorer("lexical", lexical_relevance)  # idempotent
```

### braidio.relevance.resolve_scorer(spec)

A scorer from a registered name, a callable, or `None` (the default).

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)[[`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)]], [`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]]

### braidio.relevance.scorer_id(spec)

The id a panel records for `spec`: its registered name, else its qualname.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> scorer_id(None), scorer_id(lexical_relevance)
('lexical', 'lexical')
```


# _autosummary/braidio.render.html.md

# braidio.render

Render a [`Script`](_autosummary/braidio.script.html.md#braidio.script.Script) into an audio file.

Walks the beats (filtered by the rights [`Profile`](_autosummary/braidio.rights.html.md#braidio.rights.Profile)):
narration beats are synthesized ([`braidio.tts`](_autosummary/braidio.tts.html.md#module-braidio.tts)) — as one call, or as paced
turns when the [`WeaveConfig`](_autosummary/braidio.weave_config.html.md#braidio.weave_config.WeaveConfig) asks for a smaller
`segmentation_unit` ([`braidio.pacing`](_autosummary/braidio.pacing.html.md#module-braidio.pacing)); segment beats are resolved
via a [`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource) and extracted (padded + faded);
scene breaks become a sting or a pause ([`braidio.structure`](_autosummary/braidio.structure.html.md#module-braidio.structure)). Every spoken
or clipped part is loudness-normalized, then either woven on a timeline (clips
tuck under narration — [`braidio.weave.weave_timeline()`](_autosummary/braidio.weave.html.md#braidio.weave.weave_timeline)) when a
[`WeaveConfig`](_autosummary/braidio.weave_config.html.md#braidio.weave_config.WeaveConfig) enables it, or concatenated.

This is the no-graph fast path; the same core is reused by the nw-app
transforms (which add provenance + partial re-render).

### Functions

| [`render_production`](_autosummary/braidio.render.html.md#braidio.render.render_production)(script, \*, source[, ...])   | Render `script` under `profile` → a single audio file.   |
|-------------------------------------------------------------------------------------------------|----------------------------------------------------------|

### braidio.render.render_production(script, , source, api_key=None, config=None, profile=Profile.PERSONAL, rights=None, delivery=None, cast=ConversationCast(roles={'A': 'cgSgspJ2msm6clMCkdW9', 'B': 'iP95p4xoKVk53GoZ742B'}, model_id='eleven_v3', settings={'stability': 0.45}), out_path=None, voice_id=None, crossfade_s=0.12, normalize=True, music_bed=None, structure=None, end_fade_s=0.35, end_silence_s=0.7, return_timeline=False, tts_dir='data/tts', clips_dir='data/clips', episodes_dir='data/episodes')

Render `script` under `profile` → a single audio file. Returns the path.

With `return_timeline=True` returns `(path, TimelineBreakdown)` instead —
the render records what it spent time on (per-beat kind, source interval,
duration, and offset) rather than leaving it to be reconstructed afterward,
plus, in `TimelineBreakdown.settings`, the settings that produced those
timings (see [`braidio.timeline.render_settings()`](_autosummary/braidio.timeline.html.md#braidio.timeline.render_settings)). Recording them is
what keeps a render reproducible: the pacing knobs below mean one script has
many possible cuts, so a persisted breakdown has to say which one it is.

Segment beats are resolved through `source` (a `SegmentSource`).
When `config` has `clip_edge_overlap_s > 0`, a clip is `placement="under"`,
or a `music_bed` is given, the parts are woven on a timeline; otherwise they
are concatenated. `rights` (if given) sets which segment rights are
publishable. `music_bed` lays an instrumental underscore under the whole
production (see [`braidio.music.MusicBed`](_autosummary/braidio.music.html.md#braidio.music.MusicBed)).

`structure` (a [`braidio.structure.MusicStructure`](_autosummary/braidio.structure.html.md#braidio.structure.MusicStructure)) is how the
production marks its structure with music: a scene-break beat plays its
`sting` (or a pause when there is none / the break is marked `"none"`),
and a spotlit segment beat drops the bed out for its duration. `None`
uses the inert defaults — no sting asset, no clip spotlit by default — so a
script without scene breaks or spotlight flags renders exactly as before.

**Pacing inside a narration beat** is a `config` choice. By default
(`WeaveConfig.segmentation_unit == "beat"`) a beat is one TTS call — one
prosodic arc, no silence anywhere in it. Set a smaller
`segmentation_unit` and the beat is cut into turns of
`min_turn..max_turn` units, each synthesized separately with a jittered
`speed_base ± speed_jitter` (where the model has a speed knob) and
followed by `gap_turn_s` scaled to how strongly the boundary closes —
see [`braidio.pacing`](_autosummary/braidio.pacing.html.md#module-braidio.pacing). Those four knobs do **nothing** here at the
default unit, which is the historical behavior kept byte-identical.

`api_key` is an optional per-request ElevenLabs key threaded to every
synthesized beat — both narration ([`braidio.tts.narrate()`](_autosummary/braidio.tts.html.md#braidio.tts.narrate)) and dialogue
([`braidio.conversation.render_dialogue()`](_autosummary/braidio.conversation.html.md#braidio.conversation.render_dialogue)). When `None` (default) each
synthesizer falls back to `$ELEVENLABS_API_KEY` (unchanged behavior); an
explicit key lets a caller (e.g. a per-user BYO-key request) override the
environment without mutating it. Segment beats never call ElevenLabs, so the
key does not touch them.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`object`](https://docs.python.org/3/builtins/functions.html#object)]


# _autosummary/braidio.rights.html.md

# braidio.rights

Render profiles: enforce personal-vs-published rights as data.

Rights are encoded on the beats (`SegmentBeat.rights`, and — mechanically —
the presence of forbidden verbatim text in narration) and a **render profile**
filters the script into the beats that may actually render:

- `PERSONAL`  — include everything, including owned/copyrighted segment audio.
- `PUBLISHED` — exclude any non-publishable segment audio and any beat
  carrying forbidden verbatim text; keep original narration, publishable
  segments, and short transformative substitutes.

The rule is enforced *mechanically*: [`plan_production()`](_autosummary/braidio.rights.html.md#braidio.rights.plan_production) routes beats by
their `rights` flag, and [`find_verbatim_text()`](_autosummary/braidio.rights.html.md#braidio.rights.find_verbatim_text) / [`content_violations()`](_autosummary/braidio.rights.html.md#braidio.rights.content_violations)
scan the resulting published beats against a **caller-supplied** set of
forbidden texts (via [`RightsPolicy`](_autosummary/braidio.rights.html.md#braidio.rights.RightsPolicy)) — so a leak is a test failure, not a
judgment call. braidio owns the scanner + profile filter; the consumer injects
*what* is forbidden (e.g. Hamilton injects the song’s lyric lines).

Not legal advice.

### Module Attributes

| [`DEFAULT_PROFILE`](_autosummary/braidio.rights.html.md#braidio.rights.DEFAULT_PROFILE)   | The profile a render uses when the caller declares none — the permissive one, so a production that never mentions rights renders everything it was given.   |
|--------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|

### Functions

| [`clip_plays_under`](_autosummary/braidio.rights.html.md#braidio.rights.clip_plays_under)(profile, rights[, publishable])   | Whether a segment with `rights` plays as audio under `profile`.   |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
| [`content_violations`](_autosummary/braidio.rights.html.md#braidio.rights.content_violations)(plan, forbidden, \*[, ...])     | Rights violations in a *published* plan (empty list = clean).     |
| [`find_verbatim_text`](_autosummary/braidio.rights.html.md#braidio.rights.find_verbatim_text)(text, forbidden, \*[, ...])     | Forbidden lines that appear (near-)verbatim in `text`.            |
| [`plan_production`](_autosummary/braidio.rights.html.md#braidio.rights.plan_production)(script, profile, \*[, ...])        | Filter `script` into the beats renderable under `profile`.        |
| [`rights_are_publishable`](_autosummary/braidio.rights.html.md#braidio.rights.rights_are_publishable)(rights[, publishable])      | The publishable test at the level of a bare `rights` string.      |
| [`segment_is_publishable`](_autosummary/braidio.rights.html.md#braidio.rights.segment_is_publishable)(beat[, publishable])        | Whether `beat`'s audio may play in the published cut.             |

### Classes

| [`PlannedBeat`](_autosummary/braidio.rights.html.md#braidio.rights.PlannedBeat)(kind, content, from_index[, ...])   | A beat resolved for a profile — what the renderer actually plays.   |
|--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------|
| [`Profile`](_autosummary/braidio.rights.html.md#braidio.rights.Profile)(\*values)                               | Which projection of the production we render.                       |
| [`RenderPlan`](_autosummary/braidio.rights.html.md#braidio.rights.RenderPlan)(profile[, beats, dropped, ...])      |                                                                     |
| [`RightsPolicy`](_autosummary/braidio.rights.html.md#braidio.rights.RightsPolicy)([forbidden_texts, ...])            | Injected rights configuration for the published profile.            |

### Exceptions

| [`RightsViolation`](_autosummary/braidio.rights.html.md#braidio.rights.RightsViolation)   | A render would play source audio the profile it claims forbids.   |
|--------------------------------------------------------------------|-------------------------------------------------------------------|

### braidio.rights.DEFAULT_PROFILE *: [Profile](_autosummary/braidio.rights.html.md#braidio.rights.Profile)* *= Profile.PERSONAL*

The profile a render uses when the caller declares none — the permissive
one, so a production that never mentions rights renders everything it was
given. **Every** entry point resolves its default from here (the no-graph
fast path [`braidio.render.render_production()`](_autosummary/braidio.render.html.md#braidio.render.render_production) /
[`braidio.formats.render_format()`](_autosummary/braidio.formats.html.md#braidio.formats.render_format), the graph path
`braidio.transforms.weave_project()`, and the MCP tools), so the two
render paths cannot drift into disagreeing about what “unspecified” means.

### *class* braidio.rights.PlannedBeat(kind, content, from_index, note='', turns=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A beat resolved for a profile — what the renderer actually plays.

`kind` is `"narration"` (synthesize `content`), `"clip"` (resolve
`content` as a segment reference and cut audio), `"dialogue"` (synthesize
`turns`) or `"scene_break"` (a structural boundary — no content; the
renderer marks it with music). `from_index` points at the source beat;
`note` records any substitution/drop reasoning.

### *class* braidio.rights.Profile(\*values)

Bases: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

Which projection of the production we render.

### *class* braidio.rights.RenderPlan(profile, beats=<factory>, dropped=<factory>, substituted=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

### *class* braidio.rights.RightsPolicy(forbidden_texts=<function RightsPolicy.<lambda>>, publishable_clip_rights=frozenset({'public-domain'}))

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Injected rights configuration for the published profile.

`forbidden_texts` yields the strings that must not appear verbatim in
published narration (e.g. copyrighted lyric lines). `publishable_clip_rights`
is the set of segment `rights` values allowed in the published cut.

### *exception* braidio.rights.RightsViolation

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A render would play source audio the profile it claims forbids.

Raised where a rights decision is *verified* rather than made — today by the
graph path’s episode transform, which checks the members it is about to
weave against the profile it is about to stamp on them. A `ValueError`
subclass so existing callers keep catching it, typed so a caller that cares
can tell a rights refusal from a malformed input.

### braidio.rights.clip_plays_under(profile, rights, publishable=frozenset({'public-domain'}))

Whether a segment with `rights` plays as audio under `profile`.

The whole clip-routing rule, in one place: `PERSONAL` plays everything,
`PUBLISHED` plays only publishable rights. [`plan_production()`](_autosummary/braidio.rights.html.md#braidio.rights.plan_production) asks it
when it filters a script, and the graph’s episode transform re-asks it at
weave time to verify that the members it inherited still match the profile
the production now declares — one rule, asked twice, never copied.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### braidio.rights.content_violations(plan, forbidden, , min_words=5)

Rights violations in a *published* plan (empty list = clean).

Fails if any planned beat plays non-publishable segment audio, or any
narration beat contains forbidden verbatim text.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### braidio.rights.find_verbatim_text(text, forbidden, , min_words=5)

Forbidden lines that appear (near-)verbatim in `text`.

A line counts as leaked if `text` shares a run of `min_words` consecutive
words with it (case-insensitive, word-level). Single words and short common
phrases don’t trip it — only substantial verbatim quoting.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### braidio.rights.plan_production(script, profile, , publishable_clip_rights=frozenset({'public-domain'}))

Filter `script` into the beats renderable under `profile`.

* **Return type:**
  [`RenderPlan`](_autosummary/braidio.rights.html.md#braidio.rights.RenderPlan)

### braidio.rights.rights_are_publishable(rights, publishable=frozenset({'public-domain'}))

The publishable test at the level of a bare `rights` string.

The graph records rights on nodes, not on `SegmentBeat`s, so the check
has to be askable without a beat in hand — but it must stay the *same*
check. [`segment_is_publishable()`](_autosummary/braidio.rights.html.md#braidio.rights.segment_is_publishable) is this function with a beat
unwrapped, never a parallel rule.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### braidio.rights.segment_is_publishable(beat, publishable=frozenset({'public-domain'}))

Whether `beat`’s audio may play in the published cut.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)


# _autosummary/braidio.script.html.md

# braidio.script

Composition model — the ordered beats a render walks.

A [`Script`](_autosummary/braidio.script.html.md#braidio.script.Script) is an ordered list of beats: a [`Narration`](_autosummary/braidio.script.html.md#braidio.script.Narration) (spoken,
synthesized), a [`Dialogue`](_autosummary/braidio.script.html.md#braidio.script.Dialogue) (a multi-voice exchange), a [`SegmentBeat`](_autosummary/braidio.script.html.md#braidio.script.SegmentBeat)
(a *reference* to a span of source media to resolve and weave in), or a
[`SceneBreak`](_autosummary/braidio.script.html.md#braidio.script.SceneBreak) (a structural boundary — “new section” — that the renderer
marks with music, see [`braidio.structure`](_autosummary/braidio.structure.html.md#module-braidio.structure)). This is the generic,
media-agnostic projection a renderer consumes; how a reference maps to audio is a
[`braidio.sources.SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource) concern, and what the beats are backed by
(lyrics, a graph, hand-authoring) is the consumer’s concern.

### Functions

| [`narration_segments`](_autosummary/braidio.script.html.md#braidio.script.narration_segments)(script)   | All narration (default text) of a script, as sentence-level segments.   |
|-------------------------------------------------------------------------------|-------------------------------------------------------------------------|

### Classes

| [`Dialogue`](_autosummary/braidio.script.html.md#braidio.script.Dialogue)(turns[, label])                      | A multi-speaker commentary exchange (the conversational register).      |
|------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`Narration`](_autosummary/braidio.script.html.md#braidio.script.Narration)(text[, style, published_text, ...]) | A spoken narration beat (authored, synthesized by TTS).                 |
| [`SceneBreak`](_autosummary/braidio.script.html.md#braidio.script.SceneBreak)([label, marker])                   | A structural boundary between sections — the "new scene" beat.          |
| [`Script`](_autosummary/braidio.script.html.md#braidio.script.Script)(title, id_slug[, beats])               | An ordered production script.                                           |
| [`SegmentBeat`](_autosummary/braidio.script.html.md#braidio.script.SegmentBeat)(reference[, label, rights, ...])  | A span of source media to weave in, addressed by an opaque `reference`. |

### *class* braidio.script.Dialogue(turns, label='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A multi-speaker commentary exchange (the conversational register).

`turns` is an ordered tuple of `(role, text)` pairs (roles like
`"A"`/`"B"` map to voices via a [`ConversationCast`](_autosummary/braidio.conversation.html.md#braidio.conversation.ConversationCast)
at render time). Rendered in ONE pass via Text-to-Dialogue so it sounds like
people talking to each other. This is our own commentary → always publishable
(its text is still scanned for forbidden verbatim quotes in the published cut).

### *class* braidio.script.Narration(text, style=None, published_text=None, lead_gap_s=0.0, voice=None, voice_settings=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A spoken narration beat (authored, synthesized by TTS).

`published_text` is an optional rights-safe rewrite the published profile
uses when the default `text` quotes forbidden (e.g. copyrighted) content;
leave it `None` when the narration is already clean.

`lead_gap_s` prepends a beat of silence — breathing room before a
register change (e.g. a book-read entering after a conversation), so it
doesn’t feel glued to the previous speaker.

`voice` and `voice_settings` are per-beat overrides so one timeline can
carry contrasting roles — e.g. a lively *presenter* and a graver \*book
narrator\* (different voice, and/or a different delivery preset’s
`voice_settings`). Both fall back to the render’s production-level defaults
when `None`.

### *class* braidio.script.SceneBreak(label='', marker=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A structural boundary between sections — the “new scene” beat.

Audio has no visual white space, so a boundary has to be *heard*: the
renderer marks it with a sting (a short musical marker) when the production
supplies one and the resolved `marker` is `"sting"`, and otherwise with
a beat of silence. This is the beat the format templates’ structure — a
debate’s open / rebuttal / close, a panel’s rounds, a documentary’s acts —
is expressed with; the talk on either side is unchanged.

`marker` overrides the format default per break (`None` = defer to
`braidio.structure.MusicStructure.scene_marker`). `label` names the
section that starts here (e.g. `"rebuttal"`) for the timeline breakdown.
A scene break synthesizes nothing, so it costs nothing.

### *class* braidio.script.Script(title, id_slug, beats=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

An ordered production script.

### *class* braidio.script.SegmentBeat(reference, label='', rights='owned-local', published_substitute=None, placement='before', spotlight=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A span of source media to weave in, addressed by an opaque `reference`.

The renderer resolves `reference` → `[start,end)` via a
[`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource) and cuts it. `rights` (e.g.
`owned-local` / `copyrighted` / `public-domain`) drives the render
profile; `published_substitute` is a transformative narration the
published profile swaps in when the segment’s audio can’t be used.

`placement` is how the clip sits against the talk (the weaving grammar):
`"before"` (default) and `"after"` play the clip **clean** in its own
slot — the set-up→clip and clip→payoff patterns, distinguished by where you
order the beat relative to the talk. `"under"` plays the clip
**concurrently beneath the following talk beat**, ducked by `duck_db` —
the “host talks over the clip” technique (place the clip *immediately before*
the talk it should sit under).

`spotlight` is the fade-to-spotlight override: `True` drops the music
bed out before this clip so it lands in silence rather than competing with
underscore (the bed resumes after it); `False` keeps the bed under it.
`None` (default) defers to the format’s
`braidio.structure.MusicStructure.spotlight_clips`. Without a music
bed the flag is inert.

### braidio.script.narration_segments(script)

All narration (default text) of a script, as sentence-level segments.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]


# _autosummary/braidio.sources.html.md

# braidio.sources

Segment sources: resolve a *reference* to a cuttable `[start, end]` window.

A production weaves in extracted segments of source media. *How* a reference
(a lyric quote, an audiobook passage, a news phrase, an SFX cue) maps to a
concrete `[start, end]` span of a source asset is pluggable via the
[`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource) protocol — the weave engine never needs to know it is
lyrics.

This module ships two generic implementations. [`TimedLineSegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.TimedLineSegmentSource),
a token-F1 matcher over time-aligned lines ([`TimedLine`](_autosummary/braidio.sources.html.md#braidio.sources.TimedLine)). It answers a
reference by the best contiguous run of lines — handling exact, sub-line, and
multi-line references. Consumers bind it to their own timed lines + asset (e.g.
Hamilton binds LRCLIB line timings + owned song audio). And
[`NamespacedSegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.NamespacedSegmentSource) routes a *prefixed* reference to one of several
sub-sources, which is how a production that cuts between two recordings of the
same work says which recording a given quote should come from.

Also provides the lower-level resolver ([`find_segment()`](_autosummary/braidio.sources.html.md#braidio.sources.find_segment), [`load_timing()`](_autosummary/braidio.sources.html.md#braidio.sources.load_timing))
and the resolve-and-cut convenience ([`cut_quote()`](_autosummary/braidio.sources.html.md#braidio.sources.cut_quote)) used directly by the
Hamilton pilot; new code should prefer the [`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource) protocol +
[`braidio.weave.extract_padded()`](_autosummary/braidio.weave.html.md#braidio.weave.extract_padded).

### Functions

| [`cut_quote`](_autosummary/braidio.sources.html.md#braidio.sources.cut_quote)(audio_path, lines, quote, out_path, \*)   | Resolve `quote` → segment and cut it from `audio_path` (pad + fades).                                       |
|------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
| [`find_segment`](_autosummary/braidio.sources.html.md#braidio.sources.find_segment)(lines, quote, \*[, max_span, ...])     | Best contiguous run of timed lines matching `quote`, or `None`.                                             |
| [`load_timing`](_autosummary/braidio.sources.html.md#braidio.sources.load_timing)(path)                                   | Load a `{lines: [{index,start_s,end_s,text}]}` JSON into <br/><br/>```<br/>`<br/>```<br/><br/>TimedLine\`s. |

### Classes

| [`NamespacedSegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.NamespacedSegmentSource)(sources, \*[, sep, ...])   | A [`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource) that routes prefixed references to sub-sources.   |
|-----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
| [`ResolvedSegment`](_autosummary/braidio.sources.html.md#braidio.sources.ResolvedSegment)(asset_path, start_s, end_s)        | A cuttable span of a source asset — what a [`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource) returns. |
| [`Segment`](_autosummary/braidio.sources.html.md#braidio.sources.Segment)(start_s, end_s, score, line_start, ...)    | A resolved window for a reference, with the matched line span.                                                     |
| [`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource)(\*args, \*\*kwargs)                  | Resolve an opaque `reference` to a [`ResolvedSegment`](_autosummary/braidio.sources.html.md#braidio.sources.ResolvedSegment) (or None).     |
| [`TimedLine`](_autosummary/braidio.sources.html.md#braidio.sources.TimedLine)(index, start_s, end_s, text)             | A source line with a `[start_s, end_s)` window (end may be None = tail).                                           |
| [`TimedLineSegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.TimedLineSegmentSource)(\*, lines, asset_path)      | A [`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource) over time-aligned lines + one source asset.       |

### *class* braidio.sources.NamespacedSegmentSource(sources, , sep=':', default=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A [`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource) that routes prefixed references to sub-sources.

A production that cuts between *several* recordings of the same work — two
takes of one song, a studio master and a live version, an audiobook read in
two languages — hits a wall: `render_production(..., source=...)` takes one
[`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource), and a reference like a lyric line matches equally
well in every recording. Which asset a quote should come from is a
*production* decision, not something a token matcher can infer.

This routes on an explicit prefix, so the script says which recording it
means:

```default
source = NamespacedSegmentSource(
    {
        "1966": TimedLineSegmentSource(lines=studio, asset_path="studio.mp3"),
        "1981": TimedLineSegmentSource(lines=live, asset_path="live.mp3"),
    }
)
SegmentBeat("1981: and in the naked light I saw")  # → the live master
```

* **Parameters:**
  * **sources** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource)]) – Namespace → sub-source. Namespaces are matched
    case-insensitively and with surrounding whitespace stripped.
  * **sep** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Separator between the namespace and the reference body.
  * **default** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Namespace used for an *unprefixed* reference. `None` (the
    default) makes an unprefixed reference an error.
* **Raises:**
  [**KeyError**](https://docs.python.org/3/builtins/exceptions.html#KeyError) – on an unknown namespace, or an unprefixed reference with no
      `default`. This is deliberate: silently falling through to the
      wrong recording would ship one performance under commentary that
      describes another — the failure a listener cannot detect and the
      author cannot see in a diff.

#### *property* namespaces *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

The namespaces this source routes, in insertion order.

#### split(reference)

Split `reference` into `(namespace, body)`, applying the default.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### *class* braidio.sources.ResolvedSegment(asset_path, start_s, end_s, score=1.0, matched_text='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A cuttable span of a source asset — what a [`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource) returns.

### *class* braidio.sources.Segment(start_s, end_s, score, line_start, line_end, matched_text)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A resolved window for a reference, with the matched line span.

### *class* braidio.sources.SegmentSource(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Resolve an opaque `reference` to a [`ResolvedSegment`](_autosummary/braidio.sources.html.md#braidio.sources.ResolvedSegment) (or None).

### *class* braidio.sources.TimedLine(index, start_s, end_s, text)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A source line with a `[start_s, end_s)` window (end may be None = tail).

### *class* braidio.sources.TimedLineSegmentSource(, lines, asset_path, song_end_s=None, min_score=0.5)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A [`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource) over time-aligned lines + one source asset.

Binds the generic [`find_segment()`](_autosummary/braidio.sources.html.md#braidio.sources.find_segment) matcher to a concrete asset so the
weave engine can `resolve(reference) -> ResolvedSegment`.

### braidio.sources.cut_quote(audio_path, lines, quote, out_path, , pad_pre_s=0.15, pad_post_s=0.35, fade_s=0.04, min_score=0.5, song_end_s=None)

Resolve `quote` → segment and cut it from `audio_path` (pad + fades).

Convenience combining [`find_segment()`](_autosummary/braidio.sources.html.md#braidio.sources.find_segment) + an ffmpeg cut. Returns the
resolved [`Segment`](_autosummary/braidio.sources.html.md#braidio.sources.Segment) (raises `LookupError` if unmatched). New code
should prefer a [`SegmentSource`](_autosummary/braidio.sources.html.md#braidio.sources.SegmentSource) + [`braidio.weave.extract_padded()`](_autosummary/braidio.weave.html.md#braidio.weave.extract_padded).

* **Return type:**
  [`Segment`](_autosummary/braidio.sources.html.md#braidio.sources.Segment)

### braidio.sources.find_segment(lines, quote, , max_span=12, min_score=0.5, song_end_s=None)

Best contiguous run of timed lines matching `quote`, or `None`.

Scores every run `lines[i..j]` (up to `max_span` lines) by token F1
against the reference’s tokens and returns the highest-scoring run clearing
`min_score`. Handles single-line, sub-line, and multi-line references.

* **Return type:**
  [`Segment`](_autosummary/braidio.sources.html.md#braidio.sources.Segment) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### braidio.sources.load_timing(path)

Load a `{lines: [{index,start_s,end_s,text}]}` JSON into 

```
`
```

TimedLine\`s.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`TimedLine`](_autosummary/braidio.sources.html.md#braidio.sources.TimedLine)]


# _autosummary/braidio.structure.html.md

# braidio.structure

Structural music — stings at scene breaks, fade-to-spotlight on exhibits.

The music bed ([`braidio.music`](_autosummary/braidio.music.html.md#module-braidio.music)) is the *continuous* music layer. This module
is the *structural* one: the cues that tell a listener where they are, because
audio has no visual white space (`misc/docs/research/commentary-formats-and-
styles.md`, “Music as structure”):

- a **sting** — a short musical marker — at a [`SceneBreak`](_autosummary/braidio.script.html.md#braidio.script.SceneBreak)
  (“new section”);
- **fade-to-spotlight** — the bed drops *out* before a key exhibit so the clip
  lands in silence instead of competing with underscore, and resumes after it.

Both are driven by a [`MusicStructure`](_autosummary/braidio.structure.html.md#braidio.structure.MusicStructure): the format’s defaults
(`scene_marker`, `spotlight_clips`) plus the production’s [`Sting`](_autosummary/braidio.structure.html.md#braidio.structure.Sting)
asset. It is the single seam [`braidio.render.render_production()`](_autosummary/braidio.render.html.md#braidio.render.render_production) takes
(`structure=`); `None` means “no structure” and reproduces the plain weave
exactly. A beat overrides the defaults with `SceneBreak.marker` /
`SegmentBeat.spotlight`.

braidio ships **no music**: like the bed, the sting is an app-supplied asset
(`render_format(..., sting_asset=…)`). A scene break with no sting to play is
still audible — as `MusicStructure.pause_s` of silence — so the structure
is never silently lost.

### Functions

| [`prepare_pause`](_autosummary/braidio.structure.html.md#braidio.structure.prepare_pause)(seconds, out_path, \*[, ...])     | Render `seconds` of stereo silence — a scene break with no sting to play.   |
|--------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`prepare_sting`](_autosummary/braidio.structure.html.md#braidio.structure.prepare_sting)(sting, out_path, \*, target_lufs) | Render `sting` to a ready-to-place part: trimmed, faded, levelled, padded.  |

### Classes

| [`MusicStructure`](_autosummary/braidio.structure.html.md#braidio.structure.MusicStructure)([sting, scene_marker, ...])   | How a production marks its structure with music — the render seam.   |
|-----------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| [`Sting`](_autosummary/braidio.structure.html.md#braidio.structure.Sting)(asset_path[, gain_db, max_len_s, ...]) | A short musical marker played at a scene break.                      |

### *class* braidio.structure.MusicStructure(sting=None, scene_marker='sting', spotlight_clips=False, pause_s=0.8)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

How a production marks its structure with music — the render seam.

`scene_marker` is the default for a [`SceneBreak`](_autosummary/braidio.script.html.md#braidio.script.SceneBreak)
whose `marker` is `None` (`"sting"` / `"none"`). `spotlight_clips`
is the default for a [`SegmentBeat`](_autosummary/braidio.script.html.md#braidio.script.SegmentBeat) whose
`spotlight` is `None` — `True` drops the bed under every exhibit.
`sting` is the production’s sting asset; with none supplied a
`"sting"`-marked break falls back to `pause_s` of silence.

The all-defaults value is inert for a script with no scene breaks and no
spotlight-marked clips: a format that declares no structure renders exactly
as it did without this layer.

#### marker_for(beat)

The marker a scene break renders with (its override, else the default).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### marker_of(marker)

The marker a break with this per-beat override renders with.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### plays_sting(beat)

Whether `beat` plays the sting (marked `"sting"` *and* one is supplied).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### plays_sting_of(marker)

Whether this per-beat marker plays the sting (and one is supplied).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### spotlight_for(beat)

Whether `beat` is spotlit (its override, else the format default).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### spotlight_of(spotlight)

Whether this per-beat override is spotlit (`None` = the format default).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### *class* braidio.structure.Sting(asset_path, gain_db=-6.0, max_len_s=3.0, fade_out_s=0.5, gap_after_s=0.3)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A short musical marker played at a scene break.

`asset_path` is an app-supplied sound (a hit, a riser, a few notes of the
theme). It is trimmed to `max_len_s` with a `fade_out_s` tail, levelled
to the production’s loudness target, sat `gain_db` under the voice, and
followed by `gap_after_s` of breathing room before the talk resumes.

### braidio.structure.prepare_pause(seconds, out_path, , sample_rate=44100)

Render `seconds` of stereo silence — a scene break with no sting to play.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### braidio.structure.prepare_sting(sting, out_path, , target_lufs, sample_rate=44100)

Render `sting` to a ready-to-place part: trimmed, faded, levelled, padded.

The result is already at production level (`target_lufs` then
`sting.gain_db`), so the caller places it as-is rather than normalizing
it again. Returns `out_path`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/braidio.style.html.md

# braidio.style

Style audit — flag the recycled rhetorical tics in commentary text.

The companion, in code, to `misc/docs/style/anti-platitude-checklist.md`: scan
authored commentary for the empty structural platitudes a writing model falls
back on, so a production can self-check (and measure) instead of eyeballing.

> from braidio.style import audit_platitudes, platitude_rate
> findings = audit_platitudes(commentary_text)   # [{pattern, match, start}, …]
> rate = platitude_rate(commentary_text)          # flagged hits per 1000 words

Not every match is a crime — the guide says these moves are fine *sparingly*; the
value is the RATE and which patterns dominate. Chiasmus (“the X that did Y is the
X that does Z”) isn’t reliably regex-detectable and is intentionally omitted.

### Module Attributes

| [`PLATITUDE_PATTERNS`](_autosummary/braidio.style.html.md#braidio.style.PLATITUDE_PATTERNS)   | name → compiled pattern for the detectable overused moves.   |
|-----------------------------------------------------------------------|--------------------------------------------------------------|
| [`TAG_RATE_FLOOR`](_autosummary/braidio.style.html.md#braidio.style.TAG_RATE_FLOOR)       | Tags per 100 words.                                          |

### Functions

| [`audio_tag_rate`](_autosummary/braidio.style.html.md#braidio.style.audio_tag_rate)(text, \*[, per])   | Inline audio tags per `per` words — the expressiveness dial.                                        |
|------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
| [`audio_tags`](_autosummary/braidio.style.html.md#braidio.style.audio_tags)(text)                  | Every inline `[audio tag]` in `text`, in order.                                                     |
| [`audit_expressiveness`](_autosummary/braidio.style.html.md#braidio.style.audit_expressiveness)(text)        | Human-readable complaints about a script's written-in performance.                                  |
| [`audit_platitudes`](_autosummary/braidio.style.html.md#braidio.style.audit_platitudes)(text)            | Return every [`Finding`](_autosummary/braidio.style.html.md#braidio.style.Finding) in `text`, in document order. |
| [`platitude_rate`](_autosummary/braidio.style.html.md#braidio.style.platitude_rate)(text, \*[, per])   | Flagged hits per `per` words (default 1000).                                                        |

### Classes

| [`Finding`](_autosummary/braidio.style.html.md#braidio.style.Finding)(pattern, match, start)   | One flagged platitude.   |
|-----------------------------------------------------------------------------------|--------------------------|

### *class* braidio.style.Finding(pattern, match, start)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One flagged platitude.

### braidio.style.PLATITUDE_PATTERNS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Pattern](https://docs.python.org/3/library/re.html#re.Pattern)]* *= {'director-cue': re.compile('\\\\b(?:listen to|notice|watch|catch)\\\\s+(?:how|what|the)\\\\b', re.IGNORECASE), 'heres-the': re.compile("\\\\bhere'?s the\\\\b", re.IGNORECASE), 'machinery-naming': re.compile('\\\\bthe (?:turn|tell|button|trick|move|thesis)\\\\b', re.IGNORECASE), 'negation-just': re.compile("\\\\bisn'?t just\\\\b", re.IGNORECASE), 'reduction': re.compile("\\\\b(?:that'?s the whole|the whole \\\\w+ in|in (?:two|three|four|five|six|seven|eight|nine|ten|\\\\d+) words)\\\\b", re.IGNORECASE)}*

name → compiled pattern for the detectable overused moves.

### braidio.style.TAG_RATE_FLOOR *= 1.5*

Tags per 100 words. Below the floor a v3 read goes flat; above the ceiling it
starts performing every clause and reads as camp. Derived from the measured
table above plus the “kitsch” complaint on a real episode.

### braidio.style.audio_tag_rate(text, , per=100)

Inline audio tags per `per` words — the expressiveness dial.

Only meaningful on a delivery whose model renders tags at all
(`braidio.delivery.Delivery.supports_audio_tags`); on
`eleven_multilingual_v2` the tags are inert text and this number is a lie.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### braidio.style.audio_tags(text)

Every inline `[audio tag]` in `text`, in order.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### braidio.style.audit_expressiveness(text)

Human-readable complaints about a script’s written-in performance.

Returns an empty list when the script sits in the target band. This is the
gate [`audit_platitudes()`](_autosummary/braidio.style.html.md#braidio.style.audit_platitudes) is not: platitudes catch recycled *phrases*,
this catches a script that will be read flatly however good the words are.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### braidio.style.audit_platitudes(text)

Return every [`Finding`](_autosummary/braidio.style.html.md#braidio.style.Finding) in `text`, in document order.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/braidio.style.html.md#braidio.style.Finding)]

### braidio.style.platitude_rate(text, , per=1000)

Flagged hits per `per` words (default 1000). 0.0 for empty text.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)


# _autosummary/braidio.textprep.html.md

# braidio.textprep

Text preparation for scripts — clean OCR’d source, tidy authored lines.

Small, reusable helpers a commentary production reaches for repeatedly:

- [`clean_ocr()`](_autosummary/braidio.textprep.html.md#braidio.textprep.clean_ocr) — normalize text extracted from PDFs/scans (ligatures,
  soft-hyphens, doubled hyphens, whitespace) so a TTS voice reads it cleanly.
- [`strip_speaker_labels()`](_autosummary/braidio.textprep.html.md#braidio.textprep.strip_speaker_labels) — drop a leaked `"Host: "` / `"Chris: "`
  prefix that a writing model sometimes prepends and that would otherwise be
  read aloud.

These were duplicated in ad-hoc rendering scripts; they belong here so any
consumer (Hamilton and the next app) shares one implementation.

### Module Attributes

| [`LIGATURES`](_autosummary/braidio.textprep.html.md#braidio.textprep.LIGATURES)   | Latin ligatures that OCR emits as single code points.   |
|--------------------------------------------------------------|---------------------------------------------------------|

### Functions

| [`clean_ocr`](_autosummary/braidio.textprep.html.md#braidio.textprep.clean_ocr)(text, \*[, collapse_whitespace])   | Normalize OCR/PDF-extracted text for clean narration.                |
|-----------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| [`strip_speaker_labels`](_autosummary/braidio.textprep.html.md#braidio.textprep.strip_speaker_labels)(text)                   | Remove a leading speaker-label prefix (e.g. `"Chris: "`) if present. |

### braidio.textprep.LIGATURES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'ﬀ': 'ff', 'ﬁ': 'fi', 'ﬂ': 'fl', 'ﬃ': 'ffi', 'ﬄ': 'ffl', 'ﬅ': 'ft', 'ﬆ': 'st'}*

Latin ligatures that OCR emits as single code points.

### braidio.textprep.clean_ocr(text, , collapse_whitespace=True)

Normalize OCR/PDF-extracted text for clean narration.

Expands ligatures, removes soft-hyphens (used at scan line-breaks), turns a
doubled hyphen `--` into an em-dash (so TTS phrases it as a pause), and
(by default) collapses runs of whitespace to single spaces.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### braidio.textprep.strip_speaker_labels(text)

Remove a leading speaker-label prefix (e.g. `"Chris: "`) if present.

Only strips a single leading `Word:` / `Host:` style label so it isn’t
read aloud; leaves colons that are part of the sentence untouched.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/braidio.timeline.html.md

# braidio.timeline

Timeline breakdown — what a production spends its time on, and in what order.

A rendered production is a walk of beats; this module turns that walk into a
queryable object: for each beat, its **kind**, **source interval** (for clips),
**rendered duration**, and **offset on the episode timeline** — plus per-kind
**totals** and a self-contained **HTML view**. It’s built on the same layout math
([`braidio.weave.layout_placed()`](_autosummary/braidio.weave.html.md#braidio.weave.layout_placed)) the renderer uses, so the offsets match the
actual mix.

Get one straight from the renderer:

```default
out, tl = render_production(script, source=src, return_timeline=True)
tl.totals()          # {"clip": 78.6, "book-passage": 346.4, "narration": 616.5}
tl.shares()          # same, as fractions
open("anatomy.html", "w").write(tl.to_html("Episode anatomy"))
```

or build one from raw render data with [`build_timeline()`](_autosummary/braidio.timeline.html.md#braidio.timeline.build_timeline). This is the
first-class, reusable form of what used to be reconstructed by ad-hoc scripts
(braidio#3): the render now *records* its timeline instead of anyone probing the
output after the fact.

### Functions

| [`build_timeline`](_autosummary/braidio.timeline.html.md#braidio.timeline.build_timeline)(\*, kinds, durations[, ...])   | Assemble a [`TimelineBreakdown`](_autosummary/braidio.timeline.html.md#braidio.timeline.TimelineBreakdown) from per-beat render data (pure).   |
|------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| [`render_settings`](_autosummary/braidio.timeline.html.md#braidio.timeline.render_settings)(\*, config, crossfade_s, ...) | The record of *how* a production was rendered, as plain JSON types.                                               |

### Classes

| [`BeatSpan`](_autosummary/braidio.timeline.html.md#braidio.timeline.BeatSpan)(index, kind[, label, source_start, ...])   | One beat's place on the timeline.                                         |
|------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| [`TimelineBreakdown`](_autosummary/braidio.timeline.html.md#braidio.timeline.TimelineBreakdown)([beats, title, settings])         | The ordered beats of a production, with per-kind totals and an HTML view. |

### *class* braidio.timeline.BeatSpan(index, kind, label='', source_start=None, source_end=None, duration=0.0, start=0.0, placement=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One beat’s place on the timeline.

### *class* braidio.timeline.TimelineBreakdown(beats=<factory>, title='', settings=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The ordered beats of a production, with per-kind totals and an HTML view.

`settings` is the **render record**: the settings that actually produced
these timings (see [`render_settings()`](_autosummary/braidio.timeline.html.md#braidio.timeline.render_settings)), or `None` for a breakdown
built by hand. It exists because a render stopped being reproducible from
the script alone the moment `WeaveConfig.segmentation_unit` became a real
switch (braidio#63): the same script now renders to different durations
under different pacing knobs, so a persisted breakdown whose settings are
unknown cannot be re-derived, and any downstream data keyed to its timings
(panel cues, captions) silently stops matching a later re-render. Recording
the settings next to the timings closes that hole on the no-graph fast path,
where the nw/lacing provenance layer is not in play at all.

It is always plain JSON types, so a consumer that already persists the
breakdown gets the record for free.

#### *property* duration *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Total timeline length (s) — the max beat end, accounting for overlaps.

#### *classmethod* from_dict(d)

Rebuild a breakdown from `to_dict()` output (the persisted form).

`totals` and `duration` are derived, so they are recomputed rather
than read back — the beats are the record.

* **Return type:**
  [`TimelineBreakdown`](_autosummary/braidio.timeline.html.md#braidio.timeline.TimelineBreakdown)

```pycon
>>> tl = build_timeline(kinds=["narration", "clip"], durations=[4.0, 2.0],
...                     labels=["a", "b"], source_spans=[None, (1.0, 3.0)])
>>> TimelineBreakdown.from_dict(tl.to_dict()) == tl
True
```

#### shares()

Fraction of spoken+clip time per `kind` (sums to 1).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

#### to_html(title=None, subtitle='')

A self-contained HTML view: totals bar, walking-order timeline, table.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### totals()

Seconds spent per `kind` (insertion-ordered by first appearance).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

### braidio.timeline.build_timeline(, kinds, durations, placements=None, labels=None, source_spans=None, clip_edge_overlap_s=0.5, narration_crossfade_s=0.12, title='', settings=None)

Assemble a [`TimelineBreakdown`](_autosummary/braidio.timeline.html.md#braidio.timeline.TimelineBreakdown) from per-beat render data (pure).

`kinds` are the aggregation labels (any string; `"clip"` is the only one
the layout treats specially — as an overlapping segment). `placements` is
the per-beat `"sequential"`/`"under"` used by the weave; offsets are
computed with the same [`layout_placed()`](_autosummary/braidio.weave.html.md#braidio.weave.layout_placed) the renderer uses.

`settings` is the optional render record ([`render_settings()`](_autosummary/braidio.timeline.html.md#braidio.timeline.render_settings)) — what
produced these durations. It stays optional so building a timeline by hand
(tests, a hand-cut episode, the captions/video doctests) needs nothing
extra; a real render always passes one.

* **Return type:**
  [`TimelineBreakdown`](_autosummary/braidio.timeline.html.md#braidio.timeline.TimelineBreakdown)

### braidio.timeline.render_settings(, config, crossfade_s, clip_edge_overlap_s, target_lufs, duck_db, delivery=None, profile=None, normalize=True)

The record of *how* a production was rendered, as plain JSON types.

Why it exists: since braidio#63 the pacing knobs are real, so two renders of
one script can differ by minutes — and until this, nothing on disk said which
settings produced which file. This is the cheap, no-graph answer: the
renderer hands it to the [`TimelineBreakdown`](_autosummary/braidio.timeline.html.md#braidio.timeline.TimelineBreakdown) it already returns, so a
consumer that persists the breakdown persists the settings with it.

The whole resolved [`WeaveConfig`](_autosummary/braidio.weave_config.html.md#braidio.weave_config.WeaveConfig) goes in under
`"weave"` rather than a hand-picked subset — adding a knob then keeps the
record correct for free, where a curated list would quietly drift.
`"resolved"` carries the values the render *actually used*, which are not
always the config’s: a caller may pass no config at all (`"weave"` is then
`None`, and the pacing knobs were never consulted — each beat was one TTS
call), the concat path resolves its crossfade from either the config or the
`crossfade_s` argument, and `normalize` is a render argument rather than
a weave choice.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> from braidio.delivery import V2_TUNED
>>> from braidio.weave_config import WeaveConfig
>>> rec = render_settings(
...     config=WeaveConfig(segmentation_unit="sentence", gap_turn_s=0.28),
...     crossfade_s=0.12, clip_edge_overlap_s=0.5,
...     target_lufs=-16.0, duck_db=-15.0,
...     delivery=V2_TUNED, profile="published",
... )
>>> rec["weave"]["gap_turn_s"], rec["resolved"]["paces_narration"]
(0.28, True)
>>> rec["delivery"]["model_id"], rec["profile"]
('eleven_multilingual_v2', 'published')
```


# _autosummary/braidio.tts.html.md

# braidio.tts

ElevenLabs narration synthesis.

Thin wrapper over `mixing.text_to_speech()` (the ElevenLabs entry point,
with on-disk caching) applying a voice preset: the `eleven_multilingual_v2`
quality model and a locked voice + settings so a whole production sounds like
one narrator.

Voice defaults to “George — Warm, Captivating Storyteller”; override with the
`BRAIDIO_TTS_VOICE` env var (`VOICE_ENV_VAR`) or the `voice_id` arg.

### Module Attributes

| [`DEFAULT_MODEL_ID`](_autosummary/braidio.tts.html.md#braidio.tts.DEFAULT_MODEL_ID)   | The narration model.   |
|---------------------------------------------------------------------|------------------------|

### Functions

| [`narrate`](_autosummary/braidio.tts.html.md#braidio.tts.narrate)(text, out_path, \*[, api_key, ...])   | Synthesize `text` to `out_path` (mp3).                                         |
|------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`resolve_voice_id`](_autosummary/braidio.tts.html.md#braidio.tts.resolve_voice_id)([voice_id])                  | Voice id from arg → `VOICE_ENV_VAR` env → default.                             |
| [`text_to_dialogue`](_autosummary/braidio.tts.html.md#braidio.tts.text_to_dialogue)(turns, \*[, model_id, ...])  | Synthesize a multi-speaker exchange in ONE pass (ElevenLabs Text-to-Dialogue). |

### braidio.tts.DEFAULT_MODEL_ID *= 'eleven_v3'*

The narration model. eleven_v3 rather than eleven_multilingual_v2 because
v2 cannot render inline [audio tags] AT ALL, and tag density is the main
lever on whether a read sounds alive (braidio.style.audio_tag_rate). A v2
default caps every production at the flat end before an author writes a
word. Override per-render with `delivery=`, or persist your own in
~/.config/braidio/config.json — see braidio.defaults.

### braidio.tts.narrate(text, out_path, , api_key=None, voice_id=None, model_id='eleven_v3', voice_settings=None, output_format='mp3_44100_128', refresh=False, return_cache_status=False)

Synthesize `text` to `out_path` (mp3). Returns the path.

Caching is handled by `mixing.text_to_speech` (keyed on text+voice+model);
pass `refresh=True` to regenerate.

`api_key` is an optional per-request ElevenLabs key: when given it wins
over the environment; when `None` (default) resolution falls back to
`$ELEVENLABS_API_KEY` (unchanged behavior). This is what lets a caller
thread a per-user BYO key without touching the process environment.

`return_cache_status`: when `True`, return `(path, was_cached)` where
`was_cached` is `True` iff mixing served the audio from its on-disk cache
(no ElevenLabs call = $0 real spend). Lets the caller attribute real cost
(braidio#8). Default `False` keeps the `Path` return.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`bool`](https://docs.python.org/3/builtins/functions.html#bool)]

### braidio.tts.resolve_voice_id(voice_id=None)

Voice id from arg → `VOICE_ENV_VAR` env → default.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### braidio.tts.text_to_dialogue(turns, , model_id='eleven_v3', output_format='mp3_44100_128', settings=None, seed=None, api_key=None, cache=True, refresh=False, return_cache_status=False)

Synthesize a multi-speaker exchange in ONE pass (ElevenLabs Text-to-Dialogue).

Unlike per-line [`narrate()`](_autosummary/braidio.tts.html.md#braidio.tts.narrate), this renders the whole conversation together
so prosody is conditioned across turns — the key to sounding like people
*talking to each other* rather than alternating monologues (see braidio#1 /
`docs/research/conversational-vs-narration-tts.md`). `eleven_v3` only.

**Cached** (like [`narrate()`](_autosummary/braidio.tts.html.md#braidio.tts.narrate)/`mixing.text_to_speech`): the result is
keyed on the SHA-256 of every parameter that affects the audio (turns,
voices, model, settings, seed, format). A cache hit returns the *same* bytes
instantly — no API call, deterministic master, and the basis for partial
re-render (change one exchange → only its key changes). `cache=True`
(default) uses `DIALOGUE_CACHE_ENV_KEY` / the default cache dir;
`cache=False` disables it; a path uses that dir. `refresh=True` forces a
re-render (to re-roll a v3 take). Because v3 is nondeterministic, caching a
seedless call freezes one random take — pass a `seed` for reproducibility.

* **Parameters:**
  * **turns** – ordered `(voice_id, text)` pairs (or `{"voice_id", "text"}`).
    Keep each request under ~2000 chars total (API limit).
  * **settings** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – optional model settings dict (e.g. `{"stability": 0.45}`).
  * **return_cache_status** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – when `True`, return `(audio, was_cached)` where
    `was_cached` is `True` iff the bytes came from the on-disk cache
    (no ElevenLabs call = $0 real spend) — the same attribution
    [`narrate()`](_autosummary/braidio.tts.html.md#braidio.tts.narrate) offers (braidio#8). Default `False` keeps the
    `bytes` return.
* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes), [`bool`](https://docs.python.org/3/builtins/functions.html#bool)]
* **Returns:**
  raw audio bytes in `output_format`.


# _autosummary/braidio.video.html.md

# braidio.video

Turn a rendered production into a Ken Burns film over still images.

braidio’s delegation table sends *video render* to `reelee` – and still does
for anything with generated footage, characters or shots. This module is the one
case reelee cannot serve: reelee imports braidio, so braidio cannot import
reelee. What it needs instead is a leaf dependency, and pan/zoom over stills is
exactly that: `burns` owns the motion, this module owns *where the cuts go*,
which is braidio-specific knowledge because only braidio knows where a beat ends.

**Optional layer.** Nothing here is imported by `braidio/__init__` eagerly and
no function-level dependency is imported at module scope, so `import
braidio.video` succeeds with nothing extra installed. `HAS_VIDEO` reports
whether the render path’s dependencies (`braidio[video]` -> `burns`,
`pillow`) are actually present; [`plan_spans()`](_autosummary/braidio.video.html.md#braidio.video.plan_spans) and [`assign_stills()`](_autosummary/braidio.video.html.md#braidio.video.assign_stills)
are pure and work regardless.

The seams, and what each defaults to:

Deliberately *not* seams: sentence splitting, the credits-card layout, the
panel-length bounds. They are written directly, with constants at module top.

Why the blurred fill: a pool of found stills mixes portrait and landscape, and
cover-cropping a tall image into a wide frame throws most of it away. Compositing
onto a blurred, darkened enlargement of the image itself keeps the whole picture
and never shows dead black bars.

### Functions

| [`assign_stills`](_autosummary/braidio.video.html.md#braidio.video.assign_stills)(spans, stills, \*[, zoom])         | Attach stills to `spans`, cycling so none repeats back-to-back.                     |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
| [`credits_card`](_autosummary/braidio.video.html.md#braidio.video.credits_card)(lines, dst, \*[, heading, ...])     | Render an end card listing `lines`, **fitted** so none is lost.                     |
| [`missing_dependencies`](_autosummary/braidio.video.html.md#braidio.video.missing_dependencies)()                           | Which `braidio[video]` dependencies are absent (empty when ready).                  |
| [`plan_spans`](_autosummary/braidio.video.html.md#braidio.video.plan_spans)(timeline, \*[, min_panel_s, ...])     | Cut `timeline` into contiguous spans of roughly one still each.                     |
| [`prepare_still`](_autosummary/braidio.video.html.md#braidio.video.prepare_still)(src, dst, \*[, size])              | Composite `src` onto a blurred fill of itself at exactly `size`.                    |
| [`save_atomically`](_autosummary/braidio.video.html.md#braidio.video.save_atomically)(img, dst, \*\*encode)            | `img.save(dst)` through a sibling temp file and an atomic rename.                   |
| [`render_video`](_autosummary/braidio.video.html.md#braidio.video.render_video)(panels, \*, audio_path, out_path)   | Render `panels` as one film and mux `audio_path` under it.                          |
| [`concat_argv`](_autosummary/braidio.video.html.md#braidio.video.concat_argv)(listing, \*, audio_path, ...[, ...]) | Join the pieces a concat `listing` names, under `audio_path`, as the delivered mp4. |
| [`segment_argv`](_autosummary/braidio.video.html.md#braidio.video.segment_argv)(source, in_s, frames, \*, out_path) | One cut: exactly `frames` frames of `source` from `in_s`, as a piece.               |
| [`frame_counts`](_autosummary/braidio.video.html.md#braidio.video.frame_counts)(panels, \*, fps)                    | Frames per panel, rounded on the ABSOLUTE timeline so they never drift.             |
| [`runs`](_autosummary/braidio.video.html.md#braidio.video.runs)(panels)                                     | Group consecutive panels into `("footage" | "stills", indices)` runs.               |

### Classes

| [`Footage`](_autosummary/braidio.video.html.md#braidio.video.Footage)(path[, in_s])                        | Recorded video a panel plays as it is — a straight cut, no camera move.                                     |
|-----------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
| [`Panel`](_autosummary/braidio.video.html.md#braidio.video.Panel)(start, end, still[, style, zoom, ...]) | A [`Span`](_autosummary/braidio.video.html.md#braidio.video.Span) with a still and its camera move — or with footage. |
| [`Span`](_autosummary/braidio.video.html.md#braidio.video.Span)(start, end, beat_index[, kind, label])  | A stretch of screen time, before any image is chosen for it.                                                |

### *class* braidio.video.Footage(path, in_s=0.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Recorded video a panel plays as it is — a straight cut, no camera move.

`in_s` is where in `path` the panel’s span starts; the panel’s own
duration says how much of it plays. A screen recording, a clip of a talk,
a phone video: anything ffmpeg reads.

### *class* braidio.video.Panel(start, end, still, style='push', zoom=1.18, label='', footage=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A [`Span`](_autosummary/braidio.video.html.md#braidio.video.Span) with a still and its camera move — or with footage.

With `footage` set, the panel plays that video over its span and
`still`/`style`/`zoom` are not rendered: `still` stays the panel’s
poster (what a storyboard or a picker shows for it).

### *class* braidio.video.Span(start, end, beat_index, kind='', label='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A stretch of screen time, before any image is chosen for it.

`beat_index` / `kind` / `label` carry the beat this came from, so a
caller assigning pictures can see what is being said over each span.

### braidio.video.assign_stills(spans, stills, , zoom=1.18)

Attach stills to `spans`, cycling so none repeats back-to-back.

This is the *mechanical* default. Which picture belongs over which sentence is
an authoring decision — it needs to know what is being said — so a caller that
cares builds [`Panel`](_autosummary/braidio.video.html.md#braidio.video.Panel) objects directly and uses each span’s `label`
and `beat_index` to choose. This function is what you want when the stills
are interchangeable texture.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Panel`](_autosummary/braidio.video.html.md#braidio.video.Panel)]

### Examples

```pycon
>>> spans = [Span(0, 5, 0), Span(5, 10, 1), Span(10, 15, 2)]
>>> [p.still for p in assign_stills(spans, ["a.jpg", "b.jpg"])]
['a.jpg', 'b.jpg', 'a.jpg']
>>> [p.style for p in assign_stills(spans, ["a.jpg", "b.jpg"])]
['push', 'drift', 'push']
```

### braidio.video.concat_argv(listing, , audio_path, out_path, total_frames, fps=30, ffmpeg='ffmpeg', crf=18)

Join the pieces a concat `listing` names, under `audio_path`, as the delivered mp4.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> argv = concat_argv("l.txt", audio_path="a.wav", out_path="o.mp4", total_frames=90)
>>> argv[argv.index("-t") + 1], argv[argv.index("-f") + 1]
('3.000', 'concat')
```

### braidio.video.credits_card(lines, dst, , heading='Credits', footer='', size=(1920, 1080))

Render an end card listing `lines`, **fitted** so none is lost.

Reusing a photograph under CC BY / CC BY-SA is conditional on crediting it, so
for a film built from found images this card is part of the licence
compliance, not decoration. Generate `lines` from whatever manifest recorded
the fetches, so the card cannot drift from what was actually used.

Because it is compliance, it must not *silently* drop an attribution — and a
fixed 31px leading did exactly that past roughly thirty lines: the tail ran
off the bottom of the frame and the footer drew over whatever was left. A
forty-three-image film credited twenty-eight of them and looked fine.

So the type is fitted to the content instead. Leading shrinks first, then the
list goes to two columns, and only if it still will not fit does this raise —
which is the right failure, because a caller who cannot see the problem cannot
fix it. Pass a bigger `size` or split across two cards.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### braidio.video.frame_counts(panels, , fps)

Frames per panel, rounded on the ABSOLUTE timeline so they never drift.

Rounding each duration alone would let a film of many short panels slide
off its narration by a frame per cut; rounding each boundary keeps every
cut within half a frame of where the audio says it is.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]

```pycon
>>> ps = [Panel(0, 1.01, "a"), Panel(1.01, 2.02, "b"), Panel(2.02, 3.03, "c")]
>>> frame_counts(ps, fps=30)
[30, 31, 30]
```

### braidio.video.missing_dependencies()

Which `braidio[video]` dependencies are absent (empty when ready).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### braidio.video.plan_spans(timeline, , min_panel_s=5.0, max_panel_s=9.0)

Cut `timeline` into contiguous spans of roughly one still each.

Cuts land on **beat boundaries** wherever a beat is already a comfortable
length, because that is where the narration actually changes subject. A beat
longer than `max_panel_s` is divided into equal parts (so a 30-second
passage becomes four panels rather than one stalled photograph); a beat
shorter than `min_panel_s` is merged forward into the next one rather than
producing a flicker.

The result is gapless and ordered: `spans[i].end == spans[i + 1].start`.

* **Parameters:**
  * **timeline** – a [`TimelineBreakdown`](_autosummary/braidio.timeline.html.md#braidio.timeline.TimelineBreakdown).
  * **min_panel_s** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – below this, a beat is merged into the following span.
  * **max_panel_s** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – above this, a beat is split into equal parts.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Span`](_autosummary/braidio.video.html.md#braidio.video.Span)]
* **Returns:**
  Spans covering the whole production, in playback order.

### Examples

```pycon
>>> from braidio.timeline import build_timeline
>>> tl = build_timeline(kinds=["narration"], durations=[20.0])
>>> [round(s.duration, 2) for s in plan_spans(tl)]
[6.67, 6.67, 6.67]
```

A short beat does not become its own panel:

```pycon
>>> tl = build_timeline(kinds=["narration", "narration"], durations=[2.0, 8.0])
>>> len(plan_spans(tl))
1
```

### braidio.video.prepare_still(src, dst, , size=(1920, 1080))

Composite `src` onto a blurred fill of itself at exactly `size`.

Nothing is cropped away and no dead black bar appears, whatever the source
aspect. Idempotent: an existing `dst` is returned untouched, so re-running a
build does not redo the work.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### braidio.video.render_video(panels, , audio_path, out_path, size=(1920, 1080), fps=30, workdir=None, prepare=None, path_for=None, runner=None, footage_duration=None, \*\*write_kwargs)

Render `panels` as one film and mux `audio_path` under it.

Stills get a Ken Burns move; panels with [`Footage`](_autosummary/braidio.video.html.md#braidio.video.Footage) play their video
as a straight cut. A film of stills only is one `burns.ken_burns_film`
pass rather than per-panel renders plus a concat: that avoids a re-encode
seam at every cut and a frozen frame at every panel tail. A film with
footage renders each run of consecutive stills that way (silent), cuts each
run and each footage panel into a piece of exactly its frames, one at a time
so memory stays flat ([`segment_argv()`](_autosummary/braidio.video.html.md#braidio.video.segment_argv)), and joins the pieces under the
audio in one final encode ([`concat_argv()`](_autosummary/braidio.video.html.md#braidio.video.concat_argv)).

* **Parameters:**
  * **panels** ([`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)[[`Panel`](_autosummary/braidio.video.html.md#braidio.video.Panel)]) – the stills (or footage) and their screen time, in order.
  * **audio_path** – the finished mix. Its length should match the panels; pad it
    first if the film ends on a credits card.
  * **out_path** – mp4 to write.
  * **size** ([`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)]) – frame geometry and rate.
  * **fps** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – frame geometry and rate.
  * **workdir** – where prepared canvases are cached (default: next to `out_path`).
  * **prepare** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]]) – still -> frame-sized image. Default [`prepare_still()`](_autosummary/braidio.video.html.md#braidio.video.prepare_still).
  * **path_for** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis), [`object`](https://docs.python.org/3/builtins/functions.html#object)]]) – `(image_path, index, panel) -> BurnsPath`. Default is
    `burns.content_aware_path_for`, which frames on the image’s salient
    region so a slow push stays on the subject.
  * **runner** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis), [`object`](https://docs.python.org/3/builtins/functions.html#object)]]) – runs an ffmpeg argv (default [`subprocess.run()`](https://docs.python.org/3/library/subprocess.html#subprocess.run) with
    `check=True`); only the footage path shells out.
  * **footage_duration** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`float`](https://docs.python.org/3/builtins/functions.html#float)]]) – a footage file’s length in seconds (default: ffprobe);
    an in-point at or past it is refused before anything renders.
  * **\*\*write_kwargs** – forwarded to `burns.ken_burns_film` (the still runs;
    the footage path encodes its own pieces).
* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)
* **Returns:**
  The written mp4 path.

### braidio.video.runs(panels)

Group consecutive panels into `("footage" | "stills", indices)` runs.

Each footage panel is its own run (it has its own in-point); consecutive
stills share one, rendered as a single Ken Burns pass.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]]]

```pycon
>>> P = lambda f=None: Panel(0, 1, "a.jpg", footage=f)
>>> runs([P(), P(), P(Footage("v.mp4")), P(Footage("v.mp4", 3)), P()])
[('stills', [0, 1]), ('footage', [2]), ('footage', [3]), ('stills', [4])]
```

### braidio.video.save_atomically(img, dst, \*\*encode)

`img.save(dst)` through a sibling temp file and an atomic rename.

A cached canvas is shared: the studio’s move preview and a render can both
ask for it at once, and a reader that opens a half-written JPEG would
otherwise cache the truncation under its content key for good.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### braidio.video.segment_argv(source, in_s, frames, , out_path, size=(1920, 1080), fps=30, ffmpeg='ffmpeg')

One cut: exactly `frames` frames of `source` from `in_s`, as a piece.

Seeked at the input (fast, and exact in modern ffmpeg), resampled to
`fps`, fitted inside `size` without cropping (letterboxed on black),
and — should the source run out early — held on its last frame rather than
cutting short, so the picture never slides off the narration. Pieces are
near-lossless (they are re-encoded once more, by [`concat_argv()`](_autosummary/braidio.video.html.md#braidio.video.concat_argv)).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> argv = segment_argv("rec.mp4", 2.5, 60, out_path="p.mp4", size=(1080, 1920))
>>> argv[argv.index("-ss") + 1], argv[argv.index("-t") + 1]
('2.500', '3.000')
>>> "trim=end_frame=60" in argv[argv.index("-vf") + 1]
True
```


# _autosummary/braidio.weave.html.md

# braidio.weave

Weave narration + audio clips on a timeline (#21) — the reusable mix engine.

Two generic, Hamilton-agnostic primitives:

- [`extract_padded()`](_autosummary/braidio.weave.html.md#braidio.weave.extract_padded) — extract `[start, end]` from a source asset but
  **padded** by pre/post-roll (so the words are captured cleanly) with in/out
  fades on the padded edges. The padded, faded edges are what we tuck under the
  neighbouring narration.
- [`weave_timeline()`](_autosummary/braidio.weave.html.md#braidio.weave.weave_timeline) — place ordered narration/clip parts on a timeline
  where each clip **overlaps its neighbours** by `clip_edge_overlap_s` and its
  faded padded edges duck under the speech, so **speech stays dominant**, then
  mix (`amix` sum) and loudness-normalize.

This is the audio counterpart of a video timeline; it consumes plain file paths

+ numbers, no Genius/lyrics knowledge (that resolution lives in
  `graph/align.py` and the Hamilton adapters). Ducking here is achieved by
  fade-shaped overlap; a dynamic sidechain duck (`duck_db`) is a documented
  refinement (see #21 / the research).

### Functions

| `duration_s`(path)                                                                               |                                                                  |
|--------------------------------------------------------------------------------------------------|------------------------------------------------------------------|
| [`extract_padded`](_autosummary/braidio.weave.html.md#braidio.weave.extract_padded)(asset_path, start_s, end_s, ...) | Extract `[start_s-pre_roll, end_s+post_roll]` with in/out fades. |
| [`layout_placed`](_autosummary/braidio.weave.html.md#braidio.weave.layout_placed)(kinds, durs, placements, \*, ...) | Start offset (s) of each part, placement-aware.                  |
| [`layout_starts`](_autosummary/braidio.weave.html.md#braidio.weave.layout_starts)(kinds, durs, \*, ...)             | Start offset (s) of each part (all sequential).                  |
| [`weave_timeline`](_autosummary/braidio.weave.html.md#braidio.weave.weave_timeline)(items, out_path, \*[, ...])      | Place items on a timeline and mix.                               |

### Classes

| [`TimelineItem`](_autosummary/braidio.weave.html.md#braidio.weave.TimelineItem)(kind, path[, placement, ...])   | One part on the weave timeline.   |
|-----------------------------------------------------------------------------------------------|-----------------------------------|

### *class* braidio.weave.TimelineItem(kind, path, placement='sequential', duck_db=0.0, spotlight=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One part on the weave timeline.

`placement` `"sequential"` (default — narration, and clean `before` /
`after` clips) lays the part in its own slot. `"under"` overlays the part
beneath the *following* sequential part (it does not consume its own slot),
attenuated by `duck_db` — a ducked underlay (a clip talked over, or later a
music bed).

`spotlight` marks the part the music bed drops out for (fade-to-spotlight):
the bed is silent over this part’s span and resumes after it. Inert without
a bed.

### braidio.weave.extract_padded(asset_path, start_s, end_s, out_path, , pre_roll_s=0.4, post_roll_s=0.3, fade_in_s=0.5, fade_out_s=0.8, min_len_s=2.2)

Extract `[start_s-pre_roll, end_s+post_roll]` with in/out fades.

The target words sit in the middle; the padded, faded head/tail are the
parts that overlap (tuck under) neighbouring narration in the weave.

Two guards keep **short** clips from sounding like they just swell in and
out (no steady body):

- the tail is extended so the clip is at least `min_len_s` long, giving the
  fade somewhere to breathe;
- the fades are **adaptive** — capped to a fraction of the clip so they never
  swallow it (a 1.5 s clip gets ~0.3 s fades, not 0.5 s + 0.8 s).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### braidio.weave.layout_placed(kinds, durs, placements, , clip_edge_overlap_s, narration_crossfade_s)

Start offset (s) of each part, placement-aware. Pure function.

Sequential parts advance a running cursor: a clip (or a part following a clip)
starts `clip_edge_overlap_s` before the cursor so its faded edges tuck under
the neighbour; narration-after-narration overlaps by `narration_crossfade_s`.
An `"under"` part starts *at* the cursor (concurrent with the next
sequential part) and does **not** advance it — so it overlays what follows.
Clamped ≥ 0.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

### braidio.weave.layout_starts(kinds, durs, , clip_edge_overlap_s, narration_crossfade_s)

Start offset (s) of each part (all sequential). Thin wrapper over
[`layout_placed()`](_autosummary/braidio.weave.html.md#braidio.weave.layout_placed) — kept for callers that don’t use placement.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

### braidio.weave.weave_timeline(items, out_path, , clip_edge_overlap_s=0.5, narration_crossfade_s=0.12, target_lufs=-16.0, true_peak=-1.0, sample_rate=44100, bed=None)

Place items on a timeline and mix. Clips overlap neighbours by
`clip_edge_overlap_s` (their faded edges tuck under narration); narration
parts butt-join with a small crossfade. Returns `out_path`.

`bed` (a [`MusicBed`](_autosummary/braidio.music.html.md#braidio.music.MusicBed)) lays an instrumental underscore
under the whole production: it’s rendered to cover the timeline, attenuated,
and mixed in posted by `bed.lead_in_s`. Items marked `spotlight` open a
gap in it (the bed is rendered as the regions around them — see
[`braidio.music.bed_regions()`](_autosummary/braidio.music.html.md#braidio.music.bed_regions)); with none marked the bed is the single
whole-span file. Falls back to a plain concat feel when
`clip_edge_overlap_s == 0` and there’s nothing to overlay.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/braidio.weave_config.html.md

# braidio.weave_config

WeaveConfig — every editing choice for weaving narration + segments (#20).

One frozen config object that exposes **all** the knobs (casting, turns, pacing,
timing, clip weaving, loudness) so a composer picks combinations rather than
inheriting one hardcoded style (the “enable all choices” mandate of #18/#28).

Defaults come from `docs/research/podcast-audio-weaving-editing.md`. This
module is deliberately **Hamilton-agnostic** — no Genius/LRCLIB/episode imports —
so it can move to the reusable weave package unchanged (#19). It references the
generic voice pools/ids only.

The config is designed to serialize cleanly ([`WeaveConfig.to_dict()`](_autosummary/braidio.weave_config.html.md#braidio.weave_config.WeaveConfig.to_dict)) into a
`render-config` provenance node (#26), so a render is reproducible from its
recorded choices.

### Classes

| [`WeaveConfig`](_autosummary/braidio.weave_config.html.md#braidio.weave_config.WeaveConfig)([voices, pool_label, ...])   | All editing choices for a narration+segment weave.   |
|-------------------------------------------------------------------------------------------|------------------------------------------------------|

### *class* braidio.weave_config.WeaveConfig(voices=('JBFqnCBsd6RMkjVDRZzb', ), pool_label='single', voice_seed=7, avoid_immediate_repeat=True, model_id='eleven_multilingual_v2', voice_settings=<factory>, segmentation_unit='beat', min_turn=2, max_turn=4, speed_base=1.0, speed_jitter=0.04, crossfade_s=0.12, gap_turn_s=0.0, overlap_turn_s=0.0, clip_pre_roll_s=0.4, clip_post_roll_s=0.3, clip_fade_in_s=0.5, clip_fade_out_s=0.8, clip_min_len_s=2.2, clip_edge_overlap_s=0.5, duck_db=-15.0, target_lufs=-16.0, true_peak_dbtp=-1.0, sample_rate=44100)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

All editing choices for a narration+segment weave. Frozen + serializable.

#### *property* paces_narration *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether a render cuts each narration beat into separately-spoken turns.

`False` (the default, `segmentation_unit="beat"`) is one TTS call per
beat — no intra-beat gaps, no per-turn speed, so `gap_turn_s` and
`speed_jitter` do nothing on the [`braidio.render.render_production()`](_autosummary/braidio.render.html.md#braidio.render.render_production)
path. Opt in by setting `segmentation_unit` to a smaller unit.

```pycon
>>> WeaveConfig().paces_narration
False
>>> WeaveConfig(segmentation_unit="sentence").paces_narration
True
```

#### to_dict()

Stable serialization for a `render-config` provenance node (#26).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

#### with_(\*\*changes)

Return a copy with fields overridden (e.g. `cfg.with_(min_turn=1)`).

* **Return type:**
  [`WeaveConfig`](_autosummary/braidio.weave_config.html.md#braidio.weave_config.WeaveConfig)


# about-this-build.html.md

<!-- generated by epythet -->

# About this build

This documentation was built on **2026-10-14 12:40 UTC** from commit <a href="https://github.com/reeleehq/braidio/commit/40cdebd5e60f289ef95f776d752fdd156195f5a6"><code>40cdebd</code></a> on branch <code>main</code>, for **braidio 0.0.71** (from <code>pyproject.toml</code>).

#### NOTE
Nothing suggests a mismatch: the tree was clean at the commit above, and the documented version is the one on PyPI.

## Source

|                     |                                                                                                                                                         |
|---------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
| Commit              | <a href="https://github.com/reeleehq/braidio/commit/40cdebd5e60f289ef95f776d752fdd156195f5a6"><code>40cdebd5e60f289ef95f776d752fdd156195f5a6</code></a> |
| Branch              | <code>main</code>                                                                                                                                       |
| Tags at this commit | <code>0.0.71</code>                                                                                                                                     |
| Working tree        | clean                                                                                                                                                   |
| Remote              | <code>https://github.com/reeleehq/braidio</code>                                                                                                        |

## Continuous integration

|              |                                                                                            |
|--------------|--------------------------------------------------------------------------------------------|
| Repository   | <code>reeleehq/braidio</code>                                                              |
| Run          | <a href="https://github.com/reeleehq/braidio/actions/runs/38565038035">38565038035</a>     |
| Ref          | <code>refs/heads/main</code>                                                               |
| Event commit | <code>441f4a7ea081faeecbde16ea44a52280c0938ceb</code> (in the history of the built commit) |

## Tools

|          |         |
|----------|---------|
| epythet  | 0.2.17  |
| Sphinx   | 9.1.0   |
| docutils | 0.22.4  |
| Python   | 3.12.15 |

## Configuration as resolved

|               |                                                                   |
|---------------|-------------------------------------------------------------------|
| theme         | <code>auto</code> (Sphinx theme <code>pydata_sphinx_theme</code>) |
| accent        | <code>#006e5b</code>                                              |
| api_generator | <code>autosummary</code>                                          |
| ignore        | <code>tests/</code>, <code>scrap/</code>, <code>examples/</code>  |
| agent_outputs | <code>true</code>                                                 |
| aggregates    | <code>md</code>                                                   |
| ai_artifacts  | <code>true</code>                                                 |

## Package on PyPI

Latest release: <a href="https://pypi.org/project/braidio/0.0.71/">0.0.71</a>, the same as the documented version.

## Reproduce

```bash
git clone https://github.com/reeleehq/braidio && cd braidio
git checkout 40cdebd5e60f289ef95f776d752fdd156195f5a6
pip install "epythet==0.2.17"
epythet quickstart . --ignore tests/ scrap/ examples/
```

The same data, for machines: <a href="build_info.json"><code>build_info.json</code></a> (schema version 1).


# ai-agents.html.md

<!-- generated by epythet -->

# For AI agents

`braidio` ships artifacts for coding agents alongside its code. This page lists
them, says where each lives in the repository, and points at the
machine-readable copies of this documentation.

## Skills

Skills are folders holding a `SKILL.md` (the [Agent Skills](https://agentskills.io) format): a description that tells an agent when to use it and a body with the procedure. Install one into your agent with `gh skill` (any host: `--agent claude-code`, `copilot`, `cursor`, `codex`, `gemini`), or use the copy bundled in the wheel.

### `braidio`

Use `braidio` to produce narrated commentary audio — a podcast-style episode that talks *about* something (a song, a book, a film, a paper, an event) and weaves in clips of the source. Trigger on requests like “make a podcast-style commentary”, “turn this document/article/paper into an episode”, “narrate over these clips”, “two hosts discussing this”, “duo commentary track”, “panel discussion audio”, “roundtable about this”, “debate two sides of this”, “audio essay”, “documentary voice-over”, “Song Exploder style breakdown”, “interview-style audio”, “deep dive on this track”, “add a narrator over this song”, “voice this script with two speakers”, “commentary with clips of the original”. Covers: authoring the Script of beats, picking a ready-made format template (solo / deep-dive / interview / panel / debate / documentary VO), estimating the ElevenLabs cost before paying, pulling source audio into an asset library, and rendering the mixed episode. For plain audio/video editing (trim, fade, concat, transcribe, dub) use `mixing` instead; braidio is the layer that *composes a production*.

```bash
gh skill install thorwhalen/braidio braidio --agent claude-code
```

Source: [`braidio/data/skills/braidio`](https://github.com/thorwhalen/braidio/tree/HEAD/braidio/data/skills/braidio) (bundled with the pip package).

### `braidio-commentary-video`

Turn a braidio commentary episode into a watchable video — a Ken Burns pan/zoom film over still images, cut to the narration, optionally captioned and published. Use when someone wants a commentary/analysis *video* rather than audio: “make a video essay about this song”, “turn this episode into a video”, “Ken Burns video over these images”, “video version of the podcast”, “publish this commentary to YouTube”, “add visuals to the narration”, “slideshow video with narration”, “music video essay”. Covers picking the format and fixing the robotic-TTS defaults, planning panels from the render’s own timeline, sourcing freely-licensed stills, rendering with `burns`, building captions without ASR, and publishing privately with `yb`. For audio only, use the `braidio` skill; for generated footage, characters or shots, use `reelee`.

```bash
gh skill install thorwhalen/braidio braidio-commentary-video --agent claude-code
```

Source: [`braidio/data/skills/braidio-commentary-video`](https://github.com/thorwhalen/braidio/tree/HEAD/braidio/data/skills/braidio-commentary-video) (bundled with the pip package).

The bundled skills are also on disk after `pip install braidio`, under the package’s `data/skills/` directory; link them into an agent without network access with `skill link-skills <that directory>`.

## Subagents

Subagents are Markdown files with a frontmatter (`name`, `description`, `tools`) and a system prompt as the body. Copy one into your project’s `.claude/agents/` (or your agent host’s equivalent) to delegate the task it describes.

### `commentary-video`

Build a commentary video end to end from a subject — research it, author the script, render the audio with braidio, source freely-licensed stills, cut them into a Ken Burns film, caption it, and hand back a file (publishing only if asked). Use when someone names a target and wants the finished thing: “make a video essay about <song/book/film/paper>”, “do the Actually Romantic treatment for <X>”, “commentary video on <topic>”. Returns the artifact paths, the sources it used, and anything it could not source. Does not publish without an explicit instruction to.

Source: [`.claude/agents/commentary-video.md`](https://github.com/thorwhalen/braidio/tree/HEAD/.claude/agents/commentary-video.md); tools: `Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch, Skill`.

## Instruction files

Files agents read before working in this repository.

- [`.claude/CLAUDE.md`](https://github.com/thorwhalen/braidio/tree/HEAD/.claude/CLAUDE.md): read by Claude Code

## Machine-readable documentation

This site publishes the same documentation in forms that fit an agent’s context window:

- [`llms.txt`](https://thorwhalen.github.io/braidio/llms.txt): an index of every page with a one-line description ([llms.txt](https://llmstxt.org) format)
- [`braidio.md`](https://thorwhalen.github.io/braidio/braidio.md): the whole documentation as one Markdown file
- `<page>.html.md`: a rendered Markdown twin of every page, advertised from each page’s `<head>` with `<link rel="alternate" type="text/markdown">`
- [`objects.inv`](https://thorwhalen.github.io/braidio/objects.inv): the Sphinx inventory: a symbol-to-URL index (`sphobjinv convert plain objects.inv -`)


# api.html.md

# API reference

| [`braidio`](_autosummary/braidio.html.md#module-braidio)   | braidio — weave narration with extracted media segments into productions.   |
|---------------------------------------------------------------------------|-----------------------------------------------------------------------------|


