Real footage of the installed 0.1.5 apps, composited in Remotion with shape morphs between sections; synthesized score and UI sounds. Logs P3-17 (ElyTable keyboard zoom leaves the grid half-scrolled). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
48 lines
2.0 KiB
Python
48 lines
2.0 KiB
Python
"""Finds each take's sync jump; writes footage time = log time + offset.
|
|
|
|
Usage: sync.py <takes dir> <name> ...
|
|
Converts each take to constant 60 fps (`<name>.60.mp4`) and writes
|
|
`<name>.sync.json` with the offset and the source start time.
|
|
"""
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import av
|
|
import numpy as np
|
|
|
|
|
|
def first_change(video: Path, x: int, y: int, after: float, until: float):
|
|
"""The first frame time when the patch at (x, y) changes."""
|
|
with av.open(str(video)) as box:
|
|
stream = box.streams.video[0]
|
|
start = float(stream.start_time * stream.time_base) if stream.start_time else 0.0
|
|
base = None
|
|
for frame in box.decode(stream):
|
|
t = float(frame.pts * stream.time_base) - start
|
|
if t < after:
|
|
continue
|
|
if t > until:
|
|
break
|
|
patch = frame.to_ndarray(format="gray")[y - 24:y + 24, x - 24:x + 24].astype(np.int16)
|
|
if base is None:
|
|
base = patch
|
|
continue
|
|
if np.abs(patch - base).mean() > 6:
|
|
return t, start
|
|
raise RuntimeError(f"{video.name}: no sync jump between {after} and {until} s")
|
|
|
|
|
|
root = Path(sys.argv[1])
|
|
for name in sys.argv[2:]:
|
|
events = json.loads((root / f"{name}.json").read_text(encoding="utf-8"))
|
|
sync = next(e for e in events if e["kind"] == "sync")
|
|
seen, start = first_change(root / f"{name}.mp4", sync["x"], sync["y"], sync["t"] - 0.5, sync["t"] + 1.5)
|
|
offset = round(seen - sync["t"], 4)
|
|
(root / f"{name}.sync.json").write_text(json.dumps({"offset": offset, "start": start}), encoding="utf-8")
|
|
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-i", str(root / f"{name}.mp4"),
|
|
"-vf", "fps=60", "-c:v", "h264_nvenc", "-preset", "p7", "-rc", "vbr", "-cq", "14",
|
|
"-g", "30", "-pix_fmt", "yuv420p", str(root / f"{name}.60.mp4")], check=True)
|
|
print(f"{name}: jump seen at {seen:.3f} s, logged at {sync['t']:.3f} s, offset {offset:+.3f} s")
|