livepilot 1.17.1 → 1.17.3
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 +183 -0
- package/README.md +8 -7
- package/m4l_device/BUILD_GUIDE.md +24 -20
- 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/m4l_bridge.py +2 -1
- package/mcp_server/preview_studio/engine.py +85 -11
- package/mcp_server/preview_studio/models.py +8 -0
- package/mcp_server/preview_studio/tools.py +107 -51
- package/mcp_server/runtime/capability_state.py +18 -0
- package/mcp_server/runtime/degradation.py +62 -0
- package/mcp_server/runtime/tools.py +61 -4
- package/mcp_server/song_brain/tools.py +23 -0
- package/mcp_server/synthesis_brain/timbre.py +14 -8
- package/mcp_server/tools/_agent_os_engine/__init__.py +10 -0
- package/mcp_server/tools/_agent_os_engine/iteration.py +481 -0
- package/mcp_server/tools/agent_os.py +194 -3
- package/mcp_server/tools/analyzer.py +19 -6
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/requirements.txt +5 -5
- package/server.json +3 -3
|
@@ -270,7 +270,15 @@ async def commit_preview_variant(
|
|
|
270
270
|
if not ps:
|
|
271
271
|
return {"error": f"Preview set {set_id} not found"}
|
|
272
272
|
|
|
273
|
-
chosen
|
|
273
|
+
# Resolve the chosen variant WITHOUT mutating state yet. We have to
|
|
274
|
+
# short-circuit analytical-only / blocked picks BEFORE engine.commit_variant
|
|
275
|
+
# runs, otherwise `preview_set.status` gets flipped to "committed" and
|
|
276
|
+
# sibling variants get discarded even though nothing executed.
|
|
277
|
+
chosen = None
|
|
278
|
+
for v in ps.variants:
|
|
279
|
+
if v.variant_id == variant_id:
|
|
280
|
+
chosen = v
|
|
281
|
+
break
|
|
274
282
|
if not chosen:
|
|
275
283
|
available = [v.variant_id for v in ps.variants]
|
|
276
284
|
return {
|
|
@@ -278,8 +286,58 @@ async def commit_preview_variant(
|
|
|
278
286
|
"available_variants": available,
|
|
279
287
|
}
|
|
280
288
|
|
|
289
|
+
# ── Truth-gap guard: refuse to "commit" a variant that can't execute ──
|
|
290
|
+
# If the variant was flagged blocked/failed upstream or lacks a
|
|
291
|
+
# compiled plan, the old code still marked preview_set.status='committed'
|
|
292
|
+
# and returned committed=False as a silent contradiction. Close that
|
|
293
|
+
# gap: return an honest no-op and leave state untouched so the caller
|
|
294
|
+
# can pick a different variant.
|
|
295
|
+
plan = chosen.compiled_plan
|
|
296
|
+
plan_is_empty = (
|
|
297
|
+
plan is None
|
|
298
|
+
or (isinstance(plan, list) and len(plan) == 0)
|
|
299
|
+
or (isinstance(plan, dict) and len(plan.get("steps") or []) == 0)
|
|
300
|
+
)
|
|
301
|
+
blocked = chosen.status in {"blocked", "failed"}
|
|
302
|
+
if plan_is_empty or blocked:
|
|
303
|
+
reason = "blocked" if blocked and plan_is_empty is False else "analytical_only"
|
|
304
|
+
return {
|
|
305
|
+
"committed": False,
|
|
306
|
+
"status": "analytical_only" if reason == "analytical_only" else "blocked",
|
|
307
|
+
"reason": reason,
|
|
308
|
+
"preview_set_id": set_id,
|
|
309
|
+
"variant_id": chosen.variant_id,
|
|
310
|
+
"label": chosen.label,
|
|
311
|
+
"intent": chosen.intent,
|
|
312
|
+
"move_id": chosen.move_id,
|
|
313
|
+
"identity_effect": chosen.identity_effect,
|
|
314
|
+
"what_preserved": chosen.what_preserved,
|
|
315
|
+
"message": (
|
|
316
|
+
"chose analytical variant; no session changes applied"
|
|
317
|
+
if reason == "analytical_only"
|
|
318
|
+
else "variant is blocked; no session changes applied"
|
|
319
|
+
),
|
|
320
|
+
"note": (
|
|
321
|
+
"Variant has no compiled plan (analytical-only). Preview set "
|
|
322
|
+
"was left in its pre-commit state so you can pick a different "
|
|
323
|
+
"variant."
|
|
324
|
+
if reason == "analytical_only"
|
|
325
|
+
else "Variant is blocked/failed. Preview set was left in its "
|
|
326
|
+
"pre-commit state so you can pick a different variant."
|
|
327
|
+
),
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# ── P1#2 fix (v1.17.3): execute BEFORE flipping state ──
|
|
331
|
+
# Prior behavior: engine.commit_variant() ran here, BEFORE execution.
|
|
332
|
+
# If every step then failed, the returned payload correctly said
|
|
333
|
+
# committed=False / status='failed' — but preview_set.status was
|
|
334
|
+
# already "committed" and Wonder lifecycle advance fired regardless.
|
|
335
|
+
# Response and stored state contradicted each other.
|
|
336
|
+
#
|
|
337
|
+
# New flow: we already have `chosen` from the resolution block above.
|
|
338
|
+
# Execute the plan first, count successes, THEN flip state only when
|
|
339
|
+
# at least one step actually applied. Zero successes = honest no-op.
|
|
281
340
|
result = {
|
|
282
|
-
"committed": True,
|
|
283
341
|
"variant_id": chosen.variant_id,
|
|
284
342
|
"label": chosen.label,
|
|
285
343
|
"intent": chosen.intent,
|
|
@@ -289,57 +347,55 @@ async def commit_preview_variant(
|
|
|
289
347
|
}
|
|
290
348
|
|
|
291
349
|
# ── v1.10.3: actually execute the compiled plan ──
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
350
|
+
from ..runtime.execution_router import execute_plan_steps_async
|
|
351
|
+
plan = chosen.compiled_plan
|
|
352
|
+
steps = plan if isinstance(plan, list) else plan.get("steps", []) or []
|
|
353
|
+
ableton = _get_ableton(ctx)
|
|
354
|
+
bridge = ctx.lifespan_context.get("m4l")
|
|
355
|
+
mcp_registry = ctx.lifespan_context.get("mcp_dispatch", {})
|
|
356
|
+
|
|
357
|
+
exec_results = await execute_plan_steps_async(
|
|
358
|
+
steps,
|
|
359
|
+
ableton=ableton,
|
|
360
|
+
bridge=bridge,
|
|
361
|
+
mcp_registry=mcp_registry,
|
|
362
|
+
ctx=ctx,
|
|
363
|
+
stop_on_failure=False,
|
|
364
|
+
)
|
|
365
|
+
log = [
|
|
366
|
+
{
|
|
367
|
+
"tool": r.tool,
|
|
368
|
+
"backend": r.backend,
|
|
369
|
+
"ok": r.ok,
|
|
370
|
+
**({"result": r.result} if r.ok else {"error": r.error}),
|
|
371
|
+
}
|
|
372
|
+
for r in exec_results
|
|
373
|
+
]
|
|
374
|
+
steps_ok = sum(1 for r in exec_results if r.ok)
|
|
375
|
+
steps_failed = len(exec_results) - steps_ok
|
|
376
|
+
|
|
377
|
+
result["execution_log"] = log
|
|
378
|
+
result["steps_ok"] = steps_ok
|
|
379
|
+
result["steps_failed"] = steps_failed
|
|
380
|
+
|
|
381
|
+
# ── P1#2: only flip preview-set state when at least one step succeeded ──
|
|
382
|
+
if steps_failed == 0 and steps_ok > 0:
|
|
383
|
+
result["status"] = "committed"
|
|
384
|
+
result["committed"] = True
|
|
385
|
+
engine.commit_variant(ps, variant_id)
|
|
386
|
+
elif steps_ok > 0:
|
|
387
|
+
result["status"] = "committed_with_errors"
|
|
388
|
+
result["committed"] = True # partial but real commit
|
|
389
|
+
engine.commit_variant(ps, variant_id)
|
|
302
390
|
else:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
391
|
+
# Every step failed — do NOT flip preview-set state, do NOT advance
|
|
392
|
+
# Wonder. The response already reflects the failure; the stored
|
|
393
|
+
# state must agree.
|
|
394
|
+
result["status"] = "failed"
|
|
395
|
+
result["committed"] = False
|
|
396
|
+
return result
|
|
309
397
|
|
|
310
|
-
|
|
311
|
-
steps,
|
|
312
|
-
ableton=ableton,
|
|
313
|
-
bridge=bridge,
|
|
314
|
-
mcp_registry=mcp_registry,
|
|
315
|
-
ctx=ctx,
|
|
316
|
-
stop_on_failure=False,
|
|
317
|
-
)
|
|
318
|
-
log = [
|
|
319
|
-
{
|
|
320
|
-
"tool": r.tool,
|
|
321
|
-
"backend": r.backend,
|
|
322
|
-
"ok": r.ok,
|
|
323
|
-
**({"result": r.result} if r.ok else {"error": r.error}),
|
|
324
|
-
}
|
|
325
|
-
for r in exec_results
|
|
326
|
-
]
|
|
327
|
-
steps_ok = sum(1 for r in exec_results if r.ok)
|
|
328
|
-
steps_failed = len(exec_results) - steps_ok
|
|
329
|
-
|
|
330
|
-
result["execution_log"] = log
|
|
331
|
-
result["steps_ok"] = steps_ok
|
|
332
|
-
result["steps_failed"] = steps_failed
|
|
333
|
-
|
|
334
|
-
if steps_failed == 0 and steps_ok > 0:
|
|
335
|
-
result["status"] = "committed"
|
|
336
|
-
elif steps_ok > 0:
|
|
337
|
-
result["status"] = "committed_with_errors"
|
|
338
|
-
else:
|
|
339
|
-
result["status"] = "failed"
|
|
340
|
-
result["committed"] = False
|
|
341
|
-
|
|
342
|
-
# Wonder lifecycle hooks
|
|
398
|
+
# Wonder lifecycle hooks — only reached when steps_ok > 0.
|
|
343
399
|
ws = _find_wonder_session_by_preview(set_id)
|
|
344
400
|
if ws:
|
|
345
401
|
ws.selected_variant_id = variant_id
|
|
@@ -155,6 +155,9 @@ def build_capability_state(
|
|
|
155
155
|
)
|
|
156
156
|
|
|
157
157
|
# ── web ──────────────────────────────────────────────────────────
|
|
158
|
+
# Server-side outbound HTTP capability. True when the MCP host can
|
|
159
|
+
# reach an arbitrary public URL. Does NOT imply curated research
|
|
160
|
+
# corpora are installed — see the ``research`` domain below.
|
|
158
161
|
web_reasons: list[str] = []
|
|
159
162
|
if not web_ok:
|
|
160
163
|
web_reasons.append("web_unavailable")
|
|
@@ -166,6 +169,21 @@ def build_capability_state(
|
|
|
166
169
|
reasons=web_reasons,
|
|
167
170
|
)
|
|
168
171
|
|
|
172
|
+
# ── flucoma ──────────────────────────────────────────────────────
|
|
173
|
+
# Optional dependency (the ``flucoma`` Python package). Emitted
|
|
174
|
+
# unconditionally so consumers can distinguish "probed and missing"
|
|
175
|
+
# from "probe not run yet".
|
|
176
|
+
flucoma_reasons: list[str] = []
|
|
177
|
+
if not flucoma_ok:
|
|
178
|
+
flucoma_reasons.append("flucoma_not_installed")
|
|
179
|
+
domains["flucoma"] = CapabilityDomain(
|
|
180
|
+
name="flucoma",
|
|
181
|
+
available=flucoma_ok,
|
|
182
|
+
confidence=0.9 if flucoma_ok else 0.0,
|
|
183
|
+
mode="available" if flucoma_ok else "unavailable",
|
|
184
|
+
reasons=flucoma_reasons,
|
|
185
|
+
)
|
|
186
|
+
|
|
169
187
|
# ── research (composite) ────────────────────────────────────────
|
|
170
188
|
research_reasons: list[str] = []
|
|
171
189
|
research_sources = 0
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Explicit degradation signalling for engines that fall back to synthesized data.
|
|
2
|
+
|
|
3
|
+
Before PR-B, several engines silently substituted defaults when a data
|
|
4
|
+
source failed — ``song_brain`` injected ``tempo=120.0, track_count=0``
|
|
5
|
+
on session-fetch failure, and ``preview_studio`` compiled variants
|
|
6
|
+
against an empty-but-valid kernel when the caller didn't supply one.
|
|
7
|
+
Downstream consumers had no way to tell synthesized data from real
|
|
8
|
+
data, so polished outputs were returned as if they were real.
|
|
9
|
+
|
|
10
|
+
``DegradationInfo`` is the shared payload engines attach to their
|
|
11
|
+
responses whenever they substitute fallback values. Consumers can
|
|
12
|
+
inspect ``is_degraded``, ``reasons``, and ``substituted_fields`` to
|
|
13
|
+
decide whether to trust the response or re-try the operation.
|
|
14
|
+
|
|
15
|
+
Usage::
|
|
16
|
+
|
|
17
|
+
from mcp_server.runtime.degradation import DegradationInfo
|
|
18
|
+
|
|
19
|
+
deg = DegradationInfo()
|
|
20
|
+
try:
|
|
21
|
+
data = fetch_real_data()
|
|
22
|
+
except Exception:
|
|
23
|
+
data = FALLBACK_DATA
|
|
24
|
+
deg = DegradationInfo(
|
|
25
|
+
is_degraded=True,
|
|
26
|
+
reasons=["data_source_unreachable"],
|
|
27
|
+
substituted_fields=["tempo", "track_count"],
|
|
28
|
+
)
|
|
29
|
+
return {..., "degradation": deg.to_dict()}
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class DegradationInfo:
|
|
39
|
+
"""A structured signal that an engine substituted fallback data.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
is_degraded: True when any field in the response was substituted
|
|
43
|
+
with a synthesized/default value. False means the response
|
|
44
|
+
is fully backed by real data sources.
|
|
45
|
+
reasons: Short machine-readable tokens describing why degradation
|
|
46
|
+
happened (e.g., ``"session_fetch_failed"``,
|
|
47
|
+
``"empty_kernel_fallback"``). Intentionally open-ended — the
|
|
48
|
+
set grows as new fallback paths get flagged.
|
|
49
|
+
substituted_fields: Names of top-level response fields whose
|
|
50
|
+
values came from the fallback path, not the real source.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
is_degraded: bool = False
|
|
54
|
+
reasons: list[str] = field(default_factory=list)
|
|
55
|
+
substituted_fields: list[str] = field(default_factory=list)
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
return {
|
|
59
|
+
"is_degraded": self.is_degraded,
|
|
60
|
+
"reasons": list(self.reasons),
|
|
61
|
+
"substituted_fields": list(self.substituted_fields),
|
|
62
|
+
}
|
|
@@ -7,6 +7,9 @@ Tools:
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import importlib.util
|
|
11
|
+
import logging
|
|
12
|
+
import urllib.request
|
|
10
13
|
from typing import Optional
|
|
11
14
|
|
|
12
15
|
from fastmcp import Context
|
|
@@ -14,13 +17,55 @@ from fastmcp import Context
|
|
|
14
17
|
from ..server import mcp
|
|
15
18
|
from ..memory.technique_store import TechniqueStore
|
|
16
19
|
from .capability_state import build_capability_state
|
|
17
|
-
import logging
|
|
18
20
|
|
|
19
21
|
logger = logging.getLogger(__name__)
|
|
20
22
|
|
|
21
23
|
_memory_store = TechniqueStore()
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
# ── Capability probes ──────────────────────────────────────────────────
|
|
27
|
+
#
|
|
28
|
+
# These helpers are module-level so tests can monkeypatch them directly.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _probe_web(timeout: float = 0.5) -> bool:
|
|
32
|
+
"""Server-side outbound HTTP probe.
|
|
33
|
+
|
|
34
|
+
True when the MCP host can reach an arbitrary public URL. Does NOT
|
|
35
|
+
imply curated research corpora are installed — see the ``research``
|
|
36
|
+
domain for that.
|
|
37
|
+
|
|
38
|
+
Implementation: a ``timeout``-second HEAD request to
|
|
39
|
+
``https://api.github.com`` using stdlib ``urllib.request``. Any
|
|
40
|
+
exception (DNS failure, TLS error, socket timeout, proxy block,
|
|
41
|
+
non-2xx response) collapses to False so the probe is safe to call
|
|
42
|
+
from any code path.
|
|
43
|
+
"""
|
|
44
|
+
req = urllib.request.Request("https://api.github.com", method="HEAD")
|
|
45
|
+
try:
|
|
46
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
47
|
+
status = getattr(resp, "status", None)
|
|
48
|
+
return status is not None and 200 <= status < 400
|
|
49
|
+
except Exception as exc: # noqa: BLE001 — swallow everything to False
|
|
50
|
+
logger.debug("_probe_web failed: %s", exc)
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _probe_flucoma() -> bool:
|
|
55
|
+
"""Check whether the ``flucoma`` Python package is importable.
|
|
56
|
+
|
|
57
|
+
Uses ``importlib.util.find_spec`` so no import side-effects fire
|
|
58
|
+
(matching the pattern already used for optional capability probes
|
|
59
|
+
elsewhere in the codebase). Returns False if the package is missing
|
|
60
|
+
or if the spec lookup itself raises.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
return importlib.util.find_spec("flucoma") is not None
|
|
64
|
+
except Exception as exc: # noqa: BLE001
|
|
65
|
+
logger.debug("_probe_flucoma failed: %s", exc)
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
24
69
|
@mcp.tool()
|
|
25
70
|
def get_capability_state(ctx: Context) -> dict:
|
|
26
71
|
"""Probe the runtime and return a capability state snapshot.
|
|
@@ -59,9 +104,13 @@ def get_capability_state(ctx: Context) -> dict:
|
|
|
59
104
|
logger.debug("get_capability_state failed: %s", exc)
|
|
60
105
|
memory_ok = False
|
|
61
106
|
|
|
62
|
-
# ── Web
|
|
63
|
-
|
|
64
|
-
|
|
107
|
+
# ── Web — actually probe outbound HTTP egress ───────────────────
|
|
108
|
+
# Scoped to server-side outbound HTTP reachability; does NOT imply
|
|
109
|
+
# a curated research corpus is installed (see ``research`` domain).
|
|
110
|
+
web_ok = _probe_web()
|
|
111
|
+
|
|
112
|
+
# ── FluCoMa — optional import via find_spec (no side effects) ───
|
|
113
|
+
flucoma_ok = _probe_flucoma()
|
|
65
114
|
|
|
66
115
|
state = build_capability_state(
|
|
67
116
|
session_ok=session_ok,
|
|
@@ -130,11 +179,19 @@ def get_session_kernel(
|
|
|
130
179
|
if analyzer_ok:
|
|
131
180
|
analyzer_fresh = spectral.get("spectrum") is not None
|
|
132
181
|
|
|
182
|
+
# P2#3 (v1.17.3): probe web + flucoma the same way get_capability_state
|
|
183
|
+
# does, and propagate through. Without this the kernel's capability view
|
|
184
|
+
# lies to orchestration planners.
|
|
185
|
+
web_ok = _probe_web()
|
|
186
|
+
flucoma_ok = _probe_flucoma()
|
|
187
|
+
|
|
133
188
|
state = build_capability_state(
|
|
134
189
|
session_ok=session_ok,
|
|
135
190
|
analyzer_ok=analyzer_ok,
|
|
136
191
|
analyzer_fresh=analyzer_fresh,
|
|
137
192
|
memory_ok=True,
|
|
193
|
+
web_ok=web_ok,
|
|
194
|
+
flucoma_ok=flucoma_ok,
|
|
138
195
|
)
|
|
139
196
|
|
|
140
197
|
# Optional subcomponents — degrade gracefully, but reach into the SAME
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from fastmcp import Context
|
|
11
11
|
|
|
12
|
+
from ..runtime.degradation import DegradationInfo
|
|
12
13
|
from ..server import mcp
|
|
13
14
|
from . import builder
|
|
14
15
|
from .models import SongBrain
|
|
@@ -55,6 +56,12 @@ def _fetch_session_data(ctx: Context) -> dict:
|
|
|
55
56
|
- composition_analysis: from musical intelligence section inference
|
|
56
57
|
- role_graph: from semantic move resolvers (track role inference)
|
|
57
58
|
- recent_moves: from session-scoped action ledger
|
|
59
|
+
|
|
60
|
+
On session-fetch failure the fallback session_info shape is injected
|
|
61
|
+
(``tempo=120.0, track_count=0``) and a ``DegradationInfo`` is attached
|
|
62
|
+
under the ``_degradation`` key so callers can tell synthesized data
|
|
63
|
+
from real data. ``_fetch_session_data`` never raises — it always
|
|
64
|
+
returns a dict with the expected keys.
|
|
58
65
|
"""
|
|
59
66
|
ableton = _get_ableton(ctx)
|
|
60
67
|
data: dict = {
|
|
@@ -66,12 +73,19 @@ def _fetch_session_data(ctx: Context) -> dict:
|
|
|
66
73
|
"role_graph": {},
|
|
67
74
|
"recent_moves": [],
|
|
68
75
|
}
|
|
76
|
+
degradation = DegradationInfo()
|
|
69
77
|
|
|
70
78
|
try:
|
|
71
79
|
data["session_info"] = ableton.send_command("get_session_info", {})
|
|
72
80
|
except Exception as exc:
|
|
73
81
|
logger.debug("_fetch_session_data failed: %s", exc)
|
|
74
82
|
data["session_info"] = {"tempo": 120.0, "track_count": 0}
|
|
83
|
+
degradation.is_degraded = True
|
|
84
|
+
if "session_fetch_failed" not in degradation.reasons:
|
|
85
|
+
degradation.reasons.append("session_fetch_failed")
|
|
86
|
+
for fld in ("tempo", "track_count"):
|
|
87
|
+
if fld not in degradation.substituted_fields:
|
|
88
|
+
degradation.substituted_fields.append(fld)
|
|
75
89
|
|
|
76
90
|
try:
|
|
77
91
|
matrix = ableton.send_command("get_scene_matrix")
|
|
@@ -135,6 +149,10 @@ def _fetch_session_data(ctx: Context) -> dict:
|
|
|
135
149
|
except Exception as exc:
|
|
136
150
|
logger.debug("_fetch_session_data failed: %s", exc)
|
|
137
151
|
|
|
152
|
+
# Attach the degradation signal so build_song_brain can surface it.
|
|
153
|
+
# Under a reserved key (leading underscore) so it never collides with
|
|
154
|
+
# a real session data field.
|
|
155
|
+
data["_degradation"] = degradation
|
|
138
156
|
return data
|
|
139
157
|
|
|
140
158
|
|
|
@@ -180,10 +198,15 @@ def build_song_brain(ctx: Context) -> dict:
|
|
|
180
198
|
)
|
|
181
199
|
_set_brain(ctx, brain)
|
|
182
200
|
|
|
201
|
+
# Surface the degradation payload so callers can distinguish a
|
|
202
|
+
# tempo=120 / track_count=0 synthesized response from a real one.
|
|
203
|
+
degradation = data.get("_degradation") or DegradationInfo()
|
|
204
|
+
|
|
183
205
|
return {
|
|
184
206
|
**brain.to_dict(),
|
|
185
207
|
"summary": brain.summary,
|
|
186
208
|
"capability": cap.to_dict(),
|
|
209
|
+
"degradation": degradation.to_dict(),
|
|
187
210
|
}
|
|
188
211
|
|
|
189
212
|
|
|
@@ -19,12 +19,16 @@ from .models import TimbralFingerprint
|
|
|
19
19
|
|
|
20
20
|
# ── Band-based brightness / warmth mapping ──────────────────────────────
|
|
21
21
|
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
22
|
+
# Two upstream producers feed this extractor with different band schemas:
|
|
23
|
+
# 1. get_master_spectrum (M4L analyzer) — v1.16+: 9 bands (sub_low,
|
|
24
|
+
# sub, low, low_mid, mid, high_mid, high, presence, air);
|
|
25
|
+
# pre-v1.16: 8 bands (no sub_low).
|
|
26
|
+
# 2. analyze_spectrum_offline — 8 bands with legacy names
|
|
27
|
+
# (sub, low, low_mid, mid, high_mid, high, very_high, ultra).
|
|
28
|
+
# We index the union of both name sets below; `_band_energy` uses dict.get
|
|
29
|
+
# so missing bands simply return 0 without complaint.
|
|
26
30
|
|
|
27
|
-
_BANDS = ("sub", "low", "low_mid", "mid", "high_mid", "high", "very_high", "ultra")
|
|
31
|
+
_BANDS = ("sub_low", "sub", "low", "low_mid", "mid", "high_mid", "high", "presence", "air", "very_high", "ultra")
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
def _band_energy(spectrum: Optional[dict], band: str) -> float:
|
|
@@ -55,9 +59,11 @@ def extract_timbre_fingerprint(
|
|
|
55
59
|
Inputs are all optional — the function degrades gracefully when only
|
|
56
60
|
some dimensions are measurable.
|
|
57
61
|
|
|
58
|
-
spectrum: either
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
spectrum: either the 9-band shape from get_master_spectrum
|
|
63
|
+
({sub_low, sub, low, low_mid, mid, high_mid, high, presence, air}),
|
|
64
|
+
the legacy 8-band shape from analyze_spectrum_offline
|
|
65
|
+
({sub, low, low_mid, mid, high_mid, high, very_high, ultra}),
|
|
66
|
+
or {"bands": {...}} wrapping either. Missing bands default to 0.
|
|
61
67
|
loudness: {"rms": float, "peak": float, "lufs": float, "lra": float} —
|
|
62
68
|
output shape from analyze_loudness.
|
|
63
69
|
spectral_shape: FluCoMa descriptors when available — {"centroid", "flatness",
|
|
@@ -35,6 +35,12 @@ from .taste import (
|
|
|
35
35
|
compute_taste_fit,
|
|
36
36
|
get_taste_profile,
|
|
37
37
|
)
|
|
38
|
+
from .iteration import (
|
|
39
|
+
iterate_toward_goal_engine,
|
|
40
|
+
iterate_toward_goal_engine_async,
|
|
41
|
+
IterationResult,
|
|
42
|
+
IterationStep,
|
|
43
|
+
)
|
|
38
44
|
|
|
39
45
|
__all__ = [
|
|
40
46
|
"QUALITY_DIMENSIONS", "MEASURABLE_PROXIES",
|
|
@@ -49,4 +55,8 @@ __all__ = [
|
|
|
49
55
|
"analyze_outcome_history",
|
|
50
56
|
"compute_taste_fit",
|
|
51
57
|
"get_taste_profile",
|
|
58
|
+
"iterate_toward_goal_engine",
|
|
59
|
+
"iterate_toward_goal_engine_async",
|
|
60
|
+
"IterationResult",
|
|
61
|
+
"IterationStep",
|
|
52
62
|
]
|