livepilot 1.23.6 → 1.25.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/CHANGELOG.md +107 -0
- package/README.md +60 -14
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +17 -3
- package/mcp_server/atlas/explore_tools.py +332 -0
- package/mcp_server/atlas/tools.py +161 -0
- package/mcp_server/audit/__init__.py +6 -0
- package/mcp_server/audit/checks.py +618 -0
- package/mcp_server/audit/tools.py +232 -0
- package/mcp_server/composer/branch_producer.py +5 -2
- package/mcp_server/composer/develop/__init__.py +19 -0
- package/mcp_server/composer/develop/apply.py +217 -0
- package/mcp_server/composer/develop/brief_builder.py +269 -0
- package/mcp_server/composer/develop/seed_introspector.py +195 -0
- package/mcp_server/composer/engine.py +15 -521
- package/mcp_server/composer/fast/__init__.py +62 -0
- package/mcp_server/composer/fast/apply.py +533 -0
- package/mcp_server/composer/fast/brief_builder.py +1479 -0
- package/mcp_server/composer/fast/tier_classification.py +159 -0
- package/mcp_server/composer/framework/__init__.py +0 -0
- package/mcp_server/composer/framework/applier.py +179 -0
- package/mcp_server/composer/framework/artist_loader.py +63 -0
- package/mcp_server/composer/framework/atlas_resolver.py +554 -0
- package/mcp_server/composer/framework/brief.py +79 -0
- package/mcp_server/composer/framework/event_lexicon.py +71 -0
- package/mcp_server/composer/framework/genre_loader.py +77 -0
- package/mcp_server/composer/framework/intent_source.py +137 -0
- package/mcp_server/composer/framework/knowledge_pack.py +140 -0
- package/mcp_server/composer/framework/plan_compiler.py +10 -0
- package/mcp_server/composer/full/__init__.py +10 -0
- package/mcp_server/composer/full/apply.py +1139 -0
- package/mcp_server/composer/full/brief_builder.py +227 -0
- package/mcp_server/composer/full/engine.py +541 -0
- package/mcp_server/composer/full/layer_planner.py +491 -0
- package/mcp_server/composer/layer_planner.py +19 -465
- package/mcp_server/composer/sample_resolver.py +80 -7
- package/mcp_server/composer/tools.py +626 -28
- package/mcp_server/server.py +1 -0
- package/mcp_server/splice_client/client.py +7 -0
- package/mcp_server/tools/_analyzer_engine/sample.py +172 -7
- package/mcp_server/tools/_planner_engine.py +25 -63
- package/mcp_server/tools/analyzer.py +10 -4
- package/mcp_server/tools/browser.py +102 -19
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/server.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,112 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.25.0 — 2026-05-02
|
|
4
|
+
|
|
5
|
+
Hybrid Knowledge Surface — closes the gap between "compose runs successfully" and "compose makes thoughtful production decisions" by giving the agent three layers of atlas-corpus access during plan design instead of one-shot pre-resolution.
|
|
6
|
+
|
|
7
|
+
### Added — three new MCP tools
|
|
8
|
+
|
|
9
|
+
- **`atlas_explore(role, mood, genre, artists?, n=5, avoid_uris?, cohort_constraint?)`** — refined per-role candidate query callable mid-design. Wraps `AtlasResolver.resolve_for_role` with corpus-deep ranking signals: tag/genre match base, signature_techniques mood overlap (+0.20), curated `.adg` boost (+0.10), recent positive preference (+0.10), §1 banned-default penalty for melodic roles (−0.50), opaque-M4L pad penalty (−0.30), §7 #2 anti-repeat (−0.15), caller avoid-list (−0.30). Returns 3-5 ranked candidates with reasoning trails and a cohort_hint inferred from result frequency.
|
|
10
|
+
- **`atlas_audition(uri)`** — full sidecar dump for a single URI: character_tags, signature_techniques (joined from `device_techniques_index.json`), producer-curated macro names (joined via `preset_resolver`), curated `.adg` paths, related demos placeholder. Use BEFORE committing to a candidate when its tags alone aren't enough.
|
|
11
|
+
- **`atlas_substitute(current_uri, anti_tag, n=3)`** — anti-tag-driven swap for after analyze_sound_design or analyze_mix flags an issue. Substring-matches anti_tag against the 11-key map (bright/harsh/aggressive/sparse/thin/muddy/clean/dark/warm/static/generic) to derive (excluded_tags, preferred_tags), filters role-mate candidates that don't carry excluded character_tags, ranks the survivors with preferred_tags as mood boost.
|
|
12
|
+
|
|
13
|
+
### Added — framework
|
|
14
|
+
|
|
15
|
+
- **`mcp_server/composer/framework/atlas_resolver.py`** — `AtlasResolver` class with `resolve_anchors()` (Layer A: cohort + role-anchored URIs at brief-build time, wraps `atlas_pack_aware_compose` with coarse→fine role mapping) and `resolve_for_role()` (Layer B: per-role ranked candidates with cohort_constraint + excluded_uris). `AtlasCandidate` and `AtlasAnchors` dataclasses define the shared shape. Memory-rule constants `BANNED_DEFAULT_MELODIC` and `OPAQUE_M4L_FOR_PAD` are exported.
|
|
16
|
+
- **`mcp_server/atlas/explore_tools.py`** — pure-Python implementations for the three new MCP tools, including `_ANTI_TAG_MAP` (11-key inversion table) and `_load_device_techniques_index()` lazy loader.
|
|
17
|
+
|
|
18
|
+
### Changed — KnowledgePack + brief
|
|
19
|
+
|
|
20
|
+
- `KnowledgePack.build()` accepts `atlas`, `ableton`, `ctx`, `brief_text` kwargs. When `mode="full"` AND `atlas` AND `brief_text` are provided, populates `atlas_anchors` (best-effort — silently `None` on any failure path).
|
|
21
|
+
- `build_full_brief` now threads the atlas object through. The brief carries `atlas_anchors` alongside the existing fields.
|
|
22
|
+
- `_DESIGN_TARGETS` text in `full/brief_builder.py` updated to teach the LLM about the three new tools and when to call each.
|
|
23
|
+
|
|
24
|
+
### Tests
|
|
25
|
+
|
|
26
|
+
- `tests/composer/framework/test_atlas_resolver.py` — 17 cases covering ranking math (8), `resolve_for_role` (6), candidate shape (1), `resolve_anchors` integration with mocked `pack_aware_compose` (2).
|
|
27
|
+
- `tests/atlas/test_explore_tools.py` — 20 cases covering `atlas_explore` (6), `atlas_audition` (5), `_resolve_anti_tags` (3), `atlas_substitute` (6).
|
|
28
|
+
- `tests/test_tools_contract.py` — count assertion updated 459 → 462; atlas tool registry expects the three new names.
|
|
29
|
+
|
|
30
|
+
### Tool count
|
|
31
|
+
|
|
32
|
+
- Net delta: **459 → 462** (+3: atlas_explore, atlas_audition, atlas_substitute).
|
|
33
|
+
|
|
34
|
+
### Changed — `resolve_for_role` four-source union
|
|
35
|
+
|
|
36
|
+
- **`AtlasResolver.resolve_for_role()`** previously queried only the bundled atlas tag index (`self._atlas._by_tag`). User-curated rack instruments and pack overlays were structurally invisible to the agent's design-time queries.
|
|
37
|
+
- Two-pass overlay union added via new helper `_gather_from_overlays()`:
|
|
38
|
+
- **Pass A** — explicit `entity_type="demo_project"` query against the `packs` namespace. Demo-project entries (analyzed `.als` parses with `track_names` + `parent_pack` + `device_class_counts`) are the highest-confidence per-role anchors. Each survivor receives a **+0.15 score boost** with reasoning trail entry `"demo_project ground-truth (+0.15)"`.
|
|
39
|
+
- **Pass B** — full overlay search with no namespace filter. Captures `packs/pack`, `packs/cross_pack_workflow`, `m4l-devices/*`, `user.*`, `elektron/*` entries that share role tags.
|
|
40
|
+
- New helper `_overlay_entry_to_device()` synthesizes `overlay://<namespace>/<entity_id>` URIs so overlay candidates render in the same shape as factory atlas candidates. Agents resolve to a loadable URI via `extension_atlas_get(namespace, entity_id)` afterward.
|
|
41
|
+
- Best-effort: import or runtime failure of the overlay backend silently returns an empty list rather than raising — matches the existing `resolve_anchors` failure semantics.
|
|
42
|
+
|
|
43
|
+
### Changed — `_DESIGN_TARGETS` four-source search mandate
|
|
44
|
+
|
|
45
|
+
- Brief text in `mcp_server/composer/full/brief_builder.py::_DESIGN_TARGETS` rewritten to explicitly require the agent to UNION four sources before committing any role pick:
|
|
46
|
+
1. **Source 1 — Factory atlas** (already in `atlas_anchors`): `atlas_explore` / `atlas_audition` / `atlas_substitute`.
|
|
47
|
+
2. **Source 2 — User corpus** (mandatory): `extension_atlas_search(query=role)` and `extension_atlas_search(query=role, entity_type="demo_project")` for ground-truth role→URI mappings, plus `extension_atlas_get(namespace, entity_id)` for full-body inspection.
|
|
48
|
+
3. **Source 3 — Anthropic Ableton Knowledge MCP**: `mcp__Ableton_Knowledge__search_transcripts` / `search_live_manual` / `search_knowledge_base` / `search_videos` for tutorial-grade context.
|
|
49
|
+
4. **Five-step protocol** documented per role: read anchor → atlas_explore → extension_atlas_search (corpus + demo_project) → Ableton_Knowledge search → union, score, commit.
|
|
50
|
+
- Production motivation: factory-atlas-only selection consistently missed canonical user-curated rack instruments (e.g., the `808 Trap Selector Rack.adg` from the Trap Drums by Sound Oracle pack lives in the packs overlay, not the factory tag index).
|
|
51
|
+
|
|
52
|
+
### Fixed — `load_browser_item(role="drum")` post-load defaults
|
|
53
|
+
|
|
54
|
+
Two compounding bugs in `mcp_server/tools/browser.py` made `role="drum"` a no-op since v1.20:
|
|
55
|
+
|
|
56
|
+
- **Wrong parameter name** — `_SIMPLER_ROLE_DEFAULTS["drum"]` set `"Sample Pitch Coarse"` to 36, but that parameter does NOT exist on `OriginalSimpler`. The `set_device_parameter` call raised `Parameter 'Sample Pitch Coarse' not found`, was swallowed by the per-param try/except, and silently lost. Replaced with `("Transpose", 24)` (range −48..+48 semitones); +24 compensates for Simpler's default sample root C3 vs the drum-pad MIDI convention C1. Melodic and texture roles updated to `("Transpose", 0)` (no shift — C3 default matches their input range).
|
|
57
|
+
- **Device-detection logic never triggered** — wrapper checked `result.get("device_index")` and `result.get("class_name")` from the load response, but the underlying TCP `load_browser_item` command returns only `{loaded, name, device_count}`. Both fields were always `None`/`""`, the `Simpler in device_class` check failed, and the entire role-defaults branch was skipped on every call. Fixed: post-load probe via `get_device_info(track_index, device_index=0)` to read class + name, treating chain-head as the canonical instrument slot Live places fresh instruments at.
|
|
58
|
+
- Same broken parameter name was also patched in `mcp_server/tools/_analyzer_engine/sample.py::_simpler_post_load_hygiene`. Auto-detect-drum-root-note now translates `drum_root → Transpose = 60 − drum_root`, clamped to ±48 semitones.
|
|
59
|
+
- Response now carries `role`, `role_defaults_applied: [{parameter, value|skipped}…]`, and `device_class` so callers can verify the four defaults landed.
|
|
60
|
+
|
|
61
|
+
### Added — M4L instrument post-load hygiene
|
|
62
|
+
|
|
63
|
+
- New `_M4L_INSTRUMENT_HYGIENE` dict in `mcp_server/tools/browser.py` maps a device-name substring to a list of `(parameter_name, value)` tames. Runs **unconditionally** (not gated on `role`) post-load, after Simpler role defaults.
|
|
64
|
+
- Initial entry: `Harmonic Drone Generator` (Drone Lab pack) — sets `Latch=0` (off), `Volume=-40` (≈ −20 dB display, vs default ≈ −6 dB), `Density=40` (40 %, vs default 80 %). Without these tames every fresh HDG load slammed an 8-voice drone at full volume the moment any MIDI note hit it. The `Latch` issue compounded by keeping that note ringing forever even after the MIDI source released.
|
|
65
|
+
- Response now carries `m4l_hygiene: {device_name, applied: [{parameter, value|skipped}…]}` when a hygiene entry matches. One match per load (`break` after first hit).
|
|
66
|
+
|
|
67
|
+
### Tests
|
|
68
|
+
|
|
69
|
+
- `tests/test_next_steps_2026_04_22.py::test_role_defaults_reasonable_for_drum_role` — assertion updated to `Transpose == 24`; regression guard `"Sample Pitch Coarse" not in drum` prevents the broken param name from ever reappearing.
|
|
70
|
+
- `tests/test_next_steps_2026_04_22.py::test_role_defaults_reasonable_for_melodic_role` — assertion updated to `Transpose == 0` plus the same regression guard.
|
|
71
|
+
- All four role-defaults tests pass against the new `_SIMPLER_ROLE_DEFAULTS` shape.
|
|
72
|
+
|
|
73
|
+
## v1.24.0 — 2026-05-02
|
|
74
|
+
|
|
75
|
+
Compose framework rebuild — fast / full / develop modes share an Applier substrate; full mode is a clean-room rewrite around an LLM-creative two-phase brief flow (LLM provides FORM, framework provides VOCABULARY).
|
|
76
|
+
|
|
77
|
+
### Added — compose framework
|
|
78
|
+
|
|
79
|
+
- **`mcp_server/composer/framework/applier.py`** — shared pre-flight + post-flight skeleton. `preflight()` loads analyzer, reconnects bridge, retries handshake up to 3× with 200ms gaps (fixes the M4L UDP-bind race that previously left "bridge not connected" failures on the first instrument-load call). `postflight()` sets monitoring=Auto on every newly-created track and calls `back_to_arranger`. Functions are dependency-injected so each mode's apply.py wires its own analyzer/bridge funcs.
|
|
80
|
+
- **`mcp_server/composer/framework/knowledge_pack.py`** — `event_lexicon` (42 structural events across 7 categories: drum_density, harmonic, texture, vocal, rhythm_feel, tension, fx_gesture), `genre_context` loader (parses `livepilot/skills/livepilot-core/references/genre-vocabularies.md`, 15 genres), `artist_context` loader (parses `artist-vocabularies.md`, ~25 producers). `atlas_candidates_per_role` is scaffolded but **left as empty stub** — see v1.25 plan.
|
|
81
|
+
- **`mcp_server/composer/full/apply.py::apply_full_plan_v2`** — full-mode rebuild. Takes an LLM-authored plan with `form` (sections), `tracks` (instruments + variants + arrangement_clips), `events`. Per-section variants prevent the BUG-FULL-MODE-18 flat tile. Native arrangement clip flow via `create_native_arrangement_clip` + `add_arrangement_notes` + `set_clip_loop` produces a single arrangement clip per section instead of 32 tiny tiles (BUG-FULL-MODE-23).
|
|
82
|
+
- **`mcp_server/composer/develop/`** — develop mode (extend an existing 8-bar loop). `seed_introspector.py` classifies tracks by name + content; `brief_builder.py` pulls artist references from the user prompt + research hooks; `apply.py` writes per-track variants without disturbing the seed.
|
|
83
|
+
- **`mcp_server/composer/fast/brief_builder.py`** — fast-mode brief authoring with tier classification (Tier-A curated > Tier-B audible default > Tier-C never). Hunt order: `search_browser(path="sounds")` first to surface curated `.adg` chains, then atlas, then bare instruments only as last resort. Bans bare-melodic Tier-B when curated alternatives exist (§1 violation prevention).
|
|
84
|
+
|
|
85
|
+
### Fixed — live-test wave
|
|
86
|
+
|
|
87
|
+
- **BUG-FULL-MODE-14: bridge UDP race** — `apply_full_plan` returned success on `bridge.connect()` but the M4L JS listener takes 100-500ms to bind the UDP socket; next bridge call ("UDP bridge is not connected"). Fixed in `Applier.preflight` with handshake retry loop.
|
|
88
|
+
- **BUG-FULL-MODE-17: monitoring=In on all tracks** — Phase 4 Task 4 set monitoring `state=0` ("In", always passes input) instead of `state=1` ("Auto", default). Live screenshot showed every track armed-red. Fixed: state=1 in `Applier.postflight`.
|
|
89
|
+
- **BUG-FULL-MODE-18: flat tiling instead of per-section variants** — full mode reused one MIDI variant for the whole arrangement. Fixed: per-section `variant_id` in plan + per-section variant resolution in apply.
|
|
90
|
+
- **BUG-FULL-MODE-19: `track_index` vs `index` field** — `apply_full_plan_v2` read `result["track_index"]` from `create_midi_track`, but the Remote Script returns `result["index"]`. Same bug class as the v1.23.x `parameter_name` vs `name_or_index` fix.
|
|
91
|
+
- **BUG-FULL-MODE-20: zombie/leftover tracks from previous sessions** — postflight only deleted default-named tracks (`1-MIDI`, `2-Audio`). Now also deletes tracks with no clips AND no instrument device (true-empty zombies) regardless of name.
|
|
92
|
+
- **BUG-FULL-MODE-21: drum-pitch super-low** — Simpler default root C3=60, drum clips firing at MIDI 36 = 24 semitones below native pitch. Drum-role repair (Volume=0, Snap=Off, Transpose=+24) was already in fast mode; now ported to full mode for parity. (`composer/fast/apply._apply_drum_role_repair` is imported by full mode.)
|
|
93
|
+
- **BUG-FULL-MODE-22: arrangement clip length wrong** — fixed alongside BUG-FULL-MODE-23.
|
|
94
|
+
- **BUG-FULL-MODE-23: 32-tile arrangement** — `create_arrangement_clip` duplicated the session clip every `loop_length` beats, producing a tile-grid arrangement. Switched to `create_native_arrangement_clip` + `add_arrangement_notes` + `set_clip_loop` for native arrangement clip authoring; one clip per section, looped to fill section length.
|
|
95
|
+
|
|
96
|
+
### Known gaps (deferred to v1.25)
|
|
97
|
+
|
|
98
|
+
- **`KnowledgePack.atlas_candidates_per_role` is empty stub** — agent still falls through to `search_browser` filename matching instead of consulting the indexed atlas. Ranking + truncation per role needs careful design (1-2 days). Documented as **BUG-FULL-MODE-24** and is the headline feature of v1.25.
|
|
99
|
+
- **Reasoning loop (Scope B)** — full mode currently does best-effort `analyze_mix`/`analyze_loudness`/`analyze_sound_design` calls but doesn't iterate on findings. v1.25 wires an analyze→adjust→re-analyze loop with a budget.
|
|
100
|
+
- **Drum craft + bass craft sophistication passes** — full mode produces correct but generic drum/bass programming. v1.25 lifts these to "poweruser" depth (per-bar variation, sidechain wiring, ghost notes, swing curves, sub/mid/click frequency separation).
|
|
101
|
+
- **`verify_track_audible` MCP tool** — Phase 4 Task 18c, deferred.
|
|
102
|
+
|
|
103
|
+
### Tool count
|
|
104
|
+
- Net delta: **453 → 459** (+6: compose framework expansion).
|
|
105
|
+
|
|
106
|
+
### Tests
|
|
107
|
+
- `tests/composer/` — 184 passing across fast / full / develop / framework subdirectories.
|
|
108
|
+
- `tests/test_tools_contract.py` — 459 tools verified.
|
|
109
|
+
|
|
3
110
|
## v1.23.6 — 2026-04-30
|
|
4
111
|
|
|
5
112
|
### Fixed
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
19
|
An agentic production system for Ableton Live 12.<br>
|
|
20
|
-
|
|
20
|
+
462 tools. 55 domains. Device atlas. Plan-aware Splice integration. Auto-composition. Spectral perception. Technique memory. Drum-rack pad builder. Live dead-device detection.
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
23
|
<br>
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
> Side effects that touch state outside the Live project — Splice downloads, memory/ledger writes,
|
|
30
30
|
> installer actions, atlas scans, filesystem writes — persist beyond undo.
|
|
31
31
|
|
|
32
|
+
> [!WARNING]
|
|
33
|
+
> LivePilot is actively in development. Tools, behavior, and APIs change frequently between versions.
|
|
34
|
+
> Pin to a specific version for stable work. Known gaps and in-progress features are documented in
|
|
35
|
+
> each release's CHANGELOG entry.
|
|
36
|
+
|
|
32
37
|
<br>
|
|
33
38
|
|
|
34
39
|
---
|
|
@@ -54,7 +59,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
|
|
|
54
59
|
|
|
55
60
|
## Two Ways to Talk to LivePilot
|
|
56
61
|
|
|
57
|
-
Pick whichever is faster for the idea in your head — both reach the same
|
|
62
|
+
Pick whichever is faster for the idea in your head — both reach the same 462-tool surface.
|
|
58
63
|
|
|
59
64
|
### Route A — Artist / aesthetic shorthand
|
|
60
65
|
|
|
@@ -107,8 +112,8 @@ Most sessions do both. Lead with shorthand to anchor the aesthetic, then refine
|
|
|
107
112
|
│ └─────────────────┼──────────────────┘ │
|
|
108
113
|
│ ▼ │
|
|
109
114
|
│ ┌─────────────────┐ │
|
|
110
|
-
│ │
|
|
111
|
-
│ │
|
|
115
|
+
│ │ 462 MCP Tools │ │
|
|
116
|
+
│ │ 55 domains │ │
|
|
112
117
|
│ └────────┬────────┘ │
|
|
113
118
|
│ │ │
|
|
114
119
|
│ Remote Script ──┤── TCP 9878 │
|
|
@@ -148,7 +153,7 @@ Most sessions do both. Lead with shorthand to anchor the aesthetic, then refine
|
|
|
148
153
|
|
|
149
154
|
## The Intelligence Layer
|
|
150
155
|
|
|
151
|
-
12 engines sit on top of the
|
|
156
|
+
12 engines sit on top of the 462 tools. They give the AI musical judgment, not just musical execution.
|
|
152
157
|
|
|
153
158
|
### SongBrain — What the Song Is
|
|
154
159
|
|
|
@@ -200,7 +205,7 @@ Every engine follows: **measure before → act → measure after → compare**.
|
|
|
200
205
|
|
|
201
206
|
## Tools
|
|
202
207
|
|
|
203
|
-
|
|
208
|
+
462 tools across 55 domains. Highlights below — [full catalog here](docs/manual/tool-catalog.md).
|
|
204
209
|
|
|
205
210
|
<br>
|
|
206
211
|
|
|
@@ -425,9 +430,9 @@ LivePilot reads Splice's local SQLite database to search your downloaded samples
|
|
|
425
430
|
|
|
426
431
|
<br>
|
|
427
432
|
|
|
428
|
-
### Composer —
|
|
433
|
+
### Composer — three modes (v1.25.0)
|
|
429
434
|
|
|
430
|
-
Prompt-to-plan auto-composition engine.
|
|
435
|
+
Prompt-to-plan auto-composition engine. Three modes share a common Applier substrate (preflight: bridge handshake retry + analyzer load; postflight: monitoring=Auto on new tracks + back_to_arranger). All modes return executable plans — the agent executes each step, it does not run autonomously.
|
|
431
436
|
|
|
432
437
|
```
|
|
433
438
|
"dark minimal techno 128bpm with industrial textures and ghostly vocals"
|
|
@@ -451,10 +456,51 @@ Prompt-to-plan auto-composition engine.
|
|
|
451
456
|
└─────────────────┘
|
|
452
457
|
```
|
|
453
458
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
459
|
+
#### fast mode — `compose_fast_apply`
|
|
460
|
+
|
|
461
|
+
Quick loop sketch. Single scene in session view. Intended for roughing out ideas quickly.
|
|
462
|
+
|
|
463
|
+
- Hunt order: curated `.adg` chains from the browser first, atlas devices second, bare instruments only as last resort
|
|
464
|
+
- Drum-role pitch repair included (Simpler default root vs. MIDI 36 offset)
|
|
465
|
+
- 4 genre defaults: house, techno, trap, ambient (unknown genres fall back to a neutral layer plan)
|
|
466
|
+
- Invoke with: *"Make me a [genre] loop at [tempo] BPM"*
|
|
467
|
+
|
|
468
|
+
#### full mode — `compose_full_apply`
|
|
469
|
+
|
|
470
|
+
Full track with song form: intro, verse, hook, breakdown, outro. Uses a two-phase LLM-creative brief flow — the LLM authors the form (sections, track list, per-section variants); the framework supplies the vocabulary (device hunt order, MIDI generation rules, arrangement conventions).
|
|
471
|
+
|
|
472
|
+
- Per-section MIDI variants prevent repeated tiles across the arrangement
|
|
473
|
+
- Native arrangement clips via `create_native_arrangement_clip` (one clip per section, looped to fill section length)
|
|
474
|
+
- Zombie-track cleanup in postflight (removes tracks with no clips and no instrument device)
|
|
475
|
+
- Drum-role pitch repair ported from fast mode
|
|
476
|
+
- **Known gap (v1.25):** `KnowledgePack.atlas_candidates_per_role` is an empty stub — the agent currently falls through to `search_browser` filename matching instead of consulting the indexed atlas. This is BUG-FULL-MODE-24 and is the headline feature of v1.25.
|
|
477
|
+
- Invoke with: *"Write a full [genre] track at [tempo] BPM"* or *"Build a full arrangement"*
|
|
478
|
+
|
|
479
|
+
#### develop mode — `develop_apply`
|
|
480
|
+
|
|
481
|
+
Extends an existing 8-bar loop without disturbing the seed material.
|
|
482
|
+
|
|
483
|
+
- Introspects the existing session (classifies tracks by name and content)
|
|
484
|
+
- Pulls artist and stylistic references from the user prompt
|
|
485
|
+
- Writes per-track variants and new supporting layers alongside the seed
|
|
486
|
+
- Invoke with: *"Develop this loop"* or *"Extend what's here into a longer idea"*
|
|
487
|
+
|
|
488
|
+
#### KnowledgePack scaffolding (v1.25.0)
|
|
489
|
+
|
|
490
|
+
All three modes share a `KnowledgePack` that provides structured creative context at runtime:
|
|
491
|
+
|
|
492
|
+
- `event_lexicon` — 42 structural events across 7 categories (drum density, harmonic, texture, vocal, rhythm feel, tension, fx gesture)
|
|
493
|
+
- `genre_context` — parses the 15-genre `genre-vocabularies.md` at load time
|
|
494
|
+
- `artist_context` — parses the ~25-producer `artist-vocabularies.md` at load time
|
|
495
|
+
- `atlas_candidates_per_role` — **stubbed in v1.25.0**, will be populated in v1.25
|
|
496
|
+
|
|
497
|
+
#### Core composer tools
|
|
498
|
+
|
|
499
|
+
- `compose` — plan a multi-layer composition from text prompt (entry point, mode-agnostic)
|
|
500
|
+
- `compose_fast_apply` — execute fast mode directly
|
|
501
|
+
- `compose_full_apply` — execute full mode directly
|
|
502
|
+
- `develop_apply` — execute develop mode directly
|
|
503
|
+
- `augment_with_samples` — plan sample-based layers for an existing session
|
|
458
504
|
- `get_composition_plan` — dry-run preview (see the plan without credit checks)
|
|
459
505
|
|
|
460
506
|
<br>
|
|
@@ -494,7 +540,7 @@ The V2 intelligence layer. These tools analyze, diagnose, plan, evaluate, and le
|
|
|
494
540
|
| Creative Constraints | 5 | constraint activation, reference-inspired variants |
|
|
495
541
|
| Preview Studio | 5 | variant creation, preview rendering, comparison, commit |
|
|
496
542
|
|
|
497
|
-
> **[View all
|
|
543
|
+
> **[View all 462 tools →](docs/manual/tool-catalog.md)**
|
|
498
544
|
|
|
499
545
|
<br>
|
|
500
546
|
|
|
@@ -721,7 +767,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for architecture details, code guidelines
|
|
|
721
767
|
|
|
722
768
|
| Document | What's inside |
|
|
723
769
|
|----------|---------------|
|
|
724
|
-
| [Manual](docs/manual/index.md) | Complete reference: architecture, all
|
|
770
|
+
| [Manual](docs/manual/index.md) | Complete reference: architecture, all 462 tools, workflows |
|
|
725
771
|
| [Intelligence Layer](docs/manual/intelligence.md) | How the 12 engines connect — conductor, moves, preview, evaluation |
|
|
726
772
|
| [Device Atlas](docs/manual/device-atlas.md) | 5264 devices indexed — search, suggest, chain building |
|
|
727
773
|
| [Samples & Slicing](docs/manual/samples.md) | 3-source search, fitness critics, slice workflows |
|
|
Binary file
|
|
@@ -34,7 +34,7 @@ outlets = 2; // 0: to udpsend (responses), 1: to buffer~/status
|
|
|
34
34
|
// Single source of truth for the bridge version — bumped alongside the
|
|
35
35
|
// rest of the release manifest. Surfaced in the UI via messnamed("livepilot_version", ...)
|
|
36
36
|
// so the frozen .amxd visibly reports which build it was last exported from.
|
|
37
|
-
var VERSION = "1.
|
|
37
|
+
var VERSION = "1.25.0";
|
|
38
38
|
|
|
39
39
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
40
40
|
|
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.25.0"
|
|
@@ -57,9 +57,23 @@ class AtlasManager:
|
|
|
57
57
|
if dev_category:
|
|
58
58
|
self._by_category.setdefault(dev_category, []).append(dev)
|
|
59
59
|
|
|
60
|
-
# Tag index
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
# Tag index — pull from BOTH legacy "tags" AND enriched
|
|
61
|
+
# "character_tags". BUG-P (caught 2026-05-01 live demo): the
|
|
62
|
+
# bundled factory atlas uses character_tags exclusively, so
|
|
63
|
+
# _by_tag was empty across the board, breaking every tag-based
|
|
64
|
+
# role picker. Reading both fields makes the index actually
|
|
65
|
+
# populated for normal user atlases.
|
|
66
|
+
seen_tags = set()
|
|
67
|
+
for tag in dev.get("tags", []) or []:
|
|
68
|
+
tag_lower = str(tag).lower()
|
|
69
|
+
if tag_lower not in seen_tags:
|
|
70
|
+
seen_tags.add(tag_lower)
|
|
71
|
+
self._by_tag.setdefault(tag_lower, []).append(dev)
|
|
72
|
+
for tag in dev.get("character_tags", []) or []:
|
|
73
|
+
tag_lower = str(tag).lower()
|
|
74
|
+
if tag_lower not in seen_tags:
|
|
75
|
+
seen_tags.add(tag_lower)
|
|
76
|
+
self._by_tag.setdefault(tag_lower, []).append(dev)
|
|
63
77
|
|
|
64
78
|
# Genre index (primary + secondary)
|
|
65
79
|
for genre in dev.get("genres", {}).get("primary", []):
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""Three v1.25 atlas knowledge-surface tools — agent-callable mid-design.
|
|
2
|
+
|
|
3
|
+
atlas_explore — refined per-role candidate query (wraps AtlasResolver)
|
|
4
|
+
atlas_audition — full sidecar dump for one URI (signature_techniques +
|
|
5
|
+
producer-curated macro values + related demos + curated ADGs)
|
|
6
|
+
atlas_substitute — anti-tag-driven swap (used after analyze_sound_design or
|
|
7
|
+
analyze_mix flags an issue with a chosen layer)
|
|
8
|
+
|
|
9
|
+
Imported and registered via @mcp.tool() decorators in atlas/tools.py.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
from dataclasses import asdict
|
|
17
|
+
from functools import lru_cache
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Optional
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ── Anti-tag → character-tag inversion map ──────────────────────────
|
|
25
|
+
#
|
|
26
|
+
# Maps a free-text "anti" descriptor to (excluded_tags, preferred_tags).
|
|
27
|
+
# Used by atlas_substitute. Keys are lowercased; matched as substring of the
|
|
28
|
+
# caller's anti_tag string so "too bright" and "bright" both resolve.
|
|
29
|
+
|
|
30
|
+
_ANTI_TAG_MAP: dict[str, tuple[tuple[str, ...], tuple[str, ...]]] = {
|
|
31
|
+
"bright": (("bright", "high", "shimmer", "airy"), ("warm", "dark", "muted", "vintage")),
|
|
32
|
+
"harsh": (("harsh", "aggressive", "distorted"), ("smooth", "soft", "clean")),
|
|
33
|
+
"aggressive": (("harsh", "aggressive", "punchy"), ("soft", "warm", "subtle")),
|
|
34
|
+
"sparse": (("minimal", "sparse", "thin"), ("dense", "lush", "thick", "wide")),
|
|
35
|
+
"thin": (("thin", "minimal"), ("thick", "fat", "dense", "wide")),
|
|
36
|
+
"muddy": (("muddy", "low_mid"), ("clear", "open", "defined")),
|
|
37
|
+
"clean": (("clean", "pristine"), ("dirty", "saturated", "vintage", "lo-fi")),
|
|
38
|
+
"dark": (("dark", "muted"), ("bright", "shimmer", "airy")),
|
|
39
|
+
"warm": (("warm", "vintage"), ("clean", "modern", "digital")),
|
|
40
|
+
"static": (("static", "single_layer"), ("evolving", "modulated", "movement")),
|
|
41
|
+
"generic": (("generic", "default"), ("character", "unique", "signature")),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _resolve_anti_tags(anti_tag: str) -> tuple[list[str], list[str]]:
|
|
46
|
+
"""Return (excluded_tags, preferred_tags) for an anti-tag string.
|
|
47
|
+
|
|
48
|
+
Substring-matches the caller's anti_tag string against keys in the map;
|
|
49
|
+
aggregates all hits. "too bright and harsh" → matches "bright" + "harsh".
|
|
50
|
+
Empty input returns ([], []) — caller should treat that as no-op.
|
|
51
|
+
"""
|
|
52
|
+
excluded: list[str] = []
|
|
53
|
+
preferred: list[str] = []
|
|
54
|
+
s = (anti_tag or "").lower()
|
|
55
|
+
for key, (excl, pref) in _ANTI_TAG_MAP.items():
|
|
56
|
+
if key in s:
|
|
57
|
+
for t in excl:
|
|
58
|
+
if t not in excluded:
|
|
59
|
+
excluded.append(t)
|
|
60
|
+
for t in pref:
|
|
61
|
+
if t not in preferred:
|
|
62
|
+
preferred.append(t)
|
|
63
|
+
return excluded, preferred
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ── Tech-index lookup ───────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@lru_cache(maxsize=1)
|
|
70
|
+
def _load_device_techniques_index() -> dict[str, list[dict]]:
|
|
71
|
+
"""Lazy-load the device_techniques_index.json sidecar.
|
|
72
|
+
|
|
73
|
+
Returns the inner `devices` dict keyed by device id/slug, or {} on miss.
|
|
74
|
+
"""
|
|
75
|
+
path = Path(__file__).parent / "device_techniques_index.json"
|
|
76
|
+
if not path.exists():
|
|
77
|
+
return {}
|
|
78
|
+
try:
|
|
79
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
80
|
+
return data.get("devices") or {}
|
|
81
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
82
|
+
logger.debug("device_techniques_index load failed: %s", exc)
|
|
83
|
+
return {}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _lookup_techniques(device: dict) -> list[dict]:
|
|
87
|
+
"""Find signature_techniques entries for a device.
|
|
88
|
+
|
|
89
|
+
Tries device id first (matches the index key), then name slug.
|
|
90
|
+
Returns the list of {technique, description, aesthetic, kind} dicts;
|
|
91
|
+
empty list on miss.
|
|
92
|
+
"""
|
|
93
|
+
if not device:
|
|
94
|
+
return []
|
|
95
|
+
index = _load_device_techniques_index()
|
|
96
|
+
if not index:
|
|
97
|
+
return []
|
|
98
|
+
dev_id = (device.get("id") or "").lower()
|
|
99
|
+
if dev_id and dev_id in index:
|
|
100
|
+
return list(index[dev_id])
|
|
101
|
+
name_slug = (device.get("name") or "").lower().replace(" ", "_")
|
|
102
|
+
if name_slug and name_slug in index:
|
|
103
|
+
return list(index[name_slug])
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ── Tool implementations ────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def explore(
|
|
111
|
+
*,
|
|
112
|
+
atlas: Any,
|
|
113
|
+
role: str,
|
|
114
|
+
mood: str = "",
|
|
115
|
+
genre: str = "",
|
|
116
|
+
artists: Optional[list[str]] = None,
|
|
117
|
+
n: int = 5,
|
|
118
|
+
avoid_uris: Optional[list[str]] = None,
|
|
119
|
+
cohort_constraint: Optional[list[str]] = None,
|
|
120
|
+
) -> dict:
|
|
121
|
+
"""atlas_explore implementation — wraps AtlasResolver.resolve_for_role.
|
|
122
|
+
|
|
123
|
+
Returns a structured dict (not raw dataclass) so MCP serialization is clean.
|
|
124
|
+
Falls through gracefully when atlas is missing — empty `candidates` plus a
|
|
125
|
+
`reasoning` line that says why.
|
|
126
|
+
"""
|
|
127
|
+
if atlas is None:
|
|
128
|
+
return {
|
|
129
|
+
"candidates": [],
|
|
130
|
+
"cohort_hint": None,
|
|
131
|
+
"reasoning": "atlas not loaded — no candidates available",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
from ..composer.framework.atlas_resolver import AtlasResolver
|
|
135
|
+
|
|
136
|
+
resolver = AtlasResolver(atlas=atlas)
|
|
137
|
+
candidates = resolver.resolve_for_role(
|
|
138
|
+
role=role,
|
|
139
|
+
genre=genre,
|
|
140
|
+
mood=mood,
|
|
141
|
+
artist_refs=list(artists or []),
|
|
142
|
+
avoid=[],
|
|
143
|
+
cohort_constraint=list(cohort_constraint or []) or None,
|
|
144
|
+
excluded_uris=set(avoid_uris or []) or None,
|
|
145
|
+
n=n,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
cohort_hint: Optional[str] = None
|
|
149
|
+
if cohort_constraint:
|
|
150
|
+
cohort_hint = cohort_constraint[0]
|
|
151
|
+
elif candidates:
|
|
152
|
+
# Most-frequent pack across top candidates
|
|
153
|
+
packs = [c.in_pack for c in candidates if c.in_pack]
|
|
154
|
+
if packs:
|
|
155
|
+
cohort_hint = max(set(packs), key=packs.count)
|
|
156
|
+
|
|
157
|
+
reasoning_bits: list[str] = []
|
|
158
|
+
if cohort_constraint:
|
|
159
|
+
reasoning_bits.append(f"cohort-constrained to: {', '.join(cohort_constraint)}")
|
|
160
|
+
if mood:
|
|
161
|
+
reasoning_bits.append(f"mood: {mood}")
|
|
162
|
+
if genre:
|
|
163
|
+
reasoning_bits.append(f"genre: {genre}")
|
|
164
|
+
if not candidates:
|
|
165
|
+
reasoning_bits.append(f"no candidates matched role '{role}'")
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
"candidates": [asdict(c) for c in candidates],
|
|
169
|
+
"cohort_hint": cohort_hint,
|
|
170
|
+
"reasoning": "; ".join(reasoning_bits) or f"role '{role}' resolved",
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def audition(*, atlas: Any, uri: str) -> dict:
|
|
175
|
+
"""atlas_audition implementation — full sidecar dump for a single URI.
|
|
176
|
+
|
|
177
|
+
Joins atlas device record + device_techniques_index entries +
|
|
178
|
+
preset_resolver curated macros (when pack is known and device user_name
|
|
179
|
+
matches a sidecar). Best-effort on every join — missing data returns
|
|
180
|
+
empty fields rather than failing the whole call.
|
|
181
|
+
"""
|
|
182
|
+
if atlas is None:
|
|
183
|
+
return {"error": "atlas not loaded", "uri": uri}
|
|
184
|
+
if not uri:
|
|
185
|
+
return {"error": "uri is required", "uri": uri}
|
|
186
|
+
|
|
187
|
+
device = atlas.lookup(uri) if hasattr(atlas, "lookup") else None
|
|
188
|
+
if not device:
|
|
189
|
+
return {
|
|
190
|
+
"error": "device not found in atlas",
|
|
191
|
+
"uri": uri,
|
|
192
|
+
"hint": "URI may be a runtime browser URI (FileId-keyed). Try atlas_explore to find the canonical atlas URI.",
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
techniques = _lookup_techniques(device)
|
|
196
|
+
pack = device.get("pack")
|
|
197
|
+
user_name = device.get("name") or ""
|
|
198
|
+
|
|
199
|
+
producer_macros: list[dict] = []
|
|
200
|
+
curated_adg_paths: list[str] = []
|
|
201
|
+
if pack and user_name:
|
|
202
|
+
try:
|
|
203
|
+
from .preset_resolver import resolve_preset_for_device
|
|
204
|
+
preset_match = resolve_preset_for_device(
|
|
205
|
+
pack_slug=pack,
|
|
206
|
+
device_class=device.get("class") or "",
|
|
207
|
+
device_user_name=user_name,
|
|
208
|
+
)
|
|
209
|
+
if preset_match.get("found"):
|
|
210
|
+
# macro_names is {idx: name}; surface as a list ordered by index
|
|
211
|
+
names = preset_match.get("macro_names") or {}
|
|
212
|
+
for idx in sorted(names.keys()):
|
|
213
|
+
producer_macros.append({
|
|
214
|
+
"index": idx,
|
|
215
|
+
"name": names[idx],
|
|
216
|
+
"source_preset": preset_match.get("preset_name"),
|
|
217
|
+
})
|
|
218
|
+
if preset_match.get("preset_file"):
|
|
219
|
+
curated_adg_paths.append(preset_match["preset_file"])
|
|
220
|
+
except Exception as exc:
|
|
221
|
+
logger.debug("audition: preset_resolver failed: %s", exc)
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
"uri": uri,
|
|
225
|
+
"name": user_name,
|
|
226
|
+
"id": device.get("id", ""),
|
|
227
|
+
"pack": pack,
|
|
228
|
+
"category": device.get("category", ""),
|
|
229
|
+
"character_tags": list(device.get("character_tags") or device.get("tags") or []),
|
|
230
|
+
"signature_techniques": techniques,
|
|
231
|
+
"producer_macros": producer_macros,
|
|
232
|
+
"curated_adg_paths": curated_adg_paths,
|
|
233
|
+
"enriched": bool(device.get("enriched")),
|
|
234
|
+
# Related demos: defer until v1.25.x reverse-index lands; explicit empty
|
|
235
|
+
# so callers can rely on the field's presence.
|
|
236
|
+
"related_demos": [],
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def substitute(
|
|
241
|
+
*,
|
|
242
|
+
atlas: Any,
|
|
243
|
+
current_uri: str,
|
|
244
|
+
anti_tag: str,
|
|
245
|
+
n: int = 3,
|
|
246
|
+
) -> dict:
|
|
247
|
+
"""atlas_substitute implementation — anti-tag-driven candidate swap.
|
|
248
|
+
|
|
249
|
+
Looks up the current device's role/category/tags, derives an excluded-tag
|
|
250
|
+
set from the anti_tag string, and returns N alternatives that:
|
|
251
|
+
- share role/category with the current pick
|
|
252
|
+
- do NOT carry any excluded character_tag
|
|
253
|
+
- are scored by AtlasResolver with the excluded names on the avoid list
|
|
254
|
+
"""
|
|
255
|
+
if atlas is None:
|
|
256
|
+
return {"error": "atlas not loaded"}
|
|
257
|
+
if not current_uri:
|
|
258
|
+
return {"error": "current_uri is required"}
|
|
259
|
+
|
|
260
|
+
current = atlas.lookup(current_uri) if hasattr(atlas, "lookup") else None
|
|
261
|
+
if not current:
|
|
262
|
+
return {
|
|
263
|
+
"error": "current device not found in atlas",
|
|
264
|
+
"current_uri": current_uri,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
excluded_tags, preferred_tags = _resolve_anti_tags(anti_tag)
|
|
268
|
+
if not excluded_tags:
|
|
269
|
+
return {
|
|
270
|
+
"error": f"unrecognized anti_tag '{anti_tag}'",
|
|
271
|
+
"supported_anti_tags": sorted(_ANTI_TAG_MAP.keys()),
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# Derive a role tag from the current device's character_tags. Prefer the
|
|
275
|
+
# most-specific role term (kick/snare/hat/bass/pad/lead/atmos) over generic.
|
|
276
|
+
current_tags_lower = [
|
|
277
|
+
str(t).lower() for t in (current.get("character_tags") or current.get("tags") or [])
|
|
278
|
+
]
|
|
279
|
+
role_priority = ("kick", "snare", "hihat", "hi-hat", "hat", "perc",
|
|
280
|
+
"bass", "pad", "lead", "atmos", "vocal", "fx", "spectral")
|
|
281
|
+
role_tag = next((t for t in role_priority if t in current_tags_lower), "")
|
|
282
|
+
role = role_tag or current.get("category") or "unknown"
|
|
283
|
+
|
|
284
|
+
# Collect candidates from the same role tag, filter out any carrying an
|
|
285
|
+
# excluded character_tag.
|
|
286
|
+
by_tag = getattr(atlas, "_by_tag", {}) or {}
|
|
287
|
+
role_candidates: list[dict] = []
|
|
288
|
+
seen: set[str] = set()
|
|
289
|
+
for dev in by_tag.get(role_tag.lower(), []) if role_tag else []:
|
|
290
|
+
uri = dev.get("uri") or ""
|
|
291
|
+
if not uri or uri in seen or uri == current_uri:
|
|
292
|
+
continue
|
|
293
|
+
dev_tags = [str(t).lower() for t in (dev.get("character_tags") or dev.get("tags") or [])]
|
|
294
|
+
if any(excl in dev_tags for excl in excluded_tags):
|
|
295
|
+
continue
|
|
296
|
+
seen.add(uri)
|
|
297
|
+
role_candidates.append(dev)
|
|
298
|
+
|
|
299
|
+
# Score the survivors via AtlasResolver and pick top N
|
|
300
|
+
from ..composer.framework.atlas_resolver import AtlasResolver
|
|
301
|
+
|
|
302
|
+
resolver = AtlasResolver(atlas=atlas)
|
|
303
|
+
scored: list[tuple[float, dict]] = []
|
|
304
|
+
for dev in role_candidates:
|
|
305
|
+
score, reasoning = resolver._score(
|
|
306
|
+
dev,
|
|
307
|
+
role=role if role in {
|
|
308
|
+
"kick", "snare", "hat", "perc", "clap",
|
|
309
|
+
"bass", "pad", "lead", "atmos", "vocal_chop", "fx", "spectral",
|
|
310
|
+
} else "",
|
|
311
|
+
genre="",
|
|
312
|
+
mood=" ".join(preferred_tags),
|
|
313
|
+
artist_refs=[],
|
|
314
|
+
avoid=[],
|
|
315
|
+
)
|
|
316
|
+
scored.append((score, AtlasResolver._to_candidate(dev, score, reasoning, source="atlas").__dict__))
|
|
317
|
+
|
|
318
|
+
scored.sort(key=lambda x: -x[0])
|
|
319
|
+
alternatives = [c for _, c in scored[:n]]
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
"current_uri": current_uri,
|
|
323
|
+
"current_name": current.get("name", ""),
|
|
324
|
+
"anti_tag": anti_tag,
|
|
325
|
+
"excluded_tags": excluded_tags,
|
|
326
|
+
"preferred_tags": preferred_tags,
|
|
327
|
+
"alternatives": alternatives,
|
|
328
|
+
"reasoning": (
|
|
329
|
+
f"Excluded tags {excluded_tags} from candidates sharing role '{role}'. "
|
|
330
|
+
f"Boosted candidates whose character_tags overlap preferred tags {preferred_tags}."
|
|
331
|
+
),
|
|
332
|
+
}
|