livepilot 1.9.13 → 1.9.15

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.
Files changed (105) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +3 -3
  3. package/CHANGELOG.md +51 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +7 -7
  6. package/bin/livepilot.js +32 -8
  7. package/installer/install.js +21 -2
  8. package/livepilot/.Codex-plugin/plugin.json +2 -2
  9. package/livepilot/.claude-plugin/plugin.json +2 -2
  10. package/livepilot/agents/livepilot-producer/AGENT.md +243 -49
  11. package/livepilot/skills/livepilot-core/SKILL.md +81 -6
  12. package/livepilot/skills/livepilot-core/references/m4l-devices.md +2 -2
  13. package/livepilot/skills/livepilot-core/references/overview.md +3 -3
  14. package/livepilot/skills/livepilot-core/references/sound-design.md +3 -2
  15. package/livepilot/skills/livepilot-release/SKILL.md +13 -13
  16. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  17. package/m4l_device/livepilot_bridge.js +6 -3
  18. package/mcp_server/__init__.py +1 -1
  19. package/mcp_server/curves.py +11 -3
  20. package/mcp_server/evaluation/__init__.py +1 -0
  21. package/mcp_server/evaluation/fabric.py +575 -0
  22. package/mcp_server/evaluation/feature_extractors.py +84 -0
  23. package/mcp_server/evaluation/policy.py +67 -0
  24. package/mcp_server/evaluation/tools.py +53 -0
  25. package/mcp_server/memory/__init__.py +11 -2
  26. package/mcp_server/memory/anti_memory.py +78 -0
  27. package/mcp_server/memory/promotion.py +94 -0
  28. package/mcp_server/memory/session_memory.py +108 -0
  29. package/mcp_server/memory/taste_memory.py +158 -0
  30. package/mcp_server/memory/technique_store.py +2 -1
  31. package/mcp_server/memory/tools.py +112 -0
  32. package/mcp_server/mix_engine/__init__.py +1 -0
  33. package/mcp_server/mix_engine/critics.py +299 -0
  34. package/mcp_server/mix_engine/models.py +152 -0
  35. package/mcp_server/mix_engine/planner.py +103 -0
  36. package/mcp_server/mix_engine/state_builder.py +316 -0
  37. package/mcp_server/mix_engine/tools.py +214 -0
  38. package/mcp_server/performance_engine/__init__.py +1 -0
  39. package/mcp_server/performance_engine/models.py +148 -0
  40. package/mcp_server/performance_engine/planner.py +267 -0
  41. package/mcp_server/performance_engine/safety.py +162 -0
  42. package/mcp_server/performance_engine/tools.py +183 -0
  43. package/mcp_server/project_brain/__init__.py +6 -0
  44. package/mcp_server/project_brain/arrangement_graph.py +64 -0
  45. package/mcp_server/project_brain/automation_graph.py +72 -0
  46. package/mcp_server/project_brain/builder.py +123 -0
  47. package/mcp_server/project_brain/capability_graph.py +64 -0
  48. package/mcp_server/project_brain/models.py +282 -0
  49. package/mcp_server/project_brain/refresh.py +80 -0
  50. package/mcp_server/project_brain/role_graph.py +103 -0
  51. package/mcp_server/project_brain/session_graph.py +51 -0
  52. package/mcp_server/project_brain/tools.py +144 -0
  53. package/mcp_server/reference_engine/__init__.py +1 -0
  54. package/mcp_server/reference_engine/gap_analyzer.py +239 -0
  55. package/mcp_server/reference_engine/models.py +105 -0
  56. package/mcp_server/reference_engine/profile_builder.py +149 -0
  57. package/mcp_server/reference_engine/tactic_router.py +117 -0
  58. package/mcp_server/reference_engine/tools.py +235 -0
  59. package/mcp_server/runtime/__init__.py +1 -0
  60. package/mcp_server/runtime/action_ledger.py +117 -0
  61. package/mcp_server/runtime/action_ledger_models.py +84 -0
  62. package/mcp_server/runtime/action_tools.py +57 -0
  63. package/mcp_server/runtime/capability_state.py +218 -0
  64. package/mcp_server/runtime/safety_kernel.py +339 -0
  65. package/mcp_server/runtime/safety_tools.py +42 -0
  66. package/mcp_server/runtime/tools.py +64 -0
  67. package/mcp_server/server.py +23 -1
  68. package/mcp_server/sound_design/__init__.py +1 -0
  69. package/mcp_server/sound_design/critics.py +297 -0
  70. package/mcp_server/sound_design/models.py +147 -0
  71. package/mcp_server/sound_design/planner.py +104 -0
  72. package/mcp_server/sound_design/tools.py +297 -0
  73. package/mcp_server/tools/_agent_os_engine.py +947 -0
  74. package/mcp_server/tools/_composition_engine.py +1530 -0
  75. package/mcp_server/tools/_conductor.py +199 -0
  76. package/mcp_server/tools/_conductor_budgets.py +222 -0
  77. package/mcp_server/tools/_evaluation_contracts.py +91 -0
  78. package/mcp_server/tools/_form_engine.py +416 -0
  79. package/mcp_server/tools/_motif_engine.py +351 -0
  80. package/mcp_server/tools/_planner_engine.py +516 -0
  81. package/mcp_server/tools/_research_engine.py +542 -0
  82. package/mcp_server/tools/_research_provider.py +185 -0
  83. package/mcp_server/tools/_snapshot_normalizer.py +49 -0
  84. package/mcp_server/tools/agent_os.py +440 -0
  85. package/mcp_server/tools/analyzer.py +18 -0
  86. package/mcp_server/tools/automation.py +25 -10
  87. package/mcp_server/tools/composition.py +563 -0
  88. package/mcp_server/tools/motif.py +104 -0
  89. package/mcp_server/tools/planner.py +144 -0
  90. package/mcp_server/tools/research.py +223 -0
  91. package/mcp_server/tools/tracks.py +18 -3
  92. package/mcp_server/tools/transport.py +10 -2
  93. package/mcp_server/transition_engine/__init__.py +6 -0
  94. package/mcp_server/transition_engine/archetypes.py +167 -0
  95. package/mcp_server/transition_engine/critics.py +340 -0
  96. package/mcp_server/transition_engine/models.py +90 -0
  97. package/mcp_server/transition_engine/tools.py +291 -0
  98. package/mcp_server/translation_engine/__init__.py +5 -0
  99. package/mcp_server/translation_engine/critics.py +297 -0
  100. package/mcp_server/translation_engine/models.py +27 -0
  101. package/mcp_server/translation_engine/tools.py +74 -0
  102. package/package.json +2 -2
  103. package/remote_script/LivePilot/__init__.py +1 -1
  104. package/remote_script/LivePilot/arrangement.py +12 -2
  105. package/requirements.txt +1 -1
@@ -0,0 +1,267 @@
1
+ """Performance Engine planner — scene transitions and energy steering.
2
+
3
+ Pure computation — builds HandoffPlans and suggests energy-aware moves.
4
+
5
+ Zero I/O.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .models import (
11
+ EnergyWindow,
12
+ HandoffPlan,
13
+ LiveSafeMove,
14
+ PerformanceState,
15
+ SceneRole,
16
+ )
17
+ from .safety import get_blocked_moves, get_safe_moves
18
+
19
+
20
+ # ── Scene transition planning ─────────────────────────────────────────
21
+
22
+
23
+ def plan_scene_transition(
24
+ from_scene: SceneRole,
25
+ to_scene: SceneRole,
26
+ ) -> HandoffPlan:
27
+ """Plan a transition from one scene to another.
28
+
29
+ Generates an energy path (linear interpolation) and
30
+ appropriate transition gestures based on the energy delta
31
+ and role pairing.
32
+ """
33
+ energy_delta = to_scene.energy_level - from_scene.energy_level
34
+ abs_delta = abs(energy_delta)
35
+
36
+ # Determine number of steps based on energy distance
37
+ if abs_delta < 0.2:
38
+ steps = 2
39
+ elif abs_delta < 0.5:
40
+ steps = 4
41
+ else:
42
+ steps = 6
43
+
44
+ # Build linear energy path
45
+ energy_path: list[float] = []
46
+ for i in range(steps + 1):
47
+ t = i / steps
48
+ energy = from_scene.energy_level + t * energy_delta
49
+ energy_path.append(round(energy, 3))
50
+
51
+ # Build gesture sequence
52
+ gestures: list[dict] = []
53
+
54
+ # Pre-transition: prepare
55
+ gestures.append({
56
+ "type": "prepare",
57
+ "step": 0,
58
+ "description": f"Prepare transition from '{from_scene.name}' to '{to_scene.name}'",
59
+ })
60
+
61
+ # Energy-based transition gestures
62
+ if energy_delta > 0.3:
63
+ # Building energy — use additive layering
64
+ gestures.append({
65
+ "type": "filter_sweep",
66
+ "step": 1,
67
+ "description": "Open filter to build energy",
68
+ "parameters": {"direction": "up"},
69
+ })
70
+ gestures.append({
71
+ "type": "send_nudge",
72
+ "step": 2,
73
+ "description": "Increase send levels for spatial build",
74
+ "parameters": {"delta": 0.1},
75
+ })
76
+ elif energy_delta < -0.3:
77
+ # Dropping energy — use subtractive approach
78
+ gestures.append({
79
+ "type": "filter_sweep",
80
+ "step": 1,
81
+ "description": "Close filter to reduce energy",
82
+ "parameters": {"direction": "down"},
83
+ })
84
+ gestures.append({
85
+ "type": "mute_toggle",
86
+ "step": 2,
87
+ "description": "Strip layers to reduce density",
88
+ "parameters": {},
89
+ })
90
+ else:
91
+ # Similar energy — smooth crossfade
92
+ gestures.append({
93
+ "type": "volume_nudge",
94
+ "step": 1,
95
+ "description": "Smooth crossfade between scenes",
96
+ "parameters": {"delta_db": 0.0},
97
+ })
98
+
99
+ # Scene launch at the midpoint
100
+ gestures.append({
101
+ "type": "scene_launch",
102
+ "step": steps // 2,
103
+ "description": f"Launch scene {to_scene.scene_index} ('{to_scene.name}')",
104
+ "parameters": {"scene_index": to_scene.scene_index},
105
+ })
106
+
107
+ # Post-transition: settle
108
+ gestures.append({
109
+ "type": "settle",
110
+ "step": steps,
111
+ "description": f"Settle into '{to_scene.name}' ({to_scene.role})",
112
+ })
113
+
114
+ return HandoffPlan(
115
+ from_scene=from_scene.scene_index,
116
+ to_scene=to_scene.scene_index,
117
+ gestures=gestures,
118
+ energy_path=energy_path,
119
+ )
120
+
121
+
122
+ # ── Energy-aware move suggestions ─────────────────────────────────────
123
+
124
+
125
+ def suggest_energy_moves(
126
+ energy_window: EnergyWindow,
127
+ scene: SceneRole,
128
+ ) -> list[LiveSafeMove]:
129
+ """Suggest moves to steer energy toward the target.
130
+
131
+ Considers the current scene's role to tailor suggestions.
132
+ """
133
+ moves: list[LiveSafeMove] = []
134
+ gap = energy_window.target_energy - energy_window.current_energy
135
+
136
+ if abs(gap) < 0.05:
137
+ # Close enough — hold steady
138
+ moves.append(LiveSafeMove(
139
+ move_type="macro_nudge",
140
+ target="performance_macro",
141
+ description="Maintain current energy — micro-adjust macro for variation",
142
+ risk_level="safe",
143
+ parameters={"delta": 0.01},
144
+ reversible=True,
145
+ ))
146
+ return moves
147
+
148
+ if gap > 0:
149
+ # Need more energy
150
+ moves.append(LiveSafeMove(
151
+ move_type="volume_nudge",
152
+ target="master",
153
+ description=f"Lift volume toward target energy ({energy_window.target_energy:.1f})",
154
+ risk_level="safe",
155
+ parameters={"delta_db": min(2.0, gap * 6.0)},
156
+ reversible=True,
157
+ ))
158
+ if scene.role in ("build", "chorus", "drop"):
159
+ moves.append(LiveSafeMove(
160
+ move_type="send_nudge",
161
+ target="delay_send",
162
+ description="Add delay send for rhythmic energy boost",
163
+ risk_level="safe",
164
+ parameters={"delta": min(0.15, gap * 0.3)},
165
+ reversible=True,
166
+ ))
167
+ moves.append(LiveSafeMove(
168
+ move_type="filter_sweep",
169
+ target="master_filter",
170
+ description="Open high-pass for brightness lift",
171
+ risk_level="safe",
172
+ parameters={"direction": "up", "range": [2000, 16000]},
173
+ reversible=True,
174
+ ))
175
+ else:
176
+ # Need less energy
177
+ moves.append(LiveSafeMove(
178
+ move_type="filter_sweep",
179
+ target="master_filter",
180
+ description=f"Low-pass sweep toward target energy ({energy_window.target_energy:.1f})",
181
+ risk_level="safe",
182
+ parameters={"direction": "down", "range": [200, 8000]},
183
+ reversible=True,
184
+ ))
185
+ if scene.role in ("breakdown", "outro", "intro"):
186
+ moves.append(LiveSafeMove(
187
+ move_type="mute_toggle",
188
+ target="percussion_layer",
189
+ description="Mute percussion to soften breakdown",
190
+ risk_level="safe",
191
+ parameters={},
192
+ reversible=True,
193
+ ))
194
+ moves.append(LiveSafeMove(
195
+ move_type="volume_nudge",
196
+ target="master",
197
+ description="Reduce volume toward target energy",
198
+ risk_level="safe",
199
+ parameters={"delta_db": max(-2.0, gap * 6.0)},
200
+ reversible=True,
201
+ ))
202
+
203
+ return moves
204
+
205
+
206
+ # ── Build full performance state ──────────────────────────────────────
207
+
208
+
209
+ def build_performance_state(
210
+ scenes: list[SceneRole],
211
+ current_scene_index: int,
212
+ ) -> PerformanceState:
213
+ """Build a complete PerformanceState from scene roles.
214
+
215
+ Computes energy window from current scene context and
216
+ populates safe/blocked move lists.
217
+ """
218
+ # Find current scene
219
+ current = None
220
+ for s in scenes:
221
+ if s.scene_index == current_scene_index:
222
+ current = s
223
+ break
224
+
225
+ if current is None:
226
+ # Fallback: use defaults
227
+ current_energy = 0.5
228
+ target_energy = 0.5
229
+ direction = "hold"
230
+ else:
231
+ current_energy = current.energy_level
232
+
233
+ # Infer target from adjacent scenes
234
+ next_scenes = [s for s in scenes if s.scene_index > current_scene_index]
235
+ if next_scenes:
236
+ next_scene = min(next_scenes, key=lambda s: s.scene_index)
237
+ target_energy = next_scene.energy_level
238
+ else:
239
+ target_energy = current_energy
240
+
241
+ delta = target_energy - current_energy
242
+ if delta > 0.05:
243
+ direction = "up"
244
+ elif delta < -0.05:
245
+ direction = "down"
246
+ else:
247
+ direction = "hold"
248
+
249
+ urgency = min(1.0, abs(target_energy - current_energy) * 2.0)
250
+
251
+ energy_window = EnergyWindow(
252
+ current_energy=current_energy,
253
+ target_energy=target_energy,
254
+ direction=direction,
255
+ urgency=round(urgency, 3),
256
+ )
257
+
258
+ safe_moves = get_safe_moves(scenes, current_scene_index, energy_window)
259
+ blocked = get_blocked_moves()
260
+
261
+ return PerformanceState(
262
+ scenes=scenes,
263
+ current_scene=current_scene_index,
264
+ energy_window=energy_window,
265
+ safe_moves=safe_moves,
266
+ blocked_moves=blocked,
267
+ )
@@ -0,0 +1,162 @@
1
+ """Performance Engine safety — move classification and policy.
2
+
3
+ Defines which move types are safe during live performance,
4
+ which require caution, and which are blocked entirely.
5
+
6
+ Zero I/O.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .models import EnergyWindow, LiveSafeMove, SceneRole
12
+
13
+
14
+ # ── Move classification sets ──────────────────────────────────────────
15
+
16
+ SAFE_MOVE_TYPES: frozenset = frozenset({
17
+ "scene_launch",
18
+ "send_nudge",
19
+ "macro_nudge",
20
+ "filter_sweep",
21
+ "mute_toggle",
22
+ "volume_nudge",
23
+ })
24
+
25
+ CAUTION_MOVE_TYPES: frozenset = frozenset({
26
+ "tempo_nudge",
27
+ "device_toggle",
28
+ "pan_nudge",
29
+ })
30
+
31
+ BLOCKED_MOVE_TYPES: frozenset = frozenset({
32
+ "device_chain_surgery",
33
+ "arrangement_edit",
34
+ "track_create_delete",
35
+ "note_edit",
36
+ "clip_create_delete",
37
+ })
38
+
39
+
40
+ # ── Classification ────────────────────────────────────────────────────
41
+
42
+
43
+ def classify_move_safety(move_type: str) -> str:
44
+ """Classify a move type as 'safe', 'caution', or 'blocked'.
45
+
46
+ Returns:
47
+ 'safe' if the move is in SAFE_MOVE_TYPES,
48
+ 'blocked' if in BLOCKED_MOVE_TYPES,
49
+ 'caution' otherwise (unknown or caution-class moves).
50
+ """
51
+ if move_type in SAFE_MOVE_TYPES:
52
+ return "safe"
53
+ if move_type in BLOCKED_MOVE_TYPES:
54
+ return "blocked"
55
+ return "caution"
56
+
57
+
58
+ # ── Safe move suggestions ─────────────────────────────────────────────
59
+
60
+
61
+ def get_safe_moves(
62
+ scene_roles: list[SceneRole],
63
+ current_scene: int,
64
+ energy_window: EnergyWindow,
65
+ ) -> list[LiveSafeMove]:
66
+ """Suggest safe moves based on current scene and energy state.
67
+
68
+ Returns a list of LiveSafeMove instances — only safe-class moves.
69
+ """
70
+ moves: list[LiveSafeMove] = []
71
+
72
+ # Scene launch suggestions: suggest adjacent scenes
73
+ for scene in scene_roles:
74
+ if scene.scene_index == current_scene:
75
+ continue
76
+ energy_delta = scene.energy_level - energy_window.current_energy
77
+ # Suggest scenes that move toward the target energy
78
+ if energy_window.direction == "up" and energy_delta > 0:
79
+ moves.append(LiveSafeMove(
80
+ move_type="scene_launch",
81
+ target=f"scene_{scene.scene_index}",
82
+ description=f"Launch '{scene.name}' (energy {scene.energy_level:.1f}, role: {scene.role})",
83
+ risk_level="safe",
84
+ parameters={"scene_index": scene.scene_index},
85
+ reversible=True,
86
+ ))
87
+ elif energy_window.direction == "down" and energy_delta < 0:
88
+ moves.append(LiveSafeMove(
89
+ move_type="scene_launch",
90
+ target=f"scene_{scene.scene_index}",
91
+ description=f"Launch '{scene.name}' (energy {scene.energy_level:.1f}, role: {scene.role})",
92
+ risk_level="safe",
93
+ parameters={"scene_index": scene.scene_index},
94
+ reversible=True,
95
+ ))
96
+ elif energy_window.direction == "hold":
97
+ # Suggest scenes with similar energy
98
+ if abs(energy_delta) < 0.2:
99
+ moves.append(LiveSafeMove(
100
+ move_type="scene_launch",
101
+ target=f"scene_{scene.scene_index}",
102
+ description=f"Launch '{scene.name}' (similar energy {scene.energy_level:.1f})",
103
+ risk_level="safe",
104
+ parameters={"scene_index": scene.scene_index},
105
+ reversible=True,
106
+ ))
107
+
108
+ # Energy-based nudge suggestions
109
+ if energy_window.direction == "up":
110
+ moves.append(LiveSafeMove(
111
+ move_type="volume_nudge",
112
+ target="master",
113
+ description="Nudge master volume up for energy lift",
114
+ risk_level="safe",
115
+ parameters={"delta_db": 1.0},
116
+ reversible=True,
117
+ ))
118
+ moves.append(LiveSafeMove(
119
+ move_type="send_nudge",
120
+ target="reverb_send",
121
+ description="Increase reverb send for spatial expansion",
122
+ risk_level="safe",
123
+ parameters={"delta": 0.05},
124
+ reversible=True,
125
+ ))
126
+ elif energy_window.direction == "down":
127
+ moves.append(LiveSafeMove(
128
+ move_type="filter_sweep",
129
+ target="master",
130
+ description="Low-pass filter sweep to reduce energy",
131
+ risk_level="safe",
132
+ parameters={"direction": "down", "range": [200, 8000]},
133
+ reversible=True,
134
+ ))
135
+ moves.append(LiveSafeMove(
136
+ move_type="mute_toggle",
137
+ target="high_energy_track",
138
+ description="Mute a high-energy layer to reduce intensity",
139
+ risk_level="safe",
140
+ parameters={},
141
+ reversible=True,
142
+ ))
143
+
144
+ # Always offer macro nudge as a safe option
145
+ moves.append(LiveSafeMove(
146
+ move_type="macro_nudge",
147
+ target="performance_macro",
148
+ description="Adjust performance macro for subtle variation",
149
+ risk_level="safe",
150
+ parameters={"delta": 0.02},
151
+ reversible=True,
152
+ ))
153
+
154
+ return moves
155
+
156
+
157
+ # ── Blocked move list ─────────────────────────────────────────────────
158
+
159
+
160
+ def get_blocked_moves() -> list[str]:
161
+ """Return all blocked move types as a sorted list."""
162
+ return sorted(BLOCKED_MOVE_TYPES)
@@ -0,0 +1,183 @@
1
+ """Performance Engine MCP tools — 3 tools for live performance mode.
2
+
3
+ Each tool fetches scene data from Ableton via the shared connection,
4
+ then delegates to pure-computation modules.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from mcp.server.fastmcp import Context
10
+
11
+ from ..server import mcp
12
+ from .models import EnergyWindow, SceneRole
13
+ from .planner import build_performance_state, plan_scene_transition, suggest_energy_moves
14
+ from .safety import classify_move_safety, get_blocked_moves, get_safe_moves
15
+
16
+
17
+ # ── Helpers ─────────────────────────────────────────────────────────
18
+
19
+
20
+ def _infer_role(name: str, index: int, scene_count: int) -> str:
21
+ """Infer a scene's role from its name or position."""
22
+ lower = name.lower()
23
+ for role in ("intro", "verse", "chorus", "build", "drop", "breakdown", "outro", "transition"):
24
+ if role in lower:
25
+ return role
26
+ # Positional fallback
27
+ if index == 0:
28
+ return "intro"
29
+ if index == scene_count - 1:
30
+ return "outro"
31
+ if scene_count > 4:
32
+ quarter = scene_count / 4
33
+ if index < quarter:
34
+ return "intro"
35
+ elif index < quarter * 2:
36
+ return "verse"
37
+ elif index < quarter * 3:
38
+ return "chorus"
39
+ else:
40
+ return "outro"
41
+ return "verse"
42
+
43
+
44
+ def _infer_energy(role: str) -> float:
45
+ """Infer energy level from scene role."""
46
+ energy_map = {
47
+ "intro": 0.2,
48
+ "verse": 0.4,
49
+ "build": 0.6,
50
+ "chorus": 0.7,
51
+ "drop": 0.9,
52
+ "breakdown": 0.3,
53
+ "transition": 0.5,
54
+ "outro": 0.2,
55
+ }
56
+ return energy_map.get(role, 0.5)
57
+
58
+
59
+ def _fetch_scene_data(ctx: Context) -> tuple[list[SceneRole], int]:
60
+ """Fetch scene info from Ableton and build SceneRole list."""
61
+ ableton = ctx.lifespan_context["ableton"]
62
+
63
+ scenes_info = ableton.send_command("get_scenes_info", {})
64
+ scenes_list = scenes_info.get("scenes", [])
65
+ scene_count = len(scenes_list)
66
+
67
+ scene_roles: list[SceneRole] = []
68
+ for i, scene_data in enumerate(scenes_list):
69
+ name = scene_data.get("name", f"Scene {i}")
70
+ role = _infer_role(name, i, scene_count)
71
+ energy = _infer_energy(role)
72
+ scene_roles.append(SceneRole(
73
+ scene_index=i,
74
+ name=name,
75
+ energy_level=energy,
76
+ role=role,
77
+ ))
78
+
79
+ # Determine current scene — default to 0 since session_info
80
+ # doesn't expose a selected_scene field
81
+ current_scene = 0
82
+ try:
83
+ session_info = ableton.send_command("get_session_info", {})
84
+ # Check if any scene is marked as triggered/playing
85
+ session_scenes = session_info.get("scenes", [])
86
+ for i, s in enumerate(session_scenes):
87
+ if s.get("is_triggered", False):
88
+ current_scene = i
89
+ break
90
+ except Exception:
91
+ pass
92
+
93
+ return scene_roles, current_scene
94
+
95
+
96
+ # ── MCP Tools ───────────────────────────────────────────────────────
97
+
98
+
99
+ @mcp.tool()
100
+ def get_performance_state(ctx: Context) -> dict:
101
+ """Get current live performance overview — scenes, energy, safe moves.
102
+
103
+ Returns scene roles with energy levels, current energy window
104
+ with steering direction, available safe moves, and blocked move types.
105
+ Use this to understand the performance context before making changes.
106
+ """
107
+ scene_roles, current_scene = _fetch_scene_data(ctx)
108
+ state = build_performance_state(scene_roles, current_scene)
109
+ return state.to_dict()
110
+
111
+
112
+ @mcp.tool()
113
+ def get_performance_safe_moves(ctx: Context) -> dict:
114
+ """Get available safe moves for live performance.
115
+
116
+ Returns only performance-safe moves based on current scene
117
+ and energy direction. All moves are reversible and low-risk.
118
+ Also returns the full blocked move list for transparency.
119
+ """
120
+ scene_roles, current_scene = _fetch_scene_data(ctx)
121
+ state = build_performance_state(scene_roles, current_scene)
122
+
123
+ # Also get energy-specific suggestions
124
+ current = None
125
+ for s in scene_roles:
126
+ if s.scene_index == current_scene:
127
+ current = s
128
+ break
129
+
130
+ energy_moves: list[dict] = []
131
+ if current is not None:
132
+ em = suggest_energy_moves(state.energy_window, current)
133
+ energy_moves = [m.to_dict() for m in em]
134
+
135
+ return {
136
+ "safe_moves": [m.to_dict() for m in state.safe_moves],
137
+ "energy_moves": energy_moves,
138
+ "blocked_moves": state.blocked_moves,
139
+ "safe_move_count": len(state.safe_moves),
140
+ "energy_move_count": len(energy_moves),
141
+ }
142
+
143
+
144
+ @mcp.tool()
145
+ def plan_scene_handoff(
146
+ ctx: Context,
147
+ from_scene: int,
148
+ to_scene: int,
149
+ ) -> dict:
150
+ """Plan a safe transition between two scenes.
151
+
152
+ Generates an energy path and gesture sequence for smooth
153
+ scene-to-scene handoffs during live performance.
154
+
155
+ Args:
156
+ from_scene: Source scene index.
157
+ to_scene: Destination scene index.
158
+ """
159
+ scene_roles, _ = _fetch_scene_data(ctx)
160
+
161
+ # Find the two scenes
162
+ from_role = None
163
+ to_role = None
164
+ for s in scene_roles:
165
+ if s.scene_index == from_scene:
166
+ from_role = s
167
+ if s.scene_index == to_scene:
168
+ to_role = s
169
+
170
+ if from_role is None:
171
+ return {"error": f"Scene {from_scene} not found", "code": "NOT_FOUND"}
172
+ if to_role is None:
173
+ return {"error": f"Scene {to_scene} not found", "code": "NOT_FOUND"}
174
+
175
+ plan = plan_scene_transition(from_role, to_role)
176
+ return {
177
+ "handoff_plan": plan.to_dict(),
178
+ "from_scene": from_role.to_dict(),
179
+ "to_scene": to_role.to_dict(),
180
+ "energy_delta": round(to_role.energy_level - from_role.energy_level, 3),
181
+ "gesture_count": len(plan.gestures),
182
+ "step_count": len(plan.energy_path),
183
+ }
@@ -0,0 +1,6 @@
1
+ """Project Brain v1 — shared state substrate for LivePilot.
2
+
3
+ Provides one coherent, inspectable, updateable representation of the
4
+ current project state. All engines read from this instead of each
5
+ rebuilding partial state from scratch.
6
+ """
@@ -0,0 +1,64 @@
1
+ """Arrangement graph builder — transforms scenes + clip matrix into ArrangementGraph.
2
+
3
+ Reuses _composition_engine.build_section_graph_from_scenes for the heavy
4
+ inference, then converts composition SectionNodes to brain SectionNodes.
5
+
6
+ Pure computation, zero I/O.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from ..tools._composition_engine import (
12
+ build_section_graph_from_scenes as _ce_build_sections,
13
+ )
14
+ from .models import ArrangementGraph, SectionNode
15
+
16
+
17
+ def build_arrangement_graph(
18
+ scenes: list[dict],
19
+ clip_matrix: list[list[dict]],
20
+ track_count: int,
21
+ beats_per_bar: int = 4,
22
+ ) -> ArrangementGraph:
23
+ """Build an ArrangementGraph from session-view scenes and clip matrix.
24
+
25
+ Args:
26
+ scenes: list of {index, name, tempo, color_index}.
27
+ clip_matrix: [scene_index][track_index] = {state, name, ...} or None.
28
+ track_count: total number of tracks in the session.
29
+ beats_per_bar: beats per bar (default 4).
30
+
31
+ Returns:
32
+ ArrangementGraph with sections, boundaries, and freshness (unfreshed).
33
+ """
34
+ graph = ArrangementGraph()
35
+
36
+ if not scenes:
37
+ return graph
38
+
39
+ # Delegate to composition engine for section inference
40
+ ce_sections = _ce_build_sections(scenes, clip_matrix, track_count, beats_per_bar)
41
+
42
+ # Convert composition SectionNodes -> brain SectionNodes
43
+ for ce_sec in ce_sections:
44
+ graph.sections.append(SectionNode(
45
+ section_id=ce_sec.section_id,
46
+ start_bar=ce_sec.start_bar,
47
+ end_bar=ce_sec.end_bar,
48
+ section_type=ce_sec.section_type.value,
49
+ energy=ce_sec.energy,
50
+ density=ce_sec.density,
51
+ ))
52
+
53
+ # Build boundary list (transitions between adjacent sections)
54
+ for i in range(len(graph.sections) - 1):
55
+ curr = graph.sections[i]
56
+ nxt = graph.sections[i + 1]
57
+ graph.boundaries.append({
58
+ "from_section": curr.section_id,
59
+ "to_section": nxt.section_id,
60
+ "bar": curr.end_bar,
61
+ "energy_delta": round(nxt.energy - curr.energy, 3),
62
+ })
63
+
64
+ return graph