#!/usr/bin/env python3 """Build motion previews for bursts: one small looping animated WebP per burst, from the existing 1400px Previews (deterministic re-encode, no photo changes). Usage: python3 build_burst_media.py burst_plan.csv Writes: ClientPicker/Bursts/.webp + .scratch/bursts.json (for build_picker_v3.py) """ import csv, json, os, subprocess, sys from pathlib import Path SHOOT = Path("/Volumes/Sam Storage/YellowMellow/2026/Monster High Theme (10:01:2026)") PRE = SHOOT / "ClientPicker" / "Previews" OUTDIR = SHOOT / "ClientPicker" / "Bursts" SCRATCH = Path("/Volumes/Sam Storage/YellowMellow/.scratch") MAX_FRAMES = 20 # cap preview length (~3.3s at 6fps); full sequence browsable in lightbox plan_file = Path(sys.argv[1]) rows = [r for r in csv.DictReader(open(plan_file)) if int(r["unique_frames"]) >= 5] print("%d qualifying bursts (>=5 frames)" % len(rows)) OUTDIR.mkdir(exist_ok=True) def split_parts(first, last): """Split card-counter wrap (e.g. 7O9A9971 -> 7O9A0017) into two real bursts.""" a, b = int(first[4:]), int(last[4:]) if a <= b: return [(first, last)] return [(first, "7O9A%04d" % 9999), ("7O9A%04d" % 1, last)] bursts = {} for r in rows: f0, l0 = os.path.splitext(r["first_file"])[0], os.path.splitext(r["last_file"])[0] fps = max(3, min(12, int(float(r.get("playback_fps") or 6)))) for first, last in split_parts(f0, l0): a, b = int(first[4:]), int(last[4:]) frames = [PRE / ("7O9A%04d.jpg" % n) for n in range(a, b + 1)] missing = [f.name for f in frames if not f.exists()] if missing: print("SKIP %s-%s: missing previews %d" % (first, last, len(missing))) continue frames = frames[:MAX_FRAMES] out = OUTDIR / (first + ".webp") cmd = ["magick", "-loop", "0", "-delay", str(round(100 / fps))] cmd += [f.as_posix() for f in frames] cmd += ["-resize", "384x", "-quality", "75", out.as_posix()] p = subprocess.run(cmd, capture_output=True, text=True) if p.returncode != 0 or not out.exists(): print("FAIL %s: %s" % (first, p.stderr[:300])) continue bursts[first] = {"first": first, "last": last, "fps": fps, "frames": len(frames), "kb": out.stat().st_size // 1024} print("OK %s-%s (%d frames @%dfps) -> %.0f KB" % (first, last, len(frames), fps, out.stat().st_size / 1024)) json.dump(bursts, open(SCRATCH / "bursts.json", "w")) total = sum(v["frames"] for v in bursts.values()) print("DONE: %d bursts, %d burst frames" % (len(bursts), total))