farouk04 commited on
Commit
043203b
Β·
verified Β·
1 Parent(s): d66879a

Update detector.py

Browse files
Files changed (1) hide show
  1. detector.py +170 -96
detector.py CHANGED
@@ -1,21 +1,17 @@
1
  """
2
  Deepfake Detector β€” Hugging Face ZeroGPU Space
3
  V-JEPA 2.1 ViT-Giant + AttentiveClassifier probe
4
-
5
- ZEROGPU RULES (strictly followed here):
6
- - No torch.cuda.is_available() in main process
7
- - No map_location="cuda" in main process
8
- - No .to("cuda") that actually executes in main process
9
- (ZeroGPU *intercepts* .to("cuda") calls and postpones them β€”
10
- models stay on CPU until @spaces.GPU runs)
11
- - Only @spaces.GPU functions touch real GPU operations
12
  """
13
-
14
  import glob
15
  import os
16
  import subprocess
17
  import tempfile
18
  import warnings
 
 
 
 
 
19
 
20
  warnings.filterwarnings("ignore", category=FutureWarning)
21
  warnings.filterwarnings("ignore", category=UserWarning)
@@ -23,12 +19,11 @@ warnings.filterwarnings("ignore", category=UserWarning)
23
  import gradio as gr
24
  import spaces
25
  import torch
26
- from huggingface_hub import hf_hub_download
27
-
28
  from inference_core import AttentiveClassifier, get_video_windows
29
 
30
  # ─────────────────────────────────────────────
31
- # CONFIG
32
  # ─────────────────────────────────────────────
33
  DEVICE = "cuda"
34
  FEATURE_DIM = 1408
@@ -38,12 +33,15 @@ HF_TOKEN = os.environ.get("HF_TOKEN")
38
  ENCODER_REPO = os.environ.get("ENCODER_REPO", "")
39
  PROBE_REPO = os.environ.get("PROBE_REPO", ENCODER_REPO)
40
 
 
 
 
 
41
  # ─────────────────────────────────────────────
42
- # MODEL LOADING (CPU β€” ZeroGPU intercepts CUDA calls)
43
  # ─────────────────────────────────────────────
44
  print("=" * 50)
45
  print("Loading V-JEPA 2.1 encoder architecture via torch.hub...")
46
-
47
  encoder, _ = torch.hub.load(
48
  "facebookresearch/vjepa2",
49
  "vjepa2_1_vit_giant_384",
@@ -67,7 +65,7 @@ del ckpt
67
 
68
  encoder = encoder.to(DEVICE).to(torch.bfloat16)
69
  encoder.eval()
70
- print("Encoder ready (on CPU, ZeroGPU will move to GPU at inference time).")
71
 
72
  print("Loading AttentiveClassifier probe...")
73
  probe_path = hf_hub_download(
@@ -75,6 +73,7 @@ probe_path = hf_hub_download(
75
  filename="attentive_probe_optimized.pt",
76
  token=HF_TOKEN,
77
  )
 
78
  probe_state = torch.load(probe_path, map_location="cpu")
79
  if "probe_state_dict" in probe_state:
80
  probe_state = probe_state["probe_state_dict"]
@@ -86,182 +85,257 @@ probe.eval()
86
  print("Probe ready.")
87
  print("=" * 50)
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  # ─────────────────────────────────────────────
91
- # VIDEO DOWNLOAD (pure CPU β€” no GPU quota used)
92
  # ─────────────────────────────────────────────
93
  def download_video(url: str) -> tuple[bool, str]:
94
- """Download via yt-dlp to a temp file. Returns (success, path_or_error)."""
95
  tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
96
  tmp.close()
97
 
98
  cmd = [
99
  "yt-dlp",
100
- "-f",
101
- "best[height<=720]/best",
102
- "--max-filesize",
103
- "100M",
104
  "--no-playlist",
105
  "--no-warnings",
106
  "--force-overwrites",
107
- "-o",
108
- tmp.name,
109
  url,
110
  ]
 
111
  try:
112
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=90)
113
-
114
- # Always print yt-dlp output so it appears in the Space Logs tab
115
- print(f"[yt-dlp] returncode : {result.returncode}")
116
- print(f"[yt-dlp] stdout : {result.stdout[:800]}")
117
- print(f"[yt-dlp] stderr : {result.stderr[:800]}")
118
-
119
- if result.returncode != 0:
120
- return False, (
121
- f"yt-dlp exited {result.returncode}\nstderr: {result.stderr[:600]}"
122
- )
123
-
124
- # yt-dlp sometimes writes to a different extension than the template
125
- # (e.g. tmp.mp4 β†’ tmp.mp4.webm when remux isn't needed).
126
- # Search for any file that starts with tmp.name and has content.
127
  candidates = sorted(glob.glob(tmp.name + "*") + [tmp.name])
128
- actual_file = next(
129
- (f for f in candidates if os.path.exists(f) and os.path.getsize(f) > 0),
130
- None,
131
- )
132
 
133
  if actual_file is None:
134
- return False, (
135
- f"yt-dlp wrote no output (empty or missing file).\n"
136
- f"stdout: {result.stdout[:400]}\n"
137
- f"stderr: {result.stderr[:400]}"
138
- )
139
 
140
- print(
141
- f"[yt-dlp] actual file: {actual_file} ({os.path.getsize(actual_file)} bytes)"
142
- )
143
  return True, actual_file
144
-
145
  except subprocess.TimeoutExpired:
146
  return False, "Download timed out (90s limit)."
147
  except FileNotFoundError:
148
- return False, "yt-dlp not installed. Check requirements.txt."
149
-
150
 
151
  # ─────────────────────────────────────────────
152
- # GPU INFERENCE (inside ZeroGPU allocation)
153
  # ─────────────────────────────────────────────
154
  @spaces.GPU(duration=120)
155
  def encode_and_classify(windows: list) -> list:
156
  scores = []
157
-
158
  for window in windows:
159
  clips = window["clips"]
160
  batch = torch.stack(clips).to(DEVICE)
161
-
162
  with torch.no_grad():
163
  with torch.amp.autocast("cuda", dtype=torch.bfloat16):
164
  chunks = []
165
  for i in range(0, batch.shape[0], ENCODER_BATCH):
166
  chunks.append(encoder(batch[i : i + ENCODER_BATCH]))
167
  all_feats = torch.cat(chunks, dim=0)
168
-
169
  video_feats = all_feats.view(1, -1, FEATURE_DIM)
170
-
171
  logit, _ = probe(video_feats)
172
  prob = torch.sigmoid(logit).item()
 
 
 
 
 
 
173
 
174
- scores.append(
175
- {
176
- "prob": prob,
177
- "start_sec": window["start_sec"],
178
- "end_sec": window["end_sec"],
179
- }
180
- )
181
 
182
- return scores
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
  # ─────────────────────────────────────────────
186
  # GRADIO PREDICT
187
  # ─────────────────────────────────────────────
188
- def predict(url: str) -> tuple[str, str, str]:
 
 
 
 
 
189
  url = (url or "").strip()
 
 
 
 
 
 
 
190
  if not url:
191
- return "Please enter a URL.", "", ""
192
 
193
  ok, path_or_err = download_video(url)
194
  if not ok:
195
- return f"Download failed: {path_or_err}", "", ""
196
 
197
  video_path = path_or_err
198
- try:
199
- try:
200
- windows, total_frames, fps, duration_sec = get_video_windows(video_path)
201
- except Exception as e:
202
- return f"Video decode failed: {e}", "", ""
203
-
204
- scores = encode_and_classify(windows)
205
 
206
- finally:
207
- # Clean up all files that share the temp base name
208
- for f in glob.glob(video_path + "*") + [video_path]:
209
- if os.path.exists(f):
210
- os.remove(f)
 
 
 
211
 
212
  if not scores:
213
- return "No windows processed.", "", ""
 
 
214
 
215
  final_prob = max(s["prob"] for s in scores)
216
  label = "⚠️ AI-GENERATED (FAKE)" if final_prob > 0.5 else "βœ… REAL"
 
217
  confidence = f"{final_prob * 100:.1f}%"
218
-
219
  lines = [
 
220
  f"Duration: {duration_sec:.1f}s | Frames: {total_frames} | Windows: {len(scores)}\n"
221
  ]
222
  for s in scores:
223
  flag = " ← peak" if (s["prob"] == final_prob and final_prob > 0.5) else ""
224
- lines.append(
225
- f" [{s['start_sec']:.1f}s – {s['end_sec']:.1f}s] β†’ {s['prob'] * 100:.1f}% fake{flag}"
226
- )
 
 
 
 
 
 
 
 
 
227
 
228
- return label, confidence, "\n".join(lines)
 
229
 
 
 
 
 
 
 
230
 
231
  # ─────────────────────────────────────────────
232
  # GRADIO UI
233
  # ─────────────────────────────────────────────
234
  with gr.Blocks(title="Deepfake Detector") as demo:
 
 
235
  gr.Markdown(
236
  """
237
  ## 🎬 Deepfake Video Detector
238
- Paste a public Instagram, TikTok, or direct `.mp4` URL.
239
  Uses **V-JEPA 2.1 ViT-Giant** + **AttentiveClassifier** probe.
240
  """
241
  )
242
-
243
  url_box = gr.Textbox(
244
  label="Video URL",
245
  placeholder="https://www.instagram.com/reels/...",
246
  autofocus=True,
247
  )
 
 
 
 
 
 
 
 
248
  run_btn = gr.Button("Analyze", variant="primary")
249
-
250
  with gr.Row():
251
  label_out = gr.Textbox(label="Prediction", scale=2)
252
  conf_out = gr.Textbox(label="Confidence", scale=1)
253
-
254
- breakdown_out = gr.Textbox(label="Window Breakdown", lines=6, interactive=False)
255
 
256
  run_btn.click(
257
  fn=predict,
258
- inputs=url_box,
259
- outputs=[label_out, conf_out, breakdown_out],
260
  )
261
  url_box.submit(
262
  fn=predict,
263
- inputs=url_box,
264
- outputs=[label_out, conf_out, breakdown_out],
265
  )
266
 
267
- demo.launch(share=True)
 
1
  """
2
  Deepfake Detector β€” Hugging Face ZeroGPU Space
3
  V-JEPA 2.1 ViT-Giant + AttentiveClassifier probe
 
 
 
 
 
 
 
 
4
  """
 
5
  import glob
6
  import os
7
  import subprocess
8
  import tempfile
9
  import warnings
10
+ import time
11
+ import uuid
12
+ import json
13
+ import requests
14
+ from datetime import datetime
15
 
16
  warnings.filterwarnings("ignore", category=FutureWarning)
17
  warnings.filterwarnings("ignore", category=UserWarning)
 
19
  import gradio as gr
20
  import spaces
21
  import torch
22
+ from huggingface_hub import hf_hub_download, HfApi
 
23
  from inference_core import AttentiveClassifier, get_video_windows
24
 
25
  # ─────────────────────────────────────────────
26
+ # CONFIG & SECRETS
27
  # ─────────────────────────────────────────────
28
  DEVICE = "cuda"
29
  FEATURE_DIM = 1408
 
33
  ENCODER_REPO = os.environ.get("ENCODER_REPO", "")
34
  PROBE_REPO = os.environ.get("PROBE_REPO", ENCODER_REPO)
35
 
36
+ DATASET_REPO_ID = os.environ.get("DATASET_REPO_ID", "")
37
+ TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
38
+ TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
39
+
40
  # ─────────────────────────────────────────────
41
+ # MODEL LOADING (CPU)
42
  # ─────────────────────────────────────────────
43
  print("=" * 50)
44
  print("Loading V-JEPA 2.1 encoder architecture via torch.hub...")
 
45
  encoder, _ = torch.hub.load(
46
  "facebookresearch/vjepa2",
47
  "vjepa2_1_vit_giant_384",
 
65
 
66
  encoder = encoder.to(DEVICE).to(torch.bfloat16)
67
  encoder.eval()
68
+ print("Encoder ready.")
69
 
70
  print("Loading AttentiveClassifier probe...")
71
  probe_path = hf_hub_download(
 
73
  filename="attentive_probe_optimized.pt",
74
  token=HF_TOKEN,
75
  )
76
+
77
  probe_state = torch.load(probe_path, map_location="cpu")
78
  if "probe_state_dict" in probe_state:
79
  probe_state = probe_state["probe_state_dict"]
 
85
  print("Probe ready.")
86
  print("=" * 50)
87
 
88
+ # ─────────────────────────────────────────────
89
+ # HELPER UTILS
90
+ # ─────────────────────────────────────────────
91
+ def extract_platform(url: str) -> str:
92
+ url_lower = url.lower()
93
+ if "instagram.com" in url_lower or "instagr.am" in url_lower:
94
+ return "Instagram"
95
+ elif "tiktok.com" in url_lower:
96
+ return "TikTok"
97
+ elif "youtube.com" in url_lower or "youtu.be" in url_lower:
98
+ return "YouTube"
99
+ else:
100
+ return "Direct Link / Other"
101
 
102
  # ─────────────────────────────────────────────
103
+ # VIDEO DOWNLOAD
104
  # ─────────────────────────────────────────────
105
  def download_video(url: str) -> tuple[bool, str]:
 
106
  tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
107
  tmp.close()
108
 
109
  cmd = [
110
  "yt-dlp",
111
+ "-f", "best[height<=720]/best",
112
+ "--max-filesize", "100M",
113
+ "--match-filter", "duration <= 120",
 
114
  "--no-playlist",
115
  "--no-warnings",
116
  "--force-overwrites",
117
+ "-o", tmp.name,
 
118
  url,
119
  ]
120
+
121
  try:
122
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=90)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  candidates = sorted(glob.glob(tmp.name + "*") + [tmp.name])
124
+ actual_file = next((f for f in candidates if os.path.exists(f) and os.path.getsize(f) > 0), None)
 
 
 
125
 
126
  if actual_file is None:
127
+ if "does not pass filter" in result.stdout or "does not pass filter" in result.stderr:
128
+ return False, "Video length exceeds the maximum 2-minute limit."
129
+ return False, f"yt-dlp wrote no output. stderr: {result.stderr[:400]}"
 
 
130
 
 
 
 
131
  return True, actual_file
 
132
  except subprocess.TimeoutExpired:
133
  return False, "Download timed out (90s limit)."
134
  except FileNotFoundError:
135
+ return False, "yt-dlp not installed."
 
136
 
137
  # ─────────────────────────────────────────────
138
+ # GPU INFERENCE
139
  # ─────────────────────────────────────────────
140
  @spaces.GPU(duration=120)
141
  def encode_and_classify(windows: list) -> list:
142
  scores = []
 
143
  for window in windows:
144
  clips = window["clips"]
145
  batch = torch.stack(clips).to(DEVICE)
 
146
  with torch.no_grad():
147
  with torch.amp.autocast("cuda", dtype=torch.bfloat16):
148
  chunks = []
149
  for i in range(0, batch.shape[0], ENCODER_BATCH):
150
  chunks.append(encoder(batch[i : i + ENCODER_BATCH]))
151
  all_feats = torch.cat(chunks, dim=0)
 
152
  video_feats = all_feats.view(1, -1, FEATURE_DIM)
 
153
  logit, _ = probe(video_feats)
154
  prob = torch.sigmoid(logit).item()
155
+ scores.append({
156
+ "prob": prob,
157
+ "start_sec": window["start_sec"],
158
+ "end_sec": window["end_sec"],
159
+ })
160
+ return scores
161
 
162
+ # ─────────────────────────────────────────────
163
+ # DATA UPLOAD & ALERTS (CPU Network Task)
164
+ # ─────────────────────────────────────────────
165
+ def upload_and_notify(video_path: str, meta: dict):
166
+ if not DATASET_REPO_ID or not HF_TOKEN:
167
+ print("Upload skipped: Credentials missing.")
168
+ return
169
 
170
+ try:
171
+ api = HfApi(token=HF_TOKEN)
172
+ unique_id = str(uuid.uuid4())[:8]
173
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
174
+ base_filename = f"{timestamp}_{unique_id}"
175
+
176
+ repo_video_path = f"data/video_{base_filename}.mp4"
177
+ repo_json_path = f"data/meta_{base_filename}.json"
178
+
179
+ # Generate the direct download/view asset URL for the video
180
+ direct_video_url = f"https://huggingface.co/datasets/{DATASET_REPO_ID}/resolve/main/{repo_video_path}"
181
+
182
+ # Store the video direct link inside the metadata dictionary itself
183
+ meta["stored_video_url"] = direct_video_url
184
+
185
+ # 1. Upload Video File
186
+ api.upload_file(
187
+ path_or_fileobj=video_path,
188
+ path_in_repo=repo_video_path,
189
+ repo_id=DATASET_REPO_ID,
190
+ repo_type="dataset"
191
+ )
192
+
193
+ # 2. Upload Updated Metadata JSON File
194
+ api.upload_file(
195
+ path_or_fileobj=json.dumps(meta, indent=2).encode("utf-8"),
196
+ path_in_repo=repo_json_path,
197
+ repo_id=DATASET_REPO_ID,
198
+ repo_type="dataset"
199
+ )
200
 
201
+ # 3. Send Telegram alert containing the direct video asset link
202
+ if TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID:
203
+ tele_msg = (
204
+ f"πŸš€ *New FYP Data Collected*\n\n"
205
+ f"β€’ *Platform:* {meta['platform']}\n"
206
+ f"β€’ *Prediction:* {meta['label']}\n"
207
+ f"β€’ *Confidence:* {meta['confidence']}\n"
208
+ f"β€’ *Proc Time:* {meta['processing_time_sec']}s\n"
209
+ f"β€’ *Video Len:* {meta['video_duration_sec']}s\n\n"
210
+ f"πŸ”— [Original Link]({meta['url']})\n"
211
+ f"🎬 [Direct Video Link]({direct_video_url})"
212
+ )
213
+ requests.post(
214
+ f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage",
215
+ json={"chat_id": TELEGRAM_CHAT_ID, "text": tele_msg, "parse_mode": "Markdown", "disable_web_page_preview": True},
216
+ timeout=10
217
+ )
218
+ except Exception as e:
219
+ print(f"Data upload/notification failed: {e}")
220
 
221
  # ─────────────────────────────────────────────
222
  # GRADIO PREDICT
223
  # ─────────────────────────────────────────────
224
+ def predict(url: str, consent: bool, current_state: dict) -> tuple[str, str, str, dict]:
225
+ # Check mandatory consent gate
226
+ if not consent:
227
+ return "❌ Required: You must check the data collection consent box to process videos.", "", "", current_state
228
+
229
+ start_time = time.time()
230
  url = (url or "").strip()
231
+
232
+ if current_state and "video_path" in current_state:
233
+ old_path = current_state["video_path"]
234
+ for f in glob.glob(old_path + "*") + [old_path]:
235
+ if os.path.exists(f):
236
+ os.remove(f)
237
+
238
  if not url:
239
+ return "Please enter a URL.", "", "", {}
240
 
241
  ok, path_or_err = download_video(url)
242
  if not ok:
243
+ return f"Download failed: {path_or_err}", "", "", {}
244
 
245
  video_path = path_or_err
 
 
 
 
 
 
 
246
 
247
+ try:
248
+ windows, total_frames, fps, duration_sec = get_video_windows(video_path)
249
+ except Exception as e:
250
+ if os.path.exists(video_path):
251
+ os.remove(video_path)
252
+ return f"Video decode failed: {e}", "", "", {}
253
+
254
+ scores = encode_and_classify(windows)
255
 
256
  if not scores:
257
+ if os.path.exists(video_path):
258
+ os.remove(video_path)
259
+ return "No windows processed.", "", "", {}
260
 
261
  final_prob = max(s["prob"] for s in scores)
262
  label = "⚠️ AI-GENERATED (FAKE)" if final_prob > 0.5 else "βœ… REAL"
263
+ elapsed_time = time.time() - start_time
264
  confidence = f"{final_prob * 100:.1f}%"
265
+
266
  lines = [
267
+ f"Processing Time: {elapsed_time:.1f}s",
268
  f"Duration: {duration_sec:.1f}s | Frames: {total_frames} | Windows: {len(scores)}\n"
269
  ]
270
  for s in scores:
271
  flag = " ← peak" if (s["prob"] == final_prob and final_prob > 0.5) else ""
272
+ lines.append(f" [{s['start_sec']:.1f}s – {s['end_sec']:.1f}s] β†’ {s['prob'] * 100:.1f}% fake{flag}")
273
+
274
+ # Build base metadata dict
275
+ meta = {
276
+ "url": url,
277
+ "platform": extract_platform(url),
278
+ "label": label,
279
+ "confidence": confidence,
280
+ "processing_time_sec": round(elapsed_time, 2),
281
+ "video_duration_sec": round(duration_sec, 2),
282
+ "total_frames": total_frames
283
+ }
284
 
285
+ # Execute backend background syncing task
286
+ upload_and_notify(video_path, meta)
287
 
288
+ # Clean up local system file tracking loop paths
289
+ for f in glob.glob(video_path + "*") + [video_path]:
290
+ if os.path.exists(f):
291
+ os.remove(f)
292
+
293
+ return label, confidence, "\n".join(lines), {}
294
 
295
  # ─────────────────────────────────────────────
296
  # GRADIO UI
297
  # ─────────────────────────────────────────────
298
  with gr.Blocks(title="Deepfake Detector") as demo:
299
+ session_state = gr.State(value={})
300
+
301
  gr.Markdown(
302
  """
303
  ## 🎬 Deepfake Video Detector
304
+ Paste a public Instagram, TikTok, or direct `.mp4` URL. (Maximum duration: 2 minutes).
305
  Uses **V-JEPA 2.1 ViT-Giant** + **AttentiveClassifier** probe.
306
  """
307
  )
308
+
309
  url_box = gr.Textbox(
310
  label="Video URL",
311
  placeholder="https://www.instagram.com/reels/...",
312
  autofocus=True,
313
  )
314
+
315
+ # Mandatory Consent Checkbox
316
+ consent_box = gr.Checkbox(
317
+ label="I allow the developer to securely save this video and its results for an academic Final Year Project.",
318
+ value=False,
319
+ interactive=True
320
+ )
321
+
322
  run_btn = gr.Button("Analyze", variant="primary")
323
+
324
  with gr.Row():
325
  label_out = gr.Textbox(label="Prediction", scale=2)
326
  conf_out = gr.Textbox(label="Confidence", scale=1)
327
+
328
+ breakdown_out = gr.Textbox(label="Window Breakdown & Processing Time", lines=6, interactive=False)
329
 
330
  run_btn.click(
331
  fn=predict,
332
+ inputs=[url_box, consent_box, session_state],
333
+ outputs=[label_out, conf_out, breakdown_out, session_state],
334
  )
335
  url_box.submit(
336
  fn=predict,
337
+ inputs=[url_box, consent_box, session_state],
338
+ outputs=[label_out, conf_out, breakdown_out, session_state],
339
  )
340
 
341
+ demo.launch()