livepilot 1.9.21 → 1.9.23
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 +2 -2
- package/CHANGELOG.md +47 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +47 -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 -14
- package/livepilot/commands/beat.md +68 -21
- package/livepilot/commands/evaluate.md +23 -13
- package/livepilot/commands/mix.md +35 -11
- package/livepilot/commands/perform.md +31 -19
- package/livepilot/commands/sounddesign.md +38 -17
- 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 +15 -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/m4l_device/livepilot_bridge.js +1 -1
- 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 +342 -0
- package/mcp_server/hook_hunter/models.py +57 -0
- package/mcp_server/hook_hunter/tools.py +586 -0
- package/mcp_server/memory/taste_graph.py +261 -0
- package/mcp_server/memory/tools.py +88 -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 +421 -0
- package/mcp_server/musical_intelligence/phrase_critic.py +163 -0
- package/mcp_server/musical_intelligence/tools.py +221 -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 +73 -0
- package/mcp_server/preview_studio/tools.py +423 -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 +204 -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/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 +235 -0
- package/mcp_server/song_brain/__init__.py +6 -0
- package/mcp_server/song_brain/builder.py +477 -0
- package/mcp_server/song_brain/models.py +132 -0
- package/mcp_server/song_brain/tools.py +294 -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 +285 -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
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Compilers for sound-design-domain semantic moves.
|
|
2
|
+
|
|
3
|
+
These prefer native Ableton devices. Volume/send adjustments are used
|
|
4
|
+
as safe fallbacks when device chain details aren't in the kernel.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .compiler import CompiledPlan, CompiledStep, register_compiler
|
|
10
|
+
from .models import SemanticMove
|
|
11
|
+
from . import resolvers
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _compile_add_warmth(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
15
|
+
"""Compile 'add_warmth': volume boost + reverb send for perceived warmth.
|
|
16
|
+
|
|
17
|
+
SAFETY: Never blindly set device parameters — device_index=0, parameter_index=0
|
|
18
|
+
can kill audio if the first device isn't a Saturator. Only adjust device params
|
|
19
|
+
when find_device_on_track confirms a Saturator is present.
|
|
20
|
+
"""
|
|
21
|
+
steps = []
|
|
22
|
+
descriptions = []
|
|
23
|
+
warnings = []
|
|
24
|
+
|
|
25
|
+
# Target melodic or bass tracks for warmth
|
|
26
|
+
targets = resolvers.find_tracks_by_role(kernel, ["bass", "chords", "pad"])
|
|
27
|
+
if not targets:
|
|
28
|
+
targets = resolvers.find_tracks_by_role(kernel, ["lead"])
|
|
29
|
+
|
|
30
|
+
for t in targets[:2]:
|
|
31
|
+
idx = t["index"]
|
|
32
|
+
name = t["name"]
|
|
33
|
+
|
|
34
|
+
# Try to find a Saturator on the track (safe device adjustment)
|
|
35
|
+
saturator = resolvers.find_device_on_track(kernel, idx, "Saturator")
|
|
36
|
+
if saturator:
|
|
37
|
+
steps.append(CompiledStep(
|
|
38
|
+
tool="set_device_parameter",
|
|
39
|
+
params={
|
|
40
|
+
"track_index": idx,
|
|
41
|
+
"device_index": saturator["device_index"],
|
|
42
|
+
"parameter_index": 0,
|
|
43
|
+
"value": 0.3,
|
|
44
|
+
},
|
|
45
|
+
description=f"Gentle Saturator drive on {name}",
|
|
46
|
+
))
|
|
47
|
+
descriptions.append(f"Saturate {name}")
|
|
48
|
+
else:
|
|
49
|
+
# No Saturator found — use volume + send instead of risky device params
|
|
50
|
+
warnings.append(f"No Saturator on {name} — using volume+reverb for warmth")
|
|
51
|
+
|
|
52
|
+
# Boost volume slightly for perceived warmth
|
|
53
|
+
steps.append(CompiledStep(
|
|
54
|
+
tool="set_track_volume",
|
|
55
|
+
params={"track_index": idx, "volume": 0.65},
|
|
56
|
+
description=f"Boost {name} slightly for warmth",
|
|
57
|
+
))
|
|
58
|
+
|
|
59
|
+
# Add reverb send for depth/warmth perception
|
|
60
|
+
steps.append(CompiledStep(
|
|
61
|
+
tool="set_track_send",
|
|
62
|
+
params={"track_index": idx, "send_index": 0, "value": 0.25},
|
|
63
|
+
description=f"Add reverb warmth to {name}",
|
|
64
|
+
))
|
|
65
|
+
descriptions.append(f"Warm {name}")
|
|
66
|
+
|
|
67
|
+
steps.append(CompiledStep(
|
|
68
|
+
tool="get_track_meters",
|
|
69
|
+
params={"include_stereo": True},
|
|
70
|
+
description="Verify warmth — tracks producing audio, no distortion",
|
|
71
|
+
))
|
|
72
|
+
|
|
73
|
+
return CompiledPlan(
|
|
74
|
+
move_id=move.move_id,
|
|
75
|
+
intent=move.intent,
|
|
76
|
+
steps=steps,
|
|
77
|
+
before_reads=[{"tool": "get_master_spectrum", "params": {}}],
|
|
78
|
+
after_reads=[{"tool": "get_master_spectrum", "params": {}}],
|
|
79
|
+
risk_level="low",
|
|
80
|
+
summary="; ".join(descriptions) if descriptions else "No tracks for warmth",
|
|
81
|
+
requires_approval=(kernel.get("mode", "improve") != "explore"),
|
|
82
|
+
warnings=warnings,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _compile_add_texture(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
87
|
+
"""Compile 'add_texture': perlin filter motion + delay send."""
|
|
88
|
+
steps = []
|
|
89
|
+
descriptions = []
|
|
90
|
+
|
|
91
|
+
targets = resolvers.find_tracks_by_role(kernel, ["pad", "chords", "lead"])
|
|
92
|
+
|
|
93
|
+
for t in targets[:1]:
|
|
94
|
+
idx = t["index"]
|
|
95
|
+
name = t["name"]
|
|
96
|
+
steps.append(CompiledStep(
|
|
97
|
+
tool="apply_automation_shape",
|
|
98
|
+
params={
|
|
99
|
+
"track_index": idx,
|
|
100
|
+
"clip_index": 0,
|
|
101
|
+
"parameter_type": "device",
|
|
102
|
+
"device_index": 0,
|
|
103
|
+
"parameter_index": 0,
|
|
104
|
+
"curve_type": "perlin",
|
|
105
|
+
"center": 0.4,
|
|
106
|
+
"amplitude": 0.2,
|
|
107
|
+
"duration": 8,
|
|
108
|
+
"density": 16,
|
|
109
|
+
},
|
|
110
|
+
description=f"Perlin filter motion on {name} for organic texture",
|
|
111
|
+
))
|
|
112
|
+
descriptions.append(f"Perlin filter on {name}")
|
|
113
|
+
|
|
114
|
+
# Add delay send
|
|
115
|
+
steps.append(CompiledStep(
|
|
116
|
+
tool="set_track_send",
|
|
117
|
+
params={"track_index": idx, "send_index": 1, "value": 0.20},
|
|
118
|
+
description=f"Add delay send on {name} for spatial texture",
|
|
119
|
+
))
|
|
120
|
+
descriptions.append(f"Delay texture on {name}")
|
|
121
|
+
|
|
122
|
+
steps.append(CompiledStep(
|
|
123
|
+
tool="get_track_meters",
|
|
124
|
+
params={"include_stereo": True},
|
|
125
|
+
description="Verify texture — track active with variation",
|
|
126
|
+
))
|
|
127
|
+
|
|
128
|
+
return CompiledPlan(
|
|
129
|
+
move_id=move.move_id,
|
|
130
|
+
intent=move.intent,
|
|
131
|
+
steps=steps,
|
|
132
|
+
risk_level="medium",
|
|
133
|
+
summary="; ".join(descriptions) if descriptions else "No tracks for texture",
|
|
134
|
+
requires_approval=(kernel.get("mode", "improve") != "explore"),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _compile_shape_transients(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
139
|
+
"""Compile 'shape_transients': push drum volume for punch, adjust sends.
|
|
140
|
+
|
|
141
|
+
SAFETY: Never blindly set device parameters. Only adjust Compressor params
|
|
142
|
+
when find_device_on_track confirms one exists. Otherwise use volume for punch.
|
|
143
|
+
"""
|
|
144
|
+
steps = []
|
|
145
|
+
descriptions = []
|
|
146
|
+
warnings = []
|
|
147
|
+
|
|
148
|
+
drums = resolvers.find_tracks_by_role(kernel, ["drums", "percussion"])
|
|
149
|
+
if not drums:
|
|
150
|
+
return CompiledPlan(
|
|
151
|
+
move_id=move.move_id,
|
|
152
|
+
intent=move.intent,
|
|
153
|
+
summary="No drum/percussion tracks found",
|
|
154
|
+
warnings=["No rhythm tracks for transient shaping"],
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
for dt in drums[:1]:
|
|
158
|
+
idx = dt["index"]
|
|
159
|
+
name = dt["name"]
|
|
160
|
+
|
|
161
|
+
# Try to find a Compressor on the track
|
|
162
|
+
compressor = resolvers.find_device_on_track(kernel, idx, "Compressor")
|
|
163
|
+
if compressor:
|
|
164
|
+
steps.append(CompiledStep(
|
|
165
|
+
tool="set_device_parameter",
|
|
166
|
+
params={
|
|
167
|
+
"track_index": idx,
|
|
168
|
+
"device_index": compressor["device_index"],
|
|
169
|
+
"parameter_index": 0,
|
|
170
|
+
"value": 0.2,
|
|
171
|
+
},
|
|
172
|
+
description=f"Faster Compressor attack on {name} for snap",
|
|
173
|
+
))
|
|
174
|
+
descriptions.append(f"Shape {name} compressor")
|
|
175
|
+
else:
|
|
176
|
+
warnings.append(f"No Compressor on {name} — using volume push for punch")
|
|
177
|
+
|
|
178
|
+
# Push volume for transient punch regardless
|
|
179
|
+
steps.append(CompiledStep(
|
|
180
|
+
tool="set_track_volume",
|
|
181
|
+
params={"track_index": idx, "volume": 0.75},
|
|
182
|
+
description=f"Push {name} to 0.75 for transient punch",
|
|
183
|
+
))
|
|
184
|
+
descriptions.append(f"Push {name} for punch")
|
|
185
|
+
|
|
186
|
+
# Reduce reverb send to tighten transients
|
|
187
|
+
steps.append(CompiledStep(
|
|
188
|
+
tool="set_track_send",
|
|
189
|
+
params={"track_index": idx, "send_index": 0, "value": 0.10},
|
|
190
|
+
description=f"Tighten reverb on {name} for cleaner transients",
|
|
191
|
+
))
|
|
192
|
+
|
|
193
|
+
steps.append(CompiledStep(
|
|
194
|
+
tool="get_track_meters",
|
|
195
|
+
params={"include_stereo": True},
|
|
196
|
+
description="Verify transient character after shaping",
|
|
197
|
+
))
|
|
198
|
+
|
|
199
|
+
return CompiledPlan(
|
|
200
|
+
move_id=move.move_id,
|
|
201
|
+
intent=move.intent,
|
|
202
|
+
steps=steps,
|
|
203
|
+
risk_level="low",
|
|
204
|
+
summary="; ".join(descriptions),
|
|
205
|
+
requires_approval=(kernel.get("mode", "improve") != "explore"),
|
|
206
|
+
warnings=warnings,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _compile_add_space(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
211
|
+
"""Compile 'add_space': reverb + delay + pan widening."""
|
|
212
|
+
steps = []
|
|
213
|
+
descriptions = []
|
|
214
|
+
|
|
215
|
+
targets = resolvers.find_tracks_by_role(kernel, ["chords", "lead", "pad"])
|
|
216
|
+
|
|
217
|
+
for t in targets[:2]:
|
|
218
|
+
idx = t["index"]
|
|
219
|
+
name = t["name"]
|
|
220
|
+
steps.append(CompiledStep(
|
|
221
|
+
tool="set_track_send",
|
|
222
|
+
params={"track_index": idx, "send_index": 0, "value": 0.30},
|
|
223
|
+
description=f"Add reverb depth to {name}",
|
|
224
|
+
))
|
|
225
|
+
descriptions.append(f"Reverb on {name}")
|
|
226
|
+
|
|
227
|
+
# Widen one element
|
|
228
|
+
for t in targets[:1]:
|
|
229
|
+
steps.append(CompiledStep(
|
|
230
|
+
tool="set_track_pan",
|
|
231
|
+
params={"track_index": t["index"], "pan": -0.20},
|
|
232
|
+
description=f"Pan {t['name']} slightly left for spatial width",
|
|
233
|
+
))
|
|
234
|
+
for t in targets[1:2]:
|
|
235
|
+
steps.append(CompiledStep(
|
|
236
|
+
tool="set_track_pan",
|
|
237
|
+
params={"track_index": t["index"], "pan": 0.20},
|
|
238
|
+
description=f"Pan {t['name']} slightly right for spatial width",
|
|
239
|
+
))
|
|
240
|
+
|
|
241
|
+
descriptions.append("Widen spatial field")
|
|
242
|
+
|
|
243
|
+
steps.append(CompiledStep(
|
|
244
|
+
tool="get_track_meters",
|
|
245
|
+
params={"include_stereo": True},
|
|
246
|
+
description="Verify spatial depth — stereo present, no phase issues",
|
|
247
|
+
))
|
|
248
|
+
|
|
249
|
+
return CompiledPlan(
|
|
250
|
+
move_id=move.move_id,
|
|
251
|
+
intent=move.intent,
|
|
252
|
+
steps=steps,
|
|
253
|
+
before_reads=[{"tool": "analyze_mix", "params": {}}],
|
|
254
|
+
after_reads=[{"tool": "analyze_mix", "params": {}}],
|
|
255
|
+
risk_level="low",
|
|
256
|
+
summary="; ".join(descriptions) if descriptions else "No tracks for space",
|
|
257
|
+
requires_approval=(kernel.get("mode", "improve") != "explore"),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ── Register ────────────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
register_compiler("add_warmth", _compile_add_warmth)
|
|
264
|
+
register_compiler("add_texture", _compile_add_texture)
|
|
265
|
+
register_compiler("shape_transients", _compile_shape_transients)
|
|
266
|
+
register_compiler("add_space", _compile_add_space)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Sound-design-domain semantic moves — musical intents for timbre and texture.
|
|
2
|
+
|
|
3
|
+
These moves prefer native Ableton devices over third-party plugins.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .models import SemanticMove
|
|
7
|
+
from .registry import register
|
|
8
|
+
|
|
9
|
+
ADD_WARMTH = SemanticMove(
|
|
10
|
+
move_id="add_warmth",
|
|
11
|
+
family="sound_design",
|
|
12
|
+
intent="Add warmth to a track or the mix — gentle saturation and low-mid boost",
|
|
13
|
+
targets={"warmth": 0.5, "depth": 0.3, "cohesion": 0.2},
|
|
14
|
+
protect={"clarity": 0.6, "punch": 0.5},
|
|
15
|
+
risk_level="low",
|
|
16
|
+
compile_plan=[
|
|
17
|
+
{"tool": "set_device_parameter", "params": {"description": "Add Saturator drive +2-4dB for harmonic warmth"}, "description": "Add saturation"},
|
|
18
|
+
{"tool": "set_device_parameter", "params": {"description": "Boost EQ low-mid shelf +1-2dB"}, "description": "Low-mid warmth"},
|
|
19
|
+
],
|
|
20
|
+
verification_plan=[
|
|
21
|
+
{"tool": "get_master_spectrum", "check": "low-mid energy increased, high-mid stable"},
|
|
22
|
+
{"tool": "get_track_meters", "check": "target track producing audio"},
|
|
23
|
+
],
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
ADD_TEXTURE = SemanticMove(
|
|
27
|
+
move_id="add_texture",
|
|
28
|
+
family="sound_design",
|
|
29
|
+
intent="Add texture and movement to a static sound — modulation and grain",
|
|
30
|
+
targets={"motion": 0.4, "novelty": 0.3, "depth": 0.3},
|
|
31
|
+
protect={"clarity": 0.6},
|
|
32
|
+
risk_level="medium",
|
|
33
|
+
compile_plan=[
|
|
34
|
+
{"tool": "apply_automation_shape", "params": {"curve_type": "perlin", "description": "Perlin noise on filter cutoff for organic texture"}, "description": "Organic filter motion"},
|
|
35
|
+
{"tool": "set_track_send", "params": {"description": "Increase delay send for spatial texture"}, "description": "Spatial texture via delay"},
|
|
36
|
+
],
|
|
37
|
+
verification_plan=[
|
|
38
|
+
{"tool": "get_track_meters", "check": "track producing audio with variation"},
|
|
39
|
+
],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
SHAPE_TRANSIENTS = SemanticMove(
|
|
43
|
+
move_id="shape_transients",
|
|
44
|
+
family="sound_design",
|
|
45
|
+
intent="Shape transient character — sharpen or soften attack for rhythmic clarity",
|
|
46
|
+
targets={"punch": 0.5, "clarity": 0.3, "groove": 0.2},
|
|
47
|
+
protect={"warmth": 0.5},
|
|
48
|
+
risk_level="low",
|
|
49
|
+
compile_plan=[
|
|
50
|
+
{"tool": "set_device_parameter", "params": {"description": "Adjust Compressor attack time (faster = sharper transients, slower = rounder)"}, "description": "Shape attack"},
|
|
51
|
+
{"tool": "set_device_parameter", "params": {"description": "Adjust Compressor release for rhythmic pumping"}, "description": "Shape release"},
|
|
52
|
+
],
|
|
53
|
+
verification_plan=[
|
|
54
|
+
{"tool": "get_track_meters", "check": "track producing audio with expected transient character"},
|
|
55
|
+
],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
ADD_SPACE = SemanticMove(
|
|
59
|
+
move_id="add_space",
|
|
60
|
+
family="sound_design",
|
|
61
|
+
intent="Add spatial depth — reverb, delay, and stereo enhancement without muddying",
|
|
62
|
+
targets={"depth": 0.5, "width": 0.3, "clarity": 0.2},
|
|
63
|
+
protect={"punch": 0.6, "clarity": 0.5},
|
|
64
|
+
risk_level="low",
|
|
65
|
+
compile_plan=[
|
|
66
|
+
{"tool": "set_track_send", "params": {"description": "Increase reverb send to 25-35%"}, "description": "Add reverb depth"},
|
|
67
|
+
{"tool": "set_track_send", "params": {"description": "Add subtle delay send 10-15%"}, "description": "Add delay texture"},
|
|
68
|
+
{"tool": "set_track_pan", "params": {"description": "Widen pan slightly for spatial presence"}, "description": "Widen spatial field"},
|
|
69
|
+
],
|
|
70
|
+
verification_plan=[
|
|
71
|
+
{"tool": "get_track_meters", "check": "stereo output present, no phase cancellation"},
|
|
72
|
+
{"tool": "analyze_mix", "check": "stereo.mono_risk is false"},
|
|
73
|
+
],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Register all sound design moves
|
|
77
|
+
for _move in [ADD_WARMTH, ADD_TEXTURE, SHAPE_TRANSIENTS, ADD_SPACE]:
|
|
78
|
+
register(_move)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Semantic move MCP tools — propose, preview, and apply musical intents.
|
|
2
|
+
|
|
3
|
+
3 tools:
|
|
4
|
+
list_semantic_moves — discover available moves by domain
|
|
5
|
+
preview_semantic_move — see what a move will do before applying
|
|
6
|
+
propose_next_best_move — AI-ranked suggestions based on current session state
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from fastmcp import Context
|
|
14
|
+
|
|
15
|
+
from ..server import mcp
|
|
16
|
+
from . import registry
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@mcp.tool()
|
|
20
|
+
def list_semantic_moves(
|
|
21
|
+
ctx: Context,
|
|
22
|
+
domain: str = "",
|
|
23
|
+
style: str = "",
|
|
24
|
+
) -> dict:
|
|
25
|
+
"""List available semantic moves — high-level musical intents.
|
|
26
|
+
|
|
27
|
+
Semantic moves express WHAT to achieve musically, not HOW parametrically.
|
|
28
|
+
Each move compiles into a sequence of existing deterministic tools.
|
|
29
|
+
|
|
30
|
+
domain: filter by family (mix, arrangement, transition, sound_design, performance)
|
|
31
|
+
style: filter by genre/style (reserved for future use)
|
|
32
|
+
|
|
33
|
+
Returns: list of moves with move_id, family, intent, targets, risk_level.
|
|
34
|
+
"""
|
|
35
|
+
moves = registry.list_moves(domain=domain, style=style)
|
|
36
|
+
return {"moves": moves, "count": len(moves), "available_domains": ["mix", "arrangement"]}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@mcp.tool()
|
|
40
|
+
def preview_semantic_move(
|
|
41
|
+
ctx: Context,
|
|
42
|
+
move_id: str,
|
|
43
|
+
) -> dict:
|
|
44
|
+
"""Preview what a semantic move will do before applying it.
|
|
45
|
+
|
|
46
|
+
Returns the full compile plan (tool sequence), verification plan,
|
|
47
|
+
targets, protection constraints, and risk level. Use this to understand
|
|
48
|
+
the impact before committing.
|
|
49
|
+
"""
|
|
50
|
+
move = registry.get_move(move_id)
|
|
51
|
+
if not move:
|
|
52
|
+
available = [m["move_id"] for m in registry.list_moves()]
|
|
53
|
+
return {
|
|
54
|
+
"error": f"Unknown move_id: {move_id}",
|
|
55
|
+
"available_moves": available,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return move.to_full_dict()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@mcp.tool()
|
|
62
|
+
def propose_next_best_move(
|
|
63
|
+
ctx: Context,
|
|
64
|
+
request_text: str,
|
|
65
|
+
limit: int = 3,
|
|
66
|
+
) -> dict:
|
|
67
|
+
"""Propose the best semantic moves for a natural language request.
|
|
68
|
+
|
|
69
|
+
Analyzes the request text and ranks available semantic moves by
|
|
70
|
+
relevance. Returns up to `limit` suggestions with confidence scores.
|
|
71
|
+
|
|
72
|
+
request_text: what the user wants (e.g., "make this punchier",
|
|
73
|
+
"tighten the low end", "reduce repetition")
|
|
74
|
+
limit: max suggestions to return (default 3)
|
|
75
|
+
"""
|
|
76
|
+
if not request_text.strip():
|
|
77
|
+
return {"error": "request_text cannot be empty"}
|
|
78
|
+
|
|
79
|
+
# Simple keyword matching for now — will be replaced by conductor
|
|
80
|
+
# routing + taste ranking in V2 Step 7
|
|
81
|
+
request_lower = request_text.lower()
|
|
82
|
+
all_moves = list(registry._REGISTRY.values())
|
|
83
|
+
|
|
84
|
+
scored = []
|
|
85
|
+
for move in all_moves:
|
|
86
|
+
score = 0.0
|
|
87
|
+
# Match keywords from intent and move_id
|
|
88
|
+
intent_lower = move.intent.lower()
|
|
89
|
+
move_words = set(move.move_id.replace("_", " ").split())
|
|
90
|
+
intent_words = set(intent_lower.split())
|
|
91
|
+
request_words = set(request_lower.split())
|
|
92
|
+
|
|
93
|
+
# Word overlap scoring
|
|
94
|
+
overlap = request_words & (move_words | intent_words)
|
|
95
|
+
score += len(overlap) * 0.3
|
|
96
|
+
|
|
97
|
+
# Dimension matching
|
|
98
|
+
for dim in move.targets:
|
|
99
|
+
if dim in request_lower:
|
|
100
|
+
score += 0.2
|
|
101
|
+
|
|
102
|
+
# Boost exact intent matches
|
|
103
|
+
if move.move_id.replace("_", " ") in request_lower:
|
|
104
|
+
score += 1.0
|
|
105
|
+
|
|
106
|
+
if score > 0:
|
|
107
|
+
scored.append((move, min(score, 1.0)))
|
|
108
|
+
|
|
109
|
+
# Sort by score descending
|
|
110
|
+
scored.sort(key=lambda x: -x[1])
|
|
111
|
+
top = scored[:limit]
|
|
112
|
+
|
|
113
|
+
suggestions = []
|
|
114
|
+
for move, score in top:
|
|
115
|
+
d = move.to_dict()
|
|
116
|
+
d["match_score"] = round(score, 3)
|
|
117
|
+
suggestions.append(d)
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"request": request_text,
|
|
121
|
+
"suggestions": suggestions,
|
|
122
|
+
"count": len(suggestions),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@mcp.tool()
|
|
127
|
+
def apply_semantic_move(
|
|
128
|
+
ctx: Context,
|
|
129
|
+
move_id: str,
|
|
130
|
+
mode: str = "improve",
|
|
131
|
+
) -> dict:
|
|
132
|
+
"""Compile and optionally execute a semantic move against the current session.
|
|
133
|
+
|
|
134
|
+
Resolves the move's intent into concrete, parameterized tool calls based
|
|
135
|
+
on the current session topology (track names, roles, devices).
|
|
136
|
+
|
|
137
|
+
mode controls behavior:
|
|
138
|
+
- "improve" / "finish": compile and RETURN the plan for user approval.
|
|
139
|
+
The agent should present the steps and ask "Shall I do it?"
|
|
140
|
+
- "explore": compile and EXECUTE immediately, capturing before/after.
|
|
141
|
+
- "observe" / "diagnose": compile only, never execute. Return the plan.
|
|
142
|
+
|
|
143
|
+
Returns: CompiledPlan with concrete steps, summary, and execution status.
|
|
144
|
+
"""
|
|
145
|
+
from . import compiler
|
|
146
|
+
|
|
147
|
+
move = registry.get_move(move_id)
|
|
148
|
+
if not move:
|
|
149
|
+
return {"error": f"Unknown move_id: {move_id}"}
|
|
150
|
+
|
|
151
|
+
# Build a lightweight kernel from session info
|
|
152
|
+
ableton = ctx.lifespan_context["ableton"]
|
|
153
|
+
session_info = ableton.send_command("get_session_info")
|
|
154
|
+
kernel = {
|
|
155
|
+
"session_info": session_info,
|
|
156
|
+
"mode": mode,
|
|
157
|
+
"capability_state": {},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Compile the move
|
|
161
|
+
plan = compiler.compile(move, kernel)
|
|
162
|
+
|
|
163
|
+
if not plan.executable:
|
|
164
|
+
result = plan.to_dict()
|
|
165
|
+
result["executed"] = False
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
if mode in ("observe", "diagnose"):
|
|
169
|
+
result = plan.to_dict()
|
|
170
|
+
result["executed"] = False
|
|
171
|
+
result["note"] = f"Mode '{mode}' — plan compiled but not executed"
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
if mode in ("improve", "finish"):
|
|
175
|
+
result = plan.to_dict()
|
|
176
|
+
result["executed"] = False
|
|
177
|
+
result["note"] = "Awaiting approval — present the plan to the user, then execute steps individually"
|
|
178
|
+
return result
|
|
179
|
+
|
|
180
|
+
# explore mode — execute immediately
|
|
181
|
+
executed_steps = []
|
|
182
|
+
for step in plan.steps:
|
|
183
|
+
try:
|
|
184
|
+
tool_result = ableton.send_command(step.tool, step.params)
|
|
185
|
+
executed_steps.append({
|
|
186
|
+
"tool": step.tool,
|
|
187
|
+
"description": step.description,
|
|
188
|
+
"result": tool_result,
|
|
189
|
+
"ok": True,
|
|
190
|
+
})
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
executed_steps.append({
|
|
193
|
+
"tool": step.tool,
|
|
194
|
+
"description": step.description,
|
|
195
|
+
"error": str(exc),
|
|
196
|
+
"ok": False,
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
result = plan.to_dict()
|
|
200
|
+
result["executed"] = True
|
|
201
|
+
result["execution_results"] = executed_steps
|
|
202
|
+
result["success_count"] = sum(1 for s in executed_steps if s["ok"])
|
|
203
|
+
result["failure_count"] = sum(1 for s in executed_steps if not s["ok"])
|
|
204
|
+
return result
|