Cortex-0 / tests /test_audio.py
Frankenstein-Labs's picture
style(audio): nettoyage ruff
80cef99 verified
Raw History Blame Contribute Delete
5.04 kB
"""Phase 7 — tests de l'encodeur audio from scratch.
CPU, rapides : mel-spectrogramme maison, encodeur, shapes, et entraînement réel
court qui doit transcrire les voyelles synthétiques.
"""
from __future__ import annotations
import numpy as np
import pytest
import torch
from src.data.audio_features import (
LogMelSpectrogram,
hz_to_mel,
mel_filterbank,
mel_to_hz,
stft_magnitude,
)
from src.data.prepare_audio import prepare
from src.data.toy_audio import SR, generate_dataset, synth_vowel, to_arrays
from src.model.audio_encoder import AudioConfig, AudioEncoder
from src.tokenizer import load_tokenizer
@pytest.fixture(scope="module")
def tokenizer():
tok = load_tokenizer()
if tok.vocab_size != 32000:
pytest.skip("tokenizer final 32k requis")
return tok
# ---------------------------------------------------------------------------
# Mel-spectrogramme maison
# ---------------------------------------------------------------------------
def test_hz_mel_roundtrip():
f = torch.tensor([0.0, 440.0, 1000.0, 8000.0])
assert torch.allclose(mel_to_hz(hz_to_mel(f)).float(), f, atol=1e-3)
def test_mel_filterbank_shape_and_nonneg():
fb = mel_filterbank(16000, 256, 64)
assert fb.shape == (64, 129)
assert (fb >= 0).all()
assert fb.sum() > 0
def test_stft_pure_tone_peaks_at_frequency():
sr, f0 = 16000, 1000.0
t = torch.arange(4096) / sr
x = torch.sin(2 * np.pi * f0 * t).unsqueeze(0)
mag = stft_magnitude(x, n_fft=512, hop_length=128)
freqs = torch.linspace(0, sr / 2, mag.size(-1))
peak = freqs[mag[0].mean(0).argmax()]
assert abs(float(peak) - f0) < 60
def test_logmel_shape():
wav = torch.from_numpy(to_arrays(generate_dataset(4, seed=0)))
mel = LogMelSpectrogram(sr=SR, n_fft=256, hop_length=64, n_mels=64)(wav)
assert mel.shape[0] == 4 and mel.shape[1] == 1 and mel.shape[2] == 64
assert torch.isfinite(mel).all()
def test_synth_vowel_deterministic_and_bounded():
a = synth_vowel("a", "grave", rng=np.random.default_rng(0))
b = synth_vowel("a", "grave", rng=np.random.default_rng(0))
assert np.allclose(a, b)
assert np.abs(a).max() <= 1.0 + 1e-6
assert a.shape == (6144,)
def test_generate_dataset_classes():
items = generate_dataset(10, seed=0)
assert len(items) == 10
assert len({it.caption for it in items}) == 10
# ---------------------------------------------------------------------------
# Données préparées
# ---------------------------------------------------------------------------
def test_prepare_audio_shapes(tmp_path):
meta = prepare(tmp_path, n_train=20, n_val=10, n_mels=64, n_frames=96)
assert meta["n_classes"] == 10
from src.data.prepare_audio import load_mels
tr, caps = load_mels(tmp_path, "train")
assert tr.shape == (20, 64, 96)
assert len(caps) == 20
# ---------------------------------------------------------------------------
# Encodeur
# ---------------------------------------------------------------------------
def test_audio_encoder_shapes():
enc = AudioEncoder(AudioConfig(n_mels=64, n_frames=96, patch_mels=16,
patch_frames=16, n_layer=1, n_head=2, n_embd=64))
mel = torch.randn(2, 1, 64, 96)
tokens = enc(mel)
# grid 4 x 6 = 24 patchs, pool mean -> pas de CLS
assert enc.n_audio_tokens == 24
assert tokens.shape == (2, 24, 64)
assert enc.pool(tokens).shape == (2, 64)
def test_audio_encoder_is_bidirectional():
"""L'audio n'est pas causal : modifier une trame change les features des autres."""
torch.manual_seed(0)
enc = AudioEncoder(AudioConfig(n_mels=64, n_frames=96, patch_mels=16,
patch_frames=16, n_layer=1, n_head=2, n_embd=64)).eval()
mel = torch.randn(1, 1, 64, 96)
a = enc(mel)
mel2 = mel.clone()
mel2[0, :, :, :16] += 1.0 # modifie la 1re colonne de patchs (trames)
b = enc(mel2)
# Les tokens de patchs lointains (dernière colonne) changent aussi -> bidirectionnel.
assert not torch.allclose(a[:, -1], b[:, -1], atol=1e-4)
# ---------------------------------------------------------------------------
# Entraînement réel court
# ---------------------------------------------------------------------------
def test_audio_model_transcribes(tmp_path, tokenizer):
from src.train.train_audio import AudioTrainConfig, train
prepare(tmp_path / "data", n_train=800, n_val=200, n_mels=64, n_frames=96)
cfg = AudioTrainConfig(
data_dir=str(tmp_path / "data"), out_dir=str(tmp_path / "ckpt"),
epochs=400, batch_size=32, lr=3e-3, text_layers=2, text_embd=96,
text_heads=4, audio_layers=2, audio_embd=96, patch_mels=16,
patch_frames=16, device="cpu",
)
summary = train(cfg)
assert (tmp_path / "ckpt" / "best.pt").is_file()
# 10 classes -> hasard 0.10 ; la transcription doit nettement dépasser.
assert summary["greedy_acc"] > 0.5, summary["examples"]
assert summary["final_loss"] < 1.5