livepilot 1.9.22 → 1.9.24
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/.mcpbignore +40 -0
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +84 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +141 -72
- package/bin/livepilot.js +135 -0
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/agents/livepilot-producer/AGENT.md +13 -0
- package/livepilot/commands/arrange.md +42 -23
- package/livepilot/commands/mix.md +34 -19
- package/livepilot/commands/perform.md +31 -19
- package/livepilot/commands/sounddesign.md +38 -25
- package/livepilot/skills/livepilot-arrangement/SKILL.md +2 -1
- package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +60 -4
- package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +11 -11
- package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +25 -25
- package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +21 -21
- package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +13 -13
- package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +13 -13
- package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +5 -5
- package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +16 -16
- package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +40 -40
- package/livepilot/skills/livepilot-core/references/m4l-devices.md +3 -3
- package/livepilot/skills/livepilot-core/references/overview.md +4 -4
- package/livepilot/skills/livepilot-evaluation/SKILL.md +12 -8
- package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +2 -2
- package/livepilot/skills/livepilot-mix-engine/SKILL.md +1 -1
- package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +2 -2
- package/livepilot/skills/livepilot-mixing/SKILL.md +3 -1
- package/livepilot/skills/livepilot-notes/SKILL.md +2 -1
- package/livepilot/skills/livepilot-release/SKILL.md +29 -15
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +2 -2
- package/livepilot/skills/livepilot-wonder/SKILL.md +62 -0
- package/livepilot.mcpb +0 -0
- package/manifest.json +91 -0
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/creative_constraints/__init__.py +6 -0
- package/mcp_server/creative_constraints/engine.py +277 -0
- package/mcp_server/creative_constraints/models.py +75 -0
- package/mcp_server/creative_constraints/tools.py +341 -0
- package/mcp_server/experiment/__init__.py +6 -0
- package/mcp_server/experiment/engine.py +213 -0
- package/mcp_server/experiment/models.py +120 -0
- package/mcp_server/experiment/tools.py +263 -0
- package/mcp_server/hook_hunter/__init__.py +5 -0
- package/mcp_server/hook_hunter/analyzer.py +365 -0
- package/mcp_server/hook_hunter/models.py +58 -0
- package/mcp_server/hook_hunter/tools.py +588 -0
- package/mcp_server/memory/taste_graph.py +328 -0
- package/mcp_server/memory/tools.py +99 -0
- package/mcp_server/mix_engine/critics.py +2 -2
- package/mcp_server/mix_engine/models.py +1 -1
- package/mcp_server/mix_engine/state_builder.py +2 -2
- package/mcp_server/musical_intelligence/__init__.py +8 -0
- package/mcp_server/musical_intelligence/detectors.py +434 -0
- package/mcp_server/musical_intelligence/phrase_critic.py +163 -0
- package/mcp_server/musical_intelligence/tools.py +224 -0
- package/mcp_server/persistence/__init__.py +1 -0
- package/mcp_server/persistence/base_store.py +82 -0
- package/mcp_server/persistence/project_store.py +106 -0
- package/mcp_server/persistence/taste_store.py +122 -0
- package/mcp_server/preview_studio/__init__.py +5 -0
- package/mcp_server/preview_studio/engine.py +280 -0
- package/mcp_server/preview_studio/models.py +74 -0
- package/mcp_server/preview_studio/tools.py +466 -0
- package/mcp_server/runtime/capability.py +66 -0
- package/mcp_server/runtime/capability_probe.py +118 -0
- package/mcp_server/runtime/execution_router.py +139 -0
- package/mcp_server/runtime/remote_commands.py +82 -0
- package/mcp_server/runtime/session_kernel.py +96 -0
- package/mcp_server/runtime/tools.py +90 -1
- package/mcp_server/semantic_moves/__init__.py +13 -0
- package/mcp_server/semantic_moves/compiler.py +116 -0
- package/mcp_server/semantic_moves/mix_compilers.py +291 -0
- package/mcp_server/semantic_moves/mix_moves.py +157 -0
- package/mcp_server/semantic_moves/models.py +46 -0
- package/mcp_server/semantic_moves/performance_compilers.py +208 -0
- package/mcp_server/semantic_moves/performance_moves.py +81 -0
- package/mcp_server/semantic_moves/registry.py +32 -0
- package/mcp_server/semantic_moves/resolvers.py +126 -0
- package/mcp_server/semantic_moves/sound_design_compilers.py +266 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +78 -0
- package/mcp_server/semantic_moves/tools.py +205 -0
- package/mcp_server/semantic_moves/transition_compilers.py +222 -0
- package/mcp_server/semantic_moves/transition_moves.py +76 -0
- package/mcp_server/server.py +10 -0
- package/mcp_server/services/__init__.py +1 -0
- package/mcp_server/services/motif_service.py +67 -0
- package/mcp_server/session_continuity/__init__.py +6 -0
- package/mcp_server/session_continuity/models.py +86 -0
- package/mcp_server/session_continuity/tools.py +230 -0
- package/mcp_server/session_continuity/tracker.py +263 -0
- package/mcp_server/song_brain/__init__.py +6 -0
- package/mcp_server/song_brain/builder.py +504 -0
- package/mcp_server/song_brain/models.py +136 -0
- package/mcp_server/song_brain/tools.py +312 -0
- package/mcp_server/stuckness_detector/__init__.py +5 -0
- package/mcp_server/stuckness_detector/detector.py +400 -0
- package/mcp_server/stuckness_detector/models.py +66 -0
- package/mcp_server/stuckness_detector/tools.py +195 -0
- package/mcp_server/tools/_conductor.py +104 -6
- package/mcp_server/tools/analyzer.py +1 -1
- package/mcp_server/tools/devices.py +34 -0
- package/mcp_server/wonder_mode/__init__.py +6 -0
- package/mcp_server/wonder_mode/diagnosis.py +84 -0
- package/mcp_server/wonder_mode/engine.py +493 -0
- package/mcp_server/wonder_mode/session.py +114 -0
- package/mcp_server/wonder_mode/tools.py +290 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/browser.py +4 -1
- package/remote_script/LivePilot/devices.py +29 -0
- package/remote_script/LivePilot/tracks.py +11 -4
- package/scripts/generate_tool_catalog.py +131 -0
- package/scripts/sync_metadata.py +132 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""SongBrain MCP tools — 3 tools for song identity modeling.
|
|
2
|
+
|
|
3
|
+
build_song_brain — construct the musical identity of the current piece
|
|
4
|
+
explain_song_identity — human-readable summary of what the song is about
|
|
5
|
+
detect_identity_drift — compare before/after to detect identity damage
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from fastmcp import Context
|
|
11
|
+
|
|
12
|
+
from ..server import mcp
|
|
13
|
+
from . import builder
|
|
14
|
+
from .models import SongBrain
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Module-level fallback for consumers without ctx.
|
|
18
|
+
# Prefer ctx.lifespan_context["current_brain"] when ctx is available.
|
|
19
|
+
_current_brain: SongBrain | None = None
|
|
20
|
+
|
|
21
|
+
# Snapshot store: brain_id -> SongBrain, max 10 snapshots
|
|
22
|
+
_brain_snapshots: dict[str, SongBrain] = {}
|
|
23
|
+
_MAX_SNAPSHOTS = 10
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _set_brain(ctx: Context, brain: SongBrain) -> None:
|
|
27
|
+
"""Store brain in lifespan_context, module fallback, and snapshot store."""
|
|
28
|
+
global _current_brain
|
|
29
|
+
_current_brain = brain
|
|
30
|
+
ctx.lifespan_context["current_brain"] = brain
|
|
31
|
+
# Save snapshot for later drift comparison
|
|
32
|
+
_brain_snapshots[brain.brain_id] = brain
|
|
33
|
+
# Evict oldest if over limit
|
|
34
|
+
while len(_brain_snapshots) > _MAX_SNAPSHOTS:
|
|
35
|
+
oldest_key = next(iter(_brain_snapshots))
|
|
36
|
+
del _brain_snapshots[oldest_key]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_snapshot(brain_id: str) -> SongBrain | None:
|
|
40
|
+
"""Retrieve a past brain snapshot by ID."""
|
|
41
|
+
return _brain_snapshots.get(brain_id)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_ableton(ctx: Context):
|
|
45
|
+
return ctx.lifespan_context["ableton"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _fetch_session_data(ctx: Context) -> dict:
|
|
49
|
+
"""Fetch all available session data for brain building.
|
|
50
|
+
|
|
51
|
+
Populates real data from Ableton and pure-computation modules:
|
|
52
|
+
- motif_data: from get_motif_graph (motif engine)
|
|
53
|
+
- composition_analysis: from musical intelligence section inference
|
|
54
|
+
- role_graph: from semantic move resolvers (track role inference)
|
|
55
|
+
- recent_moves: from session-scoped action ledger
|
|
56
|
+
"""
|
|
57
|
+
ableton = _get_ableton(ctx)
|
|
58
|
+
data: dict = {
|
|
59
|
+
"session_info": {},
|
|
60
|
+
"scenes": [],
|
|
61
|
+
"tracks": [],
|
|
62
|
+
"motif_data": {},
|
|
63
|
+
"composition_analysis": {},
|
|
64
|
+
"role_graph": {},
|
|
65
|
+
"recent_moves": [],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
data["session_info"] = ableton.send_command("get_session_info", {})
|
|
70
|
+
except Exception:
|
|
71
|
+
data["session_info"] = {"tempo": 120.0, "track_count": 0}
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
matrix = ableton.send_command("get_scene_matrix")
|
|
75
|
+
data["scenes"] = [
|
|
76
|
+
{"name": s.get("name", f"Scene {i}"), "clips": row}
|
|
77
|
+
for i, (s, row) in enumerate(
|
|
78
|
+
zip(matrix.get("scenes", []), matrix.get("matrix", []))
|
|
79
|
+
)
|
|
80
|
+
]
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
info = data["session_info"]
|
|
86
|
+
tracks_list = info.get("tracks", [])
|
|
87
|
+
data["tracks"] = tracks_list if isinstance(tracks_list, list) else []
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
# Motif data — via shared motif service (pure-Python, not TCP)
|
|
92
|
+
try:
|
|
93
|
+
from ..services.motif_service import get_motif_data, fetch_notes_from_ableton
|
|
94
|
+
notes_by_track = fetch_notes_from_ableton(ableton, data.get("tracks", []))
|
|
95
|
+
data["motif_data"] = get_motif_data(notes_by_track)
|
|
96
|
+
except Exception:
|
|
97
|
+
pass # Motif graph requires notes in clips; empty is valid
|
|
98
|
+
|
|
99
|
+
# Composition analysis — from musical intelligence detectors (pure computation)
|
|
100
|
+
try:
|
|
101
|
+
from ..musical_intelligence import detectors
|
|
102
|
+
total_tracks = data["session_info"].get("track_count", 6)
|
|
103
|
+
purposes = detectors.infer_section_purposes(data["scenes"], total_tracks)
|
|
104
|
+
arc = detectors.score_emotional_arc(purposes)
|
|
105
|
+
data["composition_analysis"] = {
|
|
106
|
+
"sections": [p.to_dict() for p in purposes],
|
|
107
|
+
"emotional_arc": arc.to_dict(),
|
|
108
|
+
}
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
# Role graph — from semantic move resolvers (pure computation, no I/O)
|
|
113
|
+
try:
|
|
114
|
+
from ..semantic_moves.resolvers import infer_role
|
|
115
|
+
roles = {}
|
|
116
|
+
for track in data["tracks"]:
|
|
117
|
+
name = track.get("name", "")
|
|
118
|
+
role = infer_role(name)
|
|
119
|
+
roles[name] = {"index": track.get("index", 0), "role": role}
|
|
120
|
+
data["role_graph"] = roles
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
# Recent moves — from session-scoped action ledger
|
|
125
|
+
try:
|
|
126
|
+
from ..runtime.action_ledger import SessionLedger
|
|
127
|
+
ledger = ctx.lifespan_context.get("action_ledger")
|
|
128
|
+
if isinstance(ledger, SessionLedger):
|
|
129
|
+
recent = ledger.get_recent_moves(limit=10)
|
|
130
|
+
data["recent_moves"] = [e.to_dict() for e in recent]
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
return data
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@mcp.tool()
|
|
138
|
+
def build_song_brain(ctx: Context) -> dict:
|
|
139
|
+
"""Build the musical identity model for the current song.
|
|
140
|
+
|
|
141
|
+
Analyzes the session to identify:
|
|
142
|
+
- identity_core: the strongest defining idea
|
|
143
|
+
- sacred_elements: motifs/textures/grooves that must be preserved
|
|
144
|
+
- section_purposes: what each section is trying to do emotionally
|
|
145
|
+
- energy_arc: rise/fall shape across sections
|
|
146
|
+
- open_questions: what the song has not resolved yet
|
|
147
|
+
|
|
148
|
+
Call this at the start of complex creative workflows.
|
|
149
|
+
Returns the full SongBrain as a dict.
|
|
150
|
+
"""
|
|
151
|
+
data = _fetch_session_data(ctx)
|
|
152
|
+
|
|
153
|
+
# Capability reporting — what data was actually available
|
|
154
|
+
from ..runtime.capability import build_capability
|
|
155
|
+
cap = build_capability(
|
|
156
|
+
required=["session_info", "scenes", "tracks", "motif_data", "composition_analysis", "role_graph"],
|
|
157
|
+
available={
|
|
158
|
+
"session_info": bool(data.get("session_info", {}).get("tempo")),
|
|
159
|
+
"scenes": bool(data.get("scenes")),
|
|
160
|
+
"tracks": bool(data.get("tracks")),
|
|
161
|
+
"motif_data": bool(data.get("motif_data")),
|
|
162
|
+
"composition_analysis": bool(data.get("composition_analysis")),
|
|
163
|
+
"role_graph": bool(data.get("role_graph")),
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
brain = builder.build_song_brain(
|
|
168
|
+
session_info=data["session_info"],
|
|
169
|
+
scenes=data["scenes"],
|
|
170
|
+
tracks=data["tracks"],
|
|
171
|
+
motif_data=data["motif_data"],
|
|
172
|
+
composition_analysis=data["composition_analysis"],
|
|
173
|
+
role_graph=data["role_graph"],
|
|
174
|
+
recent_moves=data["recent_moves"],
|
|
175
|
+
)
|
|
176
|
+
_set_brain(ctx, brain)
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
**brain.to_dict(),
|
|
180
|
+
"summary": brain.summary,
|
|
181
|
+
"capability": cap.to_dict(),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@mcp.tool()
|
|
186
|
+
def explain_song_identity(ctx: Context) -> dict:
|
|
187
|
+
"""Explain the current song's identity in human musical language.
|
|
188
|
+
|
|
189
|
+
If no SongBrain exists yet, builds one first. Returns a structured
|
|
190
|
+
explanation suitable for the agent to talk about the song naturally.
|
|
191
|
+
"""
|
|
192
|
+
if _current_brain is None:
|
|
193
|
+
data = _fetch_session_data(ctx)
|
|
194
|
+
brain = builder.build_song_brain(
|
|
195
|
+
session_info=data["session_info"],
|
|
196
|
+
scenes=data["scenes"],
|
|
197
|
+
tracks=data["tracks"],
|
|
198
|
+
motif_data=data["motif_data"],
|
|
199
|
+
composition_analysis=data["composition_analysis"],
|
|
200
|
+
role_graph=data["role_graph"],
|
|
201
|
+
recent_moves=data["recent_moves"],
|
|
202
|
+
)
|
|
203
|
+
_set_brain(ctx, brain)
|
|
204
|
+
|
|
205
|
+
brain = _current_brain
|
|
206
|
+
explanation: dict = {
|
|
207
|
+
"identity": brain.identity_core,
|
|
208
|
+
"confidence": brain.identity_confidence,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# Sacred elements in natural language
|
|
212
|
+
if brain.sacred_elements:
|
|
213
|
+
explanation["protect"] = [
|
|
214
|
+
f"{e.element_type}: {e.description}" for e in brain.sacred_elements
|
|
215
|
+
]
|
|
216
|
+
else:
|
|
217
|
+
explanation["protect"] = ["No clearly sacred elements detected yet"]
|
|
218
|
+
|
|
219
|
+
# What each section does
|
|
220
|
+
if brain.section_purposes:
|
|
221
|
+
explanation["sections"] = [
|
|
222
|
+
f"{s.label} — {s.emotional_intent} (energy {s.energy_level:.0%})"
|
|
223
|
+
for s in brain.section_purposes
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
# Energy shape
|
|
227
|
+
if brain.energy_arc:
|
|
228
|
+
arc = brain.energy_arc
|
|
229
|
+
if len(arc) >= 3:
|
|
230
|
+
peak_idx = arc.index(max(arc))
|
|
231
|
+
peak_pct = peak_idx / max(len(arc) - 1, 1)
|
|
232
|
+
if peak_pct < 0.3:
|
|
233
|
+
explanation["energy_shape"] = "front-loaded — peaks early"
|
|
234
|
+
elif peak_pct > 0.7:
|
|
235
|
+
explanation["energy_shape"] = "slow burn — builds to late peak"
|
|
236
|
+
else:
|
|
237
|
+
explanation["energy_shape"] = "centered arc — peaks in the middle"
|
|
238
|
+
else:
|
|
239
|
+
explanation["energy_shape"] = "short form — limited arc data"
|
|
240
|
+
|
|
241
|
+
# Open questions
|
|
242
|
+
if brain.open_questions:
|
|
243
|
+
explanation["open_questions"] = [q.question for q in brain.open_questions]
|
|
244
|
+
|
|
245
|
+
# Drift warning
|
|
246
|
+
if brain.identity_drift_risk > 0.3:
|
|
247
|
+
explanation["warning"] = (
|
|
248
|
+
f"Identity drift risk is {brain.identity_drift_risk:.0%} — "
|
|
249
|
+
"recent edits may be moving the song away from itself"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
explanation["summary"] = brain.summary
|
|
253
|
+
return explanation
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@mcp.tool()
|
|
257
|
+
def detect_identity_drift(
|
|
258
|
+
ctx: Context,
|
|
259
|
+
before_brain_id: str = "",
|
|
260
|
+
) -> dict:
|
|
261
|
+
"""Detect whether recent changes have damaged the song's identity.
|
|
262
|
+
|
|
263
|
+
Compares the current state against a previous SongBrain snapshot.
|
|
264
|
+
If before_brain_id is provided, looks up that specific snapshot.
|
|
265
|
+
If empty, uses the last cached brain.
|
|
266
|
+
If no previous brain exists, builds baseline and reports no drift.
|
|
267
|
+
|
|
268
|
+
before_brain_id: optional brain_id from a previous build_song_brain call.
|
|
269
|
+
|
|
270
|
+
Returns drift score, changed elements, sacred damage, and recommendation.
|
|
271
|
+
"""
|
|
272
|
+
# Look up the "before" brain — by ID if provided, else use last cached
|
|
273
|
+
if before_brain_id:
|
|
274
|
+
before = _get_snapshot(before_brain_id)
|
|
275
|
+
if before is None:
|
|
276
|
+
available = list(_brain_snapshots.keys())
|
|
277
|
+
return {
|
|
278
|
+
"error": f"No snapshot found for brain_id '{before_brain_id}'",
|
|
279
|
+
"available_snapshots": available,
|
|
280
|
+
}
|
|
281
|
+
else:
|
|
282
|
+
before = _current_brain
|
|
283
|
+
|
|
284
|
+
# Build fresh brain from current state
|
|
285
|
+
data = _fetch_session_data(ctx)
|
|
286
|
+
after = builder.build_song_brain(
|
|
287
|
+
session_info=data["session_info"],
|
|
288
|
+
scenes=data["scenes"],
|
|
289
|
+
tracks=data["tracks"],
|
|
290
|
+
motif_data=data["motif_data"],
|
|
291
|
+
composition_analysis=data["composition_analysis"],
|
|
292
|
+
role_graph=data["role_graph"],
|
|
293
|
+
recent_moves=data["recent_moves"],
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if before is None:
|
|
297
|
+
_set_brain(ctx, after)
|
|
298
|
+
return {
|
|
299
|
+
"drift_score": 0.0,
|
|
300
|
+
"note": "No previous brain to compare — this is the baseline",
|
|
301
|
+
"brain_id": after.brain_id,
|
|
302
|
+
"recommendation": "safe",
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
drift = builder.detect_identity_drift(before, after)
|
|
306
|
+
_set_brain(ctx, after)
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
**drift.to_dict(),
|
|
310
|
+
"before_brain_id": before.brain_id,
|
|
311
|
+
"after_brain_id": after.brain_id,
|
|
312
|
+
}
|