Update : Git Ignore .env
Browse files- BackEnd/.gitignore +20 -2
- BackEnd/.idea/.gitignore +8 -0
- BackEnd/.idea/BackEnd.iml +20 -0
- BackEnd/.idea/inspectionProfiles/profiles_settings.xml +6 -0
- BackEnd/.idea/misc.xml +7 -0
- BackEnd/.idea/modules.xml +8 -0
- BackEnd/.idea/vcs.xml +6 -0
- BackEnd/IsItFake_API.postman_collection.json +277 -0
- BackEnd/app.py +152 -152
- BackEnd/app/__init__.py +0 -0
- BackEnd/controllers/analysis_controllers.py +48 -0
- BackEnd/controllers/auth_controllers.py +62 -0
- BackEnd/controllers/daily_statistic_controllers.py +31 -0
- BackEnd/controllers/test_controllers.py +19 -0
- BackEnd/core/db_connector.py +16 -0
- BackEnd/core/response_json.py +35 -0
- BackEnd/logo-full.png +3 -0
- BackEnd/middlewares/auth.py +60 -0
- BackEnd/{best_model.onnx → ml_models/best_model.onnx} +0 -0
- BackEnd/{best_model.pth → ml_models/best_model.pth} +0 -0
- BackEnd/models/scan_history.py +30 -0
- BackEnd/models/user.py +39 -0
- BackEnd/noted.txt +2 -0
- BackEnd/requirements.txt +67 -2
- BackEnd/run.py +35 -0
- BackEnd/services/analysis_service.py +157 -0
- BackEnd/services/auth_service.py +61 -0
- BackEnd/services/statistic_service.py +70 -0
- FrontEnd/.gitignore +4 -0
BackEnd/.gitignore
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
__pycache__/
|
| 2 |
-
*.
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 23 |
-
)
|
| 24 |
-
|
| 25 |
-
def crop_face(image_bgr):
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 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 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def preprocess_image(face_bgr):
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
# ============================================================
|
| 88 |
-
# ROUTES
|
| 89 |
-
# ============================================================
|
| 90 |
-
@app.route('/', methods=['GET'])
|
| 91 |
-
def health_check():
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
@app.route('/api/scan', methods=['POST'])
|
| 100 |
-
def scan_image():
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
if __name__ == '__main__':
|
| 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 = "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
|
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=
|
| 6 |
-
Werkzeug=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|