Jennigwen commited on
Commit
c25c614
·
1 Parent(s): 4c8f8a7

Update : Git Ignore .env

Browse files
BackEnd/.gitignore CHANGED
@@ -1,4 +1,22 @@
 
 
 
 
1
  __pycache__/
2
- *.pkl
3
- # Apple Mac's Hidden Desktop Files
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  .DS_Store
 
1
+ # --- Security / Environments ---
2
+ .env
3
+
4
+ # --- Python / Flask ---
5
  __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+ venv/
9
+ env/
10
+ .venv/
11
+
12
+ # --- IDEs / Editors ---
13
+ .idea/
14
+ .vscode/
15
+
16
+ # --- Machine Learning Models ---
17
+ # (Abaikan kalau modelnya kecil dan memang mau disimpan di GitHub)
18
+ *.pth
19
+ *.onnx
20
+
21
+ # --- OS generated files ---
22
  .DS_Store
BackEnd/.idea/.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
4
+ # Editor-based HTTP Client requests
5
+ /httpRequests/
6
+ # Datasource local storage ignored files
7
+ /dataSources/
8
+ /dataSources.local.xml
BackEnd/.idea/BackEnd.iml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="Flask">
4
+ <option name="enabled" value="true" />
5
+ </component>
6
+ <component name="NewModuleRootManager">
7
+ <content url="file://$MODULE_DIR$">
8
+ <excludeFolder url="file://$MODULE_DIR$/.venv" />
9
+ </content>
10
+ <orderEntry type="jdk" jdkName="Python 3.13 (BackEnd)" jdkType="Python SDK" />
11
+ <orderEntry type="sourceFolder" forTests="false" />
12
+ </component>
13
+ <component name="PyDocumentationSettings">
14
+ <option name="format" value="PLAIN" />
15
+ <option name="myDocStringFormat" value="Plain" />
16
+ </component>
17
+ <component name="TemplatesService">
18
+ <option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
19
+ </component>
20
+ </module>
BackEnd/.idea/inspectionProfiles/profiles_settings.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
BackEnd/.idea/misc.xml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="Black">
4
+ <option name="sdkName" value="Python 3.13 (BackEnd)" />
5
+ </component>
6
+ <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (BackEnd)" project-jdk-type="Python SDK" />
7
+ </project>
BackEnd/.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/BackEnd.iml" filepath="$PROJECT_DIR$/.idea/BackEnd.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
BackEnd/.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="$PROJECT_DIR$/.." vcs="Git" />
5
+ </component>
6
+ </project>
BackEnd/IsItFake_API.postman_collection.json ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "info": {
3
+ "name": "IsItFake? Deepfake Detector API",
4
+ "description": "Koleksi API lengkap untuk aplikasi pendeteksi deepfake IsItFake?",
5
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
6
+ },
7
+ "variable": [
8
+ {
9
+ "key": "base_url",
10
+ "value": "http://127.0.0.1:5000",
11
+ "type": "string"
12
+ },
13
+ {
14
+ "key": "jwt_token",
15
+ "value": "PASTE_TOKEN_KAMU_DI_SINI_SETELAH_LOGIN",
16
+ "type": "string"
17
+ }
18
+ ],
19
+ "item": [
20
+ {
21
+ "name": "1. System",
22
+ "item": [
23
+ {
24
+ "name": "Test Database Connection",
25
+ "request": {
26
+ "method": "GET",
27
+ "header": [],
28
+ "url": {
29
+ "raw": "{{base_url}}/api/test-db",
30
+ "host": [
31
+ "{{base_url}}"
32
+ ],
33
+ "path": [
34
+ "api",
35
+ "test-db"
36
+ ]
37
+ }
38
+ },
39
+ "response": []
40
+ }
41
+ ]
42
+ },
43
+ {
44
+ "name": "2. Authentication",
45
+ "item": [
46
+ {
47
+ "name": "Register",
48
+ "request": {
49
+ "method": "POST",
50
+ "header": [
51
+ {
52
+ "key": "Content-Type",
53
+ "value": "application/json"
54
+ }
55
+ ],
56
+ "body": {
57
+ "mode": "raw",
58
+ "raw": "{\n \"email\": \"tester@mail.com\",\n \"username\": \"tester_ai\",\n \"password\": \"password123\",\n \"display_name\": \"Si Paling Tester\"\n}"
59
+ },
60
+ "url": {
61
+ "raw": "{{base_url}}/api/register",
62
+ "host": [
63
+ "{{base_url}}"
64
+ ],
65
+ "path": [
66
+ "api",
67
+ "register"
68
+ ]
69
+ }
70
+ },
71
+ "response": []
72
+ },
73
+ {
74
+ "name": "Login",
75
+ "request": {
76
+ "method": "POST",
77
+ "header": [
78
+ {
79
+ "key": "Content-Type",
80
+ "value": "application/json"
81
+ }
82
+ ],
83
+ "body": {
84
+ "mode": "raw",
85
+ "raw": "{\n \"email\": \"tester@mail.com\",\n \"password\": \"password123\"\n}"
86
+ },
87
+ "url": {
88
+ "raw": "{{base_url}}/api/login",
89
+ "host": [
90
+ "{{base_url}}"
91
+ ],
92
+ "path": [
93
+ "api",
94
+ "login"
95
+ ]
96
+ },
97
+ "description": "Setelah login berhasil, copy `access_token` dari response dan masukkan ke tab Variables di folder utama collection (jwt_token)."
98
+ },
99
+ "response": []
100
+ },
101
+ {
102
+ "name": "Logout",
103
+ "request": {
104
+ "auth": {
105
+ "type": "bearer",
106
+ "bearer": [
107
+ {
108
+ "key": "token",
109
+ "value": "{{jwt_token}}",
110
+ "type": "string"
111
+ }
112
+ ]
113
+ },
114
+ "method": "POST",
115
+ "header": [],
116
+ "url": {
117
+ "raw": "{{base_url}}/api/logout",
118
+ "host": [
119
+ "{{base_url}}"
120
+ ],
121
+ "path": [
122
+ "api",
123
+ "logout"
124
+ ]
125
+ }
126
+ },
127
+ "response": []
128
+ },
129
+ {
130
+ "name": "Get Profile",
131
+ "request": {
132
+ "auth": {
133
+ "type": "bearer",
134
+ "bearer": [
135
+ {
136
+ "key": "token",
137
+ "value": "{{jwt_token}}",
138
+ "type": "string"
139
+ }
140
+ ]
141
+ },
142
+ "method": "GET",
143
+ "header": [],
144
+ "url": {
145
+ "raw": "{{base_url}}/api/profile",
146
+ "host": [
147
+ "{{base_url}}"
148
+ ],
149
+ "path": [
150
+ "api",
151
+ "profile"
152
+ ]
153
+ }
154
+ },
155
+ "response": []
156
+ }
157
+ ]
158
+ },
159
+ {
160
+ "name": "3. Analysis",
161
+ "item": [
162
+ {
163
+ "name": "Scan Image (Deepfake Detection)",
164
+ "request": {
165
+ "auth": {
166
+ "type": "bearer",
167
+ "bearer": [
168
+ {
169
+ "key": "token",
170
+ "value": "{{jwt_token}}",
171
+ "type": "string"
172
+ }
173
+ ]
174
+ },
175
+ "method": "POST",
176
+ "header": [],
177
+ "body": {
178
+ "mode": "formdata",
179
+ "formdata": [
180
+ {
181
+ "key": "file",
182
+ "type": "file",
183
+ "src": []
184
+ }
185
+ ]
186
+ },
187
+ "url": {
188
+ "raw": "{{base_url}}/api/scan",
189
+ "host": [
190
+ "{{base_url}}"
191
+ ],
192
+ "path": [
193
+ "api",
194
+ "scan"
195
+ ]
196
+ },
197
+ "description": "Ingat: Jika kamu tidak ingin menyimpan riwayat (mode Guest), matikan centang 'Authorization' di tab Auth."
198
+ },
199
+ "response": []
200
+ }
201
+ ]
202
+ },
203
+ {
204
+ "name": "4. Statistics",
205
+ "item": [
206
+ {
207
+ "name": "Get Dashboard Summary",
208
+ "request": {
209
+ "auth": {
210
+ "type": "bearer",
211
+ "bearer": [
212
+ {
213
+ "key": "token",
214
+ "value": "{{jwt_token}}",
215
+ "type": "string"
216
+ }
217
+ ]
218
+ },
219
+ "method": "GET",
220
+ "header": [],
221
+ "url": {
222
+ "raw": "{{base_url}}/api/statistics/summary",
223
+ "host": [
224
+ "{{base_url}}"
225
+ ],
226
+ "path": [
227
+ "api",
228
+ "statistics",
229
+ "summary"
230
+ ]
231
+ }
232
+ },
233
+ "response": []
234
+ },
235
+ {
236
+ "name": "Get Scan History (Pagination)",
237
+ "request": {
238
+ "auth": {
239
+ "type": "bearer",
240
+ "bearer": [
241
+ {
242
+ "key": "token",
243
+ "value": "{{jwt_token}}",
244
+ "type": "string"
245
+ }
246
+ ]
247
+ },
248
+ "method": "GET",
249
+ "header": [],
250
+ "url": {
251
+ "raw": "{{base_url}}/api/statistics/history?page=1&limit=5",
252
+ "host": [
253
+ "{{base_url}}"
254
+ ],
255
+ "path": [
256
+ "api",
257
+ "statistics",
258
+ "history"
259
+ ],
260
+ "query": [
261
+ {
262
+ "key": "page",
263
+ "value": "1"
264
+ },
265
+ {
266
+ "key": "limit",
267
+ "value": "5"
268
+ }
269
+ ]
270
+ }
271
+ },
272
+ "response": []
273
+ }
274
+ ]
275
+ }
276
+ ]
277
+ }
BackEnd/app.py CHANGED
@@ -1,152 +1,152 @@
1
- from flask import Flask, request, jsonify
2
- from flask_cors import CORS
3
- import numpy as np
4
- import cv2
5
- import onnxruntime as ort
6
-
7
- app = Flask(__name__)
8
- CORS(app)
9
-
10
- # ============================================================
11
- # LOAD ONNX MODEL
12
- # ============================================================
13
- MODEL_PATH = "best_model.onnx"
14
- session = ort.InferenceSession(MODEL_PATH, providers=['CPUExecutionProvider'])
15
- input_name = session.get_inputs()[0].name
16
- print(f"ONNX model loaded: {MODEL_PATH}")
17
-
18
- # ============================================================
19
- # FACE CROPPER
20
- # ============================================================
21
- face_cascade = cv2.CascadeClassifier(
22
- cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
23
- )
24
-
25
- def crop_face(image_bgr):
26
- """Finds the largest face in the image and crops it with 10% padding"""
27
- gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
28
- faces = face_cascade.detectMultiScale(
29
- gray, scaleFactor=1.1, minNeighbors=8, minSize=(80, 80)
30
- )
31
- if len(faces) > 0:
32
- x, y, w, h = max(faces, key=lambda f: f[2] * f[3])
33
- pad = int(0.10 * min(w, h))
34
- x1 = max(0, x - pad)
35
- y1 = max(0, y - pad)
36
- x2 = min(image_bgr.shape[1], x + w + pad)
37
- y2 = min(image_bgr.shape[0], y + h + pad)
38
- return image_bgr[y1:y2, x1:x2]
39
- return image_bgr
40
-
41
-
42
- # ============================================================
43
- # PREPROCESSING (exact copy from training notebook Cell 27 + 37)
44
- # ============================================================
45
- def make_fft_channel(image_bgr, size=224):
46
- """
47
- Exact copy of training notebook's make_fft_channel.
48
- Power spectrum: log1p(|F|^2), normalized to [0,1].
49
- """
50
- gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
51
- gray = cv2.resize(gray, (size, size)).astype(np.float32)
52
- f = np.fft.fft2(gray)
53
- f_shift = np.fft.fftshift(f)
54
- ps = np.log1p(np.abs(f_shift) ** 2)
55
- ps = (ps - ps.min()) / (ps.max() - ps.min() + 1e-8)
56
- return ps
57
-
58
-
59
- def preprocess_image(face_bgr):
60
- """
61
- Matches training notebook's predict_image() (Cell 37):
62
- 1. RGB 224x224 -> ImageNet normalize -> numpy array
63
- 2. FFT power spectrum channel
64
- 3. Concatenate to 4 channels
65
- """
66
- # --- RGB channels ---
67
- face_rgb = cv2.cvtColor(face_bgr, cv2.COLOR_BGR2RGB)
68
- img_input = cv2.resize(face_rgb, (224, 224)).astype(np.float32) / 255.0
69
-
70
- mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
71
- std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
72
- img_normalized = (img_input - mean) / std
73
-
74
- # (224,224,3) -> (3,224,224)
75
- img_chw = np.transpose(img_normalized, (2, 0, 1))
76
-
77
- # --- FFT channel ---
78
- fft_ch = make_fft_channel(face_bgr, size=224)
79
- fft_ch = np.expand_dims(fft_ch, axis=0) # (1, 224, 224)
80
-
81
- # --- Combine: (4, 224, 224) -> (1, 4, 224, 224) ---
82
- combined = np.concatenate([img_chw, fft_ch], axis=0)
83
- combined = np.expand_dims(combined, axis=0).astype(np.float32)
84
- return combined
85
-
86
-
87
- # ============================================================
88
- # ROUTES
89
- # ============================================================
90
- @app.route('/', methods=['GET'])
91
- def health_check():
92
- return jsonify({
93
- "status": "online",
94
- "message": "SynthScan Neural Engine is awake and ready!",
95
- "version": "2.0 (ONNX)"
96
- }), 200
97
-
98
-
99
- @app.route('/api/scan', methods=['POST'])
100
- def scan_image():
101
- if 'file' not in request.files:
102
- return jsonify({"status": "error", "message": "No file uploaded"}), 400
103
-
104
- file = request.files['file']
105
- file_bytes = np.frombuffer(file.read(), np.uint8)
106
- face_bgr = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
107
-
108
- if face_bgr is None:
109
- return jsonify({"status": "error", "message": "Invalid image format"}), 400
110
-
111
- try:
112
- # 1. Crop the largest face
113
- cropped_face = crop_face(face_bgr)
114
-
115
- # 2. Preprocess (matching training notebook exactly)
116
- input_array = preprocess_image(cropped_face)
117
-
118
- # 3. Inference
119
- logit = float(session.run(None, {input_name: input_array})[0][0])
120
- prob_fake = 1.0 / (1.0 + np.exp(-max(-50, min(50, logit))))
121
-
122
- prob_real = 1.0 - prob_fake
123
- fake_percent = round(prob_fake * 100, 1)
124
- real_percent = round(prob_real * 100, 1)
125
-
126
- # Training notebook: prob >= 0.5 = FAKE, prob < 0.5 = REAL
127
- if prob_fake >= 0.5:
128
- final_result = "Deepfake"
129
- confidence = fake_percent
130
- else:
131
- final_result = "Real"
132
- confidence = real_percent
133
-
134
- print(f"Logit: {logit:.4f} | P(fake): {fake_percent}% | Result: {final_result} ({confidence}%)")
135
-
136
- return jsonify({
137
- "status": "success",
138
- "result": final_result,
139
- "probability": confidence,
140
- "probability_fake": round(prob_fake * 100, 2),
141
- "probability_real": round(prob_real * 100, 2)
142
- })
143
-
144
- except Exception as e:
145
- print("ERROR:", str(e))
146
- import traceback
147
- traceback.print_exc()
148
- return jsonify({"status": "error", "message": str(e)}), 500
149
-
150
-
151
- if __name__ == '__main__':
152
- app.run(debug=True, port=5001)
 
1
+ # from flask import Flask, request, jsonify
2
+ # from flask_cors import CORS
3
+ # import numpy as np
4
+ # import cv2
5
+ # import onnxruntime as ort
6
+ #
7
+ # app = Flask(__name__)
8
+ # CORS(app)
9
+ #
10
+ # # ============================================================
11
+ # # LOAD ONNX MODEL
12
+ # # ============================================================
13
+ # MODEL_PATH = "ml_models/best_model.onnx"
14
+ # session = ort.InferenceSession(MODEL_PATH, providers=['CPUExecutionProvider'])
15
+ # input_name = session.get_inputs()[0].name
16
+ # print(f"ONNX model loaded: {MODEL_PATH}")
17
+ #
18
+ # # ============================================================
19
+ # # FACE CROPPER
20
+ # # ============================================================
21
+ # face_cascade = cv2.CascadeClassifier(
22
+ # cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
23
+ # )
24
+ #
25
+ # def crop_face(image_bgr):
26
+ # """Finds the largest face in the image and crops it with 10% padding"""
27
+ # gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
28
+ # faces = face_cascade.detectMultiScale(
29
+ # gray, scaleFactor=1.1, minNeighbors=8, minSize=(80, 80)
30
+ # )
31
+ # if len(faces) > 0:
32
+ # x, y, w, h = max(faces, key=lambda f: f[2] * f[3])
33
+ # pad = int(0.10 * min(w, h))
34
+ # x1 = max(0, x - pad)
35
+ # y1 = max(0, y - pad)
36
+ # x2 = min(image_bgr.shape[1], x + w + pad)
37
+ # y2 = min(image_bgr.shape[0], y + h + pad)
38
+ # return image_bgr[y1:y2, x1:x2]
39
+ # return image_bgr
40
+ #
41
+ #
42
+ # # ============================================================
43
+ # # PREPROCESSING (exact copy from training notebook Cell 27 + 37)
44
+ # # ============================================================
45
+ # def make_fft_channel(image_bgr, size=224):
46
+ # """
47
+ # Exact copy of training notebook's make_fft_channel.
48
+ # Power spectrum: log1p(|F|^2), normalized to [0,1].
49
+ # """
50
+ # gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
51
+ # gray = cv2.resize(gray, (size, size)).astype(np.float32)
52
+ # f = np.fft.fft2(gray)
53
+ # f_shift = np.fft.fftshift(f)
54
+ # ps = np.log1p(np.abs(f_shift) ** 2)
55
+ # ps = (ps - ps.min()) / (ps.max() - ps.min() + 1e-8)
56
+ # return ps
57
+ #
58
+ #
59
+ # def preprocess_image(face_bgr):
60
+ # """
61
+ # Matches training notebook's predict_image() (Cell 37):
62
+ # 1. RGB 224x224 -> ImageNet normalize -> numpy array
63
+ # 2. FFT power spectrum channel
64
+ # 3. Concatenate to 4 channels
65
+ # """
66
+ # # --- RGB channels ---
67
+ # face_rgb = cv2.cvtColor(face_bgr, cv2.COLOR_BGR2RGB)
68
+ # img_input = cv2.resize(face_rgb, (224, 224)).astype(np.float32) / 255.0
69
+ #
70
+ # mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
71
+ # std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
72
+ # img_normalized = (img_input - mean) / std
73
+ #
74
+ # # (224,224,3) -> (3,224,224)
75
+ # img_chw = np.transpose(img_normalized, (2, 0, 1))
76
+ #
77
+ # # --- FFT channel ---
78
+ # fft_ch = make_fft_channel(face_bgr, size=224)
79
+ # fft_ch = np.expand_dims(fft_ch, axis=0) # (1, 224, 224)
80
+ #
81
+ # # --- Combine: (4, 224, 224) -> (1, 4, 224, 224) ---
82
+ # combined = np.concatenate([img_chw, fft_ch], axis=0)
83
+ # combined = np.expand_dims(combined, axis=0).astype(np.float32)
84
+ # return combined
85
+ #
86
+ #
87
+ # # ============================================================
88
+ # # ROUTES
89
+ # # ============================================================
90
+ # @app.route('/', methods=['GET'])
91
+ # def health_check():
92
+ # return jsonify({
93
+ # "status": "online",
94
+ # "message": "SynthScan Neural Engine is awake and ready!",
95
+ # "version": "2.0 (ONNX)"
96
+ # }), 200
97
+ #
98
+ #
99
+ # @app.route('/api/scan', methods=['POST'])
100
+ # def scan_image():
101
+ # if 'file' not in request.files:
102
+ # return jsonify({"status": "error", "message": "No file uploaded"}), 400
103
+ #
104
+ # file = request.files['file']
105
+ # file_bytes = np.frombuffer(file.read(), np.uint8)
106
+ # face_bgr = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
107
+ #
108
+ # if face_bgr is None:
109
+ # return jsonify({"status": "error", "message": "Invalid image format"}), 400
110
+ #
111
+ # try:
112
+ # # 1. Crop the largest face
113
+ # cropped_face = crop_face(face_bgr)
114
+ #
115
+ # # 2. Preprocess (matching training notebook exactly)
116
+ # input_array = preprocess_image(cropped_face)
117
+ #
118
+ # # 3. Inference
119
+ # logit = float(session.run(None, {input_name: input_array})[0][0])
120
+ # prob_fake = 1.0 / (1.0 + np.exp(-max(-50, min(50, logit))))
121
+ #
122
+ # prob_real = 1.0 - prob_fake
123
+ # fake_percent = round(prob_fake * 100, 1)
124
+ # real_percent = round(prob_real * 100, 1)
125
+ #
126
+ # # Training notebook: prob >= 0.5 = FAKE, prob < 0.5 = REAL
127
+ # if prob_fake >= 0.5:
128
+ # final_result = "Deepfake"
129
+ # confidence = fake_percent
130
+ # else:
131
+ # final_result = "Real"
132
+ # confidence = real_percent
133
+ #
134
+ # print(f"Logit: {logit:.4f} | P(fake): {fake_percent}% | Result: {final_result} ({confidence}%)")
135
+ #
136
+ # return jsonify({
137
+ # "status": "success",
138
+ # "result": final_result,
139
+ # "probability": confidence,
140
+ # "probability_fake": round(prob_fake * 100, 2),
141
+ # "probability_real": round(prob_real * 100, 2)
142
+ # })
143
+ #
144
+ # except Exception as e:
145
+ # print("ERROR:", str(e))
146
+ # import traceback
147
+ # traceback.print_exc()
148
+ # return jsonify({"status": "error", "message": str(e)}), 500
149
+ #
150
+ #
151
+ # if __name__ == '__main__':
152
+ # app.run(debug=True, port=5001)
BackEnd/app/__init__.py ADDED
File without changes
BackEnd/controllers/analysis_controllers.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from flask import request
3
+ import numpy as np
4
+ import traceback
5
+
6
+ from core.response_json import error_response, success_response, server_error_response
7
+ from middlewares.auth import token_optional
8
+ from services.analysis_service import run_deepfake_analysis, save_scan_history
9
+
10
+
11
+ @token_optional
12
+ def scan_image(current_user):
13
+ if 'file' not in request.files:
14
+ return error_response(message="No file uploaded", status_code=400)
15
+
16
+ file = request.files['file']
17
+
18
+ if file.filename == '':
19
+ return error_response(message="No selected file", status_code=400)
20
+
21
+ try:
22
+ file_bytes = np.frombuffer(file.read(), np.uint8)
23
+
24
+ analysis_result = run_deepfake_analysis(file_bytes)
25
+
26
+ if current_user is not None:
27
+ save_scan_history(
28
+ user_id=current_user.get("user_id"),
29
+ file_name=file.filename,
30
+ result=analysis_result["result"],
31
+ confidence_score=analysis_result["probability"],
32
+ processing_time=analysis_result["processing_time"]
33
+ )
34
+ analysis_result["saved_to_history"] = True
35
+ else:
36
+ analysis_result["saved_to_history"] = False
37
+
38
+ return success_response(
39
+ message="Analysis complete",
40
+ data=analysis_result
41
+ )
42
+
43
+ except ValueError as ve:
44
+ return error_response(message=str(ve), status_code=400)
45
+ except Exception as e:
46
+ print("ERROR:", str(e))
47
+ traceback.print_exc()
48
+ return server_error_response(error_details=str(e))
BackEnd/controllers/auth_controllers.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # File: BackEnd/app/controllers/auth_controllers.py
2
+
3
+ from flask import request
4
+
5
+ from core.response_json import error_response, success_response, server_error_response
6
+ from middlewares.auth import token_required
7
+ from services.auth_service import register_new_user, authenticate_user
8
+
9
+
10
+ def register():
11
+ try:
12
+ data = request.get_json()
13
+
14
+ if not data or not data.get("email") or not data.get("username") or not data.get("password"):
15
+ return error_response("Email, username, dan password wajib diisi!", status_code=400)
16
+
17
+ user_data = register_new_user(
18
+ email=data.get("email"),
19
+ username=data.get("username"),
20
+ password=data.get("password"),
21
+ display_name=data.get("display_name")
22
+ )
23
+
24
+ return success_response(message="Registrasi berhasil!", data=user_data, status_code=201)
25
+
26
+ except ValueError as ve:
27
+ return error_response(message=str(ve), status_code=409)
28
+ except Exception as e:
29
+ return server_error_response(error_details=str(e))
30
+
31
+
32
+ def login():
33
+ try:
34
+ data = request.get_json()
35
+
36
+ if not data or not data.get("email") or not data.get("password"):
37
+ return error_response("Email dan password wajib diisi!", status_code=400)
38
+
39
+ auth_data = authenticate_user(
40
+ email=data.get("email"),
41
+ password=data.get("password")
42
+ )
43
+
44
+ return success_response(message="Login berhasil!", data=auth_data, status_code=200)
45
+
46
+ except ValueError as ve:
47
+ return error_response(message=str(ve), status_code=401)
48
+ except Exception as e:
49
+ return server_error_response(error_details=str(e))
50
+
51
+
52
+ @token_required
53
+ def logout(current_user):
54
+ try:
55
+ email_user = current_user.get('email')
56
+
57
+ return success_response(
58
+ message=f"Logout berhasil untuk {email_user}. Silakan hapus token di sisi Frontend.",
59
+ status_code=200
60
+ )
61
+ except Exception as e:
62
+ return server_error_response(error_details=str(e))
BackEnd/controllers/daily_statistic_controllers.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # File: BackEnd/app/controllers/daily_statistic_controllers.py
2
+
3
+ from flask import request
4
+
5
+ from core.response_json import success_response, server_error_response
6
+ from middlewares.auth import token_required
7
+ from services.statistic_service import get_dashboard_stats, get_paginated_scans
8
+
9
+
10
+ @token_required
11
+ def get_stats(current_user):
12
+ try:
13
+ user_id = current_user.get('user_id')
14
+ stats = get_dashboard_stats(user_id)
15
+ return success_response(message="Statistik berhasil diambil", data=stats)
16
+ except Exception as e:
17
+ return server_error_response(error_details=str(e))
18
+
19
+
20
+ @token_required
21
+ def show_all_data(current_user):
22
+ try:
23
+ user_id = current_user.get('user_id')
24
+
25
+ page = int(request.args.get('page', 1))
26
+ limit = int(request.args.get('limit', 10))
27
+
28
+ data = get_paginated_scans(user_id, page, limit)
29
+ return success_response(message="Riwayat scan berhasil diambil", data=data)
30
+ except Exception as e:
31
+ return server_error_response(error_details=str(e))
BackEnd/controllers/test_controllers.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from core.db_connector import get_db
2
+ from core.response_json import success_response, server_error_response
3
+
4
+
5
+ def test_connection():
6
+ try:
7
+ db = get_db()
8
+ response = db.table("users").select("id").limit(1).execute()
9
+ return success_response(
10
+ message="Database connection successful!",
11
+ data={
12
+ "connection": "OK",
13
+ "test_query_data": response.data # Akan mengembalikan [] jika tabel masih kosong
14
+ },
15
+ status_code=200
16
+ )
17
+
18
+ except Exception as e:
19
+ return server_error_response(error_details=str(e))
BackEnd/core/db_connector.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from supabase import create_client, Client
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
8
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY")
9
+
10
+ if not SUPABASE_URL or not SUPABASE_KEY:
11
+ raise ValueError("Missing Supabase URL or Key. Check your .env file.")
12
+
13
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
14
+
15
+ def get_db():
16
+ return supabase
BackEnd/core/response_json.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from flask import jsonify
3
+
4
+
5
+ def success_response(message="Success", data=None, status_code=200):
6
+ response = {
7
+ "status": "success",
8
+ "message": message
9
+ }
10
+ if data is not None:
11
+ response["data"] = data
12
+
13
+ return jsonify(response), status_code
14
+
15
+
16
+ def error_response(message="An error occurred", error_details=None, status_code=400):
17
+ response = {
18
+ "status": "error",
19
+ "message": message
20
+ }
21
+ if error_details is not None:
22
+ response["details"] = error_details
23
+
24
+ return jsonify(response), status_code
25
+
26
+
27
+ def server_error_response(error_details=None):
28
+ response = {
29
+ "status": "fail",
30
+ "message": "Internal server error"
31
+ }
32
+ if error_details is not None:
33
+ response["details"] = str(error_details)
34
+
35
+ return jsonify(response), 500
BackEnd/logo-full.png ADDED

Git LFS Details

  • SHA256: 9db7e41cef0f8bc952428da71acf7e2cbd3bb9d017e39fcc357975a638b94e94
  • Pointer size: 131 Bytes
  • Size of remote file: 216 kB
BackEnd/middlewares/auth.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # File: BackEnd/app/middlewares/auth.py
2
+
3
+ from functools import wraps
4
+ from flask import request
5
+ import jwt
6
+ import os
7
+
8
+ from core.response_json import error_response
9
+
10
+
11
+ def token_required(f):
12
+ @wraps(f)
13
+ def decorated(*args, **kwargs):
14
+ token = None
15
+
16
+ if "Authorization" in request.headers:
17
+ auth_header = request.headers["Authorization"]
18
+ if auth_header.startswith("Bearer "):
19
+ token = auth_header.split(" ")[1]
20
+
21
+ if not token:
22
+ return error_response("Token akses tidak ditemukan! Silakan login.", status_code=401)
23
+
24
+ try:
25
+ secret_key = os.getenv("JWT_SECRET")
26
+ decoded_data = jwt.decode(token, secret_key, algorithms=["HS256"])
27
+
28
+ current_user = decoded_data
29
+
30
+ except jwt.ExpiredSignatureError:
31
+ return error_response("Token sudah kedaluwarsa! Silakan login ulang.", status_code=401)
32
+ except jwt.InvalidTokenError:
33
+ return error_response("Token tidak valid!", status_code=401)
34
+
35
+ return f(current_user, *args, **kwargs)
36
+
37
+ return decorated
38
+
39
+
40
+
41
+ def token_optional(f):
42
+ @wraps(f)
43
+ def decorated(*args, **kwargs):
44
+ current_user = None
45
+
46
+ if "Authorization" in request.headers:
47
+ auth_header = request.headers["Authorization"]
48
+ if auth_header.startswith("Bearer "):
49
+ token = auth_header.split(" ")[1]
50
+ try:
51
+ secret_key = os.getenv("JWT_SECRET")
52
+ # Jika berhasil decode, masukkan ke current_user
53
+ current_user = jwt.decode(token, secret_key, algorithms=["HS256"])
54
+ except Exception:
55
+ # Abaikan error (expired/invalid), tetap izinkan masuk sebagai Guest
56
+ pass
57
+
58
+ return f(current_user, *args, **kwargs)
59
+
60
+ return decorated
BackEnd/{best_model.onnx → ml_models/best_model.onnx} RENAMED
File without changes
BackEnd/{best_model.pth → ml_models/best_model.pth} RENAMED
File without changes
BackEnd/models/scan_history.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+
5
+ @dataclass
6
+ class ScanHistory:
7
+ user_id: str
8
+ file_name: str
9
+ url_file: str
10
+ result: str
11
+ confidence_score: float
12
+ processing_time: float
13
+ id: Optional[str] = None
14
+ created_at: Optional[str] = None
15
+
16
+ @staticmethod
17
+ def from_dict(data: dict) -> 'ScanHistory':
18
+ if not data:
19
+ return None
20
+
21
+ return ScanHistory(
22
+ id=data.get("id"),
23
+ user_id=data.get("user_id"),
24
+ file_name=data.get("file_name"),
25
+ url_file=data.get("url_file"),
26
+ result=data.get("result"),
27
+ confidence_score=float(data.get("confidence_score", 0.0)),
28
+ processing_time=float(data.get("processing_time", 0.0)),
29
+ created_at=data.get("created_at")
30
+ )
BackEnd/models/user.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+
6
+ @dataclass
7
+ class User:
8
+ email: str
9
+ username: str
10
+ password_hash: str
11
+ display_name: Optional[str] = None
12
+ role: str = "user"
13
+ id: Optional[str] = None
14
+ created_at: Optional[str] = None
15
+
16
+ @staticmethod
17
+ def from_dict(data: dict) -> 'User':
18
+ if not data:
19
+ return None
20
+
21
+ return User(
22
+ id=data.get("id"),
23
+ email=data.get("email"),
24
+ username=data.get("username"),
25
+ password_hash=data.get("password_hash"),
26
+ display_name=data.get("display_name"),
27
+ role=data.get("role", "user"),
28
+ created_at=data.get("created_at")
29
+ )
30
+
31
+ def to_dict(self) -> dict:
32
+ return {
33
+ "id": self.id,
34
+ "username": self.username,
35
+ "email": self.email,
36
+ "display_name": self.display_name,
37
+ "role": self.role,
38
+ "created_at": self.created_at
39
+ }
BackEnd/noted.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+
2
+ main file nya aku rubah sebelumnya app.py sekarang run.py
BackEnd/requirements.txt CHANGED
@@ -2,5 +2,70 @@ Flask==3.0.0
2
  Flask-Cors==4.0.0
3
  numpy==1.26.4
4
  opencv-python-headless==4.9.0.80
5
- onnxruntime==1.18.0
6
- Werkzeug==3.0.1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  Flask-Cors==4.0.0
3
  numpy==1.26.4
4
  opencv-python-headless==4.9.0.80
5
+ onnxruntime~=1.24.4
6
+ Werkzeug~=3.1.8
7
+ supabase~=2.28.3
8
+ dotenv~=0.9.9
9
+ python-dotenv~=1.2.2
10
+ hpack~=4.1.0
11
+ hyperframe~=6.1.0
12
+ h11~=0.16.0
13
+ cryptography~=46.0.7
14
+ pip~=25.0.1
15
+ typing_extensions~=4.15.0
16
+ cffi~=2.0.0
17
+ rich~=14.3.3
18
+ Pygments~=2.20.0
19
+ markdown-it-py~=4.0.0
20
+ idna~=3.11
21
+ multidict~=6.7.1
22
+ propcache~=0.4.1
23
+ pydantic~=2.12.5
24
+ pydantic_core~=2.41.5
25
+ click~=8.3.2
26
+ httpcore~=1.0.9
27
+ httpx~=0.28.1
28
+ zstandard~=0.25.0
29
+ mdurl~=0.1.2
30
+ charset-normalizer~=3.4.7
31
+ fsspec~=2026.3.0
32
+ requests~=2.33.1
33
+ yarl~=1.23.0
34
+ certifi~=2026.2.25
35
+ h2~=4.3.0
36
+ urllib3~=2.6.3
37
+ six~=1.17.0
38
+ python-dateutil~=2.9.0.post0
39
+ anyio~=4.13.0
40
+ annotated-types~=0.7.0
41
+ realtime~=2.28.3
42
+ websockets~=15.0.1
43
+ storage3~=2.28.3
44
+ pyiceberg~=0.11.1
45
+ deprecation~=2.1.0
46
+ postgrest~=2.28.3
47
+ tenacity~=9.1.4
48
+ pyroaring~=1.0.4
49
+ mmh3~=5.2.1
50
+ strictyaml~=1.7.3
51
+ pyparsing~=3.3.2
52
+ cachetools~=6.2.6
53
+ Jinja2~=3.1.6
54
+ packaging~=26.0
55
+ PyJWT~=2.12.1
56
+ typing_extensions~=4.15.0
57
+ protobuf~=7.34.1
58
+ pydantic_core~=2.41.5
59
+ Cython~=3.2.4
60
+ sympy~=1.14.0
61
+ mpmath~=1.3.0
62
+ pyglet~=2.1.14
63
+ decorator~=5.2.1
64
+ pillow~=12.2.0
65
+ flatbuffers~=25.12.19
66
+ scipy~=1.17.1
67
+ tqdm~=4.67.3
68
+ manim~=0.20.1
69
+ typing_extensions~=4.15.0
70
+ pydantic_core~=2.41.5
71
+ gTTS~=2.5.4
BackEnd/run.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # File: BackEnd/run.py
2
+
3
+ from flask import Flask
4
+ from flask_cors import CORS
5
+
6
+ from controllers.analysis_controllers import scan_image
7
+ from controllers.auth_controllers import register, login, logout
8
+ from controllers.daily_statistic_controllers import get_stats, show_all_data
9
+ from controllers.test_controllers import test_connection
10
+ from core.response_json import success_response
11
+ from middlewares.auth import token_required
12
+
13
+ app = Flask(__name__)
14
+ CORS(app)
15
+
16
+ # --- PUBLIC ROUTES ---
17
+ app.add_url_rule('/api/test-db', view_func=test_connection, methods=['GET'])
18
+ app.add_url_rule('/api/register', view_func=register, methods=['POST'])
19
+ app.add_url_rule('/api/login', view_func=login, methods=['POST'])
20
+ app.add_url_rule('/api/logout', view_func=logout, methods=['POST'])
21
+
22
+ app.add_url_rule('/api/statistics/summary', view_func=get_stats, methods=['GET'])
23
+ app.add_url_rule('/api/statistics/history', view_func=show_all_data, methods=['GET'])
24
+ app.add_url_rule('/api/scan', view_func=scan_image, methods=['POST'])
25
+
26
+ @app.route('/api/profile', methods=['GET'])
27
+ @token_required
28
+ def get_profile(current_user):
29
+ return success_response(
30
+ message="Selamat datang di area privat!",
31
+ data={"user_aktif": current_user}
32
+ )
33
+
34
+ if __name__ == '__main__':
35
+ app.run(debug=True, port=5000)
BackEnd/services/analysis_service.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # File: BackEnd/app/services/analysis_service.py
2
+ import os
3
+
4
+ import cv2
5
+ import numpy as np
6
+ import onnxruntime as ort
7
+ import time
8
+
9
+ from core.db_connector import get_db
10
+
11
+ # ============================================================
12
+ # LOAD ONNX MODEL
13
+ # ============================================================
14
+
15
+ # Gunakan base directory agar path selalu benar dimanapun server dijalankan
16
+ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17
+ MODEL_PATH = os.path.join(BASE_DIR, "ml_models", "best_model.onnx")
18
+
19
+ # Inisialisasi session sebagai None dulu agar tidak NameError
20
+ session = None
21
+ input_name = None
22
+ try:
23
+ if not os.path.exists(MODEL_PATH):
24
+ print(f"❌ File model tidak ditemukan di: {MODEL_PATH}")
25
+ else:
26
+ session = ort.InferenceSession(MODEL_PATH, providers=['CPUExecutionProvider'])
27
+ input_name = session.get_inputs()[0].name
28
+ print(f"✅ ONNX model loaded successfully from: {MODEL_PATH}")
29
+ except Exception as e:
30
+ print(f"❌ Gagal memuat ONNX model: {e}")
31
+
32
+
33
+ # ============================================================
34
+ # FACE CROPPER
35
+ # ============================================================
36
+ face_cascade = cv2.CascadeClassifier(
37
+ cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
38
+ )
39
+
40
+ def crop_face(image_bgr):
41
+ """Finds the largest face in the image and crops it with 10% padding"""
42
+ gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
43
+ faces = face_cascade.detectMultiScale(
44
+ gray, scaleFactor=1.1, minNeighbors=8, minSize=(80, 80)
45
+ )
46
+ if len(faces) > 0:
47
+ x, y, w, h = max(faces, key=lambda f: f[2] * f[3])
48
+ pad = int(0.10 * min(w, h))
49
+ x1 = max(0, x - pad)
50
+ y1 = max(0, y - pad)
51
+ x2 = min(image_bgr.shape[1], x + w + pad)
52
+ y2 = min(image_bgr.shape[0], y + h + pad)
53
+ return image_bgr[y1:y2, x1:x2]
54
+ return image_bgr
55
+
56
+ # ============================================================
57
+ # PREPROCESSING (exact copy from training notebook Cell 27 + 37)
58
+ # ============================================================
59
+ def make_fft_channel(image_bgr, size=224):
60
+ """
61
+ Exact copy of training notebook's make_fft_channel.
62
+ Power spectrum: log1p(|F|^2), normalized to [0,1].
63
+ """
64
+ gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
65
+ gray = cv2.resize(gray, (size, size)).astype(np.float32)
66
+ f = np.fft.fft2(gray)
67
+ f_shift = np.fft.fftshift(f)
68
+ ps = np.log1p(np.abs(f_shift) ** 2)
69
+ ps = (ps - ps.min()) / (ps.max() - ps.min() + 1e-8)
70
+ return ps
71
+
72
+ def preprocess_image(face_bgr):
73
+ """
74
+ Matches training notebook's predict_image() (Cell 37):
75
+ 1. RGB 224x224 -> ImageNet normalize -> numpy array
76
+ 2. FFT power spectrum channel
77
+ 3. Concatenate to 4 channels
78
+ """
79
+ # --- RGB channels ---
80
+ face_rgb = cv2.cvtColor(face_bgr, cv2.COLOR_BGR2RGB)
81
+ img_input = cv2.resize(face_rgb, (224, 224)).astype(np.float32) / 255.0
82
+
83
+ mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
84
+ std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
85
+ img_normalized = (img_input - mean) / std
86
+
87
+ # (224,224,3) -> (3,224,224)
88
+ img_chw = np.transpose(img_normalized, (2, 0, 1))
89
+
90
+ # --- FFT channel ---
91
+ fft_ch = make_fft_channel(face_bgr, size=224)
92
+ fft_ch = np.expand_dims(fft_ch, axis=0) # (1, 224, 224)
93
+
94
+ # --- Combine: (4, 224, 224) -> (1, 4, 224, 224) ---
95
+ combined = np.concatenate([img_chw, fft_ch], axis=0)
96
+ combined = np.expand_dims(combined, axis=0).astype(np.float32)
97
+ return combined
98
+
99
+ # ============================================================
100
+ # MAIN INFERENCE SERVICE
101
+ # ============================================================
102
+ def run_deepfake_analysis(file_bytes):
103
+ start_time = time.time()
104
+
105
+ face_bgr = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
106
+ if face_bgr is None:
107
+ raise ValueError("Invalid image format")
108
+
109
+ # 1. Crop the largest face
110
+ cropped_face = crop_face(face_bgr)
111
+
112
+ # 2. Preprocess (matching training notebook exactly)
113
+ input_array = preprocess_image(cropped_face)
114
+
115
+ # 3. Inference
116
+ logit = float(session.run(None, {input_name: input_array})[0][0])
117
+ prob_fake = 1.0 / (1.0 + np.exp(-max(-50, min(50, logit))))
118
+
119
+ prob_real = 1.0 - prob_fake
120
+ fake_percent = round(prob_fake * 100, 1)
121
+ real_percent = round(prob_real * 100, 1)
122
+
123
+ # Training notebook: prob >= 0.5 = FAKE, prob < 0.5 = REAL
124
+ if prob_fake >= 0.5:
125
+ final_result = "Deepfake"
126
+ confidence = fake_percent
127
+ else:
128
+ final_result = "Real"
129
+ confidence = real_percent
130
+
131
+ # Print logit persis seperti kode lamamu untuk debugging di terminal
132
+ print(f"Logit: {logit:.4f} | P(fake): {fake_percent}% | Result: {final_result} ({confidence}%)")
133
+
134
+ processing_time = round(time.time() - start_time, 2)
135
+
136
+ return {
137
+ "result": final_result,
138
+ "probability": confidence,
139
+ "probability_fake": round(prob_fake * 100, 2),
140
+ "probability_real": round(prob_real * 100, 2),
141
+ "processing_time": processing_time
142
+ }
143
+
144
+ # ============================================================
145
+ # DATABASE INSERTION SERVICE
146
+ # ============================================================
147
+ def save_scan_history(user_id, file_name, result, confidence_score, processing_time):
148
+ db = get_db()
149
+ data = {
150
+ "user_id": user_id,
151
+ "file_name": file_name,
152
+ "url_file": f"/uploads/{file_name}",
153
+ "result": result,
154
+ "confidence_score": confidence_score,
155
+ "processing_time": processing_time
156
+ }
157
+ db.table("scan_histories").insert(data).execute()
BackEnd/services/auth_service.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from werkzeug.security import generate_password_hash, check_password_hash
2
+ import jwt
3
+ import os
4
+ import datetime
5
+
6
+ from core.db_connector import get_db
7
+ from models.user import User
8
+
9
+
10
+ def register_new_user(email, username, password, display_name):
11
+ db = get_db()
12
+
13
+ cek_email = db.table("users").select("id").eq("email", email).execute()
14
+ if len(cek_email.data) > 0:
15
+ raise ValueError("Email sudah digunakan!")
16
+
17
+ cek_username = db.table("users").select("id").eq("username", username).execute()
18
+ if len(cek_username.data) > 0:
19
+ raise ValueError("Username sudah digunakan!")
20
+
21
+ hashed_password = generate_password_hash(password)
22
+ new_user_data = {
23
+ "email": email,
24
+ "username": username,
25
+ "password_hash": hashed_password,
26
+ "display_name": display_name
27
+ }
28
+
29
+ response = db.table("users").insert(new_user_data).execute()
30
+
31
+ user_obj = User.from_dict(response.data[0])
32
+ return user_obj.to_dict()
33
+
34
+
35
+ def authenticate_user(email, password):
36
+ db = get_db()
37
+
38
+ response = db.table("users").select("*").eq("email", email).execute()
39
+ if len(response.data) == 0:
40
+ raise ValueError("Email atau password salah!")
41
+
42
+ user_data = response.data[0]
43
+
44
+ if not check_password_hash(user_data["password_hash"], password):
45
+ raise ValueError("Email atau password salah!")
46
+
47
+ secret_key = os.getenv("JWT_SECRET")
48
+ token_payload = {
49
+ "user_id": user_data["id"],
50
+ "email": user_data["email"],
51
+ "role": user_data["role"],
52
+ "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=24)
53
+ }
54
+
55
+ token = jwt.encode(token_payload, secret_key, algorithm="HS256")
56
+
57
+ user_obj = User.from_dict(user_data)
58
+ return {
59
+ "access_token": token,
60
+ "user": user_obj.to_dict()
61
+ }
BackEnd/services/statistic_service.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import date
2
+ from core.db_connector import get_db
3
+ from models.scan_history import ScanHistory
4
+
5
+
6
+ def get_dashboard_stats(user_id):
7
+ db = get_db()
8
+ today = date.today().isoformat()
9
+
10
+ today_res = db.table("scan_histories").select("id", count="exact") \
11
+ .eq("user_id", user_id) \
12
+ .gte("created_at", f"{today}T00:00:00") \
13
+ .execute()
14
+ today_scans = today_res.count if today_res.count is not None else 0
15
+
16
+ fakes_res = db.table("scan_histories").select("id", count="exact") \
17
+ .eq("user_id", user_id) \
18
+ .eq("result", "Deepfake") \
19
+ .execute()
20
+ detected_fakes = fakes_res.count if fakes_res.count is not None else 0
21
+
22
+ all_data_res = db.table("scan_histories").select("confidence_score, processing_time").eq("user_id",
23
+ user_id).execute()
24
+
25
+ avg_confidence = 0.0
26
+ avg_time = 0.0
27
+
28
+ if all_data_res.data:
29
+ scores = [float(d['confidence_score']) for d in all_data_res.data if d.get('confidence_score') is not None]
30
+ times = [float(d['processing_time']) for d in all_data_res.data if d.get('processing_time') is not None]
31
+
32
+ avg_confidence = sum(scores) / len(scores) if scores else 0.0
33
+ avg_time = sum(times) / len(times) if times else 0.0
34
+
35
+ return {
36
+ "today_scans": today_scans,
37
+ "detected_fakes": detected_fakes,
38
+ "avg_confidence": round(avg_confidence, 1), # Dibulatkan 1 desimal (misal 95.2)
39
+ "avg_processing_time": round(avg_time, 1) # Dibulatkan 1 desimal (misal 3.0)
40
+ }
41
+
42
+
43
+ def get_paginated_scans(user_id, page=1, limit=10):
44
+ # ... (Kode get_paginated_scans tetap sama persis seperti punyamu sebelumnya) ...
45
+ db = get_db()
46
+
47
+ start = (page - 1) * limit
48
+ end = start + limit - 1
49
+
50
+ response = db.table("scan_histories") \
51
+ .select("*", count="exact") \
52
+ .eq("user_id", user_id) \
53
+ .order("created_at", desc=True) \
54
+ .range(start, end) \
55
+ .execute()
56
+
57
+ scans = [ScanHistory.from_dict(d) for d in response.data]
58
+
59
+ total_items = response.count
60
+ total_pages = (total_items + limit - 1) // limit
61
+
62
+ return {
63
+ "items": [s.__dict__ for s in scans],
64
+ "pagination": {
65
+ "current_page": page,
66
+ "limit": limit,
67
+ "total_items": total_items,
68
+ "total_pages": total_pages
69
+ }
70
+ }
FrontEnd/.gitignore CHANGED
@@ -7,6 +7,10 @@ yarn-error.log*
7
  pnpm-debug.log*
8
  lerna-debug.log*
9
 
 
 
 
 
10
  node_modules
11
  dist
12
  dist-ssr
 
7
  pnpm-debug.log*
8
  lerna-debug.log*
9
 
10
+ env/
11
+ .venv/
12
+
13
+
14
  node_modules
15
  dist
16
  dist-ssr