feat(pipeline): resolve bundled ffmpeg fallback; integration test runs on real forest base

- run.py: add resolve_ffmpeg() mirroring setup_scales_media.py; change
  process_clip ff param to None and resolve at call time
- test_pipeline_integration.py: gate on resolve_ffmpeg() not PATH ffmpeg;
  verify proxy dims with cv2 instead of ffprobe
- ffmpeg_ops.py: add fps= to trim segments in crossfade_loop_args so xfade
  receives CFR input (xfade rejects 1/0 rate from raw trim output)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-24 08:05:11 -07:00
parent 38a1046433
commit f1c83c1a9a
3 changed files with 41 additions and 18 deletions
+12 -1
View File
@@ -4,6 +4,7 @@ this glue is covered by the opt-in integration test."""
from __future__ import annotations
import shutil
import subprocess
from pathlib import Path
@@ -12,6 +13,15 @@ from .ffmpeg_ops import crossfade_loop_args, frame_args, transcode_args
MASTER_MAX_W = 3840
def resolve_ffmpeg() -> str:
"""ffmpeg from PATH, else the bundled imageio-ffmpeg binary (matches
simulator/setup_scales_media.py). The Pi/dev box may lack a system ffmpeg."""
if shutil.which("ffmpeg"):
return "ffmpeg"
import imageio_ffmpeg
return imageio_ffmpeg.get_ffmpeg_exe()
def probe_duration(src, *, ff: str = "ffprobe") -> float:
out = subprocess.run(
[ff, "-v", "quiet", "-show_entries", "format=duration",
@@ -27,9 +37,10 @@ def _run(args: list[str]) -> None:
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:
ff: str | None = None) -> dict:
"""Run stages 24 for one clip; return {'master','proxy','loop','mezzanine'} paths.
`duration` defaults to the full source length (minus the trim start)."""
ff = ff or resolve_ffmpeg()
src = Path(src)
out_dir = Path(out_dir)
out_dir.mkdir(parents=True, exist_ok=True)