livepilot 1.10.5 → 1.10.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/.mcp.json.disabled +9 -0
- package/.mcpbignore +3 -0
- package/AGENTS.md +3 -3
- package/BUGS.md +1570 -0
- package/CHANGELOG.md +92 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +7 -7
- package/bin/livepilot.js +28 -8
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +4 -4
- package/livepilot/skills/livepilot-core/references/overview.md +2 -2
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +8 -8
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/LivePilot_Analyzer.amxd.pre-presentation-backup +0 -0
- package/m4l_device/LivePilot_Analyzer.maxproj +53 -0
- package/m4l_device/livepilot_bridge.js +226 -3
- package/manifest.json +3 -3
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +93 -26
- package/mcp_server/composer/sample_resolver.py +10 -6
- package/mcp_server/composer/tools.py +10 -6
- package/mcp_server/connection.py +6 -1
- package/mcp_server/creative_constraints/tools.py +214 -40
- package/mcp_server/experiment/engine.py +16 -14
- package/mcp_server/experiment/tools.py +9 -9
- package/mcp_server/hook_hunter/analyzer.py +62 -9
- package/mcp_server/hook_hunter/tools.py +74 -18
- package/mcp_server/m4l_bridge.py +32 -6
- package/mcp_server/memory/taste_graph.py +7 -2
- package/mcp_server/mix_engine/tools.py +8 -3
- package/mcp_server/musical_intelligence/detectors.py +32 -0
- package/mcp_server/musical_intelligence/tools.py +15 -10
- package/mcp_server/performance_engine/tools.py +117 -30
- package/mcp_server/preview_studio/engine.py +89 -8
- package/mcp_server/preview_studio/tools.py +43 -21
- package/mcp_server/project_brain/automation_graph.py +71 -19
- package/mcp_server/project_brain/builder.py +2 -0
- package/mcp_server/project_brain/tools.py +73 -15
- package/mcp_server/reference_engine/profile_builder.py +129 -3
- package/mcp_server/reference_engine/tools.py +54 -11
- package/mcp_server/runtime/capability_probe.py +10 -4
- package/mcp_server/runtime/execution_router.py +50 -0
- package/mcp_server/runtime/mcp_dispatch.py +75 -3
- package/mcp_server/runtime/remote_commands.py +4 -2
- package/mcp_server/runtime/tools.py +8 -2
- package/mcp_server/sample_engine/analyzer.py +131 -4
- package/mcp_server/sample_engine/critics.py +29 -8
- package/mcp_server/sample_engine/models.py +20 -1
- package/mcp_server/sample_engine/tools.py +74 -31
- package/mcp_server/semantic_moves/sound_design_compilers.py +22 -59
- package/mcp_server/semantic_moves/tools.py +5 -1
- package/mcp_server/semantic_moves/transition_compilers.py +12 -19
- package/mcp_server/server.py +78 -11
- package/mcp_server/services/motif_service.py +9 -3
- package/mcp_server/session_continuity/models.py +4 -0
- package/mcp_server/session_continuity/tools.py +7 -3
- package/mcp_server/session_continuity/tracker.py +23 -9
- package/mcp_server/song_brain/builder.py +110 -12
- package/mcp_server/song_brain/tools.py +94 -25
- package/mcp_server/sound_design/tools.py +112 -1
- package/mcp_server/splice_client/client.py +19 -6
- package/mcp_server/stuckness_detector/detector.py +90 -0
- package/mcp_server/stuckness_detector/tools.py +49 -5
- package/mcp_server/tools/_agent_os_engine/__init__.py +52 -0
- package/mcp_server/tools/_agent_os_engine/critics.py +158 -0
- package/mcp_server/tools/_agent_os_engine/evaluation.py +206 -0
- package/mcp_server/tools/_agent_os_engine/models.py +132 -0
- package/mcp_server/tools/_agent_os_engine/taste.py +192 -0
- package/mcp_server/tools/_agent_os_engine/techniques.py +161 -0
- package/mcp_server/tools/_agent_os_engine/world_model.py +170 -0
- package/mcp_server/tools/_composition_engine/__init__.py +67 -0
- package/mcp_server/tools/_composition_engine/analysis.py +174 -0
- package/mcp_server/tools/_composition_engine/critics.py +522 -0
- package/mcp_server/tools/_composition_engine/gestures.py +230 -0
- package/mcp_server/tools/_composition_engine/harmony.py +160 -0
- package/mcp_server/tools/_composition_engine/models.py +193 -0
- package/mcp_server/tools/_composition_engine/sections.py +414 -0
- package/mcp_server/tools/_harmony_engine.py +52 -8
- package/mcp_server/tools/_perception_engine.py +18 -11
- package/mcp_server/tools/_research_engine.py +98 -19
- package/mcp_server/tools/_theory_engine.py +138 -9
- package/mcp_server/tools/agent_os.py +43 -18
- package/mcp_server/tools/analyzer.py +105 -8
- package/mcp_server/tools/automation.py +6 -1
- package/mcp_server/tools/clips.py +45 -0
- package/mcp_server/tools/composition.py +90 -38
- package/mcp_server/tools/devices.py +32 -7
- package/mcp_server/tools/harmony.py +115 -14
- package/mcp_server/tools/midi_io.py +13 -1
- package/mcp_server/tools/mixing.py +35 -1
- package/mcp_server/tools/motif.py +56 -5
- package/mcp_server/tools/planner.py +6 -2
- package/mcp_server/tools/research.py +37 -10
- package/mcp_server/tools/theory.py +108 -16
- package/mcp_server/transition_engine/critics.py +18 -11
- package/mcp_server/transition_engine/tools.py +6 -1
- package/mcp_server/translation_engine/tools.py +8 -6
- package/mcp_server/wonder_mode/engine.py +8 -3
- package/mcp_server/wonder_mode/tools.py +29 -21
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +57 -2
- package/remote_script/LivePilot/clips.py +69 -0
- package/remote_script/LivePilot/mixing.py +117 -0
- package/remote_script/LivePilot/router.py +13 -1
- package/scripts/generate_tool_catalog.py +13 -38
- package/scripts/sync_metadata.py +231 -14
- package/mcp_server/tools/_agent_os_engine.py +0 -947
- package/mcp_server/tools/_composition_engine.py +0 -1530
|
@@ -7,19 +7,23 @@
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import logging
|
|
11
|
+
|
|
10
12
|
from fastmcp import Context
|
|
11
13
|
|
|
12
14
|
from ..server import mcp
|
|
13
15
|
from . import engine
|
|
14
16
|
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
def _get_song_brain_dict() -> dict:
|
|
17
21
|
try:
|
|
18
22
|
from ..song_brain.tools import _current_brain
|
|
19
23
|
if _current_brain is not None:
|
|
20
24
|
return _current_brain.to_dict()
|
|
21
|
-
except Exception:
|
|
22
|
-
|
|
25
|
+
except Exception as exc:
|
|
26
|
+
logger.warning("song_brain lookup failed: %s", exc)
|
|
23
27
|
return {}
|
|
24
28
|
|
|
25
29
|
|
|
@@ -37,8 +41,8 @@ def _get_taste_graph(ctx: Context):
|
|
|
37
41
|
taste_store=taste_store, anti_store=anti_store,
|
|
38
42
|
persistent_store=persistent,
|
|
39
43
|
)
|
|
40
|
-
except Exception:
|
|
41
|
-
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
logger.warning("taste_graph build failed: %s", exc)
|
|
42
46
|
return None
|
|
43
47
|
|
|
44
48
|
|
|
@@ -47,7 +51,8 @@ def _get_active_constraints():
|
|
|
47
51
|
try:
|
|
48
52
|
from ..creative_constraints.tools import _active_constraints
|
|
49
53
|
return _active_constraints
|
|
50
|
-
except Exception:
|
|
54
|
+
except Exception as exc:
|
|
55
|
+
logger.debug("creative_constraints not importable: %s", exc)
|
|
51
56
|
return None
|
|
52
57
|
|
|
53
58
|
|
|
@@ -60,7 +65,8 @@ def _get_ledger_entries(ctx: Context) -> list[dict]:
|
|
|
60
65
|
)
|
|
61
66
|
entries = ledger.get_recent_moves(limit=20)
|
|
62
67
|
return [e.to_dict() for e in entries]
|
|
63
|
-
except Exception:
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
logger.warning("action_ledger recent_moves failed: %s", exc)
|
|
64
70
|
return []
|
|
65
71
|
|
|
66
72
|
|
|
@@ -77,15 +83,16 @@ def _get_stuckness_report(ctx: Context, song_brain: dict) -> dict | None:
|
|
|
77
83
|
ableton = ctx.lifespan_context.get("ableton")
|
|
78
84
|
if ableton:
|
|
79
85
|
session_info = ableton.send_command("get_session_info", {})
|
|
80
|
-
except Exception:
|
|
81
|
-
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
logger.warning("session_info fetch for stuckness failed: %s", exc)
|
|
82
88
|
report = detect_stuckness(
|
|
83
89
|
action_history=action_ledger,
|
|
84
90
|
session_info=session_info,
|
|
85
91
|
song_brain=song_brain,
|
|
86
92
|
)
|
|
87
93
|
return report.to_dict()
|
|
88
|
-
except Exception:
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
logger.warning("stuckness detection failed: %s", exc)
|
|
89
96
|
return None
|
|
90
97
|
|
|
91
98
|
|
|
@@ -144,8 +151,9 @@ def enter_wonder_mode(
|
|
|
144
151
|
sample_context["sample_file_path"] = best.get("file_path", "")
|
|
145
152
|
sample_context["sample_name"] = best.get("name", "")
|
|
146
153
|
sample_context["material_type"] = best.get("material_type", "")
|
|
147
|
-
except Exception:
|
|
148
|
-
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
# Graceful degradation — analytical variants still work
|
|
156
|
+
logger.warning("sample opportunity search failed: %s", exc)
|
|
149
157
|
|
|
150
158
|
# 1c. Get session info for kernel
|
|
151
159
|
session_info = {}
|
|
@@ -153,8 +161,8 @@ def enter_wonder_mode(
|
|
|
153
161
|
ableton = ctx.lifespan_context.get("ableton")
|
|
154
162
|
if ableton:
|
|
155
163
|
session_info = ableton.send_command("get_session_info", {})
|
|
156
|
-
except Exception:
|
|
157
|
-
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
logger.warning("session_info fetch for kernel failed: %s", exc)
|
|
158
166
|
|
|
159
167
|
# 2. Generate variants
|
|
160
168
|
result = engine.generate_wonder_variants(
|
|
@@ -194,8 +202,8 @@ def enter_wonder_mode(
|
|
|
194
202
|
domain=thread_domain,
|
|
195
203
|
)
|
|
196
204
|
ws.creative_thread_id = thread.thread_id
|
|
197
|
-
except Exception:
|
|
198
|
-
|
|
205
|
+
except Exception as exc:
|
|
206
|
+
logger.warning("open creative thread failed: %s", exc)
|
|
199
207
|
|
|
200
208
|
# 5. Store session
|
|
201
209
|
store_wonder_session(ws)
|
|
@@ -290,8 +298,8 @@ def discard_wonder_session(
|
|
|
290
298
|
identity_effect="",
|
|
291
299
|
user_sentiment="disliked",
|
|
292
300
|
)
|
|
293
|
-
except Exception:
|
|
294
|
-
|
|
301
|
+
except Exception as exc:
|
|
302
|
+
logger.warning("record_turn_resolution(rejected) failed: %s", exc)
|
|
295
303
|
|
|
296
304
|
# Update taste graph — rejection is a negative signal for all executable variants
|
|
297
305
|
try:
|
|
@@ -304,16 +312,16 @@ def discard_wonder_session(
|
|
|
304
312
|
family=v["family"],
|
|
305
313
|
kept=False,
|
|
306
314
|
)
|
|
307
|
-
except Exception:
|
|
308
|
-
|
|
315
|
+
except Exception as exc:
|
|
316
|
+
logger.warning("taste_graph negative-signal update failed: %s", exc)
|
|
309
317
|
|
|
310
318
|
# Discard linked preview set
|
|
311
319
|
if ws.preview_set_id:
|
|
312
320
|
try:
|
|
313
321
|
from ..preview_studio.engine import discard_set
|
|
314
322
|
discard_set(ws.preview_set_id)
|
|
315
|
-
except Exception:
|
|
316
|
-
|
|
323
|
+
except Exception as exc:
|
|
324
|
+
logger.warning("discard_set(%s) failed: %s", ws.preview_set_id, exc)
|
|
317
325
|
|
|
318
326
|
return {
|
|
319
327
|
"discarded": True,
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.7",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
|
-
"description": "Agentic production system for Ableton Live 12 —
|
|
5
|
+
"description": "Agentic production system for Ableton Live 12 — 323 tools, 45 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 engines)",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
7
|
"license": "BSL-1.1",
|
|
8
8
|
"type": "commonjs",
|
|
@@ -5,9 +5,10 @@ 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.10.
|
|
8
|
+
__version__ = "1.10.7"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
|
+
from . import router
|
|
11
12
|
from .server import LivePilotServer
|
|
12
13
|
from . import transport # noqa: F401 — registers transport handlers
|
|
13
14
|
from . import tracks # noqa: F401 — registers track handlers
|
|
@@ -23,8 +24,62 @@ from . import clip_automation # noqa: F401 — registers clip automation hand
|
|
|
23
24
|
from . import version_detect # noqa: F401 — version detection
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
# ── Reload plumbing (BUG-B-reload, Batch 20) ──────────────────────────────
|
|
28
|
+
# Ableton keeps `sys.modules["LivePilot.*"]` cached across Control Surface
|
|
29
|
+
# toggles. Without intervention, edits to handler files don't take effect
|
|
30
|
+
# until a full Ableton restart — the toggle just re-instantiates the
|
|
31
|
+
# ControlSurface class without re-importing submodules.
|
|
32
|
+
#
|
|
33
|
+
# Fix: track whether this is the first create_instance call per
|
|
34
|
+
# interpreter lifetime. On subsequent calls, force-reload the router
|
|
35
|
+
# (which clears _handlers) and every handler module (which re-fires
|
|
36
|
+
# @register decorators with the updated code). Result: a Control Surface
|
|
37
|
+
# toggle now behaves like a fresh module reload, so live-editing mixing.py
|
|
38
|
+
# / devices.py / etc. and re-toggling is enough — no Ableton restart.
|
|
39
|
+
|
|
40
|
+
_FIRST_CREATE_INSTANCE = True
|
|
41
|
+
|
|
42
|
+
_HANDLER_MODULES = (
|
|
43
|
+
transport, tracks, clips, notes, devices, scenes,
|
|
44
|
+
mixing, browser, arrangement, diagnostics,
|
|
45
|
+
clip_automation, version_detect,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _force_reload_handlers():
|
|
50
|
+
"""Force Python to re-read the handler modules from disk.
|
|
51
|
+
|
|
52
|
+
Called on every create_instance() except the first, so edits to
|
|
53
|
+
handler files take effect via Control Surface toggle without
|
|
54
|
+
restarting Ableton. Order matters: router first (clears _handlers),
|
|
55
|
+
then each handler module (re-registers its @register decorators).
|
|
56
|
+
"""
|
|
57
|
+
import importlib
|
|
58
|
+
try:
|
|
59
|
+
importlib.reload(router)
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
for mod in _HANDLER_MODULES:
|
|
63
|
+
try:
|
|
64
|
+
importlib.reload(mod)
|
|
65
|
+
except Exception:
|
|
66
|
+
# Don't block Ableton startup on a single bad reload —
|
|
67
|
+
# the stale version will still work for that handler
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
26
71
|
def create_instance(c_instance):
|
|
27
|
-
"""Factory function called by Ableton Live.
|
|
72
|
+
"""Factory function called by Ableton Live.
|
|
73
|
+
|
|
74
|
+
Called once on initial Control Surface enable, AND every time the
|
|
75
|
+
user toggles the Control Surface off and on. The reload path below
|
|
76
|
+
makes the toggle behave like a fresh import — crucial for dev
|
|
77
|
+
ergonomics when iterating on mixing.py / devices.py / etc.
|
|
78
|
+
"""
|
|
79
|
+
global _FIRST_CREATE_INSTANCE
|
|
80
|
+
if not _FIRST_CREATE_INSTANCE:
|
|
81
|
+
_force_reload_handlers()
|
|
82
|
+
_FIRST_CREATE_INSTANCE = False
|
|
28
83
|
return LivePilot(c_instance)
|
|
29
84
|
|
|
30
85
|
|
|
@@ -36,6 +36,16 @@ def get_clip_info(song, params):
|
|
|
36
36
|
if clip.is_audio_clip:
|
|
37
37
|
result["warping"] = clip.warping
|
|
38
38
|
result["warp_mode"] = clip.warp_mode
|
|
39
|
+
# BUG-A4: expose pitch/gain so callers can reason about sample
|
|
40
|
+
# transposition vs session key. Pitch is in semitones (int, -48..+48),
|
|
41
|
+
# pitch_fine in cents (-50..+50), gain linear (normalized 0..1 in Live).
|
|
42
|
+
# Some Live builds expose these as None on freshly-recorded clips
|
|
43
|
+
# before a warp pass; wrap in try/except for safety.
|
|
44
|
+
for attr in ("pitch_coarse", "pitch_fine", "gain"):
|
|
45
|
+
try:
|
|
46
|
+
result[attr] = getattr(clip, attr)
|
|
47
|
+
except AttributeError:
|
|
48
|
+
pass
|
|
39
49
|
|
|
40
50
|
return result
|
|
41
51
|
|
|
@@ -202,6 +212,65 @@ def set_clip_launch(song, params):
|
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
|
|
215
|
+
@register("set_clip_pitch")
|
|
216
|
+
def set_clip_pitch(song, params):
|
|
217
|
+
"""Set pitch/gain on an audio clip (BUG-A5).
|
|
218
|
+
|
|
219
|
+
Audio clips only — MIDI clips raise ValueError.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
track_index, clip_index : int
|
|
224
|
+
coarse : int, optional -- semitones, -48..+48
|
|
225
|
+
fine : float, optional -- cents, -50.0..+50.0
|
|
226
|
+
gain : float, optional -- linear normalized (0..1 in Live)
|
|
227
|
+
|
|
228
|
+
At least one of (coarse, fine, gain) must be provided.
|
|
229
|
+
"""
|
|
230
|
+
track_index = int(params["track_index"])
|
|
231
|
+
clip_index = int(params["clip_index"])
|
|
232
|
+
clip = get_clip(song, track_index, clip_index)
|
|
233
|
+
|
|
234
|
+
if clip.is_midi_clip:
|
|
235
|
+
raise ValueError("set_clip_pitch only works on audio clips")
|
|
236
|
+
|
|
237
|
+
coarse = params.get("coarse")
|
|
238
|
+
fine = params.get("fine")
|
|
239
|
+
gain = params.get("gain")
|
|
240
|
+
|
|
241
|
+
if coarse is None and fine is None and gain is None:
|
|
242
|
+
raise ValueError(
|
|
243
|
+
"Provide at least one of: coarse (semitones), fine (cents), gain (0-1)"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if coarse is not None:
|
|
247
|
+
c = int(coarse)
|
|
248
|
+
if c < -48 or c > 48:
|
|
249
|
+
raise ValueError("coarse must be in -48..+48 semitones")
|
|
250
|
+
clip.pitch_coarse = c
|
|
251
|
+
if fine is not None:
|
|
252
|
+
f = float(fine)
|
|
253
|
+
if f < -50.0 or f > 50.0:
|
|
254
|
+
raise ValueError("fine must be in -50..+50 cents")
|
|
255
|
+
clip.pitch_fine = f
|
|
256
|
+
if gain is not None:
|
|
257
|
+
g = float(gain)
|
|
258
|
+
if g < 0.0 or g > 1.0:
|
|
259
|
+
raise ValueError("gain must be in 0..1")
|
|
260
|
+
clip.gain = g
|
|
261
|
+
|
|
262
|
+
result = {
|
|
263
|
+
"track_index": track_index,
|
|
264
|
+
"clip_index": clip_index,
|
|
265
|
+
}
|
|
266
|
+
for attr in ("pitch_coarse", "pitch_fine", "gain"):
|
|
267
|
+
try:
|
|
268
|
+
result[attr] = getattr(clip, attr)
|
|
269
|
+
except AttributeError:
|
|
270
|
+
pass
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
|
|
205
274
|
@register("set_clip_warp_mode")
|
|
206
275
|
def set_clip_warp_mode(song, params):
|
|
207
276
|
"""Set warp mode for an audio clip."""
|
|
@@ -293,3 +293,120 @@ def set_track_routing(song, params):
|
|
|
293
293
|
result["output_routing_channel"] = track.output_routing_channel.display_name
|
|
294
294
|
|
|
295
295
|
return result
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@register("set_compressor_sidechain")
|
|
299
|
+
def set_compressor_sidechain(song, params):
|
|
300
|
+
"""Configure a Compressor's sidechain input routing (BUG-A3).
|
|
301
|
+
|
|
302
|
+
Same LOM pattern as set_track_routing — the sidechain properties
|
|
303
|
+
(available_sidechain_input_routing_types / _channels) are on the
|
|
304
|
+
device object and Python's Remote Script accesses them cleanly.
|
|
305
|
+
This used to be routed through the M4L bridge (v1.10.6) but Max JS
|
|
306
|
+
LiveAPI couldn't read the available_* lists in Live 12.3.6, so it's
|
|
307
|
+
back on the Python side where it belongs.
|
|
308
|
+
|
|
309
|
+
Params:
|
|
310
|
+
track_index: 0+ regular, -1/-2 returns, -1000 master
|
|
311
|
+
device_index: Compressor's position in the chain
|
|
312
|
+
source_type (optional): RoutingType display name
|
|
313
|
+
(e.g. "1-DRUMS", "Ext. In", "No Input")
|
|
314
|
+
source_channel (optional): RoutingChannel display name
|
|
315
|
+
(e.g. "Post FX", "Pre FX", "Post Mixer")
|
|
316
|
+
|
|
317
|
+
Omit a param to leave that property unchanged.
|
|
318
|
+
"""
|
|
319
|
+
track_index = int(params["track_index"])
|
|
320
|
+
device_index = int(params["device_index"])
|
|
321
|
+
source_type = params.get("source_type")
|
|
322
|
+
source_channel = params.get("source_channel")
|
|
323
|
+
|
|
324
|
+
track = get_track(song, track_index)
|
|
325
|
+
devices = list(track.devices)
|
|
326
|
+
if device_index < 0 or device_index >= len(devices):
|
|
327
|
+
raise ValueError(
|
|
328
|
+
"Device index %d out of range (track has %d devices)"
|
|
329
|
+
% (device_index, len(devices))
|
|
330
|
+
)
|
|
331
|
+
device = devices[device_index]
|
|
332
|
+
|
|
333
|
+
class_name = device.class_name
|
|
334
|
+
if class_name not in ("Compressor2", "Compressor"):
|
|
335
|
+
raise ValueError(
|
|
336
|
+
"Not a Compressor device (class is %s)" % class_name
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Older Compressor builds may not expose `sidechain_enabled` as a
|
|
340
|
+
# property; the automatable "S/C On" parameter is the fallback.
|
|
341
|
+
# Try both paths so the sidechain gets enabled whichever surface is
|
|
342
|
+
# available on this Live version.
|
|
343
|
+
try:
|
|
344
|
+
device.sidechain_enabled = True
|
|
345
|
+
except AttributeError:
|
|
346
|
+
# Fallback: find the "S/C On" parameter and toggle it
|
|
347
|
+
for param in device.parameters:
|
|
348
|
+
if param.name == "S/C On":
|
|
349
|
+
param.value = 1
|
|
350
|
+
break
|
|
351
|
+
|
|
352
|
+
result = {
|
|
353
|
+
"ok": True,
|
|
354
|
+
"track_index": track_index,
|
|
355
|
+
"device_index": device_index,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if source_type is not None and source_type != "":
|
|
359
|
+
if not hasattr(device, "available_sidechain_input_routing_types"):
|
|
360
|
+
raise ValueError(
|
|
361
|
+
"This Live build doesn't expose "
|
|
362
|
+
"device.available_sidechain_input_routing_types"
|
|
363
|
+
)
|
|
364
|
+
available = list(device.available_sidechain_input_routing_types)
|
|
365
|
+
matched = None
|
|
366
|
+
for rt in available:
|
|
367
|
+
if rt.display_name == source_type:
|
|
368
|
+
matched = rt
|
|
369
|
+
break
|
|
370
|
+
if matched is None:
|
|
371
|
+
options = [rt.display_name for rt in available]
|
|
372
|
+
raise ValueError(
|
|
373
|
+
"Sidechain input type '%s' not found. Available: %s"
|
|
374
|
+
% (source_type, ", ".join(options))
|
|
375
|
+
)
|
|
376
|
+
device.sidechain_input_routing_type = matched
|
|
377
|
+
|
|
378
|
+
if source_channel is not None and source_channel != "":
|
|
379
|
+
if not hasattr(device, "available_sidechain_input_routing_channels"):
|
|
380
|
+
raise ValueError(
|
|
381
|
+
"This Live build doesn't expose "
|
|
382
|
+
"device.available_sidechain_input_routing_channels"
|
|
383
|
+
)
|
|
384
|
+
available = list(device.available_sidechain_input_routing_channels)
|
|
385
|
+
matched = None
|
|
386
|
+
for ch in available:
|
|
387
|
+
if ch.display_name == source_channel:
|
|
388
|
+
matched = ch
|
|
389
|
+
break
|
|
390
|
+
if matched is None:
|
|
391
|
+
options = [ch.display_name for ch in available]
|
|
392
|
+
raise ValueError(
|
|
393
|
+
"Sidechain input channel '%s' not found. Available: %s"
|
|
394
|
+
% (source_channel, ", ".join(options))
|
|
395
|
+
)
|
|
396
|
+
device.sidechain_input_routing_channel = matched
|
|
397
|
+
|
|
398
|
+
# Read back canonical display names
|
|
399
|
+
try:
|
|
400
|
+
result["sidechain"] = {
|
|
401
|
+
"type": device.sidechain_input_routing_type.display_name,
|
|
402
|
+
"channel": device.sidechain_input_routing_channel.display_name,
|
|
403
|
+
"enabled": bool(getattr(device, "sidechain_enabled", True)),
|
|
404
|
+
}
|
|
405
|
+
except AttributeError:
|
|
406
|
+
# Very old Compressor — fall back to whatever we set above
|
|
407
|
+
result["sidechain"] = {
|
|
408
|
+
"type": source_type or "",
|
|
409
|
+
"channel": source_channel or "",
|
|
410
|
+
"enabled": True,
|
|
411
|
+
}
|
|
412
|
+
return result
|
|
@@ -62,8 +62,20 @@ def dispatch(song, command):
|
|
|
62
62
|
)
|
|
63
63
|
|
|
64
64
|
# Built-in ping — no handler registration needed.
|
|
65
|
+
# BUG-A1: embed remote_script version + supported command set so the
|
|
66
|
+
# MCP server can detect stale installs. An old Remote Script (pre-12.3
|
|
67
|
+
# handler adds) would return an older version here; the MCP server
|
|
68
|
+
# compares and warns the user to reinstall.
|
|
65
69
|
if cmd_type == "ping":
|
|
66
|
-
|
|
70
|
+
try:
|
|
71
|
+
from . import __version__ as rs_version
|
|
72
|
+
except ImportError:
|
|
73
|
+
rs_version = "unknown"
|
|
74
|
+
return success_response(request_id, {
|
|
75
|
+
"pong": True,
|
|
76
|
+
"remote_script_version": rs_version,
|
|
77
|
+
"commands": sorted(_handlers.keys()),
|
|
78
|
+
})
|
|
67
79
|
|
|
68
80
|
handler = _handlers.get(cmd_type)
|
|
69
81
|
if handler is None:
|
|
@@ -51,45 +51,20 @@ def get_tools() -> list[dict]:
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def infer_domain(module: str) -> str:
|
|
54
|
-
"""Infer domain from module path
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return "Musical Intelligence"
|
|
61
|
-
if "memory.tools" in module:
|
|
62
|
-
return "Memory Fabric"
|
|
63
|
-
if "mix_engine" in module:
|
|
64
|
-
return "Mix Engine"
|
|
65
|
-
if "sound_design" in module:
|
|
66
|
-
return "Sound Design"
|
|
67
|
-
if "transition_engine" in module:
|
|
68
|
-
return "Transition Engine"
|
|
69
|
-
if "reference_engine" in module:
|
|
70
|
-
return "Reference Engine"
|
|
71
|
-
if "translation_engine" in module:
|
|
72
|
-
return "Translation Engine"
|
|
73
|
-
if "performance_engine" in module:
|
|
74
|
-
return "Performance Engine"
|
|
75
|
-
if "project_brain" in module:
|
|
76
|
-
return "Project Brain"
|
|
77
|
-
if "evaluation" in module:
|
|
78
|
-
return "Evaluation"
|
|
79
|
-
if "runtime" in module:
|
|
80
|
-
return "Runtime"
|
|
81
|
-
|
|
82
|
-
# Core tools — extract from module name
|
|
54
|
+
"""Infer domain from module path using the same module-layout rule as
|
|
55
|
+
``scripts/sync_metadata.py``:
|
|
56
|
+
- ``mcp_server.<X>.<...>`` → ``<X>``
|
|
57
|
+
- ``mcp_server.tools.<Y>`` → ``<Y>``
|
|
58
|
+
The display name is Title-Cased; underscores become spaces.
|
|
59
|
+
"""
|
|
83
60
|
parts = module.split(".")
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return "Other"
|
|
61
|
+
if len(parts) < 2 or parts[0] != "mcp_server":
|
|
62
|
+
return "Other"
|
|
63
|
+
if parts[1] == "tools":
|
|
64
|
+
domain_key = parts[2] if len(parts) > 2 else "other"
|
|
65
|
+
else:
|
|
66
|
+
domain_key = parts[1]
|
|
67
|
+
return domain_key.replace("_", " ").title()
|
|
93
68
|
|
|
94
69
|
|
|
95
70
|
def main():
|