livepilot 1.9.21 → 1.9.23

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 (110) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.mcpbignore +40 -0
  3. package/AGENTS.md +2 -2
  4. package/CHANGELOG.md +47 -0
  5. package/CONTRIBUTING.md +1 -1
  6. package/README.md +47 -72
  7. package/bin/livepilot.js +135 -0
  8. package/livepilot/.Codex-plugin/plugin.json +2 -2
  9. package/livepilot/.claude-plugin/plugin.json +2 -2
  10. package/livepilot/agents/livepilot-producer/AGENT.md +13 -0
  11. package/livepilot/commands/arrange.md +42 -14
  12. package/livepilot/commands/beat.md +68 -21
  13. package/livepilot/commands/evaluate.md +23 -13
  14. package/livepilot/commands/mix.md +35 -11
  15. package/livepilot/commands/perform.md +31 -19
  16. package/livepilot/commands/sounddesign.md +38 -17
  17. package/livepilot/skills/livepilot-arrangement/SKILL.md +2 -1
  18. package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +2 -2
  19. package/livepilot/skills/livepilot-core/SKILL.md +60 -4
  20. package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +11 -11
  21. package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +25 -25
  22. package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +21 -21
  23. package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +13 -13
  24. package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +13 -13
  25. package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +5 -5
  26. package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +16 -16
  27. package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +40 -40
  28. package/livepilot/skills/livepilot-core/references/m4l-devices.md +3 -3
  29. package/livepilot/skills/livepilot-core/references/overview.md +4 -4
  30. package/livepilot/skills/livepilot-evaluation/SKILL.md +12 -8
  31. package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +2 -2
  32. package/livepilot/skills/livepilot-mix-engine/SKILL.md +1 -1
  33. package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +2 -2
  34. package/livepilot/skills/livepilot-mixing/SKILL.md +3 -1
  35. package/livepilot/skills/livepilot-notes/SKILL.md +2 -1
  36. package/livepilot/skills/livepilot-release/SKILL.md +15 -15
  37. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +2 -2
  38. package/livepilot/skills/livepilot-wonder/SKILL.md +62 -0
  39. package/livepilot.mcpb +0 -0
  40. package/m4l_device/livepilot_bridge.js +1 -1
  41. package/manifest.json +91 -0
  42. package/mcp_server/__init__.py +1 -1
  43. package/mcp_server/creative_constraints/__init__.py +6 -0
  44. package/mcp_server/creative_constraints/engine.py +277 -0
  45. package/mcp_server/creative_constraints/models.py +75 -0
  46. package/mcp_server/creative_constraints/tools.py +341 -0
  47. package/mcp_server/experiment/__init__.py +6 -0
  48. package/mcp_server/experiment/engine.py +213 -0
  49. package/mcp_server/experiment/models.py +120 -0
  50. package/mcp_server/experiment/tools.py +263 -0
  51. package/mcp_server/hook_hunter/__init__.py +5 -0
  52. package/mcp_server/hook_hunter/analyzer.py +342 -0
  53. package/mcp_server/hook_hunter/models.py +57 -0
  54. package/mcp_server/hook_hunter/tools.py +586 -0
  55. package/mcp_server/memory/taste_graph.py +261 -0
  56. package/mcp_server/memory/tools.py +88 -0
  57. package/mcp_server/mix_engine/critics.py +2 -2
  58. package/mcp_server/mix_engine/models.py +1 -1
  59. package/mcp_server/mix_engine/state_builder.py +2 -2
  60. package/mcp_server/musical_intelligence/__init__.py +8 -0
  61. package/mcp_server/musical_intelligence/detectors.py +421 -0
  62. package/mcp_server/musical_intelligence/phrase_critic.py +163 -0
  63. package/mcp_server/musical_intelligence/tools.py +221 -0
  64. package/mcp_server/preview_studio/__init__.py +5 -0
  65. package/mcp_server/preview_studio/engine.py +280 -0
  66. package/mcp_server/preview_studio/models.py +73 -0
  67. package/mcp_server/preview_studio/tools.py +423 -0
  68. package/mcp_server/runtime/session_kernel.py +96 -0
  69. package/mcp_server/runtime/tools.py +90 -1
  70. package/mcp_server/semantic_moves/__init__.py +13 -0
  71. package/mcp_server/semantic_moves/compiler.py +116 -0
  72. package/mcp_server/semantic_moves/mix_compilers.py +291 -0
  73. package/mcp_server/semantic_moves/mix_moves.py +157 -0
  74. package/mcp_server/semantic_moves/models.py +46 -0
  75. package/mcp_server/semantic_moves/performance_compilers.py +208 -0
  76. package/mcp_server/semantic_moves/performance_moves.py +81 -0
  77. package/mcp_server/semantic_moves/registry.py +32 -0
  78. package/mcp_server/semantic_moves/resolvers.py +126 -0
  79. package/mcp_server/semantic_moves/sound_design_compilers.py +266 -0
  80. package/mcp_server/semantic_moves/sound_design_moves.py +78 -0
  81. package/mcp_server/semantic_moves/tools.py +204 -0
  82. package/mcp_server/semantic_moves/transition_compilers.py +222 -0
  83. package/mcp_server/semantic_moves/transition_moves.py +76 -0
  84. package/mcp_server/server.py +10 -0
  85. package/mcp_server/session_continuity/__init__.py +6 -0
  86. package/mcp_server/session_continuity/models.py +86 -0
  87. package/mcp_server/session_continuity/tools.py +230 -0
  88. package/mcp_server/session_continuity/tracker.py +235 -0
  89. package/mcp_server/song_brain/__init__.py +6 -0
  90. package/mcp_server/song_brain/builder.py +477 -0
  91. package/mcp_server/song_brain/models.py +132 -0
  92. package/mcp_server/song_brain/tools.py +294 -0
  93. package/mcp_server/stuckness_detector/__init__.py +5 -0
  94. package/mcp_server/stuckness_detector/detector.py +400 -0
  95. package/mcp_server/stuckness_detector/models.py +66 -0
  96. package/mcp_server/stuckness_detector/tools.py +195 -0
  97. package/mcp_server/tools/_conductor.py +104 -6
  98. package/mcp_server/tools/analyzer.py +1 -1
  99. package/mcp_server/tools/devices.py +34 -0
  100. package/mcp_server/wonder_mode/__init__.py +6 -0
  101. package/mcp_server/wonder_mode/diagnosis.py +84 -0
  102. package/mcp_server/wonder_mode/engine.py +493 -0
  103. package/mcp_server/wonder_mode/session.py +114 -0
  104. package/mcp_server/wonder_mode/tools.py +285 -0
  105. package/package.json +2 -2
  106. package/remote_script/LivePilot/__init__.py +1 -1
  107. package/remote_script/LivePilot/browser.py +4 -1
  108. package/remote_script/LivePilot/devices.py +29 -0
  109. package/remote_script/LivePilot/tracks.py +11 -4
  110. package/scripts/generate_tool_catalog.py +131 -0
@@ -0,0 +1,261 @@
1
+ """TasteGraph — extended taste model for personalized move ranking.
2
+
3
+ Builds on the existing TasteMemoryStore and AntiMemoryStore to add:
4
+ - Move family scoring (which semantic move families does the user prefer?)
5
+ - Device affinities (which synths, effects, and kits resonate?)
6
+ - Novelty band (how experimental vs. conservative does the user want to be?)
7
+ - Evidence tracking (what decisions informed each inference?)
8
+
9
+ The TasteGraph is the bridge between "what moves are available" and
10
+ "what moves does THIS user want." It powers rank_moves_by_taste.
11
+
12
+ Pure computation — no I/O. Updated from outcome data.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import time
18
+ from dataclasses import dataclass, field
19
+ from typing import Optional
20
+
21
+
22
+ @dataclass
23
+ class MoveFamilyScore:
24
+ """How much a user favors a semantic move family."""
25
+ family: str # mix, arrangement, transition, sound_design
26
+ score: float = 0.0 # -1 to 1 (negative = dislikes, positive = prefers)
27
+ kept_count: int = 0
28
+ undone_count: int = 0
29
+ last_updated_ms: int = 0
30
+
31
+ def to_dict(self) -> dict:
32
+ return {
33
+ "family": self.family,
34
+ "score": round(self.score, 3),
35
+ "kept_count": self.kept_count,
36
+ "undone_count": self.undone_count,
37
+ }
38
+
39
+
40
+ @dataclass
41
+ class DeviceAffinity:
42
+ """How much a user likes a particular device or device class."""
43
+ device_name: str
44
+ affinity: float = 0.0 # -1 to 1
45
+ use_count: int = 0
46
+ last_used_ms: int = 0
47
+
48
+ def to_dict(self) -> dict:
49
+ return {
50
+ "device_name": self.device_name,
51
+ "affinity": round(self.affinity, 3),
52
+ "use_count": self.use_count,
53
+ }
54
+
55
+
56
+ @dataclass
57
+ class TasteGraph:
58
+ """Extended taste model for personalized ranking.
59
+
60
+ Combines dimension preferences, move family scores, device affinities,
61
+ and novelty tolerance into a single queryable model.
62
+ """
63
+ # Core dimension preferences (from existing TasteMemoryStore)
64
+ dimension_weights: dict[str, float] = field(default_factory=dict)
65
+
66
+ # Dimension avoidances (from AntiMemoryStore)
67
+ dimension_avoidances: dict[str, str] = field(default_factory=dict)
68
+
69
+ # Move family preferences
70
+ move_family_scores: dict[str, MoveFamilyScore] = field(default_factory=dict)
71
+
72
+ # Device preferences
73
+ device_affinities: dict[str, DeviceAffinity] = field(default_factory=dict)
74
+
75
+ # Novelty tolerance: 0 = very conservative, 1 = very experimental
76
+ novelty_band: float = 0.5
77
+
78
+ # Total evidence count (how many decisions informed this graph)
79
+ evidence_count: int = 0
80
+ last_updated_ms: int = 0
81
+
82
+ def to_dict(self) -> dict:
83
+ return {
84
+ "dimension_weights": self.dimension_weights,
85
+ "dimension_avoidances": self.dimension_avoidances,
86
+ "move_family_scores": {
87
+ k: v.to_dict() for k, v in self.move_family_scores.items()
88
+ },
89
+ "device_affinities": {
90
+ k: v.to_dict()
91
+ for k, v in sorted(
92
+ self.device_affinities.items(),
93
+ key=lambda x: -x[1].affinity,
94
+ )[:10] # Top 10 only
95
+ },
96
+ "novelty_band": round(self.novelty_band, 3),
97
+ "evidence_count": self.evidence_count,
98
+ }
99
+
100
+ # ── Update methods ───────────────────────────────────────────────
101
+
102
+ def record_move_outcome(
103
+ self, move_id: str, family: str, kept: bool, score: float = 0.0
104
+ ) -> None:
105
+ """Update taste from a kept/undone semantic move."""
106
+ now = int(time.time() * 1000)
107
+
108
+ if family not in self.move_family_scores:
109
+ self.move_family_scores[family] = MoveFamilyScore(family=family)
110
+
111
+ fam = self.move_family_scores[family]
112
+ if kept:
113
+ fam.score = min(1.0, fam.score + 0.1)
114
+ fam.kept_count += 1
115
+ else:
116
+ fam.score = max(-1.0, fam.score - 0.12)
117
+ fam.undone_count += 1
118
+ fam.last_updated_ms = now
119
+
120
+ self.evidence_count += 1
121
+ self.last_updated_ms = now
122
+
123
+ def record_device_use(self, device_name: str, positive: bool = True) -> None:
124
+ """Update device affinity from usage."""
125
+ now = int(time.time() * 1000)
126
+
127
+ if device_name not in self.device_affinities:
128
+ self.device_affinities[device_name] = DeviceAffinity(
129
+ device_name=device_name
130
+ )
131
+
132
+ dev = self.device_affinities[device_name]
133
+ dev.use_count += 1
134
+ if positive:
135
+ dev.affinity = min(1.0, dev.affinity + 0.05)
136
+ else:
137
+ dev.affinity = max(-1.0, dev.affinity - 0.08)
138
+ dev.last_used_ms = now
139
+
140
+ self.evidence_count += 1
141
+ self.last_updated_ms = now
142
+
143
+ def update_novelty_from_experiment(self, chose_bold: bool) -> None:
144
+ """Shift novelty band based on experiment choices."""
145
+ if chose_bold:
146
+ self.novelty_band = min(1.0, self.novelty_band + 0.05)
147
+ else:
148
+ self.novelty_band = max(0.0, self.novelty_band - 0.05)
149
+
150
+ # ── Ranking ──────────────────────────────────────────────────────
151
+
152
+ def rank_moves(self, move_specs: list[dict]) -> list[dict]:
153
+ """Rank a list of semantic move dicts by taste fit.
154
+
155
+ Each move dict should have: move_id, family, targets, risk_level.
156
+ Returns the same dicts with added 'taste_score' field, sorted desc.
157
+ """
158
+ ranked = []
159
+ for move in move_specs:
160
+ taste_score = 0.5 # Neutral baseline
161
+
162
+ # Family preference
163
+ family = move.get("family", "")
164
+ fam_score = self.move_family_scores.get(family)
165
+ if fam_score:
166
+ taste_score += fam_score.score * 0.3
167
+
168
+ # Dimension alignment
169
+ targets = move.get("targets", {})
170
+ for dim, weight in targets.items():
171
+ dim_pref = self.dimension_weights.get(dim, 0.0)
172
+ taste_score += dim_pref * weight * 0.2
173
+
174
+ # Anti-preference penalty
175
+ for dim in targets:
176
+ if dim in self.dimension_avoidances:
177
+ taste_score -= 0.3
178
+
179
+ # Novelty/risk alignment
180
+ risk = move.get("risk_level", "low")
181
+ risk_val = {"low": 0.2, "medium": 0.5, "high": 0.8}.get(risk, 0.5)
182
+ novelty_match = 1.0 - abs(risk_val - self.novelty_band)
183
+ taste_score += novelty_match * 0.1
184
+
185
+ # Clamp
186
+ taste_score = max(0.0, min(1.0, taste_score))
187
+
188
+ result = dict(move)
189
+ result["taste_score"] = round(taste_score, 3)
190
+ ranked.append(result)
191
+
192
+ ranked.sort(key=lambda x: -x["taste_score"])
193
+ return ranked
194
+
195
+ def explain(self) -> dict:
196
+ """Generate a human-readable explanation of taste inferences."""
197
+ explanations = []
198
+
199
+ # Top move families
200
+ top_families = sorted(
201
+ self.move_family_scores.values(),
202
+ key=lambda f: -f.score,
203
+ )[:3]
204
+ for fam in top_families:
205
+ if fam.score > 0.1:
206
+ explanations.append(
207
+ f"Prefers {fam.family} moves (score {fam.score:.2f}, "
208
+ f"{fam.kept_count} kept, {fam.undone_count} undone)"
209
+ )
210
+ elif fam.score < -0.1:
211
+ explanations.append(
212
+ f"Tends to reject {fam.family} moves (score {fam.score:.2f})"
213
+ )
214
+
215
+ # Novelty
216
+ if self.novelty_band > 0.65:
217
+ explanations.append("Prefers experimental/bold approaches")
218
+ elif self.novelty_band < 0.35:
219
+ explanations.append("Prefers conservative/safe approaches")
220
+
221
+ # Top devices
222
+ top_devs = sorted(
223
+ self.device_affinities.values(),
224
+ key=lambda d: -d.affinity,
225
+ )[:3]
226
+ for dev in top_devs:
227
+ if dev.affinity > 0.1 and dev.use_count >= 2:
228
+ explanations.append(
229
+ f"Likes {dev.device_name} (used {dev.use_count}x)"
230
+ )
231
+
232
+ # Avoidances
233
+ for dim, direction in self.dimension_avoidances.items():
234
+ explanations.append(f"Avoids {direction} {dim}")
235
+
236
+ return {
237
+ "evidence_count": self.evidence_count,
238
+ "novelty_band": round(self.novelty_band, 3),
239
+ "explanations": explanations,
240
+ }
241
+
242
+
243
+ # ── Builder ──────────────────────────────────────────────────────────────────
244
+
245
+ def build_taste_graph(
246
+ taste_store=None, # TasteMemoryStore
247
+ anti_store=None, # AntiMemoryStore
248
+ ) -> TasteGraph:
249
+ """Build a TasteGraph from existing memory stores."""
250
+ graph = TasteGraph()
251
+
252
+ if taste_store:
253
+ for dim in taste_store.get_taste_dimensions():
254
+ if dim.evidence_count > 0:
255
+ graph.dimension_weights[dim.name] = dim.value
256
+
257
+ if anti_store:
258
+ for pref in anti_store.get_anti_preferences():
259
+ graph.dimension_avoidances[pref.dimension] = pref.direction
260
+
261
+ return graph
@@ -110,3 +110,91 @@ def get_taste_dimensions(ctx: Context) -> dict:
110
110
  """Return all taste dimensions — user preferences inferred from kept/undone outcomes."""
111
111
  store = _get_taste_memory(ctx)
112
112
  return store.to_dict()
113
+
114
+
115
+ # ── Taste Graph (V2) ────────────────────────────────────────────────
116
+
117
+
118
+ @mcp.tool()
119
+ def get_taste_graph(ctx: Context) -> dict:
120
+ """Get the full TasteGraph — extended preferences including move families,
121
+ device affinities, novelty tolerance, and dimension weights.
122
+
123
+ The TasteGraph combines taste dimensions, anti-preferences, and
124
+ move/device tracking into a single model for personalized ranking.
125
+ """
126
+ from .taste_graph import build_taste_graph
127
+
128
+ taste_store = _get_taste_memory(ctx)
129
+ anti_store = _get_anti_memory(ctx)
130
+ graph = build_taste_graph(taste_store=taste_store, anti_store=anti_store)
131
+ return graph.to_dict()
132
+
133
+
134
+ @mcp.tool()
135
+ def explain_taste_inference(ctx: Context) -> dict:
136
+ """Explain why the system thinks the user prefers certain approaches.
137
+
138
+ Returns human-readable explanations of inferred taste based on
139
+ evidence from kept moves, undone moves, device usage, and anti-preferences.
140
+ """
141
+ from .taste_graph import build_taste_graph
142
+
143
+ taste_store = _get_taste_memory(ctx)
144
+ anti_store = _get_anti_memory(ctx)
145
+ graph = build_taste_graph(taste_store=taste_store, anti_store=anti_store)
146
+ return graph.explain()
147
+
148
+
149
+ @mcp.tool()
150
+ def rank_moves_by_taste(
151
+ ctx: Context,
152
+ move_specs: list,
153
+ ) -> dict:
154
+ """Rank semantic moves by taste fit for the current user.
155
+
156
+ move_specs: list of dicts with {move_id, family, targets, risk_level}
157
+ Returns: the same moves sorted by taste_score (descending).
158
+
159
+ Use this after propose_next_best_move to personalize the ranking.
160
+ """
161
+ from .taste_graph import build_taste_graph
162
+
163
+ taste_store = _get_taste_memory(ctx)
164
+ anti_store = _get_anti_memory(ctx)
165
+ graph = build_taste_graph(taste_store=taste_store, anti_store=anti_store)
166
+
167
+ if isinstance(move_specs, str):
168
+ import json
169
+ move_specs = json.loads(move_specs)
170
+
171
+ ranked = graph.rank_moves(move_specs)
172
+ return {"ranked_moves": ranked, "count": len(ranked)}
173
+
174
+
175
+ @mcp.tool()
176
+ def record_positive_preference(
177
+ ctx: Context,
178
+ dimension: str,
179
+ direction: str,
180
+ evidence: str = "",
181
+ ) -> dict:
182
+ """Record a user preference for more/less of a dimension.
183
+
184
+ dimension: quality axis (e.g., "warmth", "width", "punch")
185
+ direction: "increase" or "decrease"
186
+ evidence: optional note about what triggered this preference
187
+
188
+ Complements record_anti_preference — this records what users LIKE,
189
+ not just what they dislike.
190
+ """
191
+ taste_store = _get_taste_memory(ctx)
192
+ # Map to outcome signal
193
+ signal = f"{dimension}_{direction}_kept"
194
+ taste_store.update_from_outcome({"signal": signal})
195
+ return {
196
+ "recorded": True,
197
+ "dimension": dimension,
198
+ "direction": direction,
199
+ "evidence": evidence,
200
+ }
@@ -148,7 +148,7 @@ def run_dynamics_critic(dynamics: DynamicsState) -> list[MixIssue]:
148
148
  recommended_moves=["transient_shaping", "gain_staging"],
149
149
  ))
150
150
 
151
- if dynamics.headroom < 1.0:
151
+ if dynamics.headroom is not None and dynamics.headroom < 1.0:
152
152
  issues.append(MixIssue(
153
153
  issue_type="low_headroom",
154
154
  critic="dynamics",
@@ -266,7 +266,7 @@ def run_translation_critic(
266
266
  ))
267
267
 
268
268
  # Harshness risk: over-compressed + low headroom
269
- if dynamics.over_compressed and dynamics.headroom < 3.0:
269
+ if dynamics.over_compressed and dynamics.headroom is not None and dynamics.headroom < 3.0:
270
270
  issues.append(MixIssue(
271
271
  issue_type="harshness_risk",
272
272
  critic="translation",
@@ -93,7 +93,7 @@ class DynamicsState:
93
93
 
94
94
  crest_factor_db: float = 0.0
95
95
  over_compressed: bool = False
96
- headroom: float = 0.0
96
+ headroom: Optional[float] = None
97
97
 
98
98
  def to_dict(self) -> dict:
99
99
  return asdict(self)
@@ -170,7 +170,7 @@ def build_dynamics_state(
170
170
  peak: master peak level in linear (0-1) or dB.
171
171
  """
172
172
  if rms is None or peak is None or rms <= 0:
173
- return DynamicsState(crest_factor_db=0.0, over_compressed=False, headroom=0.0)
173
+ return DynamicsState(crest_factor_db=0.0, over_compressed=False, headroom=None)
174
174
 
175
175
  # If values look like they're in dB (negative), convert to linear
176
176
  if rms < 0:
@@ -181,7 +181,7 @@ def build_dynamics_state(
181
181
  peak_linear = peak if peak else rms
182
182
 
183
183
  if rms_linear <= 0:
184
- return DynamicsState(crest_factor_db=0.0, over_compressed=False, headroom=0.0)
184
+ return DynamicsState(crest_factor_db=0.0, over_compressed=False, headroom=None)
185
185
 
186
186
  crest = 20 * math.log10(max(peak_linear, 1e-10) / max(rms_linear, 1e-10))
187
187
 
@@ -0,0 +1,8 @@
1
+ """Musical intelligence — pure computation modules for song-level analysis.
2
+
3
+ Detects structural and compositional issues that parameter-level analysis misses:
4
+ - Repetition fatigue: when patterns have been heard too many times
5
+ - Role conflicts: when multiple tracks compete for the same musical role
6
+ - Section purpose: infers what each section is trying to do musically
7
+ - Emotional arc: scores the tension/release curve across the arrangement
8
+ """