Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |
| 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}" | |