Spaces:
Sleeping
Sleeping
Eslam Waleed commited on
Commit ·
a8ee124
0
Parent(s):
Initial Release: MediChat AI Dashboard
Browse files- .gitattributes +35 -0
- .streamlit/config.toml +0 -0
- README.md +25 -0
- requirements.txt +6 -0
- 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})
|