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

Fix attendance marking issues and enhance session management

Browse files
Files changed (3) hide show
  1. app.py +539 -682
  2. app/static/js/camera.js +615 -334
  3. requirements.txt +7 -7
app.py CHANGED
@@ -1,8 +1,14 @@
1
  import os
2
  import tempfile
3
  import secrets
 
 
4
 
5
- # FIX: Use system temp directory for DeepFace and models (guaranteed writable)
 
 
 
 
6
  deepface_cache = os.path.join(tempfile.gettempdir(), "deepface_cache")
7
  os.makedirs(deepface_cache, exist_ok=True)
8
  os.environ["DEEPFACE_HOME"] = deepface_cache
@@ -14,7 +20,6 @@ import pymongo
14
  from pymongo import MongoClient
15
  from bson.binary import Binary
16
  import base64
17
- from datetime import datetime, timezone
18
  from dotenv import load_dotenv
19
  import numpy as np
20
  import cv2
@@ -24,12 +29,32 @@ from deepface import DeepFace
24
  from sklearn.metrics.pairwise import cosine_similarity
25
  import tensorflow as tf
26
 
27
- # Optimize TensorFlow for HuggingFace Spaces
28
  tf.config.threading.set_intra_op_parallelism_threads(1)
29
  tf.config.threading.set_inter_op_parallelism_threads(1)
30
  os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
31
 
32
- # --- Evaluation Metrics Counters ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  total_attempts = 0
34
  correct_recognitions = 0
35
  false_accepts = 0
@@ -37,144 +62,74 @@ false_rejects = 0
37
  unauthorized_attempts = 0
38
  inference_times = []
39
 
40
- # Load environment variables
41
- load_dotenv()
42
-
43
- # Initialize Flask app
44
- app = Flask(__name__, static_folder='app/static', template_folder='app/templates')
45
-
46
- # CRITICAL FIX: Proper session configuration for HuggingFace Spaces
47
- app.secret_key = 'fixed-secret-key-for-testing-change-in-production-' + str(hash('face-attendance-app'))
48
-
49
- # Essential session settings for containerized environment
50
- app.config['SESSION_COOKIE_SECURE'] = False # MUST be False for HTTP
51
- app.config['SESSION_COOKIE_HTTPONLY'] = True
52
- app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
53
- app.config['SESSION_COOKIE_PATH'] = '/'
54
- app.config['SESSION_COOKIE_DOMAIN'] = None # Let Flask auto-detect
55
- app.config['PERMANENT_SESSION_LIFETIME'] = 3600 # 1 hour
56
-
57
- # Force session to work
58
- app.config['SESSION_TYPE'] = None # Use Flask's default signed cookies
59
-
60
- # MongoDB Connection
61
- try:
62
- mongo_uri = os.getenv('MONGO_URI', 'mongodb://localhost:27017/')
63
- client = MongoClient(mongo_uri)
64
- db = client['face_attendance_system']
65
- students_collection = db['students']
66
- teachers_collection = db['teachers']
67
- attendance_collection = db['attendance']
68
- metrics_events = db['metrics_events']
69
-
70
- # Indexes
71
- students_collection.create_index([("student_id", pymongo.ASCENDING)], unique=True)
72
- teachers_collection.create_index([("teacher_id", pymongo.ASCENDING)], unique=True)
73
- attendance_collection.create_index([
74
- ("student_id", pymongo.ASCENDING),
75
- ("date", pymongo.ASCENDING),
76
- ("subject", pymongo.ASCENDING)
77
- ])
78
- metrics_events.create_index([("ts", pymongo.DESCENDING)])
79
- metrics_events.create_index([("event", pymongo.ASCENDING)])
80
- metrics_events.create_index([("attempt_type", pymongo.ASCENDING)])
81
- print("MongoDB connection successful")
82
- except Exception as e:
83
- print(f"MongoDB connection error: {e}")
84
-
85
- # ---------------- Model Download Functions (FIXED) ----------------
86
-
87
- def download_file_from_google_drive(file_id, destination):
88
- """Download file from Google Drive - Fixed version"""
89
  try:
90
- if not os.path.exists(destination):
91
- print(f"Downloading {destination}...")
92
- # FIXED URL format without HTML entities
93
- url = f"https://drive.google.com/uc?export=download&id={file_id}"
94
-
95
- os.makedirs(os.path.dirname(destination), exist_ok=True)
96
-
97
- session_requests = requests.Session()
98
- response = session_requests.get(url, stream=True)
99
-
100
- # Handle Google Drive's virus scan warning for large files
101
- if 'download_warning' in response.text:
102
- for line in response.text.split('\n'):
103
- if 'confirm=' in line:
104
- confirm_token = line.split('confirm=')[1].split('&')[0]
105
- url = f"https://drive.google.com/uc?export=download&confirm={confirm_token}&id={file_id}"
106
- response = session_requests.get(url, stream=True)
107
- break
 
 
 
 
 
 
108
 
109
- if response.status_code == 200:
110
- with open(destination, 'wb') as f:
111
- for chunk in response.iter_content(chunk_size=8192):
112
- if chunk:
113
- f.write(chunk)
114
- print(f"Downloaded {destination}")
115
- return True
116
- else:
117
- print(f"Failed to download {destination}: HTTP {response.status_code}")
118
- return False
119
- else:
120
- print(f"{destination} already exists")
121
- return True
122
  except Exception as e:
123
- print(f"Error downloading {destination}: {e}")
 
124
  return False
125
 
126
- def setup_models():
127
- """Download and setup all required models - FIXED VERSION"""
128
- # FIX: Use temp directory for models (guaranteed writable)
129
- models_dir = os.path.join(tempfile.gettempdir(), 'models')
130
- antispoof_dir = os.path.join(models_dir, 'anti-spoofing')
131
-
132
- os.makedirs(models_dir, exist_ok=True)
133
- os.makedirs(antispoof_dir, exist_ok=True)
134
-
135
- # Model configurations with your Google Drive IDs
136
- models_config = {
137
- 'yolov5s-face.onnx': {
138
- 'drive_id': '1sybYq9GGriXN6sY8YV1-RXMeVqYzhDrV',
139
- 'path': os.path.join(models_dir, 'yolov5s-face.onnx'),
140
- 'required': True
141
- },
142
- 'AntiSpoofing_bin_1.5_128.onnx': {
143
- 'drive_id': '1nH5G7dAHFE2KlW_H65txc8GDKSB7Zpy4',
144
- 'path': os.path.join(antispoof_dir, 'AntiSpoofing_bin_1.5_128.onnx'),
145
- 'required': True
146
- }
147
- }
148
-
149
- print("Downloading models from Google Drive...")
150
-
151
- # Download YOLO model
152
- yolo_downloaded = download_file_from_google_drive(
153
- models_config['yolov5s-face.onnx']['drive_id'],
154
- models_config['yolov5s-face.onnx']['path']
155
- )
156
-
157
- # Download anti-spoofing model
158
- antispoof_downloaded = download_file_from_google_drive(
159
- models_config['AntiSpoofing_bin_1.5_128.onnx']['drive_id'],
160
- models_config['AntiSpoofing_bin_1.5_128.onnx']['path']
161
- )
162
-
163
- # Print final status
164
- print("\n" + "="*50)
165
- print("MODEL DOWNLOAD STATUS:")
166
- print(f"YOLO Face Model: {'✅ Available' if yolo_downloaded else '❌ Failed'}")
167
- print(f"Anti-Spoof Model: {'✅ Available' if antispoof_downloaded else '❌ Failed'}")
168
- print("="*50 + "\n")
169
-
170
- return yolo_downloaded, antispoof_downloaded, models_config
171
 
172
- # Initialize models on startup
173
- print("Setting up models...")
174
- yolo_available, antispoof_available, model_paths = setup_models()
175
 
176
- # ---------------- YOLO Face Detection (FIXED) ----------------
 
 
 
 
177
 
 
178
  def _get_providers():
179
  available = ort.get_available_providers()
180
  if "CUDAExecutionProvider" in available:
@@ -232,6 +187,7 @@ def _nms(boxes: np.ndarray, scores: np.ndarray, iou_threshold: float):
232
  order = order[inds + 1]
233
  return keep
234
 
 
235
  class YoloV5FaceDetector:
236
  def __init__(self, model_path: str, input_size: int = 640, conf_threshold: float = 0.3, iou_threshold: float = 0.45):
237
  if not os.path.exists(model_path):
@@ -243,9 +199,6 @@ class YoloV5FaceDetector:
243
  self.session = ort.InferenceSession(model_path, providers=_get_providers())
244
  self.input_name = self.session.get_inputs()[0].name
245
  self.output_names = [o.name for o in self.session.get_outputs()]
246
- shape = self.session.get_inputs()[0].shape
247
- if isinstance(shape[2], int):
248
- self.input_size = int(shape[2])
249
 
250
  @staticmethod
251
  def _xywh2xyxy(x: np.ndarray) -> np.ndarray:
@@ -263,14 +216,17 @@ class YoloV5FaceDetector:
263
  img = img.astype(np.float32) / 255.0
264
  img = np.transpose(img, (2, 0, 1))
265
  img = np.expand_dims(img, 0)
 
266
  preds = self.session.run(self.output_names, {self.input_name: img})[0]
267
  if preds.ndim == 3 and preds.shape[0] == 1:
268
  preds = preds[0]
269
  if preds.ndim != 2:
270
  raise RuntimeError(f"Unexpected YOLO output shape: {preds.shape}")
 
271
  num_attrs = preds.shape[1]
272
  has_landmarks = num_attrs >= 15
273
  boxes_xywh = preds[:, 0:4]
 
274
  if has_landmarks:
275
  scores = preds[:, 4]
276
  else:
@@ -281,11 +237,14 @@ class YoloV5FaceDetector:
281
  else:
282
  class_conf = cls_scores.max(axis=1, keepdims=True)
283
  scores = (obj * class_conf).squeeze(-1)
 
284
  keep = scores > self.conf_threshold
285
  boxes_xywh = boxes_xywh[keep]
286
  scores = scores[keep]
 
287
  if boxes_xywh.shape[0] == 0:
288
  return []
 
289
  boxes_xyxy = self._xywh2xyxy(boxes_xywh)
290
  boxes_xyxy[:, [0, 2]] -= dwdh[0]
291
  boxes_xyxy[:, [1, 3]] -= dwdh[1]
@@ -294,16 +253,17 @@ class YoloV5FaceDetector:
294
  boxes_xyxy[:, 1] = np.clip(boxes_xyxy[:, 1], 0, h0 - 1)
295
  boxes_xyxy[:, 2] = np.clip(boxes_xyxy[:, 2], 0, w0 - 1)
296
  boxes_xyxy[:, 3] = np.clip(boxes_xyxy[:, 3], 0, h0 - 1)
 
297
  keep_inds = _nms(boxes_xyxy, scores, self.iou_threshold)
298
  if len(keep_inds) > max_det:
299
  keep_inds = keep_inds[:max_det]
 
300
  dets = []
301
  for i in keep_inds:
302
  dets.append({"bbox": boxes_xyxy[i].tolist(), "score": float(scores[i])})
303
  return dets
304
 
305
- # ---------------- Anti-Spoof Model (FIXED) ----------------
306
-
307
  def _sigmoid(x: np.ndarray) -> np.ndarray:
308
  return 1.0 / (1.0 + np.exp(-x))
309
 
@@ -349,8 +309,7 @@ class AntiSpoofBinary:
349
  live_prob = float(_sigmoid(out.astype(np.float32)))
350
  return max(0.0, min(1.0, live_prob))
351
 
352
- # ---------------- Helper Functions ----------------
353
-
354
  def expand_and_clip_box(bbox_xyxy, scale: float, w: int, h: int):
355
  x1, y1, x2, y2 = bbox_xyxy
356
  bw = x2 - x1
@@ -389,34 +348,40 @@ def decode_image(base64_image):
389
  image = cv2.imdecode(np_array, cv2.IMREAD_COLOR)
390
  return image
391
 
392
- # Model paths (using temp directory)
393
- YOLO_FACE_MODEL_PATH = model_paths['yolov5s-face.onnx']['path']
394
- ANTI_SPOOF_BIN_MODEL_PATH = model_paths['AntiSpoofing_bin_1.5_128.onnx']['path']
395
-
396
- # Initialize models with timeout protection
397
  yolo_face = None
398
  anti_spoof_bin = None
399
 
400
- try:
401
- if yolo_available:
402
- yolo_face = YoloV5FaceDetector(YOLO_FACE_MODEL_PATH, input_size=640, conf_threshold=0.3, iou_threshold=0.45)
403
- print("YOLO Face model loaded successfully")
404
- else:
405
- print("Warning: YOLO Face model not available")
406
- except Exception as e:
407
- print(f"Error loading YOLO model: {e}")
408
-
409
- try:
410
- if antispoof_available:
411
- anti_spoof_bin = AntiSpoofBinary(ANTI_SPOOF_BIN_MODEL_PATH, input_size=128, rgb=True, normalize=True, live_index=1)
412
- print("Anti-spoofing model loaded successfully")
413
- else:
414
- print("Warning: Anti-spoofing model not available")
415
- except Exception as e:
416
- print(f"Error loading anti-spoofing model: {e}")
417
 
418
- # ----------------------------- DeepFace Recognition -----------------------------
 
 
 
 
 
 
 
 
 
419
 
 
 
 
 
420
  def get_face_features_deepface(image):
421
  """Extract face features using DeepFace with timeout protection"""
422
  try:
@@ -434,7 +399,7 @@ def get_face_features_deepface(image):
434
  return np.array(embedding['embedding']) if 'embedding' in embedding else None
435
 
436
  except Exception as e:
437
- print(f"Error in DeepFace feature extraction: {e}")
438
  return None
439
 
440
  def recognize_face_deepface(image, user_id, user_type='student'):
@@ -487,27 +452,18 @@ def recognize_face_deepface(image, user_id, user_type='student'):
487
  def recognize_face(image, user_id, user_type='student'):
488
  return recognize_face_deepface(image, user_id, user_type)
489
 
490
- # ---------------------- Metrics helpers ----------------------
491
-
492
  def log_metrics_event(event: dict):
493
  try:
494
- metrics_events.insert_one(event)
 
495
  except Exception as e:
496
- print("Failed to log metrics event:", e)
497
-
498
- def log_metrics_event_normalized(
499
- *,
500
- event: str,
501
- attempt_type: str,
502
- claimed_id: Optional[str],
503
- recognized_id: Optional[str],
504
- liveness_pass: bool,
505
- distance: Optional[float],
506
- live_prob: Optional[float],
507
- latency_ms: Optional[float],
508
- client_ip: Optional[str],
509
- reason: Optional[str] = None
510
- ):
511
  if not liveness_pass:
512
  decision = "spoof_blocked"
513
  else:
@@ -529,128 +485,28 @@ def log_metrics_event_normalized(
529
  }
530
  log_metrics_event(doc)
531
 
532
- def classify_event(ev: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
533
- if ev.get("event"):
534
- e = ev.get("event")
535
- at = ev.get("attempt_type")
536
- if not at:
537
- if e in ("accept_true", "reject_false"):
538
- at = "genuine"
539
- elif e in ("accept_false", "reject_true"):
540
- at = "impostor"
541
- return e, at
542
-
543
- decision = ev.get("decision")
544
- success = ev.get("success")
545
- reason = (ev.get("reason") or "") if isinstance(ev.get("reason"), str) else ev.get("reason")
546
-
547
- if decision == "recognized" and (success is True or success is None):
548
- return "accept_true", "genuine"
549
- if decision == "spoof_blocked":
550
- return "reject_true", "impostor"
551
- if decision == "not_recognized":
552
- if reason in ("false_reject",):
553
- return "reject_false", "genuine"
554
- if reason in ("unauthorized_attempt", "liveness_fail", "mismatch_claim", "no_face_detected", "failed_crop", "recognition_error"):
555
- return "reject_true", "impostor"
556
- return "reject_true", "impostor"
557
-
558
- return None, None
559
-
560
- def compute_metrics(limit: int = 10000):
561
- cursor = metrics_events.find({}, {"_id": 0}).sort("ts", -1).limit(limit)
562
- counts = {
563
- "trueAccepts": 0,
564
- "falseAccepts": 0,
565
- "trueRejects": 0,
566
- "falseRejects": 0,
567
- "genuineAttempts": 0,
568
- "impostorAttempts": 0,
569
- "unauthorizedRejected": 0,
570
- "unauthorizedAccepted": 0,
571
- }
572
-
573
- total_attempts_calc = 0
574
-
575
- for ev in cursor:
576
- e, at = classify_event(ev)
577
- if not e:
578
- continue
579
- total_attempts_calc += 1
580
-
581
- if e == "accept_true":
582
- counts["trueAccepts"] += 1
583
- elif e == "accept_false":
584
- counts["falseAccepts"] += 1
585
- counts["unauthorizedAccepted"] += 1
586
- elif e == "reject_true":
587
- counts["trueRejects"] += 1
588
- counts["unauthorizedRejected"] += 1
589
- elif e == "reject_false":
590
- counts["falseRejects"] += 1
591
-
592
- if at == "genuine":
593
- counts["genuineAttempts"] += 1
594
- elif at == "impostor":
595
- counts["impostorAttempts"] += 1
596
-
597
- genuine_attempts = max(counts["genuineAttempts"], 1)
598
- impostor_attempts = max(counts["impostorAttempts"], 1)
599
- total_attempts_final = max(total_attempts_calc, 1)
600
-
601
- FAR = counts["falseAccepts"] / impostor_attempts
602
- FRR = counts["falseRejects"] / genuine_attempts
603
- accuracy = (counts["trueAccepts"] + counts["trueRejects"]) / total_attempts_final
604
-
605
- return {
606
- "counts": counts,
607
- "rates": {
608
- "FAR": FAR,
609
- "FRR": FRR,
610
- "accuracy": accuracy
611
- },
612
- "totals": {
613
- "totalAttempts": total_attempts_calc
614
- }
615
- }
616
-
617
- def compute_latency_avg(limit: int = 300) -> Optional[float]:
618
- cursor = metrics_events.find({"latency_ms": {"$exists": True}}, {"latency_ms": 1, "_id": 0}).sort("ts", -1).limit(limit)
619
- vals = [float(d["latency_ms"]) for d in cursor if isinstance(d.get("latency_ms"), (int, float))]
620
- if not vals:
621
- return None
622
- return sum(vals) / len(vals)
623
-
624
- # --------- DEBUG ROUTES ---------
625
-
626
- @app.route('/debug-session')
627
- def debug_session():
628
- """Enhanced debug route to check session and cookie state"""
629
- import pprint
630
-
631
- debug_info = {
632
- 'session_data': dict(session),
633
- 'session_keys': list(session.keys()),
634
- 'session_permanent': session.permanent,
635
- 'session_modified': getattr(session, 'modified', 'unknown'),
636
- 'request_cookies': dict(request.cookies),
637
- 'flask_config': {
638
- 'SECRET_KEY': app.secret_key[:20] + '...' if app.secret_key else None,
639
- 'SESSION_COOKIE_SECURE': app.config.get('SESSION_COOKIE_SECURE'),
640
- 'SESSION_COOKIE_HTTPONLY': app.config.get('SESSION_COOKIE_HTTPONLY'),
641
- 'SESSION_COOKIE_SAMESITE': app.config.get('SESSION_COOKIE_SAMESITE'),
642
- 'SESSION_COOKIE_PATH': app.config.get('SESSION_COOKIE_PATH'),
643
- 'SESSION_COOKIE_DOMAIN': app.config.get('SESSION_COOKIE_DOMAIN'),
644
- }
645
- }
646
-
647
- print("[DEBUG] Session Debug Info:")
648
- pprint.pprint(debug_info)
649
-
650
- return jsonify(debug_info)
651
-
652
- # --------- ALL ROUTES ---------
653
 
 
654
  @app.route('/')
655
  def home():
656
  return render_template('home.html')
@@ -664,12 +520,26 @@ def register_page():
664
  return render_template('register.html')
665
 
666
  @app.route('/metrics')
 
667
  def metrics_dashboard():
668
  return render_template('metrics.html')
669
 
 
 
 
 
 
 
 
 
 
670
  @app.route('/register', methods=['POST'])
671
  def register():
672
  try:
 
 
 
 
673
  student_data = {
674
  'student_id': request.form.get('student_id'),
675
  'name': request.form.get('name'),
@@ -684,6 +554,7 @@ def register():
684
  'password': request.form.get('password'),
685
  'created_at': datetime.now()
686
  }
 
687
  face_image = request.form.get('face_image')
688
  if face_image and ',' in face_image:
689
  image_data = face_image.split(',')[1]
@@ -700,67 +571,66 @@ def register():
700
  else:
701
  flash('Registration failed. Please try again.', 'danger')
702
  return redirect(url_for('register_page'))
 
703
  except pymongo.errors.DuplicateKeyError:
704
  flash('Student ID already exists. Please use a different ID.', 'danger')
705
  return redirect(url_for('register_page'))
706
  except Exception as e:
 
707
  flash(f'Registration failed: {str(e)}', 'danger')
708
  return redirect(url_for('register_page'))
709
 
710
  @app.route('/login', methods=['POST'])
711
  def login():
712
- student_id = request.form.get('student_id')
713
- password = request.form.get('password')
714
-
715
- print(f"[DEBUG] Login attempt for student_id: {student_id}")
716
-
717
- if not student_id or not password:
718
- flash('Please enter both student ID and password.', 'danger')
719
- return redirect(url_for('login_page'))
720
-
721
- student = students_collection.find_one({'student_id': student_id})
722
-
723
- if student and student.get('password') == password:
724
- # CRITICAL FIX: Clear session and set data, then create response manually
725
- session.clear()
726
- session['logged_in'] = True
727
- session['user_type'] = 'student'
728
- session['student_id'] = student_id
729
- session['name'] = student.get('name')
730
- session.permanent = True
731
-
732
- print(f"[DEBUG] Session set after login: {dict(session)}")
733
- print(f"[DEBUG] Session modified: {session.modified}")
734
-
735
- # Create response manually to ensure cookie is set
736
- response = redirect(url_for('dashboard'))
737
 
738
- # Force session cookie to be set
739
- app.save_session(session, response)
 
740
 
741
- flash('Login successful!', 'success')
742
- return response
743
- else:
744
- print(f"[DEBUG] Login failed for student: {student_id}")
745
- flash('Invalid credentials. Please try again.', 'danger')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
746
  return redirect(url_for('login_page'))
747
 
748
  @app.route('/face-login', methods=['POST'])
749
  def face_login():
750
- print(f"[DEBUG] Face login attempt started")
751
-
752
  try:
 
 
 
 
753
  face_image = request.form.get('face_image')
754
  face_role = request.form.get('face_role')
755
 
756
  if not face_image or not face_role:
757
- print(f"[DEBUG] Missing face_image or face_role")
758
  flash('Face image and role are required for face login.', 'danger')
759
  return redirect(url_for('login_page'))
760
 
761
  image = decode_image(face_image)
762
  if image is None:
763
- print(f"[DEBUG] Invalid image data")
764
  flash('Invalid image data.', 'danger')
765
  return redirect(url_for('login_page'))
766
 
@@ -773,7 +643,6 @@ def face_login():
773
  id_field = 'teacher_id'
774
  dashboard_route = 'teacher_dashboard'
775
  else:
776
- print(f"[DEBUG] Invalid face_role: {face_role}")
777
  flash('Invalid role selected for face login.', 'danger')
778
  return redirect(url_for('login_page'))
779
 
@@ -781,12 +650,9 @@ def face_login():
781
  test_features = get_face_features_deepface(image)
782
 
783
  if test_features is None:
784
- print(f"[DEBUG] No face features extracted")
785
  flash('No face detected or processing failed. Please try again.', 'danger')
786
  return redirect(url_for('login_page'))
787
 
788
- print(f"[DEBUG] Checking against {collection.count_documents({'face_image': {'$exists': True, '$ne': None}})} users")
789
-
790
  for user in users:
791
  try:
792
  ref_image_bytes = user['face_image']
@@ -800,52 +666,45 @@ def face_login():
800
  similarity = cosine_similarity([test_features], [ref_features])[0][0]
801
  distance = 1 - similarity
802
 
803
- print(f"[DEBUG] User {user[id_field]} - Distance: {distance:.3f}")
804
-
805
  if distance < 0.4:
806
- # CRITICAL FIX: Clear session, set data, create response manually
807
- session.clear()
808
  session['logged_in'] = True
809
  session['user_type'] = face_role
810
  session[id_field] = user[id_field]
811
  session['name'] = user.get('name')
812
- session.permanent = True
813
-
814
- print(f"[DEBUG] Face login SUCCESS - Session set: {dict(session)}")
815
- print(f"[DEBUG] Session modified: {session.modified}")
816
-
817
- # Create response manually to ensure cookie is set
818
- response = redirect(url_for(dashboard_route))
819
-
820
- # Force session cookie to be set
821
- app.save_session(session, response)
822
 
823
  flash('Face login successful!', 'success')
824
- return response
825
 
826
  except Exception as e:
827
- print(f"[DEBUG] Error processing user {user.get(id_field)}: {e}")
828
  continue
829
 
830
- print("[DEBUG] Face login FAILED - no match found")
831
  flash('Face not recognized. Please try again or contact admin.', 'danger')
832
  return redirect(url_for('login_page'))
833
 
834
  except Exception as e:
835
- print(f"[DEBUG] Face login ERROR: {e}")
836
  flash('Login failed due to server error. Please try again.', 'danger')
837
  return redirect(url_for('login_page'))
838
 
839
  @app.route('/auto-face-login', methods=['POST'])
840
  def auto_face_login():
841
  try:
 
 
 
842
  data = request.json
843
  face_image = data.get('face_image')
844
  face_role = data.get('face_role', 'student')
 
845
  if not face_image:
846
  return jsonify({'success': False, 'message': 'No image received'})
 
847
  image = decode_image(face_image)
848
  test_features = get_face_features_deepface(image)
 
849
  if test_features is None:
850
  return jsonify({'success': False, 'message': 'No face detected'})
851
 
@@ -859,11 +718,13 @@ def auto_face_login():
859
  dashboard_route = '/dashboard'
860
 
861
  users = collection.find({'face_image': {'$exists': True, '$ne': None}}).limit(20)
 
862
  for user in users:
863
  try:
864
  ref_image_array = np.frombuffer(user['face_image'], np.uint8)
865
  ref_image = cv2.imdecode(ref_image_array, cv2.IMREAD_COLOR)
866
  ref_features = get_face_features_deepface(ref_image)
 
867
  if ref_features is None:
868
  continue
869
 
@@ -871,13 +732,12 @@ def auto_face_login():
871
  distance = 1 - similarity
872
 
873
  if distance < 0.4:
874
- # Clear existing session and set new session data
875
- session.clear()
876
  session['logged_in'] = True
877
  session['user_type'] = face_role
878
  session[id_field] = user[id_field]
879
  session['name'] = user.get('name')
880
- session.permanent = True
881
 
882
  return jsonify({
883
  'success': True,
@@ -885,262 +745,213 @@ def auto_face_login():
885
  'redirect_url': dashboard_route,
886
  'face_role': face_role
887
  })
 
888
  except Exception as e:
889
- print(f"Error processing user {user.get(id_field)}: {e}")
890
  continue
891
 
892
  return jsonify({'success': False, 'message': f'Face not recognized in {face_role} database'})
 
893
  except Exception as e:
894
- print(f"Auto face login error: {e}")
895
  return jsonify({'success': False, 'message': 'Login failed due to server error'})
896
 
897
  @app.route('/attendance.html')
 
898
  def attendance_page():
899
- if 'logged_in' not in session or session.get('user_type') != 'student':
900
- return redirect(url_for('login_page'))
901
  student_id = session.get('student_id')
902
  student = students_collection.find_one({'student_id': student_id})
903
  return render_template('attendance.html', student=student)
904
 
905
  @app.route('/dashboard')
 
906
  def dashboard():
907
- print(f"[DEBUG] Dashboard accessed")
908
- print(f"[DEBUG] Request cookies: {dict(request.cookies)}")
909
- print(f"[DEBUG] Session before access: {dict(session)}")
910
- print(f"[DEBUG] Session keys: {list(session.keys())}")
911
- print(f"[DEBUG] Session logged_in: {session.get('logged_in')}")
912
- print(f"[DEBUG] Session user_type: {session.get('user_type')}")
913
-
914
- # Check if user is logged in
915
- if not session.get('logged_in') or session.get('user_type') != 'student':
916
- print("[DEBUG] Dashboard access DENIED - session check failed")
917
- print(f"[DEBUG] Session contents: {dict(session)}")
918
- flash('Please log in to access the dashboard.', 'warning')
919
- return redirect(url_for('login_page'))
920
-
921
- student_id = session.get('student_id')
922
- if not student_id:
923
- print("[DEBUG] Dashboard access DENIED - no student_id")
924
- session.clear()
925
- return redirect(url_for('login_page'))
926
-
927
- student = students_collection.find_one({'student_id': student_id})
928
- if not student:
929
- print(f"[DEBUG] Dashboard access DENIED - student not found: {student_id}")
930
- session.clear()
 
 
931
  return redirect(url_for('login_page'))
932
-
933
- # Process face image for display
934
- if student and 'face_image' in student and student['face_image']:
935
- face_image_base64 = base64.b64encode(student['face_image']).decode('utf-8')
936
- mime_type = student.get('face_image_type', 'image/jpeg')
937
- student['face_image_url'] = f"data:{mime_type};base64,{face_image_base64}"
938
-
939
- attendance_records = list(attendance_collection.find({'student_id': student_id}).sort('date', -1))
940
-
941
- print(f"[DEBUG] Dashboard SUCCESS - loaded for: {student_id}")
942
- return render_template('dashboard.html', student=student, attendance_records=attendance_records)
943
 
944
  @app.route('/mark-attendance', methods=['POST'])
 
945
  def mark_attendance():
946
- if 'logged_in' not in session or session.get('user_type') != 'student':
947
- return jsonify({'success': False, 'message': 'Not logged in'})
948
-
949
- if not yolo_face:
950
- return jsonify({'success': False, 'message': 'Face detection model not available. Please contact admin.'})
951
-
952
- data = request.json
953
- student_id = session.get('student_id') or data.get('student_id')
954
- program = data.get('program')
955
- semester = data.get('semester')
956
- course = data.get('course')
957
- face_image = data.get('face_image')
958
-
959
- if not all([student_id, program, semester, course, face_image]):
960
- return jsonify({'success': False, 'message': 'Missing required data'})
961
-
962
- client_ip = request.remote_addr
963
- t0 = time.time()
964
-
965
- image = decode_image(face_image)
966
- if image is None or image.size == 0:
967
- return jsonify({'success': False, 'message': 'Invalid image data'})
968
-
969
- h, w = image.shape[:2]
970
- vis = image.copy()
971
-
972
- detections = yolo_face.detect(image, max_det=20)
973
- if not detections:
974
- overlay = image_to_data_uri(vis)
975
- log_metrics_event_normalized(
976
- event="reject_true",
977
- attempt_type="impostor",
978
- claimed_id=student_id,
979
- recognized_id=None,
980
- liveness_pass=False,
981
- distance=None,
982
- live_prob=None,
983
- latency_ms=round((time.time() - t0) * 1000.0, 2),
984
- client_ip=client_ip,
985
- reason="no_face_detected"
986
- )
987
- return jsonify({'success': False, 'message': 'No face detected for liveness', 'overlay': overlay})
988
-
989
- best = max(detections, key=lambda d: d["score"])
990
- x1, y1, x2, y2 = [int(v) for v in best["bbox"]]
991
- x1e, y1e, x2e, y2e = expand_and_clip_box((x1, y1, x2, y2), scale=1.2, w=w, h=h)
992
- face_crop = image[y1e:y2e, x1e:x2e]
993
- if face_crop.size == 0:
994
- overlay = image_to_data_uri(vis)
995
- log_metrics_event_normalized(
996
- event="reject_true",
997
- attempt_type="impostor",
998
- claimed_id=student_id,
999
- recognized_id=None,
1000
- liveness_pass=False,
1001
- distance=None,
1002
- live_prob=None,
1003
- latency_ms=round((time.time() - t0) * 1000.0, 2),
1004
- client_ip=client_ip,
1005
- reason="failed_crop"
1006
- )
1007
- return jsonify({'success': False, 'message': 'Failed to crop face for liveness', 'overlay': overlay})
1008
 
1009
- # Anti-spoofing
1010
- live_prob = 1.0
1011
- is_live = True
1012
-
1013
- if anti_spoof_bin:
1014
- live_prob = anti_spoof_bin.predict_live_prob(face_crop)
1015
- is_live = live_prob >= 0.7
1016
-
1017
- label = "LIVE" if is_live else "SPOOF"
1018
- color = (0, 200, 0) if is_live else (0, 0, 255)
1019
- draw_live_overlay(vis, (x1e, y1e, x2e, y2e), label, live_prob, color)
1020
- overlay_data = image_to_data_uri(vis)
1021
-
1022
- if not is_live:
1023
- log_metrics_event_normalized(
1024
- event="reject_true",
1025
- attempt_type="impostor",
1026
- claimed_id=student_id,
1027
- recognized_id=None,
1028
- liveness_pass=False,
1029
- distance=None,
1030
- live_prob=float(live_prob),
1031
- latency_ms=round((time.time() - t0) * 1000.0, 2),
1032
- client_ip=client_ip,
1033
- reason="liveness_fail"
1034
- )
1035
- return jsonify({'success': False, 'message': f'Spoof detected or face not live (p={live_prob:.2f}).', 'overlay': overlay_data})
1036
 
1037
- success, message = recognize_face(image, student_id, user_type='student')
1038
- total_latency_ms = round((time.time() - t0) * 1000.0, 2)
 
 
 
 
1039
 
1040
- distance_val = None
1041
- try:
1042
- if "distance=" in message:
1043
- part = message.split("distance=")[1]
1044
- distance_val = float(part.split(",")[0].strip(") "))
1045
- except Exception:
1046
- pass
1047
 
1048
- reason = None
1049
- if not success:
1050
- if message.startswith("Unauthorized attempt"):
1051
- reason = "unauthorized_attempt"
1052
- elif message.startswith("No face detected"):
1053
- reason = "no_face_detected"
1054
- elif message.startswith("False reject"):
1055
- reason = "false_reject"
1056
- elif message.startswith("Error in face recognition"):
1057
- reason = "recognition_error"
1058
- else:
1059
- reason = "not_recognized"
1060
-
1061
- if success:
1062
- log_metrics_event_normalized(
1063
- event="accept_true",
1064
- attempt_type="genuine",
1065
- claimed_id=student_id,
1066
- recognized_id=student_id,
1067
- liveness_pass=True,
1068
- distance=distance_val,
1069
- live_prob=float(live_prob),
1070
- latency_ms=total_latency_ms,
1071
- client_ip=client_ip,
1072
- reason=None
1073
- )
1074
- attendance_data = {
1075
- 'student_id': student_id,
1076
- 'program': program,
1077
- 'semester': semester,
1078
- 'subject': course,
1079
- 'date': datetime.now().date().isoformat(),
1080
- 'time': datetime.now().time().strftime('%H:%M:%S'),
1081
- 'status': 'present',
1082
- 'created_at': datetime.now()
1083
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1084
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1085
  existing_attendance = attendance_collection.find_one({
1086
  'student_id': student_id,
1087
  'subject': course,
1088
  'date': datetime.now().date().isoformat()
1089
  })
 
1090
  if existing_attendance:
1091
  return jsonify({'success': False, 'message': 'Attendance already marked for this course today', 'overlay': overlay_data})
 
 
 
 
 
 
 
 
 
 
 
 
1092
  attendance_collection.insert_one(attendance_data)
1093
  return jsonify({'success': True, 'message': 'Attendance marked successfully', 'overlay': overlay_data})
1094
- except Exception as e:
1095
- return jsonify({'success': False, 'message': f'Database error: {str(e)}', 'overlay': overlay_data})
1096
- else:
1097
- if reason == "false_reject":
1098
- log_metrics_event_normalized(
1099
- event="reject_false",
1100
- attempt_type="genuine",
1101
- claimed_id=student_id,
1102
- recognized_id=student_id,
1103
- liveness_pass=True,
1104
- distance=distance_val,
1105
- live_prob=float(live_prob),
1106
- latency_ms=total_latency_ms,
1107
- client_ip=client_ip,
1108
- reason=reason
1109
- )
1110
  else:
 
 
 
 
 
 
 
1111
  log_metrics_event_normalized(
1112
- event="reject_true",
1113
- attempt_type="impostor",
1114
- claimed_id=student_id,
1115
- recognized_id=None,
1116
- liveness_pass=True,
1117
- distance=distance_val,
1118
- live_prob=float(live_prob),
1119
- latency_ms=total_latency_ms,
1120
- client_ip=client_ip,
1121
- reason=reason
1122
  )
1123
- return jsonify({'success': False, 'message': message, 'overlay': overlay_data})
 
 
 
 
1124
 
1125
  @app.route('/liveness-preview', methods=['POST'])
 
1126
  def liveness_preview():
1127
- if 'logged_in' not in session or session.get('user_type') != 'student':
1128
- return jsonify({'success': False, 'message': 'Not logged in'})
1129
-
1130
- if not yolo_face:
1131
- return jsonify({'success': False, 'message': 'Face detection model not available'})
1132
-
1133
  try:
 
 
 
1134
  data = request.json or {}
1135
  face_image = data.get('face_image')
1136
  if not face_image:
1137
  return jsonify({'success': False, 'message': 'No image received'})
 
1138
  image = decode_image(face_image)
1139
  if image is None or image.size == 0:
1140
  return jsonify({'success': False, 'message': 'Invalid image data'})
 
1141
  h, w = image.shape[:2]
1142
  vis = image.copy()
1143
  detections = yolo_face.detect(image, max_det=10)
 
1144
  if not detections:
1145
  overlay_data = image_to_data_uri(vis)
1146
  return jsonify({
@@ -1150,10 +961,12 @@ def liveness_preview():
1150
  'message': 'No face detected',
1151
  'overlay': overlay_data
1152
  })
 
1153
  best = max(detections, key=lambda d: d["score"])
1154
  x1, y1, x2, y2 = [int(v) for v in best["bbox"]]
1155
  x1e, y1e, x2e, y2e = expand_and_clip_box((x1, y1, x2, y2), scale=1.2, w=w, h=h)
1156
  face_crop = image[y1e:y2e, x1e:x2e]
 
1157
  if face_crop.size == 0:
1158
  overlay_data = image_to_data_uri(vis)
1159
  return jsonify({
@@ -1165,7 +978,7 @@ def liveness_preview():
1165
  })
1166
 
1167
  live_prob = 1.0
1168
- if anti_spoof_bin:
1169
  live_prob = anti_spoof_bin.predict_live_prob(face_crop)
1170
 
1171
  threshold = 0.7
@@ -1173,17 +986,19 @@ def liveness_preview():
1173
  color = (0, 200, 0) if label == "LIVE" else (0, 0, 255)
1174
  draw_live_overlay(vis, (x1e, y1e, x2e, y2e), label, live_prob, color)
1175
  overlay_data = image_to_data_uri(vis)
 
1176
  return jsonify({
1177
  'success': True,
1178
  'live': bool(live_prob >= threshold),
1179
  'live_prob': float(live_prob),
1180
  'overlay': overlay_data
1181
  })
 
1182
  except Exception as e:
1183
- print("liveness_preview error:", e)
1184
  return jsonify({'success': False, 'message': 'Server error during preview'})
1185
 
1186
- # --------- TEACHER ROUTES ---------
1187
  @app.route('/teacher_register.html')
1188
  def teacher_register_page():
1189
  return render_template('teacher_register.html')
@@ -1195,6 +1010,10 @@ def teacher_login_page():
1195
  @app.route('/teacher_register', methods=['POST'])
1196
  def teacher_register():
1197
  try:
 
 
 
 
1198
  teacher_data = {
1199
  'teacher_id': request.form.get('teacher_id'),
1200
  'name': request.form.get('name'),
@@ -1207,6 +1026,7 @@ def teacher_register():
1207
  'password': request.form.get('password'),
1208
  'created_at': datetime.now()
1209
  }
 
1210
  face_image = request.form.get('face_image')
1211
  if face_image and ',' in face_image:
1212
  image_data = face_image.split(',')[1]
@@ -1215,6 +1035,7 @@ def teacher_register():
1215
  else:
1216
  flash('Face image is required for registration.', 'danger')
1217
  return redirect(url_for('teacher_register_page'))
 
1218
  result = teachers_collection.insert_one(teacher_data)
1219
  if result.inserted_id:
1220
  flash('Registration successful! You can now login.', 'success')
@@ -1222,75 +1043,76 @@ def teacher_register():
1222
  else:
1223
  flash('Registration failed. Please try again.', 'danger')
1224
  return redirect(url_for('teacher_register_page'))
 
1225
  except pymongo.errors.DuplicateKeyError:
1226
  flash('Teacher ID already exists. Please use a different ID.', 'danger')
1227
  return redirect(url_for('teacher_register_page'))
1228
  except Exception as e:
 
1229
  flash(f'Registration failed: {str(e)}', 'danger')
1230
  return redirect(url_for('teacher_register_page'))
1231
 
1232
  @app.route('/teacher_login', methods=['POST'])
1233
  def teacher_login():
1234
- teacher_id = request.form.get('teacher_id')
1235
- password = request.form.get('password')
1236
-
1237
- print(f"[DEBUG] Teacher login attempt for teacher_id: {teacher_id}")
1238
-
1239
- if not teacher_id or not password:
1240
- flash('Please enter both teacher ID and password.', 'danger')
1241
- return redirect(url_for('teacher_login_page'))
1242
-
1243
- teacher = teachers_collection.find_one({'teacher_id': teacher_id})
1244
- if teacher and teacher.get('password') == password:
1245
- # Clear any existing session data first
1246
- session.clear()
1247
-
1248
- session['logged_in'] = True
1249
- session['user_type'] = 'teacher'
1250
- session['teacher_id'] = teacher_id
1251
- session['name'] = teacher.get('name')
1252
- session.permanent = True
1253
-
1254
- print(f"[DEBUG] Teacher session set after login: {dict(session)}")
1255
 
1256
- # Create response manually to ensure cookie is set
1257
- response = redirect(url_for('teacher_dashboard'))
1258
- app.save_session(session, response)
1259
 
1260
- flash('Login successful!', 'success')
1261
- return response
1262
- else:
1263
- print(f"[DEBUG] Teacher login failed for: {teacher_id}")
1264
- flash('Invalid credentials. Please try again.', 'danger')
 
 
 
 
 
 
 
 
 
 
 
 
 
1265
  return redirect(url_for('teacher_login_page'))
1266
 
1267
  @app.route('/teacher_dashboard')
 
1268
  def teacher_dashboard():
1269
- print(f"[DEBUG] Teacher Dashboard accessed - Session: {dict(session)}")
1270
-
1271
- if not session.get('logged_in') or session.get('user_type') != 'teacher':
1272
- print("[DEBUG] Teacher dashboard access denied - redirecting to login")
1273
- return redirect(url_for('teacher_login_page'))
1274
-
1275
- teacher_id = session.get('teacher_id')
1276
- if not teacher_id:
1277
- print("[DEBUG] Teacher dashboard access denied - no teacher_id in session")
1278
- session.clear()
1279
- return redirect(url_for('teacher_login_page'))
1280
-
1281
- teacher = teachers_collection.find_one({'teacher_id': teacher_id})
1282
- if not teacher:
1283
- print(f"[DEBUG] Teacher dashboard access denied - teacher not found: {teacher_id}")
1284
- session.clear()
 
 
 
 
 
 
 
1285
  return redirect(url_for('teacher_login_page'))
1286
-
1287
- if teacher and 'face_image' in teacher and teacher['face_image']:
1288
- face_image_base64 = base64.b64encode(teacher['face_image']).decode('utf-8')
1289
- mime_type = teacher.get('face_image_type', 'image/jpeg')
1290
- teacher['face_image_url'] = f"data:{mime_type};base64,{face_image_base64}"
1291
-
1292
- print(f"[DEBUG] Teacher dashboard loaded successfully for: {teacher_id}")
1293
- return render_template('teacher_dashboard.html', teacher=teacher)
1294
 
1295
  @app.route('/teacher_logout')
1296
  def teacher_logout():
@@ -1304,80 +1126,115 @@ def logout():
1304
  flash('You have been logged out', 'info')
1305
  return redirect(url_for('login_page'))
1306
 
1307
- # --------- METRICS JSON ENDPOINTS ---------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1308
  @app.route('/metrics-data', methods=['GET'])
 
1309
  def metrics_data():
1310
  data = compute_metrics()
1311
- recent = list(metrics_events.find({}, {"_id": 0}).sort("ts", -1).limit(200))
1312
- normalized_recent = []
1313
- for r in recent:
1314
- if isinstance(r.get("ts"), datetime):
1315
- r["ts"] = r["ts"].isoformat()
1316
- event, attempt_type = classify_event(r)
1317
- if event and not r.get("event"):
1318
- r["event"] = event
1319
- if attempt_type and not r.get("attempt_type"):
1320
- r["attempt_type"] = attempt_type
1321
- if "liveness_pass" not in r:
1322
- if r.get("decision") == "spoof_blocked":
1323
- r["liveness_pass"] = False
1324
- elif isinstance(r.get("live_prob"), (int, float)):
1325
- r["liveness_pass"] = bool(r["live_prob"] >= 0.7)
1326
- else:
1327
- r["liveness_pass"] = None
1328
- normalized_recent.append(r)
1329
-
1330
- data["recent"] = normalized_recent
1331
  data["avg_latency_ms"] = compute_latency_avg()
1332
  return jsonify(data)
1333
 
1334
  @app.route('/metrics-json')
 
1335
  def metrics_json():
1336
  m = compute_metrics()
1337
  counts = m["counts"]
1338
  rates = m["rates"]
1339
  totals = m["totals"]
1340
  avg_latency = compute_latency_avg()
1341
- accuracy_pct = rates["accuracy"] * 100.0
1342
- far_pct = rates["FAR"] * 100.0
1343
- frr_pct = rates["FRR"] * 100.0
 
1344
 
1345
  return jsonify({
1346
  'Accuracy': f"{accuracy_pct:.2f}%" if totals["totalAttempts"] > 0 else "N/A",
1347
- 'False Accepts (FAR)': f"{far_pct:.2f}%" if counts["impostorAttempts"] > 0 else "N/A",
1348
- 'False Rejects (FRR)': f"{frr_pct:.2f}%" if counts["genuineAttempts"] > 0 else "N/A",
1349
  'Average Inference Time (s)': f"{(avg_latency/1000.0):.2f}" if isinstance(avg_latency, (int, float)) else "N/A",
1350
- 'Correct Recognitions': counts["trueAccepts"],
1351
  'Total Attempts': totals["totalAttempts"],
1352
- 'Unauthorized Attempts': counts["unauthorizedRejected"],
1353
- 'enhanced': {
1354
- 'totals': {
1355
- 'attempts': totals["totalAttempts"],
1356
- 'trueAccepts': counts["trueAccepts"],
1357
- 'falseAccepts': counts["falseAccepts"],
1358
- 'trueRejects': counts["trueRejects"],
1359
- 'falseRejects': counts["falseRejects"],
1360
- 'genuineAttempts': counts["genuineAttempts"],
1361
- 'impostorAttempts': counts["impostorAttempts"],
1362
- 'unauthorizedRejected': counts["unauthorizedRejected"],
1363
- 'unauthorizedAccepted': counts["unauthorizedAccepted"],
1364
- },
1365
- 'accuracy_pct': round(accuracy_pct, 2),
1366
- 'avg_latency_ms': round(avg_latency, 2) if isinstance(avg_latency, (int, float)) else None
1367
- }
1368
  })
1369
 
1370
- @app.route('/metrics-events')
1371
- def metrics_events_api():
1372
- limit = int(request.args.get("limit", 200))
1373
- cursor = metrics_events.find({}, {"_id": 0}).sort("ts", -1).limit(limit)
1374
- events = list(cursor)
1375
- for ev in events:
1376
- if isinstance(ev.get("ts"), datetime):
1377
- ev["ts"] = ev["ts"].isoformat()
1378
- return jsonify(events)
1379
-
1380
- # FIXED: Port 7860 for HuggingFace Spaces
1381
  if __name__ == '__main__':
1382
- port = int(os.environ.get('PORT', 7860)) # HuggingFace default port
1383
  app.run(host='0.0.0.0', port=port, debug=False)
 
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
 
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
 
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
  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:
 
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):
 
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:
 
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:
 
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]
 
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
 
 
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
 
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:
 
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'):
 
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
  }
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
  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
  '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
  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
 
 
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
 
 
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']
 
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
 
 
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
 
 
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,
 
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)
957
  return jsonify({
 
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)
968
  face_crop = image[y1e:y2e, x1e:x2e]
969
+
970
  if face_crop.size == 0:
971
  overlay_data = image_to_data_uri(vis)
972
  return jsonify({
 
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
 
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
  @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
  '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
  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
  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():
 
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)
app/static/js/camera.js CHANGED
@@ -1,37 +1,50 @@
1
  /*
2
- camera.js - Updated for Render deployment
3
-
4
- - Keeps your existing camera flows for:
5
- - Student registration
6
- - Teacher registration
7
- - Face login
8
-
9
- - NEW: Live liveness overlay during Attendance camera preview
10
- - Streams frames at ~2-3 fps to /liveness-preview
11
- - Renders server overlay (LIVE/SPOOF bbox) into <img id="attendanceOverlayImg">
12
- - Stops preview when you capture the still photo
13
- - Continues working with Mark Attendance (which already returns overlay too)
14
- - Added error handling for network issues and model availability
15
-
16
- Attendance page expected element IDs (ensure these exist in attendance.html):
17
- - Buttons: startCameraAttendance, captureImageAttendance, retakeImageAttendance, markAttendanceBtn
18
- - Media: videoAttendance (video), canvasAttendance (canvas), attendanceOverlayImg (img)
19
- - Status: attendanceStatus (div/span for messages)
20
- - Fields: program, semester, course, student_id (optional, server may read from session)
21
  */
22
 
23
  document.addEventListener('DOMContentLoaded', function () {
24
- // Enhanced error handling for network requests
25
- async function makeRequest(url, options = {}) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  try {
27
- const controller = new AbortController();
28
- const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
29
-
30
  const response = await fetch(url, {
31
  ...options,
32
  signal: controller.signal,
33
  headers: {
34
  'Content-Type': 'application/json',
 
35
  ...options.headers
36
  }
37
  });
@@ -39,105 +52,217 @@ document.addEventListener('DOMContentLoaded', function () {
39
  clearTimeout(timeoutId);
40
 
41
  if (!response.ok) {
42
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
 
45
- return await response.json();
 
46
  } catch (error) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  if (error.name === 'AbortError') {
48
- throw new Error('Request timed out. Please check your internet connection.');
 
 
49
  }
 
50
  throw error;
51
  }
52
  }
53
 
54
- // Show loading indicator
55
- function showLoading(element, message = 'Processing...') {
56
- if (element) {
57
- element.innerHTML = `
58
- <div class="d-flex align-items-center">
59
- <div class="spinner-border spinner-border-sm me-2" role="status">
60
- <span class="visually-hidden">Loading...</span>
61
- </div>
62
- ${message}
63
- </div>
64
- `;
65
- }
 
 
 
 
 
66
  }
67
 
68
- // Hide loading indicator
69
- function hideLoading(element, message = '') {
70
- if (element) {
71
- element.innerHTML = message;
 
 
 
 
 
 
 
72
  }
73
  }
74
 
75
- // Enhanced camera access with better error handling
76
  async function getCameraStream(constraints = {}) {
77
- const defaultConstraints = {
78
- video: {
79
- width: { ideal: 640, max: 1280 },
80
- height: { ideal: 480, max: 720 },
81
- facingMode: 'user',
82
- frameRate: { ideal: 30, max: 60 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  }
84
- };
85
 
86
- const finalConstraints = {
87
- ...defaultConstraints,
88
- ...constraints
89
- };
90
 
91
- try {
92
- // Try with ideal constraints first
93
- return await navigator.mediaDevices.getUserMedia(finalConstraints);
94
- } catch (error) {
95
- console.warn('Failed with ideal constraints, trying basic:', error);
96
  try {
97
- // Fallback to basic constraints
98
- return await navigator.mediaDevices.getUserMedia({
99
- video: { facingMode: 'user' }
100
- });
101
- } catch (fallbackError) {
102
- console.error('Camera access failed completely:', fallbackError);
103
- throw new Error('Unable to access camera. Please ensure camera permissions are granted and no other application is using the camera.');
 
 
104
  }
105
  }
106
  }
107
 
108
- // Reusable section setup for Registration/Login
109
- function setupCameraSection(config) {
110
- const video = document.getElementById(config.videoId);
111
- const canvas = document.getElementById(config.canvasId);
112
- const startCameraBtn = document.getElementById(config.startCameraBtnId);
113
- const captureImageBtn = document.getElementById(config.captureImageBtnId);
114
- const retakeImageBtn = document.getElementById(config.retakeImageBtnId);
115
- const cameraOverlay = document.getElementById(config.cameraOverlayId);
116
- const faceImageInput = document.getElementById(config.faceImageInputId);
117
- const actionBtn = document.getElementById(config.actionBtnId);
118
 
119
- let stream = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
- if (
122
- !video ||
123
- !canvas ||
124
- !startCameraBtn ||
125
- !captureImageBtn ||
126
- !retakeImageBtn ||
127
- !cameraOverlay ||
128
- !faceImageInput ||
129
- !actionBtn
130
- ) {
131
- // Missing elements → skip this section gracefully
132
  return;
133
  }
134
 
135
- // Start camera with enhanced error handling
136
- startCameraBtn.addEventListener('click', async function () {
 
 
 
 
 
137
  try {
138
- startCameraBtn.disabled = true;
139
- showLoading(startCameraBtn, 'Starting camera...');
140
 
 
 
 
 
 
 
 
 
141
  stream = await getCameraStream({
142
  video: {
143
  width: { ideal: 400, max: 640 },
@@ -146,63 +271,88 @@ document.addEventListener('DOMContentLoaded', function () {
146
  }
147
  });
148
 
149
- video.srcObject = stream;
150
- await video.play();
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
- startCameraBtn.classList.add('d-none');
153
- captureImageBtn.classList.remove('d-none');
154
- retakeImageBtn.classList.add('d-none');
155
- cameraOverlay.classList.add('d-none');
156
- video.classList.remove('d-none');
157
- canvas.classList.add('d-none');
158
- actionBtn.disabled = true;
159
  } catch (err) {
160
- console.error('Error accessing camera:', err);
161
- alert(err.message || 'Could not access the camera. Please make sure it is connected and permissions are granted.');
 
162
  } finally {
163
- startCameraBtn.disabled = false;
164
- hideLoading(startCameraBtn, 'Start Camera');
 
 
 
 
165
  }
166
  });
167
 
168
- // Capture image
169
- captureImageBtn.addEventListener('click', function () {
170
  try {
171
- const context = canvas.getContext('2d');
172
- canvas.width = video.videoWidth || 400;
173
- canvas.height = video.videoHeight || 300;
174
- context.drawImage(video, 0, 0, canvas.width, canvas.height);
 
 
 
 
 
 
 
175
 
176
- const imageDataURL = canvas.toDataURL('image/jpeg', 0.8);
177
- faceImageInput.value = imageDataURL;
 
 
178
 
179
- cameraOverlay.classList.remove('d-none');
180
- captureImageBtn.classList.add('d-none');
181
- retakeImageBtn.classList.remove('d-none');
182
- video.classList.add('d-none');
183
- canvas.classList.remove('d-none');
184
- actionBtn.disabled = false;
 
 
 
185
 
 
186
  if (stream) {
187
- stream.getTracks().forEach((track) => track.stop());
188
  stream = null;
189
  }
 
190
  } catch (err) {
191
  console.error('Error capturing image:', err);
192
  alert('Failed to capture image. Please try again.');
193
  }
194
  });
195
 
196
- // Retake image
197
- retakeImageBtn.addEventListener('click', async function () {
198
  try {
199
- retakeImageBtn.disabled = true;
200
- showLoading(retakeImageBtn, 'Restarting...');
201
 
202
- const context = canvas.getContext('2d');
203
- context.clearRect(0, 0, canvas.width, canvas.height);
204
- faceImageInput.value = '';
 
205
 
 
206
  stream = await getCameraStream({
207
  video: {
208
  width: { ideal: 400, max: 640 },
@@ -211,82 +361,118 @@ document.addEventListener('DOMContentLoaded', function () {
211
  }
212
  });
213
 
214
- video.srcObject = stream;
215
- await video.play();
 
 
 
 
 
 
 
 
 
 
216
 
217
- cameraOverlay.classList.add('d-none');
218
- captureImageBtn.classList.remove('d-none');
219
- retakeImageBtn.classList.add('d-none');
220
- video.classList.remove('d-none');
221
- canvas.classList.add('d-none');
222
- actionBtn.disabled = true;
223
  } catch (err) {
224
  console.error('Error restarting camera:', err);
225
  alert(err.message || 'Error restarting camera.');
226
  } finally {
227
- retakeImageBtn.disabled = false;
228
- hideLoading(retakeImageBtn, 'Retake');
 
 
 
 
 
 
229
  }
230
  });
231
  }
232
 
233
- // Attendance-specific section with LIVE overlay streaming
234
  function setupAttendanceSection(config) {
235
- const video = document.getElementById(config.videoId);
236
- const canvas = document.getElementById(config.canvasId);
237
- const startBtn = document.getElementById(config.startCameraBtnId);
238
- const captureBtn = document.getElementById(config.captureImageBtnId);
239
- const retakeBtn = document.getElementById(config.retakeImageBtnId);
240
- const markBtn = document.getElementById(config.markBtnId);
241
- const overlayImg = document.getElementById(config.overlayImgId);
242
- const statusEl = document.getElementById(config.statusId);
243
-
244
- // Optional fields (server may use session for student_id)
245
- const programEl = document.getElementById(config.programId);
246
- const semesterEl = document.getElementById(config.semesterId);
247
- const courseEl = document.getElementById(config.courseId);
248
- const studentIdEl = document.getElementById(config.studentIdInputId);
 
 
 
 
 
 
 
 
249
 
250
  let stream = null;
251
  let capturedDataUrl = '';
 
 
 
 
 
 
 
252
 
253
- // Live preview control
254
- let previewActive = false;
255
- let previewBusy = false;
256
- let previewTimer = null;
257
- let consecutiveErrors = 0;
258
- const maxConsecutiveErrors = 5;
259
  const previewCanvas = document.createElement('canvas');
260
  const previewCtx = previewCanvas.getContext('2d');
261
 
262
- if (!video || !canvas || !startBtn || !captureBtn || !retakeBtn || !markBtn) {
263
- return;
264
- }
265
-
266
- function setStatus(msg, isError = false) {
267
- if (!statusEl) return;
268
- statusEl.textContent = msg || '';
269
- statusEl.classList.remove('text-success', 'text-danger', 'text-warning');
270
- if (isError) {
271
- statusEl.classList.add('text-danger');
272
- } else if (msg.includes('SPOOF')) {
273
- statusEl.classList.add('text-warning');
274
- } else {
275
- statusEl.classList.add('text-success');
 
 
 
 
276
  }
277
  }
278
 
279
  function clearOverlay() {
280
- if (overlayImg) {
281
- overlayImg.src = '';
282
- overlayImg.classList.add('d-none');
283
  }
284
  }
285
 
286
  async function startCamera() {
287
  try {
288
- startBtn.disabled = true;
289
- showLoading(startBtn, 'Starting camera...');
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
  stream = await getCameraStream({
292
  video: {
@@ -296,61 +482,63 @@ document.addEventListener('DOMContentLoaded', function () {
296
  }
297
  });
298
 
299
- video.srcObject = stream;
300
- await video.play();
301
-
302
- startBtn.classList.add('d-none');
303
- captureBtn.classList.remove('d-none');
304
- retakeBtn.classList.add('d-none');
305
- markBtn.disabled = true;
306
-
307
- video.classList.remove('d-none');
308
- canvas.classList.add('d-none');
309
- setStatus('Camera started. Live preview will begin shortly...');
 
 
 
 
 
310
  clearOverlay();
 
311
 
312
- // Configure preview canvas size (lower res for network efficiency)
313
- previewCanvas.width = 480;
314
- previewCanvas.height = Math.round(previewCanvas.width * (video.videoHeight || 480) / (video.videoWidth || 640));
315
-
316
- // Wait a moment for video to stabilize before starting preview
317
  setTimeout(() => {
318
- if (stream && video.readyState >= 2) {
319
- startPreview();
320
  }
321
- }, 1000);
322
 
323
  } catch (err) {
324
- console.error('Error accessing camera:', err);
325
- alert(err.message || 'Could not access the camera. Please ensure permissions are granted.');
 
326
  } finally {
327
- startBtn.disabled = false;
328
- hideLoading(startBtn, 'Start Camera');
329
  }
330
  }
331
 
332
- function stopCamera() {
333
- if (stream) {
334
- stream.getTracks().forEach((t) => t.stop());
335
- stream = null;
336
- }
337
- }
338
-
339
- function startPreview() {
340
- if (previewActive) return;
341
- previewActive = true;
342
- consecutiveErrors = 0;
343
-
344
- // Stream frames at ~2-3 fps to reduce server load and handle Render's limitations
345
- const intervalMs = 500; // 2 fps for better stability on cloud hosting
346
- previewTimer = setInterval(async () => {
347
- if (!previewActive || previewBusy || !stream || video.readyState < 2) return;
 
348
 
349
- previewBusy = true;
350
  try {
351
- // Draw current frame to offscreen canvas
352
- previewCtx.drawImage(video, 0, 0, previewCanvas.width, previewCanvas.height);
353
- const frameDataUrl = previewCanvas.toDataURL('image/jpeg', 0.5); // Lower quality for faster transfer
354
 
355
  const data = await makeRequest('/liveness-preview', {
356
  method: 'POST',
@@ -358,172 +546,223 @@ document.addEventListener('DOMContentLoaded', function () {
358
  });
359
 
360
  // Reset error counter on success
361
- consecutiveErrors = 0;
 
362
 
363
- if (overlayImg && data.overlay) {
364
- overlayImg.src = data.overlay;
365
- overlayImg.classList.remove('d-none');
 
366
  }
367
 
368
- // Enhanced status feedback
369
  if (typeof data.live === 'boolean' && typeof data.live_prob === 'number') {
370
- const confidenceLevel = data.live_prob >= 0.9 ? 'High' : data.live_prob >= 0.7 ? 'Good' : 'Low';
371
- setStatus(`${data.live ? 'LIVE' : 'SPOOF'} (${confidenceLevel}: ${data.live_prob.toFixed(2)})`, !data.live);
 
 
 
 
 
 
 
 
372
  } else if (data.message) {
373
- setStatus(data.message, !data.success);
374
  }
375
 
376
  } catch (error) {
377
- consecutiveErrors++;
378
- console.warn(`Preview failed (${consecutiveErrors}/${maxConsecutiveErrors}):`, error);
379
 
380
- if (consecutiveErrors >= maxConsecutiveErrors) {
381
- setStatus('Preview temporarily unavailable. You can still capture and mark attendance.', true);
382
- stopPreview(); // Stop trying after too many failures
383
- } else if (consecutiveErrors === 1) {
384
- setStatus('Preview connection issue...', true);
 
 
 
385
  }
386
  } finally {
387
- previewBusy = false;
388
  }
389
  }, intervalMs);
390
  }
391
 
392
- function stopPreview() {
393
- previewActive = false;
394
- if (previewTimer) {
395
- clearInterval(previewTimer);
396
- previewTimer = null;
397
  }
398
- previewBusy = false;
399
- consecutiveErrors = 0;
400
  }
401
 
402
- function captureFrame() {
403
- // Stop live preview while capturing a still
404
- stopPreview();
405
- setStatus('Image captured. Ready to mark attendance.');
 
 
406
 
407
- const ctx = canvas.getContext('2d');
408
- canvas.width = video.videoWidth || 640;
409
- canvas.height = video.videoHeight || 480;
410
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
411
- capturedDataUrl = canvas.toDataURL('image/jpeg', 0.9);
 
 
 
 
412
 
413
- captureBtn.classList.add('d-none');
414
- retakeBtn.classList.remove('d-none');
415
- markBtn.disabled = false;
 
 
 
416
 
417
- video.classList.add('d-none');
418
- canvas.classList.remove('d-none');
419
 
420
- stopCamera();
 
 
 
 
 
 
 
 
 
421
  }
422
 
423
- async function retake() {
424
  try {
425
- retakeBtn.disabled = true;
426
- showLoading(retakeBtn, 'Restarting...');
427
-
428
- const ctx = canvas.getContext('2d');
429
- ctx.clearRect(0, 0, canvas.width, canvas.height);
430
  capturedDataUrl = '';
431
  clearOverlay();
 
 
432
  await startCamera();
 
433
  } catch (err) {
434
  console.error('Error during retake:', err);
435
- alert(err.message || 'Error restarting camera.');
436
  } finally {
437
- retakeBtn.disabled = false;
438
- hideLoading(retakeBtn, 'Retake');
439
  }
440
  }
441
 
442
  async function markAttendance() {
443
  try {
444
  if (!capturedDataUrl) {
445
- setStatus('Please capture an image first.', true);
446
  return;
447
  }
448
 
 
449
  const payload = {
450
- student_id:
451
- (studentIdEl && studentIdEl.value) ||
452
- (markBtn && markBtn.dataset && markBtn.dataset.studentId) ||
453
- null,
454
- program: (programEl && programEl.value) || '',
455
- semester: (semesterEl && semesterEl.value) || '',
456
- course: (courseEl && courseEl.value) || '',
457
  face_image: capturedDataUrl,
458
  };
459
 
460
  if (!payload.program || !payload.semester || !payload.course) {
461
- setStatus('Program, Semester, and Course are required.', true);
462
  return;
463
  }
464
 
465
- markBtn.disabled = true;
466
- showLoading(markBtn, 'Marking attendance...');
467
- setStatus('Processing attendance... Please wait.');
468
 
469
  const data = await makeRequest('/mark-attendance', {
470
  method: 'POST',
471
  body: JSON.stringify(payload)
472
  });
473
 
474
- // Show final overlay image with LIVE/SPOOF bbox (from server)
475
- if (overlayImg && data.overlay) {
476
- overlayImg.src = data.overlay;
477
- overlayImg.classList.remove('d-none');
478
  }
479
 
480
  if (data.success) {
481
- setStatus(data.message || 'Attendance marked successfully.', false);
 
482
  // Auto-refresh after successful attendance
483
  setTimeout(() => {
 
484
  window.location.reload();
485
  }, 3000);
486
  } else {
487
- setStatus(data.message || 'Failed to mark attendance.', true);
 
 
 
 
 
 
 
 
 
 
488
  }
489
 
490
  } catch (err) {
491
  console.error('markAttendance error:', err);
492
- const errorMsg = err.message.includes('timed out')
493
- ? 'Request timed out. Please check your connection and try again.'
494
- : err.message.includes('models not available')
495
- ? 'Face recognition service is temporarily unavailable. Please try again later.'
496
- : 'An error occurred while marking attendance. Please try again.';
497
- setStatus(errorMsg, true);
 
 
 
 
 
498
  } finally {
499
- markBtn.disabled = false;
500
- hideLoading(markBtn, 'Mark Attendance');
501
  }
502
  }
503
 
504
- // Wire events
505
- startBtn.addEventListener('click', startCamera);
506
- captureBtn.addEventListener('click', captureFrame);
507
- retakeBtn.addEventListener('click', retake);
508
- markBtn.addEventListener('click', markAttendance);
509
 
510
- // Cleanup on page unload
511
  window.addEventListener('beforeunload', () => {
512
- stopPreview();
513
- stopCamera();
 
 
514
  });
515
 
516
- // Handle visibility change (pause preview when tab not active)
517
  document.addEventListener('visibilitychange', () => {
518
- if (document.hidden && previewActive) {
519
- stopPreview();
520
- } else if (!document.hidden && stream && video.readyState >= 2 && !previewActive) {
521
- setTimeout(startPreview, 500);
 
 
 
 
522
  }
523
  });
524
  }
525
 
526
- // Enhanced auto face login with better error handling
527
  function setupAutoFaceLogin() {
528
  const autoLoginBtn = document.getElementById('autoFaceLoginBtn');
529
  const roleSelect = document.getElementById('faceRole');
@@ -533,10 +772,12 @@ document.addEventListener('DOMContentLoaded', function () {
533
  autoLoginBtn.addEventListener('click', async function() {
534
  try {
535
  autoLoginBtn.disabled = true;
536
- showLoading(autoLoginBtn, 'Accessing camera...');
537
 
538
  const role = roleSelect ? roleSelect.value : 'student';
539
 
 
 
540
  const stream = await getCameraStream({
541
  video: {
542
  width: { ideal: 640, max: 1280 },
@@ -545,25 +786,28 @@ document.addEventListener('DOMContentLoaded', function () {
545
  }
546
  });
547
 
548
- // Create temporary video element for auto-login
549
  const tempVideo = document.createElement('video');
550
  tempVideo.srcObject = stream;
551
  tempVideo.muted = true;
 
 
552
  await tempVideo.play();
553
 
554
- // Wait for video to stabilize
555
- await new Promise(resolve => setTimeout(resolve, 1000));
556
 
557
- // Capture frame
 
558
  const tempCanvas = document.createElement('canvas');
559
  const ctx = tempCanvas.getContext('2d');
560
  tempCanvas.width = tempVideo.videoWidth || 640;
561
  tempCanvas.height = tempVideo.videoHeight || 480;
562
  ctx.drawImage(tempVideo, 0, 0, tempCanvas.width, tempCanvas.height);
563
 
564
- const imageDataURL = tempCanvas.toDataURL('image/jpeg', 0.8);
565
 
566
- // Clean up camera
567
  stream.getTracks().forEach(track => track.stop());
568
 
569
  showLoading(autoLoginBtn, 'Recognizing face...');
@@ -577,25 +821,30 @@ document.addEventListener('DOMContentLoaded', function () {
577
  });
578
 
579
  if (data.success) {
580
- setStatus && setStatus('Login successful! Redirecting...', false);
581
  setTimeout(() => {
582
  window.location.href = data.redirect_url;
583
- }, 1000);
584
  } else {
585
- alert(data.message || 'Face recognition failed. Please try again.');
586
  }
587
 
588
  } catch (err) {
589
  console.error('Auto face login error:', err);
590
  alert(err.message || 'Auto face login failed. Please try manual login.');
 
591
  } finally {
592
- autoLoginBtn.disabled = false;
593
- hideLoading(autoLoginBtn, 'Auto Face Login');
 
594
  }
595
  });
596
  }
597
 
598
- // Student Registration Camera
 
 
 
599
  setupCameraSection({
600
  videoId: 'videoStudent',
601
  canvasId: 'canvasStudent',
@@ -605,9 +854,10 @@ document.addEventListener('DOMContentLoaded', function () {
605
  cameraOverlayId: 'cameraOverlayStudent',
606
  faceImageInputId: 'face_image_student',
607
  actionBtnId: 'registerBtnStudent',
 
608
  });
609
 
610
- // Teacher Registration Camera
611
  setupCameraSection({
612
  videoId: 'videoTeacher',
613
  canvasId: 'canvasTeacher',
@@ -617,9 +867,10 @@ document.addEventListener('DOMContentLoaded', function () {
617
  cameraOverlayId: 'cameraOverlayTeacher',
618
  faceImageInputId: 'face_image_teacher',
619
  actionBtnId: 'registerBtnTeacher',
 
620
  });
621
 
622
- // Face Login Camera (if present)
623
  setupCameraSection({
624
  videoId: 'video',
625
  canvasId: 'canvas',
@@ -628,10 +879,10 @@ document.addEventListener('DOMContentLoaded', function () {
628
  retakeImageBtnId: 'retakeImage',
629
  cameraOverlayId: 'cameraOverlay',
630
  faceImageInputId: 'face_image',
631
- actionBtnId: 'faceLoginBtn',
632
  });
633
 
634
- // Attendance Camera (with live liveness preview)
635
  setupAttendanceSection({
636
  videoId: 'videoAttendance',
637
  canvasId: 'canvasAttendance',
@@ -641,24 +892,54 @@ document.addEventListener('DOMContentLoaded', function () {
641
  markBtnId: 'markAttendanceBtn',
642
  overlayImgId: 'attendanceOverlayImg',
643
  statusId: 'attendanceStatus',
644
- // Optional form fields
645
  programId: 'program',
646
  semesterId: 'semester',
647
  courseId: 'course',
648
- studentIdInputId: 'student_id',
649
  });
650
 
651
- // Setup auto face login if available
652
  setupAutoFaceLogin();
653
 
654
- // Global error handling for network issues
655
  window.addEventListener('online', () => {
656
- console.log('Connection restored');
657
- setStatus && setStatus('Connection restored', false);
 
 
 
 
 
 
 
 
658
  });
659
 
660
  window.addEventListener('offline', () => {
661
- console.log('Connection lost');
662
- setStatus && setStatus('No internet connection', true);
 
 
 
 
 
 
663
  });
664
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /*
2
+ camera.js - Enhanced for Hugging Face Spaces deployment
3
+
4
+ Features:
5
+ - Robust session management and authentication handling
6
+ - Enhanced error handling for network timeouts and server errors
7
+ - Improved retry mechanisms for unreliable connections
8
+ - Better resource cleanup and camera management
9
+ - Live liveness detection preview for attendance
10
+ - Fallback handling when models are unavailable
11
+ - Progressive image quality for better performance on cloud platforms
 
 
 
 
 
 
 
 
 
12
  */
13
 
14
  document.addEventListener('DOMContentLoaded', function () {
15
+ // Configuration
16
+ const CONFIG = {
17
+ REQUEST_TIMEOUT: 45000, // 45 seconds for cloud deployment
18
+ RETRY_ATTEMPTS: 3,
19
+ RETRY_DELAY: 1000,
20
+ PREVIEW_FPS: 1.5, // Reduced for cloud hosting stability
21
+ MAX_CONSECUTIVE_ERRORS: 3,
22
+ IMAGE_QUALITY: {
23
+ preview: 0.4,
24
+ capture: 0.8,
25
+ registration: 0.9
26
+ }
27
+ };
28
+
29
+ // Global state management
30
+ let globalState = {
31
+ sessionValid: true,
32
+ modelsAvailable: true,
33
+ networkOnline: navigator.onLine
34
+ };
35
+
36
+ // Enhanced request function with retry logic and session handling
37
+ async function makeRequest(url, options = {}, retries = CONFIG.RETRY_ATTEMPTS) {
38
+ const controller = new AbortController();
39
+ const timeoutId = setTimeout(() => controller.abort(), CONFIG.REQUEST_TIMEOUT);
40
+
41
  try {
 
 
 
42
  const response = await fetch(url, {
43
  ...options,
44
  signal: controller.signal,
45
  headers: {
46
  'Content-Type': 'application/json',
47
+ 'Cache-Control': 'no-cache',
48
  ...options.headers
49
  }
50
  });
 
52
  clearTimeout(timeoutId);
53
 
54
  if (!response.ok) {
55
+ // Handle specific HTTP status codes
56
+ if (response.status === 401 || response.status === 403) {
57
+ globalState.sessionValid = false;
58
+ throw new Error('Session expired. Please login again.');
59
+ } else if (response.status === 503) {
60
+ throw new Error('Service temporarily unavailable. Please try again later.');
61
+ } else {
62
+ throw new Error(`Server error (${response.status}). Please try again.`);
63
+ }
64
+ }
65
+
66
+ const data = await response.json();
67
+
68
+ // Handle application-level redirects and session issues
69
+ if (data.redirect || (data.message && data.message.includes('Not logged in'))) {
70
+ globalState.sessionValid = false;
71
+ throw new Error('Session expired. Redirecting to login...');
72
+ }
73
+
74
+ // Check for model availability issues
75
+ if (data.message && data.message.includes('model not available')) {
76
+ globalState.modelsAvailable = false;
77
  }
78
 
79
+ return data;
80
+
81
  } catch (error) {
82
+ clearTimeout(timeoutId);
83
+
84
+ // Handle session expiration
85
+ if (!globalState.sessionValid || error.message.includes('Session expired')) {
86
+ setTimeout(() => {
87
+ window.location.href = '/login.html';
88
+ }, 2000);
89
+ throw error;
90
+ }
91
+
92
+ // Handle network errors with retry logic
93
+ if (retries > 0 && (
94
+ error.name === 'AbortError' ||
95
+ error.name === 'TypeError' ||
96
+ error.message.includes('fetch')
97
+ )) {
98
+ console.warn(`Request failed, retrying... (${CONFIG.RETRY_ATTEMPTS - retries + 1}/${CONFIG.RETRY_ATTEMPTS})`);
99
+ await new Promise(resolve => setTimeout(resolve, CONFIG.RETRY_DELAY));
100
+ return makeRequest(url, options, retries - 1);
101
+ }
102
+
103
+ // Enhanced error messages for user
104
  if (error.name === 'AbortError') {
105
+ throw new Error('Request timed out. Please check your internet connection and try again.');
106
+ } else if (error.name === 'TypeError') {
107
+ throw new Error('Network error. Please check your internet connection.');
108
  }
109
+
110
  throw error;
111
  }
112
  }
113
 
114
+ // Enhanced loading indicator with progress
115
+ function showLoading(element, message = 'Processing...', showSpinner = true) {
116
+ if (!element) return;
117
+
118
+ const spinner = showSpinner ? `
119
+ <div class="spinner-border spinner-border-sm me-2" role="status">
120
+ <span class="visually-hidden">Loading...</span>
121
+ </div>
122
+ ` : '';
123
+
124
+ element.innerHTML = `
125
+ <div class="d-flex align-items-center justify-content-center">
126
+ ${spinner}
127
+ <span class="loading-text">${message}</span>
128
+ </div>
129
+ `;
130
+ element.disabled = true;
131
  }
132
 
133
+ function hideLoading(element, message = '', isError = false) {
134
+ if (!element) return;
135
+
136
+ element.innerHTML = message;
137
+ element.disabled = false;
138
+
139
+ if (isError) {
140
+ element.classList.add('btn-outline-danger');
141
+ setTimeout(() => {
142
+ element.classList.remove('btn-outline-danger');
143
+ }, 3000);
144
  }
145
  }
146
 
147
+ // Enhanced camera access with progressive fallback
148
  async function getCameraStream(constraints = {}) {
149
+ const configurations = [
150
+ // High quality for registration/login
151
+ {
152
+ video: {
153
+ width: { ideal: 640, max: 1280 },
154
+ height: { ideal: 480, max: 720 },
155
+ facingMode: 'user',
156
+ frameRate: { ideal: 30, max: 60 }
157
+ }
158
+ },
159
+ // Medium quality fallback
160
+ {
161
+ video: {
162
+ width: { ideal: 480, max: 640 },
163
+ height: { ideal: 360, max: 480 },
164
+ facingMode: 'user',
165
+ frameRate: { ideal: 15, max: 30 }
166
+ }
167
+ },
168
+ // Basic fallback
169
+ {
170
+ video: { facingMode: 'user' }
171
+ },
172
+ // Last resort - any camera
173
+ {
174
+ video: true
175
  }
176
+ ];
177
 
178
+ // Merge user constraints with defaults
179
+ if (Object.keys(constraints).length > 0) {
180
+ configurations.unshift(constraints);
181
+ }
182
 
183
+ for (let i = 0; i < configurations.length; i++) {
 
 
 
 
184
  try {
185
+ console.log(`Trying camera configuration ${i + 1}/${configurations.length}`);
186
+ const stream = await navigator.mediaDevices.getUserMedia(configurations[i]);
187
+ console.log('Camera access successful with configuration:', configurations[i]);
188
+ return stream;
189
+ } catch (error) {
190
+ console.warn(`Camera configuration ${i + 1} failed:`, error);
191
+ if (i === configurations.length - 1) {
192
+ throw new Error('Unable to access camera. Please ensure camera permissions are granted and no other application is using the camera.');
193
+ }
194
  }
195
  }
196
  }
197
 
198
+ // Check system capabilities
199
+ async function checkSystemCapabilities() {
200
+ try {
201
+ // Check camera availability
202
+ const devices = await navigator.mediaDevices.enumerateDevices();
203
+ const hasCamera = devices.some(device => device.kind === 'videoinput');
204
+
205
+ if (!hasCamera) {
206
+ throw new Error('No camera detected on this device.');
207
+ }
208
 
209
+ // Check server health
210
+ const healthData = await makeRequest('/health-check');
211
+ globalState.modelsAvailable = healthData.models &&
212
+ (healthData.models.yolo_loaded || healthData.models.antispoof_loaded);
213
+
214
+ return {
215
+ camera: hasCamera,
216
+ models: globalState.modelsAvailable,
217
+ database: healthData.database_connected
218
+ };
219
+ } catch (error) {
220
+ console.warn('System capability check failed:', error);
221
+ return { camera: true, models: false, database: false }; // Optimistic defaults
222
+ }
223
+ }
224
+
225
+ // Reusable camera section for Registration/Login with enhanced error handling
226
+ function setupCameraSection(config) {
227
+ const elements = {
228
+ video: document.getElementById(config.videoId),
229
+ canvas: document.getElementById(config.canvasId),
230
+ startBtn: document.getElementById(config.startCameraBtnId),
231
+ captureBtn: document.getElementById(config.captureImageBtnId),
232
+ retakeBtn: document.getElementById(config.retakeImageBtnId),
233
+ overlay: document.getElementById(config.cameraOverlayId),
234
+ imageInput: document.getElementById(config.faceImageInputId),
235
+ actionBtn: document.getElementById(config.actionBtnId)
236
+ };
237
 
238
+ // Validate required elements
239
+ const requiredElements = ['video', 'canvas', 'startBtn', 'captureBtn', 'retakeBtn', 'imageInput'];
240
+ const missingElements = requiredElements.filter(key => !elements[key]);
241
+
242
+ if (missingElements.length > 0) {
243
+ console.warn(`Skipping camera section - missing elements: ${missingElements.join(', ')}`);
 
 
 
 
 
244
  return;
245
  }
246
 
247
+ let stream = null;
248
+ let isCapturing = false;
249
+
250
+ // Enhanced start camera with capability check
251
+ elements.startBtn.addEventListener('click', async function () {
252
+ if (isCapturing) return;
253
+
254
  try {
255
+ isCapturing = true;
256
+ showLoading(elements.startBtn, 'Checking camera...');
257
 
258
+ // Quick capability check
259
+ const capabilities = await checkSystemCapabilities();
260
+ if (!capabilities.camera) {
261
+ throw new Error('No camera available on this device.');
262
+ }
263
+
264
+ showLoading(elements.startBtn, 'Starting camera...');
265
+
266
  stream = await getCameraStream({
267
  video: {
268
  width: { ideal: 400, max: 640 },
 
271
  }
272
  });
273
 
274
+ elements.video.srcObject = stream;
275
+ await elements.video.play();
276
+
277
+ // Update UI
278
+ elements.startBtn.classList.add('d-none');
279
+ elements.captureBtn.classList.remove('d-none');
280
+ elements.retakeBtn.classList.add('d-none');
281
+
282
+ if (elements.overlay) elements.overlay.classList.add('d-none');
283
+
284
+ elements.video.classList.remove('d-none');
285
+ elements.canvas.classList.add('d-none');
286
+
287
+ if (elements.actionBtn) elements.actionBtn.disabled = true;
288
 
 
 
 
 
 
 
 
289
  } catch (err) {
290
+ console.error('Error starting camera:', err);
291
+ alert(err.message || 'Could not access the camera. Please check permissions.');
292
+ hideLoading(elements.startBtn, 'Start Camera', true);
293
  } finally {
294
+ isCapturing = false;
295
+ if (!stream) {
296
+ hideLoading(elements.startBtn, 'Start Camera');
297
+ } else {
298
+ hideLoading(elements.startBtn, 'Camera Active');
299
+ }
300
  }
301
  });
302
 
303
+ // Enhanced capture with quality options
304
+ elements.captureBtn.addEventListener('click', function () {
305
  try {
306
+ if (!stream || elements.video.readyState < 2) {
307
+ alert('Camera not ready. Please wait a moment and try again.');
308
+ return;
309
+ }
310
+
311
+ const context = elements.canvas.getContext('2d');
312
+ elements.canvas.width = elements.video.videoWidth || 400;
313
+ elements.canvas.height = elements.video.videoHeight || 300;
314
+
315
+ // Draw with better quality
316
+ context.drawImage(elements.video, 0, 0, elements.canvas.width, elements.canvas.height);
317
 
318
+ // Use appropriate quality based on use case
319
+ const quality = config.isRegistration ? CONFIG.IMAGE_QUALITY.registration : CONFIG.IMAGE_QUALITY.capture;
320
+ const imageDataURL = elements.canvas.toDataURL('image/jpeg', quality);
321
+ elements.imageInput.value = imageDataURL;
322
 
323
+ // Update UI
324
+ if (elements.overlay) elements.overlay.classList.remove('d-none');
325
+
326
+ elements.captureBtn.classList.add('d-none');
327
+ elements.retakeBtn.classList.remove('d-none');
328
+ elements.video.classList.add('d-none');
329
+ elements.canvas.classList.remove('d-none');
330
+
331
+ if (elements.actionBtn) elements.actionBtn.disabled = false;
332
 
333
+ // Stop camera stream
334
  if (stream) {
335
+ stream.getTracks().forEach(track => track.stop());
336
  stream = null;
337
  }
338
+
339
  } catch (err) {
340
  console.error('Error capturing image:', err);
341
  alert('Failed to capture image. Please try again.');
342
  }
343
  });
344
 
345
+ // Enhanced retake with cleanup
346
+ elements.retakeBtn.addEventListener('click', async function () {
347
  try {
348
+ showLoading(elements.retakeBtn, 'Restarting...');
 
349
 
350
+ // Clear canvas and input
351
+ const context = elements.canvas.getContext('2d');
352
+ context.clearRect(0, 0, elements.canvas.width, elements.canvas.height);
353
+ elements.imageInput.value = '';
354
 
355
+ // Restart camera
356
  stream = await getCameraStream({
357
  video: {
358
  width: { ideal: 400, max: 640 },
 
361
  }
362
  });
363
 
364
+ elements.video.srcObject = stream;
365
+ await elements.video.play();
366
+
367
+ // Update UI
368
+ if (elements.overlay) elements.overlay.classList.add('d-none');
369
+
370
+ elements.captureBtn.classList.remove('d-none');
371
+ elements.retakeBtn.classList.add('d-none');
372
+ elements.video.classList.remove('d-none');
373
+ elements.canvas.classList.add('d-none');
374
+
375
+ if (elements.actionBtn) elements.actionBtn.disabled = true;
376
 
 
 
 
 
 
 
377
  } catch (err) {
378
  console.error('Error restarting camera:', err);
379
  alert(err.message || 'Error restarting camera.');
380
  } finally {
381
+ hideLoading(elements.retakeBtn, 'Retake');
382
+ }
383
+ });
384
+
385
+ // Cleanup on page unload
386
+ window.addEventListener('beforeunload', () => {
387
+ if (stream) {
388
+ stream.getTracks().forEach(track => track.stop());
389
  }
390
  });
391
  }
392
 
393
+ // Enhanced attendance section with robust liveness preview
394
  function setupAttendanceSection(config) {
395
+ const elements = {
396
+ video: document.getElementById(config.videoId),
397
+ canvas: document.getElementById(config.canvasId),
398
+ startBtn: document.getElementById(config.startCameraBtnId),
399
+ captureBtn: document.getElementById(config.captureImageBtnId),
400
+ retakeBtn: document.getElementById(config.retakeImageBtnId),
401
+ markBtn: document.getElementById(config.markBtnId),
402
+ overlayImg: document.getElementById(config.overlayImgId),
403
+ statusEl: document.getElementById(config.statusId),
404
+ // Form fields
405
+ programEl: document.getElementById(config.programId),
406
+ semesterEl: document.getElementById(config.semesterId),
407
+ courseEl: document.getElementById(config.courseId),
408
+ studentIdEl: document.getElementById(config.studentIdInputId)
409
+ };
410
+
411
+ // Validate core elements
412
+ if (!elements.video || !elements.canvas || !elements.startBtn || !elements.captureBtn ||
413
+ !elements.retakeBtn || !elements.markBtn) {
414
+ console.warn('Skipping attendance section - missing core elements');
415
+ return;
416
+ }
417
 
418
  let stream = null;
419
  let capturedDataUrl = '';
420
+ let previewState = {
421
+ active: false,
422
+ busy: false,
423
+ timer: null,
424
+ consecutiveErrors: 0,
425
+ lastSuccessTime: 0
426
+ };
427
 
428
+ // Preview canvas for efficiency
 
 
 
 
 
429
  const previewCanvas = document.createElement('canvas');
430
  const previewCtx = previewCanvas.getContext('2d');
431
 
432
+ function setStatus(msg, type = 'info') {
433
+ if (!elements.statusEl) return;
434
+
435
+ elements.statusEl.textContent = msg || '';
436
+ elements.statusEl.classList.remove('text-success', 'text-danger', 'text-warning', 'text-info');
437
+
438
+ switch (type) {
439
+ case 'error':
440
+ elements.statusEl.classList.add('text-danger');
441
+ break;
442
+ case 'warning':
443
+ elements.statusEl.classList.add('text-warning');
444
+ break;
445
+ case 'success':
446
+ elements.statusEl.classList.add('text-success');
447
+ break;
448
+ default:
449
+ elements.statusEl.classList.add('text-info');
450
  }
451
  }
452
 
453
  function clearOverlay() {
454
+ if (elements.overlayImg) {
455
+ elements.overlayImg.src = '';
456
+ elements.overlayImg.classList.add('d-none');
457
  }
458
  }
459
 
460
  async function startCamera() {
461
  try {
462
+ showLoading(elements.startBtn, 'Initializing camera...');
463
+ setStatus('Starting camera system...');
464
+
465
+ // Check system capabilities first
466
+ const capabilities = await checkSystemCapabilities();
467
+ if (!capabilities.camera) {
468
+ throw new Error('No camera detected. Please ensure a camera is connected.');
469
+ }
470
+
471
+ if (!capabilities.models) {
472
+ setStatus('Warning: Face recognition models may not be fully available', 'warning');
473
+ }
474
+
475
+ showLoading(elements.startBtn, 'Accessing camera...');
476
 
477
  stream = await getCameraStream({
478
  video: {
 
482
  }
483
  });
484
 
485
+ elements.video.srcObject = stream;
486
+ await elements.video.play();
487
+
488
+ // Configure preview canvas
489
+ previewCanvas.width = 480; // Reduced for better network performance
490
+ previewCanvas.height = Math.round(previewCanvas.width *
491
+ (elements.video.videoHeight || 480) / (elements.video.videoWidth || 640));
492
+
493
+ // Update UI
494
+ elements.startBtn.classList.add('d-none');
495
+ elements.captureBtn.classList.remove('d-none');
496
+ elements.retakeBtn.classList.add('d-none');
497
+ elements.markBtn.disabled = true;
498
+ elements.video.classList.remove('d-none');
499
+ elements.canvas.classList.add('d-none');
500
+
501
  clearOverlay();
502
+ setStatus('Camera active. Starting liveness detection...');
503
 
504
+ // Wait for video stabilization before starting preview
 
 
 
 
505
  setTimeout(() => {
506
+ if (stream && elements.video.readyState >= 2) {
507
+ startLivenessPreview();
508
  }
509
+ }, 1500);
510
 
511
  } catch (err) {
512
+ console.error('Error starting camera:', err);
513
+ setStatus(`Camera error: ${err.message}`, 'error');
514
+ alert(err.message || 'Could not access camera. Please check permissions.');
515
  } finally {
516
+ hideLoading(elements.startBtn, 'Start Camera');
 
517
  }
518
  }
519
 
520
+ function startLivenessPreview() {
521
+ if (previewState.active) return;
522
+
523
+ previewState.active = true;
524
+ previewState.consecutiveErrors = 0;
525
+ previewState.lastSuccessTime = Date.now();
526
+
527
+ // Reduced FPS for cloud hosting stability
528
+ const intervalMs = Math.round(1000 / CONFIG.PREVIEW_FPS);
529
+
530
+ previewState.timer = setInterval(async () => {
531
+ if (!previewState.active || previewState.busy || !stream ||
532
+ elements.video.readyState < 2) {
533
+ return;
534
+ }
535
+
536
+ previewState.busy = true;
537
 
 
538
  try {
539
+ // Capture frame at lower quality for preview
540
+ previewCtx.drawImage(elements.video, 0, 0, previewCanvas.width, previewCanvas.height);
541
+ const frameDataUrl = previewCanvas.toDataURL('image/jpeg', CONFIG.IMAGE_QUALITY.preview);
542
 
543
  const data = await makeRequest('/liveness-preview', {
544
  method: 'POST',
 
546
  });
547
 
548
  // Reset error counter on success
549
+ previewState.consecutiveErrors = 0;
550
+ previewState.lastSuccessTime = Date.now();
551
 
552
+ // Update overlay
553
+ if (elements.overlayImg && data.overlay) {
554
+ elements.overlayImg.src = data.overlay;
555
+ elements.overlayImg.classList.remove('d-none');
556
  }
557
 
558
+ // Enhanced status with confidence indicators
559
  if (typeof data.live === 'boolean' && typeof data.live_prob === 'number') {
560
+ const confidence = data.live_prob;
561
+ let confidenceText = confidence >= 0.9 ? 'Excellent' :
562
+ confidence >= 0.8 ? 'Good' :
563
+ confidence >= 0.7 ? 'Fair' : 'Poor';
564
+
565
+ const statusType = data.live ? (confidence >= 0.8 ? 'success' : 'warning') : 'error';
566
+ setStatus(
567
+ `${data.live ? 'LIVE' : 'SPOOF'} detected - Confidence: ${confidenceText} (${confidence.toFixed(2)})`,
568
+ statusType
569
+ );
570
  } else if (data.message) {
571
+ setStatus(data.message, data.success ? 'info' : 'warning');
572
  }
573
 
574
  } catch (error) {
575
+ previewState.consecutiveErrors++;
576
+ const timeSinceLastSuccess = Date.now() - previewState.lastSuccessTime;
577
 
578
+ console.warn(`Preview error ${previewState.consecutiveErrors}/${CONFIG.MAX_CONSECUTIVE_ERRORS}:`, error);
579
+
580
+ if (previewState.consecutiveErrors >= CONFIG.MAX_CONSECUTIVE_ERRORS ||
581
+ timeSinceLastSuccess > 30000) {
582
+ setStatus('Liveness preview temporarily unavailable. You can still capture and mark attendance.', 'warning');
583
+ stopLivenessPreview();
584
+ } else if (previewState.consecutiveErrors === 1) {
585
+ setStatus('Connecting to liveness service...', 'warning');
586
  }
587
  } finally {
588
+ previewState.busy = false;
589
  }
590
  }, intervalMs);
591
  }
592
 
593
+ function stopLivenessPreview() {
594
+ previewState.active = false;
595
+ if (previewState.timer) {
596
+ clearInterval(previewState.timer);
597
+ previewState.timer = null;
598
  }
599
+ previewState.busy = false;
600
+ previewState.consecutiveErrors = 0;
601
  }
602
 
603
+ function captureAttendanceFrame() {
604
+ try {
605
+ if (!stream || elements.video.readyState < 2) {
606
+ setStatus('Camera not ready. Please wait and try again.', 'error');
607
+ return;
608
+ }
609
 
610
+ // Stop preview during capture
611
+ stopLivenessPreview();
612
+
613
+ const ctx = elements.canvas.getContext('2d');
614
+ elements.canvas.width = elements.video.videoWidth || 640;
615
+ elements.canvas.height = elements.video.videoHeight || 480;
616
+ ctx.drawImage(elements.video, 0, 0, elements.canvas.width, elements.canvas.height);
617
+
618
+ capturedDataUrl = elements.canvas.toDataURL('image/jpeg', CONFIG.IMAGE_QUALITY.capture);
619
 
620
+ // Update UI
621
+ elements.captureBtn.classList.add('d-none');
622
+ elements.retakeBtn.classList.remove('d-none');
623
+ elements.markBtn.disabled = false;
624
+ elements.video.classList.add('d-none');
625
+ elements.canvas.classList.remove('d-none');
626
 
627
+ setStatus('Image captured successfully. Ready to mark attendance.', 'success');
 
628
 
629
+ // Stop camera
630
+ if (stream) {
631
+ stream.getTracks().forEach(track => track.stop());
632
+ stream = null;
633
+ }
634
+
635
+ } catch (err) {
636
+ console.error('Error capturing frame:', err);
637
+ setStatus('Failed to capture image. Please try again.', 'error');
638
+ }
639
  }
640
 
641
+ async function retakeAttendanceFrame() {
642
  try {
643
+ showLoading(elements.retakeBtn, 'Restarting camera...');
644
+
645
+ // Clear previous capture
646
+ const ctx = elements.canvas.getContext('2d');
647
+ ctx.clearRect(0, 0, elements.canvas.width, elements.canvas.height);
648
  capturedDataUrl = '';
649
  clearOverlay();
650
+
651
+ // Restart camera
652
  await startCamera();
653
+
654
  } catch (err) {
655
  console.error('Error during retake:', err);
656
+ setStatus('Error restarting camera. Please refresh the page.', 'error');
657
  } finally {
658
+ hideLoading(elements.retakeBtn, 'Retake');
 
659
  }
660
  }
661
 
662
  async function markAttendance() {
663
  try {
664
  if (!capturedDataUrl) {
665
+ setStatus('Please capture an image first.', 'error');
666
  return;
667
  }
668
 
669
+ // Validate form fields
670
  const payload = {
671
+ student_id: (elements.studentIdEl && elements.studentIdEl.value) || null,
672
+ program: (elements.programEl && elements.programEl.value) || '',
673
+ semester: (elements.semesterEl && elements.semesterEl.value) || '',
674
+ course: (elements.courseEl && elements.courseEl.value) || '',
 
 
 
675
  face_image: capturedDataUrl,
676
  };
677
 
678
  if (!payload.program || !payload.semester || !payload.course) {
679
+ setStatus('Please fill in Program, Semester, and Course fields.', 'error');
680
  return;
681
  }
682
 
683
+ showLoading(elements.markBtn, 'Processing attendance...');
684
+ setStatus('Analyzing face and marking attendance... This may take a moment.');
 
685
 
686
  const data = await makeRequest('/mark-attendance', {
687
  method: 'POST',
688
  body: JSON.stringify(payload)
689
  });
690
 
691
+ // Show final overlay with detection results
692
+ if (elements.overlayImg && data.overlay) {
693
+ elements.overlayImg.src = data.overlay;
694
+ elements.overlayImg.classList.remove('d-none');
695
  }
696
 
697
  if (data.success) {
698
+ setStatus('✅ ' + (data.message || 'Attendance marked successfully!'), 'success');
699
+
700
  // Auto-refresh after successful attendance
701
  setTimeout(() => {
702
+ setStatus('Refreshing page...', 'info');
703
  window.location.reload();
704
  }, 3000);
705
  } else {
706
+ const errorMessage = data.message || 'Failed to mark attendance.';
707
+ setStatus('❌ ' + errorMessage, 'error');
708
+
709
+ // Provide helpful suggestions based on error type
710
+ if (errorMessage.includes('already marked')) {
711
+ setStatus('Attendance already marked for this course today.', 'warning');
712
+ } else if (errorMessage.includes('not recognized')) {
713
+ setStatus('Face not recognized. Please ensure good lighting and try again.', 'warning');
714
+ } else if (errorMessage.includes('SPOOF')) {
715
+ setStatus('Spoof detection activated. Please ensure you are physically present and try again.', 'warning');
716
+ }
717
  }
718
 
719
  } catch (err) {
720
  console.error('markAttendance error:', err);
721
+ let errorMsg = 'An error occurred while marking attendance.';
722
+
723
+ if (err.message.includes('Session expired')) {
724
+ errorMsg = 'Session expired. You will be redirected to login.';
725
+ } else if (err.message.includes('timed out')) {
726
+ errorMsg = 'Request timed out. Please check your connection and try again.';
727
+ } else if (err.message.includes('model not available')) {
728
+ errorMsg = 'Face recognition service is temporarily unavailable. Please try again later.';
729
+ }
730
+
731
+ setStatus('❌ ' + errorMsg, 'error');
732
  } finally {
733
+ hideLoading(elements.markBtn, 'Mark Attendance');
 
734
  }
735
  }
736
 
737
+ // Event listeners
738
+ elements.startBtn.addEventListener('click', startCamera);
739
+ elements.captureBtn.addEventListener('click', captureAttendanceFrame);
740
+ elements.retakeBtn.addEventListener('click', retakeAttendanceFrame);
741
+ elements.markBtn.addEventListener('click', markAttendance);
742
 
743
+ // Cleanup handlers
744
  window.addEventListener('beforeunload', () => {
745
+ stopLivenessPreview();
746
+ if (stream) {
747
+ stream.getTracks().forEach(track => track.stop());
748
+ }
749
  });
750
 
751
+ // Handle page visibility changes
752
  document.addEventListener('visibilitychange', () => {
753
+ if (document.hidden) {
754
+ if (previewState.active) {
755
+ stopLivenessPreview();
756
+ }
757
+ } else if (!document.hidden && stream && elements.video.readyState >= 2 && !previewState.active) {
758
+ setTimeout(() => {
759
+ startLivenessPreview();
760
+ }, 1000);
761
  }
762
  });
763
  }
764
 
765
+ // Enhanced auto face login with progressive quality
766
  function setupAutoFaceLogin() {
767
  const autoLoginBtn = document.getElementById('autoFaceLoginBtn');
768
  const roleSelect = document.getElementById('faceRole');
 
772
  autoLoginBtn.addEventListener('click', async function() {
773
  try {
774
  autoLoginBtn.disabled = true;
775
+ showLoading(autoLoginBtn, 'Initializing...');
776
 
777
  const role = roleSelect ? roleSelect.value : 'student';
778
 
779
+ showLoading(autoLoginBtn, 'Accessing camera...');
780
+
781
  const stream = await getCameraStream({
782
  video: {
783
  width: { ideal: 640, max: 1280 },
 
786
  }
787
  });
788
 
789
+ // Create temporary elements
790
  const tempVideo = document.createElement('video');
791
  tempVideo.srcObject = stream;
792
  tempVideo.muted = true;
793
+
794
+ showLoading(autoLoginBtn, 'Preparing camera...');
795
  await tempVideo.play();
796
 
797
+ // Wait for stabilization
798
+ await new Promise(resolve => setTimeout(resolve, 2000));
799
 
800
+ showLoading(autoLoginBtn, 'Capturing image...');
801
+
802
  const tempCanvas = document.createElement('canvas');
803
  const ctx = tempCanvas.getContext('2d');
804
  tempCanvas.width = tempVideo.videoWidth || 640;
805
  tempCanvas.height = tempVideo.videoHeight || 480;
806
  ctx.drawImage(tempVideo, 0, 0, tempCanvas.width, tempCanvas.height);
807
 
808
+ const imageDataURL = tempCanvas.toDataURL('image/jpeg', CONFIG.IMAGE_QUALITY.capture);
809
 
810
+ // Cleanup camera
811
  stream.getTracks().forEach(track => track.stop());
812
 
813
  showLoading(autoLoginBtn, 'Recognizing face...');
 
821
  });
822
 
823
  if (data.success) {
824
+ showLoading(autoLoginBtn, 'Login successful! Redirecting...', false);
825
  setTimeout(() => {
826
  window.location.href = data.redirect_url;
827
+ }, 1500);
828
  } else {
829
+ throw new Error(data.message || 'Face recognition failed. Please try again.');
830
  }
831
 
832
  } catch (err) {
833
  console.error('Auto face login error:', err);
834
  alert(err.message || 'Auto face login failed. Please try manual login.');
835
+ hideLoading(autoLoginBtn, 'Auto Face Login', true);
836
  } finally {
837
+ if (autoLoginBtn.innerHTML === autoLoginBtn.textContent) {
838
+ autoLoginBtn.disabled = false;
839
+ }
840
  }
841
  });
842
  }
843
 
844
+ // Initialize all camera sections
845
+ console.log('Initializing camera systems...');
846
+
847
+ // Student Registration (enhanced quality)
848
  setupCameraSection({
849
  videoId: 'videoStudent',
850
  canvasId: 'canvasStudent',
 
854
  cameraOverlayId: 'cameraOverlayStudent',
855
  faceImageInputId: 'face_image_student',
856
  actionBtnId: 'registerBtnStudent',
857
+ isRegistration: true
858
  });
859
 
860
+ // Teacher Registration (enhanced quality)
861
  setupCameraSection({
862
  videoId: 'videoTeacher',
863
  canvasId: 'canvasTeacher',
 
867
  cameraOverlayId: 'cameraOverlayTeacher',
868
  faceImageInputId: 'face_image_teacher',
869
  actionBtnId: 'registerBtnTeacher',
870
+ isRegistration: true
871
  });
872
 
873
+ // Face Login
874
  setupCameraSection({
875
  videoId: 'video',
876
  canvasId: 'canvas',
 
879
  retakeImageBtnId: 'retakeImage',
880
  cameraOverlayId: 'cameraOverlay',
881
  faceImageInputId: 'face_image',
882
+ actionBtnId: 'faceLoginBtn'
883
  });
884
 
885
+ // Attendance (with liveness preview)
886
  setupAttendanceSection({
887
  videoId: 'videoAttendance',
888
  canvasId: 'canvasAttendance',
 
892
  markBtnId: 'markAttendanceBtn',
893
  overlayImgId: 'attendanceOverlayImg',
894
  statusId: 'attendanceStatus',
 
895
  programId: 'program',
896
  semesterId: 'semester',
897
  courseId: 'course',
898
+ studentIdInputId: 'student_id'
899
  });
900
 
901
+ // Auto face login
902
  setupAutoFaceLogin();
903
 
904
+ // Network status monitoring
905
  window.addEventListener('online', () => {
906
+ globalState.networkOnline = true;
907
+ console.log('Network connection restored');
908
+
909
+ const statusElements = document.querySelectorAll('[id*="Status"]');
910
+ statusElements.forEach(el => {
911
+ if (el.textContent.includes('connection')) {
912
+ el.textContent = 'Connection restored';
913
+ el.className = 'text-success';
914
+ }
915
+ });
916
  });
917
 
918
  window.addEventListener('offline', () => {
919
+ globalState.networkOnline = false;
920
+ console.log('Network connection lost');
921
+
922
+ const statusElements = document.querySelectorAll('[id*="Status"]');
923
+ statusElements.forEach(el => {
924
+ el.textContent = 'No internet connection - Please check your network';
925
+ el.className = 'text-danger';
926
+ });
927
  });
928
+
929
+ // System capability check on load
930
+ checkSystemCapabilities().then(capabilities => {
931
+ console.log('System capabilities:', capabilities);
932
+
933
+ if (!capabilities.camera) {
934
+ console.warn('No camera detected');
935
+ }
936
+
937
+ if (!capabilities.models) {
938
+ console.warn('Face recognition models may not be fully available');
939
+ }
940
+ }).catch(err => {
941
+ console.warn('Failed to check system capabilities:', err);
942
+ });
943
+
944
+ console.log('Camera system initialization complete');
945
+ });
requirements.txt CHANGED
@@ -1,11 +1,11 @@
1
- flask==2.3.3
2
- pymongo==4.3.3
3
  python-dotenv==1.0.0
4
  opencv-python-headless==4.8.1.78
 
 
 
5
  numpy==1.24.3
6
- onnxruntime==1.19.2
7
  requests==2.31.0
8
- deepface==0.0.79
9
- tensorflow==2.13.0
10
- scikit-learn==1.2.2
11
- pillow==9.5.0
 
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