Audio-reactive video generator. Input: audio → output: video where the camera flies forward forever and punches in on every kick, with the punch strength driven by low-frequency (<150 Hz) energy. Snares whisper, kicks punch.
Reference: reference_video.mp4 (style/behavior only — never overwrite it).
audio (librosa) → per-beat kick/snare strengths → beat canvases (Z-Image) → frames → ffmpeg
Beat chain (default, --beat-mode chain). One canvas per beat (at 200 BPM
that's a fresh image every 0.3 s). Each canvas is img2img'd from the previous
beat's zoomed end view (rebase), so the flight zoom never steps back.
Inside a beat the zoom is linear plus a 2-frame kick step:
z_beat = min(--zmax, --drift + --kick-gain * kick^1.5 + --snare-gain * snare)
kick= <150 Hz spectral-flux envelope (max near the beat),snare= 150–500 Hz.- Seams hidden with a 2-frame dissolve (
--beat-blend), per-beat palette locked with a LAB color match, opening blooms from white (--intro-fade). - Beat 0 comes from
--init-image(a proven canvas) or auto-picked from--seed-triestxt2img candidates. - Framing is texture-seeking: each beat crops the most detailed window, so the flight never gets stranded in flat sky (plus: put texture in every prompt's sky).
- Grade tames neon extremes + highlight rolloff + S-curve (skip:
--no-grade).
Legacy --beat-mode cycle (crossfading keyframes) is kept for --no-diffusion
pipeline tests only — its blend resets cause visible zoom-back.
generate.py # thin entry shim (all logic in the package below)
audioreactive/ # the actual codebase
config.py # models, prompts constants, ALL tuning numbers
audio.py # tempo / onsets / kick-snare bands / beat grid
camera.py # rebase crops, texture-seeking + clamped pans
color.py # LAB match, tame_blue, anchor_pull, grade, upscale
diffusion.py # model adapters (Z-Image, Qwen-Image-2.1) + beat chain (GPU only)
render.py # procedural mocks, beat-chain + legacy cycle renderers
cli.py # argparse, chain/cycle orchestration, run metadata
jobs/ # SLURM jobs (submit from repo root)
common.sh # env, prompt pack A, shared chain/full-model flags
sample.sh # Turbo check on the 10 s sample
smoke.sh # final-render settings on the 10 s sample
iter.sh # full song at 640x368 for drift checks (srun)
newbeginning.sh # full-song HD render
qwen_sample.sh # Qwen-Image-2.1 chain on the 10 s sample
ztest.sh # model timing kick test (scripts/ztest.py)
scripts/ # dev tools: ztest.py (model kick test), check_drift.py
tests/ # CPU-only pytest suite (camera/color/cli/audio)
python -m audioreactive works too (same flags). Tuning numbers live in
audioreactive/config.py — change them there, never as magic literals.
Conda env nibbler (/home/d.ramos/miniconda/envs/nibbler: torch+CUDA,
diffusers 0.40, librosa, opencv). On this cluster, always:
export LD_LIBRARY_PATH=/home/d.ramos/miniconda/envs/nibbler/lib:$LD_LIBRARY_PATH(without it, the ohpc libstdc++ breaks scipy/diffusers imports). There is no
system ffmpeg — generate.py falls back to the imageio-ffmpeg binary.
Model Tongyi-MAI/Z-Image-Turbo is cached in ~/.cache/huggingface/hub
(reused offline via HF_HUB_OFFLINE=1); the login node has no GPU, so
diffusion runs on the SLURM gpu partition (see jobs/).
Fast pipeline check, no GPU (procedural canvases, exercises the full audio → zoom → mux path):
export LD_LIBRARY_PATH=/home/d.ramos/miniconda/envs/nibbler/lib:$LD_LIBRARY_PATH
/home/d.ramos/miniconda/envs/nibbler/bin/python generate.py \
--audio audio_sample.wav --output output/test.mp4 \
--no-diffusion --beat-mode chain --tempo 200 --prompt "abstract thing"Procedural renders are deterministic: when refactoring, compare md5s of the
chain and --beat-mode cycle outputs before/after.
CPU-only test suite (no GPU, runs on the login node):
/home/d.ramos/miniconda/envs/nibbler/bin/python -m pytest tests/ -qDiffusion renders run on the GPU node; submit from the repo root:
sbatch jobs/sample.sh # 10 s sample, 34 beats, Turbo
sbatch jobs/smoke.sh # 10 s sample, full model at 960x544
sbatch jobs/newbeginning.sh # NewBeginning.wav, 188 s ≈ 628 beats, 1280x720(or bash jobs/newbeginning.sh directly on a GPU machine with
CUDA_VISIBLE_DEVICES set — the login node has no GPU). All jobs seed beat 0
from output/chain_init.png (--init-image).
Key flags: --tempo (else estimated — librosa often halves 200→99, pass it
manually), --width/--height (HD = 1280×720), --model-id, --steps and
--guidance-scale (default per model, see Models), --gen-w/--gen-h (default
per model: 640×368 Z-Image sharp domain, 960×544 full-model HD, 1024×576 Qwen;
snapped to the model's multiple), --canvas-scale (1.2 output
headroom), --beat-strength (0.4 chain continuity), --beats-per-prompt (4),
--kick-gain (0.30), --snare-gain (0.05), --drift (0.03), --zmax (0.30),
--kick-step (0.05, 2-frame attack), --beat-blend (2-frame seam dissolve),
--intro-fade (0.35 s bloom opening), --init-image (proven beat-0 canvas),
--seed-tries (auto-pick contentful txt2img), --max-duration (trim for tests).
Outputs land in output/ (gitignored): video, .json (model rev, seed, beats,
kick strengths, zoom params), keys/ canvases, slurm logs.
Every prompt shares a STYLE prefix; each one must describe sky texture
(clouds/birds/branches) — flat unique-color skies read as dead air on quiet
beats. Prompts cycle (--beats-per-prompt beats each). The negative prompt
must keep the anti-blue tail (config.ANTI_BLUE_NEG, also in the CLI default).
Lives in jobs/common.sh (STYLE, NEG, 12 SCENES) — the single copy all
jobs share.
STYLE = abstract digital painting, flowing three-dimensional shapes, intricate surface detail everywhere, no flat backgrounds, no text, no watermark
(negative: blurry, low detail, watermark, text, flat color, empty background, gradient background)
- molten gold and teal silk ribbons folding in dark space, glowing filaments, drifting sparks
- crystalline cavern of refractive prisms, light shafts, floating dust motes
- close flight through giant translucent membranes with bioluminescent veins, drifting motes
- slow cosmic lava blobs of magenta and cyan, grainy nebula texture, tiny stars
- endless corridor of glowing arches over a reflective floor, luminous haze
- macro shot of iridescent oil film, swirling rainbow slick, tiny bubbles
- layered frozen glass waves, refractions and caustics, suspended droplets
- vortex tunnel of glowing embers and ice crystals spiraling inward
Measure per-frame zoom % as optical-flow (Farneback) divergence. The meter
used during development lived in /tmp/opencode/measure_zoom.py and was never
committed. Reference: median ≈ +1.9 %/frame, spikes to +9 %, never negative.
Procedural mocks should read median ≳ 1.2 with 0 % zoom-back within beats;
on rich diffusion content the meter under-reads ~2× (dark/pastel areas go
quiet), so judge cruising speed on mocks and aesthetics on contact sheets.
Per-model defaults (steps, guidance, gen size, dimension multiple) live in
config.MODEL_PRESETS; diffusion.load_image_model picks the adapter.
- Default:
Tongyi-MAI/Z-Image-Turbo(8 steps, no CFG) — fast checks. - Full
Tongyi-MAI/Z-Image(30 steps +guidance_scale4): use--model-id Tongyi-MAI/Z-Image --steps 30 --guidance-scale 4.0. Qwen/Qwen-Image-2.1(7B, Qwen Research License, untested here): use--model-id Qwen/Qwen-Image-2.1(sbatch jobs/qwen_sample.sh). Needsdiffusers>=0.41+transformers>=5.17; not cached yet (first run downloads it). Differences from Z-Image:- No img2img strength. The previous beat's end view is passed as a
condition image and each beat is denoised from scratch, so
--beat-strengthis ignored; continuity comes from the reference. If beats wander, tuneconfig.QWEN21_CHAIN_PROMPT(wraps each chained prompt). - CFG off by default (the model is meant to run without it), which makes
the negative prompt — and its anti-blue tail — inactive. Pass
--guidance-scale 4to enable it (~2× cost). - Dims must be multiples of 32; defaults to 40 steps at 1024×576.
- No img2img strength. The previous beat's end view is passed as a
condition image and each beat is denoised from scratch, so
- **Kick test verdict (H200 NVL, 640×368, 30 steps): txt2img 3.1 s,
img2img ~1.4 s → a 188 s / 200 BPM song (≈628 beats) renders in ~15 min —
far under 2 h. Quality is a clear step up (painterly detail, natural skies,
prompt adherence). Tested via
scripts/ztest.py/jobs/ztest.sh(output/ztest_*.png, verdictUNDER_2Hin the job log). Recommended for final renders; generate at 960×544 for HD output. - **Head-to-head at HD gen res (960×544, same prompt/seed, foreground
srun,output/cmp_*.png): Turbo (8 steps) txt2img 1.9 s / img2img 0.8 s → song ≈ 8 min, BUT on this seed it drew an abstract yellow-red gradient (weak prompt adherence); full Z-Image (30 steps + guidance 4) txt2img 4.8 s / img2img 2.0 s → song ≈ 21 min (UNDER_2H) with detailed temples, storm clouds, eagles. Outputs differ hugely (mean abs diff 94.8 → genuinely different weights, not a relabel). Full model wins for finals; Turbo stays for fast checks. Proof that--model-idselects the weights: runs logmodel=+rev=([chain] model=...line) and the.jsonrecords both — Turbo rev013496ad, full rev04cc4abb.