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 than falaw.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.

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

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).

turns is an ordered tuple of (role, text) pairs (roles like "A"/"B" map to voices via a 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: str, match: str, start: int)[source]

One flagged platitude.

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 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: str | Path | None = None, profile: Profile = Profile.PERSONAL, **overrides) Path[source]

Render script with this format’s defaults (see render_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_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: 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_marker is the default for a SceneBreak whose marker is None ("sting" / "none"). spotlight_clips is the default for a SegmentBeat whose spotlight is NoneTrue 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: 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 beat plays 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 beat is spotlit (its override, else the format default).

spotlight_of(spotlight: bool | None) bool[source]

Whether this per-beat override is spotlit (None = 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_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: str, content: str, from_index: int, note: str = '', turns: tuple | None = None)[source]

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)[source]

Which projection of the production we render.

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 SegmentSource returns.

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_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[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 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: 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 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: 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 a 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)[source]

Resolve an opaque reference to a ResolvedSegment (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_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: 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 SegmentSource over time-aligned lines + one source asset.

Binds the generic find_segment() matcher to a concrete asset so the weave engine can resolve(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.

shares() dict[str, float][source]

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

to_html(title: str | None = None, subtitle: str = '') str[source]

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

totals() dict[str, float][source]

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

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 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: 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.

to_dict() dict[str, Any][source]

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

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 n turns — random, seeded, no immediate repeats (so it isn’t a rigid round-robin).

braidio.audit_platitudes(text: str) list[Finding][source]

Return every Finding in text, in document order.

braidio.bed_for_intensity(asset_path: str, intensity: str, **overrides) MusicBed | None[source]

Build a 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.

braidio.billable_chars(text: str | None) int[source]

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.

>>> 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 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() the renderer uses.

braidio.captions_for(script, timeline, *, max_chars: int = 0) str[source]

The SRT document for script as laid out by timeline.

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 rights plays as audio under profile.

The whole clip-routing rule, in one place: PERSONAL plays everything, PUBLISHED plays 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 segments under configout_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() (and thence every synthesized turn); None (default) keeps the $ELEVENLABS_API_KEY fallback.

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 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 that was rendered.

  • timeline – the TimelineBreakdown from render_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 from audio_path (pad + fades).

Convenience combining find_segment() + an ffmpeg cut. Returns the resolved Segment (raises LookupError if unmatched). New code should prefer a SegmentSource + 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_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() 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.

braidio.estimate_cost(source: Script | str, *, model_id: str | None = None) CostRollup[source]

Estimate ElevenLabs spend for a braidio.Script or a raw string.

Free/local work (segment extraction, weaving) contributes nothing. The returned CostRollup reports exact characters and an honest dollar sum (a lower bound when some text is unpriced; see usd_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_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).

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, 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.

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 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.

braidio.format_srt(cues) str[source]

Render cues as an SRT document.

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_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.

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 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.

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 script into the beats renderable under profile.

braidio.platitude_rate(text: str, *, per: int = 1000) float[source]

Flagged hits per per words (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 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(). 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()).

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 segments cycling poolout_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() 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.

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 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.

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).

structure (a 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.

api_key is an optional per-request ElevenLabs key threaded to every synthesized beat — both narration (braidio.tts.narrate()) and dialogue (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.

braidio.resolve_voice_id(voice_id: str | None = None) str[source]

Voice id from arg → VOICE_ENV_VAR env → default.

braidio.rights_are_publishable(rights: str, publishable: frozenset[str] = frozenset({'public-domain'})) bool[source]

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

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.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() 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 override RATE_ENV_VARDEFAULT_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.

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. Returns out_path.

bed (a 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()); 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.