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
|
@@ -13,6 +13,9 @@ from fastmcp import Context
|
|
|
13
13
|
|
|
14
14
|
from ..server import mcp
|
|
15
15
|
from . import detectors
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
def _get_ableton(ctx: Context):
|
|
@@ -34,7 +37,8 @@ def detect_repetition_fatigue(ctx: Context) -> dict:
|
|
|
34
37
|
# Get scene matrix for clip reuse analysis
|
|
35
38
|
try:
|
|
36
39
|
matrix = ableton.send_command("get_scene_matrix")
|
|
37
|
-
except Exception:
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
logger.debug("detect_repetition_fatigue failed: %s", exc)
|
|
38
42
|
matrix = {}
|
|
39
43
|
|
|
40
44
|
scenes = []
|
|
@@ -53,8 +57,8 @@ def detect_repetition_fatigue(ctx: Context) -> dict:
|
|
|
53
57
|
track_list = session_info.get("tracks", [])
|
|
54
58
|
notes_by_track = fetch_notes_from_ableton(ableton, track_list)
|
|
55
59
|
motif_graph = get_motif_data(notes_by_track)
|
|
56
|
-
except Exception:
|
|
57
|
-
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
logger.debug("detect_repetition_fatigue failed: %s", exc)
|
|
58
62
|
|
|
59
63
|
report = detectors.detect_repetition_fatigue(scenes, motif_graph)
|
|
60
64
|
return report.to_dict()
|
|
@@ -97,7 +101,8 @@ def infer_section_purposes(ctx: Context) -> dict:
|
|
|
97
101
|
# Get scene matrix for density analysis
|
|
98
102
|
try:
|
|
99
103
|
matrix = ableton.send_command("get_scene_matrix")
|
|
100
|
-
except Exception:
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
logger.debug("infer_section_purposes failed: %s", exc)
|
|
101
106
|
matrix = {}
|
|
102
107
|
|
|
103
108
|
scenes = []
|
|
@@ -132,7 +137,8 @@ def score_emotional_arc(ctx: Context) -> dict:
|
|
|
132
137
|
|
|
133
138
|
try:
|
|
134
139
|
matrix = ableton.send_command("get_scene_matrix")
|
|
135
|
-
except Exception:
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
logger.debug("score_emotional_arc failed: %s", exc)
|
|
136
142
|
matrix = {}
|
|
137
143
|
|
|
138
144
|
scenes = []
|
|
@@ -147,7 +153,6 @@ def score_emotional_arc(ctx: Context) -> dict:
|
|
|
147
153
|
arc = detectors.score_emotional_arc(purposes)
|
|
148
154
|
return arc.to_dict()
|
|
149
155
|
|
|
150
|
-
|
|
151
156
|
# ── Phrase Evaluation ────────────────────────────────────────────────
|
|
152
157
|
|
|
153
158
|
|
|
@@ -179,14 +184,14 @@ def analyze_phrase_arc(
|
|
|
179
184
|
try:
|
|
180
185
|
from ..tools._perception_engine import compute_loudness
|
|
181
186
|
loudness_data = compute_loudness(file_path, detail="full")
|
|
182
|
-
except Exception:
|
|
183
|
-
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
logger.debug("analyze_phrase_arc failed: %s", exc)
|
|
184
189
|
|
|
185
190
|
try:
|
|
186
191
|
from ..tools._perception_engine import compute_spectral
|
|
187
192
|
spectrum_data = compute_spectral(file_path)
|
|
188
|
-
except Exception:
|
|
189
|
-
|
|
193
|
+
except Exception as exc:
|
|
194
|
+
logger.debug("analyze_phrase_arc failed: %s", exc)
|
|
190
195
|
|
|
191
196
|
critique = phrase_critic.analyze_phrase(loudness_data, spectrum_data, target)
|
|
192
197
|
critique.render_id = file_path.split("/")[-1] if "/" in file_path else file_path
|
|
@@ -12,6 +12,10 @@ from ..server import mcp
|
|
|
12
12
|
from .models import EnergyWindow, SceneRole
|
|
13
13
|
from .planner import build_performance_state, plan_scene_transition, suggest_energy_moves
|
|
14
14
|
from .safety import classify_move_safety, get_blocked_moves, get_safe_moves
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
# ── Helpers ─────────────────────────────────────────────────────────
|
|
@@ -87,8 +91,8 @@ def _fetch_scene_data(ctx: Context) -> tuple[list[SceneRole], int]:
|
|
|
87
91
|
if s.get("is_triggered", False):
|
|
88
92
|
current_scene = i
|
|
89
93
|
break
|
|
90
|
-
except Exception:
|
|
91
|
-
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
logger.debug("_fetch_scene_data failed: %s", exc)
|
|
92
96
|
|
|
93
97
|
return scene_roles, current_scene
|
|
94
98
|
|
|
@@ -15,6 +15,9 @@ from fastmcp import Context
|
|
|
15
15
|
|
|
16
16
|
from ..server import mcp
|
|
17
17
|
from . import engine
|
|
18
|
+
import logging
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
def _get_ableton(ctx: Context):
|
|
@@ -39,10 +42,9 @@ def _find_wonder_session_by_preview(set_id: str):
|
|
|
39
42
|
try:
|
|
40
43
|
from ..wonder_mode.session import find_session_by_preview_set
|
|
41
44
|
return find_session_by_preview_set(set_id)
|
|
42
|
-
except Exception:
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
logger.debug("_find_wonder_session_by_preview failed: %s", exc)
|
|
43
47
|
return None
|
|
44
|
-
|
|
45
|
-
|
|
46
48
|
@mcp.tool()
|
|
47
49
|
def create_preview_set(
|
|
48
50
|
ctx: Context,
|
|
@@ -148,8 +150,8 @@ def create_preview_set(
|
|
|
148
150
|
# Fallback: if no keyword match, take top 3 from full registry
|
|
149
151
|
if not available_moves:
|
|
150
152
|
available_moves = registry.list_moves()[:3]
|
|
151
|
-
except Exception:
|
|
152
|
-
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
logger.debug("create_preview_set failed: %s", exc)
|
|
153
155
|
|
|
154
156
|
# Get song brain if available
|
|
155
157
|
song_brain: dict = {}
|
|
@@ -177,8 +179,8 @@ def create_preview_set(
|
|
|
177
179
|
persistent_store=persistent,
|
|
178
180
|
)
|
|
179
181
|
taste_graph = graph.to_dict()
|
|
180
|
-
except Exception:
|
|
181
|
-
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
logger.debug("create_preview_set failed: %s", exc)
|
|
182
184
|
|
|
183
185
|
ps = engine.create_preview_set(
|
|
184
186
|
request_text=request_text,
|
|
@@ -345,8 +347,8 @@ async def commit_preview_variant(
|
|
|
345
347
|
)
|
|
346
348
|
if ws.creative_thread_id:
|
|
347
349
|
resolve_thread(ws.creative_thread_id)
|
|
348
|
-
except Exception:
|
|
349
|
-
|
|
350
|
+
except Exception as exc:
|
|
351
|
+
logger.debug("commit_preview_variant failed: %s", exc)
|
|
350
352
|
|
|
351
353
|
# Update taste graph (with persistent backing)
|
|
352
354
|
try:
|
|
@@ -373,8 +375,8 @@ async def commit_preview_variant(
|
|
|
373
375
|
family=family,
|
|
374
376
|
kept=True,
|
|
375
377
|
)
|
|
376
|
-
except Exception:
|
|
377
|
-
|
|
378
|
+
except Exception as exc:
|
|
379
|
+
logger.debug("commit_preview_variant failed: %s", exc)
|
|
378
380
|
|
|
379
381
|
result["wonder_session_id"] = ws.session_id
|
|
380
382
|
|
|
@@ -488,6 +490,7 @@ async def render_preview_variant(
|
|
|
488
490
|
playback_started = True
|
|
489
491
|
|
|
490
492
|
import time as _time
|
|
493
|
+
|
|
491
494
|
_time.sleep(play_seconds)
|
|
492
495
|
|
|
493
496
|
spectral_after = cache.get_all()
|
|
@@ -496,7 +499,8 @@ async def render_preview_variant(
|
|
|
496
499
|
playback_started = False
|
|
497
500
|
|
|
498
501
|
preview_mode = "audible_preview"
|
|
499
|
-
except Exception:
|
|
502
|
+
except Exception as exc:
|
|
503
|
+
logger.debug("render_preview_variant failed: %s", exc)
|
|
500
504
|
# Spectral capture is best-effort; keep preview_mode as metadata_only
|
|
501
505
|
pass
|
|
502
506
|
|
|
@@ -507,12 +511,14 @@ async def render_preview_variant(
|
|
|
507
511
|
if playback_started:
|
|
508
512
|
try:
|
|
509
513
|
ableton.send_command("stop_playback", {})
|
|
510
|
-
except Exception:
|
|
511
|
-
|
|
514
|
+
except Exception as exc:
|
|
515
|
+
logger.debug("render_preview_variant failed: %s", exc)
|
|
516
|
+
|
|
512
517
|
for _ in range(applied_count):
|
|
513
518
|
try:
|
|
514
519
|
ableton.send_command("undo")
|
|
515
|
-
except Exception:
|
|
520
|
+
except Exception as exc:
|
|
521
|
+
logger.debug("render_preview_variant failed: %s", exc)
|
|
516
522
|
break
|
|
517
523
|
|
|
518
524
|
variant.status = "rendered"
|
|
@@ -11,6 +11,10 @@ from fastmcp import Context
|
|
|
11
11
|
|
|
12
12
|
from ..server import mcp
|
|
13
13
|
from .builder import build_project_state_from_data
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
def _get_ableton(ctx: Context):
|
|
@@ -39,7 +43,8 @@ def build_project_brain(ctx: Context) -> dict:
|
|
|
39
43
|
try:
|
|
40
44
|
scenes_resp = ableton.send_command("get_scenes_info")
|
|
41
45
|
scenes = scenes_resp.get("scenes", [])
|
|
42
|
-
except Exception:
|
|
46
|
+
except Exception as exc:
|
|
47
|
+
logger.debug("build_project_brain failed: %s", exc)
|
|
43
48
|
scenes = session_info.get("scenes", [])
|
|
44
49
|
|
|
45
50
|
# 3. Get clip matrix (scene_matrix)
|
|
@@ -47,8 +52,8 @@ def build_project_brain(ctx: Context) -> dict:
|
|
|
47
52
|
try:
|
|
48
53
|
matrix_resp = ableton.send_command("get_scene_matrix")
|
|
49
54
|
clip_matrix = matrix_resp.get("matrix", [])
|
|
50
|
-
except Exception:
|
|
51
|
-
|
|
55
|
+
except Exception as exc:
|
|
56
|
+
logger.debug("build_project_brain failed: %s", exc)
|
|
52
57
|
|
|
53
58
|
# 4. Gather per-track info with devices
|
|
54
59
|
track_infos = []
|
|
@@ -58,7 +63,8 @@ def build_project_brain(ctx: Context) -> dict:
|
|
|
58
63
|
"track_index": track["index"],
|
|
59
64
|
})
|
|
60
65
|
track_infos.append(info)
|
|
61
|
-
except Exception:
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
logger.debug("build_project_brain failed: %s", exc)
|
|
62
68
|
track_infos.append({
|
|
63
69
|
"index": track.get("index", 0),
|
|
64
70
|
"name": track.get("name", ""),
|
|
@@ -75,8 +81,8 @@ def build_project_brain(ctx: Context) -> dict:
|
|
|
75
81
|
clips = arr.get("clips", [])
|
|
76
82
|
if clips:
|
|
77
83
|
arrangement_clips[track["index"]] = clips
|
|
78
|
-
except Exception:
|
|
79
|
-
|
|
84
|
+
except Exception as exc:
|
|
85
|
+
logger.debug("build_project_brain failed: %s", exc)
|
|
80
86
|
|
|
81
87
|
# 5b. Build notes_map for role inference.
|
|
82
88
|
# Shape: {section_id: {track_index: [notes]}}. Without this, role_graph
|
|
@@ -102,12 +108,14 @@ def build_project_brain(ctx: Context) -> dict:
|
|
|
102
108
|
notes = notes_resp.get("notes", [])
|
|
103
109
|
if notes:
|
|
104
110
|
per_track[t_idx] = notes
|
|
105
|
-
except Exception:
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
logger.debug("build_project_brain failed: %s", exc)
|
|
106
113
|
# Individual note fetch failing is fine — continue with others
|
|
107
114
|
continue
|
|
108
115
|
if per_track:
|
|
109
116
|
notes_map[section_id] = per_track
|
|
110
|
-
except Exception:
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
logger.debug("build_project_brain failed: %s", exc)
|
|
111
119
|
# Overall failure: empty map, degrade to "all tracks active" fallback
|
|
112
120
|
notes_map = {}
|
|
113
121
|
|
|
@@ -127,8 +135,8 @@ def build_project_brain(ctx: Context) -> dict:
|
|
|
127
135
|
if spectral.get(key) is not None:
|
|
128
136
|
flucoma_ok = True
|
|
129
137
|
break
|
|
130
|
-
except Exception:
|
|
131
|
-
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
logger.debug("build_project_brain failed: %s", exc)
|
|
132
140
|
|
|
133
141
|
# 7. Build state
|
|
134
142
|
state = build_project_state_from_data(
|
|
@@ -13,7 +13,9 @@ from ..tools._research_engine import get_style_tactics
|
|
|
13
13
|
from .profile_builder import build_audio_reference_profile, build_style_reference_profile
|
|
14
14
|
from .gap_analyzer import analyze_gaps, classify_gap_relevance, detect_identity_warnings
|
|
15
15
|
from .tactic_router import build_reference_plan
|
|
16
|
+
import logging
|
|
16
17
|
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
17
19
|
|
|
18
20
|
# ── Helpers ────────────────────────────────────────────────────────
|
|
19
21
|
|
|
@@ -50,6 +52,7 @@ def _fetch_project_snapshot(ctx: Context) -> dict:
|
|
|
50
52
|
rms = rms_snap["value"] if isinstance(rms_snap["value"], (int, float)) else 0.0
|
|
51
53
|
if rms > 0:
|
|
52
54
|
import math
|
|
55
|
+
|
|
53
56
|
snapshot["loudness"] = round(20 * math.log10(max(rms, 1e-10)), 2)
|
|
54
57
|
|
|
55
58
|
spec_data = spectral.get("spectrum")
|
|
@@ -58,8 +61,8 @@ def _fetch_project_snapshot(ctx: Context) -> dict:
|
|
|
58
61
|
key_data = spectral.get("key")
|
|
59
62
|
if key_data:
|
|
60
63
|
snapshot["spectral"]["detected_key"] = key_data["value"]
|
|
61
|
-
except Exception:
|
|
62
|
-
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
logger.debug("_fetch_project_snapshot failed: %s", exc)
|
|
63
66
|
|
|
64
67
|
# Try to get session info for pacing / density
|
|
65
68
|
try:
|
|
@@ -69,12 +72,11 @@ def _fetch_project_snapshot(ctx: Context) -> dict:
|
|
|
69
72
|
# Rough density estimate
|
|
70
73
|
snapshot["density"] = min(1.0, track_count / 20.0)
|
|
71
74
|
snapshot["pacing"] = [{"label": f"scene_{i}", "bars": 8} for i in range(scene_count)]
|
|
72
|
-
except Exception:
|
|
73
|
-
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
logger.debug("_fetch_project_snapshot failed: %s", exc)
|
|
74
77
|
|
|
75
78
|
return snapshot
|
|
76
79
|
|
|
77
|
-
|
|
78
80
|
# ── MCP Tools ──────────────────────────────────────────────────────
|
|
79
81
|
|
|
80
82
|
|
|
@@ -9,6 +9,9 @@ from __future__ import annotations
|
|
|
9
9
|
import os
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
def probe_capabilities(
|
|
@@ -27,8 +30,9 @@ def probe_capabilities(
|
|
|
27
30
|
try:
|
|
28
31
|
info = ableton.send_command("ping")
|
|
29
32
|
ableton_ok = info is not None
|
|
30
|
-
except Exception:
|
|
31
|
-
|
|
33
|
+
except Exception as exc:
|
|
34
|
+
logger.debug("probe_capabilities failed: %s", exc)
|
|
35
|
+
|
|
32
36
|
report["ableton"] = {
|
|
33
37
|
"status": "ok" if ableton_ok else "unavailable",
|
|
34
38
|
"detail": "TCP 9878 connection active" if ableton_ok else "Not connected",
|
|
@@ -40,8 +44,9 @@ def probe_capabilities(
|
|
|
40
44
|
try:
|
|
41
45
|
info = ableton.send_command("get_session_info")
|
|
42
46
|
live_version_str = info.get("live_version", "12.0.0")
|
|
43
|
-
except Exception:
|
|
44
|
-
|
|
47
|
+
except Exception as exc:
|
|
48
|
+
logger.debug("probe_capabilities failed: %s", exc)
|
|
49
|
+
|
|
45
50
|
from .live_version import LiveVersionCapabilities
|
|
46
51
|
version_caps = LiveVersionCapabilities.from_version_string(live_version_str)
|
|
47
52
|
report["live_version"] = {
|
|
@@ -75,6 +80,7 @@ def probe_capabilities(
|
|
|
75
80
|
numpy_ok = False
|
|
76
81
|
try:
|
|
77
82
|
import numpy # noqa: F401
|
|
83
|
+
|
|
78
84
|
numpy_ok = True
|
|
79
85
|
except ImportError:
|
|
80
86
|
pass
|
|
@@ -12,6 +12,9 @@ from fastmcp import Context
|
|
|
12
12
|
from ..server import mcp
|
|
13
13
|
from ..memory.technique_store import TechniqueStore
|
|
14
14
|
from .capability_state import build_capability_state
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
15
18
|
|
|
16
19
|
_memory_store = TechniqueStore()
|
|
17
20
|
|
|
@@ -31,7 +34,8 @@ def get_capability_state(ctx: Context) -> dict:
|
|
|
31
34
|
try:
|
|
32
35
|
result = ableton.send_command("get_session_info")
|
|
33
36
|
session_ok = isinstance(result, dict) and "error" not in result
|
|
34
|
-
except Exception:
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
logger.debug("get_capability_state failed: %s", exc)
|
|
35
39
|
session_ok = False
|
|
36
40
|
|
|
37
41
|
# ── Probe analyzer (M4L bridge) ─────────────────────────────────
|
|
@@ -49,7 +53,8 @@ def get_capability_state(ctx: Context) -> dict:
|
|
|
49
53
|
try:
|
|
50
54
|
_memory_store.list_techniques(limit=1)
|
|
51
55
|
memory_ok = True
|
|
52
|
-
except Exception:
|
|
56
|
+
except Exception as exc:
|
|
57
|
+
logger.debug("get_capability_state failed: %s", exc)
|
|
53
58
|
memory_ok = False
|
|
54
59
|
|
|
55
60
|
# ── Web / FluCoMa — not probed live, default to False ───────────
|
|
@@ -180,6 +185,7 @@ def get_session_kernel(
|
|
|
180
185
|
if request_text.strip():
|
|
181
186
|
try:
|
|
182
187
|
from ..tools._conductor import classify_request
|
|
188
|
+
|
|
183
189
|
plan = classify_request(request_text)
|
|
184
190
|
kernel.recommended_engines = [r.engine for r in plan.routes[:3]]
|
|
185
191
|
kernel.recommended_workflow = plan.workflow_mode
|
|
@@ -6,12 +6,15 @@ direct Splice online catalog hunt/download via the gRPC client.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import logging
|
|
9
10
|
import os
|
|
10
11
|
from typing import Optional
|
|
11
12
|
|
|
12
13
|
from fastmcp import Context
|
|
13
14
|
|
|
14
15
|
from ..server import mcp
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
15
18
|
from .models import SampleProfile, SampleIntent, SampleFitReport
|
|
16
19
|
from .analyzer import build_profile_from_filename
|
|
17
20
|
from .critics import run_all_sample_critics
|
|
@@ -47,8 +50,8 @@ async def analyze_sample(
|
|
|
47
50
|
)
|
|
48
51
|
if not result.get("error"):
|
|
49
52
|
file_path = result.get("file_path")
|
|
50
|
-
except Exception:
|
|
51
|
-
|
|
53
|
+
except Exception as exc:
|
|
54
|
+
logger.warning("m4l get_clip_file_path failed: %s", exc)
|
|
52
55
|
|
|
53
56
|
if file_path is None:
|
|
54
57
|
return {"error": "Could not determine file path — provide file_path directly"}
|
|
@@ -97,7 +100,8 @@ def evaluate_sample_fit(
|
|
|
97
100
|
name = track_info.get("name", "").lower()
|
|
98
101
|
if name:
|
|
99
102
|
existing_roles.append(name)
|
|
100
|
-
except Exception:
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
logger.debug("get_track_info(%d) skipped: %s", i, exc)
|
|
101
105
|
continue
|
|
102
106
|
|
|
103
107
|
# Detect key from MIDI tracks
|
|
@@ -119,12 +123,13 @@ def evaluate_sample_fit(
|
|
|
119
123
|
mode_suffix = "m" if "minor" in mode else ""
|
|
120
124
|
song_key = f"{key_result['tonic_name']}{mode_suffix}"
|
|
121
125
|
break
|
|
122
|
-
except Exception:
|
|
126
|
+
except Exception as exc:
|
|
127
|
+
logger.debug("key detection on track %d skipped: %s", i, exc)
|
|
123
128
|
continue
|
|
124
129
|
except ImportError:
|
|
125
130
|
pass
|
|
126
|
-
except Exception:
|
|
127
|
-
|
|
131
|
+
except Exception as exc:
|
|
132
|
+
logger.warning("session context for evaluate_sample_fit failed: %s", exc)
|
|
128
133
|
|
|
129
134
|
critics = run_all_sample_critics(
|
|
130
135
|
profile=profile,
|
|
@@ -243,7 +248,8 @@ async def search_samples(
|
|
|
243
248
|
},
|
|
244
249
|
})
|
|
245
250
|
used_grpc = True
|
|
246
|
-
except Exception:
|
|
251
|
+
except Exception as exc:
|
|
252
|
+
logger.warning("Splice gRPC search failed, falling back to SQL: %s", exc)
|
|
247
253
|
used_grpc = False
|
|
248
254
|
|
|
249
255
|
# Also query local index (if not already covered by gRPC) to surface
|
|
@@ -282,10 +288,11 @@ async def search_samples(
|
|
|
282
288
|
d = candidate.to_dict()
|
|
283
289
|
d["source_priority"] = 2
|
|
284
290
|
results.append(d)
|
|
285
|
-
except Exception:
|
|
291
|
+
except Exception as exc:
|
|
292
|
+
logger.debug("browser search %s skipped: %s", category, exc)
|
|
286
293
|
continue
|
|
287
|
-
except Exception:
|
|
288
|
-
|
|
294
|
+
except Exception as exc:
|
|
295
|
+
logger.warning("browser search unavailable: %s", exc)
|
|
289
296
|
|
|
290
297
|
# Filesystem search
|
|
291
298
|
if source in (None, "filesystem"):
|
|
@@ -442,7 +449,8 @@ def get_sample_opportunities(ctx: Context) -> dict:
|
|
|
442
449
|
try:
|
|
443
450
|
ableton = ctx.lifespan_context["ableton"]
|
|
444
451
|
info = ableton.send_command("get_session_info", {})
|
|
445
|
-
except Exception:
|
|
452
|
+
except Exception as exc:
|
|
453
|
+
logger.warning("get_sample_opportunities: Ableton not reachable: %s", exc)
|
|
446
454
|
return {"opportunities": [], "note": "Cannot read session — Ableton not connected"}
|
|
447
455
|
|
|
448
456
|
track_count = info.get("track_count", 0)
|
|
@@ -458,7 +466,8 @@ def get_sample_opportunities(ctx: Context) -> dict:
|
|
|
458
466
|
for d in devices:
|
|
459
467
|
if d.get("class_name") in ("OriginalSimpler", "MultiSampler"):
|
|
460
468
|
has_sampler = True
|
|
461
|
-
except Exception:
|
|
469
|
+
except Exception as exc:
|
|
470
|
+
logger.debug("track scan idx=%d skipped: %s", i, exc)
|
|
462
471
|
continue
|
|
463
472
|
|
|
464
473
|
# No organic texture
|
|
@@ -542,8 +551,8 @@ def plan_slice_workflow(
|
|
|
542
551
|
if ableton:
|
|
543
552
|
info = ableton.send_command("get_session_info", {})
|
|
544
553
|
tempo = float(info.get("tempo", 120.0))
|
|
545
|
-
except Exception:
|
|
546
|
-
|
|
554
|
+
except Exception as exc:
|
|
555
|
+
logger.debug("plan_slice_workflow tempo fetch failed (using 120): %s", exc)
|
|
547
556
|
|
|
548
557
|
# Read slice count from existing Simpler if track provided
|
|
549
558
|
slice_count = 8 # Default transient slice count
|
|
@@ -556,8 +565,8 @@ def plan_slice_workflow(
|
|
|
556
565
|
})
|
|
557
566
|
if isinstance(slices, dict) and slices.get("slice_count"):
|
|
558
567
|
slice_count = slices["slice_count"]
|
|
559
|
-
except Exception:
|
|
560
|
-
|
|
568
|
+
except Exception as exc:
|
|
569
|
+
logger.debug("get_simpler_slices failed (using default 8): %s", exc)
|
|
561
570
|
|
|
562
571
|
# Build the plan
|
|
563
572
|
plan = plan_slice_steps(
|
|
@@ -891,7 +900,7 @@ async def splice_download_sample(
|
|
|
891
900
|
try:
|
|
892
901
|
info = await client.get_credits()
|
|
893
902
|
response["credits_remaining"] = int(info.credits)
|
|
894
|
-
except Exception:
|
|
895
|
-
|
|
903
|
+
except Exception as exc:
|
|
904
|
+
logger.warning("post-download credit check failed: %s", exc)
|
|
896
905
|
|
|
897
906
|
return response
|
|
@@ -14,6 +14,9 @@ from fastmcp import Context
|
|
|
14
14
|
|
|
15
15
|
from ..server import mcp
|
|
16
16
|
from . import registry
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
@mcp.tool()
|
|
@@ -80,7 +83,8 @@ def preview_semantic_move(
|
|
|
80
83
|
info = ableton.send_command("get_session_info")
|
|
81
84
|
if isinstance(info, dict):
|
|
82
85
|
session_info = info
|
|
83
|
-
except Exception:
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
logger.debug("preview_semantic_move failed: %s", exc)
|
|
84
88
|
session_info = {}
|
|
85
89
|
|
|
86
90
|
state = build_capability_state(
|
package/mcp_server/server.py
CHANGED
|
@@ -44,9 +44,9 @@ def _master_has_livepilot_analyzer(ableton: AbletonConnection) -> bool:
|
|
|
44
44
|
"""Check whether the analyzer device is currently on the master track."""
|
|
45
45
|
try:
|
|
46
46
|
track = ableton.send_command("get_master_track")
|
|
47
|
-
except Exception:
|
|
47
|
+
except Exception as exc:
|
|
48
|
+
logger.debug("_master_has_livepilot_analyzer failed: %s", exc)
|
|
48
49
|
return False
|
|
49
|
-
|
|
50
50
|
devices = track.get("devices", []) if isinstance(track, dict) else []
|
|
51
51
|
for device in devices:
|
|
52
52
|
normalized = " ".join(
|
|
@@ -92,7 +92,8 @@ async def lifespan(server):
|
|
|
92
92
|
splice_client = SpliceGRPCClient()
|
|
93
93
|
try:
|
|
94
94
|
await splice_client.connect()
|
|
95
|
-
except Exception:
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
logger.debug("lifespan failed: %s", exc)
|
|
96
97
|
pass # client remains in disconnected state
|
|
97
98
|
|
|
98
99
|
# Start UDP listener for incoming M4L spectral data (port 9880)
|
|
@@ -144,10 +145,8 @@ async def lifespan(server):
|
|
|
144
145
|
ableton.disconnect()
|
|
145
146
|
try:
|
|
146
147
|
await splice_client.disconnect()
|
|
147
|
-
except Exception:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
logger.debug("lifespan failed: %s", exc)
|
|
151
150
|
mcp = FastMCP("LivePilot", lifespan=lifespan)
|
|
152
151
|
|
|
153
152
|
# Import tool modules so they register with `mcp`
|
|
@@ -199,7 +198,9 @@ from .device_forge import tools as device_forge_tools # noqa: F401, E40
|
|
|
199
198
|
from .sample_engine import tools as sample_engine_tools # noqa: F401, E402
|
|
200
199
|
from .atlas import tools as atlas_tools # noqa: F401, E402
|
|
201
200
|
from .composer import tools as composer_tools # noqa: F401, E402
|
|
201
|
+
import logging
|
|
202
202
|
|
|
203
|
+
logger = logging.getLogger(__name__)
|
|
203
204
|
|
|
204
205
|
# ---------------------------------------------------------------------------
|
|
205
206
|
# Schema coercion patch — accept strings for numeric parameters
|
|
@@ -213,6 +214,7 @@ from .composer import tools as composer_tools # noqa: F401, E40
|
|
|
213
214
|
# "5" → 5 and "0.75" → 0.75 automatically, so no tool code changes needed.
|
|
214
215
|
# ---------------------------------------------------------------------------
|
|
215
216
|
|
|
217
|
+
|
|
216
218
|
def _coerce_schema_property(prop: dict) -> None:
|
|
217
219
|
"""Widen a single JSON Schema property to also accept strings."""
|
|
218
220
|
if prop.get("type") in ("integer", "number") and "anyOf" not in prop:
|
|
@@ -253,6 +255,7 @@ def _get_all_tools():
|
|
|
253
255
|
if hasattr(mcp, "_local_provider") and hasattr(mcp._local_provider, "_components"):
|
|
254
256
|
return list(mcp._local_provider._components.values())
|
|
255
257
|
import sys
|
|
258
|
+
|
|
256
259
|
print(
|
|
257
260
|
"LivePilot: WARNING — could not access FastMCP tool registry, "
|
|
258
261
|
"string-to-number schema coercion will not work",
|
|
@@ -273,7 +276,6 @@ def _patch_tool_schemas() -> None:
|
|
|
273
276
|
if isinstance(definition, dict):
|
|
274
277
|
_coerce_schema_property(definition)
|
|
275
278
|
|
|
276
|
-
|
|
277
279
|
_patch_tool_schemas()
|
|
278
280
|
|
|
279
281
|
|
|
@@ -281,6 +283,5 @@ def main():
|
|
|
281
283
|
"""Run the MCP server over stdio."""
|
|
282
284
|
mcp.run(transport="stdio")
|
|
283
285
|
|
|
284
|
-
|
|
285
286
|
if __name__ == "__main__":
|
|
286
287
|
main()
|
|
@@ -9,6 +9,9 @@ Pure computation — no I/O. Callers provide pre-fetched data.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
from typing import Optional
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
def get_motif_data(
|
|
@@ -28,13 +31,15 @@ def get_motif_data(
|
|
|
28
31
|
|
|
29
32
|
try:
|
|
30
33
|
from ..tools import _motif_engine as motif_engine
|
|
34
|
+
|
|
31
35
|
motifs = motif_engine.detect_motifs(notes_by_track)
|
|
32
36
|
return {
|
|
33
37
|
"motifs": [m.to_dict() for m in motifs],
|
|
34
38
|
"motif_count": len(motifs),
|
|
35
39
|
"tracks_analyzed": len(notes_by_track),
|
|
36
40
|
}
|
|
37
|
-
except Exception:
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
logger.debug("get_motif_data failed: %s", exc)
|
|
38
43
|
return {"motifs": [], "motif_count": 0, "tracks_analyzed": 0}
|
|
39
44
|
|
|
40
45
|
|
|
@@ -60,8 +65,9 @@ def fetch_notes_from_ableton(ableton, tracks: list[dict], max_clips: int = 8) ->
|
|
|
60
65
|
"clip_index": clip_idx,
|
|
61
66
|
})
|
|
62
67
|
track_notes.extend(result.get("notes", []))
|
|
63
|
-
except Exception:
|
|
64
|
-
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
logger.debug("fetch_notes_from_ableton failed: %s", exc)
|
|
70
|
+
|
|
65
71
|
if track_notes:
|
|
66
72
|
notes_by_track[t_idx] = track_notes
|
|
67
73
|
return notes_by_track
|