livepilot 1.9.13 → 1.9.15
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.
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +51 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +7 -7
- package/bin/livepilot.js +32 -8
- package/installer/install.js +21 -2
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/agents/livepilot-producer/AGENT.md +243 -49
- package/livepilot/skills/livepilot-core/SKILL.md +81 -6
- package/livepilot/skills/livepilot-core/references/m4l-devices.md +2 -2
- package/livepilot/skills/livepilot-core/references/overview.md +3 -3
- package/livepilot/skills/livepilot-core/references/sound-design.md +3 -2
- package/livepilot/skills/livepilot-release/SKILL.md +13 -13
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +6 -3
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/curves.py +11 -3
- package/mcp_server/evaluation/__init__.py +1 -0
- package/mcp_server/evaluation/fabric.py +575 -0
- package/mcp_server/evaluation/feature_extractors.py +84 -0
- package/mcp_server/evaluation/policy.py +67 -0
- package/mcp_server/evaluation/tools.py +53 -0
- package/mcp_server/memory/__init__.py +11 -2
- package/mcp_server/memory/anti_memory.py +78 -0
- package/mcp_server/memory/promotion.py +94 -0
- package/mcp_server/memory/session_memory.py +108 -0
- package/mcp_server/memory/taste_memory.py +158 -0
- package/mcp_server/memory/technique_store.py +2 -1
- package/mcp_server/memory/tools.py +112 -0
- package/mcp_server/mix_engine/__init__.py +1 -0
- package/mcp_server/mix_engine/critics.py +299 -0
- package/mcp_server/mix_engine/models.py +152 -0
- package/mcp_server/mix_engine/planner.py +103 -0
- package/mcp_server/mix_engine/state_builder.py +316 -0
- package/mcp_server/mix_engine/tools.py +214 -0
- package/mcp_server/performance_engine/__init__.py +1 -0
- package/mcp_server/performance_engine/models.py +148 -0
- package/mcp_server/performance_engine/planner.py +267 -0
- package/mcp_server/performance_engine/safety.py +162 -0
- package/mcp_server/performance_engine/tools.py +183 -0
- package/mcp_server/project_brain/__init__.py +6 -0
- package/mcp_server/project_brain/arrangement_graph.py +64 -0
- package/mcp_server/project_brain/automation_graph.py +72 -0
- package/mcp_server/project_brain/builder.py +123 -0
- package/mcp_server/project_brain/capability_graph.py +64 -0
- package/mcp_server/project_brain/models.py +282 -0
- package/mcp_server/project_brain/refresh.py +80 -0
- package/mcp_server/project_brain/role_graph.py +103 -0
- package/mcp_server/project_brain/session_graph.py +51 -0
- package/mcp_server/project_brain/tools.py +144 -0
- package/mcp_server/reference_engine/__init__.py +1 -0
- package/mcp_server/reference_engine/gap_analyzer.py +239 -0
- package/mcp_server/reference_engine/models.py +105 -0
- package/mcp_server/reference_engine/profile_builder.py +149 -0
- package/mcp_server/reference_engine/tactic_router.py +117 -0
- package/mcp_server/reference_engine/tools.py +235 -0
- package/mcp_server/runtime/__init__.py +1 -0
- package/mcp_server/runtime/action_ledger.py +117 -0
- package/mcp_server/runtime/action_ledger_models.py +84 -0
- package/mcp_server/runtime/action_tools.py +57 -0
- package/mcp_server/runtime/capability_state.py +218 -0
- package/mcp_server/runtime/safety_kernel.py +339 -0
- package/mcp_server/runtime/safety_tools.py +42 -0
- package/mcp_server/runtime/tools.py +64 -0
- package/mcp_server/server.py +23 -1
- package/mcp_server/sound_design/__init__.py +1 -0
- package/mcp_server/sound_design/critics.py +297 -0
- package/mcp_server/sound_design/models.py +147 -0
- package/mcp_server/sound_design/planner.py +104 -0
- package/mcp_server/sound_design/tools.py +297 -0
- package/mcp_server/tools/_agent_os_engine.py +947 -0
- package/mcp_server/tools/_composition_engine.py +1530 -0
- package/mcp_server/tools/_conductor.py +199 -0
- package/mcp_server/tools/_conductor_budgets.py +222 -0
- package/mcp_server/tools/_evaluation_contracts.py +91 -0
- package/mcp_server/tools/_form_engine.py +416 -0
- package/mcp_server/tools/_motif_engine.py +351 -0
- package/mcp_server/tools/_planner_engine.py +516 -0
- package/mcp_server/tools/_research_engine.py +542 -0
- package/mcp_server/tools/_research_provider.py +185 -0
- package/mcp_server/tools/_snapshot_normalizer.py +49 -0
- package/mcp_server/tools/agent_os.py +440 -0
- package/mcp_server/tools/analyzer.py +18 -0
- package/mcp_server/tools/automation.py +25 -10
- package/mcp_server/tools/composition.py +563 -0
- package/mcp_server/tools/motif.py +104 -0
- package/mcp_server/tools/planner.py +144 -0
- package/mcp_server/tools/research.py +223 -0
- package/mcp_server/tools/tracks.py +18 -3
- package/mcp_server/tools/transport.py +10 -2
- package/mcp_server/transition_engine/__init__.py +6 -0
- package/mcp_server/transition_engine/archetypes.py +167 -0
- package/mcp_server/transition_engine/critics.py +340 -0
- package/mcp_server/transition_engine/models.py +90 -0
- package/mcp_server/transition_engine/tools.py +291 -0
- package/mcp_server/translation_engine/__init__.py +5 -0
- package/mcp_server/translation_engine/critics.py +297 -0
- package/mcp_server/translation_engine/models.py +27 -0
- package/mcp_server/translation_engine/tools.py +74 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/arrangement.py +12 -2
- package/requirements.txt +1 -1
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
"""Planner Engine — loop-to-song arrangement planning and orchestration.
|
|
2
|
+
|
|
3
|
+
Transforms a single loop analysis into a full arrangement blueprint:
|
|
4
|
+
section sequence, element reveal order, gesture automation, and
|
|
5
|
+
orchestration plan.
|
|
6
|
+
|
|
7
|
+
Zero external dependencies beyond stdlib. The MCP tool wrappers in
|
|
8
|
+
planner.py handle data fetching; this module handles computation.
|
|
9
|
+
|
|
10
|
+
Design: spec at docs/specs/2026-04-08-phase2-4-roadmap.md, Round 3 (3.3).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import asdict, dataclass, field
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from ._composition_engine import (
|
|
19
|
+
GestureIntent,
|
|
20
|
+
GesturePlan,
|
|
21
|
+
RoleNode,
|
|
22
|
+
RoleType,
|
|
23
|
+
SectionNode,
|
|
24
|
+
SectionType,
|
|
25
|
+
plan_gesture,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ── Section Templates ────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
# Prototypical section sequences by style. Each entry:
|
|
32
|
+
# (section_type, energy_target, density_target, typical_bars)
|
|
33
|
+
STYLE_TEMPLATES: dict[str, list[tuple[SectionType, float, float, int]]] = {
|
|
34
|
+
"electronic": [
|
|
35
|
+
(SectionType.INTRO, 0.2, 0.2, 16),
|
|
36
|
+
(SectionType.VERSE, 0.5, 0.5, 16),
|
|
37
|
+
(SectionType.BUILD, 0.6, 0.6, 8),
|
|
38
|
+
(SectionType.DROP, 0.9, 0.9, 16),
|
|
39
|
+
(SectionType.BREAKDOWN, 0.3, 0.3, 8),
|
|
40
|
+
(SectionType.BUILD, 0.7, 0.7, 8),
|
|
41
|
+
(SectionType.DROP, 1.0, 1.0, 16),
|
|
42
|
+
(SectionType.OUTRO, 0.2, 0.2, 16),
|
|
43
|
+
],
|
|
44
|
+
"hiphop": [
|
|
45
|
+
(SectionType.INTRO, 0.3, 0.3, 8),
|
|
46
|
+
(SectionType.VERSE, 0.6, 0.6, 16),
|
|
47
|
+
(SectionType.CHORUS, 0.8, 0.8, 8),
|
|
48
|
+
(SectionType.VERSE, 0.6, 0.6, 16),
|
|
49
|
+
(SectionType.CHORUS, 0.8, 0.8, 8),
|
|
50
|
+
(SectionType.BRIDGE, 0.5, 0.4, 8),
|
|
51
|
+
(SectionType.CHORUS, 0.9, 0.9, 8),
|
|
52
|
+
(SectionType.OUTRO, 0.3, 0.3, 8),
|
|
53
|
+
],
|
|
54
|
+
"pop": [
|
|
55
|
+
(SectionType.INTRO, 0.3, 0.3, 8),
|
|
56
|
+
(SectionType.VERSE, 0.5, 0.5, 16),
|
|
57
|
+
(SectionType.PRE_CHORUS, 0.6, 0.6, 8),
|
|
58
|
+
(SectionType.CHORUS, 0.8, 0.8, 8),
|
|
59
|
+
(SectionType.VERSE, 0.5, 0.5, 16),
|
|
60
|
+
(SectionType.PRE_CHORUS, 0.6, 0.6, 8),
|
|
61
|
+
(SectionType.CHORUS, 0.9, 0.9, 8),
|
|
62
|
+
(SectionType.BRIDGE, 0.4, 0.4, 8),
|
|
63
|
+
(SectionType.CHORUS, 1.0, 1.0, 8),
|
|
64
|
+
(SectionType.OUTRO, 0.3, 0.3, 8),
|
|
65
|
+
],
|
|
66
|
+
"ambient": [
|
|
67
|
+
(SectionType.INTRO, 0.1, 0.1, 32),
|
|
68
|
+
(SectionType.VERSE, 0.3, 0.3, 32),
|
|
69
|
+
(SectionType.VERSE, 0.5, 0.5, 32),
|
|
70
|
+
(SectionType.BREAKDOWN, 0.2, 0.2, 16),
|
|
71
|
+
(SectionType.VERSE, 0.4, 0.4, 32),
|
|
72
|
+
(SectionType.OUTRO, 0.1, 0.1, 32),
|
|
73
|
+
],
|
|
74
|
+
"techno": [
|
|
75
|
+
(SectionType.INTRO, 0.3, 0.3, 16),
|
|
76
|
+
(SectionType.VERSE, 0.6, 0.6, 32),
|
|
77
|
+
(SectionType.BUILD, 0.7, 0.7, 8),
|
|
78
|
+
(SectionType.DROP, 1.0, 1.0, 32),
|
|
79
|
+
(SectionType.BREAKDOWN, 0.3, 0.3, 16),
|
|
80
|
+
(SectionType.BUILD, 0.8, 0.8, 8),
|
|
81
|
+
(SectionType.DROP, 1.0, 1.0, 32),
|
|
82
|
+
(SectionType.OUTRO, 0.3, 0.3, 16),
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
VALID_STYLES = frozenset(STYLE_TEMPLATES.keys())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ── Loop Identity ────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class LoopIdentity:
|
|
93
|
+
"""What makes this loop recognizable — its core musical DNA."""
|
|
94
|
+
track_count: int
|
|
95
|
+
foreground_tracks: list[int] # track indices of lead/hook elements
|
|
96
|
+
rhythm_tracks: list[int] # kick, hats, perc
|
|
97
|
+
harmonic_tracks: list[int] # pads, chords, bass
|
|
98
|
+
texture_tracks: list[int] # fx, atmosphere
|
|
99
|
+
energy: float # 0-1
|
|
100
|
+
density: float # 0-1
|
|
101
|
+
estimated_bars: int
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> dict:
|
|
104
|
+
return asdict(self)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def analyze_loop_identity(
|
|
108
|
+
roles: list[RoleNode],
|
|
109
|
+
sections: list[SectionNode],
|
|
110
|
+
) -> LoopIdentity:
|
|
111
|
+
"""Identify the musical DNA of a loop from its role and section analysis.
|
|
112
|
+
|
|
113
|
+
Works with a single-section (loop) or multi-section input.
|
|
114
|
+
"""
|
|
115
|
+
# Use first section as the loop
|
|
116
|
+
section = sections[0] if sections else None
|
|
117
|
+
track_count = len(section.tracks_active) if section else 0
|
|
118
|
+
energy = section.energy if section else 0.5
|
|
119
|
+
density = section.density if section else 0.5
|
|
120
|
+
bars = section.length_bars() if section else 8
|
|
121
|
+
|
|
122
|
+
# Classify tracks by role
|
|
123
|
+
foreground = []
|
|
124
|
+
rhythm = []
|
|
125
|
+
harmonic = []
|
|
126
|
+
texture = []
|
|
127
|
+
|
|
128
|
+
section_id = section.section_id if section else ""
|
|
129
|
+
for role in roles:
|
|
130
|
+
if role.section_id != section_id:
|
|
131
|
+
continue
|
|
132
|
+
if role.role in (RoleType.LEAD, RoleType.HOOK):
|
|
133
|
+
foreground.append(role.track_index)
|
|
134
|
+
elif role.role in (RoleType.KICK_ANCHOR, RoleType.RHYTHMIC_TEXTURE):
|
|
135
|
+
rhythm.append(role.track_index)
|
|
136
|
+
elif role.role in (RoleType.BASS_ANCHOR, RoleType.HARMONY_BED):
|
|
137
|
+
harmonic.append(role.track_index)
|
|
138
|
+
elif role.role in (RoleType.TEXTURE_WASH, RoleType.TRANSITION_FX):
|
|
139
|
+
texture.append(role.track_index)
|
|
140
|
+
|
|
141
|
+
return LoopIdentity(
|
|
142
|
+
track_count=track_count,
|
|
143
|
+
foreground_tracks=foreground,
|
|
144
|
+
rhythm_tracks=rhythm,
|
|
145
|
+
harmonic_tracks=harmonic,
|
|
146
|
+
texture_tracks=texture,
|
|
147
|
+
energy=energy,
|
|
148
|
+
density=density,
|
|
149
|
+
estimated_bars=bars,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ── Arrangement Plan ─────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
@dataclass
|
|
156
|
+
class SectionPlan:
|
|
157
|
+
"""Planned section in the arrangement."""
|
|
158
|
+
section_type: SectionType
|
|
159
|
+
start_bar: int
|
|
160
|
+
end_bar: int
|
|
161
|
+
energy_target: float
|
|
162
|
+
density_target: float
|
|
163
|
+
tracks_active: list[int] # which tracks should play
|
|
164
|
+
tracks_entering: list[int] # new elements introduced in this section
|
|
165
|
+
tracks_exiting: list[int] # elements removed in this section
|
|
166
|
+
|
|
167
|
+
def length_bars(self) -> int:
|
|
168
|
+
return self.end_bar - self.start_bar
|
|
169
|
+
|
|
170
|
+
def to_dict(self) -> dict:
|
|
171
|
+
d = asdict(self)
|
|
172
|
+
d["section_type"] = self.section_type.value
|
|
173
|
+
d["length_bars"] = self.length_bars()
|
|
174
|
+
return d
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class ArrangementPlan:
|
|
179
|
+
"""Full arrangement blueprint from a loop."""
|
|
180
|
+
style: str
|
|
181
|
+
total_bars: int
|
|
182
|
+
sections: list[SectionPlan]
|
|
183
|
+
gesture_plan: list[dict] # gesture template suggestions per transition
|
|
184
|
+
reveal_order: list[dict] # [{track_index, enters_at_section, role}]
|
|
185
|
+
notes: list[str]
|
|
186
|
+
|
|
187
|
+
def to_dict(self) -> dict:
|
|
188
|
+
return {
|
|
189
|
+
"style": self.style,
|
|
190
|
+
"total_bars": self.total_bars,
|
|
191
|
+
"sections": [s.to_dict() for s in self.sections],
|
|
192
|
+
"section_count": len(self.sections),
|
|
193
|
+
"gesture_plan": self.gesture_plan,
|
|
194
|
+
"reveal_order": self.reveal_order,
|
|
195
|
+
"notes": self.notes,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ── Core Planner ─────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
def plan_arrangement_from_loop(
|
|
202
|
+
loop_identity: LoopIdentity,
|
|
203
|
+
target_duration_bars: int = 128,
|
|
204
|
+
style: str = "electronic",
|
|
205
|
+
) -> ArrangementPlan:
|
|
206
|
+
"""Transform a loop identity into a full arrangement blueprint.
|
|
207
|
+
|
|
208
|
+
1. Select section template based on style
|
|
209
|
+
2. Scale section lengths to target duration
|
|
210
|
+
3. Plan element reveal order (what enters/exits when)
|
|
211
|
+
4. Suggest gesture automation for transitions
|
|
212
|
+
"""
|
|
213
|
+
if style not in STYLE_TEMPLATES:
|
|
214
|
+
raise ValueError(f"Unknown style '{style}'. Valid: {sorted(VALID_STYLES)}")
|
|
215
|
+
|
|
216
|
+
template = STYLE_TEMPLATES[style]
|
|
217
|
+
|
|
218
|
+
# 1. Scale sections to target duration
|
|
219
|
+
template_bars = sum(s[3] for s in template)
|
|
220
|
+
scale_factor = target_duration_bars / template_bars
|
|
221
|
+
|
|
222
|
+
sections: list[SectionPlan] = []
|
|
223
|
+
current_bar = 0
|
|
224
|
+
|
|
225
|
+
for stype, energy_target, density_target, base_bars in template:
|
|
226
|
+
# Scale but keep bar counts as multiples of 4
|
|
227
|
+
scaled_bars = max(4, round(base_bars * scale_factor / 4) * 4)
|
|
228
|
+
end_bar = current_bar + scaled_bars
|
|
229
|
+
|
|
230
|
+
sections.append(SectionPlan(
|
|
231
|
+
section_type=stype,
|
|
232
|
+
start_bar=current_bar,
|
|
233
|
+
end_bar=end_bar,
|
|
234
|
+
energy_target=energy_target,
|
|
235
|
+
density_target=density_target,
|
|
236
|
+
tracks_active=[], # Filled in by reveal planning
|
|
237
|
+
tracks_entering=[],
|
|
238
|
+
tracks_exiting=[],
|
|
239
|
+
))
|
|
240
|
+
current_bar = end_bar
|
|
241
|
+
|
|
242
|
+
# 2. Plan element reveal order
|
|
243
|
+
all_tracks = (
|
|
244
|
+
loop_identity.rhythm_tracks
|
|
245
|
+
+ loop_identity.harmonic_tracks
|
|
246
|
+
+ loop_identity.foreground_tracks
|
|
247
|
+
+ loop_identity.texture_tracks
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
reveal_order = _plan_reveal_order(
|
|
251
|
+
sections, loop_identity, all_tracks,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Apply reveal order to section active tracks
|
|
255
|
+
active_set: set[int] = set()
|
|
256
|
+
for i, section in enumerate(sections):
|
|
257
|
+
# Add entering tracks
|
|
258
|
+
for entry in reveal_order:
|
|
259
|
+
if entry["enters_at_section"] == i:
|
|
260
|
+
active_set.add(entry["track_index"])
|
|
261
|
+
section.tracks_entering.append(entry["track_index"])
|
|
262
|
+
|
|
263
|
+
# Remove exiting tracks (for breakdowns/bridges)
|
|
264
|
+
if section.section_type in (SectionType.BREAKDOWN, SectionType.BRIDGE):
|
|
265
|
+
# Keep only rhythm + harmony (strip foreground + texture)
|
|
266
|
+
to_remove = [t for t in active_set
|
|
267
|
+
if t in loop_identity.foreground_tracks
|
|
268
|
+
or t in loop_identity.texture_tracks]
|
|
269
|
+
for t in to_remove:
|
|
270
|
+
active_set.discard(t)
|
|
271
|
+
section.tracks_exiting.append(t)
|
|
272
|
+
|
|
273
|
+
section.tracks_active = sorted(active_set)
|
|
274
|
+
|
|
275
|
+
# 3. Suggest gesture templates for transitions
|
|
276
|
+
gesture_plan = _plan_transition_gestures(sections)
|
|
277
|
+
|
|
278
|
+
# 4. Notes
|
|
279
|
+
total_bars = sections[-1].end_bar if sections else 0
|
|
280
|
+
notes = []
|
|
281
|
+
if total_bars != target_duration_bars:
|
|
282
|
+
notes.append(f"Actual duration: {total_bars} bars (target was {target_duration_bars})")
|
|
283
|
+
if not loop_identity.foreground_tracks:
|
|
284
|
+
notes.append("No clear foreground element detected — consider adding a lead/hook")
|
|
285
|
+
if not loop_identity.rhythm_tracks:
|
|
286
|
+
notes.append("No rhythm tracks detected — arrangement may feel floaty without groove")
|
|
287
|
+
|
|
288
|
+
return ArrangementPlan(
|
|
289
|
+
style=style,
|
|
290
|
+
total_bars=total_bars,
|
|
291
|
+
sections=sections,
|
|
292
|
+
gesture_plan=gesture_plan,
|
|
293
|
+
reveal_order=reveal_order,
|
|
294
|
+
notes=notes,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _plan_reveal_order(
|
|
299
|
+
sections: list[SectionPlan],
|
|
300
|
+
loop_identity: LoopIdentity,
|
|
301
|
+
all_tracks: list[int],
|
|
302
|
+
) -> list[dict]:
|
|
303
|
+
"""Plan when each track enters the arrangement.
|
|
304
|
+
|
|
305
|
+
Strategy: stagger element introductions for maximum impact.
|
|
306
|
+
- Intro: minimal (rhythm only, or partial rhythm)
|
|
307
|
+
- First verse/section: add bass + harmony
|
|
308
|
+
- Build/pre-chorus: add texture
|
|
309
|
+
- Drop/chorus: full reveal (foreground enters)
|
|
310
|
+
"""
|
|
311
|
+
if not all_tracks or not sections:
|
|
312
|
+
return []
|
|
313
|
+
|
|
314
|
+
reveal: list[dict] = []
|
|
315
|
+
assigned: set[int] = set()
|
|
316
|
+
|
|
317
|
+
# Group tracks by priority
|
|
318
|
+
groups = [
|
|
319
|
+
("rhythm_foundation", loop_identity.rhythm_tracks[:2]), # Kick + one more
|
|
320
|
+
("harmonic_base", loop_identity.harmonic_tracks[:2]), # Bass + pad
|
|
321
|
+
("texture_layer", loop_identity.texture_tracks),
|
|
322
|
+
("foreground_reveal", loop_identity.foreground_tracks),
|
|
323
|
+
("remaining_rhythm", loop_identity.rhythm_tracks[2:]),
|
|
324
|
+
("remaining_harmony", loop_identity.harmonic_tracks[2:]),
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
# Map groups to section types (when they should enter)
|
|
328
|
+
entry_map: dict[str, list[SectionType]] = {
|
|
329
|
+
"rhythm_foundation": [SectionType.INTRO],
|
|
330
|
+
"harmonic_base": [SectionType.VERSE],
|
|
331
|
+
"texture_layer": [SectionType.BUILD, SectionType.PRE_CHORUS, SectionType.VERSE],
|
|
332
|
+
"foreground_reveal": [SectionType.DROP, SectionType.CHORUS],
|
|
333
|
+
"remaining_rhythm": [SectionType.VERSE, SectionType.BUILD],
|
|
334
|
+
"remaining_harmony": [SectionType.BUILD, SectionType.PRE_CHORUS],
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
for group_name, tracks in groups:
|
|
338
|
+
target_types = entry_map.get(group_name, [SectionType.VERSE])
|
|
339
|
+
|
|
340
|
+
# Find first section matching target type
|
|
341
|
+
target_section_idx = 0
|
|
342
|
+
for i, section in enumerate(sections):
|
|
343
|
+
if section.section_type in target_types:
|
|
344
|
+
target_section_idx = i
|
|
345
|
+
break
|
|
346
|
+
|
|
347
|
+
for track in tracks:
|
|
348
|
+
if track in assigned:
|
|
349
|
+
continue
|
|
350
|
+
reveal.append({
|
|
351
|
+
"track_index": track,
|
|
352
|
+
"enters_at_section": target_section_idx,
|
|
353
|
+
"group": group_name,
|
|
354
|
+
})
|
|
355
|
+
assigned.add(track)
|
|
356
|
+
|
|
357
|
+
# Any remaining unassigned tracks enter at section 1
|
|
358
|
+
for track in all_tracks:
|
|
359
|
+
if track not in assigned:
|
|
360
|
+
reveal.append({
|
|
361
|
+
"track_index": track,
|
|
362
|
+
"enters_at_section": min(1, len(sections) - 1),
|
|
363
|
+
"group": "unassigned",
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
return reveal
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _plan_transition_gestures(sections: list[SectionPlan]) -> list[dict]:
|
|
370
|
+
"""Suggest gesture templates for each section transition."""
|
|
371
|
+
gestures = []
|
|
372
|
+
|
|
373
|
+
for i in range(1, len(sections)):
|
|
374
|
+
prev = sections[i - 1]
|
|
375
|
+
curr = sections[i]
|
|
376
|
+
|
|
377
|
+
suggestion = {
|
|
378
|
+
"transition": f"{prev.section_type.value} → {curr.section_type.value}",
|
|
379
|
+
"boundary_bar": curr.start_bar,
|
|
380
|
+
"templates": [],
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
# Select templates based on transition type
|
|
384
|
+
energy_increase = curr.energy_target > prev.energy_target + 0.15
|
|
385
|
+
|
|
386
|
+
if curr.section_type in (SectionType.DROP, SectionType.CHORUS) and energy_increase:
|
|
387
|
+
suggestion["templates"].append("pre_arrival_vacuum")
|
|
388
|
+
if curr.tracks_entering:
|
|
389
|
+
suggestion["templates"].append("re_entry_spotlight")
|
|
390
|
+
|
|
391
|
+
elif curr.section_type == SectionType.BUILD:
|
|
392
|
+
suggestion["templates"].append("tension_ratchet")
|
|
393
|
+
|
|
394
|
+
elif curr.section_type in (SectionType.BREAKDOWN, SectionType.BRIDGE):
|
|
395
|
+
suggestion["templates"].append("sectional_width_bloom")
|
|
396
|
+
|
|
397
|
+
elif curr.section_type == SectionType.OUTRO:
|
|
398
|
+
suggestion["templates"].append("outro_decay_dissolve")
|
|
399
|
+
|
|
400
|
+
elif curr.section_type == SectionType.VERSE and prev.section_type == SectionType.CHORUS:
|
|
401
|
+
suggestion["templates"].append("turnaround_accent")
|
|
402
|
+
|
|
403
|
+
else:
|
|
404
|
+
# Generic transition
|
|
405
|
+
if energy_increase:
|
|
406
|
+
suggestion["templates"].append("harmonic_tint_rise")
|
|
407
|
+
else:
|
|
408
|
+
suggestion["templates"].append("phrase_end_throw")
|
|
409
|
+
|
|
410
|
+
gestures.append(suggestion)
|
|
411
|
+
|
|
412
|
+
return gestures
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# ── Orchestration Planner (Round 4) ──────────────────────────────────
|
|
416
|
+
|
|
417
|
+
def plan_orchestration(
|
|
418
|
+
sections: list[SectionNode],
|
|
419
|
+
roles: list[RoleNode],
|
|
420
|
+
motif_count: int = 0,
|
|
421
|
+
) -> dict:
|
|
422
|
+
"""Plan which instruments play in which sections across the full arrangement.
|
|
423
|
+
|
|
424
|
+
Prevents "everything plays everywhere" syndrome by enforcing:
|
|
425
|
+
- No more than 3 foreground voices per section
|
|
426
|
+
- Bass + kick always paired
|
|
427
|
+
- Textures rotate
|
|
428
|
+
- Hook appears in chorus but not every verse
|
|
429
|
+
|
|
430
|
+
Returns: {section_id: {track_index: "active"|"silent"|"reduced"}}
|
|
431
|
+
"""
|
|
432
|
+
if not sections:
|
|
433
|
+
return {"orchestration": {}, "notes": []}
|
|
434
|
+
|
|
435
|
+
orchestration: dict[str, dict[int, str]] = {}
|
|
436
|
+
notes: list[str] = []
|
|
437
|
+
|
|
438
|
+
# Collect all track indices and their roles
|
|
439
|
+
all_tracks: set[int] = set()
|
|
440
|
+
role_map: dict[int, RoleType] = {}
|
|
441
|
+
for r in roles:
|
|
442
|
+
all_tracks.add(r.track_index)
|
|
443
|
+
role_map[r.track_index] = r.role
|
|
444
|
+
|
|
445
|
+
# Group tracks by role type
|
|
446
|
+
kick_tracks = [t for t, r in role_map.items() if r == RoleType.KICK_ANCHOR]
|
|
447
|
+
bass_tracks = [t for t, r in role_map.items() if r == RoleType.BASS_ANCHOR]
|
|
448
|
+
lead_tracks = [t for t, r in role_map.items() if r in (RoleType.LEAD, RoleType.HOOK)]
|
|
449
|
+
harmony_tracks = [t for t, r in role_map.items() if r == RoleType.HARMONY_BED]
|
|
450
|
+
texture_tracks = [t for t, r in role_map.items() if r == RoleType.TEXTURE_WASH]
|
|
451
|
+
rhythm_tracks = [t for t, r in role_map.items() if r == RoleType.RHYTHMIC_TEXTURE]
|
|
452
|
+
|
|
453
|
+
for section in sections:
|
|
454
|
+
section_orch: dict[int, str] = {}
|
|
455
|
+
stype = section.section_type
|
|
456
|
+
|
|
457
|
+
for track in all_tracks:
|
|
458
|
+
role = role_map.get(track, RoleType.UNKNOWN)
|
|
459
|
+
|
|
460
|
+
# Default: active
|
|
461
|
+
status = "active"
|
|
462
|
+
|
|
463
|
+
# Rule 1: Intros are sparse
|
|
464
|
+
if stype == SectionType.INTRO:
|
|
465
|
+
if role in (RoleType.LEAD, RoleType.HOOK):
|
|
466
|
+
status = "silent"
|
|
467
|
+
elif role == RoleType.TEXTURE_WASH:
|
|
468
|
+
status = "reduced"
|
|
469
|
+
|
|
470
|
+
# Rule 2: Breakdowns strip foreground
|
|
471
|
+
elif stype in (SectionType.BREAKDOWN, SectionType.BRIDGE):
|
|
472
|
+
if role in (RoleType.LEAD, RoleType.HOOK):
|
|
473
|
+
status = "silent"
|
|
474
|
+
elif role == RoleType.TEXTURE_WASH:
|
|
475
|
+
status = "active" # Textures shine in breakdowns
|
|
476
|
+
|
|
477
|
+
# Rule 3: Outros thin out
|
|
478
|
+
elif stype == SectionType.OUTRO:
|
|
479
|
+
if role in (RoleType.LEAD, RoleType.HOOK):
|
|
480
|
+
status = "reduced"
|
|
481
|
+
elif role == RoleType.RHYTHMIC_TEXTURE:
|
|
482
|
+
status = "reduced"
|
|
483
|
+
|
|
484
|
+
# Rule 4: Builds add tension elements but not full foreground
|
|
485
|
+
elif stype == SectionType.BUILD:
|
|
486
|
+
if role == RoleType.HOOK:
|
|
487
|
+
status = "silent" # Save hook for drop
|
|
488
|
+
|
|
489
|
+
# Rule 5: Bass+kick pairing
|
|
490
|
+
if track in bass_tracks and not any(
|
|
491
|
+
section_orch.get(k) == "active" for k in kick_tracks
|
|
492
|
+
):
|
|
493
|
+
# If no kick is active yet, bass is fine; they'll pair naturally
|
|
494
|
+
pass
|
|
495
|
+
|
|
496
|
+
section_orch[track] = status
|
|
497
|
+
|
|
498
|
+
# Rule 6: Cap foreground at 3
|
|
499
|
+
active_fg = [t for t in lead_tracks
|
|
500
|
+
if section_orch.get(t) == "active"]
|
|
501
|
+
if len(active_fg) > 3:
|
|
502
|
+
for t in active_fg[3:]:
|
|
503
|
+
section_orch[t] = "reduced"
|
|
504
|
+
notes.append(
|
|
505
|
+
f"Section '{section.name or section.section_id}': "
|
|
506
|
+
f"capped foreground from {len(active_fg)} to 3"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
orchestration[section.section_id] = section_orch
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
"orchestration": orchestration,
|
|
513
|
+
"section_count": len(sections),
|
|
514
|
+
"track_count": len(all_tracks),
|
|
515
|
+
"notes": notes,
|
|
516
|
+
}
|