PrashanthB461 commited on
Commit
19078d9
Β·
verified Β·
1 Parent(s): e4119ba

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +214 -205
app.py CHANGED
@@ -11,7 +11,7 @@ import threading
11
  import time
12
  from datetime import datetime, date
13
  from io import BytesIO
14
- from typing import Tuple, Optional, List, Dict
15
  import pickle
16
 
17
  # Third-Party Imports
@@ -47,6 +47,16 @@ SF_CREDENTIALS = {
47
  "domain": os.getenv("SF_DOMAIN", "login")
48
  }
49
 
 
 
 
 
 
 
 
 
 
 
50
  # --- SALESFORCE CONNECTION ---
51
 
52
  @retry(stop_max_attempt_number=3, wait_fixed=2000)
@@ -83,15 +93,12 @@ class AttendanceSystem:
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()
@@ -117,7 +124,8 @@ class AttendanceSystem:
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):
@@ -128,7 +136,7 @@ class AttendanceSystem:
128
  self.known_face_ids = temp_ids
129
  self.next_worker_id = max_id + 1
130
  self.save_local_worker_data()
131
- logger.info(f"βœ… Loaded {len(self.known_face_ids)} workers from Salesforce.")
132
  except Exception as e:
133
  logger.error(f"❌ Error loading from Salesforce: {e}. Attempting local load.")
134
  self._load_local_worker_data()
@@ -144,7 +152,7 @@ class AttendanceSystem:
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
 
@@ -161,41 +169,37 @@ class AttendanceSystem:
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)
@@ -205,13 +209,16 @@ class AttendanceSystem:
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})
@@ -221,209 +228,196 @@ class AttendanceSystem:
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
- logger.info(log_msg)
260
  return True
261
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  # --- Video Processing ---
263
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
264
- """
265
- Process a single video frame with optimizations for speed and accuracy.
266
- """
267
  try:
268
- # Skip frames for faster processing
269
- self.frame_counter += 1
270
- if self.frame_counter % (self.frame_skip + 1) != 0:
271
- return frame
272
-
273
- # Resize frame for faster processing (keeping aspect ratio)
274
- height, width = frame.shape[:2]
275
- new_width = 360 # Reduced resolution for speed
276
- new_height = int((new_width / width) * height)
277
- small_frame = cv2.resize(frame, (new_width, new_height))
278
-
279
- # Use OpenCV detector with relaxed settings
280
- face_objs = DeepFace.extract_faces(
281
- img_path=small_frame,
282
- detector_backend='opencv',
283
- enforce_detection=False,
284
- align=True
285
- )
286
-
287
- if not face_objs:
288
- logger.warning(f"No faces detected in frame at {datetime.now().strftime('%H:%M:%S')}")
289
- return frame
290
 
 
291
  for face_obj in face_objs:
292
- confidence = face_obj.get('confidence', 0.0)
293
- if confidence < 0.85: # Further lowered threshold to catch more faces
 
 
294
  continue
295
 
296
  facial_area = face_obj['facial_area']
297
  x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
298
-
299
- # Scale coordinates back to original frame size
300
- x = int(x * width / new_width)
301
- y = int(y * height / new_height)
302
- w = int(w * width / new_width)
303
- h = int(h * height / new_height)
304
-
305
  face_image = frame[y:y+h, x:x+w]
306
- if face_image.size == 0:
307
- continue
308
 
309
- # Only process faces that haven't been detected recently
310
- current_time = time.time()
311
- face_key = f"{x}_{y}_{w}_{h}"
312
- if face_key in self.last_detected_faces and (current_time - self.last_detected_faces[face_key] < 2.0):
313
- continue
314
- self.last_detected_faces[face_key] = current_time
315
-
316
- embedding = DeepFace.represent(
317
- img_path=face_image,
318
- model_name='Facenet',
319
- enforce_detection=False,
320
- align=True
321
- )[0]['embedding']
322
 
323
  if not self.known_face_embeddings:
324
- logger.info("No known face embeddings loaded.")
 
 
325
  continue
326
 
 
327
  distances = [np.linalg.norm(np.array(embedding) - known) for known in self.known_face_embeddings]
328
- min_dist = min(distances) if distances else float('inf')
329
- match_index = distances.index(min_dist) if min_dist < 10.0 else -1
 
 
 
 
 
330
 
331
- color, worker_id, worker_name = (0, 0, 255), None, "Unknown"
332
-
333
  if match_index != -1:
 
334
  worker_id = self.known_face_ids[match_index]
335
  worker_name = self.known_face_names[match_index]
336
- color = (0, 255, 0) # Green for known workers
337
- self.mark_attendance(worker_id, worker_name)
 
338
  else:
339
- # Attempt auto-registration for high-quality faces
340
- analysis = DeepFace.analyze(img_path=face_image, actions=['emotion'], enforce_detection=False)
341
- if analysis[0].get('face_confidence', 0.0) >= 0.85: # Lowered threshold
342
- color = (0, 165, 255) # Orange for potential new worker
343
  new_worker = self._register_worker_auto(face_image)
344
  if new_worker:
345
  worker_id, worker_name = new_worker
346
- self.mark_attendance(worker_id, worker_name)
347
-
348
- # Draw rectangle and label
 
 
349
  label = f"{worker_name}" + (f" ({worker_id})" if worker_id else "")
350
  cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
351
  cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
352
 
353
  return frame
354
  except Exception as e:
 
355
  logger.error(f"ERROR in process_frame: {e}")
356
  return frame
357
 
358
  def _processing_loop(self, source):
359
  video_capture = cv2.VideoCapture(source)
360
  if not video_capture.isOpened():
361
- 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."
362
  self.error_message = err_msg
363
  self.is_processing.clear()
364
  return
365
-
366
- # Set higher FPS if possible (for live camera)
367
- if isinstance(source, int):
368
- video_capture.set(cv2.CAP_PROP_FPS, 30)
369
-
 
370
  while self.is_processing.is_set():
371
  ret, frame = video_capture.read()
372
- if not ret:
373
- break
374
-
375
- processed_frame = self.process_frame(frame)
376
-
377
- if processed_frame is not None and not self.frame_queue.full():
 
 
 
 
 
 
 
 
378
  self.frame_queue.put(processed_frame)
379
-
380
- self.last_processed_frame = processed_frame
381
- time.sleep(0.01) # Reduced sleep for faster processing
382
 
 
 
 
 
383
  self.final_log = self.session_log.copy()
384
  video_capture.release()
385
  self.is_processing.clear()
386
 
387
  def start_processing(self, source) -> str:
388
- if self.is_processing.is_set():
389
- return "⚠️ Processing is already active."
390
-
391
- # Reset states for new session
392
  self.session_log.clear()
393
  self.last_recognition_time.clear()
394
- self.today_attendance.clear()
395
- self.last_detected_faces.clear()
396
  self.error_message = None
397
  self.last_processed_frame = None
398
  self.final_log = None
399
- self.frame_counter = 0
400
-
 
 
401
  self.is_processing.set()
402
- self.processing_thread = threading.Thread(
403
- target=self._processing_loop,
404
- args=(source,),
405
- daemon=True
406
- )
407
  self.processing_thread.start()
408
  return f"βœ… Started processing..."
409
 
410
  def stop_processing(self) -> str:
 
 
411
  self.is_processing.clear()
412
- self.error_message = None
413
- self.last_processed_frame = None
414
- self.final_log = None
415
  return "βœ… Processing stopped by user."
416
 
417
  # --- Helper & Reporting ---
418
  def _get_image_caption(self, image: Image.Image) -> str:
419
- if not HF_API_TOKEN:
420
- return "Hugging Face API token not configured."
421
  try:
422
  buffered = BytesIO()
423
  image.save(buffered, format="JPEG")
424
  img_data = buffered.getvalue()
425
  headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
426
- response = requests.post(HF_API_URL, headers=headers, data=img_data)
427
  response.raise_for_status()
428
  result = response.json()
429
  return result[0].get("generated_text", "No caption found.")
@@ -432,35 +426,34 @@ class AttendanceSystem:
432
  return "Caption generation failed."
433
 
434
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
435
- if not self.sf:
436
- return None
437
  try:
438
  buffered = BytesIO()
439
  image.save(buffered, format="JPEG")
440
  encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
 
441
  cv = self.sf.ContentVersion.create({
442
  'Title': f'Image_{worker_id}',
443
  'PathOnClient': f'{worker_id}.jpg',
444
  'VersionData': encoded_image,
445
- 'FirstPublishLocationId': record_id
446
  })
447
- return f"/{cv['id']}" # Relative URL
 
 
 
 
448
  except Exception as e:
449
  logger.error(f"Salesforce image upload error: {e}")
450
  return None
451
 
452
  def get_registered_workers_info(self) -> str:
453
- if not self.sf:
454
- return "❌ Salesforce not connected."
455
- try:
456
- records = self.sf.query_all("SELECT Name, Worker_ID__c FROM Worker__c ORDER BY Name")['records']
457
- if not records:
458
- return "No workers registered."
459
- return f"**πŸ‘₯ Registered Workers ({len(records)})**\n" + "\n".join(
460
- [f"- **{w['Name']}** (ID: {w['Worker_ID__c']})" for w in records]
461
- )
462
- except Exception as e:
463
- return f"Error: {e}"
464
 
465
  # --- GRADIO UI ---
466
  attendance_system = AttendanceSystem()
@@ -476,7 +469,7 @@ def create_interface():
476
  selected_tab_index = gr.Number(value=0, visible=False)
477
  with gr.Tabs() as video_tabs:
478
  with gr.Tab("Live Camera", id=0):
479
- camera_source = gr.Number(label="Camera Source", value=0, precision=0)
480
  with gr.Tab("Upload Video", id=1):
481
  video_file = gr.Video(label="Upload Video File", sources=["upload"])
482
  with gr.Column(scale=1):
@@ -484,88 +477,104 @@ def create_interface():
484
  stop_btn = gr.Button("⏹️ Stop Processing", variant="stop")
485
  status_box = gr.Textbox(label="Status", interactive=False, value="System Ready.")
486
  gr.Markdown("### 2. View Results in the 'Output & Log' Tab")
487
- gr.Markdown("**🎨 Color Coding:** <font color='green'>Green</font> = Known, <font color='orange'>Orange</font> = New, <font color='red'>Red</font> = Unknown")
488
 
489
  with gr.Tab("πŸ“Š Output & Log"):
490
  with gr.Row():
491
  with gr.Column(scale=2):
492
- video_output = gr.Image(label="Recognition Output", interactive=False)
493
  with gr.Column(scale=1):
494
  session_log_display = gr.Markdown(label="πŸ“‹ Session Log", value="System is ready.")
495
 
496
  with gr.Tab("πŸ‘€ Worker Management"):
497
  with gr.Row():
498
  with gr.Column():
499
- register_image = gr.Image(label="Upload Worker's Photo", type="pil")
 
500
  register_name = gr.Textbox(label="Worker's Full Name")
501
  register_btn = gr.Button("Register Worker", variant="primary")
502
  register_output = gr.Textbox(label="Registration Status", interactive=False)
503
  with gr.Column():
 
504
  registered_workers_info = gr.Markdown(value=attendance_system.get_registered_workers_info())
505
  refresh_workers_btn = gr.Button("πŸ”„ Refresh List")
506
 
507
  # --- Event Handlers ---
508
- def on_tab_select(evt: gr.SelectData):
509
- return evt.index
510
-
511
  video_tabs.select(fn=on_tab_select, inputs=None, outputs=[selected_tab_index])
512
-
513
  def start_wrapper(tab_index, cam_src, vid_path):
514
- source = cam_src if tab_index == 0 else vid_path
515
  return "Please provide an input source." if source is None else attendance_system.start_processing(source)
516
-
517
- start_btn.click(
518
- fn=start_wrapper,
519
- inputs=[selected_tab_index, camera_source, video_file],
520
- outputs=[status_box]
521
- )
522
 
523
- stop_btn.click(
524
- fn=attendance_system.stop_processing,
525
- inputs=None,
526
- outputs=[status_box]
527
- )
528
 
529
  register_btn.click(
530
- fn=attendance_system.register_worker_manual,
531
- inputs=[register_image, register_name],
532
  outputs=[register_output, registered_workers_info]
533
  )
534
-
535
  refresh_workers_btn.click(
536
- fn=attendance_system.get_registered_workers_info,
537
  outputs=[registered_workers_info]
538
  )
539
 
540
  def update_ui_generator():
541
  while True:
 
542
  if attendance_system.error_message:
543
- yield None, attendance_system.error_message
 
 
544
  time.sleep(2)
545
- attendance_system.error_message = None
546
  continue
547
-
 
 
 
548
  if attendance_system.is_processing.is_set():
549
- frame = None
550
  try:
 
551
  if not attendance_system.frame_queue.empty():
552
- frame = attendance_system.frame_queue.get_nowait()
553
- if frame is not None:
554
- frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
555
  except queue.Empty:
556
- pass
557
- log_md = "\n".join(reversed(attendance_system.session_log[-20:])) or "Processing..."
558
- yield frame, log_md
559
  else:
 
 
560
  if attendance_system.last_processed_frame is not None:
561
  final_frame = cv2.cvtColor(attendance_system.last_processed_frame, cv2.COLOR_BGR2RGB)
562
- final_log_md = "\n".join(reversed(attendance_system.final_log[-20:])) or "Processing complete."
563
- yield final_frame, final_log_md
 
 
564
  else:
565
- yield None, "System stopped. Go to 'Controls & Status' to start."
566
- time.sleep(0.03) # Faster UI updates
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
567
 
568
- demo.load(fn=update_ui_generator, outputs=[video_output, session_log_display])
569
  return demo
570
 
571
  if __name__ == "__main__":
 
11
  import time
12
  from datetime import datetime, date
13
  from io import BytesIO
14
+ from typing import Tuple, Optional, List
15
  import pickle
16
 
17
  # Third-Party Imports
 
47
  "domain": os.getenv("SF_DOMAIN", "login")
48
  }
49
 
50
+ # --- MODIFICATION: Added configuration for performance and accuracy ---
51
+ # Defines how many frames to skip between face analysis. Higher value = faster but less frequent detection.
52
+ FRAME_PROCESS_RATE = 3
53
+ # Stricter distance threshold for Facenet. Faces with distance > this are considered different people.
54
+ # Default DeepFace threshold is ~10, which is too loose. A value between 0.4 and 0.7 is recommended for Facenet.
55
+ FACE_MATCH_THRESHOLD = 0.6
56
+ # Minimum confidence score from the face detector to consider a face for auto-registration.
57
+ AUTO_REGISTER_CONFIDENCE = 0.99
58
+ # --- END MODIFICATION ---
59
+
60
  # --- SALESFORCE CONNECTION ---
61
 
62
  @retry(stop_max_attempt_number=3, wait_fixed=2000)
 
93
  self.next_worker_id: int = 1
94
 
95
  # Session Tracking
96
+ self.last_recognition_time = {}
97
+ self.recognition_cooldown = 5
98
  self.session_log: List[str] = []
99
+ # --- MODIFICATION: Set to track workers already marked present in the current session for unique UI logs ---
100
+ self.session_attended_ids = set()
101
+ # --- END MODIFICATION ---
 
 
 
102
 
103
  # Initialize
104
  self.sf = connect_to_salesforce()
 
124
  temp_names.append(worker['Name'])
125
  temp_ids.append(worker['Worker_ID__c'])
126
  try:
127
+ # Robustly extract number from Worker ID like "W0042"
128
+ worker_num = int(''.join(filter(str.isdigit, worker['Worker_ID__c'])))
129
  if worker_num > max_id:
130
  max_id = worker_num
131
  except (ValueError, TypeError):
 
136
  self.known_face_ids = temp_ids
137
  self.next_worker_id = max_id + 1
138
  self.save_local_worker_data()
139
+ logger.info(f"βœ… Loaded {len(self.known_face_ids)} workers from Salesforce. Next ID: {self.next_worker_id}")
140
  except Exception as e:
141
  logger.error(f"❌ Error loading from Salesforce: {e}. Attempting local load.")
142
  self._load_local_worker_data()
 
152
  self.known_face_names = data.get("names", [])
153
  self.known_face_ids = data.get("ids", [])
154
  self.next_worker_id = data.get("next_id", 1)
155
+ logger.info(f"βœ… Loaded {len(self.known_face_ids)} workers from local cache. Next ID: {self.next_worker_id}")
156
  except Exception as e:
157
  logger.error(f"❌ Error loading local data: {e}")
158
 
 
169
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
170
  try:
171
  image_array = np.array(image)
172
+ # Ensure a face exists before proceeding
173
+ DeepFace.analyze(img_path=image_array, actions=['emotion'], enforce_detection=True)
174
+ embedding = DeepFace.represent(img_path=image_array, model_name='Facenet', enforce_detection=False)[0]['embedding']
 
175
 
 
176
  if self._is_duplicate_face(embedding):
177
  return f"❌ Face matches an existing worker!", self.get_registered_workers_info()
178
 
179
+ # --- MODIFICATION: Ensure unique ID assignment is robust ---
180
  worker_id = f"W{self.next_worker_id:04d}"
181
  name = name.strip().title()
182
+
183
  self._add_worker_to_system(worker_id, name, embedding, image_array)
184
  self.save_local_worker_data()
185
+ # self.load_worker_data() # Not strictly necessary, can rely on local update
186
  return f"βœ… {name} registered with ID: {worker_id}!", self.get_registered_workers_info()
187
  except ValueError:
188
  return "❌ No face detected in the image!", self.get_registered_workers_info()
189
  except Exception as e:
190
+ logger.error(f"Manual registration error: {e}")
191
  return f"❌ Registration error: {e}", self.get_registered_workers_info()
192
 
193
  def _register_worker_auto(self, face_image: np.ndarray) -> Optional[Tuple[str, str]]:
194
  try:
 
 
 
 
 
195
  embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
196
+ if self._is_duplicate_face(embedding): return None
197
+
 
198
  worker_id = f"W{self.next_worker_id:04d}"
199
+ worker_name = f"Unknown Worker {self.next_worker_id}"
200
  self._add_worker_to_system(worker_id, worker_name, embedding, face_image)
201
  self.save_local_worker_data()
202
+
203
  log_msg = f"πŸ†• [{datetime.now().strftime('%H:%M:%S')}] Auto-registered: {worker_name} ({worker_id})"
204
  self.session_log.append(log_msg)
205
  logger.info(log_msg)
 
209
  return None
210
 
211
  def _add_worker_to_system(self, worker_id: str, name: str, embedding: List[float], image_array: np.ndarray):
212
+ """Adds worker to local lists, increments ID, saves image, and syncs to Salesforce."""
213
  self.known_face_embeddings.append(np.array(embedding))
214
  self.known_face_names.append(name)
215
  self.known_face_ids.append(worker_id)
216
+ self.next_worker_id += 1 # This ensures the next ID is always unique
217
+
218
  face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
219
  face_pil.save(f"data/faces/{worker_id}.jpg")
220
  caption = self._get_image_caption(face_pil)
221
+
222
  if self.sf:
223
  try:
224
  worker_record = self.sf.Worker__c.create({'Name': name, 'Worker_ID__c': worker_id, 'Face_Embedding__c': json.dumps(embedding), 'Image_Caption__c': caption})
 
228
  except Exception as e:
229
  logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
230
 
231
+ def _is_duplicate_face(self, embedding: List[float]) -> bool:
232
+ """Checks if a new face embedding is too close to any known embeddings."""
233
  if not self.known_face_embeddings: return False
234
+ new_embedding = np.array(embedding)
235
+ # --- MODIFICATION: Using stricter threshold for duplicate check ---
236
+ distances = [np.linalg.norm(new_embedding - known_embedding) for known_embedding in self.known_face_embeddings]
237
+ return min(distances) < FACE_MATCH_THRESHOLD
238
+ # --- END MODIFICATION ---
239
 
240
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
241
+ """Marks attendance if not already marked in this session. Returns True if newly marked."""
242
+ # --- MODIFICATION: Check session log first for immediate UI uniqueness ---
243
+ if worker_id in self.session_attended_ids:
244
+ return False # Already logged in this session
245
+ # --- END MODIFICATION ---
246
+
247
  today_str = date.today().isoformat()
248
+ if self._has_attended_today_in_sf(worker_id, today_str):
249
+ self.session_attended_ids.add(worker_id) # Add to session log to prevent re-logging
 
 
 
 
 
 
250
  return False
251
+
252
+ current_time = datetime.now()
 
253
  if self.sf:
254
  try:
255
+ 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"})
 
 
 
 
 
 
256
  except Exception as e:
257
  logger.error(f"❌ Error saving attendance to Salesforce: {e}")
258
+
259
+ log_msg = f"βœ… [{current_time.strftime('%H:%M:%S')}] Marked Present: {worker_name} ({worker_id})"
 
 
260
  self.session_log.append(log_msg)
261
+ self.session_attended_ids.add(worker_id) # Mark as attended for this session
262
  return True
263
 
264
+ def _has_attended_today_in_sf(self, worker_id: str, today_str: str) -> bool:
265
+ """Checks Salesforce to see if an attendance record for today already exists."""
266
+ if self.sf:
267
+ try:
268
+ query = f"SELECT Id FROM Attendance__c WHERE Worker_ID__c = '{worker_id}' AND Date__c = {today_str}"
269
+ if self.sf.query(query)['totalSize'] > 0:
270
+ logger.info(f"Worker {worker_id} already marked present today in Salesforce.")
271
+ return True
272
+ except Exception as e:
273
+ logger.error(f"Error checking SF for attendance: {e}")
274
+ return False
275
+
276
  # --- Video Processing ---
277
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
278
+ """Main function to process a single video frame."""
 
 
279
  try:
280
+ # Using 'opencv' backend which is generally fastest
281
+ face_objs = DeepFace.extract_faces(img_path=frame, detector_backend='opencv', enforce_detection=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
+ # Iterate through each face found in the frame
284
  for face_obj in face_objs:
285
+ confidence = face_obj['confidence']
286
+
287
+ # Only process faces with a reasonable confidence score
288
+ if confidence < 0.95:
289
  continue
290
 
291
  facial_area = face_obj['facial_area']
292
  x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
 
 
 
 
 
 
 
293
  face_image = frame[y:y+h, x:x+w]
294
+
295
+ if face_image.size == 0: continue
296
 
297
+ # Get the embedding for the detected face
298
+ embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
 
 
 
 
 
 
 
 
 
 
 
299
 
300
  if not self.known_face_embeddings:
301
+ # If no workers are registered yet, attempt to register this one
302
+ if confidence > AUTO_REGISTER_CONFIDENCE:
303
+ self._register_worker_auto(face_image)
304
  continue
305
 
306
+ # Compare the face to all known faces
307
  distances = [np.linalg.norm(np.array(embedding) - known) for known in self.known_face_embeddings]
308
+ min_dist = min(distances)
309
+ # --- MODIFICATION: Using the stricter threshold for matching ---
310
+ match_index = distances.index(min_dist) if min_dist < FACE_MATCH_THRESHOLD else -1
311
+ # --- END MODIFICATION ---
312
+
313
+ worker_id, worker_name = None, "Unknown"
314
+ color = (0, 0, 255) # Red for Unknown
315
 
 
 
316
  if match_index != -1:
317
+ # --- KNOWN FACE FOUND ---
318
  worker_id = self.known_face_ids[match_index]
319
  worker_name = self.known_face_names[match_index]
320
+ color = (0, 255, 0) # Green for Known/Present
321
+ if self.mark_attendance(worker_id, worker_name):
322
+ self.last_recognition_time[worker_id] = time.time()
323
  else:
324
+ # --- UNKNOWN FACE ---
325
+ color = (0, 165, 255) # Orange for newly registered
326
+ # --- MODIFICATION: Only auto-register very clear faces ---
327
+ if confidence > AUTO_REGISTER_CONFIDENCE:
328
  new_worker = self._register_worker_auto(face_image)
329
  if new_worker:
330
  worker_id, worker_name = new_worker
331
+ if self.mark_attendance(worker_id, worker_name):
332
+ self.last_recognition_time[worker_id] = time.time()
333
+ # --- END MODIFICATION ---
334
+
335
+ # Draw bounding box and label on the frame
336
  label = f"{worker_name}" + (f" ({worker_id})" if worker_id else "")
337
  cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
338
  cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
339
 
340
  return frame
341
  except Exception as e:
342
+ # Log error but don't crash the loop
343
  logger.error(f"ERROR in process_frame: {e}")
344
  return frame
345
 
346
  def _processing_loop(self, source):
347
  video_capture = cv2.VideoCapture(source)
348
  if not video_capture.isOpened():
349
+ err_msg = f"❌ **Error:** Could not open video source. Please check the file path or camera index."
350
  self.error_message = err_msg
351
  self.is_processing.clear()
352
  return
353
+
354
+ # --- MODIFICATION: Frame skipping for performance ---
355
+ frame_count = 0
356
+ last_annotated_frame = None
357
+ # --- END MODIFICATION ---
358
+
359
  while self.is_processing.is_set():
360
  ret, frame = video_capture.read()
361
+ if not ret: break
362
+
363
+ # --- MODIFICATION: Implement frame skipping ---
364
+ frame_count += 1
365
+ if frame_count % FRAME_PROCESS_RATE == 0:
366
+ # Process this frame fully
367
+ processed_frame = self.process_frame(frame)
368
+ last_annotated_frame = processed_frame.copy() # Save the annotated frame
369
+ else:
370
+ # For skipped frames, just use the last annotated frame to keep the UI responsive
371
+ processed_frame = last_annotated_frame if last_annotated_frame is not None else frame
372
+ # --- END MODIFICATION ---
373
+
374
+ if not self.frame_queue.full():
375
  self.frame_queue.put(processed_frame)
 
 
 
376
 
377
+ # Continuously update last frame to show at the end
378
+ self.last_processed_frame = processed_frame
379
+ time.sleep(0.01) # Small sleep to yield CPU
380
+
381
  self.final_log = self.session_log.copy()
382
  video_capture.release()
383
  self.is_processing.clear()
384
 
385
  def start_processing(self, source) -> str:
386
+ if self.is_processing.is_set(): return "⚠️ Processing is already active."
387
+ # Reset states for the new session
 
 
388
  self.session_log.clear()
389
  self.last_recognition_time.clear()
 
 
390
  self.error_message = None
391
  self.last_processed_frame = None
392
  self.final_log = None
393
+ # --- MODIFICATION: Clear the session attendance tracker ---
394
+ self.session_attended_ids.clear()
395
+ # --- END MODIFICATION ---
396
+
397
  self.is_processing.set()
398
+ self.processing_thread = threading.Thread(target=self._processing_loop, args=(source,))
399
+ self.processing_thread.daemon = True
 
 
 
400
  self.processing_thread.start()
401
  return f"βœ… Started processing..."
402
 
403
  def stop_processing(self) -> str:
404
+ if not self.is_processing.is_set():
405
+ return "⚠️ Processing is not currently active."
406
  self.is_processing.clear()
407
+ # Give the thread a moment to finish
408
+ if self.processing_thread:
409
+ self.processing_thread.join(timeout=2)
410
  return "βœ… Processing stopped by user."
411
 
412
  # --- Helper & Reporting ---
413
  def _get_image_caption(self, image: Image.Image) -> str:
414
+ if not HF_API_TOKEN: return "Hugging Face API token not configured."
 
415
  try:
416
  buffered = BytesIO()
417
  image.save(buffered, format="JPEG")
418
  img_data = buffered.getvalue()
419
  headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
420
+ response = requests.post(HF_API_URL, headers=headers, data=img_data, timeout=10)
421
  response.raise_for_status()
422
  result = response.json()
423
  return result[0].get("generated_text", "No caption found.")
 
426
  return "Caption generation failed."
427
 
428
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
429
+ if not self.sf: return None
 
430
  try:
431
  buffered = BytesIO()
432
  image.save(buffered, format="JPEG")
433
  encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
434
+ # Create a ContentVersion (File)
435
  cv = self.sf.ContentVersion.create({
436
  'Title': f'Image_{worker_id}',
437
  'PathOnClient': f'{worker_id}.jpg',
438
  'VersionData': encoded_image,
439
+ 'FirstPublishLocationId': record_id # Link to the Worker__c record
440
  })
441
+ # To get a usable URL, you query the ContentDocumentLink
442
+ content_doc_link = self.sf.query(f"SELECT ContentDocumentId FROM ContentDocumentLink WHERE LinkedEntityId = '{record_id}'")['records'][0]
443
+ content_doc_id = content_doc_link['ContentDocumentId']
444
+ # This relative URL can be used within Salesforce
445
+ return f"/sfc/servlet.shepherd/document/download/{content_doc_id}"
446
  except Exception as e:
447
  logger.error(f"Salesforce image upload error: {e}")
448
  return None
449
 
450
  def get_registered_workers_info(self) -> str:
451
+ # Refresh local data from Salesforce before displaying
452
+ self.load_worker_data()
453
+ if not self.known_face_ids: return "No workers registered."
454
+
455
+ info_list = [f"- **{name}** (ID: {id})" for name, id in sorted(zip(self.known_face_names, self.known_face_ids))]
456
+ return f"**πŸ‘₯ Registered Workers ({len(info_list)})**\n" + "\n".join(info_list)
 
 
 
 
 
457
 
458
  # --- GRADIO UI ---
459
  attendance_system = AttendanceSystem()
 
469
  selected_tab_index = gr.Number(value=0, visible=False)
470
  with gr.Tabs() as video_tabs:
471
  with gr.Tab("Live Camera", id=0):
472
+ camera_source = gr.Number(label="Camera Source Index", value=0, precision=0)
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):
 
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/Present, <font color='orange'>Orange</font> = New/Registered, <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, type="pil")
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
+ gr.Markdown("### Register New Worker")
493
+ register_image = gr.Image(label="Upload Worker's Photo", type="pil", sources=["upload"])
494
  register_name = gr.Textbox(label="Worker's Full Name")
495
  register_btn = gr.Button("Register Worker", variant="primary")
496
  register_output = gr.Textbox(label="Registration Status", interactive=False)
497
  with gr.Column():
498
+ gr.Markdown("### Current Worker Roster")
499
  registered_workers_info = gr.Markdown(value=attendance_system.get_registered_workers_info())
500
  refresh_workers_btn = gr.Button("πŸ”„ Refresh List")
501
 
502
  # --- Event Handlers ---
503
+ def on_tab_select(evt: gr.SelectData): return evt.index
 
 
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 = int(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(fn=start_wrapper, inputs=[selected_tab_index, camera_source, video_file], outputs=[status_box])
511
+ stop_btn.click(fn=attendance_system.stop_processing, inputs=None, outputs=[status_box])
 
 
 
512
 
513
  register_btn.click(
514
+ fn=attendance_system.register_worker_manual,
515
+ inputs=[register_image, register_name],
516
  outputs=[register_output, registered_workers_info]
517
  )
 
518
  refresh_workers_btn.click(
519
+ fn=attendance_system.get_registered_workers_info,
520
  outputs=[registered_workers_info]
521
  )
522
 
523
  def update_ui_generator():
524
  while True:
525
+ # Handle error messages first
526
  if attendance_system.error_message:
527
+ error = attendance_system.error_message
528
+ attendance_system.error_message = None # Clear after sending
529
+ yield None, error, f"Status: {error}"
530
  time.sleep(2)
 
531
  continue
532
+
533
+ log_md = "\n".join(reversed(attendance_system.session_log)) or "Processing..."
534
+ frame = None
535
+
536
  if attendance_system.is_processing.is_set():
 
537
  try:
538
+ # Non-blocking get from the queue
539
  if not attendance_system.frame_queue.empty():
540
+ frame_bgr = attendance_system.frame_queue.get_nowait()
541
+ if frame_bgr is not None:
542
+ frame = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
543
  except queue.Empty:
544
+ pass # No new frame, just update log
545
+ yield frame, log_md, "Status: Processing..."
 
546
  else:
547
+ # Processing has stopped
548
+ final_frame = None
549
  if attendance_system.last_processed_frame is not None:
550
  final_frame = cv2.cvtColor(attendance_system.last_processed_frame, cv2.COLOR_BGR2RGB)
551
+
552
+ final_log_md = "Processing complete. Here is the final log:"
553
+ if attendance_system.final_log:
554
+ final_log_md = "\n".join(reversed(attendance_system.final_log))
555
  else:
556
+ final_log_md = "Processing stopped or finished with no new attendance."
557
+
558
+ yield final_frame, final_log_md, "Status: Stopped. Go to 'Controls & Status' to start."
559
+
560
+ time.sleep(0.1) # UI update frequency
561
+
562
+ # Use a separate generator for status to avoid conflicts
563
+ def update_status_generator():
564
+ while True:
565
+ if attendance_system.is_processing.is_set():
566
+ yield "Status: Processing..."
567
+ else:
568
+ yield "Status: Stopped. Go to 'Controls & Status' to start."
569
+ time.sleep(1)
570
+
571
+ # Bind the generator to update the UI components
572
+ demo.load(
573
+ fn=update_ui_generator,
574
+ outputs=[video_output, session_log_display, status_box],
575
+ every=0.1
576
+ )
577
 
 
578
  return demo
579
 
580
  if __name__ == "__main__":