diff --git a/tests/test_mediatools.py b/tests/test_mediatools.py new file mode 100644 index 0000000..eafb097 --- /dev/null +++ b/tests/test_mediatools.py @@ -0,0 +1,38 @@ +from tools.mediatools import ( + compute_dominant_color, + dominant_color_from_rgb, + extract_frame, +) + + +def test_dominant_color_from_rgb_pure_red(): + assert dominant_color_from_rgb(b"\xff\x00\x00") == "#ff0000" + + +def test_dominant_color_from_rgb_arbitrary(): + assert dominant_color_from_rgb(b"\x12\xab\x0f") == "#12ab0f" + + +def test_compute_dominant_color_uses_runner(): + captured = {} + + def fake_runner(args): + captured["args"] = args + return b"\x00\x80\xff" + + color = compute_dominant_color("clip.mp4", midpoint_s=5.0, runner=fake_runner) + assert color == "#0080ff" + assert "ffmpeg" in captured["args"] + assert "5.0" in captured["args"] + + +def test_extract_frame_invokes_runner_and_returns_dest(tmp_path): + dest = tmp_path / "frame.png" + calls = [] + + def fake_runner(args): + calls.append(args) + + out = extract_frame("clip.mp4", dest, midpoint_s=2.0, runner=fake_runner) + assert out == dest + assert calls and "ffmpeg" in calls[0] and str(dest) in calls[0] diff --git a/tools/mediatools.py b/tools/mediatools.py new file mode 100644 index 0000000..d397982 --- /dev/null +++ b/tools/mediatools.py @@ -0,0 +1,58 @@ +"""ffmpeg helpers: representative frame; OPTIONAL dominant color. Runner injectable.""" + +from __future__ import annotations + +import subprocess + + +def _run_bytes(args) -> bytes: + return subprocess.run(args, capture_output=True, check=True).stdout + + +def dominant_color_from_rgb(rgb: bytes) -> str: + r, g, b = rgb[0], rgb[1], rgb[2] + return f"#{r:02x}{g:02x}{b:02x}" + + +def compute_dominant_color(path, *, midpoint_s=0.0, runner=_run_bytes) -> str: + """ffmpeg-only single-color palette of a mid-segment frame -> #rrggbb.""" + args = [ + "ffmpeg", + "-v", + "quiet", + "-ss", + str(midpoint_s), + "-i", + str(path), + "-vf", + "thumbnail,palettegen=max_colors=1", + "-frames:v", + "1", + "-f", + "rawvideo", + "-pix_fmt", + "rgb24", + "-", + ] + return dominant_color_from_rgb(runner(args)) + + +def extract_frame(path, dest, *, midpoint_s=0.0, runner=None): + """Write one representative frame to dest (PNG) for the review preview.""" + runner = runner or (lambda a: subprocess.run(a, check=True)) + runner( + [ + "ffmpeg", + "-v", + "quiet", + "-y", + "-ss", + str(midpoint_s), + "-i", + str(path), + "-frames:v", + "1", + str(dest), + ] + ) + return dest