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
|
@@ -43,7 +43,8 @@ def _require_analyzer(cache) -> None:
|
|
|
43
43
|
ctx.lifespan_context["ableton"].send_command("get_master_track")
|
|
44
44
|
if ctx else {}
|
|
45
45
|
)
|
|
46
|
-
except Exception:
|
|
46
|
+
except Exception as exc:
|
|
47
|
+
logger.debug("_require_analyzer failed: %s", exc)
|
|
47
48
|
track = {}
|
|
48
49
|
|
|
49
50
|
devices = track.get("devices", []) if isinstance(track, dict) else []
|
|
@@ -254,7 +255,6 @@ async def walk_device_tree(
|
|
|
254
255
|
bridge = _get_m4l(ctx)
|
|
255
256
|
return await bridge.send_command("walk_rack", track_index, device_index)
|
|
256
257
|
|
|
257
|
-
|
|
258
258
|
# ── Phase 2: Sample Operations ─────────────────────────────────────────
|
|
259
259
|
|
|
260
260
|
|
|
@@ -276,6 +276,130 @@ async def get_clip_file_path(
|
|
|
276
276
|
bridge = _get_m4l(ctx)
|
|
277
277
|
return await bridge.send_command("get_clip_file_path", track_index, clip_index)
|
|
278
278
|
|
|
279
|
+
import os # for filename parsing in smart-defaults helper
|
|
280
|
+
import re
|
|
281
|
+
import logging
|
|
282
|
+
|
|
283
|
+
logger = logging.getLogger(__name__)
|
|
284
|
+
|
|
285
|
+
# ── Sample loading helpers (P0-1, P1-1, P2-6 fixes) ────────────────────────
|
|
286
|
+
#
|
|
287
|
+
# Critical bug 2026-04-14 (see docs/2026-04-14-bugs-discovered.md):
|
|
288
|
+
#
|
|
289
|
+
# The M4L bridge's `replace_simpler_sample` command can report success even
|
|
290
|
+
# when the sample is still the bootstrap placeholder. The Simpler's display
|
|
291
|
+
# name also does NOT refresh after a replace. After loading, Simpler's Snap
|
|
292
|
+
# parameter is ON by default which causes the Sample Start position to
|
|
293
|
+
# snap to a location outside the new sample's valid audio — resulting in
|
|
294
|
+
# silent playback.
|
|
295
|
+
#
|
|
296
|
+
# The fixes below:
|
|
297
|
+
# 1. After replace, verify by reading the actual device name via
|
|
298
|
+
# get_track_info and comparing to the expected filename stem. If the
|
|
299
|
+
# name doesn't match, return a clear error so the caller doesn't
|
|
300
|
+
# silently ship the wrong audio.
|
|
301
|
+
# 2. Auto-set Snap=0 to disarm the zero-crossing snap that breaks playback.
|
|
302
|
+
# 3. For WARPED LOOPS (detected by "NNbpm" in the filename), set
|
|
303
|
+
# S Start=0, S Length=1, S Loop On=1 so the full loop plays in its
|
|
304
|
+
# musical phrasing. For ONE-SHOTS, leave defaults alone.
|
|
305
|
+
|
|
306
|
+
_BPM_IN_FILENAME_RE = re.compile(r"(\d{2,3})\s*bpm", re.IGNORECASE)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _is_warped_loop(file_path: str) -> bool:
|
|
310
|
+
"""Return True if the filename contains a BPM marker (likely a tempo-locked loop)."""
|
|
311
|
+
stem = os.path.splitext(os.path.basename(file_path))[0]
|
|
312
|
+
return bool(_BPM_IN_FILENAME_RE.search(stem))
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _filename_stem(file_path: str) -> str:
|
|
316
|
+
return os.path.splitext(os.path.basename(file_path))[0]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
async def _simpler_post_load_hygiene(
|
|
320
|
+
bridge,
|
|
321
|
+
ableton,
|
|
322
|
+
track_index: int,
|
|
323
|
+
device_index: int,
|
|
324
|
+
file_path: str,
|
|
325
|
+
) -> dict:
|
|
326
|
+
"""Apply post-load hygiene to a newly loaded Simpler and verify success.
|
|
327
|
+
|
|
328
|
+
Steps:
|
|
329
|
+
1. Read track info to verify the device's actual name matches the
|
|
330
|
+
expected sample stem. If it doesn't, return an error.
|
|
331
|
+
2. Set Snap=0 (Off) — required so sample playback works.
|
|
332
|
+
3. If filename indicates a warped loop, set S Start=0, S Length=1,
|
|
333
|
+
S Loop On=1 so the loop plays fully instead of being cropped.
|
|
334
|
+
4. Return a verified response dict.
|
|
335
|
+
"""
|
|
336
|
+
expected_stem = _filename_stem(file_path)
|
|
337
|
+
|
|
338
|
+
# Step 1: verify device name matches expected file
|
|
339
|
+
try:
|
|
340
|
+
track_info = ableton.send_command(
|
|
341
|
+
"get_track_info", {"track_index": track_index}
|
|
342
|
+
)
|
|
343
|
+
except Exception as exc:
|
|
344
|
+
return {"error": f"Verification read failed: {exc}"}
|
|
345
|
+
|
|
346
|
+
devices = track_info.get("devices", []) or []
|
|
347
|
+
if device_index < 0 or device_index >= len(devices):
|
|
348
|
+
return {
|
|
349
|
+
"error": (
|
|
350
|
+
f"Device index {device_index} out of range after load "
|
|
351
|
+
f"(track has {len(devices)} devices)"
|
|
352
|
+
),
|
|
353
|
+
"verified": False,
|
|
354
|
+
}
|
|
355
|
+
device = devices[device_index]
|
|
356
|
+
actual_name = str(device.get("name") or "")
|
|
357
|
+
verified = expected_stem in actual_name or actual_name in expected_stem
|
|
358
|
+
if not verified:
|
|
359
|
+
return {
|
|
360
|
+
"error": (
|
|
361
|
+
f"Sample verification FAILED — Simpler name '{actual_name}' "
|
|
362
|
+
f"does not match requested file '{expected_stem}'. The bridge "
|
|
363
|
+
f"reported success but the actual sample is different. "
|
|
364
|
+
f"Try `load_browser_item` with a user_library URI instead."
|
|
365
|
+
),
|
|
366
|
+
"verified": False,
|
|
367
|
+
"actual_device_name": actual_name,
|
|
368
|
+
"expected_stem": expected_stem,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# Step 2: turn Snap OFF — required for reliable playback after replace
|
|
372
|
+
hygiene_params: list[dict] = [
|
|
373
|
+
{"name_or_index": "Snap", "value": 0},
|
|
374
|
+
]
|
|
375
|
+
|
|
376
|
+
# Step 3: smart defaults for warped loops
|
|
377
|
+
if _is_warped_loop(file_path):
|
|
378
|
+
hygiene_params.extend([
|
|
379
|
+
{"name_or_index": "S Start", "value": 0.0},
|
|
380
|
+
{"name_or_index": "S Length", "value": 1.0},
|
|
381
|
+
{"name_or_index": "S Loop On", "value": 1},
|
|
382
|
+
])
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
ableton.send_command("batch_set_parameters", {
|
|
386
|
+
"track_index": track_index,
|
|
387
|
+
"device_index": device_index,
|
|
388
|
+
"parameters": hygiene_params,
|
|
389
|
+
})
|
|
390
|
+
except Exception as exc:
|
|
391
|
+
logger.debug("_simpler_post_load_hygiene failed: %s", exc)
|
|
392
|
+
# non-fatal — verification already succeeded
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
"verified": True,
|
|
397
|
+
"device_name": actual_name,
|
|
398
|
+
"track_index": track_index,
|
|
399
|
+
"device_index": device_index,
|
|
400
|
+
"warped_loop_defaults_applied": _is_warped_loop(file_path),
|
|
401
|
+
}
|
|
402
|
+
|
|
279
403
|
|
|
280
404
|
@mcp.tool()
|
|
281
405
|
async def replace_simpler_sample(
|
|
@@ -292,6 +416,17 @@ async def replace_simpler_sample(
|
|
|
292
416
|
manually first or use find_and_load_device to load a preset that already
|
|
293
417
|
contains a sample.
|
|
294
418
|
|
|
419
|
+
**Prefer `load_browser_item(track, uri)` when possible** — see P0-1 in
|
|
420
|
+
docs/2026-04-14-bugs-discovered.md. The M4L bridge's replace path can
|
|
421
|
+
silently keep the bootstrap placeholder in some conditions; this tool
|
|
422
|
+
now verifies by reading back the device name and will return an error
|
|
423
|
+
if the replace didn't actually take effect.
|
|
424
|
+
|
|
425
|
+
Also auto-applies post-load hygiene:
|
|
426
|
+
- Sets Simpler Snap=0 (required for playback after replace)
|
|
427
|
+
- For warped loops (filename contains 'NNbpm'), sets S Start=0,
|
|
428
|
+
S Length=1, S Loop On=1
|
|
429
|
+
|
|
295
430
|
Use get_clip_file_path to get the path of a resampled clip, then pass
|
|
296
431
|
it here to load it into Simpler for slicing.
|
|
297
432
|
Requires LivePilot Analyzer on master track.
|
|
@@ -299,6 +434,7 @@ async def replace_simpler_sample(
|
|
|
299
434
|
cache = _get_spectral(ctx)
|
|
300
435
|
_require_analyzer(cache)
|
|
301
436
|
bridge = _get_m4l(ctx)
|
|
437
|
+
ableton = ctx.lifespan_context["ableton"]
|
|
302
438
|
result = await bridge.send_command(
|
|
303
439
|
"replace_simpler_sample", track_index, device_index, file_path
|
|
304
440
|
)
|
|
@@ -312,6 +448,16 @@ async def replace_simpler_sample(
|
|
|
312
448
|
"error": "Sample may not have loaded. Ensure the Simpler already "
|
|
313
449
|
"has a sample loaded — replace_sample silently fails on empty Simplers."
|
|
314
450
|
}
|
|
451
|
+
|
|
452
|
+
# Verify by reading back the device name — guards against the silent
|
|
453
|
+
# failure mode where the bridge reports success but keeps the placeholder.
|
|
454
|
+
hygiene = await _simpler_post_load_hygiene(
|
|
455
|
+
bridge, ableton, track_index, device_index, file_path
|
|
456
|
+
)
|
|
457
|
+
if not hygiene.get("verified"):
|
|
458
|
+
return hygiene
|
|
459
|
+
|
|
460
|
+
result.update(hygiene)
|
|
315
461
|
return result
|
|
316
462
|
|
|
317
463
|
|
|
@@ -327,10 +473,18 @@ async def load_sample_to_simpler(
|
|
|
327
473
|
This is the full workflow for programmatic sample loading:
|
|
328
474
|
1. Loads a dummy sample via the browser (creates Simpler with a sample)
|
|
329
475
|
2. Replaces the dummy with your audio file
|
|
330
|
-
3.
|
|
476
|
+
3. Applies post-load hygiene (Snap=0, loop defaults for warped loops)
|
|
477
|
+
4. Verifies by reading back the device name — returns an error if
|
|
478
|
+
the Simpler still has the bootstrap placeholder (P0-1 guard)
|
|
331
479
|
|
|
332
480
|
Use this instead of replace_simpler_sample when the track has no Simpler
|
|
333
481
|
or the Simpler is empty. Works with any audio file path.
|
|
482
|
+
|
|
483
|
+
**For files that exist in Ableton's browser index** (Samples, User Library,
|
|
484
|
+
Packs), PREFER `load_browser_item(track, uri)` — it goes through Ableton's
|
|
485
|
+
native loading path and is more reliable. This tool is a workaround for
|
|
486
|
+
files that aren't browser-indexed.
|
|
487
|
+
|
|
334
488
|
Requires LivePilot Analyzer on master track.
|
|
335
489
|
"""
|
|
336
490
|
cache = _get_spectral(ctx)
|
|
@@ -380,6 +534,14 @@ async def load_sample_to_simpler(
|
|
|
380
534
|
if not result.get("sample_loaded"):
|
|
381
535
|
return {"error": "Sample replacement failed after bootstrap"}
|
|
382
536
|
|
|
537
|
+
# Step 4: Verify by reading back the device name (P0-1 guard)
|
|
538
|
+
hygiene = await _simpler_post_load_hygiene(
|
|
539
|
+
bridge, ableton, track_index, actual_device_index, file_path
|
|
540
|
+
)
|
|
541
|
+
if not hygiene.get("verified"):
|
|
542
|
+
return hygiene
|
|
543
|
+
|
|
544
|
+
result.update(hygiene)
|
|
383
545
|
result["method"] = "bootstrap_and_replace"
|
|
384
546
|
result["device_index"] = actual_device_index # additive — for step-result binding
|
|
385
547
|
result["track_index"] = track_index
|
|
@@ -457,7 +619,6 @@ async def warp_simpler(
|
|
|
457
619
|
bridge = _get_m4l(ctx)
|
|
458
620
|
return await bridge.send_command("warp_simpler", track_index, device_index, beats)
|
|
459
621
|
|
|
460
|
-
|
|
461
622
|
# ── Phase 2: Warp Markers ──────────────────────────────────────────────
|
|
462
623
|
|
|
463
624
|
|
|
@@ -542,7 +703,6 @@ async def remove_warp_marker(
|
|
|
542
703
|
"remove_warp_marker", track_index, clip_index, beat_time
|
|
543
704
|
)
|
|
544
705
|
|
|
545
|
-
|
|
546
706
|
# ── Phase 2: Clip & Display ────────────────────────────────────────────
|
|
547
707
|
|
|
548
708
|
|
|
@@ -600,7 +760,6 @@ async def get_display_values(
|
|
|
600
760
|
bridge = _get_m4l(ctx)
|
|
601
761
|
return await bridge.send_command("get_display_values", track_index, device_index, timeout=15.0)
|
|
602
762
|
|
|
603
|
-
|
|
604
763
|
# ── Phase 3: Audio Capture ─────────────────────────────────────────────
|
|
605
764
|
|
|
606
765
|
|
|
@@ -659,6 +818,7 @@ async def capture_audio(
|
|
|
659
818
|
dst_path = os.path.join(CAPTURE_DIR, dst_name)
|
|
660
819
|
try:
|
|
661
820
|
import shutil
|
|
821
|
+
|
|
662
822
|
shutil.move(src_path, dst_path)
|
|
663
823
|
result["file_path"] = dst_path
|
|
664
824
|
except OSError:
|
|
@@ -684,7 +844,6 @@ async def capture_stop(ctx: Context) -> dict:
|
|
|
684
844
|
await bridge.cancel_capture_future()
|
|
685
845
|
return await bridge.send_command("capture_stop")
|
|
686
846
|
|
|
687
|
-
|
|
688
847
|
# ── Phase 4: FluCoMa Real-Time ───────────────────────────────────────────
|
|
689
848
|
|
|
690
849
|
PITCH_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
|
@@ -14,6 +14,9 @@ from pydantic import BaseModel, Field
|
|
|
14
14
|
|
|
15
15
|
from ..curves import generate_curve, generate_from_recipe, list_recipes
|
|
16
16
|
from ..server import mcp
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
def _get_ableton(ctx: Context):
|
|
@@ -23,6 +26,7 @@ def _get_ableton(ctx: Context):
|
|
|
23
26
|
def _ensure_list(v: Any) -> list:
|
|
24
27
|
if isinstance(v, str):
|
|
25
28
|
import json
|
|
29
|
+
|
|
26
30
|
try:
|
|
27
31
|
return json.loads(v)
|
|
28
32
|
except json.JSONDecodeError as exc:
|
|
@@ -372,7 +376,8 @@ def apply_automation_recipe(
|
|
|
372
376
|
if abs(p_max - p_min) > 1.5 or p_min < -0.5:
|
|
373
377
|
for pt in points:
|
|
374
378
|
pt["value"] = p_min + pt["value"] * (p_max - p_min)
|
|
375
|
-
except Exception:
|
|
379
|
+
except Exception as exc:
|
|
380
|
+
logger.debug("apply_automation_recipe failed: %s", exc)
|
|
376
381
|
pass # Fail open — write values as-is if we can't read the range
|
|
377
382
|
|
|
378
383
|
# Safety clamp: auto_pan amplitude should be limited to avoid full L/R swing
|
|
@@ -14,6 +14,7 @@ These tools power the composition intelligence layer:
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import json
|
|
17
|
+
import logging
|
|
17
18
|
from typing import Optional
|
|
18
19
|
|
|
19
20
|
from fastmcp import Context
|
|
@@ -22,6 +23,8 @@ from ..server import mcp
|
|
|
22
23
|
from ..memory.technique_store import TechniqueStore
|
|
23
24
|
from . import _composition_engine as engine
|
|
24
25
|
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
25
28
|
_memory_store = TechniqueStore()
|
|
26
29
|
|
|
27
30
|
|
|
@@ -49,7 +52,8 @@ def _build_clip_matrix(ableton, scene_count: int, track_count: int) -> list[list
|
|
|
49
52
|
matrix_data = ableton.send_command("get_scene_matrix")
|
|
50
53
|
raw_matrix = matrix_data.get("matrix", [])
|
|
51
54
|
return raw_matrix
|
|
52
|
-
except Exception:
|
|
55
|
+
except Exception as exc:
|
|
56
|
+
logger.warning("get_scene_matrix failed, using empty matrix: %s", exc)
|
|
53
57
|
return [[] for _ in range(scene_count)]
|
|
54
58
|
|
|
55
59
|
|
|
@@ -95,8 +99,8 @@ def analyze_composition(ctx: Context) -> dict:
|
|
|
95
99
|
clips = arr.get("clips", [])
|
|
96
100
|
if clips:
|
|
97
101
|
arr_clips[track["index"]] = clips
|
|
98
|
-
except Exception:
|
|
99
|
-
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
logger.debug("arrangement_clips track=%s skipped: %s", track.get("index"), exc)
|
|
100
104
|
|
|
101
105
|
if not sections and arr_clips:
|
|
102
106
|
sections = engine.build_section_graph_from_arrangement(
|
|
@@ -111,7 +115,8 @@ def analyze_composition(ctx: Context) -> dict:
|
|
|
111
115
|
"track_index": track["index"]
|
|
112
116
|
})
|
|
113
117
|
track_data.append(ti)
|
|
114
|
-
except Exception:
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
logger.debug("get_track_info track=%s fallback: %s", track.get("index"), exc)
|
|
115
120
|
track_data.append({"index": track["index"], "name": track.get("name", ""),
|
|
116
121
|
"devices": []})
|
|
117
122
|
|
|
@@ -130,8 +135,8 @@ def analyze_composition(ctx: Context) -> dict:
|
|
|
130
135
|
})
|
|
131
136
|
notes = result.get("notes", [])
|
|
132
137
|
track_notes.extend(notes)
|
|
133
|
-
except Exception:
|
|
134
|
-
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
logger.debug("get_notes t=%d s=%d skipped: %s", t_idx, s_idx, exc)
|
|
135
140
|
all_notes_by_track[t_idx] = track_notes
|
|
136
141
|
|
|
137
142
|
# Map notes to sections
|
|
@@ -244,7 +249,8 @@ def get_phrase_grid(
|
|
|
244
249
|
"clip_index": scene_idx,
|
|
245
250
|
})
|
|
246
251
|
notes_by_track[t_idx] = result.get("notes", [])
|
|
247
|
-
except Exception:
|
|
252
|
+
except Exception as exc:
|
|
253
|
+
logger.debug("get_notes t=%d s=%d empty: %s", t_idx, scene_idx, exc)
|
|
248
254
|
notes_by_track[t_idx] = []
|
|
249
255
|
|
|
250
256
|
phrases = engine.detect_phrases(section, notes_by_track)
|
|
@@ -464,8 +470,8 @@ def get_harmony_field(
|
|
|
464
470
|
"pattern": pattern,
|
|
465
471
|
"classification": classification,
|
|
466
472
|
}
|
|
467
|
-
except Exception:
|
|
468
|
-
|
|
473
|
+
except Exception as exc:
|
|
474
|
+
logger.warning("neo-Riemannian classify failed: %s", exc)
|
|
469
475
|
|
|
470
476
|
# Populate voice_leading_info from chord groups
|
|
471
477
|
if harmony_analysis and not voice_leading_info:
|
|
@@ -483,12 +489,13 @@ def get_harmony_field(
|
|
|
483
489
|
"issue_count": len(all_vl_issues),
|
|
484
490
|
"quality": "clean" if not all_vl_issues else "has_issues",
|
|
485
491
|
}
|
|
486
|
-
except Exception:
|
|
487
|
-
|
|
492
|
+
except Exception as exc:
|
|
493
|
+
logger.warning("voice_leading analysis failed: %s", exc)
|
|
488
494
|
|
|
489
495
|
if scale_info and harmony_analysis:
|
|
490
496
|
break
|
|
491
|
-
except Exception:
|
|
497
|
+
except Exception as exc:
|
|
498
|
+
logger.debug("harmony scan on track %d skipped: %s", t_idx, exc)
|
|
492
499
|
continue
|
|
493
500
|
|
|
494
501
|
hf = engine.build_harmony_field(
|
|
@@ -535,7 +542,8 @@ def get_transition_analysis(ctx: Context) -> dict:
|
|
|
535
542
|
try:
|
|
536
543
|
ti = ableton.send_command("get_track_info", {"track_index": t_idx})
|
|
537
544
|
track_data.append(ti)
|
|
538
|
-
except Exception:
|
|
545
|
+
except Exception as exc:
|
|
546
|
+
logger.debug("get_track_info transition t=%d fallback: %s", t_idx, exc)
|
|
539
547
|
track_data.append({"index": t_idx, "name": track.get("name", ""), "devices": []})
|
|
540
548
|
|
|
541
549
|
for section in sections:
|
|
@@ -623,7 +631,8 @@ def get_section_outcomes(
|
|
|
623
631
|
techniques = _memory_store.list_techniques(
|
|
624
632
|
type_filter="composition_outcome", sort_by="updated_at", limit=limit,
|
|
625
633
|
)
|
|
626
|
-
except Exception:
|
|
634
|
+
except Exception as exc:
|
|
635
|
+
logger.warning("list_techniques(composition_outcome) failed: %s", exc)
|
|
627
636
|
techniques = []
|
|
628
637
|
|
|
629
638
|
outcomes = []
|
|
@@ -633,8 +642,8 @@ def get_section_outcomes(
|
|
|
633
642
|
payload = full.get("payload", {})
|
|
634
643
|
if isinstance(payload, dict):
|
|
635
644
|
outcomes.append(payload)
|
|
636
|
-
except Exception:
|
|
637
|
-
|
|
645
|
+
except Exception as exc:
|
|
646
|
+
logger.debug("technique %s payload read failed: %s", t.get("id"), exc)
|
|
638
647
|
|
|
639
648
|
result = engine.analyze_section_outcomes(outcomes)
|
|
640
649
|
|
|
@@ -11,6 +11,10 @@ from typing import Any, Optional
|
|
|
11
11
|
from fastmcp import Context
|
|
12
12
|
|
|
13
13
|
from ..server import mcp, _identify_port_holder
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
def _ensure_list(value: Any) -> list:
|
|
@@ -130,9 +134,9 @@ def _postflight_loaded_device(ctx: Context, result: dict) -> dict:
|
|
|
130
134
|
track_info = _get_ableton(ctx).send_command("get_track_info", {
|
|
131
135
|
"track_index": int(track_index),
|
|
132
136
|
})
|
|
133
|
-
except Exception:
|
|
137
|
+
except Exception as exc:
|
|
138
|
+
logger.debug("_postflight_loaded_device failed: %s", exc)
|
|
134
139
|
return annotated
|
|
135
|
-
|
|
136
140
|
devices = track_info.get("devices", []) if isinstance(track_info, dict) else []
|
|
137
141
|
if not isinstance(devices, list) or not devices:
|
|
138
142
|
return annotated
|
|
@@ -156,9 +160,8 @@ def _postflight_loaded_device(ctx: Context, result: dict) -> dict:
|
|
|
156
160
|
"device_index": int(match["index"]),
|
|
157
161
|
})
|
|
158
162
|
param_count = full_info.get("parameter_count", 0)
|
|
159
|
-
except Exception:
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
logger.debug("_postflight_loaded_device failed: %s", exc)
|
|
162
165
|
device_info = _annotate_device_info({
|
|
163
166
|
"name": match.get("name"),
|
|
164
167
|
"class_name": match.get("class_name"),
|
|
@@ -597,7 +600,8 @@ def _require_analyzer(cache) -> None:
|
|
|
597
600
|
ctx.lifespan_context["ableton"].send_command("get_master_track")
|
|
598
601
|
if ctx else {}
|
|
599
602
|
)
|
|
600
|
-
except Exception:
|
|
603
|
+
except Exception as exc:
|
|
604
|
+
logger.debug("_require_analyzer failed: %s", exc)
|
|
601
605
|
track = {}
|
|
602
606
|
|
|
603
607
|
devices = track.get("devices", []) if isinstance(track, dict) else []
|
|
@@ -11,6 +11,10 @@ from fastmcp import Context
|
|
|
11
11
|
|
|
12
12
|
from ..server import mcp
|
|
13
13
|
from . import _motif_engine as motif_engine
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
def _get_ableton(ctx: Context):
|
|
@@ -46,8 +50,9 @@ def get_motif_graph(ctx: Context) -> dict:
|
|
|
46
50
|
"clip_index": clip_idx,
|
|
47
51
|
})
|
|
48
52
|
track_notes.extend(result.get("notes", []))
|
|
49
|
-
except Exception:
|
|
50
|
-
|
|
53
|
+
except Exception as exc:
|
|
54
|
+
logger.debug("get_motif_graph failed: %s", exc)
|
|
55
|
+
|
|
51
56
|
if track_notes:
|
|
52
57
|
notes_by_track[t_idx] = track_notes
|
|
53
58
|
|
|
@@ -17,6 +17,9 @@ from fastmcp import Context
|
|
|
17
17
|
from ..server import mcp
|
|
18
18
|
from . import _composition_engine as comp_engine
|
|
19
19
|
from . import _planner_engine as planner_engine
|
|
20
|
+
import logging
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
def _get_ableton(ctx: Context):
|
|
@@ -67,7 +70,8 @@ def plan_arrangement(
|
|
|
67
70
|
try:
|
|
68
71
|
ti = ableton.send_command("get_track_info", {"track_index": t_idx})
|
|
69
72
|
track_data.append(ti)
|
|
70
|
-
except Exception:
|
|
73
|
+
except Exception as exc:
|
|
74
|
+
logger.debug("plan_arrangement failed: %s", exc)
|
|
71
75
|
track_data.append({"index": t_idx, "name": track.get("name", ""), "devices": []})
|
|
72
76
|
|
|
73
77
|
for section in sections:
|
|
@@ -96,7 +100,6 @@ def plan_arrangement(
|
|
|
96
100
|
result["available_styles"] = sorted(planner_engine.VALID_STYLES)
|
|
97
101
|
return result
|
|
98
102
|
|
|
99
|
-
|
|
100
103
|
# ── transform_section (Round 4) ─────────────────────────────────────
|
|
101
104
|
|
|
102
105
|
|
|
@@ -130,6 +133,7 @@ def transform_section(
|
|
|
130
133
|
track_count = session.get("track_count", 0)
|
|
131
134
|
|
|
132
135
|
from .composition import _build_clip_matrix
|
|
136
|
+
|
|
133
137
|
clip_matrix = _build_clip_matrix(ableton, len(scenes), track_count)
|
|
134
138
|
sections = comp_engine.build_section_graph_from_scenes(scenes, clip_matrix, track_count)
|
|
135
139
|
|
|
@@ -17,6 +17,9 @@ from fastmcp import Context
|
|
|
17
17
|
from ..server import mcp
|
|
18
18
|
from ..memory.technique_store import TechniqueStore
|
|
19
19
|
from . import _research_engine as research_engine
|
|
20
|
+
import logging
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
20
23
|
|
|
21
24
|
_memory_store = TechniqueStore()
|
|
22
25
|
|
|
@@ -66,8 +69,8 @@ def research_technique(
|
|
|
66
69
|
if ref and not ref.get("error") and ref.get("count", 0) > 0:
|
|
67
70
|
device_atlas_results.append(ref)
|
|
68
71
|
break # Found in this category, skip others
|
|
69
|
-
except Exception:
|
|
70
|
-
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
logger.debug("research_technique failed: %s", exc)
|
|
71
74
|
|
|
72
75
|
# 3. Search memory for related techniques (direct TechniqueStore)
|
|
73
76
|
memory_results = []
|
|
@@ -75,16 +78,16 @@ def research_technique(
|
|
|
75
78
|
memory_results.extend(
|
|
76
79
|
_memory_store.list_techniques(type_filter="technique_card", sort_by="updated_at", limit=10)
|
|
77
80
|
)
|
|
78
|
-
except Exception:
|
|
79
|
-
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
logger.debug("research_technique failed: %s", exc)
|
|
80
83
|
|
|
81
84
|
try:
|
|
82
85
|
# "research" is not a valid type in TechniqueStore — search broadly
|
|
83
86
|
memory_results.extend(
|
|
84
87
|
_memory_store.search(query=query, limit=5)
|
|
85
88
|
)
|
|
86
|
-
except Exception:
|
|
87
|
-
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
logger.debug("research_technique failed: %s", exc)
|
|
88
91
|
|
|
89
92
|
if scope == "targeted":
|
|
90
93
|
result = research_engine.targeted_research(
|
|
@@ -159,7 +162,8 @@ def get_emotional_arc(ctx: Context) -> dict:
|
|
|
159
162
|
hf.mode = detected.get("mode", "")
|
|
160
163
|
hf.confidence = detected.get("confidence", 0.0)
|
|
161
164
|
break
|
|
162
|
-
except Exception:
|
|
165
|
+
except Exception as exc:
|
|
166
|
+
logger.debug("get_emotional_arc failed: %s", exc)
|
|
163
167
|
continue
|
|
164
168
|
harmony_fields.append(hf)
|
|
165
169
|
|
|
@@ -186,7 +190,6 @@ def get_emotional_arc(ctx: Context) -> dict:
|
|
|
186
190
|
"section_count": len(sections),
|
|
187
191
|
}
|
|
188
192
|
|
|
189
|
-
|
|
190
193
|
# ── get_style_tactics (Round 4) ─────────────────────────────────────
|
|
191
194
|
|
|
192
195
|
|
|
@@ -213,8 +216,8 @@ def get_style_tactics(
|
|
|
213
216
|
memory_tactics = _memory_store.search(
|
|
214
217
|
query=artist_or_genre, limit=10,
|
|
215
218
|
)
|
|
216
|
-
except Exception:
|
|
217
|
-
|
|
219
|
+
except Exception as exc:
|
|
220
|
+
logger.debug("get_style_tactics failed: %s", exc)
|
|
218
221
|
|
|
219
222
|
tactics = research_engine.get_style_tactics(artist_or_genre, memory_tactics)
|
|
220
223
|
|
|
@@ -14,6 +14,10 @@ from ..tools import _composition_engine as comp_engine
|
|
|
14
14
|
from .archetypes import TRANSITION_ARCHETYPES, select_archetype
|
|
15
15
|
from .critics import run_all_transition_critics
|
|
16
16
|
from .models import TransitionBoundary, TransitionPlan, TransitionScore
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
# ── Helpers ───────────────────────────────────────────────────────────
|
|
@@ -29,7 +33,8 @@ def _build_sections_from_ableton(ctx: Context) -> list[comp_engine.SectionNode]:
|
|
|
29
33
|
try:
|
|
30
34
|
matrix_data = ableton.send_command("get_scene_matrix")
|
|
31
35
|
clip_matrix = matrix_data.get("matrix", [])
|
|
32
|
-
except Exception:
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
logger.debug("_build_sections_from_ableton failed: %s", exc)
|
|
33
38
|
clip_matrix = [[] for _ in range(len(scenes))]
|
|
34
39
|
|
|
35
40
|
return comp_engine.build_section_graph_from_scenes(
|
|
@@ -33,9 +33,8 @@ def _fetch_translation_data(ctx: Context) -> dict:
|
|
|
33
33
|
spec_data = spectral.get("spectrum")
|
|
34
34
|
if spec_data and isinstance(spec_data["value"], dict):
|
|
35
35
|
spectrum_bands = spec_data["value"]
|
|
36
|
-
except Exception:
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
logger.debug("_fetch_translation_data failed: %s", exc)
|
|
39
38
|
# Estimate stereo width from track pans via session info
|
|
40
39
|
stereo_width = 0.0
|
|
41
40
|
center_strength = 0.5
|
|
@@ -61,9 +60,8 @@ def _fetch_translation_data(ctx: Context) -> dict:
|
|
|
61
60
|
|
|
62
61
|
# Simple foreground detection: at least one unmuted, non-quiet track
|
|
63
62
|
has_foreground = any(not t.get("muted", False) for t in tracks)
|
|
64
|
-
except Exception:
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
logger.debug("_get_pan failed: %s", exc)
|
|
67
65
|
return {
|
|
68
66
|
"stereo_width": stereo_width,
|
|
69
67
|
"center_strength": center_strength,
|
|
@@ -99,6 +97,10 @@ def get_translation_issues(ctx: Context) -> dict:
|
|
|
99
97
|
|
|
100
98
|
Lighter than check_translation — returns only detected issues
|
|
101
99
|
from the 5 playback robustness critics.
|
|
100
|
+
import logging
|
|
101
|
+
|
|
102
|
+
logger = logging.getLogger(__name__)
|
|
103
|
+
|
|
102
104
|
"""
|
|
103
105
|
mix_snapshot = _fetch_translation_data(ctx)
|
|
104
106
|
issues = run_all_translation_critics(mix_snapshot)
|
|
@@ -9,9 +9,12 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import hashlib
|
|
11
11
|
import json
|
|
12
|
+
import logging
|
|
12
13
|
import math
|
|
13
14
|
from typing import Optional
|
|
14
15
|
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
15
18
|
|
|
16
19
|
# ── Move discovery ───────────────────────────────────────────────
|
|
17
20
|
|
|
@@ -104,8 +107,9 @@ def discover_moves(
|
|
|
104
107
|
if validation["valid"]:
|
|
105
108
|
filtered.append(move)
|
|
106
109
|
result = filtered
|
|
107
|
-
except Exception:
|
|
108
|
-
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
# constraint filtering is optional — keep the unfiltered list
|
|
112
|
+
logger.warning("constraint filtering skipped: %s", exc)
|
|
109
113
|
|
|
110
114
|
return result
|
|
111
115
|
|
|
@@ -212,7 +216,8 @@ def _compile_variant_plan(move_dict: dict, kernel: dict | None) -> dict | None:
|
|
|
212
216
|
try:
|
|
213
217
|
plan = sem_compile(move_obj, kernel)
|
|
214
218
|
return plan.to_dict()
|
|
215
|
-
except Exception:
|
|
219
|
+
except Exception as exc:
|
|
220
|
+
logger.warning("sem_compile(%s) failed: %s", move_obj, exc)
|
|
216
221
|
return None
|
|
217
222
|
|
|
218
223
|
|