- capture/: frame sampling at configurable fps, ffmpeg audio extraction (WAV, whisper-ready) - vision/: changed-pixel motion scoring, stable/moving segmentation, contour + perspective document detection, Laplacian sharpness scoring, optional CLAHE enhancement - pipeline: two-pass detect (motion timeline, then best-frame crop per stable window) writing crops, debug frames, report.json, and motion_scores.csv - CLI: probe / detect / extract-audio subcommands - config.yaml with tunable thresholds; placeholder packages for events, transcribe, ocr, naming, pdf, review_cli - synthetic sample video generator + 16 unit tests
48 lines
1.4 KiB
Python
48 lines
1.4 KiB
Python
import cv2
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from paperpod.capture.video import iter_frames, probe_video
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def tiny_video(tmp_path_factory):
|
|
"""3s, 24fps, 320x240 video with a moving square."""
|
|
path = tmp_path_factory.mktemp("video") / "tiny.mp4"
|
|
writer = cv2.VideoWriter(str(path), cv2.VideoWriter_fourcc(*"mp4v"), 24, (320, 240))
|
|
for i in range(72):
|
|
frame = np.full((240, 320, 3), 100, dtype=np.uint8)
|
|
x = 10 + i * 3
|
|
cv2.rectangle(frame, (x, 100), (x + 40, 140), (255, 255, 255), -1)
|
|
writer.write(frame)
|
|
writer.release()
|
|
return path
|
|
|
|
|
|
def test_probe(tiny_video):
|
|
meta = probe_video(tiny_video)
|
|
assert meta.fps == pytest.approx(24, abs=0.5)
|
|
assert meta.frame_count == 72
|
|
assert meta.width == 320 and meta.height == 240
|
|
assert meta.duration_s == pytest.approx(3.0, abs=0.1)
|
|
|
|
|
|
def test_iter_frames_sample_rate(tiny_video):
|
|
frames = list(iter_frames(tiny_video, sample_fps=8))
|
|
# 24fps sampled at 8fps -> every 3rd frame -> 24 frames
|
|
assert len(frames) == 24
|
|
ts = [f.t for f in frames]
|
|
assert ts == sorted(ts)
|
|
assert ts[0] == 0.0
|
|
|
|
|
|
def test_iter_frames_resize(tiny_video):
|
|
frame = next(iter_frames(tiny_video, sample_fps=8, resize_width=160))
|
|
assert frame.image.shape[1] == 160
|
|
assert frame.image.shape[0] == 120
|
|
|
|
|
|
def test_probe_missing_file():
|
|
with pytest.raises(FileNotFoundError):
|
|
probe_video("does_not_exist.mp4")
|