Saicharan21 commited on
Commit
3698602
·
verified ·
1 Parent(s): 2fa14ea

Upload versions/app_v37_final.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. versions/app_v37_final.py +837 -0
versions/app_v37_final.py ADDED
@@ -0,0 +1,837 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os, requests, io, json
3
+ import numpy as np
4
+ import pandas as pd
5
+ import matplotlib
6
+ matplotlib.use("Agg")
7
+ import matplotlib.pyplot as plt
8
+ from groq import Groq
9
+ from PIL import Image
10
+ from datetime import datetime
11
+ from huggingface_hub import HfApi, hf_hub_download
12
+
13
+ GROQ_KEY = os.environ.get("GROQ_API_KEY", "")
14
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
15
+ HISTORY_REPO = "Saicharan21/cardiolab-chat-history"
16
+ PAPERS_DB_REPO = "Saicharan21/cardiolab-papers-db"
17
+ CARDIOLAB_MODEL = "Saicharan21/CardioLab-AI-Model"
18
+
19
+ CHAT_MODELS = {
20
+ "CardioLab Fine-tuned (SJSU)": "cardiolab",
21
+ "Llama 3.3 70B (Best)": "llama-3.3-70b-versatile",
22
+ "Llama 3.1 8B (Fast)": "llama-3.1-8b-instant",
23
+ "Mixtral 8x7B": "mixtral-8x7b-32768",
24
+ "Llama 4 Scout (New)": "meta-llama/llama-4-scout-17b-16e-instruct",
25
+ }
26
+
27
+ KNOWHOW = ("MCL: Sylgard 184 PDMS 10:1 ratio 48hr cure green laser PIV 70bpm 5L/min cardiac output 80-120mmHg. "
28
+ "TGT: Arduino Uno Stepper Motor 150mL blood sampled at 0 20 40 60 minutes. "
29
+ "NORMAL RANGES: TAT below 8 ng/mL. PF1.2 below 2.0 nmol/L. Free hemoglobin below 20 mg/L. Platelets above 150 thousand per uL. "
30
+ "HIGH RISK: TAT above 15. PF1.2 above 3.0. Hemoglobin above 50. Platelets below 100. "
31
+ "uPAD: Jaffe reaction creatinine picric acid orange-red. Normal creatinine 0.6-1.2 mg/dL. Borderline 1.2-1.5. CKD above 1.5. Stage2 1.5-3.0. Stage3-4 3.0-6.0. Stage5 above 6.0. "
32
+ "MHV: 27mm SJM Regent bileaflet also trileaflet monoleaflet pediatric designs. "
33
+ "PIV: green laser 532nm time-resolved. Normal velocity 0.5-2.0 m/s. Normal shear below 5 Pa. Risk above 10 Pa. "
34
+ "Equipment: Heska Element HT5 hematology analyzer time-resolved PIV Tygon tubing Arduino Uno stepper motor.")
35
+
36
+ # ── LOAD PAPERS + FINE-TUNED MODEL ON STARTUP ─────────────────────
37
+ CHUNKS = []
38
+ METADATA = []
39
+ EMBEDDINGS = None
40
+ PAPERS_LOADED = False
41
+ EMBEDDER = None
42
+ CARDIOLAB_TOKENIZER = None
43
+ CARDIOLAB_LLM = None
44
+ CARDIOLAB_MODEL_LOADED = False
45
+
46
+ def load_papers():
47
+ global CHUNKS, METADATA, EMBEDDINGS, PAPERS_LOADED, EMBEDDER
48
+ try:
49
+ from sentence_transformers import SentenceTransformer
50
+ chunks_path = hf_hub_download(repo_id=PAPERS_DB_REPO, filename="chunks.json", repo_type="dataset", token=HF_TOKEN)
51
+ meta_path = hf_hub_download(repo_id=PAPERS_DB_REPO, filename="metadata.json", repo_type="dataset", token=HF_TOKEN)
52
+ emb_path = hf_hub_download(repo_id=PAPERS_DB_REPO, filename="embeddings.npy", repo_type="dataset", token=HF_TOKEN)
53
+ with open(chunks_path) as f: CHUNKS = json.load(f)
54
+ with open(meta_path) as f: METADATA = json.load(f)
55
+ EMBEDDINGS = np.load(emb_path)
56
+ EMBEDDER = SentenceTransformer("all-MiniLM-L6-v2")
57
+ PAPERS_LOADED = True
58
+ print(f"Papers loaded: {len(CHUNKS)} chunks from {len(set(m['paper'] for m in METADATA))} papers")
59
+ return True
60
+ except Exception as e:
61
+ print(f"Paper load error: {e}")
62
+ return False
63
+
64
+ def load_cardiolab_model():
65
+ global CARDIOLAB_TOKENIZER, CARDIOLAB_LLM, CARDIOLAB_MODEL_LOADED
66
+ try:
67
+ import torch
68
+ from transformers import AutoModelForCausalLM, AutoTokenizer
69
+ from peft import PeftModel
70
+ print("Loading CardioLab fine-tuned model...")
71
+ base_model = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
72
+ CARDIOLAB_TOKENIZER = AutoTokenizer.from_pretrained(CARDIOLAB_MODEL, token=HF_TOKEN)
73
+ CARDIOLAB_TOKENIZER.pad_token = CARDIOLAB_TOKENIZER.eos_token
74
+ device = "cuda" if torch.cuda.is_available() else "cpu"
75
+ CARDIOLAB_LLM = AutoModelForCausalLM.from_pretrained(
76
+ CARDIOLAB_MODEL, token=HF_TOKEN,
77
+ torch_dtype=torch.float16 if device=="cuda" else torch.float32,
78
+ device_map="auto" if device=="cuda" else None,
79
+ low_cpu_mem_usage=True
80
+ )
81
+ CARDIOLAB_MODEL_LOADED = True
82
+ print(f"CardioLab model loaded on {device}!")
83
+ return True
84
+ except Exception as e:
85
+ print(f"CardioLab model load error: {e}")
86
+ return False
87
+
88
+ load_papers()
89
+ load_cardiolab_model()
90
+
91
+ # ── SEMANTIC SEARCH ────────────────────────────────────────────────
92
+ def search_papers(query, n=4):
93
+ if not PAPERS_LOADED or EMBEDDINGS is None or EMBEDDER is None:
94
+ return "", []
95
+ try:
96
+ q_emb = EMBEDDER.encode([query])
97
+ norms = np.linalg.norm(EMBEDDINGS, axis=1, keepdims=True)
98
+ emb_norm = EMBEDDINGS / (norms + 1e-10)
99
+ q_norm = q_emb / (np.linalg.norm(q_emb) + 1e-10)
100
+ scores = (emb_norm @ q_norm.T).flatten()
101
+ top_idx = np.argsort(scores)[::-1][:n]
102
+ context = ""
103
+ results = []
104
+ seen = set()
105
+ for idx in top_idx:
106
+ chunk = CHUNKS[idx]
107
+ meta = METADATA[idx]
108
+ score = float(scores[idx])
109
+ if score > 0.25:
110
+ results.append({"chunk":chunk,"paper":meta["paper"],"pillar":meta.get("pillar",""),"score":score})
111
+ if meta["paper"] not in seen:
112
+ context += chr(10)+"=== FROM: "+meta["paper"]+" ==="+chr(10)
113
+ seen.add(meta["paper"])
114
+ context += chunk[:500]+chr(10)
115
+ return context, results
116
+ except Exception as e:
117
+ return "", []
118
+
119
+ def answer_with_cardiolab_model(question, paper_context=""):
120
+ if not CARDIOLAB_MODEL_LOADED:
121
+ return None
122
+ try:
123
+ import torch
124
+ system = "You are CardioLab AI for SJSU Biomedical Engineering."
125
+ if paper_context:
126
+ system += " Use these SJSU research papers: "+paper_context[:500]
127
+ prompt = f"<|system|>{system}</s><|user|>{question}</s><|assistant|>"
128
+ inputs = CARDIOLAB_TOKENIZER(prompt, return_tensors="pt", truncation=True, max_length=512)
129
+ device = next(CARDIOLAB_LLM.parameters()).device
130
+ inputs = {k:v.to(device) for k,v in inputs.items()}
131
+ with torch.no_grad():
132
+ outputs = CARDIOLAB_LLM.generate(
133
+ **inputs, max_new_tokens=200, do_sample=True,
134
+ temperature=0.3, pad_token_id=CARDIOLAB_TOKENIZER.eos_token_id
135
+ )
136
+ response = CARDIOLAB_TOKENIZER.decode(outputs[0], skip_special_tokens=True)
137
+ if "<|assistant|>" in response:
138
+ answer = response.split("<|assistant|>")[-1].strip()
139
+ else:
140
+ answer = response[len(prompt):].strip() if len(response) > len(prompt) else response
141
+ return answer if len(answer) > 20 else None
142
+ except Exception as e:
143
+ print(f"CardioLab model error: {e}")
144
+ return None
145
+
146
+ CSS = """
147
+ body, .gradio-container { background: #f7f7f8 !important; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif !important; }
148
+ .tab-nav { background: #ffffff !important; border-bottom: 1px solid #e5e7eb !important; padding: 0 16px !important; display: flex !important; flex-wrap: wrap !important; }
149
+ .tab-nav button { background: transparent !important; color: #6b7280 !important; border: none !important; border-bottom: 2px solid transparent !important; padding: 10px 12px !important; font-weight: 500 !important; font-size: 0.8em !important; white-space: nowrap !important; border-radius: 0 !important; }
150
+ .tab-nav button:hover { color: #111827 !important; background: #f9fafb !important; }
151
+ .tab-nav button.selected { color: #c1121f !important; border-bottom: 2px solid #c1121f !important; font-weight: 700 !important; background: transparent !important; }
152
+ .message.user { background: #f3f4f6 !important; color: #1a202c !important; border-radius: 12px !important; }
153
+ .message.bot { background: #ffffff !important; color: #1a202c !important; border-left: 3px solid #c1121f !important; }
154
+ textarea { background: #ffffff !important; color: #1a202c !important; border: 1px solid #d1d5db !important; border-radius: 10px !important; }
155
+ button.primary { background: #c1121f !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 600 !important; }
156
+ button.secondary { background: #f3f4f6 !important; color: #374151 !important; border: 1px solid #d1d5db !important; border-radius: 8px !important; }
157
+ input[type=number] { background: #f9fafb !important; color: #1a202c !important; border: 1px solid #d1d5db !important; border-radius: 8px !important; }
158
+ """
159
+
160
+ HEADER = """<div style="background:linear-gradient(135deg,#0a0f2e 0%,#1a0a0a 100%);padding:0;border-bottom:3px solid #c1121f;overflow:hidden;">
161
+ <svg style="position:absolute;opacity:0.07;width:100%;height:100%;" viewBox="0 0 1200 120" preserveAspectRatio="none">
162
+ <polyline points="0,60 100,60 130,20 150,100 170,10 200,90 220,60 400,60 430,20 450,100 470,10 500,90 520,60 700,60 730,20 750,100 770,10 800,90 820,60 1000,60 1030,20 1050,100 1070,10 1100,90 1120,60 1200,60" fill="none" stroke="#c1121f" stroke-width="3"/>
163
+ </svg>
164
+ <div style="max-width:1200px;margin:0 auto;padding:16px 24px;display:flex;align-items:center;justify-content:space-between;position:relative;z-index:1;">
165
+ <div style="display:flex;align-items:center;gap:14px;">
166
+ <svg width="55" height="55" viewBox="0 0 100 100"><circle cx="50" cy="35" r="28" fill="#0057a8" opacity="0.9"/><ellipse cx="50" cy="14" rx="22" ry="10" fill="#0057a8"/>
167
+ <polygon points="30,14 33,4 36,14" fill="#e8a020"/><polygon points="36,12 39,2 42,12" fill="#e8a020"/>
168
+ <polygon points="42,11 45,1 48,11" fill="#e8a020"/><polygon points="48,11 51,1 54,11" fill="#e8a020"/>
169
+ <polygon points="54,12 57,2 60,12" fill="#e8a020"/><polygon points="60,14 63,4 66,14" fill="#e8a020"/>
170
+ <rect x="36" y="30" width="28" height="22" rx="4" fill="#0057a8"/><rect x="40" y="35" width="8" height="12" rx="2" fill="#e8a020"/>
171
+ <rect x="34" y="50" width="32" height="8" rx="4" fill="#0057a8"/></svg>
172
+ <div><div style="color:#9ca3af;font-size:0.7em;letter-spacing:2px;text-transform:uppercase;">San Jose State University</div>
173
+ <div style="color:#e8a020;font-size:0.82em;font-weight:700;">Biomedical Engineering</div></div></div>
174
+ <div style="text-align:center;flex:1;padding:0 20px;">
175
+ <div style="display:flex;align-items:center;justify-content:center;gap:10px;margin-bottom:3px;">
176
+ <svg width="100" height="28" viewBox="0 0 120 32"><polyline points="0,16 20,16 26,4 30,28 34,2 38,26 44,16 120,16" fill="none" stroke="#c1121f" stroke-width="2.5" stroke-linecap="round"/></svg>
177
+ <div style="font-size:2em;font-weight:900;letter-spacing:2px;"><span style="color:#ffffff;">Cardio</span><span style="color:#c1121f;">Lab</span><span style="color:#ffffff;"> AI</span></div>
178
+ <svg width="100" height="28" viewBox="0 0 120 32" style="transform:scaleX(-1);"><polyline points="0,16 20,16 26,4 30,28 34,2 38,26 44,16 120,16" fill="none" stroke="#c1121f" stroke-width="2.5" stroke-linecap="round"/></svg></div>
179
+ <div style="color:#9ca3af;font-size:0.68em;letter-spacing:2px;text-transform:uppercase;">RAG + Fine-tuned | BioGPT | ClinicalTrials | Weekly Updates | 5 AI Models</div></div>
180
+ <div style="display:flex;align-items:center;gap:14px;">
181
+ <div style="text-align:right;"><div style="color:#9ca3af;font-size:0.68em;text-transform:uppercase;">Research Pillars</div>
182
+ <div style="color:#ffffff;font-size:0.72em;margin-top:3px;">MHV CKD FSI</div>
183
+ <div style="color:#9ca3af;font-size:0.62em;margin-top:2px;">MCL PIV TGT uPAD COMSOL</div></div>
184
+ <svg width="48" height="48" viewBox="0 0 100 90">
185
+ <path d="M50 85 C50 85 5 55 5 30 C5 15 18 5 30 5 C38 5 45 9 50 15 C55 9 62 5 70 5 C82 5 95 15 95 30 C95 55 50 85 50 85Z" fill="#c1121f" opacity="0.9"/>
186
+ <polyline points="25,45 32,45 35,35 38,55 41,30 44,50 50,45 75,45" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" opacity="0.9"/></svg></div></div>
187
+ <div style="height:3px;background:linear-gradient(90deg,#0057a8,#c1121f,#e8a020,#c1121f,#0057a8);"></div></div>"""
188
+
189
+ def load_all_sessions():
190
+ if not HF_TOKEN: return {}
191
+ try:
192
+ path = hf_hub_download(repo_id=HISTORY_REPO, filename="chat_history.json", repo_type="dataset", token=HF_TOKEN)
193
+ with open(path) as f: return json.load(f)
194
+ except: return {}
195
+
196
+ def save_all_sessions(sessions):
197
+ if not HF_TOKEN: return False
198
+ try:
199
+ api2 = HfApi(token=HF_TOKEN)
200
+ api2.upload_file(path_or_fileobj=json.dumps(sessions, indent=2).encode(),
201
+ path_in_repo="chat_history.json", repo_id=HISTORY_REPO,
202
+ repo_type="dataset", token=HF_TOKEN, commit_message="Update")
203
+ return True
204
+ except: return False
205
+
206
+ def get_session_list():
207
+ s = load_all_sessions()
208
+ return list(reversed(list(s.keys()))) if s else ["No saved sessions"]
209
+
210
+ def save_session(history, name):
211
+ if not history: return "Nothing to save", gr.update()
212
+ if not name or not name.strip(): name = "Chat "+datetime.now().strftime("%b %d %H:%M")
213
+ sessions = load_all_sessions()
214
+ sessions[name] = {"messages":history,"saved_at":datetime.now().isoformat()}
215
+ ok = save_all_sessions(sessions)
216
+ choices = get_session_list()
217
+ return ("Saved: "+name if ok else "Save failed"), gr.update(choices=choices, value=name)
218
+
219
+ def load_session(name):
220
+ if not name or "No saved" in name: return [], "Select a session"
221
+ sessions = load_all_sessions()
222
+ return (sessions[name]["messages"], "Loaded: "+name) if name in sessions else ([], "Not found")
223
+
224
+ def delete_session(name):
225
+ if not name or "No saved" in name: return "Select a session", gr.update()
226
+ sessions = load_all_sessions()
227
+ if name in sessions:
228
+ del sessions[name]; save_all_sessions(sessions)
229
+ choices = get_session_list()
230
+ return "Deleted: "+name, gr.update(choices=choices, value=choices[0] if choices else None)
231
+ return "Not found", gr.update()
232
+
233
+ def new_chat(): return [], "", "New chat started"
234
+
235
+ def get_pubmed_chat(query, n=3):
236
+ try:
237
+ r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
238
+ params={"db":"pubmed","term":query+" AND (heart valve OR hemodynamics OR microfluidic OR thrombogen OR creatinine OR CKD)","retmax":n,"retmode":"json","sort":"date","field":"tiab"},timeout=10)
239
+ ids = r.json()["esearchresult"]["idlist"]
240
+ return chr(10).join(["https://pubmed.ncbi.nlm.nih.gov/"+i for i in ids]) if ids else ""
241
+ except: return ""
242
+
243
+
244
+ # ── PHASE C: BIOGPT + CLINICALTRIALS + WEEKLY UPDATE ──────────────
245
+
246
+ def search_biogpt(query):
247
+ """Search BioGPT — trained on 15M PubMed papers via HuggingFace API"""
248
+ if not HF_TOKEN: return ""
249
+ try:
250
+ headers = {"Authorization": "Bearer "+HF_TOKEN}
251
+ # Use BioGPT for biomedical question answering
252
+ payload = {"inputs": query+" [SEP] Answer based on biomedical literature:"}
253
+ r = requests.post(
254
+ "https://api-inference.huggingface.co/models/microsoft/BioGPT-Large-PubMedQA",
255
+ headers=headers, json=payload, timeout=20
256
+ )
257
+ if r.status_code == 200:
258
+ result = r.json()
259
+ if isinstance(result, list) and len(result) > 0:
260
+ text = result[0].get("generated_text","")
261
+ # Extract just the answer part
262
+ if "[SEP]" in text:
263
+ text = text.split("[SEP]")[-1].strip()
264
+ return text[:400] if text else ""
265
+ return ""
266
+ except: return ""
267
+
268
+ def search_clinical_trials(query, n=5):
269
+ """Search ClinicalTrials.gov for heart valve and CKD trials"""
270
+ try:
271
+ r = requests.get(
272
+ "https://clinicaltrials.gov/api/v2/studies",
273
+ params={
274
+ "query.term": query,
275
+ "filter.overallStatus": "RECRUITING|COMPLETED",
276
+ "pageSize": n,
277
+ "format": "json",
278
+ "fields": "NCTId,BriefTitle,OverallStatus,Phase,StartDate,Condition"
279
+ },
280
+ timeout=12
281
+ )
282
+ if r.status_code != 200: return []
283
+ studies = r.json().get("studies",[])
284
+ results = []
285
+ for s in studies:
286
+ proto = s.get("protocolSection",{})
287
+ ident = proto.get("identificationModule",{})
288
+ status = proto.get("statusModule",{})
289
+ nct = ident.get("nctId","")
290
+ title = ident.get("briefTitle","")
291
+ phase = status.get("phase","")
292
+ overall = status.get("overallStatus","")
293
+ if nct and title:
294
+ results.append({
295
+ "nct": nct,
296
+ "title": title,
297
+ "status": overall,
298
+ "phase": phase,
299
+ "url": "https://clinicaltrials.gov/study/"+nct
300
+ })
301
+ return results
302
+ except: return []
303
+
304
+ def get_weekly_pubmed_update(topics=None):
305
+ """Get papers published in last 7 days on CardioLab topics"""
306
+ if topics is None:
307
+ topics = [
308
+ "mechanical heart valve thrombogenicity",
309
+ "microfluidic creatinine CKD diagnosis",
310
+ "PIV hemodynamics prosthetic valve",
311
+ "Mock Circulatory Loop cardiac",
312
+ "bileaflet valve fluid structure interaction"
313
+ ]
314
+ all_new = []
315
+ try:
316
+ from datetime import datetime, timedelta
317
+ week_ago = (datetime.now() - timedelta(days=7)).strftime("%Y/%m/%d")
318
+ for topic in topics:
319
+ r = requests.get(
320
+ "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
321
+ params={
322
+ "db":"pubmed",
323
+ "term":topic,
324
+ "mindate":week_ago,
325
+ "datetype":"pdat",
326
+ "retmax":3,
327
+ "retmode":"json",
328
+ "sort":"date"
329
+ },
330
+ timeout=10
331
+ )
332
+ ids = r.json()["esearchresult"]["idlist"]
333
+ for pmid in ids:
334
+ all_new.append({
335
+ "pmid": pmid,
336
+ "topic": topic,
337
+ "url": "https://pubmed.ncbi.nlm.nih.gov/"+pmid
338
+ })
339
+ return all_new
340
+ except: return []
341
+
342
+ def full_research_search(query, search_model="Llama 3.3 70B (Best)"):
343
+ """Complete search across ALL sources including Phase C additions"""
344
+ if not query.strip(): return "Please enter a research topic."
345
+
346
+ model_id = CHAT_MODELS.get(search_model, "llama-3.3-70b-versatile")
347
+ expanded = expand_query_ai(query, model_id) if GROQ_KEY else query
348
+
349
+ # All search sources
350
+ pubmed = fetch_pubmed(expanded, n=6)
351
+ scholar = fetch_scholar(expanded, n=5)
352
+ europe = fetch_europe_pmc(expanded, n=4)
353
+ trials = search_clinical_trials(query, n=4)
354
+ weekly = get_weekly_pubmed_update()
355
+ biogpt_answer = search_biogpt(query)
356
+
357
+ # Format output
358
+ out = "QUERY: "+query+chr(10)
359
+ out += "AI EXPANDED: "+expanded+chr(10)
360
+ out += "SOURCES: PubMed + Scholar + EuropePMC + ClinicalTrials + SJSU + BioGPT"+chr(10)
361
+ out += "="*50+chr(10)+chr(10)
362
+
363
+ # BioGPT answer first
364
+ if biogpt_answer:
365
+ out += "BIOGPT ANSWER (trained on 15M PubMed papers):"+chr(10)
366
+ out += biogpt_answer+chr(10)+chr(10)
367
+ out += "="*50+chr(10)+chr(10)
368
+
369
+ # PubMed results
370
+ if pubmed:
371
+ out += "PUBMED ("+str(len(pubmed))+" papers):"+chr(10)
372
+ for p in pubmed[:6]:
373
+ out += p["title"][:85]+" ("+p["year"]+")"+chr(10)
374
+ out += " "+p["url"]+chr(10)+chr(10)
375
+
376
+ # Scholar results
377
+ if scholar:
378
+ out += "SEMANTIC SCHOLAR ("+str(len(scholar))+" papers):"+chr(10)
379
+ for p in scholar[:5]:
380
+ out += p["title"][:85]+" ("+p["year"]+")"
381
+ if p["citations"] not in ("N/A","","0"): out += " | "+p["citations"]+" citations"
382
+ out += chr(10)+" "+p["url"]+chr(10)+chr(10)
383
+
384
+ # Clinical trials
385
+ if trials:
386
+ out += "CLINICALTRIALS.GOV ("+str(len(trials))+" trials):"+chr(10)
387
+ for t in trials:
388
+ out += t["title"][:80]+" | "+t["status"]+" | "+t.get("phase","")+" "+chr(10)
389
+ out += " "+t["url"]+chr(10)+chr(10)
390
+
391
+ # Weekly updates
392
+ weekly_relevant = [w for w in weekly if any(
393
+ kw in query.lower() for kw in ["valve","heart","ckd","creatinine","piv","tgt","mcl"]
394
+ )]
395
+ if weekly_relevant:
396
+ out += "NEW THIS WEEK (last 7 days):"+chr(10)
397
+ for w in weekly_relevant[:5]:
398
+ out += " "+w["url"]+" ["+w["topic"][:40]+"]"+chr(10)
399
+
400
+ # SJSU ScholarWorks
401
+ out += chr(10)+"SJSU SCHOLARWORKS:"+chr(10)
402
+ out += " https://scholarworks.sjsu.edu/do/search/?q="+requests.utils.quote(query)+"&context=6781027"
403
+
404
+ return out
405
+
406
+
407
+ def research_chat(message, history, chat_model="Llama 3.3 70B (Best)"):
408
+ if not message.strip(): return "", history
409
+ paper_context, paper_results = search_papers(message, n=4)
410
+
411
+ # Use fine-tuned CardioLab model if selected
412
+ if chat_model == "CardioLab Fine-tuned (SJSU)" and CARDIOLAB_MODEL_LOADED:
413
+ answer = answer_with_cardiolab_model(message, paper_context)
414
+ if answer:
415
+ if paper_results:
416
+ unique_papers = list(dict.fromkeys([r["paper"] for r in paper_results]))
417
+ answer += chr(10)+chr(10)+"Sources from SJSU CardioLab papers:"
418
+ for p in unique_papers[:3]:
419
+ answer += chr(10)+" - "+p.replace('.pdf','').replace('_',' ')
420
+ pubmed = get_pubmed_chat(message, n=2)
421
+ if pubmed: answer += chr(10)+"PubMed: "+pubmed
422
+ history.append({"role":"user","content":message})
423
+ history.append({"role":"assistant","content":"[CardioLab Fine-tuned Model] "+answer})
424
+ return "", history
425
+
426
+ # Fall back to Groq models
427
+ if not GROQ_KEY:
428
+ history.append({"role":"user","content":message})
429
+ history.append({"role":"assistant","content":"Error: Add GROQ_API_KEY to Space Settings."})
430
+ return "", history
431
+ try:
432
+ model_id = CHAT_MODELS.get(chat_model, "llama-3.3-70b-versatile")
433
+ client = Groq(api_key=GROQ_KEY)
434
+ if paper_context:
435
+ system_prompt = ("You are CardioLab AI for SJSU Biomedical Engineering. "
436
+ "Answer using SJSU CardioLab research papers below. "
437
+ "Always cite the paper name when using specific data."+chr(10)+chr(10)+
438
+ "SJSU CARDIOLAB PAPERS:"+chr(10)+paper_context+chr(10)+chr(10)+
439
+ "ADDITIONAL KNOWLEDGE: "+KNOWHOW)
440
+ else:
441
+ system_prompt = "You are CardioLab AI for SJSU Biomedical Engineering. Expert in MHV MCL PIV TGT uPAD CKD FSI. "+KNOWHOW
442
+ msgs = [{"role":"system","content":system_prompt}]
443
+ for item in history:
444
+ if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
445
+ msgs.append({"role":"user","content":message})
446
+ resp = client.chat.completions.create(model=model_id, messages=msgs, max_tokens=800)
447
+ answer = resp.choices[0].message.content
448
+ if paper_results:
449
+ unique_papers = list(dict.fromkeys([r["paper"] for r in paper_results]))
450
+ answer += chr(10)+chr(10)+"Sources from SJSU CardioLab papers:"
451
+ for p in unique_papers[:3]:
452
+ answer += chr(10)+" - "+p.replace('.pdf','').replace('_',' ')
453
+ pubmed = get_pubmed_chat(message, n=2)
454
+ if pubmed: answer += chr(10)+"PubMed: "+pubmed
455
+ history.append({"role":"user","content":message})
456
+ history.append({"role":"assistant","content":answer})
457
+ return "", history
458
+ except Exception as e:
459
+ history.append({"role":"user","content":message})
460
+ history.append({"role":"assistant","content":"Error: "+str(e)})
461
+ return "", history
462
+
463
+ def voice_chat(audio, history):
464
+ if audio is None:
465
+ history.append({"role":"assistant","content":"Please record your question first."})
466
+ return history
467
+ try:
468
+ client = Groq(api_key=GROQ_KEY)
469
+ with open(audio, "rb") as f:
470
+ tx = client.audio.transcriptions.create(file=("audio.wav", f, "audio/wav"), model="whisper-large-v3")
471
+ paper_context, _ = search_papers(tx.text, n=3)
472
+ system = "You are CardioLab AI. "+KNOWHOW
473
+ if paper_context: system = "You are CardioLab AI. Use these SJSU papers:"+chr(10)+paper_context+chr(10)+KNOWHOW
474
+ msgs = [{"role":"system","content":system}]
475
+ for item in history:
476
+ if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
477
+ msgs.append({"role":"user","content":tx.text})
478
+ resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=500)
479
+ history.append({"role":"user","content":"Voice: "+tx.text})
480
+ history.append({"role":"assistant","content":resp.choices[0].message.content})
481
+ return history
482
+ except Exception as e:
483
+ history.append({"role":"assistant","content":"Voice error: "+str(e)})
484
+ return history
485
+
486
+ def expand_query_ai(query):
487
+ if not GROQ_KEY: return query
488
+ try:
489
+ client = Groq(api_key=GROQ_KEY)
490
+ resp = client.chat.completions.create(model="llama-3.1-8b-instant",
491
+ messages=[{"role":"system","content":"Biomedical PubMed expert. Convert to MeSH terms for heart valves hemodynamics PIV thrombogenicity FSI microfluidics CKD. Return ONLY terms."},
492
+ {"role":"user","content":"Optimize: "+query}],max_tokens=80)
493
+ return resp.choices[0].message.content.strip() or query
494
+ except: return query
495
+
496
+ def quick_search(query, search_model="Llama 3.3 70B (Best)"):
497
+ if not query.strip(): return "Please enter a topic."
498
+ expanded = expand_query_ai(query)
499
+ results = []
500
+ try:
501
+ forced = expanded+" AND (heart valve OR hemodynamics OR microfluidic OR thrombogen OR creatinine OR PIV OR CFD OR CKD)"
502
+ r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
503
+ params={"db":"pubmed","term":forced,"retmax":8,"retmode":"json","sort":"date","field":"tiab"},timeout=12)
504
+ ids = r.json()["esearchresult"]["idlist"]
505
+ if ids:
506
+ r2 = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi",
507
+ params={"db":"pubmed","id":",".join(ids),"retmode":"xml","rettype":"abstract"},timeout=12)
508
+ import xml.etree.ElementTree as ET
509
+ root = ET.fromstring(r2.content)
510
+ for article in root.findall(".//PubmedArticle"):
511
+ try:
512
+ title = article.find(".//ArticleTitle").text or "No title"
513
+ pmid = article.find(".//PMID").text or ""
514
+ year_el = article.find(".//PubDate/Year")
515
+ year = year_el.text if year_el is not None else ""
516
+ results.append({"source":"PubMed","title":str(title),"year":year,"url":"https://pubmed.ncbi.nlm.nih.gov/"+pmid,"citations":"N/A"})
517
+ except: continue
518
+ except: pass
519
+ try:
520
+ r = requests.get("https://api.semanticscholar.org/graph/v1/paper/search",
521
+ params={"query":expanded,"limit":6,"fields":"title,year,url,citationCount"},timeout=12)
522
+ for p in r.json().get("data",[]):
523
+ year = p.get("year",0) or 0
524
+ if int(year) >= 2015:
525
+ results.append({"source":"Scholar","title":p.get("title",""),"year":str(year),"url":p.get("url",""),"citations":str(p.get("citationCount",0))})
526
+ except: pass
527
+ out = "QUERY: "+query+chr(10)+"AI EXPANDED: "+expanded+chr(10)+"="*45+chr(10)+chr(10)
528
+ groups = {"PubMed":[],"Scholar":[]}
529
+ seen = set()
530
+ for r in results:
531
+ key = r["title"][:50].lower()
532
+ if key not in seen and r["url"]:
533
+ seen.add(key); groups[r["source"]].append(r)
534
+ for source, papers in groups.items():
535
+ if not papers: continue
536
+ out += "--- "+source+" ---"+chr(10)
537
+ for p in papers[:8]:
538
+ out += p["title"][:85]+" ("+p["year"]+")"
539
+ if p["citations"] not in ("N/A","","0"): out += " | "+p["citations"]+" citations"
540
+ out += chr(10)+" "+p["url"]+chr(10)+chr(10)
541
+ out += "--- SJSU ScholarWorks ---"+chr(10)
542
+ out += "https://scholarworks.sjsu.edu/do/search/?q="+requests.utils.quote(query)+"&context=6781027"
543
+ return out
544
+
545
+ def analyze_upad_photo(image):
546
+ if image is None: return None, "Upload a uPAD photo first."
547
+ try:
548
+ img = Image.fromarray(image) if not isinstance(image, Image.Image) else image
549
+ arr = np.array(img); h,w = arr.shape[:2]
550
+ y1,y2,x1,x2 = int(h*0.35),int(h*0.65),int(w*0.35),int(w*0.65)
551
+ zone = arr[y1:y2,x1:x2]
552
+ R,G,B = float(np.mean(zone[:,:,0])),float(np.mean(zone[:,:,1])),float(np.mean(zone[:,:,2]))
553
+ c = max(0,round(0.018*(R-B)-0.3,2))
554
+ if c<1.2: s,a="Normal","Monitor annually."
555
+ elif c<1.5: s,a="Borderline","Repeat in 3 months."
556
+ elif c<3.0: s,a="Stage 2 CKD","Consult nephrologist."
557
+ elif c<6.0: s,a="Stage 3-4 CKD","Immediate consultation."
558
+ else: s,a="Stage 5 CKD","Emergency care."
559
+ ri=img.copy()
560
+ import PIL.ImageDraw as D; D.Draw(ri).rectangle([x1,y1,x2,y2],outline=(0,255,0),width=3)
561
+ return ri,("uPAD ANALYSIS"+chr(10)+"R:"+str(round(R,1))+" G:"+str(round(G,1))+" B:"+str(round(B,1))+chr(10)+"Creatinine: "+str(c)+" mg/dL"+chr(10)+"Stage: "+s+chr(10)+"Action: "+a)
562
+ except Exception as e: return None,"Error: "+str(e)
563
+
564
+ def mk_chart(fn,title,bg,fg,gc,ac,pb):
565
+ fig2,ax=plt.subplots(figsize=(8,5)); fig2.patch.set_facecolor(bg); ax.set_facecolor(pb)
566
+ fn(ax); ax.set_title(title,color=fg,fontweight="bold",fontsize=13,pad=8)
567
+ ax.tick_params(colors=ac,labelsize=10); ax.grid(True,alpha=0.3,color=gc,linestyle="--")
568
+ for sp in ["top","right"]: ax.spines[sp].set_visible(False)
569
+ for sp in ["bottom","left"]: ax.spines[sp].set_color(gc)
570
+ plt.tight_layout(); buf=io.BytesIO(); plt.savefig(buf,format="png",facecolor=bg,bbox_inches="tight",dpi=130); buf.seek(0)
571
+ res=Image.open(buf).copy(); plt.close(); return res
572
+
573
+ def analyze_piv_csv(file,theme="White"):
574
+ if file is None: return None,None,None,None,"Upload PIV CSV first."
575
+ try:
576
+ df=pd.read_csv(file.name); cols=[c.lower().strip() for c in df.columns]; df.columns=cols
577
+ num_cols=df.select_dtypes(include=[np.number]).columns.tolist()
578
+ if not num_cols: return None,None,None,None,"No numeric columns."
579
+ bg="#fff" if theme=="White" else "#0a1628"; fg="#1a202c" if theme=="White" else "white"
580
+ gc="#e2e8f0" if theme=="White" else "#2d4a8a"; ac="#4a5568" if theme=="White" else "#a8b2d8"
581
+ pb="#f7fafc" if theme=="White" else "#132340"
582
+ x=np.arange(len(df))
583
+ vc=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)
584
+ sc2=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)
585
+ tc=next((c for c in cols if "time" in c or "frame" in c),None); xv=df[tc] if tc else x
586
+ def pv(ax):
587
+ if vc:
588
+ ax.plot(xv,df[vc],color="#c1121f",linewidth=2.5,marker="o",markersize=5)
589
+ ax.fill_between(xv,df[vc],alpha=0.15,color="#c1121f")
590
+ ax.axhline(y=2.0,color="#f59e0b",linestyle="--",linewidth=2,label="Risk: 2.0 m/s")
591
+ ax.set_ylabel("Velocity (m/s)",color=ac); ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
592
+ def ps(ax):
593
+ if sc2:
594
+ xp=xv.values if tc else x
595
+ ax.plot(xp,df[sc2],color="#0057a8",linewidth=2.5,marker="s",markersize=5)
596
+ ax.fill_between(xp,df[sc2],alpha=0.15,color="#0057a8")
597
+ ax.axhline(y=5,color="#f59e0b",linestyle="--",linewidth=2,label="Caution 5 Pa")
598
+ ax.axhline(y=10,color="#c1121f",linestyle="--",linewidth=2,label="High risk 10 Pa")
599
+ ax.set_ylabel("Shear (Pa)",color=ac); ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
600
+ def psc(ax):
601
+ if vc and sc2:
602
+ s3=ax.scatter(df[vc],df[sc2],c=x,cmap="RdYlGn_r",s=90,edgecolors=fg,linewidth=0.5,zorder=5)
603
+ cb=plt.colorbar(s3,ax=ax,label="Time"); cb.ax.yaxis.label.set_color(fg); cb.ax.tick_params(colors=ac)
604
+ ax.axvline(x=2.0,color="#f59e0b",linestyle="--",linewidth=2); ax.axhline(y=10,color="#c1121f",linestyle="--",linewidth=2)
605
+ ax.set_xlabel("Velocity (m/s)",color=ac); ax.set_ylabel("Shear (Pa)",color=ac)
606
+ def psum(ax):
607
+ ax.axis("off"); risk=[]
608
+ st="CLINICAL SUMMARY"+chr(10)+"="*20+chr(10)+chr(10)
609
+ for col in num_cols[:3]:
610
+ mn=round(df[col].mean(),3); mx=round(df[col].max(),3)
611
+ st+=col[:14]+":"+chr(10)+" Mean: "+str(mn)+chr(10)+" Max: "+str(mx)+chr(10)+chr(10)
612
+ if "vel" in col and mx>2.0: risk.append("HIGH VELOCITY")
613
+ if "shear" in col and mx>10: risk.append("HIGH SHEAR")
614
+ bc="#c1121f" if risk else "#2ecc71"
615
+ st+="="*20+chr(10)+("OVERALL: HIGH RISK" if risk else "OVERALL: LOW RISK")
616
+ ax.text(0.05,0.97,st,transform=ax.transAxes,color=fg,fontsize=10,va="top",fontfamily="monospace",
617
+ bbox=dict(boxstyle="round,pad=0.8",facecolor=pb,edgecolor=bc,linewidth=2.5))
618
+ i1=mk_chart(pv,"Velocity Profile",bg,fg,gc,ac,pb); i2=mk_chart(ps,"Wall Shear Stress",bg,fg,gc,ac,pb)
619
+ i3=mk_chart(psc,"Velocity vs Shear",bg,fg,gc,ac,pb); i4=mk_chart(psum,"Clinical Summary",bg,fg,gc,ac,pb)
620
+ ai=""
621
+ if GROQ_KEY:
622
+ try:
623
+ client=Groq(api_key=GROQ_KEY)
624
+ resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
625
+ messages=[{"role":"system","content":"PIV expert SJSU CardioLab."},
626
+ {"role":"user","content":"PIV from 27mm SJM Regent:"+chr(10)+df.describe().to_string()[:500]}],max_tokens=250)
627
+ ai=chr(10)+"AI: "+resp.choices[0].message.content
628
+ except: pass
629
+ return i1,i2,i3,i4,"PIV: "+str(len(df))+" rows"+ai
630
+ except Exception as e: return None,None,None,None,"Error: "+str(e)
631
+
632
+ def analyze_tgt_csv(file,theme="White"):
633
+ if file is None: return None,None,None,None,"Upload TGT CSV first."
634
+ try:
635
+ df=pd.read_csv(file.name); cols=[c.lower().strip() for c in df.columns]; df.columns=cols
636
+ num_cols=df.select_dtypes(include=[np.number]).columns.tolist()
637
+ bg="#fff" if theme=="White" else "#0a1628"; fg="#1a202c" if theme=="White" else "white"
638
+ gc="#e2e8f0" if theme=="White" else "#2d4a8a"; ac="#4a5568" if theme=="White" else "#a8b2d8"
639
+ pb="#f7fafc" if theme=="White" else "#132340"
640
+ tc=next((c for c in cols if "time" in c or "min" in c),None)
641
+ tatc=next((c for c in cols if "tat" in c),num_cols[0] if num_cols else None)
642
+ pfc=next((c for c in cols if "pf" in c),num_cols[1] if len(num_cols)>1 else None)
643
+ hc=next((c for c in cols if "hemo" in c),num_cols[2] if len(num_cols)>2 else None)
644
+ plc=next((c for c in cols if "platelet" in c or "plt" in c),num_cols[3] if len(num_cols)>3 else None)
645
+ def mk2(dc,color,yl,lim,ll,title,bar=False):
646
+ def fn(ax):
647
+ if dc and dc in df.columns:
648
+ xp=df[tc].values if tc else range(len(df)); yp=df[dc].values
649
+ if bar:
650
+ bs=ax.bar(range(len(yp)),yp,color=color,alpha=0.85,edgecolor=bg,width=0.6)
651
+ for b,v in zip(bs,yp): ax.text(b.get_x()+b.get_width()/2,b.get_height()+0.5,str(round(v,1)),ha="center",va="bottom",color=fg,fontsize=10,fontweight="bold")
652
+ else:
653
+ ax.plot(xp,yp,color=color,linewidth=3,marker="o",markersize=8)
654
+ ax.fill_between(xp,yp,alpha=0.15,color=color)
655
+ for xi,yi in zip(xp,yp): ax.annotate(str(round(yi,1)),(xi,yi),textcoords="offset points",xytext=(0,10),ha="center",color=fg,fontsize=10,fontweight="bold")
656
+ ax.axhline(y=lim,color="#f59e0b",linestyle="--",linewidth=2.5,label=ll)
657
+ ax.legend(fontsize=10,labelcolor=fg,facecolor=pb); ax.set_ylabel(yl,color=ac)
658
+ mv=round(float(np.max(yp)),2)
659
+ ax.set_title(title+chr(10)+"Max: "+str(mv)+" - "+("HIGH" if mv>lim else "NORMAL"),color=fg,fontweight="bold",fontsize=12)
660
+ return mk_chart(fn,title,bg,fg,gc,ac,pb)
661
+ i1=mk2(tatc,"#c1121f","TAT (ng/mL)",8,"Normal: 8","TAT"); i2=mk2(pfc,"#0057a8","PF1.2",2.0,"Normal: 2.0","PF1.2")
662
+ i3=mk2(hc,"#2ecc71","Free Hgb (mg/L)",20,"Normal: 20","Free Hemoglobin",bar=True); i4=mk2(plc,"#e8a020","Platelets",150,"Normal>150","Platelets")
663
+ ai=""
664
+ if GROQ_KEY:
665
+ try:
666
+ client=Groq(api_key=GROQ_KEY)
667
+ resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
668
+ messages=[{"role":"system","content":"Hematology expert. Give thrombogenicity risk."},
669
+ {"role":"user","content":"TGT:"+chr(10)+df.describe().to_string()[:500]}],max_tokens=250)
670
+ ai=chr(10)+"AI: "+resp.choices[0].message.content
671
+ except: pass
672
+ return i1,i2,i3,i4,"TGT: "+str(len(df))+" rows"+ai
673
+ except Exception as e: return None,None,None,None,"Error: "+str(e)
674
+
675
+ def generate_image(prompt):
676
+ if not prompt.strip(): return None,"Enter description.","";
677
+ if not HF_TOKEN: return None,"Add HF_TOKEN to Space secrets.","";
678
+ try:
679
+ enhanced,desc=prompt,""
680
+ if GROQ_KEY:
681
+ try:
682
+ client=Groq(api_key=GROQ_KEY)
683
+ resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
684
+ messages=[{"role":"system","content":"Format: DESCRIPTION: [2 sentences] PROMPT: [detailed image prompt]"},
685
+ {"role":"user","content":"Biomedical image: "+prompt}],max_tokens=200)
686
+ full=resp.choices[0].message.content
687
+ if "DESCRIPTION:" in full and "PROMPT:" in full:
688
+ desc=full.split("DESCRIPTION:")[1].split("PROMPT:")[0].strip()
689
+ enhanced=full.split("PROMPT:")[1].strip()
690
+ except: pass
691
+ headers={"Authorization":"Bearer "+HF_TOKEN,"Content-Type":"application/json"}
692
+ for url in ["https://router.huggingface.co/hf-inference/models/black-forest-labs/FLUX.1-schnell",
693
+ "https://router.huggingface.co/hf-inference/models/stabilityai/stable-diffusion-xl-base-1.0"]:
694
+ try:
695
+ r=requests.post(url,headers=headers,json={"inputs":enhanced,"parameters":{"num_inference_steps":8}},timeout=60)
696
+ if r.status_code==200: return Image.open(io.BytesIO(r.content)),"Generated!",desc
697
+ except: continue
698
+ return None,"Models busy.",desc
699
+ except Exception as e: return None,"Error: "+str(e),""
700
+
701
+ def piv_manual(v,s,h):
702
+ vr="HIGH-stenosis" if float(v)>2.0 else "NORMAL"
703
+ sr="HIGH-thrombosis" if float(s)>10 else "ELEVATED" if float(s)>5 else "NORMAL"
704
+ return "Velocity: "+str(v)+" m/s — "+vr+chr(10)+"Shear: "+str(s)+" Pa — "+sr+chr(10)+"HR: "+str(h)+" bpm"
705
+
706
+ def tgt_manual(t,p,h,pl,tm):
707
+ risk=sum([float(t)>15,float(p)>2.0,float(h)>50,float(pl)<150])
708
+ return "TAT:"+str(t)+" PF1.2:"+str(p)+chr(10)+"Hemo:"+str(h)+" Plt:"+str(pl)+chr(10)+"RESULT: "+("HIGH RISK" if risk>=3 else "MODERATE" if risk>=2 else "LOW RISK")
709
+
710
+ with gr.Blocks(title="CardioLab AI - SJSU", css=CSS) as demo:
711
+ gr.HTML(HEADER)
712
+
713
+ papers_count = len(set(m["paper"] for m in METADATA)) if PAPERS_LOADED else 0
714
+ model_status = "CardioLab Fine-tuned Model LOADED" if CARDIOLAB_MODEL_LOADED else "Fine-tuned model loading..."
715
+ rag_status = f"RAG: {len(CHUNKS)} chunks from {papers_count} SJSU papers" if PAPERS_LOADED else "RAG: loading..."
716
+ gr.HTML(f'''<div style="background:#1a7340;color:white;text-align:center;padding:7px;font-size:0.82em;font-weight:700;">
717
+ {rag_status} | {model_status} | Select "CardioLab Fine-tuned (SJSU)" in Model dropdown to use your custom model!</div>''')
718
+
719
+ with gr.Tabs():
720
+ with gr.Tab("Chat"):
721
+ with gr.Row():
722
+ with gr.Column(scale=1, min_width=200):
723
+ gr.HTML('''<div style="background:#202123;padding:10px;border-radius:8px;margin-bottom:6px;">
724
+ <div style="color:#e8a020;font-weight:700;font-size:0.85em;">SJSU CARDIOLAB</div>
725
+ <div style="color:#9ca3af;font-size:0.7em;">Conversations</div></div>''')
726
+ new_chat_btn = gr.Button("New Chat", variant="secondary")
727
+ session_dropdown = gr.Dropdown(choices=get_session_list(), label="Saved Sessions", interactive=True)
728
+ load_btn = gr.Button("Load Session", variant="primary")
729
+ session_name_box = gr.Textbox(placeholder="Session name...", label="", lines=1, container=False)
730
+ with gr.Row():
731
+ save_btn = gr.Button("Save", variant="primary", scale=2)
732
+ delete_btn = gr.Button("Del", variant="secondary", scale=1)
733
+ session_status = gr.Textbox(label="", lines=1, interactive=False, container=False)
734
+ with gr.Column(scale=4):
735
+ chatbot = gr.Chatbot(label="", height=460, show_label=False, container=False)
736
+ with gr.Row():
737
+ msg_box = gr.Textbox(placeholder="Ask anything — AI searches 16 SJSU papers + PubMed...", label="", lines=2, scale=4, container=False)
738
+ with gr.Column(scale=1, min_width=160):
739
+ chat_model_dd = gr.Dropdown(choices=list(CHAT_MODELS.keys()), value="Llama 3.3 70B (Best)", label="AI Model")
740
+ send_btn = gr.Button("Send", variant="primary")
741
+ clear_btn = gr.Button("Clear", variant="secondary")
742
+ send_btn.click(research_chat, inputs=[msg_box, chatbot, chat_model_dd], outputs=[msg_box, chatbot])
743
+ msg_box.submit(research_chat, inputs=[msg_box, chatbot, chat_model_dd], outputs=[msg_box, chatbot])
744
+ clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg_box])
745
+ new_chat_btn.click(new_chat, outputs=[chatbot, msg_box, session_status])
746
+ save_btn.click(save_session, inputs=[chatbot, session_name_box], outputs=[session_status, session_dropdown])
747
+ load_btn.click(load_session, inputs=session_dropdown, outputs=[chatbot, session_status])
748
+ delete_btn.click(delete_session, inputs=session_dropdown, outputs=[session_status, session_dropdown])
749
+
750
+ with gr.Tab("Voice"):
751
+ voice_chatbot = gr.Chatbot(label="", height=360, show_label=False)
752
+ audio_input = gr.Audio(sources=["microphone"], type="filepath", label="Record Question")
753
+ with gr.Row():
754
+ voice_btn = gr.Button("Ask by Voice", variant="primary")
755
+ voice_clear = gr.Button("Clear", variant="secondary")
756
+ voice_btn.click(voice_chat, inputs=[audio_input, voice_chatbot], outputs=voice_chatbot)
757
+ voice_clear.click(lambda: [], outputs=voice_chatbot)
758
+
759
+ with gr.Tab("Papers"):
760
+ gr.Markdown("### Search PubMed + Scholar + EuropePMC + ClinicalTrials.gov + SJSU + BioGPT (15M papers)")
761
+ with gr.Row():
762
+ search_input = gr.Textbox(placeholder="e.g. bileaflet mechanical heart valve thrombogenicity hemodynamics", label="Research Topic", scale=3)
763
+ search_model_dd = gr.Dropdown(choices=list(CHAT_MODELS.keys()), value="Llama 3.3 70B (Best)", label="AI Model", scale=1)
764
+ search_btn = gr.Button("Search", variant="primary", scale=1)
765
+ search_output = gr.Textbox(label="Results", lines=22)
766
+ search_btn.click(full_research_search, inputs=[search_input, search_model_dd], outputs=search_output)
767
+ search_input.submit(full_research_search, inputs=[search_input, search_model_dd], outputs=search_output)
768
+
769
+ with gr.Tab("PIV CSV"):
770
+ with gr.Row():
771
+ piv_file = gr.File(label="Upload PIV CSV", file_types=[".csv"], scale=3)
772
+ piv_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
773
+ piv_btn = gr.Button("Analyze PIV Data", variant="primary")
774
+ piv_result = gr.Textbox(label="AI Analysis", lines=4)
775
+ with gr.Row():
776
+ piv_c1=gr.Image(label="Velocity",type="pil"); piv_c2=gr.Image(label="Shear Stress",type="pil")
777
+ with gr.Row():
778
+ piv_c3=gr.Image(label="Vel vs Shear",type="pil"); piv_c4=gr.Image(label="Clinical Summary",type="pil")
779
+ piv_btn.click(analyze_piv_csv, inputs=[piv_file,piv_theme], outputs=[piv_c1,piv_c2,piv_c3,piv_c4,piv_result])
780
+
781
+ with gr.Tab("TGT CSV"):
782
+ with gr.Row():
783
+ tgt_file = gr.File(label="Upload TGT CSV", file_types=[".csv"], scale=3)
784
+ tgt_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
785
+ tgt_btn = gr.Button("Analyze TGT Data", variant="primary")
786
+ tgt_result = gr.Textbox(label="AI Assessment", lines=4)
787
+ with gr.Row():
788
+ tgt_c1=gr.Image(label="TAT",type="pil"); tgt_c2=gr.Image(label="PF1.2",type="pil")
789
+ with gr.Row():
790
+ tgt_c3=gr.Image(label="Hemoglobin",type="pil"); tgt_c4=gr.Image(label="Platelets",type="pil")
791
+ tgt_btn.click(analyze_tgt_csv, inputs=[tgt_file,tgt_theme], outputs=[tgt_c1,tgt_c2,tgt_c3,tgt_c4,tgt_result])
792
+
793
+ with gr.Tab("uPAD"):
794
+ with gr.Row():
795
+ with gr.Column():
796
+ photo_input = gr.Image(label="Upload uPAD Photo", type="numpy", height=260)
797
+ analyze_btn = gr.Button("Analyze uPAD", variant="primary")
798
+ with gr.Column():
799
+ photo_img = gr.Image(label="Detection Zone", type="pil", height=260)
800
+ photo_text = gr.Textbox(label="CKD Result", lines=8)
801
+ analyze_btn.click(analyze_upad_photo, inputs=photo_input, outputs=[photo_img, photo_text])
802
+ with gr.Row():
803
+ r=gr.Number(label="R",value=210); g=gr.Number(label="G",value=140); b=gr.Number(label="B",value=80)
804
+ out3=gr.Textbox(label="Result",lines=3)
805
+ gr.Button("Analyze RGB",variant="secondary").click(
806
+ 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"),
807
+ inputs=[r,g,b],outputs=out3)
808
+
809
+ with gr.Tab("AI Image"):
810
+ with gr.Row():
811
+ img_prompt = gr.Textbox(placeholder="e.g. 27mm bileaflet mechanical heart valve cross section", label="Describe image", lines=2, scale=4)
812
+ with gr.Column(scale=1):
813
+ img_btn = gr.Button("Generate", variant="primary")
814
+ img_status = gr.Textbox(label="Status", lines=1)
815
+ img_desc = gr.Textbox(label="AI Description", lines=2, interactive=False)
816
+ img_output = gr.Image(label="Generated Image", type="pil", height=400)
817
+ img_btn.click(generate_image, inputs=img_prompt, outputs=[img_output,img_status,img_desc])
818
+
819
+ with gr.Tab("PIV Manual"):
820
+ with gr.Row():
821
+ with gr.Column():
822
+ v=gr.Number(label="Max Velocity m/s",value=1.8); s=gr.Number(label="Wall Shear Pa",value=6.5)
823
+ h=gr.Number(label="Heart Rate bpm",value=72); piv_out=gr.Textbox(label="Result",lines=4)
824
+ gr.Button("Analyze PIV",variant="primary").click(piv_manual,inputs=[v,s,h],outputs=piv_out)
825
+
826
+ with gr.Tab("TGT Manual"):
827
+ with gr.Row():
828
+ with gr.Column():
829
+ t1=gr.Number(label="TAT ng/mL",value=18); t2=gr.Number(label="PF1.2",value=2.5)
830
+ t3=gr.Number(label="Hemoglobin mg/L",value=60); t4=gr.Number(label="Platelets",value=140)
831
+ t5=gr.Number(label="Time min",value=40); out2=gr.Textbox(label="Result",lines=6)
832
+ gr.Button("Analyze TGT",variant="primary").click(tgt_manual,inputs=[t1,t2,t3,t4,t5],outputs=out2)
833
+
834
+ gr.HTML("""<div style="text-align:center;padding:10px;border-top:1px solid #e5e7eb;background:#f9fafb;">
835
+ <span style="color:#9ca3af;font-size:0.75em;">CardioLab AI v37 | SJSU Biomedical Engineering | Fine-tuned on 16 SJSU Papers | RAG + Custom Model | Inspired by <a href="https://github.com/snap-stanford/Biomni" style="color:#c1121f;">Biomni Stanford</a> | <a href="https://github.com/pranatechsol/Cardio-Lab-Ai" style="color:#0057a8;">GitHub</a> | Apache 2.0 | $0 Cost</span></div>""")
836
+
837
+ demo.launch()