lyimo commited on
Commit
e7efe8e
Β·
verified Β·
1 Parent(s): c606e96

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +337 -270
app.py CHANGED
@@ -1,5 +1,5 @@
1
  import os
2
- import base64
3
  import gradio as gr
4
  import pandas as pd
5
  from groq import Groq
@@ -7,178 +7,225 @@ from PIL import Image
7
  import io
8
  import datetime
9
  import re
 
 
10
 
11
-
12
- # Initialize Groq client
13
- client = Groq(
14
  api_key=os.environ.get("GROQ_API_KEY")
15
  )
16
 
17
- # Function to encode images to base64
18
- def encode_image(image):
19
- buffered = io.BytesIO()
20
- image.save(buffered, format="JPEG")
21
- return base64.b64encode(buffered.getvalue()).decode('utf-8')
 
 
 
22
 
23
- # Process patient history file
24
  def process_patient_history(file):
25
  if file is None:
26
  return ""
27
-
28
  try:
29
  # Check file extension
30
  file_ext = os.path.splitext(file.name)[1].lower()
31
-
32
  if file_ext == '.txt':
33
  # Read text file
34
- content = file.read().decode('utf-8')
 
 
 
 
35
  return content
36
-
37
  elif file_ext in ['.csv', '.xlsx', '.xls']:
38
  # Read spreadsheet file
39
  if file_ext == '.csv':
40
  df = pd.read_csv(file.name)
41
  else:
42
  df = pd.read_excel(file.name)
43
-
44
  # Convert dataframe to formatted string
45
  formatted_data = "PATIENT INFORMATION:\n\n"
46
- for column in df.columns:
47
- formatted_data += f"{column}: {df.iloc[0][column]}\n"
48
-
 
 
 
 
 
 
49
  return formatted_data
50
-
51
  else:
52
  return "Unsupported file format. Please upload a .txt, .csv, or .xlsx file."
53
-
54
  except Exception as e:
55
  return f"Error processing patient history file: {str(e)}"
56
 
57
- # Extract ECG readings from image using Llama Vision model
58
- def analyze_ecg_image(image, vision_model="llama-3.2-90b-vision-preview"):
59
- # Fixed model - always use llama-3.2-90b-vision-preview
60
- vision_model = "llama-3.2-90b-vision-preview"
61
-
 
62
  if image is None:
63
  return "<strong style='color:red'>No image provided.</strong>"
64
-
65
- # Convert to PIL Image if needed
66
  if not isinstance(image, Image.Image):
67
- image = Image.open(image)
68
-
69
- # Encode the image
70
- base64_image = encode_image(image)
71
-
72
- # Get current timestamp
73
- timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
74
-
75
- # Create chat completion with vision model
76
- vision_prompt = f"""Analyze this ECG image carefully. You are a cardiologist analyzing an electrocardiogram (ECG).
77
-
78
- Extract and report all visible parameters, including but not limited to:
79
- 1. Heart rate
80
- 2. PR interval
81
- 3. QRS duration
82
- 4. QT/QTc interval
83
- 5. P wave morphology
84
- 6. ST segment changes
85
- 7. T wave morphology
86
- 8. Rhythm classification
87
- 9. Specific patterns (if any)
88
-
89
- Report exact numerical values where visible. Format your response using HTML list elements for better readability.
90
-
91
- If certain measurements aren't visible in the image, indicate that they cannot be determined.
92
-
93
- If you notice any abnormalities or concerning patterns, highlight them clearly but avoid making definitive diagnoses.
94
-
95
- Format your response like this:
96
- <h3>ECG Report</h3>
97
- <ul>
98
- <li><strong>Date and Time:</strong> {timestamp}</li>
99
- <li><strong>Heart Rate:</strong> [value] bpm</li>
100
- <li><strong>PR Interval:</strong> [value] ms</li>
101
- <!-- Other measurements -->
102
- </ul>
103
-
104
- <h3>Additional Observations</h3>
105
- <ul>
106
- <li>Observation 1</li>
107
- <li>Observation 2</li>
108
- </ul>
109
-
110
- <h3>Conclusion</h3>
111
- <p>[Your conclusion text]</p>
112
-
113
- Important formatting instructions:
114
- - Use HTML elements for structure (<ul>, <li>, <strong>, <h3>, etc.)
115
- - Do not use asterisks (**) for emphasis - use proper HTML formatting instead
116
- - For any urgent findings, use <span style="color:red"> to highlight them
117
- """
118
-
119
  try:
120
- vision_completion = client.chat.completions.create(
121
- messages=[
122
- {
123
- "role": "user",
124
- "content": [
125
- {"type": "text", "text": vision_prompt},
126
- {
127
- "type": "image_url",
128
- "image_url": {
129
- "url": f"data:image/jpeg;base64,{base64_image}",
130
- },
131
- },
132
- ],
133
- }
134
- ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  model=vision_model,
136
- temperature=0.2, # Lower temperature for more factual responses
137
- max_completion_tokens=1024,
 
 
 
 
 
 
 
138
  )
139
-
140
- ecg_analysis = vision_completion.choices[0].message.content
141
-
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  # Process the response to convert any remaining ** to HTML tags
143
  ecg_analysis = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', ecg_analysis)
144
-
145
  # Make sure all headers are properly formatted (with complete pattern)
146
  ecg_analysis = re.sub(r'^(#+)\s+(.+)$', r'<h3>\2</h3>', ecg_analysis, flags=re.MULTILINE)
147
-
148
  # If there's no HTML formatting at all, wrap in basic structure
149
  if not re.search(r'<[^>]+>', ecg_analysis):
150
  lines = ecg_analysis.split('\n')
151
  formatted_html = "<h3>ECG Report</h3>\n<ul>\n"
152
-
153
  for line in lines:
154
  if line.strip():
155
  # Try to identify key-value pairs
156
- match = re.match(r'^([^:]+):\s*(.+)$', line)
157
  if match:
158
  key, value = match.groups()
159
- formatted_html += f" <li><strong>{key}:</strong> {value}</li>\n"
160
  else:
161
- formatted_html += f" <li>{line}</li>\n"
162
-
163
  formatted_html += "</ul>"
164
  ecg_analysis = formatted_html
165
-
166
  return ecg_analysis
167
-
168
  except Exception as e:
169
- return f"<strong style='color:red'>Error analyzing ECG image:</strong> {str(e)}"
170
-
171
- # Generate medical assessment based on ECG readings and patient history
172
- def generate_assessment(ecg_analysis, patient_history=None, chat_model="llama-3.3-70b-versatile"):
173
- # Fixed model - always use llama-3.3-70b-versatile
174
- chat_model = "llama-3.3-70b-versatile"
175
-
176
- if not ecg_analysis or ecg_analysis.startswith("<strong style='color:red'>Error"):
 
 
 
 
177
  return "<strong style='color:red'>Please analyze an ECG image first.</strong>"
178
-
179
  # Get current timestamp
180
  timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
181
-
182
  # Construct prompt based on available information
183
  if patient_history and patient_history.strip():
184
  prompt = f"""You are a highly trained cardiologist assistant. Based on the ECG analysis below and the patient's history, provide a comprehensive assessment of the patient's cardiac status. Indicate clearly if there are any concerning findings that require immediate medical attention.
@@ -283,9 +330,9 @@ Important formatting instructions:
283
  - Do not use asterisks (**) for emphasis - use proper HTML formatting instead
284
  - For any urgent findings, use <span style="color:red"> to highlight them
285
  """
286
-
287
  try:
288
- assessment_completion = client.chat.completions.create(
289
  messages=[
290
  {
291
  "role": "system",
@@ -297,265 +344,285 @@ Important formatting instructions:
297
  }
298
  ],
299
  model=chat_model,
300
- temperature=0.2, # Lower temperature for more factual responses
301
- max_completion_tokens=2048,
302
  )
303
-
304
  assessment_text = assessment_completion.choices[0].message.content
305
-
306
  # Process the response to convert any remaining ** to HTML tags
307
  assessment_text = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', assessment_text)
308
-
309
  # Make sure all headers are properly formatted (with complete pattern)
310
  assessment_text = re.sub(r'^(#+)\s+(.+)$', r'<h3>\2</h3>', assessment_text, flags=re.MULTILINE)
311
-
312
- # If there's no HTML formatting at all, wrap in basic structure
313
  if not re.search(r'<[^>]+>', assessment_text):
314
  sections = [
315
- "Summary of Findings",
316
- "Key Abnormalities",
317
- "Potential Clinical Implications",
318
- "Recommendation",
319
  "Differential Considerations"
320
  ]
321
-
322
- # Split by sections and format
323
  formatted_html = ""
324
- current_section = "Summary of Findings" # Default
325
- content_lines = []
326
-
327
  lines = assessment_text.split('\n')
328
-
 
 
 
 
 
 
 
 
 
 
329
  for line in lines:
330
- # Check if this line is a section header
331
- is_section = False
332
- for section in sections:
333
- if section.lower() in line.lower():
334
- # If we have collected content for a previous section, output it
335
- if content_lines:
336
- formatted_html += f"<h3>{current_section}</h3>\n<ul>\n"
337
- for content_line in content_lines:
338
- formatted_html += f" <li>{content_line}</li>\n"
339
- formatted_html += "</ul>\n"
340
- content_lines = []
341
-
342
- current_section = section
343
- is_section = True
344
- break
345
-
346
- if not is_section and line.strip():
347
- content_lines.append(line.strip())
348
-
349
- # Don't forget the last section
350
- if content_lines:
351
- formatted_html += f"<h3>{current_section}</h3>\n<ul>\n"
352
- for content_line in content_lines:
353
- formatted_html += f" <li>{content_line}</li>\n"
354
- formatted_html += "</ul>\n"
355
-
356
- assessment_text = formatted_html if formatted_html else assessment_text
357
-
358
  return assessment_text
359
-
360
  except Exception as e:
 
 
361
  return f"<strong style='color:red'>Error generating assessment:</strong> {str(e)}"
362
 
363
- # Doctor's chat interaction with the model about the patient
364
- def doctor_chat(message, chat_history, ecg_analysis, patient_history, assessment, chat_model="llama-3.3-70b-versatile"):
365
- # Fixed model - always use llama-3.3-70b-versatile
366
- chat_model = "llama-3.3-70b-versatile"
367
-
368
- if not ecg_analysis or ecg_analysis.startswith("<strong style='color:red'>Error"):
369
- return "Please analyze an ECG image first before starting a chat.", chat_history
370
-
 
 
 
 
371
  if not message.strip():
372
- return "", chat_history
373
-
374
  # Get current timestamp
375
  timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
376
-
377
  # Prepare chat context
378
- context = f"""ECG ANALYSIS:
 
 
 
 
 
 
379
  {ecg_analysis}
380
 
381
  MEDICAL ASSESSMENT:
382
- {assessment}
 
383
 
384
- TIMESTAMP: {timestamp}
385
  """
386
-
387
- if patient_history and patient_history.strip():
388
- context += f"""PATIENT HISTORY:
389
- {patient_history}
390
- """
391
-
392
  # Construct full chat history for context
393
  messages = [
394
  {
395
  "role": "system",
396
- "content": f"You are a medical AI assistant specialized in cardiology. You are helping a doctor interpret ECG results and patient data. Answer the doctor's questions based on the following information:\n\n{context}"
397
  }
398
  ]
399
-
400
- # Add chat history to the context (limited to last 10 exchanges to avoid token limits)
401
- for entry in chat_history[-10:]:
402
- messages.append({"role": "user", "content": entry[0]})
403
- messages.append({"role": "assistant", "content": entry[1]})
404
-
 
 
405
  # Add the current message
406
  messages.append({"role": "user", "content": message})
407
-
408
  try:
409
- chat_completion = client.chat.completions.create(
410
  messages=messages,
411
  model=chat_model,
412
  temperature=0.3,
413
- max_completion_tokens=1024,
414
  )
415
-
416
  response = chat_completion.choices[0].message.content
417
-
418
  # Process any remaining asterisks to HTML tags in the response
419
  response = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', response)
420
-
421
  chat_history.append((message, response))
422
- return "", chat_history
423
-
424
  except Exception as e:
 
 
425
  error_message = f"<strong style='color:red'>Error in chat:</strong> {str(e)}"
426
  chat_history.append((message, error_message))
427
- return "", chat_history
428
 
429
  # Create Gradio interface
430
  with gr.Blocks(title="Cardiac ECG Analysis System", theme=gr.themes.Soft()) as app:
431
- # Session state to store data
432
- ecg_analysis_state = gr.State("")
433
-
434
  gr.Markdown("# πŸ«€ Cardiac ECG Analysis System")
435
  gr.Markdown("Upload an ECG image and optional patient history to get an automated analysis and assessment.")
436
-
437
  with gr.Tabs():
438
  with gr.TabItem("πŸ’» Main Interface"):
439
  with gr.Row():
440
  with gr.Column(scale=1):
441
  # Input components
442
- with gr.Group(): # Using Group instead of Box
443
  gr.Markdown("### πŸ“Š ECG Image")
444
  ecg_image = gr.Image(type="pil", label="Upload ECG Image")
445
- # Display fixed model info
446
- gr.Markdown("**Vision Model:** llama-3.2-90b-vision-preview")
447
  analyze_button = gr.Button("Analyze ECG Image", variant="primary")
448
-
449
- with gr.Group(): # Using Group instead of Box
450
  gr.Markdown("### πŸ“‹ Patient Information")
451
  patient_history_text = gr.Textbox(
452
- lines=8,
453
- label="Patient History (Manual Entry)",
454
- placeholder="Enter patient's medical history, age, sex, symptoms, medications, etc."
455
  )
456
  patient_history_file = gr.File(
457
  label="Upload Patient History File (Optional, .txt, .csv, or .xlsx)",
458
  file_types=[".txt", ".csv", ".xlsx", ".xls"]
459
  )
460
  load_history_button = gr.Button("Load Patient History from File")
461
-
462
- with gr.Group(): # Using Group instead of Box
463
  gr.Markdown("### 🧠 Assessment Settings")
464
- # Display fixed model info
465
- gr.Markdown("**Chat Model:** llama-3.3-70b-versatile")
 
466
  assess_button = gr.Button("Generate Assessment", variant="primary")
467
-
468
  with gr.Column(scale=1):
469
  # Output components
470
- with gr.Group(): # Using Group instead of Box
471
  gr.Markdown("### πŸ“ˆ ECG Analysis Results")
472
  ecg_analysis_output = gr.HTML(label="ECG Analysis", elem_id="ecg-analysis")
473
-
474
- with gr.Group(): # Using Group instead of Box
475
  gr.Markdown("### πŸ“ Medical Assessment")
476
  assessment_output = gr.HTML(label="Assessment", elem_id="assessment-output")
477
-
478
  gr.Markdown("## πŸ‘¨β€βš•οΈ Doctor's Consultation")
479
  gr.Markdown("Ask follow-up questions about the patient's ECG results and medical condition.")
480
-
481
- with gr.Group(): # Using Group instead of Box
482
- chatbot = gr.Chatbot(label="Consultation", height=400)
483
  with gr.Row():
484
  message = gr.Textbox(
485
- lines=2,
486
  label="Doctor's Question",
487
  placeholder="Ask a question about this patient's cardiac status...",
488
- scale=4
 
 
489
  )
490
  chat_button = gr.Button("Send", scale=1, variant="primary")
491
-
492
  with gr.TabItem("ℹ️ Instructions"):
493
  gr.Markdown("""
494
  ## How to Use This Application
495
-
496
  ### Step 1: Upload and Analyze ECG
497
- 1. Upload an ECG image using the file uploader in the Main Interface tab
498
- 2. Click "Analyze ECG Image" to extract readings from the image
499
-
 
500
  ### Step 2: Add Patient Information (Optional)
501
- - Enter patient history directly in the text box, OR
502
- - Upload a patient history file (.txt, .csv, or .xlsx) and click "Load Patient History from File"
503
-
504
  ### Step 3: Generate Assessment
505
- - Click "Generate Assessment" to get an AI-assisted interpretation
506
-
 
507
  ### Step 4: Consultation
508
- - Use the chatbot interface to ask follow-up questions
509
- - The AI will consider the ECG analysis, patient history, and previous assessment in its responses
510
-
 
511
  ### Important Notes
512
- - This tool is designed to assist healthcare professionals, not replace clinical judgment
513
- - Always validate AI-generated medical interpretations with proper medical expertise
514
- - Patient data privacy should be maintained according to relevant regulations
 
515
  """)
516
-
517
- # Set up event handlers
 
 
518
  analyze_button.click(
519
- analyze_ecg_image,
520
- inputs=[ecg_image],
521
  outputs=ecg_analysis_output
522
- ).then(
523
- lambda x: x, # Pass through function to update state
524
- inputs=ecg_analysis_output,
525
- outputs=ecg_analysis_state
526
  )
527
-
528
- def process_and_update_history(file):
529
- if file is None:
530
- return "No file uploaded."
531
- processed_text = process_patient_history(file)
532
- return processed_text
533
-
534
  load_history_button.click(
535
- process_and_update_history,
536
  inputs=[patient_history_file],
537
  outputs=[patient_history_text]
538
  )
539
-
 
540
  assess_button.click(
541
- generate_assessment,
542
- inputs=[ecg_analysis_output, patient_history_text],
543
  outputs=assessment_output
544
  )
545
-
 
546
  chat_button.click(
547
- doctor_chat,
548
- inputs=[message, chatbot, ecg_analysis_output, patient_history_text, assessment_output],
549
- outputs=[message, chatbot]
550
  )
551
-
552
- # Also trigger chat on Enter key
553
  message.submit(
554
  doctor_chat,
555
  inputs=[message, chatbot, ecg_analysis_output, patient_history_text, assessment_output],
556
- outputs=[message, chatbot]
557
  )
558
 
 
559
  # Launch the app
560
  if __name__ == "__main__":
 
 
561
  app.launch()
 
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
 
7
  import io
8
  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
+ groq_client = Groq(
 
15
  api_key=os.environ.get("GROQ_API_KEY")
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
+ # Function to encode images to base64 - REMOVED as not needed for Gemini bytes input
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
  file_ext = os.path.splitext(file.name)[1].lower()
35
+
36
  if file_ext == '.txt':
37
  # Read text file
38
+ # Gradio File object might need .name for path, let's assume it provides a file-like object
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(file.name)
49
  else:
50
  df = pd.read_excel(file.name)
51
+
52
  # Convert dataframe to formatted string
53
  formatted_data = "PATIENT INFORMATION:\n\n"
54
+ if not df.empty:
55
+ # Assuming the first row contains the relevant patient data
56
+ for column in df.columns:
57
+ # Handle potential missing values gracefully
58
+ value = df.iloc[0].get(column, 'N/A')
59
+ formatted_data += f"{column}: {value}\n"
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
+
72
+ # Extract ECG readings from image using Gemini Vision model
73
+ def analyze_ecg_image(image, vision_model="gemini-2.0-flash-exp"):
74
+ # Fixed model
75
+ vision_model = "gemini-2.0-flash-exp"
76
+
77
  if image is None:
78
  return "<strong style='color:red'>No image provided.</strong>"
79
+
80
+ # Ensure image is PIL Image
81
  if not isinstance(image, Image.Image):
82
+ try:
83
+ image = Image.open(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")
91
+ if not gemini_api_key:
92
+ return "<strong style='color:red'>GEMINI_API_KEY environment variable not set.</strong>"
93
+
94
+ # Initialize Gemini client
95
+ # Using configure for simplicity if preferred, or Client() directly
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
+ image.save(buffered, format="JPEG")
107
+ image_bytes = buffered.getvalue()
108
+
109
+ # Get current timestamp (computer time)
110
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
111
+
112
+ # Create prompt for Gemini
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. Specific patterns (if any)
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 in the image, indicate that they cannot be determined.
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>Date and Time:</strong> {timestamp}</li>
136
+ <li><strong>Heart Rate:</strong> [value] bpm</li>
137
+ <li><strong>PR Interval:</strong> [value] ms</li>
138
+ <!-- Other measurements -->
139
+ </ul>
140
+
141
+ <h3>Additional Observations</h3>
142
+ <ul>
143
+ <li>Observation 1</li>
144
+ <li>Observation 2</li>
145
+ </ul>
146
+
147
+ <h3>Conclusion</h3>
148
+ <p>[Your conclusion text]</p>
149
+
150
+ Important formatting instructions:
151
+ - Use HTML elements for structure (<ul>, <li>, <strong>, <h3>, etc.)
152
+ - Do not use asterisks (**) for emphasis - use proper HTML formatting instead
153
+ - For any urgent findings, use <span style="color:red"> to highlight them
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/jpeg")]
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"},
165
+ # {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
166
+ # {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
167
+ # ]
168
  )
169
+
170
+ # Handle potential blocks or errors in response
171
+ if not response.candidates:
172
+ # Check finish_reason if available, e.g., safety settings block
173
+ finish_reason = getattr(response, 'prompt_feedback', None)
174
+ if finish_reason and getattr(finish_reason, 'block_reason', None):
175
+ return f"<strong style='color:red'>Analysis blocked due to: {finish_reason.block_reason}. Check safety settings or content.</strong>"
176
+ else:
177
+ return "<strong style='color:red'>No content generated by the model. The request might have been blocked or failed.</strong>"
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
  # Process the response to convert any remaining ** to HTML tags
186
  ecg_analysis = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', ecg_analysis)
187
+
188
  # Make sure all headers are properly formatted (with complete pattern)
189
  ecg_analysis = re.sub(r'^(#+)\s+(.+)$', r'<h3>\2</h3>', ecg_analysis, flags=re.MULTILINE)
190
+
191
  # If there's no HTML formatting at all, wrap in basic structure
192
  if not re.search(r'<[^>]+>', ecg_analysis):
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
+ import traceback
214
+ print(traceback.format_exc()) # Print full traceback to console for debugging
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 (Unchanged - uses Groq)
219
+ def generate_assessment(ecg_analysis, patient_history=None, chat_model="llama-3.1-70b-versatile"): # Adjusted default model slightly if needed
220
+ # Fixed model
221
+ chat_model = "llama-3.1-70b-versatile" # Or keep llama-3.3 if available/preferred
222
+
223
+ if not ecg_analysis or ecg_analysis.startswith("<strong style='color:red'>"):
224
  return "<strong style='color:red'>Please analyze an ECG image first.</strong>"
225
+
226
  # Get current timestamp
227
  timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
228
+
229
  # Construct prompt based on available information
230
  if patient_history and patient_history.strip():
231
  prompt = f"""You are a highly trained cardiologist assistant. Based on the ECG analysis below and the patient's history, provide a comprehensive assessment of the patient's cardiac status. Indicate clearly if there are any concerning findings that require immediate medical attention.
 
330
  - Do not use asterisks (**) for emphasis - use proper HTML formatting instead
331
  - For any urgent findings, use <span style="color:red"> to highlight them
332
  """
333
+
334
  try:
335
+ assessment_completion = groq_client.chat.completions.create(
336
  messages=[
337
  {
338
  "role": "system",
 
344
  }
345
  ],
346
  model=chat_model,
347
+ temperature=0.2,
348
+ max_tokens=2048, # Note: Groq uses max_tokens
349
  )
350
+
351
  assessment_text = assessment_completion.choices[0].message.content
352
+
353
  # Process the response to convert any remaining ** to HTML tags
354
  assessment_text = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', assessment_text)
355
+
356
  # Make sure all headers are properly formatted (with complete pattern)
357
  assessment_text = re.sub(r'^(#+)\s+(.+)$', r'<h3>\2</h3>', assessment_text, flags=re.MULTILINE)
358
+
359
+ # If there's no HTML formatting at all, wrap in basic structure (Improved fallback)
360
  if not re.search(r'<[^>]+>', assessment_text):
361
  sections = [
362
+ "Summary of Findings",
363
+ "Key Abnormalities",
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
+ import traceback
413
+ print(traceback.format_exc()) # Print full traceback
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 = "llama-3.1-70b-versatile" # Or keep llama-3.3 if available/preferred
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 instead of returning it directly
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
+
428
  if not message.strip():
429
+ return "", chat_history # Ignore empty messages
430
+
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
+ {patient_history if patient_history and patient_history.strip() else "No patient history provided."}
440
+
441
+ ECG ANALYSIS:
442
  {ecg_analysis}
443
 
444
  MEDICAL ASSESSMENT:
445
+ {assessment if assessment and not assessment.startswith("<strong style='color:red'>") else "No assessment generated yet or assessment failed."}
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 medical AI assistant specialized in cardiology, conversing with a doctor. Your knowledge is strictly limited to the patient information provided in the context below. Do not invent information or access external knowledge.\n\n{context}"
456
  }
457
  ]
458
+
459
+ # Add chat history to the context (limited to last 5 exchanges to avoid token limits)
460
+ for user_msg, assistant_msg in chat_history[-5:]:
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
467
  messages.append({"role": "user", "content": message})
468
+
469
  try:
470
+ chat_completion = groq_client.chat.completions.create(
471
  messages=messages,
472
  model=chat_model,
473
  temperature=0.3,
474
+ max_tokens=1024, # Note: Groq uses max_tokens
475
  )
476
+
477
  response = chat_completion.choices[0].message.content
478
+
479
  # Process any remaining asterisks to HTML tags in the response
480
  response = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', response)
481
+
482
  chat_history.append((message, response))
483
+ return "", chat_history # Clear input message box
 
484
  except Exception as e:
485
+ import traceback
486
+ print(traceback.format_exc()) # Print full traceback
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 to get an automated analysis and assessment.")
498
+
499
  with gr.Tabs():
500
  with gr.TabItem("πŸ’» Main Interface"):
501
  with gr.Row():
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
+ # Display UPDATED fixed model info for vision
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():
512
  gr.Markdown("### πŸ“‹ Patient Information")
513
  patient_history_text = gr.Textbox(
514
+ lines=8,
515
+ label="Patient History (Manual Entry or Loaded from File)",
516
+ placeholder="Enter patient's medical history, age, sex, symptoms, medications, etc. OR upload a file below and click Load."
517
  )
518
  patient_history_file = gr.File(
519
  label="Upload Patient History File (Optional, .txt, .csv, or .xlsx)",
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 Settings")
526
+ # Display fixed model info for chat/assessment
527
+ # Make sure this model name matches the one used in generate_assessment/doctor_chat
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):
532
  # Output components
533
+ with gr.Group():
534
  gr.Markdown("### πŸ“ˆ ECG Analysis Results")
535
  ecg_analysis_output = gr.HTML(label="ECG Analysis", elem_id="ecg-analysis")
536
+
537
+ with gr.Group():
538
  gr.Markdown("### πŸ“ Medical Assessment")
539
  assessment_output = gr.HTML(label="Assessment", elem_id="assessment-output")
540
+
541
  gr.Markdown("## πŸ‘¨β€βš•οΈ Doctor's Consultation")
542
  gr.Markdown("Ask follow-up questions about the patient's ECG results and medical condition.")
543
+
544
+ with gr.Group():
545
+ chatbot = gr.Chatbot(label="Consultation", height=400, bubble_full_width=False)
546
  with gr.Row():
547
  message = gr.Textbox(
548
+ lines=2,
549
  label="Doctor's Question",
550
  placeholder="Ask a question about this patient's cardiac status...",
551
+ scale=4,
552
+ show_label=False,
553
+ container=False, # Makes textbox slimmer vertically
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
  ### Step 1: Upload and Analyze ECG
562
+ 1. Upload an ECG image using the file uploader in the "Main Interface" tab.
563
+ 2. Click **Analyze ECG Image**. The system will use Gemini Vision to interpret the image.
564
+ 3. Wait for the "ECG Analysis Results" to appear.
565
+
566
  ### Step 2: Add Patient Information (Optional)
567
+ * Enter patient history directly into the "Patient History" text box, OR
568
+ * Upload a patient history file (.txt, .csv, or .xlsx) and click **Load Patient History from File**. The content will populate the text box.
569
+
570
  ### Step 3: Generate Assessment
571
+ * Once the ECG analysis is complete, click **Generate Assessment**. The system will use a Llama model (via Groq) combining the ECG analysis and patient history (if provided).
572
+ * Wait for the "Medical Assessment" to appear.
573
+
574
  ### Step 4: Consultation
575
+ * Use the chat interface at the bottom to ask follow-up questions.
576
+ * Type your question and click **Send** or press Enter.
577
+ * The AI (Llama via Groq) will consider the ECG analysis, patient history, and the generated assessment in its responses.
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
+ # Chain analysis button click to update output
589
  analyze_button.click(
590
+ analyze_ecg_image,
591
+ inputs=[ecg_image],
592
  outputs=ecg_analysis_output
 
 
 
 
593
  )
594
+
595
+ # Load history file content into the textbox
 
 
 
 
 
596
  load_history_button.click(
597
+ process_patient_history,
598
  inputs=[patient_history_file],
599
  outputs=[patient_history_text]
600
  )
601
+
602
+ # Generate assessment based on ECG output and history textbox
603
  assess_button.click(
604
+ generate_assessment,
605
+ inputs=[ecg_analysis_output, patient_history_text],
606
  outputs=assessment_output
607
  )
608
+
609
+ # Handle chat interactions
610
  chat_button.click(
611
+ doctor_chat,
612
+ inputs=[message, chatbot, ecg_analysis_output, patient_history_text, assessment_output],
613
+ outputs=[message, chatbot] # Clear message input, update chatbot history
614
  )
615
+
616
+ # Also trigger chat on Enter key press in the message textbox
617
  message.submit(
618
  doctor_chat,
619
  inputs=[message, chatbot, ecg_analysis_output, patient_history_text, assessment_output],
620
+ outputs=[message, chatbot] # Clear message input, update chatbot history
621
  )
622
 
623
+
624
  # Launch the app
625
  if __name__ == "__main__":
626
+ # For development, you might want debug=True
627
+ # app.launch(debug=True)
628
  app.launch()