Source code for braidio.compose

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

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

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

from __future__ import annotations

from pathlib import Path

from braidio.multivoice import POOL_MANY, Voice, render_multivoice
from braidio.tts import DEFAULT_VOICE_ID
from braidio.weave_config import WeaveConfig

# Registry so pooled ids render with human-readable turn labels; unknown ids
# fall back to the id itself.
_KNOWN_VOICES: dict[str, Voice] = {v.id: v for v in POOL_MANY}
_KNOWN_VOICES.setdefault(
    DEFAULT_VOICE_ID, Voice(DEFAULT_VOICE_ID, "George", "M", "British", "storyteller")
)


def _pool_from_config(config: WeaveConfig) -> list[Voice]:
    return [
        _KNOWN_VOICES.get(vid, Voice(vid, vid[:6], "?"))
        for vid in config.voices
    ]


[docs] def 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]]: """Render ``segments`` under ``config`` → ``out_path``. Single-voice and multi-voice go through the same turn-based path (a single voice is just a one-voice pool). Returns the ``(voice, turn_text)`` assignment for reporting / provenance. ``api_key`` is an optional per-request ElevenLabs key threaded to :func:`braidio.multivoice.render_multivoice` (and thence every synthesized turn); ``None`` (default) keeps the ``$ELEVENLABS_API_KEY`` fallback. """ return render_multivoice( segments, _pool_from_config(config), out_path=out_path, api_key=api_key, work_dir=work_dir, seed=config.voice_seed, min_turn=config.min_turn, max_turn=config.max_turn, avoid_immediate_repeat=config.avoid_immediate_repeat, model_id=config.model_id, base_settings=dict(config.voice_settings), speed_base=config.speed_base, speed_jitter=config.speed_jitter, crossfade_s=config.crossfade_s, gap_s=config.gap_turn_s, target_lufs=config.target_lufs, )