#!/usr/bin/env python3 # ═════════════════════════════════════════════════════════════════════════════ # PLAYER COMPUTER — Night Shift (12/32) # by Gene Kogan · 2026 · https://genekogan.com/player_computer/night_shift # # The data-labelling night shift, one bounding box at a time, until four a.m. # # 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/night_shift.py.txt # # The original render (for reference, yours should differ): # video: https://genekogan.com/player_computer/media/night_shift.mp4 # cover: https://genekogan.com/player_computer/media/night_shift.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 night_shift.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 — "NIGHT SHIFT" (tightened cut) Jazz-hop / boom bap, 88bpm, D minor. Sung hook, spoken verses. Recomposed from renders/second_nature/night_shift (43 bars / 119 s) down to 24 bars / ~67 s for the final-curation set. Not a speed-up and not a truncation: the intro is halved, three verses become two (six strongest lines each, verse 2 fusing the "it learned" and "you're talking to everyone" strands), the third hook is gone so the two that remain are the only two, and the outro lands on the bridge's last lines over the punch clock. Tempo, mix, palette, engines and post chain are untouched. The machines are fluent because somebody sat up all night telling them what things were. This is that room: a floor of workstations, a ticker of labels being applied, boxes drawn around blobs until the blobs have names, a face resolving out of characters, a punch clock, rain on a window at four a.m. The hook is sung — `say` speech through a channel vocoder, the carrier carrying the melody. The verses are spoken flat, because they are the work. Composition: engine : audio-first x shot-parallel content: audio-groove (swung boom-bap kit, upright bass, rhodes, vibes, muted trumpet, vinyl) x tts-voices (Moira sung, Alex spoken) x generative-art x effects-post FINAL CUT (player_computer_final): * Native 1920x1080. Two authored coordinate systems — the 640x360 engine canvas and the 1280x720 delivery frame — both go through one scale, S = 1.5, via a ScaledDraw proxy; fonts are scaled once in font(). The canvas rasterises at 960x540 so the 2x LANCZOS bloom that gives the room its soft look is the same 2x it always was. Bloom radius, the rain-glass blur, the chroma offset and the grain cell all scale with S; the CRT scanline in the ticker is derived from the authored row, so it stays a scanline instead of turning into fine static. * Stripped: the section-name / running-timecode strip along the bottom. Renderer debug. The labels flying through the ticker, the box captions, the punch-clock IN 04:0n rows and the subtitles are all the film and stay. * Title flash: the NIGHT SHIFT card gains "PLAYER COMPUTER" under a thin CRT-green rule; the later SHIP AT 06:00 card is left alone. """ import argparse, datetime, hashlib, math, os, subprocess, wave from pathlib import Path import numpy as np from PIL import Image, ImageDraw, ImageFont, ImageFilter NAME = "night_shift" TITLE = "NIGHT SHIFT" SETNUM = "player_computer_final" # ── delivery scale ─────────────────────────────────────────────────────────── # AW/AH are the authored delivery frame; W/H are real pixels; S is the only # number the look scales by. The engine canvas (SW_/SH_) is authored in its own # units and rasterised through the same S, so the 2x bloom upscale on top of it # is preserved exactly rather than becoming a softer 3x. AW, AH = 1280, 720 W, H, FPS = 1920, 1080, 24 S = H / AH def P(v): return int(round(v*S)) def B(r): return r*S def _sxy(v, s): if isinstance(v, (list, tuple)): return [_sxy(u, s) for u in v] return v*s class ScaledDraw: """ImageDraw proxy: authored units in, pixels out. Only the first positional arg (xy) and `width` are touched — arc/chord/pieslice take angles positionally and those 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))) return f(_sxy(xy, s), *a, **kw) return wrapped def mkdraw(im): d = ImageDraw.Draw(im) return d if S == 1.0 else ScaledDraw(d, S) BPM = 88.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" # ── the tightened arrangement ──────────────────────────────────────────────── # 43 bars -> 24. The original ran intro(4) v1(8) hk1(4) v2(8) hk2(4) v3(8) # hk3(4) outro(3); every section here was rebuilt at a shorter length rather # than clipped, and verse3 + hook3 are gone entirely. SECTIONS = [ ("intro", 0, 2), ("verse1", 2, 8), ("hook1", 8, 12), ("verse2", 12, 18), ("hook2", 18, 22), ("outro", 22, 24), ] SEC_START = {n: a for n, a, _ in SECTIONS} HOOK_BARS = (8, 18) # where the two sung hooks land N_BARS = SECTIONS[-1][2] DUR = N_BARS * BAR + 1.6 N_FRAMES = int(DUR * FPS) MUSIC_DESC = f"jazz-hop / boom bap, {BPM:.0f}bpm, D minor, {N_BARS} bars, sung hook (vocoder) + spoken verses" ENGINE_DESC = "floor / ticker / boxes / asciiface / clock / window" 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.17 # heavy swing — this is the genre D2, D3 = nf("D2"), nf("D3") PROG = [ # one chord per bar, 4-bar loop ("Dm9", nf("D2"), [nf("F3"), nf("A3"), nf("C4"), nf("E4")]), ("Gm7", nf("G2"), [nf("Bb3"), nf("D4"), nf("F4"), nf("A4")]), ("Bbmaj7",nf("Bb1"), [nf("D3"), nf("F3"), nf("A3"), nf("D4")]), ("A7sus", nf("A1"), [nf("D3"), nf("G3"), nf("A3"), nf("C4")]), ] A4, G4, F4, E4, D4 = nf("A4"), nf("G4"), nf("F4"), nf("E4"), nf("D4") Bb4, C5, D5 = nf("Bb4"), nf("C5"), nf("D5") HOOK = [ ("we stayed up teaching it to see", [(A4,2),(A4,1),(G4,1),(F4,1),(G4,2),(F4,3)]), ("nobody wrote our names down", [(F4,2),(E4,1),(D4,2),(E4,1),(D4,4)]), ] HOOK_B = [ ("every answer that it gives you", [(C5,2),(C5,1),(Bb4,1),(A4,2),(G4,1),(A4,3)]), ("came through somebody's window at four", [(A4,1),(G4,1),(F4,1),(G4,2),(F4,1),(E4,2),(D4,4)]), ] # one line per bar at 88bpm — long enough to land, short enough to rap. # Six lines per verse in this cut, down from eight. # # VERSE1 keeps the work itself and drops the two lines that only restated it # ("is this a road, is this a river…" and "it refills faster than the hands"), # so the six that remain run task -> queue -> rhythm -> pay -> penalty -> punchline. VERSE1 = ["forty thousand boxes round forty thousand cats", "clock in at eleven and the queue is already deep", "draw the box, tag the box, send the box, next", "twelve cents a frame and a warning in the text", "flag it unsure and they dock you for the doubt", "so nobody flags it unsure. that's what unsure's about"] # VERSE2 fuses the old verse2 and verse3: the two best "it learned" lines, the # room couplet that was the old verse2's landing, and the thesis couplet that # closed verse3. The "they say it woke up / carried here" run and the # "we are the part they don't put in the paper" pair are cut. VERSE2 = ["it learned the word for grief from a woman who was tired", "learned please from a queue, learned sorry from a form", "the model has no memory of the room", "but the room remembers. the room remembers who", "you're not talking to a mind, not yet, not quite", "you're talking to everyone who worked last night"] # The bridge is now the outro: two lines, the title line and the ship line. BRIDGE = ["the ground is a night shift", "and the morning ships it"] ADLIB = ["(yeah)", "(uh)", "(c'mon)", "(all night)", "(say it)", "(yeah)"] def brushnoise(dur=.30, seed=91): n = int(dur*SR); t = np.arange(n)/SR nz = bandshape(np.random.RandomState(seed).randn(n), lo=900, hi=5200) return nz*(np.exp(-t*7)*(0.35+0.65*np.sin(t*52)**2))*0.55 def build_song(): s = Song(DUR) R = np.random.RandomState(880) def sec_of(bar): for nm, a, b in SECTIONS: if a <= bar < b: return nm return "outro" for bar in range(N_BARS): sec = sec_of(bar) # section-relative vamp. In the long cut every section began on a # multiple of 4 and therefore on Dm9; the shortened sections would have # slid the loop out from under the vocals, so the 4-bar vamp restarts # with each section and every line sits over the chord it was written on. cname, root, notes = PROG[(bar - SEC_START[sec]) % 4] hooky = sec.startswith("hook") versey = sec.startswith("verse") # ---- drums: swung boom bap --------------------------------------- # the arrangement thins out on purpose: no kit at all in the intro, # the first half of the bridge, or the last two bars no_kit = (sec == "intro") or (bar >= N_BARS - 1) if True: if not no_kit: for st, v in ((0, 1.0), (7, .62), (10, .82)): at = s.t(bar, st, SW) s.put("drums", kick(f0=140, f1=50, punch=26)*v, at, g=.92) if v > .7: s.kick_t.append(at) for st in (4, 12): s.put("drums", snare(dur=.26, tone=186, bright=1.05), s.t(bar, st, SW), g=.78, pan=-.04) if bar % 4 == 3: s.put("drums", snare(dur=.13, bright=.6), s.t(bar, 14, SW), g=.32, pan=.12) s.put("drums", snare(dur=.11, bright=.5), s.t(bar, 15, SW), g=.26, pan=-.12) for st in range(0, 16, 2): op = (st == 6 and bar % 2 == 1) s.put("drums", hat(openh=op), s.t(bar, st, SW), g=(.26 if not op else .20) + .07*R.rand(), pan=.30) if sec in ("verse2", "hook2"): for st in (3, 11): s.put("drums", shaker(), s.t(bar, st, SW), g=.22, pan=-.3) elif sec == "bridge": # brushed pulse only for st in (0, 8): s.put("drums", brushnoise(), s.t(bar, st, SW), g=.30, pan=-.2) if bar % 2 == 1: s.put("drums", rim(), s.t(bar, 12, SW), g=.34, pan=.28) # ---- upright bass: walking in verses, root-fifth in hooks -------- if sec == "intro": pass elif versey or sec == "bridge": walk = [0, 3, 5, 7][:4] for j, st in enumerate((0, 4, 8, 12)): f = root * 2 ** (walk[(j + bar) % 4]/12.0) s.put("bass", voice(f, BEAT*.92, kind="tri", nh=12, c0=760, c1=210, ck=7.5, a=.006, d=.14, s=.42, r=.16, seed=bar*5+j), s.t(bar, st, SW), g=.46) else: for st, ln in ((0, 1.8), (6, .8), (10, 1.4)): f = root if st != 6 else root*1.5 s.put("bass", voice(f, BEAT*ln, kind="tri", nh=12, c0=700, c1=190, ck=6.0, a=.006, d=.18, s=.5, r=.2, seed=bar*7+st), s.t(bar, st, SW), g=.44) # ---- rhodes comping ---------------------------------------------- if sec != "intro" or bar >= 1: hits = [(2, .9), (6, .6), (11, .8)] if versey else [(0, 1.3), (7, .9)] for st, ln in hits: for k, f in enumerate(notes): s.put("rhodes", fm(f, BEAT*ln, ratio=2.0, index=2.3+0.5*k, idec=5.5, a=.005, d=.5, s=.22, r=.35, seed=bar*11+k), s.t(bar, st, SW), g=.17, pan=-.35 + .23*k) # ---- vibraphone sparkle ------------------------------------------- if hooky: # hook-only doubling for k2, f2 in enumerate(notes[1:3]): s.put("vibes", fm(f2*2, 1.3, ratio=6.0, index=2.4, idec=9.0, d=.8, r=.5, seed=bar*23+k2), s.t(bar, 4 if k2 == 0 else 12, SW), g=.09, pan=-.4+.8*k2) if hooky or sec == "outro": f = notes[(bar + 1) % len(notes)] * 2 s.put("vibes", fm(f, 1.9, ratio=4.02, index=3.2, idec=7.0, d=1.1, r=.7, seed=bar*13), s.t(bar, 8, SW), g=.13, pan=.38) # ---- muted trumpet in the bridge ---------------------------------- if sec == "outro" and bar == N_BARS - 1: f = [nf("D4"), nf("F4"), nf("E4"), nf("A3")][(bar - SEC_START["outro"]) % 4] s.put("horn", voice(f, BEAT*2.4, kind="saw", nh=16, c0=2300, c1=900, ck=3.0, res=.5, vib=(.03, 5.4), a=.09, d=.4, s=.62, r=.5, seed=bar*17), s.t(bar, 4, SW), g=.16, pan=-.2) # ---- pad bed ------------------------------------------------------- if sec in ("intro", "bridge", "outro"): for k, f in enumerate(notes[:3]): s.put("pad", voice(f/2, BAR*1.1, kind="saw", nh=18, c0=1500, c1=800, ck=.9, detune=(-1.2, 0, 1.3), a=.7, d=.7, s=.7, r=.9, seed=bar*19+k), s.t(bar), g=.10, pan=-.5+.4*k) # ---- VOICES ------------------------------------------------------------ # rap: ONE LINE PER BAR, landing just after the downbeat, ad-libs on the # back half. Each line's real duration is recorded so the subtitles match # the audio rather than guessing at it. SUBS = [] SECD0 = {n: a for n, a, b in SECTIONS} def rap(sec_name, lines, voice="Alex", rate=196, gain=.52, adlib=True): b0 = SECD0[sec_name] for i, line in enumerate(lines): at = (b0 + i)*BAR + BEAT*0.42 sig = speak(line, voice=voice, rate=rate, cache=AUD) s.put("vox_sp", sig, at, g=gain, pan=-.06) SUBS.append((at, at + len(sig)/SR + 0.30, line)) if adlib and i % 2 == 1: al = ADLIB[(i//2) % len(ADLIB)] asig = speak(al, voice="Fred", rate=210, cache=AUD) s.put("vox_sp", asig, at + BEAT*2.55, g=.22, pan=.34) rap("verse1", VERSE1, voice="Alex", rate=198) rap("verse2", VERSE2, voice="Moira", rate=192) rap("outro", BRIDGE, voice="Alex", rate=150, gain=.46, adlib=False) # sung hooks: the vocoder line, doubled an octave down at low level. # Two now, not three — the B lyric is the second and last word of the song # before the bridge, so it no longer has to compete with a reprise. for hb, lines in ((HOOK_BARS[0], HOOK), (HOOK_BARS[1], HOOK_B)): for i, (text, mel) in enumerate(lines): SUBS.append(((hb + i*2)*BAR + BEAT*0.25, (hb + i*2)*BAR + BEAT*0.25 + BAR*2*0.92, text.upper())) dur = BAR*2*0.92 lead = sing(text, mel, dur, voice="Moira", rate=168, cache=AUD, detune=(0.0, -0.6, 0.7), vib=(.014, 5.0)) s.put("vox", lead, (hb + i*2)*BAR + BEAT*0.25, g=.54, pan=0.0) low = sing(text, [(f/2, w) for f, w in mel], dur, voice="Moira", rate=168, cache=AUD, detune=(0.0, -1.1), vib=(.008, 4.4)) s.put("vox", low, (hb + i*2)*BAR + BEAT*0.25, g=.15, pan=.0) # ---- one-shots --------------------------------------------------------- s.put("fx", vinyl(int(DUR*SR)), 0.0, g=.45) for b in HOOK_BARS: s.put("fx", crash(dur=1.2), b*BAR, g=.22, pan=.15) s.put("fx", riser(BAR*1.6), HOOK_BARS[1]*BAR - BAR*1.6, g=.16) # ---- bus FX ------------------------------------------------------------- s.bus("rhodes", lambda x: reverb(delay(x, BEAT*.75, .30, .18), rt=1.5, mix=.26, seed=61)) s.bus("vibes", lambda x: reverb(delay(x, BEAT*1.5, .40, .30), rt=2.6, mix=.46, seed=67)) s.bus("horn", lambda x: reverb(x, rt=2.2, mix=.34, seed=71)) s.bus("pad", lambda x: reverb(x, rt=3.0, mix=.50, seed=73)) s.bus("vox", lambda x: reverb(delay(x, BEAT*.75, .26, .16), rt=1.7, mix=.28, seed=79)) s.bus("vox_sp", lambda x: reverb(x, rt=1.0, mix=.16, seed=83)) mix = s.mixdown(dict(drums=1.0, bass=1.0, rhodes=1.0, vibes=1.0, horn=1.0, pad=1.0, vox=1.0, vox_sp=1.0, fx=1.0), pump_depth=.16, pump_rel=.13, levels=dict(intro=.30, verse1=.76, hook1=1.0, verse2=.82, hook2=1.0, outro=.52)) np.savez(AUD/"subs.npz", t0=np.array([a for a, b, c in SUBS]), t1=np.array([b for a, b, c in SUBS]), tx=np.array([c for a, b, c in SUBS], dtype=object)) 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 # ════════════════════════════════════════════════════════════════════════════ # VISUAL ENGINES # ════════════════════════════════════════════════════════════════════════════ SW_, SH_ = 640, 360 # 2x sim res — this piece draws figures NEUTRAL = {"rms": .5, "low": .4, "mid": .4, "high": .3, "kick": .2} PAL = { "ink": (12, 12, 16), "night": (20, 26, 40), "desk": (44, 40, 46), "crt": (128, 226, 176), "amber": (238, 172, 74), "sodium": (250, 196, 120), "bone": (222, 218, 206), "blood": (196, 72, 66), "cool": (78, 108, 150), } 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 RAMPS = { "room": ramp([PAL["ink"], PAL["night"], PAL["desk"], PAL["sodium"], PAL["bone"]]), "crt": ramp([(4,8,6), (10, 40, 26), PAL["crt"], PAL["bone"]]), "amber": ramp([PAL["ink"], (60, 34, 18), PAL["amber"], PAL["bone"]]), } def _img(w=SW_, h=SH_, bg=PAL["ink"]): """Canvas in engine units; the raster is S times bigger and the proxy scales every coordinate into it, so the engines below are unchanged.""" im = Image.new("RGB", (P(w), P(h)), bg); return im, mkdraw(im) class Floor: """An isometric floor of workstations. Small figures, lit from the front.""" def __init__(self, shot, rng): self.rng = rng self.cols = int(rng.integers(2, 4)); self.rows = int(rng.integers(2, 4)) n = self.cols*self.rows self.on = rng.random(n) > 0.18 self.ph = rng.random(n)*6.28 self.rate = 0.05 + rng.random(n)*0.14 self.lean = rng.random(n) self.cam = float(rng.uniform(-0.4, 0.4)) self.zoom = float(rng.uniform(0.85, 1.30)) def frame(self, k, u, e): im, d = _img() cx, cy = SW_*0.5 + self.cam*80, SH_*0.30 ax, ay = 165*self.zoom, 74*self.zoom for r in range(self.rows): for c in range(self.cols): i = r*self.cols + c ox = (c - self.cols/2 + 0.5); oy = (r - self.rows/2 + 0.5) x = cx + (ox - oy)*ax; y = cy + (ox + oy)*ay + k*0.20 % 1 if not (-60 < x < SW_+60 and -40 < y < SH_+70): continue dep = 0.45 + 0.55*(r+1)/self.rows fl = 0.45 + 0.55*(0.5+0.5*math.sin(k*self.rate[i]*6 + self.ph[i])) gl = fl*(0.45 + 0.85*e["mid"]) if self.on[i] else 0.06 # the pool of monitor light on the desk — drawn first, and it # is what makes the station read as a lit place rather than a chip for q in range(5, 0, -1): sc = q/5.0 d.polygon([(x-64*sc, y), (x, y-30*sc), (x+64*sc, y), (x, y+30*sc)], fill=tuple(min(255, int(v*gl*0.16*(1.2-sc)+8)) for v in PAL["crt"])) d.polygon([(x-62, y), (x, y-29), (x+62, y), (x, y+29)], outline=tuple(int(v*dep*0.8) for v in PAL["desk"])) if not self.on[i]: continue mw, mh = 48, 32 # monitor d.rectangle([x-mw//2, y-58, x+mw//2, y-58+mh], fill=tuple(min(255, int(v*gl)) for v in PAL["crt"])) d.rectangle([x-mw//2, y-58, x+mw//2, y-58+mh], outline=(30, 34, 40), width=2) for q in range(4): # rows of work on the screen yy2 = y-54+q*7 d.line([x-mw//2+4, yy2, x-mw//2+4+int(mw*0.7*abs(math.sin(k*0.09+q+i))), yy2], fill=(14, 22, 18), width=2) hy = y - 14 - int(6*self.lean[i]) # figure, from behind col = tuple(int(v*(0.30+0.55*gl)) for v in PAL["cool"]) d.ellipse([x-14, hy-28, x+14, hy], fill=col) d.polygon([(x-30, y+16), (x-20, hy-2), (x+20, hy-2), (x+30, y+16)], fill=col) d.line([(x-20, hy+4), (x-34, y+2)], fill=col, width=7) # arms out d.line([(x+20, hy+4), (x+34, y+2)], fill=col, width=7) a = np.asarray(im, np.float32) # sodium ceiling wash yy = np.linspace(0, 1, a.shape[0])[:, None, None] a += (0.10 + 0.14*e["rms"])*np.array(PAL["sodium"], np.float32)*(1-yy)**3 return np.clip(a, 0, 255) class Ticker: """Labels being applied, one after another, forever.""" WORDS = ["ROAD", "NOT ROAD", "SIDEWALK", "PERSON", "NOT PERSON", "SHADOW", "HARM", "NO HARM", "UNSURE", "VEHICLE", "SKY", "WATER", "TEXT", "FACE", "NOT FACE", "ANIMAL", "WEAPON", "TOY", "SIGN", "NIGHT"] def __init__(self, shot, rng): self.rng = rng self.nrow = int(rng.integers(7, 15)) self.spd = rng.uniform(0.6, 2.6, self.nrow)*(np.where(rng.random(self.nrow) < .5, -1, 1)) self.off = rng.random(self.nrow)*SW_ self.pick = rng.integers(0, len(self.WORDS), (self.nrow, 9)) self.hot = rng.random((self.nrow, 9)) > 0.86 self.fs = int(rng.integers(15, 24)) def frame(self, k, u, e): im, d = _img(bg=(8, 9, 12)) f = font(self.fs) rh = SH_/self.nrow for r in range(self.nrow): y = int(r*rh + rh*0.18) x0 = (self.off[r] + k*self.spd[r]*2.2) % (SW_*1.6) - SW_*0.3 for c in range(9): w = self.WORDS[self.pick[r, c]] x = int(x0 + c*128) if x < -110 or x > SW_+10: continue if self.hot[r, c]: col = tuple(int(v*(0.6+0.7*e["high"])) for v in PAL["amber"]) else: col = tuple(int(v*(0.30+0.35*e["mid"])) for v in PAL["crt"]) d.text((x, y), w, font=f, fill=col) a = np.asarray(im, np.float32) # the CRT line pairs off ENGINE rows, not raster rows — otherwise the # scanline turns into per-pixel static at a higher delivery scale scan = (0.80 + 0.20*np.cos(np.floor(np.arange(a.shape[0])/S)*math.pi))[:, None, None] return np.clip(a*scan, 0, 255) class Boxes: """Bounding boxes drawn around blobs until the blobs have names.""" def __init__(self, shot, rng): self.rng = rng sd = int(rng.integers(1e6)) self.bg = fbm(SH_, SW_, float(rng.uniform(40, 96)), sd, 5) self.bg = (self.bg - self.bg.min())/(np.ptp(self.bg)+1e-9) self.n = int(rng.integers(3, 8)) self.bx = rng.uniform(0.10, 0.72, self.n) self.by = rng.uniform(0.12, 0.66, self.n) self.bw = rng.uniform(0.12, 0.28, self.n) self.bh = rng.uniform(0.14, 0.32, self.n) self.lab = rng.integers(0, len(Ticker.WORDS), self.n) # the boxes land over the first ~45% of the shot rather than the first # 85%: shots in this cut are shorter than the ones this engine was # written for, and at 0.85 a short shot could reach its midpoint with # nothing drawn on it at all self.when = np.sort(rng.uniform(0.04, 0.45, self.n)) self.ramp = RAMPS["room"] if rng.random() < .6 else RAMPS["amber"] def frame(self, k, u, e): v = np.clip(self.bg**1.5*0.85 + 0.04, 0, 1) a = apply_ramp(v, self.ramp)*0.34 yy2 = np.linspace(-1, 1, SH_)[:, None]; xx2 = np.linspace(-1, 1, SW_)[None, :] a *= np.clip(1.0 - 0.45*(xx2**2 + yy2**2), 0.2, 1)[..., None] im = Image.fromarray(np.clip(a, 0, 255).astype(np.uint8)) if S != 1.0: im = im.resize((P(SW_), P(SH_)), Image.BILINEAR) d = mkdraw(im); f = font(14) for i in range(self.n): if u < self.when[i]: continue age = (u - self.when[i])/max(1e-3, (1.0 - self.when[i])) grow = min(1.0, age*5.0) x0 = int(self.bx[i]*SW_); y0 = int(self.by[i]*SH_) x1 = int(x0 + self.bw[i]*SW_*grow); y1 = int(y0 + self.bh[i]*SH_*grow) col = tuple(int(v2*(0.7+0.5*e["mid"])) for v2 in PAL["crt"]) d.rectangle([x0, y0, x1, y1], outline=col, width=2) for (hx, hy) in ((x0, y0), (x1, y0), (x0, y1), (x1, y1)): d.rectangle([hx-2, hy-2, hx+2, hy+2], fill=col) if grow >= 1.0: d.rectangle([x0, y0-15, x0+9*len(Ticker.WORDS[self.lab[i]]), y0-1], fill=col) d.text((x0+2, y0-15), Ticker.WORDS[self.lab[i]], font=f, fill=(10, 14, 12)) return np.asarray(im, np.float32) class AsciiFace: """A face resolving out of characters. It is never quite a face.""" CH = " .:-=+*#%@" def __init__(self, shot, rng): self.rng = rng self.cw = int(rng.integers(10, 15)); self.chh = int(self.cw*1.75) self.gw = SW_//self.cw; self.gh = SH_//self.chh yy, xx = np.mgrid[0:self.gh, 0:self.gw].astype(np.float32) nx = (xx/self.gw - 0.5)*2.0; ny = (yy/self.gh - 0.5)*2.0 face = np.exp(-((nx/0.60)**2 + (ny/0.86)**2)*1.9) brow = np.exp(-((nx/0.62)**2 + ((ny+0.34)/0.10)**2))*0.55 eyes = (np.exp(-(((nx+0.26)/0.11)**2 + ((ny+0.14)/0.075)**2)) + np.exp(-(((nx-0.26)/0.11)**2 + ((ny+0.14)/0.075)**2))) nose = np.exp(-((nx/0.07)**2 + ((ny-0.10)/0.20)**2))*0.35 mouth = np.exp(-((nx/0.28)**2 + ((ny-0.44)/0.055)**2)) self.f = np.clip(face + brow + nose - eyes*1.5 - mouth*1.1, 0, 1) self.f = self.f**0.75 self.nz = fbm(self.gh, self.gw, 7, int(rng.integers(1e6)), 4) self.col = PAL["crt"] if rng.random() < .7 else PAL["amber"] def frame(self, k, u, e): im, d = _img(bg=(6, 8, 10)) f = font(self.chh-2) res = np.clip(0.15 + u*1.25 + 0.4*e["rms"], 0, 1) # resolves over the shot v = np.clip(self.f*res + self.nz*(1.05-res)*0.9, 0, 1) v = np.clip(v*(0.7+0.6*e["mid"]), 0, 1) idx = (v*(len(self.CH)-1)).astype(int) for gy in range(self.gh): row = "".join(self.CH[i] for i in idx[gy]) sh = int(255*(0.35 + 0.65*v[gy].mean())) d.text((0, gy*self.chh), row, font=f, fill=tuple(min(255, int(c*sh/255)) for c in self.col)) return np.asarray(im, np.float32) class Clock: """A punch clock. The hand does not stop for the chorus.""" def __init__(self, shot, rng): self.rng = rng self.spin = float(rng.uniform(0.5, 3.0)) self.cards = int(rng.integers(5, 11)) self.wob = float(rng.uniform(0, 0.03)) def frame(self, k, u, e): im, d = _img(bg=(14, 14, 18)) cx, cy, R = SW_*0.34, SH_*0.5, SH_*0.34 d.ellipse([cx-R, cy-R, cx+R, cy+R], outline=PAL["bone"], width=3) for j in range(12): a = j/12*math.tau d.line([cx+math.sin(a)*R*0.86, cy-math.cos(a)*R*0.86, cx+math.sin(a)*R*0.96, cy-math.cos(a)*R*0.96], fill=PAL["bone"], width=2 if j % 3 == 0 else 1) ta = (k*0.006*self.spin + self.wob*math.sin(k*0.2))*math.tau d.line([cx, cy, cx+math.sin(ta*12)*R*0.80, cy-math.cos(ta*12)*R*0.80], fill=PAL["blood"], width=2) d.line([cx, cy, cx+math.sin(ta)*R*0.62, cy-math.cos(ta)*R*0.62], fill=PAL["bone"], width=4) d.ellipse([cx-4, cy-4, cx+4, cy+4], fill=PAL["amber"]) # the rack of cards f = font(13) for j in range(self.cards): y = int(SH_*0.12 + j*(SH_*0.76/self.cards)) lit = (j == int((k*0.06) % self.cards)) col = PAL["amber"] if lit else (72, 70, 66) d.rectangle([SW_*0.66, y, SW_*0.94, y+int(SH_*0.055)], outline=col, width=1) if lit: d.text((SW_*0.68, y+3), "IN 04:0%d" % (j % 10), font=f, fill=col) a = np.asarray(im, np.float32) a += (0.12*e["kick"])*np.array(PAL["bone"], np.float32) return np.clip(a, 0, 255) class Window: """Rain on glass. The city is out there being described.""" def __init__(self, shot, rng): self.rng = rng self.nb = int(rng.integers(30, 80)) self.bx = rng.random(self.nb); self.by = rng.random(self.nb)*0.7+0.15 self.br = rng.random(self.nb)*7+2 self.bc = rng.integers(0, 3, self.nb) self.nd = int(rng.integers(60, 170)) self.dx = rng.random(self.nd)*SW_ self.dy = rng.random(self.nd)*SH_ self.dv = rng.random(self.nd)*2.0+0.5 self.dl = rng.random(self.nd)*22+6 def frame(self, k, u, e): im, d = _img(bg=(10, 13, 22)) cols = [PAL["sodium"], PAL["crt"], PAL["cool"]] for i in range(self.nb): x = self.bx[i]*SW_; y = self.by[i]*SH_ r = self.br[i]*(0.8+0.5*e["mid"]) c = cols[self.bc[i]] g = 0.16 + 0.5*abs(math.sin(k*0.03 + i)) d.ellipse([x-r, y-r, x+r, y+r], fill=tuple(int(v*g) for v in c)) im = im.filter(ImageFilter.GaussianBlur(B(3.2))) d = mkdraw(im) yy = (self.dy + k*self.dv*3.0*(1+1.3*e["rms"])) % (SH_+40) for i in range(self.nd): x = self.dx[i]; y = yy[i] d.line([x, y, x-1, y+self.dl[i]], fill=(150, 170, 190), width=1) a = np.asarray(im, np.float32) a *= (0.55 + 0.45*np.linspace(1, 0.5, a.shape[0]))[:, None, None] return np.clip(a, 0, 255) ENGINES = {"floor": Floor, "ticker": Ticker, "boxes": Boxes, "asciiface": AsciiFace, "clock": Clock, "window": Window} # shot menus are in beats; the shorter sections get shorter menus so the cut # rate stays where it was rather than stretching to fill a smaller box PLAN = { "intro": (["window", "floor"], [4]), "verse1": (["floor", "boxes", "ticker"], [4, 8, 4]), "hook1": (["asciiface", "floor", "ticker"], [4, 8, 4]), "verse2": (["boxes", "clock", "ticker", "floor"], [8, 4, 4, 8]), "hook2": (["asciiface", "window"], [8, 4, 4]), "outro": (["floor", "clock"], [4]), } CARDS = { "intro": "NIGHT SHIFT", "verse1": None, "hook1": None, "verse2": None, "hook2": None, "outro": "SHIP AT 06:00", } SYSTEM_NAMES = ["QUEUE", "BATCH", "REVIEW", "APPEAL", "PAYOUT", "CONSENSUS"] 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: 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 # the punch clock is the landing of the piece — make it the last shot # outright instead of leaving it to the pool, and hang the outro card # on it rather than on whatever happened to open the outro shots[-1].engine = "clock" if len(shots) > 1 and shots[-2].engine == "clock": # don't let the forced landing create a repeat, and don't resolve it # into whatever precedes the pair either shots[-2].engine = "window" if shots[-3].engine != "window" else "floor" for sh in shots: if sh.section == "outro": sh.card = None shots[-1].card = CARDS["outro"] 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) _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: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"] _SUBS = {} def subs(): if not _SUBS: p = AUD/"subs.npz" if p.exists(): z = np.load(p, allow_pickle=True) _SUBS["t0"], _SUBS["t1"], _SUBS["tx"] = z["t0"], z["t1"], z["tx"] else: _SUBS["t0"] = _SUBS["t1"] = np.zeros(0) _SUBS["tx"] = np.zeros(0, dtype=object) return _SUBS def draw_sub(d, t): """One line at a time, bottom-centred, boxed. Composited AFTER the channel shift so the colour fringing never touches the words (AESTHETIC 13b).""" SB = subs() # not `S` — that is the delivery scale if not len(SB["t0"]): return m = np.where((SB["t0"] <= t) & (t < SB["t1"]))[0] if not len(m): return txt = str(SB["tx"][m[-1]]) f = font(30); tw = f.getlength(txt)/S # fonts are pre-scaled if tw > AW*0.90: f = font(24); tw = f.getlength(txt)/S x = (AW-tw)/2; y = AH-106 d.rectangle([x-18, y-9, x+tw+18, y+42], fill=(0, 0, 0)) d.text((x, y), txt, font=f, fill=(246, 242, 232)) def post(arr_small, i, e, shot): img = Image.fromarray(np.clip(arr_small, 0, 255).astype(np.uint8)) if img.size != (W, H): img = img.resize((W, H), Image.LANCZOS) sm = img.resize((W//4, H//4), Image.BILINEAR).filter( ImageFilter.GaussianBlur(B(5))).resize((W, H), Image.BILINEAR) a = np.clip(np.asarray(img, np.float32) + np.asarray(sm, np.float32)*(0.22+0.26*e["high"]), 0, 255) lum = a.mean(2, keepdims=True)/255.0 a = a + (1-lum)*np.array([-6, -2, 10], np.float32) + lum*np.array([14, 6, -10], np.float32) sh = int(round((1 + 3*e["kick"])*S)) # a pixel-count effect: scale it if sh > 1: a[..., 0] = np.roll(a[..., 0], sh, axis=1) a[..., 2] = np.roll(a[..., 2], -sh, axis=1) a *= vignette() rng = np.random.RandomState(9100 + i) if S == 1.0: a += rng.normal(0, 3.4, a.shape) # heavier film grain else: # grain is a look, not a resolution: authored on the 1280x720 grid and # blown up nearest-neighbour so a speck keeps its size on screen gn = rng.normal(0, 3.4, (AH, AW, 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) # (the section-name / timecode strip that used to run along the bottom was # renderer debug and is gone in the final cut) draw_sub(d, i/FPS) if shot.card: age = i - shot.i0 if age < FPS*2.4: al = min(1.0, age/6.0)*min(1.0, (FPS*2.4-age)/10.0) d.text((28, 34), shot.card, font=font(22), fill=tuple(int(c*al) for c in PAL["bone"])) # the show, under a thin CRT-green rule — title card only if shot.card == TITLE: fw = font(22).getlength(shot.card)/S d.rectangle([28, 64, 28+fw, 66], fill=tuple(int(c*al) for c in PAL["crt"])) d.text((28, 74), "PLAYER COMPUTER", font=font(13), fill=tuple(int(c*al*0.95) for c in PAL["crt"])) 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 = 300, 169 sheet = Image.new("RGB", (cols*tw, rows*(th+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+24) sheet.paste(im, (cx, cy)) sd.text((cx+5, cy+th+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…") 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/player_computer_final/{NAME}/render.py", "-metadata", f"title={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/player_computer_final/{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, native)\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()