livepilot 1.18.2 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +248 -0
- package/README.md +7 -7
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/creative_director/__init__.py +21 -0
- package/mcp_server/creative_director/compliance.py +263 -0
- package/mcp_server/creative_director/hybrid.py +429 -0
- package/mcp_server/creative_director/tools.py +135 -0
- package/mcp_server/experiment/baseline.py +138 -0
- package/mcp_server/experiment/engine.py +20 -0
- package/mcp_server/experiment/models.py +9 -1
- package/mcp_server/experiment/tools.py +22 -0
- package/mcp_server/server.py +1 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/server.json +3 -3
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Experiment baseline transport state — capture once, restore between branches.
|
|
2
|
+
|
|
3
|
+
v1.19 Item A: running N branches sequentially produces inconsistent
|
|
4
|
+
``before_snapshot`` values because playback position, mute/solo/arm, and
|
|
5
|
+
playing-clip state drift across branches. ``undo()`` reverts command
|
|
6
|
+
history but doesn't guarantee transport state is identical at the start
|
|
7
|
+
of each branch's capture window.
|
|
8
|
+
|
|
9
|
+
Flow in ``run_experiment``:
|
|
10
|
+
|
|
11
|
+
1. Before the first branch: ``capture_baseline(ableton)`` and stash on
|
|
12
|
+
the :class:`ExperimentSet`.
|
|
13
|
+
2. Between branches (before capturing the next before_snapshot): call
|
|
14
|
+
``restore_baseline(ableton, baseline)`` to stop transport, reset
|
|
15
|
+
mute/solo/arm, and pause briefly for meters to settle.
|
|
16
|
+
|
|
17
|
+
The module is deliberately thin — no LOM subscription, no state
|
|
18
|
+
monitoring. Just a snapshot dataclass + two functions.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import time
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class BaselineTransportState:
|
|
33
|
+
"""Transport + per-track state captured before the first experiment branch.
|
|
34
|
+
|
|
35
|
+
Kept deliberately shallow: we don't try to freeze automation or scene
|
|
36
|
+
state. Those are out of scope (plan §2 "What NOT to do" — automation
|
|
37
|
+
drift is an accepted limitation).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
is_playing: bool = False
|
|
41
|
+
song_time: float = 0.0
|
|
42
|
+
track_states: list[dict] = field(default_factory=list)
|
|
43
|
+
captured_at_ms: int = 0
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict:
|
|
46
|
+
return {
|
|
47
|
+
"is_playing": self.is_playing,
|
|
48
|
+
"song_time": self.song_time,
|
|
49
|
+
"track_states": list(self.track_states),
|
|
50
|
+
"captured_at_ms": self.captured_at_ms,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def capture_baseline(ableton) -> BaselineTransportState:
|
|
55
|
+
"""Capture current transport + per-track state.
|
|
56
|
+
|
|
57
|
+
Uses ``get_session_info`` (single round-trip for all fields we need).
|
|
58
|
+
Returns a frozen-in-time snapshot; subsequent state drift doesn't
|
|
59
|
+
affect it.
|
|
60
|
+
"""
|
|
61
|
+
info = ableton.send_command("get_session_info")
|
|
62
|
+
if not isinstance(info, dict):
|
|
63
|
+
info = {}
|
|
64
|
+
|
|
65
|
+
tracks = info.get("tracks") or []
|
|
66
|
+
track_states: list[dict] = []
|
|
67
|
+
for i, t in enumerate(tracks):
|
|
68
|
+
if not isinstance(t, dict):
|
|
69
|
+
continue
|
|
70
|
+
track_states.append({
|
|
71
|
+
"index": int(t.get("index", i)),
|
|
72
|
+
"mute": bool(t.get("mute", False)),
|
|
73
|
+
"solo": bool(t.get("solo", False)),
|
|
74
|
+
"arm": bool(t.get("arm", False)),
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return BaselineTransportState(
|
|
78
|
+
is_playing=bool(info.get("is_playing", False)),
|
|
79
|
+
song_time=float(info.get("current_song_time", 0.0) or 0.0),
|
|
80
|
+
track_states=track_states,
|
|
81
|
+
captured_at_ms=int(time.time() * 1000),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def restore_baseline(
|
|
86
|
+
ableton,
|
|
87
|
+
baseline: BaselineTransportState,
|
|
88
|
+
stabilize_ms: int = 300,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Restore transport + per-track state to the captured baseline.
|
|
91
|
+
|
|
92
|
+
Sequence:
|
|
93
|
+
1. ``stop_playback`` (halt transport — also stops any live clips)
|
|
94
|
+
2. For each track: ``set_track_mute`` / ``set_track_solo`` /
|
|
95
|
+
``set_track_arm`` (best-effort; per-track failure is logged,
|
|
96
|
+
not fatal — a single flaky track should never abort restore
|
|
97
|
+
for the rest).
|
|
98
|
+
3. Sleep ``stabilize_ms`` milliseconds so meters settle before the
|
|
99
|
+
next ``before_snapshot`` reads them. Pass ``0`` in tests.
|
|
100
|
+
|
|
101
|
+
We deliberately do NOT seek the transport to ``baseline.song_time``.
|
|
102
|
+
Returning from stopped transport is enough — re-seeking a stopped
|
|
103
|
+
transport is equivalent to not moving, and on a playing transport it
|
|
104
|
+
introduces its own stutter artefact. If a future branch needs timeline
|
|
105
|
+
position consistency, add a ``jump_to_time`` call here.
|
|
106
|
+
|
|
107
|
+
Return-track arms are skipped — ``set_track_arm`` on a negative index
|
|
108
|
+
raises (return tracks aren't armable in Live).
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
ableton.send_command("stop_playback")
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
logger.debug("restore_baseline stop_playback failed: %s", exc)
|
|
114
|
+
|
|
115
|
+
for ts in baseline.track_states:
|
|
116
|
+
idx = ts.get("index", -1)
|
|
117
|
+
try:
|
|
118
|
+
ableton.send_command("set_track_mute", {
|
|
119
|
+
"track_index": idx, "mute": bool(ts.get("mute", False)),
|
|
120
|
+
})
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
logger.debug("restore_baseline set_track_mute(%s) failed: %s", idx, exc)
|
|
123
|
+
try:
|
|
124
|
+
ableton.send_command("set_track_solo", {
|
|
125
|
+
"track_index": idx, "solo": bool(ts.get("solo", False)),
|
|
126
|
+
})
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
logger.debug("restore_baseline set_track_solo(%s) failed: %s", idx, exc)
|
|
129
|
+
if idx >= 0:
|
|
130
|
+
try:
|
|
131
|
+
ableton.send_command("set_track_arm", {
|
|
132
|
+
"track_index": idx, "arm": bool(ts.get("arm", False)),
|
|
133
|
+
})
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
logger.debug("restore_baseline set_track_arm(%s) failed: %s", idx, exc)
|
|
136
|
+
|
|
137
|
+
if stabilize_ms > 0:
|
|
138
|
+
time.sleep(stabilize_ms / 1000.0)
|
|
@@ -445,3 +445,23 @@ def discard_experiment(experiment_id: str) -> dict:
|
|
|
445
445
|
exp.status = "discarded"
|
|
446
446
|
|
|
447
447
|
return {"discarded": True, "experiment_id": experiment_id}
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# ── v1.19 Item A — between-branch baseline restore ───────────────────────────
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def prepare_for_next_branch(ableton, baseline, stabilize_ms: int = 300) -> None:
|
|
454
|
+
"""Restore baseline transport state before capturing the next branch.
|
|
455
|
+
|
|
456
|
+
Called by ``run_experiment`` between branches so each branch's
|
|
457
|
+
``before_snapshot`` reads from identical starting conditions. No-op
|
|
458
|
+
when ``baseline`` is None (first branch — the baseline was just
|
|
459
|
+
captured, no drift to correct).
|
|
460
|
+
|
|
461
|
+
Thin wrapper around ``baseline.restore_baseline``; exists so the
|
|
462
|
+
MCP tool body stays small and the wiring is testable in isolation.
|
|
463
|
+
"""
|
|
464
|
+
if baseline is None:
|
|
465
|
+
return
|
|
466
|
+
from .baseline import restore_baseline
|
|
467
|
+
restore_baseline(ableton, baseline, stabilize_ms=stabilize_ms)
|
|
@@ -19,6 +19,7 @@ from dataclasses import dataclass, field
|
|
|
19
19
|
from typing import Any, Optional
|
|
20
20
|
|
|
21
21
|
from ..branches import BranchSeed
|
|
22
|
+
from .baseline import BaselineTransportState
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
@dataclass
|
|
@@ -249,6 +250,10 @@ class ExperimentSet:
|
|
|
249
250
|
status: str = "open" # open | evaluated | committed | discarded
|
|
250
251
|
winner_branch_id: Optional[str] = None
|
|
251
252
|
created_at_ms: int = 0
|
|
253
|
+
# v1.19 Item A — transport state captured before the first branch runs
|
|
254
|
+
# and used to restore identical starting conditions between branches.
|
|
255
|
+
# See mcp_server.experiment.baseline for the snapshot / restore pair.
|
|
256
|
+
baseline_transport: Optional[BaselineTransportState] = None
|
|
252
257
|
|
|
253
258
|
@property
|
|
254
259
|
def branch_count(self) -> int:
|
|
@@ -290,7 +295,7 @@ class ExperimentSet:
|
|
|
290
295
|
return _branch_rank_key(branch)
|
|
291
296
|
|
|
292
297
|
def to_dict(self) -> dict:
|
|
293
|
-
|
|
298
|
+
d = {
|
|
294
299
|
"experiment_id": self.experiment_id,
|
|
295
300
|
"request_text": self.request_text,
|
|
296
301
|
"status": self.status,
|
|
@@ -302,3 +307,6 @@ class ExperimentSet:
|
|
|
302
307
|
for b in self.ranked_branches()
|
|
303
308
|
],
|
|
304
309
|
}
|
|
310
|
+
if self.baseline_transport is not None:
|
|
311
|
+
d["baseline_transport"] = self.baseline_transport.to_dict()
|
|
312
|
+
return d
|
|
@@ -343,11 +343,33 @@ async def run_experiment(
|
|
|
343
343
|
# Import compiler
|
|
344
344
|
from ..semantic_moves import registry, compiler
|
|
345
345
|
|
|
346
|
+
# v1.19 Item A — capture baseline transport state BEFORE any branch runs.
|
|
347
|
+
# Each branch's before_snapshot is only comparable if it starts from the
|
|
348
|
+
# same reference state. Without this, live testing (v1.18.0 Test 8) showed
|
|
349
|
+
# 3 branches produce wildly inconsistent before_snapshot.track_meters[0].level
|
|
350
|
+
# values — clip stopped mid-experiment between branches.
|
|
351
|
+
if experiment.baseline_transport is None:
|
|
352
|
+
from .baseline import capture_baseline
|
|
353
|
+
try:
|
|
354
|
+
experiment.baseline_transport = capture_baseline(ableton)
|
|
355
|
+
except Exception as exc:
|
|
356
|
+
logger.debug("baseline capture failed: %s", exc)
|
|
357
|
+
experiment.baseline_transport = None
|
|
358
|
+
|
|
346
359
|
results = []
|
|
360
|
+
pending_seen = 0
|
|
347
361
|
for branch in experiment.branches:
|
|
348
362
|
if branch.status != "pending":
|
|
349
363
|
continue
|
|
350
364
|
|
|
365
|
+
# Between branches (not before the first), restore the baseline so
|
|
366
|
+
# the next before_snapshot reads from the same reference state.
|
|
367
|
+
if pending_seen > 0:
|
|
368
|
+
engine.prepare_for_next_branch(
|
|
369
|
+
ableton, experiment.baseline_transport, stabilize_ms=300,
|
|
370
|
+
)
|
|
371
|
+
pending_seen += 1
|
|
372
|
+
|
|
351
373
|
# PR3: respect a pre-existing compiled_plan on the branch (freeform /
|
|
352
374
|
# synthesis / composer producers bring their own). Only compile from
|
|
353
375
|
# move_id when the branch arrived without a plan — which requires a
|
package/mcp_server/server.py
CHANGED
|
@@ -301,6 +301,7 @@ from .stuckness_detector import tools as stuckness_tools # noqa: F401, E40
|
|
|
301
301
|
from .wonder_mode import tools as wonder_mode_tools # noqa: F401, E402
|
|
302
302
|
from .session_continuity import tools as session_cont_tools # noqa: F401, E402
|
|
303
303
|
from .creative_constraints import tools as constraints_tools # noqa: F401, E402
|
|
304
|
+
from .creative_director import tools as creative_director_tools # noqa: F401, E402
|
|
304
305
|
from .device_forge import tools as device_forge_tools # noqa: F401, E402
|
|
305
306
|
from .sample_engine import tools as sample_engine_tools # noqa: F401, E402
|
|
306
307
|
from .atlas import tools as atlas_tools # noqa: F401, E402
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
|
-
"description": "Agentic production system for Ableton Live 12 —
|
|
5
|
+
"description": "Agentic production system for Ableton Live 12 — 429 tools, 53 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 engines)",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
7
|
"license": "BSL-1.1",
|
|
8
8
|
"type": "commonjs",
|
|
@@ -5,7 +5,7 @@ Entry point for the ControlSurface. Ableton calls create_instance(c_instance)
|
|
|
5
5
|
when this script is selected in Preferences > Link, Tempo & MIDI.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "1.
|
|
8
|
+
__version__ = "1.19.0"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from . import router
|
package/server.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.dreamrec/livepilot",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "429-tool agentic MCP production system for Ableton Live 12 — device atlas, sample engine, composer",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/dreamrec/LivePilot",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.19.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "livepilot",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.19.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|