livepilot 1.9.13 → 1.9.15
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 +51 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +7 -7
- package/bin/livepilot.js +32 -8
- package/installer/install.js +21 -2
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/agents/livepilot-producer/AGENT.md +243 -49
- package/livepilot/skills/livepilot-core/SKILL.md +81 -6
- package/livepilot/skills/livepilot-core/references/m4l-devices.md +2 -2
- package/livepilot/skills/livepilot-core/references/overview.md +3 -3
- package/livepilot/skills/livepilot-core/references/sound-design.md +3 -2
- package/livepilot/skills/livepilot-release/SKILL.md +13 -13
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +6 -3
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/curves.py +11 -3
- package/mcp_server/evaluation/__init__.py +1 -0
- package/mcp_server/evaluation/fabric.py +575 -0
- package/mcp_server/evaluation/feature_extractors.py +84 -0
- package/mcp_server/evaluation/policy.py +67 -0
- package/mcp_server/evaluation/tools.py +53 -0
- package/mcp_server/memory/__init__.py +11 -2
- package/mcp_server/memory/anti_memory.py +78 -0
- package/mcp_server/memory/promotion.py +94 -0
- package/mcp_server/memory/session_memory.py +108 -0
- package/mcp_server/memory/taste_memory.py +158 -0
- package/mcp_server/memory/technique_store.py +2 -1
- package/mcp_server/memory/tools.py +112 -0
- package/mcp_server/mix_engine/__init__.py +1 -0
- package/mcp_server/mix_engine/critics.py +299 -0
- package/mcp_server/mix_engine/models.py +152 -0
- package/mcp_server/mix_engine/planner.py +103 -0
- package/mcp_server/mix_engine/state_builder.py +316 -0
- package/mcp_server/mix_engine/tools.py +214 -0
- package/mcp_server/performance_engine/__init__.py +1 -0
- package/mcp_server/performance_engine/models.py +148 -0
- package/mcp_server/performance_engine/planner.py +267 -0
- package/mcp_server/performance_engine/safety.py +162 -0
- package/mcp_server/performance_engine/tools.py +183 -0
- package/mcp_server/project_brain/__init__.py +6 -0
- package/mcp_server/project_brain/arrangement_graph.py +64 -0
- package/mcp_server/project_brain/automation_graph.py +72 -0
- package/mcp_server/project_brain/builder.py +123 -0
- package/mcp_server/project_brain/capability_graph.py +64 -0
- package/mcp_server/project_brain/models.py +282 -0
- package/mcp_server/project_brain/refresh.py +80 -0
- package/mcp_server/project_brain/role_graph.py +103 -0
- package/mcp_server/project_brain/session_graph.py +51 -0
- package/mcp_server/project_brain/tools.py +144 -0
- package/mcp_server/reference_engine/__init__.py +1 -0
- package/mcp_server/reference_engine/gap_analyzer.py +239 -0
- package/mcp_server/reference_engine/models.py +105 -0
- package/mcp_server/reference_engine/profile_builder.py +149 -0
- package/mcp_server/reference_engine/tactic_router.py +117 -0
- package/mcp_server/reference_engine/tools.py +235 -0
- package/mcp_server/runtime/__init__.py +1 -0
- package/mcp_server/runtime/action_ledger.py +117 -0
- package/mcp_server/runtime/action_ledger_models.py +84 -0
- package/mcp_server/runtime/action_tools.py +57 -0
- package/mcp_server/runtime/capability_state.py +218 -0
- package/mcp_server/runtime/safety_kernel.py +339 -0
- package/mcp_server/runtime/safety_tools.py +42 -0
- package/mcp_server/runtime/tools.py +64 -0
- package/mcp_server/server.py +23 -1
- package/mcp_server/sound_design/__init__.py +1 -0
- package/mcp_server/sound_design/critics.py +297 -0
- package/mcp_server/sound_design/models.py +147 -0
- package/mcp_server/sound_design/planner.py +104 -0
- package/mcp_server/sound_design/tools.py +297 -0
- package/mcp_server/tools/_agent_os_engine.py +947 -0
- package/mcp_server/tools/_composition_engine.py +1530 -0
- package/mcp_server/tools/_conductor.py +199 -0
- package/mcp_server/tools/_conductor_budgets.py +222 -0
- package/mcp_server/tools/_evaluation_contracts.py +91 -0
- package/mcp_server/tools/_form_engine.py +416 -0
- package/mcp_server/tools/_motif_engine.py +351 -0
- package/mcp_server/tools/_planner_engine.py +516 -0
- package/mcp_server/tools/_research_engine.py +542 -0
- package/mcp_server/tools/_research_provider.py +185 -0
- package/mcp_server/tools/_snapshot_normalizer.py +49 -0
- package/mcp_server/tools/agent_os.py +440 -0
- package/mcp_server/tools/analyzer.py +18 -0
- package/mcp_server/tools/automation.py +25 -10
- package/mcp_server/tools/composition.py +563 -0
- package/mcp_server/tools/motif.py +104 -0
- package/mcp_server/tools/planner.py +144 -0
- package/mcp_server/tools/research.py +223 -0
- package/mcp_server/tools/tracks.py +18 -3
- package/mcp_server/tools/transport.py +10 -2
- package/mcp_server/transition_engine/__init__.py +6 -0
- package/mcp_server/transition_engine/archetypes.py +167 -0
- package/mcp_server/transition_engine/critics.py +340 -0
- package/mcp_server/transition_engine/models.py +90 -0
- package/mcp_server/transition_engine/tools.py +291 -0
- package/mcp_server/translation_engine/__init__.py +5 -0
- package/mcp_server/translation_engine/critics.py +297 -0
- package/mcp_server/translation_engine/models.py +27 -0
- package/mcp_server/translation_engine/tools.py +74 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/arrangement.py +12 -2
- package/requirements.txt +1 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Transition archetype library — 7 curated transition patterns.
|
|
2
|
+
|
|
3
|
+
Each archetype encodes a musically validated approach to section
|
|
4
|
+
boundaries, with use cases, risk profile, devices, gestures, and
|
|
5
|
+
verification cues.
|
|
6
|
+
|
|
7
|
+
Zero I/O.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .models import TransitionArchetype, TransitionBoundary
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ── Archetype Library ─────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
TRANSITION_ARCHETYPES: dict[str, TransitionArchetype] = {
|
|
18
|
+
"subtractive_inhale": TransitionArchetype(
|
|
19
|
+
name="subtractive_inhale",
|
|
20
|
+
description="Pull energy back before impact — strip elements to create anticipation, then release into the new section.",
|
|
21
|
+
use_cases=["build_to_drop", "verse_to_chorus", "pre_peak_tension"],
|
|
22
|
+
risk_profile="low",
|
|
23
|
+
devices=["Auto Filter", "Utility", "Compressor"],
|
|
24
|
+
gestures=["inhale", "conceal", "release"],
|
|
25
|
+
verification=[
|
|
26
|
+
"Energy dips before boundary bar",
|
|
27
|
+
"At least 2 tracks reduce volume or filter cutoff",
|
|
28
|
+
"New section feels louder by contrast, not by gain",
|
|
29
|
+
],
|
|
30
|
+
),
|
|
31
|
+
"fill_and_reset": TransitionArchetype(
|
|
32
|
+
name="fill_and_reset",
|
|
33
|
+
description="Drum fill or rhythmic intensification, then clean downbeat — classic transition that signals change without ambiguity.",
|
|
34
|
+
use_cases=["verse_to_chorus", "chorus_to_verse", "any_section_change"],
|
|
35
|
+
risk_profile="low",
|
|
36
|
+
devices=["Drum Rack", "Simpler", "Beat Repeat"],
|
|
37
|
+
gestures=["punctuate", "release"],
|
|
38
|
+
verification=[
|
|
39
|
+
"Fill occupies last 1-2 bars before boundary",
|
|
40
|
+
"Downbeat of new section is rhythmically clean",
|
|
41
|
+
"Fill density doesn't overwhelm the arrangement",
|
|
42
|
+
],
|
|
43
|
+
),
|
|
44
|
+
"tail_throw": TransitionArchetype(
|
|
45
|
+
name="tail_throw",
|
|
46
|
+
description="Delay or reverb throw on the last hit of outgoing section — creates continuity while the new section enters.",
|
|
47
|
+
use_cases=["phrase_cadence", "section_end_punctuation", "dub_style_transition"],
|
|
48
|
+
risk_profile="low",
|
|
49
|
+
devices=["Delay", "Reverb", "Echo"],
|
|
50
|
+
gestures=["punctuate", "drift"],
|
|
51
|
+
verification=[
|
|
52
|
+
"Send level spikes on last beat/bar of outgoing section",
|
|
53
|
+
"Tail decays naturally into new section",
|
|
54
|
+
"Tail doesn't mask arrival of new elements",
|
|
55
|
+
],
|
|
56
|
+
),
|
|
57
|
+
"width_bloom": TransitionArchetype(
|
|
58
|
+
name="width_bloom",
|
|
59
|
+
description="Narrow the stereo field before the boundary, then widen at arrival — creates a sense of opening up.",
|
|
60
|
+
use_cases=["verse_to_chorus", "breakdown_to_drop", "section_expansion"],
|
|
61
|
+
risk_profile="medium",
|
|
62
|
+
devices=["Utility", "Auto Pan", "Chorus-Ensemble", "Wider"],
|
|
63
|
+
gestures=["conceal", "reveal"],
|
|
64
|
+
verification=[
|
|
65
|
+
"Stereo width measurably narrows before boundary",
|
|
66
|
+
"Width expands at or just after boundary bar",
|
|
67
|
+
"Mono compatibility preserved during narrow phase",
|
|
68
|
+
],
|
|
69
|
+
),
|
|
70
|
+
"harmonic_suspend": TransitionArchetype(
|
|
71
|
+
name="harmonic_suspend",
|
|
72
|
+
description="Suspend chord (sus2/sus4) or dominant 7th at section end, resolve on arrival — harmonic tension drives the transition.",
|
|
73
|
+
use_cases=["key_change", "chord_progression_pivot", "harmonic_lift"],
|
|
74
|
+
risk_profile="medium",
|
|
75
|
+
devices=["Instrument Rack", "Wavetable", "Operator"],
|
|
76
|
+
gestures=["lift", "release"],
|
|
77
|
+
verification=[
|
|
78
|
+
"Suspension or dominant chord appears in last 1-2 bars",
|
|
79
|
+
"Resolution lands on downbeat of new section",
|
|
80
|
+
"Voice leading is smooth (no large parallel jumps)",
|
|
81
|
+
],
|
|
82
|
+
),
|
|
83
|
+
"impact_vacuum": TransitionArchetype(
|
|
84
|
+
name="impact_vacuum",
|
|
85
|
+
description="Everything cuts to silence (or near-silence), brief pause, then full impact — maximum contrast transition.",
|
|
86
|
+
use_cases=["build_to_drop", "pre_peak_climax", "dramatic_reentry"],
|
|
87
|
+
risk_profile="high",
|
|
88
|
+
devices=["Utility", "Gate", "Compressor"],
|
|
89
|
+
gestures=["conceal", "release"],
|
|
90
|
+
verification=[
|
|
91
|
+
"Clear silence or near-silence for at least half a bar",
|
|
92
|
+
"Impact arrival is immediate and full",
|
|
93
|
+
"Silence doesn't feel like a mistake or glitch",
|
|
94
|
+
],
|
|
95
|
+
),
|
|
96
|
+
"delayed_foreground_handoff": TransitionArchetype(
|
|
97
|
+
name="delayed_foreground_handoff",
|
|
98
|
+
description="Old lead element fades while new lead enters underneath — overlapping handoff avoids abrupt role change.",
|
|
99
|
+
use_cases=["lead_change", "hook_rotation", "verse_vocal_to_instrumental_hook"],
|
|
100
|
+
risk_profile="medium",
|
|
101
|
+
devices=["Utility", "Auto Filter", "EQ Eight"],
|
|
102
|
+
gestures=["conceal", "reveal", "handoff"],
|
|
103
|
+
verification=[
|
|
104
|
+
"Outgoing lead fades over 2-4 bars across boundary",
|
|
105
|
+
"Incoming lead enters before outgoing fully exits",
|
|
106
|
+
"No frequency masking between overlapping leads",
|
|
107
|
+
],
|
|
108
|
+
),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ── Archetype Selection ───────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
# Maps (from_type, to_type) pairs to preferred archetypes.
|
|
115
|
+
_SECTION_PAIR_PREFERENCES: dict[tuple[str, str], list[str]] = {
|
|
116
|
+
("build", "drop"): ["subtractive_inhale", "impact_vacuum"],
|
|
117
|
+
("verse", "chorus"): ["fill_and_reset", "width_bloom", "subtractive_inhale"],
|
|
118
|
+
("chorus", "verse"): ["tail_throw", "delayed_foreground_handoff"],
|
|
119
|
+
("intro", "verse"): ["delayed_foreground_handoff", "fill_and_reset"],
|
|
120
|
+
("breakdown", "drop"): ["impact_vacuum", "subtractive_inhale"],
|
|
121
|
+
("breakdown", "chorus"): ["width_bloom", "subtractive_inhale"],
|
|
122
|
+
("verse", "bridge"): ["harmonic_suspend", "tail_throw"],
|
|
123
|
+
("bridge", "chorus"): ["subtractive_inhale", "width_bloom"],
|
|
124
|
+
("chorus", "bridge"): ["tail_throw", "harmonic_suspend"],
|
|
125
|
+
("pre_chorus", "chorus"): ["subtractive_inhale", "fill_and_reset"],
|
|
126
|
+
("verse", "pre_chorus"): ["fill_and_reset", "tail_throw"],
|
|
127
|
+
("drop", "breakdown"): ["tail_throw", "delayed_foreground_handoff"],
|
|
128
|
+
("chorus", "outro"): ["tail_throw", "delayed_foreground_handoff"],
|
|
129
|
+
("verse", "outro"): ["tail_throw", "delayed_foreground_handoff"],
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def select_archetype(boundary: TransitionBoundary) -> TransitionArchetype:
|
|
134
|
+
"""Pick the best-fit archetype for a given boundary.
|
|
135
|
+
|
|
136
|
+
Strategy:
|
|
137
|
+
1. Check section-type pair preferences.
|
|
138
|
+
2. Fall back to energy-delta heuristics.
|
|
139
|
+
"""
|
|
140
|
+
pair = (boundary.from_type, boundary.to_type)
|
|
141
|
+
|
|
142
|
+
# Check explicit section-pair preferences
|
|
143
|
+
if pair in _SECTION_PAIR_PREFERENCES:
|
|
144
|
+
preferred = _SECTION_PAIR_PREFERENCES[pair]
|
|
145
|
+
return TRANSITION_ARCHETYPES[preferred[0]]
|
|
146
|
+
|
|
147
|
+
# Energy-delta heuristics
|
|
148
|
+
ed = boundary.energy_delta
|
|
149
|
+
|
|
150
|
+
# Large energy increase — needs preparation
|
|
151
|
+
if ed > 0.3:
|
|
152
|
+
return TRANSITION_ARCHETYPES["subtractive_inhale"]
|
|
153
|
+
|
|
154
|
+
# Large energy decrease — needs graceful exit
|
|
155
|
+
if ed < -0.3:
|
|
156
|
+
return TRANSITION_ARCHETYPES["tail_throw"]
|
|
157
|
+
|
|
158
|
+
# Moderate increase — fill works universally
|
|
159
|
+
if ed > 0.1:
|
|
160
|
+
return TRANSITION_ARCHETYPES["fill_and_reset"]
|
|
161
|
+
|
|
162
|
+
# Moderate decrease — handoff preserves continuity
|
|
163
|
+
if ed < -0.1:
|
|
164
|
+
return TRANSITION_ARCHETYPES["delayed_foreground_handoff"]
|
|
165
|
+
|
|
166
|
+
# Flat energy — width bloom adds interest without energy change
|
|
167
|
+
return TRANSITION_ARCHETYPES["width_bloom"]
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Transition Engine critics — 5 boundary-specific critics.
|
|
2
|
+
|
|
3
|
+
Detect transition quality issues: boundary clarity, payoff strength,
|
|
4
|
+
overtelegraphing, energy redirection, and gesture fit.
|
|
5
|
+
|
|
6
|
+
All pure computation, zero I/O.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import asdict, dataclass, field
|
|
12
|
+
|
|
13
|
+
from .models import TransitionBoundary, TransitionPlan, TransitionScore
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ── TransitionIssue ───────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class TransitionIssue:
|
|
21
|
+
"""A single detected transition issue."""
|
|
22
|
+
|
|
23
|
+
issue_type: str = ""
|
|
24
|
+
critic: str = ""
|
|
25
|
+
severity: float = 0.0 # 0.0-1.0
|
|
26
|
+
confidence: float = 0.0 # 0.0-1.0
|
|
27
|
+
boundary: dict = field(default_factory=dict)
|
|
28
|
+
evidence: str = ""
|
|
29
|
+
recommended_moves: list[str] = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> dict:
|
|
32
|
+
return asdict(self)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── Boundary Clarity Critic ───────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_boundary_clarity_critic(
|
|
39
|
+
boundary: TransitionBoundary,
|
|
40
|
+
) -> list[TransitionIssue]:
|
|
41
|
+
"""Detect unclear boundaries — no energy or density change."""
|
|
42
|
+
issues: list[TransitionIssue] = []
|
|
43
|
+
|
|
44
|
+
abs_energy = abs(boundary.energy_delta)
|
|
45
|
+
abs_density = abs(boundary.density_delta)
|
|
46
|
+
|
|
47
|
+
# Both energy and density flat — boundary is invisible
|
|
48
|
+
if abs_energy < 0.05 and abs_density < 0.05:
|
|
49
|
+
issues.append(TransitionIssue(
|
|
50
|
+
issue_type="invisible_boundary",
|
|
51
|
+
critic="boundary_clarity",
|
|
52
|
+
severity=0.7,
|
|
53
|
+
confidence=0.75,
|
|
54
|
+
boundary=boundary.to_dict(),
|
|
55
|
+
evidence=(
|
|
56
|
+
f"Neither energy (delta={boundary.energy_delta:.2f}) nor density "
|
|
57
|
+
f"(delta={boundary.density_delta:.2f}) changes at boundary bar "
|
|
58
|
+
f"{boundary.boundary_bar}"
|
|
59
|
+
),
|
|
60
|
+
recommended_moves=[
|
|
61
|
+
"add_fill_at_boundary",
|
|
62
|
+
"vary_density_before_arrival",
|
|
63
|
+
"insert_breath_before_downbeat",
|
|
64
|
+
],
|
|
65
|
+
))
|
|
66
|
+
|
|
67
|
+
# Only density changes but energy is flat — structural but not felt
|
|
68
|
+
if abs_energy < 0.05 and abs_density >= 0.1:
|
|
69
|
+
issues.append(TransitionIssue(
|
|
70
|
+
issue_type="structural_only_boundary",
|
|
71
|
+
critic="boundary_clarity",
|
|
72
|
+
severity=0.4,
|
|
73
|
+
confidence=0.60,
|
|
74
|
+
boundary=boundary.to_dict(),
|
|
75
|
+
evidence=(
|
|
76
|
+
f"Density shifts (delta={boundary.density_delta:.2f}) but energy "
|
|
77
|
+
f"is flat (delta={boundary.energy_delta:.2f}) — transition may feel "
|
|
78
|
+
f"like track muting, not a musical boundary"
|
|
79
|
+
),
|
|
80
|
+
recommended_moves=[
|
|
81
|
+
"add_energy_gesture_at_boundary",
|
|
82
|
+
"automate_filter_or_volume_sweep",
|
|
83
|
+
],
|
|
84
|
+
))
|
|
85
|
+
|
|
86
|
+
return issues
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ── Payoff Critic ─────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def run_payoff_critic(
|
|
93
|
+
boundary: TransitionBoundary,
|
|
94
|
+
score: TransitionScore,
|
|
95
|
+
) -> list[TransitionIssue]:
|
|
96
|
+
"""Detect weak payoff — high energy arrival without reward."""
|
|
97
|
+
issues: list[TransitionIssue] = []
|
|
98
|
+
|
|
99
|
+
# High energy increase but low payoff score
|
|
100
|
+
if boundary.energy_delta > 0.2 and score.payoff_strength < 0.4:
|
|
101
|
+
issues.append(TransitionIssue(
|
|
102
|
+
issue_type="weak_payoff",
|
|
103
|
+
critic="payoff",
|
|
104
|
+
severity=0.7,
|
|
105
|
+
confidence=0.70,
|
|
106
|
+
boundary=boundary.to_dict(),
|
|
107
|
+
evidence=(
|
|
108
|
+
f"Energy rises by {boundary.energy_delta:.2f} at bar "
|
|
109
|
+
f"{boundary.boundary_bar} but payoff_strength is only "
|
|
110
|
+
f"{score.payoff_strength:.2f} — arrival doesn't feel earned"
|
|
111
|
+
),
|
|
112
|
+
recommended_moves=[
|
|
113
|
+
"add_pre_arrival_subtraction",
|
|
114
|
+
"increase_contrast_at_boundary",
|
|
115
|
+
"add_impact_element_at_downbeat",
|
|
116
|
+
],
|
|
117
|
+
))
|
|
118
|
+
|
|
119
|
+
# Build section into drop/chorus with low payoff
|
|
120
|
+
build_types = {"build", "pre_chorus"}
|
|
121
|
+
peak_types = {"drop", "chorus"}
|
|
122
|
+
if (boundary.from_type in build_types
|
|
123
|
+
and boundary.to_type in peak_types
|
|
124
|
+
and score.payoff_strength < 0.5):
|
|
125
|
+
issues.append(TransitionIssue(
|
|
126
|
+
issue_type="anticlimactic_arrival",
|
|
127
|
+
critic="payoff",
|
|
128
|
+
severity=0.8,
|
|
129
|
+
confidence=0.75,
|
|
130
|
+
boundary=boundary.to_dict(),
|
|
131
|
+
evidence=(
|
|
132
|
+
f"{boundary.from_type} -> {boundary.to_type} transition at bar "
|
|
133
|
+
f"{boundary.boundary_bar} has payoff {score.payoff_strength:.2f} "
|
|
134
|
+
f"— peak section should feel like a reward"
|
|
135
|
+
),
|
|
136
|
+
recommended_moves=[
|
|
137
|
+
"deepen_subtraction_in_build",
|
|
138
|
+
"add_impact_vacuum",
|
|
139
|
+
"widen_stereo_at_arrival",
|
|
140
|
+
],
|
|
141
|
+
))
|
|
142
|
+
|
|
143
|
+
return issues
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ── Overtelegraphing Critic ───────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def run_overtelegraphing_critic(
|
|
150
|
+
plan: TransitionPlan,
|
|
151
|
+
) -> list[TransitionIssue]:
|
|
152
|
+
"""Detect transitions that use too many gestures — trying too hard."""
|
|
153
|
+
issues: list[TransitionIssue] = []
|
|
154
|
+
|
|
155
|
+
total_gestures = len(plan.lead_in_gestures) + len(plan.arrival_gestures)
|
|
156
|
+
|
|
157
|
+
# Too many gestures — overproduced transition
|
|
158
|
+
if total_gestures > 5:
|
|
159
|
+
issues.append(TransitionIssue(
|
|
160
|
+
issue_type="overtelegraphed_transition",
|
|
161
|
+
critic="overtelegraphing",
|
|
162
|
+
severity=min(1.0, 0.3 + (total_gestures - 5) * 0.15),
|
|
163
|
+
confidence=0.65,
|
|
164
|
+
boundary=plan.boundary.to_dict(),
|
|
165
|
+
evidence=(
|
|
166
|
+
f"Transition uses {total_gestures} gestures "
|
|
167
|
+
f"({len(plan.lead_in_gestures)} lead-in, "
|
|
168
|
+
f"{len(plan.arrival_gestures)} arrival) — more FX != better transition"
|
|
169
|
+
),
|
|
170
|
+
recommended_moves=[
|
|
171
|
+
"remove_weakest_gesture",
|
|
172
|
+
"simplify_to_one_lead_in_and_one_arrival",
|
|
173
|
+
"trust_the_arrangement",
|
|
174
|
+
],
|
|
175
|
+
))
|
|
176
|
+
|
|
177
|
+
# High-risk archetype with many gestures — doubly obvious
|
|
178
|
+
if plan.archetype.risk_profile == "high" and total_gestures > 3:
|
|
179
|
+
issues.append(TransitionIssue(
|
|
180
|
+
issue_type="high_risk_overloaded",
|
|
181
|
+
critic="overtelegraphing",
|
|
182
|
+
severity=0.6,
|
|
183
|
+
confidence=0.60,
|
|
184
|
+
boundary=plan.boundary.to_dict(),
|
|
185
|
+
evidence=(
|
|
186
|
+
f"High-risk archetype '{plan.archetype.name}' combined with "
|
|
187
|
+
f"{total_gestures} gestures — dramatic archetype needs fewer "
|
|
188
|
+
f"gestures, not more"
|
|
189
|
+
),
|
|
190
|
+
recommended_moves=[
|
|
191
|
+
"reduce_to_core_gestures_only",
|
|
192
|
+
"let_archetype_do_the_work",
|
|
193
|
+
],
|
|
194
|
+
))
|
|
195
|
+
|
|
196
|
+
return issues
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ── Energy Redirection Critic ─────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def run_energy_redirection_critic(
|
|
203
|
+
boundary: TransitionBoundary,
|
|
204
|
+
) -> list[TransitionIssue]:
|
|
205
|
+
"""Detect boundaries where energy doesn't redirect enough."""
|
|
206
|
+
issues: list[TransitionIssue] = []
|
|
207
|
+
|
|
208
|
+
abs_energy = abs(boundary.energy_delta)
|
|
209
|
+
|
|
210
|
+
# Section types that demand energy change
|
|
211
|
+
high_contrast_pairs = {
|
|
212
|
+
("build", "drop"), ("breakdown", "drop"),
|
|
213
|
+
("breakdown", "chorus"), ("pre_chorus", "chorus"),
|
|
214
|
+
}
|
|
215
|
+
low_contrast_pairs = {
|
|
216
|
+
("verse", "verse"), ("chorus", "chorus"),
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
pair = (boundary.from_type, boundary.to_type)
|
|
220
|
+
|
|
221
|
+
# High-contrast pair with low energy change — transition falls flat
|
|
222
|
+
if pair in high_contrast_pairs and abs_energy < 0.15:
|
|
223
|
+
issues.append(TransitionIssue(
|
|
224
|
+
issue_type="flat_high_contrast_transition",
|
|
225
|
+
critic="energy_redirection",
|
|
226
|
+
severity=0.7,
|
|
227
|
+
confidence=0.70,
|
|
228
|
+
boundary=boundary.to_dict(),
|
|
229
|
+
evidence=(
|
|
230
|
+
f"{boundary.from_type} -> {boundary.to_type} expects significant "
|
|
231
|
+
f"energy change but delta is only {boundary.energy_delta:.2f}"
|
|
232
|
+
),
|
|
233
|
+
recommended_moves=[
|
|
234
|
+
"increase_energy_contrast",
|
|
235
|
+
"subtract_before_arrival",
|
|
236
|
+
"add_elements_at_arrival",
|
|
237
|
+
],
|
|
238
|
+
))
|
|
239
|
+
|
|
240
|
+
# Same section type repeating with large energy change — unintentional
|
|
241
|
+
if pair in low_contrast_pairs and abs_energy > 0.3:
|
|
242
|
+
issues.append(TransitionIssue(
|
|
243
|
+
issue_type="unexpected_energy_shift",
|
|
244
|
+
critic="energy_redirection",
|
|
245
|
+
severity=0.4,
|
|
246
|
+
confidence=0.55,
|
|
247
|
+
boundary=boundary.to_dict(),
|
|
248
|
+
evidence=(
|
|
249
|
+
f"Repeating section type '{boundary.from_type}' has energy delta "
|
|
250
|
+
f"of {boundary.energy_delta:.2f} — may feel inconsistent"
|
|
251
|
+
),
|
|
252
|
+
recommended_moves=[
|
|
253
|
+
"normalize_energy_across_repeats",
|
|
254
|
+
"differentiate_section_types_if_intentional",
|
|
255
|
+
],
|
|
256
|
+
))
|
|
257
|
+
|
|
258
|
+
return issues
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ── Gesture Fit Critic ────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def run_gesture_fit_critic(
|
|
265
|
+
plan: TransitionPlan,
|
|
266
|
+
) -> list[TransitionIssue]:
|
|
267
|
+
"""Detect mismatches between archetype and section types."""
|
|
268
|
+
issues: list[TransitionIssue] = []
|
|
269
|
+
|
|
270
|
+
boundary = plan.boundary
|
|
271
|
+
archetype = plan.archetype
|
|
272
|
+
|
|
273
|
+
# Check if the section pair matches any of the archetype's use cases
|
|
274
|
+
pair_tags = {
|
|
275
|
+
f"{boundary.from_type}_to_{boundary.to_type}",
|
|
276
|
+
boundary.from_type,
|
|
277
|
+
boundary.to_type,
|
|
278
|
+
}
|
|
279
|
+
use_case_match = any(
|
|
280
|
+
tag in uc or uc in tag
|
|
281
|
+
for tag in pair_tags
|
|
282
|
+
for uc in archetype.use_cases
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if not use_case_match:
|
|
286
|
+
issues.append(TransitionIssue(
|
|
287
|
+
issue_type="archetype_section_mismatch",
|
|
288
|
+
critic="gesture_fit",
|
|
289
|
+
severity=0.5,
|
|
290
|
+
confidence=0.55,
|
|
291
|
+
boundary=boundary.to_dict(),
|
|
292
|
+
evidence=(
|
|
293
|
+
f"Archetype '{archetype.name}' (use_cases={archetype.use_cases}) "
|
|
294
|
+
f"doesn't match section pair {boundary.from_type} -> "
|
|
295
|
+
f"{boundary.to_type}"
|
|
296
|
+
),
|
|
297
|
+
recommended_moves=[
|
|
298
|
+
"select_different_archetype",
|
|
299
|
+
"customize_gestures_for_section_pair",
|
|
300
|
+
],
|
|
301
|
+
))
|
|
302
|
+
|
|
303
|
+
# High-risk archetype for low-energy transitions — overkill
|
|
304
|
+
if archetype.risk_profile == "high" and abs(boundary.energy_delta) < 0.15:
|
|
305
|
+
issues.append(TransitionIssue(
|
|
306
|
+
issue_type="overkill_archetype",
|
|
307
|
+
critic="gesture_fit",
|
|
308
|
+
severity=0.5,
|
|
309
|
+
confidence=0.60,
|
|
310
|
+
boundary=boundary.to_dict(),
|
|
311
|
+
evidence=(
|
|
312
|
+
f"High-risk archetype '{archetype.name}' used for low-contrast "
|
|
313
|
+
f"transition (energy_delta={boundary.energy_delta:.2f}) — "
|
|
314
|
+
f"dramatic technique for a subtle moment"
|
|
315
|
+
),
|
|
316
|
+
recommended_moves=[
|
|
317
|
+
"use_lower_risk_archetype",
|
|
318
|
+
"increase_energy_contrast_if_dramatic_intent",
|
|
319
|
+
],
|
|
320
|
+
))
|
|
321
|
+
|
|
322
|
+
return issues
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ── Run All ───────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def run_all_transition_critics(
|
|
329
|
+
boundary: TransitionBoundary,
|
|
330
|
+
plan: TransitionPlan,
|
|
331
|
+
score: TransitionScore,
|
|
332
|
+
) -> list[TransitionIssue]:
|
|
333
|
+
"""Run all 5 transition critics and return combined issues."""
|
|
334
|
+
issues: list[TransitionIssue] = []
|
|
335
|
+
issues.extend(run_boundary_clarity_critic(boundary))
|
|
336
|
+
issues.extend(run_payoff_critic(boundary, score))
|
|
337
|
+
issues.extend(run_overtelegraphing_critic(plan))
|
|
338
|
+
issues.extend(run_energy_redirection_critic(boundary))
|
|
339
|
+
issues.extend(run_gesture_fit_critic(plan))
|
|
340
|
+
return issues
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Transition Engine state models — all dataclasses with to_dict().
|
|
2
|
+
|
|
3
|
+
Pure data structures for boundary analysis, transition planning,
|
|
4
|
+
archetype selection, and scoring.
|
|
5
|
+
|
|
6
|
+
Zero I/O.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import asdict, dataclass, field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ── Boundary ──────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TransitionBoundary:
|
|
19
|
+
"""The junction between two adjacent sections."""
|
|
20
|
+
|
|
21
|
+
from_section_id: str = ""
|
|
22
|
+
to_section_id: str = ""
|
|
23
|
+
boundary_bar: int = 0
|
|
24
|
+
from_type: str = "unknown"
|
|
25
|
+
to_type: str = "unknown"
|
|
26
|
+
energy_delta: float = 0.0 # positive = energy rises
|
|
27
|
+
density_delta: float = 0.0 # positive = density rises
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict:
|
|
30
|
+
return asdict(self)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ── Archetype ─────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class TransitionArchetype:
|
|
38
|
+
"""A curated transition pattern with risk profile and verification cues."""
|
|
39
|
+
|
|
40
|
+
name: str = ""
|
|
41
|
+
description: str = ""
|
|
42
|
+
use_cases: list[str] = field(default_factory=list)
|
|
43
|
+
risk_profile: str = "low" # "low", "medium", "high"
|
|
44
|
+
devices: list[str] = field(default_factory=list)
|
|
45
|
+
gestures: list[str] = field(default_factory=list)
|
|
46
|
+
verification: list[str] = field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> dict:
|
|
49
|
+
return asdict(self)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ── Plan ──────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class TransitionPlan:
|
|
57
|
+
"""A concrete plan for executing a transition at a boundary."""
|
|
58
|
+
|
|
59
|
+
boundary: TransitionBoundary = field(default_factory=TransitionBoundary)
|
|
60
|
+
archetype: TransitionArchetype = field(default_factory=TransitionArchetype)
|
|
61
|
+
lead_in_gestures: list[dict] = field(default_factory=list)
|
|
62
|
+
arrival_gestures: list[dict] = field(default_factory=list)
|
|
63
|
+
payoff_estimate: float = 0.0 # 0.0-1.0
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> dict:
|
|
66
|
+
return {
|
|
67
|
+
"boundary": self.boundary.to_dict(),
|
|
68
|
+
"archetype": self.archetype.to_dict(),
|
|
69
|
+
"lead_in_gestures": list(self.lead_in_gestures),
|
|
70
|
+
"arrival_gestures": list(self.arrival_gestures),
|
|
71
|
+
"payoff_estimate": self.payoff_estimate,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ── Score ─────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class TransitionScore:
|
|
80
|
+
"""Multi-dimensional quality score for a transition."""
|
|
81
|
+
|
|
82
|
+
boundary_clarity: float = 0.0 # 0.0-1.0
|
|
83
|
+
payoff_strength: float = 0.0 # 0.0-1.0
|
|
84
|
+
energy_redirection: float = 0.0 # 0.0-1.0
|
|
85
|
+
identity_preservation: float = 0.0 # 0.0-1.0
|
|
86
|
+
cliche_risk: float = 0.0 # 0.0-1.0 (lower is better)
|
|
87
|
+
overall: float = 0.0 # 0.0-1.0
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> dict:
|
|
90
|
+
return asdict(self)
|