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.
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +119 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +144 -13
- package/bin/livepilot.js +87 -0
- package/installer/codex.js +147 -0
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +21 -4
- package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
- package/livepilot/skills/livepilot-core/references/overview.md +13 -9
- package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
- package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
- package/livepilot/skills/livepilot-devices/SKILL.md +16 -2
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +19 -5
- package/livepilot/skills/livepilot-sample-engine/SKILL.md +104 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +45 -0
- package/livepilot/skills/livepilot-wonder/SKILL.md +15 -0
- package/livepilot.mcpb +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/manifest.json +2 -2
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +357 -0
- package/mcp_server/atlas/device_atlas.json +44067 -0
- package/mcp_server/atlas/enrichments/__init__.py +111 -0
- package/mcp_server/atlas/enrichments/audio_effects/auto_filter.yaml +162 -0
- package/mcp_server/atlas/enrichments/audio_effects/beat_repeat.yaml +183 -0
- package/mcp_server/atlas/enrichments/audio_effects/channel_eq.yaml +126 -0
- package/mcp_server/atlas/enrichments/audio_effects/chorus_ensemble.yaml +149 -0
- package/mcp_server/atlas/enrichments/audio_effects/color_limiter.yaml +109 -0
- package/mcp_server/atlas/enrichments/audio_effects/compressor.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb.yaml +143 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb_pro.yaml +178 -0
- package/mcp_server/atlas/enrichments/audio_effects/delay.yaml +151 -0
- package/mcp_server/atlas/enrichments/audio_effects/drum_buss.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/dynamic_tube.yaml +147 -0
- package/mcp_server/atlas/enrichments/audio_effects/echo.yaml +167 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_eight.yaml +148 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_three.yaml +121 -0
- package/mcp_server/atlas/enrichments/audio_effects/erosion.yaml +103 -0
- package/mcp_server/atlas/enrichments/audio_effects/filter_delay.yaml +173 -0
- package/mcp_server/atlas/enrichments/audio_effects/gate.yaml +130 -0
- package/mcp_server/atlas/enrichments/audio_effects/gated_delay.yaml +133 -0
- package/mcp_server/atlas/enrichments/audio_effects/glue_compressor.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/grain_delay.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/hybrid_reverb.yaml +160 -0
- package/mcp_server/atlas/enrichments/audio_effects/limiter.yaml +97 -0
- package/mcp_server/atlas/enrichments/audio_effects/multiband_dynamics.yaml +174 -0
- package/mcp_server/atlas/enrichments/audio_effects/overdrive.yaml +119 -0
- package/mcp_server/atlas/enrichments/audio_effects/pedal.yaml +145 -0
- package/mcp_server/atlas/enrichments/audio_effects/phaser_flanger.yaml +161 -0
- package/mcp_server/atlas/enrichments/audio_effects/redux.yaml +114 -0
- package/mcp_server/atlas/enrichments/audio_effects/reverb.yaml +190 -0
- package/mcp_server/atlas/enrichments/audio_effects/roar.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/saturator.yaml +146 -0
- package/mcp_server/atlas/enrichments/audio_effects/shifter.yaml +154 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_resonator.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_time.yaml +164 -0
- package/mcp_server/atlas/enrichments/audio_effects/vector_delay.yaml +140 -0
- package/mcp_server/atlas/enrichments/audio_effects/vinyl_distortion.yaml +141 -0
- package/mcp_server/atlas/enrichments/instruments/analog.yaml +222 -0
- package/mcp_server/atlas/enrichments/instruments/bass.yaml +202 -0
- package/mcp_server/atlas/enrichments/instruments/collision.yaml +150 -0
- package/mcp_server/atlas/enrichments/instruments/drift.yaml +167 -0
- package/mcp_server/atlas/enrichments/instruments/electric.yaml +137 -0
- package/mcp_server/atlas/enrichments/instruments/emit.yaml +163 -0
- package/mcp_server/atlas/enrichments/instruments/meld.yaml +164 -0
- package/mcp_server/atlas/enrichments/instruments/operator.yaml +197 -0
- package/mcp_server/atlas/enrichments/instruments/poli.yaml +192 -0
- package/mcp_server/atlas/enrichments/instruments/sampler.yaml +218 -0
- package/mcp_server/atlas/enrichments/instruments/simpler.yaml +217 -0
- package/mcp_server/atlas/enrichments/instruments/tension.yaml +156 -0
- package/mcp_server/atlas/enrichments/instruments/tree_tone.yaml +162 -0
- package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +165 -0
- package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +166 -0
- package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +162 -0
- package/mcp_server/atlas/enrichments/midi_effects/arpeggiator.yaml +156 -0
- package/mcp_server/atlas/enrichments/midi_effects/bouncy_notes.yaml +93 -0
- package/mcp_server/atlas/enrichments/midi_effects/chord.yaml +147 -0
- package/mcp_server/atlas/enrichments/midi_effects/melodic_steps.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_echo.yaml +108 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_length.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/pitch.yaml +76 -0
- package/mcp_server/atlas/enrichments/midi_effects/random.yaml +117 -0
- package/mcp_server/atlas/enrichments/midi_effects/rhythmic_steps.yaml +103 -0
- package/mcp_server/atlas/enrichments/midi_effects/scale.yaml +83 -0
- package/mcp_server/atlas/enrichments/midi_effects/step_arp.yaml +112 -0
- package/mcp_server/atlas/enrichments/midi_effects/velocity.yaml +119 -0
- package/mcp_server/atlas/enrichments/utility/amp.yaml +159 -0
- package/mcp_server/atlas/enrichments/utility/cabinet.yaml +109 -0
- package/mcp_server/atlas/enrichments/utility/corpus.yaml +150 -0
- package/mcp_server/atlas/enrichments/utility/resonators.yaml +131 -0
- package/mcp_server/atlas/enrichments/utility/spectrum.yaml +63 -0
- package/mcp_server/atlas/enrichments/utility/tuner.yaml +51 -0
- package/mcp_server/atlas/enrichments/utility/utility.yaml +136 -0
- package/mcp_server/atlas/enrichments/utility/vocoder.yaml +160 -0
- package/mcp_server/atlas/scanner.py +236 -0
- package/mcp_server/atlas/tools.py +224 -0
- package/mcp_server/composer/__init__.py +1 -0
- package/mcp_server/composer/engine.py +452 -0
- package/mcp_server/composer/layer_planner.py +427 -0
- package/mcp_server/composer/prompt_parser.py +329 -0
- package/mcp_server/composer/tools.py +201 -0
- package/mcp_server/connection.py +53 -8
- package/mcp_server/corpus/__init__.py +377 -0
- package/mcp_server/device_forge/__init__.py +1 -0
- package/mcp_server/device_forge/builder.py +377 -0
- package/mcp_server/device_forge/models.py +142 -0
- package/mcp_server/device_forge/templates.py +483 -0
- package/mcp_server/device_forge/tools.py +162 -0
- package/mcp_server/hook_hunter/analyzer.py +23 -0
- package/mcp_server/hook_hunter/models.py +1 -0
- package/mcp_server/hook_hunter/tools.py +4 -2
- package/mcp_server/m4l_bridge.py +1 -0
- package/mcp_server/memory/taste_graph.py +68 -1
- package/mcp_server/memory/tools.py +15 -4
- package/mcp_server/musical_intelligence/detectors.py +14 -1
- package/mcp_server/musical_intelligence/tools.py +11 -8
- package/mcp_server/persistence/__init__.py +1 -0
- package/mcp_server/persistence/base_store.py +82 -0
- package/mcp_server/persistence/project_store.py +106 -0
- package/mcp_server/persistence/taste_store.py +122 -0
- package/mcp_server/preview_studio/models.py +1 -0
- package/mcp_server/preview_studio/tools.py +56 -13
- package/mcp_server/runtime/capability.py +66 -0
- package/mcp_server/runtime/capability_probe.py +137 -0
- package/mcp_server/runtime/execution_router.py +143 -0
- package/mcp_server/runtime/live_version.py +102 -0
- package/mcp_server/runtime/remote_commands.py +87 -0
- package/mcp_server/runtime/tools.py +18 -4
- package/mcp_server/sample_engine/__init__.py +1 -0
- package/mcp_server/sample_engine/analyzer.py +216 -0
- package/mcp_server/sample_engine/critics.py +390 -0
- package/mcp_server/sample_engine/models.py +193 -0
- package/mcp_server/sample_engine/moves.py +127 -0
- package/mcp_server/sample_engine/planner.py +186 -0
- package/mcp_server/sample_engine/sources.py +540 -0
- package/mcp_server/sample_engine/techniques.py +908 -0
- package/mcp_server/sample_engine/tools.py +442 -0
- package/mcp_server/semantic_moves/__init__.py +3 -0
- package/mcp_server/semantic_moves/device_creation_moves.py +237 -0
- package/mcp_server/semantic_moves/mix_moves.py +41 -41
- package/mcp_server/semantic_moves/performance_moves.py +13 -13
- package/mcp_server/semantic_moves/sample_compilers.py +372 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +15 -15
- package/mcp_server/semantic_moves/tools.py +18 -17
- package/mcp_server/semantic_moves/transition_moves.py +16 -16
- package/mcp_server/server.py +51 -0
- package/mcp_server/services/__init__.py +1 -0
- package/mcp_server/services/motif_service.py +67 -0
- package/mcp_server/session_continuity/tracker.py +29 -1
- package/mcp_server/song_brain/builder.py +28 -1
- package/mcp_server/song_brain/models.py +4 -0
- package/mcp_server/song_brain/tools.py +20 -2
- package/mcp_server/sound_design/critics.py +89 -1
- package/mcp_server/splice_client/__init__.py +1 -0
- package/mcp_server/splice_client/client.py +347 -0
- package/mcp_server/splice_client/models.py +96 -0
- package/mcp_server/splice_client/protos/__init__.py +1 -0
- package/mcp_server/splice_client/protos/app_pb2.py +319 -0
- package/mcp_server/splice_client/protos/app_pb2.pyi +1153 -0
- package/mcp_server/splice_client/protos/app_pb2_grpc.py +1946 -0
- package/mcp_server/tools/arrangement.py +69 -0
- package/mcp_server/tools/automation.py +15 -2
- package/mcp_server/tools/devices.py +117 -6
- package/mcp_server/tools/notes.py +37 -4
- package/mcp_server/wonder_mode/diagnosis.py +5 -0
- package/mcp_server/wonder_mode/engine.py +85 -1
- package/mcp_server/wonder_mode/tools.py +6 -1
- package/package.json +12 -2
- package/remote_script/LivePilot/__init__.py +8 -1
- package/remote_script/LivePilot/arrangement.py +114 -0
- package/remote_script/LivePilot/browser.py +56 -1
- package/remote_script/LivePilot/devices.py +236 -6
- package/remote_script/LivePilot/mixing.py +8 -3
- package/remote_script/LivePilot/server.py +5 -1
- package/remote_script/LivePilot/transport.py +3 -0
- package/remote_script/LivePilot/version_detect.py +78 -0
- 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()
|