#!/usr/bin/env python3 # ═════════════════════════════════════════════════════════════════════════════ # PLAYER COMPUTER — Vacancy (27/32) # by Gene Kogan · 2026 · https://genekogan.com/player_computer/vacancy # # Dancehall in motel neon, with a sign that sings VACANCY to itself all night. # # This single file IS the piece: it draws every frame, synthesizes every sound, # and muxes them into the final video with ffmpeg. No other project files are # needed. You (or your agent) are invited to make a VARIATION of it: # # Generate a variation of this music video using only code. # Start from https://genekogan.com/player_computer/code/vacancy.py.txt # # The original render (for reference, yours should differ): # video: https://genekogan.com/player_computer/media/vacancy.mp4 # cover: https://genekogan.com/player_computer/media/vacancy.jpg # # Requirements: python3, numpy, pillow, and ffmpeg on PATH. # pip install numpy "pillow<13" # Speech/vocals (in pieces that have them) use the macOS `say` command; on # other platforms swap in espeak-ng / any TTS at the say_wav()/speak() calls, # or mute those lines. git provenance stamps degrade gracefully outside a repo. # Run: python3 vacancy.py (writes frames/, audio/, and the final mp4 # next to the script; writes ~8 GB of frames, # takes 5-15 min on a modern machine) # ═════════════════════════════════════════════════════════════════════════════ """ player_computer_final — "VACANCY" (final delivery cut) Dancehall, 100bpm, E minor. 26 bars, instrumental with a vocoded tag. Intro(2) Riddim1(4) Hook(8) Bridge(2) Hook2(6) Dawn(4) — tightened from the 44-bar source (renders/spiral_jam/vacancy): riddim2 cut entirely, intro and bridge halved, hook2 trimmed, dawn kept whole. Closing time. The people go home and the signs wake up: the MOTEL arrow runs its chevrons on the eighth notes, the diner cup tips on the kick and steams, the bowling pin gets knocked down every two bars and gets back up (which is the whole philosophy of dancehall), the donut rotates its sprinkles, 24HR pulses like it means it. The cat is the protagonist now: it opens the piece crossing the empty street at ground level, walks the high wire, pads along the windowsill through the rain bridge, and at dawn walks home along the wet asphalt while the signs switch off one by one and the motel flips to NO VACANCY — full house, good night. Look: neon on a wet street. Dark planes of building, bright sign cores with bloom, everything mirrored in wavy asphalt reflections (the ground cat included); rain falls through the whole piece in delivery space. Round 2 (player_computer_2): the cat gets a night, not just a walk. Behind the diner it finds a mouse in the bins. There is a chase — an alley, a chain-link fence the mouse goes through and the cat very much does not, the lit drums of a 24-hour laundromat — and then a standoff on the kerb where neither of them can be bothered any more and the mouse hands over a chip. They walk home together. At dawn the motel flips to NO VACANCY and there are two lit windows: a cat curled up in one and, one floor higher, a mouse with the penthouse. Five new sets (bins / alley / fence / laundromat / kerb) and a mouse to go with the cat, all in the same rim-lit neon language. Delivered at 1920x1080 (16:9) — the whole picture is laid out in W-relative coordinates, so the wider frame gives more street rather than a stretched one, and the cinematic bars are gone. FINAL CUT (player_computer_final). The film is unchanged; the delivery is: * 1920x1080 native. Every neon sign, cat, mouse and brick in this file is authored against the 1280x720 delivery frame, so rather than hand-scale a thousand literals the raster is allocated 1.5x larger and every draw call goes through `ScaledDraw`, which multiplies coordinates and stroke widths on the way to Pillow. Bloom radii, the wet-asphalt reflection wobble, the rain streaks and the scanline period all scale by S = H/720 = 1.5; grain is drawn at 1280x720 and NEAREST-blown-up so a speck keeps its size. * No debug-metadata overlays — this piece never burnt any in. * Title moment: the "VACANCY" card is the piece's own broken neon. It now flickers PLAYER COMPUTER under it in the same failing tubes, on its own per-letter dropout seeds, so the show name reads as one more sign on the street rather than a caption over it. Composition: engine : audio-first x shot-parallel content: audio-groove (dancehall riddim, skank organ, sub) x ytp (neon strobe language) x effects-post Run from repo root: python3 renders/player_computer_final/vacancy/render.py --sheet python3 renders/player_computer_final/vacancy/render.py --jobs 3 """ import argparse, datetime, hashlib, math, os, subprocess, wave from pathlib import Path import numpy as np from PIL import Image, ImageDraw, ImageFont, ImageFilter NAME = "vacancy" TITLE = "VACANCY" SUBT = "PLAYER COMPUTER" SETDIR = "player_computer_final" SETNUM = "06" W, H, FPS = 1280, 720, 30 # AUTHORING frame — every coordinate below is here # ── delivery scale ────────────────────────────────────────────────────────── # The delivered raster is RW x RH; S = RH/H is the one number the look scales # by. Drawing code keeps writing 1280x720 coordinates and passes through # ScaledDraw, so nothing is upscaled after the fact — it is re-rasterised. RW, RH = 1920, 1080 S = RH / H def P(v): return int(round(v*S)) def PF(v): return v*S def _scale_xy(v, s): if isinstance(v, (list, tuple)): return [_scale_xy(u, s) for u in v] return v*s class ScaledDraw: """ImageDraw proxy that multiplies geometry by S at rasterisation time. Only the first positional argument (the xy geometry) and the `width` keyword are touched — arc/chord/pieslice take *angles* as positionals 2 and 3, which must pass through untouched. """ __slots__ = ("_d", "_s") _GEOM = frozenset(("line", "rectangle", "rounded_rectangle", "ellipse", "polygon", "arc", "chord", "pieslice", "point", "text")) def __init__(self, d, s): self._d, self._s = d, s def __getattr__(self, name): f = getattr(self._d, name) if name not in self._GEOM: return f s = self._s def wrapped(xy, *a, **kw): w = kw.get("width") if w is not None: kw["width"] = max(2, int(round(w*s))) # neon never hairlines return f(_scale_xy(xy, s), *a, **kw) return wrapped def mkdraw(im): d = ImageDraw.Draw(im) return d if S == 1.0 else ScaledDraw(d, S) def new_frame(bg): """A blank delivery frame at real size, with an authoring-space pen.""" im = Image.new("RGB", (RW, RH), bg) return im, mkdraw(im) BPM = 100.0 BEAT = 60.0 / BPM BAR = 4 * BEAT SR = 44100 OUT = Path(__file__).parent FRAMES = OUT / "frames"; FRAMES.mkdir(exist_ok=True) AUD = OUT / "audio"; AUD.mkdir(exist_ok=True) ROOT = Path(__file__).resolve().parent # standalone: was repo root (used for git provenance) FONTS = ROOT / "fonts" SECTIONS = [ ("intro", 0, 2), # the empty street, the cat crosses ("riddim1", 2, 6), # the signs wake up ("spot", 6, 9), # behind the diner: something small moves ("chase", 9, 15), # alley -> fence -> laundromat ("bridge", 15, 17), # rain on the glass; they end up on the same sill ("standoff", 17, 21), # nose to nose on the kerb ("hook2", 21, 26), # walking the street together ("dawn", 26, 32), # signs out, NO VACANCY, two lit windows ] SEC_START = {nm: a for nm, a, b in SECTIONS} SEC_END = {nm: b for nm, a, b in SECTIONS} def sec_u(section, t): """0..1 across a whole SECTION, so a set can tell a beat over several shots instead of restarting its story on every cut.""" a, b = SEC_START[section], SEC_END[section] return float(np.clip((t/BAR - a)/max(1e-6, b - a), 0.0, 1.0)) N_BARS = SECTIONS[-1][2] DUR = N_BARS * BAR + 2.0 N_FRAMES = int(DUR * FPS) MUSIC_DESC = f"dancehall, {BPM:.0f}bpm, E minor, {N_BARS} bars, vocoded tag" ENGINE_DESC = ("street / signsolo / cat / bins / alley / fence / laundro / " "standoff / rainglass / dawn (neon + wet asphalt)") def mtof(m): return 440.0 * 2.0 ** ((m - 69) / 12.0) _PC = {"C":0,"C#":1,"Db":1,"D":2,"D#":3,"Eb":3,"E":4,"F":5,"F#":6,"Gb":6, "G":7,"G#":8,"Ab":8,"A":9,"A#":10,"Bb":10,"B":11} def nf(name): i = 2 if (len(name) > 2 and name[1] in "#b") else 1 return mtof(12 * (int(name[i:]) + 1) + _PC[name[:i]]) def adsr(n, a, d, s, r): e = np.zeros(n) ai, di, ri = max(1, int(a*SR)), max(1, int(d*SR)), max(1, int(r*SR)) ai = min(ai, n); e[:ai] = np.linspace(0, 1, ai) if ai < n: dd = min(di, n - ai) e[ai:ai+dd] = np.linspace(1, s, dd); e[ai+dd:] = s if ri < n: e[-ri:] *= np.linspace(1, 0, ri) return e def voice(freq, dur, kind="saw", nh=26, c0=5200, c1=700, ck=8.0, res=0.0, detune=(0.0,), a=.005, d=.09, s=.7, r=.10, vib=(0.0, 0.0), seed=0): """Additive voice through a *moving* emulated filter (cutoff array). The cutoff glides c0->c1 at rate ck; `res` bumps harmonics near the cutoff. This is what gives plucks, reeses and stabs their motion. """ n = int(dur * SR) if n <= 0: return np.zeros(0) t = np.arange(n) / SR co = c1 + (c0 - c1) * np.exp(-t * ck) rng = np.random.RandomState(seed) out = np.zeros(n) vd, vr = vib for det in detune: f0 = freq * (1 + det * 0.006) for k in range(1, nh + 1): if kind == "saw": base = 1.0 / k elif kind == "square": base = (1.0 / k) if k % 2 else 0.0 elif kind == "tri": base = (1.0 / (k*k)) if k % 2 else 0.0 elif kind == "sine": base = 1.0 if k == 1 else 0.0 else: base = 1.0 / k if base == 0.0: continue fk = f0 * k if fk > SR * 0.45: break g = base / np.sqrt(1.0 + (fk / co) ** 4) if res: g = g + res * base * np.exp(-((fk - co) / (0.3 * co + 1)) ** 2) ph = rng.uniform(0, 2*np.pi) phase = 2*np.pi*fk*t + ph if vd: phase = phase + vd * np.sin(2*np.pi*vr*t) out += g * np.sin(phase) out /= len(detune) return out * adsr(n, a, d, s, r) def fm(freq, dur, ratio=2.0, index=4.0, idec=6.0, a=.002, d=.4, s=.0, r=.2, seed=0): """2-op FM — rhodes / bells / glassy leads.""" n = int(dur*SR); t = np.arange(n)/SR mod = np.sin(2*np.pi*freq*ratio*t) * index * np.exp(-t*idec) return np.sin(2*np.pi*freq*t + mod) * adsr(n, a, d, s, r) def ks(freq, dur, damp=0.996, seed=0): """Karplus-Strong pluck — guitar / harp.""" n = int(dur*SR); L = max(2, int(SR/freq)) rng = np.random.RandomState(seed) buf = rng.uniform(-1, 1, L) out = np.zeros(n); j = 0 for i in range(n): out[i] = buf[j] buf[j] = damp * 0.5 * (buf[j] + buf[(j+1) % L]) j = (j+1) % L return out * adsr(n, .001, .05, .85, .25) def bandshape(x, lo=0.0, hi=0.0, order=4): """Exact FFT band shaping. Noise sources go through this so nothing in the kit is a raw full-band blast (AESTHETIC 13a).""" n = len(x) if n < 8: return x X = np.fft.rfft(x); fq = np.maximum(np.fft.rfftfreq(n, 1/SR), 1e-6) g = np.ones_like(fq) if lo: g *= 1.0/np.sqrt(1.0 + (lo/fq)**order) if hi: g *= 1.0/np.sqrt(1.0 + (fq/hi)**order) return np.fft.irfft(X*g, n) def kick(dur=.30, f0=155, f1=48, punch=30, click=.5, seed=1): n = int(dur*SR); t = np.arange(n)/SR f = f1 + (f0-f1)*np.exp(-t*punch) body = np.sin(2*np.pi*np.cumsum(f)/SR) * np.exp(-t*10.5) ck = np.random.RandomState(seed).randn(n) * np.exp(-t*300) * click return np.tanh((body + ck) * 1.7) * .95 def snare(dur=.22, tone=196, bright=1.0, seed=2): n = int(dur*SR); t = np.arange(n)/SR rng = np.random.RandomState(seed) nz = bandshape(rng.randn(n), lo=280, hi=6200) body = np.sin(2*np.pi*tone*t) + .6*np.sin(2*np.pi*tone*1.58*t) return nz*np.exp(-t*19)*.85*bright + body*np.exp(-t*26)*.50 def hat(dur=.055, openh=False, seed=7): n = int(dur*SR); t = np.arange(n)/SR nz = bandshape(np.random.RandomState(seed).randn(n), lo=5200, hi=9800) return nz * np.exp(-t*(14 if openh else 85)) * .40 def ride(dur=.6, seed=9): n = int(dur*SR); t = np.arange(n)/SR bell = sum(np.sin(2*np.pi*f*t) for f in (522, 831, 1180, 1567, 2103)) nz = bandshape(np.random.RandomState(seed).randn(n), lo=3800, hi=9000) return bell*np.exp(-t*10)*.10 + nz*np.exp(-t*5)*.16 def rim(dur=.09, seed=3): n = int(dur*SR); t = np.arange(n)/SR return (np.sin(2*np.pi*1750*t) + .5*np.sin(2*np.pi*2600*t)) * np.exp(-t*90) * .5 def shaker(dur=.09, seed=5): n = int(dur*SR); t = np.arange(n)/SR nz = bandshape(np.random.RandomState(seed).randn(n), lo=3600, hi=8600) return nz * (np.exp(-t*40) * np.clip(t*260, 0, 1)) * .40 def crash(dur=1.6, seed=13): n = int(dur*SR); t = np.arange(n)/SR nz = bandshape(np.random.RandomState(seed).randn(n), lo=1400, hi=8200) return nz * (np.exp(-t*2.6) + .3*np.exp(-t*.6)) * .55 def riser(dur=2.0, seed=17): n = int(dur*SR); t = np.arange(n)/SR env = (t/dur) ** 1.7 sweep = np.sin(2*np.pi*np.cumsum(140 + 3000*(t/dur)**2)/SR) # noise through a band that *rises with the sweep* — pitched motion, # not a static full-band hiss (AESTHETIC 13a) rng = np.random.RandomState(seed) nz = np.zeros(n); blk = 2048 for i in range(0, n, blk): u = (i/max(1, n)) ** 1.4 fc = 300 + 5200*u seg = rng.randn(min(blk, n-i) + 256) nz[i:i+min(blk, n-i)] = bandshape(seg, lo=fc*.72, hi=fc*1.5)[:min(blk, n-i)] return (nz*env*.55 + sweep*env*.22) * .8 def vinyl(n, seed=23): """Surface noise: filtered hiss + sparse crackle.""" rng = np.random.RandomState(seed) hiss = bandshape(rng.randn(n), lo=140, hi=5200) * .022 cr = np.zeros(n) idx = rng.choice(n, size=max(1, n//2400), replace=False) cr[idx] = rng.uniform(-1, 1, len(idx)) * .10 cr = np.convolve(cr, np.exp(-np.arange(60)/9), "same") return hiss + cr def reverb(x, rt=1.6, mix=.3, seed=29, pre=0.02): """FFT convolution with a synthetic exponentially-decaying noise IR.""" n = int(rt*SR); t = np.arange(n)/SR rng = np.random.RandomState(seed) ir = rng.randn(n) * np.exp(-t*(5.0/rt)) ir[:int(pre*SR)] = 0 ir /= np.abs(ir).sum() / 40.0 + 1e-9 from numpy.fft import rfft, irfft L = 1 << int(np.ceil(np.log2(len(x) + n))) wet = irfft(rfft(x, L) * rfft(ir, L))[:len(x)] wet /= np.max(np.abs(wet)) + 1e-9 return x * (1-mix) + wet * mix * (np.max(np.abs(x)) + 1e-9) def delay(x, time=.25, fb=.38, mix=.25, taps=7): d = int(time*SR); out = x.copy() for i in range(1, taps+1): g = mix * (fb ** i); s = d*i if s >= len(x): break out[s:] += x[:len(x)-s] * g return out def lowpass(x, fc): a = np.exp(-2*np.pi*fc/SR); z = 0.0; y = np.empty_like(x) for i in range(len(x)): z = (1-a)*x[i] + a*z; y[i] = z return y class Song: """A multitrack canvas placed on an absolute bar/beat grid.""" def __init__(self, dur): self.n = int(dur*SR) self.tr = {} self.kick_t = [] def t(self, bar, step=0, swing=0.0): """absolute seconds of 16th-step `step` inside `bar`.""" sw = swing * (BEAT/4) if (step % 2) else 0.0 return bar*BAR + step*(BEAT/4) + sw def put(self, track, sig, at, g=1.0, pan=0.0): b = self.tr.setdefault(track, np.zeros((self.n, 2))) i = int(at*SR); j = min(self.n, i+len(sig)) if i >= self.n or j <= i: return th = (pan*.5+.5) * (np.pi/2) st = np.stack([sig[:j-i]*np.cos(th), sig[:j-i]*np.sin(th)], 1) * g b[i:j] += st def bus(self, track, fn): if track in self.tr: b = self.tr[track] self.tr[track] = np.stack([fn(b[:,0]), fn(b[:,1])], 1) def sec_env(self, levels, glide=0.35): """Section dynamics: a smooth per-sample gain built from {section_name: level}. Arrangement alone tends to come out flat — this is the macro arc the ear actually follows.""" env = np.ones(self.n) for nm, b0, b1 in SECTIONS: i0, i1 = int(b0*BAR*SR), min(self.n, int(b1*BAR*SR)) if i1 > i0: env[i0:i1] = levels.get(nm, 1.0) env[int(SECTIONS[-1][2]*BAR*SR):] = levels.get(SECTIONS[-1][0], 1.0) k = max(1, int(glide*SR)) return np.convolve(env, np.ones(k)/k, "same") def mixdown(self, gains, pump_depth=.30, pump_rel=.16, levels=None): mix = np.zeros((self.n, 2)) for k, b in self.tr.items(): mix += b * gains.get(k, 1.0) if levels: mix *= self.sec_env(levels)[:, None] if self.kick_t: env = np.ones(self.n); rl = int(pump_rel*SR) shape = 1 - pump_depth*np.exp(-np.arange(rl)/(pump_rel*SR/4)) for at in self.kick_t: i = int(at*SR); j = min(self.n, i+rl) if i < self.n: env[i:j] = np.minimum(env[i:j], shape[:j-i]) env = np.convolve(env, np.ones(320)/320, "same") mix *= env[:, None] # DC / sub-30Hz rumble trim (one-pole HP per channel, vectorised # via cumulative difference of a one-pole LP) a = math.exp(-2*math.pi*30.0/SR) for c in range(2): lp = np.empty(self.n); z = 0.0 col = mix[:, c] for i in range(0, self.n, 4096): blk = col[i:i+4096] for j in range(len(blk)): z = (1-a)*blk[j] + a*z; lp[i+j] = z mix[:, c] = col - lp mix = np.tanh(mix*1.25)/np.tanh(1.25) return mix / (np.max(np.abs(mix))+1e-9) * .94 def write(self, path, mix): with wave.open(str(path), "w") as w: w.setnchannels(2); w.setsampwidth(2); w.setframerate(SR) w.writeframes((np.clip(mix, -1, 1)*32767).astype(" macOS `say` (the canonical voices) -> espeak-ng / # espeak (Linux; language mapped from the say voice name, rate is wpm in both) # -> Windows SAPI (default voice, rate mapped from wpm) -> timed silence as # the last resort (duration from a chars/wpm heuristic, loud warning, never # cached so a later run with an engine present re-voices). A re-voiced film is # a different performance of the same score; that is by design. # Force a tier with POOP_TTS=say|espeak|sapi|none. def _tts_lang(voice): v = str(voice) if "Spanish" in v: return "es-mx" if "Mexico" in v else "es" if "Portuguese" in v: return "pt-br" if "Brazil" in v else "pt" if "English (UK)" in v: return "en-gb" return "en-us" def _tts_engine(): import shutil, platform want = os.environ.get("POOP_TTS", "").strip().lower() if want: return want if shutil.which("say"): return "say" if shutil.which("espeak-ng") or shutil.which("espeak"): return "espeak" if platform.system() == "Windows": return "sapi" return "none" def _tts_render(text, voice, rate, path): """Synthesize text -> mono 44.1k wav at `path` with the best available engine. Returns False if no engine (caller falls back to timed silence).""" import shutil, sys, base64 eng = _tts_engine() tmp = path.with_suffix(".tts.wav") try: if eng == "say": aiff = path.with_suffix(".aiff") subprocess.run(["say", "-v", voice, "-r", str(rate), "-o", str(aiff), text], check=True) subprocess.run(["ffmpeg", "-y", "-i", str(aiff), "-ar", str(SR), "-ac", "1", str(path)], check=True, capture_output=True) aiff.unlink(missing_ok=True) return True if eng == "espeak": exe = shutil.which("espeak-ng") or shutil.which("espeak") or "espeak-ng" subprocess.run([exe, "-v", _tts_lang(voice), "-s", str(int(rate)), "-w", str(tmp), str(text)], check=True) elif eng == "sapi": r = max(-10, min(10, round((int(rate) - 175) / 25))) esc = str(text).replace("'", "''") ps = ("Add-Type -AssemblyName System.Speech;" "$s=New-Object System.Speech.Synthesis.SpeechSynthesizer;" f"$s.Rate={r};$s.SetOutputToWaveFile('{tmp}');" f"$s.Speak('{esc}');$s.Dispose()") enc = base64.b64encode(ps.encode("utf-16-le")).decode() subprocess.run(["powershell", "-NoProfile", "-EncodedCommand", enc], check=True) else: return False subprocess.run(["ffmpeg", "-y", "-i", str(tmp), "-ar", str(SR), "-ac", "1", str(path)], check=True, capture_output=True) return True except Exception as e: print(f"[tts] {eng} failed ({e}) — falling back to timed silence", file=sys.stderr) return False finally: tmp.unlink(missing_ok=True) def _tts_silence(text, rate): import sys dur = max(0.6, len(str(text)) / (max(60, int(rate)) * 5.0 / 60.0)) print(f'[tts] no speech engine — timed silence ({dur:.2f}s): ' f'"{str(text)[:48]}"', file=sys.stderr) return np.zeros(int(dur * SR)) def say_wav(text, voice, rate, path): """text -> mono 44.1k voice wav (cached on disk; deterministic per engine).""" path = Path(path) if not path.exists(): if not _tts_render(text, voice, rate, path): return _tts_silence(text, rate) return read_wav(path) def fit(x, n): """Resample to exactly n samples. Shifts formants a little; pitch is the carrier's job, so this is free.""" if len(x) < 2: return np.zeros(n) return np.interp(np.linspace(0, len(x)-1, n), np.arange(len(x)), x) def carrier(f_per_sample, nh=30, detune=(0.0, -0.55, 0.62), vib=(0.0, 0.0)): """Band-limited additive carrier with continuous phase across note changes.""" n = len(f_per_sample) t = np.arange(n)/SR out = np.zeros(n) for d in detune: f = f_per_sample*(1 + d*0.005) if vib[0]: f = f*(1 + vib[0]*np.sin(2*np.pi*vib[1]*t)) ph = 2*np.pi*np.cumsum(f)/SR for k in range(1, nh+1): live = (f*k) < SR*0.45 if not live.any(): break out += np.sin(ph*k)/k * live return out/len(detune) def vocode(mod, car, nfft=1024, hop=256, bands=26, lo=110, hi=6500, gmax=12.0, rel=0.55, sib=0.06, tilt=4200.0): """Transfer mod's band envelope onto car. Gains are clamped and the band set is bounded — an unclamped vocoder turns carrier aliasing into hiss.""" n = max(len(mod), len(car)) mod = np.pad(mod, (0, n-len(mod))); car = np.pad(car, (0, n-len(car))) win = np.hanning(nfft); nfr = 1 + max(0, (n-nfft))//hop fr = np.fft.rfftfreq(nfft, 1/SR) edges = np.geomspace(lo, hi, bands+1) idx = [np.where((fr >= edges[b]) & (fr < edges[b+1]))[0] for b in range(bands)] keep = np.zeros(len(fr), bool) for ii in idx: keep[ii] = True out = np.zeros(n); wsum = np.zeros(n)+1e-9 prev = np.zeros(bands) for f in range(nfr): s = f*hop M = np.fft.rfft(mod[s:s+nfft]*win); C = np.fft.rfft(car[s:s+nfft]*win) am = np.abs(M); ac = np.abs(C) g = np.zeros(len(fr)) for b, ii in enumerate(idx): if not len(ii): continue em = np.sqrt((am[ii]**2).mean()); ec = np.sqrt((ac[ii]**2).mean()) gb = np.clip(em/(ec+1e-4), 0, gmax) gb = prev[b]*rel + gb*(1-rel) prev[b] = gb; g[ii] = gb out[s:s+nfft] += np.fft.irfft(C*g*keep)*win wsum[s:s+nfft] += win**2 # floor the window sum: at the ramp-in/out edges it -> 0 and the divide # detonates into a single enormous spike ws = np.maximum(wsum, 0.35*np.median(wsum[nfft:max(nfft+1, n-nfft)])) y = out/ws y[:hop] = 0.0; y[-hop:] = 0.0 y /= (np.max(np.abs(y))+1e-9) hp = np.zeros_like(mod); hp[1:] = mod[1:]-mod[:-1] for _ in range(2): hp = np.convolve(hp, [1, -0.93], "same") hp = np.clip(hp/(np.percentile(np.abs(hp), 99.5)+1e-9), -1, 1) a = math.exp(-2*math.pi*tilt/SR); z = 0.0; lp = np.empty_like(y) for i in range(len(y)): z = (1-a)*y[i] + a*z; lp[i] = z y = 0.55*y + 0.85*lp + sib*hp return y/(np.max(np.abs(y))+1e-9) def sing(text, notes, dur, voice="Moira", rate=170, cache=None, nh=30, detune=(0.0, -0.55, 0.62), vib=(0.012, 5.2), gliss=0.012, **vk): """A sung line. `notes` = [(freq, weight), …] carved across `dur` seconds.""" n = int(dur*SR) key = cache/("say_"+_h(text, voice, rate)+".wav") mod = fit(say_wav(text, voice, rate, key), n) tot = sum(w for _, w in notes) or 1.0 f = np.zeros(n); at = 0 for i, (fq, w) in enumerate(notes): ln = int(n*w/tot) if i < len(notes)-1 else n-at f[at:at+ln] = fq; at += ln if gliss: # portamento: smooth the note edges k = max(3, int(gliss*SR)); f = np.convolve(f, np.ones(k)/k, "same") f[:k] = f[k]; f[-k:] = f[-k-1] car = carrier(f, nh=nh, detune=detune, vib=vib) return vocode(mod, car, **vk) def speak(text, dur=None, voice="Alex", rate=170, cache=None, pitch=1.0): """Plain spoken line (no vocoder) — for verses that shouldn't sing.""" key = cache/("say_"+_h(text, voice, rate)+".wav") x = say_wav(text, voice, rate, key) if pitch != 1.0: x = fit(x, int(len(x)/pitch)) if dur: x = fit(x, int(dur*SR)) if len(x) > int(dur*SR) else \ np.pad(x, (0, int(dur*SR)-len(x))) return x/(np.max(np.abs(x))+1e-9) # ════════════════════════════════════════════════════════════════════════════ # THE SONG # ════════════════════════════════════════════════════════════════════════════ SW = 0.08 E1 = nf("E1") def rainbed(dur, seed=91): n = int(dur*SR); t = np.arange(n)/SR rng = np.random.RandomState(seed) hiss = bandshape(rng.randn(n), lo=1200, hi=9000)*.05 # droplet ticks drops = np.zeros(n) idx = rng.choice(n-200, size=max(1, n//2500), replace=False) tt = t[:90] for i in idx: fq = rng.uniform(2200, 4600) drops[i:i+90] += np.sin(2*np.pi*fq*tt)*np.exp(-tt*300)*rng.uniform(.2, .6) return hiss + drops*.4 def zap(seed=93): n = int(.09*SR); t = np.arange(n)/SR nz = bandshape(np.random.RandomState(seed).randn(n), lo=2200, hi=9500) am = (np.sin(2*np.pi*120*t) > 0) return nz*am*np.exp(-t*60)*.5 def thundersub(dur=2.0, seed=97): n = int(dur*SR); t = np.arange(n)/SR f = 44 + 14*np.sin(2*np.pi*.8*t) x = np.sin(2*np.pi*np.cumsum(f)/SR) return x*np.exp(-t*1.8)*np.clip(t*30, 0, 1)*.8 def squeak(seed=213): """A mouse. Two short chirps, an octave apart, very short.""" n = int(.22*SR); t = np.arange(n)/SR; out = np.zeros(n) for k2, (off, f0) in enumerate(((0.0, 2400), (0.085, 3100))): i = int(off*SR); m = n-i if m <= 0: continue tt = np.arange(m)/SR f = f0*(1 + 0.55*np.exp(-tt*40)) out[i:] += np.sin(2*np.pi*np.cumsum(f)/SR)*np.exp(-tt*46) return out*0.42 def clatter(dur=.9, seed=211): """A bin lid finding the ground.""" n = int(dur*SR); rng = np.random.RandomState(seed); out = np.zeros(n) for off, dk in ((0.0, 18), (0.11, 26), (0.19, 34), (0.26, 46), (0.32, 60)): i = int(off*SR); m = n-i if m <= 0: continue tt = np.arange(m)/SR body = sum(np.sin(2*np.pi*f*tt) for f in (410, 733, 1180, 1970, 2840)) nz = bandshape(rng.randn(m), lo=900, hi=7200) out[i:] += (body*0.18 + nz*0.5)*np.exp(-tt*dk) return out*0.30 def rattle(dur=1.1, seed=219): """Chain-link, hit at speed.""" n = int(dur*SR); rng = np.random.RandomState(seed); out = np.zeros(n) for _ in range(46): i = rng.randint(0, n-1400); m = 1200 tt = np.arange(m)/SR f = rng.uniform(1500, 5200) out[i:i+m] += np.sin(2*np.pi*f*tt)*np.exp(-tt*70)*rng.uniform(.2, 1.0) env = np.exp(-np.arange(n)/SR*3.4) return out*env*0.30 def meow(seed=99): n = int(.5*SR); t = np.arange(n)/SR f = 620 + 320*np.sin(np.pi*np.clip(t/.5, 0, 1))**1.6 - 200*(t/.5) x = np.sin(2*np.pi*np.cumsum(f)/SR)*(1+.4*np.sin(2*np.pi*31*t)) x += .5*np.sin(2*2*np.pi*np.cumsum(f)/SR) env = np.sin(np.pi*np.clip(t/.5, 0, 1))**.7 return np.tanh(x*1.2)*env*.5 def buswhoosh(dur=1.6, seed=101): n = int(dur*SR); t = np.arange(n)/SR rng = np.random.RandomState(seed) out = np.zeros(n); blk = 2048 for i in range(0, n, blk): u = i/max(1, n) env = math.sin(math.pi*u) fc = 300 + 1400*env seg = rng.randn(min(blk, n-i)+256) out[i:i+min(blk, n-i)] = bandshape(seg, lo=fc*.5, hi=fc*2.2)[:min(blk, n-i)]*env return out*.6 def build_song(): s = Song(DUR) R = np.random.RandomState(10000) def sec_of(bar): for nm, a, b in SECTIONS: if a <= bar < b: return nm, a return "dawn", SECTIONS[-1][1] # Em -> C -> G -> D... stay diatonic to E natural minor: Em C G Bm PROG = [("E2", (0, 3, 7)), ("C2", (0, 4, 7)), ("G2", (0, 4, 7)), ("B1", (0, 3, 7))] for bar in range(N_BARS): sec, sec_b0 = sec_of(bar) rel = bar - sec_b0 # section-relative bar: sections no bridge = sec == "bridge" # longer sit on 4-bar boundaries, so quiet = sec in ("intro", "dawn") # the wheel restarts on Em at tense = sec == "standoff" # every section downbeat hooky = sec in ("chase", "hook2") # the two full-riddim stretches rootn, ivs = PROG[rel % 4] rootf = nf(rootn) # ---- the standoff: everything stops but the low end --------------- if tense: s.put("drums", kick(dur=.30, f0=126, f1=46, punch=20, click=.2), s.t(bar, 0, SW), g=.72) s.kick_t.append(s.t(bar, 0, SW)) s.put("drums", rim(), s.t(bar, 8, SW), g=.34, pan=.2) for st in (5, 13): s.put("drums", shaker(), s.t(bar, st, SW), g=.16, pan=-.3) s.put("sub", voice(rootf, BEAT*3.2, kind="sine", nh=3, c0=170, c1=82, ck=2.0, a=.02, d=.4, s=.85, r=.4, seed=bar*3), s.t(bar, 0, SW), g=.46) # one long organ note per bar, climbing — nobody is moving yet iv2 = (0, 3, 7, 10)[rel % 4] s.put("pad", voice(nf("E3")*2**(iv2/12.0), BAR*1.4, kind="tri", nh=9, c0=1200, c1=520, ck=.6, detune=(-1.2, 0, 1.3), vib=(.008, 4.4), a=.5, d=.7, s=.78, r=.9, seed=bar*23), s.t(bar, 0, SW), g=.16, pan=-.1) continue # ---- dancehall kick: 3+3+2 ---------------------------------------- if not bridge: for st in (0, 3, 6, 8, 11, 14) if hooky else (0, 3, 6, 10): at = s.t(bar, st, SW) g = .92 if st in (0, 8) else .55 s.put("drums", kick(dur=.28, f0=130, f1=48, punch=22, click=.28), at, g=g if not quiet else g*.6) if st == 0: s.kick_t.append(at) s.put("drums", snare(dur=.22, tone=205, bright=1.0), s.t(bar, 8, SW), g=.6 if not quiet else .35, pan=-.05) for st in (2, 5, 10, 13): s.put("drums", rim(), s.t(bar, st, SW), g=.3, pan=.22) for st in range(1, 16, 2): s.put("drums", hat(dur=.04), s.t(bar, st, SW), g=.12+.04*R.rand(), pan=-.25+.5*R.rand()) # ---- sub ---------------------------------------------------------- if not bridge: for st, ln in ((0, 1.4), (6, .8), (8, 1.2), (14, .6)): s.put("sub", voice(rootf, BEAT*ln, kind="sine", nh=3, c0=180, c1=85, ck=2.5, a=.006, d=.2, s=.85, r=.1, seed=bar*3+st), s.t(bar, st, SW), g=.44) # ---- skank organ on the offbeats ---------------------------------- if not bridge and not quiet: for st in (2, 6, 10, 14): for k2, iv in enumerate(ivs): s.put("skank", voice(rootf*4*2**(iv/12.0), BEAT*.30, kind="square", nh=10, c0=2200, c1=900, ck=10, a=.004, d=.09, s=.3, r=.06, seed=bar*7+st+k2), s.t(bar, st, SW), g=.09, pan=-.25+.18*k2) # ---- hook melody: the VACANCY tag + horn line ---------------------- if hooky and rel % 4 == 0: tag = sing("va can cy", [(nf("E4"), 1), (nf("G4"), 1), (nf("B3"), 1.6)], BAR*0.9, voice="Moira", rate=150, cache=AUD, detune=(0.0, -0.7, 0.8), vib=(.014, 5.0)) s.put("vox", tag, s.t(bar, 4, SW), g=.5, pan=.05) low = sing("va can cy", [(nf("E3"), 1), (nf("G3"), 1), (nf("B2"), 1.6)], BAR*0.9, voice="Moira", rate=150, cache=AUD, detune=(0.0, -1.2), vib=(.008, 4.2)) s.put("vox", low, s.t(bar, 4, SW), g=.16, pan=.05) if hooky and rel % 2 == 1: MEL = [7, 10, 12, 10, 7, 5] for j in (0, 5, 8, 12): iv = MEL[(bar+j) % 6] s.put("horn", voice(nf("E3")*2**(iv/12.0), BEAT*.7, kind="saw", nh=16, c0=2100, c1=850, ck=5, res=.4, vib=(.014, 5.2), a=.03, d=.2, s=.6, r=.15, seed=bar*11+j), s.t(bar, j, SW), g=.13, pan=-.15) # ---- bridge: rain + neon buzz solo -------------------------------- if bridge: s.put("drums", rim(), s.t(bar, 8, SW), g=.35) s.put("sub", voice(rootf, BEAT*2.6, kind="sine", nh=2, c0=150, c1=80, ck=1.5, a=.05, d=.5, s=.8, r=.6, seed=bar), s.t(bar, 0, SW), g=.34) for st in (0, 6, 10): s.put("fx", zap(seed=bar*17+st), s.t(bar, st, SW), g=.4, pan=-.3+.3*(st/10)) for k2, iv in enumerate(ivs): s.put("pad", voice(rootf*2*2**(iv/12.0), BAR*1.3, kind="tri", nh=8, c0=1100, c1=520, ck=.7, detune=(-1.3, 0, 1.4), a=.6, d=.8, s=.75, r=1.0, seed=bar*13+k2), s.t(bar, 0, SW), g=.17, pan=-.4+.27*k2) # thin pad under the bookends so they aren't kick + rain alone if quiet: for k2, iv in enumerate(ivs[:2]): s.put("pad", voice(rootf*2*2**(iv/12.0), BAR*1.2, kind="tri", nh=8, c0=800, c1=420, ck=.6, detune=(-1.0, 1.1), a=.7, d=.8, s=.7, r=1.0, seed=bar*19+k2), s.t(bar, 0, SW), g=.11, pan=-.2+.2*k2) # ---- world one-shots (remapped to the 26-bar arc) ----------------------- s.put("fx", rainbed(DUR), 0.0, g=.6) s.put("fx", thundersub(seed=201), 15*BAR, g=.5) # bridge start s.put("fx", buswhoosh(seed=203), 4*BAR + BEAT, g=.5, pan=-.2) s.put("fx", buswhoosh(seed=207), 23*BAR + BEAT*2, g=.45, pan=.25) # the cast, in sound: a cat, a mouse, a bin lid and a fence s.put("fx", meow(), 0*BAR + BEAT*2, g=.36, pan=.1) # opening cameo s.put("fx", clatter(seed=211), 6*BAR + BEAT*1.5, g=.42, pan=-.15) s.put("fx", squeak(seed=213), 7*BAR + BEAT*0.5, g=.34, pan=.30) s.put("fx", meow(), 8*BAR + BEAT*2, g=.44, pan=.15) # it has seen it s.put("fx", squeak(seed=217), 9*BAR + BEAT*0.5, g=.32, pan=.36) s.put("fx", rattle(seed=219), 11*BAR, g=.40, pan=-.2) # the fence s.put("fx", meow(), 11*BAR + BEAT*1.2, g=.40, pan=-.25) # stuck s.put("fx", squeak(seed=223), 13*BAR + BEAT*2, g=.28, pan=.2) s.put("fx", squeak(seed=227), 18*BAR + BEAT*1, g=.34, pan=.3) s.put("fx", meow(), 19*BAR + BEAT*2.5, g=.30, pan=-.15) # standoff, over s.put("fx", meow(), 29*BAR + BEAT*2, g=.28, pan=-.1) # walking home for b in (9, 21): # the two hook downbeats s.put("fx", crash(dur=1.0), b*BAR, g=.18, pan=.1) for b in (28, 29, 30): # signs switching off s.put("fx", zap(seed=300+b), b*BAR, g=.4, pan=-.3+.2*(b-28)) s.bus("skank", lambda x: reverb(delay(x, BEAT*.75, .34, .24), rt=1.8, mix=.3, seed=601)) s.bus("horn", lambda x: reverb(delay(x, BEAT*.75, .3, .2), rt=2.2, mix=.34, seed=603)) s.bus("vox", lambda x: reverb(delay(x, BEAT*1.5, .4, .28), rt=2.6, mix=.4, seed=607)) s.bus("pad", lambda x: reverb(x, rt=3.4, mix=.5, seed=609)) s.bus("fx", lambda x: reverb(x, rt=1.6, mix=.2, seed=613)) mix = s.mixdown(dict(drums=1.0, sub=1.0, skank=1.0, horn=1.0, vox=1.0, pad=1.0, fx=1.0), pump_depth=.22, pump_rel=.14, levels=dict(intro=.5, riddim1=.85, spot=.74, chase=1.0, bridge=.5, standoff=.60, hook2=1.0, dawn=.5)) wav = AUD / "final.wav" s.write(wav, mix) return wav, mix def analyze(mix): x = mix.mean(1) hop = SR / FPS; win = int(hop*1.7) E = {k: np.zeros(N_FRAMES) for k in ("rms", "low", "mid", "high")} for f in range(N_FRAMES): i = int(f*hop); seg = x[i:i+win] if len(seg) < 16: continue E["rms"][f] = np.sqrt((seg**2).mean()) sp = np.abs(np.fft.rfft(seg*np.hanning(len(seg)))) fr = np.fft.rfftfreq(len(seg), 1/SR) E["low"][f] = sp[fr < 170].sum() E["mid"][f] = sp[(fr >= 170) & (fr < 2400)].sum() E["high"][f] = sp[fr >= 2400].sum() for k in E: p = np.percentile(E[k], 96) + 1e-9 E[k] = np.clip(E[k]/p, 0, 1.25) lo = E["low"] flux = np.maximum(0, lo - np.concatenate([[0], lo[:-1]])) E["kick"] = np.clip(np.convolve(flux, [.25,.5,.25], "same") / (np.percentile(flux, 97)+1e-9), 0, 1) np.savez(AUD/"env.npz", **E) return E _ENV = {} def env(): if not _ENV: z = np.load(AUD/"env.npz") for k in z.files: _ENV[k] = z[k] return _ENV # ════════════════════════════════════════════════════════════════════════════ # ANIMATION HELPERS — motion is the point, so it gets its own primitives # ════════════════════════════════════════════════════════════════════════════ def ease_io(u): return u*u*(3-2*u) def ease_out(u): return 1-(1-u)**3 def ease_in(u): return u**3 def bounce(u, n=3): return abs(math.sin(u*math.pi*n))*(1-u) def lerp(a, b, u): return a + (b-a)*u def walk(t, speed=1.0): """Returns (leg_swing, body_bob, arm_swing) for a walk cycle at time t.""" p = t*speed*math.tau return math.sin(p), abs(math.sin(p))*-1.0, math.sin(p+math.pi) def spring(u, freq=3.0, damp=5.0): """Overshoot-and-settle, for things that arrive.""" if u <= 0: return 0.0 return 1 - math.exp(-damp*u)*math.cos(freq*math.tau*u) def arc(p0, p1, u, h=0.3): """Ballistic arc between two points.""" x = lerp(p0[0], p1[0], u) y = lerp(p0[1], p1[1], u) - math.sin(u*math.pi)*h*abs(p1[0]-p0[0]) return x, y def ramp(stops, n=256): stops = np.array(stops, np.float32) xs = np.linspace(0, 1, len(stops)); g = np.linspace(0, 1, n) return np.stack([np.interp(g, xs, stops[:, c]) for c in range(3)], 1) def apply_ramp(v01, lut): i = np.clip(v01*(len(lut)-1), 0, len(lut)-1).astype(np.int32) return lut[i] def value_noise(h, w, scale, seed): rng = np.random.RandomState(seed) gh, gw = int(h/scale)+2, int(w/scale)+2 g = rng.rand(gh, gw) ys = np.linspace(0, gh-1-1e-3, h); xs = np.linspace(0, gw-1-1e-3, w) y0 = ys.astype(int); x0 = xs.astype(int) fy = (ys-y0)[:, None]; fx = (fx0 := (xs-x0))[None, :] sy = fy*fy*(3-2*fy); sx = fx*fx*(3-2*fx) g00 = g[np.ix_(y0, x0)]; g01 = g[np.ix_(y0, x0+1)] g10 = g[np.ix_(y0+1, x0)]; g11 = g[np.ix_(y0+1, x0+1)] return (g00*(1-sx)+g01*sx)*(1-sy) + (g10*(1-sx)+g11*sx)*sy def fbm(h, w, scale, seed, oct=4): out = np.zeros((h, w)); amp = 1.0; nrm = 0.0 for o in range(oct): out += amp*value_noise(h, w, max(2, scale/(2**o)), seed+o) nrm += amp; amp *= .5 return out/nrm # ════════════════════════════════════════════════════════════════════════════ # NEON ON WET ASPHALT # ════════════════════════════════════════════════════════════════════════════ NEUTRAL = {"rms": .5, "low": .4, "mid": .4, "high": .3, "kick": .2} NIGHT = (8, 10, 22) BLDG = (16, 18, 34) BLDG2 = (22, 24, 44) PINK = (255, 84, 164) CYAN = (84, 228, 255) YELL = (255, 212, 84) GRN = (124, 255, 148) ORNG = (255, 144, 64) WHT = (240, 244, 255) def dimc(c, f): return tuple(int(v*f) for v in c) def flick_on(sig, t, rate=13, p=0.92): """Deterministic neon flicker: mostly on, sometimes out.""" h = (sig*7919 + int(t*rate)*104729) % 1000 return h < p*1000 def neon_text(d, x, y, text, col, size=64, sig=0, t=0.0, fnt="Impact.ttf", broken=None): f = font(size, fnt) for i, ch in enumerate(text): on = flick_on(sig+i, t) and (broken is None or i != broken or flick_on(sig+i*3, t, 5, 0.4)) c = col if on else dimc(col, 0.13) cw = d.textlength(ch, font=f) d.text((x, y), ch, font=f, fill=c) x += cw return x def sign_motel(d, x, y, t, e, sc=1.0, no_vac=False): """MOTEL + VACANCY + running arrow chevrons. At dawn the second line flips to NO VACANCY — full house, good night.""" d.rounded_rectangle([x, y, x+360*sc, y+230*sc], 14, outline=dimc(CYAN, .8), width=4) neon_text(d, x+26*sc, y+8*sc, "MOTEL", PINK, int(74*sc), sig=1, t=t) if no_vac: nx = neon_text(d, x+26*sc, y+100*sc, "NO ", PINK, int(42*sc), sig=61, t=t) neon_text(d, nx, y+100*sc, "VACANCY", CYAN, int(42*sc), sig=11, t=t) else: neon_text(d, x+30*sc, y+96*sc, "VACANCY", CYAN, int(46*sc), sig=11, t=t, broken=4) # arrow: chevrons flow on 8th notes step = int(t/(BEAT/2)) % 5 for q in range(5): cx = x + 40*sc + q*64*sc on = (q == step) or (q == (step+1) % 5) c = YELL if on else dimc(YELL, .18) yy = y + 175*sc d.line([(cx, yy-14*sc), (cx+30*sc, yy), (cx, yy+14*sc)], fill=c, width=int(7*sc), joint="curve") def sign_cup(d, x, y, t, e, sc=1.0): """The diner cup: tips on the kick, steam rises.""" tip = -0.10*e["kick"] cx, cy = x+90*sc, y+120*sc pts = [(cx-70*sc, cy-40*sc), (cx+70*sc, cy-40*sc), (cx+52*sc, cy+50*sc), (cx-52*sc, cy+50*sc)] pts = [( (px-cx)*math.cos(tip)-(py-cy)*math.sin(tip)+cx, (px-cx)*math.sin(tip)+(py-cy)*math.cos(tip)+cy) for px, py in pts] d.line(pts + [pts[0]], fill=WHT, width=int(6*sc), joint="curve") d.arc([cx+40*sc, cy-30*sc, cx+95*sc, cy+25*sc], -60, 60, fill=WHT, width=int(6*sc)) # saucer d.line([(cx-80*sc, cy+58*sc), (cx+80*sc, cy+58*sc)], fill=WHT, width=int(6*sc)) # steam: three phase-shifted curls, gated by section energy for q in range(3): ph = t*1.8 + q*2.1 seg = int(t/BEAT) % 3 c = ORNG if q == seg else dimc(ORNG, .3) pts2 = [] for j in range(8): jj = j/7.0 pts2.append((cx - 30*sc + q*30*sc + math.sin(ph + jj*3.2)*14*sc, cy - 50*sc - jj*80*sc)) d.line(pts2, fill=c, width=int(5*sc), joint="curve") neon_text(d, x, y+190*sc, "DINER", YELL, int(44*sc), sig=21, t=t) def sign_pin(d, x, y, t, e, sc=1.0): """Bowling: ball rolls in, pin goes down, gets back up. Every 2 bars.""" ph = (t/(BAR*2)) % 1.0 px, py = x+120*sc, y+110*sc # pin: upright until hit at ph=0.5, then tilts, then springs back if ph < 0.5: ang = 0.0 elif ph < 0.72: ang = ease_out(min(1, (ph-0.5)/0.2))*1.35 else: ang = (1-spring((ph-0.72)/0.28, 2.6, 5.0))*1.35 c, s2 = math.cos(ang), math.sin(ang) def rp(ax, ay): ax, ay = ax*sc, ay*sc return (px + ax*c - ay*s2, py + ax*s2 + ay*c) pin_pts = [rp(-16, 0), rp(-10, -40), rp(-18, -70), rp(-8, -95), rp(8, -95), rp(18, -70), rp(10, -40), rp(16, 0)] d.line(pin_pts + [pin_pts[0]], fill=WHT, width=int(5*sc), joint="curve") d.line([rp(-14, -58), rp(14, -58)], fill=PINK, width=int(5*sc)) # ball: rolls across before the hit bu = np.clip(ph/0.5, 0, 1) bx = x + (20 + 160*ease_in(bu))*sc d.ellipse([bx-26*sc, py-26*sc, bx+26*sc, py+26*sc], outline=GRN if ph < 0.55 else dimc(GRN, .3), width=int(6*sc)) neon_text(d, x+8*sc, y+130*sc, "BOWL", GRN, int(46*sc), sig=31, t=t) def sign_donut(d, x, y, t, e, sc=1.0): cx, cy = x+90*sc, y+80*sc d.ellipse([cx-70*sc, cy-70*sc, cx+70*sc, cy+70*sc], outline=ORNG, width=int(8*sc)) d.ellipse([cx-26*sc, cy-26*sc, cx+26*sc, cy+26*sc], outline=ORNG, width=int(6*sc)) a0 = t*2.2 for q in range(8): a = a0 + q*math.tau/8 r = 48*sc c = [PINK, CYAN, YELL][q % 3] d.line([(cx+math.cos(a)*r-8*sc*math.sin(a), cy+math.sin(a)*r+8*sc*math.cos(a)), (cx+math.cos(a)*r+8*sc*math.sin(a), cy+math.sin(a)*r-8*sc*math.cos(a))], fill=c, width=int(5*sc)) neon_text(d, x+16*sc, y+160*sc, "DONUT", PINK, int(42*sc), sig=41, t=t) def sign_24(d, x, y, t, e, sc=1.0): pl = 0.5 + 0.5*e["kick"] c = dimc(CYAN, 0.35+0.65*pl) neon_text(d, x, y, "24HR", c, int(58*sc), sig=51, t=t) d.rounded_rectangle([x-14*sc, y-10*sc, x+164*sc, y+80*sc], 10, outline=dimc(c, .8), width=4) SIGNS = { "motel": (sign_motel, 380, 240), "cup": (sign_cup, 220, 250), "pin": (sign_pin, 240, 200), "donut": (sign_donut, 200, 210), "hr": (sign_24, 190, 100), } def draw_cat(d, cx, cy, s3, t, col=PINK, flip=False, legs=True, blink=False): """Rim-lit neon cat with a beat-quantized walk cycle. (cx, cy) = feet baseline (what the cat stands on); s3 = scale; flip=True faces left. Same silhouette language as before (dark fill, neon rim) but now it has legs, so it can *walk* — on wires, sills, and the ground.""" F = (6, 7, 14) sgn = -1 if flip else 1 beat = t/BEAT ph = (int(beat) + ease_io(min(1.0, (beat-int(beat))*1.8)))*math.pi by = cy - 26*s3 - abs(math.sin(ph))*2.5*s3 # body centre, with bob # legs first, behind the body: two pairs, opposite phase if legs: for ox, phq in ((-20, 0.0), (-13, math.pi), (16, math.pi), (23, 0.0)): sw = math.sin(ph + phq)*7*s3 hx = cx + sgn*ox*s3 lift = max(0.0, math.sin(ph + phq))*4*s3 d.line([(hx, by+6*s3), (hx + sw*sgn*0.5, cy - lift)], fill=F, width=max(2, int(5*s3))) d.line([(hx, by+6*s3), (hx + sw*sgn*0.5, cy - lift)], fill=col, width=max(1, int(1.5*s3))) # body + head d.ellipse([cx-30*s3, by-16*s3, cx+30*s3, by+10*s3], fill=F, outline=col, width=max(2, int(2*s3))) hx0 = cx + sgn*26*s3 d.ellipse([hx0-14*s3, by-34*s3, hx0+14*s3, by-8*s3], fill=F, outline=col, width=max(2, int(2*s3))) for exx in (hx0 - sgn*9*s3, hx0 + sgn*3*s3): d.polygon([(exx, by-30*s3), (exx + sgn*6*s3, by-44*s3), (exx + sgn*11*s3, by-29*s3)], fill=F, outline=col) # tail: sways on the beat tw = math.sin(t*BPM/60*math.tau)*11*s3 d.line([(cx - sgn*28*s3, by), (cx - sgn*52*s3, by-22*s3+tw)], fill=F, width=max(3, int(5*s3))) d.line([(cx - sgn*28*s3, by), (cx - sgn*52*s3, by-22*s3+tw)], fill=col, width=max(1, int(1.4*s3))) # eyes for exx in (hx0 - sgn*7*s3, hx0 + sgn*3*s3): if blink: d.line([(exx-1*s3, by-24*s3), (exx+5.5*s3, by-24*s3)], fill=YELL, width=max(1, int(1.6*s3))) else: d.ellipse([exx, by-26*s3, exx+4.5*s3, by-21.5*s3], fill=YELL) def street_scene(t, e, focus=None, dawn_u=0.0, cat_u=None, cat_g=None, cat_g_flip=False, mouse_g=None, windows=False): """Draw the street with signs onto a stage-sized canvas; returns arr. cat_u: cat on the high wire (0..1 across). cat_g: cat walking the street at ground level (0..1 across) — drawn before the reflection pass so the wet asphalt mirrors it like everything else.""" sky_top = np.array(NIGHT, np.float32) if dawn_u > 0: sky_top = sky_top + (np.array([120, 90, 80]) - sky_top)*dawn_u img, d = new_frame(tuple(int(v) for v in sky_top)) horizon = int(H*0.74) # buildings for i, (bx, bw, bh) in enumerate(((0, .3, .58), (.27, .26, .5), (.5, .3, .62), (.77, .25, .46))): c = BLDG if i % 2 else BLDG2 if dawn_u > 0: c = tuple(int(v + 30*dawn_u) for v in c) d.rectangle([W*bx, horizon-H*bh, W*(bx+bw), horizon], fill=c) # dark windows, a few lit for wy in range(3): for wx in range(5): if ((i*7+wx*3+wy) % 11) == 0 and dawn_u < 0.5: d.rectangle([W*bx+20+wx*36, horizon-H*bh+30+wy*44, W*bx+38+wx*36, horizon-H*bh+52+wy*44], fill=(70, 66, 44)) # signs on the buildings onoff = lambda idx: 1.0 if dawn_u <= 0 else (0.0 if dawn_u*5 > idx+1 else 1.0) # switch-off order recomposed: the motel goes LAST (idx 4 never trips) # so the closing image reads "NO VACANCY" instead of a lone "NO" if onoff(4): sign_motel(d, W*0.04, H*0.10, t, e, 1.0, no_vac=(dawn_u >= 0.85)) # two lit windows in the motel block: a cat downstairs, a mouse in the # penthouse. Full house — which is what the sign has just started saying. if windows: wu = float(np.clip((dawn_u-0.55)/0.30, 0, 1)) for q, (wx, wy, who) in enumerate(((W*0.055, H*0.62, "cat"), (W*0.055, H*0.44, "mouse"))): d.rectangle([wx, wy, wx+168, wy+124], fill=(int(30+186*wu), int(28+160*wu), int(20+96*wu)), outline=(24, 24, 34), width=6) d.line([(wx+84, wy), (wx+84, wy+124)], fill=(24, 24, 34), width=4) if wu > 0.35: if who == "cat": draw_cat(d, wx+92, wy+112, 1.5, t*0.12, col=(40, 30, 20), legs=False, blink=True) else: draw_mouse(d, wx+96, wy+112, 1.5, t*0.12, col=(40, 30, 20), still=True) if onoff(0): sign_cup(d, W*0.44, H*0.06, t, e, 0.62) if onoff(1): sign_pin(d, W*0.62, H*0.16, t, e, 0.8) if onoff(2): sign_donut(d, W*0.82, H*0.05, t, e, 0.62) if onoff(3): sign_24(d, W*0.35, H*0.52, t, e, 0.8) # the cat on its wire — silhouette with a neon rim so it reads on night if cat_u is not None: wy = H*0.42 d.line([0, wy+26, W, wy+12], fill=(44, 46, 68), width=4) cx = W*cat_u cy = wy + 22 - (cx/W)*14 draw_cat(d, cx, cy, 1.6, t) # the cat at street level — bigger, walking the wet asphalt; drawn # above the horizon line so the reflection pass mirrors it if cat_g is not None: draw_cat(d, W*cat_g, horizon - 3, 2.3, t, flip=cat_g_flip) # after the kerb they go everywhere together, the mouse trailing a little if mouse_g is not None: draw_mouse(d, W*mouse_g, horizon - 3, 1.5, t, flip=cat_g_flip) # street d.rectangle([0, horizon, W, H], fill=(12, 13, 24) if dawn_u < .5 else (40, 40, 52)) # reflection: flip the upper band, squash, wave, dim hz = P(horizon) # the horizon in real pixels band = np.asarray(img.crop((0, 0, RW, hz)), np.float32) ref = band[::-1][:RH-hz+P(40)] # the wobble is a wavelength and an amplitude, so both scale with S yy = np.arange(ref.shape[0])/S shift = (np.sin(yy*0.24 + t*2.4)*PF(4) + np.sin(yy*0.07 - t*1.1)*PF(7)).astype(int) for r in range(ref.shape[0]): ref[r] = np.roll(ref[r], shift[r], axis=0) fade = np.linspace(0.42, 0.06, ref.shape[0])[:, None, None] arr = np.asarray(img, np.float32) arr[hz:hz+ref.shape[0]] = \ arr[hz:hz+ref.shape[0]]*0.55 + ref[:RH-hz]*fade[:RH-hz] return arr def glow(arr, amount=0.85, radius=8): im = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8)) sm = im.resize((RW//3, RH//3), Image.BILINEAR).filter( ImageFilter.GaussianBlur(PF(radius))).resize((RW, RH), Image.BILINEAR) return np.clip(arr + np.asarray(sm, np.float32)*amount, 0, 255) def draw_mouse(d, cx, cy, s3, t, col=CYAN, flip=False, still=False): """The cat's opposite number in the same rim-lit language, at about a third of the size — the size difference is most of the joke.""" F = (6, 7, 14) sgn = -1 if flip else 1 beat = t/BEAT ph = 0.0 if still else (int(beat) + ease_io(min(1.0, (beat-int(beat))*1.8)))*math.pi by = cy - 11*s3 - abs(math.sin(ph))*1.6*s3 for ox, phq in ((-8, 0.0), (7, math.pi)): sw = 0.0 if still else math.sin(ph+phq)*4*s3 hx = cx + sgn*ox*s3 d.line([(hx, by+4*s3), (hx+sw*sgn*0.5, cy)], fill=F, width=max(2, int(3*s3))) d.line([(hx, by+4*s3), (hx+sw*sgn*0.5, cy)], fill=col, width=max(1, int(1.2*s3))) d.ellipse([cx-16*s3, by-9*s3, cx+16*s3, by+8*s3], fill=F, outline=col, width=max(2, int(2*s3))) hx0 = cx + sgn*14*s3 d.ellipse([hx0-10*s3, by-16*s3, hx0+10*s3, by-1*s3], fill=F, outline=col, width=max(2, int(2*s3))) for exo in (-6, 3): # big round ears exx = hx0 + sgn*exo*s3; r = 7.5*s3 d.ellipse([exx-r, by-23*s3, exx+r, by-23*s3+r*2], fill=F, outline=col, width=max(2, int(2*s3))) d.ellipse([hx0+sgn*8*s3-2.5*s3, by-8*s3, hx0+sgn*8*s3+2.5*s3, by-3*s3], fill=PINK) d.ellipse([hx0+sgn*1*s3, by-12*s3, hx0+sgn*1*s3+3.5*s3, by-8.5*s3], fill=YELL) tw = math.sin(t*BPM/60*math.tau + 1.1)*10*s3 d.line([(cx-sgn*15*s3, by+3*s3), (cx-sgn*32*s3, by-3*s3+tw), (cx-sgn*44*s3, by-17*s3+tw*0.4)], fill=col, width=max(1, int(1.8*s3)), joint="curve") def _brickwall(d, y0, y1, off=0, base=(20, 21, 40), mortar=(11, 12, 24)): for y in range(y0, y1, 44): row = (y//44) % 2 for x in range(-260, W+260, 124): bx = x - off + (62 if row else 0) d.rectangle([bx, y, bx+114, y+36], fill=base, outline=mortar, width=2) def bins_scene(t, e, su): """Behind the diner. Three bins, a spilled carton, a caged bulb, and something small that should not be there.""" img, d = new_frame((7, 8, 18)) ground = int(H*0.80) d.rectangle([0, 0, W, ground], fill=(18, 19, 36)) _brickwall(d, 30, ground, base=(30, 30, 54), mortar=(14, 15, 30)) for q in range(7): # DINER neon spill r = 70 + q*54 d.ellipse([W-30-r, H*0.14-r*0.7, W-30+r, H*0.14+r*0.7], fill=(int(20+7*(7-q)), int(11+3*(7-q)), int(8+2*(7-q)))) dx = W*0.06 d.rectangle([dx, H*0.20, dx+196, ground], fill=(11, 12, 24), outline=(44, 48, 70), width=4) d.rectangle([dx+20, H*0.26, dx+156, H*0.42], fill=(9, 10, 20), outline=(34, 38, 56), width=3) bx, by = dx+98, H*0.165 on = flick_on(9, t, 7, 0.93) if on: d.polygon([(bx-18, by+12), (bx+18, by+12), (bx+190, ground), (bx-190, ground)], fill=(32, 28, 16)) d.ellipse([bx-17, by-17, bx+17, by+17], fill=YELL if on else dimc(YELL, .18)) for q in range(5): a = math.pi + q*math.pi/4 d.line([(bx, by), (bx+math.cos(a)*22, by+math.sin(a)*22)], fill=(52, 56, 78), width=2) for q, (px, hh) in enumerate(((W*0.32, 156), (W*0.45, 186), (W*0.575, 144))): lid = -9 if (q == 1 and 0.10 < su < 0.34) else 0 d.rectangle([px, ground-hh, px+118, ground], fill=(16, 17, 33), outline=(92, 98, 128), width=3) for ry in range(ground-hh+24, ground-16, 28): d.line([(px+8, ry), (px+110, ry)], fill=(44, 48, 72), width=2) d.rectangle([px-9, ground-hh-15+lid, px+127, ground-hh+lid], fill=(22, 23, 42), outline=(118, 126, 158), width=3) d.rectangle([W*0.72, ground-96, W*0.72+128, ground], fill=(16, 17, 32), outline=(66, 60, 42), width=3) d.line([(W*0.72, ground-52), (W*0.72+128, ground-52)], fill=(66, 60, 42), width=3) cxc = W*0.655 d.polygon([(cxc, ground), (cxc+62, ground), (cxc+53, ground-44), (cxc+9, ground-44)], fill=(26, 22, 15), outline=ORNG) for q in range(7): px2 = cxc - 46 + q*24 + (q % 3)*8 d.line([(px2, ground-6-(q % 2)*6), (px2+18, ground-10-(q % 2)*6)], fill=YELL, width=4) d.rectangle([0, ground, W, H], fill=(11, 12, 22)) d.ellipse([W*0.20, ground+24, W*0.54, ground+66], fill=(19, 23, 42)) if su > 0.05: # the mouse mu = min(1.0, (su-0.05)/0.24) mx = lerp(W*0.49, W*0.645, ease_out(mu)) draw_mouse(d, mx, ground+4, 2.4, t, col=CYAN, still=(mu >= 1.0 and su < 0.58)) if su > 0.18: # the cat cu = min(1.0, (su-0.18)/0.26) draw_cat(d, lerp(-220, W*0.20, ease_out(cu)), ground+4, 3.4, t, col=PINK) return np.asarray(img, np.float32) ALLEY_PROPS = [(0, "door"), (300, "pipe"), (520, "esc"), (860, "dump"), (1120, "grate"), (1320, "pipe"), (1560, "door"), (1840, "esc"), (2160, "dump"), (2400, "grate"), (2620, "pipe"), (2860, "door"), (3120, "esc"), (3420, "dump")] ALLEY_W = 3660 def alley_scene(t, e, scroll, pair=True): """A side-scrolling alley. The set moves, they don't — which is what a chase looks like from the side.""" img, d = new_frame((6, 7, 16)) ground = int(H*0.82) d.rectangle([0, 0, W, ground], fill=(16, 17, 32)) _brickwall(d, -10, ground, off=int(scroll) % 124, base=(28, 29, 52), mortar=(13, 14, 28)) for px, kind in ALLEY_PROPS: x = (px - scroll) % ALLEY_W if x > W + 380: x -= ALLEY_W if x < -420: continue if kind == "door": d.rectangle([x, H*0.30, x+150, ground], fill=(10, 11, 22), outline=(46, 50, 72), width=4) lx, ly = x+75, H*0.255 on = flick_on(int(px) % 97, t, 6, 0.9) if on: d.polygon([(lx-16, ly+10), (lx+16, ly+10), (lx+140, ground), (lx-140, ground)], fill=(30, 26, 15)) d.ellipse([lx-15, ly-15, lx+15, ly+15], fill=ORNG if on else dimc(ORNG, .2)) elif kind == "pipe": d.rectangle([x, 0, x+22, ground], fill=(17, 18, 34), outline=(40, 44, 64), width=3) for yy in range(60, ground, 130): d.rectangle([x-8, yy, x+30, yy+14], fill=(24, 26, 44)) elif kind == "esc": d.rectangle([x, H*0.24, x+240, H*0.28], fill=(20, 22, 40), outline=(58, 62, 88), width=3) for q in range(9): d.line([(x+14+q*26, H*0.10), (x+14+q*26, H*0.24)], fill=(46, 50, 72), width=3) d.line([(x+10, H*0.10), (x+230, H*0.10)], fill=(46, 50, 72), width=3) for q in range(7): d.line([(x+186, H*0.28+q*22), (x+232, H*0.28+q*22)], fill=(52, 56, 80), width=3) elif kind == "dump": d.polygon([(x, ground), (x+230, ground), (x+214, ground-116), (x+16, ground-116)], fill=(12, 13, 26), outline=(62, 66, 92)) d.rectangle([x+4, ground-130, x+226, ground-112], fill=(19, 20, 38), outline=(78, 84, 112), width=3) d.line([(x+40, ground-100), (x+40, ground-10)], fill=GRN, width=2) else: # grate + steam d.rectangle([x, ground+8, x+150, ground+30], fill=(18, 20, 34)) for q in range(6): d.line([(x+12+q*24, ground+8), (x+12+q*24, ground+30)], fill=(8, 9, 18), width=4) for q in range(4): ph2 = t*1.4 + q*1.9 + px pts = [(x+30+q*30 + math.sin(ph2 + j*0.7)*16, ground - j*36) for j in range(7)] d.line(pts, fill=(30, 34, 52), width=int(9-q), joint="curve") d.rectangle([0, ground, W, H], fill=(12, 13, 24)) for q in range(6): sx = ((q*400 - scroll*0.55) % (W+700)) - 350 d.ellipse([sx, ground+16+q*11, sx+330, ground+40+q*11], fill=(26, 32, 54)) for q in range(16): # speed streaks sy = H*0.16 + ((q*83 + int(scroll*1.6)) % int(H*0.62)) sx = W - ((q*211 + int(scroll*3.1)) % (W+400)) d.line([(sx, sy), (sx+150, sy)], fill=(38, 44, 70), width=2) if pair: draw_mouse(d, W*0.745, ground+3, 2.4, t, col=CYAN) draw_cat(d, W*0.300, ground+3, 3.6, t, col=PINK) return np.asarray(img, np.float32) def fence_scene(t, e, su): """Chain-link. The mouse goes through it. The cat does not.""" img, d = new_frame((8, 9, 20)) ground = int(H*0.84) for q in range(9): # city glow behind r = 200 + q*90 d.ellipse([W*0.72-r, H*0.42-r*0.5, W*0.72+r, H*0.42+r*0.5], fill=(int(12+2*(9-q)), int(11+2*(9-q)), int(24+3*(9-q)))) for bx, bw2, bh2 in ((0.02, .22, .40), (.26, .16, .30), (.46, .2, .46), (.70, .18, .34), (.88, .16, .42)): d.rectangle([W*bx, ground-H*bh2, W*(bx+bw2), ground], fill=(12, 13, 26)) for wy in range(3): for wx in range(4): if ((int(bx*100)+wx*5+wy*3) % 7) == 0: d.rectangle([W*bx+22+wx*40, ground-H*bh2+28+wy*46, W*bx+42+wx*40, ground-H*bh2+52+wy*46], fill=(64, 58, 36)) d.rectangle([0, ground, W, H], fill=(10, 11, 21)) shake = math.sin(t*46)*7*max(0.0, 1.0 - abs(su-0.56)*9) # the hit HOLE = (W*0.60, ground-108, W*0.72, ground) def in_hole(px, py): return HOLE[0] < px < HOLE[2] and HOLE[1] < py < HOLE[3] top = int(H*0.10) for x in range(-int(H), W+int(H), 26): # the mesh for a in (1, -1): x0 = x + shake; y0 = top x1 = x + a*(ground-top) + shake; y1 = ground steps = 16 for j in range(steps): u0, u1 = j/steps, (j+1)/steps px0, py0 = lerp(x0, x1, u0), lerp(y0, y1, u0) px1, py1 = lerp(x0, x1, u1), lerp(y0, y1, u1) if in_hole(px0, py0) or in_hole(px1, py1): continue d.line([(px0, py0), (px1, py1)], fill=(58, 64, 88), width=2) d.line([(0, top+shake), (W, top+shake)], fill=(80, 86, 112), width=6) for x in range(60, W, 300): d.rectangle([x+shake-6, top, x+shake+6, ground+10], fill=(46, 50, 72)) d.line([(HOLE[0], HOLE[3]), (HOLE[0]+18, HOLE[1]), (HOLE[2]-10, HOLE[1]+22), (HOLE[2], HOLE[3])], fill=(96, 102, 128), width=4, joint="curve") if su < 0.52: # mouse goes through mu = min(1.0, su/0.46) draw_mouse(d, lerp(W*0.40, W*0.86, ease_io(mu)), ground+2, 2.4, t, col=CYAN) else: draw_mouse(d, W*0.88 + math.sin(t*2)*4, ground+2, 2.4, t, col=CYAN, still=True) if su > 0.34: # cat arrives, sticks cu = min(1.0, (su-0.34)/0.20) cx = lerp(-200, W*0.46, ease_out(cu)) squash = max(0.0, 1.0 - abs(su-0.58)*7) draw_cat(d, cx, ground+2 - squash*10, 3.8, t, col=PINK) if su > 0.60: # paws in the links for px in (cx+72, cx+118): d.ellipse([px-17, ground-208, px+17, ground-174], fill=(6, 7, 14), outline=PINK, width=3) return np.asarray(img, np.float32) def laundro_scene(t, e, su): """24-hour laundromat. Five drums turning on the beat, one door open.""" img, d = new_frame((10, 12, 18)) ground = int(H*0.86) d.rectangle([0, 0, W, ground], fill=(24, 34, 32)) for x in range(0, W, 120): # tiles d.line([(x, 0), (x, ground)], fill=(20, 29, 27), width=2) for y in range(60, ground, 90): d.line([(0, y), (W, y)], fill=(20, 29, 27), width=2) for q in range(4): # fluorescent tubes fx = 150 + q*330 on = flick_on(200+q, t, 9, 0.94) c = (198, 236, 210) if on else (60, 76, 66) d.rectangle([fx-110, H*0.06, fx+110, H*0.10], fill=c) if on: d.polygon([(fx-110, H*0.10), (fx+110, H*0.10), (fx+230, ground), (fx-230, ground)], fill=(30, 44, 40)) d.rectangle([0, H*0.44, W, H*0.50], fill=(34, 46, 44)) # counter for q in range(5): mx = 60 + q*250 d.rectangle([mx, H*0.50, mx+210, ground], fill=(34, 48, 46), outline=(70, 96, 88), width=3) d.rectangle([mx+14, H*0.515, mx+196, H*0.555], fill=(20, 30, 30)) cx2, cy2 = mx+105, H*0.70 r = 76 d.ellipse([cx2-r, cy2-r, cx2+r, cy2+r], fill=(14, 20, 22), outline=(96, 128, 118), width=6) spin = (t*(2.0 + 0.4*q) + q)*math.tau*0.5 for j in range(3): a = spin + j*math.tau/3 rr = r*0.52 d.ellipse([cx2+math.cos(a)*rr-20, cy2+math.sin(a)*rr-16, cx2+math.cos(a)*rr+20, cy2+math.sin(a)*rr+16], fill=[(180, 90, 120), (90, 150, 190), (200, 190, 110)][j]) d.arc([cx2-r+10, cy2-r+10, cx2+r-10, cy2+r-10], 200, 320, fill=(160, 200, 190), width=4) d.rectangle([0, ground, W, H], fill=(20, 28, 28)) d.rectangle([W*0.80, ground-60, W*0.80+150, ground], fill=(40, 54, 52), outline=(80, 106, 98), width=3) if su > 0.10: # they cross, fast mu = min(1.0, (su-0.10)/0.60) draw_mouse(d, lerp(W*1.08, -W*0.08, mu), ground+2, 2.3, t, col=CYAN, flip=True) if su > 0.22: cu = min(1.0, (su-0.22)/0.62) draw_cat(d, lerp(W*1.16, -W*0.14, cu), ground+2, 3.5, t, col=PINK, flip=True) return np.asarray(img, np.float32) def standoff_scene(t, e, su): """The kerb. He is enormous and she is on a bin lid, so they are the same height, which turns out to matter.""" img, d = new_frame((7, 8, 18)) kerb = int(H*0.78) for i, (bx, bw2, bh2) in enumerate(((0.0, .32, .5), (.28, .22, .38), (.52, .28, .56), (.78, .26, .42))): d.rectangle([W*bx, kerb-H*bh2, W*(bx+bw2), kerb], fill=(12, 13, 26)) for q in range(11): # blurred neon behind cxq = W*(0.08 + (q*0.093) % 0.92); cyq = H*(0.14 + ((q*7) % 5)*0.055) r = 46 + (q % 4)*26 c = [PINK, CYAN, YELL, GRN, ORNG][q % 5] d.ellipse([cxq-r, cyq-r, cxq+r, cyq+r], fill=dimc(c, 0.10 + 0.03*(q % 3))) lx = W*0.20 d.rectangle([lx-7, 0, lx+7, kerb], fill=(20, 22, 38)) d.ellipse([lx-40, H*0.10, lx+40, H*0.16], fill=YELL) d.polygon([(lx-40, H*0.14), (lx+40, H*0.14), (lx+330, kerb), (lx-330, kerb)], fill=(30, 28, 18)) d.rectangle([0, kerb, W, kerb+22], fill=(24, 26, 42)) d.rectangle([0, kerb+22, W, H], fill=(11, 12, 22)) d.ellipse([W*0.30, kerb+40, W*0.78, kerb+96], fill=(18, 22, 40)) # the mouse's platform: an upturned bin lid on a crate px = W*0.735 d.rectangle([px-72, kerb-96, px+72, kerb], fill=(14, 15, 30), outline=(58, 62, 88), width=3) d.ellipse([px-92, kerb-118, px+92, kerb-84], fill=(20, 21, 40), outline=(84, 90, 118), width=3) top = kerb-104 # the chip changes hands at the midpoint if su < 0.60: chx = lerp(px-44, W*0.50, ease_io(min(1.0, su/0.60))) d.line([(chx, top-6), (chx+26, top-12)], fill=YELL, width=7) tense = max(0.0, 1.0 - su*2.4) sit = su > 0.66 draw_mouse(d, px + math.sin(t*1.3)*3, top, 3.4, t, col=CYAN, flip=True, still=(su > 0.22)) draw_cat(d, W*0.26, kerb + 4, 4.6, t, col=PINK, legs=not sit, blink=sit) if su > 0.72: # he has the chip d.line([(W*0.26+96, kerb-118), (W*0.26+124, kerb-126)], fill=YELL, width=7) if tense > 0.05: # tail lashing for q in range(3): d.line([(W*0.26-130 - q*26, kerb-40 + math.sin(t*7+q)*26*tense), (W*0.26-160 - q*26, kerb-70 + math.sin(t*7+q+1)*26*tense)], fill=dimc(PINK, 0.35*tense), width=3) return np.asarray(img, np.float32) class Bins: def __init__(self, shot, rng): self.i0, self.sec = shot.i0, shot.section def frame(self, k, u, e): t = (self.i0+k)/FPS return glow(bins_scene(t, e, sec_u(self.sec, t)), 0.70+0.4*e["rms"], 8) class Alley: def __init__(self, shot, rng): self.i0 = shot.i0 self.s0 = float(rng.uniform(0, ALLEY_W)) def frame(self, k, u, e): t = (self.i0+k)/FPS return glow(alley_scene(t, e, self.s0 + (t - self.i0/FPS)*1150.0), 0.70+0.4*e["rms"], 8) class Fence: def __init__(self, shot, rng): self.i0, self.sec = shot.i0, shot.section def frame(self, k, u, e): t = (self.i0+k)/FPS su = float(np.clip((t/BAR - 10.4)/2.4, 0, 1)) # its own beat in chase return glow(fence_scene(t, e, su), 0.66+0.4*e["rms"], 8) class Laundro: def __init__(self, shot, rng): self.i0 = shot.i0 def frame(self, k, u, e): t = (self.i0+k)/FPS return glow(laundro_scene(t, e, u), 0.42+0.3*e["rms"], 7) class Standoff: def __init__(self, shot, rng): self.i0, self.sec = shot.i0, shot.section def frame(self, k, u, e): t = (self.i0+k)/FPS return glow(standoff_scene(t, e, sec_u(self.sec, t)), 0.72+0.4*e["rms"], 9) class Street: def __init__(self, shot, rng): self.rng = rng; self.i0 = shot.i0 # every street shot gets an event: a bus passes, or the cat # crosses at ground level (usually the cat — Gene's note) r = rng.random() self.bus = r < 0.35 self.catg = not self.bus self.cat_flip = rng.random() < 0.5 self.cat_d0 = float(rng.uniform(0.05, 0.25)) # after the kerb they travel as a pair self.pair = shot.section in ("hook2", "dawn") def frame(self, k, u, e): t = (self.i0+k)/FPS cg = None if self.catg: beat = t/BEAT adv = (int(beat) + ease_io(min(1, (beat-int(beat))*1.8)))*0.045 cg = (self.cat_d0 + adv) % 1.2 if self.cat_flip: cg = 1.1 - cg cg = cg if -0.1 < cg < 1.1 else None mg = None if cg is not None and self.pair: mg = cg + (0.062 if self.cat_flip else -0.062) arr = street_scene(t, e, cat_g=cg, cat_g_flip=self.cat_flip, mouse_g=mg) if self.bus and 0.3 < u < 0.75: bu = (u-0.3)/0.45 im = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8)) d = mkdraw(im) bx = W*(1.3 - bu*1.7) d.rectangle([bx, H*0.62, bx+W*0.34, H*0.74], outline=(60, 62, 84), width=4) for q in range(4): d.rectangle([bx+18+q*88, H*0.64, bx+70+q*88, H*0.69], fill=(84, 80, 60)) d.ellipse([bx-30, H*0.655, bx-2, H*0.685], fill=YELL) arr = np.asarray(im, np.float32) return glow(arr, 0.75+0.4*e["rms"], 8) class SignSolo: ORDER = ["motel", "cup", "pin", "donut"] def __init__(self, shot, rng): self.rng = rng; self.i0 = shot.i0 # rotate by shot index so successive solos never repeat a sign # (idx//2 because solos land on even strides in this cut) self.which = self.ORDER[(shot.idx//2 + 2) % len(self.ORDER)] def frame(self, k, u, e): t = (self.i0+k)/FPS img, d = new_frame(NIGHT) d.rectangle([W*0.08, H*0.06, W*0.92, H*0.9], fill=BLDG) fn, sw2, sh2 = SIGNS[self.which] sc = min(W*0.6/sw2, H*0.6/sh2) fn(d, W/2 - sw2*sc/2, H/2 - sh2*sc/2, t, e, sc) arr = np.asarray(img, np.float32) return glow(arr, 0.9+0.5*e["rms"], 10) class Cat: """The cat as protagonist: walks the high wire OR the street itself. Ground mode is weighted up per Gene's note — the cat belongs to the asphalt as much as to the ledges.""" def __init__(self, shot, rng): self.rng = rng; self.i0 = shot.i0 self.mode = "ground" if rng.random() < 0.6 else "wire" if shot.section == "intro": self.mode = "ground" # the opening image: self.flip = rng.random() < 0.5 # cat crosses the street self.pair = shot.section in ("hook2", "dawn") # one full crossing per shot: beat-quantized steps sized so the # walk spans the frame across the shot's own length (midpoint of # every cat shot has the cat near centre — no edge-hugging) self.nb = max(1.0, shot.n/FPS/BEAT) def frame(self, k, u, e): t = (self.i0+k)/FPS lb = (k/FPS)/BEAT adv = int(lb) + ease_io(min(1, (lb-int(lb))*1.8)) cu = 0.06 + adv*(0.88/self.nb) if self.flip: cu = 1.0 - cu if self.mode == "wire": arr = street_scene(t, e, cat_u=cu) else: mg = (cu + (0.062 if self.flip else -0.062)) if self.pair else None arr = street_scene(t, e, cat_g=cu, cat_g_flip=self.flip, mouse_g=mg) return glow(arr, 0.75+0.4*e["rms"], 8) class Rainglass: """Bridge: through a window — drops run down, neon bokeh behind.""" def __init__(self, shot, rng): self.rng = rng; self.i0 = shot.i0 R = np.random.RandomState(int(rng.integers(1e6))) self.drops = R.random((26, 3)) self.bok = R.random((14, 3)) def frame(self, k, u, e): t = (self.i0+k)/FPS img, d = new_frame(NIGHT) cols = [PINK, CYAN, YELL, GRN, ORNG] for i, (bx, by, bs) in enumerate(self.bok): r = 40 + bs*90 pl = 0.35 + 0.3*abs(math.sin(t*1.5 + i)) + 0.3*e["kick"] c = dimc(cols[i % 5], 0.25*pl) d.ellipse([bx*W-r, by*H*0.8-r, bx*W+r, by*H*0.8+r], fill=c) arr = np.asarray(img, np.float32) im2 = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8)).filter( ImageFilter.GaussianBlur(PF(9))) d2 = mkdraw(im2) # drops tracing down, refracting bright for i, (dx, dy, ds) in enumerate(self.drops): yy = ((dy + t*0.06*(0.4+ds)) % 1.1)*H xx = dx*W + math.sin(yy*0.02 + i)*10 r = 4 + ds*7 d2.ellipse([xx-r, yy-r*1.4, xx+r, yy+r*1.4], fill=(150, 160, 190), outline=(210, 220, 240), width=2) d2.line([(xx, yy-24-ds*30), (xx, yy-r)], fill=(90, 100, 130), width=int(r*0.8)) # the windowsill, and the cat padding along it in front of the # glass — crisp silhouette against the blurred bokeh (Gene's note: # the cat keeps its ledges/sills, plus the ground elsewhere) sy = H*0.88 d2.line([(0, sy), (W, sy)], fill=(30, 32, 50), width=10) d2.line([(0, sy-5), (W, sy-5)], fill=dimc(CYAN, .35), width=2) draw_cat(d2, W*(0.14 + 0.72*u), sy - 4, 1.5, t, col=dimc(PINK, .8)) # it is not alone on the sill any more draw_mouse(d2, W*(0.14 + 0.72*u) - 96, sy - 4, 1.1, t, col=dimc(CYAN, .8)) return np.asarray(im2, np.float32) class Dawn: def __init__(self, shot, rng): self.rng = rng; self.i0 = shot.i0 self.t0 = SECTIONS[-1][1]*BAR # dawn section start self.span = (SECTIONS[-1][2] - SECTIONS[-1][1])*BAR def frame(self, k, u, e): t = (self.i0+k)/FPS du = np.clip((t - self.t0)/self.span, 0, 1) # the cat walks home along the ground as the signs go out — # beat-quantized steps like everywhere else lb = (k/FPS)/BEAT adv = int(lb) + ease_io(min(1.0, (lb-int(lb))*1.8)) cg = 0.92 - adv*0.058 arr = street_scene(t, e, dawn_u=du, windows=True, cat_g=cg if cg > -0.08 else None, cat_g_flip=True, mouse_g=(cg + 0.062) if cg > -0.08 else None) return glow(arr, 0.7*(1-du*0.7), 8) ENGINES = {"street": Street, "signsolo": SignSolo, "cat": Cat, "rainglass": Rainglass, "dawn": Dawn, "bins": Bins, "alley": Alley, "fence": Fence, "laundro": Laundro, "standoff": Standoff} PLAN = { "intro": (["cat"], [8]), "riddim1": (["street", "signsolo", "cat"], [8, 8, 16]), "spot": (["bins", "bins", "street"], [6, 8]), "chase": (["alley", "fence", "laundro", "alley"], [6, 6, 8]), "bridge": (["rainglass"], [8]), "standoff": (["standoff"], [8, 12]), "hook2": (["street", "cat", "signsolo", "alley"], [8, 8, 12]), "dawn": (["dawn"], [16]), } CARDS = {"intro": "VACANCY", "riddim1": None, "spot": None, "chase": None, "bridge": None, "standoff": None, "hook2": None, "dawn": None} SYSTEM_NAMES = ["MOTEL", "DINER", "BOWL", "DONUT", "24HR", "NO VACANCY"] class Shot: __slots__ = ("idx", "i0", "i1", "n", "engine", "section", "seed", "text", "card") def __init__(self, idx, i0, i1, engine, section, text=None, card=None): self.idx, self.i0, self.i1 = idx, i0, i1 self.n = i1 - i0 self.engine, self.section = engine, section self.seed = 90210 + idx*7919 self.text, self.card = text, card def build_shots(): """Deterministic, but not a cycle: each section draws from its pool with no immediate repeats, and shot lengths come from a menu so the cut rhythm breathes instead of ticking.""" R = np.random.RandomState(5150) shots = []; idx = 0; last = None for nm, b0, b1 in SECTIONS: engs, menu = PLAN[nm] t = b0*BAR; j = 0 while t < b1*BAR - 1e-6: step = menu[R.randint(len(menu))]*BEAT t2 = min(t+step, b1*BAR) if (b1*BAR - t2) < BEAT*1.5: t2 = b1*BAR # no orphan sliver i0, i1 = int(t*FPS), int(t2*FPS) if i1 > i0: # rotate through the section's engine list instead of random # pool draws — the random version starved the sign solos and # produced runs of near-identical street shots eng = engs[j % len(engs)] if eng == last and len(engs) > 1: eng = engs[(j+1) % len(engs)] last = eng txt = [SYSTEM_NAMES[(idx+q) % len(SYSTEM_NAMES)] for q in range(2)] shots.append(Shot(idx, i0, i1, eng, nm, txt, CARDS[nm] if j == 0 else None)) idx += 1; j += 1 t = t2 if shots: shots[-1].i1 = N_FRAMES; shots[-1].n = N_FRAMES - shots[-1].i0 return shots # ── portable font resolution (cross-platform; replaces the repo-only lookup) ── import warnings as _warnings _FONT_ALIASES = { "Menlo.ttc": ["Menlo.ttc", "DejaVuSansMono.ttf", "consola.ttf", "LiberationMono-Regular.ttf"], "Georgia.ttf": ["Georgia.ttf", "georgia.ttf", "DejaVuSerif.ttf", "LiberationSerif-Regular.ttf"], "Georgia Bold.ttf": ["Georgia Bold.ttf", "georgiab.ttf", "DejaVuSerif-Bold.ttf", "LiberationSerif-Bold.ttf"], "Georgia Italic.ttf": ["Georgia Italic.ttf", "georgiai.ttf", "DejaVuSerif-Italic.ttf", "LiberationSerif-Italic.ttf"], "Impact.ttf": ["Impact.ttf", "impact.ttf", "Anton-Regular.ttf", "DejaVuSans-Bold.ttf"], "Helvetica.ttc": ["Helvetica.ttc", "arial.ttf", "Arial.ttf", "DejaVuSans.ttf", "LiberationSans-Regular.ttf"], } def _font_dirs(): here = Path(__file__).resolve() dirs = [here.parent / "fonts"] + [p / "fonts" for p in list(here.parents)[1:4]] try: home = Path.home() except Exception: home = None dirs += [Path("/System/Library/Fonts"), Path("/System/Library/Fonts/Supplemental"), Path("/Library/Fonts"), Path("C:/Windows/Fonts"), Path("/usr/share/fonts"), Path("/usr/local/share/fonts")] if home: dirs += [home / "Library/Fonts", home / ".fonts", home / ".local/share/fonts"] return dirs _FONT_DIRS = _font_dirs() _FF = {} def _find_font(name): """Path of a usable font file for `name`, or None. Cached per name.""" if name in _FF: return _FF[name] found = None for cand in _FONT_ALIASES.get(name, [name]): for d in _FONT_DIRS: if not d.is_dir(): continue p = d / cand if p.is_file(): found = p; break try: found = next(iter(d.rglob(cand)), None) except OSError: found = None if found: break if found: break if found is None: _warnings.warn(f"font {name} not found in fonts/ or system font dirs; " f"using Pillow default (layout will differ)") _FF[name] = found return found def _load_font(p, size): """ImageFont for path `p` (from _find_font) at `size`; Pillow default if p is None.""" if p is None: try: return ImageFont.load_default(size=int(size)) except TypeError: return ImageFont.load_default() return ImageFont.truetype(str(p), size) _FC = {} def font(size, name="Menlo.ttc"): key = (size, name) if key not in _FC: p = _find_font(name) # Scaled ONCE, here. Call sites always pass authoring sizes. _FC[key] = _load_font(p, max(1, P(size))) return _FC[key] _VIG = {} def vignette(): if "v" not in _VIG: yy, xx = np.mgrid[0:RH, 0:RW] nx = (xx-RW/2)/(RW/2); ny = (yy-RH/2)/(RH/2) r = np.sqrt(nx**2+ny**2)/1.42 _VIG["v"] = np.clip(1.0-0.40*r**2.2, 0, 1)[..., None] return _VIG["v"] def post(arr, i, e, shot): a = arr.astype(np.float32) if isinstance(arr, np.ndarray) else \ np.asarray(arr, np.float32) # rain streaks in delivery space im = Image.fromarray(np.clip(a, 0, 255).astype(np.uint8)) d = mkdraw(im) # rain is authored in delivery units rng = np.random.RandomState(200 + (i % 6)) nr = 60 # the same rain, photographed larger for q in range(nr): rx = rng.uniform(0, W); ry = rng.uniform(0, H) ln = rng.uniform(10, 26) d.line([rx, ry, rx-3, ry+ln], fill=(70, 80, 110), width=1) a = np.asarray(im, np.float32) a *= vignette() rng2 = np.random.RandomState(9600 + i) if S == 1.0: a += rng2.normal(0, 2.6, a.shape) else: # Grain is a look, not a resolution: drawn at 1280x720 and blown up # nearest-neighbour so a speck covers the same fraction of the frame. gn = rng2.normal(0, 2.6, (H, W, 3))*8.0 + 128.0 gi = Image.fromarray(np.clip(gn, 0, 255).astype(np.uint8)) a += (np.asarray(gi.resize((RW, RH), Image.NEAREST), np.float32) - 128.0)/8.0 # the interlace hum: one authoring scanline, so its period scales too a *= (0.86 + 0.14*np.cos(np.arange(RH)/S*math.pi))[:, None, None] out = Image.fromarray(np.clip(a, 0, 255).astype(np.uint8)) d = ImageDraw.Draw(out) # type is composited in real pixels if shot.card: # THE TITLE MOMENT. The card is a neon sign with bad tubes, so the # show name is another one — smaller, hung under it, dropping out on # its own seeds. Glitch around the words, never through them. age = i - shot.i0 if age < FPS*3.2: al = min(1.0, age/6.0)*min(1.0, (FPS*3.2-age)/12.0) f = font(58, "Impact.ttf") lw = d.textlength(shot.card, font=f) x = RW/2 - lw/2 for ch_i, ch in enumerate(shot.card): on = flick_on(70+ch_i, i/FPS) d.text((x, RH*0.40), ch, font=f, fill=tuple(int(v*al*(1 if on else .15)) for v in CYAN)) x += d.textlength(ch, font=f) fs = font(20, "Impact.ttf") tr = PF(9.0) sw2 = sum(d.textlength(c, font=fs) + tr for c in SUBT) - tr x = RW/2 - sw2/2; sy = RH*0.40 + PF(64) d.line([RW/2-sw2/2-PF(16), sy-PF(11), RW/2+sw2/2+PF(16), sy-PF(11)], fill=tuple(int(v*al*0.45) for v in PINK), width=max(2, P(2))) for ch_i, ch in enumerate(SUBT): on = flick_on(210+ch_i, i/FPS) d.text((x, sy), ch, font=fs, fill=tuple(int(v*al*(1 if on else .12)) for v in PINK)) x += d.textlength(ch, font=fs) + tr return out def render_shot(job): shot, force = job E = env() rng = np.random.default_rng(shot.seed) eng = ENGINES[shot.engine](shot, rng) made = 0 for k in range(shot.n): i = shot.i0 + k e = {kk: float(E[kk][min(i, N_FRAMES-1)]) for kk in E} p = FRAMES / f"f{i:05d}.png" u = k/max(1, shot.n-1) arr = eng.frame(k, u, e) # ALWAYS step the engine if p.exists() and not force: continue post(arr, i, e, shot).save(p, compress_level=1) made += 1 return f"shot {shot.idx:02d} {shot.engine:8s} {shot.section:9s} {made}/{shot.n}" def contact_sheet(shots): cols = 6; rows = (len(shots)+cols-1)//cols tw, th = P(320), P(180) # 16:9 thumbs sheet = Image.new("RGB", (cols*tw, rows*(th+P(24))), (10, 10, 14)) sd = ImageDraw.Draw(sheet) E = env() for n, sh in enumerate(shots): rng = np.random.default_rng(sh.seed) eng = ENGINES[sh.engine](sh, rng) mid = sh.n//2 arr = None for k in range(mid+1): i = sh.i0+k e = {kk: float(E[kk][min(i, N_FRAMES-1)]) for kk in E} arr = eng.frame(k, k/max(1, sh.n-1), e) im = post(arr, sh.i0+mid, e, sh).resize((tw, th), Image.LANCZOS) cx, cy = (n % cols)*tw, (n//cols)*(th+P(24)) sheet.paste(im, (cx, cy)) sd.text((cx+P(5), cy+th+P(4)), f"{sh.idx:02d} {sh.engine} · {sh.section} · {sh.i0/FPS:.1f}s", font=font(13), fill=(190, 195, 205)) p = OUT/"contact_sheet.png"; sheet.save(p) print(f"contact sheet -> {p} ({len(shots)} shots)") def main(): ap = argparse.ArgumentParser() ap.add_argument("--sheet", action="store_true") ap.add_argument("--shots", default="") ap.add_argument("--force", action="store_true") ap.add_argument("--mux-only", action="store_true") ap.add_argument("--audio-only", action="store_true") ap.add_argument("--jobs", type=int, default=min(14, os.cpu_count())) a = ap.parse_args() wav = AUD/"final.wav" if not wav.exists() or not (AUD/"env.npz").exists() or a.force: print(f"[1/3] song… {N_BARS} bars @ {BPM:.0f}bpm = {DUR:.1f}s") wav, mix = build_song(); analyze(mix) if a.audio_only: print(f"audio -> {wav}"); return shots = build_shots() if a.sheet: contact_sheet(shots); return if not a.mux_only: sel = set(int(x) for x in a.shots.split(",") if x.strip() != "") jobs = [(s, a.force) for s in shots if not sel or s.idx in sel] print(f"[2/3] frames… {len(jobs)} shots / {N_FRAMES} frames on {a.jobs} workers") import multiprocessing as mp with mp.get_context("fork").Pool(a.jobs) as pool: for r in pool.imap_unordered(render_shot, jobs): print(" ", r) print("[3/3] mux…") sd = globals().get("SETDIR", "second_nature") out = OUT/f"{NAME}.mp4" subprocess.run(["ffmpeg", "-y", "-framerate", str(FPS), "-i", str(FRAMES/"f%05d.png"), "-i", str(wav), "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "256k", "-shortest", "-movflags", "+faststart", "-metadata", f"generator=renders/{sd}/{NAME}/render.py", "-metadata", f"title={SUBT} — {TITLE}", str(out)], check=True, capture_output=True) try: sha = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"], cwd=ROOT).decode().strip() br = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=ROOT).decode().strip() except Exception: sha = br = "unknown" (OUT/"PROVENANCE.txt").write_text( f"generator: renders/{sd}/{NAME}/render.py\n" f"git: {sha} branch: {br}\n" f"timestamp: {datetime.datetime.now().astimezone().isoformat()}\n" f"duration: {DUR:.2f}s fps: {FPS} size: {RW}x{RH} (16:9)\n" f"scale: S={S} — native re-rasterisation from {W}x{H} authoring units\n" f"music: {MUSIC_DESC}\n" f"sections: {' '.join(n for n,_,_ in SECTIONS)}\n" f"engines: {ENGINE_DESC} (shot-parallel, stateful per shot)\n") print(f"DONE {out} ({DUR:.1f}s)") if __name__ == "__main__": main()