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.
Files changed (191) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +3 -3
  3. package/CHANGELOG.md +119 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +144 -13
  6. package/bin/livepilot.js +87 -0
  7. package/installer/codex.js +147 -0
  8. package/livepilot/.Codex-plugin/plugin.json +2 -2
  9. package/livepilot/.claude-plugin/plugin.json +2 -2
  10. package/livepilot/skills/livepilot-core/SKILL.md +21 -4
  11. package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
  12. package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
  13. package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
  14. package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
  15. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
  16. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
  17. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
  18. package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
  19. package/livepilot/skills/livepilot-core/references/overview.md +13 -9
  20. package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
  21. package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
  22. package/livepilot/skills/livepilot-devices/SKILL.md +16 -2
  23. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  24. package/livepilot/skills/livepilot-release/SKILL.md +19 -5
  25. package/livepilot/skills/livepilot-sample-engine/SKILL.md +104 -0
  26. package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
  27. package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
  28. package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
  29. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +45 -0
  30. package/livepilot/skills/livepilot-wonder/SKILL.md +15 -0
  31. package/livepilot.mcpb +0 -0
  32. package/m4l_device/livepilot_bridge.js +1 -1
  33. package/manifest.json +2 -2
  34. package/mcp_server/__init__.py +1 -1
  35. package/mcp_server/atlas/__init__.py +357 -0
  36. package/mcp_server/atlas/device_atlas.json +44067 -0
  37. package/mcp_server/atlas/enrichments/__init__.py +111 -0
  38. package/mcp_server/atlas/enrichments/audio_effects/auto_filter.yaml +162 -0
  39. package/mcp_server/atlas/enrichments/audio_effects/beat_repeat.yaml +183 -0
  40. package/mcp_server/atlas/enrichments/audio_effects/channel_eq.yaml +126 -0
  41. package/mcp_server/atlas/enrichments/audio_effects/chorus_ensemble.yaml +149 -0
  42. package/mcp_server/atlas/enrichments/audio_effects/color_limiter.yaml +109 -0
  43. package/mcp_server/atlas/enrichments/audio_effects/compressor.yaml +159 -0
  44. package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb.yaml +143 -0
  45. package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb_pro.yaml +178 -0
  46. package/mcp_server/atlas/enrichments/audio_effects/delay.yaml +151 -0
  47. package/mcp_server/atlas/enrichments/audio_effects/drum_buss.yaml +142 -0
  48. package/mcp_server/atlas/enrichments/audio_effects/dynamic_tube.yaml +147 -0
  49. package/mcp_server/atlas/enrichments/audio_effects/echo.yaml +167 -0
  50. package/mcp_server/atlas/enrichments/audio_effects/eq_eight.yaml +148 -0
  51. package/mcp_server/atlas/enrichments/audio_effects/eq_three.yaml +121 -0
  52. package/mcp_server/atlas/enrichments/audio_effects/erosion.yaml +103 -0
  53. package/mcp_server/atlas/enrichments/audio_effects/filter_delay.yaml +173 -0
  54. package/mcp_server/atlas/enrichments/audio_effects/gate.yaml +130 -0
  55. package/mcp_server/atlas/enrichments/audio_effects/gated_delay.yaml +133 -0
  56. package/mcp_server/atlas/enrichments/audio_effects/glue_compressor.yaml +142 -0
  57. package/mcp_server/atlas/enrichments/audio_effects/grain_delay.yaml +141 -0
  58. package/mcp_server/atlas/enrichments/audio_effects/hybrid_reverb.yaml +160 -0
  59. package/mcp_server/atlas/enrichments/audio_effects/limiter.yaml +97 -0
  60. package/mcp_server/atlas/enrichments/audio_effects/multiband_dynamics.yaml +174 -0
  61. package/mcp_server/atlas/enrichments/audio_effects/overdrive.yaml +119 -0
  62. package/mcp_server/atlas/enrichments/audio_effects/pedal.yaml +145 -0
  63. package/mcp_server/atlas/enrichments/audio_effects/phaser_flanger.yaml +161 -0
  64. package/mcp_server/atlas/enrichments/audio_effects/redux.yaml +114 -0
  65. package/mcp_server/atlas/enrichments/audio_effects/reverb.yaml +190 -0
  66. package/mcp_server/atlas/enrichments/audio_effects/roar.yaml +159 -0
  67. package/mcp_server/atlas/enrichments/audio_effects/saturator.yaml +146 -0
  68. package/mcp_server/atlas/enrichments/audio_effects/shifter.yaml +154 -0
  69. package/mcp_server/atlas/enrichments/audio_effects/spectral_resonator.yaml +141 -0
  70. package/mcp_server/atlas/enrichments/audio_effects/spectral_time.yaml +164 -0
  71. package/mcp_server/atlas/enrichments/audio_effects/vector_delay.yaml +140 -0
  72. package/mcp_server/atlas/enrichments/audio_effects/vinyl_distortion.yaml +141 -0
  73. package/mcp_server/atlas/enrichments/instruments/analog.yaml +222 -0
  74. package/mcp_server/atlas/enrichments/instruments/bass.yaml +202 -0
  75. package/mcp_server/atlas/enrichments/instruments/collision.yaml +150 -0
  76. package/mcp_server/atlas/enrichments/instruments/drift.yaml +167 -0
  77. package/mcp_server/atlas/enrichments/instruments/electric.yaml +137 -0
  78. package/mcp_server/atlas/enrichments/instruments/emit.yaml +163 -0
  79. package/mcp_server/atlas/enrichments/instruments/meld.yaml +164 -0
  80. package/mcp_server/atlas/enrichments/instruments/operator.yaml +197 -0
  81. package/mcp_server/atlas/enrichments/instruments/poli.yaml +192 -0
  82. package/mcp_server/atlas/enrichments/instruments/sampler.yaml +218 -0
  83. package/mcp_server/atlas/enrichments/instruments/simpler.yaml +217 -0
  84. package/mcp_server/atlas/enrichments/instruments/tension.yaml +156 -0
  85. package/mcp_server/atlas/enrichments/instruments/tree_tone.yaml +162 -0
  86. package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +165 -0
  87. package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +166 -0
  88. package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +162 -0
  89. package/mcp_server/atlas/enrichments/midi_effects/arpeggiator.yaml +156 -0
  90. package/mcp_server/atlas/enrichments/midi_effects/bouncy_notes.yaml +93 -0
  91. package/mcp_server/atlas/enrichments/midi_effects/chord.yaml +147 -0
  92. package/mcp_server/atlas/enrichments/midi_effects/melodic_steps.yaml +97 -0
  93. package/mcp_server/atlas/enrichments/midi_effects/note_echo.yaml +108 -0
  94. package/mcp_server/atlas/enrichments/midi_effects/note_length.yaml +97 -0
  95. package/mcp_server/atlas/enrichments/midi_effects/pitch.yaml +76 -0
  96. package/mcp_server/atlas/enrichments/midi_effects/random.yaml +117 -0
  97. package/mcp_server/atlas/enrichments/midi_effects/rhythmic_steps.yaml +103 -0
  98. package/mcp_server/atlas/enrichments/midi_effects/scale.yaml +83 -0
  99. package/mcp_server/atlas/enrichments/midi_effects/step_arp.yaml +112 -0
  100. package/mcp_server/atlas/enrichments/midi_effects/velocity.yaml +119 -0
  101. package/mcp_server/atlas/enrichments/utility/amp.yaml +159 -0
  102. package/mcp_server/atlas/enrichments/utility/cabinet.yaml +109 -0
  103. package/mcp_server/atlas/enrichments/utility/corpus.yaml +150 -0
  104. package/mcp_server/atlas/enrichments/utility/resonators.yaml +131 -0
  105. package/mcp_server/atlas/enrichments/utility/spectrum.yaml +63 -0
  106. package/mcp_server/atlas/enrichments/utility/tuner.yaml +51 -0
  107. package/mcp_server/atlas/enrichments/utility/utility.yaml +136 -0
  108. package/mcp_server/atlas/enrichments/utility/vocoder.yaml +160 -0
  109. package/mcp_server/atlas/scanner.py +236 -0
  110. package/mcp_server/atlas/tools.py +224 -0
  111. package/mcp_server/composer/__init__.py +1 -0
  112. package/mcp_server/composer/engine.py +452 -0
  113. package/mcp_server/composer/layer_planner.py +427 -0
  114. package/mcp_server/composer/prompt_parser.py +329 -0
  115. package/mcp_server/composer/tools.py +201 -0
  116. package/mcp_server/connection.py +53 -8
  117. package/mcp_server/corpus/__init__.py +377 -0
  118. package/mcp_server/device_forge/__init__.py +1 -0
  119. package/mcp_server/device_forge/builder.py +377 -0
  120. package/mcp_server/device_forge/models.py +142 -0
  121. package/mcp_server/device_forge/templates.py +483 -0
  122. package/mcp_server/device_forge/tools.py +162 -0
  123. package/mcp_server/hook_hunter/analyzer.py +23 -0
  124. package/mcp_server/hook_hunter/models.py +1 -0
  125. package/mcp_server/hook_hunter/tools.py +4 -2
  126. package/mcp_server/m4l_bridge.py +1 -0
  127. package/mcp_server/memory/taste_graph.py +68 -1
  128. package/mcp_server/memory/tools.py +15 -4
  129. package/mcp_server/musical_intelligence/detectors.py +14 -1
  130. package/mcp_server/musical_intelligence/tools.py +11 -8
  131. package/mcp_server/persistence/__init__.py +1 -0
  132. package/mcp_server/persistence/base_store.py +82 -0
  133. package/mcp_server/persistence/project_store.py +106 -0
  134. package/mcp_server/persistence/taste_store.py +122 -0
  135. package/mcp_server/preview_studio/models.py +1 -0
  136. package/mcp_server/preview_studio/tools.py +56 -13
  137. package/mcp_server/runtime/capability.py +66 -0
  138. package/mcp_server/runtime/capability_probe.py +137 -0
  139. package/mcp_server/runtime/execution_router.py +143 -0
  140. package/mcp_server/runtime/live_version.py +102 -0
  141. package/mcp_server/runtime/remote_commands.py +87 -0
  142. package/mcp_server/runtime/tools.py +18 -4
  143. package/mcp_server/sample_engine/__init__.py +1 -0
  144. package/mcp_server/sample_engine/analyzer.py +216 -0
  145. package/mcp_server/sample_engine/critics.py +390 -0
  146. package/mcp_server/sample_engine/models.py +193 -0
  147. package/mcp_server/sample_engine/moves.py +127 -0
  148. package/mcp_server/sample_engine/planner.py +186 -0
  149. package/mcp_server/sample_engine/sources.py +540 -0
  150. package/mcp_server/sample_engine/techniques.py +908 -0
  151. package/mcp_server/sample_engine/tools.py +442 -0
  152. package/mcp_server/semantic_moves/__init__.py +3 -0
  153. package/mcp_server/semantic_moves/device_creation_moves.py +237 -0
  154. package/mcp_server/semantic_moves/mix_moves.py +41 -41
  155. package/mcp_server/semantic_moves/performance_moves.py +13 -13
  156. package/mcp_server/semantic_moves/sample_compilers.py +372 -0
  157. package/mcp_server/semantic_moves/sound_design_moves.py +15 -15
  158. package/mcp_server/semantic_moves/tools.py +18 -17
  159. package/mcp_server/semantic_moves/transition_moves.py +16 -16
  160. package/mcp_server/server.py +51 -0
  161. package/mcp_server/services/__init__.py +1 -0
  162. package/mcp_server/services/motif_service.py +67 -0
  163. package/mcp_server/session_continuity/tracker.py +29 -1
  164. package/mcp_server/song_brain/builder.py +28 -1
  165. package/mcp_server/song_brain/models.py +4 -0
  166. package/mcp_server/song_brain/tools.py +20 -2
  167. package/mcp_server/sound_design/critics.py +89 -1
  168. package/mcp_server/splice_client/__init__.py +1 -0
  169. package/mcp_server/splice_client/client.py +347 -0
  170. package/mcp_server/splice_client/models.py +96 -0
  171. package/mcp_server/splice_client/protos/__init__.py +1 -0
  172. package/mcp_server/splice_client/protos/app_pb2.py +319 -0
  173. package/mcp_server/splice_client/protos/app_pb2.pyi +1153 -0
  174. package/mcp_server/splice_client/protos/app_pb2_grpc.py +1946 -0
  175. package/mcp_server/tools/arrangement.py +69 -0
  176. package/mcp_server/tools/automation.py +15 -2
  177. package/mcp_server/tools/devices.py +117 -6
  178. package/mcp_server/tools/notes.py +37 -4
  179. package/mcp_server/wonder_mode/diagnosis.py +5 -0
  180. package/mcp_server/wonder_mode/engine.py +85 -1
  181. package/mcp_server/wonder_mode/tools.py +6 -1
  182. package/package.json +12 -2
  183. package/remote_script/LivePilot/__init__.py +8 -1
  184. package/remote_script/LivePilot/arrangement.py +114 -0
  185. package/remote_script/LivePilot/browser.py +56 -1
  186. package/remote_script/LivePilot/devices.py +236 -6
  187. package/remote_script/LivePilot/mixing.py +8 -3
  188. package/remote_script/LivePilot/server.py +5 -1
  189. package/remote_script/LivePilot/transport.py +3 -0
  190. package/remote_script/LivePilot/version_detect.py +78 -0
  191. 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
+ ]