65 lines
2.2 KiB
Python
65 lines
2.2 KiB
Python
|
|
import json
|
||
|
|
import wave
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
|
||
|
|
from app.audio import build_audio_profile, classify_audio
|
||
|
|
from app.config import settings
|
||
|
|
from app.profile_store import Profile
|
||
|
|
|
||
|
|
|
||
|
|
def _write_tone(path: Path, frequency: float, duration: float = 1.2) -> None:
|
||
|
|
sample_rate = 16000
|
||
|
|
time = np.arange(int(sample_rate * duration)) / sample_rate
|
||
|
|
envelope = np.minimum(1.0, time * 8) * np.minimum(1.0, (duration - time) * 8)
|
||
|
|
samples = 0.7 * np.sin(2 * np.pi * frequency * time) * envelope
|
||
|
|
pcm = (samples * 32767).astype("<i2")
|
||
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
with wave.open(str(path), "wb") as output:
|
||
|
|
output.setnchannels(1)
|
||
|
|
output.setsampwidth(2)
|
||
|
|
output.setframerate(sample_rate)
|
||
|
|
output.writeframes(pcm.tobytes())
|
||
|
|
|
||
|
|
|
||
|
|
def test_audio_profile_classifies_and_rejects(tmp_path: Path) -> None:
|
||
|
|
profiles = tmp_path / "profiles"
|
||
|
|
artifacts = tmp_path / "artifacts"
|
||
|
|
settings.profiles_dir = profiles
|
||
|
|
settings.artifacts_dir = artifacts
|
||
|
|
directory = profiles / "test" / "tones" / "v1"
|
||
|
|
config = {
|
||
|
|
"code": "test.tones.v1",
|
||
|
|
"analysisType": "AUDIO_CLASSIFICATION",
|
||
|
|
"decision": {
|
||
|
|
"maxDistance": 0.45,
|
||
|
|
"minDistanceMargin": 0.02,
|
||
|
|
"unknownLabel": "UNKNOWN",
|
||
|
|
},
|
||
|
|
}
|
||
|
|
directory.mkdir(parents=True)
|
||
|
|
(directory / "profile.json").write_text(json.dumps(config), encoding="utf-8")
|
||
|
|
reference_tones = {
|
||
|
|
"POWER_ON": 440,
|
||
|
|
"POWER_OFF": 660,
|
||
|
|
"ARMED": 880,
|
||
|
|
"DISARMED": 1100,
|
||
|
|
}
|
||
|
|
for label, frequency in reference_tones.items():
|
||
|
|
_write_tone(directory / "references" / label / f"{label.lower()}.wav", frequency)
|
||
|
|
profile = Profile("test.tones.v1", "AUDIO_CLASSIFICATION", directory, config)
|
||
|
|
|
||
|
|
build_audio_profile(profile)
|
||
|
|
classified_paths = {}
|
||
|
|
for label, frequency in reference_tones.items():
|
||
|
|
path = tmp_path / f"{label.lower()}-test.wav"
|
||
|
|
_write_tone(path, frequency)
|
||
|
|
classified_paths[label] = path
|
||
|
|
other_path = tmp_path / "other-test.wav"
|
||
|
|
_write_tone(other_path, 1600)
|
||
|
|
|
||
|
|
for label, path in classified_paths.items():
|
||
|
|
assert classify_audio(profile, path)["label"] == label
|
||
|
|
assert classify_audio(profile, other_path)["label"] == "UNKNOWN"
|