""" Lab audio fixtures — the classifier must agree with what each one claims to be. These guard the *fixtures*, not the classifier. `tests/lab/sounds/generate.py` synthesises music/speech/silence that the Asterisk lab plays down a real call; if a fixture drifts into the wrong class, every lab result built on it is quietly meaningless — a hold-music scenario that never classifies as music proves nothing about the hold slayer. The first version of these fixtures passed on the opening 3s window and drifted to MUSIC after, which a single-window check would not have caught. Hence the sweep across every window. Skipped when the fixtures have not been generated: they are gitignored (~680K, reproducible from a fixed seed), so a fresh checkout has none until `python tests/lab/sounds/generate.py` runs. """ import subprocess import sys from pathlib import Path import numpy as np import pytest from config import Settings from models.call import AudioClassification from services.audio_classifier import SAMPLE_RATE, AudioClassifier SOUNDS_DIR = Path(__file__).parent / "lab" / "sounds" GENERATOR = SOUNDS_DIR / "generate.py" # The lab writes 8 kHz .sln; the classifier works at 16 kHz. LAB_RATE = 8000 WINDOW_SAMPLES = SAMPLE_RATE * 3 # classifier's 3s analysis window FIXTURES = [ ("lab-music.sln", AudioClassification.MUSIC), ("lab-speech.sln", AudioClassification.LIVE_HUMAN), ("lab-silence.sln", AudioClassification.SILENCE), ] def _load_16k(path: Path) -> np.ndarray: """Load an 8 kHz .sln and upsample to the classifier's 16 kHz.""" return np.repeat(np.fromfile(path, dtype=" dict[str, bytes]: subprocess.run( [sys.executable, str(GENERATOR), str(target)], check=True, capture_output=True, ) return {p.name: p.read_bytes() for p in sorted(target.glob("*.sln"))} first = run(tmp_path / "a") second = run(tmp_path / "b") assert first, "generator produced no .sln files" assert first.keys() == second.keys() for name in first: assert first[name] == second[name], f"{name} differs between runs"