livepilot 1.26.2 → 1.27.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.
- package/CHANGELOG.md +92 -0
- package/README.md +34 -14
- package/bin/livepilot.js +4 -2
- package/installer/install.js +21 -12
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +12 -12
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +2 -1
- package/livepilot/skills/livepilot-core/references/overview.md +6 -4
- package/livepilot/skills/livepilot-core/references/sound-design.md +5 -2
- package/livepilot/skills/livepilot-creative-director/SKILL.md +36 -0
- package/livepilot/skills/livepilot-creative-director/references/move-family-diversity-rule.md +1 -1
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +15 -9
- package/livepilot/skills/livepilot-release/SKILL.md +7 -5
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +20 -1
- package/mcp_server/atlas/tools.py +83 -22
- package/mcp_server/composer/full/apply.py +9 -0
- package/mcp_server/composer/full/layer_planner.py +16 -6
- package/mcp_server/creative_constraints/engine.py +46 -0
- package/mcp_server/experiment/engine.py +10 -3
- package/mcp_server/grader/client.py +30 -2
- package/mcp_server/grader/tools.py +8 -2
- package/mcp_server/hook_hunter/analyzer.py +15 -2
- package/mcp_server/m4l_bridge.py +19 -21
- package/mcp_server/memory/taste_graph.py +16 -0
- package/mcp_server/memory/technique_store.py +23 -3
- package/mcp_server/mix_engine/state_builder.py +4 -2
- package/mcp_server/musical_intelligence/detectors.py +11 -2
- package/mcp_server/musical_intelligence/tools.py +15 -2
- package/mcp_server/preview_studio/engine.py +30 -1
- package/mcp_server/project_brain/tools.py +56 -52
- package/mcp_server/reference_engine/tools.py +22 -2
- package/mcp_server/runtime/capability_probe.py +1 -1
- package/mcp_server/runtime/capability_state.py +66 -9
- package/mcp_server/runtime/live_version.py +45 -8
- package/mcp_server/runtime/safety_kernel.py +11 -0
- package/mcp_server/runtime/session_kernel.py +7 -0
- package/mcp_server/runtime/tools.py +324 -22
- package/mcp_server/sample_engine/critics.py +7 -3
- package/mcp_server/sample_engine/slice_workflow.py +3 -2
- package/mcp_server/sample_engine/tools.py +53 -21
- package/mcp_server/song_brain/builder.py +17 -1
- package/mcp_server/sound_design/tools.py +1 -1
- package/mcp_server/splice_client/http_bridge.py +43 -1
- package/mcp_server/splice_client/models.py +7 -3
- package/mcp_server/synthesis_brain/adapters/analog.py +13 -1
- package/mcp_server/synthesis_brain/adapters/operator.py +5 -2
- package/mcp_server/synthesis_brain/adapters/wavetable.py +4 -4
- package/mcp_server/tools/_composition_engine/models.py +6 -0
- package/mcp_server/tools/_composition_engine/sections.py +4 -0
- package/mcp_server/tools/analyzer.py +38 -1
- package/mcp_server/tools/clips.py +5 -4
- package/mcp_server/tools/composition.py +5 -1
- package/mcp_server/tools/midi_io.py +40 -1
- package/mcp_server/tools/transport.py +1 -1
- package/mcp_server/user_corpus/plugin_engine/detector.py +66 -11
- package/mcp_server/user_corpus/runner.py +7 -1
- package/mcp_server/user_corpus/scanners/amxd.py +24 -13
- package/mcp_server/user_corpus/tools.py +45 -9
- package/mcp_server/wonder_mode/tools.py +66 -27
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/server.py +23 -3
- package/remote_script/LivePilot/version_detect.py +3 -0
- package/requirements.txt +4 -4
- package/server.json +3 -3
|
@@ -7,10 +7,11 @@ Tools:
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import importlib.util
|
|
11
10
|
import logging
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
12
13
|
import urllib.request
|
|
13
|
-
from typing import Optional
|
|
14
|
+
from typing import Any, Optional
|
|
14
15
|
|
|
15
16
|
from fastmcp import Context
|
|
16
17
|
|
|
@@ -21,6 +22,14 @@ from .capability_state import build_capability_state
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
23
24
|
_memory_store = TechniqueStore()
|
|
25
|
+
_FLUCOMA_STREAM_KEYS = (
|
|
26
|
+
"spectral_shape",
|
|
27
|
+
"mel_bands",
|
|
28
|
+
"chroma",
|
|
29
|
+
"onset",
|
|
30
|
+
"novelty",
|
|
31
|
+
"loudness",
|
|
32
|
+
)
|
|
24
33
|
|
|
25
34
|
|
|
26
35
|
# ── Capability probes ──────────────────────────────────────────────────
|
|
@@ -51,21 +60,296 @@ def _probe_web(timeout: float = 0.5) -> bool:
|
|
|
51
60
|
return False
|
|
52
61
|
|
|
53
62
|
|
|
54
|
-
def
|
|
55
|
-
"""
|
|
63
|
+
def _flucoma_package_dirs() -> list[Path]:
|
|
64
|
+
"""Return likely Max package locations for FluCoMa."""
|
|
65
|
+
home = Path.home()
|
|
66
|
+
if os.name == "nt":
|
|
67
|
+
docs = Path(os.environ.get("USERPROFILE", str(home))) / "Documents"
|
|
68
|
+
else:
|
|
69
|
+
docs = home / "Documents"
|
|
70
|
+
return [
|
|
71
|
+
docs / "Max 9" / "Packages" / "FluidCorpusManipulation",
|
|
72
|
+
docs / "Max 8" / "Packages" / "FluidCorpusManipulation",
|
|
73
|
+
]
|
|
56
74
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
|
|
76
|
+
def _probe_flucoma_package() -> bool:
|
|
77
|
+
"""Check whether the FluCoMa Max package exists on disk.
|
|
78
|
+
|
|
79
|
+
LivePilot's real-time FluCoMa support is Max-for-Live based. There is
|
|
80
|
+
no required Python package named ``flucoma``; probing importability
|
|
81
|
+
caused false ``flucoma_not_installed`` reports on healthy systems.
|
|
61
82
|
"""
|
|
62
83
|
try:
|
|
63
|
-
|
|
84
|
+
for package_dir in _flucoma_package_dirs():
|
|
85
|
+
if not package_dir.exists():
|
|
86
|
+
continue
|
|
87
|
+
if (package_dir / "package-info.json").exists():
|
|
88
|
+
return True
|
|
89
|
+
externals = package_dir / "externals"
|
|
90
|
+
if externals.exists() and any(externals.glob("fluid.*")):
|
|
91
|
+
return True
|
|
92
|
+
return True
|
|
93
|
+
return False
|
|
64
94
|
except Exception as exc: # noqa: BLE001
|
|
65
|
-
logger.debug("
|
|
95
|
+
logger.debug("_probe_flucoma_package failed: %s", exc)
|
|
66
96
|
return False
|
|
67
97
|
|
|
68
98
|
|
|
99
|
+
def _probe_flucoma(spectral=None) -> dict:
|
|
100
|
+
"""Probe the Max/bridge-backed FluCoMa runtime.
|
|
101
|
+
|
|
102
|
+
``available`` means at least one FluCoMa stream has reached the
|
|
103
|
+
spectral cache. ``device_loaded`` means the external package appears
|
|
104
|
+
installed (or streams prove it is working from a frozen analyzer).
|
|
105
|
+
"""
|
|
106
|
+
package_installed = _probe_flucoma_package()
|
|
107
|
+
bridge_connected = bool(
|
|
108
|
+
spectral is not None and getattr(spectral, "is_connected", False)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
streams: dict[str, bool] = {}
|
|
112
|
+
if bridge_connected:
|
|
113
|
+
for key in _FLUCOMA_STREAM_KEYS:
|
|
114
|
+
try:
|
|
115
|
+
streams[key] = spectral.get(key) is not None
|
|
116
|
+
except Exception as exc: # noqa: BLE001
|
|
117
|
+
logger.debug("_probe_flucoma stream %s failed: %s", key, exc)
|
|
118
|
+
streams[key] = False
|
|
119
|
+
|
|
120
|
+
active_streams = sum(1 for present in streams.values() if present)
|
|
121
|
+
available = active_streams > 0
|
|
122
|
+
device_loaded = package_installed or available
|
|
123
|
+
|
|
124
|
+
reasons: list[str] = []
|
|
125
|
+
if not device_loaded:
|
|
126
|
+
reasons.append("flucoma_not_installed")
|
|
127
|
+
if not bridge_connected:
|
|
128
|
+
reasons.append("flucoma_bridge_unavailable")
|
|
129
|
+
elif not available:
|
|
130
|
+
reasons.append("flucoma_no_streams")
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"available": available,
|
|
134
|
+
"device_loaded": device_loaded,
|
|
135
|
+
"bridge_connected": bridge_connected,
|
|
136
|
+
"active_streams": active_streams,
|
|
137
|
+
"streams": streams,
|
|
138
|
+
"reasons": reasons,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _normalize_flucoma_probe(raw) -> dict:
|
|
143
|
+
"""Back-compat shim for tests/extensions that monkeypatch a bool probe."""
|
|
144
|
+
if isinstance(raw, bool):
|
|
145
|
+
return {
|
|
146
|
+
"available": raw,
|
|
147
|
+
"device_loaded": raw,
|
|
148
|
+
"reasons": [] if raw else ["flucoma_not_installed"],
|
|
149
|
+
}
|
|
150
|
+
if isinstance(raw, dict):
|
|
151
|
+
available = bool(raw.get("available", False))
|
|
152
|
+
return {
|
|
153
|
+
**raw,
|
|
154
|
+
"available": available,
|
|
155
|
+
"device_loaded": bool(raw.get("device_loaded", available)),
|
|
156
|
+
"reasons": list(raw.get("reasons", [])),
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
"available": False,
|
|
160
|
+
"device_loaded": False,
|
|
161
|
+
"reasons": ["flucoma_probe_failed"],
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _run_flucoma_probe(spectral=None) -> dict:
|
|
166
|
+
try:
|
|
167
|
+
return _normalize_flucoma_probe(_probe_flucoma(spectral))
|
|
168
|
+
except TypeError:
|
|
169
|
+
# Older tests monkeypatch _probe_flucoma as a no-arg callable.
|
|
170
|
+
return _normalize_flucoma_probe(_probe_flucoma())
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _session_info(ableton) -> dict:
|
|
174
|
+
try:
|
|
175
|
+
info = ableton.send_command("get_session_info")
|
|
176
|
+
return info if isinstance(info, dict) else {}
|
|
177
|
+
except Exception as exc: # noqa: BLE001
|
|
178
|
+
logger.debug("_session_info probe failed: %s", exc)
|
|
179
|
+
return {}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _live_version_from_session(session_info: dict) -> str:
|
|
183
|
+
version = session_info.get("live_version")
|
|
184
|
+
return str(version) if version else "12.0.0"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _normalize_probe_surface(
|
|
188
|
+
raw: Any,
|
|
189
|
+
*,
|
|
190
|
+
default_reason: str,
|
|
191
|
+
default_mode: str = "manual_only",
|
|
192
|
+
) -> dict:
|
|
193
|
+
if isinstance(raw, dict):
|
|
194
|
+
mode = str(raw.get("mode") or default_mode)
|
|
195
|
+
available = bool(raw.get("available", mode in {"readable", "routable", "callable"}))
|
|
196
|
+
reasons = list(raw.get("reasons") or [])
|
|
197
|
+
if not available and not reasons:
|
|
198
|
+
reasons.append(default_reason)
|
|
199
|
+
return {
|
|
200
|
+
"mode": mode,
|
|
201
|
+
"available": available,
|
|
202
|
+
"reasons": reasons,
|
|
203
|
+
"observed": dict(raw.get("observed") or {}),
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
"mode": default_mode,
|
|
207
|
+
"available": False,
|
|
208
|
+
"reasons": [default_reason],
|
|
209
|
+
"observed": {},
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _probe_link_audio_surface(ableton, session_info: Optional[dict] = None) -> dict:
|
|
214
|
+
"""Read-only Link Audio surface probe.
|
|
215
|
+
|
|
216
|
+
Current Live 12.4 builds expose Link Audio as product UX, but no stable
|
|
217
|
+
Remote Script/M4L routing contract has been proven. This helper looks for
|
|
218
|
+
explicit evidence if future Remote Scripts surface it; otherwise it reports
|
|
219
|
+
manual_only with a reason instead of claiming support from version alone.
|
|
220
|
+
"""
|
|
221
|
+
info = session_info or _session_info(ableton)
|
|
222
|
+
observed = {
|
|
223
|
+
"peers_visible": bool(info.get("link_audio_peers") or info.get("link_peers")),
|
|
224
|
+
"inputs_visible": bool(info.get("link_audio_inputs") or info.get("audio_inputs")),
|
|
225
|
+
"routing_visible": bool(info.get("link_audio_routing")),
|
|
226
|
+
}
|
|
227
|
+
if observed["peers_visible"] and observed["inputs_visible"] and observed["routing_visible"]:
|
|
228
|
+
return {
|
|
229
|
+
"mode": "routable",
|
|
230
|
+
"available": True,
|
|
231
|
+
"reasons": [],
|
|
232
|
+
"observed": observed,
|
|
233
|
+
}
|
|
234
|
+
if observed["peers_visible"] or observed["inputs_visible"]:
|
|
235
|
+
return {
|
|
236
|
+
"mode": "readable",
|
|
237
|
+
"available": True,
|
|
238
|
+
"reasons": [],
|
|
239
|
+
"observed": observed,
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
"mode": "manual_only",
|
|
243
|
+
"available": False,
|
|
244
|
+
"reasons": ["link_audio_not_exposed"],
|
|
245
|
+
"observed": observed,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _probe_stem_workflow_surface(ableton, session_info: Optional[dict] = None) -> dict:
|
|
250
|
+
"""Read-only selected-time stem workflow probe.
|
|
251
|
+
|
|
252
|
+
This intentionally does not invoke stem separation. It only reports
|
|
253
|
+
callable evidence if a future Remote Script/M4L surface advertises a
|
|
254
|
+
stable command path.
|
|
255
|
+
"""
|
|
256
|
+
info = session_info or _session_info(ableton)
|
|
257
|
+
callable_paths = list(info.get("stem_callable_paths") or [])
|
|
258
|
+
if callable_paths:
|
|
259
|
+
return {
|
|
260
|
+
"mode": "callable",
|
|
261
|
+
"available": True,
|
|
262
|
+
"reasons": [],
|
|
263
|
+
"observed": {"callable_paths": callable_paths},
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
"mode": "manual_only",
|
|
267
|
+
"available": False,
|
|
268
|
+
"reasons": ["stem_command_not_observable"],
|
|
269
|
+
"observed": {"callable_paths": []},
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _probe_live_12_4_domain(
|
|
274
|
+
*,
|
|
275
|
+
ableton,
|
|
276
|
+
session_info: dict,
|
|
277
|
+
feature_available: bool,
|
|
278
|
+
surface_probe,
|
|
279
|
+
default_reason: str,
|
|
280
|
+
) -> dict:
|
|
281
|
+
live_version = _live_version_from_session(session_info)
|
|
282
|
+
if not feature_available:
|
|
283
|
+
return {
|
|
284
|
+
"ok": True,
|
|
285
|
+
"live_version": live_version,
|
|
286
|
+
"mode": "unavailable",
|
|
287
|
+
"available": False,
|
|
288
|
+
"reasons": ["live_version_below_12_4"],
|
|
289
|
+
"observed": {},
|
|
290
|
+
}
|
|
291
|
+
try:
|
|
292
|
+
surface = _normalize_probe_surface(
|
|
293
|
+
surface_probe(ableton, session_info=session_info),
|
|
294
|
+
default_reason=default_reason,
|
|
295
|
+
)
|
|
296
|
+
except Exception as exc: # noqa: BLE001
|
|
297
|
+
logger.debug("Live 12.4 surface probe failed: %s", exc)
|
|
298
|
+
surface = {
|
|
299
|
+
"mode": "manual_only",
|
|
300
|
+
"available": False,
|
|
301
|
+
"reasons": [f"probe_failed: {type(exc).__name__}"],
|
|
302
|
+
"observed": {},
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
"ok": True,
|
|
306
|
+
"live_version": live_version,
|
|
307
|
+
**surface,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _probe_link_audio_domain(ableton, session_info: dict) -> dict:
|
|
312
|
+
from .live_version import LiveVersionCapabilities
|
|
313
|
+
|
|
314
|
+
caps = LiveVersionCapabilities.from_session_info(session_info)
|
|
315
|
+
return _probe_live_12_4_domain(
|
|
316
|
+
ableton=ableton,
|
|
317
|
+
session_info=session_info,
|
|
318
|
+
feature_available=caps.has_link_audio,
|
|
319
|
+
surface_probe=_probe_link_audio_surface,
|
|
320
|
+
default_reason="link_audio_not_exposed",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _probe_stem_workflow_domain(ableton, session_info: dict) -> dict:
|
|
325
|
+
from .live_version import LiveVersionCapabilities
|
|
326
|
+
|
|
327
|
+
caps = LiveVersionCapabilities.from_session_info(session_info)
|
|
328
|
+
return _probe_live_12_4_domain(
|
|
329
|
+
ableton=ableton,
|
|
330
|
+
session_info=session_info,
|
|
331
|
+
feature_available=caps.has_stem_time_selection,
|
|
332
|
+
surface_probe=_probe_stem_workflow_surface,
|
|
333
|
+
default_reason="stem_command_not_observable",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@mcp.tool()
|
|
338
|
+
def probe_link_audio(ctx: Context) -> dict:
|
|
339
|
+
"""Read-only probe for Live 12.4 Link Audio MCP controllability."""
|
|
340
|
+
ableton = ctx.lifespan_context["ableton"]
|
|
341
|
+
session_info = _session_info(ableton)
|
|
342
|
+
return _probe_link_audio_domain(ableton, session_info)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@mcp.tool()
|
|
346
|
+
def probe_stem_workflow(ctx: Context) -> dict:
|
|
347
|
+
"""Read-only probe for Live 12.4 selected-time stem workflow support."""
|
|
348
|
+
ableton = ctx.lifespan_context["ableton"]
|
|
349
|
+
session_info = _session_info(ableton)
|
|
350
|
+
return _probe_stem_workflow_domain(ableton, session_info)
|
|
351
|
+
|
|
352
|
+
|
|
69
353
|
@mcp.tool()
|
|
70
354
|
def get_capability_state(ctx: Context) -> dict:
|
|
71
355
|
"""Probe the runtime and return a capability state snapshot.
|
|
@@ -77,13 +361,8 @@ def get_capability_state(ctx: Context) -> dict:
|
|
|
77
361
|
spectral = ctx.lifespan_context.get("spectral")
|
|
78
362
|
|
|
79
363
|
# ── Probe session ───────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
result = ableton.send_command("get_session_info")
|
|
83
|
-
session_ok = isinstance(result, dict) and "error" not in result
|
|
84
|
-
except Exception as exc:
|
|
85
|
-
logger.debug("get_capability_state failed: %s", exc)
|
|
86
|
-
session_ok = False
|
|
364
|
+
session_info = _session_info(ableton)
|
|
365
|
+
session_ok = bool(session_info) and "error" not in session_info
|
|
87
366
|
|
|
88
367
|
# ── Probe analyzer (M4L bridge) ─────────────────────────────────
|
|
89
368
|
analyzer_ok = False
|
|
@@ -109,8 +388,12 @@ def get_capability_state(ctx: Context) -> dict:
|
|
|
109
388
|
# a curated research corpus is installed (see ``research`` domain).
|
|
110
389
|
web_ok = _probe_web()
|
|
111
390
|
|
|
112
|
-
# ── FluCoMa —
|
|
113
|
-
|
|
391
|
+
# ── FluCoMa — Max package + live bridge streams ─────────────────
|
|
392
|
+
flucoma_probe = _run_flucoma_probe(spectral)
|
|
393
|
+
|
|
394
|
+
# ── Live 12.4 probe-first surfaces ──────────────────────────────
|
|
395
|
+
link_probe = _probe_link_audio_domain(ableton, session_info)
|
|
396
|
+
stem_probe = _probe_stem_workflow_domain(ableton, session_info)
|
|
114
397
|
|
|
115
398
|
state = build_capability_state(
|
|
116
399
|
session_ok=session_ok,
|
|
@@ -118,7 +401,13 @@ def get_capability_state(ctx: Context) -> dict:
|
|
|
118
401
|
analyzer_fresh=analyzer_fresh,
|
|
119
402
|
memory_ok=memory_ok,
|
|
120
403
|
web_ok=web_ok,
|
|
121
|
-
flucoma_ok=
|
|
404
|
+
flucoma_ok=flucoma_probe["available"],
|
|
405
|
+
flucoma_device_loaded=flucoma_probe["device_loaded"],
|
|
406
|
+
flucoma_reasons=flucoma_probe["reasons"],
|
|
407
|
+
link_audio_mode=link_probe["mode"],
|
|
408
|
+
link_audio_reasons=link_probe["reasons"],
|
|
409
|
+
stem_workflow_mode=stem_probe["mode"],
|
|
410
|
+
stem_workflow_reasons=stem_probe["reasons"],
|
|
122
411
|
)
|
|
123
412
|
|
|
124
413
|
return state.to_dict()
|
|
@@ -134,6 +423,7 @@ def get_session_kernel(
|
|
|
134
423
|
creativity_profile: str = "",
|
|
135
424
|
sacred_elements: Optional[list] = None,
|
|
136
425
|
synth_hints: Optional[dict] = None,
|
|
426
|
+
operation_profile: str = "studio_deep",
|
|
137
427
|
) -> dict:
|
|
138
428
|
"""Build the unified turn snapshot for V2 orchestration.
|
|
139
429
|
|
|
@@ -159,6 +449,9 @@ def get_session_kernel(
|
|
|
159
449
|
synth_hints: focus hints for synthesis_brain; shape is open in PR2
|
|
160
450
|
and firms up in PR9. Typical keys: track_indices, device_paths,
|
|
161
451
|
target_timbre, preferred_devices.
|
|
452
|
+
operation_profile: safety/intent posture for this turn. Known values:
|
|
453
|
+
safe_live, studio_deep, arrangement_build, sound_design_deep,
|
|
454
|
+
release_audit.
|
|
162
455
|
|
|
163
456
|
Returns: SessionKernel dict with kernel_id, session topology, capabilities,
|
|
164
457
|
memory context, routing hints, and (if provided) creative controls.
|
|
@@ -183,7 +476,9 @@ def get_session_kernel(
|
|
|
183
476
|
# does, and propagate through. Without this the kernel's capability view
|
|
184
477
|
# lies to orchestration planners.
|
|
185
478
|
web_ok = _probe_web()
|
|
186
|
-
|
|
479
|
+
flucoma_probe = _run_flucoma_probe(spectral)
|
|
480
|
+
link_probe = _probe_link_audio_domain(ableton, session_info if isinstance(session_info, dict) else {})
|
|
481
|
+
stem_probe = _probe_stem_workflow_domain(ableton, session_info if isinstance(session_info, dict) else {})
|
|
187
482
|
|
|
188
483
|
# v1.17.4: probe memory the same way too. Previously memory_ok=True was
|
|
189
484
|
# hardcoded — if the store raised, the kernel still reported memory
|
|
@@ -201,7 +496,13 @@ def get_session_kernel(
|
|
|
201
496
|
analyzer_fresh=analyzer_fresh,
|
|
202
497
|
memory_ok=memory_ok,
|
|
203
498
|
web_ok=web_ok,
|
|
204
|
-
flucoma_ok=
|
|
499
|
+
flucoma_ok=flucoma_probe["available"],
|
|
500
|
+
flucoma_device_loaded=flucoma_probe["device_loaded"],
|
|
501
|
+
flucoma_reasons=flucoma_probe["reasons"],
|
|
502
|
+
link_audio_mode=link_probe["mode"],
|
|
503
|
+
link_audio_reasons=link_probe["reasons"],
|
|
504
|
+
stem_workflow_mode=stem_probe["mode"],
|
|
505
|
+
stem_workflow_reasons=stem_probe["reasons"],
|
|
205
506
|
)
|
|
206
507
|
|
|
207
508
|
# Optional subcomponents — degrade gracefully, but reach into the SAME
|
|
@@ -286,6 +587,7 @@ def get_session_kernel(
|
|
|
286
587
|
anti_preferences=anti_prefs,
|
|
287
588
|
freshness=freshness,
|
|
288
589
|
creativity_profile=creativity_profile,
|
|
590
|
+
operation_profile=operation_profile,
|
|
289
591
|
sacred_elements=sacred_elements,
|
|
290
592
|
synth_hints=synth_hints,
|
|
291
593
|
)
|
|
@@ -301,9 +301,13 @@ def run_vibe_fit_critic(
|
|
|
301
301
|
recommendation="No taste evidence yet — neutral score",
|
|
302
302
|
)
|
|
303
303
|
|
|
304
|
-
# Compute energy proxy from sample characteristics (0.0 - 1.0)
|
|
305
|
-
# brightness
|
|
306
|
-
|
|
304
|
+
# Compute energy proxy from sample characteristics (0.0 - 1.0).
|
|
305
|
+
# brightness is already a 0.0-1.0 fraction, but transient_density is peaks
|
|
306
|
+
# PER SECOND (unbounded). Normalize it through a saturating curve before
|
|
307
|
+
# averaging so a busy break does not pin the proxy to 1.0 for every sample.
|
|
308
|
+
# ~12 peaks/s (busy 16ths) maps to ~1.0.
|
|
309
|
+
transient_norm = max(0.0, min(1.0, profile.transient_density / 12.0))
|
|
310
|
+
energy = (profile.brightness + transient_norm) / 2.0
|
|
307
311
|
energy = max(0.0, min(1.0, energy))
|
|
308
312
|
|
|
309
313
|
# Compare against novelty_band as taste proxy
|
|
@@ -12,8 +12,9 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import hashlib
|
|
14
14
|
|
|
15
|
-
# Simpler maps slices to MIDI notes starting at
|
|
16
|
-
|
|
15
|
+
# Simpler maps slices to MIDI notes starting at C1 (36): slice N -> pitch 36+N.
|
|
16
|
+
# Notes at 60+ (C3) trigger no slice and produce silence.
|
|
17
|
+
SLICE_BASE_NOTE = 36
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def plan_slice_steps(
|
|
@@ -6,6 +6,7 @@ direct Splice online catalog hunt/download via the gRPC client.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import asyncio
|
|
9
10
|
import logging
|
|
10
11
|
import os
|
|
11
12
|
from typing import Optional
|
|
@@ -57,7 +58,11 @@ async def analyze_sample(
|
|
|
57
58
|
return {"error": "Could not determine file path — provide file_path directly"}
|
|
58
59
|
|
|
59
60
|
source = "session_clip" if track_index is not None else "filesystem"
|
|
60
|
-
|
|
61
|
+
# Offload audio decode + numpy FFT off the event loop (heavy CPU/IO).
|
|
62
|
+
loop = asyncio.get_running_loop()
|
|
63
|
+
profile = await loop.run_in_executor(
|
|
64
|
+
None, build_profile_from_filename, file_path, source
|
|
65
|
+
)
|
|
61
66
|
return profile.to_dict()
|
|
62
67
|
|
|
63
68
|
|
|
@@ -86,6 +91,11 @@ def evaluate_sample_fit(
|
|
|
86
91
|
song_key = None
|
|
87
92
|
session_tempo = 120.0
|
|
88
93
|
existing_roles: list[str] = []
|
|
94
|
+
# Map track index -> name so the key-detection loop below can look a
|
|
95
|
+
# track's name up by its real index. existing_roles is a *packed*
|
|
96
|
+
# list (unnamed/errored tracks are skipped), so indexing it by track
|
|
97
|
+
# index misaligns names with the clip that produced the notes.
|
|
98
|
+
track_names_by_index: dict[int, str] = {}
|
|
89
99
|
|
|
90
100
|
try:
|
|
91
101
|
ableton = ctx.lifespan_context["ableton"]
|
|
@@ -100,6 +110,7 @@ def evaluate_sample_fit(
|
|
|
100
110
|
name = track_info.get("name", "").lower()
|
|
101
111
|
if name:
|
|
102
112
|
existing_roles.append(name)
|
|
113
|
+
track_names_by_index[i] = name
|
|
103
114
|
except Exception as exc:
|
|
104
115
|
logger.debug("get_track_info(%d) skipped: %s", i, exc)
|
|
105
116
|
continue
|
|
@@ -147,9 +158,7 @@ def evaluate_sample_fit(
|
|
|
147
158
|
) else []
|
|
148
159
|
if not notes:
|
|
149
160
|
continue
|
|
150
|
-
track_name = (
|
|
151
|
-
existing_roles[i] if i < len(existing_roles) else ""
|
|
152
|
-
)
|
|
161
|
+
track_name = track_names_by_index.get(i, "")
|
|
153
162
|
if harmonic_score(notes, track_name) >= 0.3:
|
|
154
163
|
harmonic_pool.extend(notes)
|
|
155
164
|
|
|
@@ -189,7 +198,11 @@ def evaluate_sample_fit(
|
|
|
189
198
|
recommended_intent=intent,
|
|
190
199
|
surgeon_plan=surgeon_plan,
|
|
191
200
|
alchemist_plan=alchemist_plan,
|
|
192
|
-
warnings=[
|
|
201
|
+
warnings=[
|
|
202
|
+
c.recommendation
|
|
203
|
+
for c in critics.values()
|
|
204
|
+
if getattr(c, "available", True) and c.score < 0.5
|
|
205
|
+
],
|
|
193
206
|
)
|
|
194
207
|
return report.to_dict()
|
|
195
208
|
|
|
@@ -316,12 +329,16 @@ async def search_samples(
|
|
|
316
329
|
if not used_grpc:
|
|
317
330
|
splice = SpliceSource()
|
|
318
331
|
if splice.enabled:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
332
|
+
# Offload blocking SQLite query off the event loop.
|
|
333
|
+
splice_results = await asyncio.get_running_loop().run_in_executor(
|
|
334
|
+
None,
|
|
335
|
+
lambda: splice.search(
|
|
336
|
+
query=query,
|
|
337
|
+
max_results=max_results,
|
|
338
|
+
key=key,
|
|
339
|
+
bpm_min=bpm_min,
|
|
340
|
+
bpm_max=bpm_max,
|
|
341
|
+
),
|
|
325
342
|
)
|
|
326
343
|
for candidate in splice_results:
|
|
327
344
|
d = candidate.to_dict()
|
|
@@ -335,12 +352,16 @@ async def search_samples(
|
|
|
335
352
|
browser = BrowserSource()
|
|
336
353
|
for category in browser.DEFAULT_CATEGORIES:
|
|
337
354
|
try:
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
"
|
|
342
|
-
|
|
343
|
-
|
|
355
|
+
# Offload blocking TCP round-trip off the event loop.
|
|
356
|
+
search_result = await asyncio.get_running_loop().run_in_executor(
|
|
357
|
+
None,
|
|
358
|
+
lambda: ableton.send_command("search_browser", {
|
|
359
|
+
"path": category,
|
|
360
|
+
"name_filter": query,
|
|
361
|
+
"loadable_only": True,
|
|
362
|
+
"max_results": max_results,
|
|
363
|
+
}),
|
|
364
|
+
)
|
|
344
365
|
raw = search_result.get("results", [])
|
|
345
366
|
parsed = browser.parse_results(raw, category)
|
|
346
367
|
for candidate in parsed:
|
|
@@ -359,7 +380,10 @@ async def search_samples(
|
|
|
359
380
|
"~/Music", "~/Documents/Samples",
|
|
360
381
|
"~/Documents/LivePilot/downloads",
|
|
361
382
|
])
|
|
362
|
-
|
|
383
|
+
# Offload blocking recursive filesystem scan off the event loop.
|
|
384
|
+
fs_results = await asyncio.get_running_loop().run_in_executor(
|
|
385
|
+
None, lambda: fs.search(query, max_results=max_results)
|
|
386
|
+
)
|
|
363
387
|
for candidate in fs_results:
|
|
364
388
|
d = candidate.to_dict()
|
|
365
389
|
d["source_priority"] = 3
|
|
@@ -1049,10 +1073,18 @@ async def splice_download_sample(
|
|
|
1049
1073
|
if copy_to_user_library:
|
|
1050
1074
|
dest_dir = os.path.expanduser(_SPLICE_USER_LIB_DEST)
|
|
1051
1075
|
try:
|
|
1052
|
-
os.makedirs(dest_dir, exist_ok=True)
|
|
1053
1076
|
dest_path = os.path.join(dest_dir, os.path.basename(local_path))
|
|
1054
|
-
|
|
1055
|
-
|
|
1077
|
+
|
|
1078
|
+
def _copy_into_user_library():
|
|
1079
|
+
os.makedirs(dest_dir, exist_ok=True)
|
|
1080
|
+
if not os.path.exists(dest_path):
|
|
1081
|
+
shutil.copy2(local_path, dest_path)
|
|
1082
|
+
|
|
1083
|
+
# Offload the blocking filesystem copy off the event loop,
|
|
1084
|
+
# mirroring the run_in_executor used in splice_preview_sample.
|
|
1085
|
+
await asyncio.get_running_loop().run_in_executor(
|
|
1086
|
+
None, _copy_into_user_library
|
|
1087
|
+
)
|
|
1056
1088
|
response["user_library_path"] = dest_path
|
|
1057
1089
|
# URI format Ableton uses for user_library samples
|
|
1058
1090
|
response["browser_uri"] = (
|
|
@@ -566,7 +566,23 @@ def detect_identity_drift(
|
|
|
566
566
|
drift.drift_score += 0.2 * len(lost)
|
|
567
567
|
|
|
568
568
|
# Energy arc shift
|
|
569
|
-
|
|
569
|
+
# Align on section_id rather than list position: inserting/removing/
|
|
570
|
+
# renaming-to-empty a scene shifts energy_arc indices (the BUG-B12 skip
|
|
571
|
+
# drops empty sections), so a positional diff fabricates drift on every
|
|
572
|
+
# section after the change. Diffing the shared section_ids compares like
|
|
573
|
+
# with like and is stable under reordering/insertion.
|
|
574
|
+
before_energy = {s.section_id: s.energy_level for s in before.section_purposes}
|
|
575
|
+
after_energy = {s.section_id: s.energy_level for s in after.section_purposes}
|
|
576
|
+
shared_ids = before_energy.keys() & after_energy.keys()
|
|
577
|
+
if shared_ids:
|
|
578
|
+
diff = sum(
|
|
579
|
+
abs(before_energy[sid] - after_energy[sid]) for sid in shared_ids
|
|
580
|
+
) / len(shared_ids)
|
|
581
|
+
drift.energy_arc_shift = round(diff, 3)
|
|
582
|
+
drift.drift_score += diff * 0.2
|
|
583
|
+
elif before.energy_arc and after.energy_arc:
|
|
584
|
+
# Fallback for snapshots that carry energy_arc but no section_purposes
|
|
585
|
+
# (e.g. older brains): keep the legacy positional comparison.
|
|
570
586
|
min_len = min(len(before.energy_arc), len(after.energy_arc))
|
|
571
587
|
if min_len > 0:
|
|
572
588
|
diff = sum(
|
|
@@ -408,7 +408,7 @@ def _cross_engine_hint_for_track(
|
|
|
408
408
|
return None
|
|
409
409
|
track_issues = [
|
|
410
410
|
i for i in mix_issues
|
|
411
|
-
if getattr(i, "
|
|
411
|
+
if track_index in (getattr(i, "affected_tracks", []) or [])
|
|
412
412
|
]
|
|
413
413
|
if not track_issues:
|
|
414
414
|
return None
|