Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -2,13 +2,13 @@ import spaces
|
|
| 2 |
|
| 3 |
# Configure ZeroGPU
|
| 4 |
@spaces.GPU
|
| 5 |
-
def process_video_with_gpu(video, resize_option):
|
| 6 |
-
"""ZeroGPU-accelerated video processing"""
|
| 7 |
# Create assessor inside the GPU function to avoid pickling issues
|
| 8 |
from google import genai
|
| 9 |
client = genai.Client(api_key=GOOGLE_API_KEY)
|
| 10 |
assessor = CICE_Assessment(client)
|
| 11 |
-
return process_video_core(video, resize_option, assessor)
|
| 12 |
|
| 13 |
import gradio as gr
|
| 14 |
from google import genai
|
|
@@ -32,23 +32,23 @@ import subprocess
|
|
| 32 |
import shutil
|
| 33 |
|
| 34 |
# Configure Google API Key from environment variable or Hugging Face secrets
|
| 35 |
-
print("
|
| 36 |
GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')
|
| 37 |
|
| 38 |
if not GOOGLE_API_KEY:
|
| 39 |
raise ValueError("GOOGLE_API_KEY environment variable is not set. Please set it in Hugging Face Spaces secrets.")
|
| 40 |
|
| 41 |
client = genai.Client(api_key=GOOGLE_API_KEY)
|
| 42 |
-
print("
|
| 43 |
|
| 44 |
-
# Define the CICE Assessment Class
|
| 45 |
class CICE_Assessment:
|
| 46 |
def __init__(self, client):
|
| 47 |
self.client = client
|
| 48 |
self.model_name = "gemini-2.0-flash-exp"
|
| 49 |
|
| 50 |
-
def analyze_video(self, video_path):
|
| 51 |
-
"""Analyze video using
|
| 52 |
|
| 53 |
try:
|
| 54 |
# Determine mime type based on file extension
|
|
@@ -59,12 +59,12 @@ class CICE_Assessment:
|
|
| 59 |
mime_type = 'video/mp4'
|
| 60 |
|
| 61 |
# Upload video to Gemini
|
| 62 |
-
print(f"
|
| 63 |
with open(video_path, 'rb') as f:
|
| 64 |
video_file = self.client.files.upload(file=f, config={'mime_type': mime_type})
|
| 65 |
|
| 66 |
# Wait for processing
|
| 67 |
-
print("
|
| 68 |
max_wait = 300
|
| 69 |
wait_time = 0
|
| 70 |
while video_file.state == "PROCESSING" and wait_time < max_wait:
|
|
@@ -75,103 +75,148 @@ class CICE_Assessment:
|
|
| 75 |
if video_file.state == "FAILED":
|
| 76 |
raise Exception("Video processing failed")
|
| 77 |
|
| 78 |
-
print("
|
| 79 |
|
| 80 |
-
#
|
| 81 |
-
prompt =
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
LOOK FOR: CPR/AED prioritized before bleeding/dental injury, EpiPen administered before addressing secondary injuries
|
| 93 |
-
|
| 94 |
-
4. VERBALIZES DISCIPLINE-SPECIFIC ROLE (PRE-BRIEF)
|
| 95 |
-
LOOK FOR: Students acknowledge interprofessional communication expectations and scene safety review before scenario begins
|
| 96 |
-
|
| 97 |
-
5. OFFERS TO SEEK GUIDANCE FROM COLLEAGUES
|
| 98 |
-
LOOK FOR: Peer-to-peer checks (e.g., dental to dental: confirm tooth storage; nursing to nursing: confirm CPR quality)
|
| 99 |
-
|
| 100 |
-
6. COMMUNICATES ABOUT COST-EFFECTIVE AND TIMELY CARE
|
| 101 |
-
LOOK FOR: Team chooses readily available supplies (AED, saline, tourniquet) without delay, states need for rapid EMS transfer
|
| 102 |
-
|
| 103 |
-
7. DIRECTS QUESTIONS TO OTHER HEALTH PROFESSIONALS BASED ON EXPERTISE
|
| 104 |
-
LOOK FOR: Asks discipline-specific expertise (e.g., "Dental—what do we do with the tooth?"), invites pharmacy/medical input on epinephrine use
|
| 105 |
-
|
| 106 |
-
8. AVOIDS DISCIPLINE-SPECIFIC TERMINOLOGY
|
| 107 |
-
LOOK FOR: Uses plain language like "no pulse" instead of "asystole"
|
| 108 |
-
|
| 109 |
-
9. EXPLAINS DISCIPLINE-SPECIFIC TERMINOLOGY WHEN NECESSARY
|
| 110 |
-
LOOK FOR: Clarifies medical/dental terms for others when necessary
|
| 111 |
-
|
| 112 |
-
10. COMMUNICATES ROLES AND RESPONSIBILITIES CLEARLY
|
| 113 |
-
LOOK FOR: Announces assignments out loud: "I'll do compressions," "I'll call 911," "I'll document"
|
| 114 |
-
|
| 115 |
-
11. ENGAGES IN ACTIVE LISTENING
|
| 116 |
-
LOOK FOR: Repeats back instructions ("Everyone clear for shock"), pauses to hear teammates' updates
|
| 117 |
-
|
| 118 |
-
12. SOLICITS AND ACKNOWLEDGES PERSPECTIVES
|
| 119 |
-
LOOK FOR: Leader asks "Anything else we need to address?", responds to peer input respectfully
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
-
|
| 131 |
-
LOOK FOR: Notes strong teamwork, communication, or role clarity after the scenario
|
| 132 |
|
| 133 |
-
|
| 134 |
-
LOOK FOR: Identifies confusion, delays, or role overlap in debriefing
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
STRUCTURE YOUR RESPONSE AS FOLLOWS:
|
| 140 |
|
| 141 |
-
## OVERALL ASSESSMENT
|
| 142 |
-
|
| 143 |
|
| 144 |
-
## DETAILED
|
| 145 |
-
For each
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
|
|
|
|
|
|
| 149 |
|
| 150 |
-
## STRENGTHS
|
| 151 |
-
Top 3-5
|
| 152 |
|
| 153 |
-
##
|
| 154 |
-
Top 3-5 areas needing
|
| 155 |
|
| 156 |
-
##
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
| 162 |
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
contents=[
|
| 166 |
-
types.Part.from_uri(file_uri=video_file.uri, mime_type=video_file.mime_type),
|
| 167 |
-
prompt
|
| 168 |
-
]
|
| 169 |
-
)
|
| 170 |
-
print("✅ Analysis complete!")
|
| 171 |
-
return response.text
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
def generate_audio_feedback(self, text):
|
| 177 |
"""Generate a concise 1-minute audio feedback summary"""
|
|
@@ -191,11 +236,11 @@ Overall Performance Level: [Exemplary (85-100%)/Proficient (70-84%)/Developing (
|
|
| 191 |
clean_text = re.sub(r'[-•·]\s+', '', clean_text)
|
| 192 |
|
| 193 |
# Add introduction and conclusion for better audio experience
|
| 194 |
-
audio_script = f"""
|
| 195 |
|
| 196 |
{clean_text}
|
| 197 |
|
| 198 |
-
Please refer to the detailed written report for complete
|
| 199 |
End of audio summary."""
|
| 200 |
|
| 201 |
# Generate audio with gTTS
|
|
@@ -209,99 +254,61 @@ Overall Performance Level: [Exemplary (85-100%)/Proficient (70-84%)/Developing (
|
|
| 209 |
|
| 210 |
return temp_audio.name
|
| 211 |
except Exception as e:
|
| 212 |
-
print(f"
|
| 213 |
return None
|
| 214 |
|
| 215 |
def create_brief_summary(self, text):
|
| 216 |
"""Create a brief summary if AUDIO SUMMARY section is not found"""
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
elif percentage >= 50:
|
| 229 |
-
level = "Developing"
|
| 230 |
-
else:
|
| 231 |
-
level = "Needs Improvement"
|
| 232 |
-
|
| 233 |
-
summary = f"""The team demonstrated {level} performance with {observed_count} out of {total} competencies observed,
|
| 234 |
-
achieving {percentage:.0f} percent overall.
|
| 235 |
-
|
| 236 |
-
Key strengths included strong team communication and role clarity.
|
| 237 |
-
|
| 238 |
-
Areas for improvement include enhancing active listening and conflict resolution skills.
|
| 239 |
-
|
| 240 |
-
The team should focus on pre-briefing protocols and post-scenario debriefing to enhance future performance.
|
| 241 |
-
Emphasis should be placed on clear role assignment and closed-loop communication during critical interventions."""
|
| 242 |
|
| 243 |
return summary
|
| 244 |
|
| 245 |
-
def parse_assessment_scores(self, assessment_text):
|
| 246 |
-
"""Parse assessment text to extract scores"""
|
| 247 |
|
| 248 |
-
#
|
| 249 |
import re
|
| 250 |
|
| 251 |
-
#
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
# Count only "OBSERVED" (not "NOT OBSERVED")
|
| 256 |
-
observed_count = sum(1 for match in matches if match.upper() == "OBSERVED")
|
| 257 |
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
for i, line in enumerate(lines):
|
| 265 |
-
# Look for competency indicators followed by status
|
| 266 |
-
if 'Competency' in line and i + 1 < len(lines):
|
| 267 |
-
next_line = lines[i + 1]
|
| 268 |
-
# Check if the status line indicates OBSERVED (not NOT OBSERVED)
|
| 269 |
-
if 'OBSERVED' in next_line.upper() and 'NOT OBSERVED' not in next_line.upper():
|
| 270 |
-
observed_count += 1
|
| 271 |
-
|
| 272 |
-
# If still no matches, use a more robust pattern
|
| 273 |
-
if observed_count == 0:
|
| 274 |
-
# Count lines that say "OBSERVED" but not "NOT OBSERVED"
|
| 275 |
-
for line in lines:
|
| 276 |
-
# Clean line for better matching
|
| 277 |
-
clean_line = line.strip().upper()
|
| 278 |
-
if clean_line.startswith('STATUS:'):
|
| 279 |
-
if 'NOT OBSERVED' in clean_line:
|
| 280 |
-
continue
|
| 281 |
-
elif 'OBSERVED' in clean_line:
|
| 282 |
-
observed_count += 1
|
| 283 |
|
| 284 |
-
|
| 285 |
-
percentage = (observed_count / total_competencies) * 100 if total_competencies > 0 else 0
|
| 286 |
|
| 287 |
-
#
|
| 288 |
-
if
|
| 289 |
level = "Exemplary"
|
| 290 |
color = "#0F766E" # Deep teal
|
| 291 |
-
elif
|
| 292 |
level = "Proficient"
|
| 293 |
color = "#1E40AF" # Professional blue
|
| 294 |
-
elif
|
| 295 |
level = "Developing"
|
| 296 |
color = "#EA580C" # Professional orange
|
| 297 |
else:
|
| 298 |
level = "Needs Improvement"
|
| 299 |
color = "#B91C1C" # Deep red
|
| 300 |
|
| 301 |
-
return
|
| 302 |
|
| 303 |
-
def generate_pdf_report(self, assessment_text):
|
| 304 |
-
"""Generate a PDF report from the assessment text"""
|
| 305 |
|
| 306 |
try:
|
| 307 |
# Create a temporary file for the PDF
|
|
@@ -326,7 +333,7 @@ Overall Performance Level: [Exemplary (85-100%)/Proficient (70-84%)/Developing (
|
|
| 326 |
'CustomTitle',
|
| 327 |
parent=styles['Heading1'],
|
| 328 |
fontSize=24,
|
| 329 |
-
textColor=HexColor('#111827'),
|
| 330 |
spaceAfter=30,
|
| 331 |
alignment=TA_CENTER
|
| 332 |
)
|
|
@@ -335,7 +342,7 @@ Overall Performance Level: [Exemplary (85-100%)/Proficient (70-84%)/Developing (
|
|
| 335 |
'CustomHeading',
|
| 336 |
parent=styles['Heading2'],
|
| 337 |
fontSize=14,
|
| 338 |
-
textColor=HexColor('#1E40AF'),
|
| 339 |
spaceAfter=12,
|
| 340 |
spaceBefore=12,
|
| 341 |
bold=True
|
|
@@ -350,13 +357,23 @@ Overall Performance Level: [Exemplary (85-100%)/Proficient (70-84%)/Developing (
|
|
| 350 |
)
|
| 351 |
|
| 352 |
# Add title
|
| 353 |
-
elements.append(Paragraph("
|
|
|
|
| 354 |
elements.append(Spacer(1, 12))
|
| 355 |
|
| 356 |
# Add timestamp
|
| 357 |
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 358 |
elements.append(Paragraph(f"<b>Assessment Date:</b> {timestamp}", body_style))
|
| 359 |
elements.append(Spacer(1, 20))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
|
| 361 |
# Process the assessment text into PDF-friendly format
|
| 362 |
lines = assessment_text.split('\n')
|
|
@@ -370,15 +387,12 @@ Overall Performance Level: [Exemplary (85-100%)/Proficient (70-84%)/Developing (
|
|
| 370 |
# Major heading
|
| 371 |
heading_text = line.replace('##', '').strip()
|
| 372 |
elements.append(Paragraph(heading_text, heading_style))
|
| 373 |
-
elif line.startswith('
|
| 374 |
-
#
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
# Sub-items
|
| 378 |
-
elements.append(Paragraph(line, body_style))
|
| 379 |
else:
|
| 380 |
-
# Regular text
|
| 381 |
-
# Escape special characters for PDF
|
| 382 |
line = line.replace('&', '&').replace('<', '<').replace('>', '>')
|
| 383 |
elements.append(Paragraph(line, body_style))
|
| 384 |
|
|
@@ -389,13 +403,15 @@ Overall Performance Level: [Exemplary (85-100%)/Proficient (70-84%)/Developing (
|
|
| 389 |
return temp_pdf.name
|
| 390 |
|
| 391 |
except Exception as e:
|
| 392 |
-
print(f"
|
| 393 |
# Fallback to text file
|
| 394 |
temp_txt = tempfile.NamedTemporaryFile(delete=False, suffix='.txt', mode='w')
|
| 395 |
-
temp_txt.write("
|
|
|
|
| 396 |
temp_txt.write("="*60 + "\n")
|
| 397 |
temp_txt.write(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
| 398 |
temp_txt.write("="*60 + "\n\n")
|
|
|
|
| 399 |
temp_txt.write(assessment_text)
|
| 400 |
temp_txt.close()
|
| 401 |
return temp_txt.name
|
|
@@ -421,7 +437,7 @@ def resize_video(input_path, target_width, target_height):
|
|
| 421 |
# Create video writer with new dimensions
|
| 422 |
out = cv2.VideoWriter(temp_output.name, fourcc, fps, (target_width, target_height))
|
| 423 |
|
| 424 |
-
print(f"
|
| 425 |
frame_count = 0
|
| 426 |
|
| 427 |
while True:
|
|
@@ -437,11 +453,11 @@ def resize_video(input_path, target_width, target_height):
|
|
| 437 |
cap.release()
|
| 438 |
out.release()
|
| 439 |
|
| 440 |
-
print(f"
|
| 441 |
return temp_output.name
|
| 442 |
|
| 443 |
except Exception as e:
|
| 444 |
-
print(f"
|
| 445 |
return input_path # Return original if resize fails
|
| 446 |
|
| 447 |
def get_video_info(video_path):
|
|
@@ -496,7 +512,7 @@ def show_saving_status(video):
|
|
| 496 |
</style>
|
| 497 |
<div style="text-align: center; color: white;">
|
| 498 |
<div style="font-size: 24px; font-weight: bold; margin-bottom: 10px;">
|
| 499 |
-
|
| 500 |
</div>
|
| 501 |
<div style="font-size: 16px; opacity: 0.95;">
|
| 502 |
Saving video file • Preparing for download
|
|
@@ -530,7 +546,7 @@ def save_recorded_video_with_status(video):
|
|
| 530 |
<div style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 15px; border-radius: 10px; margin: 20px 0;">
|
| 531 |
<div style="text-align: center; color: white;">
|
| 532 |
<div style="font-size: 20px; font-weight: bold;">
|
| 533 |
-
|
| 534 |
</div>
|
| 535 |
<div style="font-size: 14px; margin-top: 5px; opacity: 0.95;">
|
| 536 |
Ready for download • Click "Analyze Video" to assess
|
|
@@ -539,16 +555,16 @@ def save_recorded_video_with_status(video):
|
|
| 539 |
</div>
|
| 540 |
"""
|
| 541 |
|
| 542 |
-
print(f"
|
| 543 |
return temp_output.name, gr.update(value=success_html, visible=True)
|
| 544 |
|
| 545 |
except Exception as e:
|
| 546 |
-
print(f"
|
| 547 |
error_html = """
|
| 548 |
<div style="background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); padding: 15px; border-radius: 10px; margin: 20px 0;">
|
| 549 |
<div style="text-align: center; color: white;">
|
| 550 |
<div style="font-size: 20px; font-weight: bold;">
|
| 551 |
-
|
| 552 |
</div>
|
| 553 |
<div style="font-size: 14px; margin-top: 5px;">
|
| 554 |
Please try recording again
|
|
@@ -558,15 +574,9 @@ def save_recorded_video_with_status(video):
|
|
| 558 |
"""
|
| 559 |
return None, gr.update(value=error_html, visible=True)
|
| 560 |
|
| 561 |
-
# Function to hide status after a delay
|
| 562 |
-
def hide_status_after_delay():
|
| 563 |
-
"""Hide the status bar after showing success"""
|
| 564 |
-
time.sleep(3) # Wait 3 seconds
|
| 565 |
-
return gr.update(value="", visible=False)
|
| 566 |
-
|
| 567 |
# Define the core processing function (separate from GPU wrapper)
|
| 568 |
-
def process_video_core(video, resize_option, assessor):
|
| 569 |
-
"""Process uploaded or recorded video"""
|
| 570 |
|
| 571 |
if video is None:
|
| 572 |
return "Please upload or record a video first.", None, None, None
|
|
@@ -575,11 +585,11 @@ def process_video_core(video, resize_option, assessor):
|
|
| 575 |
# Get original video info
|
| 576 |
orig_width, orig_height, fps, frame_count = get_video_info(video)
|
| 577 |
if orig_width and orig_height:
|
| 578 |
-
print(f"
|
| 579 |
|
| 580 |
# Get file size
|
| 581 |
file_size_mb = os.path.getsize(video) / (1024 * 1024)
|
| 582 |
-
print(f"
|
| 583 |
|
| 584 |
# Apply resizing based on user selection
|
| 585 |
video_to_process = video
|
|
@@ -603,12 +613,13 @@ def process_video_core(video, resize_option, assessor):
|
|
| 603 |
|
| 604 |
# Check new file size
|
| 605 |
new_file_size_mb = os.path.getsize(video_to_process) / (1024 * 1024)
|
| 606 |
-
print(f"
|
| 607 |
|
| 608 |
-
# Start assessment
|
| 609 |
-
print("
|
|
|
|
| 610 |
|
| 611 |
-
assessment_result = assessor.analyze_video(video_to_process)
|
| 612 |
|
| 613 |
# Clean up temporary resized file if created
|
| 614 |
if temp_resized_file and temp_resized_file != video:
|
|
@@ -621,29 +632,29 @@ def process_video_core(video, resize_option, assessor):
|
|
| 621 |
return assessment_result, None, None, None
|
| 622 |
|
| 623 |
# Generate 1-minute audio feedback
|
| 624 |
-
print("
|
| 625 |
audio_path = assessor.generate_audio_feedback(assessment_result)
|
| 626 |
|
| 627 |
-
# Generate PDF report
|
| 628 |
-
print("
|
| 629 |
-
pdf_path = assessor.generate_pdf_report(assessment_result)
|
| 630 |
|
| 631 |
# Parse scores for visual summary
|
| 632 |
-
|
| 633 |
|
| 634 |
-
# Create enhanced visual summary HTML with
|
| 635 |
summary_html = f"""
|
| 636 |
<div style="max-width:800px; margin:20px auto; padding:30px; border-radius:15px; box-shadow:0 2px 10px rgba(0,0,0,0.08); background:white;">
|
| 637 |
-
<h2 style="text-align:center; color:#111827; margin-bottom:30px; font-weight:600;">
|
| 638 |
|
| 639 |
<div style="display:flex; justify-content:space-around; margin:30px 0;">
|
| 640 |
<div style="text-align:center;">
|
| 641 |
-
<div style="font-size:48px; font-weight:bold; color:{color};">{
|
| 642 |
-
<div style="color:#4B5563; margin-top:10px; font-weight:500;">
|
| 643 |
</div>
|
| 644 |
<div style="text-align:center;">
|
| 645 |
<div style="font-size:48px; font-weight:bold; color:{color};">{percentage:.0f}%</div>
|
| 646 |
-
<div style="color:#4B5563; margin-top:10px; font-weight:500;">Overall
|
| 647 |
</div>
|
| 648 |
</div>
|
| 649 |
|
|
@@ -652,43 +663,66 @@ def process_video_core(video, resize_option, assessor):
|
|
| 652 |
</div>
|
| 653 |
|
| 654 |
<div style="margin-top:30px;">
|
| 655 |
-
<h3 style="color:#111827; margin-bottom:20px; font-weight:600;">
|
| 656 |
-
|
| 657 |
-
<div style="background:#F8FAFC; padding:
|
| 658 |
-
<
|
| 659 |
-
|
| 660 |
-
<
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
<
|
| 664 |
-
</
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
<
|
| 670 |
-
|
| 671 |
-
<
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 675 |
</div>
|
|
|
|
| 676 |
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
</
|
| 685 |
</div>
|
| 686 |
</div>
|
| 687 |
|
| 688 |
<div style="margin-top:30px; padding:20px; background:#FFF7ED; border-radius:10px; border-left:4px solid #EA580C;">
|
| 689 |
<p style="text-align:center; color:#431407; margin:0; font-weight:600;">
|
| 690 |
-
|
| 691 |
-
|
| 692 |
</p>
|
| 693 |
</div>
|
| 694 |
</div>
|
|
@@ -697,37 +731,34 @@ def process_video_core(video, resize_option, assessor):
|
|
| 697 |
return assessment_result, summary_html, audio_path, pdf_path
|
| 698 |
|
| 699 |
except Exception as e:
|
| 700 |
-
error_msg = f"
|
| 701 |
print(error_msg)
|
| 702 |
return error_msg, None, None, None
|
| 703 |
|
| 704 |
# Wrapper function that calls the GPU-accelerated version
|
| 705 |
-
def process_video(video, resize_option):
|
| 706 |
-
"""Wrapper function to call GPU-accelerated processing"""
|
| 707 |
-
return process_video_with_gpu(video, resize_option)
|
| 708 |
|
| 709 |
-
# Create and launch the Gradio interface
|
| 710 |
-
print("
|
| 711 |
|
| 712 |
-
with gr.Blocks(title="
|
| 713 |
|
| 714 |
gr.Markdown("""
|
| 715 |
-
#
|
| 716 |
-
|
| 717 |
-
**
|
| 718 |
|
| 719 |
-
This tool
|
| 720 |
-
|
| 721 |
-
- Roles/responsibilities
|
| 722 |
-
- Interprofessional communication
|
| 723 |
-
- Teams and teamwork
|
| 724 |
|
| 725 |
---
|
| 726 |
""")
|
| 727 |
|
| 728 |
with gr.Row():
|
| 729 |
with gr.Column(scale=1):
|
| 730 |
-
gr.Markdown("###
|
| 731 |
|
| 732 |
# Video resolution dropdown
|
| 733 |
resize_dropdown = gr.Dropdown(
|
|
@@ -749,8 +780,8 @@ with gr.Blocks(title="CICE 2.0 Healthcare Assessment Tool", theme=gr.themes.Soft
|
|
| 749 |
include_audio=True,
|
| 750 |
interactive=True,
|
| 751 |
webcam_constraints={"video": {"width": 800, "height": 600}},
|
| 752 |
-
autoplay=False,
|
| 753 |
-
show_download_button=True
|
| 754 |
)
|
| 755 |
|
| 756 |
# Status bar for immediate feedback
|
|
@@ -762,41 +793,74 @@ with gr.Blocks(title="CICE 2.0 Healthcare Assessment Tool", theme=gr.themes.Soft
|
|
| 762 |
|
| 763 |
# Add download component for recorded videos
|
| 764 |
recorded_video_download = gr.File(
|
| 765 |
-
label="
|
| 766 |
interactive=False,
|
| 767 |
visible=False
|
| 768 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 769 |
|
| 770 |
gr.Markdown("""
|
| 771 |
-
###
|
| 772 |
-
1. **
|
| 773 |
-
2. **
|
| 774 |
-
3.
|
| 775 |
-
4. Click **Analyze Video**
|
| 776 |
-
5.
|
| 777 |
-
6. Listen to the **1-minute audio summary**
|
| 778 |
-
7. Download the **PDF report** for documentation
|
| 779 |
-
|
| 780 |
-
**Video Resolution Guide:**
|
| 781 |
-
- **640x480**: Fastest processing, uses least quota
|
| 782 |
-
- **800x600**: Recommended balance (default)
|
| 783 |
-
- **1280x720**: Best quality, takes longer
|
| 784 |
-
- **Original**: No resizing (slowest)
|
| 785 |
-
|
| 786 |
-
**Key Behaviors Assessed:**
|
| 787 |
-
- Allergy/medical history identification
|
| 788 |
-
- CPR/AED prioritization
|
| 789 |
-
- Clear role assignments
|
| 790 |
-
- Plain language communication
|
| 791 |
-
- Active listening behaviors
|
| 792 |
-
- Team respect and conflict resolution
|
| 793 |
""")
|
| 794 |
|
| 795 |
with gr.Column(scale=2):
|
| 796 |
-
gr.Markdown("###
|
| 797 |
|
| 798 |
-
# Move analyze button here
|
| 799 |
-
analyze_btn = gr.Button("
|
| 800 |
|
| 801 |
# Visual summary
|
| 802 |
summary_output = gr.HTML(
|
|
@@ -806,21 +870,21 @@ with gr.Blocks(title="CICE 2.0 Healthcare Assessment Tool", theme=gr.themes.Soft
|
|
| 806 |
|
| 807 |
# Audio feedback - downloadable
|
| 808 |
audio_output = gr.Audio(
|
| 809 |
-
label="
|
| 810 |
type="filepath",
|
| 811 |
interactive=False
|
| 812 |
)
|
| 813 |
|
| 814 |
# PDF report - downloadable
|
| 815 |
pdf_output = gr.File(
|
| 816 |
-
label="
|
| 817 |
interactive=False,
|
| 818 |
file_types=[".pdf", ".txt"]
|
| 819 |
)
|
| 820 |
|
| 821 |
# Detailed assessment text
|
| 822 |
assessment_output = gr.Textbox(
|
| 823 |
-
label="Detailed
|
| 824 |
lines=20,
|
| 825 |
max_lines=30,
|
| 826 |
interactive=False,
|
|
@@ -830,22 +894,22 @@ with gr.Blocks(title="CICE 2.0 Healthcare Assessment Tool", theme=gr.themes.Soft
|
|
| 830 |
# Footer
|
| 831 |
gr.Markdown("""
|
| 832 |
---
|
| 833 |
-
### About
|
| 834 |
-
This tool uses Google's Gemini AI to
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
-
|
| 838 |
-
-
|
| 839 |
-
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
|
| 845 |
**Powered by Google Gemini 2.0 Flash | ZeroGPU on HuggingFace Spaces**
|
| 846 |
""")
|
| 847 |
|
| 848 |
-
# Auto-save video when recording stops
|
| 849 |
video_input.stop_recording(
|
| 850 |
fn=show_saving_status,
|
| 851 |
inputs=[video_input],
|
|
@@ -870,10 +934,18 @@ with gr.Blocks(title="CICE 2.0 Healthcare Assessment Tool", theme=gr.themes.Soft
|
|
| 870 |
outputs=[status_bar]
|
| 871 |
)
|
| 872 |
|
| 873 |
-
# Connect the analyze button
|
| 874 |
analyze_btn.click(
|
| 875 |
fn=process_video,
|
| 876 |
-
inputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 877 |
outputs=[assessment_output, summary_output, audio_output, pdf_output],
|
| 878 |
api_name="analyze"
|
| 879 |
)
|
|
|
|
| 2 |
|
| 3 |
# Configure ZeroGPU
|
| 4 |
@spaces.GPU
|
| 5 |
+
def process_video_with_gpu(video, resize_option, param1, param2, param3, param4, param5):
|
| 6 |
+
"""ZeroGPU-accelerated video processing with custom parameters"""
|
| 7 |
# Create assessor inside the GPU function to avoid pickling issues
|
| 8 |
from google import genai
|
| 9 |
client = genai.Client(api_key=GOOGLE_API_KEY)
|
| 10 |
assessor = CICE_Assessment(client)
|
| 11 |
+
return process_video_core(video, resize_option, assessor, param1, param2, param3, param4, param5)
|
| 12 |
|
| 13 |
import gradio as gr
|
| 14 |
from google import genai
|
|
|
|
| 32 |
import shutil
|
| 33 |
|
| 34 |
# Configure Google API Key from environment variable or Hugging Face secrets
|
| 35 |
+
print("Setting up Google API Key...")
|
| 36 |
GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')
|
| 37 |
|
| 38 |
if not GOOGLE_API_KEY:
|
| 39 |
raise ValueError("GOOGLE_API_KEY environment variable is not set. Please set it in Hugging Face Spaces secrets.")
|
| 40 |
|
| 41 |
client = genai.Client(api_key=GOOGLE_API_KEY)
|
| 42 |
+
print("Google Generative AI configured successfully!")
|
| 43 |
|
| 44 |
+
# Define the CICE Assessment Class with parameters
|
| 45 |
class CICE_Assessment:
|
| 46 |
def __init__(self, client):
|
| 47 |
self.client = client
|
| 48 |
self.model_name = "gemini-2.0-flash-exp"
|
| 49 |
|
| 50 |
+
def analyze_video(self, video_path, param1, param2, param3, param4, param5):
|
| 51 |
+
"""Analyze video using customizable assessment parameters"""
|
| 52 |
|
| 53 |
try:
|
| 54 |
# Determine mime type based on file extension
|
|
|
|
| 59 |
mime_type = 'video/mp4'
|
| 60 |
|
| 61 |
# Upload video to Gemini
|
| 62 |
+
print(f"Uploading video to Gemini AI (type: {mime_type})...")
|
| 63 |
with open(video_path, 'rb') as f:
|
| 64 |
video_file = self.client.files.upload(file=f, config={'mime_type': mime_type})
|
| 65 |
|
| 66 |
# Wait for processing
|
| 67 |
+
print("Processing video (this may take 30-60 seconds)...")
|
| 68 |
max_wait = 300
|
| 69 |
wait_time = 0
|
| 70 |
while video_file.state == "PROCESSING" and wait_time < max_wait:
|
|
|
|
| 75 |
if video_file.state == "FAILED":
|
| 76 |
raise Exception("Video processing failed")
|
| 77 |
|
| 78 |
+
print("Analyzing team interactions with custom parameters...")
|
| 79 |
|
| 80 |
+
# Build dynamic assessment prompt based on parameters
|
| 81 |
+
prompt = self.build_assessment_prompt(param1, param2, param3, param4, param5)
|
| 82 |
|
| 83 |
+
response = self.client.models.generate_content(
|
| 84 |
+
model=self.model_name,
|
| 85 |
+
contents=[
|
| 86 |
+
types.Part.from_uri(file_uri=video_file.uri, mime_type=video_file.mime_type),
|
| 87 |
+
prompt
|
| 88 |
+
]
|
| 89 |
+
)
|
| 90 |
+
print("Analysis complete!")
|
| 91 |
+
return response.text, param1, param2, param3, param4, param5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
+
except Exception as e:
|
| 94 |
+
return f"Error during analysis: {str(e)}", param1, param2, param3, param4, param5
|
| 95 |
|
| 96 |
+
def build_assessment_prompt(self, communication_weight, teamwork_weight, safety_weight, emergency_weight, leadership_weight):
|
| 97 |
+
"""Build a dynamic prompt based on user-selected parameters"""
|
| 98 |
+
|
| 99 |
+
# Normalize weights
|
| 100 |
+
total_weight = communication_weight + teamwork_weight + safety_weight + emergency_weight + leadership_weight
|
| 101 |
+
if total_weight == 0:
|
| 102 |
+
total_weight = 1 # Avoid division by zero
|
| 103 |
+
|
| 104 |
+
comm_pct = (communication_weight / total_weight) * 100
|
| 105 |
+
team_pct = (teamwork_weight / total_weight) * 100
|
| 106 |
+
safety_pct = (safety_weight / total_weight) * 100
|
| 107 |
+
emerg_pct = (emergency_weight / total_weight) * 100
|
| 108 |
+
lead_pct = (leadership_weight / total_weight) * 100
|
| 109 |
+
|
| 110 |
+
prompt = f"""Analyze this healthcare team interaction video with the following CUSTOMIZED EVALUATION PARAMETERS:
|
| 111 |
|
| 112 |
+
EVALUATION WEIGHTS (Total 100%):
|
| 113 |
+
1. COMMUNICATION EFFECTIVENESS: {comm_pct:.1f}% weight
|
| 114 |
+
2. TEAMWORK & COLLABORATION: {team_pct:.1f}% weight
|
| 115 |
+
3. SAFETY PROTOCOLS: {safety_pct:.1f}% weight
|
| 116 |
+
4. EMERGENCY RESPONSE: {emerg_pct:.1f}% weight
|
| 117 |
+
5. LEADERSHIP & ROLES: {lead_pct:.1f}% weight
|
| 118 |
|
| 119 |
+
Please evaluate the video based on these weighted priorities:
|
|
|
|
| 120 |
|
| 121 |
+
"""
|
|
|
|
| 122 |
|
| 123 |
+
# Add detailed criteria based on weights
|
| 124 |
+
criteria_sections = []
|
| 125 |
+
|
| 126 |
+
if communication_weight > 0:
|
| 127 |
+
criteria_sections.append(f"""
|
| 128 |
+
## COMMUNICATION EFFECTIVENESS (Weight: {communication_weight}/10)
|
| 129 |
+
Evaluate:
|
| 130 |
+
- Clear verbal communication
|
| 131 |
+
- Active listening behaviors
|
| 132 |
+
- Closed-loop communication
|
| 133 |
+
- Use of plain language vs. jargon
|
| 134 |
+
- Information sharing quality
|
| 135 |
+
""")
|
| 136 |
+
|
| 137 |
+
if teamwork_weight > 0:
|
| 138 |
+
criteria_sections.append(f"""
|
| 139 |
+
## TEAMWORK & COLLABORATION (Weight: {teamwork_weight}/10)
|
| 140 |
+
Evaluate:
|
| 141 |
+
- Team coordination
|
| 142 |
+
- Mutual support behaviors
|
| 143 |
+
- Collaborative problem-solving
|
| 144 |
+
- Respect for team members
|
| 145 |
+
- Conflict resolution
|
| 146 |
+
""")
|
| 147 |
+
|
| 148 |
+
if safety_weight > 0:
|
| 149 |
+
criteria_sections.append(f"""
|
| 150 |
+
## SAFETY PROTOCOLS (Weight: {safety_weight}/10)
|
| 151 |
+
Evaluate:
|
| 152 |
+
- Patient safety measures
|
| 153 |
+
- Infection control practices
|
| 154 |
+
- Equipment safety checks
|
| 155 |
+
- Environmental awareness
|
| 156 |
+
- Risk identification
|
| 157 |
+
""")
|
| 158 |
+
|
| 159 |
+
if emergency_weight > 0:
|
| 160 |
+
criteria_sections.append(f"""
|
| 161 |
+
## EMERGENCY RESPONSE (Weight: {emergency_weight}/10)
|
| 162 |
+
Evaluate:
|
| 163 |
+
- Response time and urgency
|
| 164 |
+
- Priority setting (ABC - Airway, Breathing, Circulation)
|
| 165 |
+
- Emergency protocol adherence
|
| 166 |
+
- Critical intervention timing
|
| 167 |
+
- Resource utilization
|
| 168 |
+
""")
|
| 169 |
+
|
| 170 |
+
if leadership_weight > 0:
|
| 171 |
+
criteria_sections.append(f"""
|
| 172 |
+
## LEADERSHIP & ROLES (Weight: {lead_weight}/10)
|
| 173 |
+
Evaluate:
|
| 174 |
+
- Clear role assignments
|
| 175 |
+
- Leadership presence
|
| 176 |
+
- Decision-making clarity
|
| 177 |
+
- Delegation effectiveness
|
| 178 |
+
- Team guidance
|
| 179 |
+
""")
|
| 180 |
+
|
| 181 |
+
prompt += "".join(criteria_sections)
|
| 182 |
+
|
| 183 |
+
prompt += f"""
|
| 184 |
|
| 185 |
STRUCTURE YOUR RESPONSE AS FOLLOWS:
|
| 186 |
|
| 187 |
+
## OVERALL WEIGHTED ASSESSMENT
|
| 188 |
+
Provide an overall score based on the weighted parameters above.
|
| 189 |
|
| 190 |
+
## DETAILED EVALUATION BY PARAMETER
|
| 191 |
+
For each parameter with weight > 0, provide:
|
| 192 |
+
- Parameter Name: [Name]
|
| 193 |
+
- Weight: [X/10]
|
| 194 |
+
- Score: [X/10]
|
| 195 |
+
- Observations: [Specific behaviors observed]
|
| 196 |
+
- Recommendations: [Specific improvements]
|
| 197 |
|
| 198 |
+
## KEY STRENGTHS
|
| 199 |
+
Top 3-5 strengths observed (prioritize based on weighted parameters)
|
| 200 |
|
| 201 |
+
## CRITICAL IMPROVEMENTS NEEDED
|
| 202 |
+
Top 3-5 areas needing improvement (prioritize based on weighted parameters)
|
| 203 |
|
| 204 |
+
## WEIGHTED FINAL SCORE
|
| 205 |
+
Calculate the weighted average score:
|
| 206 |
+
- Communication: {communication_weight}/10 weight × [score]/10
|
| 207 |
+
- Teamwork: {teamwork_weight}/10 weight × [score]/10
|
| 208 |
+
- Safety: {safety_weight}/10 weight × [score]/10
|
| 209 |
+
- Emergency Response: {emergency_weight}/10 weight × [score]/10
|
| 210 |
+
- Leadership: {leadership_weight}/10 weight × [score]/10
|
| 211 |
|
| 212 |
+
TOTAL WEIGHTED SCORE: [X]/10
|
| 213 |
+
Performance Level: [Exemplary (8.5-10)/Proficient (7-8.4)/Developing (5-6.9)/Needs Improvement (0-4.9)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
+
## AUDIO SUMMARY
|
| 216 |
+
[Create a 60-second spoken summary focusing on the overall weighted score, top strengths in high-weight areas, critical improvements in high-weight areas, and 2-3 actionable recommendations. Write in natural, conversational tone for text-to-speech.]
|
| 217 |
+
"""
|
| 218 |
+
|
| 219 |
+
return prompt
|
| 220 |
|
| 221 |
def generate_audio_feedback(self, text):
|
| 222 |
"""Generate a concise 1-minute audio feedback summary"""
|
|
|
|
| 236 |
clean_text = re.sub(r'[-•·]\s+', '', clean_text)
|
| 237 |
|
| 238 |
# Add introduction and conclusion for better audio experience
|
| 239 |
+
audio_script = f"""Healthcare Team Assessment Summary.
|
| 240 |
|
| 241 |
{clean_text}
|
| 242 |
|
| 243 |
+
Please refer to the detailed written report for complete evaluation and specific recommendations.
|
| 244 |
End of audio summary."""
|
| 245 |
|
| 246 |
# Generate audio with gTTS
|
|
|
|
| 254 |
|
| 255 |
return temp_audio.name
|
| 256 |
except Exception as e:
|
| 257 |
+
print(f"Audio generation failed: {str(e)}")
|
| 258 |
return None
|
| 259 |
|
| 260 |
def create_brief_summary(self, text):
|
| 261 |
"""Create a brief summary if AUDIO SUMMARY section is not found"""
|
| 262 |
+
|
| 263 |
+
summary = f"""The team assessment has been completed based on your customized evaluation parameters.
|
| 264 |
+
|
| 265 |
+
The analysis focused on the specific areas you prioritized, with weighted scores reflecting
|
| 266 |
+
the importance you assigned to each parameter.
|
| 267 |
+
|
| 268 |
+
Key strengths were identified in the high-priority areas, and recommendations have been
|
| 269 |
+
provided for critical improvements.
|
| 270 |
+
|
| 271 |
+
Please review the detailed report for specific behavioral observations and actionable feedback
|
| 272 |
+
tailored to your evaluation criteria."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
return summary
|
| 275 |
|
| 276 |
+
def parse_assessment_scores(self, assessment_text, param1, param2, param3, param4, param5):
|
| 277 |
+
"""Parse assessment text to extract weighted scores"""
|
| 278 |
|
| 279 |
+
# Try to extract the weighted score from the text
|
| 280 |
import re
|
| 281 |
|
| 282 |
+
# Look for "TOTAL WEIGHTED SCORE: X/10" pattern
|
| 283 |
+
score_pattern = r'TOTAL WEIGHTED SCORE:\s*([0-9.]+)/10'
|
| 284 |
+
match = re.search(score_pattern, assessment_text, re.IGNORECASE)
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
+
if match:
|
| 287 |
+
weighted_score = float(match.group(1))
|
| 288 |
+
else:
|
| 289 |
+
# Fallback calculation
|
| 290 |
+
weighted_score = 7.5 # Default middle score
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
|
| 292 |
+
percentage = (weighted_score / 10) * 100
|
|
|
|
| 293 |
|
| 294 |
+
# Determine performance level and color based on score
|
| 295 |
+
if weighted_score >= 8.5:
|
| 296 |
level = "Exemplary"
|
| 297 |
color = "#0F766E" # Deep teal
|
| 298 |
+
elif weighted_score >= 7:
|
| 299 |
level = "Proficient"
|
| 300 |
color = "#1E40AF" # Professional blue
|
| 301 |
+
elif weighted_score >= 5:
|
| 302 |
level = "Developing"
|
| 303 |
color = "#EA580C" # Professional orange
|
| 304 |
else:
|
| 305 |
level = "Needs Improvement"
|
| 306 |
color = "#B91C1C" # Deep red
|
| 307 |
|
| 308 |
+
return weighted_score, percentage, level, color
|
| 309 |
|
| 310 |
+
def generate_pdf_report(self, assessment_text, param1, param2, param3, param4, param5):
|
| 311 |
+
"""Generate a PDF report from the assessment text with parameter information"""
|
| 312 |
|
| 313 |
try:
|
| 314 |
# Create a temporary file for the PDF
|
|
|
|
| 333 |
'CustomTitle',
|
| 334 |
parent=styles['Heading1'],
|
| 335 |
fontSize=24,
|
| 336 |
+
textColor=HexColor('#111827'),
|
| 337 |
spaceAfter=30,
|
| 338 |
alignment=TA_CENTER
|
| 339 |
)
|
|
|
|
| 342 |
'CustomHeading',
|
| 343 |
parent=styles['Heading2'],
|
| 344 |
fontSize=14,
|
| 345 |
+
textColor=HexColor('#1E40AF'),
|
| 346 |
spaceAfter=12,
|
| 347 |
spaceBefore=12,
|
| 348 |
bold=True
|
|
|
|
| 357 |
)
|
| 358 |
|
| 359 |
# Add title
|
| 360 |
+
elements.append(Paragraph("Healthcare Team Assessment Report", title_style))
|
| 361 |
+
elements.append(Paragraph("(Customized Evaluation Parameters)", body_style))
|
| 362 |
elements.append(Spacer(1, 12))
|
| 363 |
|
| 364 |
# Add timestamp
|
| 365 |
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 366 |
elements.append(Paragraph(f"<b>Assessment Date:</b> {timestamp}", body_style))
|
| 367 |
elements.append(Spacer(1, 20))
|
| 368 |
+
|
| 369 |
+
# Add parameter settings
|
| 370 |
+
elements.append(Paragraph("<b>Evaluation Parameters Used:</b>", heading_style))
|
| 371 |
+
elements.append(Paragraph(f"• Communication Effectiveness: {param1}/10", body_style))
|
| 372 |
+
elements.append(Paragraph(f"• Teamwork & Collaboration: {param2}/10", body_style))
|
| 373 |
+
elements.append(Paragraph(f"• Safety Protocols: {param3}/10", body_style))
|
| 374 |
+
elements.append(Paragraph(f"• Emergency Response: {param4}/10", body_style))
|
| 375 |
+
elements.append(Paragraph(f"• Leadership & Roles: {param5}/10", body_style))
|
| 376 |
+
elements.append(Spacer(1, 20))
|
| 377 |
|
| 378 |
# Process the assessment text into PDF-friendly format
|
| 379 |
lines = assessment_text.split('\n')
|
|
|
|
| 387 |
# Major heading
|
| 388 |
heading_text = line.replace('##', '').strip()
|
| 389 |
elements.append(Paragraph(heading_text, heading_style))
|
| 390 |
+
elif line.startswith('#'):
|
| 391 |
+
# Sub-heading
|
| 392 |
+
heading_text = line.replace('#', '').strip()
|
| 393 |
+
elements.append(Paragraph(heading_text, body_style))
|
|
|
|
|
|
|
| 394 |
else:
|
| 395 |
+
# Regular text - escape special characters for PDF
|
|
|
|
| 396 |
line = line.replace('&', '&').replace('<', '<').replace('>', '>')
|
| 397 |
elements.append(Paragraph(line, body_style))
|
| 398 |
|
|
|
|
| 403 |
return temp_pdf.name
|
| 404 |
|
| 405 |
except Exception as e:
|
| 406 |
+
print(f"PDF generation failed: {str(e)}")
|
| 407 |
# Fallback to text file
|
| 408 |
temp_txt = tempfile.NamedTemporaryFile(delete=False, suffix='.txt', mode='w')
|
| 409 |
+
temp_txt.write("Healthcare Team Assessment Report\n")
|
| 410 |
+
temp_txt.write("(Customized Evaluation Parameters)\n")
|
| 411 |
temp_txt.write("="*60 + "\n")
|
| 412 |
temp_txt.write(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
| 413 |
temp_txt.write("="*60 + "\n\n")
|
| 414 |
+
temp_txt.write(f"Parameters: Communication={param1}, Teamwork={param2}, Safety={param3}, Emergency={param4}, Leadership={param5}\n\n")
|
| 415 |
temp_txt.write(assessment_text)
|
| 416 |
temp_txt.close()
|
| 417 |
return temp_txt.name
|
|
|
|
| 437 |
# Create video writer with new dimensions
|
| 438 |
out = cv2.VideoWriter(temp_output.name, fourcc, fps, (target_width, target_height))
|
| 439 |
|
| 440 |
+
print(f"Resizing video to {target_width}x{target_height}...")
|
| 441 |
frame_count = 0
|
| 442 |
|
| 443 |
while True:
|
|
|
|
| 453 |
cap.release()
|
| 454 |
out.release()
|
| 455 |
|
| 456 |
+
print(f"Video resized successfully ({frame_count} frames)")
|
| 457 |
return temp_output.name
|
| 458 |
|
| 459 |
except Exception as e:
|
| 460 |
+
print(f"Video resize failed: {str(e)}")
|
| 461 |
return input_path # Return original if resize fails
|
| 462 |
|
| 463 |
def get_video_info(video_path):
|
|
|
|
| 512 |
</style>
|
| 513 |
<div style="text-align: center; color: white;">
|
| 514 |
<div style="font-size: 24px; font-weight: bold; margin-bottom: 10px;">
|
| 515 |
+
Processing Your Recording...
|
| 516 |
</div>
|
| 517 |
<div style="font-size: 16px; opacity: 0.95;">
|
| 518 |
Saving video file • Preparing for download
|
|
|
|
| 546 |
<div style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 15px; border-radius: 10px; margin: 20px 0;">
|
| 547 |
<div style="text-align: center; color: white;">
|
| 548 |
<div style="font-size: 20px; font-weight: bold;">
|
| 549 |
+
Video Saved Successfully!
|
| 550 |
</div>
|
| 551 |
<div style="font-size: 14px; margin-top: 5px; opacity: 0.95;">
|
| 552 |
Ready for download • Click "Analyze Video" to assess
|
|
|
|
| 555 |
</div>
|
| 556 |
"""
|
| 557 |
|
| 558 |
+
print(f"Video saved: {output_filename}")
|
| 559 |
return temp_output.name, gr.update(value=success_html, visible=True)
|
| 560 |
|
| 561 |
except Exception as e:
|
| 562 |
+
print(f"Failed to save video: {str(e)}")
|
| 563 |
error_html = """
|
| 564 |
<div style="background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); padding: 15px; border-radius: 10px; margin: 20px 0;">
|
| 565 |
<div style="text-align: center; color: white;">
|
| 566 |
<div style="font-size: 20px; font-weight: bold;">
|
| 567 |
+
Error Saving Video
|
| 568 |
</div>
|
| 569 |
<div style="font-size: 14px; margin-top: 5px;">
|
| 570 |
Please try recording again
|
|
|
|
| 574 |
"""
|
| 575 |
return None, gr.update(value=error_html, visible=True)
|
| 576 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
# Define the core processing function (separate from GPU wrapper)
|
| 578 |
+
def process_video_core(video, resize_option, assessor, param1, param2, param3, param4, param5):
|
| 579 |
+
"""Process uploaded or recorded video with custom parameters"""
|
| 580 |
|
| 581 |
if video is None:
|
| 582 |
return "Please upload or record a video first.", None, None, None
|
|
|
|
| 585 |
# Get original video info
|
| 586 |
orig_width, orig_height, fps, frame_count = get_video_info(video)
|
| 587 |
if orig_width and orig_height:
|
| 588 |
+
print(f"Original video: {orig_width}x{orig_height} @ {fps}fps ({frame_count} frames)")
|
| 589 |
|
| 590 |
# Get file size
|
| 591 |
file_size_mb = os.path.getsize(video) / (1024 * 1024)
|
| 592 |
+
print(f"Processing video ({file_size_mb:.1f}MB)...")
|
| 593 |
|
| 594 |
# Apply resizing based on user selection
|
| 595 |
video_to_process = video
|
|
|
|
| 613 |
|
| 614 |
# Check new file size
|
| 615 |
new_file_size_mb = os.path.getsize(video_to_process) / (1024 * 1024)
|
| 616 |
+
print(f"Resized video: {new_file_size_mb:.1f}MB (saved {file_size_mb - new_file_size_mb:.1f}MB)")
|
| 617 |
|
| 618 |
+
# Start assessment with parameters
|
| 619 |
+
print(f"Starting Healthcare Team Assessment...")
|
| 620 |
+
print(f"Parameters: Communication={param1}, Teamwork={param2}, Safety={param3}, Emergency={param4}, Leadership={param5}")
|
| 621 |
|
| 622 |
+
assessment_result, p1, p2, p3, p4, p5 = assessor.analyze_video(video_to_process, param1, param2, param3, param4, param5)
|
| 623 |
|
| 624 |
# Clean up temporary resized file if created
|
| 625 |
if temp_resized_file and temp_resized_file != video:
|
|
|
|
| 632 |
return assessment_result, None, None, None
|
| 633 |
|
| 634 |
# Generate 1-minute audio feedback
|
| 635 |
+
print("Generating 1-minute audio summary...")
|
| 636 |
audio_path = assessor.generate_audio_feedback(assessment_result)
|
| 637 |
|
| 638 |
+
# Generate PDF report with parameters
|
| 639 |
+
print("Generating PDF report...")
|
| 640 |
+
pdf_path = assessor.generate_pdf_report(assessment_result, param1, param2, param3, param4, param5)
|
| 641 |
|
| 642 |
# Parse scores for visual summary
|
| 643 |
+
weighted_score, percentage, level, color = assessor.parse_assessment_scores(assessment_result, param1, param2, param3, param4, param5)
|
| 644 |
|
| 645 |
+
# Create enhanced visual summary HTML with parameter information
|
| 646 |
summary_html = f"""
|
| 647 |
<div style="max-width:800px; margin:20px auto; padding:30px; border-radius:15px; box-shadow:0 2px 10px rgba(0,0,0,0.08); background:white;">
|
| 648 |
+
<h2 style="text-align:center; color:#111827; margin-bottom:30px; font-weight:600;">Customized Assessment Summary</h2>
|
| 649 |
|
| 650 |
<div style="display:flex; justify-content:space-around; margin:30px 0;">
|
| 651 |
<div style="text-align:center;">
|
| 652 |
+
<div style="font-size:48px; font-weight:bold; color:{color};">{weighted_score:.1f}/10</div>
|
| 653 |
+
<div style="color:#4B5563; margin-top:10px; font-weight:500;">Weighted Score</div>
|
| 654 |
</div>
|
| 655 |
<div style="text-align:center;">
|
| 656 |
<div style="font-size:48px; font-weight:bold; color:{color};">{percentage:.0f}%</div>
|
| 657 |
+
<div style="color:#4B5563; margin-top:10px; font-weight:500;">Overall Performance</div>
|
| 658 |
</div>
|
| 659 |
</div>
|
| 660 |
|
|
|
|
| 663 |
</div>
|
| 664 |
|
| 665 |
<div style="margin-top:30px;">
|
| 666 |
+
<h3 style="color:#111827; margin-bottom:20px; font-weight:600;">Your Evaluation Parameters:</h3>
|
| 667 |
+
|
| 668 |
+
<div style="background:#F8FAFC; padding:20px; border-radius:10px; border:1px solid #E2E8F0;">
|
| 669 |
+
<div style="display:flex; justify-content:space-between; margin:10px 0;">
|
| 670 |
+
<span style="color:#374151; font-weight:500;">Communication Effectiveness:</span>
|
| 671 |
+
<span style="color:#111827; font-weight:bold;">{param1}/10</span>
|
| 672 |
+
</div>
|
| 673 |
+
<div style="height:8px; background:#E5E7EB; border-radius:4px; margin:5px 0;">
|
| 674 |
+
<div style="height:100%; background:#3B82F6; border-radius:4px; width:{param1*10}%;"></div>
|
| 675 |
+
</div>
|
| 676 |
+
|
| 677 |
+
<div style="display:flex; justify-content:space-between; margin:10px 0; margin-top:20px;">
|
| 678 |
+
<span style="color:#374151; font-weight:500;">Teamwork & Collaboration:</span>
|
| 679 |
+
<span style="color:#111827; font-weight:bold;">{param2}/10</span>
|
| 680 |
+
</div>
|
| 681 |
+
<div style="height:8px; background:#E5E7EB; border-radius:4px; margin:5px 0;">
|
| 682 |
+
<div style="height:100%; background:#10B981; border-radius:4px; width:{param2*10}%;"></div>
|
| 683 |
+
</div>
|
| 684 |
+
|
| 685 |
+
<div style="display:flex; justify-content:space-between; margin:10px 0; margin-top:20px;">
|
| 686 |
+
<span style="color:#374151; font-weight:500;">Safety Protocols:</span>
|
| 687 |
+
<span style="color:#111827; font-weight:bold;">{param3}/10</span>
|
| 688 |
+
</div>
|
| 689 |
+
<div style="height:8px; background:#E5E7EB; border-radius:4px; margin:5px 0;">
|
| 690 |
+
<div style="height:100%; background:#F59E0B; border-radius:4px; width:{param3*10}%;"></div>
|
| 691 |
+
</div>
|
| 692 |
+
|
| 693 |
+
<div style="display:flex; justify-content:space-between; margin:10px 0; margin-top:20px;">
|
| 694 |
+
<span style="color:#374151; font-weight:500;">Emergency Response:</span>
|
| 695 |
+
<span style="color:#111827; font-weight:bold;">{param4}/10</span>
|
| 696 |
+
</div>
|
| 697 |
+
<div style="height:8px; background:#E5E7EB; border-radius:4px; margin:5px 0;">
|
| 698 |
+
<div style="height:100%; background:#EF4444; border-radius:4px; width:{param4*10}%;"></div>
|
| 699 |
+
</div>
|
| 700 |
+
|
| 701 |
+
<div style="display:flex; justify-content:space-between; margin:10px 0; margin-top:20px;">
|
| 702 |
+
<span style="color:#374151; font-weight:500;">Leadership & Roles:</span>
|
| 703 |
+
<span style="color:#111827; font-weight:bold;">{param5}/10</span>
|
| 704 |
+
</div>
|
| 705 |
+
<div style="height:8px; background:#E5E7EB; border-radius:4px; margin:5px 0;">
|
| 706 |
+
<div style="height:100%; background:#60A5FA; border-radius:4px; width:{param5*10}%;"></div>
|
| 707 |
+
</div>
|
| 708 |
</div>
|
| 709 |
+
</div>
|
| 710 |
|
| 711 |
+
<div style="margin-top:30px;">
|
| 712 |
+
<h3 style="color:#111827; margin-bottom:20px; font-weight:600;">Key Assessment Areas:</h3>
|
| 713 |
+
|
| 714 |
+
<div style="background:#F8FAFC; padding:15px; border-radius:10px; margin:10px 0; border:1px solid #E2E8F0;">
|
| 715 |
+
<p style="color:#374151; line-height:1.8;">
|
| 716 |
+
Your assessment focused on the parameters you prioritized. Areas with higher weights
|
| 717 |
+
received more detailed evaluation and have greater impact on the final score.
|
| 718 |
+
</p>
|
| 719 |
</div>
|
| 720 |
</div>
|
| 721 |
|
| 722 |
<div style="margin-top:30px; padding:20px; background:#FFF7ED; border-radius:10px; border-left:4px solid #EA580C;">
|
| 723 |
<p style="text-align:center; color:#431407; margin:0; font-weight:600;">
|
| 724 |
+
Listen to the 1-minute audio summary for key findings<br>
|
| 725 |
+
Download the PDF report for complete documentation
|
| 726 |
</p>
|
| 727 |
</div>
|
| 728 |
</div>
|
|
|
|
| 731 |
return assessment_result, summary_html, audio_path, pdf_path
|
| 732 |
|
| 733 |
except Exception as e:
|
| 734 |
+
error_msg = f"Error during processing: {str(e)}"
|
| 735 |
print(error_msg)
|
| 736 |
return error_msg, None, None, None
|
| 737 |
|
| 738 |
# Wrapper function that calls the GPU-accelerated version
|
| 739 |
+
def process_video(video, resize_option, param1, param2, param3, param4, param5):
|
| 740 |
+
"""Wrapper function to call GPU-accelerated processing with parameters"""
|
| 741 |
+
return process_video_with_gpu(video, resize_option, param1, param2, param3, param4, param5)
|
| 742 |
|
| 743 |
+
# Create and launch the Gradio interface with parameter controls
|
| 744 |
+
print("Launching Healthcare Assessment Tool with Custom Parameters...")
|
| 745 |
|
| 746 |
+
with gr.Blocks(title="Healthcare Team Assessment Tool", theme=gr.themes.Soft()) as demo:
|
| 747 |
|
| 748 |
gr.Markdown("""
|
| 749 |
+
# Healthcare Team Assessment Tool
|
| 750 |
+
|
| 751 |
+
**Customize your video evaluation with 5 key parameters to focus on what matters most to your team**
|
| 752 |
|
| 753 |
+
This tool allows you to adjust the evaluation weights for different aspects of healthcare team performance.
|
| 754 |
+
Set higher values for areas you want to prioritize in the assessment.
|
|
|
|
|
|
|
|
|
|
| 755 |
|
| 756 |
---
|
| 757 |
""")
|
| 758 |
|
| 759 |
with gr.Row():
|
| 760 |
with gr.Column(scale=1):
|
| 761 |
+
gr.Markdown("### Video Input")
|
| 762 |
|
| 763 |
# Video resolution dropdown
|
| 764 |
resize_dropdown = gr.Dropdown(
|
|
|
|
| 780 |
include_audio=True,
|
| 781 |
interactive=True,
|
| 782 |
webcam_constraints={"video": {"width": 800, "height": 600}},
|
| 783 |
+
autoplay=False,
|
| 784 |
+
show_download_button=True
|
| 785 |
)
|
| 786 |
|
| 787 |
# Status bar for immediate feedback
|
|
|
|
| 793 |
|
| 794 |
# Add download component for recorded videos
|
| 795 |
recorded_video_download = gr.File(
|
| 796 |
+
label="Download Recorded Video",
|
| 797 |
interactive=False,
|
| 798 |
visible=False
|
| 799 |
)
|
| 800 |
+
|
| 801 |
+
gr.Markdown("### Evaluation Parameters")
|
| 802 |
+
gr.Markdown("**Set the importance (0-10) for each assessment area:**")
|
| 803 |
+
|
| 804 |
+
# Add the 5 parameter sliders
|
| 805 |
+
param1_slider = gr.Slider(
|
| 806 |
+
minimum=0,
|
| 807 |
+
maximum=10,
|
| 808 |
+
value=8,
|
| 809 |
+
step=1,
|
| 810 |
+
label="Communication Effectiveness",
|
| 811 |
+
info="Clear verbal communication, active listening, information sharing"
|
| 812 |
+
)
|
| 813 |
+
|
| 814 |
+
param2_slider = gr.Slider(
|
| 815 |
+
minimum=0,
|
| 816 |
+
maximum=10,
|
| 817 |
+
value=7,
|
| 818 |
+
step=1,
|
| 819 |
+
label="Teamwork & Collaboration",
|
| 820 |
+
info="Team coordination, mutual support, collaborative problem-solving"
|
| 821 |
+
)
|
| 822 |
+
|
| 823 |
+
param3_slider = gr.Slider(
|
| 824 |
+
minimum=0,
|
| 825 |
+
maximum=10,
|
| 826 |
+
value=6,
|
| 827 |
+
step=1,
|
| 828 |
+
label="Safety Protocols",
|
| 829 |
+
info="Patient safety measures, infection control, risk identification"
|
| 830 |
+
)
|
| 831 |
+
|
| 832 |
+
param4_slider = gr.Slider(
|
| 833 |
+
minimum=0,
|
| 834 |
+
maximum=10,
|
| 835 |
+
value=9,
|
| 836 |
+
step=1,
|
| 837 |
+
label="Emergency Response",
|
| 838 |
+
info="Response time, priority setting, critical intervention timing"
|
| 839 |
+
)
|
| 840 |
+
|
| 841 |
+
param5_slider = gr.Slider(
|
| 842 |
+
minimum=0,
|
| 843 |
+
maximum=10,
|
| 844 |
+
value=5,
|
| 845 |
+
step=1,
|
| 846 |
+
label="Leadership & Roles",
|
| 847 |
+
info="Role assignments, leadership presence, delegation effectiveness"
|
| 848 |
+
)
|
| 849 |
|
| 850 |
gr.Markdown("""
|
| 851 |
+
### Instructions:
|
| 852 |
+
1. **Set your evaluation parameters** (higher = more important)
|
| 853 |
+
2. **Select video resolution** (lower = faster)
|
| 854 |
+
3. **Upload** or **Record** a video
|
| 855 |
+
4. Click **Analyze Video** to start assessment
|
| 856 |
+
5. Review results weighted by your priorities
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 857 |
""")
|
| 858 |
|
| 859 |
with gr.Column(scale=2):
|
| 860 |
+
gr.Markdown("### Assessment Results")
|
| 861 |
|
| 862 |
+
# Move analyze button here
|
| 863 |
+
analyze_btn = gr.Button("Analyze Video", variant="primary", size="lg")
|
| 864 |
|
| 865 |
# Visual summary
|
| 866 |
summary_output = gr.HTML(
|
|
|
|
| 870 |
|
| 871 |
# Audio feedback - downloadable
|
| 872 |
audio_output = gr.Audio(
|
| 873 |
+
label="1-Minute Audio Summary (Downloadable)",
|
| 874 |
type="filepath",
|
| 875 |
interactive=False
|
| 876 |
)
|
| 877 |
|
| 878 |
# PDF report - downloadable
|
| 879 |
pdf_output = gr.File(
|
| 880 |
+
label="Download Full PDF Report",
|
| 881 |
interactive=False,
|
| 882 |
file_types=[".pdf", ".txt"]
|
| 883 |
)
|
| 884 |
|
| 885 |
# Detailed assessment text
|
| 886 |
assessment_output = gr.Textbox(
|
| 887 |
+
label="Detailed Assessment (Text View)",
|
| 888 |
lines=20,
|
| 889 |
max_lines=30,
|
| 890 |
interactive=False,
|
|
|
|
| 894 |
# Footer
|
| 895 |
gr.Markdown("""
|
| 896 |
---
|
| 897 |
+
### About Customizable Assessment
|
| 898 |
+
This tool uses Google's Gemini AI to evaluate healthcare team performance based on YOUR priorities.
|
| 899 |
+
|
| 900 |
+
**How Parameters Work:**
|
| 901 |
+
- Higher values (8-10) = Critical importance in evaluation
|
| 902 |
+
- Medium values (4-7) = Moderate importance
|
| 903 |
+
- Lower values (0-3) = Less emphasis in assessment
|
| 904 |
+
- Set to 0 to exclude from evaluation
|
| 905 |
+
|
| 906 |
+
The final score is weighted based on your parameter settings, ensuring the assessment
|
| 907 |
+
focuses on what matters most to your training objectives.
|
| 908 |
|
| 909 |
**Powered by Google Gemini 2.0 Flash | ZeroGPU on HuggingFace Spaces**
|
| 910 |
""")
|
| 911 |
|
| 912 |
+
# Auto-save video when recording stops
|
| 913 |
video_input.stop_recording(
|
| 914 |
fn=show_saving_status,
|
| 915 |
inputs=[video_input],
|
|
|
|
| 934 |
outputs=[status_bar]
|
| 935 |
)
|
| 936 |
|
| 937 |
+
# Connect the analyze button with all parameters
|
| 938 |
analyze_btn.click(
|
| 939 |
fn=process_video,
|
| 940 |
+
inputs=[
|
| 941 |
+
video_input,
|
| 942 |
+
resize_dropdown,
|
| 943 |
+
param1_slider,
|
| 944 |
+
param2_slider,
|
| 945 |
+
param3_slider,
|
| 946 |
+
param4_slider,
|
| 947 |
+
param5_slider
|
| 948 |
+
],
|
| 949 |
outputs=[assessment_output, summary_output, audio_output, pdf_output],
|
| 950 |
api_name="analyze"
|
| 951 |
)
|