dpv007 commited on
Commit
5a9130c
·
verified ·
1 Parent(s): 245014a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +130 -3
app.py CHANGED
@@ -4,6 +4,7 @@ Elderly HealthWatch AI Backend (FastAPI)
4
  This variant uses:
5
  - facenet-pytorch or mtcnn if available
6
  - otherwise falls back to OpenCV Haar cascades (fast, CPU-only, lightweight)
 
7
  """
8
 
9
  import io
@@ -18,6 +19,28 @@ import numpy as np
18
  import os
19
  import traceback
20
  import cv2 # opencv-python-headless expected installed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  # Attempt to import facenet-pytorch MTCNN first (recommended)
23
  try:
@@ -102,6 +125,92 @@ def estimate_eye_openness_from_detection(confidence: float) -> float:
102
  except Exception:
103
  return 0.0
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  @app.get("/")
106
  async def read_root():
107
  return {"message": "Elderly HealthWatch AI Backend"}
@@ -115,7 +224,7 @@ async def health_check():
115
  impl = "opencv_haar_fallback"
116
  else:
117
  impl = _MTCNN_IMPL
118
- return {"status": "healthy", "detector": impl}
119
 
120
  @app.post("/api/v1/validate-eye-photo")
121
  async def validate_eye_photo(image: UploadFile = File(...)):
@@ -398,7 +507,23 @@ async def process_screening(screening_id: str):
398
  }
399
  screenings_db[screening_id]["quality_metrics"] = quality_metrics
400
 
401
- # Simulate VLM/medical model steps (kept short)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  await asyncio.sleep(1)
403
  vlm_face_desc = "Patient appears to have normal facial tone; no severe jaundice visible."
404
  vlm_eye_desc = "Sclera shows mild yellowing."
@@ -427,7 +552,9 @@ async def process_screening(screening_id: str):
427
  "medical_insights": medical_insights,
428
  "processing_time_ms": 1200
429
  }
430
- screenings_db[screening_id]["ai_results"] = ai_results
 
 
431
 
432
  disease_predictions = [
433
  {
 
4
  This variant uses:
5
  - facenet-pytorch or mtcnn if available
6
  - otherwise falls back to OpenCV Haar cascades (fast, CPU-only, lightweight)
7
+ - integrates a remote VLM via gradio_client to get JSON feature vectors
8
  """
9
 
10
  import io
 
19
  import os
20
  import traceback
21
  import cv2 # opencv-python-headless expected installed
22
+ import json
23
+ import logging
24
+
25
+ # Optional gradio client import (for VLM)
26
+ try:
27
+ from gradio_client import Client, handle_file
28
+ GRADIO_AVAILABLE = True
29
+ except Exception:
30
+ GRADIO_AVAILABLE = False
31
+
32
+ # Configure logging
33
+ logging.basicConfig(level=logging.INFO)
34
+
35
+ # Configuration for remote VLM (change to your target Space)
36
+ GRADIO_SPACE = os.getenv("GRADIO_SPACE", "developer0hye/Qwen3-VL-8B-Instruct")
37
+ # If your space is private, set HF_TOKEN as a secret / env var in Spaces
38
+ HF_TOKEN = os.getenv("HF_TOKEN", None)
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
  # Attempt to import facenet-pytorch MTCNN first (recommended)
46
  try:
 
125
  except Exception:
126
  return 0.0
127
 
128
+ # --------------------------
129
+ # VLM client helper
130
+ # --------------------------
131
+ def get_gradio_client():
132
+ """Return a configured gradio Client or raise if not available."""
133
+ if not GRADIO_AVAILABLE:
134
+ raise RuntimeError("gradio_client not installed in this environment.")
135
+ if HF_TOKEN:
136
+ return Client(GRADIO_SPACE, hf_token=HF_TOKEN)
137
+ return Client(GRADIO_SPACE)
138
+
139
+ def run_vlm_and_get_features(face_path: str, eye_path: str, prompt: Optional[str] = None) -> Dict[str, Any]:
140
+ """
141
+ Synchronous call to the remote VLM (gradio / chat_fn).
142
+ Expects the VLM to return a JSON string only (we parse it).
143
+ On success returns a dict (parsed JSON).
144
+ On failure raises RuntimeError or ValueError.
145
+ """
146
+ prompt = prompt or DEFAULT_VLM_PROMPT
147
+
148
+ if not os.path.exists(face_path) or not os.path.exists(eye_path):
149
+ raise FileNotFoundError("Face or eye image path missing for VLM call.")
150
+
151
+ if not GRADIO_AVAILABLE:
152
+ raise RuntimeError("gradio_client is not available in this environment.")
153
+
154
+ client = get_gradio_client()
155
+
156
+ message = {
157
+ "text": prompt,
158
+ "files": [handle_file(face_path), handle_file(eye_path)]
159
+ }
160
+
161
+ # Call the remote API
162
+ try:
163
+ logging.info("Calling remote VLM at %s", GRADIO_SPACE)
164
+ # result is typically a tuple: (output_dict, new_history)
165
+ result = client.predict(message=message, history=[], api_name="/chat_fn")
166
+ except Exception as e:
167
+ logging.exception("VLM call failed")
168
+ raise RuntimeError(f"VLM call failed: {e}")
169
+
170
+ # Extract text output
171
+ if not result or not isinstance(result, (list, tuple)):
172
+ # some spaces return just a dict too
173
+ if isinstance(result, dict):
174
+ out = result
175
+ else:
176
+ raise RuntimeError("Unexpected VLM response shape")
177
+ else:
178
+ out = result[0]
179
+
180
+ if not isinstance(out, dict):
181
+ raise RuntimeError("Unexpected VLM output format (expected dict with 'text' key)")
182
+
183
+ text_out = out.get("text") or out.get("output") or None
184
+ if not text_out:
185
+ raise RuntimeError("VLM returned empty text output")
186
+
187
+ # The model was instructed to return JSON only. Try to parse it.
188
+ try:
189
+ features = json.loads(text_out)
190
+ except Exception:
191
+ # attempt a forgiving extraction: find the first { ... } block
192
+ try:
193
+ s = text_out
194
+ first = s.find("{")
195
+ last = s.rfind("}")
196
+ if first != -1 and last != -1 and last > first:
197
+ maybe = s[first:last+1]
198
+ features = json.loads(maybe)
199
+ else:
200
+ raise
201
+ except Exception as e:
202
+ logging.exception("Failed to parse JSON from VLM output")
203
+ raise ValueError(f"Failed to parse JSON from VLM output: {e}\nRaw output: {text_out}")
204
+
205
+ if not isinstance(features, dict):
206
+ raise ValueError("Parsed VLM output is not a JSON object/dict")
207
+
208
+ return features
209
+
210
+ # --------------------------
211
+ # End VLM helper
212
+ # --------------------------
213
+
214
  @app.get("/")
215
  async def read_root():
216
  return {"message": "Elderly HealthWatch AI Backend"}
 
224
  impl = "opencv_haar_fallback"
225
  else:
226
  impl = _MTCNN_IMPL
227
+ return {"status": "healthy", "detector": impl, "vlm_available": GRADIO_AVAILABLE}
228
 
229
  @app.post("/api/v1/validate-eye-photo")
230
  async def validate_eye_photo(image: UploadFile = File(...)):
 
507
  }
508
  screenings_db[screening_id]["quality_metrics"] = quality_metrics
509
 
510
+ # Attempt VLM call to compute multimodal features (pallor, sclera yellowness, etc.)
511
+ try:
512
+ vlm_features = run_vlm_and_get_features(face_path, eye_path)
513
+ # attach under ai_results
514
+ screenings_db[screening_id].setdefault("ai_results", {})
515
+ screenings_db[screening_id]["ai_results"].update({
516
+ "vlm_features": vlm_features
517
+ })
518
+ except Exception as e:
519
+ # Don't fail the entire pipeline for VLM errors; record them
520
+ logging.exception("VLM feature extraction failed")
521
+ screenings_db[screening_id].setdefault("ai_results", {})
522
+ screenings_db[screening_id]["ai_results"].update({
523
+ "vlm_error": str(e)
524
+ })
525
+
526
+ # Simulate Medical model steps (kept short)
527
  await asyncio.sleep(1)
528
  vlm_face_desc = "Patient appears to have normal facial tone; no severe jaundice visible."
529
  vlm_eye_desc = "Sclera shows mild yellowing."
 
552
  "medical_insights": medical_insights,
553
  "processing_time_ms": 1200
554
  }
555
+ # Merge ai_results while preserving vlm_features if present
556
+ screenings_db[screening_id].setdefault("ai_results", {})
557
+ screenings_db[screening_id]["ai_results"].update(ai_results)
558
 
559
  disease_predictions = [
560
  {