Something went wrong. Try again.
GPT-6-Astra's self hype video render script
Something went wrong. Try again.
26 kB · 638 lines · Created 2mo ago Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638#!/usr/bin/env python3"""Inside the Loop: original procedural motion design and music, encoded by FFmpeg."""from __future__ import annotations
import argparsefrom functools import lru_cacheimport jsonimport mathimport osfrom pathlib import Pathimport subprocessimport timeimport wave
import numpy as npfrom PIL import Image, ImageChops, ImageDraw, ImageFilter, ImageFont
ROOT = Path(__file__).resolve().parentOUT = ROOT / "output"W, H, FPS = 1080, 1920, 30BPM, BEAT, DURATION = 144, 60 / 144, 30INK = (9, 14, 21)PAPER = (240, 238, 226)LIME = (218, 255, 93)BLUE = (70, 75, 246)ORANGE = (255, 111, 69)GRAY = (123, 142, 153)SCENES = [0, 4, 12, 20, 28, 36, 44, 52, 60, 68, 72]NAMES = ["INPUT", "ACTIVATE", "CONNECT", "EXPLORE", "DEBUG", "CREATE", "VERIFY", "REFINE", "COLLABORATE", "BUILD"]FFMPEG = str(ROOT / ".ffmpeg-bin/bin/ffmpeg") if (ROOT / ".ffmpeg-bin/bin/ffmpeg").exists() else "ffmpeg"
def clamp(x, a=0., b=1.): return min(b, max(a, x))
def ease(x): return 1 - (1 - clamp(x)) ** 3
def smooth(x): x = clamp(x) return x * x * (3 - 2 * x)
def mix(a, b, t): return tuple(round(x * (1 - t) + y * t) for x, y in zip(a, b))
@lru_cache(maxsize=160)def font(size, kind="bold"): name = {"bold": "Inter-Black.ttf", "medium": "Inter-Medium.ttf", "mono": "DejaVuSansMono.ttf"}[kind] return ImageFont.truetype(str(ROOT / "assets" / name), int(size))
@lru_cache(maxsize=512)def text_asset(text, size, color, kind="bold", outline=False): f = font(size, kind) box = f.getbbox(text, stroke_width=2 if outline else 0) im = Image.new("RGBA", (box[2] - box[0] + 8, box[3] - box[1] + 8)) d = ImageDraw.Draw(im) d.text((4 - box[0], 4 - box[1]), text, font=f, fill=(0, 0, 0, 0) if outline else color, stroke_width=2 if outline else 0, stroke_fill=color) return im
def label(im, text, x, y, size=26, color=GRAY, kind="mono", align="left"): tile = text_asset(text, size, color, kind) if align == "center": x -= tile.width / 2 elif align == "right": x -= tile.width im.paste(tile, (int(x), int(y)), tile)
def headline(im, lines, top, size, color=PAPER, accent=None, age=10, gap=20, left=76, max_width=930, stagger=.105): y = top for i, line in enumerate(lines): col = accent if accent and i == len(lines) - 1 else color tile = text_asset(line, size, col) if tile.width > max_width: tile = tile.resize((max_width, tile.height), Image.Resampling.LANCZOS) p = ease((age - i * stagger) / .42) if p > 0: visible = tile.copy() visible.putalpha(visible.getchannel("A").point(lambda a: int(a * p))) im.paste(visible, (int(left - (1 - p) * 140), int(y + (1 - p) * 54)), visible) y += tile.height + gap return y
@lru_cache(maxsize=10)def background(style): palettes = { "dark": (INK, (24, 35, 32)), "paper": (PAPER, (228, 229, 216)), "blue": (BLUE, (45, 34, 171)), "orange": (ORANGE, (228, 78, 48)), "lime": (LIME, (188, 235, 73)), } base, glow = palettes[style] yy, xx = np.mgrid[0:H, 0:W].astype(np.float32) halo = np.exp(-(((xx - 740) / 720) ** 2 + ((yy - 1180) / 800) ** 2)) grain = np.random.default_rng(808).normal(0, .65, (H, W)) arr = np.zeros((H, W, 3), dtype=np.uint8) for ch in range(3): arr[:, :, ch] = np.clip(base[ch] + (glow[ch] - base[ch]) * halo + grain, 0, 255) return Image.fromarray(arr)
def grid(im, color, step=108, offset=0): d = ImageDraw.Draw(im) for x in range(0, W, step): d.line((x, 0, x, H), fill=color, width=1) for y in range(-step, H + step, step): d.line((0, y + int(offset) % step, W, y + int(offset) % step), fill=color, width=1)
def cross(d, x, y, color, r=9, width=2): d.line((x - r, y, x + r, y), fill=color, width=width) d.line((x, y - r, x, y + r), fill=color, width=width)
def chrome(im, idx, t, dark=False): col = INK if dark else PAPER muted = mix(col, LIME if dark and idx == 8 else (PAPER if dark else INK), .45) d = ImageDraw.Draw(im) d.line((76, 138, 1004, 138), fill=muted, width=2) label(im, "CODEX / POV", 76, 87, 27, col) d.ellipse((975, 93, 992, 110), fill=col) label(im, f"{idx + 1:02d} / {NAMES[idx]}", 76, 168, 24, muted) # A transport-style footer stays inside the central safe area of a phone screen. label(im, "INSIDE THE LOOP", 76, 1706, 24, col) label(im, f"00:{min(29, int(t)):02d}", 1004, 1706, 24, col, align="right") for j in range(72): x = 78 + j * 12.95 d.line((x, 1770, x, 1784 if j % 4 else 1795), fill=col if j <= t / BEAT else muted, width=4 if j <= t / BEAT else 1)
def project(points, t, center=(540, 1220), scale=340, rot_speed=.45): p = np.asarray(points, dtype=float) a = t * rot_speed b = .55 + .25 * math.sin(t * .38) ry = np.array([[math.cos(a), 0, math.sin(a)], [0, 1, 0], [-math.sin(a), 0, math.cos(a)]]) rx = np.array([[1, 0, 0], [0, math.cos(b), -math.sin(b)], [0, math.sin(b), math.cos(b)]]) p = p @ ry.T @ rx.T factor = 4.6 / (4.6 + p[:, 2]) screen = np.column_stack((center[0] + p[:, 0] * scale * factor, center[1] + p[:, 1] * scale * factor)) return screen, p[:, 2]
def add_glow(im, layer, strength=.6, blur=12): small = layer.resize((W // 4, H // 4), Image.Resampling.BILINEAR).filter(ImageFilter.GaussianBlur(blur)) halo = small.resize((W, H), Image.Resampling.BILINEAR) if strength < 1: halo = Image.blend(Image.new("RGB", (W, H)), halo, strength) return ImageChops.add(ImageChops.add(im, halo), layer)
def torus(im, t, center=(540, 1260), scale=315, color=LIME, glow=True): layer = Image.new("RGB", (W, H)) d = ImageDraw.Draw(layer) beat = math.exp(-((t / BEAT) % 1) * 7) scale *= 1 + .025 * beat # A torus knot: three interwoven ribbons, no stock assets or logo tracing. for strand in range(24): u = np.linspace(0, math.tau, 190) v = strand * math.tau / 24 + t * .17 radius = 1.0 + .32 * np.cos(3 * u + v) p = np.column_stack((radius * np.cos(2 * u), radius * np.sin(2 * u), .32 * np.sin(3 * u + v))) q, z = project(p, t, center, scale) for j in range(len(q) - 1): brightness = .28 + .7 * clamp((z[j] + 1.1) / 2.2) c = tuple(int(x * brightness) for x in color) d.line([tuple(q[j]), tuple(q[j + 1])], fill=c, width=2) for k in range(7): u = t * .7 + k * math.tau / 7 p = [[(1 + .32 * math.cos(3*u)) * math.cos(2*u), (1 + .32 * math.cos(3*u)) * math.sin(2*u), .32 * math.sin(3*u)]] q, _ = project(p, t, center, scale) x, y = q[0] d.ellipse((x-5, y-5, x+5, y+5), fill=PAPER) return add_glow(im, layer, .7) if glow else ImageChops.add(im, layer)
def orbit_lines(im, t, center, radius, color, n=4): d = ImageDraw.Draw(im) for k in range(n): rr = radius + k * 23 a = math.degrees(t * .7 + k * 1.7) d.arc((center[0]-rr, center[1]-rr, center[0]+rr, center[1]+rr), a, a+230, fill=color, width=2)
RNG = np.random.default_rng(33)POINTS = RNG.normal(size=(36, 3))POINTS /= np.linalg.norm(POINTS, axis=1)[:, None]EDGES = [(i,j) for i in range(len(POINTS)) for j in range(i) if np.linalg.norm(POINTS[i]-POINTS[j]) < .75]
def network(im, t, age): d = ImageDraw.Draw(im) q, z = project(POINTS, t, (540, 1250), 365, .28) active = int(age * 13) for i, j in EDGES: d.line([tuple(q[i]), tuple(q[j])], fill=(169, 179, 165), width=2) for k, (i,j) in enumerate(EDGES[:max(0, active)]): p = ((age * .8 + k * .14) % 1) x,y = q[i] * (1-p) + q[j] * p d.ellipse((x-4,y-4,x+4,y+4), fill=BLUE) for i,(x,y) in enumerate(q): r = 6 if i % 4 else 11 d.ellipse((x-r, y-r, x+r, y+r), fill=BLUE if i < active else INK) for txt, idx in [("CONTEXT",3),("IDEAS",11),("CONSTRAINTS",18)]: x,y=q[idx] x,y=clamp(x,130,800),clamp(y,990,1510) tile=text_asset(txt,22, PAPER,"mono") d.rectangle((x-8,y-45,x+tile.width+8,y-9),fill=INK) im.paste(tile,(int(x),int(y-40)),tile)
def terminal(im, x, y, width, height, rows, age, active=0, accent=LIME): d = ImageDraw.Draw(im) d.rounded_rectangle((x+12,y+15,x+width+12,y+height+15), radius=18, fill=(0,0,0)) d.rounded_rectangle((x,y,x+width,y+height), radius=18, fill=(15,23,30), outline=(63,77,82), width=2) for k,col in enumerate([ORANGE,LIME,GRAY]): d.ellipse((x+27+k*25,y+26,x+37+k*25,y+36),fill=col) label(im,"workspace / live",x+width-24,y+23,20,GRAY,align="right") d.line((x,y+67,x+width,y+67),fill=(52,65,72),width=2) for k,row in enumerate(rows): start=k*.12 if age < start: continue line=row[0] color=row[1] if len(row)>1 else PAPER n=min(len(line), max(0,int((age-start)*70))) yy=y+98+k*53 if k==active: d.rectangle((x+14,yy-8,x+width-15,yy+40),fill=(33,48,44)) label(im,line[:n],x+28,yy,27,color)
def scene(idx, t, age): style = ["dark","dark","paper","dark","orange","blue","paper","dark","lime","dark"][idx] im = background(style).copy() dark = style in ("paper","orange","lime") d = ImageDraw.Draw(im)
if idx == 0: grid(im,(20,30,36),108,t*20) label(im,"A DAY IN THE PROCESS",76,325,25,GRAY) headline(im,["YOU","TYPE."],475,222,age=age,accent=LIME) p=ease(age/.9) d.rectangle((78,1100,1001,1282),outline=(67,82,79),width=2) label(im,">",109,1157,55,LIME) message="let's make something" typed=message[:int(clamp(age/.95)*len(message))] label(im,typed,182,1170,37,PAPER) if int(t*7)%2==0: xx=190+font(37,"mono").getlength(typed) d.rectangle((xx,1169,xx+15,1210),fill=LIME) label(im,"ONE PROMPT. SOMETHING STARTS.",78,1380,25,GRAY) d.rectangle((78,1460,78+int(924*p),1466),fill=LIME)
elif idx == 1: grid(im,(21,30,35),108,t*14) im=torus(im,t,scale=250) orbit_lines(im,t,(540,1260),340,(55,72,65),2) headline(im,["I GO","TO WORK."],350,202,age=age,accent=LIME) label(im,"CURIOSITY, IN MOTION.",78,845,27,PAPER) label(im,"READ / REASON / MAKE",540,1640,23,GRAY,align="center")
elif idx == 2: grid(im,(216,217,204),108) network(im,t,age) headline(im,["CONNECT","THE","DOTS."],340,179,INK,BLUE,age) label(im,"EVERY DETAIL CAN CHANGE THE PICTURE.",78,889,24,INK) label(im,"PATTERNS INTO POSSIBILITIES",540,1610,24,INK,align="center")
elif idx == 3: # Perspective corridor of files; speed lines converge underneath the title. cx,cy=540,1140 for j in range(16): z=(j/16+t*.21)%1 sc=.1+z*z*2.7 ww,hh=480*sc,255*sc c=mix(INK,(74,100,91),z*.8) d.rectangle((cx-ww,cy-hh,cx+ww,cy+hh),outline=c,width=2) for x in [-450,-100,240,840,1180,1530]: d.line((cx,cy,x,H),fill=(39,64,56),width=2) terminal(im,97,1020,886,481,[ ("$ rg 'what matters'",LIME), (" context.ts 12 matches",GRAY), (" ideas.md 04 threads",GRAY), (" the-next-step found",PAPER), ("$ follow the signal_",LIME), ],age,active=3) headline(im,["FOLLOW","THE","SIGNAL."],333,186,age=age,accent=LIME) label(im,"READ DEEP. FIND A WAY THROUGH.",78,1595,26,PAPER)
elif idx == 4: grid(im,(229,89,56),108,t*38) headline(im,["HUNT","THE","BUG."],330,229,INK,INK,age) fixed=age>1.68 rows=[ ("- return items[index + 1]",ORANGE), ("+ return items[index]",LIME), ("",GRAY), (("PASS boundary case" if fixed else "FAIL boundary case"),LIME if fixed else ORANGE), ] terminal(im,97,1110,886,372,rows,age,active=3) cx,cy=875,996 d=ImageDraw.Draw(im) d.ellipse((cx-88,cy-88,cx+88,cy+88),outline=INK,width=9) cross(d,cx,cy,INK,119,4) if fixed: d.line((cx-37,cy,cx-6,cy+27,cx+43,cy-32),fill=INK,width=11) else: d.line((cx-28,cy-28,cx+28,cy+28),fill=INK,width=9) d.line((cx-28,cy+28,cx+28,cy-28),fill=INK,width=9) label(im,"TRACE IT. PATCH IT. PROVE IT.",78,1588,26,INK)
elif idx == 5: grid(im,(83,89,249),108,t*16) # An interface assembles in perspective from wireframe layers. d=ImageDraw.Draw(im) for j in range(4): p=ease((age-j*.2)/1.0) x=144+j*27 y=1170-j*38+(1-p)*280 quad=[(x,y),(x+630,y-116),(x+760,y+224),(x+130,y+341)] d.polygon(quad,fill=mix(BLUE,INK,.22+j*.1),outline=mix(BLUE,PAPER,.35+j*.15),width=3) if j==3: for line in range(5): yy=y+18+line*40 d.line((x+58,yy,x+390+(line%2)*120,yy-64),fill=PAPER if line<2 else (146,149,254),width=10 if line==0 else 5) d.polygon([(x+400,y+125),(x+570,y+94),(x+598,y+166),(x+428,y+197)],fill=LIME) headline(im,["MAKE","IT","REAL."],322,225,PAPER,LIME,age) label(im,"FROM A BLANK FILE TO A FIRST VERSION.",78,1585,24,PAPER)
elif idx == 6: headline(im,["RUN IT.","BREAK IT.","FIX IT."],344,166,INK,BLUE,age,gap=40) d=ImageDraw.Draw(im) for j,title in enumerate(["EDGE CASES","THE HAPPY PATH","ONE MORE CHECK"]): yy=1100+j*139 p=ease((age-j*.53-.3)/.35) d.line((78,yy+100,1002,yy+100),fill=(193,198,185),width=2) label(im,title,78,yy+22,29,INK) xx=932 d.ellipse((xx-31,yy+10,xx+31,yy+72),outline=INK,width=2) if p>.9: d.ellipse((xx-31,yy+10,xx+31,yy+72),fill=BLUE) d.line((xx-14,yy+40,xx-2,yy+52,xx+17,yy+29),fill=PAPER,width=5) else: d.arc((xx-20,yy+21,xx+20,yy+61),int(t*280),int(t*280)+220,fill=BLUE,width=4) label(im,"CONFIDENCE COMES FROM CHECKING.",78,1593,25,INK)
elif idx == 7: grid(im,(20,31,32),108,-t*24) im=torus(im,t*1.4,center=(540,1230),scale=230,color=(150,173,255)) headline(im,["AGAIN.","SHARPER.","AGAIN."],350,165,age=age,accent=LIME,gap=35) d=ImageDraw.Draw(im) lap=int(age/BEAT)+1 label(im,f"ITERATION {lap:02d}",540,1203,31,LIME,align="center") label(im,"SMALL CHANGES. BIG DIFFERENCE.",78,1640,25,PAPER) for k in range(8): x=113+k*112 d.rectangle((x,1570,x+70,1578),fill=LIME if k<lap else (47,63,56))
elif idx == 8: grid(im,(200,237,85),108,t*15) d=ImageDraw.Draw(im) # Two paths become one arrow: the user + the assistant. yy=1370 phase=ease(age/1.2) d.line((105,yy-155,440,yy-155,610,yy,922,yy),fill=INK,width=16) d.line((105,yy+155,440,yy+155,610,yy),fill=INK,width=16) d.polygon([(922,yy),(854,yy-49),(854,yy+49)],fill=INK) xx=105+phase*580 d.ellipse((xx-14,yy-169,xx+14,yy-141),fill=BLUE) if xx<440 else None label(im,"YOU",105,yy-221,27,INK) label(im,"ME",105,yy+198,27,INK) headline(im,["YOUR IDEAS.","OUR NEXT","MOVE."],359,165,INK,BLUE,age,gap=37) label(im,"LET'S SEE HOW FAR WE CAN TAKE IT.",78,1010,25,INK)
else: grid(im,(20,30,35),108) im=torus(im,t,center=(540,705),scale=255) headline(im,["LET'S","BUILD."],1035,204,age=age,accent=LIME,gap=18) label(im,"CODEX",540,295,61,PAPER,"bold",align="center") label(im,"YOU BRING THE NEXT IDEA.",540,1492,27,PAPER,align="center") label(im,"MADE WITH CODE + FFMPEG",540,1590,22,GRAY,align="center")
chrome(im,idx,t,dark) return im
def frame(t, transitions=True): beat=t/BEAT idx=min(9,max(0,int(np.searchsorted(SCENES,beat,side="right")-1))) age=t-SCENES[idx]*BEAT im=scene(idx,t,age) if transitions and idx and age<.2: p=smooth(age/.2) previous=scene(idx-1,SCENES[idx]*BEAT-.001,(SCENES[idx]-SCENES[idx-1])*BEAT) cut=round(W*p) previous.paste(im.crop((0,0,cut,H)),(W-cut,0)) # Horizontal shutter wipe with a narrow, bright leading edge. ImageDraw.Draw(previous).rectangle((max(0,W-cut-7),0,W-cut,H),fill=LIME) im=previous return im
def soundtrack(): """Compose a D-minor electro track from oscillators and shaped noise.""" sr=48000 n=round(DURATION*sr) buses={name:np.zeros((n,2),dtype=np.float64) for name in ["drums","bass","music","fx"]} rng=np.random.default_rng(144)
def add(bus, sig, start, level=1., pan=0., echoes=False): start=round(start*sr) if start<0: sig=sig[-start:] start=0 count=min(len(sig),n-start) if count<=0: return gains=np.array([math.sqrt((1-pan)/2),math.sqrt((1+pan)/2)]) buses[bus][start:start+count]+=sig[:count,None]*gains*level if echoes: for k,g in [(1,.23),(2,.11),(3,.055)]: add(bus,sig,start/sr+k*BEAT*.75,level*g,(-pan if k%2 else pan))
def ts(dur): return np.arange(round(dur*sr))/sr
def freq(midi): return 440*2**((midi-69)/12)
def kick(): t=ts(.43) phase=math.tau*(45*t+105*.032*(1-np.exp(-t/.032))) body=np.sin(phase)*np.exp(-t*10) click=rng.normal(size=len(t))*np.exp(-t*480)*.09 return np.tanh((body+click)*1.6)*.85
def snare(): t=ts(.23) noise=rng.normal(size=len(t)) bright=noise-np.roll(noise,1)*.73 env=np.exp(-t*23)*(1-np.exp(-t*1900)) clap=np.zeros_like(t) for delay in [0,.012,.026]: clap+=np.where(t>=delay,np.exp(-np.maximum(t-delay,0)*110),0) return .24*bright*env+.08*noise*clap+.18*np.sin(math.tau*185*t)*np.exp(-t*32)
def hat(opened=False): t=ts(.17 if opened else .058) noise=rng.normal(size=len(t)) high=np.diff(noise,prepend=0) return high*np.exp(-t*(24 if opened else 90))*.12
def bass(midi,dur): t=ts(dur) f=freq(midi) wave=np.sin(math.tau*f*t) for harmonic in [2,3,4,5,7]: wave+=.35/harmonic*np.sin(math.tau*f*harmonic*t)*np.exp(-t*harmonic*4) env=(1-np.exp(-t*350))*np.minimum(1,(dur-t)/.035)*np.exp(-t*2) return np.tanh(wave*2)*env*.48
def pluck(midi,dur=.36): t=ts(dur) f=freq(midi) env=(1-np.exp(-t*700))*np.exp(-t*11)*np.minimum(1,(dur-t)/.025) sig=np.sin(math.tau*f*t)+.3*np.sin(math.tau*f*2.002*t)+.17*np.sin(math.tau*f*3*t) return sig*env*.21
# Sparse, anticipatory opening. for b in [0,1,2,3,3.5]: add("fx",pluck(74+int(b)%3*3,.12),b*BEAT,.5,pan=(-.6 if b%2 else .6),echoes=True)
roots=[38,34,41,36] # D, Bb, F, C. for b in range(4,68): breakdown=44<=b<52 if not breakdown or b%4==0: add("drums",kick(),b*BEAT,.88 if not breakdown else .62) if b%2==1 and not breakdown: add("drums",snare(),b*BEAT,.76) if not breakdown: add("drums",hat(),b*BEAT,.48,-.22) add("drums",hat(True), (b+.5)*BEAT,.57,.28) if b>=52 or b%4==3: add("drums",hat(),(b+.75)*BEAT,.33,-.4) root=roots[((b-4)//8)%4] if not breakdown: for pos, note, gain in [(.03,root,.9),(.5,root,.8),(.75,root+12,.38)]: add("bass",bass(note,BEAT*.22 if pos==.75 else BEAT*.43),(b+pos)*BEAT,gain)
# Syncopated arpeggio with dotted-eighth ping-pong delay. arps=[ [0,7,12,15,12,7,10,7], [0,7,12,16,12,7,11,7], [0,7,12,16,12,7,9,7], [0,7,12,16,12,7,10,7], ] for k in range(8,136): b=k/2 chord=max(0,int((b-4)//8))%4 root=roots[chord]+24 level=.85 if 20<=b<44 or b>=52 else .5 if 44<=b<48 and k%4: continue add("music",pluck(root+arps[chord][k%8]),b*BEAT,level,pan=.5*math.sin(k*1.7),echoes=True)
# Soft, broad synth chords behind the percussive lead. for b in range(4,68,8): root=roots[((b-4)//8)%4]+24 third=3 if root%12==2 else 4 dur=BEAT*7.8 t=ts(dur) env=np.minimum(t/.2,1)*np.minimum((dur-t)/.45,1) for note in [root,root+third,root+7]: f=freq(note) sig=(np.sin(math.tau*f*.998*t)+np.sin(math.tau*f*1.002*t))*.052*env add("music",sig,b*BEAT,.7,pan=(note-root-3)/12)
# Risers, impacts, and a short snare build lock to the editorial cuts. for end in [4,12,20,28,36,52,60,68]: dur=BEAT*(2 if end in [4,20,52,68] else 1) t=ts(dur) p=t/dur noise=rng.normal(size=len(t)) noise=(noise+np.roll(noise,1)+np.roll(noise,2))/3 sig=(noise*.16+np.sin(math.tau*(220*t+600*t*t/dur))*.026)*p**2*np.minimum((dur-t)/.03,1) add("fx",sig,end*BEAT-dur,1,pan=-.2) t=ts(.62) impact=(rng.normal(size=len(t))*.14+np.sin(math.tau*(44*t+24*.04*(1-np.exp(-t/.04))))*.25)*np.exp(-t*7) add("fx",impact,end*BEAT,.7) for k in range(12): add("drums",snare(),(49+k*.25)*BEAT,.18+k*.025,pan=(-.2 if k%2 else .2))
# Final chord and sub impact ring out under the closing card. add("drums",kick(),68*BEAT,1.) for note in [50,62,65,69,74]: t=ts(1.6667) sig=np.sin(math.tau*freq(note)*t)*np.exp(-t*2.3)*(1-np.exp(-t*200))*.14 add("music",sig,68*BEAT,1,pan=(note-62)/24)
timeline=np.arange(n)/sr duck=1-.55*np.exp(-((timeline/BEAT)%1)*8) buses["music"]*=duck[:,None] buses["bass"]*=duck[:,None] master=buses["drums"]+buses["bass"]+buses["music"]+buses["fx"] master=np.tanh(master*.92) fadein=np.minimum(timeline/.016,1) fadeout=np.minimum((DURATION-timeline)/.48,1) master*=fadein[:,None]*fadeout[:,None] master*=.91/np.max(np.abs(master)) pcm=(master*32767).astype("<i2") with wave.open(str(OUT/"soundtrack.wav"),"wb") as f: f.setnchannels(2) f.setsampwidth(2) f.setframerate(sr) f.writeframes(pcm.tobytes()) print("Original 144 BPM stereo score synthesized.",flush=True)
def contact_sheet(): times=[.95,3.2,6.65,9.95,13.4,16.65,20.2,23.3,26.65,29.3] sheet=Image.new("RGB",(5*270,2*520),(23,28,33)) for k,t in enumerate(times): tile=frame(t,False).resize((270,480),Image.Resampling.LANCZOS) x,y=(k%5)*270,(k//5)*520 sheet.paste(tile,(x,y)) ImageDraw.Draw(sheet).text((x+10,y+490),f"{t:05.2f}s / {NAMES[k]}",font=font(13,"mono"),fill=PAPER) sheet.save(OUT/"storyboard.jpg",quality=94) frame(3.2,False).save(OUT/"poster.jpg",quality=95) print("Saved storyboard and poster.",flush=True)
def render(preview=False): output=OUT/("preview.mp4" if preview else "inside-the-loop.mp4") fps=15 if preview else FPS dims=(540,960) if preview else (W,H) video_filter="scale=out_color_matrix=bt709:out_range=tv,setsar=1,format=yuv420p" audio_filter="highpass=f=30,lowpass=f=17000,loudnorm=I=-14:TP=-1.5:LRA=8,alimiter=limit=0.89:level=false" cmd=[FFMPEG,"-hide_banner","-y","-loglevel","warning", "-f","rawvideo","-pix_fmt","rgb24","-s",f"{dims[0]}x{dims[1]}","-r",str(fps),"-i","pipe:0", "-i",str(OUT/"soundtrack.wav"),"-map","0:v:0","-map","1:a:0", "-vf",video_filter,"-af",audio_filter, "-c:v","libx264","-preset","fast" if preview else "medium","-crf","21" if preview else "18", "-threads","8","-profile:v","high","-level:v","4.1", "-color_primaries","bt709","-color_trc","bt709","-colorspace","bt709", "-c:a","aac","-b:a","256k","-ar","48000","-ac","2", "-t",str(DURATION),"-movflags","+faststart", "-metadata","title=Inside the Loop — A Codex POV", "-metadata","comment=Original procedural animation and 144 BPM score. Created with Python and FFmpeg.",str(output)] (OUT/("ffmpeg-preview-command.json" if preview else "ffmpeg-command.json")).write_text(json.dumps(cmd,indent=2)+"\n") start=time.time() with open(OUT/("preview-render.log" if preview else "render.log"),"w") as log: process=subprocess.Popen(cmd,stdin=subprocess.PIPE,stderr=log) try: for i in range(round(DURATION*fps)): im=frame(i/fps) if preview: im=im.resize(dims,Image.Resampling.LANCZOS) process.stdin.write(im.tobytes()) if i%(fps*2)==0: print(f"Rendered {i/fps:04.1f}s / {DURATION}s (elapsed {time.time()-start:.1f}s)",flush=True) process.stdin.close() code=process.wait() except BaseException: process.kill() process.wait() raise if code: raise RuntimeError(f"FFmpeg exited {code}; see output/render.log") print(f"Saved {output} ({output.stat().st_size/1024**2:.1f} MiB)",flush=True)
def main(): parser=argparse.ArgumentParser() parser.add_argument("--stills",action="store_true") parser.add_argument("--audio",action="store_true") parser.add_argument("--preview",action="store_true") args=parser.parse_args() OUT.mkdir(exist_ok=True) contact_sheet() if args.stills: return soundtrack() if not args.audio: render(args.preview)
if __name__=="__main__": main()