ChristophSchuhmann commited on
Commit
660ee8d
Β·
verified Β·
1 Parent(s): 4e21738

Add 10 speech artifact detector models with inference code and README

Browse files
README.md ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Speech Artifact Detectors
2
+
3
+ 10 lightweight CNN binary classifiers for detecting audio processing artifacts in speech. Each model takes 16 kHz mono audio and outputs an artifact score in [0, 1] (0 = clean, 1 = artifact detected).
4
+
5
+ ## Models
6
+
7
+ ### TTS/Vocoder Artifact Detectors
8
+
9
+ Trained on 16,000 samples: 8,000 clean (real podcast audio) + 8,000 artifact (6,000 BigVGAN-vocoded TTS predictions at varying noise levels + 2,000 comb-filtered augmentations). These detect broad vocoder synthesis artifacts including metallic resonance, harmonic distortion from Snake activations, and spectral smoothing.
10
+
11
+ | Model | Architecture | Parameters | Val Accuracy | What It Detects |
12
+ |-------|-------------|------------|-------------|-----------------|
13
+ | `stft_classifier` | MultiResSTFTClassifier | 1.69M | 99.88% | Vocoder synthesis fingerprint via multi-scale spectral analysis. The most sensitive detector β€” captures harmonic structure from Snake activations across 4 STFT resolutions (256/512/1024/2048 FFT). |
14
+ | `waveform_1d` | Waveform1DCNN | 1.90M | 97.44% | Waveform-level anomalies from vocoder synthesis. Operates directly on raw audio, detecting temporal artifacts that may not be visible in spectrograms. |
15
+ | `mel_classifier` | MelCNNClassifier | 1.01M | 99.75% | Mel-domain artifacts from vocoder reconstruction. Detects spectral smoothing and missing fine structure in the 80-band mel representation. |
16
+
17
+ ### Augmentation Artifact Detectors
18
+
19
+ Each trained on 6,000 samples (3,000 clean + 3,000 corrupted) from 48 kHz podcast audio downsampled to 16 kHz. Corruptions use variable magnitude to teach the classifier to detect artifacts at all intensities. All use the MultiResSTFTClassifier architecture (1.69M params).
20
+
21
+ | Model | Val Accuracy | What It Detects | Corruption Details |
22
+ |-------|-------------|-----------------|-------------------|
23
+ | `spectral_denoising` | 99.83% | Spectral gating denoiser artifacts β€” metallic, underwater quality from aggressive noise reduction | noisereduce with prop_decrease 0.3–1.0 |
24
+ | `pitch_correction` | 100% | Autotune stepping artifacts β€” unnatural pitch quantization where F0 snaps to the nearest semitone | pyworld F0 extraction + semitone snapping, strength 0.3–1.0 |
25
+ | `codec_compression` | 100% | MP3 compression artifacts β€” warbling, pre-echo, and bandwidth loss at low bitrates | ffmpeg MP3 encoding at 8–48 kbps round-trip |
26
+ | `comb_filtering` | 100% | Comb filter resonance peaks and notches β€” the characteristic "hollow" or "phaser" sound from delay + feedback mixing | Feedforward comb: delay 0.5–8 ms, wet 0.3–0.95 |
27
+ | `phase_vocoder` | 100% | Phase vocoder smearing β€” transient blurring and "phasiness" from STFT-based time-stretching | librosa time_stretch at rate 0.6–0.85 or 1.2–1.6 |
28
+ | `bandwidth_limitation` | 100% | Missing high-frequency content from lowpass filtering β€” "telephone" or "muffled" quality | Butterworth lowpass, cutoff 2–6 kHz |
29
+ | `clipping_distortion` | 98.33% | Hard/soft clipping and harmonic distortion from overdrive β€” "crunchy" or "fuzzy" audio | pedalboard Distortion with drive_db 3–25 |
30
+
31
+ ## Quick Start
32
+
33
+ ### From HuggingFace Hub
34
+
35
+ ```python
36
+ from speech_artifact_detector import load_from_hub, score_file_all
37
+
38
+ # Load all 10 models
39
+ models = load_from_hub(device="cuda")
40
+
41
+ # Score an audio file
42
+ scores = score_file_all(models, "audio.wav")
43
+ for name, score in scores.items():
44
+ print(f" {name}: {score:.4f}")
45
+ ```
46
+
47
+ ### From Local Directory
48
+
49
+ ```python
50
+ from speech_artifact_detector import load_all_models, score_file_all
51
+
52
+ # Load from local checkpoints
53
+ models = load_all_models("./", device="cuda")
54
+
55
+ # Score a file
56
+ scores = score_file_all(models, "audio.wav")
57
+ ```
58
+
59
+ ### Load Specific Categories
60
+
61
+ ```python
62
+ from speech_artifact_detector import load_from_hub
63
+
64
+ # Only TTS/vocoder detectors (3 models)
65
+ tts_models = load_from_hub(categories=["tts_artifact"])
66
+
67
+ # Only augmentation detectors (7 models)
68
+ aug_models = load_from_hub(categories=["augmentation"])
69
+ ```
70
+
71
+ ### Batch Scoring
72
+
73
+ ```python
74
+ from speech_artifact_detector import load_from_hub, score_batch
75
+
76
+ models = load_from_hub(device="cuda")
77
+ results = score_batch(models, ["file1.wav", "file2.wav", "file3.wav"], batch_size=16)
78
+ for path, scores in zip(paths, results):
79
+ print(f"{path}: mean={sum(scores.values())/len(scores):.3f}")
80
+ ```
81
+
82
+ ### CLI
83
+
84
+ ```bash
85
+ # Score with all models from Hub
86
+ python speech_artifact_detector.py --hub --input audio.wav
87
+
88
+ # Score a directory
89
+ python speech_artifact_detector.py --hub --input /path/to/wavs/ --ext mp3
90
+
91
+ # Only augmentation detectors
92
+ python speech_artifact_detector.py --hub --category augmentation --input audio.wav
93
+
94
+ # Single model from local checkpoint
95
+ python speech_artifact_detector.py --checkpoint codec_compression.pt --input audio.wav
96
+ ```
97
+
98
+ ## Architecture Details
99
+
100
+ ### MultiResSTFTClassifier (1,688,833 params)
101
+
102
+ Used by `stft_classifier` and all 7 augmentation detectors. Computes STFT at 4 resolutions in parallel, each processed by a 5-layer 2D CNN:
103
+
104
+ ```
105
+ Input: [B, 1, T] at 16 kHz (max 10 seconds)
106
+ β”œβ”€ STFT n_fft=256, hop=64 β†’ CNN (32β†’64β†’128β†’128β†’128) β†’ pool β†’ [B, 128]
107
+ β”œβ”€ STFT n_fft=512, hop=128 β†’ CNN (32β†’64β†’128β†’128β†’128) β†’ pool β†’ [B, 128]
108
+ β”œβ”€ STFT n_fft=1024, hop=256 β†’ CNN (32β†’64β†’128β†’128β†’128) β†’ pool β†’ [B, 128]
109
+ └─ STFT n_fft=2048, hop=512 β†’ CNN (32β†’64β†’128β†’128β†’128) β†’ pool β†’ [B, 128]
110
+ concat β†’ [B, 512]
111
+ Linear(512, 256) + GELU + Dropout(0.2)
112
+ Linear(256, 1) β†’ sigmoid β†’ score
113
+ ```
114
+
115
+ Each CNN block uses BatchNorm2d + GELU activations. The multi-resolution design captures artifacts at different time-frequency tradeoffs: the 256-FFT branch has 4ms resolution for transient artifacts, while the 2048-FFT branch has 0.5 Hz frequency resolution for tonal artifacts.
116
+
117
+ ### Waveform1DCNN (1,898,753 params)
118
+
119
+ Used by `waveform_1d`. Six 1D convolutional blocks operating directly on the raw waveform:
120
+
121
+ ```
122
+ Input: [B, 1, T] at 16 kHz
123
+ Conv1d(1β†’64, k=15, s=4) + BN + GELU + MaxPool(2)
124
+ Conv1d(64β†’128, k=11, s=2) + BN + GELU + MaxPool(2)
125
+ Conv1d(128β†’256, k=7, s=2) + BN + GELU + MaxPool(2)
126
+ Conv1d(256β†’256, k=5, s=2) + BN + GELU + MaxPool(2)
127
+ Conv1d(256β†’512, k=3, s=2) + BN + GELU + MaxPool(2)
128
+ Conv1d(512β†’512, k=3, s=1) + BN + GELU
129
+ AdaptiveAvgPool1d(1) β†’ Linear(512, 128) + GELU + Dropout(0.2) β†’ Linear(128, 1) β†’ sigmoid
130
+ ```
131
+
132
+ ### MelCNNClassifier (1,012,417 params)
133
+
134
+ Used by `mel_classifier`. Computes 80-band mel spectrogram then 5-layer 2D CNN:
135
+
136
+ ```
137
+ Input: [B, 1, T] at 16 kHz
138
+ MelSpectrogram(sr=16000, n_fft=1024, hop=256, n_mels=80) β†’ log
139
+ Conv2d(1β†’32, 3Γ—3) + BN + GELU + MaxPool(2)
140
+ Conv2d(32β†’64, 3Γ—3) + BN + GELU + MaxPool(2)
141
+ Conv2d(64β†’128, 3Γ—3) + BN + GELU + MaxPool(2)
142
+ Conv2d(128β†’256, 3Γ—3) + BN + GELU + MaxPool(2)
143
+ Conv2d(256β†’256, 3Γ—3) + BN + GELU
144
+ AdaptiveAvgPool2d(1) β†’ Linear(256, 128) + GELU + Dropout(0.2) β†’ Linear(128, 1) β†’ sigmoid
145
+ ```
146
+
147
+ ## Training Details
148
+
149
+ ### TTS/Vocoder Artifact Detectors
150
+
151
+ **Training data**: 16,000 samples per epoch drawn from:
152
+ - **Clean class (8,000)**: 2,000 real podcast audio clips (48 kHz, downsampled to 16 kHz) with 4x oversampling
153
+ - **Artifact class (8,000)**: 6,000 TTS-predicted audio (teacher-forced through DramaBox DiT at Οƒ=0.15/0.275/0.4, decoded via BigVGAN vocoder) + 2,000 on-the-fly comb-filter augmented clips
154
+
155
+ **Training setup**: 30 epochs, AdamW with lr=3e-4 and weight_decay=1e-4, BCEWithLogitsLoss, 10-second random crops at 16 kHz.
156
+
157
+ **Key finding**: The `stft_classifier` is the most sensitive β€” it detects the fundamental spectral fingerprint of BigVGAN-style vocoder synthesis (characteristic harmonics from Snake activations), not just specific comb-filter artifacts. It scores ~1.0 for ALL vocoder-synthesized audio including vanilla DramaBox with no fine-tuning. The `mel_classifier` and `waveform_1d` are more specific to degradation from LoRA fine-tuning and respond to vocoder LoRA adaptation.
158
+
159
+ **Source repo for original 3 models**: [`laion/tts-comb-artefact-detectors`](https://huggingface.co/laion/tts-comb-artefact-detectors)
160
+
161
+ ### Augmentation Artifact Detectors
162
+
163
+ **Training data**: 6,000 samples per augmentation (3,000 clean + 3,000 corrupted). Source audio: random podcast recordings from a 48 kHz speech corpus, downsampled to 16 kHz. Each augmentation uses variable magnitude (uniformly sampled across its range) so the classifier learns the artifact signature at all intensities rather than only detecting extreme cases.
164
+
165
+ **Training setup**: 30 epochs max with patience=10 early stopping, batch size 32, AdamW with lr=3e-4 and weight_decay=1e-4, gradient clipping at 1.0, 90/10 train/val split.
166
+
167
+ **Training data repo**: [`TTS-AGI/augmentation-artifact-detector-data`](https://huggingface.co/datasets/TTS-AGI/augmentation-artifact-detector-data)
168
+
169
+ **Convergence**: Most detectors converge very early (epoch 1-4) with near-perfect accuracy. `comb_filtering` took longest (epoch 11) and `clipping_distortion` was the hardest task (98.33% at epoch 28), likely because soft clipping at low drive levels produces subtle harmonic distortion that overlaps with natural speech harmonics.
170
+
171
+ ## Checkpoint Format
172
+
173
+ Each `.pt` file is a PyTorch checkpoint dict with these keys:
174
+
175
+ ```python
176
+ {
177
+ "model_state_dict": ..., # Model weights
178
+ "architecture": str, # "stft_classifier", "waveform_1d", "mel_classifier",
179
+ # or "MultiResSTFTClassifier" (for augmentation detectors)
180
+ "epoch": int, # Best epoch number
181
+ "val_acc": float, # Validation accuracy (0-1)
182
+ "val_f1": float, # Validation F1 score
183
+ "val_prec": float, # Validation precision
184
+ "val_rec": float, # Validation recall
185
+ "n_params": int, # Number of trainable parameters
186
+ "input_sr": int, # Expected input sample rate (16000)
187
+ # Augmentation detectors also have:
188
+ "augmentation": str, # Augmentation type name
189
+ "config": dict, # Augmentation configuration
190
+ }
191
+ ```
192
+
193
+ ## Use Cases
194
+
195
+ 1. **TTS quality filtering**: Score generated speech before release. High scores on TTS detectors indicate vocoder artifacts.
196
+ 2. **Audio processing pipeline QA**: Run all 7 augmentation detectors to identify which processing step introduced artifacts.
197
+ 3. **Dataset cleaning**: Filter training data to remove samples with processing artifacts that could degrade downstream model quality.
198
+ 4. **Codec quality assessment**: Use `codec_compression` to detect over-compressed audio in datasets.
199
+ 5. **Voice cloning detection**: The TTS detectors can distinguish real from synthesized speech (though this is not their primary purpose).
200
+
201
+ ## Requirements
202
+
203
+ ```
204
+ torch>=2.0
205
+ torchaudio>=2.0
206
+ soundfile
207
+ numpy
208
+ huggingface_hub # only for load_from_hub()
209
+ ```
210
+
211
+ ## License
212
+
213
+ Apache 2.0
214
+
215
+ ## Citation
216
+
217
+ ```bibtex
218
+ @misc{laion-speech-artifact-detectors,
219
+ title={Speech Artifact Detectors: 10 CNN Classifiers for Audio Processing Artifacts},
220
+ author={LAION},
221
+ year={2025},
222
+ url={https://huggingface.co/laion/speech-artifact-detectors}
223
+ }
224
+ ```
bandwidth_limitation.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:48b1397959442fb2b8238bd321987e843597d67e586202cfae9464482001bb20
3
+ size 6810902
clipping_distortion.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d500514f6d6bdc41e21e19440d5554e7c9ebc537de383cdec975fed067ee8f84
3
+ size 6810838
codec_compression.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:699ddfa1ed9db7ec37adf3078ef81d1c0ac7c13c860a3689209f24fecc07e98d
3
+ size 6810902
comb_filtering.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6431662a1682e564232c257b55e3853a6a0771ef92edc2b6747aafdaea28d3a8
3
+ size 6810838
mel_classifier.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:651f363f46f86fd18b02ceaacd66c15941f7b0edde556612fa9f4b7fa38443b4
3
+ size 4238748
phase_vocoder.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:090f2c59d169332d60c2750a1c8fa5a487eee86750b153ad3b534f538895701e
3
+ size 6810838
pitch_correction.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b94afb093e5ecc67f9da8a10edb6f5befaa5bc7e5504337d07630e416ca408d8
3
+ size 6810838
spectral_denoising.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:624226b7769b0a592d44311d4822e645a7a442388841d63c3bf883bac63403ea
3
+ size 6810902
speech_artifact_detector.py ADDED
@@ -0,0 +1,696 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Speech Artifact Detectors β€” 10 CNN classifiers for detecting audio processing artifacts.
2
+
3
+ Three architecture families, 10 specialized models:
4
+
5
+ **Comb/TTS Artifact Detectors** (trained on real vs TTS-predicted audio + comb-filtered augmentations):
6
+ - stft_classifier: Multi-resolution STFT CNN (~1.69M params, 99.88% val acc)
7
+ - waveform_1d: Raw waveform 1D CNN (~1.90M params, 97.44% val acc)
8
+ - mel_classifier: Mel spectrogram 2D CNN (~1.01M params, 99.75% val acc)
9
+
10
+ **Augmentation Artifact Detectors** (each trained on clean vs specifically-corrupted audio):
11
+ - spectral_denoising: Detects spectral gating denoiser artifacts (99.83% val acc)
12
+ - pitch_correction: Detects autotune/pitch correction artifacts (100% val acc)
13
+ - codec_compression: Detects MP3 codec compression artifacts (100% val acc)
14
+ - comb_filtering: Detects comb filter resonance/notches (100% val acc)
15
+ - phase_vocoder: Detects phase vocoder time-stretch smearing (100% val acc)
16
+ - bandwidth_limitation: Detects missing high frequencies from lowpass filtering (100% val acc)
17
+ - clipping_distortion: Detects hard/soft clipping and harmonic distortion (98.33% val acc)
18
+
19
+ All models operate on 16 kHz mono waveforms and output an artifact score in [0, 1]:
20
+ 0 = clean, 1 = artifact detected.
21
+
22
+ Usage:
23
+ from speech_artifact_detector import load_model, load_all_models, score_file
24
+
25
+ # Single model
26
+ model = load_model("stft_classifier.pt")
27
+ score = score_file(model, "audio.wav")
28
+ print(f"Artifact score: {score:.4f}")
29
+
30
+ # All models
31
+ models = load_all_models(".")
32
+ scores = score_file_all(models, "audio.wav")
33
+ for name, s in scores.items():
34
+ print(f" {name}: {s:.4f}")
35
+
36
+ # Batch scoring
37
+ scores = score_batch(models, ["audio1.wav", "audio2.wav", "audio3.wav"])
38
+ """
39
+
40
+ import os
41
+ from typing import Dict, List, Optional, Union
42
+
43
+ import numpy as np
44
+ import soundfile as sf
45
+ import torch
46
+ import torch.nn as nn
47
+ import torchaudio
48
+
49
+ TARGET_SR = 16000
50
+ MAX_SAMPLES = 10 * TARGET_SR # 10 seconds
51
+
52
+
53
+ # ═══════════════════════════════════════════════════════════════════════════════
54
+ # Architecture 1: Multi-Resolution STFT Classifier (~1.69M params)
55
+ # Used by: stft_classifier, all 7 augmentation detectors
56
+ # ═══════════════════════════════════════════════════════════════════════════════
57
+
58
+ class STFTResolutionBlock(nn.Module):
59
+ """Single-resolution STFT -> 2D CNN -> pooled features."""
60
+
61
+ def __init__(self, n_fft, hop_length, out_dim=128):
62
+ super().__init__()
63
+ self.n_fft = n_fft
64
+ self.hop_length = hop_length
65
+ self.convs = nn.Sequential(
66
+ nn.Conv2d(1, 32, (5, 5), (2, 2), (2, 2)),
67
+ nn.BatchNorm2d(32), nn.GELU(),
68
+ nn.Conv2d(32, 64, (3, 3), (2, 2), (1, 1)),
69
+ nn.BatchNorm2d(64), nn.GELU(),
70
+ nn.Conv2d(64, 128, (3, 3), (2, 2), (1, 1)),
71
+ nn.BatchNorm2d(128), nn.GELU(),
72
+ nn.Conv2d(128, out_dim, (3, 3), (2, 2), (1, 1)),
73
+ nn.BatchNorm2d(out_dim), nn.GELU(),
74
+ nn.Conv2d(out_dim, out_dim, (3, 3), (1, 1), (1, 1)),
75
+ nn.BatchNorm2d(out_dim), nn.GELU(),
76
+ )
77
+ self.pool = nn.AdaptiveAvgPool2d(1)
78
+
79
+ def forward(self, waveform):
80
+ # waveform: [B, T]
81
+ window = torch.hann_window(self.n_fft, device=waveform.device, dtype=waveform.dtype)
82
+ stft = torch.stft(
83
+ waveform, self.n_fft, self.hop_length,
84
+ window=window, return_complex=True,
85
+ )
86
+ mag = torch.log(stft.abs().clamp(min=1e-7)) # [B, F, T']
87
+ mag = mag.unsqueeze(1) # [B, 1, F, T']
88
+ h = self.convs(mag)
89
+ h = self.pool(h).flatten(1) # [B, out_dim]
90
+ return h
91
+
92
+
93
+ class MultiResSTFTClassifier(nn.Module):
94
+ """Multi-resolution STFT classifier (1,688,833 parameters).
95
+
96
+ Computes STFT at 4 resolutions (256/512/1024/2048 FFT), runs each
97
+ through a 5-layer 2D CNN, concatenates the 128-dim features from
98
+ each resolution, and classifies through an MLP head.
99
+ """
100
+
101
+ def __init__(self):
102
+ super().__init__()
103
+ self.blocks = nn.ModuleList([
104
+ STFTResolutionBlock(n_fft=256, hop_length=64),
105
+ STFTResolutionBlock(n_fft=512, hop_length=128),
106
+ STFTResolutionBlock(n_fft=1024, hop_length=256),
107
+ STFTResolutionBlock(n_fft=2048, hop_length=512),
108
+ ])
109
+ self.head = nn.Sequential(
110
+ nn.Linear(512, 256),
111
+ nn.GELU(),
112
+ nn.Dropout(0.2),
113
+ nn.Linear(256, 1),
114
+ )
115
+
116
+ def forward(self, waveform):
117
+ """waveform: [B, 1, T] -> logits [B, 1]"""
118
+ x = waveform.squeeze(1) # [B, T]
119
+ feats = [block(x) for block in self.blocks]
120
+ h = torch.cat(feats, dim=1) # [B, 512]
121
+ return self.head(h)
122
+
123
+ def artifact_score(self, waveform):
124
+ """Returns artifact probability in [0, 1]."""
125
+ return torch.sigmoid(self.forward(waveform))
126
+
127
+
128
+ # ═══════════════════════════════════════════════════════════════════════════════
129
+ # Architecture 2: Waveform 1D CNN (~1.90M params)
130
+ # Used by: waveform_1d
131
+ # ═══════════════════════════════════════════════════════════════════════════════
132
+
133
+ class Waveform1DCNN(nn.Module):
134
+ """Direct 1D convolution classifier on raw waveform (1,898,753 parameters).
135
+
136
+ 6 conv blocks with increasing channels [1->64->128->256->256->512->512],
137
+ BatchNorm, GELU, and MaxPool. Global average pooling then MLP head.
138
+ """
139
+
140
+ def __init__(self):
141
+ super().__init__()
142
+ channels = [1, 64, 128, 256, 256, 512, 512]
143
+ kernels = [15, 11, 7, 5, 3, 3]
144
+ strides = [4, 2, 2, 2, 2, 1]
145
+
146
+ blocks = []
147
+ for i in range(6):
148
+ blocks.extend([
149
+ nn.Conv1d(channels[i], channels[i + 1], kernels[i],
150
+ stride=strides[i], padding=kernels[i] // 2),
151
+ nn.BatchNorm1d(channels[i + 1]),
152
+ nn.GELU(),
153
+ ])
154
+ if strides[i] > 1:
155
+ blocks.append(nn.MaxPool1d(2))
156
+ self.features = nn.Sequential(*blocks)
157
+ self.head = nn.Sequential(
158
+ nn.AdaptiveAvgPool1d(1),
159
+ nn.Flatten(),
160
+ nn.Linear(512, 128),
161
+ nn.GELU(),
162
+ nn.Dropout(0.2),
163
+ nn.Linear(128, 1),
164
+ )
165
+
166
+ def forward(self, waveform):
167
+ """waveform: [B, 1, T] -> logits [B, 1]"""
168
+ h = self.features(waveform)
169
+ return self.head(h)
170
+
171
+ def artifact_score(self, waveform):
172
+ """Returns artifact probability in [0, 1]."""
173
+ return torch.sigmoid(self.forward(waveform))
174
+
175
+
176
+ # ═══════════════════════════════════════════════════════════════════════════════
177
+ # Architecture 3: Mel Spectrogram CNN (~1.01M params)
178
+ # Used by: mel_classifier
179
+ # ═══════════════════════════════════════════════════════════════════════════════
180
+
181
+ class MelCNNClassifier(nn.Module):
182
+ """Mel spectrogram -> 2D CNN classifier (1,012,417 parameters).
183
+
184
+ Computes 80-band mel spectrogram from raw waveform (differentiable),
185
+ then runs through 5-layer 2D CNN with BatchNorm and GELU.
186
+ """
187
+
188
+ def __init__(self, sr=TARGET_SR, n_fft=1024, hop_length=256, n_mels=80):
189
+ super().__init__()
190
+ self.mel_spec = torchaudio.transforms.MelSpectrogram(
191
+ sample_rate=sr, n_fft=n_fft, hop_length=hop_length,
192
+ n_mels=n_mels, f_min=0, f_max=sr // 2, power=2.0,
193
+ )
194
+ self.convs = nn.Sequential(
195
+ nn.Conv2d(1, 32, 3, padding=1),
196
+ nn.BatchNorm2d(32), nn.GELU(), nn.MaxPool2d(2),
197
+ nn.Conv2d(32, 64, 3, padding=1),
198
+ nn.BatchNorm2d(64), nn.GELU(), nn.MaxPool2d(2),
199
+ nn.Conv2d(64, 128, 3, padding=1),
200
+ nn.BatchNorm2d(128), nn.GELU(), nn.MaxPool2d(2),
201
+ nn.Conv2d(128, 256, 3, padding=1),
202
+ nn.BatchNorm2d(256), nn.GELU(), nn.MaxPool2d(2),
203
+ nn.Conv2d(256, 256, 3, padding=1),
204
+ nn.BatchNorm2d(256), nn.GELU(),
205
+ )
206
+ self.head = nn.Sequential(
207
+ nn.AdaptiveAvgPool2d(1),
208
+ nn.Flatten(),
209
+ nn.Linear(256, 128),
210
+ nn.GELU(),
211
+ nn.Dropout(0.2),
212
+ nn.Linear(128, 1),
213
+ )
214
+
215
+ def forward(self, waveform):
216
+ """waveform: [B, 1, T] -> logits [B, 1]"""
217
+ x = waveform.squeeze(1) # [B, T]
218
+ mel = self.mel_spec(x) # [B, n_mels, T']
219
+ mel = torch.log(mel.clamp(min=1e-7))
220
+ mel = mel.unsqueeze(1) # [B, 1, n_mels, T']
221
+ h = self.convs(mel)
222
+ return self.head(h)
223
+
224
+ def artifact_score(self, waveform):
225
+ """Returns artifact probability in [0, 1]."""
226
+ return torch.sigmoid(self.forward(waveform))
227
+
228
+
229
+ # ═══════════════════════════════════════════════════════════════════════════════
230
+ # Model registry and metadata
231
+ # ═════════════════════════════════════════════════════════════════���═════════════
232
+
233
+ # Maps architecture name -> class
234
+ ARCHITECTURE_REGISTRY = {
235
+ "MultiResSTFTClassifier": MultiResSTFTClassifier,
236
+ "stft_classifier": MultiResSTFTClassifier,
237
+ "waveform_1d": Waveform1DCNN,
238
+ "mel_classifier": MelCNNClassifier,
239
+ }
240
+
241
+ # All 10 models with their checkpoint filenames and descriptions
242
+ MODEL_CATALOG = {
243
+ # --- Comb/TTS artifact detectors (3) ---
244
+ "stft_classifier": {
245
+ "file": "stft_classifier.pt",
246
+ "architecture": "stft_classifier",
247
+ "category": "tts_artifact",
248
+ "description": "Multi-resolution STFT classifier for TTS vocoder artifacts",
249
+ "detects": "Vocoder synthesis artifacts, comb filtering, metallic resonance",
250
+ "params": "1.69M",
251
+ "val_acc": 99.88,
252
+ },
253
+ "waveform_1d": {
254
+ "file": "waveform_1d.pt",
255
+ "architecture": "waveform_1d",
256
+ "category": "tts_artifact",
257
+ "description": "Raw waveform 1D CNN for TTS vocoder artifacts",
258
+ "detects": "Vocoder synthesis artifacts, waveform-level anomalies",
259
+ "params": "1.90M",
260
+ "val_acc": 97.44,
261
+ },
262
+ "mel_classifier": {
263
+ "file": "mel_classifier.pt",
264
+ "architecture": "mel_classifier",
265
+ "category": "tts_artifact",
266
+ "description": "Mel spectrogram 2D CNN for TTS vocoder artifacts",
267
+ "detects": "Vocoder mel-domain artifacts, spectral smoothing",
268
+ "params": "1.01M",
269
+ "val_acc": 99.75,
270
+ },
271
+ # --- Augmentation artifact detectors (7) ---
272
+ "spectral_denoising": {
273
+ "file": "spectral_denoising.pt",
274
+ "architecture": "MultiResSTFTClassifier",
275
+ "category": "augmentation",
276
+ "description": "Spectral gating denoiser artifact detector",
277
+ "detects": "Artifacts from spectral gating noise reduction (metallic, underwater sound)",
278
+ "params": "1.69M",
279
+ "val_acc": 99.83,
280
+ },
281
+ "pitch_correction": {
282
+ "file": "pitch_correction.pt",
283
+ "architecture": "MultiResSTFTClassifier",
284
+ "category": "augmentation",
285
+ "description": "Pitch correction / autotune artifact detector",
286
+ "detects": "Autotune stepping artifacts, unnatural pitch quantization to semitone grid",
287
+ "params": "1.69M",
288
+ "val_acc": 100.0,
289
+ },
290
+ "codec_compression": {
291
+ "file": "codec_compression.pt",
292
+ "architecture": "MultiResSTFTClassifier",
293
+ "category": "augmentation",
294
+ "description": "Codec compression artifact detector",
295
+ "detects": "MP3 compression artifacts at low bitrates (8-48 kbps): warbling, pre-echo, bandwidth loss",
296
+ "params": "1.69M",
297
+ "val_acc": 100.0,
298
+ },
299
+ "comb_filtering": {
300
+ "file": "comb_filtering.pt",
301
+ "architecture": "MultiResSTFTClassifier",
302
+ "category": "augmentation",
303
+ "description": "Comb filter artifact detector",
304
+ "detects": "Comb filter resonance peaks and notches from delay+feedback mixing",
305
+ "params": "1.69M",
306
+ "val_acc": 100.0,
307
+ },
308
+ "phase_vocoder": {
309
+ "file": "phase_vocoder.pt",
310
+ "architecture": "MultiResSTFTClassifier",
311
+ "category": "augmentation",
312
+ "description": "Phase vocoder time-stretch artifact detector",
313
+ "detects": "Phase smearing and transient blurring from STFT-based time-stretching",
314
+ "params": "1.69M",
315
+ "val_acc": 100.0,
316
+ },
317
+ "bandwidth_limitation": {
318
+ "file": "bandwidth_limitation.pt",
319
+ "architecture": "MultiResSTFTClassifier",
320
+ "category": "augmentation",
321
+ "description": "Bandwidth limitation artifact detector",
322
+ "detects": "Missing high-frequency content from lowpass filtering (cutoff 2-6 kHz)",
323
+ "params": "1.69M",
324
+ "val_acc": 100.0,
325
+ },
326
+ "clipping_distortion": {
327
+ "file": "clipping_distortion.pt",
328
+ "architecture": "MultiResSTFTClassifier",
329
+ "category": "augmentation",
330
+ "description": "Clipping/distortion artifact detector",
331
+ "detects": "Hard/soft clipping and harmonic distortion from overdrive",
332
+ "params": "1.69M",
333
+ "val_acc": 98.33,
334
+ },
335
+ }
336
+
337
+
338
+ # ═══════════════════════════════════════════════════════════════════════════════
339
+ # Loading utilities
340
+ # ═══════════════════════════════════════════════════════════════════════════════
341
+
342
+ def load_model(
343
+ checkpoint_path: str,
344
+ device: Optional[str] = None,
345
+ ) -> nn.Module:
346
+ """Load a single artifact detector from a checkpoint file.
347
+
348
+ Args:
349
+ checkpoint_path: Path to .pt checkpoint file.
350
+ device: "cuda", "cpu", or None for auto-detect.
351
+
352
+ Returns:
353
+ Model in eval mode on the specified device.
354
+ """
355
+ if device is None:
356
+ device = "cuda" if torch.cuda.is_available() else "cpu"
357
+ device = torch.device(device)
358
+
359
+ ckpt = torch.load(checkpoint_path, map_location=device, weights_only=True)
360
+ arch = ckpt["architecture"]
361
+ cls = ARCHITECTURE_REGISTRY[arch]
362
+ model = cls()
363
+ model.load_state_dict(ckpt["model_state_dict"])
364
+ model.to(device).eval()
365
+ return model
366
+
367
+
368
+ def load_all_models(
369
+ model_dir: str,
370
+ device: Optional[str] = None,
371
+ categories: Optional[List[str]] = None,
372
+ ) -> Dict[str, nn.Module]:
373
+ """Load all available artifact detectors from a directory.
374
+
375
+ Args:
376
+ model_dir: Directory containing .pt checkpoint files.
377
+ device: "cuda", "cpu", or None for auto-detect.
378
+ categories: Optional filter. List of categories to load:
379
+ "tts_artifact", "augmentation", or both. None = load all.
380
+
381
+ Returns:
382
+ Dict mapping model name to loaded model.
383
+ """
384
+ if device is None:
385
+ device = "cuda" if torch.cuda.is_available() else "cpu"
386
+
387
+ models = {}
388
+ for name, info in MODEL_CATALOG.items():
389
+ if categories and info["category"] not in categories:
390
+ continue
391
+ path = os.path.join(model_dir, info["file"])
392
+ if os.path.exists(path):
393
+ models[name] = load_model(path, device)
394
+ return models
395
+
396
+
397
+ def load_from_hub(
398
+ repo_id: str = "laion/speech-artifact-detectors",
399
+ device: Optional[str] = None,
400
+ categories: Optional[List[str]] = None,
401
+ ) -> Dict[str, nn.Module]:
402
+ """Load all models directly from HuggingFace Hub.
403
+
404
+ Args:
405
+ repo_id: HuggingFace model repository ID.
406
+ device: "cuda", "cpu", or None for auto-detect.
407
+ categories: Optional filter. List of categories to load.
408
+
409
+ Returns:
410
+ Dict mapping model name to loaded model.
411
+ """
412
+ from huggingface_hub import hf_hub_download
413
+
414
+ if device is None:
415
+ device = "cuda" if torch.cuda.is_available() else "cpu"
416
+
417
+ models = {}
418
+ for name, info in MODEL_CATALOG.items():
419
+ if categories and info["category"] not in categories:
420
+ continue
421
+ try:
422
+ path = hf_hub_download(repo_id, info["file"])
423
+ models[name] = load_model(path, device)
424
+ except Exception as e:
425
+ print(f"Warning: Could not load {name}: {e}")
426
+ return models
427
+
428
+
429
+ # ═══════════════════════════════════════════════════════════════════════════════
430
+ # Audio loading
431
+ # ═══════════════════════════════════════════════════════════════════════════════
432
+
433
+ def load_audio(path: str, target_sr: int = TARGET_SR) -> torch.Tensor:
434
+ """Load an audio file and convert to 16 kHz mono float32 tensor.
435
+
436
+ Returns:
437
+ Tensor of shape [T] (1-D).
438
+ """
439
+ audio_np, sr = sf.read(path, dtype="float32")
440
+ if audio_np.ndim > 1:
441
+ audio_np = audio_np[:, 0] # take first channel
442
+ wav = torch.from_numpy(audio_np).float()
443
+ if sr != target_sr:
444
+ wav = torchaudio.functional.resample(wav.unsqueeze(0), sr, target_sr).squeeze(0)
445
+ return wav
446
+
447
+
448
+ def prepare_waveform(
449
+ waveform: torch.Tensor,
450
+ max_samples: int = MAX_SAMPLES,
451
+ ) -> torch.Tensor:
452
+ """Prepare a 1-D waveform tensor for model input.
453
+
454
+ Truncates to max_samples and pads if shorter. Returns [1, 1, T].
455
+ """
456
+ wav = waveform[:max_samples]
457
+ if wav.shape[0] < max_samples:
458
+ wav = torch.cat([wav, torch.zeros(max_samples - wav.shape[0])])
459
+ return wav.unsqueeze(0).unsqueeze(0) # [1, 1, T]
460
+
461
+
462
+ # ═══════════════════════════════════════════════════════════════════════════════
463
+ # Inference utilities
464
+ # ═══════════════════════════════════════════════════════════════════════════════
465
+
466
+ def score_waveform(
467
+ model: nn.Module,
468
+ waveform: torch.Tensor,
469
+ device: Optional[str] = None,
470
+ max_samples: int = MAX_SAMPLES,
471
+ ) -> float:
472
+ """Score a waveform tensor with a single model.
473
+
474
+ Args:
475
+ model: Loaded artifact detector model.
476
+ waveform: 1-D tensor [T] at 16 kHz.
477
+ device: Device to run inference on.
478
+ max_samples: Truncate waveform to this length.
479
+
480
+ Returns:
481
+ Artifact score in [0, 1].
482
+ """
483
+ if device is None:
484
+ device = next(model.parameters()).device
485
+ else:
486
+ device = torch.device(device)
487
+
488
+ x = prepare_waveform(waveform, max_samples).to(device)
489
+ with torch.no_grad():
490
+ return model.artifact_score(x).item()
491
+
492
+
493
+ def score_waveform_batch(
494
+ model: nn.Module,
495
+ waveforms: List[torch.Tensor],
496
+ device: Optional[str] = None,
497
+ max_samples: int = MAX_SAMPLES,
498
+ batch_size: int = 16,
499
+ ) -> List[float]:
500
+ """Score multiple waveforms with a single model.
501
+
502
+ Args:
503
+ model: Loaded artifact detector model.
504
+ waveforms: List of 1-D tensors [T] at 16 kHz.
505
+ device: Device to run inference on.
506
+ max_samples: Truncate waveforms to this length.
507
+ batch_size: Batch size for inference.
508
+
509
+ Returns:
510
+ List of artifact scores in [0, 1].
511
+ """
512
+ if device is None:
513
+ device = next(model.parameters()).device
514
+ else:
515
+ device = torch.device(device)
516
+
517
+ all_scores = []
518
+ for i in range(0, len(waveforms), batch_size):
519
+ batch_wavs = waveforms[i:i + batch_size]
520
+ # Prepare batch
521
+ prepared = []
522
+ for wav in batch_wavs:
523
+ w = wav[:max_samples]
524
+ if w.shape[0] < max_samples:
525
+ w = torch.cat([w, torch.zeros(max_samples - w.shape[0])])
526
+ prepared.append(w)
527
+ x = torch.stack(prepared).unsqueeze(1).to(device) # [B, 1, T]
528
+ with torch.no_grad():
529
+ scores = model.artifact_score(x).squeeze(1).tolist()
530
+ all_scores.extend(scores)
531
+ return all_scores
532
+
533
+
534
+ def score_file(
535
+ model: nn.Module,
536
+ audio_path: str,
537
+ device: Optional[str] = None,
538
+ ) -> float:
539
+ """Score an audio file with a single model.
540
+
541
+ Args:
542
+ model: Loaded artifact detector model.
543
+ audio_path: Path to audio file (any format supported by soundfile).
544
+ device: Device to run inference on.
545
+
546
+ Returns:
547
+ Artifact score in [0, 1].
548
+ """
549
+ wav = load_audio(audio_path)
550
+ return score_waveform(model, wav, device)
551
+
552
+
553
+ def score_file_all(
554
+ models: Dict[str, nn.Module],
555
+ audio_path: str,
556
+ device: Optional[str] = None,
557
+ ) -> Dict[str, float]:
558
+ """Score an audio file with all loaded models.
559
+
560
+ Args:
561
+ models: Dict of {name: model} from load_all_models().
562
+ audio_path: Path to audio file.
563
+ device: Device to run inference on.
564
+
565
+ Returns:
566
+ Dict mapping model name to artifact score in [0, 1].
567
+ """
568
+ wav = load_audio(audio_path)
569
+ return {name: score_waveform(m, wav, device) for name, m in models.items()}
570
+
571
+
572
+ def score_batch(
573
+ models: Dict[str, nn.Module],
574
+ audio_paths: List[str],
575
+ device: Optional[str] = None,
576
+ batch_size: int = 16,
577
+ ) -> List[Dict[str, float]]:
578
+ """Score multiple audio files with all loaded models.
579
+
580
+ Args:
581
+ models: Dict of {name: model} from load_all_models().
582
+ audio_paths: List of paths to audio files.
583
+ device: Device to run inference on.
584
+ batch_size: Batch size for inference.
585
+
586
+ Returns:
587
+ List of dicts, each mapping model name to artifact score.
588
+ """
589
+ # Load all audio
590
+ waveforms = [load_audio(p) for p in audio_paths]
591
+
592
+ # Score with each model
593
+ results = [{} for _ in audio_paths]
594
+ for name, model in models.items():
595
+ scores = score_waveform_batch(model, waveforms, device, batch_size=batch_size)
596
+ for i, s in enumerate(scores):
597
+ results[i][name] = s
598
+ return results
599
+
600
+
601
+ # ═══════════════════════════════════════════════════════════════════════════════
602
+ # CLI
603
+ # ═══════════════════════════════════════════════════════════════════════════════
604
+
605
+ def main():
606
+ import argparse
607
+ import glob
608
+
609
+ p = argparse.ArgumentParser(
610
+ description="Score audio files for speech artifacts using 10 CNN detectors",
611
+ formatter_class=argparse.RawDescriptionHelpFormatter,
612
+ epilog="""Examples:
613
+ # Score a single file with all models (from local directory)
614
+ python speech_artifact_detector.py --model-dir . --input audio.wav
615
+
616
+ # Score using models from HuggingFace Hub
617
+ python speech_artifact_detector.py --hub --input audio.wav
618
+
619
+ # Score only augmentation detectors
620
+ python speech_artifact_detector.py --hub --category augmentation --input audio.wav
621
+
622
+ # Score only TTS artifact detectors
623
+ python speech_artifact_detector.py --hub --category tts_artifact --input audio.wav
624
+
625
+ # Score a directory of files
626
+ python speech_artifact_detector.py --model-dir . --input /path/to/wavs/ --ext wav
627
+
628
+ # Score with a specific model
629
+ python speech_artifact_detector.py --checkpoint stft_classifier.pt --input audio.wav
630
+ """,
631
+ )
632
+ p.add_argument("--model-dir", help="Directory with .pt checkpoints")
633
+ p.add_argument("--hub", action="store_true",
634
+ help="Load models from HuggingFace Hub (laion/speech-artifact-detectors)")
635
+ p.add_argument("--checkpoint", help="Path to a single checkpoint")
636
+ p.add_argument("--input", required=True, help="Audio file or directory to score")
637
+ p.add_argument("--ext", default="wav", help="File extension when --input is a directory")
638
+ p.add_argument("--device", default=None, help="Device (cuda/cpu, auto-detect if omitted)")
639
+ p.add_argument("--category", choices=["tts_artifact", "augmentation"],
640
+ help="Load only models from this category")
641
+ p.add_argument("--threshold", type=float, default=0.5,
642
+ help="Threshold for artifact classification (default: 0.5)")
643
+ p.add_argument("--batch-size", type=int, default=16,
644
+ help="Batch size for inference (default: 16)")
645
+ args = p.parse_args()
646
+
647
+ device = args.device or ("cuda" if torch.cuda.is_available() else "cpu")
648
+ print(f"Device: {device}")
649
+
650
+ categories = [args.category] if args.category else None
651
+
652
+ # Load model(s)
653
+ if args.checkpoint:
654
+ model = load_model(args.checkpoint, device)
655
+ ckpt = torch.load(args.checkpoint, map_location="cpu", weights_only=True)
656
+ name = ckpt.get("augmentation", ckpt.get("architecture", "unknown"))
657
+ models = {name: model}
658
+ print(f"Loaded {name} (val_acc={ckpt.get('val_acc', '?')})")
659
+ elif args.hub:
660
+ models = load_from_hub(device=device, categories=categories)
661
+ print(f"Loaded {len(models)} models from Hub: {list(models.keys())}")
662
+ elif args.model_dir:
663
+ models = load_all_models(args.model_dir, device, categories=categories)
664
+ print(f"Loaded {len(models)} models: {list(models.keys())}")
665
+ else:
666
+ p.error("Provide --model-dir, --hub, or --checkpoint")
667
+
668
+ # Collect files
669
+ if os.path.isdir(args.input):
670
+ files = sorted(glob.glob(os.path.join(args.input, f"*.{args.ext}")))
671
+ else:
672
+ files = [args.input]
673
+ print(f"Scoring {len(files)} file(s)...\n")
674
+
675
+ model_names = list(models.keys())
676
+ header = f"{'File':<40s} " + " ".join(f"{n:>20s}" for n in model_names) + f" {'Mean':>8s} {'Verdict'}"
677
+ print(header)
678
+ print("-" * len(header))
679
+
680
+ n_artifact = 0
681
+ for fpath in files:
682
+ scores = score_file_all(models, fpath, device)
683
+ mean_score = np.mean(list(scores.values()))
684
+ verdict = "ARTIFACT" if mean_score >= args.threshold else "clean"
685
+ if verdict == "ARTIFACT":
686
+ n_artifact += 1
687
+
688
+ fname = os.path.basename(fpath)
689
+ row = f"{fname:<40s} " + " ".join(f"{scores[n]:>20.4f}" for n in model_names) + f" {mean_score:>8.4f} {verdict}"
690
+ print(row)
691
+
692
+ print(f"\n{n_artifact}/{len(files)} classified as artifact (threshold={args.threshold})")
693
+
694
+
695
+ if __name__ == "__main__":
696
+ main()
stft_classifier.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a9d235acd81d921180134c5314fa122d0cd4789a5d9cc0ee35c96a7deb477456
3
+ size 6821437
waveform_1d.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7a7d96b6b97e20b8657f09656c33bd6b3390ca7b6d60b7236eb77f572ea039a2
3
+ size 7625389