AI-RESEARCHER-2024 commited on
Commit
7e553d9
Β·
verified Β·
1 Parent(s): eac30c9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +368 -499
app.py CHANGED
@@ -13,6 +13,7 @@ import numpy as np
13
  import cv2
14
  import threading
15
  import queue
 
16
 
17
  # Page configuration
18
  st.set_page_config(
@@ -22,42 +23,124 @@ st.set_page_config(
22
  initial_sidebar_state="expanded"
23
  )
24
 
25
- # Custom CSS for styling
26
  st.markdown("""
27
  <style>
28
  .main-header {
29
- background: linear-gradient(90deg, #0066cc, #004499);
30
- padding: 20px;
31
- border-radius: 10px;
32
  color: white;
33
  text-align: center;
34
- margin-bottom: 30px;
 
 
 
 
 
 
 
 
 
 
 
35
  }
36
  .assessment-box {
37
- background: #f0fdf4;
38
- padding: 20px;
39
- border-radius: 10px;
40
- border-left: 5px solid #059669;
41
- margin: 20px 0;
42
  }
43
  .competency-item {
44
- background: #f8fafc;
45
- padding: 15px;
46
- margin: 10px 0;
47
  border-radius: 8px;
48
- border-left: 4px solid #0891b2;
 
 
 
 
49
  }
50
  .score-display {
51
  text-align: center;
52
- padding: 30px;
53
  background: white;
54
- border-radius: 15px;
55
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
56
- margin: 20px 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  }
58
  </style>
59
  """, unsafe_allow_html=True)
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  # CICE Assessment Class
62
  class CICE_Assessment:
63
  def __init__(self, api_key):
@@ -76,7 +159,7 @@ class CICE_Assessment:
76
  temp_path = None
77
  try:
78
  # Save uploaded file to temporary location
79
- with tempfile.NamedTemporaryFile(delete=False, suffix='.webm') as tmp_file:
80
  tmp_file.write(video_file.read())
81
  temp_path = tmp_file.name
82
 
@@ -86,17 +169,28 @@ class CICE_Assessment:
86
  # Wait for processing
87
  max_wait = 300
88
  wait_time = 0
 
 
 
89
  while uploaded_file.state.name == "PROCESSING" and wait_time < max_wait:
90
  time.sleep(3)
91
  wait_time += 3
 
 
 
92
  uploaded_file = genai.get_file(uploaded_file.name)
93
 
 
 
 
94
  if uploaded_file.state.name == "FAILED":
95
  raise Exception("Video processing failed")
96
 
97
  # The 18-point CICE 2.0 assessment prompt
98
  prompt = """Analyze this healthcare team interaction video and provide a comprehensive assessment based on the CICE 2.0 instrument's 18 interprofessional competencies.
 
99
  For EACH of the following 18 competencies, clearly state whether it was "OBSERVED" or "NOT OBSERVED" and provide specific examples with timestamps when possible:
 
100
  1. IDENTIFIES FACTORS INFLUENCING HEALTH STATUS
101
  2. IDENTIFIES TEAM GOALS FOR THE PATIENT
102
  3. PRIORITIZES GOALS FOCUSED ON IMPROVING HEALTH OUTCOMES
@@ -115,20 +209,27 @@ class CICE_Assessment:
115
  16. REFLECTS ON STRENGTHS OF TEAM INTERACTIONS
116
  17. REFLECTS ON CHALLENGES OF TEAM INTERACTIONS
117
  18. IDENTIFIES HOW TO IMPROVE TEAM EFFECTIVENESS
 
118
  STRUCTURE YOUR RESPONSE AS FOLLOWS:
 
119
  ## OVERALL ASSESSMENT
120
  Provide a brief overview of the team interaction quality and professionalism.
 
121
  ## DETAILED COMPETENCY EVALUATION
122
  For each of the 18 competencies, format as:
123
  Competency [number]: [name]
124
  Status: [OBSERVED/NOT OBSERVED]
125
  Evidence: [Specific examples from the video, or explanation of why it wasn't observed]
 
126
  ## STRENGTHS
127
  List 3-5 key strengths observed in the team interaction
 
128
  ## AREAS FOR IMPROVEMENT
129
  List 3-5 specific areas where the team could improve
 
130
  ## RECOMMENDATIONS
131
  Provide 3-5 actionable recommendations for enhancing team collaboration and patient care
 
132
  ## FINAL SCORE
133
  Competencies Observed: X/18
134
  Overall Performance Level: [Exemplary/Proficient/Developing/Needs Improvement]"""
@@ -140,6 +241,8 @@ class CICE_Assessment:
140
  # Clean up temporary file
141
  if temp_path and os.path.exists(temp_path):
142
  os.unlink(temp_path)
 
 
143
 
144
  def generate_audio_feedback(self, text):
145
  """Convert assessment text to audio feedback"""
@@ -153,18 +256,12 @@ class CICE_Assessment:
153
  # Generate audio with gTTS
154
  tts = gTTS(text=clean_text, lang='en', slow=False)
155
 
156
- # Save to temporary file
157
- temp_audio = tempfile.NamedTemporaryFile(delete=False, suffix='.mp3')
158
- tts.save(temp_audio.name)
159
-
160
- # Read audio data
161
- with open(temp_audio.name, 'rb') as f:
162
- audio_data = f.read()
163
 
164
- # Clean up temporary file
165
- os.unlink(temp_audio.name)
166
-
167
- return audio_data
168
 
169
  except Exception as e:
170
  st.error(f"⚠️ Audio generation failed: {str(e)}")
@@ -173,8 +270,6 @@ class CICE_Assessment:
173
  def parse_assessment_score(assessment_text):
174
  """Parse the assessment text to extract score"""
175
  try:
176
- # Look for pattern like "X/18" in the text
177
- import re
178
  pattern = r'(\d+)/18'
179
  match = re.search(pattern, assessment_text)
180
  if match:
@@ -199,75 +294,37 @@ def parse_assessment_score(assessment_text):
199
  pass
200
  return 0, 0, "Unknown", "#6b7280"
201
 
202
- # Video recording state
203
- if "recording" not in st.session_state:
204
- st.session_state.recording = False
205
- if "recorded_frames" not in st.session_state:
206
- st.session_state.recorded_frames = []
207
- if "frame_queue" not in st.session_state:
208
- st.session_state.frame_queue = queue.Queue()
209
- if "webrtc_ctx" not in st.session_state:
210
- st.session_state.webrtc_ctx = None
211
- if "webrtc_error" not in st.session_state:
212
- st.session_state.webrtc_error = False
213
-
214
- def video_frame_callback(frame):
215
- """Callback function to process video frames during recording"""
216
- try:
217
- if st.session_state.recording and frame is not None:
218
- img = frame.to_ndarray(format="bgr24")
219
- # Use a non-blocking put to avoid queue overflow
220
- if st.session_state.frame_queue.qsize() < 1000: # Limit queue size
221
- st.session_state.frame_queue.put(img, block=False)
222
- return frame
223
- except Exception as e:
224
- # Log error but don't stop recording
225
- print(f"Frame callback error: {e}")
226
- return frame
227
-
228
- def save_recorded_video():
229
- """Save recorded frames to a video file"""
230
- try:
231
- if st.session_state.recorded_frames:
232
- # Create temporary file
233
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
234
- temp_path = temp_file.name
235
- temp_file.close()
236
-
237
- # Video properties
238
- height, width, _ = st.session_state.recorded_frames[0].shape
239
- fourcc = cv2.VideoWriter_fourcc(*'mp4v')
240
- fps = 20 # frames per second
241
-
242
- # Create video writer
243
- out = cv2.VideoWriter(temp_path, fourcc, fps, (width, height))
244
-
245
- # Write frames
246
- for frame in st.session_state.recorded_frames:
247
- out.write(frame)
248
-
249
- out.release()
250
- return temp_path
251
  return None
252
- except Exception as e:
253
- st.error(f"❌ Error saving video: {str(e)}")
254
- return None
255
-
256
- def cleanup_webrtc():
257
- """Clean up WebRTC resources properly"""
258
- try:
259
- if st.session_state.webrtc_ctx:
260
- # Try to stop the WebRTC context gracefully
261
- if hasattr(st.session_state.webrtc_ctx, 'state') and st.session_state.webrtc_ctx.state:
262
- try:
263
- st.session_state.webrtc_ctx.state = None
264
- except:
265
- pass
266
- st.session_state.webrtc_ctx = None
267
- except Exception as e:
268
- print(f"WebRTC cleanup error: {e}")
269
- # Reset anyway
270
- st.session_state.webrtc_ctx = None
 
 
 
 
 
 
 
 
271
 
272
  def main():
273
  # Header
@@ -295,458 +352,270 @@ def main():
295
  st.markdown("---")
296
  st.markdown("""
297
  ### πŸ“‹ CICE 2.0 Competencies
 
 
298
  1. Health Status Factors
299
  2. Team Goals Identification
300
  3. Goal Prioritization
301
  4. Role Verbalization
302
  5. Seeking Guidance
 
 
303
  6. Cost-Effective Communication
304
  7. Expertise-Based Questions
305
  8. Avoiding Jargon
306
  9. Explaining Terminology
307
  10. Clear Role Communication
 
 
308
  11. Active Listening
309
  12. Soliciting Perspectives
310
  13. Recognizing Contributions
311
  14. Team Respect
312
  15. Conflict Resolution
 
 
313
  16. Strength Reflection
314
  17. Challenge Reflection
315
  18. Improvement Identification
316
  """)
317
 
318
- # Main content
319
- col1, col2 = st.columns([2, 1])
320
 
321
- with col1:
322
- st.header("πŸ“Ή Record or Upload Healthcare Team Video")
323
 
324
- # Tab selection for recording vs uploading
325
- tab1, tab2 = st.tabs(["πŸŽ₯ Record Video", "πŸ“ Upload Video"])
326
 
327
- with tab1:
328
- st.subheader("Live Video Recording")
329
- st.markdown("Click **Start Recording** to activate your webcam and begin recording a healthcare team interaction.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
- # WebRTC configuration
332
- rtc_configuration = RTCConfiguration({
333
- "iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]
334
- })
 
 
 
 
 
 
335
 
336
- # Recording controls - only show when no completed recording exists
337
- if not st.session_state.recorded_frames:
338
- st.markdown("### 🎬 Recording Controls")
339
- col_start, col_stop, col_status = st.columns([1, 1, 2])
340
-
341
- with col_start:
342
- if st.button("πŸ”΄ Start Recording", disabled=st.session_state.recording, type="primary", key="start_rec_btn"):
343
- try:
344
- st.session_state.recording = True
345
- st.session_state.recorded_frames = []
346
- st.session_state.webrtc_error = False
347
- # Clear the queue
348
- while not st.session_state.frame_queue.empty():
349
- try:
350
- st.session_state.frame_queue.get_nowait()
351
- except:
352
- break
353
- st.rerun()
354
- except Exception as e:
355
- st.error(f"❌ Failed to start recording: {str(e)}")
356
- st.session_state.recording = False
357
-
358
- with col_stop:
359
- if st.button("⏹️ Stop Recording", disabled=not st.session_state.recording, type="secondary", key="stop_rec_btn"):
360
- try:
361
- st.session_state.recording = False
362
-
363
- # Collect frames from queue with timeout
364
- frames_collected = []
365
- timeout_counter = 0
366
- max_timeout = 50 # 5 seconds timeout
367
-
368
- while not st.session_state.frame_queue.empty() and timeout_counter < max_timeout:
369
- try:
370
- frame = st.session_state.frame_queue.get_nowait()
371
- frames_collected.append(frame)
372
- except queue.Empty:
373
- timeout_counter += 1
374
- time.sleep(0.1)
375
- continue
376
-
377
- st.session_state.recorded_frames = frames_collected
378
-
379
- if len(frames_collected) > 0:
380
- st.success(f"Recording stopped! Captured {len(frames_collected)} frames.")
381
- else:
382
- st.warning("⚠️ No frames were captured. Please ensure camera access is working.")
383
-
384
- # Clean up WebRTC resources
385
- cleanup_webrtc()
386
- st.rerun()
387
-
388
- except Exception as e:
389
- st.error(f"❌ Error stopping recording: {str(e)}")
390
- st.session_state.recording = False
391
-
392
- with col_status:
393
- if st.session_state.recording:
394
- st.markdown("πŸ”΄ **Recording in progress...**")
395
- st.markdown("*Click Stop Recording when finished*")
396
  else:
397
- st.markdown("βšͺ **Ready to record**")
398
- st.markdown("*Click Start Recording to begin*")
399
-
400
- # Show recording completion status
401
- if st.session_state.recorded_frames and not st.session_state.recording:
402
- st.markdown("### βœ… Recording Complete")
403
- st.success(f"**Recording finished successfully!** Captured {len(st.session_state.recorded_frames)} frames")
404
- st.info("πŸ“Ή Your video is ready for analysis. Use the Process Video button below.")
405
-
406
- # Only show WebRTC streamer when recording is active or about to start
407
- if st.session_state.recording or not st.session_state.recorded_frames:
408
- try:
409
- # WebRTC configuration with more robust settings
410
- rtc_configuration = RTCConfiguration({
411
- "iceServers": [
412
- {"urls": ["stun:stun.l.google.com:19302"]},
413
- {"urls": ["stun:stun1.l.google.com:19302"]},
414
- ]
415
- })
416
-
417
- # WebRTC streamer with error handling
418
- st.session_state.webrtc_ctx = webrtc_streamer(
419
- key="video-recorder",
420
- mode=WebRtcMode.SENDONLY,
421
- rtc_configuration=rtc_configuration,
422
- video_frame_callback=video_frame_callback,
423
- media_stream_constraints={
424
- "video": {
425
- "width": {"ideal": 640, "max": 640},
426
- "height": {"ideal": 480, "max": 480},
427
- "frameRate": {"ideal": 15, "max": 20} # Reduced frame rate
428
- },
429
- "audio": False # Disable audio for now
430
- },
431
- async_processing=True,
432
- )
433
-
434
- # Check WebRTC connection state
435
- if st.session_state.webrtc_ctx and hasattr(st.session_state.webrtc_ctx, 'state'):
436
- if st.session_state.webrtc_ctx.state and st.session_state.webrtc_ctx.state.playing:
437
- if st.session_state.webrtc_error:
438
- st.success("βœ… WebRTC connection restored!")
439
- st.session_state.webrtc_error = False
440
- elif st.session_state.recording:
441
- st.warning("⚠️ Camera connection may be unstable. If issues persist, try refreshing the page.")
442
-
443
- except Exception as e:
444
- st.session_state.webrtc_error = True
445
- st.error(f"❌ WebRTC Error: Camera access failed. Please check your camera permissions or try refreshing the page.")
446
- st.info("πŸ’‘ **Troubleshooting Tips:**\n- Ensure your camera is not being used by another application\n- Check browser permissions for camera access\n- Try refreshing the page\n- Use the Upload Video tab as an alternative")
447
-
448
- # Provide fallback option
449
- if st.button("πŸ”„ Reset Camera Connection"):
450
- cleanup_webrtc()
451
- st.session_state.webrtc_error = False
452
- st.rerun()
453
-
454
- # Show process button only after recording is complete
455
- if st.session_state.recorded_frames and not st.session_state.recording:
456
- st.markdown("---")
457
- st.subheader("🎯 Process Recorded Video")
458
-
459
- col_process, col_restart = st.columns([2, 1])
460
-
461
- with col_process:
462
- if st.button("πŸ” Process Video with CICE 2.0", type="primary", use_container_width=True, key="process_recorded"):
463
- if not api_key:
464
- st.error("❌ Please enter your Google Gemini API key in the sidebar first")
465
- else:
466
- with st.spinner("πŸ’Ύ Converting recording to video file..."):
467
- video_path = save_recorded_video()
468
- if video_path:
469
- # Read the saved video for analysis
470
- with open(video_path, 'rb') as f:
471
- video_bytes = f.read()
472
-
473
- # Create a file-like object for analysis
474
- class VideoFile:
475
- def __init__(self, data, name):
476
- self.data = data
477
- self.name = name
478
-
479
- def read(self):
480
- return self.data
481
-
482
- def seek(self, position):
483
- pass
484
-
485
- uploaded_file = VideoFile(video_bytes, "recorded_video.mp4")
486
-
487
- # Proceed with analysis
488
- analyze_video(uploaded_file, api_key)
489
-
490
- # Clean up temporary file
491
- os.unlink(video_path)
492
- else:
493
- st.error("❌ Failed to save recording")
494
-
495
- with col_restart:
496
- if st.button("πŸ”„ New Recording", use_container_width=True, key="restart_recording"):
497
  try:
498
- # Clean up everything
499
- st.session_state.recorded_frames = []
500
- st.session_state.recording = False
501
- st.session_state.webrtc_error = False
502
-
503
- # Clear frame queue
504
- while not st.session_state.frame_queue.empty():
505
- try:
506
- st.session_state.frame_queue.get_nowait()
507
- except:
508
- break
509
 
510
- # Clean up WebRTC
511
- cleanup_webrtc()
512
- st.rerun()
 
 
 
513
  except Exception as e:
514
- st.error(f"❌ Error restarting: {str(e)}")
 
 
 
 
 
515
 
516
- # Add fallback information
517
- st.markdown("---")
518
- st.markdown("### πŸ†˜ Having Camera Issues?")
519
- st.info("""
520
- **If you're experiencing camera or recording problems:**
521
-
522
- πŸ“± **Alternative Solutions:**
523
- - Use your phone to record the video and upload it using the "Upload Video" tab
524
- - Record using your computer's built-in camera app and upload the file
525
- - Use screen recording software if recording a virtual meeting
526
-
527
- πŸ”§ **Troubleshooting:**
528
- - Refresh the browser page
529
- - Check camera permissions in your browser
530
- - Close other applications using your camera
531
- - Try using a different browser (Chrome recommended)
532
- """)
533
-
534
- if st.button("πŸ”„ Reset All Camera Settings", key="reset_all_camera"):
535
- try:
536
- # Complete reset
537
- for key in ['recording', 'recorded_frames', 'frame_queue', 'webrtc_ctx', 'webrtc_error']:
538
- if key in st.session_state:
539
- if key == 'frame_queue':
540
- st.session_state[key] = queue.Queue()
541
- elif key == 'recorded_frames':
542
- st.session_state[key] = []
543
- else:
544
- st.session_state[key] = False
545
-
546
- cleanup_webrtc()
547
- st.success("βœ… Camera settings reset! Please refresh the page for a clean start.")
548
- except Exception as e:
549
- st.error(f"❌ Reset failed: {str(e)}")
550
-
551
- with tab2:
552
- st.subheader("Upload Video File")
553
- uploaded_file = st.file_uploader(
554
- "Choose a video file",
555
- type=['mp4', 'webm', 'avi', 'mov'],
556
- help="Upload a video of healthcare team interaction for CICE 2.0 assessment"
557
  )
 
 
 
 
 
 
 
 
 
 
 
 
558
 
559
- if uploaded_file is not None:
560
- st.success(f"βœ… Video uploaded: {uploaded_file.name}")
561
-
562
- # Display video
563
  st.video(uploaded_file)
 
 
 
 
564
 
565
- # Analyze button
566
- if st.button("πŸ” Analyze with CICE 2.0", type="primary"):
567
  if not api_key:
568
- st.error("❌ Please enter your Google Gemini API key in the sidebar")
569
  else:
570
- analyze_video(uploaded_file, api_key)
 
 
 
 
 
 
 
 
571
 
572
- with col2:
573
- st.header("ℹ️ About CICE 2.0")
574
- st.markdown("""
575
- The **Collaborative Interprofessional Team Environment (CICE) 2.0** instrument evaluates healthcare team interactions across 18 key competencies.
576
-
577
- ### 🎯 Purpose
578
- - Assess interprofessional collaboration
579
- - Identify team strengths
580
- - Highlight improvement areas
581
- - Enhance patient care quality
582
-
583
- ### πŸ“Š Scoring Levels
584
- - **Exemplary** (85-100%): Outstanding collaboration
585
- - **Proficient** (70-84%): Good teamwork
586
- - **Developing** (50-69%): Needs improvement
587
- - **Needs Improvement** (<50%): Significant gaps
588
-
589
- ### πŸš€ Getting Started
590
- 1. Enter your Google Gemini API key
591
- 2. Upload a healthcare team video
592
- 3. Click "Analyze with CICE 2.0"
593
- 4. Review detailed results
594
- 5. Download assessment report
595
- """)
596
-
597
- def analyze_video(uploaded_file, api_key):
598
- """Analyze uploaded video with CICE 2.0 assessment"""
599
- try:
600
- assessor = CICE_Assessment(api_key)
601
-
602
- with st.spinner("πŸ€– Analyzing video with CICE 2.0 framework... This may take 1-2 minutes"):
603
- # Reset file pointer
604
- uploaded_file.seek(0)
605
- assessment_result = assessor.analyze_video(uploaded_file)
606
-
607
- # Parse score
608
- observed, percentage, level, color = parse_assessment_score(assessment_result)
609
-
610
- # Display summary
611
- st.markdown(f"""
612
- <div class="score-display">
613
- <h2>CICE 2.0 Assessment Results</h2>
614
- <div style="display: flex; justify-content: space-around; margin: 30px 0;">
615
- <div style="text-align: center;">
616
- <div style="font-size: 48px; font-weight: bold; color: {color};">{observed}/18</div>
617
- <div style="color: #6b7280;">Competencies Observed</div>
618
  </div>
619
- <div style="text-align: center;">
620
- <div style="font-size: 48px; font-weight: bold; color: {color};">{percentage:.0f}%</div>
621
- <div style="color: #6b7280;">Overall Score</div>
622
  </div>
623
  </div>
624
- <div style="text-align: center; padding: 20px; background: #f9fafb; border-radius: 10px;">
625
- <div style="font-size: 24px; font-weight: bold; color: {color};">Performance Level: {level}</div>
626
- </div>
627
- </div>
628
- """, unsafe_allow_html=True)
629
-
630
- # Display detailed assessment
631
- st.markdown('<div class="assessment-box">', unsafe_allow_html=True)
632
- st.markdown("### πŸ“‹ Detailed Assessment Report")
633
- st.write(assessment_result)
634
- st.markdown('</div>', unsafe_allow_html=True)
635
-
636
- # Generate audio feedback
637
- with st.spinner("πŸ”Š Generating audio feedback..."):
638
- audio_data = assessor.generate_audio_feedback(assessment_result)
639
-
640
- # Create formatted text report
641
- timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
642
- formatted_report = f"""CICE 2.0 Healthcare Team Interaction Assessment
643
  {'='*60}
644
  Assessment Date: {timestamp}
645
- Video File: {getattr(uploaded_file, 'name', 'recorded_video.mp4')}
646
  Overall Score: {observed}/18 ({percentage:.1f}%)
647
  Performance Level: {level}
648
  {'='*60}
 
649
  {assessment_result}
 
650
  {'='*60}
651
  Generated by CICE 2.0 Healthcare Assessment Tool
652
  Powered by Google Gemini AI
653
  {'='*60}"""
654
-
655
- # Download options
656
- st.markdown("### πŸ“₯ Download Options")
657
- col_text, col_audio = st.columns(2)
658
-
659
- with col_text:
660
- st.download_button(
661
- label="πŸ“„ Download Text Report",
662
- data=formatted_report,
663
- file_name=f"cice_assessment_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt",
664
- mime="text/plain",
665
- help="Download the complete assessment report as a text file"
666
- )
667
-
668
- with col_audio:
669
- if audio_data:
670
  st.download_button(
671
- label="πŸ”Š Download Audio Report",
672
- data=audio_data,
673
- file_name=f"cice_assessment_audio_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp3",
674
- mime="audio/mpeg",
675
- help="Download the assessment report as an audio file"
676
  )
677
-
678
- # Also display audio player
679
- st.audio(audio_data, format='audio/mp3')
680
- st.caption("🎧 Listen to the assessment report above")
681
- else:
682
- st.error("❌ Audio generation failed")
683
-
684
- # Interactive Q&A Section
685
- st.markdown("### πŸ’¬ Ask Questions About the Assessment")
686
- st.markdown("You can ask specific questions about the CICE competencies and assessment results.")
687
-
688
- # Question input
689
- question = st.text_input(
690
- "❓ Your question:",
691
- placeholder="e.g., 'Was active listening demonstrated?' or 'How did the team handle conflicts?'",
692
- help="Ask specific questions about the competencies or assessment results"
693
- )
694
-
695
- if question and st.button("πŸ€– Get Answer"):
696
- try:
697
- with st.spinner("πŸ€– Analyzing your question..."):
698
- qa_prompt = f"""Based on the CICE 2.0 assessment of this healthcare team video,
699
- please answer this specific question: {question}
700
- Assessment Results:
701
- {assessment_result}
702
- Please provide a detailed answer referring to the relevant competencies from the 18-point CICE framework."""
703
-
704
- # Reset file pointer for Q&A
705
- uploaded_file.seek(0)
706
- temp_path = None
707
- try:
708
- # Re-upload file for Q&A
709
- with tempfile.NamedTemporaryFile(delete=False, suffix='.webm') as tmp_file:
710
- tmp_file.write(uploaded_file.read())
711
- temp_path = tmp_file.name
712
-
713
- uploaded_file_qa = genai.upload_file(path=temp_path, display_name="healthcare_interaction_qa")
714
-
715
- # Wait for processing
716
- max_wait = 60
717
- wait_time = 0
718
- while uploaded_file_qa.state.name == "PROCESSING" and wait_time < max_wait:
719
- time.sleep(2)
720
- wait_time += 2
721
- uploaded_file_qa = genai.get_file(uploaded_file_qa.name)
722
-
723
- if uploaded_file_qa.state.name == "FAILED":
724
- raise Exception("Video processing failed for Q&A")
725
-
726
- qa_response = assessor.model.generate_content([uploaded_file_qa, qa_prompt])
727
-
728
- st.markdown("#### πŸ“ Answer:")
729
- st.write(qa_response.text)
730
 
731
- finally:
732
- if temp_path and os.path.exists(temp_path):
733
- os.unlink(temp_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
734
 
735
- except Exception as e:
736
- st.error(f"❌ Error processing question: {str(e)}")
737
-
738
- # Example questions
739
- st.markdown("**Example questions:**")
740
- st.markdown("""
741
- - Was active listening demonstrated by the team?
742
- - How did the team handle interprofessional conflicts?
743
- - What specific improvements are recommended?
744
- - Which competencies were most lacking?
745
- - How well did team members communicate their roles?
746
- """)
747
-
748
- except Exception as e:
749
- st.error(f"❌ Error during assessment: {str(e)}")
750
 
751
  if __name__ == "__main__":
752
- main()
 
13
  import cv2
14
  import threading
15
  import queue
16
+ from io import BytesIO
17
 
18
  # Page configuration
19
  st.set_page_config(
 
23
  initial_sidebar_state="expanded"
24
  )
25
 
26
+ # Custom CSS for improved styling
27
  st.markdown("""
28
  <style>
29
  .main-header {
30
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
31
+ padding: 2rem;
32
+ border-radius: 15px;
33
  color: white;
34
  text-align: center;
35
+ margin-bottom: 2rem;
36
+ box-shadow: 0 10px 20px rgba(0,0,0,0.1);
37
+ }
38
+ .main-header h1 {
39
+ margin: 0;
40
+ font-size: 2.5rem;
41
+ font-weight: 700;
42
+ }
43
+ .main-header p {
44
+ margin-top: 0.5rem;
45
+ opacity: 0.95;
46
+ font-size: 1.1rem;
47
  }
48
  .assessment-box {
49
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
50
+ padding: 1.5rem;
51
+ border-radius: 12px;
52
+ border-left: 5px solid #667eea;
53
+ margin: 1.5rem 0;
54
  }
55
  .competency-item {
56
+ background: white;
57
+ padding: 1rem;
58
+ margin: 0.5rem 0;
59
  border-radius: 8px;
60
+ border-left: 4px solid #764ba2;
61
+ transition: transform 0.2s;
62
+ }
63
+ .competency-item:hover {
64
+ transform: translateX(5px);
65
  }
66
  .score-display {
67
  text-align: center;
68
+ padding: 2rem;
69
  background: white;
70
+ border-radius: 20px;
71
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
72
+ margin: 1.5rem 0;
73
+ }
74
+ .stButton > button {
75
+ border-radius: 8px;
76
+ font-weight: 600;
77
+ transition: all 0.3s;
78
+ }
79
+ .stButton > button:hover {
80
+ transform: translateY(-2px);
81
+ box-shadow: 0 5px 15px rgba(0,0,0,0.2);
82
+ }
83
+ .recording-status {
84
+ padding: 1rem;
85
+ border-radius: 10px;
86
+ margin: 1rem 0;
87
+ text-align: center;
88
+ font-weight: 600;
89
+ }
90
+ .recording-active {
91
+ background: #fee2e2;
92
+ color: #dc2626;
93
+ border: 2px solid #dc2626;
94
+ animation: pulse 2s infinite;
95
+ }
96
+ .recording-complete {
97
+ background: #d1fae5;
98
+ color: #059669;
99
+ border: 2px solid #059669;
100
+ }
101
+ @keyframes pulse {
102
+ 0% { opacity: 1; }
103
+ 50% { opacity: 0.7; }
104
+ 100% { opacity: 1; }
105
  }
106
  </style>
107
  """, unsafe_allow_html=True)
108
 
109
+ # Initialize session state
110
+ if "recording" not in st.session_state:
111
+ st.session_state.recording = False
112
+ if "recorded_video_data" not in st.session_state:
113
+ st.session_state.recorded_video_data = None
114
+ if "video_processor" not in st.session_state:
115
+ st.session_state.video_processor = None
116
+ if "assessment_result" not in st.session_state:
117
+ st.session_state.assessment_result = None
118
+
119
+ class VideoProcessor:
120
+ def __init__(self):
121
+ self.frames = []
122
+ self.recording = False
123
+
124
+ def recv(self, frame):
125
+ img = frame.to_ndarray(format="bgr24")
126
+
127
+ if self.recording:
128
+ self.frames.append(img)
129
+ # Add recording indicator
130
+ cv2.putText(img, "RECORDING", (30, 30),
131
+ cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
132
+ cv2.circle(img, (15, 30), 5, (0, 0, 255), -1)
133
+
134
+ return av.VideoFrame.from_ndarray(img, format="bgr24")
135
+
136
+ def start_recording(self):
137
+ self.frames = []
138
+ self.recording = True
139
+
140
+ def stop_recording(self):
141
+ self.recording = False
142
+ return self.frames
143
+
144
  # CICE Assessment Class
145
  class CICE_Assessment:
146
  def __init__(self, api_key):
 
159
  temp_path = None
160
  try:
161
  # Save uploaded file to temporary location
162
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_file:
163
  tmp_file.write(video_file.read())
164
  temp_path = tmp_file.name
165
 
 
169
  # Wait for processing
170
  max_wait = 300
171
  wait_time = 0
172
+ progress_bar = st.progress(0)
173
+ status_text = st.empty()
174
+
175
  while uploaded_file.state.name == "PROCESSING" and wait_time < max_wait:
176
  time.sleep(3)
177
  wait_time += 3
178
+ progress = min(wait_time / max_wait, 0.9)
179
+ progress_bar.progress(progress)
180
+ status_text.text(f"Processing video... {wait_time}s / {max_wait}s")
181
  uploaded_file = genai.get_file(uploaded_file.name)
182
 
183
+ progress_bar.progress(1.0)
184
+ status_text.text("Video processing complete!")
185
+
186
  if uploaded_file.state.name == "FAILED":
187
  raise Exception("Video processing failed")
188
 
189
  # The 18-point CICE 2.0 assessment prompt
190
  prompt = """Analyze this healthcare team interaction video and provide a comprehensive assessment based on the CICE 2.0 instrument's 18 interprofessional competencies.
191
+
192
  For EACH of the following 18 competencies, clearly state whether it was "OBSERVED" or "NOT OBSERVED" and provide specific examples with timestamps when possible:
193
+
194
  1. IDENTIFIES FACTORS INFLUENCING HEALTH STATUS
195
  2. IDENTIFIES TEAM GOALS FOR THE PATIENT
196
  3. PRIORITIZES GOALS FOCUSED ON IMPROVING HEALTH OUTCOMES
 
209
  16. REFLECTS ON STRENGTHS OF TEAM INTERACTIONS
210
  17. REFLECTS ON CHALLENGES OF TEAM INTERACTIONS
211
  18. IDENTIFIES HOW TO IMPROVE TEAM EFFECTIVENESS
212
+
213
  STRUCTURE YOUR RESPONSE AS FOLLOWS:
214
+
215
  ## OVERALL ASSESSMENT
216
  Provide a brief overview of the team interaction quality and professionalism.
217
+
218
  ## DETAILED COMPETENCY EVALUATION
219
  For each of the 18 competencies, format as:
220
  Competency [number]: [name]
221
  Status: [OBSERVED/NOT OBSERVED]
222
  Evidence: [Specific examples from the video, or explanation of why it wasn't observed]
223
+
224
  ## STRENGTHS
225
  List 3-5 key strengths observed in the team interaction
226
+
227
  ## AREAS FOR IMPROVEMENT
228
  List 3-5 specific areas where the team could improve
229
+
230
  ## RECOMMENDATIONS
231
  Provide 3-5 actionable recommendations for enhancing team collaboration and patient care
232
+
233
  ## FINAL SCORE
234
  Competencies Observed: X/18
235
  Overall Performance Level: [Exemplary/Proficient/Developing/Needs Improvement]"""
 
241
  # Clean up temporary file
242
  if temp_path and os.path.exists(temp_path):
243
  os.unlink(temp_path)
244
+ progress_bar.empty()
245
+ status_text.empty()
246
 
247
  def generate_audio_feedback(self, text):
248
  """Convert assessment text to audio feedback"""
 
256
  # Generate audio with gTTS
257
  tts = gTTS(text=clean_text, lang='en', slow=False)
258
 
259
+ # Save to BytesIO instead of file
260
+ audio_buffer = BytesIO()
261
+ tts.write_to_fp(audio_buffer)
262
+ audio_buffer.seek(0)
 
 
 
263
 
264
+ return audio_buffer.getvalue()
 
 
 
265
 
266
  except Exception as e:
267
  st.error(f"⚠️ Audio generation failed: {str(e)}")
 
270
  def parse_assessment_score(assessment_text):
271
  """Parse the assessment text to extract score"""
272
  try:
 
 
273
  pattern = r'(\d+)/18'
274
  match = re.search(pattern, assessment_text)
275
  if match:
 
294
  pass
295
  return 0, 0, "Unknown", "#6b7280"
296
 
297
+ def save_frames_to_video(frames):
298
+ """Convert frames to video bytes"""
299
+ if not frames:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  return None
301
+
302
+ height, width, _ = frames[0].shape
303
+
304
+ # Create temporary file
305
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
306
+ temp_path = temp_file.name
307
+ temp_file.close()
308
+
309
+ # Video writer settings
310
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
311
+ fps = 20
312
+ out = cv2.VideoWriter(temp_path, fourcc, fps, (width, height))
313
+
314
+ # Write frames
315
+ for frame in frames:
316
+ out.write(frame)
317
+
318
+ out.release()
319
+
320
+ # Read video bytes
321
+ with open(temp_path, 'rb') as f:
322
+ video_bytes = f.read()
323
+
324
+ # Clean up
325
+ os.unlink(temp_path)
326
+
327
+ return video_bytes
328
 
329
  def main():
330
  # Header
 
352
  st.markdown("---")
353
  st.markdown("""
354
  ### πŸ“‹ CICE 2.0 Competencies
355
+
356
+ **Communication & Teamwork:**
357
  1. Health Status Factors
358
  2. Team Goals Identification
359
  3. Goal Prioritization
360
  4. Role Verbalization
361
  5. Seeking Guidance
362
+
363
+ **Professional Interaction:**
364
  6. Cost-Effective Communication
365
  7. Expertise-Based Questions
366
  8. Avoiding Jargon
367
  9. Explaining Terminology
368
  10. Clear Role Communication
369
+
370
+ **Collaboration Skills:**
371
  11. Active Listening
372
  12. Soliciting Perspectives
373
  13. Recognizing Contributions
374
  14. Team Respect
375
  15. Conflict Resolution
376
+
377
+ **Reflection & Improvement:**
378
  16. Strength Reflection
379
  17. Challenge Reflection
380
  18. Improvement Identification
381
  """)
382
 
383
+ # Main content tabs
384
+ tab1, tab2, tab3 = st.tabs(["πŸŽ₯ Record Video", "πŸ“ Upload Video", "πŸ“Š Results"])
385
 
386
+ with tab1:
387
+ st.header("Live Video Recording")
388
 
389
+ col1, col2 = st.columns([3, 1])
 
390
 
391
+ with col1:
392
+ if not st.session_state.recording and not st.session_state.recorded_video_data:
393
+ st.info("πŸ“Ή Click **Start Recording** to begin capturing your healthcare team interaction")
394
+ elif st.session_state.recording:
395
+ st.markdown('<div class="recording-status recording-active">πŸ”΄ Recording in Progress...</div>',
396
+ unsafe_allow_html=True)
397
+ elif st.session_state.recorded_video_data:
398
+ st.markdown('<div class="recording-status recording-complete">βœ… Recording Complete!</div>',
399
+ unsafe_allow_html=True)
400
+
401
+ with col2:
402
+ if not st.session_state.recording and not st.session_state.recorded_video_data:
403
+ if st.button("πŸ”΄ Start Recording", key="start_btn", use_container_width=True):
404
+ st.session_state.recording = True
405
+ if st.session_state.video_processor:
406
+ st.session_state.video_processor.start_recording()
407
+ st.rerun()
408
 
409
+ elif st.session_state.recording:
410
+ if st.button("⏹️ Stop Recording", key="stop_btn", type="primary", use_container_width=True):
411
+ st.session_state.recording = False
412
+ if st.session_state.video_processor:
413
+ frames = st.session_state.video_processor.stop_recording()
414
+ if frames:
415
+ video_bytes = save_frames_to_video(frames)
416
+ st.session_state.recorded_video_data = video_bytes
417
+ st.success(f"Recording saved! Captured {len(frames)} frames")
418
+ st.rerun()
419
 
420
+ elif st.session_state.recorded_video_data:
421
+ if st.button("πŸ”„ New Recording", key="new_rec_btn", use_container_width=True):
422
+ st.session_state.recorded_video_data = None
423
+ st.session_state.recording = False
424
+ if st.session_state.video_processor:
425
+ st.session_state.video_processor.frames = []
426
+ st.rerun()
427
+
428
+ # Video display area
429
+ if st.session_state.recorded_video_data:
430
+ st.subheader("πŸ“Ή Recorded Video")
431
+ st.video(st.session_state.recorded_video_data)
432
+
433
+ # Process button
434
+ col1, col2, col3 = st.columns([1, 2, 1])
435
+ with col2:
436
+ if st.button("πŸ” Analyze with CICE 2.0", key="analyze_recorded",
437
+ type="primary", use_container_width=True):
438
+ if not api_key:
439
+ st.error("❌ Please enter your Google Gemini API key in the sidebar")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
  try:
442
+ # Create file-like object
443
+ video_file = BytesIO(st.session_state.recorded_video_data)
444
+ video_file.seek(0)
 
 
 
 
 
 
 
 
445
 
446
+ assessor = CICE_Assessment(api_key)
447
+ with st.spinner("πŸ€– Analyzing video with CICE 2.0 framework..."):
448
+ assessment_result = assessor.analyze_video(video_file)
449
+ st.session_state.assessment_result = assessment_result
450
+ st.success("βœ… Analysis complete! Check the Results tab")
451
+
452
  except Exception as e:
453
+ st.error(f"❌ Error: {str(e)}")
454
+ else:
455
+ # WebRTC streamer for recording
456
+ rtc_configuration = RTCConfiguration({
457
+ "iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]
458
+ })
459
 
460
+ if not st.session_state.video_processor:
461
+ st.session_state.video_processor = VideoProcessor()
462
+
463
+ webrtc_ctx = webrtc_streamer(
464
+ key="recorder",
465
+ mode=WebRtcMode.SENDRECV,
466
+ rtc_configuration=rtc_configuration,
467
+ video_processor_factory=lambda: st.session_state.video_processor,
468
+ media_stream_constraints={
469
+ "video": {
470
+ "width": {"ideal": 640},
471
+ "height": {"ideal": 480}
472
+ },
473
+ "audio": False
474
+ },
475
+ async_processing=True,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
  )
477
+
478
+ with tab2:
479
+ st.header("Upload Video File")
480
+
481
+ uploaded_file = st.file_uploader(
482
+ "Choose a video file",
483
+ type=['mp4', 'webm', 'avi', 'mov'],
484
+ help="Upload a video of healthcare team interaction for CICE 2.0 assessment"
485
+ )
486
+
487
+ if uploaded_file is not None:
488
+ col1, col2 = st.columns([3, 1])
489
 
490
+ with col1:
 
 
 
491
  st.video(uploaded_file)
492
+
493
+ with col2:
494
+ st.info(f"**File:** {uploaded_file.name}")
495
+ st.info(f"**Size:** {uploaded_file.size / 1024 / 1024:.2f} MB")
496
 
497
+ if st.button("πŸ” Analyze Video", key="analyze_uploaded",
498
+ type="primary", use_container_width=True):
499
  if not api_key:
500
+ st.error("❌ Please enter your API key")
501
  else:
502
+ try:
503
+ assessor = CICE_Assessment(api_key)
504
+ with st.spinner("πŸ€– Analyzing video..."):
505
+ uploaded_file.seek(0)
506
+ assessment_result = assessor.analyze_video(uploaded_file)
507
+ st.session_state.assessment_result = assessment_result
508
+ st.success("βœ… Analysis complete! Check the Results tab")
509
+ except Exception as e:
510
+ st.error(f"❌ Error: {str(e)}")
511
 
512
+ with tab3:
513
+ if st.session_state.assessment_result:
514
+ assessment_result = st.session_state.assessment_result
515
+
516
+ # Parse score
517
+ observed, percentage, level, color = parse_assessment_score(assessment_result)
518
+
519
+ # Display summary
520
+ st.markdown(f"""
521
+ <div class="score-display">
522
+ <h2>CICE 2.0 Assessment Results</h2>
523
+ <div style="display: flex; justify-content: space-around; margin: 30px 0;">
524
+ <div style="text-align: center;">
525
+ <div style="font-size: 48px; font-weight: bold; color: {color};">{observed}/18</div>
526
+ <div style="color: #6b7280;">Competencies Observed</div>
527
+ </div>
528
+ <div style="text-align: center;">
529
+ <div style="font-size: 48px; font-weight: bold; color: {color};">{percentage:.0f}%</div>
530
+ <div style="color: #6b7280;">Overall Score</div>
531
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
  </div>
533
+ <div style="text-align: center; padding: 20px; background: #f9fafb; border-radius: 10px;">
534
+ <div style="font-size: 24px; font-weight: bold; color: {color};">Performance Level: {level}</div>
 
535
  </div>
536
  </div>
537
+ """, unsafe_allow_html=True)
538
+
539
+ # Detailed assessment
540
+ with st.expander("πŸ“‹ View Detailed Assessment Report", expanded=True):
541
+ st.markdown(assessment_result)
542
+
543
+ # Generate downloads
544
+ assessor = CICE_Assessment(api_key) if api_key else None
545
+
546
+ # Create formatted report
547
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
548
+ formatted_report = f"""CICE 2.0 Healthcare Team Interaction Assessment
 
 
 
 
 
 
 
549
  {'='*60}
550
  Assessment Date: {timestamp}
 
551
  Overall Score: {observed}/18 ({percentage:.1f}%)
552
  Performance Level: {level}
553
  {'='*60}
554
+
555
  {assessment_result}
556
+
557
  {'='*60}
558
  Generated by CICE 2.0 Healthcare Assessment Tool
559
  Powered by Google Gemini AI
560
  {'='*60}"""
561
+
562
+ # Download section
563
+ st.markdown("### πŸ“₯ Download Assessment")
564
+
565
+ col1, col2 = st.columns(2)
566
+
567
+ with col1:
 
 
 
 
 
 
 
 
 
568
  st.download_button(
569
+ label="πŸ“„ Download Text Report",
570
+ data=formatted_report,
571
+ file_name=f"cice_assessment_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt",
572
+ mime="text/plain",
573
+ use_container_width=True
574
  )
575
+
576
+ with col2:
577
+ if assessor and api_key:
578
+ with st.spinner("Generating audio..."):
579
+ audio_data = assessor.generate_audio_feedback(assessment_result)
580
+
581
+ if audio_data:
582
+ st.download_button(
583
+ label="πŸ”Š Download Audio Report",
584
+ data=audio_data,
585
+ file_name=f"cice_audio_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp3",
586
+ mime="audio/mpeg",
587
+ use_container_width=True
588
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
 
590
+ st.audio(audio_data, format='audio/mp3')
591
+
592
+ # Q&A Section
593
+ st.markdown("---")
594
+ st.markdown("### πŸ’¬ Ask Questions About the Assessment")
595
+
596
+ question = st.text_input(
597
+ "Your question:",
598
+ placeholder="e.g., 'Was active listening demonstrated?'",
599
+ key="question_input"
600
+ )
601
+
602
+ if question and st.button("Get Answer", key="qa_btn"):
603
+ if assessor and api_key:
604
+ try:
605
+ with st.spinner("Analyzing question..."):
606
+ qa_prompt = f"""Based on this CICE 2.0 assessment, answer: {question}
607
 
608
+ Assessment: {assessment_result}"""
609
+
610
+ response = assessor.model.generate_content(qa_prompt)
611
+ st.markdown("#### Answer:")
612
+ st.write(response.text)
613
+ except Exception as e:
614
+ st.error(f"Error: {str(e)}")
615
+ else:
616
+ st.error("Please configure API key")
617
+ else:
618
+ st.info("No assessment results yet. Please record or upload a video and run the analysis.")
 
 
 
 
619
 
620
  if __name__ == "__main__":
621
+ main()