livepilot 1.10.5 → 1.10.6
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/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +50 -0
- package/livepilot/.Codex-plugin/plugin.json +1 -1
- package/livepilot/.claude-plugin/plugin.json +1 -1
- package/livepilot/skills/livepilot-core/references/overview.md +2 -2
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +12 -1
- package/manifest.json +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/composer/sample_resolver.py +10 -6
- package/mcp_server/composer/tools.py +10 -6
- package/mcp_server/connection.py +6 -1
- package/mcp_server/creative_constraints/tools.py +9 -8
- package/mcp_server/experiment/engine.py +9 -5
- package/mcp_server/experiment/tools.py +9 -9
- package/mcp_server/hook_hunter/tools.py +14 -9
- package/mcp_server/m4l_bridge.py +11 -0
- package/mcp_server/memory/taste_graph.py +7 -2
- package/mcp_server/mix_engine/tools.py +8 -3
- package/mcp_server/musical_intelligence/tools.py +15 -10
- package/mcp_server/performance_engine/tools.py +6 -2
- package/mcp_server/preview_studio/tools.py +21 -15
- package/mcp_server/project_brain/tools.py +18 -10
- package/mcp_server/reference_engine/tools.py +7 -5
- package/mcp_server/runtime/capability_probe.py +10 -4
- package/mcp_server/runtime/tools.py +8 -2
- package/mcp_server/sample_engine/tools.py +27 -18
- package/mcp_server/semantic_moves/tools.py +5 -1
- package/mcp_server/server.py +10 -9
- package/mcp_server/services/motif_service.py +9 -3
- package/mcp_server/session_continuity/tools.py +7 -3
- package/mcp_server/session_continuity/tracker.py +9 -8
- package/mcp_server/song_brain/tools.py +17 -12
- package/mcp_server/splice_client/client.py +19 -6
- package/mcp_server/stuckness_detector/tools.py +8 -5
- package/mcp_server/tools/_agent_os_engine/__init__.py +52 -0
- package/mcp_server/tools/_agent_os_engine/critics.py +134 -0
- package/mcp_server/tools/_agent_os_engine/evaluation.py +206 -0
- package/mcp_server/tools/_agent_os_engine/models.py +132 -0
- package/mcp_server/tools/_agent_os_engine/taste.py +192 -0
- package/mcp_server/tools/_agent_os_engine/techniques.py +161 -0
- package/mcp_server/tools/_agent_os_engine/world_model.py +170 -0
- package/mcp_server/tools/_composition_engine/__init__.py +67 -0
- package/mcp_server/tools/_composition_engine/analysis.py +174 -0
- package/mcp_server/tools/_composition_engine/critics.py +522 -0
- package/mcp_server/tools/_composition_engine/gestures.py +230 -0
- package/mcp_server/tools/_composition_engine/harmony.py +70 -0
- package/mcp_server/tools/_composition_engine/models.py +193 -0
- package/mcp_server/tools/_composition_engine/sections.py +371 -0
- package/mcp_server/tools/_perception_engine.py +18 -11
- package/mcp_server/tools/agent_os.py +23 -15
- package/mcp_server/tools/analyzer.py +7 -8
- package/mcp_server/tools/automation.py +6 -1
- package/mcp_server/tools/composition.py +25 -16
- package/mcp_server/tools/devices.py +10 -6
- package/mcp_server/tools/motif.py +7 -2
- package/mcp_server/tools/planner.py +6 -2
- package/mcp_server/tools/research.py +13 -10
- package/mcp_server/transition_engine/tools.py +6 -1
- package/mcp_server/translation_engine/tools.py +8 -6
- package/mcp_server/wonder_mode/engine.py +8 -3
- package/mcp_server/wonder_mode/tools.py +29 -21
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +1 -1
- package/mcp_server/tools/_agent_os_engine.py +0 -947
- package/mcp_server/tools/_composition_engine.py +0 -1530
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{
|
|
11
11
|
"name": "livepilot",
|
|
12
12
|
"description": "Agentic production system for Ableton Live 12 — 320 tools, 43 domains, device atlas, spectral perception, technique memory, sample intelligence, auto-composition, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
13
|
-
"version": "1.10.
|
|
13
|
+
"version": "1.10.6",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Pilot Studio"
|
|
16
16
|
},
|
package/AGENTS.md
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.10.6 — Debuggability + Engine Modularization (April 17 2026)
|
|
4
|
+
|
|
5
|
+
Defensive-programming release. Zero behavior change for users; substantial
|
|
6
|
+
quality-of-life gains for developers and future debugging sessions.
|
|
7
|
+
|
|
8
|
+
### Debuggability
|
|
9
|
+
|
|
10
|
+
- **Silent-exception sweep.** All 79 `except Exception: pass` sites across
|
|
11
|
+
`mcp_server/` now emit a `logger.debug("<func> failed: %s", exc)` breadcrumb
|
|
12
|
+
while preserving the original body (pass / return X / continue). Previously
|
|
13
|
+
invisible failures now leave a trail. Run with `LOG_LEVEL=DEBUG` to surface.
|
|
14
|
+
- **Credit-floor guard hardened.** `SpliceGRPCClient.download_sample()` now
|
|
15
|
+
enforces `CREDIT_HARD_FLOOR` defensively via `can_afford(1, budget=1)` before
|
|
16
|
+
the gRPC call. Tool-layer callers still gate upstream for UX; this closes
|
|
17
|
+
the hole if any future caller forgets. The docstring claimed this guard
|
|
18
|
+
existed — now the code matches.
|
|
19
|
+
|
|
20
|
+
### Engine modularization
|
|
21
|
+
|
|
22
|
+
Two single-file engines (2,477 LOC combined) split into packages while keeping
|
|
23
|
+
the public surface identical. Callers that did `from . import X as engine` or
|
|
24
|
+
`from .X import Symbol` continue to work unchanged.
|
|
25
|
+
|
|
26
|
+
- **`mcp_server/tools/_composition_engine/`** — 6 sub-modules (models, sections,
|
|
27
|
+
critics, gestures, harmony, analysis) + facade. Was 1,530 LOC in one file;
|
|
28
|
+
now no sub-module exceeds 522 LOC.
|
|
29
|
+
- **`mcp_server/tools/_agent_os_engine/`** — 6 sub-modules (models, world_model,
|
|
30
|
+
critics, evaluation, techniques, taste) + facade. Was 947 LOC; now no
|
|
31
|
+
sub-module exceeds 207 LOC. `_clamp` promoted to models.py to resolve a
|
|
32
|
+
circular-dep risk between `evaluation` and `taste`.
|
|
33
|
+
|
|
34
|
+
### Infra
|
|
35
|
+
|
|
36
|
+
- **CI matrix adds Python 3.11.** Ableton 12.3's embedded Python is 3.11 on
|
|
37
|
+
some platforms — catching drift pre-merge.
|
|
38
|
+
- **`livepilot.mcpb` removed from git tracking.** It was already excluded from
|
|
39
|
+
`.npmignore` and `.mcpbignore`; now it's no longer bloating git history every
|
|
40
|
+
release. Distribute via GitHub Releases.
|
|
41
|
+
- **`.git-backup-full/` deleted.** 3.4 MB worktree reclaim.
|
|
42
|
+
|
|
43
|
+
### Docs
|
|
44
|
+
|
|
45
|
+
- **OSC address convention** documented in both `m4l_device/livepilot_bridge.js`
|
|
46
|
+
and `mcp_server/m4l_bridge.py` — the existing tolerant normalization at
|
|
47
|
+
`_parse_osc` now has a written contract.
|
|
48
|
+
|
|
49
|
+
### Tests
|
|
50
|
+
|
|
51
|
+
1756 pass, 1 skipped (macOS-only path test on non-darwin), 0 failures.
|
|
52
|
+
|
|
3
53
|
## 1.10.5 — Splice online catalog unblocked + Simpler sample-loading fixes (April 14 2026)
|
|
4
54
|
|
|
5
55
|
The Splice integration was **never working online** in previous releases. The
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.6",
|
|
4
4
|
"description": "Agentic production system for Ableton Live 12 — 320 tools, 43 domains, device atlas, sample intelligence, auto-composition, spectral perception, 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.10.
|
|
3
|
+
"version": "1.10.6",
|
|
4
4
|
"description": "Agentic production system for Ableton Live 12 — 320 tools, 43 domains, device atlas, sample intelligence, auto-composition, spectral perception, 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
|
-
# LivePilot v1.10.
|
|
1
|
+
# LivePilot v1.10.6 — Architecture & Tool Reference
|
|
2
2
|
|
|
3
|
-
Agentic production system for Ableton Live 12. 320 tools across 43 domains. Device atlas (1305 devices, 81 enriched), spectral perception (M4L analyzer), 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, Splice online catalog search and download via gRPC (v1.10.
|
|
3
|
+
Agentic production system for Ableton Live 12. 320 tools across 43 domains. Device atlas (1305 devices, 81 enriched), spectral perception (M4L analyzer), 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, Splice online catalog search and download via gRPC (v1.10.6 unblocked 19,690+ catalog hits previously inaccessible).
|
|
4
4
|
|
|
5
5
|
## Architecture
|
|
6
6
|
|
|
@@ -104,7 +104,7 @@ Call `get_capability_state` at the start of any evaluation session. The response
|
|
|
104
104
|
{
|
|
105
105
|
"mode": "normal",
|
|
106
106
|
"analyzer_connected": true,
|
|
107
|
-
"bridge_version": "1.10.
|
|
107
|
+
"bridge_version": "1.10.6",
|
|
108
108
|
"spectral_cache_age_ms": 1200,
|
|
109
109
|
"flucoma_available": false,
|
|
110
110
|
"session_connected": true
|
|
Binary file
|
|
@@ -14,6 +14,17 @@
|
|
|
14
14
|
* - Chunk parameter reads: 4 per batch, 50ms delay
|
|
15
15
|
* - Base64 encode all JSON responses
|
|
16
16
|
* - Defer all LiveAPI operations via deferlow()
|
|
17
|
+
*
|
|
18
|
+
* OSC address convention:
|
|
19
|
+
* - OUTGOING (this file → server via udpsend): use WITH leading slash,
|
|
20
|
+
* e.g. outlet(0, "/response", encoded). The slash is part of the
|
|
21
|
+
* OSC address that udpsend packs into the packet.
|
|
22
|
+
* - INCOMING (server → this file via udpreceive): Max's udpreceive
|
|
23
|
+
* routes on the selector, so the server's address string must be
|
|
24
|
+
* "response"/"cmd" WITHOUT a leading slash (Max would otherwise
|
|
25
|
+
* treat the slash as a literal selector character). See
|
|
26
|
+
* mcp_server/m4l_bridge.py for the sending side and `_parse_osc`
|
|
27
|
+
* for the tolerant normalization.
|
|
17
28
|
*/
|
|
18
29
|
|
|
19
30
|
autowatch = 1;
|
|
@@ -84,7 +95,7 @@ function anything() {
|
|
|
84
95
|
function dispatch(cmd, args) {
|
|
85
96
|
switch(cmd) {
|
|
86
97
|
case "ping":
|
|
87
|
-
send_response({"ok": true, "version": "1.10.
|
|
98
|
+
send_response({"ok": true, "version": "1.10.6"});
|
|
88
99
|
break;
|
|
89
100
|
case "get_params":
|
|
90
101
|
cmd_get_params(args);
|
package/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "livepilot",
|
|
4
4
|
"display_name": "LivePilot — AI for Ableton Live",
|
|
5
|
-
"version": "1.10.
|
|
5
|
+
"version": "1.10.6",
|
|
6
6
|
"description": "Agentic production system for Ableton Live 12. Make beats, mix tracks, design sounds, and arrange songs with 320 AI-powered tools.",
|
|
7
7
|
"long_description": "LivePilot is an agentic production system for Ableton Live 12. 320 tools across 43 domains — device atlas (1305 devices), sample intelligence (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, and 12 creative engines.\n\n**What it does:**\n- Creates MIDI clips with notes, chords, and rhythms\n- Loads instruments and effects via Device Atlas (1305 devices indexed)\n- Searches samples across Splice, Ableton browser, and filesystem\n- Plans compositions from text prompts with genre-aware layering\n- Slices samples with intent-based MIDI generation\n- Mixes with volume, panning, sends, and automation\n- Analyzes your mix with real-time spectral data (M4L bridge)\n- Diagnoses stuck sessions and generates creative rescue variants\n- Remembers your production style across sessions\n\n**How it works:**\nLivePilot installs a Remote Script in Ableton that communicates with the AI over a local TCP connection. Everything runs on your machine — no audio leaves your computer.",
|
|
8
8
|
"author": {
|
package/mcp_server/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
|
|
2
|
-
__version__ = "1.10.
|
|
2
|
+
__version__ = "1.10.6"
|
|
@@ -45,6 +45,10 @@ from pathlib import Path
|
|
|
45
45
|
from typing import Optional, Tuple
|
|
46
46
|
|
|
47
47
|
from .layer_planner import LayerSpec
|
|
48
|
+
import logging
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
48
52
|
|
|
49
53
|
|
|
50
54
|
_AUDIO_EXTENSIONS = (".wav", ".aif", ".aiff", ".flac")
|
|
@@ -216,9 +220,9 @@ async def _splice_resolve(
|
|
|
216
220
|
query=layer.search_query,
|
|
217
221
|
per_page=5,
|
|
218
222
|
)
|
|
219
|
-
except Exception:
|
|
223
|
+
except Exception as exc:
|
|
224
|
+
logger.debug("_splice_resolve failed: %s", exc)
|
|
220
225
|
return None, "unresolved"
|
|
221
|
-
|
|
222
226
|
samples = list(result.samples) if result and hasattr(result, "samples") else []
|
|
223
227
|
if not samples:
|
|
224
228
|
return None, "unresolved"
|
|
@@ -243,7 +247,8 @@ async def _splice_resolve(
|
|
|
243
247
|
downloaded = await splice_client.download_sample(file_hash)
|
|
244
248
|
if downloaded and Path(downloaded).exists():
|
|
245
249
|
return downloaded, "splice_remote"
|
|
246
|
-
except Exception:
|
|
250
|
+
except Exception as exc:
|
|
251
|
+
logger.debug("_splice_resolve failed: %s", exc)
|
|
247
252
|
continue # try next hit
|
|
248
253
|
|
|
249
254
|
return None, "unresolved"
|
|
@@ -286,7 +291,6 @@ async def resolve_sample_for_layer(
|
|
|
286
291
|
lp = hit.get("file_path") if isinstance(hit, dict) else None
|
|
287
292
|
if lp and Path(lp).exists():
|
|
288
293
|
return lp, "browser"
|
|
289
|
-
except Exception:
|
|
290
|
-
|
|
291
|
-
|
|
294
|
+
except Exception as exc:
|
|
295
|
+
logger.debug("resolve_sample_for_layer failed: %s", exc)
|
|
292
296
|
return None, "unresolved"
|
|
@@ -14,6 +14,10 @@ from fastmcp import Context
|
|
|
14
14
|
from ..server import mcp
|
|
15
15
|
from .prompt_parser import parse_prompt
|
|
16
16
|
from .engine import ComposerEngine
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
# Singleton engine — stateless, safe to reuse
|
|
@@ -29,8 +33,8 @@ def _get_search_roots(ctx: Context) -> list:
|
|
|
29
33
|
cfg = ctx.lifespan_context.get("sample_search_roots") if hasattr(ctx, "lifespan_context") else None
|
|
30
34
|
if cfg:
|
|
31
35
|
roots.extend(cfg)
|
|
32
|
-
except Exception:
|
|
33
|
-
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
logger.debug("_get_search_roots failed: %s", exc)
|
|
34
38
|
return roots
|
|
35
39
|
|
|
36
40
|
|
|
@@ -52,7 +56,8 @@ async def _credit_safety_prelude(splice_client, max_credits: int) -> tuple[int,
|
|
|
52
56
|
try:
|
|
53
57
|
info = await splice_client.get_credits()
|
|
54
58
|
credits_remaining = getattr(info, "credits", None)
|
|
55
|
-
except Exception:
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
logger.debug("_credit_safety_prelude failed: %s", exc)
|
|
56
61
|
credits_remaining = None
|
|
57
62
|
|
|
58
63
|
if credits_remaining is None:
|
|
@@ -153,9 +158,8 @@ async def augment_with_samples(
|
|
|
153
158
|
info = ableton.send_command("get_session_info", {})
|
|
154
159
|
session_context["tempo"] = info.get("tempo", 120)
|
|
155
160
|
session_context["track_count"] = info.get("track_count", 0)
|
|
156
|
-
except Exception:
|
|
157
|
-
|
|
158
|
-
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
logger.debug("augment_with_samples failed: %s", exc)
|
|
159
163
|
result = await _engine.augment(
|
|
160
164
|
request=request,
|
|
161
165
|
max_credits=max_credits,
|
package/mcp_server/connection.py
CHANGED
|
@@ -11,6 +11,10 @@ import time
|
|
|
11
11
|
import uuid
|
|
12
12
|
from collections import deque
|
|
13
13
|
from typing import Optional
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
14
18
|
|
|
15
19
|
CONNECT_TIMEOUT = 5
|
|
16
20
|
RECV_TIMEOUT = 20
|
|
@@ -153,7 +157,8 @@ class AbletonConnection:
|
|
|
153
157
|
"""Send a ping and return True if a pong is received."""
|
|
154
158
|
try:
|
|
155
159
|
return self.send_command("ping").get("pong") is True
|
|
156
|
-
except Exception:
|
|
160
|
+
except Exception as exc:
|
|
161
|
+
logger.debug("ping failed: %s", exc)
|
|
157
162
|
return False
|
|
158
163
|
|
|
159
164
|
def send_command(self, command_type: str, params: Optional[dict] = None) -> dict:
|
|
@@ -17,7 +17,9 @@ from fastmcp import Context
|
|
|
17
17
|
from ..server import mcp
|
|
18
18
|
from . import engine
|
|
19
19
|
from .models import CONSTRAINT_MODES
|
|
20
|
+
import logging
|
|
20
21
|
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
21
23
|
|
|
22
24
|
# Module-level cache for active constraints and distillations
|
|
23
25
|
_active_constraints: Optional[engine.ConstraintSet] = None
|
|
@@ -110,9 +112,8 @@ def distill_reference_principles(
|
|
|
110
112
|
"groove_posture": tactics.get("groove_posture", {}),
|
|
111
113
|
"harmonic_character": tactics.get("harmonic_character", ""),
|
|
112
114
|
}
|
|
113
|
-
except Exception:
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
logger.debug("distill_reference_principles failed: %s", exc)
|
|
116
117
|
# Try to get a reference profile from the reference engine
|
|
117
118
|
if not reference_profile:
|
|
118
119
|
try:
|
|
@@ -121,7 +122,8 @@ def distill_reference_principles(
|
|
|
121
122
|
style_name or reference_description
|
|
122
123
|
)
|
|
123
124
|
reference_profile = profile.to_dict()
|
|
124
|
-
except Exception:
|
|
125
|
+
except Exception as exc:
|
|
126
|
+
logger.debug("distill_reference_principles failed: %s", exc)
|
|
125
127
|
# Fallback: build from description keywords
|
|
126
128
|
reference_profile = _profile_from_description(reference_description)
|
|
127
129
|
|
|
@@ -204,9 +206,8 @@ def generate_constrained_variants(
|
|
|
204
206
|
taste_store = ctx.lifespan_context.setdefault("taste_memory", TasteMemoryStore())
|
|
205
207
|
anti_store = ctx.lifespan_context.setdefault("anti_memory", AntiMemoryStore())
|
|
206
208
|
taste_graph = build_taste_graph(taste_store=taste_store, anti_store=anti_store).to_dict()
|
|
207
|
-
except Exception:
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
except Exception as exc:
|
|
210
|
+
logger.debug("generate_constrained_variants failed: %s", exc)
|
|
210
211
|
ps = ps_engine.create_preview_set(
|
|
211
212
|
request_text=f"[Constrained: {', '.join(active.constraints)}] {request_text}",
|
|
212
213
|
kernel_id=kernel_id,
|
|
@@ -293,7 +294,6 @@ def generate_reference_inspired_variants(
|
|
|
293
294
|
except Exception as e:
|
|
294
295
|
return {"error": f"Failed to generate reference-inspired variants: {e}"}
|
|
295
296
|
|
|
296
|
-
|
|
297
297
|
# ── Helpers ───────────────────────────────────────────────────────
|
|
298
298
|
|
|
299
299
|
|
|
@@ -305,6 +305,7 @@ def _get_song_brain_dict() -> dict:
|
|
|
305
305
|
except Exception as _e:
|
|
306
306
|
if __debug__:
|
|
307
307
|
import sys
|
|
308
|
+
|
|
308
309
|
print(f"LivePilot: SongBrain unavailable in creative_constraints: {_e}", file=sys.stderr)
|
|
309
310
|
return {}
|
|
310
311
|
|
|
@@ -22,7 +22,9 @@ import time
|
|
|
22
22
|
from typing import Optional
|
|
23
23
|
|
|
24
24
|
from .models import ExperimentSet, ExperimentBranch, BranchSnapshot
|
|
25
|
+
import logging
|
|
25
26
|
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
26
28
|
|
|
27
29
|
# ── In-memory experiment store ───────────────────────────────────────────────
|
|
28
30
|
|
|
@@ -34,9 +36,9 @@ def _gen_id(prefix: str, seed: str) -> str:
|
|
|
34
36
|
h = hashlib.sha256(f"{prefix}:{seed}:{time.time()}".encode()).hexdigest()[:8]
|
|
35
37
|
return f"{prefix}_{h}"
|
|
36
38
|
|
|
37
|
-
|
|
38
39
|
# ── Create experiments ───────────────────────────────────────────────────────
|
|
39
40
|
|
|
41
|
+
|
|
40
42
|
def create_experiment(
|
|
41
43
|
request_text: str,
|
|
42
44
|
move_ids: list[str],
|
|
@@ -83,9 +85,9 @@ def list_experiments() -> list[dict]:
|
|
|
83
85
|
"""List all experiment sets."""
|
|
84
86
|
return [exp.to_dict() for exp in _EXPERIMENTS.values()]
|
|
85
87
|
|
|
86
|
-
|
|
87
88
|
# ── Run experiments (requires Ableton connection) ────────────────────────────
|
|
88
89
|
|
|
90
|
+
|
|
89
91
|
def run_branch(
|
|
90
92
|
branch: ExperimentBranch,
|
|
91
93
|
ableton, # AbletonConnection
|
|
@@ -143,7 +145,8 @@ def _run_branch_sync(branch, ableton, compiled_plan, capture_fn):
|
|
|
143
145
|
for _ in range(steps_executed):
|
|
144
146
|
try:
|
|
145
147
|
ableton.send_command("undo", {})
|
|
146
|
-
except Exception:
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
logger.debug("_run_branch_sync failed: %s", exc)
|
|
147
150
|
break
|
|
148
151
|
|
|
149
152
|
branch.status = "evaluated" if steps_executed > 0 else "failed"
|
|
@@ -215,7 +218,8 @@ async def run_branch_async(
|
|
|
215
218
|
for _ in range(steps_executed):
|
|
216
219
|
try:
|
|
217
220
|
ableton.send_command("undo", {})
|
|
218
|
-
except Exception:
|
|
221
|
+
except Exception as exc:
|
|
222
|
+
logger.debug("run_branch_async failed: %s", exc)
|
|
219
223
|
break
|
|
220
224
|
|
|
221
225
|
# A branch is "evaluated" only if it actually applied at least one step.
|
|
@@ -244,9 +248,9 @@ def evaluate_branch(
|
|
|
244
248
|
branch.score = result.get("score", 0.0)
|
|
245
249
|
return branch
|
|
246
250
|
|
|
247
|
-
|
|
248
251
|
# ── Commit / discard ─────────────────────────────────────────────────────────
|
|
249
252
|
|
|
253
|
+
|
|
250
254
|
async def commit_branch_async(
|
|
251
255
|
experiment: ExperimentSet,
|
|
252
256
|
branch_id: str,
|
|
@@ -18,6 +18,9 @@ from fastmcp import Context
|
|
|
18
18
|
from ..server import mcp
|
|
19
19
|
from . import engine
|
|
20
20
|
from .models import BranchSnapshot
|
|
21
|
+
import logging
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
def _get_ableton(ctx: Context):
|
|
@@ -35,25 +38,22 @@ def _capture_snapshot(ctx: Context) -> BranchSnapshot:
|
|
|
35
38
|
try:
|
|
36
39
|
meters = ableton.send_command("get_track_meters", {"include_stereo": True})
|
|
37
40
|
snapshot.track_meters = meters.get("tracks", [])
|
|
38
|
-
except Exception:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
logger.debug("_capture_snapshot failed: %s", exc)
|
|
41
43
|
# Spectral data (requires M4L analyzer)
|
|
42
44
|
if spectral and spectral.is_connected:
|
|
43
45
|
try:
|
|
44
46
|
spec = spectral.get("spectrum")
|
|
45
47
|
if spec:
|
|
46
48
|
snapshot.spectrum = spec.get("value", {})
|
|
47
|
-
except Exception:
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
logger.debug("_capture_snapshot failed: %s", exc)
|
|
50
51
|
try:
|
|
51
52
|
rms_data = spectral.get("rms")
|
|
52
53
|
if rms_data:
|
|
53
54
|
snapshot.rms = rms_data.get("value")
|
|
54
|
-
except Exception:
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
except Exception as exc:
|
|
56
|
+
logger.debug("_capture_snapshot failed: %s", exc)
|
|
57
57
|
return snapshot
|
|
58
58
|
|
|
59
59
|
|
|
@@ -17,6 +17,9 @@ from fastmcp import Context
|
|
|
17
17
|
|
|
18
18
|
from ..server import mcp
|
|
19
19
|
from . import analyzer
|
|
20
|
+
import logging
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
def _get_ableton(ctx: Context):
|
|
@@ -39,8 +42,8 @@ def _fetch_tracks_and_scenes(ctx: Context) -> tuple[list[dict], list[dict], dict
|
|
|
39
42
|
try:
|
|
40
43
|
session = ableton.send_command("get_session_info", {})
|
|
41
44
|
tracks = session.get("tracks", [])
|
|
42
|
-
except Exception:
|
|
43
|
-
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
logger.debug("_fetch_tracks_and_scenes failed: %s", exc)
|
|
44
47
|
|
|
45
48
|
try:
|
|
46
49
|
matrix = ableton.send_command("get_scene_matrix")
|
|
@@ -50,15 +53,16 @@ def _fetch_tracks_and_scenes(ctx: Context) -> tuple[list[dict], list[dict], dict
|
|
|
50
53
|
zip(matrix.get("scenes", []), matrix.get("matrix", []))
|
|
51
54
|
)
|
|
52
55
|
]
|
|
53
|
-
except Exception:
|
|
54
|
-
|
|
56
|
+
except Exception as exc:
|
|
57
|
+
logger.debug("_fetch_tracks_and_scenes failed: %s", exc)
|
|
55
58
|
|
|
56
59
|
# Fetch motif data — via shared motif service
|
|
57
60
|
try:
|
|
58
61
|
from ..services.motif_service import get_motif_data, fetch_notes_from_ableton
|
|
59
62
|
notes_by_track = fetch_notes_from_ableton(ableton, tracks)
|
|
60
63
|
motif_data = get_motif_data(notes_by_track)
|
|
61
|
-
except Exception:
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
logger.debug("_fetch_tracks_and_scenes failed: %s", exc)
|
|
62
66
|
pass # Motif graph requires notes in clips; empty dict is valid fallback
|
|
63
67
|
|
|
64
68
|
return tracks, scenes, motif_data
|
|
@@ -325,7 +329,6 @@ def suggest_payoff_repair(ctx: Context) -> dict:
|
|
|
325
329
|
"repair_count": len(repairs),
|
|
326
330
|
}
|
|
327
331
|
|
|
328
|
-
|
|
329
332
|
# ── Helpers ───────────────────────────────────────────────────────
|
|
330
333
|
|
|
331
334
|
|
|
@@ -376,8 +379,8 @@ def _get_section_data(ableton) -> list[dict]:
|
|
|
376
379
|
"density": round(density, 3),
|
|
377
380
|
"has_drums": has_drums,
|
|
378
381
|
})
|
|
379
|
-
except Exception:
|
|
380
|
-
|
|
382
|
+
except Exception as exc:
|
|
383
|
+
logger.debug("_get_section_data failed: %s", exc)
|
|
381
384
|
|
|
382
385
|
return sections
|
|
383
386
|
|
|
@@ -391,6 +394,7 @@ def _get_song_brain_dict() -> dict:
|
|
|
391
394
|
except Exception as _e:
|
|
392
395
|
if __debug__:
|
|
393
396
|
import sys
|
|
397
|
+
|
|
394
398
|
print(f"LivePilot: SongBrain unavailable in hook_hunter: {_e}", file=sys.stderr)
|
|
395
399
|
return {}
|
|
396
400
|
|
|
@@ -421,7 +425,8 @@ def detect_hook_neglect(ctx: Context) -> dict:
|
|
|
421
425
|
|
|
422
426
|
try:
|
|
423
427
|
matrix = ableton.send_command("get_scene_matrix")
|
|
424
|
-
except Exception:
|
|
428
|
+
except Exception as exc:
|
|
429
|
+
logger.debug("detect_hook_neglect failed: %s", exc)
|
|
425
430
|
return {
|
|
426
431
|
"neglected": False,
|
|
427
432
|
"hook": hook.to_dict(),
|
package/mcp_server/m4l_bridge.py
CHANGED
|
@@ -7,6 +7,17 @@ the M4L device on the master track. Sends commands back for deep LOM access.
|
|
|
7
7
|
Architecture:
|
|
8
8
|
M4L → UDP:9880 → SpectralReceiver → SpectralCache → MCP tools
|
|
9
9
|
MCP tools → M4LBridge → UDP:9881 → M4L device
|
|
10
|
+
|
|
11
|
+
OSC address convention:
|
|
12
|
+
- OUTGOING (this side → M4L): address string is sent WITHOUT a leading
|
|
13
|
+
slash because Max's `udpreceive` treats a literal '/' as part of the
|
|
14
|
+
selector. The JS side (livepilot_bridge.js) routes on bare selectors
|
|
15
|
+
like "cmd" / "ping".
|
|
16
|
+
- INCOMING (M4L → this side): the M4L side uses Max's `udpsend`, whose
|
|
17
|
+
outlet messages include the leading slash (e.g. "/response"). The
|
|
18
|
+
`_parse_osc` helper normalizes with `rest = "/" + rest.lstrip("/\\")`
|
|
19
|
+
so both forms are tolerated — keep that normalization; both sides
|
|
20
|
+
bend toward leniency but the outgoing convention here is slash-less.
|
|
10
21
|
"""
|
|
11
22
|
|
|
12
23
|
from __future__ import annotations
|
|
@@ -17,6 +17,9 @@ from __future__ import annotations
|
|
|
17
17
|
import time
|
|
18
18
|
from dataclasses import dataclass, field
|
|
19
19
|
from typing import Optional
|
|
20
|
+
import logging
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
@dataclass
|
|
@@ -127,7 +130,8 @@ class TasteGraph:
|
|
|
127
130
|
if self._persistent_store is not None:
|
|
128
131
|
try:
|
|
129
132
|
self._persistent_store.record_move_outcome(move_id, family, kept, score)
|
|
130
|
-
except Exception:
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
logger.debug("record_move_outcome failed: %s", exc)
|
|
131
135
|
pass # persistence is best-effort
|
|
132
136
|
|
|
133
137
|
def record_device_use(self, device_name: str, positive: bool = True) -> None:
|
|
@@ -249,9 +253,9 @@ class TasteGraph:
|
|
|
249
253
|
"explanations": explanations,
|
|
250
254
|
}
|
|
251
255
|
|
|
252
|
-
|
|
253
256
|
# ── Builder ──────────────────────────────────────────────────────────────────
|
|
254
257
|
|
|
258
|
+
|
|
255
259
|
def build_taste_graph(
|
|
256
260
|
taste_store=None, # TasteMemoryStore
|
|
257
261
|
anti_store=None, # AntiMemoryStore
|
|
@@ -299,6 +303,7 @@ def build_taste_graph(
|
|
|
299
303
|
# Device affinities
|
|
300
304
|
for dev_name, dev_data in persisted.get("device_affinities", {}).items():
|
|
301
305
|
from .taste_graph import DeviceAffinity
|
|
306
|
+
|
|
302
307
|
graph.device_affinities[dev_name] = DeviceAffinity(
|
|
303
308
|
device_name=dev_name,
|
|
304
309
|
affinity=dev_data.get("affinity", 0.0),
|
|
@@ -15,6 +15,10 @@ from ..evaluation.fabric import evaluate_sonic_move
|
|
|
15
15
|
from .state_builder import build_mix_state
|
|
16
16
|
from .critics import run_all_mix_critics
|
|
17
17
|
from .planner import plan_mix_moves
|
|
18
|
+
import logging
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
# ── Helpers ─────────────────────────────────────────────────────────
|
|
@@ -32,7 +36,8 @@ def _fetch_mix_data(ctx: Context) -> dict:
|
|
|
32
36
|
try:
|
|
33
37
|
info = ableton.send_command("get_track_info", {"track_index": i})
|
|
34
38
|
track_infos.append(info)
|
|
35
|
-
except Exception:
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
logger.debug("_fetch_mix_data failed: %s", exc)
|
|
36
41
|
continue
|
|
37
42
|
|
|
38
43
|
# Get spectrum and RMS data directly from SpectralCache (not TCP)
|
|
@@ -51,8 +56,8 @@ def _fetch_mix_data(ctx: Context) -> dict:
|
|
|
51
56
|
rms_snap = spectral.get("rms")
|
|
52
57
|
if rms_snap:
|
|
53
58
|
rms_data = rms_snap["value"] if isinstance(rms_snap["value"], dict) else rms_snap["value"]
|
|
54
|
-
except Exception:
|
|
55
|
-
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
logger.debug("_fetch_mix_data failed: %s", exc)
|
|
56
61
|
|
|
57
62
|
return {
|
|
58
63
|
"session_info": session_info,
|