Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |