PrashanthB461 commited on
Commit
64ead65
·
verified ·
1 Parent(s): 7aaa703

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +218 -576
app.py CHANGED
@@ -9,8 +9,6 @@ import logging
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
@@ -63,49 +61,6 @@ def connect_to_salesforce() -> Optional[Salesforce]:
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 60x60 pixels - reduced for better detection)
76
- min_face_size = 50
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 = 30 # Further reduced threshold for better detection
83
- blur_check = blur_score > blur_threshold
84
-
85
- # 3. Brightness check
86
- brightness = np.mean(gray)
87
- brightness_check = 20 < brightness < 230 # Even more lenient brightness range
88
-
89
- # 4. Face detection confidence (from facial_area if available)
90
- confidence_check = facial_area.get('confidence', 0) > 0.80 # Further reduced threshold
91
-
92
- quality_metrics = {
93
- 'size_score': min(w, h),
94
- 'blur_score': blur_score,
95
- 'brightness_score': brightness,
96
- 'confidence_score': facial_area.get('confidence', 0),
97
- 'size_check': size_check,
98
- 'blur_check': blur_check,
99
- 'brightness_check': brightness_check,
100
- 'confidence_check': confidence_check
101
- }
102
-
103
- # At least 2 out of 4 checks must pass for good quality (more lenient)
104
- passed_checks = sum([size_check, blur_check, brightness_check, confidence_check])
105
- is_good_quality = passed_checks >= 2
106
-
107
- return is_good_quality, quality_metrics
108
-
109
  # --- CORE LOGIC ---
110
 
111
  class AttendanceSystem:
@@ -118,63 +73,33 @@ class AttendanceSystem:
118
  self.is_processing = threading.Event()
119
  self.frame_queue = queue.Queue(maxsize=10)
120
  self.error_message = None
121
- self.last_processed_frame = None
122
- self.final_log = None
123
 
124
- # Data Storage with improved structure
125
- self.known_face_embeddings: List[List[np.ndarray]] = [] # Multiple embeddings per person
126
  self.known_face_names: List[str] = []
127
  self.known_face_ids: List[str] = []
128
- self.worker_metadata: Dict[str, Dict] = {} # Store additional info per worker
129
  self.next_worker_id: int = 1
130
 
131
- # Session Tracking with improvements
132
- self.last_recognition_time = {}
133
- self.recognition_cooldown = 3 # Reduced cooldown
134
  self.session_log: List[str] = []
135
- self.daily_attendance: set = set() # Track who attended today
136
- self.session_attendees: set = set() # Track who attended this session
137
-
138
- # Processing optimization
139
- self.frame_skip_count = 0
140
- self.frame_skip_rate = 1 # Process every frame initially
141
- self.processing_resolution = (640, 480)
142
 
143
  # Initialize
144
  self.sf = connect_to_salesforce()
145
  self._create_directories()
146
  self.load_worker_data()
147
- self._load_daily_attendance()
148
 
149
  def _create_directories(self):
150
  os.makedirs("data/faces", exist_ok=True)
151
- os.makedirs("data/embeddings", exist_ok=True)
152
-
153
- def _generate_unique_worker_id(self) -> str:
154
- """Generate a truly unique worker ID with timestamp and UUID components."""
155
- timestamp = datetime.now().strftime("%y%m%d")
156
- unique_suffix = str(uuid.uuid4())[:8].upper()
157
- worker_id = f"W{self.next_worker_id:04d}-{timestamp}-{unique_suffix}"
158
-
159
- # Ensure uniqueness by checking against existing IDs
160
- while worker_id in self.known_face_ids:
161
- self.next_worker_id += 1
162
- worker_id = f"W{self.next_worker_id:04d}-{timestamp}-{unique_suffix}"
163
-
164
- return worker_id
165
-
166
- def _load_daily_attendance(self):
167
- """Load today's attendance records to prevent duplicates."""
168
- today_str = date.today().isoformat()
169
- self.daily_attendance.clear()
170
-
171
- if self.sf:
172
- try:
173
- records = self.sf.query_all(f"SELECT Worker_ID__c FROM Attendance__c WHERE Date__c = {today_str}")['records']
174
- self.daily_attendance.update([record['Worker_ID__c'] for record in records])
175
- logger.info(f"✅ Loaded {len(self.daily_attendance)} attendance records for today.")
176
- except Exception as e:
177
- logger.error(f"❌ Error loading daily attendance: {e}")
178
 
179
  def load_worker_data(self):
180
  logger.info("Loading worker data...")
@@ -188,24 +113,14 @@ class AttendanceSystem:
188
  temp_embeddings, temp_names, temp_ids, max_id = [], [], [], 0
189
  for worker in workers:
190
  if worker.get('Face_Embedding__c'):
191
- # Support multiple embeddings per worker (stored as JSON array)
192
- embedding_data = json.loads(worker['Face_Embedding__c'])
193
- if isinstance(embedding_data[0], list):
194
- # Multiple embeddings
195
- temp_embeddings.append([np.array(emb) for emb in embedding_data])
196
- else:
197
- # Single embedding - wrap in list for consistency
198
- temp_embeddings.append([np.array(embedding_data)])
199
-
200
  temp_names.append(worker['Name'])
201
  temp_ids.append(worker['Worker_ID__c'])
202
-
203
- # Extract numeric part for next ID
204
  try:
205
- worker_num = int(worker['Worker_ID__c'].split('-')[0][1:])
206
  if worker_num > max_id:
207
  max_id = worker_num
208
- except (ValueError, TypeError, IndexError):
209
  continue
210
 
211
  self.known_face_embeddings = temp_embeddings
@@ -224,28 +139,19 @@ class AttendanceSystem:
224
  def _load_local_worker_data(self):
225
  try:
226
  if os.path.exists("data/workers.pkl"):
227
- with open("data/workers.pkl", "rb") as f:
228
- data = pickle.load(f)
229
  self.known_face_embeddings = data.get("embeddings", [])
230
  self.known_face_names = data.get("names", [])
231
  self.known_face_ids = data.get("ids", [])
232
  self.next_worker_id = data.get("next_id", 1)
233
- self.worker_metadata = data.get("metadata", {})
234
  logger.info(f"✅ Loaded {len(self.known_face_ids)} workers from local cache.")
235
  except Exception as e:
236
  logger.error(f"❌ Error loading local data: {e}")
237
 
238
  def save_local_worker_data(self):
239
  try:
240
- worker_data = {
241
- "embeddings": self.known_face_embeddings,
242
- "names": self.known_face_names,
243
- "ids": self.known_face_ids,
244
- "next_id": self.next_worker_id,
245
- "metadata": self.worker_metadata
246
- }
247
- with open("data/workers.pkl", "wb") as f:
248
- pickle.dump(worker_data, f)
249
  except Exception as e:
250
  logger.error(f"❌ Error saving local worker data: {e}")
251
 
@@ -253,433 +159,253 @@ class AttendanceSystem:
253
  def register_worker_manual(self, image: Image.Image, name: str) -> Tuple[str, str]:
254
  if image is None or not name.strip():
255
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
256
-
257
  try:
258
  image_array = np.array(image)
 
 
 
 
259
 
260
- # Extract face with quality assessment
261
- face_objs = DeepFace.extract_faces(img_path=image_array, detector_backend='opencv', enforce_detection=True)
262
-
263
- if not face_objs:
264
- return "❌ No face detected in the image!", self.get_registered_workers_info()
265
-
266
- # Use the best quality face
267
- best_face = None
268
- best_quality_score = 0
269
-
270
- for face_obj in face_objs:
271
- facial_area = face_obj['facial_area']
272
- face_region = image_array[facial_area['y']:facial_area['y']+facial_area['h'],
273
- facial_area['x']:facial_area['x']+facial_area['w']]
274
-
275
- is_good_quality, quality_metrics = assess_face_quality(face_region, facial_area)
276
- quality_score = (quality_metrics['blur_score'] + quality_metrics['confidence_score'] * 1000 +
277
- quality_metrics['size_score'] + quality_metrics['brightness_score']) / 4
278
-
279
- if is_good_quality and quality_score > best_quality_score:
280
- best_quality_score = quality_score
281
- best_face = (face_region, facial_area)
282
-
283
- if best_face is None:
284
- return "❌ Face quality too low! Please provide a clearer image with good lighting.", self.get_registered_workers_info()
285
-
286
- face_image, facial_area = best_face
287
- embedding = DeepFace.represent(img_path=face_image, model_name='Facenet')[0]['embedding']
288
-
289
- # Enhanced duplicate checking
290
- if self._is_duplicate_face_enhanced(embedding):
291
- return f"❌ Face matches an existing worker! Please check the registered workers list.", self.get_registered_workers_info()
292
 
293
- worker_id = self._generate_unique_worker_id()
294
  name = name.strip().title()
295
- self._add_worker_to_system(worker_id, name, [embedding], face_image, quality_metrics)
296
  self.save_local_worker_data()
297
-
298
- return f"✅ {name} registered successfully with ID: {worker_id}!", self.get_registered_workers_info()
299
-
300
- except ValueError as e:
301
- return f"❌ Face detection failed: {str(e)}", self.get_registered_workers_info()
302
  except Exception as e:
303
  return f"❌ Registration error: {e}", self.get_registered_workers_info()
304
 
305
- def _register_worker_auto(self, face_image: np.ndarray, facial_area: Dict) -> Optional[Tuple[str, str]]:
306
- """Auto-register with enhanced quality and duplicate checking."""
307
  try:
308
- logger.info("Attempting auto-registration...")
309
-
310
- # Quality assessment for auto-registration
311
- is_good_quality, quality_metrics = assess_face_quality(face_image, facial_area)
312
-
313
- logger.info(f"Quality check - Good: {is_good_quality}, Metrics: {quality_metrics}")
314
-
315
- # More lenient standards for auto-registration
316
- if not is_good_quality:
317
- logger.info(f"Face quality insufficient for auto-registration: {quality_metrics}")
318
  return None
319
-
320
- logger.info("Generating embedding for auto-registration...")
321
  embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
322
-
323
- # Check for duplicates with more lenient threshold for auto-registration
324
- if self._is_duplicate_face_enhanced(embedding, threshold=15.0):
325
- logger.info("Face matches existing worker - skipping auto-registration")
326
  return None
327
-
328
- logger.info("Creating new worker record...")
329
- worker_id = self._generate_unique_worker_id()
330
- worker_name = f"Auto-Worker-{self.next_worker_id}"
331
-
332
- self._add_worker_to_system(worker_id, worker_name, [embedding], face_image, quality_metrics)
333
  self.save_local_worker_data()
334
-
335
- log_msg = f"🆕 [{datetime.now().strftime('%H:%M:%S')}] Auto-registered: {worker_name} (ID: {worker_id})"
336
  self.session_log.append(log_msg)
337
  logger.info(log_msg)
338
-
339
  return worker_id, worker_name
340
-
341
  except Exception as e:
342
  logger.error(f"❌ Auto-registration error: {e}")
343
  return None
344
 
345
- def _add_worker_to_system(self, worker_id: str, name: str, embeddings: List[np.ndarray],
346
- image_array: np.ndarray, quality_metrics: Dict = None):
347
- """Add worker with multiple embeddings support."""
348
- self.known_face_embeddings.append(embeddings)
349
  self.known_face_names.append(name)
350
  self.known_face_ids.append(worker_id)
351
- self.worker_metadata[worker_id] = {
352
- 'registration_time': datetime.now().isoformat(),
353
- 'quality_metrics': quality_metrics or {},
354
- 'embedding_count': len(embeddings)
355
- }
356
  self.next_worker_id += 1
357
-
358
- # Save face image
359
  face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
360
  face_pil.save(f"data/faces/{worker_id}.jpg")
361
-
362
- # Sync to Salesforce
363
  if self.sf:
364
  try:
365
- # Store first embedding for Salesforce compatibility
366
- embedding_data = embeddings[0].tolist() if len(embeddings) == 1 else [emb.tolist() for emb in embeddings]
367
- caption = self._get_image_caption(face_pil)
368
-
369
- worker_record = self.sf.Worker__c.create({
370
- 'Name': name,
371
- 'Worker_ID__c': worker_id,
372
- 'Face_Embedding__c': json.dumps(embedding_data),
373
- 'Image_Caption__c': caption,
374
- 'Registration_Timestamp__c': datetime.now().isoformat()
375
- })
376
-
377
  image_url = self._upload_image_to_salesforce(face_pil, worker_record['id'], worker_id)
378
- if image_url:
379
- self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
380
-
381
  logger.info(f"✅ Worker {worker_id} synced to Salesforce.")
382
  except Exception as e:
383
  logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
384
 
385
- def _is_duplicate_face_enhanced(self, embedding: List[float], threshold: float = 10.0) -> bool:
386
- """Enhanced duplicate detection with multiple embeddings per worker."""
387
- if not self.known_face_embeddings:
388
- return False
389
-
390
- embedding_array = np.array(embedding)
391
-
392
- for worker_embeddings in self.known_face_embeddings:
393
- # Check against all embeddings for this worker
394
- for known_embedding in worker_embeddings:
395
- distance = np.linalg.norm(embedding_array - known_embedding)
396
- if distance < threshold:
397
- return True
398
-
399
- return False
400
-
401
- def _find_best_match(self, embedding: np.ndarray) -> Tuple[int, float]:
402
- """Find the best matching worker with minimum distance."""
403
- if not self.known_face_embeddings:
404
- return -1, float('inf')
405
-
406
- best_match_index = -1
407
- min_distance = float('inf')
408
-
409
- for worker_idx, worker_embeddings in enumerate(self.known_face_embeddings):
410
- for known_embedding in worker_embeddings:
411
- distance = np.linalg.norm(embedding - known_embedding)
412
- if distance < min_distance:
413
- min_distance = distance
414
- best_match_index = worker_idx
415
-
416
- return best_match_index, min_distance
417
 
418
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
419
- """Mark attendance with duplicate prevention."""
 
420
  # Check if already marked today
421
- if worker_id in self.daily_attendance:
422
- logger.debug(f"Worker {worker_id} already marked present today")
423
  return False
424
-
425
- # Check session cooldown
426
  current_time = time.time()
427
- last_seen = self.last_recognition_time.get(worker_id, 0)
428
- if current_time - last_seen < self.recognition_cooldown:
429
- logger.debug(f"Worker {worker_id} in cooldown period")
430
- return False
431
-
432
- # Check if already marked in this session
433
- if worker_id in self.session_attendees:
434
- logger.debug(f"Worker {worker_id} already marked in this session")
435
  return False
436
-
437
- today_str = date.today().isoformat()
438
- current_datetime = datetime.now()
439
-
440
- # Mark in Salesforce
441
  if self.sf:
442
  try:
443
  self.sf.Attendance__c.create({
444
  'Worker_ID__c': worker_id,
445
  'Name__c': worker_name,
446
  'Date__c': today_str,
447
- 'Timestamp__c': current_datetime.isoformat(),
448
  'Status__c': "Present"
449
  })
450
  except Exception as e:
451
  logger.error(f"❌ Error saving attendance to Salesforce: {e}")
452
 
453
- # Update tracking sets
454
- self.daily_attendance.add(worker_id)
455
- self.session_attendees.add(worker_id)
456
  self.last_recognition_time[worker_id] = current_time
457
-
458
- log_msg = f"✅ [{current_datetime.strftime('%H:%M:%S')}] Marked Present: {worker_name} (ID: {worker_id})"
459
  self.session_log.append(log_msg)
460
- logger.info(log_msg)
461
-
462
  return True
463
 
464
- # --- Video Processing with Speed Optimizations ---
465
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
466
  """
467
- Optimized frame processing with better recognition and auto-registration.
468
  """
469
  try:
470
- # Frame skipping for speed
471
- self.frame_skip_count += 1
472
- if self.frame_skip_count % self.frame_skip_rate != 0:
473
- return frame
474
-
475
- # Resize frame for faster processing
476
- original_height, original_width = frame.shape[:2]
477
- target_width, target_height = self.processing_resolution
478
-
479
- if original_width > target_width:
480
- scale_factor = target_width / original_width
481
- new_width = target_width
482
- new_height = int(original_height * scale_factor)
483
- resized_frame = cv2.resize(frame, (new_width, new_height))
484
- else:
485
- resized_frame = frame
486
- scale_factor = 1.0
487
-
488
- # Extract faces
489
- try:
490
- face_objs = DeepFace.extract_faces(img_path=resized_frame, detector_backend='opencv', enforce_detection=False)
491
- except Exception as e:
492
- logger.error(f"Face extraction failed: {e}")
493
  return frame
494
 
495
- if face_objs and len(face_objs) > 0:
496
- logger.info(f"Found {len(face_objs)} faces in frame")
497
- else:
498
- logger.debug("No faces detected in frame")
499
- return frame
500
 
501
- for i, face_obj in enumerate(face_objs):
502
- facial_area = face_obj['facial_area']
503
- confidence = facial_area.get('confidence', 0)
504
-
505
- # Lower confidence threshold for better detection
506
- if confidence < 0.85:
 
 
 
 
507
  continue
508
 
509
- # Scale coordinates back to original frame
510
- x = int(facial_area['x'] / scale_factor)
511
- y = int(facial_area['y'] / scale_factor)
512
- w = int(facial_area['w'] / scale_factor)
513
- h = int(facial_area['h'] / scale_factor)
514
 
515
- # Extract face from original frame
516
- face_image = frame[y:y+h, x:x+w]
 
 
 
517
 
 
518
  if face_image.size == 0:
519
  continue
520
 
521
- # Quality assessment
522
- is_good_quality, quality_metrics = assess_face_quality(face_image, facial_area)
 
 
 
 
 
 
 
 
 
 
 
523
 
524
- # Show all faces but process only good quality ones
525
- if not is_good_quality:
526
- # Draw yellow box for poor quality faces
527
- cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 255), 2)
528
- cv2.putText(frame, "Poor Quality - Skipped", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2)
529
- logger.debug(f"Skipping poor quality face: {quality_metrics}")
530
  continue
531
 
532
- try:
533
- # Generate embedding for recognition
534
- embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
535
- embedding_array = np.array(embedding)
536
-
537
- # Find best match
538
- match_index, min_distance = self._find_best_match(embedding_array)
539
-
540
- # Recognition threshold - more lenient for existing workers
541
- recognition_threshold = 15.0
542
-
543
- if match_index != -1 and min_distance < recognition_threshold:
544
- # Known worker
545
- worker_id = self.known_face_ids[match_index]
546
- worker_name = self.known_face_names[match_index]
547
- color = (0, 255, 0) # Green
548
-
549
- logger.info(f"Recognized {worker_name} with distance {min_distance:.4f}")
550
-
551
- # Try to mark attendance (will handle duplicates internally)
552
- attendance_marked = self.mark_attendance(worker_id, worker_name)
553
- if attendance_marked:
554
- label = f"{worker_name} - PRESENT"
555
- else:
556
- label = f"{worker_name} - Already Present"
557
-
558
- else:
559
- # Unknown face - attempt auto-registration
560
- color = (0, 165, 255) # Orange
561
- label = "Unknown - Processing..."
562
-
563
- logger.debug(f"Unknown face with min distance {min_distance:.4f}")
564
-
565
- # Auto-register if quality is sufficient
566
- new_worker = self._register_worker_auto(face_image, facial_area)
567
  if new_worker:
568
- worker_id, worker_name = new_worker
569
  self.mark_attendance(worker_id, worker_name)
570
- label = f"{worker_name} - NEWLY REGISTERED"
571
- color = (255, 0, 0) # Blue for new registration
572
- else:
573
- label = "Unknown - Low Quality"
574
-
575
- # For debugging - show why auto-registration failed
576
- if not new_worker:
577
- logger.info(f"Auto-registration failed - Quality: {is_good_quality}, Distance: {min_distance:.2f}")
578
-
579
- # Draw bounding box and label
580
- cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
581
-
582
- # Add quality and distance info
583
- quality_text = f"Q:{quality_metrics.get('blur_score', 0):.0f} D:{min_distance:.1f}"
584
- cv2.putText(frame, label, (x, y-30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
585
- cv2.putText(frame, quality_text, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)
586
-
587
- except Exception as embed_error:
588
- logger.error(f"Error generating embedding: {embed_error}")
589
- # Draw red box for embedding errors
590
- cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 0, 255), 2)
591
- cv2.putText(frame, "Processing Error", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
592
 
593
  return frame
594
-
595
  except Exception as e:
596
  logger.error(f"ERROR in process_frame: {e}")
597
  return frame
598
 
599
  def _processing_loop(self, source):
600
- """Optimized processing loop with better error handling."""
601
  video_capture = cv2.VideoCapture(source)
602
-
603
  if not video_capture.isOpened():
604
- err_msg = f"❌ **Error:** Could not open video source. Please check the source and try again."
605
  self.error_message = err_msg
606
  self.is_processing.clear()
607
  return
608
-
609
- # Set camera properties for speed
610
- if isinstance(source, int): # Camera source
611
- video_capture.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
612
- video_capture.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
613
  video_capture.set(cv2.CAP_PROP_FPS, 30)
614
- video_capture.set(cv2.CAP_PROP_BUFFERSIZE, 1)
615
-
616
- frame_count = 0
617
- start_time = time.time()
618
-
619
  while self.is_processing.is_set():
620
  ret, frame = video_capture.read()
621
  if not ret:
622
- logger.info("End of video stream or camera disconnected")
623
  break
624
-
625
- frame_count += 1
626
-
627
- # Process frame
628
  processed_frame = self.process_frame(frame)
629
 
630
- # Update frame queue (non-blocking)
631
- try:
632
- if not self.frame_queue.full():
633
- self.frame_queue.put(processed_frame, block=False)
634
- except queue.Full:
635
- pass # Skip frame if queue is full
636
-
637
  self.last_processed_frame = processed_frame
 
638
 
639
- # Small delay to prevent overwhelming the system
640
- time.sleep(0.03)
641
-
642
- # Cleanup
643
  self.final_log = self.session_log.copy()
644
  video_capture.release()
645
  self.is_processing.clear()
646
- logger.info(f"Processing stopped. Processed {frame_count} frames.")
647
 
648
  def start_processing(self, source) -> str:
649
- """Start processing with session reset."""
650
  if self.is_processing.is_set():
651
  return "⚠️ Processing is already active."
652
-
653
  # Reset states for new session
654
  self.session_log.clear()
655
- self.session_attendees.clear()
656
  self.last_recognition_time.clear()
 
 
657
  self.error_message = None
658
  self.last_processed_frame = None
659
  self.final_log = None
660
- self.frame_skip_count = 0
661
-
662
- # Reload daily attendance to get latest data
663
- self._load_daily_attendance()
664
 
665
  self.is_processing.set()
666
- self.processing_thread = threading.Thread(target=self._processing_loop, args=(source,))
667
- self.processing_thread.daemon = True
 
 
 
668
  self.processing_thread.start()
669
-
670
- return f"✅ Started processing with enhanced recognition..."
671
 
672
  def stop_processing(self) -> str:
673
- """Stop processing and clean up."""
674
  self.is_processing.clear()
675
  self.error_message = None
676
-
677
- # Generate session summary
678
- if self.session_attendees:
679
- summary = f"📊 Session Summary: {len(self.session_attendees)} unique attendees marked present"
680
- self.session_log.append(summary)
681
-
682
- return "✅ Processing stopped. Session summary generated."
683
 
684
  # --- Helper & Reporting ---
685
  def _get_image_caption(self, image: Image.Image) -> str:
@@ -690,7 +416,7 @@ class AttendanceSystem:
690
  image.save(buffered, format="JPEG")
691
  img_data = buffered.getvalue()
692
  headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
693
- response = requests.post(HF_API_URL, headers=headers, data=img_data, timeout=10)
694
  response.raise_for_status()
695
  result = response.json()
696
  return result[0].get("generated_text", "No caption found.")
@@ -706,57 +432,35 @@ class AttendanceSystem:
706
  image.save(buffered, format="JPEG")
707
  encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
708
  cv = self.sf.ContentVersion.create({
709
- 'Title': f'Image_{worker_id}',
710
- 'PathOnClient': f'{worker_id}.jpg',
711
- 'VersionData': encoded_image,
712
  'FirstPublishLocationId': record_id
713
  })
714
- return f"/{cv['id']}"
715
  except Exception as e:
716
  logger.error(f"Salesforce image upload error: {e}")
717
  return None
718
 
719
  def get_registered_workers_info(self) -> str:
720
- """Get formatted info about registered workers."""
721
- if not self.known_face_ids:
722
- return "No workers registered yet."
723
-
724
  try:
725
- info_lines = [f"**👥 Registered Workers ({len(self.known_face_ids)})**\n"]
726
-
727
- for i, (worker_id, name) in enumerate(zip(self.known_face_ids, self.known_face_names)):
728
- status = " Present Today" if worker_id in self.daily_attendance else " Not Marked"
729
- embedding_count = len(self.known_face_embeddings[i])
730
- info_lines.append(f"- **{name}** (ID: `{worker_id}`) - {status} - {embedding_count} face(s)")
731
-
732
- return "\n".join(info_lines)
733
-
734
- except Exception as e:
735
- return f"Error loading worker info: {e}"
736
-
737
- def get_attendance_stats(self) -> str:
738
- """Get attendance statistics for today."""
739
- today_str = date.today().isoformat()
740
- total_workers = len(self.known_face_ids)
741
- present_today = len(self.daily_attendance)
742
- attendance_rate = (present_today / max(total_workers, 1)) * 100
743
-
744
- return f"""
745
- **📊 Today's Attendance Statistics**
746
- - **Date**: {today_str}
747
- - **Total Workers**: {total_workers}
748
- - **Present Today**: {present_today}
749
- - **Attendance Rate**: {attendance_rate:.1f}%
750
- - **Session Attendees**: {len(self.session_attendees)}
751
- """
752
 
753
  # --- GRADIO UI ---
754
  attendance_system = AttendanceSystem()
755
 
756
  def create_interface():
757
- with gr.Blocks(theme=gr.themes.Soft(), title="Advanced Attendance System") as demo:
758
- gr.Markdown("# 🎯 Advanced Face Recognition Attendance System v2.0")
759
-
760
  with gr.Tabs():
761
  with gr.Tab("⚙��� Controls & Status"):
762
  gr.Markdown("### 1. Choose Input Source & Start Processing")
@@ -769,66 +473,51 @@ def create_interface():
769
  with gr.Tab("Upload Video", id=1):
770
  video_file = gr.Video(label="Upload Video File", sources=["upload"])
771
  with gr.Column(scale=1):
772
- start_btn = gr.Button("▶️ Start Processing", variant="primary", size="lg")
773
- stop_btn = gr.Button("⏹️ Stop Processing", variant="stop", size="lg")
774
- status_box = gr.Textbox(label="Status", interactive=False, value="System Ready - Enhanced Recognition & Auto-Registration")
775
-
776
- gr.Markdown("### 2. Enhanced Features")
777
- gr.Markdown("""
778
- **🚀 Key Features:**
779
- - ✅ **Smart Recognition**: Recognizes existing workers from different angles
780
- - ✅ **Auto-Registration**: Automatically registers unknown faces with good quality
781
- - ✅ **Duplicate Prevention**: No duplicate attendance logs per day/session
782
- - ✅ **Unique IDs**: Enhanced ID generation with timestamps and UUIDs
783
- - ✅ **Quality Control**: Only processes clear, high-quality faces
784
- - ✅ **Speed Optimization**: Fast video processing for live feeds
785
-
786
- **🎨 Color Coding:**
787
- - <span style='color: green'>**Green**</span> = Recognized Worker (Present)
788
- - <span style='color: blue'>**Blue**</span> = New Auto-Registration
789
- - <span style='color: orange'>**Orange**</span> = Unknown Face
790
- - <span style='color: yellow'>**Yellow**</span> = Poor Quality Face
791
- """)
792
 
793
- with gr.Tab("📊 Output & Live Feed"):
794
  with gr.Row():
795
  with gr.Column(scale=2):
796
- video_output = gr.Image(label="🎥 Live Recognition Feed", interactive=False)
797
  with gr.Column(scale=1):
798
- session_log_display = gr.Markdown(label="📋 Live Session Log", value="System ready for processing...")
799
 
800
  with gr.Tab("👤 Worker Management"):
801
  with gr.Row():
802
  with gr.Column():
803
- gr.Markdown("### Register New Worker")
804
- register_image = gr.Image(label="📸 Upload Clear Photo (Good Lighting Required)", type="pil")
805
- register_name = gr.Textbox(label="👤 Worker's Full Name", placeholder="Enter full name...")
806
- register_btn = gr.Button("✅ Register Worker", variant="primary", size="lg")
807
  register_output = gr.Textbox(label="Registration Status", interactive=False)
808
-
809
  with gr.Column():
810
- gr.Markdown("### Registered Workers")
811
  registered_workers_info = gr.Markdown(value=attendance_system.get_registered_workers_info())
812
- refresh_workers_btn = gr.Button("🔄 Refresh Workers List", variant="secondary")
813
-
814
- gr.Markdown("### Today's Statistics")
815
- attendance_stats = gr.Markdown(value=attendance_system.get_attendance_stats())
816
- refresh_stats_btn = gr.Button("📊 Refresh Statistics", variant="secondary")
817
 
818
  # --- Event Handlers ---
819
  def on_tab_select(evt: gr.SelectData):
820
  return evt.index
821
-
822
  video_tabs.select(fn=on_tab_select, inputs=None, outputs=[selected_tab_index])
823
 
824
  def start_wrapper(tab_index, cam_src, vid_path):
825
  source = cam_src if tab_index == 0 else vid_path
826
- if source is None:
827
- return "❌ Please provide an input source."
828
- return attendance_system.start_processing(source)
 
 
 
 
829
 
830
- start_btn.click(fn=start_wrapper, inputs=[selected_tab_index, camera_source, video_file], outputs=[status_box])
831
- stop_btn.click(fn=attendance_system.stop_processing, inputs=None, outputs=[status_box])
 
 
 
832
 
833
  register_btn.click(
834
  fn=attendance_system.register_worker_manual,
@@ -841,84 +530,37 @@ def create_interface():
841
  outputs=[registered_workers_info]
842
  )
843
 
844
- refresh_stats_btn.click(
845
- fn=attendance_system.get_attendance_stats,
846
- outputs=[attendance_stats]
847
- )
848
-
849
  def update_ui_generator():
850
- """Enhanced UI update generator with better error handling."""
851
  while True:
852
- try:
853
- # Handle errors
854
- if attendance_system.error_message:
855
- yield None, f"❌ **Error**: {attendance_system.error_message}"
856
- time.sleep(3)
857
- attendance_system.error_message = None
858
- continue
859
-
860
- # Active processing
861
- if attendance_system.is_processing.is_set():
862
- frame = None
863
- log_content = "🔄 Processing..."
864
-
865
- # Get latest frame
866
- try:
867
- if not attendance_system.frame_queue.empty():
868
- frame = attendance_system.frame_queue.get_nowait()
869
- if frame is not None:
870
- frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
871
- except queue.Empty:
872
- pass
873
-
874
- # Format session log
875
- if attendance_system.session_log:
876
- recent_logs = attendance_system.session_log[-10:] # Show last 10 entries
877
- log_content = "### 📋 Live Session Activity\n" + "\n".join(reversed(recent_logs))
878
-
879
- yield frame, log_content
880
 
881
- # Processing stopped - show final results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
882
  else:
883
- if attendance_system.last_processed_frame is not None:
884
- final_frame = cv2.cvtColor(attendance_system.last_processed_frame, cv2.COLOR_BGR2RGB)
885
- final_log_content = "### 📋 Session Complete\n"
886
-
887
- if attendance_system.final_log:
888
- final_log_content += "\n".join(reversed(attendance_system.final_log))
889
- else:
890
- final_log_content += "No activities recorded in this session."
891
-
892
- # Add session summary
893
- if attendance_system.session_attendees:
894
- final_log_content += f"\n\n**📊 Session Summary:** {len(attendance_system.session_attendees)} unique attendees processed"
895
-
896
- yield final_frame, final_log_content
897
- else:
898
- yield None, "### 🏠 System Ready\nGo to 'Controls & Status' tab to start processing."
899
-
900
- time.sleep(0.1) # Update frequency
901
-
902
- except Exception as e:
903
- logger.error(f"UI update error: {e}")
904
- yield None, f"❌ UI Error: {str(e)}"
905
- time.sleep(1)
906
 
907
- # Auto-refresh UI
908
  demo.load(fn=update_ui_generator, outputs=[video_output, session_log_display])
909
-
910
  return demo
911
 
912
  if __name__ == "__main__":
913
- print("🚀 Starting Advanced Face Recognition Attendance System v2.0...")
914
- print("📊 Enhanced with: Smart Recognition, Auto-Registration, Quality Control")
915
-
916
  app = create_interface()
917
- app.queue(max_size=20)
918
- app.launch(
919
- server_name="0.0.0.0",
920
- server_port=7860,
921
- show_error=True,
922
- debug=False,
923
- share=False
924
- )
 
9
  import queue
10
  import threading
11
  import time
 
 
12
  from datetime import datetime, date
13
  from io import BytesIO
14
  from typing import Tuple, Optional, List, Dict
 
61
  logger.error(f"❌ Salesforce connection failed: {e}")
62
  raise
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  # --- CORE LOGIC ---
65
 
66
  class AttendanceSystem:
 
73
  self.is_processing = threading.Event()
74
  self.frame_queue = queue.Queue(maxsize=10)
75
  self.error_message = None
76
+ self.last_processed_frame = None # Holds the final frame after processing
77
+ self.final_log = None # Holds the final log after processing
78
 
79
+ # Data Storage
80
+ self.known_face_embeddings: List[np.ndarray] = []
81
  self.known_face_names: List[str] = []
82
  self.known_face_ids: List[str] = []
 
83
  self.next_worker_id: int = 1
84
 
85
+ # Session Tracking
86
+ self.last_recognition_time: Dict[str, float] = {}
87
+ self.recognition_cooldown = 5 # seconds
88
  self.session_log: List[str] = []
89
+ self.today_attendance: Dict[str, bool] = {} # Track attendance for today
90
+ self.last_detected_faces: Dict[str, float] = {} # Track last detection time per worker
91
+
92
+ # Performance optimization
93
+ self.frame_skip = 2 # Process every 3rd frame for faster processing
94
+ self.frame_counter = 0
 
95
 
96
  # Initialize
97
  self.sf = connect_to_salesforce()
98
  self._create_directories()
99
  self.load_worker_data()
 
100
 
101
  def _create_directories(self):
102
  os.makedirs("data/faces", exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
  def load_worker_data(self):
105
  logger.info("Loading worker data...")
 
113
  temp_embeddings, temp_names, temp_ids, max_id = [], [], [], 0
114
  for worker in workers:
115
  if worker.get('Face_Embedding__c'):
116
+ temp_embeddings.append(np.array(json.loads(worker['Face_Embedding__c'])))
 
 
 
 
 
 
 
 
117
  temp_names.append(worker['Name'])
118
  temp_ids.append(worker['Worker_ID__c'])
 
 
119
  try:
120
+ worker_num = int(worker['Worker_ID__c'][1:])
121
  if worker_num > max_id:
122
  max_id = worker_num
123
+ except (ValueError, TypeError):
124
  continue
125
 
126
  self.known_face_embeddings = temp_embeddings
 
139
  def _load_local_worker_data(self):
140
  try:
141
  if os.path.exists("data/workers.pkl"):
142
+ with open("data/workers.pkl", "rb") as f: data = pickle.load(f)
 
143
  self.known_face_embeddings = data.get("embeddings", [])
144
  self.known_face_names = data.get("names", [])
145
  self.known_face_ids = data.get("ids", [])
146
  self.next_worker_id = data.get("next_id", 1)
 
147
  logger.info(f"✅ Loaded {len(self.known_face_ids)} workers from local cache.")
148
  except Exception as e:
149
  logger.error(f"❌ Error loading local data: {e}")
150
 
151
  def save_local_worker_data(self):
152
  try:
153
+ worker_data = {"embeddings": self.known_face_embeddings, "names": self.known_face_names, "ids": self.known_face_ids, "next_id": self.next_worker_id}
154
+ with open("data/workers.pkl", "wb") as f: pickle.dump(worker_data, f)
 
 
 
 
 
 
 
155
  except Exception as e:
156
  logger.error(f"❌ Error saving local worker data: {e}")
157
 
 
159
  def register_worker_manual(self, image: Image.Image, name: str) -> Tuple[str, str]:
160
  if image is None or not name.strip():
161
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
 
162
  try:
163
  image_array = np.array(image)
164
+ # Analyze face quality before registration
165
+ analysis = DeepFace.analyze(img_path=image_array, actions=['emotion'], enforce_detection=True)
166
+ if analysis[0]['face_confidence'] < 0.95:
167
+ return "❌ Face not clear enough for registration!", self.get_registered_workers_info()
168
 
169
+ embedding = DeepFace.represent(img_path=image_array, model_name='Facenet')[0]['embedding']
170
+ if self._is_duplicate_face(embedding):
171
+ return f"❌ Face matches an existing worker!", self.get_registered_workers_info()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
+ worker_id = f"W{self.next_worker_id:04d}"
174
  name = name.strip().title()
175
+ self._add_worker_to_system(worker_id, name, embedding, image_array)
176
  self.save_local_worker_data()
177
+ self.load_worker_data()
178
+ return f"✅ {name} registered with ID: {worker_id}!", self.get_registered_workers_info()
179
+ except ValueError:
180
+ return "❌ No face detected in the image!", self.get_registered_workers_info()
 
181
  except Exception as e:
182
  return f"❌ Registration error: {e}", self.get_registered_workers_info()
183
 
184
+ def _register_worker_auto(self, face_image: np.ndarray) -> Optional[Tuple[str, str]]:
 
185
  try:
186
+ # Check face quality before auto-registration
187
+ analysis = DeepFace.analyze(img_path=face_image, actions=['emotion'], enforce_detection=False)
188
+ if analysis[0]['face_confidence'] < 0.95:
 
 
 
 
 
 
 
189
  return None
190
+
 
191
  embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
192
+ if self._is_duplicate_face(embedding):
 
 
 
193
  return None
194
+
195
+ worker_id = f"W{self.next_worker_id:04d}"
196
+ worker_name = f"Worker {self.next_worker_id}"
197
+ self._add_worker_to_system(worker_id, worker_name, embedding, face_image)
 
 
198
  self.save_local_worker_data()
199
+ log_msg = f"🆕 [{datetime.now().strftime('%H:%M:%S')}] Auto-registered: {worker_name} ({worker_id})"
 
200
  self.session_log.append(log_msg)
201
  logger.info(log_msg)
 
202
  return worker_id, worker_name
 
203
  except Exception as e:
204
  logger.error(f"❌ Auto-registration error: {e}")
205
  return None
206
 
207
+ def _add_worker_to_system(self, worker_id: str, name: str, embedding: List[float], image_array: np.ndarray):
208
+ self.known_face_embeddings.append(np.array(embedding))
 
 
209
  self.known_face_names.append(name)
210
  self.known_face_ids.append(worker_id)
 
 
 
 
 
211
  self.next_worker_id += 1
 
 
212
  face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
213
  face_pil.save(f"data/faces/{worker_id}.jpg")
214
+ caption = self._get_image_caption(face_pil)
 
215
  if self.sf:
216
  try:
217
+ worker_record = self.sf.Worker__c.create({'Name': name, 'Worker_ID__c': worker_id, 'Face_Embedding__c': json.dumps(embedding), 'Image_Caption__c': caption})
 
 
 
 
 
 
 
 
 
 
 
218
  image_url = self._upload_image_to_salesforce(face_pil, worker_record['id'], worker_id)
219
+ if image_url: self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
 
 
220
  logger.info(f"✅ Worker {worker_id} synced to Salesforce.")
221
  except Exception as e:
222
  logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
223
 
224
+ def _is_duplicate_face(self, embedding: List[float], threshold: float = 10.0) -> bool:
225
+ if not self.known_face_embeddings: return False
226
+ distances = [np.linalg.norm(np.array(embedding) - known_embedding) for known_embedding in self.known_face_embeddings]
227
+ return min(distances) < threshold
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
230
+ today_str = date.today().isoformat()
231
+
232
  # Check if already marked today
233
+ if worker_id in self.today_attendance:
 
234
  return False
235
+
236
+ # Check cooldown period
237
  current_time = time.time()
238
+ if worker_id in self.last_recognition_time and (current_time - self.last_recognition_time[worker_id] < self.recognition_cooldown):
 
 
 
 
 
 
 
239
  return False
240
+
241
+ # Mark attendance
242
+ current_time_dt = datetime.now()
 
 
243
  if self.sf:
244
  try:
245
  self.sf.Attendance__c.create({
246
  'Worker_ID__c': worker_id,
247
  'Name__c': worker_name,
248
  'Date__c': today_str,
249
+ 'Timestamp__c': current_time_dt.isoformat(),
250
  'Status__c': "Present"
251
  })
252
  except Exception as e:
253
  logger.error(f"❌ Error saving attendance to Salesforce: {e}")
254
 
255
+ self.today_attendance[worker_id] = True
 
 
256
  self.last_recognition_time[worker_id] = current_time
257
+ log_msg = f"✅ [{current_time_dt.strftime('%H:%M:%S')}] Marked Present: {worker_name} ({worker_id})"
 
258
  self.session_log.append(log_msg)
 
 
259
  return True
260
 
261
+ # --- Video Processing ---
262
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
263
  """
264
+ Process a single video frame with optimizations for speed and accuracy.
265
  """
266
  try:
267
+ # Skip frames for faster processing
268
+ self.frame_counter += 1
269
+ if self.frame_counter % (self.frame_skip + 1) != 0:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  return frame
271
 
272
+ # Resize frame for faster processing (keeping aspect ratio)
273
+ height, width = frame.shape[:2]
274
+ new_width = 640
275
+ new_height = int((new_width / width) * height)
276
+ small_frame = cv2.resize(frame, (new_width, new_height))
277
 
278
+ face_objs = DeepFace.extract_faces(
279
+ img_path=small_frame,
280
+ detector_backend='opencv',
281
+ enforce_detection=False,
282
+ align=True
283
+ )
284
+
285
+ for face_obj in face_objs:
286
+ confidence = face_obj['confidence']
287
+ if confidence < 0.95: # Higher confidence threshold
288
  continue
289
 
290
+ facial_area = face_obj['facial_area']
291
+ x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
 
 
 
292
 
293
+ # Scale coordinates back to original frame size
294
+ x = int(x * width / new_width)
295
+ y = int(y * height / new_height)
296
+ w = int(w * width / new_width)
297
+ h = int(h * height / new_height)
298
 
299
+ face_image = frame[y:y+h, x:x+w]
300
  if face_image.size == 0:
301
  continue
302
 
303
+ # Only process faces that haven't been detected recently
304
+ current_time = time.time()
305
+ face_key = f"{x}_{y}_{w}_{h}"
306
+ if face_key in self.last_detected_faces and (current_time - self.last_detected_faces[face_key] < 2.0):
307
+ continue
308
+ self.last_detected_faces[face_key] = current_time
309
+
310
+ embedding = DeepFace.represent(
311
+ img_path=face_image,
312
+ model_name='Facenet',
313
+ enforce_detection=False,
314
+ align=True
315
+ )[0]['embedding']
316
 
317
+ if not self.known_face_embeddings:
 
 
 
 
 
318
  continue
319
 
320
+ distances = [np.linalg.norm(np.array(embedding) - known) for known in self.known_face_embeddings]
321
+ min_dist = min(distances) if distances else float('inf')
322
+ match_index = distances.index(min_dist) if min_dist < 10.0 else -1
323
+
324
+ color, worker_id, worker_name = (0, 0, 255), None, "Unknown"
325
+
326
+ if match_index != -1:
327
+ worker_id = self.known_face_ids[match_index]
328
+ worker_name = self.known_face_names[match_index]
329
+ color = (0, 255, 0) # Green for known workers
330
+ self.mark_attendance(worker_id, worker_name)
331
+ else:
332
+ # Only attempt auto-registration for high-quality faces
333
+ analysis = DeepFace.analyze(img_path=face_image, actions=['emotion'], enforce_detection=False)
334
+ if analysis[0]['face_confidence'] >= 0.95:
335
+ color = (0, 165, 255) # Orange for potential new worker
336
+ new_worker = self._register_worker_auto(face_image)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  if new_worker:
338
+ worker_id, worker_name = new_worker[0], new_worker[1]
339
  self.mark_attendance(worker_id, worker_name)
340
+
341
+ # Draw rectangle and label
342
+ label = f"{worker_name}" + (f" ({worker_id})" if worker_id else "")
343
+ cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
344
+ cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
  return frame
 
347
  except Exception as e:
348
  logger.error(f"ERROR in process_frame: {e}")
349
  return frame
350
 
351
  def _processing_loop(self, source):
 
352
  video_capture = cv2.VideoCapture(source)
 
353
  if not video_capture.isOpened():
354
+ err_msg = f"❌ **Error:** Could not open video source. The file may be corrupt or in an unsupported format. Please try converting it to a standard MP4."
355
  self.error_message = err_msg
356
  self.is_processing.clear()
357
  return
358
+
359
+ # Set higher FPS if possible (for live camera)
360
+ if isinstance(source, int):
 
 
361
  video_capture.set(cv2.CAP_PROP_FPS, 30)
362
+
 
 
 
 
363
  while self.is_processing.is_set():
364
  ret, frame = video_capture.read()
365
  if not ret:
 
366
  break
367
+
 
 
 
368
  processed_frame = self.process_frame(frame)
369
 
370
+ if not self.frame_queue.full():
371
+ self.frame_queue.put(processed_frame)
372
+
 
 
 
 
373
  self.last_processed_frame = processed_frame
374
+ time.sleep(0.02) # Reduced sleep for faster processing
375
 
 
 
 
 
376
  self.final_log = self.session_log.copy()
377
  video_capture.release()
378
  self.is_processing.clear()
 
379
 
380
  def start_processing(self, source) -> str:
 
381
  if self.is_processing.is_set():
382
  return "⚠️ Processing is already active."
383
+
384
  # Reset states for new session
385
  self.session_log.clear()
 
386
  self.last_recognition_time.clear()
387
+ self.today_attendance.clear()
388
+ self.last_detected_faces.clear()
389
  self.error_message = None
390
  self.last_processed_frame = None
391
  self.final_log = None
392
+ self.frame_counter = 0
 
 
 
393
 
394
  self.is_processing.set()
395
+ self.processing_thread = threading.Thread(
396
+ target=self._processing_loop,
397
+ args=(source,),
398
+ daemon=True
399
+ )
400
  self.processing_thread.start()
401
+ return f"✅ Started processing..."
 
402
 
403
  def stop_processing(self) -> str:
 
404
  self.is_processing.clear()
405
  self.error_message = None
406
+ self.last_processed_frame = None
407
+ self.final_log = None
408
+ return "✅ Processing stopped by user."
 
 
 
 
409
 
410
  # --- Helper & Reporting ---
411
  def _get_image_caption(self, image: Image.Image) -> str:
 
416
  image.save(buffered, format="JPEG")
417
  img_data = buffered.getvalue()
418
  headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
419
+ response = requests.post(HF_API_URL, headers=headers, data=img_data)
420
  response.raise_for_status()
421
  result = response.json()
422
  return result[0].get("generated_text", "No caption found.")
 
432
  image.save(buffered, format="JPEG")
433
  encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
434
  cv = self.sf.ContentVersion.create({
435
+ 'Title': f'Image_{worker_id}',
436
+ 'PathOnClient': f'{worker_id}.jpg',
437
+ 'VersionData': encoded_image,
438
  'FirstPublishLocationId': record_id
439
  })
440
+ return f"/{cv['id']}" # Relative URL
441
  except Exception as e:
442
  logger.error(f"Salesforce image upload error: {e}")
443
  return None
444
 
445
  def get_registered_workers_info(self) -> str:
446
+ if not self.sf:
447
+ return "❌ Salesforce not connected."
 
 
448
  try:
449
+ records = self.sf.query_all("SELECT Name, Worker_ID__c FROM Worker__c ORDER BY Name")['records']
450
+ if not records:
451
+ return "No workers registered."
452
+ return f"**👥 Registered Workers ({len(records)})**\n" + "\n".join(
453
+ [f"- **{w['Name']}** (ID: {w['Worker_ID__c']})" for w in records]
454
+ )
455
+ except Exception as e:
456
+ return f"Error: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
 
458
  # --- GRADIO UI ---
459
  attendance_system = AttendanceSystem()
460
 
461
  def create_interface():
462
+ with gr.Blocks(theme=gr.themes.Soft(), title="Attendance System") as demo:
463
+ gr.Markdown("# 🎯 Advanced Face Recognition Attendance System")
 
464
  with gr.Tabs():
465
  with gr.Tab("⚙��� Controls & Status"):
466
  gr.Markdown("### 1. Choose Input Source & Start Processing")
 
473
  with gr.Tab("Upload Video", id=1):
474
  video_file = gr.Video(label="Upload Video File", sources=["upload"])
475
  with gr.Column(scale=1):
476
+ start_btn = gr.Button("▶️ Start Processing", variant="primary")
477
+ stop_btn = gr.Button("⏹️ Stop Processing", variant="stop")
478
+ status_box = gr.Textbox(label="Status", interactive=False, value="System Ready.")
479
+ gr.Markdown("### 2. View Results in the 'Output & Log' Tab")
480
+ gr.Markdown("**🎨 Color Coding:** <font color='green'>Green</font> = Known, <font color='orange'>Orange</font> = New, <font color='red'>Red</font> = Unknown")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
 
482
+ with gr.Tab("📊 Output & Log"):
483
  with gr.Row():
484
  with gr.Column(scale=2):
485
+ video_output = gr.Image(label="Recognition Output", interactive=False)
486
  with gr.Column(scale=1):
487
+ session_log_display = gr.Markdown(label="📋 Session Log", value="System is ready.")
488
 
489
  with gr.Tab("👤 Worker Management"):
490
  with gr.Row():
491
  with gr.Column():
492
+ register_image = gr.Image(label="Upload Worker's Photo", type="pil")
493
+ register_name = gr.Textbox(label="Worker's Full Name")
494
+ register_btn = gr.Button("Register Worker", variant="primary")
 
495
  register_output = gr.Textbox(label="Registration Status", interactive=False)
 
496
  with gr.Column():
 
497
  registered_workers_info = gr.Markdown(value=attendance_system.get_registered_workers_info())
498
+ refresh_workers_btn = gr.Button("🔄 Refresh List")
 
 
 
 
499
 
500
  # --- Event Handlers ---
501
  def on_tab_select(evt: gr.SelectData):
502
  return evt.index
503
+
504
  video_tabs.select(fn=on_tab_select, inputs=None, outputs=[selected_tab_index])
505
 
506
  def start_wrapper(tab_index, cam_src, vid_path):
507
  source = cam_src if tab_index == 0 else vid_path
508
+ return "Please provide an input source." if source is None else attendance_system.start_processing(source)
509
+
510
+ start_btn.click(
511
+ fn=start_wrapper,
512
+ inputs=[selected_tab_index, camera_source, video_file],
513
+ outputs=[status_box]
514
+ )
515
 
516
+ stop_btn.click(
517
+ fn=attendance_system.stop_processing,
518
+ inputs=None,
519
+ outputs=[status_box]
520
+ )
521
 
522
  register_btn.click(
523
  fn=attendance_system.register_worker_manual,
 
530
  outputs=[registered_workers_info]
531
  )
532
 
 
 
 
 
 
533
  def update_ui_generator():
 
534
  while True:
535
+ if attendance_system.error_message:
536
+ yield None, attendance_system.error_message
537
+ time.sleep(2)
538
+ attendance_system.error_message = None
539
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
 
541
+ if attendance_system.is_processing.is_set():
542
+ frame, log_md = None, "\n".join(reversed(attendance_system.session_log[-20:])) or "Processing..."
543
+ try:
544
+ if not attendance_system.frame_queue.empty():
545
+ frame = attendance_system.frame_queue.get_nowait()
546
+ if frame is not None:
547
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
548
+ except queue.Empty:
549
+ pass
550
+ yield frame, log_md
551
+ else:
552
+ if attendance_system.last_processed_frame is not None:
553
+ final_frame = cv2.cvtColor(attendance_system.last_processed_frame, cv2.COLOR_BGR2RGB)
554
+ final_log_md = "\n".join(reversed(attendance_system.final_log[-20:])) or "Processing complete. No log entries."
555
+ yield final_frame, final_log_md
556
  else:
557
+ yield None, "System stopped. Go to 'Controls & Status' to start."
558
+ time.sleep(0.05) # Faster UI updates
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
 
 
560
  demo.load(fn=update_ui_generator, outputs=[video_output, session_log_display])
 
561
  return demo
562
 
563
  if __name__ == "__main__":
 
 
 
564
  app = create_interface()
565
+ app.queue()
566
+ app.launch(server_name="0.0.0.0", server_port=7860, show_error=True, debug=True)