livepilot 1.9.23 → 1.10.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/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +119 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +144 -13
- 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-core/SKILL.md +21 -4
- 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 +16 -2
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +19 -5
- package/livepilot/skills/livepilot-sample-engine/SKILL.md +104 -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 +15 -0
- package/livepilot.mcpb +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/manifest.json +2 -2
- 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 +452 -0
- package/mcp_server/composer/layer_planner.py +427 -0
- package/mcp_server/composer/prompt_parser.py +329 -0
- package/mcp_server/composer/tools.py +201 -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/hook_hunter/analyzer.py +23 -0
- package/mcp_server/hook_hunter/models.py +1 -0
- package/mcp_server/hook_hunter/tools.py +4 -2
- package/mcp_server/m4l_bridge.py +1 -0
- package/mcp_server/memory/taste_graph.py +68 -1
- package/mcp_server/memory/tools.py +15 -4
- package/mcp_server/musical_intelligence/detectors.py +14 -1
- package/mcp_server/musical_intelligence/tools.py +11 -8
- package/mcp_server/persistence/__init__.py +1 -0
- package/mcp_server/persistence/base_store.py +82 -0
- package/mcp_server/persistence/project_store.py +106 -0
- package/mcp_server/persistence/taste_store.py +122 -0
- package/mcp_server/preview_studio/models.py +1 -0
- package/mcp_server/preview_studio/tools.py +56 -13
- package/mcp_server/runtime/capability.py +66 -0
- package/mcp_server/runtime/capability_probe.py +137 -0
- package/mcp_server/runtime/execution_router.py +143 -0
- package/mcp_server/runtime/live_version.py +102 -0
- package/mcp_server/runtime/remote_commands.py +87 -0
- package/mcp_server/runtime/tools.py +18 -4
- 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/sources.py +540 -0
- package/mcp_server/sample_engine/techniques.py +908 -0
- package/mcp_server/sample_engine/tools.py +442 -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 +41 -41
- package/mcp_server/semantic_moves/performance_moves.py +13 -13
- package/mcp_server/semantic_moves/sample_compilers.py +372 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +15 -15
- package/mcp_server/semantic_moves/tools.py +18 -17
- package/mcp_server/semantic_moves/transition_moves.py +16 -16
- package/mcp_server/server.py +51 -0
- package/mcp_server/services/__init__.py +1 -0
- package/mcp_server/services/motif_service.py +67 -0
- package/mcp_server/session_continuity/tracker.py +29 -1
- package/mcp_server/song_brain/builder.py +28 -1
- package/mcp_server/song_brain/models.py +4 -0
- package/mcp_server/song_brain/tools.py +20 -2
- 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/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/wonder_mode/diagnosis.py +5 -0
- package/mcp_server/wonder_mode/engine.py +85 -1
- package/mcp_server/wonder_mode/tools.py +6 -1
- package/package.json +12 -2
- 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 +236 -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/scripts/sync_metadata.py +132 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Device Atlas scanner — transforms raw browser scan data into atlas entries.
|
|
3
|
+
|
|
4
|
+
Converts the flat {categories: {cat: [items]}} payload from scan_browser_deep
|
|
5
|
+
into normalised device dicts ready for enrichment and querying.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ── ID generation ────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
def make_device_id(name: str, prefix: str = "") -> str:
|
|
17
|
+
"""Convert a human-readable device name to a snake_case identifier.
|
|
18
|
+
|
|
19
|
+
>>> make_device_id("EQ Eight")
|
|
20
|
+
'eq_eight'
|
|
21
|
+
>>> make_device_id("Model D", prefix="auv3_moog")
|
|
22
|
+
'auv3_moog_model_d'
|
|
23
|
+
"""
|
|
24
|
+
slug = re.sub(r"[^a-zA-Z0-9]+", "_", name).strip("_").lower()
|
|
25
|
+
if prefix:
|
|
26
|
+
prefix_slug = re.sub(r"[^a-zA-Z0-9]+", "_", prefix).strip("_").lower()
|
|
27
|
+
return f"{prefix_slug}_{slug}"
|
|
28
|
+
return slug
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ── Category / subcategory mapping ───────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
_CATEGORY_MAP: dict[str, str] = {
|
|
34
|
+
"instruments": "instruments",
|
|
35
|
+
"audio_effects": "audio_effects",
|
|
36
|
+
"midi_effects": "midi_effects",
|
|
37
|
+
"drums": "drum_kits",
|
|
38
|
+
"max_for_live": "max_for_live",
|
|
39
|
+
"plugins": "plugins",
|
|
40
|
+
"sounds": "sounds",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_INSTRUMENT_SUBCATEGORIES: dict[str, str] = {
|
|
44
|
+
# Synths
|
|
45
|
+
"analog": "synths",
|
|
46
|
+
"wavetable": "synths",
|
|
47
|
+
"operator": "synths",
|
|
48
|
+
"drift": "synths",
|
|
49
|
+
"meld": "synths",
|
|
50
|
+
"emit": "synths",
|
|
51
|
+
"poli": "synths",
|
|
52
|
+
"tree_tone": "synths",
|
|
53
|
+
"vector_fm": "synths",
|
|
54
|
+
"vector_grain": "synths",
|
|
55
|
+
"bass": "synths",
|
|
56
|
+
# Physical modelling
|
|
57
|
+
"collision": "physical_modeling",
|
|
58
|
+
"tension": "physical_modeling",
|
|
59
|
+
"electric": "physical_modeling",
|
|
60
|
+
# Samplers
|
|
61
|
+
"simpler": "samplers",
|
|
62
|
+
"sampler": "samplers",
|
|
63
|
+
# Drums
|
|
64
|
+
"drum_rack": "drums",
|
|
65
|
+
"drum_sampler": "drums",
|
|
66
|
+
"impulse": "drums",
|
|
67
|
+
# Racks
|
|
68
|
+
"instrument_rack": "racks",
|
|
69
|
+
# Routing
|
|
70
|
+
"external_instrument": "routing",
|
|
71
|
+
# Granular
|
|
72
|
+
"granulator_iii": "granular",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_AUDIO_EFFECT_SUBCATEGORIES: dict[str, str] = {
|
|
76
|
+
# Dynamics
|
|
77
|
+
"compressor": "dynamics",
|
|
78
|
+
"glue_compressor": "dynamics",
|
|
79
|
+
"limiter": "dynamics",
|
|
80
|
+
"color_limiter": "dynamics",
|
|
81
|
+
"multiband_dynamics": "dynamics",
|
|
82
|
+
"gate": "dynamics",
|
|
83
|
+
"drum_buss": "dynamics",
|
|
84
|
+
"re_enveloper": "dynamics",
|
|
85
|
+
# EQ
|
|
86
|
+
"eq_eight": "eq",
|
|
87
|
+
"eq_three": "eq",
|
|
88
|
+
"channel_eq": "eq",
|
|
89
|
+
# Filter
|
|
90
|
+
"auto_filter": "filter",
|
|
91
|
+
"spectral_resonator": "filter",
|
|
92
|
+
# Delay
|
|
93
|
+
"delay": "delay",
|
|
94
|
+
"echo": "delay",
|
|
95
|
+
"grain_delay": "delay",
|
|
96
|
+
"filter_delay": "delay",
|
|
97
|
+
"gated_delay": "delay",
|
|
98
|
+
"vector_delay": "delay",
|
|
99
|
+
"beat_repeat": "delay",
|
|
100
|
+
"spectral_time": "delay",
|
|
101
|
+
"align_delay": "delay",
|
|
102
|
+
# Reverb
|
|
103
|
+
"reverb": "reverb",
|
|
104
|
+
"hybrid_reverb": "reverb",
|
|
105
|
+
"convolution_reverb": "reverb",
|
|
106
|
+
"convolution_reverb_pro": "reverb",
|
|
107
|
+
# Distortion
|
|
108
|
+
"saturator": "distortion",
|
|
109
|
+
"overdrive": "distortion",
|
|
110
|
+
"pedal": "distortion",
|
|
111
|
+
"roar": "distortion",
|
|
112
|
+
"dynamic_tube": "distortion",
|
|
113
|
+
"erosion": "distortion",
|
|
114
|
+
"redux": "distortion",
|
|
115
|
+
"vinyl_distortion": "distortion",
|
|
116
|
+
"amp": "distortion",
|
|
117
|
+
"cabinet": "distortion",
|
|
118
|
+
# Modulation
|
|
119
|
+
"chorus_ensemble": "modulation",
|
|
120
|
+
"phaser_flanger": "modulation",
|
|
121
|
+
"shifter": "modulation",
|
|
122
|
+
"auto_pan_tremolo": "modulation",
|
|
123
|
+
"auto_shift": "modulation",
|
|
124
|
+
"shaper": "modulation",
|
|
125
|
+
"lfo": "modulation",
|
|
126
|
+
"envelope_follower": "modulation",
|
|
127
|
+
"vector_map": "modulation",
|
|
128
|
+
# Utility
|
|
129
|
+
"utility": "utility",
|
|
130
|
+
"spectrum": "utility",
|
|
131
|
+
"tuner": "utility",
|
|
132
|
+
"variations": "utility",
|
|
133
|
+
"prearranger": "utility",
|
|
134
|
+
# Spatial
|
|
135
|
+
"surround_panner": "spatial",
|
|
136
|
+
# Performance
|
|
137
|
+
"looper": "performance",
|
|
138
|
+
"arrangement_looper": "performance",
|
|
139
|
+
"performer": "performance",
|
|
140
|
+
# Spectral
|
|
141
|
+
"spectral_blur": "spectral",
|
|
142
|
+
# Pitch
|
|
143
|
+
"pitch_hack": "pitch",
|
|
144
|
+
"pitchloop89": "pitch",
|
|
145
|
+
# Physical modelling
|
|
146
|
+
"corpus": "physical_modeling",
|
|
147
|
+
"resonators": "physical_modeling",
|
|
148
|
+
# Special
|
|
149
|
+
"vocoder": "special",
|
|
150
|
+
# Racks
|
|
151
|
+
"audio_effect_rack": "racks",
|
|
152
|
+
# Routing
|
|
153
|
+
"external_audio_effect": "routing",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _classify_subcategory(device_id: str, category: str) -> str:
|
|
158
|
+
"""Return the subcategory for a device based on its id and category."""
|
|
159
|
+
if category == "instruments":
|
|
160
|
+
return _INSTRUMENT_SUBCATEGORIES.get(device_id, "other")
|
|
161
|
+
if category == "audio_effects":
|
|
162
|
+
return _AUDIO_EFFECT_SUBCATEGORIES.get(device_id, "other")
|
|
163
|
+
return "other"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ── Empty device template ────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
def _empty_device(
|
|
169
|
+
device_id: str,
|
|
170
|
+
name: str,
|
|
171
|
+
uri: str | None,
|
|
172
|
+
category: str,
|
|
173
|
+
subcategory: str,
|
|
174
|
+
source: str,
|
|
175
|
+
) -> dict[str, Any]:
|
|
176
|
+
"""Return a skeleton device dict with all atlas fields set to defaults."""
|
|
177
|
+
return {
|
|
178
|
+
"id": device_id,
|
|
179
|
+
"name": name,
|
|
180
|
+
"uri": uri,
|
|
181
|
+
"category": category,
|
|
182
|
+
"subcategory": subcategory,
|
|
183
|
+
"source": source,
|
|
184
|
+
"enriched": False,
|
|
185
|
+
"character_tags": [],
|
|
186
|
+
"use_cases": [],
|
|
187
|
+
"genre_affinity": {"primary": [], "secondary": []},
|
|
188
|
+
"self_contained": True,
|
|
189
|
+
"key_parameters": [],
|
|
190
|
+
"pairs_well_with": [],
|
|
191
|
+
"starter_recipes": [],
|
|
192
|
+
"gotchas": [],
|
|
193
|
+
"health_flags": [],
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ── Normaliser ───────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
def normalize_scan_results(raw_scan: dict[str, Any]) -> list[dict[str, Any]]:
|
|
200
|
+
"""Convert raw scan_browser_deep output to a flat list of device dicts.
|
|
201
|
+
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
raw_scan : dict
|
|
205
|
+
``{"categories": {cat_name: [{"name", "uri", "is_loadable"}, ...], ...}}``
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
list[dict]
|
|
210
|
+
Deduplicated device entries with all atlas fields initialised.
|
|
211
|
+
"""
|
|
212
|
+
categories_data = raw_scan.get("categories", {})
|
|
213
|
+
seen_uris: set[str] = set()
|
|
214
|
+
devices: list[dict[str, Any]] = []
|
|
215
|
+
|
|
216
|
+
for raw_cat, items in categories_data.items():
|
|
217
|
+
category = _CATEGORY_MAP.get(raw_cat, raw_cat)
|
|
218
|
+
source = "native" if raw_cat in _CATEGORY_MAP else raw_cat
|
|
219
|
+
|
|
220
|
+
for item in items:
|
|
221
|
+
name = item.get("name", "")
|
|
222
|
+
uri = item.get("uri")
|
|
223
|
+
|
|
224
|
+
# Deduplicate by URI when available
|
|
225
|
+
if uri and uri in seen_uris:
|
|
226
|
+
continue
|
|
227
|
+
if uri:
|
|
228
|
+
seen_uris.add(uri)
|
|
229
|
+
|
|
230
|
+
device_id = make_device_id(name)
|
|
231
|
+
subcategory = _classify_subcategory(device_id, category)
|
|
232
|
+
devices.append(
|
|
233
|
+
_empty_device(device_id, name, uri, category, subcategory, source)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return devices
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Atlas MCP tools — search, suggest, compare, and scan the device database.
|
|
2
|
+
|
|
3
|
+
6 tools for the atlas domain.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from fastmcp import Context
|
|
13
|
+
|
|
14
|
+
from ..server import mcp
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_ableton(ctx: Context):
|
|
18
|
+
return ctx.lifespan_context["ableton"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_atlas():
|
|
22
|
+
"""Get the global AtlasManager instance, loading lazily if needed."""
|
|
23
|
+
from . import _atlas_instance, _load_atlas
|
|
24
|
+
if _atlas_instance is None:
|
|
25
|
+
try:
|
|
26
|
+
_load_atlas()
|
|
27
|
+
except FileNotFoundError:
|
|
28
|
+
return None
|
|
29
|
+
from . import _atlas_instance as inst
|
|
30
|
+
return inst
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@mcp.tool()
|
|
34
|
+
def atlas_search(ctx: Context, query: str, category: str = "all", limit: int = 10) -> dict:
|
|
35
|
+
"""Search the device atlas for instruments, effects, kits, or plugins.
|
|
36
|
+
|
|
37
|
+
query: natural language search — name, sonic character, use case, or genre
|
|
38
|
+
Examples: "warm analog bass", "reverb", "808 kit", "granular"
|
|
39
|
+
category: filter by category (all, instruments, audio_effects, midi_effects,
|
|
40
|
+
max_for_live, drum_kits, plugins)
|
|
41
|
+
limit: max results (default 10)
|
|
42
|
+
"""
|
|
43
|
+
atlas = _get_atlas()
|
|
44
|
+
if atlas is None:
|
|
45
|
+
return {"error": "Atlas not loaded. Run scan_full_library first.", "results": []}
|
|
46
|
+
|
|
47
|
+
results = atlas.search(query, category=category, limit=limit)
|
|
48
|
+
return {
|
|
49
|
+
"query": query,
|
|
50
|
+
"category": category,
|
|
51
|
+
"count": len(results),
|
|
52
|
+
"results": [
|
|
53
|
+
{
|
|
54
|
+
"id": r["device"].get("id", ""),
|
|
55
|
+
"name": r["device"].get("name", ""),
|
|
56
|
+
"uri": r["device"].get("uri", ""),
|
|
57
|
+
"category": r["device"].get("category", ""),
|
|
58
|
+
"sonic_description": r["device"].get("sonic_description", "")[:120],
|
|
59
|
+
"character_tags": r["device"].get("character_tags", [])[:5],
|
|
60
|
+
"enriched": r["device"].get("enriched", False),
|
|
61
|
+
"score": r.get("score", 0),
|
|
62
|
+
}
|
|
63
|
+
for r in results
|
|
64
|
+
],
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@mcp.tool()
|
|
69
|
+
def atlas_device_info(ctx: Context, device_id: str) -> dict:
|
|
70
|
+
"""Get complete atlas knowledge about a device — parameters, recipes, pairings, gotchas.
|
|
71
|
+
|
|
72
|
+
device_id: the atlas ID or device name (e.g., "drift", "Compressor", "808_core_kit")
|
|
73
|
+
"""
|
|
74
|
+
atlas = _get_atlas()
|
|
75
|
+
if atlas is None:
|
|
76
|
+
return {"error": "Atlas not loaded. Run scan_full_library first."}
|
|
77
|
+
|
|
78
|
+
entry = atlas.lookup(device_id)
|
|
79
|
+
if entry is None:
|
|
80
|
+
return {"error": f"Device '{device_id}' not found in atlas", "suggestion": "Use atlas_search to find devices"}
|
|
81
|
+
return entry
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@mcp.tool()
|
|
85
|
+
def atlas_suggest(
|
|
86
|
+
ctx: Context,
|
|
87
|
+
intent: str,
|
|
88
|
+
genre: str = "",
|
|
89
|
+
energy: str = "medium",
|
|
90
|
+
key: str = "",
|
|
91
|
+
) -> dict:
|
|
92
|
+
"""Suggest devices for a production intent.
|
|
93
|
+
|
|
94
|
+
intent: what you're trying to achieve — "warm bass", "crispy hi-hats", "evolving texture"
|
|
95
|
+
genre: target genre for better recommendations
|
|
96
|
+
energy: low/medium/high — affects sonic character suggestions
|
|
97
|
+
key: musical key context (e.g., "Cm") for tuned percussion suggestions
|
|
98
|
+
"""
|
|
99
|
+
atlas = _get_atlas()
|
|
100
|
+
if atlas is None:
|
|
101
|
+
return {"error": "Atlas not loaded. Run scan_full_library first."}
|
|
102
|
+
|
|
103
|
+
results = atlas.suggest(intent, genre=genre, energy=energy)
|
|
104
|
+
return {
|
|
105
|
+
"intent": intent,
|
|
106
|
+
"genre": genre,
|
|
107
|
+
"energy": energy,
|
|
108
|
+
"suggestions": [
|
|
109
|
+
{
|
|
110
|
+
"device_id": r["device"]["id"],
|
|
111
|
+
"device_name": r["device"]["name"],
|
|
112
|
+
"uri": r["device"].get("uri", ""),
|
|
113
|
+
"rationale": r["rationale"],
|
|
114
|
+
"recipe": r.get("recipe"),
|
|
115
|
+
}
|
|
116
|
+
for r in results
|
|
117
|
+
],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@mcp.tool()
|
|
122
|
+
def atlas_chain_suggest(ctx: Context, role: str, genre: str = "") -> dict:
|
|
123
|
+
"""Suggest a full device chain for a track role.
|
|
124
|
+
|
|
125
|
+
role: the musical role — "bass", "lead", "pad", "drums", "percussion", "texture"
|
|
126
|
+
genre: target genre for style-appropriate choices
|
|
127
|
+
"""
|
|
128
|
+
atlas = _get_atlas()
|
|
129
|
+
if atlas is None:
|
|
130
|
+
return {"error": "Atlas not loaded. Run scan_full_library first."}
|
|
131
|
+
|
|
132
|
+
return atlas.chain_suggest(role, genre=genre)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@mcp.tool()
|
|
136
|
+
def atlas_compare(ctx: Context, device_a: str, device_b: str, role: str = "") -> dict:
|
|
137
|
+
"""Compare two devices — strengths, weaknesses, and recommendation for a role.
|
|
138
|
+
|
|
139
|
+
device_a: first device name or ID
|
|
140
|
+
device_b: second device name or ID
|
|
141
|
+
role: optional role context (e.g., "bass", "pad")
|
|
142
|
+
"""
|
|
143
|
+
atlas = _get_atlas()
|
|
144
|
+
if atlas is None:
|
|
145
|
+
return {"error": "Atlas not loaded. Run scan_full_library first."}
|
|
146
|
+
|
|
147
|
+
return atlas.compare(device_a, device_b, role=role)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@mcp.tool()
|
|
151
|
+
def scan_full_library(ctx: Context, force: bool = False) -> dict:
|
|
152
|
+
"""Scan the full Ableton browser and rebuild the device atlas.
|
|
153
|
+
|
|
154
|
+
Walks every category (instruments, audio_effects, midi_effects, max_for_live,
|
|
155
|
+
drums, plugins, packs) and records every loadable item with its URI.
|
|
156
|
+
Results are merged with curated enrichments and saved to device_atlas.json.
|
|
157
|
+
|
|
158
|
+
force: if True, rescan even if atlas already exists (default False)
|
|
159
|
+
"""
|
|
160
|
+
from .scanner import normalize_scan_results
|
|
161
|
+
from .enrichments import load_enrichments, merge_enrichments
|
|
162
|
+
from . import AtlasManager
|
|
163
|
+
|
|
164
|
+
atlas_dir = os.path.dirname(os.path.abspath(__file__))
|
|
165
|
+
atlas_path = os.path.join(atlas_dir, "device_atlas.json")
|
|
166
|
+
enrichments_dir = os.path.join(atlas_dir, "enrichments")
|
|
167
|
+
|
|
168
|
+
if not force and os.path.exists(atlas_path):
|
|
169
|
+
age = time.time() - os.path.getmtime(atlas_path)
|
|
170
|
+
if age < 86400:
|
|
171
|
+
# Reload if not already loaded
|
|
172
|
+
import mcp_server.atlas as atlas_mod
|
|
173
|
+
if atlas_mod._atlas_instance is None:
|
|
174
|
+
atlas_mod._atlas_instance = AtlasManager(atlas_path)
|
|
175
|
+
return {
|
|
176
|
+
"status": "already_exists",
|
|
177
|
+
"age_hours": round(age / 3600, 1),
|
|
178
|
+
"device_count": atlas_mod._atlas_instance.device_count,
|
|
179
|
+
"message": "Atlas is recent. Use force=True to rescan.",
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Scan browser
|
|
183
|
+
ableton = _get_ableton(ctx)
|
|
184
|
+
raw = ableton.send_command("scan_browser_deep", {"max_per_category": 1000})
|
|
185
|
+
|
|
186
|
+
# Normalize
|
|
187
|
+
devices = normalize_scan_results(raw)
|
|
188
|
+
|
|
189
|
+
# Load and merge enrichments
|
|
190
|
+
enrichments = load_enrichments(enrichments_dir)
|
|
191
|
+
devices = merge_enrichments(devices, enrichments)
|
|
192
|
+
|
|
193
|
+
# Count stats
|
|
194
|
+
stats: dict = {"total_devices": len(devices)}
|
|
195
|
+
for device in devices:
|
|
196
|
+
cat = device.get("category", "other")
|
|
197
|
+
stats[cat] = stats.get(cat, 0) + 1
|
|
198
|
+
stats["enriched_devices"] = sum(1 for d in devices if d.get("enriched"))
|
|
199
|
+
|
|
200
|
+
# Build atlas
|
|
201
|
+
atlas_data = {
|
|
202
|
+
"version": "2.0.0",
|
|
203
|
+
"live_version": "12.3.6",
|
|
204
|
+
"scanned_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
205
|
+
"stats": stats,
|
|
206
|
+
"devices": devices,
|
|
207
|
+
"packs": [],
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Write
|
|
211
|
+
with open(atlas_path, "w") as f:
|
|
212
|
+
json.dump(atlas_data, f, indent=2)
|
|
213
|
+
|
|
214
|
+
# Reload into global
|
|
215
|
+
import mcp_server.atlas as atlas_mod
|
|
216
|
+
atlas_mod._atlas_instance = AtlasManager(atlas_path)
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
"status": "scanned",
|
|
220
|
+
"device_count": len(devices),
|
|
221
|
+
"enriched_count": stats["enriched_devices"],
|
|
222
|
+
"stats": stats,
|
|
223
|
+
"atlas_path": atlas_path,
|
|
224
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Composer Engine — auto-composition from text prompts via Splice + Sample Engine."""
|