Spaces:
Running
Running
| import os | |
| import torch | |
| import torch.nn as nn | |
| import numpy as np | |
| import librosa | |
| import matplotlib.pyplot as plt | |
| from scipy.ndimage import gaussian_filter | |
| # ββ CONFIG ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| AUDIO_FILE = "file2.wav" # your audio file path | |
| MODEL_PATH = "crnn_transformer_model (2).pth" # your saved model path | |
| THRESHOLD = 0.2 | |
| SAVE_PLOT = None # e.g. "result.png" or None to show inline | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ββ Model βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TransformerEncoder(nn.Module): | |
| def __init__(self, d_model=128, nhead=4, num_layers=2, dropout=0.4): | |
| super().__init__() | |
| self.input_proj = nn.Linear(3200, d_model) | |
| self.pos_embedding = nn.Parameter(torch.randn(1, 200, d_model) * 0.01) | |
| encoder_layer = nn.TransformerEncoderLayer( | |
| d_model=d_model, nhead=nhead, | |
| dim_feedforward=d_model * 2, | |
| dropout=dropout, batch_first=True, norm_first=True | |
| ) | |
| self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) | |
| self.norm = nn.LayerNorm(d_model) | |
| def forward(self, x): | |
| x = self.input_proj(x) | |
| x = x + self.pos_embedding[:, :x.size(1), :] | |
| x = self.transformer(x) | |
| return self.norm(x) | |
| class CRNN(nn.Module): | |
| def __init__(self, num_classes=2, d_model=128, nhead=4, num_transformer_layers=2): | |
| super().__init__() | |
| self.conv = nn.Sequential( | |
| nn.Conv2d(2, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.Dropout2d(0.1), nn.MaxPool2d(2, 2), | |
| nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.Dropout2d(0.1), nn.MaxPool2d(2, 2), | |
| nn.Conv2d(64, 128,kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(), nn.Dropout2d(0.2), nn.MaxPool2d(2, 2), | |
| ) | |
| self.transformer_enc = TransformerEncoder(d_model, nhead, num_transformer_layers, dropout=0.4) | |
| self.fc = nn.Sequential( | |
| nn.Linear(d_model, 64), nn.ReLU(), nn.Dropout(0.6), nn.Linear(64, num_classes) | |
| ) | |
| def forward(self, x): | |
| x = self.conv(x) | |
| b, c, h, w = x.size() | |
| x = x.permute(0, 2, 1, 3).reshape(b, h, c * w) | |
| x = self.transformer_enc(x) | |
| return self.fc(x.mean(dim=1)) | |
| # ββ Feature extraction ββββββββββββββββββββββββββββββββββββββββ | |
| def extract_features(audio, sr=16000, n_mels=128, n_fft=1024, hop_length=512, max_len=200): | |
| # Mel | |
| mel = librosa.feature.melspectrogram(y=audio, sr=sr, n_mels=n_mels, n_fft=n_fft, hop_length=hop_length) | |
| mel = librosa.power_to_db(mel, ref=np.max) | |
| mel = (mel - mel.mean()) / (mel.std() + 1e-8) | |
| # LFCC | |
| stft = np.abs(librosa.stft(audio, n_fft=n_fft, hop_length=hop_length)) | |
| n_bins = stft.shape[0] | |
| freqs = np.linspace(0, sr // 2, n_bins) | |
| centers = np.linspace(0, sr // 2, n_mels + 2) | |
| lf = np.zeros((n_mels, n_bins)) | |
| for i in range(n_mels): | |
| lo, mid, hi = centers[i], centers[i+1], centers[i+2] | |
| up = (freqs >= lo) & (freqs <= mid) | |
| down = (freqs > mid) & (freqs <= hi) | |
| lf[i, up] = (freqs[up] - lo) / (mid - lo + 1e-8) | |
| lf[i, down] = (hi - freqs[down]) / (hi - mid + 1e-8) | |
| lfcc = np.dot(lf, stft) | |
| lfcc = librosa.power_to_db(lfcc ** 2 + 1e-8, ref=np.max) | |
| lfcc = (lfcc - lfcc.mean()) / (lfcc.std() + 1e-8) | |
| def pad(spec): | |
| l = spec.shape[1] | |
| if l < max_len: | |
| return np.pad(spec, ((0, 0), (0, max_len - l)), mode='edge' if l >= 2 else 'constant') | |
| return spec[:, :max_len] | |
| return pad(mel), pad(lfcc) | |
| # ββ Main predict + explain ββββββββββββββββββββββββββββββββββββ | |
| def predict_and_explain(audio_file, model_path, threshold, save_plot=None): | |
| if not os.path.exists(model_path): | |
| print(f"β Model not found: {model_path}"); return | |
| if not os.path.exists(audio_file): | |
| print(f"β Audio file not found: {audio_file}"); return | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| # Load model | |
| model = CRNN().to(device) | |
| model.load_state_dict(torch.load(model_path, map_location=device)) | |
| model.eval() | |
| # Load audio | |
| try: | |
| audio, _ = librosa.load(audio_file, sr=16000) | |
| if len(audio) == 0: | |
| audio = np.zeros(16000) | |
| except Exception as e: | |
| print(f"Error loading audio: {e}") | |
| audio = np.zeros(16000) | |
| mel, lfcc = extract_features(audio) | |
| features = np.stack([mel, lfcc], axis=0) # (2, 128, 200) | |
| feat_t = torch.tensor(features, dtype=torch.float32).unsqueeze(0).to(device) | |
| # ββ Prediction ββββββββββββββββββββββββββββββββββββββββββββ | |
| with torch.no_grad(): | |
| prob = torch.softmax(model(feat_t), dim=1) | |
| fake_prob = prob[0][1].item() | |
| real_prob = prob[0][0].item() | |
| label = "FAKE π΄" if fake_prob > threshold else "REAL β " | |
| print(f"\n File : {os.path.basename(audio_file)}") | |
| print(f" Result : {label}") | |
| print(f" FAKE prob : {fake_prob*100:.1f}%") | |
| print(f" REAL prob : {real_prob*100:.1f}%") | |
| bar = 40 | |
| f = int(fake_prob * bar) | |
| print(f"\n REAL {'β' * (bar-f)}{'β' * f} FAKE") | |
| print(f" {real_prob*100:>5.1f}%{' '*(bar-10)}{fake_prob*100:>5.1f}%") | |
| # ββ Saliency map (gradient-based XAI) ββββββββββββββββββββ | |
| # How it works: we do a forward pass with gradients enabled, | |
| # then backpropagate from the predicted class score back to | |
| # the input. Large gradients = input regions the model relied | |
| # on most heavily for its decision. | |
| model.eval() | |
| feat_grad = feat_t.clone().requires_grad_(True) | |
| output = model(feat_grad) | |
| pred_class = output.argmax(dim=1).item() | |
| model.zero_grad() | |
| output[0, pred_class].backward() | |
| # Average absolute gradient across Mel + LFCC channels β (128, 200) | |
| saliency = feat_grad.grad.abs().mean(dim=1).squeeze().cpu().numpy() | |
| saliency_smooth = gaussian_filter(saliency, sigma=1.5) | |
| saliency_norm = (saliency_smooth - saliency_smooth.min()) / \ | |
| (saliency_smooth.max() - saliency_smooth.min() + 1e-8) | |
| # ββ Text summary ββββββββββββββββββββββββββββββββββββββββββ | |
| hop_sec = 512 / 16000 | |
| time_imp = saliency_norm.mean(axis=0) | |
| freq_imp = saliency_norm.mean(axis=1) | |
| top_frames = np.argsort(time_imp)[-5:][::-1] | |
| top_bands = np.argsort(freq_imp)[-5:][::-1] | |
| mel_freqs = librosa.mel_frequencies(n_mels=128, fmin=0, fmax=8000) | |
| print("\n" + "β" * 50) | |
| print(" EXPLAINABILITY SUMMARY") | |
| print("β" * 50) | |
| print(f" Prediction : {'FAKE' if fake_prob > threshold else 'REAL'} " | |
| f"({max(fake_prob, real_prob)*100:.1f}% confidence)") | |
| print("\n Most suspicious TIME regions:") | |
| for fr in top_frames: | |
| print(f" {fr*hop_sec:.3f}s β {(fr+1)*hop_sec:.3f}s " | |
| f"(importance {time_imp[fr]:.3f})") | |
| print("\n Most suspicious FREQUENCY bands:") | |
| for b in top_bands: | |
| print(f" ~{mel_freqs[b]:.0f} Hz " | |
| f"(mel band {b}, importance {freq_imp[b]:.3f})") | |
| print("β" * 50) | |
| # ββ Plot ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| extent = [0, 200 * 512 / 16000, 0, 8] # time (s) vs freq (kHz) | |
| fig, axes = plt.subplots(3, 1, figsize=(12, 11)) | |
| fig.suptitle( | |
| f"Explainability Report | {os.path.basename(audio_file)}\n" | |
| f"Prediction: {'FAKE' if fake_prob > threshold else 'REAL'} " | |
| f"| FAKE prob: {fake_prob*100:.1f}% | Threshold: {threshold}", | |
| fontsize=13, fontweight='bold' | |
| ) | |
| # Plot 1 β Mel | |
| im0 = axes[0].imshow(mel, aspect='auto', origin='lower', cmap='magma', extent=extent) | |
| axes[0].set_title("Channel 1: Mel Spectrogram") | |
| axes[0].set_xlabel("Time (s)"); axes[0].set_ylabel("Frequency (kHz)") | |
| plt.colorbar(im0, ax=axes[0], label="dB (normalised)") | |
| # Plot 2 β LFCC | |
| im1 = axes[1].imshow(lfcc, aspect='auto', origin='lower', cmap='viridis', extent=extent) | |
| axes[1].set_title("Channel 2: LFCC (linear filterbank β captures synthesis artifacts)") | |
| axes[1].set_xlabel("Time (s)"); axes[1].set_ylabel("Frequency (kHz)") | |
| plt.colorbar(im1, ax=axes[1], label="dB (normalised)") | |
| # Plot 3 β Saliency | |
| im2 = axes[2].imshow(saliency_norm, aspect='auto', origin='lower', cmap='hot', extent=extent) | |
| axes[2].set_title("Saliency Map β bright regions = where model focused for FAKE/REAL decision") | |
| axes[2].set_xlabel("Time (s)"); axes[2].set_ylabel("Frequency (kHz)") | |
| plt.colorbar(im2, ax=axes[2], label="Importance (normalised)") | |
| plt.tight_layout() | |
| if save_plot: | |
| plt.savefig(save_plot, dpi=150, bbox_inches='tight') | |
| print(f"\n Plot saved β {save_plot}") | |
| else: | |
| plt.show() | |
| plt.close(fig) | |
| if __name__ == "__main__": | |
| predict_and_explain(AUDIO_FILE, MODEL_PATH, THRESHOLD, SAVE_PLOT) |