diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index f4d234d..2e51d15 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -175,7 +175,16 @@ every transition, so fast navigation stays responsive on placeholder media. scales-library/right-axis design §1/§4); a real multi-strength flow-stabilized re-bake per base clip + the multilingual label/string tables + TTS. The **scale-ring transitions** (forward/reverse per-edge morphs) are part of this - offline pipeline too — the simulator currently uses placeholders. + offline pipeline too. + - **Vertical slice done (session 0012):** the Right-variant half is + productionized and run for the **forest** scale — `simulator/bake_right_variants.py` + (the POC flow-restyle algorithm, repo-resident) bakes real strengths 1–4 on a + by-eye keyframe-strength ramp; forest now carries real Right variants in the + sim, so the dreamlike-axis look is judgeable on real footage. **Still + deferred:** real Right variants for cosmos/abyss (need their real strict-PD + bases sourced first) and the **real i2v ring transitions** (need a 2nd real + base + a generative-video model not in the POC). Plan: + [`2026-06-08-v2v-vertical-slice-forest-right-variants.md`](./superpowers/plans/2026-06-08-v2v-vertical-slice-forest-right-variants.md). - **Catalog model changes** — audio *source* + "neutral base" vs "altered variant" flag (sub-project 2 territory, design §13). diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index be2412b..59f737d 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -298,16 +298,22 @@ piece moved from *selecting* clips to *altering* them.) **One-time setup — populate the sample footage** (look-tuning only; not shipped content): - python simulator/setup_sample_media.py # the forest scale (real POC base + Right variants) - python simulator/setup_scales_media.py # the cosmos + abyss scales + ring transitions + python simulator/setup_sample_media.py # stage the forest neutral base + python -m simulator.bake_right_variants --scale forest # real Right variants (SD on MPS, ~11 min) + python simulator/setup_scales_media.py # the cosmos + abyss scales + ring transitions -`setup_sample_media.py` copies the session-0008 POC artifacts (`~/hef-poc/out/`) -into `simulator/sample_media/forest/` — the neutral base clip and the real -flow-stabilized Right restyle — and generates placeholder intermediate Right -strengths. `setup_scales_media.py` makes the scale **ring** demonstrable: cheap -synthetic placeholder bases for the two true-PD scales (`cosmos` = NASA/Hubble, -`abyss` = NOAA Ocean Exploration) plus the per-edge zoom/warp transition clips. -The `.mp4` binaries are gitignored. +`setup_sample_media.py` stages the forest neutral base from the session-0008 POC +artifacts (`~/hef-poc/out/neutral.mp4`). `bake_right_variants.py` then produces +the **real** flow-stabilized painterly Right variants (strengths 1–4) for the +forest scale — SD img2img keyframes + Farneback optical-flow tweens on Apple MPS, +the temporally-calm POC algorithm, productionized (scales design §1/§4). The +keyframe-strength ramp per Right level is by-eye tunable in +`simulator/bake_right_variants.py`. `setup_scales_media.py` makes the scale +**ring** demonstrable: cheap synthetic placeholder bases for the two true-PD +scales (`cosmos` = NASA/Hubble, `abyss` = NOAA Ocean Exploration) plus the +per-edge zoom/warp transition clips — these scales carry a raw base only (no real +Right variants yet; the baker can extend to them once their real footage is +sourced). The `.mp4` binaries are gitignored. **Run it (Docker):** diff --git a/docs/superpowers/plans/2026-06-08-v2v-vertical-slice-forest-right-variants.md b/docs/superpowers/plans/2026-06-08-v2v-vertical-slice-forest-right-variants.md new file mode 100644 index 0000000..c982f70 --- /dev/null +++ b/docs/superpowers/plans/2026-06-08-v2v-vertical-slice-forest-right-variants.md @@ -0,0 +1,239 @@ +# v2v Vertical Slice — Real Forest Right Variants Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Produce the first *real* (not placeholder) multi-strength Right-axis +restyle media for the simulator — flow-stabilized painterly variants of the +forest scale at Right strengths 1–4 — so the dreamlike-axis *look* can finally be +judged by eye on real footage, breaking the "can't judge the ring liked without +real content" chicken-and-egg (operator chose a vertical slice, session 0012). + +**Architecture:** Productionize the proven session-0008 POC restyle algorithm +(`~/hef-poc/flow_restyle.py`) into a repo-resident offline baker, +`simulator/bake_right_variants.py`, a sibling of the existing +`setup_sample_media.py` / `setup_scales_media.py` media-generation scripts. The +algorithm is unchanged (extract frames → SD img2img on keyframes → Farneback +optical-flow warp + light refine on tweens → reassemble), which sessions 0008 +proved is temporally calm (no boiling). The one new idea is a **strength curve**: +Right level 1–4 maps to an increasing keyframe img2img `strength`, so the four +discrete pre-baked variants form a subtle→strong dreamlike ramp. Only the curve +helper is unit-tested; the rendered media is verified by eye (screenshots), as +the other media-setup scripts are. + +**Tech Stack:** Python, diffusers `AutoPipelineForImage2Image` + `stabilityai/sd-turbo` +(cached, 4.8 G) on Apple MPS (torch 2.12, available in the project `.venv`), +OpenCV Farneback optical flow, imageio-ffmpeg. ~2.7 min/clip × 4 ≈ ~11 min local +compute. + +**Scope note — what this slice is NOT:** the *real i2v ring transition* half of +the v2v build is deferred: it needs a second real scale base (cosmos/abyss are +still synthetic placeholders) plus a generative-video model not in the POC, and +real strict-PD footage sourcing (NASA/NOAA, sub-project 2). This slice does the +Right-variant half, which is fully achievable now (forest has a real base). + +--- + +### Task 1: The Right strength curve (pure, testable) + +**Files:** +- Create: `simulator/bake_right_variants.py` +- Test: `tests/test_bake_right_variants.py` + +- [ ] **Step 1: Write the failing test** + +```python +# tests/test_bake_right_variants.py +import pytest + +from simulator.bake_right_variants import right_strength_params, RIGHT_LEVELS + + +def test_levels_are_one_through_four(): + assert RIGHT_LEVELS == (1, 2, 3, 4) + + +def test_strength_increases_with_level(): + keys = [right_strength_params(l)[0] for l in RIGHT_LEVELS] + assert keys == sorted(keys) # monotonic ramp + assert keys[0] < keys[-1] # subtle -> strong + + +def test_tween_strength_is_a_fraction_of_key_strength(): + for l in RIGHT_LEVELS: + key, tween = right_strength_params(l) + assert 0.0 < tween < key # tween is a light refine only + + +def test_rejects_out_of_range_level(): + with pytest.raises(ValueError): + right_strength_params(0) + with pytest.raises(ValueError): + right_strength_params(5) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `.venv/bin/python -m pytest tests/test_bake_right_variants.py -q` +Expected: FAIL (ModuleNotFoundError / ImportError — module not created yet) + +- [ ] **Step 3: Write minimal implementation (curve only, no torch import at module load)** + +```python +# simulator/bake_right_variants.py (top of file) +"""Offline baker: real flow-stabilized painterly Right-axis variants (scales +design §1/§4). Productionizes the session-0008 POC (~/hef-poc/flow_restyle.py): +SD img2img keyframes + Farneback optical-flow tweens = temporally calm restyle. + +Right level -> keyframe img2img strength ramp (subtle -> strong); tween strength +is a light refine fraction of the keyframe strength. Curve is by-eye tunable. + +Usage: .venv/bin/python -m simulator.bake_right_variants [--scale forest] +Requires: torch+MPS, diffusers, sd-turbo (cached), opencv, imageio-ffmpeg. +""" +from __future__ import annotations + +RIGHT_LEVELS = (1, 2, 3, 4) + +# Keyframe img2img strength per Right level (by-eye ramp). Level 4 ~ the POC's +# proven 0.5-0.58 painterly look; level 1 is a barely-there dream haze. +_KEY_STRENGTH = {1: 0.28, 2: 0.38, 3: 0.48, 4: 0.58} +_TWEEN_RATIO = 0.36 # POC ratio (0.18 / 0.5): tweens are a light refine only + + +def right_strength_params(level: int) -> tuple[float, float]: + """(keyframe_strength, tween_strength) for a Right level in RIGHT_LEVELS.""" + if level not in _KEY_STRENGTH: + raise ValueError(f"Right level must be one of {RIGHT_LEVELS}, got {level}") + key = _KEY_STRENGTH[level] + return key, round(key * _TWEEN_RATIO, 4) +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `.venv/bin/python -m pytest tests/test_bake_right_variants.py -q` +Expected: PASS (4 passed) + +- [ ] **Step 5: Commit** + +```bash +git add simulator/bake_right_variants.py tests/test_bake_right_variants.py +git commit -m "feat(pipeline): Right strength curve for the v2v variant baker" +``` + +--- + +### Task 2: The baker (flow restyle, productionized) + +**Files:** +- Modify: `simulator/bake_right_variants.py` + +No unit test for the render itself (non-trivial ML output; verified by eye in +Task 4). The pure curve is already covered by Task 1. + +- [ ] **Step 1: Add the restyle engine + per-level bake, porting the POC algorithm verbatim** + +Port `extract`, `warp`, and the keyframe/tween loop from +`~/hef-poc/flow_restyle.py` unchanged (it is the approved calm algorithm). Wrap +it as `restyle_clip(src, out, key_strength, tween_strength, *, fps=12.0, keyint=24)` +and add a `bake_scale(scale="forest")` that loops `RIGHT_LEVELS`, calling +`restyle_clip` with `right_strength_params(level)` and writing +`simulator/sample_media//right.mp4`. Load the SD pipeline ONCE and +reuse it across levels. `PROMPT`/`NEG`/Farneback params/seed = the POC's. + +Source base = `simulator/sample_media//base.mp4` (forest = the real +Yosemite POC clip). `main()` parses `--scale` (default `forest`), `--fps`, +`--keyint` and calls `bake_scale`. + +- [ ] **Step 2: Smoke-check the module imports and the curve still passes** + +Run: `.venv/bin/python -m pytest tests/test_bake_right_variants.py -q` +Expected: PASS (torch imported lazily inside the bake funcs, not at module load, +so the test stays fast and import-safe) + +- [ ] **Step 3: Commit** + +```bash +git add simulator/bake_right_variants.py +git commit -m "feat(pipeline): forest Right-variant baker (POC flow restyle, productionized)" +``` + +--- + +### Task 3: Run the bake (real media, ~11 min) + +**Files:** +- Writes (gitignored): `simulator/sample_media/forest/right{1,2,3,4}.mp4` + +- [ ] **Step 1: Run the baker** + +Run: `.venv/bin/python -m simulator.bake_right_variants --scale forest` +Expected: 4 clips written; per-level timing printed; ~11 min total. Replaces the +3 placeholder strengths (1–3) and the single real strength (4) with a consistent +real ramp. + +- [ ] **Step 2: Confirm the outputs exist and are real video** + +Run: `ls -la simulator/sample_media/forest/right*.mp4` +Expected: right1–4.mp4 present, each a multi-MB H.264 clip. + +(No commit — media is gitignored; it is reproduced by the baker.) + +--- + +### Task 4: Wire the manifest + verify by eye + +**Files:** +- Modify: `simulator/sample_media/manifest.json` (forest `right_variants` models) +- Modify: `docs/USER_GUIDE.md` (note real forest Right variants + the baker) +- Modify: `docs/ROADMAP.md` (v2v slice progress) + +- [ ] **Step 1: Update the forest variant models in the manifest** + +Set every forest `right_variants` entry (1–4) `model` to +`"sd-turbo+farneback-flow"` (drop the `"placeholder"` labels) — they are all real +bakes now. + +- [ ] **Step 2: Boot the sim and drive the Right axis across 0→4 on forest** + +Run: `.venv/bin/python -m uvicorn simulator.app:app --port 8013` (background), +navigate to the forest scale, and capture headless screenshots at Right 0/1/2/3/4. +Expected: a coherent subtle→strong painterly ramp on the real Yosemite footage, +temporally calm (no boiling), grade/overlay still compose on top. + +- [ ] **Step 3: Update docs** + +USER_GUIDE: note forest now carries real flow-stabilized Right variants generated +by `simulator/bake_right_variants.py`. ROADMAP: mark the v2v Right-variant vertical +slice done; note the i2v ring-transition half still deferred (needs a 2nd real +base + video model). + +- [ ] **Step 4: Run the full suite** + +Run: `.venv/bin/python -m pytest -q` +Expected: PASS (existing + Task-1 curve tests). + +- [ ] **Step 5: Commit** + +```bash +git add simulator/sample_media/manifest.json docs/USER_GUIDE.md docs/ROADMAP.md +git commit -m "feat(sim): forest carries real multi-strength Right variants; docs" +``` + +--- + +## Self-Review + +**Spec coverage:** scales design §1 (local restyle pipeline) + §4 (economics: +local bake) → Tasks 2–3 productionize + run it. The operator's vertical-slice +choice (real Right variants for one scale to judge the look) → Tasks 3–4. The +deferred i2v transition half is explicitly scoped out (noted in header + Task 4). +The strength curve (the one new design element) → Task 1. + +**Placeholder scan:** none — the baker algorithm is a verbatim port of the +existing `~/hef-poc/flow_restyle.py` (cited), the curve is concrete constants, +commands are exact. + +**Type consistency:** `right_strength_params(level) -> (key, tween)` defined in +Task 1, consumed by `bake_scale` in Task 2. `RIGHT_LEVELS` shared. Output paths +`simulator/sample_media/forest/right.mp4` consistent with the manifest's +existing forest `right_variants` map (Task 4). diff --git a/simulator/bake_right_variants.py b/simulator/bake_right_variants.py new file mode 100644 index 0000000..c895ea9 --- /dev/null +++ b/simulator/bake_right_variants.py @@ -0,0 +1,193 @@ +"""Offline baker: real flow-stabilized painterly Right-axis variants (scales +design §1/§4). + +Productionizes the session-0008 POC (``~/hef-poc/flow_restyle.py``): SD img2img +on KEYFRAMES + Farneback optical-flow warp on in-between frames = a temporally +calm painterly restyle (no per-frame "boiling" — the look the operator approved +in 0008). The one new idea on top of the POC is a **strength curve**: each Right +level 1-4 maps to an increasing keyframe img2img ``strength`` so the four +discrete pre-baked variants form a subtle -> strong dreamlike ramp. Level 0 is +always the raw base (handled by the runtime, not baked here). + +The curve (``right_strength_params``) is pure + unit-tested; the rendered media +is verified by eye (like the other ``simulator/setup_*_media.py`` scripts). torch +/ diffusers are imported lazily inside the bake functions so importing this +module (e.g. for the curve test) is fast and dependency-light. + +Usage: + .venv/bin/python -m simulator.bake_right_variants [--scale forest] + [--fps 12] [--keyint 24] + +Requires: torch + Apple MPS, diffusers, ``stabilityai/sd-turbo`` (cached), +opencv-python, imageio-ffmpeg. ~2.7 min/clip x 4 levels ~= ~11 min local compute. +""" +from __future__ import annotations + +import argparse +import glob +import os +import subprocess +import tempfile +import time +from pathlib import Path + +MEDIA = Path(__file__).parent / "sample_media" + +RIGHT_LEVELS = (1, 2, 3, 4) + +# Keyframe img2img strength per Right level (by-eye ramp). sd-turbo's painterly +# transformation is sharply NON-LINEAR in strength — it barely registers below +# ~0.5 and ramps hard through ~0.5-0.65 (verified by eye, session 0012: a linear +# 0.28-0.58 ramp left levels 1-3 ~indistinguishable from raw). So the curve is +# clustered in that active band for real perceptual progression: a gentle dream +# haze (L1) -> clearly painterly (L3, ~the POC's proven look) -> strongest (L4). +# Still by-eye tunable — re-run the baker after editing. +_KEY_STRENGTH = {1: 0.46, 2: 0.52, 3: 0.58, 4: 0.64} +_TWEEN_RATIO = 0.36 # POC ratio (0.18 / 0.5): tweens are a light refine only. + +# The restyle target (matches the POC spike that the operator approved). +PROMPT = ( + "impressionist oil painting, dreamlike, soft painterly brushstrokes, " + "ethereal, luminous, fine art" +) +NEG = "text, watermark, frame, border, ugly, deformed, lowres" +SEED = 42 + + +def right_strength_params(level: int) -> tuple[float, float]: + """(keyframe_strength, tween_strength) for a Right level in RIGHT_LEVELS.""" + if level not in _KEY_STRENGTH: + raise ValueError(f"Right level must be one of {RIGHT_LEVELS}, got {level}") + key = _KEY_STRENGTH[level] + return key, round(key * _TWEEN_RATIO, 4) + + +# --- the restyle engine (verbatim port of ~/hef-poc/flow_restyle.py) --- + + +def _ffmpeg() -> str: + import imageio_ffmpeg + + return imageio_ffmpeg.get_ffmpeg_exe() + + +def _run(cmd: list[str]) -> None: + subprocess.run(cmd, check=True, capture_output=True) + + +def _extract(ff: str, src: str, d: str, fps: float) -> list[str]: + _run([ff, "-hide_banner", "-v", "error", "-i", src, "-vf", f"fps={fps}", + os.path.join(d, "f_%05d.png"), "-y"]) + return sorted(glob.glob(os.path.join(d, "f_*.png"))) + + +def _warp(prev_bgr, src_prev_gray, src_cur_gray): + """Warp the previous stylized frame to align with the current source frame + via Farneback optical flow — this is what kills the per-frame flicker.""" + import cv2 + import numpy as np + + flow = cv2.calcOpticalFlowFarneback( + src_prev_gray, src_cur_gray, None, + pyr_scale=0.5, levels=3, winsize=21, iterations=3, + poly_n=7, poly_sigma=1.5, flags=0) + h, w = src_cur_gray.shape + gx, gy = np.meshgrid(np.arange(w), np.arange(h)) + mx = (gx + flow[..., 0]).astype(np.float32) + my = (gy + flow[..., 1]).astype(np.float32) + return cv2.remap(prev_bgr, mx, my, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT) + + +def _load_pipe(model: str): + """Load the SD img2img pipeline once (reused across all levels).""" + import torch + from diffusers import AutoPipelineForImage2Image + + dev = "mps" if torch.backends.mps.is_available() else "cpu" + pipe = AutoPipelineForImage2Image.from_pretrained( + model, torch_dtype=torch.float16 if dev == "mps" else torch.float32, + safety_checker=None).to(dev) + return pipe, dev + + +def restyle_clip(pipe, dev, src: str, out: str, key_strength: float, + tween_strength: float, *, fps: float = 12.0, keyint: int = 24) -> None: + """Restyle one clip: SD img2img keyframes + flow-warped light-refine tweens.""" + import cv2 + import numpy as np + import torch + from PIL import Image + + ff = _ffmpeg() + gen = torch.Generator(device=dev).manual_seed(SEED) + + def img2img(pil, strength): + steps = max(2, int(np.ceil(1.0 / max(strength, 0.05))) + 1) + return pipe(prompt=PROMPT, negative_prompt=NEG, image=pil, + strength=strength, guidance_scale=0.0, + num_inference_steps=steps, generator=gen).images[0] + + with tempfile.TemporaryDirectory() as d: + frames = _extract(ff, src, d, fps) + print(f" [frames] {len(frames)} key_s={key_strength} tween_s={tween_strength}") + prev_styled = None # BGR uint8 + prev_src_gray = None + t0 = time.time() + for i, fp in enumerate(frames): + src_bgr = cv2.imread(fp) + h0, w0 = src_bgr.shape[:2] + scale = 768 / max(h0, w0) + wh = (int(w0 * scale) // 8 * 8, int(h0 * scale) // 8 * 8) + src_small = cv2.resize(src_bgr, wh) + src_gray = cv2.cvtColor(src_small, cv2.COLOR_BGR2GRAY) + + if i % keyint == 0 or prev_styled is None: + pil = Image.fromarray(cv2.cvtColor(src_small, cv2.COLOR_BGR2RGB)) + out_img = img2img(pil, key_strength) + else: + warped = _warp(prev_styled, prev_src_gray, src_gray) + pil = Image.fromarray(cv2.cvtColor(warped, cv2.COLOR_BGR2RGB)) + out_img = img2img(pil, tween_strength) # light refine only + styled = cv2.cvtColor(np.array(out_img), cv2.COLOR_RGB2BGR) + prev_styled, prev_src_gray = styled, src_gray + cv2.imwrite(os.path.join(d, f"s_{i+1:05d}.png"), + cv2.resize(styled, (w0, h0))) + if i % 12 == 0: + print(f" frame {i+1}/{len(frames)} {time.time()-t0:.1f}s") + + _run([ff, "-hide_banner", "-v", "error", "-framerate", str(fps), + "-i", os.path.join(d, "s_%05d.png"), "-c:v", "libx264", "-crf", "20", + "-preset", "medium", "-pix_fmt", "yuv420p", out, "-y"]) + print(f" [done] {time.time()-t0:.1f}s -> {out}") + + +def bake_scale(scale: str = "forest", *, fps: float = 12.0, keyint: int = 24, + model: str = "stabilityai/sd-turbo") -> None: + """Bake all RIGHT_LEVELS variants for one scale's base clip.""" + base = MEDIA / scale / "base.mp4" + if not base.exists(): + raise FileNotFoundError( + f"no base clip at {base} — run the media-setup scripts first") + print(f"[bake] scale={scale} base={base}") + pipe, dev = _load_pipe(model) + print(f"[device] {dev}") + for level in RIGHT_LEVELS: + key_s, tween_s = right_strength_params(level) + out = MEDIA / scale / f"right{level}.mp4" + print(f" [right {level}] -> {out}") + restyle_clip(pipe, dev, str(base), str(out), key_s, tween_s, + fps=fps, keyint=keyint) + print(f"[bake] {scale} done — {len(RIGHT_LEVELS)} real Right variants") + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--scale", default="forest") + ap.add_argument("--fps", type=float, default=12.0) + ap.add_argument("--keyint", type=int, default=24) + a = ap.parse_args() + bake_scale(a.scale, fps=a.fps, keyint=a.keyint) + + +if __name__ == "__main__": + main() diff --git a/simulator/sample_media/manifest.json b/simulator/sample_media/manifest.json index be9c3e9..d5c1ce7 100644 --- a/simulator/sample_media/manifest.json +++ b/simulator/sample_media/manifest.json @@ -25,9 +25,9 @@ "license": "poc-sample (look-tuning only; not shipped content)", "source": "hef-poc/out/neutral.mp4", "right_variants": { - "1": {"file": "forest/right1.mp4", "model": "placeholder"}, - "2": {"file": "forest/right2.mp4", "model": "placeholder"}, - "3": {"file": "forest/right3.mp4", "model": "placeholder"}, + "1": {"file": "forest/right1.mp4", "model": "sd-turbo+farneback-flow"}, + "2": {"file": "forest/right2.mp4", "model": "sd-turbo+farneback-flow"}, + "3": {"file": "forest/right3.mp4", "model": "sd-turbo+farneback-flow"}, "4": {"file": "forest/right4.mp4", "model": "sd-turbo+farneback-flow"} }, "annotations": [ diff --git a/simulator/setup_sample_media.py b/simulator/setup_sample_media.py index 6f947a6..764983c 100644 --- a/simulator/setup_sample_media.py +++ b/simulator/setup_sample_media.py @@ -1,52 +1,33 @@ -"""Populate simulator/sample_media/forest/ from the session-0008 POC artifacts. +"""Stage the forest scale's neutral BASE clip from the session-0008 POC artifacts. -Copies the real neutral base + the real flow-stabilized Right restyle out of -~/hef-poc/out/, and generates placeholder intermediate Right strengths (1..3) by -blending the base toward the real restyle with ffmpeg. The media binaries are -gitignored; only the manifest is committed. Sample footage is for look-tuning -only, not shipped content. +Copies the real neutral Yosemite base out of ~/hef-poc/out/ into +simulator/sample_media/forest/. The real multi-strength Right variants are then +produced by the offline baker (`simulator/bake_right_variants.py`) — this script +no longer generates placeholder Right strengths, so re-running it never clobbers +a real bake. The media binaries are gitignored; only the manifest is committed. +Sample footage is for look-tuning only, not shipped content. -Usage: python simulator/setup_sample_media.py -Requires: ffmpeg on PATH (or `pip install imageio-ffmpeg`), and ~/hef-poc/out/. +Usage: + python simulator/setup_sample_media.py # stage the base + python -m simulator.bake_right_variants --scale forest # then bake variants +Requires: ~/hef-poc/out/neutral.mp4 (the POC neutral base). """ from __future__ import annotations import shutil -import subprocess from pathlib import Path POC = Path.home() / "hef-poc" / "out" DEST = Path(__file__).parent / "sample_media" / "forest" -def _ffmpeg() -> str: - if shutil.which("ffmpeg"): - return "ffmpeg" - import imageio_ffmpeg - return imageio_ffmpeg.get_ffmpeg_exe() - - def main() -> None: DEST.mkdir(parents=True, exist_ok=True) base = DEST / "base.mp4" - right4 = DEST / "right4.mp4" shutil.copyfile(POC / "neutral.mp4", base) - shutil.copyfile(POC / "right_flow.mp4", right4) - ff = _ffmpeg() - # Placeholder strengths 1..3: opacity-blend base toward the real restyle. - for strength, alpha in ((1, 0.25), (2, 0.5), (3, 0.75)): - out = DEST / f"right{strength}.mp4" - subprocess.run( - [ff, "-y", "-i", str(base), "-i", str(right4), - "-filter_complex", - f"[1:v]format=yuva444p,colorchannelmixer=aa={alpha}[top];" - f"[0:v][top]overlay=shortest=1[v]", - "-map", "[v]", "-an", str(out)], - check=True, - ) - print(f"generated {out.name} (alpha {alpha})") - print(f"sample media ready in {DEST}") + print(f"staged {base} (real POC neutral base)") + print("next: python -m simulator.bake_right_variants --scale forest") if __name__ == "__main__": diff --git a/tests/test_bake_right_variants.py b/tests/test_bake_right_variants.py new file mode 100644 index 0000000..582d40c --- /dev/null +++ b/tests/test_bake_right_variants.py @@ -0,0 +1,26 @@ +import pytest + +from simulator.bake_right_variants import RIGHT_LEVELS, right_strength_params + + +def test_levels_are_one_through_four(): + assert RIGHT_LEVELS == (1, 2, 3, 4) + + +def test_strength_increases_with_level(): + keys = [right_strength_params(l)[0] for l in RIGHT_LEVELS] + assert keys == sorted(keys) # monotonic ramp + assert keys[0] < keys[-1] # subtle -> strong + + +def test_tween_strength_is_a_fraction_of_key_strength(): + for l in RIGHT_LEVELS: + key, tween = right_strength_params(l) + assert 0.0 < tween < key # tween is a light refine only + + +def test_rejects_out_of_range_level(): + with pytest.raises(ValueError): + right_strength_params(0) + with pytest.raises(ValueError): + right_strength_params(5)