Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -454,15 +454,15 @@ def calculate_hrqol_scores(hrqol_responses):
|
|
454 |
return domain_scores
|
455 |
|
456 |
def get_score_color(score):
|
457 |
-
"""Return color based on score"""
|
458 |
if score >= 80:
|
459 |
-
return "#4CAF50" # Green
|
460 |
elif score >= 60:
|
461 |
-
return "#FFC107" # Yellow
|
462 |
elif score >= 40:
|
463 |
-
return "#FF9800" # Orange
|
464 |
else:
|
465 |
-
return "#F44336" # Red
|
466 |
|
467 |
def get_healthspan_grade(score):
|
468 |
if score >= 85:
|
@@ -482,15 +482,12 @@ def comprehensive_healthspan_analysis(input_type, image_input, video_input, bree
|
|
482 |
"""Combine image/video analysis with HRQOL assessment based on input type"""
|
483 |
|
484 |
# Determine which input to use based on dropdown selection
|
485 |
-
if input_type == "Image
|
486 |
selected_media = image_input
|
487 |
media_type = "image"
|
488 |
-
elif input_type == "Video
|
489 |
selected_media = video_input
|
490 |
media_type = "video"
|
491 |
-
elif input_type == "Live Recording":
|
492 |
-
selected_media = video_input # Live recording uses the same video component
|
493 |
-
media_type = "video"
|
494 |
else:
|
495 |
return "β **Error**: Please select an input type."
|
496 |
|
@@ -576,67 +573,55 @@ def comprehensive_healthspan_analysis(input_type, image_input, video_input, bree
|
|
576 |
final_healthspan_score = (video_score * video_weight) + (hrqol_composite * hrqol_weight)
|
577 |
final_healthspan_score = min(100, max(0, final_healthspan_score))
|
578 |
|
579 |
-
# Generate comprehensive report
|
580 |
input_type_icon = "πΈ" if media_type == "image" else "π₯"
|
581 |
|
582 |
report_html = f"""
|
583 |
<div style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 1000px; margin: 0 auto;">
|
584 |
-
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 15px; margin: 20px 0; text-align: center;">
|
585 |
-
<h2 style="margin: 0; font-size: 2em;">{input_type_icon} Comprehensive Healthspan Assessment</h2>
|
586 |
-
<div style="font-size: 1.1em; margin: 10px 0;">Analysis Type: {input_type}</div>
|
587 |
-
<div style="font-size: 3em; font-weight: bold; margin: 15px 0;">{final_healthspan_score:.1f}/100</div>
|
588 |
-
<div style="font-size: 1.2em;">{get_healthspan_grade(final_healthspan_score)}</div>
|
589 |
</div>
|
590 |
|
591 |
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin: 30px 0;">
|
592 |
-
|
593 |
-
|
594 |
-
|
595 |
-
|
596 |
-
|
597 |
-
|
598 |
-
|
599 |
-
|
600 |
-
|
601 |
-
|
602 |
-
|
603 |
-
|
604 |
-
</div>
|
605 |
-
<div style="font-size: 1.1em; font-weight: bold;">{hrqol_scores['comfort']:.1f}/100</div>
|
606 |
-
</div>
|
607 |
-
|
608 |
-
<div style="border: 2px solid #e0e0e0; padding: 20px; border-radius: 12px; background: #f8f9fa;">
|
609 |
-
<h4 style="margin: 0 0 15px 0; color: #667eea;">π Emotional</h4>
|
610 |
-
<div style="background: #e9ecef; height: 8px; border-radius: 4px; margin: 10px 0;">
|
611 |
-
<div style="background: {get_score_color(hrqol_scores['emotional_wellbeing'])}; height: 100%; width: {hrqol_scores['emotional_wellbeing']}%; border-radius: 4px; transition: width 0.3s ease;"></div>
|
612 |
-
</div>
|
613 |
-
<div style="font-size: 1.1em; font-weight: bold;">{hrqol_scores['emotional_wellbeing']:.1f}/100</div>
|
614 |
-
</div>
|
615 |
-
|
616 |
-
<div style="border: 2px solid #e0e0e0; padding: 20px; border-radius: 12px; background: #f8f9fa;">
|
617 |
-
<h4 style="margin: 0 0 15px 0; color: #667eea;">π§ Alertness</h4>
|
618 |
-
<div style="background: #e9ecef; height: 8px; border-radius: 4px; margin: 10px 0;">
|
619 |
-
<div style="background: {get_score_color(hrqol_scores['alertness'])}; height: 100%; width: {hrqol_scores['alertness']}%; border-radius: 4px; transition: width 0.3s ease;"></div>
|
620 |
</div>
|
621 |
-
<div style="font-size: 1.1em; font-weight: bold;">{hrqol_scores[
|
622 |
</div>
|
623 |
-
|
624 |
-
|
|
|
625 |
|
626 |
-
#
|
627 |
if breed_info:
|
628 |
pace_info = ""
|
629 |
if age and age > 0:
|
630 |
pace = breed_info["bio_age"] / age
|
631 |
pace_status = "Accelerated" if pace > 1.2 else "Normal" if pace > 0.8 else "Slow"
|
632 |
-
|
|
|
|
|
|
|
633 |
|
634 |
report_html += f"""
|
635 |
-
<div style="border: 2px solid #
|
636 |
-
<h3 style="color: #
|
637 |
-
<p><strong>Detected Breed:</strong> {breed_info['breed']} ({breed_info['confidence']:.1%} confidence)</p>
|
638 |
-
<p><strong>Estimated Biological Age:</strong> {breed_info['bio_age']} years</p>
|
639 |
-
<p><strong>Chronological Age:</strong> {age or 'Not provided'} years</p>
|
640 |
{pace_info}
|
641 |
</div>
|
642 |
"""
|
@@ -644,25 +629,39 @@ def comprehensive_healthspan_analysis(input_type, image_input, video_input, bree
|
|
644 |
# Add video-specific analysis if available
|
645 |
if video_features:
|
646 |
report_html += f"""
|
647 |
-
<div style="border: 2px solid #
|
648 |
-
<h3 style="color: #
|
649 |
-
<p><strong>Duration:</strong> {video_features['duration_sec']} seconds</p>
|
650 |
-
<p><strong>Mobility Assessment:</strong> {video_features['mobility_assessment']}</p>
|
651 |
-
<p><strong>Comfort Assessment:</strong> {video_features['comfort_assessment']}</p>
|
652 |
-
<p><strong>Vitality Assessment:</strong> {video_features['vitality_assessment']}</p>
|
653 |
-
<p><strong>Frames Analyzed:</strong> {video_features['frames_analyzed']}</p>
|
654 |
</div>
|
655 |
"""
|
656 |
|
657 |
-
#
|
658 |
if health_aspects and media_type == "image":
|
659 |
report_html += f"""
|
660 |
-
<div style="border: 2px solid #
|
661 |
-
<h3 style="color: #
|
662 |
"""
|
663 |
for aspect, data in health_aspects.items():
|
664 |
-
|
665 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
666 |
report_html += "</div>"
|
667 |
|
668 |
# Add recommendations
|
@@ -678,15 +677,20 @@ def comprehensive_healthspan_analysis(input_type, image_input, video_input, bree
|
|
678 |
|
679 |
if recommendations:
|
680 |
report_html += f"""
|
681 |
-
<div style="border: 2px solid #
|
682 |
-
<h3 style="color: #
|
683 |
-
{''.join([f'<p>{rec}</p>' for rec in recommendations])}
|
684 |
</div>
|
685 |
"""
|
686 |
|
|
|
687 |
report_html += """
|
688 |
-
<div style="background: #
|
689 |
-
<p
|
|
|
|
|
|
|
|
|
690 |
</div>
|
691 |
</div>
|
692 |
"""
|
@@ -695,9 +699,9 @@ def comprehensive_healthspan_analysis(input_type, image_input, video_input, bree
|
|
695 |
|
696 |
def update_media_input(input_type):
|
697 |
"""Update the visibility of media inputs based on dropdown selection"""
|
698 |
-
if input_type == "Image
|
699 |
return gr.update(visible=True), gr.update(visible=False)
|
700 |
-
else: # Video
|
701 |
return gr.update(visible=False), gr.update(visible=True)
|
702 |
|
703 |
# Gradio Interface
|
@@ -712,24 +716,26 @@ with gr.Blocks(title="πΆ VetMetrica HRQOL Dog Health Analyzer", theme=gr.theme
|
|
712 |
with gr.Column(scale=1):
|
713 |
gr.Markdown("### πΈ **Media Input Selection**")
|
714 |
|
715 |
-
#
|
716 |
input_type_dropdown = gr.Dropdown(
|
717 |
-
choices=["Image
|
718 |
-
label="Select
|
719 |
-
value="Image
|
720 |
interactive=True
|
721 |
)
|
722 |
|
723 |
-
# Media input components
|
724 |
image_input = gr.Image(
|
725 |
type="pil",
|
726 |
-
label="Upload Dog Photo",
|
727 |
-
visible=True
|
|
|
728 |
)
|
729 |
|
730 |
video_input = gr.Video(
|
731 |
-
label="Upload Video (10-30 seconds) or Record
|
732 |
-
visible=False
|
|
|
733 |
)
|
734 |
|
735 |
# Update visibility based on dropdown selection
|
@@ -800,7 +806,7 @@ with gr.Blocks(title="πΆ VetMetrica HRQOL Dog Health Analyzer", theme=gr.theme
|
|
800 |
- **π― Evidence-Based Assessment**: Uses clinically validated HRQOL domains (Vitality, Comfort, Emotional Wellbeing, Alertness)
|
801 |
- **π€ AI-Powered Analysis**: Combines CLIP vision models with BiomedCLIP for medical insights
|
802 |
- **π Comprehensive Scoring**: Generates composite healthspan scores normalized by breed and age
|
803 |
-
-
|
804 |
- **π Personalized Insights**: Provides domain-specific recommendations based on assessment results
|
805 |
|
806 |
**π¬ Scientific Validation**: Based on peer-reviewed research in veterinary medicine and validated against clinical outcomes.
|
|
|
454 |
return domain_scores
|
455 |
|
456 |
def get_score_color(score):
|
457 |
+
"""Return background and text color based on score for better visibility"""
|
458 |
if score >= 80:
|
459 |
+
return {"bg": "#4CAF50", "text": "#FFFFFF"} # Green background, white text
|
460 |
elif score >= 60:
|
461 |
+
return {"bg": "#FFC107", "text": "#000000"} # Yellow background, black text
|
462 |
elif score >= 40:
|
463 |
+
return {"bg": "#FF9800", "text": "#FFFFFF"} # Orange background, white text
|
464 |
else:
|
465 |
+
return {"bg": "#F44336", "text": "#FFFFFF"} # Red background, white text
|
466 |
|
467 |
def get_healthspan_grade(score):
|
468 |
if score >= 85:
|
|
|
482 |
"""Combine image/video analysis with HRQOL assessment based on input type"""
|
483 |
|
484 |
# Determine which input to use based on dropdown selection
|
485 |
+
if input_type == "Image Analysis":
|
486 |
selected_media = image_input
|
487 |
media_type = "image"
|
488 |
+
elif input_type == "Video Analysis":
|
489 |
selected_media = video_input
|
490 |
media_type = "video"
|
|
|
|
|
|
|
491 |
else:
|
492 |
return "β **Error**: Please select an input type."
|
493 |
|
|
|
573 |
final_healthspan_score = (video_score * video_weight) + (hrqol_composite * hrqol_weight)
|
574 |
final_healthspan_score = min(100, max(0, final_healthspan_score))
|
575 |
|
576 |
+
# Generate comprehensive report with improved colors
|
577 |
input_type_icon = "πΈ" if media_type == "image" else "π₯"
|
578 |
|
579 |
report_html = f"""
|
580 |
<div style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 1000px; margin: 0 auto;">
|
581 |
+
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 15px; margin: 20px 0; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
582 |
+
<h2 style="margin: 0; font-size: 2em; text-shadow: 1px 1px 2px rgba(0,0,0,0.3);">{input_type_icon} Comprehensive Healthspan Assessment</h2>
|
583 |
+
<div style="font-size: 1.1em; margin: 10px 0; opacity: 0.9;">Analysis Type: {input_type}</div>
|
584 |
+
<div style="font-size: 3em; font-weight: bold; margin: 15px 0; text-shadow: 2px 2px 4px rgba(0,0,0,0.3);">{final_healthspan_score:.1f}/100</div>
|
585 |
+
<div style="font-size: 1.2em; background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px; display: inline-block;">{get_healthspan_grade(final_healthspan_score)}</div>
|
586 |
</div>
|
587 |
|
588 |
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin: 30px 0;">
|
589 |
+
"""
|
590 |
+
|
591 |
+
# Add domain score cards with improved contrast
|
592 |
+
for domain, score in [("vitality", "π Vitality"), ("comfort", "π Comfort"), ("emotional_wellbeing", "π Emotional"), ("alertness", "π§ Alertness")]:
|
593 |
+
colors = get_score_color(hrqol_scores[domain])
|
594 |
+
report_html += f"""
|
595 |
+
<div style="border: 2px solid #e0e0e0; padding: 20px; border-radius: 12px; background: #ffffff; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
596 |
+
<h4 style="margin: 0 0 15px 0; color: #333333; font-weight: 600;">{score.split()[1]}</h4>
|
597 |
+
<div style="background: #e9ecef; height: 12px; border-radius: 6px; margin: 10px 0; border: 1px solid #dee2e6;">
|
598 |
+
<div style="background: {colors['bg']}; height: 100%; width: {hrqol_scores[domain]}%; border-radius: 6px; transition: width 0.3s ease; position: relative; display: flex; align-items: center; justify-content: center;">
|
599 |
+
<span style="color: {colors['text']}; font-size: 10px; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.3);">{hrqol_scores[domain]:.0f}</span>
|
600 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
601 |
</div>
|
602 |
+
<div style="font-size: 1.1em; font-weight: bold; color: #333333;">{hrqol_scores[domain]:.1f}/100</div>
|
603 |
</div>
|
604 |
+
"""
|
605 |
+
|
606 |
+
report_html += "</div>"
|
607 |
|
608 |
+
# Visual Analysis section with better contrast
|
609 |
if breed_info:
|
610 |
pace_info = ""
|
611 |
if age and age > 0:
|
612 |
pace = breed_info["bio_age"] / age
|
613 |
pace_status = "Accelerated" if pace > 1.2 else "Normal" if pace > 0.8 else "Slow"
|
614 |
+
pace_color = "#FF5722" if pace > 1.2 else "#4CAF50" if pace < 0.8 else "#FF9800"
|
615 |
+
pace_info = f"""<p style="margin: 8px 0;"><strong style="color: #333;">Aging Pace:</strong>
|
616 |
+
<span style="background: {pace_color}; color: white; padding: 4px 8px; border-radius: 12px; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.3);">
|
617 |
+
{pace:.2f}Γ ({pace_status})</span></p>"""
|
618 |
|
619 |
report_html += f"""
|
620 |
+
<div style="border: 2px solid #2196F3; padding: 20px; border-radius: 12px; margin: 20px 0; background: #ffffff; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
621 |
+
<h3 style="color: #1976D2; margin: 0 0 15px 0; font-weight: 600; border-bottom: 2px solid #E3F2FD; padding-bottom: 8px;">{input_type_icon} Visual Analysis</h3>
|
622 |
+
<p style="margin: 8px 0; color: #333;"><strong>Detected Breed:</strong> <span style="color: #1976D2; font-weight: 600;">{breed_info['breed']}</span> <span style="background: #E3F2FD; color: #1976D2; padding: 2px 6px; border-radius: 8px; font-size: 0.9em;">({breed_info['confidence']:.1%} confidence)</span></p>
|
623 |
+
<p style="margin: 8px 0; color: #333;"><strong>Estimated Biological Age:</strong> <span style="color: #1976D2; font-weight: 600;">{breed_info['bio_age']} years</span></p>
|
624 |
+
<p style="margin: 8px 0; color: #333;"><strong>Chronological Age:</strong> <span style="color: #1976D2; font-weight: 600;">{age or 'Not provided'} years</span></p>
|
625 |
{pace_info}
|
626 |
</div>
|
627 |
"""
|
|
|
629 |
# Add video-specific analysis if available
|
630 |
if video_features:
|
631 |
report_html += f"""
|
632 |
+
<div style="border: 2px solid #FF5722; padding: 20px; border-radius: 12px; margin: 20px 0; background: #ffffff; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
633 |
+
<h3 style="color: #D84315; margin: 0 0 15px 0; font-weight: 600; border-bottom: 2px solid #FFEBE7; padding-bottom: 8px;">π₯ Video Gait Analysis</h3>
|
634 |
+
<p style="margin: 8px 0; color: #333;"><strong>Duration:</strong> <span style="color: #D84315; font-weight: 600;">{video_features['duration_sec']} seconds</span></p>
|
635 |
+
<p style="margin: 8px 0; color: #333;"><strong>Mobility Assessment:</strong> <span style="color: #D84315; font-weight: 600;">{video_features['mobility_assessment']}</span></p>
|
636 |
+
<p style="margin: 8px 0; color: #333;"><strong>Comfort Assessment:</strong> <span style="color: #D84315; font-weight: 600;">{video_features['comfort_assessment']}</span></p>
|
637 |
+
<p style="margin: 8px 0; color: #333;"><strong>Vitality Assessment:</strong> <span style="color: #D84315; font-weight: 600;">{video_features['vitality_assessment']}</span></p>
|
638 |
+
<p style="margin: 8px 0; color: #333;"><strong>Frames Analyzed:</strong> <span style="color: #D84315; font-weight: 600;">{video_features['frames_analyzed']}</span></p>
|
639 |
</div>
|
640 |
"""
|
641 |
|
642 |
+
# Physical Health Assessment with improved visibility
|
643 |
if health_aspects and media_type == "image":
|
644 |
report_html += f"""
|
645 |
+
<div style="border: 2px solid #4CAF50; padding: 20px; border-radius: 12px; margin: 20px 0; background: #ffffff; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
646 |
+
<h3 style="color: #2E7D32; margin: 0 0 15px 0; font-weight: 600; border-bottom: 2px solid #E8F5E8; padding-bottom: 8px;">πΈ Physical Health Assessment</h3>
|
647 |
"""
|
648 |
for aspect, data in health_aspects.items():
|
649 |
+
is_healthy = any(word in data["assessment"].lower() for word in ["healthy", "bright", "clean", "ideal"])
|
650 |
+
status_icon = "β
" if is_healthy else "β οΈ"
|
651 |
+
status_color = "#2E7D32" if is_healthy else "#F57C00"
|
652 |
+
bg_color = "#E8F5E8" if is_healthy else "#FFF3E0"
|
653 |
+
|
654 |
+
report_html += f"""
|
655 |
+
<div style="margin: 10px 0; padding: 12px; background: {bg_color}; border-radius: 8px; border-left: 4px solid {status_color};">
|
656 |
+
<p style="margin: 0; color: #333;">
|
657 |
+
<span style="font-size: 1.2em;">{status_icon}</span>
|
658 |
+
<strong style="color: {status_color};">{aspect}:</strong>
|
659 |
+
<span style="color: #333; font-weight: 500;">{data['assessment']}</span>
|
660 |
+
<span style="background: #E0E0E0; color: #424242; padding: 2px 6px; border-radius: 8px; font-size: 0.85em; margin-left: 8px;">
|
661 |
+
({data['confidence']:.1%} confidence)</span>
|
662 |
+
</p>
|
663 |
+
</div>
|
664 |
+
"""
|
665 |
report_html += "</div>"
|
666 |
|
667 |
# Add recommendations
|
|
|
677 |
|
678 |
if recommendations:
|
679 |
report_html += f"""
|
680 |
+
<div style="border: 2px solid #FF9800; padding: 20px; border-radius: 12px; margin: 20px 0; background: #ffffff; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
681 |
+
<h3 style="color: #F57C00; margin: 0 0 15px 0; font-weight: 600; border-bottom: 2px solid #FFF3E0; padding-bottom: 8px;">π― Personalized Recommendations</h3>
|
682 |
+
{''.join([f'<div style="margin: 10px 0; padding: 12px; background: #FFF8E1; border-radius: 8px; border-left: 4px solid #FF9800;"><p style="margin: 0; color: #333; font-weight: 500;">{rec}</p></div>' for rec in recommendations])}
|
683 |
</div>
|
684 |
"""
|
685 |
|
686 |
+
# Disclaimer with improved visibility
|
687 |
report_html += """
|
688 |
+
<div style="background: #F5F5F5; border: 1px solid #E0E0E0; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
689 |
+
<p style="margin: 0; font-size: 0.9em; color: #424242; line-height: 1.5;">
|
690 |
+
<strong style="color: #D32F2F;">β οΈ Important Disclaimer:</strong>
|
691 |
+
This analysis uses validated HRQOL assessment tools but is for educational purposes only.
|
692 |
+
Always consult with a qualified veterinarian for professional medical advice and diagnosis.
|
693 |
+
</p>
|
694 |
</div>
|
695 |
</div>
|
696 |
"""
|
|
|
699 |
|
700 |
def update_media_input(input_type):
|
701 |
"""Update the visibility of media inputs based on dropdown selection"""
|
702 |
+
if input_type == "Image Analysis":
|
703 |
return gr.update(visible=True), gr.update(visible=False)
|
704 |
+
else: # Video Analysis
|
705 |
return gr.update(visible=False), gr.update(visible=True)
|
706 |
|
707 |
# Gradio Interface
|
|
|
716 |
with gr.Column(scale=1):
|
717 |
gr.Markdown("### πΈ **Media Input Selection**")
|
718 |
|
719 |
+
# Streamlined dropdown with only 2 options
|
720 |
input_type_dropdown = gr.Dropdown(
|
721 |
+
choices=["Image Analysis", "Video Analysis"],
|
722 |
+
label="Select Analysis Type",
|
723 |
+
value="Image Analysis",
|
724 |
interactive=True
|
725 |
)
|
726 |
|
727 |
+
# Media input components with webcam access for both
|
728 |
image_input = gr.Image(
|
729 |
type="pil",
|
730 |
+
label="Upload Dog Photo or Use Webcam",
|
731 |
+
visible=True,
|
732 |
+
sources=["upload", "webcam"] # Enable webcam for images
|
733 |
)
|
734 |
|
735 |
video_input = gr.Video(
|
736 |
+
label="Upload Video (10-30 seconds) or Record with Webcam",
|
737 |
+
visible=False,
|
738 |
+
sources=["upload", "webcam"] # Enable webcam for videos
|
739 |
)
|
740 |
|
741 |
# Update visibility based on dropdown selection
|
|
|
806 |
- **π― Evidence-Based Assessment**: Uses clinically validated HRQOL domains (Vitality, Comfort, Emotional Wellbeing, Alertness)
|
807 |
- **π€ AI-Powered Analysis**: Combines CLIP vision models with BiomedCLIP for medical insights
|
808 |
- **π Comprehensive Scoring**: Generates composite healthspan scores normalized by breed and age
|
809 |
+
- **πΈπ₯ Multi-Modal Input**: Both image and video analysis support upload and real-time webcam capture
|
810 |
- **π Personalized Insights**: Provides domain-specific recommendations based on assessment results
|
811 |
|
812 |
**π¬ Scientific Validation**: Based on peer-reviewed research in veterinary medicine and validated against clinical outcomes.
|