Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -12,131 +12,152 @@ from groq import Groq
|
|
| 12 |
from fpdf import FPDF
|
| 13 |
from datetime import datetime
|
| 14 |
|
| 15 |
-
# --- 1. CORE CONFIG ---
|
| 16 |
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
|
| 17 |
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
|
| 18 |
|
| 19 |
if not GROQ_API_KEY or not GOOGLE_API_KEY:
|
| 20 |
-
st.error("β οΈ API Keys Missing!")
|
| 21 |
st.stop()
|
| 22 |
|
| 23 |
genai.configure(api_key=GOOGLE_API_KEY)
|
| 24 |
vision_model = genai.GenerativeModel('gemini-2.0-flash')
|
| 25 |
|
| 26 |
-
st.set_page_config(page_title="MediScan Pro | Hassan Naseer", layout="wide")
|
| 27 |
-
|
| 28 |
-
# --- 2.
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
@st.cache_resource
|
| 37 |
-
def
|
| 38 |
client = chromadb.PersistentClient(path="./med_vector_db")
|
| 39 |
emb_fn = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
main_col, signal_col
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
pdf.add_page(); pdf.set_font("Arial", 'B', 16)
|
| 50 |
-
pdf.cell(200, 10, txt="NUTECH MEDISCAN CLINICAL REPORT", ln=True, align='C')
|
| 51 |
-
pdf.set_font("Arial", size=10); pdf.ln(10)
|
| 52 |
-
for m in history:
|
| 53 |
-
label = "PATIENT: " if m["role"] == "user" else "AI DOCTOR: "
|
| 54 |
-
pdf.set_font("Arial", 'B', 11); pdf.cell(200, 8, txt=label, ln=True)
|
| 55 |
-
pdf.set_font("Arial", size=10); pdf.multi_cell(0, 8, txt=m["content"].encode('latin-1', 'ignore').decode('latin-1'))
|
| 56 |
-
return pdf.output(dest='S').encode('latin-1')
|
| 57 |
-
|
| 58 |
-
# --- 4. AUTH & NAVIGATION ---
|
| 59 |
if "auth" not in st.session_state: st.session_state.auth = False
|
| 60 |
if "msgs" not in st.session_state: st.session_state.msgs = []
|
|
|
|
| 61 |
|
|
|
|
| 62 |
if not st.session_state.auth:
|
| 63 |
-
st.
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
st.stop()
|
| 70 |
|
|
|
|
| 71 |
with st.sidebar:
|
| 72 |
-
st.title(f"π€ {st.session_state.role}")
|
| 73 |
if st.button("Logout"): st.session_state.auth = False; st.rerun()
|
| 74 |
st.divider()
|
| 75 |
if st.session_state.role == "Patient":
|
| 76 |
-
nav = st.radio("
|
| 77 |
else:
|
| 78 |
-
nav = st.radio("
|
|
|
|
|
|
|
| 79 |
|
| 80 |
-
# ---
|
| 81 |
if st.session_state.role == "Patient":
|
| 82 |
-
if nav == "π¬ AI Chat":
|
| 83 |
-
st.header("π¬ AI Assistant
|
| 84 |
-
if st.session_state.msgs:
|
| 85 |
-
st.download_button("π Export Clinical PDF", data=generate_pro_pdf(st.session_state.msgs), file_name="MedReport.pdf")
|
| 86 |
for m in st.session_state.msgs: st.chat_message(m["role"]).write(m["content"])
|
| 87 |
|
| 88 |
-
q = st.chat_input("
|
| 89 |
c1, c2 = st.columns(2)
|
| 90 |
-
with c1:
|
| 91 |
-
with c2:
|
| 92 |
|
| 93 |
-
|
| 94 |
-
if
|
| 95 |
-
with st.spinner("
|
| 96 |
-
if
|
| 97 |
-
with pdfplumber.open(
|
| 98 |
-
else:
|
| 99 |
-
elif
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
| 106 |
ans = Groq(api_key=GROQ_API_KEY).chat.completions.create(model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": f"Context: {ctx}"}] + st.session_state.msgs)
|
| 107 |
st.session_state.msgs.append({"role": "assistant", "content": ans.choices[0].message.content})
|
| 108 |
st.rerun()
|
| 109 |
|
| 110 |
elif nav == "π§ͺ Diagnostics":
|
| 111 |
-
st.header("π§ͺ
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
room = f"NUTECH-{np.random.randint(100,999)}"
|
| 120 |
-
signal_col.upsert(documents=[f"CALL|{
|
| 121 |
-
st.session_state.
|
| 122 |
-
|
| 123 |
-
|
|
|
|
| 124 |
|
| 125 |
elif nav == "π My Records":
|
| 126 |
-
st.header("π Visit
|
| 127 |
-
if os.path.exists(
|
| 128 |
else: st.info("No records found.")
|
| 129 |
|
| 130 |
-
# ---
|
| 131 |
elif st.session_state.role == "Doctor":
|
| 132 |
-
if nav == "π₯οΈ
|
| 133 |
-
st.header("π₯οΈ
|
| 134 |
res = signal_col.get(ids=["latest"])
|
| 135 |
if res['documents']:
|
| 136 |
parts = res['documents'][0].split("|")
|
| 137 |
-
st.warning(f"π CALL:
|
| 138 |
-
if st.button("Join"):
|
| 139 |
st.components.v1.html(f'<iframe src="https://meet.jit.si/{parts[2]}" width="100%" height="600px" allow="camera; microphone;"></iframe>', height=650)
|
| 140 |
-
if st.button("
|
| 141 |
-
|
| 142 |
-
signal_col.delete(ids=["latest"]); st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
from fpdf import FPDF
|
| 13 |
from datetime import datetime
|
| 14 |
|
| 15 |
+
# --- 1. CORE SYSTEM CONFIG ---
|
| 16 |
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
|
| 17 |
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
|
| 18 |
|
| 19 |
if not GROQ_API_KEY or not GOOGLE_API_KEY:
|
| 20 |
+
st.error("β οΈ API Keys Missing! Please add them to Space Secrets.")
|
| 21 |
st.stop()
|
| 22 |
|
| 23 |
genai.configure(api_key=GOOGLE_API_KEY)
|
| 24 |
vision_model = genai.GenerativeModel('gemini-2.0-flash')
|
| 25 |
|
| 26 |
+
st.set_page_config(page_title="MediScan Pro | Hassan Naseer", layout="wide", page_icon="π₯")
|
| 27 |
+
|
| 28 |
+
# --- 2. PERSISTENT DATABASE ENGINE ---
|
| 29 |
+
# Shared file for both Doctor and Patient portals
|
| 30 |
+
RECORDS_FILE = "clinical_records.csv"
|
| 31 |
+
|
| 32 |
+
def save_clinical_record(doctor, room, chat_history):
|
| 33 |
+
# Convert chat history into a readable summary string
|
| 34 |
+
chat_summary = " | ".join([f"{m['role'].upper()}: {m['content'][:50]}..." for m in chat_history])
|
| 35 |
+
df = pd.read_csv(RECORDS_FILE) if os.path.exists(RECORDS_FILE) else pd.DataFrame(columns=["Timestamp", "Doctor", "RoomID", "Summary"])
|
| 36 |
+
|
| 37 |
+
new_entry = pd.DataFrame([{
|
| 38 |
+
"Timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
| 39 |
+
"Doctor": doctor,
|
| 40 |
+
"RoomID": room,
|
| 41 |
+
"Summary": chat_summary
|
| 42 |
+
}])
|
| 43 |
+
pd.concat([df, new_entry]).to_csv(RECORDS_FILE, index=False)
|
| 44 |
|
| 45 |
@st.cache_resource
|
| 46 |
+
def get_chroma_db():
|
| 47 |
client = chromadb.PersistentClient(path="./med_vector_db")
|
| 48 |
emb_fn = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")
|
| 49 |
+
# Medical Knowledge Base
|
| 50 |
+
main_col = client.get_or_create_collection(name="nutech_final_prod", embedding_function=emb_fn)
|
| 51 |
+
# Live Signaling for Video Calls
|
| 52 |
+
signal_col = client.get_or_create_collection(name="call_signals_prod", embedding_function=emb_fn)
|
| 53 |
+
return main_col, signal_col
|
| 54 |
+
|
| 55 |
+
main_col, signal_col = get_chroma_db()
|
| 56 |
+
|
| 57 |
+
# --- 3. SESSION STATE ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
if "auth" not in st.session_state: st.session_state.auth = False
|
| 59 |
if "msgs" not in st.session_state: st.session_state.msgs = []
|
| 60 |
+
if "v_hash" not in st.session_state: st.session_state.v_hash = None
|
| 61 |
|
| 62 |
+
# --- 4. AUTHENTICATION GATE ---
|
| 63 |
if not st.session_state.auth:
|
| 64 |
+
st.markdown("<h1 style='text-align: center;'>π₯ MediScan Enterprise</h1>", unsafe_allow_html=True)
|
| 65 |
+
c1, c2, c3 = st.columns([1, 1.5, 1])
|
| 66 |
+
with c2:
|
| 67 |
+
st.markdown('<div style="background:white; padding:20px; border-radius:10px; border:1px solid #ddd;">', unsafe_allow_html=True)
|
| 68 |
+
role = st.selectbox("I am a:", ["Patient", "Doctor"])
|
| 69 |
+
pwd = st.text_input("Access Key", type="password")
|
| 70 |
+
if st.button("Enter Portal"):
|
| 71 |
+
if (role == "Patient" and pwd == "p123") or (role == "Doctor" and pwd == "d123"):
|
| 72 |
+
st.session_state.auth = True; st.session_state.role = role; st.rerun()
|
| 73 |
+
else: st.error("Incorrect Password.")
|
| 74 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 75 |
st.stop()
|
| 76 |
|
| 77 |
+
# --- 5. NAVIGATION ---
|
| 78 |
with st.sidebar:
|
| 79 |
+
st.title(f"π€ {st.session_state.role} Portal")
|
| 80 |
if st.button("Logout"): st.session_state.auth = False; st.rerun()
|
| 81 |
st.divider()
|
| 82 |
if st.session_state.role == "Patient":
|
| 83 |
+
nav = st.radio("Services", ["π¬ AI Chat & Docs", "π§ͺ Diagnostics", "π Contact Doctor", "πΈ Vision Scanner", "π My Records"])
|
| 84 |
else:
|
| 85 |
+
nav = st.radio("Doctor Desk", ["π₯οΈ Live Tele-Consult", "π Global Patient Logs"])
|
| 86 |
+
st.divider()
|
| 87 |
+
st.info("System Status: Synchronized (V3.0)")
|
| 88 |
|
| 89 |
+
# --- 6. PATIENT PORTAL MODULE ---
|
| 90 |
if st.session_state.role == "Patient":
|
| 91 |
+
if nav == "π¬ AI Chat & Docs":
|
| 92 |
+
st.header("π¬ Clinical AI Assistant")
|
|
|
|
|
|
|
| 93 |
for m in st.session_state.msgs: st.chat_message(m["role"]).write(m["content"])
|
| 94 |
|
| 95 |
+
q = st.chat_input("Explain your symptoms...")
|
| 96 |
c1, c2 = st.columns(2)
|
| 97 |
+
with c1: v_in = st.audio_input("Voice Input", key=f"v_{len(st.session_state.msgs)}")
|
| 98 |
+
with c2: doc_in = st.file_uploader("Upload Lab Report (PDF/IMG)", type=['pdf', 'png', 'jpg'])
|
| 99 |
|
| 100 |
+
final_q = q if q else None
|
| 101 |
+
if doc_in:
|
| 102 |
+
with st.spinner("Analyzing Document..."):
|
| 103 |
+
if doc_in.type == "application/pdf":
|
| 104 |
+
with pdfplumber.open(doc_in) as pdf: final_q = "Summarize: " + " ".join([p.extract_text() for p in pdf.pages if p.extract_text()])
|
| 105 |
+
else: final_q = "Analyze Scan: " + " ".join(easyocr.Reader(['en']).readtext(np.array(Image.open(doc_in)), detail=0))
|
| 106 |
+
elif v_in and v_in.size > 0:
|
| 107 |
+
vh = hash(v_in.getvalue())
|
| 108 |
+
if st.session_state.v_hash != vh:
|
| 109 |
+
final_q = Groq(api_key=GROQ_API_KEY).audio.transcriptions.create(file=("a.wav", v_in.getvalue()), model="whisper-large-v3", response_format="text")
|
| 110 |
+
st.session_state.v_hash = vh
|
| 111 |
+
|
| 112 |
+
if final_q:
|
| 113 |
+
st.session_state.msgs.append({"role": "user", "content": final_q})
|
| 114 |
+
res = main_col.query(query_texts=[final_q], n_results=1)
|
| 115 |
+
ctx = res['documents'][0][0] if res['documents'] else "N/A"
|
| 116 |
ans = Groq(api_key=GROQ_API_KEY).chat.completions.create(model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": f"Context: {ctx}"}] + st.session_state.msgs)
|
| 117 |
st.session_state.msgs.append({"role": "assistant", "content": ans.choices[0].message.content})
|
| 118 |
st.rerun()
|
| 119 |
|
| 120 |
elif nav == "π§ͺ Diagnostics":
|
| 121 |
+
st.header("π§ͺ Interactive Health Metrics")
|
| 122 |
+
col1, col2 = st.columns(2)
|
| 123 |
+
with col1:
|
| 124 |
+
bpm = st.slider("Current Heart Rate (BPM)", 40, 200, 72); st.metric("Pulse", bpm)
|
| 125 |
+
with col2:
|
| 126 |
+
sugar = st.slider("Blood Glucose (mg/dL)", 50, 400, 95); st.metric("Glucose", sugar)
|
| 127 |
+
st.image("https://www.verywellfit.com/thmb/p2O_L3uS9m7O3r4E1uG2F3G_Y3I=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/HeartRateChart-5b8d5a1bc9e77c00507a2a1a.jpg", use_container_width=True)
|
| 128 |
+
|
| 129 |
+
elif nav == "π Contact Doctor":
|
| 130 |
+
st.header("π Start Video Consultation")
|
| 131 |
+
doc_name = st.selectbox("Available Specialist", ["Dr. Ahmed", "Dr. Sara"])
|
| 132 |
+
if st.button("Request Meeting"):
|
| 133 |
room = f"NUTECH-{np.random.randint(100,999)}"
|
| 134 |
+
signal_col.upsert(documents=[f"CALL|{doc_name}|{room}|{datetime.now().strftime('%H:%M')}"], ids=["latest"])
|
| 135 |
+
st.session_state.active_room = room
|
| 136 |
+
st.success("Calling Specialist...")
|
| 137 |
+
if "active_room" in st.session_state:
|
| 138 |
+
st.components.v1.html(f'<iframe src="https://meet.jit.si/{st.session_state.active_room}" width="100%" height="550px" allow="camera; microphone;"></iframe>', height=600)
|
| 139 |
|
| 140 |
elif nav == "π My Records":
|
| 141 |
+
st.header("π My Permanent Visit Records")
|
| 142 |
+
if os.path.exists(RECORDS_FILE): st.table(pd.read_csv(RECORDS_FILE))
|
| 143 |
else: st.info("No records found.")
|
| 144 |
|
| 145 |
+
# --- 7. DOCTOR PORTAL MODULE ---
|
| 146 |
elif st.session_state.role == "Doctor":
|
| 147 |
+
if nav == "π₯οΈ Live Tele-Consult":
|
| 148 |
+
st.header("π₯οΈ Consultation Dashboard")
|
| 149 |
res = signal_col.get(ids=["latest"])
|
| 150 |
if res['documents']:
|
| 151 |
parts = res['documents'][0].split("|")
|
| 152 |
+
st.warning(f"π INCOMING CALL: {parts[1]} is requesting a session.")
|
| 153 |
+
if st.button("Accept & Join"):
|
| 154 |
st.components.v1.html(f'<iframe src="https://meet.jit.si/{parts[2]}" width="100%" height="600px" allow="camera; microphone;"></iframe>', height=650)
|
| 155 |
+
if st.button("β
Archive Session & Save Patient Logs"):
|
| 156 |
+
save_clinical_record(parts[1], parts[2], st.session_state.msgs)
|
| 157 |
+
signal_col.delete(ids=["latest"]); st.success("Session Archived!"); st.rerun()
|
| 158 |
+
else: st.info("Waiting for patient requests...")
|
| 159 |
+
|
| 160 |
+
elif nav == "π Global Patient Logs":
|
| 161 |
+
st.header("π Global Consultation History")
|
| 162 |
+
if os.path.exists(RECORDS_FILE): st.dataframe(pd.read_csv(RECORDS_FILE), use_container_width=True)
|
| 163 |
+
else: st.info("No global records found.")
|