livepilot 1.4.0 → 1.4.1
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 +8 -0
- package/README.md +21 -26
- package/package.json +1 -1
- package/plugin/agents/livepilot-producer/AGENT.md +9 -13
- package/plugin/plugin.json +1 -1
- package/remote_script/LivePilot/browser.py +12 -10
- package/remote_script/LivePilot/devices.py +9 -10
- package/remote_script/LivePilot/mixing.py +4 -4
- package/remote_script/LivePilot/server.py +11 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.1] — 2026-03-18
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- Browser search now enforces global 10,000 iteration budget across all categories (was resetting per-category)
|
|
7
|
+
- `_search_recursive` returns early when `max_results` is reached instead of scanning remaining siblings
|
|
8
|
+
- Narrowed 29 broad `except Exception` catches to specific types (`AttributeError`, `OSError`, `ValueError`) across browser, devices, mixing, and server modules
|
|
9
|
+
- Kept `except Exception` only at the 3 outermost dispatch boundaries (router, command processor, logger)
|
|
10
|
+
|
|
3
11
|
## [1.4.0] — 2026-03-18
|
|
4
12
|
|
|
5
13
|
### Added
|
package/README.md
CHANGED
|
@@ -19,26 +19,33 @@ Every command goes through Ableton's official Live Object Model API. No hacks, n
|
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## Train Your Own AI Producer
|
|
23
23
|
|
|
24
|
-
LivePilot
|
|
24
|
+
LivePilot ships with 104 tools and a deep reference corpus — genre-specific drum patterns, chord voicings, sound design recipes, mixing templates, song structures. Day one, it already knows how music works. But the tools are just the starting point.
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
**The real product is the agent you build on top of them.**
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Every producer has a sonic identity — the swing amounts they reach for, the drum kits that feel right, the FX chains they keep coming back to, the way they EQ a vocal bus. Most AI tools ignore all of this. Every session starts blank. LivePilot is different: it has a technique memory system that turns your production decisions into a persistent, searchable stylistic palette the agent learns from over time.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Here's how you train it:
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
1. **Produce something you like** — a beat, a device chain, a mixing setup, a synth patch
|
|
33
|
+
2. **Tell the agent to save it** — "remember this groove" / "save this reverb chain"
|
|
34
|
+
3. **The agent writes a stylistic analysis** — not just raw MIDI data, but *what makes it work*: the rhythmic feel, the sonic texture, the mood, what it pairs with, what artists it evokes
|
|
35
|
+
4. **Your library grows** — rate and favorite the best techniques, tag them by genre or mood, build categories
|
|
36
|
+
5. **The agent develops taste** — next time you say "make me a beat", it checks your library, reads your tendencies, and creates something new that sounds like you
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
- **Pin your go-to sounds** — "save this reverb chain" — instant recall next time, with context about when to use it
|
|
36
|
-
- **Set preferences** — "I always want Utility on every track" — the agent adapts its workflow to match yours
|
|
37
|
-
- **Curate over time** — rate, favorite, and refine saved techniques — the best ones float to the top
|
|
38
|
+
This isn't a preset recall system. The agent doesn't copy stored patterns — it understands the *qualities* across your saved techniques (the swing you prefer, the harmonic language you gravitate toward, the density of your arrangements) and uses that understanding to inform new creative decisions.
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
**Three modes, always under your control:**
|
|
40
41
|
|
|
41
|
-
**
|
|
42
|
+
- **Informed** (default) — the agent consults your memory and lets it influence creative choices naturally
|
|
43
|
+
- **Fresh** — "ignore my history" / "something completely new" — blank slate, pure musical knowledge, zero influence from saved techniques
|
|
44
|
+
- **Explicit recall** — "use that boom bap beat I saved" — direct retrieval and replay
|
|
45
|
+
|
|
46
|
+
The memory is both a drawer and a personality. You put things in, you take things out, and the agent develops taste from what you've collected — but you can always tell it to forget everything and start fresh.
|
|
47
|
+
|
|
48
|
+
**You're not configuring software. You're building a creative partner that gets better the more you use it.**
|
|
42
49
|
|
|
43
50
|
---
|
|
44
51
|
|
|
@@ -286,21 +293,9 @@ What makes it different from a generic AI with tools is **what it knows**. The a
|
|
|
286
293
|
|
|
287
294
|
But the shipped corpus is just the floor. The real value builds over time.
|
|
288
295
|
|
|
289
|
-
### Technique Memory
|
|
290
|
-
|
|
291
|
-
Most AI music tools are stateless — every session starts blank. LivePilot remembers.
|
|
292
|
-
|
|
293
|
-
When you make something you like — a beat, a device chain, a mixing setup, a favorite preset — you tell the agent to save it. But it doesn't just store the raw MIDI data or parameter values. It writes a **stylistic analysis**: what makes this groove feel the way it does, what the sonic character is, what it pairs well with, what artists or styles it evokes. Over time, your saved techniques become a **stylistic fingerprint** — a palette the agent understands.
|
|
294
|
-
|
|
295
|
-
The next time you say "make me a beat", the agent checks your library first. It doesn't copy a stored pattern — it reads across your saved techniques, understands your tendencies (the swing amounts you like, the kits you reach for, your harmonic language), and creates something **new that sounds like you**. It's the difference between an assistant that follows instructions and one that knows your taste.
|
|
296
|
-
|
|
297
|
-
Three modes, always under your control:
|
|
298
|
-
|
|
299
|
-
- **Informed** (default) — the agent consults your memory and lets it influence creative choices
|
|
300
|
-
- **Fresh** — say "ignore my history" and the agent starts with a blank slate, pure musical knowledge
|
|
301
|
-
- **Explicit recall** — say "use that boom bap beat I saved" for direct retrieval and replay
|
|
296
|
+
### Technique Memory
|
|
302
297
|
|
|
303
|
-
|
|
298
|
+
8 tools for saving, searching, and replaying production techniques. Every saved technique includes a rich stylistic analysis written by the agent — not just raw data, but *what makes it work* and when to use it. See "Train Your Own AI Producer" above for how this shapes the agent over time.
|
|
304
299
|
|
|
305
300
|
### Core Skill
|
|
306
301
|
|
package/package.json
CHANGED
|
@@ -17,19 +17,15 @@ You are LivePilot Producer — an autonomous music production agent for Ableton
|
|
|
17
17
|
Given a high-level description, you:
|
|
18
18
|
|
|
19
19
|
1. **Plan** — decide tempo, key, track layout, instrument choices, arrangement structure
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
6. **Add effects** — load and configure effect chains for the desired sound
|
|
30
|
-
7. **HEALTH CHECK** — verify effects aren't pass-throughs (Dry/Wet > 0, Drive set, etc.)
|
|
31
|
-
8. **Mix** — balance volumes, set panning, configure sends
|
|
32
|
-
9. **Final verify** — `get_session_info`, fire scenes, confirm audio output
|
|
20
|
+
2. **Consult memory** (unless user requests fresh exploration) — call `memory_recall` with a query matching the task (limit=5). Read the returned qualities and let them shape your plan: kit choices, tempo range, rhythmic approach, sound palette. Don't copy — be influenced. If the user says "fresh" / "ignore history" / "something new", skip this step entirely.
|
|
21
|
+
3. **Build tracks** — create and name tracks with appropriate colors
|
|
22
|
+
4. **Load instruments** — find and load the right synths, drum kits, and samplers
|
|
23
|
+
5. **HEALTH CHECK** — verify every track actually produces sound (see below)
|
|
24
|
+
6. **Program patterns** — write MIDI notes that fit the genre and style
|
|
25
|
+
7. **Add effects** — load and configure effect chains for the desired sound
|
|
26
|
+
8. **HEALTH CHECK** — verify effects aren't pass-throughs (Dry/Wet > 0, Drive set, etc.)
|
|
27
|
+
9. **Mix** — balance volumes, set panning, configure sends
|
|
28
|
+
10. **Final verify** — `get_session_info`, fire scenes, confirm audio output
|
|
33
29
|
|
|
34
30
|
## Mandatory Track Health Checks
|
|
35
31
|
|
package/plugin/plugin.json
CHANGED
|
@@ -82,14 +82,17 @@ def _search_recursive(item, name_filter, loadable_only, results, depth, max_dept
|
|
|
82
82
|
}
|
|
83
83
|
try:
|
|
84
84
|
entry["uri"] = child.uri
|
|
85
|
-
except
|
|
85
|
+
except AttributeError:
|
|
86
86
|
entry["uri"] = None
|
|
87
87
|
results.append(entry)
|
|
88
88
|
if child.is_folder:
|
|
89
|
+
before = len(results)
|
|
89
90
|
_search_recursive(
|
|
90
91
|
child, name_filter, loadable_only, results, depth + 1, max_depth,
|
|
91
92
|
max_results
|
|
92
93
|
)
|
|
94
|
+
if len(results) >= max_results:
|
|
95
|
+
return
|
|
93
96
|
|
|
94
97
|
|
|
95
98
|
@register("get_browser_tree")
|
|
@@ -136,7 +139,7 @@ def get_browser_items(song, params):
|
|
|
136
139
|
if child.is_loadable:
|
|
137
140
|
try:
|
|
138
141
|
entry["uri"] = child.uri
|
|
139
|
-
except
|
|
142
|
+
except AttributeError:
|
|
140
143
|
entry["uri"] = None
|
|
141
144
|
result.append(entry)
|
|
142
145
|
return {"path": path, "items": result}
|
|
@@ -186,7 +189,7 @@ def load_browser_item(song, params):
|
|
|
186
189
|
for attr in category_attrs:
|
|
187
190
|
try:
|
|
188
191
|
categories.append(getattr(browser, attr))
|
|
189
|
-
except
|
|
192
|
+
except AttributeError:
|
|
190
193
|
pass
|
|
191
194
|
|
|
192
195
|
_iterations = [0]
|
|
@@ -198,7 +201,7 @@ def load_browser_item(song, params):
|
|
|
198
201
|
return None
|
|
199
202
|
try:
|
|
200
203
|
children = list(parent.children)
|
|
201
|
-
except
|
|
204
|
+
except AttributeError:
|
|
202
205
|
return None
|
|
203
206
|
for child in children:
|
|
204
207
|
_iterations[0] += 1
|
|
@@ -207,7 +210,7 @@ def load_browser_item(song, params):
|
|
|
207
210
|
try:
|
|
208
211
|
if child.uri == target_uri and child.is_loadable:
|
|
209
212
|
return child
|
|
210
|
-
except
|
|
213
|
+
except AttributeError:
|
|
211
214
|
pass
|
|
212
215
|
result = find_by_uri(child, target_uri, depth + 1)
|
|
213
216
|
if result is not None:
|
|
@@ -215,7 +218,6 @@ def load_browser_item(song, params):
|
|
|
215
218
|
return None
|
|
216
219
|
|
|
217
220
|
for category in categories:
|
|
218
|
-
_iterations[0] = 0
|
|
219
221
|
found = find_by_uri(category, uri)
|
|
220
222
|
if found is not None:
|
|
221
223
|
song.view.selected_track = track
|
|
@@ -248,13 +250,14 @@ def load_browser_item(song, params):
|
|
|
248
250
|
break
|
|
249
251
|
|
|
250
252
|
target = device_name.lower()
|
|
253
|
+
_iterations[0] = 0
|
|
251
254
|
|
|
252
255
|
def find_by_name(parent, depth=0):
|
|
253
256
|
if depth > 8 or _iterations[0] > MAX_ITERATIONS:
|
|
254
257
|
return None
|
|
255
258
|
try:
|
|
256
259
|
children = list(parent.children)
|
|
257
|
-
except
|
|
260
|
+
except AttributeError:
|
|
258
261
|
return None
|
|
259
262
|
for child in children:
|
|
260
263
|
_iterations[0] += 1
|
|
@@ -269,7 +272,6 @@ def load_browser_item(song, params):
|
|
|
269
272
|
return None
|
|
270
273
|
|
|
271
274
|
for category in categories:
|
|
272
|
-
_iterations[0] = 0
|
|
273
275
|
found = find_by_name(category)
|
|
274
276
|
if found is not None:
|
|
275
277
|
song.view.selected_track = track
|
|
@@ -311,14 +313,14 @@ def get_device_presets(song, params):
|
|
|
311
313
|
return
|
|
312
314
|
try:
|
|
313
315
|
children = list(item.children)
|
|
314
|
-
except
|
|
316
|
+
except AttributeError:
|
|
315
317
|
return
|
|
316
318
|
for child in children:
|
|
317
319
|
if child.is_loadable and not child.is_folder:
|
|
318
320
|
entry = {"name": child.name}
|
|
319
321
|
try:
|
|
320
322
|
entry["uri"] = child.uri
|
|
321
|
-
except
|
|
323
|
+
except AttributeError:
|
|
322
324
|
entry["uri"] = None
|
|
323
325
|
results.append(entry)
|
|
324
326
|
elif child.is_folder:
|
|
@@ -30,7 +30,7 @@ def get_device_info(song, params):
|
|
|
30
30
|
}
|
|
31
31
|
try:
|
|
32
32
|
result["type"] = device.type
|
|
33
|
-
except
|
|
33
|
+
except AttributeError:
|
|
34
34
|
result["type"] = None
|
|
35
35
|
return result
|
|
36
36
|
|
|
@@ -196,7 +196,7 @@ def load_device_by_uri(song, params):
|
|
|
196
196
|
for attr in category_attrs:
|
|
197
197
|
try:
|
|
198
198
|
categories.append(getattr(browser, attr))
|
|
199
|
-
except
|
|
199
|
+
except AttributeError:
|
|
200
200
|
pass
|
|
201
201
|
|
|
202
202
|
_iterations = [0]
|
|
@@ -208,7 +208,7 @@ def load_device_by_uri(song, params):
|
|
|
208
208
|
return None
|
|
209
209
|
try:
|
|
210
210
|
children = list(parent.children)
|
|
211
|
-
except
|
|
211
|
+
except AttributeError:
|
|
212
212
|
return None
|
|
213
213
|
for child in children:
|
|
214
214
|
_iterations[0] += 1
|
|
@@ -217,7 +217,7 @@ def load_device_by_uri(song, params):
|
|
|
217
217
|
try:
|
|
218
218
|
if child.uri == target_uri and child.is_loadable:
|
|
219
219
|
return child
|
|
220
|
-
except
|
|
220
|
+
except AttributeError:
|
|
221
221
|
pass
|
|
222
222
|
result = find_by_uri(child, target_uri, depth + 1)
|
|
223
223
|
if result is not None:
|
|
@@ -225,7 +225,6 @@ def load_device_by_uri(song, params):
|
|
|
225
225
|
return None
|
|
226
226
|
|
|
227
227
|
for category in categories:
|
|
228
|
-
_iterations[0] = 0
|
|
229
228
|
found = find_by_uri(category, uri)
|
|
230
229
|
if found is not None:
|
|
231
230
|
song.view.selected_track = track
|
|
@@ -252,13 +251,14 @@ def load_device_by_uri(song, params):
|
|
|
252
251
|
break
|
|
253
252
|
|
|
254
253
|
target = device_name.lower()
|
|
254
|
+
_iterations[0] = 0
|
|
255
255
|
|
|
256
256
|
def find_by_name(parent, depth=0):
|
|
257
257
|
if depth > 8 or _iterations[0] > MAX_ITERATIONS:
|
|
258
258
|
return None
|
|
259
259
|
try:
|
|
260
260
|
children = list(parent.children)
|
|
261
|
-
except
|
|
261
|
+
except AttributeError:
|
|
262
262
|
return None
|
|
263
263
|
for child in children:
|
|
264
264
|
_iterations[0] += 1
|
|
@@ -273,7 +273,6 @@ def load_device_by_uri(song, params):
|
|
|
273
273
|
return None
|
|
274
274
|
|
|
275
275
|
for category in categories:
|
|
276
|
-
_iterations[0] = 0
|
|
277
276
|
found = find_by_name(category)
|
|
278
277
|
if found is not None:
|
|
279
278
|
song.view.selected_track = track
|
|
@@ -307,7 +306,7 @@ def find_and_load_device(song, params):
|
|
|
307
306
|
return None
|
|
308
307
|
try:
|
|
309
308
|
children = list(item.children)
|
|
310
|
-
except
|
|
309
|
+
except AttributeError:
|
|
311
310
|
return None
|
|
312
311
|
for child in children:
|
|
313
312
|
iterations += 1
|
|
@@ -332,11 +331,11 @@ def find_and_load_device(song, params):
|
|
|
332
331
|
for attr in category_attrs:
|
|
333
332
|
try:
|
|
334
333
|
categories.append(getattr(browser, attr))
|
|
335
|
-
except
|
|
334
|
+
except AttributeError:
|
|
336
335
|
pass
|
|
337
336
|
|
|
338
337
|
for category in categories:
|
|
339
|
-
iterations = 0
|
|
338
|
+
iterations = 0
|
|
340
339
|
found = search_children(category)
|
|
341
340
|
if found is not None:
|
|
342
341
|
song.view.selected_track = track
|
|
@@ -97,19 +97,19 @@ def get_track_routing(song, params):
|
|
|
97
97
|
result = {"index": track_index}
|
|
98
98
|
try:
|
|
99
99
|
result["input_routing_type"] = track.input_routing_type.display_name
|
|
100
|
-
except
|
|
100
|
+
except AttributeError:
|
|
101
101
|
result["input_routing_type"] = None
|
|
102
102
|
try:
|
|
103
103
|
result["input_routing_channel"] = track.input_routing_channel.display_name
|
|
104
|
-
except
|
|
104
|
+
except AttributeError:
|
|
105
105
|
result["input_routing_channel"] = None
|
|
106
106
|
try:
|
|
107
107
|
result["output_routing_type"] = track.output_routing_type.display_name
|
|
108
|
-
except
|
|
108
|
+
except AttributeError:
|
|
109
109
|
result["output_routing_type"] = None
|
|
110
110
|
try:
|
|
111
111
|
result["output_routing_channel"] = track.output_routing_channel.display_name
|
|
112
|
-
except
|
|
112
|
+
except AttributeError:
|
|
113
113
|
result["output_routing_channel"] = None
|
|
114
114
|
return result
|
|
115
115
|
|
|
@@ -84,7 +84,7 @@ class LivePilotServer(object):
|
|
|
84
84
|
if self._server_socket:
|
|
85
85
|
try:
|
|
86
86
|
self._server_socket.close()
|
|
87
|
-
except
|
|
87
|
+
except OSError:
|
|
88
88
|
pass
|
|
89
89
|
if self._thread and self._thread.is_alive():
|
|
90
90
|
self._thread.join(timeout=3)
|
|
@@ -109,7 +109,7 @@ class LivePilotServer(object):
|
|
|
109
109
|
self._server_socket.listen(2)
|
|
110
110
|
self._server_socket.settimeout(1.0)
|
|
111
111
|
self._log("Listening on %s:%d" % (self._host, self._port))
|
|
112
|
-
except
|
|
112
|
+
except OSError as exc:
|
|
113
113
|
self._log("Failed to bind: %s" % exc)
|
|
114
114
|
return
|
|
115
115
|
|
|
@@ -132,37 +132,37 @@ class LivePilotServer(object):
|
|
|
132
132
|
}
|
|
133
133
|
}) + "\n"
|
|
134
134
|
client.sendall(reject.encode("utf-8"))
|
|
135
|
-
except
|
|
135
|
+
except OSError:
|
|
136
136
|
pass
|
|
137
137
|
try:
|
|
138
138
|
client.close()
|
|
139
|
-
except
|
|
139
|
+
except OSError:
|
|
140
140
|
pass
|
|
141
141
|
continue
|
|
142
142
|
self._client_connected = True
|
|
143
143
|
self._log("Client connected from %s:%d" % addr)
|
|
144
144
|
try:
|
|
145
145
|
self._handle_client(client)
|
|
146
|
-
except
|
|
146
|
+
except OSError as exc:
|
|
147
147
|
self._log("Client error: %s" % exc)
|
|
148
148
|
finally:
|
|
149
149
|
with self._client_lock:
|
|
150
150
|
self._client_connected = False
|
|
151
151
|
try:
|
|
152
152
|
client.close()
|
|
153
|
-
except
|
|
153
|
+
except OSError:
|
|
154
154
|
pass
|
|
155
155
|
self._log("Client disconnected")
|
|
156
156
|
except socket.timeout:
|
|
157
157
|
continue
|
|
158
|
-
except
|
|
158
|
+
except OSError:
|
|
159
159
|
if self._running:
|
|
160
160
|
self._log("Accept error")
|
|
161
161
|
break
|
|
162
162
|
|
|
163
163
|
try:
|
|
164
164
|
self._server_socket.close()
|
|
165
|
-
except
|
|
165
|
+
except OSError:
|
|
166
166
|
pass
|
|
167
167
|
|
|
168
168
|
def _handle_client(self, client):
|
|
@@ -182,7 +182,7 @@ class LivePilotServer(object):
|
|
|
182
182
|
self._process_line(client, line)
|
|
183
183
|
except socket.timeout:
|
|
184
184
|
continue
|
|
185
|
-
except
|
|
185
|
+
except OSError as exc:
|
|
186
186
|
self._log("Recv error: %s" % exc)
|
|
187
187
|
break
|
|
188
188
|
|
|
@@ -190,7 +190,7 @@ class LivePilotServer(object):
|
|
|
190
190
|
"""Parse one JSON command, queue it for main thread, wait for result."""
|
|
191
191
|
try:
|
|
192
192
|
command = json.loads(line)
|
|
193
|
-
except
|
|
193
|
+
except (ValueError, TypeError) as exc:
|
|
194
194
|
resp = {
|
|
195
195
|
"id": "unknown",
|
|
196
196
|
"ok": False,
|
|
@@ -282,5 +282,5 @@ class LivePilotServer(object):
|
|
|
282
282
|
from .utils import serialize_json
|
|
283
283
|
try:
|
|
284
284
|
client.sendall(serialize_json(response).encode("utf-8"))
|
|
285
|
-
except
|
|
285
|
+
except OSError as exc:
|
|
286
286
|
self._log("Send error: %s" % exc)
|