PrashanthB461 commited on
Commit
5119d8e
Β·
verified Β·
1 Parent(s): 64b5832

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +775 -213
app.py CHANGED
@@ -11,8 +11,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
@@ -28,8 +30,15 @@ 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
@@ -47,6 +56,156 @@ 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)
@@ -59,13 +218,13 @@ def connect_to_salesforce() -> Optional[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
@@ -73,8 +232,14 @@ class AttendanceSystem:
73
  self.is_processing = threading.Event()
74
  self.frame_queue = queue.Queue(maxsize=10)
75
  self.error_message = None
76
- self.last_processed_frame = None # Holds the final frame after processing
77
- self.final_log = None # Holds the final log after processing
 
 
 
 
 
 
78
 
79
  # Data Storage
80
  self.known_face_embeddings: List[np.ndarray] = []
@@ -84,298 +249,569 @@ class AttendanceSystem:
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():
@@ -390,9 +826,17 @@ def create_interface():
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):
@@ -400,6 +844,7 @@ def create_interface():
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():
@@ -411,30 +856,135 @@ def create_interface():
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:
@@ -443,12 +993,24 @@ def create_interface():
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)
 
 
 
 
 
 
 
 
 
 
 
11
  import time
12
  from datetime import datetime, date
13
  from io import BytesIO
14
+ from typing import Tuple, Optional, List, Dict, Any
15
  import pickle
16
+ import sqlite3
17
+ from pathlib import Path
18
 
19
  # Third-Party Imports
20
  import cv2
 
30
 
31
  # --- CONFIGURATION ---
32
 
33
+ # Setup logging with better formatting
34
+ logging.basicConfig(
35
+ level=logging.INFO,
36
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
37
+ handlers=[
38
+ logging.FileHandler("attendance_system.log"),
39
+ logging.StreamHandler()
40
+ ]
41
+ )
42
  logger = logging.getLogger(__name__)
43
 
44
  # Load environment variables from .env file
 
56
  "domain": os.getenv("SF_DOMAIN", "login")
57
  }
58
 
59
+ # System configuration
60
+ FACE_RECOGNITION_THRESHOLD = float(os.getenv("FACE_THRESHOLD", "10.0"))
61
+ CONFIDENCE_THRESHOLD = float(os.getenv("CONFIDENCE_THRESHOLD", "0.95"))
62
+ RECOGNITION_COOLDOWN = int(os.getenv("RECOGNITION_COOLDOWN", "5"))
63
+
64
+ # --- DATABASE SETUP ---
65
+
66
+ class LocalDatabase:
67
+ """Local SQLite database for offline functionality and backup."""
68
+
69
+ def __init__(self, db_path: str = "data/attendance.db"):
70
+ self.db_path = db_path
71
+ Path(db_path).parent.mkdir(parents=True, exist_ok=True)
72
+ self._init_database()
73
+
74
+ def _init_database(self):
75
+ """Initialize database tables."""
76
+ with sqlite3.connect(self.db_path) as conn:
77
+ cursor = conn.cursor()
78
+
79
+ # Workers table
80
+ cursor.execute('''
81
+ CREATE TABLE IF NOT EXISTS workers (
82
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
83
+ worker_id TEXT UNIQUE NOT NULL,
84
+ name TEXT NOT NULL,
85
+ face_embedding TEXT NOT NULL,
86
+ image_path TEXT,
87
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
88
+ synced_to_sf BOOLEAN DEFAULT FALSE
89
+ )
90
+ ''')
91
+
92
+ # Attendance table
93
+ cursor.execute('''
94
+ CREATE TABLE IF NOT EXISTS attendance (
95
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
96
+ worker_id TEXT NOT NULL,
97
+ worker_name TEXT NOT NULL,
98
+ date TEXT NOT NULL,
99
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
100
+ status TEXT DEFAULT 'Present',
101
+ synced_to_sf BOOLEAN DEFAULT FALSE,
102
+ UNIQUE(worker_id, date)
103
+ )
104
+ ''')
105
+
106
+ # System logs table
107
+ cursor.execute('''
108
+ CREATE TABLE IF NOT EXISTS system_logs (
109
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
110
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
111
+ level TEXT NOT NULL,
112
+ message TEXT NOT NULL,
113
+ module TEXT
114
+ )
115
+ ''')
116
+
117
+ conn.commit()
118
+
119
+ def add_worker(self, worker_id: str, name: str, embedding: List[float], image_path: str = None) -> bool:
120
+ """Add a new worker to the database."""
121
+ try:
122
+ with sqlite3.connect(self.db_path) as conn:
123
+ cursor = conn.cursor()
124
+ cursor.execute('''
125
+ INSERT INTO workers (worker_id, name, face_embedding, image_path)
126
+ VALUES (?, ?, ?, ?)
127
+ ''', (worker_id, name, json.dumps(embedding), image_path))
128
+ conn.commit()
129
+ return True
130
+ except sqlite3.IntegrityError:
131
+ logger.warning(f"Worker {worker_id} already exists in database")
132
+ return False
133
+ except Exception as e:
134
+ logger.error(f"Error adding worker to database: {e}")
135
+ return False
136
+
137
+ def get_all_workers(self) -> List[Dict[str, Any]]:
138
+ """Get all workers from the database."""
139
+ try:
140
+ with sqlite3.connect(self.db_path) as conn:
141
+ cursor = conn.cursor()
142
+ cursor.execute('SELECT worker_id, name, face_embedding, image_path FROM workers')
143
+ workers = []
144
+ for row in cursor.fetchall():
145
+ workers.append({
146
+ 'worker_id': row[0],
147
+ 'name': row[1],
148
+ 'embedding': json.loads(row[2]),
149
+ 'image_path': row[3]
150
+ })
151
+ return workers
152
+ except Exception as e:
153
+ logger.error(f"Error fetching workers from database: {e}")
154
+ return []
155
+
156
+ def mark_attendance(self, worker_id: str, worker_name: str, date_str: str) -> bool:
157
+ """Mark attendance for a worker."""
158
+ try:
159
+ with sqlite3.connect(self.db_path) as conn:
160
+ cursor = conn.cursor()
161
+ cursor.execute('''
162
+ INSERT OR IGNORE INTO attendance (worker_id, worker_name, date)
163
+ VALUES (?, ?, ?)
164
+ ''', (worker_id, worker_name, date_str))
165
+ conn.commit()
166
+ return cursor.rowcount > 0
167
+ except Exception as e:
168
+ logger.error(f"Error marking attendance: {e}")
169
+ return False
170
+
171
+ def has_attended_today(self, worker_id: str, date_str: str) -> bool:
172
+ """Check if worker has already attended today."""
173
+ try:
174
+ with sqlite3.connect(self.db_path) as conn:
175
+ cursor = conn.cursor()
176
+ cursor.execute('''
177
+ SELECT COUNT(*) FROM attendance
178
+ WHERE worker_id = ? AND date = ?
179
+ ''', (worker_id, date_str))
180
+ return cursor.fetchone()[0] > 0
181
+ except Exception as e:
182
+ logger.error(f"Error checking attendance: {e}")
183
+ return False
184
+
185
+ def get_attendance_report(self, start_date: str = None, end_date: str = None) -> pd.DataFrame:
186
+ """Get attendance report as pandas DataFrame."""
187
+ try:
188
+ with sqlite3.connect(self.db_path) as conn:
189
+ query = 'SELECT * FROM attendance'
190
+ params = []
191
+
192
+ if start_date and end_date:
193
+ query += ' WHERE date BETWEEN ? AND ?'
194
+ params = [start_date, end_date]
195
+ elif start_date:
196
+ query += ' WHERE date >= ?'
197
+ params = [start_date]
198
+ elif end_date:
199
+ query += ' WHERE date <= ?'
200
+ params = [end_date]
201
+
202
+ query += ' ORDER BY timestamp DESC'
203
+
204
+ return pd.read_sql_query(query, conn, params=params)
205
+ except Exception as e:
206
+ logger.error(f"Error generating attendance report: {e}")
207
+ return pd.DataFrame()
208
+
209
  # --- SALESFORCE CONNECTION ---
210
 
211
  @retry(stop_max_attempt_number=3, wait_fixed=2000)
 
218
  return sf
219
  except Exception as e:
220
  logger.error(f"❌ Salesforce connection failed: {e}")
221
+ return None # Don't raise, allow offline mode
222
 
223
  # --- CORE LOGIC ---
224
 
225
  class AttendanceSystem:
226
  """
227
+ Enhanced attendance system with improved error handling, offline support, and new features.
228
  """
229
  def __init__(self):
230
  # State Management
 
232
  self.is_processing = threading.Event()
233
  self.frame_queue = queue.Queue(maxsize=10)
234
  self.error_message = None
235
+ self.last_processed_frame = None
236
+ self.final_log = None
237
+ self.processing_stats = {
238
+ 'frames_processed': 0,
239
+ 'faces_detected': 0,
240
+ 'workers_recognized': 0,
241
+ 'new_registrations': 0
242
+ }
243
 
244
  # Data Storage
245
  self.known_face_embeddings: List[np.ndarray] = []
 
249
 
250
  # Session Tracking
251
  self.last_recognition_time = {}
252
+ self.recognition_cooldown = RECOGNITION_COOLDOWN
253
  self.session_log: List[str] = []
254
 
255
+ # Initialize components
256
+ self.db = LocalDatabase()
257
  self.sf = connect_to_salesforce()
258
  self._create_directories()
259
  self.load_worker_data()
260
 
261
  def _create_directories(self):
262
+ """Create necessary directories."""
263
+ directories = ["data/faces", "data/reports", "data/backups"]
264
+ for directory in directories:
265
+ os.makedirs(directory, exist_ok=True)
266
 
267
  def load_worker_data(self):
268
+ """Load worker data with improved error handling and offline support."""
269
  logger.info("Loading worker data...")
270
+
271
+ # First try to load from local database
272
+ local_workers = self.db.get_all_workers()
273
+ if local_workers:
274
+ self._load_workers_from_list(local_workers)
275
+ logger.info(f"βœ… Loaded {len(local_workers)} workers from local database.")
276
+
277
+ # Then sync with Salesforce if available
278
  if self.sf:
279
  try:
280
+ self._sync_with_salesforce()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  except Exception as e:
282
+ logger.error(f"❌ Error syncing with Salesforce: {e}")
 
283
  else:
284
+ logger.warning("Salesforce not connected. Running in offline mode.")
 
285
 
286
+ def _load_workers_from_list(self, workers: List[Dict[str, Any]]):
287
+ """Load workers from a list of worker dictionaries."""
288
+ temp_embeddings, temp_names, temp_ids, max_id = [], [], [], 0
289
+
290
+ for worker in workers:
291
+ temp_embeddings.append(np.array(worker['embedding']))
292
+ temp_names.append(worker['name'])
293
+ temp_ids.append(worker['worker_id'])
294
+
295
+ try:
296
+ worker_num = int(worker['worker_id'][1:])
297
+ if worker_num > max_id:
298
+ max_id = worker_num
299
+ except (ValueError, TypeError):
300
+ continue
301
+
302
+ self.known_face_embeddings = temp_embeddings
303
+ self.known_face_names = temp_names
304
+ self.known_face_ids = temp_ids
305
+ self.next_worker_id = max_id + 1
306
 
307
+ def _sync_with_salesforce(self):
308
+ """Sync data with Salesforce."""
309
  try:
310
+ workers = self.sf.query_all("SELECT Worker_ID__c, Name, Face_Embedding__c FROM Worker__c")['records']
311
+
312
+ for worker in workers:
313
+ if worker.get('Face_Embedding__c'):
314
+ # Add to local database if not exists
315
+ self.db.add_worker(
316
+ worker['Worker_ID__c'],
317
+ worker['Name'],
318
+ json.loads(worker['Face_Embedding__c'])
319
+ )
320
+
321
+ logger.info(f"βœ… Synced {len(workers)} workers from Salesforce.")
322
  except Exception as e:
323
+ logger.error(f"❌ Salesforce sync error: {e}")
324
+
 
325
  def register_worker_manual(self, image: Image.Image, name: str) -> Tuple[str, str]:
326
+ """Register a worker manually with enhanced validation."""
327
  if image is None or not name.strip():
328
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
329
+
330
  try:
331
+ # Validate image quality
332
  image_array = np.array(image)
333
+ if image_array.shape[0] < 100 or image_array.shape[1] < 100:
334
+ return "❌ Image resolution too low. Please use a higher quality image!", self.get_registered_workers_info()
335
+
336
+ # Detect face
337
+ face_analysis = DeepFace.analyze(
338
+ img_path=image_array,
339
+ actions=['emotion'],
340
+ enforce_detection=True
341
+ )
342
+
343
+ # Get embedding
344
+ embedding = DeepFace.represent(
345
+ img_path=image_array,
346
+ model_name='Facenet'
347
+ )[0]['embedding']
348
+
349
+ # Check for duplicates
350
  if self._is_duplicate_face(embedding):
351
+ return f"❌ Face matches an existing worker!", self.get_registered_workers_info()
352
 
353
  worker_id = f"W{self.next_worker_id:04d}"
354
  name = name.strip().title()
355
+
356
+ # Add to system
357
+ success = self._add_worker_to_system(worker_id, name, embedding, image_array)
358
+ if success:
359
+ return f"βœ… {name} registered with ID: {worker_id}!", self.get_registered_workers_info()
360
+ else:
361
+ return f"❌ Failed to register worker!", self.get_registered_workers_info()
362
+
363
+ except ValueError as e:
364
+ return "❌ No face detected in the image! Please ensure the face is clearly visible.", self.get_registered_workers_info()
365
  except Exception as e:
366
+ logger.error(f"Registration error: {e}")
367
+ return f"❌ Registration error: {str(e)}", self.get_registered_workers_info()
368
 
369
  def _register_worker_auto(self, face_image: np.ndarray) -> Optional[Tuple[str, str]]:
370
+ """Auto-register a new worker with improved validation."""
371
  try:
372
+ # Skip very small faces
373
+ if face_image.shape[0] < 50 or face_image.shape[1] < 50:
374
+ return None
375
+
376
+ embedding = DeepFace.represent(
377
+ img_path=face_image,
378
+ model_name='Facenet',
379
+ enforce_detection=False
380
+ )[0]['embedding']
381
+
382
+ if self._is_duplicate_face(embedding):
383
+ return None
384
+
385
  worker_id = f"W{self.next_worker_id:04d}"
386
  worker_name = f"Unknown Worker {self.next_worker_id}"
387
+
388
+ success = self._add_worker_to_system(worker_id, worker_name, embedding, face_image)
389
+ if success:
390
+ log_msg = f"πŸ†• [{datetime.now().strftime('%H:%M:%S')}] Auto-registered: {worker_name} ({worker_id})"
391
+ self.session_log.append(log_msg)
392
+ logger.info(log_msg)
393
+ self.processing_stats['new_registrations'] += 1
394
+ return worker_id, worker_name
395
+
396
  except Exception as e:
397
  logger.error(f"❌ Auto-registration error: {e}")
398
+
399
+ return None
400
 
401
+ def _add_worker_to_system(self, worker_id: str, name: str, embedding: List[float], image_array: np.ndarray) -> bool:
402
+ """Add worker to system with improved error handling."""
403
+ try:
404
+ # Add to memory
405
+ self.known_face_embeddings.append(np.array(embedding))
406
+ self.known_face_names.append(name)
407
+ self.known_face_ids.append(worker_id)
408
+ self.next_worker_id += 1
409
+
410
+ # Save face image
411
+ face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
412
+ image_path = f"data/faces/{worker_id}.jpg"
413
+ face_pil.save(image_path)
414
+
415
+ # Add to local database
416
+ db_success = self.db.add_worker(worker_id, name, embedding, image_path)
417
+
418
+ # Sync to Salesforce if available
419
+ if self.sf:
420
+ try:
421
+ caption = self._get_image_caption(face_pil)
422
+ worker_record = self.sf.Worker__c.create({
423
+ 'Name': name,
424
+ 'Worker_ID__c': worker_id,
425
+ 'Face_Embedding__c': json.dumps(embedding),
426
+ 'Image_Caption__c': caption
427
+ })
428
+
429
+ image_url = self._upload_image_to_salesforce(face_pil, worker_record['id'], worker_id)
430
+ if image_url:
431
+ self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
432
+
433
+ logger.info(f"βœ… Worker {worker_id} synced to Salesforce.")
434
+ except Exception as e:
435
+ logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
436
+
437
+ return db_success
438
+
439
+ except Exception as e:
440
+ logger.error(f"❌ Error adding worker to system: {e}")
441
+ return False
442
 
443
+ def _is_duplicate_face(self, embedding: List[float], threshold: float = None) -> bool:
444
+ """Check for duplicate faces with configurable threshold."""
445
+ if not self.known_face_embeddings:
446
+ return False
447
+
448
+ threshold = threshold or FACE_RECOGNITION_THRESHOLD
449
+ distances = [
450
+ np.linalg.norm(np.array(embedding) - known_embedding)
451
+ for known_embedding in self.known_face_embeddings
452
+ ]
453
  return min(distances) < threshold
454
 
455
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
456
+ """Mark attendance with improved tracking."""
457
  today_str = date.today().isoformat()
458
+
459
+ # Check cooldown period
460
+ last_seen = self.last_recognition_time.get(worker_id)
461
+ if last_seen and (time.time() - last_seen < self.recognition_cooldown):
462
+ return False
463
+
464
+ # Check if already attended today
465
+ if self.db.has_attended_today(worker_id, today_str):
466
+ return False
467
+
468
+ # Mark attendance in local database
469
+ db_success = self.db.mark_attendance(worker_id, worker_name, today_str)
470
+
471
+ # Sync to Salesforce if available
472
  if self.sf:
473
  try:
474
+ current_time = datetime.now()
475
+ self.sf.Attendance__c.create({
476
+ 'Worker_ID__c': worker_id,
477
+ 'Name__c': worker_name,
478
+ 'Date__c': today_str,
479
+ 'Timestamp__c': current_time.isoformat(),
480
+ 'Status__c': "Present"
481
+ })
482
  except Exception as e:
483
  logger.error(f"❌ Error saving attendance to Salesforce: {e}")
484
+
485
+ if db_success:
486
+ current_time = datetime.now()
487
+ log_msg = f"βœ… [{current_time.strftime('%H:%M:%S')}] Marked Present: {worker_name} ({worker_id})"
488
+ self.session_log.append(log_msg)
489
+ self.processing_stats['workers_recognized'] += 1
490
+
491
+ return db_success
 
 
 
 
 
492
 
 
493
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
494
+ """Enhanced frame processing with better error handling and statistics."""
 
 
495
  try:
496
+ self.processing_stats['frames_processed'] += 1
497
+
498
+ # Extract faces with improved error handling
499
+ try:
500
+ face_objs = DeepFace.extract_faces(
501
+ img_path=frame,
502
+ detector_backend='opencv',
503
+ enforce_detection=False
504
+ )
505
+ except Exception as e:
506
+ logger.warning(f"Face extraction error: {e}")
507
+ return frame
508
 
509
  if face_objs:
510
+ self.processing_stats['faces_detected'] += len(face_objs)
511
+ logger.debug(f"Found {len(face_objs)} faces in frame")
512
 
513
  for i, face_obj in enumerate(face_objs):
514
+ try:
515
+ confidence = face_obj.get('confidence', 0)
516
+
517
+ if confidence < CONFIDENCE_THRESHOLD:
518
+ continue
519
+
520
+ # Extract facial area coordinates
521
+ facial_area = face_obj['facial_area']
522
+ x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
523
+
524
+ # Validate coordinates
525
+ if x < 0 or y < 0 or w <= 0 or h <= 0:
526
+ continue
527
+
528
+ face_image = frame[y:y+h, x:x+w]
529
+
530
+ if face_image.size == 0:
531
+ continue
532
+
533
+ # Get face embedding
534
+ try:
535
+ embedding = DeepFace.represent(
536
+ img_path=face_image,
537
+ model_name='Facenet',
538
+ enforce_detection=False
539
+ )[0]['embedding']
540
+ except Exception as e:
541
+ logger.warning(f"Embedding extraction error: {e}")
542
+ continue
543
+
544
+ # Face recognition
545
+ color, worker_id, worker_name = self._recognize_face(embedding, face_image)
546
+
547
+ # Draw bounding box and label
548
+ label = f"{worker_name}" + (f" ({worker_id})" if worker_id else "")
549
+ cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
550
+
551
+ # Add background for text
552
+ label_size = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.7, 2)[0]
553
+ cv2.rectangle(frame, (x, y-30), (x + label_size[0], y), color, -1)
554
+ cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
555
+
556
+ except Exception as e:
557
+ logger.warning(f"Error processing face {i}: {e}")
558
  continue
559
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  return frame
561
+
562
  except Exception as e:
563
+ logger.error(f"ERROR in process_frame: {e}")
564
  return frame
565
 
566
+ def _recognize_face(self, embedding: List[float], face_image: np.ndarray) -> Tuple[Tuple[int, int, int], Optional[str], str]:
567
+ """Recognize face and return color, worker_id, and worker_name."""
568
+ if not self.known_face_embeddings:
569
+ return (0, 0, 255), None, "Unknown" # Red for unknown
570
+
571
+ distances = [
572
+ np.linalg.norm(np.array(embedding) - known)
573
+ for known in self.known_face_embeddings
574
+ ]
575
+ min_dist = min(distances)
576
+ match_index = distances.index(min_dist) if min_dist < FACE_RECOGNITION_THRESHOLD else -1
577
+
578
+ if match_index != -1:
579
+ worker_id = self.known_face_ids[match_index]
580
+ worker_name = self.known_face_names[match_index]
581
+
582
+ # Mark attendance
583
+ if self.mark_attendance(worker_id, worker_name):
584
+ self.last_recognition_time[worker_id] = time.time()
585
+
586
+ return (0, 255, 0), worker_id, worker_name # Green for recognized
587
+ else:
588
+ # Try auto-registration
589
+ new_worker = self._register_worker_auto(face_image)
590
+ if new_worker:
591
+ worker_id, worker_name = new_worker[0], new_worker[1]
592
+ if self.mark_attendance(worker_id, worker_name):
593
+ self.last_recognition_time[worker_id] = time.time()
594
+ return (0, 165, 255), worker_id, worker_name # Orange for new
595
+
596
+ return (0, 0, 255), None, "Unknown" # Red for unknown
597
+
598
  def _processing_loop(self, source):
599
+ """Enhanced processing loop with better error handling."""
600
+ video_capture = None
601
+ try:
602
+ video_capture = cv2.VideoCapture(source)
603
+ if not video_capture.isOpened():
604
+ err_msg = f"❌ **Error:** Could not open video source. Please check the source and try again."
605
+ self.error_message = err_msg
606
+ self.is_processing.clear()
607
+ return
608
+
609
+ # Reset statistics
610
+ self.processing_stats = {
611
+ 'frames_processed': 0,
612
+ 'faces_detected': 0,
613
+ 'workers_recognized': 0,
614
+ 'new_registrations': 0
615
+ }
616
+
617
+ while self.is_processing.is_set():
618
+ ret, frame = video_capture.read()
619
+ if not ret:
620
+ break
621
+
622
+ processed_frame = self.process_frame(frame)
623
+
624
+ if not self.frame_queue.full():
625
+ self.frame_queue.put(processed_frame)
626
+
627
+ self.last_processed_frame = processed_frame
628
+ time.sleep(0.05) # Control frame rate
629
+
630
+ except Exception as e:
631
+ logger.error(f"Processing loop error: {e}")
632
+ self.error_message = f"❌ Processing error: {str(e)}"
633
+ finally:
634
+ if video_capture:
635
+ video_capture.release()
636
+
637
+ self.final_log = self.session_log.copy()
638
  self.is_processing.clear()
 
 
 
 
 
 
 
 
 
 
 
639
 
640
  def start_processing(self, source) -> str:
641
+ """Start processing with enhanced validation."""
642
+ if self.is_processing.is_set():
643
+ return "⚠️ Processing is already active."
644
+
645
+ if source is None:
646
+ return "❌ Please provide a valid input source."
647
+
648
  # Reset states for the new session
649
+ self.session_log.clear()
650
+ self.last_recognition_time.clear()
651
+ self.error_message = None
652
+ self.last_processed_frame = None
653
+ self.final_log = None
654
+
655
  self.is_processing.set()
656
+ self.processing_thread = threading.Thread(target=self._processing_loop, args=(source,))
657
+ self.processing_thread.daemon = True
658
  self.processing_thread.start()
659
+
660
+ return f"βœ… Started processing from source: {source}"
661
 
662
  def stop_processing(self) -> str:
663
+ """Stop processing and generate summary."""
664
+ if not self.is_processing.is_set():
665
+ return "⚠️ No processing is currently active."
666
+
667
+ self.is_processing.clear()
668
+
669
+ # Generate processing summary
670
+ stats = self.processing_stats
671
+ summary = f"""βœ… Processing stopped.
672
+ πŸ“Š Session Summary:
673
+ - Frames processed: {stats['frames_processed']}
674
+ - Faces detected: {stats['faces_detected']}
675
+ - Workers recognized: {stats['workers_recognized']}
676
+ - New registrations: {stats['new_registrations']}"""
677
+
678
+ return summary
679
+
680
+ def get_processing_stats(self) -> str:
681
+ """Get current processing statistics."""
682
+ stats = self.processing_stats
683
+ return f"""πŸ“Š **Processing Statistics:**
684
+ - Frames processed: {stats['frames_processed']}
685
+ - Faces detected: {stats['faces_detected']}
686
+ - Workers recognized: {stats['workers_recognized']}
687
+ - New registrations: {stats['new_registrations']}"""
688
+
689
+ def generate_attendance_report(self, start_date: str = None, end_date: str = None) -> pd.DataFrame:
690
+ """Generate attendance report."""
691
+ return self.db.get_attendance_report(start_date, end_date)
692
+
693
+ def export_attendance_csv(self, start_date: str = None, end_date: str = None) -> str:
694
+ """Export attendance data to CSV."""
695
+ try:
696
+ df = self.generate_attendance_report(start_date, end_date)
697
+ if df.empty:
698
+ return "❌ No attendance data found for the specified period."
699
+
700
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
701
+ filename = f"data/reports/attendance_report_{timestamp}.csv"
702
+ df.to_csv(filename, index=False)
703
+
704
+ return f"βœ… Attendance report exported to: {filename}"
705
+ except Exception as e:
706
+ logger.error(f"Error exporting attendance report: {e}")
707
+ return f"❌ Error exporting report: {str(e)}"
708
+
709
+ def backup_system_data(self) -> str:
710
+ """Create a backup of system data."""
711
+ try:
712
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
713
+ backup_dir = f"data/backups/backup_{timestamp}"
714
+ os.makedirs(backup_dir, exist_ok=True)
715
+
716
+ # Backup database
717
+ import shutil
718
+ shutil.copy2(self.db.db_path, f"{backup_dir}/attendance.db")
719
+
720
+ # Backup face images
721
+ if os.path.exists("data/faces"):
722
+ shutil.copytree("data/faces", f"{backup_dir}/faces")
723
+
724
+ return f"βœ… System backup created: {backup_dir}"
725
+ except Exception as e:
726
+ logger.error(f"Backup error: {e}")
727
+ return f"❌ Backup failed: {str(e)}"
728
 
729
  # --- Helper & Reporting ---
730
  def _get_image_caption(self, image: Image.Image) -> str:
731
+ """Generate image caption using Hugging Face API."""
732
+ if not HF_API_TOKEN:
733
+ return "Hugging Face API token not configured."
734
+
735
  try:
736
  buffered = BytesIO()
737
  image.save(buffered, format="JPEG")
738
  img_data = buffered.getvalue()
739
  headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
740
+
741
+ response = requests.post(HF_API_URL, headers=headers, data=img_data, timeout=30)
742
  response.raise_for_status()
743
  result = response.json()
744
+
745
  return result[0].get("generated_text", "No caption found.")
746
+ except requests.exceptions.Timeout:
747
+ return "Caption generation timed out."
748
  except Exception as e:
749
  logger.error(f"Hugging Face API error: {e}")
750
  return "Caption generation failed."
751
 
752
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
753
+ """Upload image to Salesforce with error handling."""
754
+ if not self.sf:
755
+ return None
756
+
757
  try:
758
  buffered = BytesIO()
759
  image.save(buffered, format="JPEG")
760
  encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
761
+
762
+ cv = self.sf.ContentVersion.create({
763
+ 'Title': f'Image_{worker_id}',
764
+ 'PathOnClient': f'{worker_id}.jpg',
765
+ 'VersionData': encoded_image,
766
+ 'FirstPublishLocationId': record_id
767
+ })
768
+
769
+ return f"/{cv['id']}" # Relative URL
770
  except Exception as e:
771
  logger.error(f"Salesforce image upload error: {e}")
772
  return None
773
 
774
  def get_registered_workers_info(self) -> str:
775
+ """Get information about registered workers."""
776
  try:
777
+ workers = self.db.get_all_workers()
778
+ if not workers:
779
+ return "No workers registered."
780
+
781
+ worker_list = "\n".join([
782
+ f"- **{w['name']}** (ID: {w['worker_id']})"
783
+ for w in workers
784
+ ])
785
+
786
+ return f"**πŸ‘₯ Registered Workers ({len(workers)})**\n{worker_list}"
787
+ except Exception as e:
788
+ return f"Error: {e}"
789
+
790
+ def get_system_status(self) -> str:
791
+ """Get comprehensive system status."""
792
+ sf_status = "βœ… Connected" if self.sf else "❌ Offline"
793
+ db_workers = len(self.db.get_all_workers())
794
+
795
+ status = f"""πŸ”§ **System Status:**
796
+ - Salesforce: {sf_status}
797
+ - Registered Workers: {db_workers}
798
+ - Recognition Threshold: {FACE_RECOGNITION_THRESHOLD}
799
+ - Confidence Threshold: {CONFIDENCE_THRESHOLD}
800
+ - Processing: {'βœ… Active' if self.is_processing.is_set() else '⏹️ Stopped'}"""
801
 
802
+ return status
803
+
804
  # --- GRADIO UI ---
805
  attendance_system = AttendanceSystem()
806
+
807
  def create_interface():
808
+ """Create enhanced Gradio interface with new features."""
809
+ with gr.Blocks(theme=gr.themes.Soft(), title="Advanced Attendance System") as demo:
810
  gr.Markdown("# 🎯 Advanced Face Recognition Attendance System")
811
+ gr.Markdown("*Enhanced with offline support, statistics, and reporting features*")
812
+
813
  with gr.Tabs():
814
+ # Main Controls Tab
815
  with gr.Tab("βš™οΈ Controls & Status"):
816
  gr.Markdown("### 1. Choose Input Source & Start Processing")
817
  with gr.Row():
 
826
  start_btn = gr.Button("▢️ Start Processing", variant="primary")
827
  stop_btn = gr.Button("⏹️ Stop Processing", variant="stop")
828
  status_box = gr.Textbox(label="Status", interactive=False, value="System Ready.")
829
+
830
+ gr.Markdown("### 2. System Information")
831
+ with gr.Row():
832
+ system_status = gr.Markdown(value=attendance_system.get_system_status())
833
+ processing_stats = gr.Markdown(value="πŸ“Š **Processing Statistics:** Not started")
834
+ refresh_status_btn = gr.Button("πŸ”„ Refresh Status")
835
+
836
+ gr.Markdown("### 3. View Results in the 'Output & Log' Tab")
837
  gr.Markdown("**🎨 Color Coding:** <font color='green'>Green</font> = Known, <font color='orange'>Orange</font> = New, <font color='red'>Red</font> = Unknown")
838
 
839
+ # Output Tab
840
  with gr.Tab("πŸ“Š Output & Log"):
841
  with gr.Row():
842
  with gr.Column(scale=2):
 
844
  with gr.Column(scale=1):
845
  session_log_display = gr.Markdown(label="πŸ“‹ Session Log", value="System is ready.")
846
 
847
+ # Worker Management Tab
848
  with gr.Tab("πŸ‘€ Worker Management"):
849
  with gr.Row():
850
  with gr.Column():
 
856
  registered_workers_info = gr.Markdown(value=attendance_system.get_registered_workers_info())
857
  refresh_workers_btn = gr.Button("πŸ”„ Refresh List")
858
 
859
+ # Reports Tab
860
+ with gr.Tab("πŸ“ˆ Reports & Analytics"):
861
+ gr.Markdown("### Attendance Reports")
862
+ with gr.Row():
863
+ start_date = gr.Textbox(label="Start Date (YYYY-MM-DD)", placeholder="2024-01-01")
864
+ end_date = gr.Textbox(label="End Date (YYYY-MM-DD)", placeholder="2024-12-31")
865
+
866
+ with gr.Row():
867
+ generate_report_btn = gr.Button("πŸ“Š Generate Report", variant="primary")
868
+ export_csv_btn = gr.Button("πŸ’Ύ Export CSV")
869
+
870
+ report_output = gr.Dataframe(label="Attendance Report")
871
+ export_status = gr.Textbox(label="Export Status", interactive=False)
872
+
873
+ # System Management Tab
874
+ with gr.Tab("πŸ”§ System Management"):
875
+ gr.Markdown("### System Operations")
876
+ with gr.Row():
877
+ backup_btn = gr.Button("πŸ’Ύ Create Backup", variant="secondary")
878
+ backup_status = gr.Textbox(label="Backup Status", interactive=False)
879
+
880
+ gr.Markdown("### System Configuration")
881
+ with gr.Row():
882
+ with gr.Column():
883
+ gr.Markdown(f"**Current Settings:**")
884
+ gr.Markdown(f"- Face Recognition Threshold: {FACE_RECOGNITION_THRESHOLD}")
885
+ gr.Markdown(f"- Confidence Threshold: {CONFIDENCE_THRESHOLD}")
886
+ gr.Markdown(f"- Recognition Cooldown: {RECOGNITION_COOLDOWN}s")
887
+ with gr.Column():
888
+ gr.Markdown("**System Directories:**")
889
+ gr.Markdown("- Faces: `data/faces/`")
890
+ gr.Markdown("- Reports: `data/reports/`")
891
+ gr.Markdown("- Backups: `data/backups/`")
892
+
893
  # --- Event Handlers ---
894
+ def on_tab_select(evt: gr.SelectData):
895
+ return evt.index
896
+
897
  video_tabs.select(fn=on_tab_select, inputs=None, outputs=[selected_tab_index])
898
+
899
  def start_wrapper(tab_index, cam_src, vid_path):
900
  source = cam_src if tab_index == 0 else vid_path
901
+ if source is None:
902
+ return "❌ Please provide an input source."
903
+ return attendance_system.start_processing(source)
904
+
905
+ def stop_wrapper():
906
+ return attendance_system.stop_processing()
907
+
908
+ def refresh_status():
909
+ return attendance_system.get_system_status(), attendance_system.get_processing_stats()
910
+
911
+ def generate_report(start_date_val, end_date_val):
912
+ try:
913
+ df = attendance_system.generate_attendance_report(
914
+ start_date_val if start_date_val else None,
915
+ end_date_val if end_date_val else None
916
+ )
917
+ return df
918
+ except Exception as e:
919
+ logger.error(f"Report generation error: {e}")
920
+ return pd.DataFrame()
921
+
922
+ def export_csv_wrapper(start_date_val, end_date_val):
923
+ return attendance_system.export_attendance_csv(
924
+ start_date_val if start_date_val else None,
925
+ end_date_val if end_date_val else None
926
+ )
927
+
928
+ # Connect event handlers
929
+ start_btn.click(
930
+ fn=start_wrapper,
931
+ inputs=[selected_tab_index, camera_source, video_file],
932
+ outputs=[status_box]
933
+ )
934
+
935
+ stop_btn.click(fn=stop_wrapper, inputs=None, outputs=[status_box])
936
+
937
+ register_btn.click(
938
+ fn=attendance_system.register_worker_manual,
939
+ inputs=[register_image, register_name],
940
+ outputs=[register_output, registered_workers_info]
941
+ )
942
 
943
+ refresh_workers_btn.click(
944
+ fn=attendance_system.get_registered_workers_info,
945
+ outputs=[registered_workers_info]
946
+ )
947
+
948
+ refresh_status_btn.click(
949
+ fn=refresh_status,
950
+ outputs=[system_status, processing_stats]
951
+ )
952
+
953
+ generate_report_btn.click(
954
+ fn=generate_report,
955
+ inputs=[start_date, end_date],
956
+ outputs=[report_output]
957
+ )
958
+
959
+ export_csv_btn.click(
960
+ fn=export_csv_wrapper,
961
+ inputs=[start_date, end_date],
962
+ outputs=[export_status]
963
+ )
964
+
965
+ backup_btn.click(
966
+ fn=attendance_system.backup_system_data,
967
+ outputs=[backup_status]
968
+ )
969
+
970
+ # UI Update Generator
971
  def update_ui_generator():
972
  while True:
973
  if attendance_system.error_message:
974
  yield None, attendance_system.error_message
975
+ time.sleep(2)
976
+ attendance_system.error_message = None
977
  continue
978
+
979
  if attendance_system.is_processing.is_set():
980
  frame, log_md = None, "\n".join(reversed(attendance_system.session_log)) or "Processing..."
981
  try:
982
  if not attendance_system.frame_queue.empty():
983
  frame = attendance_system.frame_queue.get_nowait()
984
+ if frame is not None:
985
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
986
+ except queue.Empty:
987
+ pass
988
  yield frame, log_md
989
  else:
990
  if attendance_system.last_processed_frame is not None:
 
993
  yield final_frame, final_log_md
994
  else:
995
  yield None, "System stopped. Go to 'Controls & Status' to start."
996
+
997
  time.sleep(0.1)
998
 
999
  demo.load(fn=update_ui_generator, outputs=[video_output, session_log_display])
1000
+
1001
  return demo
1002
 
1003
  if __name__ == "__main__":
1004
+ try:
1005
+ app = create_interface()
1006
+ app.queue()
1007
+ app.launch(
1008
+ server_name="0.0.0.0",
1009
+ server_port=7860,
1010
+ show_error=True,
1011
+ debug=True,
1012
+ share=False # Set to True if you want to create a public link
1013
+ )
1014
+ except Exception as e:
1015
+ logger.error(f"Failed to launch application: {e}")
1016
+ print(f"❌ Application failed to start: {e}")