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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,188 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.17.3 — Truth-gap remediation, for real (April 23 2026)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`iterate_toward_goal` now inspects `commit_fn` return value** (P1,
|
|
8
|
+
`mcp_server/tools/_agent_os_engine/iteration.py`): prior to this release
|
|
9
|
+
the iteration loop awaited the commit callback and dropped the return
|
|
10
|
+
value on the floor, then unconditionally returned `status="committed"`.
|
|
11
|
+
If the underlying `commit_branch_async` applied zero steps or partially
|
|
12
|
+
succeeded, the iteration result claimed success — the exact bug pattern
|
|
13
|
+
the release was meant to fix elsewhere. New `_classify_commit_result()`
|
|
14
|
+
helper maps known payload shapes to three statuses: `"committed"` (clean),
|
|
15
|
+
`"committed_with_errors"` (steps_ok > 0 AND steps_failed > 0), and
|
|
16
|
+
`"commit_failed"` (committed=False, ok=False, status="failed", or
|
|
17
|
+
steps_ok == 0). Both sync and async cores now zero out
|
|
18
|
+
`committed_experiment_id`/`committed_branch_id` when the commit truly
|
|
19
|
+
failed, and surface the raw commit payload on `IterationResult.commit_result`.
|
|
20
|
+
- **Preview Studio commit-before-execute ordering** (P1,
|
|
21
|
+
`mcp_server/preview_studio/tools.py`): `commit_preview_variant()` called
|
|
22
|
+
`engine.commit_variant()` BEFORE `execute_plan_steps_async` ran. That
|
|
23
|
+
flipped `preview_set.status = "committed"` and `committed_variant_id`
|
|
24
|
+
up front, so when every execution step failed the response correctly
|
|
25
|
+
said `committed: false / status: "failed"` but the stored state still
|
|
26
|
+
said the opposite. Wonder lifecycle advance also fired regardless.
|
|
27
|
+
Reorder: execute first, then flip state only when `steps_ok > 0`.
|
|
28
|
+
Zero-success path now returns honestly and leaves `preview_set` and
|
|
29
|
+
WonderSession untouched. Partial-success stays a legitimate commit
|
|
30
|
+
with `status="committed_with_errors"`.
|
|
31
|
+
- **`get_session_kernel` propagates web + flucoma probe results** (P2,
|
|
32
|
+
`mcp_server/runtime/tools.py`): the kernel builder called
|
|
33
|
+
`build_capability_state(...)` with only session/analyzer/memory params,
|
|
34
|
+
so `web_ok` and `flucoma_ok` silently defaulted to `False`. Meanwhile
|
|
35
|
+
`get_capability_state()` correctly probed both. Planners that read
|
|
36
|
+
the kernel (the documented orchestration entrypoint) stayed on
|
|
37
|
+
degraded paths even when probes would have reported available. Fix:
|
|
38
|
+
call `_probe_web()` + `_probe_flucoma()` inside `get_session_kernel`
|
|
39
|
+
and pass through.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **10 new tests** covering the three truth-gap classes:
|
|
44
|
+
- `test_iterate_toward_goal.py`: 4 tests for commit inspection
|
|
45
|
+
(failed commit, partial commit, timeout commit_best, back-compat
|
|
46
|
+
clean success).
|
|
47
|
+
- `test_preview_studio_truth_gap.py`: 3 tests for
|
|
48
|
+
executable-variant-fails paths (all-steps-fail preserves state,
|
|
49
|
+
Wonder not advanced, partial-success honest commit).
|
|
50
|
+
- `test_runtime_capability_probes.py`: 3 tests for kernel
|
|
51
|
+
propagation (web probe → kernel, flucoma probe → kernel,
|
|
52
|
+
both-unavailable back-compat).
|
|
53
|
+
- **`IterationResult.commit_result`** — the raw commit_fn payload,
|
|
54
|
+
surfaced on the returned dict whenever a commit was attempted.
|
|
55
|
+
Callers can inspect `result["commit_result"]["steps_failed"]`,
|
|
56
|
+
`result["commit_result"]["error"]`, etc.
|
|
57
|
+
|
|
58
|
+
This release closes what the post-v1.17.2 review correctly flagged:
|
|
59
|
+
the feature we shipped to "close the evaluation loop" had a truth-gap
|
|
60
|
+
at the innermost step. 2712 → 2722 tests pass.
|
|
61
|
+
|
|
62
|
+
## 1.17.2 — iterate_toward_goal + preview-studio truth-gap (April 23 2026)
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- **`iterate_toward_goal` MCP tool** (`mcp_server/tools/agent_os.py`,
|
|
67
|
+
`mcp_server/tools/_agent_os_engine/iteration.py`): closes the outer
|
|
68
|
+
evaluation loop. Given a compiled `GoalVector` and a list of candidate
|
|
69
|
+
move sets, runs up to N experiments sequentially. Each iteration
|
|
70
|
+
creates an experiment, runs all branches (with per-branch
|
|
71
|
+
apply-snapshot-undo already handled by the existing experiment engine),
|
|
72
|
+
scores the top branch against the goal, and either commits (score ≥
|
|
73
|
+
threshold) or discards and tries the next candidate set. On timeout,
|
|
74
|
+
commits the best-so-far (`on_timeout="commit_best"`, default) or
|
|
75
|
+
commits nothing (`on_timeout="discard_on_timeout"`). Per-branch undo
|
|
76
|
+
stays inside `run_experiment` — this loop never issues a raw undo.
|
|
77
|
+
Tool count: 426 → 427.
|
|
78
|
+
|
|
79
|
+
Engine ships as both a pure-sync `iterate_toward_goal_engine` (for
|
|
80
|
+
tests with in-memory fakes) and `iterate_toward_goal_engine_async`
|
|
81
|
+
(for the live MCP wrapper with coroutine callbacks); the sync entry
|
|
82
|
+
auto-detects coroutine callbacks and dispatches accordingly. Covered
|
|
83
|
+
by 11 tests in `tests/test_iterate_toward_goal.py` spanning happy
|
|
84
|
+
path, exhaustion + commit-best, exhaustion + discard, no candidates,
|
|
85
|
+
no-winner iterations, max_iterations capping, async coroutine
|
|
86
|
+
callbacks, and MCP registration.
|
|
87
|
+
|
|
88
|
+
This is the P0 item from the v1.17.1 review gap-analysis between
|
|
89
|
+
"tool orchestration" and "agentic optimization" — the create /
|
|
90
|
+
run / compare / commit primitives existed but nothing drove them
|
|
91
|
+
toward a scalar goal. `iterate_toward_goal` is that driver.
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- **Preview Studio truth-gap** (`mcp_server/preview_studio/engine.py`,
|
|
96
|
+
`mcp_server/preview_studio/tools.py`): two compounding bugs made the
|
|
97
|
+
system lie about committed state.
|
|
98
|
+
1. `compare_variants()` scored every variant without filtering for
|
|
99
|
+
`status="blocked"` or missing `compiled_plan`. A blocked /
|
|
100
|
+
analytical-only variant could win the recommendation even with a
|
|
101
|
+
higher taste_fit than the only executable option. Fix: partition
|
|
102
|
+
variants into executable vs analytical, score only the executable
|
|
103
|
+
list, surface the analytical bucket on a new `analytical_candidates`
|
|
104
|
+
field for introspection. `recommended` stays a bare string (or
|
|
105
|
+
`None` when no executable variant exists) so no API shape breaks.
|
|
106
|
+
2. `commit_preview_variant()` called `engine.commit_variant()` — which
|
|
107
|
+
flips `preview_set.status = "committed"` and discards every sibling
|
|
108
|
+
variant — BEFORE checking whether the chosen variant had a compiled
|
|
109
|
+
plan. Analytical-only picks therefore got recorded as committed
|
|
110
|
+
with `committed=False` in the response and the preview set's
|
|
111
|
+
in-memory state said the opposite. Wonder lifecycle also advanced
|
|
112
|
+
to `resolved`. Fix: short-circuit analytical/blocked picks at the
|
|
113
|
+
top of the handler, return `{committed: False, reason:
|
|
114
|
+
"analytical_only" | "blocked", ...}`, leave `preview_set.status`
|
|
115
|
+
untouched, and gate Wonder lifecycle hooks behind the executable
|
|
116
|
+
branch. New regressions in `tests/test_preview_studio_truth_gap.py`
|
|
117
|
+
lock all four scenarios (A1-A4 from the remediation plan).
|
|
118
|
+
- **Runtime capability probes stop lying about `web` and `flucoma`**
|
|
119
|
+
(`mcp_server/runtime/tools.py`, `mcp_server/runtime/capability_state.py`):
|
|
120
|
+
`get_capability_state` previously hardcoded `web_ok=False` and never
|
|
121
|
+
emitted a `flucoma` domain at all, causing `route_request` to pick
|
|
122
|
+
degraded research/perception paths on machines where those
|
|
123
|
+
capabilities were actually available. `_probe_web()` now runs a
|
|
124
|
+
500 ms HEAD request to `https://api.github.com` using stdlib
|
|
125
|
+
`urllib.request` (no new dependency); `_probe_flucoma()` uses
|
|
126
|
+
`importlib.util.find_spec("flucoma")` with safe exception swallowing.
|
|
127
|
+
The `flucoma` domain is now emitted unconditionally so consumers can
|
|
128
|
+
distinguish "probed and missing" from "not probed yet".
|
|
129
|
+
- **`build_song_brain` flags degraded responses**
|
|
130
|
+
(`mcp_server/song_brain/tools.py`): When `get_session_info` fails,
|
|
131
|
+
the tool injected `{tempo: 120.0, track_count: 0}` and returned a
|
|
132
|
+
polished SongBrain with no indication the inputs were synthesized.
|
|
133
|
+
The fallback is preserved for backward compatibility but the
|
|
134
|
+
response now carries a top-level `degradation` payload
|
|
135
|
+
(`{is_degraded, reasons, substituted_fields}`) so callers can branch
|
|
136
|
+
on synthesized vs real data.
|
|
137
|
+
- **`create_preview_set` flags the empty-kernel fallback**
|
|
138
|
+
(`mcp_server/preview_studio/engine.py`,
|
|
139
|
+
`mcp_server/preview_studio/models.py`): When the caller omits a real
|
|
140
|
+
session kernel, `create_preview_set` synthesizes an empty-but-valid
|
|
141
|
+
shape so compilers degrade to no-op steps. `PreviewSet` now carries a
|
|
142
|
+
`degradation` field that is marked
|
|
143
|
+
`is_degraded=True, reasons=["empty_kernel_fallback"]` whenever that
|
|
144
|
+
substitution fires, so downstream consumers can tell a synthesized
|
|
145
|
+
compile from a kernel-backed one.
|
|
146
|
+
|
|
147
|
+
### Added
|
|
148
|
+
|
|
149
|
+
- **`DegradationInfo` dataclass** (`mcp_server/runtime/degradation.py`):
|
|
150
|
+
New shared payload that engines attach to their responses whenever
|
|
151
|
+
they substitute fallback data. Three fields:
|
|
152
|
+
`is_degraded: bool`, `reasons: list[str]`, `substituted_fields: list[str]`.
|
|
153
|
+
Intentionally minimal and import-safe so any engine can adopt it
|
|
154
|
+
without circular-import risk. Wired into `song_brain` and
|
|
155
|
+
`preview_studio`; other engines will adopt it as audits surface more
|
|
156
|
+
silent-fallback paths.
|
|
157
|
+
- **`flucoma` capability domain** now emitted by
|
|
158
|
+
`build_capability_state` alongside `session_access`, `analyzer`,
|
|
159
|
+
`memory`, `web`, and `research`. Matches the existing
|
|
160
|
+
`CapabilityDomain` schema.
|
|
161
|
+
|
|
162
|
+
### Changed
|
|
163
|
+
|
|
164
|
+
- **`capability-modes.md` reference doc rewritten to match the actual
|
|
165
|
+
response shape** (`livepilot/skills/livepilot-evaluation/references/capability-modes.md`).
|
|
166
|
+
The old example JSON described a flat
|
|
167
|
+
`{mode, analyzer_connected, bridge_version, spectral_cache_age_ms, flucoma_available, session_connected}`
|
|
168
|
+
shape that hasn't matched `get_capability_state` for releases. The
|
|
169
|
+
new section documents the nested `capability_state.domains.<name>`
|
|
170
|
+
structure, explicit per-domain and per-field definitions, and
|
|
171
|
+
explicitly scopes the `web` domain as *"server-side outbound HTTP
|
|
172
|
+
capability; does NOT imply curated research corpora are installed"*.
|
|
173
|
+
|
|
174
|
+
### Tests
|
|
175
|
+
|
|
176
|
+
- `tests/test_preview_studio_truth_gap.py` — 5 tests locking the four
|
|
177
|
+
A1-A4 scenarios from the remediation plan.
|
|
178
|
+
- `tests/test_runtime_capability_probes.py` — 6 tests covering the
|
|
179
|
+
web probe (true/false/exception-swallow) and the flucoma probe
|
|
180
|
+
(emitted-when-importable, emitted-when-missing, find_spec-backed).
|
|
181
|
+
- `tests/test_degradation_signalling.py` — 8 tests covering the
|
|
182
|
+
`DegradationInfo` dataclass defaults, `song_brain` degradation on
|
|
183
|
+
session failure, and `preview_studio` degradation on empty-kernel
|
|
184
|
+
fallback.
|
|
185
|
+
|
|
3
186
|
## 1.17.1 — Splice auto-reconnect + Codex installer fix (April 23 2026)
|
|
4
187
|
|
|
5
188
|
Two bug fixes discovered in a parallel worktree hours after v1.17.0
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
19
|
An agentic production system for Ableton Live 12.<br>
|
|
20
|
-
|
|
20
|
+
427 tools. 52 domains. Device atlas. Plan-aware Splice integration. Auto-composition. Spectral perception. Technique memory. Drum-rack pad builder. Live dead-device detection.
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
23
|
<br>
|
|
@@ -80,7 +80,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
|
|
|
80
80
|
│ └─────────────────┼──────────────────┘ │
|
|
81
81
|
│ ▼ │
|
|
82
82
|
│ ┌─────────────────┐ │
|
|
83
|
-
│ │
|
|
83
|
+
│ │ 427 MCP Tools │ │
|
|
84
84
|
│ │ 52 domains │ │
|
|
85
85
|
│ └────────┬────────┘ │
|
|
86
86
|
│ │ │
|
|
@@ -121,7 +121,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
|
|
|
121
121
|
|
|
122
122
|
## The Intelligence Layer
|
|
123
123
|
|
|
124
|
-
12 engines sit on top of the
|
|
124
|
+
12 engines sit on top of the 427 tools. They give the AI musical judgment, not just musical execution.
|
|
125
125
|
|
|
126
126
|
### SongBrain — What the Song Is
|
|
127
127
|
|
|
@@ -173,7 +173,7 @@ Every engine follows: **measure before → act → measure after → compare**.
|
|
|
173
173
|
|
|
174
174
|
## Tools
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
427 tools across 52 domains. Highlights below — [full catalog here](docs/manual/tool-catalog.md).
|
|
177
177
|
|
|
178
178
|
<br>
|
|
179
179
|
|
|
@@ -208,7 +208,8 @@ The M4L Analyzer sits on the master track. UDP 9880 carries spectral data to the
|
|
|
208
208
|
> Most tools work without the analyzer — it adds 32 spectral/analyzer tools (frequency, loudness, perception) and closes the feedback loop.
|
|
209
209
|
|
|
210
210
|
```
|
|
211
|
-
SPECTRAL ───────
|
|
211
|
+
SPECTRAL ─────── 9-band frequency decomposition (sub_low → air)
|
|
212
|
+
sub_low (20-60 Hz) split off so kick fundamentals don't hide inside sub
|
|
212
213
|
true RMS / peak metering
|
|
213
214
|
Krumhansl-Schmuckler key detection
|
|
214
215
|
|
|
@@ -361,7 +362,7 @@ The V2 intelligence layer. These tools analyze, diagnose, plan, evaluate, and le
|
|
|
361
362
|
| Creative Constraints | 5 | constraint activation, reference-inspired variants |
|
|
362
363
|
| Preview Studio | 5 | variant creation, preview rendering, comparison, commit |
|
|
363
364
|
|
|
364
|
-
> **[View all
|
|
365
|
+
> **[View all 427 tools →](docs/manual/tool-catalog.md)**
|
|
365
366
|
|
|
366
367
|
<br>
|
|
367
368
|
|
|
@@ -588,7 +589,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for architecture details, code guidelines
|
|
|
588
589
|
|
|
589
590
|
| Document | What's inside |
|
|
590
591
|
|----------|---------------|
|
|
591
|
-
| [Manual](docs/manual/index.md) | Complete reference: architecture, all
|
|
592
|
+
| [Manual](docs/manual/index.md) | Complete reference: architecture, all 427 tools, workflows |
|
|
592
593
|
| [Intelligence Layer](docs/manual/intelligence.md) | How the 12 engines connect — conductor, moves, preview, evaluation |
|
|
593
594
|
| [Device Atlas](docs/manual/device-atlas.md) | 1305 devices indexed — search, suggest, chain building |
|
|
594
595
|
| [Samples & Slicing](docs/manual/samples.md) | 3-source search, fitness critics, slice workflows |
|
|
@@ -32,27 +32,31 @@ We tap the audio for analysis without affecting the pass-through.
|
|
|
32
32
|
4. Add object: `[*~ 0.5]` (scale to prevent clipping)
|
|
33
33
|
5. Connect: `[+~]` outlet → `[*~ 0.5]` inlet
|
|
34
34
|
|
|
35
|
-
## Step 4:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- Band
|
|
44
|
-
- Band
|
|
45
|
-
- Band
|
|
46
|
-
- Band
|
|
47
|
-
- Band
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
35
|
+
## Step 4: 9-Band Spectrum Analysis
|
|
36
|
+
|
|
37
|
+
(v1.16+ layout. Pre-v1.16 devices used `[fffb~ 8]`; the server still accepts
|
|
38
|
+
8-band payloads for backward compatibility, but new builds should use 9.)
|
|
39
|
+
|
|
40
|
+
1. Add object: `[fffb~ 9]` (fast 9-band filter bank)
|
|
41
|
+
2. Connect: `[*~ 0.5]` outlet → `[fffb~ 9]` inlet
|
|
42
|
+
3. Set `fffb~` center frequencies in Inspector or via message:
|
|
43
|
+
- Band 1: 35 Hz (sub_low) — kick fundamentals, Villalobos subs
|
|
44
|
+
- Band 2: 85 Hz (sub) — 808s, sub-bass body
|
|
45
|
+
- Band 3: 175 Hz (low) — bass body, warmth
|
|
46
|
+
- Band 4: 350 Hz (low_mid) — mud zone
|
|
47
|
+
- Band 5: 700 Hz (mid) — vocal presence, snare body
|
|
48
|
+
- Band 6: 1400 Hz (high_mid) — consonants, pick attack
|
|
49
|
+
- Band 7: 2800 Hz (high) — presence, intelligibility
|
|
50
|
+
- Band 8: 5600 Hz (presence) — cymbal definition
|
|
51
|
+
- Band 9: 12000 Hz (air) — shimmer, sparkle
|
|
52
|
+
|
|
53
|
+
To set: add `[loadmess 35. 85. 175. 350. 700. 1400. 2800. 5600. 12000.]` → `[fffb~ 9]` right inlet
|
|
54
|
+
|
|
55
|
+
4. For each of the 9 outlets of `[fffb~ 9]`:
|
|
52
56
|
- Add `[abs~]` (rectify to positive)
|
|
53
57
|
- Add `[snapshot~ 200]` (sample at 5 Hz)
|
|
54
58
|
|
|
55
|
-
5. Add `[pack f f f f f f f f]` and connect all
|
|
59
|
+
5. Add `[pack f f f f f f f f f]` and connect all 9 `[snapshot~]` outlets to it
|
|
56
60
|
6. Add `[prepend /spectrum]` → connect from `[pack]`
|
|
57
61
|
7. Add `[udpsend 127.0.0.1 9880]` → connect from `[prepend]`
|
|
58
62
|
|
|
@@ -131,7 +135,7 @@ We tap the audio for analysis without affecting the pass-through.
|
|
|
131
135
|
|
|
132
136
|
1. Drop `LivePilot Analyzer` on the **master track**
|
|
133
137
|
2. Play some audio
|
|
134
|
-
3. In Claude Code, run: `get_master_spectrum` — should return
|
|
138
|
+
3. In Claude Code, run: `get_master_spectrum` — should return 9 band values (v1.16+) or 8 values (pre-v1.16 .amxd)
|
|
135
139
|
4. Run: `get_master_rms` — should return RMS and peak
|
|
136
140
|
5. After 8+ bars: `get_detected_key` — should return key and scale
|
|
137
141
|
|
|
@@ -143,7 +147,7 @@ We tap the audio for analysis without affecting the pass-through.
|
|
|
143
147
|
│ │
|
|
144
148
|
plugin~ ──┤──L+R──► plugout~ (pass-through) │
|
|
145
149
|
│ │
|
|
146
|
-
│──L+R──► +~ ──► *~ 0.5 ──┬──► fffb~
|
|
150
|
+
│──L+R──► +~ ──► *~ 0.5 ──┬──► fffb~ 9 ──► UDP │
|
|
147
151
|
│ ├──► peakamp~ ──► UDP │
|
|
148
152
|
│ ├──► average~ ──► UDP │
|
|
149
153
|
│ └──► sigmund~ ──► JS │
|
|
Binary file
|
package/mcp_server/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
|
|
2
|
-
__version__ = "1.17.
|
|
2
|
+
__version__ = "1.17.3"
|
package/mcp_server/m4l_bridge.py
CHANGED
|
@@ -471,7 +471,8 @@ class SpectralReceiver(asyncio.DatagramProtocol):
|
|
|
471
471
|
"""Receives OSC-formatted UDP packets from the M4L device.
|
|
472
472
|
|
|
473
473
|
OSC messages:
|
|
474
|
-
/spectrum f f f f f f f f — 8
|
|
474
|
+
/spectrum f f f f f f f f [f] — 8 or 9 band spectrum
|
|
475
|
+
(9 = v1.16+ with sub_low; 8 = legacy)
|
|
475
476
|
/peak f — peak level
|
|
476
477
|
/rms f — RMS level
|
|
477
478
|
/pitch f f — MIDI note, amplitude
|
|
@@ -11,6 +11,7 @@ import json
|
|
|
11
11
|
import time
|
|
12
12
|
from typing import Optional
|
|
13
13
|
|
|
14
|
+
from ..runtime.degradation import DegradationInfo
|
|
14
15
|
from .models import PreviewSet, PreviewVariant
|
|
15
16
|
|
|
16
17
|
|
|
@@ -52,7 +53,10 @@ def create_preview_set(
|
|
|
52
53
|
kernel: the live session kernel (track topology + device chains). Compilers
|
|
53
54
|
resolve targets from it — without it, variants degrade into no-ops or
|
|
54
55
|
generic reads. Callers that have a `ctx` should fetch a real kernel
|
|
55
|
-
via runtime.tools.get_session_kernel(ctx).
|
|
56
|
+
via runtime.tools.get_session_kernel(ctx). When omitted the engine
|
|
57
|
+
synthesizes an empty-but-valid kernel (see ``_build_triptych``) and
|
|
58
|
+
flags the resulting PreviewSet with ``degradation.is_degraded=True``
|
|
59
|
+
so callers can tell a synthesized compile from a real one.
|
|
56
60
|
"""
|
|
57
61
|
set_id = _compute_set_id(request_text, kernel_id)
|
|
58
62
|
now = int(time.time() * 1000)
|
|
@@ -61,6 +65,18 @@ def create_preview_set(
|
|
|
61
65
|
song_brain = song_brain or {}
|
|
62
66
|
taste_graph = taste_graph or {}
|
|
63
67
|
|
|
68
|
+
# Degradation bookkeeping — if the caller didn't supply a kernel the
|
|
69
|
+
# compiler receives a synthesized one (see engine.py line 128 area)
|
|
70
|
+
# and every variant is scored against that synthetic topology.
|
|
71
|
+
if kernel:
|
|
72
|
+
degradation = DegradationInfo()
|
|
73
|
+
else:
|
|
74
|
+
degradation = DegradationInfo(
|
|
75
|
+
is_degraded=True,
|
|
76
|
+
reasons=["empty_kernel_fallback"],
|
|
77
|
+
substituted_fields=["compile_kernel"],
|
|
78
|
+
)
|
|
79
|
+
|
|
64
80
|
if strategy == "creative_triptych":
|
|
65
81
|
variants = _build_triptych(
|
|
66
82
|
request_text, moves, song_brain, taste_graph, set_id, now, kernel,
|
|
@@ -79,6 +95,7 @@ def create_preview_set(
|
|
|
79
95
|
source_kernel_id=kernel_id,
|
|
80
96
|
variants=variants,
|
|
81
97
|
created_at_ms=now,
|
|
98
|
+
degradation=degradation,
|
|
82
99
|
)
|
|
83
100
|
store_preview_set(ps)
|
|
84
101
|
return ps
|
|
@@ -258,31 +275,66 @@ def _build_binary(
|
|
|
258
275
|
# ── Comparison ────────────────────────────────────────────────────
|
|
259
276
|
|
|
260
277
|
|
|
278
|
+
_NON_EXECUTABLE_STATUSES = {"blocked", "failed"}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _is_executable(variant: PreviewVariant) -> bool:
|
|
282
|
+
"""A variant is executable when it has a compiled plan AND its status
|
|
283
|
+
hasn't been flagged as blocked/failed upstream.
|
|
284
|
+
|
|
285
|
+
The compiled plan may be a non-empty list of steps OR a dict with a
|
|
286
|
+
non-empty ``steps`` key — both shapes exist in the wild.
|
|
287
|
+
"""
|
|
288
|
+
if variant.status in _NON_EXECUTABLE_STATUSES:
|
|
289
|
+
return False
|
|
290
|
+
plan = variant.compiled_plan
|
|
291
|
+
if plan is None:
|
|
292
|
+
return False
|
|
293
|
+
if isinstance(plan, list):
|
|
294
|
+
return len(plan) > 0
|
|
295
|
+
if isinstance(plan, dict):
|
|
296
|
+
return len(plan.get("steps") or []) > 0
|
|
297
|
+
# Any other truthy shape is treated as executable; falsy as not.
|
|
298
|
+
return bool(plan)
|
|
299
|
+
|
|
300
|
+
|
|
261
301
|
def compare_variants(
|
|
262
302
|
preview_set: PreviewSet,
|
|
263
303
|
criteria: Optional[dict] = None,
|
|
264
304
|
) -> dict:
|
|
265
|
-
"""Compare variants within a preview set and rank them.
|
|
305
|
+
"""Compare variants within a preview set and rank them.
|
|
306
|
+
|
|
307
|
+
Truth-gap fix (PR-A): variants that are blocked/failed OR lack a
|
|
308
|
+
compiled_plan are partitioned out of the scored ranking. They appear
|
|
309
|
+
in ``analytical_candidates`` (just their variant_ids) and ALSO stay
|
|
310
|
+
in ``rankings`` at the bottom for introspection, but they can never
|
|
311
|
+
populate ``recommended``. When no executable variant exists,
|
|
312
|
+
``recommended`` is ``None`` so callers can surface a clear message
|
|
313
|
+
instead of silently committing a no-op.
|
|
314
|
+
"""
|
|
266
315
|
criteria = criteria or {}
|
|
267
316
|
weight_taste = criteria.get("taste_weight", 0.3)
|
|
268
317
|
weight_novelty = criteria.get("novelty_weight", 0.2)
|
|
269
318
|
weight_identity = criteria.get("identity_weight", 0.5)
|
|
270
319
|
|
|
271
|
-
|
|
320
|
+
executable: list[PreviewVariant] = []
|
|
321
|
+
analytical: list[PreviewVariant] = []
|
|
272
322
|
for v in preview_set.variants:
|
|
273
|
-
|
|
323
|
+
(executable if _is_executable(v) else analytical).append(v)
|
|
324
|
+
|
|
325
|
+
def _score(v: PreviewVariant) -> float:
|
|
274
326
|
taste_score = v.taste_fit
|
|
275
327
|
novelty_score = 1.0 - abs(v.novelty_level - 0.5) * 2 # bell curve around 0.5
|
|
276
328
|
identity_score = _identity_effect_score(v.identity_effect)
|
|
277
|
-
|
|
278
329
|
composite = (
|
|
279
330
|
taste_score * weight_taste
|
|
280
331
|
+ novelty_score * weight_novelty
|
|
281
332
|
+ identity_score * weight_identity
|
|
282
333
|
)
|
|
283
|
-
|
|
334
|
+
return round(composite, 3)
|
|
284
335
|
|
|
285
|
-
|
|
336
|
+
def _row(v: PreviewVariant) -> dict:
|
|
337
|
+
return {
|
|
286
338
|
"variant_id": v.variant_id,
|
|
287
339
|
"label": v.label,
|
|
288
340
|
"score": v.score,
|
|
@@ -292,13 +344,35 @@ def compare_variants(
|
|
|
292
344
|
"summary": v.intent,
|
|
293
345
|
"what_preserved": v.what_preserved,
|
|
294
346
|
"why_it_matters": v.why_it_matters,
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
347
|
+
"status": v.status,
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
executable_rows: list[dict] = []
|
|
351
|
+
for v in executable:
|
|
352
|
+
v.score = _score(v)
|
|
353
|
+
executable_rows.append(_row(v))
|
|
354
|
+
executable_rows.sort(key=lambda r: r["score"], reverse=True)
|
|
355
|
+
|
|
356
|
+
# Analytical variants still get a score computed so introspection
|
|
357
|
+
# shows the same shape, but they're appended AFTER the sorted
|
|
358
|
+
# executables so they can never land at position 0.
|
|
359
|
+
analytical_rows: list[dict] = []
|
|
360
|
+
for v in analytical:
|
|
361
|
+
v.score = _score(v)
|
|
362
|
+
analytical_rows.append(_row(v))
|
|
363
|
+
|
|
364
|
+
rankings = executable_rows + analytical_rows
|
|
365
|
+
|
|
366
|
+
recommended: Optional[str]
|
|
367
|
+
if executable_rows:
|
|
368
|
+
recommended = executable_rows[0]["variant_id"]
|
|
369
|
+
else:
|
|
370
|
+
recommended = None
|
|
298
371
|
|
|
299
372
|
comparison = {
|
|
300
373
|
"rankings": rankings,
|
|
301
|
-
"recommended":
|
|
374
|
+
"recommended": recommended,
|
|
375
|
+
"analytical_candidates": [v.variant_id for v in analytical],
|
|
302
376
|
"criteria_used": {
|
|
303
377
|
"taste_weight": weight_taste,
|
|
304
378
|
"novelty_weight": weight_novelty,
|
|
@@ -6,6 +6,8 @@ import time
|
|
|
6
6
|
from dataclasses import asdict, dataclass, field
|
|
7
7
|
from typing import Optional
|
|
8
8
|
|
|
9
|
+
from ..runtime.degradation import DegradationInfo
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
@dataclass
|
|
11
13
|
class PreviewVariant:
|
|
@@ -59,6 +61,11 @@ class PreviewSet:
|
|
|
59
61
|
committed_variant_id: str = ""
|
|
60
62
|
status: str = "pending" # pending, compared, committed, discarded
|
|
61
63
|
created_at_ms: int = field(default_factory=lambda: int(time.time() * 1000))
|
|
64
|
+
# Degradation signalling — set when the engine substituted a fallback
|
|
65
|
+
# (e.g. an empty-but-valid kernel) during variant compilation. Callers
|
|
66
|
+
# can inspect .degradation.is_degraded to tell synthesized preview
|
|
67
|
+
# topology apart from a real kernel-backed compile.
|
|
68
|
+
degradation: DegradationInfo = field(default_factory=DegradationInfo)
|
|
62
69
|
|
|
63
70
|
def to_dict(self) -> dict:
|
|
64
71
|
return {
|
|
@@ -71,4 +78,5 @@ class PreviewSet:
|
|
|
71
78
|
"committed_variant_id": self.committed_variant_id,
|
|
72
79
|
"status": self.status,
|
|
73
80
|
"variant_count": len(self.variants),
|
|
81
|
+
"degradation": self.degradation.to_dict(),
|
|
74
82
|
}
|