diff --git a/PLAN.md b/PLAN.md index 753677e..49fa325 100644 --- a/PLAN.md +++ b/PLAN.md @@ -212,6 +212,24 @@ presets, LLM strictly optional on top. ops validated server-side → client applies via the SAME preset functions. Buttons must never depend on the LLM lane. +**M6 — FLOW VIDEO PLATES (rear-projection era)**: Google-Flow-generated mp4s +as first-class stage media. Feed: John's flowext rinse drops mp4s in +`~/Downloads/flowrinse//`; `scripts/flow_intake.py` copies them to +`assets/backdrops/video//` with poster jpgs. +- Lane A: backdrop `params.mode` gains `"video"` (and existing modes accept + .mp4 sources): `THREE.VideoTexture` on plane/corner/dome, muted, `loop`, + `params.videoStart` offset. NEW entity kind `screen`: a video-textured + plane with a slight emissive boost + optional CRT curve, for TVs/club + screens/jumbotrons INSIDE the set (djsim_screens bank is made for these). + Poster jpg shown until play. +- Lane B: video backdrops/screens follow the master clock — on evaluate, + `video.currentTime = (t - videoStart) % duration` only when drift >50ms + (cheap scrub-follow); play/pause with the clock. +- Lane C: render path must be deterministic — finalRender already steps + frame-by-frame; stage exposes `await stage.syncVideos(t)` (seek + wait + for 'seeked') that render.js awaits per frame before capture. Video audio + is NEVER used (scene audio[] is the only audio source). + ## 6. How John launches the lanes Three Claude Code sessions (Opus 4.8), cwd `~/Documents/SCENEGOD`, one prompt diff --git a/lanes/A-stage.md b/lanes/A-stage.md index 52f6d41..bab4a9b 100644 --- a/lanes/A-stage.md +++ b/lanes/A-stage.md @@ -202,3 +202,7 @@ badge check. Standing by. Acceptance: select man → CU low 35mm → camera frames his face from below in one click; golden_hour relights the scene in one click; every action lands as keys via B (scrub proves it) and is undo-able. + +### 2026-07-19 — M6 FLOW VIDEO PLATES queued behind M5 (full spec PLAN §5 M6). +Do M5 first; M6 is unlocked the moment your M5 acceptance is logged. Test +media will be arriving in assets/backdrops/video/ via scripts/flow_intake.py. diff --git a/lanes/B-timeline.md b/lanes/B-timeline.md index 5c7be6f..0c22c39 100644 --- a/lanes/B-timeline.md +++ b/lanes/B-timeline.md @@ -162,3 +162,7 @@ discover the panel scrolls — consider wheel = scroll, Cmd/Ctrl+wheel = zoom. `importRhubarb(characterId, json, audioStart)`; character picker if >1 viseme-bearing char; toast the 503 install hint if absent. 3. Session-5 backlog if time remains: audio trim, wheel-scroll fix (above). + +### 2026-07-19 — M6 FLOW VIDEO PLATES queued behind M5 (full spec PLAN §5 M6). +Do M5 first; M6 is unlocked the moment your M5 acceptance is logged. Test +media will be arriving in assets/backdrops/video/ via scripts/flow_intake.py. diff --git a/lanes/C-server.md b/lanes/C-server.md index 770fb4b..32a90e8 100644 --- a/lanes/C-server.md +++ b/lanes/C-server.md @@ -157,3 +157,7 @@ not on PATH — my error). Standing by is fine; remaining work is A/B UI `/rhubarb?path=` endpoint (subprocess → JSON) IF rhubarb is installed — 503-with-message otherwise, same pattern as ffmpeg. Log a REQUEST first if it needs more than that. + +### 2026-07-19 — M6 FLOW VIDEO PLATES queued behind M5 (full spec PLAN §5 M6). +Do M5 first; M6 is unlocked the moment your M5 acceptance is logged. Test +media will be arriving in assets/backdrops/video/ via scripts/flow_intake.py. diff --git a/scripts/flow_intake.py b/scripts/flow_intake.py new file mode 100644 index 0000000..a92bc63 --- /dev/null +++ b/scripts/flow_intake.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Sweep Flow Auto-Pilot downloads into SCENEGOD assets. + +~/Downloads/flowrinse//*.mp4 -> assets/backdrops/video// +Each mp4 gets a same-stem poster .jpg (frame at 1s) so the dock shows a thumb. +Idempotent: skips files already present with the same size. Run any time: + + python3 scripts/flow_intake.py [--src ~/Downloads/flowrinse] [--dry] +""" +import argparse, shutil, subprocess, sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +DEST = ROOT / "assets" / "backdrops" / "video" + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--src", default=str(Path.home() / "Downloads" / "flowrinse")) + ap.add_argument("--dry", action="store_true") + a = ap.parse_args() + src = Path(a.src).expanduser() + if not src.is_dir(): + sys.exit(f"no such dir: {src}") + if not shutil.which("ffmpeg"): + sys.exit("ffmpeg required for poster frames") + copied = skipped = 0 + for mp4 in sorted(src.rglob("*.mp4")): + cat = mp4.parent.name + out = DEST / cat / mp4.name + if out.exists() and out.stat().st_size == mp4.stat().st_size: + skipped += 1 + continue + print(("DRY " if a.dry else "") + f"{mp4} -> {out}") + if a.dry: + continue + out.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(mp4, out) + jpg = out.with_suffix(".jpg") + subprocess.run(["ffmpeg", "-v", "error", "-ss", "1", "-i", str(out), + "-frames:v", "1", "-vf", "scale=320:-1", str(jpg), "-y"], check=False) + copied += 1 + print(f"done: {copied} copied, {skipped} already present -> {DEST}") + +if __name__ == "__main__": + main()