livepilot 1.26.0 → 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.
- package/CHANGELOG.md +41 -0
- package/README.md +1 -1
- package/installer/codex.js +87 -9
- package/livepilot/.Codex-plugin/plugin.json +8 -0
- package/livepilot/.claude-plugin/plugin.json +8 -0
- package/livepilot/.mcp.json +8 -0
- package/livepilot/agents/livepilot-producer/AGENT.md +314 -0
- package/livepilot/commands/arrange.md +47 -0
- package/livepilot/commands/beat.md +81 -0
- package/livepilot/commands/evaluate.md +51 -0
- package/livepilot/commands/memory.md +22 -0
- package/livepilot/commands/mix.md +47 -0
- package/livepilot/commands/perform.md +42 -0
- package/livepilot/commands/session.md +13 -0
- package/livepilot/commands/sounddesign.md +58 -0
- package/livepilot/rubrics/default_preset_check.md +82 -0
- package/livepilot/rubrics/layer_accumulation.md +79 -0
- package/livepilot/rubrics/layer_precision.md +79 -0
- package/livepilot/rubrics/modulation_presence.md +63 -0
- package/livepilot/rubrics/sound_design_depth.md +40 -0
- package/livepilot/skills/livepilot-arrangement/SKILL.md +164 -0
- package/livepilot/skills/livepilot-composition-engine/SKILL.md +151 -0
- package/livepilot/skills/livepilot-composition-engine/references/form-patterns.md +97 -0
- package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +102 -0
- package/livepilot/skills/livepilot-core/SKILL.md +261 -0
- package/livepilot/skills/livepilot-core/references/ableton-workflow-patterns.md +831 -0
- package/livepilot/skills/livepilot-core/references/affordances/_schema.md +160 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/auto-filter.yaml +133 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/chorus-ensemble.yaml +91 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/compressor.yaml +98 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/convolution-reverb.yaml +113 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/corpus.yaml +84 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/drift.yaml +105 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/echo.yaml +108 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/eq-eight.yaml +95 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/glue-compressor.yaml +88 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/granulator-iii.yaml +104 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/hybrid-reverb.yaml +83 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/operator.yaml +98 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/ping-pong-delay.yaml +104 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/poli.yaml +98 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/saturator.yaml +98 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/shifter.yaml +77 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/simpler.yaml +113 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/utility.yaml +95 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/vinyl-distortion.yaml +92 -0
- package/livepilot/skills/livepilot-core/references/affordances/devices/wavetable.yaml +98 -0
- package/livepilot/skills/livepilot-core/references/artist-vocabularies.md +389 -0
- package/livepilot/skills/livepilot-core/references/automation-atlas.md +272 -0
- package/livepilot/skills/livepilot-core/references/concepts/_schema.md +158 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/akufen.yaml +116 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/aphex-twin.yaml +133 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/arca-sophie.yaml +131 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/autechre.yaml +130 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/basic-channel.yaml +140 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/basinski.yaml +126 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/boards-of-canada.yaml +124 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/burial.yaml +127 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/com-truise-tycho.yaml +121 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/daft-punk.yaml +117 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/dj-premier-rza.yaml +119 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/gas.yaml +134 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/hawtin.yaml +127 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/isolee-luomo.yaml +130 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/j-dilla.yaml +133 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/jeff-mills.yaml +120 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/johannsson-richter.yaml +132 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/madlib.yaml +124 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/moodymann-theo-parrish.yaml +121 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/oneohtrix-point-never.yaml +126 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/photek-source-direct.yaml +120 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/rashad-spinn-traxman.yaml +122 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/robert-henke.yaml +113 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/shackleton.yaml +124 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/skream-mala.yaml +119 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/stars-of-the-lid.yaml +119 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/tim-hecker.yaml +122 -0
- package/livepilot/skills/livepilot-core/references/concepts/artists/villalobos.yaml +135 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/ambient.yaml +137 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/boom_bap.yaml +124 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/deep-minimal.yaml +130 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/deep_house.yaml +130 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/detroit_techno.yaml +116 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/disco.yaml +123 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/downtempo.yaml +129 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/drone.yaml +133 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/drum-and-bass.yaml +119 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/dub-techno.yaml +132 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/dub.yaml +129 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/dubstep.yaml +120 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/experimental.yaml +136 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/footwork.yaml +119 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/hip-hop.yaml +132 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/house.yaml +126 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/hyperpop.yaml +128 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/idm.yaml +134 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/lo_fi.yaml +129 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/microhouse.yaml +138 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/minimal-techno.yaml +116 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/modern-classical.yaml +123 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/soul.yaml +125 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/synthwave.yaml +123 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/techno.yaml +123 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/trap.yaml +120 -0
- package/livepilot/skills/livepilot-core/references/concepts/genres/uk-garage.yaml +121 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/00-index.md +110 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +687 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +753 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +525 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +402 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +963 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +874 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +571 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +714 -0
- package/livepilot/skills/livepilot-core/references/device-atlas/synths-native.md +953 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
- package/livepilot/skills/livepilot-core/references/genre-vocabularies.md +382 -0
- package/livepilot/skills/livepilot-core/references/m4l-devices.md +352 -0
- package/livepilot/skills/livepilot-core/references/memory-guide.md +178 -0
- package/livepilot/skills/livepilot-core/references/midi-recipes.md +402 -0
- package/livepilot/skills/livepilot-core/references/mixing-patterns.md +578 -0
- package/livepilot/skills/livepilot-core/references/overview.md +300 -0
- package/livepilot/skills/livepilot-core/references/pack-knowledge.md +319 -0
- package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
- package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
- package/livepilot/skills/livepilot-core/references/sound-design.md +393 -0
- package/livepilot/skills/livepilot-corpus-builder/SKILL.md +379 -0
- package/livepilot/skills/livepilot-creative-director/SKILL.md +462 -0
- package/livepilot/skills/livepilot-creative-director/references/anti-repetition-rules.md +214 -0
- package/livepilot/skills/livepilot-creative-director/references/creative-brief-template.md +222 -0
- package/livepilot/skills/livepilot-creative-director/references/hybrid-compilation.md +185 -0
- package/livepilot/skills/livepilot-creative-director/references/move-family-diversity-rule.md +258 -0
- package/livepilot/skills/livepilot-creative-director/references/phase-6-execution.md +409 -0
- package/livepilot/skills/livepilot-creative-director/references/the-four-move-rule.md +192 -0
- package/livepilot/skills/livepilot-devices/SKILL.md +213 -0
- package/livepilot/skills/livepilot-devices/references/load_browser_item-uri-grammar.md +82 -0
- package/livepilot/skills/livepilot-evaluation/SKILL.md +195 -0
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +176 -0
- package/livepilot/skills/livepilot-evaluation/references/evaluation-contracts.md +121 -0
- package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +110 -0
- package/livepilot/skills/livepilot-mix-engine/SKILL.md +144 -0
- package/livepilot/skills/livepilot-mix-engine/references/mix-critics.md +143 -0
- package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +105 -0
- package/livepilot/skills/livepilot-mixing/SKILL.md +164 -0
- package/livepilot/skills/livepilot-notes/SKILL.md +130 -0
- package/livepilot/skills/livepilot-performance-engine/SKILL.md +122 -0
- package/livepilot/skills/livepilot-performance-engine/references/performance-safety.md +98 -0
- package/livepilot/skills/livepilot-release/SKILL.md +151 -0
- package/livepilot/skills/livepilot-sample-engine/SKILL.md +117 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +247 -0
- package/livepilot/skills/livepilot-sound-design-engine/references/patch-model.md +119 -0
- package/livepilot/skills/livepilot-sound-design-engine/references/sound-design-critics.md +118 -0
- package/livepilot/skills/livepilot-wonder/SKILL.md +143 -0
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/LivePilot_Elektron.amxd +0 -0
- package/m4l_device/LivePilot_Elektron.maxpat +758 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/m4l_device/livepilot_elektron_bridge.js +82 -0
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/composer/develop/apply.py +1 -1
- package/mcp_server/composer/full/apply.py +32 -6
- package/mcp_server/composer/full/brief_builder.py +9 -0
- package/mcp_server/evaluation/feature_extractors.py +152 -8
- package/mcp_server/m4l_bridge.py +5 -0
- package/mcp_server/mix_engine/state_builder.py +19 -2
- package/mcp_server/mix_engine/tools.py +22 -0
- package/mcp_server/runtime/execution_router.py +6 -0
- package/mcp_server/runtime/mcp_dispatch.py +18 -0
- package/mcp_server/runtime/remote_commands.py +2 -0
- package/mcp_server/server.py +11 -7
- package/mcp_server/sound_design/tools.py +33 -0
- package/mcp_server/tools/_agent_os_engine/evaluation.py +7 -44
- package/mcp_server/tools/_agent_os_engine/models.py +2 -1
- package/mcp_server/tools/_conductor.py +5 -2
- package/mcp_server/tools/_evaluation_contracts.py +1 -1
- package/mcp_server/tools/_snapshot_normalizer.py +32 -3
- package/package.json +20 -5
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/server.py +63 -2
- package/requirements.txt +3 -3
- package/server.json +3 -3
|
@@ -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.
|
|
37
|
+
var VERSION = "1.26.2";
|
|
38
38
|
|
|
39
39
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
40
40
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// m4l_device/livepilot_elektron_bridge.js
|
|
2
|
+
// LivePilot Elektron Bridge — runs inside LivePilot_Elektron.amxd.
|
|
3
|
+
//
|
|
4
|
+
// Wire model:
|
|
5
|
+
//
|
|
6
|
+
// M4L → Python : UDP 9882
|
|
7
|
+
// Inbound SysEx from physical MIDI [sysexin → sxformat
|
|
8
|
+
// → prepend sysex] arrives at JS as a "sysex <bytes...>"
|
|
9
|
+
// message; forwards out outlet 1 (→ [udpsend 9882]).
|
|
10
|
+
//
|
|
11
|
+
// Python → M4L : UDP 9883
|
|
12
|
+
// [udpreceive 9883] outputs bytes as a "list" message
|
|
13
|
+
// → JS function list() pushes onto sendQueue.
|
|
14
|
+
// [metro 50 @active 1] drives bang() which drains one
|
|
15
|
+
// chunk per tick onto outlet 0 (→ [midiout]).
|
|
16
|
+
//
|
|
17
|
+
// Outlets:
|
|
18
|
+
// 0 → [midiout] paced chunks to physical MIDI port
|
|
19
|
+
// 1 → [udpsend 127.0.0.1 9882] sysex-from-device upstream to Python
|
|
20
|
+
// 2 → "set <text>" → live.text status display
|
|
21
|
+
// 3 → +1 counter → live.numbox RX message counter
|
|
22
|
+
//
|
|
23
|
+
// Phase 1 design note: heartbeat is intentionally OUT for v1 of the M4L
|
|
24
|
+
// device. The Python bridge uses per-operation timeouts (per design
|
|
25
|
+
// spec §7.3) so it does not need M4L liveness polling. Heartbeat / pong
|
|
26
|
+
// can be added in a Phase 2 patch to the .maxpat if "is M4L loaded?"
|
|
27
|
+
// detection becomes useful — the ping()/pong() functions below are
|
|
28
|
+
// kept as reserved stubs for that future wiring.
|
|
29
|
+
|
|
30
|
+
inlets = 1;
|
|
31
|
+
outlets = 4;
|
|
32
|
+
|
|
33
|
+
var sendQueue = [];
|
|
34
|
+
var rxCount = 0;
|
|
35
|
+
|
|
36
|
+
// Called by [metro 50 @active 1] — drain one queued chunk per tick.
|
|
37
|
+
function bang() {
|
|
38
|
+
if (sendQueue.length > 0) {
|
|
39
|
+
var chunk = sendQueue.shift();
|
|
40
|
+
// Spread chunk into outlet args so Max sees it as a list message
|
|
41
|
+
outlet.apply(this, [0].concat(chunk));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Called by [udpreceive 9883] — outputs raw UDP bytes as a list.
|
|
46
|
+
// This is the Python → M4L outbound MIDI path. Each datagram is a
|
|
47
|
+
// pre-chunked SysEx fragment; we queue and let bang() pace them.
|
|
48
|
+
function list() {
|
|
49
|
+
var bytes = [];
|
|
50
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
51
|
+
bytes.push(arguments[i]);
|
|
52
|
+
}
|
|
53
|
+
sendQueue.push(bytes);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Called when [sysexin → sxformat → prepend sysex] delivers a complete
|
|
57
|
+
// SysEx as a "sysex <bytes...>" message. We forward to Python and
|
|
58
|
+
// bump the RX counter + status.
|
|
59
|
+
function sysex() {
|
|
60
|
+
var bytes = [];
|
|
61
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
62
|
+
bytes.push(arguments[i]);
|
|
63
|
+
}
|
|
64
|
+
// Forward to Python via outlet 1 → [udpsend 9882]
|
|
65
|
+
outlet.apply(this, [1].concat(bytes));
|
|
66
|
+
// Bump RX counter via outlet 3
|
|
67
|
+
rxCount += 1;
|
|
68
|
+
outlet(3, rxCount);
|
|
69
|
+
// Update status via outlet 2
|
|
70
|
+
outlet(2, "set", "Online — receiving SysEx");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Phase 2 reserved: heartbeat ping. Wire [metro 1000] → [t ping] → js
|
|
74
|
+
// to enable. Not wired in Phase 1 .maxpat.
|
|
75
|
+
function ping() {
|
|
76
|
+
outlet(1, 0xF0, 0x7F, 0x7F, 0x00, 0xF7);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Phase 2 reserved: receive pong from Python. Currently unused.
|
|
80
|
+
function pong() {
|
|
81
|
+
outlet(2, "set", "Online — pong received");
|
|
82
|
+
}
|
package/mcp_server/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
|
|
2
|
-
__version__ = "1.26.
|
|
2
|
+
__version__ = "1.26.2"
|
|
@@ -64,7 +64,7 @@ async def _bridge_ping_stub(ctx: Any) -> dict:
|
|
|
64
64
|
bridge = ctx.lifespan_context.get("m4l_bridge")
|
|
65
65
|
if bridge is None:
|
|
66
66
|
raise RuntimeError("bridge not available")
|
|
67
|
-
return await bridge.send_command("ping",
|
|
67
|
+
return await bridge.send_command("ping", timeout=0.5)
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
async def _back_to_arranger(ctx: Any) -> dict:
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
import re as _re
|
|
7
|
+
import inspect
|
|
7
8
|
import time
|
|
8
9
|
|
|
9
10
|
from fastmcp import Context
|
|
@@ -92,6 +93,28 @@ _TONAL_ROLES_ALWAYS_WARP: frozenset[str] = frozenset({
|
|
|
92
93
|
})
|
|
93
94
|
|
|
94
95
|
|
|
96
|
+
async def _call_mcp_analysis_tool(ctx: Context, tool: str, params: dict) -> dict:
|
|
97
|
+
"""Dispatch analyzer/intelligence tools through the MCP registry.
|
|
98
|
+
|
|
99
|
+
These tools are Python MCP tools, not Remote Script TCP handlers. Keeping
|
|
100
|
+
full-mode analysis on the registry path prevents creative plans from
|
|
101
|
+
passing tests against mocks and then failing in Live as unknown commands.
|
|
102
|
+
"""
|
|
103
|
+
lifespan = getattr(ctx, "lifespan_context", {}) or {}
|
|
104
|
+
registry = lifespan.get("mcp_dispatch")
|
|
105
|
+
if registry is None:
|
|
106
|
+
from ...runtime.mcp_dispatch import build_mcp_dispatch_registry
|
|
107
|
+
registry = build_mcp_dispatch_registry()
|
|
108
|
+
|
|
109
|
+
fn = registry.get(tool) if registry else None
|
|
110
|
+
if fn is None:
|
|
111
|
+
return {"error": f"MCP analysis tool '{tool}' is not registered"}
|
|
112
|
+
|
|
113
|
+
call = fn(params, ctx=ctx)
|
|
114
|
+
result = await call if inspect.isawaitable(call) else call
|
|
115
|
+
return result if isinstance(result, dict) else {"result": result}
|
|
116
|
+
|
|
117
|
+
|
|
95
118
|
def _decide_warp_loops(
|
|
96
119
|
role: str,
|
|
97
120
|
file_path: str,
|
|
@@ -825,8 +848,9 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
825
848
|
# Goal: give the agent acoustic characteristics of the loaded sound so
|
|
826
849
|
# it can reason about fit. ONLY static analysis here (no playback);
|
|
827
850
|
# active solo-trigger analysis is Scope B / v1.25.
|
|
828
|
-
# Analysis is
|
|
829
|
-
#
|
|
851
|
+
# Analysis is MCP-side intelligence, not a Remote Script TCP command.
|
|
852
|
+
# Route through the MCP dispatch registry so live execution matches
|
|
853
|
+
# the same boundary the async plan router uses.
|
|
830
854
|
role = track_spec.get("role", "")
|
|
831
855
|
instrument_uri = (track_spec.get("instrument") or {}).get("uri", "")
|
|
832
856
|
layer_analysis: dict = {"status": "skipped", "reason": "no analyzer applicable"}
|
|
@@ -834,7 +858,8 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
834
858
|
if instrument_uri.startswith(("query:Synths#", "query:Sounds#")):
|
|
835
859
|
# Synth / preset — analyze the patch
|
|
836
860
|
try:
|
|
837
|
-
patch_result =
|
|
861
|
+
patch_result = await _call_mcp_analysis_tool(
|
|
862
|
+
ctx,
|
|
838
863
|
"analyze_synth_patch",
|
|
839
864
|
{"track_index": track_index, "device_index": 0},
|
|
840
865
|
)
|
|
@@ -854,7 +879,8 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
854
879
|
any(instrument_uri.lower().endswith(ext) for ext in (".aif", ".wav", ".mp3", ".flac")):
|
|
855
880
|
# Sample-based — analyze via track reference (no file_path needed)
|
|
856
881
|
try:
|
|
857
|
-
sample_result =
|
|
882
|
+
sample_result = await _call_mcp_analysis_tool(
|
|
883
|
+
ctx,
|
|
858
884
|
"analyze_sample",
|
|
859
885
|
{"track_index": track_index, "clip_index": 0},
|
|
860
886
|
)
|
|
@@ -1034,10 +1060,10 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
1034
1060
|
# session state.
|
|
1035
1061
|
mix_analysis: dict = {"status": "skipped", "reason": "not run"}
|
|
1036
1062
|
try:
|
|
1037
|
-
mix_result =
|
|
1063
|
+
mix_result = await _call_mcp_analysis_tool(ctx, "analyze_mix", {})
|
|
1038
1064
|
if isinstance(mix_result, dict) and not mix_result.get("error"):
|
|
1039
1065
|
try:
|
|
1040
|
-
masking_result =
|
|
1066
|
+
masking_result = await _call_mcp_analysis_tool(ctx, "get_masking_report", {})
|
|
1041
1067
|
except Exception as mask_exc:
|
|
1042
1068
|
masking_result = {"error": str(mask_exc)}
|
|
1043
1069
|
mix_analysis = {
|
|
@@ -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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
+
}
|
package/mcp_server/m4l_bridge.py
CHANGED
|
@@ -1014,6 +1014,11 @@ class M4LBridge:
|
|
|
1014
1014
|
while len(s_bytes) % 4 != 0:
|
|
1015
1015
|
s_bytes += b'\x00'
|
|
1016
1016
|
arg_data += s_bytes
|
|
1017
|
+
else:
|
|
1018
|
+
raise TypeError(
|
|
1019
|
+
"OSC argument for %s must be int, float, or str, got %s"
|
|
1020
|
+
% (address, type(arg).__name__)
|
|
1021
|
+
)
|
|
1017
1022
|
|
|
1018
1023
|
tag_bytes = type_tags.encode('ascii') + b'\x00'
|
|
1019
1024
|
while len(tag_bytes) % 4 != 0:
|
|
@@ -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
|
-
|
|
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(),
|
|
@@ -36,7 +36,10 @@ from .remote_commands import BRIDGE_COMMANDS, REMOTE_COMMANDS
|
|
|
36
36
|
MCP_TOOLS: frozenset[str] = frozenset({
|
|
37
37
|
"apply_automation_shape",
|
|
38
38
|
"apply_gesture_template",
|
|
39
|
+
"analyze_sample",
|
|
40
|
+
"analyze_synth_patch",
|
|
39
41
|
"analyze_mix",
|
|
42
|
+
"get_masking_report",
|
|
40
43
|
"get_master_spectrum",
|
|
41
44
|
"get_emotional_arc",
|
|
42
45
|
"get_motif_graph",
|
|
@@ -92,7 +95,10 @@ READ_ONLY_TOOLS: frozenset[str] = frozenset({
|
|
|
92
95
|
"get_cue_points",
|
|
93
96
|
"get_rack_chains",
|
|
94
97
|
"get_clip_automation",
|
|
98
|
+
"analyze_sample",
|
|
99
|
+
"analyze_synth_patch",
|
|
95
100
|
"analyze_mix",
|
|
101
|
+
"get_masking_report",
|
|
96
102
|
"get_emotional_arc",
|
|
97
103
|
"get_motif_graph",
|
|
98
104
|
"get_session_diagnostics",
|
|
@@ -60,11 +60,26 @@ async def _apply_gesture_template(params: dict, ctx: Any = None) -> dict:
|
|
|
60
60
|
return await _call(apply_gesture_template, ctx, params)
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
async def _analyze_sample(params: dict, ctx: Any = None) -> dict:
|
|
64
|
+
from ..sample_engine.tools import analyze_sample
|
|
65
|
+
return await _call(analyze_sample, ctx, params)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def _analyze_synth_patch(params: dict, ctx: Any = None) -> dict:
|
|
69
|
+
from ..synthesis_brain.tools import analyze_synth_patch
|
|
70
|
+
return await _call(analyze_synth_patch, ctx, params)
|
|
71
|
+
|
|
72
|
+
|
|
63
73
|
async def _analyze_mix(params: dict, ctx: Any = None) -> dict:
|
|
64
74
|
from ..mix_engine.tools import analyze_mix
|
|
65
75
|
return await _call(analyze_mix, ctx, params)
|
|
66
76
|
|
|
67
77
|
|
|
78
|
+
async def _get_masking_report(params: dict, ctx: Any = None) -> dict:
|
|
79
|
+
from ..mix_engine.tools import get_masking_report
|
|
80
|
+
return await _call(get_masking_report, ctx, params)
|
|
81
|
+
|
|
82
|
+
|
|
68
83
|
async def _get_master_spectrum(params: dict, ctx: Any = None) -> dict:
|
|
69
84
|
from ..tools.analyzer import get_master_spectrum
|
|
70
85
|
return await _call(get_master_spectrum, ctx, params)
|
|
@@ -151,7 +166,10 @@ def build_mcp_dispatch_registry() -> dict[str, Callable]:
|
|
|
151
166
|
"load_sample_to_simpler": _load_sample_to_simpler,
|
|
152
167
|
"apply_automation_shape": _apply_automation_shape,
|
|
153
168
|
"apply_gesture_template": _apply_gesture_template,
|
|
169
|
+
"analyze_sample": _analyze_sample,
|
|
170
|
+
"analyze_synth_patch": _analyze_synth_patch,
|
|
154
171
|
"analyze_mix": _analyze_mix,
|
|
172
|
+
"get_masking_report": _get_masking_report,
|
|
155
173
|
"get_master_spectrum": _get_master_spectrum,
|
|
156
174
|
"get_emotional_arc": _get_emotional_arc,
|
|
157
175
|
"get_motif_graph": _get_motif_graph,
|
|
@@ -83,6 +83,8 @@ REMOTE_COMMANDS: frozenset[str] = frozenset({
|
|
|
83
83
|
"capture_midi", "start_recording", "stop_recording",
|
|
84
84
|
"get_cue_points", "jump_to_cue", "toggle_cue_point",
|
|
85
85
|
"back_to_arranger", "force_arrangement",
|
|
86
|
+
"arrangement_automation_via_session_record_start",
|
|
87
|
+
"arrangement_automation_via_session_record_complete",
|
|
86
88
|
# scales — Song + per-clip scale awareness (Live 12.0+)
|
|
87
89
|
"get_song_scale", "set_song_scale", "set_song_scale_mode",
|
|
88
90
|
"list_available_scales",
|
package/mcp_server/server.py
CHANGED
|
@@ -355,14 +355,14 @@ def _coerce_schema_property(prop: dict) -> None:
|
|
|
355
355
|
def _get_all_tools():
|
|
356
356
|
"""Get all registered tools — defends against FastMCP internal drift.
|
|
357
357
|
|
|
358
|
-
FastMCP's public API doesn't expose the registry as of 3.
|
|
358
|
+
FastMCP's public API doesn't expose the registry as of 3.3.x (see
|
|
359
359
|
docs/FASTMCP_UPSTREAM_FR.md). Until it does, we probe known internal
|
|
360
360
|
attribute paths. Each probe fires in try/except so a structural
|
|
361
|
-
rearrangement (e.g. ``_components`` renamed under 3.
|
|
361
|
+
rearrangement (e.g. ``_components`` renamed under 3.4+) falls through
|
|
362
362
|
to the next path rather than exploding.
|
|
363
363
|
|
|
364
364
|
WARNING: Accesses FastMCP private internals. Pinned to
|
|
365
|
-
fastmcp>=3.
|
|
365
|
+
fastmcp>=3.3.1,<3.4.0 in requirements.txt. The startup self-test
|
|
366
366
|
(_assert_tool_registry_accessible) will fail loudly if every probe
|
|
367
367
|
returns empty — better than silently returning [] and disabling
|
|
368
368
|
schema coercion.
|
|
@@ -370,14 +370,18 @@ def _get_all_tools():
|
|
|
370
370
|
probes = [
|
|
371
371
|
# FastMCP 0.x: mcp._tool_manager._tools (dict of name -> Tool)
|
|
372
372
|
("_tool_manager._tools", lambda: list(mcp._tool_manager._tools.values())),
|
|
373
|
-
# FastMCP 3.0–3.
|
|
373
|
+
# FastMCP 3.0–3.3: mcp._local_provider._components
|
|
374
|
+
# (verified 2026-05-21 against fastmcp 3.3.1 — still the active path)
|
|
374
375
|
(
|
|
375
376
|
"_local_provider._components",
|
|
376
377
|
lambda: list(mcp._local_provider._components.values()),
|
|
377
378
|
),
|
|
378
|
-
# FastMCP 3.
|
|
379
|
-
# rename based on naming conventions in other providers).
|
|
380
|
-
#
|
|
379
|
+
# FastMCP 3.4+ speculative: mcp._local_provider._tools (anticipated
|
|
380
|
+
# rename based on naming conventions in other providers). Verified
|
|
381
|
+
# 2026-05-21 against fastmcp 3.3.1 — the rename did NOT happen in
|
|
382
|
+
# 3.3.x; ``_local_provider._components`` remains the live registry.
|
|
383
|
+
# Kept here so a future bump that DOES rename surfaces a partial
|
|
384
|
+
# match rather than a full miss.
|
|
381
385
|
(
|
|
382
386
|
"_local_provider._tools",
|
|
383
387
|
lambda: list(mcp._local_provider._tools.values()),
|