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
|
@@ -60,3 +60,18 @@ 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
|
+
Every Wonder variant should include:
|
|
73
|
+
- A **filter arc** — at least one element's filter evolving over the full section
|
|
74
|
+
- A **space arc** — reverb/delay sends breathing with arrangement density
|
|
75
|
+
- **Micro-modulation** — every sustained sound has sub-perceptual LFO on at least one parameter
|
|
76
|
+
- **2-3 macro gestures** — coordinated multi-parameter moves at section transitions
|
|
77
|
+
- A variant with no automation is not a real variant — automation IS the music
|
package/livepilot.mcpb
CHANGED
|
Binary file
|
package/manifest.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
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
|
|
5
|
+
"version": "1.10.0",
|
|
6
|
+
"description": "Agentic production system for Ableton Live 12. Make beats, mix tracks, design sounds, and arrange songs with 316 AI-powered tools.",
|
|
7
7
|
"long_description": "LivePilot is an AI production assistant that connects directly to Ableton Live 12. It can create drum patterns, program basslines, write chord progressions, design sounds, mix your tracks, analyze your audio, and arrange full songs — all through natural language.\n\n**What it does:**\n- Creates MIDI clips with notes, chords, and rhythms\n- Loads instruments and effects from Ableton's browser\n- Shapes sounds by adjusting device parameters\n- Mixes with volume, panning, sends, and automation\n- Analyzes your mix with real-time spectral data\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",
|
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.0"
|
|
@@ -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
|