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.
Files changed (48) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +60 -14
  3. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  4. package/m4l_device/livepilot_bridge.js +1 -1
  5. package/mcp_server/__init__.py +1 -1
  6. package/mcp_server/atlas/__init__.py +17 -3
  7. package/mcp_server/atlas/explore_tools.py +332 -0
  8. package/mcp_server/atlas/tools.py +161 -0
  9. package/mcp_server/audit/__init__.py +6 -0
  10. package/mcp_server/audit/checks.py +618 -0
  11. package/mcp_server/audit/tools.py +232 -0
  12. package/mcp_server/composer/branch_producer.py +5 -2
  13. package/mcp_server/composer/develop/__init__.py +19 -0
  14. package/mcp_server/composer/develop/apply.py +217 -0
  15. package/mcp_server/composer/develop/brief_builder.py +269 -0
  16. package/mcp_server/composer/develop/seed_introspector.py +195 -0
  17. package/mcp_server/composer/engine.py +15 -521
  18. package/mcp_server/composer/fast/__init__.py +62 -0
  19. package/mcp_server/composer/fast/apply.py +533 -0
  20. package/mcp_server/composer/fast/brief_builder.py +1479 -0
  21. package/mcp_server/composer/fast/tier_classification.py +159 -0
  22. package/mcp_server/composer/framework/__init__.py +0 -0
  23. package/mcp_server/composer/framework/applier.py +179 -0
  24. package/mcp_server/composer/framework/artist_loader.py +63 -0
  25. package/mcp_server/composer/framework/atlas_resolver.py +554 -0
  26. package/mcp_server/composer/framework/brief.py +79 -0
  27. package/mcp_server/composer/framework/event_lexicon.py +71 -0
  28. package/mcp_server/composer/framework/genre_loader.py +77 -0
  29. package/mcp_server/composer/framework/intent_source.py +137 -0
  30. package/mcp_server/composer/framework/knowledge_pack.py +140 -0
  31. package/mcp_server/composer/framework/plan_compiler.py +10 -0
  32. package/mcp_server/composer/full/__init__.py +10 -0
  33. package/mcp_server/composer/full/apply.py +1139 -0
  34. package/mcp_server/composer/full/brief_builder.py +227 -0
  35. package/mcp_server/composer/full/engine.py +541 -0
  36. package/mcp_server/composer/full/layer_planner.py +491 -0
  37. package/mcp_server/composer/layer_planner.py +19 -465
  38. package/mcp_server/composer/sample_resolver.py +80 -7
  39. package/mcp_server/composer/tools.py +626 -28
  40. package/mcp_server/server.py +1 -0
  41. package/mcp_server/splice_client/client.py +7 -0
  42. package/mcp_server/tools/_analyzer_engine/sample.py +172 -7
  43. package/mcp_server/tools/_planner_engine.py +25 -63
  44. package/mcp_server/tools/analyzer.py +10 -4
  45. package/mcp_server/tools/browser.py +102 -19
  46. package/package.json +2 -2
  47. package/remote_script/LivePilot/__init__.py +1 -1
  48. package/server.json +3 -3
@@ -0,0 +1,159 @@
1
+ """Tier classification for instruments — used by fast-mode brief hunt-order.
2
+
3
+ The framework provides VOCABULARY (descriptive). The LLM provides FORM (creative).
4
+ For instrument candidates: Tier-A and Tier-B are safe to return in briefs.
5
+ Tier-C bare URIs MUST NEVER be returned — the brief substitutes a curated preset
6
+ or omits that synth entirely.
7
+
8
+ Tier table (BINDING):
9
+ A_curated_preset — .adg / .adv from sounds/ folder. Loaded with character.
10
+ A_drum_sample — raw sample (.aif/.wav) from drums/ folder. load_browser_item
11
+ auto-wraps in Simpler with drum-role defaults.
12
+ B_drum_synth — drum-specific synth (DS Kick etc). Default patch IS the drum.
13
+ B_audible_default — generic melodic synth (Operator, Wavetable etc).
14
+ Default patch is "generic AI synth" — only return as fallback
15
+ when sounds/ returns nothing for a melodic role.
16
+ C_needs_preset — empty container (Drum Sampler, Emit etc). NEVER return.
17
+
18
+ Legacy tier value "A_sample_ready" is accepted in VALID_BRIEF_TIERS so old
19
+ tests still pass; new candidates use the specific A_* values.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Optional
25
+
26
+
27
+ # ── Tier VALUE constants (used as the `tier` field in brief candidates) ──
28
+
29
+ TIER_A_CURATED_PRESET = "A_curated_preset"
30
+ TIER_A_DRUM_SAMPLE = "A_drum_sample"
31
+ TIER_B_DRUM_SYNTH = "B_drum_synth"
32
+ TIER_B_AUDIBLE_DEFAULT_VALUE = "B_audible_default"
33
+ TIER_C_NEEDS_PRESET_VALUE = "C_needs_preset"
34
+
35
+ # Tiers that are valid in brief output (Tier-C is NEVER valid)
36
+ VALID_BRIEF_TIERS = frozenset({
37
+ TIER_A_CURATED_PRESET,
38
+ TIER_A_DRUM_SAMPLE,
39
+ TIER_B_DRUM_SYNTH,
40
+ TIER_B_AUDIBLE_DEFAULT_VALUE,
41
+ # Legacy alias — kept so old briefs and tests still work
42
+ "A_sample_ready",
43
+ # Legacy alias — "B_audible_default" is also the old tier value and is valid
44
+ "B_audible_default",
45
+ })
46
+
47
+ # Role sets
48
+ DRUM_ROLES = frozenset({"kick", "snare", "hat", "perc", "clap", "tom", "drum"})
49
+ MELODIC_ROLES = frozenset({"bass", "lead", "pad", "atmos", "vox", "fx", "texture"})
50
+
51
+
52
+ # ── Frozensets of instrument names (used for name-based classification) ──
53
+
54
+ # Drum-specific synths — purpose-built drum sound generators.
55
+ # Their default patches ARE the intended drum sound (unlike generic melodic
56
+ # synths whose defaults are "generic AI synth").
57
+ # Allowed in drum-role briefs as Tier-B without curated presets.
58
+ DRUM_SPECIFIC_SYNTHS: frozenset[str] = frozenset({
59
+ "DS Kick",
60
+ "DS Snare",
61
+ "DS Hi-Hat",
62
+ "DS Clap",
63
+ "DS Cymbal",
64
+ "DS Tom",
65
+ "DS Sampler",
66
+ "DS Drum Bus",
67
+ })
68
+
69
+ # Melodic synths with audible defaults — "generic AI synth" risk.
70
+ # Renamed from TIER_B_AUDIBLE_DEFAULT to MELODIC_AUDIBLE_DEFAULTS to
71
+ # disambiguate from the tier VALUE string TIER_B_AUDIBLE_DEFAULT_VALUE.
72
+ # The old name TIER_B_AUDIBLE_DEFAULT is kept as an alias for backward compat.
73
+ MELODIC_AUDIBLE_DEFAULTS: frozenset[str] = frozenset({
74
+ "Operator",
75
+ "Wavetable",
76
+ "Drift",
77
+ "Analog",
78
+ "Bass",
79
+ "Electric",
80
+ "Tension",
81
+ "Collision",
82
+ "Meld",
83
+ })
84
+
85
+ # Backward-compat alias (old tests and callers use this name)
86
+ TIER_B_AUDIBLE_DEFAULT = MELODIC_AUDIBLE_DEFAULTS
87
+
88
+ # Containers + programming-required synths — NEVER return bare URIs.
89
+ # Renamed from TIER_C_NEEDS_PRESET to CONTAINERS_NEEDING_PRESETS to
90
+ # disambiguate from the tier VALUE string TIER_C_NEEDS_PRESET_VALUE.
91
+ # The old name TIER_C_NEEDS_PRESET is kept as an alias for backward compat.
92
+ CONTAINERS_NEEDING_PRESETS: frozenset[str] = frozenset({
93
+ "Drum Sampler",
94
+ "Drum Rack",
95
+ "DrumGroup", # internal alias for Drum Rack
96
+ "Simpler",
97
+ "Sampler",
98
+ "Impulse",
99
+ "Emit",
100
+ "Vector FM",
101
+ "Vector Grain",
102
+ "Granulator III",
103
+ "Granulator II", # legacy
104
+ "Instrument Rack",
105
+ "Looper",
106
+ "External Instrument",
107
+ })
108
+
109
+ # Backward-compat alias
110
+ TIER_C_NEEDS_PRESET = CONTAINERS_NEEDING_PRESETS
111
+
112
+ # Combined lookup: name → tier string
113
+ # Drum-specific synths → "B_drum_synth"; melodic audible defaults → "B_audible_default";
114
+ # containers → "C_needs_preset". Unknown instruments return None.
115
+ TIER_CLASSIFICATION: dict[str, str] = {
116
+ name: "B_drum_synth" for name in DRUM_SPECIFIC_SYNTHS
117
+ } | {
118
+ name: "B_audible_default" for name in MELODIC_AUDIBLE_DEFAULTS
119
+ } | {
120
+ name: "C_needs_preset" for name in CONTAINERS_NEEDING_PRESETS
121
+ }
122
+
123
+
124
+ def classify_instrument(name: str) -> Optional[str]:
125
+ """Classify an instrument by name.
126
+
127
+ Returns one of:
128
+ "B_drum_synth" — drum-specific synth, safe for drum roles bare
129
+ "B_audible_default" — generic melodic synth, only as fallback
130
+ "C_needs_preset" — container, NEVER return bare
131
+ None — unknown instrument
132
+ """
133
+ return TIER_CLASSIFICATION.get(name)
134
+
135
+
136
+ def is_drum_specific_synth(name: str) -> bool:
137
+ """True if `name` is a drum-specific synth — safe to return bare for drum roles."""
138
+ return name in DRUM_SPECIFIC_SYNTHS
139
+
140
+
141
+ # Search terms for hunting curated chains in sounds/ and drums/ per role.
142
+ # Used by build_creative_brief to populate instruments_by_role.
143
+ ROLE_SEARCH_TERMS: dict[str, dict[str, Optional[str]]] = {
144
+ # drum roles — search drums/ first, sounds/ second
145
+ "kick": {"sounds_term": "kick", "drums_term": "kick"},
146
+ "snare": {"sounds_term": "snare", "drums_term": "snare"},
147
+ "hat": {"sounds_term": "hihat", "drums_term": "hihat"},
148
+ "perc": {"sounds_term": "percussion", "drums_term": "perc"},
149
+ "clap": {"sounds_term": "clap", "drums_term": "clap"},
150
+ "tom": {"sounds_term": "tom", "drums_term": "tom"},
151
+ # melodic roles — search sounds/ for curated presets
152
+ "bass": {"sounds_term": "bass", "drums_term": None},
153
+ "lead": {"sounds_term": "lead", "drums_term": None},
154
+ "pad": {"sounds_term": "pad", "drums_term": None},
155
+ "atmos": {"sounds_term": "ambient", "drums_term": None},
156
+ "vox": {"sounds_term": "vocal", "drums_term": None},
157
+ "fx": {"sounds_term": "fx", "drums_term": None},
158
+ "texture": {"sounds_term": "texture", "drums_term": None},
159
+ }
File without changes
@@ -0,0 +1,179 @@
1
+ """Applier — shared Phase-3 executor for fast / full / develop compose modes.
2
+
3
+ Concentrates pre-flight (analyzer load + bridge connect + handshake retry)
4
+ and post-flight (monitoring state + back_to_arranger) so fixes for
5
+ BUG-FULL-MODE-14 (bridge race) and BUG-FULL-MODE-17 (manual arm required)
6
+ land once across all three modes instead of three times.
7
+
8
+ Functions are dependency-injected (not imported directly) so unit tests can
9
+ mock them without monkey-patching.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import logging
16
+ from typing import Any, Awaitable, Callable, Optional
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ # Type aliases
22
+ AsyncCtxFn = Callable[[Any], Awaitable[Any]]
23
+ AsyncTrackFn = Callable[..., Awaitable[Any]]
24
+
25
+
26
+ class Applier:
27
+ """Shared pre-flight + apply + post-flight skeleton for compose modes.
28
+
29
+ v1.24 Phase 1: pre-flight + post-flight only. The `run()` method (full
30
+ plan execution) stays a stub; each mode's apply.py wires its own
31
+ plan-execution loop using `Applier.preflight()` and `Applier.postflight()`
32
+ around it.
33
+
34
+ Future phases may extend this with a unified plan-walker.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ *,
40
+ ensure_analyzer_fn: AsyncCtxFn,
41
+ reconnect_bridge_fn: AsyncCtxFn,
42
+ bridge_ping_fn: AsyncCtxFn,
43
+ set_track_input_monitoring_fn: Optional[AsyncTrackFn] = None,
44
+ back_to_arranger_fn: Optional[AsyncCtxFn] = None,
45
+ handshake_max_attempts: int = 3,
46
+ handshake_gap_seconds: float = 0.2,
47
+ ):
48
+ self._ensure_analyzer_fn = ensure_analyzer_fn
49
+ self._reconnect_bridge_fn = reconnect_bridge_fn
50
+ self._bridge_ping_fn = bridge_ping_fn
51
+ self._set_track_input_monitoring_fn = set_track_input_monitoring_fn
52
+ self._back_to_arranger_fn = back_to_arranger_fn
53
+ self._handshake_max_attempts = handshake_max_attempts
54
+ self._handshake_gap_seconds = handshake_gap_seconds
55
+
56
+ # ── pre-flight ──────────────────────────────────────────────────
57
+
58
+ async def preflight(self, ctx: Any) -> dict:
59
+ """Load analyzer, connect bridge, handshake until ping succeeds.
60
+
61
+ Fixes BUG-FULL-MODE-14: the previous code returned success on
62
+ bridge.connect() but the M4L JS listener takes 100-500ms to bind
63
+ the UDP socket. Without the handshake retry loop, the next
64
+ bridge-using call (load_sample_to_simpler etc.) fails with
65
+ "UDP bridge is not connected".
66
+
67
+ Returns dict with keys: analyzer_status, bridge_connected,
68
+ handshake_attempts, and optionally handshake_error if all
69
+ retries failed.
70
+ """
71
+ analyzer_result = await self._ensure_analyzer_fn(ctx)
72
+ analyzer_status = (
73
+ analyzer_result.get("status", "unknown")
74
+ if isinstance(analyzer_result, dict)
75
+ else "unknown"
76
+ )
77
+
78
+ bridge_result = await self._reconnect_bridge_fn(ctx)
79
+ bridge_connected_initial = (
80
+ bridge_result.get("connected", False)
81
+ if isinstance(bridge_result, dict)
82
+ else False
83
+ )
84
+
85
+ handshake_attempts = 0
86
+ handshake_error: Optional[str] = None
87
+ bridge_ready = False
88
+
89
+ if bridge_connected_initial:
90
+ for attempt in range(1, self._handshake_max_attempts + 1):
91
+ handshake_attempts = attempt
92
+ try:
93
+ await self._bridge_ping_fn(ctx)
94
+ bridge_ready = True
95
+ break
96
+ except Exception as exc:
97
+ handshake_error = str(exc)
98
+ logger.debug(
99
+ "Applier preflight handshake attempt %d failed: %s",
100
+ attempt,
101
+ exc,
102
+ )
103
+ if attempt < self._handshake_max_attempts:
104
+ await asyncio.sleep(self._handshake_gap_seconds)
105
+
106
+ result = {
107
+ "analyzer_status": analyzer_status,
108
+ "bridge_connected": bridge_ready,
109
+ "handshake_attempts": handshake_attempts,
110
+ }
111
+ if not bridge_ready and handshake_error is not None:
112
+ result["handshake_error"] = handshake_error
113
+ return result
114
+
115
+ # ── post-flight ─────────────────────────────────────────────────
116
+
117
+ async def postflight(
118
+ self,
119
+ ctx: Any,
120
+ applied_track_indices: list[int],
121
+ ) -> dict:
122
+ """Set monitoring=Auto on each newly-created track, then back_to_arranger.
123
+
124
+ BUG-FIX (post-Phase-4-Task-4 live test): the original BUG-FULL-MODE-17
125
+ fix set monitoring to state=0 ("In"), which is WRONG. State codes:
126
+ 0 = In (always pass input through — leaves track "hot")
127
+ 1 = Auto (monitor when armed + recording — DEFAULT for new tracks)
128
+ 2 = Off (never monitor)
129
+
130
+ New tracks default to Auto (1) and arrangement clips already play
131
+ correctly with Auto. The actual fix for "manual arm required" is
132
+ back_to_arranger alone — clearing the session-vs-arrangement
133
+ override flag. We set state=Auto here defensively in case any
134
+ other code path moved monitoring away from default.
135
+
136
+ applied_track_indices: list of track indices that were created
137
+ during this apply pass. Empty list (e.g. develop mode writing
138
+ only session clips, no new tracks) skips the per-track step but
139
+ still calls back_to_arranger.
140
+ """
141
+ tracks_set = 0
142
+ if self._set_track_input_monitoring_fn is not None:
143
+ for track_index in applied_track_indices:
144
+ try:
145
+ # state=1 (Auto) — the default for new tracks. NOT 0 (In).
146
+ await self._set_track_input_monitoring_fn(
147
+ ctx, track_index=track_index, state=1
148
+ )
149
+ tracks_set += 1
150
+ except Exception as exc:
151
+ logger.warning(
152
+ "Applier postflight set_monitoring failed for track %d: %s",
153
+ track_index,
154
+ exc,
155
+ )
156
+
157
+ back_to_arranger_ok = False
158
+ if self._back_to_arranger_fn is not None:
159
+ try:
160
+ await self._back_to_arranger_fn(ctx)
161
+ back_to_arranger_ok = True
162
+ except Exception as exc:
163
+ logger.warning("Applier postflight back_to_arranger failed: %s", exc)
164
+
165
+ return {
166
+ "tracks_set": tracks_set,
167
+ "back_to_arranger": back_to_arranger_ok,
168
+ }
169
+
170
+ # ── stub run() — not used in v1.24 ──────────────────────────────
171
+
172
+ async def run(self, ctx: Any, plan: list) -> dict:
173
+ """Stub kept from Task 1 — full plan-walk implementation deferred.
174
+
175
+ Phase 1 only extracts pre/post-flight. Each mode's apply.py
176
+ continues to execute its own plan loop, just sandwiched by
177
+ ``preflight()`` and ``postflight()``.
178
+ """
179
+ return {"status": "stub"}
@@ -0,0 +1,63 @@
1
+ """Markdown parser for livepilot/skills/livepilot-core/references/artist-vocabularies.md."""
2
+
3
+ from __future__ import annotations
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+
9
+ _ARTIST_CACHE: Optional[dict[str, dict]] = None
10
+
11
+
12
+ def _artist_md_path() -> Optional[Path]:
13
+ here = Path(__file__).resolve()
14
+ for parent in here.parents:
15
+ candidate = parent / "livepilot" / "skills" / "livepilot-core" / "references" / "artist-vocabularies.md"
16
+ if candidate.exists():
17
+ return candidate
18
+ return None
19
+
20
+
21
+ def _load_artist_md() -> dict[str, dict]:
22
+ """Parse artist entries. Existing pattern in develop/brief_builder.py:
23
+ `### Name` headers with body content."""
24
+ global _ARTIST_CACHE
25
+ if _ARTIST_CACHE is not None:
26
+ return _ARTIST_CACHE
27
+ path = _artist_md_path()
28
+ if path is None:
29
+ _ARTIST_CACHE = {}
30
+ return _ARTIST_CACHE
31
+ text = path.read_text(encoding="utf-8")
32
+
33
+ artists: dict[str, dict] = {}
34
+ sections = re.split(r"^###\s+(.+?)\s*$", text, flags=re.MULTILINE)
35
+ for i in range(1, len(sections), 2):
36
+ name = sections[i].strip()
37
+ if not name or "Vocabulary" in name or "How to" in name:
38
+ continue
39
+ body = sections[i + 1] if i + 1 < len(sections) else ""
40
+ # Strip parenthetical aliases for canonical key: "Aphex Twin (Richard D. James)" → "Aphex Twin"
41
+ primary = re.sub(r"\s*\(.*?\)\s*$", "", name).strip()
42
+ if primary:
43
+ artists[primary] = {"name": primary, "raw_md": body.strip()[:2000]}
44
+ _ARTIST_CACHE = artists
45
+ return _ARTIST_CACHE
46
+
47
+
48
+ def load_artist_context(artist_names: list[str]) -> dict[str, dict]:
49
+ """Load context for the given artist names.
50
+
51
+ artist_names: list of names (case-insensitive matched against parser keys)
52
+ Returns: {artist_name: {raw_md: ..., ...}, ...}
53
+ """
54
+ if not artist_names:
55
+ return {}
56
+ parsed = _load_artist_md()
57
+ result = {}
58
+ parsed_lower = {k.lower(): k for k in parsed.keys()}
59
+ for name in artist_names:
60
+ canonical = parsed_lower.get(name.lower())
61
+ if canonical:
62
+ result[canonical] = parsed[canonical]
63
+ return result