#!/usr/bin/env python3 """Generate placeholder clips for every clip referenced by engine/scenes.json (actions, deaths, choice deaths, needle-drop alts, attract/win/gameover/ needle_skip/stinger) so the whole game is playable before any Flow credits are spent. Existing files are SKIPPED — real Flow footage is never overwritten. Text is rendered to PNG cards with Pillow (this machine's ffmpeg lacks drawtext); ffmpeg composites flash + glyph overlays + a 1kHz beep at exactly each QTE window so timing is verifiable by eye/ear. Choice windows show all option glyphs; hold windows show HOLD; needle-drop flashes magenta top-right; white-label collects flash white top-left. Deaths: red 'DEATH' cards.""" import glob import json import os import subprocess import sys import tempfile from pathlib import Path try: from PIL import Image, ImageDraw, ImageFont except ModuleNotFoundError: for cand in glob.glob(str(Path.home() / "Documents/MODELBEAST/venvs/*/bin/python3")): if subprocess.run([cand, "-c", "import PIL"], capture_output=True).returncode == 0: os.execv(cand, [cand] + sys.argv) sys.exit("Pillow not found in any interpreter; pip install pillow") ROOT = Path(__file__).resolve().parent.parent / "engine" CLIPS = ROOT / "clips" COLORS = ["#1a2a55", "#1f4d2a", "#4d3a1f", "#3a1f4d", "#1f4d4d"] GLYPH = {"left": "<<<", "right": ">>>", "up": "^^^", "down": "vvv", "action": "(GO)"} FONTS = ["/System/Library/Fonts/Supplemental/Arial Bold.ttf", "/System/Library/Fonts/Supplemental/Arial.ttf"] FONT = next((f for f in FONTS if Path(f).exists()), None) W, H = 1280, 720 def png_card(path, color, text, size=110, fg="white"): img = Image.new("RGB", (W, H), color) d = ImageDraw.Draw(img) f = ImageFont.truetype(FONT, size) if FONT else ImageFont.load_default() d.text((W / 2, H / 2), text, font=f, fill=fg, anchor="mm") img.save(path) def png_glyph(path, text, size=200, fill="#ffee00"): img = Image.new("RGBA", (W, H), (0, 0, 0, 0)) d = ImageDraw.Draw(img) f = ImageFont.truetype(FONT, size) if FONT else ImageFont.load_default() d.text((W / 2, H - 220), text, font=f, fill=fill, anchor="mm") img.save(path) def win_glyph(w): if "choices" in w: return " ".join(GLYPH[d] for d in w["choices"]) g = GLYPH[w["input"]] return g + " HOLD" if w.get("hold") else g def clip(out, base_png, dur, windows=(), flashes=(), tmp=None): """base card + per-window white flash, glyph overlay, beep; extra flashes are (t0,t1,color) boxes (needle-drop magenta / white-label white).""" args = ["ffmpeg", "-y", "-loglevel", "error", "-loop", "1", "-t", str(dur), "-framerate", "24", "-i", str(base_png), "-f", "lavfi", "-i", f"sine=frequency=1000:sample_rate=44100:duration={dur}"] fc, last, vol, vi = [], "0:v", "0", 2 for i, w in enumerate(windows): t0, t1 = w["t"] g = Path(tmp) / f"{out.stem}_g{i}.png" png_glyph(g, win_glyph(w), size=140 if "choices" in w or w.get("hold") else 200) args += ["-loop", "1", "-t", str(dur), "-framerate", "24", "-i", str(g)] nxt = f"v{i}" fc.append(f"[{last}]drawbox=t=fill:color=white@0.35:enable='between(t,{t0},{t0 + 0.12})'" f"[f{i}];[f{i}][{vi}:v]overlay=enable='between(t,{t0},{t1})'[{nxt}]") last, vi = nxt, vi + 1 vol = f"if(between(t,{t0},{t1}),1,{vol})" for j, (t0, t1, color, y) in enumerate(flashes): nxt = f"x{j}" fc.append(f"[{last}]drawbox=x={W - 260 if y == 'tr' else 40}:y=40:w=220:h=120:" f"t=fill:color={color}@0.8:enable='between(t,{t0},{t1})'[{nxt}]") last = nxt fc.append(f"[{last}]format=yuv420p[vout]") args += ["-filter_complex", ";".join(fc), "-map", "[vout]", "-map", "1:a", "-af", f"volume='{vol}':eval=frame", "-c:v", "libx264", "-crf", "23", "-c:a", "aac", "-shortest", "-movflags", "+faststart", str(out)] subprocess.run(args, check=True) print("wrote", out.name) def main(): CLIPS.mkdir(exist_ok=True) data = json.loads((ROOT / "scenes.json").read_text()) deaths, alts = set(), set() with tempfile.TemporaryDirectory() as tmp: def make(name, color, text, dur, windows=(), flashes=()): out = CLIPS / name if out.exists(): return print("skip (exists)", name) base = Path(tmp) / (Path(name).stem + ".png") png_card(base, color, text) clip(out, base, dur, windows, flashes, tmp) for i, (sid, s) in enumerate(data["scenes"].items()): if "random" in s: continue # variants carry their own clips windows = s.get("windows", []) dur = max([5.0] + [w["t"][1] + 0.8 for w in windows]) flashes = [] if "needledrop" in s: nd = s["needledrop"] flashes.append((nd["t"][0], nd["t"][1], "magenta", "tr")) alts.add(nd["clip"]) if "collect" in s: c = s["collect"] flashes.append((c["t"][0], c["t"][1], "white", "tl")) make(Path(s["clip"]).name, COLORS[i % len(COLORS)], sid.upper(), dur, windows, flashes) if s.get("death"): deaths.add(s["death"]) for w in windows: for c in w.get("choices", {}).values(): if "death" in c: deaths.add(c["death"]) for d in sorted(deaths): make(Path(d).name, "#550000", f"DEATH {Path(d).stem.upper()}", 2.5) for a in sorted(alts): make(Path(a).name, "#0a3a5a", Path(a).stem.upper() + " !", 5.0) make("attract.mp4", "#111111", "RECORD STORE GUY", 4.0) make("win.mp4", "#004400", "YOU SURVIVED", 4.0) make("gameover.mp4", "#220022", "GAME OVER", 3.0) make("needle_skip.mp4", "#000000", "~ skip ~", 1.5) make("stinger.mp4", "#2a1a00", "STINGER: MILDRED", 4.0) if __name__ == "__main__": main()