livepilot 1.9.23 → 1.10.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.
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +119 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +144 -13
- package/bin/livepilot.js +87 -0
- package/installer/codex.js +147 -0
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +21 -4
- package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
- package/livepilot/skills/livepilot-core/references/overview.md +13 -9
- package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
- package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
- package/livepilot/skills/livepilot-devices/SKILL.md +16 -2
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +19 -5
- package/livepilot/skills/livepilot-sample-engine/SKILL.md +104 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +45 -0
- package/livepilot/skills/livepilot-wonder/SKILL.md +15 -0
- package/livepilot.mcpb +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/manifest.json +2 -2
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +357 -0
- package/mcp_server/atlas/device_atlas.json +44067 -0
- package/mcp_server/atlas/enrichments/__init__.py +111 -0
- package/mcp_server/atlas/enrichments/audio_effects/auto_filter.yaml +162 -0
- package/mcp_server/atlas/enrichments/audio_effects/beat_repeat.yaml +183 -0
- package/mcp_server/atlas/enrichments/audio_effects/channel_eq.yaml +126 -0
- package/mcp_server/atlas/enrichments/audio_effects/chorus_ensemble.yaml +149 -0
- package/mcp_server/atlas/enrichments/audio_effects/color_limiter.yaml +109 -0
- package/mcp_server/atlas/enrichments/audio_effects/compressor.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb.yaml +143 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb_pro.yaml +178 -0
- package/mcp_server/atlas/enrichments/audio_effects/delay.yaml +151 -0
- package/mcp_server/atlas/enrichments/audio_effects/drum_buss.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/dynamic_tube.yaml +147 -0
- package/mcp_server/atlas/enrichments/audio_effects/echo.yaml +167 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_eight.yaml +148 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_three.yaml +121 -0
- package/mcp_server/atlas/enrichments/audio_effects/erosion.yaml +103 -0
- package/mcp_server/atlas/enrichments/audio_effects/filter_delay.yaml +173 -0
- package/mcp_server/atlas/enrichments/audio_effects/gate.yaml +130 -0
- package/mcp_server/atlas/enrichments/audio_effects/gated_delay.yaml +133 -0
- package/mcp_server/atlas/enrichments/audio_effects/glue_compressor.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/grain_delay.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/hybrid_reverb.yaml +160 -0
- package/mcp_server/atlas/enrichments/audio_effects/limiter.yaml +97 -0
- package/mcp_server/atlas/enrichments/audio_effects/multiband_dynamics.yaml +174 -0
- package/mcp_server/atlas/enrichments/audio_effects/overdrive.yaml +119 -0
- package/mcp_server/atlas/enrichments/audio_effects/pedal.yaml +145 -0
- package/mcp_server/atlas/enrichments/audio_effects/phaser_flanger.yaml +161 -0
- package/mcp_server/atlas/enrichments/audio_effects/redux.yaml +114 -0
- package/mcp_server/atlas/enrichments/audio_effects/reverb.yaml +190 -0
- package/mcp_server/atlas/enrichments/audio_effects/roar.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/saturator.yaml +146 -0
- package/mcp_server/atlas/enrichments/audio_effects/shifter.yaml +154 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_resonator.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_time.yaml +164 -0
- package/mcp_server/atlas/enrichments/audio_effects/vector_delay.yaml +140 -0
- package/mcp_server/atlas/enrichments/audio_effects/vinyl_distortion.yaml +141 -0
- package/mcp_server/atlas/enrichments/instruments/analog.yaml +222 -0
- package/mcp_server/atlas/enrichments/instruments/bass.yaml +202 -0
- package/mcp_server/atlas/enrichments/instruments/collision.yaml +150 -0
- package/mcp_server/atlas/enrichments/instruments/drift.yaml +167 -0
- package/mcp_server/atlas/enrichments/instruments/electric.yaml +137 -0
- package/mcp_server/atlas/enrichments/instruments/emit.yaml +163 -0
- package/mcp_server/atlas/enrichments/instruments/meld.yaml +164 -0
- package/mcp_server/atlas/enrichments/instruments/operator.yaml +197 -0
- package/mcp_server/atlas/enrichments/instruments/poli.yaml +192 -0
- package/mcp_server/atlas/enrichments/instruments/sampler.yaml +218 -0
- package/mcp_server/atlas/enrichments/instruments/simpler.yaml +217 -0
- package/mcp_server/atlas/enrichments/instruments/tension.yaml +156 -0
- package/mcp_server/atlas/enrichments/instruments/tree_tone.yaml +162 -0
- package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +165 -0
- package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +166 -0
- package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +162 -0
- package/mcp_server/atlas/enrichments/midi_effects/arpeggiator.yaml +156 -0
- package/mcp_server/atlas/enrichments/midi_effects/bouncy_notes.yaml +93 -0
- package/mcp_server/atlas/enrichments/midi_effects/chord.yaml +147 -0
- package/mcp_server/atlas/enrichments/midi_effects/melodic_steps.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_echo.yaml +108 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_length.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/pitch.yaml +76 -0
- package/mcp_server/atlas/enrichments/midi_effects/random.yaml +117 -0
- package/mcp_server/atlas/enrichments/midi_effects/rhythmic_steps.yaml +103 -0
- package/mcp_server/atlas/enrichments/midi_effects/scale.yaml +83 -0
- package/mcp_server/atlas/enrichments/midi_effects/step_arp.yaml +112 -0
- package/mcp_server/atlas/enrichments/midi_effects/velocity.yaml +119 -0
- package/mcp_server/atlas/enrichments/utility/amp.yaml +159 -0
- package/mcp_server/atlas/enrichments/utility/cabinet.yaml +109 -0
- package/mcp_server/atlas/enrichments/utility/corpus.yaml +150 -0
- package/mcp_server/atlas/enrichments/utility/resonators.yaml +131 -0
- package/mcp_server/atlas/enrichments/utility/spectrum.yaml +63 -0
- package/mcp_server/atlas/enrichments/utility/tuner.yaml +51 -0
- package/mcp_server/atlas/enrichments/utility/utility.yaml +136 -0
- package/mcp_server/atlas/enrichments/utility/vocoder.yaml +160 -0
- package/mcp_server/atlas/scanner.py +236 -0
- package/mcp_server/atlas/tools.py +224 -0
- package/mcp_server/composer/__init__.py +1 -0
- package/mcp_server/composer/engine.py +452 -0
- package/mcp_server/composer/layer_planner.py +427 -0
- package/mcp_server/composer/prompt_parser.py +329 -0
- package/mcp_server/composer/tools.py +201 -0
- package/mcp_server/connection.py +53 -8
- package/mcp_server/corpus/__init__.py +377 -0
- package/mcp_server/device_forge/__init__.py +1 -0
- package/mcp_server/device_forge/builder.py +377 -0
- package/mcp_server/device_forge/models.py +142 -0
- package/mcp_server/device_forge/templates.py +483 -0
- package/mcp_server/device_forge/tools.py +162 -0
- package/mcp_server/hook_hunter/analyzer.py +23 -0
- package/mcp_server/hook_hunter/models.py +1 -0
- package/mcp_server/hook_hunter/tools.py +4 -2
- package/mcp_server/m4l_bridge.py +1 -0
- package/mcp_server/memory/taste_graph.py +68 -1
- package/mcp_server/memory/tools.py +15 -4
- package/mcp_server/musical_intelligence/detectors.py +14 -1
- package/mcp_server/musical_intelligence/tools.py +11 -8
- package/mcp_server/persistence/__init__.py +1 -0
- package/mcp_server/persistence/base_store.py +82 -0
- package/mcp_server/persistence/project_store.py +106 -0
- package/mcp_server/persistence/taste_store.py +122 -0
- package/mcp_server/preview_studio/models.py +1 -0
- package/mcp_server/preview_studio/tools.py +56 -13
- package/mcp_server/runtime/capability.py +66 -0
- package/mcp_server/runtime/capability_probe.py +137 -0
- package/mcp_server/runtime/execution_router.py +143 -0
- package/mcp_server/runtime/live_version.py +102 -0
- package/mcp_server/runtime/remote_commands.py +87 -0
- package/mcp_server/runtime/tools.py +18 -4
- package/mcp_server/sample_engine/__init__.py +1 -0
- package/mcp_server/sample_engine/analyzer.py +216 -0
- package/mcp_server/sample_engine/critics.py +390 -0
- package/mcp_server/sample_engine/models.py +193 -0
- package/mcp_server/sample_engine/moves.py +127 -0
- package/mcp_server/sample_engine/planner.py +186 -0
- package/mcp_server/sample_engine/sources.py +540 -0
- package/mcp_server/sample_engine/techniques.py +908 -0
- package/mcp_server/sample_engine/tools.py +442 -0
- package/mcp_server/semantic_moves/__init__.py +3 -0
- package/mcp_server/semantic_moves/device_creation_moves.py +237 -0
- package/mcp_server/semantic_moves/mix_moves.py +41 -41
- package/mcp_server/semantic_moves/performance_moves.py +13 -13
- package/mcp_server/semantic_moves/sample_compilers.py +372 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +15 -15
- package/mcp_server/semantic_moves/tools.py +18 -17
- package/mcp_server/semantic_moves/transition_moves.py +16 -16
- package/mcp_server/server.py +51 -0
- package/mcp_server/services/__init__.py +1 -0
- package/mcp_server/services/motif_service.py +67 -0
- package/mcp_server/session_continuity/tracker.py +29 -1
- package/mcp_server/song_brain/builder.py +28 -1
- package/mcp_server/song_brain/models.py +4 -0
- package/mcp_server/song_brain/tools.py +20 -2
- package/mcp_server/sound_design/critics.py +89 -1
- package/mcp_server/splice_client/__init__.py +1 -0
- package/mcp_server/splice_client/client.py +347 -0
- package/mcp_server/splice_client/models.py +96 -0
- package/mcp_server/splice_client/protos/__init__.py +1 -0
- package/mcp_server/splice_client/protos/app_pb2.py +319 -0
- package/mcp_server/splice_client/protos/app_pb2.pyi +1153 -0
- package/mcp_server/splice_client/protos/app_pb2_grpc.py +1946 -0
- package/mcp_server/tools/arrangement.py +69 -0
- package/mcp_server/tools/automation.py +15 -2
- package/mcp_server/tools/devices.py +117 -6
- package/mcp_server/tools/notes.py +37 -4
- package/mcp_server/wonder_mode/diagnosis.py +5 -0
- package/mcp_server/wonder_mode/engine.py +85 -1
- package/mcp_server/wonder_mode/tools.py +6 -1
- package/package.json +12 -2
- package/remote_script/LivePilot/__init__.py +8 -1
- package/remote_script/LivePilot/arrangement.py +114 -0
- package/remote_script/LivePilot/browser.py +56 -1
- package/remote_script/LivePilot/devices.py +236 -6
- package/remote_script/LivePilot/mixing.py +8 -3
- package/remote_script/LivePilot/server.py +5 -1
- package/remote_script/LivePilot/transport.py +3 -0
- package/remote_script/LivePilot/version_detect.py +78 -0
- package/scripts/sync_metadata.py +132 -0
|
@@ -14,12 +14,12 @@ ADD_WARMTH = SemanticMove(
|
|
|
14
14
|
protect={"clarity": 0.6, "punch": 0.5},
|
|
15
15
|
risk_level="low",
|
|
16
16
|
compile_plan=[
|
|
17
|
-
{"tool": "set_device_parameter", "params": {"description": "Add Saturator drive +2-4dB for harmonic warmth"}, "description": "Add saturation"},
|
|
18
|
-
{"tool": "set_device_parameter", "params": {"description": "Boost EQ low-mid shelf +1-2dB"}, "description": "Low-mid warmth"},
|
|
17
|
+
{"tool": "set_device_parameter", "params": {"description": "Add Saturator drive +2-4dB for harmonic warmth"}, "description": "Add saturation", "backend": "remote_command"},
|
|
18
|
+
{"tool": "set_device_parameter", "params": {"description": "Boost EQ low-mid shelf +1-2dB"}, "description": "Low-mid warmth", "backend": "remote_command"},
|
|
19
19
|
],
|
|
20
20
|
verification_plan=[
|
|
21
|
-
{"tool": "get_master_spectrum", "check": "low-mid energy increased, high-mid stable"},
|
|
22
|
-
{"tool": "get_track_meters", "check": "target track producing audio"},
|
|
21
|
+
{"tool": "get_master_spectrum", "check": "low-mid energy increased, high-mid stable", "backend": "mcp_tool"},
|
|
22
|
+
{"tool": "get_track_meters", "check": "target track producing audio", "backend": "remote_command"},
|
|
23
23
|
],
|
|
24
24
|
)
|
|
25
25
|
|
|
@@ -31,11 +31,11 @@ ADD_TEXTURE = SemanticMove(
|
|
|
31
31
|
protect={"clarity": 0.6},
|
|
32
32
|
risk_level="medium",
|
|
33
33
|
compile_plan=[
|
|
34
|
-
{"tool": "apply_automation_shape", "params": {"curve_type": "perlin", "description": "Perlin noise on filter cutoff for organic texture"}, "description": "Organic filter motion"},
|
|
35
|
-
{"tool": "set_track_send", "params": {"description": "Increase delay send for spatial texture"}, "description": "Spatial texture via delay"},
|
|
34
|
+
{"tool": "apply_automation_shape", "params": {"curve_type": "perlin", "description": "Perlin noise on filter cutoff for organic texture"}, "description": "Organic filter motion", "backend": "mcp_tool"},
|
|
35
|
+
{"tool": "set_track_send", "params": {"description": "Increase delay send for spatial texture"}, "description": "Spatial texture via delay", "backend": "remote_command"},
|
|
36
36
|
],
|
|
37
37
|
verification_plan=[
|
|
38
|
-
{"tool": "get_track_meters", "check": "track producing audio with variation"},
|
|
38
|
+
{"tool": "get_track_meters", "check": "track producing audio with variation", "backend": "remote_command"},
|
|
39
39
|
],
|
|
40
40
|
)
|
|
41
41
|
|
|
@@ -47,11 +47,11 @@ SHAPE_TRANSIENTS = SemanticMove(
|
|
|
47
47
|
protect={"warmth": 0.5},
|
|
48
48
|
risk_level="low",
|
|
49
49
|
compile_plan=[
|
|
50
|
-
{"tool": "set_device_parameter", "params": {"description": "Adjust Compressor attack time (faster = sharper transients, slower = rounder)"}, "description": "Shape attack"},
|
|
51
|
-
{"tool": "set_device_parameter", "params": {"description": "Adjust Compressor release for rhythmic pumping"}, "description": "Shape release"},
|
|
50
|
+
{"tool": "set_device_parameter", "params": {"description": "Adjust Compressor attack time (faster = sharper transients, slower = rounder)"}, "description": "Shape attack", "backend": "remote_command"},
|
|
51
|
+
{"tool": "set_device_parameter", "params": {"description": "Adjust Compressor release for rhythmic pumping"}, "description": "Shape release", "backend": "remote_command"},
|
|
52
52
|
],
|
|
53
53
|
verification_plan=[
|
|
54
|
-
{"tool": "get_track_meters", "check": "track producing audio with expected transient character"},
|
|
54
|
+
{"tool": "get_track_meters", "check": "track producing audio with expected transient character", "backend": "remote_command"},
|
|
55
55
|
],
|
|
56
56
|
)
|
|
57
57
|
|
|
@@ -63,13 +63,13 @@ ADD_SPACE = SemanticMove(
|
|
|
63
63
|
protect={"punch": 0.6, "clarity": 0.5},
|
|
64
64
|
risk_level="low",
|
|
65
65
|
compile_plan=[
|
|
66
|
-
{"tool": "set_track_send", "params": {"description": "Increase reverb send to 25-35%"}, "description": "Add reverb depth"},
|
|
67
|
-
{"tool": "set_track_send", "params": {"description": "Add subtle delay send 10-15%"}, "description": "Add delay texture"},
|
|
68
|
-
{"tool": "set_track_pan", "params": {"description": "Widen pan slightly for spatial presence"}, "description": "Widen spatial field"},
|
|
66
|
+
{"tool": "set_track_send", "params": {"description": "Increase reverb send to 25-35%"}, "description": "Add reverb depth", "backend": "remote_command"},
|
|
67
|
+
{"tool": "set_track_send", "params": {"description": "Add subtle delay send 10-15%"}, "description": "Add delay texture", "backend": "remote_command"},
|
|
68
|
+
{"tool": "set_track_pan", "params": {"description": "Widen pan slightly for spatial presence"}, "description": "Widen spatial field", "backend": "remote_command"},
|
|
69
69
|
],
|
|
70
70
|
verification_plan=[
|
|
71
|
-
{"tool": "get_track_meters", "check": "stereo output present, no phase cancellation"},
|
|
72
|
-
{"tool": "analyze_mix", "check": "stereo.mono_risk is false"},
|
|
71
|
+
{"tool": "get_track_meters", "check": "stereo output present, no phase cancellation", "backend": "remote_command"},
|
|
72
|
+
{"tool": "analyze_mix", "check": "stereo.mono_risk is false", "backend": "mcp_tool"},
|
|
73
73
|
],
|
|
74
74
|
)
|
|
75
75
|
|
|
@@ -177,24 +177,25 @@ def apply_semantic_move(
|
|
|
177
177
|
result["note"] = "Awaiting approval — present the plan to the user, then execute steps individually"
|
|
178
178
|
return result
|
|
179
179
|
|
|
180
|
-
# explore mode — execute
|
|
180
|
+
# explore mode — execute through unified router
|
|
181
|
+
from ..runtime.execution_router import execute_plan_steps
|
|
182
|
+
|
|
183
|
+
step_dicts = [
|
|
184
|
+
{"tool": step.tool, "params": step.params, "description": step.description}
|
|
185
|
+
for step in plan.steps
|
|
186
|
+
]
|
|
187
|
+
exec_results = execute_plan_steps(step_dicts, ableton=ableton, ctx=ctx)
|
|
188
|
+
|
|
181
189
|
executed_steps = []
|
|
182
|
-
for
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
except Exception as exc:
|
|
192
|
-
executed_steps.append({
|
|
193
|
-
"tool": step.tool,
|
|
194
|
-
"description": step.description,
|
|
195
|
-
"error": str(exc),
|
|
196
|
-
"ok": False,
|
|
197
|
-
})
|
|
190
|
+
for i, er in enumerate(exec_results):
|
|
191
|
+
executed_steps.append({
|
|
192
|
+
"tool": er.tool,
|
|
193
|
+
"backend": er.backend,
|
|
194
|
+
"description": step_dicts[i].get("description", ""),
|
|
195
|
+
"result": er.result if er.ok else None,
|
|
196
|
+
"error": er.error if not er.ok else None,
|
|
197
|
+
"ok": er.ok,
|
|
198
|
+
})
|
|
198
199
|
|
|
199
200
|
result = plan.to_dict()
|
|
200
201
|
result["executed"] = True
|
|
@@ -11,12 +11,12 @@ INCREASE_FORWARD_MOTION = SemanticMove(
|
|
|
11
11
|
protect={"clarity": 0.6},
|
|
12
12
|
risk_level="low",
|
|
13
13
|
compile_plan=[
|
|
14
|
-
{"tool": "apply_automation_shape", "params": {"curve_type": "exponential", "description": "Rising filter cutoff over 4 bars"}, "description": "Rising filter sweep"},
|
|
15
|
-
{"tool": "set_track_volume", "params": {"description": "Push rhythm elements +5-8%"}, "description": "Push rhythm forward"},
|
|
16
|
-
{"tool": "apply_automation_shape", "params": {"curve_type": "linear", "description": "Rising reverb send for anticipation"}, "description": "Build reverb wash"},
|
|
14
|
+
{"tool": "apply_automation_shape", "params": {"curve_type": "exponential", "description": "Rising filter cutoff over 4 bars"}, "description": "Rising filter sweep", "backend": "mcp_tool"},
|
|
15
|
+
{"tool": "set_track_volume", "params": {"description": "Push rhythm elements +5-8%"}, "description": "Push rhythm forward", "backend": "remote_command"},
|
|
16
|
+
{"tool": "apply_automation_shape", "params": {"curve_type": "linear", "description": "Rising reverb send for anticipation"}, "description": "Build reverb wash", "backend": "mcp_tool"},
|
|
17
17
|
],
|
|
18
18
|
verification_plan=[
|
|
19
|
-
{"tool": "get_track_meters", "check": "energy increasing, all tracks alive"},
|
|
19
|
+
{"tool": "get_track_meters", "check": "energy increasing, all tracks alive", "backend": "remote_command"},
|
|
20
20
|
],
|
|
21
21
|
)
|
|
22
22
|
|
|
@@ -28,13 +28,13 @@ OPEN_CHORUS = SemanticMove(
|
|
|
28
28
|
protect={"clarity": 0.6, "cohesion": 0.5},
|
|
29
29
|
risk_level="medium",
|
|
30
30
|
compile_plan=[
|
|
31
|
-
{"tool": "set_track_volume", "params": {"description": "Push all melodic tracks +10-15%"}, "description": "Push chorus energy"},
|
|
32
|
-
{"tool": "set_track_pan", "params": {"description": "Widen stereo field on chords/pads"}, "description": "Widen stereo"},
|
|
33
|
-
{"tool": "set_track_send", "params": {"description": "Increase reverb/delay sends for spaciousness"}, "description": "Add space"},
|
|
31
|
+
{"tool": "set_track_volume", "params": {"description": "Push all melodic tracks +10-15%"}, "description": "Push chorus energy", "backend": "remote_command"},
|
|
32
|
+
{"tool": "set_track_pan", "params": {"description": "Widen stereo field on chords/pads"}, "description": "Widen stereo", "backend": "remote_command"},
|
|
33
|
+
{"tool": "set_track_send", "params": {"description": "Increase reverb/delay sends for spaciousness"}, "description": "Add space", "backend": "remote_command"},
|
|
34
34
|
],
|
|
35
35
|
verification_plan=[
|
|
36
|
-
{"tool": "get_track_meters", "check": "overall energy increased, stereo field wider"},
|
|
37
|
-
{"tool": "analyze_mix", "check": "no clipping, stereo.mono_risk is false"},
|
|
36
|
+
{"tool": "get_track_meters", "check": "overall energy increased, stereo field wider", "backend": "remote_command"},
|
|
37
|
+
{"tool": "analyze_mix", "check": "no clipping, stereo.mono_risk is false", "backend": "mcp_tool"},
|
|
38
38
|
],
|
|
39
39
|
)
|
|
40
40
|
|
|
@@ -46,12 +46,12 @@ CREATE_BREAKDOWN = SemanticMove(
|
|
|
46
46
|
protect={"cohesion": 0.5},
|
|
47
47
|
risk_level="medium",
|
|
48
48
|
compile_plan=[
|
|
49
|
-
{"tool": "set_track_volume", "params": {"description": "Pull drums to 20-30%"}, "description": "Strip drums"},
|
|
50
|
-
{"tool": "set_track_volume", "params": {"description": "Pull bass to 30-40%"}, "description": "Reduce bass"},
|
|
51
|
-
{"tool": "set_track_send", "params": {"description": "Increase reverb send on remaining elements"}, "description": "Add reverb depth"},
|
|
49
|
+
{"tool": "set_track_volume", "params": {"description": "Pull drums to 20-30%"}, "description": "Strip drums", "backend": "remote_command"},
|
|
50
|
+
{"tool": "set_track_volume", "params": {"description": "Pull bass to 30-40%"}, "description": "Reduce bass", "backend": "remote_command"},
|
|
51
|
+
{"tool": "set_track_send", "params": {"description": "Increase reverb send on remaining elements"}, "description": "Add reverb depth", "backend": "remote_command"},
|
|
52
52
|
],
|
|
53
53
|
verification_plan=[
|
|
54
|
-
{"tool": "get_track_meters", "check": "energy significantly reduced, at least one element still prominent"},
|
|
54
|
+
{"tool": "get_track_meters", "check": "energy significantly reduced, at least one element still prominent", "backend": "remote_command"},
|
|
55
55
|
],
|
|
56
56
|
)
|
|
57
57
|
|
|
@@ -63,11 +63,11 @@ BRIDGE_SECTIONS = SemanticMove(
|
|
|
63
63
|
protect={"clarity": 0.6},
|
|
64
64
|
risk_level="low",
|
|
65
65
|
compile_plan=[
|
|
66
|
-
{"tool": "apply_automation_shape", "params": {"curve_type": "cosine", "description": "Gentle filter sweep across bridge"}, "description": "Bridge filter motion"},
|
|
67
|
-
{"tool": "set_track_volume", "params": {"description": "Gentle volume crossfade between section elements"}, "description": "Crossfade elements"},
|
|
66
|
+
{"tool": "apply_automation_shape", "params": {"curve_type": "cosine", "description": "Gentle filter sweep across bridge"}, "description": "Bridge filter motion", "backend": "mcp_tool"},
|
|
67
|
+
{"tool": "set_track_volume", "params": {"description": "Gentle volume crossfade between section elements"}, "description": "Crossfade elements", "backend": "remote_command"},
|
|
68
68
|
],
|
|
69
69
|
verification_plan=[
|
|
70
|
-
{"tool": "get_track_meters", "check": "smooth level transition, no dropouts"},
|
|
70
|
+
{"tool": "get_track_meters", "check": "smooth level transition, no dropouts", "backend": "remote_command"},
|
|
71
71
|
],
|
|
72
72
|
)
|
|
73
73
|
|
package/mcp_server/server.py
CHANGED
|
@@ -40,6 +40,40 @@ def _identify_port_holder(port: int) -> str | None:
|
|
|
40
40
|
return None
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
def _master_has_livepilot_analyzer(ableton: AbletonConnection) -> bool:
|
|
44
|
+
"""Check whether the analyzer device is currently on the master track."""
|
|
45
|
+
try:
|
|
46
|
+
track = ableton.send_command("get_master_track")
|
|
47
|
+
except Exception:
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
devices = track.get("devices", []) if isinstance(track, dict) else []
|
|
51
|
+
for device in devices:
|
|
52
|
+
normalized = " ".join(
|
|
53
|
+
str(device.get("name") or "").replace("_", " ").replace("-", " ").lower().split()
|
|
54
|
+
)
|
|
55
|
+
if normalized == "livepilot analyzer":
|
|
56
|
+
return True
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def _warm_analyzer_bridge(
|
|
61
|
+
ableton: AbletonConnection,
|
|
62
|
+
spectral: SpectralCache,
|
|
63
|
+
timeout: float = 3.0,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Give the analyzer stream a short startup window before first use."""
|
|
66
|
+
if not _master_has_livepilot_analyzer(ableton):
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
loop = asyncio.get_running_loop()
|
|
70
|
+
deadline = loop.time() + max(timeout, 0.0)
|
|
71
|
+
while loop.time() < deadline:
|
|
72
|
+
if spectral.is_connected:
|
|
73
|
+
return
|
|
74
|
+
await asyncio.sleep(0.05)
|
|
75
|
+
|
|
76
|
+
|
|
43
77
|
@asynccontextmanager
|
|
44
78
|
async def lifespan(server):
|
|
45
79
|
"""Create and yield the shared AbletonConnection + M4L bridge."""
|
|
@@ -80,6 +114,8 @@ async def lifespan(server):
|
|
|
80
114
|
}
|
|
81
115
|
|
|
82
116
|
try:
|
|
117
|
+
if bridge_state["transport"] is not None:
|
|
118
|
+
await _warm_analyzer_bridge(ableton, spectral)
|
|
83
119
|
yield {
|
|
84
120
|
"ableton": ableton,
|
|
85
121
|
"spectral": spectral,
|
|
@@ -140,6 +176,10 @@ from .stuckness_detector import tools as stuckness_tools # noqa: F401, E40
|
|
|
140
176
|
from .wonder_mode import tools as wonder_mode_tools # noqa: F401, E402
|
|
141
177
|
from .session_continuity import tools as session_cont_tools # noqa: F401, E402
|
|
142
178
|
from .creative_constraints import tools as constraints_tools # noqa: F401, E402
|
|
179
|
+
from .device_forge import tools as device_forge_tools # noqa: F401, E402
|
|
180
|
+
from .sample_engine import tools as sample_engine_tools # noqa: F401, E402
|
|
181
|
+
from .atlas import tools as atlas_tools # noqa: F401, E402
|
|
182
|
+
from .composer import tools as composer_tools # noqa: F401, E402
|
|
143
183
|
|
|
144
184
|
|
|
145
185
|
# ---------------------------------------------------------------------------
|
|
@@ -170,6 +210,14 @@ def _coerce_schema_property(prop: dict) -> None:
|
|
|
170
210
|
# Recurse into array items so list[int]/list[float] params also accept strings
|
|
171
211
|
if "items" in prop and isinstance(prop["items"], dict):
|
|
172
212
|
_coerce_schema_property(prop["items"])
|
|
213
|
+
if "properties" in prop and isinstance(prop["properties"], dict):
|
|
214
|
+
for nested in prop["properties"].values():
|
|
215
|
+
if isinstance(nested, dict):
|
|
216
|
+
_coerce_schema_property(nested)
|
|
217
|
+
if "$defs" in prop and isinstance(prop["$defs"], dict):
|
|
218
|
+
for nested in prop["$defs"].values():
|
|
219
|
+
if isinstance(nested, dict):
|
|
220
|
+
_coerce_schema_property(nested)
|
|
173
221
|
|
|
174
222
|
|
|
175
223
|
def _get_all_tools():
|
|
@@ -202,6 +250,9 @@ def _patch_tool_schemas() -> None:
|
|
|
202
250
|
if name == "ctx":
|
|
203
251
|
continue # skip the Context parameter
|
|
204
252
|
_coerce_schema_property(prop)
|
|
253
|
+
for definition in tool.parameters.get("$defs", {}).values():
|
|
254
|
+
if isinstance(definition, dict):
|
|
255
|
+
_coerce_schema_property(definition)
|
|
205
256
|
|
|
206
257
|
|
|
207
258
|
_patch_tool_schemas()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shared services consumed by multiple domains."""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Shared motif service — one entry point for all motif consumers.
|
|
2
|
+
|
|
3
|
+
SongBrain, HookHunter, and musical_intelligence all import from here
|
|
4
|
+
instead of making ad-hoc calls to the motif engine or TCP.
|
|
5
|
+
|
|
6
|
+
Pure computation — no I/O. Callers provide pre-fetched data.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_motif_data(
|
|
15
|
+
notes_by_track: dict[int, list[dict]],
|
|
16
|
+
) -> dict:
|
|
17
|
+
"""Extract motif data from pre-fetched notes.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
notes_by_track: {track_index: [note_dicts]} from get_notes calls
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Motif analysis dict with motifs, motif_count, tracks_analyzed.
|
|
24
|
+
Returns empty result if no notes or engine unavailable.
|
|
25
|
+
"""
|
|
26
|
+
if not notes_by_track:
|
|
27
|
+
return {"motifs": [], "motif_count": 0, "tracks_analyzed": 0}
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from ..tools import _motif_engine as motif_engine
|
|
31
|
+
motifs = motif_engine.detect_motifs(notes_by_track)
|
|
32
|
+
return {
|
|
33
|
+
"motifs": [m.to_dict() for m in motifs],
|
|
34
|
+
"motif_count": len(motifs),
|
|
35
|
+
"tracks_analyzed": len(notes_by_track),
|
|
36
|
+
}
|
|
37
|
+
except Exception:
|
|
38
|
+
return {"motifs": [], "motif_count": 0, "tracks_analyzed": 0}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def fetch_notes_from_ableton(ableton, tracks: list[dict], max_clips: int = 8) -> dict[int, list[dict]]:
|
|
42
|
+
"""Fetch notes from Ableton for motif analysis.
|
|
43
|
+
|
|
44
|
+
This is the I/O helper — calls get_notes through valid TCP commands.
|
|
45
|
+
Callers pass the ableton connection; this function does the fetching.
|
|
46
|
+
"""
|
|
47
|
+
notes_by_track: dict[int, list[dict]] = {}
|
|
48
|
+
for track in tracks:
|
|
49
|
+
t_idx = track.get("index", 0)
|
|
50
|
+
if not track.get("has_midi_input", False) and not any(
|
|
51
|
+
kw in track.get("name", "").lower()
|
|
52
|
+
for kw in ("midi", "synth", "bass", "lead", "pad", "keys", "piano", "chord")
|
|
53
|
+
):
|
|
54
|
+
continue
|
|
55
|
+
track_notes = []
|
|
56
|
+
for clip_idx in range(max_clips):
|
|
57
|
+
try:
|
|
58
|
+
result = ableton.send_command("get_notes", {
|
|
59
|
+
"track_index": t_idx,
|
|
60
|
+
"clip_index": clip_idx,
|
|
61
|
+
})
|
|
62
|
+
track_notes.extend(result.get("notes", []))
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
if track_notes:
|
|
66
|
+
notes_by_track[t_idx] = track_notes
|
|
67
|
+
return notes_by_track
|
|
@@ -23,14 +23,22 @@ from .models import (
|
|
|
23
23
|
_story = SessionStory()
|
|
24
24
|
_threads: dict[str, CreativeThread] = {}
|
|
25
25
|
_turns: list[TurnResolution] = []
|
|
26
|
+
_project_store = None # Optional PersistentProjectStore
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def set_project_store(store) -> None:
|
|
30
|
+
"""Attach a persistent project store for flush-on-write."""
|
|
31
|
+
global _project_store
|
|
32
|
+
_project_store = store
|
|
26
33
|
|
|
27
34
|
|
|
28
35
|
def reset_story() -> None:
|
|
29
36
|
"""Reset session story (for testing)."""
|
|
30
|
-
global _story, _threads, _turns
|
|
37
|
+
global _story, _threads, _turns, _project_store
|
|
31
38
|
_story = SessionStory()
|
|
32
39
|
_threads = {}
|
|
33
40
|
_turns = []
|
|
41
|
+
_project_store = None
|
|
34
42
|
|
|
35
43
|
|
|
36
44
|
# ── Session story ─────────────────────────────────────────────────
|
|
@@ -117,6 +125,13 @@ def record_turn_resolution(
|
|
|
117
125
|
else:
|
|
118
126
|
_story.mood_arc.append("neutral")
|
|
119
127
|
|
|
128
|
+
# Flush to persistent store
|
|
129
|
+
if _project_store is not None:
|
|
130
|
+
try:
|
|
131
|
+
_project_store.save_turn(turn.to_dict())
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
120
135
|
return turn
|
|
121
136
|
|
|
122
137
|
|
|
@@ -138,6 +153,14 @@ def open_thread(description: str, domain: str = "", priority: float = 0.5) -> Cr
|
|
|
138
153
|
last_touched_ms=now,
|
|
139
154
|
)
|
|
140
155
|
_threads[thread_id] = thread
|
|
156
|
+
|
|
157
|
+
# Flush to persistent store
|
|
158
|
+
if _project_store is not None:
|
|
159
|
+
try:
|
|
160
|
+
_project_store.save_thread(thread.to_dict())
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
|
|
141
164
|
return thread
|
|
142
165
|
|
|
143
166
|
|
|
@@ -147,6 +170,11 @@ def resolve_thread(thread_id: str) -> Optional[CreativeThread]:
|
|
|
147
170
|
if thread:
|
|
148
171
|
thread.status = "resolved"
|
|
149
172
|
thread.last_touched_ms = int(time.time() * 1000)
|
|
173
|
+
if _project_store is not None:
|
|
174
|
+
try:
|
|
175
|
+
_project_store.save_thread(thread.to_dict())
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
150
178
|
return thread
|
|
151
179
|
|
|
152
180
|
|
|
@@ -74,16 +74,43 @@ def build_song_brain(
|
|
|
74
74
|
|
|
75
75
|
drift_risk = _estimate_drift_risk(recent_moves, sacred)
|
|
76
76
|
|
|
77
|
+
# Evidence-weighted confidence adjustment
|
|
78
|
+
# Weights: motif=0.4, composition=0.2, role_graph=0.15, scenes=0.15, recent_moves=0.1
|
|
79
|
+
evidence_weights = {
|
|
80
|
+
"motif_data": 0.4,
|
|
81
|
+
"composition_analysis": 0.2,
|
|
82
|
+
"role_graph": 0.15,
|
|
83
|
+
"scenes": 0.15,
|
|
84
|
+
"recent_moves": 0.1,
|
|
85
|
+
}
|
|
86
|
+
evidence_score = sum(
|
|
87
|
+
weight for source, weight in evidence_weights.items()
|
|
88
|
+
if built_from.get(source, False)
|
|
89
|
+
)
|
|
90
|
+
# Adjust identity confidence by evidence availability
|
|
91
|
+
adjusted_confidence = round(identity_confidence * (0.4 + 0.6 * evidence_score), 3)
|
|
92
|
+
|
|
93
|
+
evidence_breakdown = {
|
|
94
|
+
"raw_confidence": identity_confidence,
|
|
95
|
+
"evidence_score": round(evidence_score, 3),
|
|
96
|
+
"adjusted_confidence": adjusted_confidence,
|
|
97
|
+
"sources": {
|
|
98
|
+
source: {"available": built_from.get(source, False), "weight": weight}
|
|
99
|
+
for source, weight in evidence_weights.items()
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
77
103
|
return SongBrain(
|
|
78
104
|
brain_id=brain_id,
|
|
79
105
|
identity_core=identity_core,
|
|
80
|
-
identity_confidence=
|
|
106
|
+
identity_confidence=adjusted_confidence,
|
|
81
107
|
sacred_elements=sacred,
|
|
82
108
|
section_purposes=sections,
|
|
83
109
|
energy_arc=energy_arc,
|
|
84
110
|
identity_drift_risk=drift_risk,
|
|
85
111
|
payoff_targets=payoff_targets,
|
|
86
112
|
open_questions=open_questions,
|
|
113
|
+
evidence_breakdown=evidence_breakdown,
|
|
87
114
|
built_from=built_from,
|
|
88
115
|
)
|
|
89
116
|
|
|
@@ -84,6 +84,9 @@ class SongBrain:
|
|
|
84
84
|
# Open questions the song has not resolved
|
|
85
85
|
open_questions: list[OpenQuestion] = field(default_factory=list)
|
|
86
86
|
|
|
87
|
+
# Evidence breakdown — what data informed each inference
|
|
88
|
+
evidence_breakdown: dict = field(default_factory=dict)
|
|
89
|
+
|
|
87
90
|
# Metadata
|
|
88
91
|
built_from: dict = field(default_factory=dict) # what data sources contributed
|
|
89
92
|
|
|
@@ -98,6 +101,7 @@ class SongBrain:
|
|
|
98
101
|
"identity_drift_risk": self.identity_drift_risk,
|
|
99
102
|
"payoff_targets": self.payoff_targets,
|
|
100
103
|
"open_questions": [q.to_dict() for q in self.open_questions],
|
|
104
|
+
"evidence_breakdown": self.evidence_breakdown,
|
|
101
105
|
"built_from": self.built_from,
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -88,9 +88,11 @@ def _fetch_session_data(ctx: Context) -> dict:
|
|
|
88
88
|
except Exception:
|
|
89
89
|
pass
|
|
90
90
|
|
|
91
|
-
# Motif data —
|
|
91
|
+
# Motif data — via shared motif service (pure-Python, not TCP)
|
|
92
92
|
try:
|
|
93
|
-
|
|
93
|
+
from ..services.motif_service import get_motif_data, fetch_notes_from_ableton
|
|
94
|
+
notes_by_track = fetch_notes_from_ableton(ableton, data.get("tracks", []))
|
|
95
|
+
data["motif_data"] = get_motif_data(notes_by_track)
|
|
94
96
|
except Exception:
|
|
95
97
|
pass # Motif graph requires notes in clips; empty is valid
|
|
96
98
|
|
|
@@ -147,6 +149,21 @@ def build_song_brain(ctx: Context) -> dict:
|
|
|
147
149
|
Returns the full SongBrain as a dict.
|
|
148
150
|
"""
|
|
149
151
|
data = _fetch_session_data(ctx)
|
|
152
|
+
|
|
153
|
+
# Capability reporting — what data was actually available
|
|
154
|
+
from ..runtime.capability import build_capability
|
|
155
|
+
cap = build_capability(
|
|
156
|
+
required=["session_info", "scenes", "tracks", "motif_data", "composition_analysis", "role_graph"],
|
|
157
|
+
available={
|
|
158
|
+
"session_info": bool(data.get("session_info", {}).get("tempo")),
|
|
159
|
+
"scenes": bool(data.get("scenes")),
|
|
160
|
+
"tracks": bool(data.get("tracks")),
|
|
161
|
+
"motif_data": bool(data.get("motif_data")),
|
|
162
|
+
"composition_analysis": bool(data.get("composition_analysis")),
|
|
163
|
+
"role_graph": bool(data.get("role_graph")),
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
|
|
150
167
|
brain = builder.build_song_brain(
|
|
151
168
|
session_info=data["session_info"],
|
|
152
169
|
scenes=data["scenes"],
|
|
@@ -161,6 +178,7 @@ def build_song_brain(ctx: Context) -> dict:
|
|
|
161
178
|
return {
|
|
162
179
|
**brain.to_dict(),
|
|
163
180
|
"summary": brain.summary,
|
|
181
|
+
"capability": cap.to_dict(),
|
|
164
182
|
}
|
|
165
183
|
|
|
166
184
|
|
|
@@ -281,17 +281,105 @@ def run_layer_overlap_critic(
|
|
|
281
281
|
return issues
|
|
282
282
|
|
|
283
283
|
|
|
284
|
+
# ── Corpus Intelligence Critic ──────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def run_corpus_critic(
|
|
288
|
+
patch: PatchModel,
|
|
289
|
+
goal: TimbralGoalVector,
|
|
290
|
+
) -> list[SoundDesignIssue]:
|
|
291
|
+
"""Use the device-knowledge corpus to flag missed opportunities.
|
|
292
|
+
|
|
293
|
+
Checks each device in the chain against the corpus for known
|
|
294
|
+
techniques, parameter sweet spots, and creative possibilities
|
|
295
|
+
that the current patch doesn't exploit.
|
|
296
|
+
"""
|
|
297
|
+
try:
|
|
298
|
+
from ..corpus import get_corpus
|
|
299
|
+
except ImportError:
|
|
300
|
+
return []
|
|
301
|
+
|
|
302
|
+
corpus = get_corpus()
|
|
303
|
+
if not corpus.emotional_recipes and not corpus.device_knowledge:
|
|
304
|
+
return [] # Corpus not loaded
|
|
305
|
+
|
|
306
|
+
issues: list[SoundDesignIssue] = []
|
|
307
|
+
|
|
308
|
+
# Check if any device in the chain has corpus knowledge
|
|
309
|
+
for block in patch.blocks:
|
|
310
|
+
dk = corpus.get_device(block.device_name)
|
|
311
|
+
if dk and dk.techniques and block.block_type == "oscillator":
|
|
312
|
+
# Oscillator with known techniques — suggest if patch is simple
|
|
313
|
+
has_character_block = any(
|
|
314
|
+
b.block_type in ("saturation", "spectral")
|
|
315
|
+
for b in patch.blocks
|
|
316
|
+
)
|
|
317
|
+
if not has_character_block and len(dk.techniques) > 2:
|
|
318
|
+
issues.append(SoundDesignIssue(
|
|
319
|
+
issue_type="corpus_technique_available",
|
|
320
|
+
critic="corpus",
|
|
321
|
+
severity=0.25,
|
|
322
|
+
confidence=0.6,
|
|
323
|
+
affected_blocks=[block.device_name],
|
|
324
|
+
evidence=(
|
|
325
|
+
f"Corpus has {len(dk.techniques)} known techniques "
|
|
326
|
+
f"for {block.device_name} but chain lacks character "
|
|
327
|
+
f"processing (saturation/spectral). First technique: "
|
|
328
|
+
f"{dk.techniques[0][:80]}"
|
|
329
|
+
),
|
|
330
|
+
recommended_moves=["modulation_injection", "filter_contour"],
|
|
331
|
+
))
|
|
332
|
+
|
|
333
|
+
# Check if goal maps to a known emotional recipe
|
|
334
|
+
emotion_map = {
|
|
335
|
+
"warmth": ("warmth", goal.warmth),
|
|
336
|
+
"brightness": ("euphoria", goal.brightness),
|
|
337
|
+
"instability": ("tension", goal.instability),
|
|
338
|
+
"softness": ("melancholy", goal.softness),
|
|
339
|
+
}
|
|
340
|
+
for quality, (emotion_key, goal_value) in emotion_map.items():
|
|
341
|
+
if goal_value > 0.3:
|
|
342
|
+
recipe = corpus.suggest_for_emotion(emotion_key)
|
|
343
|
+
if recipe and recipe.techniques:
|
|
344
|
+
# Check if any corpus technique device is in the chain
|
|
345
|
+
chain_names_lower = {d.lower() for d in patch.device_chain}
|
|
346
|
+
recipe_devices = set()
|
|
347
|
+
for tech in recipe.techniques:
|
|
348
|
+
# Extract bold device names from technique strings
|
|
349
|
+
for match in re.finditer(r"\*\*(.+?)\*\*", tech):
|
|
350
|
+
recipe_devices.add(match.group(1).lower())
|
|
351
|
+
|
|
352
|
+
missing = recipe_devices - chain_names_lower
|
|
353
|
+
if missing and len(missing) <= 3:
|
|
354
|
+
issues.append(SoundDesignIssue(
|
|
355
|
+
issue_type="corpus_emotion_opportunity",
|
|
356
|
+
critic="corpus",
|
|
357
|
+
severity=0.2,
|
|
358
|
+
confidence=0.5,
|
|
359
|
+
affected_blocks=list(missing)[:3],
|
|
360
|
+
evidence=(
|
|
361
|
+
f"Goal wants {quality}={goal_value:.2f}. "
|
|
362
|
+
f"Corpus '{recipe.emotion}' recipe suggests "
|
|
363
|
+
f"devices not in chain: {', '.join(list(missing)[:3])}"
|
|
364
|
+
),
|
|
365
|
+
recommended_moves=["filter_contour", "modulation_injection"],
|
|
366
|
+
))
|
|
367
|
+
|
|
368
|
+
return issues
|
|
369
|
+
|
|
370
|
+
|
|
284
371
|
# ── Run all critics ──────────────────────────────────────────────────
|
|
285
372
|
|
|
286
373
|
|
|
287
374
|
def run_all_sound_design_critics(
|
|
288
375
|
state: SoundDesignState,
|
|
289
376
|
) -> list[SoundDesignIssue]:
|
|
290
|
-
"""Run all
|
|
377
|
+
"""Run all six critics and aggregate issues."""
|
|
291
378
|
issues: list[SoundDesignIssue] = []
|
|
292
379
|
issues.extend(run_static_timbre_critic(state.patch, state.goal))
|
|
293
380
|
issues.extend(run_weak_identity_critic(state.patch))
|
|
294
381
|
issues.extend(run_masking_role_critic(state.patch, state.layers))
|
|
295
382
|
issues.extend(run_modulation_flatness_critic(state.patch))
|
|
296
383
|
issues.extend(run_layer_overlap_critic(state.layers))
|
|
384
|
+
issues.extend(run_corpus_critic(state.patch, state.goal))
|
|
297
385
|
return issues
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Splice gRPC client — connect to Splice desktop's local API for sample search and download."""
|