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
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Corpus Intelligence Layer — structured knowledge from device-knowledge markdown.
|
|
2
|
+
|
|
3
|
+
Parses the device-knowledge corpus (creative-thinking.md, automation-as-music.md,
|
|
4
|
+
effects-*.md, instruments-synths.md, chains-genre.md) into queryable Python
|
|
5
|
+
structures. This gives sound_design critics, wonder_mode engine, and other
|
|
6
|
+
modules access to deep creative knowledge at runtime — not just LLM guidance.
|
|
7
|
+
|
|
8
|
+
Lazy-loaded at first access; pure computation, no I/O after initial load.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ── Data structures ─────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class EmotionalRecipe:
|
|
24
|
+
"""Maps an emotion/quality to specific technical actions."""
|
|
25
|
+
emotion: str = ""
|
|
26
|
+
techniques: list[str] = field(default_factory=list)
|
|
27
|
+
parameters: dict[str, str] = field(default_factory=dict) # param_hint -> value_hint
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class PhysicalModelRecipe:
|
|
32
|
+
"""Maps a physical material (water, metal, glass) to device chains."""
|
|
33
|
+
material: str = ""
|
|
34
|
+
devices: list[str] = field(default_factory=list)
|
|
35
|
+
techniques: list[str] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class AutomationGesture:
|
|
40
|
+
"""A multi-parameter automation macro gesture."""
|
|
41
|
+
name: str = ""
|
|
42
|
+
description: str = ""
|
|
43
|
+
parameters: list[dict] = field(default_factory=list) # [{param, from, to}]
|
|
44
|
+
duration_bars: str = ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class GenreChain:
|
|
49
|
+
"""A complete effect chain recipe for a genre."""
|
|
50
|
+
genre: str = ""
|
|
51
|
+
devices: list[str] = field(default_factory=list)
|
|
52
|
+
parameter_hints: dict[str, str] = field(default_factory=dict)
|
|
53
|
+
description: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class DeviceKnowledge:
|
|
58
|
+
"""Deep knowledge about a specific Ableton device."""
|
|
59
|
+
device_name: str = ""
|
|
60
|
+
category: str = "" # synth, effect, spectral, etc.
|
|
61
|
+
techniques: list[str] = field(default_factory=list)
|
|
62
|
+
sweet_spots: dict[str, str] = field(default_factory=dict)
|
|
63
|
+
anti_patterns: list[str] = field(default_factory=list)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class Corpus:
|
|
68
|
+
"""The full parsed corpus — queryable from any module."""
|
|
69
|
+
emotional_recipes: dict[str, EmotionalRecipe] = field(default_factory=dict)
|
|
70
|
+
physical_models: dict[str, PhysicalModelRecipe] = field(default_factory=dict)
|
|
71
|
+
automation_gestures: dict[str, AutomationGesture] = field(default_factory=dict)
|
|
72
|
+
genre_chains: dict[str, GenreChain] = field(default_factory=dict)
|
|
73
|
+
device_knowledge: dict[str, DeviceKnowledge] = field(default_factory=dict)
|
|
74
|
+
anti_patterns: list[str] = field(default_factory=list)
|
|
75
|
+
|
|
76
|
+
# ── Query methods ───────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
def suggest_for_emotion(self, emotion: str) -> Optional[EmotionalRecipe]:
|
|
79
|
+
"""Find techniques for an emotional quality (warmth, tension, etc.)."""
|
|
80
|
+
emotion_lower = emotion.lower()
|
|
81
|
+
if emotion_lower in self.emotional_recipes:
|
|
82
|
+
return self.emotional_recipes[emotion_lower]
|
|
83
|
+
# Fuzzy: check if emotion is a substring of any key
|
|
84
|
+
for key, recipe in self.emotional_recipes.items():
|
|
85
|
+
if emotion_lower in key or key in emotion_lower:
|
|
86
|
+
return recipe
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def suggest_for_material(self, material: str) -> Optional[PhysicalModelRecipe]:
|
|
90
|
+
"""Find device chains for a physical material quality."""
|
|
91
|
+
return self.physical_models.get(material.lower())
|
|
92
|
+
|
|
93
|
+
def get_gesture(self, name: str) -> Optional[AutomationGesture]:
|
|
94
|
+
"""Get a named automation macro gesture."""
|
|
95
|
+
return self.automation_gestures.get(name.lower())
|
|
96
|
+
|
|
97
|
+
def get_genre_chain(self, genre: str) -> Optional[GenreChain]:
|
|
98
|
+
"""Get a genre-specific effect chain recipe."""
|
|
99
|
+
genre_lower = genre.lower()
|
|
100
|
+
if genre_lower in self.genre_chains:
|
|
101
|
+
return self.genre_chains[genre_lower]
|
|
102
|
+
for key, chain in self.genre_chains.items():
|
|
103
|
+
if genre_lower in key or key in genre_lower:
|
|
104
|
+
return chain
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
def get_device(self, name: str) -> Optional[DeviceKnowledge]:
|
|
108
|
+
"""Get deep knowledge about a specific device."""
|
|
109
|
+
return self.device_knowledge.get(name.lower())
|
|
110
|
+
|
|
111
|
+
def recommend_modulation_for_device(self, device_name: str) -> list[str]:
|
|
112
|
+
"""Given a device, suggest what to modulate based on corpus knowledge."""
|
|
113
|
+
dk = self.get_device(device_name)
|
|
114
|
+
if dk and dk.techniques:
|
|
115
|
+
return dk.techniques[:5]
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
def get_automation_density_for_section(self, section_type: str) -> dict:
|
|
119
|
+
"""Return recommended automation parameter count + rate for a section type."""
|
|
120
|
+
density_map = {
|
|
121
|
+
"intro": {"param_count": "1-2", "rate": "very slow (0.05-0.1 Hz)", "purpose": "establish mood"},
|
|
122
|
+
"build": {"param_count": "3-5", "rate": "accelerating exponential", "purpose": "create tension"},
|
|
123
|
+
"peak": {"param_count": "5-8", "rate": "mixed slow + rhythmic", "purpose": "maximum energy"},
|
|
124
|
+
"breakdown": {"param_count": "1-2", "rate": "very slow, gentle", "purpose": "breathing room"},
|
|
125
|
+
"outro": {"param_count": "1-2", "rate": "gradually reducing", "purpose": "return to start"},
|
|
126
|
+
}
|
|
127
|
+
return density_map.get(section_type.lower(), density_map["peak"])
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ── Parser ──────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _find_corpus_dir() -> Optional[str]:
|
|
134
|
+
"""Find the device-knowledge corpus directory."""
|
|
135
|
+
# Check in the skill references (repo path)
|
|
136
|
+
candidates = [
|
|
137
|
+
os.path.join(os.path.dirname(__file__), "..", "..", "livepilot", "skills",
|
|
138
|
+
"livepilot-core", "references", "device-knowledge"),
|
|
139
|
+
# Also check plugin cache paths
|
|
140
|
+
os.path.expanduser("~/.claude/plugins/livepilot/skills/livepilot-core/references/device-knowledge"),
|
|
141
|
+
]
|
|
142
|
+
for path in candidates:
|
|
143
|
+
if os.path.isdir(path):
|
|
144
|
+
return path
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _parse_emotional_section(text: str) -> dict[str, EmotionalRecipe]:
|
|
149
|
+
"""Parse the emotional-to-technical mapping section."""
|
|
150
|
+
recipes: dict[str, EmotionalRecipe] = {}
|
|
151
|
+
current_emotion = ""
|
|
152
|
+
current_techniques: list[str] = []
|
|
153
|
+
|
|
154
|
+
for line in text.split("\n"):
|
|
155
|
+
line = line.strip()
|
|
156
|
+
# New emotion header: ### Tension & Anxiety
|
|
157
|
+
if line.startswith("### "):
|
|
158
|
+
if current_emotion and current_techniques:
|
|
159
|
+
recipes[current_emotion.lower()] = EmotionalRecipe(
|
|
160
|
+
emotion=current_emotion,
|
|
161
|
+
techniques=current_techniques,
|
|
162
|
+
)
|
|
163
|
+
current_emotion = line[4:].strip()
|
|
164
|
+
current_techniques = []
|
|
165
|
+
elif line.startswith("- **") and current_emotion:
|
|
166
|
+
# Extract technique: - **High-resonance filter sweep** — description
|
|
167
|
+
technique = line.lstrip("- ")
|
|
168
|
+
current_techniques.append(technique)
|
|
169
|
+
|
|
170
|
+
# Don't forget the last one
|
|
171
|
+
if current_emotion and current_techniques:
|
|
172
|
+
recipes[current_emotion.lower()] = EmotionalRecipe(
|
|
173
|
+
emotion=current_emotion,
|
|
174
|
+
techniques=current_techniques,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return recipes
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _parse_physical_models(text: str) -> dict[str, PhysicalModelRecipe]:
|
|
181
|
+
"""Parse the physical world modeling section."""
|
|
182
|
+
models: dict[str, PhysicalModelRecipe] = {}
|
|
183
|
+
current_material = ""
|
|
184
|
+
current_techniques: list[str] = []
|
|
185
|
+
current_devices: list[str] = []
|
|
186
|
+
|
|
187
|
+
for line in text.split("\n"):
|
|
188
|
+
line = line.strip()
|
|
189
|
+
if line.startswith("### "):
|
|
190
|
+
if current_material:
|
|
191
|
+
models[current_material.lower()] = PhysicalModelRecipe(
|
|
192
|
+
material=current_material,
|
|
193
|
+
devices=current_devices,
|
|
194
|
+
techniques=current_techniques,
|
|
195
|
+
)
|
|
196
|
+
current_material = line[4:].strip()
|
|
197
|
+
current_techniques = []
|
|
198
|
+
current_devices = []
|
|
199
|
+
elif line.startswith("- **") and current_material:
|
|
200
|
+
# Extract device name from bold
|
|
201
|
+
match = re.match(r"- \*\*(.+?)\*\*", line)
|
|
202
|
+
if match:
|
|
203
|
+
dev_name = match.group(1)
|
|
204
|
+
current_devices.append(dev_name)
|
|
205
|
+
current_techniques.append(line.lstrip("- "))
|
|
206
|
+
|
|
207
|
+
if current_material:
|
|
208
|
+
models[current_material.lower()] = PhysicalModelRecipe(
|
|
209
|
+
material=current_material,
|
|
210
|
+
devices=current_devices,
|
|
211
|
+
techniques=current_techniques,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return models
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _parse_automation_gestures(text: str) -> dict[str, AutomationGesture]:
|
|
218
|
+
"""Parse the macro gesture section from automation-as-music.md."""
|
|
219
|
+
gestures: dict[str, AutomationGesture] = {}
|
|
220
|
+
current_name = ""
|
|
221
|
+
current_desc = ""
|
|
222
|
+
current_params: list[dict] = []
|
|
223
|
+
|
|
224
|
+
for line in text.split("\n"):
|
|
225
|
+
line = line.strip()
|
|
226
|
+
if line.startswith("### The ") and "Gesture" in line:
|
|
227
|
+
if current_name:
|
|
228
|
+
gestures[current_name.lower()] = AutomationGesture(
|
|
229
|
+
name=current_name,
|
|
230
|
+
description=current_desc,
|
|
231
|
+
parameters=current_params,
|
|
232
|
+
)
|
|
233
|
+
# Extract name: ### The "Open Up" Gesture
|
|
234
|
+
match = re.search(r'"(.+?)"', line)
|
|
235
|
+
current_name = match.group(1) if match else line[4:].strip()
|
|
236
|
+
current_desc = ""
|
|
237
|
+
current_params = []
|
|
238
|
+
elif line.startswith("- **") and current_name:
|
|
239
|
+
# Parameter hint: - **Filter cutoff:** 30% -> 65%
|
|
240
|
+
current_params.append({"raw": line.lstrip("- ")})
|
|
241
|
+
elif line.startswith("- **Musical meaning:**"):
|
|
242
|
+
current_desc = line.replace("- **Musical meaning:**", "").strip()
|
|
243
|
+
|
|
244
|
+
if current_name:
|
|
245
|
+
gestures[current_name.lower()] = AutomationGesture(
|
|
246
|
+
name=current_name,
|
|
247
|
+
description=current_desc,
|
|
248
|
+
parameters=current_params,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return gestures
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _parse_genre_chains(text: str) -> dict[str, GenreChain]:
|
|
255
|
+
"""Parse genre chain recipes from chains-genre.md."""
|
|
256
|
+
chains: dict[str, GenreChain] = {}
|
|
257
|
+
current_genre = ""
|
|
258
|
+
current_devices: list[str] = []
|
|
259
|
+
current_desc = ""
|
|
260
|
+
|
|
261
|
+
for line in text.split("\n"):
|
|
262
|
+
line = line.strip()
|
|
263
|
+
if line.startswith("### ") or line.startswith("## "):
|
|
264
|
+
if current_genre and current_devices:
|
|
265
|
+
chains[current_genre.lower()] = GenreChain(
|
|
266
|
+
genre=current_genre,
|
|
267
|
+
devices=current_devices,
|
|
268
|
+
description=current_desc,
|
|
269
|
+
)
|
|
270
|
+
header = line.lstrip("#").strip()
|
|
271
|
+
current_genre = header
|
|
272
|
+
current_devices = []
|
|
273
|
+
current_desc = ""
|
|
274
|
+
elif line.startswith("- **") and current_genre:
|
|
275
|
+
match = re.match(r"- \*\*(.+?)\*\*", line)
|
|
276
|
+
if match:
|
|
277
|
+
current_devices.append(match.group(1))
|
|
278
|
+
elif line and not line.startswith("-") and not line.startswith("#") and current_genre and not current_desc:
|
|
279
|
+
current_desc = line
|
|
280
|
+
|
|
281
|
+
if current_genre and current_devices:
|
|
282
|
+
chains[current_genre.lower()] = GenreChain(
|
|
283
|
+
genre=current_genre,
|
|
284
|
+
devices=current_devices,
|
|
285
|
+
description=current_desc,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
return chains
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _read_file(path: str) -> str:
|
|
292
|
+
"""Read a file, returning empty string on failure."""
|
|
293
|
+
try:
|
|
294
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
295
|
+
return f.read()
|
|
296
|
+
except (OSError, UnicodeDecodeError):
|
|
297
|
+
return ""
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def load_corpus() -> Corpus:
|
|
301
|
+
"""Load and parse the full device-knowledge corpus."""
|
|
302
|
+
corpus_dir = _find_corpus_dir()
|
|
303
|
+
if not corpus_dir:
|
|
304
|
+
return Corpus() # Empty corpus — no files found
|
|
305
|
+
|
|
306
|
+
corpus = Corpus()
|
|
307
|
+
|
|
308
|
+
# Parse creative-thinking.md
|
|
309
|
+
ct_text = _read_file(os.path.join(corpus_dir, "creative-thinking.md"))
|
|
310
|
+
if ct_text:
|
|
311
|
+
# Split by Part headers
|
|
312
|
+
parts = re.split(r"## Part \d+:", ct_text)
|
|
313
|
+
for part in parts:
|
|
314
|
+
if "Emotional-to-Technical" in part:
|
|
315
|
+
corpus.emotional_recipes = _parse_emotional_section(part)
|
|
316
|
+
elif "Physical World" in part:
|
|
317
|
+
corpus.physical_models = _parse_physical_models(part)
|
|
318
|
+
elif "Anti-Patterns" in part:
|
|
319
|
+
# Extract anti-pattern names
|
|
320
|
+
for line in part.split("\n"):
|
|
321
|
+
if line.strip().startswith("### The ") and "Trap" in line:
|
|
322
|
+
corpus.anti_patterns.append(line.strip().lstrip("# "))
|
|
323
|
+
|
|
324
|
+
# Parse automation-as-music.md
|
|
325
|
+
auto_text = _read_file(os.path.join(corpus_dir, "automation-as-music.md"))
|
|
326
|
+
if auto_text:
|
|
327
|
+
parts = re.split(r"## Part \d+:", auto_text)
|
|
328
|
+
for part in parts:
|
|
329
|
+
if "Multi-Parameter" in part or "Macro Gesture" in part:
|
|
330
|
+
corpus.automation_gestures = _parse_automation_gestures(part)
|
|
331
|
+
|
|
332
|
+
# Parse chains-genre.md
|
|
333
|
+
genre_text = _read_file(os.path.join(corpus_dir, "chains-genre.md"))
|
|
334
|
+
if genre_text:
|
|
335
|
+
corpus.genre_chains = _parse_genre_chains(genre_text)
|
|
336
|
+
|
|
337
|
+
# Parse instrument and effect knowledge files
|
|
338
|
+
for filename in ["instruments-synths.md", "effects-distortion.md",
|
|
339
|
+
"effects-space.md", "effects-spectral.md"]:
|
|
340
|
+
file_text = _read_file(os.path.join(corpus_dir, filename))
|
|
341
|
+
if file_text:
|
|
342
|
+
current_device = ""
|
|
343
|
+
current_techniques: list[str] = []
|
|
344
|
+
for line in file_text.split("\n"):
|
|
345
|
+
line = line.strip()
|
|
346
|
+
if line.startswith("## ") or line.startswith("### "):
|
|
347
|
+
if current_device and current_techniques:
|
|
348
|
+
corpus.device_knowledge[current_device.lower()] = DeviceKnowledge(
|
|
349
|
+
device_name=current_device,
|
|
350
|
+
category=filename.replace(".md", ""),
|
|
351
|
+
techniques=current_techniques,
|
|
352
|
+
)
|
|
353
|
+
current_device = line.lstrip("#").strip()
|
|
354
|
+
current_techniques = []
|
|
355
|
+
elif line.startswith("- **") and current_device:
|
|
356
|
+
current_techniques.append(line.lstrip("- "))
|
|
357
|
+
if current_device and current_techniques:
|
|
358
|
+
corpus.device_knowledge[current_device.lower()] = DeviceKnowledge(
|
|
359
|
+
device_name=current_device,
|
|
360
|
+
category=filename.replace(".md", ""),
|
|
361
|
+
techniques=current_techniques,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
return corpus
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ── Module-level lazy singleton ─────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
_corpus_instance: Optional[Corpus] = None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def get_corpus() -> Corpus:
|
|
373
|
+
"""Get the global corpus instance (lazy-loaded on first call)."""
|
|
374
|
+
global _corpus_instance
|
|
375
|
+
if _corpus_instance is None:
|
|
376
|
+
_corpus_instance = load_corpus()
|
|
377
|
+
return _corpus_instance
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Device Forge — programmatic M4L device generation."""
|