Download tests/test_audio.py from Frankenstein-Labs/Cortex-0: direct link, hf CLI and curl.
- Browser
- Download file 5.04 kB
-
https://huggingface.co/Frankenstein-Labs/Cortex-0/resolve/main/tests/test_audio.py
- Command line
-
hf download hf://Frankenstein-Labs/Cortex-0/tests/test_audio.py
-
curl -L -o test_audio.py https://huggingface.co/Frankenstein-Labs/Cortex-0/resolve/main/tests/test_audio.py
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 | |
| 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 | |