livepilot 1.23.6 → 1.24.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/CHANGELOG.md +37 -0
- package/README.md +59 -13
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +17 -3
- package/mcp_server/audit/__init__.py +6 -0
- package/mcp_server/audit/checks.py +618 -0
- package/mcp_server/audit/tools.py +232 -0
- package/mcp_server/composer/branch_producer.py +5 -2
- package/mcp_server/composer/develop/__init__.py +19 -0
- package/mcp_server/composer/develop/apply.py +217 -0
- package/mcp_server/composer/develop/brief_builder.py +269 -0
- package/mcp_server/composer/develop/seed_introspector.py +195 -0
- package/mcp_server/composer/engine.py +15 -521
- package/mcp_server/composer/fast/__init__.py +62 -0
- package/mcp_server/composer/fast/apply.py +533 -0
- package/mcp_server/composer/fast/brief_builder.py +1479 -0
- package/mcp_server/composer/fast/tier_classification.py +159 -0
- package/mcp_server/composer/framework/__init__.py +0 -0
- package/mcp_server/composer/framework/applier.py +179 -0
- package/mcp_server/composer/framework/artist_loader.py +63 -0
- package/mcp_server/composer/framework/brief.py +79 -0
- package/mcp_server/composer/framework/event_lexicon.py +71 -0
- package/mcp_server/composer/framework/genre_loader.py +77 -0
- package/mcp_server/composer/framework/intent_source.py +137 -0
- package/mcp_server/composer/framework/knowledge_pack.py +49 -0
- package/mcp_server/composer/framework/plan_compiler.py +10 -0
- package/mcp_server/composer/full/__init__.py +10 -0
- package/mcp_server/composer/full/apply.py +1139 -0
- package/mcp_server/composer/full/brief_builder.py +144 -0
- package/mcp_server/composer/full/engine.py +541 -0
- package/mcp_server/composer/full/layer_planner.py +491 -0
- package/mcp_server/composer/layer_planner.py +19 -465
- package/mcp_server/composer/sample_resolver.py +80 -7
- package/mcp_server/composer/tools.py +626 -28
- package/mcp_server/server.py +1 -0
- package/mcp_server/splice_client/client.py +7 -0
- package/mcp_server/tools/_analyzer_engine/sample.py +162 -6
- package/mcp_server/tools/_planner_engine.py +25 -63
- package/mcp_server/tools/analyzer.py +10 -4
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/server.json +3 -3
|
@@ -1,527 +1,21 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Backward-compat shim — engine has moved to full/engine.py.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Existing code that imports from ``mcp_server.composer.engine`` continues
|
|
4
|
+
to work unchanged. New code should import from
|
|
5
|
+
``mcp_server.composer.full.engine`` directly.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
The returned plan contains only REAL tool calls with concrete params. It
|
|
9
|
-
never emits:
|
|
10
|
-
- pseudo-tools like _agent_pick_best_sample or _apply_technique
|
|
11
|
-
- placeholder strings like "{downloaded_path}"
|
|
12
|
-
- invalid sentinels like device_index: -1 or track_index: -1
|
|
13
|
-
- hardcoded clip_slot_index: 0 for tracks with no source clip
|
|
14
|
-
|
|
15
|
-
Samples are resolved at PLAN time via sample_resolver.resolve_sample_for_layer.
|
|
16
|
-
Layers that don't resolve to a concrete local file are dropped from `plan`
|
|
17
|
-
but kept in `layers` for descriptive output, and the unresolved role is
|
|
18
|
-
named in `warnings`. Processing chains use step_id + $from_step bindings
|
|
19
|
-
to bind set_device_parameter.device_index to the actual inserted device
|
|
20
|
-
position returned by insert_device.
|
|
7
|
+
This shim sets ``__file__`` to the real implementation path so that any
|
|
8
|
+
code doing ``open(engine.__file__).read()`` sees the actual source.
|
|
21
9
|
"""
|
|
10
|
+
import sys as _sys
|
|
11
|
+
import importlib as _importlib
|
|
22
12
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
from dataclasses import dataclass, field
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
from typing import Any, Optional
|
|
28
|
-
|
|
29
|
-
from .prompt_parser import CompositionIntent, parse_prompt
|
|
30
|
-
from .layer_planner import LayerSpec, plan_layers, plan_sections
|
|
31
|
-
from .sample_resolver import resolve_sample_for_layer
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# ── Result Models ──────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
@dataclass
|
|
37
|
-
class CompositionResult:
|
|
38
|
-
"""Result of a full composition run."""
|
|
39
|
-
|
|
40
|
-
intent: CompositionIntent = field(default_factory=CompositionIntent)
|
|
41
|
-
layers: list[LayerSpec] = field(default_factory=list)
|
|
42
|
-
sections: list[dict] = field(default_factory=list)
|
|
43
|
-
plan: list[dict] = field(default_factory=list) # executable steps only
|
|
44
|
-
credits_estimated: int = 0
|
|
45
|
-
dry_run: bool = False
|
|
46
|
-
warnings: list[str] = field(default_factory=list)
|
|
47
|
-
resolved_samples: dict = field(default_factory=dict) # role -> local_path
|
|
48
|
-
|
|
49
|
-
def to_dict(self) -> dict:
|
|
50
|
-
return {
|
|
51
|
-
"intent": self.intent.to_dict(),
|
|
52
|
-
"layer_count": len(self.layers),
|
|
53
|
-
"layers": [l.to_dict() for l in self.layers],
|
|
54
|
-
"sections": self.sections,
|
|
55
|
-
"plan_step_count": len(self.plan),
|
|
56
|
-
"plan": self.plan,
|
|
57
|
-
"credits_estimated": self.credits_estimated,
|
|
58
|
-
"dry_run": self.dry_run,
|
|
59
|
-
"warnings": self.warnings,
|
|
60
|
-
"resolved_samples": self.resolved_samples,
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@dataclass
|
|
65
|
-
class AugmentResult:
|
|
66
|
-
"""Result of an augmentation run."""
|
|
67
|
-
|
|
68
|
-
request: str = ""
|
|
69
|
-
intent: CompositionIntent = field(default_factory=CompositionIntent)
|
|
70
|
-
new_layers: list[LayerSpec] = field(default_factory=list)
|
|
71
|
-
plan: list[dict] = field(default_factory=list)
|
|
72
|
-
credits_estimated: int = 0
|
|
73
|
-
warnings: list[str] = field(default_factory=list)
|
|
74
|
-
resolved_samples: dict = field(default_factory=dict)
|
|
75
|
-
|
|
76
|
-
def to_dict(self) -> dict:
|
|
77
|
-
return {
|
|
78
|
-
"request": self.request,
|
|
79
|
-
"intent": self.intent.to_dict(),
|
|
80
|
-
"new_layer_count": len(self.new_layers),
|
|
81
|
-
"new_layers": [l.to_dict() for l in self.new_layers],
|
|
82
|
-
"plan_step_count": len(self.plan),
|
|
83
|
-
"plan": self.plan,
|
|
84
|
-
"credits_estimated": self.credits_estimated,
|
|
85
|
-
"warnings": self.warnings,
|
|
86
|
-
"resolved_samples": self.resolved_samples,
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# ── Step builders ──────────────────────────────────────────────────
|
|
91
|
-
|
|
92
|
-
def _step_set_tempo(tempo: int) -> dict:
|
|
93
|
-
return {
|
|
94
|
-
"tool": "set_tempo",
|
|
95
|
-
"params": {"tempo": tempo},
|
|
96
|
-
"description": f"Set tempo to {tempo} BPM",
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _step_create_midi_track(track_index: int, role: str, step_id: str) -> dict:
|
|
101
|
-
return {
|
|
102
|
-
"step_id": step_id,
|
|
103
|
-
"tool": "create_midi_track",
|
|
104
|
-
"params": {"index": track_index},
|
|
105
|
-
"description": f"Create MIDI track for {role}",
|
|
106
|
-
"role": role,
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def _step_set_track_name(track_index: int, role: str) -> dict:
|
|
111
|
-
return {
|
|
112
|
-
"tool": "set_track_name",
|
|
113
|
-
"params": {"track_index": track_index, "name": role.title()},
|
|
114
|
-
"description": f"Name track: {role.title()}",
|
|
115
|
-
"role": role,
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def _step_load_sample_to_simpler(track_index: int, layer: LayerSpec, file_path: str) -> dict:
|
|
120
|
-
return {
|
|
121
|
-
"tool": "load_sample_to_simpler",
|
|
122
|
-
"params": {"track_index": track_index, "file_path": file_path},
|
|
123
|
-
"description": f"Load sample into Simpler on track {track_index}",
|
|
124
|
-
"backend": "mcp_tool",
|
|
125
|
-
"role": layer.role,
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
# NOTE: there used to be a _step_suggest_technique helper here that emitted a
|
|
130
|
-
# `suggest_sample_technique` step into the executable plan with params
|
|
131
|
-
# {"technique_id": layer.technique_id}. This was broken: the real tool's
|
|
132
|
-
# signature is (file_path, intent, philosophy, max_suggestions) and takes
|
|
133
|
-
# no technique_id param. The step would have failed at runtime with a
|
|
134
|
-
# "required file_path missing" error.
|
|
135
|
-
#
|
|
136
|
-
# Removed in v1.10.3 (Truth Release). Technique suggestions for composer
|
|
137
|
-
# layers are now surfaced in the descriptive result output (result.layers[*].
|
|
138
|
-
# technique_id) — the agent can call suggest_sample_technique separately
|
|
139
|
-
# with the resolved sample path if it wants per-sample recipe advice. The
|
|
140
|
-
# executable plan emits only real, validated tool calls.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def _processing_steps_with_binding(
|
|
144
|
-
track_index: int,
|
|
145
|
-
layer: LayerSpec,
|
|
146
|
-
layer_idx: int,
|
|
147
|
-
) -> list[dict]:
|
|
148
|
-
"""Build insert_device + set_device_parameter pairs using step_id bindings.
|
|
149
|
-
|
|
150
|
-
Each insert_device carries a unique step_id like 'layer_0_dev_1'. The
|
|
151
|
-
following set_device_parameter steps bind their device_index param to
|
|
152
|
-
that id via $from_step — the async router resolves it to the real
|
|
153
|
-
device index returned by insert_device at runtime.
|
|
154
|
-
"""
|
|
155
|
-
steps: list[dict] = []
|
|
156
|
-
for dev_idx, device in enumerate(layer.processing):
|
|
157
|
-
device_name = device.get("name", "")
|
|
158
|
-
if not device_name:
|
|
159
|
-
continue
|
|
160
|
-
step_id = f"layer_{layer_idx}_dev_{dev_idx}"
|
|
161
|
-
steps.append({
|
|
162
|
-
"step_id": step_id,
|
|
163
|
-
"tool": "insert_device",
|
|
164
|
-
"params": {
|
|
165
|
-
"track_index": track_index,
|
|
166
|
-
"device_name": device_name,
|
|
167
|
-
},
|
|
168
|
-
"description": f"Insert {device_name} on track {track_index}",
|
|
169
|
-
"role": layer.role,
|
|
170
|
-
})
|
|
171
|
-
for param_name, param_value in device.get("params", {}).items():
|
|
172
|
-
steps.append({
|
|
173
|
-
"tool": "set_device_parameter",
|
|
174
|
-
"params": {
|
|
175
|
-
"track_index": track_index,
|
|
176
|
-
"device_index": {"$from_step": step_id, "path": "device_index"},
|
|
177
|
-
"parameter_name": param_name,
|
|
178
|
-
"value": param_value,
|
|
179
|
-
},
|
|
180
|
-
"description": f"Set {device_name} {param_name} = {param_value}",
|
|
181
|
-
"role": layer.role,
|
|
182
|
-
})
|
|
183
|
-
return steps
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def _mix_steps(track_index: int, layer: LayerSpec) -> list[dict]:
|
|
187
|
-
steps: list[dict] = []
|
|
188
|
-
# dB to linear with 0dB -> 0.85 convention (Ableton native scale)
|
|
189
|
-
linear = max(0.0, min(1.0, 10 ** (layer.volume_db / 20.0) * 0.85))
|
|
190
|
-
steps.append({
|
|
191
|
-
"tool": "set_track_volume",
|
|
192
|
-
"params": {"track_index": track_index, "volume": round(linear, 3)},
|
|
193
|
-
"description": f"Set {layer.role} volume to {layer.volume_db}dB ({linear:.3f} linear)",
|
|
194
|
-
"role": layer.role,
|
|
195
|
-
})
|
|
196
|
-
if layer.pan != 0.0:
|
|
197
|
-
steps.append({
|
|
198
|
-
"tool": "set_track_pan",
|
|
199
|
-
"params": {"track_index": track_index, "pan": layer.pan},
|
|
200
|
-
"description": f"Set {layer.role} pan to {layer.pan}",
|
|
201
|
-
"role": layer.role,
|
|
202
|
-
})
|
|
203
|
-
return steps
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def _arrangement_steps(
|
|
207
|
-
track_index: int,
|
|
208
|
-
layer: LayerSpec,
|
|
209
|
-
sections: list[dict],
|
|
210
|
-
) -> list[dict]:
|
|
211
|
-
"""Emit the full arrangement sequence for a layer.
|
|
212
|
-
|
|
213
|
-
For each layer that appears in at least one section, we emit:
|
|
214
|
-
|
|
215
|
-
1. create_clip — a 1-bar MIDI clip in session slot 0 (the source)
|
|
216
|
-
2. add_notes — a single C3 trigger note so Simpler actually sounds
|
|
217
|
-
3. create_arrangement_clip (×N) — one per section the layer is in,
|
|
218
|
-
tiling the 1-bar source across each section's bar count
|
|
219
|
-
|
|
220
|
-
The trigger-clip-plus-tile approach is intentionally minimal: Simpler
|
|
221
|
-
in classic mode plays the full sample on every note, so a single C3
|
|
222
|
-
at bar 0 is enough for a playable baseline. The suggest_sample_technique
|
|
223
|
-
step elsewhere in the plan produces a recipe the agent can use later
|
|
224
|
-
to replace the trigger pattern with something more musical.
|
|
225
|
-
"""
|
|
226
|
-
active_sections = [s for s in sections if s["name"] in layer.sections]
|
|
227
|
-
if not active_sections:
|
|
228
|
-
return []
|
|
229
|
-
|
|
230
|
-
steps: list[dict] = []
|
|
231
|
-
|
|
232
|
-
# 1. Source session clip — 1 bar = 4 beats at 4/4
|
|
233
|
-
SOURCE_SLOT = 0
|
|
234
|
-
SOURCE_BEATS = 4.0
|
|
235
|
-
steps.append({
|
|
236
|
-
"tool": "create_clip",
|
|
237
|
-
"params": {
|
|
238
|
-
"track_index": track_index,
|
|
239
|
-
"clip_index": SOURCE_SLOT,
|
|
240
|
-
"length": SOURCE_BEATS,
|
|
241
|
-
},
|
|
242
|
-
"description": f"Create 1-bar source clip for {layer.role} (slot {SOURCE_SLOT})",
|
|
243
|
-
"role": layer.role,
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
# 2. Single trigger note at beat 0 — C3 (MIDI 60), 1 beat duration.
|
|
247
|
-
# Simpler plays the full sample from this note; shorter durations
|
|
248
|
-
# are fine because Simpler doesn't gate on note-off in classic mode.
|
|
249
|
-
steps.append({
|
|
250
|
-
"tool": "add_notes",
|
|
251
|
-
"params": {
|
|
252
|
-
"track_index": track_index,
|
|
253
|
-
"clip_index": SOURCE_SLOT,
|
|
254
|
-
"notes": [{
|
|
255
|
-
"pitch": 60, # C3
|
|
256
|
-
"start_time": 0.0,
|
|
257
|
-
"duration": 1.0, # 1 beat
|
|
258
|
-
"velocity": 100,
|
|
259
|
-
}],
|
|
260
|
-
},
|
|
261
|
-
"description": f"Add C3 trigger note to {layer.role} source clip",
|
|
262
|
-
"role": layer.role,
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
# 3. One arrangement clip per section this layer appears in
|
|
266
|
-
for section in active_sections:
|
|
267
|
-
start_bar = section["start_bar"]
|
|
268
|
-
bar_count = section["bars"]
|
|
269
|
-
steps.append({
|
|
270
|
-
"tool": "create_arrangement_clip",
|
|
271
|
-
"params": {
|
|
272
|
-
"track_index": track_index,
|
|
273
|
-
"clip_slot_index": SOURCE_SLOT,
|
|
274
|
-
"start_time": float(start_bar * 4.0), # bars → beats
|
|
275
|
-
"length": float(bar_count * 4.0),
|
|
276
|
-
"loop_length": SOURCE_BEATS, # tile 1-bar source
|
|
277
|
-
},
|
|
278
|
-
"description": (
|
|
279
|
-
f"Arrange {layer.role} into '{section['name']}' "
|
|
280
|
-
f"(bar {start_bar}, {bar_count} bars)"
|
|
281
|
-
),
|
|
282
|
-
"role": layer.role,
|
|
283
|
-
"section": section["name"],
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
return steps
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
# ── Engine ─────────────────────────────────────────────────────────
|
|
290
|
-
|
|
291
|
-
class ComposerEngine:
|
|
292
|
-
"""Orchestrates the full composition pipeline.
|
|
293
|
-
|
|
294
|
-
Pure computation — returns compiled plan dicts.
|
|
295
|
-
The tool layer (tools.py) handles actual execution.
|
|
296
|
-
|
|
297
|
-
Async because sample resolution may download from Splice over gRPC.
|
|
298
|
-
Filesystem-only callers still get near-instant resolution — the resolver
|
|
299
|
-
only awaits when it actually hits the network.
|
|
300
|
-
"""
|
|
301
|
-
|
|
302
|
-
async def compose(
|
|
303
|
-
self,
|
|
304
|
-
intent: CompositionIntent,
|
|
305
|
-
dry_run: bool = False,
|
|
306
|
-
max_credits: int = 10,
|
|
307
|
-
search_roots: Optional[list] = None,
|
|
308
|
-
splice_client: object = None,
|
|
309
|
-
browser_client: object = None,
|
|
310
|
-
) -> CompositionResult:
|
|
311
|
-
"""Plan a full multi-layer composition from a CompositionIntent.
|
|
312
|
-
|
|
313
|
-
Returns a CompositionResult where `plan` contains only executable
|
|
314
|
-
steps. Unresolved layers are kept in `layers` (descriptive) but
|
|
315
|
-
dropped from `plan`, with warnings naming the unresolved roles.
|
|
316
|
-
|
|
317
|
-
splice_client is typically `ctx.lifespan_context["splice_client"]`
|
|
318
|
-
from the tool layer. When connected, its catalog is searched after
|
|
319
|
-
filesystem and remote samples are downloaded one credit at a time
|
|
320
|
-
(subject to the hard floor).
|
|
321
|
-
"""
|
|
322
|
-
result = CompositionResult(intent=intent, dry_run=dry_run)
|
|
323
|
-
|
|
324
|
-
layers = plan_layers(intent)
|
|
325
|
-
sections = plan_sections(intent)
|
|
326
|
-
result.layers = layers
|
|
327
|
-
result.sections = sections
|
|
328
|
-
result.credits_estimated = len(layers)
|
|
329
|
-
|
|
330
|
-
if result.credits_estimated > max_credits:
|
|
331
|
-
result.warnings.append(
|
|
332
|
-
f"Estimated {result.credits_estimated} credits needed, "
|
|
333
|
-
f"but budget is {max_credits}. Some layers may use "
|
|
334
|
-
f"downloaded samples or browser fallback."
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
plan: list[dict] = []
|
|
338
|
-
|
|
339
|
-
# Step 1: Tempo
|
|
340
|
-
plan.append(_step_set_tempo(intent.tempo))
|
|
341
|
-
|
|
342
|
-
# Step 2: Per-layer build, resolving samples at plan time
|
|
343
|
-
for layer_idx, layer in enumerate(layers):
|
|
344
|
-
track_index = layer_idx
|
|
345
|
-
|
|
346
|
-
file_path, source = await resolve_sample_for_layer(
|
|
347
|
-
layer,
|
|
348
|
-
search_roots=search_roots,
|
|
349
|
-
splice_client=splice_client,
|
|
350
|
-
browser_client=browser_client,
|
|
351
|
-
credit_budget=max_credits,
|
|
352
|
-
)
|
|
353
|
-
if not file_path:
|
|
354
|
-
result.warnings.append(
|
|
355
|
-
f"Unresolved sample for layer '{layer.role}' "
|
|
356
|
-
f"(query: {layer.search_query!r}). Dropped from plan."
|
|
357
|
-
)
|
|
358
|
-
continue
|
|
359
|
-
|
|
360
|
-
result.resolved_samples[layer.role] = {"path": file_path, "source": source}
|
|
361
|
-
|
|
362
|
-
track_step_id = f"layer_{layer_idx}_track"
|
|
363
|
-
plan.append(_step_create_midi_track(track_index, layer.role, track_step_id))
|
|
364
|
-
plan.append(_step_set_track_name(track_index, layer.role))
|
|
365
|
-
|
|
366
|
-
plan.append(_step_load_sample_to_simpler(track_index, layer, file_path))
|
|
367
|
-
|
|
368
|
-
# technique_id intentionally NOT emitted as an executable step —
|
|
369
|
-
# see note above _step_suggest_technique removal. layer.technique_id
|
|
370
|
-
# is still surfaced in result.layers for descriptive output.
|
|
371
|
-
|
|
372
|
-
plan.extend(_processing_steps_with_binding(track_index, layer, layer_idx))
|
|
373
|
-
plan.extend(_mix_steps(track_index, layer))
|
|
374
|
-
plan.extend(_arrangement_steps(track_index, layer, sections))
|
|
375
|
-
|
|
376
|
-
result.plan = plan
|
|
377
|
-
return result
|
|
378
|
-
|
|
379
|
-
async def augment(
|
|
380
|
-
self,
|
|
381
|
-
request: str,
|
|
382
|
-
max_credits: int = 3,
|
|
383
|
-
max_layers: int = 3,
|
|
384
|
-
search_roots: Optional[list] = None,
|
|
385
|
-
splice_client: object = None,
|
|
386
|
-
browser_client: object = None,
|
|
387
|
-
) -> AugmentResult:
|
|
388
|
-
"""Plan augmentation layers to add to an existing session.
|
|
389
|
-
|
|
390
|
-
Like compose(), resolves samples at plan time and drops unresolved
|
|
391
|
-
layers. Since the actual track count isn't known at plan time, this
|
|
392
|
-
uses track_index: -1 only for create_midi_track (where the Remote
|
|
393
|
-
Script interprets -1 as append-at-end) and then binds later steps
|
|
394
|
-
to the actual created track via $from_step — same pattern as the
|
|
395
|
-
device_index binding in compose().
|
|
396
|
-
"""
|
|
397
|
-
intent = parse_prompt(request)
|
|
398
|
-
intent.layer_count = min(intent.layer_count or max_layers, max_layers)
|
|
399
|
-
|
|
400
|
-
result = AugmentResult(request=request, intent=intent)
|
|
401
|
-
|
|
402
|
-
layers = plan_layers(intent)[:max_layers]
|
|
403
|
-
result.new_layers = layers
|
|
404
|
-
result.credits_estimated = len(layers)
|
|
405
|
-
|
|
406
|
-
if result.credits_estimated > max_credits:
|
|
407
|
-
result.warnings.append(
|
|
408
|
-
f"Estimated {result.credits_estimated} credits needed, "
|
|
409
|
-
f"but budget is {max_credits}."
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
plan: list[dict] = []
|
|
413
|
-
|
|
414
|
-
for layer_idx, layer in enumerate(layers):
|
|
415
|
-
file_path, source = await resolve_sample_for_layer(
|
|
416
|
-
layer,
|
|
417
|
-
search_roots=search_roots,
|
|
418
|
-
splice_client=splice_client,
|
|
419
|
-
browser_client=browser_client,
|
|
420
|
-
credit_budget=max_credits,
|
|
421
|
-
)
|
|
422
|
-
if not file_path:
|
|
423
|
-
result.warnings.append(
|
|
424
|
-
f"Unresolved sample for layer '{layer.role}' "
|
|
425
|
-
f"(query: {layer.search_query!r}). Dropped from plan."
|
|
426
|
-
)
|
|
427
|
-
continue
|
|
428
|
-
|
|
429
|
-
result.resolved_samples[layer.role] = {"path": file_path, "source": source}
|
|
430
|
-
|
|
431
|
-
# We don't know the absolute track index yet. create_midi_track's
|
|
432
|
-
# result carries "index" (via Remote Script) — later steps bind
|
|
433
|
-
# track_index to that via $from_step. The composer tools layer
|
|
434
|
-
# passes existing_track_count in via a hint when available.
|
|
435
|
-
track_step_id = f"aug_layer_{layer_idx}_track"
|
|
436
|
-
plan.append({
|
|
437
|
-
"step_id": track_step_id,
|
|
438
|
-
"tool": "create_midi_track",
|
|
439
|
-
"params": {"index": -1}, # append at end — Remote Script convention
|
|
440
|
-
"description": f"Create MIDI track for {layer.role}",
|
|
441
|
-
"role": layer.role,
|
|
442
|
-
})
|
|
443
|
-
|
|
444
|
-
track_ref = {"$from_step": track_step_id, "path": "index"}
|
|
445
|
-
|
|
446
|
-
plan.append({
|
|
447
|
-
"tool": "set_track_name",
|
|
448
|
-
"params": {"track_index": track_ref, "name": f"+ {layer.role.title()}"},
|
|
449
|
-
"description": f"Name new track: + {layer.role.title()}",
|
|
450
|
-
"role": layer.role,
|
|
451
|
-
})
|
|
452
|
-
|
|
453
|
-
plan.append({
|
|
454
|
-
"tool": "load_sample_to_simpler",
|
|
455
|
-
"params": {"track_index": track_ref, "file_path": file_path},
|
|
456
|
-
"description": f"Load sample into Simpler",
|
|
457
|
-
"backend": "mcp_tool",
|
|
458
|
-
"role": layer.role,
|
|
459
|
-
})
|
|
460
|
-
|
|
461
|
-
# technique_id intentionally NOT emitted (see compose() above).
|
|
462
|
-
# Surfaced in result.new_layers for descriptive output only.
|
|
463
|
-
|
|
464
|
-
for dev_idx, device in enumerate(layer.processing):
|
|
465
|
-
device_name = device.get("name", "")
|
|
466
|
-
if not device_name:
|
|
467
|
-
continue
|
|
468
|
-
dev_step_id = f"aug_layer_{layer_idx}_dev_{dev_idx}"
|
|
469
|
-
plan.append({
|
|
470
|
-
"step_id": dev_step_id,
|
|
471
|
-
"tool": "insert_device",
|
|
472
|
-
"params": {"track_index": track_ref, "device_name": device_name},
|
|
473
|
-
"description": f"Insert {device_name}",
|
|
474
|
-
"role": layer.role,
|
|
475
|
-
})
|
|
476
|
-
for param_name, param_value in device.get("params", {}).items():
|
|
477
|
-
plan.append({
|
|
478
|
-
"tool": "set_device_parameter",
|
|
479
|
-
"params": {
|
|
480
|
-
"track_index": track_ref,
|
|
481
|
-
"device_index": {"$from_step": dev_step_id, "path": "device_index"},
|
|
482
|
-
"parameter_name": param_name,
|
|
483
|
-
"value": param_value,
|
|
484
|
-
},
|
|
485
|
-
"description": f"Set {device_name} {param_name} = {param_value}",
|
|
486
|
-
"role": layer.role,
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
linear = max(0.0, min(1.0, 10 ** (layer.volume_db / 20.0) * 0.85))
|
|
490
|
-
plan.append({
|
|
491
|
-
"tool": "set_track_volume",
|
|
492
|
-
"params": {"track_index": track_ref, "volume": round(linear, 3)},
|
|
493
|
-
"description": f"Set {layer.role} volume to {layer.volume_db}dB",
|
|
494
|
-
"role": layer.role,
|
|
495
|
-
})
|
|
496
|
-
if layer.pan != 0.0:
|
|
497
|
-
plan.append({
|
|
498
|
-
"tool": "set_track_pan",
|
|
499
|
-
"params": {"track_index": track_ref, "pan": layer.pan},
|
|
500
|
-
"description": f"Set {layer.role} pan to {layer.pan}",
|
|
501
|
-
"role": layer.role,
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
result.plan = plan
|
|
505
|
-
return result
|
|
13
|
+
# Ensure full.engine is loaded
|
|
14
|
+
_full = _importlib.import_module("mcp_server.composer.full.engine")
|
|
506
15
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
search_roots: Optional[list] = None,
|
|
511
|
-
splice_client: object = None,
|
|
512
|
-
browser_client: object = None,
|
|
513
|
-
) -> dict:
|
|
514
|
-
"""Dry run — return the full composition plan without execution.
|
|
16
|
+
# Re-export everything
|
|
17
|
+
from .full.engine import * # noqa: F401, F403
|
|
18
|
+
from .full.engine import ComposerEngine, CompositionResult # explicit re-export
|
|
515
19
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
"""
|
|
519
|
-
result = await self.compose(
|
|
520
|
-
intent,
|
|
521
|
-
dry_run=True,
|
|
522
|
-
max_credits=0,
|
|
523
|
-
search_roots=search_roots,
|
|
524
|
-
splice_client=splice_client,
|
|
525
|
-
browser_client=browser_client,
|
|
526
|
-
)
|
|
527
|
-
return result.to_dict()
|
|
20
|
+
# Point __file__ at the real implementation so source-inspection tests work
|
|
21
|
+
__file__ = _full.__file__
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Fast compose mode — LLM-creative two-phase flow for 8-bar loop sketches.
|
|
2
|
+
|
|
3
|
+
Phase 1: build_fast_brief returns a creative brief (atlas-filtered instruments,
|
|
4
|
+
scale pitches, genre guidance). Phase 2 (agent): designs MIDI inline.
|
|
5
|
+
Phase 3: apply_fast_plan executes the agent-designed plan.
|
|
6
|
+
"""
|
|
7
|
+
# Re-export the public API from sub-modules
|
|
8
|
+
from .brief_builder import build_creative_brief
|
|
9
|
+
from .apply import apply_fast_plan
|
|
10
|
+
|
|
11
|
+
# Re-export all names that were previously accessible via
|
|
12
|
+
# `from mcp_server.composer import fast` on the flat fast.py module.
|
|
13
|
+
# This preserves backward compatibility for tests and tools.py.
|
|
14
|
+
from .brief_builder import (
|
|
15
|
+
GENRE_CREATIVE_GUIDANCE,
|
|
16
|
+
GENRE_KNOWLEDGE_QUERIES,
|
|
17
|
+
RECOMMENDED_OCTAVES_PER_ROLE,
|
|
18
|
+
_extract_loaded_device_names,
|
|
19
|
+
chord_at_degree,
|
|
20
|
+
degree_to_pitch,
|
|
21
|
+
detect_fresh_project,
|
|
22
|
+
get_creative_guidance,
|
|
23
|
+
get_knowledge_queries_for_role,
|
|
24
|
+
get_role_candidates,
|
|
25
|
+
is_default_track_name,
|
|
26
|
+
is_viable_instrument_uri,
|
|
27
|
+
parse_key,
|
|
28
|
+
pick_anti_defaults,
|
|
29
|
+
pick_by_role_tag,
|
|
30
|
+
pick_creative_seed,
|
|
31
|
+
pick_instrument_uri,
|
|
32
|
+
reference_artist_queries,
|
|
33
|
+
scale_pitches_in_octave,
|
|
34
|
+
simpler_role_for,
|
|
35
|
+
track_is_empty,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"build_creative_brief",
|
|
40
|
+
"apply_fast_plan",
|
|
41
|
+
"GENRE_CREATIVE_GUIDANCE",
|
|
42
|
+
"GENRE_KNOWLEDGE_QUERIES",
|
|
43
|
+
"RECOMMENDED_OCTAVES_PER_ROLE",
|
|
44
|
+
"_extract_loaded_device_names",
|
|
45
|
+
"chord_at_degree",
|
|
46
|
+
"degree_to_pitch",
|
|
47
|
+
"detect_fresh_project",
|
|
48
|
+
"get_creative_guidance",
|
|
49
|
+
"get_knowledge_queries_for_role",
|
|
50
|
+
"get_role_candidates",
|
|
51
|
+
"is_default_track_name",
|
|
52
|
+
"is_viable_instrument_uri",
|
|
53
|
+
"parse_key",
|
|
54
|
+
"pick_anti_defaults",
|
|
55
|
+
"pick_by_role_tag",
|
|
56
|
+
"pick_creative_seed",
|
|
57
|
+
"pick_instrument_uri",
|
|
58
|
+
"reference_artist_queries",
|
|
59
|
+
"scale_pitches_in_octave",
|
|
60
|
+
"simpler_role_for",
|
|
61
|
+
"track_is_empty",
|
|
62
|
+
]
|