spideyhead commited on
Commit
65744f1
·
verified ·
1 Parent(s): 9b75405

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +1 -2
  2. main.py +84 -220
  3. requirements.txt +0 -3
Dockerfile CHANGED
@@ -1,12 +1,11 @@
1
  FROM python:3.9-slim
2
 
3
- # Install system dependencies: OpenCV + ffmpeg for audio extraction
4
  RUN apt-get update && apt-get install -y \
5
  libglib2.0-0 \
6
  libsm6 \
7
  libxext6 \
8
  libxrender-dev \
9
- ffmpeg \
10
  && rm -rf /var/lib/apt/lists/*
11
 
12
  # Set up a non-root user for Hugging Face Spaces
 
1
  FROM python:3.9-slim
2
 
3
+ # Install system dependencies required by OpenCV
4
  RUN apt-get update && apt-get install -y \
5
  libglib2.0-0 \
6
  libsm6 \
7
  libxext6 \
8
  libxrender-dev \
 
9
  && rm -rf /var/lib/apt/lists/*
10
 
11
  # Set up a non-root user for Hugging Face Spaces
main.py CHANGED
@@ -2,14 +2,9 @@ import os
2
  import cv2
3
  import torch
4
  import shutil
5
- import subprocess
6
- import numpy as np
7
  from fastapi import FastAPI, UploadFile, File
8
  from fastapi.middleware.cors import CORSMiddleware
9
- from transformers import (
10
- VideoMAEImageProcessor, VideoMAEForVideoClassification,
11
- AutoFeatureExtractor, AutoModelForAudioClassification
12
- )
13
  from facenet_pytorch import MTCNN
14
  from PIL import Image
15
 
@@ -18,279 +13,148 @@ app = FastAPI()
18
  # Enable CORS for the React frontend
19
  app.add_middleware(
20
  CORSMiddleware,
21
- allow_origins=["*"],
22
  allow_credentials=True,
23
  allow_methods=["*"],
24
  allow_headers=["*"],
25
  )
26
 
27
- os.makedirs("temp", exist_ok=True)
28
-
29
- # ─── Load Models on Startup ────────────────────────────────────────────────────
30
  print("Loading MTCNN Face Detector...")
31
  mtcnn = MTCNN(keep_all=False, select_largest=True, post_process=False)
32
 
33
- print("Loading VideoMAE Temporal Deepfake Detector...")
34
- video_model_name = "Ammar2k/videomae-base-finetuned-deepfake-subset"
35
- video_processor = VideoMAEImageProcessor.from_pretrained(video_model_name)
36
- video_model = VideoMAEForVideoClassification.from_pretrained(video_model_name)
37
- video_model.eval()
38
-
39
- print("Loading Wav2Vec2 Audio Deepfake Detector...")
40
- audio_model_name = "MelodyMachine/Deepfake-audio-detection-V2"
41
- audio_extractor = AutoFeatureExtractor.from_pretrained(audio_model_name)
42
- audio_model = AutoModelForAudioClassification.from_pretrained(audio_model_name)
43
- audio_model.eval()
44
 
45
- print("All models loaded successfully!")
 
46
 
47
- # ─── Video Analysis ─────────────────────────────────────────────────────────
48
- def extract_face_sequence(video_path, num_frames=32):
49
- """Extract a continuous face-tracked sequence of N frames from the video."""
50
  cap = cv2.VideoCapture(video_path)
51
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
52
-
53
- if total_frames == 0:
54
- cap.release()
55
  return []
56
-
57
- # Start from the middle of the video to get the best face-visible region
58
- start_frame = max(0, (total_frames // 2) - (num_frames // 2))
59
  cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
60
-
61
  faces = []
62
  last_box = None
63
-
64
- for _ in range(num_frames):
65
  ret, frame = cap.read()
66
  if not ret:
67
  break
68
-
 
69
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
70
  pil_img = Image.fromarray(frame_rgb)
71
-
 
72
  boxes, _ = mtcnn.detect(pil_img)
73
-
74
- # Bounding-box smoothing: use last known good box if detection fails
75
  if boxes is not None and len(boxes) > 0:
76
- last_box = boxes[0]
77
-
78
- if last_box is None:
79
- continue
80
-
81
- box = last_box
 
 
82
  w = box[2] - box[0]
83
  h = box[3] - box[1]
84
  pad_w = int(w * 0.3)
85
  pad_h = int(h * 0.3)
 
86
  x1 = max(0, int(box[0]) - pad_w)
87
  y1 = max(0, int(box[1]) - pad_h)
88
  x2 = min(pil_img.width, int(box[2]) + pad_w)
89
  y2 = min(pil_img.height, int(box[3]) + pad_h)
90
-
91
  if x2 > x1 and y2 > y1:
92
  face_crop = pil_img.crop((x1, y1, x2, y2))
93
  faces.append(face_crop)
94
-
95
  cap.release()
96
-
97
- if not faces:
 
98
  return []
99
-
100
- # Pad to ensure we always have exactly num_frames
101
- while len(faces) < num_frames:
102
  faces.append(faces[-1])
 
 
103
 
104
- return faces[:num_frames]
105
-
106
-
107
- def analyze_video_temporal(faces):
108
- """Run VideoMAE on two 16-frame batches and average the results."""
109
- half = len(faces) // 2
110
- batch_a = faces[:half]
111
- batch_b = faces[half:]
112
-
113
- scores = []
114
- for batch in [batch_a, batch_b]:
115
- if len(batch) < 16:
116
- while len(batch) < 16:
117
- batch.append(batch[-1])
118
- batch = batch[:16]
119
-
120
- inputs = video_processor(list(batch), return_tensors="pt")
121
- with torch.no_grad():
122
- outputs = video_model(**inputs)
123
- probs = torch.nn.functional.softmax(outputs.logits[0], dim=-1)
124
- predicted_idx = probs.argmax(-1).item()
125
- label = video_model.config.id2label[predicted_idx].lower()
126
- is_fake = 'fake' in label
127
- fake_prob = probs[predicted_idx].item() if is_fake else (1.0 - probs[predicted_idx].item())
128
- scores.append(fake_prob)
129
-
130
- avg_fake_prob = sum(scores) / len(scores)
131
- is_fake = avg_fake_prob > 0.5
132
- confidence = round((avg_fake_prob if is_fake else (1.0 - avg_fake_prob)) * 100, 2)
133
- return is_fake, confidence, avg_fake_prob
134
-
135
-
136
- # ─── Audio Analysis ─────────────────────────────────────────────────────────
137
- def extract_audio(video_path):
138
- """Extract audio from video using ffmpeg. Returns numpy array or None."""
139
- audio_path = video_path.replace(os.path.splitext(video_path)[1], "_audio.wav")
140
- try:
141
- result = subprocess.run(
142
- ["ffmpeg", "-y", "-i", video_path, "-ar", "16000", "-ac", "1", "-f", "wav", audio_path],
143
- stdout=subprocess.PIPE,
144
- stderr=subprocess.PIPE,
145
- timeout=30
146
- )
147
- if result.returncode != 0 or not os.path.exists(audio_path):
148
- return None, None
149
-
150
- import soundfile as sf
151
- audio_array, sample_rate = sf.read(audio_path)
152
- os.remove(audio_path)
153
- return audio_array.astype(np.float32), sample_rate
154
-
155
- except Exception as e:
156
- print(f"Audio extraction failed: {e}")
157
- if os.path.exists(audio_path):
158
- os.remove(audio_path)
159
- return None, None
160
-
161
-
162
- def analyze_audio(audio_array, sample_rate):
163
- """Run Wav2Vec2 audio deepfake detector on the audio array."""
164
- try:
165
- # Use max 10 seconds of audio for speed
166
- max_samples = 16000 * 10
167
- if len(audio_array) > max_samples:
168
- audio_array = audio_array[:max_samples]
169
-
170
- inputs = audio_extractor(
171
- audio_array,
172
- sampling_rate=16000,
173
- return_tensors="pt",
174
- padding=True
175
- )
176
-
177
- with torch.no_grad():
178
- outputs = audio_model(**inputs)
179
-
180
- probs = torch.nn.functional.softmax(outputs.logits[0], dim=-1)
181
- predicted_idx = probs.argmax(-1).item()
182
- label = audio_model.config.id2label[predicted_idx].lower()
183
- is_fake = 'fake' in label or 'spoof' in label
184
- fake_prob = probs[predicted_idx].item() if is_fake else (1.0 - probs[predicted_idx].item())
185
- confidence = round((fake_prob if is_fake else (1.0 - fake_prob)) * 100, 2)
186
- return is_fake, confidence, fake_prob
187
-
188
- except Exception as e:
189
- print(f"Audio analysis failed: {e}")
190
- return None, None, None
191
-
192
-
193
- # ─── API Endpoint ────────────────────────────────────────────────────────────
194
  @app.post("/api/analyze")
195
  async def analyze_video(file: UploadFile = File(...)):
196
  print(f"Received file: {file.filename}")
197
-
198
  temp_video_path = os.path.join("temp", file.filename)
199
  with open(temp_video_path, "wb") as buffer:
200
  shutil.copyfileobj(file.file, buffer)
201
-
202
  try:
203
- # ── Video Analysis ──────────────────────────────────────────────────
204
- print("Extracting 32-frame face sequence...")
205
- faces = extract_face_sequence(temp_video_path, num_frames=32)
206
-
207
  if not faces:
208
  return {
209
- "isFake": False,
210
- "confidence": 0,
211
- "explanation": "Could not detect a clear face in the video. Please ensure the subject's face is clearly visible.",
212
  "details": []
213
  }
214
-
215
- print(f"Running dual-batch VideoMAE temporal inference ({len(faces)} frames)...")
216
- video_is_fake, video_confidence, video_fake_prob = analyze_video_temporal(faces)
217
-
218
- # ── Audio Analysis ���─────────────────────────────────────────────────
219
- print("Extracting audio track...")
220
- audio_array, sample_rate = extract_audio(temp_video_path)
221
-
222
- audio_is_fake = None
223
- audio_confidence = None
224
- audio_fake_prob = None
225
- has_audio = False
226
-
227
- if audio_array is not None and len(audio_array) > 1600:
228
- has_audio = True
229
- print("Running Wav2Vec2 audio deepfake analysis...")
230
- audio_is_fake, audio_confidence, audio_fake_prob = analyze_audio(audio_array, sample_rate)
231
-
232
- # ── Ensemble Score Merger ───────────────────────────────────────────
233
- if has_audio and audio_fake_prob is not None:
234
- # Weight: 60% video, 40% audio
235
- ensemble_fake_prob = (video_fake_prob * 0.6) + (audio_fake_prob * 0.4)
236
- is_fake = ensemble_fake_prob > 0.5
237
- confidence = round(ensemble_fake_prob * 100, 2) if is_fake else round((1 - ensemble_fake_prob) * 100, 2)
238
- analysis_type = "Ensemble (Video + Audio)"
239
- else:
240
- is_fake = video_is_fake
241
- confidence = video_confidence
242
- analysis_type = "Video-Only (No audio track detected)"
243
-
244
- print(f"Final Result: isFake={is_fake}, confidence={confidence}%, type={analysis_type}")
245
-
246
- # ── Build Detailed Report ───────────────────────────────────────────
247
- explanation = (
248
- "Our ensemble AI analyzed both facial motion and voice patterns. "
249
- "Significant temporal artifacts and/or synthetic voice characteristics were detected."
250
- if is_fake else
251
- "Our ensemble AI analyzed both facial motion and voice patterns. "
252
- "Natural micro-expressions, consistent facial flow, and authentic voice characteristics were found."
253
- )
254
-
255
- details = [
256
- {
257
- "title": "🎬 Video Temporal Analysis",
258
- "desc": f"VideoMAE 3D Transformer analyzed two 16-frame clips. Verdict: {'FAKE' if video_is_fake else 'REAL'} ({video_confidence}% confidence)."
259
- },
260
- ]
261
-
262
- if has_audio and audio_is_fake is not None:
263
- details.append({
264
- "title": "🎙️ Audio Voice Analysis",
265
- "desc": f"Wav2Vec2 SSL model analyzed the voice track for synthetic cloning. Verdict: {'FAKE' if audio_is_fake else 'REAL'} ({audio_confidence}% confidence)."
266
- })
267
- else:
268
- details.append({
269
- "title": "🎙️ Audio Voice Analysis",
270
- "desc": "No audio track was detected in this video file. Analysis based on video only."
271
- })
272
-
273
- details.append({
274
- "title": "🧠 Ensemble Score",
275
- "desc": f"Final combined confidence: {confidence}%. Analysis type: {analysis_type}."
276
- })
277
-
278
  return {
279
  "isFake": is_fake,
280
  "confidence": confidence,
281
  "explanation": explanation,
282
- "details": details
 
 
 
283
  }
284
-
285
  except Exception as e:
286
  print(f"Error analyzing video: {e}")
287
  return {"error": str(e)}
288
-
289
  finally:
290
  if os.path.exists(temp_video_path):
291
  os.remove(temp_video_path)
292
 
293
-
294
  @app.get("/")
295
  def health_check():
296
- return {"status": "Ensemble Deepfake Detection Backend is running!"}
 
2
  import cv2
3
  import torch
4
  import shutil
 
 
5
  from fastapi import FastAPI, UploadFile, File
6
  from fastapi.middleware.cors import CORSMiddleware
7
+ from transformers import VideoMAEImageProcessor, VideoMAEForVideoClassification
 
 
 
8
  from facenet_pytorch import MTCNN
9
  from PIL import Image
10
 
 
13
  # Enable CORS for the React frontend
14
  app.add_middleware(
15
  CORSMiddleware,
16
+ allow_origins=["*"],
17
  allow_credentials=True,
18
  allow_methods=["*"],
19
  allow_headers=["*"],
20
  )
21
 
22
+ # Initialize Models globally so they load once on startup
 
 
23
  print("Loading MTCNN Face Detector...")
24
  mtcnn = MTCNN(keep_all=False, select_largest=True, post_process=False)
25
 
26
+ print("Loading Hugging Face Temporal Deepfake Detector (VideoMAE)...")
27
+ model_name = "Ammar2k/videomae-base-finetuned-deepfake-subset"
28
+ processor = VideoMAEImageProcessor.from_pretrained(model_name)
29
+ model = VideoMAEForVideoClassification.from_pretrained(model_name)
 
 
 
 
 
 
 
30
 
31
+ # Ensure temp directory exists
32
+ os.makedirs("temp", exist_ok=True)
33
 
34
+ def extract_faces_sequence(video_path, sequence_length=16):
35
+ """Extracts a sequence of continuous frames and tracks the face temporally."""
 
36
  cap = cv2.VideoCapture(video_path)
37
+ frames_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
38
+
39
+ if frames_count == 0:
 
40
  return []
41
+
42
+ # Try to get frames from the middle of the video
43
+ start_frame = max(0, (frames_count // 2) - (sequence_length // 2))
44
  cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
45
+
46
  faces = []
47
  last_box = None
48
+
49
+ for _ in range(sequence_length):
50
  ret, frame = cap.read()
51
  if not ret:
52
  break
53
+
54
+ # Convert BGR to RGB for MTCNN and PIL
55
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
56
  pil_img = Image.fromarray(frame_rgb)
57
+
58
+ # Detect and crop face
59
  boxes, _ = mtcnn.detect(pil_img)
60
+
61
+ # Bounding box smoothing: fallback to last known box if detection fails on a frame
62
  if boxes is not None and len(boxes) > 0:
63
+ box = boxes[0]
64
+ last_box = box
65
+ elif last_box is not None:
66
+ box = last_box
67
+ else:
68
+ continue # Skip if no face found yet
69
+
70
+ # Add 30% padding around the face for better context
71
  w = box[2] - box[0]
72
  h = box[3] - box[1]
73
  pad_w = int(w * 0.3)
74
  pad_h = int(h * 0.3)
75
+
76
  x1 = max(0, int(box[0]) - pad_w)
77
  y1 = max(0, int(box[1]) - pad_h)
78
  x2 = min(pil_img.width, int(box[2]) + pad_w)
79
  y2 = min(pil_img.height, int(box[3]) + pad_h)
80
+
81
  if x2 > x1 and y2 > y1:
82
  face_crop = pil_img.crop((x1, y1, x2, y2))
83
  faces.append(face_crop)
84
+
85
  cap.release()
86
+
87
+ # VideoMAE requires exactly `sequence_length` frames. Pad by duplicating last frame if short.
88
+ if len(faces) == 0:
89
  return []
90
+
91
+ while len(faces) < sequence_length:
 
92
  faces.append(faces[-1])
93
+
94
+ return faces
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  @app.post("/api/analyze")
97
  async def analyze_video(file: UploadFile = File(...)):
98
  print(f"Received file: {file.filename}")
99
+
100
  temp_video_path = os.path.join("temp", file.filename)
101
  with open(temp_video_path, "wb") as buffer:
102
  shutil.copyfileobj(file.file, buffer)
103
+
104
  try:
105
+ print("Extracting facial sequence from video...")
106
+ # Extract 16 consecutive frames
107
+ faces = extract_faces_sequence(temp_video_path, sequence_length=16)
108
+
109
  if not faces:
110
  return {
111
+ "isFake": False,
112
+ "confidence": 0,
113
+ "explanation": "Could not detect a clear face in the video sequence.",
114
  "details": []
115
  }
116
+
117
+ print(f"Extracted {len(faces)} frame sequence. Running temporal inference...")
118
+
119
+ # Prepare for VideoMAE 3D model
120
+ inputs = processor(list(faces), return_tensors="pt")
121
+
122
+ # Run inference
123
+ with torch.no_grad():
124
+ outputs = model(**inputs)
125
+
126
+ # Video classification models return logits for the whole sequence
127
+ probabilities = torch.nn.functional.softmax(outputs.logits[0], dim=-1)
128
+
129
+ # Get highest probability label
130
+ predicted_class_idx = probabilities.argmax(-1).item()
131
+ label = model.config.id2label[predicted_class_idx].lower()
132
+
133
+ is_fake = 'fake' in label
134
+ confidence = round(probabilities[predicted_class_idx].item() * 100, 2)
135
+
136
+ explanation = "Our 3D Temporal AI analyzed facial motion and detected unnatural movement, micro-expressions, or spatial inconsistencies typical of deepfakes." if is_fake else "Our 3D Temporal AI analyzed the facial movement and found natural micro-expressions and consistent temporal flow."
137
+
138
+ print(f"Result: isFake={is_fake}, confidence={confidence}%")
139
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  return {
141
  "isFake": is_fake,
142
  "confidence": confidence,
143
  "explanation": explanation,
144
+ "details": [
145
+ {"title": "Temporal Analysis", "desc": "Analyzed a continuous 16-frame clip using a VideoMAE 3D Transformer."},
146
+ {"title": "Motion Tracking", "desc": "Tracked facial landmarks across time to detect jitter, blending artifacts, and lip-sync inconsistencies."}
147
+ ]
148
  }
149
+
150
  except Exception as e:
151
  print(f"Error analyzing video: {e}")
152
  return {"error": str(e)}
153
+
154
  finally:
155
  if os.path.exists(temp_video_path):
156
  os.remove(temp_video_path)
157
 
 
158
  @app.get("/")
159
  def health_check():
160
+ return {"status": "3D Temporal Backend is running!"}
requirements.txt CHANGED
@@ -4,9 +4,6 @@ python-multipart
4
  opencv-python-headless
5
  torch
6
  torchvision
7
- torchaudio
8
  transformers
9
  facenet-pytorch
10
  Pillow
11
- librosa
12
- soundfile
 
4
  opencv-python-headless
5
  torch
6
  torchvision
 
7
  transformers
8
  facenet-pytorch
9
  Pillow