livepilot 1.26.1 → 1.26.2

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/livepilot/.Codex-plugin/plugin.json +1 -1
  3. package/livepilot/.claude-plugin/plugin.json +1 -1
  4. package/livepilot/agents/livepilot-producer/AGENT.md +2 -2
  5. package/livepilot/commands/beat.md +3 -3
  6. package/livepilot/commands/evaluate.md +3 -1
  7. package/livepilot/commands/mix.md +2 -2
  8. package/livepilot/commands/sounddesign.md +2 -2
  9. package/livepilot/skills/livepilot-core/SKILL.md +6 -6
  10. package/livepilot/skills/livepilot-core/references/overview.md +1 -1
  11. package/livepilot/skills/livepilot-creative-director/SKILL.md +7 -0
  12. package/livepilot/skills/livepilot-creative-director/references/move-family-diversity-rule.md +1 -1
  13. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  14. package/livepilot/skills/livepilot-mix-engine/SKILL.md +9 -1
  15. package/livepilot/skills/livepilot-mixing/SKILL.md +12 -5
  16. package/livepilot/skills/livepilot-release/SKILL.md +2 -2
  17. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +25 -3
  18. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  19. package/m4l_device/livepilot_bridge.js +1 -1
  20. package/mcp_server/__init__.py +1 -1
  21. package/mcp_server/composer/full/brief_builder.py +9 -0
  22. package/mcp_server/evaluation/feature_extractors.py +152 -8
  23. package/mcp_server/mix_engine/state_builder.py +19 -2
  24. package/mcp_server/mix_engine/tools.py +22 -0
  25. package/mcp_server/sound_design/tools.py +33 -0
  26. package/mcp_server/tools/_agent_os_engine/evaluation.py +7 -44
  27. package/mcp_server/tools/_agent_os_engine/models.py +2 -1
  28. package/mcp_server/tools/_conductor.py +5 -2
  29. package/mcp_server/tools/_evaluation_contracts.py +1 -1
  30. package/mcp_server/tools/_snapshot_normalizer.py +32 -3
  31. package/package.json +1 -1
  32. package/remote_script/LivePilot/__init__.py +1 -1
  33. package/server.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.26.2 — 2026-05-27
4
+
5
+ Patch release for Claude/Codex plugin instruction correctness and local install reliability.
6
+
7
+ ### Fixed
8
+
9
+ - Claude/Codex plugin commands now use `ensure_analyzer_on_master` instead of direct master-track analyzer loading, preserving the invariant that `LivePilot_Analyzer` measures the final post-master-chain signal.
10
+ - `/beat` now builds the master processing chain before ensuring the analyzer, preventing pre-effect spectral/RMS reads in fresh sessions.
11
+ - V2 semantic-move guidance now matches runtime behavior: `apply_semantic_move(mode="improve")` compiles an approval-ready plan and does not execute until the returned steps are run.
12
+ - Plugin device-loading guidance now routes through the Device Atlas first, then exact browser URI loading, with `find_and_load_device` reserved for simple built-in effects.
13
+ - Release checklist stale claims were corrected: removed the obsolete non-analyzer subtotal and updated the domain-list reminder from 45 to 56 domains.
14
+ - Producer-agent capability guidance now distinguishes stale/intermittent analyzer data (`measured_degraded`) from analyzer absence (`judgment_only`).
15
+
16
+ ### Tests
17
+
18
+ - Added `tests/test_plugin_instruction_contracts.py` to prevent regressions in analyzer preflight guidance, semantic-move approval semantics, release-count claims, and core-skill enriched-device metadata coverage.
19
+
3
20
  ## v1.26.1 — 2026-05-24
4
21
 
5
22
  Patch release for installer/release hygiene and live execution correctness.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.26.1",
3
+ "version": "1.26.2",
4
4
  "description": "Agentic production system for Ableton Live 12 \u2014 465 tools, 56 domains, 44 semantic moves, device atlas (5264 devices, 120 enriched, 7 indexes), Splice intelligence (gRPC + GraphQL describe-a-sound + preview + collections + presets), 9-band spectral perception auto-loaded via ensure_analyzer_on_master, Creative Director skill, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
5
5
  "author": {
6
6
  "name": "Pilot Studio"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.26.1",
3
+ "version": "1.26.2",
4
4
  "description": "Agentic production system for Ableton Live 12 \u2014 465 tools, 56 domains, 44 semantic moves, device atlas (5264 devices, 120 enriched, 7 indexes), Splice intelligence (gRPC + GraphQL describe-a-sound + preview + collections + presets), 9-band spectral perception auto-loaded via ensure_analyzer_on_master, Creative Director skill, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
5
5
  "author": {
6
6
  "name": "Pilot Studio"
@@ -206,7 +206,7 @@ After loading any instrument:
206
206
  ### Critical device loading rules:
207
207
 
208
208
  - **NEVER load bare "Drum Rack"** — it's empty. Load a **kit preset**: `search_browser` path="Drums" name_filter="Kit" → `load_browser_item`
209
- - **For synths, use `search_browser` → `load_browser_item`** with exact URI
209
+ - **For synths, consult `atlas_search` or `atlas_suggest` first, then use `search_browser` → `load_browser_item`** with exact URI
210
210
  - **After loading any effect**, set key parameters to non-default values
211
211
 
212
212
  ## V2 Engine Intelligence
@@ -241,7 +241,7 @@ This replaces ad-hoc `get_session_info` + `get_track_info` calls for complex tas
241
241
 
242
242
  Call `get_capability_state` to know what's trustworthy right now:
243
243
  - `normal`: full analyzer + evaluation loop available
244
- - `measured_degraded`: no analyzer — defer to musical judgment for keep/undo
244
+ - `measured_degraded`: analyzer data is stale or intermittent refresh playback/cache before trusting spectral comparisons
245
245
  - `judgment_only`: minimal evidence — be conservative
246
246
  - `read_only`: can inspect but not mutate
247
247
 
@@ -11,8 +11,8 @@ If the user asks for a **fresh start** (new track, clean slate, start from scrat
11
11
 
12
12
  1. **Read the session** — `get_session_info` to see what exists
13
13
  2. **Delete all existing tracks** — loop through all tracks with `delete_track`, starting from the highest index down to 0 (deleting from the top prevents index shifts)
14
- 3. **Load the M4L Analyzer on master** — `find_and_load_device(track_index=-1000, device_name="LivePilot_Analyzer")`. This enables real-time spectral analysis, RMS metering, and key detection for the entire session. If it fails, try `search_browser(path="max_for_live", name_filter="LivePilot")` to find the URI and load manually.
15
- 4. **Set up master chain** — load Glue Compressor + EQ Eight + Utility on master for bus processing
14
+ 3. **Set up master chain** — load Glue Compressor + EQ Eight + Utility on master for bus processing
15
+ 4. **Ensure analyzer last** — call `ensure_analyzer_on_master`. This enables real-time spectral analysis, RMS metering, and key detection, and prevents the analyzer from measuring pre-master-chain audio. If it returns `install_required`, call `install_m4l_device(source_path="<repo>/m4l_device/LivePilot_Analyzer.amxd")` and retry. If it warns that the analyzer is not last on master, tell the user and do not trust spectral reads until it is moved after all effects.
16
16
  5. **Verify analyzer** — wait 2 seconds, then call `get_master_spectrum`. If it returns data, the bridge is connected. If it errors with "UDP bridge not connected", call `reconnect_bridge` to rebind.
17
17
 
18
18
  If the user is adding to an **existing project**, skip step 0 — just call `get_session_info` and work with what's there.
@@ -24,7 +24,7 @@ Genre, tempo range, mood, reference tracks.
24
24
  `set_tempo`, create tracks for drums/bass/harmony/melody with `create_midi_track`, name and color them.
25
25
 
26
26
  ## Step 3: Load instruments
27
- Use `search_browser` to find devices by name, `load_browser_item` or `find_and_load_device` to load them.
27
+ Consult `atlas_search` or `atlas_suggest` first, then use `search_browser` to resolve the exact URI and `load_browser_item` to load it. Use `find_and_load_device` only for simple built-in effects named in the livepilot-core exception list.
28
28
 
29
29
  ## Step 4: Verify device health
30
30
  After loading, run `get_device_info` on each loaded device. A `parameter_count` of 0 or 1 on AU/VST plugins means the plugin failed to initialize (dead plugin). If detected:
@@ -12,7 +12,9 @@ Run the universal evaluation loop on recent production changes.
12
12
  - `read_only` — session disconnected
13
13
 
14
14
  2. **Ensure analyzer** — if mode is `judgment_only`, try to get full perception:
15
- - `find_and_load_device(track_index=-1000, device_name="LivePilot_Analyzer")`
15
+ - `ensure_analyzer_on_master`
16
+ - If it returns `install_required`, call `install_m4l_device(source_path="<repo>/m4l_device/LivePilot_Analyzer.amxd")` and retry
17
+ - If it warns that the analyzer is not last on master, report that spectral evidence is untrusted until repaired
16
18
  - Wait 2s, then `get_master_spectrum` to test the bridge
17
19
  - If bridge disconnected: `reconnect_bridge`
18
20
  - If still unavailable: proceed with `judgment_only` but tell the user
@@ -9,7 +9,7 @@ Help the user mix their session using the V2 orchestration pipeline.
9
9
 
10
10
  1. **Session kernel** — `get_session_kernel(request_text=<user's request>, mode="improve")` for the full turn snapshot
11
11
  2. **Route** — `route_request(<user's request>)` to get engine routes + semantic move recommendations
12
- 3. **Ensure analyzer** — if `get_master_spectrum` errors, load it: `find_and_load_device(track_index=-1000, device_name="LivePilot_Analyzer")`. If bridge disconnected, try `reconnect_bridge`.
12
+ 3. **Ensure analyzer** — call `ensure_analyzer_on_master` before trusting spectral reads. If it returns `install_required`, call `install_m4l_device(source_path="<repo>/m4l_device/LivePilot_Analyzer.amxd")` and retry. If it warns that the analyzer is not last on master, tell the user and treat spectral data as untrusted until it is repaired. If the bridge is disconnected, try `reconnect_bridge`.
13
13
 
14
14
  ## Analysis Phase
15
15
 
@@ -29,7 +29,7 @@ Help the user mix their session using the V2 orchestration pipeline.
29
29
 
30
30
  ## Execution Phase
31
31
 
32
- 11. **Apply with approval** — `apply_semantic_move(move_id, mode="improve")` returns the compiled plan. Present it to the user: "I'd suggest: push Drums to 0.75, pull Pad to 0.25. Shall I do it?"
32
+ 11. **Compile for approval** — `apply_semantic_move(move_id, mode="improve")` returns the compiled plan without executing it. Present the concrete steps to the user. After approval, execute the returned steps individually and verify each write.
33
33
  12. **Verify after EVERY change** — read `value_string` in response, call `get_track_meters(include_stereo=true)`, check no track went silent
34
34
  13. **Capture + analyze** — `capture_audio` then `analyze_loudness` for LUFS/LRA, `analyze_spectrum_offline` for centroid/balance
35
35
  14. **Evaluate** — `evaluate_mix_move` with before/after snapshots. If `keep_change` is false, `undo` immediately.
@@ -13,7 +13,7 @@ Guide the user through designing a sound using the V2 orchestration pipeline.
13
13
  ## Design Phase
14
14
 
15
15
  3. **Ask about target** — what character? (warm pad, aggressive bass, shimmering lead, etc.)
16
- 4. **Choose instrument** — `search_browser` to find devices, `load_browser_item` to load
16
+ 4. **Choose instrument** — consult `atlas_search` or `atlas_suggest` for candidate devices, then use `search_browser` to resolve the exact URI and `load_browser_item` to load
17
17
  5. **Verify health** — `get_device_info` to confirm plugin initialized. Read `value_string` from `get_device_parameters` to understand actual units.
18
18
  6. **Shape sound** — `set_device_parameter` or `batch_set_parameters`. **ALWAYS read `value_string` in response** to confirm Hz/dB/% values make sense.
19
19
  7. **Verify after every change** — `get_track_meters(include_stereo=true)` — if stereo drops to 0, the effect killed the signal.
@@ -26,7 +26,7 @@ Guide the user through designing a sound using the V2 orchestration pipeline.
26
26
 
27
27
  ## Effects & Automation
28
28
 
29
- 11. **Add effects** — load with `find_and_load_device(track_index, device_name)`. Verify health.
29
+ 11. **Add effects** — consult the atlas or relevant device-knowledge reference first. Load exact browser URIs with `load_browser_item`, or use `find_and_load_device` only for simple built-in effects named in the livepilot-core exception list. Verify health.
30
30
  12. **Organic movement** — `apply_automation_shape(curve_type="perlin")` for filter/send drift
31
31
  13. **Automation recipes** — `apply_automation_recipe` for breathing, vinyl_crackle, auto_pan. Verify after applying.
32
32
 
@@ -7,7 +7,7 @@ description: Core discipline for LivePilot — agentic production system for Abl
7
7
 
8
8
  Agentic production system for Ableton Live 12. 465 tools across 56 domains, three layers:
9
9
 
10
- - **Device Atlas** — 5264 devices indexed (135 enriched with sonic intelligence, 683 drum kits). Consult `atlas_search` or `atlas_suggest` before loading any device. Never guess a device name.
10
+ - **Device Atlas** — 5264 devices indexed (120 enriched with sonic intelligence, 683 drum kits). Consult `atlas_search` or `atlas_suggest` before loading any device. Never guess a device name.
11
11
  - **M4L Analyzer** — Real-time audio analysis on the master bus (9-band spectrum sub_low → air, RMS/peak, key detection). Optional — all core tools work without it.
12
12
  - **Technique Memory** — Persistent storage for production decisions. Consult `memory_recall` before creative tasks to understand user taste.
13
13
 
@@ -18,7 +18,7 @@ Agentic production system for Ableton Live 12. 465 tools across 56 domains, thre
18
18
  3. **Use `undo` liberally** — mention it to users when doing destructive ops
19
19
  4. **One operation at a time** — verify between steps
20
20
  5. **Track indices are 0-based** — negative for return tracks (-1=A, -2=B), -1000 for master
21
- 6. **NEVER invent device/preset names** — always `search_browser` first, use exact `uri` from results. Exception: `find_and_load_device` for built-in effects only ("Reverb", "Delay", "Compressor", "EQ Eight", "Saturator", "Utility")
21
+ 6. **NEVER invent device/preset names** — consult `atlas_search` or `atlas_suggest` first, then use `search_browser` and load the exact `uri` from results. Exception: `find_and_load_device` for built-in effects only ("Reverb", "Delay", "Compressor", "EQ Eight", "Saturator", "Utility")
22
22
  7. **Color indices 0-69** — Ableton's fixed palette
23
23
  8. **Volume 0.0-1.0, pan -1.0 to 1.0** — normalized, not dB
24
24
  9. **Tempo range 20-999 BPM**
@@ -76,7 +76,7 @@ Report ALL errors to the user immediately. Common failure modes:
76
76
  - **M4L bridge timeout** — device may be busy or removed → retry or skip analyzer features
77
77
  - **Connection timeout** — Ableton unresponsive → check if session is heavy
78
78
  - **Volume reset on scene fire** — Ableton restores mixer state when firing scenes. Always re-apply `set_track_volume`/`set_track_pan` after `fire_scene` if your mix settings differ from what was stored in the clips
79
- - **M4L Analyzer not connected** — if `get_master_spectrum` errors with "Analyzer not detected", auto-load it: `find_and_load_device(track_index=-1000, device_name="LivePilot_Analyzer")`. If it errors with "UDP bridge not connected", try `reconnect_bridge` first
79
+ - **M4L Analyzer not connected** — if `get_master_spectrum` errors with "Analyzer not detected", call `ensure_analyzer_on_master`. If it returns `install_required`, call `install_m4l_device(source_path="<repo>/m4l_device/LivePilot_Analyzer.amxd")` and retry. If it errors with "UDP bridge not connected", try `reconnect_bridge` first
80
80
  - **Another client connected** — Remote Script only accepts one TCP client on port 9878. If you see this error, the MCP server is already connected. Use MCP tools instead of raw TCP
81
81
 
82
82
  ## Technique Memory
@@ -176,8 +176,8 @@ Use when the user has a concrete, specific request ("tighten the low end", "make
176
176
  2. **`get_session_kernel`** — build the unified turn snapshot
177
177
  3. **`propose_next_best_move`** — get ranked semantic move suggestions (taste-aware)
178
178
  4. **`preview_semantic_move`** — see what a move will do before committing
179
- 5. **`apply_semantic_move`** — compile and execute the move
180
- 6. **Evaluate** — use the appropriate evaluator to check the result
179
+ 5. **`apply_semantic_move(mode="improve")`** — compile the move and return the concrete plan for approval; after approval, execute the returned steps individually
180
+ 6. **Evaluate** — use the appropriate evaluator after the approved steps actually run
181
181
 
182
182
  ### Flow B — Exploratory (branch-native, for creative search)
183
183
  Use when the user wants options, variants, or is stuck ("surprise me", "try some things", "I don't know what I want", "make it more like X"). **Flow B is also correct when `route_request` returns `workflow_mode="creative_search"`.**
@@ -198,7 +198,7 @@ Use when the user wants options, variants, or is stuck ("surprise me", "try some
198
198
  **Rule of thumb**: if the user asked for a specific fix, Flow A. If they asked "what would you do?" or mentioned feel/vibe without parameters, Flow B.
199
199
 
200
200
  ### Semantic Moves
201
- High-level musical intents that compile to deterministic tool sequences. 7 families (44 moves as of v1.26.1):
201
+ High-level musical intents that compile to deterministic tool sequences. 7 families (44 moves as of v1.26.2):
202
202
  - **mix** — `tighten_low_end`, `widen_stereo`, `make_punchier`, `darken_without_losing_width`, `reduce_repetition_fatigue`, `make_kick_bass_lock`, `reduce_foreground_competition`
203
203
  - **arrangement** — `refresh_repeated_section`, plus structural moves defined alongside mix
204
204
  - **transition** — `create_buildup_tension`, `smooth_scene_handoff`, `increase_contrast_before_payoff`, `bridge_sections`, `increase_forward_motion`, `open_chorus`, `create_breakdown`
@@ -1,4 +1,4 @@
1
- # LivePilot v1.26.1 — Architecture & Tool Reference
1
+ # LivePilot v1.26.2 — Architecture & Tool Reference
2
2
 
3
3
  Agentic production system for Ableton Live 12. 465 tools across 56 domains. Device atlas (5264 devices, 120 enriched, 47 with aesthetic-tagged `signature_techniques`), spectral perception (M4L analyzer with 9-band FFT — sub_low / sub / low / low_mid / mid / high_mid / high / presence / air), technique memory, automation intelligence (16 curve types, 15 recipes), music theory (Krumhansl-Schmuckler, species counterpoint), generative algorithms (Euclidean rhythm, tintinnabuli, phase shift, additive process), neo-Riemannian harmony (PRL transforms, Tonnetz), MIDI file I/O, **LIVE Splice describe-a-sound + variations via captured GraphQL endpoints (v1.17)**, drum-rack pad-by-pad construction, live dead-device detection via meter sampling, role-aware Simpler defaults, session-record arrangement-automation workaround.
4
4
 
@@ -107,6 +107,12 @@ this skill. No → divergence path.
107
107
 
108
108
  When triggered, these phases are REQUIRED in order. Skip none.
109
109
 
110
+ ## Character-First Bias
111
+
112
+ For open-ended quality requests, treat timbre and spectral character as the main creative surface. Do not let the `mix` family win just because words like punch, clean, warm, dark, bright, or wide could be solved by volume/pan/send changes. Prefer `sound_design`, `device_creation`, `sample`, `arrangement`, or `transition` when analyzer evidence suggests a source, instrument, parameter, modulation, envelope, or structural decision would create more musical value.
113
+
114
+ The `mix` family is dominant only when the user asks for balance, loudness, headroom, masking, stereo translation, send levels, or an explicit mix pass. Otherwise use mix analysis as safety/evidence and keep it out of the main creative slot.
115
+
110
116
  ### Phase 1 — Ground
111
117
 
112
118
  Read in parallel (all are fast). All of these are REQUIRED, not
@@ -119,6 +125,7 @@ advisory — skipping them is how pattern-repetition survives:
119
125
  - `get_action_ledger_summary(limit=10)` — recent committed moves (repeat detection, see `references/anti-repetition-rules.md` for the recency threshold table). **v1.20 correction**: previous docs pointed at `memory_list`, which actually reads the persistent technique library (opt-in `memory_learn` writes) — a DIFFERENT store. The action ledger is the authoritative source; `apply_semantic_move` in explore mode populates it automatically.
120
126
  - `get_last_move` — the single most recent committed move; populate the brief's `last_move_target` field so Phase 3 cannot repeat it
121
127
  - `get_project_brain_summary` (or `build_project_brain` if absent) — track identity, accepted novelty band
128
+ - Analyzer character read when available: `get_master_spectrum`, `get_spectral_shape`, `get_onsets`, `get_novelty`, and `get_momentary_loudness` for evidence about brightness, flatness, motion, transient shape, and loudness safety. Use these to bias Phase 3 toward instrument/device/parameter decisions, not low-value level tweaks.
122
129
  - `explain_song_identity` when the project has one
123
130
  - `detect_stuckness` — cheap; its confidence drives escalation decisions (see §Anti-Repetition Protocol below)
124
131
  - **Concept packet load (HARD filter when present):** if the user named an artist or genre, or if `project_brain` has a genre identity, retrieve the structured YAML packet from `livepilot-core/references/concepts/artists/<slug>.yaml` or `livepilot-core/references/concepts/genres/<slug>.yaml`. Fall back to the narrative .md entry only if no matching YAML exists. The packet's `avoid` list is a HARD filter on Phase 3 candidates. The packet's `reach_for` lists seed the candidate device pool. The packet's `key_techniques` list resolves to atlas `signature_techniques` or `sample-techniques.md` / `sound-design-deep.md` entries. If NO reference is named and `project_brain` has no genre identity, skip packet loading — do not infer. See `livepilot-core/references/concepts/_schema.md` for the full packet structure and loading rules.
@@ -44,7 +44,7 @@ Never invent an eighth family at the director level.
44
44
 
45
45
  **Discovery:** always call `list_semantic_moves(domain=<family>)` at
46
46
  runtime to enumerate — do not hardcode move IDs. Families are stable;
47
- the move catalog grows. As of v1.26.1 the runtime returns 44 moves
47
+ the move catalog grows. As of v1.26.2 the runtime returns 44 moves
48
48
  across all 7 domains.
49
49
 
50
50
  **Why the director never invents an eighth `rhythmic` family:** the
@@ -173,4 +173,4 @@ routing is transparent.
173
173
  - Link Audio (tempo-sync sharing between Live sets) — tracked as a
174
174
  future Collaborative-tier feature.
175
175
  - Stem Separation v2 — tracked as a future Collaborative-tier feature.
176
- Neither is available in the 1.26.1 release — still pending.
176
+ Neither is available in the 1.26.2 release — still pending.
@@ -7,6 +7,14 @@ description: This skill should be used when the user asks to "analyze my mix", "
7
7
 
8
8
  The mix engine runs an iterative critic loop: analyze, plan, execute, measure, evaluate, keep or undo. Every mix change is measured before and after. Nothing stays unless it scores better than the original.
9
9
 
10
+ ## Character-First Default
11
+
12
+ Do not treat the full loop as the default for vague requests like "make it better", "more character", "more alive", "punchier", "warmer", or "more interesting". Those are usually sound-design or creative-direction requests. Start from analyzer character (`sonic_character`, `get_spectral_shape`, `get_novelty`, `get_onsets`, `get_mel_spectrum`) and prefer source, instrument, device-chain, envelope, filter, saturation, modulation, and transient-shape decisions before generic level changes.
13
+
14
+ Use `set_track_volume`, `set_track_pan`, and broad send-level balancing only when the user explicitly asks for balance/level/pan/send work, or when analyzer evidence shows a safety/translation problem such as clipping, headroom collapse, mono collapse, or a severe masking issue. Producers can adjust simple loudness by ear quickly; LivePilot's value is in hearing spectral character and choosing a smarter musical intervention.
15
+
16
+ For normal work, cap mix-engine action to one high-value move plus a short verdict. Enter the repeated full loop only for explicit requests like "deep mix pass", "mastering prep", "fix all mix issues", or an exact target such as LUFS/headroom/mono compatibility.
17
+
10
18
  ## The Mix Critic Loop
11
19
 
12
20
  Follow these steps in order. Do not skip the evaluation step.
@@ -81,7 +89,7 @@ If the move scored above 0.7 and the user confirms satisfaction, call `memory_le
81
89
 
82
90
  ### Step 9 — Repeat
83
91
 
84
- Return to Step 1 and re-analyze. The critic list updates after each change. Continue until no high-severity issues remain or the user says to stop.
92
+ Return to Step 1 and re-analyze only when the user requested a deep/full mix pass. Otherwise stop after the first measured high-value intervention and report the remaining optional issues as suggestions. Avoid spending a turn on small volume-balancing loops unless they are the requested task.
85
93
 
86
94
  ## Quick Mix Checks
87
95
 
@@ -7,6 +7,12 @@ description: This skill should be used when the user asks to "mix", "balance lev
7
7
 
8
8
  Balance track levels, configure routing, apply mix effects, and analyze frequency content in Ableton Live.
9
9
 
10
+ ## Default Value Filter
11
+
12
+ For broad musical requests, do not spend the turn on manual-feeling volume balancing. Levels, pan, and sends are useful when the user asks for them, when clipping/headroom/translation is objectively unsafe, or when a routing architecture is part of the style. Otherwise, treat meters as context and use analyzer character to make higher-value choices: source selection, filter/envelope shape, saturation, modulation, transient design, or a better device/preset.
13
+
14
+ When a request says "more punch", "more warmth", "more character", "less flat", "more alive", or similar, route through sound-design/creative-director first and use mix tools only as safety checks.
15
+
10
16
  ## Read Before Write
11
17
 
12
18
  Always understand the current state before changing anything:
@@ -121,7 +127,7 @@ When the LivePilot Analyzer M4L device is on the master track:
121
127
  - `get_master_rms` — true RMS and peak levels for loudness assessment
122
128
  - `get_detected_key` — detect musical key from audio content
123
129
 
124
- Use spectrum data to make informed EQ decisions. If the low_mid band is 6 dB hotter than everything else, there is mud to clean up. If the air band is absent, the mix may sound dull.
130
+ Use spectrum data to make informed EQ decisions. If the low_mid band is 6 dB hotter than everything else, there is mud to clean up. If the air band is absent, the mix may sound dull. When FluCoMa streams are active, prefer `get_spectral_shape`, `get_mel_spectrum`, `get_onsets`, and `get_novelty` for character decisions; those descriptors tell you whether the sound is bright/dark, flat/peaked, static/moving, or transient/soft in a way simple level meters cannot.
125
131
 
126
132
  ## Mix Engine — Critic-Driven Analysis
127
133
 
@@ -144,11 +150,12 @@ Use the mix engine when the user wants a critical evaluation of their mix, not j
144
150
 
145
151
  Follow this progression — start fast, go deeper only when needed:
146
152
 
147
- 1. **Instant:** `get_master_spectrum` + `get_track_meters` — frequency balance + levels. Answers 80% of mix questions.
148
- 2. **Fast (1-5s):** `analyze_loudness` + `analyze_mix` — LUFS, true peak, and full mix analysis. For mastering prep.
149
- 3. **Slow (5-15s):** `compare_to_reference` + `analyze_spectrum_offline` — reference matching, offline spectral analysis. Ask the user first.
153
+ 1. **Instant:** `get_master_spectrum` + `get_track_meters` — frequency balance + safety context.
154
+ 2. **Fast character:** `get_spectral_shape` + `get_novelty` + `get_onsets` when available decide whether the next move belongs to sound design, arrangement, or mix.
155
+ 3. **Fast mix (1-5s):** `analyze_loudness` + `analyze_mix` — LUFS, true peak, and full mix analysis. For mastering prep or explicit mix critique.
156
+ 4. **Slow (5-15s):** `compare_to_reference` + `analyze_spectrum_offline` — reference matching, offline spectral analysis. Ask the user first.
150
157
 
151
- Never skip levels. Start at the lowest appropriate level and offer to go deeper.
158
+ Never skip safety context. Do not let safety context become a long volume-tweaking session unless the user asked for that.
152
159
 
153
160
  ## Reference
154
161
 
@@ -29,7 +29,7 @@ Run this checklist EVERY time the user says "update everything", "push", "releas
29
29
  ## 2. Tool Count (must ALL match)
30
30
 
31
31
  Current: **465 tools across 56 domains**.
32
- Spectral/analyzer (bridge-only): **38**. All others: **292** (work without bridge, some with graceful fallbacks). Backed by 32 bridge commands.
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
34
  Verify: `grep -rc "@mcp.tool" mcp_server/tools/ | grep -v ":0" | awk -F: '{sum+=$2} END{print sum}'`
35
35
 
@@ -64,7 +64,7 @@ Files that reference tool count:
64
64
  Current: **56 domains**: transport, tracks, clips, notes, devices, scenes, mixing, browser, arrangement, memory, analyzer, automation, theory, generative, harmony, midi_io, perception, agent_os, composition, motif, research, planner, project_brain, runtime, evaluation, mix_engine, sound_design, transition_engine, reference_engine, translation_engine, performance_engine, song_brain, preview_studio, hook_hunter, stuckness_detector, wonder_mode, session_continuity, creative_constraints, device_forge, sample_engine, atlas, composer, experiment, musical_intelligence, semantic_moves, diagnostics, follow_actions, grooves, scales, take_lanes, miditool, synthesis_brain, creative_director, user_corpus, audit, grader.
65
65
 
66
66
  - [ ] All files that mention domain count say "56 domains"
67
- - [ ] Domain lists include ALL 45 (especially newer domains — they're the most often omitted)
67
+ - [ ] Domain lists include ALL 56 (especially newer domains — they're the most often omitted)
68
68
 
69
69
  ## 4. npm Registry
70
70
 
@@ -7,6 +7,28 @@ description: This skill should be used when the user asks to "design a sound", "
7
7
 
8
8
  The sound design engine analyzes synth patches, identifies timbral weaknesses, and iteratively refines them through a measured critic loop. Every change is evaluated against the before state.
9
9
 
10
+ ## Analyzer Character Is the Main Signal
11
+
12
+ For broad quality requests, this skill is the primary route. "More punch", "warmer", "darker", "brighter", "less flat", "more alive", "more texture", and "more character" should become source/device/parameter decisions before they become volume moves.
13
+
14
+ When the analyzer is available, read character, not just level:
15
+
16
+ - `get_master_spectrum` for the 9-band contour
17
+ - `get_spectral_shape` for centroid, flatness, crest, rolloff, and brightness/noise shape
18
+ - `get_mel_spectrum` when EQ or source choice needs perceptual detail
19
+ - `get_onsets` for transient/envelope decisions
20
+ - `get_novelty` for movement/staticness decisions
21
+ - `get_momentary_loudness` only for safety/headroom/loudness context
22
+
23
+ Translate those measurements into musical moves:
24
+
25
+ - Bright/harsh character → filter contour, softer source, de-harshing, saturation tone; do not merely lower volume.
26
+ - Dark/dull character → oscillator/filter opening, excitation, air-band source, tasteful saturation; do not merely raise volume.
27
+ - Static/low novelty → modulation, envelope drift, LFO, generative device, granular/vector source.
28
+ - Weak punch → envelope/transient shaping, source layering, attack/release work; volume push is last.
29
+ - Flat/noisy spectrum → source substitution, subtractive filtering, simpler spectral role.
30
+ - Weak weight → instrument/register/source decision before master or track gain.
31
+
10
32
  ## Atlas-first reflex (v1.23.x+, MANDATORY before any creative move)
11
33
 
12
34
  Before producing ANY creative response, query the user's atlas overlays. The corpus contains 337 entries across 3 namespaces, plus 3,917 parameter-level JSON sidecars — far richer than anything inferable from training data alone.
@@ -97,7 +119,7 @@ Move vocabulary:
97
119
  ### Step 4 — Capture Before
98
120
 
99
121
  1. Call `get_device_parameters(track_index, device_index)` — save current parameter state
100
- 2. Call `get_master_spectrum` — save spectral snapshot (if analyzer available)
122
+ 2. Call `get_master_spectrum` plus the relevant character streams above — save spectral snapshot (if analyzer available)
101
123
 
102
124
  ### Step 5 — Execute
103
125
 
@@ -116,7 +138,7 @@ Execute one move at a time. Verify before continuing.
116
138
  Repeat the same measurements:
117
139
 
118
140
  1. Call `get_device_parameters(track_index, device_index)` — confirm the change took effect
119
- 2. Call `get_master_spectrum` — save post-change spectral snapshot
141
+ 2. Call `get_master_spectrum` plus the same character streams used before — save post-change spectral snapshot
120
142
 
121
143
  ### Step 7 — Evaluate
122
144
 
@@ -135,7 +157,7 @@ If `keep_change` is `true`, report the improvement. If score > 0.7, consider cal
135
157
 
136
158
  ### Step 9 — Repeat
137
159
 
138
- Return to Step 2. Re-analyze after each kept change. The critic list updates as issues are resolved. Continue until the user is satisfied or no high-severity issues remain.
160
+ Return to Step 2 only when the user asked for a deep refinement pass. In normal mode, stop after one meaningful character-improving move and summarize what changed plus the next optional direction. Avoid long loops of small parameter nudges.
139
161
 
140
162
  ## Working with Opaque Plugins
141
163
 
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.26.1";
37
+ var VERSION = "1.26.2";
38
38
 
39
39
  // ── State ──────────────────────────────────────────────────────────────────
40
40
 
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.26.1"
2
+ __version__ = "1.26.2"
@@ -47,6 +47,15 @@ _DESIGN_TARGETS = (
47
47
  "moves to schedule at chosen phrase boundaries. For niche style references "
48
48
  "in research_hooks, run WebSearch to ground your form choices in the "
49
49
  "actual conventions of that subgenre.\n\n"
50
+ "CHARACTER-FIRST NORMAL MODE:\n"
51
+ "Do not spend full mode on long level-balancing loops. Producers can adjust "
52
+ "simple volume by ear; your high-value job is to choose instruments, sources, "
53
+ "device chains, macro states, envelopes, filters, saturation, modulation, "
54
+ "and structural reveals that fit the requested character. Use analyzer/mix "
55
+ "feedback as evidence and safety, but prefer timbral/source decisions over "
56
+ "`set_track_volume`, `set_track_pan`, or broad send tweaking unless the "
57
+ "brief explicitly asks for mix balance, loudness, headroom, stereo translation, "
58
+ "or masking repair.\n\n"
50
59
  "INSTRUMENT SELECTION (v1.25 hybrid knowledge surface — MANDATED FOUR-SOURCE SEARCH):\n"
51
60
  "The brief's `atlas_anchors` is ONE source. Before committing any role pick "
52
61
  "you MUST also query the other three sources below. Factory-atlas-only picks "
@@ -9,7 +9,7 @@ All returned values are clamped to 0.0-1.0 for consistent scoring.
9
9
  from __future__ import annotations
10
10
 
11
11
  import math
12
- from typing import Optional
12
+ from typing import Any, Optional
13
13
 
14
14
  from ..tools._evaluation_contracts import MEASURABLE_DIMENSIONS
15
15
 
@@ -19,6 +19,42 @@ def _clamp(value: float, lo: float = 0.0, hi: float = 1.0) -> float:
19
19
  return max(lo, min(hi, value))
20
20
 
21
21
 
22
+ def _number(value: Any) -> Optional[float]:
23
+ """Best-effort numeric coercion for analyzer payloads."""
24
+ if isinstance(value, bool):
25
+ return float(value)
26
+ if isinstance(value, (int, float)):
27
+ return float(value)
28
+ return None
29
+
30
+
31
+ def _nested_number(payload: Any, *keys: str) -> Optional[float]:
32
+ """Read a numeric value from a dict payload using candidate keys."""
33
+ if isinstance(payload, dict):
34
+ for key in keys:
35
+ value = _number(payload.get(key))
36
+ if value is not None:
37
+ return value
38
+ return _number(payload)
39
+
40
+
41
+ def _centroid_to_unit(centroid: float) -> float:
42
+ """Map centroid-like values to 0..1.
43
+
44
+ FluCoMa deployments may report centroid in Hz or normalized units.
45
+ Values <= 1 are treated as normalized. Larger values are mapped across
46
+ a practical musical range where 150 Hz is very dark and 8 kHz is bright.
47
+ """
48
+ if centroid <= 1.0:
49
+ return _clamp(centroid)
50
+ return _clamp((centroid - 150.0) / (8000.0 - 150.0))
51
+
52
+
53
+ def _lufs_to_unit(lufs: float) -> float:
54
+ """Map momentary LUFS to a rough 0..1 energy proxy."""
55
+ return _clamp((lufs + 60.0) / 60.0)
56
+
57
+
22
58
  def extract_dimension_value(
23
59
  snapshot: dict,
24
60
  dimension: str,
@@ -37,31 +73,49 @@ def extract_dimension_value(
37
73
  if not snapshot or not isinstance(snapshot, dict):
38
74
  return None
39
75
 
40
- bands = snapshot.get("spectrum")
41
- if not bands:
42
- return None
76
+ bands = snapshot.get("spectrum") or {}
77
+ spectral_shape = snapshot.get("spectral_shape") or {}
78
+ onset = snapshot.get("onset") or {}
79
+ novelty = snapshot.get("novelty") or {}
80
+ loudness = snapshot.get("loudness") or {}
43
81
 
44
82
  rms = snapshot.get("rms")
45
83
  peak = snapshot.get("peak")
46
84
 
47
85
  if dimension == "brightness":
86
+ centroid = _nested_number(spectral_shape, "centroid", "centroid_hz")
87
+ if centroid is not None:
88
+ return _centroid_to_unit(centroid)
89
+ if not bands:
90
+ return None
48
91
  high = bands.get("high", 0)
49
92
  presence = bands.get("presence", 0)
50
93
  return _clamp((high + presence) / 2.0)
51
94
 
52
95
  elif dimension == "warmth":
96
+ if not bands:
97
+ return None
53
98
  return _clamp(bands.get("low_mid", 0))
54
99
 
55
100
  elif dimension == "weight":
56
- sub = bands.get("sub", 0)
101
+ if not bands:
102
+ return None
103
+ sub = bands.get("sub_low", bands.get("sub", 0))
57
104
  low = bands.get("low", 0)
58
105
  return _clamp((sub + low) / 2.0)
59
106
 
60
107
  elif dimension == "clarity":
108
+ if not bands:
109
+ return None
61
110
  low_mid = bands.get("low_mid", 0)
62
111
  return _clamp(1.0 - low_mid)
63
112
 
64
113
  elif dimension == "density":
114
+ flatness = _nested_number(spectral_shape, "flatness", "spectral_flatness")
115
+ if flatness is not None:
116
+ return _clamp(flatness)
117
+ if not bands:
118
+ return None
65
119
  vals = [max(v, 1e-10) for v in bands.values()
66
120
  if isinstance(v, (int, float))]
67
121
  if not vals:
@@ -71,14 +125,104 @@ def extract_dimension_value(
71
125
  return _clamp(geo_mean / max(arith_mean, 1e-10))
72
126
 
73
127
  elif dimension == "energy":
74
- return _clamp(rms) if rms is not None else None
128
+ rms_value = _number(rms)
129
+ if rms_value is not None:
130
+ return _clamp(rms_value)
131
+ lufs = _nested_number(loudness, "momentary_lufs", "lufs", "integrated_lufs")
132
+ if lufs is not None:
133
+ return _lufs_to_unit(lufs)
134
+ return None
75
135
 
76
136
  elif dimension == "punch":
77
- if rms and peak and rms > 0:
78
- crest_db = 20.0 * math.log10(max(peak / rms, 1.0))
137
+ rms_value = _number(rms)
138
+ peak_value = _number(peak)
139
+ if rms_value and peak_value and rms_value > 0:
140
+ crest_db = 20.0 * math.log10(max(peak_value / rms_value, 1.0))
79
141
  return _clamp(crest_db / 20.0)
142
+ onset_strength = _nested_number(onset, "strength", "onset")
143
+ if onset_strength is not None:
144
+ return _clamp(onset_strength)
145
+ spectral_crest = _nested_number(spectral_shape, "crest")
146
+ if spectral_crest is not None:
147
+ return _clamp(spectral_crest)
148
+ return None
149
+
150
+ elif dimension == "novelty":
151
+ novelty_score = _nested_number(novelty, "score", "novelty", "value")
152
+ return _clamp(novelty_score) if novelty_score is not None else None
153
+
154
+ elif dimension == "motion":
155
+ novelty_score = _nested_number(novelty, "score", "novelty", "value")
156
+ onset_strength = _nested_number(onset, "strength", "onset")
157
+ vals = [v for v in (novelty_score, onset_strength) if v is not None]
158
+ if vals:
159
+ return _clamp(sum(vals) / len(vals))
80
160
  return None
81
161
 
82
162
  else:
83
163
  # Unmeasurable dimension
84
164
  return None
165
+
166
+
167
+ def _label_low_mid_high(value: float, low: str, mid: str, high: str) -> str:
168
+ if value < 0.33:
169
+ return low
170
+ if value > 0.67:
171
+ return high
172
+ return mid
173
+
174
+
175
+ def extract_character_profile(snapshot: dict) -> dict:
176
+ """Summarize analyzer data as a production-oriented character profile.
177
+
178
+ This is intentionally descriptive, not prescriptive. Engines can attach
179
+ the profile to their analysis response so the agent chooses sound-source,
180
+ device, and parameter moves before reaching for generic level changes.
181
+ """
182
+ if not snapshot or not isinstance(snapshot, dict):
183
+ return {"available": False, "values": {}, "labels": {}, "biases": []}
184
+
185
+ dimensions = (
186
+ "brightness", "warmth", "weight", "clarity", "density",
187
+ "energy", "punch", "motion", "novelty",
188
+ )
189
+ values = {
190
+ dim: round(val, 4)
191
+ for dim in dimensions
192
+ if (val := extract_dimension_value(snapshot, dim)) is not None
193
+ }
194
+
195
+ labels: dict[str, str] = {}
196
+ if "brightness" in values:
197
+ labels["brightness"] = _label_low_mid_high(values["brightness"], "dark", "balanced", "bright")
198
+ if "warmth" in values:
199
+ labels["warmth"] = _label_low_mid_high(values["warmth"], "lean", "warm", "thick")
200
+ if "weight" in values:
201
+ labels["weight"] = _label_low_mid_high(values["weight"], "light", "grounded", "heavy")
202
+ if "density" in values:
203
+ labels["density"] = _label_low_mid_high(values["density"], "peaked", "shaped", "flat/noisy")
204
+ if "punch" in values:
205
+ labels["punch"] = _label_low_mid_high(values["punch"], "soft", "defined", "spiky")
206
+ if "motion" in values:
207
+ labels["motion"] = _label_low_mid_high(values["motion"], "static", "moving", "busy")
208
+
209
+ biases: list[str] = []
210
+ if values.get("brightness", 0.5) > 0.72:
211
+ biases.append("prefer filter tone, source choice, or de-harshing over lowering track volume")
212
+ if values.get("brightness", 0.5) < 0.28:
213
+ biases.append("prefer oscillator/filter opening, excitation, or air-band source choice over level boosts")
214
+ if values.get("motion", 0.5) < 0.25:
215
+ biases.append("prefer modulation, envelope drift, or evolving devices before static mix moves")
216
+ if values.get("punch", 0.5) < 0.25:
217
+ biases.append("prefer envelope/transient shaping or source layering before pushing volume")
218
+ if values.get("density", 0.0) > 0.75:
219
+ biases.append("prefer subtractive filtering or simpler source selection when the spectrum is flat/noisy")
220
+ if values.get("weight", 0.5) < 0.25:
221
+ biases.append("prefer instrument/register/source changes for low-end weight before master gain")
222
+
223
+ return {
224
+ "available": bool(values),
225
+ "values": values,
226
+ "labels": labels,
227
+ "biases": biases,
228
+ }
@@ -68,6 +68,19 @@ def _name_signals_non_anchor(track_name: str) -> bool:
68
68
  # Frequency bands where masking is most problematic.
69
69
  _MASKING_BANDS = ("sub", "low", "low_mid", "mid", "high_mid", "presence", "high")
70
70
 
71
+ _MASKING_ROLE_ALIASES = {
72
+ "sub_bass": "bass",
73
+ "hihat": "percussion",
74
+ "hat": "percussion",
75
+ "clap": "percussion",
76
+ "snare": "percussion",
77
+ }
78
+
79
+
80
+ def _masking_role(role: str) -> str:
81
+ """Normalize detailed track roles into the collision-rule vocabulary."""
82
+ return _MASKING_ROLE_ALIASES.get(role, role)
83
+
71
84
 
72
85
  # ── Balance ─────────────────────────────────────────────────────────
73
86
 
@@ -164,7 +177,7 @@ def build_masking_map(
164
177
  # Build role->indices mapping
165
178
  role_to_indices: dict[str, list[int]] = {}
166
179
  for idx, role in track_roles.items():
167
- role_to_indices.setdefault(role, []).append(idx)
180
+ role_to_indices.setdefault(_masking_role(role), []).append(idx)
168
181
 
169
182
  # Known problematic role pairs and their collision bands
170
183
  collision_rules: list[tuple[str, str, str, float]] = [
@@ -267,7 +280,11 @@ def build_mix_state(
267
280
  role_hints = role_hints or {}
268
281
 
269
282
  balance = build_balance_state(track_infos, role_hints)
270
- masking = build_masking_map(spectrum, role_hints)
283
+ inferred_roles = {
284
+ track.track_index: role_hints.get(track.track_index, track.role)
285
+ for track in balance.track_states
286
+ }
287
+ masking = build_masking_map(spectrum, inferred_roles)
271
288
 
272
289
  # Extract peak from spectrum if available
273
290
  peak = None
@@ -11,6 +11,7 @@ from fastmcp import Context
11
11
  from ..server import mcp
12
12
  from ..tools._evaluation_contracts import EvaluationRequest
13
13
  from ..tools._snapshot_normalizer import normalize_sonic_snapshot
14
+ from ..evaluation.feature_extractors import extract_character_profile
14
15
  from ..evaluation.fabric import evaluate_sonic_move
15
16
  from .state_builder import build_mix_state
16
17
  from .critics import run_all_mix_critics
@@ -56,6 +57,18 @@ def _fetch_mix_data(ctx: Context) -> dict:
56
57
  rms_snap = spectral.get("rms")
57
58
  if rms_snap:
58
59
  rms_data = rms_snap["value"] if isinstance(rms_snap["value"], dict) else rms_snap["value"]
60
+ if spectrum is not None:
61
+ spectrum["rms"] = rms_data.get("rms") if isinstance(rms_data, dict) else rms_data
62
+ peak_snap = spectral.get("peak")
63
+ if peak_snap and spectrum is not None:
64
+ spectrum["peak"] = peak_snap["value"]
65
+
66
+ for key in ("spectral_shape", "mel_bands", "chroma", "onset", "novelty", "loudness"):
67
+ snap = spectral.get(key)
68
+ if snap:
69
+ if spectrum is None:
70
+ spectrum = {}
71
+ spectrum[key] = snap["value"]
59
72
  except Exception as exc:
60
73
  logger.debug("_fetch_mix_data failed: %s", exc)
61
74
 
@@ -86,9 +99,12 @@ def analyze_mix(ctx: Context) -> dict:
86
99
  )
87
100
  issues = run_all_mix_critics(mix_state)
88
101
  moves = plan_mix_moves(issues, mix_state)
102
+ sonic_snapshot = normalize_sonic_snapshot(data["spectrum"], source="mix_engine")
103
+ sonic_character = extract_character_profile(sonic_snapshot or {})
89
104
 
90
105
  return {
91
106
  "mix_state": mix_state.to_dict(),
107
+ "sonic_character": sonic_character,
92
108
  "issues": [i.to_dict() for i in issues],
93
109
  "suggested_moves": [m.to_dict() for m in moves],
94
110
  "issue_count": len(issues),
@@ -110,8 +126,10 @@ def get_mix_issues(ctx: Context) -> dict:
110
126
  rms_data=data["rms_data"],
111
127
  )
112
128
  issues = run_all_mix_critics(mix_state)
129
+ sonic_snapshot = normalize_sonic_snapshot(data["spectrum"], source="mix_engine")
113
130
 
114
131
  return {
132
+ "sonic_character": extract_character_profile(sonic_snapshot or {}),
115
133
  "issues": [i.to_dict() for i in issues],
116
134
  "issue_count": len(issues),
117
135
  }
@@ -133,8 +151,10 @@ def plan_mix_move(ctx: Context) -> dict:
133
151
  )
134
152
  issues = run_all_mix_critics(mix_state)
135
153
  moves = plan_mix_moves(issues, mix_state)
154
+ sonic_snapshot = normalize_sonic_snapshot(data["spectrum"], source="mix_engine")
136
155
 
137
156
  return {
157
+ "sonic_character": extract_character_profile(sonic_snapshot or {}),
138
158
  "moves": [m.to_dict() for m in moves],
139
159
  "move_count": len(moves),
140
160
  "issue_count": len(issues),
@@ -212,10 +232,12 @@ def get_mix_summary(ctx: Context) -> dict:
212
232
  rms_data=data["rms_data"],
213
233
  )
214
234
  issues = run_all_mix_critics(mix_state)
235
+ sonic_snapshot = normalize_sonic_snapshot(data["spectrum"], source="mix_engine")
215
236
 
216
237
  return {
217
238
  "track_count": len(mix_state.balance.track_states),
218
239
  "issue_count": len(issues),
240
+ "sonic_character": extract_character_profile(sonic_snapshot or {}),
219
241
  "dynamics": mix_state.dynamics.to_dict(),
220
242
  "stereo": mix_state.stereo.to_dict(),
221
243
  "depth": mix_state.depth.to_dict(),
@@ -9,6 +9,8 @@ from __future__ import annotations
9
9
  from fastmcp import Context
10
10
 
11
11
  from ..server import mcp
12
+ from ..evaluation.feature_extractors import extract_character_profile
13
+ from ..tools._snapshot_normalizer import normalize_sonic_snapshot
12
14
  from .models import (
13
15
  LayerStrategy,
14
16
  PatchBlock,
@@ -189,9 +191,36 @@ def _fetch_sound_design_data(ctx: Context, track_index: int) -> dict:
189
191
  # Get devices from track_info response (already included by Remote Script)
190
192
  devices: list[dict] = track_info.get("devices", [])
191
193
 
194
+ sonic = None
195
+ try:
196
+ spectral = ctx.lifespan_context.get("spectral")
197
+ if spectral and spectral.is_connected:
198
+ sonic = {}
199
+ spec_data = spectral.get("spectrum")
200
+ if spec_data:
201
+ sonic["bands"] = spec_data["value"]
202
+ rms_snap = spectral.get("rms")
203
+ if rms_snap:
204
+ sonic["rms"] = rms_snap["value"]
205
+ peak_snap = spectral.get("peak")
206
+ if peak_snap:
207
+ sonic["peak"] = peak_snap["value"]
208
+ key_snap = spectral.get("key")
209
+ if key_snap:
210
+ sonic["detected_key"] = key_snap["value"]
211
+ for key in ("spectral_shape", "mel_bands", "chroma", "onset", "novelty", "loudness"):
212
+ snap = spectral.get(key)
213
+ if snap:
214
+ sonic[key] = snap["value"]
215
+ if not sonic:
216
+ sonic = None
217
+ except Exception:
218
+ sonic = None
219
+
192
220
  return {
193
221
  "track_info": track_info,
194
222
  "devices": devices,
223
+ "sonic_snapshot": normalize_sonic_snapshot(sonic, source="sound_design"),
195
224
  }
196
225
 
197
226
 
@@ -262,9 +291,11 @@ def analyze_sound_design(ctx: Context, track_index: int) -> dict:
262
291
  issues, data["track_info"].get("name", "")
263
292
  )
264
293
  moves = plan_sound_design_moves(issues, state)
294
+ sonic_character = extract_character_profile(data.get("sonic_snapshot") or {})
265
295
 
266
296
  return {
267
297
  "state": state.to_dict(),
298
+ "sonic_character": sonic_character,
268
299
  "issues": [i.to_dict() for i in issues],
269
300
  "suggested_moves": [m.to_dict() for m in moves],
270
301
  "issue_count": len(issues),
@@ -295,6 +326,7 @@ def get_sound_design_issues(ctx: Context, track_index: int) -> dict:
295
326
  )
296
327
 
297
328
  return {
329
+ "sonic_character": extract_character_profile(data.get("sonic_snapshot") or {}),
298
330
  "issues": [i.to_dict() for i in issues],
299
331
  "issue_count": len(issues),
300
332
  }
@@ -330,6 +362,7 @@ def plan_sound_design_move(ctx: Context, track_index: int) -> dict:
330
362
  moves = plan_sound_design_moves(issues, state)
331
363
 
332
364
  result: dict = {
365
+ "sonic_character": extract_character_profile(data.get("sonic_snapshot") or {}),
333
366
  "moves": [m.to_dict() for m in moves],
334
367
  "move_count": len(moves),
335
368
  "issue_count": len(issues),
@@ -11,6 +11,10 @@ import re
11
11
  from dataclasses import asdict, dataclass, field
12
12
  from typing import Any, Optional
13
13
 
14
+ from ...evaluation.feature_extractors import (
15
+ extract_dimension_value as _shared_extract_dimension_value,
16
+ )
17
+ from .._snapshot_normalizer import normalize_sonic_snapshot
14
18
  from .models import QUALITY_DIMENSIONS, GoalVector, WorldModel, _clamp
15
19
  from .taste import compute_taste_fit
16
20
 
@@ -29,50 +33,10 @@ def _extract_dimension_value(
29
33
  """
30
34
  if not sonic:
31
35
  return None
32
- # Accept both "spectrum" and "bands" keys — get_master_spectrum returns
33
- # {"bands": {...}} while the evaluator historically expected {"spectrum": {...}}.
34
- # Finding 2 fix: tolerate either shape so raw analyzer output works.
35
- bands = sonic.get("spectrum") or sonic.get("bands")
36
- if not bands:
37
- return None
38
- rms = sonic.get("rms")
39
- peak = sonic.get("peak")
40
-
41
- if dimension == "brightness":
42
- high = bands.get("high", 0)
43
- presence = bands.get("presence", 0)
44
- return _clamp((high + presence) / 2.0)
45
- elif dimension == "warmth":
46
- return _clamp(bands.get("low_mid", 0))
47
- elif dimension == "weight":
48
- sub = bands.get("sub", 0)
49
- low = bands.get("low", 0)
50
- return _clamp((sub + low) / 2.0)
51
- elif dimension == "clarity":
52
- low_mid = bands.get("low_mid", 0)
53
- return _clamp(1.0 - low_mid)
54
- elif dimension == "density":
55
- # Spectral flatness: geometric mean / arithmetic mean of band values.
56
- # Higher = more evenly distributed energy (noise-like).
57
- # Lower = more tonal (energy concentrated in few bands).
58
- vals = [max(v, 1e-10) for v in bands.values() if isinstance(v, (int, float))]
59
- if not vals:
60
- return None
61
- geo_mean = math.exp(sum(math.log(v) for v in vals) / len(vals))
62
- arith_mean = sum(vals) / len(vals)
63
- return _clamp(geo_mean / max(arith_mean, 1e-10))
64
- elif dimension == "energy":
65
- return _clamp(rms) if rms is not None else None
66
- elif dimension == "punch":
67
- if rms and peak and rms > 0:
68
- crest_db = 20.0 * math.log10(max(peak / rms, 1.0))
69
- # Normalize: 0 dB = 0.0, 20 dB = 1.0
70
- return _clamp(crest_db / 20.0)
71
- return None
72
- else:
73
- # Unmeasurable in Phase 1 (width, depth, motion, contrast,
74
- # groove, tension, novelty, polish, emotion, cohesion)
36
+ normalized = normalize_sonic_snapshot(sonic, source="agent_os")
37
+ if normalized is None:
75
38
  return None
39
+ return _shared_extract_dimension_value(normalized, dimension)
76
40
 
77
41
  def compute_evaluation_score(
78
42
  goal: GoalVector,
@@ -203,4 +167,3 @@ def compute_evaluation_score(
203
167
  # I5: hint for the agent to track consecutive undos
204
168
  "consecutive_undo_hint": not keep_change,
205
169
  }
206
-
@@ -33,6 +33,8 @@ MEASURABLE_PROXIES: dict[str, str] = {
33
33
  "density": "spectral flatness (geometric/arithmetic mean ratio)",
34
34
  "energy": "RMS level",
35
35
  "punch": "crest factor in dB (20*log10(peak/rms))",
36
+ "motion": "spectral novelty + onset strength",
37
+ "novelty": "FluCoMa novelty score",
36
38
  }
37
39
 
38
40
  VALID_MODES = frozenset({"observe", "improve", "explore", "finish", "diagnose"})
@@ -129,4 +131,3 @@ class TechniqueCard:
129
131
  "verification": self.verification,
130
132
  "evidence": self.evidence,
131
133
  }
132
-
@@ -70,7 +70,7 @@ class ConductorPlan:
70
70
  _ROUTING_PATTERNS: list[tuple[str, str, str, str, list[str]]] = [
71
71
  # Mix requests
72
72
  (r"clean|mud|muddy|low.?mid|eq|equaliz", "mix_engine", "mix", "analyze_mix", ["plan_mix_move", "evaluate_mix_move"]),
73
- (r"punch|punchy|transient|dynamics|compress", "mix_engine", "mix", "analyze_mix", ["plan_mix_move"]),
73
+ (r"dynamics|compress|crest|over.?compress|flat.?dynamics", "mix_engine", "mix", "analyze_mix", ["plan_mix_move"]),
74
74
  (r"wide|wider|width|stereo|narrow|mono.?compat", "mix_engine", "mix", "analyze_mix", ["plan_mix_move"]),
75
75
  (r"glue|cohes|bus.?comp|mix.?bus", "mix_engine", "mix", "analyze_mix", ["plan_mix_move"]),
76
76
  (r"balance|level|volume.?balanc|gain.?stag", "mix_engine", "mix", "analyze_mix", ["plan_mix_move"]),
@@ -87,6 +87,7 @@ _ROUTING_PATTERNS: list[tuple[str, str, str, str, list[str]]] = [
87
87
 
88
88
  # Sound design requests
89
89
  (r"synth|patch|oscillat|timbre|timbral|wavetable|operator", "sound_design", "sound_design", "analyze_sound_design", ["plan_sound_design_move"]),
90
+ (r"punch|punchy|hit.?harder|snap|attack|transient", "sound_design", "sound_design", "analyze_sound_design", ["plan_sound_design_move"]),
90
91
  (r"haunted|lush|aggressive|warm.?pad|fat.?bass|bright.?lead", "sound_design", "sound_design", "analyze_sound_design", ["plan_sound_design_move"]),
91
92
  (r"modulation|lfo|movement|evolv|texture", "sound_design", "sound_design", "get_patch_model", ["analyze_sound_design"]),
92
93
  (r"layer|sub.?layer|transient.?layer|body", "sound_design", "sound_design", "analyze_sound_design", ["plan_sound_design_move"]),
@@ -254,7 +255,7 @@ def classify_request(request: str) -> ConductorPlan:
254
255
 
255
256
  # Determine capability requirements
256
257
  caps = ["session_access"]
257
- if any(r.engine == "mix_engine" for r in routes):
258
+ if any(r.engine in ("mix_engine", "sound_design") for r in routes):
258
259
  caps.append("analyzer")
259
260
  if any(r.engine in ("reference_engine",) for r in routes):
260
261
  caps.append("offline_perception")
@@ -267,6 +268,8 @@ def classify_request(request: str) -> ConductorPlan:
267
268
  notes.append("Multi-engine task — start with get_session_kernel for shared state")
268
269
  if any(r.engine == "mix_engine" for r in routes):
269
270
  notes.append("Mix engine works best with analyzer data — check get_capability_state")
271
+ if any(r.engine == "sound_design" for r in routes):
272
+ notes.append("Sound design should use analyzer character before level or pan changes")
270
273
 
271
274
  # V2: Search semantic moves for matching intents
272
275
  semantic_moves = _find_matching_semantic_moves(lower)
@@ -22,7 +22,7 @@ from typing import Optional
22
22
  # must report confidence=0.0 for that dimension.
23
23
  MEASURABLE_DIMENSIONS: frozenset[str] = frozenset({
24
24
  "brightness", "warmth", "weight", "clarity",
25
- "density", "energy", "punch",
25
+ "density", "energy", "punch", "motion", "novelty",
26
26
  })
27
27
 
28
28
  # All valid quality dimensions (measurable + unmeasurable).
@@ -13,6 +13,18 @@ import time
13
13
  from typing import Optional
14
14
 
15
15
 
16
+ _RICH_ANALYZER_KEYS = (
17
+ "spectral_shape",
18
+ "mel_bands",
19
+ "chroma",
20
+ "onset",
21
+ "onsets",
22
+ "novelty",
23
+ "loudness",
24
+ "sub_detail",
25
+ )
26
+
27
+
16
28
  def normalize_sonic_snapshot(
17
29
  raw: Optional[dict],
18
30
  source: str = "unknown",
@@ -20,6 +32,9 @@ def normalize_sonic_snapshot(
20
32
  """Normalize a raw analyzer/perception output into canonical snapshot form.
21
33
 
22
34
  Accepts both {"bands": {...}} and {"spectrum": {...}} shapes.
35
+ Rich analyzer streams are preserved when present so evaluators can
36
+ reason from FluCoMa character descriptors instead of collapsing every
37
+ decision down to the 9-band spectrum.
23
38
  Returns None if input is empty or None.
24
39
 
25
40
  Canonical form:
@@ -28,6 +43,12 @@ def normalize_sonic_snapshot(
28
43
  "rms": float or None,
29
44
  "peak": float or None,
30
45
  "detected_key": str or None,
46
+ "spectral_shape": dict or None,
47
+ "mel_bands": list or None,
48
+ "chroma": dict/list or None,
49
+ "onset": dict or None,
50
+ "novelty": dict or None,
51
+ "loudness": dict or None,
31
52
  "source": str,
32
53
  "normalized_at_ms": int,
33
54
  }
@@ -35,11 +56,12 @@ def normalize_sonic_snapshot(
35
56
  if not raw or not isinstance(raw, dict):
36
57
  return None
37
58
 
38
- bands = raw.get("spectrum") or raw.get("bands")
39
- if not bands:
59
+ bands = raw.get("spectrum") or raw.get("bands") or {}
60
+ has_rich_analyzer_data = any(raw.get(k) is not None for k in _RICH_ANALYZER_KEYS)
61
+ if not bands and not has_rich_analyzer_data:
40
62
  return None
41
63
 
42
- return {
64
+ normalized = {
43
65
  "spectrum": bands,
44
66
  "rms": raw.get("rms"),
45
67
  "peak": raw.get("peak"),
@@ -47,3 +69,10 @@ def normalize_sonic_snapshot(
47
69
  "source": source,
48
70
  "normalized_at_ms": int(time.time() * 1000),
49
71
  }
72
+
73
+ for key in _RICH_ANALYZER_KEYS:
74
+ if key in raw and raw.get(key) is not None:
75
+ out_key = "onset" if key == "onsets" else key
76
+ normalized[out_key] = raw.get(key)
77
+
78
+ return normalized
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.26.1",
3
+ "version": "1.26.2",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
5
  "description": "Agentic production system for Ableton Live 12 — 465 tools, 56 domains, 44 semantic moves. Device atlas (5264 devices, 120 enriched, 7 indexes), Splice intelligence (gRPC + GraphQL describe-a-sound + preview + collections + presets), 9-band spectral perception auto-loaded via ensure_analyzer_on_master, Creative Director skill, technique memory, 12 creative intelligence engines",
6
6
  "author": "Pilot Studio",
@@ -5,7 +5,7 @@ Entry point for the ControlSurface. Ableton calls create_instance(c_instance)
5
5
  when this script is selected in Preferences > Link, Tempo & MIDI.
6
6
  """
7
7
 
8
- __version__ = "1.26.1"
8
+ __version__ = "1.26.2"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
11
  from . import router
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/dreamrec/LivePilot",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.26.1",
9
+ "version": "1.26.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "livepilot",
14
- "version": "1.26.1",
14
+ "version": "1.26.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }