Spaces:
Sleeping
Sleeping
| """ | |
| MultiModalDetector β Unified orchestrator for DeepShield | |
| Handles Video (visual + audio fusion), Image, and Audio detection. | |
| """ | |
| import os, uuid, time, subprocess | |
| import numpy as np | |
| from pathlib import Path | |
| from typing import Optional | |
| import torch | |
| from detector import DeepfakeDetector # visual video branch | |
| from audio_detector import AudioDeepfakeDetector | |
| from image_detector import ImageDeepfakeDetector | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Utility: Extract audio from video using ffmpeg | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def extract_audio_from_video(video_path: str, output_wav: str) -> bool: | |
| """ | |
| Extracts the audio track from a video file and saves as 16 kHz mono WAV. | |
| Returns True on success, False if the video has no audio or ffmpeg fails. | |
| """ | |
| try: | |
| cmd = [ | |
| "ffmpeg", "-y", | |
| "-i", video_path, | |
| "-vn", # No video | |
| "-ar", "16000", # Resample to 16 kHz | |
| "-ac", "1", # Mono | |
| "-f", "wav", | |
| output_wav, | |
| ] | |
| result = subprocess.run(cmd, capture_output=True, timeout=120) | |
| return result.returncode == 0 and Path(output_wav).exists() and Path(output_wav).stat().st_size > 0 | |
| except Exception as e: | |
| print(f"[AudioExtract] ffmpeg failed: {e}") | |
| return False | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # MultiModalDetector | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class MultiModalDetector: | |
| """ | |
| Unified deepfake detector for three modalities: | |
| - Video : EfficientNet (visual) + Wav2Vec2 (audio) β fused score | |
| - Image : EfficientNetV2-S + MTCNN face detection | |
| - Audio : Wav2Vec2-base + attention-pooling classifier | |
| Fusion strategy for video: | |
| fused_score = 0.60 Γ visual_score + 0.40 Γ audio_score | |
| (if no audio track, fused_score = visual_score) | |
| """ | |
| VISUAL_WEIGHT = 0.60 | |
| AUDIO_WEIGHT = 0.40 | |
| def __init__( | |
| self, | |
| video_model_path: Optional[str] = None, | |
| image_model_path: Optional[str] = None, | |
| audio_model_path: Optional[str] = None, | |
| device: Optional[str] = None, | |
| threshold: float = 0.5, | |
| ): | |
| self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") | |
| self.threshold = threshold | |
| print(f"[DeepShield] Initializing MultiModalDetector on {self.device}") | |
| # ββ Video (visual) detector βββββββββββββββββββββββββββ | |
| self.video_detector = DeepfakeDetector( | |
| model_path=video_model_path, | |
| device=self.device, | |
| max_frames=32, | |
| threshold=threshold, | |
| ) | |
| # ββ Image detector ββββββββββββββββββββββββββββββββββββ | |
| self.image_detector = ImageDeepfakeDetector( | |
| model_path=image_model_path, | |
| device=self.device, | |
| threshold=threshold, | |
| ) | |
| # ββ Audio detector ββββββββββββββββββββββββββββββββββββ | |
| self.audio_available = False | |
| self.audio_model = None | |
| try: | |
| from audio_detector import AudioDeepfakeDetector, WAV2VEC_AVAILABLE | |
| if WAV2VEC_AVAILABLE: | |
| self.audio_model = AudioDeepfakeDetector( | |
| pretrained=True, | |
| freeze_base=True, | |
| ) | |
| if audio_model_path and Path(audio_model_path).exists(): | |
| state = torch.load( | |
| audio_model_path, map_location=self.device, weights_only=True | |
| ) | |
| self.audio_model.load_state_dict(state) | |
| print(f"[AudioDetector] Loaded weights from {audio_model_path}") | |
| else: | |
| print("[AudioDetector] Using pretrained Wav2Vec2 features (demo mode).") | |
| self.audio_model.to(self.device).eval() | |
| self.audio_available = True | |
| else: | |
| print("[AudioDetector] transformers not installed β audio branch disabled.") | |
| except Exception as e: | |
| print(f"[AudioDetector] Init failed: {e}") | |
| # ββ VIDEO βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def analyze_video(self, video_path: str, session_id: Optional[str] = None) -> dict: | |
| """ | |
| Full multimodal video analysis: | |
| 1. Visual branch: face extraction β EfficientNet inference β Grad-CAM | |
| 2. Audio branch : ffmpeg extract β Wav2Vec2 inference | |
| 3. Score fusion : 60/40 weighted average | |
| """ | |
| t0 = time.time() | |
| session_id = session_id or str(uuid.uuid4()) | |
| # 1. Visual analysis (existing pipeline) | |
| visual_result = self.video_detector.analyze(video_path, session_id=session_id) | |
| visual_score = visual_result.get("fake_prob", 0.5) | |
| # 2. Audio analysis | |
| audio_info = {"audio_available": False, "audio_fake_prob": None} | |
| if self.audio_available and self.audio_model is not None: | |
| audio_wav = str(Path("uploads") / session_id / "audio.wav") | |
| has_audio = extract_audio_from_video(video_path, audio_wav) | |
| if has_audio: | |
| try: | |
| waveform = AudioDeepfakeDetector.load_audio(audio_wav) | |
| audio_prob = self.audio_model.predict_proba(waveform, self.device) | |
| audio_info = { | |
| "audio_available": True, | |
| "audio_fake_prob": round(audio_prob, 4), | |
| "audio_verdict": "FAKE" if audio_prob >= self.threshold else "REAL", | |
| "audio_confidence": round(audio_prob * 100, 2), | |
| } | |
| except Exception as e: | |
| audio_info = {"audio_available": False, "audio_error": str(e)} | |
| else: | |
| audio_info = {"audio_available": False, "audio_error": "No audio track found."} | |
| # 3. Fusion | |
| if audio_info.get("audio_available") and audio_info.get("audio_fake_prob") is not None: | |
| fused_score = ( | |
| self.VISUAL_WEIGHT * visual_score | |
| + self.AUDIO_WEIGHT * audio_info["audio_fake_prob"] | |
| ) | |
| else: | |
| fused_score = visual_score | |
| fused_verdict = "FAKE" if fused_score >= self.threshold else "REAL" | |
| return { | |
| **visual_result, | |
| **audio_info, | |
| "visual_fake_prob": round(visual_score, 4), | |
| "visual_confidence": round(visual_score * 100, 2), | |
| "visual_verdict": "FAKE" if visual_score >= self.threshold else "REAL", | |
| "fused_fake_prob": round(fused_score, 4), | |
| "fused_confidence": round(fused_score, 4), | |
| "fused_verdict": fused_verdict, | |
| "verdict": fused_verdict, | |
| "confidence": round(fused_score, 4), | |
| "fake_prob": round(fused_score, 4), | |
| "elapsed_sec": round(time.time() - t0, 2), | |
| "modality": "video", | |
| } | |
| # ββ IMAGE βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def analyze_image(self, image_path: str) -> dict: | |
| """EfficientNetV2-S + MTCNN image pipeline.""" | |
| return self.image_detector.analyze(image_path) | |
| # ββ AUDIO βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def analyze_audio(self, audio_path: str) -> dict: | |
| """Wav2Vec2 audio-only pipeline.""" | |
| if not self.audio_available or self.audio_model is None: | |
| return { | |
| "verdict": "ERROR", | |
| "error": "Audio detection unavailable. Install: transformers, librosa.", | |
| "confidence": 0.0, | |
| "fake_prob": 0.0, | |
| "modality": "audio", | |
| } | |
| try: | |
| waveform = AudioDeepfakeDetector.load_audio(audio_path) | |
| prob = self.audio_model.predict_proba(waveform, self.device) | |
| return { | |
| "verdict": "FAKE" if prob >= self.threshold else "REAL", | |
| "confidence": round(prob, 4), | |
| "fake_prob": round(prob, 4), | |
| "modality": "audio", | |
| } | |
| except Exception as e: | |
| return { | |
| "verdict": "ERROR", | |
| "error": str(e), | |
| "confidence": 0.0, | |
| "fake_prob": 0.0, | |
| "modality": "audio", | |
| } | |