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,452 @@
1
+ """ComposerEngine — orchestrate prompt → layers → compiled plan.
2
+
3
+ Pure computation engine. Does NOT call MCP tools directly.
4
+ Returns compiled plan dicts that the tool layer (tools.py) executes.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Optional
11
+
12
+ from .prompt_parser import CompositionIntent, parse_prompt
13
+ from .layer_planner import LayerSpec, plan_layers, plan_sections
14
+
15
+
16
+ # ── Result Models ──────────────────────────────────────────────────
17
+
18
+ @dataclass
19
+ class CompositionResult:
20
+ """Result of a full composition run."""
21
+
22
+ intent: CompositionIntent = field(default_factory=CompositionIntent)
23
+ layers: list[LayerSpec] = field(default_factory=list)
24
+ sections: list[dict] = field(default_factory=list)
25
+ plan: list[dict] = field(default_factory=list) # compiled execution steps
26
+ credits_estimated: int = 0
27
+ dry_run: bool = False
28
+ warnings: list[str] = field(default_factory=list)
29
+
30
+ def to_dict(self) -> dict:
31
+ return {
32
+ "intent": self.intent.to_dict(),
33
+ "layer_count": len(self.layers),
34
+ "layers": [l.to_dict() for l in self.layers],
35
+ "sections": self.sections,
36
+ "plan_step_count": len(self.plan),
37
+ "plan": self.plan,
38
+ "credits_estimated": self.credits_estimated,
39
+ "dry_run": self.dry_run,
40
+ "warnings": self.warnings,
41
+ }
42
+
43
+
44
+ @dataclass
45
+ class AugmentResult:
46
+ """Result of an augmentation run."""
47
+
48
+ request: str = ""
49
+ intent: CompositionIntent = field(default_factory=CompositionIntent)
50
+ new_layers: list[LayerSpec] = field(default_factory=list)
51
+ plan: list[dict] = field(default_factory=list)
52
+ credits_estimated: int = 0
53
+ warnings: list[str] = field(default_factory=list)
54
+
55
+ def to_dict(self) -> dict:
56
+ return {
57
+ "request": self.request,
58
+ "intent": self.intent.to_dict(),
59
+ "new_layer_count": len(self.new_layers),
60
+ "new_layers": [l.to_dict() for l in self.new_layers],
61
+ "plan_step_count": len(self.plan),
62
+ "plan": self.plan,
63
+ "credits_estimated": self.credits_estimated,
64
+ "warnings": self.warnings,
65
+ }
66
+
67
+
68
+ # ── Compiled Step Builders ─────────────────────────────────────────
69
+
70
+ def _compile_set_tempo_step(tempo: int) -> dict:
71
+ """Compile a set_tempo step."""
72
+ return {
73
+ "tool": "set_tempo",
74
+ "params": {"tempo": tempo},
75
+ "description": f"Set tempo to {tempo} BPM",
76
+ }
77
+
78
+
79
+ def _compile_create_track_step(track_index: int, role: str) -> dict:
80
+ """Compile a create_midi_track step."""
81
+ return {
82
+ "tool": "create_midi_track",
83
+ "params": {"index": track_index},
84
+ "description": f"Create MIDI track for {role}",
85
+ }
86
+
87
+
88
+ def _compile_name_track_step(track_index: int, role: str) -> dict:
89
+ """Compile a set_track_name step."""
90
+ return {
91
+ "tool": "set_track_name",
92
+ "params": {"track_index": track_index, "name": role.title()},
93
+ "description": f"Name track: {role.title()}",
94
+ }
95
+
96
+
97
+ def _compile_search_step(layer: LayerSpec) -> dict:
98
+ """Compile a Splice search step."""
99
+ return {
100
+ "tool": "search_samples",
101
+ "params": {
102
+ "query": layer.search_query,
103
+ "source": "splice",
104
+ "max_results": 10,
105
+ **{k: v for k, v in layer.splice_filters.items()
106
+ if k in ("key", "bpm_range", "material_type")},
107
+ },
108
+ "description": f"Search Splice: {layer.search_query}",
109
+ "role": layer.role,
110
+ }
111
+
112
+
113
+ def _compile_download_step(layer: LayerSpec) -> dict:
114
+ """Compile a Splice download step (placeholder — filled at runtime)."""
115
+ return {
116
+ "tool": "_splice_download",
117
+ "params": {"file_hash": "{best_match.file_hash}"},
118
+ "description": f"Download best match for {layer.role}",
119
+ "role": layer.role,
120
+ "conditional": True, # only if not already downloaded
121
+ }
122
+
123
+
124
+ def _compile_load_sample_step(track_index: int, layer: LayerSpec) -> dict:
125
+ """Compile a load_sample_to_simpler step."""
126
+ return {
127
+ "tool": "load_sample_to_simpler",
128
+ "params": {
129
+ "track_index": track_index,
130
+ "file_path": "{downloaded_path}",
131
+ },
132
+ "description": f"Load sample into Simpler on track {track_index}",
133
+ "role": layer.role,
134
+ }
135
+
136
+
137
+ def _compile_technique_step(track_index: int, layer: LayerSpec) -> dict:
138
+ """Compile a technique application step."""
139
+ return {
140
+ "tool": "_apply_technique",
141
+ "params": {
142
+ "track_index": track_index,
143
+ "technique_id": layer.technique_id,
144
+ },
145
+ "description": f"Apply technique '{layer.technique_id}' on track {track_index}",
146
+ "role": layer.role,
147
+ }
148
+
149
+
150
+ def _compile_processing_steps(track_index: int, layer: LayerSpec) -> list[dict]:
151
+ """Compile device insertion and parameter setting steps."""
152
+ steps: list[dict] = []
153
+ for i, device in enumerate(layer.processing):
154
+ device_name = device.get("name", "")
155
+ steps.append({
156
+ "tool": "insert_device",
157
+ "params": {
158
+ "track_index": track_index,
159
+ "device_name": device_name,
160
+ },
161
+ "description": f"Insert {device_name} on track {track_index}",
162
+ "role": layer.role,
163
+ })
164
+ # Parameter setting
165
+ for param_name, param_value in device.get("params", {}).items():
166
+ steps.append({
167
+ "tool": "set_device_parameter",
168
+ "params": {
169
+ "track_index": track_index,
170
+ "device_index": -1, # last inserted device
171
+ "parameter_name": param_name,
172
+ "value": param_value,
173
+ },
174
+ "description": f"Set {device_name} {param_name} = {param_value}",
175
+ "role": layer.role,
176
+ })
177
+ return steps
178
+
179
+
180
+ def _compile_mix_steps(track_index: int, layer: LayerSpec) -> list[dict]:
181
+ """Compile volume and pan steps."""
182
+ steps = []
183
+ steps.append({
184
+ "tool": "set_track_volume",
185
+ "params": {"track_index": track_index, "volume_db": layer.volume_db},
186
+ "description": f"Set {layer.role} volume to {layer.volume_db}dB",
187
+ "role": layer.role,
188
+ })
189
+ if layer.pan != 0.0:
190
+ steps.append({
191
+ "tool": "set_track_pan",
192
+ "params": {"track_index": track_index, "pan": layer.pan},
193
+ "description": f"Set {layer.role} pan to {layer.pan}",
194
+ "role": layer.role,
195
+ })
196
+ return steps
197
+
198
+
199
+ def _compile_arrangement_steps(
200
+ track_index: int,
201
+ layer: LayerSpec,
202
+ sections: list[dict],
203
+ ) -> list[dict]:
204
+ """Compile arrangement clip creation steps for the layer's sections."""
205
+ steps: list[dict] = []
206
+
207
+ for section in sections:
208
+ if section["name"] not in layer.sections:
209
+ continue
210
+
211
+ # Check for volume offset in section layer refs (e.g. "drums:-6dB")
212
+ volume_offset_db = 0.0
213
+ for layer_ref in section.get("layers", []):
214
+ parts = layer_ref.split(":")
215
+ if parts[0] == layer.role and len(parts) > 1:
216
+ try:
217
+ volume_offset_db = float(parts[1].replace("dB", ""))
218
+ except ValueError:
219
+ pass
220
+
221
+ start_bar = section["start_bar"]
222
+ bar_count = section["bars"]
223
+
224
+ steps.append({
225
+ "tool": "create_arrangement_clip",
226
+ "params": {
227
+ "track_index": track_index,
228
+ "start_time": start_bar * 4.0, # bars → beats (4/4)
229
+ "length": bar_count * 4.0,
230
+ },
231
+ "description": f"Create arrangement clip: {layer.role} in {section['name']} "
232
+ f"(bar {start_bar}, {bar_count} bars)",
233
+ "role": layer.role,
234
+ "section": section["name"],
235
+ })
236
+
237
+ # Add volume automation at section boundaries if there's an offset
238
+ if volume_offset_db != 0.0:
239
+ steps.append({
240
+ "tool": "set_arrangement_automation",
241
+ "params": {
242
+ "track_index": track_index,
243
+ "parameter_name": "Volume",
244
+ "time": start_bar * 4.0,
245
+ "value": layer.volume_db + volume_offset_db,
246
+ },
247
+ "description": f"Automate volume fade: {layer.role} at {volume_offset_db}dB "
248
+ f"in {section['name']}",
249
+ "role": layer.role,
250
+ "section": section["name"],
251
+ })
252
+
253
+ return steps
254
+
255
+
256
+ # ── Engine ─────────────────────────────────────────────────────────
257
+
258
+ class ComposerEngine:
259
+ """Orchestrates the full composition pipeline.
260
+
261
+ Pure computation — returns compiled plan dicts.
262
+ The tool layer (tools.py) handles actual execution.
263
+ """
264
+
265
+ def compose(
266
+ self,
267
+ intent: CompositionIntent,
268
+ dry_run: bool = False,
269
+ max_credits: int = 10,
270
+ ) -> CompositionResult:
271
+ """Plan a full multi-layer composition from a CompositionIntent.
272
+
273
+ Returns a CompositionResult with compiled execution steps.
274
+ """
275
+ result = CompositionResult(
276
+ intent=intent,
277
+ dry_run=dry_run,
278
+ )
279
+
280
+ # Plan layers and sections
281
+ layers = plan_layers(intent)
282
+ sections = plan_sections(intent)
283
+ result.layers = layers
284
+ result.sections = sections
285
+
286
+ # Estimate credits needed (1 per non-downloaded layer)
287
+ credits_needed = len(layers)
288
+ result.credits_estimated = credits_needed
289
+
290
+ if credits_needed > max_credits:
291
+ result.warnings.append(
292
+ f"Estimated {credits_needed} credits needed, "
293
+ f"but budget is {max_credits}. Some layers may use "
294
+ f"downloaded samples or browser fallback."
295
+ )
296
+
297
+ # Compile the execution plan
298
+ plan: list[dict] = []
299
+
300
+ # Step 1: Set tempo
301
+ plan.append(_compile_set_tempo_step(intent.tempo))
302
+
303
+ # Step 2: Create tracks and layers
304
+ for track_idx, layer in enumerate(layers):
305
+ # Create track
306
+ plan.append(_compile_create_track_step(track_idx, layer.role))
307
+ plan.append(_compile_name_track_step(track_idx, layer.role))
308
+
309
+ # Search for sample
310
+ plan.append(_compile_search_step(layer))
311
+
312
+ # Download if needed
313
+ plan.append(_compile_download_step(layer))
314
+
315
+ # Load into Simpler
316
+ plan.append(_compile_load_sample_step(track_idx, layer))
317
+
318
+ # Apply technique
319
+ if layer.technique_id:
320
+ plan.append(_compile_technique_step(track_idx, layer))
321
+
322
+ # Insert processing devices
323
+ plan.extend(_compile_processing_steps(track_idx, layer))
324
+
325
+ # Set mix levels
326
+ plan.extend(_compile_mix_steps(track_idx, layer))
327
+
328
+ # Arrange into sections
329
+ plan.extend(_compile_arrangement_steps(track_idx, layer, sections))
330
+
331
+ result.plan = plan
332
+ return result
333
+
334
+ def augment(
335
+ self,
336
+ request: str,
337
+ max_credits: int = 3,
338
+ max_layers: int = 3,
339
+ ) -> AugmentResult:
340
+ """Plan augmentation layers to add to an existing session.
341
+
342
+ Parses the request as a composition prompt but limits to max_layers.
343
+ """
344
+ intent = parse_prompt(request)
345
+
346
+ # Override layer count to respect max_layers
347
+ intent.layer_count = min(intent.layer_count or max_layers, max_layers)
348
+
349
+ result = AugmentResult(
350
+ request=request,
351
+ intent=intent,
352
+ )
353
+
354
+ # Plan layers
355
+ layers = plan_layers(intent)
356
+ # Limit to max_layers
357
+ layers = layers[:max_layers]
358
+ result.new_layers = layers
359
+
360
+ # Estimate credits
361
+ result.credits_estimated = len(layers)
362
+
363
+ if result.credits_estimated > max_credits:
364
+ result.warnings.append(
365
+ f"Estimated {result.credits_estimated} credits needed, "
366
+ f"but budget is {max_credits}."
367
+ )
368
+
369
+ # Compile augmentation plan
370
+ # Track indices start from a placeholder — the tool layer will
371
+ # determine the actual track offset at runtime
372
+ plan: list[dict] = []
373
+ for offset, layer in enumerate(layers):
374
+ track_placeholder = f"{{existing_track_count}} + {offset}"
375
+
376
+ plan.append({
377
+ "tool": "create_midi_track",
378
+ "params": {"index": -1}, # append at end
379
+ "description": f"Create MIDI track for {layer.role}",
380
+ "role": layer.role,
381
+ })
382
+
383
+ plan.append({
384
+ "tool": "set_track_name",
385
+ "params": {"track_index": -1, "name": f"+ {layer.role.title()}"},
386
+ "description": f"Name new track: + {layer.role.title()}",
387
+ "role": layer.role,
388
+ })
389
+
390
+ plan.append(_compile_search_step(layer))
391
+ plan.append(_compile_download_step(layer))
392
+
393
+ plan.append({
394
+ "tool": "load_sample_to_simpler",
395
+ "params": {"track_index": -1, "file_path": "{downloaded_path}"},
396
+ "description": f"Load sample into Simpler",
397
+ "role": layer.role,
398
+ })
399
+
400
+ if layer.technique_id:
401
+ plan.append({
402
+ "tool": "_apply_technique",
403
+ "params": {"track_index": -1, "technique_id": layer.technique_id},
404
+ "description": f"Apply technique '{layer.technique_id}'",
405
+ "role": layer.role,
406
+ })
407
+
408
+ for device in layer.processing:
409
+ device_name = device.get("name", "")
410
+ plan.append({
411
+ "tool": "insert_device",
412
+ "params": {"track_index": -1, "device_name": device_name},
413
+ "description": f"Insert {device_name}",
414
+ "role": layer.role,
415
+ })
416
+ for param_name, param_value in device.get("params", {}).items():
417
+ plan.append({
418
+ "tool": "set_device_parameter",
419
+ "params": {
420
+ "track_index": -1,
421
+ "device_index": -1,
422
+ "parameter_name": param_name,
423
+ "value": param_value,
424
+ },
425
+ "description": f"Set {device_name} {param_name} = {param_value}",
426
+ "role": layer.role,
427
+ })
428
+
429
+ plan.append({
430
+ "tool": "set_track_volume",
431
+ "params": {"track_index": -1, "volume_db": layer.volume_db},
432
+ "description": f"Set {layer.role} volume to {layer.volume_db}dB",
433
+ "role": layer.role,
434
+ })
435
+ if layer.pan != 0.0:
436
+ plan.append({
437
+ "tool": "set_track_pan",
438
+ "params": {"track_index": -1, "pan": layer.pan},
439
+ "description": f"Set {layer.role} pan to {layer.pan}",
440
+ "role": layer.role,
441
+ })
442
+
443
+ result.plan = plan
444
+ return result
445
+
446
+ def get_plan(
447
+ self,
448
+ intent: CompositionIntent,
449
+ ) -> dict:
450
+ """Dry run — return the full composition plan without execution."""
451
+ result = self.compose(intent, dry_run=True, max_credits=0)
452
+ return result.to_dict()