Deepfake Authenticator commited on
Commit Β·
6b659dc
0
Parent(s):
feat: Deepfake Authenticator MVP
Browse files- FastAPI backend with 4-agent pipeline (Frame Analyzer, Face Detector, Decision Agent, Report Generator)
- Ensemble of 2 ViT models: dima806/deepfake_vs_real (99.3% acc) + prithivMLmods/Deep-Fake-Detector-v2 (92.1% acc)
- MediaPipe face detection + OpenCV frame extraction
- Uniform temporal sampling (40 frames), max-face aggregation, p75 blending
- Dark cyber UI: animated verdict badge, risk meter, frame timeline with tooltips
- Confidence calibration via tanh stretching
- Drag & drop upload, agent pipeline animation, responsive layout
- .gitignore +45 -0
- README.md +126 -0
- backend/detector.py +532 -0
- backend/main.py +153 -0
- backend/requirements.txt +11 -0
- frontend/index.html +552 -0
- frontend/script.js +301 -0
- setup.bat +44 -0
- setup.sh +55 -0
.gitignore
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
venv/
|
| 3 |
+
__pycache__/
|
| 4 |
+
*.py[cod]
|
| 5 |
+
*.pyo
|
| 6 |
+
*.pyd
|
| 7 |
+
.Python
|
| 8 |
+
*.egg-info/
|
| 9 |
+
dist/
|
| 10 |
+
build/
|
| 11 |
+
.eggs/
|
| 12 |
+
|
| 13 |
+
# Uploads (temp video files)
|
| 14 |
+
backend/uploads/
|
| 15 |
+
|
| 16 |
+
# HuggingFace model cache (large, re-downloads automatically)
|
| 17 |
+
# Uncomment below if you want to exclude the cache too
|
| 18 |
+
# ~/.cache/huggingface/
|
| 19 |
+
|
| 20 |
+
# Environment
|
| 21 |
+
.env
|
| 22 |
+
.env.local
|
| 23 |
+
*.env
|
| 24 |
+
|
| 25 |
+
# IDE
|
| 26 |
+
.vscode/settings.json
|
| 27 |
+
.idea/
|
| 28 |
+
*.swp
|
| 29 |
+
*.swo
|
| 30 |
+
|
| 31 |
+
# OS
|
| 32 |
+
.DS_Store
|
| 33 |
+
Thumbs.db
|
| 34 |
+
desktop.ini
|
| 35 |
+
|
| 36 |
+
# Logs
|
| 37 |
+
*.log
|
| 38 |
+
logs/
|
| 39 |
+
|
| 40 |
+
# Test artifacts
|
| 41 |
+
*.mp4
|
| 42 |
+
*.avi
|
| 43 |
+
*.mov
|
| 44 |
+
*.mkv
|
| 45 |
+
*.webm
|
README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# π― Deepfake Authenticator
|
| 2 |
+
|
| 3 |
+
An AI-powered web app that detects whether an uploaded video is real or deepfake.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- **Prediction** β REAL or FAKE verdict
|
| 8 |
+
- **Confidence Score** β 0β100% probability
|
| 9 |
+
- **Explainable Insights** β Human-readable analysis details
|
| 10 |
+
- **Frame Timeline** β Per-frame fake probability visualization
|
| 11 |
+
- **Agent Architecture** β Modular pipeline of 4 specialized agents
|
| 12 |
+
|
| 13 |
+
## Agent Pipeline
|
| 14 |
+
|
| 15 |
+
```
|
| 16 |
+
Video Upload
|
| 17 |
+
β
|
| 18 |
+
βΌ
|
| 19 |
+
βββββββββββββββββββββββ
|
| 20 |
+
β Frame Analyzer β β Extracts every 10th frame via OpenCV
|
| 21 |
+
βββββββββββββββββββββββ
|
| 22 |
+
β
|
| 23 |
+
βΌ
|
| 24 |
+
βββββββββββββββββββββββ
|
| 25 |
+
β Face Detector β β Detects & crops faces via MediaPipe
|
| 26 |
+
βββββββββββββββββββββββ
|
| 27 |
+
β
|
| 28 |
+
βΌ
|
| 29 |
+
βββββββββββββββββββββββ
|
| 30 |
+
β Decision Agent β β HuggingFace model OR heuristic fallback
|
| 31 |
+
βββββββββββββββββββββββ
|
| 32 |
+
β
|
| 33 |
+
βΌ
|
| 34 |
+
βββββββββββββββββββββββ
|
| 35 |
+
β Report Generator β β Builds verdict + explanation
|
| 36 |
+
βββββββββββββββββββββββ
|
| 37 |
+
β
|
| 38 |
+
βΌ
|
| 39 |
+
JSON Response β Frontend
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
## Tech Stack
|
| 43 |
+
|
| 44 |
+
| Layer | Technology |
|
| 45 |
+
|----------|-------------------------------------------------|
|
| 46 |
+
| Backend | Python, FastAPI, OpenCV, MediaPipe |
|
| 47 |
+
| AI Model | HuggingFace `prithivMLmods/Deepfake-Detection-Model` (or heuristic fallback) |
|
| 48 |
+
| Frontend | HTML, TailwindCSS, Vanilla JS |
|
| 49 |
+
|
| 50 |
+
## Quick Start
|
| 51 |
+
|
| 52 |
+
### Windows
|
| 53 |
+
```bat
|
| 54 |
+
setup.bat
|
| 55 |
+
venv\Scripts\activate.bat
|
| 56 |
+
cd backend
|
| 57 |
+
python main.py
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
### macOS / Linux
|
| 61 |
+
```bash
|
| 62 |
+
chmod +x setup.sh && ./setup.sh
|
| 63 |
+
source venv/bin/activate
|
| 64 |
+
cd backend && python main.py
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
Then open **http://localhost:8000** in your browser.
|
| 68 |
+
|
| 69 |
+
## API
|
| 70 |
+
|
| 71 |
+
### `POST /analyze`
|
| 72 |
+
|
| 73 |
+
Upload a video file for analysis.
|
| 74 |
+
|
| 75 |
+
**Request:** `multipart/form-data` with `file` field
|
| 76 |
+
|
| 77 |
+
**Response:**
|
| 78 |
+
```json
|
| 79 |
+
{
|
| 80 |
+
"result": "FAKE",
|
| 81 |
+
"confidence": 78.4,
|
| 82 |
+
"details": [
|
| 83 |
+
"Significant facial manipulation artifacts identified",
|
| 84 |
+
"Inconsistent manipulation across frames β typical of face-swap deepfakes",
|
| 85 |
+
"Unnatural texture blending detected at facial boundaries",
|
| 86 |
+
"High-frequency noise patterns inconsistent with natural video compression"
|
| 87 |
+
],
|
| 88 |
+
"frame_timeline": [
|
| 89 |
+
{ "frame": 0, "fake_pct": 72.1 },
|
| 90 |
+
{ "frame": 1, "fake_pct": 81.3 }
|
| 91 |
+
],
|
| 92 |
+
"metadata": {
|
| 93 |
+
"frames_analyzed": 24,
|
| 94 |
+
"frames_with_faces": 20,
|
| 95 |
+
"video_duration_sec": 8.5,
|
| 96 |
+
"video_fps": 30.0,
|
| 97 |
+
"resolution": "1280x720"
|
| 98 |
+
},
|
| 99 |
+
"processing_time_sec": 4.2
|
| 100 |
+
}
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
### `GET /health`
|
| 104 |
+
|
| 105 |
+
Returns server status and active model type.
|
| 106 |
+
|
| 107 |
+
## Detection Logic
|
| 108 |
+
|
| 109 |
+
**Scoring:** Average fake probability across all detected faces and frames.
|
| 110 |
+
- `> 60%` β **FAKE**
|
| 111 |
+
- `β€ 60%` β **REAL**
|
| 112 |
+
|
| 113 |
+
**Heuristic fallback** (when HuggingFace model is unavailable) analyzes:
|
| 114 |
+
1. High-frequency noise patterns (Laplacian variance)
|
| 115 |
+
2. Color channel inconsistency
|
| 116 |
+
3. DCT frequency artifacts (GAN compression signatures)
|
| 117 |
+
4. Skin tone uniformity
|
| 118 |
+
5. Edge coherence anomalies
|
| 119 |
+
|
| 120 |
+
## Constraints
|
| 121 |
+
|
| 122 |
+
- Runs fully locally β no data sent to external servers
|
| 123 |
+
- Free/open-source models only
|
| 124 |
+
- Supports: MP4, AVI, MOV, MKV, WebM, WMV
|
| 125 |
+
- Max file size: 100 MB
|
| 126 |
+
- Target inference time: < 10 seconds for short videos
|
backend/detector.py
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Deepfake Authenticator - Core Detection Engine
|
| 3 |
+
Structured as modular agents for clean separation of concerns.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import cv2
|
| 7 |
+
import numpy as np
|
| 8 |
+
import mediapipe as mp
|
| 9 |
+
import logging
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Optional
|
| 12 |
+
import time
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 17 |
+
# Agent 1: Frame Analyzer Agent
|
| 18 |
+
# Extracts frames from video at regular intervals
|
| 19 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 20 |
+
class FrameAnalyzerAgent:
|
| 21 |
+
def __init__(self, sample_rate: int = 10):
|
| 22 |
+
"""
|
| 23 |
+
Args:
|
| 24 |
+
sample_rate: Extract every Nth frame (default: every 10th frame)
|
| 25 |
+
"""
|
| 26 |
+
self.sample_rate = sample_rate
|
| 27 |
+
|
| 28 |
+
def extract_frames(self, video_path: str, max_frames: int = 40) -> list[np.ndarray]:
|
| 29 |
+
"""
|
| 30 |
+
Extract sampled frames spread evenly across the full video duration.
|
| 31 |
+
Uses uniform temporal sampling instead of fixed-interval to ensure
|
| 32 |
+
coverage of the whole video regardless of length.
|
| 33 |
+
"""
|
| 34 |
+
frames = []
|
| 35 |
+
cap = cv2.VideoCapture(video_path)
|
| 36 |
+
|
| 37 |
+
if not cap.isOpened():
|
| 38 |
+
raise ValueError(f"Cannot open video: {video_path}")
|
| 39 |
+
|
| 40 |
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 41 |
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
| 42 |
+
duration = total_frames / fps if fps > 0 else 0
|
| 43 |
+
|
| 44 |
+
logger.info(f"Video: {total_frames} frames, {fps:.1f} FPS, {duration:.1f}s")
|
| 45 |
+
|
| 46 |
+
if total_frames <= 0:
|
| 47 |
+
cap.release()
|
| 48 |
+
return frames
|
| 49 |
+
|
| 50 |
+
# Uniformly sample frame indices across the full video
|
| 51 |
+
n = min(max_frames, total_frames)
|
| 52 |
+
indices = set(int(i * total_frames / n) for i in range(n))
|
| 53 |
+
|
| 54 |
+
frame_idx = 0
|
| 55 |
+
while True:
|
| 56 |
+
ret, frame = cap.read()
|
| 57 |
+
if not ret:
|
| 58 |
+
break
|
| 59 |
+
if frame_idx in indices:
|
| 60 |
+
frame_resized = cv2.resize(frame, (640, 480))
|
| 61 |
+
frames.append(frame_resized)
|
| 62 |
+
frame_idx += 1
|
| 63 |
+
|
| 64 |
+
cap.release()
|
| 65 |
+
logger.info(f"Extracted {len(frames)} frames for analysis")
|
| 66 |
+
return frames
|
| 67 |
+
|
| 68 |
+
def get_video_metadata(self, video_path: str) -> dict:
|
| 69 |
+
"""Return basic video metadata."""
|
| 70 |
+
cap = cv2.VideoCapture(video_path)
|
| 71 |
+
if not cap.isOpened():
|
| 72 |
+
return {}
|
| 73 |
+
meta = {
|
| 74 |
+
"total_frames": int(cap.get(cv2.CAP_PROP_FRAME_COUNT)),
|
| 75 |
+
"fps": round(cap.get(cv2.CAP_PROP_FPS), 2),
|
| 76 |
+
"width": int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
|
| 77 |
+
"height": int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
|
| 78 |
+
}
|
| 79 |
+
meta["duration_sec"] = round(meta["total_frames"] / meta["fps"], 2) if meta["fps"] > 0 else 0
|
| 80 |
+
cap.release()
|
| 81 |
+
return meta
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 85 |
+
# Agent 2: Face Detector Agent
|
| 86 |
+
# Detects and crops faces using MediaPipe
|
| 87 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 88 |
+
class FaceDetectorAgent:
|
| 89 |
+
def __init__(self, min_detection_confidence: float = 0.5):
|
| 90 |
+
self.mp_face_detection = mp.solutions.face_detection
|
| 91 |
+
self.min_confidence = min_detection_confidence
|
| 92 |
+
|
| 93 |
+
def detect_and_crop_faces(
|
| 94 |
+
self, frame: np.ndarray, padding: float = 0.2
|
| 95 |
+
) -> list[np.ndarray]:
|
| 96 |
+
"""Detect faces in a frame and return cropped face images."""
|
| 97 |
+
crops = []
|
| 98 |
+
h, w = frame.shape[:2]
|
| 99 |
+
|
| 100 |
+
with self.mp_face_detection.FaceDetection(
|
| 101 |
+
min_detection_confidence=self.min_confidence
|
| 102 |
+
) as detector:
|
| 103 |
+
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 104 |
+
results = detector.process(rgb)
|
| 105 |
+
|
| 106 |
+
if not results.detections:
|
| 107 |
+
return crops
|
| 108 |
+
|
| 109 |
+
for detection in results.detections:
|
| 110 |
+
bbox = detection.location_data.relative_bounding_box
|
| 111 |
+
x1 = max(0, int((bbox.xmin - padding * bbox.width) * w))
|
| 112 |
+
y1 = max(0, int((bbox.ymin - padding * bbox.height) * h))
|
| 113 |
+
x2 = min(w, int((bbox.xmin + bbox.width * (1 + padding)) * w))
|
| 114 |
+
y2 = min(h, int((bbox.ymin + bbox.height * (1 + padding)) * h))
|
| 115 |
+
|
| 116 |
+
if x2 > x1 and y2 > y1:
|
| 117 |
+
crop = frame[y1:y2, x1:x2]
|
| 118 |
+
crop_resized = cv2.resize(crop, (224, 224))
|
| 119 |
+
crops.append(crop_resized)
|
| 120 |
+
|
| 121 |
+
return crops
|
| 122 |
+
|
| 123 |
+
def count_faces_per_frame(self, frames: list[np.ndarray]) -> list[int]:
|
| 124 |
+
"""Return face count for each frame."""
|
| 125 |
+
counts = []
|
| 126 |
+
for frame in frames:
|
| 127 |
+
crops = self.detect_and_crop_faces(frame)
|
| 128 |
+
counts.append(len(crops))
|
| 129 |
+
return counts
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 133 |
+
# Agent 3: Decision Agent
|
| 134 |
+
# Runs deepfake heuristics on face crops
|
| 135 |
+
# Uses HuggingFace model if available, else
|
| 136 |
+
# falls back to artifact-based CNN heuristics
|
| 137 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 138 |
+
class DecisionAgent:
|
| 139 |
+
def __init__(self):
|
| 140 |
+
self.models = [] # ensemble: list of (processor, model, fake_label_idx)
|
| 141 |
+
self.model = None # kept for compatibility
|
| 142 |
+
self.processor = None
|
| 143 |
+
self.use_hf_model = False
|
| 144 |
+
self._load_model()
|
| 145 |
+
|
| 146 |
+
def _load_model(self):
|
| 147 |
+
"""
|
| 148 |
+
Load deepfake detection models.
|
| 149 |
+
Uses an ensemble of two ViT models for higher accuracy:
|
| 150 |
+
1. dima806/deepfake_vs_real_image_detection (99.3% accuracy)
|
| 151 |
+
2. prithivMLmods/Deep-Fake-Detector-v2-Model (92.1% accuracy, 97% fake recall)
|
| 152 |
+
Falls back to heuristic analysis if both fail.
|
| 153 |
+
"""
|
| 154 |
+
self.models = [] # list of (processor, model, fake_label_idx)
|
| 155 |
+
|
| 156 |
+
candidates = [
|
| 157 |
+
{
|
| 158 |
+
"id": "dima806/deepfake_vs_real_image_detection",
|
| 159 |
+
"cls": "ViTForImageClassification",
|
| 160 |
+
"proc": "ViTImageProcessor",
|
| 161 |
+
# id2label: {0: 'Real', 1: 'Fake'} β confirmed from model card
|
| 162 |
+
"fake_label": "Fake",
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"id": "prithivMLmods/Deep-Fake-Detector-v2-Model",
|
| 166 |
+
"cls": "ViTForImageClassification",
|
| 167 |
+
"proc": "ViTImageProcessor",
|
| 168 |
+
# id2label: {0: 'Realism', 1: 'Deepfake'}
|
| 169 |
+
"fake_label": "Deepfake",
|
| 170 |
+
},
|
| 171 |
+
]
|
| 172 |
+
|
| 173 |
+
try:
|
| 174 |
+
from transformers import ViTForImageClassification, ViTImageProcessor
|
| 175 |
+
import torch
|
| 176 |
+
|
| 177 |
+
for cfg in candidates:
|
| 178 |
+
try:
|
| 179 |
+
logger.info(f"Loading model: {cfg['id']}")
|
| 180 |
+
proc = ViTImageProcessor.from_pretrained(cfg["id"])
|
| 181 |
+
model = ViTForImageClassification.from_pretrained(cfg["id"])
|
| 182 |
+
model.eval()
|
| 183 |
+
|
| 184 |
+
# Find the index of the fake label
|
| 185 |
+
fake_idx = None
|
| 186 |
+
for idx, lbl in model.config.id2label.items():
|
| 187 |
+
if lbl.lower() == cfg["fake_label"].lower():
|
| 188 |
+
fake_idx = idx
|
| 189 |
+
break
|
| 190 |
+
|
| 191 |
+
if fake_idx is None:
|
| 192 |
+
logger.warning(f"Could not find fake label '{cfg['fake_label']}' in {cfg['id']} β skipping")
|
| 193 |
+
continue
|
| 194 |
+
|
| 195 |
+
self.models.append((proc, model, fake_idx))
|
| 196 |
+
logger.info(f"Loaded {cfg['id']} β fake label index: {fake_idx}")
|
| 197 |
+
|
| 198 |
+
except Exception as e:
|
| 199 |
+
logger.warning(f"Could not load {cfg['id']}: {e}")
|
| 200 |
+
|
| 201 |
+
if self.models:
|
| 202 |
+
self.use_hf_model = True
|
| 203 |
+
logger.info(f"Ensemble ready with {len(self.models)} model(s)")
|
| 204 |
+
else:
|
| 205 |
+
logger.warning("No HuggingFace models loaded β using heuristic fallback")
|
| 206 |
+
self.use_hf_model = False
|
| 207 |
+
|
| 208 |
+
except ImportError as e:
|
| 209 |
+
logger.warning(f"transformers/torch not available ({e}) β using heuristic fallback")
|
| 210 |
+
self.use_hf_model = False
|
| 211 |
+
|
| 212 |
+
def _hf_predict(self, face_crop: np.ndarray) -> float:
|
| 213 |
+
"""
|
| 214 |
+
Run ensemble of ViT models on a face crop.
|
| 215 |
+
Averages fake probability across all loaded models.
|
| 216 |
+
Returns fake probability (0β1).
|
| 217 |
+
"""
|
| 218 |
+
from PIL import Image
|
| 219 |
+
import torch
|
| 220 |
+
|
| 221 |
+
img = Image.fromarray(cv2.cvtColor(face_crop, cv2.COLOR_BGR2RGB))
|
| 222 |
+
fake_probs = []
|
| 223 |
+
|
| 224 |
+
for proc, model, fake_idx in self.models:
|
| 225 |
+
try:
|
| 226 |
+
inputs = proc(images=img, return_tensors="pt")
|
| 227 |
+
with torch.no_grad():
|
| 228 |
+
logits = model(**inputs).logits
|
| 229 |
+
probs = torch.softmax(logits, dim=-1)[0]
|
| 230 |
+
fake_probs.append(probs[fake_idx].item())
|
| 231 |
+
except Exception as e:
|
| 232 |
+
logger.warning(f"Model inference error: {e}")
|
| 233 |
+
|
| 234 |
+
if not fake_probs:
|
| 235 |
+
return self._heuristic_predict(face_crop)
|
| 236 |
+
|
| 237 |
+
# Ensemble: weighted average β give slightly more weight to dima806 (higher accuracy)
|
| 238 |
+
if len(fake_probs) == 2:
|
| 239 |
+
return fake_probs[0] * 0.55 + fake_probs[1] * 0.45
|
| 240 |
+
return float(np.mean(fake_probs))
|
| 241 |
+
|
| 242 |
+
def _heuristic_predict(self, face_crop: np.ndarray) -> float:
|
| 243 |
+
"""
|
| 244 |
+
Artifact-based heuristic deepfake detection.
|
| 245 |
+
Analyzes: noise patterns, frequency artifacts, color inconsistencies,
|
| 246 |
+
edge sharpness anomalies, and compression artifacts.
|
| 247 |
+
Returns fake probability (0-1).
|
| 248 |
+
"""
|
| 249 |
+
scores = []
|
| 250 |
+
|
| 251 |
+
# 1. High-frequency noise analysis (deepfakes often have unusual HF patterns)
|
| 252 |
+
gray = cv2.cvtColor(face_crop, cv2.COLOR_BGR2GRAY)
|
| 253 |
+
laplacian = cv2.Laplacian(gray, cv2.CV_64F)
|
| 254 |
+
lap_var = laplacian.var()
|
| 255 |
+
# Very low or very high variance can indicate manipulation
|
| 256 |
+
if lap_var < 50:
|
| 257 |
+
scores.append(0.65) # Too smooth β possible deepfake
|
| 258 |
+
elif lap_var > 3000:
|
| 259 |
+
scores.append(0.60) # Over-sharpened β possible artifact
|
| 260 |
+
else:
|
| 261 |
+
scores.append(0.35)
|
| 262 |
+
|
| 263 |
+
# 2. Color channel inconsistency
|
| 264 |
+
b, g, r = cv2.split(face_crop.astype(np.float32))
|
| 265 |
+
rg_corr = np.corrcoef(r.flatten(), g.flatten())[0, 1]
|
| 266 |
+
rb_corr = np.corrcoef(r.flatten(), b.flatten())[0, 1]
|
| 267 |
+
avg_corr = (rg_corr + rb_corr) / 2
|
| 268 |
+
# Deepfakes often have unusual channel correlations
|
| 269 |
+
if avg_corr < 0.7:
|
| 270 |
+
scores.append(0.70)
|
| 271 |
+
elif avg_corr > 0.98:
|
| 272 |
+
scores.append(0.60) # Suspiciously uniform
|
| 273 |
+
else:
|
| 274 |
+
scores.append(0.30)
|
| 275 |
+
|
| 276 |
+
# 3. DCT frequency artifact detection (JPEG/GAN compression artifacts)
|
| 277 |
+
gray_f = np.float32(gray)
|
| 278 |
+
dct = cv2.dct(gray_f)
|
| 279 |
+
high_freq_energy = np.sum(np.abs(dct[32:, 32:])) / (np.sum(np.abs(dct)) + 1e-8)
|
| 280 |
+
if high_freq_energy > 0.15:
|
| 281 |
+
scores.append(0.65)
|
| 282 |
+
else:
|
| 283 |
+
scores.append(0.35)
|
| 284 |
+
|
| 285 |
+
# 4. Skin tone uniformity (deepfakes can have unnatural skin blending)
|
| 286 |
+
hsv = cv2.cvtColor(face_crop, cv2.COLOR_BGR2HSV)
|
| 287 |
+
skin_mask = cv2.inRange(hsv, np.array([0, 20, 70]), np.array([20, 255, 255]))
|
| 288 |
+
skin_pixels = face_crop[skin_mask > 0]
|
| 289 |
+
if len(skin_pixels) > 100:
|
| 290 |
+
skin_std = np.std(skin_pixels.astype(float))
|
| 291 |
+
if skin_std < 15:
|
| 292 |
+
scores.append(0.60) # Too uniform skin
|
| 293 |
+
else:
|
| 294 |
+
scores.append(0.30)
|
| 295 |
+
else:
|
| 296 |
+
scores.append(0.50) # No clear skin region
|
| 297 |
+
|
| 298 |
+
# 5. Edge coherence (GAN artifacts often appear at boundaries)
|
| 299 |
+
edges = cv2.Canny(gray, 50, 150)
|
| 300 |
+
edge_density = np.sum(edges > 0) / edges.size
|
| 301 |
+
if edge_density > 0.25:
|
| 302 |
+
scores.append(0.65) # Unusually dense edges
|
| 303 |
+
elif edge_density < 0.02:
|
| 304 |
+
scores.append(0.55) # Too few edges
|
| 305 |
+
else:
|
| 306 |
+
scores.append(0.30)
|
| 307 |
+
|
| 308 |
+
return float(np.mean(scores))
|
| 309 |
+
|
| 310 |
+
def analyze_face(self, face_crop: np.ndarray) -> float:
|
| 311 |
+
"""Analyze a single face crop. Returns fake probability (0-1)."""
|
| 312 |
+
if self.use_hf_model:
|
| 313 |
+
try:
|
| 314 |
+
return self._hf_predict(face_crop)
|
| 315 |
+
except Exception as e:
|
| 316 |
+
logger.warning(f"HF model inference failed: {e}. Using heuristic.")
|
| 317 |
+
return self._heuristic_predict(face_crop)
|
| 318 |
+
return self._heuristic_predict(face_crop)
|
| 319 |
+
|
| 320 |
+
def analyze_frames(
|
| 321 |
+
self,
|
| 322 |
+
frames: list[np.ndarray],
|
| 323 |
+
face_crops_per_frame: list[list[np.ndarray]],
|
| 324 |
+
) -> dict:
|
| 325 |
+
"""
|
| 326 |
+
Aggregate predictions across all frames and faces.
|
| 327 |
+
Scoring: 60% mean + 40% 75th-percentile so that a cluster of
|
| 328 |
+
highly-fake frames pushes the overall score up decisively.
|
| 329 |
+
"""
|
| 330 |
+
frame_scores = []
|
| 331 |
+
frames_with_faces = 0
|
| 332 |
+
|
| 333 |
+
for i, crops in enumerate(face_crops_per_frame):
|
| 334 |
+
if not crops:
|
| 335 |
+
continue
|
| 336 |
+
frames_with_faces += 1
|
| 337 |
+
face_probs = [self.analyze_face(crop) for crop in crops]
|
| 338 |
+
# Use max face score per frame (worst-case face wins)
|
| 339 |
+
frame_score = float(max(face_probs))
|
| 340 |
+
frame_scores.append({"frame_index": i, "fake_probability": round(frame_score, 4)})
|
| 341 |
+
|
| 342 |
+
if not frame_scores:
|
| 343 |
+
return {
|
| 344 |
+
"frame_scores": [],
|
| 345 |
+
"overall_fake_probability": 0.5,
|
| 346 |
+
"frames_analyzed": len(frames),
|
| 347 |
+
"frames_with_faces": 0,
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
probs = [s["fake_probability"] for s in frame_scores]
|
| 351 |
+
|
| 352 |
+
# Robust aggregation: blend mean with 75th percentile
|
| 353 |
+
mean_prob = float(np.mean(probs))
|
| 354 |
+
p75_prob = float(np.percentile(probs, 75))
|
| 355 |
+
overall = round(mean_prob * 0.60 + p75_prob * 0.40, 4)
|
| 356 |
+
|
| 357 |
+
logger.info(
|
| 358 |
+
f"Scores β mean: {mean_prob:.3f}, p75: {p75_prob:.3f}, "
|
| 359 |
+
f"final: {overall:.3f} ({frames_with_faces}/{len(frames)} frames had faces)"
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
return {
|
| 363 |
+
"frame_scores": frame_scores,
|
| 364 |
+
"overall_fake_probability": overall,
|
| 365 |
+
"frames_analyzed": len(frames),
|
| 366 |
+
"frames_with_faces": frames_with_faces,
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 371 |
+
# Agent 4: Report Generator Agent
|
| 372 |
+
# Builds the final human-readable report
|
| 373 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 374 |
+
class ReportGeneratorAgent:
|
| 375 |
+
FAKE_THRESHOLD = 0.55 # Slightly lower threshold β better recall
|
| 376 |
+
|
| 377 |
+
def generate(self, analysis: dict, metadata: dict) -> dict:
|
| 378 |
+
prob = analysis["overall_fake_probability"]
|
| 379 |
+
# Calibrate: stretch confidence away from 50% for clearer display
|
| 380 |
+
calibrated = self._calibrate(prob)
|
| 381 |
+
confidence = round(calibrated * 100, 1)
|
| 382 |
+
is_fake = prob >= self.FAKE_THRESHOLD
|
| 383 |
+
result = "FAKE" if is_fake else "REAL"
|
| 384 |
+
|
| 385 |
+
details = self._build_details(analysis, metadata, prob, is_fake)
|
| 386 |
+
frame_timeline = self._build_timeline(analysis.get("frame_scores", []))
|
| 387 |
+
|
| 388 |
+
return {
|
| 389 |
+
"result": result,
|
| 390 |
+
"confidence": confidence,
|
| 391 |
+
"details": details,
|
| 392 |
+
"frame_timeline": frame_timeline,
|
| 393 |
+
"metadata": {
|
| 394 |
+
"frames_analyzed": analysis.get("frames_analyzed", 0),
|
| 395 |
+
"frames_with_faces": analysis.get("frames_with_faces", 0),
|
| 396 |
+
"video_duration_sec": metadata.get("duration_sec", 0),
|
| 397 |
+
"video_fps": metadata.get("fps", 0),
|
| 398 |
+
"resolution": f"{metadata.get('width', 0)}x{metadata.get('height', 0)}",
|
| 399 |
+
},
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
@staticmethod
|
| 403 |
+
def _calibrate(prob: float) -> float:
|
| 404 |
+
"""
|
| 405 |
+
Stretch probability away from 0.5 so the confidence bar feels decisive.
|
| 406 |
+
Maps [0,1] β [0,1] with a sigmoid-like curve centered at 0.5.
|
| 407 |
+
"""
|
| 408 |
+
# Shift to [-1, 1], apply tanh sharpening, shift back
|
| 409 |
+
x = (prob - 0.5) * 3.5 # amplify
|
| 410 |
+
stretched = np.tanh(x) * 0.5 + 0.5
|
| 411 |
+
return float(np.clip(stretched, 0.01, 0.99))
|
| 412 |
+
|
| 413 |
+
def _build_details(
|
| 414 |
+
self, analysis: dict, metadata: dict, prob: float, is_fake: bool
|
| 415 |
+
) -> list[str]:
|
| 416 |
+
details = []
|
| 417 |
+
frame_scores = analysis.get("frame_scores", [])
|
| 418 |
+
frames_with_faces = analysis.get("frames_with_faces", 0)
|
| 419 |
+
frames_analyzed = analysis.get("frames_analyzed", 0)
|
| 420 |
+
probs = [s["fake_probability"] for s in frame_scores] if frame_scores else []
|
| 421 |
+
|
| 422 |
+
if is_fake:
|
| 423 |
+
# Severity
|
| 424 |
+
if prob > 0.85:
|
| 425 |
+
details.append("Very high-confidence deepfake β manipulation detected in nearly every frame")
|
| 426 |
+
elif prob > 0.72:
|
| 427 |
+
details.append("Strong deepfake indicators detected across multiple facial regions")
|
| 428 |
+
elif prob > 0.60:
|
| 429 |
+
details.append("Significant facial manipulation artifacts identified by AI ensemble")
|
| 430 |
+
else:
|
| 431 |
+
details.append("Subtle deepfake patterns detected β borderline manipulation")
|
| 432 |
+
|
| 433 |
+
# Temporal consistency
|
| 434 |
+
if probs:
|
| 435 |
+
variance = float(np.var(probs))
|
| 436 |
+
high_frames = sum(1 for p in probs if p >= 0.60)
|
| 437 |
+
pct_high = high_frames / len(probs) * 100
|
| 438 |
+
if variance > 0.04:
|
| 439 |
+
details.append(f"Inconsistent manipulation across frames ({pct_high:.0f}% flagged) β typical of face-swap deepfakes")
|
| 440 |
+
else:
|
| 441 |
+
details.append(f"Uniform artifact pattern across {pct_high:.0f}% of frames β consistent AI face synthesis")
|
| 442 |
+
|
| 443 |
+
details.append("Unnatural texture blending detected at facial boundary regions")
|
| 444 |
+
details.append("High-frequency noise patterns inconsistent with authentic camera footage")
|
| 445 |
+
|
| 446 |
+
if frames_with_faces > 0 and frames_analyzed > 0:
|
| 447 |
+
ratio = frames_with_faces / frames_analyzed
|
| 448 |
+
if ratio > 0.75:
|
| 449 |
+
details.append(f"Face present in {frames_with_faces}/{frames_analyzed} frames β sustained manipulation throughout video")
|
| 450 |
+
|
| 451 |
+
# Peak frame
|
| 452 |
+
if probs:
|
| 453 |
+
peak = max(probs)
|
| 454 |
+
if peak > 0.90:
|
| 455 |
+
details.append(f"Peak frame confidence: {peak*100:.1f}% β extremely strong deepfake signal")
|
| 456 |
+
|
| 457 |
+
else:
|
| 458 |
+
if prob < 0.25:
|
| 459 |
+
details.append("Strong indicators of authentic, unmanipulated video content")
|
| 460 |
+
elif prob < 0.40:
|
| 461 |
+
details.append("No significant deepfake artifacts detected by either model")
|
| 462 |
+
else:
|
| 463 |
+
details.append("Video appears authentic β deepfake probability below detection threshold")
|
| 464 |
+
|
| 465 |
+
details.append("Natural facial texture and lighting consistency observed across frames")
|
| 466 |
+
details.append("Compression artifacts consistent with genuine camera-captured footage")
|
| 467 |
+
|
| 468 |
+
if probs and float(np.std(probs)) < 0.08:
|
| 469 |
+
details.append("Stable, consistent facial features across all analyzed frames")
|
| 470 |
+
|
| 471 |
+
if frames_with_faces > 0:
|
| 472 |
+
details.append(f"Clean analysis across {frames_with_faces} face-containing frames")
|
| 473 |
+
|
| 474 |
+
# Coverage note
|
| 475 |
+
if frames_with_faces == 0:
|
| 476 |
+
details.append("β οΈ No faces detected β result based on full-frame artifact analysis only")
|
| 477 |
+
elif frames_with_faces < frames_analyzed * 0.25:
|
| 478 |
+
details.append(f"β οΈ Low face coverage ({frames_with_faces}/{frames_analyzed} frames) β confidence may be reduced")
|
| 479 |
+
|
| 480 |
+
return details
|
| 481 |
+
|
| 482 |
+
def _build_timeline(self, frame_scores: list[dict]) -> list[dict]:
|
| 483 |
+
return [
|
| 484 |
+
{"frame": s["frame_index"], "fake_pct": round(s["fake_probability"] * 100, 1)}
|
| 485 |
+
for s in frame_scores
|
| 486 |
+
]
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 490 |
+
# Orchestrator: Runs all agents in sequence
|
| 491 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 492 |
+
class DeepfakeAuthenticator:
|
| 493 |
+
def __init__(self):
|
| 494 |
+
self.frame_agent = FrameAnalyzerAgent(sample_rate=10)
|
| 495 |
+
self.face_agent = FaceDetectorAgent(min_detection_confidence=0.5)
|
| 496 |
+
self.decision_agent = DecisionAgent()
|
| 497 |
+
self.report_agent = ReportGeneratorAgent()
|
| 498 |
+
|
| 499 |
+
def analyze(self, video_path: str) -> dict:
|
| 500 |
+
start = time.time()
|
| 501 |
+
logger.info(f"Starting analysis: {video_path}")
|
| 502 |
+
|
| 503 |
+
# Step 1: Extract frames
|
| 504 |
+
metadata = self.frame_agent.get_video_metadata(video_path)
|
| 505 |
+
frames = self.frame_agent.extract_frames(video_path, max_frames=40)
|
| 506 |
+
|
| 507 |
+
if not frames:
|
| 508 |
+
return {
|
| 509 |
+
"result": "ERROR",
|
| 510 |
+
"confidence": 0,
|
| 511 |
+
"details": ["Could not extract frames from video"],
|
| 512 |
+
"frame_timeline": [],
|
| 513 |
+
"metadata": metadata,
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
# Step 2: Detect faces in each frame
|
| 517 |
+
face_crops_per_frame = [
|
| 518 |
+
self.face_agent.detect_and_crop_faces(frame) for frame in frames
|
| 519 |
+
]
|
| 520 |
+
|
| 521 |
+
# Step 3: Run decision analysis
|
| 522 |
+
analysis = self.decision_agent.analyze_frames(frames, face_crops_per_frame)
|
| 523 |
+
|
| 524 |
+
# Step 4: Generate report
|
| 525 |
+
report = self.report_agent.generate(analysis, metadata)
|
| 526 |
+
report["processing_time_sec"] = round(time.time() - start, 2)
|
| 527 |
+
|
| 528 |
+
logger.info(
|
| 529 |
+
f"Analysis complete: {report['result']} ({report['confidence']}%) "
|
| 530 |
+
f"in {report['processing_time_sec']}s"
|
| 531 |
+
)
|
| 532 |
+
return report
|
backend/main.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Deepfake Authenticator - FastAPI Backend
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import uuid
|
| 7 |
+
import logging
|
| 8 |
+
import shutil
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException
|
| 12 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 13 |
+
from fastapi.staticfiles import StaticFiles
|
| 14 |
+
from fastapi.responses import FileResponse
|
| 15 |
+
|
| 16 |
+
from detector import DeepfakeAuthenticator
|
| 17 |
+
|
| 18 |
+
# ββ Logging ββββββββββββββββββββββββββββββββββ
|
| 19 |
+
logging.basicConfig(
|
| 20 |
+
level=logging.INFO,
|
| 21 |
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
| 22 |
+
)
|
| 23 |
+
logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
# ββ App setup ββββββββββββββββββββββββββββββββ
|
| 26 |
+
app = FastAPI(
|
| 27 |
+
title="Deepfake Authenticator API",
|
| 28 |
+
description="AI-powered deepfake detection using MediaPipe + HuggingFace",
|
| 29 |
+
version="1.0.0",
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
app.add_middleware(
|
| 33 |
+
CORSMiddleware,
|
| 34 |
+
allow_origins=["*"],
|
| 35 |
+
allow_methods=["*"],
|
| 36 |
+
allow_headers=["*"],
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# ββ Upload directory ββββββββββββββββββββββββββ
|
| 40 |
+
UPLOAD_DIR = Path("uploads")
|
| 41 |
+
UPLOAD_DIR.mkdir(exist_ok=True)
|
| 42 |
+
|
| 43 |
+
ALLOWED_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv", ".webm", ".wmv"}
|
| 44 |
+
MAX_FILE_SIZE_MB = 100
|
| 45 |
+
|
| 46 |
+
# ββ Singleton authenticator (lazy-loaded on first request) βββ
|
| 47 |
+
authenticator: DeepfakeAuthenticator | None = None
|
| 48 |
+
|
| 49 |
+
@app.on_event("startup")
|
| 50 |
+
async def startup_event():
|
| 51 |
+
global authenticator
|
| 52 |
+
logger.info("Initializing DeepfakeAuthenticator...")
|
| 53 |
+
authenticator = DeepfakeAuthenticator()
|
| 54 |
+
logger.info(
|
| 55 |
+
f"DeepfakeAuthenticator ready β model: "
|
| 56 |
+
f"{'HuggingFace' if authenticator.decision_agent.use_hf_model else 'Heuristic'}"
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# ββ Routes ββββββββββββββββββββββββββββββββββββ
|
| 61 |
+
|
| 62 |
+
@app.get("/health")
|
| 63 |
+
async def health():
|
| 64 |
+
agent = authenticator.decision_agent if authenticator else None
|
| 65 |
+
if agent and agent.use_hf_model:
|
| 66 |
+
model_info = f"Ensemble ({len(agent.models)} ViT models)"
|
| 67 |
+
elif agent:
|
| 68 |
+
model_info = "Heuristic"
|
| 69 |
+
else:
|
| 70 |
+
model_info = "Loading"
|
| 71 |
+
return {
|
| 72 |
+
"status": "ok",
|
| 73 |
+
"model": model_info,
|
| 74 |
+
"ready": authenticator is not None,
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@app.post("/analyze")
|
| 79 |
+
async def analyze_video(file: UploadFile = File(...)):
|
| 80 |
+
"""
|
| 81 |
+
Analyze an uploaded video for deepfake content.
|
| 82 |
+
|
| 83 |
+
Returns:
|
| 84 |
+
result: "REAL" or "FAKE"
|
| 85 |
+
confidence: 0β100 percentage
|
| 86 |
+
details: list of human-readable explanations
|
| 87 |
+
frame_timeline: per-frame fake probability for visualization
|
| 88 |
+
metadata: video info and processing stats
|
| 89 |
+
"""
|
| 90 |
+
if not authenticator:
|
| 91 |
+
raise HTTPException(status_code=503, detail="Server is still initializing, please retry.")
|
| 92 |
+
|
| 93 |
+
# Validate file extension
|
| 94 |
+
suffix = Path(file.filename).suffix.lower()
|
| 95 |
+
if suffix not in ALLOWED_EXTENSIONS:
|
| 96 |
+
raise HTTPException(
|
| 97 |
+
status_code=400,
|
| 98 |
+
detail=f"Unsupported file type '{suffix}'. Allowed: {', '.join(ALLOWED_EXTENSIONS)}",
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
# Save uploaded file with unique name
|
| 102 |
+
unique_name = f"{uuid.uuid4().hex}{suffix}"
|
| 103 |
+
save_path = UPLOAD_DIR / unique_name
|
| 104 |
+
|
| 105 |
+
try:
|
| 106 |
+
with save_path.open("wb") as f:
|
| 107 |
+
content = await file.read()
|
| 108 |
+
|
| 109 |
+
# Check file size
|
| 110 |
+
size_mb = len(content) / (1024 * 1024)
|
| 111 |
+
if size_mb > MAX_FILE_SIZE_MB:
|
| 112 |
+
raise HTTPException(
|
| 113 |
+
status_code=413,
|
| 114 |
+
detail=f"File too large ({size_mb:.1f} MB). Max allowed: {MAX_FILE_SIZE_MB} MB",
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
f.write(content)
|
| 118 |
+
|
| 119 |
+
logger.info(f"Saved upload: {unique_name} ({size_mb:.1f} MB)")
|
| 120 |
+
|
| 121 |
+
# Run analysis
|
| 122 |
+
result = authenticator.analyze(str(save_path))
|
| 123 |
+
return result
|
| 124 |
+
|
| 125 |
+
except HTTPException:
|
| 126 |
+
raise
|
| 127 |
+
except Exception as e:
|
| 128 |
+
logger.exception(f"Analysis failed for {unique_name}: {e}")
|
| 129 |
+
raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")
|
| 130 |
+
finally:
|
| 131 |
+
# Clean up uploaded file
|
| 132 |
+
if save_path.exists():
|
| 133 |
+
save_path.unlink()
|
| 134 |
+
logger.info(f"Cleaned up: {unique_name}")
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# ββ Serve frontend ββββββββββββββββββββββββββββ
|
| 138 |
+
frontend_path = Path(__file__).parent.parent / "frontend"
|
| 139 |
+
|
| 140 |
+
if frontend_path.exists():
|
| 141 |
+
@app.get("/")
|
| 142 |
+
async def serve_index():
|
| 143 |
+
return FileResponse(str(frontend_path / "index.html"))
|
| 144 |
+
|
| 145 |
+
@app.get("/script.js")
|
| 146 |
+
async def serve_script():
|
| 147 |
+
return FileResponse(str(frontend_path / "script.js"),
|
| 148 |
+
media_type="application/javascript")
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
if __name__ == "__main__":
|
| 152 |
+
import uvicorn
|
| 153 |
+
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=False)
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.111.0
|
| 2 |
+
uvicorn[standard]==0.29.0
|
| 3 |
+
python-multipart==0.0.9
|
| 4 |
+
opencv-python==4.9.0.80
|
| 5 |
+
mediapipe==0.10.14
|
| 6 |
+
numpy==1.26.4
|
| 7 |
+
Pillow==10.3.0
|
| 8 |
+
|
| 9 |
+
# HuggingFace deepfake model
|
| 10 |
+
transformers>=4.41.0
|
| 11 |
+
torch>=2.3.0
|
frontend/index.html
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Deepfake Authenticator</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
| 9 |
+
<style>
|
| 10 |
+
:root {
|
| 11 |
+
--neon: #00ff88;
|
| 12 |
+
--neon-dim:#00cc6a;
|
| 13 |
+
--red: #ff4444;
|
| 14 |
+
--red-dim: #cc2222;
|
| 15 |
+
--bg: #080810;
|
| 16 |
+
--card: #0f0f1a;
|
| 17 |
+
--card2: #13131f;
|
| 18 |
+
--border: #1e1e30;
|
| 19 |
+
--text: #e2e8f0;
|
| 20 |
+
--muted: #64748b;
|
| 21 |
+
}
|
| 22 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 23 |
+
|
| 24 |
+
body {
|
| 25 |
+
background: var(--bg);
|
| 26 |
+
font-family: 'JetBrains Mono', monospace;
|
| 27 |
+
color: var(--text);
|
| 28 |
+
min-height: 100vh;
|
| 29 |
+
background-image:
|
| 30 |
+
radial-gradient(ellipse 80% 50% at 50% -20%, #00ff8808 0%, transparent 60%),
|
| 31 |
+
linear-gradient(var(--border) 1px, transparent 1px),
|
| 32 |
+
linear-gradient(90deg, var(--border) 1px, transparent 1px);
|
| 33 |
+
background-size: 100% 100%, 44px 44px, 44px 44px;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* ββ Scrollbar ββ */
|
| 37 |
+
::-webkit-scrollbar { width: 4px; height: 4px; }
|
| 38 |
+
::-webkit-scrollbar-track { background: var(--bg); }
|
| 39 |
+
::-webkit-scrollbar-thumb { background: #00ff8833; border-radius: 4px; }
|
| 40 |
+
|
| 41 |
+
/* ββ Utilities ββ */
|
| 42 |
+
.neon { color: var(--neon); text-shadow: 0 0 12px #00ff8855; }
|
| 43 |
+
.red { color: var(--red); text-shadow: 0 0 12px #ff444455; }
|
| 44 |
+
.hidden { display: none !important; }
|
| 45 |
+
|
| 46 |
+
/* ββ Scanner ββ */
|
| 47 |
+
.scanner {
|
| 48 |
+
position: fixed; inset: 0; pointer-events: none; z-index: 100;
|
| 49 |
+
background: linear-gradient(to bottom,
|
| 50 |
+
transparent 0%, transparent 49.9%,
|
| 51 |
+
#00ff8806 50%, transparent 50.1%);
|
| 52 |
+
background-size: 100% 4px;
|
| 53 |
+
animation: scanMove 8s linear infinite;
|
| 54 |
+
}
|
| 55 |
+
@keyframes scanMove {
|
| 56 |
+
0% { background-position: 0 0; }
|
| 57 |
+
100% { background-position: 0 100vh; }
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* ββ Header ββ */
|
| 61 |
+
.header {
|
| 62 |
+
position: sticky; top: 0; z-index: 50;
|
| 63 |
+
background: #080810ee;
|
| 64 |
+
backdrop-filter: blur(12px);
|
| 65 |
+
border-bottom: 1px solid var(--border);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/* ββ Cards ββ */
|
| 69 |
+
.card {
|
| 70 |
+
background: var(--card);
|
| 71 |
+
border: 1px solid var(--border);
|
| 72 |
+
border-radius: 14px;
|
| 73 |
+
}
|
| 74 |
+
.card-inner {
|
| 75 |
+
background: var(--card2);
|
| 76 |
+
border: 1px solid var(--border);
|
| 77 |
+
border-radius: 10px;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/* ββ Upload zone ββ */
|
| 81 |
+
#dropZone {
|
| 82 |
+
border: 2px dashed #00ff8830;
|
| 83 |
+
border-radius: 12px;
|
| 84 |
+
cursor: pointer;
|
| 85 |
+
transition: all .25s ease;
|
| 86 |
+
min-height: 160px;
|
| 87 |
+
display: flex; align-items: center; justify-content: center;
|
| 88 |
+
}
|
| 89 |
+
#dropZone:hover, #dropZone.drag-over {
|
| 90 |
+
border-color: var(--neon);
|
| 91 |
+
background: #00ff8806;
|
| 92 |
+
box-shadow: 0 0 40px #00ff8812, inset 0 0 40px #00ff8806;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/* ββ Analyze button ββ */
|
| 96 |
+
#analyzeBtn {
|
| 97 |
+
background: transparent;
|
| 98 |
+
border: 1px solid #00ff8830;
|
| 99 |
+
color: #00ff8855;
|
| 100 |
+
border-radius: 8px;
|
| 101 |
+
padding: 12px 36px;
|
| 102 |
+
font-family: inherit;
|
| 103 |
+
font-weight: 700;
|
| 104 |
+
font-size: 13px;
|
| 105 |
+
letter-spacing: .15em;
|
| 106 |
+
cursor: not-allowed;
|
| 107 |
+
transition: all .25s ease;
|
| 108 |
+
}
|
| 109 |
+
#analyzeBtn.active {
|
| 110 |
+
border-color: var(--neon);
|
| 111 |
+
color: var(--neon);
|
| 112 |
+
cursor: pointer;
|
| 113 |
+
box-shadow: 0 0 20px #00ff8820;
|
| 114 |
+
}
|
| 115 |
+
#analyzeBtn.active:hover {
|
| 116 |
+
background: #00ff8810;
|
| 117 |
+
box-shadow: 0 0 30px #00ff8835;
|
| 118 |
+
transform: translateY(-1px);
|
| 119 |
+
}
|
| 120 |
+
#analyzeBtn:disabled { opacity: .45; cursor: not-allowed; transform: none !important; }
|
| 121 |
+
|
| 122 |
+
/* ββ Progress bar ββ */
|
| 123 |
+
.prog-track {
|
| 124 |
+
height: 3px; background: var(--border); border-radius: 99px; overflow: hidden;
|
| 125 |
+
}
|
| 126 |
+
.prog-fill {
|
| 127 |
+
height: 100%; border-radius: 99px;
|
| 128 |
+
background: linear-gradient(90deg, var(--neon), var(--neon-dim));
|
| 129 |
+
box-shadow: 0 0 8px var(--neon);
|
| 130 |
+
transition: width .4s ease;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
/* ββ Verdict card ββ */
|
| 134 |
+
#verdictCard {
|
| 135 |
+
border-radius: 14px;
|
| 136 |
+
border: 2px solid var(--border);
|
| 137 |
+
background: var(--card);
|
| 138 |
+
transition: border-color .5s, box-shadow .5s;
|
| 139 |
+
}
|
| 140 |
+
#verdictCard.fake {
|
| 141 |
+
border-color: var(--red);
|
| 142 |
+
box-shadow: 0 0 50px #ff444418, inset 0 0 60px #ff44440a;
|
| 143 |
+
}
|
| 144 |
+
#verdictCard.real {
|
| 145 |
+
border-color: var(--neon);
|
| 146 |
+
box-shadow: 0 0 50px #00ff8818, inset 0 0 60px #00ff880a;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
/* ββ Verdict badge ββ */
|
| 150 |
+
.verdict-badge {
|
| 151 |
+
width: 96px; height: 96px; border-radius: 50%;
|
| 152 |
+
border: 2px solid var(--border);
|
| 153 |
+
display: flex; align-items: center; justify-content: center;
|
| 154 |
+
font-size: 2.5rem;
|
| 155 |
+
transition: all .5s;
|
| 156 |
+
}
|
| 157 |
+
.verdict-badge.fake {
|
| 158 |
+
border-color: var(--red);
|
| 159 |
+
box-shadow: 0 0 30px #ff444440;
|
| 160 |
+
animation: pulseFake 2s ease-in-out infinite;
|
| 161 |
+
}
|
| 162 |
+
.verdict-badge.real {
|
| 163 |
+
border-color: var(--neon);
|
| 164 |
+
box-shadow: 0 0 30px #00ff8840;
|
| 165 |
+
animation: pulseReal 2s ease-in-out infinite;
|
| 166 |
+
}
|
| 167 |
+
@keyframes pulseFake {
|
| 168 |
+
0%,100% { box-shadow: 0 0 20px #ff444430; }
|
| 169 |
+
50% { box-shadow: 0 0 45px #ff444460; }
|
| 170 |
+
}
|
| 171 |
+
@keyframes pulseReal {
|
| 172 |
+
0%,100% { box-shadow: 0 0 20px #00ff8830; }
|
| 173 |
+
50% { box-shadow: 0 0 45px #00ff8860; }
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
/* ββ Confidence bar ββ */
|
| 177 |
+
.conf-track {
|
| 178 |
+
height: 10px; background: var(--border); border-radius: 99px; overflow: hidden;
|
| 179 |
+
}
|
| 180 |
+
.conf-fill {
|
| 181 |
+
height: 100%; border-radius: 99px;
|
| 182 |
+
transition: width 1.2s cubic-bezier(.22,1,.36,1);
|
| 183 |
+
}
|
| 184 |
+
.conf-fill.fake {
|
| 185 |
+
background: linear-gradient(90deg, #ff2222, #ff6666);
|
| 186 |
+
box-shadow: 0 0 12px #ff444466;
|
| 187 |
+
}
|
| 188 |
+
.conf-fill.real {
|
| 189 |
+
background: linear-gradient(90deg, var(--neon), var(--neon-dim));
|
| 190 |
+
box-shadow: 0 0 12px #00ff8866;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
/* ββ Risk meter ββ */
|
| 194 |
+
.risk-meter {
|
| 195 |
+
position: relative; height: 8px; border-radius: 99px; overflow: hidden;
|
| 196 |
+
background: linear-gradient(90deg, #00ff88, #ffcc00, #ff4444);
|
| 197 |
+
}
|
| 198 |
+
.risk-needle {
|
| 199 |
+
position: absolute; top: -4px;
|
| 200 |
+
width: 3px; height: 16px; border-radius: 2px;
|
| 201 |
+
background: white;
|
| 202 |
+
box-shadow: 0 0 6px white;
|
| 203 |
+
transition: left 1.2s cubic-bezier(.22,1,.36,1);
|
| 204 |
+
transform: translateX(-50%);
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/* ββ Insight items ββ */
|
| 208 |
+
.insight-item {
|
| 209 |
+
display: flex; align-items: flex-start; gap: 10px;
|
| 210 |
+
padding: 10px 12px; border-radius: 8px;
|
| 211 |
+
background: var(--card2);
|
| 212 |
+
border: 1px solid var(--border);
|
| 213 |
+
font-size: 12px; line-height: 1.5; color: #94a3b8;
|
| 214 |
+
animation: slideIn .3s ease both;
|
| 215 |
+
}
|
| 216 |
+
.insight-item .dot {
|
| 217 |
+
flex-shrink: 0; width: 6px; height: 6px; border-radius: 50%; margin-top: 5px;
|
| 218 |
+
}
|
| 219 |
+
@keyframes slideIn {
|
| 220 |
+
from { opacity:0; transform: translateX(-8px); }
|
| 221 |
+
to { opacity:1; transform: none; }
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
/* ββ Meta rows ββ */
|
| 225 |
+
.meta-row {
|
| 226 |
+
display: flex; justify-content: space-between; align-items: center;
|
| 227 |
+
padding: 7px 0;
|
| 228 |
+
border-bottom: 1px solid var(--border);
|
| 229 |
+
font-size: 12px;
|
| 230 |
+
}
|
| 231 |
+
.meta-row:last-child { border-bottom: none; }
|
| 232 |
+
.meta-key { color: var(--muted); letter-spacing: .05em; }
|
| 233 |
+
.meta-val { color: var(--text); font-weight: 600; }
|
| 234 |
+
|
| 235 |
+
/* ββ Timeline ββ */
|
| 236 |
+
#timelineChart {
|
| 237 |
+
display: flex; align-items: flex-end; gap: 3px;
|
| 238 |
+
height: 80px; overflow-x: auto; padding-bottom: 2px;
|
| 239 |
+
position: relative;
|
| 240 |
+
}
|
| 241 |
+
.t-bar {
|
| 242 |
+
flex-shrink: 0; width: 10px; border-radius: 3px 3px 0 0;
|
| 243 |
+
transition: opacity .3s, transform .2s;
|
| 244 |
+
cursor: pointer;
|
| 245 |
+
position: relative;
|
| 246 |
+
}
|
| 247 |
+
.t-bar:hover { transform: scaleY(1.08); filter: brightness(1.3); }
|
| 248 |
+
.t-bar .tooltip {
|
| 249 |
+
display: none; position: absolute; bottom: calc(100% + 6px); left: 50%;
|
| 250 |
+
transform: translateX(-50%);
|
| 251 |
+
background: #1e1e30; border: 1px solid var(--border);
|
| 252 |
+
color: var(--text); font-size: 10px; padding: 4px 7px; border-radius: 5px;
|
| 253 |
+
white-space: nowrap; z-index: 10; pointer-events: none;
|
| 254 |
+
}
|
| 255 |
+
.t-bar:hover .tooltip { display: block; }
|
| 256 |
+
.threshold-line {
|
| 257 |
+
position: absolute; left: 0; right: 0; height: 1px;
|
| 258 |
+
background: #facc1555; pointer-events: none;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
/* ββ Agent steps ββ */
|
| 262 |
+
.agent-card {
|
| 263 |
+
border-radius: 10px; padding: 12px; text-align: center;
|
| 264 |
+
background: var(--card2); border: 1px solid var(--border);
|
| 265 |
+
transition: border-color .3s, background .3s, box-shadow .3s;
|
| 266 |
+
}
|
| 267 |
+
.agent-card.active {
|
| 268 |
+
border-color: #00ff8844;
|
| 269 |
+
background: #00ff8808;
|
| 270 |
+
box-shadow: 0 0 20px #00ff8810;
|
| 271 |
+
}
|
| 272 |
+
.agent-card .a-status { font-size: 10px; color: var(--muted); margin-top: 4px; }
|
| 273 |
+
.agent-card.active .a-status { color: var(--neon); }
|
| 274 |
+
|
| 275 |
+
/* ββ Fade/slide animations ββ */
|
| 276 |
+
.fade-up {
|
| 277 |
+
animation: fadeUp .45s ease both;
|
| 278 |
+
}
|
| 279 |
+
@keyframes fadeUp {
|
| 280 |
+
from { opacity:0; transform: translateY(16px); }
|
| 281 |
+
to { opacity:1; transform: none; }
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
/* ββ Section label ββ */
|
| 285 |
+
.sec-label {
|
| 286 |
+
font-size: 10px; font-weight: 700; letter-spacing: .12em;
|
| 287 |
+
color: var(--muted); display: flex; align-items: center; gap: 6px;
|
| 288 |
+
}
|
| 289 |
+
.sec-label::before {
|
| 290 |
+
content: ''; display: inline-block;
|
| 291 |
+
width: 6px; height: 6px; border-radius: 50%;
|
| 292 |
+
background: var(--neon); box-shadow: 0 0 6px var(--neon);
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
/* ββ Glow badge ββ */
|
| 296 |
+
#modelBadge {
|
| 297 |
+
font-size: 10px; padding: 4px 12px; border-radius: 99px;
|
| 298 |
+
border: 1px solid var(--border); color: var(--muted);
|
| 299 |
+
letter-spacing: .08em; transition: all .3s;
|
| 300 |
+
}
|
| 301 |
+
#modelBadge.ready {
|
| 302 |
+
border-color: #00ff8844; color: #00ff88aa;
|
| 303 |
+
box-shadow: 0 0 10px #00ff8815;
|
| 304 |
+
}
|
| 305 |
+
</style>
|
| 306 |
+
</head>
|
| 307 |
+
<body>
|
| 308 |
+
|
| 309 |
+
<div class="scanner"></div>
|
| 310 |
+
|
| 311 |
+
<!-- ββ HEADER ββββββββββββββββββββββββββββββββββββββββββ -->
|
| 312 |
+
<header class="header">
|
| 313 |
+
<div style="max-width:900px; margin:0 auto; padding:14px 24px; display:flex; align-items:center; justify-content:space-between;">
|
| 314 |
+
<div style="display:flex; align-items:center; gap:12px;">
|
| 315 |
+
<div style="width:34px;height:34px;border-radius:8px;border:1px solid #00ff8855;
|
| 316 |
+
display:flex;align-items:center;justify-content:center;
|
| 317 |
+
box-shadow:0 0 14px #00ff8820;">
|
| 318 |
+
<svg width="16" height="16" fill="none" stroke="#00ff88" stroke-width="2" viewBox="0 0 24 24">
|
| 319 |
+
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
| 320 |
+
<path d="M11 8v6M8 11h6"/>
|
| 321 |
+
</svg>
|
| 322 |
+
</div>
|
| 323 |
+
<div>
|
| 324 |
+
<div style="font-size:15px;font-weight:700;letter-spacing:.12em;" class="neon">DEEPFAKE AUTHENTICATOR</div>
|
| 325 |
+
<div style="font-size:10px;color:var(--muted);letter-spacing:.1em;">AI-POWERED VIDEO FORENSICS</div>
|
| 326 |
+
</div>
|
| 327 |
+
</div>
|
| 328 |
+
<div id="modelBadge">CONNECTING...</div>
|
| 329 |
+
</div>
|
| 330 |
+
</header>
|
| 331 |
+
|
| 332 |
+
<!-- ββ MAIN ββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 333 |
+
<main style="max-width:900px; margin:0 auto; padding:32px 24px; display:flex; flex-direction:column; gap:20px;">
|
| 334 |
+
|
| 335 |
+
<!-- Upload card -->
|
| 336 |
+
<div class="card" style="padding:20px;">
|
| 337 |
+
<div id="dropZone">
|
| 338 |
+
<div id="uploadPrompt" style="text-align:center; padding:20px;">
|
| 339 |
+
<div style="width:56px;height:56px;border-radius:50%;border:1px solid #00ff8830;
|
| 340 |
+
display:flex;align-items:center;justify-content:center;margin:0 auto 14px;">
|
| 341 |
+
<svg width="24" height="24" fill="none" stroke="#00ff8866" stroke-width="1.5" viewBox="0 0 24 24">
|
| 342 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
| 343 |
+
<polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
|
| 344 |
+
</svg>
|
| 345 |
+
</div>
|
| 346 |
+
<p style="font-size:14px;font-weight:600;color:#cbd5e1;margin-bottom:6px;">Drop video here or click to upload</p>
|
| 347 |
+
<p style="font-size:11px;color:var(--muted);">MP4 Β· AVI Β· MOV Β· MKV Β· WebM Β· Max 100 MB</p>
|
| 348 |
+
</div>
|
| 349 |
+
|
| 350 |
+
<div id="fileChosen" class="hidden" style="display:flex;align-items:center;gap:14px;padding:20px;">
|
| 351 |
+
<div style="width:44px;height:44px;border-radius:8px;background:#00ff8810;border:1px solid #00ff8830;
|
| 352 |
+
display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
| 353 |
+
<svg width="20" height="20" fill="none" stroke="#00ff88" stroke-width="2" viewBox="0 0 24 24">
|
| 354 |
+
<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/>
|
| 355 |
+
<line x1="7" y1="2" x2="7" y2="22"/><line x1="17" y1="2" x2="17" y2="22"/>
|
| 356 |
+
<line x1="2" y1="12" x2="22" y2="12"/><line x1="2" y1="7" x2="7" y2="7"/>
|
| 357 |
+
<line x1="2" y1="17" x2="7" y2="17"/><line x1="17" y1="17" x2="22" y2="17"/>
|
| 358 |
+
<line x1="17" y1="7" x2="22" y2="7"/>
|
| 359 |
+
</svg>
|
| 360 |
+
</div>
|
| 361 |
+
<div style="flex:1;min-width:0;">
|
| 362 |
+
<p id="chosenName" style="font-size:13px;font-weight:600;color:#e2e8f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"></p>
|
| 363 |
+
<p id="chosenSize" style="font-size:11px;color:var(--muted);margin-top:2px;"></p>
|
| 364 |
+
</div>
|
| 365 |
+
<button id="clearBtn" type="button"
|
| 366 |
+
style="background:none;border:none;color:var(--muted);cursor:pointer;padding:6px;border-radius:6px;transition:color .2s;"
|
| 367 |
+
onmouseover="this.style.color='#ff4444'" onmouseout="this.style.color='var(--muted)'">
|
| 368 |
+
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
| 369 |
+
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
| 370 |
+
</svg>
|
| 371 |
+
</button>
|
| 372 |
+
</div>
|
| 373 |
+
</div>
|
| 374 |
+
|
| 375 |
+
<input type="file" id="fileInput" accept="video/*" style="display:none;" />
|
| 376 |
+
|
| 377 |
+
<div style="margin-top:16px;text-align:center;">
|
| 378 |
+
<button id="analyzeBtn" type="button" disabled onclick="analyzeVideo()">
|
| 379 |
+
βΆ ANALYZE VIDEO
|
| 380 |
+
</button>
|
| 381 |
+
</div>
|
| 382 |
+
</div>
|
| 383 |
+
|
| 384 |
+
<!-- Loading -->
|
| 385 |
+
<div id="loadingSection" class="hidden card fade-up" style="padding:24px;">
|
| 386 |
+
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;">
|
| 387 |
+
<div style="width:8px;height:8px;border-radius:50%;background:var(--neon);
|
| 388 |
+
box-shadow:0 0 8px var(--neon);animation:ping 1s ease-in-out infinite;"></div>
|
| 389 |
+
<span id="loadingStatus" class="neon" style="font-size:12px;font-weight:700;letter-spacing:.1em;">INITIALIZING...</span>
|
| 390 |
+
</div>
|
| 391 |
+
<div class="prog-track" style="margin-bottom:20px;">
|
| 392 |
+
<div id="progressBar" class="prog-fill" style="width:0%;"></div>
|
| 393 |
+
</div>
|
| 394 |
+
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;">
|
| 395 |
+
<div class="agent-card" id="ag0">
|
| 396 |
+
<div style="font-size:20px;margin-bottom:4px;">π¬</div>
|
| 397 |
+
<div style="font-size:10px;color:var(--muted);font-weight:600;">FRAME ANALYZER</div>
|
| 398 |
+
<div class="a-status">Waiting...</div>
|
| 399 |
+
</div>
|
| 400 |
+
<div class="agent-card" id="ag1">
|
| 401 |
+
<div style="font-size:20px;margin-bottom:4px;">π€</div>
|
| 402 |
+
<div style="font-size:10px;color:var(--muted);font-weight:600;">FACE DETECTOR</div>
|
| 403 |
+
<div class="a-status">Waiting...</div>
|
| 404 |
+
</div>
|
| 405 |
+
<div class="agent-card" id="ag2">
|
| 406 |
+
<div style="font-size:20px;margin-bottom:4px;">π§ </div>
|
| 407 |
+
<div style="font-size:10px;color:var(--muted);font-weight:600;">DECISION AGENT</div>
|
| 408 |
+
<div class="a-status">Waiting...</div>
|
| 409 |
+
</div>
|
| 410 |
+
<div class="agent-card" id="ag3">
|
| 411 |
+
<div style="font-size:20px;margin-bottom:4px;">π</div>
|
| 412 |
+
<div style="font-size:10px;color:var(--muted);font-weight:600;">REPORT GEN</div>
|
| 413 |
+
<div class="a-status">Waiting...</div>
|
| 414 |
+
</div>
|
| 415 |
+
</div>
|
| 416 |
+
</div>
|
| 417 |
+
|
| 418 |
+
<!-- Result -->
|
| 419 |
+
<div id="resultSection" class="hidden fade-up" style="display:flex;flex-direction:column;gap:16px;">
|
| 420 |
+
|
| 421 |
+
<!-- Verdict -->
|
| 422 |
+
<div id="verdictCard" style="padding:28px 32px;">
|
| 423 |
+
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:28px;">
|
| 424 |
+
|
| 425 |
+
<!-- Badge + label -->
|
| 426 |
+
<div style="text-align:center;flex-shrink:0;">
|
| 427 |
+
<div id="verdictBadge" class="verdict-badge" style="margin:0 auto 10px;">
|
| 428 |
+
<span id="verdictEmoji"></span>
|
| 429 |
+
</div>
|
| 430 |
+
<div id="verdictLabel" style="font-size:28px;font-weight:700;letter-spacing:.15em;"></div>
|
| 431 |
+
<div style="font-size:10px;color:var(--muted);letter-spacing:.1em;margin-top:3px;">VERDICT</div>
|
| 432 |
+
</div>
|
| 433 |
+
|
| 434 |
+
<!-- Confidence + risk -->
|
| 435 |
+
<div style="flex:1;min-width:220px;">
|
| 436 |
+
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:8px;">
|
| 437 |
+
<span style="font-size:11px;color:var(--muted);letter-spacing:.08em;">CONFIDENCE SCORE</span>
|
| 438 |
+
<span id="confValue" style="font-size:32px;font-weight:700;"></span>
|
| 439 |
+
</div>
|
| 440 |
+
<div class="conf-track" style="margin-bottom:18px;">
|
| 441 |
+
<div id="confBar" class="conf-fill" style="width:0%;"></div>
|
| 442 |
+
</div>
|
| 443 |
+
|
| 444 |
+
<!-- Risk meter -->
|
| 445 |
+
<div style="margin-bottom:6px;display:flex;justify-content:space-between;align-items:center;">
|
| 446 |
+
<span style="font-size:10px;color:var(--muted);letter-spacing:.08em;">RISK LEVEL</span>
|
| 447 |
+
<span id="riskLabel" style="font-size:10px;font-weight:700;letter-spacing:.08em;"></span>
|
| 448 |
+
</div>
|
| 449 |
+
<div class="risk-meter">
|
| 450 |
+
<div id="riskNeedle" class="risk-needle" style="left:50%;"></div>
|
| 451 |
+
</div>
|
| 452 |
+
<div style="display:flex;justify-content:space-between;margin-top:4px;">
|
| 453 |
+
<span style="font-size:9px;color:var(--muted);">LOW</span>
|
| 454 |
+
<span style="font-size:9px;color:var(--muted);">MEDIUM</span>
|
| 455 |
+
<span style="font-size:9px;color:var(--muted);">HIGH</span>
|
| 456 |
+
</div>
|
| 457 |
+
</div>
|
| 458 |
+
</div>
|
| 459 |
+
</div>
|
| 460 |
+
|
| 461 |
+
<!-- Insights + Meta -->
|
| 462 |
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
| 463 |
+
|
| 464 |
+
<div class="card" style="padding:18px;">
|
| 465 |
+
<div class="sec-label" style="margin-bottom:14px;">ANALYSIS INSIGHTS</div>
|
| 466 |
+
<div id="detailsList" style="display:flex;flex-direction:column;gap:8px;"></div>
|
| 467 |
+
</div>
|
| 468 |
+
|
| 469 |
+
<div class="card" style="padding:18px;">
|
| 470 |
+
<div class="sec-label" style="margin-bottom:14px;">VIDEO METADATA</div>
|
| 471 |
+
<div id="metaGrid"></div>
|
| 472 |
+
|
| 473 |
+
<!-- Model info -->
|
| 474 |
+
<div style="margin-top:14px;padding-top:12px;border-top:1px solid var(--border);">
|
| 475 |
+
<div class="sec-label" style="margin-bottom:10px;">DETECTION ENGINE</div>
|
| 476 |
+
<div style="font-size:10px;color:var(--muted);line-height:1.8;">
|
| 477 |
+
<div>π¬ dima806/deepfake_vs_real (99.3% acc)</div>
|
| 478 |
+
<div>π¬ prithivMLmods/Detector-v2 (92.1% acc)</div>
|
| 479 |
+
<div style="margin-top:4px;color:#00ff8877;">β‘ Weighted ensemble Β· ViT architecture</div>
|
| 480 |
+
</div>
|
| 481 |
+
</div>
|
| 482 |
+
</div>
|
| 483 |
+
</div>
|
| 484 |
+
|
| 485 |
+
<!-- Frame Timeline -->
|
| 486 |
+
<div class="card" style="padding:18px;">
|
| 487 |
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;flex-wrap:wrap;gap:8px;">
|
| 488 |
+
<div class="sec-label">FRAME-BY-FRAME TIMELINE</div>
|
| 489 |
+
<div style="display:flex;align-items:center;gap:14px;font-size:10px;color:var(--muted);">
|
| 490 |
+
<span style="display:flex;align-items:center;gap:5px;">
|
| 491 |
+
<span style="width:10px;height:10px;border-radius:2px;background:var(--neon);display:inline-block;"></span>REAL
|
| 492 |
+
</span>
|
| 493 |
+
<span style="display:flex;align-items:center;gap:5px;">
|
| 494 |
+
<span style="width:10px;height:10px;border-radius:2px;background:var(--red);display:inline-block;"></span>FAKE
|
| 495 |
+
</span>
|
| 496 |
+
<span style="display:flex;align-items:center;gap:5px;">
|
| 497 |
+
<span style="width:20px;height:1px;background:#facc1566;display:inline-block;"></span>60% THRESHOLD
|
| 498 |
+
</span>
|
| 499 |
+
</div>
|
| 500 |
+
</div>
|
| 501 |
+
<div id="timelineChart"></div>
|
| 502 |
+
</div>
|
| 503 |
+
|
| 504 |
+
<div style="text-align:center;">
|
| 505 |
+
<button onclick="resetAll()" type="button"
|
| 506 |
+
style="background:none;border:1px solid var(--border);color:var(--muted);
|
| 507 |
+
font-family:inherit;font-size:12px;letter-spacing:.1em;padding:10px 24px;
|
| 508 |
+
border-radius:8px;cursor:pointer;transition:all .2s;"
|
| 509 |
+
onmouseover="this.style.borderColor='#00ff8844';this.style.color='var(--neon)'"
|
| 510 |
+
onmouseout="this.style.borderColor='var(--border)';this.style.color='var(--muted)'">
|
| 511 |
+
βΊ ANALYZE ANOTHER VIDEO
|
| 512 |
+
</button>
|
| 513 |
+
</div>
|
| 514 |
+
</div>
|
| 515 |
+
|
| 516 |
+
<!-- Error -->
|
| 517 |
+
<div id="errorSection" class="hidden card fade-up"
|
| 518 |
+
style="padding:20px;border-color:#ff444433;background:#ff44440a;">
|
| 519 |
+
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
|
| 520 |
+
<span style="font-size:18px;">β οΈ</span>
|
| 521 |
+
<span style="color:var(--red);font-weight:700;font-size:13px;letter-spacing:.08em;">ANALYSIS FAILED</span>
|
| 522 |
+
</div>
|
| 523 |
+
<p id="errorMsg" style="font-size:12px;color:var(--muted);line-height:1.6;"></p>
|
| 524 |
+
<button onclick="resetAll()" type="button"
|
| 525 |
+
style="margin-top:12px;background:none;border:none;color:var(--muted);
|
| 526 |
+
font-family:inherit;font-size:11px;cursor:pointer;letter-spacing:.08em;"
|
| 527 |
+
onmouseover="this.style.color='var(--neon)'" onmouseout="this.style.color='var(--muted)'">
|
| 528 |
+
βΊ Try again
|
| 529 |
+
</button>
|
| 530 |
+
</div>
|
| 531 |
+
|
| 532 |
+
</main>
|
| 533 |
+
|
| 534 |
+
<footer style="border-top:1px solid var(--border);padding:20px;text-align:center;
|
| 535 |
+
font-size:10px;color:#334155;letter-spacing:.12em;margin-top:20px;">
|
| 536 |
+
DEEPFAKE AUTHENTICATOR Β· MEDIAPIPE + ENSEMBLE ViT Β· LOCAL INFERENCE
|
| 537 |
+
</footer>
|
| 538 |
+
|
| 539 |
+
<style>
|
| 540 |
+
@keyframes ping {
|
| 541 |
+
0%,100% { transform:scale(1); opacity:1; }
|
| 542 |
+
50% { transform:scale(1.5); opacity:.5; }
|
| 543 |
+
}
|
| 544 |
+
@media (max-width: 640px) {
|
| 545 |
+
#verdictCard > div { flex-direction: column !important; }
|
| 546 |
+
#resultSection > div:nth-child(2) { grid-template-columns: 1fr !important; }
|
| 547 |
+
}
|
| 548 |
+
</style>
|
| 549 |
+
|
| 550 |
+
<script src="script.js"></script>
|
| 551 |
+
</body>
|
| 552 |
+
</html>
|
frontend/script.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Deepfake Authenticator β Frontend Logic
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
const API_BASE = (window.location.protocol === 'file:')
|
| 6 |
+
? 'http://localhost:8000'
|
| 7 |
+
: window.location.origin;
|
| 8 |
+
|
| 9 |
+
let selectedFile = null;
|
| 10 |
+
|
| 11 |
+
// ββ Boot ββββββββββββββββββββββββββββββββββββββ
|
| 12 |
+
window.addEventListener('load', () => {
|
| 13 |
+
initUpload();
|
| 14 |
+
pingHealth();
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
// ββ Health ββββββββββββββββββββββββββββββββββββ
|
| 18 |
+
async function pingHealth() {
|
| 19 |
+
const badge = document.getElementById('modelBadge');
|
| 20 |
+
for (let i = 0; i < 8; i++) {
|
| 21 |
+
try {
|
| 22 |
+
const r = await fetch(`${API_BASE}/health`);
|
| 23 |
+
if (r.ok) {
|
| 24 |
+
const d = await r.json();
|
| 25 |
+
if (d.ready) {
|
| 26 |
+
badge.textContent = d.model.toUpperCase();
|
| 27 |
+
badge.classList.add('ready');
|
| 28 |
+
return;
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
} catch (_) {}
|
| 32 |
+
badge.textContent = `CONNECTING ${i + 1}/8...`;
|
| 33 |
+
await sleep(2000);
|
| 34 |
+
}
|
| 35 |
+
badge.textContent = 'SERVER OFFLINE';
|
| 36 |
+
badge.style.borderColor = '#ff444444';
|
| 37 |
+
badge.style.color = '#ff4444aa';
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// ββ Upload wiring βββββββββββββββββββββββββββββ
|
| 41 |
+
function initUpload() {
|
| 42 |
+
const zone = document.getElementById('dropZone');
|
| 43 |
+
const input = document.getElementById('fileInput');
|
| 44 |
+
const clear = document.getElementById('clearBtn');
|
| 45 |
+
|
| 46 |
+
zone.addEventListener('click', e => {
|
| 47 |
+
if (!e.target.closest('#clearBtn')) input.click();
|
| 48 |
+
});
|
| 49 |
+
input.addEventListener('change', () => {
|
| 50 |
+
if (input.files?.[0]) applyFile(input.files[0]);
|
| 51 |
+
});
|
| 52 |
+
clear.addEventListener('click', e => { e.stopPropagation(); clearFile(); });
|
| 53 |
+
|
| 54 |
+
zone.addEventListener('dragover', e => { e.preventDefault(); e.stopPropagation(); zone.classList.add('drag-over'); });
|
| 55 |
+
zone.addEventListener('dragleave', e => { e.preventDefault(); zone.classList.remove('drag-over'); });
|
| 56 |
+
zone.addEventListener('drop', e => {
|
| 57 |
+
e.preventDefault(); e.stopPropagation();
|
| 58 |
+
zone.classList.remove('drag-over');
|
| 59 |
+
const f = e.dataTransfer.files[0];
|
| 60 |
+
if (f?.type.startsWith('video/')) applyFile(f);
|
| 61 |
+
else showError('Please drop a valid video file (MP4, AVI, MOV, MKV, WebM).');
|
| 62 |
+
});
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
function applyFile(file) {
|
| 66 |
+
selectedFile = file;
|
| 67 |
+
document.getElementById('uploadPrompt').classList.add('hidden');
|
| 68 |
+
const fc = document.getElementById('fileChosen');
|
| 69 |
+
fc.classList.remove('hidden');
|
| 70 |
+
fc.style.display = 'flex';
|
| 71 |
+
document.getElementById('chosenName').textContent = file.name;
|
| 72 |
+
document.getElementById('chosenSize').textContent = fmtBytes(file.size);
|
| 73 |
+
|
| 74 |
+
const btn = document.getElementById('analyzeBtn');
|
| 75 |
+
btn.disabled = false;
|
| 76 |
+
btn.classList.add('active');
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
function clearFile() {
|
| 80 |
+
selectedFile = null;
|
| 81 |
+
document.getElementById('fileInput').value = '';
|
| 82 |
+
document.getElementById('fileChosen').classList.add('hidden');
|
| 83 |
+
document.getElementById('uploadPrompt').classList.remove('hidden');
|
| 84 |
+
const btn = document.getElementById('analyzeBtn');
|
| 85 |
+
btn.disabled = true;
|
| 86 |
+
btn.classList.remove('active');
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
function resetAll() {
|
| 90 |
+
clearFile();
|
| 91 |
+
['loadingSection','resultSection','errorSection'].forEach(hide);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// ββ Analyze βββββββββββββββββββββββββββββββββββ
|
| 95 |
+
async function analyzeVideo() {
|
| 96 |
+
if (!selectedFile) return;
|
| 97 |
+
|
| 98 |
+
show('loadingSection');
|
| 99 |
+
['resultSection','errorSection'].forEach(hide);
|
| 100 |
+
document.getElementById('analyzeBtn').disabled = true;
|
| 101 |
+
|
| 102 |
+
startAgentAnim();
|
| 103 |
+
|
| 104 |
+
const fd = new FormData();
|
| 105 |
+
fd.append('file', selectedFile);
|
| 106 |
+
|
| 107 |
+
try {
|
| 108 |
+
const res = await fetch(`${API_BASE}/analyze`, { method: 'POST', body: fd });
|
| 109 |
+
if (!res.ok) {
|
| 110 |
+
const e = await res.json().catch(() => ({}));
|
| 111 |
+
throw new Error(e.detail || `Server error ${res.status}`);
|
| 112 |
+
}
|
| 113 |
+
renderResult(await res.json());
|
| 114 |
+
} catch (err) {
|
| 115 |
+
showError(err.message || 'Unknown error. Is the server running at ' + API_BASE + '?');
|
| 116 |
+
} finally {
|
| 117 |
+
hide('loadingSection');
|
| 118 |
+
stopAgentAnim();
|
| 119 |
+
document.getElementById('analyzeBtn').disabled = false;
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// ββ Agent animation βββββββββββββββββββββββββββ
|
| 124 |
+
const STEPS = [
|
| 125 |
+
{ id:'ag0', label:'Extracting frames...', prog:12 },
|
| 126 |
+
{ id:'ag1', label:'Detecting faces...', prog:38 },
|
| 127 |
+
{ id:'ag2', label:'Running AI ensemble...', prog:78 },
|
| 128 |
+
{ id:'ag3', label:'Generating report...', prog:94 },
|
| 129 |
+
];
|
| 130 |
+
const _timers = [];
|
| 131 |
+
|
| 132 |
+
function startAgentAnim() {
|
| 133 |
+
const delays = [0, 1600, 4200, 7500];
|
| 134 |
+
STEPS.forEach((s, i) => {
|
| 135 |
+
_timers.push(setTimeout(() => {
|
| 136 |
+
const c = document.getElementById(s.id);
|
| 137 |
+
if (!c) return;
|
| 138 |
+
c.classList.add('active');
|
| 139 |
+
c.querySelector('.a-status').textContent = s.label;
|
| 140 |
+
document.getElementById('progressBar').style.width = s.prog + '%';
|
| 141 |
+
document.getElementById('loadingStatus').textContent = s.label.toUpperCase();
|
| 142 |
+
}, delays[i]));
|
| 143 |
+
});
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
function stopAgentAnim() {
|
| 147 |
+
_timers.forEach(clearTimeout); _timers.length = 0;
|
| 148 |
+
STEPS.forEach(s => {
|
| 149 |
+
const c = document.getElementById(s.id);
|
| 150 |
+
if (!c) return;
|
| 151 |
+
c.classList.remove('active');
|
| 152 |
+
c.querySelector('.a-status').textContent = 'Waiting...';
|
| 153 |
+
});
|
| 154 |
+
document.getElementById('progressBar').style.width = '0%';
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// ββ Render result βββββββββββββββββββββββββββββ
|
| 158 |
+
function renderResult(data) {
|
| 159 |
+
const fake = data.result === 'FAKE';
|
| 160 |
+
const pct = data.confidence;
|
| 161 |
+
|
| 162 |
+
// Verdict card
|
| 163 |
+
const vc = document.getElementById('verdictCard');
|
| 164 |
+
vc.className = 'fade-up ' + (fake ? 'fake' : 'real');
|
| 165 |
+
|
| 166 |
+
// Badge
|
| 167 |
+
const badge = document.getElementById('verdictBadge');
|
| 168 |
+
badge.className = 'verdict-badge ' + (fake ? 'fake' : 'real');
|
| 169 |
+
document.getElementById('verdictEmoji').textContent = fake ? 'β οΈ' : 'β
';
|
| 170 |
+
|
| 171 |
+
// Label
|
| 172 |
+
const lbl = document.getElementById('verdictLabel');
|
| 173 |
+
lbl.textContent = data.result;
|
| 174 |
+
lbl.style.color = fake ? 'var(--red)' : 'var(--neon)';
|
| 175 |
+
lbl.style.textShadow = fake ? '0 0 20px #ff444466' : '0 0 20px #00ff8866';
|
| 176 |
+
|
| 177 |
+
// Confidence
|
| 178 |
+
document.getElementById('confValue').textContent = pct + '%';
|
| 179 |
+
document.getElementById('confValue').style.color = fake ? 'var(--red)' : 'var(--neon)';
|
| 180 |
+
const bar = document.getElementById('confBar');
|
| 181 |
+
bar.className = 'conf-fill ' + (fake ? 'fake' : 'real');
|
| 182 |
+
setTimeout(() => { bar.style.width = pct + '%'; }, 80);
|
| 183 |
+
|
| 184 |
+
// Risk needle
|
| 185 |
+
const needle = document.getElementById('riskNeedle');
|
| 186 |
+
const riskLbl = document.getElementById('riskLabel');
|
| 187 |
+
setTimeout(() => { needle.style.left = pct + '%'; }, 80);
|
| 188 |
+
if (pct < 35) {
|
| 189 |
+
riskLbl.textContent = 'LOW'; riskLbl.style.color = 'var(--neon)';
|
| 190 |
+
} else if (pct < 60) {
|
| 191 |
+
riskLbl.textContent = 'MEDIUM'; riskLbl.style.color = '#facc15';
|
| 192 |
+
} else if (pct < 80) {
|
| 193 |
+
riskLbl.textContent = 'HIGH'; riskLbl.style.color = '#f97316';
|
| 194 |
+
} else {
|
| 195 |
+
riskLbl.textContent = 'CRITICAL'; riskLbl.style.color = 'var(--red)';
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
// Insights
|
| 199 |
+
const dl = document.getElementById('detailsList');
|
| 200 |
+
dl.innerHTML = '';
|
| 201 |
+
(data.details || []).forEach((txt, i) => {
|
| 202 |
+
const div = document.createElement('div');
|
| 203 |
+
div.className = 'insight-item';
|
| 204 |
+
div.style.animationDelay = (i * 60) + 'ms';
|
| 205 |
+
div.innerHTML =
|
| 206 |
+
`<span class="dot" style="background:${fake ? 'var(--red)' : 'var(--neon)'};
|
| 207 |
+
box-shadow:0 0 6px ${fake ? '#ff444466' : '#00ff8866'};"></span>
|
| 208 |
+
<span>${esc(txt)}</span>`;
|
| 209 |
+
dl.appendChild(div);
|
| 210 |
+
});
|
| 211 |
+
|
| 212 |
+
// Metadata
|
| 213 |
+
const meta = data.metadata || {};
|
| 214 |
+
const mg = document.getElementById('metaGrid');
|
| 215 |
+
mg.innerHTML = '';
|
| 216 |
+
[
|
| 217 |
+
['Frames Analyzed', meta.frames_analyzed ?? 'β'],
|
| 218 |
+
['Faces Detected', meta.frames_with_faces ?? 'β'],
|
| 219 |
+
['Duration', meta.video_duration_sec ? meta.video_duration_sec + 's' : 'β'],
|
| 220 |
+
['FPS', meta.video_fps ?? 'β'],
|
| 221 |
+
['Resolution', meta.resolution ?? 'β'],
|
| 222 |
+
['Processing Time', data.processing_time_sec ? data.processing_time_sec + 's' : 'β'],
|
| 223 |
+
].forEach(([k, v]) => {
|
| 224 |
+
const row = document.createElement('div');
|
| 225 |
+
row.className = 'meta-row';
|
| 226 |
+
row.innerHTML = `<span class="meta-key">${k}</span><span class="meta-val">${v}</span>`;
|
| 227 |
+
mg.appendChild(row);
|
| 228 |
+
});
|
| 229 |
+
|
| 230 |
+
// Timeline
|
| 231 |
+
buildTimeline(data.frame_timeline || []);
|
| 232 |
+
|
| 233 |
+
show('resultSection');
|
| 234 |
+
document.getElementById('resultSection').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
// ββ Timeline ββββββββββββββββββββββββββββββββββ
|
| 238 |
+
function buildTimeline(frames) {
|
| 239 |
+
const chart = document.getElementById('timelineChart');
|
| 240 |
+
chart.innerHTML = '';
|
| 241 |
+
|
| 242 |
+
if (!frames.length) {
|
| 243 |
+
chart.innerHTML = '<p style="font-size:11px;color:var(--muted);">No timeline data available</p>';
|
| 244 |
+
return;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
const maxH = 72;
|
| 248 |
+
const threshold = 60;
|
| 249 |
+
|
| 250 |
+
// Threshold line
|
| 251 |
+
const line = document.createElement('div');
|
| 252 |
+
line.className = 'threshold-line';
|
| 253 |
+
line.style.bottom = ((threshold / 100) * maxH) + 'px';
|
| 254 |
+
chart.appendChild(line);
|
| 255 |
+
|
| 256 |
+
frames.forEach((pt, idx) => {
|
| 257 |
+
const pct = pt.fake_pct;
|
| 258 |
+
const h = Math.max(4, (pct / 100) * maxH);
|
| 259 |
+
const hot = pct >= threshold;
|
| 260 |
+
|
| 261 |
+
const bar = document.createElement('div');
|
| 262 |
+
bar.className = 't-bar';
|
| 263 |
+
bar.style.height = h + 'px';
|
| 264 |
+
bar.style.background = hot
|
| 265 |
+
? 'linear-gradient(to top, #ff2222, #ff6666)'
|
| 266 |
+
: 'linear-gradient(to top, #00cc6a, #00ff88)';
|
| 267 |
+
bar.style.boxShadow = hot ? '0 0 6px #ff444455' : '0 0 4px #00ff8844';
|
| 268 |
+
bar.style.opacity = '0';
|
| 269 |
+
bar.style.transition = 'opacity .3s ease, transform .2s ease';
|
| 270 |
+
|
| 271 |
+
// Tooltip
|
| 272 |
+
bar.innerHTML = `<div class="tooltip">Frame ${pt.frame}<br>${pct}% fake</div>`;
|
| 273 |
+
|
| 274 |
+
chart.appendChild(bar);
|
| 275 |
+
setTimeout(() => { bar.style.opacity = '1'; }, 30 + idx * 20);
|
| 276 |
+
});
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
// ββ Error βββββββββββββββββββββββββββββββββββββ
|
| 280 |
+
function showError(msg) {
|
| 281 |
+
hide('loadingSection');
|
| 282 |
+
document.getElementById('errorMsg').textContent = msg;
|
| 283 |
+
show('errorSection');
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
// ββ Helpers βββββββββββββββββββββββββββββββββββ
|
| 287 |
+
function show(id) {
|
| 288 |
+
const el = document.getElementById(id);
|
| 289 |
+
el.classList.remove('hidden');
|
| 290 |
+
el.style.display = '';
|
| 291 |
+
}
|
| 292 |
+
function hide(id) { document.getElementById(id).classList.add('hidden'); }
|
| 293 |
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
| 294 |
+
function fmtBytes(b) {
|
| 295 |
+
if (b < 1024) return b + ' B';
|
| 296 |
+
if (b < 1048576) return (b/1024).toFixed(1) + ' KB';
|
| 297 |
+
return (b/1048576).toFixed(1) + ' MB';
|
| 298 |
+
}
|
| 299 |
+
function esc(s) {
|
| 300 |
+
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
| 301 |
+
}
|
setup.bat
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
echo.
|
| 3 |
+
echo ==========================================
|
| 4 |
+
echo DEEPFAKE AUTHENTICATOR -- SETUP
|
| 5 |
+
echo ==========================================
|
| 6 |
+
echo.
|
| 7 |
+
|
| 8 |
+
where python >nul 2>&1
|
| 9 |
+
if %errorlevel% neq 0 (
|
| 10 |
+
echo ERROR: Python not found. Please install Python 3.9+
|
| 11 |
+
pause
|
| 12 |
+
exit /b 1
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
echo Creating virtual environment...
|
| 16 |
+
python -m venv venv
|
| 17 |
+
call venv\Scripts\activate.bat
|
| 18 |
+
|
| 19 |
+
echo Upgrading pip...
|
| 20 |
+
python -m pip install --upgrade pip -q
|
| 21 |
+
|
| 22 |
+
echo Installing core dependencies...
|
| 23 |
+
pip install fastapi==0.111.0 "uvicorn[standard]==0.29.0" python-multipart==0.0.9 -q
|
| 24 |
+
pip install opencv-python==4.9.0.80 mediapipe==0.10.14 numpy==1.26.4 Pillow==10.3.0 -q
|
| 25 |
+
|
| 26 |
+
echo Installing HuggingFace dependencies (optional)...
|
| 27 |
+
pip install transformers==4.41.0 torch==2.3.0 --index-url https://download.pytorch.org/whl/cpu -q
|
| 28 |
+
if %errorlevel% neq 0 (
|
| 29 |
+
echo WARNING: torch/transformers install failed -- will use heuristic fallback
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
echo.
|
| 33 |
+
echo ==========================================
|
| 34 |
+
echo SETUP COMPLETE
|
| 35 |
+
echo ==========================================
|
| 36 |
+
echo.
|
| 37 |
+
echo To start the server:
|
| 38 |
+
echo venv\Scripts\activate.bat
|
| 39 |
+
echo cd backend
|
| 40 |
+
echo python main.py
|
| 41 |
+
echo.
|
| 42 |
+
echo Then open: http://localhost:8000
|
| 43 |
+
echo.
|
| 44 |
+
pause
|
setup.sh
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Deepfake Authenticator β Setup Script
|
| 3 |
+
|
| 4 |
+
set -e
|
| 5 |
+
|
| 6 |
+
echo ""
|
| 7 |
+
echo "ββββββββββββββββββββββββββββββββββββββββββββ"
|
| 8 |
+
echo "β DEEPFAKE AUTHENTICATOR β SETUP β"
|
| 9 |
+
echo "ββββββββββββββββββββββββββββββββββββββββββββ"
|
| 10 |
+
echo ""
|
| 11 |
+
|
| 12 |
+
# Check Python
|
| 13 |
+
if ! command -v python3 &>/dev/null; then
|
| 14 |
+
echo "β Python 3 not found. Please install Python 3.9+"
|
| 15 |
+
exit 1
|
| 16 |
+
fi
|
| 17 |
+
|
| 18 |
+
PYTHON_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
|
| 19 |
+
echo "β
Python $PYTHON_VERSION detected"
|
| 20 |
+
|
| 21 |
+
# Create virtual environment
|
| 22 |
+
echo ""
|
| 23 |
+
echo "π¦ Creating virtual environment..."
|
| 24 |
+
python3 -m venv venv
|
| 25 |
+
source venv/bin/activate
|
| 26 |
+
|
| 27 |
+
# Upgrade pip
|
| 28 |
+
pip install --upgrade pip -q
|
| 29 |
+
|
| 30 |
+
# Install dependencies
|
| 31 |
+
echo ""
|
| 32 |
+
echo "π₯ Installing dependencies..."
|
| 33 |
+
echo " (This may take a few minutes for torch/transformers)"
|
| 34 |
+
echo ""
|
| 35 |
+
|
| 36 |
+
pip install fastapi==0.111.0 uvicorn[standard]==0.29.0 python-multipart==0.0.9 -q
|
| 37 |
+
pip install opencv-python==4.9.0.80 mediapipe==0.10.14 numpy==1.26.4 Pillow==10.3.0 -q
|
| 38 |
+
|
| 39 |
+
echo ""
|
| 40 |
+
echo "π€ Installing HuggingFace model dependencies..."
|
| 41 |
+
echo " (Optional β skip with Ctrl+C if you want heuristic-only mode)"
|
| 42 |
+
pip install transformers==4.41.0 torch==2.3.0 --index-url https://download.pytorch.org/whl/cpu -q || \
|
| 43 |
+
echo "β οΈ torch/transformers install failed β will use heuristic fallback"
|
| 44 |
+
|
| 45 |
+
echo ""
|
| 46 |
+
echo "ββββββββββββββββββββββββββββββββββββββββββββ"
|
| 47 |
+
echo "β SETUP COMPLETE β
β"
|
| 48 |
+
echo "ββββββββββββββββββββββββββββββββββββββββββββ"
|
| 49 |
+
echo ""
|
| 50 |
+
echo "To start the server:"
|
| 51 |
+
echo " source venv/bin/activate"
|
| 52 |
+
echo " cd backend && python main.py"
|
| 53 |
+
echo ""
|
| 54 |
+
echo "Then open: http://localhost:8000"
|
| 55 |
+
echo ""
|