ICEFROG96 commited on
Commit
7513d63
·
0 Parent(s):

Create final Hugging Face Space snapshot

Browse files
.dockerignore ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitignore
3
+ .venv
4
+ .uv-cache
5
+ .idea
6
+ __pycache__/
7
+ *.pyc
8
+ *.pyo
9
+ *.pyd
10
+ *.log
11
+ .DS_Store
12
+ App/uploads
13
+ train_sample_videos
14
+ prepared_dataset
15
+ split_dataset
16
+ mtcnn
17
+ archive.zip
18
+ best_model.keras
19
+ tests
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ tmp_checkpoint/best_model.keras filter=lfs diff=lfs merge=lfs -text
2
+ App/static/images.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # agent
10
+ .codex
11
+
12
+ # Distribution / packaging
13
+ .Python
14
+ build/
15
+ develop-eggs/
16
+ dist/
17
+ downloads/
18
+ eggs/
19
+ .eggs/
20
+ lib/
21
+ lib64/
22
+ parts/
23
+ sdist/
24
+ var/
25
+ wheels/
26
+ pip-wheel-metadata/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+
57
+ # Translations
58
+ *.mo
59
+ *.pot
60
+
61
+ # Django stuff:
62
+ *.log
63
+ local_settings.py
64
+ db.sqlite3
65
+ db.sqlite3-journal
66
+
67
+ # Flask stuff:
68
+ instance/
69
+ .webassets-cache
70
+
71
+ # Scrapy stuff:
72
+ .scrapy
73
+
74
+ # Sphinx documentation
75
+ docs/_build/
76
+
77
+ # PyBuilder
78
+ target/
79
+
80
+ # Jupyter Notebook
81
+ .ipynb_checkpoints
82
+
83
+ # IPython
84
+ profile_default/
85
+ ipython_config.py
86
+
87
+ # pyenv
88
+ .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98
+ __pypackages__/
99
+
100
+ # Celery stuff
101
+ celerybeat-schedule
102
+ celerybeat.pid
103
+
104
+ # SageMath parsed files
105
+ *.sage.py
106
+
107
+ # Environments
108
+ .env
109
+ .venv
110
+ env/
111
+ venv/
112
+ ENV/
113
+ env.bak/
114
+ venv.bak/
115
+
116
+ # Spyder project settings
117
+ .spyderproject
118
+ .spyproject
119
+
120
+ # Rope project settings
121
+ .ropeproject
122
+
123
+ # mkdocs documentation
124
+ /site
125
+
126
+ # mypy
127
+ .mypy_cache/
128
+ .dmypy.json
129
+ dmypy.json
130
+
131
+ # Pyre type checker
132
+ .pyre/
133
+
134
+ #temp dataset folders
135
+ tmp_*/
136
+ !tmp_checkpoint/
137
+ !tmp_checkpoint/best_model.keras
138
+ !tmp_checkpoint/best_model_phase1.keras
139
+ mtcnn/
140
+ /.conda
141
+ /train_sample_videos
142
+ /split_dataset
143
+ /prepared_dataset
144
+ /App/uploads
145
+ archive.zip
App/app.py ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ import math
4
+ import logging
5
+ import subprocess
6
+ import cv2
7
+ import numpy as np
8
+ import imageio_ffmpeg
9
+ from mtcnn import MTCNN
10
+ from flask import Flask
11
+ from tensorflow.keras.models import load_model
12
+ from tensorflow.keras.applications.efficientnet import preprocess_input
13
+ import keras.src.layers.normalization.batch_normalization as _bn_module
14
+
15
+ import sys
16
+ from config import preview_face_detector_enabled, resolve_server_port
17
+
18
+ logging.basicConfig(
19
+ level=logging.INFO,
20
+ format='%(asctime)s [%(levelname)s] %(message)s',
21
+ datefmt='%Y-%m-%d %H:%M:%S',
22
+ stream=sys.stderr
23
+ )
24
+
25
+ # Monkey-patch BatchNormalization to accept legacy renorm kwargs
26
+ _OrigBN = _bn_module.BatchNormalization
27
+ _orig_bn_init = _OrigBN.__init__
28
+
29
+
30
+ def _patched_bn_init(self, *args, **kwargs):
31
+ kwargs.pop('renorm', None)
32
+ kwargs.pop('renorm_clipping', None)
33
+ kwargs.pop('renorm_momentum', None)
34
+ _orig_bn_init(self, *args, **kwargs)
35
+
36
+
37
+ _OrigBN.__init__ = _patched_bn_init
38
+ logger = logging.getLogger(__name__)
39
+
40
+ app = Flask(__name__)
41
+ app.config['UPLOAD_FOLDER'] = os.path.join(os.path.dirname(__file__), 'uploads')
42
+ app.config['MAX_CONTENT_LENGTH'] = 200 * 1024 * 1024 # 200 MB limit
43
+ ALLOWED_EXTENSIONS = {'mp4', 'avi', 'mov', 'mkv', 'wmv'}
44
+
45
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
46
+
47
+ PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
48
+
49
+
50
+ def resolve_model_path():
51
+ """Prefer the canonical training output, but tolerate the legacy root copy."""
52
+ candidates = [
53
+ os.path.join(PROJECT_ROOT, 'tmp_checkpoint', 'best_model.keras'),
54
+ os.path.join(PROJECT_ROOT, 'best_model.keras'),
55
+ ]
56
+ for candidate in candidates:
57
+ if os.path.isfile(candidate):
58
+ return candidate
59
+ raise FileNotFoundError(
60
+ 'No model file found. Expected one of: '
61
+ + ', '.join(candidates)
62
+ )
63
+
64
+
65
+ # Load the trained model
66
+ MODEL_PATH = resolve_model_path()
67
+ logger.info('Loading model from %s', MODEL_PATH)
68
+ model = load_model(MODEL_PATH)
69
+ logger.info('Model loaded successfully')
70
+ INPUT_SIZE = 224
71
+ MIN_FACE_SIZE = 90 # same as 02-prepare_fake_real_dataset.py
72
+
73
+ # Initialize MTCNN face detector (same as training pipeline 01-crop_faces_with_mtcnn.py)
74
+ logger.info('Initializing MTCNN face detector')
75
+ mtcnn_detector = MTCNN()
76
+ logger.info('MTCNN face detector ready')
77
+
78
+ # Initialize YOLO face detector (for processed video overlay only)
79
+ FACE_MODEL_PATH = os.path.join(os.path.dirname(__file__), 'yolov8n-face.pt')
80
+
81
+
82
+ def build_preview_face_detector():
83
+ if not preview_face_detector_enabled():
84
+ logger.info('Preview face detector disabled by configuration')
85
+ return None
86
+
87
+ try:
88
+ from ultralytics import YOLO
89
+ except ImportError:
90
+ logger.warning('ultralytics is unavailable; processed preview will fall back to the uploaded video')
91
+ return None
92
+
93
+ logger.info('Initializing YOLO face detector')
94
+ detector = YOLO(FACE_MODEL_PATH)
95
+ logger.info('YOLO face detector ready')
96
+ return detector
97
+
98
+
99
+ face_detector = build_preview_face_detector()
100
+
101
+ # In-memory job store: job_id -> {status, result, ...}
102
+ jobs = {}
103
+
104
+
105
+ def allowed_file(filename):
106
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
107
+
108
+
109
+ def face_to_base64(face_rgb):
110
+ face_bgr = cv2.cvtColor(face_rgb, cv2.COLOR_RGB2BGR)
111
+ _, buffer = cv2.imencode('.png', face_bgr)
112
+ return base64.b64encode(buffer).decode('utf-8')
113
+
114
+
115
+ def reencode_to_h264(input_path, output_path=None):
116
+ """Re-encode a video to H.264 for browser compatibility. Overwrites in-place if no output_path."""
117
+ ffmpeg_exe = imageio_ffmpeg.get_ffmpeg_exe()
118
+ if output_path is None:
119
+ output_path = input_path
120
+ tmp = input_path + '.reencode.mp4'
121
+ cmd = [
122
+ ffmpeg_exe, '-y', '-i', input_path,
123
+ '-c:v', 'libx264', '-preset', 'fast',
124
+ '-movflags', '+faststart', '-pix_fmt', 'yuv420p',
125
+ tmp
126
+ ]
127
+ result = subprocess.run(cmd, capture_output=True, text=True)
128
+ if result.returncode != 0:
129
+ logger.error('ffmpeg reencode failed: %s', result.stderr)
130
+ try:
131
+ os.remove(tmp)
132
+ except OSError:
133
+ pass
134
+ return False
135
+ try:
136
+ os.replace(tmp, output_path)
137
+ except OSError:
138
+ os.remove(input_path)
139
+ os.rename(tmp, output_path)
140
+ return True
141
+
142
+
143
+ def scale_frame(frame):
144
+ """Scale frame exactly like 00-convert_video_to_image.py"""
145
+ h, w = frame.shape[:2]
146
+ if w < 300:
147
+ scale_ratio = 2
148
+ elif w > 1900:
149
+ scale_ratio = 0.33
150
+ elif w > 1000:
151
+ scale_ratio = 0.5
152
+ else:
153
+ scale_ratio = 1
154
+ if scale_ratio != 1:
155
+ new_w = int(w * scale_ratio)
156
+ new_h = int(h * scale_ratio)
157
+ frame = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_AREA)
158
+ return frame
159
+
160
+
161
+ def extract_faces_from_video(video_path):
162
+ """Extract faces using MTCNN — matching training pipeline (01-crop_faces_with_mtcnn.py)."""
163
+ logger.info('Extracting faces from video: %s', video_path)
164
+ faces = []
165
+ cap = cv2.VideoCapture(video_path)
166
+ frame_rate = cap.get(cv2.CAP_PROP_FPS)
167
+ if frame_rate == 0:
168
+ logger.warning('Could not read frame rate from video')
169
+ cap.release()
170
+ return faces
171
+
172
+ while cap.isOpened():
173
+ frame_id = cap.get(cv2.CAP_PROP_POS_FRAMES)
174
+ ret, frame = cap.read()
175
+ if not ret:
176
+ break
177
+ if frame_id % math.floor(frame_rate) == 0:
178
+ # Step 1: Scale frame (same as 00-convert_video_to_image.py)
179
+ frame = scale_frame(frame)
180
+ image_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
181
+ h, w = image_rgb.shape[:2]
182
+
183
+ # Step 2: MTCNN face detection (same as 01-crop_faces_with_mtcnn.py)
184
+ results = mtcnn_detector.detect_faces(image_rgb)
185
+ num_faces = len(results)
186
+
187
+ for result in results:
188
+ bounding_box = result['box']
189
+ confidence = result['confidence']
190
+ # Same logic as training: if single face keep it, if multiple only keep > 0.95
191
+ if num_faces < 2 or confidence > 0.95:
192
+ bx, by, bw, bh = bounding_box
193
+ margin_x = bw * 0.3
194
+ margin_y = bh * 0.3
195
+ x1 = int(max(0, bx - margin_x))
196
+ x2 = int(min(w, bx + bw + margin_x))
197
+ y1 = int(max(0, by - margin_y))
198
+ y2 = int(min(h, by + bh + margin_y))
199
+ crop = image_rgb[y1:y2, x1:x2]
200
+ # Step 3: Filter small faces (same as 02-prepare_fake_real_dataset.py MIN_IMAGE_SIZE=90)
201
+ if crop.shape[0] < MIN_FACE_SIZE or crop.shape[1] < MIN_FACE_SIZE:
202
+ continue
203
+ if crop.size > 0:
204
+ crop_resized = cv2.resize(crop, (INPUT_SIZE, INPUT_SIZE))
205
+ faces.append(crop_resized)
206
+
207
+ cap.release()
208
+ logger.info('Face extraction complete — %d faces found', len(faces))
209
+ return faces
210
+
211
+
212
+ def create_processed_video(video_path, output_path, face_scores=None):
213
+ """Create video with face bounding boxes using ffmpeg drawbox (much faster than OpenCV)."""
214
+ logger.info('Creating processed video with bounding boxes: %s', output_path)
215
+
216
+ if face_detector is None:
217
+ logger.info('Preview face detector unavailable; re-encoding original video without overlays')
218
+ reencode_to_h264(video_path, output_path)
219
+ return
220
+
221
+ cap = cv2.VideoCapture(video_path)
222
+ fps = cap.get(cv2.CAP_PROP_FPS) or 30
223
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
224
+ duration = total_frames / fps if fps > 0 else 0
225
+
226
+ # Sample a few frames spread across the video to detect faces
227
+ sample_count = min(5, max(1, int(duration))) # ~1 sample per second, max 5
228
+ sample_positions = [int(i * total_frames / sample_count) for i in range(sample_count)]
229
+
230
+ # Collect all face boxes across sampled frames
231
+ all_boxes = []
232
+ for pos in sample_positions:
233
+ cap.set(cv2.CAP_PROP_POS_FRAMES, pos)
234
+ ret, frame = cap.read()
235
+ if not ret:
236
+ continue
237
+ results = face_detector(frame, verbose=False)[0]
238
+ for box in results.boxes:
239
+ if box.conf[0] > 0.5:
240
+ bx1, by1, bx2, by2 = map(int, box.xyxy[0])
241
+ all_boxes.append((max(0, bx1), max(0, by1), bx2, by2))
242
+
243
+ cap.release()
244
+
245
+ # Build ffmpeg drawbox filter from detected boxes
246
+ ffmpeg_exe = imageio_ffmpeg.get_ffmpeg_exe()
247
+ if all_boxes:
248
+ # Use the most common box region (largest by area) for a stable overlay
249
+ # Deduplicate similar boxes by averaging nearby ones
250
+ unique_boxes = []
251
+ for box in all_boxes:
252
+ merged = False
253
+ for i, ub in enumerate(unique_boxes):
254
+ # If boxes overlap significantly, merge them
255
+ if (abs(box[0] - ub[0]) < 40 and abs(box[1] - ub[1]) < 40 and
256
+ abs(box[2] - ub[2]) < 40 and abs(box[3] - ub[3]) < 40):
257
+ unique_boxes[i] = (
258
+ (ub[0] + box[0]) // 2, (ub[1] + box[1]) // 2,
259
+ (ub[2] + box[2]) // 2, (ub[3] + box[3]) // 2
260
+ )
261
+ merged = True
262
+ break
263
+ if not merged:
264
+ unique_boxes.append(box)
265
+
266
+ drawbox_filters = []
267
+ for (x1, y1, x2, y2) in unique_boxes:
268
+ w = x2 - x1
269
+ h = y2 - y1
270
+ drawbox_filters.append(f"drawbox=x={x1}:y={y1}:w={w}:h={h}:color=green:t=2")
271
+ filter_str = ','.join(drawbox_filters)
272
+ else:
273
+ filter_str = 'null'
274
+
275
+ cmd = [
276
+ ffmpeg_exe, '-y', '-i', video_path,
277
+ '-vf', filter_str,
278
+ '-c:v', 'libx264', '-preset', 'fast',
279
+ '-movflags', '+faststart', '-pix_fmt', 'yuv420p',
280
+ output_path
281
+ ]
282
+ logger.info('Running ffmpeg with %d face boxes', len(all_boxes))
283
+ result = subprocess.run(cmd, capture_output=True, text=True)
284
+ if result.returncode != 0:
285
+ logger.error('ffmpeg drawbox failed: %s', result.stderr[-500:])
286
+ else:
287
+ logger.info('Processed video saved: %s', output_path)
288
+
289
+
290
+ def predict_deepfake(faces):
291
+ if not faces:
292
+ logger.warning('No faces to predict on')
293
+ return None, 0, []
294
+
295
+ logger.info('Running prediction on %d face(s)', len(faces))
296
+
297
+ face_array = preprocess_input(np.array(faces, dtype='float32'))
298
+ predictions = model.predict(face_array, verbose=0)
299
+ flat_preds = predictions.flatten()
300
+ # Use top-K mean: average the top 30% of predictions (at least 3)
301
+ # Rationale: real videos have many high-confidence real frames; fake videos have NONE
302
+ sorted_desc = np.sort(flat_preds)[::-1] # highest first
303
+ k = max(3, int(len(sorted_desc) * 0.3))
304
+ top_k = sorted_desc[:k]
305
+ avg_prediction = float(np.mean(top_k))
306
+ # Write diagnostics to file
307
+ diag_path = os.path.join(os.path.dirname(__file__), 'diag_log.txt')
308
+ with open(diag_path, 'a') as f:
309
+ f.write(f'Raw predictions: min={float(np.min(predictions)):.4f}, max={float(np.max(predictions)):.4f}, top{k}_mean={avg_prediction:.4f}, mean={float(np.mean(predictions)):.4f}\n')
310
+ f.write(f'All scores (sorted desc): {sorted_desc.tolist()}\n')
311
+ f.write(f'Top-{k} used: {top_k.tolist()}\n')
312
+ f.write(f'Num faces: {len(faces)}\n\n')
313
+ logger.info('Raw predictions: min=%.4f, max=%.4f, top%d_mean=%.4f, mean=%.4f, n=%d',
314
+ float(np.min(predictions)), float(np.max(predictions)),
315
+ k, avg_prediction, float(np.mean(flat_preds)), len(flat_preds))
316
+
317
+ # Build per-face details (up to 5 faces sorted by relevance)
318
+ is_real = avg_prediction > 0.5
319
+ # Sort face indices by score: highest first for REAL, lowest first for FAKE
320
+ sorted_indices = np.argsort(flat_preds)[::-1] if is_real else np.argsort(flat_preds)
321
+ indices = sorted_indices[:5].tolist()
322
+
323
+ faces_detail = []
324
+ for i in indices:
325
+ faces_detail.append({
326
+ 'thumbnail': face_to_base64(faces[i]),
327
+ 'score': float(predictions[i][0])
328
+ })
329
+
330
+ logger.info('Prediction complete — avg score: %.4f, faces: %d', avg_prediction, len(faces))
331
+ return avg_prediction, len(faces), faces_detail
332
+
333
+
334
+ def cleanup_old_uploads(exclude=None):
335
+ """Delete all files in the upload folder except those in exclude."""
336
+ exclude = set(exclude or [])
337
+ folder = app.config['UPLOAD_FOLDER']
338
+ for f in os.listdir(folder):
339
+ fpath = os.path.join(folder, f)
340
+ if os.path.isfile(fpath) and fpath not in exclude:
341
+ try:
342
+ os.remove(fpath)
343
+ except PermissionError:
344
+ pass
345
+
346
+
347
+ from route import routes
348
+ app.register_blueprint(routes)
349
+
350
+
351
+ if __name__ == '__main__':
352
+ port = resolve_server_port()
353
+ logger.info('Starting Flask server on http://0.0.0.0:%s', port)
354
+ app.run(debug=False, host='0.0.0.0', port=port)
App/config.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+
4
+ DEFAULT_APP_PORT = 5001
5
+
6
+
7
+ def resolve_server_port(default=DEFAULT_APP_PORT):
8
+ raw_value = os.getenv('PORT')
9
+ if raw_value is None:
10
+ return default
11
+
12
+ try:
13
+ port = int(raw_value)
14
+ except ValueError:
15
+ return default
16
+
17
+ if 1 <= port <= 65535:
18
+ return port
19
+ return default
20
+
21
+
22
+ def preview_face_detector_enabled():
23
+ raw_value = os.getenv('ENABLE_PREVIEW_FACE_DETECTOR', '1').strip().lower()
24
+ return raw_value not in {'0', 'false', 'no', 'off'}
App/route.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import uuid
4
+ import threading
5
+ from flask import Blueprint, request, render_template, send_from_directory, jsonify
6
+ from werkzeug.utils import secure_filename
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ routes = Blueprint('routes', __name__)
11
+
12
+
13
+ def _get_app_deps():
14
+ """Import app-level objects to avoid circular imports."""
15
+ from app import (
16
+ app, jobs, allowed_file, cleanup_old_uploads,
17
+ extract_faces_from_video, predict_deepfake,
18
+ create_processed_video, reencode_to_h264
19
+ )
20
+ return app, jobs, allowed_file, cleanup_old_uploads, \
21
+ extract_faces_from_video, predict_deepfake, \
22
+ create_processed_video, reencode_to_h264
23
+
24
+
25
+ @routes.route('/', methods=['GET'])
26
+ def index():
27
+ return render_template('index.html')
28
+
29
+
30
+ @routes.route('/uploads/<filename>')
31
+ def uploaded_video(filename):
32
+ from app import app
33
+ return send_from_directory(app.config['UPLOAD_FOLDER'], filename, mimetype='video/mp4')
34
+
35
+
36
+ def process_video_job(job_id, filepath, unique_name):
37
+ """Background worker: extract faces, predict, create processed video."""
38
+ app, jobs, _, _, extract_faces_from_video, predict_deepfake, \
39
+ create_processed_video, _ = _get_app_deps()
40
+ try:
41
+ logger.info('[Job %s] Starting face detection', job_id)
42
+ jobs[job_id]['status'] = 'detecting'
43
+
44
+ faces = extract_faces_from_video(filepath)
45
+ avg_score, num_faces, faces_detail = predict_deepfake(faces)
46
+
47
+ if avg_score is None:
48
+ logger.warning('[Job %s] No faces detected', job_id)
49
+ jobs[job_id].update({
50
+ 'status': 'done',
51
+ 'error': 'No faces detected in the video.',
52
+ 'video_url': f'/uploads/{unique_name}',
53
+ })
54
+ return
55
+
56
+ is_real = avg_score > 0.5
57
+ label = 'REAL' if is_real else 'FAKE'
58
+ confidence = avg_score if is_real else (1 - avg_score)
59
+
60
+ logger.info('[Job %s] Detection done — result: %s, confidence: %.2f%%, faces: %d',
61
+ job_id, label, confidence * 100, num_faces)
62
+ jobs[job_id].update({
63
+ 'status': 'processing_video',
64
+ 'result': label,
65
+ 'confidence': round(confidence * 100, 2),
66
+ 'score': round(avg_score, 4),
67
+ 'num_faces': num_faces,
68
+ 'faces_detail': faces_detail,
69
+ 'video_url': f'/uploads/{unique_name}',
70
+ })
71
+
72
+ logger.info('[Job %s] Starting video processing', job_id)
73
+ processed_name = f"processed_{unique_name}"
74
+ processed_path = os.path.join(app.config['UPLOAD_FOLDER'], processed_name)
75
+ create_processed_video(filepath, processed_path)
76
+
77
+ logger.info('[Job %s] Video processing done', job_id)
78
+ jobs[job_id].update({
79
+ 'status': 'done',
80
+ 'processed_url': f'/uploads/{processed_name}',
81
+ })
82
+ except Exception as e:
83
+ logger.error('[Job %s] Error: %s', job_id, e)
84
+ jobs[job_id].update({'status': 'done', 'error': str(e)})
85
+
86
+
87
+ @routes.route('/predict', methods=['POST'])
88
+ def predict():
89
+ app, jobs, allowed_file, cleanup_old_uploads, _, _, _, reencode_to_h264 = _get_app_deps()
90
+
91
+ if 'video' not in request.files:
92
+ return jsonify({'error': 'No video file uploaded.'}), 400
93
+
94
+ file = request.files['video']
95
+ if file.filename == '':
96
+ return jsonify({'error': 'No file selected.'}), 400
97
+
98
+ if not allowed_file(file.filename):
99
+ return jsonify({'error': 'Invalid file type. Allowed: mp4, avi, mov, mkv, wmv'}), 400
100
+
101
+ cleanup_old_uploads()
102
+
103
+ ext = secure_filename(file.filename).rsplit('.', 1)[1].lower()
104
+ unique_name = f"{uuid.uuid4().hex}.{ext}"
105
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_name)
106
+ file.save(filepath)
107
+ logger.info('Video uploaded: %s (%s)', file.filename, unique_name)
108
+
109
+ logger.info('Re-encoding uploaded video to H.264')
110
+ reencode_to_h264(filepath)
111
+
112
+ job_id = uuid.uuid4().hex
113
+ logger.info('Created job %s for %s', job_id, unique_name)
114
+ jobs[job_id] = {'status': 'uploading', 'video_url': f'/uploads/{unique_name}'}
115
+
116
+ thread = threading.Thread(target=process_video_job, args=(job_id, filepath, unique_name))
117
+ thread.start()
118
+
119
+ return jsonify({'job_id': job_id, 'video_url': f'/uploads/{unique_name}'})
120
+
121
+
122
+ @routes.route('/status/<job_id>')
123
+ def job_status(job_id):
124
+ from app import jobs
125
+ job = jobs.get(job_id)
126
+ if not job:
127
+ return jsonify({'error': 'Job not found'}), 404
128
+ return jsonify(job)
App/static/Product.jsx ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ── Upload Area ── */
2
+ function UploadArea({ file, onFileChange }) {
3
+ const inputRef = React.useRef();
4
+ return (
5
+ <div
6
+ className={`upload-area${file ? ' has-file' : ''}`}
7
+ onClick={() => inputRef.current.click()}
8
+ >
9
+ <div className="upload-text">
10
+ Drop a video here or click to upload &mdash; MP4, AVI, MOV, max 200 MB
11
+ </div>
12
+ {file && <div className="file-name">{file.name}</div>}
13
+ <input
14
+ ref={inputRef}
15
+ type="file"
16
+ accept=".mp4,.avi,.mov,.mkv,.wmv"
17
+ onChange={e => { onFileChange(e.target.files[0] || null); e.target.value = ''; }}
18
+ />
19
+ </div>
20
+ );
21
+ }
22
+
23
+ /* ── Status Spinner ── */
24
+ function StatusIndicator({ status }) {
25
+ if (!status || status === 'done') return null;
26
+ return (
27
+ <>
28
+ <div className="spinner" />
29
+ <p className="processing-text">{STATUS_MESSAGES[status] || 'Processing\u2026'}</p>
30
+ </>
31
+ );
32
+ }
33
+
34
+ /* ── Video Comparison ── */
35
+ const VideoComparison = React.memo(function VideoComparison({ file, processedUrl, isProcessing }) {
36
+ const videoRef = React.useRef(null);
37
+ const [localUrl, setLocalUrl] = React.useState(null);
38
+
39
+ React.useEffect(() => {
40
+ if (file) {
41
+ const url = URL.createObjectURL(file);
42
+ setLocalUrl(url);
43
+ return () => URL.revokeObjectURL(url);
44
+ }
45
+ setLocalUrl(null);
46
+ }, [file]);
47
+
48
+ if (!localUrl) return null;
49
+ const showProcessed = processedUrl || isProcessing;
50
+ return (
51
+ <section className="video-compare">
52
+ <h2>{showProcessed ? 'Face Detection' : 'Uploaded Video'}</h2>
53
+ <div className={`compare-grid${showProcessed ? '' : ' single'}`}>
54
+ <div className="compare-item">
55
+ <video ref={videoRef} controls src={localUrl} />
56
+ <div className="compare-label original">Original</div>
57
+ </div>
58
+ {showProcessed && (
59
+ <div className="compare-item">
60
+ {processedUrl
61
+ ? <video controls src={processedUrl} />
62
+ : <div className="preview-placeholder"><div className="spinner" /></div>
63
+ }
64
+ <div className="compare-label detected">
65
+ {processedUrl ? 'Detected Faces' : 'Generating\u2026'}
66
+ </div>
67
+ </div>
68
+ )}
69
+ </div>
70
+ </section>
71
+ );
72
+ });
73
+
74
+ /* ── Face Row ── */
75
+ function FaceRow({ face, index }) {
76
+ const pct = (face.score * 100).toFixed(2);
77
+ const w = (face.score * 100).toFixed(1);
78
+ return (
79
+ <div className="face-row">
80
+ <img className="face-thumb" src={`data:image/png;base64,${face.thumbnail}`} alt={`Face ${index + 1}`} />
81
+ <div className="face-info">
82
+ <div className="face-bar-track">
83
+ <div className={`face-bar-fill ${barClass(face.score)}`} style={{ width: `${w}%` }} />
84
+ </div>
85
+ <div className={`face-score ${scoreClass(face.score)}`}>{pct}% authentic</div>
86
+ </div>
87
+ </div>
88
+ );
89
+ }
90
+
91
+ /* ── Results Panel ── */
92
+ function ResultsPanel({ data }) {
93
+ if (!data || !data.result) return null;
94
+ const cls = data.result.toLowerCase();
95
+ return (
96
+ <section className="results-section">
97
+ <div className="results-panel">
98
+ <h2>Results</h2>
99
+ <p className="results-hint">
100
+ Authenticity score: likelihood the face is real.{' '}
101
+ <span className="score-red">Red &lt;20%</span>,{' '}
102
+ <span className="score-orange">Orange 20-80%</span>,{' '}
103
+ <span className="score-green">Green &gt;80%</span>.
104
+ </p>
105
+ <div className={`overall-result ${cls}`}>
106
+ <div className="overall-label">{data.result}</div>
107
+ <div className="overall-details">
108
+ Confidence: {data.confidence}%<br />
109
+ Model Score: {data.score}<br />
110
+ Faces Analyzed: {data.num_faces}
111
+ </div>
112
+ </div>
113
+ {data.faces_detail && data.faces_detail.map((face, i) => (
114
+ <FaceRow key={i} face={face} index={i} />
115
+ ))}
116
+ </div>
117
+ </section>
118
+ );
119
+ }
120
+
121
+ /* ── Product Page ── */
122
+ function ProductPage({ file, setFile, status, setStatus, error, setError, result, setResult, submitting, setSubmitting }) {
123
+ const timerRef = React.useRef(null);
124
+
125
+ const reset = () => { setResult(null); setError(null); setStatus(null); };
126
+ const handleFileChange = (f) => { setFile(f); reset(); };
127
+
128
+ const pollJob = React.useCallback((jobId) => {
129
+ timerRef.current = setTimeout(async () => {
130
+ try {
131
+ const res = await fetch(`/status/${jobId}`);
132
+ const data = await res.json();
133
+ setStatus(data.status);
134
+
135
+ if (data.result) {
136
+ setResult(prev => ({ ...prev, ...data }));
137
+ }
138
+
139
+ if (data.status === 'done') {
140
+ setSubmitting(false);
141
+ if (data.error && !data.result) setError(data.error);
142
+ } else {
143
+ pollJob(jobId);
144
+ }
145
+ } catch {
146
+ setSubmitting(false);
147
+ setStatus(null);
148
+ setError('Connection lost. Please try again.');
149
+ }
150
+ }, 1000);
151
+ }, []);
152
+
153
+ React.useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current); }, []);
154
+
155
+ const handleSubmit = async (e) => {
156
+ e.preventDefault();
157
+ if (!file) return;
158
+ reset();
159
+ setSubmitting(true);
160
+ setStatus('uploading');
161
+
162
+ const fd = new FormData();
163
+ fd.append('video', file);
164
+
165
+ try {
166
+ const res = await fetch('/predict', { method: 'POST', body: fd });
167
+ const data = await res.json();
168
+ if (data.error) {
169
+ setError(data.error);
170
+ setSubmitting(false);
171
+ setStatus(null);
172
+ } else {
173
+ pollJob(data.job_id);
174
+ }
175
+ } catch {
176
+ setError('Upload failed. Please try again.');
177
+ setSubmitting(false);
178
+ setStatus(null);
179
+ }
180
+ };
181
+
182
+ return (
183
+ <>
184
+ <section className="hero">
185
+ <div className="hero-left">
186
+ <h1 className="hero-title">AI-Deepfake Video Detection</h1>
187
+ <p className="hero-desc">
188
+ Free deepfake detection tool for videos. Upload a video and get
189
+ per-face authenticity scores in seconds. AI-powered synthetic face detection.
190
+ </p>
191
+
192
+ <form onSubmit={handleSubmit}>
193
+ <UploadArea file={file} onFileChange={handleFileChange} />
194
+ <button type="submit" className="btn" disabled={!file || submitting}>
195
+ {submitting ? (STATUS_MESSAGES[status] || 'Processing\u2026') : 'Analyze Video'}
196
+ </button>
197
+ </form>
198
+
199
+ <StatusIndicator status={submitting ? status : null} />
200
+ {error && <div className="error-box">{error}</div>}
201
+ </div>
202
+
203
+ <div className="hero-right">
204
+ <img src="/static/images.png" alt="AI Deepfake Detection" className="hero-image" />
205
+ </div>
206
+ </section>
207
+
208
+ <VideoComparison
209
+ file={file}
210
+ processedUrl={result?.processed_url}
211
+ isProcessing={submitting}
212
+ />
213
+ <ResultsPanel data={result} />
214
+ </>
215
+ );
216
+ }
App/static/Technology.jsx ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ── Step Diagram SVG ── */
2
+ function StepDiagram({ number, title, color }) {
3
+ return (
4
+ <svg width="80" height="80" viewBox="0 0 80 80" className="step-icon">
5
+ <circle cx="40" cy="40" r="36" fill="none" stroke={color} strokeWidth="2.5" />
6
+ <text x="40" y="46" textAnchor="middle" fill={color} fontSize="28" fontWeight="700">{number}</text>
7
+ </svg>
8
+ );
9
+ }
10
+
11
+ /* ── Pipeline Flow Arrow ── */
12
+ function FlowArrow() {
13
+ return (
14
+ <div className="flow-arrow">
15
+ <svg width="40" height="40" viewBox="0 0 40 40">
16
+ <path d="M10 20 L28 20 M22 13 L30 20 L22 27" fill="none" stroke="#6c8cff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
17
+ </svg>
18
+ </div>
19
+ );
20
+ }
21
+
22
+ /* ── Technology Page ── */
23
+ function TechnologyPage() {
24
+ const steps = [
25
+ {
26
+ number: 1,
27
+ title: 'Video to Frames',
28
+ color: '#6c8cff',
29
+ description: 'Raw training videos are split into individual image frames. One frame is extracted per second of video using OpenCV. Each frame is automatically scaled based on its resolution to normalize image sizes across the dataset.',
30
+ details: [
31
+ 'Reads MP4 videos from the FaceForensics++ dataset',
32
+ 'Extracts 1 frame per second (at the video\'s native frame rate)',
33
+ 'Auto-scales: 2\u00d7 for small frames (<300px), 0.5\u00d7 for HD, 0.33\u00d7 for Full HD+',
34
+ 'Saves frames as individual JPG images organized by video',
35
+ ],
36
+ diagram: (
37
+ <svg viewBox="0 0 400 120" className="step-illustration">
38
+ <rect x="10" y="15" width="90" height="90" rx="8" fill="#1a1a2e" stroke="#6c8cff" strokeWidth="1.5"/>
39
+ <polygon points="40,35 40,80 70,57" fill="#6c8cff" opacity="0.8"/>
40
+ <text x="55" y="112" textAnchor="middle" fill="#888" fontSize="11">Video</text>
41
+ <path d="M115 60 L155 60" stroke="#6c8cff" strokeWidth="2" markerEnd="url(#arrow1)"/>
42
+ <defs><marker id="arrow1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#6c8cff"/></marker></defs>
43
+ {[0,1,2,3,4].map(i => (
44
+ <g key={i}>
45
+ <rect x={165 + i*46} y={20 + (i%2)*15} width="38" height="38" rx="4" fill="#1a1a2e" stroke="#4caf50" strokeWidth="1"/>
46
+ <text x={184 + i*46} y={44 + (i%2)*15} textAnchor="middle" fill="#4caf50" fontSize="9">F{i+1}</text>
47
+ </g>
48
+ ))}
49
+ <text x="300" y="112" textAnchor="middle" fill="#888" fontSize="11">Extracted Frames (1/sec)</text>
50
+ </svg>
51
+ ),
52
+ },
53
+ {
54
+ number: 2,
55
+ title: 'Face Detection & Cropping',
56
+ color: '#ff9800',
57
+ description: 'MTCNN (Multi-task Cascaded Convolutional Network) scans each extracted frame to detect faces. Detected faces are cropped with a 30% margin around the bounding box to preserve context like hair and jawline, which helps the model detect manipulation artifacts.',
58
+ details: [
59
+ 'Uses MTCNN deep learning face detector for accurate face localization',
60
+ 'Filters low-confidence detections (>95% threshold for multi-face frames)',
61
+ 'Adds 30% margin around each face bounding box',
62
+ 'Crops and saves individual face images for training',
63
+ ],
64
+ diagram: (
65
+ <svg viewBox="0 0 400 120" className="step-illustration">
66
+ <rect x="10" y="10" width="100" height="100" rx="6" fill="#1a1a2e" stroke="#444" strokeWidth="1"/>
67
+ <circle cx="60" cy="45" r="15" fill="none" stroke="#ff9800" strokeWidth="1.5" strokeDasharray="3,2"/>
68
+ <circle cx="60" cy="42" r="6" fill="#ff9800" opacity="0.4"/>
69
+ <ellipse cx="60" cy="55" rx="10" ry="6" fill="#ff9800" opacity="0.3"/>
70
+ <rect x="35" y="25" width="50" height="50" rx="4" fill="none" stroke="#ff9800" strokeWidth="2"/>
71
+ <text x="60" y="112" textAnchor="middle" fill="#888" fontSize="11">Frame + Detection</text>
72
+ <path d="M125 55 L165 55" stroke="#ff9800" strokeWidth="2" markerEnd="url(#arrow2)"/>
73
+ <defs><marker id="arrow2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#ff9800"/></marker></defs>
74
+ <rect x="175" y="20" width="70" height="70" rx="8" fill="#1a1a2e" stroke="#ff9800" strokeWidth="2"/>
75
+ <circle cx="210" cy="45" r="12" fill="none" stroke="#ff9800" strokeWidth="1.5"/>
76
+ <circle cx="210" cy="42" r="5" fill="#ff9800" opacity="0.5"/>
77
+ <ellipse cx="210" cy="52" rx="8" ry="5" fill="#ff9800" opacity="0.4"/>
78
+ <text x="210" y="112" textAnchor="middle" fill="#888" fontSize="11">Cropped Face (+30% margin)</text>
79
+ <text x="340" y="40" fill="#ff9800" fontSize="11" fontWeight="600">{'\u2713'} 95%+ confidence</text>
80
+ <text x="340" y="58" fill="#888" fontSize="10">30% margin padding</text>
81
+ <text x="340" y="76" fill="#888" fontSize="10">Context preserved</text>
82
+ </svg>
83
+ ),
84
+ },
85
+ {
86
+ number: 3,
87
+ title: 'Dataset Preparation',
88
+ color: '#4caf50',
89
+ description: 'Cropped face images are organized into "real" and "fake" categories based on FaceForensics++ metadata. Small or corrupted images (<90px) are filtered out. The dataset is then split into training (80%), validation (10%), and test (10%) sets using stratified splitting.',
90
+ details: [
91
+ 'Labels faces as REAL or FAKE using FaceForensics++ CSV metadata',
92
+ 'Filters out low-quality images smaller than 90\u00d790 pixels',
93
+ 'Balances fake samples across manipulation methods (Deepfakes, Face2Face, FaceSwap, NeuralTextures, FaceShifter)',
94
+ 'Splits into train/val/test (80/10/10) with stratified random sampling',
95
+ ],
96
+ diagram: (
97
+ <svg viewBox="0 0 400 120" className="step-illustration">
98
+ <g>
99
+ <rect x="10" y="15" width="55" height="40" rx="4" fill="rgba(76,175,80,0.15)" stroke="#4caf50" strokeWidth="1.5"/>
100
+ <text x="37" y="39" textAnchor="middle" fill="#4caf50" fontSize="11" fontWeight="600">REAL</text>
101
+ <rect x="10" y="65" width="55" height="40" rx="4" fill="rgba(244,67,54,0.15)" stroke="#f44336" strokeWidth="1.5"/>
102
+ <text x="37" y="89" textAnchor="middle" fill="#f44336" fontSize="11" fontWeight="600">FAKE</text>
103
+ </g>
104
+ <path d="M80 55 L120 55" stroke="#4caf50" strokeWidth="2" markerEnd="url(#arrow3)"/>
105
+ <defs><marker id="arrow3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#4caf50"/></marker></defs>
106
+ <rect x="130" y="8" width="100" height="28" rx="4" fill="rgba(76,175,80,0.1)" stroke="#4caf50" strokeWidth="1"/>
107
+ <text x="180" y="26" textAnchor="middle" fill="#4caf50" fontSize="10">Train 80%</text>
108
+ <rect x="130" y="44" width="100" height="28" rx="4" fill="rgba(255,152,0,0.1)" stroke="#ff9800" strokeWidth="1"/>
109
+ <text x="180" y="62" textAnchor="middle" fill="#ff9800" fontSize="10">Val 10%</text>
110
+ <rect x="130" y="80" width="100" height="28" rx="4" fill="rgba(108,140,255,0.1)" stroke="#6c8cff" strokeWidth="1"/>
111
+ <text x="180" y="98" textAnchor="middle" fill="#6c8cff" fontSize="10">Test 10%</text>
112
+ <text x="310" y="30" fill="#888" fontSize="10">{'\u2713'} Min 90\u00d790px filter</text>
113
+ <text x="310" y="50" fill="#888" fontSize="10">{'\u2713'} Stratified split</text>
114
+ <text x="310" y="70" fill="#888" fontSize="10">{'\u2713'} Multi-method balance</text>
115
+ <text x="310" y="90" fill="#888" fontSize="10">{'\u2713'} CSV metadata labels</text>
116
+ </svg>
117
+ ),
118
+ },
119
+ {
120
+ number: 4,
121
+ title: 'CNN Training (EfficientNetB0)',
122
+ color: '#f44336',
123
+ description: 'A two-phase transfer learning approach trains an EfficientNetB0-based classifier. Phase 1 freezes the pre-trained ImageNet backbone and trains only the classification head. Phase 2 unfreezes the entire network for fine-tuning with a very low learning rate, achieving ~92% accuracy.',
124
+ details: [
125
+ 'EfficientNetB0 backbone pre-trained on ImageNet (224\u00d7224 input)',
126
+ 'Phase 1: Frozen base, train head only (lr=1e-3, up to 15 epochs)',
127
+ 'Phase 2: Full fine-tuning (lr=1e-5, up to 30 epochs)',
128
+ 'Data augmentation: rotation, flip, zoom, shift, brightness',
129
+ 'Class weight balancing for imbalanced datasets',
130
+ 'Callbacks: EarlyStopping, ReduceLROnPlateau, ModelCheckpoint',
131
+ 'Output: binary sigmoid \u2014 score > 0.5 = REAL, \u2264 0.5 = FAKE',
132
+ ],
133
+ diagram: (
134
+ <svg viewBox="0 0 400 140" className="step-illustration">
135
+ <rect x="10" y="30" width="70" height="80" rx="6" fill="#1a1a2e" stroke="#f44336" strokeWidth="1.5"/>
136
+ <text x="45" y="55" textAnchor="middle" fill="#f44336" fontSize="9" fontWeight="600">224\u00d7224</text>
137
+ <text x="45" y="70" textAnchor="middle" fill="#888" fontSize="8">Face Input</text>
138
+ <text x="45" y="95" textAnchor="middle" fill="#666" fontSize="8">preprocess</text>
139
+ <text x="45" y="106" textAnchor="middle" fill="#666" fontSize="8">[-1, 1]</text>
140
+ <path d="M90 70 L115 70" stroke="#f44336" strokeWidth="1.5" markerEnd="url(#arrow4)"/>
141
+ <rect x="120" y="15" width="100" height="110" rx="6" fill="#1a1a2e" stroke="#ff9800" strokeWidth="1.5"/>
142
+ <text x="170" y="35" textAnchor="middle" fill="#ff9800" fontSize="10" fontWeight="600">EfficientNetB0</text>
143
+ <text x="170" y="52" textAnchor="middle" fill="#888" fontSize="8">(ImageNet weights)</text>
144
+ {[0,1,2,3].map(i => (
145
+ <rect key={i} x="135" y={60 + i*14} width="70" height="10" rx="2" fill={`rgba(255,152,0,${0.15 + i*0.1})`} stroke="#ff9800" strokeWidth="0.5"/>
146
+ ))}
147
+ <defs><marker id="arrow4" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#f44336"/></marker></defs>
148
+ <path d="M230 70 L255 70" stroke="#f44336" strokeWidth="1.5" markerEnd="url(#arrow4)"/>
149
+ <rect x="260" y="25" width="80" height="90" rx="6" fill="#1a1a2e" stroke="#6c8cff" strokeWidth="1.5"/>
150
+ <text x="300" y="45" textAnchor="middle" fill="#6c8cff" fontSize="9" fontWeight="600">Head</text>
151
+ <text x="300" y="62" textAnchor="middle" fill="#888" fontSize="7">GlobalAvgPool</text>
152
+ <text x="300" y="74" textAnchor="middle" fill="#888" fontSize="7">BatchNorm</text>
153
+ <text x="300" y="86" textAnchor="middle" fill="#888" fontSize="7">Dense(256)+Dropout</text>
154
+ <text x="300" y="98" textAnchor="middle" fill="#888" fontSize="7">Dense(1, sigmoid)</text>
155
+ <path d="M350 70 L375 70" stroke="#4caf50" strokeWidth="1.5" markerEnd="url(#arrow5)"/>
156
+ <defs><marker id="arrow5" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#4caf50"/></marker></defs>
157
+ <text x="388" y="65" fill="#4caf50" fontSize="11" fontWeight="700">0\u20131</text>
158
+ <text x="388" y="80" fill="#888" fontSize="8">Score</text>
159
+ </svg>
160
+ ),
161
+ },
162
+ ];
163
+
164
+ return (
165
+ <section className="tech-page">
166
+ <div className="tech-hero">
167
+ <h1 className="tech-title">How It Works</h1>
168
+ <p className="tech-subtitle">
169
+ Our deepfake detection pipeline processes videos through four stages — from raw video
170
+ to a trained AI model that scores each face for authenticity.
171
+ </p>
172
+ </div>
173
+
174
+ {/* Pipeline overview */}
175
+ <div className="pipeline-overview">
176
+ {steps.map((step, i) => (
177
+ <React.Fragment key={step.number}>
178
+ <div className="pipeline-step-mini">
179
+ <StepDiagram number={step.number} title={step.title} color={step.color} />
180
+ <span style={{ color: step.color, fontWeight: 600, fontSize: 13 }}>{step.title}</span>
181
+ </div>
182
+ {i < steps.length - 1 && <FlowArrow />}
183
+ </React.Fragment>
184
+ ))}
185
+ </div>
186
+
187
+ {/* Detailed steps */}
188
+ {steps.map((step) => (
189
+ <div className="tech-step" key={step.number}>
190
+ <div className="tech-step-header">
191
+ <StepDiagram number={step.number} title={step.title} color={step.color} />
192
+ <div>
193
+ <h2 className="tech-step-title" style={{ color: step.color }}>
194
+ Step {step.number}: {step.title}
195
+ </h2>
196
+ </div>
197
+ </div>
198
+ <p className="tech-step-desc">{step.description}</p>
199
+ <div className="tech-step-diagram">
200
+ {step.diagram}
201
+ </div>
202
+ <ul className="tech-step-details">
203
+ {step.details.map((d, i) => <li key={i}>{d}</li>)}
204
+ </ul>
205
+ </div>
206
+ ))}
207
+
208
+ {/* Inference section */}
209
+ <div className="tech-step">
210
+ <div className="tech-step-header">
211
+ <svg width="80" height="80" viewBox="0 0 80 80" className="step-icon">
212
+ <circle cx="40" cy="40" r="36" fill="none" stroke="#6c8cff" strokeWidth="2.5"/>
213
+ <text x="40" y="46" textAnchor="middle" fill="#6c8cff" fontSize="22" fontWeight="700">{'\u25b6'}</text>
214
+ </svg>
215
+ <div>
216
+ <h2 className="tech-step-title" style={{ color: '#6c8cff' }}>
217
+ Real-Time Inference
218
+ </h2>
219
+ </div>
220
+ </div>
221
+ <p className="tech-step-desc">
222
+ When you upload a video, the app uses YOLOv8 for fast face detection on each frame,
223
+ then feeds cropped faces through the trained EfficientNetB0 model. Each face gets an
224
+ authenticity score (0 = fake, 1 = real), and the processed video shows bounding boxes
225
+ with per-face REAL/FAKE labels overlaid in real time.
226
+ </p>
227
+ <div className="tech-step-diagram">
228
+ <svg viewBox="0 0 400 80" className="step-illustration">
229
+ <rect x="5" y="15" width="65" height="50" rx="6" fill="#1a1a2e" stroke="#6c8cff" strokeWidth="1.5"/>
230
+ <text x="37" y="44" textAnchor="middle" fill="#6c8cff" fontSize="9" fontWeight="600">Upload</text>
231
+ <path d="M80 40 L105 40" stroke="#6c8cff" strokeWidth="1.5" markerEnd="url(#arrowI)"/>
232
+ <rect x="110" y="15" width="65" height="50" rx="6" fill="#1a1a2e" stroke="#ff9800" strokeWidth="1.5"/>
233
+ <text x="142" y="38" textAnchor="middle" fill="#ff9800" fontSize="9" fontWeight="600">YOLOv8</text>
234
+ <text x="142" y="50" textAnchor="middle" fill="#888" fontSize="7">Face Detect</text>
235
+ <path d="M185 40 L210 40" stroke="#ff9800" strokeWidth="1.5" markerEnd="url(#arrowI)"/>
236
+ <rect x="215" y="15" width="65" height="50" rx="6" fill="#1a1a2e" stroke="#f44336" strokeWidth="1.5"/>
237
+ <text x="247" y="38" textAnchor="middle" fill="#f44336" fontSize="8" fontWeight="600">EfficientNet</text>
238
+ <text x="247" y="50" textAnchor="middle" fill="#888" fontSize="7">Predict</text>
239
+ <path d="M290 40 L315 40" stroke="#4caf50" strokeWidth="1.5" markerEnd="url(#arrowI)"/>
240
+ <rect x="320" y="15" width="70" height="50" rx="6" fill="#1a1a2e" stroke="#4caf50" strokeWidth="1.5"/>
241
+ <text x="355" y="38" textAnchor="middle" fill="#4caf50" fontSize="9" fontWeight="600">REAL</text>
242
+ <text x="355" y="50" textAnchor="middle" fill="#f44336" fontSize="9" fontWeight="600">FAKE</text>
243
+ <defs><marker id="arrowI" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#6c8cff"/></marker></defs>
244
+ </svg>
245
+ </div>
246
+ </div>
247
+ </section>
248
+ );
249
+ }
App/static/app.jsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { useState, useRef, useEffect, useCallback } = React;
2
+
3
+ const STATUS_MESSAGES = {
4
+ uploading: 'Uploading video\u2026',
5
+ detecting: 'Detecting faces & predicting deepfake\u2026',
6
+ processing_video: 'Generating face detection video\u2026',
7
+ };
8
+
9
+ function barClass(s) { return s > 0.8 ? 'bar-green' : s > 0.2 ? 'bar-orange' : 'bar-red'; }
10
+ function scoreClass(s) { return s > 0.8 ? 'score-green' : s > 0.2 ? 'score-orange' : 'score-red'; }
11
+
12
+ /* ── Simple hash-based router ── */
13
+ function useHashRoute() {
14
+ const [route, setRoute] = useState(window.location.hash || '#/');
15
+ useEffect(() => {
16
+ const onHash = () => setRoute(window.location.hash || '#/');
17
+ window.addEventListener('hashchange', onHash);
18
+ return () => window.removeEventListener('hashchange', onHash);
19
+ }, []);
20
+ return route;
21
+ }
22
+
23
+ /* ── Navbar ── */
24
+ function Navbar({ route }) {
25
+ return (
26
+ <header className="navbar">
27
+ <div className="logo"><span>AI</span>-Deepfake Video Detection</div>
28
+ <nav>
29
+ <a href="#/" className={route === '#/' || route === '' ? 'active' : ''}>Product</a>
30
+ <a href="#/technology" className={route === '#/technology' ? 'active' : ''}>Technology</a>
31
+ </nav>
32
+ </header>
33
+ );
34
+ }
35
+
36
+ /* ── Main App ── */
37
+ function App() {
38
+ const [file, setFile] = useState(null);
39
+ const [status, setStatus] = useState(null);
40
+ const [error, setError] = useState(null);
41
+ const [result, setResult] = useState(null);
42
+ const [submitting, setSubmitting] = useState(false);
43
+ const route = useHashRoute();
44
+
45
+ return (
46
+ <>
47
+ <Navbar route={route} />
48
+ {route === '#/technology' ? (
49
+ <TechnologyPage />
50
+ ) : (
51
+ <ProductPage
52
+ file={file} setFile={setFile}
53
+ status={status} setStatus={setStatus}
54
+ error={error} setError={setError}
55
+ result={result} setResult={setResult}
56
+ submitting={submitting} setSubmitting={setSubmitting}
57
+ />
58
+ )}
59
+ </>
60
+ );
61
+ }
62
+
63
+ ReactDOM.createRoot(document.getElementById('root')).render(<App />);
App/static/images.png ADDED

Git LFS Details

  • SHA256: 7274ea83d72c7de3177271bee46025db8c95cb9763420ada73a005dc62cf1e43
  • Pointer size: 131 Bytes
  • Size of remote file: 605 kB
App/static/style.css ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * { box-sizing: border-box; margin: 0; padding: 0; }
2
+ body {
3
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
4
+ background: #0b0b1a;
5
+ color: #d0d0d0;
6
+ min-height: 100vh;
7
+ }
8
+ .navbar {
9
+ display: flex; align-items: center; justify-content: space-between;
10
+ padding: 16px 40px; background: #0b0b1a; border-bottom: 1px solid #1a1a2e;
11
+ }
12
+ .navbar .logo { font-size: 22px; font-weight: 700; color: #fff; letter-spacing: 0.5px; }
13
+ .navbar .logo span { color: #6c8cff; }
14
+ .navbar nav a {
15
+ color: #888; text-decoration: none; margin-left: 32px; font-size: 14px; transition: color 0.2s;
16
+ }
17
+ .navbar nav a:hover, .navbar nav a.active { color: #6c8cff; }
18
+ .hero {
19
+ display: flex; align-items: flex-start; justify-content: space-between;
20
+ max-width: 1100px; margin: 60px auto 0; padding: 0 40px; gap: 60px;
21
+ }
22
+ .hero-left { flex: 1; max-width: 480px; }
23
+ .hero-title { font-size: 48px; font-weight: 800; color: #fff; line-height: 1.1; margin-bottom: 18px; }
24
+ .hero-desc { font-size: 15px; color: #999; line-height: 1.7; margin-bottom: 32px; }
25
+ .upload-area {
26
+ border: 2px dashed #2a2a40; border-radius: 12px; padding: 28px;
27
+ text-align: center; cursor: pointer; transition: border-color 0.3s; margin-bottom: 14px;
28
+ }
29
+ .upload-area:hover { border-color: #6c8cff; }
30
+ .upload-area.has-file { border-color: #4caf50; }
31
+ .upload-text { color: #666; font-size: 14px; }
32
+ .file-name { color: #6c8cff; font-weight: 600; margin-top: 8px; word-break: break-all; font-size: 14px; }
33
+ input[type="file"] { display: none; }
34
+ .btn {
35
+ display: block; width: 100%; padding: 13px; background: #6c8cff; color: #fff;
36
+ border: none; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer; transition: background 0.3s;
37
+ }
38
+ .btn:hover { background: #5a7ae6; }
39
+ .btn:disabled { background: #2a2a40; color: #555; cursor: not-allowed; }
40
+ .video-preview { flex: 1; display: flex; justify-content: center; align-items: center; }
41
+ .video-preview video {
42
+ max-width: 100%; max-height: 400px; border-radius: 12px; border: 1px solid #1e1e35; background: #111122;
43
+ }
44
+ .hero-right { flex: 1; display: flex; justify-content: center; align-items: stretch; }
45
+ .hero-image {
46
+ width: 100%; height: 100%; border-radius: 14px; border: 1px solid #1e1e35;
47
+ object-fit: cover; box-shadow: 0 8px 32px rgba(108,140,255,0.10);
48
+ }
49
+ .preview-placeholder {
50
+ width: 100%; max-width: 460px; height: 280px; border-radius: 12px; background: #111122;
51
+ border: 1px solid #1e1e35; display: flex; align-items: center; justify-content: center; color: #333; font-size: 48px;
52
+ }
53
+ .spinner {
54
+ margin: 16px auto; width: 36px; height: 36px; border: 3px solid #1a1a30;
55
+ border-top: 3px solid #6c8cff; border-radius: 50%; animation: spin 0.7s linear infinite;
56
+ }
57
+ .processing-text { text-align: center; color: #666; font-size: 13px; margin-top: 8px; }
58
+ @keyframes spin { to { transform: rotate(360deg); } }
59
+ .error-box {
60
+ margin-top: 20px; padding: 16px; background: rgba(244,67,54,0.1); border: 1px solid rgba(244,67,54,0.3);
61
+ border-radius: 10px; color: #f44336; text-align: center; font-size: 14px;
62
+ }
63
+ .video-compare { max-width: 1100px; margin: 40px auto 0; padding: 0 40px; }
64
+ .video-compare h2 { font-size: 20px; font-weight: 700; color: #fff; margin-bottom: 16px; }
65
+ .compare-grid { display: flex; gap: 24px; }
66
+ .compare-grid.single { justify-content: center; }
67
+ .compare-grid.single .compare-item { max-width: 640px; }
68
+ .compare-item { flex: 1; text-align: center; }
69
+ .compare-item video {
70
+ width: 100%; max-height: 360px; border-radius: 12px; border: 1px solid #1e1e35; background: #111122;
71
+ }
72
+ .compare-label { margin-top: 8px; font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; }
73
+ .compare-label.original { color: #6c8cff; }
74
+ .compare-label.detected { color: #4caf50; }
75
+ .results-section { max-width: 1100px; margin: 50px auto 60px; padding: 0 40px; }
76
+ .results-panel { background: #111122; border: 1px solid #1e1e35; border-radius: 14px; padding: 30px 36px; }
77
+ .results-panel h2 { font-size: 20px; font-weight: 700; color: #fff; margin-bottom: 6px; }
78
+ .results-hint { font-size: 13px; color: #777; margin-bottom: 24px; line-height: 1.5; }
79
+ .overall-result { display: flex; align-items: center; gap: 20px; padding: 20px; border-radius: 12px; margin-bottom: 24px; }
80
+ .overall-result.real { background: rgba(76,175,80,0.08); border: 1px solid rgba(76,175,80,0.3); }
81
+ .overall-result.fake { background: rgba(244,67,54,0.08); border: 1px solid rgba(244,67,54,0.3); }
82
+ .overall-label { font-size: 32px; font-weight: 800; }
83
+ .overall-result.real .overall-label { color: #4caf50; }
84
+ .overall-result.fake .overall-label { color: #f44336; }
85
+ .overall-details { font-size: 14px; color: #aaa; line-height: 1.6; }
86
+ .face-row { display: flex; align-items: center; gap: 16px; padding: 14px 0; border-top: 1px solid #1a1a30; }
87
+ .face-thumb { width: 52px; height: 52px; border-radius: 8px; object-fit: cover; border: 2px solid #222; background: #1a1a2e; }
88
+ .face-info { flex: 1; }
89
+ .face-bar-track { height: 8px; background: #1a1a30; border-radius: 4px; overflow: hidden; margin-bottom: 6px; }
90
+ .face-bar-fill { height: 100%; border-radius: 4px; transition: width 0.6s ease; }
91
+ .bar-green { background: #4caf50; } .bar-orange { background: #ff9800; } .bar-red { background: #f44336; }
92
+ .face-score { font-size: 13px; font-weight: 600; }
93
+ .score-green { color: #4caf50; } .score-orange { color: #ff9800; } .score-red { color: #f44336; }
94
+ @media (max-width: 768px) {
95
+ .hero { flex-direction: column; padding: 0 20px; margin-top: 30px; gap: 30px; }
96
+ .hero-left { max-width: 100%; } .hero-title { font-size: 32px; }
97
+ .hero-right { display: none; }
98
+ .results-section { padding: 0 20px; } .results-panel { padding: 20px; }
99
+ .navbar { padding: 14px 20px; } .navbar nav a { margin-left: 16px; font-size: 13px; }
100
+ .compare-grid { flex-direction: column; }
101
+ .pipeline-overview { flex-direction: column; align-items: center; }
102
+ .flow-arrow { transform: rotate(90deg); }
103
+ .tech-step-header { flex-direction: column; align-items: center; text-align: center; }
104
+ .tech-page { padding: 0 16px; }
105
+ }
106
+
107
+ /* ── Technology Page ── */
108
+ .tech-page { max-width: 900px; margin: 0 auto; padding: 0 40px 60px; }
109
+ .tech-hero { text-align: center; margin: 50px 0 40px; }
110
+ .tech-title { font-size: 42px; font-weight: 800; color: #fff; margin-bottom: 14px; }
111
+ .tech-subtitle { font-size: 15px; color: #999; line-height: 1.7; max-width: 600px; margin: 0 auto; }
112
+
113
+ .pipeline-overview {
114
+ display: flex; align-items: center; justify-content: center; gap: 12px;
115
+ margin-bottom: 50px; flex-wrap: wrap;
116
+ }
117
+ .pipeline-step-mini {
118
+ display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center; width: 110px;
119
+ }
120
+ .flow-arrow { display: flex; align-items: center; color: #6c8cff; }
121
+
122
+ .tech-step {
123
+ background: #111122; border: 1px solid #1e1e35; border-radius: 14px;
124
+ padding: 30px 36px; margin-bottom: 28px;
125
+ }
126
+ .tech-step-header { display: flex; align-items: center; gap: 20px; margin-bottom: 16px; }
127
+ .tech-step-title { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
128
+ .tech-file {
129
+ font-size: 12px; color: #6c8cff; background: rgba(108,140,255,0.1);
130
+ padding: 3px 10px; border-radius: 4px; font-family: 'Consolas', monospace;
131
+ }
132
+ .tech-step-desc { font-size: 14px; color: #aaa; line-height: 1.7; margin-bottom: 20px; }
133
+ .tech-step-diagram {
134
+ background: #0b0b1a; border: 1px solid #1a1a2e; border-radius: 10px;
135
+ padding: 20px; margin-bottom: 20px; text-align: center; overflow-x: auto;
136
+ }
137
+ .step-illustration { max-width: 100%; height: auto; }
138
+ .step-icon { flex-shrink: 0; }
139
+ .tech-step-details {
140
+ list-style: none; padding: 0;
141
+ }
142
+ .tech-step-details li {
143
+ font-size: 13px; color: #bbb; padding: 6px 0 6px 20px; position: relative; line-height: 1.5;
144
+ }
145
+ .tech-step-details li::before {
146
+ content: '→'; position: absolute; left: 0; color: #6c8cff; font-weight: 700;
147
+ }
App/templates/index.html ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>AI-Deepfake Video Detection</title>
7
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
8
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
9
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
10
+ <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+
15
+ <script type="text/babel" src="/static/Product.jsx"></script>
16
+ <script type="text/babel" src="/static/Technology.jsx"></script>
17
+ <script type="text/babel" src="/static/app.jsx"></script>
18
+ </body>
19
+ </html>
Dockerfile ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ RUN apt-get update && apt-get install -y --no-install-recommends \
4
+ ffmpeg \
5
+ libgl1 \
6
+ libglib2.0-0 \
7
+ libsm6 \
8
+ libxext6 \
9
+ libxrender1 \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ RUN useradd -m -u 1000 user
13
+
14
+ USER user
15
+ ENV HOME=/home/user \
16
+ PATH=/home/user/.local/bin:$PATH \
17
+ PYTHONDONTWRITEBYTECODE=1 \
18
+ PYTHONUNBUFFERED=1 \
19
+ PORT=7860
20
+
21
+ ENV ENABLE_PREVIEW_FACE_DETECTOR=0
22
+
23
+ WORKDIR $HOME/app
24
+
25
+ RUN pip install --no-cache-dir --upgrade pip
26
+
27
+ COPY --chown=user requirements.txt $HOME/app/requirements.txt
28
+ RUN pip install --no-cache-dir -r requirements.txt
29
+
30
+ COPY --chown=user . $HOME/app
31
+
32
+ EXPOSE 7860
33
+
34
+ CMD ["python", "App/app.py"]
README.md ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: DeepFake Detect
3
+ emoji: 🎭
4
+ colorFrom: red
5
+ colorTo: gray
6
+ sdk: docker
7
+ app_port: 7860
8
+ short_description: Face-level deepfake video detection with a Docker-based web app.
9
+ ---
10
+
11
+ # DeepFake-Detect
12
+
13
+ A Python-based deepfake video detection project with two main parts:
14
+
15
+ 1. An offline training pipeline that extracts frames, crops faces, prepares labeled data, and trains a binary classifier.
16
+ 2. A Flask web application that accepts uploaded videos, analyzes faces, returns authenticity scores, and generates a processed preview video with face boxes.
17
+
18
+ The current repository is focused on face-level deepfake detection rather than general-purpose video understanding.
19
+
20
+ ## What This Project Does
21
+
22
+ - Takes videos as input and samples frames once per second.
23
+ - Uses `MTCNN` to detect and crop faces from extracted frames.
24
+ - Uses FaceForensics++ CSV metadata to label face samples as `REAL` or `FAKE`.
25
+ - Filters out very small face crops and builds `train / val / test` datasets.
26
+ - Trains an `EfficientNetB0`-based classifier with transfer learning and fine-tuning.
27
+ - Provides a Flask web app for interactive video analysis.
28
+
29
+ ## Tech Stack
30
+
31
+ - Backend: `Flask`
32
+ - Deep learning: `TensorFlow / Keras`
33
+ - Main classifier: `EfficientNetB0`
34
+ - Face detection for training/inference crops: `MTCNN`
35
+ - Face detection for preview overlays: `YOLOv8 face`
36
+ - Video processing: `OpenCV`, `ffmpeg` via `imageio-ffmpeg`
37
+ - Dataset splitting: `split-folders`
38
+
39
+ ## Detection Pipeline
40
+
41
+ ### Training Stage
42
+
43
+ #### 1. Convert videos to frames
44
+ Script: [00-convert_video_to_image.py](/Users/zhangke/Documents/Projects/DeepFake-Detect/00-convert_video_to_image.py)
45
+
46
+ - Reads videos from subfolders under `train_sample_videos/FaceForensics++_C23/`.
47
+ - Extracts 1 frame per second.
48
+ - Dynamically rescales each frame based on source resolution:
49
+ - Width `< 300`: scale to `2x`
50
+ - Width `> 1900`: scale to `0.33x`
51
+ - Width `1000 ~ 1900`: scale to `0.5x`
52
+ - Otherwise: keep original size
53
+ - Stores extracted PNG frames in a per-video directory.
54
+
55
+ #### 2. Detect and crop faces
56
+ Script: [01-crop_faces_with_mtcnn.py](/Users/zhangke/Documents/Projects/DeepFake-Detect/01-crop_faces_with_mtcnn.py)
57
+
58
+ - Runs `MTCNN` on every extracted frame.
59
+ - If a frame contains only one face, that detection is kept.
60
+ - If a frame contains multiple faces, only detections with confidence above `0.95` are kept.
61
+ - Expands each bounding box by `30%` to preserve more facial context.
62
+ - Saves cropped faces into a `faces/` subdirectory for each video.
63
+
64
+ #### 3. Prepare the dataset
65
+ Script: [02-prepare_fake_real_dataset.py](/Users/zhangke/Documents/Projects/DeepFake-Detect/02-prepare_fake_real_dataset.py)
66
+
67
+ - Reads labels from `csv/*.csv`.
68
+ - Copies face crops into:
69
+ - `prepared_dataset/real`
70
+ - `prepared_dataset/fake`
71
+ - Filters out any image with width or height smaller than `90px`.
72
+ - Splits the final dataset with `split-folders` into:
73
+ - `split_dataset/train`
74
+ - `split_dataset/val`
75
+ - `split_dataset/test`
76
+
77
+ #### 4. Train the classifier
78
+ Script: [03-train_cnn.py](/Users/zhangke/Documents/Projects/DeepFake-Detect/03-train_cnn.py)
79
+
80
+ - Input size: `224x224`
81
+ - Backbone: `EfficientNetB0(weights="imagenet")`
82
+ - Output: single-unit `sigmoid` binary classifier
83
+ - Training strategy:
84
+ - Phase 1: freeze the backbone and train only the head with learning rate `1e-3`
85
+ - Phase 2: unfreeze the full model and fine-tune with learning rate `1e-5`
86
+ - Data augmentation includes:
87
+ - rotation
88
+ - horizontal flip
89
+ - zoom
90
+ - translation
91
+ - brightness jitter
92
+ - Uses:
93
+ - `EarlyStopping`
94
+ - `ModelCheckpoint`
95
+ - `ReduceLROnPlateau`
96
+ - Applies class weights to reduce `fake/real` imbalance.
97
+
98
+ The best trained model is saved to:
99
+
100
+ ```text
101
+ tmp_checkpoint/best_model.keras
102
+ ```
103
+
104
+ This is the canonical model output path used by the training pipeline.
105
+
106
+ ## Web App Inference Flow
107
+
108
+ App entry point: [App/app.py](/Users/zhangke/Documents/Projects/DeepFake-Detect/App/app.py)
109
+
110
+ Routes: [App/route.py](/Users/zhangke/Documents/Projects/DeepFake-Detect/App/route.py)
111
+
112
+ Frontend files:
113
+
114
+ - [App/templates/index.html](/Users/zhangke/Documents/Projects/DeepFake-Detect/App/templates/index.html)
115
+ - [App/static/app.jsx](/Users/zhangke/Documents/Projects/DeepFake-Detect/App/static/app.jsx)
116
+ - [App/static/Product.jsx](/Users/zhangke/Documents/Projects/DeepFake-Detect/App/static/Product.jsx)
117
+ - [App/static/Technology.jsx](/Users/zhangke/Documents/Projects/DeepFake-Detect/App/static/Technology.jsx)
118
+
119
+ ### Actual inference logic
120
+
121
+ When a video is uploaded, the backend performs these steps:
122
+
123
+ 1. Validates the file type: `mp4`, `avi`, `mov`, `mkv`, `wmv`
124
+ 2. Re-encodes the uploaded video to browser-friendly `H.264`
125
+ 3. Reads frames roughly once per second
126
+ 4. Uses `MTCNN` to extract faces for classification
127
+ 5. Runs `EfficientNetB0` inference on each face crop
128
+ 6. Sorts all face scores from highest to lowest and averages the top `30%`
129
+ 7. If the averaged score is `> 0.5`, the video is labeled `REAL`; otherwise `FAKE`
130
+ 8. Returns:
131
+ - final label
132
+ - confidence
133
+ - model score
134
+ - number of faces analyzed
135
+ - face thumbnails with per-face scores
136
+ 9. Uses `YOLOv8 face` to generate a preview video with face bounding boxes
137
+
138
+ Notes:
139
+
140
+ - `MTCNN` is used for the actual face crops fed into the classifier.
141
+ - `YOLOv8 face` is currently used for visualization in the processed preview video, not as the classifier itself.
142
+
143
+ ## Repository Structure
144
+
145
+ ```text
146
+ DeepFake-Detect/
147
+ ├── 00-convert_video_to_image.py
148
+ ├── 01-crop_faces_with_mtcnn.py
149
+ ├── 02-prepare_fake_real_dataset.py
150
+ ├── 03-train_cnn.py
151
+ ├── tmp_checkpoint/
152
+ │ ├── best_model.keras
153
+ │ └── best_model_phase1.keras
154
+ ├── App/
155
+ │ ├── app.py
156
+ │ ├── route.py
157
+ │ ├── yolov8n-face.pt
158
+ │ ├── static/
159
+ │ └── templates/
160
+ ├── train_sample_videos/
161
+ │ └── FaceForensics++_C23/
162
+ ├── best_model.keras
163
+ ├── pyproject.toml
164
+ └── uv.lock
165
+ ```
166
+
167
+ ## Dataset Layout
168
+
169
+ The training scripts expect the dataset root at:
170
+
171
+ ```text
172
+ train_sample_videos/FaceForensics++_C23/
173
+ ```
174
+
175
+ The repository currently contains these visible subdirectories:
176
+
177
+ - `original`
178
+ - `Deepfakes`
179
+ - `DeepFakeDetection`
180
+ - `Face2Face`
181
+ - `FaceSwap`
182
+ - `FaceShifter`
183
+ - `NeuralTextures`
184
+ - `csv`
185
+
186
+ CSV files include fields such as:
187
+
188
+ - `File Path`
189
+ - `Label`
190
+ - `Frame Count`
191
+ - `Width`
192
+ - `Height`
193
+ - `Codec`
194
+ - `File Size(MB)`
195
+
196
+ ## Requirements
197
+
198
+ - Python `>= 3.12`
199
+ - `uv` is recommended
200
+ - For training, an NVIDIA GPU that TensorFlow can detect is strongly recommended
201
+ - For the web app, a working `ffmpeg` runtime is required; the project accesses it through `imageio-ffmpeg`
202
+
203
+ ## Install Dependencies
204
+
205
+ ### Option 1: use uv
206
+
207
+ ```bash
208
+ uv sync
209
+ ```
210
+
211
+ ### Option 2: use venv + pip
212
+
213
+ ```bash
214
+ python -m venv .venv
215
+ source .venv/bin/activate
216
+ pip install -e .
217
+ ```
218
+
219
+ ## Quick Start
220
+
221
+ ### 1. Prepare the model file
222
+
223
+ The web app resolves the model in this order:
224
+
225
+ ```text
226
+ tmp_checkpoint/best_model.keras
227
+ best_model.keras
228
+ ```
229
+
230
+ `tmp_checkpoint/best_model.keras` is the canonical location. The root-level `best_model.keras` is treated as a compatibility fallback only.
231
+
232
+ If you want the project to use the standard training output path, place the model here:
233
+
234
+ ```bash
235
+ mkdir -p tmp_checkpoint
236
+ cp best_model.keras tmp_checkpoint/best_model.keras
237
+ ```
238
+
239
+ If you want to train from scratch, follow the training sequence below. The training script will generate this file automatically.
240
+
241
+ ### 2. Start the web app
242
+
243
+ ```bash
244
+ uv run python App/app.py
245
+ ```
246
+
247
+ Then open:
248
+
249
+ ```text
250
+ http://127.0.0.1:5001
251
+ ```
252
+
253
+ The app now defaults to port `5001`. You can override it with an environment variable:
254
+
255
+ ```bash
256
+ PORT=5050 uv run python App/app.py
257
+ ```
258
+
259
+ ## Hugging Face Spaces Deployment
260
+
261
+ This repository is now prepared for a Docker-based Hugging Face Space.
262
+
263
+ Deployment files:
264
+
265
+ - [Dockerfile](/Users/zhangke/Documents/Projects/DeepFake-Detect/Dockerfile)
266
+ - [.dockerignore](/Users/zhangke/Documents/Projects/DeepFake-Detect/.dockerignore)
267
+ - [requirements.txt](/Users/zhangke/Documents/Projects/DeepFake-Detect/requirements.txt)
268
+
269
+ The Space configuration is defined in the YAML header at the top of this README:
270
+
271
+ - `sdk: docker`
272
+ - `app_port: 7860`
273
+
274
+ Container runtime behavior:
275
+
276
+ - The container runs the Flask app with `python App/app.py`
277
+ - The Docker image sets `PORT=7860`
278
+ - The Docker image sets `ENABLE_PREVIEW_FACE_DETECTOR=0`
279
+ - The app itself already supports `PORT`, so it matches Hugging Face Spaces routing
280
+
281
+ Recommended deployment steps:
282
+
283
+ 1. Create a new Hugging Face Space and choose `Docker` as the SDK.
284
+ 2. Push this repository to that Space repository.
285
+ 3. Wait for the image build to complete.
286
+ 4. Open the Space once the container becomes healthy.
287
+
288
+ Notes for this project on Spaces:
289
+
290
+ - The Docker build excludes local training data and the duplicate root-level `best_model.keras` from the build context.
291
+ - The canonical runtime model remains `tmp_checkpoint/best_model.keras`.
292
+ - The app uses CPU by default unless you assign GPU hardware to the Space.
293
+ - To keep the Docker image smaller and easier to build, the Space disables YOLO-based preview overlays by default and falls back to a re-encoded original video.
294
+
295
+ Local Docker smoke test:
296
+
297
+ ```bash
298
+ docker build -t deepfake-detect-space .
299
+ docker run --rm -p 7860:7860 deepfake-detect-space
300
+ ```
301
+
302
+ Then open:
303
+
304
+ ```text
305
+ http://127.0.0.1:7860
306
+ ```
307
+
308
+ ## Training Order
309
+
310
+ To reproduce the full training pipeline, run the scripts in this order:
311
+
312
+ ```bash
313
+ uv run python 00-convert_video_to_image.py
314
+ uv run python 01-crop_faces_with_mtcnn.py
315
+ uv run python 02-prepare_fake_real_dataset.py
316
+ uv run python 03-train_cnn.py
317
+ ```
318
+
319
+ ## Key Outputs
320
+
321
+ - Frame extraction output: per-video frame folders
322
+ - Face crops: `faces/` inside each processed video folder
323
+ - Aggregated dataset: `prepared_dataset/`
324
+ - Train/validation/test splits: `split_dataset/`
325
+ - Trained model: `tmp_checkpoint/best_model.keras`
326
+ - Phase 1 checkpoint: `tmp_checkpoint/best_model_phase1.keras`
327
+ - Compatibility fallback model: `best_model.keras`
328
+ - Web upload directory: `App/uploads/`
329
+ - Inference diagnostics log: `App/diag_log.txt`
330
+
331
+ ## Important Implementation Details
332
+
333
+ These details matter when understanding the current system:
334
+
335
+ - This is a face-crop-based binary classifier, not an end-to-end video transformer.
336
+ - The model score semantics are: values closer to `1` mean more likely real, values closer to `0` mean more likely fake.
337
+ - The video-level decision is not a plain average across all faces; it uses the mean of the highest-scoring subset.
338
+ - The upload endpoint enforces a `200 MB` limit.
339
+ - The app cleans old uploaded files, so `App/uploads/` should not be treated as persistent storage.
340
+
341
+ ## Known Limitations
342
+
343
+ - Both training and inference depend heavily on face detection quality.
344
+ - The current sampling strategy uses only 1 frame per second, which may miss short-lived manipulation artifacts.
345
+ - The repository does not currently provide a standalone CLI inference script; the primary entry point is the Flask app.
346
+ - The canonical model path is `tmp_checkpoint/best_model.keras`, but the app also supports `best_model.keras` in the repository root as a fallback.
347
+ - This README is based on the current codebase behavior. If UI text and code behavior differ, trust the code.
348
+
349
+ ## Possible Next Improvements
350
+
351
+ - Add a CLI inference entry point
352
+ - Move paths, thresholds, and input/output directories into configuration
353
+ - Persist training metrics and experiment logs
354
+ - Add batch video inference support
355
+ - Add Docker and deployment documentation
356
+
357
+ ## License
358
+
359
+ No explicit license file is present in the repository at the moment. If you plan to publish or use this project commercially, add a proper license first.
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flask>=3.1.3
2
+ h5py>=3.14.0
3
+ imageio-ffmpeg>=0.6.0
4
+ mtcnn>=0.1.0
5
+ numpy>=2.4.4
6
+ opencv-python>=4.1.0
7
+ pandas>=3.0.2
8
+ pillow>=12.2.0
9
+ scikit-learn>=1.8.0
10
+ split-folders>=0.6.1
11
+ tensorflow==2.21.0
12
+ werkzeug>=3.1.8
tmp_checkpoint/best_model.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:64588ac82fba3224df0d322785d260899abd2e7fec5122fbaa61ad798cc785b4
3
+ size 53251173