Spaces:
Sleeping
Sleeping
| """ | |
| LectureLens β Tests: Audio Analyzer | |
| Run with: pytest tests/test_audio.py -v | |
| """ | |
| from __future__ import annotations | |
| import struct | |
| import wave | |
| from pathlib import Path | |
| import numpy as np | |
| import pytest | |
| SAMPLE_FILES = Path(__file__).parent / "sample_files" | |
| # ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def make_wav(path: Path, duration: float = 3.0, sr: int = 44100, amplitude: float = 0.3) -> Path: | |
| """Generate a simple sine-wave WAV file for testing.""" | |
| t = np.linspace(0, duration, int(sr * duration), endpoint=False) | |
| samples = (np.sin(2 * np.pi * 440 * t) * amplitude * 32767).astype(np.int16) | |
| with wave.open(str(path), "w") as wf: | |
| wf.setnchannels(1) | |
| wf.setsampwidth(2) | |
| wf.setframerate(sr) | |
| wf.writeframes(samples.tobytes()) | |
| return path | |
| def make_clipped_wav(path: Path, duration: float = 2.0, sr: int = 44100) -> Path: | |
| """Generate a WAV that clips (amplitude > 1.0 before clamping).""" | |
| t = np.linspace(0, duration, int(sr * duration), endpoint=False) | |
| # amplitude = 1.5 β clips after int16 conversion | |
| raw = np.sin(2 * np.pi * 440 * t) * 1.5 | |
| samples = np.clip(raw * 32767, -32768, 32767).astype(np.int16) | |
| with wave.open(str(path), "w") as wf: | |
| wf.setnchannels(1) | |
| wf.setsampwidth(2) | |
| wf.setframerate(sr) | |
| wf.writeframes(samples.tobytes()) | |
| return path | |
| # ββ Setup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def clean_wav(tmp_path_factory) -> Path: | |
| p = tmp_path_factory.mktemp("audio") / "clean.wav" | |
| return make_wav(p, duration=5.0, amplitude=0.3) | |
| def clipped_wav(tmp_path_factory) -> Path: | |
| p = tmp_path_factory.mktemp("audio") / "clipped.wav" | |
| return make_clipped_wav(p) | |
| # ββ Clipping βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_no_clipping_in_clean_audio(clean_wav): | |
| from app.analyzers.audio_analyzer import count_clipped_samples | |
| clips = count_clipped_samples(clean_wav) | |
| assert clips == 0, f"Expected 0 clipped samples, got {clips}" | |
| def test_clipping_detected_in_hot_audio(clipped_wav): | |
| from app.analyzers.audio_analyzer import count_clipped_samples | |
| clips = count_clipped_samples(clipped_wav) | |
| assert clips > 0, "Expected clipped samples to be detected" | |
| # ββ SNR ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_snr_returns_positive_value(clean_wav): | |
| from app.analyzers.audio_analyzer import calculate_snr | |
| snr = calculate_snr(clean_wav) | |
| assert snr is not None | |
| assert snr >= 0, f"SNR should be non-negative, got {snr}" | |
| # ββ Silence detection ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_no_silence_in_continuous_audio(clean_wav): | |
| from app.analyzers.audio_analyzer import detect_silence | |
| segs = detect_silence(clean_wav, noise_db=-40, min_duration=1.0) | |
| # A sine wave has no silence | |
| assert isinstance(segs, list) | |
| def test_silence_detected_in_padded_audio(tmp_path): | |
| """WAV with 3 seconds of silence in the middle should trigger detection.""" | |
| import soundfile as sf | |
| sr = 16_000 | |
| tone = np.sin(2 * np.pi * 440 * np.linspace(0, 2, sr * 2)).astype(np.float32) * 0.4 | |
| silence = np.zeros(sr * 4, dtype=np.float32) | |
| audio = np.concatenate([tone, silence, tone]) | |
| path = tmp_path / "silence_test.wav" | |
| sf.write(str(path), audio, sr) | |
| from app.analyzers.audio_analyzer import detect_silence | |
| segs = detect_silence(path, noise_db=-40, min_duration=1.0) | |
| assert len(segs) >= 1, "Expected at least one silence segment" | |
| durations = [s.end - s.start for s in segs] | |
| assert max(durations) > 3.0, f"Expected silence > 3s, got max {max(durations):.1f}s" | |
| # ββ Alert engine (audio) βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_alert_generated_for_low_loudness(): | |
| from app.schemas import AudioMetrics | |
| from app.alert_engine import generate_alerts | |
| metrics = AudioMetrics(integrated_loudness_lufs=-28.0) | |
| alerts = generate_alerts(metrics, "audio", thresholds_path="thresholds.yaml") | |
| kpis = [a.kpi for a in alerts] | |
| assert "integrated_loudness_lufs" in kpis | |
| def test_no_alerts_for_good_audio(): | |
| from app.schemas import AudioMetrics | |
| from app.alert_engine import generate_alerts | |
| metrics = AudioMetrics( | |
| integrated_loudness_lufs=-14.0, | |
| true_peak_dbtp=-2.0, | |
| clipped_samples_count=0, | |
| snr_db=30.0, | |
| loudness_range_lu=8.0, | |
| dnsmos_ovrl=4.0, | |
| ) | |
| alerts = generate_alerts(metrics, "audio", thresholds_path="thresholds.yaml") | |
| assert len(alerts) == 0, f"Expected no alerts, got: {alerts}" | |
| def test_critical_alert_for_clipping(): | |
| from app.schemas import AudioMetrics | |
| from app.alert_engine import generate_alerts | |
| metrics = AudioMetrics(clipped_samples_count=500) | |
| alerts = generate_alerts(metrics, "audio", thresholds_path="thresholds.yaml") | |
| critical = [a for a in alerts if a.kpi == "clipped_samples_count"] | |
| assert len(critical) >= 1 | |
| # ββ Score ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_audio_score_range(): | |
| from app.schemas import AudioMetrics | |
| from app.alert_engine import compute_audio_score | |
| m = AudioMetrics( | |
| integrated_loudness_lufs=-14.0, | |
| true_peak_dbtp=-2.0, | |
| clipped_samples_count=0, | |
| snr_db=30.0, | |
| dnsmos_ovrl=4.2, | |
| ) | |
| score = compute_audio_score(m) | |
| assert 0.0 <= score <= 1.0, f"Score out of range: {score}" | |
| assert score > 0.7, f"Expected high score for good audio, got {score}" | |