livepilot 1.10.8 → 1.10.9
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 +128 -0
- package/README.md +15 -15
- 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/evaluation/fabric.py +62 -1
- package/mcp_server/m4l_bridge.py +15 -5
- package/mcp_server/project_brain/automation_graph.py +23 -1
- package/mcp_server/project_brain/builder.py +2 -0
- package/mcp_server/project_brain/models.py +20 -1
- package/mcp_server/project_brain/tools.py +10 -3
- package/mcp_server/semantic_moves/tools.py +139 -31
- package/mcp_server/server.py +140 -14
- package/mcp_server/session_continuity/models.py +13 -0
- package/mcp_server/session_continuity/tools.py +2 -0
- package/mcp_server/session_continuity/tracker.py +93 -0
- package/mcp_server/tools/_analyzer_engine/__init__.py +39 -0
- package/mcp_server/tools/_analyzer_engine/context.py +103 -0
- package/mcp_server/tools/_analyzer_engine/flucoma.py +23 -0
- package/mcp_server/tools/_analyzer_engine/sample.py +122 -0
- package/mcp_server/tools/_motif_engine.py +19 -4
- package/mcp_server/tools/analyzer.py +22 -178
- package/mcp_server/tools/clips.py +239 -1
- package/mcp_server/tools/transport.py +58 -3
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +8 -1
- package/server.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,133 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.10.9 — Second-pass audit + deferred-bugs shipped (April 18 2026)
|
|
4
|
+
|
|
5
|
+
Completes every non-feature item on the v1.10.8 audit backlog. 2116 → 2132
|
|
6
|
+
passing tests (+16 regression guards). 324 → 325 tools (`check_clip_key_consistency`
|
|
7
|
+
lands from BUG-D1). Every deferred BUG-C and BUG-D entry either ships or is
|
|
8
|
+
scoped to a follow-up feature; BUG-C4 is filed upstream as
|
|
9
|
+
[PrefectHQ/fastmcp#3967](https://github.com/PrefectHQ/fastmcp/issues/3967).
|
|
10
|
+
|
|
11
|
+
### Ship-stoppers fixed
|
|
12
|
+
|
|
13
|
+
- **`send_capture` phantom 35s hang when receiver is None** —
|
|
14
|
+
[`mcp_server/m4l_bridge.py:441`](mcp_server/m4l_bridge.py). `send_command`
|
|
15
|
+
had a receiver-None guard; `send_capture` didn't. When UDP 9880 failed
|
|
16
|
+
to bind but the cache still reported connected, the OSC packet was
|
|
17
|
+
sent, the capture future was never registered, and the full 35s
|
|
18
|
+
timeout fired with a misleading "device may be busy" error. Added the
|
|
19
|
+
matching 5-line guard so the real cause surfaces immediately.
|
|
20
|
+
- **`utils.py` stale after Control Surface toggle** —
|
|
21
|
+
[`remote_script/LivePilot/__init__.py:43`](remote_script/LivePilot/__init__.py).
|
|
22
|
+
Every handler does `from .utils import get_track, get_device`.
|
|
23
|
+
`importlib.reload(devices)` rebinds those names, but because `utils`
|
|
24
|
+
was not in `_HANDLER_MODULES`, the re-import resolved against the
|
|
25
|
+
stale `sys.modules["LivePilot.utils"]`. Added `utils` first in the
|
|
26
|
+
reload order so edits to the shared helpers pick up on toggle too,
|
|
27
|
+
honoring the dev-workflow guarantee documented at the top of the file.
|
|
28
|
+
|
|
29
|
+
### New tools
|
|
30
|
+
|
|
31
|
+
- **`check_clip_key_consistency`** (BUG-D1) — parses Splice-style
|
|
32
|
+
filename key tokens (`_D#min`, `_Ebmaj`, `_Cm`, …), cross-checks
|
|
33
|
+
against `get_detected_key`, returns the exact `set_clip_pitch(coarse=±N)`
|
|
34
|
+
call that would realign on mismatch. Handles `#`/`b` accidentals,
|
|
35
|
+
`min`/`m`/`maj` suffixes, absent tokens gracefully.
|
|
36
|
+
- **`get_session_diagnostics(check_clip_keys=True)`** — opt-in scan
|
|
37
|
+
across every audio clip, appending `clip_key_mismatch` warnings with
|
|
38
|
+
the one-call fix attached.
|
|
39
|
+
|
|
40
|
+
### Features shipped
|
|
41
|
+
|
|
42
|
+
- **Session continuity persistence wired at startup** (BUG from external
|
|
43
|
+
audit). `bind_project_store_from_session()` now computes a project
|
|
44
|
+
fingerprint, opens the matching `ProjectStore`, AND rehydrates
|
|
45
|
+
`_threads` + `_turns` from disk. Wired into `server.py` lifespan with
|
|
46
|
+
a lazy rebind on first `record_turn_resolution` / `open_creative_thread`.
|
|
47
|
+
Creative threads and turn history now survive server restarts — the
|
|
48
|
+
README's "return to a project" claim is now load-bearing.
|
|
49
|
+
- **Taste-aware `propose_next_best_move`** — replaces keyword-only
|
|
50
|
+
matching with `0.55 × keyword + 0.30 × taste_alignment + 0.15 ×
|
|
51
|
+
(1 − avoidance) ± 0.10 × family_bonus`. Cold-start users identical to
|
|
52
|
+
before; users with history get personalized ranking via
|
|
53
|
+
`dimension_weights` and `dimension_avoidances`. New return fields:
|
|
54
|
+
`score_breakdown`, `taste_active`, `taste_evidence_count`.
|
|
55
|
+
- **Evaluation `taste_fit`** — previously hardcoded to `0.0`. Now
|
|
56
|
+
computed from `outcome_history` by matching same-direction deltas on
|
|
57
|
+
the same dimensions: ±0.2 per kept/undone match, neutral 0.5 baseline.
|
|
58
|
+
- **`AutomationGraph.coverage_pct`** (BUG-D2, detection half) — new
|
|
59
|
+
fields `coverage_pct`, `clip_envelope_count`, `clips_scanned`
|
|
60
|
+
distinguish "no automation exists" from "we couldn't probe" from
|
|
61
|
+
"sparse but present". Surfaced in `get_project_brain_summary` as
|
|
62
|
+
`automation_coverage_pct`.
|
|
63
|
+
|
|
64
|
+
### Refactor (BUG-C1)
|
|
65
|
+
|
|
66
|
+
- **`mcp_server/tools/analyzer.py`** 1069 → 913 LOC. All 32
|
|
67
|
+
`@mcp.tool()` decorators stay in the module (FastMCP registration
|
|
68
|
+
order unchanged), helpers moved to `_analyzer_engine/`:
|
|
69
|
+
`context.py` (SpectralCache/bridge accessors, analyzer health check),
|
|
70
|
+
`sample.py` (Simpler post-load hygiene, filename heuristics),
|
|
71
|
+
`flucoma.py` (FluCoMa hint text, pitch-name table). Same
|
|
72
|
+
package-facade pattern as `_composition_engine` and `_agent_os_engine`.
|
|
73
|
+
- **`sync_metadata::get_domains()` now skips `_*`** directories + files
|
|
74
|
+
under `mcp_server/tools/`. Matches Python private-package convention,
|
|
75
|
+
prevents internal helpers from registering as false domains.
|
|
76
|
+
|
|
77
|
+
### Resilience (BUG-C3)
|
|
78
|
+
|
|
79
|
+
- **`_get_all_tools()` probe chain extended** in `mcp_server/server.py`
|
|
80
|
+
to 4 paths: existing `_tool_manager._tools` and
|
|
81
|
+
`_local_provider._components`, plus speculative 3.3+ rename
|
|
82
|
+
`_local_provider._tools` and the future public `mcp.list_tools()`.
|
|
83
|
+
Each wrapped in try/except. All-empty fall-through now prints
|
|
84
|
+
`fastmcp.__version__` + the attempted probe labels — prior silent `[]`
|
|
85
|
+
return would disable schema coercion with no signal.
|
|
86
|
+
- **New `_assert_tool_registry_accessible()`** self-test runs at module
|
|
87
|
+
import. Empty registry or a count mismatch against
|
|
88
|
+
`tests/test_tools_contract.py` fails loudly via stderr.
|
|
89
|
+
|
|
90
|
+
### Upstream (BUG-C4)
|
|
91
|
+
|
|
92
|
+
- **FastMCP feature request filed** —
|
|
93
|
+
[PrefectHQ/fastmcp#3967](https://github.com/PrefectHQ/fastmcp/issues/3967)
|
|
94
|
+
"Feature request: public tool-enumeration API". Migration is a
|
|
95
|
+
no-op once upstream lands a `mcp.list_tools()` or `mcp.tools`
|
|
96
|
+
surface — the probe chain already anticipates both.
|
|
97
|
+
|
|
98
|
+
### Metadata + docs
|
|
99
|
+
|
|
100
|
+
- **`sync_metadata.py` extended** — catches both `N tools` and hyphenated
|
|
101
|
+
`N-tool`, now covers `manifest.json` + `docs/manual/intelligence.md` +
|
|
102
|
+
`.claude-plugin/marketplace.json`. New prose-claim checks for
|
|
103
|
+
bridge-command count, enriched-device YAML count, and `GENRE_DEFAULTS`
|
|
104
|
+
key count — every narrative number now traces to a code derivation.
|
|
105
|
+
- **README / CLAUDE.md / docs drift closed** — 28→30 bridge commands,
|
|
106
|
+
81→71 enriched devices, 7→4 genre defaults, 323→325 tool count in
|
|
107
|
+
stale marketplace/plugin/manifest descriptors. Intelligence manual
|
|
108
|
+
example signatures for `record_positive_preference`,
|
|
109
|
+
`record_anti_preference`, `evaluate_move` updated to match the
|
|
110
|
+
shipping APIs.
|
|
111
|
+
- **Skill cleanups** — `livepilot-release` tool-count drift fixed;
|
|
112
|
+
`livepilot-wonder` reference paths corrected to point at their real
|
|
113
|
+
home in `livepilot-core`; arrangement vs composition-engine triggers
|
|
114
|
+
deduplicated (constructive vs analytical split).
|
|
115
|
+
- **New test** `tests/test_claim_consistency.py` — 12 guards running
|
|
116
|
+
`sync_metadata --check` from pytest, verifying `manifest.json` and
|
|
117
|
+
`intelligence.md` stay in the sweep, and asserting that
|
|
118
|
+
`bind_project_store_from_session` keeps a non-test caller.
|
|
119
|
+
|
|
120
|
+
### Quality-of-life
|
|
121
|
+
|
|
122
|
+
- **`scripts/test.sh`** — blessed test entrypoint always uses `.venv/bin/python`,
|
|
123
|
+
closes the contributor trap where bare `pytest` failed 28 tests on
|
|
124
|
+
system Python.
|
|
125
|
+
- **`.gitignore`** additions: `m4l_device/*.pre-presentation-backup`,
|
|
126
|
+
`m4l_device/*.pre-*-backup`, `.mcp.json.disabled`.
|
|
127
|
+
- **Stale `livepilot-1.10.5.tgz`** removed from the repo root.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
3
131
|
## 1.10.8 — Deep audit fix pass (April 18 2026)
|
|
4
132
|
|
|
5
133
|
Outcome of a cross-subsystem audit (Remote Script, MCP server, M4L bridge,
|
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
|
+
325 tools. 45 domains. Device atlas. Splice integration. Auto-composition. Spectral perception. Technique memory.
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
23
|
<br>
|
|
@@ -38,7 +38,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
|
|
|
38
38
|
| Layer | What it provides |
|
|
39
39
|
|-------|-----------------|
|
|
40
40
|
| **Deterministic Tools** | Direct control: transport, tracks, clips, notes, devices, scenes, mixing, arrangement, browser, automation |
|
|
41
|
-
| **Device Atlas** | Knowledge of every device in Ableton's library — 1305 devices indexed by name, URI, category, tag, and genre.
|
|
41
|
+
| **Device Atlas** | Knowledge of every device in Ableton's library — 1305 devices indexed by name, URI, category, tag, and genre. 71 enriched with sonic intelligence. 683 drum kits mapped |
|
|
42
42
|
| **Sample Engine** | Three-source sample intelligence — searches Ableton's browser, your filesystem, and Splice's catalog simultaneously. 6 fitness critics score every result. 29 processing techniques |
|
|
43
43
|
| **Spectral Perception** | Real-time ears via M4L — 8-band FFT, RMS/peak metering, Krumhansl-Schmuckler key detection, pitch tracking. Closes the feedback loop so the AI hears its own changes |
|
|
44
44
|
| **Technique Memory** | Persistent library of production decisions. Save a beat pattern, device chain, or mix template. Recall by mood, genre, or texture across sessions |
|
|
@@ -58,7 +58,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
|
|
|
58
58
|
│ │
|
|
59
59
|
│ Device Atlas 8-band FFT recall by mood, │
|
|
60
60
|
│ 1305 devices RMS / peak genre, texture │
|
|
61
|
-
│
|
|
61
|
+
│ 71 enriched pitch tracking 29 techniques │
|
|
62
62
|
│ 683 drum kits key detection replay into session │
|
|
63
63
|
│ │
|
|
64
64
|
│ Sample Engine Corpus Intelligence Taste Graph │
|
|
@@ -79,7 +79,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
|
|
|
79
79
|
│ └─────────────────┼──────────────────┘ │
|
|
80
80
|
│ ▼ │
|
|
81
81
|
│ ┌─────────────────┐ │
|
|
82
|
-
│ │
|
|
82
|
+
│ │ 325 MCP Tools │ │
|
|
83
83
|
│ │ 45 domains │ │
|
|
84
84
|
│ └────────┬────────┘ │
|
|
85
85
|
│ │ │
|
|
@@ -100,15 +100,15 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
|
|
|
100
100
|
|
|
101
101
|
**MCP Server** (`mcp_server/`) — Python FastMCP server. Validates inputs, routes commands to the Remote Script over TCP, manages the M4L bridge, runs the atlas, sample engine, composer, and all intelligence engines. This is what your AI client connects to.
|
|
102
102
|
|
|
103
|
-
**M4L Bridge** (`m4l_device/`) — Optional Max for Live Audio Effect on the master track. Provides deep LOM access through Max's LiveAPI that the ControlSurface API can't reach. UDP 9880 (M4L to server) carries spectral data and LiveAPI responses. OSC 9881 (server to M4L) sends commands.
|
|
103
|
+
**M4L Bridge** (`m4l_device/`) — Optional Max for Live Audio Effect on the master track. Provides deep LOM access through Max's LiveAPI that the ControlSurface API can't reach. UDP 9880 (M4L to server) carries spectral data and LiveAPI responses. OSC 9881 (server to M4L) sends commands. The 32 spectral/analyzer tools strictly require the bridge; device and sample tools that call the bridge also have graceful fallbacks, so core functionality works without it. Backed by 30 bridge commands for hidden parameters, Simpler internals, warp markers, display values, and Simpler warp / Compressor sidechain writes that live on child objects Python can't reach.
|
|
104
104
|
|
|
105
|
-
**Device Atlas** (`mcp_server/atlas/`) — In-memory indexed JSON database. 1305 devices with browser URIs,
|
|
105
|
+
**Device Atlas** (`mcp_server/atlas/`) — In-memory indexed JSON database. 1305 devices with browser URIs, 71 enriched with YAML sonic intelligence profiles (mood, genre, texture, recommended chains). 6 indexes: by_id, by_name, by_uri, by_category, by_tag, by_genre. The AI never hallucinates a device name or preset — it always resolves against the atlas first.
|
|
106
106
|
|
|
107
107
|
**Sample Engine** (`mcp_server/sample_engine/`) — Searches three sources simultaneously: BrowserSource (Ableton's library), SpliceSource (local Splice catalog via SQLite), FilesystemSource (user directories). Every result passes through a 6-critic fitness battery (key, tempo, spectral, genre, mood, technical). 29 processing techniques (Surgeon precision vs. Alchemist experimentation). Builds complete sample processing plans with warp, slice, and effect recommendations.
|
|
108
108
|
|
|
109
109
|
**Splice Client** (`mcp_server/splice_client/`) — Searches Splice's catalog through two layers: the local SQLite database (`sounds.db`, already-downloaded samples) and the live gRPC API (full catalog, including samples you haven't downloaded yet). The gRPC client auto-detects Splice's dynamic port via `port.conf`, handles self-signed TLS, and enforces a 5-credit safety floor before any download. Per-call timeouts (5–10s) prevent a hung Splice process from stalling the MCP event loop. Graceful fallback to SQL-only if grpcio isn't installed. No API key needed — authentication comes from the running Splice desktop app.
|
|
110
110
|
|
|
111
|
-
**Composer** (`mcp_server/composer/`) — Prompt-to-plan pipeline. Parses natural language ("dark minimal techno 128bpm with industrial textures") into a CompositionIntent (genre, mood, tempo, key). Plans layers using role templates (kick, bass, percussion, texture, lead, pad, fx). Compiles to a step-by-step plan of tool calls that the agent executes. Does not execute autonomously — returns the plan.
|
|
111
|
+
**Composer** (`mcp_server/composer/`) — Prompt-to-plan pipeline. Parses natural language ("dark minimal techno 128bpm with industrial textures") into a CompositionIntent (genre, mood, tempo, key). Plans layers using role templates (kick, bass, percussion, texture, lead, pad, fx). Compiles to a step-by-step plan of tool calls that the agent executes. Does not execute autonomously — returns the plan. 4 genre defaults (house, techno, trap, ambient) — genres outside this set fall back to a neutral layer plan.
|
|
112
112
|
|
|
113
113
|
**Corpus** (`mcp_server/corpus/`) — Parsed device-knowledge markdown converted to queryable Python structures: EmotionalRecipe, GenreChain, PhysicalModelRecipe, AutomationGesture. Feeds Wonder Mode, Sound Design critics, and the Composer with deep creative knowledge at runtime — not just LLM prompts, actual structured data.
|
|
114
114
|
|
|
@@ -120,7 +120,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
|
|
|
120
120
|
|
|
121
121
|
## The Intelligence Layer
|
|
122
122
|
|
|
123
|
-
12 engines sit on top of the
|
|
123
|
+
12 engines sit on top of the 325 tools. They give the AI musical judgment, not just musical execution.
|
|
124
124
|
|
|
125
125
|
### SongBrain — What the Song Is
|
|
126
126
|
|
|
@@ -156,7 +156,7 @@ When a session is stuck — repeated undos, overpolished loops, no structural pr
|
|
|
156
156
|
|
|
157
157
|
### Hook Hunter
|
|
158
158
|
|
|
159
|
-
Identifies the most salient musical idea — ranks by
|
|
159
|
+
Identifies the most salient musical idea — ranks candidates by recurrence across scenes, motif salience, and section placement (payoff-section boost). Tracks whether hooks are developed, neglected, or undermined, and flags when a transition fails to deliver expected payoff. Rhythm-side ranking is currently heuristic (drum-track detection + clip reuse); true onset-based rhythmic features are on the roadmap.
|
|
160
160
|
|
|
161
161
|
### Session Continuity
|
|
162
162
|
|
|
@@ -172,7 +172,7 @@ Every engine follows: **measure before → act → measure after → compare**.
|
|
|
172
172
|
|
|
173
173
|
## Tools
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
325 tools across 45 domains. Highlights below — [full catalog here](docs/manual/tool-catalog.md).
|
|
176
176
|
|
|
177
177
|
<br>
|
|
178
178
|
|
|
@@ -204,7 +204,7 @@ Every engine follows: **measure before → act → measure after → compare**.
|
|
|
204
204
|
The M4L Analyzer sits on the master track. UDP 9880 carries spectral data to the server. OSC 9881 sends commands back.
|
|
205
205
|
|
|
206
206
|
> [!TIP]
|
|
207
|
-
>
|
|
207
|
+
> Most tools work without the analyzer — it adds 32 spectral/analyzer tools (frequency, loudness, perception) and closes the feedback loop.
|
|
208
208
|
|
|
209
209
|
```
|
|
210
210
|
SPECTRAL ─────── 8-band frequency decomposition (sub → air)
|
|
@@ -232,7 +232,7 @@ The atlas is an in-memory indexed database of Ableton's entire device library.
|
|
|
232
232
|
|
|
233
233
|
```
|
|
234
234
|
1305 devices total
|
|
235
|
-
|
|
235
|
+
71 enriched with sonic intelligence (mood, genre, texture, chains)
|
|
236
236
|
683 drum kits mapped with note assignments
|
|
237
237
|
6 indexes: by_id, by_name, by_uri, by_category, by_tag, by_genre
|
|
238
238
|
```
|
|
@@ -317,7 +317,7 @@ Prompt-to-plan auto-composition engine.
|
|
|
317
317
|
└─────────────────┘
|
|
318
318
|
```
|
|
319
319
|
|
|
320
|
-
-
|
|
320
|
+
- 4 genre defaults: house, techno, trap, ambient (unknown genres fall back to a neutral plan)
|
|
321
321
|
- Returns step-by-step plans — the agent executes each tool call in sequence
|
|
322
322
|
- `compose` — plan a multi-layer composition from text prompt
|
|
323
323
|
- `augment_with_samples` — plan sample-based layers for existing session
|
|
@@ -360,7 +360,7 @@ The V2 intelligence layer. These tools analyze, diagnose, plan, evaluate, and le
|
|
|
360
360
|
| Creative Constraints | 5 | constraint activation, reference-inspired variants |
|
|
361
361
|
| Preview Studio | 5 | variant creation, preview rendering, comparison, commit |
|
|
362
362
|
|
|
363
|
-
> **[View all
|
|
363
|
+
> **[View all 325 tools →](docs/manual/tool-catalog.md)**
|
|
364
364
|
|
|
365
365
|
<br>
|
|
366
366
|
|
|
@@ -587,7 +587,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for architecture details, code guidelines
|
|
|
587
587
|
|
|
588
588
|
| Document | What's inside |
|
|
589
589
|
|----------|---------------|
|
|
590
|
-
| [Manual](docs/manual/index.md) | Complete reference: architecture, all
|
|
590
|
+
| [Manual](docs/manual/index.md) | Complete reference: architecture, all 325 tools, workflows |
|
|
591
591
|
| [Intelligence Layer](docs/manual/intelligence.md) | How the 12 engines connect — conductor, moves, preview, evaluation |
|
|
592
592
|
| [Device Atlas](docs/manual/device-atlas.md) | 1305 devices indexed — search, suggest, chain building |
|
|
593
593
|
| [Samples & Slicing](docs/manual/samples.md) | 3-source search, fitness critics, slice workflows |
|
|
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.10.
|
|
2
|
+
__version__ = "1.10.9"
|
|
@@ -31,6 +31,66 @@ from .policy import apply_hard_rules
|
|
|
31
31
|
# ── Sonic Evaluator ──────────────────────────────────────────────────
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def _compute_taste_fit(
|
|
35
|
+
dimension_changes: dict[str, dict],
|
|
36
|
+
outcome_history: Optional[list[dict]],
|
|
37
|
+
) -> float:
|
|
38
|
+
"""Score how well this move aligns with the user's recent taste.
|
|
39
|
+
|
|
40
|
+
Shipped in v1.10.9 — previously hardcoded to 0.0.
|
|
41
|
+
|
|
42
|
+
For each dimension that moved (in ``dimension_changes``), look at the
|
|
43
|
+
user's last few kept/undone outcomes for the same dimension:
|
|
44
|
+
* kept with the same direction of delta → +0.2 per match
|
|
45
|
+
* undone with the same direction of delta → −0.2 per match
|
|
46
|
+
|
|
47
|
+
Returns a value in 0..1 (0.5 = neutral, neither signal). Empty history
|
|
48
|
+
returns 0.5, which is the correct "no information yet" neutral the
|
|
49
|
+
composite score already expects.
|
|
50
|
+
|
|
51
|
+
``outcome_history`` entries are dicts of the shape::
|
|
52
|
+
|
|
53
|
+
{"dimension": "punch", "delta": 0.12, "kept": True}
|
|
54
|
+
|
|
55
|
+
Callers that pass a richer shape should extract those three fields.
|
|
56
|
+
Malformed entries are skipped silently so a schema bump upstream can't
|
|
57
|
+
break the evaluator.
|
|
58
|
+
"""
|
|
59
|
+
if not outcome_history or not dimension_changes:
|
|
60
|
+
return 0.5
|
|
61
|
+
|
|
62
|
+
# Only weigh the most recent slice — taste drifts, and older signals
|
|
63
|
+
# shouldn't veto a current evaluation.
|
|
64
|
+
recent = outcome_history[-10:]
|
|
65
|
+
|
|
66
|
+
adjustment = 0.0
|
|
67
|
+
matched = 0
|
|
68
|
+
for dim, change in dimension_changes.items():
|
|
69
|
+
current_delta = change.get("delta", 0.0)
|
|
70
|
+
current_sign = 1 if current_delta > 0 else (-1 if current_delta < 0 else 0)
|
|
71
|
+
if current_sign == 0:
|
|
72
|
+
continue
|
|
73
|
+
for entry in recent:
|
|
74
|
+
if not isinstance(entry, dict):
|
|
75
|
+
continue
|
|
76
|
+
if entry.get("dimension") != dim:
|
|
77
|
+
continue
|
|
78
|
+
past_delta = entry.get("delta")
|
|
79
|
+
if not isinstance(past_delta, (int, float)):
|
|
80
|
+
continue
|
|
81
|
+
past_sign = 1 if past_delta > 0 else (-1 if past_delta < 0 else 0)
|
|
82
|
+
if past_sign != current_sign:
|
|
83
|
+
continue
|
|
84
|
+
kept = bool(entry.get("kept", False))
|
|
85
|
+
adjustment += 0.2 if kept else -0.2
|
|
86
|
+
matched += 1
|
|
87
|
+
|
|
88
|
+
if matched == 0:
|
|
89
|
+
return 0.5
|
|
90
|
+
# Neutral baseline 0.5 + averaged adjustment, clamped to [0, 1].
|
|
91
|
+
return _clamp(0.5 + adjustment / matched)
|
|
92
|
+
|
|
93
|
+
|
|
34
94
|
def evaluate_sonic_move(
|
|
35
95
|
request: EvaluationRequest,
|
|
36
96
|
outcome_history: Optional[list[dict]] = None,
|
|
@@ -109,12 +169,13 @@ def evaluate_sonic_move(
|
|
|
109
169
|
measurable_component = _clamp(0.5 + measurable_delta)
|
|
110
170
|
preservation = _clamp(1.0 - collateral_damage * 5)
|
|
111
171
|
confidence = measurable_count / max(len(targets), 1)
|
|
172
|
+
taste_fit = _compute_taste_fit(dimension_changes, outcome_history)
|
|
112
173
|
|
|
113
174
|
score = (
|
|
114
175
|
0.30 * goal_fit
|
|
115
176
|
+ 0.25 * measurable_component
|
|
116
177
|
+ 0.15 * preservation
|
|
117
|
-
+ 0.10 *
|
|
178
|
+
+ 0.10 * taste_fit
|
|
118
179
|
+ 0.10 * confidence
|
|
119
180
|
+ 0.10 * 1.0 # reversibility: 1.0 for undo-able moves
|
|
120
181
|
)
|
package/mcp_server/m4l_bridge.py
CHANGED
|
@@ -443,17 +443,28 @@ class M4LBridge:
|
|
|
443
443
|
if not self.cache.is_connected:
|
|
444
444
|
return {"error": "LivePilot Analyzer not connected. Drop it on the master track."}
|
|
445
445
|
|
|
446
|
+
# Fail fast if there is no receiver to correlate the reply. Prior
|
|
447
|
+
# versions sent the OSC packet anyway, never registered a future,
|
|
448
|
+
# and then waited out the full 35s timeout with a misleading
|
|
449
|
+
# "device may be busy or removed" diagnosis — the real cause was
|
|
450
|
+
# "no receiver wired" (UDP 9880 failed to bind at startup).
|
|
451
|
+
if self.receiver is None:
|
|
452
|
+
return {
|
|
453
|
+
"error": "M4L bridge has no active receiver — the UDP 9880 "
|
|
454
|
+
"listener did not start. Check server startup logs "
|
|
455
|
+
"for a bind failure on port 9880."
|
|
456
|
+
}
|
|
457
|
+
|
|
446
458
|
if self._cmd_lock is None:
|
|
447
459
|
self._cmd_lock = asyncio.Lock()
|
|
448
460
|
async with self._cmd_lock:
|
|
449
461
|
# Cancel any stale capture future before creating a new one
|
|
450
|
-
if self.receiver
|
|
462
|
+
if self.receiver._capture_future and not self.receiver._capture_future.done():
|
|
451
463
|
self.receiver._capture_future.cancel()
|
|
452
464
|
|
|
453
465
|
loop = asyncio.get_running_loop()
|
|
454
466
|
future = loop.create_future()
|
|
455
|
-
|
|
456
|
-
self.receiver.set_capture_future(future)
|
|
467
|
+
self.receiver.set_capture_future(future)
|
|
457
468
|
|
|
458
469
|
osc_data = self._build_osc(command, args)
|
|
459
470
|
self._sock.sendto(osc_data, self._m4l_addr)
|
|
@@ -463,8 +474,7 @@ class M4LBridge:
|
|
|
463
474
|
return result
|
|
464
475
|
except asyncio.TimeoutError:
|
|
465
476
|
# Clean up the dangling future
|
|
466
|
-
|
|
467
|
-
self.receiver._capture_future = None
|
|
477
|
+
self.receiver._capture_future = None
|
|
468
478
|
return {"error": "M4L capture timeout — device may be busy or removed"}
|
|
469
479
|
|
|
470
480
|
async def cancel_capture_future(self) -> None:
|
|
@@ -12,6 +12,7 @@ def build_automation_graph(
|
|
|
12
12
|
track_infos: list[dict],
|
|
13
13
|
sections: list[dict] | None = None,
|
|
14
14
|
clip_automation: list[dict] | None = None,
|
|
15
|
+
clips_scanned: int = 0,
|
|
15
16
|
) -> AutomationGraph:
|
|
16
17
|
"""Build an AutomationGraph covering both device-parameter automation
|
|
17
18
|
hints and real clip envelopes (BUG-E2).
|
|
@@ -27,11 +28,17 @@ def build_automation_graph(
|
|
|
27
28
|
parameter_name, parameter_type, device_name}].
|
|
28
29
|
This is the ground truth — `device.parameters[i].is_automated`
|
|
29
30
|
only reflects mapping state, not the presence of an envelope.
|
|
31
|
+
clips_scanned: total number of session clips the caller actually
|
|
32
|
+
probed for envelopes. Used to compute ``coverage_pct``; pass 0
|
|
33
|
+
when the caller couldn't enumerate clips (unknown → 0.0).
|
|
30
34
|
|
|
31
35
|
Returns:
|
|
32
|
-
AutomationGraph with automated_params and
|
|
36
|
+
AutomationGraph with automated_params, density_by_section, and
|
|
37
|
+
the v1.10.9 coverage signals (coverage_pct, clip_envelope_count,
|
|
38
|
+
clips_scanned).
|
|
33
39
|
"""
|
|
34
40
|
graph = AutomationGraph()
|
|
41
|
+
graph.clips_scanned = max(0, int(clips_scanned))
|
|
35
42
|
|
|
36
43
|
if not track_infos and not clip_automation:
|
|
37
44
|
return graph
|
|
@@ -121,4 +128,19 @@ def build_automation_graph(
|
|
|
121
128
|
else:
|
|
122
129
|
graph.density_by_section[section_id] = 0.0
|
|
123
130
|
|
|
131
|
+
# BUG-D2 coverage signals.
|
|
132
|
+
# clip_envelope_count = distinct (track, clip) slots containing any envelope.
|
|
133
|
+
clip_slots_with_envelope: set[tuple[int, int | None]] = set()
|
|
134
|
+
for env in clip_automation or []:
|
|
135
|
+
clip_slots_with_envelope.add(
|
|
136
|
+
(int(env.get("track_index", -1)), env.get("clip_index"))
|
|
137
|
+
)
|
|
138
|
+
graph.clip_envelope_count = len(clip_slots_with_envelope)
|
|
139
|
+
if graph.clips_scanned > 0:
|
|
140
|
+
graph.coverage_pct = min(
|
|
141
|
+
1.0, graph.clip_envelope_count / float(graph.clips_scanned)
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
graph.coverage_pct = 0.0
|
|
145
|
+
|
|
124
146
|
return graph
|
|
@@ -24,6 +24,7 @@ def build_project_state_from_data(
|
|
|
24
24
|
notes_map: Optional[dict[str, dict[int, list[dict]]]] = None,
|
|
25
25
|
arrangement_clips: Optional[dict] = None,
|
|
26
26
|
clip_automation: Optional[list[dict]] = None,
|
|
27
|
+
clips_scanned: int = 0,
|
|
27
28
|
analyzer_ok: bool = False,
|
|
28
29
|
flucoma_ok: bool = False,
|
|
29
30
|
plugin_health: Optional[dict[str, Any]] = None,
|
|
@@ -107,6 +108,7 @@ def build_project_state_from_data(
|
|
|
107
108
|
track_infos=track_infos or [],
|
|
108
109
|
sections=section_dicts_for_auto,
|
|
109
110
|
clip_automation=clip_automation or [],
|
|
111
|
+
clips_scanned=clips_scanned,
|
|
110
112
|
)
|
|
111
113
|
state.automation_graph.freshness.mark_fresh(state.revision)
|
|
112
114
|
|
|
@@ -205,16 +205,35 @@ class RoleGraph:
|
|
|
205
205
|
|
|
206
206
|
@dataclass
|
|
207
207
|
class AutomationGraph:
|
|
208
|
-
"""Automation presence and gesture density.
|
|
208
|
+
"""Automation presence and gesture density.
|
|
209
|
+
|
|
210
|
+
``coverage_pct`` is the fraction of scanned clips that have at least
|
|
211
|
+
one automation envelope (0.0–1.0). Introduced in v1.10.9 to close
|
|
212
|
+
BUG-D2's "is this session missing automation?" signal — downstream
|
|
213
|
+
engines (Wonder Mode, Sound Design, etc.) can branch on a low
|
|
214
|
+
coverage value to recommend filter sweeps, volume crescendos, and
|
|
215
|
+
dub-style handoffs that the producer hasn't written yet.
|
|
216
|
+
|
|
217
|
+
``clip_envelope_count`` is the raw total of per-clip envelopes
|
|
218
|
+
discovered; distinguishes "no automation in the project at all"
|
|
219
|
+
(count=0) from "automation exists but is lightly used" (count>0 but
|
|
220
|
+
coverage_pct<0.2).
|
|
221
|
+
"""
|
|
209
222
|
|
|
210
223
|
automated_params: list[dict] = field(default_factory=list)
|
|
211
224
|
density_by_section: dict[str, float] = field(default_factory=dict)
|
|
225
|
+
coverage_pct: float = 0.0
|
|
226
|
+
clip_envelope_count: int = 0
|
|
227
|
+
clips_scanned: int = 0
|
|
212
228
|
freshness: FreshnessInfo = field(default_factory=FreshnessInfo)
|
|
213
229
|
|
|
214
230
|
def to_dict(self) -> dict:
|
|
215
231
|
return {
|
|
216
232
|
"automated_params": list(self.automated_params),
|
|
217
233
|
"density_by_section": dict(self.density_by_section),
|
|
234
|
+
"coverage_pct": round(self.coverage_pct, 3),
|
|
235
|
+
"clip_envelope_count": self.clip_envelope_count,
|
|
236
|
+
"clips_scanned": self.clips_scanned,
|
|
218
237
|
"freshness": self.freshness.to_dict(),
|
|
219
238
|
}
|
|
220
239
|
|
|
@@ -131,11 +131,15 @@ def build_project_brain(ctx: Context) -> dict:
|
|
|
131
131
|
# automation actually lives on each clip (session + arrangement). We
|
|
132
132
|
# walk every clip slot that has a clip and ask get_clip_automation, then
|
|
133
133
|
# aggregate into a flat list keyed by section.
|
|
134
|
+
#
|
|
135
|
+
# clips_scanned is the denominator for coverage_pct (BUG-D2) — it
|
|
136
|
+
# counts how many (track, scene) slots we probed, regardless of
|
|
137
|
+
# whether an envelope came back. Without this, a session with zero
|
|
138
|
+
# automation would be indistinguishable from a session where we
|
|
139
|
+
# failed to probe, which is exactly the ambiguity BUG-D2 flagged.
|
|
134
140
|
clip_automation: list[dict] = []
|
|
141
|
+
clips_scanned = 0
|
|
135
142
|
try:
|
|
136
|
-
# Iterate session scenes x tracks, plus arrangement clips we already have.
|
|
137
|
-
# Use the raw enumerate index for section_id so it stays aligned with
|
|
138
|
-
# arrangement_graph sections (which use the same scheme — see E1 fix).
|
|
139
143
|
for scene_idx, scene in enumerate(scenes or []):
|
|
140
144
|
scene_name = str(scene.get("name", "")).strip()
|
|
141
145
|
if not scene_name:
|
|
@@ -143,6 +147,7 @@ def build_project_brain(ctx: Context) -> dict:
|
|
|
143
147
|
section_id = f"sec_{scene_idx:02d}"
|
|
144
148
|
for track in tracks:
|
|
145
149
|
t_idx = track.get("index", 0)
|
|
150
|
+
clips_scanned += 1
|
|
146
151
|
try:
|
|
147
152
|
auto_resp = ableton.send_command("get_clip_automation", {
|
|
148
153
|
"track_index": t_idx,
|
|
@@ -196,6 +201,7 @@ def build_project_brain(ctx: Context) -> dict:
|
|
|
196
201
|
notes_map=notes_map if notes_map else None,
|
|
197
202
|
arrangement_clips=arrangement_clips if arrangement_clips else None,
|
|
198
203
|
clip_automation=clip_automation if clip_automation else None,
|
|
204
|
+
clips_scanned=clips_scanned,
|
|
199
205
|
analyzer_ok=analyzer_ok,
|
|
200
206
|
flucoma_ok=flucoma_ok,
|
|
201
207
|
session_ok=True,
|
|
@@ -230,6 +236,7 @@ def get_project_brain_summary(ctx: Context) -> dict:
|
|
|
230
236
|
"section_count": len(state.arrangement_graph.sections),
|
|
231
237
|
"role_count": len(state.role_graph.roles),
|
|
232
238
|
"automated_param_count": len(state.automation_graph.automated_params),
|
|
239
|
+
"automation_coverage_pct": round(state.automation_graph.coverage_pct, 3),
|
|
233
240
|
"tempo": state.session_graph.tempo,
|
|
234
241
|
"time_signature": state.session_graph.time_signature,
|
|
235
242
|
"is_stale": state.is_stale(),
|