"""Shared pytest fixtures: ffmpeg-synthesized clips and scripts/ on sys.path.""" from __future__ import annotations import subprocess import sys from pathlib import Path import pytest # Make the bundled scripts importable (mirrors watch.py's sys.path insert). SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "skills" / "watch" / "scripts" sys.path.insert(0, str(SCRIPTS_DIR)) # 14 visually distinct fills → 14 abrupt cuts → x264 emits a keyframe per cut. COLORS = [ "red", "green", "blue", "white", "black", "yellow", "cyan", "magenta", "gray", "orange", "purple", "brown", "navy", "olive", ] def _run(cmd: list[str]) -> None: result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(f"ffmpeg failed: {' '.join(cmd)}\n{result.stderr}") def build_cut_clip( path: Path, n: int = 14, seg: float = 0.4, size: str = "320x240", fps: int = 10, ) -> None: """Concatenate ``n`` solid-color segments into one clip with ``n`` cuts. Each color change is a hard scene cut, so the scene selector finds ~n-1 changes. x264's own scenecut detection is unreliable on flat fills, so we force a keyframe at every ``seg`` boundary — giving ~n real keyframes for the keyframe engine to find. """ inputs: list[str] = [] for i in range(n): color = COLORS[i % len(COLORS)] inputs += ["-f", "lavfi", "-t", str(seg), "-i", f"color=c={color}:s={size}:r={fps}"] streams = "".join(f"[{i}:v]" for i in range(n)) filt = f"{streams}concat=n={n}:v=1:a=0[out]" _run([ "ffmpeg", "-hide_banner", "-loglevel", "error", "-y", *inputs, "-filter_complex", filt, "-map", "[out]", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-force_key_frames", f"expr:gte(t,n_forced*{seg})", str(path), ]) def build_static_clip( path: Path, duration: float = 3.0, size: str = "320x240", fps: int = 10, ) -> None: """One solid color: 1 keyframe, no scene changes → triggers both fallbacks.""" _run([ "ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-f", "lavfi", "-t", str(duration), "-i", f"color=c=blue:s={size}:r={fps}", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-g", "600", str(path), ]) @pytest.fixture(scope="session") def cut_clip(tmp_path_factory: pytest.TempPathFactory) -> Path: path = tmp_path_factory.mktemp("clips") / "cuts.mp4" build_cut_clip(path) return path @pytest.fixture(scope="session") def static_clip(tmp_path_factory: pytest.TempPathFactory) -> Path: path = tmp_path_factory.mktemp("clips") / "static.mp4" build_static_clip(path) return path