livepilot 1.9.23 → 1.10.0

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.
Files changed (191) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +3 -3
  3. package/CHANGELOG.md +119 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +144 -13
  6. package/bin/livepilot.js +87 -0
  7. package/installer/codex.js +147 -0
  8. package/livepilot/.Codex-plugin/plugin.json +2 -2
  9. package/livepilot/.claude-plugin/plugin.json +2 -2
  10. package/livepilot/skills/livepilot-core/SKILL.md +21 -4
  11. package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
  12. package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
  13. package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
  14. package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
  15. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
  16. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
  17. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
  18. package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
  19. package/livepilot/skills/livepilot-core/references/overview.md +13 -9
  20. package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
  21. package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
  22. package/livepilot/skills/livepilot-devices/SKILL.md +16 -2
  23. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  24. package/livepilot/skills/livepilot-release/SKILL.md +19 -5
  25. package/livepilot/skills/livepilot-sample-engine/SKILL.md +104 -0
  26. package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
  27. package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
  28. package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
  29. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +45 -0
  30. package/livepilot/skills/livepilot-wonder/SKILL.md +15 -0
  31. package/livepilot.mcpb +0 -0
  32. package/m4l_device/livepilot_bridge.js +1 -1
  33. package/manifest.json +2 -2
  34. package/mcp_server/__init__.py +1 -1
  35. package/mcp_server/atlas/__init__.py +357 -0
  36. package/mcp_server/atlas/device_atlas.json +44067 -0
  37. package/mcp_server/atlas/enrichments/__init__.py +111 -0
  38. package/mcp_server/atlas/enrichments/audio_effects/auto_filter.yaml +162 -0
  39. package/mcp_server/atlas/enrichments/audio_effects/beat_repeat.yaml +183 -0
  40. package/mcp_server/atlas/enrichments/audio_effects/channel_eq.yaml +126 -0
  41. package/mcp_server/atlas/enrichments/audio_effects/chorus_ensemble.yaml +149 -0
  42. package/mcp_server/atlas/enrichments/audio_effects/color_limiter.yaml +109 -0
  43. package/mcp_server/atlas/enrichments/audio_effects/compressor.yaml +159 -0
  44. package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb.yaml +143 -0
  45. package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb_pro.yaml +178 -0
  46. package/mcp_server/atlas/enrichments/audio_effects/delay.yaml +151 -0
  47. package/mcp_server/atlas/enrichments/audio_effects/drum_buss.yaml +142 -0
  48. package/mcp_server/atlas/enrichments/audio_effects/dynamic_tube.yaml +147 -0
  49. package/mcp_server/atlas/enrichments/audio_effects/echo.yaml +167 -0
  50. package/mcp_server/atlas/enrichments/audio_effects/eq_eight.yaml +148 -0
  51. package/mcp_server/atlas/enrichments/audio_effects/eq_three.yaml +121 -0
  52. package/mcp_server/atlas/enrichments/audio_effects/erosion.yaml +103 -0
  53. package/mcp_server/atlas/enrichments/audio_effects/filter_delay.yaml +173 -0
  54. package/mcp_server/atlas/enrichments/audio_effects/gate.yaml +130 -0
  55. package/mcp_server/atlas/enrichments/audio_effects/gated_delay.yaml +133 -0
  56. package/mcp_server/atlas/enrichments/audio_effects/glue_compressor.yaml +142 -0
  57. package/mcp_server/atlas/enrichments/audio_effects/grain_delay.yaml +141 -0
  58. package/mcp_server/atlas/enrichments/audio_effects/hybrid_reverb.yaml +160 -0
  59. package/mcp_server/atlas/enrichments/audio_effects/limiter.yaml +97 -0
  60. package/mcp_server/atlas/enrichments/audio_effects/multiband_dynamics.yaml +174 -0
  61. package/mcp_server/atlas/enrichments/audio_effects/overdrive.yaml +119 -0
  62. package/mcp_server/atlas/enrichments/audio_effects/pedal.yaml +145 -0
  63. package/mcp_server/atlas/enrichments/audio_effects/phaser_flanger.yaml +161 -0
  64. package/mcp_server/atlas/enrichments/audio_effects/redux.yaml +114 -0
  65. package/mcp_server/atlas/enrichments/audio_effects/reverb.yaml +190 -0
  66. package/mcp_server/atlas/enrichments/audio_effects/roar.yaml +159 -0
  67. package/mcp_server/atlas/enrichments/audio_effects/saturator.yaml +146 -0
  68. package/mcp_server/atlas/enrichments/audio_effects/shifter.yaml +154 -0
  69. package/mcp_server/atlas/enrichments/audio_effects/spectral_resonator.yaml +141 -0
  70. package/mcp_server/atlas/enrichments/audio_effects/spectral_time.yaml +164 -0
  71. package/mcp_server/atlas/enrichments/audio_effects/vector_delay.yaml +140 -0
  72. package/mcp_server/atlas/enrichments/audio_effects/vinyl_distortion.yaml +141 -0
  73. package/mcp_server/atlas/enrichments/instruments/analog.yaml +222 -0
  74. package/mcp_server/atlas/enrichments/instruments/bass.yaml +202 -0
  75. package/mcp_server/atlas/enrichments/instruments/collision.yaml +150 -0
  76. package/mcp_server/atlas/enrichments/instruments/drift.yaml +167 -0
  77. package/mcp_server/atlas/enrichments/instruments/electric.yaml +137 -0
  78. package/mcp_server/atlas/enrichments/instruments/emit.yaml +163 -0
  79. package/mcp_server/atlas/enrichments/instruments/meld.yaml +164 -0
  80. package/mcp_server/atlas/enrichments/instruments/operator.yaml +197 -0
  81. package/mcp_server/atlas/enrichments/instruments/poli.yaml +192 -0
  82. package/mcp_server/atlas/enrichments/instruments/sampler.yaml +218 -0
  83. package/mcp_server/atlas/enrichments/instruments/simpler.yaml +217 -0
  84. package/mcp_server/atlas/enrichments/instruments/tension.yaml +156 -0
  85. package/mcp_server/atlas/enrichments/instruments/tree_tone.yaml +162 -0
  86. package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +165 -0
  87. package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +166 -0
  88. package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +162 -0
  89. package/mcp_server/atlas/enrichments/midi_effects/arpeggiator.yaml +156 -0
  90. package/mcp_server/atlas/enrichments/midi_effects/bouncy_notes.yaml +93 -0
  91. package/mcp_server/atlas/enrichments/midi_effects/chord.yaml +147 -0
  92. package/mcp_server/atlas/enrichments/midi_effects/melodic_steps.yaml +97 -0
  93. package/mcp_server/atlas/enrichments/midi_effects/note_echo.yaml +108 -0
  94. package/mcp_server/atlas/enrichments/midi_effects/note_length.yaml +97 -0
  95. package/mcp_server/atlas/enrichments/midi_effects/pitch.yaml +76 -0
  96. package/mcp_server/atlas/enrichments/midi_effects/random.yaml +117 -0
  97. package/mcp_server/atlas/enrichments/midi_effects/rhythmic_steps.yaml +103 -0
  98. package/mcp_server/atlas/enrichments/midi_effects/scale.yaml +83 -0
  99. package/mcp_server/atlas/enrichments/midi_effects/step_arp.yaml +112 -0
  100. package/mcp_server/atlas/enrichments/midi_effects/velocity.yaml +119 -0
  101. package/mcp_server/atlas/enrichments/utility/amp.yaml +159 -0
  102. package/mcp_server/atlas/enrichments/utility/cabinet.yaml +109 -0
  103. package/mcp_server/atlas/enrichments/utility/corpus.yaml +150 -0
  104. package/mcp_server/atlas/enrichments/utility/resonators.yaml +131 -0
  105. package/mcp_server/atlas/enrichments/utility/spectrum.yaml +63 -0
  106. package/mcp_server/atlas/enrichments/utility/tuner.yaml +51 -0
  107. package/mcp_server/atlas/enrichments/utility/utility.yaml +136 -0
  108. package/mcp_server/atlas/enrichments/utility/vocoder.yaml +160 -0
  109. package/mcp_server/atlas/scanner.py +236 -0
  110. package/mcp_server/atlas/tools.py +224 -0
  111. package/mcp_server/composer/__init__.py +1 -0
  112. package/mcp_server/composer/engine.py +452 -0
  113. package/mcp_server/composer/layer_planner.py +427 -0
  114. package/mcp_server/composer/prompt_parser.py +329 -0
  115. package/mcp_server/composer/tools.py +201 -0
  116. package/mcp_server/connection.py +53 -8
  117. package/mcp_server/corpus/__init__.py +377 -0
  118. package/mcp_server/device_forge/__init__.py +1 -0
  119. package/mcp_server/device_forge/builder.py +377 -0
  120. package/mcp_server/device_forge/models.py +142 -0
  121. package/mcp_server/device_forge/templates.py +483 -0
  122. package/mcp_server/device_forge/tools.py +162 -0
  123. package/mcp_server/hook_hunter/analyzer.py +23 -0
  124. package/mcp_server/hook_hunter/models.py +1 -0
  125. package/mcp_server/hook_hunter/tools.py +4 -2
  126. package/mcp_server/m4l_bridge.py +1 -0
  127. package/mcp_server/memory/taste_graph.py +68 -1
  128. package/mcp_server/memory/tools.py +15 -4
  129. package/mcp_server/musical_intelligence/detectors.py +14 -1
  130. package/mcp_server/musical_intelligence/tools.py +11 -8
  131. package/mcp_server/persistence/__init__.py +1 -0
  132. package/mcp_server/persistence/base_store.py +82 -0
  133. package/mcp_server/persistence/project_store.py +106 -0
  134. package/mcp_server/persistence/taste_store.py +122 -0
  135. package/mcp_server/preview_studio/models.py +1 -0
  136. package/mcp_server/preview_studio/tools.py +56 -13
  137. package/mcp_server/runtime/capability.py +66 -0
  138. package/mcp_server/runtime/capability_probe.py +137 -0
  139. package/mcp_server/runtime/execution_router.py +143 -0
  140. package/mcp_server/runtime/live_version.py +102 -0
  141. package/mcp_server/runtime/remote_commands.py +87 -0
  142. package/mcp_server/runtime/tools.py +18 -4
  143. package/mcp_server/sample_engine/__init__.py +1 -0
  144. package/mcp_server/sample_engine/analyzer.py +216 -0
  145. package/mcp_server/sample_engine/critics.py +390 -0
  146. package/mcp_server/sample_engine/models.py +193 -0
  147. package/mcp_server/sample_engine/moves.py +127 -0
  148. package/mcp_server/sample_engine/planner.py +186 -0
  149. package/mcp_server/sample_engine/sources.py +540 -0
  150. package/mcp_server/sample_engine/techniques.py +908 -0
  151. package/mcp_server/sample_engine/tools.py +442 -0
  152. package/mcp_server/semantic_moves/__init__.py +3 -0
  153. package/mcp_server/semantic_moves/device_creation_moves.py +237 -0
  154. package/mcp_server/semantic_moves/mix_moves.py +41 -41
  155. package/mcp_server/semantic_moves/performance_moves.py +13 -13
  156. package/mcp_server/semantic_moves/sample_compilers.py +372 -0
  157. package/mcp_server/semantic_moves/sound_design_moves.py +15 -15
  158. package/mcp_server/semantic_moves/tools.py +18 -17
  159. package/mcp_server/semantic_moves/transition_moves.py +16 -16
  160. package/mcp_server/server.py +51 -0
  161. package/mcp_server/services/__init__.py +1 -0
  162. package/mcp_server/services/motif_service.py +67 -0
  163. package/mcp_server/session_continuity/tracker.py +29 -1
  164. package/mcp_server/song_brain/builder.py +28 -1
  165. package/mcp_server/song_brain/models.py +4 -0
  166. package/mcp_server/song_brain/tools.py +20 -2
  167. package/mcp_server/sound_design/critics.py +89 -1
  168. package/mcp_server/splice_client/__init__.py +1 -0
  169. package/mcp_server/splice_client/client.py +347 -0
  170. package/mcp_server/splice_client/models.py +96 -0
  171. package/mcp_server/splice_client/protos/__init__.py +1 -0
  172. package/mcp_server/splice_client/protos/app_pb2.py +319 -0
  173. package/mcp_server/splice_client/protos/app_pb2.pyi +1153 -0
  174. package/mcp_server/splice_client/protos/app_pb2_grpc.py +1946 -0
  175. package/mcp_server/tools/arrangement.py +69 -0
  176. package/mcp_server/tools/automation.py +15 -2
  177. package/mcp_server/tools/devices.py +117 -6
  178. package/mcp_server/tools/notes.py +37 -4
  179. package/mcp_server/wonder_mode/diagnosis.py +5 -0
  180. package/mcp_server/wonder_mode/engine.py +85 -1
  181. package/mcp_server/wonder_mode/tools.py +6 -1
  182. package/package.json +12 -2
  183. package/remote_script/LivePilot/__init__.py +8 -1
  184. package/remote_script/LivePilot/arrangement.py +114 -0
  185. package/remote_script/LivePilot/browser.py +56 -1
  186. package/remote_script/LivePilot/devices.py +236 -6
  187. package/remote_script/LivePilot/mixing.py +8 -3
  188. package/remote_script/LivePilot/server.py +5 -1
  189. package/remote_script/LivePilot/transport.py +3 -0
  190. package/remote_script/LivePilot/version_detect.py +78 -0
  191. package/scripts/sync_metadata.py +132 -0
@@ -0,0 +1,427 @@
1
+ """Layer planner — convert CompositionIntent into LayerSpec list.
2
+
3
+ Pure computation. Determines which layers to create, what to search for,
4
+ which techniques to use, and how to arrange sections. No I/O.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ from dataclasses import dataclass, field
11
+ from typing import Optional
12
+
13
+ from .prompt_parser import CompositionIntent
14
+
15
+
16
+ # ── Data Model ─────────────────────────────────────────────────────
17
+
18
+ @dataclass
19
+ class LayerSpec:
20
+ """Specification for a single layer in a composition."""
21
+
22
+ role: str # "drums", "bass", "lead", "pad", "texture", "vocal", "percussion", "fx"
23
+ search_query: str # Splice search query
24
+ splice_filters: dict = field(default_factory=dict) # key, bpm_range, genre, tags, sample_type
25
+ technique_id: str = "" # from the 29-technique library
26
+ processing: list[dict] = field(default_factory=list) # devices to add + param targets
27
+ volume_db: float = 0.0 # mix level
28
+ pan: float = 0.0 # -1.0 to 1.0
29
+ sections: list[str] = field(default_factory=list) # which arrangement sections
30
+ priority: int = 5 # download order (1=first, 10=last)
31
+
32
+ def to_dict(self) -> dict:
33
+ return {
34
+ "role": self.role,
35
+ "search_query": self.search_query,
36
+ "splice_filters": self.splice_filters,
37
+ "technique_id": self.technique_id,
38
+ "processing": self.processing,
39
+ "volume_db": self.volume_db,
40
+ "pan": self.pan,
41
+ "sections": self.sections,
42
+ "priority": self.priority,
43
+ }
44
+
45
+
46
+ # ── Role Templates ─────────────────────────────────────────────────
47
+ # role → default config used to build LayerSpec
48
+
49
+ _ROLE_TEMPLATES: dict[str, dict] = {
50
+ "drums": {
51
+ "query_template": "{genre} drums {tempo}bpm",
52
+ "sample_type": "loop",
53
+ "technique_id": "slice_and_sequence",
54
+ "processing": [
55
+ {"name": "EQ Eight", "params": {"1 Filter Type A": "highpass", "1 Frequency A": 30.0}},
56
+ {"name": "Compressor", "params": {"Threshold": -12.0, "Ratio": 4.0}},
57
+ ],
58
+ "volume_db": -3.0,
59
+ "pan": 0.0,
60
+ "priority": 1,
61
+ },
62
+ "bass": {
63
+ "query_template": "{genre} bass {key} oneshot",
64
+ "sample_type": "oneshot",
65
+ "technique_id": "key_matched_layer",
66
+ "processing": [
67
+ {"name": "Saturator", "params": {"Drive": 6.0}},
68
+ {"name": "EQ Eight", "params": {"1 Filter Type A": "highpass", "1 Frequency A": 30.0}},
69
+ ],
70
+ "volume_db": -5.0,
71
+ "pan": 0.0,
72
+ "priority": 2,
73
+ },
74
+ "lead": {
75
+ "query_template": "{genre} {mood} melody {key}",
76
+ "sample_type": "loop",
77
+ "technique_id": "counterpoint_from_chops",
78
+ "processing": [
79
+ {"name": "Auto Filter", "params": {"Frequency": 2000.0, "Resonance": 0.3}},
80
+ {"name": "Delay", "params": {"Feedback": 0.35}},
81
+ ],
82
+ "volume_db": -6.0,
83
+ "pan": 0.0,
84
+ "priority": 4,
85
+ },
86
+ "pad": {
87
+ "query_template": "{mood} pad {key}",
88
+ "sample_type": "loop",
89
+ "technique_id": "extreme_stretch",
90
+ "processing": [
91
+ {"name": "Reverb", "params": {"Decay Time": 4.0, "Dry/Wet": 0.6}},
92
+ {"name": "Chorus-Ensemble", "params": {"Rate 1": 0.5}},
93
+ ],
94
+ "volume_db": -10.0,
95
+ "pan": 0.0,
96
+ "priority": 5,
97
+ },
98
+ "texture": {
99
+ "query_template": "{mood} texture ambient",
100
+ "sample_type": "loop",
101
+ "technique_id": "granular_scatter",
102
+ "processing": [
103
+ {"name": "Grain Delay", "params": {"Frequency": 1000.0, "Dry/Wet": 0.5}},
104
+ {"name": "Reverb", "params": {"Decay Time": 6.0, "Dry/Wet": 0.7}},
105
+ ],
106
+ "volume_db": -15.0,
107
+ "pan": 0.0,
108
+ "priority": 6,
109
+ },
110
+ "vocal": {
111
+ "query_template": "vocal {mood} {key}",
112
+ "sample_type": "loop",
113
+ "technique_id": "vocal_chop_rhythm",
114
+ "processing": [
115
+ {"name": "Auto Filter", "params": {"Frequency": 3000.0}},
116
+ {"name": "Reverb", "params": {"Decay Time": 2.5, "Dry/Wet": 0.4}},
117
+ ],
118
+ "volume_db": -8.0,
119
+ "pan": 0.0,
120
+ "priority": 7,
121
+ },
122
+ "percussion": {
123
+ "query_template": "{genre} percussion loop",
124
+ "sample_type": "loop",
125
+ "technique_id": "ghost_note_texture",
126
+ "processing": [
127
+ {"name": "EQ Eight", "params": {"1 Filter Type A": "highpass", "1 Frequency A": 200.0}},
128
+ {"name": "Compressor", "params": {"Threshold": -15.0, "Ratio": 3.0}},
129
+ ],
130
+ "volume_db": -12.0,
131
+ "pan": 0.0,
132
+ "priority": 3,
133
+ },
134
+ "fx": {
135
+ "query_template": "{genre} riser fx",
136
+ "sample_type": "oneshot",
137
+ "technique_id": "one_sample_challenge",
138
+ "processing": [],
139
+ "volume_db": -6.0,
140
+ "pan": 0.0,
141
+ "priority": 8,
142
+ },
143
+ }
144
+
145
+
146
+ # ── Role Selection per Genre + Energy ──────────────────────────────
147
+ # Define which roles appear at different energy levels per genre.
148
+
149
+ _GENRE_ROLE_PRIORITY: dict[str, list[str]] = {
150
+ # Roles listed in order of priority (first added, last dropped)
151
+ "techno": ["drums", "bass", "percussion", "lead", "texture", "vocal", "fx"],
152
+ "house": ["drums", "bass", "lead", "pad", "vocal", "texture"],
153
+ "hip hop": ["drums", "bass", "lead", "vocal", "texture", "fx"],
154
+ "ambient": ["pad", "texture", "vocal", "lead", "percussion"],
155
+ "drum and bass": ["drums", "bass", "lead", "percussion", "texture", "vocal", "fx"],
156
+ "trap": ["drums", "bass", "lead", "vocal", "fx", "texture"],
157
+ "lo-fi": ["drums", "bass", "pad", "texture", "vocal"],
158
+ }
159
+
160
+ _DEFAULT_ROLE_PRIORITY = ["drums", "bass", "lead", "pad", "texture", "vocal", "percussion", "fx"]
161
+
162
+
163
+ # ── Section Templates ──────────────────────────────────────────────
164
+ # Each section: name, bar count, which roles play (with optional volume offset)
165
+
166
+ SECTION_TEMPLATES: dict[str, list[dict]] = {
167
+ "techno": [
168
+ {"name": "Intro", "bars": 8, "layers": ["drums:-6dB", "texture"]},
169
+ {"name": "Build", "bars": 8, "layers": ["drums", "bass", "percussion"]},
170
+ {"name": "Drop", "bars": 16, "layers": ["drums", "bass", "lead", "percussion", "texture"]},
171
+ {"name": "Breakdown", "bars": 8, "layers": ["texture", "vocal", "pad"]},
172
+ {"name": "Drop 2", "bars": 16, "layers": ["drums", "bass", "lead", "percussion", "vocal", "texture"]},
173
+ {"name": "Outro", "bars": 8, "layers": ["drums:-6dB", "texture", "pad"]},
174
+ ],
175
+ "house": [
176
+ {"name": "Intro", "bars": 8, "layers": ["drums:-6dB", "pad"]},
177
+ {"name": "Verse", "bars": 16, "layers": ["drums", "bass", "pad"]},
178
+ {"name": "Drop", "bars": 16, "layers": ["drums", "bass", "lead", "vocal"]},
179
+ {"name": "Breakdown", "bars": 8, "layers": ["pad", "texture", "vocal"]},
180
+ {"name": "Drop 2", "bars": 16, "layers": ["drums", "bass", "lead", "vocal", "texture"]},
181
+ {"name": "Outro", "bars": 8, "layers": ["drums:-6dB", "pad"]},
182
+ ],
183
+ "hip hop": [
184
+ {"name": "Intro", "bars": 4, "layers": ["texture"]},
185
+ {"name": "Verse", "bars": 16, "layers": ["drums", "bass", "texture"]},
186
+ {"name": "Hook", "bars": 8, "layers": ["drums", "bass", "lead", "vocal"]},
187
+ {"name": "Verse 2", "bars": 16, "layers": ["drums", "bass", "percussion", "texture"]},
188
+ {"name": "Hook 2", "bars": 8, "layers": ["drums", "bass", "lead", "vocal", "fx"]},
189
+ {"name": "Outro", "bars": 4, "layers": ["texture", "vocal:-10dB"]},
190
+ ],
191
+ "ambient": [
192
+ {"name": "Opening", "bars": 16, "layers": ["pad", "texture"]},
193
+ {"name": "Evolve", "bars": 16, "layers": ["pad", "texture", "vocal"]},
194
+ {"name": "Peak", "bars": 16, "layers": ["pad", "texture", "vocal", "lead"]},
195
+ {"name": "Dissolve", "bars": 16, "layers": ["pad", "texture"]},
196
+ ],
197
+ "drum and bass": [
198
+ {"name": "Intro", "bars": 8, "layers": ["texture", "percussion:-6dB"]},
199
+ {"name": "Build", "bars": 8, "layers": ["drums:-6dB", "bass", "percussion"]},
200
+ {"name": "Drop", "bars": 16, "layers": ["drums", "bass", "lead", "percussion", "texture"]},
201
+ {"name": "Breakdown", "bars": 8, "layers": ["texture", "vocal", "pad"]},
202
+ {"name": "Drop 2", "bars": 16, "layers": ["drums", "bass", "lead", "percussion", "vocal", "fx"]},
203
+ {"name": "Outro", "bars": 8, "layers": ["drums:-6dB", "texture"]},
204
+ ],
205
+ "trap": [
206
+ {"name": "Intro", "bars": 4, "layers": ["texture"]},
207
+ {"name": "Verse", "bars": 16, "layers": ["drums", "bass", "texture"]},
208
+ {"name": "Drop", "bars": 8, "layers": ["drums", "bass", "lead", "vocal", "fx"]},
209
+ {"name": "Verse 2", "bars": 16, "layers": ["drums", "bass", "texture", "vocal"]},
210
+ {"name": "Drop 2", "bars": 8, "layers": ["drums", "bass", "lead", "vocal", "fx"]},
211
+ {"name": "Outro", "bars": 4, "layers": ["texture:-6dB"]},
212
+ ],
213
+ "lo-fi": [
214
+ {"name": "Intro", "bars": 4, "layers": ["texture", "pad"]},
215
+ {"name": "A", "bars": 16, "layers": ["drums", "bass", "pad", "texture"]},
216
+ {"name": "B", "bars": 16, "layers": ["drums", "bass", "pad", "vocal"]},
217
+ {"name": "A2", "bars": 16, "layers": ["drums", "bass", "pad", "texture"]},
218
+ {"name": "Outro", "bars": 8, "layers": ["pad", "texture"]},
219
+ ],
220
+ }
221
+
222
+ # Fallback template for unknown genres
223
+ _DEFAULT_SECTION_TEMPLATE: list[dict] = [
224
+ {"name": "Intro", "bars": 8, "layers": ["texture"]},
225
+ {"name": "Build", "bars": 8, "layers": ["drums", "bass"]},
226
+ {"name": "Main", "bars": 16, "layers": ["drums", "bass", "lead", "texture"]},
227
+ {"name": "Breakdown", "bars": 8, "layers": ["pad", "texture"]},
228
+ {"name": "Main 2", "bars": 16, "layers": ["drums", "bass", "lead", "vocal", "texture"]},
229
+ {"name": "Outro", "bars": 8, "layers": ["drums:-6dB", "texture"]},
230
+ ]
231
+
232
+
233
+ # ── Planner Functions ──────────────────────────────────────────────
234
+
235
+ def _build_search_query(template: str, intent: CompositionIntent) -> str:
236
+ """Fill a query template with intent fields."""
237
+ return template.format(
238
+ genre=intent.genre or "electronic",
239
+ mood=intent.mood or "",
240
+ key=intent.key or "",
241
+ tempo=intent.tempo or 120,
242
+ ).strip()
243
+
244
+
245
+ def _build_splice_filters(
246
+ intent: CompositionIntent,
247
+ sample_type: str,
248
+ ) -> dict:
249
+ """Build Splice filter dict from intent."""
250
+ filters: dict = {}
251
+
252
+ # Key → Splice format (lowercase root, separate chord_type)
253
+ if intent.key:
254
+ key = intent.key
255
+ if key.endswith("m") and len(key) >= 2:
256
+ root = key[:-1].lower()
257
+ filters["chord_type"] = "minor"
258
+ else:
259
+ root = key.lower()
260
+ filters["chord_type"] = "major"
261
+ filters["key"] = root
262
+
263
+ # BPM range (+-5)
264
+ if intent.tempo:
265
+ filters["bpm_min"] = max(1, intent.tempo - 5)
266
+ filters["bpm_max"] = intent.tempo + 5
267
+
268
+ if intent.genre:
269
+ filters["genre"] = intent.genre
270
+
271
+ if sample_type:
272
+ filters["sample_type"] = sample_type
273
+
274
+ return filters
275
+
276
+
277
+ def _select_roles(intent: CompositionIntent) -> list[str]:
278
+ """Select which roles to include based on genre, energy, and explicit elements."""
279
+ role_priority = _GENRE_ROLE_PRIORITY.get(intent.genre, _DEFAULT_ROLE_PRIORITY)
280
+
281
+ # How many layers to pick
282
+ count = intent.layer_count or 5
283
+
284
+ # Start with the top N roles by priority
285
+ roles = list(role_priority[:count])
286
+
287
+ # Add any explicitly requested elements as roles
288
+ element_to_role = {
289
+ "vocal": "vocal",
290
+ "808": "bass",
291
+ "bass": "bass",
292
+ "drums": "drums",
293
+ "percussion": "percussion",
294
+ "pad": "pad",
295
+ "texture": "texture",
296
+ "fx": "fx",
297
+ "strings": "pad", # strings map to pad role
298
+ "piano": "lead", # piano maps to lead role
299
+ "guitar": "lead",
300
+ "brass": "lead",
301
+ "synth": "lead",
302
+ }
303
+
304
+ for element in intent.explicit_elements:
305
+ role = element_to_role.get(element)
306
+ if role and role not in roles:
307
+ roles.append(role)
308
+
309
+ return roles
310
+
311
+
312
+ def plan_layers(intent: CompositionIntent) -> list[LayerSpec]:
313
+ """Convert a CompositionIntent into a list of LayerSpec.
314
+
315
+ Each LayerSpec describes one track to create: what to search for,
316
+ which technique to use, processing chain, and mix settings.
317
+ """
318
+ roles = _select_roles(intent)
319
+ sections = plan_sections(intent)
320
+ section_names = [s["name"] for s in sections]
321
+
322
+ layers: list[LayerSpec] = []
323
+
324
+ for role in roles:
325
+ template = _ROLE_TEMPLATES.get(role)
326
+ if not template:
327
+ continue
328
+
329
+ # Build search query
330
+ query = _build_search_query(template["query_template"], intent)
331
+
332
+ # Add descriptors to query for richer searches
333
+ if intent.descriptors:
334
+ query += " " + " ".join(intent.descriptors[:2])
335
+
336
+ # Build Splice filters
337
+ splice_filters = _build_splice_filters(intent, template["sample_type"])
338
+
339
+ # Determine which sections this role appears in
340
+ role_sections: list[str] = []
341
+ for section in sections:
342
+ section_layers = section.get("layers", [])
343
+ for layer_ref in section_layers:
344
+ # Parse "drums:-6dB" → "drums"
345
+ layer_role = layer_ref.split(":")[0]
346
+ if layer_role == role:
347
+ role_sections.append(section["name"])
348
+ break
349
+ # If no section template match, include in all sections
350
+ if not role_sections:
351
+ role_sections = section_names
352
+
353
+ # Pan spread for stereo width
354
+ pan = _compute_pan(role, intent.energy)
355
+
356
+ layer = LayerSpec(
357
+ role=role,
358
+ search_query=query,
359
+ splice_filters=splice_filters,
360
+ technique_id=template["technique_id"],
361
+ processing=list(template["processing"]), # copy
362
+ volume_db=template["volume_db"],
363
+ pan=pan,
364
+ sections=role_sections,
365
+ priority=template["priority"],
366
+ )
367
+
368
+ layers.append(layer)
369
+
370
+ # Sort by priority (drums first, fx last)
371
+ layers.sort(key=lambda l: l.priority)
372
+
373
+ return layers
374
+
375
+
376
+ def plan_sections(intent: CompositionIntent) -> list[dict]:
377
+ """Plan arrangement sections based on genre and duration.
378
+
379
+ Returns a list of dicts: {name, bars, layers, start_bar}.
380
+ """
381
+ template = SECTION_TEMPLATES.get(intent.genre, _DEFAULT_SECTION_TEMPLATE)
382
+
383
+ # Scale sections to fit duration_bars
384
+ total_template_bars = sum(s["bars"] for s in template)
385
+ if total_template_bars == 0:
386
+ total_template_bars = 64
387
+
388
+ scale = intent.duration_bars / total_template_bars
389
+
390
+ sections: list[dict] = []
391
+ current_bar = 0
392
+
393
+ for section in template:
394
+ scaled_bars = max(4, round(section["bars"] * scale))
395
+ # Round to nearest 4 bars
396
+ scaled_bars = max(4, (scaled_bars // 4) * 4)
397
+
398
+ sections.append({
399
+ "name": section["name"],
400
+ "bars": scaled_bars,
401
+ "layers": list(section["layers"]),
402
+ "start_bar": current_bar,
403
+ })
404
+ current_bar += scaled_bars
405
+
406
+ return sections
407
+
408
+
409
+ def _compute_pan(role: str, energy: float) -> float:
410
+ """Compute pan position for a role.
411
+
412
+ Core elements (drums, bass) stay centered.
413
+ Support elements get wider spread at higher energy.
414
+ """
415
+ _PAN_MAP = {
416
+ "drums": 0.0,
417
+ "bass": 0.0,
418
+ "lead": 0.0,
419
+ "pad": 0.0,
420
+ "vocal": 0.0,
421
+ "percussion": 0.3,
422
+ "texture": -0.3,
423
+ "fx": 0.4,
424
+ }
425
+ base_pan = _PAN_MAP.get(role, 0.0)
426
+ # Widen slightly with energy
427
+ return base_pan * (0.5 + 0.5 * energy)