Spaces:
Running
Running
Upload app.py with huggingface_hub
Browse files
app.py
CHANGED
|
@@ -95,9 +95,9 @@ def voice_chat(audio, history):
|
|
| 95 |
history.append({"role":"assistant","content":"Voice error: "+str(e)})
|
| 96 |
return history
|
| 97 |
|
| 98 |
-
def analyze_piv_csv(file):
|
| 99 |
if file is None:
|
| 100 |
-
return None, "Please upload a PIV CSV file first."
|
| 101 |
try:
|
| 102 |
df = pd.read_csv(file.name)
|
| 103 |
cols = [c.lower().strip() for c in df.columns]
|
|
@@ -214,11 +214,103 @@ def analyze_piv_csv(file):
|
|
| 214 |
ai_text = chr(10)+"━"*25+chr(10)+"AI ANALYSIS:"+chr(10)+resp.choices[0].message.content
|
| 215 |
except: pass
|
| 216 |
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
except Exception as e:
|
| 219 |
-
return None, "Error: "+str(e)
|
| 220 |
|
| 221 |
-
def analyze_tgt_csv(file):
|
| 222 |
if file is None:
|
| 223 |
return None, "Please upload a TGT CSV file first."
|
| 224 |
try:
|
|
@@ -299,9 +391,59 @@ def analyze_tgt_csv(file):
|
|
| 299 |
ai_text = chr(10)+"━"*25+chr(10)+"AI ASSESSMENT:"+chr(10)+resp.choices[0].message.content
|
| 300 |
except: pass
|
| 301 |
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
except Exception as e:
|
| 304 |
-
return None, "Error: "+str(e)
|
| 305 |
|
| 306 |
def analyze_upad_photo(image):
|
| 307 |
if image is None: return None, "Upload a uPAD photo first."
|
|
@@ -398,28 +540,38 @@ with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
|
|
| 398 |
search_input.submit(quick_search, inputs=search_input, outputs=search_output)
|
| 399 |
|
| 400 |
with gr.Tab("PIV CSV"):
|
| 401 |
-
gr.Markdown("### Upload PIV CSV file — AI generates
|
| 402 |
gr.Markdown("CSV columns: **time, velocity, shear_stress** (any column names work)")
|
| 403 |
with gr.Row():
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
|
| 412 |
with gr.Tab("TGT CSV"):
|
| 413 |
gr.Markdown("### Upload TGT CSV file — AI generates blood biomarker charts + thrombogenicity assessment")
|
| 414 |
gr.Markdown("CSV columns: **time, TAT, PF12, hemoglobin, platelets** (any column names work)")
|
| 415 |
with gr.Row():
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
|
| 424 |
with gr.Tab("uPAD Photo"):
|
| 425 |
gr.Markdown("### Upload uPAD Photo — Instant CKD diagnosis from Jaffe reaction color")
|
|
|
|
| 95 |
history.append({"role":"assistant","content":"Voice error: "+str(e)})
|
| 96 |
return history
|
| 97 |
|
| 98 |
+
def analyze_piv_csv(file, theme="White"):
|
| 99 |
if file is None:
|
| 100 |
+
return None, None, None, None, "Please upload a PIV CSV file first."
|
| 101 |
try:
|
| 102 |
df = pd.read_csv(file.name)
|
| 103 |
cols = [c.lower().strip() for c in df.columns]
|
|
|
|
| 214 |
ai_text = chr(10)+"━"*25+chr(10)+"AI ANALYSIS:"+chr(10)+resp.choices[0].message.content
|
| 215 |
except: pass
|
| 216 |
|
| 217 |
+
# Generate 4 separate charts
|
| 218 |
+
bg = "#ffffff" if theme=="White" else "#0a1628"
|
| 219 |
+
fg = "#1a202c" if theme=="White" else "white"
|
| 220 |
+
grid_color = "#e2e8f0" if theme=="White" else "#2d4a8a"
|
| 221 |
+
ax_color = "#4a5568" if theme=="White" else "#a8b2d8"
|
| 222 |
+
plot_bg = "#f7fafc" if theme=="White" else "#132340"
|
| 223 |
+
|
| 224 |
+
def make_single_chart(plot_fn, title):
|
| 225 |
+
fig2, ax = plt.subplots(figsize=(8, 5))
|
| 226 |
+
fig2.patch.set_facecolor(bg)
|
| 227 |
+
ax.set_facecolor(plot_bg)
|
| 228 |
+
plot_fn(ax)
|
| 229 |
+
ax.set_title(title, color=fg, fontweight="bold", fontsize=14, pad=10)
|
| 230 |
+
ax.tick_params(colors=ax_color, labelsize=11)
|
| 231 |
+
ax.grid(True, alpha=0.3, color=grid_color, linestyle="--")
|
| 232 |
+
for spine in ["top","right"]: ax.spines[spine].set_visible(False)
|
| 233 |
+
for spine in ["bottom","left"]: ax.spines[spine].set_color(grid_color)
|
| 234 |
+
plt.tight_layout()
|
| 235 |
+
buf2 = io.BytesIO()
|
| 236 |
+
plt.savefig(buf2, format="png", facecolor=bg, bbox_inches="tight", dpi=130)
|
| 237 |
+
buf2.seek(0)
|
| 238 |
+
result = Image.open(buf2).copy()
|
| 239 |
+
plt.close()
|
| 240 |
+
return result
|
| 241 |
+
|
| 242 |
+
x = np.arange(len(df))
|
| 243 |
+
vel_col2 = next((c for c in cols if any(k in c for k in ["vel","speed","v_mag"])), num_cols[0] if num_cols else None)
|
| 244 |
+
shear_col2 = 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)
|
| 245 |
+
time_col2 = next((c for c in cols if "time" in c or "frame" in c), None)
|
| 246 |
+
x_vals2 = df[time_col2] if time_col2 else x
|
| 247 |
+
|
| 248 |
+
def plot_velocity(ax):
|
| 249 |
+
if vel_col2:
|
| 250 |
+
ax.plot(x_vals2, df[vel_col2], color="#e63946", linewidth=2.5, marker="o", markersize=5)
|
| 251 |
+
ax.fill_between(x_vals2, df[vel_col2], alpha=0.2, color="#e63946")
|
| 252 |
+
ax.axhline(y=2.0, color="#f59e0b", linestyle="--", linewidth=2, label="Risk: 2.0 m/s")
|
| 253 |
+
ax.set_ylabel("Velocity (m/s)", color=ax_color, fontsize=12)
|
| 254 |
+
ax.set_xlabel(time_col2 or "Sample", color=ax_color, fontsize=12)
|
| 255 |
+
ax.legend(fontsize=10, labelcolor=fg, facecolor=plot_bg)
|
| 256 |
+
|
| 257 |
+
def plot_shear(ax):
|
| 258 |
+
if shear_col2:
|
| 259 |
+
xp = x_vals2.values if time_col2 else x
|
| 260 |
+
ax.plot(xp, df[shear_col2], color="#4361ee", linewidth=2.5, marker="s", markersize=5)
|
| 261 |
+
ax.fill_between(xp, df[shear_col2], alpha=0.2, color="#4361ee")
|
| 262 |
+
ax.axhline(y=5, color="#f59e0b", linestyle="--", linewidth=2, label="Caution: 5 Pa")
|
| 263 |
+
ax.axhline(y=10, color="#e63946", linestyle="--", linewidth=2, label="High risk: 10 Pa")
|
| 264 |
+
ax.set_ylabel("Shear Stress (Pa)", color=ax_color, fontsize=12)
|
| 265 |
+
ax.set_xlabel(time_col2 or "Sample", color=ax_color, fontsize=12)
|
| 266 |
+
ax.legend(fontsize=10, labelcolor=fg, facecolor=plot_bg)
|
| 267 |
+
|
| 268 |
+
def plot_scatter(ax):
|
| 269 |
+
if vel_col2 and shear_col2:
|
| 270 |
+
sc = ax.scatter(df[vel_col2], df[shear_col2], c=x, cmap="RdYlGn_r", s=100, edgecolors=fg, linewidth=0.5, zorder=5)
|
| 271 |
+
cb = plt.colorbar(sc, ax=ax, label="Time")
|
| 272 |
+
cb.ax.yaxis.label.set_color(fg)
|
| 273 |
+
cb.ax.tick_params(colors=ax_color)
|
| 274 |
+
ax.axvline(x=2.0, color="#f59e0b", linestyle="--", linewidth=2, label="Vel risk: 2.0")
|
| 275 |
+
ax.axhline(y=10, color="#e63946", linestyle="--", linewidth=2, label="Shear risk: 10")
|
| 276 |
+
ax.set_xlabel("Velocity (m/s)", color=ax_color, fontsize=12)
|
| 277 |
+
ax.set_ylabel("Shear Stress (Pa)", color=ax_color, fontsize=12)
|
| 278 |
+
ax.legend(fontsize=10, labelcolor=fg, facecolor=plot_bg)
|
| 279 |
+
|
| 280 |
+
def plot_summary(ax):
|
| 281 |
+
ax.axis("off")
|
| 282 |
+
risk = []
|
| 283 |
+
stats = "CLINICAL SUMMARY"+chr(10)+"━"*22+chr(10)+chr(10)
|
| 284 |
+
for col in num_cols[:3]:
|
| 285 |
+
mn = round(df[col].mean(),3)
|
| 286 |
+
mx = round(df[col].max(),3)
|
| 287 |
+
mn_v = round(df[col].min(),3)
|
| 288 |
+
stats += col[:14]+":"+chr(10)+" Mean: "+str(mn)+chr(10)+" Max: "+str(mx)+chr(10)+" Min: "+str(mn_v)+chr(10)+chr(10)
|
| 289 |
+
if "vel" in col and mx > 2.0: risk.append("HIGH VELOCITY (>2.0 m/s)")
|
| 290 |
+
if "shear" in col and mx > 10: risk.append("HIGH SHEAR (>10 Pa)")
|
| 291 |
+
stats += "━"*22+chr(10)
|
| 292 |
+
if risk:
|
| 293 |
+
stats += "RISK FLAGS:"+chr(10)
|
| 294 |
+
for r in risk: stats += " ⚠ "+r+chr(10)
|
| 295 |
+
stats += chr(10)+"OVERALL: HIGH RISK"
|
| 296 |
+
border_color = "#e63946"
|
| 297 |
+
else:
|
| 298 |
+
stats += "STATUS: All values normal"+chr(10)+"OVERALL: LOW RISK"
|
| 299 |
+
border_color = "#2ecc71"
|
| 300 |
+
ax.text(0.05, 0.97, stats, transform=ax.transAxes, color=fg, fontsize=11,
|
| 301 |
+
va="top", fontfamily="monospace",
|
| 302 |
+
bbox=dict(boxstyle="round,pad=0.8", facecolor=plot_bg, edgecolor=border_color, linewidth=2.5))
|
| 303 |
+
|
| 304 |
+
img1 = make_single_chart(plot_velocity, "Velocity Profile")
|
| 305 |
+
img2 = make_single_chart(plot_shear, "Wall Shear Stress")
|
| 306 |
+
img3 = make_single_chart(plot_scatter, "Velocity vs Shear Stress")
|
| 307 |
+
img4 = make_single_chart(plot_summary, "Clinical Summary")
|
| 308 |
+
|
| 309 |
+
return img1, img2, img3, img4, "PIV CSV LOADED: "+str(len(df))+" rows | Columns: "+", ".join(df.columns.tolist())+ai_text
|
| 310 |
except Exception as e:
|
| 311 |
+
return None, None, None, None, "Error: "+str(e)
|
| 312 |
|
| 313 |
+
def analyze_tgt_csv(file, theme="White"):
|
| 314 |
if file is None:
|
| 315 |
return None, "Please upload a TGT CSV file first."
|
| 316 |
try:
|
|
|
|
| 391 |
ai_text = chr(10)+"━"*25+chr(10)+"AI ASSESSMENT:"+chr(10)+resp.choices[0].message.content
|
| 392 |
except: pass
|
| 393 |
|
| 394 |
+
bg = "#ffffff" if theme=="White" else "#0a1628"
|
| 395 |
+
fg = "#1a202c" if theme=="White" else "white"
|
| 396 |
+
grid_color = "#e2e8f0" if theme=="White" else "#2d4a8a"
|
| 397 |
+
ax_color = "#4a5568" if theme=="White" else "#a8b2d8"
|
| 398 |
+
plot_bg = "#f7fafc" if theme=="White" else "#132340"
|
| 399 |
+
|
| 400 |
+
def make_tgt_chart(data_col, color, ylabel, limit, limit_label, title, chart_type="line"):
|
| 401 |
+
fig2, ax = plt.subplots(figsize=(8, 5))
|
| 402 |
+
fig2.patch.set_facecolor(bg)
|
| 403 |
+
ax.set_facecolor(plot_bg)
|
| 404 |
+
if data_col and data_col in df.columns:
|
| 405 |
+
xp = df[time_col].values if time_col else range(len(df))
|
| 406 |
+
yp = df[data_col].values
|
| 407 |
+
if chart_type == "bar":
|
| 408 |
+
bars = ax.bar(range(len(yp)), yp, color=color, alpha=0.85, edgecolor=bg, width=0.6)
|
| 409 |
+
for bar, val in zip(bars, yp):
|
| 410 |
+
ax.text(bar.get_x()+bar.get_width()/2, bar.get_height()+0.5, str(round(val,1)),
|
| 411 |
+
ha="center", va="bottom", color=fg, fontsize=10, fontweight="bold")
|
| 412 |
+
else:
|
| 413 |
+
ax.plot(xp, yp, color=color, linewidth=3, marker="o", markersize=8)
|
| 414 |
+
ax.fill_between(xp, yp, alpha=0.2, color=color)
|
| 415 |
+
for i,(xi,yi) in enumerate(zip(xp,yp)):
|
| 416 |
+
ax.annotate(str(round(yi,1)), (xi,yi), textcoords="offset points",
|
| 417 |
+
xytext=(0,10), ha="center", color=fg, fontsize=10, fontweight="bold")
|
| 418 |
+
ax.axhline(y=limit, color="#f59e0b", linestyle="--", linewidth=2.5, label=limit_label)
|
| 419 |
+
ax.legend(fontsize=11, labelcolor=fg, facecolor=plot_bg)
|
| 420 |
+
ax.set_ylabel(ylabel, color=ax_color, fontsize=12)
|
| 421 |
+
ax.set_xlabel(time_col or "Sample", color=ax_color, fontsize=12)
|
| 422 |
+
mean_val = round(float(np.mean(yp)),2)
|
| 423 |
+
max_val = round(float(np.max(yp)),2)
|
| 424 |
+
status = "HIGH" if max_val > limit else "NORMAL"
|
| 425 |
+
ax.set_title(title+chr(10)+"Mean: "+str(mean_val)+" Max: "+str(max_val)+" Status: "+status,
|
| 426 |
+
color=fg, fontweight="bold", fontsize=12, pad=8)
|
| 427 |
+
ax.tick_params(colors=ax_color, labelsize=11)
|
| 428 |
+
ax.grid(True, alpha=0.3, color=grid_color, linestyle="--")
|
| 429 |
+
for spine in ["top","right"]: ax.spines[spine].set_visible(False)
|
| 430 |
+
for spine in ["bottom","left"]: ax.spines[spine].set_color(grid_color)
|
| 431 |
+
plt.tight_layout()
|
| 432 |
+
buf2 = io.BytesIO()
|
| 433 |
+
plt.savefig(buf2, format="png", facecolor=bg, bbox_inches="tight", dpi=130)
|
| 434 |
+
buf2.seek(0)
|
| 435 |
+
result = Image.open(buf2).copy()
|
| 436 |
+
plt.close()
|
| 437 |
+
return result
|
| 438 |
+
|
| 439 |
+
img1 = make_tgt_chart(tat_col, "#e63946", "TAT (ng/mL)", 8, "Normal limit: 8 ng/mL", "Thrombin-Antithrombin (TAT)")
|
| 440 |
+
img2 = make_tgt_chart(pf_col, "#4361ee", "PF1.2 (nmol/L)", 2.0, "Normal limit: 2.0", "Prothrombin Fragment PF1.2")
|
| 441 |
+
img3 = make_tgt_chart(hemo_col, "#2ecc71", "Free Hemoglobin (mg/L)", 20, "Normal limit: 20 mg/L", "Free Hemoglobin - Hemolysis", "bar")
|
| 442 |
+
img4 = make_tgt_chart(plt_col, "#e67e22", "Platelet Count (10³/μL)", 150, "Normal minimum: 150", "Platelet Count")
|
| 443 |
+
|
| 444 |
+
return img1, img2, img3, img4, "TGT CSV LOADED: "+str(len(df))+" rows | Columns: "+", ".join(df.columns.tolist())+ai_text
|
| 445 |
except Exception as e:
|
| 446 |
+
return None, None, None, None, "Error: "+str(e)
|
| 447 |
|
| 448 |
def analyze_upad_photo(image):
|
| 449 |
if image is None: return None, "Upload a uPAD photo first."
|
|
|
|
| 540 |
search_input.submit(quick_search, inputs=search_input, outputs=search_output)
|
| 541 |
|
| 542 |
with gr.Tab("PIV CSV"):
|
| 543 |
+
gr.Markdown("### Upload PIV CSV file — AI generates separate charts + clinical analysis")
|
| 544 |
gr.Markdown("CSV columns: **time, velocity, shear_stress** (any column names work)")
|
| 545 |
with gr.Row():
|
| 546 |
+
piv_file = gr.File(label="CLICK HERE TO UPLOAD PIV CSV", file_types=[".csv"], scale=3)
|
| 547 |
+
piv_theme = gr.Radio(["Dark", "White"], value="White", label="Chart Theme", scale=1)
|
| 548 |
+
piv_btn = gr.Button("Analyze PIV Data", variant="primary")
|
| 549 |
+
piv_result = gr.Textbox(label="AI Clinical Analysis", lines=6)
|
| 550 |
+
gr.Markdown("### Charts")
|
| 551 |
+
with gr.Row():
|
| 552 |
+
piv_chart1 = gr.Image(label="Velocity Profile", type="pil")
|
| 553 |
+
piv_chart2 = gr.Image(label="Shear Stress", type="pil")
|
| 554 |
+
with gr.Row():
|
| 555 |
+
piv_chart3 = gr.Image(label="Velocity vs Shear", type="pil")
|
| 556 |
+
piv_chart4 = gr.Image(label="Clinical Summary", type="pil")
|
| 557 |
+
piv_btn.click(analyze_piv_csv, inputs=[piv_file, piv_theme], outputs=[piv_chart1, piv_chart2, piv_chart3, piv_chart4, piv_result])
|
| 558 |
|
| 559 |
with gr.Tab("TGT CSV"):
|
| 560 |
gr.Markdown("### Upload TGT CSV file — AI generates blood biomarker charts + thrombogenicity assessment")
|
| 561 |
gr.Markdown("CSV columns: **time, TAT, PF12, hemoglobin, platelets** (any column names work)")
|
| 562 |
with gr.Row():
|
| 563 |
+
tgt_file = gr.File(label="CLICK HERE TO UPLOAD TGT CSV", file_types=[".csv"], scale=3)
|
| 564 |
+
tgt_theme = gr.Radio(["Dark", "White"], value="White", label="Chart Theme", scale=1)
|
| 565 |
+
tgt_btn = gr.Button("Analyze TGT Data", variant="primary")
|
| 566 |
+
tgt_result = gr.Textbox(label="AI Thrombogenicity Assessment", lines=6)
|
| 567 |
+
gr.Markdown("### Charts")
|
| 568 |
+
with gr.Row():
|
| 569 |
+
tgt_chart1 = gr.Image(label="TAT Over Time", type="pil")
|
| 570 |
+
tgt_chart2 = gr.Image(label="PF1.2 Over Time", type="pil")
|
| 571 |
+
with gr.Row():
|
| 572 |
+
tgt_chart3 = gr.Image(label="Free Hemoglobin", type="pil")
|
| 573 |
+
tgt_chart4 = gr.Image(label="Platelet Count", type="pil")
|
| 574 |
+
tgt_btn.click(analyze_tgt_csv, inputs=[tgt_file, tgt_theme], outputs=[tgt_chart1, tgt_chart2, tgt_chart3, tgt_chart4, tgt_result])
|
| 575 |
|
| 576 |
with gr.Tab("uPAD Photo"):
|
| 577 |
gr.Markdown("### Upload uPAD Photo — Instant CKD diagnosis from Jaffe reaction color")
|