livepilot 1.9.24 → 1.10.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/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +223 -0
- package/CONTRIBUTING.md +2 -2
- package/LICENSE +62 -21
- package/README.md +291 -276
- package/bin/livepilot.js +87 -0
- package/installer/codex.js +147 -0
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/skills/livepilot-arrangement/SKILL.md +18 -1
- package/livepilot/skills/livepilot-core/SKILL.md +22 -5
- package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
- package/livepilot/skills/livepilot-core/references/overview.md +13 -9
- package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
- package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
- package/livepilot/skills/livepilot-devices/SKILL.md +39 -4
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +23 -19
- package/livepilot/skills/livepilot-sample-engine/SKILL.md +105 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +45 -0
- package/livepilot/skills/livepilot-wonder/SKILL.md +17 -0
- package/livepilot.mcpb +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/manifest.json +4 -4
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +357 -0
- package/mcp_server/atlas/device_atlas.json +44067 -0
- package/mcp_server/atlas/enrichments/__init__.py +111 -0
- package/mcp_server/atlas/enrichments/audio_effects/auto_filter.yaml +162 -0
- package/mcp_server/atlas/enrichments/audio_effects/beat_repeat.yaml +183 -0
- package/mcp_server/atlas/enrichments/audio_effects/channel_eq.yaml +126 -0
- package/mcp_server/atlas/enrichments/audio_effects/chorus_ensemble.yaml +149 -0
- package/mcp_server/atlas/enrichments/audio_effects/color_limiter.yaml +109 -0
- package/mcp_server/atlas/enrichments/audio_effects/compressor.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb.yaml +143 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb_pro.yaml +178 -0
- package/mcp_server/atlas/enrichments/audio_effects/delay.yaml +151 -0
- package/mcp_server/atlas/enrichments/audio_effects/drum_buss.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/dynamic_tube.yaml +147 -0
- package/mcp_server/atlas/enrichments/audio_effects/echo.yaml +167 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_eight.yaml +148 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_three.yaml +121 -0
- package/mcp_server/atlas/enrichments/audio_effects/erosion.yaml +103 -0
- package/mcp_server/atlas/enrichments/audio_effects/filter_delay.yaml +173 -0
- package/mcp_server/atlas/enrichments/audio_effects/gate.yaml +130 -0
- package/mcp_server/atlas/enrichments/audio_effects/gated_delay.yaml +133 -0
- package/mcp_server/atlas/enrichments/audio_effects/glue_compressor.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/grain_delay.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/hybrid_reverb.yaml +160 -0
- package/mcp_server/atlas/enrichments/audio_effects/limiter.yaml +97 -0
- package/mcp_server/atlas/enrichments/audio_effects/multiband_dynamics.yaml +174 -0
- package/mcp_server/atlas/enrichments/audio_effects/overdrive.yaml +119 -0
- package/mcp_server/atlas/enrichments/audio_effects/pedal.yaml +145 -0
- package/mcp_server/atlas/enrichments/audio_effects/phaser_flanger.yaml +161 -0
- package/mcp_server/atlas/enrichments/audio_effects/redux.yaml +114 -0
- package/mcp_server/atlas/enrichments/audio_effects/reverb.yaml +190 -0
- package/mcp_server/atlas/enrichments/audio_effects/roar.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/saturator.yaml +146 -0
- package/mcp_server/atlas/enrichments/audio_effects/shifter.yaml +154 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_resonator.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_time.yaml +164 -0
- package/mcp_server/atlas/enrichments/audio_effects/vector_delay.yaml +140 -0
- package/mcp_server/atlas/enrichments/audio_effects/vinyl_distortion.yaml +141 -0
- package/mcp_server/atlas/enrichments/instruments/analog.yaml +222 -0
- package/mcp_server/atlas/enrichments/instruments/bass.yaml +202 -0
- package/mcp_server/atlas/enrichments/instruments/collision.yaml +150 -0
- package/mcp_server/atlas/enrichments/instruments/drift.yaml +167 -0
- package/mcp_server/atlas/enrichments/instruments/electric.yaml +137 -0
- package/mcp_server/atlas/enrichments/instruments/emit.yaml +163 -0
- package/mcp_server/atlas/enrichments/instruments/meld.yaml +164 -0
- package/mcp_server/atlas/enrichments/instruments/operator.yaml +197 -0
- package/mcp_server/atlas/enrichments/instruments/poli.yaml +192 -0
- package/mcp_server/atlas/enrichments/instruments/sampler.yaml +218 -0
- package/mcp_server/atlas/enrichments/instruments/simpler.yaml +217 -0
- package/mcp_server/atlas/enrichments/instruments/tension.yaml +156 -0
- package/mcp_server/atlas/enrichments/instruments/tree_tone.yaml +162 -0
- package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +165 -0
- package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +166 -0
- package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +162 -0
- package/mcp_server/atlas/enrichments/midi_effects/arpeggiator.yaml +156 -0
- package/mcp_server/atlas/enrichments/midi_effects/bouncy_notes.yaml +93 -0
- package/mcp_server/atlas/enrichments/midi_effects/chord.yaml +147 -0
- package/mcp_server/atlas/enrichments/midi_effects/melodic_steps.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_echo.yaml +108 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_length.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/pitch.yaml +76 -0
- package/mcp_server/atlas/enrichments/midi_effects/random.yaml +117 -0
- package/mcp_server/atlas/enrichments/midi_effects/rhythmic_steps.yaml +103 -0
- package/mcp_server/atlas/enrichments/midi_effects/scale.yaml +83 -0
- package/mcp_server/atlas/enrichments/midi_effects/step_arp.yaml +112 -0
- package/mcp_server/atlas/enrichments/midi_effects/velocity.yaml +119 -0
- package/mcp_server/atlas/enrichments/utility/amp.yaml +159 -0
- package/mcp_server/atlas/enrichments/utility/cabinet.yaml +109 -0
- package/mcp_server/atlas/enrichments/utility/corpus.yaml +150 -0
- package/mcp_server/atlas/enrichments/utility/resonators.yaml +131 -0
- package/mcp_server/atlas/enrichments/utility/spectrum.yaml +63 -0
- package/mcp_server/atlas/enrichments/utility/tuner.yaml +51 -0
- package/mcp_server/atlas/enrichments/utility/utility.yaml +136 -0
- package/mcp_server/atlas/enrichments/utility/vocoder.yaml +160 -0
- package/mcp_server/atlas/scanner.py +236 -0
- package/mcp_server/atlas/tools.py +224 -0
- package/mcp_server/composer/__init__.py +1 -0
- package/mcp_server/composer/engine.py +532 -0
- package/mcp_server/composer/layer_planner.py +427 -0
- package/mcp_server/composer/prompt_parser.py +329 -0
- package/mcp_server/composer/sample_resolver.py +153 -0
- package/mcp_server/composer/tools.py +211 -0
- package/mcp_server/connection.py +53 -8
- package/mcp_server/corpus/__init__.py +377 -0
- package/mcp_server/device_forge/__init__.py +1 -0
- package/mcp_server/device_forge/builder.py +377 -0
- package/mcp_server/device_forge/models.py +142 -0
- package/mcp_server/device_forge/templates.py +483 -0
- package/mcp_server/device_forge/tools.py +162 -0
- package/mcp_server/m4l_bridge.py +1 -0
- package/mcp_server/memory/taste_accessors.py +47 -0
- package/mcp_server/preview_studio/engine.py +9 -2
- package/mcp_server/preview_studio/tools.py +78 -35
- package/mcp_server/project_brain/tools.py +34 -0
- package/mcp_server/runtime/capability_probe.py +21 -2
- package/mcp_server/runtime/execution_router.py +184 -38
- package/mcp_server/runtime/live_version.py +102 -0
- package/mcp_server/runtime/mcp_dispatch.py +46 -0
- package/mcp_server/runtime/remote_commands.py +13 -5
- package/mcp_server/runtime/tools.py +66 -29
- package/mcp_server/sample_engine/__init__.py +1 -0
- package/mcp_server/sample_engine/analyzer.py +216 -0
- package/mcp_server/sample_engine/critics.py +390 -0
- package/mcp_server/sample_engine/models.py +193 -0
- package/mcp_server/sample_engine/moves.py +127 -0
- package/mcp_server/sample_engine/planner.py +186 -0
- package/mcp_server/sample_engine/slice_workflow.py +190 -0
- package/mcp_server/sample_engine/sources.py +540 -0
- package/mcp_server/sample_engine/techniques.py +908 -0
- package/mcp_server/sample_engine/tools.py +545 -0
- package/mcp_server/semantic_moves/__init__.py +3 -0
- package/mcp_server/semantic_moves/device_creation_moves.py +237 -0
- package/mcp_server/semantic_moves/mix_moves.py +8 -8
- package/mcp_server/semantic_moves/models.py +7 -7
- package/mcp_server/semantic_moves/performance_moves.py +4 -4
- package/mcp_server/semantic_moves/sample_compilers.py +377 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +4 -4
- package/mcp_server/semantic_moves/tools.py +63 -10
- package/mcp_server/semantic_moves/transition_moves.py +4 -4
- package/mcp_server/server.py +71 -1
- package/mcp_server/session_continuity/tracker.py +4 -1
- package/mcp_server/sound_design/critics.py +89 -1
- package/mcp_server/splice_client/__init__.py +1 -0
- package/mcp_server/splice_client/client.py +347 -0
- package/mcp_server/splice_client/models.py +96 -0
- package/mcp_server/splice_client/protos/__init__.py +1 -0
- package/mcp_server/splice_client/protos/app_pb2.py +319 -0
- package/mcp_server/splice_client/protos/app_pb2.pyi +1153 -0
- package/mcp_server/splice_client/protos/app_pb2_grpc.py +1946 -0
- package/mcp_server/tools/_conductor.py +16 -0
- package/mcp_server/tools/_planner_engine.py +24 -0
- package/mcp_server/tools/analyzer.py +2 -0
- package/mcp_server/tools/arrangement.py +69 -0
- package/mcp_server/tools/automation.py +15 -2
- package/mcp_server/tools/devices.py +117 -6
- package/mcp_server/tools/notes.py +37 -4
- package/mcp_server/tools/planner.py +3 -0
- package/mcp_server/wonder_mode/diagnosis.py +5 -0
- package/mcp_server/wonder_mode/engine.py +144 -14
- package/mcp_server/wonder_mode/tools.py +33 -1
- package/package.json +14 -4
- package/remote_script/LivePilot/__init__.py +8 -1
- package/remote_script/LivePilot/arrangement.py +114 -0
- package/remote_script/LivePilot/browser.py +56 -1
- package/remote_script/LivePilot/devices.py +246 -6
- package/remote_script/LivePilot/mixing.py +8 -3
- package/remote_script/LivePilot/server.py +5 -1
- package/remote_script/LivePilot/transport.py +3 -0
- package/remote_script/LivePilot/version_detect.py +78 -0
|
@@ -25,11 +25,11 @@ def discover_moves(
|
|
|
25
25
|
"""Find semantic moves relevant to the request.
|
|
26
26
|
|
|
27
27
|
Uses keyword scoring + optional taste reranking + constraint filtering.
|
|
28
|
-
Returns full move dicts including
|
|
28
|
+
Returns full move dicts including plan_template (via registry.get_move).
|
|
29
29
|
"""
|
|
30
30
|
from ..semantic_moves import registry
|
|
31
31
|
|
|
32
|
-
all_moves = registry.list_moves() # returns to_dict() — no
|
|
32
|
+
all_moves = registry.list_moves() # returns to_dict() — no plan_template
|
|
33
33
|
if not all_moves:
|
|
34
34
|
return []
|
|
35
35
|
|
|
@@ -77,7 +77,7 @@ def discover_moves(
|
|
|
77
77
|
|
|
78
78
|
scored.sort(key=lambda x: -x[1])
|
|
79
79
|
|
|
80
|
-
# Enrich with full
|
|
80
|
+
# Enrich with full plan_template via get_move()
|
|
81
81
|
result = []
|
|
82
82
|
for move_dict, score in scored:
|
|
83
83
|
full_move = registry.get_move(move_dict["move_id"])
|
|
@@ -98,7 +98,7 @@ def discover_moves(
|
|
|
98
98
|
for move in result:
|
|
99
99
|
plan = {"steps": [
|
|
100
100
|
{"action": step.get("tool", ""), **step}
|
|
101
|
-
for step in (move.get("
|
|
101
|
+
for step in (move.get("plan_template") or [])
|
|
102
102
|
]}
|
|
103
103
|
validation = validate_plan_against_constraints(plan, active_constraints)
|
|
104
104
|
if validation["valid"]:
|
|
@@ -137,9 +137,9 @@ def _with_envelope(move: dict, tier: str) -> dict:
|
|
|
137
137
|
# ── Distinctness selection ───────────────────────────────────────
|
|
138
138
|
|
|
139
139
|
|
|
140
|
-
def
|
|
141
|
-
"""Extract the set of tool names from a move's
|
|
142
|
-
plan = move.get("
|
|
140
|
+
def _plan_template_shape(move: dict) -> frozenset[str]:
|
|
141
|
+
"""Extract the set of tool names from a move's plan_template."""
|
|
142
|
+
plan = move.get("plan_template") or []
|
|
143
143
|
return frozenset(step.get("tool", "") for step in plan if step.get("tool"))
|
|
144
144
|
|
|
145
145
|
|
|
@@ -147,7 +147,7 @@ def select_distinct_variants(scored_moves: list[dict]) -> list[dict]:
|
|
|
147
147
|
"""Select genuinely distinct moves for variant generation.
|
|
148
148
|
|
|
149
149
|
Each selected move must differ from all previously selected moves by
|
|
150
|
-
at least one of: move_id, family, or
|
|
150
|
+
at least one of: move_id, family, or plan_template shape.
|
|
151
151
|
Returns 0-3 moves.
|
|
152
152
|
"""
|
|
153
153
|
if not scored_moves:
|
|
@@ -160,7 +160,7 @@ def select_distinct_variants(scored_moves: list[dict]) -> list[dict]:
|
|
|
160
160
|
for move in scored_moves:
|
|
161
161
|
mid = move.get("move_id", "")
|
|
162
162
|
family = move.get("family", "")
|
|
163
|
-
shape =
|
|
163
|
+
shape = _plan_template_shape(move)
|
|
164
164
|
|
|
165
165
|
# Skip duplicate move_ids
|
|
166
166
|
if mid in used_ids:
|
|
@@ -190,14 +190,45 @@ _NOVELTY_LEVELS = {"safe": 0.25, "strong": 0.55, "unexpected": 0.85}
|
|
|
190
190
|
_RISK_TO_EFFECT = {"low": "preserves", "medium": "evolves", "high": "contrasts"}
|
|
191
191
|
|
|
192
192
|
|
|
193
|
+
def _compile_variant_plan(move_dict: dict, kernel: dict | None) -> dict | None:
|
|
194
|
+
"""Compile a move through the semantic compiler if possible.
|
|
195
|
+
|
|
196
|
+
Returns CompiledPlan.to_dict() or None if no compiler is registered.
|
|
197
|
+
"""
|
|
198
|
+
if kernel is None:
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
move_id = move_dict.get("move_id", "")
|
|
202
|
+
from ..semantic_moves.compiler import compile as sem_compile, _COMPILERS
|
|
203
|
+
from ..semantic_moves import registry
|
|
204
|
+
|
|
205
|
+
if move_id not in _COMPILERS:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
move_obj = registry.get_move(move_id)
|
|
209
|
+
if move_obj is None:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
plan = sem_compile(move_obj, kernel)
|
|
214
|
+
return plan.to_dict()
|
|
215
|
+
except Exception:
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
|
|
193
219
|
def build_variant(
|
|
194
220
|
label: str,
|
|
195
221
|
move_dict: dict,
|
|
196
222
|
song_brain: Optional[dict] = None,
|
|
197
223
|
novelty_level: float = 0.5,
|
|
198
224
|
variant_id: str = "",
|
|
225
|
+
kernel: dict | None = None,
|
|
199
226
|
) -> dict:
|
|
200
|
-
"""Build a variant dict from a real move + SongBrain context.
|
|
227
|
+
"""Build a variant dict from a real move + SongBrain context.
|
|
228
|
+
|
|
229
|
+
If kernel is provided, compiles the move through the semantic compiler
|
|
230
|
+
for an executable plan. Otherwise falls back to plan_template metadata.
|
|
231
|
+
"""
|
|
201
232
|
song_brain = song_brain or {}
|
|
202
233
|
targets = move_dict.get("targets", {})
|
|
203
234
|
protect = move_dict.get("protect", {})
|
|
@@ -226,6 +257,10 @@ def build_variant(
|
|
|
226
257
|
if sacred and identity_effect == "preserves":
|
|
227
258
|
why += f". Preserves {sacred[0].get('description', 'sacred elements')}"
|
|
228
259
|
|
|
260
|
+
# Compile through semantic compiler if kernel available
|
|
261
|
+
compiled = _compile_variant_plan(move_dict, kernel)
|
|
262
|
+
analytical = compiled is None
|
|
263
|
+
|
|
229
264
|
return {
|
|
230
265
|
"variant_id": variant_id,
|
|
231
266
|
"label": label,
|
|
@@ -239,11 +274,11 @@ def build_variant(
|
|
|
239
274
|
"novelty_level": novelty_level,
|
|
240
275
|
"taste_fit": 0.5,
|
|
241
276
|
"targets_snapshot": dict(targets),
|
|
242
|
-
"compiled_plan":
|
|
277
|
+
"compiled_plan": compiled,
|
|
243
278
|
"score": 0.0,
|
|
244
279
|
"rank": 0,
|
|
245
280
|
"score_breakdown": {},
|
|
246
|
-
"analytical_only":
|
|
281
|
+
"analytical_only": analytical,
|
|
247
282
|
"distinctness_reason": "",
|
|
248
283
|
}
|
|
249
284
|
|
|
@@ -390,8 +425,86 @@ def _all_same_family(variants: list[dict]) -> bool:
|
|
|
390
425
|
return len(families) <= 1 and len(variants) > 1
|
|
391
426
|
|
|
392
427
|
|
|
393
|
-
# ──
|
|
428
|
+
# ── Corpus intelligence enrichment ──────────────────────────────
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _get_corpus_hints(request_text: str, diagnosis: dict | None) -> dict | None:
|
|
432
|
+
"""Query the corpus for creative hints relevant to the request.
|
|
433
|
+
|
|
434
|
+
Returns a dict with emotional_recipe, genre_chain, automation_density,
|
|
435
|
+
and technique_suggestions — or None if corpus is unavailable.
|
|
436
|
+
"""
|
|
437
|
+
try:
|
|
438
|
+
from ..corpus import get_corpus
|
|
439
|
+
except ImportError:
|
|
440
|
+
return None
|
|
394
441
|
|
|
442
|
+
corpus = get_corpus()
|
|
443
|
+
if not corpus.emotional_recipes and not corpus.genre_chains:
|
|
444
|
+
return None
|
|
445
|
+
|
|
446
|
+
hints: dict = {}
|
|
447
|
+
request_lower = request_text.lower()
|
|
448
|
+
|
|
449
|
+
# Check for emotional keywords
|
|
450
|
+
_EMOTION_KEYWORDS = {
|
|
451
|
+
"warm": "warmth & comfort", "cold": "tension & anxiety",
|
|
452
|
+
"dark": "melancholy", "bright": "euphoria",
|
|
453
|
+
"aggressive": "danger", "soft": "warmth & comfort",
|
|
454
|
+
"anxious": "tension & anxiety", "nostalgic": "nostalgia",
|
|
455
|
+
"vast": "vastness", "ethereal": "vastness",
|
|
456
|
+
"sad": "melancholy", "happy": "euphoria",
|
|
457
|
+
"tension": "tension & anxiety", "release": "euphoria",
|
|
458
|
+
}
|
|
459
|
+
for keyword, emotion_key in _EMOTION_KEYWORDS.items():
|
|
460
|
+
if keyword in request_lower:
|
|
461
|
+
recipe = corpus.suggest_for_emotion(emotion_key)
|
|
462
|
+
if recipe:
|
|
463
|
+
hints["emotional_recipe"] = {
|
|
464
|
+
"emotion": recipe.emotion,
|
|
465
|
+
"technique_count": len(recipe.techniques),
|
|
466
|
+
"first_techniques": [t[:100] for t in recipe.techniques[:3]],
|
|
467
|
+
}
|
|
468
|
+
break
|
|
469
|
+
|
|
470
|
+
# Check for genre keywords
|
|
471
|
+
_GENRE_KEYWORDS = ["dub", "techno", "minimal", "ambient", "idm", "trap",
|
|
472
|
+
"sophie", "arca", "house", "trance", "drum and bass"]
|
|
473
|
+
for genre in _GENRE_KEYWORDS:
|
|
474
|
+
if genre in request_lower:
|
|
475
|
+
chain = corpus.get_genre_chain(genre)
|
|
476
|
+
if chain:
|
|
477
|
+
hints["genre_chain"] = {
|
|
478
|
+
"genre": chain.genre,
|
|
479
|
+
"devices": chain.devices[:5],
|
|
480
|
+
"description": chain.description[:120],
|
|
481
|
+
}
|
|
482
|
+
break
|
|
483
|
+
|
|
484
|
+
# Check for physical model keywords
|
|
485
|
+
_MATERIAL_KEYWORDS = ["water", "metal", "glass", "breath", "fire", "electric"]
|
|
486
|
+
for material in _MATERIAL_KEYWORDS:
|
|
487
|
+
if material in request_lower:
|
|
488
|
+
model = corpus.suggest_for_material(material)
|
|
489
|
+
if model:
|
|
490
|
+
hints["physical_model"] = {
|
|
491
|
+
"material": model.material,
|
|
492
|
+
"devices": model.devices[:4],
|
|
493
|
+
}
|
|
494
|
+
break
|
|
495
|
+
|
|
496
|
+
# Automation density from diagnosis section type
|
|
497
|
+
if diagnosis:
|
|
498
|
+
problem_class = diagnosis.get("problem_class", "")
|
|
499
|
+
if "static" in problem_class or "flat" in problem_class:
|
|
500
|
+
hints["automation_density"] = corpus.get_automation_density_for_section("peak")
|
|
501
|
+
elif "breakdown" in problem_class:
|
|
502
|
+
hints["automation_density"] = corpus.get_automation_density_for_section("breakdown")
|
|
503
|
+
|
|
504
|
+
return hints if hints else None
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# ── Pipeline orchestrator ────────────────────────────────────────
|
|
395
508
|
|
|
396
509
|
|
|
397
510
|
def generate_wonder_variants(
|
|
@@ -401,6 +514,8 @@ def generate_wonder_variants(
|
|
|
401
514
|
song_brain: dict | None = None,
|
|
402
515
|
taste_graph: object = None,
|
|
403
516
|
active_constraints: object = None,
|
|
517
|
+
session_info: dict | None = None,
|
|
518
|
+
sample_context: dict | None = None,
|
|
404
519
|
) -> dict:
|
|
405
520
|
"""Full wonder mode pipeline: discover -> select distinct -> build -> taste -> rank."""
|
|
406
521
|
song_brain = song_brain or {}
|
|
@@ -414,6 +529,17 @@ def generate_wonder_variants(
|
|
|
414
529
|
labels = ["safe", "strong", "unexpected"]
|
|
415
530
|
variants = []
|
|
416
531
|
|
|
532
|
+
# Load corpus intelligence for variant enrichment
|
|
533
|
+
corpus_hints = _get_corpus_hints(request_text, diagnosis)
|
|
534
|
+
|
|
535
|
+
# Build kernel for variant compilation
|
|
536
|
+
kernel = {
|
|
537
|
+
"session_info": session_info or {},
|
|
538
|
+
"mode": "improve",
|
|
539
|
+
}
|
|
540
|
+
if sample_context:
|
|
541
|
+
kernel.update(sample_context)
|
|
542
|
+
|
|
417
543
|
# Build executable variants from distinct moves
|
|
418
544
|
for i, move in enumerate(distinct):
|
|
419
545
|
label = labels[i]
|
|
@@ -424,11 +550,15 @@ def generate_wonder_variants(
|
|
|
424
550
|
song_brain=song_brain,
|
|
425
551
|
novelty_level=_NOVELTY_LEVELS.get(label, 0.5),
|
|
426
552
|
variant_id=f"{set_prefix}_{label}",
|
|
553
|
+
kernel=kernel,
|
|
427
554
|
)
|
|
428
555
|
if taste_graph is not None:
|
|
429
556
|
# Score taste on envelope-adjusted move for consistency with targets_snapshot
|
|
430
557
|
v["taste_fit"] = compute_taste_fit(move_with_envelope, taste_graph)
|
|
431
558
|
v["distinctness_reason"] = _explain_distinctness(move, distinct, i)
|
|
559
|
+
# Enrich with corpus knowledge
|
|
560
|
+
if corpus_hints:
|
|
561
|
+
v["corpus_hints"] = corpus_hints
|
|
432
562
|
variants.append(v)
|
|
433
563
|
|
|
434
564
|
executable_count = len(variants)
|
|
@@ -483,7 +613,7 @@ def _explain_distinctness(move: dict, all_moves: list[dict], index: int) -> str:
|
|
|
483
613
|
|
|
484
614
|
if family not in other_families:
|
|
485
615
|
return f"Different family: {family}"
|
|
486
|
-
shape =
|
|
616
|
+
shape = _plan_template_shape(move)
|
|
487
617
|
return f"Different approach: {', '.join(sorted(shape))}"
|
|
488
618
|
|
|
489
619
|
|
|
@@ -126,14 +126,46 @@ def enter_wonder_mode(
|
|
|
126
126
|
action_ledger=action_ledger,
|
|
127
127
|
)
|
|
128
128
|
|
|
129
|
+
# 1b. If diagnosis includes sample domains, search for candidates
|
|
130
|
+
sample_context = {}
|
|
131
|
+
diag_dict = diagnosis.to_dict()
|
|
132
|
+
candidate_domains = diag_dict.get("candidate_domains") or []
|
|
133
|
+
if "sample" in candidate_domains:
|
|
134
|
+
try:
|
|
135
|
+
from ..sample_engine.tools import get_sample_opportunities, search_samples
|
|
136
|
+
opportunities = get_sample_opportunities(ctx)
|
|
137
|
+
if opportunities.get("opportunities"):
|
|
138
|
+
opp = opportunities["opportunities"][0]
|
|
139
|
+
query = opp.get("search_query", opp.get("description", "sample"))
|
|
140
|
+
results = search_samples(ctx, query=query, max_results=3)
|
|
141
|
+
candidates = results.get("results", [])
|
|
142
|
+
if candidates:
|
|
143
|
+
best = candidates[0]
|
|
144
|
+
sample_context["sample_file_path"] = best.get("file_path", "")
|
|
145
|
+
sample_context["sample_name"] = best.get("name", "")
|
|
146
|
+
sample_context["material_type"] = best.get("material_type", "")
|
|
147
|
+
except Exception:
|
|
148
|
+
pass # Graceful degradation — analytical variants still work
|
|
149
|
+
|
|
150
|
+
# 1c. Get session info for kernel
|
|
151
|
+
session_info = {}
|
|
152
|
+
try:
|
|
153
|
+
ableton = ctx.lifespan_context.get("ableton")
|
|
154
|
+
if ableton:
|
|
155
|
+
session_info = ableton.send_command("get_session_info", {})
|
|
156
|
+
except Exception:
|
|
157
|
+
pass
|
|
158
|
+
|
|
129
159
|
# 2. Generate variants
|
|
130
160
|
result = engine.generate_wonder_variants(
|
|
131
161
|
request_text=request_text,
|
|
132
|
-
diagnosis=
|
|
162
|
+
diagnosis=diag_dict,
|
|
133
163
|
kernel_id=kernel_id,
|
|
134
164
|
song_brain=song_brain,
|
|
135
165
|
taste_graph=taste_graph,
|
|
136
166
|
active_constraints=active_constraints,
|
|
167
|
+
session_info=session_info,
|
|
168
|
+
sample_context=sample_context,
|
|
137
169
|
)
|
|
138
170
|
|
|
139
171
|
# 3. Create WonderSession (unique per invocation, not deterministic)
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.1",
|
|
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 — 317 tools, 43 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 engines)",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
|
-
"license": "
|
|
7
|
+
"license": "BSL-1.1",
|
|
8
8
|
"type": "commonjs",
|
|
9
9
|
"bin": {
|
|
10
10
|
"livepilot": "./bin/livepilot.js"
|
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
"bugs": {
|
|
18
18
|
"url": "https://github.com/dreamrec/LivePilot/issues"
|
|
19
19
|
},
|
|
20
|
+
"funding": [
|
|
21
|
+
{
|
|
22
|
+
"type": "github",
|
|
23
|
+
"url": "https://github.com/sponsors/dreamrec"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
20
26
|
"keywords": [
|
|
21
27
|
"mcp",
|
|
22
28
|
"mcp-server",
|
|
@@ -29,7 +35,11 @@
|
|
|
29
35
|
"ai",
|
|
30
36
|
"sound-design",
|
|
31
37
|
"mixing",
|
|
32
|
-
"arrangement"
|
|
38
|
+
"arrangement",
|
|
39
|
+
"splice",
|
|
40
|
+
"sample-engine",
|
|
41
|
+
"auto-composition",
|
|
42
|
+
"device-atlas"
|
|
33
43
|
],
|
|
34
44
|
"engines": {
|
|
35
45
|
"node": ">=18.0.0"
|
|
@@ -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.
|
|
8
|
+
__version__ = "1.10.1"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from .server import LivePilotServer
|
|
@@ -20,6 +20,7 @@ from . import browser # noqa: F401 — registers browser handlers
|
|
|
20
20
|
from . import arrangement # noqa: F401 — registers arrangement handlers
|
|
21
21
|
from . import diagnostics # noqa: F401 — registers diagnostics handler
|
|
22
22
|
from . import clip_automation # noqa: F401 — registers clip automation handlers
|
|
23
|
+
from . import version_detect # noqa: F401 — version detection
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def create_instance(c_instance):
|
|
@@ -36,6 +37,12 @@ class LivePilot(ControlSurface):
|
|
|
36
37
|
self._server.start()
|
|
37
38
|
self.log_message("LivePilot v%s starting..." % __version__)
|
|
38
39
|
self.show_message("LivePilot v%s starting..." % __version__)
|
|
40
|
+
v = version_detect.version_string()
|
|
41
|
+
self.log_message("LivePilot detected Ableton Live %s" % v)
|
|
42
|
+
features = version_detect.get_api_features()
|
|
43
|
+
enabled = [k for k, flag in features.items() if flag]
|
|
44
|
+
if enabled:
|
|
45
|
+
self.log_message(" Enabled features: %s" % ", ".join(enabled))
|
|
39
46
|
|
|
40
47
|
def disconnect(self):
|
|
41
48
|
"""Called by Ableton when the script is unloaded."""
|
|
@@ -169,6 +169,70 @@ def create_arrangement_clip(song, params):
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
|
|
172
|
+
@register("create_native_arrangement_clip")
|
|
173
|
+
def create_native_arrangement_clip(song, params):
|
|
174
|
+
"""Create an empty MIDI clip in arrangement using the native 12.1.10+ API.
|
|
175
|
+
|
|
176
|
+
Unlike create_arrangement_clip (which duplicates a session clip),
|
|
177
|
+
this creates a true native clip with full automation envelope support.
|
|
178
|
+
|
|
179
|
+
Required: track_index, start_time, length
|
|
180
|
+
Optional: name, color_index
|
|
181
|
+
"""
|
|
182
|
+
from .version_detect import has_feature
|
|
183
|
+
|
|
184
|
+
if not has_feature("create_midi_clip_arrangement"):
|
|
185
|
+
raise RuntimeError(
|
|
186
|
+
"create_native_arrangement_clip requires Live 12.1.10+. "
|
|
187
|
+
"Use create_arrangement_clip (session clip duplication) instead."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
track_index = int(params["track_index"])
|
|
191
|
+
start_time = float(params["start_time"])
|
|
192
|
+
length = float(params["length"])
|
|
193
|
+
if length <= 0:
|
|
194
|
+
raise ValueError("length must be > 0")
|
|
195
|
+
if start_time < 0:
|
|
196
|
+
raise ValueError("start_time must be >= 0")
|
|
197
|
+
|
|
198
|
+
track = get_track(song, track_index)
|
|
199
|
+
if not track.has_midi_input:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
"Track %d is not a MIDI track — create_native_arrangement_clip "
|
|
202
|
+
"only works on MIDI tracks" % track_index
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
song.begin_undo_step()
|
|
206
|
+
try:
|
|
207
|
+
clip = track.create_midi_clip(start_time, length)
|
|
208
|
+
|
|
209
|
+
name = params.get("name")
|
|
210
|
+
if name:
|
|
211
|
+
clip.name = str(name)
|
|
212
|
+
color_index = params.get("color_index")
|
|
213
|
+
if color_index is not None:
|
|
214
|
+
clip.color_index = int(color_index)
|
|
215
|
+
finally:
|
|
216
|
+
song.end_undo_step()
|
|
217
|
+
|
|
218
|
+
# Find the clip index in arrangement_clips
|
|
219
|
+
clip_index = None
|
|
220
|
+
for i, c in enumerate(track.arrangement_clips):
|
|
221
|
+
if abs(c.start_time - start_time) < 0.01:
|
|
222
|
+
clip_index = i
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
"track_index": track_index,
|
|
227
|
+
"clip_index": clip_index,
|
|
228
|
+
"start_time": start_time,
|
|
229
|
+
"length": length,
|
|
230
|
+
"name": clip.name,
|
|
231
|
+
"has_envelope_support": True,
|
|
232
|
+
"native": True,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
172
236
|
@register("add_arrangement_notes")
|
|
173
237
|
def add_arrangement_notes(song, params):
|
|
174
238
|
"""Add MIDI notes to an arrangement clip (by index in arrangement_clips)."""
|
|
@@ -713,3 +777,53 @@ def back_to_arranger(song, params):
|
|
|
713
777
|
"""Switch playback from session clips back to the arrangement timeline."""
|
|
714
778
|
song.back_to_arranger = True
|
|
715
779
|
return {"back_to_arranger": True}
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
@register("force_arrangement")
|
|
783
|
+
def force_arrangement(song, params):
|
|
784
|
+
"""Force ALL tracks to follow the arrangement timeline.
|
|
785
|
+
|
|
786
|
+
Stops all session clips, releases every track from session override,
|
|
787
|
+
sets back_to_arranger, and optionally jumps to a start position.
|
|
788
|
+
|
|
789
|
+
This is the atomic "play the arrangement from the top" command.
|
|
790
|
+
"""
|
|
791
|
+
# 1. Stop playback
|
|
792
|
+
was_playing = song.is_playing
|
|
793
|
+
if was_playing:
|
|
794
|
+
song.stop_playing()
|
|
795
|
+
|
|
796
|
+
# 2. Stop playing clip slots individually to release session overrides
|
|
797
|
+
# (track.stop_all_clips() throws STATE_ERROR when tracks have no clips)
|
|
798
|
+
for track in list(song.tracks) + list(song.return_tracks):
|
|
799
|
+
try:
|
|
800
|
+
for slot in track.clip_slots:
|
|
801
|
+
if slot.has_clip and slot.is_playing:
|
|
802
|
+
slot.clip.stop()
|
|
803
|
+
except Exception:
|
|
804
|
+
pass
|
|
805
|
+
|
|
806
|
+
# 3. Global back-to-arranger
|
|
807
|
+
song.back_to_arranger = True
|
|
808
|
+
|
|
809
|
+
# 4. Jump to position (default: start)
|
|
810
|
+
beat_time = float(params.get("beat_time", 0))
|
|
811
|
+
song.current_song_time = max(0, beat_time)
|
|
812
|
+
|
|
813
|
+
# 5. Set loop if requested
|
|
814
|
+
if "loop_length" in params:
|
|
815
|
+
song.loop_start = float(params.get("loop_start", 0))
|
|
816
|
+
song.loop_length = float(params["loop_length"])
|
|
817
|
+
song.loop = True
|
|
818
|
+
|
|
819
|
+
# 6. Start playback if requested (default: yes)
|
|
820
|
+
play = params.get("play", True)
|
|
821
|
+
if play:
|
|
822
|
+
song.start_playing()
|
|
823
|
+
|
|
824
|
+
return {
|
|
825
|
+
"arrangement_active": True,
|
|
826
|
+
"position": song.current_song_time,
|
|
827
|
+
"is_playing": song.is_playing,
|
|
828
|
+
"loop": song.loop,
|
|
829
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
LivePilot - Browser domain handlers (
|
|
2
|
+
LivePilot - Browser domain handlers (6 commands).
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import Live
|
|
@@ -386,6 +386,61 @@ def load_browser_item(song, params):
|
|
|
386
386
|
)
|
|
387
387
|
|
|
388
388
|
|
|
389
|
+
_SCAN_MAX_ITERATIONS = 100000
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _scan_recursive(item, results, depth, max_depth, max_per_category,
|
|
393
|
+
_counter=None):
|
|
394
|
+
"""Recursively collect loadable browser items with iteration cap."""
|
|
395
|
+
if _counter is None:
|
|
396
|
+
_counter = [0]
|
|
397
|
+
if depth > max_depth or len(results) >= max_per_category:
|
|
398
|
+
return
|
|
399
|
+
for child in item.children:
|
|
400
|
+
_counter[0] += 1
|
|
401
|
+
if _counter[0] > _SCAN_MAX_ITERATIONS or len(results) >= max_per_category:
|
|
402
|
+
return
|
|
403
|
+
if child.is_loadable:
|
|
404
|
+
entry = {"name": child.name, "is_loadable": True}
|
|
405
|
+
try:
|
|
406
|
+
entry["uri"] = child.uri
|
|
407
|
+
except AttributeError:
|
|
408
|
+
entry["uri"] = None
|
|
409
|
+
results.append(entry)
|
|
410
|
+
if child.is_folder:
|
|
411
|
+
_scan_recursive(
|
|
412
|
+
child, results, depth + 1, max_depth, max_per_category,
|
|
413
|
+
_counter
|
|
414
|
+
)
|
|
415
|
+
if len(results) >= max_per_category:
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@register("scan_browser_deep")
|
|
420
|
+
def scan_browser_deep(song, params):
|
|
421
|
+
"""Walk the entire browser tree and return all loadable items by category.
|
|
422
|
+
|
|
423
|
+
Parameters
|
|
424
|
+
----------
|
|
425
|
+
max_per_category : int, optional
|
|
426
|
+
Maximum items to collect per top-level category (default 1000).
|
|
427
|
+
max_depth : int, optional
|
|
428
|
+
Maximum recursion depth into the browser tree (default 4).
|
|
429
|
+
"""
|
|
430
|
+
max_per_category = int(params.get("max_per_category", 1000))
|
|
431
|
+
max_depth = int(params.get("max_depth", 4))
|
|
432
|
+
browser = _get_browser()
|
|
433
|
+
categories = _get_categories(browser)
|
|
434
|
+
|
|
435
|
+
result = {}
|
|
436
|
+
for cat_name, cat_item in categories.items():
|
|
437
|
+
items = []
|
|
438
|
+
_scan_recursive(cat_item, items, 0, max_depth, max_per_category)
|
|
439
|
+
result[cat_name] = items
|
|
440
|
+
|
|
441
|
+
return {"categories": result}
|
|
442
|
+
|
|
443
|
+
|
|
389
444
|
@register("get_device_presets")
|
|
390
445
|
def get_device_presets(song, params):
|
|
391
446
|
"""List available presets for a device type by searching the browser.
|