From e137a5fc119480916ce3a178679bf8ddfb3aada2 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 13 Aug 2026 16:46:46 -0400 Subject: [PATCH 1/6] pop/maytrax: femrag++ rhythmic rework + SHAKEOUT video pipeline The track had three harmonic collisions before the first drop: the snare's tonal body swept 196->152 Hz (a G in an A-major tune), the oom chord stabs landed on the same beats the backbeat snare entered on, and `ticks` played fixed pitches in straight 16ths against the swung rag. The snare body moves to 220->165, the stabs move to the offbeats and thin to two notes, and ticks becomes a regular 3-over-2 polyrhythm voiced from the current chord. Restructured 92 -> 80 bars so the whole track lives in one rhythm world: the intro is now the drop's two-step groove at low power rather than swung four-on-the-floor, so the first drop (0:27, was 1:07) intensifies instead of switching gears. New ragga-donk section at 1:33 - a falling-octave donk on every swung offbeat, dembow-leaning swung kit, and a deep quarter-note wub an octave under the bassline. The renderer now also emits .events.json and .struct.json, so the video pipeline reads the arrangement from the score rather than a hand-maintained copy. Adds the SHAKEOUT music video pipeline: gen-sections-* renders twelve needle-felt panels of the recurring AC bunny (anchor-ref character consistency + gpt-5.5 vision QA on ears/glasses), gen-motion-* drives Seedance over them against the track's own section map. Co-Authored-By: Claude Opus 5 (1M context) --- .../gen-motion-femrag-plusplus-shakeout.mjs | 146 +++++ .../gen-sections-femrag-plusplus-shakeout.mjs | 329 +++++++++++ pop/maytrax/bin/render-femrag-plusplus.mjs | 517 ++++++++++++++++++ 3 files changed, 992 insertions(+) create mode 100644 pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs create mode 100644 pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs create mode 100644 pop/maytrax/bin/render-femrag-plusplus.mjs diff --git a/pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs b/pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs new file mode 100644 index 0000000000..de0c5041a0 --- /dev/null +++ b/pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs @@ -0,0 +1,146 @@ +#!/usr/bin/env node +// maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs — the SHAKEOUT motion +// pass: Seedance shots from the twelve femrag++ panels, cut to the track's +// own section map. Thin lane driver over pop/lib/motion-pipeline.mjs. +// +// Panels come from gen-sections-femrag-plusplus-shakeout.mjs; sections from +// out/femrag-plusplus.struct.json, which is the actual arrangement of +// bin/render-femrag-plusplus.mjs (runway → groove → buildup → two drops → +// breakdown → the 1:33 ragga-donk → outro). +// +// Shot grammar: ALL CUTS except two scripted morphs — buildup1→drop1a and +// buildup2→drop2a. Both are same-camera escalations (a coiled bunny +// releasing into a leap), which is exactly the case morphs are for; every +// other transition changes room state or camera, where a morph would invent +// doubled figures (see pop-motion-pipeline memory). +// +// Usage: +// node pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs --dry-run +// node pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs --only drop1a +// node pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs +// node pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs --assemble + +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { runMotionCli, parseFlags } from "../../lib/motion-pipeline.mjs"; +import { NEEDLE_FELT_WOOL_MOTION, FRAMING_YT_LANDSCAPE_MOTION } from "../../lib/mediums.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const LANE = resolve(HERE, ".."); +const OUT = `${LANE}/out`; +const SLUG = "femrag-plusplus"; + +const MEDIUM_MOTION = [ + `A needle-felt music video at 144 BPM — every motion is DANCE, driven and rhythmic, landing on beats rather than drifting. The wool bunny never goes limp between moves; he is always either loading or releasing.`, + FRAMING_YT_LANDSCAPE_MOTION, + NEEDLE_FELT_WOOL_MOTION, +].join("\n\n"); + +// The panel carries the LOOK; the prompt carries only MOTION. +// EAR CONTINUITY is this film's through-line: heavy → swinging → whipping +// horizontal at the drops → loose and behind-the-beat in the dancehall → +// hanging spent at the end. FUZZ CONTINUITY: his wool gets progressively +// more disheveled and the room accumulates loose wisps; it never resets. +const SHOTS = { + runway: { + motion: +`The felt bunny starts to move on the rug — a small weight-shift from one hind paw to the other, then a second, finding the pulse. His two long ears hang heavy and swing only slightly, a few degrees, lagging behind each shift. One front paw taps twice against his own chest. The camera holds nearly still and drifts in by a hair. Dim, warm, contained — a dance that has not committed yet.`, + }, + groove: { + motion: +`The bunny settles into a two-step shuffle: one hind paw plants, the other slides across the wool rug and back, hips turning slightly with each step, both front paws bouncing loosely at rib height. His two long ears swing together in a matched arc, lifting further off vertical with each bar. The rug rumples under the planted paw and stays rumpled. The camera tracks slowly sideways with him, staying wide. Confident and easy — the groove is found.`, + }, + buildup1: { + // Same-camera escalation into the drop — a legitimate morph. + morphTo: "drop1a", + physical: "extreme", + contacts: ["both hind paws → rug", "front paws → own chest", "ears → held back under tension"], + invariants: ["exactly one bunny in frame at all times", "both long ears visible throughout"], + beats: [ + { at: 0, action: "close on the bunny, shoulders drawing in, chin tucking, front paws pulling to his chest" }, + { at: .35, action: "his two long ears sweep back behind his head and pull taut, straining — maximum tension" }, + { at: .7, action: "he compresses further, knees bending, weight sinking onto both hind paws — the coil" }, + { at: 1, action: "the release begins: hind paws drive against the rug and he starts to rise, ears whipping forward off their tension, camera pulling back and down toward the low angle" }, + ], + motion: +`One continuous escalation on the SAME bunny — never two figures. He winds up tight in close-up, ears swept back and straining, then explodes: hind paws drive into the rug, he launches, and the camera falls back and down to the low rug-level angle as he goes airborne — landing exactly on the final frame. The room tears open around him as he rises.`, + }, + drop1a: { + physical: "extreme", + contacts: ["hind paws → airborne, no contact", "rug → visibly kicked and rucked where he launched"], + invariants: ["both long ears present and extended", "one bunny only"], + beats: [ + { at: 0, action: "the bunny is at the top of his leap, both hind paws off the rug, ears flung horizontal to each side" }, + { at: .3, action: "he twists through the air, front paws sweeping wide, ears whipping across the turn" }, + { at: .6, action: "hind paws reach down and strike the rug, wool compressing under the impact, ears snapping downward with the landing" }, + { at: 1, action: "he rebounds straight back up into the next shake, ears flying out again — the dance is continuous" }, + ], + motion: +`THE DROP, shot low from the rug looking up. The bunny lands hard, rebounds, and keeps going — a full-body shakeout on the beat: hips whipping, front paws thrown wide, both long ears flying out horizontally and snapping with every direction change. Loose wool wisps lift off him with each impact and hang in the bulb light. The yarn cables on the floor jump slightly when he lands. The camera stays low and holds, letting him dominate the frame.`, + }, + drop1b: { + motion: +`Close on the bunny's head and shoulders, deep in the dance — head thrown back, bead eyes squeezed happy inside their red lenses, his two long ears mid-whip, carving wide S-curves past his head and doubling back with each beat. Loose wool wisps peel off the ears at the ends of their arcs and drift through the light. The camera handholds gently with him, staying tight — never pulling out to a wide.`, + }, + breakdown: { + motion: +`The bunny drops into slow heavy half-time. He stands at the big felted speaker cabinet with one front paw laid flat on its oatmeal wool cone; the cone visibly pushes outward against his paw and relaxes, a slow physical pulse, and his whole body rocks with it — knees bending deep, weight rolling side to side. His two long ears hang low and swing in a lazy pendulum, the two ears out of phase with each other. The camera holds wide and almost still. Enormous and patient.`, + }, + buildup2: { + morphTo: "drop2a", + physical: "extreme", + contacts: ["front paws → rug", "hind paws → rug", "ears → pinned flat along his spine"], + invariants: ["exactly one bunny throughout", "both ears visible"], + beats: [ + { at: 0, action: "tight close-up: the bunny crouches all the way down, chin near his knees, both front paws pressed flat to the rug" }, + { at: .4, action: "his ears pin flat back along his spine and lock there; his whole wool body gathers and stills" }, + { at: .75, action: "hind paws load and the rug compresses visibly beneath them — the deepest compression of the film" }, + { at: 1, action: "he fires upward off both hind paws, ears tearing up off his spine, camera dropping back to the low wide angle as he clears the rug" }, + ], + motion: +`The second wind-up, steeper and lower than the first, on the SAME single bunny. He crushes down into the rug, ears pinned, everything gathered — then launches, and the camera falls back and down to the low wide angle as he rises out of the crouch, landing exactly on the final frame at the top of the leap.`, + }, + drop2a: { + physical: "extreme", + contacts: ["hind paws → airborne at the apex", "rug → folded and kicked beneath him"], + invariants: ["both long ears present", "one bunny only", "the speaker stack stays a single leaning tower"], + beats: [ + { at: 0, action: "apex of the leap — body arched back, all four paws flung out in a star, both ears whipping up behind his head" }, + { at: .25, action: "he falls, paws gathering under him, ears streaming upward against the drop" }, + { at: .5, action: "hind paws slam the rug and it folds; loose wool bursts off him and the speaker stack shudders" }, + { at: 1, action: "he rebounds into a wilder, faster shakeout than before — bigger arcs, harder direction changes, ears cracking like whips" }, + ], + motion: +`THE SECOND DROP, low and wide — bigger than the first in every way. He lands, folds the rug, and tears into the hardest dancing of the film: faster turns, wider paw throws, both long ears whipping and cracking through their arcs. A cloud of loose wool wisps hangs and swirls where he lands. The camera stays low and pushes in slightly.`, + }, + drop2b: { + motion: +`Tight on the bunny's hind paws and the rug, filling the frame at maximum fiber resolution. His felted paws stamp the beat: one paw drives down and compresses the rug's matted pile, the other lifts away trailing wisps, then they trade — over and over, locked to the rhythm. A yarn cable on the floor jumps slightly with each stamp. Above and out of focus, the blurred oatmeal wool of his body keeps moving. The camera stays locked and close.`, + }, + "ragga-a": { + motion: +`The dancehall. The bunny's whole rhythm changes — looser, heavier, deliberately behind the beat. He skanks: leaning forward with his weight low, one shoulder dropped, both front paws swinging across his body in long lazy diagonals, one hind paw kicking back and returning. His two long ears swing in slow wide arcs that lag noticeably behind his body — cool, not frantic. Above him the huge felted speaker cones pulse outward in slow deep pushes, the wool physically bulging and relaxing. The camera drifts slowly in a wide arc around him. Warm amber and deep green.`, + }, + "ragga-b": { + motion: +`Close and low — the bunny leaning back against the mouth of the biggest felted speaker cone, half-turned to it, still skanking loose and easy. One long ear swings forward across his own face and away again with each bar; the other trails behind. Behind him the enormous oatmeal wool cone visibly pushes outward at its center on every bass note and settles back, and loose fibers around its rim lift away from the surface with each push. His bead eyes stay happy inside the red lenses. The camera holds close, swaying a little with him.`, + }, + outro: { + motion: +`The bunny finishes. He slows over the first moments — the swinging arcs shortening, his ears sinking lower with each one — then stops entirely at the center of the wrecked rug, standing still, one front paw settling onto his own chest, his sides rising and falling with breath. His two long ears hang all the way down, completely spent. His wool is visibly fluffed and disheveled everywhere, and the last loose wisps drift down and settle onto the rug around him. Behind him the speaker cones go still. The camera drifts slowly back to a wide as the warm little lamp is left the only light. Held, warm, finished.`, + }, +}; + +const cfg = { + slug: SLUG, + laneDir: LANE, + structPath: `${OUT}/${SLUG}.struct.json`, + panelFor: (name, i) => `${OUT}/${SLUG}-yt-sec-${i}-${name}.png`, + shots: SHOTS, + mediumMotion: MEDIUM_MOTION, + ratio: "16:9", + audio: `${OUT}/${SLUG}.mp3`, + finalOut: `${OUT}/${SLUG}-shakeout-yt.mp4`, +}; + +await runMotionCli(cfg, parseFlags()); diff --git a/pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs b/pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs new file mode 100644 index 0000000000..40b5292680 --- /dev/null +++ b/pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs @@ -0,0 +1,329 @@ +#!/usr/bin/env node +// maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs — panels for +// SHAKEOUT, the femrag++ music video. The recurring AESTHETIC COMPUTER +// felt BUNNY throws himself a one-bunny dance: a wool sound-system party +// that starts as a lone shuffle on a rag-time parlor rug, tears open into +// drum and bass, and lands in a Jamaican dancehall of stacked felt +// speakers when the donks drop at 1:33. +// +// Character law is inherited VERBATIM from pop/momboba/bunny/PREAMBLE.txt +// (two long ears always, red glasses ride HIGH with an eye inside each +// lens, rims never empty). His EARS carry the rhythm here the way they +// carried sleepiness in the momboba cut — this lane's whole arc is in +// how hard the ears are flying. +// +// Medium: needle-felt wool (momboba's lane medium — the bunny is felt, +// and felt is FUZZY, which is the brief). 16:9 landscape for the video. +// +// Anchor pattern (from gen-sections-fluttabap360-bunny-reel.mjs): the +// first bunny beat `runway` generates with NO refs; every later beat +// passes the finished runway PNG as the character-consistency ref so the +// same bunny dances through all twelve sections. Face-visible beats are +// gated by a gpt-5.5 vision QA (two ears, eyes inside the red rims), +// re-rolled up to 3× on fail, rejects archived to out/rejected/. +// +// Output: pop/maytrax/out/femrag-plusplus-yt-sec--.png +// (the name the motion pipeline's pop-panel convention expects) +// +// Usage: +// node pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs --list +// node pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs --only runway +// node pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs # all, cached +// node pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs --force + +import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, renameSync } from "node:fs"; +import { resolve, dirname, basename, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { NEEDLE_FELT_WOOL, FRAMING_YT_LANDSCAPE } from "../../lib/mediums.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const LANE = resolve(HERE, ".."); +const REPO = resolve(LANE, "..", ".."); +const SLUG = "femrag-plusplus"; +const OUT = `${LANE}/out`; +const REJECTED = `${OUT}/rejected`; +const SIZE = "1536x1024"; // 16:9-ish landscape +const QA_MODEL = "gpt-5.5"; +const QA_ATTEMPTS = 3; + +const flags = {}; +for (let i = 2; i < process.argv.length; i++) { + const a = process.argv[i]; + if (!a.startsWith("--")) continue; + const next = process.argv[i + 1]; + if (next === undefined || next.startsWith("--")) flags[a.slice(2)] = true; + else { flags[a.slice(2)] = next; i++; } +} +const FORCE = flags.force === true; +const ONLY = flags.only ? String(flags.only).split(",") : null; +mkdirSync(OUT, { recursive: true }); + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +function loadOpenAIKey() { + if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY; + const vault = `${REPO}/aesthetic-computer-vault/.devcontainer/envs/devcontainer.env`; + if (existsSync(vault)) { + for (const line of readFileSync(vault, "utf8").split("\n")) { + if (line.startsWith("OPENAI_API_KEY=")) { + return line.slice("OPENAI_API_KEY=".length).trim().replace(/^['"]|['"]$/g, ""); + } + } + } + throw new Error("OPENAI_API_KEY not set and not found in vault devcontainer.env"); +} + +// ── the character — inherited from pop/momboba/bunny/PREAMBLE.txt ──────── +const BUNNY = +`THE BUNNY — a single needle-felted wool BUNNY, the recurring AESTHETIC COMPUTER character: a wonderfully woolly little rabbit with an off-white body, soft-grey ear tips and paws, TWO LONG felt ears, a little pink felt nose, tiny dark glossy bead eyes, and a round puff of a tail. he wears tiny round soft RED felt glasses — his signature. he stands and dances upright on his hind legs. the SAME bunny in every scene: identical wool colors, same two long ears, same red glasses. +HIS EARS ARE THE RHYTHM — in this story the ears carry the beat the way a dancer's hair does: hanging soft and heavy when the groove is quiet, swinging in wide arcs as it builds, flying and whipping out sideways at full extension when the drums drop, and finally settling. BOTH of his two long ears are ALWAYS present — even when one flies wide, the second stays visible. NEVER draw him with only one ear. +GLASSES GEOMETRY (CRITICAL): the bunny has exactly TWO eyes. a bunny's eyes sit HIGH and WIDE on the head — so the red glasses ride HIGH on his face, up over the eyes, NOT down low on the muzzle or snout. each eye — open bead eye or closed happy lash-line — is centered INSIDE its own round red lens, one eye per lens, the lens ringing the eye like real worn glasses. the red rims are NEVER empty. NEVER draw an eye ABOVE the rims with the empty lenses sitting lower on the snout (one real eye plus two empty red circles reads as extra eyes — the single worst failure).`; + +// The world: one continuous felt room that transforms with the music. +const WORLD = +`THE ROOM — one continuous hand-felted room, roughly a 40 cm diorama, that TRANSFORMS with the music across the story: it begins as a hushed little wool parlor (a worn felt rag-rug, a stitched upright piano against the back wall, a dim wool lamp) and ends as a full JAMAICAN SOUND-SYSTEM DANCE — a leaning stack of enormous hand-felted speaker cabinets, wool cones bulging, tangled yarn cables across the floor. the SAME room throughout: the same rug, the same back wall, the same little lamp, progressively taken over by the speaker stack. everything is wool: the speakers, the cables, the light itself feels felted.`; + +const LIGHT = +`LIGHT — diegetic and physically motivated, always. act one is warm dim lamp-light on wool. after the first drop, the room gains a real hand-felted lighting rig: a few small warm bulbs and one cool blue-green wool-diffused lamp, casting soft double shadows across the fibers. the dancehall section is lit warmer and lower — amber and deep green, the way a bass-heavy room feels. NO neon, NO glow filters, NO laser beams, NO light shafts drawn as objects.`; + +const PALETTE = +`PALETTE — off-white and soft-grey wool bunny; round red glasses; oatmeal and dusty-rose rag-rug; warm brown felted wood; charcoal-grey speaker cabinets with oatmeal cones; brass-yellow bulb light; one cool teal accent lamp; amber and deep-green dancehall wash. warm, soft, matted, unsaturated — the colors of dyed wool, never of plastic.`; + +const AVOID = +`AVOID — any 3d-render, clay, plastic, or smooth plush-toy look (this is real matted needle-felted wool); ANY human person anywhere (the bunny is the only character); a second bunny, a mirror, or any doubled figure; only one ear on the bunny; empty red glasses rims; any readable text, wordmark, signage, band name, or logo of any kind; any screen, phone, tablet, laptop, or DJ controller with a display; neon, glow filters, lens flares, laser beams, or light shafts drawn as solid objects; motion-blur streaks or drawn speed-lines; the character looking at or acknowledging the camera; any real-world brand.`; + +const FRAME_NOTE = +`${FRAMING_YT_LANDSCAPE} +HONOR THE SHOT DIRECTIVE in this beat exactly: a WIDE shot lets the whole room breathe with the bunny comfortably within it; a CLOSE-UP crops in tight and FILLS the frame with the detail (do NOT pull back to a wide); a LOW-ANGLE puts the camera down on the rug looking up.`; + +// ── the twelve beats — one per struct section ──────────────────────────── +// Names + order MUST match pop/maytrax/out/femrag-plusplus.struct.json. +const BEATS = [ + { + name: "runway", faceVisible: true, + scene: +`WIDE, low and quiet. The felt bunny stands alone in the middle of the worn oatmeal rag-rug in the dim wool parlor, seen three-quarters from the front. He has just started to move — weight settled onto one hind paw, the other heel lifted a few millimeters off the rug, one front paw curled loosely at his chest. His two long ears hang heavy and straight down, only just beginning to sway. Behind him the stitched felt upright piano waits against the wall and the little wool lamp glows warm. Nothing else in the room has moved yet. The moment before a dance.`, + }, + { + name: "groove", faceVisible: true, + scene: +`WIDE. The bunny has found the groove — a small contained two-step shuffle on the rug, hips turned slightly, one hind paw planted and the other sliding a few centimeters across the wool, both front paws up at rib height in a loose easy bounce. Both long ears now swing in a slow matched arc out to one side, lifted maybe thirty degrees off vertical. Bead eyes half-lidded, content. The rug has begun to rumple under his planted paw. Same parlor, same lamp — but a first small felted speaker cabinet has appeared beside the piano, its oatmeal wool cone facing him.`, + }, + { + name: "buildup1", faceVisible: true, + scene: +`CLOSE-UP on the bunny from the chest up, filling the frame — individual wool fibers and needle pokes clearly resolved. He is winding up: chin tucked, shoulders drawn in tight, both front paws pulled to his chest, coiled. His two long ears are swept back behind his head and held there under tension, straining, at their tautest of the whole story — the frame just before release. Bead eyes wide inside the red lenses. Behind him the room is dark and out of focus except a single hot rim of bulb light along his fuzz. Every fiber on his silhouette lifts as a soft halo.`, + }, + { + name: "drop1a", faceVisible: true, + scene: +`WIDE, LOW-ANGLE from down on the rug looking up. THE DROP — the bunny is airborne in a full shakeout: both hind paws off the rug by several centimeters, body twisted mid-turn, front paws flung wide and open. His two long ears fly straight out sideways at full extension, horizontal, one to each side. The rag-rug is visibly rucked and kicked up behind him where he launched. The room has torn open into the sound system: three hand-felted speaker cabinets now stacked crookedly against the back wall, wool cones bulging outward, yarn cables snaking across the floor around his feet. Warm bulbs and one teal wool lamp throw soft double shadows.`, + }, + { + name: "drop1b", faceVisible: true, + scene: +`CLOSE-UP, filling the frame — the bunny's head and shoulders in full motion, single fibers and matted clumps resolving. He is deep in it: head thrown back, mouth-line open in delight, bead eyes squeezed into happy closed lash-lines centered inside their red lenses. Both long ears are mid-whip, caught in a wide S-curve as they swing past his head, wool wisps lifting off them. Sweat is not a thing here — but loose fiber is: a scatter of tiny wool wisps floats free in the bulb light around him. Behind, out of focus, the charcoal wall of speaker cabinets.`, + }, + { + name: "breakdown", faceVisible: true, + scene: +`WIDE and still. The breakdown — the bunny has dropped into a slow heavy half-time sway, standing near the largest felted speaker cabinet with one front paw laid flat against its oatmeal wool cone, feeling the bass through his paw. Body low, knees bent, rocking. His two long ears hang low and heavy again, swinging in a slow lazy pendulum, out of phase with each other. The lighting has cooled: the teal wool lamp dominates, the warm bulbs are down to embers. The rug is thoroughly rumpled. Quiet, enormous, patient.`, + }, + { + name: "buildup2", faceVisible: true, + scene: +`CLOSE-UP, tight on the bunny's face and upper body, filling the frame. The second wind-up, steeper than the first: he is crouched right down with his chin nearly at his own knees, both front paws pressed to the rug, every muscle of the little wool body gathered. His two long ears are pinned flat back along his spine, held under maximum tension. One bead eye visible inside its red lens, fixed and wide. A single hot bulb rim-lights his whole fuzzy outline against near-blackness. The most compressed frame in the film.`, + }, + { + name: "drop2a", faceVisible: true, + scene: +`WIDE, LOW-ANGLE. THE SECOND DROP — bigger than the first. The bunny is at the very top of a leap, higher off the rug than before, body arched back, all four paws flung out in a star, tail puff visible. His two long ears whip up and back in matching arcs above his head. The rag-rug is kicked into a fold beneath him and a small cloud of loose wool wisps hangs in the air where he launched. The speaker stack has grown to a leaning wall of five cabinets, cones bulging, cables everywhere. Warm bulbs and teal lamp at full strength, hard double shadows across the fibers.`, + }, + { + name: "drop2b", faceVisible: true, + scene: +`CLOSE-UP on the bunny's hind paws and the rug, filling the frame — this is the only beat where his face is not the subject. Wool fibers at maximum resolution: the matted pile of the rag-rug, individual dyed strands, the soft-grey felted pads of his hind paws mid-stamp, one paw planted deep enough to compress the rug's pile, the other lifting away with wisps trailing. A yarn cable curls past. Above and out of focus, the blurred oatmeal wool of his body still in motion.`, + }, + { + name: "ragga-a", faceVisible: true, + scene: +`WIDE. THE DANCEHALL — the room has fully become a Jamaican sound-system dance. The stack is now enormous: seven or eight hand-felted speaker cabinets leaning in a crooked tower to the ceiling, huge oatmeal wool cones, thick yarn cables looped across the floor. The lighting has gone warm and low — deep amber and forest green. The bunny dances in the classic skanking posture: leaning forward, weight low, one shoulder dropped, both front paws swinging loosely across his body, one hind paw kicked back. His two long ears swing in a lazy, deliberately behind-the-beat arc — loose and cool, not frantic. He is having the time of his life.`, + }, + { + name: "ragga-b", faceVisible: true, + scene: +`CLOSE-UP, low, filling the frame — the bunny and the mouth of the biggest felted speaker cone behind him. He has his back half-turned to the cabinet, leaning into it, one long ear flung forward across his own face by the swing and the other trailing behind, bead eyes happy inside their red lenses. The enormous oatmeal wool cone behind him is visibly pushed outward at its center — the wool physically bulging with a bass note — and loose fibers around its rim lift away from the surface. Deep amber light, forest-green shadows in the fuzz.`, + }, + { + name: "outro", faceVisible: true, + scene: +`WIDE and warm. The end. The bunny has stopped dancing and stands still at the center of the thoroughly wrecked rug, breathing, one front paw resting on his own chest. His two long ears hang all the way down, completely spent, wool visibly fluffed and disheveled all over him — he has genuinely shaken his fuzz out. Loose wisps have settled across the rug like snow. The speaker stack looms quiet and dark behind him, cones at rest. Only the little original wool lamp is still lit, back to its warm act-one glow. The same parlor, after.`, + }, +]; + +if (flags.list) { + BEATS.forEach((b, i) => console.log(`${i}\t${b.name}`)); + process.exit(0); +} + +const panelPath = (i, name) => `${OUT}/${SLUG}-yt-sec-${i}-${name}.png`; +const ANCHOR = panelPath(0, "runway"); + +function build(beat) { + return [ + `SHAKEOUT — one beat from a needle-felted music video.`, + NEEDLE_FELT_WOOL, + FRAME_NOTE, + BUNNY, + WORLD, + LIGHT, + PALETTE, + `THIS BEAT — ${beat.scene}`, + AVOID, + ].join("\n\n"); +} + +const apiKey = loadOpenAIKey(); + +async function genOnce(promptText, refs, useEdit, label) { + const MAX_TRIES = 4; + for (let attempt = 1; attempt <= MAX_TRIES; attempt++) { + const t0 = Date.now(); + try { + let res; + if (useEdit) { + const fd = new FormData(); + fd.append("model", "gpt-image-2"); + fd.append("prompt", promptText); + fd.append("size", SIZE); + fd.append("quality", "high"); + fd.append("n", "1"); + for (const ref of refs) { + const buf = readFileSync(ref); + fd.append("image[]", new Blob([buf], { type: "image/png" }), basename(ref)); + } + res = await fetch("https://api.openai.com/v1/images/edits", { + method: "POST", headers: { Authorization: `Bearer ${apiKey}` }, body: fd, + }); + } else { + res = await fetch("https://api.openai.com/v1/images/generations", { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify({ model: "gpt-image-2", prompt: promptText, size: SIZE, quality: "high", n: 1 }), + }); + } + if (!res.ok) { + const err = await res.text(); + const transient = res.status === 429 || res.status >= 500; + if (transient && attempt < MAX_TRIES) { + const wait = 4000 * attempt; + console.warn(` ⚠ OpenAI ${res.status} (${label}) — retry ${attempt} in ${wait / 1000}s`); + await sleep(wait); continue; + } + console.error(`✗ OpenAI ${res.status} (${label}): ${err.slice(0, 400)}`); + return null; + } + const json = await res.json(); + const b64 = json.data?.[0]?.b64_json; + if (!b64) { console.error(`✗ no image (${label})`); return null; } + console.log(` · gen ${((Date.now() - t0) / 1000).toFixed(1)}s`); + return b64; + } catch (e) { + const cause = e?.cause?.code || e?.message || "unknown"; + if (attempt < MAX_TRIES) { + const wait = 4000 * attempt; + console.warn(` ⚠ network fail (${label}: ${cause}) — retry in ${wait / 1000}s`); + await sleep(wait); continue; + } + console.error(`✗ network fail (${label}): ${cause}`); + return null; + } + } + return null; +} + +// Vision gate: two ears, an eye inside each red lens, exactly one bunny. +async function qaPanel(b64) { + const ask = +`You are checking one frame of a needle-felt stop-motion music video for character errors. Answer STRICTLY as JSON: {"pass":true|false,"reason":"..."}. +FAIL if any of these are true: +1. The rabbit has only ONE visible long ear when the pose would show two (both ears must be present somewhere in frame). +2. Any red glasses lens is EMPTY — an eye sitting above/outside its rim while a red circle sits elsewhere on the face. Each visible eye must be centered INSIDE a red lens. +3. More than TWO eyes read on the face (an eye plus two empty rims counts as this failure). +4. There is MORE THAN ONE rabbit in the frame, or a human person appears. +5. There is readable text, a wordmark, or a logo anywhere. +Otherwise PASS. If the frame is a close-up of paws/rug where no face is shown, PASS (rules 1-3 do not apply).`; + try { + const r = await fetch("https://api.openai.com/v1/chat/completions", { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify({ + model: QA_MODEL, + messages: [{ + role: "user", + content: [ + { type: "text", text: ask }, + { type: "image_url", image_url: { url: `data:image/png;base64,${b64}` } }, + ], + }], + }), + }); + if (!r.ok) { console.warn(` ⚠ QA unavailable (${r.status}) — accepting`); return { pass: true, reason: "qa-skipped" }; } + const j = await r.json(); + const text = j.choices?.[0]?.message?.content || ""; + const m = text.match(/\{[\s\S]*\}/); + if (!m) return { pass: true, reason: "qa-unparsed" }; + return JSON.parse(m[0]); + } catch { + console.warn(" ⚠ QA network fail — accepting"); + return { pass: true, reason: "qa-skipped" }; + } +} + +function archiveRejected(png, reason) { + mkdirSync(REJECTED, { recursive: true }); + const base = basename(png).replace(/\.png$/, ""); + const n = readdirSync(REJECTED).filter((f) => f.startsWith(base + ".rej")).length + 1; + const dest = join(REJECTED, `${base}.rej${n}.png`); + renameSync(png, dest); + console.log(` ⌂ rejected (${reason}) → rejected/${basename(dest)}`); +} + +async function generate(beat, i) { + const outPath = panelPath(i, beat.name); + const rel = outPath.replace(REPO + "/", ""); + if (existsSync(outPath) && !FORCE) { console.log(`✓ cached → ${rel}`); return true; } + // Every beat after the anchor refs the anchor so it is the same bunny. + const useEdit = i > 0 && existsSync(ANCHOR); + const refs = useEdit ? [ANCHOR] : []; + const attempts = beat.faceVisible ? QA_ATTEMPTS : 1; + console.log(`▸ ${i} ${beat.name} · ${SIZE} · ${useEdit ? "anchor ref" : "no refs"}${beat.faceVisible ? " · QA" : ""}`); + for (let qa = 1; qa <= attempts; qa++) { + const b64 = await genOnce(build(beat), refs, useEdit, beat.name); + if (!b64) return false; + if (beat.faceVisible) { + const verdict = await qaPanel(b64); + if (!verdict.pass) { + writeFileSync(outPath, Buffer.from(b64, "base64")); + archiveRejected(outPath, `QA ${qa}/${attempts}: ${verdict.reason}`); + continue; + } + console.log(` ✓ QA pass (${verdict.reason || "clean"})`); + } + writeFileSync(outPath, Buffer.from(b64, "base64")); + console.log(`✓ ${rel}`); + return true; + } + console.error(`✗ ${beat.name}: QA failed ${attempts}×`); + return false; +} + +// The anchor must exist (and go first) before any ref'd beat runs. +const todo = BEATS.map((b, i) => ({ b, i })).filter(({ b }) => !ONLY || ONLY.includes(b.name)); +const needsAnchor = todo.some(({ i }) => i > 0) && !existsSync(ANCHOR); +if (needsAnchor && !todo.some(({ i }) => i === 0)) todo.unshift({ b: BEATS[0], i: 0 }); + +let failed = 0; +for (const { b, i } of todo) if (!(await generate(b, i))) failed++; +console.log(failed ? `✗ ${failed} panel(s) failed` : `✓ panels ready`); +process.exit(failed ? 1 : 0); diff --git a/pop/maytrax/bin/render-femrag-plusplus.mjs b/pop/maytrax/bin/render-femrag-plusplus.mjs new file mode 100644 index 0000000000..64fdf90fd0 --- /dev/null +++ b/pop/maytrax/bin/render-femrag-plusplus.mjs @@ -0,0 +1,517 @@ +#!/usr/bin/env node +// femrag++ — femrag's FEM-marimba rag grown into a drum-and-bass engine. +// Act one keeps femrag's sine-only vocabulary; then a snare-roll buildup +// tears the floor out and the track drops into straight two-step drum and +// bass: noise snares and hats, a tanh-driven sub bassline, and the rag +// bells riding double-time on top. +// +// 80 bars × 4 beats × 60 / 144 BPM = 133.33 seconds ≈ 2:13. +// +// node pop/maytrax/bin/render-femrag-plusplus.mjs +// node pop/maytrax/bin/render-femrag-plusplus.mjs --out ~/femrag-plusplus.mp3 + +import { mkdirSync, unlinkSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const TITLE = "femrag++"; +const SLUG = "femrag-plusplus"; +const SR = 48_000; +const BPM = 144; +const BEAT = 60 / BPM; +const BAR = BEAT * 4; +const BARS = 80; +const SECONDS = BARS * BAR; +const NS = Math.ceil(SECONDS * SR); +const TAU = Math.PI * 2; +const SWING = .62; +const out = [new Float32Array(NS), new Float32Array(NS)]; +let voices = 0; +// Every musical hit also lands here, so the video renderer can draw the +// exact same performance the ear hears. +const EVENTS = []; + +const argv = (flag) => { + const i = process.argv.indexOf(flag); + return i < 0 ? null : process.argv[i + 1]; +}; +const expandHome = (p) => !p ? p : p === "~" ? homedir() + : p.startsWith("~/") ? resolve(homedir(), p.slice(2)) : p; +const hz = (midi) => 440 * 2 ** ((midi - 69) / 12); +const at = (bar, beat = 0) => bar * BAR + beat * BEAT; +const swung = (beat) => { + const eighth = beat * 2; + const whole = Math.floor(eighth + 1e-9); + const fraction = eighth - whole; + return Math.floor(whole / 2) * BEAT + (whole % 2 ? SWING * BEAT : 0) + + fraction * BEAT / 2; +}; + +// Deterministic noise so every render is the same take. +let seed = 0x9e3779b9; +const rand = () => { + seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5; + seed >>>= 0; + return seed / 0xffffffff * 2 - 1; +}; + +function oscillator(t0, duration, freq, gain, { + attack = .003, release = .08, pan = 0, phase = 0, freqEnd = freq, +} = {}) { + const begin = Math.max(0, Math.floor(t0 * SR)); + const end = Math.min(NS, Math.ceil((t0 + duration) * SR)); + const angle = (pan + 1) * Math.PI / 4; + const lr = [Math.cos(angle), Math.sin(angle)]; + let p = phase; + voices++; + for (let i = begin; i < end; i++) { + const elapsed = i / SR - t0; + const remaining = duration - elapsed; + const a = Math.min(1, elapsed / attack); + const r = Math.min(1, remaining / release); + const env = Math.sin(Math.max(0, a) * Math.PI / 2) ** 2 + * Math.sin(Math.max(0, r) * Math.PI / 2) ** 2; + const f = freq * (freqEnd / freq) ** (elapsed / duration); + p += TAU * f / SR; + const sample = Math.sin(p) * gain * env; + out[0][i] += sample * lr[0]; + out[1][i] += sample * lr[1]; + } +} + +// Band-passed noise burst through a state-variable filter — the ++ over +// femrag's sine-only rule. Center frequency can sweep for risers. +function noiseBurst(t0, duration, gain, { + attack = .001, release = .08, pan = 0, center = 1800, centerEnd = center, + q = 1.2, +} = {}) { + const begin = Math.max(0, Math.floor(t0 * SR)); + const end = Math.min(NS, Math.ceil((t0 + duration) * SR)); + const angle = (pan + 1) * Math.PI / 4; + const lr = [Math.cos(angle), Math.sin(angle)]; + let low = 0, band = 0; + voices++; + for (let i = begin; i < end; i++) { + const elapsed = i / SR - t0; + const remaining = duration - elapsed; + const a = Math.min(1, elapsed / attack); + const r = Math.min(1, remaining / release); + const env = Math.sin(Math.max(0, a) * Math.PI / 2) ** 2 + * Math.sin(Math.max(0, r) * Math.PI / 2) ** 2; + const fc = center * (centerEnd / center) ** (elapsed / duration); + const f = 2 * Math.sin(Math.PI * Math.min(fc, SR * .22) / SR); + const high = rand() - low - band / q; + band += f * high; + low += f * band; + const sample = band * gain * env; + out[0][i] += sample * lr[0]; + out[1][i] += sample * lr[1]; + } +} + +// Sub bass: a sine pushed through tanh so it grows teeth. Optional amplitude +// wobble reads as the classic dnb LFO bass. +function sub(t0, duration, midi, gain, { + drive = 2.6, wobble = 0, slideTo = midi, attack = .004, release = .07, +} = {}) { + EVENTS.push({ i: "sub", t: t0, dur: duration, midi, gain, wobble }); + const begin = Math.max(0, Math.floor(t0 * SR)); + const end = Math.min(NS, Math.ceil((t0 + duration) * SR)); + const f0 = hz(midi), f1 = hz(slideTo); + let p = 0; + voices++; + for (let i = begin; i < end; i++) { + const elapsed = i / SR - t0; + const remaining = duration - elapsed; + const a = Math.min(1, elapsed / attack); + const r = Math.min(1, remaining / release); + const env = Math.sin(Math.max(0, a) * Math.PI / 2) ** 2 + * Math.sin(Math.max(0, r) * Math.PI / 2) ** 2; + const f = f0 * (f1 / f0) ** (elapsed / duration); + p += TAU * f / SR; + const wob = wobble ? .55 + .45 * Math.sin(TAU * wobble * elapsed - Math.PI / 2) : 1; + const sample = Math.tanh(Math.sin(p) * drive) * gain * env * wob; + out[0][i] += sample * .5; + out[1][i] += sample * .5; + } +} + +// femrag's FEM-marimba bell: bowl body modes + the first FEM-derived handbell +// shell modes from pop/bell, decayed short enough to play like a bar. +function bell(t0, midi, gain = .06, pan = 0, length = .22) { + EVENTS.push({ i: "bell", t: t0, midi, gain, pan, length }); + const f = hz(midi); + const partials = [ + [.461, .22, length * 1.35], [.688, .25, length * 1.18], [1, 1, length], + [3.479, .17, length * .54], [3.559, .14, length * .48], [4.81, .08, length * .34], + ]; + partials.forEach(([ratio, level, duration], i) => oscillator(t0, duration, f * ratio, gain * level, { + attack: .0015, release: duration * .88, pan: pan + (i - 1) * .04, phase: i * 1.1, + })); +} + +function boom(t0, gain = .18) { + EVENTS.push({ i: "boom", t: t0, gain }); + oscillator(t0, .34, 148, gain, { + attack: .0015, release: .31, freqEnd: 43, pan: 0, + }); + oscillator(t0, .19, 58, gain * .45, { + attack: .002, release: .17, freqEnd: 45, pan: 0, phase: .7, + }); +} + +// The dnb kick: femrag's boom with a tanh click so it punches through snares. +function kick(t0, gain = .2) { + boom(t0, gain); + oscillator(t0, .02, 320, gain * .5, { + attack: .0005, release: .015, freqEnd: 90, pan: 0, + }); +} + +function snare(t0, gain = .16, { ghost = false } = {}) { + EVENTS.push({ i: "snare", t: t0, gain, ghost }); + const body = ghost ? .6 : 1; + oscillator(t0, .09, 220, gain * .5 * body, { + attack: .001, release: .07, freqEnd: 165, pan: 0, + }); + noiseBurst(t0, ghost ? .07 : .15, gain * body, { + attack: .0008, release: ghost ? .05 : .12, center: 2100, q: .9, + pan: ghost ? .3 : 0, + }); + if (!ghost) noiseBurst(t0, .05, gain * .5, { + attack: .0005, release: .04, center: 5200, q: 1.6, pan: -.15, + }); +} + +function hat(t0, gain = .05, open = false) { + EVENTS.push({ i: "hat", t: t0, gain, open }); + noiseBurst(t0, open ? .16 : .035, gain, { + attack: .0006, release: open ? .13 : .025, center: 8600, q: 2.2, + pan: open ? -.5 : .55, + }); +} + +function riser(t0, duration, gain = .1) { + EVENTS.push({ i: "riser", t: t0, dur: duration }); + noiseBurst(t0, duration, gain, { + attack: duration * .8, release: duration * .15, + center: 500, centerEnd: 7500, q: 1.4, pan: 0, + }); + oscillator(t0, duration, hz(57), gain * .5, { + attack: duration * .85, release: duration * .12, freqEnd: hz(81), pan: .2, + }); + oscillator(t0, duration, hz(57) * 1.011, gain * .5, { + attack: duration * .85, release: duration * .12, freqEnd: hz(81) * 1.011, + pan: -.2, phase: 2, + }); +} + +const CHORDS = [ + { bass: 45, notes: [57, 61, 64, 69] }, // A + { bass: 38, notes: [57, 62, 66, 69] }, // D + { bass: 42, notes: [57, 61, 66, 69] }, // F#m + { bass: 40, notes: [56, 59, 64, 68] }, // E +]; +const RAG = [0, 2, 1, 3, 0, 2, 1, 3]; +const LEAD = [ + [[69, 0], [73, 1.5], [76, 2.5], [73, 3.25]], + [[78, 0], [76, .75], [73, 1.5], [71, 2.5], [73, 3.25]], + [[69, 0], [71, .75], [73, 1.5], [76, 2.25], [78, 3]], + [[76, 0], [73, 1], [71, 2], [68, 2.75], [69, 3.5]], +]; + +function kickBar(bar, weight = 1, pickup = false) { + for (let beat = 0; beat < 4; beat++) boom(at(bar, beat), .16 * weight * (beat === 0 ? 1.08 : 1)); + if (pickup) boom(at(bar, 3.75), .075 * weight); +} + +function ragBar(bar, density = 1, octave = 0, { pattern = true, oom = true, straight = false } = {}) { + const chord = CHORDS[bar % 4]; + const step = (beat) => straight ? beat * BEAT : swung(beat); + if (pattern) for (let e = 0; e < 8; e++) { + if (density < .7 && e % 2) continue; + const tt = at(bar) + step(e * .5); + const midi = chord.notes[RAG[e]] + octave; + bell(tt, midi, (e % 2 ? .035 : .052) * density, e % 2 ? .46 : -.38, .16); + } + if (oom) { + for (const beat of [0, 2]) bell(at(bar, beat), chord.bass, .072 * density, -.55, .2); + for (const beat of [1, 3]) chord.notes.slice(1, 3).forEach((m, i) => + bell(at(bar, beat + .5) + i * .008, m + octave, .022 * density, .18 + i * .22, .13)); + } +} + +function leadBar(bar, amount = 1, octave = 0, { straight = false } = {}) { + const step = (beat) => straight ? beat * BEAT : swung(beat); + LEAD[bar % 4].forEach(([midi, beat], i) => { + const tt = at(bar) + step(beat); + bell(tt, midi + octave, .075 * amount, .18 + (i % 2) * .35, .24); + if (i % 2 === 0) bell(tt + BEAT * .25, midi + 12 + octave, .025 * amount, -.58, .1); + }); +} + +// High bells in a regular 3-over-2 cross-rhythm — six even hits per bar — +// voiced from the current chord so the shimmer always agrees with the harmony. +function ticks(bar, amount = 1) { + const notes = CHORDS[bar % 4].notes; + for (let s = 1; s < 6; s++) { + const note = notes[[0, 2, 1, 3, 2, 1][s]] + 24; + bell(at(bar, s * 2 / 3), note, .012 * amount * (s % 2 ? 1 : .7), s % 2 ? .82 : -.82, .06); + } +} + +// Straight two-step: kick on 1 and the and-of-2, snare on 2 and 4, ghosts and +// 16th hats between. `fill` swaps the last half-bar for a tumbling snare run. +function dnbBar(bar, weight = 1, { fill = false, doubleKick = false } = {}) { + kick(at(bar, 0), .2 * weight); + kick(at(bar, 2.5), .19 * weight); + if (doubleKick) kick(at(bar, 1.75), .13 * weight); + snare(at(bar, 1), .15 * weight); + if (!fill) { + snare(at(bar, 3), .15 * weight); + snare(at(bar, 3.5), .05 * weight, { ghost: true }); + if (bar % 2 === 1) snare(at(bar, 1.75), .045 * weight, { ghost: true }); + } else { + for (let s = 0; s < 6; s++) { + snare(at(bar, 2.5 + s * .25), (.06 + s * .016) * weight, { ghost: s < 3 }); + } + } + for (let s = 0; s < 16; s++) { + if (s === 0 || s === 10) continue; + hat(at(bar, s / 4), .032 * weight * (s % 2 ? .7 : 1), fill ? false : s === 14 && bar % 4 === 3); + } +} + +// The dnb bassline walks the rag's roots an octave down, sliding and wobbling. +function bassBar(bar, weight = 1, { wobble = false } = {}) { + const root = CHORDS[bar % 4].bass - 12; + if (wobble) { + sub(at(bar, 0), BEAT * 3.4, root, .21 * weight, { + drive: 3.2, wobble: bar % 2 ? 6 : 4.5, + }); + } else { + sub(at(bar, 0), BEAT * 1.4, root, .21 * weight, { drive: 3, slideTo: root }); + sub(at(bar, 1.5), BEAT * .4, root + 12, .12 * weight, { drive: 2.4 }); + sub(at(bar, 2), BEAT * 1.3, root, .2 * weight, { + drive: 3, slideTo: bar % 4 === 3 ? root - 5 : root, + }); + sub(at(bar, 3.5), BEAT * .45, root + 7, .13 * weight, { drive: 2.6, slideTo: root }); + } +} + +// The donk: a driven sine that falls an octave in 150 ms — UK donk dink. +function donk(t0, gain = .15, midi = 57) { + EVENTS.push({ i: "donk", t: t0, gain }); + sub(t0, .15, midi, gain, { + drive: 5, slideTo: midi - 12, attack: .0015, release: .05, + }); +} + +// Ragga-donk kit: swung two-step with a dembow lean — donks riding every +// swung offbeat, hats swung hard, the snare talking patois on 3. +function raggaBar(bar, weight = 1) { + kick(at(bar, 0), .2 * weight); + kick(at(bar) + swung(2.5), .18 * weight); + snare(at(bar, 1), .12 * weight); + snare(at(bar, 3), .16 * weight); + snare(at(bar) + swung(1.75), .05 * weight, { ghost: true }); + if (bar % 2 === 1) snare(at(bar) + swung(3.75), .045 * weight, { ghost: true }); + for (const beat of [.5, 1.5, 2.5, 3.5]) donk(at(bar) + swung(beat), .15 * weight); + for (let e = 1; e < 8; e++) { + if (e === 5) continue; + hat(at(bar) + swung(e * .5), .034 * weight * (e % 2 ? .75 : 1), e === 7 && bar % 2 === 1); + } +} + +// The deep wub: slow quarter-note wobble an octave under the dnb bassline, +// with a driven octave doubling so the wub reads on small speakers. +function wubBar(bar, weight = 1) { + const root = CHORDS[bar % 4].bass - 12; + const rate = bar % 4 === 3 ? 3 : 2.25; + sub(at(bar, 0), BEAT * 3.6, root, .2 * weight, { drive: 3.6, wobble: rate }); + sub(at(bar, 0), BEAT * 3.6, root + 12, .07 * weight, { drive: 4, wobble: rate }); +} + +// ── act one · femrag compressed to a fuse ─────────────────────────────────── + +// 0–4: the runway — the drop's two-step skeleton arrives FIRST, quiet, with +// the rag straight-time on top. The whole track lives in one rhythm world. +for (let bar = 0; bar < 4; bar++) { + const rise = .5 + bar * .1; + kick(at(bar, 0), .17 * rise); + kick(at(bar, 2.5), .16 * rise); + snare(at(bar, 1), .09 * rise, { ghost: bar < 2 }); + snare(at(bar, 3), .09 * rise, { ghost: bar < 2 }); + if (bar >= 1) ragBar(bar, .45 + bar * .08, 12, { oom: false, straight: true }); + if (bar >= 2) ticks(bar, .3 + (bar - 2) * .2); +} + +// 4–12: the drop's groove at half power — full dnb kit muted, rag and lead +// straight-time, sub pips walking the roots. The drop will only turn it up. +for (let bar = 4; bar < 12; bar++) { + const grow = .55 + (bar - 4) * .04; + dnbBar(bar, grow, { fill: bar === 11 }); + ragBar(bar, .82, 12, { oom: false, straight: true }); + leadBar(bar, .7, bar < 8 ? 0 : 12, { straight: true }); + ticks(bar, .5); + if (bar >= 6) sub(at(bar, 0), BEAT * 1.6, CHORDS[bar % 4].bass - 12, .08 + (bar - 6) * .015, { drive: 2 }); +} + +// ── the buildup · snare roll 8ths → 32nds in four bars ───────────────────── + +for (let bar = 12; bar < 16; bar++) { + const heat = (bar - 12) / 4; + kickBar(bar, .95 + heat * .2); + ragBar(bar, .5 - heat * .3, 12, { oom: false, straight: true }); + ticks(bar, .4 + heat * .5); + const division = [4, 8, 16, 32][bar - 12]; + for (let s = 0; s < division; s++) { + snare(at(bar, s * 4 / division), .05 + heat * .11 + s / division * .05, { + ghost: division <= 8 && s % 2 === 1, + }); + } + sub(at(bar, 0), BAR * .95, 33, .1 + heat * .12, { drive: 2.8, wobble: 2 + heat * 7 }); +} +riser(at(12), BAR * 4 - BEAT * .5, .12); +// The last half-beat before the drop goes silent — the intake of breath. +snare(at(15, 3.5), .22); + +// ── first drop (0:27) · straight two-step drum and bass ──────────────────── + +// 16–32: straight time, heavy sub walking the rag roots, bells double-time +// on top so the old tune rides the new engine. +for (let bar = 16; bar < 32; bar++) { + const w = bar === 16 ? 1.1 : 1; + dnbBar(bar, w, { fill: bar % 8 === 7, doubleKick: bar % 4 === 2 }); + bassBar(bar, 1); + ragBar(bar, .85, 12, { oom: false, straight: true }); + if (bar >= 20) leadBar(bar, .7, 12, { straight: true }); + if (bar >= 24) ticks(bar, .6); +} +sub(at(16, 0), BEAT * 3.8, 21, .24, { drive: 3.4, slideTo: 33 }); // drop stinger + +// 32–40: breakdown — half-time feel, wobble bass, the rag drifts back swung +// like a memory of act one. +for (let bar = 32; bar < 40; bar++) { + kick(at(bar, 0), .17); + snare(at(bar, 2), .13); + bassBar(bar, .9, { wobble: true }); + ragBar(bar, .4, bar >= 36 ? 12 : 0); + if (bar % 2 === 0) leadBar(bar, .5, 0); + hat(at(bar, 1.5), .03); hat(at(bar, 3.5), .03, bar % 2 === 1); +} + +// 40–44: second buildup, straight to 16ths then 32nds. +for (let bar = 40; bar < 44; bar++) { + const heat = (bar - 40) / 4; + kickBar(bar, 1 + heat * .15); + ticks(bar, .5 + heat * .5); + const division = [4, 8, 16, 32][bar - 40]; + for (let s = 0; s < division; s++) { + snare(at(bar, s * 4 / division), .05 + heat * .12 + s / division * .05, { + ghost: division <= 8 && s % 2 === 1, + }); + } + sub(at(bar, 0), BAR * .95, 33, .12 + heat * .12, { drive: 3, wobble: 4 + heat * 8 }); + for (let e = 0; e < 4; e++) bell(at(bar, e), 69 + 12 + (bar - 40) * 2 + e, .04 + heat * .03, e % 2 ? .6 : -.6, .1); +} +riser(at(41), BAR * 3 - BEAT * .25, .13); + +// 44–56: second drop — fills every fourth bar, double kicks, lead up top. +for (let bar = 44; bar < 56; bar++) { + const w = bar === 44 ? 1.15 : 1.05; + dnbBar(bar, w, { fill: bar % 4 === 3, doubleKick: bar % 2 === 1 }); + bassBar(bar, 1.1); + ragBar(bar, .9, 12, { oom: false, straight: true }); + leadBar(bar, .85, 12, { straight: true }); + ticks(bar, .75); +} +sub(at(44, 0), BEAT * 3.8, 21, .26, { drive: 3.6, slideTo: 33 }); + +// ── 56–72 (1:33) · the ragga-donk · deep wub, swing back on, serious wibes ─ + +for (let bar = 56; bar < 72; bar++) { + const w = bar === 56 ? 1.15 : 1.05; + raggaBar(bar, w); + wubBar(bar, 1); + ragBar(bar, .8, 12, { oom: false }); + if (bar % 4 >= 2) leadBar(bar, .6, 12); + if (bar % 8 === 4) for (let s = 0; s < 8; s++) { + bell(at(bar, 2) + s * BEAT / 4, CHORDS[bar % 4].notes[s % 4] + 24, .028, s % 2 ? .7 : -.7, .08); + } +} +sub(at(56, 0), BEAT * 3.8, 21, .28, { drive: 4, slideTo: 33 }); + +// 72–80: outro — the wub winds down and femrag's swing gets the last word. +for (let bar = 72; bar < 76; bar++) { + const fade = 1 - (bar - 72) * .18; + raggaBar(bar, .8 * fade); + wubBar(bar, .75 * fade); + ragBar(bar, .55 * fade, 12, { oom: false }); +} +for (let bar = 76; bar < 80; bar++) { + const fade = 1 - (bar - 76) * .2; + kickBar(bar, .7 * fade); + ragBar(bar, .45 * fade); + if (bar === 76) leadBar(bar, .5); + if (bar === 78) leadBar(bar, .35, -12); +} +bell(at(79, 3), 45, .085, 0, .42); +sub(at(79, 3), BEAT * 1.2, 33, .1, { drive: 2, release: .4 }); + +// ── master ───────────────────────────────────────────────────────────────── + +let peak = 0; +for (const channel of out) for (const sample of channel) peak = Math.max(peak, Math.abs(sample)); +const gain = peak ? .88 / peak : 1; +const fadeIn = Math.floor(.006 * SR); +const fadeOut = Math.floor(.55 * SR); +for (const channel of out) { + for (let i = 0; i < NS; i++) channel[i] *= gain; + for (let i = 0; i < fadeIn; i++) channel[i] *= i / fadeIn; + for (let i = 0; i < fadeOut; i++) channel[NS - 1 - i] *= i / fadeOut; +} + +const outPath = expandHome(argv("--out")) || resolve(HERE, "..", "out", `${SLUG}.mp3`); +mkdirSync(dirname(outPath), { recursive: true }); +writeFileSync(outPath.replace(/\.mp3$/, ".events.json"), + JSON.stringify({ bpm: BPM, bars: BARS, seconds: SECONDS, events: EVENTS })); + +// The arrangement, as the video pipeline reads it. Bar ranges here are the +// same ones the section loops above use — change one, change both. +const SECTIONS = [ + ["runway", 0, 4], ["groove", 4, 12], ["buildup1", 12, 16], + ["drop1a", 16, 24], ["drop1b", 24, 32], ["breakdown", 32, 40], + ["buildup2", 40, 44], ["drop2a", 44, 52], ["drop2b", 52, 56], + ["ragga-a", 56, 64], ["ragga-b", 64, 72], ["outro", 72, 80], +]; +writeFileSync(outPath.replace(/\.mp3$/, ".struct.json"), JSON.stringify({ + totalSec: SECONDS, + sections: SECTIONS.map(([name, from, to]) => ({ + name, startSec: at(from), endSec: at(to), + })), +}, null, 2)); +const rawPath = `${outPath}.f32.raw`; +const buffer = Buffer.alloc(NS * 8); +for (let i = 0; i < NS; i++) { + buffer.writeFloatLE(out[0][i], i * 8); + buffer.writeFloatLE(out[1][i], i * 8 + 4); +} +writeFileSync(rawPath, buffer); +const ffmpeg = spawnSync("ffmpeg", [ + "-hide_banner", "-y", "-loglevel", "error", + "-f", "f32le", "-ar", String(SR), "-ac", "2", "-i", rawPath, + "-af", "acompressor=threshold=-16dB:ratio=2.4:attack=12:release=90:makeup=2:knee=6,alimiter=limit=.96:attack=4:release=70", + "-c:a", "libmp3lame", "-b:a", "320k", + "-metadata", `title=${TITLE}`, "-metadata", "artist=jeffrey", "-metadata", "album=pixsies", + outPath, +], { stdio: "inherit" }); +try { unlinkSync(rawPath); } catch {} +if (ffmpeg.status !== 0) { + console.error("✗ ffmpeg failed"); + process.exit(1); +} +console.log(`✓ ${outPath} · ${BPM} BPM · ${BARS} bars · ${SECONDS.toFixed(1)} s · ${voices} voices`); -- 2.51.2 From e08ce4ec73a3fc1037f1c716aa0501f5931c4aef Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 13 Aug 2026 16:49:53 -0400 Subject: [PATCH 2/6] pop/maytrax: resolve shakeout panels by name, not index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @jeffrey trimmed the four runway bars off the front of femrag++ (TRIM in render-femrag-plusplus.mjs), so the record opens cold on the groove and the struct now carries 11 sections instead of 12. The panel filenames still number `runway` as 0, which left the motion driver looking for sec-0-groove.png while the file on disk was sec-1-groove.png. panelFor now globs by section NAME and ignores the index in the filename, so trimming the arrangement can move a section's position without breaking its panel lookup. The runway panel stays as the character anchor every other panel is generated against; it is simply never a shot. The groove shot's motion is rewritten as a cold open — he starts already in the groove rather than finding it. Co-Authored-By: Claude Opus 5 (1M context) --- .../gen-motion-femrag-plusplus-shakeout.mjs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs b/pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs index de0c5041a0..a0465f07b9 100644 --- a/pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs +++ b/pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs @@ -20,6 +20,7 @@ // node pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs // node pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs --assemble +import { readdirSync } from "node:fs"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { runMotionCli, parseFlags } from "../../lib/motion-pipeline.mjs"; @@ -42,13 +43,19 @@ const MEDIUM_MOTION = [ // hanging spent at the end. FUZZ CONTINUITY: his wool gets progressively // more disheveled and the room accumulates loose wisps; it never resets. const SHOTS = { + // `runway` is deliberately not in the struct — @jeffrey trimmed those four + // bars off the front of the track (render-femrag-plusplus.mjs TRIM), so the + // record opens cold on the groove. Its panel survives as the character + // ANCHOR every other panel is generated against; it is simply never a shot. runway: { motion: `The felt bunny starts to move on the rug — a small weight-shift from one hind paw to the other, then a second, finding the pulse. His two long ears hang heavy and swing only slightly, a few degrees, lagging behind each shift. One front paw taps twice against his own chest. The camera holds nearly still and drifts in by a hair. Dim, warm, contained — a dance that has not committed yet.`, }, groove: { + // The film's COLD OPEN — the track starts here, hats first, already + // moving. He is not finding the groove; he is already in it. motion: -`The bunny settles into a two-step shuffle: one hind paw plants, the other slides across the wool rug and back, hips turning slightly with each step, both front paws bouncing loosely at rib height. His two long ears swing together in a matched arc, lifting further off vertical with each bar. The rug rumples under the planted paw and stays rumpled. The camera tracks slowly sideways with him, staying wide. Confident and easy — the groove is found.`, +`Open already in motion: the bunny is mid two-step shuffle from the very first frame — one hind paw plants, the other slides across the wool rug and back, hips turning with each step, both front paws bouncing loosely at rib height. His two long ears swing together in a matched arc that widens with each bar. The rug rumples under the planted paw and stays rumpled. The camera tracks slowly sideways with him, staying wide. Confident and easy — no hesitation, no warm-up.`, }, buildup1: { // Same-camera escalation into the drop — a legitimate morph. @@ -131,11 +138,23 @@ const SHOTS = { }, }; +// Panels resolve by NAME, not by the index baked into their filename: the +// arrangement is trimmed from the front (render-femrag-plusplus.mjs TRIM), +// so a section's position moves while its identity does not. +function panelFor(name) { + const suffix = `-${name}.png`; + const hit = readdirSync(OUT) + .filter((f) => f.startsWith(`${SLUG}-yt-sec-`) && f.endsWith(suffix)) + .sort(); + if (!hit.length) return `${OUT}/${SLUG}-yt-sec-?-${name}.png`; // let the CLI report it missing + return `${OUT}/${hit[0]}`; +} + const cfg = { slug: SLUG, laneDir: LANE, structPath: `${OUT}/${SLUG}.struct.json`, - panelFor: (name, i) => `${OUT}/${SLUG}-yt-sec-${i}-${name}.png`, + panelFor, shots: SHOTS, mediumMotion: MEDIUM_MOTION, ratio: "16:9", -- 2.51.2 From 390947c6dd476eb7aefb8ca37cd7669baae58880 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 13 Aug 2026 17:52:37 -0400 Subject: [PATCH 3/6] =?UTF-8?q?pop/maytrax:=20SHAKEOUT=20v2=20=E2=80=94=20?= =?UTF-8?q?electric=20felt=20in=20the=20Ashland=20A-frame?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @jeffrey's note on v1 was that it was inert, and the art direction was the reason: it explicitly BANNED neon and glow, so the film had nowhere to go when the drops hit. v2 inverts that and gives the bunny his own power. - ELECTRIC FELT: copper and magnet wire needle-felted into his body as circuit traces, LEDs embedded UNDER the felt lighting him from within (wool is translucent backlit, so the glow blooms through the fibers and each LED reads as a hot core in a soft patch), LEDs inside both ears so they glow translucent when they swing, and a wire tail plugging him into the speaker stack. He is part of the circuit. - THE ROOM is now @jeffrey's old Ashland A-frame, generated against the reference contact sheet already in pop/nullabye: exposed rafters meeting in a peak, honey plank walls and floor, gable window, round green rug. - LIGHT is a practical rig, not a filter: real bent neon and EL-wire zip-tied to the rafters, blasting on the dynamic beats and dropping to embers in the quiet ones, with his internal glow as the light of last resort in the breakdown. - The arrangement moved again (TRIM=16, 88 bars): the track now opens cold on the drop and the ragga section grew ragga-breathe/push/push-b. Beats and SHOTS follow, `groove`/`buildup1` are gone, and the POV laptop-making-the-beat opening lands on drop1a as the film's first shot. - EAR COUNT is now enforced in BOTH places. The 4-ear artifact is a motion failure (a fast ear duplicating across frames) but the law only existed in the still prompts; EAR_LAW now rides every Seedance prompt, and the vision QA counts ears rather than only checking for a missing one. - ragga-push-b carries a real camera orbit. Co-Authored-By: Claude Opus 5 (1M context) --- .../gen-motion-femrag-plusplus-shakeout.mjs | 55 ++++---- .../gen-sections-femrag-plusplus-shakeout.mjs | 119 ++++++++++++------ 2 files changed, 102 insertions(+), 72 deletions(-) diff --git a/pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs b/pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs index a0465f07b9..9eacad7a13 100644 --- a/pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs +++ b/pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs @@ -31,8 +31,17 @@ const LANE = resolve(HERE, ".."); const OUT = `${LANE}/out`; const SLUG = "femrag-plusplus"; +// EAR COUNT is the failure mode this film actually hits: a fast-swinging ear +// duplicates across frames and the bunny reads as having three or four. The +// still prompts guard it too, but a still can only be wrong once — motion has +// to hold the count every frame, so the contract says so explicitly. +const EAR_LAW = +`EAR COUNT — ABSOLUTE, EVERY SINGLE FRAME: the bunny has EXACTLY TWO EARS, attached at two fixed points on the top of his head. TWO. Never three, never four. When an ear swings fast it must stay ONE continuous ear that moves — it must NEVER split into two, echo, ghost, smear into a duplicate, leave a second copy behind at its old position, or grow an extra ear on the far side of his head. If a pose or a fast swing makes the count ambiguous, slow that motion down rather than duplicating geometry. Both ears remain attached to his head at all times; neither detaches, floats free, or passes through his skull. +LIGHT CONTINUITY — the LEDs inside his body and inside both ears stay lit and stay in the SAME physical places for the whole shot; the internal glow may pulse in brightness with the beat but never migrates, switches off entirely, or turns into a flat overlay. The neon tubes in the rafters are physical fixtures fixed in place — they may change brightness or color on the beat, but they never move, bend, drift, or reposition themselves between frames.`; + const MEDIUM_MOTION = [ `A needle-felt music video at 144 BPM — every motion is DANCE, driven and rhythmic, landing on beats rather than drifting. The wool bunny never goes limp between moves; he is always either loading or releasing.`, + EAR_LAW, FRAMING_YT_LANDSCAPE_MOTION, NEEDLE_FELT_WOOL_MOTION, ].join("\n\n"); @@ -50,40 +59,12 @@ const SHOTS = { runway: { motion: `The felt bunny starts to move on the rug — a small weight-shift from one hind paw to the other, then a second, finding the pulse. His two long ears hang heavy and swing only slightly, a few degrees, lagging behind each shift. One front paw taps twice against his own chest. The camera holds nearly still and drifts in by a hair. Dim, warm, contained — a dance that has not committed yet.`, - }, - groove: { - // The film's COLD OPEN — the track starts here, hats first, already - // moving. He is not finding the groove; he is already in it. - motion: -`Open already in motion: the bunny is mid two-step shuffle from the very first frame — one hind paw plants, the other slides across the wool rug and back, hips turning with each step, both front paws bouncing loosely at rib height. His two long ears swing together in a matched arc that widens with each bar. The rug rumples under the planted paw and stays rumpled. The camera tracks slowly sideways with him, staying wide. Confident and easy — no hesitation, no warm-up.`, - }, - buildup1: { - // Same-camera escalation into the drop — a legitimate morph. - morphTo: "drop1a", - physical: "extreme", - contacts: ["both hind paws → rug", "front paws → own chest", "ears → held back under tension"], - invariants: ["exactly one bunny in frame at all times", "both long ears visible throughout"], - beats: [ - { at: 0, action: "close on the bunny, shoulders drawing in, chin tucking, front paws pulling to his chest" }, - { at: .35, action: "his two long ears sweep back behind his head and pull taut, straining — maximum tension" }, - { at: .7, action: "he compresses further, knees bending, weight sinking onto both hind paws — the coil" }, - { at: 1, action: "the release begins: hind paws drive against the rug and he starts to rise, ears whipping forward off their tension, camera pulling back and down toward the low angle" }, - ], - motion: -`One continuous escalation on the SAME bunny — never two figures. He winds up tight in close-up, ears swept back and straining, then explodes: hind paws drive into the rug, he launches, and the camera falls back and down to the low rug-level angle as he goes airborne — landing exactly on the final frame. The room tears open around him as he rises.`, }, drop1a: { - physical: "extreme", - contacts: ["hind paws → airborne, no contact", "rug → visibly kicked and rucked where he launched"], - invariants: ["both long ears present and extended", "one bunny only"], - beats: [ - { at: 0, action: "the bunny is at the top of his leap, both hind paws off the rug, ears flung horizontal to each side" }, - { at: .3, action: "he twists through the air, front paws sweeping wide, ears whipping across the turn" }, - { at: .6, action: "hind paws reach down and strike the rug, wool compressing under the impact, ears snapping downward with the landing" }, - { at: 1, action: "he rebounds straight back up into the next shake, ears flying out again — the dance is continuous" }, - ], + // The film OPENS here — the track starts cold on the drop, so this POV + // is already at full power. No wind-up exists to morph from. motion: -`THE DROP, shot low from the rug looking up. The bunny lands hard, rebounds, and keeps going — a full-body shakeout on the beat: hips whipping, front paws thrown wide, both long ears flying out horizontally and snapping with every direction change. Loose wool wisps lift off him with each impact and hang in the bulb light. The yarn cables on the floor jump slightly when he lands. The camera stays low and holds, letting him dominate the frame.`, +`FIRST-PERSON POV, opening the film at full power: the camera is the bunny's own eyes looking down at the felted laptop on the rug. His own two forepaws hammer the felted keycaps in rhythm — real strikes, paws lifting and landing on the beat, the wool of the keys compressing under each hit. The colored blocks of light on the felt screen change and pulse as he plays, throwing shifting magenta, cyan and amber light up onto his paws and forearms; the LEDs under his wool flare brighter with each strike. Overhead at the edges of frame the neon rig blasts across the rafters. The camera breathes slightly with his head — small, human, never a smooth mechanical move — and NEVER shows his face or a second bunny.`, }, drop1b: { motion: @@ -131,6 +112,18 @@ const SHOTS = { "ragga-b": { motion: `Close and low — the bunny leaning back against the mouth of the biggest felted speaker cone, half-turned to it, still skanking loose and easy. One long ear swings forward across his own face and away again with each bar; the other trails behind. Behind him the enormous oatmeal wool cone visibly pushes outward at its center on every bass note and settles back, and loose fibers around its rim lift away from the surface with each push. His bead eyes stay happy inside the red lenses. The camera holds close, swaying a little with him.`, + }, + "ragga-breathe": { + motion: +`The room takes a breath. The neon drops away almost entirely and the bunny's own internal glow becomes the light source for the whole shot. He keeps skanking but small and easy — weight low, shoulders rolling, both front paws swinging gently across his body, his TWO long ears swinging in slow behind-the-beat arcs, warm gold and translucent. The light spilling out of his chest visibly moves across the green rug and the plank boards as he sways, and the felted speaker cones beside him pulse outward in slow deep pushes. The camera drifts in slowly and steadily. All glow, no blast.`, + }, + "ragga-push": { + motion: +`The room switches back on around him. Neon tubes along the rafters light one after another through the shot — amber, then forest-green, then hot orange — each one adding hard color to the beams and spilling further down the sloped ceiling onto the floor, so the A-frame builds from near-dark to fully lit across the take. The bunny digs in as it builds: the skank drops lower and gets heavier, paws swinging in bigger diagonals, his TWO long ears carving wider arcs, and his internal LEDs push from warm gold up toward white. The camera pushes in slowly with the build.`, + }, + "ragga-push-b": { + motion: +`Maximum dancehall, shot low from the plank floor looking up into the A-frame peak. Every neon tube is blazing at once and the whole triangular room is saturated with color. The bunny dances at full power — the hardest, widest skanking of the film, body low and turning, hind paw kicking back, front paws thrown wide, his TWO long ears whipping through big arcs and blazing translucent, his whole body a lantern throwing colored light onto the rug. CAMERA: a slow continuous ORBIT around him — the camera arcs steadily sideways through the shot so the speaker tower, the rafter peak and the gable window all sweep past behind him and the parallax reads as a real move around a real room. Loose wool wisps hang lit in the colored air.`, }, outro: { motion: diff --git a/pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs b/pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs index 40b5292680..8cd1336be0 100644 --- a/pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs +++ b/pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs @@ -73,24 +73,42 @@ function loadOpenAIKey() { throw new Error("OPENAI_API_KEY not set and not found in vault devcontainer.env"); } -// ── the character — inherited from pop/momboba/bunny/PREAMBLE.txt ──────── +// ── the character — pop/momboba/bunny/PREAMBLE.txt, wired for power ────── const BUNNY = -`THE BUNNY — a single needle-felted wool BUNNY, the recurring AESTHETIC COMPUTER character: a wonderfully woolly little rabbit with an off-white body, soft-grey ear tips and paws, TWO LONG felt ears, a little pink felt nose, tiny dark glossy bead eyes, and a round puff of a tail. he wears tiny round soft RED felt glasses — his signature. he stands and dances upright on his hind legs. the SAME bunny in every scene: identical wool colors, same two long ears, same red glasses. -HIS EARS ARE THE RHYTHM — in this story the ears carry the beat the way a dancer's hair does: hanging soft and heavy when the groove is quiet, swinging in wide arcs as it builds, flying and whipping out sideways at full extension when the drums drop, and finally settling. BOTH of his two long ears are ALWAYS present — even when one flies wide, the second stays visible. NEVER draw him with only one ear. +`THE BUNNY — a single needle-felted wool BUNNY, the recurring AESTHETIC COMPUTER character: a wonderfully woolly little rabbit with an off-white body, soft-grey ear tips and paws, EXACTLY TWO LONG felt ears, a little pink felt nose, tiny dark glossy bead eyes, and a round puff of a tail. he wears tiny round soft RED felt glasses — his signature. he stands and dances upright on his hind legs. the SAME bunny in every scene: identical wool colors, same two long ears, same red glasses. +EAR COUNT (CRITICAL): he has EXACTLY TWO EARS. TWO. never three, never four, never a spare pair behind the first pair. when the ears fly or blur with motion they must still resolve as TWO separate ears attached at two points on the top of his head — a flying ear NEVER splits, doubles, echoes, or leaves a duplicate behind it. +HIS EARS ARE THE RHYTHM — the two ears carry the beat the way a dancer's hair does: hanging heavy when the groove is quiet, swinging in wide arcs as it builds, flying out sideways at full extension when the drums drop, and finally settling. GLASSES GEOMETRY (CRITICAL): the bunny has exactly TWO eyes. a bunny's eyes sit HIGH and WIDE on the head — so the red glasses ride HIGH on his face, up over the eyes, NOT down low on the muzzle or snout. each eye — open bead eye or closed happy lash-line — is centered INSIDE its own round red lens, one eye per lens, the lens ringing the eye like real worn glasses. the red rims are NEVER empty. NEVER draw an eye ABOVE the rims with the empty lenses sitting lower on the snout (one real eye plus two empty red circles reads as extra eyes — the single worst failure).`; -// The world: one continuous felt room that transforms with the music. +// The ++ made visible: he is a felted creature with real electronics in him. +const ELECTRIC = +`ELECTRIC FELT — the bunny is WIRED. This is the whole look of the film and it must read in every frame: +· CIRCUITRY IN THE WOOL — fine enameled COPPER and colored magnet WIRE is needle-felted directly INTO his body, running across his chest, shoulders, back and down his limbs in deliberate looping CIRCUIT TRACES, stitched flush into the fibers like veins under skin. tiny felted solder-blobs and little brass eyelets sit where traces meet. the wire is a craft material here — hand-couched into wool, sometimes lifting a millimeter clear of the surface, never printed-on graphics. +· LEDS INSIDE HIM — small bright LEDs are embedded UNDERNEATH the felt, inside his body and inside each ear, and they light him FROM WITHIN. wool is translucent when backlit, so the light BLOOMS THROUGH the fibers: his chest and belly glow like a paper lantern, individual fibers over each LED catch fire with light and read as a soft bright halo, and the wool immediately around each LED goes warm and semi-transparent while the wool further away stays matted and opaque. you can see the discrete POINT of each LED as a hot core inside a soft glowing patch — never a flat even wash, never an overlay. +· HIS EARS LIGHT UP — each ear has a run of LEDs inside it, so when an ear swings past a dark background the whole ear glows through from the inside, the wool going translucent pink-white with the fiber structure visible against the light, like a hand held over a torch. +· THE GLASSES CATCH IT — the red felt rims pick up the internal glow from below and read hot red. +· THE TETHER — a slim felted battery pack sits at the small of his back, and a colored wire tail trails off him across the floor to the speaker stack: he is PLUGGED IN to the sound system, part of the circuit.`; + +// The room: @jeffrey's old Ashland A-frame, felted. const WORLD = -`THE ROOM — one continuous hand-felted room, roughly a 40 cm diorama, that TRANSFORMS with the music across the story: it begins as a hushed little wool parlor (a worn felt rag-rug, a stitched upright piano against the back wall, a dim wool lamp) and ends as a full JAMAICAN SOUND-SYSTEM DANCE — a leaning stack of enormous hand-felted speaker cabinets, wool cones bulging, tangled yarn cables across the floor. the SAME room throughout: the same rug, the same back wall, the same little lamp, progressively taken over by the speaker stack. everything is wool: the speakers, the cables, the light itself feels felted.`; +`THE ROOM — a needle-felted miniature of an A-FRAME HOUSE INTERIOR (modeled on the reference photo sheet): steep TRIANGULAR A-frame geometry is the defining shape of every wide shot — massive exposed timber rafters felted in dark walnut wool running up and meeting in a peak overhead, the sloped ceiling planes closing in on both sides, honey-and-amber WOOD PLANK walls and a wood plank FLOOR felted in warm ochre wool with visible board seams, and a big window set into the gable end. a round green wool rug sits on the floor. the SAME room throughout the film — same rafters, same plank floor, same gable window — progressively taken over by the sound system. +THE SOUND SYSTEM GROWS — hand-felted speaker cabinets accumulate along the walls and up under the rafters until they are a leaning tower; thick colored yarn CABLES snake everywhere across the plank floor and up into the beams, plugging into him.`; const LIGHT = -`LIGHT — diegetic and physically motivated, always. act one is warm dim lamp-light on wool. after the first drop, the room gains a real hand-felted lighting rig: a few small warm bulbs and one cool blue-green wool-diffused lamp, casting soft double shadows across the fibers. the dancehall section is lit warmer and lower — amber and deep green, the way a bass-heavy room feels. NO neon, NO glow filters, NO laser beams, NO light shafts drawn as objects.`; +`LIGHT — this is a LIGHT SHOW and it is all PHYSICALLY BUILT, never a filter. The A-frame rafters are strung with practical lighting the way a real party rigs a room: +· NEON — actual bent glass NEON TUBING and glowing EL-WIRE runs along the rafters and down the sloped ceiling planes, zip-tied to the beams with felted brackets, saturated magenta, cyan, acid-green and hot orange. the tubes are OBJECTS in the room with visible glass ends and wire leads — they light the wood, throw colored pools onto the plank floor, and put hard colored rims on the bunny's fuzz. +· BLASTING ON THE BEAT — the rig is mid-BLAST in the dynamic beats: several tubes at full blaze at once, colored light hitting the rafters hard, hot spill across the floor, deep saturated color everywhere and the wood glowing in it. in the quiet beats most tubes are dark or dimmed to a low ember and only his internal LEDs and one or two tubes are lit. +· HIS INTERNAL GLOW IS ALWAYS PART OF THE LIGHTING — the light coming out through his wool actually illuminates what is near him: the rug under his feet, his own paws, the wool he touches. +· contrast is high and the room is DARK between the lights — deep shadow in the rafter peak, hot saturated color below. every light source is a real fixture visible in frame or just outside it.`; const PALETTE = -`PALETTE — off-white and soft-grey wool bunny; round red glasses; oatmeal and dusty-rose rag-rug; warm brown felted wood; charcoal-grey speaker cabinets with oatmeal cones; brass-yellow bulb light; one cool teal accent lamp; amber and deep-green dancehall wash. warm, soft, matted, unsaturated — the colors of dyed wool, never of plastic.`; +`PALETTE — off-white and soft-grey wool bunny lit from inside in warm white and amber; round red glasses; copper and enamel-green wire traces; honey, ochre and walnut felted wood; charcoal speaker cabinets with oatmeal cones; and the neon rig in saturated magenta, cyan, acid-green and hot orange. the WOOL stays matted and real — the saturation lives in the light, not in a filter over the image.`; const AVOID = -`AVOID — any 3d-render, clay, plastic, or smooth plush-toy look (this is real matted needle-felted wool); ANY human person anywhere (the bunny is the only character); a second bunny, a mirror, or any doubled figure; only one ear on the bunny; empty red glasses rims; any readable text, wordmark, signage, band name, or logo of any kind; any screen, phone, tablet, laptop, or DJ controller with a display; neon, glow filters, lens flares, laser beams, or light shafts drawn as solid objects; motion-blur streaks or drawn speed-lines; the character looking at or acknowledging the camera; any real-world brand.`; +`AVOID — any 3d-render, clay, plastic, or smooth plush-toy look (this is real matted needle-felted wool, and the light is real fixtures); ANY human person anywhere (the bunny is the only character); a second bunny, a mirror, or any doubled figure; MORE THAN TWO EARS on the bunny (no third ear, no doubled or echoed ear); empty red glasses rims; any readable text, wordmark, signage, band name, or logo of any kind; any post-processing look — no bloom filter over the whole frame, no lens flares, no laser beams as solid cones, no light shafts drawn as objects, no glowing outline traced around the character; motion-blur streaks or drawn speed-lines; the character looking at or acknowledging the camera; any real-world brand.`; + +const POV_NOTE = +`POV INTEGRITY — this is the bunny's OWN first-person viewpoint: his eyes ARE the camera. The only parts of him in frame are his own front PAWS and forearms in the near foreground, seen from behind as his own hands would be. ABSOLUTELY NO second bunny, no mirror, no reflection, no face — his head is never visible because the camera is inside it.`; const FRAME_NOTE = `${FRAMING_YT_LANDSCAPE} @@ -100,64 +118,72 @@ HONOR THE SHOT DIRECTIVE in this beat exactly: a WIDE shot lets the whole room b // Names + order MUST match pop/maytrax/out/femrag-plusplus.struct.json. const BEATS = [ { + // Not a track section — the character/room ANCHOR every other panel is + // generated against. Establishes the electric felt look in full. name: "runway", faceVisible: true, scene: -`WIDE, low and quiet. The felt bunny stands alone in the middle of the worn oatmeal rag-rug in the dim wool parlor, seen three-quarters from the front. He has just started to move — weight settled onto one hind paw, the other heel lifted a few millimeters off the rug, one front paw curled loosely at his chest. His two long ears hang heavy and straight down, only just beginning to sway. Behind him the stitched felt upright piano waits against the wall and the little wool lamp glows warm. Nothing else in the room has moved yet. The moment before a dance.`, - }, - { - name: "groove", faceVisible: true, - scene: -`WIDE. The bunny has found the groove — a small contained two-step shuffle on the rug, hips turned slightly, one hind paw planted and the other sliding a few centimeters across the wool, both front paws up at rib height in a loose easy bounce. Both long ears now swing in a slow matched arc out to one side, lifted maybe thirty degrees off vertical. Bead eyes half-lidded, content. The rug has begun to rumple under his planted paw. Same parlor, same lamp — but a first small felted speaker cabinet has appeared beside the piano, its oatmeal wool cone facing him.`, +`WIDE, three-quarters from the front, the establishing frame of the film. The felt bunny stands on the round green wool rug on the plank floor of the dim A-frame room, the great walnut rafters rising and meeting in a peak above him and the gable window a dark blue rectangle behind. He is powered up and standing still: the copper circuit traces felted across his chest and limbs clearly visible, the LEDs inside his body glowing warm white-amber THROUGH his wool so his chest and belly read as a soft lantern and his two long ears glow translucent pink-white from the inside where they hang. His slim felted battery pack sits at the small of his back and its colored wire tail trails away across the plank floor. One neon tube along the near rafter is lit a low magenta; everything else in the room is dark. His red glasses catch the glow from below. Two ears, hanging heavy. Quiet, powered, waiting.`, }, - { - name: "buildup1", faceVisible: true, - scene: -`CLOSE-UP on the bunny from the chest up, filling the frame — individual wool fibers and needle pokes clearly resolved. He is winding up: chin tucked, shoulders drawn in tight, both front paws pulled to his chest, coiled. His two long ears are swept back behind his head and held there under tension, straining, at their tautest of the whole story — the frame just before release. Bead eyes wide inside the red lenses. Behind him the room is dark and out of focus except a single hot rim of bulb light along his fuzz. Every fiber on his silhouette lifts as a soft halo.`, - }, - { - name: "drop1a", faceVisible: true, + { + // The film OPENS here — the track starts cold on the drop. + name: "drop1a", faceVisible: false, pov: true, scene: -`WIDE, LOW-ANGLE from down on the rug looking up. THE DROP — the bunny is airborne in a full shakeout: both hind paws off the rug by several centimeters, body twisted mid-turn, front paws flung wide and open. His two long ears fly straight out sideways at full extension, horizontal, one to each side. The rag-rug is visibly rucked and kicked up behind him where he launched. The room has torn open into the sound system: three hand-felted speaker cabinets now stacked crookedly against the back wall, wool cones bulging outward, yarn cables snaking across the floor around his feet. Warm bulbs and one teal wool lamp throw soft double shadows.`, +`FIRST-PERSON POV, and this is the FIRST SHOT OF THE FILM — the track opens cold on the drop, so this frame is already at full power. The camera is the bunny's own eyes, looking DOWN at the little felted LAPTOP open on the green wool rug in front of him. His own two off-white furry FOREPAWS reach into frame from the bottom, seen from behind as his own hands, both down on the laptop's felted keyboard mid-strike — he is HAMMERING the beat out. The laptop is entirely needle-felted: a soft charcoal-wool clamshell, a keyboard of tiny pale felted keycaps, and a SCREEN that is a flat panel of glowing felt showing only soft abstract blocks and bars of colored light (magenta, cyan, amber) in even rows — a music grid read purely as colored wool light, with NO readable text, numbers or letters anywhere. The screen blazes and throws hard colored light up onto his paws. The copper circuit traces felted into his forearms catch it, and the LEDs under the wool of his arms are at FULL BLAST, glowing hot from inside so his own forearms are lanterns. Beyond the laptop: the plank floor, the speaker tower, and the dark A-frame rafters overhead with the whole neon rig BLASTING ON — magenta, cyan and acid-green tubes at once, colored light raking the beams and pooling on the boards.`, }, { name: "drop1b", faceVisible: true, scene: -`CLOSE-UP, filling the frame — the bunny's head and shoulders in full motion, single fibers and matted clumps resolving. He is deep in it: head thrown back, mouth-line open in delight, bead eyes squeezed into happy closed lash-lines centered inside their red lenses. Both long ears are mid-whip, caught in a wide S-curve as they swing past his head, wool wisps lifting off them. Sweat is not a thing here — but loose fiber is: a scatter of tiny wool wisps floats free in the bulb light around him. Behind, out of focus, the charcoal wall of speaker cabinets.`, +`CLOSE-UP, filling the frame — the bunny's head and shoulders in full motion, single fibers and matted clumps resolving. Head thrown back, mouth-line open in delight, bead eyes squeezed into happy closed lash-lines centered inside their red lenses. His TWO long ears are mid-whip in a wide S-curve past his head — exactly two ears, each a separate glowing translucent shape with its internal LEDs visible as bright cores through the wool, fiber structure lit up against the light. The felted copper traces on his shoulders flare where cyan neon rakes across them. Loose wool wisps float free, catching magenta light. Behind, out of focus, the dark rafters and the blazing tubes.`, }, { name: "breakdown", faceVisible: true, scene: -`WIDE and still. The breakdown — the bunny has dropped into a slow heavy half-time sway, standing near the largest felted speaker cabinet with one front paw laid flat against its oatmeal wool cone, feeling the bass through his paw. Body low, knees bent, rocking. His two long ears hang low and heavy again, swinging in a slow lazy pendulum, out of phase with each other. The lighting has cooled: the teal wool lamp dominates, the warm bulbs are down to embers. The rug is thoroughly rumpled. Quiet, enormous, patient.`, +`WIDE and still, the room at its darkest. The bunny stands at the largest felted speaker cabinet with one front paw laid flat against its oatmeal wool cone, feeling the bass through his paw. Body low, knees bent, rocking. His TWO long ears hang low and heavy, swinging in a slow lazy pendulum. Almost every neon tube is DARK now — only one cyan tube along a far rafter still burns low, and the dominant light in the entire room is the glow coming OUT of the bunny himself: his chest lantern throws warm light across the speaker cone he is touching, up onto his own chin, and down onto the plank floor around his feet, his ears two soft glowing shapes in the dark. The A-frame rafters recede into blackness above. Enormous, quiet, patient.`, }, { name: "buildup2", faceVisible: true, scene: -`CLOSE-UP, tight on the bunny's face and upper body, filling the frame. The second wind-up, steeper than the first: he is crouched right down with his chin nearly at his own knees, both front paws pressed to the rug, every muscle of the little wool body gathered. His two long ears are pinned flat back along his spine, held under maximum tension. One bead eye visible inside its red lens, fixed and wide. A single hot bulb rim-lights his whole fuzzy outline against near-blackness. The most compressed frame in the film.`, +`CLOSE-UP, tight on the bunny's face and upper body, filling the frame. The second wind-up, steeper than the first: crouched all the way down with his chin nearly at his own knees, both front paws pressed to the plank floor, every muscle of the little wool body gathered. His TWO long ears are pinned flat back along his spine and held under maximum tension — two ears, both glowing hard from within. The LEDs inside him are pulsing brighter and faster than anywhere in the film, the wool over his chest nearly white-hot and translucent, individual fibers blazing, light spilling onto the boards under his paws. One bead eye visible inside its red lens, fixed and wide. The rest of the A-frame is black. The most compressed frame in the film.`, }, { name: "drop2a", faceVisible: true, scene: -`WIDE, LOW-ANGLE. THE SECOND DROP — bigger than the first. The bunny is at the very top of a leap, higher off the rug than before, body arched back, all four paws flung out in a star, tail puff visible. His two long ears whip up and back in matching arcs above his head. The rag-rug is kicked into a fold beneath him and a small cloud of loose wool wisps hangs in the air where he launched. The speaker stack has grown to a leaning wall of five cabinets, cones bulging, cables everywhere. Warm bulbs and teal lamp at full strength, hard double shadows across the fibers.`, +`WIDE, LOW-ANGLE into the A-frame peak. THE SECOND DROP — bigger than the first. The bunny is at the very top of a leap, higher than before, body arched back, all four paws flung out in a star, tail puff visible. His TWO long ears — exactly two — whip up and back above his head, both blazing translucent. He is at maximum power: his whole body glowing, throwing colored light onto the rug below. The neon rig is at FULL BLAST and there is more of it now — magenta, cyan, acid-green and hot orange tubes running the length of every rafter and down both sloped ceiling planes, the speaker tower stacked up under the beams behind him, hard saturated light everywhere, wool wisps hanging in the colored air where he launched.`, }, { name: "drop2b", faceVisible: true, scene: -`CLOSE-UP on the bunny's hind paws and the rug, filling the frame — this is the only beat where his face is not the subject. Wool fibers at maximum resolution: the matted pile of the rag-rug, individual dyed strands, the soft-grey felted pads of his hind paws mid-stamp, one paw planted deep enough to compress the rug's pile, the other lifting away with wisps trailing. A yarn cable curls past. Above and out of focus, the blurred oatmeal wool of his body still in motion.`, +`EXTREME CLOSE-UP on the bunny's hind paws and the plank floor, filling the frame — the only beat where his face is not the subject. Wool fibers at maximum resolution: the soft-grey felted pads of his hind paws mid-stamp, one paw planted and compressing the wool of the green rug, the other lifting away with wisps trailing. Right through the frame run the felted-in COPPER CIRCUIT TRACES on his ankles and a thick colored yarn cable snaking across the boards, and an LED under the wool of his lower leg glows hot through the fibers, throwing light onto the plank floor. Magenta and cyan neon rake across the wood from above. Above and out of focus, the blurred glowing mass of his body still moving.`, }, { name: "ragga-a", faceVisible: true, scene: -`WIDE. THE DANCEHALL — the room has fully become a Jamaican sound-system dance. The stack is now enormous: seven or eight hand-felted speaker cabinets leaning in a crooked tower to the ceiling, huge oatmeal wool cones, thick yarn cables looped across the floor. The lighting has gone warm and low — deep amber and forest green. The bunny dances in the classic skanking posture: leaning forward, weight low, one shoulder dropped, both front paws swinging loosely across his body, one hind paw kicked back. His two long ears swing in a lazy, deliberately behind-the-beat arc — loose and cool, not frantic. He is having the time of his life.`, +`WIDE. THE DANCEHALL — the A-frame is now a full sound-system dance. Felted speaker cabinets are stacked in a leaning tower up under the rafters, huge oatmeal wool cones, thick yarn cables looped across the plank floor and up into the beams. The neon has gone warm and heavy for this section: deep amber and forest-green tubes along the rafters, one hot orange run down the sloped ceiling, the wood glowing warm in it. The bunny skanks: leaning forward, weight low, one shoulder dropped, both front paws swinging loosely across his body, one hind paw kicked back. His TWO long ears swing in a lazy behind-the-beat arc, both glowing from within. His internal light has gone warm gold here, and his wire tail runs from his back across the floor into the base of the stack — plugged in.`, }, { name: "ragga-b", faceVisible: true, scene: -`CLOSE-UP, low, filling the frame — the bunny and the mouth of the biggest felted speaker cone behind him. He has his back half-turned to the cabinet, leaning into it, one long ear flung forward across his own face by the swing and the other trailing behind, bead eyes happy inside their red lenses. The enormous oatmeal wool cone behind him is visibly pushed outward at its center — the wool physically bulging with a bass note — and loose fibers around its rim lift away from the surface. Deep amber light, forest-green shadows in the fuzz.`, +`CLOSE-UP, low — the bunny and the mouth of the biggest felted speaker cone behind him. He has his back half-turned to the cabinet, leaning into it, ONE long ear flung forward across his own face by the swing and the OTHER trailing behind — exactly two ears, both lit from the inside and translucent against the dark cone. Bead eyes happy inside their red lenses. The enormous oatmeal wool cone behind him is visibly pushed outward at its center — the wool physically bulging with a bass note — and loose fibers around its rim lift away from the surface. Amber and forest-green neon rakes across the scene from the rafters above; his own gold internal glow lights the cone's wool where he leans on it.`, + }, + { + name: "ragga-breathe", faceVisible: true, + scene: +`WIDE, the dancehall taking a breath. The neon rig has dropped almost all the way out — only one deep-amber tube along a far rafter still burns low — and the room falls back to the bunny's own light. He keeps skanking but small and easy, weight low, one shoulder dropped, both front paws swinging gently across his body. His TWO long ears swing in slow behind-the-beat arcs, glowing warm gold from the LEDs inside them, and the light coming out of his chest is now the brightest thing in the A-frame, throwing real warm light onto the green rug, the plank boards, and the base of the speaker tower beside him. The great rafters recede into darkness above. His wire tail runs across the floor into the stack. Held breath, all glow, no blast.`, + }, + { + name: "ragga-push", faceVisible: true, + scene: +`WIDE, the room coming back. The neon is switching back ON across the rafters — amber, forest-green and hot orange tubes lighting one after another so the A-frame reads half-lit and building, hard color returning to the walnut beams and spilling down the sloped ceiling planes onto the plank floor. The bunny has dug in: deeper skank, weight dropped lower, one hind paw kicked further back, both front paws swinging in bigger diagonals across his body. His TWO long ears swing in wide committed arcs, both blazing translucent from within. His internal LEDs have pushed from warm gold up to near-white, the wool of his chest hot and semi-transparent. The speaker tower looms lit behind him, cables everywhere.`, + }, + { + name: "ragga-push-b", faceVisible: true, + scene: +`WIDE, LOW-ANGLE from the plank floor looking up into the A-frame peak — the biggest dancehall frame in the film. Every neon tube in the rafters is at FULL BLAST at once: magenta, cyan, acid-green, amber and hot orange running the length of every beam and down both sloped ceiling planes, the whole triangular geometry of the room screaming with saturated color, hard light raking the walnut and pooling across the boards. The bunny is mid-skank at maximum power, body low and turned, one hind paw kicked back, both front paws thrown wide, his TWO long ears — exactly two — whipping out in big arcs, each one blazing translucent. His whole body is a lantern at full output, throwing colored light down onto the rug. The speaker tower rises the full height of the frame beside him, cones bulging, yarn cables everywhere, his wire tail plugged into its base. Loose wool wisps hang lit in the colored air.`, }, { name: "outro", faceVisible: true, scene: -`WIDE and warm. The end. The bunny has stopped dancing and stands still at the center of the thoroughly wrecked rug, breathing, one front paw resting on his own chest. His two long ears hang all the way down, completely spent, wool visibly fluffed and disheveled all over him — he has genuinely shaken his fuzz out. Loose wisps have settled across the rug like snow. The speaker stack looms quiet and dark behind him, cones at rest. Only the little original wool lamp is still lit, back to its warm act-one glow. The same parlor, after.`, +`WIDE and warm, the A-frame at rest. The bunny has stopped dancing and stands still at the center of the green rug on the plank floor, breathing, one front paw resting on his own chest. His TWO long ears hang all the way down, completely spent, and their internal light has faded to a last dim ember. His wool is visibly fluffed and disheveled all over — he has genuinely shaken his fuzz out — and loose wisps have settled across the rug and boards around him. Every neon tube in the rafters is DARK except one, a low magenta run along the near beam. The LEDs inside his chest have dimmed to a slow soft pulse, the only real light left, glowing gently through the wool. The speaker tower looms quiet behind him. The great rafters rise into the dark. After.`, }, ]; @@ -168,6 +194,10 @@ if (flags.list) { const panelPath = (i, name) => `${OUT}/${SLUG}-yt-sec-${i}-${name}.png`; const ANCHOR = panelPath(0, "runway"); +// @jeffrey's old Ashland A-frame — the room is modeled on this contact sheet +// (exposed rafters meeting in a peak, honey plank walls and floor, gable +// window, round green rug, string lights already strung along the beams). +const ASHLAND = resolve(REPO, "pop/nullabye/reel/special-sign/refs/ashland-house-platter-contact.jpg"); function build(beat) { return [ @@ -175,12 +205,14 @@ function build(beat) { NEEDLE_FELT_WOOL, FRAME_NOTE, BUNNY, + ELECTRIC, WORLD, LIGHT, PALETTE, + beat.pov ? POV_NOTE : null, `THIS BEAT — ${beat.scene}`, AVOID, - ].join("\n\n"); + ].filter(Boolean).join("\n\n"); } const apiKey = loadOpenAIKey(); @@ -200,7 +232,9 @@ async function genOnce(promptText, refs, useEdit, label) { fd.append("n", "1"); for (const ref of refs) { const buf = readFileSync(ref); - fd.append("image[]", new Blob([buf], { type: "image/png" }), basename(ref)); + const lower = ref.toLowerCase(); + const ext = lower.endsWith(".png") ? "png" : lower.endsWith(".webp") ? "webp" : "jpeg"; + fd.append("image[]", new Blob([buf], { type: `image/${ext}` }), basename(ref)); } res = await fetch("https://api.openai.com/v1/images/edits", { method: "POST", headers: { Authorization: `Bearer ${apiKey}` }, body: fd, @@ -247,12 +281,12 @@ async function qaPanel(b64) { const ask = `You are checking one frame of a needle-felt stop-motion music video for character errors. Answer STRICTLY as JSON: {"pass":true|false,"reason":"..."}. FAIL if any of these are true: -1. The rabbit has only ONE visible long ear when the pose would show two (both ears must be present somewhere in frame). +1. EAR COUNT IS NOT TWO. Count every long rabbit ear in the frame, including any that is blurred, flying, or partly out of frame. The count must be EXACTLY TWO. Three or four ears — including a duplicated, echoed, or split ear trailing behind a moving one — is a FAIL. Only ONE ear visible when the pose would show both is also a FAIL. 2. Any red glasses lens is EMPTY — an eye sitting above/outside its rim while a red circle sits elsewhere on the face. Each visible eye must be centered INSIDE a red lens. 3. More than TWO eyes read on the face (an eye plus two empty rims counts as this failure). 4. There is MORE THAN ONE rabbit in the frame, or a human person appears. -5. There is readable text, a wordmark, or a logo anywhere. -Otherwise PASS. If the frame is a close-up of paws/rug where no face is shown, PASS (rules 1-3 do not apply).`; +5. There is readable text, a wordmark, letters, or numbers anywhere — including on any screen in frame. +Otherwise PASS. If the frame is a close-up of paws/floor, or a first-person POV showing only the rabbit's own forepaws with no head visible, then rules 1-3 do not apply — PASS unless rule 4 or 5 is broken.`; try { const r = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", @@ -293,15 +327,18 @@ async function generate(beat, i) { const outPath = panelPath(i, beat.name); const rel = outPath.replace(REPO + "/", ""); if (existsSync(outPath) && !FORCE) { console.log(`✓ cached → ${rel}`); return true; } - // Every beat after the anchor refs the anchor so it is the same bunny. - const useEdit = i > 0 && existsSync(ANCHOR); - const refs = useEdit ? [ANCHOR] : []; - const attempts = beat.faceVisible ? QA_ATTEMPTS : 1; - console.log(`▸ ${i} ${beat.name} · ${SIZE} · ${useEdit ? "anchor ref" : "no refs"}${beat.faceVisible ? " · QA" : ""}`); + // The Ashland sheet carries the ROOM (A-frame geometry, plank wood, gable + // window); the anchor carries the BUNNY. The anchor itself is generated + // against Ashland alone, since it is what defines the character. + const refs = [existsSync(ASHLAND) ? ASHLAND : null, i > 0 && existsSync(ANCHOR) ? ANCHOR : null].filter(Boolean); + const useEdit = refs.length > 0; + const needsQA = beat.faceVisible || beat.pov; // POV still gets the no-text check + const attempts = needsQA ? QA_ATTEMPTS : 1; + console.log(`▸ ${i} ${beat.name} · ${SIZE} · ${useEdit ? refs.length + " refs" : "no refs"}${needsQA ? " · QA" : ""}${beat.pov ? " · POV" : ""}`); for (let qa = 1; qa <= attempts; qa++) { const b64 = await genOnce(build(beat), refs, useEdit, beat.name); if (!b64) return false; - if (beat.faceVisible) { + if (needsQA) { const verdict = await qaPanel(b64); if (!verdict.pass) { writeFileSync(outPath, Buffer.from(b64, "base64")); -- 2.51.2 From d6da2d768b10a4f87643962d9c25d1b676da30c7 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 13 Aug 2026 17:57:12 -0400 Subject: [PATCH 4/6] =?UTF-8?q?pop/maytrax:=20beat-lights=20=E2=80=94=20dr?= =?UTF-8?q?ive=20the=20neon=20rig=20from=20the=20score=20itself?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The renderer already writes every hit it plays to .events.json. This reads that feed and paints a light-overlay video: colored pools blasting in from the frame edges the way the felted neon tubes in the rafters would if they were wired to the mixer. Kick lights amber from below, snare magenta from the sides, donk green, sub blue, hats cyan off the top, risers wash the whole frame. It is exactly in time because it reads the same event list the audio was rendered from — there is no beat detection involved and nothing to drift. Locally generated, no model, no API, no cost. Rendered at 320x180 and upscaled with a blur (the light is all soft gradients, so it costs nothing to paint small) and streamed to ffmpeg as raw video, so nothing large touches the disk. Events are bucketed per frame up front and the per-pixel falloff fields are precomputed, leaving the inner loop as an envelope multiply. Co-Authored-By: Claude Opus 5 (1M context) --- pop/maytrax/bin/beat-lights.mjs | 188 ++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 pop/maytrax/bin/beat-lights.mjs diff --git a/pop/maytrax/bin/beat-lights.mjs b/pop/maytrax/bin/beat-lights.mjs new file mode 100644 index 0000000000..a141da5f9e --- /dev/null +++ b/pop/maytrax/bin/beat-lights.mjs @@ -0,0 +1,188 @@ +#!/usr/bin/env node +// beat-lights.mjs — the neon rig, driven by the score itself. +// +// render-femrag-plusplus.mjs writes every musical hit it plays to +// out/.events.json (kick, snare, hat, bell, sub, donk, riser, with +// exact times and gains). This reads that feed and paints a light-overlay +// video: colored pools that BLAST from the frame edges the way the felted +// neon tubes in the rafters would if they were wired to the mixer. +// +// It is locally generated — no model, no API, no cost — and it is exactly +// in time because it is reading the same event list the audio was rendered +// from, not a beat-detector's guess about it. +// +// The overlay is rendered small (the light is all soft gradients, so it +// upscales cleanly) and streamed to ffmpeg as raw video, then screen-blended +// over the cut. Nothing large ever touches the disk. +// +// node pop/maytrax/bin/beat-lights.mjs # overlay only +// node pop/maytrax/bin/beat-lights.mjs --over # + composite +// node pop/maytrax/bin/beat-lights.mjs --opacity .5 + +import { existsSync, readFileSync } from "node:fs"; +import { spawn, spawnSync } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const LANE = resolve(HERE, ".."); +const OUT = `${LANE}/out`; +const SLUG = "femrag-plusplus"; + +const flags = {}; +for (let i = 2; i < process.argv.length; i++) { + const a = process.argv[i]; + if (!a.startsWith("--")) continue; + const next = process.argv[i + 1]; + if (next === undefined || next.startsWith("--")) flags[a.slice(2)] = true; + else { flags[a.slice(2)] = next; i++; } +} + +const FPS = 24; +const W = 320, H = 180; // overlay resolution — soft light upscales fine +const OPACITY = Number(flags.opacity ?? .55); + +const eventsPath = `${OUT}/${SLUG}.events.json`; +if (!existsSync(eventsPath)) { + console.error(`✗ no event feed at ${eventsPath} — run the renderer first`); + process.exit(1); +} +const feed = JSON.parse(readFileSync(eventsPath, "utf8")); +const DUR = feed.seconds; +const FRAMES = Math.ceil(DUR * FPS); + +// ── the rig ──────────────────────────────────────────────────────────────── +// Each instrument owns a lamp: a color, a place in the frame, and a decay. +// `spread` is how far the pool reaches from its anchor (in frame heights), +// `decay` how many seconds the flash takes to fall away. +const LAMPS = { + boom: { rgb: [255, 150, 40], from: "bottom", spread: .95, decay: .20, gain: 5.2 }, + snare: { rgb: [255, 60, 190], from: "sides", spread: .85, decay: .16, gain: 5.0 }, + donk: { rgb: [120, 255, 90], from: "left", spread: .90, decay: .13, gain: 5.6 }, + sub: { rgb: [ 90, 120, 255], from: "bottom", spread: 1.1, decay: .30, gain: 2.0 }, + hat: { rgb: [140, 235, 255], from: "top", spread: .55, decay: .07, gain: 3.0 }, + bell: { rgb: [255, 225, 150], from: "top", spread: .45, decay: .09, gain: 1.1 }, + riser: { rgb: [255, 255, 255], from: "all", spread: 1.3, decay: .50, gain: 1.6 }, +}; + +// Bucket events into frames so each frame only walks its own recent hits. +// A hit can still light frames after it (its decay tail), so each event is +// filed into every frame its tail reaches. +const perFrame = Array.from({ length: FRAMES }, () => []); +let placed = 0; +for (const e of feed.events) { + const lamp = LAMPS[e.i]; + if (!lamp) continue; + // A riser lights its whole duration, not just its onset. + const span = e.i === "riser" ? (e.dur || 0) : 0; + const start = Math.floor(e.t * FPS); + const end = Math.ceil((e.t + span + lamp.decay * 3) * FPS); + for (let f = Math.max(0, start); f < Math.min(FRAMES, end); f++) { + perFrame[f].push(e); + } + placed++; +} + +// Precompute each pixel's distance from each anchor, so the per-frame work is +// just an envelope multiply rather than a distance calculation. +const FIELD = {}; +for (const key of new Set(Object.values(LAMPS).map((l) => l.from))) { + const f = new Float32Array(W * H); + for (let y = 0; y < H; y++) { + for (let x = 0; x < W; x++) { + const u = x / (W - 1), v = y / (H - 1); + let d; + switch (key) { + case "bottom": d = 1 - v; break; + case "top": d = v; break; + case "left": d = u; break; + case "sides": d = Math.min(u, 1 - u) * 2; break; + default: d = Math.min(Math.min(u, 1 - u), Math.min(v, 1 - v)) * 2; break; + } + f[y * W + x] = d; + } + } + FIELD[key] = f; +} + +const frame = Buffer.alloc(W * H * 3); +const acc = new Float32Array(W * H * 3); + +function renderFrame(fi) { + acc.fill(0); + const t = fi / FPS; + for (const e of perFrame[fi]) { + const lamp = LAMPS[e.i]; + const span = e.i === "riser" ? (e.dur || 0) : 0; + const since = t - e.t; + if (since < 0) continue; + // Inside a sustained event the envelope holds at 1; after it, it decays. + const past = Math.max(0, since - span); + const env = Math.exp(-past / lamp.decay); + const amp = env * (e.gain ?? .1) * lamp.gain; + if (amp < .004) continue; + const field = FIELD[lamp.from]; + const [r, g, b] = lamp.rgb; + for (let p = 0, n = W * H; p < n; p++) { + // Light falls off with distance from the lamp's edge. + const fall = Math.max(0, 1 - field[p] / lamp.spread); + const k = fall * fall * amp; + if (k < .002) continue; + const o = p * 3; + acc[o] += r * k; acc[o + 1] += g * k; acc[o + 2] += b * k; + } + } + for (let i = 0, n = W * H * 3; i < n; i++) { + const v = acc[i]; + frame[i] = v >= 255 ? 255 : v <= 0 ? 0 : v; // implicit floor + } + return frame; +} + +const overlayPath = `${OUT}/${SLUG}-beat-lights.mp4`; +console.log(`▸ beat-lights · ${FRAMES} frames · ${DUR.toFixed(1)}s · ${placed} events wired`); + +const ff = spawn("ffmpeg", [ + "-hide_banner", "-loglevel", "error", "-y", + "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", `${W}x${H}`, "-r", String(FPS), "-i", "pipe:0", + "-vf", "scale=1280:720:flags=bicubic,gblur=sigma=6", + "-c:v", "libx264", "-preset", "veryfast", "-crf", "20", "-pix_fmt", "yuv420p", + overlayPath, +], { stdio: ["pipe", "inherit", "inherit"] }); + +let done; +const finished = new Promise((r) => { done = r; }); +ff.on("close", (code) => done(code)); + +for (let f = 0; f < FRAMES; f++) { + if (!ff.stdin.write(Buffer.from(renderFrame(f)))) { + await new Promise((r) => ff.stdin.once("drain", r)); + } + if (f % 480 === 0) process.stdout.write(`\r ${Math.round(f / FRAMES * 100)}% `); +} +ff.stdin.end(); +const code = await finished; +process.stdout.write("\r"); +if (code !== 0) { console.error("✗ overlay encode failed"); process.exit(1); } +console.log(`✓ ${overlayPath}`); + +// ── composite ────────────────────────────────────────────────────────────── +const over = flags.over === true ? `${OUT}/${SLUG}-shakeout-yt.mp4` : flags.over; +if (!over) { + console.log(` (pass --over to screen-blend it onto a cut)`); + process.exit(0); +} +if (!existsSync(over)) { console.error(`✗ no cut at ${over}`); process.exit(1); } +const litPath = over.replace(/\.mp4$/, "-lit.mp4"); +console.log(` compositing over ${over.split("/").pop()} at opacity ${OPACITY} …`); +const mix = spawnSync("ffmpeg", [ + "-hide_banner", "-loglevel", "error", "-y", + "-i", over, "-i", overlayPath, + "-filter_complex", + `[1:v]format=yuv420p[o];[0:v][o]blend=all_mode=screen:all_opacity=${OPACITY}[v]`, + "-map", "[v]", "-map", "0:a?", + "-c:v", "libx264", "-preset", "medium", "-crf", "18", "-pix_fmt", "yuv420p", + "-c:a", "copy", "-movflags", "+faststart", litPath, +], { stdio: ["ignore", "inherit", "inherit"] }); +if (mix.status !== 0) { console.error("✗ composite failed"); process.exit(1); } +console.log(`✓ ${litPath}`); -- 2.51.2 From 550c89650a9db5b8696d2b799b30dca26f42807b Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 13 Aug 2026 18:26:08 -0400 Subject: [PATCH 5/6] juke: stop the DJ platter from stranding the desktop, and give track names a floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The trackpad lock hands the system pointer to the platter via CGAssociateMouseAndMouseCursorPosition(0), freezing and hiding the cursor machine-wide. A bare double-click engaged it, and every route back out assumed the deck window was still key — so a misfire could strand the whole desktop. Engaging now needs option; releasing stays on a bare double-click. Escape works through global and local monitors, the lock drops when the app deactivates, and a watchdog releases it the moment our window stops being key. JukeLiquidSurface painted a 4-10% chassis tint over fully clear glass, leaving nothing behind the track list but the desktop. Adds a legibility substrate between the glass and the controls, following appearance. Co-Authored-By: Claude Fable 5 --- .../Sources/JukeWizard/DJPopoutDeck.swift | 61 ++++++++++++++++++- .../JukeWizard/JukeLiquidSurface.swift | 26 ++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/juke-wizard/Sources/JukeWizard/DJPopoutDeck.swift b/juke-wizard/Sources/JukeWizard/DJPopoutDeck.swift index 1bd8d9f5f4..beaf450698 100644 --- a/juke-wizard/Sources/JukeWizard/DJPopoutDeck.swift +++ b/juke-wizard/Sources/JukeWizard/DJPopoutDeck.swift @@ -182,6 +182,10 @@ final class DJRadialRecordView: NSView { private var multitouchTravel: Double = 0 private var trackpadLockActive = false private var cursorHiddenByLock = false + private var lockGlobalKeyMonitor: Any? + private var lockLocalKeyMonitor: Any? + private var lockResignObserver: NSObjectProtocol? + private var lockWatchdog: Timer? private var sparks: [Spark] = [] private var grooveTrails: [GrooveTrail] = [] private var lastEffectTime = ProcessInfo.processInfo.systemUptime @@ -603,8 +607,17 @@ final class DJRadialRecordView: NSView { return } if event.clickCount == 2 { - trackpadLockActive ? releaseTrackpadLock() : engageTrackpadLock() - return + // Releasing is always available on a bare double-click; ENGAGING + // needs ⌥ so an ordinary double-click on the platter can't hand + // the whole desktop's pointer over by accident. + if trackpadLockActive { + releaseTrackpadLock() + return + } + if event.modifierFlags.contains(.option) { + engageTrackpadLock() + return + } } window?.makeFirstResponder(self) NSCursor.closedHand.set() @@ -926,6 +939,13 @@ final class DJRadialRecordView: NSView { private var requiredTouchCount: Int { trackpadLockActive ? 1 : 2 } + // The lock disconnects the physical trackpad from the system pointer so + // the platter can be scratched like real vinyl. That is a whole-machine + // takeover: while it holds, the cursor is frozen and hidden EVERYWHERE, + // so every route back out has to survive Juke losing focus. Hence the + // belt and braces below — a global Escape monitor, an app-deactivation + // release, and a watchdog that lets go the moment our window stops being + // key. Without them a stray double-click stranded the whole desktop. private func engageTrackpadLock() { guard !trackpadLockActive else { return } trackpadLockActive = true @@ -935,17 +955,54 @@ final class DJRadialRecordView: NSView { NSCursor.hide() cursorHiddenByLock = true } + installLockEscapeHatches() DJFocusFlash.shared.flash(rising: true) DJFocusDing.shared.play(rising: true) needsDisplay = true } + private func installLockEscapeHatches() { + // Escape works even when the keystroke never reaches our responder + // chain (another app is frontmost, focus drifted, the deck window + // quietly resigned key). + lockGlobalKeyMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.keyDown]) { [weak self] event in + guard event.keyCode == 53 else { return } + DispatchQueue.main.async { self?.releaseTrackpadLock() } + } + lockLocalKeyMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { [weak self] event in + guard event.keyCode == 53, self?.trackpadLockActive == true else { return event } + self?.releaseTrackpadLock() + return nil + } + lockResignObserver = NotificationCenter.default.addObserver( + forName: NSApplication.didResignActiveNotification, object: nil, queue: .main + ) { [weak self] _ in self?.releaseTrackpadLock() } + // Last resort: if our window is no longer key, nothing can deliver a + // keystroke to us, so the lock has no way to be dismissed on purpose. + lockWatchdog = DJRunLoopTimer.scheduled(every: 0.5) { [weak self] _ in + guard let self, self.trackpadLockActive else { return } + if self.window?.isKeyWindow != true { self.releaseTrackpadLock() } + } + } + + private func removeLockEscapeHatches() { + if let monitor = lockGlobalKeyMonitor { NSEvent.removeMonitor(monitor) } + if let monitor = lockLocalKeyMonitor { NSEvent.removeMonitor(monitor) } + lockGlobalKeyMonitor = nil + lockLocalKeyMonitor = nil + if let observer = lockResignObserver { NotificationCenter.default.removeObserver(observer) } + lockResignObserver = nil + lockWatchdog?.invalidate() + lockWatchdog = nil + } + private func releaseTrackpadLock() { guard trackpadLockActive || cursorHiddenByLock else { return } let wasActive = trackpadLockActive if multitouchScratching || multitouchArmed { finishMultitouch() } touchPositions.removeAll() trackpadLockActive = false + removeLockEscapeHatches() CGAssociateMouseAndMouseCursorPosition(1) if cursorHiddenByLock { NSCursor.unhide() diff --git a/juke-wizard/Sources/JukeWizard/JukeLiquidSurface.swift b/juke-wizard/Sources/JukeWizard/JukeLiquidSurface.swift index 705024ee43..677e013f46 100644 --- a/juke-wizard/Sources/JukeWizard/JukeLiquidSurface.swift +++ b/juke-wizard/Sources/JukeWizard/JukeLiquidSurface.swift @@ -8,6 +8,13 @@ final class JukeLiquidSurface: NSView { } private let backdrop: NSView + /// A legibility floor between the glass and the controls. `.clear` glass + /// refracts the desktop at full strength, and the chassis tint on top of + /// it is only 4–10% opaque, so track names ended up competing with + /// whatever wallpaper or window sat behind Juke. This scrim is the one + /// surface guaranteeing text has something to sit on; it stays + /// translucent enough that the glass still reads as glass. + private let substrate = PassthroughTintView() private let tint = PassthroughTintView() override init(frame frameRect: NSRect) { @@ -36,16 +43,35 @@ final class JukeLiquidSurface: NSView { backdrop.autoresizingMask = [.width, .height] addSubview(backdrop) + substrate.wantsLayer = true + substrate.frame = bounds + substrate.autoresizingMask = [.width, .height] + addSubview(substrate) + tint.wantsLayer = true tint.frame = bounds tint.autoresizingMask = [.width, .height] addSubview(tint) + + applySubstrate() } required init?(coder: NSCoder) { fatalError() } override var isOpaque: Bool { false } + override func viewDidChangeEffectiveAppearance() { + super.viewDidChangeEffectiveAppearance() + applySubstrate() + } + + private func applySubstrate() { + let dark = effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua + substrate.layer?.backgroundColor = (dark + ? NSColor(srgbRed: 0.05, green: 0.07, blue: 0.08, alpha: 0.62) + : NSColor(srgbRed: 0.99, green: 0.99, blue: 1.00, alpha: 0.66)).cgColor + } + func setTint(_ color: NSColor) { tint.layer?.backgroundColor = color.cgColor } -- 2.51.2 From 8a4cdbdb8434dea2b9c14750122808b8f669b7f4 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 13 Aug 2026 18:37:43 -0400 Subject: [PATCH 6/6] =?UTF-8?q?pop/maytrax:=20SHAKEOUT=20v3=20=E2=80=94=20?= =?UTF-8?q?real=20interface,=20rainbow=20glow,=20perspective=20shifts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four notes from @jeffrey, all art-direction level: REAL SCREENS, NOT INVENTED ONES. The felted laptop was showing a made-up abstract note grid. It now generates against a genuine notepat/AC-native capture (staged as out/notepat-screen-ref.png) with a SCREEN_LAW that describes the actual interface — parameter bars, instrument tabs, the pixel character on its line, and two 4x3 banks of candy-pastel note tiles. The vision QA was rejecting any letters at all, which would have failed a real interface, so it now fails only readable WORDS, brands and logos; small illegible felt marks and single tile letters pass. RAINBOW MULTIGLOW. The LEDs were one warm color. Every LED is now its own hue — magenta, cyan, lime, orange, violet, gold — so each blooms through the wool as a separate colored patch and overlapping patches mix into a third. Both ears carry chains of different-colored LEDs. PROPS PERSIST. The laptop vanished after the opening shot. A continuity rule now keeps it open, lit and in place on the rug for the whole film, along with the amps, pedals and cables — once a prop is in the room it stays in the room. PERSPECTIVE SHIFTS. No two neighbouring beats share a camera now. Four named rigs: first-person POV (drop1a on the laptop, ragga-b at the speaker cone), a third-person Grand-Theft-Auto/Roblox chase cam floating behind and above (drop1b, ragga-a), outside-the-house looking in through the gable window (breakdown), and straight-down top-down plan views (drop2b, and the final shot rising away from him). The game-cam prompts explicitly forbid a HUD so it stays a photograph of felt rather than a screenshot. Motion prompts follow the same cameras, so the panel and its shot agree. Co-Authored-By: Claude Opus 5 (1M context) --- .../gen-motion-femrag-plusplus-shakeout.mjs | 12 +-- .../gen-sections-femrag-plusplus-shakeout.mjs | 70 +++++++++++---- pop/maytrax/bin/render-femrag-plusplus.mjs | 90 ++++++++++++------- 3 files changed, 117 insertions(+), 55 deletions(-) diff --git a/pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs b/pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs index 9eacad7a13..bf37b25332 100644 --- a/pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs +++ b/pop/maytrax/bin/gen-motion-femrag-plusplus-shakeout.mjs @@ -68,11 +68,11 @@ const SHOTS = { }, drop1b: { motion: -`Close on the bunny's head and shoulders, deep in the dance — head thrown back, bead eyes squeezed happy inside their red lenses, his two long ears mid-whip, carving wide S-curves past his head and doubling back with each beat. Loose wool wisps peel off the ears at the ends of their arcs and drift through the light. The camera handholds gently with him, staying tight — never pulling out to a wide.`, +`THIRD-PERSON VIDEO-GAME CHASE CAM, exactly like Grand Theft Auto or Roblox — the camera floats behind and slightly above the bunny and FOLLOWS him as he dances, holding him in the lower-middle of frame with the A-frame room opening out ahead. He is mid-shakeout on the rug: body twisting, front paws thrown wide, his TWO long ears whipping out to either side, the chains of colored LEDs inside them streaking as they swing. The rainbow light inside his body pulses with the beat and throws moving colored pools onto the rug beneath him. The camera drifts with him like a game camera catching up — smooth, trailing, always behind — never cutting to his face. NO HUD, no crosshair, no interface overlay of any kind: this is still a real photograph of felt.`, }, breakdown: { motion: -`The bunny drops into slow heavy half-time. He stands at the big felted speaker cabinet with one front paw laid flat on its oatmeal wool cone; the cone visibly pushes outward against his paw and relaxes, a slow physical pulse, and his whole body rocks with it — knees bending deep, weight rolling side to side. His two long ears hang low and swing in a lazy pendulum, the two ears out of phase with each other. The camera holds wide and almost still. Enormous and patient.`, +`FROM OUTSIDE, THROUGH THE GABLE WINDOW, holding almost still on the breakdown. The camera stays out in the cold night looking in through the felted panes, the dark mullions fixed across the frame. Inside, small and distant, the bunny rocks in slow heavy half-time with one paw on the big speaker cone; the cone visibly pushes outward against his paw and relaxes, a slow physical pulse, and his whole body rocks with it. His rainbow glow is the only real light moving in the house — separate colored blooms breathing through his wool, his two long ears hanging low and lit. The camera creeps almost imperceptibly closer to the glass across the shot. Enormous, patient, and heard from far away.`, }, buildup2: { morphTo: "drop2a", @@ -103,15 +103,15 @@ const SHOTS = { }, drop2b: { motion: -`Tight on the bunny's hind paws and the rug, filling the frame at maximum fiber resolution. His felted paws stamp the beat: one paw drives down and compresses the rug's matted pile, the other lifts away trailing wisps, then they trade — over and over, locked to the rhythm. A yarn cable on the floor jumps slightly with each stamp. Above and out of focus, the blurred oatmeal wool of his body keeps moving. The camera stays locked and close.`, +`TOP-DOWN, straight overhead, locked off. The round green rug holds as a circle in frame and the bunny stamps the beat directly below the camera — seen from above, both hind paws driving down onto the rug and lifting alternately in rhythm, body foreshortened, his TWO long ears splayed flat on the floor to either side and swinging in arcs across the boards with each stamp. The rainbow light inside him spills outward across the rug in overlapping colored pools that PULSE with every impact, and the yarn cables loop flat on the boards and jump slightly when he lands. The camera does not move at all — the whole shot is his motion under a fixed overhead eye.`, }, "ragga-a": { motion: -`The dancehall. The bunny's whole rhythm changes — looser, heavier, deliberately behind the beat. He skanks: leaning forward with his weight low, one shoulder dropped, both front paws swinging across his body in long lazy diagonals, one hind paw kicking back and returning. His two long ears swing in slow wide arcs that lag noticeably behind his body — cool, not frantic. Above him the huge felted speaker cones pulse outward in slow deep pushes, the wool physically bulging and relaxing. The camera drifts slowly in a wide arc around him. Warm amber and deep green.`, +`THIRD-PERSON GAME CHASE CAM again — behind and above him, following him through the dancehall as he skanks across the room. The camera trails him at a steady distance with the A-frame laid out ahead: the speaker tower rising on the right, the gable window at the far end, cables looping over the plank floor. He is seen from behind, leaning forward with his weight low, one shoulder dropped, both front paws swinging across his body, one hind paw kicking back, his TWO long ears swinging in lazy behind-the-beat arcs and glowing rainbow. The felted speaker cones pulse outward in slow deep pushes as he passes them. Smooth trailing game-camera motion, no HUD, no interface.`, }, "ragga-b": { motion: -`Close and low — the bunny leaning back against the mouth of the biggest felted speaker cone, half-turned to it, still skanking loose and easy. One long ear swings forward across his own face and away again with each bar; the other trails behind. Behind him the enormous oatmeal wool cone visibly pushes outward at its center on every bass note and settles back, and loose fibers around its rim lift away from the surface with each push. His bead eyes stay happy inside the red lenses. The camera holds close, swaying a little with him.`, +`FIRST PERSON at the speaker cone. The camera is the bunny's own eyes looking straight at the enormous felted cone filling the frame; his own two forepaws are pressed flat against its wool in the bottom of frame. The cone PUSHES OUTWARD against his paws on every bass note and settles back — a real physical pulse in the wool, loose fibers around the rim lifting and falling with it — and the rainbow LEDs under the wool of his own forearms flare brighter with each push, throwing shifting magenta, cyan and lime light onto the cone where he touches it. The camera breathes slightly with his head, small and human. NEVER shows his face or any second bunny.`, }, "ragga-breathe": { motion: @@ -127,7 +127,7 @@ const SHOTS = { }, outro: { motion: -`The bunny finishes. He slows over the first moments — the swinging arcs shortening, his ears sinking lower with each one — then stops entirely at the center of the wrecked rug, standing still, one front paw settling onto his own chest, his sides rising and falling with breath. His two long ears hang all the way down, completely spent. His wool is visibly fluffed and disheveled everywhere, and the last loose wisps drift down and settle onto the rug around him. Behind him the speaker cones go still. The camera drifts slowly back to a wide as the warm little lamp is left the only light. Held, warm, finished.`, +`TOP-DOWN, straight overhead, the last shot of the film. The bunny lies at the centre of the round green rug, finally still, sprawled with all four paws loose and his TWO long ears splayed flat to either side. His sides rise and fall with breath. The rainbow LEDs inside him fade slowly across the shot — separate colored blooms breathing gently and dimming, one or two winking out entirely — until only a soft slow pulse is left. Loose wool wisps drift down and settle onto the rug around him. The camera RISES very slowly straight up toward the rafter peak, so he grows smaller and the plank floor and looping cables open out around him. Held, warm, finished.`, }, }; diff --git a/pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs b/pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs index 8cd1336be0..2e5de15ee6 100644 --- a/pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs +++ b/pop/maytrax/bin/gen-sections-femrag-plusplus-shakeout.mjs @@ -84,11 +84,25 @@ GLASSES GEOMETRY (CRITICAL): the bunny has exactly TWO eyes. a bunny's eyes sit const ELECTRIC = `ELECTRIC FELT — the bunny is WIRED. This is the whole look of the film and it must read in every frame: · CIRCUITRY IN THE WOOL — fine enameled COPPER and colored magnet WIRE is needle-felted directly INTO his body, running across his chest, shoulders, back and down his limbs in deliberate looping CIRCUIT TRACES, stitched flush into the fibers like veins under skin. tiny felted solder-blobs and little brass eyelets sit where traces meet. the wire is a craft material here — hand-couched into wool, sometimes lifting a millimeter clear of the surface, never printed-on graphics. -· LEDS INSIDE HIM — small bright LEDs are embedded UNDERNEATH the felt, inside his body and inside each ear, and they light him FROM WITHIN. wool is translucent when backlit, so the light BLOOMS THROUGH the fibers: his chest and belly glow like a paper lantern, individual fibers over each LED catch fire with light and read as a soft bright halo, and the wool immediately around each LED goes warm and semi-transparent while the wool further away stays matted and opaque. you can see the discrete POINT of each LED as a hot core inside a soft glowing patch — never a flat even wash, never an overlay. -· HIS EARS LIGHT UP — each ear has a run of LEDs inside it, so when an ear swings past a dark background the whole ear glows through from the inside, the wool going translucent pink-white with the fiber structure visible against the light, like a hand held over a torch. +· LEDS INSIDE HIM, IN FULL RAINBOW — many small bright LEDs are embedded UNDERNEATH the felt, inside his body and inside each ear, and they light him FROM WITHIN. every LED is a DIFFERENT COLOR — magenta, cyan, lime, hot orange, violet, gold, rose — scattered all through him, so he reads as a RAINBOW MULTIGLOW rather than one warm color. wool is translucent when backlit, so each colored light BLOOMS THROUGH the fibers as its own soft round patch of that hue, and where two colored patches overlap in the wool their colors mix and bleed into a third. you can see the discrete POINT of each LED as a hot colored core inside its own soft glowing patch — never a flat even wash, never a single uniform tint, never an overlay. +· HIS EARS LIGHT UP IN COLOR — each ear has a run of differently-colored LEDs inside it, so when an ear swings past a dark background the whole ear glows through from the inside as a chain of separate colored blooms — cyan next to magenta next to lime — the wool going translucent with the fiber structure visible against each light, like a hand held over a torch. · THE GLASSES CATCH IT — the red felt rims pick up the internal glow from below and read hot red. +· THE PROPS PERSIST — the felted LAPTOP he made the beat on stays in the room for the WHOLE film. It remains open on the green rug where he left it, screen still lit and glowing, visible somewhere in every wide shot — never cleared away, never closed, never teleported. Same for the amps, the pedal boxes and the yarn cables: once a prop is in the room it stays in the room, in the same place, for the rest of the story. · THE TETHER — a slim felted battery pack sits at the small of his back, and a colored wire tail trails off him across the floor to the speaker stack: he is PLUGGED IN to the sound system, part of the circuit.`; + +// The screen is a REAL interface, not an invented one: the notepat capture in +// out/notepat-screen-ref.png is passed as a reference on every beat the +// laptop is visible, and this law describes what it actually shows. +const SCREEN_LAW = +`THE LAPTOP SCREEN — it shows the REAL AESTHETIC COMPUTER / NOTEPAT interface from the supplied screen reference, reproduced faithfully in felt and RIGHT-SIDE UP. Read the reference and match its LAYOUT exactly: +· a pale near-white background, not a dark screen. +· across the TOP, a stack of thin horizontal PARAMETER ROWS — each a narrow full-width bar with a small label at its left and a colored fill running part-way across it (one bar filled bright green almost end to end, one filled mint-green about half way, the rest mostly empty) — and a row of small dark status chips in the top-right corner. +· below those, a single row of wide pale INSTRUMENT TABS butted edge to edge, each a different very pale tint — pale blue, mint, peach, lilac, white, cream, ivory, pale cyan — with the leftmost tab highlighted. +· across the MIDDLE, a wide empty pale area with a thin horizontal line through it and a tiny pixel character standing on that line. +· across the BOTTOM, the note grid: TWO side-by-side BANKS separated by a narrow white gutter, each bank EXACTLY 4 TILES WIDE and 3 TILES TALL — twelve flat rectangular candy-pastel tiles per bank, packed edge to edge like postage stamps, a small dark letter in each tile's top-left corner and a small faint label in its bottom-left. the top row runs salmon, rust-orange, peach, olive; the middle row yellow, mint-green, teal, cornflower-blue; the bottom row violet, lilac, magenta, orchid. the two banks repeat the same colors. never crop, merge, or duplicate a row or column — always exactly 4 across and 3 down per bank. +Render all of it as flat hand-felted color fields with fuzzy wool edges — a felted REPRODUCTION of that interface, glowing. The small letters and labels may stay soft and ILLEGIBLE (they are tiny felt marks, not typography), but the layout, the proportions and the tile colors must match the reference. NO invented interface, NO waveform art, NO abstract equalizer, NO logos or wordmarks anywhere.`; + // The room: @jeffrey's old Ashland A-frame, felted. const WORLD = `THE ROOM — a needle-felted miniature of an A-FRAME HOUSE INTERIOR (modeled on the reference photo sheet): steep TRIANGULAR A-frame geometry is the defining shape of every wide shot — massive exposed timber rafters felted in dark walnut wool running up and meeting in a peak overhead, the sloped ceiling planes closing in on both sides, honey-and-amber WOOD PLANK walls and a wood plank FLOOR felted in warm ochre wool with visible board seams, and a big window set into the gable end. a round green wool rug sits on the floor. the SAME room throughout the film — same rafters, same plank floor, same gable window — progressively taken over by the sound system. @@ -114,6 +128,19 @@ const FRAME_NOTE = `${FRAMING_YT_LANDSCAPE} HONOR THE SHOT DIRECTIVE in this beat exactly: a WIDE shot lets the whole room breathe with the bunny comfortably within it; a CLOSE-UP crops in tight and FILLS the frame with the detail (do NOT pull back to a wide); a LOW-ANGLE puts the camera down on the rug looking up.`; + +// ── camera vocabulary — the film changes viewpoint constantly ──────────── +// @jeffrey's note: he likes the perspective shifts, so no two neighbouring +// beats share a camera. Four recurring rigs plus the classical wides. +const CAM_POV = +`CAMERA — FIRST PERSON. The camera IS the bunny's eyes. The only parts of him in frame are his own front PAWS and forearms reaching in from the bottom edge, seen from behind as his own hands. His head, face and ears are NEVER visible because the camera is inside his head. No mirror, no reflection, no second bunny anywhere.`; +const CAM_GAME = +`CAMERA — THIRD-PERSON VIDEO-GAME CHASE CAM, exactly like Grand Theft Auto or Roblox: the camera floats a couple of feet BEHIND and slightly ABOVE the bunny, looking down at him at a shallow angle over his shoulders, with his whole body and both ears held in the lower-middle of the frame and the room opening up ahead of him. The framing is deliberately game-like — the character read from behind, the environment laid out in front — but it is still a real photograph of a felt diorama, NOT a screenshot, NOT a rendered game, with no HUD, no crosshair, no health bar, no interface of any kind overlaid.`; +const CAM_WINDOW = +`CAMERA — OUTSIDE THE HOUSE, LOOKING IN THROUGH THE GABLE WINDOW. The camera is out in the cold blue night, level with the A-frame's gable window, shooting through its felted panes into the lit room beyond. The window frame and mullions cross the shot as dark silhouettes in the foreground, the felted exterior wall boards catch a little spill, and everything inside — the bunny, his colored glow, the neon in the rafters, the speaker tower — reads as a warm saturated pocket of light seen from the dark outside. Small in frame, distant, and completely silent-feeling.`; +const CAM_TOP = +`CAMERA — DIRECTLY OVERHEAD, TOP-DOWN. The camera hangs at the peak of the A-frame looking straight DOWN, so the round green rug reads as a perfect circle, the plank floor as parallel lines, the bunny as a small figure seen from above with both ears splayed out to the sides against the boards, and the yarn cables as loops drawn flat on the floor. A true plan view — no horizon, no walls, the floor filling the frame.`; + // ── the twelve beats — one per struct section ──────────────────────────── // Names + order MUST match pop/maytrax/out/femrag-plusplus.struct.json. const BEATS = [ @@ -126,19 +153,19 @@ const BEATS = [ }, { // The film OPENS here — the track starts cold on the drop. - name: "drop1a", faceVisible: false, pov: true, + name: "drop1a", faceVisible: false, pov: true, screen: true, scene: `FIRST-PERSON POV, and this is the FIRST SHOT OF THE FILM — the track opens cold on the drop, so this frame is already at full power. The camera is the bunny's own eyes, looking DOWN at the little felted LAPTOP open on the green wool rug in front of him. His own two off-white furry FOREPAWS reach into frame from the bottom, seen from behind as his own hands, both down on the laptop's felted keyboard mid-strike — he is HAMMERING the beat out. The laptop is entirely needle-felted: a soft charcoal-wool clamshell, a keyboard of tiny pale felted keycaps, and a SCREEN that is a flat panel of glowing felt showing only soft abstract blocks and bars of colored light (magenta, cyan, amber) in even rows — a music grid read purely as colored wool light, with NO readable text, numbers or letters anywhere. The screen blazes and throws hard colored light up onto his paws. The copper circuit traces felted into his forearms catch it, and the LEDs under the wool of his arms are at FULL BLAST, glowing hot from inside so his own forearms are lanterns. Beyond the laptop: the plank floor, the speaker tower, and the dark A-frame rafters overhead with the whole neon rig BLASTING ON — magenta, cyan and acid-green tubes at once, colored light raking the beams and pooling on the boards.`, }, { - name: "drop1b", faceVisible: true, + name: "drop1b", faceVisible: true, cam: CAM_GAME, scene: -`CLOSE-UP, filling the frame — the bunny's head and shoulders in full motion, single fibers and matted clumps resolving. Head thrown back, mouth-line open in delight, bead eyes squeezed into happy closed lash-lines centered inside their red lenses. His TWO long ears are mid-whip in a wide S-curve past his head — exactly two ears, each a separate glowing translucent shape with its internal LEDs visible as bright cores through the wool, fiber structure lit up against the light. The felted copper traces on his shoulders flare where cyan neon rakes across them. Loose wool wisps float free, catching magenta light. Behind, out of focus, the dark rafters and the blazing tubes.`, +`THIRD-PERSON GAME CAM behind him — the film's first perspective flip, straight after the POV opening. He has shoved back from the laptop and is up dancing on the green rug, seen from behind and slightly above with the whole A-frame opening out ahead of him: the speaker tower down the left, the gable window glowing dark blue at the far end, neon tubes blazing magenta and cyan along both sloped ceiling planes. He is mid-shakeout, body twisted, front paws thrown wide, his TWO long ears whipping out to either side and both blazing with their chains of colored LEDs — magenta beside lime beside cyan — the rainbow glow inside his body throwing real colored light down onto the rug. The felted laptop is still open on the rug behind him, screen still lit. His wire tail runs off toward the stack.`, }, { - name: "breakdown", faceVisible: true, + name: "breakdown", faceVisible: true, cam: CAM_WINDOW, scene: -`WIDE and still, the room at its darkest. The bunny stands at the largest felted speaker cabinet with one front paw laid flat against its oatmeal wool cone, feeling the bass through his paw. Body low, knees bent, rocking. His TWO long ears hang low and heavy, swinging in a slow lazy pendulum. Almost every neon tube is DARK now — only one cyan tube along a far rafter still burns low, and the dominant light in the entire room is the glow coming OUT of the bunny himself: his chest lantern throws warm light across the speaker cone he is touching, up onto his own chin, and down onto the plank floor around his feet, his ears two soft glowing shapes in the dark. The A-frame rafters recede into blackness above. Enormous, quiet, patient.`, +`FROM OUTSIDE, THROUGH THE GABLE WINDOW, on the breakdown. The camera is out in the cold blue night looking in through the felted window panes; the dark mullions cross the frame. Inside, the room has gone almost black — nearly every neon tube is out — and the bunny is a small distant figure standing at the big felted speaker cabinet with one front paw laid flat on its wool cone, feeling the bass. He is the brightest thing in the house: his rainbow LEDs glowing through his wool in separate colored blooms, both long ears hanging low and lit from within, the colored light spilling out of him onto the cone, the rug and the plank floor around him. The felted laptop still sits open on the rug, its pale screen glowing. One low cyan tube burns in the rafters. Warm saturated light in a small window, seen from the cold outside.`, }, { name: "buildup2", faceVisible: true, @@ -151,19 +178,19 @@ const BEATS = [ `WIDE, LOW-ANGLE into the A-frame peak. THE SECOND DROP — bigger than the first. The bunny is at the very top of a leap, higher than before, body arched back, all four paws flung out in a star, tail puff visible. His TWO long ears — exactly two — whip up and back above his head, both blazing translucent. He is at maximum power: his whole body glowing, throwing colored light onto the rug below. The neon rig is at FULL BLAST and there is more of it now — magenta, cyan, acid-green and hot orange tubes running the length of every rafter and down both sloped ceiling planes, the speaker tower stacked up under the beams behind him, hard saturated light everywhere, wool wisps hanging in the colored air where he launched.`, }, { - name: "drop2b", faceVisible: true, + name: "drop2b", faceVisible: true, cam: CAM_TOP, scene: -`EXTREME CLOSE-UP on the bunny's hind paws and the plank floor, filling the frame — the only beat where his face is not the subject. Wool fibers at maximum resolution: the soft-grey felted pads of his hind paws mid-stamp, one paw planted and compressing the wool of the green rug, the other lifting away with wisps trailing. Right through the frame run the felted-in COPPER CIRCUIT TRACES on his ankles and a thick colored yarn cable snaking across the boards, and an LED under the wool of his lower leg glows hot through the fibers, throwing light onto the plank floor. Magenta and cyan neon rake across the wood from above. Above and out of focus, the blurred glowing mass of his body still moving.`, +`TOP-DOWN, straight overhead from the peak of the A-frame. The round green rug reads as a perfect circle against the parallel lines of the ochre plank floor, the yarn cables loop flat across the boards, and the felted laptop sits open near the rug's edge with its screen glowing up at the camera. The bunny is directly below, seen from above mid-stamp — body foreshortened, both hind paws planted apart on the rug, front paws flung out, and his TWO long ears splayed out flat to either side against the floor, each one a chain of separate colored glows. The rainbow light inside him spills outward across the rug in overlapping colored pools, and the neon in the rafters rakes hard colored light across the boards from the edges of frame. A plan view of a bunny going off.`, }, { - name: "ragga-a", faceVisible: true, + name: "ragga-a", faceVisible: true, cam: CAM_GAME, scene: -`WIDE. THE DANCEHALL — the A-frame is now a full sound-system dance. Felted speaker cabinets are stacked in a leaning tower up under the rafters, huge oatmeal wool cones, thick yarn cables looped across the plank floor and up into the beams. The neon has gone warm and heavy for this section: deep amber and forest-green tubes along the rafters, one hot orange run down the sloped ceiling, the wood glowing warm in it. The bunny skanks: leaning forward, weight low, one shoulder dropped, both front paws swinging loosely across his body, one hind paw kicked back. His TWO long ears swing in a lazy behind-the-beat arc, both glowing from within. His internal light has gone warm gold here, and his wire tail runs from his back across the floor into the base of the stack — plugged in.`, +`THIRD-PERSON GAME CAM, the dancehall. The camera floats behind and above him as he skanks across the room, the whole A-frame laid out ahead: the speaker tower now an enormous leaning stack up under the rafters on the right, the gable window at the far end, thick yarn cables looping across the plank floor, the felted laptop still open and lit on the rug he has danced away from. The neon has gone warm and heavy — deep amber and forest-green tubes along the beams with one hot orange run down the sloped ceiling. He is seen from behind, leaning forward with his weight low, one shoulder dropped, both front paws swinging across his body, one hind paw kicked back, his TWO long ears swinging in lazy behind-the-beat arcs and glowing rainbow from within. His wire tail runs from his back across the floor into the base of the stack.`, }, { - name: "ragga-b", faceVisible: true, + name: "ragga-b", faceVisible: false, pov: true, scene: -`CLOSE-UP, low — the bunny and the mouth of the biggest felted speaker cone behind him. He has his back half-turned to the cabinet, leaning into it, ONE long ear flung forward across his own face by the swing and the OTHER trailing behind — exactly two ears, both lit from the inside and translucent against the dark cone. Bead eyes happy inside their red lenses. The enormous oatmeal wool cone behind him is visibly pushed outward at its center — the wool physically bulging with a bass note — and loose fibers around its rim lift away from the surface. Amber and forest-green neon rakes across the scene from the rafters above; his own gold internal glow lights the cone's wool where he leans on it.`, +`FIRST PERSON again, right up at the speaker. The camera is the bunny's own eyes looking at the mouth of the enormous felted speaker cone directly in front of him, filling most of the frame — a vast disc of oatmeal wool with its felted dust-cap at the centre, close enough to see individual fibers. His own two furry FOREPAWS reach in from the bottom of frame and press flat against the cone's wool, and the cone is visibly pushed OUTWARD at its centre against them, bulging with a bass note, loose fibers around its rim lifting off the surface. The rainbow LEDs under the wool of his own forearms throw magenta, cyan and lime light onto the cone where his paws touch it, and the copper traces on his arms catch that light. Amber and forest-green neon rakes in from the rafters above. No face, no second bunny — just his paws, the cone, and the light coming out of him.`, }, { name: "ragga-breathe", faceVisible: true, @@ -181,9 +208,9 @@ const BEATS = [ `WIDE, LOW-ANGLE from the plank floor looking up into the A-frame peak — the biggest dancehall frame in the film. Every neon tube in the rafters is at FULL BLAST at once: magenta, cyan, acid-green, amber and hot orange running the length of every beam and down both sloped ceiling planes, the whole triangular geometry of the room screaming with saturated color, hard light raking the walnut and pooling across the boards. The bunny is mid-skank at maximum power, body low and turned, one hind paw kicked back, both front paws thrown wide, his TWO long ears — exactly two — whipping out in big arcs, each one blazing translucent. His whole body is a lantern at full output, throwing colored light down onto the rug. The speaker tower rises the full height of the frame beside him, cones bulging, yarn cables everywhere, his wire tail plugged into its base. Loose wool wisps hang lit in the colored air.`, }, { - name: "outro", faceVisible: true, + name: "outro", faceVisible: true, cam: CAM_TOP, scene: -`WIDE and warm, the A-frame at rest. The bunny has stopped dancing and stands still at the center of the green rug on the plank floor, breathing, one front paw resting on his own chest. His TWO long ears hang all the way down, completely spent, and their internal light has faded to a last dim ember. His wool is visibly fluffed and disheveled all over — he has genuinely shaken his fuzz out — and loose wisps have settled across the rug and boards around him. Every neon tube in the rafters is DARK except one, a low magenta run along the near beam. The LEDs inside his chest have dimmed to a slow soft pulse, the only real light left, glowing gently through the wool. The speaker tower looms quiet behind him. The great rafters rise into the dark. After.`, +`TOP-DOWN, straight overhead, the last shot. The round green rug is a perfect circle on the ochre plank boards; loose wool wisps have settled all across it and the floor around it. The bunny lies at the centre seen from directly above, finally still, sprawled on his back with all four paws loose and his TWO long ears splayed flat out to either side on the rug — completely spent, his wool visibly fluffed and disheveled everywhere. The rainbow LEDs inside him have dimmed to a slow soft pulse, separate colored blooms breathing gently through the wool, the last real light in the room. The felted laptop still sits open at the rug's edge where he left it, its pale screen still faintly glowing. The yarn cables loop away across the boards to the dark speaker tower. Every neon tube overhead is out but one low magenta run at the frame's edge. After.`, }, ]; @@ -198,6 +225,8 @@ const ANCHOR = panelPath(0, "runway"); // (exposed rafters meeting in a peak, honey plank walls and floor, gable // window, round green rug, string lights already strung along the beams). const ASHLAND = resolve(REPO, "pop/nullabye/reel/special-sign/refs/ashland-house-platter-contact.jpg"); +// A REAL notepat/AC-native capture — the screen is never invented. +const NOTEPAT = `${OUT}/notepat-screen-ref.png`; function build(beat) { return [ @@ -209,7 +238,8 @@ function build(beat) { WORLD, LIGHT, PALETTE, - beat.pov ? POV_NOTE : null, + beat.cam ?? (beat.pov ? CAM_POV : null), + beat.screen ? SCREEN_LAW : null, `THIS BEAT — ${beat.scene}`, AVOID, ].filter(Boolean).join("\n\n"); @@ -285,7 +315,7 @@ FAIL if any of these are true: 2. Any red glasses lens is EMPTY — an eye sitting above/outside its rim while a red circle sits elsewhere on the face. Each visible eye must be centered INSIDE a red lens. 3. More than TWO eyes read on the face (an eye plus two empty rims counts as this failure). 4. There is MORE THAN ONE rabbit in the frame, or a human person appears. -5. There is readable text, a wordmark, letters, or numbers anywhere — including on any screen in frame. +5. There is a readable WORD, brand name, wordmark or logo anywhere. (Small illegible marks and single letters on interface tiles are FINE and must NOT fail — only actual readable words or brands fail.) Otherwise PASS. If the frame is a close-up of paws/floor, or a first-person POV showing only the rabbit's own forepaws with no head visible, then rules 1-3 do not apply — PASS unless rule 4 or 5 is broken.`; try { const r = await fetch("https://api.openai.com/v1/chat/completions", { @@ -330,7 +360,11 @@ async function generate(beat, i) { // The Ashland sheet carries the ROOM (A-frame geometry, plank wood, gable // window); the anchor carries the BUNNY. The anchor itself is generated // against Ashland alone, since it is what defines the character. - const refs = [existsSync(ASHLAND) ? ASHLAND : null, i > 0 && existsSync(ANCHOR) ? ANCHOR : null].filter(Boolean); + const refs = [ + existsSync(ASHLAND) ? ASHLAND : null, + i > 0 && existsSync(ANCHOR) ? ANCHOR : null, + beat.screen && existsSync(NOTEPAT) ? NOTEPAT : null, + ].filter(Boolean); const useEdit = refs.length > 0; const needsQA = beat.faceVisible || beat.pov; // POV still gets the no-text check const attempts = needsQA ? QA_ATTEMPTS : 1; diff --git a/pop/maytrax/bin/render-femrag-plusplus.mjs b/pop/maytrax/bin/render-femrag-plusplus.mjs index 64fdf90fd0..da7f5113ea 100644 --- a/pop/maytrax/bin/render-femrag-plusplus.mjs +++ b/pop/maytrax/bin/render-femrag-plusplus.mjs @@ -5,7 +5,17 @@ // bass: noise snares and hats, a tanh-driven sub bassline, and the rag // bells riding double-time on top. // -// 80 bars × 4 beats × 60 / 144 BPM = 133.33 seconds ≈ 2:13. +// No intro (@jeffrey, 2026-08-13): the runway, the groove, and the buildup all +// went — the record now opens ON the first drop, stinger and all. TRIM is how +// many bars are cut off the front; `at` shifts the whole arrangement back by +// it, so anything scored before bar TRIM lands at negative time and is simply +// never written. Move that one number to give the intro back. +// +// The drop is not slammed cold, though: the master swells in over the first +// four bars, so it reads as the record fading up mid-party. The back half was +// extended too — the ragga breathes for eight bars, then pushes for sixteen. +// +// 88 bars × 4 beats × 60 / 144 BPM = 146.67 seconds ≈ 2:27. // // node pop/maytrax/bin/render-femrag-plusplus.mjs // node pop/maytrax/bin/render-femrag-plusplus.mjs --out ~/femrag-plusplus.mp3 @@ -23,7 +33,8 @@ const SR = 48_000; const BPM = 144; const BEAT = 60 / BPM; const BAR = BEAT * 4; -const BARS = 80; +const TRIM = 16; // bars trimmed off the front — the track opens on the drop +const BARS = 104 - TRIM; const SECONDS = BARS * BAR; const NS = Math.ceil(SECONDS * SR); const TAU = Math.PI * 2; @@ -41,7 +52,7 @@ const argv = (flag) => { const expandHome = (p) => !p ? p : p === "~" ? homedir() : p.startsWith("~/") ? resolve(homedir(), p.slice(2)) : p; const hz = (midi) => 440 * 2 ** ((midi - 69) / 12); -const at = (bar, beat = 0) => bar * BAR + beat * BEAT; +const at = (bar, beat = 0) => (bar - TRIM) * BAR + beat * BEAT; const swung = (beat) => { const eighth = beat * 2; const whole = Math.floor(eighth + 1e-9); @@ -337,22 +348,10 @@ function wubBar(bar, weight = 1) { // ── act one · femrag compressed to a fuse ─────────────────────────────────── -// 0–4: the runway — the drop's two-step skeleton arrives FIRST, quiet, with -// the rag straight-time on top. The whole track lives in one rhythm world. -for (let bar = 0; bar < 4; bar++) { - const rise = .5 + bar * .1; - kick(at(bar, 0), .17 * rise); - kick(at(bar, 2.5), .16 * rise); - snare(at(bar, 1), .09 * rise, { ghost: bar < 2 }); - snare(at(bar, 3), .09 * rise, { ghost: bar < 2 }); - if (bar >= 1) ragBar(bar, .45 + bar * .08, 12, { oom: false, straight: true }); - if (bar >= 2) ticks(bar, .3 + (bar - 2) * .2); -} - -// 4–12: the drop's groove at half power — full dnb kit muted, rag and lead -// straight-time, sub pips walking the roots. The drop will only turn it up. +// 4–12: the drop's groove — full dnb kit, rag and lead straight-time, sub pips +// walking the roots. Opening here means it starts near power and only climbs. for (let bar = 4; bar < 12; bar++) { - const grow = .55 + (bar - 4) * .04; + const grow = .78 + (bar - 4) * .015; dnbBar(bar, grow, { fill: bar === 11 }); ragBar(bar, .82, 12, { oom: false, straight: true }); leadBar(bar, .7, bar < 8 ? 0 : 12, { straight: true }); @@ -445,33 +444,62 @@ for (let bar = 56; bar < 72; bar++) { } sub(at(56, 0), BEAT * 3.8, 21, .28, { drive: 4, slideTo: 33 }); -// 72–80: outro — the wub winds down and femrag's swing gets the last word. -for (let bar = 72; bar < 76; bar++) { - const fade = 1 - (bar - 72) * .18; +// 72–80: the ragga breathes — donk pulled back, wub out, the rag and lead +// carry alone. The room gets its air back before the last push. +for (let bar = 72; bar < 80; bar++) { + const back = bar < 76 ? .45 : .45 + (bar - 76) * .14; + raggaBar(bar, back); + ragBar(bar, .75, 12, { oom: false }); + leadBar(bar, .6, bar >= 76 ? 12 : 0); + if (bar >= 76) wubBar(bar, (bar - 75) * .22); + if (bar % 4 === 3) for (let s = 0; s < 4; s++) { + bell(at(bar, 2) + s * BEAT / 2, CHORDS[bar % 4].notes[s % 4] + 24, .03, s % 2 ? .6 : -.6, .12); + } +} +riser(at(78), BAR * 2 - BEAT * .5, .11); + +// 80–96: the last push — everything on, lead every bar, bell runs every four. +for (let bar = 80; bar < 96; bar++) { + const w = bar === 80 ? 1.15 : 1.05; + raggaBar(bar, w); + wubBar(bar, 1); + ragBar(bar, .85, 12, { oom: false }); + leadBar(bar, .7, 12); + if (bar % 4 === 0) for (let s = 0; s < 8; s++) { + bell(at(bar, 2) + s * BEAT / 4, CHORDS[bar % 4].notes[s % 4] + 24, .03, s % 2 ? .7 : -.7, .08); + } +} +sub(at(80, 0), BEAT * 3.8, 21, .28, { drive: 4, slideTo: 33 }); + +// 96–104: outro — the wub winds down and femrag's swing gets the last word. +for (let bar = 96; bar < 100; bar++) { + const fade = 1 - (bar - 96) * .18; raggaBar(bar, .8 * fade); wubBar(bar, .75 * fade); ragBar(bar, .55 * fade, 12, { oom: false }); } -for (let bar = 76; bar < 80; bar++) { - const fade = 1 - (bar - 76) * .2; +for (let bar = 100; bar < 104; bar++) { + const fade = 1 - (bar - 100) * .2; kickBar(bar, .7 * fade); ragBar(bar, .45 * fade); - if (bar === 76) leadBar(bar, .5); - if (bar === 78) leadBar(bar, .35, -12); + if (bar === 100) leadBar(bar, .5); + if (bar === 102) leadBar(bar, .35, -12); } -bell(at(79, 3), 45, .085, 0, .42); -sub(at(79, 3), BEAT * 1.2, 33, .1, { drive: 2, release: .4 }); +bell(at(103, 3), 45, .085, 0, .42); +sub(at(103, 3), BEAT * 1.2, 33, .1, { drive: 2, release: .4 }); // ── master ───────────────────────────────────────────────────────────────── let peak = 0; for (const channel of out) for (const sample of channel) peak = Math.max(peak, Math.abs(sample)); const gain = peak ? .88 / peak : 1; -const fadeIn = Math.floor(.006 * SR); +// Four bars of swell instead of a click-guard: the drop is already playing +// when the record fades up, which is the point of cutting the intro. +const fadeIn = Math.floor(BAR * 4 * SR); const fadeOut = Math.floor(.55 * SR); for (const channel of out) { for (let i = 0; i < NS; i++) channel[i] *= gain; - for (let i = 0; i < fadeIn; i++) channel[i] *= i / fadeIn; + for (let i = 0; i < fadeIn; i++) channel[i] *= Math.sin(i / fadeIn * Math.PI / 2); for (let i = 0; i < fadeOut; i++) channel[NS - 1 - i] *= i / fadeOut; } @@ -483,10 +511,10 @@ writeFileSync(outPath.replace(/\.mp3$/, ".events.json"), // The arrangement, as the video pipeline reads it. Bar ranges here are the // same ones the section loops above use — change one, change both. const SECTIONS = [ - ["runway", 0, 4], ["groove", 4, 12], ["buildup1", 12, 16], ["drop1a", 16, 24], ["drop1b", 24, 32], ["breakdown", 32, 40], ["buildup2", 40, 44], ["drop2a", 44, 52], ["drop2b", 52, 56], - ["ragga-a", 56, 64], ["ragga-b", 64, 72], ["outro", 72, 80], + ["ragga-a", 56, 64], ["ragga-b", 64, 72], ["ragga-breathe", 72, 80], + ["ragga-push", 80, 88], ["ragga-push-b", 88, 96], ["outro", 96, 104], ]; writeFileSync(outPath.replace(/\.mp3$/, ".struct.json"), JSON.stringify({ totalSec: SECONDS, -- 2.51.2