From 92774216d48a067018252634c86cb880a17d1f4f Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Thu, 18 Jun 2026 14:58:50 +0200 Subject: [PATCH] feat: LTX-2 ffmpeg params (target_fps, snap32, frames) Co-Authored-By: Claude Fable 5 --- core/ffmpeg.py | 16 ++++++++++++++++ tests/test_utils.py | 22 ++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/core/ffmpeg.py b/core/ffmpeg.py index 57e33ab..4827b31 100644 --- a/core/ffmpeg.py +++ b/core/ffmpeg.py @@ -79,6 +79,9 @@ def build_ffmpeg_command( image_sequence: bool = False, encoder: str = "libx264", duration: float = 8.0, + target_fps: float | None = None, + snap32: bool = False, + frames: int | None = None, ) -> list[str]: # -ss before -i: fast input-seeking. Safe here because we always re-encode, # so there is no keyframe-alignment issue from pre-input seek. @@ -109,6 +112,13 @@ def build_ffmpeg_command( f"scale='if(lt(iw,ih),{short_side},-2)':'if(lt(iw,ih),-2,{short_side})':flags=lanczos" ) + # LTX-2: centered crop to ÷32 (no rescale → no aspect distortion) then fps. + # Placed among CPU filters, after scale and before the VAAPI hwupload block. + if snap32: + filters.append("crop=trunc(iw/32)*32:trunc(ih/32)*32") + if target_fps is not None: + filters.append(f"fps={target_fps:g}") + # VAAPI: decoded frames are GPU surfaces. CPU filters need hwdownload first. if use_hw_vaapi: if filters: @@ -120,6 +130,12 @@ def build_ffmpeg_command( if filters: cmd += ["-vf", ",".join(filters)] + # LTX-2 output rate + exact frame cap (apply to both clip and webp-seq paths). + if target_fps is not None: + cmd += ["-r", f"{target_fps:g}"] + if frames is not None: + cmd += ["-frames:v", str(frames)] + if image_sequence: cmd += [ "-an", diff --git a/tests/test_utils.py b/tests/test_utils.py index 35f48a8..46f7877 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -462,3 +462,25 @@ def test_ltx2_legal_series(): s = legal_frames(min_f=9, max_f=33) assert s == [9, 17, 25, 33] + +# --- LTX-2 ffmpeg params (target_fps, snap32, frames) --- + +def test_ffmpeg_ltx2_fps_and_frames(): + cmd = build_ffmpeg_command("/in/v.mp4", 0.0, "/out/c.mp4", + short_side=512, target_fps=25, frames=201) + assert "-r" in cmd and cmd[cmd.index("-r")+1] == "25" + assert "-frames:v" in cmd and cmd[cmd.index("-frames:v")+1] == "201" + vf = cmd[cmd.index("-vf")+1] + assert "fps=25" in vf + +def test_ffmpeg_ltx2_snap32_crop(): + cmd = build_ffmpeg_command("/in/v.mp4", 0.0, "/out/c.mp4", + short_side=512, snap32=True) + vf = cmd[cmd.index("-vf")+1] + assert "crop=trunc(iw/32)*32:trunc(ih/32)*32" in vf + +def test_ffmpeg_foley_unchanged(): + cmd = build_ffmpeg_command("/in/v.mp4", 0.0, "/out/c.mp4", short_side=256) + assert "-r" not in cmd and "-frames:v" not in cmd + assert "crop=trunc" not in cmd[cmd.index("-vf")+1] +