Spaces:
Sleeping
Sleeping
Update detector.py
Browse files- detector.py +58 -46
detector.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Deepfake Detector β Hugging Face ZeroGPU Space
|
| 3 |
V-JEPA 2.1 ViT-Giant + AttentiveClassifier probe
|
| 4 |
"""
|
| 5 |
import glob
|
|
@@ -11,6 +10,7 @@ import time
|
|
| 11 |
import uuid
|
| 12 |
import json
|
| 13 |
import requests
|
|
|
|
| 14 |
from datetime import datetime
|
| 15 |
|
| 16 |
warnings.filterwarnings("ignore", category=FutureWarning)
|
|
@@ -32,7 +32,6 @@ ENCODER_BATCH = 3
|
|
| 32 |
HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 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", "")
|
|
@@ -56,13 +55,11 @@ encoder_weights_path = hf_hub_download(
|
|
| 56 |
filename="vjepa2_1_vitg_384.pt",
|
| 57 |
token=HF_TOKEN,
|
| 58 |
)
|
| 59 |
-
|
| 60 |
ckpt = torch.load(encoder_weights_path, map_location="cpu", weights_only=True)
|
| 61 |
ckpt = ckpt.get("ema_encoder", ckpt.get("encoder", ckpt))
|
| 62 |
ckpt = {k.replace("module.", "").replace("backbone.", ""): v for k, v in ckpt.items()}
|
| 63 |
encoder.load_state_dict(ckpt, strict=False)
|
| 64 |
del ckpt
|
| 65 |
-
|
| 66 |
encoder = encoder.to(DEVICE).to(torch.bfloat16)
|
| 67 |
encoder.eval()
|
| 68 |
print("Encoder ready.")
|
|
@@ -73,11 +70,9 @@ 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"]
|
| 80 |
-
|
| 81 |
probe = AttentiveClassifier(embed_dim=FEATURE_DIM)
|
| 82 |
probe.load_state_dict(probe_state)
|
| 83 |
probe = probe.to(DEVICE)
|
|
@@ -105,7 +100,6 @@ def extract_platform(url: str) -> str:
|
|
| 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",
|
|
@@ -117,17 +111,14 @@ def download_video(url: str) -> tuple[bool, str]:
|
|
| 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)."
|
|
@@ -166,7 +157,6 @@ 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]
|
|
@@ -175,13 +165,13 @@ def upload_and_notify(video_path: str, meta: dict):
|
|
| 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,
|
|
@@ -197,7 +187,7 @@ def upload_and_notify(video_path: str, meta: dict):
|
|
| 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 = (
|
|
@@ -221,11 +211,11 @@ def upload_and_notify(video_path: str, meta: dict):
|
|
| 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 |
|
|
@@ -233,16 +223,27 @@ def predict(url: str, consent: bool, current_state: dict) -> tuple[str, str, str
|
|
| 233 |
old_path = current_state["video_path"]
|
| 234 |
for f in glob.glob(old_path + "*") + [old_path]:
|
| 235 |
if os.path.exists(f):
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
if
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
|
| 247 |
try:
|
| 248 |
windows, total_frames, fps, duration_sec = get_video_windows(video_path)
|
|
@@ -250,14 +251,14 @@ def predict(url: str, consent: bool, current_state: dict) -> tuple[str, str, str
|
|
| 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
|
|
@@ -270,26 +271,29 @@ def predict(url: str, consent: bool, current_state: dict) -> tuple[str, str, str
|
|
| 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":
|
| 277 |
-
"platform":
|
| 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 |
-
|
| 292 |
-
|
|
|
|
|
|
|
|
|
|
| 293 |
return label, confidence, "\n".join(lines), {}
|
| 294 |
|
| 295 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -297,20 +301,27 @@ def predict(url: str, consent: bool, current_state: dict) -> tuple[str, str, str
|
|
| 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 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
# Mandatory Consent Checkbox
|
| 316 |
consent_box = gr.Checkbox(
|
|
@@ -329,12 +340,13 @@ with gr.Blocks(title="Deepfake Detector") as demo:
|
|
| 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 |
|
|
|
|
| 1 |
+
"""Deepfake Detector β Hugging Face ZeroGPU Space
|
|
|
|
| 2 |
V-JEPA 2.1 ViT-Giant + AttentiveClassifier probe
|
| 3 |
"""
|
| 4 |
import glob
|
|
|
|
| 10 |
import uuid
|
| 11 |
import json
|
| 12 |
import requests
|
| 13 |
+
import shutil
|
| 14 |
from datetime import datetime
|
| 15 |
|
| 16 |
warnings.filterwarnings("ignore", category=FutureWarning)
|
|
|
|
| 32 |
HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 33 |
ENCODER_REPO = os.environ.get("ENCODER_REPO", "")
|
| 34 |
PROBE_REPO = os.environ.get("PROBE_REPO", ENCODER_REPO)
|
|
|
|
| 35 |
DATASET_REPO_ID = os.environ.get("DATASET_REPO_ID", "")
|
| 36 |
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
| 37 |
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
|
|
|
|
| 55 |
filename="vjepa2_1_vitg_384.pt",
|
| 56 |
token=HF_TOKEN,
|
| 57 |
)
|
|
|
|
| 58 |
ckpt = torch.load(encoder_weights_path, map_location="cpu", weights_only=True)
|
| 59 |
ckpt = ckpt.get("ema_encoder", ckpt.get("encoder", ckpt))
|
| 60 |
ckpt = {k.replace("module.", "").replace("backbone.", ""): v for k, v in ckpt.items()}
|
| 61 |
encoder.load_state_dict(ckpt, strict=False)
|
| 62 |
del ckpt
|
|
|
|
| 63 |
encoder = encoder.to(DEVICE).to(torch.bfloat16)
|
| 64 |
encoder.eval()
|
| 65 |
print("Encoder ready.")
|
|
|
|
| 70 |
filename="attentive_probe_optimized.pt",
|
| 71 |
token=HF_TOKEN,
|
| 72 |
)
|
|
|
|
| 73 |
probe_state = torch.load(probe_path, map_location="cpu")
|
| 74 |
if "probe_state_dict" in probe_state:
|
| 75 |
probe_state = probe_state["probe_state_dict"]
|
|
|
|
| 76 |
probe = AttentiveClassifier(embed_dim=FEATURE_DIM)
|
| 77 |
probe.load_state_dict(probe_state)
|
| 78 |
probe = probe.to(DEVICE)
|
|
|
|
| 100 |
def download_video(url: str) -> tuple[bool, str]:
|
| 101 |
tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
|
| 102 |
tmp.close()
|
|
|
|
| 103 |
cmd = [
|
| 104 |
"yt-dlp",
|
| 105 |
"-f", "best[height<=720]/best",
|
|
|
|
| 111 |
"-o", tmp.name,
|
| 112 |
url,
|
| 113 |
]
|
|
|
|
| 114 |
try:
|
| 115 |
result = subprocess.run(cmd, capture_output=True, text=True, timeout=90)
|
| 116 |
candidates = sorted(glob.glob(tmp.name + "*") + [tmp.name])
|
| 117 |
actual_file = next((f for f in candidates if os.path.exists(f) and os.path.getsize(f) > 0), None)
|
|
|
|
| 118 |
if actual_file is None:
|
| 119 |
if "does not pass filter" in result.stdout or "does not pass filter" in result.stderr:
|
| 120 |
return False, "Video length exceeds the maximum 2-minute limit."
|
| 121 |
return False, f"yt-dlp wrote no output. stderr: {result.stderr[:400]}"
|
|
|
|
| 122 |
return True, actual_file
|
| 123 |
except subprocess.TimeoutExpired:
|
| 124 |
return False, "Download timed out (90s limit)."
|
|
|
|
| 157 |
if not DATASET_REPO_ID or not HF_TOKEN:
|
| 158 |
print("Upload skipped: Credentials missing.")
|
| 159 |
return
|
|
|
|
| 160 |
try:
|
| 161 |
api = HfApi(token=HF_TOKEN)
|
| 162 |
unique_id = str(uuid.uuid4())[:8]
|
|
|
|
| 165 |
|
| 166 |
repo_video_path = f"data/video_{base_filename}.mp4"
|
| 167 |
repo_json_path = f"data/meta_{base_filename}.json"
|
| 168 |
+
|
| 169 |
# Generate the direct download/view asset URL for the video
|
| 170 |
direct_video_url = f"https://huggingface.co/datasets/{DATASET_REPO_ID}/resolve/main/{repo_video_path}"
|
| 171 |
|
| 172 |
# Store the video direct link inside the metadata dictionary itself
|
| 173 |
meta["stored_video_url"] = direct_video_url
|
| 174 |
+
|
| 175 |
# 1. Upload Video File
|
| 176 |
api.upload_file(
|
| 177 |
path_or_fileobj=video_path,
|
|
|
|
| 187 |
repo_id=DATASET_REPO_ID,
|
| 188 |
repo_type="dataset"
|
| 189 |
)
|
| 190 |
+
|
| 191 |
# 3. Send Telegram alert containing the direct video asset link
|
| 192 |
if TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID:
|
| 193 |
tele_msg = (
|
|
|
|
| 211 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 212 |
# GRADIO PREDICT
|
| 213 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 214 |
+
def predict(url: str, uploaded_video: str, consent: bool, current_state: dict) -> tuple[str, str, str, dict]:
|
| 215 |
# Check mandatory consent gate
|
| 216 |
if not consent:
|
| 217 |
return "β Required: You must check the data collection consent box to process videos.", "", "", current_state
|
| 218 |
+
|
| 219 |
start_time = time.time()
|
| 220 |
url = (url or "").strip()
|
| 221 |
|
|
|
|
| 223 |
old_path = current_state["video_path"]
|
| 224 |
for f in glob.glob(old_path + "*") + [old_path]:
|
| 225 |
if os.path.exists(f):
|
| 226 |
+
try:
|
| 227 |
+
os.remove(f)
|
| 228 |
+
except Exception:
|
| 229 |
+
pass
|
| 230 |
+
|
| 231 |
+
# Source priority: Direct upload over URL
|
| 232 |
+
if uploaded_video:
|
| 233 |
+
tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
|
| 234 |
+
shutil.copy(uploaded_video, tmp.name)
|
| 235 |
+
video_path = tmp.name
|
| 236 |
+
platform_name = "Direct Upload"
|
| 237 |
+
stored_url = "Uploaded File"
|
| 238 |
+
elif url:
|
| 239 |
+
ok, path_or_err = download_video(url)
|
| 240 |
+
if not ok:
|
| 241 |
+
return f"Download failed: {path_or_err}", "", "", {}
|
| 242 |
+
video_path = path_or_err
|
| 243 |
+
platform_name = extract_platform(url)
|
| 244 |
+
stored_url = url
|
| 245 |
+
else:
|
| 246 |
+
return "Please enter a URL or upload a video.", "", "", {}
|
| 247 |
|
| 248 |
try:
|
| 249 |
windows, total_frames, fps, duration_sec = get_video_windows(video_path)
|
|
|
|
| 251 |
if os.path.exists(video_path):
|
| 252 |
os.remove(video_path)
|
| 253 |
return f"Video decode failed: {e}", "", "", {}
|
| 254 |
+
|
| 255 |
scores = encode_and_classify(windows)
|
| 256 |
+
|
| 257 |
if not scores:
|
| 258 |
if os.path.exists(video_path):
|
| 259 |
os.remove(video_path)
|
| 260 |
return "No windows processed.", "", "", {}
|
| 261 |
+
|
| 262 |
final_prob = max(s["prob"] for s in scores)
|
| 263 |
label = "β οΈ AI-GENERATED (FAKE)" if final_prob > 0.5 else "β
REAL"
|
| 264 |
elapsed_time = time.time() - start_time
|
|
|
|
| 271 |
for s in scores:
|
| 272 |
flag = " β peak" if (s["prob"] == final_prob and final_prob > 0.5) else ""
|
| 273 |
lines.append(f" [{s['start_sec']:.1f}s β {s['end_sec']:.1f}s] β {s['prob'] * 100:.1f}% fake{flag}")
|
| 274 |
+
|
| 275 |
# Build base metadata dict
|
| 276 |
meta = {
|
| 277 |
+
"url": stored_url,
|
| 278 |
+
"platform": platform_name,
|
| 279 |
"label": label,
|
| 280 |
"confidence": confidence,
|
| 281 |
"processing_time_sec": round(elapsed_time, 2),
|
| 282 |
"video_duration_sec": round(duration_sec, 2),
|
| 283 |
"total_frames": total_frames
|
| 284 |
}
|
| 285 |
+
|
| 286 |
# Execute backend background syncing task
|
| 287 |
upload_and_notify(video_path, meta)
|
| 288 |
+
|
| 289 |
# Clean up local system file tracking loop paths
|
| 290 |
for f in glob.glob(video_path + "*") + [video_path]:
|
| 291 |
if os.path.exists(f):
|
| 292 |
+
try:
|
| 293 |
+
os.remove(f)
|
| 294 |
+
except Exception:
|
| 295 |
+
pass
|
| 296 |
+
|
| 297 |
return label, confidence, "\n".join(lines), {}
|
| 298 |
|
| 299 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 301 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 302 |
with gr.Blocks(title="Deepfake Detector") as demo:
|
| 303 |
session_state = gr.State(value={})
|
| 304 |
+
|
| 305 |
gr.Markdown(
|
| 306 |
"""
|
| 307 |
## π¬ Deepfake Video Detector
|
| 308 |
+
Paste a public Instagram, TikTok, or direct `.mp4` URL, **OR** upload a video directly. (Maximum duration: 2 minutes).
|
| 309 |
Uses **V-JEPA 2.1 ViT-Giant** + **AttentiveClassifier** probe.
|
| 310 |
"""
|
| 311 |
)
|
| 312 |
|
| 313 |
+
with gr.Row():
|
| 314 |
+
url_box = gr.Textbox(
|
| 315 |
+
label="Video URL",
|
| 316 |
+
placeholder="https://www.instagram.com/reels/...",
|
| 317 |
+
autofocus=True,
|
| 318 |
+
scale=1
|
| 319 |
+
)
|
| 320 |
+
upload_box = gr.Video(
|
| 321 |
+
label="Or Upload Video directly",
|
| 322 |
+
sources=["upload"],
|
| 323 |
+
scale=1
|
| 324 |
+
)
|
| 325 |
|
| 326 |
# Mandatory Consent Checkbox
|
| 327 |
consent_box = gr.Checkbox(
|
|
|
|
| 340 |
|
| 341 |
run_btn.click(
|
| 342 |
fn=predict,
|
| 343 |
+
inputs=[url_box, upload_box, consent_box, session_state],
|
| 344 |
outputs=[label_out, conf_out, breakdown_out, session_state],
|
| 345 |
)
|
| 346 |
+
|
| 347 |
url_box.submit(
|
| 348 |
fn=predict,
|
| 349 |
+
inputs=[url_box, upload_box, consent_box, session_state],
|
| 350 |
outputs=[label_out, conf_out, breakdown_out, session_state],
|
| 351 |
)
|
| 352 |
|