PrashanthB461 commited on
Commit
4ce71a8
Β·
verified Β·
1 Parent(s): 6c51e6f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +359 -834
app.py CHANGED
@@ -2,32 +2,37 @@
2
  import os
3
  os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'
4
 
5
- import gradio as gr
6
- import cv2
7
- import numpy as np
8
- import pandas as pd
9
- from datetime import datetime, date
10
- from typing import Tuple, Optional
11
- import logging
12
- from deepface import DeepFace
13
- import pickle
14
- from io import BytesIO
15
  import base64
16
- from PIL import Image
17
  import json
 
 
18
  import threading
19
  import time
20
- import queue
 
 
 
 
 
 
 
 
 
 
21
  import requests
22
- from simple_salesforce import Salesforce
23
  from dotenv import load_dotenv
 
24
  from retrying import retry
 
 
 
25
 
26
  # Setup logging
27
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
28
  logger = logging.getLogger(__name__)
29
 
30
- # Load environment variables
31
  load_dotenv()
32
 
33
  # Hugging Face API configuration
@@ -36,894 +41,414 @@ HF_API_TOKEN = os.getenv("HUGGINGFACE_API_TOKEN")
36
 
37
  # Salesforce configuration
38
  SF_CREDENTIALS = {
39
- "username": "smartlabour@attendance.system",
40
- "password": "#Prashanth@1234",
41
- "security_token": "7xPmtDFoWlZUGK0V2QSwFZJ6c",
42
- "domain": "login"
43
  }
44
 
 
 
45
  @retry(stop_max_attempt_number=3, wait_fixed=2000)
46
- def connect_to_salesforce():
 
47
  try:
48
  sf = Salesforce(**SF_CREDENTIALS)
49
- logger.info("Connected to Salesforce")
50
- sf.describe()
51
  return sf
52
  except Exception as e:
53
- logger.error(f"Salesforce connection failed: {e}")
54
  raise
55
 
 
 
56
  class AttendanceSystem:
 
 
 
57
  def __init__(self):
58
- self.known_face_embeddings = []
59
- self.known_face_names = []
60
- self.known_face_ids = []
61
- self.attendance_records = []
62
- self.next_worker_id = 1
63
- self.video_capture = None
64
- self.is_streaming = False
65
- self.frame_queue = queue.Queue(maxsize=2)
66
- self.recognition_thread = None
 
 
 
 
 
 
67
  self.last_recognition_time = {}
68
- self.recognition_cooldown = 5 # seconds
69
- self.video_file_path = None
70
- self.video_processing = False
71
 
72
- # Initialize Salesforce
73
- try:
74
- self.sf = connect_to_salesforce()
75
- except Exception as e:
76
- logger.error(f"Error connecting to Salesforce: {e}")
77
- self.sf = None
78
-
79
- # Create directories
80
- os.makedirs("data", exist_ok=True)
81
  os.makedirs("data/faces", exist_ok=True)
82
-
83
- self.load_data()
84
 
85
- def load_data(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  try:
87
  if os.path.exists("data/workers.pkl"):
88
- with open("data/workers.pkl", "rb") as f:
89
- data = pickle.load(f)
90
- self.known_face_embeddings = data.get("embeddings", [])
91
- self.known_face_names = data.get("names", [])
92
- self.known_face_ids = data.get("ids", [])
93
- self.next_worker_id = data.get("next_id", 1)
94
-
95
- if os.path.exists("data/attendance.json"):
96
- with open("data/attendance.json", "r") as f:
97
- self.attendance_records = json.load(f)
98
-
99
- # Load embeddings from Salesforce for duplicate checks
100
- if self.sf:
101
- try:
102
- workers = self.sf.query_all("SELECT Worker_ID__c, Name, Face_Embedding__c FROM Worker__c")['records']
103
- for worker in workers:
104
- if worker['Face_Embedding__c']:
105
- embedding = json.loads(worker['Face_Embedding__c'])
106
- if worker['Worker_ID__c'] not in self.known_face_ids:
107
- self.known_face_embeddings.append(embedding)
108
- self.known_face_names.append(worker['Name'])
109
- self.known_face_ids.append(worker['Worker_ID__c'])
110
- self.next_worker_id = max(self.next_worker_id, int(worker['Worker_ID__c'][1:]) + 1)
111
- except Exception as e:
112
- logger.error(f"Error loading embeddings from Salesforce: {e}")
113
  except Exception as e:
114
- logger.error(f"Error loading data: {e}")
115
- self.known_face_embeddings = []
116
- self.known_face_names = []
117
- self.known_face_ids = []
118
- self.attendance_records = []
119
- self.next_worker_id = 1
120
-
121
- def save_data(self):
122
- try:
123
- worker_data = {
124
- "embeddings": self.known_face_embeddings,
125
- "names": self.known_face_names,
126
- "ids": self.known_face_ids,
127
- "next_id": self.next_worker_id
128
- }
129
- with open("data/workers.pkl", "wb") as f:
130
- pickle.dump(worker_data, f)
131
-
132
- with open("data/attendance.json", "w") as f:
133
- json.dump(self.attendance_records, f, indent=2)
134
- except Exception as e:
135
- logger.error(f"Error saving data: {e}")
136
-
137
- def get_image_caption(self, image: Image.Image) -> str:
138
- """Generate image caption using Hugging Face API"""
139
- try:
140
- img_byte_arr = BytesIO()
141
- image.save(img_byte_arr, format='JPEG', quality=85)
142
- img_data = img_byte_arr.getvalue()
143
-
144
- headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
145
- response = requests.post(HF_API_URL, headers=headers, data=img_data)
146
-
147
- if response.status_code == 200:
148
- result = response.json()
149
- if isinstance(result, list) and len(result) > 0:
150
- return result[0].get("generated_text", "No caption generated")
151
- return "No caption generated"
152
- else:
153
- logger.error(f"Hugging Face API error: {response.status_code} - {response.text}")
154
- return "Error generating caption"
155
- except Exception as e:
156
- logger.error(f"Error in Hugging Face API call: {e}")
157
- return "Error generating caption"
158
 
159
- def upload_image_to_salesforce(self, image: Image.Image, worker_salesforce_id: str, worker_id: str, worker_name: str) -> Optional[str]:
160
- """Upload worker image to Salesforce as ContentVersion"""
161
  try:
162
- if not image:
163
- logger.error("No image provided for upload")
164
- return None
165
-
166
- img_byte_arr = BytesIO()
167
- image.save(img_byte_arr, format='JPEG', quality=85)
168
- img_data = img_byte_arr.getvalue()
169
-
170
- encoded_image = base64.b64encode(img_data).decode('utf-8')
171
-
172
- content_version_data = {
173
- "Title": f"Worker_Image_{worker_id}_{worker_name.replace(' ', '_')}",
174
- "PathOnClient": f"worker_{worker_id}.jpg",
175
- "VersionData": encoded_image,
176
- "FirstPublishLocationId": worker_salesforce_id
177
- }
178
- content_version = self.sf.ContentVersion.create(content_version_data)
179
-
180
- file_url = f"https://{self.sf.sf_instance}/sfc/servlet.shepherd/version/download/{content_version['id']}"
181
- logger.info(f"Image uploaded to Salesforce for worker {worker_id}: {file_url}")
182
- return file_url
183
  except Exception as e:
184
- logger.error(f"Error uploading image to Salesforce: {e}")
185
- return None
186
-
187
  def register_worker_manual(self, image: Image.Image, name: str) -> Tuple[str, str]:
188
- """Manual worker registration with Hugging Face and Salesforce"""
189
  if image is None or not name.strip():
190
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
191
-
192
- image_array = np.array(image)
193
-
194
  try:
195
- face_analysis = DeepFace.analyze(img_path=image_array, actions=['emotion'], enforce_detection=True, detector_backend='opencv')
196
-
197
  embedding = DeepFace.represent(img_path=image_array, model_name='Facenet')[0]['embedding']
198
-
199
- name = name.strip().title()
200
- if name in self.known_face_names:
201
- return f"❌ {name} is already registered!", self.get_registered_workers_info()
202
-
203
- # Check for duplicate face
204
- if len(self.known_face_embeddings) > 0:
205
- distances = [np.linalg.norm(np.array(embedding) - np.array(known_embedding))
206
- for known_embedding in self.known_face_embeddings]
207
- min_distance = min(distances)
208
- if min_distance < 10:
209
- best_match_index = distances.index(min_distance)
210
- matched_name = self.known_face_names[best_match_index]
211
- matched_id = self.known_face_ids[best_match_index]
212
- return f"❌ Face matches existing worker: {matched_name} ({matched_id})!", self.get_registered_workers_info()
213
-
214
  worker_id = f"W{self.next_worker_id:04d}"
215
-
216
- caption = self.get_image_caption(image)
217
-
218
- self.known_face_embeddings.append(embedding)
219
- self.known_face_names.append(name)
220
- self.known_face_ids.append(worker_id)
221
- self.next_worker_id += 1
222
-
223
- face_image = Image.fromarray(image_array)
224
- local_path = f"data/faces/{worker_id}_{name.replace(' ', '_')}.jpg"
225
- face_image.save(local_path)
226
-
227
- image_url = None
228
- if self.sf:
229
- try:
230
- worker_record = self.sf.Worker__c.create({
231
- 'Name': name,
232
- 'Worker_ID__c': worker_id,
233
- 'Face_Embedding__c': json.dumps(embedding),
234
- 'Image_Caption__c': caption
235
- })
236
- image_url = self.upload_image_to_salesforce(face_image, worker_record['id'], worker_id, name)
237
- if image_url:
238
- self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
239
- else:
240
- logger.warning("Image URL not set due to upload failure")
241
- except Exception as e:
242
- logger.error(f"Error saving to Salesforce: {e}")
243
-
244
- self.save_data()
245
-
246
- return f"βœ… {name} has been successfully registered with ID: {worker_id}! Caption: {caption}\nImage URL: {image_url or 'Not uploaded'}", self.get_registered_workers_info()
247
-
248
- except ValueError as e:
249
- if "Face could not be detected" in str(e):
250
- return "❌ No face detected in the image!", self.get_registered_workers_info()
251
- return f"❌ Error processing image: {str(e)}", self.get_registered_workers_info()
252
  except Exception as e:
253
- return f"❌ Error during registration: {str(e)}", self.get_registered_workers_info()
254
 
255
- def register_worker_auto(self, face_image: np.ndarray) -> Tuple[Optional[str], Optional[str]]:
256
- """Automatic worker registration for unrecognized faces"""
257
  try:
258
- embedding = DeepFace.represent(img_path=face_image, model_name='Facenet')[0]['embedding']
259
-
260
- # Check for duplicate face
261
- if len(self.known_face_embeddings) > 0:
262
- distances = [np.linalg.norm(np.array(embedding) - np.array(known_embedding))
263
- for known_embedding in self.known_face_embeddings]
264
- min_distance = min(distances)
265
- if min_distance < 10:
266
- best_match_index = distances.index(min_distance)
267
- return self.known_face_ids[best_match_index], self.known_face_names[best_match_index]
268
-
269
  worker_id = f"W{self.next_worker_id:04d}"
270
- worker_name = f"Unknown_Worker_{self.next_worker_id}"
271
-
272
- face_pil = Image.fromarray(cv2.cvtColor(face_image, cv2.COLOR_BGR2RGB))
273
- caption = self.get_image_caption(face_pil)
274
-
275
- self.known_face_embeddings.append(embedding)
276
- self.known_face_names.append(worker_name)
277
- self.known_face_ids.append(worker_id)
278
- self.next_worker_id += 1
279
-
280
- local_path = f"data/faces/{worker_id}_{worker_name}.jpg"
281
- face_pil.save(local_path)
282
-
283
- image_url = None
284
- if self.sf:
285
- try:
286
- worker_record = self.sf.Worker__c.create({
287
- 'Name': worker_name,
288
- 'Worker_ID__c': worker_id,
289
- 'Face_Embedding__c': json.dumps(embedding),
290
- 'Image_Caption__c': caption
291
- })
292
- image_url = self.upload_image_to_salesforce(face_pil, worker_record['id'], worker_id, worker_name)
293
- if image_url:
294
- self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
295
- except Exception as e:
296
- logger.error(f"Error saving to Salesforce: {e}")
297
-
298
- self.save_data()
299
-
300
  return worker_id, worker_name
301
-
302
  except Exception as e:
303
- logger.error(f"Error in auto registration: {e}")
304
- return None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
 
306
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  try:
308
- today = date.today().isoformat()
309
- current_time = datetime.now()
310
-
311
- already_marked = any(
312
- record["worker_id"] == worker_id and record["date"] == today
313
- for record in self.attendance_records
314
- )
315
-
316
- if not already_marked:
317
- attendance_record = {
318
- "worker_id": worker_id,
319
- "name": worker_name,
320
- "date": today,
321
- "time": current_time.strftime("%H:%M:%S"),
322
- "timestamp": current_time.isoformat(),
323
- "status": "Present",
324
- "method": "Auto"
325
- }
326
- self.attendance_records.append(attendance_record)
327
-
328
- if self.sf:
329
- try:
330
- self.sf.Attendance__c.create({
331
- 'Worker_ID__c': worker_id,
332
- 'Name__c': worker_name,
333
- 'Date__c': today,
334
- 'Time__c': current_time.strftime("%H:%M:%S"),
335
- 'Timestamp__c': current_time.isoformat(),
336
- 'Status__c': "Present",
337
- 'Method__c': "Auto"
338
- })
339
- logger.info(f"Attendance for {worker_name} ({worker_id}) saved to Salesforce")
340
- except Exception as e:
341
- logger.error(f"Error saving attendance to Salesforce: {e}")
342
-
343
- self.save_data()
344
- return True
345
- return False
346
- except Exception as e:
347
- logger.error(f"Error marking attendance: {e}")
348
- return False
349
 
350
- def process_video_frame(self, frame: np.ndarray) -> np.ndarray:
351
- try:
352
- rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
353
-
354
- face_objs = DeepFace.extract_faces(img_path=rgb_frame, target_size=(160, 160), enforce_detection=False, detector_backend='opencv')
355
-
356
- current_time = time.time()
357
-
358
- for face_obj in face_objs:
359
- if face_obj['confidence'] > 0.9:
360
- face_area = face_obj['facial_area']
361
- x, y, w, h = face_area['x'], face_area['y'], face_area['w'], face_area['h']
362
-
363
- face_image = frame[y:y+h, x:x+w]
 
 
364
 
365
- try:
366
- embedding = DeepFace.represent(img_path=face_image, model_name='Facenet')[0]['embedding']
367
-
368
- worker_id = None
369
- worker_name = "Unknown"
370
- color = (0, 0, 255)
371
-
372
- if len(self.known_face_embeddings) > 0:
373
- distances = [np.linalg.norm(np.array(embedding) - np.array(known_embedding))
374
- for known_embedding in self.known_face_embeddings]
375
- min_distance = min(distances)
376
- best_match_index = distances.index(min_distance)
377
-
378
- if min_distance < 10:
379
- worker_id = self.known_face_ids[best_match_index]
380
- worker_name = self.known_face_names[best_match_index]
381
- color = (0, 255, 0)
382
-
383
- if worker_id not in self.last_recognition_time or \
384
- current_time - self.last_recognition_time[worker_id] > self.recognition_cooldown:
385
- if self.mark_attendance(worker_id, worker_name):
386
- logger.info(f"Attendance marked for {worker_name} ({worker_id})")
387
- self.last_recognition_time[worker_id] = current_time
388
- else:
389
- if face_image.size > 0:
390
- new_id, new_name = self.register_worker_auto(face_image)
391
- if new_id:
392
- worker_id = new_id
393
- worker_name = new_name
394
- color = (255, 165, 0)
395
- logger.info(f"New worker registered: {new_name} ({new_id})")
396
- if self.mark_attendance(worker_id, worker_name):
397
- logger.info(f"Attendance marked for new worker {worker_name} ({worker_id})")
398
-
399
- cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
400
- cv2.rectangle(frame, (x, y+h - 35), (x+w, y+h), color, cv2.FILLED)
401
-
402
- label = f"{worker_name} ({worker_id})" if worker_id else worker_name
403
- cv2.putText(frame, label, (x + 6, y+h - 6),
404
- cv2.FONT_HERSHEY_DUPLEX, 0.6, (255, 255, 255), 1)
405
 
406
- except Exception as e:
407
- logger.error(f"Error processing face: {e}")
408
- continue
409
-
410
- return frame
411
- except Exception as e:
412
- logger.error(f"Error processing frame: {e}")
413
- return frame
414
 
415
- def start_video_stream(self, camera_source: int = 0) -> str:
416
- try:
417
- if self.is_streaming:
418
- return "⚠️ Video stream is already running!"
419
-
420
- self.video_file_path = None
421
- self.video_capture = cv2.VideoCapture(camera_source)
422
- if not self.video_capture.isOpened():
423
- return "❌ Could not open camera/video source!"
424
-
425
- self.is_streaming = True
426
-
427
- def video_loop():
428
- while self.is_streaming:
429
- ret, frame = self.video_capture.read()
430
- if not ret:
431
- break
432
- processed_frame = self.process_video_frame(frame)
433
- if not self.frame_queue.full():
434
- try:
435
- self.frame_queue.put_nowait(processed_frame)
436
- except queue.Full:
437
- pass
438
- time.sleep(0.1)
439
-
440
- self.recognition_thread = threading.Thread(target=video_loop)
441
- self.recognition_thread.daemon = True
442
- self.recognition_thread.start()
443
-
444
- return "βœ… Live camera stream started successfully!"
445
- except Exception as e:
446
- return f"❌ Error starting video stream: {e}"
447
 
448
- def process_uploaded_video(self, video_path: str) -> str:
449
- try:
450
- if self.is_streaming:
451
- return "⚠️ Please stop current stream before processing a video file!"
452
-
453
- if not os.path.exists(video_path):
454
- return "❌ Video file not found!"
455
-
456
- self.video_file_path = video_path
457
- self.video_processing = True
458
-
459
- def video_processing_loop():
460
- cap = cv2.VideoCapture(video_path)
461
- fps = cap.get(cv2.CAP_PROP_FPS)
462
- frame_delay = 1.0 / fps if fps > 0 else 0.03
463
-
464
- while self.video_processing and cap.isOpened():
465
- ret, frame = cap.read()
466
- if not ret:
467
- break
468
- processed_frame = self.process_video_frame(frame)
469
- if not self.frame_queue.full():
470
- try:
471
- self.frame_queue.put_nowait(processed_frame)
472
- except queue.Full:
473
- pass
474
- time.sleep(frame_delay)
475
-
476
- cap.release()
477
- self.video_processing = False
478
-
479
- self.recognition_thread = threading.Thread(target=video_processing_loop)
480
- self.recognition_thread.daemon = True
481
- self.recognition_thread.start()
482
-
483
- return f"βœ… Video processing started successfully! ({os.path.basename(video_path)})"
484
- except Exception as e:
485
- return f"❌ Error processing video: {e}"
486
 
487
- def stop_video_stream(self) -> str:
488
- try:
489
- self.is_streaming = False
490
- self.video_processing = False
491
-
492
- if self.video_capture:
493
- self.video_capture.release()
494
- self.video_capture = None
495
-
496
- if self.recognition_thread:
497
- self.recognition_thread.join(timeout=2)
498
-
499
- while not self.frame_queue.empty():
500
- try:
501
- self.frame_queue.get_nowait()
502
- except queue.Empty:
503
- break
504
-
505
- return "βœ… Video stream/processing stopped successfully!"
506
- except Exception as e:
507
- return f"❌ Error stopping video: {e}"
508
 
509
- def get_current_frame(self) -> Optional[np.ndarray]:
510
- try:
511
- if not self.frame_queue.empty():
512
- return self.frame_queue.get_nowait()
513
- return None
514
- except queue.Empty:
515
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
 
517
- def get_registered_workers_info(self) -> str:
518
- if not self.sf:
519
- return "❌ Salesforce connection not established."
520
-
521
- try:
522
- workers = self.sf.query_all("SELECT Name, Worker_ID__c, Image_Caption__c, Image_URL__c FROM Worker__c")['records']
523
- if not workers:
524
- return "No workers registered yet."
525
-
526
- info = f"**Registered Workers ({len(workers)}):**\n\n"
527
- for i, worker in enumerate(workers, 1):
528
- info += f"{i}. **{worker['Name']}** (ID: {worker['Worker_ID__c']}) - Caption: {worker['Image_Caption__c'] or 'N/A'}\n"
529
- if worker['Image_URL__c']:
530
- info += f" Image: [View]({worker['Image_URL__c']})\n"
531
- return info
532
  except Exception as e:
533
- logger.error(f"Error fetching workers from Salesforce: {e}")
534
- return self._get_local_workers_info()
535
 
536
- def _get_local_workers_info(self) -> str:
537
- if not self.known_face_names:
538
- return "No workers registered yet."
539
-
540
- info = f"**Registered Workers ({len(self.known_face_names)}):**\n\n"
541
- for i, (worker_id, name) in enumerate(zip(self.known_face_ids, self.known_face_names), 1):
542
- info += f"{i}. **{name}** (ID: {worker_id})\n"
543
- return info
544
-
545
- def get_today_attendance(self) -> str:
546
- if not self.sf:
547
- return "❌ Salesforce connection not established."
548
-
549
- today = date.today().isoformat()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  try:
551
- records = self.sf.query_all(
552
- f"SELECT Name__c, Worker_ID__c, Time__c, Method__c FROM Attendance__c WHERE Date__c = '{today}'"
553
- )['records']
554
-
555
- if not records:
556
- return f"**Today's Attendance ({today}):**\n\nNo attendance marked yet."
557
-
558
- info = f"**Today's Attendance ({today}):**\n\n"
559
- for record in records:
560
- method_icon = "πŸ€–" if record['Method__c'] == "Auto" else "πŸ‘€"
561
- info += f"{method_icon} **{record['Name__c']}** (ID: {record['Worker_ID__c']}) - {record['Time__c']}\n"
562
- return info
563
  except Exception as e:
564
- logger.error(f"Error fetching attendance from Salesforce: {e}")
565
- return self._get_local_today_attendance()
566
 
567
- def _get_local_today_attendance(self) -> str:
568
- today = date.today().isoformat()
569
- today_records = [r for r in self.attendance_records if r["date"] == today]
570
-
571
- if not today_records:
572
- return f"**Today's Attendance ({today}):**\n\nNo attendance marked yet."
573
-
574
- info = f"**Today's Attendance ({today}):**\n\n"
575
- for record in today_records:
576
- method_icon = "πŸ€–" if record.get("method") == "Auto" else "πŸ‘€"
577
- info += f"{method_icon} **{record['name']}** (ID: {record['worker_id']}) - {record['time']}\n"
578
- return info
579
-
580
- def get_attendance_report(self, start_date: str, end_date: str) -> str:
581
- if not start_date or not end_date:
582
- return "Please select both start and end dates."
583
-
584
  try:
585
- datetime.strptime(start_date, '%Y-%m-%d')
586
- datetime.strptime(end_date, '%Y-%m-%d')
587
- except ValueError:
588
- return "Invalid date format. Please use YYYY-MM-DD."
589
-
590
- if not self.sf:
591
- return "❌ Salesforce connection not established."
592
-
593
- try:
594
- records = self.sf.query_all(
595
- f"SELECT Worker_ID__c, Name__c, Date__c, Time__c, Method__c FROM Attendance__c "
596
- f"WHERE Date__c >= '{start_date}' AND Date__c <= '{end_date}'"
597
- )['records']
598
-
599
- if not records:
600
- return f"No attendance records found between {start_date} and {end_date}."
601
-
602
- df = pd.DataFrame(records)
603
-
604
- total_days = (pd.to_datetime(end_date) - pd.to_datetime(start_date)).days + 1
605
- unique_workers = df['Worker_ID__c'].nunique()
606
- total_attendances = len(df)
607
- auto_registrations = len(df[df['Method__c'] == 'Auto'])
608
-
609
- report = f"**πŸ“Š Attendance Report ({start_date} to {end_date})**\n\n"
610
- report += f"**Summary:**\n"
611
- report += f"β€’ Total Days: {total_days}\n"
612
- report += f"β€’ Unique Workers: {unique_workers}\n"
613
- report += f"β€’ Total Attendances: {total_attendances}\n"
614
- report += f"β€’ Auto Detections: {auto_registrations}\n\n"
615
-
616
- if not df.empty:
617
- attendance_counts = df.groupby(['Worker_ID__c', 'Name__c']).size().reset_index(name='count')
618
- report += f"**πŸ‘₯ Individual Attendance:**\n"
619
- for _, row in attendance_counts.iterrows():
620
- percentage = (row['count'] / total_days) * 100
621
- report += f"β€’ **{row['Name__c']}** ({row['Worker_ID__c']}): {row['count']} days ({percentage:.1f}%)\n"
622
-
623
- return report
624
  except Exception as e:
625
- logger.error(f"Error generating report from Salesforce: {e}")
626
- return self._get_local_attendance_report(start_date, end_date)
627
-
628
- def _get_local_attendance_report(self, start_date: str, end_date: str) -> str:
629
- filtered_records = [
630
- r for r in self.attendance_records
631
- if start_date <= r["date"] <= end_date
632
- ]
633
-
634
- if not filtered_records:
635
- return f"No attendance records found between {start_date} and {end_date}."
636
-
637
- df = pd.DataFrame(filtered_records)
638
-
639
- total_days = (pd.to_datetime(end_date) - pd.to_datetime(start_date)).days + 1
640
- unique_workers = df['worker_id'].nunique()
641
- total_attendances = len(df)
642
- auto_registrations = len(df[df['method'] == 'Auto'])
643
-
644
- report = f"**πŸ“Š Attendance Report ({start_date} to {end_date})**\n\n"
645
- report += f"**Summary:**\n"
646
- report += f"β€’ Total Days: {total_days}\n"
647
- report += f"β€’ Unique Workers: {unique_workers}\n"
648
- report += f"β€’ Total Attendances: {total_attendances}\n"
649
- report += f"β€’ Auto Detections: {auto_registrations}\n\n"
650
-
651
- if not df.empty:
652
- attendance_counts = df.groupby(['worker_id', 'name']).size().reset_index(name='count')
653
- report += f"**πŸ‘₯ Individual Attendance:**\n"
654
- for _, row in attendance_counts.iterrows():
655
- percentage = (row['count'] / total_days) * 100
656
- report += f"β€’ **{row['name']}** ({row['worker_id']}): {row['count']} days ({percentage:.1f}%)\n"
657
-
658
- return report
659
 
660
- def export_attendance_csv(self) -> Tuple[Optional[str], str]:
 
661
  try:
662
- if not self.attendance_records:
663
- return None, "No attendance records to export."
664
-
665
- df = pd.DataFrame(self.attendance_records)
666
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
667
- csv_file = f"attendance_report_{timestamp}.csv"
668
- df.to_csv(csv_file, index=False)
669
 
670
- return csv_file, f"βœ… Attendance exported to {csv_file}"
671
- except Exception as e:
672
- return None, f"❌ Error exporting data: {e}"
673
-
674
- # Initialize system
675
  attendance_system = AttendanceSystem()
676
-
677
  def create_interface():
678
- with gr.Blocks(
679
- title="🎯 Advanced Attendance System with Video Recognition",
680
- theme=gr.themes.Soft(),
681
- css="""
682
- .gradio-container { max-width: 1400px !important; }
683
- .tab-nav { font-weight: bold; }
684
- .status-box { padding: 10px; border-radius: 5px; margin: 5px 0; }
685
- .video-option-tabs { margin-bottom: 15px; }
686
- """
687
- ) as demo:
688
- gr.Markdown(
689
- """
690
- # 🎯 Advanced Attendance System with Face Recognition
691
-
692
- **Comprehensive facial recognition system with live camera and video file processing, integrated with Hugging Face and Salesforce**
693
-
694
- ## πŸš€ **Key Features:**
695
- - **πŸŽ₯ Live Camera Recognition** - Real-time face detection from camera/CCTV
696
- - **πŸ“Ή Video File Processing** - Process pre-recorded videos for attendance
697
- - **πŸ€– Automatic Worker Registration** - Auto-register unknown faces with unique IDs
698
- - **πŸ‘€ Manual Registration** - Register workers manually with photos and AI-generated captions
699
- - **πŸ“… 24-Hour Attendance Rule** - One attendance mark per worker per day
700
- - **πŸ“Š Advanced Analytics** - Detailed reports and data export
701
- - **πŸ€— Hugging Face Integration** - AI-powered image captioning
702
- - **☁️ Salesforce Integration** - Store worker and attendance data in Salesforce
703
- """
704
- )
705
-
706
  with gr.Tabs():
707
- with gr.Tab("πŸŽ₯ Video Recognition", elem_classes="tab-nav"):
708
- gr.Markdown("### Face Recognition from Live Camera or Video File")
709
-
710
  with gr.Row():
711
  with gr.Column(scale=1):
712
- with gr.Tabs(selected="live", elem_classes="video-option-tabs") as video_tabs:
713
- with gr.Tab("Live Camera", id="live"):
714
- camera_source = gr.Number(
715
- label="Camera Source (0 for default camera, or RTSP URL)",
716
- value=0,
717
- precision=0
718
- )
719
-
720
- with gr.Row():
721
- start_stream_btn = gr.Button(
722
- "πŸŽ₯ Start Live Recognition",
723
- variant="primary",
724
- size="lg"
725
- )
726
-
727
- with gr.Tab("Upload Video", id="upload"):
728
- video_file = gr.Video(
729
- label="Upload Video File",
730
- sources=["upload"],
731
- format="mp4"
732
- )
733
-
734
- with gr.Row():
735
- process_video_btn = gr.Button(
736
- "πŸ“Ή Process Video File",
737
- variant="primary",
738
- size="lg"
739
- )
740
-
741
- stop_stream_btn = gr.Button(
742
- "⏹️ Stop Processing",
743
- variant="stop",
744
- size="lg"
745
- )
746
-
747
- stream_status = gr.Textbox(
748
- label="Processing Status",
749
- value="Ready to start...",
750
- interactive=False,
751
- lines=2
752
- )
753
-
754
- gr.Markdown(
755
- """
756
- **πŸ“‹ Instructions:**
757
- - **Live Camera:** Select camera source and click "Start Live Recognition"
758
- - **Video File:** Upload a video file and click "Process Video File"
759
- - Click "Stop Processing" to stop current session
760
-
761
- **🎨 Color Coding:**
762
- - 🟒 **Green:** Known worker (attendance marked)
763
- - 🟠 **Orange:** New worker (auto-registered)
764
- - πŸ”΄ **Red:** Face detected but processing
765
- """
766
- )
767
-
768
  with gr.Column(scale=1):
769
- video_output = gr.Image(
770
- label="Recognition Output",
771
- streaming=True,
772
- interactive=False
773
- )
774
-
775
- live_attendance_display = gr.Markdown(
776
- value=attendance_system.get_today_attendance(),
777
- label="Live Attendance Updates"
778
- )
779
-
780
- refresh_attendance_btn = gr.Button(
781
- "πŸ”„ Refresh Attendance",
782
- variant="secondary"
783
- )
784
-
785
- with gr.Tab("πŸ‘€ Manual Registration", elem_classes="tab-nav"):
786
- gr.Markdown("### Register Workers Manually")
787
 
 
788
  with gr.Row():
 
 
789
  with gr.Column(scale=1):
790
- register_image = gr.Image(
791
- label="Upload Worker's Photo",
792
- type="pil",
793
- height=300
794
- )
795
- register_name = gr.Textbox(
796
- label="Worker's Full Name",
797
- placeholder="Enter full name...",
798
- lines=1
799
- )
800
- register_btn = gr.Button(
801
- "πŸ‘€ Register Worker",
802
- variant="primary",
803
- size="lg"
804
- )
805
-
806
- with gr.Column(scale=1):
807
- register_output = gr.Textbox(
808
- label="Registration Status",
809
- lines=3,
810
- interactive=False
811
- )
812
- registered_workers_info = gr.Markdown(
813
- value=attendance_system.get_registered_workers_info(),
814
- label="Registered Workers Database"
815
- )
816
-
817
- with gr.Tab("πŸ“Š Reports & Analytics", elem_classes="tab-nav"):
818
- gr.Markdown("### Attendance Reports and Data Export")
819
-
820
  with gr.Row():
821
  with gr.Column():
822
- gr.Markdown("#### πŸ“… Generate Report")
823
- start_date = gr.Textbox(
824
- label="Start Date (YYYY-MM-DD)",
825
- value=date.today().replace(day=1).strftime('%Y-%m-%d')
826
- )
827
- end_date = gr.Textbox(
828
- label="End Date (YYYY-MM-DD)",
829
- value=date.today().strftime('%Y-%m-%d')
830
- )
831
- generate_report_btn = gr.Button(
832
- "πŸ“Š Generate Report",
833
- variant="primary"
834
- )
835
-
836
- gr.Markdown("#### πŸ’Ύ Export Data")
837
- export_btn = gr.Button(
838
- "πŸ“₯ Export to CSV",
839
- variant="secondary"
840
- )
841
- export_status = gr.Textbox(
842
- label="Export Status",
843
- lines=2,
844
- interactive=False
845
- )
846
- export_file = gr.File(
847
- label="Download File",
848
- visible=False
849
- )
850
-
851
  with gr.Column():
852
- report_output = gr.Markdown(
853
- value="Select date range and click 'Generate Report' to view attendance analytics.",
854
- label="Attendance Report"
855
- )
856
-
857
- start_stream_btn.click(
858
- fn=attendance_system.start_video_stream,
859
- inputs=[camera_source],
860
- outputs=[stream_status]
861
- )
862
-
863
- process_video_btn.click(
864
- fn=attendance_system.process_uploaded_video,
865
- inputs=[video_file],
866
- outputs=[stream_status]
867
- )
868
-
869
- stop_stream_btn.click(
870
- fn=attendance_system.stop_video_stream,
871
- outputs=[stream_status]
872
- )
873
-
874
- refresh_attendance_btn.click(
875
- fn=attendance_system.get_today_attendance,
876
- outputs=[live_attendance_display]
877
- )
878
-
879
- register_btn.click(
880
- fn=attendance_system.register_worker_manual,
881
- inputs=[register_image, register_name],
882
- outputs=[register_output, registered_workers_info]
883
- )
884
-
885
- generate_report_btn.click(
886
- fn=attendance_system.get_attendance_report,
887
- inputs=[start_date, end_date],
888
- outputs=[report_output]
889
- )
890
-
891
- def export_and_show():
892
- file_path, status = attendance_system.export_attendance_csv()
893
- if file_path:
894
- return status, gr.update(visible=True, value=file_path)
895
- else:
896
- return status, gr.update(visible=False)
897
-
898
- export_btn.click(
899
- fn=export_and_show,
900
- outputs=[export_status, export_file]
901
- )
902
-
903
- def update_video_frame():
904
- start_time = time.time()
905
  while True:
906
- current_time = time.time()
907
- if current_time - start_time >= 0.03:
908
- frame = attendance_system.get_current_frame()
909
- if frame is not None:
910
- frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
911
- return frame
912
- start_time = current_time
913
- time.sleep(0.01)
914
-
915
- video_thread = threading.Thread(target=lambda: demo.queue()(update_video_frame)())
916
- video_thread.daemon = True
917
- video_thread.start()
 
 
 
 
 
 
 
 
918
 
 
919
  return demo
920
 
921
  if __name__ == "__main__":
922
- demo = create_interface()
923
- demo.launch(
924
- server_name="0.0.0.0",
925
- server_port=7860,
926
- share=False,
927
- show_error=True,
928
- debug=True
929
- )
 
2
  import os
3
  os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'
4
 
5
+ # Standard Library Imports
 
 
 
 
 
 
 
 
 
6
  import base64
 
7
  import json
8
+ import logging
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
15
+ import pickle
16
+
17
+ # Third-Party Imports
18
+ import cv2
19
+ import gradio as gr
20
+ import numpy as np
21
+ import pandas as pd
22
+ from PIL import Image
23
  import requests
 
24
  from dotenv import load_dotenv
25
+ from deepface import DeepFace
26
  from retrying import retry
27
+ from simple_salesforce import Salesforce
28
+
29
+ # --- CONFIGURATION ---
30
 
31
  # Setup logging
32
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
33
  logger = logging.getLogger(__name__)
34
 
35
+ # Load environment variables from .env file
36
  load_dotenv()
37
 
38
  # Hugging Face API configuration
 
41
 
42
  # Salesforce configuration
43
  SF_CREDENTIALS = {
44
+ "username": os.getenv("SF_USERNAME", "smartlabour@attendance.system"),
45
+ "password": os.getenv("SF_PASSWORD", "#Prashanth@1234"),
46
+ "security_token": os.getenv("SF_SECURITY_TOKEN", "7xPmtDFoWlZUGK0V2QSwFZJ6c"),
47
+ "domain": os.getenv("SF_DOMAIN", "login")
48
  }
49
 
50
+ # --- SALESFORCE CONNECTION ---
51
+
52
  @retry(stop_max_attempt_number=3, wait_fixed=2000)
53
+ def connect_to_salesforce() -> Optional[Salesforce]:
54
+ """Establish a connection to Salesforce with retry logic."""
55
  try:
56
  sf = Salesforce(**SF_CREDENTIALS)
57
+ sf.describe() # Test connection
58
+ logger.info("βœ… Successfully connected to Salesforce.")
59
  return sf
60
  except Exception as e:
61
+ logger.error(f"❌ Salesforce connection failed: {e}")
62
  raise
63
 
64
+ # --- CORE LOGIC ---
65
+
66
  class AttendanceSystem:
67
+ """
68
+ Manages all backend logic for the face recognition attendance system.
69
+ """
70
  def __init__(self):
71
+ # State Management
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] = []
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 = {}
87
+ self.recognition_cooldown = 5
88
+ self.session_log: List[str] = []
 
89
 
90
+ # Initialize
91
+ self.sf = connect_to_salesforce()
92
+ self._create_directories()
93
+ self.load_worker_data()
94
+
95
+ def _create_directories(self):
 
 
 
96
  os.makedirs("data/faces", exist_ok=True)
 
 
97
 
98
+ def load_worker_data(self):
99
+ logger.info("Loading worker data...")
100
+ if self.sf:
101
+ try:
102
+ workers = self.sf.query_all("SELECT Worker_ID__c, Name, Face_Embedding__c FROM Worker__c")['records']
103
+ if not workers:
104
+ self._load_local_worker_data()
105
+ return
106
+
107
+ temp_embeddings, temp_names, temp_ids, max_id = [], [], [], 0
108
+ for worker in workers:
109
+ if worker.get('Face_Embedding__c'):
110
+ temp_embeddings.append(np.array(json.loads(worker['Face_Embedding__c'])))
111
+ temp_names.append(worker['Name'])
112
+ temp_ids.append(worker['Worker_ID__c'])
113
+ try:
114
+ worker_num = int(worker['Worker_ID__c'][1:])
115
+ if worker_num > max_id:
116
+ max_id = worker_num
117
+ except (ValueError, TypeError):
118
+ continue
119
+
120
+ self.known_face_embeddings = temp_embeddings
121
+ self.known_face_names = temp_names
122
+ self.known_face_ids = temp_ids
123
+ self.next_worker_id = max_id + 1
124
+ self.save_local_worker_data()
125
+ logger.info(f"βœ… Loaded {len(self.known_face_ids)} workers from Salesforce.")
126
+ except Exception as e:
127
+ logger.error(f"❌ Error loading from Salesforce: {e}. Attempting local load.")
128
+ self._load_local_worker_data()
129
+ else:
130
+ logger.warning("Salesforce not connected. Loading from local cache.")
131
+ self._load_local_worker_data()
132
+
133
+ def _load_local_worker_data(self):
134
  try:
135
  if os.path.exists("data/workers.pkl"):
136
+ with open("data/workers.pkl", "rb") as f: data = pickle.load(f)
137
+ self.known_face_embeddings = data.get("embeddings", [])
138
+ self.known_face_names = data.get("names", [])
139
+ self.known_face_ids = data.get("ids", [])
140
+ self.next_worker_id = data.get("next_id", 1)
141
+ logger.info(f"βœ… Loaded {len(self.known_face_ids)} workers from local cache.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  except Exception as e:
143
+ logger.error(f"❌ Error loading local data: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
+ def save_local_worker_data(self):
 
146
  try:
147
+ worker_data = {"embeddings": self.known_face_embeddings, "names": self.known_face_names, "ids": self.known_face_ids, "next_id": self.next_worker_id}
148
+ with open("data/workers.pkl", "wb") as f: pickle.dump(worker_data, f)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  except Exception as e:
150
+ logger.error(f"❌ Error saving local worker data: {e}")
151
+
152
+ # --- Registration and Attendance ---
153
  def register_worker_manual(self, image: Image.Image, name: str) -> Tuple[str, str]:
 
154
  if image is None or not name.strip():
155
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
 
 
 
156
  try:
157
+ image_array = np.array(image)
158
+ DeepFace.analyze(img_path=image_array, actions=['emotion'], enforce_detection=True)
159
  embedding = DeepFace.represent(img_path=image_array, model_name='Facenet')[0]['embedding']
160
+ if self._is_duplicate_face(embedding):
161
+ return f"❌ Face matches an existing worker!", self.get_registered_workers_info()
162
+
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  worker_id = f"W{self.next_worker_id:04d}"
164
+ name = name.strip().title()
165
+ self._add_worker_to_system(worker_id, name, embedding, image_array)
166
+ self.save_local_worker_data()
167
+ self.load_worker_data()
168
+ return f"βœ… {name} registered with ID: {worker_id}!", self.get_registered_workers_info()
169
+ except ValueError:
170
+ return "❌ No face detected in the image!", self.get_registered_workers_info()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  except Exception as e:
172
+ return f"❌ Registration error: {e}", self.get_registered_workers_info()
173
 
174
+ def _register_worker_auto(self, face_image: np.ndarray) -> Optional[Tuple[str, str]]:
 
175
  try:
176
+ embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
177
+ if self._is_duplicate_face(embedding): return None
 
 
 
 
 
 
 
 
 
178
  worker_id = f"W{self.next_worker_id:04d}"
179
+ worker_name = f"Unknown Worker {self.next_worker_id}"
180
+ self._add_worker_to_system(worker_id, worker_name, embedding, face_image)
181
+ self.save_local_worker_data()
182
+ log_msg = f"πŸ†• [{datetime.now().strftime('%H:%M:%S')}] Auto-registered: {worker_name} ({worker_id})"
183
+ self.session_log.append(log_msg)
184
+ logger.info(log_msg)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  return worker_id, worker_name
 
186
  except Exception as e:
187
+ logger.error(f"❌ Auto-registration error: {e}")
188
+ return None
189
+
190
+ def _add_worker_to_system(self, worker_id: str, name: str, embedding: List[float], image_array: np.ndarray):
191
+ self.known_face_embeddings.append(np.array(embedding))
192
+ self.known_face_names.append(name)
193
+ self.known_face_ids.append(worker_id)
194
+ self.next_worker_id += 1
195
+ face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
196
+ face_pil.save(f"data/faces/{worker_id}.jpg")
197
+ caption = self._get_image_caption(face_pil)
198
+ if self.sf:
199
+ try:
200
+ worker_record = self.sf.Worker__c.create({'Name': name, 'Worker_ID__c': worker_id, 'Face_Embedding__c': json.dumps(embedding), 'Image_Caption__c': caption})
201
+ image_url = self._upload_image_to_salesforce(face_pil, worker_record['id'], worker_id)
202
+ if image_url: self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
203
+ logger.info(f"βœ… Worker {worker_id} synced to Salesforce.")
204
+ except Exception as e:
205
+ logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
206
+
207
+ def _is_duplicate_face(self, embedding: List[float], threshold: float = 10.0) -> bool:
208
+ if not self.known_face_embeddings: return False
209
+ distances = [np.linalg.norm(np.array(embedding) - known_embedding) for known_embedding in self.known_face_embeddings]
210
+ return min(distances) < threshold
211
 
212
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
213
+ today_str = date.today().isoformat()
214
+ if self._has_attended_today(worker_id, today_str): return False
215
+ current_time = datetime.now()
216
+ if self.sf:
217
+ try:
218
+ 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"})
219
+ except Exception as e:
220
+ logger.error(f"❌ Error saving attendance to Salesforce: {e}")
221
+ log_msg = f"βœ… [{current_time.strftime('%H:%M:%S')}] Marked Present: {worker_name} ({worker_id})"
222
+ self.session_log.append(log_msg)
223
+ return True
224
+
225
+ def _has_attended_today(self, worker_id: str, today_str: str) -> bool:
226
+ last_seen = self.last_recognition_time.get(worker_id)
227
+ if last_seen and (time.time() - last_seen < self.recognition_cooldown): return True
228
+ if self.sf:
229
+ try:
230
+ if self.sf.query(f"SELECT Id FROM Attendance__c WHERE Worker_ID__c = '{worker_id}' AND Date__c = {today_str}")['totalSize'] > 0:
231
+ return True
232
+ except Exception: pass
233
+ return False
234
+
235
+ # --- Video Processing ---
236
+ def process_frame(self, frame: np.ndarray) -> np.ndarray:
237
+ """
238
+ Main function to process a single video frame with the unpacking bug fixed.
239
+ """
240
  try:
241
+ face_objs = DeepFace.extract_faces(img_path=frame, detector_backend='opencv', enforce_detection=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
+ if face_objs:
244
+ print(f"\n--- Frame Processed: Found {len(face_objs)} faces. ---")
245
+
246
+ for i, face_obj in enumerate(face_objs):
247
+ confidence = face_obj['confidence']
248
+ print(f" Face #{i+1}: Confidence Score = {confidence:.2f}")
249
+
250
+ if confidence < 0.95:
251
+ print(" -> Confidence too low, skipping.")
252
+ continue
253
+
254
+ # --- THIS IS THE FIX ---
255
+ # Instead of unpacking all values, we now access them by their specific keys.
256
+ facial_area = face_obj['facial_area']
257
+ x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
258
+ # --- END OF FIX ---
259
 
260
+ face_image = frame[y:y+h, x:x+w]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
+ if face_image.size == 0: continue
 
 
 
 
 
 
 
263
 
264
+ embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
265
+
266
+ if not self.known_face_embeddings:
267
+ print(" -> No known faces in database to compare against.")
268
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
+ distances = [np.linalg.norm(np.array(embedding) - known) for known in self.known_face_embeddings]
271
+ min_dist = min(distances)
272
+ match_index = distances.index(min_dist) if min_dist < 10.0 else -1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
 
274
+ print(f" -> Comparing to DB... Minimum Distance Found: {min_dist:.4f}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
 
276
+ color, worker_id, worker_name = (0, 0, 255), None, "Unknown"
277
+
278
+ if match_index != -1:
279
+ worker_id = self.known_face_ids[match_index]
280
+ worker_name = self.known_face_names[match_index]
281
+ color = (0, 255, 0) # Green
282
+ print(f" βœ“ MATCH! (Threshold: 10.0). Recognized as {worker_name}")
283
+ if self.mark_attendance(worker_id, worker_name):
284
+ self.last_recognition_time[worker_id] = time.time()
285
+ else:
286
+ color = (0, 165, 255) # Orange for potential new worker
287
+ print(f" βœ— NO MATCH (Threshold: 10.0). Attempting to register as new worker...")
288
+ new_worker = self._register_worker_auto(face_image)
289
+ if new_worker:
290
+ worker_id, worker_name = new_worker[0], new_worker[1]
291
+ if self.mark_attendance(worker_id, worker_name):
292
+ self.last_recognition_time[worker_id] = time.time()
293
+
294
+ label = f"{worker_name}" + (f" ({worker_id})" if worker_id else "")
295
+ cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
296
+ cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
297
 
298
+ return frame
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  except Exception as e:
300
+ print(f"ERROR in process_frame: {e}")
301
+ return frame
302
 
303
+ def _processing_loop(self, source):
304
+ video_capture = cv2.VideoCapture(source)
305
+ if not video_capture.isOpened():
306
+ 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."
307
+ self.error_message = err_msg
308
+ self.is_processing.clear()
309
+ return
310
+ while self.is_processing.is_set():
311
+ ret, frame = video_capture.read()
312
+ if not ret: break
313
+ processed_frame = self.process_frame(frame)
314
+ if not self.frame_queue.full(): self.frame_queue.put(processed_frame)
315
+ self.last_processed_frame = processed_frame # Continuously update last frame
316
+ time.sleep(0.05)
317
+ self.final_log = self.session_log.copy() # Save the final log
318
+ video_capture.release()
319
+ self.is_processing.clear()
320
+
321
+ def start_processing(self, source) -> str:
322
+ if self.is_processing.is_set(): return "⚠️ Processing is already active."
323
+ # Reset states for the new session
324
+ self.session_log.clear(); self.last_recognition_time.clear()
325
+ self.error_message = None; self.last_processed_frame = None; self.final_log = None
326
+ self.is_processing.set()
327
+ self.processing_thread = threading.Thread(target=self._processing_loop, args=(source,)); self.processing_thread.daemon = True
328
+ self.processing_thread.start()
329
+ return f"βœ… Started processing..."
330
+
331
+ def stop_processing(self) -> str:
332
+ # Reset states when stopping manually
333
+ self.is_processing.clear(); self.error_message = None
334
+ self.last_processed_frame = None; self.final_log = None
335
+ return "βœ… Processing stopped by user."
336
+
337
+ # --- Helper & Reporting ---
338
+ def _get_image_caption(self, image: Image.Image) -> str:
339
+ if not HF_API_TOKEN: return "Hugging Face API token not configured."
340
  try:
341
+ buffered = BytesIO()
342
+ image.save(buffered, format="JPEG")
343
+ img_data = buffered.getvalue()
344
+ headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
345
+ response = requests.post(HF_API_URL, headers=headers, data=img_data)
346
+ response.raise_for_status()
347
+ result = response.json()
348
+ return result[0].get("generated_text", "No caption found.")
 
 
 
 
349
  except Exception as e:
350
+ logger.error(f"Hugging Face API error: {e}")
351
+ return "Caption generation failed."
352
 
353
+ def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
354
+ if not self.sf: return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  try:
356
+ buffered = BytesIO()
357
+ image.save(buffered, format="JPEG")
358
+ encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
359
+ cv = self.sf.ContentVersion.create({'Title': f'Image_{worker_id}', 'PathOnClient': f'{worker_id}.jpg', 'VersionData': encoded_image, 'FirstPublishLocationId': record_id})
360
+ return f"/{cv['id']}" # Relative URL
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  except Exception as e:
362
+ logger.error(f"Salesforce image upload error: {e}")
363
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
 
365
+ def get_registered_workers_info(self) -> str:
366
+ if not self.sf: return "❌ Salesforce not connected."
367
  try:
368
+ records = self.sf.query_all("SELECT Name, Worker_ID__c FROM Worker__c ORDER BY Name")['records']
369
+ if not records: return "No workers registered."
370
+ return f"**πŸ‘₯ Registered Workers ({len(records)})**\n" + "\n".join([f"- **{w['Name']}** (ID: {w['Worker_ID__c']})" for w in records])
371
+ except Exception as e: return f"Error: {e}"
 
 
 
372
 
373
+ # --- GRADIO UI ---
 
 
 
 
374
  attendance_system = AttendanceSystem()
 
375
  def create_interface():
376
+ with gr.Blocks(theme=gr.themes.Soft(), title="Attendance System") as demo:
377
+ gr.Markdown("# 🎯 Advanced Face Recognition Attendance System")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  with gr.Tabs():
379
+ with gr.Tab("βš™οΈ Controls & Status"):
380
+ gr.Markdown("### 1. Choose Input Source & Start Processing")
 
381
  with gr.Row():
382
  with gr.Column(scale=1):
383
+ selected_tab_index = gr.Number(value=0, visible=False)
384
+ with gr.Tabs() as video_tabs:
385
+ with gr.Tab("Live Camera", id=0):
386
+ camera_source = gr.Number(label="Camera Source", value=0, precision=0)
387
+ with gr.Tab("Upload Video", id=1):
388
+ video_file = gr.Video(label="Upload Video File", sources=["upload"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  with gr.Column(scale=1):
390
+ start_btn = gr.Button("▢️ Start Processing", variant="primary")
391
+ stop_btn = gr.Button("⏹️ Stop Processing", variant="stop")
392
+ status_box = gr.Textbox(label="Status", interactive=False, value="System Ready.")
393
+ gr.Markdown("### 2. View Results in the 'Output & Log' Tab")
394
+ gr.Markdown("**🎨 Color Coding:** <font color='green'>Green</font> = Known, <font color='orange'>Orange</font> = New, <font color='red'>Red</font> = Unknown")
 
 
 
 
 
 
 
 
 
 
 
 
 
395
 
396
+ with gr.Tab("πŸ“Š Output & Log"):
397
  with gr.Row():
398
+ with gr.Column(scale=2):
399
+ video_output = gr.Image(label="Recognition Output", interactive=False)
400
  with gr.Column(scale=1):
401
+ session_log_display = gr.Markdown(label="πŸ“‹ Session Log", value="System is ready.")
402
+
403
+ with gr.Tab("πŸ‘€ Worker Management"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  with gr.Row():
405
  with gr.Column():
406
+ register_image = gr.Image(label="Upload Worker's Photo", type="pil")
407
+ register_name = gr.Textbox(label="Worker's Full Name")
408
+ register_btn = gr.Button("Register Worker", variant="primary")
409
+ register_output = gr.Textbox(label="Registration Status", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  with gr.Column():
411
+ registered_workers_info = gr.Markdown(value=attendance_system.get_registered_workers_info())
412
+ refresh_workers_btn = gr.Button("πŸ”„ Refresh List")
413
+
414
+ # --- Event Handlers ---
415
+ def on_tab_select(evt: gr.SelectData): return evt.index
416
+ video_tabs.select(fn=on_tab_select, inputs=None, outputs=[selected_tab_index])
417
+ def start_wrapper(tab_index, cam_src, vid_path):
418
+ source = cam_src if tab_index == 0 else vid_path
419
+ return "Please provide an input source." if source is None else attendance_system.start_processing(source)
420
+ start_btn.click(fn=start_wrapper, inputs=[selected_tab_index, camera_source, video_file], outputs=[status_box])
421
+ stop_btn.click(fn=attendance_system.stop_processing, inputs=None, outputs=[status_box])
422
+ register_btn.click(fn=attendance_system.register_worker_manual, inputs=[register_image, register_name], outputs=[register_output, registered_workers_info])
423
+ refresh_workers_btn.click(fn=attendance_system.get_registered_workers_info, outputs=[registered_workers_info])
424
+
425
+ def update_ui_generator():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  while True:
427
+ if attendance_system.error_message:
428
+ yield None, attendance_system.error_message
429
+ time.sleep(2); attendance_system.error_message = None
430
+ continue
431
+ if attendance_system.is_processing.is_set():
432
+ frame, log_md = None, "\n".join(reversed(attendance_system.session_log)) or "Processing..."
433
+ try:
434
+ if not attendance_system.frame_queue.empty():
435
+ frame = attendance_system.frame_queue.get_nowait()
436
+ if frame is not None: frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
437
+ except queue.Empty: pass
438
+ yield frame, log_md
439
+ else:
440
+ if attendance_system.last_processed_frame is not None:
441
+ final_frame = cv2.cvtColor(attendance_system.last_processed_frame, cv2.COLOR_BGR2RGB)
442
+ final_log_md = "\n".join(reversed(attendance_system.final_log)) or "Processing complete. No log entries."
443
+ yield final_frame, final_log_md
444
+ else:
445
+ yield None, "System stopped. Go to 'Controls & Status' to start."
446
+ time.sleep(0.1)
447
 
448
+ demo.load(fn=update_ui_generator, outputs=[video_output, session_log_display])
449
  return demo
450
 
451
  if __name__ == "__main__":
452
+ app = create_interface()
453
+ app.queue()
454
+ app.launch(server_name="0.0.0.0", server_port=7860, show_error=True, debug=True)