livepilot 1.9.24 → 1.10.1
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 +223 -0
- package/CONTRIBUTING.md +2 -2
- package/LICENSE +62 -21
- package/README.md +291 -276
- 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-arrangement/SKILL.md +18 -1
- package/livepilot/skills/livepilot-core/SKILL.md +22 -5
- 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 +39 -4
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +23 -19
- package/livepilot/skills/livepilot-sample-engine/SKILL.md +105 -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 +17 -0
- package/livepilot.mcpb +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/manifest.json +4 -4
- 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 +532 -0
- package/mcp_server/composer/layer_planner.py +427 -0
- package/mcp_server/composer/prompt_parser.py +329 -0
- package/mcp_server/composer/sample_resolver.py +153 -0
- package/mcp_server/composer/tools.py +211 -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/m4l_bridge.py +1 -0
- package/mcp_server/memory/taste_accessors.py +47 -0
- package/mcp_server/preview_studio/engine.py +9 -2
- package/mcp_server/preview_studio/tools.py +78 -35
- package/mcp_server/project_brain/tools.py +34 -0
- package/mcp_server/runtime/capability_probe.py +21 -2
- package/mcp_server/runtime/execution_router.py +184 -38
- package/mcp_server/runtime/live_version.py +102 -0
- package/mcp_server/runtime/mcp_dispatch.py +46 -0
- package/mcp_server/runtime/remote_commands.py +13 -5
- package/mcp_server/runtime/tools.py +66 -29
- 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/slice_workflow.py +190 -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 +545 -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 +8 -8
- package/mcp_server/semantic_moves/models.py +7 -7
- package/mcp_server/semantic_moves/performance_moves.py +4 -4
- package/mcp_server/semantic_moves/sample_compilers.py +377 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +4 -4
- package/mcp_server/semantic_moves/tools.py +63 -10
- package/mcp_server/semantic_moves/transition_moves.py +4 -4
- package/mcp_server/server.py +71 -1
- package/mcp_server/session_continuity/tracker.py +4 -1
- 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/_conductor.py +16 -0
- package/mcp_server/tools/_planner_engine.py +24 -0
- package/mcp_server/tools/analyzer.py +2 -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/tools/planner.py +3 -0
- package/mcp_server/wonder_mode/diagnosis.py +5 -0
- package/mcp_server/wonder_mode/engine.py +144 -14
- package/mcp_server/wonder_mode/tools.py +33 -1
- package/package.json +14 -4
- 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 +246 -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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Registry of in-process MCP tools callable from the async execution router.
|
|
2
|
+
|
|
3
|
+
These tools live as Python async functions in the MCP server — not TCP Remote
|
|
4
|
+
Script handlers and not M4L bridge commands. Plans that want to invoke them
|
|
5
|
+
go through this registry so the async router can dispatch them in-process.
|
|
6
|
+
|
|
7
|
+
Each entry is a thin wrapper around the real MCP tool import, keeping the
|
|
8
|
+
module cheap to import (no heavy server wiring until a caller actually
|
|
9
|
+
dispatches an MCP step).
|
|
10
|
+
|
|
11
|
+
To add a new in-process tool to plans:
|
|
12
|
+
1. Add the tool name to MCP_TOOLS in execution_router.py so classify_step
|
|
13
|
+
returns "mcp_tool" for it.
|
|
14
|
+
2. Add an _adapter function here that imports the real implementation and
|
|
15
|
+
adapts its kwargs from a plan-style params dict.
|
|
16
|
+
3. Register the adapter in build_mcp_dispatch_registry.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any, Callable
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def _load_sample_to_simpler(params: dict, ctx: Any = None) -> dict:
|
|
25
|
+
"""Adapter for mcp_server.tools.analyzer.load_sample_to_simpler.
|
|
26
|
+
|
|
27
|
+
Accepts the plan-step params dict and unpacks into the real tool's kwargs.
|
|
28
|
+
"""
|
|
29
|
+
from ..tools.analyzer import load_sample_to_simpler
|
|
30
|
+
return await load_sample_to_simpler(
|
|
31
|
+
ctx,
|
|
32
|
+
track_index=int(params["track_index"]),
|
|
33
|
+
file_path=str(params["file_path"]),
|
|
34
|
+
device_index=int(params.get("device_index", 0)),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_mcp_dispatch_registry() -> dict[str, Callable]:
|
|
39
|
+
"""Return the canonical registry of MCP-only tools for plan execution.
|
|
40
|
+
|
|
41
|
+
Callers (typically the server lifespan init) should call this once and
|
|
42
|
+
pass the registry to execute_plan_steps_async via the mcp_registry kwarg.
|
|
43
|
+
"""
|
|
44
|
+
return {
|
|
45
|
+
"load_sample_to_simpler": _load_sample_to_simpler,
|
|
46
|
+
}
|
|
@@ -38,18 +38,23 @@ REMOTE_COMMANDS: frozenset[str] = frozenset({
|
|
|
38
38
|
"fire_scene", "set_scene_name", "set_scene_color", "set_scene_tempo",
|
|
39
39
|
"get_scene_matrix", "fire_scene_clips", "stop_all_clips",
|
|
40
40
|
"get_playing_clips",
|
|
41
|
-
# devices (
|
|
41
|
+
# devices (15)
|
|
42
42
|
"get_device_info", "get_device_parameters", "set_device_parameter",
|
|
43
43
|
"batch_set_parameters", "toggle_device", "delete_device",
|
|
44
44
|
"move_device", "load_device_by_uri", "find_and_load_device",
|
|
45
45
|
"set_simpler_playback_mode", "get_rack_chains", "set_chain_volume",
|
|
46
|
+
"insert_device", # 12.3+ native device insertion
|
|
47
|
+
"insert_rack_chain", # 12.3+ rack chain insertion
|
|
48
|
+
"set_drum_chain_note", # 12.3+ drum chain note assignment
|
|
46
49
|
# clip_automation (3)
|
|
47
50
|
"get_clip_automation", "set_clip_automation", "clear_clip_automation",
|
|
48
|
-
# browser (
|
|
51
|
+
# browser (6)
|
|
49
52
|
"get_browser_tree", "get_browser_items", "search_browser",
|
|
50
53
|
"load_browser_item", "get_device_presets",
|
|
51
|
-
#
|
|
54
|
+
"scan_browser_deep", # Atlas deep scan — returns full category tree
|
|
55
|
+
# arrangement (21)
|
|
52
56
|
"get_arrangement_clips", "create_arrangement_clip",
|
|
57
|
+
"create_native_arrangement_clip",
|
|
53
58
|
"add_arrangement_notes", "get_arrangement_notes",
|
|
54
59
|
"remove_arrangement_notes", "remove_arrangement_notes_by_id",
|
|
55
60
|
"modify_arrangement_notes", "duplicate_arrangement_notes",
|
|
@@ -57,7 +62,7 @@ REMOTE_COMMANDS: frozenset[str] = frozenset({
|
|
|
57
62
|
"set_arrangement_clip_name", "jump_to_time",
|
|
58
63
|
"capture_midi", "start_recording", "stop_recording",
|
|
59
64
|
"get_cue_points", "jump_to_cue", "toggle_cue_point",
|
|
60
|
-
"back_to_arranger",
|
|
65
|
+
"back_to_arranger", "force_arrangement",
|
|
61
66
|
# diagnostics (1)
|
|
62
67
|
"get_session_diagnostics",
|
|
63
68
|
# ping (built-in)
|
|
@@ -75,7 +80,10 @@ BRIDGE_COMMANDS: frozenset[str] = frozenset({
|
|
|
75
80
|
"remove_warp_marker", "capture_audio", "capture_stop",
|
|
76
81
|
"check_flucoma", "scrub_clip", "stop_scrub", "get_display_values",
|
|
77
82
|
"get_plugin_params", "map_plugin_param", "get_plugin_presets",
|
|
78
|
-
|
|
83
|
+
# NOTE: load_sample_to_simpler used to live here, but it's actually an
|
|
84
|
+
# async Python MCP tool in mcp_server/tools/analyzer.py, not a bridge
|
|
85
|
+
# command. It has no case in livepilot_bridge.js and no @register handler
|
|
86
|
+
# in remote_script. See mcp_server/runtime/execution_router.MCP_TOOLS.
|
|
79
87
|
})
|
|
80
88
|
|
|
81
89
|
# Combined: all valid send_command targets
|
|
@@ -94,52 +94,75 @@ def get_session_kernel(
|
|
|
94
94
|
|
|
95
95
|
# Core: session info + capability state
|
|
96
96
|
session_info = ableton.send_command("get_session_info")
|
|
97
|
+
session_ok = isinstance(session_info, dict) and "error" not in session_info
|
|
97
98
|
|
|
98
99
|
analyzer_ok = False
|
|
100
|
+
analyzer_fresh = False
|
|
99
101
|
if spectral is not None:
|
|
100
102
|
analyzer_ok = spectral.is_connected
|
|
103
|
+
if analyzer_ok:
|
|
104
|
+
analyzer_fresh = spectral.get("spectrum") is not None
|
|
101
105
|
|
|
102
106
|
state = build_capability_state(
|
|
103
|
-
session_ok=
|
|
107
|
+
session_ok=session_ok,
|
|
104
108
|
analyzer_ok=analyzer_ok,
|
|
109
|
+
analyzer_fresh=analyzer_fresh,
|
|
105
110
|
memory_ok=True,
|
|
106
111
|
)
|
|
107
112
|
|
|
108
|
-
# Optional subcomponents — degrade gracefully
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
# Optional subcomponents — degrade gracefully, but reach into the SAME
|
|
114
|
+
# session-scoped stores the public memory tools read/write via
|
|
115
|
+
# ctx.lifespan_context.setdefault(...). Creating fresh stores here meant
|
|
116
|
+
# users who recorded anti-preferences, session memory, or taste signals
|
|
117
|
+
# through the MCP tools always saw an empty kernel.
|
|
118
|
+
ledger_summary: dict = {}
|
|
119
|
+
taste_graph: dict = {}
|
|
120
|
+
anti_prefs: list = []
|
|
121
|
+
session_mem: list = []
|
|
122
|
+
kernel_warnings: list[str] = []
|
|
113
123
|
|
|
114
124
|
try:
|
|
115
|
-
from .action_ledger import
|
|
116
|
-
ledger =
|
|
117
|
-
if ledger:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
from .action_ledger import SessionLedger
|
|
126
|
+
ledger = ctx.lifespan_context.get("action_ledger")
|
|
127
|
+
if ledger is None:
|
|
128
|
+
ledger = SessionLedger()
|
|
129
|
+
ctx.lifespan_context["action_ledger"] = ledger
|
|
130
|
+
recent = ledger.get_recent_moves(limit=10)
|
|
131
|
+
ledger_summary = {
|
|
132
|
+
"total_moves": len(ledger._entries),
|
|
133
|
+
"memory_candidate_count": len(ledger.get_memory_candidates()),
|
|
134
|
+
"last_move": ledger.get_last_move().to_dict() if ledger.get_last_move() else None,
|
|
135
|
+
"recent_moves": [entry.to_dict() for entry in recent],
|
|
136
|
+
}
|
|
137
|
+
except Exception as e:
|
|
138
|
+
kernel_warnings.append(f"ledger_unavailable: {e}")
|
|
139
|
+
|
|
140
|
+
# Taste graph + anti-prefs — share stores via lifespan_context, use the
|
|
141
|
+
# canonical build_taste_graph() so consumers see dimension_weights shape.
|
|
122
142
|
try:
|
|
143
|
+
from ..memory.taste_graph import build_taste_graph
|
|
123
144
|
from ..memory.taste_memory import TasteMemoryStore
|
|
124
|
-
taste_store = TasteMemoryStore()
|
|
125
|
-
taste_graph = {d.name: d.to_dict() for d in taste_store._dims.values()
|
|
126
|
-
if d.evidence_count > 0}
|
|
127
|
-
except Exception:
|
|
128
|
-
pass
|
|
129
|
-
|
|
130
|
-
try:
|
|
131
145
|
from ..memory.anti_memory import AntiMemoryStore
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
146
|
+
from ..persistence.taste_store import PersistentTasteStore
|
|
147
|
+
taste_store = ctx.lifespan_context.setdefault("taste_memory", TasteMemoryStore())
|
|
148
|
+
anti_store = ctx.lifespan_context.setdefault("anti_memory", AntiMemoryStore())
|
|
149
|
+
persistent = ctx.lifespan_context.setdefault("persistent_taste", PersistentTasteStore())
|
|
150
|
+
graph = build_taste_graph(
|
|
151
|
+
taste_store=taste_store,
|
|
152
|
+
anti_store=anti_store,
|
|
153
|
+
persistent_store=persistent,
|
|
154
|
+
)
|
|
155
|
+
taste_graph = graph.to_dict()
|
|
156
|
+
anti_prefs = [p.to_dict() for p in anti_store.get_anti_preferences()]
|
|
157
|
+
except Exception as e:
|
|
158
|
+
kernel_warnings.append(f"taste_graph_unavailable: {e}")
|
|
136
159
|
|
|
137
160
|
try:
|
|
138
161
|
from ..memory.session_memory import SessionMemoryStore
|
|
139
|
-
mem_store = SessionMemoryStore()
|
|
140
|
-
session_mem = mem_store.
|
|
141
|
-
except Exception:
|
|
142
|
-
|
|
162
|
+
mem_store = ctx.lifespan_context.setdefault("session_memory", SessionMemoryStore())
|
|
163
|
+
session_mem = [entry.to_dict() for entry in mem_store.get_recent(limit=10)]
|
|
164
|
+
except Exception as e:
|
|
165
|
+
kernel_warnings.append(f"session_memory_unavailable: {e}")
|
|
143
166
|
|
|
144
167
|
kernel = build_session_kernel(
|
|
145
168
|
session_info=session_info,
|
|
@@ -153,4 +176,18 @@ def get_session_kernel(
|
|
|
153
176
|
anti_preferences=anti_prefs,
|
|
154
177
|
)
|
|
155
178
|
|
|
156
|
-
|
|
179
|
+
# Populate routing hints from conductor when request context is available
|
|
180
|
+
if request_text.strip():
|
|
181
|
+
try:
|
|
182
|
+
from ..tools._conductor import classify_request
|
|
183
|
+
plan = classify_request(request_text)
|
|
184
|
+
kernel.recommended_engines = [r.engine for r in plan.routes[:3]]
|
|
185
|
+
kernel.recommended_workflow = plan.workflow_mode
|
|
186
|
+
except Exception as e:
|
|
187
|
+
kernel_warnings.append(f"conductor_routing_unavailable: {e}")
|
|
188
|
+
|
|
189
|
+
result_dict = kernel.to_dict()
|
|
190
|
+
if kernel_warnings:
|
|
191
|
+
# Additive — callers can ignore; debug-mode introspection benefits.
|
|
192
|
+
result_dict["warnings"] = kernel_warnings
|
|
193
|
+
return result_dict
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Sample Engine — intelligence layer for sample discovery, analysis, and manipulation."""
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""SampleAnalyzer — filename parsing, material classification, mode recommendation.
|
|
2
|
+
|
|
3
|
+
Pure computation for the offline parts. Spectral analysis requires M4L bridge
|
|
4
|
+
and is handled in tools.py which calls these functions + bridge data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from .models import SampleProfile
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ── Filename Metadata Parsing ───────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
# Key patterns: C, Cm, C#, C#m, Cb, Cbm, Csharp, Csharpmin, etc.
|
|
19
|
+
_KEY_PATTERN = re.compile(
|
|
20
|
+
r'\b([A-G])([b#]|sharp|flat)?(m|min|minor|maj|major)?\b',
|
|
21
|
+
re.IGNORECASE,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# BPM patterns: 120bpm, 120_bpm, 120 BPM, or standalone 60-300 range
|
|
25
|
+
_BPM_PATTERN = re.compile(
|
|
26
|
+
r'\b(\d{2,3})\s*(?:bpm)\b', re.IGNORECASE,
|
|
27
|
+
)
|
|
28
|
+
_BPM_STANDALONE = re.compile(
|
|
29
|
+
r'(?:^|[_\-\s])(\d{2,3})(?:[_\-\s]|$)',
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_KEY_NORMALIZE = {
|
|
33
|
+
"sharp": "#", "flat": "b",
|
|
34
|
+
"min": "m", "minor": "m", "maj": "", "major": "",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_filename_metadata(filename: str) -> dict:
|
|
39
|
+
"""Extract key and BPM from a filename string.
|
|
40
|
+
|
|
41
|
+
Returns dict with 'key' (str|None) and 'bpm' (float|None).
|
|
42
|
+
"""
|
|
43
|
+
stem = os.path.splitext(os.path.basename(filename))[0]
|
|
44
|
+
# Replace common separators with spaces for easier matching
|
|
45
|
+
normalized = stem.replace("-", " ").replace("_", " ")
|
|
46
|
+
|
|
47
|
+
key = _extract_key(normalized)
|
|
48
|
+
bpm = _extract_bpm(normalized)
|
|
49
|
+
|
|
50
|
+
return {"key": key, "bpm": bpm}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _extract_key(text: str) -> Optional[str]:
|
|
54
|
+
"""Extract musical key from text."""
|
|
55
|
+
matches = list(_KEY_PATTERN.finditer(text))
|
|
56
|
+
for match in matches:
|
|
57
|
+
root = match.group(1).upper()
|
|
58
|
+
accidental = match.group(2) or ""
|
|
59
|
+
quality = match.group(3) or ""
|
|
60
|
+
|
|
61
|
+
# Normalize accidentals
|
|
62
|
+
accidental = _KEY_NORMALIZE.get(accidental.lower(), accidental)
|
|
63
|
+
quality = _KEY_NORMALIZE.get(quality.lower(), quality) if quality else ""
|
|
64
|
+
|
|
65
|
+
# Avoid false positives: single letters that are common words
|
|
66
|
+
full = root + accidental + quality
|
|
67
|
+
if len(full) == 1 and root in ("A", "B", "C", "D", "E", "F", "G"):
|
|
68
|
+
# Single letter — only accept if it looks like it's in a key context
|
|
69
|
+
# Check surrounding chars
|
|
70
|
+
start = match.start()
|
|
71
|
+
end = match.end()
|
|
72
|
+
before = text[start - 1] if start > 0 else " "
|
|
73
|
+
after = text[end] if end < len(text) else " "
|
|
74
|
+
if before.isalpha() or after.isalpha():
|
|
75
|
+
continue # Part of a word, not a key
|
|
76
|
+
return full
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _extract_bpm(text: str) -> Optional[float]:
|
|
81
|
+
"""Extract BPM from text."""
|
|
82
|
+
# Try explicit bpm markers first
|
|
83
|
+
match = _BPM_PATTERN.search(text)
|
|
84
|
+
if match:
|
|
85
|
+
bpm = float(match.group(1))
|
|
86
|
+
if 40 <= bpm <= 300:
|
|
87
|
+
return bpm
|
|
88
|
+
|
|
89
|
+
# Try standalone numbers in valid range
|
|
90
|
+
for match in _BPM_STANDALONE.finditer(text):
|
|
91
|
+
bpm = float(match.group(1))
|
|
92
|
+
if 60 <= bpm <= 250:
|
|
93
|
+
return bpm
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ── Material Classification ─────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
_MATERIAL_KEYWORDS: dict[str, list[str]] = {
|
|
100
|
+
"vocal": ["vocal", "vox", "voice", "singer", "acapella", "spoken"],
|
|
101
|
+
"drum_loop": ["drum", "beat", "break", "breakbeat", "loop", "groove",
|
|
102
|
+
"hihat", "hat", "ride", "cymbal", "perc", "percussion",
|
|
103
|
+
"shaker", "tamb", "conga", "bongo", "top"],
|
|
104
|
+
"one_shot": ["kick", "snare", "clap", "snap", "tom", "rim", "hit",
|
|
105
|
+
"oneshot", "one shot", "stab", "shot", "impact"],
|
|
106
|
+
"instrument_loop": ["guitar", "piano", "keys", "bass", "synth",
|
|
107
|
+
"strings", "brass", "horn", "organ", "riff",
|
|
108
|
+
"chord", "arp", "pluck"],
|
|
109
|
+
"texture": ["ambient", "pad", "drone", "atmosphere", "noise",
|
|
110
|
+
"texture", "wash", "evolving", "soundscape"],
|
|
111
|
+
"foley": ["foley", "field", "recording", "room", "nature",
|
|
112
|
+
"water", "metal", "wood", "glass", "paper"],
|
|
113
|
+
"fx": ["fx", "effect", "riser", "sweep", "whoosh", "boom",
|
|
114
|
+
"transition", "downlifter", "uplifter"],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def classify_material_from_name(name: str) -> str:
|
|
119
|
+
"""Classify sample material type from filename/name keywords."""
|
|
120
|
+
lower = name.lower().replace("-", " ").replace("_", " ")
|
|
121
|
+
|
|
122
|
+
# Score each type by keyword matches
|
|
123
|
+
scores: dict[str, int] = {}
|
|
124
|
+
for material_type, keywords in _MATERIAL_KEYWORDS.items():
|
|
125
|
+
score = sum(1 for kw in keywords if kw in lower)
|
|
126
|
+
if score > 0:
|
|
127
|
+
scores[material_type] = score
|
|
128
|
+
|
|
129
|
+
if not scores:
|
|
130
|
+
return "unknown"
|
|
131
|
+
|
|
132
|
+
return max(scores, key=scores.get)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ── Simpler Mode Recommendation ────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def suggest_simpler_mode(profile: SampleProfile) -> str:
|
|
139
|
+
"""Recommend Simpler playback mode based on material analysis.
|
|
140
|
+
|
|
141
|
+
Returns: "classic", "one_shot", or "slice"
|
|
142
|
+
"""
|
|
143
|
+
if profile.duration_seconds < 0.5 or profile.material_type == "one_shot":
|
|
144
|
+
return "classic"
|
|
145
|
+
if profile.material_type == "fx":
|
|
146
|
+
return "classic"
|
|
147
|
+
if profile.material_type in ("texture", "foley"):
|
|
148
|
+
return "classic"
|
|
149
|
+
if profile.material_type in ("drum_loop", "instrument_loop",
|
|
150
|
+
"vocal", "full_mix"):
|
|
151
|
+
return "slice"
|
|
152
|
+
# Unknown material with decent length — slice is more useful
|
|
153
|
+
if profile.duration_seconds > 2.0:
|
|
154
|
+
return "slice"
|
|
155
|
+
return "classic"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def suggest_slice_method(profile: SampleProfile) -> str:
|
|
159
|
+
"""Recommend slice-by method for Simpler's Slice mode."""
|
|
160
|
+
if profile.material_type == "drum_loop":
|
|
161
|
+
return "transient"
|
|
162
|
+
if profile.material_type == "instrument_loop":
|
|
163
|
+
return "beat"
|
|
164
|
+
if profile.material_type == "vocal":
|
|
165
|
+
return "region"
|
|
166
|
+
if profile.material_type == "full_mix":
|
|
167
|
+
return "beat"
|
|
168
|
+
return "transient"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def suggest_warp_mode(profile: SampleProfile) -> str:
|
|
172
|
+
"""Recommend Ableton warp mode for the sample material."""
|
|
173
|
+
mode_map = {
|
|
174
|
+
"drum_loop": "beats",
|
|
175
|
+
"one_shot": "complex",
|
|
176
|
+
"instrument_loop": "complex_pro",
|
|
177
|
+
"vocal": "complex_pro",
|
|
178
|
+
"texture": "texture",
|
|
179
|
+
"foley": "texture",
|
|
180
|
+
"fx": "complex",
|
|
181
|
+
"full_mix": "complex_pro",
|
|
182
|
+
}
|
|
183
|
+
return mode_map.get(profile.material_type, "complex")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def build_profile_from_filename(
|
|
187
|
+
file_path: str,
|
|
188
|
+
source: str = "filesystem",
|
|
189
|
+
duration_seconds: float = 0.0,
|
|
190
|
+
) -> SampleProfile:
|
|
191
|
+
"""Build a SampleProfile from filename metadata only (no spectral analysis).
|
|
192
|
+
|
|
193
|
+
This is the fallback when M4L bridge is unavailable.
|
|
194
|
+
"""
|
|
195
|
+
name = os.path.splitext(os.path.basename(file_path))[0]
|
|
196
|
+
metadata = parse_filename_metadata(file_path)
|
|
197
|
+
material = classify_material_from_name(name)
|
|
198
|
+
|
|
199
|
+
profile = SampleProfile(
|
|
200
|
+
source=source,
|
|
201
|
+
file_path=file_path,
|
|
202
|
+
name=name,
|
|
203
|
+
key=metadata.get("key"),
|
|
204
|
+
key_confidence=0.5 if metadata.get("key") else 0.0,
|
|
205
|
+
bpm=metadata.get("bpm"),
|
|
206
|
+
bpm_confidence=0.5 if metadata.get("bpm") else 0.0,
|
|
207
|
+
material_type=material,
|
|
208
|
+
material_confidence=0.4, # filename-only is low confidence
|
|
209
|
+
duration_seconds=duration_seconds,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
profile.suggested_mode = suggest_simpler_mode(profile)
|
|
213
|
+
profile.suggested_slice_by = suggest_slice_method(profile)
|
|
214
|
+
profile.suggested_warp_mode = suggest_warp_mode(profile)
|
|
215
|
+
|
|
216
|
+
return profile
|