recordstoreguy/tools/make_placeholders.py
m3ultra 17a0bb07ed Phase 1: playable engine shell — QTE player, debug mode, placeholder clip generator
Verified end-to-end in browser: attract, QTE pass/fail, death/retry, lives, win screen.
QTE clock driven by rAF + timeupdate + interval so background-tab throttling can't
starve the too-late check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:59:59 +10:00

97 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""Generate placeholder clips for every scene in engine/scenes.json (plus
attract/win/gameover) so the whole game is playable before any Flow credits are
spent. 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. Deaths: red 'DEATH' cards.
If Pillow is missing from the running interpreter, re-execs into a MODELBEAST
venv python that has it."""
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"
DUR = 5.0
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):
img = Image.new("RGBA", (W, H), (0, 0, 0, 0))
d = ImageDraw.Draw(img)
f = ImageFont.truetype(FONT, 200) if FONT else ImageFont.load_default()
d.text((W / 2, H - 220), text, font=f, fill="#ffee00", anchor="mm")
img.save(path)
def clip(out, base_png, dur, windows=(), tmp=None):
"""base card + per-window white flash, glyph overlay, and beep."""
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 = [], "0:v", "0"
for i, w in enumerate(windows):
t0, t1 = w["t"]
g = Path(tmp) / f"g{i}.png"
png_glyph(g, GLYPH[w["input"]])
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}][{i + 2}:v]overlay=enable='between(t,{t0},{t1})'[{nxt}]")
last = nxt
vol = f"if(between(t,{t0},{t1}),1,{vol})"
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())
with tempfile.TemporaryDirectory() as tmp:
def make(name, color, text, dur, windows=()):
base = Path(tmp) / (Path(name).stem + ".png")
png_card(base, color, text)
clip(CLIPS / name, base, dur, windows, tmp)
for i, (sid, s) in enumerate(data["scenes"].items()):
make(Path(s["clip"]).name, COLORS[i % len(COLORS)], sid.upper(), DUR, s["windows"])
make(Path(s["death"]).name, "#550000", f"DEATH {sid.upper()}", 2.5)
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)
if __name__ == "__main__":
main()