livepilot 1.9.24 → 1.10.1
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 +223 -0
- package/CONTRIBUTING.md +2 -2
- package/LICENSE +62 -21
- package/README.md +291 -276
- package/bin/livepilot.js +87 -0
- package/installer/codex.js +147 -0
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/skills/livepilot-arrangement/SKILL.md +18 -1
- package/livepilot/skills/livepilot-core/SKILL.md +22 -5
- package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
- package/livepilot/skills/livepilot-core/references/overview.md +13 -9
- package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
- package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
- package/livepilot/skills/livepilot-devices/SKILL.md +39 -4
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +23 -19
- package/livepilot/skills/livepilot-sample-engine/SKILL.md +105 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +45 -0
- package/livepilot/skills/livepilot-wonder/SKILL.md +17 -0
- package/livepilot.mcpb +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/manifest.json +4 -4
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +357 -0
- package/mcp_server/atlas/device_atlas.json +44067 -0
- package/mcp_server/atlas/enrichments/__init__.py +111 -0
- package/mcp_server/atlas/enrichments/audio_effects/auto_filter.yaml +162 -0
- package/mcp_server/atlas/enrichments/audio_effects/beat_repeat.yaml +183 -0
- package/mcp_server/atlas/enrichments/audio_effects/channel_eq.yaml +126 -0
- package/mcp_server/atlas/enrichments/audio_effects/chorus_ensemble.yaml +149 -0
- package/mcp_server/atlas/enrichments/audio_effects/color_limiter.yaml +109 -0
- package/mcp_server/atlas/enrichments/audio_effects/compressor.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb.yaml +143 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb_pro.yaml +178 -0
- package/mcp_server/atlas/enrichments/audio_effects/delay.yaml +151 -0
- package/mcp_server/atlas/enrichments/audio_effects/drum_buss.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/dynamic_tube.yaml +147 -0
- package/mcp_server/atlas/enrichments/audio_effects/echo.yaml +167 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_eight.yaml +148 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_three.yaml +121 -0
- package/mcp_server/atlas/enrichments/audio_effects/erosion.yaml +103 -0
- package/mcp_server/atlas/enrichments/audio_effects/filter_delay.yaml +173 -0
- package/mcp_server/atlas/enrichments/audio_effects/gate.yaml +130 -0
- package/mcp_server/atlas/enrichments/audio_effects/gated_delay.yaml +133 -0
- package/mcp_server/atlas/enrichments/audio_effects/glue_compressor.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/grain_delay.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/hybrid_reverb.yaml +160 -0
- package/mcp_server/atlas/enrichments/audio_effects/limiter.yaml +97 -0
- package/mcp_server/atlas/enrichments/audio_effects/multiband_dynamics.yaml +174 -0
- package/mcp_server/atlas/enrichments/audio_effects/overdrive.yaml +119 -0
- package/mcp_server/atlas/enrichments/audio_effects/pedal.yaml +145 -0
- package/mcp_server/atlas/enrichments/audio_effects/phaser_flanger.yaml +161 -0
- package/mcp_server/atlas/enrichments/audio_effects/redux.yaml +114 -0
- package/mcp_server/atlas/enrichments/audio_effects/reverb.yaml +190 -0
- package/mcp_server/atlas/enrichments/audio_effects/roar.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/saturator.yaml +146 -0
- package/mcp_server/atlas/enrichments/audio_effects/shifter.yaml +154 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_resonator.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_time.yaml +164 -0
- package/mcp_server/atlas/enrichments/audio_effects/vector_delay.yaml +140 -0
- package/mcp_server/atlas/enrichments/audio_effects/vinyl_distortion.yaml +141 -0
- package/mcp_server/atlas/enrichments/instruments/analog.yaml +222 -0
- package/mcp_server/atlas/enrichments/instruments/bass.yaml +202 -0
- package/mcp_server/atlas/enrichments/instruments/collision.yaml +150 -0
- package/mcp_server/atlas/enrichments/instruments/drift.yaml +167 -0
- package/mcp_server/atlas/enrichments/instruments/electric.yaml +137 -0
- package/mcp_server/atlas/enrichments/instruments/emit.yaml +163 -0
- package/mcp_server/atlas/enrichments/instruments/meld.yaml +164 -0
- package/mcp_server/atlas/enrichments/instruments/operator.yaml +197 -0
- package/mcp_server/atlas/enrichments/instruments/poli.yaml +192 -0
- package/mcp_server/atlas/enrichments/instruments/sampler.yaml +218 -0
- package/mcp_server/atlas/enrichments/instruments/simpler.yaml +217 -0
- package/mcp_server/atlas/enrichments/instruments/tension.yaml +156 -0
- package/mcp_server/atlas/enrichments/instruments/tree_tone.yaml +162 -0
- package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +165 -0
- package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +166 -0
- package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +162 -0
- package/mcp_server/atlas/enrichments/midi_effects/arpeggiator.yaml +156 -0
- package/mcp_server/atlas/enrichments/midi_effects/bouncy_notes.yaml +93 -0
- package/mcp_server/atlas/enrichments/midi_effects/chord.yaml +147 -0
- package/mcp_server/atlas/enrichments/midi_effects/melodic_steps.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_echo.yaml +108 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_length.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/pitch.yaml +76 -0
- package/mcp_server/atlas/enrichments/midi_effects/random.yaml +117 -0
- package/mcp_server/atlas/enrichments/midi_effects/rhythmic_steps.yaml +103 -0
- package/mcp_server/atlas/enrichments/midi_effects/scale.yaml +83 -0
- package/mcp_server/atlas/enrichments/midi_effects/step_arp.yaml +112 -0
- package/mcp_server/atlas/enrichments/midi_effects/velocity.yaml +119 -0
- package/mcp_server/atlas/enrichments/utility/amp.yaml +159 -0
- package/mcp_server/atlas/enrichments/utility/cabinet.yaml +109 -0
- package/mcp_server/atlas/enrichments/utility/corpus.yaml +150 -0
- package/mcp_server/atlas/enrichments/utility/resonators.yaml +131 -0
- package/mcp_server/atlas/enrichments/utility/spectrum.yaml +63 -0
- package/mcp_server/atlas/enrichments/utility/tuner.yaml +51 -0
- package/mcp_server/atlas/enrichments/utility/utility.yaml +136 -0
- package/mcp_server/atlas/enrichments/utility/vocoder.yaml +160 -0
- package/mcp_server/atlas/scanner.py +236 -0
- package/mcp_server/atlas/tools.py +224 -0
- package/mcp_server/composer/__init__.py +1 -0
- package/mcp_server/composer/engine.py +532 -0
- package/mcp_server/composer/layer_planner.py +427 -0
- package/mcp_server/composer/prompt_parser.py +329 -0
- package/mcp_server/composer/sample_resolver.py +153 -0
- package/mcp_server/composer/tools.py +211 -0
- package/mcp_server/connection.py +53 -8
- package/mcp_server/corpus/__init__.py +377 -0
- package/mcp_server/device_forge/__init__.py +1 -0
- package/mcp_server/device_forge/builder.py +377 -0
- package/mcp_server/device_forge/models.py +142 -0
- package/mcp_server/device_forge/templates.py +483 -0
- package/mcp_server/device_forge/tools.py +162 -0
- package/mcp_server/m4l_bridge.py +1 -0
- package/mcp_server/memory/taste_accessors.py +47 -0
- package/mcp_server/preview_studio/engine.py +9 -2
- package/mcp_server/preview_studio/tools.py +78 -35
- package/mcp_server/project_brain/tools.py +34 -0
- package/mcp_server/runtime/capability_probe.py +21 -2
- package/mcp_server/runtime/execution_router.py +184 -38
- package/mcp_server/runtime/live_version.py +102 -0
- package/mcp_server/runtime/mcp_dispatch.py +46 -0
- package/mcp_server/runtime/remote_commands.py +13 -5
- package/mcp_server/runtime/tools.py +66 -29
- package/mcp_server/sample_engine/__init__.py +1 -0
- package/mcp_server/sample_engine/analyzer.py +216 -0
- package/mcp_server/sample_engine/critics.py +390 -0
- package/mcp_server/sample_engine/models.py +193 -0
- package/mcp_server/sample_engine/moves.py +127 -0
- package/mcp_server/sample_engine/planner.py +186 -0
- package/mcp_server/sample_engine/slice_workflow.py +190 -0
- package/mcp_server/sample_engine/sources.py +540 -0
- package/mcp_server/sample_engine/techniques.py +908 -0
- package/mcp_server/sample_engine/tools.py +545 -0
- package/mcp_server/semantic_moves/__init__.py +3 -0
- package/mcp_server/semantic_moves/device_creation_moves.py +237 -0
- package/mcp_server/semantic_moves/mix_moves.py +8 -8
- package/mcp_server/semantic_moves/models.py +7 -7
- package/mcp_server/semantic_moves/performance_moves.py +4 -4
- package/mcp_server/semantic_moves/sample_compilers.py +377 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +4 -4
- package/mcp_server/semantic_moves/tools.py +63 -10
- package/mcp_server/semantic_moves/transition_moves.py +4 -4
- package/mcp_server/server.py +71 -1
- package/mcp_server/session_continuity/tracker.py +4 -1
- package/mcp_server/sound_design/critics.py +89 -1
- package/mcp_server/splice_client/__init__.py +1 -0
- package/mcp_server/splice_client/client.py +347 -0
- package/mcp_server/splice_client/models.py +96 -0
- package/mcp_server/splice_client/protos/__init__.py +1 -0
- package/mcp_server/splice_client/protos/app_pb2.py +319 -0
- package/mcp_server/splice_client/protos/app_pb2.pyi +1153 -0
- package/mcp_server/splice_client/protos/app_pb2_grpc.py +1946 -0
- package/mcp_server/tools/_conductor.py +16 -0
- package/mcp_server/tools/_planner_engine.py +24 -0
- package/mcp_server/tools/analyzer.py +2 -0
- package/mcp_server/tools/arrangement.py +69 -0
- package/mcp_server/tools/automation.py +15 -2
- package/mcp_server/tools/devices.py +117 -6
- package/mcp_server/tools/notes.py +37 -4
- package/mcp_server/tools/planner.py +3 -0
- package/mcp_server/wonder_mode/diagnosis.py +5 -0
- package/mcp_server/wonder_mode/engine.py +144 -14
- package/mcp_server/wonder_mode/tools.py +33 -1
- package/package.json +14 -4
- package/remote_script/LivePilot/__init__.py +8 -1
- package/remote_script/LivePilot/arrangement.py +114 -0
- package/remote_script/LivePilot/browser.py +56 -1
- package/remote_script/LivePilot/devices.py +246 -6
- package/remote_script/LivePilot/mixing.py +8 -3
- package/remote_script/LivePilot/server.py +5 -1
- package/remote_script/LivePilot/transport.py +3 -0
- package/remote_script/LivePilot/version_detect.py +78 -0
package/mcp_server/connection.py
CHANGED
|
@@ -14,6 +14,11 @@ from typing import Optional
|
|
|
14
14
|
|
|
15
15
|
CONNECT_TIMEOUT = 5
|
|
16
16
|
RECV_TIMEOUT = 20
|
|
17
|
+
SINGLE_CLIENT_RETRY_DELAY = 0.25
|
|
18
|
+
COMMAND_RECV_TIMEOUTS = {
|
|
19
|
+
# Server-side slow write window is 35s; give the client a small buffer.
|
|
20
|
+
"freeze_track": 40,
|
|
21
|
+
}
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
class AbletonConnectionError(Exception):
|
|
@@ -47,6 +52,19 @@ def _friendly_error(code: str, message: str, command_type: str) -> str:
|
|
|
47
52
|
return " ".join(parts)
|
|
48
53
|
|
|
49
54
|
|
|
55
|
+
def _is_single_client_state_error(response: dict) -> bool:
|
|
56
|
+
"""Return True when the server rejected a fresh connection due to single-client guard."""
|
|
57
|
+
if response.get("ok") is not False:
|
|
58
|
+
return False
|
|
59
|
+
err = response.get("error", {})
|
|
60
|
+
if not isinstance(err, dict):
|
|
61
|
+
return False
|
|
62
|
+
return (
|
|
63
|
+
err.get("code") == "STATE_ERROR"
|
|
64
|
+
and "Another client is already connected" in str(err.get("message", ""))
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
50
68
|
def _identify_other_tcp_client(host: str, port: int) -> str | None:
|
|
51
69
|
"""Return a short description of another established client on the Live port."""
|
|
52
70
|
try:
|
|
@@ -134,9 +152,7 @@ class AbletonConnection:
|
|
|
134
152
|
def ping(self) -> bool:
|
|
135
153
|
"""Send a ping and return True if a pong is received."""
|
|
136
154
|
try:
|
|
137
|
-
|
|
138
|
-
resp = self._send_raw({"type": "ping"})
|
|
139
|
-
return resp.get("result", {}).get("pong") is True
|
|
155
|
+
return self.send_command("ping").get("pong") is True
|
|
140
156
|
except Exception:
|
|
141
157
|
return False
|
|
142
158
|
|
|
@@ -151,7 +167,8 @@ class AbletonConnection:
|
|
|
151
167
|
"""
|
|
152
168
|
with self._lock:
|
|
153
169
|
# Ensure we have a connection
|
|
154
|
-
|
|
170
|
+
fresh_connect = not self.is_connected()
|
|
171
|
+
if fresh_connect:
|
|
155
172
|
self.connect()
|
|
156
173
|
|
|
157
174
|
command: dict = {"type": command_type}
|
|
@@ -159,7 +176,10 @@ class AbletonConnection:
|
|
|
159
176
|
command["params"] = params
|
|
160
177
|
|
|
161
178
|
try:
|
|
162
|
-
response = self._send_raw(
|
|
179
|
+
response = self._send_raw(
|
|
180
|
+
command,
|
|
181
|
+
recv_timeout=COMMAND_RECV_TIMEOUTS.get(command_type, RECV_TIMEOUT),
|
|
182
|
+
)
|
|
163
183
|
except AbletonConnectionError as exc:
|
|
164
184
|
# If the send phase succeeded (data left this process),
|
|
165
185
|
# Ableton may have already applied the command. Never
|
|
@@ -172,12 +192,30 @@ class AbletonConnection:
|
|
|
172
192
|
# Send itself failed — safe to retry with a fresh connection
|
|
173
193
|
self.disconnect()
|
|
174
194
|
self.connect()
|
|
175
|
-
response = self._send_raw(
|
|
195
|
+
response = self._send_raw(
|
|
196
|
+
command,
|
|
197
|
+
recv_timeout=COMMAND_RECV_TIMEOUTS.get(command_type, RECV_TIMEOUT),
|
|
198
|
+
)
|
|
176
199
|
except OSError:
|
|
177
200
|
# Socket error before send — safe to retry
|
|
178
201
|
self.disconnect()
|
|
179
202
|
self.connect()
|
|
180
|
-
response = self._send_raw(
|
|
203
|
+
response = self._send_raw(
|
|
204
|
+
command,
|
|
205
|
+
recv_timeout=COMMAND_RECV_TIMEOUTS.get(command_type, RECV_TIMEOUT),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# The single-client guard can briefly reject an immediate reconnect
|
|
209
|
+
# after this process closes a previous socket. Retry once after a
|
|
210
|
+
# short delay when the command was rejected before execution.
|
|
211
|
+
if fresh_connect and _is_single_client_state_error(response):
|
|
212
|
+
self.disconnect()
|
|
213
|
+
time.sleep(SINGLE_CLIENT_RETRY_DELAY)
|
|
214
|
+
self.connect()
|
|
215
|
+
response = self._send_raw(
|
|
216
|
+
command,
|
|
217
|
+
recv_timeout=COMMAND_RECV_TIMEOUTS.get(command_type, RECV_TIMEOUT),
|
|
218
|
+
)
|
|
181
219
|
|
|
182
220
|
# Log and error handling outside the lock (no socket access needed)
|
|
183
221
|
log_entry = {
|
|
@@ -214,7 +252,7 @@ class AbletonConnection:
|
|
|
214
252
|
# Low-level transport
|
|
215
253
|
# ------------------------------------------------------------------
|
|
216
254
|
|
|
217
|
-
def _send_raw(self, command: dict) -> dict:
|
|
255
|
+
def _send_raw(self, command: dict, recv_timeout: int = RECV_TIMEOUT) -> dict:
|
|
218
256
|
"""Send a JSON command (with request_id) and read the response."""
|
|
219
257
|
if self._socket is None:
|
|
220
258
|
raise AbletonConnectionError("Not connected to Ableton Live")
|
|
@@ -222,6 +260,7 @@ class AbletonConnection:
|
|
|
222
260
|
# Don't mutate the caller's dict
|
|
223
261
|
envelope = {**command, "id": str(uuid.uuid4())[:8]}
|
|
224
262
|
payload = json.dumps(envelope) + "\n"
|
|
263
|
+
self._socket.settimeout(recv_timeout)
|
|
225
264
|
|
|
226
265
|
try:
|
|
227
266
|
self._socket.sendall(payload.encode("utf-8"))
|
|
@@ -283,3 +322,9 @@ class AbletonConnection:
|
|
|
283
322
|
raise AbletonConnectionError(
|
|
284
323
|
f"Invalid JSON from Ableton: {line[:200]}"
|
|
285
324
|
) from exc
|
|
325
|
+
finally:
|
|
326
|
+
if self._socket is not None:
|
|
327
|
+
try:
|
|
328
|
+
self._socket.settimeout(RECV_TIMEOUT)
|
|
329
|
+
except OSError:
|
|
330
|
+
pass
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Corpus Intelligence Layer — structured knowledge from device-knowledge markdown.
|
|
2
|
+
|
|
3
|
+
Parses the device-knowledge corpus (creative-thinking.md, automation-as-music.md,
|
|
4
|
+
effects-*.md, instruments-synths.md, chains-genre.md) into queryable Python
|
|
5
|
+
structures. This gives sound_design critics, wonder_mode engine, and other
|
|
6
|
+
modules access to deep creative knowledge at runtime — not just LLM guidance.
|
|
7
|
+
|
|
8
|
+
Lazy-loaded at first access; pure computation, no I/O after initial load.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ── Data structures ─────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class EmotionalRecipe:
|
|
24
|
+
"""Maps an emotion/quality to specific technical actions."""
|
|
25
|
+
emotion: str = ""
|
|
26
|
+
techniques: list[str] = field(default_factory=list)
|
|
27
|
+
parameters: dict[str, str] = field(default_factory=dict) # param_hint -> value_hint
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class PhysicalModelRecipe:
|
|
32
|
+
"""Maps a physical material (water, metal, glass) to device chains."""
|
|
33
|
+
material: str = ""
|
|
34
|
+
devices: list[str] = field(default_factory=list)
|
|
35
|
+
techniques: list[str] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class AutomationGesture:
|
|
40
|
+
"""A multi-parameter automation macro gesture."""
|
|
41
|
+
name: str = ""
|
|
42
|
+
description: str = ""
|
|
43
|
+
parameters: list[dict] = field(default_factory=list) # [{param, from, to}]
|
|
44
|
+
duration_bars: str = ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class GenreChain:
|
|
49
|
+
"""A complete effect chain recipe for a genre."""
|
|
50
|
+
genre: str = ""
|
|
51
|
+
devices: list[str] = field(default_factory=list)
|
|
52
|
+
parameter_hints: dict[str, str] = field(default_factory=dict)
|
|
53
|
+
description: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class DeviceKnowledge:
|
|
58
|
+
"""Deep knowledge about a specific Ableton device."""
|
|
59
|
+
device_name: str = ""
|
|
60
|
+
category: str = "" # synth, effect, spectral, etc.
|
|
61
|
+
techniques: list[str] = field(default_factory=list)
|
|
62
|
+
sweet_spots: dict[str, str] = field(default_factory=dict)
|
|
63
|
+
anti_patterns: list[str] = field(default_factory=list)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class Corpus:
|
|
68
|
+
"""The full parsed corpus — queryable from any module."""
|
|
69
|
+
emotional_recipes: dict[str, EmotionalRecipe] = field(default_factory=dict)
|
|
70
|
+
physical_models: dict[str, PhysicalModelRecipe] = field(default_factory=dict)
|
|
71
|
+
automation_gestures: dict[str, AutomationGesture] = field(default_factory=dict)
|
|
72
|
+
genre_chains: dict[str, GenreChain] = field(default_factory=dict)
|
|
73
|
+
device_knowledge: dict[str, DeviceKnowledge] = field(default_factory=dict)
|
|
74
|
+
anti_patterns: list[str] = field(default_factory=list)
|
|
75
|
+
|
|
76
|
+
# ── Query methods ───────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
def suggest_for_emotion(self, emotion: str) -> Optional[EmotionalRecipe]:
|
|
79
|
+
"""Find techniques for an emotional quality (warmth, tension, etc.)."""
|
|
80
|
+
emotion_lower = emotion.lower()
|
|
81
|
+
if emotion_lower in self.emotional_recipes:
|
|
82
|
+
return self.emotional_recipes[emotion_lower]
|
|
83
|
+
# Fuzzy: check if emotion is a substring of any key
|
|
84
|
+
for key, recipe in self.emotional_recipes.items():
|
|
85
|
+
if emotion_lower in key or key in emotion_lower:
|
|
86
|
+
return recipe
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def suggest_for_material(self, material: str) -> Optional[PhysicalModelRecipe]:
|
|
90
|
+
"""Find device chains for a physical material quality."""
|
|
91
|
+
return self.physical_models.get(material.lower())
|
|
92
|
+
|
|
93
|
+
def get_gesture(self, name: str) -> Optional[AutomationGesture]:
|
|
94
|
+
"""Get a named automation macro gesture."""
|
|
95
|
+
return self.automation_gestures.get(name.lower())
|
|
96
|
+
|
|
97
|
+
def get_genre_chain(self, genre: str) -> Optional[GenreChain]:
|
|
98
|
+
"""Get a genre-specific effect chain recipe."""
|
|
99
|
+
genre_lower = genre.lower()
|
|
100
|
+
if genre_lower in self.genre_chains:
|
|
101
|
+
return self.genre_chains[genre_lower]
|
|
102
|
+
for key, chain in self.genre_chains.items():
|
|
103
|
+
if genre_lower in key or key in genre_lower:
|
|
104
|
+
return chain
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
def get_device(self, name: str) -> Optional[DeviceKnowledge]:
|
|
108
|
+
"""Get deep knowledge about a specific device."""
|
|
109
|
+
return self.device_knowledge.get(name.lower())
|
|
110
|
+
|
|
111
|
+
def recommend_modulation_for_device(self, device_name: str) -> list[str]:
|
|
112
|
+
"""Given a device, suggest what to modulate based on corpus knowledge."""
|
|
113
|
+
dk = self.get_device(device_name)
|
|
114
|
+
if dk and dk.techniques:
|
|
115
|
+
return dk.techniques[:5]
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
def get_automation_density_for_section(self, section_type: str) -> dict:
|
|
119
|
+
"""Return recommended automation parameter count + rate for a section type."""
|
|
120
|
+
density_map = {
|
|
121
|
+
"intro": {"param_count": "1-2", "rate": "very slow (0.05-0.1 Hz)", "purpose": "establish mood"},
|
|
122
|
+
"build": {"param_count": "3-5", "rate": "accelerating exponential", "purpose": "create tension"},
|
|
123
|
+
"peak": {"param_count": "5-8", "rate": "mixed slow + rhythmic", "purpose": "maximum energy"},
|
|
124
|
+
"breakdown": {"param_count": "1-2", "rate": "very slow, gentle", "purpose": "breathing room"},
|
|
125
|
+
"outro": {"param_count": "1-2", "rate": "gradually reducing", "purpose": "return to start"},
|
|
126
|
+
}
|
|
127
|
+
return density_map.get(section_type.lower(), density_map["peak"])
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ── Parser ──────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _find_corpus_dir() -> Optional[str]:
|
|
134
|
+
"""Find the device-knowledge corpus directory."""
|
|
135
|
+
# Check in the skill references (repo path)
|
|
136
|
+
candidates = [
|
|
137
|
+
os.path.join(os.path.dirname(__file__), "..", "..", "livepilot", "skills",
|
|
138
|
+
"livepilot-core", "references", "device-knowledge"),
|
|
139
|
+
# Also check plugin cache paths
|
|
140
|
+
os.path.expanduser("~/.claude/plugins/livepilot/skills/livepilot-core/references/device-knowledge"),
|
|
141
|
+
]
|
|
142
|
+
for path in candidates:
|
|
143
|
+
if os.path.isdir(path):
|
|
144
|
+
return path
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _parse_emotional_section(text: str) -> dict[str, EmotionalRecipe]:
|
|
149
|
+
"""Parse the emotional-to-technical mapping section."""
|
|
150
|
+
recipes: dict[str, EmotionalRecipe] = {}
|
|
151
|
+
current_emotion = ""
|
|
152
|
+
current_techniques: list[str] = []
|
|
153
|
+
|
|
154
|
+
for line in text.split("\n"):
|
|
155
|
+
line = line.strip()
|
|
156
|
+
# New emotion header: ### Tension & Anxiety
|
|
157
|
+
if line.startswith("### "):
|
|
158
|
+
if current_emotion and current_techniques:
|
|
159
|
+
recipes[current_emotion.lower()] = EmotionalRecipe(
|
|
160
|
+
emotion=current_emotion,
|
|
161
|
+
techniques=current_techniques,
|
|
162
|
+
)
|
|
163
|
+
current_emotion = line[4:].strip()
|
|
164
|
+
current_techniques = []
|
|
165
|
+
elif line.startswith("- **") and current_emotion:
|
|
166
|
+
# Extract technique: - **High-resonance filter sweep** — description
|
|
167
|
+
technique = line.lstrip("- ")
|
|
168
|
+
current_techniques.append(technique)
|
|
169
|
+
|
|
170
|
+
# Don't forget the last one
|
|
171
|
+
if current_emotion and current_techniques:
|
|
172
|
+
recipes[current_emotion.lower()] = EmotionalRecipe(
|
|
173
|
+
emotion=current_emotion,
|
|
174
|
+
techniques=current_techniques,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return recipes
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _parse_physical_models(text: str) -> dict[str, PhysicalModelRecipe]:
|
|
181
|
+
"""Parse the physical world modeling section."""
|
|
182
|
+
models: dict[str, PhysicalModelRecipe] = {}
|
|
183
|
+
current_material = ""
|
|
184
|
+
current_techniques: list[str] = []
|
|
185
|
+
current_devices: list[str] = []
|
|
186
|
+
|
|
187
|
+
for line in text.split("\n"):
|
|
188
|
+
line = line.strip()
|
|
189
|
+
if line.startswith("### "):
|
|
190
|
+
if current_material:
|
|
191
|
+
models[current_material.lower()] = PhysicalModelRecipe(
|
|
192
|
+
material=current_material,
|
|
193
|
+
devices=current_devices,
|
|
194
|
+
techniques=current_techniques,
|
|
195
|
+
)
|
|
196
|
+
current_material = line[4:].strip()
|
|
197
|
+
current_techniques = []
|
|
198
|
+
current_devices = []
|
|
199
|
+
elif line.startswith("- **") and current_material:
|
|
200
|
+
# Extract device name from bold
|
|
201
|
+
match = re.match(r"- \*\*(.+?)\*\*", line)
|
|
202
|
+
if match:
|
|
203
|
+
dev_name = match.group(1)
|
|
204
|
+
current_devices.append(dev_name)
|
|
205
|
+
current_techniques.append(line.lstrip("- "))
|
|
206
|
+
|
|
207
|
+
if current_material:
|
|
208
|
+
models[current_material.lower()] = PhysicalModelRecipe(
|
|
209
|
+
material=current_material,
|
|
210
|
+
devices=current_devices,
|
|
211
|
+
techniques=current_techniques,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return models
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _parse_automation_gestures(text: str) -> dict[str, AutomationGesture]:
|
|
218
|
+
"""Parse the macro gesture section from automation-as-music.md."""
|
|
219
|
+
gestures: dict[str, AutomationGesture] = {}
|
|
220
|
+
current_name = ""
|
|
221
|
+
current_desc = ""
|
|
222
|
+
current_params: list[dict] = []
|
|
223
|
+
|
|
224
|
+
for line in text.split("\n"):
|
|
225
|
+
line = line.strip()
|
|
226
|
+
if line.startswith("### The ") and "Gesture" in line:
|
|
227
|
+
if current_name:
|
|
228
|
+
gestures[current_name.lower()] = AutomationGesture(
|
|
229
|
+
name=current_name,
|
|
230
|
+
description=current_desc,
|
|
231
|
+
parameters=current_params,
|
|
232
|
+
)
|
|
233
|
+
# Extract name: ### The "Open Up" Gesture
|
|
234
|
+
match = re.search(r'"(.+?)"', line)
|
|
235
|
+
current_name = match.group(1) if match else line[4:].strip()
|
|
236
|
+
current_desc = ""
|
|
237
|
+
current_params = []
|
|
238
|
+
elif line.startswith("- **") and current_name:
|
|
239
|
+
# Parameter hint: - **Filter cutoff:** 30% -> 65%
|
|
240
|
+
current_params.append({"raw": line.lstrip("- ")})
|
|
241
|
+
elif line.startswith("- **Musical meaning:**"):
|
|
242
|
+
current_desc = line.replace("- **Musical meaning:**", "").strip()
|
|
243
|
+
|
|
244
|
+
if current_name:
|
|
245
|
+
gestures[current_name.lower()] = AutomationGesture(
|
|
246
|
+
name=current_name,
|
|
247
|
+
description=current_desc,
|
|
248
|
+
parameters=current_params,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return gestures
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _parse_genre_chains(text: str) -> dict[str, GenreChain]:
|
|
255
|
+
"""Parse genre chain recipes from chains-genre.md."""
|
|
256
|
+
chains: dict[str, GenreChain] = {}
|
|
257
|
+
current_genre = ""
|
|
258
|
+
current_devices: list[str] = []
|
|
259
|
+
current_desc = ""
|
|
260
|
+
|
|
261
|
+
for line in text.split("\n"):
|
|
262
|
+
line = line.strip()
|
|
263
|
+
if line.startswith("### ") or line.startswith("## "):
|
|
264
|
+
if current_genre and current_devices:
|
|
265
|
+
chains[current_genre.lower()] = GenreChain(
|
|
266
|
+
genre=current_genre,
|
|
267
|
+
devices=current_devices,
|
|
268
|
+
description=current_desc,
|
|
269
|
+
)
|
|
270
|
+
header = line.lstrip("#").strip()
|
|
271
|
+
current_genre = header
|
|
272
|
+
current_devices = []
|
|
273
|
+
current_desc = ""
|
|
274
|
+
elif line.startswith("- **") and current_genre:
|
|
275
|
+
match = re.match(r"- \*\*(.+?)\*\*", line)
|
|
276
|
+
if match:
|
|
277
|
+
current_devices.append(match.group(1))
|
|
278
|
+
elif line and not line.startswith("-") and not line.startswith("#") and current_genre and not current_desc:
|
|
279
|
+
current_desc = line
|
|
280
|
+
|
|
281
|
+
if current_genre and current_devices:
|
|
282
|
+
chains[current_genre.lower()] = GenreChain(
|
|
283
|
+
genre=current_genre,
|
|
284
|
+
devices=current_devices,
|
|
285
|
+
description=current_desc,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
return chains
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _read_file(path: str) -> str:
|
|
292
|
+
"""Read a file, returning empty string on failure."""
|
|
293
|
+
try:
|
|
294
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
295
|
+
return f.read()
|
|
296
|
+
except (OSError, UnicodeDecodeError):
|
|
297
|
+
return ""
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def load_corpus() -> Corpus:
|
|
301
|
+
"""Load and parse the full device-knowledge corpus."""
|
|
302
|
+
corpus_dir = _find_corpus_dir()
|
|
303
|
+
if not corpus_dir:
|
|
304
|
+
return Corpus() # Empty corpus — no files found
|
|
305
|
+
|
|
306
|
+
corpus = Corpus()
|
|
307
|
+
|
|
308
|
+
# Parse creative-thinking.md
|
|
309
|
+
ct_text = _read_file(os.path.join(corpus_dir, "creative-thinking.md"))
|
|
310
|
+
if ct_text:
|
|
311
|
+
# Split by Part headers
|
|
312
|
+
parts = re.split(r"## Part \d+:", ct_text)
|
|
313
|
+
for part in parts:
|
|
314
|
+
if "Emotional-to-Technical" in part:
|
|
315
|
+
corpus.emotional_recipes = _parse_emotional_section(part)
|
|
316
|
+
elif "Physical World" in part:
|
|
317
|
+
corpus.physical_models = _parse_physical_models(part)
|
|
318
|
+
elif "Anti-Patterns" in part:
|
|
319
|
+
# Extract anti-pattern names
|
|
320
|
+
for line in part.split("\n"):
|
|
321
|
+
if line.strip().startswith("### The ") and "Trap" in line:
|
|
322
|
+
corpus.anti_patterns.append(line.strip().lstrip("# "))
|
|
323
|
+
|
|
324
|
+
# Parse automation-as-music.md
|
|
325
|
+
auto_text = _read_file(os.path.join(corpus_dir, "automation-as-music.md"))
|
|
326
|
+
if auto_text:
|
|
327
|
+
parts = re.split(r"## Part \d+:", auto_text)
|
|
328
|
+
for part in parts:
|
|
329
|
+
if "Multi-Parameter" in part or "Macro Gesture" in part:
|
|
330
|
+
corpus.automation_gestures = _parse_automation_gestures(part)
|
|
331
|
+
|
|
332
|
+
# Parse chains-genre.md
|
|
333
|
+
genre_text = _read_file(os.path.join(corpus_dir, "chains-genre.md"))
|
|
334
|
+
if genre_text:
|
|
335
|
+
corpus.genre_chains = _parse_genre_chains(genre_text)
|
|
336
|
+
|
|
337
|
+
# Parse instrument and effect knowledge files
|
|
338
|
+
for filename in ["instruments-synths.md", "effects-distortion.md",
|
|
339
|
+
"effects-space.md", "effects-spectral.md"]:
|
|
340
|
+
file_text = _read_file(os.path.join(corpus_dir, filename))
|
|
341
|
+
if file_text:
|
|
342
|
+
current_device = ""
|
|
343
|
+
current_techniques: list[str] = []
|
|
344
|
+
for line in file_text.split("\n"):
|
|
345
|
+
line = line.strip()
|
|
346
|
+
if line.startswith("## ") or line.startswith("### "):
|
|
347
|
+
if current_device and current_techniques:
|
|
348
|
+
corpus.device_knowledge[current_device.lower()] = DeviceKnowledge(
|
|
349
|
+
device_name=current_device,
|
|
350
|
+
category=filename.replace(".md", ""),
|
|
351
|
+
techniques=current_techniques,
|
|
352
|
+
)
|
|
353
|
+
current_device = line.lstrip("#").strip()
|
|
354
|
+
current_techniques = []
|
|
355
|
+
elif line.startswith("- **") and current_device:
|
|
356
|
+
current_techniques.append(line.lstrip("- "))
|
|
357
|
+
if current_device and current_techniques:
|
|
358
|
+
corpus.device_knowledge[current_device.lower()] = DeviceKnowledge(
|
|
359
|
+
device_name=current_device,
|
|
360
|
+
category=filename.replace(".md", ""),
|
|
361
|
+
techniques=current_techniques,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
return corpus
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ── Module-level lazy singleton ─────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
_corpus_instance: Optional[Corpus] = None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def get_corpus() -> Corpus:
|
|
373
|
+
"""Get the global corpus instance (lazy-loaded on first call)."""
|
|
374
|
+
global _corpus_instance
|
|
375
|
+
if _corpus_instance is None:
|
|
376
|
+
_corpus_instance = load_corpus()
|
|
377
|
+
return _corpus_instance
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Device Forge — programmatic M4L device generation."""
|