livepilot 1.9.24 → 1.10.1

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 (185) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +3 -3
  3. package/CHANGELOG.md +223 -0
  4. package/CONTRIBUTING.md +2 -2
  5. package/LICENSE +62 -21
  6. package/README.md +291 -276
  7. package/bin/livepilot.js +87 -0
  8. package/installer/codex.js +147 -0
  9. package/livepilot/.Codex-plugin/plugin.json +2 -2
  10. package/livepilot/.claude-plugin/plugin.json +2 -2
  11. package/livepilot/skills/livepilot-arrangement/SKILL.md +18 -1
  12. package/livepilot/skills/livepilot-core/SKILL.md +22 -5
  13. package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
  14. package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
  15. package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
  16. package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
  17. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
  18. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
  19. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
  20. package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
  21. package/livepilot/skills/livepilot-core/references/overview.md +13 -9
  22. package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
  23. package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
  24. package/livepilot/skills/livepilot-devices/SKILL.md +39 -4
  25. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  26. package/livepilot/skills/livepilot-release/SKILL.md +23 -19
  27. package/livepilot/skills/livepilot-sample-engine/SKILL.md +105 -0
  28. package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
  29. package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
  30. package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
  31. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +45 -0
  32. package/livepilot/skills/livepilot-wonder/SKILL.md +17 -0
  33. package/livepilot.mcpb +0 -0
  34. package/m4l_device/livepilot_bridge.js +1 -1
  35. package/manifest.json +4 -4
  36. package/mcp_server/__init__.py +1 -1
  37. package/mcp_server/atlas/__init__.py +357 -0
  38. package/mcp_server/atlas/device_atlas.json +44067 -0
  39. package/mcp_server/atlas/enrichments/__init__.py +111 -0
  40. package/mcp_server/atlas/enrichments/audio_effects/auto_filter.yaml +162 -0
  41. package/mcp_server/atlas/enrichments/audio_effects/beat_repeat.yaml +183 -0
  42. package/mcp_server/atlas/enrichments/audio_effects/channel_eq.yaml +126 -0
  43. package/mcp_server/atlas/enrichments/audio_effects/chorus_ensemble.yaml +149 -0
  44. package/mcp_server/atlas/enrichments/audio_effects/color_limiter.yaml +109 -0
  45. package/mcp_server/atlas/enrichments/audio_effects/compressor.yaml +159 -0
  46. package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb.yaml +143 -0
  47. package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb_pro.yaml +178 -0
  48. package/mcp_server/atlas/enrichments/audio_effects/delay.yaml +151 -0
  49. package/mcp_server/atlas/enrichments/audio_effects/drum_buss.yaml +142 -0
  50. package/mcp_server/atlas/enrichments/audio_effects/dynamic_tube.yaml +147 -0
  51. package/mcp_server/atlas/enrichments/audio_effects/echo.yaml +167 -0
  52. package/mcp_server/atlas/enrichments/audio_effects/eq_eight.yaml +148 -0
  53. package/mcp_server/atlas/enrichments/audio_effects/eq_three.yaml +121 -0
  54. package/mcp_server/atlas/enrichments/audio_effects/erosion.yaml +103 -0
  55. package/mcp_server/atlas/enrichments/audio_effects/filter_delay.yaml +173 -0
  56. package/mcp_server/atlas/enrichments/audio_effects/gate.yaml +130 -0
  57. package/mcp_server/atlas/enrichments/audio_effects/gated_delay.yaml +133 -0
  58. package/mcp_server/atlas/enrichments/audio_effects/glue_compressor.yaml +142 -0
  59. package/mcp_server/atlas/enrichments/audio_effects/grain_delay.yaml +141 -0
  60. package/mcp_server/atlas/enrichments/audio_effects/hybrid_reverb.yaml +160 -0
  61. package/mcp_server/atlas/enrichments/audio_effects/limiter.yaml +97 -0
  62. package/mcp_server/atlas/enrichments/audio_effects/multiband_dynamics.yaml +174 -0
  63. package/mcp_server/atlas/enrichments/audio_effects/overdrive.yaml +119 -0
  64. package/mcp_server/atlas/enrichments/audio_effects/pedal.yaml +145 -0
  65. package/mcp_server/atlas/enrichments/audio_effects/phaser_flanger.yaml +161 -0
  66. package/mcp_server/atlas/enrichments/audio_effects/redux.yaml +114 -0
  67. package/mcp_server/atlas/enrichments/audio_effects/reverb.yaml +190 -0
  68. package/mcp_server/atlas/enrichments/audio_effects/roar.yaml +159 -0
  69. package/mcp_server/atlas/enrichments/audio_effects/saturator.yaml +146 -0
  70. package/mcp_server/atlas/enrichments/audio_effects/shifter.yaml +154 -0
  71. package/mcp_server/atlas/enrichments/audio_effects/spectral_resonator.yaml +141 -0
  72. package/mcp_server/atlas/enrichments/audio_effects/spectral_time.yaml +164 -0
  73. package/mcp_server/atlas/enrichments/audio_effects/vector_delay.yaml +140 -0
  74. package/mcp_server/atlas/enrichments/audio_effects/vinyl_distortion.yaml +141 -0
  75. package/mcp_server/atlas/enrichments/instruments/analog.yaml +222 -0
  76. package/mcp_server/atlas/enrichments/instruments/bass.yaml +202 -0
  77. package/mcp_server/atlas/enrichments/instruments/collision.yaml +150 -0
  78. package/mcp_server/atlas/enrichments/instruments/drift.yaml +167 -0
  79. package/mcp_server/atlas/enrichments/instruments/electric.yaml +137 -0
  80. package/mcp_server/atlas/enrichments/instruments/emit.yaml +163 -0
  81. package/mcp_server/atlas/enrichments/instruments/meld.yaml +164 -0
  82. package/mcp_server/atlas/enrichments/instruments/operator.yaml +197 -0
  83. package/mcp_server/atlas/enrichments/instruments/poli.yaml +192 -0
  84. package/mcp_server/atlas/enrichments/instruments/sampler.yaml +218 -0
  85. package/mcp_server/atlas/enrichments/instruments/simpler.yaml +217 -0
  86. package/mcp_server/atlas/enrichments/instruments/tension.yaml +156 -0
  87. package/mcp_server/atlas/enrichments/instruments/tree_tone.yaml +162 -0
  88. package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +165 -0
  89. package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +166 -0
  90. package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +162 -0
  91. package/mcp_server/atlas/enrichments/midi_effects/arpeggiator.yaml +156 -0
  92. package/mcp_server/atlas/enrichments/midi_effects/bouncy_notes.yaml +93 -0
  93. package/mcp_server/atlas/enrichments/midi_effects/chord.yaml +147 -0
  94. package/mcp_server/atlas/enrichments/midi_effects/melodic_steps.yaml +97 -0
  95. package/mcp_server/atlas/enrichments/midi_effects/note_echo.yaml +108 -0
  96. package/mcp_server/atlas/enrichments/midi_effects/note_length.yaml +97 -0
  97. package/mcp_server/atlas/enrichments/midi_effects/pitch.yaml +76 -0
  98. package/mcp_server/atlas/enrichments/midi_effects/random.yaml +117 -0
  99. package/mcp_server/atlas/enrichments/midi_effects/rhythmic_steps.yaml +103 -0
  100. package/mcp_server/atlas/enrichments/midi_effects/scale.yaml +83 -0
  101. package/mcp_server/atlas/enrichments/midi_effects/step_arp.yaml +112 -0
  102. package/mcp_server/atlas/enrichments/midi_effects/velocity.yaml +119 -0
  103. package/mcp_server/atlas/enrichments/utility/amp.yaml +159 -0
  104. package/mcp_server/atlas/enrichments/utility/cabinet.yaml +109 -0
  105. package/mcp_server/atlas/enrichments/utility/corpus.yaml +150 -0
  106. package/mcp_server/atlas/enrichments/utility/resonators.yaml +131 -0
  107. package/mcp_server/atlas/enrichments/utility/spectrum.yaml +63 -0
  108. package/mcp_server/atlas/enrichments/utility/tuner.yaml +51 -0
  109. package/mcp_server/atlas/enrichments/utility/utility.yaml +136 -0
  110. package/mcp_server/atlas/enrichments/utility/vocoder.yaml +160 -0
  111. package/mcp_server/atlas/scanner.py +236 -0
  112. package/mcp_server/atlas/tools.py +224 -0
  113. package/mcp_server/composer/__init__.py +1 -0
  114. package/mcp_server/composer/engine.py +532 -0
  115. package/mcp_server/composer/layer_planner.py +427 -0
  116. package/mcp_server/composer/prompt_parser.py +329 -0
  117. package/mcp_server/composer/sample_resolver.py +153 -0
  118. package/mcp_server/composer/tools.py +211 -0
  119. package/mcp_server/connection.py +53 -8
  120. package/mcp_server/corpus/__init__.py +377 -0
  121. package/mcp_server/device_forge/__init__.py +1 -0
  122. package/mcp_server/device_forge/builder.py +377 -0
  123. package/mcp_server/device_forge/models.py +142 -0
  124. package/mcp_server/device_forge/templates.py +483 -0
  125. package/mcp_server/device_forge/tools.py +162 -0
  126. package/mcp_server/m4l_bridge.py +1 -0
  127. package/mcp_server/memory/taste_accessors.py +47 -0
  128. package/mcp_server/preview_studio/engine.py +9 -2
  129. package/mcp_server/preview_studio/tools.py +78 -35
  130. package/mcp_server/project_brain/tools.py +34 -0
  131. package/mcp_server/runtime/capability_probe.py +21 -2
  132. package/mcp_server/runtime/execution_router.py +184 -38
  133. package/mcp_server/runtime/live_version.py +102 -0
  134. package/mcp_server/runtime/mcp_dispatch.py +46 -0
  135. package/mcp_server/runtime/remote_commands.py +13 -5
  136. package/mcp_server/runtime/tools.py +66 -29
  137. package/mcp_server/sample_engine/__init__.py +1 -0
  138. package/mcp_server/sample_engine/analyzer.py +216 -0
  139. package/mcp_server/sample_engine/critics.py +390 -0
  140. package/mcp_server/sample_engine/models.py +193 -0
  141. package/mcp_server/sample_engine/moves.py +127 -0
  142. package/mcp_server/sample_engine/planner.py +186 -0
  143. package/mcp_server/sample_engine/slice_workflow.py +190 -0
  144. package/mcp_server/sample_engine/sources.py +540 -0
  145. package/mcp_server/sample_engine/techniques.py +908 -0
  146. package/mcp_server/sample_engine/tools.py +545 -0
  147. package/mcp_server/semantic_moves/__init__.py +3 -0
  148. package/mcp_server/semantic_moves/device_creation_moves.py +237 -0
  149. package/mcp_server/semantic_moves/mix_moves.py +8 -8
  150. package/mcp_server/semantic_moves/models.py +7 -7
  151. package/mcp_server/semantic_moves/performance_moves.py +4 -4
  152. package/mcp_server/semantic_moves/sample_compilers.py +377 -0
  153. package/mcp_server/semantic_moves/sound_design_moves.py +4 -4
  154. package/mcp_server/semantic_moves/tools.py +63 -10
  155. package/mcp_server/semantic_moves/transition_moves.py +4 -4
  156. package/mcp_server/server.py +71 -1
  157. package/mcp_server/session_continuity/tracker.py +4 -1
  158. package/mcp_server/sound_design/critics.py +89 -1
  159. package/mcp_server/splice_client/__init__.py +1 -0
  160. package/mcp_server/splice_client/client.py +347 -0
  161. package/mcp_server/splice_client/models.py +96 -0
  162. package/mcp_server/splice_client/protos/__init__.py +1 -0
  163. package/mcp_server/splice_client/protos/app_pb2.py +319 -0
  164. package/mcp_server/splice_client/protos/app_pb2.pyi +1153 -0
  165. package/mcp_server/splice_client/protos/app_pb2_grpc.py +1946 -0
  166. package/mcp_server/tools/_conductor.py +16 -0
  167. package/mcp_server/tools/_planner_engine.py +24 -0
  168. package/mcp_server/tools/analyzer.py +2 -0
  169. package/mcp_server/tools/arrangement.py +69 -0
  170. package/mcp_server/tools/automation.py +15 -2
  171. package/mcp_server/tools/devices.py +117 -6
  172. package/mcp_server/tools/notes.py +37 -4
  173. package/mcp_server/tools/planner.py +3 -0
  174. package/mcp_server/wonder_mode/diagnosis.py +5 -0
  175. package/mcp_server/wonder_mode/engine.py +144 -14
  176. package/mcp_server/wonder_mode/tools.py +33 -1
  177. package/package.json +14 -4
  178. package/remote_script/LivePilot/__init__.py +8 -1
  179. package/remote_script/LivePilot/arrangement.py +114 -0
  180. package/remote_script/LivePilot/browser.py +56 -1
  181. package/remote_script/LivePilot/devices.py +246 -6
  182. package/remote_script/LivePilot/mixing.py +8 -3
  183. package/remote_script/LivePilot/server.py +5 -1
  184. package/remote_script/LivePilot/transport.py +3 -0
  185. package/remote_script/LivePilot/version_detect.py +78 -0
@@ -0,0 +1,545 @@
1
+ """Sample Engine MCP tools — 7 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
+ section_type: Optional[str] = None,
323
+ desired_role: Optional[str] = None,
324
+ ) -> dict:
325
+ """Full end-to-end sample workflow: analyze, critique, select technique, compile plan.
326
+
327
+ Provide file_path for a known sample, or search_query to find one.
328
+ Returns a complete compiled plan ready for execution.
329
+
330
+ intent: rhythm, texture, layer, melody, vocal, atmosphere, transform
331
+ philosophy: surgeon, alchemist, auto
332
+ target_track: existing track index, or None for new track
333
+ section_type: optional section context (intro, verse, chorus, drop, etc.)
334
+ desired_role: optional sample role (hook_sample, texture_bed, break_layer, etc.)
335
+ """
336
+ if file_path is None and search_query is None:
337
+ return {"error": "Provide either file_path or search_query"}
338
+
339
+ profile = None
340
+ if file_path:
341
+ profile = build_profile_from_filename(file_path)
342
+
343
+ sample_intent = SampleIntent(
344
+ intent_type=intent, philosophy=philosophy,
345
+ description=search_query or f"Process {file_path} for {intent}",
346
+ target_track=target_track,
347
+ )
348
+
349
+ if profile is None:
350
+ # No file yet — return search guidance
351
+ queries = build_search_queries(search_query or "", material_type=None)
352
+ return {
353
+ "status": "search_needed",
354
+ "search_queries": queries,
355
+ "intent": intent,
356
+ "note": "Use search_samples to find a sample, then call again with file_path",
357
+ }
358
+
359
+ technique = select_technique(profile, sample_intent)
360
+ plan = compile_sample_plan(profile, sample_intent, target_track=target_track,
361
+ technique=technique)
362
+
363
+ return {
364
+ "sample": profile.to_dict(),
365
+ "intent": intent,
366
+ "philosophy": philosophy,
367
+ "technique": technique.name if technique else "fallback",
368
+ "technique_id": technique.technique_id if technique else "",
369
+ "step_count": len(plan),
370
+ "compiled_plan": plan,
371
+ }
372
+
373
+
374
+ @mcp.tool()
375
+ def get_sample_opportunities(ctx: Context) -> dict:
376
+ """Analyze current song and identify where samples could improve it.
377
+
378
+ Returns opportunities with suggested material types and techniques.
379
+ Used by Wonder Mode diagnosis for sample-aware creative rescue.
380
+ """
381
+ opportunities: list[dict] = []
382
+
383
+ try:
384
+ ableton = ctx.lifespan_context["ableton"]
385
+ info = ableton.send_command("get_session_info", {})
386
+ except Exception:
387
+ return {"opportunities": [], "note": "Cannot read session — Ableton not connected"}
388
+
389
+ track_count = info.get("track_count", 0)
390
+ track_names: list[str] = []
391
+ has_sampler = False
392
+
393
+ for i in range(min(track_count, 16)):
394
+ try:
395
+ track_info = ableton.send_command("get_track_info", {"track_index": i})
396
+ name = track_info.get("name", "").lower()
397
+ track_names.append(name)
398
+ devices = track_info.get("devices", [])
399
+ for d in devices:
400
+ if d.get("class_name") in ("OriginalSimpler", "MultiSampler"):
401
+ has_sampler = True
402
+ except Exception:
403
+ continue
404
+
405
+ # No organic texture
406
+ has_organic = any(
407
+ kw in name for name in track_names
408
+ for kw in ("vocal", "sample", "foley", "field", "organic", "found")
409
+ )
410
+ if not has_organic and track_count >= 3:
411
+ opportunities.append({
412
+ "type": "no_organic_texture",
413
+ "description": "No organic/sampled textures — all tracks appear synthesized",
414
+ "suggested_material": ["vocal", "foley", "texture"],
415
+ "suggested_techniques": ["vocal_chop_rhythm", "phone_recording_texture", "tail_harvest"],
416
+ "confidence": 0.6,
417
+ })
418
+
419
+ # Limited drum variety
420
+ drum_tracks = [n for n in track_names if any(
421
+ kw in n for kw in ("drum", "beat", "perc", "kick", "snare")
422
+ )]
423
+ if len(drum_tracks) <= 1 and track_count >= 4:
424
+ opportunities.append({
425
+ "type": "drum_variety_needed",
426
+ "description": "Limited percussion variety — layer a break or add ghost notes",
427
+ "suggested_material": ["drum_loop"],
428
+ "suggested_techniques": ["break_layering", "ghost_note_texture"],
429
+ "confidence": 0.5,
430
+ })
431
+
432
+ # No Simpler/Sampler devices
433
+ if not has_sampler and track_count >= 2:
434
+ opportunities.append({
435
+ "type": "no_sample_instruments",
436
+ "description": "No Simpler/Sampler devices — samples could add character",
437
+ "suggested_material": ["vocal", "instrument_loop", "one_shot"],
438
+ "suggested_techniques": ["syllable_instrument", "slice_and_sequence"],
439
+ "confidence": 0.4,
440
+ })
441
+
442
+ return {
443
+ "opportunity_count": len(opportunities),
444
+ "opportunities": opportunities,
445
+ "track_count": track_count,
446
+ }
447
+
448
+
449
+ @mcp.tool()
450
+ def plan_slice_workflow(
451
+ ctx: Context,
452
+ file_path: Optional[str] = None,
453
+ track_index: Optional[int] = None,
454
+ device_index: int = 0,
455
+ intent: str = "rhythm",
456
+ target_section: Optional[str] = None,
457
+ target_track: Optional[int] = None,
458
+ bars: int = 4,
459
+ style_hint: str = "",
460
+ ) -> dict:
461
+ """Plan an end-to-end slice workflow for a sample.
462
+
463
+ Generates a Simpler slice strategy, MIDI note mapping, and starter
464
+ pattern based on musical intent. Returns a compiled workflow plan —
465
+ does NOT execute. The agent steps through each tool call in sequence.
466
+
467
+ Provide either file_path (new sample to load) or track_index +
468
+ device_index (existing Simpler with loaded sample).
469
+
470
+ intent: rhythm | hook | texture | percussion | melodic
471
+ bars: number of bars for the pattern (default 4)
472
+ target_section: optional section name for arrangement hints
473
+ style_hint: optional genre/style context (e.g. "dilla", "burial")
474
+ """
475
+ from .slice_workflow import plan_slice_steps
476
+
477
+ # Determine slice count — default 8 for file-based, or would come from
478
+ # get_simpler_slices in a real execution
479
+ # Read tempo from session if connected, otherwise default
480
+ tempo = 120.0
481
+ try:
482
+ ableton = ctx.lifespan_context.get("ableton")
483
+ if ableton:
484
+ info = ableton.send_command("get_session_info", {})
485
+ tempo = float(info.get("tempo", 120.0))
486
+ except Exception:
487
+ pass
488
+
489
+ # Read slice count from existing Simpler if track provided
490
+ slice_count = 8 # Default transient slice count
491
+ if track_index is not None:
492
+ try:
493
+ ableton = ctx.lifespan_context.get("ableton")
494
+ if ableton:
495
+ slices = ableton.send_command("get_simpler_slices", {
496
+ "track_index": track_index, "device_index": device_index,
497
+ })
498
+ if isinstance(slices, dict) and slices.get("slice_count"):
499
+ slice_count = slices["slice_count"]
500
+ except Exception:
501
+ pass # Fall back to default
502
+
503
+ # Build the plan
504
+ plan = plan_slice_steps(
505
+ slice_count=slice_count,
506
+ intent=intent,
507
+ bars=bars,
508
+ tempo=tempo,
509
+ track_index=target_track if target_track is not None else 0,
510
+ )
511
+
512
+ # Prepend sample loading steps if file_path provided
513
+ if file_path:
514
+ load_steps = [
515
+ {
516
+ "tool": "create_midi_track",
517
+ "params": {"name": f"Slice {intent.title()}"},
518
+ "description": "Create track for sliced sample",
519
+ },
520
+ {
521
+ "tool": "load_sample_to_simpler",
522
+ "params": {"track_index": target_track or 0, "file_path": file_path},
523
+ "description": f"Load sample into Simpler: {file_path}",
524
+ },
525
+ {
526
+ "tool": "set_simpler_playback_mode",
527
+ "params": {"track_index": target_track or 0, "device_index": 0, "playback_mode": 2},
528
+ "description": "Set Simpler to Slice mode",
529
+ },
530
+ ]
531
+ plan["steps"] = load_steps + plan["steps"]
532
+
533
+ # Add arrangement hints if section provided
534
+ if target_section:
535
+ plan["arrangement_hints"] = {
536
+ "target_section": target_section,
537
+ "suggested_placement": f"Place slice pattern in {target_section}",
538
+ }
539
+
540
+ plan["file_path"] = file_path
541
+ plan["track_index"] = track_index
542
+ plan["device_index"] = device_index
543
+ plan["style_hint"] = style_hint
544
+
545
+ return plan
@@ -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