VeriFake / test.py
Saksham5iu's picture
Upload 5 files
3e5a17e verified
Raw
History Blame Contribute Delete
10 kB
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)