livepilot 1.9.14 → 1.9.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +82 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +8 -8
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/agents/livepilot-producer/AGENT.md +243 -49
- package/livepilot/skills/livepilot-core/SKILL.md +81 -6
- package/livepilot/skills/livepilot-core/references/m4l-devices.md +2 -2
- package/livepilot/skills/livepilot-core/references/overview.md +3 -3
- package/livepilot/skills/livepilot-core/references/sound-design.md +3 -2
- package/livepilot/skills/livepilot-release/SKILL.md +13 -13
- package/m4l_device/livepilot_bridge.js +32 -15
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/connection.py +24 -2
- package/mcp_server/curves.py +14 -6
- package/mcp_server/evaluation/__init__.py +1 -0
- package/mcp_server/evaluation/fabric.py +575 -0
- package/mcp_server/evaluation/feature_extractors.py +84 -0
- package/mcp_server/evaluation/policy.py +67 -0
- package/mcp_server/evaluation/tools.py +53 -0
- package/mcp_server/m4l_bridge.py +9 -1
- package/mcp_server/memory/__init__.py +11 -2
- package/mcp_server/memory/anti_memory.py +78 -0
- package/mcp_server/memory/promotion.py +94 -0
- package/mcp_server/memory/session_memory.py +108 -0
- package/mcp_server/memory/taste_memory.py +158 -0
- package/mcp_server/memory/technique_store.py +27 -18
- package/mcp_server/memory/tools.py +112 -0
- package/mcp_server/mix_engine/__init__.py +1 -0
- package/mcp_server/mix_engine/critics.py +299 -0
- package/mcp_server/mix_engine/models.py +152 -0
- package/mcp_server/mix_engine/planner.py +103 -0
- package/mcp_server/mix_engine/state_builder.py +316 -0
- package/mcp_server/mix_engine/tools.py +220 -0
- package/mcp_server/performance_engine/__init__.py +1 -0
- package/mcp_server/performance_engine/models.py +148 -0
- package/mcp_server/performance_engine/planner.py +267 -0
- package/mcp_server/performance_engine/safety.py +165 -0
- package/mcp_server/performance_engine/tools.py +183 -0
- package/mcp_server/project_brain/__init__.py +6 -0
- package/mcp_server/project_brain/arrangement_graph.py +64 -0
- package/mcp_server/project_brain/automation_graph.py +72 -0
- package/mcp_server/project_brain/builder.py +123 -0
- package/mcp_server/project_brain/capability_graph.py +64 -0
- package/mcp_server/project_brain/models.py +282 -0
- package/mcp_server/project_brain/refresh.py +86 -0
- package/mcp_server/project_brain/role_graph.py +103 -0
- package/mcp_server/project_brain/session_graph.py +51 -0
- package/mcp_server/project_brain/tools.py +144 -0
- package/mcp_server/reference_engine/__init__.py +1 -0
- package/mcp_server/reference_engine/gap_analyzer.py +239 -0
- package/mcp_server/reference_engine/models.py +105 -0
- package/mcp_server/reference_engine/profile_builder.py +149 -0
- package/mcp_server/reference_engine/tactic_router.py +117 -0
- package/mcp_server/reference_engine/tools.py +236 -0
- package/mcp_server/runtime/__init__.py +1 -0
- package/mcp_server/runtime/action_ledger.py +117 -0
- package/mcp_server/runtime/action_ledger_models.py +91 -0
- package/mcp_server/runtime/action_tools.py +57 -0
- package/mcp_server/runtime/capability_state.py +219 -0
- package/mcp_server/runtime/safety_kernel.py +339 -0
- package/mcp_server/runtime/safety_tools.py +42 -0
- package/mcp_server/runtime/tools.py +67 -0
- package/mcp_server/server.py +17 -0
- package/mcp_server/sound_design/__init__.py +1 -0
- package/mcp_server/sound_design/critics.py +297 -0
- package/mcp_server/sound_design/models.py +147 -0
- package/mcp_server/sound_design/planner.py +104 -0
- package/mcp_server/sound_design/tools.py +297 -0
- package/mcp_server/tools/_agent_os_engine.py +947 -0
- package/mcp_server/tools/_composition_engine.py +1530 -0
- package/mcp_server/tools/_conductor.py +199 -0
- package/mcp_server/tools/_conductor_budgets.py +222 -0
- package/mcp_server/tools/_evaluation_contracts.py +91 -0
- package/mcp_server/tools/_form_engine.py +416 -0
- package/mcp_server/tools/_motif_engine.py +351 -0
- package/mcp_server/tools/_planner_engine.py +516 -0
- package/mcp_server/tools/_research_engine.py +542 -0
- package/mcp_server/tools/_research_provider.py +185 -0
- package/mcp_server/tools/_snapshot_normalizer.py +49 -0
- package/mcp_server/tools/agent_os.py +448 -0
- package/mcp_server/tools/analyzer.py +18 -0
- package/mcp_server/tools/automation.py +25 -10
- package/mcp_server/tools/composition.py +645 -0
- package/mcp_server/tools/devices.py +15 -1
- package/mcp_server/tools/midi_io.py +3 -1
- package/mcp_server/tools/motif.py +104 -0
- package/mcp_server/tools/planner.py +144 -0
- package/mcp_server/tools/research.py +223 -0
- package/mcp_server/tools/tracks.py +21 -6
- package/mcp_server/tools/transport.py +10 -2
- package/mcp_server/transition_engine/__init__.py +6 -0
- package/mcp_server/transition_engine/archetypes.py +167 -0
- package/mcp_server/transition_engine/critics.py +340 -0
- package/mcp_server/transition_engine/models.py +90 -0
- package/mcp_server/transition_engine/tools.py +291 -0
- package/mcp_server/translation_engine/__init__.py +5 -0
- package/mcp_server/translation_engine/critics.py +297 -0
- package/mcp_server/translation_engine/models.py +27 -0
- package/mcp_server/translation_engine/tools.py +108 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/arrangement.py +21 -3
- package/remote_script/LivePilot/clips.py +22 -6
- package/remote_script/LivePilot/notes.py +9 -1
- package/remote_script/LivePilot/server.py +6 -6
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Translation Engine MCP tools — 2 tools for playback robustness.
|
|
2
|
+
|
|
3
|
+
Each tool fetches data from Ableton via the shared connection,
|
|
4
|
+
then delegates to pure-computation critics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from mcp.server.fastmcp import Context
|
|
10
|
+
|
|
11
|
+
from ..server import mcp
|
|
12
|
+
from .critics import build_translation_report, run_all_translation_critics
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ── Helpers ─────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _fetch_translation_data(ctx: Context) -> dict:
|
|
19
|
+
"""Fetch mix snapshot data needed for translation analysis.
|
|
20
|
+
|
|
21
|
+
Builds snapshot from real available data:
|
|
22
|
+
- Spectrum from SpectralCache (direct access, not TCP)
|
|
23
|
+
- Stereo width estimated from track pan values
|
|
24
|
+
- Foreground detection from role inference
|
|
25
|
+
"""
|
|
26
|
+
ableton = ctx.lifespan_context["ableton"]
|
|
27
|
+
|
|
28
|
+
# Get spectral data directly from SpectralCache
|
|
29
|
+
spectrum_bands = {}
|
|
30
|
+
try:
|
|
31
|
+
spectral = ctx.lifespan_context.get("spectral")
|
|
32
|
+
if spectral and spectral.is_connected:
|
|
33
|
+
spec_data = spectral.get("spectrum")
|
|
34
|
+
if spec_data and isinstance(spec_data["value"], dict):
|
|
35
|
+
spectrum_bands = spec_data["value"]
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
# Estimate stereo width from track pans via session info
|
|
40
|
+
stereo_width = 0.0
|
|
41
|
+
center_strength = 0.5
|
|
42
|
+
has_foreground = True
|
|
43
|
+
foreground_masked = False
|
|
44
|
+
try:
|
|
45
|
+
session_info = ableton.send_command("get_session_info", {})
|
|
46
|
+
tracks = session_info.get("tracks", [])
|
|
47
|
+
if tracks:
|
|
48
|
+
# Pan may be at top level or nested under mixer.panning
|
|
49
|
+
def _get_pan(t: dict) -> float:
|
|
50
|
+
mixer = t.get("mixer")
|
|
51
|
+
if isinstance(mixer, dict):
|
|
52
|
+
return abs(mixer.get("panning", 0.0))
|
|
53
|
+
return abs(t.get("pan", 0.0))
|
|
54
|
+
pan_values = [_get_pan(t) for t in tracks if not t.get("muted", False)]
|
|
55
|
+
if pan_values:
|
|
56
|
+
# Wider pans = more stereo width
|
|
57
|
+
stereo_width = min(1.0, sum(pan_values) / max(len(pan_values), 1))
|
|
58
|
+
# Center strength = proportion of tracks near center
|
|
59
|
+
center_count = sum(1 for p in pan_values if p < 0.15)
|
|
60
|
+
center_strength = center_count / max(len(pan_values), 1)
|
|
61
|
+
|
|
62
|
+
# Simple foreground detection: at least one unmuted, non-quiet track
|
|
63
|
+
has_foreground = any(not t.get("muted", False) for t in tracks)
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
"stereo_width": stereo_width,
|
|
69
|
+
"center_strength": center_strength,
|
|
70
|
+
"sub_energy": spectrum_bands.get("sub", 0.0),
|
|
71
|
+
"low_energy": spectrum_bands.get("low", 0.0),
|
|
72
|
+
"low_mid_energy": spectrum_bands.get("low_mid", 0.0),
|
|
73
|
+
"high_energy": spectrum_bands.get("high", 0.0),
|
|
74
|
+
"presence_energy": spectrum_bands.get("presence", 0.0),
|
|
75
|
+
"has_foreground": has_foreground,
|
|
76
|
+
"foreground_masked": foreground_masked,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ── MCP Tools ───────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@mcp.tool()
|
|
84
|
+
def check_translation(ctx: Context) -> dict:
|
|
85
|
+
"""Check playback robustness — mono safety, small speakers, harshness.
|
|
86
|
+
|
|
87
|
+
Returns a full translation report with robustness classification
|
|
88
|
+
(robust/fragile/critical), boolean safety flags, and suggested
|
|
89
|
+
corrective moves.
|
|
90
|
+
"""
|
|
91
|
+
mix_snapshot = _fetch_translation_data(ctx)
|
|
92
|
+
report = build_translation_report(mix_snapshot)
|
|
93
|
+
return report.to_dict()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@mcp.tool()
|
|
97
|
+
def get_translation_issues(ctx: Context) -> dict:
|
|
98
|
+
"""Get just the translation issues without the full report.
|
|
99
|
+
|
|
100
|
+
Lighter than check_translation — returns only detected issues
|
|
101
|
+
from the 5 playback robustness critics.
|
|
102
|
+
"""
|
|
103
|
+
mix_snapshot = _fetch_translation_data(ctx)
|
|
104
|
+
issues = run_all_translation_critics(mix_snapshot)
|
|
105
|
+
return {
|
|
106
|
+
"issues": [i.to_dict() for i in issues],
|
|
107
|
+
"issue_count": len(issues),
|
|
108
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.16",
|
|
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 — 236 tools, 32 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
7
|
"license": "MIT",
|
|
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.9.
|
|
8
|
+
__version__ = "1.9.16"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from .server import LivePilotServer
|
|
@@ -352,6 +352,12 @@ def modify_arrangement_notes(song, params):
|
|
|
352
352
|
note.velocity = float(mod["velocity"])
|
|
353
353
|
if "probability" in mod:
|
|
354
354
|
note.probability = float(mod["probability"])
|
|
355
|
+
if "mute" in mod:
|
|
356
|
+
note.mute = bool(mod["mute"])
|
|
357
|
+
if "velocity_deviation" in mod:
|
|
358
|
+
note.velocity_deviation = float(mod["velocity_deviation"])
|
|
359
|
+
if "release_velocity" in mod:
|
|
360
|
+
note.release_velocity = float(mod["release_velocity"])
|
|
355
361
|
modified_count += 1
|
|
356
362
|
|
|
357
363
|
song.begin_undo_step()
|
|
@@ -523,10 +529,20 @@ def set_arrangement_automation(song, params):
|
|
|
523
529
|
|
|
524
530
|
# No fallback — direct envelope creation is the only safe approach.
|
|
525
531
|
# Session-clip duplication can silently create overlapping clips.
|
|
532
|
+
#
|
|
533
|
+
# Known limitation: clips created via create_arrangement_clip
|
|
534
|
+
# (duplicate_clip_to_arrangement) often cannot have envelopes
|
|
535
|
+
# created programmatically. Workaround: record automation by
|
|
536
|
+
# arming the track and playing back with parameter changes,
|
|
537
|
+
# or use session-clip automation (set_clip_automation) and then
|
|
538
|
+
# record the session performance to arrangement.
|
|
526
539
|
raise ValueError(
|
|
527
540
|
"Cannot create automation envelope for parameter '%s' on this "
|
|
528
|
-
"arrangement clip.
|
|
529
|
-
"
|
|
541
|
+
"arrangement clip. This is a known Live API limitation for "
|
|
542
|
+
"programmatically-created arrangement clips. Workarounds: "
|
|
543
|
+
"(1) use set_clip_automation on a session clip instead, "
|
|
544
|
+
"(2) record automation by arming the track during playback, "
|
|
545
|
+
"or (3) use the M4L bridge for deeper LOM envelope access."
|
|
530
546
|
% parameter.name
|
|
531
547
|
)
|
|
532
548
|
|
|
@@ -547,7 +563,9 @@ def transpose_arrangement_notes(song, params):
|
|
|
547
563
|
clip = arr_clips[clip_index]
|
|
548
564
|
|
|
549
565
|
from_time = float(params.get("from_time", 0.0))
|
|
550
|
-
|
|
566
|
+
# Default span covers from from_time to end of clip, not the full clip length
|
|
567
|
+
default_span = max(0.0, clip.length - from_time) if clip.length > 0 else 32768.0
|
|
568
|
+
time_span = float(params.get("time_span", default_span))
|
|
551
569
|
|
|
552
570
|
all_notes = clip.get_notes_extended(0, 128, from_time, time_span)
|
|
553
571
|
|
|
@@ -50,6 +50,11 @@ def create_clip(song, params):
|
|
|
50
50
|
raise ValueError("Clip length must be > 0")
|
|
51
51
|
|
|
52
52
|
clip_slot = get_clip_slot(song, track_index, clip_index)
|
|
53
|
+
if clip_slot.has_clip:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
"Clip slot %d on track %d already has a clip. "
|
|
56
|
+
"Delete it first with delete_clip." % (clip_index, track_index)
|
|
57
|
+
)
|
|
53
58
|
clip_slot.create_clip(length)
|
|
54
59
|
clip = clip_slot.clip
|
|
55
60
|
|
|
@@ -147,12 +152,23 @@ def set_clip_loop(song, params):
|
|
|
147
152
|
clip_index = int(params["clip_index"])
|
|
148
153
|
clip = get_clip(song, track_index, clip_index)
|
|
149
154
|
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if "start" in params
|
|
155
|
-
|
|
155
|
+
# Conditional ordering to avoid Live's loop_start < loop_end clamping.
|
|
156
|
+
# When expanding the window, set the expanding edge first.
|
|
157
|
+
# When shrinking, set the contracting edge first.
|
|
158
|
+
new_end = float(params["end"]) if "end" in params else None
|
|
159
|
+
new_start = float(params["start"]) if "start" in params else None
|
|
160
|
+
|
|
161
|
+
if new_end is not None and new_end > clip.loop_end:
|
|
162
|
+
# Expanding right — set end first so start can move freely
|
|
163
|
+
clip.loop_end = new_end
|
|
164
|
+
if new_start is not None:
|
|
165
|
+
clip.loop_start = new_start
|
|
166
|
+
else:
|
|
167
|
+
# Shrinking or only changing start — set start first
|
|
168
|
+
if new_start is not None:
|
|
169
|
+
clip.loop_start = new_start
|
|
170
|
+
if new_end is not None:
|
|
171
|
+
clip.loop_end = new_end
|
|
156
172
|
if "enabled" in params:
|
|
157
173
|
clip.looping = bool(params["enabled"])
|
|
158
174
|
|
|
@@ -172,6 +172,12 @@ def modify_notes(song, params):
|
|
|
172
172
|
note.velocity = float(mod["velocity"])
|
|
173
173
|
if "probability" in mod:
|
|
174
174
|
note.probability = float(mod["probability"])
|
|
175
|
+
if "mute" in mod:
|
|
176
|
+
note.mute = bool(mod["mute"])
|
|
177
|
+
if "velocity_deviation" in mod:
|
|
178
|
+
note.velocity_deviation = float(mod["velocity_deviation"])
|
|
179
|
+
if "release_velocity" in mod:
|
|
180
|
+
note.release_velocity = float(mod["release_velocity"])
|
|
175
181
|
modified_count += 1
|
|
176
182
|
|
|
177
183
|
# Pass the original NoteVector back — Boost.Python requires the C++ type
|
|
@@ -273,7 +279,9 @@ def transpose_notes(song, params):
|
|
|
273
279
|
clip = get_clip(song, track_index, clip_index)
|
|
274
280
|
|
|
275
281
|
from_time = float(params.get("from_time", 0.0))
|
|
276
|
-
|
|
282
|
+
# Default span covers from from_time to end of clip, not the full clip length
|
|
283
|
+
default_span = max(0.0, clip.length - from_time) if clip.length > 0 else 32768.0
|
|
284
|
+
time_span = float(params.get("time_span", default_span))
|
|
277
285
|
|
|
278
286
|
# Get notes — returns C++ NoteVector that must be passed back intact
|
|
279
287
|
all_notes = clip.get_notes_extended(0, 128, from_time, time_span)
|
|
@@ -162,12 +162,12 @@ class LivePilotServer(object):
|
|
|
162
162
|
pass
|
|
163
163
|
continue
|
|
164
164
|
self._client_connected = True
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
self._client_thread = threading.Thread(
|
|
166
|
+
target=self._run_client_session,
|
|
167
|
+
args=(client, addr),
|
|
168
|
+
)
|
|
169
|
+
self._client_thread.daemon = True
|
|
170
|
+
self._client_thread.start()
|
|
171
171
|
except socket.timeout:
|
|
172
172
|
continue
|
|
173
173
|
except OSError:
|