livepilot 1.24.0 → 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 +70 -0
- package/README.md +11 -11
- 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/explore_tools.py +332 -0
- package/mcp_server/atlas/tools.py +161 -0
- package/mcp_server/composer/framework/atlas_resolver.py +554 -0
- package/mcp_server/composer/framework/knowledge_pack.py +98 -7
- package/mcp_server/composer/full/brief_builder.py +91 -8
- package/mcp_server/tools/_analyzer_engine/sample.py +10 -1
- 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
|
@@ -1,27 +1,51 @@
|
|
|
1
1
|
"""KnowledgePack — assembles the vocabulary fields each compose mode's brief carries.
|
|
2
2
|
|
|
3
|
-
For full mode: rich (event_lexicon, genre_context, artist_context,
|
|
4
|
-
|
|
3
|
+
For full mode: rich (event_lexicon, genre_context, artist_context,
|
|
4
|
+
atlas_anchors, atlas_candidates_per_role, manual_snippets).
|
|
5
|
+
For fast mode: subset (no event_lexicon — fast mode is loop-sketch scope).
|
|
5
6
|
For develop mode: subset overlapping with full.
|
|
7
|
+
|
|
8
|
+
v1.25 adds `atlas_anchors` — cohort + role-anchored URIs from
|
|
9
|
+
atlas_pack_aware_compose, populated when atlas + brief_text are provided.
|
|
10
|
+
The agent uses `atlas_explore` / `atlas_audition` / `atlas_substitute`
|
|
11
|
+
mid-design to dig past anchors when needed (hybrid knowledge surface).
|
|
6
12
|
"""
|
|
7
13
|
|
|
8
14
|
from __future__ import annotations
|
|
9
|
-
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import asdict
|
|
18
|
+
from typing import Any, Optional
|
|
10
19
|
|
|
11
20
|
from .event_lexicon import EVENT_LEXICON, get_event_lexicon
|
|
12
21
|
from .genre_loader import load_genre_context
|
|
13
22
|
from .artist_loader import load_artist_context
|
|
14
23
|
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
15
26
|
|
|
16
27
|
class KnowledgePack:
|
|
17
28
|
"""Central knowledge assembly for compose-mode briefs."""
|
|
18
29
|
|
|
19
|
-
def build(
|
|
30
|
+
def build(
|
|
31
|
+
self,
|
|
32
|
+
intent: dict,
|
|
33
|
+
mode: str = "full",
|
|
34
|
+
*,
|
|
35
|
+
atlas: Any = None,
|
|
36
|
+
ableton: Any = None,
|
|
37
|
+
ctx: Any = None,
|
|
38
|
+
brief_text: str = "",
|
|
39
|
+
) -> dict:
|
|
20
40
|
"""Build the knowledge fields for a brief.
|
|
21
41
|
|
|
22
42
|
intent: parsed CompositionIntent dict with at least 'genre', possibly 'sub_genre'.
|
|
23
43
|
May also carry 'artists' (list of producer names).
|
|
24
44
|
mode: 'fast' | 'full' | 'develop'
|
|
45
|
+
atlas: AtlasManager instance (optional — when None, atlas_anchors is None)
|
|
46
|
+
ableton: ableton client (optional — reserved for v1.25.x browser fallback)
|
|
47
|
+
ctx: lifespan context (optional — reserved for taste_profile / recent_uris)
|
|
48
|
+
brief_text: the original prompt string (required for atlas_anchors)
|
|
25
49
|
|
|
26
50
|
Returns a dict — pass directly to brief.knowledge OR spread into brief top-level.
|
|
27
51
|
"""
|
|
@@ -33,11 +57,12 @@ class KnowledgePack:
|
|
|
33
57
|
artist_names = intent.get("artists") or []
|
|
34
58
|
artist_ctx = load_artist_context(artist_names)
|
|
35
59
|
|
|
36
|
-
knowledge = {
|
|
60
|
+
knowledge: dict[str, Any] = {
|
|
37
61
|
"genre_context": genre_ctx,
|
|
38
62
|
"artist_context": artist_ctx,
|
|
39
|
-
"atlas_candidates_per_role": {}, #
|
|
40
|
-
"
|
|
63
|
+
"atlas_candidates_per_role": {}, # legacy field — empty in v1.25 (replaced by atlas_anchors)
|
|
64
|
+
"atlas_anchors": None, # populated below for full mode when atlas available
|
|
65
|
+
"manual_snippets": {},
|
|
41
66
|
}
|
|
42
67
|
|
|
43
68
|
# Event lexicon — full mode only (loop sketch doesn't design form)
|
|
@@ -46,4 +71,70 @@ class KnowledgePack:
|
|
|
46
71
|
else:
|
|
47
72
|
knowledge["event_lexicon"] = []
|
|
48
73
|
|
|
74
|
+
# Atlas anchors — full mode only, requires atlas + brief_text. Best-effort:
|
|
75
|
+
# any failure path silently leaves anchors=None and the brief still works.
|
|
76
|
+
if mode == "full" and atlas is not None and brief_text:
|
|
77
|
+
try:
|
|
78
|
+
from .atlas_resolver import AtlasResolver
|
|
79
|
+
resolver = AtlasResolver(
|
|
80
|
+
atlas=atlas,
|
|
81
|
+
ableton=ableton,
|
|
82
|
+
taste_profile=_safe_get_taste_profile(ctx),
|
|
83
|
+
recent_uris=_safe_get_recent_uris(ctx),
|
|
84
|
+
)
|
|
85
|
+
mood = _extract_mood(intent)
|
|
86
|
+
anchors = resolver.resolve_anchors(
|
|
87
|
+
brief_text=brief_text,
|
|
88
|
+
genre=genre,
|
|
89
|
+
mood=mood,
|
|
90
|
+
artist_refs=artist_names,
|
|
91
|
+
)
|
|
92
|
+
knowledge["atlas_anchors"] = asdict(anchors)
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
logger.debug("KnowledgePack.build: atlas_anchors unavailable: %s", exc)
|
|
95
|
+
|
|
49
96
|
return knowledge
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ── Helpers ─────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _extract_mood(intent: dict) -> str:
|
|
103
|
+
"""Derive a mood string from the parsed intent.
|
|
104
|
+
|
|
105
|
+
Combines mood + descriptors fields when present. Falls back to "" so
|
|
106
|
+
the resolver's mood-overlap boost simply doesn't fire (rather than
|
|
107
|
+
matching against junk).
|
|
108
|
+
"""
|
|
109
|
+
parts: list[str] = []
|
|
110
|
+
for key in ("mood", "descriptors", "modifiers"):
|
|
111
|
+
val = intent.get(key)
|
|
112
|
+
if isinstance(val, str) and val:
|
|
113
|
+
parts.append(val)
|
|
114
|
+
elif isinstance(val, (list, tuple)):
|
|
115
|
+
parts.extend(str(v) for v in val if v)
|
|
116
|
+
return " ".join(parts).strip()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _safe_get_taste_profile(ctx: Any) -> Optional[dict]:
|
|
120
|
+
"""Best-effort taste profile fetch. Returns None on any failure."""
|
|
121
|
+
if ctx is None:
|
|
122
|
+
return None
|
|
123
|
+
try:
|
|
124
|
+
# The taste graph tools live under mcp_server/tools/agent_os.py;
|
|
125
|
+
# importing here would create a cycle, so leave as None for now.
|
|
126
|
+
# v1.25.x will wire in get_taste_profile() once the cycle is broken.
|
|
127
|
+
return None
|
|
128
|
+
except Exception:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _safe_get_recent_uris(ctx: Any) -> Optional[set[str]]:
|
|
133
|
+
"""Best-effort recent-URIs fetch (§7 #2 anti-repeat). Returns None on failure."""
|
|
134
|
+
if ctx is None:
|
|
135
|
+
return None
|
|
136
|
+
try:
|
|
137
|
+
# Same cycle concern as _safe_get_taste_profile — defer to v1.25.x.
|
|
138
|
+
return None
|
|
139
|
+
except Exception:
|
|
140
|
+
return None
|
|
@@ -46,12 +46,62 @@ _DESIGN_TARGETS = (
|
|
|
46
46
|
"the song. Use the event_lexicon as a vocabulary of named structural "
|
|
47
47
|
"moves to schedule at chosen phrase boundaries. For niche style references "
|
|
48
48
|
"in research_hooks, run WebSearch to ground your form choices in the "
|
|
49
|
-
"actual conventions of that subgenre
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
49
|
+
"actual conventions of that subgenre.\n\n"
|
|
50
|
+
"INSTRUMENT SELECTION (v1.25 hybrid knowledge surface — MANDATED FOUR-SOURCE SEARCH):\n"
|
|
51
|
+
"The brief's `atlas_anchors` is ONE source. Before committing any role pick "
|
|
52
|
+
"you MUST also query the other three sources below. Factory-atlas-only picks "
|
|
53
|
+
"have repeatedly missed canonical user-curated instruments (e.g., the 808 Trap "
|
|
54
|
+
"Selector Rack from Trap Drums by Sound Oracle pack lives in the packs overlay, "
|
|
55
|
+
"not the factory tag index). Always union BEFORE deciding.\n\n"
|
|
56
|
+
"Source 1 — Factory atlas (already surfaced in atlas_anchors). Three tools:\n"
|
|
57
|
+
" • atlas_audition(uri) — full sidecar dump for a chosen URI: character "
|
|
58
|
+
"tags, signature_techniques, producer-curated macro values, related demos. "
|
|
59
|
+
"Call this BEFORE committing to a candidate when its tags alone aren't "
|
|
60
|
+
"enough to know if it fits the section.\n"
|
|
61
|
+
" • atlas_explore(role, mood, genre, artists?) — refined per-role query "
|
|
62
|
+
"when an anchor doesn't fit the section's purpose, or when you need siblings "
|
|
63
|
+
"of a role pick. Returns 3-5 ranked candidates with reasoning trails.\n"
|
|
64
|
+
" • atlas_substitute(current_uri, anti_tag) — anti-tag-driven swap to use "
|
|
65
|
+
"AFTER analyze_sound_design or analyze_mix flags an issue (\"too bright\", "
|
|
66
|
+
"\"too aggressive\", \"too sparse\", \"muddy\", \"static\", \"generic\"). "
|
|
67
|
+
"Returns 3 alternatives that explicitly avoid the unwanted property.\n\n"
|
|
68
|
+
"Source 2 — User corpus (mandatory union — ~/.livepilot/atlas-overlays/):\n"
|
|
69
|
+
" • extension_atlas_search(query=\"<role>\", limit=10) — searches all four "
|
|
70
|
+
"overlay namespaces: `packs` (Ableton factory packs with hidden_gems, "
|
|
71
|
+
"notable_presets, signature_workflows fields), `m4l-devices` (curated M4L "
|
|
72
|
+
"device knowledge), `user.*` (your scanned .amxd / plugin / preset library), "
|
|
73
|
+
"`elektron` (hardware-mirror chains). Producer-curated rack instruments "
|
|
74
|
+
"(808 Trap Selector Rack, Harmonic Drone Generator, etc.) live here, NOT "
|
|
75
|
+
"in the factory tag index. Always run this query alongside atlas_explore.\n"
|
|
76
|
+
" • extension_atlas_search(query=\"<role-or-aesthetic>\", entity_type=\"demo_project\") — "
|
|
77
|
+
"GROUND-TRUTH ROLE→URI MAPPING. The packs namespace contains 100+ analyzed "
|
|
78
|
+
"demo .als project parses; each carries actual track-by-track instrument "
|
|
79
|
+
"URIs proven on real Ableton-shipped demos. For 808 bass, query "
|
|
80
|
+
"`demo_project` to find which pack-included .als demos use 808 bass and "
|
|
81
|
+
"what URI they loaded — the highest-confidence source for any role.\n"
|
|
82
|
+
" • extension_atlas_get(namespace, entity_id) — full body of a chosen entry "
|
|
83
|
+
"including hidden_gems and signature_workflows fields the search summary trims.\n\n"
|
|
84
|
+
"Source 3 — Anthropic Ableton Knowledge MCP (mcp__Ableton_Knowledge__*):\n"
|
|
85
|
+
" • search_transcripts(query) — Ableton's official tutorial video transcripts; "
|
|
86
|
+
"ground-truth pedagogy on how producers actually use the device.\n"
|
|
87
|
+
" • search_live_manual(query) — live manual snippets for any device or feature.\n"
|
|
88
|
+
" • search_knowledge_base(query) — broader Ableton knowledge base.\n"
|
|
89
|
+
" • search_videos(query) — official tutorial video metadata.\n"
|
|
90
|
+
" Run these for the ROLE term (e.g., \"808 bass\", \"sidechain compression\") "
|
|
91
|
+
"and for any artist/genre reference in the prompt to ground your design choices.\n\n"
|
|
92
|
+
"FOUR-SOURCE SEARCH PROTOCOL per role:\n"
|
|
93
|
+
" 1. Read atlas_anchors[role] (Source 1 starting point).\n"
|
|
94
|
+
" 2. Call atlas_explore(role, mood, genre, artists) (Source 1 ranked alts).\n"
|
|
95
|
+
" 3. Call extension_atlas_search(query=role) and extension_atlas_search(query=role, "
|
|
96
|
+
"entity_type=\"demo_project\") (Source 2 user corpus + ground-truth demos).\n"
|
|
97
|
+
" 4. Call mcp__Ableton_Knowledge__search_transcripts(query=role) for context (Source 3).\n"
|
|
98
|
+
" 5. Union results, score against brief's mood/aesthetic, then commit.\n\n"
|
|
99
|
+
"The framework's job was to surface the corpus. The picks are yours. "
|
|
100
|
+
"Submit your design as a plan to compose_full_apply with: per-track variant "
|
|
101
|
+
"clips at chosen scene slots, per-section arrangement_clip placements "
|
|
102
|
+
"referencing those variants, and structural events scheduled at phrase "
|
|
103
|
+
"boundaries. The form is YOUR creative product — vocabularies tell you "
|
|
104
|
+
"what techno or BoC sound like, they do not tell you the bar count of an intro."
|
|
55
105
|
)
|
|
56
106
|
|
|
57
107
|
|
|
@@ -122,11 +172,21 @@ def build_full_brief(
|
|
|
122
172
|
artist_refs = extract_artist_refs(prompt or "")
|
|
123
173
|
research_hooks = detect_research_hooks(prompt or "")
|
|
124
174
|
|
|
125
|
-
# Build knowledge pack — populates genre_context, artist_context, event_lexicon
|
|
175
|
+
# Build knowledge pack — populates genre_context, artist_context, event_lexicon,
|
|
176
|
+
# AND v1.25 atlas_anchors when atlas + brief_text are available.
|
|
126
177
|
if artist_refs:
|
|
127
178
|
intent_dict["artists"] = artist_refs
|
|
128
179
|
kp = KnowledgePack()
|
|
129
|
-
|
|
180
|
+
atlas_obj = _safe_get_atlas(ctx)
|
|
181
|
+
ableton_obj = _safe_get_ableton(ctx)
|
|
182
|
+
knowledge = kp.build(
|
|
183
|
+
intent_dict,
|
|
184
|
+
mode="full",
|
|
185
|
+
atlas=atlas_obj,
|
|
186
|
+
ableton=ableton_obj,
|
|
187
|
+
ctx=ctx,
|
|
188
|
+
brief_text=prompt or "",
|
|
189
|
+
)
|
|
130
190
|
|
|
131
191
|
return {
|
|
132
192
|
"mode": "full",
|
|
@@ -137,8 +197,31 @@ def build_full_brief(
|
|
|
137
197
|
"artist_context": knowledge["artist_context"],
|
|
138
198
|
"event_lexicon": knowledge["event_lexicon"],
|
|
139
199
|
"atlas_candidates_per_role": knowledge["atlas_candidates_per_role"],
|
|
200
|
+
"atlas_anchors": knowledge.get("atlas_anchors"),
|
|
140
201
|
"manual_snippets": knowledge["manual_snippets"],
|
|
141
202
|
"seed_state": seed_state,
|
|
142
203
|
"research_hooks": research_hooks,
|
|
143
204
|
"design_targets": _DESIGN_TARGETS,
|
|
144
205
|
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ── Lifespan-context helpers ───────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _safe_get_atlas(ctx: Any) -> Optional[Any]:
|
|
212
|
+
"""Best-effort atlas fetch. Returns None on any failure."""
|
|
213
|
+
try:
|
|
214
|
+
from ...atlas import get_atlas
|
|
215
|
+
return get_atlas()
|
|
216
|
+
except Exception:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _safe_get_ableton(ctx: Any) -> Optional[Any]:
|
|
221
|
+
"""Best-effort ableton-client fetch from lifespan_context. None on miss."""
|
|
222
|
+
try:
|
|
223
|
+
if ctx is not None and hasattr(ctx, "lifespan_context"):
|
|
224
|
+
return ctx.lifespan_context.get("ableton")
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
return None
|
|
@@ -258,12 +258,21 @@ async def _simpler_post_load_hygiene(
|
|
|
258
258
|
# Step 4: auto-detect drum root note from filename (BUG-2026-04-22#18).
|
|
259
259
|
# Only applied for one-shots — warped loops keep Live's default root
|
|
260
260
|
# because their root note is irrelevant at loop playback speeds.
|
|
261
|
+
#
|
|
262
|
+
# 2026-05-02 — fixed param name: was "Sample Pitch Coarse" (doesn't exist
|
|
263
|
+
# on OriginalSimpler — silently failed). Correct param is "Transpose"
|
|
264
|
+
# (semitone offset from C3=60). Convert detected drum root → Transpose:
|
|
265
|
+
# Transpose = 60 - drum_root. Example: drum_root=36 (C1) → Transpose=+24,
|
|
266
|
+
# so triggering MIDI 36 plays the sample at original recorded pitch.
|
|
261
267
|
drum_root = None
|
|
262
268
|
if not is_loop:
|
|
263
269
|
drum_root = _detect_drum_root_note(file_path)
|
|
264
270
|
if drum_root is not None:
|
|
271
|
+
transpose_value = 60 - int(drum_root)
|
|
272
|
+
# Clamp to Simpler's Transpose range (-48..+48 semitones)
|
|
273
|
+
transpose_value = max(-48, min(48, transpose_value))
|
|
265
274
|
hygiene_params.append(
|
|
266
|
-
{"name_or_index": "
|
|
275
|
+
{"name_or_index": "Transpose", "value": transpose_value}
|
|
267
276
|
)
|
|
268
277
|
|
|
269
278
|
try:
|
|
@@ -158,28 +158,57 @@ def search_browser(
|
|
|
158
158
|
return _get_ableton(ctx).send_command("search_browser", params)
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
#
|
|
161
|
+
# M4L instrument post-load hygiene — 2026-05-02.
|
|
162
|
+
# Some Max-for-Live instruments load with defaults that immediately produce loud
|
|
163
|
+
# unwanted output (Harmonic Drone Generator from Drone Lab is the canonical
|
|
164
|
+
# example: Latch on + Density 80% + Volume −6 dB + all 8 voices active = a wall
|
|
165
|
+
# of sustained drone the moment any MIDI note touches it). Apply tames here so
|
|
166
|
+
# the device is workable on first load. Each entry maps a device-name match
|
|
167
|
+
# (substring) to a list of (parameter_name, value) pairs.
|
|
168
|
+
#
|
|
169
|
+
# Detection runs UNCONDITIONALLY (not gated on `role` like _SIMPLER_ROLE_DEFAULTS)
|
|
170
|
+
# because these M4L instruments are typically loaded without a role parameter.
|
|
171
|
+
_M4L_INSTRUMENT_HYGIENE: dict[str, list[tuple[str, float]]] = {
|
|
172
|
+
"Harmonic Drone Generator": [
|
|
173
|
+
("Latch", 0), # Off — prevents indefinite note sustain after one trigger
|
|
174
|
+
("Volume", -40), # ≈ -20 dB display (default is -18 / -6 dB which is too loud)
|
|
175
|
+
("Density", 40), # 40% (default 80% is too dense for a background bed)
|
|
176
|
+
],
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# Role-aware Simpler defaults — BUG-2026-04-22 #17 + #18, plus 2026-05-02 fix.
|
|
162
181
|
# Each role maps to a list of (parameter_name, value) pairs applied after
|
|
163
182
|
# load via set_device_parameter. Trigger Mode polarity per BUG #9:
|
|
164
|
-
# 0 = Trigger (one-shot), 1 = Gate (held). Volume in dB.
|
|
183
|
+
# 0 = Trigger (one-shot), 1 = Gate (held). Volume in dB. Transpose in semitones.
|
|
184
|
+
#
|
|
185
|
+
# 2026-05-02 — fixed pitch-shift bug:
|
|
186
|
+
# Earlier versions used "Sample Pitch Coarse" param name, which DOES NOT EXIST
|
|
187
|
+
# on OriginalSimpler — the call silently raised and was swallowed. Result: every
|
|
188
|
+
# drum-role Simpler played 24 semitones below original pitch ("super low" sound)
|
|
189
|
+
# because the Simpler's default sample root is C3 (60), but drum convention sends
|
|
190
|
+
# MIDI 36 (C1). The correct parameter is "Transpose" (range -48..+48 semitones);
|
|
191
|
+
# +24 compensates for the C3-vs-C1 mismatch so drum samples play at original
|
|
192
|
+
# recorded pitch when MIDI 36 is sent. Melodic/texture roles use Transpose=0
|
|
193
|
+
# because their default playback range centers on C3 (60) — no compensation needed.
|
|
165
194
|
_SIMPLER_ROLE_DEFAULTS = {
|
|
166
195
|
"drum": [
|
|
167
196
|
("Snap", 0),
|
|
168
197
|
("Volume", 0.0),
|
|
169
198
|
("Trigger Mode", 0), # Trigger / one-shot
|
|
170
|
-
("
|
|
199
|
+
("Transpose", 24), # Compensate C3-default → C1-drum-convention root
|
|
171
200
|
],
|
|
172
201
|
"melodic": [
|
|
173
202
|
("Snap", 1),
|
|
174
203
|
("Volume", 0.0),
|
|
175
204
|
("Trigger Mode", 1), # Gate / held
|
|
176
|
-
("
|
|
205
|
+
("Transpose", 0), # C3 default — melodic input range
|
|
177
206
|
],
|
|
178
207
|
"texture": [
|
|
179
208
|
("Snap", 0),
|
|
180
209
|
("Volume", -6.0),
|
|
181
210
|
("Trigger Mode", 1), # Gate
|
|
182
|
-
("
|
|
211
|
+
("Transpose", 0), # C3 default — sustained-input range
|
|
183
212
|
],
|
|
184
213
|
}
|
|
185
214
|
|
|
@@ -238,26 +267,80 @@ def load_browser_item(
|
|
|
238
267
|
"uri": uri,
|
|
239
268
|
})
|
|
240
269
|
|
|
241
|
-
# Post-load:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
270
|
+
# Post-load: probe the loaded device once, then apply two layers of hygiene.
|
|
271
|
+
#
|
|
272
|
+
# 2026-05-02 — fixed device-detection bug. The TCP load_browser_item command
|
|
273
|
+
# returns {loaded, name, device_count} with NO device_index and NO class_name,
|
|
274
|
+
# so the previous detection (`result.get("device_index")` / `result.get("class_name")`)
|
|
275
|
+
# always failed and the role-defaults branch was never entered. Resolution:
|
|
276
|
+
# treat newly-loaded sample-on-empty-track as device_index=0 (Live places the
|
|
277
|
+
# instrument at chain head) and verify class + name via get_device_info.
|
|
278
|
+
#
|
|
279
|
+
# Layer 1 (gated on `role`): Simpler role-aware defaults — Snap/Volume/
|
|
280
|
+
# Trigger Mode/Transpose for drum/melodic/texture roles.
|
|
281
|
+
# Layer 2 (unconditional): M4L instrument hygiene — name-matched tames for
|
|
282
|
+
# known problem devices (Harmonic Drone Generator's Latch + loud defaults).
|
|
283
|
+
device_index_resolved: Optional[int] = None
|
|
284
|
+
device_class = ""
|
|
285
|
+
device_name_loaded = ""
|
|
286
|
+
if isinstance(result, dict) and result.get("loaded") and not result.get("error"):
|
|
287
|
+
device_index_resolved = result.get("device_index")
|
|
288
|
+
try:
|
|
289
|
+
probe = ableton.send_command("get_device_info", {
|
|
290
|
+
"track_index": track_index,
|
|
291
|
+
"device_index": 0,
|
|
292
|
+
})
|
|
293
|
+
device_class = str(probe.get("class_name", "") or "")
|
|
294
|
+
device_name_loaded = str(probe.get("name", "") or result.get("name", "") or "")
|
|
295
|
+
if device_index_resolved is None:
|
|
296
|
+
device_index_resolved = 0
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
# Layer 1 — Simpler role-aware defaults
|
|
301
|
+
if role and device_index_resolved is not None and "Simpler" in device_class:
|
|
302
|
+
applied = []
|
|
303
|
+
for name, value in _SIMPLER_ROLE_DEFAULTS[role]:
|
|
304
|
+
try:
|
|
305
|
+
ableton.send_command("set_device_parameter", {
|
|
306
|
+
"track_index": track_index,
|
|
307
|
+
"device_index": int(device_index_resolved),
|
|
308
|
+
"parameter_name": name,
|
|
309
|
+
"value": value,
|
|
310
|
+
})
|
|
311
|
+
applied.append({"parameter": name, "value": value})
|
|
312
|
+
except Exception as exc:
|
|
313
|
+
# Don't fail the whole load if one default doesn't apply
|
|
314
|
+
# (parameter name might not exist on every Simpler variant).
|
|
315
|
+
applied.append({"parameter": name, "skipped": str(exc)})
|
|
316
|
+
result["role"] = role
|
|
317
|
+
result["role_defaults_applied"] = applied
|
|
318
|
+
result["device_class"] = device_class
|
|
319
|
+
|
|
320
|
+
# Layer 2 — M4L instrument hygiene (unconditional, name-matched).
|
|
321
|
+
# Detects Harmonic Drone Generator and other known problem M4L instruments
|
|
322
|
+
# by name substring, applies tame defaults to prevent loud-on-load surprises.
|
|
323
|
+
if device_index_resolved is not None and device_name_loaded:
|
|
324
|
+
for hygiene_name, params in _M4L_INSTRUMENT_HYGIENE.items():
|
|
325
|
+
if hygiene_name not in device_name_loaded:
|
|
326
|
+
continue
|
|
327
|
+
applied_hygiene = []
|
|
328
|
+
for name, value in params:
|
|
248
329
|
try:
|
|
249
330
|
ableton.send_command("set_device_parameter", {
|
|
250
331
|
"track_index": track_index,
|
|
251
|
-
"device_index": int(
|
|
332
|
+
"device_index": int(device_index_resolved),
|
|
252
333
|
"parameter_name": name,
|
|
253
334
|
"value": value,
|
|
254
335
|
})
|
|
255
|
-
|
|
336
|
+
applied_hygiene.append({"parameter": name, "value": value})
|
|
256
337
|
except Exception as exc:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
338
|
+
applied_hygiene.append({"parameter": name, "skipped": str(exc)})
|
|
339
|
+
result["m4l_hygiene"] = {
|
|
340
|
+
"device_name": hygiene_name,
|
|
341
|
+
"applied": applied_hygiene,
|
|
342
|
+
}
|
|
343
|
+
result.setdefault("device_class", device_class)
|
|
344
|
+
break # one hygiene match per load
|
|
262
345
|
|
|
263
346
|
return result
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
|
-
"description": "Agentic production system for Ableton Live 12 \u2014
|
|
5
|
+
"description": "Agentic production system for Ableton Live 12 \u2014 462 tools, 55 domains, 44 semantic moves. Device atlas (5264 devices, 120 enriched, 7 indexes), Splice intelligence (gRPC + GraphQL describe-a-sound + preview + collections + presets), 9-band spectral perception auto-loaded via ensure_analyzer_on_master, Creative Director skill, technique memory, 12 creative intelligence engines",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
7
|
"license": "BSL-1.1",
|
|
8
8
|
"type": "commonjs",
|
|
@@ -5,7 +5,7 @@ Entry point for the ControlSurface. Ableton calls create_instance(c_instance)
|
|
|
5
5
|
when this script is selected in Preferences > Link, Tempo & MIDI.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "1.
|
|
8
|
+
__version__ = "1.25.0"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from . import router
|
package/server.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.dreamrec/livepilot",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "462-tool agentic MCP production system for Ableton Live 12 \u2014 55 domains, 44 semantic moves, device atlas (5264 devices), Splice intelligence (gRPC + GraphQL), 9-band spectral perception auto-loaded, Creative Director skill, technique memory, 12 creative engines",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/dreamrec/LivePilot",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.25.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "livepilot",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.25.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|