dpv007 commited on
Commit
50aa8ae
·
verified ·
1 Parent(s): 892c265

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +71 -65
app.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
- Elderly HealthWatch AI Backend (FastAPI) - Refactored
3
- Simplified architecture with same API routes for frontend compatibility.
4
  """
5
 
6
  import io
@@ -32,14 +32,21 @@ except Exception:
32
  logging.basicConfig(level=logging.INFO)
33
  logger = logging.getLogger("elderly_healthwatch")
34
 
35
- GRADIO_VLM_SPACE = os.getenv("GRADIO_SPACE", "developer0hye/Qwen3-VL-8B-Instruct")
 
 
36
  LLM_GRADIO_SPACE = os.getenv("LLM_GRADIO_SPACE", "Tonic/med-gpt-oss-20b-demo")
37
  HF_TOKEN = os.getenv("HF_TOKEN", None)
38
 
39
  DEFAULT_VLM_PROMPT = (
40
- "From the provided face/eye images, compute the required screening features "
41
- "(pallor, sclera yellowness, redness, mobility metrics, quality checks) "
42
- "and output a clean JSON feature vector only."
 
 
 
 
 
43
  )
44
 
45
  LLM_SYSTEM_PROMPT = (
@@ -280,7 +287,7 @@ def extract_json_from_llm_output(raw_text: str) -> Dict[str, Any]:
280
  }
281
 
282
  # ============================================================================
283
- # VLM & LLM Integration
284
  # ============================================================================
285
  def get_gradio_client(space: str) -> Client:
286
  """Get Gradio client with optional auth"""
@@ -289,7 +296,10 @@ def get_gradio_client(space: str) -> Client:
289
  return Client(space, hf_token=HF_TOKEN) if HF_TOKEN else Client(space)
290
 
291
  def call_vlm(face_path: str, eye_path: str, prompt: Optional[str] = None) -> Tuple[Optional[Dict], str]:
292
- """Call VLM and return (parsed_features, raw_text)"""
 
 
 
293
  prompt = prompt or DEFAULT_VLM_PROMPT
294
 
295
  if not os.path.exists(face_path) or not os.path.exists(eye_path):
@@ -302,69 +312,61 @@ def call_vlm(face_path: str, eye_path: str, prompt: Optional[str] = None) -> Tup
302
  logger.info("VLM Prompt: %s", prompt[:100])
303
 
304
  client = get_gradio_client(GRADIO_VLM_SPACE)
305
- message = {"text": prompt, "files": [handle_file(face_path), handle_file(eye_path)]}
306
 
307
  try:
308
- logger.info("Calling VLM Space: %s with api_name=/chat_fn", GRADIO_VLM_SPACE)
309
- result = client.predict(message=message, history=[], api_name="/chat_fn")
310
- logger.info("VLM raw result type: %s", type(result))
311
- logger.info("VLM raw result: %s", str(result)[:500])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  except Exception as e:
313
  logger.exception("VLM call failed")
314
  raise RuntimeError(f"VLM call failed: {e}")
315
 
316
- # Normalize result - handle different return formats
317
- if isinstance(result, (list, tuple)):
318
- logger.info("VLM returned list/tuple with %d elements", len(result))
319
- if len(result) > 0:
320
- out = result[0]
321
- else:
322
- out = {}
323
- elif isinstance(result, dict):
324
- logger.info("VLM returned dict with keys: %s", list(result.keys()))
325
- out = result
326
- else:
327
- logger.info("VLM returned unknown type, converting to string")
328
- out = {"text": str(result)}
329
-
330
- # Extract text from various possible formats
331
- text_out = None
332
- if isinstance(out, dict):
333
- text_out = out.get("text") or out.get("output") or out.get("content")
334
-
335
- if not text_out:
336
- # If still no text, try the whole result
337
- if isinstance(result, str):
338
- text_out = result
339
- else:
340
- text_out = json.dumps(out)
341
-
342
- logger.info("VLM extracted text (first 300 chars): %s", text_out[:300] if text_out else "EMPTY")
343
-
344
- if not text_out or len(text_out.strip()) == 0:
345
- logger.warning("VLM returned empty text output!")
346
- text_out = "{}" # Provide empty JSON as fallback
347
-
348
- # Try to parse JSON
349
  parsed = None
350
  try:
351
- parsed = json.loads(text_out)
 
352
  if not isinstance(parsed, dict):
353
  logger.warning("VLM JSON parsed but not a dict: %s", type(parsed))
354
  parsed = None
355
  else:
356
  logger.info("VLM successfully parsed JSON with keys: %s", list(parsed.keys()))
357
- except Exception as parse_err:
358
- logger.info("VLM text is not direct JSON: %s", str(parse_err))
359
- # Try to extract JSON from text
360
  try:
361
- first = text_out.find("{")
362
- last = text_out.rfind("}")
363
  if first != -1 and last != -1 and last > first:
364
- json_str = text_out[first:last+1]
365
  parsed = json.loads(json_str)
366
  if not isinstance(parsed, dict):
367
- logger.warning("Extracted JSON is not a dict")
368
  parsed = None
369
  else:
370
  logger.info("Successfully extracted JSON from text with keys: %s", list(parsed.keys()))
@@ -372,7 +374,16 @@ def call_vlm(face_path: str, eye_path: str, prompt: Optional[str] = None) -> Tup
372
  logger.warning("Could not extract JSON from VLM text: %s", str(extract_err))
373
  parsed = None
374
 
375
- return parsed, text_out
 
 
 
 
 
 
 
 
 
376
 
377
  def get_fallback_risk_assessment(vlm_output: Any, reason: str = "LLM unavailable") -> Dict[str, Any]:
378
  """Generate basic risk assessment from VLM output when LLM is unavailable"""
@@ -414,7 +425,6 @@ def get_fallback_risk_assessment(vlm_output: Any, reason: str = "LLM unavailable
414
  neuro_prob = 0.0
415
 
416
  # Extract VLM features if available
417
- # Look for color indicators
418
  sclera_yellow = vlm_dict.get("sclera_yellowness", 0)
419
  pallor = vlm_dict.get("pallor_score", 0)
420
  redness = vlm_dict.get("redness", 0)
@@ -437,7 +447,7 @@ def get_fallback_risk_assessment(vlm_output: Any, reason: str = "LLM unavailable
437
  "anemia_probability": round(anemia_prob, 4),
438
  "hydration_issue_probability": round(hydration_prob, 4),
439
  "neurological_issue_probability": round(neuro_prob, 4),
440
- "confidence": 0.4, # Low confidence for fallback
441
  "summary": "Basic screening completed. Advanced AI analysis temporarily unavailable.",
442
  "recommendation": "Consider consulting a healthcare professional for a comprehensive assessment.",
443
  "fallback_mode": True,
@@ -498,7 +508,7 @@ def call_llm(vlm_output: Any, use_fallback_on_error: bool = True) -> Dict[str, A
498
  parsed = extract_json_from_llm_output(text_out)
499
  logger.info("LLM parsed JSON:\n%s", json.dumps(parsed, indent=2))
500
 
501
- # Check if LLM returned essentially empty results (all zeros)
502
  all_zero = all(
503
  parsed.get(k, 0) == 0
504
  for k in ["jaundice_probability", "anemia_probability",
@@ -516,14 +526,12 @@ def call_llm(vlm_output: Any, use_fallback_on_error: bool = True) -> Dict[str, A
516
  except Exception as e:
517
  logger.exception("LLM call failed: %s", str(e))
518
 
519
- # Check if it's a quota error
520
  error_msg = str(e).lower()
521
  if "quota" in error_msg or "gpu" in error_msg:
522
  logger.warning("GPU quota exceeded, using fallback assessment")
523
  if use_fallback_on_error:
524
  return get_fallback_risk_assessment(vlm_output, reason="gpu_quota_exceeded")
525
 
526
- # For other errors, also use fallback if enabled
527
  if use_fallback_on_error:
528
  logger.warning("LLM error, using fallback assessment")
529
  return get_fallback_risk_assessment(vlm_output, reason=f"llm_error: {str(e)[:100]}")
@@ -629,7 +637,7 @@ app.add_middleware(
629
 
630
  @app.get("/")
631
  async def read_root():
632
- return {"message": "Elderly HealthWatch AI Backend"}
633
 
634
  @app.get("/health")
635
  async def health_check():
@@ -637,11 +645,9 @@ async def health_check():
637
  llm_status = "available"
638
  llm_message = None
639
 
640
- # Quick test of LLM availability
641
  if GRADIO_AVAILABLE:
642
  try:
643
  client = get_gradio_client(LLM_GRADIO_SPACE)
644
- # Just checking if we can connect, not running inference
645
  llm_status = "available"
646
  except Exception as e:
647
  error_msg = str(e).lower()
@@ -660,6 +666,7 @@ async def health_check():
660
  "detector": detector_type or "none",
661
  "vlm_available": GRADIO_AVAILABLE,
662
  "vlm_space": GRADIO_VLM_SPACE,
 
663
  "llm_space": LLM_GRADIO_SPACE,
664
  "llm_status": llm_status,
665
  "llm_message": llm_message,
@@ -794,18 +801,17 @@ async def debug_spaces():
794
  "llm": {"available": False, "error": None}
795
  }
796
 
797
- # Test VLM
798
  if GRADIO_AVAILABLE:
799
  try:
800
  client = get_gradio_client(GRADIO_VLM_SPACE)
801
  results["vlm"]["available"] = True
802
  results["vlm"]["space"] = GRADIO_VLM_SPACE
 
803
  except Exception as e:
804
  results["vlm"]["error"] = str(e)
805
  else:
806
  results["vlm"]["error"] = "Gradio not installed"
807
 
808
- # Test LLM
809
  if GRADIO_AVAILABLE:
810
  try:
811
  client = get_gradio_client(LLM_GRADIO_SPACE)
 
1
  """
2
+ Elderly HealthWatch AI Backend (FastAPI) - Refactored with Qwen2.5-VL
3
+ Updated to use mrdbourke/Qwen2.5-VL-Instruct-Demo instead of Qwen3-VL
4
  """
5
 
6
  import io
 
32
  logging.basicConfig(level=logging.INFO)
33
  logger = logging.getLogger("elderly_healthwatch")
34
 
35
+ # Updated VLM space to use Qwen2.5-VL
36
+ GRADIO_VLM_SPACE = os.getenv("GRADIO_SPACE", "mrdbourke/Qwen2.5-VL-Instruct-Demo")
37
+ VLM_MODEL_ID = os.getenv("VLM_MODEL_ID", "Qwen/Qwen2.5-VL-7B-Instruct")
38
  LLM_GRADIO_SPACE = os.getenv("LLM_GRADIO_SPACE", "Tonic/med-gpt-oss-20b-demo")
39
  HF_TOKEN = os.getenv("HF_TOKEN", None)
40
 
41
  DEFAULT_VLM_PROMPT = (
42
+ "Analyze this person's face and eyes carefully. Look for signs of: "
43
+ "1) Pallor (pale skin indicating possible anemia), "
44
+ "2) Sclera yellowness (yellowing of eye whites indicating possible jaundice), "
45
+ "3) Eye redness or irritation, "
46
+ "4) Skin hydration and texture. "
47
+ "Provide your analysis in JSON format with these keys: "
48
+ "pallor_score (0-1), sclera_yellowness (0-1), redness (0-1), "
49
+ "hydration_issue (0-1), overall_quality (0-1), notes."
50
  )
51
 
52
  LLM_SYSTEM_PROMPT = (
 
287
  }
288
 
289
  # ============================================================================
290
+ # VLM & LLM Integration - UPDATED FOR QWEN2.5-VL
291
  # ============================================================================
292
  def get_gradio_client(space: str) -> Client:
293
  """Get Gradio client with optional auth"""
 
296
  return Client(space, hf_token=HF_TOKEN) if HF_TOKEN else Client(space)
297
 
298
  def call_vlm(face_path: str, eye_path: str, prompt: Optional[str] = None) -> Tuple[Optional[Dict], str]:
299
+ """
300
+ Call Qwen2.5-VL and return (parsed_features, raw_text)
301
+ Updated to use mrdbourke/Qwen2.5-VL-Instruct-Demo API
302
+ """
303
  prompt = prompt or DEFAULT_VLM_PROMPT
304
 
305
  if not os.path.exists(face_path) or not os.path.exists(eye_path):
 
312
  logger.info("VLM Prompt: %s", prompt[:100])
313
 
314
  client = get_gradio_client(GRADIO_VLM_SPACE)
 
315
 
316
  try:
317
+ logger.info("Calling Qwen2.5-VL Space: %s with api_name=/run_example", GRADIO_VLM_SPACE)
318
+
319
+ # First call with face image
320
+ result_face = client.predict(
321
+ image=handle_file(face_path),
322
+ text_input=prompt + " Focus on the face and overall skin condition.",
323
+ model_id=VLM_MODEL_ID,
324
+ api_name="/run_example"
325
+ )
326
+
327
+ # Second call with eye image
328
+ result_eye = client.predict(
329
+ image=handle_file(eye_path),
330
+ text_input=prompt + " Focus on the eyes, sclera color, and eye health.",
331
+ model_id=VLM_MODEL_ID,
332
+ api_name="/run_example"
333
+ )
334
+
335
+ logger.info("VLM Face result type: %s", type(result_face))
336
+ logger.info("VLM Eye result type: %s", type(result_eye))
337
+
338
+ # Extract text from tuple results (returns tuple of 2 elements)
339
+ face_text = result_face[0] if isinstance(result_face, (list, tuple)) and len(result_face) > 0 else str(result_face)
340
+ eye_text = result_eye[0] if isinstance(result_eye, (list, tuple)) and len(result_eye) > 0 else str(result_eye)
341
+
342
+ # Combine both analyses
343
+ combined_text = f"Face Analysis:\n{face_text}\n\nEye Analysis:\n{eye_text}"
344
+
345
+ logger.info("VLM combined text (first 500 chars): %s", combined_text[:500])
346
+
347
  except Exception as e:
348
  logger.exception("VLM call failed")
349
  raise RuntimeError(f"VLM call failed: {e}")
350
 
351
+ # Try to parse JSON from the combined text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  parsed = None
353
  try:
354
+ # Try direct JSON parse first
355
+ parsed = json.loads(combined_text)
356
  if not isinstance(parsed, dict):
357
  logger.warning("VLM JSON parsed but not a dict: %s", type(parsed))
358
  parsed = None
359
  else:
360
  logger.info("VLM successfully parsed JSON with keys: %s", list(parsed.keys()))
361
+ except Exception:
362
+ # Try to extract JSON block
 
363
  try:
364
+ first = combined_text.find("{")
365
+ last = combined_text.rfind("}")
366
  if first != -1 and last != -1 and last > first:
367
+ json_str = combined_text[first:last+1]
368
  parsed = json.loads(json_str)
369
  if not isinstance(parsed, dict):
 
370
  parsed = None
371
  else:
372
  logger.info("Successfully extracted JSON from text with keys: %s", list(parsed.keys()))
 
374
  logger.warning("Could not extract JSON from VLM text: %s", str(extract_err))
375
  parsed = None
376
 
377
+ # If no JSON found, create structured data from text
378
+ if parsed is None:
379
+ logger.info("No JSON found, creating structured data from text analysis")
380
+ parsed = {
381
+ "face_analysis": face_text[:500],
382
+ "eye_analysis": eye_text[:500],
383
+ "combined_analysis": combined_text[:1000]
384
+ }
385
+
386
+ return parsed, combined_text
387
 
388
  def get_fallback_risk_assessment(vlm_output: Any, reason: str = "LLM unavailable") -> Dict[str, Any]:
389
  """Generate basic risk assessment from VLM output when LLM is unavailable"""
 
425
  neuro_prob = 0.0
426
 
427
  # Extract VLM features if available
 
428
  sclera_yellow = vlm_dict.get("sclera_yellowness", 0)
429
  pallor = vlm_dict.get("pallor_score", 0)
430
  redness = vlm_dict.get("redness", 0)
 
447
  "anemia_probability": round(anemia_prob, 4),
448
  "hydration_issue_probability": round(hydration_prob, 4),
449
  "neurological_issue_probability": round(neuro_prob, 4),
450
+ "confidence": 0.4,
451
  "summary": "Basic screening completed. Advanced AI analysis temporarily unavailable.",
452
  "recommendation": "Consider consulting a healthcare professional for a comprehensive assessment.",
453
  "fallback_mode": True,
 
508
  parsed = extract_json_from_llm_output(text_out)
509
  logger.info("LLM parsed JSON:\n%s", json.dumps(parsed, indent=2))
510
 
511
+ # Check if LLM returned essentially empty results
512
  all_zero = all(
513
  parsed.get(k, 0) == 0
514
  for k in ["jaundice_probability", "anemia_probability",
 
526
  except Exception as e:
527
  logger.exception("LLM call failed: %s", str(e))
528
 
 
529
  error_msg = str(e).lower()
530
  if "quota" in error_msg or "gpu" in error_msg:
531
  logger.warning("GPU quota exceeded, using fallback assessment")
532
  if use_fallback_on_error:
533
  return get_fallback_risk_assessment(vlm_output, reason="gpu_quota_exceeded")
534
 
 
535
  if use_fallback_on_error:
536
  logger.warning("LLM error, using fallback assessment")
537
  return get_fallback_risk_assessment(vlm_output, reason=f"llm_error: {str(e)[:100]}")
 
637
 
638
  @app.get("/")
639
  async def read_root():
640
+ return {"message": "Elderly HealthWatch AI Backend - Using Qwen2.5-VL"}
641
 
642
  @app.get("/health")
643
  async def health_check():
 
645
  llm_status = "available"
646
  llm_message = None
647
 
 
648
  if GRADIO_AVAILABLE:
649
  try:
650
  client = get_gradio_client(LLM_GRADIO_SPACE)
 
651
  llm_status = "available"
652
  except Exception as e:
653
  error_msg = str(e).lower()
 
666
  "detector": detector_type or "none",
667
  "vlm_available": GRADIO_AVAILABLE,
668
  "vlm_space": GRADIO_VLM_SPACE,
669
+ "vlm_model": VLM_MODEL_ID,
670
  "llm_space": LLM_GRADIO_SPACE,
671
  "llm_status": llm_status,
672
  "llm_message": llm_message,
 
801
  "llm": {"available": False, "error": None}
802
  }
803
 
 
804
  if GRADIO_AVAILABLE:
805
  try:
806
  client = get_gradio_client(GRADIO_VLM_SPACE)
807
  results["vlm"]["available"] = True
808
  results["vlm"]["space"] = GRADIO_VLM_SPACE
809
+ results["vlm"]["model"] = VLM_MODEL_ID
810
  except Exception as e:
811
  results["vlm"]["error"] = str(e)
812
  else:
813
  results["vlm"]["error"] = "Gradio not installed"
814
 
 
815
  if GRADIO_AVAILABLE:
816
  try:
817
  client = get_gradio_client(LLM_GRADIO_SPACE)