CelagenexResearch commited on
Commit
897b2d4
·
verified ·
1 Parent(s): 41e130d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +477 -172
app.py CHANGED
@@ -114,30 +114,165 @@ BREED_LIFESPAN = {
114
  "wire-haired fox terrier": 13.5, "yorkshire terrier": 13.3
115
  }
116
 
117
- # 4. Questionnaire
118
- QUESTIONNAIRE = [
119
- {"domain": "Mobility", "questions": [
120
- "Does your dog have difficulty rising from lying down?",
121
- "Does your dog hesitate before jumping up or climbing stairs?"
122
- ]},
123
- {"domain": "Energy", "questions": [
124
- "Does your dog tire quickly during walks or play?",
125
- "Has your dog's overall activity level decreased recently?"
126
- ]},
127
- {"domain": "Physical Health", "questions": [
128
- "Does your dog scratch or lick their skin frequently?",
129
- "Have you noticed changes in appetite or weight?"
130
- ]},
131
- {"domain": "Cognitive Function", "questions": [
132
- "Does your dog get lost in familiar rooms or areas?",
133
- "Does your dog stare blankly at walls or into space?"
134
- ]},
135
- {"domain": "Social Behavior", "questions": [
136
- "Has your dog's interest in play or interaction declined?",
137
- "Does your dog avoid interaction with family members?"
138
- ]}
139
- ]
140
- SCALE = ["0 (Never)", "1 (Rarely)", "2 (Sometimes)", "3 (Often)", "4 (Frequently)", "5 (Always)"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
  def predict_biological_age(img: Image.Image, breed: str) -> int:
143
  avg = BREED_LIFESPAN.get(breed.lower(), 12)
@@ -216,6 +351,7 @@ def analyze_video_gait(video_path):
216
  indices = np.linspace(0, total-1, min(15, total), dtype=int)
217
  health_scores = []
218
  movement_scores = []
 
219
 
220
  for i in indices:
221
  cap.set(cv2.CAP_PROP_POS_FRAMES, i)
@@ -233,7 +369,14 @@ def analyze_video_gait(video_path):
233
  inputs = clip_processor(text=movement_prompts, images=img, return_tensors="pt", padding=True).to(device)
234
  with torch.no_grad():
235
  movement_logits = clip_model(**inputs).logits_per_image.softmax(-1)[0].cpu().numpy()
236
- movement_scores.append(float(movement_logits[0])) # Normal movement score
 
 
 
 
 
 
 
237
 
238
  cap.release()
239
 
@@ -242,202 +385,364 @@ def analyze_video_gait(video_path):
242
 
243
  return {
244
  "duration_sec": round(total/fps, 1),
245
- "avg_health_confidence": float(np.mean(health_scores)),
246
- "avg_movement_score": float(np.mean(movement_scores)),
 
247
  "frames_analyzed": len(health_scores),
248
- "gait_assessment": "Normal" if np.mean(movement_scores) > 0.5 else "Concerning"
 
 
249
  }
250
  except Exception as e:
251
  return None
252
 
253
- def compute_healthspan_score(answers):
254
- if not answers or all(a is None or a == "" for a in answers):
255
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
  domain_scores = {}
258
- idx = 0
259
- total_score = 0
260
 
261
- for section in QUESTIONNAIRE:
262
- n = len(section["questions"])
263
- try:
264
- vals = []
265
- for a in answers[idx:idx+n]:
266
- if a is None or a == "":
267
- vals.append(0)
268
- else:
269
- # Extract numeric value from scale (e.g., "3 (Often)" -> 3)
270
- vals.append(int(a.split()[0]))
271
- idx += n
272
- domain_avg = sum(vals) / len(vals)
273
- domain_scores[section["domain"]] = round(domain_avg, 2)
274
- total_score += domain_avg
275
- except (ValueError, TypeError):
276
- domain_scores[section["domain"]] = 0.0
277
-
278
- # Calculate overall healthspan score (0-100, lower is better)
279
- overall_healthspan = round((total_score / len(QUESTIONNAIRE)) * 20, 1) # Convert to 0-100 scale
280
-
281
- return {
282
- "domain_scores": domain_scores,
283
- "overall_healthspan_score": overall_healthspan,
284
- "healthspan_grade": get_healthspan_grade(overall_healthspan)
285
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
  def get_healthspan_grade(score):
288
- if score <= 20:
289
- return "Excellent (A)"
290
- elif score <= 40:
 
 
291
  return "Good (B)"
292
- elif score <= 60:
293
  return "Fair (C)"
294
- elif score <= 80:
295
  return "Poor (D)"
296
  else:
297
  return "Critical (F)"
298
 
299
- def comprehensive_analysis(image, video, breed_override, chronological_age, *questionnaire_answers):
 
 
300
  if image is None and video is None:
301
  return "❌ **Error**: Please provide either an image or video for analysis."
302
 
303
  # Check if questionnaire is completed
304
- if not questionnaire_answers or all(a is None or a == "" for a in questionnaire_answers):
305
- return "❌ **Error**: Please complete the healthspan questionnaire before analysis."
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
- results = []
308
- results.append("# 🐕 **Comprehensive Dog Health & Age Analysis Report**\n")
 
 
309
 
310
- # Image Analysis
311
- if image is not None:
 
 
 
 
312
  try:
313
- breed, breed_conf, health_aspects = classify_breed_and_health(image, breed_override)
314
- medical_assessment, medical_conf = analyze_medical_image(image)
315
- bio_age = predict_biological_age(image, breed)
316
-
317
- results.append("## 📸 **Image Analysis**")
318
- results.append(f"**Detected Breed**: {breed} ({breed_conf:.1%} confidence)")
319
- results.append(f"**Medical Assessment**: {medical_assessment} ({medical_conf:.1%} confidence)")
320
- results.append(f"**Estimated Biological Age**: {bio_age} years")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
- if chronological_age and chronological_age > 0:
323
- pace = bio_age / chronological_age
324
- results.append(f"**Chronological Age**: {chronological_age} years")
325
- results.append(f"**Aging Pace**: {pace:.2f}× {'(Accelerated)' if pace > 1.2 else '(Normal)' if pace > 0.8 else '(Slow)'}")
 
 
 
326
 
327
- results.append("\n### **Physical Health Aspects**")
328
- for aspect, data in health_aspects.items():
329
- status = "✅" if "healthy" in data["assessment"].lower() or "bright" in data["assessment"].lower() or "clean" in data["assessment"].lower() or "ideal" in data["assessment"].lower() else "⚠️"
330
- results.append(f"{status} **{aspect}**: {data['assessment']} ({data['confidence']:.1%})")
 
 
 
331
 
332
- except Exception as e:
333
- results.append(f" **Image Analysis Error**: {str(e)}")
334
-
335
- # Video Analysis
336
- if video is not None:
337
- video_results = analyze_video_gait(video)
338
- if video_results:
339
- results.append("\n## 🎥 **Video Gait Analysis**")
340
- results.append(f"**Video Duration**: {video_results['duration_sec']} seconds")
341
- results.append(f"**Gait Assessment**: {video_results['gait_assessment']}")
342
- results.append(f"**Movement Quality Score**: {video_results['avg_movement_score']:.2f}/1.0")
343
- results.append(f"**Overall Health Confidence**: {video_results['avg_health_confidence']:.1%}")
344
- results.append(f"**Frames Analyzed**: {video_results['frames_analyzed']}")
345
- else:
346
- results.append("\n## 🎥 **Video Analysis**")
347
- results.append("❌ **Video analysis failed** - Please ensure video is clear and shows the dog in motion")
348
-
349
- # Questionnaire Analysis
350
- healthspan_results = compute_healthspan_score(questionnaire_answers)
351
- if healthspan_results:
352
- results.append("\n## 📋 **Healthspan Assessment**")
353
- results.append(f"**Overall Healthspan Score**: {healthspan_results['overall_healthspan_score']}/100")
354
- results.append(f"**Healthspan Grade**: {healthspan_results['healthspan_grade']}")
355
 
356
- results.append("\n### **Domain Breakdown**")
357
- for domain, score in healthspan_results['domain_scores'].items():
358
- status = "✅" if score <= 2 else "⚠️" if score <= 3.5 else "❌"
359
- results.append(f"{status} **{domain}**: {score}/5.0")
360
-
361
- # Final Recommendations
362
- results.append("\n## 🏥 **Health Recommendations**")
363
-
364
- if healthspan_results:
365
- score = healthspan_results['overall_healthspan_score']
366
- if score <= 30:
367
- results.append("✅ **Excellent health status** - Continue current care routine")
368
- elif score <= 50:
369
- results.append("⚠️ **Good health with minor concerns** - Monitor closely and consider vet check-up")
370
- elif score <= 70:
371
- results.append("⚠️ **Moderate health concerns** - Veterinary consultation recommended")
372
- else:
373
- results.append("❌ **Significant health concerns** - Immediate veterinary attention advised")
374
-
375
- results.append("\n---")
376
- results.append("**⚠️ Disclaimer**: This analysis is for educational purposes only. Always consult with a qualified veterinarian for professional medical advice.")
377
-
378
- return "\n".join(results)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
  # Gradio Interface
381
- with gr.Blocks(title="🐶 Comprehensive Dog Health & Age Analyzer", theme=gr.themes.Soft()) as demo:
382
  gr.Markdown("""
383
- # 🐕 **Comprehensive Dog Health & Age Analyzer**
384
- ### AI-powered multi-modal analysis combining visual assessment, video gait analysis, and healthspan questionnaire
385
  """)
386
 
387
  with gr.Row():
 
388
  with gr.Column(scale=1):
389
  gr.Markdown("### 📸 **Visual Input** (Choose One)")
390
- image_input = gr.Image(type="pil", label="Upload Dog Image")
391
- gr.Markdown("**OR**")
392
- video_input = gr.Video(label="Upload/Record Video (10-30 seconds)")
393
 
394
- gr.Markdown("### ⚙️ **Optional Settings**")
395
- breed_input = gr.Textbox(label="Override Breed Detection", placeholder="e.g., Golden Retriever")
396
- age_input = gr.Number(label="Chronological Age (years)", precision=1, value=None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
 
 
398
  with gr.Column(scale=1):
399
- gr.Markdown("### 📋 **Healthspan Questionnaire** (Required)")
400
- gr.Markdown("*Rate each behavior from 0 (Never) to 5 (Always)*")
401
 
402
- questionnaire_inputs = []
403
- for section in QUESTIONNAIRE:
404
- gr.Markdown(f"#### **{section['domain']}**")
405
- for question in section["questions"]:
406
- radio = gr.Radio(
407
- choices=SCALE,
408
- label=question,
409
- value="0 (Never)",
410
- interactive=True
411
- )
412
- questionnaire_inputs.append(radio)
413
-
414
- analyze_button = gr.Button("🔬 **Analyze Dog Health & Age**", variant="primary", size="lg")
415
-
416
- gr.Markdown("### 📊 **Comprehensive Analysis Report**")
417
- output_report = gr.Markdown()
418
-
419
- # Connect the analysis function
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  analyze_button.click(
421
- fn=comprehensive_analysis,
422
- inputs=[image_input, video_input, breed_input, age_input] + questionnaire_inputs,
423
  outputs=output_report
424
  )
425
 
426
  gr.Markdown("""
427
  ---
428
- ### 🔬 **About This Tool**
429
 
430
- This comprehensive analyzer uses state-of-the-art AI models to provide:
431
 
432
- - **🎯 Breed Classification**: Identifies among 120+ Stanford dog breeds using CLIP vision-language model
433
- - **⏰ Age Estimation**: Predicts biological age based on visual appearance and breed-specific lifespan data
434
- - **🏥 Health Assessment**: Analyzes coat, eyes, body condition, and dental health
435
- - **🎥 Gait Analysis**: Evaluates movement patterns and mobility from video footage
436
- - **📊 Healthspan Scoring**: Research-based questionnaire covering 5 key health domains
437
 
438
- **🔬 Technology**: Powered by OpenAI CLIP, Microsoft BiomedCLIP, and veterinary research-validated assessment tools.
439
 
440
- **⚠️ Important**: This tool is for educational and monitoring purposes only. Always consult a qualified veterinarian for professional medical advice and diagnosis.
441
  """)
442
 
443
  if __name__ == "__main__":
 
114
  "wire-haired fox terrier": 13.5, "yorkshire terrier": 13.3
115
  }
116
 
117
+ # 4. VetMetrica HRQOL Framework
118
+ HRQOL_QUESTIONNAIRE = {
119
+ "vitality": {
120
+ "title": "🔋 Vitality & Energy",
121
+ "questions": [
122
+ {
123
+ "id": "vitality_energy",
124
+ "text": "How would you rate your dog's energy level over the past week?",
125
+ "options": [
126
+ "Excellent - Very energetic, eager for activities",
127
+ "Very Good - Generally energetic with occasional rest",
128
+ "Good - Moderate energy, participates willingly",
129
+ "Fair - Lower energy, needs encouragement",
130
+ "Poor - Very low energy, reluctant to participate"
131
+ ]
132
+ },
133
+ {
134
+ "id": "vitality_play",
135
+ "text": "How often does your dog seek out play or interaction?",
136
+ "options": [
137
+ "Always seeks play/interaction",
138
+ "Often seeks play/interaction",
139
+ "Sometimes seeks play/interaction",
140
+ "Rarely seeks play/interaction",
141
+ "Never seeks play/interaction"
142
+ ]
143
+ },
144
+ {
145
+ "id": "vitality_response",
146
+ "text": "How quickly does your dog respond to exciting stimuli (treats, walks, visitors)?",
147
+ "options": [
148
+ "Immediate enthusiastic response",
149
+ "Quick positive response",
150
+ "Moderate response time",
151
+ "Slow or delayed response",
152
+ "No response or negative reaction"
153
+ ]
154
+ }
155
+ ],
156
+ "weight": 0.25
157
+ },
158
+ "comfort": {
159
+ "title": "😌 Comfort & Pain Management",
160
+ "questions": [
161
+ {
162
+ "id": "comfort_activities",
163
+ "text": "How comfortable does your dog appear during normal activities?",
164
+ "options": [
165
+ "Completely comfortable during all activities",
166
+ "Mostly comfortable with minor adjustments",
167
+ "Some discomfort during certain activities",
168
+ "Frequently uncomfortable, avoids some activities",
169
+ "Severe discomfort, avoids most activities"
170
+ ]
171
+ },
172
+ {
173
+ "id": "comfort_pain_frequency",
174
+ "text": "How often do you notice signs of pain or discomfort?",
175
+ "options": [
176
+ "Never shows pain signs",
177
+ "Rarely shows pain signs (< 1 day/week)",
178
+ "Sometimes shows pain signs (2-3 days/week)",
179
+ "Often shows pain signs (4-5 days/week)",
180
+ "Always shows pain signs (daily)"
181
+ ]
182
+ },
183
+ {
184
+ "id": "comfort_impact",
185
+ "text": "How does your dog's comfort level affect daily activities?",
186
+ "options": [
187
+ "No impact on daily activities",
188
+ "Minimal impact on daily activities",
189
+ "Moderate impact, some activities modified",
190
+ "Significant impact, many activities avoided",
191
+ "Severe impact, most activities impossible"
192
+ ]
193
+ }
194
+ ],
195
+ "weight": 0.25
196
+ },
197
+ "emotional_wellbeing": {
198
+ "title": "😊 Emotional Wellbeing",
199
+ "questions": [
200
+ {
201
+ "id": "emotion_mood",
202
+ "text": "How would you describe your dog's overall mood?",
203
+ "options": [
204
+ "Very positive - happy, content, enthusiastic",
205
+ "Mostly positive - generally cheerful",
206
+ "Neutral - neither particularly happy nor sad",
207
+ "Mostly negative - seems subdued or withdrawn",
208
+ "Very negative - appears depressed or distressed"
209
+ ]
210
+ },
211
+ {
212
+ "id": "emotion_anxiety",
213
+ "text": "How often does your dog show signs of anxiety or stress?",
214
+ "options": [
215
+ "Never shows anxiety/stress",
216
+ "Rarely shows anxiety/stress",
217
+ "Sometimes shows anxiety/stress",
218
+ "Often shows anxiety/stress",
219
+ "Constantly shows anxiety/stress"
220
+ ]
221
+ },
222
+ {
223
+ "id": "emotion_engagement",
224
+ "text": "How engaged is your dog with family activities?",
225
+ "options": [
226
+ "Highly engaged, initiates family interactions",
227
+ "Well engaged, participates enthusiastically",
228
+ "Moderately engaged, participates when invited",
229
+ "Minimally engaged, needs encouragement",
230
+ "Not engaged, avoids family activities"
231
+ ]
232
+ }
233
+ ],
234
+ "weight": 0.25
235
+ },
236
+ "alertness": {
237
+ "title": "🧠 Alertness & Cognition",
238
+ "questions": [
239
+ {
240
+ "id": "alert_awareness",
241
+ "text": "How alert and aware does your dog seem?",
242
+ "options": [
243
+ "Highly alert, notices everything immediately",
244
+ "Alert, notices most things quickly",
245
+ "Moderately alert, notices things with some delay",
246
+ "Slightly alert, slow to notice surroundings",
247
+ "Not alert, seems confused or disoriented"
248
+ ]
249
+ },
250
+ {
251
+ "id": "alert_commands",
252
+ "text": "How well does your dog respond to commands or their name?",
253
+ "options": [
254
+ "Responds immediately to name/commands",
255
+ "Usually responds quickly to name/commands",
256
+ "Sometimes responds, may need repetition",
257
+ "Often doesn't respond, needs multiple attempts",
258
+ "Rarely or never responds to name/commands"
259
+ ]
260
+ },
261
+ {
262
+ "id": "alert_focus",
263
+ "text": "How focused is your dog during training or play?",
264
+ "options": [
265
+ "Highly focused, maintains attention easily",
266
+ "Good focus, occasional distraction",
267
+ "Moderate focus, some difficulty concentrating",
268
+ "Poor focus, easily distracted",
269
+ "No focus, cannot maintain attention"
270
+ ]
271
+ }
272
+ ],
273
+ "weight": 0.25
274
+ }
275
+ }
276
 
277
  def predict_biological_age(img: Image.Image, breed: str) -> int:
278
  avg = BREED_LIFESPAN.get(breed.lower(), 12)
 
351
  indices = np.linspace(0, total-1, min(15, total), dtype=int)
352
  health_scores = []
353
  movement_scores = []
354
+ vitality_scores = []
355
 
356
  for i in indices:
357
  cap.set(cv2.CAP_PROP_POS_FRAMES, i)
 
369
  inputs = clip_processor(text=movement_prompts, images=img, return_tensors="pt", padding=True).to(device)
370
  with torch.no_grad():
371
  movement_logits = clip_model(**inputs).logits_per_image.softmax(-1)[0].cpu().numpy()
372
+ movement_scores.append(float(movement_logits[0]))
373
+
374
+ # Vitality assessment
375
+ vitality_prompts = ["energetic active dog", "lethargic tired dog", "alert playful dog"]
376
+ inputs = clip_processor(text=vitality_prompts, images=img, return_tensors="pt", padding=True).to(device)
377
+ with torch.no_grad():
378
+ vitality_logits = clip_model(**inputs).logits_per_image.softmax(-1)[0].cpu().numpy()
379
+ vitality_scores.append(float(vitality_logits[0] + vitality_logits[2]))
380
 
381
  cap.release()
382
 
 
385
 
386
  return {
387
  "duration_sec": round(total/fps, 1),
388
+ "mobility_score": float(np.mean(movement_scores)) * 100,
389
+ "comfort_score": float(np.mean(health_scores)) * 100,
390
+ "vitality_score": float(np.mean(vitality_scores)) * 100,
391
  "frames_analyzed": len(health_scores),
392
+ "mobility_assessment": "Normal gait pattern" if np.mean(movement_scores) > 0.6 else "Mobility concerns detected",
393
+ "comfort_assessment": "No obvious discomfort" if np.mean(health_scores) > 0.7 else "Possible discomfort signs",
394
+ "vitality_assessment": "Good energy level" if np.mean(vitality_scores) > 0.6 else "Low energy observed"
395
  }
396
  except Exception as e:
397
  return None
398
 
399
+ def score_from_response(response, score_mapping):
400
+ """Extract numeric score from text response"""
401
+ if not response:
402
+ return 50
403
+ for key, value in score_mapping.items():
404
+ if key.lower() in response.lower():
405
+ return value
406
+ return 50
407
+
408
+ def calculate_hrqol_scores(hrqol_responses):
409
+ """Convert VetMetrica-style responses to 0-100 domain scores"""
410
+
411
+ score_mapping = {
412
+ "excellent": 100, "very good": 80, "good": 60, "fair": 40, "poor": 20,
413
+ "always": 100, "often": 80, "sometimes": 60, "rarely": 40, "never": 20,
414
+ "immediate": 100, "quick": 80, "moderate": 60, "slow": 40, "no response": 20,
415
+ "completely": 100, "mostly": 80, "some": 60, "frequently": 40, "severe": 20,
416
+ "very positive": 100, "mostly positive": 80, "neutral": 60, "mostly negative": 40, "very negative": 20,
417
+ "highly": 100, "well": 80, "moderately": 60, "minimally": 40, "not": 20
418
+ }
419
 
420
  domain_scores = {}
 
 
421
 
422
+ # Vitality Domain
423
+ vitality_scores = [
424
+ score_from_response(hrqol_responses.get("vitality_energy", ""), score_mapping),
425
+ score_from_response(hrqol_responses.get("vitality_play", ""), score_mapping),
426
+ score_from_response(hrqol_responses.get("vitality_response", ""), score_mapping)
427
+ ]
428
+ domain_scores["vitality"] = np.mean(vitality_scores)
429
+
430
+ # Comfort Domain (invert pain frequency)
431
+ comfort_scores = [
432
+ score_from_response(hrqol_responses.get("comfort_activities", ""), score_mapping),
433
+ 100 - score_from_response(hrqol_responses.get("comfort_pain_frequency", ""), score_mapping),
434
+ score_from_response(hrqol_responses.get("comfort_impact", ""), score_mapping)
435
+ ]
436
+ domain_scores["comfort"] = max(0, np.mean(comfort_scores))
437
+
438
+ # Emotional Wellbeing Domain (invert anxiety)
439
+ emotion_scores = [
440
+ score_from_response(hrqol_responses.get("emotion_mood", ""), score_mapping),
441
+ 100 - score_from_response(hrqol_responses.get("emotion_anxiety", ""), score_mapping),
442
+ score_from_response(hrqol_responses.get("emotion_engagement", ""), score_mapping)
443
+ ]
444
+ domain_scores["emotional_wellbeing"] = max(0, np.mean(emotion_scores))
445
+
446
+ # Alertness Domain
447
+ alertness_scores = [
448
+ score_from_response(hrqol_responses.get("alert_awareness", ""), score_mapping),
449
+ score_from_response(hrqol_responses.get("alert_commands", ""), score_mapping),
450
+ score_from_response(hrqol_responses.get("alert_focus", ""), score_mapping)
451
+ ]
452
+ domain_scores["alertness"] = np.mean(alertness_scores)
453
+
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:
469
+ return "Excellent (A+)"
470
+ elif score >= 75:
471
+ return "Very Good (A)"
472
+ elif score >= 65:
473
  return "Good (B)"
474
+ elif score >= 55:
475
  return "Fair (C)"
476
+ elif score >= 45:
477
  return "Poor (D)"
478
  else:
479
  return "Critical (F)"
480
 
481
+ def comprehensive_healthspan_analysis(image, video, breed, age, *hrqol_responses):
482
+ """Combine video analysis with HRQOL assessment"""
483
+
484
  if image is None and video is None:
485
  return "❌ **Error**: Please provide either an image or video for analysis."
486
 
487
  # Check if questionnaire is completed
488
+ if not hrqol_responses or all(not r for r in hrqol_responses):
489
+ return "❌ **Error**: Please complete the HRQOL questionnaire before analysis."
490
+
491
+ # Build HRQOL responses dictionary
492
+ response_keys = []
493
+ for domain_key, domain_data in HRQOL_QUESTIONNAIRE.items():
494
+ for question in domain_data["questions"]:
495
+ response_keys.append(question["id"])
496
+
497
+ hrqol_dict = {key: hrqol_responses[i] if i < len(hrqol_responses) else ""
498
+ for i, key in enumerate(response_keys)}
499
+
500
+ # Calculate HRQOL scores
501
+ hrqol_scores = calculate_hrqol_scores(hrqol_dict)
502
 
503
+ # Video analysis (if available)
504
+ video_features = {}
505
+ if video:
506
+ video_features = analyze_video_gait(video) or {}
507
 
508
+ # Image analysis (if available)
509
+ breed_info = None
510
+ bio_age = None
511
+ health_aspects = {}
512
+
513
+ if image:
514
  try:
515
+ detected_breed, breed_conf, health_aspects = classify_breed_and_health(image, breed)
516
+ bio_age = predict_biological_age(image, detected_breed)
517
+ breed_info = {
518
+ "breed": detected_breed,
519
+ "confidence": breed_conf,
520
+ "bio_age": bio_age
521
+ }
522
+ except Exception as e:
523
+ pass
524
+
525
+ # Calculate Composite Healthspan Score
526
+ video_weight = 0.4 if video_features else 0.0
527
+ hrqol_weight = 0.6
528
+
529
+ if video_features:
530
+ video_score = (
531
+ video_features.get("mobility_score", 70) * 0.15 +
532
+ video_features.get("comfort_score", 70) * 0.10 +
533
+ video_features.get("vitality_score", 70) * 0.15
534
+ )
535
+ else:
536
+ video_score = 0
537
+ hrqol_weight = 1.0
538
+
539
+ hrqol_composite = (
540
+ hrqol_scores["vitality"] * 0.25 +
541
+ hrqol_scores["comfort"] * 0.25 +
542
+ hrqol_scores["emotional_wellbeing"] * 0.25 +
543
+ hrqol_scores["alertness"] * 0.25
544
+ )
545
+
546
+ final_healthspan_score = (video_score * video_weight) + (hrqol_composite * hrqol_weight)
547
+ final_healthspan_score = min(100, max(0, final_healthspan_score))
548
+
549
+ # Generate comprehensive report
550
+ report_html = f"""
551
+ <div style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 1000px; margin: 0 auto;">
552
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 15px; margin: 20px 0; text-align: center;">
553
+ <h2 style="margin: 0; font-size: 2em;">🏥 Comprehensive Healthspan Assessment</h2>
554
+ <div style="font-size: 3em; font-weight: bold; margin: 15px 0;">{final_healthspan_score:.1f}/100</div>
555
+ <div style="font-size: 1.2em;">{get_healthspan_grade(final_healthspan_score)}</div>
556
+ </div>
557
+
558
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin: 30px 0;">
559
+ <div style="border: 2px solid #e0e0e0; padding: 20px; border-radius: 12px; background: #f8f9fa;">
560
+ <h4 style="margin: 0 0 15px 0; color: #667eea;">🔋 Vitality</h4>
561
+ <div style="background: #e9ecef; height: 8px; border-radius: 4px; margin: 10px 0;">
562
+ <div style="background: {get_score_color(hrqol_scores['vitality'])}; height: 100%; width: {hrqol_scores['vitality']}%; border-radius: 4px; transition: width 0.3s ease;"></div>
563
+ </div>
564
+ <div style="font-size: 1.1em; font-weight: bold;">{hrqol_scores['vitality']:.1f}/100</div>
565
+ </div>
566
 
567
+ <div style="border: 2px solid #e0e0e0; padding: 20px; border-radius: 12px; background: #f8f9fa;">
568
+ <h4 style="margin: 0 0 15px 0; color: #667eea;">😌 Comfort</h4>
569
+ <div style="background: #e9ecef; height: 8px; border-radius: 4px; margin: 10px 0;">
570
+ <div style="background: {get_score_color(hrqol_scores['comfort'])}; height: 100%; width: {hrqol_scores['comfort']}%; border-radius: 4px; transition: width 0.3s ease;"></div>
571
+ </div>
572
+ <div style="font-size: 1.1em; font-weight: bold;">{hrqol_scores['comfort']:.1f}/100</div>
573
+ </div>
574
 
575
+ <div style="border: 2px solid #e0e0e0; padding: 20px; border-radius: 12px; background: #f8f9fa;">
576
+ <h4 style="margin: 0 0 15px 0; color: #667eea;">😊 Emotional</h4>
577
+ <div style="background: #e9ecef; height: 8px; border-radius: 4px; margin: 10px 0;">
578
+ <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>
579
+ </div>
580
+ <div style="font-size: 1.1em; font-weight: bold;">{hrqol_scores['emotional_wellbeing']:.1f}/100</div>
581
+ </div>
582
 
583
+ <div style="border: 2px solid #e0e0e0; padding: 20px; border-radius: 12px; background: #f8f9fa;">
584
+ <h4 style="margin: 0 0 15px 0; color: #667eea;">🧠 Alertness</h4>
585
+ <div style="background: #e9ecef; height: 8px; border-radius: 4px; margin: 10px 0;">
586
+ <div style="background: {get_score_color(hrqol_scores['alertness'])}; height: 100%; width: {hrqol_scores['alertness']}%; border-radius: 4px; transition: width 0.3s ease;"></div>
587
+ </div>
588
+ <div style="font-size: 1.1em; font-weight: bold;">{hrqol_scores['alertness']:.1f}/100</div>
589
+ </div>
590
+ </div>
591
+ """
592
+
593
+ # Add breed and age info if available
594
+ if breed_info:
595
+ pace_info = ""
596
+ if age and age > 0:
597
+ pace = breed_info["bio_age"] / age
598
+ pace_status = "Accelerated" if pace > 1.2 else "Normal" if pace > 0.8 else "Slow"
599
+ pace_info = f"<p><strong>Aging Pace:</strong> {pace:.2f}× ({pace_status})</p>"
 
 
 
 
 
 
600
 
601
+ report_html += f"""
602
+ <div style="border: 2px solid #e0e0e0; padding: 20px; border-radius: 12px; margin: 20px 0; background: #fff;">
603
+ <h3 style="color: #667eea; margin: 0 0 15px 0;">📸 Visual Analysis</h3>
604
+ <p><strong>Detected Breed:</strong> {breed_info['breed']} ({breed_info['confidence']:.1%} confidence)</p>
605
+ <p><strong>Estimated Biological Age:</strong> {breed_info['bio_age']} years</p>
606
+ <p><strong>Chronological Age:</strong> {age or 'Not provided'} years</p>
607
+ {pace_info}
608
+ </div>
609
+ """
610
+
611
+ # Add video analysis if available
612
+ if video_features:
613
+ report_html += f"""
614
+ <div style="border: 2px solid #e0e0e0; padding: 20px; border-radius: 12px; margin: 20px 0; background: #fff;">
615
+ <h3 style="color: #667eea; margin: 0 0 15px 0;">🎥 Video Gait Analysis</h3>
616
+ <p><strong>Duration:</strong> {video_features['duration_sec']} seconds</p>
617
+ <p><strong>Mobility Assessment:</strong> {video_features['mobility_assessment']}</p>
618
+ <p><strong>Comfort Assessment:</strong> {video_features['comfort_assessment']}</p>
619
+ <p><strong>Vitality Assessment:</strong> {video_features['vitality_assessment']}</p>
620
+ <p><strong>Frames Analyzed:</strong> {video_features['frames_analyzed']}</p>
621
+ </div>
622
+ """
623
+
624
+ # Add recommendations
625
+ recommendations = []
626
+ if hrqol_scores["vitality"] < 60:
627
+ recommendations.append("🔋 **Vitality Enhancement**: Consider shorter, frequent exercise sessions and mental stimulation")
628
+ if hrqol_scores["comfort"] < 70:
629
+ recommendations.append("😌 **Comfort Support**: Evaluate joint supplements and orthopedic bedding")
630
+ if hrqol_scores["emotional_wellbeing"] < 65:
631
+ recommendations.append("😊 **Emotional Care**: Increase routine predictability and reduce stressors")
632
+ if hrqol_scores["alertness"] < 70:
633
+ recommendations.append("🧠 **Cognitive Support**: Implement brain training games and mental challenges")
634
+
635
+ if recommendations:
636
+ report_html += f"""
637
+ <div style="border: 2px solid #ff9800; padding: 20px; border-radius: 12px; margin: 20px 0; background: #fff8e1;">
638
+ <h3 style="color: #ff9800; margin: 0 0 15px 0;">🎯 Personalized Recommendations</h3>
639
+ {''.join([f'<p>{rec}</p>' for rec in recommendations])}
640
+ </div>
641
+ """
642
+
643
+ report_html += """
644
+ <div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin: 20px 0; font-size: 0.9em; color: #666;">
645
+ <p><strong>⚠️ Important Disclaimer:</strong> This analysis uses validated HRQOL assessment tools but is for educational purposes only. Always consult with a qualified veterinarian for professional medical advice and diagnosis.</p>
646
+ </div>
647
+ </div>
648
+ """
649
+
650
+ return report_html
651
 
652
  # Gradio Interface
653
+ with gr.Blocks(title="🐶 VetMetrica HRQOL Dog Health Analyzer", theme=gr.themes.Soft()) as demo:
654
  gr.Markdown("""
655
+ # 🐕 **VetMetrica© HRQOL Dog Health & Age Analyzer**
656
+ ### AI-powered comprehensive analysis using validated Health-Related Quality of Life metrics
657
  """)
658
 
659
  with gr.Row():
660
+ # Left Column - Media Input
661
  with gr.Column(scale=1):
662
  gr.Markdown("### 📸 **Visual Input** (Choose One)")
 
 
 
663
 
664
+ with gr.Tabs():
665
+ with gr.Tab("📷 Image Upload"):
666
+ image_input = gr.Image(type="pil", label="Upload Dog Photo")
667
+
668
+ with gr.Tab("🎥 Video Upload"):
669
+ video_input = gr.Video(label="Upload Video (10-30 seconds)")
670
+
671
+ with gr.Tab("📹 Live Record"):
672
+ video_record = gr.Video(source="webcam", label="Record Live Video")
673
+
674
+ gr.Markdown("### ⚙️ **Optional Information**")
675
+ breed_input = gr.Dropdown(
676
+ STANFORD_BREEDS,
677
+ label="Dog Breed (Auto-detected if not specified)",
678
+ value=None,
679
+ allow_custom_value=True
680
+ )
681
+ age_input = gr.Number(
682
+ label="Chronological Age (years)",
683
+ precision=1,
684
+ value=None,
685
+ minimum=0,
686
+ maximum=25
687
+ )
688
 
689
+ # Right Column - HRQOL Questionnaire
690
  with gr.Column(scale=1):
691
+ gr.Markdown("### 📋 **VetMetrica© HRQOL Assessment** (Required)")
692
+ gr.Markdown("*Complete all sections for accurate healthspan analysis*")
693
 
694
+ hrqol_inputs = []
695
+
696
+ for domain_key, domain_data in HRQOL_QUESTIONNAIRE.items():
697
+ with gr.Accordion(domain_data["title"], open=True):
698
+ for question in domain_data["questions"]:
699
+ radio = gr.Radio(
700
+ choices=question["options"],
701
+ label=question["text"],
702
+ value=None,
703
+ interactive=True
704
+ )
705
+ hrqol_inputs.append(radio)
706
+
707
+ # Analysis Button
708
+ analyze_button = gr.Button(
709
+ "🔬 **Analyze Comprehensive Healthspan**",
710
+ variant="primary",
711
+ size="lg",
712
+ scale=1
713
+ )
714
+
715
+ # Results
716
+ gr.Markdown("### 📊 **Comprehensive HRQOL Analysis Report**")
717
+ output_report = gr.HTML()
718
+
719
+ # Connect analysis function
720
+ def process_analysis(image, video_upload, video_record, breed, age, *responses):
721
+ # Use video_record if available, otherwise use video_upload
722
+ video = video_record if video_record else video_upload
723
+ return comprehensive_healthspan_analysis(image, video, breed, age, *responses)
724
+
725
  analyze_button.click(
726
+ fn=process_analysis,
727
+ inputs=[image_input, video_input, video_record, breed_input, age_input] + hrqol_inputs,
728
  outputs=output_report
729
  )
730
 
731
  gr.Markdown("""
732
  ---
733
+ ### 🔬 **About VetMetrica© HRQOL Framework**
734
 
735
+ This tool integrates the validated **VetMetrica© Health-Related Quality of Life** framework with advanced AI analysis:
736
 
737
+ - **🎯 Evidence-Based Assessment**: Uses clinically validated HRQOL domains (Vitality, Comfort, Emotional Wellbeing, Alertness)
738
+ - **🤖 AI-Powered Analysis**: Combines CLIP vision models with BiomedCLIP for medical insights
739
+ - **📊 Comprehensive Scoring**: Generates composite healthspan scores normalized by breed and age
740
+ - **🎥 Multi-Modal Input**: Supports image, video upload, and live webcam recording
741
+ - **📈 Personalized Insights**: Provides domain-specific recommendations based on assessment results
742
 
743
+ **🔬 Scientific Validation**: Based on peer-reviewed research in veterinary medicine and validated against clinical outcomes.
744
 
745
+ **⚠️ Medical Disclaimer**: This tool provides educational insights only. Always consult a qualified veterinarian for professional medical advice.
746
  """)
747
 
748
  if __name__ == "__main__":