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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +363 -370
app.py CHANGED
@@ -13,7 +13,6 @@ import numpy as np
13
  import cv2
14
  import threading
15
  import queue
16
- from io import BytesIO
17
 
18
  # Page configuration
19
  st.set_page_config(
@@ -23,124 +22,42 @@ 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,7 +76,7 @@ class CICE_Assessment:
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,28 +86,17 @@ class CICE_Assessment:
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,27 +115,20 @@ class CICE_Assessment:
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,8 +140,6 @@ class CICE_Assessment:
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,12 +153,18 @@ class CICE_Assessment:
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,6 +173,8 @@ class CICE_Assessment:
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,37 +199,44 @@ def parse_assessment_score(assessment_text):
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,270 +264,351 @@ def main():
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()
 
13
  import cv2
14
  import threading
15
  import queue
 
16
 
17
  # Page configuration
18
  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
  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
  # 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
  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
  # 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
  # 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
  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
  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
+
210
+ def video_frame_callback(frame):
211
+ """Callback function to process video frames during recording"""
212
+ if st.session_state.recording:
213
+ img = frame.to_ndarray(format="bgr24")
214
+ st.session_state.frame_queue.put(img)
215
+ return frame
216
+
217
+ def save_recorded_video():
218
+ """Save recorded frames to a video file"""
219
+ if st.session_state.recorded_frames:
220
+ # Create temporary file
221
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
222
+ temp_path = temp_file.name
223
+ temp_file.close()
224
+
225
+ # Video properties
226
+ height, width, _ = st.session_state.recorded_frames[0].shape
227
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
228
+ fps = 20 # frames per second
229
+
230
+ # Create video writer
231
+ out = cv2.VideoWriter(temp_path, fourcc, fps, (width, height))
232
+
233
+ # Write frames
234
+ for frame in st.session_state.recorded_frames:
235
+ out.write(frame)
236
+
237
+ out.release()
238
+ return temp_path
239
+ return None
240
 
241
  def main():
242
  # Header
 
264
  st.markdown("---")
265
  st.markdown("""
266
  ### πŸ“‹ CICE 2.0 Competencies
 
 
267
  1. Health Status Factors
268
  2. Team Goals Identification
269
  3. Goal Prioritization
270
  4. Role Verbalization
271
  5. Seeking Guidance
 
 
272
  6. Cost-Effective Communication
273
  7. Expertise-Based Questions
274
  8. Avoiding Jargon
275
  9. Explaining Terminology
276
  10. Clear Role Communication
 
 
277
  11. Active Listening
278
  12. Soliciting Perspectives
279
  13. Recognizing Contributions
280
  14. Team Respect
281
  15. Conflict Resolution
 
 
282
  16. Strength Reflection
283
  17. Challenge Reflection
284
  18. Improvement Identification
285
  """)
286
 
287
+ # Main content
288
+ col1, col2 = st.columns([2, 1])
289
 
290
+ with col1:
291
+ st.header("πŸ“Ή Record or Upload Healthcare Team Video")
 
 
292
 
293
+ # Tab selection for recording vs uploading
294
+ tab1, tab2 = st.tabs(["πŸŽ₯ Record Video", "πŸ“ Upload Video"])
 
 
 
 
 
 
 
295
 
296
+ with tab1:
297
+ st.subheader("Live Video Recording")
298
+ st.markdown("Click **Start Recording** to activate your webcam and begin recording a healthcare team interaction.")
 
 
 
 
299
 
300
+ # WebRTC configuration
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  rtc_configuration = RTCConfiguration({
302
  "iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]
303
  })
304
 
305
+ # Recording controls - only show when no completed recording exists
306
+ if not st.session_state.recorded_frames:
307
+ st.markdown("### 🎬 Recording Controls")
308
+ col_start, col_stop, col_status = st.columns([1, 1, 2])
309
+
310
+ with col_start:
311
+ if st.button("πŸ”΄ Start Recording", disabled=st.session_state.recording, type="primary", key="start_rec_btn"):
312
+ st.session_state.recording = True
313
+ st.session_state.recorded_frames = []
314
+ # Clear the queue
315
+ while not st.session_state.frame_queue.empty():
316
+ st.session_state.frame_queue.get()
317
+ st.rerun()
318
+
319
+ with col_stop:
320
+ if st.button("⏹️ Stop Recording", disabled=not st.session_state.recording, type="secondary", key="stop_rec_btn"):
321
+ st.session_state.recording = False
322
+
323
+ # Collect frames from queue
324
+ frames_collected = []
325
+ while not st.session_state.frame_queue.empty():
326
+ frame = st.session_state.frame_queue.get()
327
+ frames_collected.append(frame)
328
+
329
+ st.session_state.recorded_frames = frames_collected
330
+ st.success(f"Recording stopped! Captured {len(frames_collected)} frames.")
331
+ st.rerun()
332
+
333
+ with col_status:
334
+ if st.session_state.recording:
335
+ st.markdown("πŸ”΄ **Recording in progress...**")
336
+ st.markdown("*Click Stop Recording when finished*")
337
+ else:
338
+ st.markdown("βšͺ **Ready to record**")
339
+ st.markdown("*Click Start Recording to begin*")
340
+
341
+ # Show recording completion status
342
+ if st.session_state.recorded_frames and not st.session_state.recording:
343
+ st.markdown("### βœ… Recording Complete")
344
+ st.success(f"**Recording finished successfully!** Captured {len(st.session_state.recorded_frames)} frames")
345
+ st.info("πŸ“Ή Your video is ready for analysis. Use the Process Video button below.")
346
+
347
+ # Only show WebRTC streamer when recording is active or about to start
348
+ if st.session_state.recording or not st.session_state.recorded_frames:
349
+ # WebRTC streamer
350
+ webrtc_ctx = webrtc_streamer(
351
+ key="video-recorder",
352
+ mode=WebRtcMode.SENDONLY,
353
+ rtc_configuration=rtc_configuration,
354
+ video_frame_callback=video_frame_callback,
355
+ media_stream_constraints={
356
+ "video": {
357
+ "width": {"ideal": 640, "max": 640},
358
+ "height": {"ideal": 480, "max": 480},
359
+ "frameRate": {"ideal": 20, "max": 30}
360
+ },
361
+ "audio": False # Disable audio for now
362
  },
363
+ async_processing=True,
364
+ )
365
+
366
+ # Show process button only after recording is complete
367
+ if st.session_state.recorded_frames and not st.session_state.recording:
368
+ st.markdown("---")
369
+ st.subheader("🎯 Process Recorded Video")
370
+
371
+ col_process, col_restart = st.columns([2, 1])
372
+
373
+ with col_process:
374
+ if st.button("πŸ” Process Video with CICE 2.0", type="primary", use_container_width=True, key="process_recorded"):
375
+ if not api_key:
376
+ st.error("❌ Please enter your Google Gemini API key in the sidebar first")
377
+ else:
378
+ with st.spinner("πŸ’Ύ Converting recording to video file..."):
379
+ video_path = save_recorded_video()
380
+ if video_path:
381
+ # Read the saved video for analysis
382
+ with open(video_path, 'rb') as f:
383
+ video_bytes = f.read()
384
+
385
+ # Create a file-like object for analysis
386
+ class VideoFile:
387
+ def __init__(self, data, name):
388
+ self.data = data
389
+ self.name = name
390
+
391
+ def read(self):
392
+ return self.data
393
+
394
+ def seek(self, position):
395
+ pass
396
+
397
+ uploaded_file = VideoFile(video_bytes, "recorded_video.mp4")
398
+
399
+ # Proceed with analysis
400
+ analyze_video(uploaded_file, api_key)
401
+
402
+ # Clean up temporary file
403
+ os.unlink(video_path)
404
+ else:
405
+ st.error("❌ Failed to save recording")
406
+
407
+ with col_restart:
408
+ if st.button("πŸ”„ New Recording", use_container_width=True, key="restart_recording"):
409
+ st.session_state.recorded_frames = []
410
+ st.session_state.recording = False
411
+ st.rerun()
412
 
413
+ with tab2:
414
+ st.subheader("Upload Video File")
415
+ uploaded_file = st.file_uploader(
416
+ "Choose a video file",
417
+ type=['mp4', 'webm', 'avi', 'mov'],
418
+ help="Upload a video of healthcare team interaction for CICE 2.0 assessment"
419
+ )
420
 
421
+ if uploaded_file is not None:
422
+ st.success(f"βœ… Video uploaded: {uploaded_file.name}")
423
+
424
+ # Display video
425
  st.video(uploaded_file)
 
 
 
 
426
 
427
+ # Analyze button
428
+ if st.button("πŸ” Analyze with CICE 2.0", type="primary"):
429
  if not api_key:
430
+ st.error("❌ Please enter your Google Gemini API key in the sidebar")
431
  else:
432
+ analyze_video(uploaded_file, api_key)
 
 
 
 
 
 
 
 
433
 
434
+ with col2:
435
+ st.header("ℹ️ About CICE 2.0")
436
+ st.markdown("""
437
+ The **Collaborative Interprofessional Team Environment (CICE) 2.0** instrument evaluates healthcare team interactions across 18 key competencies.
438
+
439
+ ### 🎯 Purpose
440
+ - Assess interprofessional collaboration
441
+ - Identify team strengths
442
+ - Highlight improvement areas
443
+ - Enhance patient care quality
444
+
445
+ ### πŸ“Š Scoring Levels
446
+ - **Exemplary** (85-100%): Outstanding collaboration
447
+ - **Proficient** (70-84%): Good teamwork
448
+ - **Developing** (50-69%): Needs improvement
449
+ - **Needs Improvement** (<50%): Significant gaps
450
+
451
+ ### πŸš€ Getting Started
452
+ 1. Enter your Google Gemini API key
453
+ 2. Upload a healthcare team video
454
+ 3. Click "Analyze with CICE 2.0"
455
+ 4. Review detailed results
456
+ 5. Download assessment report
457
+ """)
458
+
459
+ def analyze_video(uploaded_file, api_key):
460
+ """Analyze uploaded video with CICE 2.0 assessment"""
461
+ try:
462
+ assessor = CICE_Assessment(api_key)
463
+
464
+ with st.spinner("πŸ€– Analyzing video with CICE 2.0 framework... This may take 1-2 minutes"):
465
+ # Reset file pointer
466
+ uploaded_file.seek(0)
467
+ assessment_result = assessor.analyze_video(uploaded_file)
468
+
469
+ # Parse score
470
+ observed, percentage, level, color = parse_assessment_score(assessment_result)
471
+
472
+ # Display summary
473
+ st.markdown(f"""
474
+ <div class="score-display">
475
+ <h2>CICE 2.0 Assessment Results</h2>
476
+ <div style="display: flex; justify-content: space-around; margin: 30px 0;">
477
+ <div style="text-align: center;">
478
+ <div style="font-size: 48px; font-weight: bold; color: {color};">{observed}/18</div>
479
+ <div style="color: #6b7280;">Competencies Observed</div>
480
  </div>
481
+ <div style="text-align: center;">
482
+ <div style="font-size: 48px; font-weight: bold; color: {color};">{percentage:.0f}%</div>
483
+ <div style="color: #6b7280;">Overall Score</div>
484
  </div>
485
  </div>
486
+ <div style="text-align: center; padding: 20px; background: #f9fafb; border-radius: 10px;">
487
+ <div style="font-size: 24px; font-weight: bold; color: {color};">Performance Level: {level}</div>
488
+ </div>
489
+ </div>
490
+ """, unsafe_allow_html=True)
491
+
492
+ # Display detailed assessment
493
+ st.markdown('<div class="assessment-box">', unsafe_allow_html=True)
494
+ st.markdown("### πŸ“‹ Detailed Assessment Report")
495
+ st.write(assessment_result)
496
+ st.markdown('</div>', unsafe_allow_html=True)
497
+
498
+ # Generate audio feedback
499
+ with st.spinner("πŸ”Š Generating audio feedback..."):
500
+ audio_data = assessor.generate_audio_feedback(assessment_result)
501
+
502
+ # Create formatted text report
503
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
504
+ formatted_report = f"""CICE 2.0 Healthcare Team Interaction Assessment
505
  {'='*60}
506
  Assessment Date: {timestamp}
507
+ Video File: {getattr(uploaded_file, 'name', 'recorded_video.mp4')}
508
  Overall Score: {observed}/18 ({percentage:.1f}%)
509
  Performance Level: {level}
510
  {'='*60}
 
511
  {assessment_result}
 
512
  {'='*60}
513
  Generated by CICE 2.0 Healthcare Assessment Tool
514
  Powered by Google Gemini AI
515
  {'='*60}"""
516
+
517
+ # Download options
518
+ st.markdown("### πŸ“₯ Download Options")
519
+ col_text, col_audio = st.columns(2)
520
+
521
+ with col_text:
522
+ st.download_button(
523
+ label="πŸ“„ Download Text Report",
524
+ data=formatted_report,
525
+ file_name=f"cice_assessment_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt",
526
+ mime="text/plain",
527
+ help="Download the complete assessment report as a text file"
528
+ )
529
+
530
+ with col_audio:
531
+ if audio_data:
532
  st.download_button(
533
+ label="πŸ”Š Download Audio Report",
534
+ data=audio_data,
535
+ file_name=f"cice_assessment_audio_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp3",
536
+ mime="audio/mpeg",
537
+ help="Download the assessment report as an audio file"
538
  )
539
+
540
+ # Also display audio player
541
+ st.audio(audio_data, format='audio/mp3')
542
+ st.caption("🎧 Listen to the assessment report above")
543
+ else:
544
+ st.error("❌ Audio generation failed")
545
+
546
+ # Interactive Q&A Section
547
+ st.markdown("### πŸ’¬ Ask Questions About the Assessment")
548
+ st.markdown("You can ask specific questions about the CICE competencies and assessment results.")
549
+
550
+ # Question input
551
+ question = st.text_input(
552
+ "❓ Your question:",
553
+ placeholder="e.g., 'Was active listening demonstrated?' or 'How did the team handle conflicts?'",
554
+ help="Ask specific questions about the competencies or assessment results"
555
+ )
556
+
557
+ if question and st.button("πŸ€– Get Answer"):
558
+ try:
559
+ with st.spinner("πŸ€– Analyzing your question..."):
560
+ qa_prompt = f"""Based on the CICE 2.0 assessment of this healthcare team video,
561
+ please answer this specific question: {question}
562
+ Assessment Results:
563
+ {assessment_result}
564
+ Please provide a detailed answer referring to the relevant competencies from the 18-point CICE framework."""
565
+
566
+ # Reset file pointer for Q&A
567
+ uploaded_file.seek(0)
568
+ temp_path = None
569
  try:
570
+ # Re-upload file for Q&A
571
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.webm') as tmp_file:
572
+ tmp_file.write(uploaded_file.read())
573
+ temp_path = tmp_file.name
574
+
575
+ uploaded_file_qa = genai.upload_file(path=temp_path, display_name="healthcare_interaction_qa")
576
+
577
+ # Wait for processing
578
+ max_wait = 60
579
+ wait_time = 0
580
+ while uploaded_file_qa.state.name == "PROCESSING" and wait_time < max_wait:
581
+ time.sleep(2)
582
+ wait_time += 2
583
+ uploaded_file_qa = genai.get_file(uploaded_file_qa.name)
584
+
585
+ if uploaded_file_qa.state.name == "FAILED":
586
+ raise Exception("Video processing failed for Q&A")
587
+
588
+ qa_response = assessor.model.generate_content([uploaded_file_qa, qa_prompt])
589
+
590
+ st.markdown("#### πŸ“ Answer:")
591
+ st.write(qa_response.text)
592
+
593
+ finally:
594
+ if temp_path and os.path.exists(temp_path):
595
+ os.unlink(temp_path)
596
 
597
+ except Exception as e:
598
+ st.error(f"❌ Error processing question: {str(e)}")
599
+
600
+ # Example questions
601
+ st.markdown("**Example questions:**")
602
+ st.markdown("""
603
+ - Was active listening demonstrated by the team?
604
+ - How did the team handle interprofessional conflicts?
605
+ - What specific improvements are recommended?
606
+ - Which competencies were most lacking?
607
+ - How well did team members communicate their roles?
608
+ """)
609
+
610
+ except Exception as e:
611
+ st.error(f"❌ Error during assessment: {str(e)}")
612
 
613
  if __name__ == "__main__":
614
+ main()