livepilot 1.26.1 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +9 -8
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/agents/livepilot-producer/AGENT.md +2 -2
- package/livepilot/commands/beat.md +3 -3
- package/livepilot/commands/evaluate.md +3 -1
- package/livepilot/commands/mix.md +2 -2
- package/livepilot/commands/sounddesign.md +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +10 -10
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +2 -1
- package/livepilot/skills/livepilot-core/references/overview.md +5 -3
- package/livepilot/skills/livepilot-core/references/sound-design.md +5 -2
- package/livepilot/skills/livepilot-creative-director/SKILL.md +43 -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-mix-engine/SKILL.md +9 -1
- package/livepilot/skills/livepilot-mixing/SKILL.md +12 -5
- package/livepilot/skills/livepilot-release/SKILL.md +9 -7
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +25 -3
- 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/composer/full/brief_builder.py +9 -0
- package/mcp_server/evaluation/feature_extractors.py +152 -8
- package/mcp_server/mix_engine/state_builder.py +19 -2
- package/mcp_server/mix_engine/tools.py +22 -0
- 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 +18 -0
- package/mcp_server/runtime/session_kernel.py +7 -0
- package/mcp_server/runtime/tools.py +324 -22
- package/mcp_server/sound_design/tools.py +33 -0
- package/mcp_server/tools/_agent_os_engine/evaluation.py +7 -44
- package/mcp_server/tools/_agent_os_engine/models.py +2 -1
- package/mcp_server/tools/_conductor.py +5 -2
- package/mcp_server/tools/_evaluation_contracts.py +1 -1
- package/mcp_server/tools/_snapshot_normalizer.py +32 -3
- package/mcp_server/tools/analyzer.py +38 -1
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/version_detect.py +3 -0
- package/requirements.txt +3 -3
- 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
|
)
|
|
@@ -9,6 +9,8 @@ from __future__ import annotations
|
|
|
9
9
|
from fastmcp import Context
|
|
10
10
|
|
|
11
11
|
from ..server import mcp
|
|
12
|
+
from ..evaluation.feature_extractors import extract_character_profile
|
|
13
|
+
from ..tools._snapshot_normalizer import normalize_sonic_snapshot
|
|
12
14
|
from .models import (
|
|
13
15
|
LayerStrategy,
|
|
14
16
|
PatchBlock,
|
|
@@ -189,9 +191,36 @@ def _fetch_sound_design_data(ctx: Context, track_index: int) -> dict:
|
|
|
189
191
|
# Get devices from track_info response (already included by Remote Script)
|
|
190
192
|
devices: list[dict] = track_info.get("devices", [])
|
|
191
193
|
|
|
194
|
+
sonic = None
|
|
195
|
+
try:
|
|
196
|
+
spectral = ctx.lifespan_context.get("spectral")
|
|
197
|
+
if spectral and spectral.is_connected:
|
|
198
|
+
sonic = {}
|
|
199
|
+
spec_data = spectral.get("spectrum")
|
|
200
|
+
if spec_data:
|
|
201
|
+
sonic["bands"] = spec_data["value"]
|
|
202
|
+
rms_snap = spectral.get("rms")
|
|
203
|
+
if rms_snap:
|
|
204
|
+
sonic["rms"] = rms_snap["value"]
|
|
205
|
+
peak_snap = spectral.get("peak")
|
|
206
|
+
if peak_snap:
|
|
207
|
+
sonic["peak"] = peak_snap["value"]
|
|
208
|
+
key_snap = spectral.get("key")
|
|
209
|
+
if key_snap:
|
|
210
|
+
sonic["detected_key"] = key_snap["value"]
|
|
211
|
+
for key in ("spectral_shape", "mel_bands", "chroma", "onset", "novelty", "loudness"):
|
|
212
|
+
snap = spectral.get(key)
|
|
213
|
+
if snap:
|
|
214
|
+
sonic[key] = snap["value"]
|
|
215
|
+
if not sonic:
|
|
216
|
+
sonic = None
|
|
217
|
+
except Exception:
|
|
218
|
+
sonic = None
|
|
219
|
+
|
|
192
220
|
return {
|
|
193
221
|
"track_info": track_info,
|
|
194
222
|
"devices": devices,
|
|
223
|
+
"sonic_snapshot": normalize_sonic_snapshot(sonic, source="sound_design"),
|
|
195
224
|
}
|
|
196
225
|
|
|
197
226
|
|
|
@@ -262,9 +291,11 @@ def analyze_sound_design(ctx: Context, track_index: int) -> dict:
|
|
|
262
291
|
issues, data["track_info"].get("name", "")
|
|
263
292
|
)
|
|
264
293
|
moves = plan_sound_design_moves(issues, state)
|
|
294
|
+
sonic_character = extract_character_profile(data.get("sonic_snapshot") or {})
|
|
265
295
|
|
|
266
296
|
return {
|
|
267
297
|
"state": state.to_dict(),
|
|
298
|
+
"sonic_character": sonic_character,
|
|
268
299
|
"issues": [i.to_dict() for i in issues],
|
|
269
300
|
"suggested_moves": [m.to_dict() for m in moves],
|
|
270
301
|
"issue_count": len(issues),
|
|
@@ -295,6 +326,7 @@ def get_sound_design_issues(ctx: Context, track_index: int) -> dict:
|
|
|
295
326
|
)
|
|
296
327
|
|
|
297
328
|
return {
|
|
329
|
+
"sonic_character": extract_character_profile(data.get("sonic_snapshot") or {}),
|
|
298
330
|
"issues": [i.to_dict() for i in issues],
|
|
299
331
|
"issue_count": len(issues),
|
|
300
332
|
}
|
|
@@ -330,6 +362,7 @@ def plan_sound_design_move(ctx: Context, track_index: int) -> dict:
|
|
|
330
362
|
moves = plan_sound_design_moves(issues, state)
|
|
331
363
|
|
|
332
364
|
result: dict = {
|
|
365
|
+
"sonic_character": extract_character_profile(data.get("sonic_snapshot") or {}),
|
|
333
366
|
"moves": [m.to_dict() for m in moves],
|
|
334
367
|
"move_count": len(moves),
|
|
335
368
|
"issue_count": len(issues),
|
|
@@ -11,6 +11,10 @@ import re
|
|
|
11
11
|
from dataclasses import asdict, dataclass, field
|
|
12
12
|
from typing import Any, Optional
|
|
13
13
|
|
|
14
|
+
from ...evaluation.feature_extractors import (
|
|
15
|
+
extract_dimension_value as _shared_extract_dimension_value,
|
|
16
|
+
)
|
|
17
|
+
from .._snapshot_normalizer import normalize_sonic_snapshot
|
|
14
18
|
from .models import QUALITY_DIMENSIONS, GoalVector, WorldModel, _clamp
|
|
15
19
|
from .taste import compute_taste_fit
|
|
16
20
|
|
|
@@ -29,50 +33,10 @@ def _extract_dimension_value(
|
|
|
29
33
|
"""
|
|
30
34
|
if not sonic:
|
|
31
35
|
return None
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# Finding 2 fix: tolerate either shape so raw analyzer output works.
|
|
35
|
-
bands = sonic.get("spectrum") or sonic.get("bands")
|
|
36
|
-
if not bands:
|
|
37
|
-
return None
|
|
38
|
-
rms = sonic.get("rms")
|
|
39
|
-
peak = sonic.get("peak")
|
|
40
|
-
|
|
41
|
-
if dimension == "brightness":
|
|
42
|
-
high = bands.get("high", 0)
|
|
43
|
-
presence = bands.get("presence", 0)
|
|
44
|
-
return _clamp((high + presence) / 2.0)
|
|
45
|
-
elif dimension == "warmth":
|
|
46
|
-
return _clamp(bands.get("low_mid", 0))
|
|
47
|
-
elif dimension == "weight":
|
|
48
|
-
sub = bands.get("sub", 0)
|
|
49
|
-
low = bands.get("low", 0)
|
|
50
|
-
return _clamp((sub + low) / 2.0)
|
|
51
|
-
elif dimension == "clarity":
|
|
52
|
-
low_mid = bands.get("low_mid", 0)
|
|
53
|
-
return _clamp(1.0 - low_mid)
|
|
54
|
-
elif dimension == "density":
|
|
55
|
-
# Spectral flatness: geometric mean / arithmetic mean of band values.
|
|
56
|
-
# Higher = more evenly distributed energy (noise-like).
|
|
57
|
-
# Lower = more tonal (energy concentrated in few bands).
|
|
58
|
-
vals = [max(v, 1e-10) for v in bands.values() if isinstance(v, (int, float))]
|
|
59
|
-
if not vals:
|
|
60
|
-
return None
|
|
61
|
-
geo_mean = math.exp(sum(math.log(v) for v in vals) / len(vals))
|
|
62
|
-
arith_mean = sum(vals) / len(vals)
|
|
63
|
-
return _clamp(geo_mean / max(arith_mean, 1e-10))
|
|
64
|
-
elif dimension == "energy":
|
|
65
|
-
return _clamp(rms) if rms is not None else None
|
|
66
|
-
elif dimension == "punch":
|
|
67
|
-
if rms and peak and rms > 0:
|
|
68
|
-
crest_db = 20.0 * math.log10(max(peak / rms, 1.0))
|
|
69
|
-
# Normalize: 0 dB = 0.0, 20 dB = 1.0
|
|
70
|
-
return _clamp(crest_db / 20.0)
|
|
71
|
-
return None
|
|
72
|
-
else:
|
|
73
|
-
# Unmeasurable in Phase 1 (width, depth, motion, contrast,
|
|
74
|
-
# groove, tension, novelty, polish, emotion, cohesion)
|
|
36
|
+
normalized = normalize_sonic_snapshot(sonic, source="agent_os")
|
|
37
|
+
if normalized is None:
|
|
75
38
|
return None
|
|
39
|
+
return _shared_extract_dimension_value(normalized, dimension)
|
|
76
40
|
|
|
77
41
|
def compute_evaluation_score(
|
|
78
42
|
goal: GoalVector,
|
|
@@ -203,4 +167,3 @@ def compute_evaluation_score(
|
|
|
203
167
|
# I5: hint for the agent to track consecutive undos
|
|
204
168
|
"consecutive_undo_hint": not keep_change,
|
|
205
169
|
}
|
|
206
|
-
|
|
@@ -33,6 +33,8 @@ MEASURABLE_PROXIES: dict[str, str] = {
|
|
|
33
33
|
"density": "spectral flatness (geometric/arithmetic mean ratio)",
|
|
34
34
|
"energy": "RMS level",
|
|
35
35
|
"punch": "crest factor in dB (20*log10(peak/rms))",
|
|
36
|
+
"motion": "spectral novelty + onset strength",
|
|
37
|
+
"novelty": "FluCoMa novelty score",
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
VALID_MODES = frozenset({"observe", "improve", "explore", "finish", "diagnose"})
|
|
@@ -129,4 +131,3 @@ class TechniqueCard:
|
|
|
129
131
|
"verification": self.verification,
|
|
130
132
|
"evidence": self.evidence,
|
|
131
133
|
}
|
|
132
|
-
|
|
@@ -70,7 +70,7 @@ class ConductorPlan:
|
|
|
70
70
|
_ROUTING_PATTERNS: list[tuple[str, str, str, str, list[str]]] = [
|
|
71
71
|
# Mix requests
|
|
72
72
|
(r"clean|mud|muddy|low.?mid|eq|equaliz", "mix_engine", "mix", "analyze_mix", ["plan_mix_move", "evaluate_mix_move"]),
|
|
73
|
-
(r"
|
|
73
|
+
(r"dynamics|compress|crest|over.?compress|flat.?dynamics", "mix_engine", "mix", "analyze_mix", ["plan_mix_move"]),
|
|
74
74
|
(r"wide|wider|width|stereo|narrow|mono.?compat", "mix_engine", "mix", "analyze_mix", ["plan_mix_move"]),
|
|
75
75
|
(r"glue|cohes|bus.?comp|mix.?bus", "mix_engine", "mix", "analyze_mix", ["plan_mix_move"]),
|
|
76
76
|
(r"balance|level|volume.?balanc|gain.?stag", "mix_engine", "mix", "analyze_mix", ["plan_mix_move"]),
|
|
@@ -87,6 +87,7 @@ _ROUTING_PATTERNS: list[tuple[str, str, str, str, list[str]]] = [
|
|
|
87
87
|
|
|
88
88
|
# Sound design requests
|
|
89
89
|
(r"synth|patch|oscillat|timbre|timbral|wavetable|operator", "sound_design", "sound_design", "analyze_sound_design", ["plan_sound_design_move"]),
|
|
90
|
+
(r"punch|punchy|hit.?harder|snap|attack|transient", "sound_design", "sound_design", "analyze_sound_design", ["plan_sound_design_move"]),
|
|
90
91
|
(r"haunted|lush|aggressive|warm.?pad|fat.?bass|bright.?lead", "sound_design", "sound_design", "analyze_sound_design", ["plan_sound_design_move"]),
|
|
91
92
|
(r"modulation|lfo|movement|evolv|texture", "sound_design", "sound_design", "get_patch_model", ["analyze_sound_design"]),
|
|
92
93
|
(r"layer|sub.?layer|transient.?layer|body", "sound_design", "sound_design", "analyze_sound_design", ["plan_sound_design_move"]),
|
|
@@ -254,7 +255,7 @@ def classify_request(request: str) -> ConductorPlan:
|
|
|
254
255
|
|
|
255
256
|
# Determine capability requirements
|
|
256
257
|
caps = ["session_access"]
|
|
257
|
-
if any(r.engine
|
|
258
|
+
if any(r.engine in ("mix_engine", "sound_design") for r in routes):
|
|
258
259
|
caps.append("analyzer")
|
|
259
260
|
if any(r.engine in ("reference_engine",) for r in routes):
|
|
260
261
|
caps.append("offline_perception")
|
|
@@ -267,6 +268,8 @@ def classify_request(request: str) -> ConductorPlan:
|
|
|
267
268
|
notes.append("Multi-engine task — start with get_session_kernel for shared state")
|
|
268
269
|
if any(r.engine == "mix_engine" for r in routes):
|
|
269
270
|
notes.append("Mix engine works best with analyzer data — check get_capability_state")
|
|
271
|
+
if any(r.engine == "sound_design" for r in routes):
|
|
272
|
+
notes.append("Sound design should use analyzer character before level or pan changes")
|
|
270
273
|
|
|
271
274
|
# V2: Search semantic moves for matching intents
|
|
272
275
|
semantic_moves = _find_matching_semantic_moves(lower)
|
|
@@ -22,7 +22,7 @@ from typing import Optional
|
|
|
22
22
|
# must report confidence=0.0 for that dimension.
|
|
23
23
|
MEASURABLE_DIMENSIONS: frozenset[str] = frozenset({
|
|
24
24
|
"brightness", "warmth", "weight", "clarity",
|
|
25
|
-
"density", "energy", "punch",
|
|
25
|
+
"density", "energy", "punch", "motion", "novelty",
|
|
26
26
|
})
|
|
27
27
|
|
|
28
28
|
# All valid quality dimensions (measurable + unmeasurable).
|