livepilot 1.23.6 → 1.25.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 +107 -0
- package/README.md +60 -14
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +17 -3
- package/mcp_server/atlas/explore_tools.py +332 -0
- package/mcp_server/atlas/tools.py +161 -0
- 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/atlas_resolver.py +554 -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 +140 -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 +227 -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 +172 -7
- package/mcp_server/tools/_planner_engine.py +25 -63
- package/mcp_server/tools/analyzer.py +10 -4
- package/mcp_server/tools/browser.py +102 -19
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/server.json +3 -3
|
@@ -0,0 +1,1479 @@
|
|
|
1
|
+
"""LLM-creative fast mode helpers.
|
|
2
|
+
|
|
3
|
+
Architecture (2026-05-01 redesign per user feedback):
|
|
4
|
+
Phase 1 — `compose(mode="fast")` returns a CREATIVE BRIEF: parsed intent,
|
|
5
|
+
atlas-filtered instrument suggestions per role, key/tempo
|
|
6
|
+
context, scale pitches, genre creative guidance, fresh-project
|
|
7
|
+
cleanup state. This phase does NOT generate content.
|
|
8
|
+
Phase 2 — The agent (LLM) reads the brief, picks instruments from
|
|
9
|
+
atlas-filtered suggestions, designs MIDI note patterns inline,
|
|
10
|
+
and submits a complete plan to `compose_fast_apply(plan)`.
|
|
11
|
+
Phase 3 — `compose_fast_apply` bulk-executes server-side: creates tracks,
|
|
12
|
+
loads instruments, populates clips with the LLM's notes, fires
|
|
13
|
+
scene. Same speed as the old template-based execute, but the
|
|
14
|
+
CONTENT is fresh per call.
|
|
15
|
+
|
|
16
|
+
Why this is faster than the old plan-walking compose AND more creative
|
|
17
|
+
than the old template-based fast mode:
|
|
18
|
+
- One LLM round-trip between brief and apply (vs ~16 round-trips
|
|
19
|
+
walking individual tool calls in plan-mode)
|
|
20
|
+
- LLM creativity per call (vs ~hundred bounded combinations in
|
|
21
|
+
template-mode)
|
|
22
|
+
|
|
23
|
+
What this module contains:
|
|
24
|
+
- Atlas viability filters (§1 ban, drum-keyword check, sample-less skip)
|
|
25
|
+
- Atlas tag-based instrument picker
|
|
26
|
+
- Fresh-project detection + cleanup helpers
|
|
27
|
+
- Key parser, scale-degree math (LLM's optional-use building blocks)
|
|
28
|
+
- Genre creative guidance (text hints, not templates)
|
|
29
|
+
- Brief builder
|
|
30
|
+
|
|
31
|
+
What this module deliberately does NOT contain (deleted 2026-05-01):
|
|
32
|
+
- Pattern generators (four_floor, bass_walk, progression_*, etc.)
|
|
33
|
+
- FAST_LAYER_TEMPLATES with fixed layer specs
|
|
34
|
+
- select_template / generate_notes_for_layer / _PATTERN_REGISTRY
|
|
35
|
+
|
|
36
|
+
Patterns now live in the LLM's creative output per call, not in this file.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import logging
|
|
42
|
+
import random
|
|
43
|
+
import re
|
|
44
|
+
from typing import Any, Optional
|
|
45
|
+
|
|
46
|
+
from ..prompt_parser import CompositionIntent
|
|
47
|
+
from .tier_classification import (
|
|
48
|
+
classify_instrument,
|
|
49
|
+
is_drum_specific_synth,
|
|
50
|
+
CONTAINERS_NEEDING_PRESETS,
|
|
51
|
+
MELODIC_AUDIBLE_DEFAULTS,
|
|
52
|
+
DRUM_ROLES,
|
|
53
|
+
TIER_A_CURATED_PRESET,
|
|
54
|
+
TIER_A_DRUM_SAMPLE,
|
|
55
|
+
TIER_B_DRUM_SYNTH,
|
|
56
|
+
TIER_B_AUDIBLE_DEFAULT_VALUE,
|
|
57
|
+
# Backward-compat alias — used by Tier-C guard in get_role_candidates
|
|
58
|
+
TIER_C_NEEDS_PRESET,
|
|
59
|
+
ROLE_SEARCH_TERMS,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
logger = logging.getLogger(__name__)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ── Atlas instrument viability filters ───────────────────────────────
|
|
66
|
+
|
|
67
|
+
_INSTRUMENT_URI_PREFIXES: tuple[str, ...] = (
|
|
68
|
+
"query:Synths#",
|
|
69
|
+
"query:Drums#",
|
|
70
|
+
"query:Instruments#",
|
|
71
|
+
"query:Sounds",
|
|
72
|
+
"query:UserLibrary#",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Bare empty devices that load silent until further configuration.
|
|
76
|
+
# Granular synths and bare racks that load silent until further config.
|
|
77
|
+
# Adding to this set means: the picker won't return them, and the agent
|
|
78
|
+
# never gets URIs that would produce silence.
|
|
79
|
+
_SILENT_WITHOUT_CONFIG: frozenset[str] = frozenset({
|
|
80
|
+
"Granulator III",
|
|
81
|
+
"Vector Grain", # BUG-M (2026-05-01 live test): same class as Granulator III
|
|
82
|
+
"Looper",
|
|
83
|
+
"External Instrument",
|
|
84
|
+
"External Audio Effect",
|
|
85
|
+
"Sampler",
|
|
86
|
+
"Drum Rack",
|
|
87
|
+
"DrumGroup",
|
|
88
|
+
"Instrument Rack",
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
# §1 ban — Analog/Poli/Drift/Meld are forbidden as defaults per CLAUDE.md.
|
|
92
|
+
# The LLM picking creatively from atlas suggestions still won't see these
|
|
93
|
+
# as candidates because the brief filters them out.
|
|
94
|
+
_BANNED_DEFAULT_DEVICES: frozenset[str] = frozenset({
|
|
95
|
+
"Analog", "Poli", "Drift", "Meld",
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
# Drum-role keyword check. Pattern-fired MIDI notes at drum-rack convention
|
|
99
|
+
# pitches (36/38/42) need actual drum sources. A tonal synth there produces
|
|
100
|
+
# wrong-pitched output. The LLM is expected to fire MIDI 36/38/42 for
|
|
101
|
+
# drum layers (standard convention), so the same correctness check applies.
|
|
102
|
+
_DRUM_ROLE_URI_KEYWORDS: dict[str, tuple[str, ...]] = {
|
|
103
|
+
"kick": ("Drums", "Drum", "Kick", "808", "Bd"),
|
|
104
|
+
"snare": ("Drums", "Drum", "Snare", "Clap", "Rim"),
|
|
105
|
+
"hat": ("Drums", "Drum", "Hat", "Cymbal", "Hh"),
|
|
106
|
+
"perc": ("Drums", "Drum", "Perc", "Tom", "Shaker", "Cowbell"),
|
|
107
|
+
"clap": ("Drums", "Drum", "Clap", "Snare"),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def is_viable_instrument_uri(
|
|
112
|
+
uri: str,
|
|
113
|
+
device_name: str = "",
|
|
114
|
+
role: str = "",
|
|
115
|
+
) -> bool:
|
|
116
|
+
"""True when this URI loads an audible, role-appropriate sound source."""
|
|
117
|
+
if not uri:
|
|
118
|
+
return False
|
|
119
|
+
if not any(uri.startswith(p) for p in _INSTRUMENT_URI_PREFIXES):
|
|
120
|
+
return False
|
|
121
|
+
if device_name in _SILENT_WITHOUT_CONFIG:
|
|
122
|
+
return False
|
|
123
|
+
if device_name in _BANNED_DEFAULT_DEVICES:
|
|
124
|
+
return False
|
|
125
|
+
if role in _DRUM_ROLE_URI_KEYWORDS:
|
|
126
|
+
keywords = _DRUM_ROLE_URI_KEYWORDS[role]
|
|
127
|
+
haystack = (uri + " " + device_name).lower()
|
|
128
|
+
if not any(k.lower() in haystack for k in keywords):
|
|
129
|
+
return False
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ── Atlas tag-based instrument picker (returns top-N for the brief) ──
|
|
134
|
+
|
|
135
|
+
_ROLE_TAGS: dict[str, tuple[str, ...]] = {
|
|
136
|
+
"kick": ("kick", "drum", "808"),
|
|
137
|
+
"snare": ("snare", "clap", "drum"),
|
|
138
|
+
"hat": ("hihat", "hi-hat", "hat", "cymbal"),
|
|
139
|
+
"perc": ("perc", "percussion", "drum"),
|
|
140
|
+
"clap": ("clap", "snare"),
|
|
141
|
+
"bass": ("bass", "sub_bass", "sub"),
|
|
142
|
+
"pad": ("pad", "texture", "atmos", "ambient"),
|
|
143
|
+
"lead": ("lead", "synth_lead", "pluck"),
|
|
144
|
+
"atmos": ("atmos", "ambient", "drone", "texture"),
|
|
145
|
+
"vox": ("vocal", "vox", "voice"),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def pick_instrument_uri(suggestions: list[dict], role: str = "") -> tuple[str, str]:
|
|
150
|
+
"""Walk atlas suggestions and return the first viable (uri, device_name)."""
|
|
151
|
+
for s in suggestions or []:
|
|
152
|
+
uri = s.get("uri") or ""
|
|
153
|
+
name = s.get("device_name") or ""
|
|
154
|
+
if is_viable_instrument_uri(uri, name, role):
|
|
155
|
+
return uri, name
|
|
156
|
+
return "", ""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# Per-role sonic-description queries used by the atlas_search fallback
|
|
160
|
+
# when _by_tag returns no candidates (BUG-K caught 2026-05-01: many user
|
|
161
|
+
# atlases don't tag canonical "kick"/"hat"/"bass"/"pad" — but atlas_search
|
|
162
|
+
# with sonic queries finds the same devices via character_tags / use_cases).
|
|
163
|
+
_ROLE_SONIC_QUERIES: dict[str, str] = {
|
|
164
|
+
"kick": "punchy techno kick drum sub bass anchor",
|
|
165
|
+
"snare": "snare clap drum",
|
|
166
|
+
"hat": "crisp closed hihat hi-hat metal",
|
|
167
|
+
"perc": "percussion shaker tom cowbell",
|
|
168
|
+
"clap": "clap snare drum",
|
|
169
|
+
"bass": "warm techno bass sub low end",
|
|
170
|
+
"pad": "evolving pad atmospheric texture warm",
|
|
171
|
+
"lead": "lead synth pluck arp",
|
|
172
|
+
"atmos": "drone atmosphere texture ambient",
|
|
173
|
+
"vox": "vocal voice",
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def get_role_candidates(
|
|
178
|
+
atlas: Any,
|
|
179
|
+
role: str,
|
|
180
|
+
genre: str = "",
|
|
181
|
+
top_n: int = 5,
|
|
182
|
+
exclude_names: Optional[set[str]] = None,
|
|
183
|
+
*,
|
|
184
|
+
ableton: Any = None,
|
|
185
|
+
excluded_uris: Optional[set[str]] = None,
|
|
186
|
+
) -> list[dict]:
|
|
187
|
+
"""Return up to top_n viable instrument candidates for a role.
|
|
188
|
+
|
|
189
|
+
v1.24 hunt order (Phase 4 Task 18a-followup — §1 fix):
|
|
190
|
+
Step 1: sounds/ — curated .adg/.adv presets (Tier-A, always preferred)
|
|
191
|
+
Step 1b: drums/ — raw samples for drum roles (Tier-A)
|
|
192
|
+
Step 2: atlas — Tier-B fallback:
|
|
193
|
+
- drum-specific synths (DS Kick etc.) → allowed bare for drum roles
|
|
194
|
+
- melodic synths (Operator/Wavetable etc.) → ONLY if Step 1 returned
|
|
195
|
+
nothing for a melodic role (§1 hard rule)
|
|
196
|
+
- Tier-C containers → NEVER
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
atlas: atlas object for tag/search fallback
|
|
200
|
+
role: "kick", "bass", "pad", etc.
|
|
201
|
+
genre: genre hint for sorting
|
|
202
|
+
top_n: max candidates to return
|
|
203
|
+
exclude_names: set of device names to filter (anti-repeat by name)
|
|
204
|
+
ableton: optional ableton client; if provided, Steps 1/1b fire
|
|
205
|
+
excluded_uris: set of recently-used preset URIs to filter (anti-repeat by URI)
|
|
206
|
+
|
|
207
|
+
Each candidate dict has: uri, name, tier, tags, pack, genre_affinity, source.
|
|
208
|
+
"""
|
|
209
|
+
if atlas is None and ableton is None:
|
|
210
|
+
return []
|
|
211
|
+
exclude_names = exclude_names or set()
|
|
212
|
+
excluded_uris = excluded_uris or set()
|
|
213
|
+
|
|
214
|
+
role_lower = role.lower()
|
|
215
|
+
is_drum = role_lower in DRUM_ROLES
|
|
216
|
+
candidates: list[dict] = []
|
|
217
|
+
|
|
218
|
+
# ── Step 1: sounds/ — curated presets (always Tier-A) ────────────────
|
|
219
|
+
if ableton is not None:
|
|
220
|
+
sounds_term = ROLE_SEARCH_TERMS.get(role_lower, {}).get("sounds_term", role_lower)
|
|
221
|
+
try:
|
|
222
|
+
sounds_results = ableton.send_command("search_browser", {
|
|
223
|
+
"path": "sounds", "name_filter": sounds_term,
|
|
224
|
+
"max_depth": 4, "max_results": 8, "loadable_only": True,
|
|
225
|
+
})
|
|
226
|
+
for item in (sounds_results or {}).get("items", []):
|
|
227
|
+
uri = item.get("uri", "")
|
|
228
|
+
name = item.get("name", "")
|
|
229
|
+
if not name or not uri:
|
|
230
|
+
continue
|
|
231
|
+
if name in exclude_names or uri in excluded_uris:
|
|
232
|
+
continue
|
|
233
|
+
name_lower = name.lower()
|
|
234
|
+
if item.get("is_loadable") and (
|
|
235
|
+
name_lower.endswith(".adg") or name_lower.endswith(".adv")
|
|
236
|
+
):
|
|
237
|
+
candidates.append({
|
|
238
|
+
"uri": uri, "name": name,
|
|
239
|
+
"tier": TIER_A_CURATED_PRESET, "source": "sounds/",
|
|
240
|
+
"tags": [], "pack": "", "genre_affinity": {},
|
|
241
|
+
})
|
|
242
|
+
except Exception as exc:
|
|
243
|
+
logger.debug("get_role_candidates sounds/ search failed for role=%s: %s", role, exc)
|
|
244
|
+
|
|
245
|
+
# ── Step 1b: drums/ — raw samples for drum roles (Tier-A) ────────
|
|
246
|
+
if is_drum:
|
|
247
|
+
drums_term = ROLE_SEARCH_TERMS.get(role_lower, {}).get("drums_term")
|
|
248
|
+
if drums_term:
|
|
249
|
+
try:
|
|
250
|
+
drums_results = ableton.send_command("search_browser", {
|
|
251
|
+
"path": "drums", "name_filter": drums_term,
|
|
252
|
+
"max_depth": 6, "max_results": 8, "loadable_only": True,
|
|
253
|
+
})
|
|
254
|
+
for item in (drums_results or {}).get("items", []):
|
|
255
|
+
uri = item.get("uri", "")
|
|
256
|
+
name = item.get("name", "")
|
|
257
|
+
if not name or not uri:
|
|
258
|
+
continue
|
|
259
|
+
if name in exclude_names or uri in excluded_uris:
|
|
260
|
+
continue
|
|
261
|
+
name_lower = name.lower()
|
|
262
|
+
if item.get("is_loadable") and name_lower.endswith(
|
|
263
|
+
(".aif", ".wav", ".adg", ".adv")
|
|
264
|
+
):
|
|
265
|
+
candidates.append({
|
|
266
|
+
"uri": uri, "name": name,
|
|
267
|
+
"tier": TIER_A_DRUM_SAMPLE, "source": "drums/",
|
|
268
|
+
"tags": [], "pack": "", "genre_affinity": {},
|
|
269
|
+
})
|
|
270
|
+
except Exception as exc:
|
|
271
|
+
logger.debug("get_role_candidates drums/ search failed for role=%s: %s", role, exc)
|
|
272
|
+
|
|
273
|
+
# has_tier_a: whether Steps 1/1b returned any curated candidates
|
|
274
|
+
has_tier_a = len(candidates) > 0
|
|
275
|
+
|
|
276
|
+
# ── Step 2: atlas — Tier-B fallback ──────────────────────────────────
|
|
277
|
+
# Collect atlas candidates (tag-based, then sonic query)
|
|
278
|
+
if atlas is not None:
|
|
279
|
+
atlas_candidates: list[dict] = []
|
|
280
|
+
seen_uris: set[str] = set()
|
|
281
|
+
|
|
282
|
+
# Stage 2a: tag-based lookup
|
|
283
|
+
by_tag = getattr(atlas, "_by_tag", None)
|
|
284
|
+
if by_tag is not None:
|
|
285
|
+
tags = _ROLE_TAGS.get(role_lower, (role_lower,))
|
|
286
|
+
for tag in tags:
|
|
287
|
+
for dev in by_tag.get(tag.lower(), []):
|
|
288
|
+
uri = dev.get("uri") or ""
|
|
289
|
+
if uri and uri not in seen_uris:
|
|
290
|
+
seen_uris.add(uri)
|
|
291
|
+
dev_copy = dict(dev)
|
|
292
|
+
dev_copy["__source"] = "tag"
|
|
293
|
+
atlas_candidates.append(dev_copy)
|
|
294
|
+
|
|
295
|
+
# Stage 2b: sonic-description query fallback
|
|
296
|
+
if len(atlas_candidates) < top_n and hasattr(atlas, "search"):
|
|
297
|
+
sonic_query = _ROLE_SONIC_QUERIES.get(role_lower, role_lower)
|
|
298
|
+
try:
|
|
299
|
+
search_results = atlas.search(sonic_query, category="instruments", limit=top_n * 2)
|
|
300
|
+
except Exception:
|
|
301
|
+
search_results = []
|
|
302
|
+
for r in search_results or []:
|
|
303
|
+
dev_data = r.get("device") if isinstance(r, dict) and "device" in r else r
|
|
304
|
+
if not isinstance(dev_data, dict):
|
|
305
|
+
continue
|
|
306
|
+
uri = dev_data.get("uri") or ""
|
|
307
|
+
if uri and uri not in seen_uris:
|
|
308
|
+
seen_uris.add(uri)
|
|
309
|
+
dev_copy = dict(dev_data)
|
|
310
|
+
dev_copy["__source"] = "search"
|
|
311
|
+
atlas_candidates.append(dev_copy)
|
|
312
|
+
|
|
313
|
+
# Filter + classify atlas candidates
|
|
314
|
+
for dev in atlas_candidates:
|
|
315
|
+
uri = dev.get("uri") or ""
|
|
316
|
+
instrument_name = dev.get("name") or ""
|
|
317
|
+
|
|
318
|
+
if instrument_name in exclude_names or uri in excluded_uris:
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
# Tier-C container: NEVER include
|
|
322
|
+
if instrument_name in CONTAINERS_NEEDING_PRESETS:
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
# Viability gate (§1 ban on Analog/Poli/Drift/Meld + drum keyword)
|
|
326
|
+
if not is_viable_instrument_uri(uri, instrument_name, role_lower):
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
# Drum-specific synths: always allowed for drum roles (their default IS the drum)
|
|
330
|
+
if is_drum_specific_synth(instrument_name):
|
|
331
|
+
if is_drum:
|
|
332
|
+
candidates.append({
|
|
333
|
+
"uri": uri, "name": instrument_name,
|
|
334
|
+
"tier": TIER_B_DRUM_SYNTH, "source": "atlas/",
|
|
335
|
+
"tags": dev.get("character_tags") or dev.get("tags") or [],
|
|
336
|
+
"pack": dev.get("pack") or "",
|
|
337
|
+
"genre_affinity": dev.get("genre_affinity") or dev.get("genres") or {},
|
|
338
|
+
})
|
|
339
|
+
# For melodic roles, drum-specific synths don't make sense — skip
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
# Known generic melodic synths (Operator, Wavetable, etc.):
|
|
343
|
+
# §1 hard rule — ONLY include as fallback when Tier-A is empty for this role.
|
|
344
|
+
# These are the "generic AI synth" defaults the user has rejected repeatedly.
|
|
345
|
+
if instrument_name in MELODIC_AUDIBLE_DEFAULTS:
|
|
346
|
+
if not has_tier_a:
|
|
347
|
+
candidates.append({
|
|
348
|
+
"uri": uri, "name": instrument_name,
|
|
349
|
+
"tier": TIER_B_AUDIBLE_DEFAULT_VALUE,
|
|
350
|
+
"source": dev.get("__source", "atlas/"),
|
|
351
|
+
"tags": dev.get("character_tags") or dev.get("tags") or [],
|
|
352
|
+
"pack": dev.get("pack") or "",
|
|
353
|
+
"genre_affinity": dev.get("genre_affinity") or dev.get("genres") or {},
|
|
354
|
+
"fallback_warning": (
|
|
355
|
+
"no curated preset found in sounds/ for this role; "
|
|
356
|
+
"bare-synth default may sound generic"
|
|
357
|
+
),
|
|
358
|
+
})
|
|
359
|
+
# If has_tier_a, SKIP this Tier-B melodic synth entirely (§1 fix)
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
# Unknown classification — custom atlas device (named preset, curated device).
|
|
363
|
+
# These are NOT generic defaults — they have specific character via atlas.
|
|
364
|
+
# Include as Tier-B; viability gate already ran above.
|
|
365
|
+
candidates.append({
|
|
366
|
+
"uri": uri, "name": instrument_name,
|
|
367
|
+
"tier": TIER_B_AUDIBLE_DEFAULT_VALUE, "source": dev.get("__source", "atlas/"),
|
|
368
|
+
"tags": dev.get("character_tags") or dev.get("tags") or [],
|
|
369
|
+
"pack": dev.get("pack") or "",
|
|
370
|
+
"genre_affinity": dev.get("genre_affinity") or dev.get("genres") or {},
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
# ── Sort: Tier-A first, then Tier-B; within same tier, genre affinity ──
|
|
374
|
+
_TIER_ORDER = {
|
|
375
|
+
TIER_A_CURATED_PRESET: 0,
|
|
376
|
+
TIER_A_DRUM_SAMPLE: 1,
|
|
377
|
+
TIER_B_DRUM_SYNTH: 2,
|
|
378
|
+
TIER_B_AUDIBLE_DEFAULT_VALUE: 3,
|
|
379
|
+
# Legacy alias — sort same as B_audible_default
|
|
380
|
+
"B_audible_default": 3,
|
|
381
|
+
}
|
|
382
|
+
gl = (genre or "").lower()
|
|
383
|
+
|
|
384
|
+
def _genre_score(c: dict) -> int:
|
|
385
|
+
if not gl:
|
|
386
|
+
return 0
|
|
387
|
+
aff = c.get("genre_affinity") or {}
|
|
388
|
+
if isinstance(aff, dict):
|
|
389
|
+
primary = [str(g).lower() for g in (aff.get("primary") or [])]
|
|
390
|
+
secondary = [str(g).lower() for g in (aff.get("secondary") or [])]
|
|
391
|
+
if gl in primary:
|
|
392
|
+
return 2
|
|
393
|
+
if gl in secondary:
|
|
394
|
+
return 1
|
|
395
|
+
return 0
|
|
396
|
+
|
|
397
|
+
candidates.sort(
|
|
398
|
+
key=lambda c: (_TIER_ORDER.get(c.get("tier", ""), 99), -_genre_score(c))
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
return candidates[:top_n]
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# Legacy name — kept for callers that still want a single pick (e.g. unit
|
|
405
|
+
# tests checking the deterministic path with top_n=1).
|
|
406
|
+
def pick_by_role_tag(
|
|
407
|
+
atlas: Any,
|
|
408
|
+
role: str,
|
|
409
|
+
genre: str = "",
|
|
410
|
+
top_n: int = 5,
|
|
411
|
+
rng: Optional[random.Random] = None,
|
|
412
|
+
) -> tuple[str, str]:
|
|
413
|
+
"""Single pick from atlas tag-based candidates with weighted random."""
|
|
414
|
+
candidates = get_role_candidates(atlas, role, genre=genre, top_n=top_n)
|
|
415
|
+
if not candidates:
|
|
416
|
+
return "", ""
|
|
417
|
+
if len(candidates) == 1 or top_n == 1:
|
|
418
|
+
return candidates[0]["uri"], candidates[0]["name"]
|
|
419
|
+
rng = rng or random.Random()
|
|
420
|
+
weights = list(range(len(candidates), 0, -1))
|
|
421
|
+
pick = rng.choices(candidates, weights=weights, k=1)[0]
|
|
422
|
+
return pick["uri"], pick["name"]
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# ── Role → simpler-role mapping for load_browser_item ────────────────
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def simpler_role_for(role: str) -> str | None:
|
|
429
|
+
if role in ("kick", "snare", "hat", "perc", "clap"):
|
|
430
|
+
return "drum"
|
|
431
|
+
if role in ("bass", "lead", "pad", "atmos"):
|
|
432
|
+
return "melodic"
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
# ── Fresh-project detection ──────────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
_DEFAULT_TRACK_NAME_RE = re.compile(
|
|
439
|
+
r"^\s*\d*[-\s]*(?:midi|audio)\s*\d*\s*$",
|
|
440
|
+
re.IGNORECASE,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def is_default_track_name(name: str) -> bool:
|
|
445
|
+
if not name:
|
|
446
|
+
return False
|
|
447
|
+
return bool(_DEFAULT_TRACK_NAME_RE.match(name))
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def detect_fresh_project(session_info: dict) -> bool:
|
|
451
|
+
tracks = session_info.get("tracks", []) or []
|
|
452
|
+
if not tracks or len(tracks) > 4:
|
|
453
|
+
return False
|
|
454
|
+
return all(is_default_track_name(t.get("name", "") or "") for t in tracks)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def track_is_empty(track_info: dict) -> bool:
|
|
458
|
+
if not track_info:
|
|
459
|
+
return True
|
|
460
|
+
has_clips = any(s.get("has_clip") for s in (track_info.get("clip_slots") or []))
|
|
461
|
+
has_devices = bool(track_info.get("devices"))
|
|
462
|
+
return not has_clips and not has_devices
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# ── Key parser + scale-degree math (helper math the LLM may reference) ─
|
|
466
|
+
|
|
467
|
+
_NOTE_TO_OFFSET = {
|
|
468
|
+
"C": 0, "C#": 1, "Db": 1, "D": 2, "D#": 3, "Eb": 3,
|
|
469
|
+
"E": 4, "F": 5, "F#": 6, "Gb": 6, "G": 7, "G#": 8,
|
|
470
|
+
"Ab": 8, "A": 9, "A#": 10, "Bb": 10, "B": 11,
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
_SCALE_INTERVALS_MINOR = [0, 2, 3, 5, 7, 8, 10] # Aeolian
|
|
474
|
+
_SCALE_INTERVALS_MAJOR = [0, 2, 4, 5, 7, 9, 11] # Ionian
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def parse_key(key_str: str) -> tuple[int, str]:
|
|
478
|
+
"""Parse "Cm" / "Am" / "F#m" / "C" / "G" → (root_offset_0_to_11, mode_str)."""
|
|
479
|
+
if not key_str:
|
|
480
|
+
return 0, "minor"
|
|
481
|
+
s = key_str.strip()
|
|
482
|
+
mode = "minor" if s.endswith("m") else "major"
|
|
483
|
+
note_part = s.rstrip("m")
|
|
484
|
+
if note_part not in _NOTE_TO_OFFSET:
|
|
485
|
+
return 0, "minor"
|
|
486
|
+
return _NOTE_TO_OFFSET[note_part], mode
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def degree_to_pitch(degree: int, key_root: int, octave: int, mode: str) -> int:
|
|
490
|
+
scale = _SCALE_INTERVALS_MINOR if mode == "minor" else _SCALE_INTERVALS_MAJOR
|
|
491
|
+
octaves_up = (degree - 1) // 7
|
|
492
|
+
in_octave = (degree - 1) % 7
|
|
493
|
+
return 12 * (octave + octaves_up) + key_root + scale[in_octave]
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def chord_at_degree(degree: int, key_root: int, octave: int, mode: str) -> list[int]:
|
|
497
|
+
return [
|
|
498
|
+
degree_to_pitch(degree, key_root, octave, mode),
|
|
499
|
+
degree_to_pitch(degree + 2, key_root, octave, mode),
|
|
500
|
+
degree_to_pitch(degree + 4, key_root, octave, mode),
|
|
501
|
+
]
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def scale_pitches_in_octave(key_root: int, octave: int, mode: str) -> list[int]:
|
|
505
|
+
"""Return the 7 scale pitches for the LLM's reference (octave starts on tonic)."""
|
|
506
|
+
return [degree_to_pitch(d, key_root, octave, mode) for d in range(1, 8)]
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# ── Genre creative guidance (Phase A — structured palettes, NOT templates) ─
|
|
510
|
+
#
|
|
511
|
+
# Each genre entry includes:
|
|
512
|
+
# - rhythmic_feel (text): high-level groove description
|
|
513
|
+
# - harmonic_palette (structured): suggested chord progressions with
|
|
514
|
+
# scale degrees + color-tone hints (modal mixture, secondary dom, etc.)
|
|
515
|
+
# - rhythmic_palette (structured): named gestures the agent can pick from
|
|
516
|
+
# (4-on-floor, 2-step, polyrhythm 3-against-4, etc.) WITH swing %
|
|
517
|
+
# - articulation_targets (structured): velocity stddev, ghost-note range,
|
|
518
|
+
# duration variation count, swing % — the agent should hit these
|
|
519
|
+
# - effect_chain_hints (structured): genre-typical effect chains per role
|
|
520
|
+
# - knowledge_search_queries (list): suggested queries for the agent to
|
|
521
|
+
# run against the Ableton Knowledge MCP for producer-voice inspiration
|
|
522
|
+
|
|
523
|
+
GENRE_CREATIVE_GUIDANCE: dict[str, dict] = {
|
|
524
|
+
"techno": {
|
|
525
|
+
"rhythmic_feel": "Driving 4-on-floor on the kick OR syncopated minimal kick. Hats on offbeats are signature; vary 8th/16th density per layer.",
|
|
526
|
+
"harmonic_palette": {
|
|
527
|
+
"summary": "Natural minor, modal — i-VI-VII or static i drones common. Avoid functional V-i.",
|
|
528
|
+
"progressions": [
|
|
529
|
+
{"name": "modal i-VI-VII", "degrees": [1, 6, 7], "feel": "ascending, hopeful within minor"},
|
|
530
|
+
{"name": "static i drone", "degrees": [1, 1, 1, 1], "feel": "hypnotic, single-chord vamp"},
|
|
531
|
+
{"name": "dorian i-IV", "degrees": [1, 4], "feel": "modal mixture, slight tension"},
|
|
532
|
+
{"name": "i-bVI-bIII", "degrees": [1, 6, 3], "feel": "chromatic mediant moves, dark"},
|
|
533
|
+
{"name": "i-VII alternation", "degrees": [1, 7], "feel": "2-bar cycle, Basic Channel adjacent"},
|
|
534
|
+
],
|
|
535
|
+
"color_tones": ["b9 over the i for tension", "11 over the iv for openness"],
|
|
536
|
+
},
|
|
537
|
+
"rhythmic_palette": [
|
|
538
|
+
{"name": "4_on_floor", "kick_pattern": "every quarter", "swing_pct": 50},
|
|
539
|
+
{"name": "syncopated_minimal", "kick_pattern": "1, 3, and-of-3 only", "swing_pct": 50},
|
|
540
|
+
{"name": "polyrhythm_3_4", "kick_pattern": "3-against-4 hat against quarter kick", "swing_pct": 55},
|
|
541
|
+
{"name": "swung_offbeats", "kick_pattern": "4OTF + offbeat hats with swing", "swing_pct": 58},
|
|
542
|
+
],
|
|
543
|
+
"articulation_targets": {
|
|
544
|
+
"velocity_stddev_min": 8,
|
|
545
|
+
"ghost_note_velocity_range": [25, 45],
|
|
546
|
+
"duration_variation_count_min": 3,
|
|
547
|
+
"swing_pct": 50,
|
|
548
|
+
"humanization_timing_ms": 5,
|
|
549
|
+
},
|
|
550
|
+
"effect_chain_hints": {
|
|
551
|
+
"kick": [
|
|
552
|
+
{"device": "Saturator", "params": {"Drive": 0.4}},
|
|
553
|
+
{"device": "Compressor", "params": {"Threshold": 0.75, "Ratio": 0.75}},
|
|
554
|
+
],
|
|
555
|
+
"bass": [
|
|
556
|
+
{"device": "Saturator", "params": {"Drive": 0.5}},
|
|
557
|
+
{"device": "Compressor", "params": {"Threshold": 0.7, "Ratio": 0.75}},
|
|
558
|
+
],
|
|
559
|
+
"hat": [
|
|
560
|
+
{"device": "EQ Eight", "params": {}},
|
|
561
|
+
],
|
|
562
|
+
"pad": [
|
|
563
|
+
{"device": "Auto Filter", "params": {}},
|
|
564
|
+
{"device": "Reverb", "params": {}},
|
|
565
|
+
],
|
|
566
|
+
},
|
|
567
|
+
"send_hints": {
|
|
568
|
+
"hat": [{"return_name": "A-Reverb", "value": 0.15}],
|
|
569
|
+
"pad": [{"return_name": "A-Reverb", "value": 0.4}, {"return_name": "B-Delay", "value": 0.2}],
|
|
570
|
+
},
|
|
571
|
+
"knowledge_search_queries": [
|
|
572
|
+
"minimal techno production technique",
|
|
573
|
+
"techno bass design saturation",
|
|
574
|
+
"techno hi-hat swing",
|
|
575
|
+
],
|
|
576
|
+
"spacing_advice": "Carve room for the kick — keep bass fundamental above 100Hz or use a sub layer. Pad usually long-released, sits behind.",
|
|
577
|
+
"production_hints": "Long pads, sidechain pumping under the kick, careful EQ around 200Hz mud.",
|
|
578
|
+
},
|
|
579
|
+
"dub techno": {
|
|
580
|
+
"rhythmic_feel": "Minimal kick (bar-1 or 4OTF). Off-beat chord stabs (and-of-2, and-of-4). Sparse percussion. Silence is the content.",
|
|
581
|
+
"harmonic_palette": {
|
|
582
|
+
"summary": "i-VII repeating cycle (Basic Channel signature). Held drone chords. Two-bar changes. Single-pitch ostinati.",
|
|
583
|
+
"progressions": [
|
|
584
|
+
{"name": "i-VII cycle", "degrees": [1, 7], "feel": "the canonical Basic Channel move, 2 bars per chord"},
|
|
585
|
+
{"name": "i-iv modal", "degrees": [1, 4], "feel": "dorian flavor, breathing"},
|
|
586
|
+
{"name": "single i drone", "degrees": [1, 1, 1, 1], "feel": "absolute stasis, all atmosphere"},
|
|
587
|
+
{"name": "i-bVI", "degrees": [1, 6], "feel": "chromatic mediant, slow descent"},
|
|
588
|
+
],
|
|
589
|
+
"color_tones": ["maj7 over the i for openness", "9 added to chord stabs"],
|
|
590
|
+
},
|
|
591
|
+
"rhythmic_palette": [
|
|
592
|
+
{"name": "minimal_kick_bar_1", "kick_pattern": "kick on bar-1 only, every other bar", "swing_pct": 50},
|
|
593
|
+
{"name": "4_on_floor_minimal", "kick_pattern": "4OTF, but quiet (vel 60-80)", "swing_pct": 50},
|
|
594
|
+
{"name": "kick_off", "kick_pattern": "no kick, just texture and chord", "swing_pct": 50},
|
|
595
|
+
],
|
|
596
|
+
"articulation_targets": {
|
|
597
|
+
"velocity_stddev_min": 6,
|
|
598
|
+
"ghost_note_velocity_range": [20, 35],
|
|
599
|
+
"duration_variation_count_min": 2,
|
|
600
|
+
"swing_pct": 52,
|
|
601
|
+
"humanization_timing_ms": 8,
|
|
602
|
+
},
|
|
603
|
+
"effect_chain_hints": {
|
|
604
|
+
"kick": [
|
|
605
|
+
{"device": "Compressor", "params": {"Threshold": 0.8, "Ratio": 0.5}},
|
|
606
|
+
{"device": "EQ Eight", "params": {}},
|
|
607
|
+
],
|
|
608
|
+
"pad": [
|
|
609
|
+
{"device": "Auto Filter", "params": {}},
|
|
610
|
+
{"device": "Echo", "params": {}},
|
|
611
|
+
],
|
|
612
|
+
"perc": [
|
|
613
|
+
{"device": "Echo", "params": {}},
|
|
614
|
+
],
|
|
615
|
+
"atmos": [
|
|
616
|
+
{"device": "Auto Filter", "params": {}},
|
|
617
|
+
],
|
|
618
|
+
},
|
|
619
|
+
"send_hints": {
|
|
620
|
+
"pad": [{"return_name": "A-Reverb", "value": 0.6}, {"return_name": "B-Delay", "value": 0.4}],
|
|
621
|
+
"perc": [{"return_name": "A-Reverb", "value": 0.5}, {"return_name": "B-Delay", "value": 0.3}],
|
|
622
|
+
"atmos": [{"return_name": "A-Reverb", "value": 0.7}],
|
|
623
|
+
},
|
|
624
|
+
"knowledge_search_queries": [
|
|
625
|
+
"dub techno production",
|
|
626
|
+
"Basic Channel production technique",
|
|
627
|
+
"dub chord stab Auto Filter",
|
|
628
|
+
],
|
|
629
|
+
"spacing_advice": "TRUST silence. 4-bar loops can be 80% empty. Atmosphere IS the content.",
|
|
630
|
+
"production_hints": "Long reverb tails (4-8 sec), low-pass filter sweeps, dub delay 1/8 with ~50% feedback.",
|
|
631
|
+
},
|
|
632
|
+
"house": {
|
|
633
|
+
"rhythmic_feel": "4OTF kick, claps on 2 and 4, hats offbeats. Bass often pumps with sidechain — space on beat 1.",
|
|
634
|
+
"harmonic_palette": {
|
|
635
|
+
"summary": "Minor or major; i-VI-iv-V common. Soulful chord extensions (maj7, m7). 4-bar cycles.",
|
|
636
|
+
"progressions": [
|
|
637
|
+
{"name": "i-VI-iv-V", "degrees": [1, 6, 4, 5], "feel": "classic house cycle"},
|
|
638
|
+
{"name": "i-iv-VII-III", "degrees": [1, 4, 7, 3], "feel": "deeper, modal"},
|
|
639
|
+
{"name": "ii-V-i", "degrees": [2, 5, 1], "feel": "jazz-house resolution"},
|
|
640
|
+
{"name": "i-VII-VI-V", "degrees": [1, 7, 6, 5], "feel": "Andalusian descending"},
|
|
641
|
+
],
|
|
642
|
+
"color_tones": ["m7 chord extensions", "9 and 11 added to pad chords"],
|
|
643
|
+
},
|
|
644
|
+
"rhythmic_palette": [
|
|
645
|
+
{"name": "4OTF_classic", "kick_pattern": "every quarter, full vel", "swing_pct": 50},
|
|
646
|
+
{"name": "4OTF_with_swing", "kick_pattern": "every quarter + 16ths swung", "swing_pct": 60},
|
|
647
|
+
{"name": "syncopated_house", "kick_pattern": "kicks 1, 2.5, 3, 3.5", "swing_pct": 55},
|
|
648
|
+
],
|
|
649
|
+
"articulation_targets": {
|
|
650
|
+
"velocity_stddev_min": 7,
|
|
651
|
+
"ghost_note_velocity_range": [30, 50],
|
|
652
|
+
"duration_variation_count_min": 3,
|
|
653
|
+
"swing_pct": 55,
|
|
654
|
+
"humanization_timing_ms": 5,
|
|
655
|
+
},
|
|
656
|
+
"effect_chain_hints": {
|
|
657
|
+
"kick": [
|
|
658
|
+
{"device": "Compressor", "params": {"Threshold": 0.75, "Ratio": 0.75}},
|
|
659
|
+
{"device": "EQ Eight", "params": {}},
|
|
660
|
+
],
|
|
661
|
+
"bass": [
|
|
662
|
+
{"device": "Saturator", "params": {"Drive": 0.45}},
|
|
663
|
+
{"device": "Compressor", "params": {"Threshold": 0.7, "Ratio": 0.85}},
|
|
664
|
+
{"device": "EQ Eight", "params": {}},
|
|
665
|
+
],
|
|
666
|
+
"clap": [
|
|
667
|
+
{"device": "EQ Eight", "params": {}},
|
|
668
|
+
],
|
|
669
|
+
"pad": [
|
|
670
|
+
{"device": "Compressor", "params": {"Threshold": 0.7, "Ratio": 0.75}},
|
|
671
|
+
{"device": "Chorus-Ensemble", "params": {}},
|
|
672
|
+
],
|
|
673
|
+
},
|
|
674
|
+
"send_hints": {
|
|
675
|
+
"clap": [{"return_name": "A-Reverb", "value": 0.3}],
|
|
676
|
+
"pad": [{"return_name": "A-Reverb", "value": 0.3}],
|
|
677
|
+
},
|
|
678
|
+
"knowledge_search_queries": [
|
|
679
|
+
"deep house production",
|
|
680
|
+
"house bass sidechain technique",
|
|
681
|
+
"house chord progression jazzy",
|
|
682
|
+
],
|
|
683
|
+
"spacing_advice": "Bass leaves room on beat 1 for kick (sidechain). Hats fill offbeats. Pad held, lead carries melody.",
|
|
684
|
+
"production_hints": "Sidechain on bass + pad triggered by kick. Warm tape saturation. Reverb on snare/clap.",
|
|
685
|
+
},
|
|
686
|
+
"hip hop": {
|
|
687
|
+
"rhythmic_feel": "Boom-bap kick (1, 2.5, 3, 3.75) OR trap-sparse kicks. Snare/clap on 2 and 4 (locked). Hats 8ths or rolling 16ths.",
|
|
688
|
+
"harmonic_palette": {
|
|
689
|
+
"summary": "Minor often, jazzy chord extensions (m7, maj7, m9). Sample-based sometimes — progressions less strict.",
|
|
690
|
+
"progressions": [
|
|
691
|
+
{"name": "i-VI-ii°-V", "degrees": [1, 6, 2, 5], "feel": "jazzy minor"},
|
|
692
|
+
{"name": "i-iv-bVII-bIII", "degrees": [1, 4, 7, 3], "feel": "modal mixture, soulful"},
|
|
693
|
+
{"name": "i-bVI-bIII-bVII", "degrees": [1, 6, 3, 7], "feel": "all minor, all chromatic mediant"},
|
|
694
|
+
{"name": "static i with passing", "degrees": [1, 1, 1, 1], "feel": "hold on i, agent adds passing tones"},
|
|
695
|
+
],
|
|
696
|
+
"color_tones": ["maj7 chord extensions on i", "9 and 11 added (jazz voicing)"],
|
|
697
|
+
},
|
|
698
|
+
"rhythmic_palette": [
|
|
699
|
+
{"name": "boom_bap", "kick_pattern": "1, 2.5, 3, 3.75 (offbeat 8ths)", "swing_pct": 58},
|
|
700
|
+
{"name": "trap_sparse", "kick_pattern": "1, 1.75, 2.5, 3.5 (sparse syncopation)", "swing_pct": 50},
|
|
701
|
+
{"name": "j_dilla_swung", "kick_pattern": "1, 2.5+30ms, 3 (Dilla swung)", "swing_pct": 65},
|
|
702
|
+
],
|
|
703
|
+
"articulation_targets": {
|
|
704
|
+
"velocity_stddev_min": 10,
|
|
705
|
+
"ghost_note_velocity_range": [30, 50],
|
|
706
|
+
"duration_variation_count_min": 4,
|
|
707
|
+
"swing_pct": 58,
|
|
708
|
+
"humanization_timing_ms": 8,
|
|
709
|
+
},
|
|
710
|
+
"effect_chain_hints": {
|
|
711
|
+
"kick": [
|
|
712
|
+
{"device": "Saturator", "params": {"Drive": 0.3}},
|
|
713
|
+
{"device": "Compressor", "params": {"Threshold": 0.8, "Ratio": 0.85}},
|
|
714
|
+
{"device": "EQ Eight", "params": {}},
|
|
715
|
+
],
|
|
716
|
+
"snare": [
|
|
717
|
+
{"device": "Compressor", "params": {"Threshold": 0.75, "Ratio": 0.75}},
|
|
718
|
+
{"device": "EQ Eight", "params": {}},
|
|
719
|
+
],
|
|
720
|
+
"bass": [
|
|
721
|
+
{"device": "Saturator", "params": {"Drive": 0.4}},
|
|
722
|
+
{"device": "EQ Eight", "params": {}},
|
|
723
|
+
],
|
|
724
|
+
"pad": [
|
|
725
|
+
{"device": "Auto Filter", "params": {}},
|
|
726
|
+
{"device": "Chorus-Ensemble", "params": {}},
|
|
727
|
+
],
|
|
728
|
+
},
|
|
729
|
+
"send_hints": {
|
|
730
|
+
"snare": [{"return_name": "A-Reverb", "value": 0.2}],
|
|
731
|
+
"pad": [{"return_name": "A-Reverb", "value": 0.25}],
|
|
732
|
+
},
|
|
733
|
+
"knowledge_search_queries": [
|
|
734
|
+
"boom bap production technique",
|
|
735
|
+
"lo-fi hip hop chord voicing",
|
|
736
|
+
"J Dilla swing humanization",
|
|
737
|
+
],
|
|
738
|
+
"spacing_advice": "Bass on the down — leave kick room. Pad/keys often jazzy chord stabs not held pads.",
|
|
739
|
+
"production_hints": "Lo-fi tape saturation, vinyl crackle (sparingly), warm low-pass filter.",
|
|
740
|
+
},
|
|
741
|
+
"drum and bass": {
|
|
742
|
+
"rhythmic_feel": "Kick on 1 and 2.75. Snare strict 2 and 4. Hats fast 16ths with strong velocity arc.",
|
|
743
|
+
"harmonic_palette": {
|
|
744
|
+
"summary": "Minor, often single-chord vamps. Reese basses chromatic. Ambient pads above the chaos.",
|
|
745
|
+
"progressions": [
|
|
746
|
+
{"name": "static i", "degrees": [1, 1, 1, 1], "feel": "hold on tonic, drums dominate"},
|
|
747
|
+
{"name": "i-bVI", "degrees": [1, 6], "feel": "chromatic mediant, dark"},
|
|
748
|
+
{"name": "chromatic walk", "degrees": [1, 1, 1, 1], "feel": "bass walks chromatically over static chord"},
|
|
749
|
+
],
|
|
750
|
+
"color_tones": ["b5 over chromatic walks (Phrygian flavor)"],
|
|
751
|
+
},
|
|
752
|
+
"rhythmic_palette": [
|
|
753
|
+
{"name": "amen_break_inspired", "kick_pattern": "1, 2.75, snare 2&4 with ghost rolls", "swing_pct": 50},
|
|
754
|
+
{"name": "two_step", "kick_pattern": "1 and 2.75 only", "swing_pct": 50},
|
|
755
|
+
{"name": "neurofunk", "kick_pattern": "1, 2.5, 3, 3.75 with double-time", "swing_pct": 50},
|
|
756
|
+
],
|
|
757
|
+
"articulation_targets": {
|
|
758
|
+
"velocity_stddev_min": 12,
|
|
759
|
+
"ghost_note_velocity_range": [25, 50],
|
|
760
|
+
"duration_variation_count_min": 4,
|
|
761
|
+
"swing_pct": 50,
|
|
762
|
+
"humanization_timing_ms": 3,
|
|
763
|
+
},
|
|
764
|
+
"effect_chain_hints": {
|
|
765
|
+
"kick": [
|
|
766
|
+
{"device": "Saturator", "params": {"Drive": 0.6}},
|
|
767
|
+
{"device": "Compressor", "params": {"Threshold": 0.85, "Ratio": 0.9}},
|
|
768
|
+
{"device": "EQ Eight", "params": {}},
|
|
769
|
+
],
|
|
770
|
+
"snare": [
|
|
771
|
+
{"device": "Compressor", "params": {"Threshold": 0.8, "Ratio": 0.9}},
|
|
772
|
+
{"device": "Saturator", "params": {"Drive": 0.4}},
|
|
773
|
+
],
|
|
774
|
+
"bass": [
|
|
775
|
+
{"device": "Auto Filter", "params": {}},
|
|
776
|
+
{"device": "Saturator", "params": {"Drive": 0.6}},
|
|
777
|
+
{"device": "EQ Eight", "params": {}},
|
|
778
|
+
],
|
|
779
|
+
"hat": [
|
|
780
|
+
{"device": "EQ Eight", "params": {}},
|
|
781
|
+
],
|
|
782
|
+
},
|
|
783
|
+
"send_hints": {
|
|
784
|
+
"snare": [{"return_name": "A-Reverb", "value": 0.3}],
|
|
785
|
+
"hat": [{"return_name": "A-Reverb", "value": 0.1}],
|
|
786
|
+
},
|
|
787
|
+
"knowledge_search_queries": [
|
|
788
|
+
"drum and bass production",
|
|
789
|
+
"reese bass design",
|
|
790
|
+
"amen break programming",
|
|
791
|
+
],
|
|
792
|
+
"spacing_advice": "Drums dominate. Bass and pad carry harmonic content but stay out of drum frequencies.",
|
|
793
|
+
"production_hints": "Reese bass = detuned saw stack with movement. Pad above the noise. Tight compression on drums.",
|
|
794
|
+
},
|
|
795
|
+
"ambient": {
|
|
796
|
+
"rhythmic_feel": "Often rhythmless, or sparse percussive accents. NO drums needed unless explicit.",
|
|
797
|
+
"harmonic_palette": {
|
|
798
|
+
"summary": "Slow chord changes (4-8 bars per chord). Modal/static. Pedal points, drones.",
|
|
799
|
+
"progressions": [
|
|
800
|
+
{"name": "single chord drone", "degrees": [1, 1, 1, 1], "feel": "16-32 bar held chord"},
|
|
801
|
+
{"name": "i-bVI very slow", "degrees": [1, 6], "feel": "8 bars per chord, chromatic mediant"},
|
|
802
|
+
{"name": "modal cycle slow", "degrees": [1, 4, 1, 5], "feel": "8 bars per chord, breathing"},
|
|
803
|
+
],
|
|
804
|
+
"color_tones": ["maj7, 9, 11 — every color tone available"],
|
|
805
|
+
},
|
|
806
|
+
"rhythmic_palette": [
|
|
807
|
+
{"name": "no_drums", "kick_pattern": "(skip drum layers entirely)", "swing_pct": 50},
|
|
808
|
+
{"name": "sparse_perc", "kick_pattern": "occasional bell or bowl, no kick", "swing_pct": 50},
|
|
809
|
+
],
|
|
810
|
+
"articulation_targets": {
|
|
811
|
+
"velocity_stddev_min": 4,
|
|
812
|
+
"ghost_note_velocity_range": [20, 40],
|
|
813
|
+
"duration_variation_count_min": 2,
|
|
814
|
+
"swing_pct": 50,
|
|
815
|
+
"humanization_timing_ms": 15,
|
|
816
|
+
},
|
|
817
|
+
"effect_chain_hints": {
|
|
818
|
+
"pad": [
|
|
819
|
+
{"device": "Auto Filter", "params": {}},
|
|
820
|
+
{"device": "Reverb", "params": {}},
|
|
821
|
+
{"device": "Chorus-Ensemble", "params": {}},
|
|
822
|
+
],
|
|
823
|
+
"atmos": [
|
|
824
|
+
{"device": "Echo", "params": {}},
|
|
825
|
+
{"device": "Erosion", "params": {}},
|
|
826
|
+
],
|
|
827
|
+
},
|
|
828
|
+
"send_hints": {
|
|
829
|
+
"atmos": [{"return_name": "A-Reverb", "value": 0.7}],
|
|
830
|
+
},
|
|
831
|
+
"knowledge_search_queries": [
|
|
832
|
+
"ambient texture design",
|
|
833
|
+
"drone production technique",
|
|
834
|
+
"Brian Eno ambient",
|
|
835
|
+
],
|
|
836
|
+
"spacing_advice": "Massive space. Single notes can carry whole bars. Reverb is the rhythm.",
|
|
837
|
+
"production_hints": "Long reverb (10s+), evolving texture, granular sample movement.",
|
|
838
|
+
},
|
|
839
|
+
"lo-fi": {
|
|
840
|
+
"rhythmic_feel": "Boom-bap-ish kick, snare on 2/4, hats with swing. Slight tempo wobble feel.",
|
|
841
|
+
"harmonic_palette": {
|
|
842
|
+
"summary": "Jazzy minor or major 7th chords. Borrowed chords, Em7-A7-Dmaj7-style progressions.",
|
|
843
|
+
"progressions": [
|
|
844
|
+
{"name": "ii-V-I", "degrees": [2, 5, 1], "feel": "jazz turnaround"},
|
|
845
|
+
{"name": "i-bIII-VI-IV", "degrees": [1, 3, 6, 4], "feel": "modal-mixture lo-fi"},
|
|
846
|
+
{"name": "i-iv-bVII-bIII", "degrees": [1, 4, 7, 3], "feel": "soulful"},
|
|
847
|
+
],
|
|
848
|
+
"color_tones": ["maj7, m7, 9, 13 (jazz voicings)"],
|
|
849
|
+
},
|
|
850
|
+
"rhythmic_palette": [
|
|
851
|
+
{"name": "boom_bap_swung", "kick_pattern": "1, 2.5, 3, 3.75 with heavy swing", "swing_pct": 62},
|
|
852
|
+
{"name": "j_dilla", "kick_pattern": "1, off-by-30ms, 3", "swing_pct": 65},
|
|
853
|
+
],
|
|
854
|
+
"articulation_targets": {
|
|
855
|
+
"velocity_stddev_min": 12,
|
|
856
|
+
"ghost_note_velocity_range": [30, 55],
|
|
857
|
+
"duration_variation_count_min": 4,
|
|
858
|
+
"swing_pct": 62,
|
|
859
|
+
"humanization_timing_ms": 12,
|
|
860
|
+
},
|
|
861
|
+
"effect_chain_hints": {
|
|
862
|
+
"kick": [
|
|
863
|
+
{"device": "Saturator", "params": {"Drive": 0.35}},
|
|
864
|
+
{"device": "EQ Eight", "params": {}},
|
|
865
|
+
],
|
|
866
|
+
"bass": [
|
|
867
|
+
{"device": "Saturator", "params": {"Drive": 0.4}},
|
|
868
|
+
{"device": "EQ Eight", "params": {}},
|
|
869
|
+
],
|
|
870
|
+
"pad": [
|
|
871
|
+
{"device": "Auto Filter", "params": {}},
|
|
872
|
+
{"device": "Chorus-Ensemble", "params": {}},
|
|
873
|
+
{"device": "Vinyl Distortion", "params": {}},
|
|
874
|
+
],
|
|
875
|
+
},
|
|
876
|
+
"send_hints": {
|
|
877
|
+
"pad": [{"return_name": "A-Reverb", "value": 0.25}],
|
|
878
|
+
},
|
|
879
|
+
"knowledge_search_queries": [
|
|
880
|
+
"lo-fi hip hop production",
|
|
881
|
+
"jazz chord voicing for lo-fi",
|
|
882
|
+
"tape saturation technique",
|
|
883
|
+
],
|
|
884
|
+
"spacing_advice": "Mid-density. Lots of room for the warmth/dust to breathe.",
|
|
885
|
+
"production_hints": "Tape saturation HEAVY. Vinyl crackle LOW. Detune chord pads slightly.",
|
|
886
|
+
},
|
|
887
|
+
"trap": {
|
|
888
|
+
"rhythmic_feel": "Sparse trap kick. Snare/clap on 2 and 4. Hats rolling 16ths/32nds with rolls.",
|
|
889
|
+
"harmonic_palette": {
|
|
890
|
+
"summary": "Minor, often single-chord. Cinematic, dark. 808 sub bass.",
|
|
891
|
+
"progressions": [
|
|
892
|
+
{"name": "static i", "degrees": [1, 1, 1, 1], "feel": "hold on tonic, drums + 808 dominate"},
|
|
893
|
+
{"name": "i-bVI", "degrees": [1, 6], "feel": "cinematic chromatic mediant"},
|
|
894
|
+
{"name": "i-iv", "degrees": [1, 4], "feel": "modal, dark"},
|
|
895
|
+
],
|
|
896
|
+
"color_tones": ["b5, m7"],
|
|
897
|
+
},
|
|
898
|
+
"rhythmic_palette": [
|
|
899
|
+
{"name": "sparse_trap", "kick_pattern": "1, 1.75, 2.5, 3, 3.75 (5 hits/bar)", "swing_pct": 50},
|
|
900
|
+
{"name": "808_long_sustain", "kick_pattern": "1 + bass 808 sustains", "swing_pct": 50},
|
|
901
|
+
],
|
|
902
|
+
"articulation_targets": {
|
|
903
|
+
"velocity_stddev_min": 8,
|
|
904
|
+
"ghost_note_velocity_range": [30, 55],
|
|
905
|
+
"duration_variation_count_min": 5,
|
|
906
|
+
"swing_pct": 50,
|
|
907
|
+
"humanization_timing_ms": 4,
|
|
908
|
+
},
|
|
909
|
+
"effect_chain_hints": {
|
|
910
|
+
"kick": [
|
|
911
|
+
{"device": "Saturator", "params": {"Drive": 0.55}},
|
|
912
|
+
{"device": "EQ Eight", "params": {}},
|
|
913
|
+
],
|
|
914
|
+
"bass": [
|
|
915
|
+
{"device": "Saturator", "params": {"Drive": 0.7}},
|
|
916
|
+
{"device": "Compressor", "params": {}},
|
|
917
|
+
{"device": "Auto Filter", "params": {}},
|
|
918
|
+
],
|
|
919
|
+
"hat": [
|
|
920
|
+
{"device": "EQ Eight", "params": {}},
|
|
921
|
+
],
|
|
922
|
+
"snare": [
|
|
923
|
+
{"device": "Reverb", "params": {}},
|
|
924
|
+
],
|
|
925
|
+
},
|
|
926
|
+
"send_hints": {
|
|
927
|
+
"snare": [{"return_name": "A-Reverb", "value": 0.22}],
|
|
928
|
+
"hat": [{"return_name": "A-Reverb", "value": 0.10}],
|
|
929
|
+
},
|
|
930
|
+
"knowledge_search_queries": [
|
|
931
|
+
"trap 808 design",
|
|
932
|
+
"trap hi-hat rolls programming",
|
|
933
|
+
"trap production",
|
|
934
|
+
],
|
|
935
|
+
"spacing_advice": "Drums are dense; bass holds the harmonic foundation. Pad/melody (if used) sparse.",
|
|
936
|
+
"production_hints": "808 sub long sustains. Tight kick. Saturated hats. Reverb on snare/clap.",
|
|
937
|
+
},
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
# ── Ableton Knowledge integration: per-genre-role query templates ───
|
|
942
|
+
#
|
|
943
|
+
# Queries fired against the Ableton Knowledge MCP (search_transcripts +
|
|
944
|
+
# search_live_manual + search_videos) to surface producer-voice technique
|
|
945
|
+
# snippets per role. The brief includes these as `recommended_searches`;
|
|
946
|
+
# the agent fires them inline before designing and attributes techniques
|
|
947
|
+
# in the apply plan.
|
|
948
|
+
#
|
|
949
|
+
# Coverage strategy: 1-2 queries per (genre × role) targeting the
|
|
950
|
+
# search_transcripts tool (the gold mine — semantic search over Ableton's
|
|
951
|
+
# YouTube tutorial transcripts, which is where producer-voice context lives).
|
|
952
|
+
|
|
953
|
+
_DEVICE_FOR_ROLE: dict[str, list[str]] = {
|
|
954
|
+
"kick": ["Saturator", "Drum Buss", "Compressor"],
|
|
955
|
+
"snare": ["Drum Buss", "Compressor", "Reverb"],
|
|
956
|
+
"hat": ["EQ Eight", "Reverb"],
|
|
957
|
+
"perc": ["Echo", "Reverb"],
|
|
958
|
+
"clap": ["Reverb", "Drum Buss"],
|
|
959
|
+
"bass": ["Saturator", "Auto Filter", "Operator", "Wavetable"],
|
|
960
|
+
"pad": ["Auto Filter", "Reverb", "Chorus-Ensemble"],
|
|
961
|
+
"lead": ["Operator", "Wavetable", "Auto Filter"],
|
|
962
|
+
"atmos": ["Reverb", "Auto Filter", "Granulator III"],
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _build_genre_role_queries(genre: str, role: str) -> list[dict]:
|
|
967
|
+
"""BUG-L (2026-05-01): Ableton's tutorial corpus is feature/device-named,
|
|
968
|
+
NOT producer-technique-named. The previous queries ("techno kick design
|
|
969
|
+
saturation transient") returned 0 results because the corpus indexes by
|
|
970
|
+
"Saturator", "Drum Buss", etc. — Live's actual feature names.
|
|
971
|
+
|
|
972
|
+
This builder generates queries that reliably hit the corpus:
|
|
973
|
+
1. Device-name searches against the Live manual (authoritative, dense)
|
|
974
|
+
2. User-question-shape transcript searches (broader hit rate)
|
|
975
|
+
3. One genre-named video search (catches "Made in Ableton" producer content)
|
|
976
|
+
"""
|
|
977
|
+
devices = _DEVICE_FOR_ROLE.get(role, ["EQ Eight"])
|
|
978
|
+
queries: list[dict] = []
|
|
979
|
+
|
|
980
|
+
# 1. Device-name in manual (always returns dense, high-quality content)
|
|
981
|
+
if devices:
|
|
982
|
+
queries.append({"tool": "search_live_manual", "query": devices[0]})
|
|
983
|
+
|
|
984
|
+
# 2. User-question shape transcript search
|
|
985
|
+
if role in ("kick", "snare", "hat", "perc", "clap"):
|
|
986
|
+
queries.append({
|
|
987
|
+
"tool": "search_transcripts",
|
|
988
|
+
"query": f"how to mix {role}",
|
|
989
|
+
})
|
|
990
|
+
elif role == "bass":
|
|
991
|
+
queries.append({"tool": "search_transcripts", "query": "bass synthesis low end"})
|
|
992
|
+
elif role == "pad":
|
|
993
|
+
queries.append({"tool": "search_transcripts", "query": "pad sound design"})
|
|
994
|
+
else:
|
|
995
|
+
queries.append({"tool": "search_transcripts", "query": f"{role} design"})
|
|
996
|
+
|
|
997
|
+
# 3. Genre-keyword video search (catches "Made in Ableton" producer content)
|
|
998
|
+
g = (genre or "").strip()
|
|
999
|
+
if g:
|
|
1000
|
+
queries.append({"tool": "search_videos", "query": f"{g} {role}"})
|
|
1001
|
+
|
|
1002
|
+
return queries
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def get_knowledge_queries_for_role(genre: str, role: str) -> list[dict]:
|
|
1006
|
+
"""Return the recommended search queries for a (genre, role) pair.
|
|
1007
|
+
|
|
1008
|
+
Built dynamically from device-name lookups + user-question patterns
|
|
1009
|
+
so we hit Ableton's actual tutorial/manual corpus (BUG-L).
|
|
1010
|
+
"""
|
|
1011
|
+
return _build_genre_role_queries(genre or "", role)
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
# Kept for backward compat — referenced by tests + reportable from
|
|
1015
|
+
# brief metadata if needed.
|
|
1016
|
+
GENRE_KNOWLEDGE_QUERIES: dict[str, dict[str, list[dict]]] = {
|
|
1017
|
+
genre: {role: _build_genre_role_queries(genre, role) for role in _DEVICE_FOR_ROLE}
|
|
1018
|
+
for genre in ("techno", "dub techno", "house", "hip hop",
|
|
1019
|
+
"drum and bass", "ambient", "lo-fi", "trap")
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def reference_artist_queries(artist: str, genre: str = "") -> list[dict]:
|
|
1024
|
+
"""Return search queries for a reference-artist composition.
|
|
1025
|
+
|
|
1026
|
+
Tier 2: when the user says compose(reference="Ricardo Villalobos"),
|
|
1027
|
+
the brief includes these searches so the agent designs USING that
|
|
1028
|
+
artist's signature techniques.
|
|
1029
|
+
"""
|
|
1030
|
+
if not artist:
|
|
1031
|
+
return []
|
|
1032
|
+
a = artist.strip()
|
|
1033
|
+
queries = [
|
|
1034
|
+
{"tool": "search_videos", "query": f"{a} production technique"},
|
|
1035
|
+
{"tool": "search_transcripts", "query": f"{a} {genre or ''}".strip()},
|
|
1036
|
+
{"tool": "search_transcripts", "query": f"{a} signature sound"},
|
|
1037
|
+
]
|
|
1038
|
+
return queries
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
# ── Creative seeds (random aesthetic bias per call) ──────────────────
|
|
1042
|
+
# The brief picks one randomly per call. The agent reads it and tilts
|
|
1043
|
+
# the design that way. Same prompt + different seed → different feel.
|
|
1044
|
+
|
|
1045
|
+
CREATIVE_SEEDS: list[dict] = [
|
|
1046
|
+
{"label": "spacious / minimal", "directive": "Leave more silence than you think you should. ~70% of the loop should breathe. Single notes can carry full bars."},
|
|
1047
|
+
{"label": "dense / driving", "directive": "Pack the rhythm — overlapping perc, busy hats, syncopated bass. Push the energy."},
|
|
1048
|
+
{"label": "fragmented / glitchy", "directive": "Break up the pulse — start mid-phrase, cut notes short, stutter rhythms. Embrace asymmetry."},
|
|
1049
|
+
{"label": "warm / dusty", "directive": "Soften everything — gentle velocities, smooth durations, no sharp transients. Tape-saturation feel even without effects."},
|
|
1050
|
+
{"label": "hypnotic / repetitive", "directive": "Lock into a single 1-bar phrase and repeat. Tiny micro-variations across bars 2-4. Trance-inducing."},
|
|
1051
|
+
{"label": "off-balance / asymmetric", "directive": "Use 5-bar phrases, polyrhythm 3-against-4, displaced downbeats. Make it feel WRONG in a good way."},
|
|
1052
|
+
{"label": "spacious dub", "directive": "Treat reverb and delay as instruments. Notes are just excitations of the space."},
|
|
1053
|
+
{"label": "cinematic / brooding", "directive": "Long sustains, slow chord changes, low-velocity layers. Atmosphere is the content."},
|
|
1054
|
+
{"label": "playful / bouncy", "directive": "Stagger note placements off-grid by tens of ms. Velocity ramps and dips. Make it dance."},
|
|
1055
|
+
{"label": "monochromatic / focused", "directive": "Stick to 2-3 pitches max across all melodic layers. The single repeated motif is the hook."},
|
|
1056
|
+
]
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
# ── Anti-defaults per genre (random anti-pattern bias per call) ──────
|
|
1060
|
+
# Forces creative reach by explicitly forbidding the most-common pattern
|
|
1061
|
+
# choice for the genre on this call. Picks 1-2 randomly.
|
|
1062
|
+
|
|
1063
|
+
ANTI_DEFAULTS_BY_GENRE: dict[str, list[str]] = {
|
|
1064
|
+
"techno": [
|
|
1065
|
+
"no four-on-floor — try a syncopated kick instead",
|
|
1066
|
+
"no held-chord pad — use stabs or evolving texture",
|
|
1067
|
+
"no bass on every 8th — leave space",
|
|
1068
|
+
"no minor key — try Phrygian or Locrian for this call",
|
|
1069
|
+
],
|
|
1070
|
+
"dub techno": [
|
|
1071
|
+
"no four-on-floor kick — minimal kick or no kick",
|
|
1072
|
+
"no on-the-grid chord stabs — push them off-beat or polyrhythmic",
|
|
1073
|
+
"no major-7 pad — use sus2/sus4 or single-pitch drone",
|
|
1074
|
+
],
|
|
1075
|
+
"house": [
|
|
1076
|
+
"no plain four-on-floor — add ghost kicks or syncopated 16ths",
|
|
1077
|
+
"no claps strictly on 2 and 4 — try displaced clap on 2.5",
|
|
1078
|
+
"no 4-bar progression — try 8-bar or 5-bar phrasing",
|
|
1079
|
+
],
|
|
1080
|
+
"hip hop": [
|
|
1081
|
+
"no boom-bap kick — try trap-sparse or off-grid placement",
|
|
1082
|
+
"no snare/clap strictly on 2 and 4 — push or pull by 30ms",
|
|
1083
|
+
"no held pad — use jazzy chord stabs",
|
|
1084
|
+
],
|
|
1085
|
+
"drum and bass": [
|
|
1086
|
+
"no straight 2-step — try amen-break-inspired ghost rolls",
|
|
1087
|
+
"no chromatic reese — try modal vamping",
|
|
1088
|
+
"no 4-bar phrase — try 6 or 8",
|
|
1089
|
+
],
|
|
1090
|
+
"ambient": [
|
|
1091
|
+
"no perfect cadence — avoid V-I",
|
|
1092
|
+
"no 4-bar phrasing — extend chord changes to 8-16 bars",
|
|
1093
|
+
"no static velocity — slow swells and dips",
|
|
1094
|
+
],
|
|
1095
|
+
"lo-fi": [
|
|
1096
|
+
"no straight boom-bap — push the swing past 65%",
|
|
1097
|
+
"no on-grid chord changes — anticipate by an 8th",
|
|
1098
|
+
"no major-key happy progression — keep it minor and jazzy",
|
|
1099
|
+
],
|
|
1100
|
+
"trap": [
|
|
1101
|
+
"no kick exactly on 1 — anticipate by 16th or push by 16th",
|
|
1102
|
+
"no 16th hat rolls — try 32nd or 64th with varying velocity arcs",
|
|
1103
|
+
"no minor pad — try a single 808 with vibrato as the only melodic content",
|
|
1104
|
+
],
|
|
1105
|
+
"_default": [
|
|
1106
|
+
"avoid the most obvious rhythmic choice for this genre",
|
|
1107
|
+
"leave at least one beat fully silent across the loop",
|
|
1108
|
+
"use a non-diatonic color tone at least once",
|
|
1109
|
+
],
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
def get_creative_guidance(genre: str, sub_genre: str = "") -> dict:
|
|
1114
|
+
"""Return the creative-guidance dict for a genre, falling back to the
|
|
1115
|
+
closest match if the exact genre isn't in our guidance map."""
|
|
1116
|
+
g = (genre or "").lower().strip()
|
|
1117
|
+
if g in GENRE_CREATIVE_GUIDANCE:
|
|
1118
|
+
return GENRE_CREATIVE_GUIDANCE[g]
|
|
1119
|
+
s = (sub_genre or "").lower().strip()
|
|
1120
|
+
if s in GENRE_CREATIVE_GUIDANCE:
|
|
1121
|
+
return GENRE_CREATIVE_GUIDANCE[s]
|
|
1122
|
+
# Generic fallback (must include all the structured fields too)
|
|
1123
|
+
return {
|
|
1124
|
+
"rhythmic_feel": "Match the prompt's vibe; favor groove + space over density.",
|
|
1125
|
+
"harmonic_palette": {
|
|
1126
|
+
"summary": "Use the requested key. Minor by default unless prompt suggests major.",
|
|
1127
|
+
"progressions": [
|
|
1128
|
+
{"name": "static i", "degrees": [1, 1, 1, 1], "feel": "hold on tonic"},
|
|
1129
|
+
{"name": "i-IV-V-i", "degrees": [1, 4, 5, 1], "feel": "classic functional"},
|
|
1130
|
+
],
|
|
1131
|
+
"color_tones": ["maj7", "9"],
|
|
1132
|
+
},
|
|
1133
|
+
"rhythmic_palette": [
|
|
1134
|
+
{"name": "generic_4OTF", "kick_pattern": "every quarter", "swing_pct": 50},
|
|
1135
|
+
],
|
|
1136
|
+
"articulation_targets": {
|
|
1137
|
+
"velocity_stddev_min": 6,
|
|
1138
|
+
"ghost_note_velocity_range": [25, 45],
|
|
1139
|
+
"duration_variation_count_min": 2,
|
|
1140
|
+
"swing_pct": 50,
|
|
1141
|
+
"humanization_timing_ms": 5,
|
|
1142
|
+
},
|
|
1143
|
+
"effect_chain_hints": {},
|
|
1144
|
+
"send_hints": {},
|
|
1145
|
+
"knowledge_search_queries": [],
|
|
1146
|
+
"spacing_advice": "Leave room between layers. More space = more impact.",
|
|
1147
|
+
"production_hints": "Velocity humanization, voice-leading, vary durations.",
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
def pick_creative_seed(rng: Optional[random.Random] = None) -> dict:
|
|
1152
|
+
"""Pick a random creative seed for THIS call. Different per call =
|
|
1153
|
+
different aesthetic bias = different sound from same prompt."""
|
|
1154
|
+
rng = rng or random.Random()
|
|
1155
|
+
return rng.choice(CREATIVE_SEEDS)
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
def pick_anti_defaults(
|
|
1159
|
+
genre: str, count: int = 2, rng: Optional[random.Random] = None
|
|
1160
|
+
) -> list[str]:
|
|
1161
|
+
"""Pick `count` random anti-defaults for the genre. Forces the agent
|
|
1162
|
+
to reach beyond the obvious genre-default move on each call."""
|
|
1163
|
+
rng = rng or random.Random()
|
|
1164
|
+
g = (genre or "").lower().strip()
|
|
1165
|
+
pool = ANTI_DEFAULTS_BY_GENRE.get(g) or ANTI_DEFAULTS_BY_GENRE.get("_default") or []
|
|
1166
|
+
if not pool:
|
|
1167
|
+
return []
|
|
1168
|
+
n = min(count, len(pool))
|
|
1169
|
+
return rng.sample(pool, n)
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
# ── Brief builder — the heart of the new fast mode ───────────────────
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
# Recommended MIDI octave per role — prevents the agent from designing
|
|
1176
|
+
# bass at A1 (sub-bass territory, often inaudible or muddy) or pad at
|
|
1177
|
+
# octave 6 (too thin). User feedback 2026-05-01: "the drums and bass
|
|
1178
|
+
# everything is super low pitched". The brief now explicitly tells the
|
|
1179
|
+
# agent which octaves work for each role.
|
|
1180
|
+
RECOMMENDED_OCTAVES_PER_ROLE: dict[str, dict] = {
|
|
1181
|
+
"kick": {"midi_pitch": 36, "note": "C1", "rationale": "drum-rack convention; sample plays at natural pitch"},
|
|
1182
|
+
"snare": {"midi_pitch": 38, "note": "D1", "rationale": "drum-rack convention"},
|
|
1183
|
+
"hat": {"midi_pitch": 42, "note": "F#1", "rationale": "drum-rack closed hat convention"},
|
|
1184
|
+
"perc": {"midi_pitch": 39, "note": "Eb1", "rationale": "drum-rack perc convention"},
|
|
1185
|
+
"clap": {"midi_pitch": 39, "note": "Eb1", "rationale": "drum-rack clap convention"},
|
|
1186
|
+
"bass": {"recommended_octave": 2, "midi_range": [33, 50], "rationale": "bass synth presence — A2-Bb2 range. AVOID octave 1 (A1=33) for techno bass; sub-territory becomes inaudible/muddy."},
|
|
1187
|
+
"pad": {"recommended_octave": 4, "midi_range": [55, 76], "rationale": "mid-range pad sits behind kick/bass. C4=60, A4=69 typical."},
|
|
1188
|
+
"lead": {"recommended_octave": 5, "midi_range": [60, 84], "rationale": "lead cuts through; octaves 5-6 typical."},
|
|
1189
|
+
"atmos": {"recommended_octave": 5, "midi_range": [60, 84], "rationale": "atmos floats above the mix. Layered drones can use multiple octaves."},
|
|
1190
|
+
"vox": {"recommended_octave": 4, "midi_range": [55, 76], "rationale": "vocal range C3-C5."},
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def _extract_loaded_device_names(session_info: dict) -> set[str]:
|
|
1195
|
+
"""Anti-repeat helper: extract names of devices currently loaded in the
|
|
1196
|
+
session, so the brief picker can bias AWAY from already-used devices.
|
|
1197
|
+
|
|
1198
|
+
This solves the "Tree Tone always wins for pad" repetition the user
|
|
1199
|
+
called out — once Tree Tone is loaded on track 3, the next call's
|
|
1200
|
+
pad picker will see it in exclude_names and pick something else.
|
|
1201
|
+
"""
|
|
1202
|
+
names: set[str] = set()
|
|
1203
|
+
for track in (session_info.get("tracks") or []):
|
|
1204
|
+
# Some session_info shapes carry track devices; if not, the brief
|
|
1205
|
+
# builder has to look up via get_track_info per track. The cheap
|
|
1206
|
+
# path is fine for now — read whatever names are available.
|
|
1207
|
+
for dev in (track.get("devices") or []):
|
|
1208
|
+
n = dev.get("name") or ""
|
|
1209
|
+
if n:
|
|
1210
|
+
names.add(n)
|
|
1211
|
+
return names
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
def _extract_loaded_preset_uris(session_info: dict) -> set[str]:
|
|
1215
|
+
"""Anti-repeat helper: extract browser URIs of curated presets already
|
|
1216
|
+
loaded in the session.
|
|
1217
|
+
|
|
1218
|
+
Looks for device names ending in .adg or .adv (these are curated presets
|
|
1219
|
+
that were loaded via load_browser_item and carry a browser URI). Adds the
|
|
1220
|
+
URI so get_role_candidates can filter them via excluded_uris.
|
|
1221
|
+
|
|
1222
|
+
Also extracts sample paths from Simpler/Sampler devices when available.
|
|
1223
|
+
"""
|
|
1224
|
+
uris: set[str] = set()
|
|
1225
|
+
for track in (session_info.get("tracks") or []):
|
|
1226
|
+
for dev in (track.get("devices") or []):
|
|
1227
|
+
uri = dev.get("uri") or dev.get("browser_uri") or ""
|
|
1228
|
+
if uri:
|
|
1229
|
+
uris.add(uri)
|
|
1230
|
+
# Also capture sample paths stored on Simpler devices
|
|
1231
|
+
sample_path = dev.get("sample_path") or dev.get("file_path") or ""
|
|
1232
|
+
if sample_path:
|
|
1233
|
+
uris.add(sample_path)
|
|
1234
|
+
return uris
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def build_creative_brief(
|
|
1238
|
+
intent: CompositionIntent,
|
|
1239
|
+
atlas: Any,
|
|
1240
|
+
fresh_project_state: dict,
|
|
1241
|
+
bars: int = 4,
|
|
1242
|
+
candidates_per_role: int = 12,
|
|
1243
|
+
rng: Optional[random.Random] = None,
|
|
1244
|
+
reference: Optional[str] = None,
|
|
1245
|
+
exclude_loaded_device_names: Optional[set[str]] = None,
|
|
1246
|
+
*,
|
|
1247
|
+
ableton: Any = None,
|
|
1248
|
+
excluded_preset_uris: Optional[set[str]] = None,
|
|
1249
|
+
) -> dict:
|
|
1250
|
+
"""Build the creative brief returned by compose(mode="fast").
|
|
1251
|
+
|
|
1252
|
+
Phase-A enrichments (2026-05-01):
|
|
1253
|
+
- 12 atlas candidates per role (was 5) → more atlas variety surfaced
|
|
1254
|
+
- Creative seed (random aesthetic bias per call from CREATIVE_SEEDS)
|
|
1255
|
+
- Anti-defaults (random anti-pattern bias per call)
|
|
1256
|
+
- Structured harmonic_palette: chord progressions w/ degrees + color tones
|
|
1257
|
+
- Structured rhythmic_palette: named gestures w/ swing %
|
|
1258
|
+
- Articulation targets: velocity stddev / ghost-note range / etc.
|
|
1259
|
+
- Effect chain hints per role (genre-typical processing)
|
|
1260
|
+
- knowledge_search_queries: queries the agent can run via Ableton Knowledge
|
|
1261
|
+
MCP (search_transcripts / search_videos / search_live_manual) for
|
|
1262
|
+
producer-voice technique inspiration
|
|
1263
|
+
|
|
1264
|
+
Phase-4 Task 18a-followup (§1 fix):
|
|
1265
|
+
- ableton: if provided, fires search_browser("sounds/") before atlas so
|
|
1266
|
+
curated .adg/.adv presets are Tier-A candidates for every role.
|
|
1267
|
+
- excluded_preset_uris: URIs of recently-loaded curated presets — filtered
|
|
1268
|
+
out so the agent can't re-pick the same .adv twice in a row.
|
|
1269
|
+
|
|
1270
|
+
The agent reads this, designs creatively, submits to compose_fast_apply.
|
|
1271
|
+
"""
|
|
1272
|
+
rng = rng or random.Random()
|
|
1273
|
+
key_root, mode_str = parse_key(intent.key)
|
|
1274
|
+
bpm = intent.tempo or 120
|
|
1275
|
+
|
|
1276
|
+
# Scale pitches at multiple octaves
|
|
1277
|
+
scale_pitches = {
|
|
1278
|
+
"tonic_at_octave": {
|
|
1279
|
+
str(o): degree_to_pitch(1, key_root, o, mode_str) for o in range(1, 6)
|
|
1280
|
+
},
|
|
1281
|
+
"scale_at_octave_4": scale_pitches_in_octave(key_root, 4, mode_str),
|
|
1282
|
+
"scale_at_octave_2": scale_pitches_in_octave(key_root, 2, mode_str),
|
|
1283
|
+
"diatonic_chord_roots_octave_4": [
|
|
1284
|
+
chord_at_degree(d, key_root, 4, mode_str)[0] for d in range(1, 8)
|
|
1285
|
+
],
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
# Atlas + sounds/ instrument candidates — 12 per role with anti-repeat exclusion.
|
|
1289
|
+
# (NEW 2026-05-01: pass currently-loaded device names so candidates bias toward
|
|
1290
|
+
# variety across calls instead of repeating same picks.)
|
|
1291
|
+
# (NEW 18a-followup: pass ableton + excluded_preset_uris for sounds/ hunt.)
|
|
1292
|
+
suggested_roles = _suggest_layer_roles(intent)
|
|
1293
|
+
instruments_by_role: dict[str, list[dict]] = {}
|
|
1294
|
+
for role in suggested_roles:
|
|
1295
|
+
candidates = get_role_candidates(
|
|
1296
|
+
atlas, role,
|
|
1297
|
+
genre=intent.genre or "",
|
|
1298
|
+
top_n=candidates_per_role,
|
|
1299
|
+
exclude_names=exclude_loaded_device_names,
|
|
1300
|
+
ableton=ableton,
|
|
1301
|
+
excluded_uris=excluded_preset_uris,
|
|
1302
|
+
)
|
|
1303
|
+
instruments_by_role[role] = candidates
|
|
1304
|
+
|
|
1305
|
+
# Per-role octave recommendations (NEW: prevents A1 bass or octave-6 pad)
|
|
1306
|
+
octaves_by_role: dict[str, dict] = {
|
|
1307
|
+
role: RECOMMENDED_OCTAVES_PER_ROLE[role]
|
|
1308
|
+
for role in suggested_roles
|
|
1309
|
+
if role in RECOMMENDED_OCTAVES_PER_ROLE
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
# Genre creative guidance (structured palettes + text advice + effect chains)
|
|
1313
|
+
guidance = get_creative_guidance(intent.genre or "", intent.sub_genre or "")
|
|
1314
|
+
|
|
1315
|
+
# Phase A — random per-call creative seed + anti-defaults force surprise
|
|
1316
|
+
creative_seed = pick_creative_seed(rng=rng)
|
|
1317
|
+
anti_defaults = pick_anti_defaults(
|
|
1318
|
+
intent.genre or "", count=2, rng=rng,
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
# Tier-1A: Per-role recommended searches against Ableton Knowledge MCP.
|
|
1322
|
+
# BUG-O fix (2026-05-01): cap at ONE recommended search per role (the
|
|
1323
|
+
# device-name manual lookup, which is the most reliable corpus). The
|
|
1324
|
+
# earlier 3-per-role design produced 12 searches × ~1-2s each = 24s of
|
|
1325
|
+
# extra latency the agent is unlikely to honor anyway. The remaining
|
|
1326
|
+
# queries are surfaced in `optional_searches` for the agent to fire
|
|
1327
|
+
# only when it has time.
|
|
1328
|
+
recommended_searches: list[dict] = []
|
|
1329
|
+
optional_searches: list[dict] = []
|
|
1330
|
+
for role in suggested_roles:
|
|
1331
|
+
role_queries = get_knowledge_queries_for_role(intent.genre or "", role)
|
|
1332
|
+
if not role_queries:
|
|
1333
|
+
continue
|
|
1334
|
+
# Most useful query per role goes in recommended (manual or first)
|
|
1335
|
+
primary = role_queries[0]
|
|
1336
|
+
recommended_searches.append({
|
|
1337
|
+
"role": role,
|
|
1338
|
+
"tool": primary["tool"],
|
|
1339
|
+
"query": primary["query"],
|
|
1340
|
+
})
|
|
1341
|
+
# Rest go in optional
|
|
1342
|
+
for q in role_queries[1:]:
|
|
1343
|
+
optional_searches.append({
|
|
1344
|
+
"role": role,
|
|
1345
|
+
"tool": q["tool"],
|
|
1346
|
+
"query": q["query"],
|
|
1347
|
+
})
|
|
1348
|
+
|
|
1349
|
+
# Tier-2: Reference-artist queries when caller passes a reference.
|
|
1350
|
+
reference_searches: list[dict] = []
|
|
1351
|
+
if reference:
|
|
1352
|
+
for q in reference_artist_queries(reference, genre=intent.genre or ""):
|
|
1353
|
+
reference_searches.append({
|
|
1354
|
+
"role": None, # global, not role-specific
|
|
1355
|
+
"tool": q["tool"],
|
|
1356
|
+
"query": q["query"],
|
|
1357
|
+
})
|
|
1358
|
+
|
|
1359
|
+
return {
|
|
1360
|
+
"phase": "brief",
|
|
1361
|
+
"mode": "fast",
|
|
1362
|
+
"phase_version": "A",
|
|
1363
|
+
"intent": intent.to_dict(),
|
|
1364
|
+
"tempo": bpm,
|
|
1365
|
+
"key": {
|
|
1366
|
+
"key_str": intent.key or "",
|
|
1367
|
+
"key_root": key_root,
|
|
1368
|
+
"mode": mode_str,
|
|
1369
|
+
},
|
|
1370
|
+
"scale_pitches": scale_pitches,
|
|
1371
|
+
"creative_guidance": guidance,
|
|
1372
|
+
"creative_seed": creative_seed,
|
|
1373
|
+
"anti_defaults": anti_defaults,
|
|
1374
|
+
"recommended_searches": recommended_searches,
|
|
1375
|
+
"optional_searches": optional_searches,
|
|
1376
|
+
"reference_artist": reference,
|
|
1377
|
+
"reference_searches": reference_searches,
|
|
1378
|
+
"instruments_by_role": instruments_by_role,
|
|
1379
|
+
"octaves_by_role": octaves_by_role,
|
|
1380
|
+
"excluded_recently_used": sorted(exclude_loaded_device_names or set()),
|
|
1381
|
+
"suggested_layer_count": len(suggested_roles),
|
|
1382
|
+
"suggested_roles": suggested_roles,
|
|
1383
|
+
"bars": bars,
|
|
1384
|
+
"fresh_project_state": fresh_project_state,
|
|
1385
|
+
"next_step": (
|
|
1386
|
+
"READ THIS BRIEF, THEN DESIGN A FRESH LAYER PLAN.\n"
|
|
1387
|
+
"1. Honor the creative_seed['directive'] — let it tilt your design.\n"
|
|
1388
|
+
"2. Honor the anti_defaults — DO NOT use the patterns listed on this call.\n"
|
|
1389
|
+
"3. **OCTAVE DISCIPLINE**: For tonal layers, design notes within the\n"
|
|
1390
|
+
" `octaves_by_role[role].midi_range`. Tonal-range traps to avoid:\n"
|
|
1391
|
+
" bass at A1=33 is sub-territory and inaudible/muddy on most systems —\n"
|
|
1392
|
+
" use A2=45 or higher. Pad at octave 6 is too thin — stay around\n"
|
|
1393
|
+
" octave 4-5. The brief gives you the exact ranges per role.\n"
|
|
1394
|
+
"4. **ANTI-REPEAT**: `excluded_recently_used` lists device names already\n"
|
|
1395
|
+
" loaded in the session — the candidate list ALREADY has them filtered\n"
|
|
1396
|
+
" out, so just pick from `instruments_by_role[role]` confidently. If\n"
|
|
1397
|
+
" the list looks short for a role, that's because we're forcing variety.\n"
|
|
1398
|
+
" **TIER A vs B distinction**: Each candidate in `instruments_by_role[role]`\n"
|
|
1399
|
+
" carries a `tier` field. Tier-A candidates ('A_sample_ready') load with\n"
|
|
1400
|
+
" sound on note-on. Tier-B candidates ('B_audible_default') also produce\n"
|
|
1401
|
+
" sound by default. Both are safe to pick. There is no Tier-C in the brief\n"
|
|
1402
|
+
" — those are filtered out before the brief reaches you.\n"
|
|
1403
|
+
"5. **TIER-1: Fire each search in `recommended_searches` BEFORE designing\n"
|
|
1404
|
+
" that role.** Each entry gives you a (tool, query) pair — call the named\n"
|
|
1405
|
+
" Ableton Knowledge MCP tool. Most queries hit the Live manual or video\n"
|
|
1406
|
+
" tutorials; capture 1 useful snippet per role and apply it.\n"
|
|
1407
|
+
"6. **TIER-2: If `reference_artist` is set**, also fire each search in\n"
|
|
1408
|
+
" `reference_searches` and design USING that artist's signature techniques.\n"
|
|
1409
|
+
"7. For each layer: pick ONE uri from instruments_by_role[role], design\n"
|
|
1410
|
+
" MIDI notes (start_time in beats, pitch, duration, velocity 0-127).\n"
|
|
1411
|
+
" Hit the articulation_targets — velocity stddev, ghost notes, etc.\n"
|
|
1412
|
+
"8. Pick ONE progression from creative_guidance.harmonic_palette.progressions\n"
|
|
1413
|
+
" (or invent your own with color tones).\n"
|
|
1414
|
+
"9. **PHASE B: EFFECT CHAIN PER LAYER** — for each layer, include an\n"
|
|
1415
|
+
" `effects` array of native Live devices (insert_device-compatible).\n"
|
|
1416
|
+
" `creative_guidance.effect_chain_hints[role]` gives you the canonical\n"
|
|
1417
|
+
" genre-typical chain: [{device: 'Saturator', params: {Drive: 0.4}}, ...]\n"
|
|
1418
|
+
" You can ADAPT values to fit the prompt mood (heavier in trap, subtler\n"
|
|
1419
|
+
" in ambient) — but anchor on the hint. Saturator/Compressor params are\n"
|
|
1420
|
+
" normalized 0-1; EQ Eight uses absolute Hz/dB. Leave params={} when\n"
|
|
1421
|
+
" you only want the device with no specific param overrides.\n"
|
|
1422
|
+
"10. **PHASE B: SENDS PER LAYER** — for layers that benefit from spatial\n"
|
|
1423
|
+
" depth (pad, vox, snare), include `sends`: [{return_name: 'A-Reverb',\n"
|
|
1424
|
+
" value: 0.25}]. `creative_guidance.send_hints[role]` gives you the\n"
|
|
1425
|
+
" starting point. Skip the layer if no return tracks exist (kicks rarely\n"
|
|
1426
|
+
" need send reverb).\n"
|
|
1427
|
+
"11. **TIER-1: Each layer in your plan MUST include `applied_technique`** —\n"
|
|
1428
|
+
" the snippet + source you used. compose_fast_apply will surface these.\n"
|
|
1429
|
+
"12. Submit the complete plan via compose_fast_apply(plan={layers:[...]}).\n"
|
|
1430
|
+
"13. NEVER use a template. Make every call genuinely different."
|
|
1431
|
+
),
|
|
1432
|
+
"apply_schema_hint": {
|
|
1433
|
+
"layers": [
|
|
1434
|
+
{
|
|
1435
|
+
"role": "string (kick/snare/hat/perc/bass/pad/lead/atmos/etc.)",
|
|
1436
|
+
"uri": "atlas URI from instruments_by_role[role]",
|
|
1437
|
+
"track_name": "optional display name",
|
|
1438
|
+
"notes": [
|
|
1439
|
+
{"pitch": "int 0-127", "start_time": "float beats from clip start",
|
|
1440
|
+
"duration": "float beats", "velocity": "int 0-127"}
|
|
1441
|
+
],
|
|
1442
|
+
"effects": [
|
|
1443
|
+
{"device": "native Live device name (e.g. Saturator, EQ Eight, Reverb)",
|
|
1444
|
+
"params": "dict of {parameter_name: value}; {} for none"}
|
|
1445
|
+
],
|
|
1446
|
+
"sends": [
|
|
1447
|
+
{"return_name": "case-insensitive return track name (e.g. A-Reverb)",
|
|
1448
|
+
"value": "float 0-1"}
|
|
1449
|
+
],
|
|
1450
|
+
"applied_technique": {
|
|
1451
|
+
"snippet": "the producer-voice snippet your design honored",
|
|
1452
|
+
"source": "video title or manual section",
|
|
1453
|
+
"source_url": "url if available",
|
|
1454
|
+
"applied_in": "how you applied it (e.g. '4-bar root sustain with sidechain LFO modulation')",
|
|
1455
|
+
},
|
|
1456
|
+
}
|
|
1457
|
+
],
|
|
1458
|
+
"scene_index": "int or null (auto-pick)",
|
|
1459
|
+
"tempo": "int (overrides intent.tempo if set)",
|
|
1460
|
+
},
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
def _suggest_layer_roles(intent: CompositionIntent) -> list[str]:
|
|
1465
|
+
"""Suggest which roles to fill based on genre, leaving the LLM free to
|
|
1466
|
+
pick fewer or rearrange. This is GUIDANCE, not prescription."""
|
|
1467
|
+
genre = (intent.genre or "").lower()
|
|
1468
|
+
if genre == "ambient":
|
|
1469
|
+
return ["pad", "atmos"]
|
|
1470
|
+
if genre == "trap":
|
|
1471
|
+
return ["kick", "snare", "hat", "bass"]
|
|
1472
|
+
if genre == "drum and bass":
|
|
1473
|
+
return ["kick", "snare", "hat", "bass"]
|
|
1474
|
+
if genre in ("dub techno", "techno"):
|
|
1475
|
+
return ["kick", "hat", "bass", "pad"]
|
|
1476
|
+
if genre in ("house", "hip hop", "lo-fi"):
|
|
1477
|
+
return ["kick", "snare", "hat", "bass", "pad"]
|
|
1478
|
+
# Default: a balanced 4-piece
|
|
1479
|
+
return ["kick", "hat", "bass", "pad"]
|