lecturelens-api / tests /test_video.py
aliSaac510's picture
refactor: simplify project by removing heavy PyTorch/MUSIQ dependency to fix HF build
6c2c36b
Raw
History Blame Contribute Delete
6.45 kB
"""
LectureLens β€” Tests: Video Analyzer
Run with: pytest tests/test_video.py -v
Requires ffprobe on PATH and opencv-python-headless installed.
"""
from __future__ import annotations
from pathlib import Path
import numpy as np
import pytest
# ── Helpers ────────────────────────────────────────────────────────────────────
def make_mp4(path: Path, duration: float = 3.0, width: int = 640, height: int = 480) -> Path:
"""
Generate a minimal MP4 using ffmpeg (solid colour, silent).
Requires ffmpeg on PATH.
"""
import subprocess
cmd = [
"ffmpeg", "-y",
"-f", "lavfi",
"-i", f"color=c=blue:size={width}x{height}:rate=25:duration={duration}",
"-f", "lavfi", "-i", "aevalsrc=0:c=mono:s=44100:d={}".format(duration),
"-c:v", "libx264", "-c:a", "aac",
"-shortest",
str(path),
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
pytest.skip(f"ffmpeg not available or failed: {result.stderr[:200]}")
return path
@pytest.fixture(scope="module")
def sample_mp4(tmp_path_factory) -> Path:
p = tmp_path_factory.mktemp("video") / "sample.mp4"
return make_mp4(p, duration=4.0)
# ── ffprobe parsing ────────────────────────────────────────────────────────────
def test_get_video_info_returns_dict(sample_mp4):
from app.analyzers.video_analyzer import get_video_info
info = get_video_info(sample_mp4)
assert "stream" in info
assert "format" in info
def test_parse_resolution(sample_mp4):
from app.analyzers.video_analyzer import get_video_info, parse_resolution
info = get_video_info(sample_mp4)
res = parse_resolution(info)
assert res == "640x480", f"Unexpected resolution: {res}"
def test_parse_fps(sample_mp4):
from app.analyzers.video_analyzer import get_video_info, parse_fps
info = get_video_info(sample_mp4)
fps = parse_fps(info)
assert fps is not None
assert 24 <= fps <= 31, f"Unexpected FPS: {fps}"
# ── Brightness / Sharpness ────────────────────────────────────────────────────
def test_brightness_solid_blue_frame(sample_mp4):
"""A solid blue frame has moderate brightness."""
from app.analyzers.video_analyzer import get_brightness_and_sharpness
brightness, sharpness = get_brightness_and_sharpness(sample_mp4, sample_rate_seconds=1)
assert brightness is not None
# Blue frame: grayscale ~29 (0.07*R + 0.72*G + 0.21*B, B=255)
assert 10 <= brightness <= 100, f"Unexpected brightness for solid blue: {brightness}"
def test_sharpness_is_low_for_solid_frame(sample_mp4):
"""A solid-colour frame has near-zero Laplacian variance (no edges)."""
from app.analyzers.video_analyzer import get_brightness_and_sharpness
_, sharpness = get_brightness_and_sharpness(sample_mp4, sample_rate_seconds=1)
assert sharpness is not None
assert sharpness < 50, f"Expected low sharpness for solid frame, got {sharpness}"
# ── Freeze / Black detection ──────────────────────────────────────────────────
def test_no_freeze_in_solid_video(sample_mp4):
"""A static solid-colour video is NOT flagged as frozen (below threshold duration)."""
from app.analyzers.video_analyzer import detect_frozen_frames
segs = detect_frozen_frames(sample_mp4)
# solid colour = technically frozen; check the structure at least
assert isinstance(segs, list)
def test_no_black_segments(sample_mp4):
from app.analyzers.video_analyzer import detect_black_frames
segs = detect_black_frames(sample_mp4)
assert isinstance(segs, list)
# Blue frame should not be flagged as black
assert len(segs) == 0, f"Unexpected black segments: {segs}"
# ── Alert engine (video) ───────────────────────────────────────────────────────
def test_alert_dark_video():
from app.schemas import VideoMetrics
from app.alert_engine import generate_alerts
metrics = VideoMetrics(avg_brightness=40.0)
alerts = generate_alerts(metrics, "video", thresholds_path="thresholds.yaml")
kpis = [a.kpi for a in alerts]
assert "avg_brightness" in kpis
def test_alert_blurry_video():
from app.schemas import VideoMetrics
from app.alert_engine import generate_alerts
metrics = VideoMetrics(avg_sharpness_laplacian=20.0)
alerts = generate_alerts(metrics, "video", thresholds_path="thresholds.yaml")
kpis = [a.kpi for a in alerts]
assert "avg_sharpness_laplacian" in kpis
def test_no_alerts_for_ideal_video():
from app.schemas import VideoMetrics
from app.alert_engine import generate_alerts
metrics = VideoMetrics(
avg_brightness=130.0,
avg_sharpness_laplacian=250.0,
dropped_frames_ratio=0.0,
compression_artifact_score=0.1,
)
alerts = generate_alerts(metrics, "video", thresholds_path="thresholds.yaml")
assert len(alerts) == 0, f"Expected no alerts, got: {alerts}"
# ── Score ──────────────────────────────────────────────────────────────────────
def test_video_score_range():
from app.schemas import VideoMetrics
from app.alert_engine import compute_video_score
m = VideoMetrics(
avg_brightness=130.0,
avg_sharpness_laplacian=250.0,
dropped_frames_ratio=0.0,
)
score = compute_video_score(m)
assert 0.0 <= score <= 1.0
assert score > 0.7
def test_video_score_low_for_bad_metrics():
from app.schemas import VideoMetrics
from app.alert_engine import compute_video_score
m = VideoMetrics(
avg_brightness=20.0,
avg_sharpness_laplacian=10.0,
dropped_frames_ratio=0.15,
)
score = compute_video_score(m)
assert score < 0.5, f"Expected low score for bad metrics, got {score}"