livepilot 1.9.22 → 1.9.23
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/.mcpbignore +40 -0
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +38 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +47 -72
- package/bin/livepilot.js +135 -0
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/agents/livepilot-producer/AGENT.md +13 -0
- package/livepilot/commands/arrange.md +42 -23
- package/livepilot/commands/mix.md +34 -19
- package/livepilot/commands/perform.md +31 -19
- package/livepilot/commands/sounddesign.md +38 -25
- package/livepilot/skills/livepilot-arrangement/SKILL.md +2 -1
- package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +60 -4
- package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +11 -11
- package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +25 -25
- package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +21 -21
- package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +13 -13
- package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +13 -13
- package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +5 -5
- package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +16 -16
- package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +40 -40
- package/livepilot/skills/livepilot-core/references/m4l-devices.md +3 -3
- package/livepilot/skills/livepilot-core/references/overview.md +4 -4
- package/livepilot/skills/livepilot-evaluation/SKILL.md +12 -8
- package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +2 -2
- package/livepilot/skills/livepilot-mix-engine/SKILL.md +1 -1
- package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +2 -2
- package/livepilot/skills/livepilot-mixing/SKILL.md +3 -1
- package/livepilot/skills/livepilot-notes/SKILL.md +2 -1
- package/livepilot/skills/livepilot-release/SKILL.md +15 -15
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +2 -2
- package/livepilot/skills/livepilot-wonder/SKILL.md +62 -0
- package/livepilot.mcpb +0 -0
- package/manifest.json +91 -0
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/creative_constraints/__init__.py +6 -0
- package/mcp_server/creative_constraints/engine.py +277 -0
- package/mcp_server/creative_constraints/models.py +75 -0
- package/mcp_server/creative_constraints/tools.py +341 -0
- package/mcp_server/experiment/__init__.py +6 -0
- package/mcp_server/experiment/engine.py +213 -0
- package/mcp_server/experiment/models.py +120 -0
- package/mcp_server/experiment/tools.py +263 -0
- package/mcp_server/hook_hunter/__init__.py +5 -0
- package/mcp_server/hook_hunter/analyzer.py +342 -0
- package/mcp_server/hook_hunter/models.py +57 -0
- package/mcp_server/hook_hunter/tools.py +586 -0
- package/mcp_server/memory/taste_graph.py +261 -0
- package/mcp_server/memory/tools.py +88 -0
- package/mcp_server/mix_engine/critics.py +2 -2
- package/mcp_server/mix_engine/models.py +1 -1
- package/mcp_server/mix_engine/state_builder.py +2 -2
- package/mcp_server/musical_intelligence/__init__.py +8 -0
- package/mcp_server/musical_intelligence/detectors.py +421 -0
- package/mcp_server/musical_intelligence/phrase_critic.py +163 -0
- package/mcp_server/musical_intelligence/tools.py +221 -0
- package/mcp_server/preview_studio/__init__.py +5 -0
- package/mcp_server/preview_studio/engine.py +280 -0
- package/mcp_server/preview_studio/models.py +73 -0
- package/mcp_server/preview_studio/tools.py +423 -0
- package/mcp_server/runtime/session_kernel.py +96 -0
- package/mcp_server/runtime/tools.py +90 -1
- package/mcp_server/semantic_moves/__init__.py +13 -0
- package/mcp_server/semantic_moves/compiler.py +116 -0
- package/mcp_server/semantic_moves/mix_compilers.py +291 -0
- package/mcp_server/semantic_moves/mix_moves.py +157 -0
- package/mcp_server/semantic_moves/models.py +46 -0
- package/mcp_server/semantic_moves/performance_compilers.py +208 -0
- package/mcp_server/semantic_moves/performance_moves.py +81 -0
- package/mcp_server/semantic_moves/registry.py +32 -0
- package/mcp_server/semantic_moves/resolvers.py +126 -0
- package/mcp_server/semantic_moves/sound_design_compilers.py +266 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +78 -0
- package/mcp_server/semantic_moves/tools.py +204 -0
- package/mcp_server/semantic_moves/transition_compilers.py +222 -0
- package/mcp_server/semantic_moves/transition_moves.py +76 -0
- package/mcp_server/server.py +10 -0
- package/mcp_server/session_continuity/__init__.py +6 -0
- package/mcp_server/session_continuity/models.py +86 -0
- package/mcp_server/session_continuity/tools.py +230 -0
- package/mcp_server/session_continuity/tracker.py +235 -0
- package/mcp_server/song_brain/__init__.py +6 -0
- package/mcp_server/song_brain/builder.py +477 -0
- package/mcp_server/song_brain/models.py +132 -0
- package/mcp_server/song_brain/tools.py +294 -0
- package/mcp_server/stuckness_detector/__init__.py +5 -0
- package/mcp_server/stuckness_detector/detector.py +400 -0
- package/mcp_server/stuckness_detector/models.py +66 -0
- package/mcp_server/stuckness_detector/tools.py +195 -0
- package/mcp_server/tools/_conductor.py +104 -6
- package/mcp_server/tools/analyzer.py +1 -1
- package/mcp_server/tools/devices.py +34 -0
- package/mcp_server/wonder_mode/__init__.py +6 -0
- package/mcp_server/wonder_mode/diagnosis.py +84 -0
- package/mcp_server/wonder_mode/engine.py +493 -0
- package/mcp_server/wonder_mode/session.py +114 -0
- package/mcp_server/wonder_mode/tools.py +285 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/browser.py +4 -1
- package/remote_script/LivePilot/devices.py +29 -0
- package/remote_script/LivePilot/tracks.py +11 -4
- package/scripts/generate_tool_catalog.py +131 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Hook Hunter analysis — pure computation, zero I/O.
|
|
2
|
+
|
|
3
|
+
Identifies hooks, ranks candidates, scores phrase impact, and
|
|
4
|
+
detects payoff failures.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from .models import HookCandidate, PayoffFailure, PhraseImpact
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ── Hook detection ────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def find_hook_candidates(
|
|
20
|
+
tracks: list[dict],
|
|
21
|
+
motif_data: Optional[dict] = None,
|
|
22
|
+
scene_data: Optional[list[dict]] = None,
|
|
23
|
+
composition: Optional[dict] = None,
|
|
24
|
+
) -> list[HookCandidate]:
|
|
25
|
+
"""Detect and rank hook candidates from session data.
|
|
26
|
+
|
|
27
|
+
Looks for: salient melodic motifs, distinctive rhythmic cells,
|
|
28
|
+
signature timbral textures, recurring harmonic progressions.
|
|
29
|
+
"""
|
|
30
|
+
motif_data = motif_data or {}
|
|
31
|
+
scene_data = scene_data or []
|
|
32
|
+
composition = composition or {}
|
|
33
|
+
candidates: list[HookCandidate] = []
|
|
34
|
+
|
|
35
|
+
# 1. Motif-based hooks
|
|
36
|
+
for motif in motif_data.get("motifs", []):
|
|
37
|
+
salience = motif.get("salience", 0)
|
|
38
|
+
recurrence = motif.get("recurrence", 0)
|
|
39
|
+
if salience > 0.2 or recurrence > 0.3:
|
|
40
|
+
candidates.append(HookCandidate(
|
|
41
|
+
hook_id=f"motif_{motif.get('name', 'unknown')}",
|
|
42
|
+
hook_type="melodic",
|
|
43
|
+
description=motif.get("description", motif.get("name", "motif")),
|
|
44
|
+
location=motif.get("location", ""),
|
|
45
|
+
memorability=min(1.0, salience * 1.2),
|
|
46
|
+
recurrence=recurrence,
|
|
47
|
+
contrast_potential=motif.get("contrast", 0.5),
|
|
48
|
+
development_potential=_estimate_development_potential(motif),
|
|
49
|
+
))
|
|
50
|
+
|
|
51
|
+
# 2. Track-name-based detection (lead, hook, melody, riff)
|
|
52
|
+
hook_keywords = {"lead", "hook", "melody", "riff", "main", "top", "vocal", "synth"}
|
|
53
|
+
for track in tracks:
|
|
54
|
+
name = track.get("name", "").lower()
|
|
55
|
+
if any(kw in name for kw in hook_keywords):
|
|
56
|
+
candidates.append(HookCandidate(
|
|
57
|
+
hook_id=f"track_{name.replace(' ', '_')}",
|
|
58
|
+
hook_type="melodic" if "melody" in name or "vocal" in name else "timbral",
|
|
59
|
+
description=f"Track: {track.get('name', name)}",
|
|
60
|
+
location=track.get("name", ""),
|
|
61
|
+
memorability=0.5,
|
|
62
|
+
recurrence=0.6, # present across scenes typically
|
|
63
|
+
contrast_potential=0.5,
|
|
64
|
+
development_potential=0.6,
|
|
65
|
+
))
|
|
66
|
+
|
|
67
|
+
# 3. Rhythmic hooks from drum/percussion patterns
|
|
68
|
+
rhythm_keywords = {"drum", "beat", "perc", "hat", "kick", "clap"}
|
|
69
|
+
groove_tracks = [t for t in tracks if any(kw in t.get("name", "").lower() for kw in rhythm_keywords)]
|
|
70
|
+
if groove_tracks:
|
|
71
|
+
# Check for distinctive rhythmic patterns via clip reuse
|
|
72
|
+
clip_reuse = _measure_clip_reuse(scene_data, groove_tracks)
|
|
73
|
+
if clip_reuse > 0.5:
|
|
74
|
+
candidates.append(HookCandidate(
|
|
75
|
+
hook_id="groove_pattern",
|
|
76
|
+
hook_type="rhythmic",
|
|
77
|
+
description="Primary groove pattern",
|
|
78
|
+
location=groove_tracks[0].get("name", "drums"),
|
|
79
|
+
memorability=0.5,
|
|
80
|
+
recurrence=clip_reuse,
|
|
81
|
+
contrast_potential=0.4,
|
|
82
|
+
development_potential=0.5,
|
|
83
|
+
))
|
|
84
|
+
|
|
85
|
+
# Score all candidates
|
|
86
|
+
for c in candidates:
|
|
87
|
+
c.salience = _compute_salience(c)
|
|
88
|
+
|
|
89
|
+
# Sort by salience
|
|
90
|
+
candidates.sort(key=lambda c: c.salience, reverse=True)
|
|
91
|
+
return candidates
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def find_primary_hook(
|
|
95
|
+
tracks: list[dict],
|
|
96
|
+
motif_data: Optional[dict] = None,
|
|
97
|
+
scene_data: Optional[list[dict]] = None,
|
|
98
|
+
composition: Optional[dict] = None,
|
|
99
|
+
) -> Optional[HookCandidate]:
|
|
100
|
+
"""Find the single most salient hook in the session."""
|
|
101
|
+
candidates = find_hook_candidates(tracks, motif_data, scene_data, composition)
|
|
102
|
+
return candidates[0] if candidates else None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ── Phrase impact scoring ─────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def score_phrase_impact(
|
|
109
|
+
section_data: dict,
|
|
110
|
+
target: str = "hook",
|
|
111
|
+
song_brain: Optional[dict] = None,
|
|
112
|
+
prev_section: Optional[dict] = None,
|
|
113
|
+
) -> PhraseImpact:
|
|
114
|
+
"""Score the emotional impact of a musical phrase/section.
|
|
115
|
+
|
|
116
|
+
Uses contrast, density shift, harmonic support, and energy
|
|
117
|
+
to judge whether the phrase "lands" emotionally.
|
|
118
|
+
"""
|
|
119
|
+
song_brain = song_brain or {}
|
|
120
|
+
prev_section = prev_section or {}
|
|
121
|
+
|
|
122
|
+
energy = section_data.get("energy", 0.5)
|
|
123
|
+
prev_energy = prev_section.get("energy", 0.5)
|
|
124
|
+
density = section_data.get("density", 0.5)
|
|
125
|
+
prev_density = prev_section.get("density", 0.5)
|
|
126
|
+
|
|
127
|
+
# Arrival: big energy jump = strong arrival
|
|
128
|
+
energy_delta = energy - prev_energy
|
|
129
|
+
arrival = min(1.0, max(0.0, energy_delta * 2 + 0.3))
|
|
130
|
+
|
|
131
|
+
# Anticipation: was there a dip before?
|
|
132
|
+
anticipation = min(1.0, max(0.0, (0.5 - prev_energy) * 2)) if prev_energy < 0.5 else 0.2
|
|
133
|
+
|
|
134
|
+
# Contrast: density or energy change
|
|
135
|
+
contrast = min(1.0, abs(density - prev_density) + abs(energy_delta))
|
|
136
|
+
|
|
137
|
+
# Repetition fatigue: high density with no change = fatiguing
|
|
138
|
+
fatigue = max(0.0, 1.0 - contrast) * 0.5
|
|
139
|
+
|
|
140
|
+
# Section clarity: does it have a clear role?
|
|
141
|
+
clarity = 0.7 if section_data.get("label") else 0.3
|
|
142
|
+
|
|
143
|
+
# Groove continuity: rhythm present
|
|
144
|
+
groove = 0.7 if section_data.get("has_drums", True) else 0.3
|
|
145
|
+
|
|
146
|
+
# Payoff balance
|
|
147
|
+
payoff = min(1.0, (arrival + anticipation) / 2)
|
|
148
|
+
|
|
149
|
+
# Composite — target-specific weighting
|
|
150
|
+
weights = _get_target_weights(target)
|
|
151
|
+
composite = (
|
|
152
|
+
arrival * weights.get("arrival", 0.2)
|
|
153
|
+
+ anticipation * weights.get("anticipation", 0.15)
|
|
154
|
+
+ contrast * weights.get("contrast", 0.2)
|
|
155
|
+
+ (1.0 - fatigue) * weights.get("freshness", 0.1)
|
|
156
|
+
+ clarity * weights.get("clarity", 0.1)
|
|
157
|
+
+ groove * weights.get("groove", 0.1)
|
|
158
|
+
+ payoff * weights.get("payoff", 0.15)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
section_id = section_data.get("id", section_data.get("name", ""))
|
|
162
|
+
|
|
163
|
+
return PhraseImpact(
|
|
164
|
+
phrase_id=f"phrase_{hashlib.sha256(str(section_id).encode()).hexdigest()[:8]}",
|
|
165
|
+
target=target,
|
|
166
|
+
arrival_strength=round(arrival, 3),
|
|
167
|
+
anticipation_strength=round(anticipation, 3),
|
|
168
|
+
contrast_quality=round(contrast, 3),
|
|
169
|
+
repetition_fatigue=round(fatigue, 3),
|
|
170
|
+
section_clarity=round(clarity, 3),
|
|
171
|
+
groove_continuity=round(groove, 3),
|
|
172
|
+
payoff_balance=round(payoff, 3),
|
|
173
|
+
composite_impact=round(composite, 3),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def compare_phrase_impacts(
|
|
178
|
+
impacts: list[PhraseImpact],
|
|
179
|
+
) -> list[dict]:
|
|
180
|
+
"""Rank multiple phrase impacts by composite score."""
|
|
181
|
+
ranked = sorted(impacts, key=lambda i: i.composite_impact, reverse=True)
|
|
182
|
+
return [
|
|
183
|
+
{
|
|
184
|
+
"rank": idx + 1,
|
|
185
|
+
"phrase_id": imp.phrase_id,
|
|
186
|
+
"target": imp.target,
|
|
187
|
+
"composite_impact": imp.composite_impact,
|
|
188
|
+
"arrival_strength": imp.arrival_strength,
|
|
189
|
+
"contrast_quality": imp.contrast_quality,
|
|
190
|
+
}
|
|
191
|
+
for idx, imp in enumerate(ranked)
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ── Payoff failure detection ─────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def detect_payoff_failures(
|
|
199
|
+
sections: list[dict],
|
|
200
|
+
song_brain: Optional[dict] = None,
|
|
201
|
+
) -> list[PayoffFailure]:
|
|
202
|
+
"""Detect sections that should deliver a payoff but don't."""
|
|
203
|
+
song_brain = song_brain or {}
|
|
204
|
+
payoff_targets = song_brain.get("payoff_targets", [])
|
|
205
|
+
failures: list[PayoffFailure] = []
|
|
206
|
+
|
|
207
|
+
for i, section in enumerate(sections):
|
|
208
|
+
section_id = section.get("id", section.get("name", f"section_{i}"))
|
|
209
|
+
label = section.get("label", "").lower()
|
|
210
|
+
energy = section.get("energy", 0.5)
|
|
211
|
+
prev_energy = sections[i - 1].get("energy", 0.5) if i > 0 else 0.3
|
|
212
|
+
|
|
213
|
+
is_payoff = (
|
|
214
|
+
section_id in payoff_targets
|
|
215
|
+
or label in ("chorus", "drop", "hook")
|
|
216
|
+
or section.get("is_payoff", False)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if not is_payoff:
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
# Check for flat arrival (no energy increase)
|
|
223
|
+
if energy - prev_energy < 0.1:
|
|
224
|
+
failures.append(PayoffFailure(
|
|
225
|
+
section_id=section_id,
|
|
226
|
+
expected_target=label or "payoff",
|
|
227
|
+
failure_type="flat_arrival",
|
|
228
|
+
severity=0.6,
|
|
229
|
+
suggestion="Increase energy contrast — try subtracting before the payoff section",
|
|
230
|
+
))
|
|
231
|
+
|
|
232
|
+
# Check for weak contrast (only if flat_arrival didn't already fire)
|
|
233
|
+
elif i > 0 and abs(energy - prev_energy) < 0.05:
|
|
234
|
+
failures.append(PayoffFailure(
|
|
235
|
+
section_id=section_id,
|
|
236
|
+
expected_target=label or "payoff",
|
|
237
|
+
failure_type="weak_contrast",
|
|
238
|
+
severity=0.5,
|
|
239
|
+
suggestion="Add density or timbral contrast leading into this section",
|
|
240
|
+
))
|
|
241
|
+
|
|
242
|
+
return failures
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def suggest_payoff_repairs(
|
|
246
|
+
failures: list[PayoffFailure],
|
|
247
|
+
) -> list[dict]:
|
|
248
|
+
"""Generate repair suggestions for payoff failures."""
|
|
249
|
+
repairs = []
|
|
250
|
+
for f in failures:
|
|
251
|
+
repair = {
|
|
252
|
+
"section_id": f.section_id,
|
|
253
|
+
"failure_type": f.failure_type,
|
|
254
|
+
"severity": f.severity,
|
|
255
|
+
"suggestion": f.suggestion,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# Add specific repair strategies
|
|
259
|
+
if f.failure_type == "flat_arrival":
|
|
260
|
+
repair["strategies"] = [
|
|
261
|
+
"Add a 2-4 bar breakdown before this section",
|
|
262
|
+
"Use a filter sweep or riser to build anticipation",
|
|
263
|
+
"Strip elements in the preceding section to create contrast",
|
|
264
|
+
]
|
|
265
|
+
elif f.failure_type == "weak_contrast":
|
|
266
|
+
repair["strategies"] = [
|
|
267
|
+
"Increase track count or add a new element at the payoff",
|
|
268
|
+
"Change the harmonic content (key change, chord substitution)",
|
|
269
|
+
"Add rhythmic variation (double-time feel, new percussion)",
|
|
270
|
+
]
|
|
271
|
+
elif f.failure_type == "no_setup":
|
|
272
|
+
repair["strategies"] = [
|
|
273
|
+
"Add a buildup section before the payoff",
|
|
274
|
+
"Use automation to create a gradual energy ramp",
|
|
275
|
+
]
|
|
276
|
+
else:
|
|
277
|
+
repair["strategies"] = [f.suggestion]
|
|
278
|
+
|
|
279
|
+
repairs.append(repair)
|
|
280
|
+
|
|
281
|
+
return repairs
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ── Helpers ───────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _compute_salience(c: HookCandidate) -> float:
|
|
288
|
+
"""Compute composite salience score for a hook candidate."""
|
|
289
|
+
return round(
|
|
290
|
+
c.memorability * 0.35
|
|
291
|
+
+ c.recurrence * 0.25
|
|
292
|
+
+ c.contrast_potential * 0.2
|
|
293
|
+
+ c.development_potential * 0.2,
|
|
294
|
+
3,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _estimate_development_potential(motif: dict) -> float:
|
|
299
|
+
"""Estimate how much room a motif has for development."""
|
|
300
|
+
# Simple heuristic: shorter motifs have more development room
|
|
301
|
+
length = motif.get("length_beats", 4)
|
|
302
|
+
if length <= 2:
|
|
303
|
+
return 0.8
|
|
304
|
+
elif length <= 4:
|
|
305
|
+
return 0.6
|
|
306
|
+
elif length <= 8:
|
|
307
|
+
return 0.4
|
|
308
|
+
return 0.3
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _measure_clip_reuse(scene_data: list[dict], target_tracks: list[dict]) -> float:
|
|
312
|
+
"""Measure how much clips are reused across scenes for target tracks."""
|
|
313
|
+
if not scene_data:
|
|
314
|
+
return 0.0
|
|
315
|
+
|
|
316
|
+
target_names = {t.get("name", "").lower() for t in target_tracks}
|
|
317
|
+
clip_names = Counter()
|
|
318
|
+
|
|
319
|
+
for scene in scene_data:
|
|
320
|
+
for clip in scene.get("clips", []):
|
|
321
|
+
clip_name = clip.get("name", "") if isinstance(clip, dict) else str(clip)
|
|
322
|
+
track_name = clip.get("track", "") if isinstance(clip, dict) else ""
|
|
323
|
+
if track_name.lower() in target_names and clip_name:
|
|
324
|
+
clip_names[clip_name] += 1
|
|
325
|
+
|
|
326
|
+
if not clip_names:
|
|
327
|
+
return 0.0
|
|
328
|
+
|
|
329
|
+
max_reuse = max(clip_names.values())
|
|
330
|
+
return min(1.0, max_reuse / max(len(scene_data), 1))
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _get_target_weights(target: str) -> dict:
|
|
334
|
+
"""Get scoring weights based on target type."""
|
|
335
|
+
presets = {
|
|
336
|
+
"hook": {"arrival": 0.15, "anticipation": 0.1, "contrast": 0.2, "freshness": 0.15, "clarity": 0.1, "groove": 0.1, "payoff": 0.2},
|
|
337
|
+
"drop": {"arrival": 0.3, "anticipation": 0.2, "contrast": 0.2, "freshness": 0.05, "clarity": 0.05, "groove": 0.1, "payoff": 0.1},
|
|
338
|
+
"chorus": {"arrival": 0.2, "anticipation": 0.15, "contrast": 0.15, "freshness": 0.1, "clarity": 0.15, "groove": 0.1, "payoff": 0.15},
|
|
339
|
+
"transition": {"arrival": 0.1, "anticipation": 0.1, "contrast": 0.3, "freshness": 0.1, "clarity": 0.1, "groove": 0.15, "payoff": 0.15},
|
|
340
|
+
"loop": {"arrival": 0.05, "anticipation": 0.05, "contrast": 0.1, "freshness": 0.25, "clarity": 0.1, "groove": 0.3, "payoff": 0.15},
|
|
341
|
+
}
|
|
342
|
+
return presets.get(target, presets["hook"])
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Hook Hunter data models — pure dataclasses, zero I/O."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class HookCandidate:
|
|
11
|
+
"""A potential hook element in the track."""
|
|
12
|
+
|
|
13
|
+
hook_id: str = ""
|
|
14
|
+
hook_type: str = "" # "melodic", "rhythmic", "timbral", "harmonic", "textural"
|
|
15
|
+
description: str = ""
|
|
16
|
+
location: str = "" # track/clip reference
|
|
17
|
+
memorability: float = 0.0 # 0-1 how catchy/memorable
|
|
18
|
+
recurrence: float = 0.0 # 0-1 how often it appears
|
|
19
|
+
contrast_potential: float = 0.0 # 0-1 how well it stands out
|
|
20
|
+
development_potential: float = 0.0 # 0-1 how much room to develop
|
|
21
|
+
salience: float = 0.0 # composite score
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict:
|
|
24
|
+
return asdict(self)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class PhraseImpact:
|
|
29
|
+
"""Phrase-level emotional impact scoring."""
|
|
30
|
+
|
|
31
|
+
phrase_id: str = ""
|
|
32
|
+
target: str = "" # "hook", "drop", "chorus", "transition", "loop"
|
|
33
|
+
arrival_strength: float = 0.0 # 0-1 does it feel like an arrival?
|
|
34
|
+
anticipation_strength: float = 0.0 # 0-1 does the setup work?
|
|
35
|
+
contrast_quality: float = 0.0 # 0-1 is there enough change?
|
|
36
|
+
repetition_fatigue: float = 0.0 # 0-1 is it overused?
|
|
37
|
+
section_clarity: float = 0.0 # 0-1 is the section role clear?
|
|
38
|
+
groove_continuity: float = 0.0 # 0-1 does the groove carry through?
|
|
39
|
+
payoff_balance: float = 0.0 # 0-1 setup vs payoff balance
|
|
40
|
+
composite_impact: float = 0.0 # weighted aggregate
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict:
|
|
43
|
+
return asdict(self)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class PayoffFailure:
|
|
48
|
+
"""A detected payoff failure — where the song should deliver but doesn't."""
|
|
49
|
+
|
|
50
|
+
section_id: str = ""
|
|
51
|
+
expected_target: str = "" # "drop", "chorus", "hook"
|
|
52
|
+
failure_type: str = "" # "flat_arrival", "weak_contrast", "no_setup", "hook_absent"
|
|
53
|
+
severity: float = 0.0 # 0-1
|
|
54
|
+
suggestion: str = ""
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> dict:
|
|
57
|
+
return asdict(self)
|