Vinh.Vu commited on
Commit
3b7fd58
·
1 Parent(s): 15d65e1

Initial the project

Browse files
.gitignore ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
130
+
131
+ #temp dataset folders
132
+ tmp_*/
133
+ mtcnn/
134
+ /.conda
135
+ /train_sample_videos
136
+ /split_dataset
137
+ /prepared_dataset
138
+ /App/uploads
00-convert_video_to_image.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import cv2
4
+ import math
5
+
6
+ base_path = '.\\train_sample_videos\\'
7
+
8
+ def get_filename_only(file_path):
9
+ file_basename = os.path.basename(file_path)
10
+ filename_only = file_basename.split('.')[0]
11
+ return filename_only
12
+
13
+ with open(os.path.join(base_path, 'metadata.json')) as metadata_json:
14
+ metadata = json.load(metadata_json)
15
+ print(len(metadata))
16
+
17
+ for filename in metadata.keys():
18
+ print(filename)
19
+ if (filename.endswith(".mp4")):
20
+ tmp_path = os.path.join(base_path, get_filename_only(filename))
21
+ print('Creating Directory: ' + tmp_path)
22
+ os.makedirs(tmp_path, exist_ok=True)
23
+ print('Converting Video to Images...')
24
+ count = 0
25
+ video_file = os.path.join(base_path, filename)
26
+ cap = cv2.VideoCapture(video_file)
27
+ frame_rate = cap.get(5) #frame rate
28
+ while(cap.isOpened()):
29
+ frame_id = cap.get(1) #current frame number
30
+ ret, frame = cap.read()
31
+ if (ret != True):
32
+ break
33
+ if (frame_id % math.floor(frame_rate) == 0):
34
+ print('Original Dimensions: ', frame.shape)
35
+ if frame.shape[1] < 300:
36
+ scale_ratio = 2
37
+ elif frame.shape[1] > 1900:
38
+ scale_ratio = 0.33
39
+ elif frame.shape[1] > 1000 and frame.shape[1] <= 1900 :
40
+ scale_ratio = 0.5
41
+ else:
42
+ scale_ratio = 1
43
+ print('Scale Ratio: ', scale_ratio)
44
+
45
+ width = int(frame.shape[1] * scale_ratio)
46
+ height = int(frame.shape[0] * scale_ratio)
47
+ dim = (width, height)
48
+ new_frame = cv2.resize(frame, dim, interpolation = cv2.INTER_AREA)
49
+ print('Resized Dimensions: ', new_frame.shape)
50
+
51
+ new_filename = '{}-{:03d}.png'.format(os.path.join(tmp_path, get_filename_only(filename)), count)
52
+ count = count + 1
53
+ cv2.imwrite(new_filename, new_frame)
54
+ cap.release()
55
+ print("Done!")
56
+ else:
57
+ continue
01a-crop_faces_with_mtcnn.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ from mtcnn import MTCNN
3
+ import sys, os.path
4
+ import json
5
+ from keras import backend as K
6
+ import tensorflow as tf
7
+ print(tf.__version__)
8
+ tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)
9
+
10
+ physical_devices = tf.config.list_physical_devices('GPU')
11
+ print(physical_devices)
12
+ if physical_devices:
13
+ tf.config.experimental.set_memory_growth(physical_devices[0], True)
14
+
15
+ base_path = '.\\train_sample_videos\\'
16
+
17
+ def get_filename_only(file_path):
18
+ file_basename = os.path.basename(file_path)
19
+ filename_only = file_basename.split('.')[0]
20
+ return filename_only
21
+
22
+ with open(os.path.join(base_path, 'metadata.json')) as metadata_json:
23
+ metadata = json.load(metadata_json)
24
+ print(len(metadata))
25
+
26
+ for filename in metadata.keys():
27
+ tmp_path = os.path.join(base_path, get_filename_only(filename))
28
+ print('Processing Directory: ' + tmp_path)
29
+ frame_images = [x for x in os.listdir(tmp_path) if os.path.isfile(os.path.join(tmp_path, x))]
30
+ faces_path = os.path.join(tmp_path, 'faces')
31
+ print('Creating Directory: ' + faces_path)
32
+ os.makedirs(faces_path, exist_ok=True)
33
+ print('Cropping Faces from Images...')
34
+
35
+ for frame in frame_images:
36
+ print('Processing ', frame)
37
+ detector = MTCNN()
38
+ image = cv2.cvtColor(cv2.imread(os.path.join(tmp_path, frame)), cv2.COLOR_BGR2RGB)
39
+ results = detector.detect_faces(image)
40
+ print('Face Detected: ', len(results))
41
+ count = 0
42
+
43
+ for result in results:
44
+ bounding_box = result['box']
45
+ print(bounding_box)
46
+ confidence = result['confidence']
47
+ print(confidence)
48
+ if len(results) < 2 or confidence > 0.95:
49
+ margin_x = bounding_box[2] * 0.3 # 30% as the margin
50
+ margin_y = bounding_box[3] * 0.3 # 30% as the margin
51
+ x1 = int(bounding_box[0] - margin_x)
52
+ if x1 < 0:
53
+ x1 = 0
54
+ x2 = int(bounding_box[0] + bounding_box[2] + margin_x)
55
+ if x2 > image.shape[1]:
56
+ x2 = image.shape[1]
57
+ y1 = int(bounding_box[1] - margin_y)
58
+ if y1 < 0:
59
+ y1 = 0
60
+ y2 = int(bounding_box[1] + bounding_box[3] + margin_y)
61
+ if y2 > image.shape[0]:
62
+ y2 = image.shape[0]
63
+ print(x1, y1, x2, y2)
64
+ crop_image = image[y1:y2, x1:x2]
65
+ new_filename = '{}-{:02d}.png'.format(os.path.join(faces_path, get_filename_only(frame)), count)
66
+ count = count + 1
67
+ cv2.imwrite(new_filename, cv2.cvtColor(crop_image, cv2.COLOR_RGB2BGR))
68
+ else:
69
+ print('Skipped a face..')
70
+
01b-crop_faces_with_azure-vision-api.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import sys, os.path
3
+ import json
4
+ import http.client, urllib.request, urllib.parse, urllib.error, base64
5
+
6
+ base_path = '.\\train_sample_videos\\'
7
+ AZURE_COMPUTER_VISION_NAME = '----REPLACE-WITH-YOUR-SERVICE-NAME----' # e.g. xxxxxxxxxx.cognitiveservices.azure.com
8
+ AZURE_COMPUTER_VISION_API_KEY = '----REPLACE-WITH-YOUR-KEY----'
9
+
10
+ def get_filename_only(file_path):
11
+ file_basename = os.path.basename(file_path)
12
+ filename_only = file_basename.split('.')[0]
13
+ return filename_only
14
+
15
+ with open(os.path.join(base_path, 'metadata.json')) as metadata_json:
16
+ metadata = json.load(metadata_json)
17
+ print(len(metadata))
18
+
19
+ for filename in metadata.keys():
20
+ tmp_path = os.path.join(base_path, get_filename_only(filename))
21
+ print('Processing Directory: ' + tmp_path)
22
+ frame_images = [x for x in os.listdir(tmp_path) if os.path.isfile(os.path.join(tmp_path, x))]
23
+ faces_path = os.path.join(tmp_path, 'faces')
24
+ print('Creating Directory: ' + faces_path)
25
+ os.makedirs(faces_path, exist_ok=True)
26
+ print('Cropping Faces from Images...')
27
+
28
+ for frame in frame_images:
29
+ print('Processing ', frame)
30
+ image = cv2.cvtColor(cv2.imread(os.path.join(tmp_path, frame)), cv2.COLOR_BGR2RGB)
31
+
32
+ # Open the binary file
33
+ with open(os.path.join(tmp_path, frame), 'rb') as file_contents:
34
+ img_data = file_contents.read()
35
+
36
+ ######### Azure Computer Vision API
37
+ headers = {
38
+ # Request headers
39
+ 'Content-Type': 'application/octet-stream',
40
+ 'Ocp-Apim-Subscription-Key': AZURE_COMPUTER_VISION_API_KEY,
41
+ }
42
+
43
+ params = urllib.parse.urlencode({
44
+ # Request parameters
45
+ 'visualFeatures': 'Faces'
46
+ })
47
+
48
+ try:
49
+ conn = http.client.HTTPSConnection(AZURE_COMPUTER_VISION_NAME)
50
+ conn.request("POST", "/vision/v3.0/analyze?%s" % params, img_data, headers)
51
+ response = conn.getresponse().read()
52
+ data = json.loads(response.decode('utf-8'))
53
+ print(data)
54
+ conn.close()
55
+ except Exception as e:
56
+ print("[Errno {0}] {1}".format(e.errno, e.strerror))
57
+ continue
58
+
59
+ print(data['faces'])
60
+ print('Face Detected: ', len(data['faces']))
61
+ count = 0
62
+
63
+ for result in data['faces']:
64
+ bounding_box = []
65
+ bounding_box.append(result['faceRectangle']['left'])
66
+ bounding_box.append(result['faceRectangle']['top'])
67
+ bounding_box.append(result['faceRectangle']['width'])
68
+ bounding_box.append(result['faceRectangle']['height'])
69
+ print(bounding_box)
70
+
71
+ margin_x = bounding_box[2] * 0.3 # 30% as the margin
72
+ margin_y = bounding_box[3] * 0.3 # 30% as the margin
73
+ x1 = int(bounding_box[0] - margin_x)
74
+ if x1 < 0:
75
+ x1 = 0
76
+ x2 = int(bounding_box[0] + bounding_box[2] + margin_x)
77
+ if x2 > image.shape[1]:
78
+ x2 = image.shape[1]
79
+ y1 = int(bounding_box[1] - margin_y)
80
+ if y1 < 0:
81
+ y1 = 0
82
+ y2 = int(bounding_box[1] + bounding_box[3] + margin_y)
83
+ if y2 > image.shape[0]:
84
+ y2 = image.shape[0]
85
+ print(x1, y1, x2, y2)
86
+ crop_image = image[y1:y2, x1:x2]
87
+ new_filename = '{}-{:02d}.png'.format(os.path.join(faces_path, get_filename_only(frame)), count)
88
+ count = count + 1
89
+ cv2.imwrite(new_filename, cv2.cvtColor(crop_image, cv2.COLOR_RGB2BGR))
02-prepare_fake_real_dataset.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from distutils.dir_util import copy_tree
4
+ import shutil
5
+ import numpy as np
6
+ import splitfolders as split_folders
7
+
8
+ base_path = '.\\train_sample_videos\\'
9
+ dataset_path = '.\\prepared_dataset\\'
10
+ print('Creating Directory: ' + dataset_path)
11
+ os.makedirs(dataset_path, exist_ok=True)
12
+
13
+ tmp_fake_path = '.\\tmp_fake_faces'
14
+ print('Creating Directory: ' + tmp_fake_path)
15
+ os.makedirs(tmp_fake_path, exist_ok=True)
16
+
17
+ def get_filename_only(file_path):
18
+ file_basename = os.path.basename(file_path)
19
+ filename_only = file_basename.split('.')[0]
20
+ return filename_only
21
+
22
+ with open(os.path.join(base_path, 'metadata.json')) as metadata_json:
23
+ metadata = json.load(metadata_json)
24
+ print(len(metadata))
25
+
26
+ real_path = os.path.join(dataset_path, 'real')
27
+ print('Creating Directory: ' + real_path)
28
+ os.makedirs(real_path, exist_ok=True)
29
+
30
+ fake_path = os.path.join(dataset_path, 'fake')
31
+ print('Creating Directory: ' + fake_path)
32
+ os.makedirs(fake_path, exist_ok=True)
33
+
34
+ for filename in metadata.keys():
35
+ print(filename)
36
+ print(metadata[filename]['label'])
37
+ tmp_path = os.path.join(os.path.join(base_path, get_filename_only(filename)), 'faces')
38
+ print(tmp_path)
39
+ if os.path.exists(tmp_path):
40
+ if metadata[filename]['label'] == 'REAL':
41
+ print('Copying to :' + real_path)
42
+ copy_tree(tmp_path, real_path)
43
+ elif metadata[filename]['label'] == 'FAKE':
44
+ print('Copying to :' + tmp_fake_path)
45
+ copy_tree(tmp_path, tmp_fake_path)
46
+ else:
47
+ print('Ignored..')
48
+
49
+ all_real_faces = [f for f in os.listdir(real_path) if os.path.isfile(os.path.join(real_path, f))]
50
+ print('Total Number of Real faces: ', len(all_real_faces))
51
+
52
+ all_fake_faces = [f for f in os.listdir(tmp_fake_path) if os.path.isfile(os.path.join(tmp_fake_path, f))]
53
+ print('Total Number of Fake faces: ', len(all_fake_faces))
54
+
55
+ random_faces = np.random.choice(all_fake_faces, len(all_real_faces), replace=False)
56
+ for fname in random_faces:
57
+ src = os.path.join(tmp_fake_path, fname)
58
+ dst = os.path.join(fake_path, fname)
59
+ shutil.copyfile(src, dst)
60
+
61
+ print('Down-sampling Done!')
62
+
63
+ # Split into Train/ Val/ Test folders
64
+ split_folders.ratio(dataset_path, output='split_dataset', seed=1377, ratio=(.8, .1, .1)) # default values
65
+ print('Train/ Val/ Test Split Done!')
03-train_cnn.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from distutils.dir_util import copy_tree
4
+ import shutil
5
+ import pandas as pd
6
+
7
+ # TensorFlow and tf.keras
8
+ import tensorflow as tf
9
+ from tensorflow.keras import backend as K
10
+ print('TensorFlow version: ', tf.__version__)
11
+
12
+ # Set to force CPU
13
+ #os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
14
+ #if tf.test.gpu_device_name():
15
+ # print('GPU found')
16
+ #else:
17
+ # print("No GPU found")
18
+
19
+ dataset_path = '.\\split_dataset\\'
20
+
21
+ tmp_debug_path = '.\\tmp_debug'
22
+ print('Creating Directory: ' + tmp_debug_path)
23
+ os.makedirs(tmp_debug_path, exist_ok=True)
24
+
25
+ def get_filename_only(file_path):
26
+ file_basename = os.path.basename(file_path)
27
+ filename_only = file_basename.split('.')[0]
28
+ return filename_only
29
+
30
+ from tensorflow.keras.preprocessing.image import ImageDataGenerator
31
+ from tensorflow.keras import applications
32
+ from tensorflow.keras.applications import EfficientNetB0
33
+ from tensorflow.keras.models import Sequential
34
+ from tensorflow.keras.layers import Dense, Dropout
35
+ from tensorflow.keras.optimizers import Adam
36
+ from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
37
+ from tensorflow.keras.models import load_model
38
+
39
+ input_size = 128
40
+ batch_size_num = 32
41
+ train_path = os.path.join(dataset_path, 'train')
42
+ val_path = os.path.join(dataset_path, 'val')
43
+ test_path = os.path.join(dataset_path, 'test')
44
+
45
+ train_datagen = ImageDataGenerator(
46
+ rescale = 1/255, #rescale the tensor values to [0,1]
47
+ rotation_range = 10,
48
+ width_shift_range = 0.1,
49
+ height_shift_range = 0.1,
50
+ shear_range = 0.2,
51
+ zoom_range = 0.1,
52
+ horizontal_flip = True,
53
+ fill_mode = 'nearest'
54
+ )
55
+
56
+ train_generator = train_datagen.flow_from_directory(
57
+ directory = train_path,
58
+ target_size = (input_size, input_size),
59
+ color_mode = "rgb",
60
+ class_mode = "binary", #"categorical", "binary", "sparse", "input"
61
+ batch_size = batch_size_num,
62
+ shuffle = True
63
+ #save_to_dir = tmp_debug_path
64
+ )
65
+
66
+ val_datagen = ImageDataGenerator(
67
+ rescale = 1/255 #rescale the tensor values to [0,1]
68
+ )
69
+
70
+ val_generator = val_datagen.flow_from_directory(
71
+ directory = val_path,
72
+ target_size = (input_size, input_size),
73
+ color_mode = "rgb",
74
+ class_mode = "binary", #"categorical", "binary", "sparse", "input"
75
+ batch_size = batch_size_num,
76
+ shuffle = True
77
+ #save_to_dir = tmp_debug_path
78
+ )
79
+
80
+ test_datagen = ImageDataGenerator(
81
+ rescale = 1/255 #rescale the tensor values to [0,1]
82
+ )
83
+
84
+ test_generator = test_datagen.flow_from_directory(
85
+ directory = test_path,
86
+ classes=['fake', 'real'],
87
+ target_size = (input_size, input_size),
88
+ color_mode = "rgb",
89
+ class_mode = None,
90
+ batch_size = 1,
91
+ shuffle = False
92
+ )
93
+
94
+ # Train a CNN classifier
95
+ efficient_net = EfficientNetB0(
96
+ weights = 'imagenet',
97
+ input_shape = (input_size, input_size, 3),
98
+ include_top = False,
99
+ pooling = 'max'
100
+ )
101
+
102
+ model = Sequential()
103
+ model.add(efficient_net)
104
+ model.add(Dense(units = 512, activation = 'relu'))
105
+ model.add(Dropout(0.5))
106
+ model.add(Dense(units = 128, activation = 'relu'))
107
+ model.add(Dense(units = 1, activation = 'sigmoid'))
108
+ model.summary()
109
+
110
+ # Compile model
111
+ model.compile(optimizer = Adam(learning_rate=0.0001), loss='binary_crossentropy', metrics=['accuracy'])
112
+
113
+ checkpoint_filepath = '.\\tmp_checkpoint'
114
+ print('Creating Directory: ' + checkpoint_filepath)
115
+ os.makedirs(checkpoint_filepath, exist_ok=True)
116
+
117
+ custom_callbacks = [
118
+ EarlyStopping(
119
+ monitor = 'val_loss',
120
+ mode = 'min',
121
+ patience = 5,
122
+ verbose = 1
123
+ ),
124
+ ModelCheckpoint(
125
+ filepath = os.path.join(checkpoint_filepath, 'best_model.h5'),
126
+ monitor = 'val_loss',
127
+ mode = 'min',
128
+ verbose = 1,
129
+ save_best_only = True
130
+ )
131
+ ]
132
+
133
+ # Train network
134
+ num_epochs = 20
135
+ history = model.fit(
136
+ train_generator,
137
+ epochs = num_epochs,
138
+ steps_per_epoch = len(train_generator),
139
+ validation_data = val_generator,
140
+ validation_steps = len(val_generator),
141
+ callbacks = custom_callbacks
142
+ )
143
+ print(history.history)
144
+
145
+ '''
146
+ # Plot results
147
+ import matplotlib.pyplot as plt
148
+
149
+ acc = history.history['acc']
150
+ val_acc = history.history['val_acc']
151
+ loss = history.history['loss']
152
+ val_loss = history.history['val_loss']
153
+
154
+ epochs = range(1, len(acc) + 1)
155
+
156
+ plt.plot(epochs, acc, 'bo', label = 'Training Accuracy')
157
+ plt.plot(epochs, val_acc, 'b', label = 'Validation Accuracy')
158
+ plt.title('Training and Validation Accuracy')
159
+ plt.legend()
160
+ plt.figure()
161
+
162
+ plt.plot(epochs, loss, 'bo', label = 'Training loss')
163
+ plt.plot(epochs, val_loss, 'b', label = 'Validation Loss')
164
+ plt.title('Training and Validation Loss')
165
+ plt.legend()
166
+
167
+ plt.show()
168
+ '''
169
+
170
+ # load the saved model that is considered the best
171
+ best_model = load_model(os.path.join(checkpoint_filepath, 'best_model.h5'))
172
+
173
+ # Generate predictions
174
+ test_generator.reset()
175
+
176
+ preds = best_model.predict(
177
+ test_generator,
178
+ verbose = 1
179
+ )
180
+
181
+ test_results = pd.DataFrame({
182
+ "Filename": test_generator.filenames,
183
+ "Prediction": preds.flatten()
184
+ })
185
+ print(test_results)
App/app.py ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import io
4
+ import base64
5
+ import math
6
+ import logging
7
+ import subprocess
8
+ import cv2
9
+ import numpy as np
10
+ import imageio_ffmpeg
11
+ import mediapipe as mp
12
+ from mediapipe.tasks.python import BaseOptions
13
+ from mediapipe.tasks.python.vision import FaceDetector, FaceDetectorOptions
14
+ from flask import Flask, request, render_template, send_from_directory, jsonify
15
+ from werkzeug.utils import secure_filename
16
+ import uuid
17
+ import threading
18
+ import tensorflow as tf
19
+ from tensorflow.keras.models import load_model
20
+
21
+ logging.basicConfig(
22
+ level=logging.INFO,
23
+ format='%(asctime)s [%(levelname)s] %(message)s',
24
+ datefmt='%Y-%m-%d %H:%M:%S'
25
+ )
26
+ logger = logging.getLogger(__name__)
27
+
28
+ app = Flask(__name__)
29
+ app.config['UPLOAD_FOLDER'] = os.path.join(os.path.dirname(__file__), 'uploads')
30
+ app.config['MAX_CONTENT_LENGTH'] = 200 * 1024 * 1024 # 200 MB limit
31
+ ALLOWED_EXTENSIONS = {'mp4', 'avi', 'mov', 'mkv', 'wmv'}
32
+
33
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
34
+
35
+ # Load the trained model (suppress lz4 I/O warnings)
36
+ MODEL_PATH = os.path.join(os.path.dirname(__file__), '..', 'tmp_checkpoint', 'best_model.h5')
37
+ logger.info('Loading model from %s', MODEL_PATH)
38
+ _stderr = sys.stderr
39
+ sys.stderr = io.StringIO()
40
+ model = load_model(MODEL_PATH)
41
+ sys.stderr = _stderr
42
+ logger.info('Model loaded successfully')
43
+ INPUT_SIZE = 128
44
+
45
+ # Initialize MediaPipe face detector
46
+ logger.info('Initializing MediaPipe face detector')
47
+ FACE_MODEL_PATH = os.path.join(os.path.dirname(__file__), 'blaze_face_short_range.tflite')
48
+ face_detector_options = FaceDetectorOptions(
49
+ base_options=BaseOptions(model_asset_path=FACE_MODEL_PATH),
50
+ min_detection_confidence=0.5
51
+ )
52
+ logger.info('MediaPipe face detector ready')
53
+
54
+ # In-memory job store: job_id -> {status, result, ...}
55
+ jobs = {}
56
+
57
+
58
+ def allowed_file(filename):
59
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
60
+
61
+
62
+ def face_to_base64(face_rgb):
63
+ face_bgr = cv2.cvtColor(face_rgb, cv2.COLOR_RGB2BGR)
64
+ _, buffer = cv2.imencode('.png', face_bgr)
65
+ return base64.b64encode(buffer).decode('utf-8')
66
+
67
+
68
+ def reencode_to_h264(input_path, output_path=None):
69
+ """Re-encode a video to H.264 for browser compatibility. Overwrites in-place if no output_path."""
70
+ ffmpeg_exe = imageio_ffmpeg.get_ffmpeg_exe()
71
+ if output_path is None:
72
+ output_path = input_path
73
+ tmp = input_path + '.reencode.mp4'
74
+ cmd = [
75
+ ffmpeg_exe, '-y', '-i', input_path,
76
+ '-c:v', 'libx264', '-preset', 'fast',
77
+ '-movflags', '+faststart', '-pix_fmt', 'yuv420p',
78
+ tmp
79
+ ]
80
+ result = subprocess.run(cmd, capture_output=True, text=True)
81
+ if result.returncode != 0:
82
+ logger.error('ffmpeg reencode failed: %s', result.stderr)
83
+ try:
84
+ os.remove(tmp)
85
+ except OSError:
86
+ pass
87
+ return False
88
+ try:
89
+ os.replace(tmp, output_path)
90
+ except OSError:
91
+ os.remove(input_path)
92
+ os.rename(tmp, output_path)
93
+ return True
94
+
95
+
96
+ def extract_faces_from_video(video_path):
97
+ logger.info('Extracting faces from video: %s', video_path)
98
+ faces = []
99
+ cap = cv2.VideoCapture(video_path)
100
+ frame_rate = cap.get(cv2.CAP_PROP_FPS)
101
+ if frame_rate == 0:
102
+ logger.warning('Could not read frame rate from video')
103
+ cap.release()
104
+ return faces
105
+
106
+ with FaceDetector.create_from_options(face_detector_options) as face_det:
107
+ while cap.isOpened():
108
+ frame_id = cap.get(cv2.CAP_PROP_POS_FRAMES)
109
+ ret, frame = cap.read()
110
+ if not ret:
111
+ break
112
+ if frame_id % math.floor(frame_rate) == 0:
113
+ image_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
114
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=image_rgb)
115
+ results = face_det.detect(mp_image)
116
+ for detection in results.detections:
117
+ score = detection.categories[0].score
118
+ if len(results.detections) < 2 or score > 0.95:
119
+ bbox = detection.bounding_box
120
+ bx, by, bw, bh = bbox.origin_x, bbox.origin_y, bbox.width, bbox.height
121
+ h, w = image_rgb.shape[:2]
122
+ margin_x = int(bw * 0.3)
123
+ margin_y = int(bh * 0.3)
124
+ x1 = max(0, bx - margin_x)
125
+ x2 = min(w, bx + bw + margin_x)
126
+ y1 = max(0, by - margin_y)
127
+ y2 = min(h, by + bh + margin_y)
128
+ crop = image_rgb[y1:y2, x1:x2]
129
+ if crop.size > 0:
130
+ crop_resized = cv2.resize(crop, (INPUT_SIZE, INPUT_SIZE))
131
+ faces.append(crop_resized)
132
+
133
+ cap.release()
134
+ logger.info('Face extraction complete — %d faces found', len(faces))
135
+ return faces
136
+
137
+
138
+ def create_processed_video(video_path, output_path, avg_score):
139
+ """Re-encode video with face bounding boxes and label drawn on every frame."""
140
+ logger.info('Creating processed video with bounding boxes: %s', output_path)
141
+ is_real = avg_score is not None and avg_score > 0.5
142
+ label = 'REAL' if is_real else 'FAKE'
143
+ color = (0, 255, 0) if is_real else (0, 0, 255) # green / red in BGR
144
+
145
+ cap = cv2.VideoCapture(video_path)
146
+ fps = cap.get(cv2.CAP_PROP_FPS) or 30
147
+ w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
148
+ h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
149
+
150
+ # Write to a temp file with mp4v codec first
151
+ temp_path = output_path + '.tmp.mp4'
152
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
153
+ out = cv2.VideoWriter(temp_path, fourcc, fps, (w, h))
154
+
155
+ if not out.isOpened():
156
+ logger.error('VideoWriter failed to open: %s', temp_path)
157
+ cap.release()
158
+ return
159
+
160
+ frame_count = 0
161
+ with FaceDetector.create_from_options(face_detector_options) as face_det:
162
+ while cap.isOpened():
163
+ ret, frame = cap.read()
164
+ if not ret:
165
+ break
166
+ image_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
167
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=image_rgb)
168
+ results = face_det.detect(mp_image)
169
+ for detection in results.detections:
170
+ conf = detection.categories[0].score
171
+ if len(results.detections) < 2 or conf > 0.95:
172
+ bbox = detection.bounding_box
173
+ x, y = max(0, bbox.origin_x), max(0, bbox.origin_y)
174
+ bw, bh = bbox.width, bbox.height
175
+ cv2.rectangle(frame, (x, y), (x + bw, y + bh), color, 2)
176
+ text = f'{label} {conf:.2f}'
177
+ cv2.putText(frame, text, (x, y - 10),
178
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
179
+ out.write(frame)
180
+ frame_count += 1
181
+
182
+ cap.release()
183
+ out.release()
184
+ logger.info('Wrote %d frames to temp file, re-encoding to H.264', frame_count)
185
+
186
+ # Re-encode to H.264 for browser compatibility
187
+ if reencode_to_h264(temp_path, output_path):
188
+ logger.info('Processed video saved (H.264): %s', output_path)
189
+ else:
190
+ logger.error('Failed to re-encode processed video')
191
+
192
+ # Clean up temp file
193
+ try:
194
+ os.remove(temp_path)
195
+ except OSError:
196
+ pass
197
+
198
+
199
+ def predict_deepfake(faces):
200
+ if not faces:
201
+ logger.warning('No faces to predict on')
202
+ return None, 0, []
203
+
204
+ logger.info('Running prediction on %d face(s)', len(faces))
205
+
206
+ face_array = np.array(faces, dtype='float32') / 255.0
207
+ predictions = model.predict(face_array, verbose=0)
208
+ avg_prediction = float(np.mean(predictions))
209
+
210
+ # Build per-face details (up to 3 evenly spaced faces)
211
+ total = len(faces)
212
+ if total <= 3:
213
+ indices = list(range(total))
214
+ else:
215
+ indices = [0, total // 2, total - 1]
216
+
217
+ faces_detail = []
218
+ for i in indices:
219
+ faces_detail.append({
220
+ 'thumbnail': face_to_base64(faces[i]),
221
+ 'score': float(predictions[i][0])
222
+ })
223
+
224
+ logger.info('Prediction complete — avg score: %.4f, faces: %d', avg_prediction, total)
225
+ return avg_prediction, total, faces_detail
226
+
227
+
228
+ def cleanup_old_uploads(exclude=None):
229
+ """Delete all files in the upload folder except those in exclude."""
230
+ exclude = set(exclude or [])
231
+ folder = app.config['UPLOAD_FOLDER']
232
+ for f in os.listdir(folder):
233
+ fpath = os.path.join(folder, f)
234
+ if os.path.isfile(fpath) and fpath not in exclude:
235
+ try:
236
+ os.remove(fpath)
237
+ except PermissionError:
238
+ pass
239
+
240
+
241
+ @app.route('/', methods=['GET'])
242
+ def index():
243
+ return render_template('index.html')
244
+
245
+
246
+ @app.route('/uploads/<filename>')
247
+ def uploaded_video(filename):
248
+ return send_from_directory(app.config['UPLOAD_FOLDER'], filename, mimetype='video/mp4')
249
+
250
+
251
+ def process_video_job(job_id, filepath, unique_name):
252
+ """Background worker: extract faces, predict, create processed video."""
253
+ try:
254
+ logger.info('[Job %s] Starting face detection', job_id)
255
+ jobs[job_id]['status'] = 'detecting'
256
+
257
+ faces = extract_faces_from_video(filepath)
258
+ avg_score, num_faces, faces_detail = predict_deepfake(faces)
259
+
260
+ if avg_score is None:
261
+ logger.warning('[Job %s] No faces detected', job_id)
262
+ jobs[job_id].update({
263
+ 'status': 'done',
264
+ 'error': 'No faces detected in the video.',
265
+ 'video_url': f'/uploads/{unique_name}',
266
+ })
267
+ return
268
+
269
+ is_real = avg_score > 0.5
270
+ label = 'REAL' if is_real else 'FAKE'
271
+ confidence = avg_score if is_real else (1 - avg_score)
272
+
273
+ # Publish detection results immediately
274
+ logger.info('[Job %s] Detection done — result: %s, confidence: %.2f%%, faces: %d',
275
+ job_id, label, confidence * 100, num_faces)
276
+ jobs[job_id].update({
277
+ 'status': 'processing_video',
278
+ 'result': label,
279
+ 'confidence': round(confidence * 100, 2),
280
+ 'score': round(avg_score, 4),
281
+ 'num_faces': num_faces,
282
+ 'faces_detail': faces_detail,
283
+ 'video_url': f'/uploads/{unique_name}',
284
+ })
285
+
286
+ # Now generate processed video (results already visible to client)
287
+ logger.info('[Job %s] Starting video processing', job_id)
288
+ processed_name = f"processed_{unique_name}"
289
+ processed_path = os.path.join(app.config['UPLOAD_FOLDER'], processed_name)
290
+ create_processed_video(filepath, processed_path, avg_score)
291
+
292
+ logger.info('[Job %s] Video processing done', job_id)
293
+ jobs[job_id].update({
294
+ 'status': 'done',
295
+ 'processed_url': f'/uploads/{processed_name}',
296
+ })
297
+ except Exception as e:
298
+ logger.error('[Job %s] Error: %s', job_id, e)
299
+ jobs[job_id].update({'status': 'done', 'error': str(e)})
300
+
301
+
302
+ @app.route('/predict', methods=['POST'])
303
+ def predict():
304
+ if 'video' not in request.files:
305
+ return jsonify({'error': 'No video file uploaded.'}), 400
306
+
307
+ file = request.files['video']
308
+ if file.filename == '':
309
+ return jsonify({'error': 'No file selected.'}), 400
310
+
311
+ if not allowed_file(file.filename):
312
+ return jsonify({'error': 'Invalid file type. Allowed: mp4, avi, mov, mkv, wmv'}), 400
313
+
314
+ cleanup_old_uploads()
315
+
316
+ ext = secure_filename(file.filename).rsplit('.', 1)[1].lower()
317
+ unique_name = f"{uuid.uuid4().hex}.{ext}"
318
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_name)
319
+ file.save(filepath)
320
+ logger.info('Video uploaded: %s (%s)', file.filename, unique_name)
321
+
322
+ # Re-encode upload to H.264 so browser can play it
323
+ logger.info('Re-encoding uploaded video to H.264')
324
+ reencode_to_h264(filepath)
325
+
326
+ job_id = uuid.uuid4().hex
327
+ logger.info('Created job %s for %s', job_id, unique_name)
328
+ jobs[job_id] = {'status': 'uploading', 'video_url': f'/uploads/{unique_name}'}
329
+
330
+ thread = threading.Thread(target=process_video_job, args=(job_id, filepath, unique_name))
331
+ thread.start()
332
+
333
+ return jsonify({'job_id': job_id, 'video_url': f'/uploads/{unique_name}'})
334
+
335
+
336
+ @app.route('/status/<job_id>')
337
+ def job_status(job_id):
338
+ job = jobs.get(job_id)
339
+ if not job:
340
+ return jsonify({'error': 'Job not found'}), 404
341
+ return jsonify(job)
342
+
343
+
344
+ if __name__ == '__main__':
345
+ logger.info('Starting Flask server on http://0.0.0.0:5000')
346
+ app.run(debug=True, host='0.0.0.0', port=5000)
App/blaze_face_short_range.tflite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b4578f35940bf5a1a655214a1cce5cab13eba73c1297cd78e1a04c2380b0152f
3
+ size 229746
App/static/app.jsx ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ /* ── Navbar ── */
13
+ function Navbar() {
14
+ return (
15
+ <header className="navbar">
16
+ <div className="logo"><span>DF</span>Detect</div>
17
+ <nav>
18
+ <a href="/" className="active">Product</a>
19
+ <a href="#">Examples</a>
20
+ <a href="#">Technology</a>
21
+ <a href="#">FAQ</a>
22
+ </nav>
23
+ </header>
24
+ );
25
+ }
26
+
27
+ /* ── Upload Area ── */
28
+ function UploadArea({ file, onFileChange }) {
29
+ const inputRef = useRef();
30
+ return (
31
+ <div
32
+ className={`upload-area${file ? ' has-file' : ''}`}
33
+ onClick={() => inputRef.current.click()}
34
+ >
35
+ <div className="upload-text">
36
+ Drop a video here or click to upload &mdash; MP4, AVI, MOV, max 200 MB
37
+ </div>
38
+ {file && <div className="file-name">{file.name}</div>}
39
+ <input
40
+ ref={inputRef}
41
+ type="file"
42
+ accept=".mp4,.avi,.mov,.mkv,.wmv"
43
+ onChange={e => { onFileChange(e.target.files[0] || null); e.target.value = ''; }}
44
+ />
45
+ </div>
46
+ );
47
+ }
48
+
49
+ /* ── Video Preview ── */
50
+ function VideoPreview({ file, serverUrl }) {
51
+ const [localUrl, setLocalUrl] = useState(null);
52
+
53
+ useEffect(() => {
54
+ if (file) {
55
+ const url = URL.createObjectURL(file);
56
+ setLocalUrl(url);
57
+ return () => URL.revokeObjectURL(url);
58
+ }
59
+ setLocalUrl(null);
60
+ }, [file]);
61
+
62
+ const src = serverUrl || localUrl;
63
+ return (
64
+ <div className="video-preview">
65
+ {src
66
+ ? <video controls src={src} key={src} />
67
+ : <div className="preview-placeholder">&#127916;</div>
68
+ }
69
+ </div>
70
+ );
71
+ }
72
+
73
+ /* ── Status Spinner ── */
74
+ function StatusIndicator({ status }) {
75
+ if (!status || status === 'done') return null;
76
+ return (
77
+ <>
78
+ <div className="spinner" />
79
+ <p className="processing-text">{STATUS_MESSAGES[status] || 'Processing\u2026'}</p>
80
+ </>
81
+ );
82
+ }
83
+
84
+ /* ── Video Comparison ── */
85
+ function VideoComparison({ file, processedUrl, isProcessing }) {
86
+ const [localUrl, setLocalUrl] = useState(null);
87
+
88
+ useEffect(() => {
89
+ if (file) {
90
+ const url = URL.createObjectURL(file);
91
+ setLocalUrl(url);
92
+ return () => URL.revokeObjectURL(url);
93
+ }
94
+ setLocalUrl(null);
95
+ }, [file]);
96
+
97
+ if (!processedUrl && !isProcessing) return null;
98
+ return (
99
+ <section className="video-compare">
100
+ <h2>Face Detection</h2>
101
+ <div className="compare-grid">
102
+ <div className="compare-item">
103
+ {localUrl
104
+ ? <video controls src={localUrl} key={localUrl} />
105
+ : <div className="preview-placeholder">&#127916;</div>
106
+ }
107
+ <div className="compare-label original">Original</div>
108
+ </div>
109
+ <div className="compare-item">
110
+ {processedUrl
111
+ ? <video controls src={processedUrl} key={processedUrl} />
112
+ : <div className="preview-placeholder"><div className="spinner" /></div>
113
+ }
114
+ <div className="compare-label detected">
115
+ {processedUrl ? 'Detected Faces' : 'Generating\u2026'}
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </section>
120
+ );
121
+ }
122
+
123
+ /* ── Face Row ── */
124
+ function FaceRow({ face, index }) {
125
+ const pct = (face.score * 100).toFixed(2);
126
+ const w = (face.score * 100).toFixed(1);
127
+ return (
128
+ <div className="face-row">
129
+ <img className="face-thumb" src={`data:image/png;base64,${face.thumbnail}`} alt={`Face ${index + 1}`} />
130
+ <div className="face-info">
131
+ <div className="face-bar-track">
132
+ <div className={`face-bar-fill ${barClass(face.score)}`} style={{ width: `${w}%` }} />
133
+ </div>
134
+ <div className={`face-score ${scoreClass(face.score)}`}>{pct}% authentic</div>
135
+ </div>
136
+ </div>
137
+ );
138
+ }
139
+
140
+ /* ── Results Panel ── */
141
+ function ResultsPanel({ data }) {
142
+ if (!data || !data.result) return null;
143
+ const cls = data.result.toLowerCase();
144
+ return (
145
+ <section className="results-section">
146
+ <div className="results-panel">
147
+ <h2>Results</h2>
148
+ <p className="results-hint">
149
+ Authenticity score: likelihood the face is real.{' '}
150
+ <span className="score-red">Red &lt;20%</span>,{' '}
151
+ <span className="score-orange">Orange 20-80%</span>,{' '}
152
+ <span className="score-green">Green &gt;80%</span>.
153
+ </p>
154
+ <div className={`overall-result ${cls}`}>
155
+ <div className="overall-label">{data.result}</div>
156
+ <div className="overall-details">
157
+ Confidence: {data.confidence}%<br />
158
+ Model Score: {data.score}<br />
159
+ Faces Analyzed: {data.num_faces}
160
+ </div>
161
+ </div>
162
+ {data.faces_detail && data.faces_detail.map((face, i) => (
163
+ <FaceRow key={i} face={face} index={i} />
164
+ ))}
165
+ </div>
166
+ </section>
167
+ );
168
+ }
169
+
170
+ /* ── Main App ── */
171
+ function App() {
172
+ const [file, setFile] = useState(null);
173
+ const [status, setStatus] = useState(null);
174
+ const [error, setError] = useState(null);
175
+ const [result, setResult] = useState(null);
176
+ const [submitting, setSubmitting] = useState(false);
177
+ const timerRef = useRef(null);
178
+
179
+ const reset = () => { setResult(null); setError(null); setStatus(null); };
180
+
181
+ const handleFileChange = (f) => { setFile(f); reset(); };
182
+
183
+ const pollJob = useCallback((jobId) => {
184
+ timerRef.current = setTimeout(async () => {
185
+ try {
186
+ const res = await fetch(`/status/${jobId}`);
187
+ const data = await res.json();
188
+ setStatus(data.status);
189
+
190
+ // Show results as soon as detection is done (processing_video has result fields)
191
+ if (data.result) {
192
+ setResult(prev => ({ ...prev, ...data }));
193
+ }
194
+
195
+ if (data.status === 'done') {
196
+ setSubmitting(false);
197
+ if (data.error && !data.result) setError(data.error);
198
+ } else {
199
+ pollJob(jobId);
200
+ }
201
+ } catch {
202
+ setSubmitting(false);
203
+ setStatus(null);
204
+ setError('Connection lost. Please try again.');
205
+ }
206
+ }, 1000);
207
+ }, []);
208
+
209
+ useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current); }, []);
210
+
211
+ const handleSubmit = async (e) => {
212
+ e.preventDefault();
213
+ if (!file) return;
214
+ reset();
215
+ setSubmitting(true);
216
+ setStatus('uploading');
217
+
218
+ const fd = new FormData();
219
+ fd.append('video', file);
220
+
221
+ try {
222
+ const res = await fetch('/predict', { method: 'POST', body: fd });
223
+ const data = await res.json();
224
+ if (data.error) {
225
+ setError(data.error);
226
+ setSubmitting(false);
227
+ setStatus(null);
228
+ } else {
229
+ pollJob(data.job_id);
230
+ }
231
+ } catch {
232
+ setError('Upload failed. Please try again.');
233
+ setSubmitting(false);
234
+ setStatus(null);
235
+ }
236
+ };
237
+
238
+ return (
239
+ <>
240
+ <Navbar />
241
+
242
+ <section className="hero">
243
+ <div className="hero-left">
244
+ <h1 className="hero-title">DFDetect</h1>
245
+ <p className="hero-desc">
246
+ Free deepfake detection tool for videos. Upload a video and get
247
+ per-face authenticity scores in seconds. AI-powered synthetic face detection.
248
+ </p>
249
+
250
+ <form onSubmit={handleSubmit}>
251
+ <UploadArea file={file} onFileChange={handleFileChange} />
252
+ <button type="submit" className="btn" disabled={!file || submitting}>
253
+ {submitting ? (STATUS_MESSAGES[status] || 'Processing\u2026') : 'Analyze Video'}
254
+ </button>
255
+ </form>
256
+
257
+ <StatusIndicator status={submitting ? status : null} />
258
+ {error && <div className="error-box">{error}</div>}
259
+ </div>
260
+
261
+ <VideoPreview file={file} serverUrl={result?.video_url} />
262
+ </section>
263
+
264
+ <VideoComparison
265
+ file={file}
266
+ processedUrl={result?.processed_url}
267
+ isProcessing={status === 'processing_video'}
268
+ />
269
+ <ResultsPanel data={result} />
270
+ </>
271
+ );
272
+ }
273
+
274
+ ReactDOM.createRoot(document.getElementById('root')).render(<App />);
App/static/style.css ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ .preview-placeholder {
45
+ width: 100%; max-width: 460px; height: 280px; border-radius: 12px; background: #111122;
46
+ border: 1px solid #1e1e35; display: flex; align-items: center; justify-content: center; color: #333; font-size: 48px;
47
+ }
48
+ .spinner {
49
+ margin: 16px auto; width: 36px; height: 36px; border: 3px solid #1a1a30;
50
+ border-top: 3px solid #6c8cff; border-radius: 50%; animation: spin 0.7s linear infinite;
51
+ }
52
+ .processing-text { text-align: center; color: #666; font-size: 13px; margin-top: 8px; }
53
+ @keyframes spin { to { transform: rotate(360deg); } }
54
+ .error-box {
55
+ margin-top: 20px; padding: 16px; background: rgba(244,67,54,0.1); border: 1px solid rgba(244,67,54,0.3);
56
+ border-radius: 10px; color: #f44336; text-align: center; font-size: 14px;
57
+ }
58
+ .video-compare { max-width: 1100px; margin: 40px auto 0; padding: 0 40px; }
59
+ .video-compare h2 { font-size: 20px; font-weight: 700; color: #fff; margin-bottom: 16px; }
60
+ .compare-grid { display: flex; gap: 24px; }
61
+ .compare-item { flex: 1; text-align: center; }
62
+ .compare-item video {
63
+ width: 100%; max-height: 360px; border-radius: 12px; border: 1px solid #1e1e35; background: #111122;
64
+ }
65
+ .compare-label { margin-top: 8px; font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; }
66
+ .compare-label.original { color: #6c8cff; }
67
+ .compare-label.detected { color: #4caf50; }
68
+ .results-section { max-width: 1100px; margin: 50px auto 60px; padding: 0 40px; }
69
+ .results-panel { background: #111122; border: 1px solid #1e1e35; border-radius: 14px; padding: 30px 36px; }
70
+ .results-panel h2 { font-size: 20px; font-weight: 700; color: #fff; margin-bottom: 6px; }
71
+ .results-hint { font-size: 13px; color: #777; margin-bottom: 24px; line-height: 1.5; }
72
+ .overall-result { display: flex; align-items: center; gap: 20px; padding: 20px; border-radius: 12px; margin-bottom: 24px; }
73
+ .overall-result.real { background: rgba(76,175,80,0.08); border: 1px solid rgba(76,175,80,0.3); }
74
+ .overall-result.fake { background: rgba(244,67,54,0.08); border: 1px solid rgba(244,67,54,0.3); }
75
+ .overall-label { font-size: 32px; font-weight: 800; }
76
+ .overall-result.real .overall-label { color: #4caf50; }
77
+ .overall-result.fake .overall-label { color: #f44336; }
78
+ .overall-details { font-size: 14px; color: #aaa; line-height: 1.6; }
79
+ .face-row { display: flex; align-items: center; gap: 16px; padding: 14px 0; border-top: 1px solid #1a1a30; }
80
+ .face-thumb { width: 52px; height: 52px; border-radius: 8px; object-fit: cover; border: 2px solid #222; background: #1a1a2e; }
81
+ .face-info { flex: 1; }
82
+ .face-bar-track { height: 8px; background: #1a1a30; border-radius: 4px; overflow: hidden; margin-bottom: 6px; }
83
+ .face-bar-fill { height: 100%; border-radius: 4px; transition: width 0.6s ease; }
84
+ .bar-green { background: #4caf50; } .bar-orange { background: #ff9800; } .bar-red { background: #f44336; }
85
+ .face-score { font-size: 13px; font-weight: 600; }
86
+ .score-green { color: #4caf50; } .score-orange { color: #ff9800; } .score-red { color: #f44336; }
87
+ @media (max-width: 768px) {
88
+ .hero { flex-direction: column; padding: 0 20px; margin-top: 30px; gap: 30px; }
89
+ .hero-left { max-width: 100%; } .hero-title { font-size: 32px; }
90
+ .results-section { padding: 0 20px; } .results-panel { padding: 20px; }
91
+ .navbar { padding: 14px 20px; } .navbar nav a { margin-left: 16px; font-size: 13px; }
92
+ .compare-grid { flex-direction: column; }
93
+ }
App/templates/index.html ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>DFDetect - DeepFake Detector</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/app.jsx"></script>
16
+ </body>
17
+ </html>
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy
2
+ pandas
3
+ tensorflow
4
+ keras>=2.2.0
5
+ opencv-python>=4.1.0
6
+ mtcnn>=0.1.0
7
+ h5py
8
+ split_folders
9
+ flask
10
+ mediapipe
11
+ imageio-ffmpeg