feat(content): content pipeline Increment 1 — tooling + annotation tracks + 5-scale ring #13
@@ -1,4 +1,4 @@
|
||||
from tools.pipeline.ffmpeg_ops import frame_args
|
||||
from tools.pipeline.ffmpeg_ops import frame_args, crossfade_loop_args
|
||||
|
||||
|
||||
def test_frame_args_trims_scales_pads_and_strips_audio():
|
||||
@@ -15,3 +15,21 @@ def test_frame_args_trims_scales_pads_and_strips_audio():
|
||||
assert "pad=1920:1080" in vf
|
||||
assert "format=yuv420p" in vf
|
||||
assert args[-1] == "out.mp4"
|
||||
|
||||
|
||||
def test_crossfade_loop_builds_head_mid_tail_xfade_concat():
|
||||
args = crossfade_loop_args("in.mp4", "loop.mp4", duration=8.0, overlap=1.0)
|
||||
fc = args[args.index("-filter_complex") + 1]
|
||||
assert "trim=0:1.0" in fc # head = first O secs
|
||||
assert "trim=1.0:7.0" in fc # mid = [O, D-O]
|
||||
assert "trim=7.0:8.0" in fc # tail = last O secs
|
||||
assert "xfade=transition=fade:duration=1.0:offset=0" in fc
|
||||
assert "concat=n=2:v=1" in fc
|
||||
assert "-an" in args
|
||||
assert args[-1] == "loop.mp4"
|
||||
|
||||
|
||||
def test_crossfade_loop_rejects_overlap_too_large():
|
||||
import pytest
|
||||
with pytest.raises(ValueError):
|
||||
crossfade_loop_args("in.mp4", "loop.mp4", duration=4.0, overlap=2.0)
|
||||
|
||||
@@ -28,3 +28,26 @@ def frame_args(src, dst, *, start: float, duration: float,
|
||||
"-c:v", "libx264", "-preset", "medium", "-crf", "16",
|
||||
"-pix_fmt", "yuv420p", "-movflags", "+faststart", str(dst),
|
||||
]
|
||||
|
||||
|
||||
def crossfade_loop_args(src, dst, *, duration: float, overlap: float,
|
||||
ff: str = "ffmpeg") -> list[str]:
|
||||
"""Stage 3 — make `src` loop seamlessly by crossfading its tail over its head.
|
||||
Output length = duration − overlap. Requires overlap < duration/2 so a non-empty
|
||||
middle remains."""
|
||||
if overlap <= 0 or overlap >= duration / 2:
|
||||
raise ValueError(f"overlap {overlap} must be in (0, duration/2={duration/2})")
|
||||
d, o = duration, overlap
|
||||
fc = (
|
||||
f"[0:v]trim=0:{o},setpts=PTS-STARTPTS[head];"
|
||||
f"[0:v]trim={o}:{d - o},setpts=PTS-STARTPTS[mid];"
|
||||
f"[0:v]trim={d - o}:{d},setpts=PTS-STARTPTS[tail];"
|
||||
f"[tail][head]xfade=transition=fade:duration={o}:offset=0[xf];"
|
||||
f"[xf][mid]concat=n=2:v=1,format=yuv420p[out]"
|
||||
)
|
||||
return [
|
||||
ff, "-y", "-i", str(src), "-filter_complex", fc,
|
||||
"-map", "[out]", "-an",
|
||||
"-c:v", "libx264", "-preset", "medium", "-crf", "16",
|
||||
"-pix_fmt", "yuv420p", "-movflags", "+faststart", str(dst),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user