PrashanthB461 commited on
Commit
9f5a84e
·
verified ·
1 Parent(s): 08fbbb7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +68 -58
app.py CHANGED
@@ -19,14 +19,33 @@ from retrying import retry
19
  import uuid
20
  from multiprocessing import Pool, cpu_count
21
  from functools import partial
 
 
22
 
23
  # ========================== # Configuration and Setup # ==========================
24
- os.environ['YOLO_CONFIG_DIR'] = '/tmp/Ultralytics'
25
- os.makedirs('/tmp/Ultralytics', exist_ok=True)
 
 
 
 
 
26
 
27
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
28
  logger = logging.getLogger(__name__)
29
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  # ========================== # ByteTrack Implementation # ==========================
31
  class BYTETracker:
32
  def __init__(self, track_thresh=0.3, track_buffer=30, match_thresh=0.7, frame_rate=30):
@@ -89,7 +108,6 @@ class BYTETracker:
89
  })
90
  else:
91
  # Create new track
92
- # Check if this detection might be the same worker from a different angle
93
  same_worker = False
94
  for worker_id, last_pos in self.last_positions.items():
95
  if self._is_same_worker([x, y], last_pos):
@@ -174,7 +192,7 @@ class BYTETracker:
174
  CONFIG = {
175
  "MODEL_PATH": "yolov8_safety.pt",
176
  "FALLBACK_MODEL": "yolov8n.pt",
177
- "OUTPUT_DIR": "static/output",
178
  "VIOLATION_LABELS": {
179
  0: "no_helmet",
180
  1: "no_harness",
@@ -211,17 +229,17 @@ CONFIG = {
211
  "improper_tool_use": 0.3
212
  },
213
  "MIN_VIOLATION_FRAMES": 1,
214
- "VIOLATION_COOLDOWN": 30.0, # Increased cooldown period
215
  "WORKER_TRACKING_DURATION": 5.0,
216
  "MAX_PROCESSING_TIME": 60,
217
- "FRAME_SKIP": 2, # Skip more frames for faster processing
218
- "BATCH_SIZE": 16,
219
  "PARALLEL_WORKERS": max(1, cpu_count() - 1),
220
  "TRACK_BUFFER": 30,
221
  "TRACK_THRESH": 0.3,
222
  "MATCH_THRESH": 0.7,
223
- "SNAPSHOT_QUALITY": 95, # Higher quality for better visibility
224
- "MAX_WORKER_DISTANCE": 100 # Maximum pixel distance to consider same worker
225
  }
226
 
227
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
@@ -255,7 +273,7 @@ def preprocess_frame(frame):
255
  return frame
256
 
257
  def draw_detections(frame, detections):
258
- """Draw bounding boxes and labels on detection frame with improved visibility"""
259
  result_frame = frame.copy()
260
 
261
  for det in detections:
@@ -271,16 +289,13 @@ def draw_detections(frame, detections):
271
 
272
  color = CONFIG["CLASS_COLORS"].get(label, (0, 0, 255))
273
 
274
- # Draw thicker rectangle with border
275
  cv2.rectangle(result_frame, (x1, y1), (x2, y2), color, 3)
276
 
277
- # Add black background behind text
278
  display_text = f"{CONFIG['DISPLAY_NAMES'].get(label, label)} (Worker {worker_id})"
279
  text_size = cv2.getTextSize(display_text, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)[0]
280
  cv2.rectangle(result_frame, (x1, y1-text_size[1]-10), (x1+text_size[0]+10, y1), (0, 0, 0), -1)
281
  cv2.putText(result_frame, display_text, (x1+5, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
282
 
283
- # Add confidence score
284
  conf_text = f"Conf: {confidence:.2f}"
285
  cv2.putText(result_frame, conf_text, (x1+5, y2+20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
286
 
@@ -296,7 +311,6 @@ def calculate_safety_score(violations):
296
  "improper_tool_use": 25
297
  }
298
 
299
- # Count unique violation types per worker
300
  worker_violations = {}
301
  for v in violations:
302
  worker_id = v.get("worker_id", "Unknown")
@@ -306,7 +320,6 @@ def calculate_safety_score(violations):
306
  worker_violations[worker_id] = set()
307
  worker_violations[worker_id].add(violation_type)
308
 
309
- # Calculate total penalty
310
  total_penalty = 0
311
  for worker_violations_set in worker_violations.values():
312
  worker_penalty = sum(penalties.get(v, 0) for v in worker_violations_set)
@@ -323,26 +336,21 @@ def generate_violation_pdf(violations, score):
323
  pdf_file = BytesIO()
324
  c = canvas.Canvas(pdf_file, pagesize=letter)
325
 
326
- # Title
327
  c.setFont("Helvetica-Bold", 16)
328
  c.drawString(1 * inch, 10 * inch, "Worksite Safety Violation Report")
329
 
330
- # Basic Information
331
  c.setFont("Helvetica", 12)
332
  c.drawString(1 * inch, 9.5 * inch, f"Date: {time.strftime('%Y-%m-%d')}")
333
  c.drawString(1 * inch, 9.2 * inch, f"Time: {time.strftime('%H:%M:%S')}")
334
 
335
- # Safety Score
336
  c.setFont("Helvetica-Bold", 14)
337
  c.drawString(1 * inch, 8.7 * inch, f"Safety Compliance Score: {score}%")
338
 
339
- # Violation Summary
340
  y_position = 8.2 * inch
341
  c.setFont("Helvetica-Bold", 12)
342
  c.drawString(1 * inch, y_position, "Summary:")
343
  y_position -= 0.3 * inch
344
 
345
- # Group violations by worker
346
  worker_violations = {}
347
  for v in violations:
348
  worker_id = v.get("worker_id", "Unknown")
@@ -361,7 +369,6 @@ def generate_violation_pdf(violations, score):
361
  c.drawString(1 * inch, y_position, f"{key}: {value}")
362
  y_position -= 0.25 * inch
363
 
364
- # Detailed Violations by Worker
365
  y_position -= 0.5 * inch
366
  c.setFont("Helvetica-Bold", 12)
367
  c.drawString(1 * inch, y_position, "Violations by Worker:")
@@ -389,7 +396,6 @@ def generate_violation_pdf(violations, score):
389
  c.save()
390
  pdf_file.seek(0)
391
 
392
- # Save PDF file
393
  with open(pdf_path, "wb") as f:
394
  f.write(pdf_file.getvalue())
395
 
@@ -445,7 +451,6 @@ def push_report_to_salesforce(violations, score, pdf_path, pdf_file):
445
  try:
446
  sf = connect_to_salesforce()
447
 
448
- # Format violations for Salesforce
449
  violations_text = ""
450
  for v in violations:
451
  display_name = CONFIG['DISPLAY_NAMES'].get(v.get('violation', 'Unknown'), 'Unknown')
@@ -494,24 +499,26 @@ def push_report_to_salesforce(violations, score, pdf_path, pdf_file):
494
 
495
  return record_id, pdf_url
496
  except Exception as e:
497
- logger.error(f"Salesforce record creation failed: {e}", exc_info=True)
498
- return None, ""
499
 
500
  def process_video(video_data):
501
  """Process video to detect safety violations"""
502
  try:
503
- os.makedirs(CONFIG["OUTPUT_DIR"], exist_ok=True)
504
- logger.info(f"Output directory ensured: {CONFIG['OUTPUT_DIR']}")
 
505
 
506
- video_path = os.path.join(CONFIG["OUTPUT_DIR"], f"temp_{int(time.time())}.mp4")
507
- with open(video_path, "wb") as f:
 
508
  f.write(video_data)
509
  logger.info(f"Video saved: {video_path}")
510
 
 
511
  cap = cv2.VideoCapture(video_path)
512
  if not cap.isOpened():
513
- os.remove(video_path)
514
- raise ValueError("Could not open video file")
515
 
516
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
517
  fps = cap.get(cv2.CAP_PROP_FPS) or 30
@@ -520,6 +527,10 @@ def process_video(video_data):
520
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
521
  logger.info(f"Video properties: {duration:.2f}s, {total_frames} frames, {fps:.1f} FPS, {width}x{height}")
522
 
 
 
 
 
523
  tracker = BYTETracker(
524
  track_thresh=CONFIG["TRACK_THRESH"],
525
  track_buffer=CONFIG["TRACK_BUFFER"],
@@ -527,8 +538,7 @@ def process_video(video_data):
527
  frame_rate=fps
528
  )
529
 
530
- # Track unique violations by worker ID
531
- unique_violations = {} # {worker_id: {violation_type: first_detection_time}}
532
  snapshots = []
533
  start_time = time.time()
534
  frame_skip = CONFIG["FRAME_SKIP"]
@@ -545,11 +555,11 @@ def process_video(video_data):
545
 
546
  ret, frame = cap.read()
547
  if not ret:
 
548
  break
549
 
550
  frame = preprocess_frame(frame)
551
 
552
- # Skip frames if needed
553
  for _ in range(frame_skip - 1):
554
  if not cap.grab():
555
  break
@@ -559,15 +569,19 @@ def process_video(video_data):
559
  processed_frames += 1
560
 
561
  if not batch_frames:
 
562
  break
563
 
564
  # Process batch with YOLO model
565
- results = model(batch_frames, device=device, conf=0.1, verbose=False)
566
-
 
 
 
 
567
  for i, (result, frame_idx) in enumerate(zip(results, batch_indices)):
568
  current_time = frame_idx / fps
569
 
570
- # Update progress every second
571
  if time.time() - start_time > 1.0:
572
  progress = (processed_frames / total_frames) * 100
573
  yield f"Processing video... {progress:.1f}% complete (Frame {processed_frames}/{total_frames})", "", "", "", ""
@@ -603,7 +617,6 @@ def process_video(video_data):
603
  np.array([t["cls"] for t in track_inputs])
604
  )
605
 
606
- # Process tracked objects for violations
607
  for obj in tracked_objects:
608
  worker_id = obj['id']
609
  label = CONFIG["VIOLATION_LABELS"].get(int(obj['cls']), None)
@@ -613,16 +626,12 @@ def process_video(video_data):
613
  if label is None:
614
  continue
615
 
616
- # Initialize worker if not seen before
617
  if worker_id not in unique_violations:
618
  unique_violations[worker_id] = {}
619
 
620
- # Check if this violation type has been recorded for this worker
621
  if label not in unique_violations[worker_id]:
622
- # This is a new violation type for this worker
623
  unique_violations[worker_id][label] = current_time
624
 
625
- # Create detection object
626
  detection = {
627
  "worker_id": worker_id,
628
  "violation": label,
@@ -631,11 +640,9 @@ def process_video(video_data):
631
  "timestamp": current_time
632
  }
633
 
634
- # Take snapshot for the new violation
635
  snapshot_frame = batch_frames[i].copy()
636
  snapshot_frame = draw_detections(snapshot_frame, [detection])
637
 
638
- # Add timestamp to snapshot
639
  cv2.putText(
640
  snapshot_frame,
641
  f"Time: {current_time:.2f}s",
@@ -646,7 +653,6 @@ def process_video(video_data):
646
  2
647
  )
648
 
649
- # Save snapshot with high quality
650
  snapshot_filename = f"violation_{label}_worker{worker_id}_{int(current_time*100)}.jpg"
651
  snapshot_path = os.path.join(CONFIG["OUTPUT_DIR"], snapshot_filename)
652
 
@@ -666,6 +672,7 @@ def process_video(video_data):
666
 
667
  logger.info(f"Captured snapshot for {label} violation by worker {worker_id} at {current_time:.2f}s")
668
 
 
669
  cap.release()
670
  if os.path.exists(video_path):
671
  os.remove(video_path)
@@ -673,14 +680,14 @@ def process_video(video_data):
673
  processing_time = time.time() - start_time
674
  logger.info(f"Processing complete in {processing_time:.2f}s")
675
 
676
- # Convert tracked violations to final violation list
677
  violations = []
678
  for worker_id, worker_violations in unique_violations.items():
679
  for label, detection_time in worker_violations.items():
680
  violation = {
681
  "worker_id": worker_id,
682
  "violation": label,
683
- "timestamp": detection_time
 
684
  }
685
  violations.append(violation)
686
 
@@ -689,16 +696,12 @@ def process_video(video_data):
689
  yield "No violations detected in the video.", "Safety Score: 100%", "No snapshots captured.", "N/A", "N/A"
690
  return
691
 
692
- # Calculate safety score
693
  score = calculate_safety_score(violations)
694
-
695
- # Generate PDF report
696
  pdf_path, pdf_url, pdf_file = generate_violation_pdf(violations, score)
697
 
698
- # Push report to Salesforce
699
- report_id, final_pdf_url = push_report_to_salesforce(violations, score, pdf_path, pdf_file)
700
 
701
- # Format violations table for display
702
  violation_table = "| Violation | Worker ID | Time (s) | Confidence |\n"
703
  violation_table += "|-----------|-----------|----------|------------|\n"
704
 
@@ -710,7 +713,6 @@ def process_video(video_data):
710
 
711
  violation_table += f"| {display_name} | {worker_id} | {timestamp:.2f} | {confidence:.2f} |\n"
712
 
713
- # Format snapshots for display
714
  snapshots_text = ""
715
  for s in snapshots:
716
  display_name = CONFIG["DISPLAY_NAMES"].get(s["violation"], "Unknown")
@@ -727,15 +729,19 @@ def process_video(video_data):
727
  violation_table,
728
  f"Safety Score: {score}%",
729
  snapshots_text,
730
- f"Salesforce Record ID: {report_id or 'N/A'}",
731
- final_pdf_url or "N/A"
732
  )
733
 
734
  except Exception as e:
735
- logger.error(f"Error processing video: {e}", exc_info=True)
736
  if 'video_path' in locals() and os.path.exists(video_path):
737
  os.remove(video_path)
738
- yield f"Error processing video: {e}", "", "", "", ""
 
 
 
 
739
 
740
  def gradio_interface(video_file):
741
  """Gradio interface for the video processing"""
@@ -746,6 +752,10 @@ def gradio_interface(video_file):
746
  with open(video_file, "rb") as f:
747
  video_data = f.read()
748
 
 
 
 
 
749
  for status, score, snapshots_text, record_id, details_url in process_video(video_data):
750
  yield status, score, snapshots_text, record_id, details_url
751
 
 
19
  import uuid
20
  from multiprocessing import Pool, cpu_count
21
  from functools import partial
22
+ import tempfile
23
+ import shutil
24
 
25
  # ========================== # Configuration and Setup # ==========================
26
+ # Use a temporary directory for storage
27
+ TEMP_DIR = tempfile.mkdtemp(prefix="Ultralytics_")
28
+ os.environ['YOLO_CONFIG_DIR'] = TEMP_DIR
29
+
30
+ # Ensure output directory exists within temp directory
31
+ OUTPUT_DIR = os.path.join(TEMP_DIR, "output")
32
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
33
 
34
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
35
  logger = logging.getLogger(__name__)
36
 
37
+ # Check for FFmpeg availability
38
+ def check_ffmpeg():
39
+ try:
40
+ subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
41
+ logger.info("FFmpeg is available.")
42
+ return True
43
+ except (subprocess.CalledProcessError, FileNotFoundError):
44
+ logger.error("FFmpeg is not installed or not found in PATH. Video processing may fail.")
45
+ return False
46
+
47
+ FFMPEG_AVAILABLE = check_ffmpeg()
48
+
49
  # ========================== # ByteTrack Implementation # ==========================
50
  class BYTETracker:
51
  def __init__(self, track_thresh=0.3, track_buffer=30, match_thresh=0.7, frame_rate=30):
 
108
  })
109
  else:
110
  # Create new track
 
111
  same_worker = False
112
  for worker_id, last_pos in self.last_positions.items():
113
  if self._is_same_worker([x, y], last_pos):
 
192
  CONFIG = {
193
  "MODEL_PATH": "yolov8_safety.pt",
194
  "FALLBACK_MODEL": "yolov8n.pt",
195
+ "OUTPUT_DIR": OUTPUT_DIR,
196
  "VIOLATION_LABELS": {
197
  0: "no_helmet",
198
  1: "no_harness",
 
229
  "improper_tool_use": 0.3
230
  },
231
  "MIN_VIOLATION_FRAMES": 1,
232
+ "VIOLATION_COOLDOWN": 30.0,
233
  "WORKER_TRACKING_DURATION": 5.0,
234
  "MAX_PROCESSING_TIME": 60,
235
+ "FRAME_SKIP": 2,
236
+ "BATCH_SIZE": 8, # Reduced batch size to lower memory usage
237
  "PARALLEL_WORKERS": max(1, cpu_count() - 1),
238
  "TRACK_BUFFER": 30,
239
  "TRACK_THRESH": 0.3,
240
  "MATCH_THRESH": 0.7,
241
+ "SNAPSHOT_QUALITY": 95,
242
+ "MAX_WORKER_DISTANCE": 100
243
  }
244
 
245
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
273
  return frame
274
 
275
  def draw_detections(frame, detections):
276
+ """Draw bounding boxes and labels on detection frame"""
277
  result_frame = frame.copy()
278
 
279
  for det in detections:
 
289
 
290
  color = CONFIG["CLASS_COLORS"].get(label, (0, 0, 255))
291
 
 
292
  cv2.rectangle(result_frame, (x1, y1), (x2, y2), color, 3)
293
 
 
294
  display_text = f"{CONFIG['DISPLAY_NAMES'].get(label, label)} (Worker {worker_id})"
295
  text_size = cv2.getTextSize(display_text, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)[0]
296
  cv2.rectangle(result_frame, (x1, y1-text_size[1]-10), (x1+text_size[0]+10, y1), (0, 0, 0), -1)
297
  cv2.putText(result_frame, display_text, (x1+5, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
298
 
 
299
  conf_text = f"Conf: {confidence:.2f}"
300
  cv2.putText(result_frame, conf_text, (x1+5, y2+20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
301
 
 
311
  "improper_tool_use": 25
312
  }
313
 
 
314
  worker_violations = {}
315
  for v in violations:
316
  worker_id = v.get("worker_id", "Unknown")
 
320
  worker_violations[worker_id] = set()
321
  worker_violations[worker_id].add(violation_type)
322
 
 
323
  total_penalty = 0
324
  for worker_violations_set in worker_violations.values():
325
  worker_penalty = sum(penalties.get(v, 0) for v in worker_violations_set)
 
336
  pdf_file = BytesIO()
337
  c = canvas.Canvas(pdf_file, pagesize=letter)
338
 
 
339
  c.setFont("Helvetica-Bold", 16)
340
  c.drawString(1 * inch, 10 * inch, "Worksite Safety Violation Report")
341
 
 
342
  c.setFont("Helvetica", 12)
343
  c.drawString(1 * inch, 9.5 * inch, f"Date: {time.strftime('%Y-%m-%d')}")
344
  c.drawString(1 * inch, 9.2 * inch, f"Time: {time.strftime('%H:%M:%S')}")
345
 
 
346
  c.setFont("Helvetica-Bold", 14)
347
  c.drawString(1 * inch, 8.7 * inch, f"Safety Compliance Score: {score}%")
348
 
 
349
  y_position = 8.2 * inch
350
  c.setFont("Helvetica-Bold", 12)
351
  c.drawString(1 * inch, y_position, "Summary:")
352
  y_position -= 0.3 * inch
353
 
 
354
  worker_violations = {}
355
  for v in violations:
356
  worker_id = v.get("worker_id", "Unknown")
 
369
  c.drawString(1 * inch, y_position, f"{key}: {value}")
370
  y_position -= 0.25 * inch
371
 
 
372
  y_position -= 0.5 * inch
373
  c.setFont("Helvetica-Bold", 12)
374
  c.drawString(1 * inch, y_position, "Violations by Worker:")
 
396
  c.save()
397
  pdf_file.seek(0)
398
 
 
399
  with open(pdf_path, "wb") as f:
400
  f.write(pdf_file.getvalue())
401
 
 
451
  try:
452
  sf = connect_to_salesforce()
453
 
 
454
  violations_text = ""
455
  for v in violations:
456
  display_name = CONFIG['DISPLAY_NAMES'].get(v.get('violation', 'Unknown'), 'Unknown')
 
499
 
500
  return record_id, pdf_url
501
  except Exception as e:
502
+ logger.error(f"Salesforce record creation failed: {e}")
503
+ return "N/A", "Salesforce integration failed."
504
 
505
  def process_video(video_data):
506
  """Process video to detect safety violations"""
507
  try:
508
+ # Validate video data
509
+ if not video_data:
510
+ raise ValueError("Empty video data provided.")
511
 
512
+ # Save video to a temporary file
513
+ video_fd, video_path = tempfile.mkstemp(suffix=".mp4", dir=TEMP_DIR)
514
+ with os.fdopen(video_fd, "wb") as f:
515
  f.write(video_data)
516
  logger.info(f"Video saved: {video_path}")
517
 
518
+ # Open video with OpenCV
519
  cap = cv2.VideoCapture(video_path)
520
  if not cap.isOpened():
521
+ raise ValueError("Could not open video file. Ensure the video format is supported (e.g., MP4) and FFmpeg is installed.")
 
522
 
523
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
524
  fps = cap.get(cv2.CAP_PROP_FPS) or 30
 
527
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
528
  logger.info(f"Video properties: {duration:.2f}s, {total_frames} frames, {fps:.1f} FPS, {width}x{height}")
529
 
530
+ # Check if video is empty
531
+ if total_frames <= 0:
532
+ raise ValueError("Video has no frames.")
533
+
534
  tracker = BYTETracker(
535
  track_thresh=CONFIG["TRACK_THRESH"],
536
  track_buffer=CONFIG["TRACK_BUFFER"],
 
538
  frame_rate=fps
539
  )
540
 
541
+ unique_violations = {}
 
542
  snapshots = []
543
  start_time = time.time()
544
  frame_skip = CONFIG["FRAME_SKIP"]
 
555
 
556
  ret, frame = cap.read()
557
  if not ret:
558
+ logger.warning(f"Failed to read frame {frame_idx}. Skipping.")
559
  break
560
 
561
  frame = preprocess_frame(frame)
562
 
 
563
  for _ in range(frame_skip - 1):
564
  if not cap.grab():
565
  break
 
569
  processed_frames += 1
570
 
571
  if not batch_frames:
572
+ logger.info("No more frames to process.")
573
  break
574
 
575
  # Process batch with YOLO model
576
+ try:
577
+ results = model(batch_frames, device=device, conf=0.1, verbose=False)
578
+ except Exception as e:
579
+ logger.error(f"Model inference failed: {e}")
580
+ raise ValueError(f"Failed to process video frames with YOLO model: {str(e)}")
581
+
582
  for i, (result, frame_idx) in enumerate(zip(results, batch_indices)):
583
  current_time = frame_idx / fps
584
 
 
585
  if time.time() - start_time > 1.0:
586
  progress = (processed_frames / total_frames) * 100
587
  yield f"Processing video... {progress:.1f}% complete (Frame {processed_frames}/{total_frames})", "", "", "", ""
 
617
  np.array([t["cls"] for t in track_inputs])
618
  )
619
 
 
620
  for obj in tracked_objects:
621
  worker_id = obj['id']
622
  label = CONFIG["VIOLATION_LABELS"].get(int(obj['cls']), None)
 
626
  if label is None:
627
  continue
628
 
 
629
  if worker_id not in unique_violations:
630
  unique_violations[worker_id] = {}
631
 
 
632
  if label not in unique_violations[worker_id]:
 
633
  unique_violations[worker_id][label] = current_time
634
 
 
635
  detection = {
636
  "worker_id": worker_id,
637
  "violation": label,
 
640
  "timestamp": current_time
641
  }
642
 
 
643
  snapshot_frame = batch_frames[i].copy()
644
  snapshot_frame = draw_detections(snapshot_frame, [detection])
645
 
 
646
  cv2.putText(
647
  snapshot_frame,
648
  f"Time: {current_time:.2f}s",
 
653
  2
654
  )
655
 
 
656
  snapshot_filename = f"violation_{label}_worker{worker_id}_{int(current_time*100)}.jpg"
657
  snapshot_path = os.path.join(CONFIG["OUTPUT_DIR"], snapshot_filename)
658
 
 
672
 
673
  logger.info(f"Captured snapshot for {label} violation by worker {worker_id} at {current_time:.2f}s")
674
 
675
+ # Ensure resources are released
676
  cap.release()
677
  if os.path.exists(video_path):
678
  os.remove(video_path)
 
680
  processing_time = time.time() - start_time
681
  logger.info(f"Processing complete in {processing_time:.2f}s")
682
 
 
683
  violations = []
684
  for worker_id, worker_violations in unique_violations.items():
685
  for label, detection_time in worker_violations.items():
686
  violation = {
687
  "worker_id": worker_id,
688
  "violation": label,
689
+ "timestamp": detection_time,
690
+ "confidence": next((s["confidence"] for s in snapshots if s["worker_id"] == worker_id and s["violation"] == label), 0.0)
691
  }
692
  violations.append(violation)
693
 
 
696
  yield "No violations detected in the video.", "Safety Score: 100%", "No snapshots captured.", "N/A", "N/A"
697
  return
698
 
 
699
  score = calculate_safety_score(violations)
 
 
700
  pdf_path, pdf_url, pdf_file = generate_violation_pdf(violations, score)
701
 
702
+ # Push to Salesforce with fallback
703
+ record_id, final_pdf_url = push_report_to_salesforce(violations, score, pdf_path, pdf_file)
704
 
 
705
  violation_table = "| Violation | Worker ID | Time (s) | Confidence |\n"
706
  violation_table += "|-----------|-----------|----------|------------|\n"
707
 
 
713
 
714
  violation_table += f"| {display_name} | {worker_id} | {timestamp:.2f} | {confidence:.2f} |\n"
715
 
 
716
  snapshots_text = ""
717
  for s in snapshots:
718
  display_name = CONFIG["DISPLAY_NAMES"].get(s["violation"], "Unknown")
 
729
  violation_table,
730
  f"Safety Score: {score}%",
731
  snapshots_text,
732
+ f"Salesforce Record ID: {record_id}",
733
+ final_pdf_url
734
  )
735
 
736
  except Exception as e:
737
+ logger.error(f"Error processing video: {str(e)}", exc_info=True)
738
  if 'video_path' in locals() and os.path.exists(video_path):
739
  os.remove(video_path)
740
+ yield f"Error processing video: {str(e)}", "", "", "", ""
741
+ finally:
742
+ # Clean up temporary directory
743
+ if os.path.exists(TEMP_DIR):
744
+ shutil.rmtree(TEMP_DIR, ignore_errors=True)
745
 
746
  def gradio_interface(video_file):
747
  """Gradio interface for the video processing"""
 
752
  with open(video_file, "rb") as f:
753
  video_data = f.read()
754
 
755
+ # Validate FFmpeg availability
756
+ if not FFMPEG_AVAILABLE:
757
+ return "FFmpeg is not available in the environment. Please install FFmpeg to process videos.", "", "", "", ""
758
+
759
  for status, score, snapshots_text, record_id, details_url in process_video(video_data):
760
  yield status, score, snapshots_text, record_id, details_url
761