From 642451925f3a1e2cfe505db744e53094d237ce76 Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Wed, 24 Jun 2026 07:53:28 -0700 Subject: [PATCH] =?UTF-8?q?feat(pipeline):=20frame=20stage=20ffmpeg=20arg?= =?UTF-8?q?=20builder=20(content=20pipeline=20=C2=A73.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_pipeline_ffmpeg_ops.py | 17 +++++++++++++++++ tools/pipeline/__init__.py | 5 +++++ tools/pipeline/ffmpeg_ops.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 tests/test_pipeline_ffmpeg_ops.py create mode 100644 tools/pipeline/__init__.py create mode 100644 tools/pipeline/ffmpeg_ops.py diff --git a/tests/test_pipeline_ffmpeg_ops.py b/tests/test_pipeline_ffmpeg_ops.py new file mode 100644 index 0000000..91d0158 --- /dev/null +++ b/tests/test_pipeline_ffmpeg_ops.py @@ -0,0 +1,17 @@ +from tools.pipeline.ffmpeg_ops import frame_args + + +def test_frame_args_trims_scales_pads_and_strips_audio(): + args = frame_args( + "src.mp4", "out.mp4", + start=2.0, duration=8.0, width=1920, height=1080, ff="ffmpeg", + ) + assert args[0] == "ffmpeg" + assert "-ss" in args and args[args.index("-ss") + 1] == "2.0" + assert "-t" in args and args[args.index("-t") + 1] == "8.0" + assert "-an" in args # audio stripped (bases are video-only) + vf = args[args.index("-vf") + 1] + assert "scale=1920:1080:force_original_aspect_ratio=decrease" in vf + assert "pad=1920:1080" in vf + assert "format=yuv420p" in vf + assert args[-1] == "out.mp4" diff --git a/tools/pipeline/__init__.py b/tools/pipeline/__init__.py new file mode 100644 index 0000000..7f38557 --- /dev/null +++ b/tools/pipeline/__init__.py @@ -0,0 +1,5 @@ +"""Content pipeline: source -> frame -> loop -> transcode -> manifest. + +Pure ffmpeg-argument builders (ffmpeg_ops), a thin runner (run), provenance +records, and manifest assembly. See docs/superpowers/specs/2026-06-24-content-pipeline-design.md. +""" diff --git a/tools/pipeline/ffmpeg_ops.py b/tools/pipeline/ffmpeg_ops.py new file mode 100644 index 0000000..5ee73aa --- /dev/null +++ b/tools/pipeline/ffmpeg_ops.py @@ -0,0 +1,30 @@ +"""Pure ffmpeg argument builders (no I/O) for the content pipeline. + +Each function returns a list[str] ready for ffmpeg, so command construction is +unit-testable without running ffmpeg. tools/pipeline/run.py executes them. The +ffmpeg binary name is injected (default "ffmpeg"). +""" + +from __future__ import annotations + + +def _fit(width: int, height: int) -> str: + """A scale+pad filter chain fitting any input into width×height (letterbox), + fixing SAR and pixel format so downstream concat/xfade get identical geometry.""" + return ( + f"scale={width}:{height}:force_original_aspect_ratio=decrease," + f"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2," + "setsar=1,format=yuv420p" + ) + + +def frame_args(src, dst, *, start: float, duration: float, + width: int, height: int, ff: str = "ffmpeg") -> list[str]: + """Stage 2 — trim [start, start+duration], fit to width×height, strip audio. + Produces a high-quality mezzanine for the loop stage.""" + return [ + ff, "-y", "-ss", str(start), "-t", str(duration), + "-i", str(src), "-vf", _fit(width, height), "-an", + "-c:v", "libx264", "-preset", "medium", "-crf", "16", + "-pix_fmt", "yuv420p", "-movflags", "+faststart", str(dst), + ]