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:
- 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.
- Full parametrization underneath, so any style is expressible: a
Scriptof beats, cast via aConversationCast, tuned byWeaveConfig+Delivery, with per-beat voice/delivery overrides.
braidiowas extracted from the Hamilton lyrics-podcast (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 byreelee. 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:
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,
braidio#39,
braidio#46 and
braidio#51.
Parametrize anything
The templates are just good defaults over the primitives — compose directly for full control:
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 |
| Project workflow, provenance, plan/execute, partial re-render | nw |
| Video render + visual support | reelee — except the stills case below |
| Pan/zoom motion over a still | burns |
| Raw audio DSP + TTS (crop, concat, duck, loudnorm, synth) | 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.
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.
Path("ep.srt").write_text(braidio.captions_for(script, timeline, max_chars=42))
braidio.captions is part of the core (pure, no extra dependencies);
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 |
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
pip install braidio
ElevenLabs credentials are needed for TTS; ffmpeg must be on PATH for
everything else.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file braidio-0.0.40.tar.gz.
File metadata
- Download URL: braidio-0.0.40.tar.gz
- Upload date:
- Size: 320.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
31ce339afc70aff5912f2636e5d1467432f9465ac00f7452b310bd7b8b48cbb6
|
|
| MD5 |
455bc0eaaad7ee124c8e7db532fa0ae2
|
|
| BLAKE2b-256 |
1e7f388f7f76f20c6763e36b1bdd5418820f172607e78a9bfb6459198aa245b1
|
File details
Details for the file braidio-0.0.40-py3-none-any.whl.
File metadata
- Download URL: braidio-0.0.40-py3-none-any.whl
- Upload date:
- Size: 170.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0ea95e80f204941f583409b53c7620c2455faae8f10d2fcb759790517967b6c3
|
|
| MD5 |
8f3c5f7afa473f4b53857b800bc78fc1
|
|
| BLAKE2b-256 |
a17b6842cb3835c68f56e5f80713e3d8a4de3167a8d73abd59e6d982d0c98c54
|