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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Sample Technique Catalog — 29 Recipes
|
|
2
|
+
|
|
3
|
+
## Category 1: Rhythmic Sampling
|
|
4
|
+
|
|
5
|
+
### slice_and_sequence (Surgeon)
|
|
6
|
+
Classic MPC workflow: load loop, slice on transients, sequence with MIDI.
|
|
7
|
+
**Material:** drum_loop, full_mix | **Steps:** load -> slice -> get slices -> create clip -> add notes
|
|
8
|
+
|
|
9
|
+
### vocal_chop_rhythm (Alchemist)
|
|
10
|
+
Chop vocal into syllable-length slices, trigger as staccato rhythm. Burial-inspired.
|
|
11
|
+
**Material:** vocal | **Steps:** load -> slice by region -> MIDI staccato -> Auto Filter
|
|
12
|
+
|
|
13
|
+
### micro_chop (Alchemist)
|
|
14
|
+
1/32 slices, varied velocity, slight timing offsets. J Dilla micro-timing.
|
|
15
|
+
**Material:** any loop | **Steps:** load -> manual slice -> dense 1/32 MIDI
|
|
16
|
+
|
|
17
|
+
### stab_isolation (Surgeon)
|
|
18
|
+
Isolate single chord stab, crop, retrigger rhythmically. DJ Premier style.
|
|
19
|
+
**Material:** full_mix, instrument_loop | **Steps:** load -> classic mode -> crop -> MIDI
|
|
20
|
+
|
|
21
|
+
### euclidean_slice_trigger (Alchemist)
|
|
22
|
+
Map Simpler slices to Euclidean rhythm for polyrhythmic texture.
|
|
23
|
+
**Material:** drum_loop, vocal, instrument_loop | **Steps:** load -> slice -> Euclidean pattern
|
|
24
|
+
|
|
25
|
+
## Category 2: Textural Transformation
|
|
26
|
+
|
|
27
|
+
### extreme_stretch (Alchemist)
|
|
28
|
+
Paulstretch-style: Texture warp at 10-50x, reverb wash. Stars of the Lid territory.
|
|
29
|
+
**Material:** any | **Steps:** load -> warp 64 beats -> Reverb 60-80% wet
|
|
30
|
+
|
|
31
|
+
### drum_to_pad (Alchemist)
|
|
32
|
+
Reverse + extreme stretch + reverb = drum hit becomes ambient pad.
|
|
33
|
+
**Material:** drum_loop, one_shot | **Steps:** load -> reverse -> stretch -> Reverb
|
|
34
|
+
|
|
35
|
+
### reverse_layer (Alchemist)
|
|
36
|
+
Reversed sample as pre-echo swell or ghostly texture.
|
|
37
|
+
**Material:** vocal, instrument_loop, one_shot | **Steps:** load -> reverse -> Delay
|
|
38
|
+
|
|
39
|
+
### granular_scatter (Alchemist)
|
|
40
|
+
Grain Delay as granular engine — scatter grains for cloud textures. Amon Tobin.
|
|
41
|
+
**Material:** vocal, instrument_loop, texture | **Steps:** load -> Grain Delay -> Reverb
|
|
42
|
+
|
|
43
|
+
### spectral_freeze (Alchemist)
|
|
44
|
+
Crop to tiny region (50-200ms), extreme stretch = spectral freeze drone.
|
|
45
|
+
**Material:** vocal, instrument_loop, full_mix | **Steps:** load -> crop -> stretch 64 beats
|
|
46
|
+
|
|
47
|
+
### tail_harvest (Alchemist)
|
|
48
|
+
Resample only the reverb/delay tail as independent texture.
|
|
49
|
+
**Material:** any | **Steps:** load -> Reverb 100% wet -> level for subtle layer
|
|
50
|
+
|
|
51
|
+
## Category 3: Melodic/Harmonic
|
|
52
|
+
|
|
53
|
+
### key_matched_layer (Surgeon)
|
|
54
|
+
Transpose to song key, EQ carve, blend behind existing elements.
|
|
55
|
+
**Material:** instrument_loop | **Steps:** load -> transpose -> EQ -> volume -6 to -10dB
|
|
56
|
+
|
|
57
|
+
### vocal_harmony_stack (Surgeon)
|
|
58
|
+
Pitch-shifted vocal layers — Bon Iver Prismizer approach.
|
|
59
|
+
**Material:** vocal | **Steps:** load -> duplicate -> transpose +3/+5/+7 -> blend
|
|
60
|
+
|
|
61
|
+
### counterpoint_from_chops (Alchemist)
|
|
62
|
+
Create countermelody from rearranged melodic fragments. Four Tet.
|
|
63
|
+
**Material:** instrument_loop, vocal, full_mix | **Steps:** load -> slice by beat -> program melody
|
|
64
|
+
|
|
65
|
+
### chord_stab_extraction (Surgeon)
|
|
66
|
+
Isolate chord from full mix, crop, retrigger. Classic house/disco.
|
|
67
|
+
**Material:** full_mix, instrument_loop | **Steps:** load -> slice -> crop -> rhythmic pattern
|
|
68
|
+
|
|
69
|
+
## Category 4: Drum Enhancement
|
|
70
|
+
|
|
71
|
+
### break_layering (Surgeon)
|
|
72
|
+
Layer drum break under programmed drums. High-pass to avoid kick clash.
|
|
73
|
+
**Material:** drum_loop | **Steps:** load -> warp 16 beats -> EQ HP 200-400Hz -> volume -10 to -15dB
|
|
74
|
+
|
|
75
|
+
### ghost_note_texture (Alchemist)
|
|
76
|
+
Heavy filter + low volume = barely audible ghost-note layer.
|
|
77
|
+
**Material:** drum_loop | **Steps:** load -> Auto Filter BP 1-4kHz -> volume -18 to -24dB
|
|
78
|
+
|
|
79
|
+
### transient_replacement (Surgeon)
|
|
80
|
+
Layer one-shot transient over existing drums for punch.
|
|
81
|
+
**Material:** one_shot | **Steps:** load -> classic mode -> MIDI on kick/snare hits
|
|
82
|
+
|
|
83
|
+
### shuffle_extract (Alchemist)
|
|
84
|
+
Extract groove timing from loop via slice positions, apply to MIDI.
|
|
85
|
+
**Material:** drum_loop | **Steps:** load -> slice by transient -> read positions
|
|
86
|
+
|
|
87
|
+
## Category 5: Vocal Processing
|
|
88
|
+
|
|
89
|
+
### syllable_instrument (Alchemist)
|
|
90
|
+
Each syllable = playable note. Vocal becomes a melodic instrument.
|
|
91
|
+
**Material:** vocal | **Steps:** load -> slice by region -> program melody across slices
|
|
92
|
+
|
|
93
|
+
### formant_shift_character (Alchemist)
|
|
94
|
+
Shift formants for alien/robotic character. Transpose +/-12st.
|
|
95
|
+
**Material:** vocal | **Steps:** load -> transpose -> Corpus for resonant body
|
|
96
|
+
|
|
97
|
+
### vocal_freeze_drone (Alchemist)
|
|
98
|
+
Sustain one vowel as ambient pad. Crop tiny region, extreme stretch.
|
|
99
|
+
**Material:** vocal | **Steps:** load -> crop 100-300ms -> stretch 64 beats -> Chorus -> Reverb
|
|
100
|
+
|
|
101
|
+
### phone_recording_texture (Alchemist)
|
|
102
|
+
Burial signature: pitch down, lo-pass, ghost-level volume.
|
|
103
|
+
**Material:** vocal, foley | **Steps:** load -> pitch -5 to -12 -> LP 800Hz -> volume -20 to -30dB
|
|
104
|
+
|
|
105
|
+
## Category 6: Resampling Chains
|
|
106
|
+
|
|
107
|
+
### serial_resample (Alchemist)
|
|
108
|
+
Multi-pass destruction: Saturator + Grain Delay + Reverb, freeze, flatten, repeat.
|
|
109
|
+
**Material:** any | **Steps:** load -> Saturator -> Grain Delay -> Reverb -> freeze -> flatten
|
|
110
|
+
|
|
111
|
+
### parallel_resample (Alchemist)
|
|
112
|
+
Duplicate, process one copy destructively, blend wet/dry.
|
|
113
|
+
**Material:** any | **Steps:** load -> duplicate -> process duplicate -> blend -6 to -12dB
|
|
114
|
+
|
|
115
|
+
### freeze_flatten_rechop (Alchemist)
|
|
116
|
+
Freeze processed material, flatten to audio, re-slice the result. Recursive.
|
|
117
|
+
**Material:** any | **Steps:** freeze -> flatten -> re-load into Simpler -> slice again
|
|
118
|
+
|
|
119
|
+
## Category 7: Creative Constraints
|
|
120
|
+
|
|
121
|
+
### one_sample_challenge (Alchemist)
|
|
122
|
+
Build entire beat from one sample: kick, snare, hat, bass, pad all from slices.
|
|
123
|
+
**Material:** any | **Steps:** load -> slice -> program full beat across slice pitches
|
|
124
|
+
|
|
125
|
+
### found_sound_only (Alchemist)
|
|
126
|
+
Non-musical field recordings as sole source material. Musique concrete.
|
|
127
|
+
**Material:** foley | **Steps:** load -> slice -> EQ to isolate musical frequencies
|
|
128
|
+
|
|
129
|
+
### reverse_engineering (Both)
|
|
130
|
+
Recreate a reference track's texture by sampling and transforming similar elements.
|
|
131
|
+
**Material:** full_mix | **Steps:** load -> slice by beat -> analyze structure
|
|
@@ -121,3 +121,48 @@ When adding processing blocks, prefer native Ableton devices for controllability
|
|
|
121
121
|
- **Saturator** — waveshaping with multiple curve types
|
|
122
122
|
|
|
123
123
|
Always `search_browser` before loading — never guess device names.
|
|
124
|
+
|
|
125
|
+
## Deep Sound Design Reference
|
|
126
|
+
|
|
127
|
+
Consult `references/sound-design-deep.md` for advanced techniques when working on creative requests. Key principles:
|
|
128
|
+
|
|
129
|
+
### Making Sounds Breathe
|
|
130
|
+
Every static sound can become alive with modulation below conscious perception:
|
|
131
|
+
- **Filter breathing:** LFO at 0.1-0.5 Hz on filter cutoff, 5-15% depth
|
|
132
|
+
- **Oscillator drift:** ±1-3 cent detune with very slow LFO (0.05-0.2 Hz)
|
|
133
|
+
- **Amplitude micro-variation:** Perlin/brownian noise on volume, ±1-3 dB
|
|
134
|
+
- **Rule:** If the listener can hear the modulation, it's too much. The best modulation is felt, not heard.
|
|
135
|
+
|
|
136
|
+
### Space as Composition
|
|
137
|
+
Reverb and delay are not decorations — in dub/minimal they ARE the composition:
|
|
138
|
+
- **Dub chord:** Short stab → long delay (70-80% feedback) + filter on the delay return
|
|
139
|
+
- **Delay throws:** Momentary send spikes (0→70% for half a beat) — the echo IS the event
|
|
140
|
+
- **Sidechain reverb:** Dry drums trigger sidechain compression on reverb returns — the room pulses
|
|
141
|
+
- **Feedback modulation:** Delay feedback at 75-85% + modulate delay time ±5-10% for warped echoes
|
|
142
|
+
|
|
143
|
+
### Creative Sidechain (Beyond Pump)
|
|
144
|
+
Sidechain compression is a modulation source, not just a mix tool:
|
|
145
|
+
- **Sidechain filter:** Envelope follower from kick modulates pad filter cutoff — pad brightens between kicks
|
|
146
|
+
- **Ghost sidechain:** Muted kick as sidechain source for textures — phantom groove on non-rhythmic elements
|
|
147
|
+
- **Multiband sidechain:** Only duck sub frequencies from pad — shimmer stays, sub clears for kick
|
|
148
|
+
|
|
149
|
+
### Effects as Instruments
|
|
150
|
+
- **Self-oscillating filter:** Push resonance until it rings, play notes by changing cutoff
|
|
151
|
+
- **Feedback loops:** Route output back to input through effects + compressor to control
|
|
152
|
+
- **Convolution as synthesis:** Load non-IR files (speech, drum break) into convolution reverb — imprints spectral character
|
|
153
|
+
- **Granular reverb:** Very short reverb (0.1-0.3s) high diffusion on percussion — smears transient into tonal cloud
|
|
154
|
+
|
|
155
|
+
### The Frequency Dance
|
|
156
|
+
At any moment, each frequency band should have one primary element. When one opens up, another pulls back:
|
|
157
|
+
- Chord filter opens into highs → pull hi-hat back
|
|
158
|
+
- Bass drops → kick shortens
|
|
159
|
+
- Reverb tail fills → dry elements duck
|
|
160
|
+
This is mix engineering as composition.
|
|
161
|
+
|
|
162
|
+
### When to Apply These
|
|
163
|
+
- User says "make it breathe" or "it sounds static" → micro-modulation
|
|
164
|
+
- User says "more space" or "deeper" → dub techniques (delay throws, reverb composition)
|
|
165
|
+
- User says "more groove" or "make it pump" → creative sidechain
|
|
166
|
+
- User says "more texture" or "more complex" → textural layering
|
|
167
|
+
- User says "surprise me" or "WTF moment" → brief textural disruption (2-8 beats max)
|
|
168
|
+
- User says "warmer" or "more analog" → oscillator drift + subtle saturation + filter breathing
|
|
@@ -60,3 +60,20 @@ For the recommendation, explain:
|
|
|
60
60
|
- Why this one over the others
|
|
61
61
|
- What risk it introduces
|
|
62
62
|
- What sacred elements it preserves
|
|
63
|
+
|
|
64
|
+
## Creative Intelligence (consult before generating variants)
|
|
65
|
+
|
|
66
|
+
Wonder Mode should produce musically interesting results, not just technically correct ones. Before generating or applying any variant:
|
|
67
|
+
|
|
68
|
+
1. Read `references/device-knowledge/automation-as-music.md` for automation shapes and macro gestures
|
|
69
|
+
2. Read `references/device-knowledge/creative-thinking.md` for emotional-to-technical mapping
|
|
70
|
+
3. Read `references/device-knowledge/chains-genre.md` if the session has a genre identity
|
|
71
|
+
|
|
72
|
+
When reviewing Wonder variants, aim for musical depth:
|
|
73
|
+
- **Filter arcs** — evolving filter across sections adds movement
|
|
74
|
+
- **Space arcs** — reverb/delay sends breathing with density
|
|
75
|
+
- **Micro-modulation** — subtle LFOs on sustained sounds
|
|
76
|
+
- **Macro gestures** — coordinated multi-parameter moves at transitions
|
|
77
|
+
|
|
78
|
+
Note: these are agent-level guidelines, not enforced by the Wonder engine.
|
|
79
|
+
The engine generates variants from semantic moves; the agent adds musical polish.
|
package/livepilot.mcpb
CHANGED
|
Binary file
|
package/manifest.json
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "livepilot",
|
|
4
4
|
"display_name": "LivePilot — AI for Ableton Live",
|
|
5
|
-
"version": "1.
|
|
6
|
-
"description": "Agentic production system for Ableton Live 12. Make beats, mix tracks, design sounds, and arrange songs with
|
|
7
|
-
"long_description": "LivePilot is an
|
|
5
|
+
"version": "1.10.1",
|
|
6
|
+
"description": "Agentic production system for Ableton Live 12. Make beats, mix tracks, design sounds, and arrange songs with 317 AI-powered tools.",
|
|
7
|
+
"long_description": "LivePilot is an agentic production system for Ableton Live 12. 317 tools across 43 domains — device atlas (1305 devices), sample intelligence (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, and 12 creative engines.\n\n**What it does:**\n- Creates MIDI clips with notes, chords, and rhythms\n- Loads instruments and effects via Device Atlas (1305 devices indexed)\n- Searches samples across Splice, Ableton browser, and filesystem\n- Plans compositions from text prompts with genre-aware layering\n- Slices samples with intent-based MIDI generation\n- Mixes with volume, panning, sends, and automation\n- Analyzes your mix with real-time spectral data (M4L bridge)\n- Diagnoses stuck sessions and generates creative rescue variants\n- Remembers your production style across sessions\n\n**How it works:**\nLivePilot installs a Remote Script in Ableton that communicates with the AI over a local TCP connection. Everything runs on your machine — no audio leaves your computer.",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Pilot Studio",
|
|
10
10
|
"url": "https://github.com/dreamrec/LivePilot"
|
|
11
11
|
},
|
|
12
|
-
"license": "
|
|
12
|
+
"license": "BSL-1.1",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
15
|
"url": "https://github.com/dreamrec/LivePilot"
|
package/mcp_server/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.10.1"
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Device Atlas v2 — indexed in-memory device knowledge base.
|
|
2
|
+
|
|
3
|
+
Loads a JSON atlas file and builds indexes for fast lookup, search,
|
|
4
|
+
suggestion, chain building, and device comparison.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AtlasManager:
|
|
15
|
+
"""In-memory device atlas with indexed lookups."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, atlas_path: str):
|
|
18
|
+
with open(atlas_path, "r") as f:
|
|
19
|
+
data = json.load(f)
|
|
20
|
+
|
|
21
|
+
self._meta = data.get("meta", {})
|
|
22
|
+
self._devices: List[Dict[str, Any]] = data.get("devices", [])
|
|
23
|
+
|
|
24
|
+
# ── Build indexes ───────────────────────────────────────────
|
|
25
|
+
self._by_id: Dict[str, Dict[str, Any]] = {}
|
|
26
|
+
self._by_name: Dict[str, Dict[str, Any]] = {} # lowercase key
|
|
27
|
+
self._by_uri: Dict[str, Dict[str, Any]] = {}
|
|
28
|
+
self._by_category: Dict[str, List[Dict[str, Any]]] = {}
|
|
29
|
+
self._by_tag: Dict[str, List[Dict[str, Any]]] = {}
|
|
30
|
+
self._by_genre: Dict[str, List[Dict[str, Any]]] = {}
|
|
31
|
+
|
|
32
|
+
for dev in self._devices:
|
|
33
|
+
dev_id = dev.get("id", "")
|
|
34
|
+
dev_name = dev.get("name", "")
|
|
35
|
+
dev_uri = dev.get("uri", "")
|
|
36
|
+
dev_category = dev.get("category", "")
|
|
37
|
+
|
|
38
|
+
if dev_id:
|
|
39
|
+
self._by_id[dev_id] = dev
|
|
40
|
+
if dev_name:
|
|
41
|
+
self._by_name[dev_name.lower()] = dev
|
|
42
|
+
if dev_uri:
|
|
43
|
+
self._by_uri[dev_uri] = dev
|
|
44
|
+
|
|
45
|
+
# Category index
|
|
46
|
+
if dev_category:
|
|
47
|
+
self._by_category.setdefault(dev_category, []).append(dev)
|
|
48
|
+
|
|
49
|
+
# Tag index
|
|
50
|
+
for tag in dev.get("tags", []):
|
|
51
|
+
self._by_tag.setdefault(tag.lower(), []).append(dev)
|
|
52
|
+
|
|
53
|
+
# Genre index (primary + secondary)
|
|
54
|
+
for genre in dev.get("genres", {}).get("primary", []):
|
|
55
|
+
self._by_genre.setdefault(genre.lower(), []).append(dev)
|
|
56
|
+
for genre in dev.get("genres", {}).get("secondary", []):
|
|
57
|
+
self._by_genre.setdefault(genre.lower(), []).append(dev)
|
|
58
|
+
|
|
59
|
+
# ── Properties ──────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def version(self) -> str:
|
|
63
|
+
return self._meta.get("version", "unknown")
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def device_count(self) -> int:
|
|
67
|
+
return len(self._devices)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def stats(self) -> Dict[str, Any]:
|
|
71
|
+
categories: Dict[str, int] = {}
|
|
72
|
+
for dev in self._devices:
|
|
73
|
+
cat = dev.get("category", "unknown")
|
|
74
|
+
categories[cat] = categories.get(cat, 0) + 1
|
|
75
|
+
return {
|
|
76
|
+
"version": self.version,
|
|
77
|
+
"device_count": self.device_count,
|
|
78
|
+
"categories": categories,
|
|
79
|
+
"index_sizes": {
|
|
80
|
+
"by_id": len(self._by_id),
|
|
81
|
+
"by_name": len(self._by_name),
|
|
82
|
+
"by_uri": len(self._by_uri),
|
|
83
|
+
"by_category": len(self._by_category),
|
|
84
|
+
"by_tag": len(self._by_tag),
|
|
85
|
+
"by_genre": len(self._by_genre),
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# ── Lookup ──────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
def lookup(self, name_or_id: str) -> Optional[Dict[str, Any]]:
|
|
92
|
+
"""Exact match by ID, name (case-insensitive), or URI. Returns None on miss."""
|
|
93
|
+
# Try ID first
|
|
94
|
+
if name_or_id in self._by_id:
|
|
95
|
+
return self._by_id[name_or_id]
|
|
96
|
+
# Try name (case-insensitive)
|
|
97
|
+
lower = name_or_id.lower()
|
|
98
|
+
if lower in self._by_name:
|
|
99
|
+
return self._by_name[lower]
|
|
100
|
+
# Try URI
|
|
101
|
+
if name_or_id in self._by_uri:
|
|
102
|
+
return self._by_uri[name_or_id]
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
# ── Search ──────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
def search(
|
|
108
|
+
self, query: str, category: str = "all", limit: int = 10
|
|
109
|
+
) -> List[Dict[str, Any]]:
|
|
110
|
+
"""Multi-signal search scoring across name, tags, use_cases, genre, description."""
|
|
111
|
+
if not query:
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
query_lower = query.lower()
|
|
115
|
+
query_words = query_lower.split()
|
|
116
|
+
results: List[Dict[str, Any]] = []
|
|
117
|
+
|
|
118
|
+
for dev in self._devices:
|
|
119
|
+
# Category filter
|
|
120
|
+
if category != "all" and dev.get("category", "") != category:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
score = 0
|
|
124
|
+
dev_name = dev.get("name", "")
|
|
125
|
+
dev_name_lower = dev_name.lower()
|
|
126
|
+
|
|
127
|
+
# Name scoring: 100pts exact, 50pts substring
|
|
128
|
+
if dev_name_lower == query_lower:
|
|
129
|
+
score += 100
|
|
130
|
+
elif query_lower in dev_name_lower:
|
|
131
|
+
score += 50
|
|
132
|
+
|
|
133
|
+
# Tag scoring: 30pts per matching tag
|
|
134
|
+
dev_tags = [t.lower() for t in dev.get("tags", [])]
|
|
135
|
+
for word in query_words:
|
|
136
|
+
if word in dev_tags:
|
|
137
|
+
score += 30
|
|
138
|
+
|
|
139
|
+
# Use case scoring: 25pts per match
|
|
140
|
+
for use_case in dev.get("use_cases", []):
|
|
141
|
+
use_lower = use_case.lower()
|
|
142
|
+
for word in query_words:
|
|
143
|
+
if word in use_lower:
|
|
144
|
+
score += 25
|
|
145
|
+
break # one match per use_case
|
|
146
|
+
|
|
147
|
+
# Genre scoring: 20pts primary, 10pts secondary
|
|
148
|
+
genres = dev.get("genres", {})
|
|
149
|
+
for genre in genres.get("primary", []):
|
|
150
|
+
if query_lower in genre.lower() or genre.lower() in query_lower:
|
|
151
|
+
score += 20
|
|
152
|
+
for genre in genres.get("secondary", []):
|
|
153
|
+
if query_lower in genre.lower() or genre.lower() in query_lower:
|
|
154
|
+
score += 10
|
|
155
|
+
|
|
156
|
+
# Description keyword scoring: 15pts
|
|
157
|
+
description = dev.get("description", "").lower()
|
|
158
|
+
for word in query_words:
|
|
159
|
+
if len(word) >= 3 and word in description:
|
|
160
|
+
score += 15
|
|
161
|
+
|
|
162
|
+
if score > 0:
|
|
163
|
+
results.append({"device": dev, "score": score})
|
|
164
|
+
|
|
165
|
+
# Sort by score descending, then by name for stability
|
|
166
|
+
results.sort(key=lambda r: (-r["score"], r["device"].get("name", "")))
|
|
167
|
+
return results[:limit]
|
|
168
|
+
|
|
169
|
+
# ── Suggest ─────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
def suggest(
|
|
172
|
+
self,
|
|
173
|
+
intent: str,
|
|
174
|
+
genre: str = "",
|
|
175
|
+
energy: str = "medium",
|
|
176
|
+
limit: int = 5,
|
|
177
|
+
) -> List[Dict[str, Any]]:
|
|
178
|
+
"""Suggest devices for an intent, returning ranked list with rationale and recipe."""
|
|
179
|
+
# Use search to find candidates
|
|
180
|
+
search_query = intent
|
|
181
|
+
if genre:
|
|
182
|
+
search_query = f"{intent} {genre}"
|
|
183
|
+
candidates = self.search(search_query, limit=limit * 2)
|
|
184
|
+
|
|
185
|
+
results = []
|
|
186
|
+
for candidate in candidates[:limit]:
|
|
187
|
+
dev = candidate["device"]
|
|
188
|
+
dev_name = dev.get("name", "")
|
|
189
|
+
dev_category = dev.get("category", "")
|
|
190
|
+
dev_tags = dev.get("tags", [])
|
|
191
|
+
dev_sweet_spot = dev.get("sweet_spot", "")
|
|
192
|
+
|
|
193
|
+
# Build rationale
|
|
194
|
+
rationale_parts = []
|
|
195
|
+
if dev_category:
|
|
196
|
+
rationale_parts.append(f"{dev_name} is a {dev_category}")
|
|
197
|
+
if dev_tags:
|
|
198
|
+
rationale_parts.append(f"suited for {', '.join(dev_tags[:3])}")
|
|
199
|
+
if genre:
|
|
200
|
+
primary_genres = dev.get("genres", {}).get("primary", [])
|
|
201
|
+
if any(genre.lower() in g.lower() for g in primary_genres):
|
|
202
|
+
rationale_parts.append(f"commonly used in {genre}")
|
|
203
|
+
rationale = " — ".join(rationale_parts) if rationale_parts else f"{dev_name} matches your intent"
|
|
204
|
+
|
|
205
|
+
# Build recipe
|
|
206
|
+
recipe = {}
|
|
207
|
+
if dev_sweet_spot:
|
|
208
|
+
recipe["sweet_spot"] = dev_sweet_spot
|
|
209
|
+
recipe["energy"] = energy
|
|
210
|
+
key_params = dev.get("key_parameters", [])
|
|
211
|
+
if key_params:
|
|
212
|
+
recipe["start_with"] = key_params[:3]
|
|
213
|
+
|
|
214
|
+
results.append({
|
|
215
|
+
"device": dev,
|
|
216
|
+
"rationale": rationale,
|
|
217
|
+
"recipe": recipe,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
return results
|
|
221
|
+
|
|
222
|
+
# ── Chain Suggest ───────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
def chain_suggest(
|
|
225
|
+
self, role: str, genre: str = ""
|
|
226
|
+
) -> Dict[str, Any]:
|
|
227
|
+
"""Suggest a device chain for a given role (e.g., 'bass', 'lead', 'pad')."""
|
|
228
|
+
chain: List[Dict[str, Any]] = []
|
|
229
|
+
position = 0
|
|
230
|
+
|
|
231
|
+
# Determine chain structure based on role
|
|
232
|
+
role_lower = role.lower()
|
|
233
|
+
|
|
234
|
+
# Stage 1: Instrument (if the role implies one)
|
|
235
|
+
instrument_intents = {
|
|
236
|
+
"bass": "bass synthesizer",
|
|
237
|
+
"lead": "lead synthesizer",
|
|
238
|
+
"pad": "pad synthesizer",
|
|
239
|
+
"keys": "keyboard instrument",
|
|
240
|
+
"drums": "drum machine",
|
|
241
|
+
"vocal": "vocal",
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
intent = instrument_intents.get(role_lower, role_lower)
|
|
245
|
+
search_q = f"{intent} {genre}" if genre else intent
|
|
246
|
+
|
|
247
|
+
# Find instrument
|
|
248
|
+
instrument_candidates = self.search(search_q, category="instrument", limit=3)
|
|
249
|
+
if instrument_candidates:
|
|
250
|
+
best = instrument_candidates[0]["device"]
|
|
251
|
+
chain.append({
|
|
252
|
+
"position": position,
|
|
253
|
+
"device": best,
|
|
254
|
+
"reason": f"Primary {role} instrument",
|
|
255
|
+
})
|
|
256
|
+
position += 1
|
|
257
|
+
|
|
258
|
+
# Stage 2: Effects
|
|
259
|
+
effect_stages = [
|
|
260
|
+
("eq", f"Shape the {role} tone"),
|
|
261
|
+
("compression", f"Control {role} dynamics"),
|
|
262
|
+
("reverb", f"Add space to {role}"),
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
for effect_type, reason in effect_stages:
|
|
266
|
+
effect_q = f"{effect_type} {genre}" if genre else effect_type
|
|
267
|
+
effect_candidates = self.search(effect_q, category="effect", limit=2)
|
|
268
|
+
if effect_candidates:
|
|
269
|
+
best = effect_candidates[0]["device"]
|
|
270
|
+
chain.append({
|
|
271
|
+
"position": position,
|
|
272
|
+
"device": best,
|
|
273
|
+
"reason": reason,
|
|
274
|
+
})
|
|
275
|
+
position += 1
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
"role": role,
|
|
279
|
+
"genre": genre,
|
|
280
|
+
"chain": chain,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# ── Compare ─────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
def compare(
|
|
286
|
+
self, device_a: str, device_b: str, role: str = ""
|
|
287
|
+
) -> Dict[str, Any]:
|
|
288
|
+
"""Compare two devices side-by-side with a recommendation."""
|
|
289
|
+
dev_a = self.lookup(device_a)
|
|
290
|
+
dev_b = self.lookup(device_b)
|
|
291
|
+
|
|
292
|
+
if not dev_a:
|
|
293
|
+
return {"error": f"Device not found: {device_a}"}
|
|
294
|
+
if not dev_b:
|
|
295
|
+
return {"error": f"Device not found: {device_b}"}
|
|
296
|
+
|
|
297
|
+
def _summarize(dev: Dict[str, Any]) -> Dict[str, Any]:
|
|
298
|
+
return {
|
|
299
|
+
"name": dev.get("name", ""),
|
|
300
|
+
"category": dev.get("category", ""),
|
|
301
|
+
"tags": dev.get("tags", []),
|
|
302
|
+
"genres": dev.get("genres", {}),
|
|
303
|
+
"use_cases": dev.get("use_cases", []),
|
|
304
|
+
"description": dev.get("description", ""),
|
|
305
|
+
"cpu_weight": dev.get("cpu_weight", "unknown"),
|
|
306
|
+
"sweet_spot": dev.get("sweet_spot", ""),
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
summary_a = _summarize(dev_a)
|
|
310
|
+
summary_b = _summarize(dev_b)
|
|
311
|
+
|
|
312
|
+
# Recommendation logic: score each for the role
|
|
313
|
+
score_a = 0
|
|
314
|
+
score_b = 0
|
|
315
|
+
if role:
|
|
316
|
+
role_lower = role.lower()
|
|
317
|
+
# Check use_cases
|
|
318
|
+
for uc in dev_a.get("use_cases", []):
|
|
319
|
+
if role_lower in uc.lower():
|
|
320
|
+
score_a += 20
|
|
321
|
+
for uc in dev_b.get("use_cases", []):
|
|
322
|
+
if role_lower in uc.lower():
|
|
323
|
+
score_b += 20
|
|
324
|
+
# Check tags
|
|
325
|
+
for tag in dev_a.get("tags", []):
|
|
326
|
+
if role_lower in tag.lower():
|
|
327
|
+
score_a += 10
|
|
328
|
+
for tag in dev_b.get("tags", []):
|
|
329
|
+
if role_lower in tag.lower():
|
|
330
|
+
score_b += 10
|
|
331
|
+
|
|
332
|
+
if score_a > score_b:
|
|
333
|
+
recommendation = f"{summary_a['name']} is better suited for {role}" if role else f"{summary_a['name']} scores higher"
|
|
334
|
+
elif score_b > score_a:
|
|
335
|
+
recommendation = f"{summary_b['name']} is better suited for {role}" if role else f"{summary_b['name']} scores higher"
|
|
336
|
+
else:
|
|
337
|
+
recommendation = "Both devices are equally suited" + (f" for {role}" if role else "")
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
"device_a": summary_a,
|
|
341
|
+
"device_b": summary_b,
|
|
342
|
+
"recommendation": recommendation,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ── Module-level lazy loader ───────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
_atlas_instance: Optional[AtlasManager] = None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _load_atlas() -> AtlasManager:
|
|
352
|
+
"""Lazy-load the atlas from device_atlas.json in the same directory."""
|
|
353
|
+
global _atlas_instance
|
|
354
|
+
if _atlas_instance is None:
|
|
355
|
+
atlas_path = os.path.join(os.path.dirname(__file__), "device_atlas.json")
|
|
356
|
+
_atlas_instance = AtlasManager(atlas_path)
|
|
357
|
+
return _atlas_instance
|