""" 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}"