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
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
"""Sample Engine MCP tools — 7 intelligence-layer tools.
|
|
2
|
+
|
|
3
|
+
No new Ableton communication — these orchestrate existing tools
|
|
4
|
+
through the analyzer, critics, planner, and technique library.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from fastmcp import Context
|
|
12
|
+
|
|
13
|
+
from ..server import mcp
|
|
14
|
+
from .models import SampleProfile, SampleIntent, SampleFitReport
|
|
15
|
+
from .analyzer import build_profile_from_filename
|
|
16
|
+
from .critics import run_all_sample_critics
|
|
17
|
+
from .planner import select_technique, compile_sample_plan
|
|
18
|
+
from .techniques import find_techniques, list_techniques, get_technique
|
|
19
|
+
from .sources import BrowserSource, FilesystemSource, SpliceSource, build_search_queries
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@mcp.tool()
|
|
23
|
+
async def analyze_sample(
|
|
24
|
+
ctx: Context,
|
|
25
|
+
file_path: Optional[str] = None,
|
|
26
|
+
track_index: Optional[int] = None,
|
|
27
|
+
clip_index: Optional[int] = None,
|
|
28
|
+
) -> dict:
|
|
29
|
+
"""Analyze a sample and build a complete SampleProfile.
|
|
30
|
+
|
|
31
|
+
Detects material type, key, BPM, spectral character, and recommends
|
|
32
|
+
Simpler mode, slice method, and warp mode. Provide either file_path
|
|
33
|
+
OR track_index + clip_index to analyze a clip in the session.
|
|
34
|
+
|
|
35
|
+
Falls back to filename-only analysis if M4L bridge unavailable.
|
|
36
|
+
"""
|
|
37
|
+
if file_path is None and track_index is None:
|
|
38
|
+
return {"error": "Provide either file_path or track_index + clip_index"}
|
|
39
|
+
|
|
40
|
+
if track_index is not None and file_path is None:
|
|
41
|
+
try:
|
|
42
|
+
bridge = ctx.lifespan_context.get("m4l")
|
|
43
|
+
if bridge:
|
|
44
|
+
result = await bridge.send_command(
|
|
45
|
+
"get_clip_file_path", track_index, clip_index or 0
|
|
46
|
+
)
|
|
47
|
+
if not result.get("error"):
|
|
48
|
+
file_path = result.get("file_path")
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
if file_path is None:
|
|
53
|
+
return {"error": "Could not determine file path — provide file_path directly"}
|
|
54
|
+
|
|
55
|
+
source = "session_clip" if track_index is not None else "filesystem"
|
|
56
|
+
profile = build_profile_from_filename(file_path, source=source)
|
|
57
|
+
return profile.to_dict()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@mcp.tool()
|
|
61
|
+
def evaluate_sample_fit(
|
|
62
|
+
ctx: Context,
|
|
63
|
+
file_path: str,
|
|
64
|
+
intent: str = "layer",
|
|
65
|
+
philosophy: str = "auto",
|
|
66
|
+
) -> dict:
|
|
67
|
+
"""Run the 6-critic battery to evaluate how well a sample fits the current song.
|
|
68
|
+
|
|
69
|
+
Returns overall score, per-critic scores, recommendations, and
|
|
70
|
+
both surgeon (precise) and alchemist (transformative) plans.
|
|
71
|
+
|
|
72
|
+
intent: rhythm, texture, layer, melody, vocal, atmosphere, transform
|
|
73
|
+
philosophy: surgeon, alchemist, auto (context-decides)
|
|
74
|
+
"""
|
|
75
|
+
profile = build_profile_from_filename(file_path)
|
|
76
|
+
sample_intent = SampleIntent(
|
|
77
|
+
intent_type=intent, philosophy=philosophy,
|
|
78
|
+
description=f"Evaluate fitness for {intent}",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Gather song context
|
|
82
|
+
song_key = None
|
|
83
|
+
session_tempo = 120.0
|
|
84
|
+
existing_roles: list[str] = []
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
ableton = ctx.lifespan_context["ableton"]
|
|
88
|
+
info = ableton.send_command("get_session_info", {})
|
|
89
|
+
session_tempo = info.get("tempo", 120.0)
|
|
90
|
+
|
|
91
|
+
# Get track names as roles
|
|
92
|
+
track_count = info.get("track_count", 0)
|
|
93
|
+
for i in range(min(track_count, 16)):
|
|
94
|
+
try:
|
|
95
|
+
track_info = ableton.send_command("get_track_info", {"track_index": i})
|
|
96
|
+
name = track_info.get("name", "").lower()
|
|
97
|
+
if name:
|
|
98
|
+
existing_roles.append(name)
|
|
99
|
+
except Exception:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
# Detect key from MIDI tracks
|
|
103
|
+
try:
|
|
104
|
+
from ..tools._theory_engine import detect_key
|
|
105
|
+
for i in range(min(track_count, 8)):
|
|
106
|
+
try:
|
|
107
|
+
clip_info = ableton.send_command("get_clip_info", {
|
|
108
|
+
"track_index": i, "clip_index": 0,
|
|
109
|
+
})
|
|
110
|
+
if clip_info.get("is_midi"):
|
|
111
|
+
notes_result = ableton.send_command("get_notes", {
|
|
112
|
+
"track_index": i, "clip_index": 0,
|
|
113
|
+
})
|
|
114
|
+
notes = notes_result.get("notes", [])
|
|
115
|
+
if notes:
|
|
116
|
+
key_result = detect_key(notes)
|
|
117
|
+
mode = key_result.get("mode", "")
|
|
118
|
+
mode_suffix = "m" if "minor" in mode else ""
|
|
119
|
+
song_key = f"{key_result['tonic_name']}{mode_suffix}"
|
|
120
|
+
break
|
|
121
|
+
except Exception:
|
|
122
|
+
continue
|
|
123
|
+
except ImportError:
|
|
124
|
+
pass
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
critics = run_all_sample_critics(
|
|
129
|
+
profile=profile,
|
|
130
|
+
intent=sample_intent,
|
|
131
|
+
song_key=song_key,
|
|
132
|
+
session_tempo=session_tempo,
|
|
133
|
+
existing_roles=existing_roles,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Build both plans
|
|
137
|
+
surgeon_plan = compile_sample_plan(
|
|
138
|
+
profile,
|
|
139
|
+
SampleIntent(intent_type=intent, philosophy="surgeon", description=""),
|
|
140
|
+
)
|
|
141
|
+
alchemist_plan = compile_sample_plan(
|
|
142
|
+
profile,
|
|
143
|
+
SampleIntent(intent_type=intent, philosophy="alchemist", description=""),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
report = SampleFitReport(
|
|
147
|
+
sample=profile,
|
|
148
|
+
critics=critics,
|
|
149
|
+
recommended_intent=intent,
|
|
150
|
+
surgeon_plan=surgeon_plan,
|
|
151
|
+
alchemist_plan=alchemist_plan,
|
|
152
|
+
warnings=[c.recommendation for c in critics.values() if c.score < 0.5],
|
|
153
|
+
)
|
|
154
|
+
return report.to_dict()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@mcp.tool()
|
|
158
|
+
def search_samples(
|
|
159
|
+
ctx: Context,
|
|
160
|
+
query: str,
|
|
161
|
+
material_type: Optional[str] = None,
|
|
162
|
+
key: Optional[str] = None,
|
|
163
|
+
bpm_range: Optional[str] = None,
|
|
164
|
+
source: Optional[str] = None,
|
|
165
|
+
max_results: int = 10,
|
|
166
|
+
) -> dict:
|
|
167
|
+
"""Search for samples across Splice library, Ableton browser, and local filesystem.
|
|
168
|
+
|
|
169
|
+
Searches all enabled sources in parallel and ranks results.
|
|
170
|
+
Splice results include rich metadata (key, BPM, genre, tags, pack info).
|
|
171
|
+
|
|
172
|
+
query: search text like "dark vocal", "breakbeat", "foley metal"
|
|
173
|
+
material_type: filter by type (vocal, drum_loop, texture, etc.)
|
|
174
|
+
key: prefer samples in this key (e.g., "Cm", "F#")
|
|
175
|
+
bpm_range: "min-max" BPM range (e.g., "120-130")
|
|
176
|
+
source: "splice", "browser", "filesystem", or None for all
|
|
177
|
+
max_results: maximum results to return (default 10)
|
|
178
|
+
"""
|
|
179
|
+
results: list[dict] = []
|
|
180
|
+
|
|
181
|
+
# Parse BPM range
|
|
182
|
+
bpm_min, bpm_max = None, None
|
|
183
|
+
if bpm_range:
|
|
184
|
+
parts = bpm_range.replace(" ", "").split("-")
|
|
185
|
+
if len(parts) == 2:
|
|
186
|
+
try:
|
|
187
|
+
bpm_min, bpm_max = float(parts[0]), float(parts[1])
|
|
188
|
+
except ValueError:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
# Splice search (richest metadata, searched first)
|
|
192
|
+
if source in (None, "splice"):
|
|
193
|
+
splice = SpliceSource()
|
|
194
|
+
if splice.enabled:
|
|
195
|
+
splice_results = splice.search(
|
|
196
|
+
query=query,
|
|
197
|
+
max_results=max_results,
|
|
198
|
+
key=key,
|
|
199
|
+
bpm_min=bpm_min,
|
|
200
|
+
bpm_max=bpm_max,
|
|
201
|
+
)
|
|
202
|
+
for candidate in splice_results:
|
|
203
|
+
d = candidate.to_dict()
|
|
204
|
+
d["source_priority"] = 1 # highest
|
|
205
|
+
results.append(d)
|
|
206
|
+
|
|
207
|
+
# Browser search
|
|
208
|
+
if source in (None, "browser"):
|
|
209
|
+
try:
|
|
210
|
+
ableton = ctx.lifespan_context["ableton"]
|
|
211
|
+
browser = BrowserSource()
|
|
212
|
+
for category in browser.DEFAULT_CATEGORIES:
|
|
213
|
+
try:
|
|
214
|
+
search_result = ableton.send_command("search_browser", {
|
|
215
|
+
"path": category,
|
|
216
|
+
"name_filter": query,
|
|
217
|
+
"loadable_only": True,
|
|
218
|
+
"max_results": max_results,
|
|
219
|
+
})
|
|
220
|
+
raw = search_result.get("results", [])
|
|
221
|
+
parsed = browser.parse_results(raw, category)
|
|
222
|
+
for candidate in parsed:
|
|
223
|
+
d = candidate.to_dict()
|
|
224
|
+
d["source_priority"] = 2
|
|
225
|
+
results.append(d)
|
|
226
|
+
except Exception:
|
|
227
|
+
continue
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
# Filesystem search
|
|
232
|
+
if source in (None, "filesystem"):
|
|
233
|
+
fs = FilesystemSource(scan_paths=[
|
|
234
|
+
"~/Music", "~/Documents/Samples",
|
|
235
|
+
"~/Documents/LivePilot/downloads",
|
|
236
|
+
])
|
|
237
|
+
fs_results = fs.search(query, max_results=max_results)
|
|
238
|
+
for candidate in fs_results:
|
|
239
|
+
d = candidate.to_dict()
|
|
240
|
+
d["source_priority"] = 3
|
|
241
|
+
results.append(d)
|
|
242
|
+
|
|
243
|
+
# Sort by source priority (Splice first), then by relevance
|
|
244
|
+
results.sort(key=lambda r: r.get("source_priority", 9))
|
|
245
|
+
|
|
246
|
+
# Build summary
|
|
247
|
+
source_counts = {}
|
|
248
|
+
for r in results:
|
|
249
|
+
src = r.get("source", "unknown")
|
|
250
|
+
source_counts[src] = source_counts.get(src, 0) + 1
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
"query": query,
|
|
254
|
+
"result_count": len(results[:max_results]),
|
|
255
|
+
"source_counts": source_counts,
|
|
256
|
+
"results": results[:max_results],
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@mcp.tool()
|
|
261
|
+
def suggest_sample_technique(
|
|
262
|
+
ctx: Context,
|
|
263
|
+
file_path: str,
|
|
264
|
+
intent: str = "rhythm",
|
|
265
|
+
philosophy: str = "auto",
|
|
266
|
+
max_suggestions: int = 3,
|
|
267
|
+
) -> dict:
|
|
268
|
+
"""Suggest sample manipulation techniques from the technique library.
|
|
269
|
+
|
|
270
|
+
Returns ranked techniques with executable step outlines for the
|
|
271
|
+
given sample + intent combination.
|
|
272
|
+
|
|
273
|
+
file_path: path to the sample
|
|
274
|
+
intent: rhythm, texture, layer, melody, vocal, atmosphere, transform, challenge
|
|
275
|
+
philosophy: surgeon, alchemist, auto
|
|
276
|
+
"""
|
|
277
|
+
profile = build_profile_from_filename(file_path)
|
|
278
|
+
sample_intent = SampleIntent(
|
|
279
|
+
intent_type=intent, philosophy=philosophy, description="",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
candidates = find_techniques(
|
|
283
|
+
material_type=profile.material_type,
|
|
284
|
+
intent=intent,
|
|
285
|
+
philosophy=philosophy if philosophy != "auto" else None,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if not candidates:
|
|
289
|
+
candidates = find_techniques(intent=intent)
|
|
290
|
+
|
|
291
|
+
suggestions = []
|
|
292
|
+
for t in candidates[:max_suggestions]:
|
|
293
|
+
steps = compile_sample_plan(profile, sample_intent, technique=t)
|
|
294
|
+
suggestions.append({
|
|
295
|
+
"technique_id": t.technique_id,
|
|
296
|
+
"name": t.name,
|
|
297
|
+
"philosophy": t.philosophy,
|
|
298
|
+
"difficulty": t.difficulty,
|
|
299
|
+
"description": t.description,
|
|
300
|
+
"inspiration": t.inspiration,
|
|
301
|
+
"step_count": len(steps),
|
|
302
|
+
"steps_preview": [s["description"] for s in steps[:5]],
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
"sample": profile.name,
|
|
307
|
+
"material_type": profile.material_type,
|
|
308
|
+
"intent": intent,
|
|
309
|
+
"suggestion_count": len(suggestions),
|
|
310
|
+
"suggestions": suggestions,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@mcp.tool()
|
|
315
|
+
def plan_sample_workflow(
|
|
316
|
+
ctx: Context,
|
|
317
|
+
file_path: Optional[str] = None,
|
|
318
|
+
search_query: Optional[str] = None,
|
|
319
|
+
intent: str = "rhythm",
|
|
320
|
+
philosophy: str = "auto",
|
|
321
|
+
target_track: Optional[int] = None,
|
|
322
|
+
section_type: Optional[str] = None,
|
|
323
|
+
desired_role: Optional[str] = None,
|
|
324
|
+
) -> dict:
|
|
325
|
+
"""Full end-to-end sample workflow: analyze, critique, select technique, compile plan.
|
|
326
|
+
|
|
327
|
+
Provide file_path for a known sample, or search_query to find one.
|
|
328
|
+
Returns a complete compiled plan ready for execution.
|
|
329
|
+
|
|
330
|
+
intent: rhythm, texture, layer, melody, vocal, atmosphere, transform
|
|
331
|
+
philosophy: surgeon, alchemist, auto
|
|
332
|
+
target_track: existing track index, or None for new track
|
|
333
|
+
section_type: optional section context (intro, verse, chorus, drop, etc.)
|
|
334
|
+
desired_role: optional sample role (hook_sample, texture_bed, break_layer, etc.)
|
|
335
|
+
"""
|
|
336
|
+
if file_path is None and search_query is None:
|
|
337
|
+
return {"error": "Provide either file_path or search_query"}
|
|
338
|
+
|
|
339
|
+
profile = None
|
|
340
|
+
if file_path:
|
|
341
|
+
profile = build_profile_from_filename(file_path)
|
|
342
|
+
|
|
343
|
+
sample_intent = SampleIntent(
|
|
344
|
+
intent_type=intent, philosophy=philosophy,
|
|
345
|
+
description=search_query or f"Process {file_path} for {intent}",
|
|
346
|
+
target_track=target_track,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
if profile is None:
|
|
350
|
+
# No file yet — return search guidance
|
|
351
|
+
queries = build_search_queries(search_query or "", material_type=None)
|
|
352
|
+
return {
|
|
353
|
+
"status": "search_needed",
|
|
354
|
+
"search_queries": queries,
|
|
355
|
+
"intent": intent,
|
|
356
|
+
"note": "Use search_samples to find a sample, then call again with file_path",
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
technique = select_technique(profile, sample_intent)
|
|
360
|
+
plan = compile_sample_plan(profile, sample_intent, target_track=target_track,
|
|
361
|
+
technique=technique)
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
"sample": profile.to_dict(),
|
|
365
|
+
"intent": intent,
|
|
366
|
+
"philosophy": philosophy,
|
|
367
|
+
"technique": technique.name if technique else "fallback",
|
|
368
|
+
"technique_id": technique.technique_id if technique else "",
|
|
369
|
+
"step_count": len(plan),
|
|
370
|
+
"compiled_plan": plan,
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@mcp.tool()
|
|
375
|
+
def get_sample_opportunities(ctx: Context) -> dict:
|
|
376
|
+
"""Analyze current song and identify where samples could improve it.
|
|
377
|
+
|
|
378
|
+
Returns opportunities with suggested material types and techniques.
|
|
379
|
+
Used by Wonder Mode diagnosis for sample-aware creative rescue.
|
|
380
|
+
"""
|
|
381
|
+
opportunities: list[dict] = []
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
ableton = ctx.lifespan_context["ableton"]
|
|
385
|
+
info = ableton.send_command("get_session_info", {})
|
|
386
|
+
except Exception:
|
|
387
|
+
return {"opportunities": [], "note": "Cannot read session — Ableton not connected"}
|
|
388
|
+
|
|
389
|
+
track_count = info.get("track_count", 0)
|
|
390
|
+
track_names: list[str] = []
|
|
391
|
+
has_sampler = False
|
|
392
|
+
|
|
393
|
+
for i in range(min(track_count, 16)):
|
|
394
|
+
try:
|
|
395
|
+
track_info = ableton.send_command("get_track_info", {"track_index": i})
|
|
396
|
+
name = track_info.get("name", "").lower()
|
|
397
|
+
track_names.append(name)
|
|
398
|
+
devices = track_info.get("devices", [])
|
|
399
|
+
for d in devices:
|
|
400
|
+
if d.get("class_name") in ("OriginalSimpler", "MultiSampler"):
|
|
401
|
+
has_sampler = True
|
|
402
|
+
except Exception:
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
# No organic texture
|
|
406
|
+
has_organic = any(
|
|
407
|
+
kw in name for name in track_names
|
|
408
|
+
for kw in ("vocal", "sample", "foley", "field", "organic", "found")
|
|
409
|
+
)
|
|
410
|
+
if not has_organic and track_count >= 3:
|
|
411
|
+
opportunities.append({
|
|
412
|
+
"type": "no_organic_texture",
|
|
413
|
+
"description": "No organic/sampled textures — all tracks appear synthesized",
|
|
414
|
+
"suggested_material": ["vocal", "foley", "texture"],
|
|
415
|
+
"suggested_techniques": ["vocal_chop_rhythm", "phone_recording_texture", "tail_harvest"],
|
|
416
|
+
"confidence": 0.6,
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
# Limited drum variety
|
|
420
|
+
drum_tracks = [n for n in track_names if any(
|
|
421
|
+
kw in n for kw in ("drum", "beat", "perc", "kick", "snare")
|
|
422
|
+
)]
|
|
423
|
+
if len(drum_tracks) <= 1 and track_count >= 4:
|
|
424
|
+
opportunities.append({
|
|
425
|
+
"type": "drum_variety_needed",
|
|
426
|
+
"description": "Limited percussion variety — layer a break or add ghost notes",
|
|
427
|
+
"suggested_material": ["drum_loop"],
|
|
428
|
+
"suggested_techniques": ["break_layering", "ghost_note_texture"],
|
|
429
|
+
"confidence": 0.5,
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
# No Simpler/Sampler devices
|
|
433
|
+
if not has_sampler and track_count >= 2:
|
|
434
|
+
opportunities.append({
|
|
435
|
+
"type": "no_sample_instruments",
|
|
436
|
+
"description": "No Simpler/Sampler devices — samples could add character",
|
|
437
|
+
"suggested_material": ["vocal", "instrument_loop", "one_shot"],
|
|
438
|
+
"suggested_techniques": ["syllable_instrument", "slice_and_sequence"],
|
|
439
|
+
"confidence": 0.4,
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
"opportunity_count": len(opportunities),
|
|
444
|
+
"opportunities": opportunities,
|
|
445
|
+
"track_count": track_count,
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@mcp.tool()
|
|
450
|
+
def plan_slice_workflow(
|
|
451
|
+
ctx: Context,
|
|
452
|
+
file_path: Optional[str] = None,
|
|
453
|
+
track_index: Optional[int] = None,
|
|
454
|
+
device_index: int = 0,
|
|
455
|
+
intent: str = "rhythm",
|
|
456
|
+
target_section: Optional[str] = None,
|
|
457
|
+
target_track: Optional[int] = None,
|
|
458
|
+
bars: int = 4,
|
|
459
|
+
style_hint: str = "",
|
|
460
|
+
) -> dict:
|
|
461
|
+
"""Plan an end-to-end slice workflow for a sample.
|
|
462
|
+
|
|
463
|
+
Generates a Simpler slice strategy, MIDI note mapping, and starter
|
|
464
|
+
pattern based on musical intent. Returns a compiled workflow plan —
|
|
465
|
+
does NOT execute. The agent steps through each tool call in sequence.
|
|
466
|
+
|
|
467
|
+
Provide either file_path (new sample to load) or track_index +
|
|
468
|
+
device_index (existing Simpler with loaded sample).
|
|
469
|
+
|
|
470
|
+
intent: rhythm | hook | texture | percussion | melodic
|
|
471
|
+
bars: number of bars for the pattern (default 4)
|
|
472
|
+
target_section: optional section name for arrangement hints
|
|
473
|
+
style_hint: optional genre/style context (e.g. "dilla", "burial")
|
|
474
|
+
"""
|
|
475
|
+
from .slice_workflow import plan_slice_steps
|
|
476
|
+
|
|
477
|
+
# Determine slice count — default 8 for file-based, or would come from
|
|
478
|
+
# get_simpler_slices in a real execution
|
|
479
|
+
# Read tempo from session if connected, otherwise default
|
|
480
|
+
tempo = 120.0
|
|
481
|
+
try:
|
|
482
|
+
ableton = ctx.lifespan_context.get("ableton")
|
|
483
|
+
if ableton:
|
|
484
|
+
info = ableton.send_command("get_session_info", {})
|
|
485
|
+
tempo = float(info.get("tempo", 120.0))
|
|
486
|
+
except Exception:
|
|
487
|
+
pass
|
|
488
|
+
|
|
489
|
+
# Read slice count from existing Simpler if track provided
|
|
490
|
+
slice_count = 8 # Default transient slice count
|
|
491
|
+
if track_index is not None:
|
|
492
|
+
try:
|
|
493
|
+
ableton = ctx.lifespan_context.get("ableton")
|
|
494
|
+
if ableton:
|
|
495
|
+
slices = ableton.send_command("get_simpler_slices", {
|
|
496
|
+
"track_index": track_index, "device_index": device_index,
|
|
497
|
+
})
|
|
498
|
+
if isinstance(slices, dict) and slices.get("slice_count"):
|
|
499
|
+
slice_count = slices["slice_count"]
|
|
500
|
+
except Exception:
|
|
501
|
+
pass # Fall back to default
|
|
502
|
+
|
|
503
|
+
# Build the plan
|
|
504
|
+
plan = plan_slice_steps(
|
|
505
|
+
slice_count=slice_count,
|
|
506
|
+
intent=intent,
|
|
507
|
+
bars=bars,
|
|
508
|
+
tempo=tempo,
|
|
509
|
+
track_index=target_track if target_track is not None else 0,
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# Prepend sample loading steps if file_path provided
|
|
513
|
+
if file_path:
|
|
514
|
+
load_steps = [
|
|
515
|
+
{
|
|
516
|
+
"tool": "create_midi_track",
|
|
517
|
+
"params": {"name": f"Slice {intent.title()}"},
|
|
518
|
+
"description": "Create track for sliced sample",
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
"tool": "load_sample_to_simpler",
|
|
522
|
+
"params": {"track_index": target_track or 0, "file_path": file_path},
|
|
523
|
+
"description": f"Load sample into Simpler: {file_path}",
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
"tool": "set_simpler_playback_mode",
|
|
527
|
+
"params": {"track_index": target_track or 0, "device_index": 0, "playback_mode": 2},
|
|
528
|
+
"description": "Set Simpler to Slice mode",
|
|
529
|
+
},
|
|
530
|
+
]
|
|
531
|
+
plan["steps"] = load_steps + plan["steps"]
|
|
532
|
+
|
|
533
|
+
# Add arrangement hints if section provided
|
|
534
|
+
if target_section:
|
|
535
|
+
plan["arrangement_hints"] = {
|
|
536
|
+
"target_section": target_section,
|
|
537
|
+
"suggested_placement": f"Place slice pattern in {target_section}",
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
plan["file_path"] = file_path
|
|
541
|
+
plan["track_index"] = track_index
|
|
542
|
+
plan["device_index"] = device_index
|
|
543
|
+
plan["style_hint"] = style_hint
|
|
544
|
+
|
|
545
|
+
return plan
|
|
@@ -5,9 +5,12 @@ from . import mix_moves # noqa: F401
|
|
|
5
5
|
from . import transition_moves # noqa: F401
|
|
6
6
|
from . import sound_design_moves # noqa: F401
|
|
7
7
|
from . import performance_moves # noqa: F401
|
|
8
|
+
from . import device_creation_moves # noqa: F401
|
|
9
|
+
from ..sample_engine import moves as sample_moves # noqa: F401
|
|
8
10
|
|
|
9
11
|
# Import compilers to auto-register them
|
|
10
12
|
from . import mix_compilers # noqa: F401
|
|
11
13
|
from . import transition_compilers # noqa: F401
|
|
12
14
|
from . import sound_design_compilers # noqa: F401
|
|
13
15
|
from . import performance_compilers # noqa: F401
|
|
16
|
+
from . import sample_compilers # noqa: F401
|