feat(pipeline): per-clip-pair morph manifest + member-pair baking (154 clips)
Tasks 1-2 of the altitude clip-pair morph plan. Manifest emits a morph entry for every video pair in adjacent altitudes, both directions (<src>__<dst>.mp4 zoom-in + .rev zoom-out); generate_media() bakes them all via the existing xfade=zoomin recipe keyed by clip member. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -372,10 +372,15 @@ def build_manifest() -> dict:
|
||||
for s in RING_ORDER
|
||||
]
|
||||
transitions = []
|
||||
n = len(RING_ORDER)
|
||||
for i in range(n):
|
||||
a, b = RING_ORDER[i], RING_ORDER[(i + 1) % n]
|
||||
transitions.append({"file": f"transitions/{a}-{b}.mp4", "model": "placeholder-zoom"})
|
||||
for H, L in _adjacent_edges(): # H higher altitude, L lower
|
||||
for h in POOLS[H]:
|
||||
for l in POOLS[L]:
|
||||
transitions.append({"from": h, "to": l,
|
||||
"file": f"transitions/{h}__{l}.mp4", # zoom IN (descend)
|
||||
"model": "placeholder-zoom"})
|
||||
transitions.append({"from": l, "to": h,
|
||||
"file": f"transitions/{h}__{l}.rev.mp4", # zoom OUT (ascend)
|
||||
"model": "placeholder-zoom"})
|
||||
return {"clips": clips, "ring": {"scales": scales, "transitions": transitions}}
|
||||
|
||||
|
||||
@@ -388,14 +393,19 @@ def _ffmpeg() -> str:
|
||||
return imageio_ffmpeg.get_ffmpeg_exe()
|
||||
|
||||
|
||||
def _make_transition(ff: str, scale_a: str, scale_b: str) -> Path:
|
||||
"""A zoom/warp morph between two scales, from their PRIMARY pool members'
|
||||
bases (mirrors setup_scales_media.py but keyed by scale->primary). This is the
|
||||
recipe behind the well-liked orbit-coast edge; generate_media() now applies it
|
||||
uniformly to every edge so none carry stale footage from an earlier ring."""
|
||||
a = MEDIA / POOLS[scale_a][0] / "base.mp4"
|
||||
b = MEDIA / POOLS[scale_b][0] / "base.mp4"
|
||||
out = MEDIA / "transitions" / f"{scale_a}-{scale_b}.mp4"
|
||||
def _adjacent_edges() -> list[tuple[str, str]]:
|
||||
"""Ordered (higher, lower) altitude edges around the ring, including the wrap."""
|
||||
n = len(RING_ORDER)
|
||||
return [(RING_ORDER[i], RING_ORDER[(i + 1) % n]) for i in range(n)]
|
||||
|
||||
|
||||
def _make_transition(ff: str, src_clip: str, dst_clip: str) -> Path:
|
||||
"""A zoom/warp morph between two CLIP MEMBERS' base footage (the well-liked
|
||||
orbit-coast recipe), keyed by clip id so every adjacent member pair gets its
|
||||
own morph. Forward = zoom IN (descend src->dst)."""
|
||||
a = MEDIA / src_clip / "base.mp4"
|
||||
b = MEDIA / dst_clip / "base.mp4"
|
||||
out = MEDIA / "transitions" / f"{src_clip}__{dst_clip}.mp4"
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
norm = "trim=0:3,setpts=PTS-STARTPTS,scale=1280:720,fps=25,setsar=1,format=yuv420p"
|
||||
subprocess.run([
|
||||
@@ -420,16 +430,16 @@ def _make_reverse(ff: str, forward: Path) -> Path:
|
||||
|
||||
|
||||
def generate_media() -> None:
|
||||
"""(Re)build EVERY ring-edge transition from the current real primary bases —
|
||||
the orbit-coast recipe applied uniformly, overwriting any stale clips left from
|
||||
an earlier ring — plus a reversed companion per edge for zoom-out moves."""
|
||||
"""Bake EVERY adjacent member-pair morph from real bases — forward (zoom in,
|
||||
descend) plus its reversed companion (zoom out, ascend), for every pair of
|
||||
clips in adjacent altitudes."""
|
||||
ff = _ffmpeg()
|
||||
n = len(RING_ORDER)
|
||||
for i in range(n):
|
||||
a, b = RING_ORDER[i], RING_ORDER[(i + 1) % n]
|
||||
fwd = _make_transition(ff, a, b)
|
||||
_make_reverse(ff, fwd)
|
||||
print(f"generated transitions/{a}-{b}.mp4 (+ .rev) from real bases")
|
||||
for H, L in _adjacent_edges():
|
||||
for h in POOLS[H]:
|
||||
for l in POOLS[L]:
|
||||
fwd = _make_transition(ff, h, l)
|
||||
_make_reverse(ff, fwd)
|
||||
print(f"generated {len(POOLS[H]) * len(POOLS[L])} {H}__{L} morphs (+ .rev) from real bases")
|
||||
|
||||
|
||||
def main(argv: list[str]) -> None:
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Tests for the hand-authored rotating-pool manifest builder
|
||||
(simulator/build_pool_manifest.py) — the per-clip-pair morph model."""
|
||||
|
||||
import simulator.build_pool_manifest as b
|
||||
|
||||
|
||||
def _expected_pairs():
|
||||
"""Every adjacent (higher H, lower L) edge, every member pair, BOTH directions."""
|
||||
order = b.RING_ORDER
|
||||
n = len(order)
|
||||
expected = set()
|
||||
for i in range(n):
|
||||
H, L = order[i], order[(i + 1) % n]
|
||||
for h in b.POOLS[H]:
|
||||
for l in b.POOLS[L]:
|
||||
expected.add((h, l, f"transitions/{h}__{l}.mp4")) # zoom in
|
||||
expected.add((l, h, f"transitions/{h}__{l}.rev.mp4")) # zoom out
|
||||
return expected
|
||||
|
||||
|
||||
def test_transitions_are_per_clip_pair_both_directions():
|
||||
m = b.build_manifest()
|
||||
trans = m["ring"]["transitions"]
|
||||
expected = _expected_pairs()
|
||||
got = {(t["from"], t["to"], t["file"]) for t in trans}
|
||||
assert got == expected
|
||||
assert len(trans) == len(expected) == 154
|
||||
assert all(t["model"] == "placeholder-zoom" for t in trans)
|
||||
|
||||
|
||||
def test_generate_media_bakes_every_member_pair(monkeypatch, tmp_path):
|
||||
calls = []
|
||||
|
||||
def fake_run(args, **kw):
|
||||
out = args[-1]
|
||||
from pathlib import Path
|
||||
Path(out).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(out).write_bytes(b"x")
|
||||
calls.append(str(out))
|
||||
|
||||
class R:
|
||||
returncode = 0
|
||||
|
||||
return R()
|
||||
|
||||
monkeypatch.setattr(b, "MEDIA", tmp_path)
|
||||
monkeypatch.setattr(b.subprocess, "run", fake_run)
|
||||
monkeypatch.setattr(b, "_ffmpeg", lambda: "ffmpeg")
|
||||
b.generate_media()
|
||||
outs = {c.split("transitions/")[-1] for c in calls}
|
||||
fwd = {f"{h}__{l}.mp4" for H, L in b._adjacent_edges()
|
||||
for h in b.POOLS[H] for l in b.POOLS[L]}
|
||||
rev = {f"{h}__{l}.rev.mp4" for H, L in b._adjacent_edges()
|
||||
for h in b.POOLS[H] for l in b.POOLS[L]}
|
||||
assert fwd <= outs and rev <= outs
|
||||
assert len(fwd) + len(rev) == 154
|
||||
Reference in New Issue
Block a user