AISPY / core /forensics.py
dev
save
a34de3f
Raw History Blame Contribute Delete
13.5 kB
import os
try:
import cv2
except Exception:
cv2 = None
import numpy as np
from PIL import Image
from typing import Optional, Dict, List, Any
class EnsembleForensicsEngine:
"""
Enterprise-grade forensics engine combining:
- Face-specific deepfake detection (fine-tuned model)
- Omni-scene manipulation detection (whole image analysis)
- Temporal consistency checking (for videos)
"""
def __init__(self, face_model_dir: str = None, omni_model_dir: str = None):
"""Initialize the dual-engine forensics system."""
print("⚙️ Booting AI-SPY Ensemble Engine (CPU)...")
# Define paths - handle spaces and special characters in directory names
self.FACE_MODEL_DIR = face_model_dir or "./face_v2_enhanced (1)"
self.OMNI_MODEL_DIR = omni_model_dir or "./omni_detector_v1"
self.face_detector = None
self.omni_detector = None
self.face_tracker = None
# Load models only when explicitly allowed via env var
if os.getenv("ALLOW_LOCAL_MODELS", "0") == "1":
self._load_models()
else:
print(" ⚠️ Local finetuned models are disabled. Set ALLOW_LOCAL_MODELS=1 to enable loading.")
def _load_models(self):
"""Load both classification pipelines."""
try:
from transformers import pipeline
except Exception as e:
print(f" ⚠️ transformers not available; cannot load local forensics models: {e}")
self.face_detector = None
self.omni_detector = None
return
try:
print(f" 📦 Loading Face Detection Model from {self.FACE_MODEL_DIR}...")
self.face_detector = pipeline(
"image-classification",
model=self.FACE_MODEL_DIR,
device=-1, # CPU mode
top_k=None,
framework="pt"
)
print(" ✓ Face detector loaded successfully.")
except Exception as e:
print(f" ⚠️ Face Model Load Error: {e}")
self.face_detector = None
try:
print(f" 📦 Loading Omni Detection Model from {self.OMNI_MODEL_DIR}...")
self.omni_detector = pipeline(
"image-classification",
model=self.OMNI_MODEL_DIR,
device=-1, # CPU mode
top_k=None,
framework="pt"
)
print(" ✓ Omni detector loaded successfully.")
except Exception as e:
print(f" ⚠️ Omni Model Load Error: {e}")
self.omni_detector = None
# Load Haar Cascade for face tracking
try:
cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
self.face_tracker = cv2.CascadeClassifier(cascade_path)
print(" ✓ Face tracker (Haar Cascade) loaded.")
except Exception as e:
print(f" ⚠️ Face Tracker Load Error: {e}")
self.face_tracker = None
def extract_real_score(self, results: List[Dict[str, Any]]) -> float:
"""
Extract the 'realness' score from model output.
Handles various label formats from different model architectures.
Returns: 0.0 to 100.0 (percentage confidence)
"""
if not results:
return 0.0
scores = {res['label'].lower(): res['score'] for res in results}
# Priority 1: Explicit "real" label
if 'real' in scores:
return scores['real'] * 100.0
# Priority 2: Label indices (label_2 often means "real" in binary models)
if 'label_2' in scores:
return scores['label_2'] * 100.0
if 'label_1' in scores:
return scores['label_1'] * 100.0
# Priority 3: Invert "fake" score
if 'fake' in scores:
return (1.0 - scores['fake']) * 100.0
# Priority 4: Invert label_0 (often "fake" or "synthetic")
if 'label_0' in scores:
return (1.0 - scores['label_0']) * 100.0
# Default: No clear real label found
return 0.0
def crop_face(self, cv_image: np.ndarray) -> Optional[Image.Image]:
"""
Detect and crop the largest face from the image.
Adds padding around the face for context.
Args:
cv_image: OpenCV image (BGR format)
Returns:
PIL Image of the cropped face, or None if no face detected
"""
if self.face_tracker is None:
return None
gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
faces = self.face_tracker.detectMultiScale(
gray,
scaleFactor=1.1,
minNeighbors=5,
minSize=(60, 60)
)
if len(faces) == 0:
return None
# Use the largest face detected
faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)
x, y, w, h = faces[0]
# Add padding (20% of face dimensions)
ih, iw, _ = cv_image.shape
mx, my = int(w * 0.2), int(h * 0.2)
x1, y1 = max(0, x - mx), max(0, y - my)
x2, y2 = min(iw, x + w + mx), min(ih, y + h + my)
# Crop and convert to PIL
crop = cv_image[y1:y2, x1:x2]
if crop.size > 0:
return Image.fromarray(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB))
return None
def _analyze_image(self, file_path: str) -> Dict[str, Any]:
"""
Analyze a single image file.
Returns:
Dict with metrics: scene_real_score, face_real_score
"""
try:
img_pil = Image.open(file_path).convert("RGB")
cv_img = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
except Exception as e:
return {
"scene_real_score": 0.0,
"face_real_score": 0.0,
"error": f"Failed to load image: {e}"
}
metrics = {}
# Scene analysis
if self.omni_detector:
try:
scene_results = self.omni_detector(img_pil)
metrics["scene_real_score"] = self.extract_real_score(scene_results)
except Exception as e:
print(f" ⚠️ Scene analysis failed: {e}")
metrics["scene_real_score"] = 0.0
else:
metrics["scene_real_score"] = 0.0
# Face analysis
if self.face_detector:
try:
face_pil = self.crop_face(cv_img)
if face_pil:
face_results = self.face_detector(face_pil)
metrics["face_real_score"] = self.extract_real_score(face_results)
else:
metrics["face_real_score"] = 100.0 # No face found, assume authentic
except Exception as e:
print(f" ⚠️ Face analysis failed: {e}")
metrics["face_real_score"] = 0.0
else:
metrics["face_real_score"] = 0.0
return metrics
def _analyze_video(self, file_path: str, max_frames: int = 30) -> Dict[str, Any]:
"""
Analyze a video by sampling frames at regular intervals.
Args:
file_path: Path to video file
max_frames: Maximum number of frames to analyze
Returns:
Dict with metrics: avg_scene, min_scene, avg_face, min_face, frame_count
"""
try:
cap = cv2.VideoCapture(file_path)
fps = int(cap.get(cv2.CAP_PROP_FPS)) or 30
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 300
# Calculate frame skip to stay within max_frames limit
frame_skip = max(1, total_frames // max_frames)
except Exception as e:
return {
"avg_scene": 0.0,
"min_scene": 0.0,
"avg_face": 0.0,
"min_face": 0.0,
"error": f"Failed to open video: {e}"
}
scene_scores = []
face_scores = []
frame_count = 0
analyzed_frames = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
if frame_count % frame_skip == 0:
analyzed_frames += 1
# Scene analysis
if self.omni_detector:
try:
pil_frame = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
scene_results = self.omni_detector(pil_frame)
scene_score = self.extract_real_score(scene_results)
scene_scores.append(scene_score)
except Exception as e:
print(f" ⚠️ Frame {frame_count} scene analysis failed: {e}")
# Face analysis
if self.face_detector:
try:
face_pil = self.crop_face(frame)
if face_pil:
face_results = self.face_detector(face_pil)
face_score = self.extract_real_score(face_results)
face_scores.append(face_score)
except Exception as e:
print(f" ⚠️ Frame {frame_count} face analysis failed: {e}")
frame_count += 1
cap.release()
# Calculate statistics
metrics = {
"avg_scene": sum(scene_scores) / len(scene_scores) if scene_scores else 100.0,
"min_scene": min(scene_scores) if scene_scores else 100.0,
"max_scene": max(scene_scores) if scene_scores else 100.0,
"avg_face": sum(face_scores) / len(face_scores) if face_scores else 100.0,
"min_face": min(face_scores) if face_scores else 100.0,
"max_face": max(face_scores) if face_scores else 100.0,
"frames_analyzed": analyzed_frames,
"total_frames": total_frames
}
return metrics
def analyze(self, file_path: str) -> Dict[str, Any]:
"""
Main entry point: Analyze media and return structured forensic report.
Args:
file_path: Path to image or video file
Returns:
Structured dictionary with verdict and metrics
"""
is_video = file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv', '.webm'))
report = {
"type": "video" if is_video else "image",
"is_fake": False,
"confidence": 0.0,
"reason": "",
"metrics": {}
}
if not os.path.exists(file_path):
report["reason"] = f"File not found: {file_path}"
return report
print(f" 🔍 Analyzing {'video' if is_video else 'image'}: {file_path}")
# Analyze media
if is_video:
metrics = self._analyze_video(file_path)
else:
metrics = self._analyze_image(file_path)
report["metrics"] = metrics
# Determine verdict based on metrics
if is_video:
min_face = metrics.get("min_face", 100.0)
min_scene = metrics.get("min_scene", 100.0)
avg_face = metrics.get("avg_face", 100.0)
avg_scene = metrics.get("avg_scene", 100.0)
# Severe facial glitch detected
if min_face < 40.0:
report["is_fake"] = True
report["confidence"] = 95.0
report["reason"] = f"🚨 SEVERE FACIAL GLITCH | Frame dropped to {min_face:.1f}% authenticity"
# Severe background anomaly
elif min_scene < 40.0:
report["is_fake"] = True
report["confidence"] = 85.0
report["reason"] = f"🚨 SEVERE BACKGROUND ANOMALY | Scene authenticity dropped to {min_scene:.1f}%"
# Strict average threshold
elif avg_face < 85.0 or avg_scene < 85.0:
report["is_fake"] = True
report["confidence"] = 80.0
report["reason"] = f"⚠️ FAILED 85% THRESHOLD | Avg Face: {avg_face:.1f}%, Avg Scene: {avg_scene:.1f}%"
# Likely authentic
else:
report["is_fake"] = False
report["confidence"] = min(avg_face, avg_scene) # Use lower score for confidence
report["reason"] = f"✓ PASSED All Checks | Avg Face: {avg_face:.1f}%, Avg Scene: {avg_scene:.1f}%"
else: # Image analysis
scene_score = metrics.get("scene_real_score", 0.0)
face_score = metrics.get("face_real_score", 0.0)
# Zero-tolerance check
if scene_score < 80.0 or face_score < 80.0:
report["is_fake"] = True
report["confidence"] = 85.0
report["reason"] = f"Failed Zero-Tolerance Check | Scene: {scene_score:.1f}%, Face: {face_score:.1f}%"
else:
report["is_fake"] = False
report["confidence"] = min(scene_score, face_score)
report["reason"] = f"Passed Authentication | Scene: {scene_score:.1f}%, Face: {face_score:.1f}%"
return report