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.
- class braidio.BeatSpan(index: int, kind: str, label: str = '', source_start: float | None = None, source_end: float | None = None, duration: float = 0.0, start: float = 0.0)[source]
One beat’s place on the timeline.
- class braidio.CostLine(label: str, kind: str, characters: int, usd: float | None, model_id: str)[source]
One billable line of an estimate (a narration beat or a dialogue beat).
Named to echo
falaw.CostLine(a line within a rollup) rather thanfalaw.CostEstimate(which means a per-call price spec) — so the vocabulary is consistent across the federation.
- class braidio.CostRollup(characters: int, usd: float | None, unpriced: bool, lines: tuple[CostLine, ...] = ())[source]
A production’s estimated TTS spend, exact on characters, honest on dollars.
usdis the sum of the priced lines;unpricedisTruewhen some billable text had no configured rate (sousdis a lower bound). No billable lines (e.g. an all-segment script) givescharacters=0, usd=0.0, unpriced=False. NamedCostRollupto matchfalaw.CostRollup.- property summary: str
One-line human summary (handy for a CLI/MCP preview).
- class braidio.Cue(start_s: float, end_s: float, text: str)[source]
One subtitle:
[start_s, end_s)and the text shown.
- 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>, structure: MusicStructure = <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;clip_placementis the recommended per-beat default andmusic_bedthe bed intensity applied when abed_assetis supplied.- 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, spotlight_fade_s: float = 0.6)[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.spotlight_fade_sis how fast the bed drops out before a spotlit exhibit (it resumes after the exhibit overfade_in_s).
- class braidio.MusicStructure(sting: Sting | None = None, scene_marker: str = 'sting', spotlight_clips: bool = False, pause_s: float = 0.8)[source]
How a production marks its structure with music — the render seam.
scene_markeris the default for aSceneBreakwhosemarkerisNone("sting"/"none").spotlight_clipsis the default for aSegmentBeatwhosespotlightisNone—Truedrops the bed under every exhibit.stingis the production’s sting asset; with none supplied a"sting"-marked break falls back topause_sof 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: SceneBreak) str[source]
The marker a scene break renders with (its override, else the default).
- marker_of(marker: str | None) str[source]
The marker a break with this per-beat override renders with.
- plays_sting(beat: SceneBreak) bool[source]
Whether
beatplays the sting (marked"sting"and one is supplied).
- plays_sting_of(marker: str | None) bool[source]
Whether this per-beat marker plays the sting (and one is supplied).
- spotlight_for(beat: SegmentBeat) bool[source]
Whether
beatis spotlit (its override, else the format default).
- 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),"clip"(resolvecontentas a segment reference and cut audio),"dialogue"(synthesizeturns) or"scene_break"(a structural boundary — no content; the renderer marks it with music).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.
- exception braidio.RightsViolation[source]
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
ValueErrorsubclass 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: str = '', marker: str | None = None)[source]
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
markeris"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.markeroverrides the format default per break (None= defer tobraidio.structure.MusicStructure.scene_marker).labelnames 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: str, id_slug: str, beats: list[Narration | SegmentBeat | Dialogue | SceneBreak] = <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', spotlight: bool | None = None)[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).spotlightis the fade-to-spotlight override:Truedrops the music bed out before this clip so it lands in silence rather than competing with underscore (the bed resumes after it);Falsekeeps the bed under it.None(default) defers to the format’sbraidio.structure.MusicStructure.spotlight_clips. Without a music bed the flag is inert.
- class braidio.SegmentSource(*args, **kwargs)[source]
Resolve an opaque
referenceto aResolvedSegment(or None).
- class braidio.Sting(asset_path: str, gain_db: float = -6.0, max_len_s: float = 3.0, fade_out_s: float = 0.5, gap_after_s: float = 0.3)[source]
A short musical marker played at a scene break.
asset_pathis an app-supplied sound (a hit, a riser, a few notes of the theme). It is trimmed tomax_len_swith afade_out_stail, levelled to the production’s loudness target, satgain_dbunder the voice, and followed bygap_after_sof breathing room before the talk resumes.
- 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.TimelineBreakdown(beats: tuple[~braidio.timeline.BeatSpan, ...]=<factory>, title: str = '')[source]
The ordered beats of a production, with per-kind totals and an HTML view.
- property duration: float
Total timeline length (s) — the max beat end, accounting for overlaps.
Fraction of spoken+clip time per
kind(sums to 1).
- class braidio.TimelineItem(kind: str, path: str, placement: str = 'sequential', duck_db: float = 0.0, spotlight: bool = False)[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).spotlightmarks 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: 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_min_len_s: float = 2.2, 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(*values)[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.audit_platitudes(text: str) list[Finding][source]
Return every
Findingintext, in document order.
- 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.billable_chars(text: str | None) int[source]
Characters ElevenLabs bills for
text(the whole submitted string).ElevenLabs charges for everything sent — including
eleven_v3audio tags like[excited]— so this is justlen(text). A named function keeps the billing definition in one place if it ever needs to change.>>> billable_chars("hello") 5 >>> billable_chars(None) 0
- braidio.build_timeline(*, kinds: list[str], durations: list[float], placements: list[str] | None = None, labels: list[str] | None = None, source_spans: list[tuple[float, float] | None] | None = None, clip_edge_overlap_s: float = 0.5, narration_crossfade_s: float = 0.12, title: str = '') TimelineBreakdown[source]
Assemble a
TimelineBreakdownfrom per-beat render data (pure).kindsare the aggregation labels (any string;"clip"is the only one the layout treats specially — as an overlapping segment).placementsis the per-beat"sequential"/"under"used by the weave; offsets are computed with the samelayout_placed()the renderer uses.
- braidio.captions_for(script, timeline, *, max_chars: int = 0) str[source]
The SRT document for
scriptas laid out bytimeline.
- braidio.clean_ocr(text: str, *, collapse_whitespace: bool = True) str[source]
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.
- braidio.clip_plays_under(profile: Profile, rights: str, publishable: frozenset[str] = frozenset({'public-domain'})) bool[source]
Whether a segment with
rightsplays as audio underprofile.The whole clip-routing rule, in one place:
PERSONALplays everything,PUBLISHEDplays only publishable 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.
- 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.cues_for(script, timeline, *, max_chars: int = 0) list[Cue][source]
Subtitle cues for
scriptas laid out bytimeline.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
startsits 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
Scriptthat was rendered.timeline – the
TimelineBreakdownfromrender_production(..., return_timeline=True).max_chars – if > 0, split sentences longer than this at whitespace, so no single cue overflows a player’s two lines. 0 leaves sentences whole.
- Returns:
Cues in playback order.
- 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.describe_asset_application(fmt: Format, script, *, bed_asset: str | None = None, sting_asset: str | None = None) dict[str, bool | str | None][source]
Which of the supplied
bed_asset/sting_assetthis format will actually render, and why not otherwise (braidio#43).Pure and pre-render — safe to call before paying for anything. A key stays
Nonewhen its asset wasn’t supplied.bed_asset’s fate is fixed byfmt.music_bedalone:"none"never renders a bed, andrender_format()refusesbed_assetthere rather than spend on one that would be dropped, sobed_appliedisFalseonly via that refusal path, never in a result you got back from a successful render.sting_asset’s fate additionally depends onscript: 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.
- braidio.estimate_cost(source: Script | str, *, model_id: str | None = None) CostRollup[source]
Estimate ElevenLabs spend for a
braidio.Scriptor a raw string.Free/local work (segment extraction, weaving) contributes nothing. The returned
CostRollupreports exact characters and an honest dollar sum (a lower bound when some text is unpriced; seeusd_per_1k_chars()).>>> 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: 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, min_len_s: float = 2.2) 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.
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_slong, giving thefade 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).
- 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, return_cache_status: bool = False) Path | tuple[Path, bool][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.return_cache_status: whenTrue, return(path, was_cached)wherewas_cachedisTrueiff mixing served the audio from its on-disk cache (no ElevenLabs call = $0 real spend). Lets the caller attribute real cost (braidio#8). DefaultFalsekeeps thePathreturn.
- 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.platitude_rate(text: str, *, per: int = 1000) float[source]
Flagged hits per
perwords (default 1000). 0.0 for empty text.
- braidio.render_format(fmt: Format, script, *, source, out_path: str | Path | None = None, profile: Profile = Profile.PERSONAL, bed_asset: str | None = None, sting_asset: str | None = None, **overrides) Path[source]
Render
scriptunderfmt’s defaults;overrideswin over them.Wires the format’s
cast/narration_voice/narration_delivery/weave/structureintobraidio.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; passmusic_bed=MusicBed(...)inoverridesfor full control. A format whosemusic_bedis"none"(e.g.SONG_EXPLODER) never renders a bed at all, sobed_assetthere raisesValueErrorbefore any rendering — the caller would otherwise pay for a bed the format silently drops (braidio#43) — unlessoverridesitself suppliesmusic_bed=, which always wins and makes the refusal moot.sting_asset(a path to an app-supplied short marker) is what aSceneBreakplays under the format’sstructure; passstructure=MusicStructure(...)inoverridesfor full control. Unlike the bed, a format’sscene_markeris only the default — an individualSceneBreak.markeroverride can still play the sting even under a"none"default — so a sting that ends up unused is not refused, only reported (seedescribe_asset_application()).
- 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, structure=None, end_fade_s: float = 0.35, end_silence_s: float = 0.7, return_timeline: bool = False, tts_dir: str | Path = 'data/tts', clips_dir: str | Path = 'data/clips', episodes_dir: str | Path = 'data/episodes') Path | tuple[Path, object][source]
Render
scriptunderprofile→ a single audio file. Returns the path.With
return_timeline=Truereturns(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.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).structure(abraidio.structure.MusicStructure) is how the production marks its structure with music: a scene-break beat plays itssting(or a pause when there is none / the break is marked"none"), and a spotlit segment beat drops the bed out for its duration.Noneuses the inert defaults — no sting asset, no clip spotlit by default — so a script without scene breaks or spotlight flags renders exactly as before.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.rights_are_publishable(rights: str, publishable: frozenset[str] = frozenset({'public-domain'})) bool[source]
The publishable test at the level of a bare
rightsstring.The graph records rights on nodes, not on
SegmentBeats, so the check has to be askable without a beat in hand — but it must stay the same check.segment_is_publishable()is this function with a beat unwrapped, never a parallel rule.
- braidio.segment_is_publishable(beat: SegmentBeat, publishable: frozenset[str] = frozenset({'public-domain'})) bool[source]
Whether
beat’s audio may play in the published cut.
- braidio.skills_dir()[source]
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:
ln -s "$(python -c 'import braidio; print(braidio.skills_dir())')/braidio" \ ~/.claude/skills/braidio
- 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.strip_speaker_labels(text: str) str[source]
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.
- braidio.tts_cost_usd(text: str | None, *, model_id: str | None = None) float | None[source]
Estimated USD to synthesize
text;0.0for empty,Noneif unpriced.Empty text is genuinely free (
0.0) regardless of the rate; non-empty text isNoneonly whenusd_per_1k_chars()is unpriced.>>> tts_cost_usd("") 0.0
- braidio.usd_per_1k_chars(model_id: str | None = None) float | None[source]
Resolved USD-per-1000-characters rate (most specific source first).
Resolution: a confirmed per-model rate in
MODEL_USD_PER_1K_CHARS→ the env overrideRATE_ENV_VAR→DEFAULT_USD_PER_1K_CHARS. ReturnsNone(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.
- 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. Items markedspotlightopen a gap in it (the bed is rendered as the regions around them — seebraidio.music.bed_regions()); with none marked the bed is the single whole-span file. Falls back to a plain concat feel whenclip_edge_overlap_s == 0and there’s nothing to overlay.