Update app.py
Browse files
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 |
-
|
10 |
-
|
11 |
-
|
12 |
|
13 |
-
# Initialize
|
14 |
-
# Ensure GROQ_API_KEY is set in your environment variables
|
15 |
try:
|
16 |
-
|
17 |
except Exception as e:
|
18 |
-
print(f"Error initializing
|
19 |
-
|
20 |
|
21 |
-
#
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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 |
-
|
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
|
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 |
-
#
|
136 |
-
|
137 |
-
|
138 |
-
|
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
|
160 |
</ul>
|
161 |
-
|
162 |
-
<h3>Key Observations / Potential Abnormalities</h3>
|
163 |
<ul>
|
164 |
-
<li>[
|
165 |
-
<li>[
|
166 |
-
<!-- List significant observations, use <span style="color:red"> for critical findings -->
|
167 |
</ul>
|
168 |
-
|
169 |
-
<
|
170 |
-
|
171 |
-
|
172 |
Important formatting instructions:
|
173 |
-
- Use EXACTLY the HTML structure shown
|
174 |
-
- Do NOT use markdown
|
175 |
-
- For
|
|
|
176 |
"""
|
177 |
|
178 |
-
# Generate content using
|
179 |
-
response =
|
180 |
-
model=
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
|
|
|
|
|
|
|
|
190 |
)
|
191 |
|
192 |
-
|
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
|
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)
|
223 |
|
224 |
-
#
|
225 |
if not ("<h3>" in ecg_analysis and "<ul>" in ecg_analysis):
|
226 |
-
print(f"Warning:
|
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 |
-
|
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
|
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
|
243 |
-
def generate_assessment(ecg_analysis, patient_history=None
|
244 |
-
|
245 |
-
|
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
|
258 |
-
|
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.
|
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,
|
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
|
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
|
289 |
-
<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
|
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
|
303 |
-
<li>[Recommendation 2
|
304 |
-
<!-- etc. -->
|
305 |
</ul>
|
306 |
-
|
307 |
<h3>Differential Considerations (Optional)</h3>
|
308 |
<ul>
|
309 |
-
<li>[List possible alternative explanations
|
310 |
<li>[Differential 2]</li>
|
311 |
-
<!-- etc. -->
|
312 |
</ul>
|
313 |
-
|
314 |
Important Instructions:
|
315 |
-
- Adhere strictly to the HTML format
|
316 |
-
- Do NOT use markdown formatting
|
317 |
-
- Base your assessment ONLY on the provided
|
318 |
-
- Do NOT make
|
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 =
|
325 |
-
|
|
|
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 |
-
|
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.
|
343 |
|
344 |
-
# Basic post-processing
|
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 |
-
#
|
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:
|
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
|
360 |
error_type = type(e).__name__
|
361 |
-
return f"<strong style='color:red'>Error generating assessment with
|
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 |
-
|
373 |
-
|
|
|
|
|
|
|
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
|
380 |
|
381 |
if not message.strip():
|
382 |
-
return "", chat_history
|
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
|
418 |
-
history_limit = 5
|
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 =
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
max_tokens=1024, # Adjust as needed
|
434 |
)
|
435 |
|
436 |
-
response = chat_completion.
|
437 |
|
438 |
-
# Basic post-processing for the chat response
|
439 |
-
response = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', response)
|
440 |
-
response = response.replace('\n', '<br>')
|
441 |
|
442 |
chat_history.append((message, response))
|
443 |
-
return "", chat_history
|
444 |
except Exception as e:
|
445 |
-
print(f"Error during
|
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
|
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
|
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 |
-
|
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("---")
|
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
|
507 |
)
|
508 |
with gr.Row():
|
509 |
message = gr.Textbox(
|
510 |
-
label="Your Question",
|
511 |
placeholder="Type your question here and press Enter or click Send...",
|
512 |
scale=4,
|
513 |
-
show_label=False,
|
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.
|
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
|
527 |
-
4. **Generate Assessment:** Click the **Generate Assessment** button.
|
528 |
-
5. **Consult:** Use the chat interface
|
529 |
-
|
530 |
---
|
531 |
## Important Disclaimer
|
532 |
-
|
533 |
-
* **
|
534 |
-
* **
|
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 |
-
#
|
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 |
-
|
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()
|
|
|
|
|
|