[m6] Flow video plates spec + flow_intake.py; M6 queued behind M5

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-19 01:25:14 +10:00
parent a8273127ba
commit 86b76f6bdc
5 changed files with 75 additions and 0 deletions

18
PLAN.md
View File

@ -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/<category>/`; `scripts/flow_intake.py` copies them to
`assets/backdrops/video/<category>/` 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

View File

@ -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.

View File

@ -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.

View File

@ -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.

45
scripts/flow_intake.py Normal file
View File

@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Sweep Flow Auto-Pilot downloads into SCENEGOD assets.
~/Downloads/flowrinse/<category>/*.mp4 -> assets/backdrops/video/<category>/
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()