livepilot 1.9.22 → 1.9.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.mcpbignore +40 -0
  3. package/AGENTS.md +3 -3
  4. package/CHANGELOG.md +84 -0
  5. package/CONTRIBUTING.md +1 -1
  6. package/README.md +141 -72
  7. package/bin/livepilot.js +135 -0
  8. package/livepilot/.Codex-plugin/plugin.json +2 -2
  9. package/livepilot/.claude-plugin/plugin.json +2 -2
  10. package/livepilot/agents/livepilot-producer/AGENT.md +13 -0
  11. package/livepilot/commands/arrange.md +42 -23
  12. package/livepilot/commands/mix.md +34 -19
  13. package/livepilot/commands/perform.md +31 -19
  14. package/livepilot/commands/sounddesign.md +38 -25
  15. package/livepilot/skills/livepilot-arrangement/SKILL.md +2 -1
  16. package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +2 -2
  17. package/livepilot/skills/livepilot-core/SKILL.md +60 -4
  18. package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +11 -11
  19. package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +25 -25
  20. package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +21 -21
  21. package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +13 -13
  22. package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +13 -13
  23. package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +5 -5
  24. package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +16 -16
  25. package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +40 -40
  26. package/livepilot/skills/livepilot-core/references/m4l-devices.md +3 -3
  27. package/livepilot/skills/livepilot-core/references/overview.md +4 -4
  28. package/livepilot/skills/livepilot-evaluation/SKILL.md +12 -8
  29. package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +2 -2
  30. package/livepilot/skills/livepilot-mix-engine/SKILL.md +1 -1
  31. package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +2 -2
  32. package/livepilot/skills/livepilot-mixing/SKILL.md +3 -1
  33. package/livepilot/skills/livepilot-notes/SKILL.md +2 -1
  34. package/livepilot/skills/livepilot-release/SKILL.md +29 -15
  35. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +2 -2
  36. package/livepilot/skills/livepilot-wonder/SKILL.md +62 -0
  37. package/livepilot.mcpb +0 -0
  38. package/manifest.json +91 -0
  39. package/mcp_server/__init__.py +1 -1
  40. package/mcp_server/creative_constraints/__init__.py +6 -0
  41. package/mcp_server/creative_constraints/engine.py +277 -0
  42. package/mcp_server/creative_constraints/models.py +75 -0
  43. package/mcp_server/creative_constraints/tools.py +341 -0
  44. package/mcp_server/experiment/__init__.py +6 -0
  45. package/mcp_server/experiment/engine.py +213 -0
  46. package/mcp_server/experiment/models.py +120 -0
  47. package/mcp_server/experiment/tools.py +263 -0
  48. package/mcp_server/hook_hunter/__init__.py +5 -0
  49. package/mcp_server/hook_hunter/analyzer.py +365 -0
  50. package/mcp_server/hook_hunter/models.py +58 -0
  51. package/mcp_server/hook_hunter/tools.py +588 -0
  52. package/mcp_server/memory/taste_graph.py +328 -0
  53. package/mcp_server/memory/tools.py +99 -0
  54. package/mcp_server/mix_engine/critics.py +2 -2
  55. package/mcp_server/mix_engine/models.py +1 -1
  56. package/mcp_server/mix_engine/state_builder.py +2 -2
  57. package/mcp_server/musical_intelligence/__init__.py +8 -0
  58. package/mcp_server/musical_intelligence/detectors.py +434 -0
  59. package/mcp_server/musical_intelligence/phrase_critic.py +163 -0
  60. package/mcp_server/musical_intelligence/tools.py +224 -0
  61. package/mcp_server/persistence/__init__.py +1 -0
  62. package/mcp_server/persistence/base_store.py +82 -0
  63. package/mcp_server/persistence/project_store.py +106 -0
  64. package/mcp_server/persistence/taste_store.py +122 -0
  65. package/mcp_server/preview_studio/__init__.py +5 -0
  66. package/mcp_server/preview_studio/engine.py +280 -0
  67. package/mcp_server/preview_studio/models.py +74 -0
  68. package/mcp_server/preview_studio/tools.py +466 -0
  69. package/mcp_server/runtime/capability.py +66 -0
  70. package/mcp_server/runtime/capability_probe.py +118 -0
  71. package/mcp_server/runtime/execution_router.py +139 -0
  72. package/mcp_server/runtime/remote_commands.py +82 -0
  73. package/mcp_server/runtime/session_kernel.py +96 -0
  74. package/mcp_server/runtime/tools.py +90 -1
  75. package/mcp_server/semantic_moves/__init__.py +13 -0
  76. package/mcp_server/semantic_moves/compiler.py +116 -0
  77. package/mcp_server/semantic_moves/mix_compilers.py +291 -0
  78. package/mcp_server/semantic_moves/mix_moves.py +157 -0
  79. package/mcp_server/semantic_moves/models.py +46 -0
  80. package/mcp_server/semantic_moves/performance_compilers.py +208 -0
  81. package/mcp_server/semantic_moves/performance_moves.py +81 -0
  82. package/mcp_server/semantic_moves/registry.py +32 -0
  83. package/mcp_server/semantic_moves/resolvers.py +126 -0
  84. package/mcp_server/semantic_moves/sound_design_compilers.py +266 -0
  85. package/mcp_server/semantic_moves/sound_design_moves.py +78 -0
  86. package/mcp_server/semantic_moves/tools.py +205 -0
  87. package/mcp_server/semantic_moves/transition_compilers.py +222 -0
  88. package/mcp_server/semantic_moves/transition_moves.py +76 -0
  89. package/mcp_server/server.py +10 -0
  90. package/mcp_server/services/__init__.py +1 -0
  91. package/mcp_server/services/motif_service.py +67 -0
  92. package/mcp_server/session_continuity/__init__.py +6 -0
  93. package/mcp_server/session_continuity/models.py +86 -0
  94. package/mcp_server/session_continuity/tools.py +230 -0
  95. package/mcp_server/session_continuity/tracker.py +263 -0
  96. package/mcp_server/song_brain/__init__.py +6 -0
  97. package/mcp_server/song_brain/builder.py +504 -0
  98. package/mcp_server/song_brain/models.py +136 -0
  99. package/mcp_server/song_brain/tools.py +312 -0
  100. package/mcp_server/stuckness_detector/__init__.py +5 -0
  101. package/mcp_server/stuckness_detector/detector.py +400 -0
  102. package/mcp_server/stuckness_detector/models.py +66 -0
  103. package/mcp_server/stuckness_detector/tools.py +195 -0
  104. package/mcp_server/tools/_conductor.py +104 -6
  105. package/mcp_server/tools/analyzer.py +1 -1
  106. package/mcp_server/tools/devices.py +34 -0
  107. package/mcp_server/wonder_mode/__init__.py +6 -0
  108. package/mcp_server/wonder_mode/diagnosis.py +84 -0
  109. package/mcp_server/wonder_mode/engine.py +493 -0
  110. package/mcp_server/wonder_mode/session.py +114 -0
  111. package/mcp_server/wonder_mode/tools.py +290 -0
  112. package/package.json +2 -2
  113. package/remote_script/LivePilot/__init__.py +1 -1
  114. package/remote_script/LivePilot/browser.py +4 -1
  115. package/remote_script/LivePilot/devices.py +29 -0
  116. package/remote_script/LivePilot/tracks.py +11 -4
  117. package/scripts/generate_tool_catalog.py +131 -0
  118. package/scripts/sync_metadata.py +132 -0
@@ -0,0 +1,222 @@
1
+ """Compilers for transition-domain semantic moves."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .compiler import CompiledPlan, CompiledStep, register_compiler
6
+ from .models import SemanticMove
7
+ from . import resolvers
8
+
9
+
10
+ def _compile_increase_forward_motion(move: SemanticMove, kernel: dict) -> CompiledPlan:
11
+ """Compile 'increase_forward_motion': rising filter + rhythm push."""
12
+ steps = []
13
+ descriptions = []
14
+
15
+ melodic = resolvers.find_tracks_by_role(kernel, ["chords", "lead", "pad"])
16
+ drums = resolvers.find_tracks_by_role(kernel, ["drums", "percussion"])
17
+
18
+ for mt in melodic[:1]:
19
+ steps.append(CompiledStep(
20
+ tool="apply_automation_shape",
21
+ params={
22
+ "track_index": mt["index"],
23
+ "clip_index": 0,
24
+ "parameter_type": "device",
25
+ "device_index": 0,
26
+ "parameter_index": 0,
27
+ "curve_type": "exponential",
28
+ "start": 0.2,
29
+ "end": 0.7,
30
+ "duration": 4,
31
+ },
32
+ description=f"Rising filter sweep on {mt['name']} over 4 bars",
33
+ ))
34
+ descriptions.append(f"Rising filter on {mt['name']}")
35
+
36
+ for dt in drums[:1]:
37
+ steps.append(CompiledStep(
38
+ tool="set_track_volume",
39
+ params={"track_index": dt["index"], "volume": 0.75},
40
+ description=f"Push {dt['name']} to 0.75 for forward drive",
41
+ ))
42
+ descriptions.append(f"Push {dt['name']} forward")
43
+
44
+ for mt in melodic[:1]:
45
+ steps.append(CompiledStep(
46
+ tool="set_track_send",
47
+ params={"track_index": mt["index"], "send_index": 0, "value": 0.30},
48
+ description=f"Build reverb wash on {mt['name']}",
49
+ ))
50
+ descriptions.append(f"Reverb wash on {mt['name']}")
51
+
52
+ steps.append(CompiledStep(
53
+ tool="get_track_meters",
54
+ params={"include_stereo": True},
55
+ description="Verify forward momentum — energy increasing",
56
+ ))
57
+
58
+ return CompiledPlan(
59
+ move_id=move.move_id,
60
+ intent=move.intent,
61
+ steps=steps,
62
+ risk_level="low",
63
+ summary="; ".join(descriptions) if descriptions else "No melodic tracks for motion",
64
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
65
+ )
66
+
67
+
68
+ def _compile_open_chorus(move: SemanticMove, kernel: dict) -> CompiledPlan:
69
+ """Compile 'open_chorus': maximize width, energy, brightness."""
70
+ steps = []
71
+ descriptions = []
72
+
73
+ melodic = resolvers.find_tracks_by_role(kernel, ["chords", "lead", "pad"])
74
+ drums = resolvers.find_tracks_by_role(kernel, ["drums"])
75
+
76
+ # Push all melodic tracks
77
+ for mt in melodic:
78
+ steps.append(CompiledStep(
79
+ tool="set_track_volume",
80
+ params={"track_index": mt["index"], "volume": 0.75},
81
+ description=f"Push {mt['name']} to 0.75 for chorus energy",
82
+ ))
83
+ descriptions.append(f"Push {mt['name']}")
84
+
85
+ # Widen chords
86
+ for ct in resolvers.find_tracks_by_role(kernel, ["chords"])[:1]:
87
+ steps.append(CompiledStep(
88
+ tool="set_track_pan",
89
+ params={"track_index": ct["index"], "pan": -0.30},
90
+ description=f"Pan {ct['name']} left for width",
91
+ ))
92
+
93
+ for lt in resolvers.find_tracks_by_role(kernel, ["lead"])[:1]:
94
+ steps.append(CompiledStep(
95
+ tool="set_track_pan",
96
+ params={"track_index": lt["index"], "pan": 0.25},
97
+ description=f"Pan {lt['name']} right for width",
98
+ ))
99
+
100
+ # Increase sends for spaciousness
101
+ for mt in melodic[:2]:
102
+ steps.append(CompiledStep(
103
+ tool="set_track_send",
104
+ params={"track_index": mt["index"], "send_index": 0, "value": 0.30},
105
+ description=f"Add reverb space to {mt['name']}",
106
+ ))
107
+
108
+ descriptions.append("Widen stereo + add space")
109
+
110
+ steps.append(CompiledStep(
111
+ tool="get_track_meters",
112
+ params={"include_stereo": True},
113
+ description="Verify chorus energy — all tracks at high level",
114
+ ))
115
+
116
+ return CompiledPlan(
117
+ move_id=move.move_id,
118
+ intent=move.intent,
119
+ steps=steps,
120
+ risk_level="medium",
121
+ summary="; ".join(descriptions) if descriptions else "No tracks for chorus",
122
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
123
+ )
124
+
125
+
126
+ def _compile_create_breakdown(move: SemanticMove, kernel: dict) -> CompiledPlan:
127
+ """Compile 'create_breakdown': strip to minimal elements."""
128
+ steps = []
129
+ descriptions = []
130
+
131
+ drums = resolvers.find_tracks_by_role(kernel, ["drums", "percussion"])
132
+ bass = resolvers.find_tracks_by_role(kernel, ["bass"])
133
+ pads = resolvers.find_tracks_by_role(kernel, ["pad"])
134
+
135
+ for dt in drums:
136
+ steps.append(CompiledStep(
137
+ tool="set_track_volume",
138
+ params={"track_index": dt["index"], "volume": 0.25},
139
+ description=f"Strip {dt['name']} to 0.25 for breakdown",
140
+ ))
141
+ descriptions.append(f"Strip {dt['name']}")
142
+
143
+ for bt in bass[:1]:
144
+ steps.append(CompiledStep(
145
+ tool="set_track_volume",
146
+ params={"track_index": bt["index"], "volume": 0.30},
147
+ description=f"Reduce {bt['name']} to 0.30",
148
+ ))
149
+ descriptions.append(f"Reduce {bt['name']}")
150
+
151
+ # Increase reverb on remaining pads for depth
152
+ for pt in pads[:1]:
153
+ steps.append(CompiledStep(
154
+ tool="set_track_send",
155
+ params={"track_index": pt["index"], "send_index": 0, "value": 0.50},
156
+ description=f"Deep reverb on {pt['name']} for breakdown atmosphere",
157
+ ))
158
+ descriptions.append(f"Deep reverb on {pt['name']}")
159
+
160
+ steps.append(CompiledStep(
161
+ tool="get_track_meters",
162
+ params={"include_stereo": True},
163
+ description="Verify breakdown — energy low, atmosphere present",
164
+ ))
165
+
166
+ return CompiledPlan(
167
+ move_id=move.move_id,
168
+ intent=move.intent,
169
+ steps=steps,
170
+ risk_level="medium",
171
+ summary="; ".join(descriptions) if descriptions else "No tracks for breakdown",
172
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
173
+ )
174
+
175
+
176
+ def _compile_bridge_sections(move: SemanticMove, kernel: dict) -> CompiledPlan:
177
+ """Compile 'bridge_sections': gentle filter motion + volume crossfade."""
178
+ steps = []
179
+ descriptions = []
180
+
181
+ melodic = resolvers.find_tracks_by_role(kernel, ["chords", "lead", "pad"])
182
+
183
+ for mt in melodic[:1]:
184
+ steps.append(CompiledStep(
185
+ tool="apply_automation_shape",
186
+ params={
187
+ "track_index": mt["index"],
188
+ "clip_index": 0,
189
+ "parameter_type": "send",
190
+ "send_index": 0,
191
+ "curve_type": "cosine",
192
+ "center": 0.25,
193
+ "amplitude": 0.15,
194
+ "duration": 4,
195
+ "density": 8,
196
+ },
197
+ description=f"Gentle reverb motion on {mt['name']} across bridge",
198
+ ))
199
+ descriptions.append(f"Bridge motion on {mt['name']}")
200
+
201
+ steps.append(CompiledStep(
202
+ tool="get_track_meters",
203
+ params={"include_stereo": True},
204
+ description="Verify smooth bridge — no dropouts",
205
+ ))
206
+
207
+ return CompiledPlan(
208
+ move_id=move.move_id,
209
+ intent=move.intent,
210
+ steps=steps,
211
+ risk_level="low",
212
+ summary="; ".join(descriptions) if descriptions else "No tracks for bridge",
213
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
214
+ )
215
+
216
+
217
+ # ── Register ────────────────────────────────────────────────────────────────
218
+
219
+ register_compiler("increase_forward_motion", _compile_increase_forward_motion)
220
+ register_compiler("open_chorus", _compile_open_chorus)
221
+ register_compiler("create_breakdown", _compile_create_breakdown)
222
+ register_compiler("bridge_sections", _compile_bridge_sections)
@@ -0,0 +1,76 @@
1
+ """Transition-domain semantic moves — musical intents for section transitions."""
2
+
3
+ from .models import SemanticMove
4
+ from .registry import register
5
+
6
+ INCREASE_FORWARD_MOTION = SemanticMove(
7
+ move_id="increase_forward_motion",
8
+ family="transition",
9
+ intent="Increase forward motion and momentum toward the next section",
10
+ targets={"motion": 0.5, "energy": 0.3, "tension": 0.2},
11
+ protect={"clarity": 0.6},
12
+ risk_level="low",
13
+ compile_plan=[
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
+ ],
18
+ verification_plan=[
19
+ {"tool": "get_track_meters", "check": "energy increasing, all tracks alive", "backend": "remote_command"},
20
+ ],
21
+ )
22
+
23
+ OPEN_CHORUS = SemanticMove(
24
+ move_id="open_chorus",
25
+ family="transition",
26
+ intent="Open into a chorus — maximize width, energy, and brightness",
27
+ targets={"energy": 0.4, "width": 0.3, "contrast": 0.3},
28
+ protect={"clarity": 0.6, "cohesion": 0.5},
29
+ risk_level="medium",
30
+ compile_plan=[
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
+ ],
35
+ verification_plan=[
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
+ ],
39
+ )
40
+
41
+ CREATE_BREAKDOWN = SemanticMove(
42
+ move_id="create_breakdown",
43
+ family="transition",
44
+ intent="Create a breakdown — strip to minimal elements for contrast before rebuild",
45
+ targets={"contrast": 0.5, "depth": 0.3, "clarity": 0.2},
46
+ protect={"cohesion": 0.5},
47
+ risk_level="medium",
48
+ compile_plan=[
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
+ ],
53
+ verification_plan=[
54
+ {"tool": "get_track_meters", "check": "energy significantly reduced, at least one element still prominent", "backend": "remote_command"},
55
+ ],
56
+ )
57
+
58
+ BRIDGE_SECTIONS = SemanticMove(
59
+ move_id="bridge_sections",
60
+ family="transition",
61
+ intent="Bridge between two sections with a transitional passage — filter sweep + density shift",
62
+ targets={"motion": 0.4, "contrast": 0.3, "cohesion": 0.3},
63
+ protect={"clarity": 0.6},
64
+ risk_level="low",
65
+ compile_plan=[
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
+ ],
69
+ verification_plan=[
70
+ {"tool": "get_track_meters", "check": "smooth level transition, no dropouts", "backend": "remote_command"},
71
+ ],
72
+ )
73
+
74
+ # Register all transition moves
75
+ for _move in [INCREASE_FORWARD_MOTION, OPEN_CHORUS, CREATE_BREAKDOWN, BRIDGE_SECTIONS]:
76
+ register(_move)
@@ -130,6 +130,16 @@ from .reference_engine import tools as reference_tools # noqa: F401, E402
130
130
  from .translation_engine import tools as translation_tools # noqa: F401, E402
131
131
  from .performance_engine import tools as performance_tools # noqa: F401, E402
132
132
  from .runtime import safety_tools # noqa: F401, E402
133
+ from .semantic_moves import tools as semantic_move_tools # noqa: F401, E402
134
+ from .experiment import tools as experiment_tools # noqa: F401, E402
135
+ from .musical_intelligence import tools as musical_intel_tools # noqa: F401, E402
136
+ from .song_brain import tools as song_brain_tools # noqa: F401, E402
137
+ from .preview_studio import tools as preview_studio_tools # noqa: F401, E402
138
+ from .hook_hunter import tools as hook_hunter_tools # noqa: F401, E402
139
+ from .stuckness_detector import tools as stuckness_tools # noqa: F401, E402
140
+ from .wonder_mode import tools as wonder_mode_tools # noqa: F401, E402
141
+ from .session_continuity import tools as session_cont_tools # noqa: F401, E402
142
+ from .creative_constraints import tools as constraints_tools # noqa: F401, E402
133
143
 
134
144
 
135
145
  # ---------------------------------------------------------------------------
@@ -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
@@ -0,0 +1,6 @@
1
+ """Session Continuity — collaborative memory for Stage 2.
2
+
3
+ Tracks unresolved creative goals, abandoned directions, what has been
4
+ tried, and what the user loved or rejected. Separates taste (cross-session)
5
+ from identity (in-song) for better ranking.
6
+ """
@@ -0,0 +1,86 @@
1
+ """Session Continuity data models — pure dataclasses, zero I/O."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from dataclasses import asdict, dataclass, field
7
+ from typing import Optional
8
+
9
+
10
+ @dataclass
11
+ class CreativeThread:
12
+ """An unresolved creative goal or direction."""
13
+
14
+ thread_id: str = ""
15
+ description: str = ""
16
+ domain: str = "" # "arrangement", "sound_design", "mix", "harmony", "identity"
17
+ status: str = "open" # "open", "resolved", "abandoned", "stale"
18
+ priority: float = 0.5
19
+ created_at_ms: int = 0
20
+ last_touched_ms: int = 0
21
+
22
+ def to_dict(self) -> dict:
23
+ return asdict(self)
24
+
25
+ @property
26
+ def is_stale(self) -> bool:
27
+ """A thread is stale if untouched for >30 minutes."""
28
+ now = int(time.time() * 1000)
29
+ return (now - self.last_touched_ms) > 30 * 60 * 1000 if self.last_touched_ms else False
30
+
31
+
32
+ @dataclass
33
+ class TurnResolution:
34
+ """What happened in a single creative turn."""
35
+
36
+ turn_id: str = ""
37
+ request_text: str = ""
38
+ outcome: str = "" # "accepted", "rejected", "modified", "undone"
39
+ move_applied: str = ""
40
+ identity_effect: str = ""
41
+ user_sentiment: str = "" # "loved", "liked", "neutral", "disliked", "hated"
42
+ timestamp_ms: int = 0
43
+
44
+ def to_dict(self) -> dict:
45
+ return asdict(self)
46
+
47
+
48
+ @dataclass
49
+ class SessionStory:
50
+ """The narrative of the current session."""
51
+
52
+ song_id: str = ""
53
+ identity_summary: str = ""
54
+ what_changed_last: str = ""
55
+ what_still_feels_open: list[str] = field(default_factory=list)
56
+ threads: list[CreativeThread] = field(default_factory=list)
57
+ turns: list[TurnResolution] = field(default_factory=list)
58
+ mood_arc: list[str] = field(default_factory=list) # sequence of moods
59
+
60
+ def to_dict(self) -> dict:
61
+ return {
62
+ "song_id": self.song_id,
63
+ "identity_summary": self.identity_summary,
64
+ "what_changed_last": self.what_changed_last,
65
+ "what_still_feels_open": self.what_still_feels_open,
66
+ "threads": [t.to_dict() for t in self.threads if t.status == "open"],
67
+ "recent_turns": [t.to_dict() for t in self.turns[-5:]],
68
+ "mood_arc": self.mood_arc[-10:],
69
+ "total_turns": len(self.turns),
70
+ }
71
+
72
+
73
+ @dataclass
74
+ class TasteIdentityRanking:
75
+ """Result of ranking candidates with separated taste and identity."""
76
+
77
+ candidate_id: str = ""
78
+ taste_score: float = 0.0
79
+ identity_score: float = 0.0
80
+ composite_score: float = 0.0
81
+ taste_explanation: str = ""
82
+ identity_explanation: str = ""
83
+ recommendation: str = ""
84
+
85
+ def to_dict(self) -> dict:
86
+ return asdict(self)