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,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
|