File size: 21,566 Bytes
3d4910d 9933a4f 9b6e627 9933a4f 9b6e627 fb83103 9b6e627 fb83103 9b6e627 3d4910d 9b6e627 e7efe8e fb83103 9933a4f e7efe8e 9933a4f 9b6e627 fb83103 e7efe8e 9933a4f fb83103 e7efe8e 9933a4f e7efe8e 9933a4f fb83103 9933a4f fb83103 9933a4f e7efe8e fb83103 e7efe8e 9933a4f e7efe8e 9933a4f fb83103 e7efe8e fb83103 9933a4f fb83103 9933a4f 9b6e627 3d4910d 2b4803b e7efe8e 9b6e627 e7efe8e 3d4910d e7efe8e fb83103 e7efe8e fb83103 e7efe8e 3d4910d 9b6e627 e7efe8e 9b6e627 e7efe8e 9b6e627 b3f74df e7efe8e fb83103 b3f74df e7efe8e b3f74df e7efe8e b3f74df e7efe8e b3f74df e7efe8e 9b6e627 4750b07 3d4910d e7efe8e 9b6e627 e7efe8e 9b6e627 fb83103 9b6e627 e7efe8e 9b6e627 fb83103 9b6e627 e7efe8e 2b4803b e7efe8e 2b4803b 9b6e627 fb83103 9b6e627 e7efe8e 9b6e627 e7efe8e fb83103 e7efe8e 9933a4f e7efe8e 9b6e627 9933a4f fb83103 9b6e627 fb83103 9b6e627 fb83103 9933a4f fb83103 9933a4f fb83103 9933a4f fb83103 9933a4f fb83103 c606e96 9b6e627 fb83103 c606e96 fb83103 c606e96 9b6e627 c606e96 9b6e627 fb83103 c606e96 fb83103 c606e96 9b6e627 c606e96 fb83103 c606e96 9b6e627 fb83103 c606e96 fb83103 9b6e627 fb83103 e7efe8e 9933a4f 9b6e627 4750b07 9933a4f e7efe8e 9b6e627 e7efe8e 9b6e627 fb83103 9b6e627 fb83103 9b6e627 fb83103 e7efe8e 2b4803b e7efe8e 2b4803b 9b6e627 fb83103 9b6e627 e7efe8e 9b6e627 e7efe8e 9b6e627 e7efe8e 9933a4f 9b6e627 e7efe8e 9933a4f e7efe8e fb83103 9933a4f e7efe8e fb83103 e7efe8e fb83103 9933a4f e7efe8e 9933a4f fb83103 9933a4f e7efe8e 9b6e627 fb83103 9b6e627 fb83103 e7efe8e 9933a4f 9b6e627 e7efe8e 9933a4f 4750b07 9b6e627 4750b07 9933a4f e7efe8e 9b6e627 e7efe8e 9b6e627 e7efe8e 9933a4f 9b6e627 9933a4f 9b6e627 fb83103 9933a4f 9b6e627 9933a4f e7efe8e 9933a4f fb83103 e7efe8e 9933a4f e7efe8e fb83103 9b6e627 9933a4f e7efe8e 9933a4f e7efe8e 9b6e627 9933a4f fb83103 9933a4f e7efe8e fb83103 9b6e627 9933a4f e7efe8e 9933a4f e7efe8e 9933a4f e7efe8e 9933a4f e7efe8e 9b6e627 fb83103 e7efe8e fb83103 9b6e627 fb83103 9933a4f 9b6e627 fb83103 e7efe8e 9b6e627 fb83103 9933a4f e7efe8e fb83103 9933a4f fb83103 9b6e627 fb83103 9b6e627 fb83103 9b6e627 fb83103 9933a4f e7efe8e 9b6e627 9933a4f fb83103 e7efe8e 9933a4f e7efe8e 9933a4f fb83103 9933a4f e7efe8e 9933a4f fb83103 e7efe8e 9933a4f e7efe8e 9933a4f fb83103 e7efe8e fb83103 9933a4f e7efe8e 9933a4f fb83103 9933a4f fb83103 3d4910d fb83103 9b6e627 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 |
import os
import gradio as gr
import pandas as pd
from PIL import Image
import io
import datetime
import re
import traceback
import base64
from openai import OpenAI
# Initialize OpenAI client
try:
openai_client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
except Exception as e:
print(f"Error initializing OpenAI client: {e}")
openai_client = None
# Function to encode the image
def encode_image(image):
if isinstance(image, Image.Image):
# Convert PIL Image to bytes
buffered = io.BytesIO()
# Ensure image is in RGB format
if image.mode in ('RGBA', 'P', 'LA'):
image = image.convert('RGB')
image.save(buffered, format="JPEG")
image_bytes = buffered.getvalue()
return base64.b64encode(image_bytes).decode("utf-8")
elif isinstance(image, str) and os.path.exists(image):
with open(image, "rb") as image_file:
return base64.b64encode(image_file.read()).decode("utf-8")
return None
# Process patient history file
def process_patient_history(file):
if file is None:
return ""
try:
# Check file extension
file_path = file.name
file_ext = os.path.splitext(file_path)[1].lower()
if file_ext == '.txt':
# Read text file
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
return content
elif file_ext in ['.csv', '.xlsx', '.xls']:
# Read spreadsheet file
if file_ext == '.csv':
df = pd.read_csv(file_path)
else:
try:
df = pd.read_excel(file_path)
except ImportError:
return "Error: `openpyxl` needed for .xlsx files. Install with `pip install openpyxl`"
except Exception as e_excel:
return f"Error reading Excel file: {e_excel}"
# Convert dataframe to formatted string
formatted_data = "PATIENT INFORMATION:\n\n"
if not df.empty:
for column in df.columns:
value = df.iloc[0].get(column, 'N/A')
formatted_data += f"{column}: {str(value)}\n"
else:
formatted_data += "Spreadsheet is empty or format is not recognized correctly."
return formatted_data
else:
return f"Unsupported file format ({file_ext}). Please upload a .txt, .csv, or .xlsx file."
except AttributeError:
return "Error: Could not get file path from Gradio File object. Ensure a file was uploaded."
except FileNotFoundError:
return f"Error: File not found at path: {file_path}"
except Exception as e:
print(f"Error processing patient history file:\n{traceback.format_exc()}")
return f"Error processing patient history file: {str(e)}"
# Extract ECG readings from image using GPT-4.1
def analyze_ecg_image(image):
if image is None:
return "<strong style='color:red'>No image provided.</strong>"
# Ensure OpenAI client is initialized
if openai_client is None:
return "<strong style='color:red'>OpenAI client not initialized. Check API Key.</strong>"
# Ensure image is PIL Image
if not isinstance(image, Image.Image):
try:
if isinstance(image, str) and os.path.exists(image):
image = Image.open(image)
elif hasattr(image, 'name'):
image = Image.open(image.name)
else:
return f"<strong style='color:red'>Unrecognized image input format: {type(image)}</strong>"
except Exception as e:
print(f"Error opening image:\n{traceback.format_exc()}")
return f"<strong style='color:red'>Error opening image: {str(e)}</strong>"
try:
# Get current timestamp
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Convert image to base64
base64_image = encode_image(image)
if not base64_image:
return "<strong style='color:red'>Failed to encode image.</strong>"
# Create prompt for GPT-4.1
vision_prompt = f"""Analyze this ECG image carefully. You are a cardiologist analyzing an electrocardiogram (ECG).
IMPORTANT: Only report what you can actually see clearly displayed in this specific ECG screen. Do not include any measurements or values that are not visible or not displayed digitally in the image. Only create sections for values that are actually shown in the image.
Look for and extract visible measurements from the ECG display, which may include:
- Heart rate (if displayed digitally)
- Any numeric measurements shown on the screen
- Visible rhythm patterns
- Any clearly labeled values or measurements
Report exact numerical values where visible. If a value is not displayed or not visible, DO NOT include that section at all in your response.
Format your response strictly like this:
<h3>ECG Report</h3>
<ul>
<li><strong>Analysis Time:</strong> {timestamp}</li>
<!-- Only include the following if they are visible in the image -->
<!-- If Heart Rate is displayed: -->
<li><strong>Heart Rate:</strong> [visible value] bpm</li>
<!-- If other measurements are visible: -->
<!-- Add only visible measurements as list items -->
</ul>
<h3>Visible Findings</h3>
<ul>
<li>[Only observations of what is actually visible in the waveform]</li>
<li>[Only visible abnormalities, if any]</li>
</ul>
<h3>Visual Assessment</h3>
<p>[Brief summary based ONLY on what is visible in this specific ECG display]</p>
Critical rules:
- Do NOT add sections for measurements not visible in the image
- Do NOT write "Not determinable from image" for any parameter
- Only include data that you can actually see in this ECG screen
- Report only the exact values or descriptions visible in the image
- If certain standard ECG parameters are not shown, simply don't include them
"""
# Generate content using GPT-4.1
response = openai_client.responses.create(
model="gpt-4.1",
input=[
{
"role": "user",
"content": [
{ "type": "input_text", "text": vision_prompt },
{
"type": "input_image",
"image_url": f"data:image/jpeg;base64,{base64_image}",
},
],
}
]
)
ecg_analysis = response.output_text
# Basic post-processing to ensure HTML format
ecg_analysis = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', ecg_analysis)
ecg_analysis = re.sub(r'^\s*#+\s+(.*?)\s*$', r'<h3>\1</h3>', ecg_analysis, flags=re.MULTILINE)
ecg_analysis = re.sub(r'^\s*[\*-]\s+(.*?)\s*$', r'<li>\1</li>', ecg_analysis, flags=re.MULTILINE)
# Check if the response looks like the requested HTML structure
if not ("<h3>" in ecg_analysis and "<ul>" in ecg_analysis):
print(f"Warning: GPT-4.1 response might not be in the expected HTML format:\n{ecg_analysis[:500]}...")
return ecg_analysis
except Exception as e:
print(f"Error during GPT-4.1 ECG analysis:\n{traceback.format_exc()}")
error_type = type(e).__name__
return f"<strong style='color:red'>Error analyzing ECG image with GPT-4.1 ({error_type}):</strong> {str(e)}"
# Generate medical assessment based on ECG readings and patient history
def generate_assessment(ecg_analysis, patient_history=None):
if openai_client is None:
return "<strong style='color:red'>OpenAI client not initialized. Check API Key.</strong>"
if not ecg_analysis or ecg_analysis.startswith("<strong style='color:red'>"):
return "<strong style='color:red'>Cannot generate assessment. Please analyze a valid ECG image first.</strong>"
# Get current timestamp
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Clean up HTML tags for the prompt context
clean_ecg_analysis = re.sub('<[^>]+>', '', ecg_analysis)
# Construct prompt based on available information
prompt_parts = [
"You are a highly trained cardiologist assistant AI. Synthesize information from the ECG analysis and patient history (if provided) into a clinical assessment.",
"Focus on integrating the findings and suggesting potential implications and recommendations.",
"Format your response strictly using the specified HTML structure.",
"\nECG ANALYSIS SUMMARY (Provided):\n" + clean_ecg_analysis,
]
if patient_history and patient_history.strip():
prompt_parts.append("\nPATIENT HISTORY (Provided):\n" + patient_history)
else:
prompt_parts.append("\nPATIENT HISTORY: Not provided.")
prompt_parts.append(f"\nASSESSMENT TIMESTAMP: {timestamp}")
prompt_parts.append("""
Format your assessment using ONLY the following HTML structure:
<h3>Summary of Integrated Findings</h3>
<ul>
<li>[Combine key ECG findings with relevant patient history points]</li>
<li>[Finding 2]</li>
</ul>
<h3>Key Abnormalities and Concerns</h3>
<ul>
<li>[List specific significant abnormalities from the ECG]</li>
<li>[Use <span style="color:red"> for urgent/critical concerns]</li>
</ul>
<h3>Potential Clinical Implications</h3>
<ul>
<li>[Suggest possible underlying conditions or risks]</li>
<li>[Implication 2]</li>
</ul>
<h3>Recommendations for Physician Review</h3>
<ul>
<li>[Suggest next steps or urgency]</li>
<li>[Recommendation 2]</li>
</ul>
<h3>Differential Considerations (Optional)</h3>
<ul>
<li>[List possible alternative explanations if applicable]</li>
<li>[Differential 2]</li>
</ul>
Important Instructions:
- Adhere strictly to the HTML format
- Do NOT use markdown formatting
- Base your assessment ONLY on the provided information
- Do NOT make definitive diagnoses
""")
prompt = "\n".join(prompt_parts)
try:
assessment_completion = openai_client.responses.create(
model="gpt-4.1",
instructions="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.",
input=prompt
)
assessment_text = assessment_completion.output_text
# Basic post-processing
assessment_text = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', assessment_text)
assessment_text = re.sub(r'^\s*#+\s+(.*?)\s*$', r'<h3>\1</h3>', assessment_text, flags=re.MULTILINE)
# Check if the response contains the expected HTML structure
if not ("<h3>" in assessment_text and "<ul>" in assessment_text):
print(f"Warning: GPT-4.1 assessment response might not be in the expected HTML format:\n{assessment_text[:500]}...")
processed_text = assessment_text.replace('\n', '<br>')
assessment_text = f"<h3>Assessment (Raw Output)</h3><p>{processed_text}</p>"
return assessment_text
except Exception as e:
print(f"Error during GPT-4.1 assessment generation:\n{traceback.format_exc()}")
error_type = type(e).__name__
return f"<strong style='color:red'>Error generating assessment with GPT-4.1 ({error_type}):</strong> {str(e)}"
# Doctor's chat interaction with the model about the patient
def doctor_chat(message, chat_history, ecg_analysis, patient_history, assessment):
if openai_client is None:
chat_history.append((message, "<strong style='color:red'>Cannot start chat. OpenAI client not initialized. Check API Key.</strong>"))
return "", chat_history
# Check if ECG analysis exists and is not an error message
if not ecg_analysis or ecg_analysis.startswith("<strong style='color:red'>"):
chat_history.append((message, "<strong style='color:red'>Cannot start chat. Please analyze a valid ECG image first.</strong>"))
return "", chat_history
if not message.strip():
return "", chat_history
# Get current timestamp
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Clean inputs for context
clean_ecg = re.sub('<[^>]+>', '', ecg_analysis)
clean_assessment = re.sub('<[^>]+>', '', assessment) if assessment and not assessment.startswith("<strong style='color:red'>") else "Assessment not available or failed."
clean_history = patient_history if patient_history and patient_history.strip() else "No patient history provided."
# Prepare chat context
context = f"""CURRENT TIMESTAMP: {timestamp}
=== BEGIN PATIENT CONTEXT ===
PATIENT HISTORY:
{clean_history}
ECG ANALYSIS SUMMARY:
{clean_ecg}
GENERATED ASSESSMENT SUMMARY:
{clean_assessment}
=== END PATIENT CONTEXT ===
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.
"""
# Construct full chat history for context
messages = [
{
"role": "system",
"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}"
}
]
# Add chat history to the context
history_limit = 5
for user_msg, assistant_msg in chat_history[-history_limit:]:
messages.append({"role": "user", "content": [{"type": "input_text", "text": user_msg}]})
if isinstance(assistant_msg, str) and not assistant_msg.startswith("<strong style='color:red'>"):
messages.append({"role": "assistant", "content": assistant_msg})
# Add the current message
messages.append({"role": "user", "content": [{"type": "input_text", "text": message}]})
try:
# Format the messages for GPT-4.1 API
system_prompt = messages[0]["content"]
# Combine all subsequent messages into the input
user_messages = []
for msg in messages[1:]:
if msg["role"] == "user":
if isinstance(msg["content"], list):
for content in msg["content"]:
if isinstance(content, dict) and content.get("type") == "input_text":
user_messages.append(content["text"])
else:
user_messages.append(str(msg["content"]))
else:
user_messages.append(msg["content"])
combined_input = "\n\n".join(user_messages)
chat_completion = openai_client.responses.create(
model="gpt-4.1",
instructions=system_prompt,
input=combined_input
)
response = chat_completion.output_text
# Basic post-processing for the chat response
response = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', response)
response = response.replace('\n', '<br>')
chat_history.append((message, response))
return "", chat_history
except Exception as e:
print(f"Error during GPT-4.1 chat:\n{traceback.format_exc()}")
error_type = type(e).__name__
error_message = f"<strong style='color:red'>Error in chat ({error_type}):</strong> {str(e)}"
chat_history.append((message, error_message))
return "", chat_history
# Create Gradio interface
with gr.Blocks(title="Cardiac ECG Analysis System", theme=gr.themes.Soft()) as app:
gr.Markdown("# π« Cardiac ECG Analysis System")
gr.Markdown("Upload an ECG image and optional patient history for AI-assisted analysis, assessment, and consultation.")
with gr.Tabs():
with gr.TabItem("π» Main Interface"):
with gr.Row():
with gr.Column(scale=1):
# Input components
with gr.Group():
gr.Markdown("### π ECG Image Upload")
ecg_image = gr.Image(type="pil", label="Upload ECG Image", height=300)
gr.Markdown("**Vision Model: GPT-4.1**")
analyze_button = gr.Button("Analyze ECG Image", variant="primary")
with gr.Group():
gr.Markdown("### π Patient Information")
patient_history_text = gr.Textbox(
lines=8,
label="Patient History (Manual Entry or Loaded from File)",
placeholder="Enter relevant patient details OR upload a file and click Load."
)
patient_history_file = gr.File(
label="Upload Patient History File (Optional)",
file_types=[".txt", ".csv", ".xlsx", ".xls"]
)
load_history_button = gr.Button("Load Patient History from File")
with gr.Group():
gr.Markdown("### π§ Generate Assessment")
gr.Markdown("**Assessment/Chat Model: GPT-4.1**")
assess_button = gr.Button("Generate Assessment", variant="primary")
with gr.Column(scale=1):
# Output components
with gr.Group():
gr.Markdown("### π ECG Analysis Results")
ecg_analysis_output = gr.HTML(label="ECG Analysis", elem_id="ecg-analysis")
with gr.Group():
gr.Markdown("### π Medical Assessment")
assessment_output = gr.HTML(label="Assessment", elem_id="assessment-output")
gr.Markdown("---")
gr.Markdown("## π¨ββοΈ Doctor's Consultation Chat")
gr.Markdown("Ask follow-up questions based on the analysis and assessment above.")
with gr.Group():
chatbot = gr.Chatbot(
label="Consultation Log",
height=450,
bubble_full_width=False,
show_label=False
)
with gr.Row():
message = gr.Textbox(
label="Your Question",
placeholder="Type your question here and press Enter or click Send...",
scale=4,
show_label=False,
container=False,
)
chat_button = gr.Button("Send", scale=1, variant="primary")
with gr.TabItem("βΉοΈ Instructions & Disclaimer"):
gr.Markdown("""
## How to Use This Application
1. **Upload ECG:** Go to the "Main Interface" tab. Upload an ECG image using the designated area.
2. **Analyze ECG:** Click the **Analyze ECG Image** button. The system will analyze using GPT-4.1 and show results.
3. **Add Patient History (Optional):**
* Type relevant details directly into the "Patient History" text box.
* OR, upload a `.txt`, `.csv`, or `.xlsx` file and click **Load Patient History from File**.
4. **Generate Assessment:** Click the **Generate Assessment** button. Results appear in the "Medical Assessment" box.
5. **Consult:** Use the chat interface to ask follow-up questions about the analysis and assessment.
---
## Important Disclaimer
* **Not a Medical Device:** This tool is for informational purposes only. It is **NOT** a certified medical device.
* **AI Limitations:** AI models can make mistakes, misinterpret images, or generate inaccurate information.
* **Professional Judgment Required:** All outputs must be reviewed by a qualified healthcare professional.
* **No Liability:** Use this tool at your own risk. The creators assume no liability for any decisions made based on its output.
""")
# Event Handlers
analyze_button.click(
fn=analyze_ecg_image,
inputs=[ecg_image],
outputs=ecg_analysis_output
)
load_history_button.click(
fn=process_patient_history,
inputs=[patient_history_file],
outputs=[patient_history_text]
)
assess_button.click(
fn=generate_assessment,
inputs=[ecg_analysis_output, patient_history_text],
outputs=assessment_output
)
chat_button.click(
fn=doctor_chat,
inputs=[message, chatbot, ecg_analysis_output, patient_history_text, assessment_output],
outputs=[message, chatbot]
)
message.submit(
fn=doctor_chat,
inputs=[message, chatbot, ecg_analysis_output, patient_history_text, assessment_output],
outputs=[message, chatbot]
)
# Launch the app
if __name__ == "__main__":
print("===== Application Startup =====")
print(f"Attempting to launch Gradio app at {datetime.datetime.now()}")
app.launch() |