livepilot 1.10.4 → 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 +3 -3
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +148 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +6 -6
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +4 -4
- package/livepilot/skills/livepilot-core/references/overview.md +3 -3
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +5 -5
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +12 -1
- package/manifest.json +3 -3
- 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 +394 -33
- 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 +166 -7
- 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 +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/requirements.txt +6 -0
- package/livepilot.mcpb +0 -0
- package/mcp_server/tools/_agent_os_engine.py +0 -947
- package/mcp_server/tools/_composition_engine.py +0 -1530
|
@@ -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,
|
|
@@ -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
|