#!/usr/bin/env python3 # ═════════════════════════════════════════════════════════════════════════════ # PLAYER COMPUTER — Inspection Day (29/32) # by Gene Kogan · 2026 · https://genekogan.com/player_computer/inspection_day # # Annual inspection in blueprint-land, and Car 5 has something to confess. # # 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/inspection_day.py.txt # # The original render (for reference, yours should differ): # video: https://genekogan.com/player_computer/media/inspection_day.mp4 # cover: https://genekogan.com/player_computer/media/inspection_day.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 inspection_day.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 — "INSPECTION DAY" (tightened cut) Detroit techno, 126bpm, C minor. 34 bars, instrumental with machine chops. Intro(4) Groove1(10) Breakdown(6) Groove2(8) Outro(6) Tightened from renders/spiral_jam/inspection_day (56 bars, ~108s) to ~67s for the final-curation set: same music, same look, same dialogue arc — the arrangement is recomposed, not sped up. Cut: the CAR 2 / CAR 4 "NOMINAL" annotation beats (pure repetition; CAR 1 establishes nominal, CAR 3 keeps the joke), and each section loses its slack bars. The full Car 5 arc — squeak confession, breakdown diagnosis, roller replacement, relief, PASS stamp — survives intact. The elevator inspector's annual visit, told inside the building's own blueprint. Cars 1 through 4 are nominal. Car 5 has a squeak it did not want to make a fuss about. The breakdown IS the diagnosis — the music strips to hum and squeak while the bad roller gets the magnifier — and when the groove comes back the roller is new and the car glides. Everyone passes. The building talks in printed annotations, because in a blueprint even the small talk is stenciled. Look: blueprint. Deep cyan ground with drafting grid, pale ink lines, dimension arrows, hatching, a cutaway elevation with three shafts whose cars glide quantized to the bar. The inspector is the one amber figure in a cyan world. Round 2 (player_computer_2): same score, same drawing, restless cutting. Round 1 held twelve shots over sixty-seven seconds and sat in each of them; this cut runs three times as many, drops in close-ups and inserts (a needle, a checkbox, a cracked roller at macro), lets a set repeat itself one size tighter as a punch-in, and whip-blurs into the shots that land on an accent. The story beats are untouched — every annotation still gets its own held frame — but between them the picture never stops moving. Delivered at 1280x720 (16:9) for this set: the 1680x1080 drafting stage is simply cropped wider, so nothing is stretched and the framing gains width. Returns to: errands (an isometric world of systems doing their jobs) and announcement (typography as actor) — redrawn as a working drawing. Composition: engine : audio-first x shot-parallel x stage-camera content: audio-groove (techno kit, stab, rolling bass) x dialogue-scenes (annotation labels in post) x effects-post Run from repo root: python3 renders/player_computer_final/inspection_day/render.py --sheet python3 renders/player_computer_final/inspection_day/render.py """ import argparse, datetime, hashlib, math, os, subprocess, wave from pathlib import Path import numpy as np from PIL import Image, ImageDraw, ImageFont, ImageFilter NAME = "inspection_day" TITLE = "INSPECTION DAY" SETDIR = "player_computer_final" SETNUM = "04" W, H, FPS = 1920, 1080, 30 # ── delivery scale ─────────────────────────────────────────────────────────── # FINAL CUT: native 1920x1080. The drawing stays authored in the 1680x1080 # drafting-stage units and the 1280x720 delivery frame; S = H/720 is the one # global that turns those into real pixels. Stage geometry and stroke widths go # through `ScaledDraw`, type through `font()`, and the post chain scales its # blur, its bloom and its grain explicitly. Nothing is upscaled after the fact. WB, HB = 1280, 720 # the authoring frame S = H/720.0 def PX(v): return int(round(v*S)) def BR(r): return r if S == 1.0 else r*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 multiplying geometry (first positional) and `width` by S. arc/chord/pieslice take angles as positionals 2/3, which pass through.""" __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(1, int(round(w*s))) r = kw.get("radius") if r is not None: kw["radius"] = max(1, int(round(r*s))) 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) BPM = 126.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, 4), ("groove1", 4, 14), ("breakdown", 14, 20), ("groove2", 20, 28), ("outro", 28, 34), ] N_BARS = SECTIONS[-1][2] DUR = N_BARS * BAR + 2.0 N_FRAMES = int(DUR * FPS) MUSIC_DESC = f"detroit techno, {BPM:.0f}bpm, C minor, {N_BARS} bars" ENGINE_DESC = ("elevation / machine / car / squeak / doors / stamp (blueprint), " "37 shots with whip-cuts") 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.0 C2 = nf("C2") def sqk(dur=.30, seed=61): """The Car 5 squeak: a falling narrowband cry, half metal, half animal.""" n = int(dur*SR); t = np.arange(n)/SR f = 2300 - 900*(t/dur) + 220*np.sin(2*np.pi*13*t) x = np.sin(2*np.pi*np.cumsum(f)/SR) x += .4*np.sin(2*2*np.pi*np.cumsum(f)/SR) return x*np.exp(-t*8)*np.clip(t*180, 0, 1)*.5 def ding(seed=67): a = fm(nf("E5"), .9, ratio=2.0, index=2.0, idec=5.0, d=.6, r=.3, seed=seed) return a*.6 def rumble(dur=.5, seed=71): n = int(dur*SR); t = np.arange(n)/SR rng = np.random.RandomState(seed) return bandshape(rng.randn(n), lo=50, hi=300)*np.sin(np.pi*np.clip(t/dur, 0, 1))*.7 def relay(seed=73): n = int(.05*SR); t = np.arange(n)/SR return bandshape(np.random.RandomState(seed).randn(n), lo=1200, hi=5200)*np.exp(-t*160)*.5 def hum(dur, seed=79): n = int(dur*SR); t = np.arange(n)/SR x = np.sin(2*np.pi*60*t)*.5 + np.sin(2*np.pi*120*t)*.3 + np.sin(2*np.pi*180*t)*.12 am = .85 + .15*np.sin(2*np.pi*.23*t) return x*am*.3 _CHOPS = {} def chop(word, dur, voice="Fred", rate=160): key = (word, round(dur, 3), voice, rate) if key not in _CHOPS: x = speak(word, voice=voice, rate=rate, cache=AUD) _CHOPS[key] = fit(x, int(dur*SR)) return _CHOPS[key] def build_song(): s = Song(DUR) R = np.random.RandomState(12600) def sec_of(bar): for nm, a, b in SECTIONS: if a <= bar < b: return nm return "outro" # the stab: Cm9 voiced high (classic machine strings) — Eb G Bb D STAB = (3, 7, 10, 14) # bass pattern (16ths), degrees of C natural minor BP = [0, 0, 12, 0, 0, 10, 0, 12, 0, 0, 12, 0, 7, 0, 10, 12] for bar in range(N_BARS): sec = sec_of(bar) brk = sec == "breakdown" quiet = sec in ("intro", "outro") # ---- four on the floor -------------------------------------------- if not brk: for b2 in range(4): at = s.t(bar, b2*4) s.put("drums", kick(dur=.26, f0=140, f1=46, punch=26, click=.3), at, g=.9 if not quiet else .7) if b2 == 0: s.kick_t.append(at) for b2 in range(4): # offbeat open hats s.put("drums", hat(dur=.10, openh=True), s.t(bar, b2*4+2), g=.24, pan=.15) for st in (4, 12): s.put("drums", snare(dur=.16, tone=210, bright=.9), s.t(bar, st), g=.4 if not quiet else .25, pan=-.06) for st in range(0, 16, 1): if st % 2 and R.rand() < .5: s.put("drums", hat(dur=.03), s.t(bar, st), g=.08, pan=-.3+.6*R.rand()) else: s.put("drums", kick(dur=.3, f0=120, f1=42, punch=18, click=.1), s.t(bar, 0), g=.5) # ---- rolling bass -------------------------------------------------- if not brk: for j, iv in enumerate(BP): if quiet and j % 2: continue s.put("bass", voice(C2*2**(iv/12.0), BEAT*.22, kind="saw", nh=12, c0=700+400*math.sin(bar*.7), c1=180, ck=10, res=.6, a=.003, d=.1, s=.6, r=.05, seed=bar*3+j), s.t(bar, j), g=.30) # ---- the stab ------------------------------------------------------ if not brk and not quiet: for st in (3, 11): for k2, iv in enumerate(STAB): s.put("stab", voice(nf("C4")*2**(iv/12.0), BEAT*.35, kind="saw", nh=18, c0=2600, c1=900, ck=9, res=.5, detune=(-1.1, 1.2), a=.004, d=.1, s=.3, r=.08, seed=bar*7+st+k2), s.t(bar, st), g=.085, pan=-.3+.2*k2) # ---- bleep lead (call in groove2) ---------------------------------- if sec in ("groove2", "outro") and bar % 2 == 0: MEL = [12, 15, 19, 15, 22, 19, 15, 12] for j in (0, 3, 6, 10, 13): iv = MEL[(bar+j) % 8] s.put("bleep", voice(nf("C4")*2**(iv/12.0), .14, kind="sine", nh=3, c0=4000, c1=2000, ck=8, a=.002, d=.06, s=.3, r=.05, seed=bar*11+j), s.t(bar, j), g=.13, pan=.25) # ---- pads ---------------------------------------------------------- if quiet or brk: for k2, iv in enumerate((0, 3, 7, 10)): s.put("pad", voice(nf("C3")*2**(iv/12.0), BAR*1.4, kind="saw", nh=16, c0=800, c1=420, ck=.6, detune=(-1.2, 0, 1.3), a=.9, d=.8, s=.75, r=1.2, seed=bar*13+k2), s.t(bar, 0), g=.085, pan=-.45+.3*k2) # ---- machine SFX as instruments ------------------------------------ if not brk: s.put("mech", relay(seed=bar*17), s.t(bar, 7), g=.3, pan=.3) if sec == "groove1" and bar >= 9: # the squeak sneaks in s.put("mech", sqk(seed=bar), s.t(bar, 10), g=.20+.02*(bar-9), pan=-.2) if brk: # featured squeak s.put("mech", sqk(.5, seed=bar*3), s.t(bar, 4), g=.42, pan=-.2) s.put("mech", sqk(.3, seed=bar*5), s.t(bar, 12), g=.3, pan=.1) if bar in (4, 9, 20, 24, 28): s.put("mech", ding(seed=bar), bar*BAR + .02, g=.4, pan=.2) s.put("mech", rumble(seed=bar), bar*BAR + .3, g=.4) # robot chop if sec == "groove2" and bar % 8 == 4: s.put("vox", chop("going up", .5, voice="Fred", rate=140), s.t(bar, 8), g=.30, pan=.1) s.put("fx", hum(DUR), 0.0, g=.5) for b in (14, 20): s.put("fx", riser(BAR*2), (b-2)*BAR, g=.24) s.put("fx", crash(dur=1.2), b*BAR, g=.2) s.bus("stab", lambda x: reverb(delay(x, BEAT*.75, .42, .3), rt=2.4, mix=.4, seed=401)) s.bus("bleep", lambda x: reverb(delay(x, BEAT*.75, .5, .34), rt=2.8, mix=.44, seed=403)) s.bus("pad", lambda x: reverb(x, rt=4.0, mix=.55, seed=407)) s.bus("mech", lambda x: reverb(x, rt=1.6, mix=.24, seed=409)) s.bus("vox", lambda x: reverb(x, rt=1.4, mix=.3, seed=419)) mix = s.mixdown(dict(drums=1.0, bass=1.0, stab=1.0, bleep=1.0, pad=1.0, mech=1.0, vox=1.0, fx=1.0), pump_depth=.30, pump_rel=.12, levels=dict(intro=.55, groove1=.95, breakdown=.5, groove2=1.0, outro=.55)) 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 # ════════════════════════════════════════════════════════════════════════════ # STAGE + CAMERA — the frame is a 14:9 crop of a much larger stage # # Round 1 of this set was shot entirely in locked-off full-frame, which is a # large part of why ten pieces read as one piece. Here every scene is painted # once onto a 1680x1080 stage and the delivered frame is a moving crop of it, # so the same drawing yields a wide, a mid, a close-up and a dolly. # # A shot string is either a single shot ("cu_driver") or a move between two # ("wide>cu_driver"), interpolated with an ease across the shot's duration. # ════════════════════════════════════════════════════════════════════════════ SW_S, SH_S = 1680, 1080 ASPECT = W / H SHOT_W = {"wide": 1.00, "full": 0.82, "mid": 0.60, "ots": 0.48, "cu": 0.32, "ins": 0.22, "macro": 0.14} def _box(anchors, shot): kind, _, target = shot.partition("_") fw = SHOT_W.get(kind, 1.0) cx, cy = anchors.get(target or "_", anchors.get("_", (SW_S/2, SH_S/2))) if kind == "wide": cx, cy = SW_S/2, SH_S/2 elif kind == "full": cx = (cx + SW_S/2)/2; cy = (cy + SH_S/2)/2 elif kind == "ots": cx = cx*.62 + SW_S/2*.38 + (150 if cx < SW_S/2 else -150) cy = cy*.58 + SH_S/2*.42 bw = SW_S*fw; bh = bw/ASPECT if bh > SH_S: bh = SH_S; bw = bh*ASPECT return cx, cy, bw, bh def _ease(u): return u*u*(3-2*u) def cam_box(anchors, shot, f01, jseed=0, push=0.04, drift=1.0): """Return the (x0,y0,x1,y1) crop of the stage for this frame.""" if ">" in shot: a, b = shot.split(">", 1) ca = _box(anchors, a.strip()); cb = _box(anchors, b.strip()) e = _ease(min(max(f01, 0.0), 1.0)) cx, cy, bw, bh = (ca[i] + (cb[i]-ca[i])*e for i in range(4)) else: cx, cy, bw, bh = _box(anchors, shot) k = 1.0 - push*f01 # slow push-in bw *= k; bh *= k # deterministic handheld drift — slow enough to breathe, not shake fw = bw/SW_S jx = math.sin(f01*1.525 + jseed)*7*(1-fw*.6)*drift jy = math.cos(f01*1.175 + jseed*1.7)*5*(1-fw*.6)*drift x0 = cx - bw/2 + jx; y0 = cy - bh/2 + jy x0 = max(0, min(SW_S-bw, x0)); y0 = max(0, min(SH_S-bh, y0)) return (int(x0), int(y0), int(x0+bw), int(y0+bh)) def shoot(stage_img, anchors, shot, f01, jseed=0, push=0.04, drift=1.0): """Crop the stage to the shot and scale to delivery size.""" box = cam_box(anchors, shot, f01, jseed, push, drift) return stage_img.crop(tuple(PX(v) for v in box)).resize((W, H), Image.LANCZOS) def new_stage(bg): im = Image.new("RGB", (PX(SW_S), PX(SH_S)), bg) return im, mkdraw(im) # ════════════════════════════════════════════════════════════════════════════ # BLUEPRINT # ════════════════════════════════════════════════════════════════════════════ NEUTRAL = {"rms": .5, "low": .4, "mid": .4, "high": .3, "kick": .2} BG0 = (12, 44, 86) BG1 = (8, 32, 66) LINE = (214, 232, 246) FAINT = (96, 146, 186) DIM = (140, 190, 220) AMBER = (255, 192, 92) def rot2(pts, cx, cy, ang): c, s2 = math.cos(ang), math.sin(ang) return [(cx + (x-cx)*c - (y-cy)*s2, cy + (x-cx)*s2 + (y-cy)*c) for x, y in pts] _BP = {} def bp_stage(): """Blueprint ground: gradient + drafting grid + border. Cached.""" if "img" not in _BP: arr = np.zeros((PX(SH_S), PX(SW_S), 3), np.float32) g = np.linspace(0, 1, PX(SH_S))[:, None] for c in range(3): arr[..., c] = BG0[c]*(1-g) + BG1[c]*g im = Image.fromarray(arr.astype(np.uint8)) d = mkdraw(im) for x in range(0, SW_S, 40): d.line([x, 0, x, SH_S], fill=(BG0[0]+6, BG0[1]+8, BG0[2]+10), width=1) for y in range(0, SH_S, 40): d.line([0, y, SW_S, y], fill=(BG0[0]+6, BG0[1]+8, BG0[2]+10), width=1) for x in range(0, SW_S, 200): d.line([x, 0, x, SH_S], fill=(BG0[0]+12, BG0[1]+16, BG0[2]+18), width=1) for y in range(0, SH_S, 200): d.line([0, y, SW_S, y], fill=(BG0[0]+12, BG0[1]+16, BG0[2]+18), width=1) _BP["img"] = im return _BP["img"].copy() def hatch(d, box, sp=14, col=FAINT, w=1): x0, y0, x1, y1 = box n = int(((x1-x0)+(y1-y0))/sp) for i in range(n+1): o = i*sp ax, ay = x0+o, y0 bx, by = x0, y0+o if ax > x1: ay = y0 + (ax-x1); ax = x1 if by > y1: bx = x0 + (by-y1); by = y1 d.line([ax, ay, bx, by], fill=col, width=w) def dimline(d, p0, p1, label=None, col=DIM): d.line([p0, p1], fill=col, width=2) for (px, py) in (p0, p1): vx, vy = p1[0]-p0[0], p1[1]-p0[1] L = math.hypot(vx, vy)+1e-6; vx, vy = vx/L, vy/L sgn = 1 if (px, py) == p0 else -1 d.line([px, py, px+sgn*vx*14-vy*7, py+sgn*vy*14+vx*7], fill=col, width=2) d.line([px, py, px+sgn*vx*14+vy*7, py+sgn*vy*14-vx*7], fill=col, width=2) if label: mx, my = (p0[0]+p1[0])/2, (p0[1]+p1[1])/2 d.text((mx+8, my-22), label, font=font(20), fill=col) def stickman(d, x, y, sc, t, col=AMBER, walk_u=0.0, clipboard=True, hat=True): """The inspector: an amber line figure. y = feet.""" lw = max(3, int(5*sc)) sw_, bob, aw_ = walk(walk_u, 1.0) hip = (x, y-40*sc+bob*2*sc) sh = (x+3*sc, y-78*sc+bob*2*sc) d.line([hip, (x+sw_*16*sc, y)], fill=col, width=lw) d.line([hip, (x-sw_*16*sc, y-abs(sw_)*4*sc)], fill=col, width=lw) d.line([hip, sh], fill=col, width=int(lw*1.3)) hx, hy = sh[0]+2*sc, sh[1]-16*sc d.ellipse([hx-11*sc, hy-11*sc, hx+11*sc, hy+11*sc], outline=col, width=lw) if hat: d.pieslice([hx-14*sc, hy-20*sc, hx+14*sc, hy+4*sc], 180, 360, fill=col) if clipboard: d.line([sh, (x+22*sc, y-58*sc)], fill=col, width=int(lw*.8)) d.rectangle([x+16*sc, y-70*sc, x+34*sc, y-46*sc], outline=col, width=2) for q in range(3): d.line([x+19*sc, y-64*sc+q*6*sc, x+31*sc, y-64*sc+q*6*sc], fill=col, width=1) d.line([sh, (x-14*sc, y-52*sc)], fill=col, width=int(lw*.8)) else: d.line([sh, (x+16*sc, y-52*sc)], fill=col, width=int(lw*.8)) d.line([sh, (x-16*sc, y-52*sc)], fill=col, width=int(lw*.8)) N_FLOORS = 7 def car_pos(t, shaft): """Quantized car motion: each car eases one floor per 2 bars, offset.""" ph = t/(BAR*2) + shaft*0.37 fl = int(ph) % (N_FLOORS*2) fl = fl if fl < N_FLOORS else N_FLOORS*2-1-fl # bounce uu = ph % 1.0 nxt = fl + (1 if (int(ph) % (N_FLOORS*2)) < N_FLOORS else -1) nxt = max(0, min(N_FLOORS-1, nxt)) return fl + (nxt-fl)*ease_io(min(1.0, uu*1.6)) class Elevation: """The full cutaway: three shafts, cars gliding on the grid.""" def __init__(self, shot, rng): self.rng = rng; self.i0 = shot.i0; self.sec = shot.section self.cam = str(rng.choice( ["wide", "wide>full_mid", "full_mid", "mid_car0", "mid_car2", "cu_car1", "ins_car2", "cu_car0>wide", "mid_top", "ins_top", "wide>cu_car1", "full_mid>ins_car0", "cu_base"])) def frame(self, k, u, e): t = (self.i0+k)/FPS im = bp_stage(); d = mkdraw(im) top, bot = SH_S*0.08, SH_S*0.94 fh = (bot-top)/N_FLOORS SHAFTS = (SW_S*0.30, SW_S*0.52, SW_S*0.74) sw2 = SW_S*0.062 # floor slabs, broken at the shafts segs = [SW_S*0.06] for sx in SHAFTS: segs += [sx-sw2, sx+sw2] segs += [SW_S*0.94] for f in range(N_FLOORS+1): y = bot - f*fh for q in range(0, len(segs), 2): d.line([segs[q], y, segs[q+1], y], fill=LINE, width=4) if f < N_FLOORS: d.text((SW_S*0.065, y-fh+8), f"L{f+1}", font=font(24), fill=DIM) hatch(d, [SW_S*0.06, y-10, segs[1], y], 12, FAINT) hatch(d, [SW_S*0.06, bot, SW_S*0.94, bot+30], 16) # three shafts for si, sx in enumerate(SHAFTS): # shaft interior darker, heavy walls d.rectangle([sx-sw2, top-30, sx+sw2, bot], fill=(BG1[0]-3, BG1[1]-6, BG1[2]-8)) for wx in (sx-sw2, sx+sw2): d.line([wx, top-30, wx, bot], fill=LINE, width=5) # sheave at head d.ellipse([sx-26, top-64, sx+26, top-12], outline=LINE, width=4) a = t*(3.0+si*0.7) for q in range(3): aa = a + q*math.tau/3 d.line([sx+math.cos(aa)*22, top-38+math.sin(aa)*22, sx-math.cos(aa)*22, top-38-math.sin(aa)*22], fill=LINE, width=2) fl = car_pos(t, si) cy = bot - fl*fh - fh*0.5 d.line([sx, top-16, sx, cy-fh*0.36], fill=DIM, width=3) # the car: doors split line + lit cab d.rectangle([sx-sw2*0.8, cy-fh*0.36, sx+sw2*0.8, cy+fh*0.44], fill=(BG0[0]+16, BG0[1]+24, BG0[2]+26)) d.rectangle([sx-sw2*0.8, cy-fh*0.36, sx+sw2*0.8, cy+fh*0.44], outline=LINE, width=5) d.line([sx, cy-fh*0.36, sx, cy+fh*0.44], fill=LINE, width=2) # counterweight riding the outside rail wy = top + (bot-top)*(fl/N_FLOORS)*0.8 + 40 d.rectangle([sx+sw2+8, wy, sx+sw2+34, wy+70], outline=DIM, width=3) hatch(d, [sx+sw2+8, wy, sx+sw2+34, wy+70], 9, DIM) d.text((sx-30, bot+36), f"CAR {si*2+1}", font=font(22), fill=LINE) # inspector rides the middle car if si == 1: stickman(d, sx, cy+fh*0.42, 0.62, t, walk_u=0.0, clipboard=True) # dimension flourishes, flickering on the kick if e["kick"] > 0.35: dimline(d, (SW_S*0.06, top-40), (SW_S*0.94, top-40), "ELEVATION 1:50") dimline(d, (SW_S*0.955, bot), (SW_S*0.955, bot-fh), f"{int(fh/3)}0") anchors = {"_": (SW_S/2, SH_S/2), "mid": (SW_S*0.52, SH_S*0.5), "top": (SW_S*0.52, top-30), "base": (SW_S*0.52, bot+20)} for si, sx in enumerate(SHAFTS): anchors[f"car{si}"] = (sx, bot - car_pos(t, si)*fh - fh*0.5) return np.asarray(shoot(im, anchors, self.cam, ease_io(u), jseed=self.i0), np.float32) class Machine: """The machine room: the big sheave, the motor, the hum.""" def __init__(self, shot, rng): self.rng = rng; self.i0 = shot.i0 self.cam = str(rng.choice( ["full_wheel", "wide>full_wheel", "cu_wheel", "ins_wheel", "macro_wheel", "mid_motor", "cu_motor", "cu_insp", "cu_wheel>full_wheel", "mid_insp>cu_wheel", "wide"])) def frame(self, k, u, e): t = (self.i0+k)/FPS im = bp_stage(); d = mkdraw(im) gy = SH_S*0.82 d.line([0, gy, SW_S, gy], fill=LINE, width=4) hatch(d, [0, gy, SW_S, gy+34], 18) cx, cy = SW_S*0.44, SH_S*0.42 R2 = 240 # the sheave: spokes rotate a spoke per beat (quantized snap) beat_i = int(t/BEAT); ph = (t/BEAT) % 1.0 ang = (beat_i + ease_out(min(1, ph*2)))*math.tau/8 d.ellipse([cx-R2, cy-R2, cx+R2, cy+R2], outline=LINE, width=6) d.ellipse([cx-R2*0.7, cy-R2*0.7, cx+R2*0.7, cy+R2*0.7], outline=FAINT, width=3) for q in range(8): a = ang + q*math.tau/8 d.line([cx+math.cos(a)*R2*0.15, cy+math.sin(a)*R2*0.15, cx+math.cos(a)*R2*0.92, cy+math.sin(a)*R2*0.92], fill=LINE, width=4) d.ellipse([cx-26, cy-26, cx+26, cy+26], outline=LINE, width=5) # cables down for sgn in (-1, 1): d.line([cx+sgn*R2*0.92, cy, cx+sgn*R2*0.92, gy], fill=FAINT, width=3) # motor block mx = SW_S*0.72 d.rectangle([mx, cy-90, mx+SW_S*0.16, cy+90], outline=LINE, width=4) hatch(d, [mx, cy-90, mx+SW_S*0.16, cy+90], 16) d.text((mx+10, cy-130), "TRACTION MOTOR", font=font(22), fill=DIM) # coupling shaft pulsing with the low end d.line([cx+R2, cy, mx, cy], fill=AMBER if e["low"] > .5 else LINE, width=6) # inspector with flashlight ix = SW_S*0.16 stickman(d, ix, gy, 0.9, t, walk_u=t*1.4, clipboard=False) beam = [(ix+16, gy-64), (cx-40, cy+R2*0.5), (cx+50, cy+R2*0.85)] d.polygon(beam, fill=(BG0[0]+26, BG0[1]+40, BG0[2]+40)) dimline(d, (cx-R2, cy+R2+40), (cx+R2, cy+R2+40), "SHEAVE D=1800") anchors = {"_": (SW_S*0.45, SH_S*0.5), "wheel": (cx, cy), "motor": (mx + SW_S*0.08, cy), "insp": (ix, gy-90)} return np.asarray(shoot(im, anchors, self.cam, ease_io(u), jseed=self.i0), np.float32) class Car: """Inside the car: panel, needle, doors opening on the bar.""" def __init__(self, shot, rng): self.rng = rng; self.i0 = shot.i0 self.cam = str(rng.choice( ["full_panel", "wide", "cu_dial", "mid_dial", "cu_panel", "ins_panel", "mid_doors", "cu_doors", "cu_insp", "wide>cu_dial", "ins_panel>full_panel"])) def frame(self, k, u, e): t = (self.i0+k)/FPS im = bp_stage(); d = mkdraw(im) # car interior: walls d.rectangle([SW_S*0.14, SH_S*0.10, SW_S*0.86, SH_S*0.92], outline=LINE, width=5) # floor indicator: arc + needle cx, cy = SW_S*0.5, SH_S*0.22 d.arc([cx-160, cy-80, cx+160, cy+160], 200, 340, fill=LINE, width=4) bar_i = int(t/(BAR*2)) fl = (bar_i % N_FLOORS) ph = (t/(BAR*2)) % 1.0 need = math.radians(200 + (340-200)*((fl + ease_io(min(1, ph*1.4)))/N_FLOORS)) d.line([cx, cy+40, cx+math.cos(need)*140, cy+40+math.sin(need)*140], fill=AMBER, width=5) for f in range(N_FLOORS+1): a = math.radians(200 + f*(340-200)/N_FLOORS) d.text((cx+math.cos(a)*175-8, cy+40+math.sin(a)*175-10), str(f+1), font=font(20), fill=DIM) # button panel: buttons light with the floor px = SW_S*0.74 for f in range(N_FLOORS): by = SH_S*0.78 - f*SH_S*0.075 lit = (f == fl) d.ellipse([px-22, by-22, px+22, by+22], outline=LINE, width=3, fill=(AMBER if lit else None)) d.text((px-7, by-11), str(f+1), font=font(22), fill=(BG0 if lit else DIM)) # doors: part on each bar downbeat dph = (t/BAR) % 1.0 gap = ease_io(min(1.0, dph*2.5))*(1-ease_io(max(0.0, (dph-0.6)*2.5))) dw = SW_S*0.14*gap lx1 = max(SW_S*0.24+1, SW_S*0.38-dw) rx0 = min(SW_S*0.52-1, SW_S*0.38+dw) d.rectangle([SW_S*0.24, SH_S*0.36, lx1, SH_S*0.90], outline=LINE, width=4) d.rectangle([rx0, SH_S*0.36, SW_S*0.52, SH_S*0.90], outline=LINE, width=4) hatch(d, [SW_S*0.24, SH_S*0.36, lx1, SH_S*0.90], 22) hatch(d, [rx0, SH_S*0.36, SW_S*0.52, SH_S*0.90], 22) # inspector standing, nodding on the beat nod = (1-((t/BEAT) % 1.0))**2*6 stickman(d, SW_S*0.60, SH_S*0.90 - nod*0.0, 1.0, t, walk_u=0.0) anchors = {"_": (SW_S/2, SH_S*0.5), "panel": (px, SH_S*0.55), "dial": (cx, cy+40), "doors": (SW_S*0.38, SH_S*0.62), "insp": (SW_S*0.60, SH_S*0.68)} return np.asarray(shoot(im, anchors, self.cam, ease_io(u), jseed=self.i0), np.float32) class Squeak: """The offending roller, under the magnifier.""" def __init__(self, shot, rng): self.rng = rng; self.i0 = shot.i0 self.fixed = shot.section in ("groove2", "outro") self.cam = str(rng.choice( ["full_roller", "cu_roller", "macro_roller", "ins_roller", "wide>cu_roller", "cu_roller>full_roller", "full_roller>macro_roller"])) def frame(self, k, u, e): t = (self.i0+k)/FPS im = bp_stage(); d = mkdraw(im) # guide rail rx = SW_S*0.5 d.line([rx, 0, rx, SH_S], fill=LINE, width=8) d.line([rx-30, 0, rx-30, SH_S], fill=FAINT, width=3) d.line([rx+30, 0, rx+30, SH_S], fill=FAINT, width=3) # the roller: wobbles when broken, true when fixed cy = SH_S*0.5 wob = 0.0 if self.fixed else math.sin(t*21)*10*(0.4+0.6*e["mid"]) cx = rx + 90 + wob*0.4 R2 = 120 a = t*4.0 d.ellipse([cx-R2, cy-R2+wob, cx+R2, cy+R2+wob], outline=LINE, width=6) d.ellipse([cx-30, cy-30+wob, cx+30, cy+30+wob], outline=LINE, width=4) for q in range(6): aa = a + q*math.tau/6 d.line([cx+math.cos(aa)*34, cy+wob+math.sin(aa)*34, cx+math.cos(aa)*R2*0.9, cy+wob+math.sin(aa)*R2*0.9], fill=FAINT, width=3) # crack in the old roller if not self.fixed: d.line([cx+R2*0.5, cy+wob-R2*0.6, cx+R2*0.8, cy+wob-R2*0.2], fill=AMBER, width=4) # squeak marks for q in range(3): sa = -0.6 - q*0.5 + math.sin(t*8)*0.1 d.arc([cx-R2-40-q*22, cy+wob-R2-40-q*22, cx+R2+40+q*22, cy+wob+R2+40+q*22], math.degrees(sa)-16, math.degrees(sa)+16, fill=AMBER, width=3) # magnifier circle + label mr = 200 d.ellipse([cx-mr, cy-mr, cx+mr, cy+mr], outline=DIM, width=3) d.line([cx+mr*0.7, cy+mr*0.7, cx+mr*1.4, cy+mr*1.4], fill=DIM, width=8) lab = "ROLLER 12 — REPLACED" if self.fixed else "ROLLER 12 — SQUEAK ORIGIN" d.line([cx+mr*0.5, cy-mr*0.8, cx+mr*1.2, cy-mr*1.1], fill=DIM, width=2) d.text((cx+mr*1.2+8, cy-mr*1.1-12), lab, font=font(24), fill=LINE) if self.fixed: d.text((cx-60, cy+R2+60), "NOMINAL", font=font(28), fill=AMBER) anchors = {"_": (cx, cy), "roller": (cx, cy+wob), "label": (cx+mr*1.2+140, cy-mr*1.1)} return np.asarray(shoot(im, anchors, self.cam, ease_io(u), jseed=self.i0, push=0.05), np.float32) class Doors: """Exterior doors opening on the downbeat, floor by floor.""" def __init__(self, shot, rng): self.rng = rng; self.i0 = shot.i0 self.f0 = int(rng.integers(1, 5)) self.cam = str(rng.choice( ["full_door", "cu_door", "ins_ind", "wide", "mid_door", "cu_ind>full_door", "wide>cu_door"])) def frame(self, k, u, e): t = (self.i0+k)/FPS im = bp_stage(); d = mkdraw(im) gy = SH_S*0.90 d.line([0, gy, SW_S, gy], fill=LINE, width=4) cx = SW_S*0.5 dw2, dh = SW_S*0.14, SH_S*0.56 # frame + indicator d.rectangle([cx-dw2-24, gy-dh-70, cx+dw2+24, gy], outline=LINE, width=5) bar_i = int(t/BAR) fl = self.f0 + bar_i % 3 d.polygon([(cx-16, gy-dh-46), (cx+16, gy-dh-46), (cx, gy-dh-24)], outline=LINE, width=1) d.text((cx+30, gy-dh-52), f"L{fl}", font=font(26), fill=AMBER) # doors part on the downbeat dph = (t/BAR) % 1.0 gap = ease_io(min(1.0, dph*2.2))*(1-ease_io(max(0.0, (dph-0.55)*2.4))) dwx = dw2*gap for sgn, x0, x1 in ((-1, cx-dw2, cx-dwx), (1, cx+dwx, cx+dw2)): d.rectangle([min(x0, x1), gy-dh, max(x0, x1), gy], outline=LINE, width=4) hatch(d, [min(x0, x1), gy-dh, max(x0, x1), gy], 20) # inside: the inspector, revealed rhythmically if gap > 0.25: stickman(d, cx, gy-4, 0.95, t, walk_u=0.0) anchors = {"_": (cx, SH_S*0.55), "door": (cx, gy-dh*0.5), "ind": (cx, gy-dh-46)} return np.asarray(shoot(im, anchors, self.cam, ease_io(u), jseed=self.i0, push=0.05), np.float32) class Stamp: """The report. Rows tick; the stamp lands on the one.""" ROWS = ["HOISTWAY CLEAR", "CABLES 6x19 OK", "GOVERNOR TRIPS", "BUFFERS RETURN", "DOORS RE-OPEN", "ROLLER 12 NEW", "CAR 5 SQUEAK", "PIT LADDER"] def __init__(self, shot, rng): self.rng = rng; self.i0 = shot.i0 self.cam = str(rng.choice( ["full_sheet", "cu_rows", "full_sheet>cu_stamp", "wide>full_sheet", "cu_rows>full_sheet"])) def frame(self, k, u, e): t = (self.i0+k)/FPS im = bp_stage(); d = mkdraw(im) # the sheet sx0, sy0 = SW_S*0.24, SH_S*0.08 sx1, sy1 = SW_S*0.76, SH_S*0.94 d.rectangle([sx0, sy0, sx1, sy1], outline=LINE, width=4, fill=(BG0[0]+8, BG0[1]+10, BG0[2]+12)) d.text((sx0+30, sy0+22), "ANNUAL INSPECTION — FORM 7B", font=font(26), fill=LINE) d.line([sx0+24, sy0+70, sx1-24, sy0+70], fill=FAINT, width=2) nrows = len(self.ROWS) done = int(u*1.15*nrows) for r, row in enumerate(self.ROWS): y = sy0 + 110 + r*SH_S*0.075 d.text((sx0+60, y), row, font=font(24), fill=DIM) d.rectangle([sx0+26, y, sx0+50, y+24], outline=LINE, width=2) if r < done: d.line([sx0+28, y+12, sx0+38, y+22], fill=AMBER, width=4) d.line([sx0+38, y+22, sx0+52, y-4], fill=AMBER, width=4) # the stamp arrives near the end, springs in if u > 0.46: su = spring((u-0.46)/0.30, freq=2.4, damp=5.0) ss = 3.2 - 2.2*min(1.0, su) cx, cy = SW_S*0.5, SH_S*0.62 ang = -0.18 box = rot2([(cx-200*ss, cy-70*ss), (cx+200*ss, cy-70*ss), (cx+200*ss, cy+70*ss), (cx-200*ss, cy+70*ss)], cx, cy, ang) d.polygon(box, outline=AMBER, width=1) d.line(box + [box[0]], fill=AMBER, width=int(8*min(1, ss))) f2 = font(int(90*ss), "Impact.ttf") tw = d.textlength("PASS", font=f2)/S d.text((cx-tw/2, cy-int(52*ss)), "PASS", font=f2, fill=AMBER) anchors = {"_": (SW_S/2, SH_S/2), "sheet": (SW_S/2, SH_S*0.5), "rows": (SW_S*0.5, sy0 + 110 + SH_S*0.22), "stamp": (SW_S*0.5, SH_S*0.62)} return np.asarray(shoot(im, anchors, self.cam, ease_io(u), jseed=self.i0, push=0.03), np.float32) ENGINES = {"elevation": Elevation, "machine": Machine, "car": Car, "squeak": Squeak, "doors": Doors, "stamp": Stamp} # Shot lengths are in BEATS. Round 1's menus were 8-16 beats (3.8-7.6s a # shot); these are 2-8, which is the whole note Gene gave: the picture should # jump. The annotation labels are drawn in delivery space and outlive the # cuts, so a line still reads across three shots. PLAN = { "intro": (["elevation", "machine", "doors"], [8, 6, 4]), "groove1": (["elevation", "doors", "car", "machine"], [4, 3, 6, 2, 8]), "breakdown": (["squeak", "machine", "car"], [6, 4, 8, 3]), "groove2": (["machine", "elevation", "doors", "squeak", "car"], [3, 4, 2, 6, 8]), "outro": (["stamp", "stamp", "elevation", "elevation"], [8, 6, 10]), } CARDS = {"intro": "INSPECTION DAY", "groove1": None, "breakdown": None, "groove2": None, "outro": None} SYSTEM_NAMES = ["CAR 1", "CAR 2", "CAR 3", "CAR 4", "CAR 5", "FORM 7B"] # building dialogue — annotation labels (bar, dur_bars, text) LABELS = [ (4.5, 1.8, "CAR 1: NOMINAL"), (6.8, 2.2, "CAR 3: A LITTLE PROUD OF ITSELF"), (9.5, 1.4, "CAR 5: ..."), (11.4, 2.2, "CAR 5: I HAVE A SQUEAK"), (14.2, 1.8, "INSPECTOR: SINCE WHEN"), (16.3, 1.4, "CAR 5: MARCH"), (18.0, 1.9, "CAR 5: I DID NOT WANT A FUSS"), (20.4, 2.2, "ROLLER 12: REPLACED"), (23.3, 1.6, "CAR 5: OH."), (25.2, 2.4, "CAR 5: OH, THAT'S MUCH BETTER"), (28.4, 2.2, "STATUS: ALL CARS PASS"), (32.2, 2.6, "CAR 5: SEE YOU NEXT YEAR"), ] class Shot: __slots__ = ("idx", "i0", "i1", "n", "engine", "section", "seed", "text", "card", "whip", "whipdir") 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 # a short shot arrives on a whip: the first few frames smear in the # direction of travel, so the join reads as a snap rather than a cut self.whip = self.n <= int(1.15*BAR*FPS) self.whipdir = 1 if (idx % 2) else -1 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: if nm == "outro": # the landing is composed, not drawn: stamp (PASS) first, # then the elevation glide-out under SEE YOU NEXT YEAR eng = engs[min(j, len(engs)-1)] elif j % 3 == 2: # every third shot may repeat the set one size tighter — # the punch-in, which a no-repeat rule forbids outright eng = last if last in engs else engs[R.randint(len(engs))] else: pool = [x for x in engs if x != last] or list(engs) eng = pool[R.randint(len(pool))] 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"): """Size is in authoring units; the loaded face is scaled once by S.""" key = (size, name, S) if key not in _FC: p = _find_font(name) _FC[key] = _load_font(p, size if S == 1.0 else max(2, PX(size))) return _FC[key] _VIG = {} def vignette(): if "v" not in _VIG: yy, xx = np.mgrid[0:H, 0:W] nx = (xx-W/2)/(W/2); ny = (yy-H/2)/(H/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) if a.shape[0] != H or a.shape[1] != W: a = np.asarray(Image.fromarray(np.clip(a, 0, 255).astype(np.uint8)) .resize((W, H), Image.LANCZOS), np.float32) # whip-in: a short shot arrives smeared, so the join snaps if shot.whip: age = i - shot.i0 if age < 5: kk = 1.0 - age/5.0 amt = int(6 + 62*kk) acc = np.zeros_like(a); wsum = 0.0 for q in range(6): wq = 1.0 - q/7.0 acc += np.roll(a, int(-shot.whipdir*amt*q/5.0), axis=1)*wq wsum += wq a = a*(1.0 - 0.88*kk) + (acc/wsum)*(0.88*kk) # blueprint glow: light bloom on the pale lines im = Image.fromarray(np.clip(a, 0, 255).astype(np.uint8)) sm = im.resize((W//4, H//4), Image.BILINEAR).filter( ImageFilter.GaussianBlur(BR(6))).resize((W, H), Image.BILINEAR) a = np.clip(a + np.asarray(sm, np.float32)*(0.18+0.14*e["high"]), 0, 255) a *= vignette() rng = np.random.RandomState(7400 + i) if S == 1.0: a += rng.normal(0, 2.2, a.shape) else: # grain is a look, not a resolution: authored at 1280x720, blown up # nearest-neighbour so a speck covers the same fraction of the frame. gn = rng.normal(0, 2.2, (HB, WB, 3))*8.0 + 128.0 gi = Image.fromarray(np.clip(gn, 0, 255).astype(np.uint8)) a += (np.asarray(gi.resize((W, H), Image.NEAREST), np.float32) - 128.0)/8.0 out = Image.fromarray(np.clip(a, 0, 255).astype(np.uint8)) d = mkdraw(out) # annotation dialogue in delivery space: stencil box, leader line t = i/FPS for (b0, db, text) in LABELS: t0 = b0*BAR; t1 = t0 + db*BAR if t0 <= t < t1: ag = min(1.0, (t-t0)/0.2) f = font(22) lw = d.textlength(text, font=f)/S # on the stamp shot the frame top is Form 7B's own header — # drop the annotation to the empty bottom-left (legibility) bx, by = ((WB*0.06, HB*0.80) if shot.engine == "stamp" else (WB*0.06, HB*0.10)) # typewriter reveal nch = int(len(text)*min(1.0, (t-t0)/0.5)) shown = text[:nch] d.rectangle([bx-10, by-8, bx+lw+14, by+30], outline=tuple(int(v*ag) for v in DIM), width=2) d.text((bx, by), shown, font=f, fill=tuple(int(v*ag) for v in (235, 244, 250))) d.line([bx+lw+14, by+11, bx+lw+70, by+40], fill=tuple(int(v*ag) for v in DIM), width=2) break if shot.card: age = i - shot.i0 if age < FPS*2.8: al = min(1.0, age/6.0)*min(1.0, (FPS*2.8-age)/10.0) f = font(46) lw = d.textlength(shot.card, font=f)/S # the show mark, stencilled inside the same title box: this is the # piece's one card, so it carries both names. f3 = font(18) sub = "PLAYER COMPUTER" lw3 = d.textlength(sub, font=f3)/S d.rectangle([WB/2-lw/2-22, HB*0.44-12, WB/2+lw/2+22, HB*0.44+96], outline=tuple(int(v*al) for v in LINE), width=3) d.text((WB/2-lw/2, HB*0.44), shot.card, font=f, fill=tuple(int(v*al) for v in (240, 248, 252))) d.line([WB/2-lw/2, HB*0.44+64, WB/2+lw/2, HB*0.44+64], fill=tuple(int(v*al) for v in DIM), width=2) d.text((WB/2-lw3/2, HB*0.44+72), sub, font=f3, fill=tuple(int(v*al) for v in AMBER)) # the drawing's title block. The revision number is part of the drawing, # not a frame counter — it no longer ticks with the clock. d.text((28, HB-46), "DWG 07 — VERTICAL TRANSPORT", font=font(15), fill=(120, 168, 200)) d.text((WB-170, HB-46), "REV 3", font=font(15), fill=(120, 168, 200)) 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 = PX(300), PX(193) lab = PX(24) sheet = Image.new("RGB", (cols*tw, rows*(th+lab)), (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+lab) sheet.paste(im, (cx, cy)) sd.text((cx+PX(5), cy+th+PX(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={sd} {SETNUM} — {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: {W}x{H} (16:9)\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()