PrashanthB461 commited on
Commit
1ffc450
·
verified ·
1 Parent(s): 2e517fe

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +513 -205
app.py CHANGED
@@ -38,80 +38,147 @@ def check_ffmpeg():
38
 
39
  FFMPEG_AVAILABLE = check_ffmpeg()
40
 
41
- # ========================== # Optimized ByteTrack Implementation # ==========================
42
  class BYTETracker:
43
- def __init__(self, track_thresh=0.3, track_buffer=30, match_thresh=0.5, frame_rate=30):
44
  self.track_thresh = track_thresh
45
- self.track_buffer = track_buffer # Reduced buffer for faster tracking
46
- self.match_thresh = match_thresh
47
  self.frame_rate = frame_rate
48
  self.next_id = 1
49
  self.tracks = {}
 
50
  self.last_positions = {}
 
51
 
52
  def update(self, dets, scores, cls):
53
  tracks = []
54
  current_time = time.time()
55
-
56
  # Prune stale tracks
57
- stale_ids = [tid for tid, track in self.tracks.items()
58
- if current_time - track['last_seen'] > self.track_buffer / self.frame_rate]
59
-
60
- for tid in stale_ids:
61
- del self.tracks[tid]
62
- if tid in self.last_positions:
63
- del self.last_positions[tid]
64
-
65
- for det, score, cl in zip(dets, scores, cls):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  if score < self.track_thresh:
67
  continue
68
-
69
  x, y, w, h = det
70
  matched = False
71
-
72
- # Find best match among active tracks
73
- best_match = None
74
  best_iou = 0
75
- for tid, track in self.tracks.items():
76
- tx, ty, tw, th = track['bbox']
 
 
 
77
  iou = self._calculate_iou([x, y, w, h], [tx, ty, tw, th])
 
78
  if iou > self.match_thresh and iou > best_iou:
79
  best_iou = iou
80
- best_match = tid
81
-
82
- if best_match is not None:
83
- # Update existing track
84
- self.tracks[best_match].update({
85
  'bbox': [x, y, w, h],
86
  'score': score,
87
  'cls': cl,
88
  'last_seen': current_time
89
  })
90
- self.last_positions[best_match] = [x, y]
 
 
 
 
91
  tracks.append({
92
- 'id': best_match,
93
  'bbox': [x, y, w, h],
94
  'score': score,
95
  'cls': cl
96
  })
97
  else:
98
- # Create new track
99
- tid = self.next_id
100
- self.next_id += 1
101
- self.tracks[tid] = {
102
- 'bbox': [x, y, w, h],
103
- 'score': score,
104
- 'cls': cl,
105
- 'last_seen': current_time
106
- }
107
- self.last_positions[tid] = [x, y]
108
- tracks.append({
109
- 'id': tid,
110
- 'bbox': [x, y, w, h],
111
- 'score': score,
112
- 'cls': cl
113
- })
114
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  return tracks
116
 
117
  def _calculate_iou(self, box1, box2):
@@ -126,7 +193,14 @@ class BYTETracker:
126
  intersection_area = (x_right - x_left) * (y_bottom - y_top)
127
  box1_area = w1 * h1
128
  box2_area = w2 * h2
129
- return intersection_area / (box1_area + box2_area - intersection_area)
 
 
 
 
 
 
 
130
 
131
  # ========================== # Optimized Configuration # ==========================
132
  CONFIG = {
@@ -169,17 +243,17 @@ CONFIG = {
169
  },
170
  "MIN_VIOLATION_FRAMES": 1,
171
  "VIOLATION_COOLDOWN": 30.0,
172
- "WORKER_TRACKING_DURATION": 5.0,
173
  "MAX_PROCESSING_TIME": 60,
174
- "FRAME_SKIP": 3, # Process every 3rd frame for speed
175
- "BATCH_SIZE": 8, # Increased batch size for better GPU utilization
176
  "PARALLEL_WORKERS": max(1, cpu_count() - 1),
177
- "TRACK_BUFFER": 30, # Reduced buffer for faster tracking
178
  "TRACK_THRESH": 0.3,
179
- "MATCH_THRESH": 0.5,
180
- "SNAPSHOT_QUALITY": 85, # Reduced quality for faster processing
181
- "MAX_WORKER_DISTANCE": 150,
182
- "TARGET_RESOLUTION": (320, 320) # Reduced resolution for faster processing
183
  }
184
 
185
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
@@ -199,7 +273,7 @@ def load_model():
199
 
200
  model = YOLO(model_path).to(device)
201
  if device.type == "cuda":
202
- model.model.half() # Use half precision for faster inference
203
  logger.info(f"Model classes: {model.names}")
204
  return model
205
  except Exception as e:
@@ -208,25 +282,39 @@ def load_model():
208
 
209
  model = load_model()
210
 
211
- # ========================== # Optimized Helper Functions # ==========================
212
  def preprocess_frame(frame):
213
- frame = cv2.resize(frame, CONFIG["TARGET_RESOLUTION"], interpolation=cv2.INTER_LINEAR)
 
 
214
  return frame
215
 
216
  def draw_detections(frame, detections):
217
  result_frame = frame.copy()
 
218
  for det in detections:
219
  label = det.get("violation", "Unknown")
 
220
  x, y, w, h = det.get("bounding_box", [0, 0, 0, 0])
221
  worker_id = det.get("worker_id", "Unknown")
222
 
223
- x1, y1 = int(x - w/2), int(y - h/2)
224
- x2, y2 = int(x + w/2), int(y + h/2)
 
 
 
225
  color = CONFIG["CLASS_COLORS"].get(label, (0, 0, 255))
226
-
227
- cv2.rectangle(result_frame, (x1, y1), (x2, y2), color, 2)
 
228
  display_text = f"{CONFIG['DISPLAY_NAMES'].get(label, label)} (Worker {worker_id})"
229
- cv2.putText(result_frame, display_text, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 1)
 
 
 
 
 
 
230
  return result_frame
231
 
232
  def calculate_safety_score(violations):
@@ -237,16 +325,23 @@ def calculate_safety_score(violations):
237
  "unsafe_zone": 35,
238
  "improper_tool_use": 25
239
  }
240
-
241
  worker_violations = {}
242
  for v in violations:
243
  worker_id = v.get("worker_id", "Unknown")
244
  violation_type = v.get("violation", "Unknown")
 
245
  if worker_id not in worker_violations:
246
  worker_violations[worker_id] = set()
247
  worker_violations[worker_id].add(violation_type)
248
-
249
- return max(0, 100 - sum(penalties.get(v, 0) for violations_set in worker_violations.values() for v in violations_set))
 
 
 
 
 
 
250
 
251
  def generate_violation_pdf(violations, score, output_dir):
252
  try:
@@ -330,11 +425,39 @@ def connect_to_salesforce():
330
  try:
331
  sf = Salesforce(**CONFIG["SF_CREDENTIALS"])
332
  logger.info("Connected to Salesforce")
 
333
  return sf
334
  except Exception as e:
335
  logger.error(f"Salesforce connection failed: {e}")
336
  raise
337
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  def push_report_to_salesforce(violations, score, pdf_path, pdf_file):
339
  try:
340
  sf = connect_to_salesforce()
@@ -361,6 +484,8 @@ def push_report_to_salesforce(violations, score, pdf_path, pdf_file):
361
  "PDF_Report_URL__c": pdf_url
362
  }
363
 
 
 
364
  try:
365
  record = sf.Safety_Video_Report__c.create(record_data)
366
  logger.info(f"Created Safety_Video_Report__c record: {record['id']}")
@@ -376,8 +501,11 @@ def push_report_to_salesforce(violations, score, pdf_path, pdf_file):
376
  if uploaded_url:
377
  try:
378
  sf.Safety_Video_Report__c.update(record_id, {"PDF_Report_URL__c": uploaded_url})
 
379
  except Exception as e:
 
380
  sf.Account.update(record_id, {"Description": uploaded_url})
 
381
  pdf_url = uploaded_url
382
 
383
  return record_id, pdf_url
@@ -385,47 +513,68 @@ def push_report_to_salesforce(violations, score, pdf_path, pdf_file):
385
  logger.error(f"Salesforce record creation failed: {e}")
386
  return "N/A", "Salesforce integration failed."
387
 
388
- def upload_pdf_to_salesforce(sf, pdf_file, report_id):
389
- try:
390
- encoded_pdf = base64.b64encode(pdf_file.getvalue()).decode('utf-8')
391
- content_version_data = {
392
- "Title": f"Safety_Violation_Report_{int(time.time())}",
393
- "PathOnClient": f"safety_violation_{int(time.time())}.pdf",
394
- "VersionData": encoded_pdf,
395
- "FirstPublishLocationId": report_id
396
- }
397
- content_version = sf.ContentVersion.create(content_version_data)
398
- result = sf.query(f"SELECT Id, ContentDocumentId FROM ContentVersion WHERE Id = '{content_version['id']}'")
399
-
400
- if not result['records']:
401
- return ""
402
-
403
- file_url = f"https://{sf.sf_instance}/sfc/servlet.shepherd/version/download/{content_version['id']}"
404
- return file_url
405
- except Exception as e:
406
- logger.error(f"Error uploading PDF to Salesforce: {e}")
407
- return ""
 
 
408
 
409
- # ========================== # Fast Video Processing # ==========================
410
  def process_video(video_data, temp_dir):
411
  video_path = None
412
  output_dir = os.path.join(temp_dir, "output")
413
  os.makedirs(output_dir, exist_ok=True)
414
-
 
415
  try:
416
- # Write video to temp file
 
 
 
 
 
 
417
  with tempfile.NamedTemporaryFile(suffix=".mp4", dir=temp_dir, delete=False) as temp_file:
418
  temp_file.write(video_data)
 
419
  video_path = temp_file.name
 
420
 
421
- cap = cv2.VideoCapture(video_path)
422
- if not cap.isOpened():
423
- raise ValueError("Could not open video file")
 
 
 
 
 
 
424
 
425
- fps = cap.get(cv2.CAP_PROP_FPS) or 30
426
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
 
 
 
 
 
 
427
  if total_frames <= 0:
428
- raise ValueError("Video has no frames")
429
 
430
  tracker = BYTETracker(
431
  track_thresh=CONFIG["TRACK_THRESH"],
@@ -436,167 +585,326 @@ def process_video(video_data, temp_dir):
436
 
437
  worker_id_mapping = {}
438
  unique_violations = {}
439
- worker_counter = 1
 
 
440
  frame_skip = CONFIG["FRAME_SKIP"]
441
  processed_frames = 0
442
- start_time = time.time()
 
443
 
444
- while True:
445
- # Skip frames for faster processing
446
- for _ in range(frame_skip):
447
- if not cap.grab():
 
 
 
448
  break
449
-
450
- ret, frame = cap.retrieve()
451
- if not ret:
452
- break
 
 
 
 
 
 
 
 
 
 
 
453
 
454
- frame = preprocess_frame(frame)
455
- frame_tensor = torch.from_numpy(frame).permute(2, 0, 1).float().to(device) / 255.0
456
- if device.type == "cuda":
457
- frame_tensor = frame_tensor.half()
458
 
459
- results = model(frame_tensor.unsqueeze(0), verbose=False)[0]
460
-
461
- current_time = cap.get(cv2.CAP_PROP_POS_MSEC) / 1000
462
- boxes = results.boxes
463
- detections = []
464
-
465
- for box in boxes:
466
- cls = int(box.cls)
467
- conf = float(box.conf)
468
- label = CONFIG["VIOLATION_LABELS"].get(cls, None)
469
- if label is None or conf < CONFIG["CONFIDENCE_THRESHOLDS"].get(label, 0.25):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  continue
471
-
472
- detections.append({
473
- "bbox": box.xywh.cpu().numpy()[0],
474
- "conf": conf,
475
- "cls": cls
476
- })
477
-
478
- if detections:
479
- tracked_objs = tracker.update(
480
- np.array([d["bbox"] for d in detections]),
481
- np.array([d["conf"] for d in detections]),
482
- np.array([d["cls"] for d in detections])
483
  )
484
-
485
- for obj in tracked_objs:
486
- tid = obj['id']
487
- if tid not in worker_id_mapping:
488
- worker_id_mapping[tid] = worker_counter
 
 
 
 
 
 
 
 
489
  worker_counter += 1
490
-
491
- worker_id = worker_id_mapping[tid]
492
- label = CONFIG["VIOLATION_LABELS"].get(int(obj['cls']), "Unknown")
493
  violation_key = (worker_id, label)
494
-
495
  if violation_key not in unique_violations:
496
- unique_violations[violation_key] = {
497
- "timestamp": current_time,
498
- "frame_idx": processed_frames,
499
- "confidence": obj['score']
500
- }
501
-
502
- processed_frames += 1
503
- if time.time() - start_time > 0.5: # Update progress every 0.5s
504
- progress = (processed_frames * frame_skip / total_frames) * 100
505
- yield f"Processing... {min(100, progress):.1f}%", "", "", "", ""
506
- start_time = time.time()
507
 
508
  cap.release()
509
-
510
- # Prepare violations list
511
- violations = [{
512
- "worker_id": worker_id,
513
- "violation": label,
514
- "timestamp": info["timestamp"],
515
- "confidence": info["confidence"],
516
- "frame_idx": info["frame_idx"]
517
- } for (worker_id, label), info in unique_violations.items()]
 
 
 
 
 
518
 
519
  if not violations:
520
- yield "No violations detected.", "Safety Score: 100%", "", "N/A", "N/A"
 
521
  return
522
 
523
- # Generate snapshots (only for first violation of each type per worker)
524
  snapshots = []
525
  cap = cv2.VideoCapture(video_path)
526
- snapshots_to_capture = min(10, len(violations)) # Limit to 10 snapshots
527
- for violation in violations[:snapshots_to_capture]:
528
- cap.set(cv2.CAP_PROP_POS_FRAMES, violation["frame_idx"])
529
  ret, frame = cap.read()
530
- if ret:
531
- frame = draw_detections(frame, [violation])
532
- snapshot_path = os.path.join(output_dir,
533
- f"violation_{violation['worker_id']}_{violation['violation']}_{int(violation['timestamp']*100)}.jpg")
534
- cv2.imwrite(snapshot_path, frame, [cv2.IMWRITE_JPEG_QUALITY, CONFIG["SNAPSHOT_QUALITY"]])
535
- snapshots.append({
536
- "path": snapshot_path,
537
- "url": f"{CONFIG['PUBLIC_URL_BASE']}{os.path.basename(snapshot_path)}",
538
- "worker_id": violation["worker_id"],
539
- "violation": violation["violation"]
540
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541
  cap.release()
542
 
543
- # Generate report
544
  score = calculate_safety_score(violations)
545
  pdf_path, pdf_url, pdf_file = generate_violation_pdf(violations, score, output_dir)
 
546
  record_id, final_pdf_url = push_report_to_salesforce(violations, score, pdf_path, pdf_file)
547
-
548
- # Worker summary
549
  worker_summary = {}
550
  for v in violations:
551
- if v["worker_id"] not in worker_summary:
552
- worker_summary[v["worker_id"]] = {"count": 0, "types": set()}
553
- worker_summary[v["worker_id"]]["count"] += 1
554
- worker_summary[v["worker_id"]]["types"].add(v["violation"])
555
-
556
- # Generate output
557
- violation_table = "## Worker Safety Summary\n"
558
- violation_table += "| Worker ID | Violations | Types |\n|-----------|------------|-------|\n"
 
 
 
 
 
 
559
  for worker_id, info in worker_summary.items():
560
- types = ", ".join([CONFIG["DISPLAY_NAMES"].get(t, t) for t in info["types"]])
561
- violation_table += f"| {worker_id} | {info['count']} | {types} |\n"
562
 
563
- snapshots_text = "\n".join(
564
- f"### Worker {s['worker_id']} - {CONFIG['DISPLAY_NAMES'].get(s['violation'], s['violation'])}\n\n" +
565
- f"![Violation]({s['url']})\n"
566
- for s in snapshots[:5] # Limit to 5 snapshots in output
567
- ) if snapshots else "No snapshots captured"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
 
569
  yield (
570
  violation_table,
571
  f"Safety Score: {score}%",
572
  snapshots_text,
573
  f"Salesforce Record ID: {record_id}",
574
- final_pdf_url if final_pdf_url else pdf_url
575
  )
576
 
577
  except Exception as e:
578
- logger.error(f"Error processing video: {e}")
579
- yield f"Error: {str(e)}", "", "", "", ""
580
  finally:
581
  if video_path and os.path.exists(video_path):
582
- os.remove(video_path)
 
 
 
 
583
  if device.type == "cuda":
584
  torch.cuda.empty_cache()
585
 
586
- # ========================== # Gradio Interface # ==========================
587
  def gradio_interface(video_file):
588
- temp_dir = tempfile.mkdtemp()
 
589
  try:
 
 
 
 
 
 
590
  with open(video_file, "rb") as f:
591
  video_data = f.read()
592
-
593
- for output in process_video(video_data, temp_dir):
594
- yield output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  except Exception as e:
596
- yield f"Error: {str(e)}", "", "", "", ""
 
597
  finally:
598
- shutil.rmtree(temp_dir, ignore_errors=True)
 
 
 
 
 
 
 
 
 
 
 
599
 
 
600
  interface = gr.Interface(
601
  fn=gradio_interface,
602
  inputs=gr.Video(label="Upload Site Video"),
@@ -608,10 +916,10 @@ interface = gr.Interface(
608
  gr.Textbox(label="Violation Details URL")
609
  ],
610
  title="Worksite Safety Violation Analyzer",
611
- description="Fast safety violation detection for worksite videos. Upload a video to analyze worker safety compliance.",
612
  allow_flagging="never"
613
  )
614
 
615
  if __name__ == "__main__":
616
- logger.info("Launching Fast Safety Analyzer App...")
617
  interface.launch()
 
38
 
39
  FFMPEG_AVAILABLE = check_ffmpeg()
40
 
41
+ # ========================== # ByteTrack Implementation # ==========================
42
  class BYTETracker:
43
+ def __init__(self, track_thresh=0.3, track_buffer=90, match_thresh=0.5, frame_rate=30):
44
  self.track_thresh = track_thresh
45
+ self.track_buffer = track_buffer
46
+ self.match_thresh = match_thresh # Increased to 0.5 for better matching
47
  self.frame_rate = frame_rate
48
  self.next_id = 1
49
  self.tracks = {}
50
+ self.worker_history = {}
51
  self.last_positions = {}
52
+ self.recently_removed = {} # Store recently removed tracks for re-identification
53
 
54
  def update(self, dets, scores, cls):
55
  tracks = []
56
  current_time = time.time()
57
+
58
  # Prune stale tracks
59
+ stale_ids = []
60
+ for track_id, track_info in self.tracks.items():
61
+ if current_time - track_info['last_seen'] > self.track_buffer / self.frame_rate:
62
+ stale_ids.append(track_id)
63
+
64
+ for track_id in stale_ids:
65
+ # Store recently removed tracks for re-identification (for 1 second)
66
+ self.recently_removed[track_id] = {
67
+ 'bbox': self.tracks[track_id]['bbox'],
68
+ 'last_seen': current_time,
69
+ 'last_position': self.last_positions.get(track_id, [0, 0])
70
+ }
71
+ del self.tracks[track_id]
72
+ if track_id in self.worker_history:
73
+ del self.worker_history[track_id]
74
+ if track_id in self.last_positions:
75
+ del self.last_positions[track_id]
76
+
77
+ # Clean up recently_removed tracks older than 1 second
78
+ to_remove = []
79
+ for track_id, info in self.recently_removed.items():
80
+ if current_time - info['last_seen'] > 1.0:
81
+ to_remove.append(track_id)
82
+ for track_id in to_remove:
83
+ del self.recently_removed[track_id]
84
+
85
+ for i, (det, score, cl) in enumerate(zip(dets, scores, cls)):
86
  if score < self.track_thresh:
87
  continue
88
+
89
  x, y, w, h = det
90
  matched = False
 
 
 
91
  best_iou = 0
92
+ best_track_id = None
93
+
94
+ # Try to match with active tracks
95
+ for track_id, track_info in self.tracks.items():
96
+ tx, ty, tw, th = track_info['bbox']
97
  iou = self._calculate_iou([x, y, w, h], [tx, ty, tw, th])
98
+
99
  if iou > self.match_thresh and iou > best_iou:
100
  best_iou = iou
101
+ best_track_id = track_id
102
+ matched = True
103
+
104
+ if matched:
105
+ self.tracks[best_track_id].update({
106
  'bbox': [x, y, w, h],
107
  'score': score,
108
  'cls': cl,
109
  'last_seen': current_time
110
  })
111
+ if best_track_id not in self.worker_history:
112
+ self.worker_history[best_track_id] = []
113
+ self.worker_history[best_track_id].append([x, y])
114
+ self.last_positions[best_track_id] = [x, y]
115
+
116
  tracks.append({
117
+ 'id': best_track_id,
118
  'bbox': [x, y, w, h],
119
  'score': score,
120
  'cls': cl
121
  })
122
  else:
123
+ # Try to re-identify with recently removed tracks
124
+ reidentified = False
125
+ for track_id, info in self.recently_removed.items():
126
+ if self._is_same_worker([x, y], info['last_position'], threshold=150): # Increased threshold
127
+ self.tracks[track_id] = {
128
+ 'bbox': [x, y, w, h],
129
+ 'score': score,
130
+ 'cls': cl,
131
+ 'last_seen': current_time
132
+ }
133
+ self.worker_history[track_id] = [[x, y]]
134
+ self.last_positions[track_id] = [x, y]
135
+ tracks.append({
136
+ 'id': track_id,
137
+ 'bbox': [x, y, w, h],
138
+ 'score': score,
139
+ 'cls': cl
140
+ })
141
+ reidentified = True
142
+ del self.recently_removed[track_id]
143
+ break
144
+
145
+ if not reidentified:
146
+ # Check if it matches an existing worker by position
147
+ same_worker = False
148
+ for worker_id, last_pos in self.last_positions.items():
149
+ if self._is_same_worker([x, y], last_pos, threshold=150): # Increased threshold
150
+ self.tracks[worker_id] = {
151
+ 'bbox': [x, y, w, h],
152
+ 'score': score,
153
+ 'cls': cl,
154
+ 'last_seen': current_time
155
+ }
156
+ tracks.append({
157
+ 'id': worker_id,
158
+ 'bbox': [x, y, w, h],
159
+ 'score': score,
160
+ 'cls': cl
161
+ })
162
+ same_worker = True
163
+ break
164
+
165
+ if not same_worker:
166
+ self.tracks[self.next_id] = {
167
+ 'bbox': [x, y, w, h],
168
+ 'score': score,
169
+ 'cls': cl,
170
+ 'last_seen': current_time
171
+ }
172
+ self.worker_history[self.next_id] = [[x, y]]
173
+ self.last_positions[self.next_id] = [x, y]
174
+ tracks.append({
175
+ 'id': self.next_id,
176
+ 'bbox': [x, y, w, h],
177
+ 'score': score,
178
+ 'cls': cl
179
+ })
180
+ self.next_id += 1
181
+
182
  return tracks
183
 
184
  def _calculate_iou(self, box1, box2):
 
193
  intersection_area = (x_right - x_left) * (y_bottom - y_top)
194
  box1_area = w1 * h1
195
  box2_area = w2 * h2
196
+ iou = intersection_area / (box1_area + box2_area - intersection_area)
197
+ return iou
198
+
199
+ def _is_same_worker(self, pos1, pos2, threshold=150): # Increased threshold to 150
200
+ x1, y1 = pos1
201
+ x2, y2 = pos2
202
+ distance = np.sqrt((x1 - x2)**2 + (y1 - y2)**2)
203
+ return distance < threshold
204
 
205
  # ========================== # Optimized Configuration # ==========================
206
  CONFIG = {
 
243
  },
244
  "MIN_VIOLATION_FRAMES": 1,
245
  "VIOLATION_COOLDOWN": 30.0,
246
+ "WORKER_TRACKING_DURATION": 10.0, # Reverted to 5.0 seconds
247
  "MAX_PROCESSING_TIME": 60,
248
+ "FRAME_SKIP": 1,
249
+ "BATCH_SIZE": 4,
250
  "PARALLEL_WORKERS": max(1, cpu_count() - 1),
251
+ "TRACK_BUFFER": 150, # 5.0 seconds at 30 fps
252
  "TRACK_THRESH": 0.3,
253
+ "MATCH_THRESH": 0.5, # Increased to 0.5
254
+ "SNAPSHOT_QUALITY": 95,
255
+ "MAX_WORKER_DISTANCE": 150, # Increased to match _is_same_worker threshold
256
+ "TARGET_RESOLUTION": (384, 384)
257
  }
258
 
259
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
273
 
274
  model = YOLO(model_path).to(device)
275
  if device.type == "cuda":
276
+ model.model.half()
277
  logger.info(f"Model classes: {model.names}")
278
  return model
279
  except Exception as e:
 
282
 
283
  model = load_model()
284
 
285
+ # ========================== # Helper Functions # ==========================
286
  def preprocess_frame(frame):
287
+ target_res = CONFIG["TARGET_RESOLUTION"]
288
+ frame = cv2.resize(frame, target_res, interpolation=cv2.INTER_LINEAR)
289
+ frame = cv2.convertScaleAbs(frame, alpha=1.2, beta=20)
290
  return frame
291
 
292
  def draw_detections(frame, detections):
293
  result_frame = frame.copy()
294
+
295
  for det in detections:
296
  label = det.get("violation", "Unknown")
297
+ confidence = det.get("confidence", 0.0)
298
  x, y, w, h = det.get("bounding_box", [0, 0, 0, 0])
299
  worker_id = det.get("worker_id", "Unknown")
300
 
301
+ x1 = int(x - w/2)
302
+ y1 = int(y - h/2)
303
+ x2 = int(x + w/2)
304
+ y2 = int(y + h/2)
305
+
306
  color = CONFIG["CLASS_COLORS"].get(label, (0, 0, 255))
307
+
308
+ cv2.rectangle(result_frame, (x1, y1), (x2, y2), color, 3)
309
+
310
  display_text = f"{CONFIG['DISPLAY_NAMES'].get(label, label)} (Worker {worker_id})"
311
+ text_size = cv2.getTextSize(display_text, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)[0]
312
+ cv2.rectangle(result_frame, (x1, y1-text_size[1]-10), (x1+text_size[0]+10, y1), (0, 0, 0), -1)
313
+ cv2.putText(result_frame, display_text, (x1+5, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
314
+
315
+ conf_text = f"Conf: {confidence:.2f}"
316
+ cv2.putText(result_frame, conf_text, (x1+5, y2+20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
317
+
318
  return result_frame
319
 
320
  def calculate_safety_score(violations):
 
325
  "unsafe_zone": 35,
326
  "improper_tool_use": 25
327
  }
328
+
329
  worker_violations = {}
330
  for v in violations:
331
  worker_id = v.get("worker_id", "Unknown")
332
  violation_type = v.get("violation", "Unknown")
333
+
334
  if worker_id not in worker_violations:
335
  worker_violations[worker_id] = set()
336
  worker_violations[worker_id].add(violation_type)
337
+
338
+ total_penalty = 0
339
+ for worker_violations_set in worker_violations.values():
340
+ worker_penalty = sum(penalties.get(v, 0) for v in worker_violations_set)
341
+ total_penalty += worker_penalty
342
+
343
+ score = max(0, 100 - total_penalty)
344
+ return score
345
 
346
  def generate_violation_pdf(violations, score, output_dir):
347
  try:
 
425
  try:
426
  sf = Salesforce(**CONFIG["SF_CREDENTIALS"])
427
  logger.info("Connected to Salesforce")
428
+ sf.describe()
429
  return sf
430
  except Exception as e:
431
  logger.error(f"Salesforce connection failed: {e}")
432
  raise
433
 
434
+ def upload_pdf_to_salesforce(sf, pdf_file, report_id):
435
+ try:
436
+ if not pdf_file:
437
+ logger.error("No PDF file provided for upload")
438
+ return ""
439
+
440
+ encoded_pdf = base64.b64encode(pdf_file.getvalue()).decode('utf-8')
441
+ content_version_data = {
442
+ "Title": f"Safety_Violation_Report_{int(time.time())}",
443
+ "PathOnClient": f"safety_violation_{int(time.time())}.pdf",
444
+ "VersionData": encoded_pdf,
445
+ "FirstPublishLocationId": report_id
446
+ }
447
+ content_version = sf.ContentVersion.create(content_version_data)
448
+ result = sf.query(f"SELECT Id, ContentDocumentId FROM ContentVersion WHERE Id = '{content_version['id']}'")
449
+
450
+ if not result['records']:
451
+ logger.error("Failed to retrieve ContentVersion")
452
+ return ""
453
+
454
+ file_url = f"https://{sf.sf_instance}/sfc/servlet.shepherd/version/download/{content_version['id']}"
455
+ logger.info(f"PDF uploaded to Salesforce: {file_url}")
456
+ return file_url
457
+ except Exception as e:
458
+ logger.error(f"Error uploading PDF to Salesforce: {e}")
459
+ return ""
460
+
461
  def push_report_to_salesforce(violations, score, pdf_path, pdf_file):
462
  try:
463
  sf = connect_to_salesforce()
 
484
  "PDF_Report_URL__c": pdf_url
485
  }
486
 
487
+ logger.info(f"Creating Salesforce record with data: {record_data}")
488
+
489
  try:
490
  record = sf.Safety_Video_Report__c.create(record_data)
491
  logger.info(f"Created Safety_Video_Report__c record: {record['id']}")
 
501
  if uploaded_url:
502
  try:
503
  sf.Safety_Video_Report__c.update(record_id, {"PDF_Report_URL__c": uploaded_url})
504
+ logger.info(f"Updated record {record_id} with PDF URL: {uploaded_url}")
505
  except Exception as e:
506
+ logger.error(f"Failed to update Safety_Video_Report__c: {e}")
507
  sf.Account.update(record_id, {"Description": uploaded_url})
508
+ logger.info(f"Updated Account record {record_id} with PDF URL")
509
  pdf_url = uploaded_url
510
 
511
  return record_id, pdf_url
 
513
  logger.error(f"Salesforce record creation failed: {e}")
514
  return "N/A", "Salesforce integration failed."
515
 
516
+ @tenacity.retry(
517
+ stop=tenacity.stop_after_attempt(3),
518
+ wait=tenacity.wait_fixed(1),
519
+ retry=tenacity.retry_if_exception_type((IOError, OSError)),
520
+ before_sleep=lambda retry_state: logger.info(f"Retrying file access (attempt {retry_state.attempt_number}/3)...")
521
+ )
522
+ def verify_and_open_video(video_path):
523
+ if not os.path.exists(video_path):
524
+ raise FileNotFoundError(f"Temporary video file not found: {video_path}")
525
+
526
+ file_size = os.path.getsize(video_path)
527
+ if file_size == 0:
528
+ raise ValueError(f"Temporary video file is empty: {video_path}")
529
+
530
+ with open(video_path, "rb") as f:
531
+ f.read(1)
532
+
533
+ cap = cv2.VideoCapture(video_path)
534
+ if not cap.isOpened():
535
+ raise ValueError("Could not open video file. Ensure the video format is supported (e.g., MP4) and FFmpeg is installed.")
536
+
537
+ return cap
538
 
 
539
  def process_video(video_data, temp_dir):
540
  video_path = None
541
  output_dir = os.path.join(temp_dir, "output")
542
  os.makedirs(output_dir, exist_ok=True)
543
+ os.environ['YOLO_CONFIG_DIR'] = temp_dir
544
+
545
  try:
546
+ if not video_data:
547
+ raise ValueError("Empty video data provided.")
548
+
549
+ logger.info(f"Received video data size: {len(video_data)} bytes")
550
+ if len(video_data) == 0:
551
+ raise ValueError("Video data is empty.")
552
+
553
  with tempfile.NamedTemporaryFile(suffix=".mp4", dir=temp_dir, delete=False) as temp_file:
554
  temp_file.write(video_data)
555
+ temp_file.flush()
556
  video_path = temp_file.name
557
+ logger.info(f"Video saved to temporary file: {video_path}")
558
 
559
+ if not os.path.exists(video_path):
560
+ raise FileNotFoundError(f"Temporary video file not found: {video_path}")
561
+ file_size = os.path.getsize(video_path)
562
+ if file_size == 0:
563
+ raise ValueError(f"Temporary video file is empty: {video_path}")
564
+ logger.info(f"Temporary video file size: {file_size} bytes")
565
+
566
+ cap = verify_and_open_video(video_path)
567
+ logger.info(f"Successfully opened video file: {video_path}")
568
 
 
569
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
570
+ fps = cap.get(cv2.CAP_PROP_FPS) or 30
571
+ duration = total_frames / fps
572
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
573
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
574
+ logger.info(f"Video properties: {duration:.2f}s, {total_frames} frames, {fps:.1f} FPS, {width}x{height}")
575
+
576
  if total_frames <= 0:
577
+ raise ValueError("Video has no frames.")
578
 
579
  tracker = BYTETracker(
580
  track_thresh=CONFIG["TRACK_THRESH"],
 
585
 
586
  worker_id_mapping = {}
587
  unique_violations = {}
588
+ violation_frames = {}
589
+ worker_violation_count = {} # Track violation count per worker
590
+ start_time = time.time()
591
  frame_skip = CONFIG["FRAME_SKIP"]
592
  processed_frames = 0
593
+ last_yield_time = start_time
594
+ worker_counter = 1
595
 
596
+ while processed_frames < total_frames:
597
+ batch_frames = []
598
+ batch_indices = []
599
+
600
+ for _ in range(CONFIG["BATCH_SIZE"]):
601
+ frame_idx = int(cap.get(cv2.CAP_PROP_POS_FRAMES))
602
+ if frame_idx >= total_frames:
603
  break
604
+
605
+ ret, frame = cap.read()
606
+ if not ret:
607
+ logger.warning(f"Failed to read frame {frame_idx}. Skipping.")
608
+ break
609
+
610
+ frame = preprocess_frame(frame)
611
+
612
+ for _ in range(frame_skip - 1):
613
+ if not cap.grab():
614
+ break
615
+
616
+ batch_frames.append(frame)
617
+ batch_indices.append(frame_idx)
618
+ processed_frames += 1
619
 
620
+ if not batch_frames:
621
+ logger.info("No more frames to process.")
622
+ break
 
623
 
624
+ try:
625
+ batch_frames_np = np.array(batch_frames)
626
+ batch_frames_tensor = torch.from_numpy(batch_frames_np).permute(0, 3, 1, 2).float() / 255.0
627
+ batch_frames_tensor = batch_frames_tensor.to(device)
628
+ if device.type == "cuda":
629
+ batch_frames_tensor = batch_frames_tensor.half()
630
+
631
+ results = model(batch_frames_tensor, device=device, conf=0.1, verbose=False)
632
+ except Exception as e:
633
+ logger.error(f"Model inference failed: {e}")
634
+ raise ValueError(f"Failed to process video frames with YOLO model: {str(e)}")
635
+ finally:
636
+ batch_frames = []
637
+ if device.type == "cuda":
638
+ torch.cuda.empty_cache()
639
+
640
+ current_time = time.time()
641
+ if current_time - last_yield_time > 0.1:
642
+ progress = (processed_frames / total_frames) * 100
643
+ elapsed_time = current_time - start_time
644
+ fps_processed = processed_frames / elapsed_time if elapsed_time > 0 else 0
645
+ yield f"Processing video... {progress:.1f}% complete (Frame {processed_frames}/{total_frames}, {fps_processed:.1f} FPS)", "", "", "", ""
646
+ last_yield_time = current_time
647
+
648
+ for i, (result, frame_idx) in enumerate(zip(results, batch_indices)):
649
+ current_time = frame_idx / fps
650
+
651
+ boxes = result.boxes
652
+ track_inputs = []
653
+
654
+ for box in boxes:
655
+ cls = int(box.cls)
656
+ conf = float(box.conf)
657
+ label = CONFIG["VIOLATION_LABELS"].get(cls, None)
658
+
659
+ if label is None:
660
+ continue
661
+
662
+ if conf < CONFIG["CONFIDENCE_THRESHOLDS"].get(label, 0.25):
663
+ continue
664
+
665
+ bbox = box.xywh.cpu().numpy()[0]
666
+ track_inputs.append({
667
+ "bbox": bbox,
668
+ "conf": conf,
669
+ "cls": cls
670
+ })
671
+
672
+ if not track_inputs:
673
  continue
674
+
675
+ tracked_objects = tracker.update(
676
+ np.array([t["bbox"] for t in track_inputs]),
677
+ np.array([t["conf"] for t in track_inputs]),
678
+ np.array([t["cls"] for t in track_inputs])
 
 
 
 
 
 
 
679
  )
680
+ logger.info(f"Frame {frame_idx}: Detected {len(tracked_objects)} workers")
681
+
682
+ for obj in tracked_objects:
683
+ tracker_id = obj['id']
684
+ label = CONFIG["VIOLATION_LABELS"].get(int(obj['cls']), None)
685
+ conf = obj['score']
686
+ bbox = obj['bbox']
687
+
688
+ if label is None:
689
+ continue
690
+
691
+ if tracker_id not in worker_id_mapping:
692
+ worker_id_mapping[tracker_id] = worker_counter
693
  worker_counter += 1
694
+
695
+ worker_id = worker_id_mapping[tracker_id]
696
+
697
  violation_key = (worker_id, label)
698
+
699
  if violation_key not in unique_violations:
700
+ unique_violations[violation_key] = current_time
701
+ violation_frames[violation_key] = frame_idx
702
+ # Update violation count for this worker
703
+ if worker_id not in worker_violation_count:
704
+ worker_violation_count[worker_id] = 0
705
+ worker_violation_count[worker_id] += 1
 
 
 
 
 
706
 
707
  cap.release()
708
+ processing_time = time.time() - start_time
709
+ logger.info(f"Processing complete in {processing_time:.2f}s")
710
+ logger.info(f"Total unique workers detected: {len(set(worker_id_mapping.values()))}")
711
+ logger.info(f"Violations per worker: {worker_violation_count}")
712
+
713
+ violations = []
714
+ for (worker_id, label), detection_time in unique_violations.items():
715
+ violations.append({
716
+ "worker_id": worker_id,
717
+ "violation": label,
718
+ "timestamp": detection_time,
719
+ "confidence": 0.0,
720
+ "frame_idx": violation_frames[(worker_id, label)]
721
+ })
722
 
723
  if not violations:
724
+ logger.info("No violations detected after processing")
725
+ yield "No violations detected in the video.", "Safety Score: 100%", "No snapshots captured.", "N/A", "N/A"
726
  return
727
 
 
728
  snapshots = []
729
  cap = cv2.VideoCapture(video_path)
730
+ for violation in violations:
731
+ frame_idx = violation["frame_idx"]
732
+ cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
733
  ret, frame = cap.read()
734
+ if not ret:
735
+ logger.warning(f"Failed to read frame {frame_idx} for snapshot.")
736
+ continue
737
+
738
+ frame = preprocess_frame(frame)
739
+ frame_tensor = torch.from_numpy(frame).permute(2, 0, 1).float() / 255.0
740
+ frame_tensor = frame_tensor.unsqueeze(0).to(device)
741
+ if device.type == "cuda":
742
+ frame_tensor = frame_tensor.half()
743
+
744
+ result = model(frame_tensor, device=device, conf=0.1, verbose=False)[0]
745
+ boxes = result.boxes
746
+
747
+ for box in boxes:
748
+ cls = int(box.cls)
749
+ conf = float(box.conf)
750
+ label = CONFIG["VIOLATION_LABELS"].get(cls, None)
751
+ if label == violation["violation"]:
752
+ violation["confidence"] = round(conf, 2)
753
+ bbox = box.xywh.cpu().numpy()[0]
754
+ detection = {
755
+ "worker_id": violation["worker_id"],
756
+ "violation": label,
757
+ "confidence": violation["confidence"],
758
+ "bounding_box": bbox,
759
+ "timestamp": violation["timestamp"]
760
+ }
761
+ snapshot_frame = frame.copy()
762
+ snapshot_frame = draw_detections(snapshot_frame, [detection])
763
+ cv2.putText(
764
+ snapshot_frame,
765
+ f"Time: {violation['timestamp']:.2f}s",
766
+ (10, 30),
767
+ cv2.FONT_HERSHEY_SIMPLEX,
768
+ 0.7,
769
+ (255, 255, 255),
770
+ 2
771
+ )
772
+ snapshot_filename = f"violation_{label}_worker{violation['worker_id']}_{int(violation['timestamp']*100)}.jpg"
773
+ snapshot_path = os.path.join(output_dir, snapshot_filename)
774
+ cv2.imwrite(
775
+ snapshot_path,
776
+ snapshot_frame,
777
+ [cv2.IMWRITE_JPEG_QUALITY, CONFIG["SNAPSHOT_QUALITY"]]
778
+ )
779
+ snapshots.append({
780
+ "violation": label,
781
+ "worker_id": violation["worker_id"],
782
+ "timestamp": violation["timestamp"],
783
+ "snapshot_path": snapshot_path,
784
+ "snapshot_url": f"{CONFIG['PUBLIC_URL_BASE']}{snapshot_filename}",
785
+ "confidence": violation["confidence"]
786
+ })
787
+ logger.info(f"Captured snapshot for {label} violation by worker {violation['worker_id']} at {violation['timestamp']:.2f}s")
788
+ break
789
+
790
  cap.release()
791
 
 
792
  score = calculate_safety_score(violations)
793
  pdf_path, pdf_url, pdf_file = generate_violation_pdf(violations, score, output_dir)
794
+
795
  record_id, final_pdf_url = push_report_to_salesforce(violations, score, pdf_path, pdf_file)
796
+
797
+ # Generate summary of workers and their violations
798
  worker_summary = {}
799
  for v in violations:
800
+ worker_id = v["worker_id"]
801
+ if worker_id not in worker_summary:
802
+ worker_summary[worker_id] = {
803
+ "count": 0,
804
+ "violations": set()
805
+ }
806
+ worker_summary[worker_id]["count"] += 1
807
+ worker_summary[worker_id]["violations"].add(v["violation"])
808
+
809
+ # Create violation table with worker summary
810
+ violation_table = "## Worker Safety Violation Summary\n\n"
811
+ violation_table += "| Worker ID | Total Violations | Violation Types |\n"
812
+ violation_table += "|-----------|------------------|-----------------|\n"
813
+
814
  for worker_id, info in worker_summary.items():
815
+ violation_types = ", ".join([CONFIG["DISPLAY_NAMES"].get(v, v) for v in info["violations"]])
816
+ violation_table += f"| {worker_id} | {info['count']} | {violation_types} |\n"
817
 
818
+ violation_table += "\n## Detailed Violation Log\n\n"
819
+ violation_table += "| Violation | Worker ID | Time (s) | Confidence |\n"
820
+ violation_table += "|-----------|-----------|----------|------------|\n"
821
+
822
+ for v in sorted(violations, key=lambda x: (x.get("worker_id", "Unknown"), x.get("timestamp", 0.0))):
823
+ display_name = CONFIG["DISPLAY_NAMES"].get(v.get("violation", "Unknown"), "Unknown")
824
+ worker_id = v.get("worker_id", "Unknown")
825
+ timestamp = v.get("timestamp", 0.0)
826
+ confidence = v.get("confidence", 0.0)
827
+ violation_table += f"| {display_name} | {worker_id} | {timestamp:.2f} | {confidence:.2f} |\n"
828
+
829
+ snapshots_text = ""
830
+ for s in snapshots:
831
+ display_name = CONFIG["DISPLAY_NAMES"].get(s["violation"], "Unknown")
832
+ worker_id = s.get("worker_id", "Unknown")
833
+ timestamp = s.get("timestamp", 0.0)
834
+ snapshots_text += f"### {display_name} - Worker {worker_id} at {timestamp:.2f}s\n\n"
835
+ snapshots_text += f"![Violation]({s['snapshot_url']})\n\n"
836
+
837
+ if not snapshots_text:
838
+ snapshots_text = "No snapshots captured."
839
 
840
  yield (
841
  violation_table,
842
  f"Safety Score: {score}%",
843
  snapshots_text,
844
  f"Salesforce Record ID: {record_id}",
845
+ final_pdf_url
846
  )
847
 
848
  except Exception as e:
849
+ logger.error(f"Error processing video: {str(e)}", exc_info=True)
850
+ yield f"Error processing video: {str(e)}", "", "", "", ""
851
  finally:
852
  if video_path and os.path.exists(video_path):
853
+ try:
854
+ os.remove(video_path)
855
+ logger.info(f"Cleaned up temporary video file: {video_path}")
856
+ except Exception as e:
857
+ logger.error(f"Failed to clean up temporary video file {video_path}: {e}")
858
  if device.type == "cuda":
859
  torch.cuda.empty_cache()
860
 
 
861
  def gradio_interface(video_file):
862
+ temp_dir = None
863
+ local_video_path = None
864
  try:
865
+ if not video_file:
866
+ return "No file uploaded.", "", "No file uploaded.", "", ""
867
+
868
+ temp_dir = tempfile.mkdtemp(prefix="Ultralytics_")
869
+ logger.info(f"Created temporary directory for video processing: {temp_dir}")
870
+
871
  with open(video_file, "rb") as f:
872
  video_data = f.read()
873
+ logger.info(f"Read Gradio video file: {video_file}, size: {len(video_data)} bytes")
874
+
875
+ if len(video_data) == 0:
876
+ return "Uploaded video file is empty.", "", "", "", ""
877
+
878
+ with tempfile.NamedTemporaryFile(suffix=".mp4", dir=temp_dir, delete=False) as temp_file:
879
+ temp_file.write(video_data)
880
+ temp_file.flush()
881
+ local_video_path = temp_file.name
882
+ logger.info(f"Copied Gradio video to local temporary file: {local_video_path}")
883
+
884
+ if not FFMPEG_AVAILABLE:
885
+ return "FFmpeg is not available in the environment. Please install FFmpeg to process videos.", "", "", "", ""
886
+
887
+ for status, score, snapshots_text, record_id, details_url in process_video(video_data, temp_dir):
888
+ yield status, score, snapshots_text, record_id, details_url
889
+
890
  except Exception as e:
891
+ logger.error(f"Error in Gradio interface: {e}", exc_info=True)
892
+ yield f"Error: {str(e)}", "", "Error in processing.", "", ""
893
  finally:
894
+ if local_video_path and os.path.exists(local_video_path):
895
+ try:
896
+ os.remove(local_video_path)
897
+ logger.info(f"Cleaned up local temporary video file: {local_video_path}")
898
+ except Exception as e:
899
+ logger.error(f"Failed to clean up local temporary video file {local_video_path}: {e}")
900
+
901
+ if temp_dir and os.path.exists(temp_dir):
902
+ shutil.rmtree(temp_dir, ignore_errors=True)
903
+ logger.info(f"Cleaned up temporary directory: {temp_dir}")
904
+ if device.type == "cuda":
905
+ torch.cuda.empty_cache()
906
 
907
+ # ========================== # Gradio Interface # ==========================
908
  interface = gr.Interface(
909
  fn=gradio_interface,
910
  inputs=gr.Video(label="Upload Site Video"),
 
916
  gr.Textbox(label="Violation Details URL")
917
  ],
918
  title="Worksite Safety Violation Analyzer",
919
+ description="Upload site videos to detect safety violations (No Helmet, No Harness, Unsafe Posture, Unsafe Zone, Improper Tool Use). Each unique violation is detected only once per worker.",
920
  allow_flagging="never"
921
  )
922
 
923
  if __name__ == "__main__":
924
+ logger.info("Launching Enhanced Safety Analyzer App...")
925
  interface.launch()