livepilot 1.10.4 → 1.10.6
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 +148 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +6 -6
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +4 -4
- package/livepilot/skills/livepilot-core/references/overview.md +3 -3
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +5 -5
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +12 -1
- package/manifest.json +3 -3
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/composer/sample_resolver.py +10 -6
- package/mcp_server/composer/tools.py +10 -6
- package/mcp_server/connection.py +6 -1
- package/mcp_server/creative_constraints/tools.py +9 -8
- package/mcp_server/experiment/engine.py +9 -5
- package/mcp_server/experiment/tools.py +9 -9
- package/mcp_server/hook_hunter/tools.py +14 -9
- package/mcp_server/m4l_bridge.py +11 -0
- package/mcp_server/memory/taste_graph.py +7 -2
- package/mcp_server/mix_engine/tools.py +8 -3
- package/mcp_server/musical_intelligence/tools.py +15 -10
- package/mcp_server/performance_engine/tools.py +6 -2
- package/mcp_server/preview_studio/tools.py +21 -15
- package/mcp_server/project_brain/tools.py +18 -10
- package/mcp_server/reference_engine/tools.py +7 -5
- package/mcp_server/runtime/capability_probe.py +10 -4
- package/mcp_server/runtime/tools.py +8 -2
- package/mcp_server/sample_engine/tools.py +394 -33
- package/mcp_server/semantic_moves/tools.py +5 -1
- package/mcp_server/server.py +10 -9
- package/mcp_server/services/motif_service.py +9 -3
- package/mcp_server/session_continuity/tools.py +7 -3
- package/mcp_server/session_continuity/tracker.py +9 -8
- package/mcp_server/song_brain/tools.py +17 -12
- package/mcp_server/splice_client/client.py +19 -6
- package/mcp_server/stuckness_detector/tools.py +8 -5
- package/mcp_server/tools/_agent_os_engine/__init__.py +52 -0
- package/mcp_server/tools/_agent_os_engine/critics.py +134 -0
- package/mcp_server/tools/_agent_os_engine/evaluation.py +206 -0
- package/mcp_server/tools/_agent_os_engine/models.py +132 -0
- package/mcp_server/tools/_agent_os_engine/taste.py +192 -0
- package/mcp_server/tools/_agent_os_engine/techniques.py +161 -0
- package/mcp_server/tools/_agent_os_engine/world_model.py +170 -0
- package/mcp_server/tools/_composition_engine/__init__.py +67 -0
- package/mcp_server/tools/_composition_engine/analysis.py +174 -0
- package/mcp_server/tools/_composition_engine/critics.py +522 -0
- package/mcp_server/tools/_composition_engine/gestures.py +230 -0
- package/mcp_server/tools/_composition_engine/harmony.py +70 -0
- package/mcp_server/tools/_composition_engine/models.py +193 -0
- package/mcp_server/tools/_composition_engine/sections.py +371 -0
- package/mcp_server/tools/_perception_engine.py +18 -11
- package/mcp_server/tools/agent_os.py +23 -15
- package/mcp_server/tools/analyzer.py +166 -7
- package/mcp_server/tools/automation.py +6 -1
- package/mcp_server/tools/composition.py +25 -16
- package/mcp_server/tools/devices.py +10 -6
- package/mcp_server/tools/motif.py +7 -2
- package/mcp_server/tools/planner.py +6 -2
- package/mcp_server/tools/research.py +13 -10
- package/mcp_server/transition_engine/tools.py +6 -1
- package/mcp_server/translation_engine/tools.py +8 -6
- package/mcp_server/wonder_mode/engine.py +8 -3
- package/mcp_server/wonder_mode/tools.py +29 -21
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/requirements.txt +6 -0
- package/livepilot.mcpb +0 -0
- package/mcp_server/tools/_agent_os_engine.py +0 -947
- package/mcp_server/tools/_composition_engine.py +0 -1530
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"""Part of the _composition_engine package — extracted from the single-file engine.
|
|
2
|
+
|
|
3
|
+
Pure-computation core, no external deps. Callers should import from the package
|
|
4
|
+
facade (e.g. `from mcp_server.tools._composition_engine import X`), which
|
|
5
|
+
re-exports everything from these sub-modules.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import asdict, dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
from .models import SectionType, RoleType, SectionNode, PhraseUnit, RoleNode, CompositionIssue, HarmonyField
|
|
16
|
+
|
|
17
|
+
def run_form_critic(sections: list[SectionNode]) -> list[CompositionIssue]:
|
|
18
|
+
"""Critique the overall form/structure of the arrangement."""
|
|
19
|
+
issues = []
|
|
20
|
+
if not sections:
|
|
21
|
+
issues.append(CompositionIssue(
|
|
22
|
+
issue_type="no_sections",
|
|
23
|
+
critic="form",
|
|
24
|
+
severity=0.8,
|
|
25
|
+
confidence=1.0,
|
|
26
|
+
evidence="No sections detected in the arrangement",
|
|
27
|
+
recommended_moves=["create_sections", "add_scene_structure"],
|
|
28
|
+
))
|
|
29
|
+
return issues
|
|
30
|
+
|
|
31
|
+
# 1. Too few sections for a full track
|
|
32
|
+
if len(sections) < 3:
|
|
33
|
+
issues.append(CompositionIssue(
|
|
34
|
+
issue_type="too_few_sections",
|
|
35
|
+
critic="form",
|
|
36
|
+
severity=0.6,
|
|
37
|
+
confidence=0.8,
|
|
38
|
+
evidence=f"Only {len(sections)} section(s) detected",
|
|
39
|
+
recommended_moves=["section_expansion", "add_contrast_section"],
|
|
40
|
+
))
|
|
41
|
+
|
|
42
|
+
# 2. No energy arc (all sections similar energy)
|
|
43
|
+
if len(sections) >= 2:
|
|
44
|
+
energies = [s.energy for s in sections]
|
|
45
|
+
energy_range = max(energies) - min(energies)
|
|
46
|
+
if energy_range < 0.15:
|
|
47
|
+
issues.append(CompositionIssue(
|
|
48
|
+
issue_type="flat_energy_arc",
|
|
49
|
+
critic="form",
|
|
50
|
+
severity=0.7,
|
|
51
|
+
confidence=0.75,
|
|
52
|
+
evidence=f"Energy range: {energy_range:.2f} (all sections similar density)",
|
|
53
|
+
recommended_moves=["vary_track_count", "add_build_section", "create_breakdown"],
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
# 3. No contrast between adjacent sections
|
|
57
|
+
for i in range(1, len(sections)):
|
|
58
|
+
prev = sections[i - 1]
|
|
59
|
+
curr = sections[i]
|
|
60
|
+
energy_diff = abs(curr.energy - prev.energy)
|
|
61
|
+
density_diff = abs(curr.density - prev.density)
|
|
62
|
+
if energy_diff < 0.1 and density_diff < 0.1:
|
|
63
|
+
issues.append(CompositionIssue(
|
|
64
|
+
issue_type="no_adjacent_contrast",
|
|
65
|
+
critic="form",
|
|
66
|
+
severity=0.5,
|
|
67
|
+
confidence=0.7,
|
|
68
|
+
scope={"sections": [prev.section_id, curr.section_id]},
|
|
69
|
+
evidence=f"Sections '{prev.name or prev.section_id}' and '{curr.name or curr.section_id}' have similar energy/density",
|
|
70
|
+
recommended_moves=["thin_one_section", "add_element_to_one", "vary_automation"],
|
|
71
|
+
))
|
|
72
|
+
|
|
73
|
+
# 4. First section too dense (reveals too much too early)
|
|
74
|
+
if sections and sections[0].density > 0.7:
|
|
75
|
+
issues.append(CompositionIssue(
|
|
76
|
+
issue_type="intro_too_dense",
|
|
77
|
+
critic="form",
|
|
78
|
+
severity=0.5,
|
|
79
|
+
confidence=0.65,
|
|
80
|
+
scope={"section_id": sections[0].section_id},
|
|
81
|
+
evidence=f"First section density: {sections[0].density:.2f} (reveals too much)",
|
|
82
|
+
recommended_moves=["remove_elements_from_intro", "defer_reveal"],
|
|
83
|
+
))
|
|
84
|
+
|
|
85
|
+
return issues
|
|
86
|
+
|
|
87
|
+
def run_section_identity_critic(
|
|
88
|
+
sections: list[SectionNode],
|
|
89
|
+
roles: list[RoleNode],
|
|
90
|
+
) -> list[CompositionIssue]:
|
|
91
|
+
"""Critique individual section identity and clarity."""
|
|
92
|
+
issues = []
|
|
93
|
+
|
|
94
|
+
for section in sections:
|
|
95
|
+
section_roles = [r for r in roles if r.section_id == section.section_id]
|
|
96
|
+
foreground_count = sum(1 for r in section_roles if r.foreground)
|
|
97
|
+
|
|
98
|
+
# 1. No clear foreground element
|
|
99
|
+
if foreground_count == 0 and len(section_roles) > 0:
|
|
100
|
+
issues.append(CompositionIssue(
|
|
101
|
+
issue_type="no_foreground",
|
|
102
|
+
critic="section_identity",
|
|
103
|
+
severity=0.6,
|
|
104
|
+
confidence=0.70,
|
|
105
|
+
scope={"section_id": section.section_id},
|
|
106
|
+
evidence=f"Section '{section.name or section.section_id}' has {len(section_roles)} tracks but none inferred as foreground",
|
|
107
|
+
recommended_moves=["assign_lead_role", "add_hook_element"],
|
|
108
|
+
))
|
|
109
|
+
|
|
110
|
+
# 2. Too many foreground voices
|
|
111
|
+
if foreground_count > 3:
|
|
112
|
+
issues.append(CompositionIssue(
|
|
113
|
+
issue_type="too_many_foregrounds",
|
|
114
|
+
critic="section_identity",
|
|
115
|
+
severity=0.5,
|
|
116
|
+
confidence=0.65,
|
|
117
|
+
scope={"section_id": section.section_id},
|
|
118
|
+
evidence=f"Section has {foreground_count} foreground elements (max recommended: 3)",
|
|
119
|
+
recommended_moves=["background_some_elements", "thin_section", "use_automation_to_rotate"],
|
|
120
|
+
))
|
|
121
|
+
|
|
122
|
+
# 3. Section type mismatch — e.g., "chorus" less energetic than "verse"
|
|
123
|
+
# (Compare against adjacent sections of different type)
|
|
124
|
+
|
|
125
|
+
# Cross-section type check
|
|
126
|
+
choruses = [s for s in sections if s.section_type == SectionType.CHORUS]
|
|
127
|
+
verses = [s for s in sections if s.section_type == SectionType.VERSE]
|
|
128
|
+
if choruses and verses:
|
|
129
|
+
chorus_energy = max(s.energy for s in choruses)
|
|
130
|
+
verse_energy = max(s.energy for s in verses)
|
|
131
|
+
if chorus_energy <= verse_energy:
|
|
132
|
+
issues.append(CompositionIssue(
|
|
133
|
+
issue_type="chorus_not_stronger_than_verse",
|
|
134
|
+
critic="section_identity",
|
|
135
|
+
severity=0.6,
|
|
136
|
+
confidence=0.60,
|
|
137
|
+
evidence=f"Chorus energy ({chorus_energy:.2f}) <= verse energy ({verse_energy:.2f})",
|
|
138
|
+
recommended_moves=["add_elements_to_chorus", "thin_verse", "add_chorus_hook"],
|
|
139
|
+
))
|
|
140
|
+
|
|
141
|
+
return issues
|
|
142
|
+
|
|
143
|
+
def run_phrase_critic(phrases: list[PhraseUnit]) -> list[CompositionIssue]:
|
|
144
|
+
"""Critique phrase structure within sections."""
|
|
145
|
+
issues = []
|
|
146
|
+
|
|
147
|
+
if len(phrases) < 2:
|
|
148
|
+
return issues
|
|
149
|
+
|
|
150
|
+
# 1. All phrases identical length (no variation)
|
|
151
|
+
lengths = [p.length_bars() for p in phrases]
|
|
152
|
+
unique_lengths = set(lengths)
|
|
153
|
+
if len(unique_lengths) == 1 and len(phrases) > 3:
|
|
154
|
+
issues.append(CompositionIssue(
|
|
155
|
+
issue_type="uniform_phrase_lengths",
|
|
156
|
+
critic="phrase",
|
|
157
|
+
severity=0.4,
|
|
158
|
+
confidence=0.60,
|
|
159
|
+
evidence=f"All {len(phrases)} phrases are {lengths[0]} bars — no structural variation",
|
|
160
|
+
recommended_moves=["extend_one_phrase", "add_pickup", "truncate_for_surprise"],
|
|
161
|
+
))
|
|
162
|
+
|
|
163
|
+
# 2. No cadence detected (all cadence_strength near 0)
|
|
164
|
+
weak_cadences = [p for p in phrases if p.cadence_strength < 0.2]
|
|
165
|
+
if len(weak_cadences) > len(phrases) * 0.7:
|
|
166
|
+
issues.append(CompositionIssue(
|
|
167
|
+
issue_type="weak_cadences",
|
|
168
|
+
critic="phrase",
|
|
169
|
+
severity=0.5,
|
|
170
|
+
confidence=0.55,
|
|
171
|
+
evidence=f"{len(weak_cadences)}/{len(phrases)} phrases have weak cadence (< 0.2)",
|
|
172
|
+
recommended_moves=["add_resolution_notes", "create_turnaround", "vary_last_bar"],
|
|
173
|
+
))
|
|
174
|
+
|
|
175
|
+
# 3. No variation between adjacent phrases
|
|
176
|
+
no_variation = sum(1 for p in phrases if not p.has_variation)
|
|
177
|
+
if no_variation >= len(phrases) - 1 and len(phrases) > 2:
|
|
178
|
+
issues.append(CompositionIssue(
|
|
179
|
+
issue_type="no_phrase_variation",
|
|
180
|
+
critic="phrase",
|
|
181
|
+
severity=0.5,
|
|
182
|
+
confidence=0.60,
|
|
183
|
+
evidence=f"{no_variation}/{len(phrases)} phrases identical to their neighbor",
|
|
184
|
+
recommended_moves=["add_fill", "vary_notes", "create_response_phrase"],
|
|
185
|
+
))
|
|
186
|
+
|
|
187
|
+
return issues
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ── Transition Critic (Round 1) ──────────────────────────────────────
|
|
191
|
+
def run_transition_critic(
|
|
192
|
+
sections: list[SectionNode],
|
|
193
|
+
roles: list[RoleNode],
|
|
194
|
+
harmony_fields: Optional[list[HarmonyField]] = None,
|
|
195
|
+
) -> list[CompositionIssue]:
|
|
196
|
+
"""Analyze boundaries between adjacent sections for transition quality."""
|
|
197
|
+
issues = []
|
|
198
|
+
if len(sections) < 2:
|
|
199
|
+
return issues
|
|
200
|
+
|
|
201
|
+
harmony_map = {}
|
|
202
|
+
if harmony_fields:
|
|
203
|
+
harmony_map = {hf.section_id: hf for hf in harmony_fields}
|
|
204
|
+
|
|
205
|
+
for i in range(1, len(sections)):
|
|
206
|
+
prev = sections[i - 1]
|
|
207
|
+
curr = sections[i]
|
|
208
|
+
|
|
209
|
+
# 1. Hard cut — no energy or density change at boundary
|
|
210
|
+
energy_delta = abs(curr.energy - prev.energy)
|
|
211
|
+
density_delta = abs(curr.density - prev.density)
|
|
212
|
+
|
|
213
|
+
if energy_delta < 0.05 and density_delta < 0.05:
|
|
214
|
+
issues.append(CompositionIssue(
|
|
215
|
+
issue_type="hard_cut_transition",
|
|
216
|
+
critic="transition",
|
|
217
|
+
severity=0.5,
|
|
218
|
+
confidence=0.70,
|
|
219
|
+
scope={"from": prev.section_id, "to": curr.section_id},
|
|
220
|
+
evidence=f"No energy/density change between '{prev.name or prev.section_id}' and '{curr.name or curr.section_id}'",
|
|
221
|
+
recommended_moves=["add_transition_fx", "create_fill", "vary_density_at_boundary"],
|
|
222
|
+
))
|
|
223
|
+
|
|
224
|
+
# 2. No pre-arrival subtraction before high-energy section
|
|
225
|
+
if curr.energy > 0.7 and prev.energy > 0.6:
|
|
226
|
+
issues.append(CompositionIssue(
|
|
227
|
+
issue_type="no_pre_arrival_subtraction",
|
|
228
|
+
critic="transition",
|
|
229
|
+
severity=0.6,
|
|
230
|
+
confidence=0.65,
|
|
231
|
+
scope={"from": prev.section_id, "to": curr.section_id},
|
|
232
|
+
evidence=f"High-energy section '{curr.name or curr.section_id}' (E={curr.energy:.2f}) not preceded by subtraction (prev E={prev.energy:.2f})",
|
|
233
|
+
recommended_moves=["thin_preceding_section", "add_breakdown_before_peak", "inhale_gesture"],
|
|
234
|
+
))
|
|
235
|
+
|
|
236
|
+
# 3. Groove break — rhythmic elements drop out at boundary
|
|
237
|
+
prev_rhythm = {r.track_index for r in roles
|
|
238
|
+
if r.section_id == prev.section_id
|
|
239
|
+
and r.role in (RoleType.KICK_ANCHOR, RoleType.RHYTHMIC_TEXTURE)}
|
|
240
|
+
curr_rhythm = {r.track_index for r in roles
|
|
241
|
+
if r.section_id == curr.section_id
|
|
242
|
+
and r.role in (RoleType.KICK_ANCHOR, RoleType.RHYTHMIC_TEXTURE)}
|
|
243
|
+
|
|
244
|
+
if prev_rhythm and not curr_rhythm:
|
|
245
|
+
issues.append(CompositionIssue(
|
|
246
|
+
issue_type="groove_break_at_transition",
|
|
247
|
+
critic="transition",
|
|
248
|
+
severity=0.5,
|
|
249
|
+
confidence=0.60,
|
|
250
|
+
scope={"from": prev.section_id, "to": curr.section_id},
|
|
251
|
+
evidence=f"All rhythmic elements ({len(prev_rhythm)} tracks) drop out at '{curr.name or curr.section_id}'",
|
|
252
|
+
recommended_moves=["carry_one_rhythm_element", "add_transition_percussion"],
|
|
253
|
+
))
|
|
254
|
+
|
|
255
|
+
# 4. Harmonic non-sequitur — key change without voice-leading support
|
|
256
|
+
prev_hf = harmony_map.get(prev.section_id)
|
|
257
|
+
curr_hf = harmony_map.get(curr.section_id)
|
|
258
|
+
|
|
259
|
+
if prev_hf and curr_hf and prev_hf.key and curr_hf.key:
|
|
260
|
+
if prev_hf.key != curr_hf.key:
|
|
261
|
+
# Key change: check if it's prepared
|
|
262
|
+
if prev_hf.resolution_potential < 0.5 and curr_hf.instability > 0.5:
|
|
263
|
+
issues.append(CompositionIssue(
|
|
264
|
+
issue_type="harmonic_non_sequitur",
|
|
265
|
+
critic="transition",
|
|
266
|
+
severity=0.6,
|
|
267
|
+
confidence=0.55,
|
|
268
|
+
scope={"from": prev.section_id, "to": curr.section_id},
|
|
269
|
+
evidence=f"Key change {prev_hf.key} → {curr_hf.key} without harmonic preparation",
|
|
270
|
+
recommended_moves=["add_pivot_chord", "use_chromatic_mediant", "prepare_with_dominant"],
|
|
271
|
+
))
|
|
272
|
+
|
|
273
|
+
# 5. Weak build — energy rises but no role rotation
|
|
274
|
+
if curr.energy > prev.energy + 0.2:
|
|
275
|
+
prev_fg = {r.track_index for r in roles
|
|
276
|
+
if r.section_id == prev.section_id and r.foreground}
|
|
277
|
+
curr_fg = {r.track_index for r in roles
|
|
278
|
+
if r.section_id == curr.section_id and r.foreground}
|
|
279
|
+
|
|
280
|
+
if prev_fg == curr_fg and prev_fg:
|
|
281
|
+
issues.append(CompositionIssue(
|
|
282
|
+
issue_type="weak_build",
|
|
283
|
+
critic="transition",
|
|
284
|
+
severity=0.4,
|
|
285
|
+
confidence=0.55,
|
|
286
|
+
scope={"from": prev.section_id, "to": curr.section_id},
|
|
287
|
+
evidence=f"Energy rises but same foreground voices ({len(prev_fg)} tracks) — no role rotation",
|
|
288
|
+
recommended_moves=["rotate_foreground_voice", "add_new_element", "handoff_gesture"],
|
|
289
|
+
))
|
|
290
|
+
|
|
291
|
+
return issues
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ── Emotional Arc Critic (Round 3) ──────────────────────────────────
|
|
295
|
+
def run_emotional_arc_critic(
|
|
296
|
+
sections: list[SectionNode],
|
|
297
|
+
harmony_fields: Optional[list["HarmonyField"]] = None,
|
|
298
|
+
) -> list[CompositionIssue]:
|
|
299
|
+
"""Analyze whether the arrangement tells an emotional story.
|
|
300
|
+
|
|
301
|
+
Builds a tension curve from energy, harmonic instability, and density,
|
|
302
|
+
then checks for common arc problems: monotone, all-climax, build
|
|
303
|
+
without payoff, no resolution.
|
|
304
|
+
"""
|
|
305
|
+
issues = []
|
|
306
|
+
if len(sections) < 3:
|
|
307
|
+
return issues # Need enough sections to judge an arc
|
|
308
|
+
|
|
309
|
+
# Build tension curve: composite of energy, density, and harmonic instability
|
|
310
|
+
harmony_map = {}
|
|
311
|
+
if harmony_fields:
|
|
312
|
+
harmony_map = {hf.section_id: hf for hf in harmony_fields}
|
|
313
|
+
|
|
314
|
+
tension_curve: list[float] = []
|
|
315
|
+
for section in sections:
|
|
316
|
+
energy_component = section.energy
|
|
317
|
+
density_component = section.density
|
|
318
|
+
|
|
319
|
+
# Add harmonic instability if available
|
|
320
|
+
hf = harmony_map.get(section.section_id)
|
|
321
|
+
instability = hf.instability if hf else 0.3 # neutral default
|
|
322
|
+
|
|
323
|
+
tension = (energy_component * 0.5 + density_component * 0.3 + instability * 0.2)
|
|
324
|
+
tension_curve.append(round(tension, 3))
|
|
325
|
+
|
|
326
|
+
# 1. Monotone arc — tension doesn't vary enough
|
|
327
|
+
tension_range = max(tension_curve) - min(tension_curve)
|
|
328
|
+
if tension_range < 0.15:
|
|
329
|
+
issues.append(CompositionIssue(
|
|
330
|
+
issue_type="monotone_arc",
|
|
331
|
+
critic="emotional_arc",
|
|
332
|
+
severity=0.7,
|
|
333
|
+
confidence=0.70,
|
|
334
|
+
evidence=f"Tension range: {tension_range:.2f} — arrangement feels static",
|
|
335
|
+
recommended_moves=[
|
|
336
|
+
"add_breakdown_section", "create_energy_contrast",
|
|
337
|
+
"thin_one_section", "add_build_before_peak",
|
|
338
|
+
],
|
|
339
|
+
))
|
|
340
|
+
|
|
341
|
+
# 2. All-climax — high tension everywhere, no rest
|
|
342
|
+
high_tension_count = sum(1 for t in tension_curve if t > 0.7)
|
|
343
|
+
if high_tension_count > len(tension_curve) * 0.6:
|
|
344
|
+
issues.append(CompositionIssue(
|
|
345
|
+
issue_type="all_climax",
|
|
346
|
+
critic="emotional_arc",
|
|
347
|
+
severity=0.6,
|
|
348
|
+
confidence=0.65,
|
|
349
|
+
evidence=f"{high_tension_count}/{len(tension_curve)} sections have tension > 0.7 — no rest",
|
|
350
|
+
recommended_moves=[
|
|
351
|
+
"add_low_energy_section", "create_breakdown",
|
|
352
|
+
"reduce_density_in_verse", "strip_back_intro",
|
|
353
|
+
],
|
|
354
|
+
))
|
|
355
|
+
|
|
356
|
+
# 3. Build without payoff — tension rises then doesn't reach peak
|
|
357
|
+
peak_idx = tension_curve.index(max(tension_curve))
|
|
358
|
+
if peak_idx < len(tension_curve) - 1:
|
|
359
|
+
# Check if there's a build (rising tension) before the peak
|
|
360
|
+
has_build = False
|
|
361
|
+
for i in range(1, peak_idx + 1):
|
|
362
|
+
if tension_curve[i] > tension_curve[i - 1] + 0.1:
|
|
363
|
+
has_build = True
|
|
364
|
+
break
|
|
365
|
+
|
|
366
|
+
if not has_build and len(tension_curve) > 4:
|
|
367
|
+
issues.append(CompositionIssue(
|
|
368
|
+
issue_type="no_clear_build",
|
|
369
|
+
critic="emotional_arc",
|
|
370
|
+
severity=0.5,
|
|
371
|
+
confidence=0.55,
|
|
372
|
+
evidence="No gradual tension increase before peak — peak arrives without anticipation",
|
|
373
|
+
recommended_moves=[
|
|
374
|
+
"add_build_section", "tension_ratchet_gesture",
|
|
375
|
+
"gradual_element_addition", "harmonic_tint_rise",
|
|
376
|
+
],
|
|
377
|
+
))
|
|
378
|
+
|
|
379
|
+
# 4. No resolution — tension stays high at the end
|
|
380
|
+
if len(tension_curve) >= 3:
|
|
381
|
+
final_tension = tension_curve[-1]
|
|
382
|
+
peak_tension = max(tension_curve)
|
|
383
|
+
if final_tension > peak_tension * 0.8 and peak_tension > 0.5:
|
|
384
|
+
issues.append(CompositionIssue(
|
|
385
|
+
issue_type="no_resolution",
|
|
386
|
+
critic="emotional_arc",
|
|
387
|
+
severity=0.5,
|
|
388
|
+
confidence=0.60,
|
|
389
|
+
evidence=f"Final tension ({final_tension:.2f}) nearly as high as peak ({peak_tension:.2f}) — no release",
|
|
390
|
+
recommended_moves=[
|
|
391
|
+
"add_outro", "create_energy_drop_at_end",
|
|
392
|
+
"outro_decay_dissolve_gesture", "strip_elements_gradually",
|
|
393
|
+
],
|
|
394
|
+
))
|
|
395
|
+
|
|
396
|
+
# 5. Peak too early — climax in first third
|
|
397
|
+
if peak_idx < len(tension_curve) / 3 and len(tension_curve) > 4:
|
|
398
|
+
issues.append(CompositionIssue(
|
|
399
|
+
issue_type="peak_too_early",
|
|
400
|
+
critic="emotional_arc",
|
|
401
|
+
severity=0.5,
|
|
402
|
+
confidence=0.55,
|
|
403
|
+
evidence=f"Peak tension at section {peak_idx + 1}/{len(tension_curve)} — climax in first third",
|
|
404
|
+
recommended_moves=[
|
|
405
|
+
"move_peak_elements_later", "add_second_bigger_climax",
|
|
406
|
+
"reorder_sections", "save_hook_reveal_for_later",
|
|
407
|
+
],
|
|
408
|
+
))
|
|
409
|
+
|
|
410
|
+
return issues
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# ── Cross-Section Critic (Round 4) ──────────────────────────────────
|
|
414
|
+
def run_cross_section_critic(
|
|
415
|
+
sections: list[SectionNode],
|
|
416
|
+
roles: list[RoleNode],
|
|
417
|
+
harmony_fields: Optional[list["HarmonyField"]] = None,
|
|
418
|
+
motif_count: int = 0,
|
|
419
|
+
) -> list[CompositionIssue]:
|
|
420
|
+
"""Reason across the entire arrangement for cross-section coherence.
|
|
421
|
+
|
|
422
|
+
Checks that the arrangement works as a whole, not just per-section:
|
|
423
|
+
- Clear reveal order (elements shouldn't all appear at once)
|
|
424
|
+
- Foreground voice rotation (same lead everywhere = fatigue)
|
|
425
|
+
- Harmonic pacing (rapid key changes everywhere = chaos)
|
|
426
|
+
- Element variety across sections
|
|
427
|
+
"""
|
|
428
|
+
issues = []
|
|
429
|
+
if len(sections) < 3:
|
|
430
|
+
return issues
|
|
431
|
+
|
|
432
|
+
# 1. All elements appear from the start — no reveal order
|
|
433
|
+
if len(sections) >= 3:
|
|
434
|
+
first_active = set(sections[0].tracks_active)
|
|
435
|
+
second_active = set(sections[1].tracks_active)
|
|
436
|
+
third_active = set(sections[2].tracks_active)
|
|
437
|
+
if first_active == second_active == third_active and first_active:
|
|
438
|
+
issues.append(CompositionIssue(
|
|
439
|
+
issue_type="no_reveal_order",
|
|
440
|
+
critic="cross_section",
|
|
441
|
+
severity=0.6,
|
|
442
|
+
confidence=0.65,
|
|
443
|
+
evidence=f"First 3 sections all have same {len(first_active)} active tracks — no staggered reveal",
|
|
444
|
+
recommended_moves=[
|
|
445
|
+
"defer_elements_to_later_sections", "strip_intro",
|
|
446
|
+
"create_reveal_sequence", "mute_tracks_in_early_sections",
|
|
447
|
+
],
|
|
448
|
+
))
|
|
449
|
+
|
|
450
|
+
# 2. Same foreground voices in every section — no rotation
|
|
451
|
+
fg_by_section: list[set[int]] = []
|
|
452
|
+
for section in sections:
|
|
453
|
+
fg = {r.track_index for r in roles
|
|
454
|
+
if r.section_id == section.section_id and r.foreground}
|
|
455
|
+
fg_by_section.append(fg)
|
|
456
|
+
|
|
457
|
+
if len(fg_by_section) >= 3:
|
|
458
|
+
all_same = all(fg == fg_by_section[0] for fg in fg_by_section[1:])
|
|
459
|
+
if all_same and fg_by_section[0]:
|
|
460
|
+
issues.append(CompositionIssue(
|
|
461
|
+
issue_type="no_foreground_rotation",
|
|
462
|
+
critic="cross_section",
|
|
463
|
+
severity=0.5,
|
|
464
|
+
confidence=0.60,
|
|
465
|
+
evidence=f"Same foreground voices ({len(fg_by_section[0])} tracks) in all {len(sections)} sections",
|
|
466
|
+
recommended_moves=[
|
|
467
|
+
"alternate_lead_voice", "handoff_gesture_between_sections",
|
|
468
|
+
"mute_lead_in_bridge", "introduce_new_hook_element",
|
|
469
|
+
],
|
|
470
|
+
))
|
|
471
|
+
|
|
472
|
+
# 3. Harmonic monotony — same key across all sections
|
|
473
|
+
if harmony_fields:
|
|
474
|
+
keys = [hf.key for hf in harmony_fields if hf.key]
|
|
475
|
+
if len(keys) >= 3 and len(set(keys)) == 1:
|
|
476
|
+
issues.append(CompositionIssue(
|
|
477
|
+
issue_type="harmonic_monotony",
|
|
478
|
+
critic="cross_section",
|
|
479
|
+
severity=0.4,
|
|
480
|
+
confidence=0.50,
|
|
481
|
+
evidence=f"All {len(keys)} sections in same key ({keys[0]}) — consider modulation",
|
|
482
|
+
recommended_moves=[
|
|
483
|
+
"modulate_for_bridge", "use_chromatic_mediant",
|
|
484
|
+
"borrow_from_parallel_key", "transpose_final_chorus",
|
|
485
|
+
],
|
|
486
|
+
))
|
|
487
|
+
|
|
488
|
+
# 4. Harmonic chaos — different key in every section
|
|
489
|
+
unique_keys = set(keys)
|
|
490
|
+
if len(unique_keys) > len(keys) * 0.7 and len(keys) >= 4:
|
|
491
|
+
issues.append(CompositionIssue(
|
|
492
|
+
issue_type="harmonic_chaos",
|
|
493
|
+
critic="cross_section",
|
|
494
|
+
severity=0.5,
|
|
495
|
+
confidence=0.45,
|
|
496
|
+
evidence=f"{len(unique_keys)} different keys across {len(keys)} sections — hard to follow",
|
|
497
|
+
recommended_moves=[
|
|
498
|
+
"consolidate_to_two_keys", "use_pivot_chords",
|
|
499
|
+
"establish_home_key", "group_related_sections",
|
|
500
|
+
],
|
|
501
|
+
))
|
|
502
|
+
|
|
503
|
+
# 5. No motif development (if motifs exist but aren't varied)
|
|
504
|
+
if motif_count > 0:
|
|
505
|
+
# Check if sections have varying density (proxy for development)
|
|
506
|
+
densities = [s.density for s in sections]
|
|
507
|
+
unique_densities = len(set(round(d, 1) for d in densities))
|
|
508
|
+
if unique_densities <= 2 and len(sections) > 4:
|
|
509
|
+
issues.append(CompositionIssue(
|
|
510
|
+
issue_type="static_arrangement",
|
|
511
|
+
critic="cross_section",
|
|
512
|
+
severity=0.4,
|
|
513
|
+
confidence=0.50,
|
|
514
|
+
evidence=f"Only {unique_densities} distinct density levels across {len(sections)} sections with {motif_count} motifs",
|
|
515
|
+
recommended_moves=[
|
|
516
|
+
"vary_motif_density_per_section", "fragment_motif_in_bridge",
|
|
517
|
+
"augment_motif_in_outro", "register_shift_for_variety",
|
|
518
|
+
],
|
|
519
|
+
))
|
|
520
|
+
|
|
521
|
+
return issues
|
|
522
|
+
|