Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
import os
|
| 2 |
-
# import base64 # No longer needed for Gemini vision part
|
| 3 |
import gradio as gr
|
| 4 |
import pandas as pd
|
| 5 |
from groq import Groq
|
|
@@ -9,45 +8,48 @@ import datetime
|
|
| 9 |
import re
|
| 10 |
import google.generativeai as genai # Added for Gemini
|
| 11 |
from google.generativeai import types # Added for Gemini
|
|
|
|
| 12 |
|
| 13 |
# Initialize Groq client (for chat/assessment)
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
)
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
# NOTE: Gemini client initialization will happen inside the analyze_ecg_image function
|
| 19 |
# Ensure GEMINI_API_KEY is set in your environment variables
|
| 20 |
|
| 21 |
-
#
|
| 22 |
-
# def encode_image(image):
|
| 23 |
-
# buffered = io.BytesIO()
|
| 24 |
-
# image.save(buffered, format="JPEG")
|
| 25 |
-
# return base64.b64encode(buffered.getvalue()).decode('utf-8')
|
| 26 |
-
|
| 27 |
-
# Process patient history file (Unchanged)
|
| 28 |
def process_patient_history(file):
|
| 29 |
if file is None:
|
| 30 |
return ""
|
| 31 |
|
| 32 |
try:
|
| 33 |
# Check file extension
|
| 34 |
-
|
|
|
|
| 35 |
|
| 36 |
if file_ext == '.txt':
|
| 37 |
# Read text file
|
| 38 |
-
|
| 39 |
-
with open(file.name, 'r', encoding='utf-8') as f:
|
| 40 |
content = f.read()
|
| 41 |
-
# If file is already an IO object (depends on Gradio version/usage)
|
| 42 |
-
# content = file.read().decode('utf-8')
|
| 43 |
return content
|
| 44 |
|
| 45 |
elif file_ext in ['.csv', '.xlsx', '.xls']:
|
| 46 |
# Read spreadsheet file
|
| 47 |
if file_ext == '.csv':
|
| 48 |
-
df = pd.read_csv(
|
| 49 |
else:
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
# Convert dataframe to formatted string
|
| 53 |
formatted_data = "PATIENT INFORMATION:\n\n"
|
|
@@ -56,16 +58,22 @@ def process_patient_history(file):
|
|
| 56 |
for column in df.columns:
|
| 57 |
# Handle potential missing values gracefully
|
| 58 |
value = df.iloc[0].get(column, 'N/A')
|
| 59 |
-
|
|
|
|
| 60 |
else:
|
| 61 |
formatted_data += "Spreadsheet is empty or format is not recognized correctly."
|
| 62 |
|
| 63 |
return formatted_data
|
| 64 |
|
| 65 |
else:
|
| 66 |
-
return "Unsupported file format. Please upload a .txt, .csv, or .xlsx file."
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
except Exception as e:
|
|
|
|
| 69 |
return f"Error processing patient history file: {str(e)}"
|
| 70 |
|
| 71 |
|
|
@@ -80,11 +88,22 @@ def analyze_ecg_image(image, vision_model="gemini-2.0-flash-exp"):
|
|
| 80 |
# Ensure image is PIL Image
|
| 81 |
if not isinstance(image, Image.Image):
|
| 82 |
try:
|
| 83 |
-
image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
except Exception as e:
|
|
|
|
| 85 |
return f"<strong style='color:red'>Error opening image: {str(e)}</strong>"
|
| 86 |
|
| 87 |
# --- Gemini Specific Part ---
|
|
|
|
| 88 |
try:
|
| 89 |
# Get Gemini API Key
|
| 90 |
gemini_api_key = os.environ.get("GEMINI_API_KEY")
|
|
@@ -92,18 +111,22 @@ def analyze_ecg_image(image, vision_model="gemini-2.0-flash-exp"):
|
|
| 92 |
return "<strong style='color:red'>GEMINI_API_KEY environment variable not set.</strong>"
|
| 93 |
|
| 94 |
# Initialize Gemini client
|
| 95 |
-
#
|
| 96 |
-
# genai.configure(api_key=gemini_api_key)
|
| 97 |
-
# model = genai.GenerativeModel(model_name=vision_model)
|
| 98 |
gemini_client = genai.Client(api_key=gemini_api_key)
|
| 99 |
|
| 100 |
-
|
| 101 |
# Convert PIL image to bytes (JPEG format)
|
| 102 |
buffered = io.BytesIO()
|
| 103 |
# Ensure image is in RGB format if it's RGBA or P which might cause issues
|
|
|
|
| 104 |
if image.mode in ('RGBA', 'P'):
|
| 105 |
image = image.convert('RGB')
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
image_bytes = buffered.getvalue()
|
| 108 |
|
| 109 |
# Get current timestamp (computer time)
|
|
@@ -113,52 +136,61 @@ def analyze_ecg_image(image, vision_model="gemini-2.0-flash-exp"):
|
|
| 113 |
vision_prompt = f"""Analyze this ECG image carefully. You are a cardiologist analyzing an electrocardiogram (ECG).
|
| 114 |
|
| 115 |
Extract and report all visible parameters, including but not limited to:
|
| 116 |
-
1. Heart rate
|
| 117 |
-
2. PR interval
|
| 118 |
-
3. QRS duration
|
| 119 |
-
4. QT/QTc interval
|
| 120 |
-
5. P wave morphology
|
| 121 |
-
6. ST segment changes
|
| 122 |
-
7. T wave morphology
|
| 123 |
-
8. Rhythm classification
|
| 124 |
-
9.
|
|
|
|
| 125 |
|
| 126 |
-
Report exact numerical values where visible. Format your response using HTML list elements for better readability.
|
| 127 |
|
| 128 |
-
If certain measurements aren't visible
|
| 129 |
|
| 130 |
-
If you notice any abnormalities or concerning patterns, highlight them clearly but avoid making definitive diagnoses.
|
| 131 |
|
| 132 |
-
Format your response like this:
|
| 133 |
<h3>ECG Report</h3>
|
| 134 |
<ul>
|
| 135 |
-
<li><strong>
|
| 136 |
-
<li><strong>Heart Rate:</strong> [value] bpm</li>
|
| 137 |
-
<li><strong>
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
</ul>
|
| 140 |
|
| 141 |
-
<h3>
|
| 142 |
<ul>
|
| 143 |
-
<li>Observation 1</li>
|
| 144 |
-
<li>Observation 2</li>
|
|
|
|
| 145 |
</ul>
|
| 146 |
|
| 147 |
-
<h3>
|
| 148 |
-
<p>[
|
| 149 |
|
| 150 |
Important formatting instructions:
|
| 151 |
-
- Use
|
| 152 |
-
- Do
|
| 153 |
-
- For
|
| 154 |
"""
|
| 155 |
|
| 156 |
# Generate content using Gemini
|
| 157 |
response = gemini_client.models.generate_content(
|
| 158 |
model=vision_model,
|
| 159 |
contents=[vision_prompt,
|
| 160 |
-
types.Part.from_bytes(data=image_bytes, mime_type="image/
|
| 161 |
-
# Add safety_settings if needed:
|
| 162 |
# safety_settings=[
|
| 163 |
# {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
|
| 164 |
# {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
|
|
@@ -169,174 +201,141 @@ def analyze_ecg_image(image, vision_model="gemini-2.0-flash-exp"):
|
|
| 169 |
|
| 170 |
# Handle potential blocks or errors in response
|
| 171 |
if not response.candidates:
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
if
|
| 175 |
-
return f"<strong style='color:red'>Analysis blocked
|
| 176 |
else:
|
| 177 |
-
|
|
|
|
|
|
|
| 178 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
-
# Assuming the first candidate has the content
|
| 181 |
-
ecg_analysis = response.text # Or response.candidates[0].content.parts[0].text
|
| 182 |
|
| 183 |
# --- End of Gemini Specific Part ---
|
| 184 |
|
| 185 |
-
#
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
-
#
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
lines = ecg_analysis.split('\n')
|
| 194 |
-
formatted_html = "<h3>ECG Report</h3>\n<ul>\n"
|
| 195 |
-
|
| 196 |
-
for line in lines:
|
| 197 |
-
if line.strip():
|
| 198 |
-
# Try to identify key-value pairs
|
| 199 |
-
match = re.match(r'^([^:]+):\s*(.+)$', line.strip())
|
| 200 |
-
if match:
|
| 201 |
-
key, value = match.groups()
|
| 202 |
-
formatted_html += f" <li><strong>{key.strip()}:</strong> {value.strip()}</li>\n"
|
| 203 |
-
else:
|
| 204 |
-
formatted_html += f" <li>{line.strip()}</li>\n"
|
| 205 |
-
|
| 206 |
-
formatted_html += "</ul>"
|
| 207 |
-
ecg_analysis = formatted_html
|
| 208 |
|
| 209 |
return ecg_analysis
|
| 210 |
|
| 211 |
except Exception as e:
|
| 212 |
# Catch potential API errors or other issues
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
return f"<strong style='color:red'>Error analyzing ECG image with Gemini:</strong> {str(e)}"
|
|
|
|
|
|
|
|
|
|
| 216 |
|
| 217 |
|
| 218 |
-
# Generate medical assessment based on ECG readings and patient history (
|
| 219 |
-
def generate_assessment(ecg_analysis, patient_history=None, chat_model="
|
| 220 |
-
#
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
|
| 223 |
if not ecg_analysis or ecg_analysis.startswith("<strong style='color:red'>"):
|
| 224 |
-
return "<strong style='color:red'>Please analyze
|
| 225 |
|
| 226 |
# Get current timestamp
|
| 227 |
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 228 |
|
| 229 |
-
#
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
ECG ANALYSIS:
|
| 234 |
-
{ecg_analysis}
|
| 235 |
-
|
| 236 |
-
PATIENT HISTORY:
|
| 237 |
-
{patient_history}
|
| 238 |
-
|
| 239 |
-
TIMESTAMP: {timestamp}
|
| 240 |
-
|
| 241 |
-
Format your assessment using HTML elements for readability:
|
| 242 |
-
|
| 243 |
-
<h3>Summary of Findings</h3>
|
| 244 |
-
<ul>
|
| 245 |
-
<li>Finding 1</li>
|
| 246 |
-
<li>Finding 2</li>
|
| 247 |
-
<!-- etc. -->
|
| 248 |
-
</ul>
|
| 249 |
-
|
| 250 |
-
<h3>Key Abnormalities</h3>
|
| 251 |
-
<ul>
|
| 252 |
-
<li>Abnormality 1</li>
|
| 253 |
-
<li>Abnormality 2 (if any)</li>
|
| 254 |
-
<!-- etc. -->
|
| 255 |
-
</ul>
|
| 256 |
-
|
| 257 |
-
<h3>Potential Clinical Implications</h3>
|
| 258 |
-
<ul>
|
| 259 |
-
<li>Implication 1</li>
|
| 260 |
-
<li>Implication 2</li>
|
| 261 |
-
<!-- etc. -->
|
| 262 |
-
</ul>
|
| 263 |
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
<h3>Differential Considerations</h3>
|
| 272 |
-
<ul>
|
| 273 |
-
<li>Differential 1</li>
|
| 274 |
-
<li>Differential 2</li>
|
| 275 |
-
<!-- etc. -->
|
| 276 |
-
</ul>
|
| 277 |
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
- Do not use asterisks (**) for emphasis - use proper HTML formatting instead
|
| 281 |
-
- For any urgent findings, use <span style="color:red"> to highlight them
|
| 282 |
-
"""
|
| 283 |
else:
|
| 284 |
-
|
| 285 |
|
| 286 |
-
|
| 287 |
-
{ecg_analysis}
|
| 288 |
|
| 289 |
-
|
|
|
|
| 290 |
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
<h3>Summary of Findings</h3>
|
| 294 |
<ul>
|
| 295 |
-
<li>
|
| 296 |
-
<li>Finding 2</li>
|
| 297 |
<!-- etc. -->
|
| 298 |
</ul>
|
| 299 |
|
| 300 |
-
<h3>Key Abnormalities</h3>
|
| 301 |
<ul>
|
| 302 |
-
<li>
|
| 303 |
-
<li>Abnormality 2 (if any)</li>
|
| 304 |
-
<!--
|
| 305 |
</ul>
|
| 306 |
|
| 307 |
<h3>Potential Clinical Implications</h3>
|
| 308 |
<ul>
|
| 309 |
-
<li>
|
| 310 |
-
<li>Implication 2</li>
|
| 311 |
<!-- etc. -->
|
| 312 |
</ul>
|
| 313 |
|
| 314 |
-
<h3>
|
| 315 |
<ul>
|
| 316 |
-
<li>
|
| 317 |
-
<li>Recommendation 2 (if
|
| 318 |
<!-- etc. -->
|
| 319 |
</ul>
|
| 320 |
|
| 321 |
-
<h3>Differential Considerations</h3>
|
| 322 |
<ul>
|
| 323 |
-
<li>
|
| 324 |
-
<li>Differential 2</li>
|
| 325 |
<!-- etc. -->
|
| 326 |
</ul>
|
| 327 |
|
| 328 |
-
Important
|
| 329 |
-
-
|
| 330 |
-
- Do
|
| 331 |
-
-
|
| 332 |
-
|
|
|
|
|
|
|
|
|
|
| 333 |
|
| 334 |
try:
|
| 335 |
assessment_completion = groq_client.chat.completions.create(
|
| 336 |
messages=[
|
| 337 |
{
|
| 338 |
"role": "system",
|
| 339 |
-
"content": "You are a medical AI assistant specialized in cardiology.
|
| 340 |
},
|
| 341 |
{
|
| 342 |
"role": "user",
|
|
@@ -345,83 +344,47 @@ Important formatting instructions:
|
|
| 345 |
],
|
| 346 |
model=chat_model,
|
| 347 |
temperature=0.2,
|
| 348 |
-
max_tokens=2048, #
|
|
|
|
|
|
|
| 349 |
)
|
| 350 |
|
| 351 |
assessment_text = assessment_completion.choices[0].message.content
|
| 352 |
|
| 353 |
-
#
|
| 354 |
-
assessment_text = re.sub(r'\*\*(
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
"Potential Clinical Implications",
|
| 365 |
-
"Recommendation",
|
| 366 |
-
"Differential Considerations"
|
| 367 |
-
]
|
| 368 |
-
formatted_html = ""
|
| 369 |
-
lines = assessment_text.split('\n')
|
| 370 |
-
current_section_content = []
|
| 371 |
-
current_section_title = ""
|
| 372 |
-
|
| 373 |
-
def format_section(title, content_lines):
|
| 374 |
-
html = f"<h3>{title}</h3>\n<ul>\n"
|
| 375 |
-
for line in content_lines:
|
| 376 |
-
if line.strip():
|
| 377 |
-
html += f" <li>{line.strip()}</li>\n"
|
| 378 |
-
html += "</ul>\n"
|
| 379 |
-
return html
|
| 380 |
-
|
| 381 |
-
for line in lines:
|
| 382 |
-
line_stripped = line.strip()
|
| 383 |
-
if not line_stripped:
|
| 384 |
-
continue
|
| 385 |
-
|
| 386 |
-
is_header = False
|
| 387 |
-
for section_title in sections:
|
| 388 |
-
# Check if line looks like a header (case-insensitive)
|
| 389 |
-
if section_title.lower() in line_stripped.lower() and len(line_stripped) < len(section_title) + 10:
|
| 390 |
-
if current_section_title and current_section_content:
|
| 391 |
-
formatted_html += format_section(current_section_title, current_section_content)
|
| 392 |
-
current_section_title = section_title # Use the canonical title
|
| 393 |
-
current_section_content = []
|
| 394 |
-
is_header = True
|
| 395 |
-
break
|
| 396 |
-
|
| 397 |
-
if not is_header:
|
| 398 |
-
# If no section started yet, assume it's summary
|
| 399 |
-
if not current_section_title:
|
| 400 |
-
current_section_title = "Summary of Findings"
|
| 401 |
-
current_section_content.append(line_stripped)
|
| 402 |
-
|
| 403 |
-
# Add the last section
|
| 404 |
-
if current_section_title and current_section_content:
|
| 405 |
-
formatted_html += format_section(current_section_title, current_section_content)
|
| 406 |
-
|
| 407 |
-
assessment_text = formatted_html if formatted_html else f"<p>{assessment_text.replace('\n', '<br>')}</p>" # Basic wrap if structure fails
|
| 408 |
|
| 409 |
return assessment_text
|
| 410 |
|
| 411 |
except Exception as e:
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
return f"<strong style='color:red'>Error generating assessment:</strong> {str(e)}"
|
|
|
|
| 415 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
|
| 417 |
-
# Doctor's chat interaction with the model about the patient (Unchanged - uses Groq)
|
| 418 |
-
def doctor_chat(message, chat_history, ecg_analysis, patient_history, assessment, chat_model="llama-3.1-70b-versatile"): # Adjusted default model slightly if needed
|
| 419 |
# Fixed model
|
| 420 |
-
chat_model = "
|
| 421 |
|
| 422 |
# Check if ECG analysis exists and is not an error message
|
| 423 |
if not ecg_analysis or ecg_analysis.startswith("<strong style='color:red'>"):
|
| 424 |
-
# Prepend error message to history
|
| 425 |
chat_history.append((message, "<strong style='color:red'>Cannot start chat. Please analyze a valid ECG image first.</strong>"))
|
| 426 |
return "", chat_history # Clear input, update history
|
| 427 |
|
|
@@ -431,36 +394,42 @@ def doctor_chat(message, chat_history, ecg_analysis, patient_history, assessment
|
|
| 431 |
# Get current timestamp
|
| 432 |
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 433 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
# Prepare chat context
|
| 435 |
context = f"""CURRENT TIMESTAMP: {timestamp}
|
| 436 |
|
| 437 |
-
=== BEGIN CONTEXT ===
|
| 438 |
PATIENT HISTORY:
|
| 439 |
-
{
|
| 440 |
|
| 441 |
-
ECG ANALYSIS:
|
| 442 |
-
{
|
| 443 |
|
| 444 |
-
|
| 445 |
-
{
|
| 446 |
-
=== END CONTEXT ===
|
| 447 |
|
| 448 |
-
Based *only* on the context provided above, answer the doctor's questions concisely. If the information is not in the context, state that.
|
| 449 |
"""
|
| 450 |
|
| 451 |
# Construct full chat history for context
|
| 452 |
messages = [
|
| 453 |
{
|
| 454 |
"role": "system",
|
| 455 |
-
"content": f"You are a
|
| 456 |
}
|
| 457 |
]
|
| 458 |
|
| 459 |
-
# Add chat history to the context (limited
|
| 460 |
-
|
|
|
|
| 461 |
messages.append({"role": "user", "content": user_msg})
|
| 462 |
# Avoid adding error messages from assistant back into context
|
| 463 |
-
if not assistant_msg.startswith("<strong style='color:red'>"):
|
| 464 |
messages.append({"role": "assistant", "content": assistant_msg})
|
| 465 |
|
| 466 |
# Add the current message
|
|
@@ -470,31 +439,30 @@ Based *only* on the context provided above, answer the doctor's questions concis
|
|
| 470 |
chat_completion = groq_client.chat.completions.create(
|
| 471 |
messages=messages,
|
| 472 |
model=chat_model,
|
| 473 |
-
temperature=0.3,
|
| 474 |
-
max_tokens=1024, #
|
| 475 |
)
|
| 476 |
|
| 477 |
response = chat_completion.choices[0].message.content
|
| 478 |
|
| 479 |
-
#
|
| 480 |
-
response = re.sub(r'\*\*(
|
|
|
|
| 481 |
|
| 482 |
chat_history.append((message, response))
|
| 483 |
return "", chat_history # Clear input message box
|
| 484 |
except Exception as e:
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
error_message = f"<strong style='color:red'>Error in chat:</strong> {str(e)}"
|
| 488 |
chat_history.append((message, error_message))
|
| 489 |
return "", chat_history # Clear input message box
|
| 490 |
|
| 491 |
# Create Gradio interface
|
| 492 |
with gr.Blocks(title="Cardiac ECG Analysis System", theme=gr.themes.Soft()) as app:
|
| 493 |
-
# Session state to store data (Removed - use outputs directly or manage state differently if needed)
|
| 494 |
-
# ecg_analysis_state = gr.State("") # Not strictly necessary if passing outputs directly
|
| 495 |
|
| 496 |
gr.Markdown("# 🫀 Cardiac ECG Analysis System")
|
| 497 |
-
gr.Markdown("Upload an ECG image and optional patient history
|
| 498 |
|
| 499 |
with gr.Tabs():
|
| 500 |
with gr.TabItem("💻 Main Interface"):
|
|
@@ -502,10 +470,9 @@ with gr.Blocks(title="Cardiac ECG Analysis System", theme=gr.themes.Soft()) as a
|
|
| 502 |
with gr.Column(scale=1):
|
| 503 |
# Input components
|
| 504 |
with gr.Group():
|
| 505 |
-
gr.Markdown("### 📊 ECG Image")
|
| 506 |
-
ecg_image = gr.Image(type="pil", label="Upload ECG Image")
|
| 507 |
-
|
| 508 |
-
gr.Markdown("**Vision Model:** gemini-2.0-flash-exp")
|
| 509 |
analyze_button = gr.Button("Analyze ECG Image", variant="primary")
|
| 510 |
|
| 511 |
with gr.Group():
|
|
@@ -513,19 +480,18 @@ with gr.Blocks(title="Cardiac ECG Analysis System", theme=gr.themes.Soft()) as a
|
|
| 513 |
patient_history_text = gr.Textbox(
|
| 514 |
lines=8,
|
| 515 |
label="Patient History (Manual Entry or Loaded from File)",
|
| 516 |
-
placeholder="Enter patient
|
| 517 |
)
|
| 518 |
patient_history_file = gr.File(
|
| 519 |
-
label="Upload Patient History File (Optional
|
| 520 |
file_types=[".txt", ".csv", ".xlsx", ".xls"]
|
| 521 |
)
|
| 522 |
load_history_button = gr.Button("Load Patient History from File")
|
| 523 |
|
| 524 |
with gr.Group():
|
| 525 |
-
gr.Markdown("### 🧠 Assessment
|
| 526 |
-
#
|
| 527 |
-
|
| 528 |
-
gr.Markdown("**Chat/Assessment Model:** llama-3.1-70b-versatile")
|
| 529 |
assess_button = gr.Button("Generate Assessment", variant="primary")
|
| 530 |
|
| 531 |
with gr.Column(scale=1):
|
|
@@ -538,91 +504,93 @@ with gr.Blocks(title="Cardiac ECG Analysis System", theme=gr.themes.Soft()) as a
|
|
| 538 |
gr.Markdown("### 📝 Medical Assessment")
|
| 539 |
assessment_output = gr.HTML(label="Assessment", elem_id="assessment-output")
|
| 540 |
|
| 541 |
-
gr.Markdown("
|
| 542 |
-
gr.Markdown("
|
|
|
|
| 543 |
|
| 544 |
with gr.Group():
|
| 545 |
-
chatbot = gr.Chatbot(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
with gr.Row():
|
| 547 |
message = gr.Textbox(
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
placeholder="Ask a question about this patient's cardiac status...",
|
| 551 |
scale=4,
|
| 552 |
-
show_label=False,
|
| 553 |
-
container=False,
|
| 554 |
)
|
| 555 |
chat_button = gr.Button("Send", scale=1, variant="primary")
|
| 556 |
|
| 557 |
-
with gr.TabItem("ℹ️ Instructions"):
|
| 558 |
gr.Markdown("""
|
| 559 |
## How to Use This Application
|
| 560 |
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
*
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
*
|
| 576 |
-
*
|
| 577 |
-
*
|
| 578 |
-
|
| 579 |
-
### Important Notes
|
| 580 |
-
* **API Keys:** Ensure `GEMINI_API_KEY` and `GROQ_API_KEY` environment variables are set correctly before running the application.
|
| 581 |
-
* **Purpose:** This tool is designed to assist healthcare professionals and is NOT a substitute for professional clinical judgment or diagnosis.
|
| 582 |
-
* **Validation:** Always validate AI-generated medical interpretations with qualified medical expertise.
|
| 583 |
-
* **Privacy:** Ensure compliance with patient data privacy regulations (e.g., HIPAA) when using this tool. Do not upload identifiable patient information if not permitted.
|
| 584 |
""")
|
| 585 |
|
| 586 |
# --- Event Handlers ---
|
| 587 |
|
| 588 |
-
#
|
| 589 |
analyze_button.click(
|
| 590 |
-
analyze_ecg_image,
|
| 591 |
inputs=[ecg_image],
|
| 592 |
outputs=ecg_analysis_output
|
| 593 |
)
|
| 594 |
|
| 595 |
-
# Load
|
| 596 |
load_history_button.click(
|
| 597 |
-
process_patient_history,
|
| 598 |
inputs=[patient_history_file],
|
| 599 |
outputs=[patient_history_text]
|
| 600 |
)
|
| 601 |
|
| 602 |
-
#
|
| 603 |
assess_button.click(
|
| 604 |
-
generate_assessment,
|
| 605 |
inputs=[ecg_analysis_output, patient_history_text],
|
| 606 |
outputs=assessment_output
|
| 607 |
)
|
| 608 |
|
| 609 |
-
#
|
| 610 |
chat_button.click(
|
| 611 |
-
doctor_chat,
|
| 612 |
inputs=[message, chatbot, ecg_analysis_output, patient_history_text, assessment_output],
|
| 613 |
-
outputs=[message, chatbot]
|
| 614 |
)
|
| 615 |
|
| 616 |
-
#
|
| 617 |
message.submit(
|
| 618 |
-
doctor_chat,
|
| 619 |
inputs=[message, chatbot, ecg_analysis_output, patient_history_text, assessment_output],
|
| 620 |
-
outputs=[message, chatbot]
|
| 621 |
)
|
| 622 |
|
| 623 |
|
| 624 |
# Launch the app
|
| 625 |
if __name__ == "__main__":
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
|
|
|
| 2 |
import gradio as gr
|
| 3 |
import pandas as pd
|
| 4 |
from groq import Groq
|
|
|
|
| 8 |
import re
|
| 9 |
import google.generativeai as genai # Added for Gemini
|
| 10 |
from google.generativeai import types # Added for Gemini
|
| 11 |
+
import traceback # For detailed error logging
|
| 12 |
|
| 13 |
# Initialize Groq client (for chat/assessment)
|
| 14 |
+
# Ensure GROQ_API_KEY is set in your environment variables
|
| 15 |
+
try:
|
| 16 |
+
groq_client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
|
| 17 |
+
except Exception as e:
|
| 18 |
+
print(f"Error initializing Groq client: {e}")
|
| 19 |
+
groq_client = None # Set to None to handle initialization errors gracefully
|
| 20 |
|
| 21 |
# NOTE: Gemini client initialization will happen inside the analyze_ecg_image function
|
| 22 |
# Ensure GEMINI_API_KEY is set in your environment variables
|
| 23 |
|
| 24 |
+
# Process patient history file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
def process_patient_history(file):
|
| 26 |
if file is None:
|
| 27 |
return ""
|
| 28 |
|
| 29 |
try:
|
| 30 |
# Check file extension
|
| 31 |
+
file_path = file.name # Gradio File object usually has a .name attribute with the path
|
| 32 |
+
file_ext = os.path.splitext(file_path)[1].lower()
|
| 33 |
|
| 34 |
if file_ext == '.txt':
|
| 35 |
# Read text file
|
| 36 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
|
|
| 37 |
content = f.read()
|
|
|
|
|
|
|
| 38 |
return content
|
| 39 |
|
| 40 |
elif file_ext in ['.csv', '.xlsx', '.xls']:
|
| 41 |
# Read spreadsheet file
|
| 42 |
if file_ext == '.csv':
|
| 43 |
+
df = pd.read_csv(file_path)
|
| 44 |
else:
|
| 45 |
+
# Specify engine for newer xlsx files if needed
|
| 46 |
+
try:
|
| 47 |
+
df = pd.read_excel(file_path)
|
| 48 |
+
except ImportError:
|
| 49 |
+
return "Error: `openpyxl` needed for .xlsx files. Install with `pip install openpyxl`"
|
| 50 |
+
except Exception as e_excel:
|
| 51 |
+
return f"Error reading Excel file: {e_excel}"
|
| 52 |
+
|
| 53 |
|
| 54 |
# Convert dataframe to formatted string
|
| 55 |
formatted_data = "PATIENT INFORMATION:\n\n"
|
|
|
|
| 58 |
for column in df.columns:
|
| 59 |
# Handle potential missing values gracefully
|
| 60 |
value = df.iloc[0].get(column, 'N/A')
|
| 61 |
+
# Convert value to string to avoid potential type issues
|
| 62 |
+
formatted_data += f"{column}: {str(value)}\n"
|
| 63 |
else:
|
| 64 |
formatted_data += "Spreadsheet is empty or format is not recognized correctly."
|
| 65 |
|
| 66 |
return formatted_data
|
| 67 |
|
| 68 |
else:
|
| 69 |
+
return f"Unsupported file format ({file_ext}). Please upload a .txt, .csv, or .xlsx file."
|
| 70 |
|
| 71 |
+
except AttributeError:
|
| 72 |
+
return "Error: Could not get file path from Gradio File object. Ensure a file was uploaded."
|
| 73 |
+
except FileNotFoundError:
|
| 74 |
+
return f"Error: File not found at path: {file_path}"
|
| 75 |
except Exception as e:
|
| 76 |
+
print(f"Error processing patient history file:\n{traceback.format_exc()}")
|
| 77 |
return f"Error processing patient history file: {str(e)}"
|
| 78 |
|
| 79 |
|
|
|
|
| 88 |
# Ensure image is PIL Image
|
| 89 |
if not isinstance(image, Image.Image):
|
| 90 |
try:
|
| 91 |
+
# If 'image' is a path (from older Gradio versions or specific setups)
|
| 92 |
+
if isinstance(image, str) and os.path.exists(image):
|
| 93 |
+
image = Image.open(image)
|
| 94 |
+
# If 'image' is file-like object from Gradio upload
|
| 95 |
+
elif hasattr(image, 'name'):
|
| 96 |
+
image = Image.open(image.name)
|
| 97 |
+
else:
|
| 98 |
+
# Assume it might be bytes or needs loading differently
|
| 99 |
+
# This part might need adjustment depending on how Gradio passes the image
|
| 100 |
+
return f"<strong style='color:red'>Unrecognized image input format: {type(image)}</strong>"
|
| 101 |
except Exception as e:
|
| 102 |
+
print(f"Error opening image:\n{traceback.format_exc()}")
|
| 103 |
return f"<strong style='color:red'>Error opening image: {str(e)}</strong>"
|
| 104 |
|
| 105 |
# --- Gemini Specific Part ---
|
| 106 |
+
gemini_client = None # Initialize to None
|
| 107 |
try:
|
| 108 |
# Get Gemini API Key
|
| 109 |
gemini_api_key = os.environ.get("GEMINI_API_KEY")
|
|
|
|
| 111 |
return "<strong style='color:red'>GEMINI_API_KEY environment variable not set.</strong>"
|
| 112 |
|
| 113 |
# Initialize Gemini client
|
| 114 |
+
# Use Client() for explicit initialization per request (safer for concurrent use)
|
|
|
|
|
|
|
| 115 |
gemini_client = genai.Client(api_key=gemini_api_key)
|
| 116 |
|
|
|
|
| 117 |
# Convert PIL image to bytes (JPEG format)
|
| 118 |
buffered = io.BytesIO()
|
| 119 |
# Ensure image is in RGB format if it's RGBA or P which might cause issues
|
| 120 |
+
img_format = "JPEG"
|
| 121 |
if image.mode in ('RGBA', 'P'):
|
| 122 |
image = image.convert('RGB')
|
| 123 |
+
# Handle potential transparency issues for PNG -> JPEG conversion
|
| 124 |
+
elif image.mode == 'LA':
|
| 125 |
+
image = image.convert('RGB') # Convert Luminance Alpha to RGB
|
| 126 |
+
# Check format if needed
|
| 127 |
+
# if image.format == 'PNG': img_format = 'PNG' # Gemini might prefer JPEG? Test this.
|
| 128 |
+
|
| 129 |
+
image.save(buffered, format=img_format)
|
| 130 |
image_bytes = buffered.getvalue()
|
| 131 |
|
| 132 |
# Get current timestamp (computer time)
|
|
|
|
| 136 |
vision_prompt = f"""Analyze this ECG image carefully. You are a cardiologist analyzing an electrocardiogram (ECG).
|
| 137 |
|
| 138 |
Extract and report all visible parameters, including but not limited to:
|
| 139 |
+
1. Heart rate (bpm)
|
| 140 |
+
2. PR interval (ms)
|
| 141 |
+
3. QRS duration (ms)
|
| 142 |
+
4. QT/QTc interval (ms)
|
| 143 |
+
5. P wave morphology (e.g., upright in lead II, biphasic, absent)
|
| 144 |
+
6. ST segment changes (e.g., elevation, depression, location)
|
| 145 |
+
7. T wave morphology (e.g., upright, inverted, peaked, flattened)
|
| 146 |
+
8. Rhythm classification (e.g., Sinus Rhythm, Atrial Fibrillation, etc.)
|
| 147 |
+
9. Axis deviation (if determinable)
|
| 148 |
+
10. Specific patterns (e.g., Bundle Branch Block, LVH criteria, WPW)
|
| 149 |
|
| 150 |
+
Report exact numerical values where visible. If a range is shown, report the range. Format your response using HTML list elements for better readability.
|
| 151 |
|
| 152 |
+
If certain measurements aren't clearly visible or determinable from the provided image quality, explicitly state that they cannot be determined.
|
| 153 |
|
| 154 |
+
If you notice any abnormalities or concerning patterns, highlight them clearly but avoid making definitive diagnoses. State observations neutrally.
|
| 155 |
|
| 156 |
+
Format your response strictly like this example:
|
| 157 |
<h3>ECG Report</h3>
|
| 158 |
<ul>
|
| 159 |
+
<li><strong>Analysis Time:</strong> {timestamp}</li>
|
| 160 |
+
<li><strong>Heart Rate:</strong> [value] bpm (or 'Not determinable')</li>
|
| 161 |
+
<li><strong>Rhythm:</strong> [description] (or 'Not determinable')</li>
|
| 162 |
+
<li><strong>PR Interval:</strong> [value] ms (or 'Not determinable')</li>
|
| 163 |
+
<li><strong>QRS Duration:</strong> [value] ms (or 'Not determinable')</li>
|
| 164 |
+
<li><strong>QT/QTc Interval:</strong> [value]/[value] ms (or 'Not determinable')</li>
|
| 165 |
+
<li><strong>Axis:</strong> [description] (or 'Not determinable')</li>
|
| 166 |
+
<li><strong>P Waves:</strong> [description]</li>
|
| 167 |
+
<li><strong>ST Segment:</strong> [description]</li>
|
| 168 |
+
<li><strong>T Waves:</strong> [description]</li>
|
| 169 |
+
<!-- Add other relevant findings as list items -->
|
| 170 |
</ul>
|
| 171 |
|
| 172 |
+
<h3>Key Observations / Potential Abnormalities</h3>
|
| 173 |
<ul>
|
| 174 |
+
<li>[Observation 1, e.g., Possible ST elevation in leads V1-V3]</li>
|
| 175 |
+
<li>[Observation 2, e.g., Inverted T waves in lead III]</li>
|
| 176 |
+
<!-- List significant observations, use <span style="color:red"> for critical findings -->
|
| 177 |
</ul>
|
| 178 |
|
| 179 |
+
<h3>Impression</h3>
|
| 180 |
+
<p>[Provide a brief summary impression based *only* on the visible findings, e.g., "Sinus tachycardia with possible anterior ST changes." Avoid definitive diagnosis.]</p>
|
| 181 |
|
| 182 |
Important formatting instructions:
|
| 183 |
+
- Use EXACTLY the HTML structure shown (<h3>, <ul>, <li>, <strong>, <p>).
|
| 184 |
+
- Do NOT use markdown like asterisks (**) or hashtags (#). Use HTML tags for formatting.
|
| 185 |
+
- For potentially urgent findings in the 'Key Observations' list, wrap the description in <span style="color:red">text</span>.
|
| 186 |
"""
|
| 187 |
|
| 188 |
# Generate content using Gemini
|
| 189 |
response = gemini_client.models.generate_content(
|
| 190 |
model=vision_model,
|
| 191 |
contents=[vision_prompt,
|
| 192 |
+
types.Part.from_bytes(data=image_bytes, mime_type=f"image/{img_format.lower()}")]
|
| 193 |
+
# Add safety_settings if needed (BLOCK_NONE allows more content but use carefully):
|
| 194 |
# safety_settings=[
|
| 195 |
# {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
|
| 196 |
# {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
|
|
|
|
| 201 |
|
| 202 |
# Handle potential blocks or errors in response
|
| 203 |
if not response.candidates:
|
| 204 |
+
feedback = getattr(response, 'prompt_feedback', None)
|
| 205 |
+
block_reason = getattr(feedback, 'block_reason', None) if feedback else None
|
| 206 |
+
if block_reason:
|
| 207 |
+
return f"<strong style='color:red'>Analysis blocked by safety filter: {block_reason}. Consider adjusting safety settings or image content if appropriate.</strong>"
|
| 208 |
else:
|
| 209 |
+
# Log the full response for debugging if possible
|
| 210 |
+
print(f"Gemini Response Error: No candidates returned. Full response: {response}")
|
| 211 |
+
return "<strong style='color:red'>No content generated by the model. The request might have been blocked, timed out, or failed unexpectedly. Check logs.</strong>"
|
| 212 |
|
| 213 |
+
# Accessing response text - check structure based on library version
|
| 214 |
+
try:
|
| 215 |
+
# Preferred way for recent versions
|
| 216 |
+
ecg_analysis = response.text
|
| 217 |
+
except ValueError:
|
| 218 |
+
# Fallback for potential older structures or errors
|
| 219 |
+
if response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
|
| 220 |
+
ecg_analysis = "".join(part.text for part in response.candidates[0].content.parts if hasattr(part, 'text'))
|
| 221 |
+
else:
|
| 222 |
+
print(f"Gemini Response Error: Could not extract text. Response structure: {response}")
|
| 223 |
+
return "<strong style='color:red'>Error processing model response. Unexpected format. Check logs.</strong>"
|
| 224 |
|
|
|
|
|
|
|
| 225 |
|
| 226 |
# --- End of Gemini Specific Part ---
|
| 227 |
|
| 228 |
+
# Basic post-processing (Gemini should follow HTML instructions, but just in case)
|
| 229 |
+
# Remove potential markdown that might slip through
|
| 230 |
+
ecg_analysis = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', ecg_analysis)
|
| 231 |
+
ecg_analysis = re.sub(r'^\s*#+\s+(.*?)\s*$', r'<h3>\1</h3>', ecg_analysis, flags=re.MULTILINE)
|
| 232 |
+
ecg_analysis = re.sub(r'^\s*[\*-]\s+(.*?)\s*$', r'<li>\1</li>', ecg_analysis, flags=re.MULTILINE) # Convert markdown lists
|
| 233 |
|
| 234 |
+
# Simple check if the response looks somewhat like the requested HTML structure
|
| 235 |
+
if not ("<h3>" in ecg_analysis and "<ul>" in ecg_analysis):
|
| 236 |
+
print(f"Warning: Gemini response might not be in the expected HTML format:\n{ecg_analysis[:500]}...")
|
| 237 |
+
# Optionally wrap in basic tags if completely unformatted
|
| 238 |
+
# ecg_analysis = f"<h3>ECG Analysis (Raw Output)</h3><p>{ecg_analysis.replace('\n', '<br>')}</p>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
|
| 240 |
return ecg_analysis
|
| 241 |
|
| 242 |
except Exception as e:
|
| 243 |
# Catch potential API errors or other issues
|
| 244 |
+
print(f"Error during Gemini ECG analysis:\n{traceback.format_exc()}")
|
| 245 |
+
error_type = type(e).__name__
|
| 246 |
+
return f"<strong style='color:red'>Error analyzing ECG image with Gemini ({error_type}):</strong> {str(e)}"
|
| 247 |
+
finally:
|
| 248 |
+
# Clean up client resource if necessary, though Client() might not require explicit closing
|
| 249 |
+
pass
|
| 250 |
|
| 251 |
|
| 252 |
+
# Generate medical assessment based on ECG readings and patient history (Uses Groq)
|
| 253 |
+
def generate_assessment(ecg_analysis, patient_history=None, chat_model="llama3-70b-8192"): # Using a common Groq model
|
| 254 |
+
# Check Groq client initialization
|
| 255 |
+
if groq_client is None:
|
| 256 |
+
return "<strong style='color:red'>Groq client not initialized. Check API Key and startup logs.</strong>"
|
| 257 |
+
|
| 258 |
+
# Use a known available and capable Groq model
|
| 259 |
+
chat_model = "llama3-70b-8192" # Or "mixtral-8x7b-32768" or another available one
|
| 260 |
|
| 261 |
if not ecg_analysis or ecg_analysis.startswith("<strong style='color:red'>"):
|
| 262 |
+
return "<strong style='color:red'>Cannot generate assessment. Please analyze a valid ECG image first.</strong>"
|
| 263 |
|
| 264 |
# Get current timestamp
|
| 265 |
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 266 |
|
| 267 |
+
# Clean up potential HTML issues in the input ECG analysis for the prompt
|
| 268 |
+
# This helps prevent confusing the assessment model
|
| 269 |
+
clean_ecg_analysis = re.sub('<[^>]+>', '', ecg_analysis) # Strip HTML tags for the prompt context
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
+
# Construct prompt based on available information
|
| 272 |
+
prompt_parts = [
|
| 273 |
+
"You are a highly trained cardiologist assistant AI. Your task is to synthesize information from an ECG analysis and patient history (if provided) into a clinical assessment for a reviewing physician.",
|
| 274 |
+
"Focus on integrating the findings and suggesting potential implications and recommendations.",
|
| 275 |
+
"Format your response strictly using the specified HTML structure.",
|
| 276 |
+
"\nECG ANALYSIS SUMMARY (Provided):\n" + clean_ecg_analysis, # Use cleaned text
|
| 277 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
|
| 279 |
+
if patient_history and patient_history.strip():
|
| 280 |
+
prompt_parts.append("\nPATIENT HISTORY (Provided):\n" + patient_history)
|
|
|
|
|
|
|
|
|
|
| 281 |
else:
|
| 282 |
+
prompt_parts.append("\nPATIENT HISTORY: Not provided.")
|
| 283 |
|
| 284 |
+
prompt_parts.append(f"\nASSESSMENT TIMESTAMP: {timestamp}")
|
|
|
|
| 285 |
|
| 286 |
+
prompt_parts.append("""
|
| 287 |
+
Format your assessment using ONLY the following HTML structure:
|
| 288 |
|
| 289 |
+
<h3>Summary of Integrated Findings</h3>
|
|
|
|
|
|
|
| 290 |
<ul>
|
| 291 |
+
<li>[Combine key ECG findings with relevant patient history points, e.g., "ECG shows sinus tachycardia in the context of reported palpitations."]</li>
|
| 292 |
+
<li>[Finding 2]</li>
|
| 293 |
<!-- etc. -->
|
| 294 |
</ul>
|
| 295 |
|
| 296 |
+
<h3>Key Abnormalities and Concerns</h3>
|
| 297 |
<ul>
|
| 298 |
+
<li>[List specific significant abnormalities from the ECG, potentially contextualized by history, e.g., "ST elevation in anterior leads concerning for possible ischemia."]</li>
|
| 299 |
+
<li>[Abnormality 2 (if any)]</li>
|
| 300 |
+
<!-- Use <span style="color:red"> for urgent/critical concerns -->
|
| 301 |
</ul>
|
| 302 |
|
| 303 |
<h3>Potential Clinical Implications</h3>
|
| 304 |
<ul>
|
| 305 |
+
<li>[Suggest possible underlying conditions or risks based on findings, e.g., "Findings could be consistent with acute coronary syndrome."]</li>
|
| 306 |
+
<li>[Implication 2]</li>
|
| 307 |
<!-- etc. -->
|
| 308 |
</ul>
|
| 309 |
|
| 310 |
+
<h3>Recommendations for Physician Review</h3>
|
| 311 |
<ul>
|
| 312 |
+
<li>[Suggest next steps or urgency, e.g., "<span style="color:red">Urgent clinical correlation and comparison with previous ECGs recommended.</span>"]</li>
|
| 313 |
+
<li>[Recommendation 2 (e.g., Consider further cardiac workup like troponins, echocardiogram if clinically indicated.)]</li>
|
| 314 |
<!-- etc. -->
|
| 315 |
</ul>
|
| 316 |
|
| 317 |
+
<h3>Differential Considerations (Optional)</h3>
|
| 318 |
<ul>
|
| 319 |
+
<li>[List possible alternative explanations for the findings, if applicable.]</li>
|
| 320 |
+
<li>[Differential 2]</li>
|
| 321 |
<!-- etc. -->
|
| 322 |
</ul>
|
| 323 |
|
| 324 |
+
Important Instructions:
|
| 325 |
+
- Adhere strictly to the HTML format (<h3>, <ul>, <li>, <strong>, <p>, <span style="color:red">).
|
| 326 |
+
- Do NOT use markdown formatting (**, #, - ).
|
| 327 |
+
- Base your assessment ONLY on the provided ECG analysis and patient history.
|
| 328 |
+
- Do NOT make a definitive diagnosis. Phrase conclusions as possibilities or suggestions for the physician.
|
| 329 |
+
- If the ECG analysis indicated 'Not determinable' for key parameters, acknowledge this limitation.
|
| 330 |
+
""")
|
| 331 |
+
prompt = "\n".join(prompt_parts)
|
| 332 |
|
| 333 |
try:
|
| 334 |
assessment_completion = groq_client.chat.completions.create(
|
| 335 |
messages=[
|
| 336 |
{
|
| 337 |
"role": "system",
|
| 338 |
+
"content": "You are a medical AI assistant specialized in cardiology. Generate a structured clinical assessment based on the provided ECG and patient data, formatted in HTML for physician review. Highlight urgent findings appropriately. Avoid definitive diagnoses."
|
| 339 |
},
|
| 340 |
{
|
| 341 |
"role": "user",
|
|
|
|
| 344 |
],
|
| 345 |
model=chat_model,
|
| 346 |
temperature=0.2,
|
| 347 |
+
max_tokens=2048, # Adjust as needed
|
| 348 |
+
# top_p=0.9, # Optional parameter tuning
|
| 349 |
+
# stop=None, # Optional stop sequences
|
| 350 |
)
|
| 351 |
|
| 352 |
assessment_text = assessment_completion.choices[0].message.content
|
| 353 |
|
| 354 |
+
# Basic post-processing (Groq should follow HTML instructions, but good to have fallbacks)
|
| 355 |
+
assessment_text = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', assessment_text)
|
| 356 |
+
assessment_text = re.sub(r'^\s*#+\s+(.*?)\s*$', r'<h3>\1</h3>', assessment_text, flags=re.MULTILINE)
|
| 357 |
+
|
| 358 |
+
# *** CORRECTED FALLBACK LOGIC ***
|
| 359 |
+
# Check if the response seems to contain the expected HTML structure
|
| 360 |
+
if not ("<h3>" in assessment_text and "<ul>" in assessment_text):
|
| 361 |
+
print(f"Warning: Groq assessment response might not be in the expected HTML format:\n{assessment_text[:500]}...")
|
| 362 |
+
# Fallback: Wrap the raw text in a paragraph tag with line breaks
|
| 363 |
+
processed_text = assessment_text.replace('\n', '<br>')
|
| 364 |
+
assessment_text = f"<h3>Assessment (Raw Output)</h3><p>{processed_text}</p>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
|
| 366 |
return assessment_text
|
| 367 |
|
| 368 |
except Exception as e:
|
| 369 |
+
print(f"Error during Groq assessment generation:\n{traceback.format_exc()}")
|
| 370 |
+
error_type = type(e).__name__
|
| 371 |
+
return f"<strong style='color:red'>Error generating assessment with Groq ({error_type}):</strong> {str(e)}"
|
| 372 |
+
|
| 373 |
|
| 374 |
+
# Doctor's chat interaction with the model about the patient (Uses Groq)
|
| 375 |
+
def doctor_chat(message, chat_history, ecg_analysis, patient_history, assessment, chat_model="llama3-70b-8192"):
|
| 376 |
+
# Check Groq client initialization
|
| 377 |
+
if groq_client is None:
|
| 378 |
+
# Prepend error message to history instead of returning it directly
|
| 379 |
+
chat_history.append((message, "<strong style='color:red'>Cannot start chat. Groq client not initialized. Check API Key.</strong>"))
|
| 380 |
+
return "", chat_history # Clear input, update history
|
| 381 |
|
|
|
|
|
|
|
| 382 |
# Fixed model
|
| 383 |
+
chat_model = "llama3-70b-8192" # Consistent with assessment
|
| 384 |
|
| 385 |
# Check if ECG analysis exists and is not an error message
|
| 386 |
if not ecg_analysis or ecg_analysis.startswith("<strong style='color:red'>"):
|
| 387 |
+
# Prepend error message to history
|
| 388 |
chat_history.append((message, "<strong style='color:red'>Cannot start chat. Please analyze a valid ECG image first.</strong>"))
|
| 389 |
return "", chat_history # Clear input, update history
|
| 390 |
|
|
|
|
| 394 |
# Get current timestamp
|
| 395 |
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 396 |
|
| 397 |
+
# Clean inputs for context
|
| 398 |
+
clean_ecg = re.sub('<[^>]+>', '', ecg_analysis)
|
| 399 |
+
clean_assessment = re.sub('<[^>]+>', '', assessment) if assessment and not assessment.startswith("<strong style='color:red'>") else "Assessment not available or failed."
|
| 400 |
+
clean_history = patient_history if patient_history and patient_history.strip() else "No patient history provided."
|
| 401 |
+
|
| 402 |
# Prepare chat context
|
| 403 |
context = f"""CURRENT TIMESTAMP: {timestamp}
|
| 404 |
|
| 405 |
+
=== BEGIN PATIENT CONTEXT ===
|
| 406 |
PATIENT HISTORY:
|
| 407 |
+
{clean_history}
|
| 408 |
|
| 409 |
+
ECG ANALYSIS SUMMARY:
|
| 410 |
+
{clean_ecg}
|
| 411 |
|
| 412 |
+
GENERATED ASSESSMENT SUMMARY:
|
| 413 |
+
{clean_assessment}
|
| 414 |
+
=== END PATIENT CONTEXT ===
|
| 415 |
|
| 416 |
+
Based *only* on the patient context provided above, answer the doctor's questions concisely and professionally. If the information needed to answer is not in the context, explicitly state that. Do not invent information or access external knowledge.
|
| 417 |
"""
|
| 418 |
|
| 419 |
# Construct full chat history for context
|
| 420 |
messages = [
|
| 421 |
{
|
| 422 |
"role": "system",
|
| 423 |
+
"content": f"You are a specialized cardiology AI assistant conversing with a doctor. Your knowledge is strictly limited to the patient information provided in the context below. Answer questions based *only* on this context.\n\n{context}"
|
| 424 |
}
|
| 425 |
]
|
| 426 |
|
| 427 |
+
# Add chat history to the context (limited number of turns to manage token count)
|
| 428 |
+
history_limit = 5 # Number of past user/assistant pairs
|
| 429 |
+
for user_msg, assistant_msg in chat_history[-history_limit:]:
|
| 430 |
messages.append({"role": "user", "content": user_msg})
|
| 431 |
# Avoid adding error messages from assistant back into context
|
| 432 |
+
if isinstance(assistant_msg, str) and not assistant_msg.startswith("<strong style='color:red'>"):
|
| 433 |
messages.append({"role": "assistant", "content": assistant_msg})
|
| 434 |
|
| 435 |
# Add the current message
|
|
|
|
| 439 |
chat_completion = groq_client.chat.completions.create(
|
| 440 |
messages=messages,
|
| 441 |
model=chat_model,
|
| 442 |
+
temperature=0.3, # Slightly higher for more conversational answers, but still factual
|
| 443 |
+
max_tokens=1024, # Adjust as needed
|
| 444 |
)
|
| 445 |
|
| 446 |
response = chat_completion.choices[0].message.content
|
| 447 |
|
| 448 |
+
# Basic post-processing for the chat response (less critical for HTML here)
|
| 449 |
+
response = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', response) # Convert bold markdown
|
| 450 |
+
response = response.replace('\n', '<br>') # Ensure line breaks are rendered in HTML chatbot
|
| 451 |
|
| 452 |
chat_history.append((message, response))
|
| 453 |
return "", chat_history # Clear input message box
|
| 454 |
except Exception as e:
|
| 455 |
+
print(f"Error during Groq chat:\n{traceback.format_exc()}")
|
| 456 |
+
error_type = type(e).__name__
|
| 457 |
+
error_message = f"<strong style='color:red'>Error in chat ({error_type}):</strong> {str(e)}"
|
| 458 |
chat_history.append((message, error_message))
|
| 459 |
return "", chat_history # Clear input message box
|
| 460 |
|
| 461 |
# Create Gradio interface
|
| 462 |
with gr.Blocks(title="Cardiac ECG Analysis System", theme=gr.themes.Soft()) as app:
|
|
|
|
|
|
|
| 463 |
|
| 464 |
gr.Markdown("# 🫀 Cardiac ECG Analysis System")
|
| 465 |
+
gr.Markdown("Upload an ECG image and optional patient history for AI-assisted analysis, assessment, and consultation.")
|
| 466 |
|
| 467 |
with gr.Tabs():
|
| 468 |
with gr.TabItem("💻 Main Interface"):
|
|
|
|
| 470 |
with gr.Column(scale=1):
|
| 471 |
# Input components
|
| 472 |
with gr.Group():
|
| 473 |
+
gr.Markdown("### 📊 ECG Image Upload")
|
| 474 |
+
ecg_image = gr.Image(type="pil", label="Upload ECG Image", height=300)
|
| 475 |
+
gr.Markdown("**Vision Model:** `gemini-2.0-flash-exp` (via Google AI)")
|
|
|
|
| 476 |
analyze_button = gr.Button("Analyze ECG Image", variant="primary")
|
| 477 |
|
| 478 |
with gr.Group():
|
|
|
|
| 480 |
patient_history_text = gr.Textbox(
|
| 481 |
lines=8,
|
| 482 |
label="Patient History (Manual Entry or Loaded from File)",
|
| 483 |
+
placeholder="Enter relevant patient details (age, sex, symptoms, meds, conditions) OR upload a file (.txt, .csv, .xlsx) and click Load."
|
| 484 |
)
|
| 485 |
patient_history_file = gr.File(
|
| 486 |
+
label="Upload Patient History File (Optional)",
|
| 487 |
file_types=[".txt", ".csv", ".xlsx", ".xls"]
|
| 488 |
)
|
| 489 |
load_history_button = gr.Button("Load Patient History from File")
|
| 490 |
|
| 491 |
with gr.Group():
|
| 492 |
+
gr.Markdown("### 🧠 Generate Assessment")
|
| 493 |
+
# Ensure this model name matches the one used in generate_assessment/doctor_chat
|
| 494 |
+
gr.Markdown("**Assessment/Chat Model:** `llama3-70b-8192` (via Groq)")
|
|
|
|
| 495 |
assess_button = gr.Button("Generate Assessment", variant="primary")
|
| 496 |
|
| 497 |
with gr.Column(scale=1):
|
|
|
|
| 504 |
gr.Markdown("### 📝 Medical Assessment")
|
| 505 |
assessment_output = gr.HTML(label="Assessment", elem_id="assessment-output")
|
| 506 |
|
| 507 |
+
gr.Markdown("---") # Separator
|
| 508 |
+
gr.Markdown("## 👨⚕️ Doctor's Consultation Chat")
|
| 509 |
+
gr.Markdown("Ask follow-up questions based on the analysis and assessment above.")
|
| 510 |
|
| 511 |
with gr.Group():
|
| 512 |
+
chatbot = gr.Chatbot(
|
| 513 |
+
label="Consultation Log",
|
| 514 |
+
height=450,
|
| 515 |
+
bubble_full_width=False,
|
| 516 |
+
show_label=False # Label provided by Markdown above
|
| 517 |
+
)
|
| 518 |
with gr.Row():
|
| 519 |
message = gr.Textbox(
|
| 520 |
+
label="Your Question", # Added label for clarity
|
| 521 |
+
placeholder="Type your question here and press Enter or click Send...",
|
|
|
|
| 522 |
scale=4,
|
| 523 |
+
show_label=False, # Hide label visually if desired, but keep for accessibility
|
| 524 |
+
container=False,
|
| 525 |
)
|
| 526 |
chat_button = gr.Button("Send", scale=1, variant="primary")
|
| 527 |
|
| 528 |
+
with gr.TabItem("ℹ️ Instructions & Disclaimer"):
|
| 529 |
gr.Markdown("""
|
| 530 |
## How to Use This Application
|
| 531 |
|
| 532 |
+
1. **Upload ECG:** Go to the "Main Interface" tab. Upload an ECG image using the designated area.
|
| 533 |
+
2. **Analyze ECG:** Click the **Analyze ECG Image** button. Wait for the results to appear in the "ECG Analysis Results" box. This uses the Gemini model.
|
| 534 |
+
3. **Add Patient History (Optional):**
|
| 535 |
+
* Type relevant details directly into the "Patient History" text box.
|
| 536 |
+
* OR, upload a `.txt`, `.csv`, or `.xlsx` file containing patient info and click **Load Patient History from File**. The content will load into the text box.
|
| 537 |
+
4. **Generate Assessment:** Click the **Generate Assessment** button. The system will combine the ECG analysis and patient history (if provided) to generate a structured assessment using a Llama model via Groq. Results appear in the "Medical Assessment" box.
|
| 538 |
+
5. **Consult:** Use the chat interface at the bottom to ask follow-up questions. Type your question and click **Send** or press Enter. The chat uses the Llama model via Groq and considers the context provided above it.
|
| 539 |
+
|
| 540 |
+
---
|
| 541 |
+
## Important Disclaimer
|
| 542 |
+
|
| 543 |
+
* **Not a Medical Device:** This tool is for informational and educational purposes only. It is **NOT** a certified medical device and should **NOT** be used for primary diagnosis, treatment decisions, or emergency situations.
|
| 544 |
+
* **AI Limitations:** AI models can make mistakes, misinterpret images, or generate inaccurate information. The outputs are not a substitute for professional medical expertise.
|
| 545 |
+
* **Professional Judgment Required:** All outputs must be critically reviewed and verified by a qualified healthcare professional in the context of the patient's overall clinical picture. Do not rely solely on the AI's interpretation.
|
| 546 |
+
* **Data Privacy:** Ensure you comply with all applicable data privacy regulations (e.g., HIPAA) when using this tool. Avoid uploading identifiable patient information unless you have explicit consent and are operating within a secure, compliant environment. The developers of this interface are not responsible for data breaches resulting from user uploads.
|
| 547 |
+
* **API Keys:** Securely manage your `GEMINI_API_KEY` and `GROQ_API_KEY`. Do not expose them publicly.
|
| 548 |
+
* **No Liability:** Use this tool at your own risk. The creators assume no liability for any decisions made based on its output.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
""")
|
| 550 |
|
| 551 |
# --- Event Handlers ---
|
| 552 |
|
| 553 |
+
# Analyze Button: Input ECG Image -> Output ECG Analysis HTML
|
| 554 |
analyze_button.click(
|
| 555 |
+
fn=analyze_ecg_image,
|
| 556 |
inputs=[ecg_image],
|
| 557 |
outputs=ecg_analysis_output
|
| 558 |
)
|
| 559 |
|
| 560 |
+
# Load History Button: Input File -> Output Text into History Textbox
|
| 561 |
load_history_button.click(
|
| 562 |
+
fn=process_patient_history,
|
| 563 |
inputs=[patient_history_file],
|
| 564 |
outputs=[patient_history_text]
|
| 565 |
)
|
| 566 |
|
| 567 |
+
# Assess Button: Input ECG Analysis HTML, History Text -> Output Assessment HTML
|
| 568 |
assess_button.click(
|
| 569 |
+
fn=generate_assessment,
|
| 570 |
inputs=[ecg_analysis_output, patient_history_text],
|
| 571 |
outputs=assessment_output
|
| 572 |
)
|
| 573 |
|
| 574 |
+
# Chat Send Button: Input Message, Chat History, Context -> Output Cleared Message, Updated Chat History
|
| 575 |
chat_button.click(
|
| 576 |
+
fn=doctor_chat,
|
| 577 |
inputs=[message, chatbot, ecg_analysis_output, patient_history_text, assessment_output],
|
| 578 |
+
outputs=[message, chatbot]
|
| 579 |
)
|
| 580 |
|
| 581 |
+
# Chat Textbox Submit (Enter): Input Message, Chat History, Context -> Output Cleared Message, Updated Chat History
|
| 582 |
message.submit(
|
| 583 |
+
fn=doctor_chat,
|
| 584 |
inputs=[message, chatbot, ecg_analysis_output, patient_history_text, assessment_output],
|
| 585 |
+
outputs=[message, chatbot]
|
| 586 |
)
|
| 587 |
|
| 588 |
|
| 589 |
# Launch the app
|
| 590 |
if __name__ == "__main__":
|
| 591 |
+
print("===== Application Startup =====")
|
| 592 |
+
print(f"Attempting to launch Gradio app at {datetime.datetime.now()}")
|
| 593 |
+
# Add share=True for a temporary public link (use with caution regarding data privacy)
|
| 594 |
+
# Add debug=True for more detailed error output during development
|
| 595 |
+
app.launch()
|
| 596 |
+
# app.launch(share=True, debug=True)
|