livepilot 1.9.23 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +119 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +144 -13
- package/bin/livepilot.js +87 -0
- package/installer/codex.js +147 -0
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +21 -4
- package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
- package/livepilot/skills/livepilot-core/references/overview.md +13 -9
- package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
- package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
- package/livepilot/skills/livepilot-devices/SKILL.md +16 -2
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +19 -5
- package/livepilot/skills/livepilot-sample-engine/SKILL.md +104 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +45 -0
- package/livepilot/skills/livepilot-wonder/SKILL.md +15 -0
- package/livepilot.mcpb +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/manifest.json +2 -2
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +357 -0
- package/mcp_server/atlas/device_atlas.json +44067 -0
- package/mcp_server/atlas/enrichments/__init__.py +111 -0
- package/mcp_server/atlas/enrichments/audio_effects/auto_filter.yaml +162 -0
- package/mcp_server/atlas/enrichments/audio_effects/beat_repeat.yaml +183 -0
- package/mcp_server/atlas/enrichments/audio_effects/channel_eq.yaml +126 -0
- package/mcp_server/atlas/enrichments/audio_effects/chorus_ensemble.yaml +149 -0
- package/mcp_server/atlas/enrichments/audio_effects/color_limiter.yaml +109 -0
- package/mcp_server/atlas/enrichments/audio_effects/compressor.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb.yaml +143 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb_pro.yaml +178 -0
- package/mcp_server/atlas/enrichments/audio_effects/delay.yaml +151 -0
- package/mcp_server/atlas/enrichments/audio_effects/drum_buss.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/dynamic_tube.yaml +147 -0
- package/mcp_server/atlas/enrichments/audio_effects/echo.yaml +167 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_eight.yaml +148 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_three.yaml +121 -0
- package/mcp_server/atlas/enrichments/audio_effects/erosion.yaml +103 -0
- package/mcp_server/atlas/enrichments/audio_effects/filter_delay.yaml +173 -0
- package/mcp_server/atlas/enrichments/audio_effects/gate.yaml +130 -0
- package/mcp_server/atlas/enrichments/audio_effects/gated_delay.yaml +133 -0
- package/mcp_server/atlas/enrichments/audio_effects/glue_compressor.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/grain_delay.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/hybrid_reverb.yaml +160 -0
- package/mcp_server/atlas/enrichments/audio_effects/limiter.yaml +97 -0
- package/mcp_server/atlas/enrichments/audio_effects/multiband_dynamics.yaml +174 -0
- package/mcp_server/atlas/enrichments/audio_effects/overdrive.yaml +119 -0
- package/mcp_server/atlas/enrichments/audio_effects/pedal.yaml +145 -0
- package/mcp_server/atlas/enrichments/audio_effects/phaser_flanger.yaml +161 -0
- package/mcp_server/atlas/enrichments/audio_effects/redux.yaml +114 -0
- package/mcp_server/atlas/enrichments/audio_effects/reverb.yaml +190 -0
- package/mcp_server/atlas/enrichments/audio_effects/roar.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/saturator.yaml +146 -0
- package/mcp_server/atlas/enrichments/audio_effects/shifter.yaml +154 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_resonator.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_time.yaml +164 -0
- package/mcp_server/atlas/enrichments/audio_effects/vector_delay.yaml +140 -0
- package/mcp_server/atlas/enrichments/audio_effects/vinyl_distortion.yaml +141 -0
- package/mcp_server/atlas/enrichments/instruments/analog.yaml +222 -0
- package/mcp_server/atlas/enrichments/instruments/bass.yaml +202 -0
- package/mcp_server/atlas/enrichments/instruments/collision.yaml +150 -0
- package/mcp_server/atlas/enrichments/instruments/drift.yaml +167 -0
- package/mcp_server/atlas/enrichments/instruments/electric.yaml +137 -0
- package/mcp_server/atlas/enrichments/instruments/emit.yaml +163 -0
- package/mcp_server/atlas/enrichments/instruments/meld.yaml +164 -0
- package/mcp_server/atlas/enrichments/instruments/operator.yaml +197 -0
- package/mcp_server/atlas/enrichments/instruments/poli.yaml +192 -0
- package/mcp_server/atlas/enrichments/instruments/sampler.yaml +218 -0
- package/mcp_server/atlas/enrichments/instruments/simpler.yaml +217 -0
- package/mcp_server/atlas/enrichments/instruments/tension.yaml +156 -0
- package/mcp_server/atlas/enrichments/instruments/tree_tone.yaml +162 -0
- package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +165 -0
- package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +166 -0
- package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +162 -0
- package/mcp_server/atlas/enrichments/midi_effects/arpeggiator.yaml +156 -0
- package/mcp_server/atlas/enrichments/midi_effects/bouncy_notes.yaml +93 -0
- package/mcp_server/atlas/enrichments/midi_effects/chord.yaml +147 -0
- package/mcp_server/atlas/enrichments/midi_effects/melodic_steps.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_echo.yaml +108 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_length.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/pitch.yaml +76 -0
- package/mcp_server/atlas/enrichments/midi_effects/random.yaml +117 -0
- package/mcp_server/atlas/enrichments/midi_effects/rhythmic_steps.yaml +103 -0
- package/mcp_server/atlas/enrichments/midi_effects/scale.yaml +83 -0
- package/mcp_server/atlas/enrichments/midi_effects/step_arp.yaml +112 -0
- package/mcp_server/atlas/enrichments/midi_effects/velocity.yaml +119 -0
- package/mcp_server/atlas/enrichments/utility/amp.yaml +159 -0
- package/mcp_server/atlas/enrichments/utility/cabinet.yaml +109 -0
- package/mcp_server/atlas/enrichments/utility/corpus.yaml +150 -0
- package/mcp_server/atlas/enrichments/utility/resonators.yaml +131 -0
- package/mcp_server/atlas/enrichments/utility/spectrum.yaml +63 -0
- package/mcp_server/atlas/enrichments/utility/tuner.yaml +51 -0
- package/mcp_server/atlas/enrichments/utility/utility.yaml +136 -0
- package/mcp_server/atlas/enrichments/utility/vocoder.yaml +160 -0
- package/mcp_server/atlas/scanner.py +236 -0
- package/mcp_server/atlas/tools.py +224 -0
- package/mcp_server/composer/__init__.py +1 -0
- package/mcp_server/composer/engine.py +452 -0
- package/mcp_server/composer/layer_planner.py +427 -0
- package/mcp_server/composer/prompt_parser.py +329 -0
- package/mcp_server/composer/tools.py +201 -0
- package/mcp_server/connection.py +53 -8
- package/mcp_server/corpus/__init__.py +377 -0
- package/mcp_server/device_forge/__init__.py +1 -0
- package/mcp_server/device_forge/builder.py +377 -0
- package/mcp_server/device_forge/models.py +142 -0
- package/mcp_server/device_forge/templates.py +483 -0
- package/mcp_server/device_forge/tools.py +162 -0
- package/mcp_server/hook_hunter/analyzer.py +23 -0
- package/mcp_server/hook_hunter/models.py +1 -0
- package/mcp_server/hook_hunter/tools.py +4 -2
- package/mcp_server/m4l_bridge.py +1 -0
- package/mcp_server/memory/taste_graph.py +68 -1
- package/mcp_server/memory/tools.py +15 -4
- package/mcp_server/musical_intelligence/detectors.py +14 -1
- package/mcp_server/musical_intelligence/tools.py +11 -8
- package/mcp_server/persistence/__init__.py +1 -0
- package/mcp_server/persistence/base_store.py +82 -0
- package/mcp_server/persistence/project_store.py +106 -0
- package/mcp_server/persistence/taste_store.py +122 -0
- package/mcp_server/preview_studio/models.py +1 -0
- package/mcp_server/preview_studio/tools.py +56 -13
- package/mcp_server/runtime/capability.py +66 -0
- package/mcp_server/runtime/capability_probe.py +137 -0
- package/mcp_server/runtime/execution_router.py +143 -0
- package/mcp_server/runtime/live_version.py +102 -0
- package/mcp_server/runtime/remote_commands.py +87 -0
- package/mcp_server/runtime/tools.py +18 -4
- package/mcp_server/sample_engine/__init__.py +1 -0
- package/mcp_server/sample_engine/analyzer.py +216 -0
- package/mcp_server/sample_engine/critics.py +390 -0
- package/mcp_server/sample_engine/models.py +193 -0
- package/mcp_server/sample_engine/moves.py +127 -0
- package/mcp_server/sample_engine/planner.py +186 -0
- package/mcp_server/sample_engine/sources.py +540 -0
- package/mcp_server/sample_engine/techniques.py +908 -0
- package/mcp_server/sample_engine/tools.py +442 -0
- package/mcp_server/semantic_moves/__init__.py +3 -0
- package/mcp_server/semantic_moves/device_creation_moves.py +237 -0
- package/mcp_server/semantic_moves/mix_moves.py +41 -41
- package/mcp_server/semantic_moves/performance_moves.py +13 -13
- package/mcp_server/semantic_moves/sample_compilers.py +372 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +15 -15
- package/mcp_server/semantic_moves/tools.py +18 -17
- package/mcp_server/semantic_moves/transition_moves.py +16 -16
- package/mcp_server/server.py +51 -0
- package/mcp_server/services/__init__.py +1 -0
- package/mcp_server/services/motif_service.py +67 -0
- package/mcp_server/session_continuity/tracker.py +29 -1
- package/mcp_server/song_brain/builder.py +28 -1
- package/mcp_server/song_brain/models.py +4 -0
- package/mcp_server/song_brain/tools.py +20 -2
- package/mcp_server/sound_design/critics.py +89 -1
- package/mcp_server/splice_client/__init__.py +1 -0
- package/mcp_server/splice_client/client.py +347 -0
- package/mcp_server/splice_client/models.py +96 -0
- package/mcp_server/splice_client/protos/__init__.py +1 -0
- package/mcp_server/splice_client/protos/app_pb2.py +319 -0
- package/mcp_server/splice_client/protos/app_pb2.pyi +1153 -0
- package/mcp_server/splice_client/protos/app_pb2_grpc.py +1946 -0
- package/mcp_server/tools/arrangement.py +69 -0
- package/mcp_server/tools/automation.py +15 -2
- package/mcp_server/tools/devices.py +117 -6
- package/mcp_server/tools/notes.py +37 -4
- package/mcp_server/wonder_mode/diagnosis.py +5 -0
- package/mcp_server/wonder_mode/engine.py +85 -1
- package/mcp_server/wonder_mode/tools.py +6 -1
- package/package.json +12 -2
- package/remote_script/LivePilot/__init__.py +8 -1
- package/remote_script/LivePilot/arrangement.py +114 -0
- package/remote_script/LivePilot/browser.py +56 -1
- package/remote_script/LivePilot/devices.py +236 -6
- package/remote_script/LivePilot/mixing.py +8 -3
- package/remote_script/LivePilot/server.py +5 -1
- package/remote_script/LivePilot/transport.py +3 -0
- package/remote_script/LivePilot/version_detect.py +78 -0
- package/scripts/sync_metadata.py +132 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Sample Engine data models — all dataclasses with to_dict().
|
|
2
|
+
|
|
3
|
+
Pure data structures for sample profiles, intents, critic results,
|
|
4
|
+
fit reports, candidates, and techniques. Zero I/O.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import asdict, dataclass, field
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
VALID_MATERIAL_TYPES = frozenset({
|
|
14
|
+
"vocal", "drum_loop", "instrument_loop", "one_shot",
|
|
15
|
+
"texture", "foley", "fx", "full_mix", "unknown",
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
VALID_INTENTS = frozenset({
|
|
19
|
+
"rhythm", "texture", "layer", "melody", "vocal",
|
|
20
|
+
"atmosphere", "transform", "challenge",
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
VALID_SIMPLER_MODES = frozenset({"classic", "one_shot", "slice"})
|
|
24
|
+
|
|
25
|
+
VALID_SLICE_METHODS = frozenset({"transient", "beat", "region", "manual"})
|
|
26
|
+
|
|
27
|
+
VALID_WARP_MODES = frozenset({
|
|
28
|
+
"beats", "tones", "texture", "complex", "complex_pro",
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class SampleProfile:
|
|
34
|
+
"""Complete fingerprint of a sample."""
|
|
35
|
+
|
|
36
|
+
source: str
|
|
37
|
+
file_path: str
|
|
38
|
+
name: str
|
|
39
|
+
uri: Optional[str] = None
|
|
40
|
+
freesound_id: Optional[int] = None
|
|
41
|
+
license: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
key: Optional[str] = None
|
|
44
|
+
key_confidence: float = 0.0
|
|
45
|
+
bpm: Optional[float] = None
|
|
46
|
+
bpm_confidence: float = 0.0
|
|
47
|
+
time_signature: str = "4/4"
|
|
48
|
+
|
|
49
|
+
material_type: str = "unknown"
|
|
50
|
+
material_confidence: float = 0.0
|
|
51
|
+
|
|
52
|
+
frequency_center: float = 0.0
|
|
53
|
+
frequency_spread: float = 0.0
|
|
54
|
+
brightness: float = 0.0
|
|
55
|
+
transient_density: float = 0.0
|
|
56
|
+
|
|
57
|
+
duration_seconds: float = 0.0
|
|
58
|
+
duration_beats: Optional[float] = None
|
|
59
|
+
bar_count: Optional[float] = None
|
|
60
|
+
has_clear_downbeat: bool = False
|
|
61
|
+
|
|
62
|
+
suggested_mode: str = "classic"
|
|
63
|
+
suggested_slice_by: str = "transient"
|
|
64
|
+
suggested_warp_mode: str = "complex"
|
|
65
|
+
|
|
66
|
+
def to_dict(self) -> dict:
|
|
67
|
+
return asdict(self)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class SampleIntent:
|
|
72
|
+
"""What the user wants to do with a sample."""
|
|
73
|
+
|
|
74
|
+
intent_type: str
|
|
75
|
+
description: str
|
|
76
|
+
philosophy: str = "auto"
|
|
77
|
+
target_track: Optional[int] = None
|
|
78
|
+
|
|
79
|
+
def to_dict(self) -> dict:
|
|
80
|
+
return asdict(self)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class CriticResult:
|
|
85
|
+
"""Result from a single sample critic."""
|
|
86
|
+
|
|
87
|
+
critic_name: str
|
|
88
|
+
score: float
|
|
89
|
+
recommendation: str
|
|
90
|
+
adjustments: list = field(default_factory=list)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def rating(self) -> str:
|
|
94
|
+
if self.score >= 0.8:
|
|
95
|
+
return "excellent"
|
|
96
|
+
if self.score >= 0.6:
|
|
97
|
+
return "good"
|
|
98
|
+
if self.score >= 0.4:
|
|
99
|
+
return "fair"
|
|
100
|
+
return "poor"
|
|
101
|
+
|
|
102
|
+
def to_dict(self) -> dict:
|
|
103
|
+
d = asdict(self)
|
|
104
|
+
d["rating"] = self.rating
|
|
105
|
+
return d
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class SampleFitReport:
|
|
110
|
+
"""Output of the 6-critic battery."""
|
|
111
|
+
|
|
112
|
+
sample: SampleProfile
|
|
113
|
+
critics: dict # str -> CriticResult
|
|
114
|
+
recommended_intent: str = ""
|
|
115
|
+
recommended_technique: str = ""
|
|
116
|
+
processing_chain: list = field(default_factory=list)
|
|
117
|
+
warnings: list = field(default_factory=list)
|
|
118
|
+
surgeon_plan: list = field(default_factory=list)
|
|
119
|
+
alchemist_plan: list = field(default_factory=list)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def overall_score(self) -> float:
|
|
123
|
+
if not self.critics:
|
|
124
|
+
return 0.0
|
|
125
|
+
scores = [c.score if isinstance(c, CriticResult) else c.get("score", 0)
|
|
126
|
+
for c in self.critics.values()]
|
|
127
|
+
return sum(scores) / len(scores) if scores else 0.0
|
|
128
|
+
|
|
129
|
+
def to_dict(self) -> dict:
|
|
130
|
+
return {
|
|
131
|
+
"sample": self.sample.to_dict(),
|
|
132
|
+
"overall_score": round(self.overall_score, 3),
|
|
133
|
+
"critics": {k: (v.to_dict() if isinstance(v, CriticResult) else v)
|
|
134
|
+
for k, v in self.critics.items()},
|
|
135
|
+
"recommended_intent": self.recommended_intent,
|
|
136
|
+
"recommended_technique": self.recommended_technique,
|
|
137
|
+
"processing_chain": self.processing_chain,
|
|
138
|
+
"warnings": self.warnings,
|
|
139
|
+
"surgeon_plan": self.surgeon_plan,
|
|
140
|
+
"alchemist_plan": self.alchemist_plan,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class SampleCandidate:
|
|
146
|
+
"""A sample discovered by a source, pre-load."""
|
|
147
|
+
|
|
148
|
+
source: str
|
|
149
|
+
name: str
|
|
150
|
+
metadata: dict = field(default_factory=dict)
|
|
151
|
+
file_path: Optional[str] = None
|
|
152
|
+
uri: Optional[str] = None
|
|
153
|
+
freesound_id: Optional[int] = None
|
|
154
|
+
relevance_score: float = 0.0
|
|
155
|
+
|
|
156
|
+
def to_dict(self) -> dict:
|
|
157
|
+
return asdict(self)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class TechniqueStep:
|
|
162
|
+
"""A single step in a sample technique recipe."""
|
|
163
|
+
|
|
164
|
+
tool: str
|
|
165
|
+
params: dict = field(default_factory=dict)
|
|
166
|
+
description: str = ""
|
|
167
|
+
condition: Optional[str] = None
|
|
168
|
+
|
|
169
|
+
def to_dict(self) -> dict:
|
|
170
|
+
return asdict(self)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class SampleTechnique:
|
|
175
|
+
"""A sample manipulation recipe from the technique library."""
|
|
176
|
+
|
|
177
|
+
technique_id: str
|
|
178
|
+
name: str
|
|
179
|
+
philosophy: str
|
|
180
|
+
material_types: list = field(default_factory=list)
|
|
181
|
+
intents: list = field(default_factory=list)
|
|
182
|
+
difficulty: str = "basic"
|
|
183
|
+
description: str = ""
|
|
184
|
+
inspiration: str = ""
|
|
185
|
+
steps: list = field(default_factory=list) # list[TechniqueStep]
|
|
186
|
+
success_signals: list = field(default_factory=list)
|
|
187
|
+
failure_signals: list = field(default_factory=list)
|
|
188
|
+
|
|
189
|
+
def to_dict(self) -> dict:
|
|
190
|
+
d = asdict(self)
|
|
191
|
+
d["steps"] = [s.to_dict() if isinstance(s, TechniqueStep) else s
|
|
192
|
+
for s in self.steps]
|
|
193
|
+
return d
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Sample-domain semantic moves — musical intents for sample manipulation.
|
|
2
|
+
|
|
3
|
+
These moves express creative sample-based intentions that compile to
|
|
4
|
+
deterministic tool sequences via the sample compilers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..semantic_moves.models import SemanticMove
|
|
8
|
+
from ..semantic_moves.registry import register
|
|
9
|
+
|
|
10
|
+
SAMPLE_CHOP_RHYTHM = SemanticMove(
|
|
11
|
+
move_id="sample_chop_rhythm",
|
|
12
|
+
family="sample",
|
|
13
|
+
intent="Chop a sample into rhythmic slices — create a new groove from existing material",
|
|
14
|
+
targets={"groove": 0.5, "novelty": 0.3, "punch": 0.2},
|
|
15
|
+
protect={"clarity": 0.6, "coherence": 0.5},
|
|
16
|
+
risk_level="medium",
|
|
17
|
+
compile_plan=[
|
|
18
|
+
{"tool": "load_sample_to_simpler", "params": {"description": "Load sample into Simpler for slicing"}, "description": "Load into Simpler", "backend": "bridge_command"},
|
|
19
|
+
{"tool": "set_simpler_playback_mode", "params": {"mode": "slice", "description": "Switch to slice mode for rhythmic chopping"}, "description": "Enable slice mode", "backend": "remote_command"},
|
|
20
|
+
{"tool": "crop_simpler", "params": {"description": "Crop to rhythmically relevant region"}, "description": "Crop to useful region", "backend": "bridge_command"},
|
|
21
|
+
],
|
|
22
|
+
verification_plan=[
|
|
23
|
+
{"tool": "get_simpler_slices", "check": "slices present and evenly distributed", "backend": "bridge_command"},
|
|
24
|
+
{"tool": "get_track_meters", "check": "track producing audio after slicing", "backend": "remote_command"},
|
|
25
|
+
],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
SAMPLE_TEXTURE_LAYER = SemanticMove(
|
|
29
|
+
move_id="sample_texture_layer",
|
|
30
|
+
family="sample",
|
|
31
|
+
intent="Layer a sample as background texture — stretching and filtering for atmosphere",
|
|
32
|
+
targets={"depth": 0.4, "motion": 0.3, "warmth": 0.3},
|
|
33
|
+
protect={"clarity": 0.7, "punch": 0.5},
|
|
34
|
+
risk_level="low",
|
|
35
|
+
compile_plan=[
|
|
36
|
+
{"tool": "load_sample_to_simpler", "params": {"description": "Load textural sample into Simpler"}, "description": "Load texture sample", "backend": "bridge_command"},
|
|
37
|
+
{"tool": "set_simpler_playback_mode", "params": {"mode": "classic", "description": "Classic mode for sustained texture playback"}, "description": "Classic playback", "backend": "remote_command"},
|
|
38
|
+
{"tool": "set_device_parameter", "params": {"description": "Lower filter cutoff to sit beneath main elements"}, "description": "Filter for background placement", "backend": "remote_command"},
|
|
39
|
+
{"tool": "set_track_send", "params": {"description": "Add reverb send for spatial depth"}, "description": "Reverb for depth", "backend": "remote_command"},
|
|
40
|
+
],
|
|
41
|
+
verification_plan=[
|
|
42
|
+
{"tool": "get_track_meters", "check": "track producing audio at low level", "backend": "remote_command"},
|
|
43
|
+
],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
SAMPLE_VOCAL_GHOST = SemanticMove(
|
|
47
|
+
move_id="sample_vocal_ghost",
|
|
48
|
+
family="sample",
|
|
49
|
+
intent="Create ghostly vocal texture — pitch-shift, reverse, and wash a vocal sample",
|
|
50
|
+
targets={"novelty": 0.4, "depth": 0.3, "motion": 0.3},
|
|
51
|
+
protect={"clarity": 0.5},
|
|
52
|
+
risk_level="medium",
|
|
53
|
+
compile_plan=[
|
|
54
|
+
{"tool": "load_sample_to_simpler", "params": {"description": "Load vocal sample into Simpler"}, "description": "Load vocal", "backend": "bridge_command"},
|
|
55
|
+
{"tool": "reverse_simpler", "params": {"description": "Reverse for ghostly character"}, "description": "Reverse vocal", "backend": "bridge_command"},
|
|
56
|
+
{"tool": "set_device_parameter", "params": {"description": "Detune -5 to -12 semitones for haunting pitch"}, "description": "Pitch down for ghost effect", "backend": "remote_command"},
|
|
57
|
+
{"tool": "set_track_send", "params": {"description": "Heavy reverb send 40-60% for wash"}, "description": "Reverb wash", "backend": "remote_command"},
|
|
58
|
+
],
|
|
59
|
+
verification_plan=[
|
|
60
|
+
{"tool": "get_track_meters", "check": "track producing audio with reverb tail", "backend": "remote_command"},
|
|
61
|
+
],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
SAMPLE_BREAK_LAYER = SemanticMove(
|
|
65
|
+
move_id="sample_break_layer",
|
|
66
|
+
family="sample",
|
|
67
|
+
intent="Layer a breakbeat over existing drums — slice and rearrange for energy",
|
|
68
|
+
targets={"groove": 0.4, "punch": 0.3, "novelty": 0.3},
|
|
69
|
+
protect={"coherence": 0.6, "clarity": 0.5},
|
|
70
|
+
risk_level="medium",
|
|
71
|
+
compile_plan=[
|
|
72
|
+
{"tool": "create_midi_track", "params": {"description": "New track for break layer"}, "description": "Create break track", "backend": "remote_command"},
|
|
73
|
+
{"tool": "load_sample_to_simpler", "params": {"description": "Load breakbeat into Simpler"}, "description": "Load break", "backend": "bridge_command"},
|
|
74
|
+
{"tool": "set_simpler_playback_mode", "params": {"mode": "slice", "slice_by": "transient", "description": "Slice by transients for individual hits"}, "description": "Slice break by transients", "backend": "remote_command"},
|
|
75
|
+
{"tool": "set_track_volume", "params": {"description": "Set break layer volume below main drums"}, "description": "Balance break level", "backend": "remote_command"},
|
|
76
|
+
],
|
|
77
|
+
verification_plan=[
|
|
78
|
+
{"tool": "get_simpler_slices", "check": "break sliced into individual hits", "backend": "bridge_command"},
|
|
79
|
+
{"tool": "get_track_meters", "check": "break track producing audio, not overpowering drums", "backend": "remote_command"},
|
|
80
|
+
],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
SAMPLE_RESAMPLE_DESTROY = SemanticMove(
|
|
84
|
+
move_id="sample_resample_destroy",
|
|
85
|
+
family="sample",
|
|
86
|
+
intent="Destructively resample — warp, bitcrush, and mangle for creative destruction",
|
|
87
|
+
targets={"novelty": 0.5, "motion": 0.3, "groove": 0.2},
|
|
88
|
+
protect={"coherence": 0.4},
|
|
89
|
+
risk_level="high",
|
|
90
|
+
compile_plan=[
|
|
91
|
+
{"tool": "load_sample_to_simpler", "params": {"description": "Load sample for destruction"}, "description": "Load source material", "backend": "bridge_command"},
|
|
92
|
+
{"tool": "warp_simpler", "params": {"description": "Extreme warp settings for time-stretch artifacts"}, "description": "Warp for artifacts", "backend": "bridge_command"},
|
|
93
|
+
{"tool": "set_device_parameter", "params": {"description": "Add Redux or bitcrusher for lo-fi destruction"}, "description": "Bitcrush/reduce", "backend": "remote_command"},
|
|
94
|
+
{"tool": "set_device_parameter", "params": {"description": "Saturator drive to maximum for harmonic distortion"}, "description": "Saturate heavily", "backend": "remote_command"},
|
|
95
|
+
],
|
|
96
|
+
verification_plan=[
|
|
97
|
+
{"tool": "get_track_meters", "check": "track producing audio, signal significantly transformed", "backend": "remote_command"},
|
|
98
|
+
],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
SAMPLE_ONE_SHOT_ACCENT = SemanticMove(
|
|
102
|
+
move_id="sample_one_shot_accent",
|
|
103
|
+
family="sample",
|
|
104
|
+
intent="Place a one-shot sample as rhythmic accent — trigger on key beats for punctuation",
|
|
105
|
+
targets={"punch": 0.4, "groove": 0.3, "novelty": 0.3},
|
|
106
|
+
protect={"clarity": 0.6, "coherence": 0.5},
|
|
107
|
+
risk_level="low",
|
|
108
|
+
compile_plan=[
|
|
109
|
+
{"tool": "load_sample_to_simpler", "params": {"description": "Load one-shot into Simpler"}, "description": "Load one-shot", "backend": "bridge_command"},
|
|
110
|
+
{"tool": "set_simpler_playback_mode", "params": {"mode": "one_shot", "description": "One-shot mode for trigger playback"}, "description": "One-shot mode", "backend": "remote_command"},
|
|
111
|
+
{"tool": "crop_simpler", "params": {"description": "Tight crop around the transient"}, "description": "Crop to transient", "backend": "bridge_command"},
|
|
112
|
+
],
|
|
113
|
+
verification_plan=[
|
|
114
|
+
{"tool": "get_track_meters", "check": "one-shot triggers cleanly on beat", "backend": "remote_command"},
|
|
115
|
+
],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Register all sample moves
|
|
119
|
+
for _move in [
|
|
120
|
+
SAMPLE_CHOP_RHYTHM,
|
|
121
|
+
SAMPLE_TEXTURE_LAYER,
|
|
122
|
+
SAMPLE_VOCAL_GHOST,
|
|
123
|
+
SAMPLE_BREAK_LAYER,
|
|
124
|
+
SAMPLE_RESAMPLE_DESTROY,
|
|
125
|
+
SAMPLE_ONE_SHOT_ACCENT,
|
|
126
|
+
]:
|
|
127
|
+
register(_move)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""SamplePlanner — technique selection and plan compilation.
|
|
2
|
+
|
|
3
|
+
Pure computation. Selects the best technique for a given sample + intent,
|
|
4
|
+
then compiles it into a concrete sequence of MCP tool calls.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from .models import SampleProfile, SampleIntent, SampleTechnique
|
|
12
|
+
from .techniques import find_techniques, get_technique
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def select_technique(
|
|
16
|
+
profile: SampleProfile,
|
|
17
|
+
intent: SampleIntent,
|
|
18
|
+
taste_graph: object = None,
|
|
19
|
+
recent_techniques: Optional[list[str]] = None,
|
|
20
|
+
) -> Optional[SampleTechnique]:
|
|
21
|
+
"""Select the best technique for this sample + intent.
|
|
22
|
+
|
|
23
|
+
Scoring: material_match(0.3) + intent_match(0.3) + philosophy_match(0.2) +
|
|
24
|
+
novelty_bonus(0.1) + taste_fit(0.1)
|
|
25
|
+
"""
|
|
26
|
+
candidates = find_techniques(
|
|
27
|
+
material_type=profile.material_type,
|
|
28
|
+
intent=intent.intent_type,
|
|
29
|
+
philosophy=intent.philosophy if intent.philosophy != "auto" else None,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if not candidates:
|
|
33
|
+
# Broaden search — try without material filter
|
|
34
|
+
candidates = find_techniques(intent=intent.intent_type)
|
|
35
|
+
|
|
36
|
+
if not candidates:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
recent = set(recent_techniques or [])
|
|
40
|
+
|
|
41
|
+
scored: list[tuple[SampleTechnique, float]] = []
|
|
42
|
+
for t in candidates:
|
|
43
|
+
score = 0.0
|
|
44
|
+
|
|
45
|
+
# Material match
|
|
46
|
+
if profile.material_type in t.material_types:
|
|
47
|
+
score += 0.3
|
|
48
|
+
elif "any" in t.material_types or not t.material_types:
|
|
49
|
+
score += 0.15
|
|
50
|
+
|
|
51
|
+
# Intent match
|
|
52
|
+
if intent.intent_type in t.intents:
|
|
53
|
+
score += 0.3
|
|
54
|
+
elif any(i in t.intents for i in _related_intents(intent.intent_type)):
|
|
55
|
+
score += 0.15
|
|
56
|
+
|
|
57
|
+
# Philosophy match
|
|
58
|
+
if intent.philosophy == "auto" or intent.philosophy == t.philosophy or t.philosophy == "both":
|
|
59
|
+
score += 0.2
|
|
60
|
+
elif intent.philosophy != t.philosophy:
|
|
61
|
+
score += 0.05
|
|
62
|
+
|
|
63
|
+
# Novelty bonus
|
|
64
|
+
if t.technique_id not in recent:
|
|
65
|
+
score += 0.1
|
|
66
|
+
|
|
67
|
+
scored.append((t, score))
|
|
68
|
+
|
|
69
|
+
scored.sort(key=lambda x: -x[1])
|
|
70
|
+
return scored[0][0] if scored else None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _related_intents(intent_type: str) -> list[str]:
|
|
74
|
+
"""Get related intents for broader matching."""
|
|
75
|
+
relations = {
|
|
76
|
+
"rhythm": ["layer", "transform"],
|
|
77
|
+
"texture": ["atmosphere", "transform"],
|
|
78
|
+
"layer": ["melody", "rhythm"],
|
|
79
|
+
"melody": ["layer", "vocal"],
|
|
80
|
+
"vocal": ["melody", "texture"],
|
|
81
|
+
"atmosphere": ["texture"],
|
|
82
|
+
"transform": ["texture", "rhythm", "atmosphere"],
|
|
83
|
+
"challenge": ["transform"],
|
|
84
|
+
}
|
|
85
|
+
return relations.get(intent_type, [])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def compile_sample_plan(
|
|
89
|
+
profile: SampleProfile,
|
|
90
|
+
intent: SampleIntent,
|
|
91
|
+
target_track: Optional[int] = None,
|
|
92
|
+
technique: Optional[SampleTechnique] = None,
|
|
93
|
+
) -> list[dict]:
|
|
94
|
+
"""Compile a concrete tool-call plan for sample manipulation.
|
|
95
|
+
|
|
96
|
+
Returns list of {tool, params, description} dicts ready for execution.
|
|
97
|
+
"""
|
|
98
|
+
if technique is None:
|
|
99
|
+
technique = select_technique(profile, intent)
|
|
100
|
+
if technique is None:
|
|
101
|
+
return _fallback_plan(profile, intent, target_track)
|
|
102
|
+
|
|
103
|
+
plan: list[dict] = []
|
|
104
|
+
|
|
105
|
+
for step in technique.steps:
|
|
106
|
+
compiled_step = {
|
|
107
|
+
"tool": step.tool,
|
|
108
|
+
"params": _resolve_params(step.params, profile, intent, target_track),
|
|
109
|
+
"description": step.description,
|
|
110
|
+
}
|
|
111
|
+
if step.condition:
|
|
112
|
+
if not _evaluate_condition(step.condition, profile, intent):
|
|
113
|
+
continue
|
|
114
|
+
plan.append(compiled_step)
|
|
115
|
+
|
|
116
|
+
return plan
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _resolve_params(
|
|
120
|
+
params: dict,
|
|
121
|
+
profile: SampleProfile,
|
|
122
|
+
intent: SampleIntent,
|
|
123
|
+
target_track: Optional[int],
|
|
124
|
+
) -> dict:
|
|
125
|
+
"""Resolve template variables in technique step params."""
|
|
126
|
+
replacements = {
|
|
127
|
+
"{file_path}": profile.file_path,
|
|
128
|
+
"{track_index}": target_track if target_track is not None else 0,
|
|
129
|
+
"{material_type}": profile.material_type,
|
|
130
|
+
"{key}": profile.key or "",
|
|
131
|
+
"{bpm}": profile.bpm or 120.0,
|
|
132
|
+
"{name}": profile.name,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def _resolve_single(v, repl):
|
|
136
|
+
"""Resolve a single value against replacements."""
|
|
137
|
+
if isinstance(v, str):
|
|
138
|
+
# Exact template match — return the raw typed value
|
|
139
|
+
if v in repl:
|
|
140
|
+
return repl[v]
|
|
141
|
+
# Partial template substitution within a longer string
|
|
142
|
+
for template, value in repl.items():
|
|
143
|
+
v = v.replace(template, str(value))
|
|
144
|
+
return v
|
|
145
|
+
return v
|
|
146
|
+
|
|
147
|
+
resolved = {}
|
|
148
|
+
for k, v in params.items():
|
|
149
|
+
if isinstance(v, list):
|
|
150
|
+
resolved[k] = [_resolve_single(item, replacements) for item in v]
|
|
151
|
+
else:
|
|
152
|
+
resolved[k] = _resolve_single(v, replacements)
|
|
153
|
+
return resolved
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _evaluate_condition(condition: str, profile: SampleProfile,
|
|
157
|
+
intent: SampleIntent) -> bool:
|
|
158
|
+
"""Evaluate a simple condition string."""
|
|
159
|
+
if "material_type" in condition:
|
|
160
|
+
for mt in ("vocal", "drum_loop", "instrument_loop", "one_shot",
|
|
161
|
+
"texture", "foley", "fx", "full_mix"):
|
|
162
|
+
if f'material_type == "{mt}"' in condition:
|
|
163
|
+
return profile.material_type == mt
|
|
164
|
+
if "philosophy" in condition:
|
|
165
|
+
for p in ("surgeon", "alchemist"):
|
|
166
|
+
if f'philosophy == "{p}"' in condition:
|
|
167
|
+
return intent.philosophy == p
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _fallback_plan(
|
|
172
|
+
profile: SampleProfile,
|
|
173
|
+
intent: SampleIntent,
|
|
174
|
+
target_track: Optional[int],
|
|
175
|
+
) -> list[dict]:
|
|
176
|
+
"""Generic fallback when no technique matches."""
|
|
177
|
+
track = target_track if target_track is not None else 0
|
|
178
|
+
return [
|
|
179
|
+
{"tool": "load_sample_to_simpler",
|
|
180
|
+
"params": {"track_index": track, "file_path": profile.file_path},
|
|
181
|
+
"description": f"Load {profile.name} into Simpler"},
|
|
182
|
+
{"tool": "set_simpler_playback_mode",
|
|
183
|
+
"params": {"track_index": track, "device_index": 0,
|
|
184
|
+
"playback_mode": 2 if profile.suggested_mode == "slice" else 0},
|
|
185
|
+
"description": f"Set Simpler to {profile.suggested_mode} mode"},
|
|
186
|
+
]
|