PrashanthB461 commited on
Commit
3e3559c
Β·
verified Β·
1 Parent(s): 69e63d4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +678 -870
app.py CHANGED
@@ -1,8 +1,6 @@
1
  # Suppress TensorFlow oneDNN warnings
2
  import os
3
  os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'
4
- os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
5
- os.environ['CUDA_VISIBLE_DEVICES'] = '-1' # Force CPU usage to avoid GPU issues
6
 
7
  # Standard Library Imports
8
  import base64
@@ -11,55 +9,31 @@ import logging
11
  import queue
12
  import threading
13
  import time
 
 
14
  from datetime import datetime, date
15
  from io import BytesIO
16
- from typing import Tuple, Optional, List, Dict, Any
17
  import pickle
18
- import sqlite3
19
- from pathlib import Path
20
 
21
- # Setup logging first before any other imports that might use it
22
- logging.basicConfig(
23
- level=logging.INFO,
24
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
25
- handlers=[
26
- logging.FileHandler("attendance_system.log"),
27
- logging.StreamHandler()
28
- ]
29
- )
30
- logger = logging.getLogger(__name__)
31
-
32
- # Third-Party Imports - with error handling
33
- try:
34
- import cv2
35
- import gradio as gr
36
- import numpy as np
37
- import pandas as pd
38
- from PIL import Image
39
- import requests
40
- from dotenv import load_dotenv
41
- from retrying import retry
42
- from simple_salesforce import Salesforce
43
-
44
- # Import DeepFace with fallback
45
- try:
46
- from deepface import DeepFace
47
- DEEPFACE_AVAILABLE = True
48
- logger.info("βœ… DeepFace loaded successfully")
49
- except ImportError as e:
50
- logger.warning(f"⚠️ DeepFace not available: {e}")
51
- DEEPFACE_AVAILABLE = False
52
- except Exception as e:
53
- logger.error(f"❌ DeepFace import error: {e}")
54
- DEEPFACE_AVAILABLE = False
55
-
56
- except ImportError as e:
57
- logger.error(f"Missing dependency: {e}")
58
- logger.error("Please install required packages using: pip install -r requirements.txt")
59
- exit(1)
60
 
61
  # --- CONFIGURATION ---
62
 
 
 
 
 
63
  # Load environment variables from .env file
64
  load_dotenv()
65
 
@@ -75,149 +49,6 @@ SF_CREDENTIALS = {
75
  "domain": os.getenv("SF_DOMAIN", "login")
76
  }
77
 
78
- # System configuration
79
- FACE_RECOGNITION_THRESHOLD = float(os.getenv("FACE_THRESHOLD", "10.0"))
80
- CONFIDENCE_THRESHOLD = float(os.getenv("CONFIDENCE_THRESHOLD", "0.95"))
81
- RECOGNITION_COOLDOWN = int(os.getenv("RECOGNITION_COOLDOWN", "5"))
82
-
83
- # --- DATABASE SETUP ---
84
-
85
- class LocalDatabase:
86
- """Local SQLite database for offline functionality and backup."""
87
-
88
- def __init__(self, db_path: str = "data/attendance.db"):
89
- self.db_path = db_path
90
- Path(db_path).parent.mkdir(parents=True, exist_ok=True)
91
- self._init_database()
92
-
93
- def _init_database(self):
94
- """Initialize database tables."""
95
- try:
96
- with sqlite3.connect(self.db_path) as conn:
97
- cursor = conn.cursor()
98
-
99
- # Workers table
100
- cursor.execute('''
101
- CREATE TABLE IF NOT EXISTS workers (
102
- id INTEGER PRIMARY KEY AUTOINCREMENT,
103
- worker_id TEXT UNIQUE NOT NULL,
104
- name TEXT NOT NULL,
105
- face_embedding TEXT NOT NULL,
106
- image_path TEXT,
107
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
108
- synced_to_sf BOOLEAN DEFAULT FALSE
109
- )
110
- ''')
111
-
112
- # Attendance table
113
- cursor.execute('''
114
- CREATE TABLE IF NOT EXISTS attendance (
115
- id INTEGER PRIMARY KEY AUTOINCREMENT,
116
- worker_id TEXT NOT NULL,
117
- worker_name TEXT NOT NULL,
118
- date TEXT NOT NULL,
119
- timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
120
- status TEXT DEFAULT 'Present',
121
- synced_to_sf BOOLEAN DEFAULT FALSE,
122
- UNIQUE(worker_id, date)
123
- )
124
- ''')
125
-
126
- conn.commit()
127
- logger.info("Database initialized successfully")
128
- except Exception as e:
129
- logger.error(f"Database initialization error: {e}")
130
-
131
- def add_worker(self, worker_id: str, name: str, embedding: List[float], image_path: str = None) -> bool:
132
- """Add a new worker to the database."""
133
- try:
134
- with sqlite3.connect(self.db_path) as conn:
135
- cursor = conn.cursor()
136
- cursor.execute('''
137
- INSERT INTO workers (worker_id, name, face_embedding, image_path)
138
- VALUES (?, ?, ?, ?)
139
- ''', (worker_id, name, json.dumps(embedding), image_path))
140
- conn.commit()
141
- return True
142
- except sqlite3.IntegrityError:
143
- logger.warning(f"Worker {worker_id} already exists in database")
144
- return False
145
- except Exception as e:
146
- logger.error(f"Error adding worker to database: {e}")
147
- return False
148
-
149
- def get_all_workers(self) -> List[Dict[str, Any]]:
150
- """Get all workers from the database."""
151
- try:
152
- with sqlite3.connect(self.db_path) as conn:
153
- cursor = conn.cursor()
154
- cursor.execute('SELECT worker_id, name, face_embedding, image_path FROM workers')
155
- workers = []
156
- for row in cursor.fetchall():
157
- workers.append({
158
- 'worker_id': row[0],
159
- 'name': row[1],
160
- 'embedding': json.loads(row[2]),
161
- 'image_path': row[3]
162
- })
163
- return workers
164
- except Exception as e:
165
- logger.error(f"Error fetching workers from database: {e}")
166
- return []
167
-
168
- def mark_attendance(self, worker_id: str, worker_name: str, date_str: str) -> bool:
169
- """Mark attendance for a worker."""
170
- try:
171
- with sqlite3.connect(self.db_path) as conn:
172
- cursor = conn.cursor()
173
- cursor.execute('''
174
- INSERT OR IGNORE INTO attendance (worker_id, worker_name, date)
175
- VALUES (?, ?, ?)
176
- ''', (worker_id, worker_name, date_str))
177
- conn.commit()
178
- return cursor.rowcount > 0
179
- except Exception as e:
180
- logger.error(f"Error marking attendance: {e}")
181
- return False
182
-
183
- def has_attended_today(self, worker_id: str, date_str: str) -> bool:
184
- """Check if worker has already attended today."""
185
- try:
186
- with sqlite3.connect(self.db_path) as conn:
187
- cursor = conn.cursor()
188
- cursor.execute('''
189
- SELECT COUNT(*) FROM attendance
190
- WHERE worker_id = ? AND date = ?
191
- ''', (worker_id, date_str))
192
- return cursor.fetchone()[0] > 0
193
- except Exception as e:
194
- logger.error(f"Error checking attendance: {e}")
195
- return False
196
-
197
- def get_attendance_report(self, start_date: str = None, end_date: str = None) -> pd.DataFrame:
198
- """Get attendance report as pandas DataFrame."""
199
- try:
200
- with sqlite3.connect(self.db_path) as conn:
201
- query = 'SELECT * FROM attendance'
202
- params = []
203
-
204
- if start_date and end_date:
205
- query += ' WHERE date BETWEEN ? AND ?'
206
- params = [start_date, end_date]
207
- elif start_date:
208
- query += ' WHERE date >= ?'
209
- params = [start_date]
210
- elif end_date:
211
- query += ' WHERE date <= ?'
212
- params = [end_date]
213
-
214
- query += ' ORDER BY timestamp DESC'
215
-
216
- return pd.read_sql_query(query, conn, params=params)
217
- except Exception as e:
218
- logger.error(f"Error generating attendance report: {e}")
219
- return pd.DataFrame()
220
-
221
  # --- SALESFORCE CONNECTION ---
222
 
223
  @retry(stop_max_attempt_number=3, wait_fixed=2000)
@@ -230,13 +61,59 @@ def connect_to_salesforce() -> Optional[Salesforce]:
230
  return sf
231
  except Exception as e:
232
  logger.error(f"❌ Salesforce connection failed: {e}")
233
- return None # Don't raise, allow offline mode
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
  # --- CORE LOGIC ---
236
 
237
  class AttendanceSystem:
238
- """Enhanced attendance system with improved error handling and offline support."""
239
-
 
240
  def __init__(self):
241
  # State Management
242
  self.processing_thread = None
@@ -245,374 +122,450 @@ class AttendanceSystem:
245
  self.error_message = None
246
  self.last_processed_frame = None
247
  self.final_log = None
248
- self.processing_stats = {
249
- 'frames_processed': 0,
250
- 'faces_detected': 0,
251
- 'workers_recognized': 0,
252
- 'new_registrations': 0
253
- }
254
 
255
- # Data Storage
256
- self.known_face_embeddings: List[np.ndarray] = []
257
  self.known_face_names: List[str] = []
258
  self.known_face_ids: List[str] = []
 
259
  self.next_worker_id: int = 1
260
 
261
- # Session Tracking
262
  self.last_recognition_time = {}
263
- self.recognition_cooldown = RECOGNITION_COOLDOWN
264
  self.session_log: List[str] = []
 
 
 
 
 
 
 
265
 
266
- # Initialize components
267
- try:
268
- self.db = LocalDatabase()
269
- self.sf = connect_to_salesforce()
270
- self._create_directories()
271
- self.load_worker_data()
272
- logger.info("βœ… Attendance system initialized successfully")
273
- except Exception as e:
274
- logger.error(f"❌ System initialization error: {e}")
275
- self.error_message = f"System initialization failed: {str(e)}"
276
 
277
  def _create_directories(self):
278
- """Create necessary directories."""
279
- directories = ["data/faces", "data/reports", "data/backups"]
280
- for directory in directories:
281
- os.makedirs(directory, exist_ok=True)
282
-
283
- def load_worker_data(self):
284
- """Load worker data with improved error handling and offline support."""
285
- logger.info("Loading worker data...")
286
 
287
- try:
288
- # First try to load from local database
289
- local_workers = self.db.get_all_workers()
290
- if local_workers:
291
- self._load_workers_from_list(local_workers)
292
- logger.info(f"βœ… Loaded {len(local_workers)} workers from local database.")
293
-
294
- # Then sync with Salesforce if available
295
- if self.sf:
296
- try:
297
- self._sync_with_salesforce()
298
- except Exception as e:
299
- logger.error(f"❌ Error syncing with Salesforce: {e}")
300
- else:
301
- logger.warning("Salesforce not connected. Running in offline mode.")
302
- except Exception as e:
303
- logger.error(f"Error loading worker data: {e}")
304
 
305
- def _load_workers_from_list(self, workers: List[Dict[str, Any]]):
306
- """Load workers from a list of worker dictionaries."""
307
- temp_embeddings, temp_names, temp_ids, max_id = [], [], [], 0
 
308
 
309
- for worker in workers:
 
 
 
 
 
 
 
 
 
 
310
  try:
311
- temp_embeddings.append(np.array(worker['embedding']))
312
- temp_names.append(worker['name'])
313
- temp_ids.append(worker['worker_id'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
 
315
- worker_num = int(worker['worker_id'][1:])
316
- if worker_num > max_id:
317
- max_id = worker_num
318
- except (ValueError, TypeError, KeyError) as e:
319
- logger.warning(f"Error processing worker data: {e}")
320
- continue
321
-
322
- self.known_face_embeddings = temp_embeddings
323
- self.known_face_names = temp_names
324
- self.known_face_ids = temp_ids
325
- self.next_worker_id = max_id + 1
 
326
 
327
- def _sync_with_salesforce(self):
328
- """Sync data with Salesforce."""
329
  try:
330
- workers = self.sf.query_all("SELECT Worker_ID__c, Name, Face_Embedding__c FROM Worker__c")['records']
331
-
332
- for worker in workers:
333
- if worker.get('Face_Embedding__c'):
334
- # Add to local database if not exists
335
- self.db.add_worker(
336
- worker['Worker_ID__c'],
337
- worker['Name'],
338
- json.loads(worker['Face_Embedding__c'])
339
- )
340
-
341
- logger.info(f"βœ… Synced {len(workers)} workers from Salesforce.")
342
  except Exception as e:
343
- logger.error(f"❌ Salesforce sync error: {e}")
344
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  def register_worker_manual(self, image: Image.Image, name: str) -> Tuple[str, str]:
346
- """Register a worker manually with enhanced validation."""
347
  if image is None or not name.strip():
348
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
349
 
350
- if not DEEPFACE_AVAILABLE:
351
- return "❌ Face recognition is not available. The system is running in limited mode.", self.get_registered_workers_info()
352
-
353
  try:
354
- # Validate image quality
355
  image_array = np.array(image)
356
- if image_array.shape[0] < 100 or image_array.shape[1] < 100:
357
- return "❌ Image resolution too low. Please use a higher quality image!", self.get_registered_workers_info()
358
 
359
- # Detect face
360
- face_analysis = DeepFace.analyze(
361
- img_path=image_array,
362
- actions=['emotion'],
363
- enforce_detection=True
364
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
 
366
- # Get embedding
367
- embedding = DeepFace.represent(
368
- img_path=image_array,
369
- model_name='Facenet'
370
- )[0]['embedding']
371
 
372
- # Check for duplicates
373
- if self._is_duplicate_face(embedding):
374
- return f"❌ Face matches an existing worker!", self.get_registered_workers_info()
375
 
376
- worker_id = f"W{self.next_worker_id:04d}"
377
  name = name.strip().title()
 
 
 
 
378
 
379
- # Add to system
380
- success = self._add_worker_to_system(worker_id, name, embedding, image_array)
381
- if success:
382
- return f"βœ… {name} registered with ID: {worker_id}!", self.get_registered_workers_info()
383
- else:
384
- return f"❌ Failed to register worker!", self.get_registered_workers_info()
385
-
386
  except ValueError as e:
387
- return "❌ No face detected in the image! Please ensure the face is clearly visible.", self.get_registered_workers_info()
388
  except Exception as e:
389
- logger.error(f"Registration error: {e}")
390
- return f"❌ Registration error: {str(e)}", self.get_registered_workers_info()
391
 
392
- def _register_worker_auto(self, face_image: np.ndarray) -> Optional[Tuple[str, str]]:
393
- """Auto-register a new worker with improved validation."""
394
- if not DEEPFACE_AVAILABLE:
395
- return None
396
-
397
  try:
398
- # Skip very small faces
399
- if face_image.shape[0] < 50 or face_image.shape[1] < 50:
400
- return None
401
-
402
- embedding = DeepFace.represent(
403
- img_path=face_image,
404
- model_name='Facenet',
405
- enforce_detection=False
406
- )[0]['embedding']
407
 
408
- if self._is_duplicate_face(embedding):
 
 
409
  return None
410
 
411
- worker_id = f"W{self.next_worker_id:04d}"
412
- worker_name = f"Unknown Worker {self.next_worker_id}"
413
 
414
- success = self._add_worker_to_system(worker_id, worker_name, embedding, face_image)
415
- if success:
416
- log_msg = f"πŸ†• [{datetime.now().strftime('%H:%M:%S')}] Auto-registered: {worker_name} ({worker_id})"
417
- self.session_log.append(log_msg)
418
- logger.info(log_msg)
419
- self.processing_stats['new_registrations'] += 1
420
- return worker_id, worker_name
421
-
422
- except Exception as e:
423
- logger.error(f"❌ Auto-registration error: {e}")
424
-
425
- return None
426
-
427
- def _add_worker_to_system(self, worker_id: str, name: str, embedding: List[float], image_array: np.ndarray) -> bool:
428
- """Add worker to system with improved error handling."""
429
- try:
430
- # Add to memory
431
- self.known_face_embeddings.append(np.array(embedding))
432
- self.known_face_names.append(name)
433
- self.known_face_ids.append(worker_id)
434
- self.next_worker_id += 1
435
 
436
- # Save face image
437
- face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
438
- image_path = f"data/faces/{worker_id}.jpg"
439
- face_pil.save(image_path)
440
 
441
- # Add to local database
442
- db_success = self.db.add_worker(worker_id, name, embedding, image_path)
443
 
444
- # Sync to Salesforce if available
445
- if self.sf:
446
- try:
447
- caption = self._get_image_caption(face_pil)
448
- worker_record = self.sf.Worker__c.create({
449
- 'Name': name,
450
- 'Worker_ID__c': worker_id,
451
- 'Face_Embedding__c': json.dumps(embedding),
452
- 'Image_Caption__c': caption
453
- })
454
-
455
- image_url = self._upload_image_to_salesforce(face_pil, worker_record['id'], worker_id)
456
- if image_url:
457
- self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
458
-
459
- logger.info(f"βœ… Worker {worker_id} synced to Salesforce.")
460
- except Exception as e:
461
- logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
462
 
463
- return db_success
464
 
465
  except Exception as e:
466
- logger.error(f"❌ Error adding worker to system: {e}")
467
- return False
468
 
469
- def _is_duplicate_face(self, embedding: List[float], threshold: float = None) -> bool:
470
- """Check for duplicate faces with configurable threshold."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  if not self.known_face_embeddings:
472
  return False
473
 
474
- threshold = threshold or FACE_RECOGNITION_THRESHOLD
475
- try:
476
- distances = [
477
- np.linalg.norm(np.array(embedding) - known_embedding)
478
- for known_embedding in self.known_face_embeddings
479
- ]
480
- return min(distances) < threshold
481
- except Exception as e:
482
- logger.error(f"Error checking duplicate face: {e}")
483
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
 
485
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
486
- """Mark attendance with improved tracking."""
487
- today_str = date.today().isoformat()
 
 
 
488
 
489
- # Check cooldown period
490
- last_seen = self.last_recognition_time.get(worker_id)
491
- if last_seen and (time.time() - last_seen < self.recognition_cooldown):
 
492
  return False
493
 
494
- # Check if already attended today
495
- if self.db.has_attended_today(worker_id, today_str):
 
496
  return False
497
 
498
- # Mark attendance in local database
499
- db_success = self.db.mark_attendance(worker_id, worker_name, today_str)
500
 
501
- # Sync to Salesforce if available
502
  if self.sf:
503
  try:
504
- current_time = datetime.now()
505
  self.sf.Attendance__c.create({
506
  'Worker_ID__c': worker_id,
507
  'Name__c': worker_name,
508
  'Date__c': today_str,
509
- 'Timestamp__c': current_time.isoformat(),
510
  'Status__c': "Present"
511
  })
512
  except Exception as e:
513
  logger.error(f"❌ Error saving attendance to Salesforce: {e}")
514
 
515
- if db_success:
516
- current_time = datetime.now()
517
- log_msg = f"βœ… [{current_time.strftime('%H:%M:%S')}] Marked Present: {worker_name} ({worker_id})"
518
- self.session_log.append(log_msg)
519
- self.processing_stats['workers_recognized'] += 1
520
 
521
- return db_success
 
 
 
 
522
 
 
523
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
524
- """Enhanced frame processing with better error handling."""
525
- if not DEEPFACE_AVAILABLE:
526
- # Add overlay for limited mode
527
- overlay = frame.copy()
528
- cv2.rectangle(overlay, (0, 0), (frame.shape[1], 120), (0, 0, 0), -1)
529
- cv2.addWeighted(overlay, 0.7, frame, 0.3, 0, frame)
530
-
531
- # Draw messages
532
- cv2.putText(frame, "SYSTEM RUNNING IN LIMITED MODE", (10, 30),
533
- cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)
534
- cv2.putText(frame, "Face recognition is not available", (10, 60),
535
- cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
536
- cv2.putText(frame, "System can still manage workers and generate reports", (10, 90),
537
- cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
538
-
539
- # Add timestamp
540
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
541
- cv2.putText(frame, f"Time: {timestamp}", (10, frame.shape[0] - 20),
542
- cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
543
 
544
- # Log limited mode activity
545
- if not hasattr(self, '_limited_mode_logged'):
546
- log_msg = f"πŸ”„ [{datetime.now().strftime('%H:%M:%S')}] System running in limited mode - Face recognition unavailable"
547
- self.session_log.append(log_msg)
548
- logger.info(log_msg)
549
- self._limited_mode_logged = True
550
 
551
- return frame
552
-
553
- try:
554
- self.processing_stats['frames_processed'] += 1
 
 
 
 
555
 
556
- # Extract faces with improved error handling
557
- try:
558
- face_objs = DeepFace.extract_faces(
559
- img_path=frame,
560
- detector_backend='opencv',
561
- enforce_detection=False
562
- )
563
- except Exception as e:
564
- logger.warning(f"Face extraction error: {e}")
565
- return frame
566
 
567
  if face_objs:
568
- self.processing_stats['faces_detected'] += len(face_objs)
569
 
570
  for i, face_obj in enumerate(face_objs):
571
- try:
572
- confidence = face_obj.get('confidence', 0)
573
-
574
- if confidence < CONFIDENCE_THRESHOLD:
575
- continue
 
576
 
577
- # Extract facial area coordinates - FIXED
578
- facial_area = face_obj['facial_area']
579
- x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
580
-
581
- # Validate coordinates
582
- if x < 0 or y < 0 or w <= 0 or h <= 0:
583
- continue
584
-
585
- face_image = frame[y:y+h, x:x+w]
586
-
587
- if face_image.size == 0:
588
- continue
589
 
590
- # Get face embedding
591
- try:
592
- embedding = DeepFace.represent(
593
- img_path=face_image,
594
- model_name='Facenet',
595
- enforce_detection=False
596
- )[0]['embedding']
597
- except Exception as e:
598
- logger.warning(f"Embedding extraction error: {e}")
599
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
600
 
601
- # Face recognition
602
- color, worker_id, worker_name = self._recognize_face(embedding, face_image)
603
 
604
- # Draw bounding box and label
605
- label = f"{worker_name}" + (f" ({worker_id})" if worker_id else "")
606
- cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
 
 
 
 
 
 
 
 
607
 
608
- # Add background for text
609
- label_size = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.7, 2)[0]
610
- cv2.rectangle(frame, (x, y-30), (x + label_size[0], y), color, -1)
611
- cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
612
 
613
- except Exception as e:
614
- logger.warning(f"Error processing face {i}: {e}")
615
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
616
 
617
  return frame
618
 
@@ -620,481 +573,336 @@ class AttendanceSystem:
620
  logger.error(f"ERROR in process_frame: {e}")
621
  return frame
622
 
623
- def _recognize_face(self, embedding: List[float], face_image: np.ndarray) -> Tuple[Tuple[int, int, int], Optional[str], str]:
624
- """Recognize face and return color, worker_id, and worker_name."""
625
- if not self.known_face_embeddings:
626
- return (0, 0, 255), None, "Unknown" # Red for unknown
627
-
628
- try:
629
- distances = [
630
- np.linalg.norm(np.array(embedding) - known)
631
- for known in self.known_face_embeddings
632
- ]
633
- min_dist = min(distances)
634
- match_index = distances.index(min_dist) if min_dist < FACE_RECOGNITION_THRESHOLD else -1
635
-
636
- if match_index != -1:
637
- worker_id = self.known_face_ids[match_index]
638
- worker_name = self.known_face_names[match_index]
639
-
640
- # Mark attendance
641
- if self.mark_attendance(worker_id, worker_name):
642
- self.last_recognition_time[worker_id] = time.time()
643
-
644
- return (0, 255, 0), worker_id, worker_name # Green for recognized
645
- else:
646
- # Try auto-registration
647
- new_worker = self._register_worker_auto(face_image)
648
- if new_worker:
649
- worker_id, worker_name = new_worker[0], new_worker[1]
650
- if self.mark_attendance(worker_id, worker_name):
651
- self.last_recognition_time[worker_id] = time.time()
652
- return (0, 165, 255), worker_id, worker_name # Orange for new
653
-
654
- return (0, 0, 255), None, "Unknown" # Red for unknown
655
- except Exception as e:
656
- logger.error(f"Face recognition error: {e}")
657
- return (0, 0, 255), None, "Error"
658
-
659
  def _processing_loop(self, source):
660
- """Enhanced processing loop with better error handling."""
661
- video_capture = None
662
- try:
663
- video_capture = cv2.VideoCapture(source)
664
- if not video_capture.isOpened():
665
- err_msg = f"❌ **Error:** Could not open video source. Please check the source and try again."
666
- self.error_message = err_msg
667
- self.is_processing.clear()
668
- return
669
-
670
- # Reset statistics
671
- self.processing_stats = {
672
- 'frames_processed': 0,
673
- 'faces_detected': 0,
674
- 'workers_recognized': 0,
675
- 'new_registrations': 0
676
- }
 
 
 
 
 
 
 
677
 
678
- # Add initial log entry
679
- start_msg = f"πŸš€ [{datetime.now().strftime('%H:%M:%S')}] Processing started"
680
- if not DEEPFACE_AVAILABLE:
681
- start_msg += " (Limited Mode)"
682
- self.session_log.append(start_msg)
683
- logger.info(start_msg)
684
-
685
- while self.is_processing.is_set():
686
- ret, frame = video_capture.read()
687
- if not ret:
688
- break
689
-
690
- processed_frame = self.process_frame(frame)
691
-
692
- if not self.frame_queue.full():
693
- self.frame_queue.put(processed_frame)
694
-
695
- self.last_processed_frame = processed_frame
696
- time.sleep(0.05) # Control frame rate
697
-
698
- # Add periodic activity log in limited mode
699
- if not DEEPFACE_AVAILABLE and self.processing_stats['frames_processed'] % 100 == 0:
700
- activity_msg = f"πŸ“Ή [{datetime.now().strftime('%H:%M:%S')}] Processed {self.processing_stats['frames_processed']} frames"
701
- self.session_log.append(activity_msg)
702
-
703
- except Exception as e:
704
- logger.error(f"Processing loop error: {e}")
705
- self.error_message = f"❌ Processing error: {str(e)}"
706
- finally:
707
- if video_capture:
708
- video_capture.release()
709
 
710
- # Add final log entry
711
- end_msg = f"⏹️ [{datetime.now().strftime('%H:%M:%S')}] Processing stopped"
712
- self.session_log.append(end_msg)
713
- logger.info(end_msg)
714
 
715
- self.final_log = self.session_log.copy()
716
- self.is_processing.clear()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
717
 
718
  def start_processing(self, source) -> str:
719
- """Start processing with enhanced validation."""
720
  if self.is_processing.is_set():
721
  return "⚠️ Processing is already active."
722
 
723
- if source is None:
724
- return "❌ Please provide a valid input source."
725
-
726
- # Reset states for the new session
727
  self.session_log.clear()
 
728
  self.last_recognition_time.clear()
729
  self.error_message = None
730
  self.last_processed_frame = None
731
  self.final_log = None
732
- self._limited_mode_logged = False # Reset limited mode logging flag
 
 
 
733
 
734
  self.is_processing.set()
735
  self.processing_thread = threading.Thread(target=self._processing_loop, args=(source,))
736
  self.processing_thread.daemon = True
737
  self.processing_thread.start()
738
 
 
739
 
740
  def stop_processing(self) -> str:
741
- """Stop processing and generate summary."""
742
- if not self.is_processing.is_set():
743
- return "⚠️ No processing is currently active."
744
-
745
  self.is_processing.clear()
 
746
 
747
- # Generate processing summary
748
- stats = self.processing_stats
749
- summary = f"""βœ… Processing stopped.
750
- πŸ“Š Session Summary:
751
- - Frames processed: {stats['frames_processed']}
752
- - Faces detected: {stats['faces_detected']}
753
- - Workers recognized: {stats['workers_recognized']}
754
- - New registrations: {stats['new_registrations']}"""
755
 
756
- return summary
757
-
758
- def get_processing_stats(self) -> str:
759
- """Get current processing statistics."""
760
- stats = self.processing_stats
761
- return f"""πŸ“Š **Processing Statistics:**
762
- - Frames processed: {stats['frames_processed']}
763
- - Faces detected: {stats['faces_detected']}
764
- - Workers recognized: {stats['workers_recognized']}
765
- - New registrations: {stats['new_registrations']}"""
766
-
767
- def generate_attendance_report(self, start_date: str = None, end_date: str = None) -> pd.DataFrame:
768
- """Generate attendance report."""
769
- return self.db.get_attendance_report(start_date, end_date)
770
-
771
- def export_attendance_csv(self, start_date: str = None, end_date: str = None) -> str:
772
- """Export attendance data to CSV."""
773
- try:
774
- df = self.generate_attendance_report(start_date, end_date)
775
- if df.empty:
776
- return "❌ No attendance data found for the specified period."
777
-
778
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
779
- filename = f"data/reports/attendance_report_{timestamp}.csv"
780
- df.to_csv(filename, index=False)
781
-
782
- return f"βœ… Attendance report exported to: {filename}"
783
- except Exception as e:
784
- logger.error(f"Error exporting attendance report: {e}")
785
- return f"❌ Error exporting report: {str(e)}"
786
 
787
  # --- Helper & Reporting ---
788
  def _get_image_caption(self, image: Image.Image) -> str:
789
- """Generate image caption using Hugging Face API."""
790
  if not HF_API_TOKEN:
791
  return "Hugging Face API token not configured."
792
-
793
  try:
794
  buffered = BytesIO()
795
  image.save(buffered, format="JPEG")
796
  img_data = buffered.getvalue()
797
  headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
798
-
799
- response = requests.post(HF_API_URL, headers=headers, data=img_data, timeout=30)
800
  response.raise_for_status()
801
  result = response.json()
802
-
803
  return result[0].get("generated_text", "No caption found.")
804
- except requests.exceptions.Timeout:
805
- return "Caption generation timed out."
806
  except Exception as e:
807
  logger.error(f"Hugging Face API error: {e}")
808
  return "Caption generation failed."
809
 
810
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
811
- """Upload image to Salesforce with error handling."""
812
  if not self.sf:
813
  return None
814
-
815
  try:
816
  buffered = BytesIO()
817
  image.save(buffered, format="JPEG")
818
  encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
819
-
820
  cv = self.sf.ContentVersion.create({
821
  'Title': f'Image_{worker_id}',
822
  'PathOnClient': f'{worker_id}.jpg',
823
  'VersionData': encoded_image,
824
  'FirstPublishLocationId': record_id
825
  })
826
-
827
- return f"/{cv['id']}" # Relative URL
828
  except Exception as e:
829
  logger.error(f"Salesforce image upload error: {e}")
830
  return None
831
 
832
  def get_registered_workers_info(self) -> str:
833
- """Get information about registered workers."""
 
 
 
834
  try:
835
- workers = self.db.get_all_workers()
836
- if not workers:
837
- return "No workers registered."
838
 
839
- worker_list = "\n".join([
840
- f"- **{w['name']}** (ID: {w['worker_id']})"
841
- for w in workers
842
- ])
843
 
844
- return f"**πŸ‘₯ Registered Workers ({len(workers)})**\n{worker_list}"
845
- except Exception as e:
846
- return f"Error: {e}"
 
847
 
848
- def get_system_status(self) -> str:
849
- """Get comprehensive system status."""
850
- sf_status = "βœ… Connected" if self.sf else "❌ Offline"
851
- deepface_status = "βœ… Available" if DEEPFACE_AVAILABLE else "❌ Not Available (Limited Mode)"
852
- db_workers = len(self.db.get_all_workers())
 
853
 
854
- status = f"""πŸ”§ **System Status:**
855
- - Salesforce: {sf_status}
856
- - Face Recognition: {deepface_status}
857
- - Registered Workers: {db_workers}
858
- - Recognition Threshold: {FACE_RECOGNITION_THRESHOLD}
859
- - Confidence Threshold: {CONFIDENCE_THRESHOLD}
860
- - Processing: {'βœ… Active' if self.is_processing.is_set() else '⏹️ Stopped'}
861
-
862
- {'⚠️ **Note:** Face recognition is disabled. System running in limited mode.' if not DEEPFACE_AVAILABLE else ''}"""
863
 
864
- return status
865
-
866
  # --- GRADIO UI ---
 
867
 
868
  def create_interface():
869
- """Create enhanced Gradio interface with better error handling."""
870
- try:
871
- attendance_system = AttendanceSystem()
872
-
873
- # Check if system is in limited mode
874
- limited_mode = not DEEPFACE_AVAILABLE
875
 
876
- with gr.Blocks(theme=gr.themes.Soft(), title="Advanced Attendance System") as demo:
877
- gr.Markdown("# 🎯 Advanced Face Recognition Attendance System")
878
- if limited_mode:
879
- gr.Markdown("⚠️ **RUNNING IN LIMITED MODE** - Face recognition is not available")
880
- gr.Markdown("*Some features may be disabled. Please check system requirements.*")
881
- else:
882
- gr.Markdown("*Enhanced with offline support, statistics, and reporting features*")
883
-
884
- with gr.Tabs():
885
- # Main Controls Tab
886
- with gr.Tab("βš™οΈ Controls & Status"):
887
- gr.Markdown("### 1. Choose Input Source & Start Processing")
888
- with gr.Row():
889
- with gr.Column(scale=1):
890
- selected_tab_index = gr.Number(value=0, visible=False)
891
- with gr.Tabs() as video_tabs:
892
- with gr.Tab("Live Camera", id=0):
893
- camera_source = gr.Number(label="Camera Source", value=0, precision=0)
894
- with gr.Tab("Upload Video", id=1):
895
- video_file = gr.Video(label="Upload Video File", sources=["upload"])
896
- with gr.Column(scale=1):
897
- start_btn = gr.Button("▢️ Start Processing", variant="primary")
898
- stop_btn = gr.Button("⏹️ Stop Processing", variant="stop")
899
- status_box = gr.Textbox(label="Status", interactive=False, value="System Ready.")
900
-
901
- gr.Markdown("### 2. System Information")
902
- with gr.Row():
903
- system_status = gr.Markdown(value=attendance_system.get_system_status())
904
- processing_stats = gr.Markdown(value="πŸ“Š **Processing Statistics:** Not started")
905
- refresh_status_btn = gr.Button("πŸ”„ Refresh Status")
906
-
907
- gr.Markdown("### 3. View Results in the 'Output & Log' Tab")
908
- gr.Markdown("**🎨 Color Coding:** <font color='green'>Green</font> = Known, <font color='orange'>Orange</font> = New, <font color='red'>Red</font> = Unknown")
909
 
910
- # Output Tab
911
- with gr.Tab("πŸ“Š Output & Log"):
912
- with gr.Row():
913
- with gr.Column(scale=2):
914
- video_output = gr.Image(label="Recognition Output", interactive=False)
915
- with gr.Column(scale=1):
916
- session_log_display = gr.Markdown(label="πŸ“‹ Session Log", value="System is ready.")
917
-
918
- # Worker Management Tab
919
- with gr.Tab("πŸ‘€ Worker Management"):
920
- with gr.Row():
921
- with gr.Column():
922
- register_image = gr.Image(label="Upload Worker's Photo", type="pil")
923
- register_name = gr.Textbox(label="Worker's Full Name")
924
- register_btn = gr.Button("Register Worker", variant="primary")
925
- register_output = gr.Textbox(label="Registration Status", interactive=False)
926
- with gr.Column():
927
- registered_workers_info = gr.Markdown(value=attendance_system.get_registered_workers_info())
928
- refresh_workers_btn = gr.Button("πŸ”„ Refresh List")
929
-
930
- # Reports Tab
931
- with gr.Tab("πŸ“ˆ Reports & Analytics"):
932
- gr.Markdown("### Attendance Reports")
933
- with gr.Row():
934
- start_date = gr.Textbox(label="Start Date (YYYY-MM-DD)", placeholder="2024-01-01")
935
- end_date = gr.Textbox(label="End Date (YYYY-MM-DD)", placeholder="2024-12-31")
936
-
937
- with gr.Row():
938
- generate_report_btn = gr.Button("πŸ“Š Generate Report", variant="primary")
939
- export_csv_btn = gr.Button("πŸ’Ύ Export CSV")
940
 
941
- report_output = gr.Dataframe(label="Attendance Report")
942
- export_status = gr.Textbox(label="Export Status", interactive=False)
 
 
 
 
 
 
943
 
944
- # --- Event Handlers ---
945
- def on_tab_select(evt: gr.SelectData):
946
- return evt.index
947
-
948
- video_tabs.select(fn=on_tab_select, inputs=None, outputs=[selected_tab_index])
949
-
950
- def start_wrapper(tab_index, cam_src, vid_path):
951
- try:
952
- source = cam_src if tab_index == 0 else vid_path
953
- if source is None:
954
- return "❌ Please provide an input source."
955
- return attendance_system.start_processing(source)
956
- except Exception as e:
957
- logger.error(f"Start processing error: {e}")
958
- return f"❌ Error starting processing: {str(e)}"
959
-
960
- def stop_wrapper():
961
- try:
962
- return attendance_system.stop_processing()
963
- except Exception as e:
964
- logger.error(f"Stop processing error: {e}")
965
- return f"❌ Error stopping processing: {str(e)}"
966
-
967
- def refresh_status():
968
- try:
969
- return attendance_system.get_system_status(), attendance_system.get_processing_stats()
970
- except Exception as e:
971
- logger.error(f"Refresh status error: {e}")
972
- return f"❌ Error: {str(e)}", f"❌ Error: {str(e)}"
973
-
974
- def generate_report(start_date_val, end_date_val):
975
- try:
976
- df = attendance_system.generate_attendance_report(
977
- start_date_val if start_date_val else None,
978
- end_date_val if end_date_val else None
979
- )
980
- return df
981
- except Exception as e:
982
- logger.error(f"Report generation error: {e}")
983
- return pd.DataFrame()
984
-
985
- def export_csv_wrapper(start_date_val, end_date_val):
986
  try:
987
- return attendance_system.export_attendance_csv(
988
- start_date_val if start_date_val else None,
989
- end_date_val if end_date_val else None
990
- )
991
- except Exception as e:
992
- logger.error(f"CSV export error: {e}")
993
- return f"❌ Export error: {str(e)}"
994
-
995
- # Connect event handlers
996
- start_btn.click(
997
- fn=start_wrapper,
998
- inputs=[selected_tab_index, camera_source, video_file],
999
- outputs=[status_box]
1000
- )
1001
-
1002
- stop_btn.click(fn=stop_wrapper, inputs=None, outputs=[status_box])
1003
-
1004
- register_btn.click(
1005
- fn=attendance_system.register_worker_manual,
1006
- inputs=[register_image, register_name],
1007
- outputs=[register_output, registered_workers_info]
1008
- )
1009
-
1010
- refresh_workers_btn.click(
1011
- fn=attendance_system.get_registered_workers_info,
1012
- outputs=[registered_workers_info]
1013
- )
1014
-
1015
- refresh_status_btn.click(
1016
- fn=refresh_status,
1017
- outputs=[system_status, processing_stats]
1018
- )
1019
-
1020
- generate_report_btn.click(
1021
- fn=generate_report,
1022
- inputs=[start_date, end_date],
1023
- outputs=[report_output]
1024
- )
1025
-
1026
- export_csv_btn.click(
1027
- fn=export_csv_wrapper,
1028
- inputs=[start_date, end_date],
1029
- outputs=[export_status]
1030
- )
1031
-
1032
- # UI Update Generator
1033
- def update_ui_generator():
1034
- while True:
1035
- try:
1036
- if attendance_system.error_message:
1037
- yield None, attendance_system.error_message
1038
- time.sleep(2)
1039
- attendance_system.error_message = None
1040
- continue
1041
 
1042
- if attendance_system.is_processing.is_set():
1043
- frame = None
1044
- log_entries = attendance_system.session_log
1045
- if log_entries:
1046
- log_md = "\n".join(reversed(log_entries))
 
 
 
 
 
 
 
 
 
 
1047
  else:
1048
- log_md = "πŸ”„ Processing started... Waiting for activity..."
 
 
 
 
1049
 
1050
- try:
1051
- if not attendance_system.frame_queue.empty():
1052
- frame = attendance_system.frame_queue.get_nowait()
1053
- if frame is not None:
1054
- frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
1055
- except queue.Empty:
1056
- pass
1057
- yield frame, log_md
1058
  else:
1059
- if attendance_system.last_processed_frame is not None:
1060
- final_frame = cv2.cvtColor(attendance_system.last_processed_frame, cv2.COLOR_BGR2RGB)
1061
- if attendance_system.final_log:
1062
- final_log_md = "\n".join(reversed(attendance_system.final_log))
1063
- else:
1064
- final_log_md = "⏹️ Processing complete. Check system status for more information."
1065
- yield final_frame, final_log_md
1066
- else:
1067
- yield None, "πŸ”§ System ready. Go to 'Controls & Status' to start processing."
1068
-
1069
- time.sleep(0.1)
1070
- except Exception as e:
1071
- logger.error(f"UI update error: {e}")
1072
- yield None, f"❌ UI Error: {str(e)}\n\nPlease refresh the page or restart the system."
1073
- time.sleep(1)
1074
 
1075
- demo.load(fn=update_ui_generator, outputs=[video_output, session_log_display])
1076
-
1077
- return demo
1078
 
1079
- except Exception as e:
1080
- logger.error(f"Interface creation error: {e}")
1081
- # Create a simple error interface
1082
- with gr.Blocks() as error_demo:
1083
- gr.Markdown(f"# ❌ System Error\n\nFailed to initialize the attendance system:\n\n`{str(e)}`\n\nPlease check the logs and ensure all dependencies are installed.")
1084
- return error_demo
1085
 
1086
  if __name__ == "__main__":
1087
- try:
1088
- app = create_interface()
1089
- app.queue()
1090
- app.launch(
1091
- server_name="0.0.0.0",
1092
- server_port=7860,
1093
- show_error=True,
1094
- debug=True,
1095
- share=False # Set to True if you want to create a public link
1096
- )
1097
- except Exception as e:
1098
- logger.error(f"Failed to launch application: {e}")
1099
- print(f"❌ Application failed to start: {e}")
1100
- print("Please check the requirements.txt file and install missing dependencies.")
 
1
  # Suppress TensorFlow oneDNN warnings
2
  import os
3
  os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'
 
 
4
 
5
  # Standard Library Imports
6
  import base64
 
9
  import queue
10
  import threading
11
  import time
12
+ import hashlib
13
+ import uuid
14
  from datetime import datetime, date
15
  from io import BytesIO
16
+ from typing import Tuple, Optional, List, Dict
17
  import pickle
 
 
18
 
19
+ # Third-Party Imports
20
+ import cv2
21
+ import gradio as gr
22
+ import numpy as np
23
+ import pandas as pd
24
+ from PIL import Image
25
+ import requests
26
+ from dotenv import load_dotenv
27
+ from deepface import DeepFace
28
+ from retrying import retry
29
+ from simple_salesforce import Salesforce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
  # --- CONFIGURATION ---
32
 
33
+ # Setup logging
34
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
35
+ logger = logging.getLogger(__name__)
36
+
37
  # Load environment variables from .env file
38
  load_dotenv()
39
 
 
49
  "domain": os.getenv("SF_DOMAIN", "login")
50
  }
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  # --- SALESFORCE CONNECTION ---
53
 
54
  @retry(stop_max_attempt_number=3, wait_fixed=2000)
 
61
  return sf
62
  except Exception as e:
63
  logger.error(f"❌ Salesforce connection failed: {e}")
64
+ raise
65
+
66
+ # --- FACE QUALITY ASSESSMENT ---
67
+
68
+ def assess_face_quality(face_image: np.ndarray, facial_area: Dict) -> Tuple[bool, Dict]:
69
+ """
70
+ Assess the quality of a detected face for registration/recognition.
71
+ Returns (is_good_quality, quality_metrics)
72
+ """
73
+ h, w = face_image.shape[:2]
74
+
75
+ # 1. Face size check (minimum 80x80 pixels)
76
+ min_face_size = 80
77
+ size_check = min(w, h) >= min_face_size
78
+
79
+ # 2. Blur detection using Laplacian variance
80
+ gray = cv2.cvtColor(face_image, cv2.COLOR_BGR2GRAY) if len(face_image.shape) == 3 else face_image
81
+ blur_score = cv2.Laplacian(gray, cv2.CV_64F).var()
82
+ blur_threshold = 100 # Higher values = less blur
83
+ blur_check = blur_score > blur_threshold
84
+
85
+ # 3. Brightness check
86
+ brightness = np.mean(gray)
87
+ brightness_check = 50 < brightness < 200 # Not too dark or too bright
88
+
89
+ # 4. Face detection confidence (from facial_area if available)
90
+ confidence_check = facial_area.get('confidence', 0) > 0.98
91
+
92
+ # 5. Face area ratio (face should take reasonable portion of the detection area)
93
+ face_area = w * h
94
+ detection_area = facial_area.get('w', w) * facial_area.get('h', h)
95
+ area_ratio = face_area / max(detection_area, 1)
96
+ area_check = area_ratio > 0.6
97
+
98
+ quality_metrics = {
99
+ 'size_score': min(w, h),
100
+ 'blur_score': blur_score,
101
+ 'brightness_score': brightness,
102
+ 'confidence_score': facial_area.get('confidence', 0),
103
+ 'area_ratio': area_ratio
104
+ }
105
+
106
+ # All checks must pass for good quality
107
+ is_good_quality = all([size_check, blur_check, brightness_check, confidence_check, area_check])
108
+
109
+ return is_good_quality, quality_metrics
110
 
111
  # --- CORE LOGIC ---
112
 
113
  class AttendanceSystem:
114
+ """
115
+ Manages all backend logic for the face recognition attendance system.
116
+ """
117
  def __init__(self):
118
  # State Management
119
  self.processing_thread = None
 
122
  self.error_message = None
123
  self.last_processed_frame = None
124
  self.final_log = None
 
 
 
 
 
 
125
 
126
+ # Data Storage with improved structure
127
+ self.known_face_embeddings: List[List[np.ndarray]] = [] # Multiple embeddings per person
128
  self.known_face_names: List[str] = []
129
  self.known_face_ids: List[str] = []
130
+ self.worker_metadata: Dict[str, Dict] = {} # Store additional info per worker
131
  self.next_worker_id: int = 1
132
 
133
+ # Session Tracking with improvements
134
  self.last_recognition_time = {}
135
+ self.recognition_cooldown = 5
136
  self.session_log: List[str] = []
137
+ self.daily_attendance: set = set() # Track who attended today
138
+ self.session_attendees: set = set() # Track who attended this session
139
+
140
+ # Processing optimization
141
+ self.frame_skip_count = 0
142
+ self.frame_skip_rate = 2 # Process every 3rd frame for speed
143
+ self.processing_resolution = (640, 480) # Reduce resolution for speed
144
 
145
+ # Initialize
146
+ self.sf = connect_to_salesforce()
147
+ self._create_directories()
148
+ self.load_worker_data()
149
+ self._load_daily_attendance()
 
 
 
 
 
150
 
151
  def _create_directories(self):
152
+ os.makedirs("data/faces", exist_ok=True)
153
+ os.makedirs("data/embeddings", exist_ok=True)
154
+
155
+ def _generate_unique_worker_id(self) -> str:
156
+ """Generate a truly unique worker ID with timestamp and UUID components."""
157
+ timestamp = datetime.now().strftime("%y%m%d")
158
+ unique_suffix = str(uuid.uuid4())[:8].upper()
159
+ worker_id = f"W{self.next_worker_id:04d}-{timestamp}-{unique_suffix}"
160
 
161
+ # Ensure uniqueness by checking against existing IDs
162
+ while worker_id in self.known_face_ids:
163
+ self.next_worker_id += 1
164
+ worker_id = f"W{self.next_worker_id:04d}-{timestamp}-{unique_suffix}"
165
+
166
+ return worker_id
 
 
 
 
 
 
 
 
 
 
 
167
 
168
+ def _load_daily_attendance(self):
169
+ """Load today's attendance records to prevent duplicates."""
170
+ today_str = date.today().isoformat()
171
+ self.daily_attendance.clear()
172
 
173
+ if self.sf:
174
+ try:
175
+ records = self.sf.query_all(f"SELECT Worker_ID__c FROM Attendance__c WHERE Date__c = {today_str}")['records']
176
+ self.daily_attendance.update([record['Worker_ID__c'] for record in records])
177
+ logger.info(f"βœ… Loaded {len(self.daily_attendance)} attendance records for today.")
178
+ except Exception as e:
179
+ logger.error(f"❌ Error loading daily attendance: {e}")
180
+
181
+ def load_worker_data(self):
182
+ logger.info("Loading worker data...")
183
+ if self.sf:
184
  try:
185
+ workers = self.sf.query_all("SELECT Worker_ID__c, Name, Face_Embedding__c FROM Worker__c")['records']
186
+ if not workers:
187
+ self._load_local_worker_data()
188
+ return
189
+
190
+ temp_embeddings, temp_names, temp_ids, max_id = [], [], [], 0
191
+ for worker in workers:
192
+ if worker.get('Face_Embedding__c'):
193
+ # Support multiple embeddings per worker (stored as JSON array)
194
+ embedding_data = json.loads(worker['Face_Embedding__c'])
195
+ if isinstance(embedding_data[0], list):
196
+ # Multiple embeddings
197
+ temp_embeddings.append([np.array(emb) for emb in embedding_data])
198
+ else:
199
+ # Single embedding - wrap in list for consistency
200
+ temp_embeddings.append([np.array(embedding_data)])
201
+
202
+ temp_names.append(worker['Name'])
203
+ temp_ids.append(worker['Worker_ID__c'])
204
+
205
+ # Extract numeric part for next ID
206
+ try:
207
+ worker_num = int(worker['Worker_ID__c'].split('-')[0][1:])
208
+ if worker_num > max_id:
209
+ max_id = worker_num
210
+ except (ValueError, TypeError, IndexError):
211
+ continue
212
 
213
+ self.known_face_embeddings = temp_embeddings
214
+ self.known_face_names = temp_names
215
+ self.known_face_ids = temp_ids
216
+ self.next_worker_id = max_id + 1
217
+ self.save_local_worker_data()
218
+ logger.info(f"βœ… Loaded {len(self.known_face_ids)} workers from Salesforce.")
219
+ except Exception as e:
220
+ logger.error(f"❌ Error loading from Salesforce: {e}. Attempting local load.")
221
+ self._load_local_worker_data()
222
+ else:
223
+ logger.warning("Salesforce not connected. Loading from local cache.")
224
+ self._load_local_worker_data()
225
 
226
+ def _load_local_worker_data(self):
 
227
  try:
228
+ if os.path.exists("data/workers.pkl"):
229
+ with open("data/workers.pkl", "rb") as f:
230
+ data = pickle.load(f)
231
+ self.known_face_embeddings = data.get("embeddings", [])
232
+ self.known_face_names = data.get("names", [])
233
+ self.known_face_ids = data.get("ids", [])
234
+ self.next_worker_id = data.get("next_id", 1)
235
+ self.worker_metadata = data.get("metadata", {})
236
+ logger.info(f"βœ… Loaded {len(self.known_face_ids)} workers from local cache.")
 
 
 
237
  except Exception as e:
238
+ logger.error(f"❌ Error loading local data: {e}")
239
 
240
+ def save_local_worker_data(self):
241
+ try:
242
+ worker_data = {
243
+ "embeddings": self.known_face_embeddings,
244
+ "names": self.known_face_names,
245
+ "ids": self.known_face_ids,
246
+ "next_id": self.next_worker_id,
247
+ "metadata": self.worker_metadata
248
+ }
249
+ with open("data/workers.pkl", "wb") as f:
250
+ pickle.dump(worker_data, f)
251
+ except Exception as e:
252
+ logger.error(f"❌ Error saving local worker data: {e}")
253
+
254
+ # --- Registration and Attendance ---
255
  def register_worker_manual(self, image: Image.Image, name: str) -> Tuple[str, str]:
 
256
  if image is None or not name.strip():
257
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
258
 
 
 
 
259
  try:
 
260
  image_array = np.array(image)
 
 
261
 
262
+ # Extract face with quality assessment
263
+ face_objs = DeepFace.extract_faces(img_path=image_array, detector_backend='opencv', enforce_detection=True)
264
+
265
+ if not face_objs:
266
+ return "❌ No face detected in the image!", self.get_registered_workers_info()
267
+
268
+ # Use the best quality face
269
+ best_face = None
270
+ best_quality_score = 0
271
+
272
+ for face_obj in face_objs:
273
+ facial_area = face_obj['facial_area']
274
+ face_region = image_array[facial_area['y']:facial_area['y']+facial_area['h'],
275
+ facial_area['x']:facial_area['x']+facial_area['w']]
276
+
277
+ is_good_quality, quality_metrics = assess_face_quality(face_region, facial_area)
278
+ quality_score = (quality_metrics['blur_score'] + quality_metrics['confidence_score'] * 1000 +
279
+ quality_metrics['size_score'] + quality_metrics['brightness_score']) / 4
280
+
281
+ if is_good_quality and quality_score > best_quality_score:
282
+ best_quality_score = quality_score
283
+ best_face = (face_region, facial_area)
284
+
285
+ if best_face is None:
286
+ return "❌ Face quality too low! Please provide a clearer image with good lighting.", self.get_registered_workers_info()
287
 
288
+ face_image, facial_area = best_face
289
+ embedding = DeepFace.represent(img_path=face_image, model_name='Facenet')[0]['embedding']
 
 
 
290
 
291
+ # Enhanced duplicate checking
292
+ if self._is_duplicate_face_enhanced(embedding):
293
+ return f"❌ Face matches an existing worker! Please check the registered workers list.", self.get_registered_workers_info()
294
 
295
+ worker_id = self._generate_unique_worker_id()
296
  name = name.strip().title()
297
+ self._add_worker_to_system(worker_id, name, [embedding], face_image, quality_metrics)
298
+ self.save_local_worker_data()
299
+
300
+ return f"βœ… {name} registered successfully with ID: {worker_id}!", self.get_registered_workers_info()
301
 
 
 
 
 
 
 
 
302
  except ValueError as e:
303
+ return f"❌ Face detection failed: {str(e)}", self.get_registered_workers_info()
304
  except Exception as e:
305
+ return f"❌ Registration error: {e}", self.get_registered_workers_info()
 
306
 
307
+ def _register_worker_auto(self, face_image: np.ndarray, facial_area: Dict) -> Optional[Tuple[str, str]]:
308
+ """Auto-register with enhanced quality and duplicate checking."""
 
 
 
309
  try:
310
+ # Quality assessment for auto-registration
311
+ is_good_quality, quality_metrics = assess_face_quality(face_image, facial_area)
 
 
 
 
 
 
 
312
 
313
+ # Higher standards for auto-registration
314
+ if not is_good_quality or quality_metrics['blur_score'] < 150 or quality_metrics['confidence_score'] < 0.99:
315
+ logger.info(f"Face quality insufficient for auto-registration: {quality_metrics}")
316
  return None
317
 
318
+ embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
 
319
 
320
+ # Enhanced duplicate checking with stricter threshold for auto-registration
321
+ if self._is_duplicate_face_enhanced(embedding, threshold=8.0):
322
+ logger.info("Face matches existing worker - skipping auto-registration")
323
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
+ worker_id = self._generate_unique_worker_id()
326
+ worker_name = f"Auto-Worker-{self.next_worker_id}"
 
 
327
 
328
+ self._add_worker_to_system(worker_id, worker_name, [embedding], face_image, quality_metrics)
329
+ self.save_local_worker_data()
330
 
331
+ log_msg = f"πŸ†• [{datetime.now().strftime('%H:%M:%S')}] Auto-registered: {worker_name} (ID: {worker_id})"
332
+ self.session_log.append(log_msg)
333
+ logger.info(log_msg)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
+ return worker_id, worker_name
336
 
337
  except Exception as e:
338
+ logger.error(f"❌ Auto-registration error: {e}")
339
+ return None
340
 
341
+ def _add_worker_to_system(self, worker_id: str, name: str, embeddings: List[np.ndarray],
342
+ image_array: np.ndarray, quality_metrics: Dict = None):
343
+ """Add worker with multiple embeddings support."""
344
+ self.known_face_embeddings.append(embeddings)
345
+ self.known_face_names.append(name)
346
+ self.known_face_ids.append(worker_id)
347
+ self.worker_metadata[worker_id] = {
348
+ 'registration_time': datetime.now().isoformat(),
349
+ 'quality_metrics': quality_metrics or {},
350
+ 'embedding_count': len(embeddings)
351
+ }
352
+ self.next_worker_id += 1
353
+
354
+ # Save face image
355
+ face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
356
+ face_pil.save(f"data/faces/{worker_id}.jpg")
357
+
358
+ # Sync to Salesforce
359
+ if self.sf:
360
+ try:
361
+ # Store first embedding for Salesforce compatibility
362
+ embedding_data = embeddings[0].tolist() if len(embeddings) == 1 else [emb.tolist() for emb in embeddings]
363
+ caption = self._get_image_caption(face_pil)
364
+
365
+ worker_record = self.sf.Worker__c.create({
366
+ 'Name': name,
367
+ 'Worker_ID__c': worker_id,
368
+ 'Face_Embedding__c': json.dumps(embedding_data),
369
+ 'Image_Caption__c': caption,
370
+ 'Registration_Timestamp__c': datetime.now().isoformat()
371
+ })
372
+
373
+ image_url = self._upload_image_to_salesforce(face_pil, worker_record['id'], worker_id)
374
+ if image_url:
375
+ self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
376
+
377
+ logger.info(f"βœ… Worker {worker_id} synced to Salesforce.")
378
+ except Exception as e:
379
+ logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
380
+
381
+ def _is_duplicate_face_enhanced(self, embedding: List[float], threshold: float = 10.0) -> bool:
382
+ """Enhanced duplicate detection with multiple embeddings per worker."""
383
  if not self.known_face_embeddings:
384
  return False
385
 
386
+ embedding_array = np.array(embedding)
387
+
388
+ for worker_embeddings in self.known_face_embeddings:
389
+ # Check against all embeddings for this worker
390
+ for known_embedding in worker_embeddings:
391
+ distance = np.linalg.norm(embedding_array - known_embedding)
392
+ if distance < threshold:
393
+ return True
394
+
395
+ return False
396
+
397
+ def _find_best_match(self, embedding: np.ndarray) -> Tuple[int, float]:
398
+ """Find the best matching worker with minimum distance."""
399
+ if not self.known_face_embeddings:
400
+ return -1, float('inf')
401
+
402
+ best_match_index = -1
403
+ min_distance = float('inf')
404
+
405
+ for worker_idx, worker_embeddings in enumerate(self.known_face_embeddings):
406
+ for known_embedding in worker_embeddings:
407
+ distance = np.linalg.norm(embedding - known_embedding)
408
+ if distance < min_distance:
409
+ min_distance = distance
410
+ best_match_index = worker_idx
411
+
412
+ return best_match_index, min_distance
413
 
414
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
415
+ """Mark attendance with duplicate prevention."""
416
+ # Check if already marked today
417
+ if worker_id in self.daily_attendance:
418
+ logger.info(f"Worker {worker_id} already marked present today")
419
+ return False
420
 
421
+ # Check session cooldown
422
+ current_time = time.time()
423
+ last_seen = self.last_recognition_time.get(worker_id, 0)
424
+ if current_time - last_seen < self.recognition_cooldown:
425
  return False
426
 
427
+ # Check if already marked in this session
428
+ if worker_id in self.session_attendees:
429
+ logger.info(f"Worker {worker_id} already marked in this session")
430
  return False
431
 
432
+ today_str = date.today().isoformat()
433
+ current_datetime = datetime.now()
434
 
435
+ # Mark in Salesforce
436
  if self.sf:
437
  try:
 
438
  self.sf.Attendance__c.create({
439
  'Worker_ID__c': worker_id,
440
  'Name__c': worker_name,
441
  'Date__c': today_str,
442
+ 'Timestamp__c': current_datetime.isoformat(),
443
  'Status__c': "Present"
444
  })
445
  except Exception as e:
446
  logger.error(f"❌ Error saving attendance to Salesforce: {e}")
447
 
448
+ # Update tracking sets
449
+ self.daily_attendance.add(worker_id)
450
+ self.session_attendees.add(worker_id)
451
+ self.last_recognition_time[worker_id] = current_time
 
452
 
453
+ log_msg = f"βœ… [{current_datetime.strftime('%H:%M:%S')}] Marked Present: {worker_name} (ID: {worker_id})"
454
+ self.session_log.append(log_msg)
455
+ logger.info(log_msg)
456
+
457
+ return True
458
 
459
+ # --- Video Processing with Speed Optimizations ---
460
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
461
+ """
462
+ Optimized frame processing with speed improvements and quality checks.
463
+ """
464
+ try:
465
+ # Frame skipping for speed
466
+ self.frame_skip_count += 1
467
+ if self.frame_skip_count % self.frame_skip_rate != 0:
468
+ return frame
 
 
 
 
 
 
 
 
 
 
 
469
 
470
+ # Resize frame for faster processing
471
+ original_height, original_width = frame.shape[:2]
472
+ target_width, target_height = self.processing_resolution
 
 
 
473
 
474
+ if original_width > target_width:
475
+ scale_factor = target_width / original_width
476
+ new_width = target_width
477
+ new_height = int(original_height * scale_factor)
478
+ resized_frame = cv2.resize(frame, (new_width, new_height))
479
+ else:
480
+ resized_frame = frame
481
+ scale_factor = 1.0
482
 
483
+ # Extract faces
484
+ face_objs = DeepFace.extract_faces(img_path=resized_frame, detector_backend='opencv', enforce_detection=False)
 
 
 
 
 
 
 
 
485
 
486
  if face_objs:
487
+ logger.debug(f"Found {len(face_objs)} faces in frame")
488
 
489
  for i, face_obj in enumerate(face_objs):
490
+ facial_area = face_obj['facial_area']
491
+ confidence = facial_area.get('confidence', 0)
492
+
493
+ # Higher confidence threshold
494
+ if confidence < 0.98:
495
+ continue
496
 
497
+ # Scale coordinates back to original frame
498
+ x = int(facial_area['x'] / scale_factor)
499
+ y = int(facial_area['y'] / scale_factor)
500
+ w = int(facial_area['w'] / scale_factor)
501
+ h = int(facial_area['h'] / scale_factor)
502
+
503
+ # Extract face from original frame
504
+ face_image = frame[y:y+h, x:x+w]
505
+
506
+ if face_image.size == 0:
507
+ continue
 
508
 
509
+ # Quality assessment
510
+ is_good_quality, quality_metrics = assess_face_quality(face_image, facial_area)
511
+
512
+ if not is_good_quality:
513
+ # Draw red box for poor quality faces
514
+ cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 0, 255), 2)
515
+ cv2.putText(frame, "Poor Quality", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
516
+ continue
517
+
518
+ # Generate embedding for recognition
519
+ embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
520
+ embedding_array = np.array(embedding)
521
+
522
+ # Find best match
523
+ match_index, min_distance = self._find_best_match(embedding_array)
524
+
525
+ # Recognition threshold
526
+ recognition_threshold = 9.0 # Slightly stricter threshold
527
+
528
+ if match_index != -1 and min_distance < recognition_threshold:
529
+ # Known worker
530
+ worker_id = self.known_face_ids[match_index]
531
+ worker_name = self.known_face_names[match_index]
532
+ color = (0, 255, 0) # Green
533
 
534
+ logger.debug(f"Recognized {worker_name} with distance {min_distance:.4f}")
 
535
 
536
+ # Try to mark attendance (will handle duplicates internally)
537
+ attendance_marked = self.mark_attendance(worker_id, worker_name)
538
+ if attendance_marked:
539
+ label = f"{worker_name} βœ“"
540
+ else:
541
+ label = f"{worker_name} (Already Present)"
542
+
543
+ else:
544
+ # Unknown face - attempt auto-registration with strict quality requirements
545
+ color = (0, 165, 255) # Orange
546
+ label = "Unknown"
547
 
548
+ logger.debug(f"Unknown face with min distance {min_distance:.4f}")
 
 
 
549
 
550
+ # Only auto-register high-quality faces
551
+ if (quality_metrics['blur_score'] > 200 and
552
+ quality_metrics['confidence_score'] > 0.99 and
553
+ quality_metrics['size_score'] > 100):
554
+
555
+ new_worker = self._register_worker_auto(face_image, facial_area)
556
+ if new_worker:
557
+ worker_id, worker_name = new_worker
558
+ self.mark_attendance(worker_id, worker_name)
559
+ label = f"{worker_name} (New)"
560
+ color = (255, 165, 0) # Orange for new registration
561
+
562
+ # Draw bounding box and label
563
+ cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
564
+
565
+ # Add quality indicator
566
+ quality_text = f"Q:{quality_metrics.get('blur_score', 0):.0f}"
567
+ cv2.putText(frame, label, (x, y-30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
568
+ cv2.putText(frame, quality_text, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)
569
 
570
  return frame
571
 
 
573
  logger.error(f"ERROR in process_frame: {e}")
574
  return frame
575
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
  def _processing_loop(self, source):
577
+ """Optimized processing loop with better error handling."""
578
+ video_capture = cv2.VideoCapture(source)
579
+
580
+ if not video_capture.isOpened():
581
+ err_msg = f"❌ **Error:** Could not open video source. Please check the source and try again."
582
+ self.error_message = err_msg
583
+ self.is_processing.clear()
584
+ return
585
+
586
+ # Set camera properties for speed
587
+ if isinstance(source, int): # Camera source
588
+ video_capture.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
589
+ video_capture.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
590
+ video_capture.set(cv2.CAP_PROP_FPS, 30)
591
+ video_capture.set(cv2.CAP_PROP_BUFFERSIZE, 1)
592
+
593
+ frame_count = 0
594
+ start_time = time.time()
595
+
596
+ while self.is_processing.is_set():
597
+ ret, frame = video_capture.read()
598
+ if not ret:
599
+ logger.info("End of video stream or camera disconnected")
600
+ break
601
 
602
+ frame_count += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
 
604
+ # Process frame
605
+ processed_frame = self.process_frame(frame)
 
 
606
 
607
+ # Update frame queue (non-blocking)
608
+ try:
609
+ if not self.frame_queue.full():
610
+ self.frame_queue.put(processed_frame, block=False)
611
+ except queue.Full:
612
+ pass # Skip frame if queue is full
613
+
614
+ self.last_processed_frame = processed_frame
615
+
616
+ # Dynamic frame rate control
617
+ if frame_count % 30 == 0: # Every 30 frames, check performance
618
+ elapsed = time.time() - start_time
619
+ fps = frame_count / elapsed
620
+ if fps < 10: # If too slow, increase frame skipping
621
+ self.frame_skip_rate = min(5, self.frame_skip_rate + 1)
622
+ elif fps > 20: # If fast enough, reduce frame skipping
623
+ self.frame_skip_rate = max(1, self.frame_skip_rate - 1)
624
+
625
+ # Small delay to prevent overwhelming the system
626
+ time.sleep(0.03)
627
+
628
+ # Cleanup
629
+ self.final_log = self.session_log.copy()
630
+ video_capture.release()
631
+ self.is_processing.clear()
632
+ logger.info(f"Processing stopped. Processed {frame_count} frames.")
633
 
634
  def start_processing(self, source) -> str:
635
+ """Start processing with session reset."""
636
  if self.is_processing.is_set():
637
  return "⚠️ Processing is already active."
638
 
639
+ # Reset states for new session
 
 
 
640
  self.session_log.clear()
641
+ self.session_attendees.clear()
642
  self.last_recognition_time.clear()
643
  self.error_message = None
644
  self.last_processed_frame = None
645
  self.final_log = None
646
+ self.frame_skip_count = 0
647
+
648
+ # Reload daily attendance to get latest data
649
+ self._load_daily_attendance()
650
 
651
  self.is_processing.set()
652
  self.processing_thread = threading.Thread(target=self._processing_loop, args=(source,))
653
  self.processing_thread.daemon = True
654
  self.processing_thread.start()
655
 
656
+ return f"βœ… Started processing with optimized speed settings..."
657
 
658
  def stop_processing(self) -> str:
659
+ """Stop processing and clean up."""
 
 
 
660
  self.is_processing.clear()
661
+ self.error_message = None
662
 
663
+ # Generate session summary
664
+ if self.session_attendees:
665
+ summary = f"πŸ“Š Session Summary: {len(self.session_attendees)} unique attendees marked present"
666
+ self.session_log.append(summary)
 
 
 
 
667
 
668
+ return "βœ… Processing stopped. Session summary generated."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
 
670
  # --- Helper & Reporting ---
671
  def _get_image_caption(self, image: Image.Image) -> str:
 
672
  if not HF_API_TOKEN:
673
  return "Hugging Face API token not configured."
 
674
  try:
675
  buffered = BytesIO()
676
  image.save(buffered, format="JPEG")
677
  img_data = buffered.getvalue()
678
  headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
679
+ response = requests.post(HF_API_URL, headers=headers, data=img_data, timeout=10)
 
680
  response.raise_for_status()
681
  result = response.json()
 
682
  return result[0].get("generated_text", "No caption found.")
 
 
683
  except Exception as e:
684
  logger.error(f"Hugging Face API error: {e}")
685
  return "Caption generation failed."
686
 
687
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
 
688
  if not self.sf:
689
  return None
 
690
  try:
691
  buffered = BytesIO()
692
  image.save(buffered, format="JPEG")
693
  encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
 
694
  cv = self.sf.ContentVersion.create({
695
  'Title': f'Image_{worker_id}',
696
  'PathOnClient': f'{worker_id}.jpg',
697
  'VersionData': encoded_image,
698
  'FirstPublishLocationId': record_id
699
  })
700
+ return f"/{cv['id']}"
 
701
  except Exception as e:
702
  logger.error(f"Salesforce image upload error: {e}")
703
  return None
704
 
705
  def get_registered_workers_info(self) -> str:
706
+ """Get formatted info about registered workers."""
707
+ if not self.known_face_ids:
708
+ return "No workers registered yet."
709
+
710
  try:
711
+ info_lines = [f"**πŸ‘₯ Registered Workers ({len(self.known_face_ids)})**\n"]
 
 
712
 
713
+ for i, (worker_id, name) in enumerate(zip(self.known_face_ids, self.known_face_names)):
714
+ status = "βœ… Present Today" if worker_id in self.daily_attendance else "⏳ Not Marked"
715
+ embedding_count = len(self.known_face_embeddings[i])
716
+ info_lines.append(f"- **{name}** (ID: `{worker_id}`) - {status} - {embedding_count} face(s)")
717
 
718
+ return "\n".join(info_lines)
719
+
720
+ except Exception as e:
721
+ return f"Error loading worker info: {e}"
722
 
723
+ def get_attendance_stats(self) -> str:
724
+ """Get attendance statistics for today."""
725
+ today_str = date.today().isoformat()
726
+ total_workers = len(self.known_face_ids)
727
+ present_today = len(self.daily_attendance)
728
+ attendance_rate = (present_today / max(total_workers, 1)) * 100
729
 
730
+ return f"""
731
+ **πŸ“Š Today's Attendance Statistics**
732
+ - **Date**: {today_str}
733
+ - **Total Workers**: {total_workers}
734
+ - **Present Today**: {present_today}
735
+ - **Attendance Rate**: {attendance_rate:.1f}%
736
+ - **Session Attendees**: {len(self.session_attendees)}
737
+ """
 
738
 
 
 
739
  # --- GRADIO UI ---
740
+ attendance_system = AttendanceSystem()
741
 
742
  def create_interface():
743
+ with gr.Blocks(theme=gr.themes.Soft(), title="Advanced Attendance System") as demo:
744
+ gr.Markdown("# 🎯 Advanced Face Recognition Attendance System v2.0")
 
 
 
 
745
 
746
+ with gr.Tabs():
747
+ with gr.Tab("βš™οΈ Controls & Status"):
748
+ gr.Markdown("### 1. Choose Input Source & Start Processing")
749
+ with gr.Row():
750
+ with gr.Column(scale=1):
751
+ selected_tab_index = gr.Number(value=0, visible=False)
752
+ with gr.Tabs() as video_tabs:
753
+ with gr.Tab("Live Camera", id=0):
754
+ camera_source = gr.Number(label="Camera Source", value=0, precision=0)
755
+ with gr.Tab("Upload Video", id=1):
756
+ video_file = gr.Video(label="Upload Video File", sources=["upload"])
757
+ with gr.Column(scale=1):
758
+ start_btn = gr.Button("▢️ Start Processing", variant="primary", size="lg")
759
+ stop_btn = gr.Button("⏹️ Stop Processing", variant="stop", size="lg")
760
+ status_box = gr.Textbox(label="Status", interactive=False, value="System Ready - Enhanced with Quality Control")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
761
 
762
+ gr.Markdown("### 2. Enhanced Features")
763
+ gr.Markdown("""
764
+ **πŸš€ New Improvements:**
765
+ - βœ… **Duplicate Prevention**: No duplicate attendance logs
766
+ - βœ… **Face Quality Control**: Only clear, high-quality faces are processed
767
+ - βœ… **Unique IDs**: Enhanced ID generation with timestamps
768
+ - βœ… **Speed Optimization**: Faster video processing with adaptive frame rates
769
+ - βœ… **Better Recognition**: Improved face matching to prevent false registrations
770
+
771
+ **🎨 Color Coding:**
772
+ - <span style='color: green'>**Green**</span> = Recognized Worker
773
+ - <span style='color: orange'>**Orange**</span> = New Registration
774
+ - <span style='color: red'>**Red**</span> = Poor Quality/Unknown
775
+ """)
776
+
777
+ with gr.Tab("πŸ“Š Output & Live Feed"):
778
+ with gr.Row():
779
+ with gr.Column(scale=2):
780
+ video_output = gr.Image(label="πŸŽ₯ Live Recognition Feed", interactive=False)
781
+ with gr.Column(scale=1):
782
+ session_log_display = gr.Markdown(label="πŸ“‹ Live Session Log", value="System ready for processing...")
783
+
784
+ with gr.Tab("πŸ‘€ Worker Management"):
785
+ with gr.Row():
786
+ with gr.Column():
787
+ gr.Markdown("### Register New Worker")
788
+ register_image = gr.Image(label="πŸ“Έ Upload Clear Photo (Good Lighting Required)", type="pil")
789
+ register_name = gr.Textbox(label="πŸ‘€ Worker's Full Name", placeholder="Enter full name...")
790
+ register_btn = gr.Button("βœ… Register Worker", variant="primary", size="lg")
791
+ register_output = gr.Textbox(label="Registration Status", interactive=False)
792
 
793
+ with gr.Column():
794
+ gr.Markdown("### Registered Workers")
795
+ registered_workers_info = gr.Markdown(value=attendance_system.get_registered_workers_info())
796
+ refresh_workers_btn = gr.Button("πŸ”„ Refresh Workers List", variant="secondary")
797
+
798
+ gr.Markdown("### Today's Statistics")
799
+ attendance_stats = gr.Markdown(value=attendance_system.get_attendance_stats())
800
+ refresh_stats_btn = gr.Button("πŸ“Š Refresh Statistics", variant="secondary")
801
 
802
+ # --- Event Handlers ---
803
+ def on_tab_select(evt: gr.SelectData):
804
+ return evt.index
805
+
806
+ video_tabs.select(fn=on_tab_select, inputs=None, outputs=[selected_tab_index])
807
+
808
+ def start_wrapper(tab_index, cam_src, vid_path):
809
+ source = cam_src if tab_index == 0 else vid_path
810
+ if source is None:
811
+ return "❌ Please provide an input source."
812
+ return attendance_system.start_processing(source)
813
+
814
+ start_btn.click(fn=start_wrapper, inputs=[selected_tab_index, camera_source, video_file], outputs=[status_box])
815
+ stop_btn.click(fn=attendance_system.stop_processing, inputs=None, outputs=[status_box])
816
+
817
+ register_btn.click(
818
+ fn=attendance_system.register_worker_manual,
819
+ inputs=[register_image, register_name],
820
+ outputs=[register_output, registered_workers_info]
821
+ )
822
+
823
+ refresh_workers_btn.click(
824
+ fn=attendance_system.get_registered_workers_info,
825
+ outputs=[registered_workers_info]
826
+ )
827
+
828
+ refresh_stats_btn.click(
829
+ fn=attendance_system.get_attendance_stats,
830
+ outputs=[attendance_stats]
831
+ )
832
+
833
+ def update_ui_generator():
834
+ """Enhanced UI update generator with better error handling."""
835
+ while True:
 
 
 
 
 
 
 
 
836
  try:
837
+ # Handle errors
838
+ if attendance_system.error_message:
839
+ yield None, f"❌ **Error**: {attendance_system.error_message}"
840
+ time.sleep(3)
841
+ attendance_system.error_message = None
842
+ continue
843
+
844
+ # Active processing
845
+ if attendance_system.is_processing.is_set():
846
+ frame = None
847
+ log_content = "πŸ”„ Processing..."
848
+
849
+ # Get latest frame
850
+ try:
851
+ if not attendance_system.frame_queue.empty():
852
+ frame = attendance_system.frame_queue.get_nowait()
853
+ if frame is not None:
854
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
855
+ except queue.Empty:
856
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
857
 
858
+ # Format session log
859
+ if attendance_system.session_log:
860
+ recent_logs = attendance_system.session_log[-10:] # Show last 10 entries
861
+ log_content = "### πŸ“‹ Live Session Activity\n" + "\n".join(reversed(recent_logs))
862
+
863
+ yield frame, log_content
864
+
865
+ # Processing stopped - show final results
866
+ else:
867
+ if attendance_system.last_processed_frame is not None:
868
+ final_frame = cv2.cvtColor(attendance_system.last_processed_frame, cv2.COLOR_BGR2RGB)
869
+ final_log_content = "### πŸ“‹ Session Complete\n"
870
+
871
+ if attendance_system.final_log:
872
+ final_log_content += "\n".join(reversed(attendance_system.final_log))
873
  else:
874
+ final_log_content += "No activities recorded in this session."
875
+
876
+ # Add session summary
877
+ if attendance_system.session_attendees:
878
+ final_log_content += f"\n\n**πŸ“Š Session Summary:** {len(attendance_system.session_attendees)} unique attendees processed"
879
 
880
+ yield final_frame, final_log_content
 
 
 
 
 
 
 
881
  else:
882
+ yield None, "### 🏠 System Ready\nGo to 'Controls & Status' tab to start processing."
883
+
884
+ time.sleep(0.1) # Reduced update frequency for better performance
885
+
886
+ except Exception as e:
887
+ logger.error(f"UI update error: {e}")
888
+ yield None, f"❌ UI Error: {str(e)}"
889
+ time.sleep(1)
 
 
 
 
 
 
 
890
 
891
+ # Auto-refresh UI
892
+ demo.load(fn=update_ui_generator, outputs=[video_output, session_log_display])
 
893
 
894
+ return demo
 
 
 
 
 
895
 
896
  if __name__ == "__main__":
897
+ print("πŸš€ Starting Advanced Face Recognition Attendance System v2.0...")
898
+ print("πŸ“Š Enhanced with: Quality Control, Duplicate Prevention, Speed Optimization")
899
+
900
+ app = create_interface()
901
+ app.queue(max_size=20) # Increased queue size for better performance
902
+ app.launch(
903
+ server_name="0.0.0.0",
904
+ server_port=7860,
905
+ show_error=True,
906
+ debug=False, # Disabled debug for better performance
907
+ share=False
908
+ )