kkt-2002 commited on
Commit
8d5f1ef
·
1 Parent(s): 6d1dc35

Update all changes

Browse files
Files changed (4) hide show
  1. Dockerfile +31 -8
  2. README.md +35 -6
  3. app.py +779 -848
  4. requirements.txt +9 -6
Dockerfile CHANGED
@@ -1,26 +1,49 @@
1
  FROM python:3.11-slim
2
 
 
 
 
 
 
 
3
  WORKDIR /app
4
 
5
- # Install system dependencies
 
 
 
 
 
 
 
6
  RUN apt-get update && apt-get install -y \
7
  libglib2.0-0 \
8
  libsm6 \
9
  libxext6 \
10
- libxrender-dev \
11
  libgomp1 \
12
  libgtk-3-0 \
13
- libgl1 \
 
 
 
14
  && rm -rf /var/lib/apt/lists/*
15
 
16
- COPY requirements.txt .
17
- RUN pip install --no-cache-dir -r requirements.txt
 
 
 
 
 
18
 
19
- # FIX: Create DeepFace directory with proper permissions
20
- RUN mkdir -p /app/.deepface && chmod 755 /app/.deepface
21
 
22
- COPY . .
 
23
 
 
24
  EXPOSE 7860
25
 
 
26
  CMD ["python", "app.py"]
 
1
  FROM python:3.11-slim
2
 
3
+ # Create user (required for Hugging Face security)
4
+ RUN useradd -m -u 1000 user
5
+ USER user
6
+ ENV PATH="/home/user/.local/bin:$PATH"
7
+
8
+ # Set working directory
9
  WORKDIR /app
10
 
11
+ # Set environment variables
12
+ ENV PYTHONDONTWRITEBYTECODE=1
13
+ ENV PYTHONUNBUFFERED=1
14
+ ENV TF_CPP_MIN_LOG_LEVEL=3
15
+ ENV CUDA_VISIBLE_DEVICES=-1
16
+
17
+ # Install system dependencies (switch to root temporarily)
18
+ USER root
19
  RUN apt-get update && apt-get install -y \
20
  libglib2.0-0 \
21
  libsm6 \
22
  libxext6 \
 
23
  libgomp1 \
24
  libgtk-3-0 \
25
+ libjpeg-dev \
26
+ libpng-dev \
27
+ ffmpeg \
28
+ curl \
29
  && rm -rf /var/lib/apt/lists/*
30
 
31
+ # Switch back to user
32
+ USER user
33
+
34
+ # Copy requirements and install Python dependencies
35
+ COPY --chown=user:user requirements.txt .
36
+ RUN pip install --no-cache-dir --upgrade pip && \
37
+ pip install --no-cache-dir -r requirements.txt
38
 
39
+ # Copy application code
40
+ COPY --chown=user:user . /app
41
 
42
+ # Create necessary directories
43
+ RUN mkdir -p app/static app/templates
44
 
45
+ # Expose Hugging Face port
46
  EXPOSE 7860
47
 
48
+ # Run the application
49
  CMD ["python", "app.py"]
README.md CHANGED
@@ -5,13 +5,42 @@ colorFrom: blue
5
  colorTo: green
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
  # Face Recognition Attendance System
11
 
12
- A comprehensive face recognition-based attendance system with:
13
- - Face detection using YOLOv5-face
14
- - Anti-spoofing detection
15
- - Face recognition using DeepFace
16
- - Student and teacher management
17
- - Real-time attendance marking
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  colorTo: green
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
  ---
10
 
11
  # Face Recognition Attendance System
12
 
13
+ An advanced face recognition system for attendance management using DeepFace and OpenCV.
14
+
15
+ ## Features
16
+
17
+ - **Student & Teacher Registration** with face capture
18
+ - **Face Recognition Login**
19
+ - **Attendance Marking** with liveness detection
20
+ - **Real-time Analytics** and metrics dashboard
21
+ - **MongoDB Integration** for data storage
22
+
23
+ ## Usage
24
+
25
+ 1. Register as a student/teacher with face capture
26
+ 2. Login using face recognition or credentials
27
+ 3. Mark attendance with live face verification
28
+ 4. View attendance records and analytics
29
+
30
+ ## Technology Stack
31
+
32
+ - **Backend**: Flask, Python
33
+ - **Face Recognition**: DeepFace, OpenCV
34
+ - **Database**: MongoDB Atlas
35
+ - **Frontend**: HTML, CSS, JavaScript
36
+ - **Deployment**: Hugging Face Spaces (Docker)
37
+
38
+ ## Environment Variables
39
+
40
+ Set these in your Hugging Face Space settings:
41
+ - `SECRET_KEY`: Your secret key
42
+ - `MONGO_URI`: MongoDB Atlas connection string
43
+ - `PORT`: 7860 (default)
44
+
45
+ ## Local Development
46
+
app.py CHANGED
@@ -1,60 +1,32 @@
 
1
  import os
2
- import tempfile
3
- import secrets
4
- from datetime import timedelta, datetime, timezone
5
  import logging
6
-
7
- # Setup logging
8
- logging.basicConfig(level=logging.INFO)
9
- logger = logging.getLogger(__name__)
10
-
11
- # Set up proper temp directories for HuggingFace Spaces
12
- deepface_cache = os.path.join(tempfile.gettempdir(), "deepface_cache")
13
- os.makedirs(deepface_cache, exist_ok=True)
14
- os.environ["DEEPFACE_HOME"] = deepface_cache
15
-
16
- from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify
17
- import onnxruntime as ort
18
  import time
 
19
  import pymongo
20
  from pymongo import MongoClient
21
  from bson.binary import Binary
22
  import base64
 
23
  from dotenv import load_dotenv
24
  import numpy as np
25
  import cv2
26
- import requests
27
  from typing import Optional, Dict, Tuple, Any
28
- from deepface import DeepFace
29
- from sklearn.metrics.pairwise import cosine_similarity
30
- import tensorflow as tf
31
-
32
- # Optimize TensorFlow
33
- tf.config.threading.set_intra_op_parallelism_threads(1)
34
- tf.config.threading.set_inter_op_parallelism_threads(1)
35
- os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
36
 
37
- # Load environment variables
38
- load_dotenv()
 
 
39
 
40
- # Initialize Flask app with proper configuration
41
- app = Flask(__name__, static_folder='app/static', template_folder='app/templates')
 
42
 
43
- # FIXED: Proper session configuration for production
44
- app.secret_key = os.getenv('SECRET_KEY', secrets.token_hex(32))
45
-
46
- # Essential session settings for Hugging Face Spaces
47
- app.config.update(
48
- SESSION_COOKIE_SECURE=False, # Keep False for HTTP (Hugging Face handles HTTPS)
49
- SESSION_COOKIE_HTTPONLY=True,
50
- SESSION_COOKIE_SAMESITE='Lax',
51
- SESSION_COOKIE_PATH='/',
52
- PERMANENT_SESSION_LIFETIME=timedelta(hours=24),
53
- SESSION_TYPE=None,
54
- SESSION_REFRESH_EACH_REQUEST=False
55
- )
56
-
57
- # Global variables for tracking
58
  total_attempts = 0
59
  correct_recognitions = 0
60
  false_accepts = 0
@@ -62,254 +34,240 @@ false_rejects = 0
62
  unauthorized_attempts = 0
63
  inference_times = []
64
 
65
- # Model status tracking
66
- model_status = {
67
- 'yolo_loaded': False,
68
- 'antispoof_loaded': False,
69
- 'database_connected': False
70
- }
71
-
72
- # Database connection with error handling
73
- def initialize_database():
74
- """Initialize MongoDB connection with proper error handling"""
75
- global client, db, students_collection, teachers_collection, attendance_collection, metrics_events
76
-
 
77
  try:
78
- mongo_uri = os.getenv('MONGO_URI', 'mongodb://localhost:27017/')
79
- client = MongoClient(mongo_uri, serverSelectionTimeoutMS=5000)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- # Test connection
82
- client.admin.command('ping')
 
 
 
 
83
 
84
- db = client['face_attendance_system']
85
- students_collection = db['students']
86
- teachers_collection = db['teachers']
87
- attendance_collection = db['attendance']
88
- metrics_events = db['metrics_events']
 
 
89
 
90
- # Create indexes
91
- try:
92
- students_collection.create_index([("student_id", pymongo.ASCENDING)], unique=True)
93
- teachers_collection.create_index([("teacher_id", pymongo.ASCENDING)], unique=True)
94
- attendance_collection.create_index([
95
- ("student_id", pymongo.ASCENDING),
96
- ("date", pymongo.ASCENDING),
97
- ("subject", pymongo.ASCENDING)
98
- ])
99
- metrics_events.create_index([("ts", pymongo.DESCENDING)])
100
- except Exception as idx_error:
101
- logger.warning(f"Index creation warning: {idx_error}")
102
-
103
- model_status['database_connected'] = True
104
- logger.info("MongoDB connection successful")
105
- return True
 
106
 
 
 
 
107
  except Exception as e:
108
- logger.error(f"MongoDB connection error: {e}")
109
- model_status['database_connected'] = False
110
- return False
111
 
112
- def check_db_connection():
113
- """Check if database is connected"""
 
 
 
 
114
  try:
115
- if not model_status['database_connected']:
116
- return initialize_database()
117
- client.admin.command('ping')
118
- return True
119
- except Exception:
120
- model_status['database_connected'] = False
121
- return initialize_database()
122
-
123
- # Initialize database
124
- initialize_database()
125
-
126
- # Model file paths using local models directory
127
- BASE_DIR = os.path.dirname(os.path.abspath(__file__))
128
- MODELS_DIR = os.path.join(BASE_DIR, 'models')
129
- YOLO_FACE_MODEL_PATH = os.path.join(MODELS_DIR, 'yolov5s-face.onnx')
130
- ANTI_SPOOF_BIN_MODEL_PATH = os.path.join(MODELS_DIR, 'anti-spoofing', 'AntiSpoofing_bin_1.5_128.onnx')
131
-
132
- # YOLO Face Detection Helper Functions
133
- def _get_providers():
134
- available = ort.get_available_providers()
135
- if "CUDAExecutionProvider" in available:
136
- return ["CUDAExecutionProvider", "CPUExecutionProvider"]
137
- return ["CPUExecutionProvider"]
138
-
139
- def _letterbox(image, new_shape=(640, 640), color=(114, 114, 114), auto=False, scaleFill=False, scaleup=True):
140
- shape = image.shape[:2]
141
- if isinstance(new_shape, int):
142
- new_shape = (new_shape, new_shape)
143
- r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
144
- if not scaleup:
145
- r = min(r, 1.0)
146
- new_unpad = (int(round(shape[1] * r)), int(round(shape[0] * r)))
147
- dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]
148
- if auto:
149
- dw, dh = np.mod(dw, 32), np.mod(dh, 32)
150
- elif scaleFill:
151
- dw, dh = 0.0, 0.0
152
- new_unpad = (new_shape[1], new_shape[0])
153
- r = new_shape[1] / shape[1], new_shape[0] / shape[0]
154
- dw /= 2
155
- dh /= 2
156
- if shape[::-1] != new_unpad:
157
- image = cv2.resize(image, new_unpad, interpolation=cv2.INTER_LINEAR)
158
- top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
159
- left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
160
- image = cv2.copyMakeBorder(image, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
161
- return image, r, (left, top)
162
-
163
- def _nms(boxes: np.ndarray, scores: np.ndarray, iou_threshold: float):
164
- if len(boxes) == 0:
165
- return []
166
- x1 = boxes[:, 0]
167
- y1 = boxes[:, 1]
168
- x2 = boxes[:, 2]
169
- y2 = boxes[:, 3]
170
- areas = (x2 - x1) * (y2 - y1)
171
- order = scores.argsort()[::-1]
172
- keep = []
173
- while order.size > 0:
174
- i = int(order[0])
175
- keep.append(i)
176
- if order.size == 1:
177
- break
178
- xx1 = np.maximum(x1[i], x1[order[1:]])
179
- yy1 = np.maximum(y1[i], y1[order[1:]])
180
- xx2 = np.minimum(x2[i], x2[order[1:]])
181
- yy2 = np.minimum(y2[i], y2[order[1:]])
182
- w = np.maximum(0.0, xx2 - xx1)
183
- h = np.maximum(0.0, yy2 - yy1)
184
- inter = w * h
185
- iou = inter / (areas[i] + areas[order[1:]] - inter + 1e-6)
186
- inds = np.where(iou <= iou_threshold)[0]
187
- order = order[inds + 1]
188
- return keep
189
-
190
- # YOLO Face Detector Class
191
- class YoloV5FaceDetector:
192
- def __init__(self, model_path: str, input_size: int = 640, conf_threshold: float = 0.3, iou_threshold: float = 0.45):
193
- if not os.path.exists(model_path):
194
- raise FileNotFoundError(f"Model file not found: {model_path}")
195
 
196
- self.input_size = int(input_size)
197
- self.conf_threshold = float(conf_threshold)
198
- self.iou_threshold = float(iou_threshold)
199
- self.session = ort.InferenceSession(model_path, providers=_get_providers())
200
- self.input_name = self.session.get_inputs()[0].name
201
- self.output_names = [o.name for o in self.session.get_outputs()]
202
-
203
- @staticmethod
204
- def _xywh2xyxy(x: np.ndarray) -> np.ndarray:
205
- y = np.zeros_like(x)
206
- y[:, 0] = x[:, 0] - x[:, 2] / 2
207
- y[:, 1] = x[:, 1] - x[:, 3] / 2
208
- y[:, 2] = x[:, 0] + x[:, 2] / 2
209
- y[:, 3] = x[:, 1] + x[:, 3] / 2
210
- return y
211
-
212
- def detect(self, image_bgr: np.ndarray, max_det: int = 20):
213
- h0, w0 = image_bgr.shape[:2]
214
- img, ratio, dwdh = _letterbox(image_bgr, new_shape=(self.input_size, self.input_size))
215
- img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
216
- img = img.astype(np.float32) / 255.0
217
- img = np.transpose(img, (2, 0, 1))
218
- img = np.expand_dims(img, 0)
219
-
220
- preds = self.session.run(self.output_names, {self.input_name: img})[0]
221
- if preds.ndim == 3 and preds.shape[0] == 1:
222
- preds = preds[0]
223
- if preds.ndim != 2:
224
- raise RuntimeError(f"Unexpected YOLO output shape: {preds.shape}")
225
 
226
- num_attrs = preds.shape[1]
227
- has_landmarks = num_attrs >= 15
228
- boxes_xywh = preds[:, 0:4]
 
229
 
230
- if has_landmarks:
231
- scores = preds[:, 4]
 
232
  else:
233
- obj = preds[:, 4:5]
234
- cls_scores = preds[:, 5:]
235
- if cls_scores.size == 0:
236
- scores = obj.squeeze(-1)
237
- else:
238
- class_conf = cls_scores.max(axis=1, keepdims=True)
239
- scores = (obj * class_conf).squeeze(-1)
240
-
241
- keep = scores > self.conf_threshold
242
- boxes_xywh = boxes_xywh[keep]
243
- scores = scores[keep]
244
 
245
- if boxes_xywh.shape[0] == 0:
246
- return []
 
247
 
248
- boxes_xyxy = self._xywh2xyxy(boxes_xywh)
249
- boxes_xyxy[:, [0, 2]] -= dwdh[0]
250
- boxes_xyxy[:, [1, 3]] -= dwdh[1]
251
- boxes_xyxy /= ratio
252
- boxes_xyxy[:, 0] = np.clip(boxes_xyxy[:, 0], 0, w0 - 1)
253
- boxes_xyxy[:, 1] = np.clip(boxes_xyxy[:, 1], 0, h0 - 1)
254
- boxes_xyxy[:, 2] = np.clip(boxes_xyxy[:, 2], 0, w0 - 1)
255
- boxes_xyxy[:, 3] = np.clip(boxes_xyxy[:, 3], 0, h0 - 1)
256
 
257
- keep_inds = _nms(boxes_xyxy, scores, self.iou_threshold)
258
- if len(keep_inds) > max_det:
259
- keep_inds = keep_inds[:max_det]
260
 
261
- dets = []
262
- for i in keep_inds:
263
- dets.append({"bbox": boxes_xyxy[i].tolist(), "score": float(scores[i])})
264
- return dets
265
-
266
- # Anti-Spoofing Model
267
- def _sigmoid(x: np.ndarray) -> np.ndarray:
268
- return 1.0 / (1.0 + np.exp(-x))
269
-
270
- class AntiSpoofBinary:
271
- def __init__(self, model_path: str, input_size: int = 128, rgb: bool = True, normalize: bool = True,
272
- mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5), live_index: int = 1):
273
- if not os.path.exists(model_path):
274
- raise FileNotFoundError(f"Model file not found: {model_path}")
275
 
276
- self.input_size = int(input_size)
277
- self.rgb = bool(rgb)
278
- self.normalize = bool(normalize)
279
- self.mean = np.array(mean, dtype=np.float32).reshape(1, 1, 3)
280
- self.std = np.array(std, dtype=np.float32).reshape(1, 1, 3)
281
- self.live_index = int(live_index)
282
- self.session = ort.InferenceSession(model_path, providers=_get_providers())
283
- self.input_name = self.session.get_inputs()[0].name
284
- self.output_names = [o.name for o in self.session.get_outputs()]
285
-
286
- def _preprocess(self, face_bgr: np.ndarray) -> np.ndarray:
287
- img = cv2.resize(face_bgr, (self.input_size, self.input_size), interpolation=cv2.INTER_LINEAR)
288
- if self.rgb:
289
- img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
290
- img = img.astype(np.float32) / 255.0
291
- if self.normalize:
292
- img = (img - self.mean) / self.std
293
- img = np.transpose(img, (2, 0, 1))
294
- img = np.expand_dims(img, 0).astype(np.float32)
295
- return img
296
-
297
- def predict_live_prob(self, face_bgr: np.ndarray) -> float:
298
- inp = self._preprocess(face_bgr)
299
- outs = self.session.run(self.output_names, {self.input_name: inp})
300
- out = outs[0]
301
- if out.ndim > 1:
302
- out = np.squeeze(out, axis=0)
303
- if out.size == 2:
304
- vec = out.astype(np.float32)
305
- probs = np.exp(vec - np.max(vec))
306
- probs = probs / (np.sum(probs) + 1e-9)
307
- live_prob = float(probs[self.live_index])
 
 
 
 
 
 
 
 
 
 
 
 
308
  else:
309
- live_prob = float(_sigmoid(out.astype(np.float32)))
310
- return max(0.0, min(1.0, live_prob))
 
 
 
 
 
 
 
 
 
 
311
 
312
- # Helper Functions
313
  def expand_and_clip_box(bbox_xyxy, scale: float, w: int, h: int):
314
  x1, y1, x2, y2 = bbox_xyxy
315
  bw = x2 - x1
@@ -346,124 +304,41 @@ def decode_image(base64_image):
346
  image_bytes = base64.b64decode(base64_image)
347
  np_array = np.frombuffer(image_bytes, np.uint8)
348
  image = cv2.imdecode(np_array, cv2.IMREAD_COLOR)
349
- return image
350
-
351
- # Initialize models with better error handling
352
- yolo_face = None
353
- anti_spoof_bin = None
354
-
355
- def initialize_models():
356
- """Initialize models with proper error handling"""
357
- global yolo_face, anti_spoof_bin
358
 
359
- try:
360
- if os.path.exists(YOLO_FACE_MODEL_PATH):
361
- yolo_face = YoloV5FaceDetector(YOLO_FACE_MODEL_PATH, input_size=640, conf_threshold=0.3, iou_threshold=0.45)
362
- model_status['yolo_loaded'] = True
363
- logger.info("YOLO Face model loaded successfully")
364
- else:
365
- logger.warning(f"YOLO model not found at: {YOLO_FACE_MODEL_PATH}")
366
- except Exception as e:
367
- logger.error(f"Error loading YOLO model: {e}")
368
- model_status['yolo_loaded'] = False
369
-
370
- try:
371
- if os.path.exists(ANTI_SPOOF_BIN_MODEL_PATH):
372
- anti_spoof_bin = AntiSpoofBinary(ANTI_SPOOF_BIN_MODEL_PATH, input_size=128, rgb=True, normalize=True, live_index=1)
373
- model_status['antispoof_loaded'] = True
374
- logger.info("Anti-spoofing model loaded successfully")
375
- else:
376
- logger.warning(f"Anti-spoof model not found at: {ANTI_SPOOF_BIN_MODEL_PATH}")
377
- except Exception as e:
378
- logger.error(f"Error loading anti-spoofing model: {e}")
379
- model_status['antispoof_loaded'] = False
380
-
381
- # Initialize models
382
- initialize_models()
383
-
384
- # DeepFace Recognition Functions
385
- def get_face_features_deepface(image):
386
- """Extract face features using DeepFace with timeout protection"""
387
- try:
388
- rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
389
- embedding = DeepFace.represent(
390
- img_path=rgb_image,
391
- model_name='VGG-Face',
392
- detector_backend='opencv',
393
- enforce_detection=False
394
- )
395
-
396
- if isinstance(embedding, list) and len(embedding) > 0:
397
- return np.array(embedding[0]['embedding'])
398
- else:
399
- return np.array(embedding['embedding']) if 'embedding' in embedding else None
400
-
401
- except Exception as e:
402
- logger.error(f"Error in DeepFace feature extraction: {e}")
403
- return None
404
-
405
- def recognize_face_deepface(image, user_id, user_type='student'):
406
- """Face recognition using DeepFace with timeout protection"""
407
- global total_attempts, correct_recognitions, false_accepts, false_rejects, inference_times, unauthorized_attempts
408
-
409
- try:
410
- start_time = time.time()
411
- features = get_face_features_deepface(image)
412
-
413
- if features is None:
414
- return False, "No face detected"
415
-
416
- if user_type == 'student':
417
- user = students_collection.find_one({'student_id': user_id})
418
- else:
419
- user = teachers_collection.find_one({'teacher_id': user_id})
420
-
421
- if not user or 'face_image' not in user:
422
- unauthorized_attempts += 1
423
- return False, f"No reference face found for {user_type} ID {user_id}"
424
-
425
- ref_image_bytes = user['face_image']
426
- ref_image_array = np.frombuffer(ref_image_bytes, np.uint8)
427
- ref_image = cv2.imdecode(ref_image_array, cv2.IMREAD_COLOR)
428
- ref_features = get_face_features_deepface(ref_image)
429
-
430
- if ref_features is None:
431
- return False, "No face detected in reference image"
432
-
433
- # Calculate cosine similarity
434
- similarity = cosine_similarity([features], [ref_features])[0][0]
435
- distance = 1 - similarity
436
- threshold = 0.4
437
-
438
- inference_time = time.time() - start_time
439
- inference_times.append(inference_time)
440
- total_attempts += 1
441
 
442
- if distance < threshold:
443
- correct_recognitions += 1
444
- return True, f"Face recognized (distance={distance:.3f}, similarity={similarity:.3f}, time={inference_time:.2f}s)"
445
- else:
446
- unauthorized_attempts += 1
447
- return False, f"Unauthorized attempt detected (distance={distance:.3f}, similarity={similarity:.3f})"
448
-
449
- except Exception as e:
450
- return False, f"Error in face recognition: {str(e)}"
451
 
452
  def recognize_face(image, user_id, user_type='student'):
 
453
  return recognize_face_deepface(image, user_id, user_type)
454
 
455
- # Metrics helpers
456
  def log_metrics_event(event: dict):
457
  try:
458
- if check_db_connection():
459
- metrics_events.insert_one(event)
460
  except Exception as e:
461
- logger.error(f"Failed to log metrics event: {e}")
462
-
463
- def log_metrics_event_normalized(*, event: str, attempt_type: str, claimed_id: Optional[str],
464
- recognized_id: Optional[str], liveness_pass: bool, distance: Optional[float],
465
- live_prob: Optional[float], latency_ms: Optional[float], client_ip: Optional[str],
466
- reason: Optional[str] = None):
 
 
 
 
 
 
 
 
 
467
  if not liveness_pass:
468
  decision = "spoof_blocked"
469
  else:
@@ -485,28 +360,116 @@ def log_metrics_event_normalized(*, event: str, attempt_type: str, claimed_id: O
485
  }
486
  log_metrics_event(doc)
487
 
488
- # Session verification decorator
489
- def login_required(user_type=None):
490
- def decorator(f):
491
- def wrapper(*args, **kwargs):
492
- if not session.get('logged_in'):
493
- if request.is_json:
494
- return jsonify({'success': False, 'message': 'Not logged in', 'redirect': '/login.html'})
495
- flash('Please log in to access this page.', 'warning')
496
- return redirect(url_for('login_page'))
497
-
498
- if user_type and session.get('user_type') != user_type:
499
- if request.is_json:
500
- return jsonify({'success': False, 'message': 'Unauthorized', 'redirect': '/login.html'})
501
- flash('Unauthorized access.', 'danger')
502
- return redirect(url_for('login_page'))
503
-
504
- return f(*args, **kwargs)
505
- wrapper.__name__ = f.__name__
506
- return wrapper
507
- return decorator
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
 
509
- # Routes
510
  @app.route('/')
511
  def home():
512
  return render_template('home.html')
@@ -520,26 +483,12 @@ def register_page():
520
  return render_template('register.html')
521
 
522
  @app.route('/metrics')
523
- @login_required('teacher')
524
  def metrics_dashboard():
525
  return render_template('metrics.html')
526
 
527
- @app.route('/health-check')
528
- def health_check():
529
- return jsonify({
530
- 'status': 'healthy',
531
- 'models': model_status,
532
- 'database_connected': check_db_connection(),
533
- 'timestamp': datetime.now().isoformat()
534
- })
535
-
536
  @app.route('/register', methods=['POST'])
537
  def register():
538
  try:
539
- if not check_db_connection():
540
- flash('Database connection error. Please try again later.', 'danger')
541
- return redirect(url_for('register_page'))
542
-
543
  student_data = {
544
  'student_id': request.form.get('student_id'),
545
  'name': request.form.get('name'),
@@ -554,7 +503,6 @@ def register():
554
  'password': request.form.get('password'),
555
  'created_at': datetime.now()
556
  }
557
-
558
  face_image = request.form.get('face_image')
559
  if face_image and ',' in face_image:
560
  image_data = face_image.split(',')[1]
@@ -571,143 +519,123 @@ def register():
571
  else:
572
  flash('Registration failed. Please try again.', 'danger')
573
  return redirect(url_for('register_page'))
574
-
575
  except pymongo.errors.DuplicateKeyError:
576
  flash('Student ID already exists. Please use a different ID.', 'danger')
577
  return redirect(url_for('register_page'))
578
  except Exception as e:
579
- logger.error(f"Registration error: {e}")
580
  flash(f'Registration failed: {str(e)}', 'danger')
581
  return redirect(url_for('register_page'))
582
 
583
  @app.route('/login', methods=['POST'])
584
  def login():
585
- try:
586
- if not check_db_connection():
587
- flash('Database connection error. Please try again later.', 'danger')
588
- return redirect(url_for('login_page'))
589
-
590
- student_id = request.form.get('student_id')
591
- password = request.form.get('password')
592
-
593
- if not student_id or not password:
594
- flash('Please enter both student ID and password.', 'danger')
595
- return redirect(url_for('login_page'))
596
-
597
- student = students_collection.find_one({'student_id': student_id})
598
-
599
- if student and student.get('password') == password:
600
- session.permanent = True
601
- session['logged_in'] = True
602
- session['user_type'] = 'student'
603
- session['student_id'] = student_id
604
- session['name'] = student.get('name')
605
- session.modified = True
606
-
607
- flash('Login successful!', 'success')
608
- return redirect(url_for('dashboard'))
609
- else:
610
- flash('Invalid credentials. Please try again.', 'danger')
611
- return redirect(url_for('login_page'))
612
-
613
- except Exception as e:
614
- logger.error(f"Login error: {e}")
615
- flash('Login failed due to server error. Please try again.', 'danger')
616
  return redirect(url_for('login_page'))
617
 
618
  @app.route('/face-login', methods=['POST'])
619
  def face_login():
620
- try:
621
- if not check_db_connection():
622
- flash('Database connection error. Please try again later.', 'danger')
623
- return redirect(url_for('login_page'))
624
-
625
- face_image = request.form.get('face_image')
626
- face_role = request.form.get('face_role')
627
 
628
- if not face_image or not face_role:
629
- flash('Face image and role are required for face login.', 'danger')
630
- return redirect(url_for('login_page'))
631
 
632
- image = decode_image(face_image)
633
- if image is None:
634
- flash('Invalid image data.', 'danger')
635
- return redirect(url_for('login_page'))
636
 
637
- if face_role == 'student':
638
- collection = students_collection
639
- id_field = 'student_id'
640
- dashboard_route = 'dashboard'
641
- elif face_role == 'teacher':
642
- collection = teachers_collection
643
- id_field = 'teacher_id'
644
- dashboard_route = 'teacher_dashboard'
645
- else:
646
- flash('Invalid role selected for face login.', 'danger')
647
- return redirect(url_for('login_page'))
648
 
649
- users = collection.find({'face_image': {'$exists': True, '$ne': None}}).limit(20)
650
- test_features = get_face_features_deepface(image)
 
 
 
 
 
 
651
 
652
- if test_features is None:
653
- flash('No face detected or processing failed. Please try again.', 'danger')
654
- return redirect(url_for('login_page'))
655
-
656
  for user in users:
 
 
 
 
 
 
 
657
  try:
658
- ref_image_bytes = user['face_image']
659
- ref_image_array = np.frombuffer(ref_image_bytes, np.uint8)
660
- ref_image = cv2.imdecode(ref_image_array, cv2.IMREAD_COLOR)
661
- ref_features = get_face_features_deepface(ref_image)
 
 
662
 
663
- if ref_features is None:
664
- continue
665
-
666
- similarity = cosine_similarity([test_features], [ref_features])[0][0]
667
- distance = 1 - similarity
668
-
669
- if distance < 0.4:
670
- session.permanent = True
671
  session['logged_in'] = True
672
  session['user_type'] = face_role
673
  session[id_field] = user[id_field]
674
  session['name'] = user.get('name')
675
- session.modified = True
676
-
677
  flash('Face login successful!', 'success')
678
- return redirect(url_for(dashboard_route))
679
 
 
 
 
 
 
 
 
 
 
680
  except Exception as e:
681
- logger.error(f"Error processing user {user.get(id_field)}: {e}")
 
682
  continue
683
-
684
- flash('Face not recognized. Please try again or contact admin.', 'danger')
685
- return redirect(url_for('login_page'))
686
 
 
 
 
687
  except Exception as e:
688
- logger.error(f"Face login error: {e}")
689
- flash('Login failed due to server error. Please try again.', 'danger')
690
- return redirect(url_for('login_page'))
 
 
 
 
691
 
692
  @app.route('/auto-face-login', methods=['POST'])
693
  def auto_face_login():
 
694
  try:
695
- if not check_db_connection():
696
- return jsonify({'success': False, 'message': 'Database connection error'})
697
-
698
  data = request.json
699
  face_image = data.get('face_image')
700
  face_role = data.get('face_role', 'student')
701
-
702
  if not face_image:
703
  return jsonify({'success': False, 'message': 'No image received'})
704
-
705
  image = decode_image(face_image)
706
- test_features = get_face_features_deepface(image)
707
 
708
- if test_features is None:
709
- return jsonify({'success': False, 'message': 'No face detected'})
710
-
711
  if face_role == 'teacher':
712
  collection = teachers_collection
713
  id_field = 'teacher_id'
@@ -717,240 +645,288 @@ def auto_face_login():
717
  id_field = 'student_id'
718
  dashboard_route = '/dashboard'
719
 
720
- users = collection.find({'face_image': {'$exists': True, '$ne': None}}).limit(20)
 
 
721
 
722
- for user in users:
723
- try:
724
- ref_image_array = np.frombuffer(user['face_image'], np.uint8)
725
- ref_image = cv2.imdecode(ref_image_array, cv2.IMREAD_COLOR)
726
- ref_features = get_face_features_deepface(ref_image)
727
-
728
- if ref_features is None:
729
- continue
730
-
731
- similarity = cosine_similarity([test_features], [ref_features])[0][0]
732
- distance = 1 - similarity
733
-
734
- if distance < 0.4:
735
- session.permanent = True
736
- session['logged_in'] = True
737
- session['user_type'] = face_role
738
- session[id_field] = user[id_field]
739
- session['name'] = user.get('name')
740
- session.modified = True
741
 
742
- return jsonify({
743
- 'success': True,
744
- 'message': f'Welcome {user["name"]}! Redirecting...',
745
- 'redirect_url': dashboard_route,
746
- 'face_role': face_role
747
- })
748
 
749
- except Exception as e:
750
- logger.error(f"Error processing user {user.get(id_field)}: {e}")
751
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
752
 
753
  return jsonify({'success': False, 'message': f'Face not recognized in {face_role} database'})
754
-
755
  except Exception as e:
756
- logger.error(f"Auto face login error: {e}")
757
  return jsonify({'success': False, 'message': 'Login failed due to server error'})
758
 
759
  @app.route('/attendance.html')
760
- @login_required('student')
761
  def attendance_page():
 
 
762
  student_id = session.get('student_id')
763
  student = students_collection.find_one({'student_id': student_id})
764
  return render_template('attendance.html', student=student)
765
 
766
  @app.route('/dashboard')
767
- @login_required('student')
768
  def dashboard():
769
- try:
770
- if not check_db_connection():
771
- flash('Database connection error. Please try again later.', 'warning')
772
- return redirect(url_for('login_page'))
773
-
774
- student_id = session.get('student_id')
775
- student = students_collection.find_one({'student_id': student_id})
776
-
777
- if not student:
778
- session.clear()
779
- flash('Student record not found. Please login again.', 'warning')
780
- return redirect(url_for('login_page'))
781
-
782
- # Process face image for display
783
- if student and 'face_image' in student and student['face_image']:
784
- face_image_base64 = base64.b64encode(student['face_image']).decode('utf-8')
785
- mime_type = student.get('face_image_type', 'image/jpeg')
786
- student['face_image_url'] = f"data:{mime_type};base64,{face_image_base64}"
787
-
788
- attendance_records = list(attendance_collection.find({'student_id': student_id}).sort('date', -1))
789
-
790
- return render_template('dashboard.html', student=student, attendance_records=attendance_records)
791
-
792
- except Exception as e:
793
- logger.error(f"Dashboard error: {e}")
794
- flash('Error loading dashboard. Please try again.', 'danger')
795
  return redirect(url_for('login_page'))
 
 
 
 
 
 
 
 
796
 
797
  @app.route('/mark-attendance', methods=['POST'])
798
- @login_required('student')
799
  def mark_attendance():
800
- try:
801
- if not check_db_connection():
802
- return jsonify({'success': False, 'message': 'Database connection error. Please try again later.'})
803
-
804
- if not model_status['yolo_loaded']:
805
- return jsonify({'success': False, 'message': 'Face detection model not available. Please contact admin.'})
806
-
807
- data = request.json
808
- student_id = session.get('student_id') or data.get('student_id')
809
- program = data.get('program')
810
- semester = data.get('semester')
811
- course = data.get('course')
812
- face_image = data.get('face_image')
813
-
814
- if not all([student_id, program, semester, course, face_image]):
815
- return jsonify({'success': False, 'message': 'Missing required data'})
816
-
817
- client_ip = request.remote_addr
818
- t0 = time.time()
819
-
820
- image = decode_image(face_image)
821
- if image is None or image.size == 0:
822
- return jsonify({'success': False, 'message': 'Invalid image data'})
823
-
824
- h, w = image.shape[:2]
825
- vis = image.copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
826
 
827
- detections = yolo_face.detect(image, max_det=20)
828
- if not detections:
829
- overlay = image_to_data_uri(vis)
830
- log_metrics_event_normalized(
831
- event="reject_true", attempt_type="impostor", claimed_id=student_id,
832
- recognized_id=None, liveness_pass=False, distance=None, live_prob=None,
833
- latency_ms=round((time.time() - t0) * 1000.0, 2), client_ip=client_ip,
834
- reason="no_face_detected"
835
- )
836
- return jsonify({'success': False, 'message': 'No face detected for liveness', 'overlay': overlay})
837
 
838
- best = max(detections, key=lambda d: d["score"])
839
- x1, y1, x2, y2 = [int(v) for v in best["bbox"]]
840
- x1e, y1e, x2e, y2e = expand_and_clip_box((x1, y1, x2, y2), scale=1.2, w=w, h=h)
841
- face_crop = image[y1e:y2e, x1e:x2e]
842
-
843
- if face_crop.size == 0:
844
- overlay = image_to_data_uri(vis)
845
- log_metrics_event_normalized(
846
- event="reject_true", attempt_type="impostor", claimed_id=student_id,
847
- recognized_id=None, liveness_pass=False, distance=None, live_prob=None,
848
- latency_ms=round((time.time() - t0) * 1000.0, 2), client_ip=client_ip,
849
- reason="failed_crop"
850
- )
851
- return jsonify({'success': False, 'message': 'Failed to crop face for liveness', 'overlay': overlay})
852
 
853
- # Anti-spoofing
854
- live_prob = 1.0
855
- is_live = True
856
-
857
- if model_status['antispoof_loaded'] and anti_spoof_bin:
858
- live_prob = anti_spoof_bin.predict_live_prob(face_crop)
859
- is_live = live_prob >= 0.7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
860
 
861
- label = "LIVE" if is_live else "SPOOF"
862
- color = (0, 200, 0) if is_live else (0, 0, 255)
863
- draw_live_overlay(vis, (x1e, y1e, x2e, y2e), label, live_prob, color)
864
- overlay_data = image_to_data_uri(vis)
865
-
866
- if not is_live:
867
- log_metrics_event_normalized(
868
- event="reject_true", attempt_type="impostor", claimed_id=student_id,
869
- recognized_id=None, liveness_pass=False, distance=None, live_prob=float(live_prob),
870
- latency_ms=round((time.time() - t0) * 1000.0, 2), client_ip=client_ip,
871
- reason="liveness_fail"
872
- )
873
- return jsonify({'success': False, 'message': f'Spoof detected or face not live (p={live_prob:.2f}).', 'overlay': overlay_data})
874
-
875
- success, message = recognize_face(image, student_id, user_type='student')
876
- total_latency_ms = round((time.time() - t0) * 1000.0, 2)
877
-
878
- distance_val = None
879
  try:
880
- if "distance=" in message:
881
- part = message.split("distance=")[1]
882
- distance_val = float(part.split(",")[0].strip(") "))
883
- except Exception:
884
- pass
885
-
886
- if success:
887
- log_metrics_event_normalized(
888
- event="accept_true", attempt_type="genuine", claimed_id=student_id,
889
- recognized_id=student_id, liveness_pass=True, distance=distance_val,
890
- live_prob=float(live_prob), latency_ms=total_latency_ms, client_ip=client_ip, reason=None
891
- )
892
-
893
- # Check if attendance already marked today
894
  existing_attendance = attendance_collection.find_one({
895
  'student_id': student_id,
896
  'subject': course,
897
  'date': datetime.now().date().isoformat()
898
  })
899
-
900
  if existing_attendance:
901
  return jsonify({'success': False, 'message': 'Attendance already marked for this course today', 'overlay': overlay_data})
902
-
903
- attendance_data = {
904
- 'student_id': student_id,
905
- 'program': program,
906
- 'semester': semester,
907
- 'subject': course,
908
- 'date': datetime.now().date().isoformat(),
909
- 'time': datetime.now().time().strftime('%H:%M:%S'),
910
- 'status': 'present',
911
- 'created_at': datetime.now()
912
- }
913
-
914
  attendance_collection.insert_one(attendance_data)
 
915
  return jsonify({'success': True, 'message': 'Attendance marked successfully', 'overlay': overlay_data})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
916
  else:
917
- # Determine reason for failure
918
- reason = "unauthorized_attempt"
919
- if "No face detected" in message:
920
- reason = "no_face_detected"
921
- elif "Error in face recognition" in message:
922
- reason = "recognition_error"
923
-
924
  log_metrics_event_normalized(
925
- event="reject_true", attempt_type="impostor", claimed_id=student_id,
926
- recognized_id=None, liveness_pass=True, distance=distance_val,
927
- live_prob=float(live_prob), latency_ms=total_latency_ms, client_ip=client_ip, reason=reason
 
 
 
 
 
 
 
928
  )
929
- return jsonify({'success': False, 'message': message, 'overlay': overlay_data})
930
-
931
- except Exception as e:
932
- logger.error(f"Mark attendance error: {e}")
933
- return jsonify({'success': False, 'message': 'Server error occurred. Please try again.'})
934
 
935
  @app.route('/liveness-preview', methods=['POST'])
936
- @login_required('student')
937
  def liveness_preview():
 
 
938
  try:
939
- if not model_status['yolo_loaded']:
940
- return jsonify({'success': False, 'message': 'Face detection model not available'})
941
-
942
  data = request.json or {}
943
  face_image = data.get('face_image')
944
  if not face_image:
945
  return jsonify({'success': False, 'message': 'No image received'})
946
-
947
  image = decode_image(face_image)
948
  if image is None or image.size == 0:
949
  return jsonify({'success': False, 'message': 'Invalid image data'})
950
-
951
  h, w = image.shape[:2]
952
  vis = image.copy()
953
- detections = yolo_face.detect(image, max_det=10)
954
 
955
  if not detections:
956
  overlay_data = image_to_data_uri(vis)
@@ -961,7 +937,7 @@ def liveness_preview():
961
  'message': 'No face detected',
962
  'overlay': overlay_data
963
  })
964
-
965
  best = max(detections, key=lambda d: d["score"])
966
  x1, y1, x2, y2 = [int(v) for v in best["bbox"]]
967
  x1e, y1e, x2e, y2e = expand_and_clip_box((x1, y1, x2, y2), scale=1.2, w=w, h=h)
@@ -977,28 +953,29 @@ def liveness_preview():
977
  'overlay': overlay_data
978
  })
979
 
980
- live_prob = 1.0
981
- if model_status['antispoof_loaded'] and anti_spoof_bin:
982
- live_prob = anti_spoof_bin.predict_live_prob(face_crop)
983
-
984
  threshold = 0.7
985
  label = "LIVE" if live_prob >= threshold else "SPOOF"
986
  color = (0, 200, 0) if label == "LIVE" else (0, 0, 255)
 
987
  draw_live_overlay(vis, (x1e, y1e, x2e, y2e), label, live_prob, color)
988
  overlay_data = image_to_data_uri(vis)
989
 
 
 
 
 
990
  return jsonify({
991
  'success': True,
992
  'live': bool(live_prob >= threshold),
993
  'live_prob': float(live_prob),
994
  'overlay': overlay_data
995
  })
996
-
997
  except Exception as e:
998
- logger.error(f"Liveness preview error: {e}")
999
  return jsonify({'success': False, 'message': 'Server error during preview'})
1000
 
1001
- # Teacher routes
1002
  @app.route('/teacher_register.html')
1003
  def teacher_register_page():
1004
  return render_template('teacher_register.html')
@@ -1010,10 +987,6 @@ def teacher_login_page():
1010
  @app.route('/teacher_register', methods=['POST'])
1011
  def teacher_register():
1012
  try:
1013
- if not check_db_connection():
1014
- flash('Database connection error. Please try again later.', 'danger')
1015
- return redirect(url_for('teacher_register_page'))
1016
-
1017
  teacher_data = {
1018
  'teacher_id': request.form.get('teacher_id'),
1019
  'name': request.form.get('name'),
@@ -1026,7 +999,6 @@ def teacher_register():
1026
  'password': request.form.get('password'),
1027
  'created_at': datetime.now()
1028
  }
1029
-
1030
  face_image = request.form.get('face_image')
1031
  if face_image and ',' in face_image:
1032
  image_data = face_image.split(',')[1]
@@ -1035,7 +1007,6 @@ def teacher_register():
1035
  else:
1036
  flash('Face image is required for registration.', 'danger')
1037
  return redirect(url_for('teacher_register_page'))
1038
-
1039
  result = teachers_collection.insert_one(teacher_data)
1040
  if result.inserted_id:
1041
  flash('Registration successful! You can now login.', 'success')
@@ -1043,76 +1014,40 @@ def teacher_register():
1043
  else:
1044
  flash('Registration failed. Please try again.', 'danger')
1045
  return redirect(url_for('teacher_register_page'))
1046
-
1047
  except pymongo.errors.DuplicateKeyError:
1048
  flash('Teacher ID already exists. Please use a different ID.', 'danger')
1049
  return redirect(url_for('teacher_register_page'))
1050
  except Exception as e:
1051
- logger.error(f"Teacher registration error: {e}")
1052
  flash(f'Registration failed: {str(e)}', 'danger')
1053
  return redirect(url_for('teacher_register_page'))
1054
 
1055
  @app.route('/teacher_login', methods=['POST'])
1056
  def teacher_login():
1057
- try:
1058
- if not check_db_connection():
1059
- flash('Database connection error. Please try again later.', 'danger')
1060
- return redirect(url_for('teacher_login_page'))
1061
-
1062
- teacher_id = request.form.get('teacher_id')
1063
- password = request.form.get('password')
1064
-
1065
- if not teacher_id or not password:
1066
- flash('Please enter both teacher ID and password.', 'danger')
1067
- return redirect(url_for('teacher_login_page'))
1068
-
1069
- teacher = teachers_collection.find_one({'teacher_id': teacher_id})
1070
- if teacher and teacher.get('password') == password:
1071
- session.permanent = True
1072
- session['logged_in'] = True
1073
- session['user_type'] = 'teacher'
1074
- session['teacher_id'] = teacher_id
1075
- session['name'] = teacher.get('name')
1076
- session.modified = True
1077
-
1078
- flash('Login successful!', 'success')
1079
- return redirect(url_for('teacher_dashboard'))
1080
- else:
1081
- flash('Invalid credentials. Please try again.', 'danger')
1082
- return redirect(url_for('teacher_login_page'))
1083
-
1084
- except Exception as e:
1085
- logger.error(f"Teacher login error: {e}")
1086
- flash('Login failed due to server error. Please try again.', 'danger')
1087
  return redirect(url_for('teacher_login_page'))
1088
 
1089
  @app.route('/teacher_dashboard')
1090
- @login_required('teacher')
1091
  def teacher_dashboard():
1092
- try:
1093
- if not check_db_connection():
1094
- flash('Database connection error. Please try again later.', 'warning')
1095
- return redirect(url_for('teacher_login_page'))
1096
-
1097
- teacher_id = session.get('teacher_id')
1098
- teacher = teachers_collection.find_one({'teacher_id': teacher_id})
1099
-
1100
- if not teacher:
1101
- session.clear()
1102
- flash('Teacher record not found. Please login again.', 'warning')
1103
- return redirect(url_for('teacher_login_page'))
1104
-
1105
- if teacher and 'face_image' in teacher and teacher['face_image']:
1106
- face_image_base64 = base64.b64encode(teacher['face_image']).decode('utf-8')
1107
- mime_type = teacher.get('face_image_type', 'image/jpeg')
1108
- teacher['face_image_url'] = f"data:{mime_type};base64,{face_image_base64}"
1109
-
1110
- return render_template('teacher_dashboard.html', teacher=teacher)
1111
-
1112
- except Exception as e:
1113
- logger.error(f"Teacher dashboard error: {e}")
1114
- flash('Error loading dashboard. Please try again.', 'danger')
1115
  return redirect(url_for('teacher_login_page'))
 
 
 
 
 
 
 
1116
 
1117
  @app.route('/teacher_logout')
1118
  def teacher_logout():
@@ -1120,121 +1055,117 @@ def teacher_logout():
1120
  flash('You have been logged out', 'info')
1121
  return redirect(url_for('teacher_login_page'))
1122
 
 
1123
  @app.route('/logout')
1124
  def logout():
1125
  session.clear()
1126
  flash('You have been logged out', 'info')
1127
  return redirect(url_for('login_page'))
1128
 
1129
- # Metrics endpoints
1130
- def compute_metrics(limit: int = 10000):
1131
- if not check_db_connection():
1132
- return {"counts": {}, "rates": {}, "totals": {"totalAttempts": 0}}
1133
-
1134
- try:
1135
- cursor = metrics_events.find({}, {"_id": 0}).sort("ts", -1).limit(limit)
1136
- counts = {
1137
- "trueAccepts": 0, "falseAccepts": 0, "trueRejects": 0, "falseRejects": 0,
1138
- "genuineAttempts": 0, "impostorAttempts": 0, "unauthorizedRejected": 0, "unauthorizedAccepted": 0,
1139
- }
1140
-
1141
- total_attempts_calc = 0
1142
- for ev in cursor:
1143
- event = ev.get("event", "")
1144
- attempt_type = ev.get("attempt_type", "")
1145
-
1146
- if not event:
1147
- continue
1148
-
1149
- total_attempts_calc += 1
1150
-
1151
- if event == "accept_true":
1152
- counts["trueAccepts"] += 1
1153
- elif event == "accept_false":
1154
- counts["falseAccepts"] += 1
1155
- counts["unauthorizedAccepted"] += 1
1156
- elif event == "reject_true":
1157
- counts["trueRejects"] += 1
1158
- counts["unauthorizedRejected"] += 1
1159
- elif event == "reject_false":
1160
- counts["falseRejects"] += 1
1161
-
1162
- if attempt_type == "genuine":
1163
- counts["genuineAttempts"] += 1
1164
- elif attempt_type == "impostor":
1165
- counts["impostorAttempts"] += 1
1166
-
1167
- genuine_attempts = max(counts["genuineAttempts"], 1)
1168
- impostor_attempts = max(counts["impostorAttempts"], 1)
1169
- total_attempts_final = max(total_attempts_calc, 1)
1170
-
1171
- FAR = counts["falseAccepts"] / impostor_attempts
1172
- FRR = counts["falseRejects"] / genuine_attempts
1173
- accuracy = (counts["trueAccepts"] + counts["trueRejects"]) / total_attempts_final
1174
-
1175
- return {
1176
- "counts": counts,
1177
- "rates": {"FAR": FAR, "FRR": FRR, "accuracy": accuracy},
1178
- "totals": {"totalAttempts": total_attempts_calc}
1179
- }
1180
- except Exception as e:
1181
- logger.error(f"Error computing metrics: {e}")
1182
- return {"counts": {}, "rates": {}, "totals": {"totalAttempts": 0}}
1183
-
1184
- def compute_latency_avg(limit: int = 300) -> Optional[float]:
1185
- if not check_db_connection():
1186
- return None
1187
-
1188
- try:
1189
- cursor = metrics_events.find({"latency_ms": {"$exists": True}}, {"latency_ms": 1, "_id": 0}).sort("ts", -1).limit(limit)
1190
- vals = [float(d["latency_ms"]) for d in cursor if isinstance(d.get("latency_ms"), (int, float))]
1191
- if not vals:
1192
- return None
1193
- return sum(vals) / len(vals)
1194
- except Exception as e:
1195
- logger.error(f"Error computing latency: {e}")
1196
- return None
1197
-
1198
  @app.route('/metrics-data', methods=['GET'])
1199
- @login_required()
1200
  def metrics_data():
1201
  data = compute_metrics()
1202
  try:
1203
  recent = list(metrics_events.find({}, {"_id": 0}).sort("ts", -1).limit(200))
 
1204
  for r in recent:
1205
  if isinstance(r.get("ts"), datetime):
1206
  r["ts"] = r["ts"].isoformat()
1207
- data["recent"] = recent
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1208
  except Exception as e:
1209
- logger.error(f"Error fetching recent events: {e}")
1210
  data["recent"] = []
1211
 
1212
  data["avg_latency_ms"] = compute_latency_avg()
1213
  return jsonify(data)
1214
 
1215
  @app.route('/metrics-json')
1216
- @login_required()
1217
  def metrics_json():
1218
  m = compute_metrics()
1219
  counts = m["counts"]
1220
  rates = m["rates"]
1221
  totals = m["totals"]
1222
  avg_latency = compute_latency_avg()
1223
-
1224
- accuracy_pct = rates.get("accuracy", 0) * 100.0
1225
- far_pct = rates.get("FAR", 0) * 100.0
1226
- frr_pct = rates.get("FRR", 0) * 100.0
1227
 
1228
  return jsonify({
1229
  'Accuracy': f"{accuracy_pct:.2f}%" if totals["totalAttempts"] > 0 else "N/A",
1230
- 'False Accepts (FAR)': f"{far_pct:.2f}%" if counts.get("impostorAttempts", 0) > 0 else "N/A",
1231
- 'False Rejects (FRR)': f"{frr_pct:.2f}%" if counts.get("genuineAttempts", 0) > 0 else "N/A",
1232
  'Average Inference Time (s)': f"{(avg_latency/1000.0):.2f}" if isinstance(avg_latency, (int, float)) else "N/A",
1233
- 'Correct Recognitions': counts.get("trueAccepts", 0),
1234
  'Total Attempts': totals["totalAttempts"],
1235
- 'Unauthorized Attempts': counts.get("unauthorizedRejected", 0),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1236
  })
1237
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1238
  if __name__ == '__main__':
1239
- port = int(os.environ.get('PORT', 7860))
1240
  app.run(host='0.0.0.0', port=port, debug=False)
 
1
+ from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify
2
  import os
3
+ import gc
 
 
4
  import logging
 
 
 
 
 
 
 
 
 
 
 
 
5
  import time
6
+ import uuid
7
  import pymongo
8
  from pymongo import MongoClient
9
  from bson.binary import Binary
10
  import base64
11
+ from datetime import datetime, timezone
12
  from dotenv import load_dotenv
13
  import numpy as np
14
  import cv2
 
15
  from typing import Optional, Dict, Tuple, Any
16
+ import tempfile
17
+ import atexit
18
+ import shutil
 
 
 
 
 
19
 
20
+ # Optimize memory usage and disable TensorFlow warnings
21
+ os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
22
+ os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
23
+ os.environ['OMP_NUM_THREADS'] = '1'
24
 
25
+ # Configure logging for production
26
+ logging.basicConfig(level=logging.WARNING)
27
+ logging.getLogger('tensorflow').setLevel(logging.ERROR)
28
 
29
+ # --- Evaluation Metrics Counters (legacy, kept for compatibility display) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  total_attempts = 0
31
  correct_recognitions = 0
32
  false_accepts = 0
 
34
  unauthorized_attempts = 0
35
  inference_times = []
36
 
37
+ # ---------------------------------------------------
38
+ # Load environment variables
39
+ load_dotenv()
40
+
41
+ # Initialize Flask app
42
+ app = Flask(__name__, static_folder='app/static', template_folder='app/templates')
43
+ app.secret_key = os.environ.get('SECRET_KEY', os.urandom(24))
44
+
45
+ # Create temporary directory for image processing
46
+ TEMP_DIR = tempfile.mkdtemp()
47
+
48
+ def cleanup_temp_dir():
49
+ """Clean up temporary directory on exit"""
50
  try:
51
+ if os.path.exists(TEMP_DIR):
52
+ shutil.rmtree(TEMP_DIR)
53
+ gc.collect() # Force garbage collection
54
+ except Exception as e:
55
+ print(f"Error cleaning up temp directory: {e}")
56
+
57
+ # Register cleanup function
58
+ atexit.register(cleanup_temp_dir)
59
+
60
+ # MongoDB Connection with connection pooling
61
+ try:
62
+ mongo_uri = os.getenv('MONGO_URI', 'mongodb://localhost:27017/')
63
+ client = MongoClient(
64
+ mongo_uri,
65
+ maxPoolSize=10,
66
+ connectTimeoutMS=5000,
67
+ socketTimeoutMS=5000,
68
+ serverSelectionTimeoutMS=5000
69
+ )
70
+ db = client['face_attendance_system']
71
+ students_collection = db['students']
72
+ teachers_collection = db['teachers']
73
+ attendance_collection = db['attendance']
74
+ metrics_events = db['metrics_events']
75
+
76
+ # Create indexes for better performance
77
+ students_collection.create_index([("student_id", pymongo.ASCENDING)], unique=True)
78
+ teachers_collection.create_index([("teacher_id", pymongo.ASCENDING)], unique=True)
79
+ attendance_collection.create_index([
80
+ ("student_id", pymongo.ASCENDING),
81
+ ("date", pymongo.ASCENDING),
82
+ ("subject", pymongo.ASCENDING)
83
+ ])
84
+ metrics_events.create_index([("ts", pymongo.DESCENDING)])
85
+ metrics_events.create_index([("event", pymongo.ASCENDING)])
86
+ metrics_events.create_index([("attempt_type", pymongo.ASCENDING)])
87
+ print("MongoDB connection successful")
88
+ except Exception as e:
89
+ print(f"MongoDB connection error: {e}")
90
+
91
+ # ---------------- Memory-Optimized Face Detection ----------------
92
+
93
+ # Initialize Haar Cascade Face Detector (lightweight and reliable)
94
+ face_detector = None
95
+ try:
96
+ face_detector = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
97
+ print("Haar cascade face detector initialized successfully")
98
+ except Exception as e:
99
+ print(f"Error initializing face detector: {e}")
100
+
101
+ # Initialize eye cascade for liveness detection
102
+ eye_cascade = None
103
+ try:
104
+ eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml')
105
+ print("Eye cascade classifier initialized successfully")
106
+ except Exception as e:
107
+ print(f"Error initializing eye cascade: {e}")
108
+
109
+ def get_unique_temp_path(prefix="temp", suffix=".jpg"):
110
+ """Generate unique temporary file path"""
111
+ unique_id = str(uuid.uuid4())
112
+ filename = f"{prefix}_{unique_id}_{int(time.time())}{suffix}"
113
+ return os.path.join(TEMP_DIR, filename)
114
+
115
+ def detect_faces_haar(image):
116
+ """Detect faces using Haar cascade - memory efficient"""
117
+ try:
118
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
119
+ faces = face_detector.detectMultiScale(
120
+ gray,
121
+ scaleFactor=1.1,
122
+ minNeighbors=5,
123
+ minSize=(30, 30)
124
+ )
125
 
126
+ detections = []
127
+ for (x, y, w, h) in faces:
128
+ detections.append({
129
+ "bbox": [x, y, x + w, y + h],
130
+ "score": 0.9
131
+ })
132
 
133
+ # Clean up memory
134
+ del gray
135
+ gc.collect()
136
+ return detections
137
+ except Exception as e:
138
+ print(f"Error in Haar cascade detection: {e}")
139
+ return []
140
 
141
+ def detect_faces_yunet(image):
142
+ """Unified face detection function - memory optimized"""
143
+ if face_detector is not None:
144
+ return detect_faces_haar(image)
145
+
146
+ # Final fallback
147
+ try:
148
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
149
+ face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
150
+ faces = face_cascade.detectMultiScale(gray, 1.3, 5)
151
+
152
+ detections = []
153
+ for (x, y, w, h) in faces:
154
+ detections.append({
155
+ "bbox": [x, y, x + w, y + h],
156
+ "score": 0.8
157
+ })
158
 
159
+ del gray
160
+ gc.collect()
161
+ return detections
162
  except Exception as e:
163
+ print(f"Error in fallback detection: {e}")
164
+ return []
 
165
 
166
+ def recognize_face_deepface(image, user_id, user_type='student'):
167
+ """Memory-optimized face recognition using DeepFace"""
168
+ global total_attempts, correct_recognitions, unauthorized_attempts, inference_times
169
+
170
+ temp_files = []
171
+
172
  try:
173
+ # Lazy import DeepFace to save memory at startup
174
+ from deepface import DeepFace
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
+ start_time = time.time()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
+ # Save current image temporarily
179
+ temp_img_path = get_unique_temp_path(f"current_{user_id}")
180
+ temp_files.append(temp_img_path)
181
+ cv2.imwrite(temp_img_path, image)
182
 
183
+ # Get user's reference image
184
+ if user_type == 'student':
185
+ user = students_collection.find_one({'student_id': user_id})
186
  else:
187
+ user = teachers_collection.find_one({'teacher_id': user_id})
 
 
 
 
 
 
 
 
 
 
188
 
189
+ if not user or 'face_image' not in user:
190
+ unauthorized_attempts += 1
191
+ return False, f"No reference face found for {user_type} ID {user_id}"
192
 
193
+ # Save reference image temporarily
194
+ ref_image_bytes = user['face_image']
195
+ ref_image_array = np.frombuffer(ref_image_bytes, np.uint8)
196
+ ref_image = cv2.imdecode(ref_image_array, cv2.IMREAD_COLOR)
197
+ temp_ref_path = get_unique_temp_path(f"ref_{user_id}")
198
+ temp_files.append(temp_ref_path)
199
+ cv2.imwrite(temp_ref_path, ref_image)
 
200
 
201
+ # Clean up arrays immediately
202
+ del ref_image_array, ref_image
 
203
 
204
+ try:
205
+ # Use lighter DeepFace model for memory efficiency
206
+ result = DeepFace.verify(
207
+ img1_path=temp_img_path,
208
+ img2_path=temp_ref_path,
209
+ model_name="Facenet", # Lighter than Facenet512
210
+ enforce_detection=False
211
+ )
 
 
 
 
 
 
212
 
213
+ is_verified = result["verified"]
214
+ distance = result["distance"]
215
+
216
+ inference_time = time.time() - start_time
217
+ inference_times.append(inference_time)
218
+ total_attempts += 1
219
+
220
+ if is_verified:
221
+ correct_recognitions += 1
222
+ return True, f"Face recognized (distance={distance:.3f}, time={inference_time:.2f}s)"
223
+ else:
224
+ unauthorized_attempts += 1
225
+ return False, f"Unauthorized attempt detected (distance={distance:.3f})"
226
+
227
+ except Exception as e:
228
+ return False, f"DeepFace verification error: {str(e)}"
229
+
230
+ except Exception as e:
231
+ return False, f"Error in face recognition: {str(e)}"
232
+
233
+ finally:
234
+ # Clean up temporary files and memory
235
+ for temp_file in temp_files:
236
+ try:
237
+ if os.path.exists(temp_file):
238
+ os.remove(temp_file)
239
+ except Exception as e:
240
+ print(f"Error cleaning up temp file {temp_file}: {e}")
241
+ gc.collect()
242
+
243
+ def simple_liveness_check(image):
244
+ """Simple liveness detection using eye detection - memory optimized"""
245
+ if eye_cascade is None:
246
+ return 0.7 # Default score if cascade not available
247
+
248
+ try:
249
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
250
+ eyes = eye_cascade.detectMultiScale(gray, 1.3, 5)
251
+
252
+ # Simple liveness scoring based on eye detection
253
+ if len(eyes) >= 2:
254
+ score = 0.8 # High confidence if both eyes detected
255
+ elif len(eyes) == 1:
256
+ score = 0.6 # Medium confidence if one eye detected
257
  else:
258
+ score = 0.4 # Low confidence if no eyes detected
259
+
260
+ # Clean up memory
261
+ del gray
262
+ gc.collect()
263
+ return score
264
+
265
+ except Exception as e:
266
+ print(f"Error in liveness check: {e}")
267
+ return 0.5
268
+ finally:
269
+ gc.collect()
270
 
 
271
  def expand_and_clip_box(bbox_xyxy, scale: float, w: int, h: int):
272
  x1, y1, x2, y2 = bbox_xyxy
273
  bw = x2 - x1
 
304
  image_bytes = base64.b64decode(base64_image)
305
  np_array = np.frombuffer(image_bytes, np.uint8)
306
  image = cv2.imdecode(np_array, cv2.IMREAD_COLOR)
 
 
 
 
 
 
 
 
 
307
 
308
+ # Clean up memory
309
+ del image_bytes, np_array
310
+ gc.collect()
311
+ return image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
 
313
+ # Legacy function for backward compatibility
314
+ def get_face_features(image):
315
+ """Legacy wrapper - now uses DeepFace internally"""
316
+ return None
 
 
 
 
 
317
 
318
  def recognize_face(image, user_id, user_type='student'):
319
+ """Legacy wrapper for the new DeepFace recognition"""
320
  return recognize_face_deepface(image, user_id, user_type)
321
 
322
+ # ---------------------- Metrics helpers ----------------------
323
  def log_metrics_event(event: dict):
324
  try:
325
+ metrics_events.insert_one(event)
 
326
  except Exception as e:
327
+ print("Failed to log metrics event:", e)
328
+
329
+ def log_metrics_event_normalized(
330
+ *,
331
+ event: str,
332
+ attempt_type: str,
333
+ claimed_id: Optional[str],
334
+ recognized_id: Optional[str],
335
+ liveness_pass: bool,
336
+ distance: Optional[float],
337
+ live_prob: Optional[float],
338
+ latency_ms: Optional[float],
339
+ client_ip: Optional[str],
340
+ reason: Optional[str] = None
341
+ ):
342
  if not liveness_pass:
343
  decision = "spoof_blocked"
344
  else:
 
360
  }
361
  log_metrics_event(doc)
362
 
363
+ def classify_event(ev: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
364
+ """Returns (event, attempt_type), robust to legacy documents."""
365
+ if ev.get("event"):
366
+ e = ev.get("event")
367
+ at = ev.get("attempt_type")
368
+ if not at:
369
+ if e in ("accept_true", "reject_false"):
370
+ at = "genuine"
371
+ elif e in ("accept_false", "reject_true"):
372
+ at = "impostor"
373
+ return e, at
374
+
375
+ decision = ev.get("decision")
376
+ success = ev.get("success")
377
+ reason = (ev.get("reason") or "") if isinstance(ev.get("reason"), str) else ev.get("reason")
378
+
379
+ if decision == "recognized" and (success is True or success is None):
380
+ return "accept_true", "genuine"
381
+
382
+ if decision == "spoof_blocked":
383
+ return "reject_true", "impostor"
384
+
385
+ if decision == "not_recognized":
386
+ if reason in ("false_reject",):
387
+ return "reject_false", "genuine"
388
+ if reason in ("unauthorized_attempt", "liveness_fail", "mismatch_claim", "no_face_detected", "failed_crop", "recognition_error"):
389
+ return "reject_true", "impostor"
390
+ return "reject_true", "impostor"
391
+
392
+ return None, None
393
+
394
+ def compute_metrics(limit: int = 10000):
395
+ """Robust metrics aggregation that tolerates legacy docs."""
396
+ try:
397
+ cursor = metrics_events.find({}, {"_id": 0}).sort("ts", -1).limit(limit)
398
+ counts = {
399
+ "trueAccepts": 0,
400
+ "falseAccepts": 0,
401
+ "trueRejects": 0,
402
+ "falseRejects": 0,
403
+ "genuineAttempts": 0,
404
+ "impostorAttempts": 0,
405
+ "unauthorizedRejected": 0,
406
+ "unauthorizedAccepted": 0,
407
+ }
408
+
409
+ total_attempts_calc = 0
410
+
411
+ for ev in cursor:
412
+ e, at = classify_event(ev)
413
+ if not e:
414
+ continue
415
+ total_attempts_calc += 1
416
+
417
+ if e == "accept_true":
418
+ counts["trueAccepts"] += 1
419
+ elif e == "accept_false":
420
+ counts["falseAccepts"] += 1
421
+ counts["unauthorizedAccepted"] += 1
422
+ elif e == "reject_true":
423
+ counts["trueRejects"] += 1
424
+ counts["unauthorizedRejected"] += 1
425
+ elif e == "reject_false":
426
+ counts["falseRejects"] += 1
427
+
428
+ if at == "genuine":
429
+ counts["genuineAttempts"] += 1
430
+ elif at == "impostor":
431
+ counts["impostorAttempts"] += 1
432
+
433
+ genuine_attempts = max(counts["genuineAttempts"], 1)
434
+ impostor_attempts = max(counts["impostorAttempts"], 1)
435
+ total_attempts_final = max(total_attempts_calc, 1)
436
+
437
+ FAR = counts["falseAccepts"] / impostor_attempts
438
+ FRR = counts["falseRejects"] / genuine_attempts
439
+ accuracy = (counts["trueAccepts"] + counts["trueRejects"]) / total_attempts_final
440
+
441
+ return {
442
+ "counts": counts,
443
+ "rates": {
444
+ "FAR": FAR,
445
+ "FRR": FRR,
446
+ "accuracy": accuracy
447
+ },
448
+ "totals": {
449
+ "totalAttempts": total_attempts_calc
450
+ }
451
+ }
452
+ except Exception as e:
453
+ print(f"Error computing metrics: {e}")
454
+ return {
455
+ "counts": {"trueAccepts": 0, "falseAccepts": 0, "trueRejects": 0, "falseRejects": 0,
456
+ "genuineAttempts": 0, "impostorAttempts": 0, "unauthorizedRejected": 0, "unauthorizedAccepted": 0},
457
+ "rates": {"FAR": 0, "FRR": 0, "accuracy": 0},
458
+ "totals": {"totalAttempts": 0}
459
+ }
460
+
461
+ def compute_latency_avg(limit: int = 300) -> Optional[float]:
462
+ try:
463
+ cursor = metrics_events.find({"latency_ms": {"$exists": True}}, {"latency_ms": 1, "_id": 0}).sort("ts", -1).limit(limit)
464
+ vals = [float(d["latency_ms"]) for d in cursor if isinstance(d.get("latency_ms"), (int, float))]
465
+ if not vals:
466
+ return None
467
+ return sum(vals) / len(vals)
468
+ except Exception as e:
469
+ print(f"Error computing latency average: {e}")
470
+ return None
471
 
472
+ # --------- STUDENT ROUTES ---------
473
  @app.route('/')
474
  def home():
475
  return render_template('home.html')
 
483
  return render_template('register.html')
484
 
485
  @app.route('/metrics')
 
486
  def metrics_dashboard():
487
  return render_template('metrics.html')
488
 
 
 
 
 
 
 
 
 
 
489
  @app.route('/register', methods=['POST'])
490
  def register():
491
  try:
 
 
 
 
492
  student_data = {
493
  'student_id': request.form.get('student_id'),
494
  'name': request.form.get('name'),
 
503
  'password': request.form.get('password'),
504
  'created_at': datetime.now()
505
  }
 
506
  face_image = request.form.get('face_image')
507
  if face_image and ',' in face_image:
508
  image_data = face_image.split(',')[1]
 
519
  else:
520
  flash('Registration failed. Please try again.', 'danger')
521
  return redirect(url_for('register_page'))
 
522
  except pymongo.errors.DuplicateKeyError:
523
  flash('Student ID already exists. Please use a different ID.', 'danger')
524
  return redirect(url_for('register_page'))
525
  except Exception as e:
 
526
  flash(f'Registration failed: {str(e)}', 'danger')
527
  return redirect(url_for('register_page'))
528
 
529
  @app.route('/login', methods=['POST'])
530
  def login():
531
+ student_id = request.form.get('student_id')
532
+ password = request.form.get('password')
533
+ student = students_collection.find_one({'student_id': student_id})
534
+
535
+ if student and student['password'] == password:
536
+ session['logged_in'] = True
537
+ session['user_type'] = 'student'
538
+ session['student_id'] = student_id
539
+ session['name'] = student.get('name')
540
+ flash('Login successful!', 'success')
541
+ return redirect(url_for('dashboard'))
542
+ else:
543
+ flash('Invalid credentials. Please try again.', 'danger')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
  return redirect(url_for('login_page'))
545
 
546
  @app.route('/face-login', methods=['POST'])
547
  def face_login():
548
+ face_image = request.form.get('face_image')
549
+ face_role = request.form.get('face_role')
 
 
 
 
 
550
 
551
+ if not face_image or not face_role:
552
+ flash('Face image and role are required for face login.', 'danger')
553
+ return redirect(url_for('login_page'))
554
 
555
+ image = decode_image(face_image)
 
 
 
556
 
557
+ if face_role == 'student':
558
+ collection = students_collection
559
+ id_field = 'student_id'
560
+ dashboard_route = 'dashboard'
561
+ elif face_role == 'teacher':
562
+ collection = teachers_collection
563
+ id_field = 'teacher_id'
564
+ dashboard_route = 'teacher_dashboard'
565
+ else:
566
+ flash('Invalid role selected for face login.', 'danger')
567
+ return redirect(url_for('login_page'))
568
 
569
+ users = collection.find({'face_image': {'$exists': True, '$ne': None}})
570
+
571
+ # Use DeepFace for face matching with improved temp file handling
572
+ temp_login_path = get_unique_temp_path("login_image")
573
+ cv2.imwrite(temp_login_path, image)
574
+
575
+ try:
576
+ from deepface import DeepFace
577
 
 
 
 
 
578
  for user in users:
579
+ ref_image_bytes = user['face_image']
580
+ ref_image_array = np.frombuffer(ref_image_bytes, np.uint8)
581
+ ref_image = cv2.imdecode(ref_image_array, cv2.IMREAD_COLOR)
582
+
583
+ temp_ref_path = get_unique_temp_path(f"ref_{user[id_field]}")
584
+ cv2.imwrite(temp_ref_path, ref_image)
585
+
586
  try:
587
+ result = DeepFace.verify(
588
+ img1_path=temp_login_path,
589
+ img2_path=temp_ref_path,
590
+ model_name="Facenet",
591
+ enforce_detection=False
592
+ )
593
 
594
+ if result["verified"]:
 
 
 
 
 
 
 
595
  session['logged_in'] = True
596
  session['user_type'] = face_role
597
  session[id_field] = user[id_field]
598
  session['name'] = user.get('name')
 
 
599
  flash('Face login successful!', 'success')
 
600
 
601
+ # Cleanup
602
+ for temp_file in [temp_ref_path, temp_login_path]:
603
+ if os.path.exists(temp_file):
604
+ os.remove(temp_file)
605
+ gc.collect()
606
+ return redirect(url_for(dashboard_route))
607
+
608
+ if os.path.exists(temp_ref_path):
609
+ os.remove(temp_ref_path)
610
  except Exception as e:
611
+ if os.path.exists(temp_ref_path):
612
+ os.remove(temp_ref_path)
613
  continue
 
 
 
614
 
615
+ if os.path.exists(temp_login_path):
616
+ os.remove(temp_login_path)
617
+
618
  except Exception as e:
619
+ if os.path.exists(temp_login_path):
620
+ os.remove(temp_login_path)
621
+ finally:
622
+ gc.collect()
623
+
624
+ flash('Face not recognized. Please try again or contact admin.', 'danger')
625
+ return redirect(url_for('login_page'))
626
 
627
  @app.route('/auto-face-login', methods=['POST'])
628
  def auto_face_login():
629
+ """Enhanced auto face login with role support"""
630
  try:
 
 
 
631
  data = request.json
632
  face_image = data.get('face_image')
633
  face_role = data.get('face_role', 'student')
 
634
  if not face_image:
635
  return jsonify({'success': False, 'message': 'No image received'})
636
+
637
  image = decode_image(face_image)
 
638
 
 
 
 
639
  if face_role == 'teacher':
640
  collection = teachers_collection
641
  id_field = 'teacher_id'
 
645
  id_field = 'student_id'
646
  dashboard_route = '/dashboard'
647
 
648
+ # Use DeepFace for recognition with improved temp file handling
649
+ temp_auto_path = get_unique_temp_path("auto_login")
650
+ cv2.imwrite(temp_auto_path, image)
651
 
652
+ try:
653
+ from deepface import DeepFace
654
+
655
+ users = collection.find({'face_image': {'$exists': True, '$ne': None}})
656
+ for user in users:
657
+ try:
658
+ ref_image_array = np.frombuffer(user['face_image'], np.uint8)
659
+ ref_image = cv2.imdecode(ref_image_array, cv2.IMREAD_COLOR)
 
 
 
 
 
 
 
 
 
 
 
660
 
661
+ temp_ref_path = get_unique_temp_path(f"auto_ref_{user[id_field]}")
662
+ cv2.imwrite(temp_ref_path, ref_image)
 
 
 
 
663
 
664
+ result = DeepFace.verify(
665
+ img1_path=temp_auto_path,
666
+ img2_path=temp_ref_path,
667
+ model_name="Facenet",
668
+ enforce_detection=False
669
+ )
670
+
671
+ if result["verified"]:
672
+ session['logged_in'] = True
673
+ session['user_type'] = face_role
674
+ session[id_field] = user[id_field]
675
+ session['name'] = user.get('name')
676
+
677
+ # Cleanup
678
+ for temp_file in [temp_ref_path, temp_auto_path]:
679
+ if os.path.exists(temp_file):
680
+ os.remove(temp_file)
681
+
682
+ gc.collect()
683
+ return jsonify({
684
+ 'success': True,
685
+ 'message': f'Welcome {user["name"]}! Redirecting...',
686
+ 'redirect_url': dashboard_route,
687
+ 'face_role': face_role
688
+ })
689
+
690
+ if os.path.exists(temp_ref_path):
691
+ os.remove(temp_ref_path)
692
+ except Exception as e:
693
+ continue
694
+
695
+ if os.path.exists(temp_auto_path):
696
+ os.remove(temp_auto_path)
697
+
698
+ except Exception as e:
699
+ if os.path.exists(temp_auto_path):
700
+ os.remove(temp_auto_path)
701
+ finally:
702
+ gc.collect()
703
 
704
  return jsonify({'success': False, 'message': f'Face not recognized in {face_role} database'})
 
705
  except Exception as e:
706
+ print(f"Auto face login error: {e}")
707
  return jsonify({'success': False, 'message': 'Login failed due to server error'})
708
 
709
  @app.route('/attendance.html')
 
710
  def attendance_page():
711
+ if 'logged_in' not in session or session.get('user_type') != 'student':
712
+ return redirect(url_for('login_page'))
713
  student_id = session.get('student_id')
714
  student = students_collection.find_one({'student_id': student_id})
715
  return render_template('attendance.html', student=student)
716
 
717
  @app.route('/dashboard')
 
718
  def dashboard():
719
+ if 'logged_in' not in session or session.get('user_type') != 'student':
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
720
  return redirect(url_for('login_page'))
721
+ student_id = session.get('student_id')
722
+ student = students_collection.find_one({'student_id': student_id})
723
+ if student and 'face_image' in student and student['face_image']:
724
+ face_image_base64 = base64.b64encode(student['face_image']).decode('utf-8')
725
+ mime_type = student.get('face_image_type', 'image/jpeg')
726
+ student['face_image_url'] = f"data:{mime_type};base64,{face_image_base64}"
727
+ attendance_records = list(attendance_collection.find({'student_id': student_id}).sort('date', -1))
728
+ return render_template('dashboard.html', student=student, attendance_records=attendance_records)
729
 
730
  @app.route('/mark-attendance', methods=['POST'])
 
731
  def mark_attendance():
732
+ if 'logged_in' not in session or session.get('user_type') != 'student':
733
+ return jsonify({'success': False, 'message': 'Not logged in'})
734
+
735
+ data = request.json
736
+ student_id = session.get('student_id') or data.get('student_id')
737
+ program = data.get('program')
738
+ semester = data.get('semester')
739
+ course = data.get('course')
740
+ face_image = data.get('face_image')
741
+
742
+ if not all([student_id, program, semester, course, face_image]):
743
+ return jsonify({'success': False, 'message': 'Missing required data'})
744
+
745
+ client_ip = request.remote_addr
746
+ t0 = time.time()
747
+
748
+ # Decode image
749
+ image = decode_image(face_image)
750
+ if image is None or image.size == 0:
751
+ return jsonify({'success': False, 'message': 'Invalid image data'})
752
+
753
+ h, w = image.shape[:2]
754
+ vis = image.copy()
755
+
756
+ # 1) Face detection using reliable methods
757
+ detections = detect_faces_yunet(image)
758
+ if not detections:
759
+ overlay = image_to_data_uri(vis)
760
+ log_metrics_event_normalized(
761
+ event="reject_true",
762
+ attempt_type="impostor",
763
+ claimed_id=student_id,
764
+ recognized_id=None,
765
+ liveness_pass=False,
766
+ distance=None,
767
+ live_prob=None,
768
+ latency_ms=round((time.time() - t0) * 1000.0, 2),
769
+ client_ip=client_ip,
770
+ reason="no_face_detected"
771
+ )
772
+ return jsonify({'success': False, 'message': 'No face detected for liveness', 'overlay': overlay})
773
+
774
+ # Pick highest-score detection
775
+ best = max(detections, key=lambda d: d["score"])
776
+ x1, y1, x2, y2 = [int(v) for v in best["bbox"]]
777
+ x1e, y1e, x2e, y2e = expand_and_clip_box((x1, y1, x2, y2), scale=1.2, w=w, h=h)
778
+ face_crop = image[y1e:y2e, x1e:x2e]
779
+ if face_crop.size == 0:
780
+ overlay = image_to_data_uri(vis)
781
+ log_metrics_event_normalized(
782
+ event="reject_true",
783
+ attempt_type="impostor",
784
+ claimed_id=student_id,
785
+ recognized_id=None,
786
+ liveness_pass=False,
787
+ distance=None,
788
+ live_prob=None,
789
+ latency_ms=round((time.time() - t0) * 1000.0, 2),
790
+ client_ip=client_ip,
791
+ reason="failed_crop"
792
+ )
793
+ return jsonify({'success': False, 'message': 'Failed to crop face for liveness', 'overlay': overlay})
794
+
795
+ # 2) Simple liveness check (lightweight)
796
+ live_prob = simple_liveness_check(face_crop)
797
+ is_live = live_prob >= 0.7
798
+ label = "LIVE" if is_live else "SPOOF"
799
+ color = (0, 200, 0) if is_live else (0, 0, 255)
800
+ draw_live_overlay(vis, (x1e, y1e, x2e, y2e), label, live_prob, color)
801
+ overlay_data = image_to_data_uri(vis)
802
+
803
+ if not is_live:
804
+ log_metrics_event_normalized(
805
+ event="reject_true",
806
+ attempt_type="impostor",
807
+ claimed_id=student_id,
808
+ recognized_id=None,
809
+ liveness_pass=False,
810
+ distance=None,
811
+ live_prob=float(live_prob),
812
+ latency_ms=round((time.time() - t0) * 1000.0, 2),
813
+ client_ip=client_ip,
814
+ reason="liveness_fail"
815
+ )
816
+ return jsonify({'success': False, 'message': f'Spoof detected or face not live (p={live_prob:.2f}).', 'overlay': overlay_data})
817
 
818
+ # 3) Face recognition using DeepFace
819
+ success, message = recognize_face_deepface(image, student_id, user_type='student')
820
+ total_latency_ms = round((time.time() - t0) * 1000.0, 2)
 
 
 
 
 
 
 
821
 
822
+ # Parse distance from message if available
823
+ distance_val = None
824
+ try:
825
+ if "distance=" in message:
826
+ part = message.split("distance=")[1]
827
+ distance_val = float(part.split(",")[0].strip(") "))
828
+ except Exception:
829
+ pass
 
 
 
 
 
 
830
 
831
+ # Derive reason string
832
+ reason = None
833
+ if not success:
834
+ if message.startswith("Unauthorized attempt"):
835
+ reason = "unauthorized_attempt"
836
+ elif message.startswith("No face detected"):
837
+ reason = "no_face_detected"
838
+ elif message.startswith("False reject"):
839
+ reason = "false_reject"
840
+ elif message.startswith("Error in face recognition") or message.startswith("DeepFace"):
841
+ reason = "recognition_error"
842
+ else:
843
+ reason = "not_recognized"
844
+
845
+ # Log event
846
+ if success:
847
+ log_metrics_event_normalized(
848
+ event="accept_true",
849
+ attempt_type="genuine",
850
+ claimed_id=student_id,
851
+ recognized_id=student_id,
852
+ liveness_pass=True,
853
+ distance=distance_val,
854
+ live_prob=float(live_prob),
855
+ latency_ms=total_latency_ms,
856
+ client_ip=client_ip,
857
+ reason=None
858
+ )
859
 
860
+ # Save attendance
861
+ attendance_data = {
862
+ 'student_id': student_id,
863
+ 'program': program,
864
+ 'semester': semester,
865
+ 'subject': course,
866
+ 'date': datetime.now().date().isoformat(),
867
+ 'time': datetime.now().time().strftime('%H:%M:%S'),
868
+ 'status': 'present',
869
+ 'created_at': datetime.now()
870
+ }
 
 
 
 
 
 
 
871
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
872
  existing_attendance = attendance_collection.find_one({
873
  'student_id': student_id,
874
  'subject': course,
875
  'date': datetime.now().date().isoformat()
876
  })
 
877
  if existing_attendance:
878
  return jsonify({'success': False, 'message': 'Attendance already marked for this course today', 'overlay': overlay_data})
 
 
 
 
 
 
 
 
 
 
 
 
879
  attendance_collection.insert_one(attendance_data)
880
+ gc.collect() # Clean up memory after successful operation
881
  return jsonify({'success': True, 'message': 'Attendance marked successfully', 'overlay': overlay_data})
882
+ except Exception as e:
883
+ return jsonify({'success': False, 'message': f'Database error: {str(e)}', 'overlay': overlay_data})
884
+ else:
885
+ if reason == "false_reject":
886
+ log_metrics_event_normalized(
887
+ event="reject_false",
888
+ attempt_type="genuine",
889
+ claimed_id=student_id,
890
+ recognized_id=student_id,
891
+ liveness_pass=True,
892
+ distance=distance_val,
893
+ live_prob=float(live_prob),
894
+ latency_ms=total_latency_ms,
895
+ client_ip=client_ip,
896
+ reason=reason
897
+ )
898
  else:
 
 
 
 
 
 
 
899
  log_metrics_event_normalized(
900
+ event="reject_true",
901
+ attempt_type="impostor",
902
+ claimed_id=student_id,
903
+ recognized_id=None,
904
+ liveness_pass=True,
905
+ distance=distance_val,
906
+ live_prob=float(live_prob),
907
+ latency_ms=total_latency_ms,
908
+ client_ip=client_ip,
909
+ reason=reason
910
  )
911
+ return jsonify({'success': False, 'message': message, 'overlay': overlay_data})
 
 
 
 
912
 
913
  @app.route('/liveness-preview', methods=['POST'])
 
914
  def liveness_preview():
915
+ if 'logged_in' not in session or session.get('user_type') != 'student':
916
+ return jsonify({'success': False, 'message': 'Not logged in'})
917
  try:
 
 
 
918
  data = request.json or {}
919
  face_image = data.get('face_image')
920
  if not face_image:
921
  return jsonify({'success': False, 'message': 'No image received'})
922
+
923
  image = decode_image(face_image)
924
  if image is None or image.size == 0:
925
  return jsonify({'success': False, 'message': 'Invalid image data'})
926
+
927
  h, w = image.shape[:2]
928
  vis = image.copy()
929
+ detections = detect_faces_yunet(image)
930
 
931
  if not detections:
932
  overlay_data = image_to_data_uri(vis)
 
937
  'message': 'No face detected',
938
  'overlay': overlay_data
939
  })
940
+
941
  best = max(detections, key=lambda d: d["score"])
942
  x1, y1, x2, y2 = [int(v) for v in best["bbox"]]
943
  x1e, y1e, x2e, y2e = expand_and_clip_box((x1, y1, x2, y2), scale=1.2, w=w, h=h)
 
953
  'overlay': overlay_data
954
  })
955
 
956
+ live_prob = simple_liveness_check(face_crop)
 
 
 
957
  threshold = 0.7
958
  label = "LIVE" if live_prob >= threshold else "SPOOF"
959
  color = (0, 200, 0) if label == "LIVE" else (0, 0, 255)
960
+
961
  draw_live_overlay(vis, (x1e, y1e, x2e, y2e), label, live_prob, color)
962
  overlay_data = image_to_data_uri(vis)
963
 
964
+ # Clean up memory
965
+ del image, vis, face_crop
966
+ gc.collect()
967
+
968
  return jsonify({
969
  'success': True,
970
  'live': bool(live_prob >= threshold),
971
  'live_prob': float(live_prob),
972
  'overlay': overlay_data
973
  })
 
974
  except Exception as e:
975
+ print("liveness_preview error:", e)
976
  return jsonify({'success': False, 'message': 'Server error during preview'})
977
 
978
+ # --------- TEACHER ROUTES ---------
979
  @app.route('/teacher_register.html')
980
  def teacher_register_page():
981
  return render_template('teacher_register.html')
 
987
  @app.route('/teacher_register', methods=['POST'])
988
  def teacher_register():
989
  try:
 
 
 
 
990
  teacher_data = {
991
  'teacher_id': request.form.get('teacher_id'),
992
  'name': request.form.get('name'),
 
999
  'password': request.form.get('password'),
1000
  'created_at': datetime.now()
1001
  }
 
1002
  face_image = request.form.get('face_image')
1003
  if face_image and ',' in face_image:
1004
  image_data = face_image.split(',')[1]
 
1007
  else:
1008
  flash('Face image is required for registration.', 'danger')
1009
  return redirect(url_for('teacher_register_page'))
 
1010
  result = teachers_collection.insert_one(teacher_data)
1011
  if result.inserted_id:
1012
  flash('Registration successful! You can now login.', 'success')
 
1014
  else:
1015
  flash('Registration failed. Please try again.', 'danger')
1016
  return redirect(url_for('teacher_register_page'))
 
1017
  except pymongo.errors.DuplicateKeyError:
1018
  flash('Teacher ID already exists. Please use a different ID.', 'danger')
1019
  return redirect(url_for('teacher_register_page'))
1020
  except Exception as e:
 
1021
  flash(f'Registration failed: {str(e)}', 'danger')
1022
  return redirect(url_for('teacher_register_page'))
1023
 
1024
  @app.route('/teacher_login', methods=['POST'])
1025
  def teacher_login():
1026
+ teacher_id = request.form.get('teacher_id')
1027
+ password = request.form.get('password')
1028
+ teacher = teachers_collection.find_one({'teacher_id': teacher_id})
1029
+ if teacher and teacher['password'] == password:
1030
+ session['logged_in'] = True
1031
+ session['user_type'] = 'teacher'
1032
+ session['teacher_id'] = teacher_id
1033
+ session['name'] = teacher.get('name')
1034
+ flash('Login successful!', 'success')
1035
+ return redirect(url_for('teacher_dashboard'))
1036
+ else:
1037
+ flash('Invalid credentials. Please try again.', 'danger')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1038
  return redirect(url_for('teacher_login_page'))
1039
 
1040
  @app.route('/teacher_dashboard')
 
1041
  def teacher_dashboard():
1042
+ if 'logged_in' not in session or session.get('user_type') != 'teacher':
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1043
  return redirect(url_for('teacher_login_page'))
1044
+ teacher_id = session.get('teacher_id')
1045
+ teacher = teachers_collection.find_one({'teacher_id': teacher_id})
1046
+ if teacher and 'face_image' in teacher and teacher['face_image']:
1047
+ face_image_base64 = base64.b64encode(teacher['face_image']).decode('utf-8')
1048
+ mime_type = teacher.get('face_image_type', 'image/jpeg')
1049
+ teacher['face_image_url'] = f"data:{mime_type};base64,{face_image_base64}"
1050
+ return render_template('teacher_dashboard.html', teacher=teacher)
1051
 
1052
  @app.route('/teacher_logout')
1053
  def teacher_logout():
 
1055
  flash('You have been logged out', 'info')
1056
  return redirect(url_for('teacher_login_page'))
1057
 
1058
+ # --------- COMMON LOGOUT ---------
1059
  @app.route('/logout')
1060
  def logout():
1061
  session.clear()
1062
  flash('You have been logged out', 'info')
1063
  return redirect(url_for('login_page'))
1064
 
1065
+ # --------- METRICS JSON ENDPOINTS ---------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1066
  @app.route('/metrics-data', methods=['GET'])
 
1067
  def metrics_data():
1068
  data = compute_metrics()
1069
  try:
1070
  recent = list(metrics_events.find({}, {"_id": 0}).sort("ts", -1).limit(200))
1071
+ normalized_recent = []
1072
  for r in recent:
1073
  if isinstance(r.get("ts"), datetime):
1074
  r["ts"] = r["ts"].isoformat()
1075
+ event, attempt_type = classify_event(r)
1076
+ if event and not r.get("event"):
1077
+ r["event"] = event
1078
+ if attempt_type and not r.get("attempt_type"):
1079
+ r["attempt_type"] = attempt_type
1080
+ if "liveness_pass" not in r:
1081
+ if r.get("decision") == "spoof_blocked":
1082
+ r["liveness_pass"] = False
1083
+ elif isinstance(r.get("live_prob"), (int, float)):
1084
+ r["liveness_pass"] = bool(r["live_prob"] >= 0.7)
1085
+ else:
1086
+ r["liveness_pass"] = None
1087
+ normalized_recent.append(r)
1088
+
1089
+ data["recent"] = normalized_recent
1090
  except Exception as e:
1091
+ print(f"Error getting recent metrics: {e}")
1092
  data["recent"] = []
1093
 
1094
  data["avg_latency_ms"] = compute_latency_avg()
1095
  return jsonify(data)
1096
 
1097
  @app.route('/metrics-json')
 
1098
  def metrics_json():
1099
  m = compute_metrics()
1100
  counts = m["counts"]
1101
  rates = m["rates"]
1102
  totals = m["totals"]
1103
  avg_latency = compute_latency_avg()
1104
+ accuracy_pct = rates["accuracy"] * 100.0
1105
+ far_pct = rates["FAR"] * 100.0
1106
+ frr_pct = rates["FRR"] * 100.0
 
1107
 
1108
  return jsonify({
1109
  'Accuracy': f"{accuracy_pct:.2f}%" if totals["totalAttempts"] > 0 else "N/A",
1110
+ 'False Accepts (FAR)': f"{far_pct:.2f}%" if counts["impostorAttempts"] > 0 else "N/A",
1111
+ 'False Rejects (FRR)': f"{frr_pct:.2f}%" if counts["genuineAttempts"] > 0 else "N/A",
1112
  'Average Inference Time (s)': f"{(avg_latency/1000.0):.2f}" if isinstance(avg_latency, (int, float)) else "N/A",
1113
+ 'Correct Recognitions': counts["trueAccepts"],
1114
  'Total Attempts': totals["totalAttempts"],
1115
+ 'Unauthorized Attempts': counts["unauthorizedRejected"],
1116
+ 'enhanced': {
1117
+ 'totals': {
1118
+ 'attempts': totals["totalAttempts"],
1119
+ 'trueAccepts': counts["trueAccepts"],
1120
+ 'falseAccepts': counts["falseAccepts"],
1121
+ 'trueRejects': counts["trueRejects"],
1122
+ 'falseRejects': counts["falseRejects"],
1123
+ 'genuineAttempts': counts["genuineAttempts"],
1124
+ 'impostorAttempts': counts["impostorAttempts"],
1125
+ 'unauthorizedRejected': counts["unauthorizedRejected"],
1126
+ 'unauthorizedAccepted': counts["unauthorizedAccepted"],
1127
+ },
1128
+ 'accuracy_pct': round(accuracy_pct, 2),
1129
+ 'avg_latency_ms': round(avg_latency, 2) if isinstance(avg_latency, (int, float)) else None
1130
+ }
1131
  })
1132
 
1133
+ @app.route('/metrics-events')
1134
+ def metrics_events_api():
1135
+ limit = int(request.args.get("limit", 200))
1136
+ try:
1137
+ cursor = metrics_events.find({}, {"_id": 0}).sort("ts", -1).limit(limit)
1138
+ events = list(cursor)
1139
+ for ev in events:
1140
+ if isinstance(ev.get("ts"), datetime):
1141
+ ev["ts"] = ev["ts"].isoformat()
1142
+ return jsonify(events)
1143
+ except Exception as e:
1144
+ print(f"Error getting metrics events: {e}")
1145
+ return jsonify([])
1146
+
1147
+ # Health check endpoint for Hugging Face
1148
+ @app.route('/health')
1149
+ def health_check():
1150
+ return jsonify({
1151
+ 'status': 'healthy',
1152
+ 'platform': 'hugging_face',
1153
+ 'memory': 'optimized',
1154
+ 'face_detector': 'haar_cascade',
1155
+ 'timestamp': datetime.now().isoformat()
1156
+ }), 200
1157
+
1158
+ # Cleanup function to be called periodically
1159
+ @app.route('/cleanup', methods=['POST'])
1160
+ def manual_cleanup():
1161
+ """Manual cleanup endpoint for memory management"""
1162
+ try:
1163
+ gc.collect()
1164
+ return jsonify({'status': 'cleanup completed'}), 200
1165
+ except Exception as e:
1166
+ return jsonify({'status': 'cleanup failed', 'error': str(e)}), 500
1167
+
1168
+ # HUGGING FACE SPECIFIC: Updated port to 7860
1169
  if __name__ == '__main__':
1170
+ port = int(os.environ.get('PORT', 7860)) # Hugging Face uses port 7860
1171
  app.run(host='0.0.0.0', port=port, debug=False)
requirements.txt CHANGED
@@ -1,11 +1,14 @@
1
  Flask==2.3.3
2
- pymongo==4.5.0
3
  python-dotenv==1.0.0
4
  opencv-python-headless==4.8.1.78
5
- tensorflow==2.13.0
6
- deepface==0.0.79
7
- scikit-learn==1.3.0
8
  numpy==1.24.3
9
- onnxruntime==1.15.1
10
- requests==2.31.0
11
  Pillow==10.0.0
 
 
 
 
 
 
 
 
 
1
  Flask==2.3.3
2
+ pymongo==4.6.0
3
  python-dotenv==1.0.0
4
  opencv-python-headless==4.8.1.78
 
 
 
5
  numpy==1.24.3
 
 
6
  Pillow==10.0.0
7
+ tensorflow-cpu==2.15.1
8
+ deepface==0.0.79
9
+ requests==2.32.3
10
+ pandas==2.2.2
11
+ scipy==1.11.4
12
+ protobuf==4.25.3
13
+ fire==0.6.0
14
+ gunicorn==21.2.0