Commit c5963f63 by PLN (Algolia)

feat(foundry): lyric-aware vocal sampler (engine/vox.py) — phrase-level,…

feat(foundry): lyric-aware vocal sampler (engine/vox.py) — phrase-level, hook-ranked, loop-capable vox one-shots

PROBLEM. The Foundry's vocal path (loops.analyze_chops) cuts vocals blind to what
is sung — onset→onset slices scored on seam/zc mechanics. But a vocal one-shot's
value to a livecoder IS the lyric. The freshly-shipped superfreak dub kit had four
tiny 0.35-0.65s onset chops (03-06) with no idea they sat on top of some of the most
iconic lyrics in funk ("she's a super freak", "kinky girl", "the kind you read about").

APPROACH. engine/vox.py: transcribe → phrase-segment → iconicity-rank → cut → grade →
lyric-name, DRY-reusing grade.py sub-scorers and naming.py convention.
 - transcribe: whisper word-level timestamps, shelled out like separate.py→demucs,
   cached at workspace/vox_transcript.json so re-runs are instant. Model escalates
   small→medium empirically when the known hooks don't surface (small garbled the
   patois delivery; medium recovers the real lyrics).
 - segment: group words on inter-word gaps (>=0.45s), cap 6s, split over-long phrases
   at their widest interior gap; keep per-phrase avg ASR confidence.
 - iconicity: feature-engineered rank = repetition (normalized phrase text + content
   n-grams recurring across the track — hooks repeat) + hook keywords (title-derived
   or --hooks) + clarity (ASR prob) + energy (RMS vs stem median) + duration sweet-spot.
 - cut: pre-pad + post-pad (clamped to next phrase), zero-crossing snap both edges,
   3-10ms fades so edge samples are ~0 regardless of where the snap landed; as-cut level.
 - kit-level loopability: report each phrase's duration in BEATS at the kit BPM; flag
   loop-capable phrases (within ±3% of a 1/2/4-bar multiple) and emit a bar-quantized
   _loop variant (tail pulled to the exact beat grid, still zc-snapped) for the best
   hooks, so a vocal can loopAt alongside instrument loops.
 - name: NN_vox_<lyricslug> (naming.lint-clean), dedup identical texts (best-graded
   instance, up to 2 takes of THE hook).

VALIDATION. 17 new mocked-transcription tests (whisper never runs in tests): gap
segmentation, over-long split, iconicity repetition-beats-oneoff + hook-keyword boost,
beat/loop annotation, cut edges ~0 after zc+fade, next-start clamp, exact-beat loop
variant, lyric-slug + lint contract. Full suite 60->77 green. Applied to the superfreak
stem: medium transcript contains every iconic line; shipped 6 curated vox files (S/B
tier) replacing the 4 blind chops, incl. one 2-bar (8.00-beat) loop variant. All
re-graded on disk: no clip (peaks <0.8), no DC (<2e-4), edges exactly 0.0.
parent 791a0d78
"""Tests for the lyric-aware vocal sampler (engine/vox.py).
Transcription is ALWAYS mocked — whisper never runs in tests. We inject a
whisper-shaped transcript dict (segments[].words[]) and exercise the pure
pipeline: phrase grouping on gaps, iconicity (repetition + hook keyword),
cut edge properties (zc + fade ⇒ edges ≈ 0), beat/loop annotation, and the
lyric-slug naming / lint contract.
"""
import sys
from pathlib import Path
import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from engine import vox as V
from engine import naming as N
# ── helpers ──────────────────────────────────────────────────────────────────
def _w(text, start, end, prob=0.9):
return {"word": text, "start": start, "end": end, "probability": prob}
def _transcript(words):
"""Wrap a flat word list as a single whisper segment dict."""
return {"segments": [{"words": words}]}
# ── phrase segmentation ────────────────────────────────────────────────────────
def test_segment_splits_on_gap():
# two words tight, then a 0.6s gap, then two more → two phrases
words = V.words_from_transcript(_transcript([
_w(" she's", 0.0, 0.3), _w(" super", 0.3, 0.6),
_w(" freak", 1.5, 1.8), _w(" now", 1.8, 2.0),
]))
phrases = V.segment_phrases(words, gap_s=0.45)
assert len(phrases) == 2
assert phrases[0].text == "she's super"
assert phrases[1].text == "freak now"
def test_segment_no_split_within_gap():
words = V.words_from_transcript(_transcript([
_w(" a", 0.0, 0.2), _w(" b", 0.3, 0.5), _w(" c", 0.6, 0.8),
]))
phrases = V.segment_phrases(words, gap_s=0.45)
assert len(phrases) == 1
assert phrases[0].text == "a b c"
def test_segment_splits_overlong_phrase_at_widest_gap():
# one contiguous run (all gaps < 0.45) but > max_phrase_s → split at widest gap
words = V.words_from_transcript(_transcript([
_w(" one", 0.0, 1.0), _w(" two", 1.2, 2.0),
_w(" three", 2.4, 3.0), _w(" four", 3.1, 4.0),
]))
phrases = V.segment_phrases(words, gap_s=0.45, max_phrase_s=3.0)
assert len(phrases) == 2 # split, widest interior gap is 2.0→2.4
assert phrases[0].text == "one two"
assert phrases[1].text == "three four"
def test_phrase_conf_is_mean_word_prob():
words = V.words_from_transcript(_transcript([
_w(" x", 0.0, 0.2, prob=0.8), _w(" y", 0.2, 0.4, prob=1.0),
]))
p = V.segment_phrases(words)[0]
assert abs(p.conf - 0.9) < 1e-6
# ── iconicity ────────────────────────────────────────────────────────────────
def _phrase(text, start=0.0, dur=1.0, conf=0.9):
return V.Phrase(text=text, start=start, end=start + dur, conf=conf)
def test_iconicity_repetition_beats_oneoff():
# "super freak" recurs 3×; "random mumble" once → repeated phrase ranks higher
phrases = [
_phrase("super freak", 0), _phrase("super freak", 5),
_phrase("super freak", 10), _phrase("random mumble here", 15),
]
ranked = V.rank_phrases(phrases, hooks=None)
assert ranked[0].text == "super freak"
hook = next(p for p in ranked if p.text == "super freak")
oneoff = next(p for p in ranked if p.text == "random mumble here")
assert hook.repeat > oneoff.repeat
assert hook.iconicity > oneoff.iconicity
def test_iconicity_hook_keyword_boost():
a = _phrase("super freak", 0)
b = _phrase("walking down street", 5)
V.rank_phrases([a, b], hooks=["super", "freak"])
assert a.hook > b.hook
assert a.iconicity > b.iconicity
def test_hooks_from_title_drops_boilerplate():
hooks = V.hooks_from_title("Soul Sugar meets Dub Shepherds ft. Jolly Joseph ★ Super freak ★")
assert "super" in hooks and "freak" in hooks
assert "meets" not in hooks and "ft" not in hooks
# ── beat / loop annotation ─────────────────────────────────────────────────────
def test_annotate_beats_flags_loop_capable():
beat_s = 60.0 / 132.5
on_grid = _phrase("kinky girl", 0.0, dur=4 * beat_s) # exactly a bar
off_grid = _phrase("home to mother", 0.0, dur=2.5 * beat_s) # 2.5 beats, off
V.annotate_beats([on_grid, off_grid], bpm=132.5)
assert on_grid.loop_capable and on_grid.loop_beats == 4
assert abs(on_grid.beats - 4.0) < 0.05
assert not off_grid.loop_capable
# ── cut edge properties ─────────────────────────────────────────────────────────
def _tone(sr, dur, freq=220.0, amp=0.5):
t = np.arange(int(sr * dur)) / sr
return (amp * np.sin(2 * np.pi * freq * t)).astype(np.float32)
def test_cut_edges_near_zero_after_fade():
sr = 44100
y = _tone(sr, 3.0) # continuous tone: no natural zero at cut point
p = _phrase("test phrase", start=1.0, dur=0.6)
clip = V.cut_phrase(y, sr, p)
assert clip.shape[0] > 100
# fade guarantees the very edges are ~0 regardless of zc landing
assert abs(float(clip[0, 0])) < 1e-3
assert abs(float(clip[-1, 0])) < 1e-3
def test_cut_respects_next_start():
sr = 44100
y = _tone(sr, 4.0)
p = _phrase("first", start=1.0, dur=0.5) # ends 1.5; post-pad would reach ~1.62
clip = V.cut_phrase(y, sr, p, next_start=1.55)
end_t = 1.0 - V.PRE_PAD_S + clip.shape[0] / sr
assert end_t <= 1.55 + 0.02 # never runs into the next phrase
def test_cut_phrase_loop_is_exact_beat_multiple():
sr, bpm = 44100, 132.5
beat_s = 60.0 / bpm
y = _tone(sr, 6.0)
p = _phrase("super freak", start=1.0, dur=4 * beat_s)
V.annotate_beats([p], bpm=bpm)
assert p.loop_capable
clip = V.cut_phrase_loop(y, sr, p, bpm=bpm)
got_beats = (clip.shape[0] / sr) / beat_s
assert abs(got_beats - p.loop_beats) < 0.06 # within zc-snap slack of exact grid
def test_cut_phrase_loop_none_when_not_capable():
sr = 44100
y = _tone(sr, 3.0)
p = _phrase("off grid", start=0.5, dur=0.37)
V.annotate_beats([p], bpm=132.5)
if not p.loop_capable:
assert V.cut_phrase_loop(y, sr, p) is None
# ── lyric slug naming / lint ─────────────────────────────────────────────────────
def test_lyric_slug_from_content_words():
assert V.lyric_slug("she's a super freak") == "superfreak" # stopwords dropped
assert V.lyric_slug("the kind you don't take home to mother") == "kindtakehome"
def test_lyric_slug_falls_back_on_all_stopwords():
slug = V.lyric_slug("oh yeah")
assert slug and slug.isalnum()
def test_lyric_name_passes_lint():
slug = V.lyric_slug("she's a very kinky girl")
name = N.suggest_name(3, "vox", character=slug)
assert N.lint(name) == []
assert name.startswith("03_vox_")
def test_lyric_name_dedupes():
slug = V.lyric_slug("super freak")
taken = {N.suggest_name(3, "vox", character=slug)}
n2 = N.suggest_name(4, "vox", character=slug, taken=taken)
assert n2 not in taken
assert N.lint(n2) == []
def test_grade_cut_returns_one_shot_for_short():
sr = 44100
clip = _tone(sr, 0.5)[:, None] # 0.5s < one_shot threshold
g = V.grade_cut(clip, sr)
assert g.kind == "one_shot"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment