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; see that project + issues #18/#28.
- class braidio.Delivery(name: str, model_id: str, voice_settings: dict = <factory>, supports_audio_tags: bool = False, note: str = '')[source]
A named narration delivery: which model + voice settings to synthesize with.
- class braidio.Dialogue(turns: tuple[tuple[str, str], ...], label: str = '')[source]
A multi-speaker commentary exchange (the conversational register).
turnsis an ordered tuple of(role, text)pairs (roles like"A"/"B"map to voices via aConversationCastat 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.Format(id: str, name: str, summary: str, aka: tuple[str, ...]=(), cast: ConversationCast | None = None, narration_voice: str | None = None, narration_delivery: 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, note='Host/presenter commentary — lively (== v2-tuned), for the spine voice.'), weave: WeaveConfig = <factory>, roles: Mapping[str, str]=<factory>, clip_placement: str = 'before', music_bed: str = 'light', scripting: str = '')[source]
A named commentary-format preset: a bundle of high-quality defaults.
Rendered fields drive
render_format()→braidio.render.render_production(). Authoring fields document how to write aScriptfor this format (and, forclip_placement/music_bed, flag render features still on the roadmap).- render(script, *, source, out_path: str | Path | None = None, profile: Profile = Profile.PERSONAL, **overrides) Path[source]
Render
scriptwith this format’s defaults (seerender_format()).
- class braidio.MusicBed(asset_path: str, gain_db: float = -22.0, fade_in_s: float = 1.5, fade_out_s: float = 2.0, lead_in_s: float = 2.0, start_s: float = 0.0, loop: bool = True)[source]
An instrumental underscore spanning the production, mixed under the talk.
asset_pathis an app-supplied instrumental.gain_dbsets how far under the voice it sits;lead_in_sposts the bed after speech onset so its entrance feels motivated;start_sis the in-point into the asset;looprepeats the asset to cover a timeline longer than it.
- class braidio.Narration(text: str, style: str | None = None, published_text: str | None = None, lead_gap_s: float = 0.0, voice: str | None = None, voice_settings: dict | None = None)[source]
A spoken narration beat (authored, synthesized by TTS).
published_textis an optional rights-safe rewrite the published profile uses when the defaulttextquotes forbidden (e.g. copyrighted) content; leave itNonewhen the narration is already clean.lead_gap_sprepends 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.voiceandvoice_settingsare 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’svoice_settings). Both fall back to the render’s production-level defaults whenNone.
- class braidio.PlannedBeat(kind: str, content: str, from_index: int, note: str = '', turns: tuple | None = None)[source]
A beat resolved for a profile — what the renderer actually plays.
kindis"narration"(synthesizecontent) or"clip"(resolvecontentas a segment reference and cut audio).from_indexpoints at the source beat;noterecords any substitution/drop reasoning.
- class braidio.RenderPlan(profile: 'Profile', beats: 'list[PlannedBeat]' = <factory>, dropped: 'list[str]' = <factory>, substituted: 'list[str]' = <factory>)[source]
- class braidio.ResolvedSegment(asset_path: Path, start_s: float, end_s: float, score: float = 1.0, matched_text: str = '')[source]
A cuttable span of a source asset — what a
SegmentSourcereturns.
- class braidio.RightsPolicy(forbidden_texts: ~typing.Callable[[~braidio.script.Script], ~typing.Iterable[str]] = <function RightsPolicy.<lambda>>, publishable_clip_rights: frozenset[str] = frozenset({'public-domain'}))[source]
Injected rights configuration for the published profile.
forbidden_textsyields the strings that must not appear verbatim in published narration (e.g. copyrighted lyric lines).publishable_clip_rightsis the set of segmentrightsvalues allowed in the published cut.
- class braidio.Script(title: str, id_slug: str, beats: list[Narration | SegmentBeat | Dialogue] = <factory>)[source]
An ordered production script.
- class braidio.Segment(start_s: float, end_s: float, score: float, line_start: int, line_end: int, matched_text: str)[source]
A resolved window for a reference, with the matched line span.
- class braidio.SegmentBeat(reference: str, label: str = '', rights: str = 'owned-local', published_substitute: str | None = None, placement: str = 'before')[source]
A span of source media to weave in, addressed by an opaque
reference.The renderer resolves
reference→[start,end)via aSegmentSourceand cuts it.rights(e.g.owned-local/copyrighted/public-domain) drives the render profile;published_substituteis a transformative narration the published profile swaps in when the segment’s audio can’t be used.placementis 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 byduck_db— the “host talks over the clip” technique (place the clip immediately before the talk it should sit under).
- class braidio.SegmentSource(*args, **kwargs)[source]
Resolve an opaque
referenceto aResolvedSegment(or None).
- class braidio.TimedLine(index: int, start_s: float, end_s: float | None, text: str)[source]
A source line with a
[start_s, end_s)window (end may be None = tail).
- class braidio.TimedLineSegmentSource(*, lines: list[TimedLine], asset_path: str | Path, song_end_s: float | None = None, min_score: float = 0.5)[source]
A
SegmentSourceover time-aligned lines + one source asset.Binds the generic
find_segment()matcher to a concrete asset so the weave engine canresolve(reference) -> ResolvedSegment.
- class braidio.TimelineItem(kind: str, path: str, placement: str = 'sequential', duck_db: float = 0.0)[source]
One part on the weave timeline.
placement"sequential"(default — narration, and cleanbefore/afterclips) lays the part in its own slot."under"overlays the part beneath the following sequential part (it does not consume its own slot), attenuated byduck_db— a ducked underlay (a clip talked over, or later a music bed).
- class braidio.Voice(id: str, name: str, gender: str, accent: str = '', note: str = '')[source]
A pooled narration voice.
- class braidio.WeaveConfig(voices: tuple[str, ...]=('JBFqnCBsd6RMkjVDRZzb', ), pool_label: str = 'single', voice_seed: int = 7, avoid_immediate_repeat: bool = True, model_id: str = 'eleven_multilingual_v2', voice_settings: Mapping[str, ~typing.Any]=<factory>, segmentation_unit: str = 'sentence', min_turn: int = 2, max_turn: int = 4, speed_base: float = 1.0, speed_jitter: float = 0.04, crossfade_s: float = 0.12, gap_turn_s: float = 0.0, overlap_turn_s: float = 0.0, clip_pre_roll_s: float = 0.4, clip_post_roll_s: float = 0.3, clip_fade_in_s: float = 0.5, clip_fade_out_s: float = 0.8, clip_edge_overlap_s: float = 0.5, duck_db: float = -15.0, target_lufs: float = -16.0, true_peak_dbtp: float = -1.0, sample_rate: int = 44100)[source]
All editing choices for a narration+segment weave. Frozen + serializable.
- with_(**changes: Any) WeaveConfig[source]
Return a copy with fields overridden (e.g.
cfg.with_(min_turn=1)).
- class braidio.WeaveKind(value)[source]
A braidio production kind.
COMMENTARY_WEAVE= narration woven with extracted media segments (audio now, video later).
- braidio.assign_voices(n: int, pool: list[Voice], *, seed: int = 0, avoid_repeats: bool = True) list[Voice][source]
Assign a voice to each of
nturns — random, seeded, no immediate repeats (so it isn’t a rigid round-robin).
- braidio.bed_for_intensity(asset_path: str, intensity: str, **overrides) MusicBed | None[source]
Build a
MusicBedat the gain for a Formatmusic_bedintensity.Returns
Nonefor"none"(or an unknown intensity), so callers can dobed = bed_for_intensity(asset, fmt.music_bed)and skip when falsy.
- braidio.compose_narration(segments: list[str], config: WeaveConfig, *, out_path: str | Path, api_key: str | None = None, work_dir: str | Path = 'data/tts/compose') list[tuple[Voice, str]][source]
Render
segmentsunderconfig→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_keyis an optional per-request ElevenLabs key threaded tobraidio.multivoice.render_multivoice()(and thence every synthesized turn);None(default) keeps the$ELEVENLABS_API_KEYfallback.
- braidio.content_violations(plan: RenderPlan, forbidden: Iterable[str], *, min_words: int = 5) list[str][source]
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.
- braidio.cut_quote(audio_path: str | Path, lines: list[TimedLine], quote: str, out_path: str | Path, *, pad_pre_s: float = 0.15, pad_post_s: float = 0.35, fade_s: float = 0.04, min_score: float = 0.5, song_end_s: float | None = None) Segment[source]
Resolve
quote→ segment and cut it fromaudio_path(pad + fades).Convenience combining
find_segment()+ an ffmpeg cut. Returns the resolvedSegment(raisesLookupErrorif unmatched). New code should prefer aSegmentSource+braidio.weave.extract_padded().
- braidio.extract_padded(asset_path: str | Path, start_s: float, end_s: float, out_path: str | Path, *, pre_roll_s: float = 0.4, post_roll_s: float = 0.3, fade_in_s: float = 0.5, fade_out_s: float = 0.8) Path[source]
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.
- braidio.find_segment(lines: list[TimedLine], quote: str, *, max_span: int = 12, min_score: float = 0.5, song_end_s: float | None = None) Segment | None[source]
Best contiguous run of timed lines matching
quote, orNone.Scores every run
lines[i..j](up tomax_spanlines) by token F1 against the reference’s tokens and returns the highest-scoring run clearingmin_score. Handles single-line, sub-line, and multi-line references.
- braidio.find_verbatim_text(text: str, forbidden: Iterable[str], *, min_words: int = 5) list[str][source]
Forbidden lines that appear (near-)verbatim in
text.A line counts as leaked if
textshares a run ofmin_wordsconsecutive words with it (case-insensitive, word-level). Single words and short common phrases don’t trip it — only substantial verbatim quoting.
- braidio.group_turns(segments: list[str], *, min_turn: int = 1, max_turn: int = 1, seed: int = 0) list[str][source]
Group consecutive segments into turns of
min_turn..max_turnsegments.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.
- braidio.layout_starts(kinds: list[str], durs: list[float], *, clip_edge_overlap_s: float, narration_crossfade_s: float) list[float][source]
Start offset (s) of each part (all sequential). Thin wrapper over
layout_placed()— kept for callers that don’t use placement.
- braidio.load_timing(path: str | Path) list[TimedLine][source]
Load a
{lines: [{index,start_s,end_s,text}]}JSON into `TimedLine`s.
- braidio.narrate(text: str, out_path: str | Path, *, api_key: str | None = None, voice_id: str | None = None, model_id: str = 'eleven_multilingual_v2', voice_settings: dict | None = None, output_format: str = 'mp3_44100_128', refresh: bool = False) Path[source]
Synthesize
texttoout_path(mp3). Returns the path.Caching is handled by
mixing.text_to_speech(keyed on text+voice+model); passrefresh=Trueto regenerate.api_keyis an optional per-request ElevenLabs key: when given it wins over the environment; whenNone(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.
- braidio.narration_segments(script: Script) list[str][source]
All narration (default text) of a script, as sentence-level segments.
- braidio.plan_production(script: Script, profile: Profile, *, publishable_clip_rights: frozenset[str] = frozenset({'public-domain'})) RenderPlan[source]
Filter
scriptinto the beats renderable underprofile.
- braidio.render_format(fmt: Format, script, *, source, out_path: str | Path | None = None, profile: Profile = Profile.PERSONAL, bed_asset: str | None = None, **overrides) Path[source]
Render
scriptunderfmt’s defaults;overrideswin over them.Wires the format’s
cast/narration_voice/narration_delivery/weaveintobraidio.render.render_production(). Any beat may still override voice/settings per-beat (e.g. a graver book-narrator inside an otherwise lively presenter piece — passV2_NARRATOR.voice_settingson thatNarrationbeat).bed_asset(a path to an app-supplied instrumental) adds a music bed at the gain implied byfmt.music_bed(skipped when the format’s intensity is"none"); passmusic_bed=MusicBed(...)inoverridesfor full control.
- braidio.render_multivoice(segments: list[str], pool: list[Voice], *, out_path: str | Path, api_key: str | None = None, work_dir: str | Path = 'data/tts/multivoice', seed: int = 7, min_turn: int = 2, max_turn: int = 4, avoid_immediate_repeat: bool = True, model_id: str = 'eleven_multilingual_v2', base_settings: dict | None = None, speed_base: float = 1.0, speed_jitter: float = 0.04, crossfade_s: float = 0.1, gap_s: float = 0.0, target_lufs: float = -16.0) list[tuple[Voice, str]][source]
Render
segmentscyclingpool→out_path.Segments are first grouped into turns of
min_turn..max_turnsegments (bigger = each voice talks longer). One voice per turn, no immediate repeat, with a jittered speed even within a speaker.gap_sinserts silence between turns (0 = none). Returns[(voice, turn_text), …]for reporting.api_keyis an optional per-request ElevenLabs key threaded to everybraidio.tts.narrate()call;None(default) keeps the$ELEVENLABS_API_KEYfallback.NOTE: overlapping/interrupting speakers and clip ducking are separate, upcoming parameters (tracked as issues) — this renders turns sequentially.
- braidio.render_production(script: Script, *, source: SegmentSource, api_key: str | None = None, config: WeaveConfig | None = None, profile: Profile = Profile.PERSONAL, rights: RightsPolicy | None = None, delivery: Delivery = Delivery(name='v2-tuned', 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, note='★ recommended: lower stability + raised style; pairs with annotated text.'), cast: ConversationCast = ConversationCast(roles={'A': 'cgSgspJ2msm6clMCkdW9', 'B': 'iP95p4xoKVk53GoZ742B'}, model_id='eleven_v3', settings={'stability': 0.45}), out_path: str | Path | None = None, voice_id: str | None = None, crossfade_s: float = 0.12, normalize: bool = True, music_bed=None, tts_dir: str | Path = 'data/tts', clips_dir: str | Path = 'data/clips', episodes_dir: str | Path = 'data/episodes') Path[source]
Render
scriptunderprofile→ a single audio file. Returns the path.Segment beats are resolved through
source(aSegmentSource). Whenconfighasclip_edge_overlap_s > 0, a clip isplacement="under", or amusic_bedis given, the parts are woven on a timeline; otherwise they are concatenated.rights(if given) sets which segment rights are publishable.music_bedlays an instrumental underscore under the whole production (seebraidio.music.MusicBed).api_keyis an optional per-request ElevenLabs key threaded to every synthesized beat — both narration (braidio.tts.narrate()) and dialogue (braidio.conversation.render_dialogue()). WhenNone(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.
- braidio.resolve_voice_id(voice_id: str | None = None) str[source]
Voice id from arg →
VOICE_ENV_VARenv → default.
- braidio.split_segments(text: str) list[str][source]
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.
- braidio.weave_timeline(items: list[TimelineItem], out_path: str | Path, *, clip_edge_overlap_s: float = 0.5, narration_crossfade_s: float = 0.12, target_lufs: float = -16.0, true_peak: float = -1.0, sample_rate: int = 44100, bed=None) Path[source]
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. Returnsout_path.bed(aMusicBed) lays an instrumental underscore under the whole production: it’s rendered to cover the timeline, attenuated, and mixed in posted bybed.lead_in_s. Falls back to a plain concat feel whenclip_edge_overlap_s == 0and there’s nothing to overlay.