livepilot 1.23.6 → 1.24.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 (42) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +59 -13
  3. package/mcp_server/__init__.py +1 -1
  4. package/mcp_server/atlas/__init__.py +17 -3
  5. package/mcp_server/audit/__init__.py +6 -0
  6. package/mcp_server/audit/checks.py +618 -0
  7. package/mcp_server/audit/tools.py +232 -0
  8. package/mcp_server/composer/branch_producer.py +5 -2
  9. package/mcp_server/composer/develop/__init__.py +19 -0
  10. package/mcp_server/composer/develop/apply.py +217 -0
  11. package/mcp_server/composer/develop/brief_builder.py +269 -0
  12. package/mcp_server/composer/develop/seed_introspector.py +195 -0
  13. package/mcp_server/composer/engine.py +15 -521
  14. package/mcp_server/composer/fast/__init__.py +62 -0
  15. package/mcp_server/composer/fast/apply.py +533 -0
  16. package/mcp_server/composer/fast/brief_builder.py +1479 -0
  17. package/mcp_server/composer/fast/tier_classification.py +159 -0
  18. package/mcp_server/composer/framework/__init__.py +0 -0
  19. package/mcp_server/composer/framework/applier.py +179 -0
  20. package/mcp_server/composer/framework/artist_loader.py +63 -0
  21. package/mcp_server/composer/framework/brief.py +79 -0
  22. package/mcp_server/composer/framework/event_lexicon.py +71 -0
  23. package/mcp_server/composer/framework/genre_loader.py +77 -0
  24. package/mcp_server/composer/framework/intent_source.py +137 -0
  25. package/mcp_server/composer/framework/knowledge_pack.py +49 -0
  26. package/mcp_server/composer/framework/plan_compiler.py +10 -0
  27. package/mcp_server/composer/full/__init__.py +10 -0
  28. package/mcp_server/composer/full/apply.py +1139 -0
  29. package/mcp_server/composer/full/brief_builder.py +144 -0
  30. package/mcp_server/composer/full/engine.py +541 -0
  31. package/mcp_server/composer/full/layer_planner.py +491 -0
  32. package/mcp_server/composer/layer_planner.py +19 -465
  33. package/mcp_server/composer/sample_resolver.py +80 -7
  34. package/mcp_server/composer/tools.py +626 -28
  35. package/mcp_server/server.py +1 -0
  36. package/mcp_server/splice_client/client.py +7 -0
  37. package/mcp_server/tools/_analyzer_engine/sample.py +162 -6
  38. package/mcp_server/tools/_planner_engine.py +25 -63
  39. package/mcp_server/tools/analyzer.py +10 -4
  40. package/package.json +2 -2
  41. package/remote_script/LivePilot/__init__.py +1 -1
  42. package/server.json +3 -3
@@ -0,0 +1,137 @@
1
+ """Abstract source of CompositionIntent — prompt-derived, session-derived, or hybrid.
2
+
3
+ PromptSource: parses a free-text prompt via prompt_parser.parse_prompt
4
+ SessionSource: introspects the live Ableton session via the MCP tool layer
5
+ HybridSource: combines both — session wins on tempo/key, prompt wins on genre/mood
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from abc import ABC, abstractmethod
12
+ from typing import Any, Optional
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class IntentSource(ABC):
18
+ """Abstract base — produces a CompositionIntent-shaped dict for any compose mode.
19
+
20
+ Returned dict shape (CompositionIntent fields):
21
+ {
22
+ "genre": str,
23
+ "mood": str,
24
+ "tempo": int,
25
+ "key": str,
26
+ "sub_genre": str,
27
+ "descriptors": list,
28
+ "explicit_elements": list,
29
+ "energy": float,
30
+ "layer_count": int,
31
+ "duration_bars": int,
32
+ # ... see prompt_parser.CompositionIntent for full list
33
+ }
34
+ """
35
+
36
+ @abstractmethod
37
+ def parse(self, ctx: Any) -> dict:
38
+ ...
39
+
40
+
41
+ class PromptSource(IntentSource):
42
+ """Parses a free-text prompt via prompt_parser.parse_prompt."""
43
+
44
+ def __init__(self, prompt: str):
45
+ self.prompt = prompt
46
+
47
+ def parse(self, ctx: Any) -> dict:
48
+ # Lazy import to avoid circular deps at framework-package import time
49
+ from ..prompt_parser import parse_prompt
50
+ intent = parse_prompt(self.prompt)
51
+ # CompositionIntent has its own to_dict(); prefer it, else fall back to asdict
52
+ if hasattr(intent, "to_dict"):
53
+ return intent.to_dict()
54
+ if hasattr(intent, "__dataclass_fields__"):
55
+ from dataclasses import asdict
56
+ return asdict(intent)
57
+ if isinstance(intent, dict):
58
+ return intent
59
+ # Fallback for unexpected return type
60
+ return {"raw_intent": str(intent)}
61
+
62
+
63
+ class SessionSource(IntentSource):
64
+ """Introspects the live Ableton session via MCP tools.
65
+
66
+ Reads session tempo, key, time signature, and per-track content.
67
+ Returns the same CompositionIntent shape as PromptSource so downstream
68
+ consumers don't care which source was used.
69
+ """
70
+
71
+ def __init__(self, scene_index: int = 0):
72
+ self.scene_index = scene_index
73
+
74
+ def parse(self, ctx: Any) -> dict:
75
+ ableton = ctx.lifespan_context.get("ableton") if hasattr(ctx, "lifespan_context") else None
76
+ if ableton is None:
77
+ logger.warning("SessionSource.parse: ableton client not in ctx — returning empty intent")
78
+ return {}
79
+
80
+ try:
81
+ session = ableton.send_command("get_session_info", {})
82
+ except Exception as exc:
83
+ logger.warning("SessionSource.parse: get_session_info failed: %s", exc)
84
+ return {}
85
+
86
+ intent: dict = {
87
+ "tempo": float(session.get("tempo", 120.0)),
88
+ "scene_index": self.scene_index,
89
+ }
90
+
91
+ # Time signature
92
+ sig_num = session.get("signature_numerator")
93
+ sig_den = session.get("signature_denominator")
94
+ if sig_num and sig_den:
95
+ intent["time_signature"] = f"{sig_num}/{sig_den}"
96
+
97
+ # Try to read song scale (Live 12.4 song-scale API)
98
+ try:
99
+ scale_result = ableton.send_command("get_song_scale", {})
100
+ if scale_result and not scale_result.get("error"):
101
+ intent["key"] = scale_result.get("root_note") or scale_result.get("key")
102
+ intent["scale_mode"] = scale_result.get("scale_name") or scale_result.get("mode")
103
+ except Exception as exc:
104
+ logger.debug("SessionSource.parse: get_song_scale unavailable: %s", exc)
105
+
106
+ return intent
107
+
108
+
109
+ class HybridSource(IntentSource):
110
+ """Combines prompt + session intent.
111
+
112
+ Merge rule:
113
+ - tempo: session wins (live truth) if present, else prompt
114
+ - key/scale_mode: session wins if present, else prompt
115
+ - genre/mood: prompt wins (creative directive)
116
+ - other fields: prompt wins, session fills gaps
117
+ """
118
+
119
+ def __init__(self, prompt: str, scene_index: int = 0):
120
+ self.prompt = prompt
121
+ self.scene_index = scene_index
122
+
123
+ def parse(self, ctx: Any) -> dict:
124
+ prompt_intent = PromptSource(self.prompt).parse(ctx)
125
+ session_intent = SessionSource(self.scene_index).parse(ctx)
126
+
127
+ merged = dict(prompt_intent) # start with prompt as base
128
+ # Session-wins overrides
129
+ for session_authoritative in ("tempo", "key", "scale_mode", "time_signature"):
130
+ session_val = session_intent.get(session_authoritative)
131
+ if session_val is not None:
132
+ merged[session_authoritative] = session_val
133
+ # Carry session-only fields the prompt doesn't have
134
+ for k, v in session_intent.items():
135
+ if k not in merged:
136
+ merged[k] = v
137
+ return merged
@@ -0,0 +1,49 @@
1
+ """KnowledgePack — assembles the vocabulary fields each compose mode's brief carries.
2
+
3
+ For full mode: rich (event_lexicon, genre_context, artist_context, atlas_candidates, manual_snippets).
4
+ For fast mode: subset (no event_lexicon — fast mode is loop-sketch scope, doesn't design form).
5
+ For develop mode: subset overlapping with full.
6
+ """
7
+
8
+ from __future__ import annotations
9
+ from typing import Any
10
+
11
+ from .event_lexicon import EVENT_LEXICON, get_event_lexicon
12
+ from .genre_loader import load_genre_context
13
+ from .artist_loader import load_artist_context
14
+
15
+
16
+ class KnowledgePack:
17
+ """Central knowledge assembly for compose-mode briefs."""
18
+
19
+ def build(self, intent: dict, mode: str = "full") -> dict:
20
+ """Build the knowledge fields for a brief.
21
+
22
+ intent: parsed CompositionIntent dict with at least 'genre', possibly 'sub_genre'.
23
+ May also carry 'artists' (list of producer names).
24
+ mode: 'fast' | 'full' | 'develop'
25
+
26
+ Returns a dict — pass directly to brief.knowledge OR spread into brief top-level.
27
+ """
28
+ # Genre context — descriptive vocabulary for the parsed style
29
+ genre = intent.get("sub_genre") or intent.get("genre") or ""
30
+ genre_ctx = load_genre_context(genre) if genre else {}
31
+
32
+ # Artist context — populated only if prompt mentioned producers
33
+ artist_names = intent.get("artists") or []
34
+ artist_ctx = load_artist_context(artist_names)
35
+
36
+ knowledge = {
37
+ "genre_context": genre_ctx,
38
+ "artist_context": artist_ctx,
39
+ "atlas_candidates_per_role": {}, # populated by brief_builder via existing get_role_candidates
40
+ "manual_snippets": {}, # populated by brief_builder per likely device
41
+ }
42
+
43
+ # Event lexicon — full mode only (loop sketch doesn't design form)
44
+ if mode in ("full", "develop"):
45
+ knowledge["event_lexicon"] = list(EVENT_LEXICON)
46
+ else:
47
+ knowledge["event_lexicon"] = []
48
+
49
+ return knowledge
@@ -0,0 +1,10 @@
1
+ """PlanCompiler — turns a Brief-derived plan into a Remote Script step sequence."""
2
+ from typing import Any
3
+
4
+
5
+ class PlanCompiler:
6
+ """Phase 1 stub — full impl in Phase 2 onwards."""
7
+
8
+ def compile(self, brief: Any) -> list:
9
+ """Return list of step dicts. Stub returns empty."""
10
+ return []
@@ -0,0 +1,10 @@
1
+ """Full compose mode — currently deterministic engine; will become two-phase
2
+ LLM-creative in v1.24 Phase 3 (BUG-FULL-MODE-18 fix).
3
+
4
+ Phase 1 of v1.24 just moves the existing code here without behavior change.
5
+ """
6
+ from .engine import ComposerEngine, CompositionResult
7
+ from .apply import apply_full_plan
8
+ from .brief_builder import build_full_brief
9
+
10
+ __all__ = ["ComposerEngine", "CompositionResult", "apply_full_plan", "build_full_brief"]