PrashanthB461 commited on
Commit
df47954
Β·
verified Β·
1 Parent(s): 7f25764

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +929 -0
app.py ADDED
@@ -0,0 +1,929 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Suppress TensorFlow oneDNN warnings
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
34
+ HF_API_URL = "https://api-inference.huggingface.co/models/Salesforce/blip-image-captioning-base"
35
+ HF_API_TOKEN = os.getenv("HUGGINGFACE_API_TOKEN")
36
+
37
+ # Salesforce configuration
38
+ SF_CREDENTIALS = {
39
+ "username": "smartlabour@attendance.system",
40
+ "password": "#Prashanth@123",
41
+ "security_token": "pasQDqmWApzD0skgbv76gVgIs",
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
+ )