Ashkan Taghipour (The University of Western Australia) commited on
Commit
3fc2595
·
1 Parent(s): a13f951

Redesign UI: Modern dashboard with animated header

Browse files
Files changed (1) hide show
  1. app.py +321 -41
app.py CHANGED
@@ -33,11 +33,20 @@ logger = logging.getLogger(__name__)
33
  # Global inference engine
34
  inference_engine = None
35
 
36
- # Sample ECG descriptions
 
 
 
 
 
 
 
 
 
37
  SAMPLE_DESCRIPTIONS = {
38
- "Normal Sinus Rhythm": "A healthy heart rhythm with regular beats originating from the sinus node.",
39
- "Atrial Flutter": "A rapid but regular atrial rhythm, typically around 250-350 bpm in the atria.",
40
- "Ventricular Tachycardia": "A fast heart rhythm originating from the ventricles, potentially life-threatening.",
41
  }
42
 
43
 
@@ -61,11 +70,14 @@ def get_sample_ecgs():
61
 
62
  samples = []
63
  for npy_file in sorted(sample_dir.glob("*.npy")):
64
- name = npy_file.stem.replace("_", " ").title()
 
 
65
  samples.append({
66
  "path": str(npy_file),
67
- "name": name,
68
- "description": SAMPLE_DESCRIPTIONS.get(name, "Sample ECG recording")
 
69
  })
70
  logger.info(f"Found {len(samples)} sample ECGs")
71
  return samples
@@ -105,13 +117,50 @@ def analyze_ecg(ecg_signal: np.ndarray, filename: str = "ECG Analysis"):
105
  afib_5y = results.get("afib_5y", 0.0)
106
  risk_fig = plot_risk_gauges(lvef_40, lvef_50, afib_5y)
107
 
108
- # Generate summary text
109
  inference_time = results.get("inference_time_ms", 0)
110
- summary = f"""## Analysis Results: {filename}
111
 
112
- **Inference Time:** {inference_time:.1f} ms
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
  ### Risk Predictions
 
115
  | Risk Factor | Probability |
116
  |-------------|-------------|
117
  | LVEF < 40% | {lvef_40*100:.1f}% |
@@ -119,15 +168,13 @@ def analyze_ecg(ecg_signal: np.ndarray, filename: str = "ECG Analysis"):
119
  | 5-year AFib Risk | {afib_5y*100:.1f}% |
120
 
121
  ### Top 5 Diagnoses
 
 
 
 
 
 
122
  """
123
- if "diagnosis_77" in results:
124
- probs = results["diagnosis_77"]["probabilities"]
125
- class_names = results["diagnosis_77"]["class_names"]
126
- top_indices = np.argsort(probs)[::-1][:5]
127
- for i, idx in enumerate(top_indices, 1):
128
- prob_pct = probs[idx] * 100
129
- bar = "█" * int(prob_pct / 10) + "░" * (10 - int(prob_pct / 10))
130
- summary += f"| {i}. {class_names[idx]} | {bar} {prob_pct:.1f}% |\n"
131
 
132
  return ecg_fig, diagnosis_fig, risk_fig, summary
133
 
@@ -173,29 +220,123 @@ def create_demo_interface():
173
  samples = get_sample_ecgs()
174
  sample_names = [s["name"] for s in samples]
175
 
176
- # Custom CSS for styling
177
  custom_css = """
178
  .gradio-container {
179
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
180
  }
 
 
181
  .main-header {
182
  text-align: center;
183
- padding: 24px;
184
- background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
 
 
185
  color: white;
186
- border-radius: 12px;
187
- margin-bottom: 20px;
188
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
 
 
189
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  .main-header h1 {
191
  margin: 0;
192
- font-size: 2.5em;
 
 
 
193
  }
 
194
  .main-header p {
195
- margin: 10px 0 0 0;
196
  opacity: 0.95;
197
- font-size: 1.1em;
 
 
198
  }
 
199
  .sample-card {
200
  padding: 16px;
201
  border-radius: 8px;
@@ -203,21 +344,155 @@ def create_demo_interface():
203
  margin: 8px 0;
204
  border-left: 4px solid #e74c3c;
205
  }
 
206
  .quick-start {
207
- background: #e8f5e9;
208
- padding: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  border-radius: 8px;
210
- margin: 16px 0;
211
- border-left: 4px solid #4caf50;
 
 
 
212
  }
213
  """
214
 
215
  with gr.Blocks(css=custom_css, title="HeartWatch AI", theme=gr.themes.Soft()) as demo:
216
- # Header
217
  gr.HTML("""
218
  <div class="main-header">
219
- <h1>❤️ HeartWatch AI</h1>
220
- <p>AI-Powered 12-Lead ECG Analysis</p>
 
 
 
 
 
 
 
 
 
 
221
  </div>
222
  """)
223
 
@@ -374,13 +649,18 @@ def create_demo_interface():
374
  *Built with Gradio and PyTorch*
375
  """)
376
 
377
- # Footer
378
- gr.Markdown("""
379
- ---
380
- <center>
381
- Made with ❤️ for cardiac health research |
382
- <a href="https://huggingface.co/spaces/AshkanTaghipour/HeartWatchAI">HuggingFace Space</a>
383
- </center>
 
 
 
 
 
384
  """)
385
 
386
  return demo
 
33
  # Global inference engine
34
  inference_engine = None
35
 
36
+ # Sample ECG descriptions - mapped by file stem (with underscores replaced by spaces and title-cased)
37
+ # The files are: Atrial_Flutter.npy, Normal_Sinus_Rhythm.npy, Ventricular_Tachycardia.npy
38
+ # They get sorted alphabetically: Atrial Flutter, Normal Sinus Rhythm, Ventricular Tachycardia
39
+ # We want to display them as Sample 1, Sample 2, Sample 3
40
+ SAMPLE_FILE_TO_DISPLAY = {
41
+ "Atrial Flutter": "Sample 1",
42
+ "Normal Sinus Rhythm": "Sample 2",
43
+ "Ventricular Tachycardia": "Sample 3",
44
+ }
45
+
46
  SAMPLE_DESCRIPTIONS = {
47
+ "Sample 1": "Atrial Flutter - A rapid but regular atrial rhythm, typically around 250-350 bpm in the atria.",
48
+ "Sample 2": "Normal Sinus Rhythm - A healthy heart rhythm with regular beats originating from the sinus node.",
49
+ "Sample 3": "Ventricular Tachycardia - A fast heart rhythm originating from the ventricles, potentially life-threatening.",
50
  }
51
 
52
 
 
70
 
71
  samples = []
72
  for npy_file in sorted(sample_dir.glob("*.npy")):
73
+ original_name = npy_file.stem.replace("_", " ").title()
74
+ # Map to new display name (Sample 1, Sample 2, Sample 3)
75
+ display_name = SAMPLE_FILE_TO_DISPLAY.get(original_name, original_name)
76
  samples.append({
77
  "path": str(npy_file),
78
+ "name": display_name,
79
+ "original_name": original_name,
80
+ "description": SAMPLE_DESCRIPTIONS.get(display_name, "Sample ECG recording")
81
  })
82
  logger.info(f"Found {len(samples)} sample ECGs")
83
  return samples
 
117
  afib_5y = results.get("afib_5y", 0.0)
118
  risk_fig = plot_risk_gauges(lvef_40, lvef_50, afib_5y)
119
 
120
+ # Generate modern HTML summary with styled diagnosis cards
121
  inference_time = results.get("inference_time_ms", 0)
 
122
 
123
+ # Build the diagnosis cards HTML
124
+ diagnosis_html = ""
125
+ if "diagnosis_77" in results:
126
+ probs = results["diagnosis_77"]["probabilities"]
127
+ class_names = results["diagnosis_77"]["class_names"]
128
+ top_indices = np.argsort(probs)[::-1][:5]
129
+
130
+ for i, idx in enumerate(top_indices, 1):
131
+ prob_pct = probs[idx] * 100
132
+ # Determine severity class based on probability
133
+ if prob_pct < 30:
134
+ severity_class = "severity-low"
135
+ elif prob_pct < 60:
136
+ severity_class = "severity-medium"
137
+ else:
138
+ severity_class = "severity-high"
139
+
140
+ diagnosis_html += f"""
141
+ <div class="diagnosis-card">
142
+ <div class="diagnosis-header">
143
+ <span class="diagnosis-rank">#{i}</span>
144
+ <span class="diagnosis-name">{class_names[idx]}</span>
145
+ <span class="diagnosis-percent {severity_class}">{prob_pct:.1f}%</span>
146
+ </div>
147
+ <div class="progress-container">
148
+ <div class="progress-bar {severity_class}" style="width: {prob_pct}%;"></div>
149
+ </div>
150
+ </div>
151
+ """
152
+
153
+ summary = f"""
154
+ <div style="padding: 10px;">
155
+
156
+ ## Analysis Results: {filename}
157
+
158
+ <div style="display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 8px 16px; border-radius: 20px; font-size: 0.9em; margin-bottom: 20px;">
159
+ Inference Time: {inference_time:.1f} ms
160
+ </div>
161
 
162
  ### Risk Predictions
163
+
164
  | Risk Factor | Probability |
165
  |-------------|-------------|
166
  | LVEF < 40% | {lvef_40*100:.1f}% |
 
168
  | 5-year AFib Risk | {afib_5y*100:.1f}% |
169
 
170
  ### Top 5 Diagnoses
171
+
172
+ <div class="diagnosis-dashboard">
173
+ {diagnosis_html}
174
+ </div>
175
+
176
+ </div>
177
  """
 
 
 
 
 
 
 
 
178
 
179
  return ecg_fig, diagnosis_fig, risk_fig, summary
180
 
 
220
  samples = get_sample_ecgs()
221
  sample_names = [s["name"] for s in samples]
222
 
223
+ # Custom CSS for styling with modern animated header
224
  custom_css = """
225
  .gradio-container {
226
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
227
  }
228
+
229
+ /* Animated Header Styles */
230
  .main-header {
231
  text-align: center;
232
+ padding: 40px 24px;
233
+ background: linear-gradient(-45deg, #ee7752, #e73c7e, #c0392b, #e74c3c);
234
+ background-size: 400% 400%;
235
+ animation: gradientShift 8s ease infinite;
236
  color: white;
237
+ border-radius: 16px;
238
+ margin-bottom: 24px;
239
+ box-shadow: 0 10px 40px rgba(231, 76, 60, 0.3);
240
+ position: relative;
241
+ overflow: hidden;
242
  }
243
+
244
+ .main-header::before {
245
+ content: '';
246
+ position: absolute;
247
+ top: 0;
248
+ left: 0;
249
+ right: 0;
250
+ bottom: 0;
251
+ background: radial-gradient(circle at 30% 50%, rgba(255,255,255,0.1) 0%, transparent 50%);
252
+ pointer-events: none;
253
+ }
254
+
255
+ @keyframes gradientShift {
256
+ 0% { background-position: 0% 50%; }
257
+ 50% { background-position: 100% 50%; }
258
+ 100% { background-position: 0% 50%; }
259
+ }
260
+
261
+ .header-content {
262
+ position: relative;
263
+ z-index: 2;
264
+ display: flex;
265
+ flex-direction: column;
266
+ align-items: center;
267
+ gap: 12px;
268
+ }
269
+
270
+ /* Pulsing Heart Container */
271
+ .heart-container {
272
+ position: relative;
273
+ width: 100px;
274
+ height: 100px;
275
+ display: flex;
276
+ align-items: center;
277
+ justify-content: center;
278
+ }
279
+
280
+ /* Heart SVG Animation */
281
+ .heart-svg {
282
+ width: 80px;
283
+ height: 80px;
284
+ animation: heartbeat 1.2s ease-in-out infinite;
285
+ filter: drop-shadow(0 0 20px rgba(255,255,255,0.5));
286
+ }
287
+
288
+ @keyframes heartbeat {
289
+ 0% { transform: scale(1); }
290
+ 14% { transform: scale(1.15); }
291
+ 28% { transform: scale(1); }
292
+ 42% { transform: scale(1.1); }
293
+ 70% { transform: scale(1); }
294
+ }
295
+
296
+ /* ECG Line Animation */
297
+ .ecg-line {
298
+ position: absolute;
299
+ width: 200px;
300
+ height: 40px;
301
+ left: 50%;
302
+ transform: translateX(-50%);
303
+ bottom: -10px;
304
+ }
305
+
306
+ .ecg-path {
307
+ stroke: rgba(255,255,255,0.8);
308
+ stroke-width: 2;
309
+ fill: none;
310
+ stroke-linecap: round;
311
+ stroke-dasharray: 200;
312
+ stroke-dashoffset: 200;
313
+ animation: ecgDraw 2s ease-in-out infinite;
314
+ }
315
+
316
+ @keyframes ecgDraw {
317
+ 0% { stroke-dashoffset: 200; opacity: 0; }
318
+ 10% { opacity: 1; }
319
+ 50% { stroke-dashoffset: 0; opacity: 1; }
320
+ 90% { opacity: 1; }
321
+ 100% { stroke-dashoffset: -200; opacity: 0; }
322
+ }
323
+
324
  .main-header h1 {
325
  margin: 0;
326
+ font-size: 2.8em;
327
+ font-weight: 700;
328
+ letter-spacing: -0.02em;
329
+ text-shadow: 0 2px 10px rgba(0,0,0,0.2);
330
  }
331
+
332
  .main-header p {
333
+ margin: 0;
334
  opacity: 0.95;
335
+ font-size: 1.2em;
336
+ font-weight: 400;
337
+ letter-spacing: 0.02em;
338
  }
339
+
340
  .sample-card {
341
  padding: 16px;
342
  border-radius: 8px;
 
344
  margin: 8px 0;
345
  border-left: 4px solid #e74c3c;
346
  }
347
+
348
  .quick-start {
349
+ background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
350
+ padding: 18px 20px;
351
+ border-radius: 12px;
352
+ margin: 20px 0;
353
+ border-left: 5px solid #4caf50;
354
+ box-shadow: 0 2px 8px rgba(76, 175, 80, 0.15);
355
+ }
356
+
357
+ /* Modern Diagnosis Card Styles */
358
+ .diagnosis-dashboard {
359
+ padding: 20px;
360
+ }
361
+
362
+ .diagnosis-card {
363
+ background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
364
+ border-radius: 16px;
365
+ padding: 20px;
366
+ margin: 12px 0;
367
+ box-shadow: 0 4px 20px rgba(0,0,0,0.08);
368
+ border: 1px solid rgba(0,0,0,0.05);
369
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
370
+ }
371
+
372
+ .diagnosis-card:hover {
373
+ transform: translateY(-2px);
374
+ box-shadow: 0 8px 30px rgba(0,0,0,0.12);
375
+ }
376
+
377
+ .diagnosis-header {
378
+ display: flex;
379
+ justify-content: space-between;
380
+ align-items: center;
381
+ margin-bottom: 12px;
382
+ }
383
+
384
+ .diagnosis-rank {
385
+ font-size: 0.85em;
386
+ font-weight: 600;
387
+ color: #666;
388
+ background: #f0f0f0;
389
+ padding: 4px 10px;
390
+ border-radius: 20px;
391
+ }
392
+
393
+ .diagnosis-name {
394
+ font-size: 1.1em;
395
+ font-weight: 600;
396
+ color: #333;
397
+ flex: 1;
398
+ margin-left: 12px;
399
+ }
400
+
401
+ .diagnosis-percent {
402
+ font-size: 1.3em;
403
+ font-weight: 700;
404
+ }
405
+
406
+ .progress-container {
407
+ height: 10px;
408
+ background: #e9ecef;
409
+ border-radius: 10px;
410
+ overflow: hidden;
411
+ }
412
+
413
+ .progress-bar {
414
+ height: 100%;
415
+ border-radius: 10px;
416
+ transition: width 0.8s ease;
417
+ }
418
+
419
+ .severity-low {
420
+ background: linear-gradient(90deg, #28a745 0%, #20c997 100%);
421
+ color: #28a745;
422
+ }
423
+
424
+ .severity-medium {
425
+ background: linear-gradient(90deg, #ffc107 0%, #fd7e14 100%);
426
+ color: #fd7e14;
427
+ }
428
+
429
+ .severity-high {
430
+ background: linear-gradient(90deg, #dc3545 0%, #e83e8c 100%);
431
+ color: #dc3545;
432
+ }
433
+
434
+ /* Footer Styles */
435
+ .footer-container {
436
+ margin-top: 40px;
437
+ padding: 30px;
438
+ background: linear-gradient(135deg, #2c3e50 0%, #1a252f 100%);
439
+ border-radius: 16px;
440
+ color: white;
441
+ text-align: center;
442
+ }
443
+
444
+ .footer-content {
445
+ max-width: 800px;
446
+ margin: 0 auto;
447
+ }
448
+
449
+ .footer-acknowledgement {
450
+ font-size: 1em;
451
+ margin-bottom: 16px;
452
+ padding-bottom: 16px;
453
+ border-bottom: 1px solid rgba(255,255,255,0.2);
454
+ }
455
+
456
+ .footer-acknowledgement a {
457
+ color: #3498db;
458
+ text-decoration: none;
459
+ font-weight: 600;
460
+ }
461
+
462
+ .footer-acknowledgement a:hover {
463
+ text-decoration: underline;
464
+ }
465
+
466
+ .footer-disclaimer {
467
+ font-size: 0.9em;
468
+ color: rgba(255,255,255,0.7);
469
+ padding: 12px 20px;
470
+ background: rgba(231, 76, 60, 0.2);
471
  border-radius: 8px;
472
+ border: 1px solid rgba(231, 76, 60, 0.3);
473
+ }
474
+
475
+ .footer-disclaimer strong {
476
+ color: #e74c3c;
477
  }
478
  """
479
 
480
  with gr.Blocks(css=custom_css, title="HeartWatch AI", theme=gr.themes.Soft()) as demo:
481
+ # Animated Header with Pulsing Heart
482
  gr.HTML("""
483
  <div class="main-header">
484
+ <div class="header-content">
485
+ <div class="heart-container">
486
+ <svg class="heart-svg" viewBox="0 0 32 29.6">
487
+ <path fill="white" d="M23.6,0c-3.4,0-6.3,2.7-7.6,5.6C14.7,2.7,11.8,0,8.4,0C3.8,0,0,3.8,0,8.4c0,9.4,9.5,11.9,16,21.2c6.1-9.3,16-12.1,16-21.2C32,3.8,28.2,0,23.6,0z"/>
488
+ </svg>
489
+ <svg class="ecg-line" viewBox="0 0 200 40">
490
+ <path class="ecg-path" d="M0,20 L40,20 L50,20 L55,5 L60,35 L65,10 L70,25 L75,20 L120,20 L130,20 L135,8 L140,32 L145,12 L150,24 L155,20 L200,20"/>
491
+ </svg>
492
+ </div>
493
+ <h1>HeartWatch AI</h1>
494
+ <p>AI-Powered 12-Lead ECG Analysis</p>
495
+ </div>
496
  </div>
497
  """)
498
 
 
649
  *Built with Gradio and PyTorch*
650
  """)
651
 
652
+ # Modern Footer with Acknowledgement and Disclaimer
653
+ gr.HTML("""
654
+ <div class="footer-container">
655
+ <div class="footer-content">
656
+ <div class="footer-acknowledgement">
657
+ Based on <a href="https://github.com/HeartWise-AI/DeepECG_Docker" target="_blank">HeartWise-AI/DeepECG_Docker</a>
658
+ </div>
659
+ <div class="footer-disclaimer">
660
+ <strong>Disclaimer:</strong> This is a research demo. Not for clinical use.
661
+ </div>
662
+ </div>
663
+ </div>
664
  """)
665
 
666
  return demo