Commit 1b81e5ea by PLN (Algolia)

feat(slop): clip-idea planner — 7 OPAL tracks are cuttable today, and Slopmotion was never missing

Two findings, one of which unblocked a task that had been stuck on a false premise.

THE FALSE PREMISE. visuals/slop/README.md said Slopmotion was "not installed here yet —
this is the real blocker", after a thorough machine search on 2026-07-25. That was wrong.
Kevin Granger IS Shipow (git log on the repo shows both identities authoring the same
commits), so Slopmotion is hydra-live-hexa — a registered working directory of this
workspace all along. Task #28 was blocked on nothing. The lesson worth keeping: resolve
the PERSON before concluding the SOFTWARE is missing. README corrected in place, with the
wrong claim quoted rather than silently deleted.

WHAT THAT UNLOCKS. Everything needed to generate is already on disk, no network at all:
764 loop mp4s across 23 playsets (including a `parvagues` pack of PLN's own imagery); a
24-pack catalog in src/data/playsetPacks.json which is a generated snapshot of the online
Supabase `packs` table; playwright 1.60 + chromium; and a working headless harness to copy
in scripts/capture_fx_previews.mjs. This matters because the online route is CLOSED:
shipow.io is NXDOMAIN, hydra-live-hexa.vercel.app returns DEPLOYMENT_NOT_FOUND, and .env
carries only HEXA_LOGIN/HEXA_PASS with no VITE_SUPABASE_URL — so the DB can't be reached
from here, and doesn't need to be.

THE PLANNER. visuals/slop/plan_clips.py joins three sources to answer "which shareable
clips can we make right now, for tracks actually in the OPAL set?" Answer: 7 of 13.
  - setlist from backlog.md § OPAL 2026, with MEASURED setcps from the BPM-arc doc rather
    than the backlog's annotations (which were wrong on three tracks).
  - recordings = Take89 (Montreuil Algorave V3, 2026-05-22): PLN-ear-validated boundaries,
    plus per-track peak-anchored 30s/90s promo_cuts already computed by the drops detector.
  - visuals = the playset catalog.
Cuttable: WAP, Take 5 Drops, Piment Bresilien, Mafia, Sunshine, Desire, Revolution. The
6 that aren't (Bombe, Doing it Right, Perfect, Gimme Acid, Vague de CRIME, Electric
Hammer) are the newer/reworked tracks with no Take89 take — they need a fresh recording,
which gig_record.sh will produce at the next rehearsal or at OPAL itself.

Playset/FX pairing is driven by MEASURED features, not vibes: tilt_mean (spectral tilt)
picks the dark/mid/bright playset family, kick_density_p70 decides whether the chain is
trigger-heavy (jumpCut/vibration/flash on kick) or breathing (liquix/wetLens/slowmo), and
ssm_novelty_max sets the Automix interval. Thresholds are the terciles of THIS set's
observed range (tilt 0.13..0.54, kick 1.75..3.75), so they split real material rather than
an abstract scale. The one frankly aesthetic layer — which pack names read as "dark" vs
"bright" — is marked as such in the source.

VALIDATION. Master-timeline offsets cross-check against an independent source: the planner
derives Desire's track start from the studio bundle's boundaries_ground_truth.json and
lands on 2926s, which is exactly the 48:46 cut in armada's separately-authored
boundaries_take89_validated.json. Two files, two provenances, same number.

Renderer (audio slice + playset -> mp4 via the real Hydra engine) is NOT built yet.
parent 52a05f96
# Slop clip ideas — OPAL 2026 x Take89
**7 OPAL tracks have a real recording we can cut from today** (6 do not). Source take: Take89 = Montreuil Algorave V3, 2026-05-22.
Every clip below is a *peak-anchored* window found by the drops detector, offset onto the master timeline using PLN's ear-validated boundaries. Playset + FX pairing is derived from measured audio features (see each row).
| # | Track | BPM | tilt | kick | nov | family | playsets | 30s cut (master) |
|---|-------|-----|------|------|-----|--------|----------|------------------|
| 2 | WAP | 133 | 0.25 | 2.25 | 10.8 | mid | liquid-metal, stripe-resonance, liminal-spaces | 24:53 |
| 4 | Take five Drops | 124 | 0.14 | 2.00 | 12.9 | dark | obsidian-tide, obsidian-iris, behind-screen | 17:05 |
| 5 | Piment Bresilien | 124 | 0.41 | 2.00 | 16.3 | mid | liquid-metal, stripe-resonance, liminal-spaces | 8:50 |
| 9 | Mafia | 160 | 0.52 | 2.75 | 10.0 | bright | iridescent, white-hole, fire-sparkles | 35:28 |
| 10 | Sunshine | 144 | 0.30 | 3.00 | 8.3 | mid | liquid-metal, stripe-resonance, liminal-spaces | 46:42 |
| 11 | Desire | 127 | 0.54 | 2.25 | 8.5 | bright | iridescent, white-hole, fire-sparkles | 51:40 |
| 12 | La Revolution Sera Samplee | 114 | 0.16 | 2.50 | 14.2 | dark | obsidian-tide, obsidian-iris, behind-screen | 72:01 |
## Not cuttable yet
- **Ceci n'est pas Une Bombe** (`bombe_dj`) — no Take89 recording — never played at Montreuil (or added since)
- **Am i Doing it Right?** (`do_it_right`) — no Take89 recording — never played at Montreuil (or added since)
- **Perfect <3** (`perfect`) — no Take89 recording — never played at Montreuil (or added since)
- **Gimme Acid** (`gimme_acid`) — no Take89 recording — never played at Montreuil (or added since)
- **Vague de CRIME** (`vague_de_crime`) — no Take89 recording — never played at Montreuil (or added since)
- **Electric Hammer** (`electric_hammer`) — no Take89 recording — never played at Montreuil (or added since)
These are the newer / reworked tracks. To get promo clips for them we need a fresh recording — which the always-on `tools/gig_record.sh` will produce at the next rehearsal or at OPAL itself.
## Rendering one
```bash
cd /home/pln/Work/Sound/hydra-live-hexa && nvm use 22
npm run dev & # the renderer drives the real app
node scripts/render_slop_clip.mjs \
--ideas ../Tidal/visuals/slop/clip_ideas.json --pick 11 --cut 30s \
--out ../Tidal/visuals/slop/out/
```
...@@ -17,14 +17,36 @@ a lot of work into it recently (new capabilities / capas). Two horizons for us: ...@@ -17,14 +17,36 @@ a lot of work into it recently (new capabilities / capas). Two horizons for us:
3. **Assess as engine** (later) — does it fit the "perf-isolated backdrop" law 3. **Assess as engine** (later) — does it fit the "perf-isolated backdrop" law
(never stalls audio/editor)? Compare with HUD Feature B (#22). (never stalls audio/editor)? Compare with HUD Feature B (#22).
## Pointers (fill in as we learn) ## Pointers
- Slopmotion source / repo: **TODO — get from Kevin** (path or URL). Searched this
machine thoroughly (2026-07-25) — not installed here yet; this is the real blocker > **CORRECTION (2026-07-26).** The line that used to sit here — "not installed on this
on actually running a first generation. > machine, this is the real blocker" — was **wrong**, and it blocked #28 for nothing.
- Install / run notes: TODO > **Kevin Granger IS Shipow** (`git log` on the repo below shows both identities on the
- Input formats it wants (audio? stems? images?): TODO > same commits), so **Slopmotion is `hydra-live-hexa`** — a registered working directory
- Export settings for Insta (aspect, codec, length): TODO — target H.264 mp4, > of this workspace the whole time. Lesson: resolve the *person* before concluding the
vertical 1080×1920 for stories/reels, ≤60s. > *software* is missing.
- **Source / repo:** `/home/pln/Work/Sound/hydra-live-hexa`
(`git@github.com:Shipow/hydra-live-hexa.git`). Read its `CLAUDE.md` + `AGENTS.md` first.
- **Run notes:** **Node 22 required** (`nvm use 22`; system node is 16 and fails).
`npm run dev``http://localhost:5173`. Studio mode is `?studio=1` (the `?` matters).
- **Headless:** `playwright` 1.60 + chromium are already installed, and
`scripts/capture_fx_previews.mjs` is a working headless harness to copy — it drives the
real app through `window.__hydraApplyScene` / `__hydraSetPreviewVideo` /
`__hydraSetTriggerGate` / `hydraSettings.fx` / `customBands`, with swiftshader GL.
- **Assets already local — no download needed:** **764 loop mp4s** across 23 playsets in
`public/loops/`, including a `parvagues` pack (26 clips of PLN's own imagery).
- **Playset/settings catalog:** `src/data/playsetPacks.json` (24 packs) is a *generated
snapshot of the online Supabase `packs` table*, so the local file is enough — no DB
round-trip required. (The deployed app was unreachable on 2026-07-26: `shipow.io`
NXDOMAIN, `hydra-live-hexa.vercel.app``DEPLOYMENT_NOT_FOUND`. `.env` holds only
`HEXA_LOGIN`/`HEXA_PASS`, no `VITE_SUPABASE_URL`, so the DB is not directly reachable
from here anyway — and doesn't need to be.)
- **Audio already local:** the shipped ParVagues studio bundle at
`public/audio/Recordings/Montreuil26_master/studio/` — the 75 MB Montreuil26 Opus
master (= **Take89**), ear-validated `boundaries_ground_truth.json`, and per-track
`highlights/` with **peak-anchored 30s + 90s `promo_cuts`** already computed.
- Export settings for Insta: TODO — target H.264 mp4, vertical 1080×1920, ≤60s.
## Staged test input (ready the moment Slopmotion is available) ## Staged test input (ready the moment Slopmotion is available)
`samples/opal2024_mauerpark_173s-233s.flac` — 60s excerpt, 2:53–3:53 of `samples/opal2024_mauerpark_173s-233s.flac` — 60s excerpt, 2:53–3:53 of
......
{
"schema": "slop clip ideas — OPAL 2026 setlist x Take89 x Slopmotion playsets",
"generated_by": "visuals/slop/plan_clips.py",
"provenance": {
"setlist": {
"src": "file:backlog.md § OPAL 2026",
"type": "file"
},
"tempi": {
"src": "file:docs/2026-07-25-opal-bpm-arc.md (measured setcps)",
"type": "derived"
},
"boundaries": {
"src": "file:armada/tide-table/boundaries_take89_validated.json",
"type": "user",
"note": "PLN ear-validated 2026-06-05"
},
"highlights": {
"src": "file:/home/pln/Work/Sound/hydra-live-hexa/public/audio/Recordings/Montreuil26_master/studio/tracks_bandcamp/highlights",
"type": "derived",
"note": "rule-based drops detection, stem-activity + kick GT + Foote SSM"
},
"playsets": {
"src": "file:hydra-live-hexa/src/data/playsetPacks.json",
"type": "derived",
"note": "local generated snapshot of Supabase packs"
}
},
"gig": {
"name": "OPAL 2026",
"note": "gig metadata is NOT canonical here — see Web/www next/content/lives/"
},
"n_ideas": 7,
"n_unmatched": 6,
"ideas": [
{
"setlist_pos": 2,
"opal_title": "WAP",
"take89_title": "W.I.P. W.A.P.",
"tidal_stem": "wap",
"genre_tag": "lightT",
"measured_bpm": 133,
"take": "Take89",
"master_audio": "/home/pln/Work/Sound/hydra-live-hexa/public/audio/Recordings/Montreuil26_master/studio/Montreuil26_streaming_final.opus",
"track_master_start": 1405.0,
"track_duration": 229.0,
"measured": {
"tilt_mean": 0.251,
"kick_density_p70": 2.25,
"ssm_novelty_max": 10.793,
"n_active_mean": 3.69
},
"promo_cuts": {
"30s": {
"track_relative_start": 88.97,
"master_start": 1493.97,
"duration": 29.78,
"anchor": "peak"
},
"90s": {
"track_relative_start": 213.71,
"master_start": 1618.7,
"duration": 15.29,
"anchor": "drop"
}
},
"visual": {
"tilt_family": "mid",
"playsets": [
"liquid-metal",
"stripe-resonance",
"liminal-spaces"
],
"alternate_playset": "parvagues",
"fx": {
"glitch": {
"base": 0.35
},
"rgbDelay": {
"base": 0.3
},
"liquix": {
"base": 0.55
},
"wetLens": {
"base": 0.6
},
"slowmo": {
"base": 0.45
}
},
"automixSeconds": 8.0,
"triggerHeavy": false
}
},
{
"setlist_pos": 4,
"opal_title": "Take five Drops",
"take89_title": "Take 5 Drops",
"tidal_stem": "take_5_drops",
"genre_tag": "jazzT",
"measured_bpm": 124,
"take": "Take89",
"master_audio": "/home/pln/Work/Sound/hydra-live-hexa/public/audio/Recordings/Montreuil26_master/studio/Montreuil26_streaming_final.opus",
"track_master_start": 716.0,
"track_duration": 418.0,
"measured": {
"tilt_mean": 0.141,
"kick_density_p70": 2.0,
"ssm_novelty_max": 12.877,
"n_active_mean": 3.75
},
"promo_cuts": {
"30s": {
"track_relative_start": 309.66,
"master_start": 1025.66,
"duration": 30.72,
"anchor": "peak"
},
"90s": {
"track_relative_start": 52.24,
"master_start": 768.25,
"duration": 88.55,
"anchor": "drop"
}
},
"visual": {
"tilt_family": "dark",
"playsets": [
"obsidian-tide",
"obsidian-iris",
"behind-screen"
],
"alternate_playset": "parvagues",
"fx": {
"glitch": {
"base": 0.35
},
"rgbDelay": {
"base": 0.3
},
"liquix": {
"base": 0.55
},
"wetLens": {
"base": 0.6
},
"slowmo": {
"base": 0.45
},
"glow": {
"base": 0.45
}
},
"automixSeconds": 4.0,
"triggerHeavy": false
}
},
{
"setlist_pos": 5,
"opal_title": "Piment Bresilien",
"take89_title": "Piment brésilien",
"tidal_stem": "piment_bresilien",
"genre_tag": "hardT",
"measured_bpm": 124,
"take": "Take89",
"master_audio": "/home/pln/Work/Sound/hydra-live-hexa/public/audio/Recordings/Montreuil26_master/studio/Montreuil26_streaming_final.opus",
"track_master_start": 362.0,
"track_duration": 354.0,
"measured": {
"tilt_mean": 0.413,
"kick_density_p70": 2.0,
"ssm_novelty_max": 16.265,
"n_active_mean": 3.29
},
"promo_cuts": {
"30s": {
"track_relative_start": 168.66,
"master_start": 530.66,
"duration": 30.24,
"anchor": "peak"
},
"90s": {
"track_relative_start": 240.35,
"master_start": 602.35,
"duration": 90.16,
"anchor": "drop"
}
},
"visual": {
"tilt_family": "mid",
"playsets": [
"liquid-metal",
"stripe-resonance",
"liminal-spaces"
],
"alternate_playset": "parvagues",
"fx": {
"glitch": {
"base": 0.35
},
"rgbDelay": {
"base": 0.3
},
"liquix": {
"base": 0.55
},
"wetLens": {
"base": 0.6
},
"slowmo": {
"base": 0.45
}
},
"automixSeconds": 4.0,
"triggerHeavy": false
}
},
{
"setlist_pos": 9,
"opal_title": "Mafia",
"take89_title": "Aria Sans Serif",
"tidal_stem": "mafia_sans_serif",
"genre_tag": "Hdnb",
"measured_bpm": 160,
"take": "Take89",
"master_audio": "/home/pln/Work/Sound/hydra-live-hexa/public/audio/Recordings/Montreuil26_master/studio/Montreuil26_streaming_final.opus",
"track_master_start": 2056.74,
"track_duration": 255.26,
"measured": {
"tilt_mean": 0.52,
"kick_density_p70": 2.75,
"ssm_novelty_max": 10.043,
"n_active_mean": 3.91
},
"promo_cuts": {
"30s": {
"track_relative_start": 71.32,
"master_start": 2128.06,
"duration": 30.57,
"anchor": "peak"
},
"90s": {
"track_relative_start": 56.89,
"master_start": 2113.63,
"duration": 90.0,
"anchor": "peak"
}
},
"visual": {
"tilt_family": "bright",
"playsets": [
"iridescent",
"white-hole",
"fire-sparkles"
],
"alternate_playset": "parvagues",
"fx": {
"glitch": {
"base": 0.55
},
"rgbDelay": {
"base": 0.3
},
"jumpCut": {
"base": 1.0,
"isTrigger": true,
"syncBand": "kick"
},
"vibration": {
"base": 0.5,
"isTrigger": true,
"syncBand": "kick"
},
"flash": {
"base": 0.45,
"isTrigger": true,
"syncBand": "kick"
},
"lumaPrint": {
"base": 0.7
}
},
"automixSeconds": 8.0,
"triggerHeavy": true
}
},
{
"setlist_pos": 10,
"opal_title": "Sunshine",
"take89_title": "You My Sunshine",
"tidal_stem": "you_my_sunshine",
"genre_tag": "Hdnb",
"measured_bpm": 144,
"take": "Take89",
"master_audio": "/home/pln/Work/Sound/hydra-live-hexa/public/audio/Recordings/Montreuil26_master/studio/Montreuil26_streaming_final.opus",
"track_master_start": 2599.74,
"track_duration": 318.68,
"measured": {
"tilt_mean": 0.299,
"kick_density_p70": 3.0,
"ssm_novelty_max": 8.336,
"n_active_mean": 4.02
},
"promo_cuts": {
"30s": {
"track_relative_start": 203.12,
"master_start": 2802.86,
"duration": 30.0,
"anchor": "peak"
},
"90s": {
"track_relative_start": 21.91,
"master_start": 2621.65,
"duration": 88.86,
"anchor": "drop"
}
},
"visual": {
"tilt_family": "mid",
"playsets": [
"liquid-metal",
"stripe-resonance",
"liminal-spaces"
],
"alternate_playset": "parvagues",
"fx": {
"glitch": {
"base": 0.55
},
"rgbDelay": {
"base": 0.3
},
"jumpCut": {
"base": 1.0,
"isTrigger": true,
"syncBand": "kick"
},
"vibration": {
"base": 0.5,
"isTrigger": true,
"syncBand": "kick"
},
"flash": {
"base": 0.45,
"isTrigger": true,
"syncBand": "kick"
}
},
"automixSeconds": 8.0,
"triggerHeavy": true
}
},
{
"setlist_pos": 11,
"opal_title": "Desire",
"take89_title": "Desire",
"tidal_stem": "desire",
"genre_tag": "FINALE",
"measured_bpm": 127,
"take": "Take89",
"master_audio": "/home/pln/Work/Sound/hydra-live-hexa/public/audio/Recordings/Montreuil26_master/studio/Montreuil26_streaming_final.opus",
"track_master_start": 2925.0,
"track_duration": 246.0,
"measured": {
"tilt_mean": 0.542,
"kick_density_p70": 2.25,
"ssm_novelty_max": 8.47,
"n_active_mean": 2.46
},
"promo_cuts": {
"30s": {
"track_relative_start": 175.23,
"master_start": 3100.23,
"duration": 29.77,
"anchor": "peak"
},
"90s": {
"track_relative_start": 71.16,
"master_start": 2996.16,
"duration": 90.0,
"anchor": "drop"
}
},
"visual": {
"tilt_family": "bright",
"playsets": [
"iridescent",
"white-hole",
"fire-sparkles"
],
"alternate_playset": "parvagues",
"fx": {
"glitch": {
"base": 0.35
},
"rgbDelay": {
"base": 0.3
},
"liquix": {
"base": 0.55
},
"wetLens": {
"base": 0.6
},
"slowmo": {
"base": 0.45
},
"lumaPrint": {
"base": 0.7
}
},
"automixSeconds": 8.0,
"triggerHeavy": false
}
},
{
"setlist_pos": 12,
"opal_title": "La Revolution Sera Samplee",
"take89_title": "La Révolution Sera Samplée",
"tidal_stem": "the_revolution_will_be_sampled",
"genre_tag": "encore",
"measured_bpm": 114,
"take": "Take89",
"master_audio": "/home/pln/Work/Sound/hydra-live-hexa/public/audio/Recordings/Montreuil26_master/studio/Montreuil26_streaming_final.opus",
"track_master_start": 4176.14,
"track_duration": 424.27,
"measured": {
"tilt_mean": 0.157,
"kick_density_p70": 2.5,
"ssm_novelty_max": 14.241,
"n_active_mean": 3.83
},
"promo_cuts": {
"30s": {
"track_relative_start": 145.17,
"master_start": 4321.31,
"duration": 30.0,
"anchor": "peak"
},
"90s": {
"track_relative_start": 373.03,
"master_start": 4549.17,
"duration": 51.24,
"anchor": "drop"
}
},
"visual": {
"tilt_family": "dark",
"playsets": [
"obsidian-tide",
"obsidian-iris",
"behind-screen"
],
"alternate_playset": "parvagues",
"fx": {
"glitch": {
"base": 0.55
},
"rgbDelay": {
"base": 0.3
},
"jumpCut": {
"base": 1.0,
"isTrigger": true,
"syncBand": "kick"
},
"vibration": {
"base": 0.5,
"isTrigger": true,
"syncBand": "kick"
},
"flash": {
"base": 0.45,
"isTrigger": true,
"syncBand": "kick"
},
"glow": {
"base": 0.45
}
},
"automixSeconds": 4.0,
"triggerHeavy": true
}
}
],
"unmatched": [
{
"setlist_pos": 1,
"title": "Ceci n'est pas Une Bombe",
"tidal_stem": "bombe_dj",
"reason": "no Take89 recording — never played at Montreuil (or added since)"
},
{
"setlist_pos": 3,
"title": "Am i Doing it Right?",
"tidal_stem": "do_it_right",
"reason": "no Take89 recording — never played at Montreuil (or added since)"
},
{
"setlist_pos": 6,
"title": "Perfect <3",
"tidal_stem": "perfect",
"reason": "no Take89 recording — never played at Montreuil (or added since)"
},
{
"setlist_pos": 7,
"title": "Gimme Acid",
"tidal_stem": "gimme_acid",
"reason": "no Take89 recording — never played at Montreuil (or added since)"
},
{
"setlist_pos": 8,
"title": "Vague de CRIME",
"tidal_stem": "vague_de_crime",
"reason": "no Take89 recording — never played at Montreuil (or added since)"
},
{
"setlist_pos": 13,
"title": "Electric Hammer",
"tidal_stem": "electric_hammer",
"reason": "no Take89 recording — never played at Montreuil (or added since)"
}
]
}
\ No newline at end of file
#!/usr/bin/env python3
"""Clip-idea planner — OPAL 2026 setlist x Take89 recordings x Slopmotion playsets.
The question this answers: *which shareable clips can we actually make right now,
from audio we already have, for tracks that are actually in the OPAL set?*
Three sources are joined, none of them invented:
1. **The setlist** — `backlog.md` § "OPAL 2026" (canonical informal source), with the
measured tempi from `docs/2026-07-25-opal-bpm-arc.md` (measured `setcps`, NOT the
backlog's annotations, which were wrong on three tracks).
2. **The recordings** — Take89 = Montreuil Algorave V3, 2026-05-22. Per-track
boundaries are PLN-ear-validated (`armada/tide-table/boundaries_take89_validated.json`);
per-track peak-anchored promo cuts + measured features come from the Slopmotion
studio bundle's `highlights/` (rule-based drops detection on stem-activity + kick GT
+ Foote SSM novelty).
3. **The visuals** — the Slopmotion (hydra-live-hexa) playset catalog, i.e. the local
generated snapshot of the online Supabase `packs` table.
Pairing is grounded on MEASURED audio features, not vibes:
* `tilt_mean` spectral tilt -> bright vs dark playset families
* `kick_density_p70` -> how trigger-heavy the FX chain should be
* `ssm_novelty_max` -> how fast Automix should cut clips
Usage:
python3 visuals/slop/plan_clips.py # write clip_ideas.json + .md
python3 visuals/slop/plan_clips.py --print # just print the table
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
TIDAL = Path(__file__).resolve().parents[2]
HEXA = Path("/home/pln/Work/Sound/hydra-live-hexa")
STUDIO = HEXA / "public/audio/Recordings/Montreuil26_master/studio"
HIGHLIGHTS = STUDIO / "tracks_bandcamp/highlights"
OUT_DIR = Path(__file__).resolve().parent
# --- Source 1: the OPAL 2026 setlist ---------------------------------------
# genre tags verbatim from backlog.md; bpm = MEASURED setcps (docs/2026-07-25-opal-bpm-arc.md)
OPAL_SETLIST = [
# (position, title, genre_tag, measured_bpm, tidal_stem)
(1, "Ceci n'est pas Une Bombe", "lightT", 124, "bombe_dj"),
(2, "WAP", "lightT", 133, "wap"),
(3, "Am i Doing it Right?", "lightT", 89, "do_it_right"),
(4, "Take five Drops", "jazzT", 124, "take_5_drops"),
(5, "Piment Bresilien", "hardT", 124, "piment_bresilien"),
(6, "Perfect <3", "T", 128, "perfect"),
(7, "Gimme Acid", "dnb", 80, "gimme_acid"),
(8, "Vague de CRIME", "lightT", 120, "vague_de_crime"),
(9, "Mafia", "Hdnb", 160, "mafia_sans_serif"),
(10, "Sunshine", "Hdnb", 144, "you_my_sunshine"),
(11, "Desire", "FINALE", 127, "desire"),
(12, "La Revolution Sera Samplee", "encore", 114, "the_revolution_will_be_sampled"),
(13, "Electric Hammer", "encore", 130, "electric_hammer"),
]
# --- The join: OPAL title -> Take89 recorded-track title --------------------
# Take89 track titles as they appear in the studio bundle's highlights/.
# Several are aka-renames; each mapping below is a same-track identity, not a guess:
# "W.I.P. W.A.P." is the work-in-progress title of WAP
# "Aria Sans Serif" is the highlights-side name for Mafia Sans Serif
# (boundaries_take89_validated.json calls the same cut "Mafia Sans Serif")
# "You My Sunshine" is Sunshine
OPAL_TO_TAKE89 = {
"WAP": "W.I.P. W.A.P.",
"Take five Drops": "Take 5 Drops",
"Piment Bresilien": "Piment brésilien",
"Mafia": "Aria Sans Serif",
"Sunshine": "You My Sunshine",
"Desire": "Desire",
"La Revolution Sera Samplee": "La Révolution Sera Samplée",
}
# --- Source 3: playset families, keyed by what the measurement says ---------
# Families are a reading of the pack names + their source imagery, and are the one
# genuinely aesthetic (i.e. arguable) layer here. Everything routing INTO them is measured.
DARK_PACKS = ["obsidian-tide", "obsidian-iris", "behind-screen", "concrete-jungle", "point-cloud"]
BRIGHT_PACKS = ["iridescent", "white-hole", "fire-sparkles", "cloud-composition", "dune-calor"]
MID_PACKS = ["liquid-metal", "stripe-resonance", "liminal-spaces", "rainy-city-night", "hex-skull"]
OWN_PACK = "parvagues" # PLN's own imagery — always offered as the alternate take
def tilt_family(tilt: float) -> tuple[str, list[str]]:
"""Spectral tilt -> playset family. Thresholds are the terciles of the observed
Take89 range (0.13 .. 0.54), so they split THIS set rather than an abstract scale."""
if tilt < 0.25:
return "dark", DARK_PACKS
if tilt > 0.45:
return "bright", BRIGHT_PACKS
return "mid", MID_PACKS
def fx_chain(kick_density: float, novelty: float, tilt: float) -> dict:
"""FX chain proposal. Every knob traces to a measurement.
kick_density_p70 is beats-above-threshold per second at the 70th percentile, i.e.
how relentless the kick is; observed Take89 range 1.75 .. 3.75.
"""
trigger_heavy = kick_density >= 2.5
chain: dict[str, dict] = {}
# Backbone: always a little grain + edge so the loop doesn't read as stock footage.
chain["glitch"] = {"base": 0.35 if not trigger_heavy else 0.55}
chain["rgbDelay"] = {"base": 0.30}
if trigger_heavy:
# Dense kick -> cut ON the kick; these are the percussive-feeling FX.
chain["jumpCut"] = {"base": 1.0, "isTrigger": True, "syncBand": "kick"}
chain["vibration"] = {"base": 0.5, "isTrigger": True, "syncBand": "kick"}
chain["flash"] = {"base": 0.45, "isTrigger": True, "syncBand": "kick"}
else:
# Sparser kick -> let the image breathe; motion comes from warps, not cuts.
chain["liquix"] = {"base": 0.55}
chain["wetLens"] = {"base": 0.60}
chain["slowmo"] = {"base": 0.45}
# Bright material can take a hard monochrome/print treatment without going muddy.
if tilt > 0.45:
chain["lumaPrint"] = {"base": 0.70}
elif tilt < 0.25:
chain["glow"] = {"base": 0.45}
# High structural novelty -> the track already changes a lot; let Automix follow it.
automix_s = 4.0 if novelty >= 12.0 else 8.0
return {"fx": chain, "automixSeconds": automix_s, "triggerHeavy": trigger_heavy}
def load_track_starts() -> dict[str, float]:
"""Cumulative master-timeline start second for each Take89 track title.
Uses the ear-validated boundaries; track N starts at boundary N-1.
"""
gt = json.loads((STUDIO / "boundaries_ground_truth.json").read_text())
cuts = [b["boundary_final_time"] for b in gt]
starts = [0.0] + cuts
titles = []
for f in sorted(HIGHLIGHTS.glob("*.highlights.json")):
titles.append((json.loads(f.read_text())["track"], json.loads(f.read_text())["title"]))
titles.sort()
return {title: starts[i] for i, (_, title) in enumerate(titles) if i < len(starts)}
def load_highlights() -> dict[str, dict]:
out = {}
for f in sorted(HIGHLIGHTS.glob("*.highlights.json")):
d = json.loads(f.read_text())
out[d["title"]] = d
return out
def build() -> dict:
highlights = load_highlights()
starts = load_track_starts()
master = STUDIO / "Montreuil26_streaming_final.opus"
ideas, unmatched = [], []
for pos, title, genre, bpm, stem in OPAL_SETLIST:
take_title = OPAL_TO_TAKE89.get(title)
if not take_title or take_title not in highlights:
unmatched.append(
{"setlist_pos": pos, "title": title, "tidal_stem": stem,
"reason": "no Take89 recording — never played at Montreuil (or added since)"}
)
continue
h = highlights[take_title]
ft = h["features"]
tilt = ft["tilt_mean"]
family, packs = tilt_family(tilt)
chain = fx_chain(ft["kick_density_p70"], ft["ssm_novelty_max"], tilt)
t0 = starts[take_title]
cuts = {}
for label, c in h["promo_cuts"].items():
cuts[label] = {
"track_relative_start": round(c["start"], 2),
"master_start": round(t0 + c["start"], 2),
"duration": round(c["end"] - c["start"], 2),
"anchor": c["anchor"],
}
ideas.append({
"setlist_pos": pos,
"opal_title": title,
"take89_title": take_title,
"tidal_stem": stem,
"genre_tag": genre,
"measured_bpm": bpm,
"take": "Take89",
"master_audio": str(master),
"track_master_start": round(t0, 2),
"track_duration": h["duration"],
"measured": {
"tilt_mean": tilt,
"kick_density_p70": ft["kick_density_p70"],
"ssm_novelty_max": ft["ssm_novelty_max"],
"n_active_mean": ft["n_active_mean"],
},
"promo_cuts": cuts,
"visual": {
"tilt_family": family,
"playsets": packs[:3],
"alternate_playset": OWN_PACK,
**chain,
},
})
return {
"schema": "slop clip ideas — OPAL 2026 setlist x Take89 x Slopmotion playsets",
"generated_by": "visuals/slop/plan_clips.py",
"provenance": {
"setlist": {"src": "file:backlog.md § OPAL 2026", "type": "file"},
"tempi": {"src": "file:docs/2026-07-25-opal-bpm-arc.md (measured setcps)", "type": "derived"},
"boundaries": {"src": "file:armada/tide-table/boundaries_take89_validated.json",
"type": "user", "note": "PLN ear-validated 2026-06-05"},
"highlights": {"src": f"file:{HIGHLIGHTS}", "type": "derived",
"note": "rule-based drops detection, stem-activity + kick GT + Foote SSM"},
"playsets": {"src": "file:hydra-live-hexa/src/data/playsetPacks.json",
"type": "derived", "note": "local generated snapshot of Supabase packs"},
},
"gig": {
"name": "OPAL 2026",
"note": "gig metadata is NOT canonical here — see Web/www next/content/lives/",
},
"n_ideas": len(ideas),
"n_unmatched": len(unmatched),
"ideas": ideas,
"unmatched": unmatched,
}
def to_markdown(doc: dict) -> str:
L = ["# Slop clip ideas — OPAL 2026 x Take89", ""]
L.append(f"**{doc['n_ideas']} OPAL tracks have a real recording we can cut from today** "
f"({doc['n_unmatched']} do not). Source take: Take89 = Montreuil Algorave V3, 2026-05-22.")
L.append("")
L.append("Every clip below is a *peak-anchored* window found by the drops detector, "
"offset onto the master timeline using PLN's ear-validated boundaries. "
"Playset + FX pairing is derived from measured audio features (see each row).")
L.append("")
L.append("| # | Track | BPM | tilt | kick | nov | family | playsets | 30s cut (master) |")
L.append("|---|-------|-----|------|------|-----|--------|----------|------------------|")
for i in doc["ideas"]:
m, v = i["measured"], i["visual"]
c = i["promo_cuts"]["30s"]
mm, ss = divmod(int(c["master_start"]), 60)
L.append(
f"| {i['setlist_pos']} | {i['opal_title']} | {i['measured_bpm']} | "
f"{m['tilt_mean']:.2f} | {m['kick_density_p70']:.2f} | {m['ssm_novelty_max']:.1f} | "
f"{v['tilt_family']} | {', '.join(v['playsets'])} | {mm}:{ss:02d} |"
)
L.append("")
L.append("## Not cuttable yet")
L.append("")
for u in doc["unmatched"]:
L.append(f"- **{u['title']}** (`{u['tidal_stem']}`) — {u['reason']}")
L.append("")
L.append("These are the newer / reworked tracks. To get promo clips for them we need a "
"fresh recording — which the always-on `tools/gig_record.sh` will produce at the "
"next rehearsal or at OPAL itself.")
L.append("")
L.append("## Rendering one")
L.append("")
L.append("```bash")
L.append("cd /home/pln/Work/Sound/hydra-live-hexa && nvm use 22")
L.append("npm run dev & # the renderer drives the real app")
L.append("node scripts/render_slop_clip.mjs \\")
L.append(" --ideas ../Tidal/visuals/slop/clip_ideas.json --pick 11 --cut 30s \\")
L.append(" --out ../Tidal/visuals/slop/out/")
L.append("```")
return "\n".join(L) + "\n"
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--print", dest="show", action="store_true")
args = ap.parse_args()
doc = build()
if args.show:
print(to_markdown(doc))
return
(OUT_DIR / "clip_ideas.json").write_text(json.dumps(doc, indent=2, ensure_ascii=False))
(OUT_DIR / "CLIP_IDEAS.md").write_text(to_markdown(doc))
print(f"wrote clip_ideas.json + CLIP_IDEAS.md — {doc['n_ideas']} ideas, "
f"{doc['n_unmatched']} unmatched")
if __name__ == "__main__":
main()
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