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

Upload 2 files

Browse files
Files changed (2) hide show
  1. main.py +202 -107
  2. requirements.txt +1 -0
main.py CHANGED
@@ -2,6 +2,7 @@ import os
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
@@ -10,151 +11,245 @@ from PIL import Image
10
 
11
  app = FastAPI()
12
 
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!"}
 
2
  import cv2
3
  import torch
4
  import shutil
5
+ import numpy as np
6
  from fastapi import FastAPI, UploadFile, File
7
  from fastapi.middleware.cors import CORSMiddleware
8
  from transformers import VideoMAEImageProcessor, VideoMAEForVideoClassification
 
11
 
12
  app = FastAPI()
13
 
 
14
  app.add_middleware(
15
  CORSMiddleware,
16
+ allow_origins=["*"],
17
  allow_credentials=True,
18
  allow_methods=["*"],
19
  allow_headers=["*"],
20
  )
21
 
22
+ # ── Load models once at startup ───────────────────────────────────────────────
23
  print("Loading MTCNN Face Detector...")
24
  mtcnn = MTCNN(keep_all=False, select_largest=True, post_process=False)
25
 
26
+ print("Loading VideoMAE Temporal Deepfake Detector...")
27
  model_name = "Ammar2k/videomae-base-finetuned-deepfake-subset"
28
+ processor = VideoMAEImageProcessor.from_pretrained(model_name)
29
+ model = VideoMAEForVideoClassification.from_pretrained(model_name)
30
+ model.eval()
31
 
 
32
  os.makedirs("temp", exist_ok=True)
33
 
34
+ # ── Configuration ─────────────────────────────────────────────────────────────
35
+ SEQUENCE_LENGTH = 16 # VideoMAE requires exactly 16 frames per clip
36
+ TARGET_FPS = 8 # Sample the video at this framerate (higher = finer detail)
37
+ NUM_WINDOWS = 4 # Number of independent clips to analyze across the video
38
+ WINDOW_SECONDS = 2 # Duration (seconds) of each clip
39
+
40
+ def get_video_fps(cap):
41
+ fps = cap.get(cv2.CAP_PROP_FPS)
42
+ return fps if fps and fps > 0 else 25.0
43
+
44
+ def smooth_box(current_box, last_box, alpha=0.6):
45
+ """Exponential moving-average smoothing on bounding boxes to reduce jitter."""
46
+ if last_box is None:
47
+ return current_box
48
+ return [
49
+ alpha * c + (1 - alpha) * p
50
+ for c, p in zip(current_box, last_box)
51
+ ]
52
+
53
+ def crop_face(pil_img, box, padding=0.35):
54
+ """Crop the face from a PIL image with proportional padding."""
55
+ w = box[2] - box[0]
56
+ h = box[3] - box[1]
57
+ pad_w = int(w * padding)
58
+ pad_h = int(h * padding)
59
+ x1 = max(0, int(box[0]) - pad_w)
60
+ y1 = max(0, int(box[1]) - pad_h)
61
+ x2 = min(pil_img.width, int(box[2]) + pad_w)
62
+ y2 = min(pil_img.height, int(box[3]) + pad_h)
63
+ if x2 > x1 and y2 > y1:
64
+ return pil_img.crop((x1, y1, x2, y2))
65
+ return None
66
+
67
+ def extract_window(cap, start_frame, video_fps):
68
+ """
69
+ Extract one 16-frame clip from [start_frame] sampled at TARGET_FPS.
70
+ Returns a list of exactly SEQUENCE_LENGTH PIL face crops.
71
+ """
72
+ # How many raw video frames to skip between each sample
73
+ frame_step = max(1, int(video_fps / TARGET_FPS))
74
+ faces = []
75
+ last_box = None
76
+
77
+ frame_idx = start_frame
78
+ attempts = 0
79
+ max_attempts = SEQUENCE_LENGTH * frame_step * 3 # safety cap
80
+
81
+ while len(faces) < SEQUENCE_LENGTH and attempts < max_attempts:
82
+ cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
83
  ret, frame = cap.read()
84
  if not ret:
85
  break
86
+
 
87
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
88
+ pil_img = Image.fromarray(frame_rgb)
89
+
 
90
  boxes, _ = mtcnn.detect(pil_img)
91
+
 
92
  if boxes is not None and len(boxes) > 0:
93
+ raw_box = boxes[0].tolist()
94
+ smoothed = smooth_box(raw_box, last_box)
95
+ last_box = smoothed
96
  elif last_box is not None:
97
+ smoothed = last_box # hold last known position
98
  else:
99
+ frame_idx += frame_step
100
+ attempts += 1
101
+ continue
102
+
103
+ crop = crop_face(pil_img, smoothed)
104
+ if crop is not None:
105
+ faces.append(crop)
106
+
107
+ frame_idx += frame_step
108
+ attempts += 1
109
+
110
+ if not faces:
 
 
 
 
 
 
 
 
 
111
  return []
112
+
113
+ # Pad to exactly SEQUENCE_LENGTH by repeating the last frame
114
+ while len(faces) < SEQUENCE_LENGTH:
115
  faces.append(faces[-1])
 
 
116
 
117
+ return faces[:SEQUENCE_LENGTH]
118
+
119
+ def run_inference_on_clip(faces):
120
+ """Run VideoMAE on a single 16-frame clip. Returns (is_fake, fake_probability)."""
121
+ inputs = processor(list(faces), return_tensors="pt")
122
+ with torch.no_grad():
123
+ outputs = model(**inputs)
124
+ probs = torch.nn.functional.softmax(outputs.logits[0], dim=-1)
125
+ class_idx = probs.argmax(-1).item()
126
+ label = model.config.id2label[class_idx].lower()
127
+ is_fake = "fake" in label
128
+ # Normalize: always return the probability assigned to "fake"
129
+ fake_prob = probs[class_idx].item() if is_fake else 1.0 - probs[class_idx].item()
130
+ return is_fake, fake_prob
131
+
132
+ # ── API endpoint ──────────────────────────────────────────────────────────────
133
  @app.post("/api/analyze")
134
  async def analyze_video(file: UploadFile = File(...)):
135
+ print(f"Received: {file.filename}")
136
+ temp_path = os.path.join("temp", file.filename)
137
+
138
+ with open(temp_path, "wb") as buf:
139
+ shutil.copyfileobj(file.file, buf)
140
+
141
  try:
142
+ cap = cv2.VideoCapture(temp_path)
143
+ video_fps = get_video_fps(cap)
144
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
145
+ duration_s = total_frames / video_fps
146
+
147
+ print(f"Video: {duration_s:.1f}s @ {video_fps:.1f}fps ({total_frames} frames)")
148
+
149
+ if total_frames == 0:
150
+ return {"isFake": False, "confidence": 0,
151
+ "explanation": "Could not read the video file.", "details": []}
152
+
153
+ # ── Determine start frames for each analysis window ───────────────────
154
+ window_frames = int(WINDOW_SECONDS * video_fps) # raw frames per window
155
+ usable_end = max(0, total_frames - window_frames)
156
+
157
+ if usable_end == 0:
158
+ # Very short video: use a single window at the start
159
+ start_frames = [0]
160
+ else:
161
+ # Spread NUM_WINDOWS evenly across the usable range
162
+ start_frames = [
163
+ int(i * usable_end / (NUM_WINDOWS - 1)) if NUM_WINDOWS > 1 else usable_end // 2
164
+ for i in range(NUM_WINDOWS)
165
+ ]
166
+ # Remove duplicate starts (can happen with short videos)
167
+ start_frames = sorted(set(start_frames))
168
+
169
+ # ── Run inference on each window ──────────────────────────────────────
170
+ all_fake_probs = []
171
+ windows_analyzed = 0
172
+
173
+ for i, start in enumerate(start_frames):
174
+ print(f" Window {i+1}/{len(start_frames)} β€” starting at frame {start}")
175
+ faces = extract_window(cap, start, video_fps)
176
+
177
+ if not faces:
178
+ print(" No face detected β€” skipping window.")
179
+ continue
180
+
181
+ _, fake_prob = run_inference_on_clip(faces)
182
+ all_fake_probs.append(fake_prob)
183
+ windows_analyzed += 1
184
+ print(f" Fake probability: {fake_prob:.3f}")
185
+
186
+ cap.release()
187
+
188
+ if not all_fake_probs:
189
+ return {"isFake": False, "confidence": 0,
190
+ "explanation": "Could not detect a clear face in the video.",
191
+ "details": []}
192
+
193
+ # ── Aggregate results (weighted toward the most suspicious window) ────
194
+ arr = np.array(all_fake_probs)
195
+ avg_fake_prob = float(np.mean(arr))
196
+ max_fake_prob = float(np.max(arr))
197
+ # Blend: 70 % mean + 30 % max β†’ catches a single damning window
198
+ blended_prob = 0.70 * avg_fake_prob + 0.30 * max_fake_prob
199
+
200
+ is_fake = blended_prob > 0.50
201
+ confidence = round((blended_prob if is_fake else 1.0 - blended_prob) * 100, 2)
202
+
203
+ print(f"FINAL β€” isFake={is_fake}, blended_prob={blended_prob:.3f}, confidence={confidence}%")
204
+
205
+ explanation = (
206
+ "Our 3D Temporal AI analyzed multiple video segments and detected "
207
+ "unnatural facial motion, micro-expression inconsistencies, or "
208
+ "spatial blending artifacts characteristic of deepfakes."
209
+ if is_fake else
210
+ "Our 3D Temporal AI analyzed multiple video segments and found "
211
+ "natural micro-expressions, consistent temporal flow, and no "
212
+ "manipulation artifacts."
213
+ )
214
+
215
  return {
216
  "isFake": is_fake,
217
  "confidence": confidence,
218
  "explanation": explanation,
219
  "details": [
220
+ {
221
+ "title": "Multi-Window Temporal Analysis",
222
+ "desc": (
223
+ f"Analyzed {windows_analyzed} independent {WINDOW_SECONDS}-second "
224
+ f"clip(s) spread across the video using a VideoMAE 3D Transformer."
225
+ )
226
+ },
227
+ {
228
+ "title": "High-FPS Face Tracking",
229
+ "desc": (
230
+ f"Sampled at {TARGET_FPS} FPS with smoothed bounding-box tracking "
231
+ "to capture jitter, blinking anomalies, and lip-sync mismatches."
232
+ )
233
+ },
234
+ {
235
+ "title": "Ensemble Scoring",
236
+ "desc": (
237
+ f"Final score = 70% average + 30% peak across all windows "
238
+ f"(avg {avg_fake_prob*100:.1f}%, peak {max_fake_prob*100:.1f}%)."
239
+ )
240
+ }
241
  ]
242
  }
243
+
244
  except Exception as e:
245
+ print(f"Error: {e}")
246
+ import traceback; traceback.print_exc()
247
  return {"error": str(e)}
248
+
249
  finally:
250
+ if os.path.exists(temp_path):
251
+ os.remove(temp_path)
252
 
253
  @app.get("/")
254
  def health_check():
255
+ return {"status": "Multi-Window Temporal Backend is running!"}
requirements.txt CHANGED
@@ -7,3 +7,4 @@ torchvision
7
  transformers
8
  facenet-pytorch
9
  Pillow
 
 
7
  transformers
8
  facenet-pytorch
9
  Pillow
10
+ numpy