Eslam Waleed commited on
Commit
a8ee124
·
0 Parent(s):

Initial Release: MediChat AI Dashboard

Browse files
Files changed (5) hide show
  1. .gitattributes +35 -0
  2. .streamlit/config.toml +0 -0
  3. README.md +25 -0
  4. requirements.txt +6 -0
  5. src/streamlit_app.py +311 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.streamlit/config.toml ADDED
File without changes
README.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: MediChatAI
3
+ emoji: 🚀
4
+ colorFrom: red
5
+ colorTo: red
6
+ sdk: streamlit
7
+ app_file: src/streamlit_app.py
8
+ tags:
9
+ - streamlit
10
+ pinned: false
11
+ short_description: ' an intelligent medical report analyzer'
12
+ ---
13
+
14
+ # 🏥 MediChat AI: Analysis Dashboard
15
+
16
+ Welcome to the MediChat AI Medical Report Analyzer!
17
+
18
+ This application uses advanced Optical Character Recognition (OCR) and the Mistral-7B Large Language Model to extract data from medical reports and provide instant, structured analysis.
19
+
20
+ ## Features
21
+ * **Instant Diagnosis:** Upload a PDF or Image of a medical report for immediate processing.
22
+ * **Smart Extraction:** Automatically flags critical severity levels and abnormal findings.
23
+ * **Interactive Chat:** Ask specific follow-up questions about the patient's data using the Doctor's Companion Chat.
24
+
25
+ *(Note: Edit `/src/streamlit_app.py` to customize the core application logic.)*
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ streamlit
2
+ easyocr
3
+ Pillow
4
+ huggingface_hub
5
+ numpy
6
+ pypdf
src/streamlit_app.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ import json
4
+ import numpy as np
5
+ import easyocr
6
+ from PIL import Image
7
+ from huggingface_hub import InferenceClient
8
+ import re
9
+ from collections import Counter
10
+ import math
11
+
12
+ # Try to import pypdf for PDF support
13
+ try:
14
+ from pypdf import PdfReader
15
+ PDF_SUPPORT = True
16
+ except ImportError:
17
+ PDF_SUPPORT = False
18
+
19
+ # --- 1. PAGE CONFIGURATION & THEME ---
20
+ st.set_page_config(page_title="MediChat AI", layout="wide", page_icon="🏥")
21
+
22
+ # Custom CSS for Professional Medical Dashboard
23
+ st.markdown("""
24
+ <style>
25
+ .stApp { background-color: #f8f9fa; color: #2c3e50; }
26
+ h1, h2, h3 { color: #1b5e20 !important; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-weight: 600; }
27
+ label[data-testid="stWidgetLabel"] { color: #2c3e50 !important; font-weight: 600; }
28
+ [data-testid="stFileUploader"] { color: #2c3e50 !important; }
29
+ [data-testid="stFileUploader"] * { color: #2c3e50 !important; }
30
+ [data-testid="stFileUploader"] small { color: #555555 !important; }
31
+ .medical-card { background-color: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); margin-bottom: 20px; border: 1px solid #e0e0e0; }
32
+ .diagnosis { border-left: 6px solid #1976d2; }
33
+ .advice { border-left: 6px solid #2e7d32; }
34
+ .warning { border-left: 6px solid #d32f2f; }
35
+ .card-label { color: #7f8c8d; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; font-weight: bold; }
36
+ .card-content { color: #2c3e50; font-size: 1.1rem; line-height: 1.6; }
37
+ div[data-testid="stFileUploader"] { background-color: white; border: 2px dashed #a5d6a7; border-radius: 12px; padding: 30px; }
38
+ div.stButton > button:first-child { background-color: #2e7d32; color: white; border-radius: 8px; border: none; padding: 0.6rem 1.2rem; font-size: 1rem; font-weight: 600; box-shadow: 0 2px 4px rgba(0,0,0,0.1); transition: all 0.2s ease; }
39
+ div.stButton > button:first-child:hover { background-color: #1b5e20; box-shadow: 0 4px 8px rgba(0,0,0,0.2); transform: translateY(-1px); }
40
+ div[data-testid="stChatMessage"] { background-color: white; border: 1px solid #e0e0e0; border-radius: 12px; padding: 15px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.02); }
41
+ div[data-testid="stChatMessage"] p, div[data-testid="stChatMessage"] div { color: #2c3e50 !important; }
42
+ div[data-testid="stChatMessage"][data-testid*="user"] { background-color: #f1f8e9; }
43
+ </style>
44
+ """, unsafe_allow_html=True)
45
+
46
+ # --- 2. SECURE BACKGROUND SETUP ---
47
+ # Fetch the token securely from Hugging Face Settings -> Secrets. No UI needed!
48
+ api_key = os.environ.get("HUGGINGFACEHUB_API_TOKEN", "")
49
+
50
+ # --- 3. LOGIC FUNCTIONS ---
51
+ @st.cache_resource
52
+ def load_ocr_reader():
53
+ return easyocr.Reader(['en'], gpu=False)
54
+
55
+ def extract_text(uploaded_file):
56
+ text = ""
57
+ try:
58
+ if uploaded_file.type in ["image/jpeg", "image/png", "image/jpg"]:
59
+ image = Image.open(uploaded_file)
60
+ image_np = np.array(image)
61
+ reader = load_ocr_reader()
62
+ result = reader.readtext(image_np, detail=0)
63
+ text = " ".join(result)
64
+ elif uploaded_file.type == "application/pdf":
65
+ if PDF_SUPPORT:
66
+ reader = PdfReader(uploaded_file)
67
+ for page in reader.pages:
68
+ text += page.extract_text() or ""
69
+ else:
70
+ return "Error: PDF support requires 'pypdf'."
71
+ except Exception as e:
72
+ return f"Error processing file: {e}"
73
+ return text
74
+
75
+ def analyze_report(text, client):
76
+ if not text: return {"error": "No text extracted"}
77
+
78
+ system_prompt = """
79
+ Analyze this medical report. Return valid JSON only.
80
+ Keys: "severity" (CRITICAL, MODERATE, NORMAL), "diagnosis_hypothesis", "abnormal_values" (list of strings), "medical_advice".
81
+
82
+ IMPORTANT INSTRUCTION:
83
+ For "abnormal_values", list EVERY abnormal finding found in the document.
84
+ FORMAT: "Condition/Test Name: Value (Reference Range if available)" or "Specific Finding".
85
+ EXAMPLE: ["Hemoglobin: 7.2 g/dL (Low)", "ECG: Sinus Tachycardia", "Potassium: 5.8 mmol/L (High)"].
86
+ Do not summarize vaguely. Be exact and exhaustive.
87
+ """
88
+
89
+ try:
90
+ response = client.chat_completion(
91
+ messages=[
92
+ {"role": "system", "content": system_prompt},
93
+ {"role": "user", "content": f"REPORT: {text[:3000]}"}
94
+ ],
95
+ model="Qwen/Qwen2.5-7B-Instruct", # Swap to "aaditya/Llama3-OpenBioLLM-8B" here if you want to test it!
96
+ max_tokens=1500,
97
+ temperature=0.1
98
+ )
99
+ raw = response.choices[0].message.content
100
+ clean = raw.replace("```json", "").replace("```", "").strip()
101
+
102
+ try:
103
+ return json.loads(clean)
104
+ except json.JSONDecodeError:
105
+ start = clean.find('{')
106
+ end = clean.rfind('}') + 1
107
+ if start != -1 and end != 0:
108
+ try: return json.loads(clean[start:end])
109
+ except: pass
110
+
111
+ fallback = {
112
+ "severity": "UNKNOWN",
113
+ "diagnosis_hypothesis": "Not found",
114
+ "abnormal_values": [],
115
+ "medical_advice": "Not found"
116
+ }
117
+
118
+ sev_match = re.search(r'"severity":\s*"([^"]+)"', clean, re.IGNORECASE)
119
+ diag_match = re.search(r'"diagnosis_hypothesis":\s*"([^"]+)"', clean, re.IGNORECASE)
120
+ advice_match = re.search(r'"medical_advice":\s*"([^"]+)"', clean, re.IGNORECASE)
121
+
122
+ if sev_match: fallback["severity"] = sev_match.group(1)
123
+ if diag_match: fallback["diagnosis_hypothesis"] = diag_match.group(1)
124
+ if advice_match: fallback["medical_advice"] = advice_match.group(1)
125
+
126
+ abnormal_match = re.search(r'"abnormal_values":\s*\[(.*?)\]', clean, re.DOTALL)
127
+ if abnormal_match:
128
+ values = re.findall(r'"([^"]+)"', abnormal_match.group(1))
129
+ fallback["abnormal_values"] = values
130
+
131
+ if fallback["diagnosis_hypothesis"] != "Not found":
132
+ return fallback
133
+
134
+ return {
135
+ "severity": "UNKNOWN",
136
+ "diagnosis_hypothesis": "AI Analysis Error: JSON formatting failed.",
137
+ "abnormal_values": ["Raw output could not be parsed"],
138
+ "medical_advice": f"Raw Output Preview: {clean[:100]}..."
139
+ }
140
+
141
+ except Exception as e:
142
+ return {"error": str(e)}
143
+
144
+ def simple_text_splitter(text, chunk_size=500, overlap=50):
145
+ chunks = []
146
+ start = 0
147
+ while start < len(text):
148
+ end = min(start + chunk_size, len(text))
149
+ chunks.append(text[start:end])
150
+ start += (chunk_size - overlap)
151
+ return chunks
152
+
153
+ def text_to_vector(text):
154
+ words = re.compile(r'\w+').findall(text.lower())
155
+ return Counter(words)
156
+
157
+ def get_cosine_similarity(vec1, vec2):
158
+ intersection = set(vec1.keys()) & set(vec2.keys())
159
+ numerator = sum([vec1[x] * vec2[x] for x in intersection])
160
+ sum1 = sum([vec1[x]**2 for x in vec1.keys()])
161
+ sum2 = sum([vec2[x]**2 for x in vec2.keys()])
162
+ denominator = math.sqrt(sum1) * math.sqrt(sum2)
163
+ return float(numerator) / denominator if denominator else 0.0
164
+
165
+ def ask_chatbot(question, chunks, client):
166
+ question_vec = text_to_vector(question)
167
+ scores = []
168
+ for i, chunk in enumerate(chunks):
169
+ chunk_vec = text_to_vector(chunk)
170
+ score = get_cosine_similarity(question_vec, chunk_vec)
171
+ scores.append((score, i))
172
+
173
+ scores.sort(key=lambda x: x[0], reverse=True)
174
+ top_indices = [idx for score, idx in scores[:3]]
175
+ context = "\n...\n".join([chunks[i] for i in top_indices])
176
+
177
+ sys_p = """
178
+ You are a Medical Assistant. Your job is to answer the user's question using EITHER the report data OR general medical knowledge.
179
+
180
+ INSTRUCTIONS:
181
+ 1. Check the "REPORT CONTEXT". Does it have the answer?
182
+ - YES -> Answer strictly based on the report. Start with "Based on your report..."
183
+ - NO -> Do NOT say "I don't know." Instead, switch to GENERAL ADVICE MODE. Start with "Your report doesn't specify this, but generally..." and provide ONE concise sentence of standard medical advice for the conditions found in the text.
184
+
185
+ CONSTRAINT: Keep your answer to ONE or TWO sentences max.
186
+ """
187
+
188
+ user_p = f"REPORT CONTEXT:\n{context}\n\nUSER QUESTION:\n{question}"
189
+
190
+ try:
191
+ resp = client.chat_completion(
192
+ messages=[{"role": "system", "content": sys_p}, {"role": "user", "content": user_p}],
193
+ model="Qwen/Qwen2.5-7B-Instruct", # Swap to "aaditya/Llama3-OpenBioLLM-8B" here if you want to test it!
194
+ max_tokens=600,
195
+ temperature=0.7
196
+ )
197
+ return resp.choices[0].message.content
198
+ except Exception as e:
199
+ return f"Error: {e}"
200
+
201
+ # --- 5. MAIN UI LAYOUT ---
202
+
203
+ st.title("🏥 MediChat AI: Analysis Dashboard")
204
+
205
+ # Put a tiny, clean status indicator in the sidebar instead of a giant drop-down menu
206
+ with st.sidebar:
207
+ if api_key:
208
+ st.success("✅ Secure AI Connection Ready")
209
+ st.caption("Engine: EasyOCR + Qwen 2.5 AI")
210
+ else:
211
+ st.error("❌ Setup Required: Please add your Hugging Face Token to the Space Settings -> Secrets tab.")
212
+
213
+ st.markdown("### Upload Report for Instant AI Diagnosis")
214
+
215
+ uploaded_file = st.file_uploader("Upload Medical Record (PDF/Image)", type=["jpg", "png", "jpeg", "pdf"])
216
+
217
+ if uploaded_file and api_key:
218
+ client = InferenceClient(token=api_key)
219
+
220
+ if "analysis" not in st.session_state: st.session_state.analysis = None
221
+ if "chunks" not in st.session_state: st.session_state.chunks = []
222
+
223
+ if st.button("Run Analysis ⚡"):
224
+ st.session_state.analysis = None
225
+ st.session_state.chunks = []
226
+
227
+ with st.spinner("Processing medical data..."):
228
+ raw_text = extract_text(uploaded_file)
229
+
230
+ if len(raw_text) < 50:
231
+ st.warning("⚠️ Low text quality detected. If this is a scanned PDF, please convert it to an Image (JPG/PNG) for accurate results.")
232
+
233
+ if len(raw_text) < 5:
234
+ st.error("Could not read text. Please try a clearer file or convert PDF to Image.")
235
+ else:
236
+ st.session_state.analysis = analyze_report(raw_text, client)
237
+ st.session_state.chunks = simple_text_splitter(raw_text)
238
+
239
+ # --- RESULTS DASHBOARD ---
240
+ if st.session_state.analysis:
241
+ res = st.session_state.analysis
242
+
243
+ if "error" in res:
244
+ st.error(f"🚨 AI API Error: {res['error']}")
245
+
246
+ else:
247
+ sev = res.get("severity", "UNKNOWN").upper()
248
+
249
+ if "CRITICAL" in sev:
250
+ status_color = "#d32f2f"
251
+ status_icon = "🚨"
252
+ elif "MODERATE" in sev:
253
+ status_color = "#f57c00"
254
+ status_icon = "⚠️"
255
+ else:
256
+ status_color = "#2e7d32"
257
+ status_icon = "✅"
258
+
259
+ st.markdown("---")
260
+ st.markdown(f"<h3 style='color:{status_color}!important'>{status_icon} Analysis Result: {sev}</h3>", unsafe_allow_html=True)
261
+
262
+ col1, col2 = st.columns(2)
263
+
264
+ with col1:
265
+ st.markdown(f"""
266
+ <div class="medical-card diagnosis">
267
+ <div class="card-label">Primary Diagnosis</div>
268
+ <div class="card-content">{res.get('diagnosis_hypothesis', 'Analysis pending...')}</div>
269
+ </div>
270
+ """, unsafe_allow_html=True)
271
+
272
+ with col2:
273
+ st.markdown(f"""
274
+ <div class="medical-card advice">
275
+ <div class="card-label">Recommended Action</div>
276
+ <div class="card-content">{res.get('medical_advice', 'Consult a doctor.')}</div>
277
+ </div>
278
+ """, unsafe_allow_html=True)
279
+
280
+ if "abnormal_values" in res and isinstance(res["abnormal_values"], list) and len(res["abnormal_values"]) > 0:
281
+ findings_html = "".join([f"<li style='margin-bottom:5px;'>{val}</li>" for val in res["abnormal_values"]])
282
+ st.markdown(f"""
283
+ <div class="medical-card warning">
284
+ <div class="card-label" style="color:#d32f2f;">⚠️ Critical Findings</div>
285
+ <ul style="margin-bottom:0; padding-left:20px;">
286
+ {findings_html}
287
+ </ul>
288
+ </div>
289
+ """, unsafe_allow_html=True)
290
+ else:
291
+ st.success("No specific abnormal values detected in the extraction.")
292
+
293
+ # --- CHAT SECTION ---
294
+ if st.session_state.chunks:
295
+ st.markdown("---")
296
+ st.subheader("💬 Doctor's Companion Chat")
297
+
298
+ if "messages" not in st.session_state: st.session_state.messages = []
299
+
300
+ for msg in st.session_state.messages:
301
+ with st.chat_message(msg["role"]): st.write(msg["content"])
302
+
303
+ if prompt := st.chat_input("Ask specific questions about this patient..."):
304
+ st.session_state.messages.append({"role": "user", "content": prompt})
305
+ with st.chat_message("user"): st.write(prompt)
306
+
307
+ with st.chat_message("assistant"):
308
+ with st.spinner("Reviewing case notes..."):
309
+ ans = ask_chatbot(prompt, st.session_state.chunks, client)
310
+ st.write(ans)
311
+ st.session_state.messages.append({"role": "assistant", "content": ans})