spideyhead commited on
Commit
f775aa8
Β·
verified Β·
1 Parent(s): 35fcc34

Upload 2 files

Browse files
Files changed (2) hide show
  1. main.py +161 -37
  2. requirements.txt +1 -0
main.py CHANGED
@@ -2,9 +2,12 @@ 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
8
  from facenet_pytorch import MTCNN
9
  from PIL import Image
10
 
@@ -28,6 +31,13 @@ processor = VideoMAEImageProcessor.from_pretrained(model_name)
28
  model = VideoMAEForVideoClassification.from_pretrained(model_name)
29
  model.eval()
30
 
 
 
 
 
 
 
 
31
  os.makedirs("temp", exist_ok=True)
32
 
33
  # ── Configuration ─────────────────────────────────────────────────────────────
@@ -37,6 +47,12 @@ SEQUENCE_LENGTH = 16 # VideoMAE requires exactly 16 frames
37
  # Real videos score ~60-75%, so we raise the bar significantly.
38
  FAKE_THRESHOLD = 0.80 # Only call FAKE if model is 80%+ confident
39
 
 
 
 
 
 
 
40
  def smooth_box(current_box, last_box, alpha=0.5):
41
  """EMA smoothing to stabilise the face bounding box across frames."""
42
  if last_box is None:
@@ -145,16 +161,92 @@ def run_inference(faces):
145
  print(f" raw fake_prob: {fake_prob:.4f}")
146
  return fake_prob
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  # ── API endpoint ──────────────────────────────────────────────────────────────
149
  @app.post("/api/analyze")
150
- async def analyze_video(file: UploadFile = File(...)):
151
- print(f"Received: {file.filename}")
152
- temp_path = os.path.join("temp", file.filename)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
- with open(temp_path, "wb") as buf:
155
- shutil.copyfileobj(file.file, buf)
156
 
157
- try:
158
  cap = cv2.VideoCapture(temp_path)
159
  video_fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
160
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
@@ -193,28 +285,60 @@ async def analyze_video(file: UploadFile = File(...)):
193
  if highest_fake_prob > 0.95:
194
  break
195
 
196
- if successful_clips == 0:
197
- return {
198
- "isFake": False, "confidence": 0,
199
- "explanation": "Could not detect a clear face in the video. Ensure the subject's face is visible.",
200
- "details": []
201
- }
202
-
203
- # ── Apply calibrated threshold ─────────────────────────────────────────
204
- # This model scores ~60-75% for real videos, so we require 80%+ to call FAKE.
205
- is_fake = highest_fake_prob >= FAKE_THRESHOLD
206
- confidence = round((highest_fake_prob if is_fake else 1.0 - highest_fake_prob) * 100, 2)
207
-
208
- print(f"FINAL β€” fake_prob={highest_fake_prob:.3f}, threshold={FAKE_THRESHOLD}, isFake={is_fake}, confidence={confidence}%")
209
-
210
- explanation = (
211
- "Our Temporal AI detected strong evidence of facial manipulation β€” "
212
- "unnatural micro-expressions, blending artifacts, or temporal inconsistencies "
213
- "characteristic of deepfake synthesis."
214
- if is_fake else
215
- "Our Temporal AI found no significant manipulation artifacts. "
216
- "Facial motion, micro-expressions, and temporal consistency appear natural across the analyzed segments."
217
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
  return {
220
  "isFake": is_fake,
@@ -222,16 +346,16 @@ async def analyze_video(file: UploadFile = File(...)):
222
  "explanation": explanation,
223
  "details": [
224
  {
225
- "title": "Multi-Segment Temporal Analysis",
226
- "desc": f"Analyzed {successful_clips} separate 16-frame consecutive clips across the video using a VideoMAE 3D Transformer."
227
  },
228
  {
229
- "title": "Optimized Face Tracking",
230
- "desc": "Frames read sequentially with bounding-box caching and downscaled initial detection for extremely fast analysis."
231
  },
232
  {
233
- "title": "Calibrated Threshold",
234
- "desc": f"Peak raw model score: {highest_fake_prob*100:.1f}%. Detection threshold: {FAKE_THRESHOLD*100:.0f}% (tuned to reduce false positives)."
235
  }
236
  ]
237
  }
@@ -242,7 +366,7 @@ async def analyze_video(file: UploadFile = File(...)):
242
  return {"error": str(e)}
243
 
244
  finally:
245
- if os.path.exists(temp_path):
246
  os.remove(temp_path)
247
 
248
  @app.get("/")
 
2
  import cv2
3
  import torch
4
  import shutil
5
+ import uuid
6
+ import yt_dlp
7
+ from typing import Optional
8
+ from fastapi import FastAPI, UploadFile, File, Form
9
  from fastapi.middleware.cors import CORSMiddleware
10
+ from transformers import VideoMAEImageProcessor, VideoMAEForVideoClassification, pipeline
11
  from facenet_pytorch import MTCNN
12
  from PIL import Image
13
 
 
31
  model = VideoMAEForVideoClassification.from_pretrained(model_name)
32
  model.eval()
33
 
34
+ print("Loading AI Image Detector (for fully synthetic AI-generated videos)...")
35
+ ai_image_detector = pipeline(
36
+ "image-classification",
37
+ model="Smogy/SMOGY-Ai-images-detector",
38
+ device=-1 # CPU
39
+ )
40
+
41
  os.makedirs("temp", exist_ok=True)
42
 
43
  # ── Configuration ─────────────────────────────────────────────────────────────
 
47
  # Real videos score ~60-75%, so we raise the bar significantly.
48
  FAKE_THRESHOLD = 0.80 # Only call FAKE if model is 80%+ confident
49
 
50
+ # Threshold for the AI image detector (probability that a frame is AI-generated)
51
+ # Using a combo: flag if MAX single frame >= 0.55 OR average >= 0.30
52
+ AI_IMAGE_AVG_THRESHOLD = 0.30 # Flag if avg across all frames is >= 30%
53
+ AI_IMAGE_MAX_THRESHOLD = 0.55 # Flag if ANY single frame hits >= 55%
54
+ AI_FRAME_SAMPLES = 8 # Number of frames to sample from the video
55
+
56
  def smooth_box(current_box, last_box, alpha=0.5):
57
  """EMA smoothing to stabilise the face bounding box across frames."""
58
  if last_box is None:
 
161
  print(f" raw fake_prob: {fake_prob:.4f}")
162
  return fake_prob
163
 
164
+
165
+ def run_ai_image_check(video_path, total_frames):
166
+ """
167
+ Sample AI_FRAME_SAMPLES evenly-spaced frames from the video and run them
168
+ through the AI image detector. Returns (is_ai_generated, avg_ai_score, triggered_frames).
169
+ This catches fully synthetic videos (Gemini Veo, Sora, Runway, etc.) that
170
+ VideoMAE misses because they have no face-swap artifacts.
171
+ """
172
+ step = max(1, total_frames // AI_FRAME_SAMPLES)
173
+ frame_indices = [min(i * step, total_frames - 1) for i in range(AI_FRAME_SAMPLES)]
174
+
175
+ ai_scores = []
176
+ cap = cv2.VideoCapture(video_path)
177
+
178
+ for idx in frame_indices:
179
+ cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
180
+ ret, frame = cap.read()
181
+ if not ret:
182
+ continue
183
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
184
+ pil_img = Image.fromarray(frame_rgb)
185
+
186
+ results = ai_image_detector(pil_img)
187
+ # Model labels vary; find the AI/Fake label score
188
+ ai_score = 0.0
189
+ for res in results:
190
+ if any(kw in res['label'].lower() for kw in ['ai', 'fake', 'artificial', 'generated', 'synthetic']):
191
+ ai_score = res['score']
192
+ break
193
+ ai_scores.append(ai_score)
194
+ print(f" Frame {idx}: AI image score = {ai_score:.4f}")
195
+
196
+ cap.release()
197
+
198
+ if not ai_scores:
199
+ return False, 0.0, 0.0, 0
200
+
201
+ avg_score = sum(ai_scores) / len(ai_scores)
202
+ max_score = max(ai_scores)
203
+ triggered = sum(1 for s in ai_scores if s >= AI_IMAGE_AVG_THRESHOLD)
204
+ # Flag as AI-generated if avg is high OR any single frame was very strongly AI-detected
205
+ is_ai = avg_score >= AI_IMAGE_AVG_THRESHOLD or max_score >= AI_IMAGE_MAX_THRESHOLD
206
+ print(f" AI Image Check β€” avg={avg_score:.4f}, max={max_score:.4f}, triggered={triggered}/{len(ai_scores)}, is_ai={is_ai}")
207
+ return is_ai, avg_score, max_score, triggered
208
+
209
  # ── API endpoint ──────────────────────────────────────────────────────────────
210
  @app.post("/api/analyze")
211
+ async def analyze_video(
212
+ file: Optional[UploadFile] = File(None),
213
+ url: Optional[str] = Form(None)
214
+ ):
215
+ temp_path = None
216
+ try:
217
+ if file is not None and file.filename:
218
+ print(f"Received file: {file.filename}")
219
+ temp_path = os.path.join("temp", f"{uuid.uuid4()}_{file.filename}")
220
+ with open(temp_path, "wb") as buf:
221
+ shutil.copyfileobj(file.file, buf)
222
+
223
+ elif url is not None and url.strip():
224
+ print(f"Received URL: {url}")
225
+ temp_id = str(uuid.uuid4())
226
+ temp_path_template = os.path.join("temp", f"{temp_id}.%(ext)s")
227
+
228
+ ydl_opts = {
229
+ 'format': 'best', # Simply download the best single file, avoiding ffmpeg merge requirements
230
+ 'outtmpl': temp_path_template,
231
+ 'noplaylist': True,
232
+ 'quiet': True,
233
+ 'max_filesize': 100 * 1024 * 1024 # Limit to 100MB
234
+ }
235
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
236
+ ydl.download([url])
237
+
238
+ # Find the actual downloaded file since extension might vary
239
+ for f in os.listdir("temp"):
240
+ if temp_id in f:
241
+ temp_path = os.path.join("temp", f)
242
+ break
243
+
244
+ if not temp_path or not os.path.exists(temp_path):
245
+ return {"error": "Failed to download the video from the provided URL."}
246
 
247
+ else:
248
+ return {"error": "Please provide either a video file or a valid URL."}
249
 
 
250
  cap = cv2.VideoCapture(temp_path)
251
  video_fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
252
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
 
285
  if highest_fake_prob > 0.95:
286
  break
287
 
288
+ # ── Run AI Image Detector on sampled frames ────────────────────────────
289
+ # This catches fully synthetic AI-generated videos (Gemini, Sora, Runway, etc.)
290
+ # that VideoMAE misses because they have no face-swap artifacts.
291
+ print("Running AI Image Detector on sampled frames...")
292
+ is_ai_generated, ai_avg_score, ai_max_score, ai_triggered = run_ai_image_check(temp_path, total_frames)
293
+
294
+ # ── Combine both signals ───────────────────────────────────────────────
295
+ # VideoMAE: catches face-swaps and traditional deepfakes
296
+ # AI Image Detector: catches fully synthetic AI-generated content
297
+ videomae_flagged = successful_clips > 0 and highest_fake_prob >= FAKE_THRESHOLD
298
+ is_fake = videomae_flagged or is_ai_generated
299
+
300
+ # Determine which method triggered and compute confidence
301
+ if videomae_flagged and is_ai_generated:
302
+ detection_method = "Dual-Model (Temporal + AI Image)"
303
+ confidence = round(max(highest_fake_prob, ai_avg_score) * 100, 2)
304
+ explanation = (
305
+ "Both our Temporal VideoMAE and AI Image Detector flagged this video. "
306
+ "It shows face-swap artifacts AND frame-level characteristics of AI-generated content."
307
+ )
308
+ elif is_ai_generated:
309
+ detection_method = "AI Image Detector"
310
+ confidence = round(max(ai_avg_score, ai_max_score) * 100, 2)
311
+ explanation = (
312
+ "Our AI Image Detector identified this video as fully synthetic β€” "
313
+ f"frame-level analysis found strong AI-generation signatures "
314
+ f"(peak score: {ai_max_score*100:.1f}%, avg: {ai_avg_score*100:.1f}%) "
315
+ "consistent with tools like Gemini Veo, Sora, Runway, or similar generative AI systems."
316
+ )
317
+ elif videomae_flagged:
318
+ detection_method = "VideoMAE Temporal Analysis"
319
+ confidence = round(highest_fake_prob * 100, 2)
320
+ explanation = (
321
+ "Our Temporal AI detected strong evidence of facial manipulation β€” "
322
+ "unnatural micro-expressions, blending artifacts, or temporal inconsistencies "
323
+ "characteristic of deepfake face-swap synthesis."
324
+ )
325
+ else:
326
+ # Neither triggered β€” real video
327
+ detection_method = "Dual-Model"
328
+ # Show highest confidence-of-real from both signals
329
+ real_conf = max(
330
+ (1.0 - highest_fake_prob) if successful_clips > 0 else 0.0,
331
+ (1.0 - ai_avg_score)
332
+ )
333
+ confidence = round(real_conf * 100, 2)
334
+ explanation = (
335
+ "Our dual-model analysis found no significant manipulation. "
336
+ "VideoMAE detected no temporal face-swap artifacts, and the AI Image Detector "
337
+ "found no frame-level synthetic generation signatures."
338
+ )
339
+
340
+ print(f"FINAL β€” videomae={highest_fake_prob:.3f}, ai_avg={ai_avg_score:.3f}, ai_max={ai_max_score:.3f}, "
341
+ f"method={detection_method}, isFake={is_fake}, confidence={confidence}%")
342
 
343
  return {
344
  "isFake": is_fake,
 
346
  "explanation": explanation,
347
  "details": [
348
  {
349
+ "title": "VideoMAE Temporal Analysis",
350
+ "desc": f"Analyzed {successful_clips} clip(s) with a 3D VideoMAE Transformer. Peak score: {highest_fake_prob*100:.1f}%."
351
  },
352
  {
353
+ "title": "AI Image Frame Analysis",
354
+ "desc": f"Sampled {AI_FRAME_SAMPLES} frames for AI-generation signatures. Peak frame score: {ai_max_score*100:.1f}%, avg: {ai_avg_score*100:.1f}%. Detects Gemini Veo, Sora, Runway, etc."
355
  },
356
  {
357
+ "title": "Detection Method",
358
+ "desc": f"Result by: {detection_method}. Thresholds: VideoMAE β‰₯{FAKE_THRESHOLD*100:.0f}% | AI avg β‰₯{AI_IMAGE_AVG_THRESHOLD*100:.0f}% or max β‰₯{AI_IMAGE_MAX_THRESHOLD*100:.0f}%."
359
  }
360
  ]
361
  }
 
366
  return {"error": str(e)}
367
 
368
  finally:
369
+ if temp_path and os.path.exists(temp_path):
370
  os.remove(temp_path)
371
 
372
  @app.get("/")
requirements.txt CHANGED
@@ -8,3 +8,4 @@ transformers
8
  facenet-pytorch
9
  Pillow
10
  numpy
 
 
8
  facenet-pytorch
9
  Pillow
10
  numpy
11
+ yt-dlp