PrashanthB461 commited on
Commit
40947be
Β·
verified Β·
1 Parent(s): db548a2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +125 -201
app.py CHANGED
@@ -72,9 +72,17 @@ class AttendanceSystem:
72
  self.processing_thread = None
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] = []
@@ -83,10 +91,10 @@ class AttendanceSystem:
83
  self.next_worker_id: int = 1
84
 
85
  # Session Tracking
86
- self.last_recognition_time = {}
87
- self.recognition_cooldown = 5
88
  self.session_log: List[str] = []
89
- self.today_attendance = set() # Track attendance for current session
 
 
90
 
91
  # Initialize
92
  self.sf = connect_to_salesforce()
@@ -158,13 +166,15 @@ class AttendanceSystem:
158
  image_array = np.array(image)
159
  DeepFace.analyze(img_path=image_array, actions=['emotion'], enforce_detection=True)
160
  embedding = DeepFace.represent(img_path=image_array, model_name='Facenet')[0]['embedding']
 
 
161
  if self._is_duplicate_face(embedding):
162
- return f"❌ Face matches an existing worker!", self.get_registered_workers_info()
163
 
164
  worker_id = f"W{self.next_worker_id:04d}"
165
  name = name.strip().title()
166
  self._add_worker_to_system(worker_id, name, embedding, image_array)
167
- self.save_local_worker_data()
168
  self.load_worker_data()
169
  return f"βœ… {name} registered with ID: {worker_id}!", self.get_registered_workers_info()
170
  except ValueError:
@@ -175,24 +185,25 @@ class AttendanceSystem:
175
  def _register_worker_auto(self, face_image: np.ndarray) -> Optional[Tuple[str, str]]:
176
  try:
177
  embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
178
- if self._is_duplicate_face(embedding): return None
 
 
 
 
179
  worker_id = f"W{self.next_worker_id:04d}"
180
  worker_name = f"Unknown Worker {self.next_worker_id}"
 
181
  self._add_worker_to_system(worker_id, worker_name, embedding, face_image)
182
  self.save_local_worker_data()
183
- log_msg = f"πŸ†• [{datetime.now().strftime('%H:%M:%S')}] Auto-registered: {worker_name} ({worker_id})"
184
- self.session_log.append(log_msg)
185
- logger.info(log_msg)
186
  return worker_id, worker_name
187
  except Exception as e:
188
  logger.error(f"❌ Auto-registration error: {e}")
189
  return None
190
 
191
  def _add_worker_to_system(self, worker_id: str, name: str, embedding: List[float], image_array: np.ndarray):
192
- self.known_face_embeddings.append(np.array(embedding))
193
- self.known_face_names.append(name)
194
- self.known_face_ids.append(worker_id)
195
- self.next_worker_id += 1
196
  face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
197
  face_pil.save(f"data/faces/{worker_id}.jpg")
198
  caption = self._get_image_caption(face_pil)
@@ -205,138 +216,107 @@ class AttendanceSystem:
205
  except Exception as e:
206
  logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
207
 
208
- def _is_duplicate_face(self, embedding: List[float], threshold: float = 10.0) -> bool:
 
209
  if not self.known_face_embeddings: return False
210
  distances = [np.linalg.norm(np.array(embedding) - known_embedding) for known_embedding in self.known_face_embeddings]
211
- return min(distances) < threshold
 
212
 
213
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
 
214
  today_str = date.today().isoformat()
215
-
216
- # Check if already marked today in this session
217
- if worker_id in self.today_attendance:
218
- return False
219
-
220
- # Check if already marked in Salesforce
221
  if self._has_attended_today(worker_id, today_str):
222
- self.today_attendance.add(worker_id) # Add to session tracking
223
- return False
224
-
225
  current_time = datetime.now()
226
  if self.sf:
227
  try:
228
- self.sf.Attendance__c.create({
229
- 'Worker_ID__c': worker_id,
230
- 'Name__c': worker_name,
231
- 'Date__c': today_str,
232
- 'Timestamp__c': current_time.isoformat(),
233
- 'Status__c': "Present"
234
- })
235
  except Exception as e:
236
  logger.error(f"❌ Error saving attendance to Salesforce: {e}")
237
  return False
238
-
239
- log_msg = f"βœ… [{current_time.strftime('%H:%M:%S')}] Marked Present: {worker_name} ({worker_id})"
240
- self.session_log.append(log_msg)
241
- self.today_attendance.add(worker_id)
242
- return True
243
 
244
  def _has_attended_today(self, worker_id: str, today_str: str) -> bool:
 
245
  if self.sf:
246
  try:
247
- query = f"SELECT Id FROM Attendance__c WHERE Worker_ID__c = '{worker_id}' AND Date__c = '{today_str}'"
248
- return self.sf.query(query)['totalSize'] > 0
249
- except Exception as e:
250
- logger.error(f"❌ Error checking attendance in Salesforce: {e}")
251
  return False
252
 
253
  # --- Video Processing ---
254
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
255
- """
256
- Process a single video frame with improved accuracy and duplicate handling.
257
- """
258
  try:
259
- # Convert to RGB for better face detection
260
- rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
261
-
262
- # Extract faces with higher confidence threshold
263
- face_objs = DeepFace.extract_faces(
264
- img_path=rgb_frame,
265
- detector_backend='opencv',
266
- enforce_detection=False,
267
- align=True
268
- )
269
 
270
  for face_obj in face_objs:
271
- confidence = face_obj['confidence']
272
-
273
- # Skip low confidence detections
274
- if confidence < 0.97: # Higher threshold for better accuracy
275
- continue
276
 
277
  facial_area = face_obj['facial_area']
278
  x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
279
- face_image = rgb_frame[y:y+h, x:x+w]
280
 
281
- if face_image.size == 0:
282
- continue
283
 
284
- # Get face embedding with normalization
285
- embedding = np.array(DeepFace.represent(
286
- img_path=face_image,
287
- model_name='Facenet',
288
- enforce_detection=False,
289
- normalization='base'
290
- )[0]['embedding'])
291
-
292
- # Skip if no known faces
293
- if not self.known_face_embeddings:
294
- cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 0, 255), 2)
295
- cv2.putText(frame, "Unknown", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
296
- continue
297
-
298
- # Calculate distances with all known faces
299
- distances = [np.linalg.norm(embedding - known) for known in self.known_face_embeddings]
300
- min_dist = min(distances)
301
- match_index = np.argmin(distances)
302
 
303
- # Determine recognition result
304
- recognized = min_dist < 8.0 # Lower threshold for stricter matching
305
- worker_id = self.known_face_ids[match_index] if recognized else None
306
- worker_name = self.known_face_names[match_index] if recognized else "Unknown"
307
-
308
- # Set display properties based on recognition status
309
- if recognized:
310
- color = (0, 255, 0) # Green for known workers
311
- label = f"{worker_name} ({worker_id})"
312
 
313
- # Mark attendance if not already done today
314
- if worker_id not in self.today_attendance:
315
- self.mark_attendance(worker_id, worker_name)
316
- else:
317
- # Attempt auto-registration only if face is clear enough
318
- if w > 100 and h > 100: # Minimum face size threshold
319
- new_worker = self._register_worker_auto(face_image)
320
- if new_worker:
321
- worker_id, worker_name = new_worker
322
- color = (0, 165, 255) # Orange for new workers
323
- label = f"{worker_name} ({worker_id})"
324
- self.mark_attendance(worker_id, worker_name)
325
- else:
326
- color = (0, 0, 255) # Red for unknown
327
- label = "Unknown"
328
- else:
329
- color = (0, 0, 255) # Red for unknown
330
- label = "Unknown (Too small)"
331
-
332
- # Draw rectangle and label
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
334
- cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
335
-
336
  return frame
337
  except Exception as e:
338
- logger.error(f"Error in process_frame: {e}")
339
- return frame
340
 
341
  def _processing_loop(self, source):
342
  video_capture = cv2.VideoCapture(source)
@@ -346,58 +326,40 @@ class AttendanceSystem:
346
  self.is_processing.clear()
347
  return
348
 
349
- # Reset attendance tracking for new session
350
- self.today_attendance.clear()
351
- self.session_log.clear()
352
-
353
  while self.is_processing.is_set():
354
  ret, frame = video_capture.read()
355
- if not ret:
356
- break
357
-
358
  processed_frame = self.process_frame(frame)
 
359
 
360
- if not self.frame_queue.full():
361
- self.frame_queue.put(processed_frame)
362
-
363
- self.last_processed_frame = processed_frame
364
  time.sleep(0.05)
365
 
366
- self.final_log = self.session_log.copy()
367
  video_capture.release()
368
  self.is_processing.clear()
369
 
370
  def start_processing(self, source) -> str:
371
- if self.is_processing.is_set():
372
- return "⚠️ Processing is already active."
373
-
374
  # Reset states for the new session
375
  self.session_log.clear()
376
- self.last_recognition_time.clear()
377
- self.error_message = None
378
- self.last_processed_frame = None
379
- self.final_log = None
380
-
381
  self.is_processing.set()
382
- self.processing_thread = threading.Thread(
383
- target=self._processing_loop,
384
- args=(source,),
385
- daemon=True
386
- )
387
  self.processing_thread.start()
388
  return f"βœ… Started processing..."
389
 
390
  def stop_processing(self) -> str:
391
- self.is_processing.clear()
392
- self.error_message = None
393
- self.last_processed_frame = None
394
- self.final_log = None
395
  return "βœ… Processing stopped by user."
396
 
397
  # --- Helper & Reporting ---
398
  def _get_image_caption(self, image: Image.Image) -> str:
399
- if not HF_API_TOKEN:
400
- return "Hugging Face API token not configured."
401
  try:
402
  buffered = BytesIO()
403
  image.save(buffered, format="JPEG")
@@ -412,39 +374,27 @@ class AttendanceSystem:
412
  return "Caption generation failed."
413
 
414
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
415
- if not self.sf:
416
- return None
417
  try:
418
  buffered = BytesIO()
419
  image.save(buffered, format="JPEG")
420
  encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
421
- cv = self.sf.ContentVersion.create({
422
- 'Title': f'Image_{worker_id}',
423
- 'PathOnClient': f'{worker_id}.jpg',
424
- 'VersionData': encoded_image,
425
- 'FirstPublishLocationId': record_id
426
- })
427
  return f"/{cv['id']}" # Relative URL
428
  except Exception as e:
429
  logger.error(f"Salesforce image upload error: {e}")
430
  return None
431
 
432
  def get_registered_workers_info(self) -> str:
433
- if not self.sf:
434
- return "❌ Salesforce not connected."
435
  try:
436
  records = self.sf.query_all("SELECT Name, Worker_ID__c FROM Worker__c ORDER BY Name")['records']
437
- if not records:
438
- return "No workers registered."
439
- return f"**πŸ‘₯ Registered Workers ({len(records)})**\n" + "\n".join(
440
- [f"- **{w['Name']}** (ID: {w['Worker_ID__c']})" for w in records]
441
- )
442
- except Exception as e:
443
- return f"Error: {e}"
444
 
445
  # --- GRADIO UI ---
446
  attendance_system = AttendanceSystem()
447
-
448
  def create_interface():
449
  with gr.Blocks(theme=gr.themes.Soft(), title="Attendance System") as demo:
450
  gr.Markdown("# 🎯 Advanced Face Recognition Attendance System")
@@ -485,55 +435,32 @@ def create_interface():
485
  refresh_workers_btn = gr.Button("πŸ”„ Refresh List")
486
 
487
  # --- Event Handlers ---
488
- def on_tab_select(evt: gr.SelectData):
489
- return evt.index
490
-
491
  video_tabs.select(fn=on_tab_select, inputs=None, outputs=[selected_tab_index])
492
-
493
  def start_wrapper(tab_index, cam_src, vid_path):
494
  source = cam_src if tab_index == 0 else vid_path
495
  return "Please provide an input source." if source is None else attendance_system.start_processing(source)
496
-
497
- start_btn.click(
498
- fn=start_wrapper,
499
- inputs=[selected_tab_index, camera_source, video_file],
500
- outputs=[status_box]
501
- )
502
-
503
- stop_btn.click(
504
- fn=attendance_system.stop_processing,
505
- inputs=None,
506
- outputs=[status_box]
507
- )
508
 
509
- register_btn.click(
510
- fn=attendance_system.register_worker_manual,
511
- inputs=[register_image, register_name],
512
- outputs=[register_output, registered_workers_info]
513
- )
514
-
515
- refresh_workers_btn.click(
516
- fn=attendance_system.get_registered_workers_info,
517
- outputs=[registered_workers_info]
518
- )
519
 
520
  def update_ui_generator():
521
  while True:
522
  if attendance_system.error_message:
523
  yield None, attendance_system.error_message
524
- time.sleep(2)
525
- attendance_system.error_message = None
526
  continue
527
-
528
  if attendance_system.is_processing.is_set():
529
  frame, log_md = None, "\n".join(reversed(attendance_system.session_log)) or "Processing..."
530
  try:
531
  if not attendance_system.frame_queue.empty():
532
  frame = attendance_system.frame_queue.get_nowait()
533
- if frame is not None:
534
- frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
535
- except queue.Empty:
536
- pass
537
  yield frame, log_md
538
  else:
539
  if attendance_system.last_processed_frame is not None:
@@ -544,10 +471,7 @@ def create_interface():
544
  yield None, "System stopped. Go to 'Controls & Status' to start."
545
  time.sleep(0.1)
546
 
547
- demo.load(
548
- fn=update_ui_generator,
549
- outputs=[video_output, session_log_display]
550
- )
551
  return demo
552
 
553
  if __name__ == "__main__":
 
72
  self.processing_thread = None
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
77
+ self.final_log = None
78
+
79
+ # Recognition Parameters (KEY IMPROVEMENT)
80
+ # Using two thresholds for better accuracy.
81
+ # RECOGNITION_THRESHOLD: A strict value for a confident match.
82
+ # DUPLICATE_THRESHOLD: A looser value to catch variations (e.g., different angles)
83
+ # and prevent re-registering a known person.
84
+ self.RECOGNITION_THRESHOLD = 1.1 # For Facenet L2 distance, a value around 1.1 is a confident match.
85
+ self.DUPLICATE_THRESHOLD = 1.3 # Anything below this is likely the same person at a different angle.
86
 
87
  # Data Storage
88
  self.known_face_embeddings: List[np.ndarray] = []
 
91
  self.next_worker_id: int = 1
92
 
93
  # Session Tracking
 
 
94
  self.session_log: List[str] = []
95
+ # NEW: Set to track worker IDs that have been logged in the current session
96
+ # to prevent duplicate "Marked Present" messages in the UI.
97
+ self.session_logged_ids = set()
98
 
99
  # Initialize
100
  self.sf = connect_to_salesforce()
 
166
  image_array = np.array(image)
167
  DeepFace.analyze(img_path=image_array, actions=['emotion'], enforce_detection=True)
168
  embedding = DeepFace.represent(img_path=image_array, model_name='Facenet')[0]['embedding']
169
+
170
+ # Use the duplicate check to see if face already exists
171
  if self._is_duplicate_face(embedding):
172
+ return f"❌ Face matches an existing worker!", self.get_registered_workers_info()
173
 
174
  worker_id = f"W{self.next_worker_id:04d}"
175
  name = name.strip().title()
176
  self._add_worker_to_system(worker_id, name, embedding, image_array)
177
+ self.save_local_worker_data() # Save and then reload to ensure consistency
178
  self.load_worker_data()
179
  return f"βœ… {name} registered with ID: {worker_id}!", self.get_registered_workers_info()
180
  except ValueError:
 
185
  def _register_worker_auto(self, face_image: np.ndarray) -> Optional[Tuple[str, str]]:
186
  try:
187
  embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
188
+
189
+ # An unknown face should not match anyone, even with the looser threshold.
190
+ if self._is_duplicate_face(embedding):
191
+ return None
192
+
193
  worker_id = f"W{self.next_worker_id:04d}"
194
  worker_name = f"Unknown Worker {self.next_worker_id}"
195
+
196
  self._add_worker_to_system(worker_id, worker_name, embedding, face_image)
197
  self.save_local_worker_data()
198
+ self.load_worker_data() # Reload data to include the new worker immediately
199
+
 
200
  return worker_id, worker_name
201
  except Exception as e:
202
  logger.error(f"❌ Auto-registration error: {e}")
203
  return None
204
 
205
  def _add_worker_to_system(self, worker_id: str, name: str, embedding: List[float], image_array: np.ndarray):
206
+ # This function now just adds the data. In-memory lists will be updated by load_worker_data().
 
 
 
207
  face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
208
  face_pil.save(f"data/faces/{worker_id}.jpg")
209
  caption = self._get_image_caption(face_pil)
 
216
  except Exception as e:
217
  logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
218
 
219
+ def _is_duplicate_face(self, embedding: List[float]) -> bool:
220
+ """Checks if a face is too similar to an already registered one."""
221
  if not self.known_face_embeddings: return False
222
  distances = [np.linalg.norm(np.array(embedding) - known_embedding) for known_embedding in self.known_face_embeddings]
223
+ # Use the looser threshold to avoid re-registering known faces at different angles
224
+ return min(distances) < self.DUPLICATE_THRESHOLD
225
 
226
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
227
+ """Marks attendance in Salesforce if not already done today. Returns True if newly marked."""
228
  today_str = date.today().isoformat()
 
 
 
 
 
 
229
  if self._has_attended_today(worker_id, today_str):
230
+ return False # Already attended, no new action needed
231
+
 
232
  current_time = datetime.now()
233
  if self.sf:
234
  try:
235
+ self.sf.Attendance__c.create({'Worker_ID__c': worker_id, 'Name__c': worker_name, 'Date__c': today_str, 'Timestamp__c': current_time.isoformat(), 'Status__c': "Present"})
236
+ logger.info(f"Salesforce attendance marked for {worker_id}")
237
+ return True
 
 
 
 
238
  except Exception as e:
239
  logger.error(f"❌ Error saving attendance to Salesforce: {e}")
240
  return False
241
+ return True # Assume success if not connected to Salesforce
 
 
 
 
242
 
243
  def _has_attended_today(self, worker_id: str, today_str: str) -> bool:
244
+ """Checks Salesforce to see if an attendance record exists for the worker today."""
245
  if self.sf:
246
  try:
247
+ if self.sf.query(f"SELECT Id FROM Attendance__c WHERE Worker_ID__c = '{worker_id}' AND Date__c = {today_str}")['totalSize'] > 0:
248
+ return True
249
+ except Exception: pass # If query fails, assume not attended
 
250
  return False
251
 
252
  # --- Video Processing ---
253
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
254
+ """Main function to process a single video frame with improved accuracy logic."""
 
 
255
  try:
256
+ face_objs = DeepFace.extract_faces(img_path=frame, detector_backend='opencv', enforce_detection=False)
 
 
 
 
 
 
 
 
 
257
 
258
  for face_obj in face_objs:
259
+ if face_obj['confidence'] < 0.95: continue
 
 
 
 
260
 
261
  facial_area = face_obj['facial_area']
262
  x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
263
+ face_image = frame[y:y+h, x:x+w]
264
 
265
+ if face_image.size == 0: continue
 
266
 
267
+ # Default to Unknown (Red)
268
+ color, label_text = (0, 0, 255), "Unknown"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
+ try:
271
+ embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
 
 
 
 
 
 
 
272
 
273
+ if self.known_face_embeddings:
274
+ distances = [np.linalg.norm(np.array(embedding) - known) for known in self.known_face_embeddings]
275
+ min_dist = min(distances)
276
+ match_index = distances.index(min_dist)
277
+
278
+ # CASE 1: CONFIDENT MATCH (Recognized Worker)
279
+ if min_dist < self.RECOGNITION_THRESHOLD:
280
+ worker_id = self.known_face_ids[match_index]
281
+ worker_name = self.known_face_names[match_index]
282
+ color = (0, 255, 0) # Green
283
+ label_text = f"{worker_name} ({worker_id})"
284
+
285
+ # Log attendance only ONCE per session for a clean UI
286
+ if worker_id not in self.session_logged_ids:
287
+ if self.mark_attendance(worker_id, worker_name):
288
+ log_msg = f"βœ… [{datetime.now().strftime('%H:%M:%S')}] Marked Present: {worker_name} ({worker_id})"
289
+ self.session_log.append(log_msg)
290
+ self.session_logged_ids.add(worker_id)
291
+
292
+ # CASE 2: UNKNOWN FACE (Not a confident match)
293
+ # We also check if it's a potential duplicate before trying to register
294
+ elif not self._is_duplicate_face(embedding):
295
+ # Attempt to auto-register this new face
296
+ new_worker = self._register_worker_auto(face_image)
297
+ if new_worker:
298
+ worker_id, worker_name = new_worker
299
+ color = (0, 165, 255) # Orange for new worker
300
+ label_text = f"{worker_name} ({worker_id})"
301
+
302
+ if worker_id not in self.session_logged_ids:
303
+ log_msg = f"πŸ†• [{datetime.now().strftime('%H:%M:%S')}] Auto-registered: {worker_name} ({worker_id})"
304
+ self.session_log.append(log_msg)
305
+ self.mark_attendance(worker_id, worker_name)
306
+ self.session_logged_ids.add(worker_id)
307
+ # else: Face is in the "gray area" (between 1.1 and 1.3). Treat as "Unknown" but don't register.
308
+
309
+ except Exception as e:
310
+ logger.debug(f"Could not process a face, likely too small or blurry. Error: {e}")
311
+
312
+ # Draw bounding box and label
313
  cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
314
+ cv2.putText(frame, label_text, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
315
+
316
  return frame
317
  except Exception as e:
318
+ logger.error(f"CRITICAL ERROR in process_frame: {e}")
319
+ return frame # Return original frame on error
320
 
321
  def _processing_loop(self, source):
322
  video_capture = cv2.VideoCapture(source)
 
326
  self.is_processing.clear()
327
  return
328
 
 
 
 
 
329
  while self.is_processing.is_set():
330
  ret, frame = video_capture.read()
331
+ if not ret: break
332
+
 
333
  processed_frame = self.process_frame(frame)
334
+ if not self.frame_queue.full(): self.frame_queue.put(processed_frame)
335
 
336
+ self.last_processed_frame = processed_frame # Continuously update last frame
 
 
 
337
  time.sleep(0.05)
338
 
339
+ self.final_log = self.session_log.copy() # Save the final log
340
  video_capture.release()
341
  self.is_processing.clear()
342
 
343
  def start_processing(self, source) -> str:
344
+ if self.is_processing.is_set(): return "⚠️ Processing is already active."
 
 
345
  # Reset states for the new session
346
  self.session_log.clear()
347
+ self.session_logged_ids.clear() # IMPORTANT: Clear the session log tracker
348
+ self.error_message = None; self.last_processed_frame = None; self.final_log = None
 
 
 
349
  self.is_processing.set()
350
+ self.processing_thread = threading.Thread(target=self._processing_loop, args=(source,)); self.processing_thread.daemon = True
 
 
 
 
351
  self.processing_thread.start()
352
  return f"βœ… Started processing..."
353
 
354
  def stop_processing(self) -> str:
355
+ # Reset states when stopping manually
356
+ self.is_processing.clear(); self.error_message = None
357
+ self.last_processed_frame = None; self.final_log = None
 
358
  return "βœ… Processing stopped by user."
359
 
360
  # --- Helper & Reporting ---
361
  def _get_image_caption(self, image: Image.Image) -> str:
362
+ if not HF_API_TOKEN: return "Hugging Face API token not configured."
 
363
  try:
364
  buffered = BytesIO()
365
  image.save(buffered, format="JPEG")
 
374
  return "Caption generation failed."
375
 
376
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
377
+ if not self.sf: return None
 
378
  try:
379
  buffered = BytesIO()
380
  image.save(buffered, format="JPEG")
381
  encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
382
+ cv = self.sf.ContentVersion.create({'Title': f'Image_{worker_id}', 'PathOnClient': f'{worker_id}.jpg', 'VersionData': encoded_image, 'FirstPublishLocationId': record_id})
 
 
 
 
 
383
  return f"/{cv['id']}" # Relative URL
384
  except Exception as e:
385
  logger.error(f"Salesforce image upload error: {e}")
386
  return None
387
 
388
  def get_registered_workers_info(self) -> str:
389
+ if not self.sf: return "❌ Salesforce not connected."
 
390
  try:
391
  records = self.sf.query_all("SELECT Name, Worker_ID__c FROM Worker__c ORDER BY Name")['records']
392
+ if not records: return "No workers registered."
393
+ return f"**πŸ‘₯ Registered Workers ({len(records)})**\n" + "\n".join([f"- **{w['Name']}** (ID: {w['Worker_ID__c']})" for w in records])
394
+ except Exception as e: return f"Error: {e}"
 
 
 
 
395
 
396
  # --- GRADIO UI ---
397
  attendance_system = AttendanceSystem()
 
398
  def create_interface():
399
  with gr.Blocks(theme=gr.themes.Soft(), title="Attendance System") as demo:
400
  gr.Markdown("# 🎯 Advanced Face Recognition Attendance System")
 
435
  refresh_workers_btn = gr.Button("πŸ”„ Refresh List")
436
 
437
  # --- Event Handlers ---
438
+ def on_tab_select(evt: gr.SelectData): return evt.index
 
 
439
  video_tabs.select(fn=on_tab_select, inputs=None, outputs=[selected_tab_index])
440
+
441
  def start_wrapper(tab_index, cam_src, vid_path):
442
  source = cam_src if tab_index == 0 else vid_path
443
  return "Please provide an input source." if source is None else attendance_system.start_processing(source)
 
 
 
 
 
 
 
 
 
 
 
 
444
 
445
+ start_btn.click(fn=start_wrapper, inputs=[selected_tab_index, camera_source, video_file], outputs=[status_box])
446
+ stop_btn.click(fn=attendance_system.stop_processing, inputs=None, outputs=[status_box])
447
+ register_btn.click(fn=attendance_system.register_worker_manual, inputs=[register_image, register_name], outputs=[register_output, registered_workers_info])
448
+ refresh_workers_btn.click(fn=attendance_system.get_registered_workers_info, outputs=[registered_workers_info])
 
 
 
 
 
 
449
 
450
  def update_ui_generator():
451
  while True:
452
  if attendance_system.error_message:
453
  yield None, attendance_system.error_message
454
+ time.sleep(2); attendance_system.error_message = None
 
455
  continue
456
+
457
  if attendance_system.is_processing.is_set():
458
  frame, log_md = None, "\n".join(reversed(attendance_system.session_log)) or "Processing..."
459
  try:
460
  if not attendance_system.frame_queue.empty():
461
  frame = attendance_system.frame_queue.get_nowait()
462
+ if frame is not None: frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
463
+ except queue.Empty: pass
 
 
464
  yield frame, log_md
465
  else:
466
  if attendance_system.last_processed_frame is not None:
 
471
  yield None, "System stopped. Go to 'Controls & Status' to start."
472
  time.sleep(0.1)
473
 
474
+ demo.load(fn=update_ui_generator, outputs=[video_output, session_log_display])
 
 
 
475
  return demo
476
 
477
  if __name__ == "__main__":