Saicharan21 commited on
Commit
7f44105
·
verified ·
1 Parent(s): 310d31d

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +273 -364
app.py CHANGED
@@ -12,17 +12,15 @@ from PIL import Image
12
  GROQ_KEY = os.environ.get("GROQ_API_KEY", "")
13
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
14
 
15
- KNOWHOW = ("SJSU CardioLab: "
16
- "MCL: Sylgard 184 PDMS 10:1 ratio 48hr cure green laser PIV 70bpm 5L/min. "
17
  "TGT: Arduino Uno Stepper Motor 150mL blood sampled at 0 20 40 60min measures TAT PF1.2 hemolysis platelets. "
18
  "uPAD: Jaffe reaction creatinine plus picric acid gives orange-red color normal 0.6-1.2 mg/dL CKD above 1.5. "
19
- "MHV: 27mm SJM Regent bileaflet also trileaflet monoleaflet pediatric. "
20
- "Equipment: Heska HT5 hematology analyzer time-resolved PIV Tygon tubing Arduino Uno.")
21
 
22
  CSS = """
23
  body, .gradio-container { background: #f0f4f8 !important; }
24
- .tab-nav { background: #ffffff !important; border-bottom: 2px solid #e2e8f0 !important; padding: 0 10px !important; }
25
- .tab-nav button { background: #f7fafc !important; color: #2d3748 !important; border: 1px solid #e2e8f0 !important; border-radius: 8px 8px 0 0 !important; padding: 12px 18px !important; font-weight: 600 !important; margin-top: 6px !important; }
26
  .tab-nav button:hover { background: #ebf4ff !important; color: #1a237e !important; }
27
  .tab-nav button.selected { background: linear-gradient(135deg, #e63946, #c1121f) !important; color: #ffffff !important; font-weight: 700 !important; }
28
  button.primary { background: linear-gradient(135deg, #e63946 0%, #c1121f 100%) !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 700 !important; }
@@ -33,112 +31,141 @@ textarea, input[type=number] { background: #f7fafc !important; color: #1a202c !i
33
  label span { color: #2b6cb0 !important; font-weight: 600 !important; font-size: 0.85em !important; text-transform: uppercase !important; }
34
  """
35
 
36
- # ─── PIV CSV ANALYSIS ────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  def analyze_piv_csv(file):
38
  if file is None:
39
- return None, "Please upload a PIV CSV file."
40
  try:
41
  df = pd.read_csv(file.name)
42
  cols = [c.lower().strip() for c in df.columns]
43
  df.columns = cols
 
 
 
44
 
45
  fig, axes = plt.subplots(2, 2, figsize=(14, 10))
46
  fig.patch.set_facecolor("#0d1b3e")
47
- fig.suptitle("PIV Data Analysis — SJSU CardioLab MCL", color="white", fontsize=16, fontweight="bold", y=1.02)
48
-
49
- colors = ["#e63946", "#4361ee", "#2ecc71", "#e67e22"]
50
-
51
- # Plot 1 — Velocity over time or position
52
- ax1 = axes[0, 0]
53
- ax1.set_facecolor("#1a2744")
54
- vel_col = next((c for c in cols if "vel" in c or "v_" in c or "u" == c or "speed" in c), cols[0] if len(cols)>0 else None)
55
- x_col = next((c for c in cols if "time" in c or "x" in c or "pos" in c or "frame" in c), None)
56
- if vel_col and x_col:
57
- ax1.plot(df[x_col], df[vel_col], color="#e63946", linewidth=2, label=vel_col)
58
- ax1.set_xlabel(x_col, color="#a8b2d8")
59
- ax1.set_ylabel(vel_col, color="#a8b2d8")
60
- elif vel_col:
61
- ax1.plot(df[vel_col], color="#e63946", linewidth=2)
62
- ax1.set_ylabel(vel_col, color="#a8b2d8")
63
- else:
64
- ax1.plot(df.iloc[:,0], color="#e63946", linewidth=2)
65
- ax1.set_title("Velocity Profile", color="white", fontweight="bold")
66
- ax1.tick_params(colors="#a8b2d8")
67
- ax1.grid(True, alpha=0.2, color="#2d4a8a")
68
- ax1.spines["bottom"].set_color("#2d4a8a")
69
- ax1.spines["left"].set_color("#2d4a8a")
70
- ax1.spines["top"].set_visible(False)
71
- ax1.spines["right"].set_visible(False)
72
-
73
- # Plot 2 — Shear stress if available
74
- ax2 = axes[0, 1]
75
- ax2.set_facecolor("#1a2744")
76
- shear_col = next((c for c in cols if "shear" in c or "stress" in c or "tau" in c or "wss" in c), None)
77
  if shear_col:
78
- ax2.fill_between(range(len(df)), df[shear_col], alpha=0.7, color="#4361ee")
79
- ax2.plot(df[shear_col], color="#4361ee", linewidth=2)
80
- ax2.axhline(y=5, color="#e63946", linestyle="--", linewidth=1.5, label="Risk threshold (5 Pa)")
81
- ax2.axhline(y=10, color="#ff4444", linestyle="--", linewidth=1.5, label="High risk (10 Pa)")
82
- ax2.set_ylabel("Shear Stress (Pa)", color="#a8b2d8")
83
  ax2.legend(fontsize=8, labelcolor="white", facecolor="#1a2744")
84
- else:
85
- # Plot second numeric column
86
- num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
87
- if len(num_cols) >= 2:
88
- ax2.fill_between(range(len(df)), df[num_cols[1]], alpha=0.7, color="#4361ee")
89
- ax2.plot(df[num_cols[1]], color="#4361ee", linewidth=2)
90
- ax2.set_ylabel(num_cols[1], color="#a8b2d8")
91
- ax2.set_title("Shear Stress / Secondary Variable", color="white", fontweight="bold")
92
- ax2.tick_params(colors="#a8b2d8")
93
- ax2.grid(True, alpha=0.2, color="#2d4a8a")
94
- ax2.spines["bottom"].set_color("#2d4a8a")
95
- ax2.spines["left"].set_color("#2d4a8a")
96
- ax2.spines["top"].set_visible(False)
97
- ax2.spines["right"].set_visible(False)
98
-
99
- # Plot 3 — Distribution histogram
100
- ax3 = axes[1, 0]
101
- ax3.set_facecolor("#1a2744")
102
- num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
103
- if num_cols:
104
- ax3.hist(df[num_cols[0]].dropna(), bins=30, color="#2ecc71", alpha=0.8, edgecolor="#1a2744")
105
- ax3.set_xlabel(num_cols[0], color="#a8b2d8")
106
- ax3.set_ylabel("Count", color="#a8b2d8")
107
- ax3.set_title("Value Distribution", color="white", fontweight="bold")
108
- ax3.tick_params(colors="#a8b2d8")
109
- ax3.grid(True, alpha=0.2, color="#2d4a8a")
110
- ax3.spines["bottom"].set_color("#2d4a8a")
111
- ax3.spines["left"].set_color("#2d4a8a")
112
- ax3.spines["top"].set_visible(False)
113
- ax3.spines["right"].set_visible(False)
114
-
115
- # Plot 4 — Summary stats
116
- ax4 = axes[1, 1]
117
  ax4.set_facecolor("#1a2744")
118
  ax4.axis("off")
119
- num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
120
- summary_text = "SUMMARY STATISTICS" + chr(10) + "━"*22 + chr(10)
121
- risk_flags = []
122
- for col in num_cols[:4]:
123
- mean_val = df[col].mean()
124
- max_val = df[col].max()
125
- min_val = df[col].min()
126
- summary_text += f"{col[:15]}:" + chr(10)
127
- summary_text += f" Mean: {mean_val:.3f}" + chr(10)
128
- summary_text += f" Max: {max_val:.3f}" + chr(10)
129
- summary_text += f" Min: {min_val:.3f}" + chr(10)
130
- if "vel" in col.lower() and max_val > 2.0:
131
- risk_flags.append("HIGH VELOCITY - stenosis risk")
132
- if "shear" in col.lower() and max_val > 10:
133
- risk_flags.append("HIGH SHEAR - thrombosis risk")
134
- if risk_flags:
135
- summary_text += chr(10) + "RISK FLAGS:" + chr(10)
136
- for flag in risk_flags:
137
- summary_text += " ⚠ " + flag + chr(10)
138
- ax4.text(0.05, 0.95, summary_text, transform=ax4.transAxes,
139
- color="white", fontsize=9, verticalalignment="top",
140
- fontfamily="monospace",
141
- bbox=dict(boxstyle="round", facecolor="#0d1b3e", edgecolor="#4361ee", alpha=0.8))
142
 
143
  plt.tight_layout()
144
  buf = io.BytesIO()
@@ -147,106 +174,83 @@ def analyze_piv_csv(file):
147
  img = Image.open(buf)
148
  plt.close()
149
 
150
- # AI analysis
151
- ai_summary = ""
152
  if GROQ_KEY:
153
  try:
154
  client = Groq(api_key=GROQ_KEY)
155
- stats = df.describe().to_string()
156
- msgs = [{"role":"system","content":"You are a PIV flow analysis expert for SJSU CardioLab. Analyze the statistics from the PIV CSV data and provide a clinical interpretation. Mention velocity ranges, shear stress levels, risk of stenosis or thrombosis, and recommendations."}]
157
- msgs.append({"role":"user","content":"Analyze this PIV data from our Mock Circulatory Loop with 27mm SJM Regent MHV at 70bpm 5L/min:"+chr(10)+stats[:1000]})
158
- resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=400)
159
- ai_summary = chr(10)+"━"*30+chr(10)+"AI CLINICAL INTERPRETATION:"+chr(10)+resp.choices[0].message.content
160
  except: pass
161
 
162
- result_text = ("PIV CSV ANALYSIS COMPLETE"+chr(10)+
163
- "Rows: "+str(len(df))+" | Columns: "+str(len(df.columns))+chr(10)+
164
- "Columns: "+", ".join(df.columns.tolist())+chr(10)+ai_summary)
165
-
166
- return img, result_text
167
-
168
  except Exception as e:
169
- return None, "Error reading CSV: "+str(e)+chr(10)+"Make sure your CSV has headers and numeric data."
170
 
171
- # ─── TGT CSV ANALYSIS ────────────────────────────────────────────
172
  def analyze_tgt_csv(file):
173
  if file is None:
174
- return None, "Please upload a TGT CSV file."
175
  try:
176
  df = pd.read_csv(file.name)
177
  cols = [c.lower().strip() for c in df.columns]
178
  df.columns = cols
 
 
 
179
 
180
  fig, axes = plt.subplots(2, 2, figsize=(14, 10))
181
  fig.patch.set_facecolor("#0d1b3e")
182
- fig.suptitle("TGT Blood Analysis — SJSU CardioLab", color="white", fontsize=16, fontweight="bold", y=1.02)
183
 
184
- # Expected TGT columns: time, TAT, PF12, hemoglobin, platelets
185
  time_col = next((c for c in cols if "time" in c or "min" in c), None)
186
- tat_col = next((c for c in cols if "tat" in c or "thrombin" in c), None)
187
- pf_col = next((c for c in cols if "pf" in c or "pf1" in c or "prothrombin" in c), None)
188
- hemo_col = next((c for c in cols if "hemo" in c or "hemoglobin" in c or "hgb" in c), None)
189
- plt_col = next((c for c in cols if "platelet" in c or "plt" in c), None)
190
-
191
- num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
192
- x_axis = df[time_col] if time_col else range(len(df))
193
- x_label = time_col if time_col else "Sample Number"
194
-
195
- normal_limits = {"tat":8, "pf":2.0, "hemo":20, "platelet":150}
196
-
197
- def style_ax(ax, title):
198
  ax.set_facecolor("#1a2744")
199
  ax.set_title(title, color="white", fontweight="bold")
 
 
200
  ax.tick_params(colors="#a8b2d8")
201
- ax.set_xlabel(x_label, color="#a8b2d8")
202
  ax.grid(True, alpha=0.2, color="#2d4a8a")
203
- ax.spines["bottom"].set_color("#2d4a8a")
204
- ax.spines["left"].set_color("#2d4a8a")
205
- ax.spines["top"].set_visible(False)
206
- ax.spines["right"].set_visible(False)
207
-
208
- # Plot 1 TAT
209
- ax1 = axes[0, 0]
210
- col = tat_col if tat_col else (num_cols[0] if num_cols else None)
211
- if col:
212
- ax1.plot(x_axis, df[col], color="#e63946", linewidth=2.5, marker="o", markersize=6, label=col)
213
- ax1.axhline(y=8, color="#ffd700", linestyle="--", linewidth=1.5, label="Normal limit (8 ng/mL)")
214
- ax1.fill_between(x_axis, df[col], alpha=0.3, color="#e63946")
215
- ax1.set_ylabel("TAT (ng/mL)", color="#a8b2d8")
216
  ax1.legend(fontsize=8, labelcolor="white", facecolor="#1a2744")
217
- style_ax(ax1, "Thrombin-Antithrombin (TAT)")
218
-
219
- # Plot 2 — PF1.2
220
- ax2 = axes[0, 1]
221
- col2 = pf_col if pf_col else (num_cols[1] if len(num_cols)>1 else None)
222
- if col2:
223
- ax2.plot(x_axis, df[col2], color="#4361ee", linewidth=2.5, marker="s", markersize=6, label=col2)
224
- ax2.axhline(y=2.0, color="#ffd700", linestyle="--", linewidth=1.5, label="Normal limit (2.0)")
225
- ax2.fill_between(x_axis, df[col2], alpha=0.3, color="#4361ee")
226
- ax2.set_ylabel("PF1.2 (nmol/L)", color="#a8b2d8")
227
  ax2.legend(fontsize=8, labelcolor="white", facecolor="#1a2744")
228
- style_ax(ax2, "Prothrombin Fragment (PF1.2)")
229
-
230
- # Plot 3 — Hemoglobin / Hemolysis
231
- ax3 = axes[1, 0]
232
- col3 = hemo_col if hemo_col else (num_cols[2] if len(num_cols)>2 else None)
233
- if col3:
234
- ax3.bar(range(len(df)), df[col3], color="#2ecc71", alpha=0.8, edgecolor="#1a2744", label=col3)
235
- ax3.axhline(y=20, color="#ffd700", linestyle="--", linewidth=1.5, label="Normal limit (20 mg/L)")
236
- ax3.set_ylabel("Free Hemoglobin (mg/L)", color="#a8b2d8")
237
  ax3.legend(fontsize=8, labelcolor="white", facecolor="#1a2744")
238
- style_ax(ax3, "Free Hemoglobin (Hemolysis)")
239
-
240
- # Plot 4 — Platelets
241
- ax4 = axes[1, 1]
242
- col4 = plt_col if plt_col else (num_cols[3] if len(num_cols)>3 else None)
243
- if col4:
244
- ax4.plot(x_axis, df[col4], color="#e67e22", linewidth=2.5, marker="^", markersize=6, label=col4)
245
- ax4.axhline(y=150, color="#ffd700", linestyle="--", linewidth=1.5, label="Normal minimum (150)")
246
- ax4.fill_between(x_axis, df[col4], 150, where=df[col4]<150, alpha=0.3, color="#e63946", label="Below normal")
247
- ax4.set_ylabel("Platelet Count (10³/μL)", color="#a8b2d8")
248
  ax4.legend(fontsize=8, labelcolor="white", facecolor="#1a2744")
249
- style_ax(ax4, "Platelet Count")
250
 
251
  plt.tight_layout()
252
  buf = io.BytesIO()
@@ -255,181 +259,91 @@ def analyze_tgt_csv(file):
255
  img = Image.open(buf)
256
  plt.close()
257
 
258
- # AI analysis
259
- ai_summary = ""
260
  if GROQ_KEY:
261
  try:
262
  client = Groq(api_key=GROQ_KEY)
263
- stats = df.describe().to_string()
264
- msgs = [{"role":"system","content":"You are a hematology expert for SJSU CardioLab TGT testing. Analyze the blood biomarker data and provide thrombogenicity assessment. Comment on TAT levels, PF1.2, hemolysis, platelet consumption. Give overall thrombogenic risk: LOW MODERATE or HIGH. Reference normal ranges: TAT below 8 ng/mL, PF1.2 below 2.0 nmol/L, Hemoglobin below 20 mg/L, Platelets above 150."}]
265
- msgs.append({"role":"user","content":"Analyze TGT blood data from 27mm SJM Regent MHV tested in our MCL at 70bpm 5L/min:"+chr(10)+stats[:1000]})
266
- resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=400)
267
- ai_summary = chr(10)+"━"*30+chr(10)+"AI THROMBOGENICITY ASSESSMENT:"+chr(10)+resp.choices[0].message.content
268
  except: pass
269
 
270
- result_text = ("TGT CSV ANALYSIS COMPLETE"+chr(10)+
271
- "Rows: "+str(len(df))+" | Columns: "+str(len(df.columns))+chr(10)+
272
- "Columns detected: "+", ".join(df.columns.tolist())+chr(10)+ai_summary)
273
-
274
- return img, result_text
275
-
276
  except Exception as e:
277
- return None, "Error reading CSV: "+str(e)+chr(10)+"Make sure your CSV has headers like: time, TAT, PF12, hemoglobin, platelets"
278
-
279
- # ─── OTHER FUNCTIONS ──────────────────────────────────────────────
280
- def get_pubmed(query, n=5):
281
- try:
282
- r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
283
- params={"db":"pubmed","term":query+" AND (mechanical heart valve OR microfluidic OR CKD OR thrombogenicity)","retmax":n,"retmode":"json","sort":"date"},timeout=10)
284
- ids = r.json()["esearchresult"]["idlist"]
285
- if not ids: return ""
286
- return chr(10).join(["https://pubmed.ncbi.nlm.nih.gov/"+i for i in ids])
287
- except: return ""
288
-
289
- def get_scholar(query, n=5):
290
- try:
291
- r = requests.get("https://api.semanticscholar.org/graph/v1/paper/search",
292
- params={"query":query+" biomedical","limit":n,"fields":"title,year,url,citationCount"},timeout=10)
293
- papers = r.json().get("data",[])
294
- out = []
295
- for p in papers:
296
- url = p.get("url","")
297
- if url: out.append(p.get("title","")[:80]+" ("+str(p.get("year",""))+") - "+str(p.get("citationCount",0))+" citations"+chr(10)+" "+url)
298
- return chr(10).join(out)
299
- except: return ""
300
-
301
- def quick_search(query):
302
- if not query.strip(): return "Please enter a research topic."
303
- pubmed = get_pubmed(query, n=8)
304
- scholar = get_scholar(query, n=5)
305
- return "PUBMED RESULTS:"+chr(10)+pubmed+chr(10)+chr(10)+"SEMANTIC SCHOLAR:"+chr(10)+scholar
306
-
307
- def research_chat(message, history):
308
- if not GROQ_KEY:
309
- history.append({"role":"user","content":message})
310
- history.append({"role":"assistant","content":"Error: Add GROQ_API_KEY to Space Settings Secrets."})
311
- return "", history
312
- try:
313
- client = Groq(api_key=GROQ_KEY)
314
- pubmed = get_pubmed(message, n=3)
315
- msgs = [{"role":"system","content":"You are CardioLab AI. Expert in MHV MCL PIV TGT uPAD CKD FSI. Remember full conversation. Never invent URLs. "+KNOWHOW}]
316
- for item in history:
317
- if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
318
- msgs.append({"role":"user","content":message})
319
- resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=700)
320
- answer = resp.choices[0].message.content
321
- if pubmed: answer += chr(10)+chr(10)+"PUBMED LINKS:"+chr(10)+pubmed
322
- history.append({"role":"user","content":message})
323
- history.append({"role":"assistant","content":answer})
324
- return "", history
325
- except Exception as e:
326
- history.append({"role":"user","content":message})
327
- history.append({"role":"assistant","content":"Error: "+str(e)})
328
- return "", history
329
-
330
- def voice_chat(audio, history):
331
- if audio is None:
332
- history.append({"role":"assistant","content":"Please record your question first."})
333
- return history
334
- try:
335
- client = Groq(api_key=GROQ_KEY)
336
- with open(audio, "rb") as f:
337
- tx = client.audio.transcriptions.create(file=("audio.wav", f, "audio/wav"), model="whisper-large-v3")
338
- text = tx.text
339
- msgs = [{"role":"system","content":"You are CardioLab AI. "+KNOWHOW}]
340
- for item in history:
341
- if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
342
- msgs.append({"role":"user","content":text})
343
- resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=500)
344
- history.append({"role":"user","content":"[Voice] "+text})
345
- history.append({"role":"assistant","content":resp.choices[0].message.content})
346
- return history
347
- except Exception as e:
348
- history.append({"role":"assistant","content":"Voice error: "+str(e)})
349
- return history
350
 
351
  def analyze_upad_photo(image):
352
- if image is None:
353
- return None, "Please upload a uPAD photo first."
354
  try:
355
  img = Image.fromarray(image) if not isinstance(image, Image.Image) else image
356
- img_array = np.array(img)
357
- h, w = img_array.shape[:2]
358
- y1,y2 = int(h*0.35),int(h*0.65)
359
- x1,x2 = int(w*0.35),int(w*0.65)
360
- zone = img_array[y1:y2, x1:x2]
361
- R = float(np.mean(zone[:,:,0]))
362
- G = float(np.mean(zone[:,:,1]))
363
- B = float(np.mean(zone[:,:,2]))
364
- orange_score = R - B
365
- creatinine = max(0, round(0.018 * orange_score - 0.3, 2))
366
- if creatinine < 1.2: stage,action = "Normal","No CKD. Monitor annually."
367
- elif creatinine < 1.5: stage,action = "Borderline","Repeat in 3 months. Consult physician."
368
- elif creatinine < 3.0: stage,action = "Stage 2 CKD","Consult nephrologist. Confirm with Heska HT5."
369
- elif creatinine < 6.0: stage,action = "Stage 3-4 CKD","Advanced CKD. Immediate medical consultation."
370
- else: stage,action = "Stage 5 CKD","Kidney failure range. Emergency care needed."
371
  result_img = img.copy()
372
- import PIL.ImageDraw as ImageDraw
373
- draw = ImageDraw.Draw(result_img)
374
  draw.rectangle([x1,y1,x2,y2], outline=(0,255,0), width=3)
375
- result = ("uPAD PHOTO ANALYSIS"+chr(10)+"━"*27+chr(10)+
376
- "R: "+str(round(R,1))+" G: "+str(round(G,1))+" B: "+str(round(B,1))+chr(10)+
377
- "Orange Score: "+str(round(orange_score,1))+chr(10)+"━"*27+chr(10)+
378
  "CREATININE: "+str(creatinine)+" mg/dL"+chr(10)+
379
- "CKD STAGE: "+stage+chr(10)+""*27+chr(10)+
380
- "ACTION: "+action+chr(10)+"Confirm with: Heska Element HT5")
381
- return result_img, result
382
- except Exception as e:
383
- return None, "Error: "+str(e)
384
-
385
- def piv_tool(velocity, shear, hr):
386
- v = "HIGH - stenosis" if float(velocity)>2.0 else "NORMAL"
387
- s = "HIGH - thrombosis" if float(shear)>10 else "ELEVATED" if float(shear)>5 else "NORMAL"
388
- return "PIV: Velocity "+str(velocity)+" m/s - "+v+chr(10)+"Shear "+str(shear)+" Pa - "+s+chr(10)+"HR "+str(hr)+" bpm"
389
-
390
- def tgt_tool(tat,pf12,hemo,platelets,time):
391
- risk=sum([float(tat)>15,float(pf12)>2.0,float(hemo)>50,float(platelets)<150])
392
- r="HIGH RISK" if risk>=3 else "MODERATE" if risk>=2 else "LOW RISK"
393
- return "TGT: TAT "+str(tat)+" PF1.2 "+str(pf12)+chr(10)+"Hemo "+str(hemo)+" Plt "+str(platelets)+chr(10)+"Time "+str(time)+" min"+chr(10)+"RESULT: "+r
394
 
395
  def generate_image(prompt):
396
  if not prompt.strip(): return None,"Enter description.","";
397
- if not HF_TOKEN: return None,"Error: Add HF_TOKEN to Space secrets.","";
398
  try:
399
- enhanced=prompt
400
- description=""
401
  if GROQ_KEY:
402
  try:
403
  client=Groq(api_key=GROQ_KEY)
404
- msgs=[{"role":"system","content":"Biomedical visualization expert. Format: DESCRIPTION: [desc] PROMPT: [prompt]"},{"role":"user","content":"Create image for: "+prompt}]
405
- resp=client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=200)
 
406
  full=resp.choices[0].message.content
407
  if "DESCRIPTION:" in full and "PROMPT:" in full:
408
- description=full.split("DESCRIPTION:")[1].split("PROMPT:")[0].strip()
409
  enhanced=full.split("PROMPT:")[1].strip()
410
  except: pass
411
  headers={"Authorization":"Bearer "+HF_TOKEN,"Content-Type":"application/json"}
412
- payload={"inputs":enhanced,"parameters":{"num_inference_steps":8}}
413
- for url in ["https://router.huggingface.co/hf-inference/models/black-forest-labs/FLUX.1-schnell","https://router.huggingface.co/hf-inference/models/stabilityai/stable-diffusion-xl-base-1.0"]:
414
  try:
415
- r=requests.post(url,headers=headers,json=payload,timeout=60)
416
- if r.status_code==200:
417
- return Image.open(io.BytesIO(r.content)),"Generated!",description
418
  except: continue
419
- return None,"Models busy. Try again.",description
420
  except Exception as e: return None,"Error: "+str(e),""
421
 
422
- # ─── UI ───────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
423
  with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
424
- gr.HTML('''<div style="background:linear-gradient(135deg,#1a237e,#b71c1c);padding:25px;text-align:center;border-radius:12px 12px 0 0"><div style="font-size:2.8em;font-weight:900;color:#fff;letter-spacing:3px">CardioLab AI</div></div>''')
425
 
426
  with gr.Tabs():
427
-
428
  with gr.Tab("Chat"):
429
- chatbot = gr.Chatbot(label="", height=450)
430
  with gr.Row():
431
- msg_box = gr.Textbox(placeholder="Ask anything about CardioLab research...", label="", lines=2, scale=4)
432
- with gr.Column(scale=1, min_width=100):
433
  send_btn = gr.Button("Send", variant="primary")
434
  clear_btn = gr.Button("Clear", variant="secondary")
435
  send_btn.click(research_chat, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot])
@@ -437,8 +351,7 @@ with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
437
  clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg_box])
438
 
439
  with gr.Tab("Voice"):
440
- gr.Markdown("### Speak your question - Groq Whisper AI")
441
- voice_chatbot = gr.Chatbot(label="", height=350)
442
  audio_input = gr.Audio(sources=["microphone"], type="filepath", label="Record Question")
443
  with gr.Row():
444
  voice_btn = gr.Button("Ask by Voice", variant="primary")
@@ -454,64 +367,49 @@ with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
454
  search_btn.click(quick_search, inputs=search_input, outputs=search_output)
455
  search_input.submit(quick_search, inputs=search_input, outputs=search_output)
456
 
457
- with gr.Tab("PIV Data"):
458
- gr.Markdown("### Upload PIV CSV — AI generates charts and clinical interpretation")
459
- gr.Markdown("**Expected columns:** time/x, velocity, shear_stress (any names work — AI detects automatically)")
460
  with gr.Row():
461
  with gr.Column(scale=1):
462
- piv_file = gr.File(label="Upload PIV CSV File", file_types=[".csv"])
463
- piv_analyze_btn = gr.Button("Analyze PIV Data", variant="primary")
464
- gr.Markdown("**Sample CSV format:**")
465
- gr.Markdown("```\ntime,velocity,shear_stress\n0,0.5,2.1\n1,1.2,4.5\n2,1.8,7.2\n```")
466
  with gr.Column(scale=2):
467
- piv_chart = gr.Image(label="PIV Charts", type="pil", height=450)
468
- piv_ai_result = gr.Textbox(label="AI Clinical Analysis", lines=10)
469
- piv_analyze_btn.click(analyze_piv_csv, inputs=piv_file, outputs=[piv_chart, piv_ai_result])
470
 
471
- with gr.Tab("TGT Data"):
472
- gr.Markdown("### Upload TGT CSV — AI generates blood biomarker charts and thrombogenicity assessment")
473
- gr.Markdown("**Expected columns:** time, TAT, PF12, hemoglobin, platelets (any names work — AI detects automatically)")
474
  with gr.Row():
475
  with gr.Column(scale=1):
476
- tgt_file = gr.File(label="Upload TGT CSV File", file_types=[".csv"])
477
- tgt_analyze_btn = gr.Button("Analyze TGT Data", variant="primary")
478
- gr.Markdown("**Sample CSV format:**")
479
- gr.Markdown("```\ntime,TAT,PF12,hemoglobin,platelets\n0,5.2,1.1,12,210\n20,9.8,1.8,18,195\n40,14.2,2.4,35,178\n60,18.6,3.1,62,145\n```")
480
  with gr.Column(scale=2):
481
- tgt_chart = gr.Image(label="TGT Blood Analysis Charts", type="pil", height=450)
482
- tgt_ai_result = gr.Textbox(label="AI Thrombogenicity Assessment", lines=10)
483
- tgt_analyze_btn.click(analyze_tgt_csv, inputs=tgt_file, outputs=[tgt_chart, tgt_ai_result])
484
 
485
  with gr.Tab("uPAD Photo"):
486
  gr.Markdown("### Upload uPAD Photo — Instant CKD diagnosis from Jaffe reaction color")
487
- with gr.Row():
488
- with gr.Column(scale=1):
489
- photo_input = gr.Image(label="Upload uPAD Photo", type="numpy", height=300)
490
- analyze_btn = gr.Button("Analyze uPAD Photo", variant="primary")
491
- with gr.Column(scale=1):
492
- photo_result_img = gr.Image(label="Analyzed Image", type="pil", height=300)
493
- photo_result_text = gr.Textbox(label="CKD Result", lines=12)
494
- analyze_btn.click(analyze_upad_photo, inputs=photo_input, outputs=[photo_result_img, photo_result_text])
495
-
496
- with gr.Tab("uPAD Manual"):
497
  with gr.Row():
498
  with gr.Column():
499
- r=gr.Number(label="R value", value=210)
500
- g=gr.Number(label="G value", value=140)
501
- b=gr.Number(label="B value", value=80)
502
- out3=gr.Textbox(label="Result", lines=6)
503
- gr.Button("Analyze", variant="primary").click(
504
- lambda r,g,b: "Creatinine: "+str(max(0,round(0.02*(r-b)-0.5,2)))+" mg/dL"+chr(10)+("Normal" if max(0,round(0.02*(r-b)-0.5,2))<1.2 else "Borderline" if max(0,round(0.02*(r-b)-0.5,2))<1.5 else "CKD"),
505
- inputs=[r,g,b], outputs=out3)
506
 
507
  with gr.Tab("AI Image"):
508
  with gr.Row():
509
- img_prompt = gr.Textbox(placeholder="e.g. bileaflet heart valve | uPAD microfluidic | Arduino TGT circuit", label="Describe image", lines=3, scale=4)
510
  with gr.Column(scale=1):
511
  img_btn = gr.Button("Generate", variant="primary")
512
- img_status = gr.Textbox(label="Status", lines=2)
513
  img_desc = gr.Textbox(label="AI Description", lines=2, interactive=False)
514
- img_output = gr.Image(label="Generated Image", type="pil", height=400)
515
  img_btn.click(generate_image, inputs=img_prompt, outputs=[img_output, img_status, img_desc])
516
 
517
  with gr.Tab("PIV Manual"):
@@ -520,18 +418,29 @@ with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
520
  v=gr.Number(label="Max Velocity m/s", value=1.8)
521
  s=gr.Number(label="Wall Shear Stress Pa", value=6.5)
522
  h=gr.Number(label="Heart Rate bpm", value=72)
523
- piv_out=gr.Textbox(label="Result", lines=5)
524
- gr.Button("Analyze PIV", variant="primary").click(piv_tool,inputs=[v,s,h],outputs=piv_out)
525
 
526
  with gr.Tab("TGT Manual"):
527
  with gr.Row():
528
  with gr.Column():
529
  t1=gr.Number(label="TAT ng/mL", value=18)
530
- t2=gr.Number(label="PF1.2 nmol/L", value=2.5)
531
- t3=gr.Number(label="Free Hemoglobin mg/L", value=60)
532
- t4=gr.Number(label="Platelet Count", value=140)
533
- t5=gr.Number(label="Time minutes", value=40)
534
- out2=gr.Textbox(label="Result", lines=8)
535
- gr.Button("Analyze TGT", variant="primary").click(tgt_tool,inputs=[t1,t2,t3,t4,t5],outputs=out2)
 
 
 
 
 
 
 
 
 
 
 
536
 
537
  demo.launch()
 
12
  GROQ_KEY = os.environ.get("GROQ_API_KEY", "")
13
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
14
 
15
+ KNOWHOW = ("MCL: Sylgard 184 PDMS 10:1 ratio 48hr cure green laser PIV 70bpm 5L/min. "
 
16
  "TGT: Arduino Uno Stepper Motor 150mL blood sampled at 0 20 40 60min measures TAT PF1.2 hemolysis platelets. "
17
  "uPAD: Jaffe reaction creatinine plus picric acid gives orange-red color normal 0.6-1.2 mg/dL CKD above 1.5. "
18
+ "MHV: 27mm SJM Regent bileaflet also trileaflet monoleaflet pediatric.")
 
19
 
20
  CSS = """
21
  body, .gradio-container { background: #f0f4f8 !important; }
22
+ .tab-nav { background: #ffffff !important; border-bottom: 2px solid #e2e8f0 !important; padding: 4px 5px 0 5px !important; display: flex !important; flex-wrap: wrap !important; gap: 3px !important; overflow: visible !important; }
23
+ .tab-nav button { background: #f7fafc !important; color: #2d3748 !important; border: 1px solid #e2e8f0 !important; border-radius: 6px 6px 0 0 !important; padding: 8px 10px !important; font-weight: 600 !important; font-size: 0.8em !important; margin: 0 !important; white-space: nowrap !important; min-width: 0 !important; }
24
  .tab-nav button:hover { background: #ebf4ff !important; color: #1a237e !important; }
25
  .tab-nav button.selected { background: linear-gradient(135deg, #e63946, #c1121f) !important; color: #ffffff !important; font-weight: 700 !important; }
26
  button.primary { background: linear-gradient(135deg, #e63946 0%, #c1121f 100%) !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 700 !important; }
 
31
  label span { color: #2b6cb0 !important; font-weight: 600 !important; font-size: 0.85em !important; text-transform: uppercase !important; }
32
  """
33
 
34
+ def get_pubmed(query, n=5):
35
+ try:
36
+ r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
37
+ params={"db":"pubmed","term":query+" AND (mechanical heart valve OR microfluidic OR CKD OR thrombogenicity)","retmax":n,"retmode":"json","sort":"date"},timeout=10)
38
+ ids = r.json()["esearchresult"]["idlist"]
39
+ if not ids: return ""
40
+ return chr(10).join(["https://pubmed.ncbi.nlm.nih.gov/"+i for i in ids])
41
+ except: return ""
42
+
43
+ def quick_search(query):
44
+ if not query.strip(): return "Please enter a topic."
45
+ pubmed = get_pubmed(query, n=8)
46
+ try:
47
+ r = requests.get("https://api.semanticscholar.org/graph/v1/paper/search",
48
+ params={"query":query+" biomedical","limit":5,"fields":"title,year,url,citationCount"},timeout=10)
49
+ papers = r.json().get("data",[])
50
+ scholar = chr(10).join([p.get("title","")[:80]+" ("+str(p.get("year",""))+")"+chr(10)+" "+p.get("url","") for p in papers if p.get("url","")])
51
+ except: scholar = ""
52
+ return "PUBMED:"+chr(10)+pubmed+chr(10)+chr(10)+"SCHOLAR:"+chr(10)+scholar
53
+
54
+ def research_chat(message, history):
55
+ if not GROQ_KEY:
56
+ history.append({"role":"user","content":message})
57
+ history.append({"role":"assistant","content":"Error: Add GROQ_API_KEY to Space Settings."})
58
+ return "", history
59
+ try:
60
+ client = Groq(api_key=GROQ_KEY)
61
+ msgs = [{"role":"system","content":"You are CardioLab AI. Expert in MHV MCL PIV TGT uPAD CKD FSI. Never invent URLs. "+KNOWHOW}]
62
+ for item in history:
63
+ if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
64
+ msgs.append({"role":"user","content":message})
65
+ resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=700)
66
+ answer = resp.choices[0].message.content
67
+ pubmed = get_pubmed(message, n=3)
68
+ if pubmed: answer += chr(10)+chr(10)+"PUBMED:"+chr(10)+pubmed
69
+ history.append({"role":"user","content":message})
70
+ history.append({"role":"assistant","content":answer})
71
+ return "", history
72
+ except Exception as e:
73
+ history.append({"role":"user","content":message})
74
+ history.append({"role":"assistant","content":"Error: "+str(e)})
75
+ return "", history
76
+
77
+ def voice_chat(audio, history):
78
+ if audio is None:
79
+ history.append({"role":"assistant","content":"Please record your question first."})
80
+ return history
81
+ try:
82
+ client = Groq(api_key=GROQ_KEY)
83
+ with open(audio, "rb") as f:
84
+ tx = client.audio.transcriptions.create(file=("audio.wav", f, "audio/wav"), model="whisper-large-v3")
85
+ msgs = [{"role":"system","content":"You are CardioLab AI. "+KNOWHOW}]
86
+ for item in history:
87
+ if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
88
+ msgs.append({"role":"user","content":tx.text})
89
+ resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=500)
90
+ history.append({"role":"user","content":"[Voice] "+tx.text})
91
+ history.append({"role":"assistant","content":resp.choices[0].message.content})
92
+ return history
93
+ except Exception as e:
94
+ history.append({"role":"assistant","content":"Voice error: "+str(e)})
95
+ return history
96
+
97
  def analyze_piv_csv(file):
98
  if file is None:
99
+ return None, "Please upload a PIV CSV file first."
100
  try:
101
  df = pd.read_csv(file.name)
102
  cols = [c.lower().strip() for c in df.columns]
103
  df.columns = cols
104
+ num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
105
+ if not num_cols:
106
+ return None, "No numeric columns found. Check your CSV file."
107
 
108
  fig, axes = plt.subplots(2, 2, figsize=(14, 10))
109
  fig.patch.set_facecolor("#0d1b3e")
110
+ fig.suptitle("PIV Data Analysis — SJSU CardioLab MCL", color="white", fontsize=16, fontweight="bold")
111
+
112
+ def style_ax(ax, title, ylabel):
113
+ ax.set_facecolor("#1a2744")
114
+ ax.set_title(title, color="white", fontweight="bold")
115
+ ax.set_ylabel(ylabel, color="#a8b2d8")
116
+ ax.tick_params(colors="#a8b2d8")
117
+ ax.grid(True, alpha=0.2, color="#2d4a8a")
118
+ for spine in ["top","right"]: ax.spines[spine].set_visible(False)
119
+ for spine in ["bottom","left"]: ax.spines[spine].set_color("#2d4a8a")
120
+
121
+ x = range(len(df))
122
+ vel_col = next((c for c in cols if any(k in c for k in ["vel","speed","u","v_mag"])), num_cols[0] if num_cols else None)
123
+ shear_col = next((c for c in cols if any(k in c for k in ["shear","stress","tau","wss"])), num_cols[1] if len(num_cols)>1 else None)
124
+
125
+ # Plot 1 - Velocity
126
+ ax1 = axes[0,0]
127
+ if vel_col:
128
+ ax1.plot(df[vel_col], color="#e63946", linewidth=2.5, marker="o", markersize=4)
129
+ ax1.axhline(y=2.0, color="#ffd700", linestyle="--", linewidth=1.5, label="Risk (2.0 m/s)")
130
+ ax1.fill_between(x, df[vel_col], alpha=0.2, color="#e63946")
131
+ ax1.legend(fontsize=8, labelcolor="white", facecolor="#1a2744")
132
+ style_ax(ax1, "Velocity Profile", "Velocity (m/s)")
133
+
134
+ # Plot 2 - Shear
135
+ ax2 = axes[0,1]
 
 
 
 
136
  if shear_col:
137
+ ax2.plot(df[shear_col], color="#4361ee", linewidth=2.5, marker="s", markersize=4)
138
+ ax2.axhline(y=5, color="#ffd700", linestyle="--", linewidth=1.5, label="Caution (5 Pa)")
139
+ ax2.axhline(y=10, color="#e63946", linestyle="--", linewidth=1.5, label="High risk (10 Pa)")
140
+ ax2.fill_between(x, df[shear_col], alpha=0.2, color="#4361ee")
 
141
  ax2.legend(fontsize=8, labelcolor="white", facecolor="#1a2744")
142
+ elif len(num_cols)>1:
143
+ ax2.plot(df[num_cols[1]], color="#4361ee", linewidth=2.5)
144
+ style_ax(ax2, "Shear Stress", "Shear Stress (Pa)")
145
+
146
+ # Plot 3 - Distribution
147
+ ax3 = axes[1,0]
148
+ if vel_col:
149
+ ax3.hist(df[vel_col].dropna(), bins=20, color="#2ecc71", alpha=0.8, edgecolor="#0d1b3e")
150
+ style_ax(ax3, "Velocity Distribution", "Count")
151
+ ax3.set_xlabel("Value", color="#a8b2d8")
152
+
153
+ # Plot 4 - Stats
154
+ ax4 = axes[1,1]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  ax4.set_facecolor("#1a2744")
156
  ax4.axis("off")
157
+ stats = ""
158
+ risk = []
159
+ for col in num_cols[:3]:
160
+ mn = df[col].mean()
161
+ mx = df[col].max()
162
+ stats += col[:12]+":"+chr(10)+" Mean: "+str(round(mn,3))+chr(10)+" Max: "+str(round(mx,3))+chr(10)+chr(10)
163
+ if "vel" in col and mx > 2.0: risk.append("HIGH VELOCITY: stenosis risk")
164
+ if "shear" in col and mx > 10: risk.append("HIGH SHEAR: thrombosis risk")
165
+ if risk: stats += "RISK FLAGS:"+chr(10)+" "+chr(10)+" ".join(risk)
166
+ ax4.text(0.05, 0.95, "SUMMARY STATS"+chr(10)+"━"*18+chr(10)+stats, transform=ax4.transAxes,
167
+ color="white", fontsize=9, va="top", fontfamily="monospace",
168
+ bbox=dict(boxstyle="round", facecolor="#0d1b3e", edgecolor="#4361ee"))
 
 
 
 
 
 
 
 
 
 
 
169
 
170
  plt.tight_layout()
171
  buf = io.BytesIO()
 
174
  img = Image.open(buf)
175
  plt.close()
176
 
177
+ ai_text = ""
 
178
  if GROQ_KEY:
179
  try:
180
  client = Groq(api_key=GROQ_KEY)
181
+ msgs = [{"role":"system","content":"You are a PIV expert for SJSU CardioLab. Analyze PIV statistics and give clinical interpretation about velocity, shear stress, stenosis and thrombosis risk."}]
182
+ msgs.append({"role":"user","content":"PIV data stats from 27mm SJM Regent MHV at 70bpm 5L/min:"+chr(10)+df.describe().to_string()[:800]})
183
+ resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=350)
184
+ ai_text = chr(10)+""*25+chr(10)+"AI ANALYSIS:"+chr(10)+resp.choices[0].message.content
 
185
  except: pass
186
 
187
+ return img, "PIV CSV LOADED: "+str(len(df))+" rows, "+str(len(df.columns))+" columns"+chr(10)+"Columns: "+", ".join(df.columns.tolist())+ai_text
 
 
 
 
 
188
  except Exception as e:
189
+ return None, "Error: "+str(e)
190
 
 
191
  def analyze_tgt_csv(file):
192
  if file is None:
193
+ return None, "Please upload a TGT CSV file first."
194
  try:
195
  df = pd.read_csv(file.name)
196
  cols = [c.lower().strip() for c in df.columns]
197
  df.columns = cols
198
+ num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
199
+ if not num_cols:
200
+ return None, "No numeric columns found."
201
 
202
  fig, axes = plt.subplots(2, 2, figsize=(14, 10))
203
  fig.patch.set_facecolor("#0d1b3e")
204
+ fig.suptitle("TGT Blood Analysis — SJSU CardioLab", color="white", fontsize=16, fontweight="bold")
205
 
 
206
  time_col = next((c for c in cols if "time" in c or "min" in c), None)
207
+ tat_col = next((c for c in cols if "tat" in c or "thrombin" in c), num_cols[0] if num_cols else None)
208
+ pf_col = next((c for c in cols if "pf" in c or "prothrombin" in c), num_cols[1] if len(num_cols)>1 else None)
209
+ hemo_col = next((c for c in cols if "hemo" in c or "hgb" in c), num_cols[2] if len(num_cols)>2 else None)
210
+ plt_col = next((c for c in cols if "platelet" in c or "plt" in c), num_cols[3] if len(num_cols)>3 else None)
211
+ x = df[time_col] if time_col else range(len(df))
212
+ xl = time_col if time_col else "Sample"
213
+
214
+ def style_ax(ax, title, ylabel):
 
 
 
 
215
  ax.set_facecolor("#1a2744")
216
  ax.set_title(title, color="white", fontweight="bold")
217
+ ax.set_ylabel(ylabel, color="#a8b2d8")
218
+ ax.set_xlabel(xl, color="#a8b2d8")
219
  ax.tick_params(colors="#a8b2d8")
 
220
  ax.grid(True, alpha=0.2, color="#2d4a8a")
221
+ for spine in ["top","right"]: ax.spines[spine].set_visible(False)
222
+ for spine in ["bottom","left"]: ax.spines[spine].set_color("#2d4a8a")
223
+
224
+ ax1 = axes[0,0]
225
+ if tat_col:
226
+ ax1.plot(x, df[tat_col], color="#e63946", linewidth=2.5, marker="o", markersize=6)
227
+ ax1.axhline(y=8, color="#ffd700", linestyle="--", linewidth=1.5, label="Normal (8 ng/mL)")
228
+ ax1.fill_between(x, df[tat_col], alpha=0.3, color="#e63946")
 
 
 
 
 
229
  ax1.legend(fontsize=8, labelcolor="white", facecolor="#1a2744")
230
+ style_ax(ax1, "TAT (Thrombin-Antithrombin)", "ng/mL")
231
+
232
+ ax2 = axes[0,1]
233
+ if pf_col:
234
+ ax2.plot(x, df[pf_col], color="#4361ee", linewidth=2.5, marker="s", markersize=6)
235
+ ax2.axhline(y=2.0, color="#ffd700", linestyle="--", linewidth=1.5, label="Normal (2.0)")
236
+ ax2.fill_between(x, df[pf_col], alpha=0.3, color="#4361ee")
 
 
 
237
  ax2.legend(fontsize=8, labelcolor="white", facecolor="#1a2744")
238
+ style_ax(ax2, "PF1.2 (Prothrombin Fragment)", "nmol/L")
239
+
240
+ ax3 = axes[1,0]
241
+ if hemo_col:
242
+ ax3.bar(range(len(df)), df[hemo_col], color="#2ecc71", alpha=0.85, edgecolor="#0d1b3e")
243
+ ax3.axhline(y=20, color="#ffd700", linestyle="--", linewidth=1.5, label="Normal (20 mg/L)")
 
 
 
244
  ax3.legend(fontsize=8, labelcolor="white", facecolor="#1a2744")
245
+ style_ax(ax3, "Free Hemoglobin (Hemolysis)", "mg/L")
246
+
247
+ ax4 = axes[1,1]
248
+ if plt_col:
249
+ ax4.plot(x, df[plt_col], color="#e67e22", linewidth=2.5, marker="^", markersize=6)
250
+ ax4.axhline(y=150, color="#ffd700", linestyle="--", linewidth=1.5, label="Normal min (150)")
251
+ ax4.fill_between(x, df[plt_col], 150, where=df[plt_col]<150, alpha=0.3, color="#e63946", label="Below normal")
 
 
 
252
  ax4.legend(fontsize=8, labelcolor="white", facecolor="#1a2744")
253
+ style_ax(ax4, "Platelet Count", "10³/μL")
254
 
255
  plt.tight_layout()
256
  buf = io.BytesIO()
 
259
  img = Image.open(buf)
260
  plt.close()
261
 
262
+ ai_text = ""
 
263
  if GROQ_KEY:
264
  try:
265
  client = Groq(api_key=GROQ_KEY)
266
+ msgs = [{"role":"system","content":"You are a hematology expert for SJSU CardioLab. Analyze TGT blood biomarker data. Give thrombogenicity risk: LOW MODERATE or HIGH. Normal: TAT<8, PF1.2<2.0, Hemo<20, Platelets>150."}]
267
+ msgs.append({"role":"user","content":"TGT data from 27mm SJM Regent MHV:"+chr(10)+df.describe().to_string()[:800]})
268
+ resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=350)
269
+ ai_text = chr(10)+""*25+chr(10)+"AI ASSESSMENT:"+chr(10)+resp.choices[0].message.content
 
270
  except: pass
271
 
272
+ return img, "TGT CSV LOADED: "+str(len(df))+" rows"+chr(10)+"Columns: "+", ".join(df.columns.tolist())+ai_text
 
 
 
 
 
273
  except Exception as e:
274
+ return None, "Error: "+str(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
 
276
  def analyze_upad_photo(image):
277
+ if image is None: return None, "Upload a uPAD photo first."
 
278
  try:
279
  img = Image.fromarray(image) if not isinstance(image, Image.Image) else image
280
+ arr = np.array(img)
281
+ h,w = arr.shape[:2]
282
+ y1,y2,x1,x2 = int(h*0.35),int(h*0.65),int(w*0.35),int(w*0.65)
283
+ zone = arr[y1:y2,x1:x2]
284
+ R,G,B = float(np.mean(zone[:,:,0])),float(np.mean(zone[:,:,1])),float(np.mean(zone[:,:,2]))
285
+ creatinine = max(0, round(0.018*(R-B)-0.3, 2))
286
+ if creatinine < 1.2: stage,action = "Normal","Monitor annually."
287
+ elif creatinine < 1.5: stage,action = "Borderline","Repeat in 3 months."
288
+ elif creatinine < 3.0: stage,action = "Stage 2 CKD","Consult nephrologist."
289
+ elif creatinine < 6.0: stage,action = "Stage 3-4 CKD","Immediate consultation."
290
+ else: stage,action = "Stage 5 CKD","Emergency care needed."
 
 
 
 
291
  result_img = img.copy()
292
+ import PIL.ImageDraw as D
293
+ draw = D.Draw(result_img)
294
  draw.rectangle([x1,y1,x2,y2], outline=(0,255,0), width=3)
295
+ return result_img, ("uPAD ANALYSIS"+chr(10)+"━"*22+chr(10)+
296
+ "R:"+str(round(R,1))+" G:"+str(round(G,1))+" B:"+str(round(B,1))+chr(10)+
297
+ "Orange Score: "+str(round(R-B,1))+chr(10)+"━"*22+chr(10)+
298
  "CREATININE: "+str(creatinine)+" mg/dL"+chr(10)+
299
+ "CKD STAGE: "+stage+chr(10)+"ACTION: "+action+chr(10)+
300
+ "Confirm: Heska Element HT5")
301
+ except Exception as e: return None, "Error: "+str(e)
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
  def generate_image(prompt):
304
  if not prompt.strip(): return None,"Enter description.","";
305
+ if not HF_TOKEN: return None,"Add HF_TOKEN to Space secrets.","";
306
  try:
307
+ enhanced,desc = prompt,""
 
308
  if GROQ_KEY:
309
  try:
310
  client=Groq(api_key=GROQ_KEY)
311
+ resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
312
+ messages=[{"role":"system","content":"Format: DESCRIPTION: [2 sentences] PROMPT: [detailed image prompt]"},
313
+ {"role":"user","content":"Biomedical image for CardioLab: "+prompt}],max_tokens=200)
314
  full=resp.choices[0].message.content
315
  if "DESCRIPTION:" in full and "PROMPT:" in full:
316
+ desc=full.split("DESCRIPTION:")[1].split("PROMPT:")[0].strip()
317
  enhanced=full.split("PROMPT:")[1].strip()
318
  except: pass
319
  headers={"Authorization":"Bearer "+HF_TOKEN,"Content-Type":"application/json"}
320
+ for url in ["https://router.huggingface.co/hf-inference/models/black-forest-labs/FLUX.1-schnell",
321
+ "https://router.huggingface.co/hf-inference/models/stabilityai/stable-diffusion-xl-base-1.0"]:
322
  try:
323
+ r=requests.post(url,headers=headers,json={"inputs":enhanced,"parameters":{"num_inference_steps":8}},timeout=60)
324
+ if r.status_code==200: return Image.open(io.BytesIO(r.content)),"Generated!",desc
 
325
  except: continue
326
+ return None,"Models busy. Try again.",desc
327
  except Exception as e: return None,"Error: "+str(e),""
328
 
329
+ def piv_manual(v,s,h):
330
+ vr="HIGH-stenosis" if float(v)>2.0 else "NORMAL"
331
+ sr="HIGH-thrombosis" if float(s)>10 else "ELEVATED" if float(s)>5 else "NORMAL"
332
+ return "Velocity: "+str(v)+" - "+vr+chr(10)+"Shear: "+str(s)+" - "+sr+chr(10)+"HR: "+str(h)+" bpm"
333
+
334
+ def tgt_manual(t,p,h,pl,tm):
335
+ risk=sum([float(t)>15,float(p)>2.0,float(h)>50,float(pl)<150])
336
+ return "TAT:"+str(t)+" PF1.2:"+str(p)+chr(10)+"Hemo:"+str(h)+" Plt:"+str(pl)+chr(10)+"Time:"+str(tm)+"min"+chr(10)+"RESULT: "+("HIGH RISK" if risk>=3 else "MODERATE" if risk>=2 else "LOW RISK")
337
+
338
  with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
339
+ gr.HTML('''<div style="background:linear-gradient(135deg,#1a237e,#b71c1c);padding:20px;text-align:center;border-radius:12px 12px 0 0"><div style="font-size:2.5em;font-weight:900;color:#fff;letter-spacing:3px">CardioLab AI</div></div>''')
340
 
341
  with gr.Tabs():
 
342
  with gr.Tab("Chat"):
343
+ chatbot = gr.Chatbot(label="", height=420)
344
  with gr.Row():
345
+ msg_box = gr.Textbox(placeholder="Ask about CardioLab research...", label="", lines=2, scale=4)
346
+ with gr.Column(scale=1, min_width=80):
347
  send_btn = gr.Button("Send", variant="primary")
348
  clear_btn = gr.Button("Clear", variant="secondary")
349
  send_btn.click(research_chat, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot])
 
351
  clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg_box])
352
 
353
  with gr.Tab("Voice"):
354
+ voice_chatbot = gr.Chatbot(label="", height=320)
 
355
  audio_input = gr.Audio(sources=["microphone"], type="filepath", label="Record Question")
356
  with gr.Row():
357
  voice_btn = gr.Button("Ask by Voice", variant="primary")
 
367
  search_btn.click(quick_search, inputs=search_input, outputs=search_output)
368
  search_input.submit(quick_search, inputs=search_input, outputs=search_output)
369
 
370
+ with gr.Tab("PIV CSV"):
371
+ gr.Markdown("### Upload PIV CSV file — AI generates 4 charts + clinical analysis")
372
+ gr.Markdown("CSV columns: **time, velocity, shear_stress** (any column names work)")
373
  with gr.Row():
374
  with gr.Column(scale=1):
375
+ piv_file = gr.File(label="CLICK HERE TO UPLOAD PIV CSV", file_types=[".csv"])
376
+ piv_btn = gr.Button("Analyze PIV Data", variant="primary")
377
+ piv_result = gr.Textbox(label="AI Analysis", lines=10)
 
378
  with gr.Column(scale=2):
379
+ piv_chart = gr.Image(label="PIV Charts", type="pil")
380
+ piv_btn.click(analyze_piv_csv, inputs=piv_file, outputs=[piv_chart, piv_result])
 
381
 
382
+ with gr.Tab("TGT CSV"):
383
+ gr.Markdown("### Upload TGT CSV file — AI generates blood biomarker charts + thrombogenicity assessment")
384
+ gr.Markdown("CSV columns: **time, TAT, PF12, hemoglobin, platelets** (any column names work)")
385
  with gr.Row():
386
  with gr.Column(scale=1):
387
+ tgt_file = gr.File(label="CLICK HERE TO UPLOAD TGT CSV", file_types=[".csv"])
388
+ tgt_btn = gr.Button("Analyze TGT Data", variant="primary")
389
+ tgt_result = gr.Textbox(label="AI Assessment", lines=10)
 
390
  with gr.Column(scale=2):
391
+ tgt_chart = gr.Image(label="TGT Blood Charts", type="pil")
392
+ tgt_btn.click(analyze_tgt_csv, inputs=tgt_file, outputs=[tgt_chart, tgt_result])
 
393
 
394
  with gr.Tab("uPAD Photo"):
395
  gr.Markdown("### Upload uPAD Photo — Instant CKD diagnosis from Jaffe reaction color")
 
 
 
 
 
 
 
 
 
 
396
  with gr.Row():
397
  with gr.Column():
398
+ photo_input = gr.Image(label="Upload uPAD Photo", type="numpy", height=280)
399
+ analyze_btn = gr.Button("Analyze uPAD", variant="primary")
400
+ with gr.Column():
401
+ photo_img = gr.Image(label="Detection Zone (green box)", type="pil", height=280)
402
+ photo_text = gr.Textbox(label="CKD Result", lines=10)
403
+ analyze_btn.click(analyze_upad_photo, inputs=photo_input, outputs=[photo_img, photo_text])
 
404
 
405
  with gr.Tab("AI Image"):
406
  with gr.Row():
407
+ img_prompt = gr.Textbox(placeholder="e.g. bileaflet heart valve | uPAD device | Arduino TGT circuit", label="Describe image", lines=2, scale=4)
408
  with gr.Column(scale=1):
409
  img_btn = gr.Button("Generate", variant="primary")
410
+ img_status = gr.Textbox(label="Status", lines=1)
411
  img_desc = gr.Textbox(label="AI Description", lines=2, interactive=False)
412
+ img_output = gr.Image(label="Generated Image", type="pil", height=380)
413
  img_btn.click(generate_image, inputs=img_prompt, outputs=[img_output, img_status, img_desc])
414
 
415
  with gr.Tab("PIV Manual"):
 
418
  v=gr.Number(label="Max Velocity m/s", value=1.8)
419
  s=gr.Number(label="Wall Shear Stress Pa", value=6.5)
420
  h=gr.Number(label="Heart Rate bpm", value=72)
421
+ piv_out=gr.Textbox(label="Result", lines=4)
422
+ gr.Button("Analyze", variant="primary").click(piv_manual,inputs=[v,s,h],outputs=piv_out)
423
 
424
  with gr.Tab("TGT Manual"):
425
  with gr.Row():
426
  with gr.Column():
427
  t1=gr.Number(label="TAT ng/mL", value=18)
428
+ t2=gr.Number(label="PF1.2", value=2.5)
429
+ t3=gr.Number(label="Hemoglobin mg/L", value=60)
430
+ t4=gr.Number(label="Platelets", value=140)
431
+ t5=gr.Number(label="Time min", value=40)
432
+ out2=gr.Textbox(label="Result", lines=6)
433
+ gr.Button("Analyze", variant="primary").click(tgt_manual,inputs=[t1,t2,t3,t4,t5],outputs=out2)
434
+
435
+ with gr.Tab("uPAD Manual"):
436
+ with gr.Row():
437
+ with gr.Column():
438
+ r=gr.Number(label="R value", value=210)
439
+ g=gr.Number(label="G value", value=140)
440
+ b=gr.Number(label="B value", value=80)
441
+ out3=gr.Textbox(label="Result", lines=4)
442
+ gr.Button("Analyze", variant="primary").click(
443
+ lambda r,g,b: "Creatinine: "+str(max(0,round(0.02*(r-b)-0.5,2)))+" mg/dL"+chr(10)+("Normal" if max(0,round(0.02*(r-b)-0.5,2))<1.2 else "Borderline" if max(0,round(0.02*(r-b)-0.5,2))<1.5 else "CKD Stage 2+" ),
444
+ inputs=[r,g,b], outputs=out3)
445
 
446
  demo.launch()