livepilot 1.12.2 → 1.14.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 (37) hide show
  1. package/CHANGELOG.md +219 -0
  2. package/README.md +7 -7
  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/branches/__init__.py +34 -0
  7. package/mcp_server/branches/types.py +286 -0
  8. package/mcp_server/composer/__init__.py +10 -1
  9. package/mcp_server/composer/branch_producer.py +349 -0
  10. package/mcp_server/composer/tools.py +58 -1
  11. package/mcp_server/evaluation/policy.py +227 -2
  12. package/mcp_server/experiment/engine.py +47 -11
  13. package/mcp_server/experiment/models.py +112 -8
  14. package/mcp_server/experiment/tools.py +502 -38
  15. package/mcp_server/memory/taste_graph.py +84 -11
  16. package/mcp_server/persistence/taste_store.py +21 -5
  17. package/mcp_server/runtime/session_kernel.py +46 -0
  18. package/mcp_server/runtime/tools.py +29 -3
  19. package/mcp_server/server.py +1 -0
  20. package/mcp_server/synthesis_brain/__init__.py +53 -0
  21. package/mcp_server/synthesis_brain/adapters/__init__.py +34 -0
  22. package/mcp_server/synthesis_brain/adapters/analog.py +273 -0
  23. package/mcp_server/synthesis_brain/adapters/base.py +86 -0
  24. package/mcp_server/synthesis_brain/adapters/drift.py +271 -0
  25. package/mcp_server/synthesis_brain/adapters/meld.py +261 -0
  26. package/mcp_server/synthesis_brain/adapters/operator.py +292 -0
  27. package/mcp_server/synthesis_brain/adapters/wavetable.py +364 -0
  28. package/mcp_server/synthesis_brain/engine.py +91 -0
  29. package/mcp_server/synthesis_brain/models.py +121 -0
  30. package/mcp_server/synthesis_brain/timbre.py +194 -0
  31. package/mcp_server/synthesis_brain/tools.py +231 -0
  32. package/mcp_server/tools/_conductor.py +144 -0
  33. package/mcp_server/wonder_mode/engine.py +324 -0
  34. package/mcp_server/wonder_mode/tools.py +153 -1
  35. package/package.json +2 -2
  36. package/remote_script/LivePilot/__init__.py +1 -1
  37. package/server.json +3 -3
@@ -0,0 +1,261 @@
1
+ """Meld adapter — Ableton 12's newest FM/granular hybrid.
2
+
3
+ Meld pairs two "Engines" with per-engine algorithms and a shared
4
+ modulation / amp / filter section. PR10 ships one canned proposer:
5
+ engine_algo_swap — changes Engine 1's algorithm to produce a
6
+ materially different core timbre without disturbing the envelope
7
+ or filter. Later PRs add engine-blend, unison, and modulation-matrix
8
+ variants.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ from typing import Optional
15
+
16
+ from ...branches import BranchSeed, freeform_seed
17
+ from ..models import (
18
+ SynthProfile,
19
+ TimbralFingerprint,
20
+ ModulationGraph,
21
+ ArticulationProfile,
22
+ NATIVE,
23
+ )
24
+ from .base import register_adapter
25
+
26
+
27
+ _KNOWN_PARAMS = {
28
+ "Engine 1 Algorithm",
29
+ "Engine 2 Algorithm",
30
+ "Engine 1 Level",
31
+ "Engine 2 Level",
32
+ "Engine 1 Morph",
33
+ "Engine 2 Morph",
34
+ "Filter Freq",
35
+ "Filter Res",
36
+ "Amp A",
37
+ "Amp D",
38
+ "Amp S",
39
+ "Amp R",
40
+ }
41
+
42
+
43
+ @register_adapter
44
+ class MeldAdapter:
45
+ device_name: str = "Meld"
46
+
47
+ def extract_profile(
48
+ self,
49
+ track_index: int,
50
+ device_index: int,
51
+ parameter_state: dict,
52
+ display_values: Optional[dict] = None,
53
+ role_hint: str = "",
54
+ ) -> SynthProfile:
55
+ notes: list[str] = []
56
+
57
+ e1_algo = parameter_state.get("Engine 1 Algorithm")
58
+ e2_algo = parameter_state.get("Engine 2 Algorithm")
59
+ if e1_algo is not None and e2_algo is not None and e1_algo == e2_algo:
60
+ notes.append(
61
+ "Both Engines on same algorithm — consider differentiating for depth"
62
+ )
63
+
64
+ articulation = ArticulationProfile(
65
+ attack_ms=float(parameter_state.get("Amp A", 0.0) or 0.0),
66
+ release_ms=float(parameter_state.get("Amp R", 0.0) or 0.0),
67
+ )
68
+
69
+ mod = ModulationGraph()
70
+ # Meld has many internal mod routes; PR10 just records engine levels
71
+ # as rough "sources" so downstream can see the mix balance.
72
+ e1_level = parameter_state.get("Engine 1 Level", 0.0)
73
+ e2_level = parameter_state.get("Engine 2 Level", 0.0)
74
+ if e1_level and e1_level > 0:
75
+ mod.routes.append({"source": "Engine 1", "target": "output", "amount": e1_level})
76
+ if e2_level and e2_level > 0:
77
+ mod.routes.append({"source": "Engine 2", "target": "output", "amount": e2_level})
78
+
79
+ focused_state = {k: v for k, v in parameter_state.items() if k in _KNOWN_PARAMS}
80
+ focused_display = (
81
+ {k: v for k, v in (display_values or {}).items() if k in _KNOWN_PARAMS}
82
+ if display_values else {}
83
+ )
84
+
85
+ return SynthProfile(
86
+ device_name=self.device_name,
87
+ opacity=NATIVE,
88
+ track_index=track_index,
89
+ device_index=device_index,
90
+ parameter_state=focused_state,
91
+ display_values=focused_display,
92
+ role_hint=role_hint,
93
+ modulation=mod,
94
+ articulation=articulation,
95
+ notes=notes,
96
+ )
97
+
98
+ def propose_branches(
99
+ self,
100
+ profile: SynthProfile,
101
+ target: TimbralFingerprint,
102
+ kernel: Optional[dict] = None,
103
+ ) -> list[tuple[BranchSeed, dict]]:
104
+ kernel = kernel or {}
105
+ results: list[tuple[BranchSeed, dict]] = []
106
+ for strategy_fn in (_strategy_engine_algo_swap, _strategy_engine_mix_shift):
107
+ try:
108
+ maybe = strategy_fn(profile, target, kernel, adapter=self)
109
+ except Exception:
110
+ continue
111
+ if maybe is not None:
112
+ results.append(maybe)
113
+ return results
114
+
115
+
116
+ # ── Strategy registry ────────────────────────────────────────────────
117
+
118
+
119
+ def _strategy_engine_algo_swap(
120
+ profile: SynthProfile,
121
+ target: TimbralFingerprint,
122
+ kernel: dict,
123
+ adapter,
124
+ ) -> Optional[tuple[BranchSeed, dict]]:
125
+ """Shift Engine 1 Algorithm by +1 (low freshness) or +3 (high).
126
+
127
+ Always applicable — algorithm swaps are guaranteed to change tone
128
+ regardless of current state.
129
+ """
130
+ freshness = float(kernel.get("freshness", 0.5) or 0.5)
131
+ track = profile.track_index
132
+ device = profile.device_index
133
+ current_algo = int(profile.parameter_state.get("Engine 1 Algorithm", 0) or 0)
134
+ shift = 1 if freshness < 0.5 else 3
135
+ new_algo = (current_algo + shift) % 10
136
+
137
+ seed = freeform_seed(
138
+ seed_id=_short_id("ml_algo", f"{track}:{device}:{new_algo}"),
139
+ hypothesis=(
140
+ f"Meld Engine 1 algorithm swap: {current_algo} → {new_algo} "
141
+ f"for a materially different core timbre"
142
+ ),
143
+ source="synthesis",
144
+ novelty_label="unexpected" if shift == 3 else "strong",
145
+ risk_label="medium",
146
+ affected_scope={
147
+ "track_indices": [track],
148
+ "device_paths": [f"track/{track}/device/{device}"],
149
+ },
150
+ distinctness_reason="changes Engine 1 algorithm",
151
+ producer_payload={
152
+ "device_name": adapter.device_name,
153
+ "track_index": track,
154
+ "device_index": device,
155
+ "strategy": "engine_algo_swap",
156
+ "topology_hint": {
157
+ "role_hint": profile.role_hint,
158
+ "current_algo": current_algo,
159
+ "new_algo": new_algo,
160
+ },
161
+ },
162
+ )
163
+ plan = {
164
+ "steps": [
165
+ {"tool": "set_device_parameter",
166
+ "params": {"track_index": track, "device_index": device,
167
+ "parameter_name": "Engine 1 Algorithm",
168
+ "value": new_algo}},
169
+ ],
170
+ "step_count": 1,
171
+ "summary": f"Engine 1 Algorithm {current_algo} → {new_algo}",
172
+ }
173
+ return (seed, plan)
174
+
175
+
176
+ def _strategy_engine_mix_shift(
177
+ profile: SynthProfile,
178
+ target: TimbralFingerprint,
179
+ kernel: dict,
180
+ adapter,
181
+ ) -> Optional[tuple[BranchSeed, dict]]:
182
+ """Rebalance Engine 1 / Engine 2 Level for layered character.
183
+
184
+ Gates: applicable when BOTH engines have non-zero Level (mix makes
185
+ no sense if one engine is silent). Shifts the balance by 0.15-0.3
186
+ depending on freshness.
187
+ """
188
+ e1 = float(profile.parameter_state.get("Engine 1 Level", 0.0) or 0.0)
189
+ e2 = float(profile.parameter_state.get("Engine 2 Level", 0.0) or 0.0)
190
+ if e1 < 0.05 or e2 < 0.05:
191
+ return None # one engine silent — mix shift is meaningless
192
+
193
+ freshness = float(kernel.get("freshness", 0.5) or 0.5)
194
+ track = profile.track_index
195
+ device = profile.device_index
196
+
197
+ # Push toward the engine with LESS level currently — highlights the
198
+ # underused engine's character. When roughly equal, pick Engine 2.
199
+ if e1 < e2:
200
+ direction = "to_e1"
201
+ delta = 0.15 if freshness < 0.5 else 0.3
202
+ new_e1 = min(1.0, e1 + delta)
203
+ new_e2 = max(0.0, e2 - delta / 2)
204
+ else:
205
+ direction = "to_e2"
206
+ delta = 0.15 if freshness < 0.5 else 0.3
207
+ new_e2 = min(1.0, e2 + delta)
208
+ new_e1 = max(0.0, e1 - delta / 2)
209
+
210
+ seed = freeform_seed(
211
+ seed_id=_short_id(
212
+ "ml_mix", f"{track}:{device}:{direction}:{new_e1:.2f}:{new_e2:.2f}"
213
+ ),
214
+ hypothesis=(
215
+ f"Meld engine mix shift {direction}: E1 {e1:.2f} → {new_e1:.2f}, "
216
+ f"E2 {e2:.2f} → {new_e2:.2f}"
217
+ ),
218
+ source="synthesis",
219
+ novelty_label="strong",
220
+ risk_label="low",
221
+ affected_scope={
222
+ "track_indices": [track],
223
+ "device_paths": [f"track/{track}/device/{device}"],
224
+ },
225
+ distinctness_reason=(
226
+ f"Meld mix rebalance {direction}; algorithm unchanged"
227
+ ),
228
+ producer_payload={
229
+ "device_name": adapter.device_name,
230
+ "track_index": track,
231
+ "device_index": device,
232
+ "strategy": f"engine_mix_shift_{direction}",
233
+ "topology_hint": {
234
+ "role_hint": profile.role_hint,
235
+ "current_e1": e1,
236
+ "current_e2": e2,
237
+ "new_e1": round(new_e1, 3),
238
+ "new_e2": round(new_e2, 3),
239
+ },
240
+ },
241
+ )
242
+ plan = {
243
+ "steps": [
244
+ {"tool": "set_device_parameter",
245
+ "params": {"track_index": track, "device_index": device,
246
+ "parameter_name": "Engine 1 Level",
247
+ "value": round(new_e1, 3)}},
248
+ {"tool": "set_device_parameter",
249
+ "params": {"track_index": track, "device_index": device,
250
+ "parameter_name": "Engine 2 Level",
251
+ "value": round(new_e2, 3)}},
252
+ ],
253
+ "step_count": 2,
254
+ "summary": f"E1 {e1:.2f}→{new_e1:.2f}, E2 {e2:.2f}→{new_e2:.2f}",
255
+ }
256
+ return (seed, plan)
257
+
258
+
259
+ def _short_id(prefix: str, key: str) -> str:
260
+ h = hashlib.sha256(f"{prefix}:{key}".encode()).hexdigest()[:10]
261
+ return f"{prefix}_{h}"
@@ -0,0 +1,292 @@
1
+ """Operator adapter — native-synth-aware branch production for Ableton's Operator.
2
+
3
+ FM synthesis is defined by operator ratios + algorithm topology + per-op
4
+ envelopes. PR9 always targeted Oscillator B because modulator role was
5
+ unknown; PR2/v2 decodes Algorithm → which oscillators are actually
6
+ carriers vs modulators, and targets the modulator with the highest Level
7
+ so the ratio shift produces a real timbral change rather than changing
8
+ an inaudible operator.
9
+
10
+ Strategy: ratio_shift_<operator>. Each seed's producer_payload captures:
11
+ {schema_version, device_name, track_index, device_index,
12
+ strategy: "ratio_shift_<op>",
13
+ topology_hint: {algorithm, carriers, modulators, targeted_op,
14
+ current_coarse, new_coarse}}
15
+
16
+ so later render-verification can confirm the shift actually altered the
17
+ modulated carrier's spectrum.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import hashlib
23
+ from typing import Optional
24
+
25
+ from ...branches import BranchSeed, freeform_seed
26
+ from ..models import (
27
+ SynthProfile,
28
+ TimbralFingerprint,
29
+ ModulationGraph,
30
+ ArticulationProfile,
31
+ NATIVE,
32
+ )
33
+ from .base import register_adapter
34
+
35
+
36
+ _KNOWN_PARAMS = {
37
+ "Algorithm",
38
+ "Oscillator A Coarse",
39
+ "Oscillator B Coarse",
40
+ "Oscillator C Coarse",
41
+ "Oscillator D Coarse",
42
+ "Oscillator A Fine",
43
+ "Oscillator B Fine",
44
+ "Oscillator A Level",
45
+ "Oscillator B Level",
46
+ "Oscillator C Level",
47
+ "Oscillator D Level",
48
+ "Oscillator A Attack",
49
+ "Oscillator A Release",
50
+ "Filter Frequency",
51
+ "Filter Resonance",
52
+ "Time", # global envelope time
53
+ }
54
+
55
+
56
+ # Static topology table for Ableton Operator's 11 algorithms. Each entry
57
+ # names the ops that act as carriers (audible) and modulators (FM sources).
58
+ # Source: Ableton Operator manual, DX7-compatible topologies. The exact
59
+ # modulation routing within an algorithm (who modulates whom) matters less
60
+ # for adapter targeting than the carrier/modulator role — what we need to
61
+ # know is "which op's Coarse, when shifted, produces an audible timbral
62
+ # change?" Answer: any op acting as a modulator.
63
+ #
64
+ # Algorithm numbering follows Ableton's 0-based display order.
65
+ _ALGO_TOPOLOGY: dict[int, dict] = {
66
+ 0: {"carriers": ["D"], "modulators": ["A", "B", "C"]},
67
+ 1: {"carriers": ["B", "D"], "modulators": ["A", "C"]},
68
+ 2: {"carriers": ["B", "C", "D"], "modulators": ["A"]},
69
+ 3: {"carriers": ["D"], "modulators": ["A", "B", "C"]},
70
+ 4: {"carriers": ["C", "D"], "modulators": ["A", "B"]},
71
+ 5: {"carriers": ["A", "B", "C", "D"], "modulators": []},
72
+ 6: {"carriers": ["B", "D"], "modulators": ["A", "C"]},
73
+ 7: {"carriers": ["D"], "modulators": ["A", "B", "C"]},
74
+ 8: {"carriers": ["B", "C", "D"], "modulators": ["A"]},
75
+ 9: {"carriers": ["A", "B", "C", "D"], "modulators": []},
76
+ 10: {"carriers": ["B", "C", "D"], "modulators": ["A"]},
77
+ }
78
+
79
+
80
+ def _topology_for_algorithm(algorithm: int) -> dict:
81
+ """Look up carrier/modulator roles for an Operator algorithm index."""
82
+ # Default to algorithm 0 (classic serial chain) if unknown.
83
+ return _ALGO_TOPOLOGY.get(int(algorithm or 0), _ALGO_TOPOLOGY[0])
84
+
85
+
86
+ def _pick_target_modulator(
87
+ topology: dict,
88
+ parameter_state: dict,
89
+ ) -> Optional[str]:
90
+ """Pick the modulator with the highest Level — the best shift target.
91
+
92
+ Returns the operator letter ("A".."D") or None when the algorithm has
93
+ no modulators (purely additive algos 5 and 9). Level-based selection
94
+ ensures the Coarse shift produces an audible change; shifting an
95
+ op whose Level is 0 is a no-op.
96
+ """
97
+ candidates = []
98
+ for op in topology.get("modulators", []):
99
+ level_key = f"Oscillator {op} Level"
100
+ level = float(parameter_state.get(level_key, 0.0) or 0.0)
101
+ candidates.append((level, op))
102
+ if not candidates:
103
+ return None
104
+ candidates.sort(reverse=True) # highest level first
105
+ top_level, top_op = candidates[0]
106
+ # If every modulator is silent, still return the first one — the shift
107
+ # primes the patch for a future Level bump. Better than no branch.
108
+ return top_op
109
+
110
+
111
+ def _fallback_carrier_target(
112
+ topology: dict,
113
+ parameter_state: dict,
114
+ ) -> Optional[str]:
115
+ """Fallback when algorithm has no modulators (additive algos).
116
+
117
+ Picks the carrier with the highest Level; shifting its Coarse changes
118
+ the fundamental spectrum rather than FM depth.
119
+ """
120
+ candidates = []
121
+ for op in topology.get("carriers", []):
122
+ level_key = f"Oscillator {op} Level"
123
+ level = float(parameter_state.get(level_key, 0.0) or 0.0)
124
+ candidates.append((level, op))
125
+ if not candidates:
126
+ return None
127
+ candidates.sort(reverse=True)
128
+ return candidates[0][1]
129
+
130
+
131
+ @register_adapter
132
+ class OperatorAdapter:
133
+ """Adapter for Ableton's native Operator."""
134
+
135
+ device_name: str = "Operator"
136
+
137
+ def extract_profile(
138
+ self,
139
+ track_index: int,
140
+ device_index: int,
141
+ parameter_state: dict,
142
+ display_values: Optional[dict] = None,
143
+ role_hint: str = "",
144
+ ) -> SynthProfile:
145
+ notes: list[str] = []
146
+
147
+ algo = parameter_state.get("Algorithm", 0)
148
+ if algo is not None:
149
+ notes.append(f"Algorithm={algo} — topology governs which ops are carriers vs modulators")
150
+
151
+ # Crude modulator-detection: any oscillator with Coarse > 1 and Level > 0
152
+ # is acting as a modulator. Precise detection needs algorithm decoding,
153
+ # which lands in PR10.
154
+ mod_routes = []
155
+ for op in ("A", "B", "C", "D"):
156
+ coarse = parameter_state.get(f"Oscillator {op} Coarse", 1)
157
+ level = parameter_state.get(f"Oscillator {op} Level", 0)
158
+ if coarse and coarse > 1 and level and level > 0:
159
+ mod_routes.append({
160
+ "source": f"Oscillator {op}",
161
+ "target": "(per algorithm)",
162
+ "amount": level,
163
+ "range": None,
164
+ "coarse": coarse,
165
+ })
166
+ mod = ModulationGraph(routes=mod_routes)
167
+
168
+ articulation = ArticulationProfile(
169
+ attack_ms=float(parameter_state.get("Oscillator A Attack", 0.0) or 0.0),
170
+ release_ms=float(parameter_state.get("Oscillator A Release", 0.0) or 0.0),
171
+ )
172
+
173
+ focused_state = {k: v for k, v in parameter_state.items() if k in _KNOWN_PARAMS}
174
+ focused_display = (
175
+ {k: v for k, v in (display_values or {}).items() if k in _KNOWN_PARAMS}
176
+ if display_values
177
+ else {}
178
+ )
179
+
180
+ return SynthProfile(
181
+ device_name=self.device_name,
182
+ opacity=NATIVE,
183
+ track_index=track_index,
184
+ device_index=device_index,
185
+ parameter_state=focused_state,
186
+ display_values=focused_display,
187
+ role_hint=role_hint,
188
+ modulation=mod,
189
+ articulation=articulation,
190
+ notes=notes,
191
+ )
192
+
193
+ def propose_branches(
194
+ self,
195
+ profile: SynthProfile,
196
+ target: TimbralFingerprint,
197
+ kernel: Optional[dict] = None,
198
+ ) -> list[tuple[BranchSeed, dict]]:
199
+ kernel = kernel or {}
200
+ freshness = float(kernel.get("freshness", 0.5) or 0.5)
201
+ track = profile.track_index
202
+ device = profile.device_index
203
+
204
+ results: list[tuple[BranchSeed, dict]] = []
205
+
206
+ # ── Branch A: algorithm-aware ratio_shift ────────────────────
207
+ # Decode the current Algorithm, pick the real modulator (highest
208
+ # Level among the algorithm's modulator ops), and shift its
209
+ # Coarse. When the algorithm is purely additive (no modulators),
210
+ # fall back to shifting the dominant carrier — changes the
211
+ # fundamental spectrum instead of FM depth.
212
+ algorithm = int(profile.parameter_state.get("Algorithm", 0) or 0)
213
+ topology = _topology_for_algorithm(algorithm)
214
+ targeted_op = _pick_target_modulator(topology, profile.parameter_state)
215
+ target_role = "modulator"
216
+
217
+ if targeted_op is None:
218
+ targeted_op = _fallback_carrier_target(topology, profile.parameter_state)
219
+ target_role = "carrier"
220
+ if targeted_op is None:
221
+ # No usable operators — skip the branch rather than emit
222
+ # something that can't produce a timbral change.
223
+ return results
224
+
225
+ coarse_key = f"Oscillator {targeted_op} Coarse"
226
+ current_coarse = int(profile.parameter_state.get(coarse_key, 1) or 1)
227
+ step = 1 if freshness < 0.5 else 2
228
+ new_coarse = min(24, current_coarse + step)
229
+ if new_coarse == current_coarse:
230
+ new_coarse = max(1, current_coarse - step)
231
+
232
+ strategy = f"ratio_shift_{targeted_op}"
233
+ topology_hint = {
234
+ "algorithm": algorithm,
235
+ "carriers": list(topology.get("carriers", [])),
236
+ "modulators": list(topology.get("modulators", [])),
237
+ "targeted_op": targeted_op,
238
+ "target_role": target_role,
239
+ "current_coarse": current_coarse,
240
+ "new_coarse": new_coarse,
241
+ }
242
+
243
+ seed_a = freeform_seed(
244
+ seed_id=_short_id(
245
+ "op_ratio", f"{track}:{device}:{algorithm}:{targeted_op}:{new_coarse}"
246
+ ),
247
+ hypothesis=(
248
+ f"Shift Operator Osc {targeted_op} ({target_role}) Coarse "
249
+ f"{current_coarse} → {new_coarse} under algorithm {algorithm} "
250
+ f"for a {'subtle' if step == 1 else 'significant'} FM tone change"
251
+ ),
252
+ source="synthesis",
253
+ novelty_label="strong" if step == 1 else "unexpected",
254
+ risk_label="medium",
255
+ affected_scope={
256
+ "track_indices": [track],
257
+ "device_paths": [f"track/{track}/device/{device}"],
258
+ },
259
+ distinctness_reason=(
260
+ f"algorithm-{algorithm} aware shift on {target_role} Osc {targeted_op}"
261
+ ),
262
+ producer_payload={
263
+ "device_name": self.device_name,
264
+ "track_index": track,
265
+ "device_index": device,
266
+ "strategy": strategy,
267
+ "topology_hint": topology_hint,
268
+ },
269
+ )
270
+ plan_a = {
271
+ "steps": [
272
+ {
273
+ "tool": "set_device_parameter",
274
+ "params": {
275
+ "track_index": track,
276
+ "device_index": device,
277
+ "parameter_name": coarse_key,
278
+ "value": new_coarse,
279
+ },
280
+ },
281
+ ],
282
+ "step_count": 1,
283
+ "summary": f"Osc {targeted_op} Coarse {current_coarse} → {new_coarse} (algo {algorithm})",
284
+ }
285
+ results.append((seed_a, plan_a))
286
+
287
+ return results
288
+
289
+
290
+ def _short_id(prefix: str, key: str) -> str:
291
+ h = hashlib.sha256(f"{prefix}:{key}".encode()).hexdigest()[:10]
292
+ return f"{prefix}_{h}"