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.
Files changed (110) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.mcpbignore +40 -0
  3. package/AGENTS.md +2 -2
  4. package/CHANGELOG.md +47 -0
  5. package/CONTRIBUTING.md +1 -1
  6. package/README.md +47 -72
  7. package/bin/livepilot.js +135 -0
  8. package/livepilot/.Codex-plugin/plugin.json +2 -2
  9. package/livepilot/.claude-plugin/plugin.json +2 -2
  10. package/livepilot/agents/livepilot-producer/AGENT.md +13 -0
  11. package/livepilot/commands/arrange.md +42 -14
  12. package/livepilot/commands/beat.md +68 -21
  13. package/livepilot/commands/evaluate.md +23 -13
  14. package/livepilot/commands/mix.md +35 -11
  15. package/livepilot/commands/perform.md +31 -19
  16. package/livepilot/commands/sounddesign.md +38 -17
  17. package/livepilot/skills/livepilot-arrangement/SKILL.md +2 -1
  18. package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +2 -2
  19. package/livepilot/skills/livepilot-core/SKILL.md +60 -4
  20. package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +11 -11
  21. package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +25 -25
  22. package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +21 -21
  23. package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +13 -13
  24. package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +13 -13
  25. package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +5 -5
  26. package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +16 -16
  27. package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +40 -40
  28. package/livepilot/skills/livepilot-core/references/m4l-devices.md +3 -3
  29. package/livepilot/skills/livepilot-core/references/overview.md +4 -4
  30. package/livepilot/skills/livepilot-evaluation/SKILL.md +12 -8
  31. package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +2 -2
  32. package/livepilot/skills/livepilot-mix-engine/SKILL.md +1 -1
  33. package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +2 -2
  34. package/livepilot/skills/livepilot-mixing/SKILL.md +3 -1
  35. package/livepilot/skills/livepilot-notes/SKILL.md +2 -1
  36. package/livepilot/skills/livepilot-release/SKILL.md +15 -15
  37. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +2 -2
  38. package/livepilot/skills/livepilot-wonder/SKILL.md +62 -0
  39. package/livepilot.mcpb +0 -0
  40. package/m4l_device/livepilot_bridge.js +1 -1
  41. package/manifest.json +91 -0
  42. package/mcp_server/__init__.py +1 -1
  43. package/mcp_server/creative_constraints/__init__.py +6 -0
  44. package/mcp_server/creative_constraints/engine.py +277 -0
  45. package/mcp_server/creative_constraints/models.py +75 -0
  46. package/mcp_server/creative_constraints/tools.py +341 -0
  47. package/mcp_server/experiment/__init__.py +6 -0
  48. package/mcp_server/experiment/engine.py +213 -0
  49. package/mcp_server/experiment/models.py +120 -0
  50. package/mcp_server/experiment/tools.py +263 -0
  51. package/mcp_server/hook_hunter/__init__.py +5 -0
  52. package/mcp_server/hook_hunter/analyzer.py +342 -0
  53. package/mcp_server/hook_hunter/models.py +57 -0
  54. package/mcp_server/hook_hunter/tools.py +586 -0
  55. package/mcp_server/memory/taste_graph.py +261 -0
  56. package/mcp_server/memory/tools.py +88 -0
  57. package/mcp_server/mix_engine/critics.py +2 -2
  58. package/mcp_server/mix_engine/models.py +1 -1
  59. package/mcp_server/mix_engine/state_builder.py +2 -2
  60. package/mcp_server/musical_intelligence/__init__.py +8 -0
  61. package/mcp_server/musical_intelligence/detectors.py +421 -0
  62. package/mcp_server/musical_intelligence/phrase_critic.py +163 -0
  63. package/mcp_server/musical_intelligence/tools.py +221 -0
  64. package/mcp_server/preview_studio/__init__.py +5 -0
  65. package/mcp_server/preview_studio/engine.py +280 -0
  66. package/mcp_server/preview_studio/models.py +73 -0
  67. package/mcp_server/preview_studio/tools.py +423 -0
  68. package/mcp_server/runtime/session_kernel.py +96 -0
  69. package/mcp_server/runtime/tools.py +90 -1
  70. package/mcp_server/semantic_moves/__init__.py +13 -0
  71. package/mcp_server/semantic_moves/compiler.py +116 -0
  72. package/mcp_server/semantic_moves/mix_compilers.py +291 -0
  73. package/mcp_server/semantic_moves/mix_moves.py +157 -0
  74. package/mcp_server/semantic_moves/models.py +46 -0
  75. package/mcp_server/semantic_moves/performance_compilers.py +208 -0
  76. package/mcp_server/semantic_moves/performance_moves.py +81 -0
  77. package/mcp_server/semantic_moves/registry.py +32 -0
  78. package/mcp_server/semantic_moves/resolvers.py +126 -0
  79. package/mcp_server/semantic_moves/sound_design_compilers.py +266 -0
  80. package/mcp_server/semantic_moves/sound_design_moves.py +78 -0
  81. package/mcp_server/semantic_moves/tools.py +204 -0
  82. package/mcp_server/semantic_moves/transition_compilers.py +222 -0
  83. package/mcp_server/semantic_moves/transition_moves.py +76 -0
  84. package/mcp_server/server.py +10 -0
  85. package/mcp_server/session_continuity/__init__.py +6 -0
  86. package/mcp_server/session_continuity/models.py +86 -0
  87. package/mcp_server/session_continuity/tools.py +230 -0
  88. package/mcp_server/session_continuity/tracker.py +235 -0
  89. package/mcp_server/song_brain/__init__.py +6 -0
  90. package/mcp_server/song_brain/builder.py +477 -0
  91. package/mcp_server/song_brain/models.py +132 -0
  92. package/mcp_server/song_brain/tools.py +294 -0
  93. package/mcp_server/stuckness_detector/__init__.py +5 -0
  94. package/mcp_server/stuckness_detector/detector.py +400 -0
  95. package/mcp_server/stuckness_detector/models.py +66 -0
  96. package/mcp_server/stuckness_detector/tools.py +195 -0
  97. package/mcp_server/tools/_conductor.py +104 -6
  98. package/mcp_server/tools/analyzer.py +1 -1
  99. package/mcp_server/tools/devices.py +34 -0
  100. package/mcp_server/wonder_mode/__init__.py +6 -0
  101. package/mcp_server/wonder_mode/diagnosis.py +84 -0
  102. package/mcp_server/wonder_mode/engine.py +493 -0
  103. package/mcp_server/wonder_mode/session.py +114 -0
  104. package/mcp_server/wonder_mode/tools.py +285 -0
  105. package/package.json +2 -2
  106. package/remote_script/LivePilot/__init__.py +1 -1
  107. package/remote_script/LivePilot/browser.py +4 -1
  108. package/remote_script/LivePilot/devices.py +29 -0
  109. package/remote_script/LivePilot/tracks.py +11 -4
  110. package/scripts/generate_tool_catalog.py +131 -0
@@ -0,0 +1,116 @@
1
+ """Semantic move compiler — resolves intents to concrete tool call sequences.
2
+
3
+ The compiler takes a SemanticMove + SessionKernel and produces a CompiledPlan:
4
+ a list of concrete, parameterized tool calls ready for execution.
5
+
6
+ Each move family registers a compiler function. The generic compile() dispatcher
7
+ routes to the right family compiler based on the move's family field.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import Callable, Optional
14
+
15
+ from .models import SemanticMove
16
+
17
+
18
+ # ── Compiled plan types ──────────────────────────────────────────────────────
19
+
20
+ @dataclass
21
+ class CompiledStep:
22
+ """A single executable tool call with resolved parameters."""
23
+ tool: str # MCP tool name, e.g. "set_track_volume"
24
+ params: dict # Concrete params, e.g. {"track_index": 0, "volume": 0.72}
25
+ description: str # Human-readable, e.g. "Push Drums from 0.65 → 0.72"
26
+ verify_after: bool = True # Whether to check meters after this step
27
+
28
+ def to_dict(self) -> dict:
29
+ return {
30
+ "tool": self.tool,
31
+ "params": self.params,
32
+ "description": self.description,
33
+ "verify_after": self.verify_after,
34
+ }
35
+
36
+
37
+ @dataclass
38
+ class CompiledPlan:
39
+ """The output of compilation — ready for execution or presentation."""
40
+ move_id: str
41
+ intent: str
42
+ steps: list[CompiledStep] = field(default_factory=list)
43
+ before_reads: list[dict] = field(default_factory=list)
44
+ after_reads: list[dict] = field(default_factory=list)
45
+ risk_level: str = "low"
46
+ summary: str = ""
47
+ requires_approval: bool = True
48
+ warnings: list[str] = field(default_factory=list)
49
+
50
+ @property
51
+ def step_count(self) -> int:
52
+ return len(self.steps)
53
+
54
+ @property
55
+ def executable(self) -> bool:
56
+ """A plan is executable if it has at least one step."""
57
+ return len(self.steps) > 0
58
+
59
+ def to_dict(self) -> dict:
60
+ return {
61
+ "move_id": self.move_id,
62
+ "intent": self.intent,
63
+ "steps": [s.to_dict() for s in self.steps],
64
+ "step_count": self.step_count,
65
+ "before_reads": self.before_reads,
66
+ "after_reads": self.after_reads,
67
+ "risk_level": self.risk_level,
68
+ "summary": self.summary,
69
+ "requires_approval": self.requires_approval,
70
+ "executable": self.executable,
71
+ "warnings": self.warnings,
72
+ }
73
+
74
+
75
+ # ── Compiler registry ────────────────────────────────────────────────────────
76
+
77
+ CompilerFn = Callable[[SemanticMove, dict], CompiledPlan]
78
+ _COMPILERS: dict[str, CompilerFn] = {}
79
+
80
+
81
+ def register_compiler(move_id: str, fn: CompilerFn) -> None:
82
+ """Register a compiler function for a specific move."""
83
+ _COMPILERS[move_id] = fn
84
+
85
+
86
+ def register_family_compiler(family: str, fn: CompilerFn) -> None:
87
+ """Register a fallback compiler for an entire move family."""
88
+ _COMPILERS[f"__family__{family}"] = fn
89
+
90
+
91
+ def compile(move: SemanticMove, kernel: dict) -> CompiledPlan:
92
+ """Compile a semantic move against a session kernel.
93
+
94
+ Looks up the compiler by move_id first, then falls back to family compiler,
95
+ then returns a non-executable plan with a warning.
96
+ """
97
+ # Try move-specific compiler
98
+ fn = _COMPILERS.get(move.move_id)
99
+ if fn:
100
+ return fn(move, kernel)
101
+
102
+ # Try family fallback
103
+ fn = _COMPILERS.get(f"__family__{move.family}")
104
+ if fn:
105
+ return fn(move, kernel)
106
+
107
+ # No compiler found — return a descriptive but non-executable plan
108
+ return CompiledPlan(
109
+ move_id=move.move_id,
110
+ intent=move.intent,
111
+ steps=[],
112
+ summary=f"No compiler registered for move '{move.move_id}'. "
113
+ f"The agent should interpret the intent and execute manually.",
114
+ requires_approval=True,
115
+ warnings=[f"No compiler for {move.move_id} — manual execution required"],
116
+ )
@@ -0,0 +1,291 @@
1
+ """Concrete compilers for mix-domain semantic moves.
2
+
3
+ Each compiler function takes (move, kernel) and returns a CompiledPlan
4
+ with fully parameterized tool calls. The compiler inspects the kernel's
5
+ track topology and device chains to resolve targets.
6
+
7
+ Pure functions — no I/O. All data comes from the kernel dict.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .compiler import CompiledPlan, CompiledStep, register_compiler
13
+ from .models import SemanticMove
14
+ from . import resolvers
15
+
16
+
17
+ def _compile_make_punchier(move: SemanticMove, kernel: dict) -> CompiledPlan:
18
+ """Compile 'make_punchier': push drums, pull pads, tighten master bus."""
19
+ steps = []
20
+ warnings = []
21
+ descriptions = []
22
+
23
+ # Find drum track
24
+ drum_tracks = resolvers.find_tracks_by_role(kernel, ["drums", "percussion"])
25
+ if not drum_tracks:
26
+ # Fallback: look for "unknown" role tracks (often drums)
27
+ drum_tracks = resolvers.find_tracks_by_role(kernel, ["unknown"])
28
+ if not drum_tracks:
29
+ warnings.append("No drum track found — skipping drum push")
30
+
31
+ # Find pad/texture tracks
32
+ pad_tracks = resolvers.find_tracks_by_role(kernel, ["pad", "fx"])
33
+
34
+ # Step 1: Read current state
35
+ steps.append(CompiledStep(
36
+ tool="get_track_meters",
37
+ params={"include_stereo": True},
38
+ description="Read current levels for all tracks",
39
+ verify_after=False,
40
+ ))
41
+
42
+ # Step 2: Push drum volume
43
+ for dt in drum_tracks[:1]: # Only first drum track
44
+ idx = dt["index"]
45
+ steps.append(CompiledStep(
46
+ tool="set_track_volume",
47
+ params={"track_index": idx, "volume": 0.75},
48
+ description=f"Push {dt['name']} (track {idx}) to 0.75 for transient punch",
49
+ ))
50
+ descriptions.append(f"Push {dt['name']} volume to 0.75")
51
+
52
+ # Step 3: Pull back pads
53
+ for pt in pad_tracks:
54
+ idx = pt["index"]
55
+ steps.append(CompiledStep(
56
+ tool="set_track_volume",
57
+ params={"track_index": idx, "volume": 0.25},
58
+ description=f"Pull {pt['name']} (track {idx}) to 0.25 for contrast",
59
+ ))
60
+ descriptions.append(f"Pull {pt['name']} volume to 0.25")
61
+
62
+ # Step 4: Verify
63
+ steps.append(CompiledStep(
64
+ tool="get_track_meters",
65
+ params={"include_stereo": True},
66
+ description="Verify all tracks still producing audio",
67
+ ))
68
+
69
+ return CompiledPlan(
70
+ move_id=move.move_id,
71
+ intent=move.intent,
72
+ steps=steps,
73
+ before_reads=[{"tool": "get_track_meters", "params": {"include_stereo": True}}],
74
+ after_reads=[
75
+ {"tool": "get_track_meters", "params": {"include_stereo": True}},
76
+ {"tool": "get_master_spectrum", "params": {}},
77
+ ],
78
+ risk_level="low",
79
+ summary="; ".join(descriptions) if descriptions else "No changes compiled",
80
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
81
+ warnings=warnings,
82
+ )
83
+
84
+
85
+ def _compile_tighten_low_end(move: SemanticMove, kernel: dict) -> CompiledPlan:
86
+ """Compile 'tighten_low_end': reduce sub volume, boost bass harmonics."""
87
+ steps = []
88
+ warnings = []
89
+ descriptions = []
90
+
91
+ bass_tracks = resolvers.find_tracks_by_role(kernel, ["bass"])
92
+ if not bass_tracks:
93
+ warnings.append("No bass track found — cannot tighten low end")
94
+ return CompiledPlan(
95
+ move_id=move.move_id,
96
+ intent=move.intent,
97
+ summary="No bass track found",
98
+ warnings=warnings,
99
+ )
100
+
101
+ bass = bass_tracks[0]
102
+ idx = bass["index"]
103
+
104
+ # Step 1: Read spectrum
105
+ steps.append(CompiledStep(
106
+ tool="get_master_spectrum",
107
+ params={},
108
+ description="Read current spectral balance",
109
+ verify_after=False,
110
+ ))
111
+
112
+ # Step 2: Reduce bass volume slightly
113
+ steps.append(CompiledStep(
114
+ tool="set_track_volume",
115
+ params={"track_index": idx, "volume": 0.58},
116
+ description=f"Reduce {bass['name']} volume to 0.58 (tighten sub)",
117
+ ))
118
+ descriptions.append(f"Reduce {bass['name']} volume to 0.58")
119
+
120
+ # Step 3: Verify
121
+ steps.append(CompiledStep(
122
+ tool="get_track_meters",
123
+ params={"include_stereo": True},
124
+ description="Verify bass still producing audio after reduction",
125
+ ))
126
+
127
+ return CompiledPlan(
128
+ move_id=move.move_id,
129
+ intent=move.intent,
130
+ steps=steps,
131
+ before_reads=[{"tool": "get_master_spectrum", "params": {}}],
132
+ after_reads=[
133
+ {"tool": "get_master_spectrum", "params": {}},
134
+ {"tool": "get_track_meters", "params": {"include_stereo": True}},
135
+ ],
136
+ risk_level="low",
137
+ summary="; ".join(descriptions),
138
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
139
+ warnings=warnings,
140
+ )
141
+
142
+
143
+ def _compile_widen_stereo(move: SemanticMove, kernel: dict) -> CompiledPlan:
144
+ """Compile 'widen_stereo': pan harmonic elements wider, add depth."""
145
+ steps = []
146
+ warnings = []
147
+ descriptions = []
148
+
149
+ chord_tracks = resolvers.find_tracks_by_role(kernel, ["chords", "pad"])
150
+ lead_tracks = resolvers.find_tracks_by_role(kernel, ["lead"])
151
+ perc_tracks = resolvers.find_tracks_by_role(kernel, ["percussion"])
152
+
153
+ if not chord_tracks and not lead_tracks:
154
+ warnings.append("No harmonic or lead tracks found for stereo widening")
155
+
156
+ # Pan chords left
157
+ for ct in chord_tracks[:1]:
158
+ steps.append(CompiledStep(
159
+ tool="set_track_pan",
160
+ params={"track_index": ct["index"], "pan": -0.35},
161
+ description=f"Pan {ct['name']} left (-35%) for width",
162
+ ))
163
+ descriptions.append(f"Pan {ct['name']} left 35%")
164
+
165
+ # Pan lead right
166
+ for lt in lead_tracks[:1]:
167
+ steps.append(CompiledStep(
168
+ tool="set_track_pan",
169
+ params={"track_index": lt["index"], "pan": 0.30},
170
+ description=f"Pan {lt['name']} right (+30%) for width",
171
+ ))
172
+ descriptions.append(f"Pan {lt['name']} right 30%")
173
+
174
+ # Pan perc slightly
175
+ for pt in perc_tracks[:1]:
176
+ steps.append(CompiledStep(
177
+ tool="set_track_pan",
178
+ params={"track_index": pt["index"], "pan": 0.15},
179
+ description=f"Pan {pt['name']} slightly right (+15%)",
180
+ ))
181
+ descriptions.append(f"Pan {pt['name']} slightly right")
182
+
183
+ # Verify
184
+ steps.append(CompiledStep(
185
+ tool="get_track_meters",
186
+ params={"include_stereo": True},
187
+ description="Verify stereo output on all panned tracks",
188
+ ))
189
+
190
+ return CompiledPlan(
191
+ move_id=move.move_id,
192
+ intent=move.intent,
193
+ steps=steps,
194
+ before_reads=[{"tool": "analyze_mix", "params": {}}],
195
+ after_reads=[
196
+ {"tool": "analyze_mix", "params": {}},
197
+ {"tool": "get_track_meters", "params": {"include_stereo": True}},
198
+ ],
199
+ risk_level="low",
200
+ summary="; ".join(descriptions) if descriptions else "No panning changes",
201
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
202
+ warnings=warnings,
203
+ )
204
+
205
+
206
+ def _compile_darken_mix(move: SemanticMove, kernel: dict) -> CompiledPlan:
207
+ """Compile 'darken_without_losing_width': reduce brightness, preserve stereo."""
208
+ steps = []
209
+ descriptions = []
210
+
211
+ # Find bright-sounding tracks (leads and chords)
212
+ bright_tracks = resolvers.find_tracks_by_role(kernel, ["lead", "chords", "percussion"])
213
+
214
+ for bt in bright_tracks[:2]: # Max 2 tracks
215
+ # Reduce volume slightly to darken
216
+ steps.append(CompiledStep(
217
+ tool="set_track_volume",
218
+ params={"track_index": bt["index"], "volume": 0.40},
219
+ description=f"Pull {bt['name']} to 0.40 for darker tone",
220
+ ))
221
+ descriptions.append(f"Darken {bt['name']} to 0.40")
222
+
223
+ steps.append(CompiledStep(
224
+ tool="get_track_meters",
225
+ params={"include_stereo": True},
226
+ description="Verify all tracks still active after darkening",
227
+ ))
228
+
229
+ return CompiledPlan(
230
+ move_id=move.move_id,
231
+ intent=move.intent,
232
+ steps=steps,
233
+ before_reads=[{"tool": "get_master_spectrum", "params": {}}],
234
+ after_reads=[{"tool": "get_master_spectrum", "params": {}}],
235
+ risk_level="low",
236
+ summary="; ".join(descriptions) if descriptions else "No changes",
237
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
238
+ )
239
+
240
+
241
+ def _compile_reduce_repetition(move: SemanticMove, kernel: dict) -> CompiledPlan:
242
+ """Compile 'reduce_repetition_fatigue': add perlin automation for organic movement."""
243
+ steps = []
244
+ descriptions = []
245
+
246
+ # Find melodic tracks for filter drift
247
+ melodic = resolvers.find_tracks_by_role(kernel, ["chords", "lead", "pad"])
248
+
249
+ for mt in melodic[:2]: # Max 2 tracks
250
+ steps.append(CompiledStep(
251
+ tool="apply_automation_shape",
252
+ params={
253
+ "track_index": mt["index"],
254
+ "clip_index": 0,
255
+ "parameter_type": "send",
256
+ "send_index": 0,
257
+ "curve_type": "perlin",
258
+ "center": 0.2,
259
+ "amplitude": 0.1,
260
+ "duration": 8,
261
+ "density": 16,
262
+ },
263
+ description=f"Add perlin reverb send drift on {mt['name']}",
264
+ ))
265
+ descriptions.append(f"Perlin reverb drift on {mt['name']}")
266
+
267
+ steps.append(CompiledStep(
268
+ tool="get_track_meters",
269
+ params={"include_stereo": True},
270
+ description="Verify tracks still active after automation",
271
+ ))
272
+
273
+ return CompiledPlan(
274
+ move_id=move.move_id,
275
+ intent=move.intent,
276
+ steps=steps,
277
+ before_reads=[],
278
+ after_reads=[{"tool": "get_track_meters", "params": {"include_stereo": True}}],
279
+ risk_level="medium",
280
+ summary="; ".join(descriptions) if descriptions else "No melodic tracks found",
281
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
282
+ )
283
+
284
+
285
+ # ── Register all compilers ──────────────────────────────────────────────────
286
+
287
+ register_compiler("make_punchier", _compile_make_punchier)
288
+ register_compiler("tighten_low_end", _compile_tighten_low_end)
289
+ register_compiler("widen_stereo", _compile_widen_stereo)
290
+ register_compiler("darken_without_losing_width", _compile_darken_mix)
291
+ register_compiler("reduce_repetition_fatigue", _compile_reduce_repetition)
@@ -0,0 +1,157 @@
1
+ """Mix-domain semantic moves — musical intents for mixing and balance."""
2
+
3
+ from .models import SemanticMove
4
+ from .registry import register
5
+
6
+ TIGHTEN_LOW_END = SemanticMove(
7
+ move_id="tighten_low_end",
8
+ family="mix",
9
+ intent="Tighten the low end — reduce sub mud, add bass definition",
10
+ targets={"weight": 0.4, "punch": 0.3, "clarity": 0.3},
11
+ protect={"warmth": 0.6},
12
+ risk_level="low",
13
+ compile_plan=[
14
+ {"tool": "get_master_spectrum", "params": {}, "description": "Check current sub/low balance"},
15
+ {"tool": "set_device_parameter", "params": {"description": "High-pass sub bass around 30-40 Hz"}, "description": "HP filter sub rumble"},
16
+ {"tool": "set_track_volume", "params": {"description": "Reduce sub bass volume 5-10% if sub > 50%"}, "description": "Reduce sub volume"},
17
+ {"tool": "set_device_parameter", "params": {"description": "Gentle saturation drive +2-4dB for harmonic definition"}, "description": "Add bass harmonics"},
18
+ ],
19
+ verification_plan=[
20
+ {"tool": "get_master_spectrum", "check": "sub band should decrease, low-mid stable"},
21
+ {"tool": "get_track_meters", "check": "bass track still producing audio"},
22
+ ],
23
+ )
24
+
25
+ WIDEN_STEREO = SemanticMove(
26
+ move_id="widen_stereo",
27
+ family="mix",
28
+ intent="Widen the stereo field without losing center focus",
29
+ targets={"width": 0.5, "clarity": 0.3, "depth": 0.2},
30
+ protect={"cohesion": 0.7},
31
+ risk_level="low",
32
+ compile_plan=[
33
+ {"tool": "analyze_mix", "params": {}, "description": "Check current stereo state"},
34
+ {"tool": "set_track_pan", "params": {"description": "Pan harmonic elements wider: +/-25-40%"}, "description": "Pan harmonics wider"},
35
+ {"tool": "set_track_pan", "params": {"description": "Pan percussion subtly: +/-10-20%"}, "description": "Pan perc subtly"},
36
+ {"tool": "set_track_send", "params": {"description": "Increase reverb send on wide elements +10-15%"}, "description": "Add depth to wide elements"},
37
+ ],
38
+ verification_plan=[
39
+ {"tool": "get_track_meters", "check": "all tracks producing stereo output"},
40
+ {"tool": "analyze_mix", "check": "stereo.mono_risk is false, side_activity > 0.1"},
41
+ ],
42
+ )
43
+
44
+ MAKE_PUNCHIER = SemanticMove(
45
+ move_id="make_punchier",
46
+ family="mix",
47
+ intent="Increase punch and transient impact across the mix",
48
+ targets={"punch": 0.5, "energy": 0.3, "contrast": 0.2},
49
+ protect={"clarity": 0.7, "warmth": 0.5},
50
+ risk_level="low",
51
+ compile_plan=[
52
+ {"tool": "get_track_meters", "params": {"include_stereo": True}, "description": "Read current levels"},
53
+ {"tool": "set_track_volume", "params": {"description": "Push drum track +5-8%"}, "description": "Push drum level"},
54
+ {"tool": "set_track_volume", "params": {"description": "Pull pad/texture -5-10%"}, "description": "Pull back pads"},
55
+ {"tool": "set_device_parameter", "params": {"description": "Lower Glue Compressor threshold 2-3dB if on master"}, "description": "Tighten master bus"},
56
+ ],
57
+ verification_plan=[
58
+ {"tool": "get_master_spectrum", "check": "mid and high-mid energy increased relative to before"},
59
+ {"tool": "get_track_meters", "check": "all tracks still producing audio"},
60
+ ],
61
+ )
62
+
63
+ DARKEN_MIX = SemanticMove(
64
+ move_id="darken_without_losing_width",
65
+ family="mix",
66
+ intent="Darken the mix tone by rolling off highs without collapsing stereo",
67
+ targets={"warmth": 0.5, "depth": 0.3, "width": 0.2},
68
+ protect={"width": 0.7, "clarity": 0.5},
69
+ risk_level="low",
70
+ compile_plan=[
71
+ {"tool": "get_master_spectrum", "params": {}, "description": "Check current tonal balance"},
72
+ {"tool": "set_device_parameter", "params": {"description": "Lower EQ/Auto Filter high shelf -2-4dB on bright tracks"}, "description": "Roll off highs"},
73
+ {"tool": "set_track_send", "params": {"description": "Increase reverb send on darkened elements for depth compensation"}, "description": "Compensate depth"},
74
+ ],
75
+ verification_plan=[
76
+ {"tool": "get_master_spectrum", "check": "high and air bands decreased, low-mid stable or increased"},
77
+ {"tool": "get_track_meters", "check": "all tracks producing audio (filter didn't kill signal)"},
78
+ ],
79
+ )
80
+
81
+ REDUCE_REPETITION = SemanticMove(
82
+ move_id="reduce_repetition_fatigue",
83
+ family="mix",
84
+ intent="Reduce repetition fatigue by adding organic movement and variation",
85
+ targets={"motion": 0.4, "novelty": 0.3, "contrast": 0.3},
86
+ protect={"cohesion": 0.6},
87
+ risk_level="medium",
88
+ compile_plan=[
89
+ {"tool": "apply_automation_shape", "params": {"curve_type": "perlin", "description": "Perlin noise on filter cutoff"}, "description": "Add organic filter drift"},
90
+ {"tool": "apply_automation_shape", "params": {"curve_type": "perlin", "description": "Perlin noise on send levels"}, "description": "Add depth movement"},
91
+ {"tool": "set_device_parameter", "params": {"description": "Increase Beat Repeat variation or chance"}, "description": "Add rhythmic variation"},
92
+ ],
93
+ verification_plan=[
94
+ {"tool": "get_track_meters", "check": "all tracks still producing audio"},
95
+ {"tool": "capture_audio", "check": "LRA > 2 LU (dynamic range should increase)"},
96
+ ],
97
+ )
98
+
99
+ MAKE_KICK_BASS_LOCK = SemanticMove(
100
+ move_id="make_kick_bass_lock",
101
+ family="mix",
102
+ intent="Make kick and bass work together — frequency separation + timing",
103
+ targets={"weight": 0.4, "punch": 0.3, "groove": 0.3},
104
+ protect={"warmth": 0.6, "cohesion": 0.7},
105
+ risk_level="low",
106
+ compile_plan=[
107
+ {"tool": "get_device_parameters", "params": {"description": "Read bass EQ/filter state"}, "description": "Inspect bass chain"},
108
+ {"tool": "set_device_parameter", "params": {"description": "High-pass bass at 40-60 Hz to clear space for kick sub"}, "description": "HP bass for kick clearance"},
109
+ {"tool": "set_device_parameter", "params": {"description": "Sidechain compressor or volume duck on bass from kick"}, "description": "Duck bass on kick hits"},
110
+ ],
111
+ verification_plan=[
112
+ {"tool": "get_master_spectrum", "check": "sub and low bands balanced, no masking"},
113
+ {"tool": "get_track_meters", "check": "both kick and bass tracks producing audio"},
114
+ ],
115
+ )
116
+
117
+ CREATE_BUILDUP_TENSION = SemanticMove(
118
+ move_id="create_buildup_tension",
119
+ family="arrangement",
120
+ intent="Create tension and energy buildup toward a drop or chorus",
121
+ targets={"tension": 0.5, "energy": 0.3, "contrast": 0.2},
122
+ protect={"clarity": 0.6},
123
+ risk_level="medium",
124
+ compile_plan=[
125
+ {"tool": "apply_gesture_template", "params": {"template_name": "tension_ratchet"}, "description": "Apply staged tension ratchet"},
126
+ {"tool": "apply_automation_shape", "params": {"curve_type": "exponential", "description": "Rising HP filter over 4-8 bars"}, "description": "HP filter rise"},
127
+ {"tool": "set_track_send", "params": {"description": "Increase reverb send for wash effect"}, "description": "Add reverb wash"},
128
+ ],
129
+ verification_plan=[
130
+ {"tool": "get_emotional_arc", "check": "tension should increase before target section"},
131
+ {"tool": "get_track_meters", "check": "all tracks still producing audio"},
132
+ ],
133
+ )
134
+
135
+ SMOOTH_SCENE_HANDOFF = SemanticMove(
136
+ move_id="smooth_scene_handoff",
137
+ family="arrangement",
138
+ intent="Create a smooth transition between two scenes",
139
+ targets={"cohesion": 0.5, "motion": 0.3, "contrast": 0.2},
140
+ protect={"clarity": 0.7},
141
+ risk_level="low",
142
+ compile_plan=[
143
+ {"tool": "apply_gesture_template", "params": {"template_name": "pre_arrival_vacuum"}, "description": "Pull energy back before transition"},
144
+ {"tool": "apply_gesture_template", "params": {"template_name": "re_entry_spotlight"}, "description": "Spotlight returning elements"},
145
+ ],
146
+ verification_plan=[
147
+ {"tool": "get_emotional_arc", "check": "transition point should show energy dip then recovery"},
148
+ ],
149
+ )
150
+
151
+ # Register all moves
152
+ for _move in [
153
+ TIGHTEN_LOW_END, WIDEN_STEREO, MAKE_PUNCHIER, DARKEN_MIX,
154
+ REDUCE_REPETITION, MAKE_KICK_BASS_LOCK, CREATE_BUILDUP_TENSION,
155
+ SMOOTH_SCENE_HANDOFF,
156
+ ]:
157
+ register(_move)
@@ -0,0 +1,46 @@
1
+ """SemanticMove — high-level musical intent that compiles to tool sequences.
2
+
3
+ A semantic move expresses WHAT to achieve musically, not HOW to achieve it
4
+ parametrically. Each move has a compile_plan that decomposes into existing
5
+ deterministic MCP tools, and a verification_plan that checks the result.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+
12
+
13
+ @dataclass
14
+ class SemanticMove:
15
+ """A musical action expressed as intent, not parameters."""
16
+
17
+ move_id: str
18
+ family: str # mix, arrangement, transition, sound_design, performance
19
+ intent: str # human-readable description of the musical goal
20
+ targets: dict = field(default_factory=dict) # dimension -> weight
21
+ protect: dict = field(default_factory=dict) # dimension -> threshold
22
+ risk_level: str = "low" # low, medium, high
23
+ required_capabilities: list = field(default_factory=list)
24
+ compile_plan: list = field(default_factory=list) # [{tool, params, description}]
25
+ verification_plan: list = field(default_factory=list) # [{tool, check}]
26
+ confidence: float = 0.7
27
+
28
+ def to_dict(self) -> dict:
29
+ return {
30
+ "move_id": self.move_id,
31
+ "family": self.family,
32
+ "intent": self.intent,
33
+ "targets": self.targets,
34
+ "protect": self.protect,
35
+ "risk_level": self.risk_level,
36
+ "required_capabilities": self.required_capabilities,
37
+ "compile_plan_steps": len(self.compile_plan),
38
+ "confidence": self.confidence,
39
+ }
40
+
41
+ def to_full_dict(self) -> dict:
42
+ """Full representation including compile and verification plans."""
43
+ d = self.to_dict()
44
+ d["compile_plan"] = self.compile_plan
45
+ d["verification_plan"] = self.verification_plan
46
+ return d