From 38a10464330b0f259469f3aa7b40649c51c34bdd Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Wed, 24 Jun 2026 08:01:25 -0700 Subject: [PATCH] =?UTF-8?q?feat(pipeline):=20runner=20+=20opt-in=20integra?= =?UTF-8?q?tion=20test=20on=20real=20forest=20base=20(content=20pipeline?= =?UTF-8?q?=20=C2=A73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- tests/test_pipeline_integration.py | 25 +++++++++++++++ tools/pipeline/run.py | 49 ++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 tests/test_pipeline_integration.py create mode 100644 tools/pipeline/run.py diff --git a/tests/test_pipeline_integration.py b/tests/test_pipeline_integration.py new file mode 100644 index 0000000..d6df6b8 --- /dev/null +++ b/tests/test_pipeline_integration.py @@ -0,0 +1,25 @@ +import shutil +import subprocess +from pathlib import Path + +import pytest + +from tools.pipeline.run import process_clip + +_HAS_FF = shutil.which("ffmpeg") is not None and shutil.which("ffprobe") is not None +_FOREST = Path("simulator/sample_media/forest/base.mp4") + + +@pytest.mark.skipif(not _HAS_FF, reason="ffmpeg/ffprobe not installed") +@pytest.mark.skipif(not _FOREST.exists(), + reason="forest POC base absent (run simulator/setup_sample_media.py)") +def test_process_clip_emits_proxy_and_master(tmp_path): + out = process_clip(_FOREST, tmp_path, overlap=0.5, start=0.0, duration=4.0) + assert out["proxy"].exists() and out["master"].exists() + # Proxy is exactly 1920x1080. + probe = subprocess.run( + ["ffprobe", "-v", "quiet", "-select_streams", "v:0", + "-show_entries", "stream=width,height", "-of", "csv=p=0", str(out["proxy"])], + capture_output=True, text=True, check=True, + ).stdout.strip() + assert probe == "1920,1080" diff --git a/tools/pipeline/run.py b/tools/pipeline/run.py new file mode 100644 index 0000000..56c7346 --- /dev/null +++ b/tools/pipeline/run.py @@ -0,0 +1,49 @@ +"""Thin runner: ffprobe the source, then execute the frame -> loop -> transcode +stages with tools.pipeline.ffmpeg_ops. The pure arg builders are unit-tested; +this glue is covered by the opt-in integration test.""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + +from .ffmpeg_ops import crossfade_loop_args, frame_args, transcode_args + +MASTER_MAX_W = 3840 + + +def probe_duration(src, *, ff: str = "ffprobe") -> float: + out = subprocess.run( + [ff, "-v", "quiet", "-show_entries", "format=duration", + "-of", "csv=p=0", str(src)], + capture_output=True, text=True, check=True, + ).stdout.strip() + return float(out) + + +def _run(args: list[str]) -> None: + subprocess.run(args, check=True, capture_output=True) + + +def process_clip(src, out_dir, *, start: float = 0.0, duration: float | None = None, + overlap: float = 1.0, master_w: int = 3840, master_h: int = 2160, + ff: str = "ffmpeg") -> dict: + """Run stages 2–4 for one clip; return {'master','proxy','loop','mezzanine'} paths. + `duration` defaults to the full source length (minus the trim start).""" + src = Path(src) + out_dir = Path(out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + if duration is None: + duration = probe_duration(src) - start + + mezz = out_dir / "mezzanine.mp4" + loop = out_dir / "loop.mp4" + master = out_dir / "master.mp4" + proxy = out_dir / "base.mp4" # the proxy is what the manifest's base_file points at + + _run(frame_args(src, mezz, start=start, duration=duration, + width=master_w, height=master_h, ff=ff)) + _run(crossfade_loop_args(mezz, loop, duration=duration, overlap=overlap, ff=ff)) + _run(transcode_args(loop, master, width=master_w, height=master_h, crf=18, ff=ff)) + _run(transcode_args(loop, proxy, width=1920, height=1080, crf=20, ff=ff)) + return {"mezzanine": mezz, "loop": loop, "master": master, "proxy": proxy}