braidio 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- braidio/__init__.py +242 -0
- braidio/bodies/__init__.py +42 -0
- braidio/bodies/_domain.py +120 -0
- braidio/bodies/_render_nodes.py +107 -0
- braidio/bodies/_tiers.py +43 -0
- braidio/compose.py +67 -0
- braidio/conversation.py +181 -0
- braidio/delivery.py +132 -0
- braidio/formats.py +332 -0
- braidio/kinds.py +12 -0
- braidio/multivoice.py +244 -0
- braidio/music.py +111 -0
- braidio/project.py +26 -0
- braidio/provenance.py +152 -0
- braidio/render.py +228 -0
- braidio/rights.py +197 -0
- braidio/script.py +117 -0
- braidio/sources.py +249 -0
- braidio/tts.py +163 -0
- braidio/weave.py +260 -0
- braidio/weave_config.py +121 -0
- braidio-0.0.2.dist-info/METADATA +168 -0
- braidio-0.0.2.dist-info/RECORD +25 -0
- braidio-0.0.2.dist-info/WHEEL +4 -0
- braidio-0.0.2.dist-info/licenses/LICENSE +21 -0
braidio/__init__.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""braidio — weave narration with extracted media segments into productions.
|
|
2
|
+
|
|
3
|
+
Braid two kinds of strand into one production: authored **narration** (TTS —
|
|
4
|
+
single voice or a cycled pool) and extracted **segments** of source media (song
|
|
5
|
+
clips, audiobook passages, news, SFX). The result renders to an audiovisual
|
|
6
|
+
object.
|
|
7
|
+
|
|
8
|
+
This is the **functional core** (pure Python over files + numbers; deps:
|
|
9
|
+
``mixing``, ``elevenlabs``, and ``ffmpeg`` on PATH). An optional nw-app layer
|
|
10
|
+
(graph bodies, transforms, provenance / partial re-render) will be added on top
|
|
11
|
+
and imported only when ``nw`` is available — ``import braidio`` never requires
|
|
12
|
+
it.
|
|
13
|
+
|
|
14
|
+
Extracted from the Hamilton lyrics-podcast; see that project + issues #18/#28.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
# --- composition model ---
|
|
20
|
+
from braidio.script import ( # noqa: F401
|
|
21
|
+
Script,
|
|
22
|
+
Narration,
|
|
23
|
+
SegmentBeat,
|
|
24
|
+
Dialogue,
|
|
25
|
+
Beat,
|
|
26
|
+
narration_segments,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# --- rights profiles ---
|
|
30
|
+
from braidio.rights import ( # noqa: F401
|
|
31
|
+
Profile,
|
|
32
|
+
RightsPolicy,
|
|
33
|
+
RenderPlan,
|
|
34
|
+
PlannedBeat,
|
|
35
|
+
plan_production,
|
|
36
|
+
find_verbatim_text,
|
|
37
|
+
content_violations,
|
|
38
|
+
segment_is_publishable,
|
|
39
|
+
PUBLISHABLE_CLIP_RIGHTS,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# --- segment sources (reference -> cuttable window) ---
|
|
43
|
+
from braidio.sources import ( # noqa: F401
|
|
44
|
+
SegmentSource,
|
|
45
|
+
ResolvedSegment,
|
|
46
|
+
Segment,
|
|
47
|
+
TimedLine,
|
|
48
|
+
TimedLineSegmentSource,
|
|
49
|
+
find_segment,
|
|
50
|
+
load_timing,
|
|
51
|
+
cut_quote,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# --- narration synthesis ---
|
|
55
|
+
from braidio.tts import ( # noqa: F401
|
|
56
|
+
narrate,
|
|
57
|
+
text_to_dialogue,
|
|
58
|
+
resolve_voice_id,
|
|
59
|
+
DEFAULT_VOICE_ID,
|
|
60
|
+
DEFAULT_MODEL_ID,
|
|
61
|
+
DEFAULT_VOICE_SETTINGS,
|
|
62
|
+
VOICE_ENV_VAR,
|
|
63
|
+
)
|
|
64
|
+
from braidio.conversation import ( # noqa: F401
|
|
65
|
+
ConversationCast,
|
|
66
|
+
DEFAULT_CAST,
|
|
67
|
+
render_dialogue,
|
|
68
|
+
render_turns_sequential,
|
|
69
|
+
)
|
|
70
|
+
from braidio.delivery import ( # noqa: F401
|
|
71
|
+
Delivery,
|
|
72
|
+
DELIVERIES,
|
|
73
|
+
BASELINE,
|
|
74
|
+
V2_TUNED,
|
|
75
|
+
V2_AGGRESSIVE,
|
|
76
|
+
V2_PRESENTER,
|
|
77
|
+
V2_NARRATOR,
|
|
78
|
+
V3_NATURAL,
|
|
79
|
+
V3_CREATIVE,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# --- multi-voice casting ---
|
|
83
|
+
from braidio.multivoice import ( # noqa: F401
|
|
84
|
+
Voice,
|
|
85
|
+
POOL_4,
|
|
86
|
+
POOL_MANY,
|
|
87
|
+
POOLS,
|
|
88
|
+
strip_markup,
|
|
89
|
+
split_segments,
|
|
90
|
+
assign_voices,
|
|
91
|
+
group_turns,
|
|
92
|
+
render_multivoice,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# --- configuration ---
|
|
96
|
+
from braidio.weave_config import WeaveConfig, PRESETS # noqa: F401
|
|
97
|
+
|
|
98
|
+
# --- music bed (instrumental underscore) ---
|
|
99
|
+
from braidio.music import ( # noqa: F401
|
|
100
|
+
MusicBed,
|
|
101
|
+
bed_for_intensity,
|
|
102
|
+
BED_GAIN_BY_INTENSITY,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# --- ready-made format templates (standard-named presets) ---
|
|
106
|
+
from braidio.formats import ( # noqa: F401
|
|
107
|
+
Format,
|
|
108
|
+
render_format,
|
|
109
|
+
FORMATS,
|
|
110
|
+
SOLO_EXPLAINER,
|
|
111
|
+
DEEP_DIVE,
|
|
112
|
+
INTERVIEW,
|
|
113
|
+
SONG_EXPLODER,
|
|
114
|
+
PANEL,
|
|
115
|
+
DEBATE,
|
|
116
|
+
DOCUMENTARY_VO,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# --- composition + weaving (audio) ---
|
|
120
|
+
from braidio.compose import compose_narration # noqa: F401
|
|
121
|
+
from braidio.render import render_production # noqa: F401
|
|
122
|
+
from braidio.weave import ( # noqa: F401
|
|
123
|
+
TimelineItem,
|
|
124
|
+
extract_padded,
|
|
125
|
+
weave_timeline,
|
|
126
|
+
layout_starts,
|
|
127
|
+
duration_s,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# --- production kinds (pure) ---
|
|
131
|
+
from braidio.kinds import WeaveKind # noqa: F401
|
|
132
|
+
|
|
133
|
+
# --- optional nw-app layer (graph bodies + provenance) --------------------
|
|
134
|
+
# Registers braidio's domain/render body schemas with lacing and exposes the
|
|
135
|
+
# provenance / partial-re-render helpers. Guarded: `import braidio` never needs
|
|
136
|
+
# lacing/nw. `HAS_GRAPH` / `HAS_NW` report what's available.
|
|
137
|
+
HAS_GRAPH = False
|
|
138
|
+
HAS_NW = False
|
|
139
|
+
try: # needs lacing
|
|
140
|
+
from braidio import bodies # noqa: F401 (registers schemas)
|
|
141
|
+
from braidio.provenance import ( # noqa: F401
|
|
142
|
+
record_render,
|
|
143
|
+
stale_after,
|
|
144
|
+
descendants_of,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
HAS_GRAPH = True
|
|
148
|
+
except ImportError: # pragma: no cover - optional dep
|
|
149
|
+
pass
|
|
150
|
+
try: # needs nw
|
|
151
|
+
from braidio.project import Project # noqa: F401
|
|
152
|
+
|
|
153
|
+
HAS_NW = True
|
|
154
|
+
except ImportError: # pragma: no cover - optional dep
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
__all__ = [
|
|
158
|
+
# production kinds
|
|
159
|
+
"WeaveKind",
|
|
160
|
+
"HAS_GRAPH",
|
|
161
|
+
"HAS_NW",
|
|
162
|
+
# script
|
|
163
|
+
"Script",
|
|
164
|
+
"Narration",
|
|
165
|
+
"SegmentBeat",
|
|
166
|
+
"Dialogue",
|
|
167
|
+
"Beat",
|
|
168
|
+
"narration_segments",
|
|
169
|
+
# rights
|
|
170
|
+
"Profile",
|
|
171
|
+
"RightsPolicy",
|
|
172
|
+
"RenderPlan",
|
|
173
|
+
"PlannedBeat",
|
|
174
|
+
"plan_production",
|
|
175
|
+
"find_verbatim_text",
|
|
176
|
+
"content_violations",
|
|
177
|
+
"segment_is_publishable",
|
|
178
|
+
"PUBLISHABLE_CLIP_RIGHTS",
|
|
179
|
+
# render
|
|
180
|
+
"render_production",
|
|
181
|
+
# sources
|
|
182
|
+
"SegmentSource",
|
|
183
|
+
"ResolvedSegment",
|
|
184
|
+
"Segment",
|
|
185
|
+
"TimedLine",
|
|
186
|
+
"TimedLineSegmentSource",
|
|
187
|
+
"find_segment",
|
|
188
|
+
"load_timing",
|
|
189
|
+
"cut_quote",
|
|
190
|
+
# tts
|
|
191
|
+
"narrate",
|
|
192
|
+
"resolve_voice_id",
|
|
193
|
+
"DEFAULT_VOICE_ID",
|
|
194
|
+
"DEFAULT_MODEL_ID",
|
|
195
|
+
"DEFAULT_VOICE_SETTINGS",
|
|
196
|
+
"VOICE_ENV_VAR",
|
|
197
|
+
# delivery
|
|
198
|
+
"Delivery",
|
|
199
|
+
"DELIVERIES",
|
|
200
|
+
"BASELINE",
|
|
201
|
+
"V2_TUNED",
|
|
202
|
+
"V2_AGGRESSIVE",
|
|
203
|
+
"V2_PRESENTER",
|
|
204
|
+
"V2_NARRATOR",
|
|
205
|
+
"V3_NATURAL",
|
|
206
|
+
"V3_CREATIVE",
|
|
207
|
+
# multivoice
|
|
208
|
+
"Voice",
|
|
209
|
+
"POOL_4",
|
|
210
|
+
"POOL_MANY",
|
|
211
|
+
"POOLS",
|
|
212
|
+
"strip_markup",
|
|
213
|
+
"split_segments",
|
|
214
|
+
"assign_voices",
|
|
215
|
+
"group_turns",
|
|
216
|
+
"render_multivoice",
|
|
217
|
+
# config
|
|
218
|
+
"WeaveConfig",
|
|
219
|
+
"PRESETS",
|
|
220
|
+
# music bed
|
|
221
|
+
"MusicBed",
|
|
222
|
+
"bed_for_intensity",
|
|
223
|
+
"BED_GAIN_BY_INTENSITY",
|
|
224
|
+
# formats (ready-made templates)
|
|
225
|
+
"Format",
|
|
226
|
+
"render_format",
|
|
227
|
+
"FORMATS",
|
|
228
|
+
"SOLO_EXPLAINER",
|
|
229
|
+
"DEEP_DIVE",
|
|
230
|
+
"INTERVIEW",
|
|
231
|
+
"SONG_EXPLODER",
|
|
232
|
+
"PANEL",
|
|
233
|
+
"DEBATE",
|
|
234
|
+
"DOCUMENTARY_VO",
|
|
235
|
+
# compose + weave
|
|
236
|
+
"compose_narration",
|
|
237
|
+
"TimelineItem",
|
|
238
|
+
"extract_padded",
|
|
239
|
+
"weave_timeline",
|
|
240
|
+
"layout_starts",
|
|
241
|
+
"duration_s",
|
|
242
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""braidio's lacing body schemas + tiers (the nw-app domain vocabulary).
|
|
2
|
+
|
|
3
|
+
Importing this package **registers every schema** with lacing (side effect of
|
|
4
|
+
importing the submodules). Requires ``lacing``; it is imported lazily by
|
|
5
|
+
``braidio/__init__.py`` only when available, so the functional core never
|
|
6
|
+
needs it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from braidio.bodies._domain import ( # noqa: F401 (import registers)
|
|
12
|
+
COMMENTARY_V1,
|
|
13
|
+
SOURCE_V1,
|
|
14
|
+
AUDIO_CLIP_V1,
|
|
15
|
+
NARRATIVE_BEAT_V1,
|
|
16
|
+
EPISODE_V1,
|
|
17
|
+
CommentaryBodyV1,
|
|
18
|
+
SourceBodyV1,
|
|
19
|
+
AudioClipBodyV1,
|
|
20
|
+
NarrativeBeatBodyV1,
|
|
21
|
+
EpisodeBodyV1,
|
|
22
|
+
DOMAIN_SCHEMAS,
|
|
23
|
+
)
|
|
24
|
+
from braidio.bodies._render_nodes import ( # noqa: F401 (import registers)
|
|
25
|
+
WEAVE_CONFIG_V1,
|
|
26
|
+
SOURCE_MEDIA_V1,
|
|
27
|
+
VOICE_ASSIGNMENT_V1,
|
|
28
|
+
NARRATION_RENDER_V1,
|
|
29
|
+
SEGMENT_EXTRACTION_V1,
|
|
30
|
+
EPISODE_RENDER_V1,
|
|
31
|
+
WeaveConfigBodyV1,
|
|
32
|
+
SourceMediaBodyV1,
|
|
33
|
+
VoiceAssignmentBodyV1,
|
|
34
|
+
NarrationRenderBodyV1,
|
|
35
|
+
SegmentExtractionBodyV1,
|
|
36
|
+
EpisodeRenderBodyV1,
|
|
37
|
+
RENDER_SCHEMAS,
|
|
38
|
+
)
|
|
39
|
+
from braidio.bodies._tiers import TIERS, register_tiers # noqa: F401
|
|
40
|
+
|
|
41
|
+
# All schema URIs braidio registers (domain + render).
|
|
42
|
+
SCHEMA_URIS: dict = {**DOMAIN_SCHEMAS, **RENDER_SCHEMAS}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Generic authoring body schemas for a commentary-weave production.
|
|
2
|
+
|
|
3
|
+
The media-agnostic domain vocabulary: curated commentary, cited sources,
|
|
4
|
+
playable media clips, narrative beats, and the episode container. Registered
|
|
5
|
+
with lacing on import. (Consumer-specific schemas — e.g. Hamilton's Genius
|
|
6
|
+
``song``/``lyric-line``/``referent``/``annotation`` — live in the consumer.)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Literal, Optional
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
from lacing.schema import register_body_schema
|
|
16
|
+
|
|
17
|
+
COMMENTARY_V1 = "annot://schema/commentary/v1"
|
|
18
|
+
SOURCE_V1 = "annot://schema/source/v1"
|
|
19
|
+
AUDIO_CLIP_V1 = "annot://schema/audio-clip/v1"
|
|
20
|
+
NARRATIVE_BEAT_V1 = "annot://schema/narrative-beat/v1"
|
|
21
|
+
EPISODE_V1 = "annot://schema/episode/v1"
|
|
22
|
+
|
|
23
|
+
CommentaryFacet = Literal["historical", "musical", "biographical", "production"]
|
|
24
|
+
ClipRights = Literal["copyrighted", "owned-local", "public-domain"]
|
|
25
|
+
"""Rights posture of a playable segment: ``owned-local`` plays only in the
|
|
26
|
+
personal cut; ``copyrighted`` never renders to audio; ``public-domain`` is safe."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CommentaryBodyV1(BaseModel):
|
|
30
|
+
"""A curated/generated commentary note, grounded in ``source`` nodes."""
|
|
31
|
+
|
|
32
|
+
model_config = {"frozen": True, "extra": "forbid"}
|
|
33
|
+
|
|
34
|
+
text: str = Field(..., description="The commentary, as narrated/derived.")
|
|
35
|
+
facet: CommentaryFacet = Field(..., description="Lens: historical/musical/…")
|
|
36
|
+
source_ids: tuple[str, ...] = Field(
|
|
37
|
+
default_factory=tuple,
|
|
38
|
+
description="Ids of the source/v1 nodes this draws on.",
|
|
39
|
+
)
|
|
40
|
+
generated_by: str = Field(
|
|
41
|
+
..., description="'human:<handle>' or 'agent:<model>@<hash>'."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SourceBodyV1(BaseModel):
|
|
46
|
+
"""A citable source node — primary material, a book, a web page."""
|
|
47
|
+
|
|
48
|
+
model_config = {"frozen": True, "extra": "forbid"}
|
|
49
|
+
|
|
50
|
+
kind: str = Field(
|
|
51
|
+
..., description="Open source class (e.g. 'founders-online', 'book')."
|
|
52
|
+
)
|
|
53
|
+
title: str = Field(..., description="Source title / heading.")
|
|
54
|
+
citation: str = Field(..., description="Human-readable citation string.")
|
|
55
|
+
url: str = Field("", description="Canonical URL, if any.")
|
|
56
|
+
public_domain: bool = Field(..., description="True if usable in the published cut.")
|
|
57
|
+
excerpt: Optional[str] = Field(None, description="Optional quoted excerpt.")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AudioClipBodyV1(BaseModel):
|
|
61
|
+
"""A playable media segment. The interval is on the annotation's ``MediaRef``;
|
|
62
|
+
this body carries clip metadata and the load-bearing ``rights`` flag."""
|
|
63
|
+
|
|
64
|
+
model_config = {"frozen": True, "extra": "forbid"}
|
|
65
|
+
|
|
66
|
+
source_node_id: str = Field(
|
|
67
|
+
..., description="Id of the source-media/song node this clip cuts."
|
|
68
|
+
)
|
|
69
|
+
label: str = Field(..., description="Human label, e.g. 'opening 8 bars'.")
|
|
70
|
+
rights: ClipRights = Field(
|
|
71
|
+
..., description="Rights posture (drives render profile)."
|
|
72
|
+
)
|
|
73
|
+
gain_db: Optional[float] = Field(None, description="Optional gain (dB) when mixed.")
|
|
74
|
+
fade: Optional[tuple[float, float]] = Field(
|
|
75
|
+
None,
|
|
76
|
+
description="Optional (fade_in_s, fade_out_s) applied when this clip plays.",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class NarrativeBeatBodyV1(BaseModel):
|
|
81
|
+
"""One unit of the script: spoken narration, ordered, optionally playing a clip."""
|
|
82
|
+
|
|
83
|
+
model_config = {"frozen": True, "extra": "forbid"}
|
|
84
|
+
|
|
85
|
+
beat_id: str = Field(..., description="Zero-padded ordering key (e.g. '0007').")
|
|
86
|
+
text: str = Field(..., description="Spoken narration text.")
|
|
87
|
+
style: Optional[str] = Field(
|
|
88
|
+
None, description="Optional delivery/voice style hint."
|
|
89
|
+
)
|
|
90
|
+
draws_on: tuple[str, ...] = Field(
|
|
91
|
+
default_factory=tuple,
|
|
92
|
+
description="Ids this beat is derived from (commentary / source / clip nodes).",
|
|
93
|
+
)
|
|
94
|
+
plays_clip: Optional[str] = Field(
|
|
95
|
+
None, description="Id of an audio-clip/v1 to play here."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class EpisodeBodyV1(BaseModel):
|
|
100
|
+
"""An ordered container of narrative beats (and the clips they reference)."""
|
|
101
|
+
|
|
102
|
+
model_config = {"frozen": True, "extra": "forbid"}
|
|
103
|
+
|
|
104
|
+
title: str = Field(..., description="Episode title.")
|
|
105
|
+
ordered_member_ids: tuple[str, ...] = Field(
|
|
106
|
+
default_factory=tuple,
|
|
107
|
+
description="Ids of narrative-beat nodes, in play order.",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
DOMAIN_SCHEMAS: dict[str, type[BaseModel]] = {
|
|
112
|
+
COMMENTARY_V1: CommentaryBodyV1,
|
|
113
|
+
SOURCE_V1: SourceBodyV1,
|
|
114
|
+
AUDIO_CLIP_V1: AudioClipBodyV1,
|
|
115
|
+
NARRATIVE_BEAT_V1: NarrativeBeatBodyV1,
|
|
116
|
+
EPISODE_V1: EpisodeBodyV1,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for _uri, _model in DOMAIN_SCHEMAS.items():
|
|
120
|
+
register_body_schema(_uri, _model)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Render-provenance body schemas — record the render *choices* + outputs.
|
|
2
|
+
|
|
3
|
+
These make a render traceable and **partially re-renderable**: each output node
|
|
4
|
+
records what it was derived from (via the annotation's
|
|
5
|
+
``provenance.was_derived_from``) plus a **cache key** hashing the inputs that
|
|
6
|
+
actually affect the audio. Change one beat or one parameter → only its
|
|
7
|
+
descendants are stale (see :mod:`braidio.provenance`). See the design in
|
|
8
|
+
``misc/docs/Extraction and nw-App Design.md`` and nw's render-provenance doc.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
from lacing.schema import register_body_schema
|
|
18
|
+
|
|
19
|
+
WEAVE_CONFIG_V1 = "annot://schema/weave-config/v1"
|
|
20
|
+
SOURCE_MEDIA_V1 = "annot://schema/source-media/v1"
|
|
21
|
+
VOICE_ASSIGNMENT_V1 = "annot://schema/voice-assignment/v1"
|
|
22
|
+
NARRATION_RENDER_V1 = "annot://schema/narration-render/v1"
|
|
23
|
+
SEGMENT_EXTRACTION_V1 = "annot://schema/segment-extraction/v1"
|
|
24
|
+
EPISODE_RENDER_V1 = "annot://schema/episode-render/v1"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WeaveConfigBodyV1(BaseModel):
|
|
28
|
+
"""A frozen snapshot of every render choice (``WeaveConfig.to_dict()``)."""
|
|
29
|
+
|
|
30
|
+
model_config = {"frozen": True, "extra": "forbid"}
|
|
31
|
+
|
|
32
|
+
config: dict[str, Any] = Field(..., description="The full WeaveConfig snapshot.")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SourceMediaBodyV1(BaseModel):
|
|
36
|
+
"""A pointer to an imported source asset (its content-addressed artifact)."""
|
|
37
|
+
|
|
38
|
+
model_config = {"frozen": True, "extra": "forbid"}
|
|
39
|
+
|
|
40
|
+
label: str = Field(
|
|
41
|
+
..., description="Human label for the source (e.g. a song title)."
|
|
42
|
+
)
|
|
43
|
+
asset_id: str = Field(
|
|
44
|
+
..., description="lacing Artifact asset_id of the source media."
|
|
45
|
+
)
|
|
46
|
+
rights: str = Field("owned-local", description="Rights posture of the source.")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class VoiceAssignmentBodyV1(BaseModel):
|
|
50
|
+
"""Which voice a turn got (+ pool + seed) — a render choice."""
|
|
51
|
+
|
|
52
|
+
model_config = {"frozen": True, "extra": "forbid"}
|
|
53
|
+
|
|
54
|
+
voice_id: str = Field(..., description="Voice id used for this turn.")
|
|
55
|
+
pool_label: str = Field("single", description="Pool the voice was drawn from.")
|
|
56
|
+
seed: int = Field(0, description="Assignment seed (for reproducibility).")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class NarrationRenderBodyV1(BaseModel):
|
|
60
|
+
"""The audio Artifact for one synthesized narration turn + its cache key."""
|
|
61
|
+
|
|
62
|
+
model_config = {"frozen": True, "extra": "forbid"}
|
|
63
|
+
|
|
64
|
+
cache_key: str = Field(..., description="hash(text, voice, model, settings).")
|
|
65
|
+
artifact_id: Optional[str] = Field(
|
|
66
|
+
None, description="lacing Artifact asset_id (once rendered)."
|
|
67
|
+
)
|
|
68
|
+
duration_s: float = Field(0.0, description="Rendered duration, seconds.")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SegmentExtractionBodyV1(BaseModel):
|
|
72
|
+
"""The cut+padded audio Artifact for one segment + its cache key."""
|
|
73
|
+
|
|
74
|
+
model_config = {"frozen": True, "extra": "forbid"}
|
|
75
|
+
|
|
76
|
+
cache_key: str = Field(
|
|
77
|
+
..., description="hash(source asset, start, end, pads, fades)."
|
|
78
|
+
)
|
|
79
|
+
start_s: float = Field(..., description="Padded extraction start (seconds).")
|
|
80
|
+
end_s: float = Field(..., description="Padded extraction end (seconds).")
|
|
81
|
+
artifact_id: Optional[str] = Field(None, description="lacing Artifact asset_id.")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class EpisodeRenderBodyV1(BaseModel):
|
|
85
|
+
"""The assembled output Artifact for an episode + its profile."""
|
|
86
|
+
|
|
87
|
+
model_config = {"frozen": True, "extra": "forbid"}
|
|
88
|
+
|
|
89
|
+
profile: str = Field(..., description="Render profile: personal | published.")
|
|
90
|
+
ordered_member_ids: tuple[str, ...] = Field(
|
|
91
|
+
default_factory=tuple, description="Ids of the member render nodes, in order."
|
|
92
|
+
)
|
|
93
|
+
artifact_id: Optional[str] = Field(None, description="lacing Artifact of the mix.")
|
|
94
|
+
duration_s: float = Field(0.0, description="Total duration, seconds.")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
RENDER_SCHEMAS: dict[str, type[BaseModel]] = {
|
|
98
|
+
WEAVE_CONFIG_V1: WeaveConfigBodyV1,
|
|
99
|
+
SOURCE_MEDIA_V1: SourceMediaBodyV1,
|
|
100
|
+
VOICE_ASSIGNMENT_V1: VoiceAssignmentBodyV1,
|
|
101
|
+
NARRATION_RENDER_V1: NarrationRenderBodyV1,
|
|
102
|
+
SEGMENT_EXTRACTION_V1: SegmentExtractionBodyV1,
|
|
103
|
+
EPISODE_RENDER_V1: EpisodeRenderBodyV1,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for _uri, _model in RENDER_SCHEMAS.items():
|
|
107
|
+
register_body_schema(_uri, _model)
|
braidio/bodies/_tiers.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Generic tiers for a commentary-weave production + its render nodes.
|
|
2
|
+
|
|
3
|
+
Standoff layers (no temporal nesting); a consumer adds its own structural spine
|
|
4
|
+
(e.g. Hamilton's ``songs → sections → lyric-lines → words``). Add to a store
|
|
5
|
+
with :func:`register_tiers`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from lacing.tier import Tier
|
|
11
|
+
|
|
12
|
+
# Authoring layers.
|
|
13
|
+
_COMMENTARY = Tier("commentary")
|
|
14
|
+
_SOURCES = Tier("sources")
|
|
15
|
+
_AUDIO_CLIPS = Tier("audio-clips")
|
|
16
|
+
_NARRATIVE_BEATS = Tier("narrative-beats")
|
|
17
|
+
# Render-provenance layers.
|
|
18
|
+
_WEAVE_CONFIGS = Tier("weave-configs")
|
|
19
|
+
_SOURCE_MEDIA = Tier("source-media")
|
|
20
|
+
_VOICE_ASSIGNMENTS = Tier("voice-assignments")
|
|
21
|
+
_NARRATION_RENDERS = Tier("narration-renders")
|
|
22
|
+
_SEGMENT_EXTRACTIONS = Tier("segment-extractions")
|
|
23
|
+
_EPISODE_RENDERS = Tier("episode-renders")
|
|
24
|
+
|
|
25
|
+
TIERS: tuple[Tier, ...] = (
|
|
26
|
+
_COMMENTARY,
|
|
27
|
+
_SOURCES,
|
|
28
|
+
_AUDIO_CLIPS,
|
|
29
|
+
_NARRATIVE_BEATS,
|
|
30
|
+
_WEAVE_CONFIGS,
|
|
31
|
+
_SOURCE_MEDIA,
|
|
32
|
+
_VOICE_ASSIGNMENTS,
|
|
33
|
+
_NARRATION_RENDERS,
|
|
34
|
+
_SEGMENT_EXTRACTIONS,
|
|
35
|
+
_EPISODE_RENDERS,
|
|
36
|
+
)
|
|
37
|
+
"""braidio's generic + render tiers."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def register_tiers(store) -> None:
|
|
41
|
+
"""Add every tier in :data:`TIERS` to ``store`` (has ``add_tier``)."""
|
|
42
|
+
for tier in TIERS:
|
|
43
|
+
store.add_tier(tier)
|
braidio/compose.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Config-driven narration composition (#20) — the reusable entrypoint.
|
|
2
|
+
|
|
3
|
+
Turn a list of narration segments + a :class:`WeaveConfig` into audio. Dispatches
|
|
4
|
+
single-voice and multi-voice uniformly (single = a pool of one), reading *every*
|
|
5
|
+
knob from the config. This is the seed of the reusable weave engine (#18/#19):
|
|
6
|
+
it has no Hamilton-specifics — it takes plain text segments and a config.
|
|
7
|
+
|
|
8
|
+
Clip weaving (padded extraction + ducking) is composed separately (#21); this
|
|
9
|
+
module renders the narration track.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from braidio.multivoice import POOL_MANY, Voice, render_multivoice
|
|
17
|
+
from braidio.tts import DEFAULT_VOICE_ID
|
|
18
|
+
from braidio.weave_config import WeaveConfig
|
|
19
|
+
|
|
20
|
+
# Registry so pooled ids render with human-readable turn labels; unknown ids
|
|
21
|
+
# fall back to the id itself.
|
|
22
|
+
_KNOWN_VOICES: dict[str, Voice] = {v.id: v for v in POOL_MANY}
|
|
23
|
+
_KNOWN_VOICES.setdefault(
|
|
24
|
+
DEFAULT_VOICE_ID, Voice(DEFAULT_VOICE_ID, "George", "M", "British", "storyteller")
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _pool_from_config(config: WeaveConfig) -> list[Voice]:
|
|
29
|
+
return [_KNOWN_VOICES.get(vid, Voice(vid, vid[:6], "?")) for vid in config.voices]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def compose_narration(
|
|
33
|
+
segments: list[str],
|
|
34
|
+
config: WeaveConfig,
|
|
35
|
+
*,
|
|
36
|
+
out_path: str | Path,
|
|
37
|
+
api_key: str | None = None,
|
|
38
|
+
work_dir: str | Path = "data/tts/compose",
|
|
39
|
+
) -> list[tuple[Voice, str]]:
|
|
40
|
+
"""Render ``segments`` under ``config`` → ``out_path``.
|
|
41
|
+
|
|
42
|
+
Single-voice and multi-voice go through the same turn-based path (a single
|
|
43
|
+
voice is just a one-voice pool). Returns the ``(voice, turn_text)``
|
|
44
|
+
assignment for reporting / provenance.
|
|
45
|
+
|
|
46
|
+
``api_key`` is an optional per-request ElevenLabs key threaded to
|
|
47
|
+
:func:`braidio.multivoice.render_multivoice` (and thence every synthesized
|
|
48
|
+
turn); ``None`` (default) keeps the ``$ELEVENLABS_API_KEY`` fallback.
|
|
49
|
+
"""
|
|
50
|
+
return render_multivoice(
|
|
51
|
+
segments,
|
|
52
|
+
_pool_from_config(config),
|
|
53
|
+
out_path=out_path,
|
|
54
|
+
api_key=api_key,
|
|
55
|
+
work_dir=work_dir,
|
|
56
|
+
seed=config.voice_seed,
|
|
57
|
+
min_turn=config.min_turn,
|
|
58
|
+
max_turn=config.max_turn,
|
|
59
|
+
avoid_immediate_repeat=config.avoid_immediate_repeat,
|
|
60
|
+
model_id=config.model_id,
|
|
61
|
+
base_settings=dict(config.voice_settings),
|
|
62
|
+
speed_base=config.speed_base,
|
|
63
|
+
speed_jitter=config.speed_jitter,
|
|
64
|
+
crossfade_s=config.crossfade_s,
|
|
65
|
+
gap_s=config.gap_turn_s,
|
|
66
|
+
target_lufs=config.target_lufs,
|
|
67
|
+
)
|