lyimo commited on
Commit
9b6e627
·
verified ·
1 Parent(s): 9e1a907

Update app.py

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