hackstorm_voice_model / app /cnn_detector.py
Frost10's picture
initail commit
2a4a312
Raw
History Blame Contribute Delete
6.94 kB
"""
CNN-Based Voice Detector using MFCC features
Trained on Human vs Non-Human (AI) voice classification
"""
import os
import numpy as np
import torch
import torch.nn as nn
import librosa
from typing import Dict, Any, Optional
# Model parameters (must match training)
SR = 16000 # Sample rate
DURATION = 22.0 # seconds
N_MFCC = 40
N_FFT = 1024
HOP_LENGTH = 256
INCLUDE_DELTAS = True
MAX_LEN_SAMPLES = int(SR * DURATION)
class SmallCNN(nn.Module):
"""CNN architecture matching indibert
."""
def __init__(self, n_feats: int = 120): # 40 MFCC * 3 (with deltas)
super().__init__()
self.net = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=3, padding=1),
nn.BatchNorm2d(16),
nn.ReLU(),
nn.MaxPool2d((2, 2)),
nn.Conv2d(16, 32, kernel_size=3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d((2, 2)),
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.AdaptiveAvgPool2d((1, 1)),
)
self.fc = nn.Linear(64, 2)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.net(x)
x = x.view(x.size(0), -1)
return self.fc(x)
class CNNVoiceDetector:
"""
CNN-based voice detector using MFCC features.
Trained on human-nonhuman dataset from Kaggle.
"""
def __init__(self, model_path: str = None):
self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.model = None
self.is_loaded = False
# Default model path from env or fallback inside app/models
if model_path is None:
env_path = os.getenv("CNN_MODEL_PATH")
if env_path:
model_path = env_path
else:
model_path = os.path.join(os.path.dirname(__file__), "models", "model_best.pt")
if not os.path.isabs(model_path):
model_path = os.path.abspath(model_path)
self.model_path = model_path
def load_model(self) -> bool:
"""Load the trained CNN model."""
if self.is_loaded:
return True
if not os.path.exists(self.model_path):
print(f"CNN model not found at {self.model_path}")
return False
try:
print(f"Loading CNN MFCC model from {self.model_path}...")
self.model = SmallCNN(n_feats=120) # 40 * 3 = 120 features
# Load weights
state_dict = torch.load(self.model_path, map_location=self.device)
self.model.load_state_dict(state_dict)
self.model.to(self.device)
self.model.eval()
self.is_loaded = True
print(f"✓ CNN model loaded on {self.device}")
return True
except Exception as e:
print(f"Failed to load CNN model: {e}")
return False
def extract_mfcc_features(self, audio: np.ndarray, sr: int = 16000) -> Optional[np.ndarray]:
"""
Extract MFCC + delta + delta2 features from audio.
Args:
audio: Audio samples as numpy array
sr: Sample rate
Returns:
MFCC features of shape (120, time_frames)
"""
try:
# Resample if needed
if sr != SR:
audio = librosa.resample(audio, orig_sr=sr, target_sr=SR)
# Pad/trim to fixed length
if len(audio) < MAX_LEN_SAMPLES:
audio = np.pad(audio, (0, MAX_LEN_SAMPLES - len(audio)), mode="constant")
else:
audio = audio[:MAX_LEN_SAMPLES]
# Extract MFCCs
mfcc = librosa.feature.mfcc(
y=audio, sr=SR, n_mfcc=N_MFCC, n_fft=N_FFT, hop_length=HOP_LENGTH
)
if INCLUDE_DELTAS:
delta = librosa.feature.delta(mfcc)
delta2 = librosa.feature.delta(mfcc, order=2)
feat = np.concatenate([mfcc, delta, delta2], axis=0)
else:
feat = mfcc
return feat.astype(np.float32)
except Exception as e:
print(f"MFCC extraction failed: {e}")
return None
def detect(self, audio: np.ndarray, sr: int = 16000) -> Dict[str, Any]:
"""
Detect if audio is AI-generated or human using CNN on MFCC features.
Args:
audio: Audio samples as numpy array
sr: Sample rate
Returns:
Detection result with classification, confidence, and explanation
"""
if not self.is_loaded:
if not self.load_model():
return {
"classification": "UNKNOWN",
"confidenceScore": 0.5,
"explanation": "CNN model not available",
"method": "cnn_unavailable",
}
# Extract MFCC features
features = self.extract_mfcc_features(audio, sr)
if features is None:
return {
"classification": "UNKNOWN",
"confidenceScore": 0.5,
"explanation": "Feature extraction failed",
"method": "cnn_error",
}
try:
# Prepare tensor: (C, T) -> (1, 1, C, T)
tensor = torch.tensor(features).unsqueeze(0).unsqueeze(0).to(self.device)
# Inference
with torch.no_grad():
logits = self.model(tensor)
probs = torch.softmax(logits, dim=1).cpu().numpy()[0]
pred = np.argmax(probs)
# pred: 0 = nonhuman (AI), 1 = human
if pred == 0:
classification = "AI_GENERATED"
confidence = float(probs[0])
explanation = "CNN detected synthetic voice patterns in MFCC features"
else:
classification = "HUMAN"
confidence = float(probs[1])
explanation = "CNN confirmed natural voice patterns in MFCC features"
return {
"classification": classification,
"confidenceScore": round(confidence, 3),
"explanation": explanation,
"method": "cnn_mfcc",
"probs": {"ai": float(probs[0]), "human": float(probs[1])},
}
except Exception as e:
print(f"CNN inference failed: {e}")
return {
"classification": "UNKNOWN",
"confidenceScore": 0.5,
"explanation": f"CNN inference error: {str(e)}",
"method": "cnn_error",
}
# Lazy-loaded singleton
_cnn_detector = None
def get_cnn_detector() -> CNNVoiceDetector:
"""Get or create the CNN voice detector singleton."""
global _cnn_detector
if _cnn_detector is None:
_cnn_detector = CNNVoiceDetector()
return _cnn_detector