livepilot 1.9.11 → 1.9.13
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 +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +57 -0
- package/README.md +11 -10
- package/bin/livepilot.js +27 -9
- package/livepilot/.Codex-plugin/plugin.json +1 -1
- package/livepilot/.claude-plugin/plugin.json +1 -1
- package/livepilot/skills/livepilot-core/SKILL.md +6 -6
- package/livepilot/skills/livepilot-core/references/overview.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +4 -4
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +77 -3
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/connection.py +5 -1
- package/mcp_server/curves.py +5 -1
- package/mcp_server/m4l_bridge.py +37 -20
- package/mcp_server/memory/technique_store.py +30 -2
- package/mcp_server/server.py +10 -2
- package/mcp_server/tools/analyzer.py +12 -1
- package/mcp_server/tools/arrangement.py +20 -5
- package/mcp_server/tools/automation.py +52 -0
- package/mcp_server/tools/devices.py +2 -3
- package/mcp_server/tools/generative.py +4 -0
- package/mcp_server/tools/midi_io.py +22 -4
- package/mcp_server/tools/theory.py +8 -1
- package/mcp_server/tools/transport.py +16 -6
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +3 -3
- package/remote_script/LivePilot/arrangement.py +12 -18
- package/remote_script/LivePilot/browser.py +19 -8
- package/remote_script/LivePilot/clip_automation.py +5 -4
- package/remote_script/LivePilot/clips.py +14 -6
- package/remote_script/LivePilot/devices.py +6 -3
- package/remote_script/LivePilot/server.py +20 -7
- package/remote_script/LivePilot/tracks.py +7 -2
- package/remote_script/LivePilot/transport.py +3 -3
- package/requirements.txt +6 -1
|
@@ -3,6 +3,7 @@ LivePilot - Device domain handlers (11 commands).
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import Live
|
|
6
|
+
from collections import deque
|
|
6
7
|
|
|
7
8
|
from .router import register
|
|
8
9
|
from .utils import get_track, get_device
|
|
@@ -178,6 +179,8 @@ def toggle_device(song, params):
|
|
|
178
179
|
break
|
|
179
180
|
if on_param is None:
|
|
180
181
|
# Fallback to parameter 0 for devices that don't use "Device On"
|
|
182
|
+
if not list(device.parameters):
|
|
183
|
+
raise ValueError("Device '%s' has no parameters to toggle" % device.name)
|
|
181
184
|
on_param = device.parameters[0]
|
|
182
185
|
|
|
183
186
|
on_param.value = 1.0 if active else 0.0
|
|
@@ -399,10 +402,10 @@ def find_and_load_device(song, params):
|
|
|
399
402
|
This ensures raw 'Operator' is found before 'Hello Operator.adg' buried
|
|
400
403
|
in a user_library subfolder."""
|
|
401
404
|
nonlocal iterations
|
|
402
|
-
# Queue of (item, depth) tuples
|
|
403
|
-
queue = [(category, 0)]
|
|
405
|
+
# Queue of (item, depth) tuples — deque for O(1) popleft
|
|
406
|
+
queue = deque([(category, 0)])
|
|
404
407
|
while queue:
|
|
405
|
-
item, depth = queue.
|
|
408
|
+
item, depth = queue.popleft()
|
|
406
409
|
if depth > 8:
|
|
407
410
|
continue
|
|
408
411
|
try:
|
|
@@ -95,7 +95,8 @@ class LivePilotServer(object):
|
|
|
95
95
|
self._thread = threading.Thread(target=self._server_loop)
|
|
96
96
|
self._thread.daemon = True
|
|
97
97
|
self._thread.start()
|
|
98
|
-
|
|
98
|
+
# Note: "Listening on ..." is logged from _server_loop after bind
|
|
99
|
+
# succeeds. Don't log "Server started" here — bind may still fail.
|
|
99
100
|
|
|
100
101
|
def stop(self):
|
|
101
102
|
"""Shutdown the server gracefully."""
|
|
@@ -187,12 +188,12 @@ class LivePilotServer(object):
|
|
|
187
188
|
except OSError as exc:
|
|
188
189
|
self._log("Client error: %s" % exc)
|
|
189
190
|
finally:
|
|
190
|
-
with self._client_lock:
|
|
191
|
-
self._client_connected = False
|
|
192
191
|
try:
|
|
193
192
|
client.close()
|
|
194
193
|
except OSError:
|
|
195
194
|
pass
|
|
195
|
+
with self._client_lock:
|
|
196
|
+
self._client_connected = False
|
|
196
197
|
self._log("Client disconnected")
|
|
197
198
|
|
|
198
199
|
def _handle_client(self, client):
|
|
@@ -205,6 +206,9 @@ class LivePilotServer(object):
|
|
|
205
206
|
if not data:
|
|
206
207
|
break
|
|
207
208
|
buf += data.decode("utf-8", errors="replace")
|
|
209
|
+
if len(buf) > 4 * 1024 * 1024: # 4 MB
|
|
210
|
+
self._log("Client buffer overflow — disconnecting")
|
|
211
|
+
break
|
|
208
212
|
while "\n" in buf:
|
|
209
213
|
line, buf = buf.split("\n", 1)
|
|
210
214
|
line = line.strip()
|
|
@@ -249,8 +253,14 @@ class LivePilotServer(object):
|
|
|
249
253
|
try:
|
|
250
254
|
self._cs.schedule_message(0, self._process_next_command)
|
|
251
255
|
except AssertionError:
|
|
252
|
-
#
|
|
253
|
-
|
|
256
|
+
# ControlSurface is disconnecting — return error instead of
|
|
257
|
+
# running LOM calls on the TCP thread (which would be unsafe)
|
|
258
|
+
response_queue.put({
|
|
259
|
+
"id": request_id,
|
|
260
|
+
"ok": False,
|
|
261
|
+
"error": {"code": "STATE_ERROR", "message": "Script is disconnecting"},
|
|
262
|
+
})
|
|
263
|
+
return
|
|
254
264
|
|
|
255
265
|
# Wait for response from main thread
|
|
256
266
|
try:
|
|
@@ -296,7 +306,8 @@ class LivePilotServer(object):
|
|
|
296
306
|
try:
|
|
297
307
|
self._cs.schedule_message(1, send_response) # ~100ms
|
|
298
308
|
except AssertionError:
|
|
299
|
-
|
|
309
|
+
# ControlSurface disconnecting — send result immediately
|
|
310
|
+
response_queue.put(result)
|
|
300
311
|
else:
|
|
301
312
|
response_queue.put(result)
|
|
302
313
|
# Drain any remaining queued commands
|
|
@@ -308,7 +319,9 @@ class LivePilotServer(object):
|
|
|
308
319
|
try:
|
|
309
320
|
self._cs.schedule_message(0, self._process_next_command)
|
|
310
321
|
except AssertionError:
|
|
311
|
-
|
|
322
|
+
# ControlSurface disconnecting — drop remaining commands
|
|
323
|
+
# rather than running LOM calls on the wrong thread
|
|
324
|
+
pass
|
|
312
325
|
|
|
313
326
|
# ── Socket I/O ───────────────────────────────────────────────────────
|
|
314
327
|
|
|
@@ -175,9 +175,14 @@ def duplicate_track(song, params):
|
|
|
175
175
|
"Track index %d out of range (0..%d)"
|
|
176
176
|
% (track_index, len(tracks) - 1)
|
|
177
177
|
)
|
|
178
|
+
count_before = len(tracks)
|
|
178
179
|
song.duplicate_track(track_index)
|
|
179
|
-
|
|
180
|
-
|
|
180
|
+
all_tracks = list(song.tracks)
|
|
181
|
+
# For group tracks, Ableton duplicates the group + all children.
|
|
182
|
+
# The duplicate block starts right after the original group's last child.
|
|
183
|
+
added = len(all_tracks) - count_before
|
|
184
|
+
new_index = track_index + added
|
|
185
|
+
return {"index": new_index, "name": all_tracks[new_index].name}
|
|
181
186
|
|
|
182
187
|
|
|
183
188
|
@register("set_track_name")
|
|
@@ -53,9 +53,9 @@ def get_session_info(song, params):
|
|
|
53
53
|
"metronome": song.metronome,
|
|
54
54
|
"record_mode": song.record_mode,
|
|
55
55
|
"session_record": song.session_record,
|
|
56
|
-
"track_count": len(
|
|
57
|
-
"return_track_count": len(
|
|
58
|
-
"scene_count": len(
|
|
56
|
+
"track_count": len(tracks_info),
|
|
57
|
+
"return_track_count": len(return_tracks_info),
|
|
58
|
+
"scene_count": len(scenes_info),
|
|
59
59
|
"tracks": tracks_info,
|
|
60
60
|
"return_tracks": return_tracks_info,
|
|
61
61
|
"scenes": scenes_info,
|
package/requirements.txt
CHANGED
|
@@ -3,9 +3,14 @@ numpy>=1.24.0
|
|
|
3
3
|
fastmcp>=3.0.0,<4.0.0
|
|
4
4
|
midiutil>=1.2.1
|
|
5
5
|
pretty_midi>=0.2.10
|
|
6
|
-
opycleid>=0.5.1
|
|
7
6
|
# v1.8 Perception Layer (offline analysis)
|
|
8
7
|
pyloudnorm>=0.1.0
|
|
9
8
|
soundfile>=0.12.0
|
|
10
9
|
scipy>=1.11.0
|
|
11
10
|
mutagen>=1.47.0
|
|
11
|
+
|
|
12
|
+
# Development / testing (not required for runtime)
|
|
13
|
+
# pip install pytest pytest-asyncio
|
|
14
|
+
|
|
15
|
+
# Optional: neo-Riemannian group theory (not required — harmony engine is pure Python)
|
|
16
|
+
# pip install opycleid>=0.5.1
|