livepilot 1.0.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 +33 -0
- package/LICENSE +21 -0
- package/README.md +409 -0
- package/bin/livepilot.js +390 -0
- package/installer/install.js +95 -0
- package/installer/paths.js +79 -0
- package/mcp_server/__init__.py +2 -0
- package/mcp_server/__main__.py +5 -0
- package/mcp_server/connection.py +210 -0
- package/mcp_server/memory/__init__.py +5 -0
- package/mcp_server/memory/technique_store.py +296 -0
- package/mcp_server/server.py +87 -0
- package/mcp_server/tools/__init__.py +1 -0
- package/mcp_server/tools/arrangement.py +407 -0
- package/mcp_server/tools/browser.py +86 -0
- package/mcp_server/tools/clips.py +218 -0
- package/mcp_server/tools/devices.py +256 -0
- package/mcp_server/tools/memory.py +198 -0
- package/mcp_server/tools/mixing.py +121 -0
- package/mcp_server/tools/notes.py +269 -0
- package/mcp_server/tools/scenes.py +89 -0
- package/mcp_server/tools/tracks.py +175 -0
- package/mcp_server/tools/transport.py +117 -0
- package/package.json +37 -0
- package/plugin/agents/livepilot-producer/AGENT.md +62 -0
- package/plugin/commands/beat.md +18 -0
- package/plugin/commands/memory.md +22 -0
- package/plugin/commands/mix.md +15 -0
- package/plugin/commands/session.md +13 -0
- package/plugin/commands/sounddesign.md +16 -0
- package/plugin/plugin.json +19 -0
- package/plugin/skills/livepilot-core/SKILL.md +208 -0
- package/plugin/skills/livepilot-core/references/ableton-workflow-patterns.md +831 -0
- package/plugin/skills/livepilot-core/references/device-atlas/00-index.md +110 -0
- package/plugin/skills/livepilot-core/references/device-atlas/distortion-and-character.md +687 -0
- package/plugin/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +753 -0
- package/plugin/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +525 -0
- package/plugin/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +402 -0
- package/plugin/skills/livepilot-core/references/device-atlas/midi-tools.md +963 -0
- package/plugin/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +874 -0
- package/plugin/skills/livepilot-core/references/device-atlas/space-and-depth.md +571 -0
- package/plugin/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +714 -0
- package/plugin/skills/livepilot-core/references/device-atlas/synths-native.md +953 -0
- package/plugin/skills/livepilot-core/references/m4l-devices.md +352 -0
- package/plugin/skills/livepilot-core/references/memory-guide.md +107 -0
- package/plugin/skills/livepilot-core/references/midi-recipes.md +402 -0
- package/plugin/skills/livepilot-core/references/mixing-patterns.md +578 -0
- package/plugin/skills/livepilot-core/references/overview.md +209 -0
- package/plugin/skills/livepilot-core/references/sound-design.md +392 -0
- package/remote_script/LivePilot/__init__.py +42 -0
- package/remote_script/LivePilot/arrangement.py +693 -0
- package/remote_script/LivePilot/browser.py +424 -0
- package/remote_script/LivePilot/clips.py +211 -0
- package/remote_script/LivePilot/devices.py +596 -0
- package/remote_script/LivePilot/diagnostics.py +198 -0
- package/remote_script/LivePilot/mixing.py +194 -0
- package/remote_script/LivePilot/notes.py +339 -0
- package/remote_script/LivePilot/router.py +74 -0
- package/remote_script/LivePilot/scenes.py +99 -0
- package/remote_script/LivePilot/server.py +293 -0
- package/remote_script/LivePilot/tracks.py +268 -0
- package/remote_script/LivePilot/transport.py +151 -0
- package/remote_script/LivePilot/utils.py +123 -0
- package/requirements.txt +2 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LivePilot - Session diagnostics handler (1 command).
|
|
3
|
+
|
|
4
|
+
Analyzes the current session and flags potential issues:
|
|
5
|
+
armed tracks, mute/solo leftovers, empty clips, unnamed tracks,
|
|
6
|
+
empty scenes, and device-less instrument tracks.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .router import register
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Default track names that indicate the user hasn't renamed them.
|
|
13
|
+
# Ableton auto-names tracks like "1-MIDI", "2-Audio", "3-MIDI", etc.
|
|
14
|
+
_DEFAULT_NAME_PATTERNS = frozenset([
|
|
15
|
+
"MIDI", "Audio", "Inst", "Return",
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _looks_default_name(name):
|
|
20
|
+
"""Check if a track name looks like an Ableton default."""
|
|
21
|
+
stripped = name.strip()
|
|
22
|
+
# Pattern: "N-Type" or just "Type" (e.g., "1-MIDI", "MIDI", "2-Audio")
|
|
23
|
+
for part in stripped.split("-"):
|
|
24
|
+
part = part.strip()
|
|
25
|
+
if part.isdigit():
|
|
26
|
+
continue
|
|
27
|
+
if part in _DEFAULT_NAME_PATTERNS:
|
|
28
|
+
return True
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@register("get_session_diagnostics")
|
|
33
|
+
def get_session_diagnostics(song, params):
|
|
34
|
+
"""Analyze the session and return a diagnostic report."""
|
|
35
|
+
issues = []
|
|
36
|
+
stats = {
|
|
37
|
+
"track_count": 0,
|
|
38
|
+
"return_track_count": 0,
|
|
39
|
+
"scene_count": 0,
|
|
40
|
+
"total_clips": 0,
|
|
41
|
+
"empty_scenes": 0,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
tracks = list(song.tracks)
|
|
45
|
+
scenes = list(song.scenes)
|
|
46
|
+
return_tracks = list(song.return_tracks)
|
|
47
|
+
|
|
48
|
+
stats["track_count"] = len(tracks)
|
|
49
|
+
stats["return_track_count"] = len(return_tracks)
|
|
50
|
+
stats["scene_count"] = len(scenes)
|
|
51
|
+
|
|
52
|
+
# ── Track-level checks ─────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
armed_tracks = []
|
|
55
|
+
soloed_tracks = []
|
|
56
|
+
muted_tracks = []
|
|
57
|
+
unnamed_tracks = []
|
|
58
|
+
empty_tracks = [] # no clips at all
|
|
59
|
+
no_device_midi_tracks = [] # MIDI tracks with no instruments
|
|
60
|
+
track_slots = [] # cached clip_slots per track (avoid re-evaluating LOM tuple)
|
|
61
|
+
|
|
62
|
+
for i, track in enumerate(tracks):
|
|
63
|
+
# Armed check
|
|
64
|
+
if track.arm:
|
|
65
|
+
armed_tracks.append({"index": i, "name": track.name})
|
|
66
|
+
|
|
67
|
+
# Solo check
|
|
68
|
+
if track.solo:
|
|
69
|
+
soloed_tracks.append({"index": i, "name": track.name})
|
|
70
|
+
|
|
71
|
+
# Muted tracks (informational, only flag if many)
|
|
72
|
+
if track.mute:
|
|
73
|
+
muted_tracks.append({"index": i, "name": track.name})
|
|
74
|
+
|
|
75
|
+
# Unnamed / default name check
|
|
76
|
+
if _looks_default_name(track.name):
|
|
77
|
+
unnamed_tracks.append({"index": i, "name": track.name})
|
|
78
|
+
|
|
79
|
+
# Cache clip_slots once per track
|
|
80
|
+
slots = list(track.clip_slots)
|
|
81
|
+
track_slots.append(slots)
|
|
82
|
+
|
|
83
|
+
# Clip count
|
|
84
|
+
clip_count = 0
|
|
85
|
+
for slot in slots:
|
|
86
|
+
if slot.has_clip:
|
|
87
|
+
clip_count += 1
|
|
88
|
+
stats["total_clips"] += clip_count
|
|
89
|
+
|
|
90
|
+
if clip_count == 0:
|
|
91
|
+
empty_tracks.append({"index": i, "name": track.name})
|
|
92
|
+
|
|
93
|
+
# MIDI track with no devices (no instrument loaded)
|
|
94
|
+
if track.has_midi_input and len(list(track.devices)) == 0:
|
|
95
|
+
no_device_midi_tracks.append({"index": i, "name": track.name})
|
|
96
|
+
|
|
97
|
+
# Build issues from checks
|
|
98
|
+
if armed_tracks:
|
|
99
|
+
issues.append({
|
|
100
|
+
"type": "armed_tracks",
|
|
101
|
+
"severity": "warning",
|
|
102
|
+
"message": "%d track(s) left armed" % len(armed_tracks),
|
|
103
|
+
"details": armed_tracks,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
if soloed_tracks:
|
|
107
|
+
issues.append({
|
|
108
|
+
"type": "soloed_tracks",
|
|
109
|
+
"severity": "warning",
|
|
110
|
+
"message": "%d track(s) soloed — other tracks are silenced" % len(soloed_tracks),
|
|
111
|
+
"details": soloed_tracks,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if len(muted_tracks) > len(tracks) * 0.5 and len(muted_tracks) > 2:
|
|
115
|
+
issues.append({
|
|
116
|
+
"type": "many_muted",
|
|
117
|
+
"severity": "info",
|
|
118
|
+
"message": "%d of %d tracks muted — consider cleaning up unused tracks" % (
|
|
119
|
+
len(muted_tracks), len(tracks)
|
|
120
|
+
),
|
|
121
|
+
"details": muted_tracks,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
if unnamed_tracks:
|
|
125
|
+
issues.append({
|
|
126
|
+
"type": "unnamed_tracks",
|
|
127
|
+
"severity": "info",
|
|
128
|
+
"message": "%d track(s) have default names" % len(unnamed_tracks),
|
|
129
|
+
"details": unnamed_tracks,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
if empty_tracks:
|
|
133
|
+
issues.append({
|
|
134
|
+
"type": "empty_tracks",
|
|
135
|
+
"severity": "info",
|
|
136
|
+
"message": "%d track(s) have no clips" % len(empty_tracks),
|
|
137
|
+
"details": empty_tracks,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
if no_device_midi_tracks:
|
|
141
|
+
issues.append({
|
|
142
|
+
"type": "no_instrument",
|
|
143
|
+
"severity": "warning",
|
|
144
|
+
"message": "%d MIDI track(s) have no instrument loaded" % len(no_device_midi_tracks),
|
|
145
|
+
"details": no_device_midi_tracks,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
# ── Scene-level checks ──────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
empty_scenes = []
|
|
151
|
+
for i, scene in enumerate(scenes):
|
|
152
|
+
has_any_clip = False
|
|
153
|
+
for slots in track_slots:
|
|
154
|
+
if i < len(slots) and slots[i].has_clip:
|
|
155
|
+
has_any_clip = True
|
|
156
|
+
break
|
|
157
|
+
if not has_any_clip:
|
|
158
|
+
empty_scenes.append({"index": i, "name": scene.name})
|
|
159
|
+
|
|
160
|
+
stats["empty_scenes"] = len(empty_scenes)
|
|
161
|
+
|
|
162
|
+
if empty_scenes and len(empty_scenes) > 1:
|
|
163
|
+
issues.append({
|
|
164
|
+
"type": "empty_scenes",
|
|
165
|
+
"severity": "info",
|
|
166
|
+
"message": "%d scene(s) have no clips across any track" % len(empty_scenes),
|
|
167
|
+
"details": empty_scenes[:10], # Cap at 10 to avoid huge payloads
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
# ── Return track checks ─────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
soloed_returns = []
|
|
173
|
+
for i, track in enumerate(return_tracks):
|
|
174
|
+
if track.solo:
|
|
175
|
+
soloed_returns.append({"index": i, "name": track.name})
|
|
176
|
+
|
|
177
|
+
if soloed_returns:
|
|
178
|
+
issues.append({
|
|
179
|
+
"type": "soloed_returns",
|
|
180
|
+
"severity": "warning",
|
|
181
|
+
"message": "%d return track(s) soloed" % len(soloed_returns),
|
|
182
|
+
"details": soloed_returns,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
# ── Summary ─────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
severity_counts = {"warning": 0, "info": 0}
|
|
188
|
+
for issue in issues:
|
|
189
|
+
severity_counts[issue["severity"]] = severity_counts.get(issue["severity"], 0) + 1
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
"healthy": len(issues) == 0,
|
|
193
|
+
"issue_count": len(issues),
|
|
194
|
+
"warnings": severity_counts.get("warning", 0),
|
|
195
|
+
"info": severity_counts.get("info", 0),
|
|
196
|
+
"issues": issues,
|
|
197
|
+
"stats": stats,
|
|
198
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LivePilot - Mixing domain handlers (8 commands).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .router import register
|
|
6
|
+
from .utils import get_track
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@register("set_track_volume")
|
|
10
|
+
def set_track_volume(song, params):
|
|
11
|
+
"""Set the volume of a track."""
|
|
12
|
+
track_index = int(params["track_index"])
|
|
13
|
+
track = get_track(song, track_index)
|
|
14
|
+
volume = float(params["volume"])
|
|
15
|
+
track.mixer_device.volume.value = volume
|
|
16
|
+
return {"index": track_index, "volume": track.mixer_device.volume.value}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@register("set_track_pan")
|
|
20
|
+
def set_track_pan(song, params):
|
|
21
|
+
"""Set the panning of a track."""
|
|
22
|
+
track_index = int(params["track_index"])
|
|
23
|
+
track = get_track(song, track_index)
|
|
24
|
+
pan = float(params["pan"])
|
|
25
|
+
track.mixer_device.panning.value = pan
|
|
26
|
+
return {"index": track_index, "pan": track.mixer_device.panning.value}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@register("set_track_send")
|
|
30
|
+
def set_track_send(song, params):
|
|
31
|
+
"""Set a send value on a track."""
|
|
32
|
+
track_index = int(params["track_index"])
|
|
33
|
+
track = get_track(song, track_index)
|
|
34
|
+
send_index = int(params["send_index"])
|
|
35
|
+
sends = list(track.mixer_device.sends)
|
|
36
|
+
if send_index < 0 or send_index >= len(sends):
|
|
37
|
+
raise IndexError(
|
|
38
|
+
"Send index %d out of range (0..%d)"
|
|
39
|
+
% (send_index, len(sends) - 1)
|
|
40
|
+
)
|
|
41
|
+
sends[send_index].value = float(params["value"])
|
|
42
|
+
return {
|
|
43
|
+
"index": track_index,
|
|
44
|
+
"send_index": send_index,
|
|
45
|
+
"value": sends[send_index].value,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@register("get_return_tracks")
|
|
50
|
+
def get_return_tracks(song, params):
|
|
51
|
+
"""Return info about all return tracks."""
|
|
52
|
+
result = []
|
|
53
|
+
for i, track in enumerate(song.return_tracks):
|
|
54
|
+
result.append({
|
|
55
|
+
"index": i,
|
|
56
|
+
"name": track.name,
|
|
57
|
+
"color_index": track.color_index,
|
|
58
|
+
"volume": track.mixer_device.volume.value,
|
|
59
|
+
"panning": track.mixer_device.panning.value,
|
|
60
|
+
})
|
|
61
|
+
return {"return_tracks": result}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@register("get_master_track")
|
|
65
|
+
def get_master_track(song, params):
|
|
66
|
+
"""Return info about the master track."""
|
|
67
|
+
master = song.master_track
|
|
68
|
+
devices = []
|
|
69
|
+
for i, device in enumerate(master.devices):
|
|
70
|
+
devices.append({
|
|
71
|
+
"index": i,
|
|
72
|
+
"name": device.name,
|
|
73
|
+
"class_name": device.class_name,
|
|
74
|
+
"is_active": device.is_active,
|
|
75
|
+
})
|
|
76
|
+
return {
|
|
77
|
+
"name": master.name,
|
|
78
|
+
"volume": master.mixer_device.volume.value,
|
|
79
|
+
"panning": master.mixer_device.panning.value,
|
|
80
|
+
"devices": devices,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@register("set_master_volume")
|
|
85
|
+
def set_master_volume(song, params):
|
|
86
|
+
"""Set the master track volume."""
|
|
87
|
+
volume = float(params["volume"])
|
|
88
|
+
song.master_track.mixer_device.volume.value = volume
|
|
89
|
+
return {"volume": song.master_track.mixer_device.volume.value}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@register("get_track_routing")
|
|
93
|
+
def get_track_routing(song, params):
|
|
94
|
+
"""Get the input/output routing for a track."""
|
|
95
|
+
track_index = int(params["track_index"])
|
|
96
|
+
track = get_track(song, track_index)
|
|
97
|
+
result = {"index": track_index}
|
|
98
|
+
try:
|
|
99
|
+
result["input_routing_type"] = track.input_routing_type.display_name
|
|
100
|
+
except AttributeError:
|
|
101
|
+
result["input_routing_type"] = None
|
|
102
|
+
try:
|
|
103
|
+
result["input_routing_channel"] = track.input_routing_channel.display_name
|
|
104
|
+
except AttributeError:
|
|
105
|
+
result["input_routing_channel"] = None
|
|
106
|
+
try:
|
|
107
|
+
result["output_routing_type"] = track.output_routing_type.display_name
|
|
108
|
+
except AttributeError:
|
|
109
|
+
result["output_routing_type"] = None
|
|
110
|
+
try:
|
|
111
|
+
result["output_routing_channel"] = track.output_routing_channel.display_name
|
|
112
|
+
except AttributeError:
|
|
113
|
+
result["output_routing_channel"] = None
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@register("set_track_routing")
|
|
118
|
+
def set_track_routing(song, params):
|
|
119
|
+
"""Set input/output routing for a track by display name."""
|
|
120
|
+
track_index = int(params["track_index"])
|
|
121
|
+
track = get_track(song, track_index)
|
|
122
|
+
if not any(k in params for k in ("input_type", "input_channel", "output_type", "output_channel")):
|
|
123
|
+
raise ValueError("At least one routing parameter must be provided")
|
|
124
|
+
result = {"index": track_index}
|
|
125
|
+
|
|
126
|
+
if "input_type" in params:
|
|
127
|
+
name = str(params["input_type"])
|
|
128
|
+
available = list(track.available_input_routing_types)
|
|
129
|
+
matched = None
|
|
130
|
+
for rt in available:
|
|
131
|
+
if rt.display_name == name:
|
|
132
|
+
matched = rt
|
|
133
|
+
break
|
|
134
|
+
if matched is None:
|
|
135
|
+
options = [rt.display_name for rt in available]
|
|
136
|
+
raise ValueError(
|
|
137
|
+
"Input routing type '%s' not found. Available: %s"
|
|
138
|
+
% (name, ", ".join(options))
|
|
139
|
+
)
|
|
140
|
+
track.input_routing_type = matched
|
|
141
|
+
result["input_routing_type"] = track.input_routing_type.display_name
|
|
142
|
+
|
|
143
|
+
if "input_channel" in params:
|
|
144
|
+
name = str(params["input_channel"])
|
|
145
|
+
available = list(track.available_input_routing_channels)
|
|
146
|
+
matched = None
|
|
147
|
+
for ch in available:
|
|
148
|
+
if ch.display_name == name:
|
|
149
|
+
matched = ch
|
|
150
|
+
break
|
|
151
|
+
if matched is None:
|
|
152
|
+
options = [ch.display_name for ch in available]
|
|
153
|
+
raise ValueError(
|
|
154
|
+
"Input routing channel '%s' not found. Available: %s"
|
|
155
|
+
% (name, ", ".join(options))
|
|
156
|
+
)
|
|
157
|
+
track.input_routing_channel = matched
|
|
158
|
+
result["input_routing_channel"] = track.input_routing_channel.display_name
|
|
159
|
+
|
|
160
|
+
if "output_type" in params:
|
|
161
|
+
name = str(params["output_type"])
|
|
162
|
+
available = list(track.available_output_routing_types)
|
|
163
|
+
matched = None
|
|
164
|
+
for rt in available:
|
|
165
|
+
if rt.display_name == name:
|
|
166
|
+
matched = rt
|
|
167
|
+
break
|
|
168
|
+
if matched is None:
|
|
169
|
+
options = [rt.display_name for rt in available]
|
|
170
|
+
raise ValueError(
|
|
171
|
+
"Output routing type '%s' not found. Available: %s"
|
|
172
|
+
% (name, ", ".join(options))
|
|
173
|
+
)
|
|
174
|
+
track.output_routing_type = matched
|
|
175
|
+
result["output_routing_type"] = track.output_routing_type.display_name
|
|
176
|
+
|
|
177
|
+
if "output_channel" in params:
|
|
178
|
+
name = str(params["output_channel"])
|
|
179
|
+
available = list(track.available_output_routing_channels)
|
|
180
|
+
matched = None
|
|
181
|
+
for ch in available:
|
|
182
|
+
if ch.display_name == name:
|
|
183
|
+
matched = ch
|
|
184
|
+
break
|
|
185
|
+
if matched is None:
|
|
186
|
+
options = [ch.display_name for ch in available]
|
|
187
|
+
raise ValueError(
|
|
188
|
+
"Output routing channel '%s' not found. Available: %s"
|
|
189
|
+
% (name, ", ".join(options))
|
|
190
|
+
)
|
|
191
|
+
track.output_routing_channel = matched
|
|
192
|
+
result["output_routing_channel"] = track.output_routing_channel.display_name
|
|
193
|
+
|
|
194
|
+
return result
|