Add 10 speech artifact detector models with inference code and README
Browse files- README.md +224 -0
- bandwidth_limitation.pt +3 -0
- clipping_distortion.pt +3 -0
- codec_compression.pt +3 -0
- comb_filtering.pt +3 -0
- mel_classifier.pt +3 -0
- phase_vocoder.pt +3 -0
- pitch_correction.pt +3 -0
- spectral_denoising.pt +3 -0
- speech_artifact_detector.py +696 -0
- stft_classifier.pt +3 -0
- waveform_1d.pt +3 -0
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
|