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.
- package/.claude-plugin/marketplace.json +3 -3
- package/.mcpbignore +40 -0
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +47 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +47 -72
- package/bin/livepilot.js +135 -0
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/agents/livepilot-producer/AGENT.md +13 -0
- package/livepilot/commands/arrange.md +42 -14
- package/livepilot/commands/beat.md +68 -21
- package/livepilot/commands/evaluate.md +23 -13
- package/livepilot/commands/mix.md +35 -11
- package/livepilot/commands/perform.md +31 -19
- package/livepilot/commands/sounddesign.md +38 -17
- package/livepilot/skills/livepilot-arrangement/SKILL.md +2 -1
- package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +60 -4
- package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +11 -11
- package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +25 -25
- package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +21 -21
- package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +13 -13
- package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +13 -13
- package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +5 -5
- package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +16 -16
- package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +40 -40
- package/livepilot/skills/livepilot-core/references/m4l-devices.md +3 -3
- package/livepilot/skills/livepilot-core/references/overview.md +4 -4
- package/livepilot/skills/livepilot-evaluation/SKILL.md +12 -8
- package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +2 -2
- package/livepilot/skills/livepilot-mix-engine/SKILL.md +1 -1
- package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +2 -2
- package/livepilot/skills/livepilot-mixing/SKILL.md +3 -1
- package/livepilot/skills/livepilot-notes/SKILL.md +2 -1
- package/livepilot/skills/livepilot-release/SKILL.md +15 -15
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +2 -2
- package/livepilot/skills/livepilot-wonder/SKILL.md +62 -0
- package/livepilot.mcpb +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/manifest.json +91 -0
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/creative_constraints/__init__.py +6 -0
- package/mcp_server/creative_constraints/engine.py +277 -0
- package/mcp_server/creative_constraints/models.py +75 -0
- package/mcp_server/creative_constraints/tools.py +341 -0
- package/mcp_server/experiment/__init__.py +6 -0
- package/mcp_server/experiment/engine.py +213 -0
- package/mcp_server/experiment/models.py +120 -0
- package/mcp_server/experiment/tools.py +263 -0
- package/mcp_server/hook_hunter/__init__.py +5 -0
- package/mcp_server/hook_hunter/analyzer.py +342 -0
- package/mcp_server/hook_hunter/models.py +57 -0
- package/mcp_server/hook_hunter/tools.py +586 -0
- package/mcp_server/memory/taste_graph.py +261 -0
- package/mcp_server/memory/tools.py +88 -0
- package/mcp_server/mix_engine/critics.py +2 -2
- package/mcp_server/mix_engine/models.py +1 -1
- package/mcp_server/mix_engine/state_builder.py +2 -2
- package/mcp_server/musical_intelligence/__init__.py +8 -0
- package/mcp_server/musical_intelligence/detectors.py +421 -0
- package/mcp_server/musical_intelligence/phrase_critic.py +163 -0
- package/mcp_server/musical_intelligence/tools.py +221 -0
- package/mcp_server/preview_studio/__init__.py +5 -0
- package/mcp_server/preview_studio/engine.py +280 -0
- package/mcp_server/preview_studio/models.py +73 -0
- package/mcp_server/preview_studio/tools.py +423 -0
- package/mcp_server/runtime/session_kernel.py +96 -0
- package/mcp_server/runtime/tools.py +90 -1
- package/mcp_server/semantic_moves/__init__.py +13 -0
- package/mcp_server/semantic_moves/compiler.py +116 -0
- package/mcp_server/semantic_moves/mix_compilers.py +291 -0
- package/mcp_server/semantic_moves/mix_moves.py +157 -0
- package/mcp_server/semantic_moves/models.py +46 -0
- package/mcp_server/semantic_moves/performance_compilers.py +208 -0
- package/mcp_server/semantic_moves/performance_moves.py +81 -0
- package/mcp_server/semantic_moves/registry.py +32 -0
- package/mcp_server/semantic_moves/resolvers.py +126 -0
- package/mcp_server/semantic_moves/sound_design_compilers.py +266 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +78 -0
- package/mcp_server/semantic_moves/tools.py +204 -0
- package/mcp_server/semantic_moves/transition_compilers.py +222 -0
- package/mcp_server/semantic_moves/transition_moves.py +76 -0
- package/mcp_server/server.py +10 -0
- package/mcp_server/session_continuity/__init__.py +6 -0
- package/mcp_server/session_continuity/models.py +86 -0
- package/mcp_server/session_continuity/tools.py +230 -0
- package/mcp_server/session_continuity/tracker.py +235 -0
- package/mcp_server/song_brain/__init__.py +6 -0
- package/mcp_server/song_brain/builder.py +477 -0
- package/mcp_server/song_brain/models.py +132 -0
- package/mcp_server/song_brain/tools.py +294 -0
- package/mcp_server/stuckness_detector/__init__.py +5 -0
- package/mcp_server/stuckness_detector/detector.py +400 -0
- package/mcp_server/stuckness_detector/models.py +66 -0
- package/mcp_server/stuckness_detector/tools.py +195 -0
- package/mcp_server/tools/_conductor.py +104 -6
- package/mcp_server/tools/analyzer.py +1 -1
- package/mcp_server/tools/devices.py +34 -0
- package/mcp_server/wonder_mode/__init__.py +6 -0
- package/mcp_server/wonder_mode/diagnosis.py +84 -0
- package/mcp_server/wonder_mode/engine.py +493 -0
- package/mcp_server/wonder_mode/session.py +114 -0
- package/mcp_server/wonder_mode/tools.py +285 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/browser.py +4 -1
- package/remote_script/LivePilot/devices.py +29 -0
- package/remote_script/LivePilot/tracks.py +11 -4
- 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",
|
|
@@ -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=
|
|
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=
|
|
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
|
+
"""
|