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,442 @@
1
+ """Sample Engine MCP tools — 6 intelligence-layer tools.
2
+
3
+ No new Ableton communication — these orchestrate existing tools
4
+ through the analyzer, critics, planner, and technique library.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Optional
10
+
11
+ from fastmcp import Context
12
+
13
+ from ..server import mcp
14
+ from .models import SampleProfile, SampleIntent, SampleFitReport
15
+ from .analyzer import build_profile_from_filename
16
+ from .critics import run_all_sample_critics
17
+ from .planner import select_technique, compile_sample_plan
18
+ from .techniques import find_techniques, list_techniques, get_technique
19
+ from .sources import BrowserSource, FilesystemSource, SpliceSource, build_search_queries
20
+
21
+
22
+ @mcp.tool()
23
+ async def analyze_sample(
24
+ ctx: Context,
25
+ file_path: Optional[str] = None,
26
+ track_index: Optional[int] = None,
27
+ clip_index: Optional[int] = None,
28
+ ) -> dict:
29
+ """Analyze a sample and build a complete SampleProfile.
30
+
31
+ Detects material type, key, BPM, spectral character, and recommends
32
+ Simpler mode, slice method, and warp mode. Provide either file_path
33
+ OR track_index + clip_index to analyze a clip in the session.
34
+
35
+ Falls back to filename-only analysis if M4L bridge unavailable.
36
+ """
37
+ if file_path is None and track_index is None:
38
+ return {"error": "Provide either file_path or track_index + clip_index"}
39
+
40
+ if track_index is not None and file_path is None:
41
+ try:
42
+ bridge = ctx.lifespan_context.get("m4l")
43
+ if bridge:
44
+ result = await bridge.send_command(
45
+ "get_clip_file_path", track_index, clip_index or 0
46
+ )
47
+ if not result.get("error"):
48
+ file_path = result.get("file_path")
49
+ except Exception:
50
+ pass
51
+
52
+ if file_path is None:
53
+ return {"error": "Could not determine file path — provide file_path directly"}
54
+
55
+ source = "session_clip" if track_index is not None else "filesystem"
56
+ profile = build_profile_from_filename(file_path, source=source)
57
+ return profile.to_dict()
58
+
59
+
60
+ @mcp.tool()
61
+ def evaluate_sample_fit(
62
+ ctx: Context,
63
+ file_path: str,
64
+ intent: str = "layer",
65
+ philosophy: str = "auto",
66
+ ) -> dict:
67
+ """Run the 6-critic battery to evaluate how well a sample fits the current song.
68
+
69
+ Returns overall score, per-critic scores, recommendations, and
70
+ both surgeon (precise) and alchemist (transformative) plans.
71
+
72
+ intent: rhythm, texture, layer, melody, vocal, atmosphere, transform
73
+ philosophy: surgeon, alchemist, auto (context-decides)
74
+ """
75
+ profile = build_profile_from_filename(file_path)
76
+ sample_intent = SampleIntent(
77
+ intent_type=intent, philosophy=philosophy,
78
+ description=f"Evaluate fitness for {intent}",
79
+ )
80
+
81
+ # Gather song context
82
+ song_key = None
83
+ session_tempo = 120.0
84
+ existing_roles: list[str] = []
85
+
86
+ try:
87
+ ableton = ctx.lifespan_context["ableton"]
88
+ info = ableton.send_command("get_session_info", {})
89
+ session_tempo = info.get("tempo", 120.0)
90
+
91
+ # Get track names as roles
92
+ track_count = info.get("track_count", 0)
93
+ for i in range(min(track_count, 16)):
94
+ try:
95
+ track_info = ableton.send_command("get_track_info", {"track_index": i})
96
+ name = track_info.get("name", "").lower()
97
+ if name:
98
+ existing_roles.append(name)
99
+ except Exception:
100
+ continue
101
+
102
+ # Detect key from MIDI tracks
103
+ try:
104
+ from ..tools._theory_engine import detect_key
105
+ for i in range(min(track_count, 8)):
106
+ try:
107
+ clip_info = ableton.send_command("get_clip_info", {
108
+ "track_index": i, "clip_index": 0,
109
+ })
110
+ if clip_info.get("is_midi"):
111
+ notes_result = ableton.send_command("get_notes", {
112
+ "track_index": i, "clip_index": 0,
113
+ })
114
+ notes = notes_result.get("notes", [])
115
+ if notes:
116
+ key_result = detect_key(notes)
117
+ mode = key_result.get("mode", "")
118
+ mode_suffix = "m" if "minor" in mode else ""
119
+ song_key = f"{key_result['tonic_name']}{mode_suffix}"
120
+ break
121
+ except Exception:
122
+ continue
123
+ except ImportError:
124
+ pass
125
+ except Exception:
126
+ pass
127
+
128
+ critics = run_all_sample_critics(
129
+ profile=profile,
130
+ intent=sample_intent,
131
+ song_key=song_key,
132
+ session_tempo=session_tempo,
133
+ existing_roles=existing_roles,
134
+ )
135
+
136
+ # Build both plans
137
+ surgeon_plan = compile_sample_plan(
138
+ profile,
139
+ SampleIntent(intent_type=intent, philosophy="surgeon", description=""),
140
+ )
141
+ alchemist_plan = compile_sample_plan(
142
+ profile,
143
+ SampleIntent(intent_type=intent, philosophy="alchemist", description=""),
144
+ )
145
+
146
+ report = SampleFitReport(
147
+ sample=profile,
148
+ critics=critics,
149
+ recommended_intent=intent,
150
+ surgeon_plan=surgeon_plan,
151
+ alchemist_plan=alchemist_plan,
152
+ warnings=[c.recommendation for c in critics.values() if c.score < 0.5],
153
+ )
154
+ return report.to_dict()
155
+
156
+
157
+ @mcp.tool()
158
+ def search_samples(
159
+ ctx: Context,
160
+ query: str,
161
+ material_type: Optional[str] = None,
162
+ key: Optional[str] = None,
163
+ bpm_range: Optional[str] = None,
164
+ source: Optional[str] = None,
165
+ max_results: int = 10,
166
+ ) -> dict:
167
+ """Search for samples across Splice library, Ableton browser, and local filesystem.
168
+
169
+ Searches all enabled sources in parallel and ranks results.
170
+ Splice results include rich metadata (key, BPM, genre, tags, pack info).
171
+
172
+ query: search text like "dark vocal", "breakbeat", "foley metal"
173
+ material_type: filter by type (vocal, drum_loop, texture, etc.)
174
+ key: prefer samples in this key (e.g., "Cm", "F#")
175
+ bpm_range: "min-max" BPM range (e.g., "120-130")
176
+ source: "splice", "browser", "filesystem", or None for all
177
+ max_results: maximum results to return (default 10)
178
+ """
179
+ results: list[dict] = []
180
+
181
+ # Parse BPM range
182
+ bpm_min, bpm_max = None, None
183
+ if bpm_range:
184
+ parts = bpm_range.replace(" ", "").split("-")
185
+ if len(parts) == 2:
186
+ try:
187
+ bpm_min, bpm_max = float(parts[0]), float(parts[1])
188
+ except ValueError:
189
+ pass
190
+
191
+ # Splice search (richest metadata, searched first)
192
+ if source in (None, "splice"):
193
+ splice = SpliceSource()
194
+ if splice.enabled:
195
+ splice_results = splice.search(
196
+ query=query,
197
+ max_results=max_results,
198
+ key=key,
199
+ bpm_min=bpm_min,
200
+ bpm_max=bpm_max,
201
+ )
202
+ for candidate in splice_results:
203
+ d = candidate.to_dict()
204
+ d["source_priority"] = 1 # highest
205
+ results.append(d)
206
+
207
+ # Browser search
208
+ if source in (None, "browser"):
209
+ try:
210
+ ableton = ctx.lifespan_context["ableton"]
211
+ browser = BrowserSource()
212
+ for category in browser.DEFAULT_CATEGORIES:
213
+ try:
214
+ search_result = ableton.send_command("search_browser", {
215
+ "path": category,
216
+ "name_filter": query,
217
+ "loadable_only": True,
218
+ "max_results": max_results,
219
+ })
220
+ raw = search_result.get("results", [])
221
+ parsed = browser.parse_results(raw, category)
222
+ for candidate in parsed:
223
+ d = candidate.to_dict()
224
+ d["source_priority"] = 2
225
+ results.append(d)
226
+ except Exception:
227
+ continue
228
+ except Exception:
229
+ pass
230
+
231
+ # Filesystem search
232
+ if source in (None, "filesystem"):
233
+ fs = FilesystemSource(scan_paths=[
234
+ "~/Music", "~/Documents/Samples",
235
+ "~/Documents/LivePilot/downloads",
236
+ ])
237
+ fs_results = fs.search(query, max_results=max_results)
238
+ for candidate in fs_results:
239
+ d = candidate.to_dict()
240
+ d["source_priority"] = 3
241
+ results.append(d)
242
+
243
+ # Sort by source priority (Splice first), then by relevance
244
+ results.sort(key=lambda r: r.get("source_priority", 9))
245
+
246
+ # Build summary
247
+ source_counts = {}
248
+ for r in results:
249
+ src = r.get("source", "unknown")
250
+ source_counts[src] = source_counts.get(src, 0) + 1
251
+
252
+ return {
253
+ "query": query,
254
+ "result_count": len(results[:max_results]),
255
+ "source_counts": source_counts,
256
+ "results": results[:max_results],
257
+ }
258
+
259
+
260
+ @mcp.tool()
261
+ def suggest_sample_technique(
262
+ ctx: Context,
263
+ file_path: str,
264
+ intent: str = "rhythm",
265
+ philosophy: str = "auto",
266
+ max_suggestions: int = 3,
267
+ ) -> dict:
268
+ """Suggest sample manipulation techniques from the technique library.
269
+
270
+ Returns ranked techniques with executable step outlines for the
271
+ given sample + intent combination.
272
+
273
+ file_path: path to the sample
274
+ intent: rhythm, texture, layer, melody, vocal, atmosphere, transform, challenge
275
+ philosophy: surgeon, alchemist, auto
276
+ """
277
+ profile = build_profile_from_filename(file_path)
278
+ sample_intent = SampleIntent(
279
+ intent_type=intent, philosophy=philosophy, description="",
280
+ )
281
+
282
+ candidates = find_techniques(
283
+ material_type=profile.material_type,
284
+ intent=intent,
285
+ philosophy=philosophy if philosophy != "auto" else None,
286
+ )
287
+
288
+ if not candidates:
289
+ candidates = find_techniques(intent=intent)
290
+
291
+ suggestions = []
292
+ for t in candidates[:max_suggestions]:
293
+ steps = compile_sample_plan(profile, sample_intent, technique=t)
294
+ suggestions.append({
295
+ "technique_id": t.technique_id,
296
+ "name": t.name,
297
+ "philosophy": t.philosophy,
298
+ "difficulty": t.difficulty,
299
+ "description": t.description,
300
+ "inspiration": t.inspiration,
301
+ "step_count": len(steps),
302
+ "steps_preview": [s["description"] for s in steps[:5]],
303
+ })
304
+
305
+ return {
306
+ "sample": profile.name,
307
+ "material_type": profile.material_type,
308
+ "intent": intent,
309
+ "suggestion_count": len(suggestions),
310
+ "suggestions": suggestions,
311
+ }
312
+
313
+
314
+ @mcp.tool()
315
+ def plan_sample_workflow(
316
+ ctx: Context,
317
+ file_path: Optional[str] = None,
318
+ search_query: Optional[str] = None,
319
+ intent: str = "rhythm",
320
+ philosophy: str = "auto",
321
+ target_track: Optional[int] = None,
322
+ ) -> dict:
323
+ """Full end-to-end sample workflow: analyze, critique, select technique, compile plan.
324
+
325
+ Provide file_path for a known sample, or search_query to find one.
326
+ Returns a complete compiled plan ready for execution.
327
+
328
+ intent: rhythm, texture, layer, melody, vocal, atmosphere, transform
329
+ philosophy: surgeon, alchemist, auto
330
+ target_track: existing track index, or None for new track
331
+ """
332
+ if file_path is None and search_query is None:
333
+ return {"error": "Provide either file_path or search_query"}
334
+
335
+ profile = None
336
+ if file_path:
337
+ profile = build_profile_from_filename(file_path)
338
+
339
+ sample_intent = SampleIntent(
340
+ intent_type=intent, philosophy=philosophy,
341
+ description=search_query or f"Process {file_path} for {intent}",
342
+ target_track=target_track,
343
+ )
344
+
345
+ if profile is None:
346
+ # No file yet — return search guidance
347
+ queries = build_search_queries(search_query or "", material_type=None)
348
+ return {
349
+ "status": "search_needed",
350
+ "search_queries": queries,
351
+ "intent": intent,
352
+ "note": "Use search_samples to find a sample, then call again with file_path",
353
+ }
354
+
355
+ technique = select_technique(profile, sample_intent)
356
+ plan = compile_sample_plan(profile, sample_intent, target_track=target_track,
357
+ technique=technique)
358
+
359
+ return {
360
+ "sample": profile.to_dict(),
361
+ "intent": intent,
362
+ "philosophy": philosophy,
363
+ "technique": technique.name if technique else "fallback",
364
+ "technique_id": technique.technique_id if technique else "",
365
+ "step_count": len(plan),
366
+ "compiled_plan": plan,
367
+ }
368
+
369
+
370
+ @mcp.tool()
371
+ def get_sample_opportunities(ctx: Context) -> dict:
372
+ """Analyze current song and identify where samples could improve it.
373
+
374
+ Returns opportunities with suggested material types and techniques.
375
+ Used by Wonder Mode diagnosis for sample-aware creative rescue.
376
+ """
377
+ opportunities: list[dict] = []
378
+
379
+ try:
380
+ ableton = ctx.lifespan_context["ableton"]
381
+ info = ableton.send_command("get_session_info", {})
382
+ except Exception:
383
+ return {"opportunities": [], "note": "Cannot read session — Ableton not connected"}
384
+
385
+ track_count = info.get("track_count", 0)
386
+ track_names: list[str] = []
387
+ has_sampler = False
388
+
389
+ for i in range(min(track_count, 16)):
390
+ try:
391
+ track_info = ableton.send_command("get_track_info", {"track_index": i})
392
+ name = track_info.get("name", "").lower()
393
+ track_names.append(name)
394
+ devices = track_info.get("devices", [])
395
+ for d in devices:
396
+ if d.get("class_name") in ("OriginalSimpler", "MultiSampler"):
397
+ has_sampler = True
398
+ except Exception:
399
+ continue
400
+
401
+ # No organic texture
402
+ has_organic = any(
403
+ kw in name for name in track_names
404
+ for kw in ("vocal", "sample", "foley", "field", "organic", "found")
405
+ )
406
+ if not has_organic and track_count >= 3:
407
+ opportunities.append({
408
+ "type": "no_organic_texture",
409
+ "description": "No organic/sampled textures — all tracks appear synthesized",
410
+ "suggested_material": ["vocal", "foley", "texture"],
411
+ "suggested_techniques": ["vocal_chop_rhythm", "phone_recording_texture", "tail_harvest"],
412
+ "confidence": 0.6,
413
+ })
414
+
415
+ # Limited drum variety
416
+ drum_tracks = [n for n in track_names if any(
417
+ kw in n for kw in ("drum", "beat", "perc", "kick", "snare")
418
+ )]
419
+ if len(drum_tracks) <= 1 and track_count >= 4:
420
+ opportunities.append({
421
+ "type": "drum_variety_needed",
422
+ "description": "Limited percussion variety — layer a break or add ghost notes",
423
+ "suggested_material": ["drum_loop"],
424
+ "suggested_techniques": ["break_layering", "ghost_note_texture"],
425
+ "confidence": 0.5,
426
+ })
427
+
428
+ # No Simpler/Sampler devices
429
+ if not has_sampler and track_count >= 2:
430
+ opportunities.append({
431
+ "type": "no_sample_instruments",
432
+ "description": "No Simpler/Sampler devices — samples could add character",
433
+ "suggested_material": ["vocal", "instrument_loop", "one_shot"],
434
+ "suggested_techniques": ["syllable_instrument", "slice_and_sequence"],
435
+ "confidence": 0.4,
436
+ })
437
+
438
+ return {
439
+ "opportunity_count": len(opportunities),
440
+ "opportunities": opportunities,
441
+ "track_count": track_count,
442
+ }
@@ -5,9 +5,12 @@ from . import mix_moves # noqa: F401
5
5
  from . import transition_moves # noqa: F401
6
6
  from . import sound_design_moves # noqa: F401
7
7
  from . import performance_moves # noqa: F401
8
+ from . import device_creation_moves # noqa: F401
9
+ from ..sample_engine import moves as sample_moves # noqa: F401
8
10
 
9
11
  # Import compilers to auto-register them
10
12
  from . import mix_compilers # noqa: F401
11
13
  from . import transition_compilers # noqa: F401
12
14
  from . import sound_design_compilers # noqa: F401
13
15
  from . import performance_compilers # noqa: F401
16
+ from . import sample_compilers # noqa: F401
@@ -0,0 +1,237 @@
1
+ """Device-creation-domain semantic moves — generate custom M4L devices.
2
+
3
+ These moves create new instruments and effects on the fly using the
4
+ Device Forge. Each move specifies a gen~ template and parameters.
5
+ """
6
+
7
+ from .models import SemanticMove
8
+ from .registry import register
9
+
10
+ CREATE_CHAOS_MODULATOR = SemanticMove(
11
+ move_id="create_chaos_modulator",
12
+ family="device_creation",
13
+ intent="Generate a Lorenz attractor M4L device for chaotic modulation — "
14
+ "smooth but unpredictable parameter movement",
15
+ targets={"novelty": 0.8, "motion": 0.6, "surprise": 0.7},
16
+ protect={"clarity": 0.5},
17
+ risk_level="medium",
18
+ compile_plan=[
19
+ {
20
+ "tool": "generate_m4l_effect",
21
+ "params": {
22
+ "name": "Wonder Chaos Mod",
23
+ "device_type": "audio_effect",
24
+ "description": "Lorenz attractor chaotic modulation source",
25
+ },
26
+ "description": "Generate chaos modulator M4L device",
27
+ "backend": "mcp_tool",
28
+ },
29
+ {
30
+ "tool": "find_and_load_device",
31
+ "params": {"query": "Wonder Chaos Mod"},
32
+ "description": "Load generated device onto target track",
33
+ "backend": "remote_command",
34
+ },
35
+ ],
36
+ verification_plan=[
37
+ {"tool": "get_track_meters", "check": "track producing audio", "backend": "remote_command"},
38
+ ],
39
+ )
40
+
41
+ CREATE_FEEDBACK_RESONATOR = SemanticMove(
42
+ move_id="create_feedback_resonator",
43
+ family="device_creation",
44
+ intent="Generate a tuned comb resonator M4L device — feeds harmonic resonance "
45
+ "at a specific frequency into the signal",
46
+ targets={"depth": 0.6, "texture": 0.5, "novelty": 0.4},
47
+ protect={"clarity": 0.6, "punch": 0.5},
48
+ risk_level="low",
49
+ compile_plan=[
50
+ {
51
+ "tool": "generate_m4l_effect",
52
+ "params": {
53
+ "name": "Wonder Resonator",
54
+ "device_type": "audio_effect",
55
+ "description": "Tuned comb filter resonator",
56
+ },
57
+ "description": "Generate resonator M4L device",
58
+ "backend": "mcp_tool",
59
+ },
60
+ {
61
+ "tool": "find_and_load_device",
62
+ "params": {"query": "Wonder Resonator"},
63
+ "description": "Load resonator onto target track",
64
+ "backend": "remote_command",
65
+ },
66
+ ],
67
+ verification_plan=[
68
+ {"tool": "get_track_meters", "check": "track producing audio", "backend": "remote_command"},
69
+ ],
70
+ )
71
+
72
+ CREATE_WAVEFOLDER_EFFECT = SemanticMove(
73
+ move_id="create_wavefolder_effect",
74
+ family="device_creation",
75
+ intent="Generate a Buchla-style wavefolder M4L effect — rich harmonic series "
76
+ "from waveform folding, great for adding edge and complexity",
77
+ targets={"edge": 0.6, "novelty": 0.5, "energy": 0.4},
78
+ protect={"warmth": 0.5},
79
+ risk_level="medium",
80
+ compile_plan=[
81
+ {
82
+ "tool": "generate_m4l_effect",
83
+ "params": {
84
+ "name": "Wonder Wavefolder",
85
+ "device_type": "audio_effect",
86
+ "description": "Buchla-style harmonic wavefolder",
87
+ },
88
+ "description": "Generate wavefolder M4L device",
89
+ "backend": "mcp_tool",
90
+ },
91
+ {
92
+ "tool": "find_and_load_device",
93
+ "params": {"query": "Wonder Wavefolder"},
94
+ "description": "Load wavefolder onto target track",
95
+ "backend": "remote_command",
96
+ },
97
+ ],
98
+ verification_plan=[
99
+ {"tool": "get_track_meters", "check": "track producing audio with harmonic content", "backend": "remote_command"},
100
+ ],
101
+ )
102
+
103
+ CREATE_BITCRUSHER_EFFECT = SemanticMove(
104
+ move_id="create_bitcrusher_effect",
105
+ family="device_creation",
106
+ intent="Generate a bitcrusher M4L effect — sample-rate and bit-depth reduction "
107
+ "from subtle aliasing to full digital destruction",
108
+ targets={"edge": 0.5, "novelty": 0.4, "texture": 0.4},
109
+ protect={"clarity": 0.4},
110
+ risk_level="low",
111
+ compile_plan=[
112
+ {
113
+ "tool": "generate_m4l_effect",
114
+ "params": {
115
+ "name": "Wonder Bitcrusher",
116
+ "device_type": "audio_effect",
117
+ "description": "Sample-rate and bit-depth crusher",
118
+ },
119
+ "description": "Generate bitcrusher M4L device",
120
+ "backend": "mcp_tool",
121
+ },
122
+ {
123
+ "tool": "find_and_load_device",
124
+ "params": {"query": "Wonder Bitcrusher"},
125
+ "description": "Load bitcrusher onto target track",
126
+ "backend": "remote_command",
127
+ },
128
+ ],
129
+ verification_plan=[
130
+ {"tool": "get_track_meters", "check": "track producing audio", "backend": "remote_command"},
131
+ ],
132
+ )
133
+
134
+ CREATE_KARPLUS_STRING = SemanticMove(
135
+ move_id="create_karplus_string",
136
+ family="device_creation",
137
+ intent="Generate a Karplus-Strong string synth M4L instrument — plucked string "
138
+ "physical model with excitation input",
139
+ targets={"novelty": 0.5, "texture": 0.6, "depth": 0.4},
140
+ protect={"clarity": 0.6},
141
+ risk_level="low",
142
+ compile_plan=[
143
+ {
144
+ "tool": "generate_m4l_effect",
145
+ "params": {
146
+ "name": "Wonder String",
147
+ "device_type": "audio_effect",
148
+ "description": "Karplus-Strong plucked string physical model",
149
+ },
150
+ "description": "Generate string synth M4L device",
151
+ "backend": "mcp_tool",
152
+ },
153
+ {
154
+ "tool": "find_and_load_device",
155
+ "params": {"query": "Wonder String"},
156
+ "description": "Load string synth onto target track",
157
+ "backend": "remote_command",
158
+ },
159
+ ],
160
+ verification_plan=[
161
+ {"tool": "get_track_meters", "check": "track producing audio", "backend": "remote_command"},
162
+ ],
163
+ )
164
+
165
+ CREATE_STOCHASTIC_TEXTURE = SemanticMove(
166
+ move_id="create_stochastic_texture",
167
+ family="device_creation",
168
+ intent="Generate a stochastic resonance M4L effect — noise + threshold + feedback "
169
+ "creates granular textures and organic movement",
170
+ targets={"texture": 0.7, "novelty": 0.6, "motion": 0.5},
171
+ protect={"clarity": 0.4},
172
+ risk_level="medium",
173
+ compile_plan=[
174
+ {
175
+ "tool": "generate_m4l_effect",
176
+ "params": {
177
+ "name": "Wonder Stochastic",
178
+ "device_type": "audio_effect",
179
+ "description": "Stochastic resonance texture generator",
180
+ },
181
+ "description": "Generate stochastic texture M4L device",
182
+ "backend": "mcp_tool",
183
+ },
184
+ {
185
+ "tool": "find_and_load_device",
186
+ "params": {"query": "Wonder Stochastic"},
187
+ "description": "Load stochastic texture device onto target track",
188
+ "backend": "remote_command",
189
+ },
190
+ ],
191
+ verification_plan=[
192
+ {"tool": "get_track_meters", "check": "track producing audio", "backend": "remote_command"},
193
+ ],
194
+ )
195
+
196
+ CREATE_FDN_REVERB = SemanticMove(
197
+ move_id="create_fdn_reverb",
198
+ family="device_creation",
199
+ intent="Generate a feedback delay network M4L effect — dense, diffuse reverb "
200
+ "with Hadamard-like cross-coupling. Tune delay times for harmonic reverb.",
201
+ targets={"depth": 0.7, "width": 0.5, "novelty": 0.4},
202
+ protect={"punch": 0.5, "clarity": 0.5},
203
+ risk_level="low",
204
+ compile_plan=[
205
+ {
206
+ "tool": "generate_m4l_effect",
207
+ "params": {
208
+ "name": "Wonder FDN Verb",
209
+ "device_type": "audio_effect",
210
+ "description": "Feedback delay network reverb",
211
+ },
212
+ "description": "Generate FDN reverb M4L device",
213
+ "backend": "mcp_tool",
214
+ },
215
+ {
216
+ "tool": "find_and_load_device",
217
+ "params": {"query": "Wonder FDN Verb"},
218
+ "description": "Load FDN reverb onto target track",
219
+ "backend": "remote_command",
220
+ },
221
+ ],
222
+ verification_plan=[
223
+ {"tool": "get_track_meters", "check": "track producing audio with reverb tail", "backend": "remote_command"},
224
+ ],
225
+ )
226
+
227
+ # Register all device creation moves
228
+ for _move in [
229
+ CREATE_CHAOS_MODULATOR,
230
+ CREATE_FEEDBACK_RESONATOR,
231
+ CREATE_WAVEFOLDER_EFFECT,
232
+ CREATE_BITCRUSHER_EFFECT,
233
+ CREATE_KARPLUS_STRING,
234
+ CREATE_STOCHASTIC_TEXTURE,
235
+ CREATE_FDN_REVERB,
236
+ ]:
237
+ register(_move)