livepilot 1.27.2 → 1.27.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 +66 -0
- package/README.md +10 -7
- package/bin/livepilot.js +97 -39
- package/livepilot/.Codex-plugin/plugin.json +1 -1
- package/livepilot/.claude-plugin/plugin.json +1 -1
- package/livepilot/skills/livepilot-core/SKILL.md +33 -1
- package/livepilot/skills/livepilot-core/references/atlas-tool-notes.md +69 -0
- package/livepilot/skills/livepilot-core/references/overview.md +18 -11
- package/livepilot/skills/livepilot-core/references/perception.md +125 -0
- package/livepilot/skills/livepilot-devices/references/device-parameter-units.md +66 -0
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-mix-engine/SKILL.md +8 -0
- package/livepilot/skills/livepilot-release/SKILL.md +1 -1
- package/livepilot/skills/livepilot-sample-engine/references/splice-tools-notes.md +56 -0
- package/livepilot/skills/livepilot-wonder/SKILL.md +6 -0
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +181 -40
- package/mcp_server/atlas/overlays.py +46 -3
- package/mcp_server/atlas/tools.py +226 -86
- package/mcp_server/audit/checks.py +50 -22
- package/mcp_server/audit/state.py +10 -2
- package/mcp_server/audit/tools.py +27 -6
- package/mcp_server/composer/develop/apply.py +7 -7
- package/mcp_server/composer/fast/apply.py +29 -23
- package/mcp_server/composer/framework/atlas_resolver.py +16 -1
- package/mcp_server/composer/full/apply.py +40 -34
- package/mcp_server/composer/full/engine.py +66 -32
- package/mcp_server/composer/tools.py +4 -3
- package/mcp_server/connection.py +67 -9
- package/mcp_server/creative_constraints/engine.py +10 -2
- package/mcp_server/creative_constraints/tools.py +24 -7
- package/mcp_server/curves.py +30 -7
- package/mcp_server/device_forge/builder.py +40 -11
- package/mcp_server/device_forge/models.py +9 -0
- package/mcp_server/device_forge/tools.py +30 -11
- package/mcp_server/experiment/engine.py +29 -22
- package/mcp_server/experiment/tools.py +15 -5
- package/mcp_server/m4l_bridge.py +69 -7
- package/mcp_server/memory/taste_graph.py +43 -0
- package/mcp_server/memory/technique_store.py +26 -3
- package/mcp_server/memory/tools.py +62 -4
- package/mcp_server/mix_engine/critics.py +187 -30
- package/mcp_server/mix_engine/models.py +21 -1
- package/mcp_server/mix_engine/state_builder.py +87 -8
- package/mcp_server/mix_engine/tools.py +16 -4
- package/mcp_server/performance_engine/tools.py +56 -8
- package/mcp_server/persistence/base_store.py +11 -0
- package/mcp_server/persistence/project_store.py +132 -8
- package/mcp_server/persistence/taste_store.py +90 -7
- package/mcp_server/preview_studio/engine.py +40 -10
- package/mcp_server/preview_studio/models.py +40 -0
- package/mcp_server/preview_studio/tools.py +56 -12
- package/mcp_server/project_brain/arrangement_graph.py +4 -0
- package/mcp_server/project_brain/models.py +2 -0
- package/mcp_server/project_brain/role_graph.py +13 -7
- package/mcp_server/reference_engine/gap_analyzer.py +58 -3
- package/mcp_server/reference_engine/profile_builder.py +47 -4
- package/mcp_server/reference_engine/tools.py +6 -0
- package/mcp_server/runtime/execution_router.py +51 -1
- package/mcp_server/runtime/tools.py +0 -1
- package/mcp_server/sample_engine/sources.py +0 -2
- package/mcp_server/sample_engine/tools.py +19 -38
- package/mcp_server/semantic_moves/mix_compilers.py +276 -51
- package/mcp_server/semantic_moves/performance_compilers.py +51 -17
- package/mcp_server/semantic_moves/resolvers.py +45 -0
- package/mcp_server/semantic_moves/sound_design_compilers.py +14 -6
- package/mcp_server/semantic_moves/tools.py +80 -2
- package/mcp_server/semantic_moves/transition_compilers.py +26 -9
- package/mcp_server/server.py +26 -24
- package/mcp_server/session_continuity/tracker.py +51 -3
- package/mcp_server/song_brain/builder.py +47 -5
- package/mcp_server/song_brain/tools.py +21 -7
- package/mcp_server/sound_design/critics.py +1 -0
- package/mcp_server/splice_client/client.py +117 -33
- package/mcp_server/splice_client/http_bridge.py +15 -3
- package/mcp_server/splice_client/quota.py +28 -0
- package/mcp_server/stuckness_detector/detector.py +8 -5
- package/mcp_server/synthesis_brain/adapters/drift.py +13 -0
- package/mcp_server/synthesis_brain/adapters/meld.py +13 -0
- package/mcp_server/tools/_analyzer_engine/sample.py +36 -10
- package/mcp_server/tools/_perception_engine.py +6 -0
- package/mcp_server/tools/agent_os.py +4 -1
- package/mcp_server/tools/analyzer.py +198 -209
- package/mcp_server/tools/arrangement.py +7 -4
- package/mcp_server/tools/automation.py +24 -4
- package/mcp_server/tools/browser.py +25 -11
- package/mcp_server/tools/clips.py +6 -0
- package/mcp_server/tools/composition.py +33 -2
- package/mcp_server/tools/devices.py +53 -53
- package/mcp_server/tools/generative.py +14 -14
- package/mcp_server/tools/harmony.py +7 -7
- package/mcp_server/tools/mixing.py +4 -4
- package/mcp_server/tools/planner.py +68 -6
- package/mcp_server/tools/research.py +20 -2
- package/mcp_server/tools/theory.py +10 -10
- package/mcp_server/tools/transport.py +7 -2
- package/mcp_server/transition_engine/critics.py +13 -1
- package/mcp_server/user_corpus/tools.py +30 -1
- package/mcp_server/wonder_mode/engine.py +82 -9
- package/mcp_server/wonder_mode/session.py +32 -10
- package/mcp_server/wonder_mode/tools.py +14 -1
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/arrangement.py +93 -33
- package/remote_script/LivePilot/browser.py +60 -4
- package/remote_script/LivePilot/devices.py +132 -62
- package/remote_script/LivePilot/mixing.py +31 -5
- package/remote_script/LivePilot/server.py +94 -22
- package/remote_script/LivePilot/transport.py +11 -0
- package/requirements.txt +5 -5
- package/server.json +2 -2
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Perception (Analyzer) Reference
|
|
2
|
+
|
|
3
|
+
Deep-dive detail for `mcp_server/tools/analyzer.py` tools. The tool
|
|
4
|
+
docstrings carry a trimmed operational contract (params, ranges,
|
|
5
|
+
gotchas); this file carries the full band tables, classification
|
|
6
|
+
thresholds, and historical bug context that motivated each behavior.
|
|
7
|
+
Read this before deep spectral/health-check work — the tool docstrings
|
|
8
|
+
point back here for the parts that don't change from call to call.
|
|
9
|
+
|
|
10
|
+
## `get_master_spectrum` — 9-band table
|
|
11
|
+
|
|
12
|
+
Band energies (fffb~ center frequencies shown in parens), values 0.0-1.0:
|
|
13
|
+
|
|
14
|
+
| Band | Range | Center | Use |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| sub_low | 20-60 Hz | ~35 Hz | kick fundamentals, deep sub-bass |
|
|
17
|
+
| sub | 60-120 Hz | ~85 Hz | 808s, sub-bass body |
|
|
18
|
+
| low | 120-250 Hz | ~175 Hz | bass body, warmth |
|
|
19
|
+
| low_mid | 250-500 Hz | ~350 Hz | mud zone, male vocal lows |
|
|
20
|
+
| mid | 500 Hz-1 kHz | ~700 Hz | vocal presence, snare body |
|
|
21
|
+
| high_mid | 1-2 kHz | ~1.4 kHz | consonants, pick attack |
|
|
22
|
+
| high | 2-4 kHz | ~2.8 kHz | presence, vocal intelligibility |
|
|
23
|
+
| presence | 4-8 kHz | ~5.6 kHz | cymbal definition, air of breath |
|
|
24
|
+
| air | 8-20 kHz | ~12 kHz | shimmer, sparkle |
|
|
25
|
+
|
|
26
|
+
Older `.amxd` builds (pre-v1.16) emit the legacy 8-band layout without
|
|
27
|
+
the explicit `sub_low` split — the server auto-detects band count from
|
|
28
|
+
the OSC payload and picks the right name set. Re-freeze the Max device
|
|
29
|
+
to get the 9-band resolution.
|
|
30
|
+
|
|
31
|
+
**BUG-2026-04-22#6 fix — windowed averaging.** Kick transients make
|
|
32
|
+
single snapshots swing wildly (0.45 → 0.05 → 0.16 within a bar). The
|
|
33
|
+
`window_ms` param samples the cache over a time window and mean-pools
|
|
34
|
+
instead of returning one instantaneous frame.
|
|
35
|
+
|
|
36
|
+
**BUG-2026-04-22#15 fix — sub-band resolution.** `sub_detail=True`
|
|
37
|
+
derives three finer buckets from the FluCoMa 40-band mel spectrum
|
|
38
|
+
(band 0-1 ≈ sub_deep 0-45 Hz — kick fundamental, band 2 ≈ sub_mid
|
|
39
|
+
45-60 Hz — 808 body/kick upper, band 3 ≈ sub_high 60-80 Hz — bass
|
|
40
|
+
guitar low/sub-bass crossover). Mel band edges are perceptual, not
|
|
41
|
+
linear Hz, so these are approximations tight enough for mixing
|
|
42
|
+
decisions ("is energy in the 30 Hz or 60 Hz range?"). Requires FluCoMa
|
|
43
|
+
active — omitted with `sub_detail_warning` otherwise.
|
|
44
|
+
|
|
45
|
+
## `verify_device_health` / `verify_all_devices_health`
|
|
46
|
+
|
|
47
|
+
**BUG-2026-04-22#19 fix.** `parameter_count` alone can't tell you
|
|
48
|
+
whether an AU/VST is alive — plenty of "loaded" plugins return N
|
|
49
|
+
params and silence. These tools fire a real test MIDI note and read
|
|
50
|
+
the track meter across a window (`get_track_meters(samples=N)` to
|
|
51
|
+
dodge the BUG-#7 "left=right=0 while level>0" artifact).
|
|
52
|
+
|
|
53
|
+
Common dead-device causes (surfaced in the `hint` field when
|
|
54
|
+
`alive=False`): (1) plugin waiting for preset/bank selection, (2)
|
|
55
|
+
algorithm/envelope configured for zero output, (3) wrong MIDI channel
|
|
56
|
+
or velocity curve, (4) dead VST (reinstall). Try opening the device UI
|
|
57
|
+
and auditioning manually.
|
|
58
|
+
|
|
59
|
+
`verify_all_devices_health` (BUG-2026-04-26#1 fix): audio-track
|
|
60
|
+
detection uses `has_midi_input`/`has_audio_input` from
|
|
61
|
+
`get_session_info` (earlier code checked nonexistent `is_audio_track`/
|
|
62
|
+
`type` fields, so detection silently always evaluated False).
|
|
63
|
+
Empty-track detection requires a `get_track_info` round-trip per track
|
|
64
|
+
because `get_session_info` doesn't embed per-track `devices` arrays.
|
|
65
|
+
|
|
66
|
+
## `classify_simpler_slices` — classification thresholds
|
|
67
|
+
|
|
68
|
+
Validated on "Break Ghosts 90 bpm" reference material:
|
|
69
|
+
- KICK: sub+low >= 45%, high < 40%
|
|
70
|
+
- HAT: high >= 70% AND mid < 25% (thin metal disc = no drum body)
|
|
71
|
+
- SNARE: mid >= 25% AND high >= 40% AND peak >= 0.6 (broadband loud)
|
|
72
|
+
- ghost: peak < 0.35
|
|
73
|
+
|
|
74
|
+
**Always run this before programming drum patterns on a sliced break.**
|
|
75
|
+
Slice content depends on transient detection order in the source
|
|
76
|
+
audio — slice 0 is NOT guaranteed to be a kick. Assuming drum-rack
|
|
77
|
+
convention produces wrong grooves that take iterations to diagnose.
|
|
78
|
+
|
|
79
|
+
File-path resolution order: explicit `file_path` param, then Remote
|
|
80
|
+
Script TCP (`get_simpler_file_path` via direct LOM read — most
|
|
81
|
+
reliable), then M4L bridge UDP fallback (kept registered for
|
|
82
|
+
environments where Remote Script is stale/unavailable; call
|
|
83
|
+
`reload_handlers` to refresh without a full restart).
|
|
84
|
+
|
|
85
|
+
## `add_drum_rack_pad`
|
|
86
|
+
|
|
87
|
+
**BUG-2026-04-22#1 fix** — this tool closes a gap where
|
|
88
|
+
`load_browser_item` replaced the existing chain on repeat calls and
|
|
89
|
+
`load_sample_to_simpler` couldn't address nested rack paths. It chains
|
|
90
|
+
six steps atomically: locate/auto-detect the Drum Rack, insert a chain,
|
|
91
|
+
assign the trigger note, insert an empty Simpler into the chain, native
|
|
92
|
+
`replace_sample_native` with nested addressing, Snap=0 hygiene.
|
|
93
|
+
|
|
94
|
+
## `replace_simpler_sample` / `load_sample_to_simpler`
|
|
95
|
+
|
|
96
|
+
Prefer `load_browser_item(track, uri)` when the file is browser-indexed
|
|
97
|
+
— the M4L bridge's replace path can silently keep the bootstrap
|
|
98
|
+
placeholder in some conditions (this is why both tools verify by
|
|
99
|
+
reading back the device name post-load and error if the replace didn't
|
|
100
|
+
actually take effect).
|
|
101
|
+
|
|
102
|
+
Nested addressing (`chain_index` + `nested_device_index`) is Live
|
|
103
|
+
12.4+ only (BUG-#1, 2026-04-22) — resolves at
|
|
104
|
+
`track.devices[device_index].chains[chain_index].devices[nested_device_index or 0]`,
|
|
105
|
+
which is how Drum Rack pad-by-pad construction works. The M4L bridge
|
|
106
|
+
fallback cannot resolve nested paths; only the native 12.4 path honors
|
|
107
|
+
`chain_index`.
|
|
108
|
+
|
|
109
|
+
`load_sample_to_simpler`'s bootstrap flow (used pre-12.4 or when no
|
|
110
|
+
Simpler exists yet): loads a dummy sample via the browser to force
|
|
111
|
+
Ableton to create a Simpler, replaces it with the target file, then
|
|
112
|
+
runs the same post-load hygiene/verification as `replace_simpler_sample`.
|
|
113
|
+
|
|
114
|
+
## `simpler_set_warp` / `compressor_set_sidechain` — LOM gap tools
|
|
115
|
+
|
|
116
|
+
**BUG-A2**: Python's Remote Script ControlSurface API can't reach
|
|
117
|
+
Simpler's `warping`/`warp_mode` — they live on the sample child object
|
|
118
|
+
(`SimplerDevice.sample.*`) that only Max for Live's JavaScript LiveAPI
|
|
119
|
+
can step into. Hence the M4L bridge round-trip.
|
|
120
|
+
|
|
121
|
+
**BUG-A3**: `compressor_set_sidechain`'s routing properties
|
|
122
|
+
(`sidechain_input_routing_type`/`_channel`) aren't in Compressor's
|
|
123
|
+
automatable parameter list, but Python's Remote Script reaches them
|
|
124
|
+
directly as device properties (same LOM pattern as `set_track_routing`)
|
|
125
|
+
— no M4L bridge needed here, unlike the warp case above.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Device Parameter Units Reference
|
|
2
|
+
|
|
3
|
+
Full per-device unit table for `set_device_parameter` / `batch_set_parameters`
|
|
4
|
+
(`mcp_server/tools/devices.py`). The tool docstrings carry the most
|
|
5
|
+
commonly-hit gotchas inline; this file is the complete list plus the
|
|
6
|
+
bug history that motivated the error-enrichment behavior.
|
|
7
|
+
|
|
8
|
+
## Parameter ranges are NOT always 0-1 (BUG-B4 / B9 / 2026-04-26#2)
|
|
9
|
+
|
|
10
|
+
Ableton devices use MIXED units depending on the parameter. Always read
|
|
11
|
+
the `value_string` in the response (and `min`/`max` from
|
|
12
|
+
`get_device_parameters`) before assuming 0-1 semantics:
|
|
13
|
+
|
|
14
|
+
- Auto Filter `Frequency`: 20-135 index (NOT normalized)
|
|
15
|
+
- Auto Filter Legacy `LFO Amount`: 0-30 absolute (displays as %)
|
|
16
|
+
- Auto Filter `Resonance`: 0-1.25 on legacy, 0-1 on AutoFilter2
|
|
17
|
+
- Auto Filter `Env. Modulation`: -127..+127 on legacy
|
|
18
|
+
- Compressor I (legacy): pre-2010 units (Threshold dB direct)
|
|
19
|
+
- **Compressor 2 (modern, default)**: 0-1 NORMALIZED. `Threshold 0.85 ≈
|
|
20
|
+
0 dB`, `Ratio 0.75 = 4:1`, `Release 0.16 = 30 ms`. Setting Threshold
|
|
21
|
+
to a dB value like -22 will fail. Compute normalized: `(dB + 50) / 50`
|
|
22
|
+
for typical dB→0-1 mapping, OR read the param's `value_string` after
|
|
23
|
+
a probe write.
|
|
24
|
+
- **Saturator** `Drive`, `Output`, `Threshold`, `Color *`: 0-1
|
|
25
|
+
NORMALIZED (Drive 0.5 ≈ 0 dB, Drive 0.6 ≈ +7 dB).
|
|
26
|
+
- Dynamic Tube, Vocoder: pre-2010 units
|
|
27
|
+
- EQ Three `Frequency Hi/Lo`: 50Hz-15kHz absolute
|
|
28
|
+
- Wavetable `Osc 1 Pos`: 0-1 normalized ✓
|
|
29
|
+
- Drift / Analog / Operator macros: 0-1 normalized ✓
|
|
30
|
+
- Pedal `Output`: -20..+20 dB direct
|
|
31
|
+
- Pedal `Bass / Mid / Treble`: -1..+1 direct
|
|
32
|
+
|
|
33
|
+
## Error enrichment (BUG-2026-04-26#2)
|
|
34
|
+
|
|
35
|
+
If the Remote Script rejects the value as out-of-range,
|
|
36
|
+
`set_device_parameter` fetches the parameter's actual
|
|
37
|
+
min/max/value_string and re-raises with that context inline — saves a
|
|
38
|
+
follow-up `get_device_parameters` round-trip in the agent loop after
|
|
39
|
+
every miss.
|
|
40
|
+
|
|
41
|
+
## Silent snapping (BUG #4, v1.20.2)
|
|
42
|
+
|
|
43
|
+
Quantized-enum params (e.g. Beat Repeat's "Gate" at an integer enum)
|
|
44
|
+
silently snap a caller's float request to the nearest step. Both
|
|
45
|
+
`set_device_parameter` (via the `snapped` field) and
|
|
46
|
+
`batch_set_parameters` (via `snapped_params`, tolerance 1e-5) surface
|
|
47
|
+
this so callers driving deterministic state don't mistake a snap for
|
|
48
|
+
success. `batch_set_parameters` accepts any of `parameter_index`,
|
|
49
|
+
`parameter_name`, `name`, `index`, or the legacy `name_or_index` per
|
|
50
|
+
entry (BUG-F4 / BUG-2026-04-22#3 — the sibling tools originally had
|
|
51
|
+
inconsistent schemas, so all shapes are now normalized so
|
|
52
|
+
`get_device_parameters`'s output can be fed straight back in).
|
|
53
|
+
|
|
54
|
+
## Drum Rack construction workflow (12.3+)
|
|
55
|
+
|
|
56
|
+
Shared by `insert_device`, `insert_rack_chain`, and the atomic
|
|
57
|
+
`add_drum_rack_pad` (see `perception.md` for that one's full history):
|
|
58
|
+
|
|
59
|
+
1. `insert_device(track_index, 'Drum Rack')` — create empty rack
|
|
60
|
+
2. `insert_rack_chain(track_index, device_index)` — add a chain
|
|
61
|
+
3. `set_drum_chain_note(chain_index, note=36)` — assign C1 (kick)
|
|
62
|
+
4. `insert_device(track_index, 'Simpler', device_index=rack_idx,
|
|
63
|
+
chain_index=0)` — add the instrument into that chain
|
|
64
|
+
|
|
65
|
+
On Live < 12.3, `insert_device`/`insert_rack_chain` return an error
|
|
66
|
+
suggesting `find_and_load_device` instead.
|
|
@@ -173,7 +173,7 @@ Live 12.4 introduces a new capability tier that unlocks native LOM access for sa
|
|
|
173
173
|
**Tool signatures:** unchanged. Callers do not need to detect the tier —
|
|
174
174
|
routing is transparent.
|
|
175
175
|
|
|
176
|
-
**Probe-first 12.4 surfaces in v1.27.
|
|
176
|
+
**Probe-first 12.4 surfaces in v1.27.3:**
|
|
177
177
|
- `probe_link_audio()` reports observed peer/input/routing visibility and
|
|
178
178
|
returns `manual_only`, `readable`, `routable`, or `unavailable`.
|
|
179
179
|
- `probe_stem_workflow()` reports observed callable paths and returns
|
|
@@ -27,6 +27,14 @@ If the M4L analyzer bridge is absent, critics fall back to role-based heuristics
|
|
|
27
27
|
|
|
28
28
|
For detailed frequency collision data, call `get_masking_report`. For a quick status overview without the full critic pass, call `get_mix_summary`.
|
|
29
29
|
|
|
30
|
+
Each masking entry carries `measured` and `severity_basis`: `measured=True` /
|
|
31
|
+
`severity_basis="spectral_overlap"` means `severity` came from real per-track
|
|
32
|
+
spectral data; `measured=False` / `severity_basis="role_heuristic"` means it's
|
|
33
|
+
a role-pair prior with no per-track measurement behind it. Treat
|
|
34
|
+
`measured=False` entries as low-confidence hypotheses, not verified problems —
|
|
35
|
+
solo the pair and read `get_master_spectrum` before acting on them. See
|
|
36
|
+
`livepilot-core` SKILL.md → "Response Fields You Must Check".
|
|
37
|
+
|
|
30
38
|
### Step 2 — Plan
|
|
31
39
|
|
|
32
40
|
Pick the highest-severity issue from the `issues` array. Call `plan_mix_move` with the issue data. The planner returns the smallest intervention that addresses the problem — a single parameter change, not a chain of edits.
|
|
@@ -31,7 +31,7 @@ Run this checklist EVERY time the user says "update everything", "push", "releas
|
|
|
31
31
|
Current: **467 tools across 56 domains**.
|
|
32
32
|
Spectral/analyzer (bridge-only): **38**. The remaining tool surface works without the bridge or degrades gracefully. Backed by 32 bridge commands.
|
|
33
33
|
|
|
34
|
-
Verify: `
|
|
34
|
+
Verify: `python3 scripts/sync_metadata.py --check`
|
|
35
35
|
|
|
36
36
|
Files that reference tool count:
|
|
37
37
|
- [ ] `README.md` — header ("467 tools. 56 domains"), bridge section ("38 spectral/analyzer tools require bridge")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Splice Tool Notes
|
|
2
|
+
|
|
3
|
+
Implementation history and worked examples for the Splice-facing tools
|
|
4
|
+
in `mcp_server/sample_engine/tools.py`. The tool docstrings carry the
|
|
5
|
+
operational contract (params, return shape, gating rules); this file
|
|
6
|
+
carries endpoint-capture history and full response examples.
|
|
7
|
+
|
|
8
|
+
## `get_splice_credits` — full response example (Ableton Live plan)
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"connected": true,
|
|
13
|
+
"username": "user-1367453956",
|
|
14
|
+
"plan_raw": "subscribed",
|
|
15
|
+
"plan_kind": "ableton_live",
|
|
16
|
+
"sounds_plan_id": 12,
|
|
17
|
+
"features": {"ableton_unmetered": true},
|
|
18
|
+
"credits_remaining": 80,
|
|
19
|
+
"credit_floor": 5,
|
|
20
|
+
"daily_quota": {
|
|
21
|
+
"used_today": 3, "remaining_today": 97, "daily_limit": 100,
|
|
22
|
+
"near_limit": false, "at_limit": false
|
|
23
|
+
},
|
|
24
|
+
"can_download_sample": true,
|
|
25
|
+
"download_gating": "daily_quota"
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`download_gating` is `"daily_quota"` on the Ableton Live plan (the
|
|
30
|
+
100/day unmetered quota) or `"credit_floor"` on Sounds+/Creator/
|
|
31
|
+
Creator+ plans (the `CREDIT_HARD_FLOOR` protects the last 5 credits).
|
|
32
|
+
`connected: false` with zero credits means the Splice desktop app isn't
|
|
33
|
+
running or `grpcio` isn't installed.
|
|
34
|
+
|
|
35
|
+
## `splice_describe_sound` / `splice_generate_variation` — endpoint history
|
|
36
|
+
|
|
37
|
+
Both hit GraphQL operations on `surfaces-graphql.splice.com`, captured
|
|
38
|
+
via mitmproxy against Splice desktop 5.4.9 + the Sounds Plugin
|
|
39
|
+
(status: LIVE as of 2026-04-22):
|
|
40
|
+
|
|
41
|
+
- `splice_describe_sound` → `SamplesSearch` with `semantic=1` +
|
|
42
|
+
`rephrase=true`. This is the Sounds Plugin's "Describe a Sound"
|
|
43
|
+
feature — Splice's AI matches free-form descriptions like "dark
|
|
44
|
+
ambient pad with shimmer" to catalog samples.
|
|
45
|
+
- `splice_generate_variation` → `AssetSimilarSoundsQuery` (the
|
|
46
|
+
right-click "Variations" menu item). Up to 10 results, no credit
|
|
47
|
+
cost — despite the tool name, this is a similarity-recommender
|
|
48
|
+
lookup, not AI audio synthesis (the "generate" naming in the original
|
|
49
|
+
handoff spec was aspirational).
|
|
50
|
+
|
|
51
|
+
Both tools strip the raw GraphQL response before returning (~270KB of
|
|
52
|
+
noise per call) — only the flattened sample list ships back.
|
|
53
|
+
|
|
54
|
+
`splice_search_with_sound` was removed 2026-04-22 — the user does this
|
|
55
|
+
in-Splice manually. The capture recipe for resurrecting it is at
|
|
56
|
+
`docs/2026-04-22-splice-https-capture-recipe.md`.
|
|
@@ -111,6 +111,12 @@ Wonder Mode now serves two callers:
|
|
|
111
111
|
references/move-family-diversity-rule.md)
|
|
112
112
|
- 1 executable + 2 analytical is an honest, useful result
|
|
113
113
|
- The `variant_count_actual` field tells you how many are real
|
|
114
|
+
- **Check `degraded_reason`** on every `enter_wonder_mode` response — when
|
|
115
|
+
non-empty, fewer than 3 variants are actually executable (or none matched
|
|
116
|
+
and the set is cold-start fallback). Do not present a degraded set as
|
|
117
|
+
"three genuinely different options"; surface the degradation to the user
|
|
118
|
+
in the same terms the field describes. See `livepilot-core` SKILL.md →
|
|
119
|
+
"Response Fields You Must Check".
|
|
114
120
|
|
|
115
121
|
## Presenting Results
|
|
116
122
|
|
|
Binary file
|
|
@@ -34,7 +34,7 @@ outlets = 2; // 0: to udpsend (responses), 1: to buffer~/status
|
|
|
34
34
|
// Single source of truth for the bridge version — bumped alongside the
|
|
35
35
|
// rest of the release manifest. Surfaced in the UI via messnamed("livepilot_version", ...)
|
|
36
36
|
// so the frozen .amxd visibly reports which build it was last exported from.
|
|
37
|
-
var VERSION = "1.27.
|
|
37
|
+
var VERSION = "1.27.3";
|
|
38
38
|
|
|
39
39
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
40
40
|
|
package/mcp_server/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
|
|
2
|
-
__version__ = "1.27.
|
|
2
|
+
__version__ = "1.27.3"
|
|
@@ -13,6 +13,20 @@ from typing import Any, Dict, List, Optional
|
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
|
+
# BUG-B39: the real atlas scanner emits "instruments" / "audio_effects" but
|
|
17
|
+
# older callers and test fixtures sometimes pass the singular "instrument" /
|
|
18
|
+
# "effect". Hoisted to module scope (was previously re-declared inline in
|
|
19
|
+
# search()) so both search() and the truncation-warning helper below share
|
|
20
|
+
# one alias table instead of drifting independently.
|
|
21
|
+
_CATEGORY_ALIASES: Dict[str, set] = {
|
|
22
|
+
"instrument": {"instrument", "instruments"},
|
|
23
|
+
"instruments": {"instrument", "instruments"},
|
|
24
|
+
"effect": {"effect", "effects", "audio_effects"},
|
|
25
|
+
"effects": {"effect", "effects", "audio_effects"},
|
|
26
|
+
"audio_effect": {"effect", "effects", "audio_effects", "audio_effect"},
|
|
27
|
+
"audio_effects": {"effect", "effects", "audio_effects", "audio_effect"},
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
|
|
17
31
|
class AtlasManager:
|
|
18
32
|
"""In-memory device atlas with indexed lookups."""
|
|
@@ -34,15 +48,50 @@ class AtlasManager:
|
|
|
34
48
|
self._meta["version"] = data["version"]
|
|
35
49
|
self._devices: List[Dict[str, Any]] = data.get("devices", [])
|
|
36
50
|
|
|
51
|
+
# P3-47 truncation observability: scan_full_library persists
|
|
52
|
+
# stats.category_truncated (keyed by atlas device-category, e.g.
|
|
53
|
+
# "drum_kits"/"sounds"/"samples") whenever the last scan hit its
|
|
54
|
+
# max_per_category cap — or ran out of the remote script's shared
|
|
55
|
+
# iteration safety bound — mid-category. Read it once here so
|
|
56
|
+
# search()/suggest() callers can be warned their results for a
|
|
57
|
+
# truncated category are a lower bound, not the full inventory.
|
|
58
|
+
raw_stats = data.get("stats", {})
|
|
59
|
+
raw_truncated = (
|
|
60
|
+
raw_stats.get("category_truncated", {})
|
|
61
|
+
if isinstance(raw_stats, dict) else {}
|
|
62
|
+
)
|
|
63
|
+
self._category_truncated: Dict[str, bool] = {
|
|
64
|
+
str(cat): bool(hit)
|
|
65
|
+
for cat, hit in raw_truncated.items()
|
|
66
|
+
} if isinstance(raw_truncated, dict) else {}
|
|
67
|
+
|
|
37
68
|
# ── Build indexes ───────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
|
|
69
|
+
# P2-12: _by_id / _by_name keep a LIST of every device sharing a
|
|
70
|
+
# key, in scan order, so colliding ids/names no longer shadow each
|
|
71
|
+
# other (last-wins). 719 ids + 702 names collide in the shipped
|
|
72
|
+
# atlas while URIs are 100% unique, so every device stays reachable
|
|
73
|
+
# via lookup_all() / atlas_device_info's surfaced URIs. lookup()
|
|
74
|
+
# still returns a single Dict (the first, deterministic match) to
|
|
75
|
+
# preserve its Optional[Dict] contract for compare()/browser.py/
|
|
76
|
+
# _research_engine.py.
|
|
77
|
+
self._by_id: Dict[str, List[Dict[str, Any]]] = {}
|
|
78
|
+
self._by_name: Dict[str, List[Dict[str, Any]]] = {} # lowercase key
|
|
40
79
|
self._by_uri: Dict[str, Dict[str, Any]] = {}
|
|
41
80
|
self._by_category: Dict[str, List[Dict[str, Any]]] = {}
|
|
42
81
|
self._by_tag: Dict[str, List[Dict[str, Any]]] = {}
|
|
43
82
|
self._by_genre: Dict[str, List[Dict[str, Any]]] = {}
|
|
44
83
|
self._by_pack: Dict[str, List[Dict[str, Any]]] = {}
|
|
45
84
|
|
|
85
|
+
# Perf batch (v1.27.3): a real 40k-device user atlas can carry
|
|
86
|
+
# ~16k duplicate ids/names/uris, and warning once per duplicate
|
|
87
|
+
# produced ~16k logger.warning calls (~122ms + multi-MB of stderr)
|
|
88
|
+
# at index-build time. Cap each category at _DUP_WARN_CAP explicit
|
|
89
|
+
# warnings and emit one summary line for the rest.
|
|
90
|
+
_DUP_WARN_CAP = 20
|
|
91
|
+
_dup_id_count = 0
|
|
92
|
+
_dup_name_count = 0
|
|
93
|
+
_dup_uri_count = 0
|
|
94
|
+
|
|
46
95
|
for dev in self._devices:
|
|
47
96
|
dev_id = dev.get("id", "")
|
|
48
97
|
dev_name = dev.get("name", "")
|
|
@@ -50,26 +99,38 @@ class AtlasManager:
|
|
|
50
99
|
dev_category = dev.get("category", "")
|
|
51
100
|
|
|
52
101
|
if dev_id:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
102
|
+
bucket = self._by_id.setdefault(dev_id, [])
|
|
103
|
+
if bucket and dev not in bucket:
|
|
104
|
+
_dup_id_count += 1
|
|
105
|
+
if _dup_id_count <= _DUP_WARN_CAP:
|
|
106
|
+
logger.warning(
|
|
107
|
+
"atlas: duplicate device id %r (name=%r); both kept, "
|
|
108
|
+
"lookup() returns the first — disambiguate by uri %r",
|
|
109
|
+
dev_id, dev_name, dev_uri,
|
|
110
|
+
)
|
|
111
|
+
if dev not in bucket:
|
|
112
|
+
bucket.append(dev)
|
|
59
113
|
if dev_name:
|
|
60
114
|
name_key = dev_name.lower()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
115
|
+
name_bucket = self._by_name.setdefault(name_key, [])
|
|
116
|
+
if name_bucket and dev not in name_bucket:
|
|
117
|
+
_dup_name_count += 1
|
|
118
|
+
if _dup_name_count <= _DUP_WARN_CAP:
|
|
119
|
+
logger.warning(
|
|
120
|
+
"atlas: duplicate device name %r (id=%r); both kept, "
|
|
121
|
+
"lookup() returns the first — disambiguate by uri %r",
|
|
122
|
+
dev_name, dev_id, dev_uri,
|
|
123
|
+
)
|
|
124
|
+
if dev not in name_bucket:
|
|
125
|
+
name_bucket.append(dev)
|
|
67
126
|
if dev_uri:
|
|
68
127
|
if dev_uri in self._by_uri and self._by_uri[dev_uri] is not dev:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
128
|
+
_dup_uri_count += 1
|
|
129
|
+
if _dup_uri_count <= _DUP_WARN_CAP:
|
|
130
|
+
logger.warning(
|
|
131
|
+
"atlas: duplicate device uri %r shadows a prior entry "
|
|
132
|
+
"(id=%r); last-wins", dev_uri, dev_id,
|
|
133
|
+
)
|
|
73
134
|
self._by_uri[dev_uri] = dev
|
|
74
135
|
|
|
75
136
|
# Category index
|
|
@@ -124,6 +185,22 @@ class AtlasManager:
|
|
|
124
185
|
if pack_name:
|
|
125
186
|
self._by_pack.setdefault(pack_name, []).append(dev)
|
|
126
187
|
|
|
188
|
+
if _dup_id_count > _DUP_WARN_CAP:
|
|
189
|
+
logger.warning(
|
|
190
|
+
"atlas: %d more duplicate device ids suppressed",
|
|
191
|
+
_dup_id_count - _DUP_WARN_CAP,
|
|
192
|
+
)
|
|
193
|
+
if _dup_name_count > _DUP_WARN_CAP:
|
|
194
|
+
logger.warning(
|
|
195
|
+
"atlas: %d more duplicate device names suppressed",
|
|
196
|
+
_dup_name_count - _DUP_WARN_CAP,
|
|
197
|
+
)
|
|
198
|
+
if _dup_uri_count > _DUP_WARN_CAP:
|
|
199
|
+
logger.warning(
|
|
200
|
+
"atlas: %d more duplicate device uris suppressed",
|
|
201
|
+
_dup_uri_count - _DUP_WARN_CAP,
|
|
202
|
+
)
|
|
203
|
+
|
|
127
204
|
# ── Properties ──────────────────────────────────────────────────
|
|
128
205
|
|
|
129
206
|
@property
|
|
@@ -155,6 +232,49 @@ class AtlasManager:
|
|
|
155
232
|
},
|
|
156
233
|
}
|
|
157
234
|
|
|
235
|
+
# ── Truncation observability (P3-47) ──────────────────────────
|
|
236
|
+
def truncated_categories(self) -> List[str]:
|
|
237
|
+
"""Categories where the atlas's last scan_full_library run hit
|
|
238
|
+
its max_per_category cap (or the remote script's shared iteration
|
|
239
|
+
safety bound) mid-category. Devices in these categories are a
|
|
240
|
+
lower bound, not the full inventory, until a rescan completes."""
|
|
241
|
+
return sorted(
|
|
242
|
+
cat for cat, hit in self._category_truncated.items() if hit
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def truncation_warning(self, category: str = "all") -> Optional[str]:
|
|
246
|
+
"""Return a warning string if `category` overlaps a category
|
|
247
|
+
flagged truncated in the loaded atlas's scan stats, else None.
|
|
248
|
+
|
|
249
|
+
category="all" (the default `atlas_search`/`atlas_suggest` filter)
|
|
250
|
+
warns if ANY category was truncated, since an unfiltered query
|
|
251
|
+
silently includes those partial results alongside complete ones.
|
|
252
|
+
A specific category only warns when it (or an alias — see
|
|
253
|
+
_CATEGORY_ALIASES) is in the truncated set.
|
|
254
|
+
"""
|
|
255
|
+
truncated = self.truncated_categories()
|
|
256
|
+
if not truncated:
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
if category == "all":
|
|
260
|
+
hit = truncated
|
|
261
|
+
else:
|
|
262
|
+
allowed = _CATEGORY_ALIASES.get(category, {category})
|
|
263
|
+
hit = [cat for cat in truncated if cat in allowed]
|
|
264
|
+
if not hit:
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
"Atlas categor%s %s may be incomplete — the last scan hit its "
|
|
269
|
+
"size cap for %s. Run scan_full_library(force=True) (raise "
|
|
270
|
+
"max_per_category if your library is unusually large) for a "
|
|
271
|
+
"complete inventory before trusting an exhaustive search here."
|
|
272
|
+
) % (
|
|
273
|
+
"y" if len(hit) == 1 else "ies",
|
|
274
|
+
", ".join(sorted(hit)),
|
|
275
|
+
"this category" if len(hit) == 1 else "these categories",
|
|
276
|
+
)
|
|
277
|
+
|
|
158
278
|
# ── Pack lookup ────────────────────────────────────────────────
|
|
159
279
|
def pack_info(self, pack_name: str) -> Dict[str, Any]:
|
|
160
280
|
"""Return summary of a pack — device list + enrichment coverage.
|
|
@@ -217,19 +337,52 @@ class AtlasManager:
|
|
|
217
337
|
# ── Lookup ──────────────────────────────────────────────────────
|
|
218
338
|
|
|
219
339
|
def lookup(self, name_or_id: str) -> Optional[Dict[str, Any]]:
|
|
220
|
-
"""Exact match by ID, name (case-insensitive), or URI. Returns None on miss.
|
|
340
|
+
"""Exact match by ID, name (case-insensitive), or URI. Returns None on miss.
|
|
341
|
+
|
|
342
|
+
When an id/name collides across devices (719 ids / 702 names in the
|
|
343
|
+
shipped atlas), this returns the FIRST device in scan order
|
|
344
|
+
deterministically. Use lookup_all() to reach every colliding device,
|
|
345
|
+
or pass a unique URI (URIs never collide) to target a specific one.
|
|
346
|
+
"""
|
|
221
347
|
# Try ID first
|
|
222
|
-
|
|
223
|
-
|
|
348
|
+
bucket = self._by_id.get(name_or_id)
|
|
349
|
+
if bucket:
|
|
350
|
+
return bucket[0]
|
|
224
351
|
# Try name (case-insensitive)
|
|
225
|
-
|
|
226
|
-
if
|
|
227
|
-
return
|
|
228
|
-
# Try URI
|
|
352
|
+
name_bucket = self._by_name.get(name_or_id.lower())
|
|
353
|
+
if name_bucket:
|
|
354
|
+
return name_bucket[0]
|
|
355
|
+
# Try URI — the only key that is guaranteed unique
|
|
229
356
|
if name_or_id in self._by_uri:
|
|
230
357
|
return self._by_uri[name_or_id]
|
|
231
358
|
return None
|
|
232
359
|
|
|
360
|
+
def lookup_all(self, name_or_id: str) -> List[Dict[str, Any]]:
|
|
361
|
+
"""Return EVERY device matching an id, name, or URI — collision-aware.
|
|
362
|
+
|
|
363
|
+
P2-12: lookup() returns a single Dict (its contract), which shadows
|
|
364
|
+
the ~719 devices sharing a colliding id and ~702 sharing a name. This
|
|
365
|
+
companion returns all of them so id/name-keyed tools can disambiguate
|
|
366
|
+
(e.g. surface each candidate's unique URI). Matches are returned in
|
|
367
|
+
scan order with duplicates removed; an exact URI match alone returns
|
|
368
|
+
the single device for that URI.
|
|
369
|
+
"""
|
|
370
|
+
out: List[Dict[str, Any]] = []
|
|
371
|
+
seen: set[int] = set()
|
|
372
|
+
|
|
373
|
+
def _extend(devs: List[Dict[str, Any]]) -> None:
|
|
374
|
+
for d in devs:
|
|
375
|
+
if id(d) not in seen:
|
|
376
|
+
seen.add(id(d))
|
|
377
|
+
out.append(d)
|
|
378
|
+
|
|
379
|
+
_extend(self._by_id.get(name_or_id, []))
|
|
380
|
+
_extend(self._by_name.get(name_or_id.lower(), []))
|
|
381
|
+
uri_hit = self._by_uri.get(name_or_id)
|
|
382
|
+
if uri_hit is not None:
|
|
383
|
+
_extend([uri_hit])
|
|
384
|
+
return out
|
|
385
|
+
|
|
233
386
|
# ── Search ──────────────────────────────────────────────────────
|
|
234
387
|
|
|
235
388
|
def search(
|
|
@@ -243,22 +396,10 @@ class AtlasManager:
|
|
|
243
396
|
query_words = query_lower.split()
|
|
244
397
|
results: List[Dict[str, Any]] = []
|
|
245
398
|
|
|
246
|
-
#
|
|
247
|
-
#
|
|
248
|
-
# pass the singular "instrument" / "effect". Build a tolerant
|
|
249
|
-
# category alias set so both forms work.
|
|
250
|
-
_CAT_ALIASES = {
|
|
251
|
-
"instrument": {"instrument", "instruments"},
|
|
252
|
-
"instruments": {"instrument", "instruments"},
|
|
253
|
-
"effect": {"effect", "effects", "audio_effects"},
|
|
254
|
-
"effects": {"effect", "effects", "audio_effects"},
|
|
255
|
-
"audio_effect": {"effect", "effects", "audio_effects",
|
|
256
|
-
"audio_effect"},
|
|
257
|
-
"audio_effects": {"effect", "effects", "audio_effects",
|
|
258
|
-
"audio_effect"},
|
|
259
|
-
}
|
|
399
|
+
# Tolerant category alias set so "instrument" and "instruments"
|
|
400
|
+
# (etc.) both work — see module-level _CATEGORY_ALIASES.
|
|
260
401
|
allowed_cats = (
|
|
261
|
-
|
|
402
|
+
_CATEGORY_ALIASES.get(category, {category})
|
|
262
403
|
if category != "all" else None
|
|
263
404
|
)
|
|
264
405
|
|