SARATHCHANDRA D.M.P.M commited on
Commit
7ceaf75
·
1 Parent(s): f587ca1

Enhance UI and functionality for audio upload and prediction features

Browse files
Files changed (1) hide show
  1. app.py +287 -86
app.py CHANGED
@@ -100,7 +100,7 @@ def _prediction_payload(model_choice: str, source_path: Path):
100
  @app.get("/")
101
  def index():
102
  classes = [_safe_label(c) for c in ESC50_CLASSES]
103
- samples = _sample_files()
104
  return render_template_string(
105
  PAGE_TEMPLATE,
106
  class_badges=classes,
@@ -203,13 +203,15 @@ PAGE_TEMPLATE = """
203
  <title>SoundEdge - Environmental Sound Classification</title>
204
  <style>
205
  :root {
206
- --bg: #f8fafc;
207
- --text: #1e293b;
208
  --muted: #64748b;
209
- --border: #cbd5e1;
210
- --panel: #ffffff;
211
- --primary: #0369a1;
212
- --accent: #0ea5e9;
 
 
213
  --danger-bg: #fff1f2;
214
  --danger: #dc2626;
215
  }
@@ -217,76 +219,187 @@ PAGE_TEMPLATE = """
217
  body {
218
  margin: 0;
219
  font-family: "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
220
- background: linear-gradient(140deg, #f8fafc 0%, #eff6ff 50%, #e0f2fe 100%);
221
  color: var(--text);
222
  }
223
  .container {
224
- width: min(1100px, 92vw);
225
- margin: 2rem auto 3rem;
226
  }
227
  .hero {
228
- background: linear-gradient(135deg, #e0f2fe 0%, #ede9fe 60%, #dbeafe 100%);
229
- border: 1px solid #bfdbfe;
230
- border-radius: 16px;
231
- padding: 1rem 1.4rem;
232
  text-align: center;
233
- margin-bottom: 1rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  }
235
- .hero h1 { margin: 0 0 0.4rem; color: #075985; }
236
- .hero p { margin: 0; color: #475569; }
237
  .panel {
238
  background: var(--panel);
239
- border: 1px solid var(--border);
240
- border-radius: 14px;
241
- padding: 1rem;
242
- margin-top: 1rem;
243
  }
244
  .section-title {
245
- font-size: 0.85rem;
246
- letter-spacing: 0.09em;
247
  text-transform: uppercase;
248
- color: #7c3aed;
249
- font-weight: 700;
250
- margin-bottom: 0.7rem;
251
  }
252
- .badge-grid { display: flex; flex-wrap: wrap; gap: 0.4rem; }
253
  .badge {
254
- background: #f1f5f9;
255
- border: 1px solid #e2e8f0;
256
  border-radius: 8px;
257
- padding: 0.28rem 0.58rem;
258
- font-size: 0.85rem;
 
259
  }
260
- .input-grid {
261
- display: grid;
262
- grid-template-columns: 1fr 1fr;
 
 
 
 
 
 
 
 
 
 
 
263
  gap: 1rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  }
265
- @media (max-width: 860px) {
266
- .input-grid { grid-template-columns: 1fr; }
 
 
 
 
 
 
 
 
267
  }
268
- .row { margin-bottom: 0.8rem; }
269
- .hint { color: var(--muted); font-size: 0.9rem; margin: 0.2rem 0 0.8rem; }
270
  .btn {
271
- border: 1px solid #0369a1;
272
- background: #0369a1;
273
  color: #fff;
274
  border-radius: 10px;
275
- padding: 0.55rem 0.9rem;
276
  font-weight: 600;
277
  cursor: pointer;
278
  }
279
  .btn.secondary {
280
- border-color: #7c3aed;
281
- background: #7c3aed;
282
  }
283
  .btn:disabled {
284
  opacity: 0.55;
285
  cursor: not-allowed;
286
  }
287
  .status {
288
- margin-top: 0.55rem;
289
- font-size: 0.9rem;
290
  color: var(--muted);
291
  min-height: 1.2rem;
292
  }
@@ -298,44 +411,97 @@ PAGE_TEMPLATE = """
298
  padding: 0.65rem;
299
  margin-top: 0.7rem;
300
  }
 
301
  .result-card {
302
  background: linear-gradient(135deg, #dbeafe 0%, #ede9fe 100%);
303
- border: 1px solid #60a5fa;
304
  border-radius: 14px;
305
- padding: 1.2rem;
306
  text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  }
308
- .result-label { font-size: 0.82rem; letter-spacing: 0.09em; color: #64748b; text-transform: uppercase; }
309
- .result-class { margin: 0.3rem 0 0; color: #0c4a6e; font-size: 1.9rem; font-weight: 800; }
310
- .result-prob { margin-top: 0.45rem; color: #6d28d9; font-size: 1.15rem; font-weight: 700; }
311
- .bar-row { margin-top: 0.7rem; }
312
  .bar-label {
313
  display: flex;
314
  justify-content: space-between;
315
- font-size: 0.95rem;
316
  margin-bottom: 0.25rem;
 
317
  }
318
  .bar-bg {
319
- height: 11px;
320
  border-radius: 999px;
321
- background: #e2e8f0;
322
  overflow: hidden;
323
  }
324
  .bar-fill {
325
- height: 11px;
326
  border-radius: 999px;
327
  background: linear-gradient(90deg, #0284c7, #7c3aed);
328
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  </style>
330
  </head>
331
  <body>
332
  <div class="container">
333
  <div class="hero">
334
- <h1>SoundEdge</h1>
335
- <p>Environmental Sound Classification with backend-managed uploads.</p>
336
  </div>
337
 
338
- <div class="panel">
339
  <div class="section-title">Supported Sound Classes</div>
340
  <div class="badge-grid">
341
  {% for cls in class_badges %}
@@ -344,43 +510,59 @@ PAGE_TEMPLATE = """
344
  </div>
345
  </div>
346
 
347
- <div class="panel">
348
  <div class="section-title">Select Model</div>
349
- <label><input type="radio" name="model" value="Original" checked> Original</label>
350
- <label style="margin-left:1rem;"><input type="radio" name="model" value="Compressed"> Compressed</label>
 
 
351
  </div>
352
 
353
- <div class="input-grid">
354
- <div class="panel">
355
- <div class="section-title">Upload Via Backend API</div>
356
- <div class="row"><input id="uploadInput" type="file" accept=".wav,audio/wav" /></div>
357
- <div class="row">
358
- <button id="uploadBtn" class="btn">Upload</button>
359
- <button id="predictUploadedBtn" class="btn secondary" disabled>Classify Uploaded Audio</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  </div>
361
- <audio id="uploadedAudio" controls style="width:100%;display:none;margin-top:0.6rem;"></audio>
362
  <div id="uploadStatus" class="status"></div>
363
  <div id="uploadError"></div>
364
  </div>
365
 
366
- <div class="panel">
367
- <div class="section-title">Try Sample Audio</div>
368
  {% if samples %}
369
- <select id="sampleSelect" style="width:100%;padding:0.5rem;border-radius:8px;border:1px solid var(--border);">
370
  {% for sample in samples %}
371
- <option value="{{ sample }}">{{ sample }}</option>
372
  {% endfor %}
373
  </select>
374
- <audio id="sampleAudio" controls style="width:100%;margin-top:0.6rem;"></audio>
375
- <div class="row" style="margin-top:0.7rem;"><button id="predictSampleBtn" class="btn">Classify Sample</button></div>
 
 
376
  {% else %}
377
- <p class="hint">No sample files found in the samples folder.</p>
378
  {% endif %}
379
  <div id="sampleStatus" class="status"></div>
380
  </div>
381
  </div>
382
 
383
- <div id="resultsPanel" class="panel" style="display:none;"></div>
384
  </div>
385
 
386
  <script>
@@ -396,6 +578,11 @@ PAGE_TEMPLATE = """
396
  const sampleStatus = document.getElementById("sampleStatus");
397
  const predictSampleBtn = document.getElementById("predictSampleBtn");
398
  const resultsPanel = document.getElementById("resultsPanel");
 
 
 
 
 
399
 
400
  function currentModel() {
401
  const selected = document.querySelector('input[name="model"]:checked');
@@ -406,6 +593,14 @@ PAGE_TEMPLATE = """
406
  target.innerHTML = message ? `<div class="error">${message}</div>` : "";
407
  }
408
 
 
 
 
 
 
 
 
 
409
  function renderResult(result) {
410
  resultsPanel.style.display = "block";
411
  if (result.lowConfidence) {
@@ -429,23 +624,24 @@ PAGE_TEMPLATE = """
429
 
430
  let allHtml = "";
431
  result.allProbs.forEach(item => {
432
- const isTop = item.className === result.topClass;
433
- const bg = isTop ? "#dbeafe" : "#f8fafc";
434
- allHtml += `<div style="display:flex;justify-content:space-between;padding:0.35rem 0.55rem;border-radius:7px;background:${bg};margin-top:0.2rem;">
435
- <span>${item.classLabel}</span><span>${item.probabilityPct.toFixed(2)}%</span>
436
- </div>`;
 
437
  });
438
 
439
  resultsPanel.innerHTML = `
440
  <div class="result-card">
441
  <div class="result-label">Predicted Sound</div>
442
  <h2 class="result-class">${result.topClassLabel}</h2>
443
- <div class="result-prob">Confidence ${result.topProbabilityPct.toFixed(2)}%</div>
444
  </div>
445
  <div class="section-title" style="margin-top:1rem;">Top 3 Predictions</div>
446
  ${top3Html}
447
  <div class="section-title" style="margin-top:1.2rem;">All Class Probabilities</div>
448
- ${allHtml}
449
  `;
450
  }
451
 
@@ -472,12 +668,12 @@ PAGE_TEMPLATE = """
472
 
473
  const file = uploadInput.files && uploadInput.files[0];
474
  if (!file) {
475
- uploadedAudio.style.display = "none";
476
  return;
477
  }
478
  const objectUrl = URL.createObjectURL(file);
479
  uploadedAudio.src = objectUrl;
480
- uploadedAudio.style.display = "block";
481
  });
482
 
483
  uploadBtn?.addEventListener("click", async () => {
@@ -501,6 +697,7 @@ PAGE_TEMPLATE = """
501
  }
502
  uploadedFileId = data.fileId;
503
  uploadedAudio.src = data.audioUrl;
 
504
  predictUploadedBtn.disabled = false;
505
  uploadStatus.textContent = `Uploaded: ${data.filename}`;
506
  } catch (err) {
@@ -531,6 +728,10 @@ PAGE_TEMPLATE = """
531
  syncSampleAudio();
532
  }
533
 
 
 
 
 
534
  predictSampleBtn?.addEventListener("click", async () => {
535
  sampleStatus.textContent = "";
536
  try {
 
100
  @app.get("/")
101
  def index():
102
  classes = [_safe_label(c) for c in ESC50_CLASSES]
103
+ samples = [{"name": s, "label": _safe_label(Path(s).stem)} for s in _sample_files()]
104
  return render_template_string(
105
  PAGE_TEMPLATE,
106
  class_badges=classes,
 
203
  <title>SoundEdge - Environmental Sound Classification</title>
204
  <style>
205
  :root {
206
+ --bg: #f1f5f9;
207
+ --text: #1f2937;
208
  --muted: #64748b;
209
+ --line: #d8dee8;
210
+ --chip-bg: #e9edf3;
211
+ --chip-border: #d8e0ec;
212
+ --panel: transparent;
213
+ --blue: #0b75b8;
214
+ --violet: #6d46e8;
215
  --danger-bg: #fff1f2;
216
  --danger: #dc2626;
217
  }
 
219
  body {
220
  margin: 0;
221
  font-family: "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
222
+ background: var(--bg);
223
  color: var(--text);
224
  }
225
  .container {
226
+ width: min(1020px, 92vw);
227
+ margin: 1.4rem auto 3rem;
228
  }
229
  .hero {
230
+ background: linear-gradient(135deg, #d8e7fb 0%, #dfe4f8 100%);
231
+ border: 1px solid #b7d1f5;
232
+ border-radius: 14px;
233
+ padding: 2rem 1.4rem 1.6rem;
234
  text-align: center;
235
+ margin-bottom: 1.9rem;
236
+ }
237
+ .hero h1 {
238
+ margin: 0;
239
+ font-size: clamp(2.2rem, 3.8vw, 3.6rem);
240
+ font-weight: 800;
241
+ background: linear-gradient(90deg, #1f82c4, #4f48d9);
242
+ -webkit-background-clip: text;
243
+ -webkit-text-fill-color: transparent;
244
+ }
245
+ .hero p {
246
+ margin: 1rem 0 0;
247
+ color: #445d78;
248
+ font-size: 2.1vmin;
249
+ min-font-size: 1rem;
250
+ }
251
+ .hero-icon {
252
+ font-size: clamp(2rem, 4vmin, 2.8rem);
253
+ margin-right: 0.55rem;
254
+ vertical-align: 0.28em;
255
+ }
256
+ .section {
257
+ border-top: 1px solid var(--line);
258
+ padding-top: 1.9rem;
259
+ margin-top: 1.7rem;
260
+ }
261
+ .section:first-of-type {
262
+ border-top: 0;
263
+ padding-top: 0;
264
+ margin-top: 0;
265
  }
 
 
266
  .panel {
267
  background: var(--panel);
268
+ border: 0;
 
 
 
269
  }
270
  .section-title {
271
+ font-size: 1.72rem;
272
+ letter-spacing: 0.12em;
273
  text-transform: uppercase;
274
+ color: var(--violet);
275
+ font-weight: 800;
276
+ margin: 0 0 1rem;
277
  }
278
+ .badge-grid { display: flex; flex-wrap: wrap; gap: 0.46rem; }
279
  .badge {
280
+ background: var(--chip-bg);
281
+ border: 1px solid var(--chip-border);
282
  border-radius: 8px;
283
+ padding: 0.32rem 0.74rem;
284
+ font-size: 1rem;
285
+ color: #425569;
286
  }
287
+ .model-row {
288
+ display: flex;
289
+ gap: 1.2rem;
290
+ align-items: center;
291
+ font-size: 1.08rem;
292
+ }
293
+ .model-row label {
294
+ display: inline-flex;
295
+ align-items: center;
296
+ gap: 0.45rem;
297
+ cursor: pointer;
298
+ }
299
+ .tabs {
300
+ display: flex;
301
  gap: 1rem;
302
+ border-bottom: 2px solid var(--line);
303
+ margin-bottom: 1.05rem;
304
+ }
305
+ .tab-btn {
306
+ border: 0;
307
+ background: transparent;
308
+ color: #334155;
309
+ font-size: 1.08rem;
310
+ padding: 0.32rem 0.1rem 0.7rem;
311
+ border-bottom: 3px solid transparent;
312
+ cursor: pointer;
313
+ }
314
+ .tab-btn.active {
315
+ color: #0f73c6;
316
+ border-color: #0f73c6;
317
+ }
318
+ .tab-panel {
319
+ display: none;
320
+ }
321
+ .tab-panel.active {
322
+ display: block;
323
+ }
324
+ .guide-box {
325
+ background: #eff8ff;
326
+ border: 1px solid #b9def9;
327
+ border-radius: 12px;
328
+ padding: 0.95rem 1rem;
329
+ color: #36516e;
330
+ margin-bottom: 0.95rem;
331
+ font-size: 0.95rem;
332
+ }
333
+ .guide-title {
334
+ font-weight: 700;
335
+ color: #0c5f9e;
336
+ margin-bottom: 0.45rem;
337
+ }
338
+ .input-grid {
339
+ display: flex;
340
+ gap: 0.75rem;
341
+ align-items: center;
342
+ flex-wrap: wrap;
343
+ }
344
+ .upload-input {
345
+ border: 1px solid #ccd7e4;
346
+ border-radius: 9px;
347
+ background: #f7fafc;
348
+ padding: 0.42rem;
349
+ min-width: 280px;
350
+ }
351
+ .sample-select {
352
+ width: 100%;
353
+ border-radius: 10px;
354
+ border: 1px solid #d6deea;
355
+ font-size: 1.2rem;
356
+ color: #374b60;
357
+ background: #eef2f7;
358
+ padding: 0.58rem 0.8rem;
359
+ }
360
+ .audio-wrap {
361
+ margin-top: 0.8rem;
362
+ width: 100%;
363
+ max-width: 100%;
364
+ border-radius: 999px;
365
+ background: #e8edf4;
366
+ padding: 0.3rem 0.65rem;
367
+ }
368
+ audio {
369
+ width: 100%;
370
+ height: 38px;
371
  }
372
+ .sample-btn {
373
+ margin-top: 0.95rem;
374
+ width: 100%;
375
+ border: 1px solid #cfd9e5;
376
+ background: #f2f5f8;
377
+ color: #233548;
378
+ border-radius: 11px;
379
+ padding: 0.72rem 0.9rem;
380
+ font-size: 2.1vmin;
381
+ cursor: pointer;
382
  }
 
 
383
  .btn {
384
+ border: 1px solid #0b77bb;
385
+ background: #0b77bb;
386
  color: #fff;
387
  border-radius: 10px;
388
+ padding: 0.57rem 1rem;
389
  font-weight: 600;
390
  cursor: pointer;
391
  }
392
  .btn.secondary {
393
+ border-color: #6d46e8;
394
+ background: #6d46e8;
395
  }
396
  .btn:disabled {
397
  opacity: 0.55;
398
  cursor: not-allowed;
399
  }
400
  .status {
401
+ margin-top: 0.6rem;
402
+ font-size: 0.96rem;
403
  color: var(--muted);
404
  min-height: 1.2rem;
405
  }
 
411
  padding: 0.65rem;
412
  margin-top: 0.7rem;
413
  }
414
+ #resultsPanel { display: none; }
415
  .result-card {
416
  background: linear-gradient(135deg, #dbeafe 0%, #ede9fe 100%);
417
+ border: 1px solid #5f96ff;
418
  border-radius: 14px;
419
+ padding: 1.9rem;
420
  text-align: center;
421
+ margin-bottom: 1.2rem;
422
+ }
423
+ .result-label {
424
+ font-size: 1.35rem;
425
+ letter-spacing: 0.12em;
426
+ color: #6a7c90;
427
+ text-transform: uppercase;
428
+ }
429
+ .result-class {
430
+ margin: 0.58rem 0 0;
431
+ color: #0d6ba9;
432
+ font-size: clamp(2.1rem, 4vmin, 3.4rem);
433
+ font-weight: 800;
434
+ }
435
+ .result-prob {
436
+ margin-top: 0.58rem;
437
+ color: #6d46e8;
438
+ font-size: clamp(1.35rem, 2.6vmin, 2rem);
439
+ font-weight: 600;
440
  }
441
+ .bar-row { margin-top: 0.9rem; }
 
 
 
442
  .bar-label {
443
  display: flex;
444
  justify-content: space-between;
445
+ font-size: 1.1rem;
446
  margin-bottom: 0.25rem;
447
+ color: #3a4b5c;
448
  }
449
  .bar-bg {
450
+ height: 14px;
451
  border-radius: 999px;
452
+ background: #cfd6df;
453
  overflow: hidden;
454
  }
455
  .bar-fill {
456
+ height: 14px;
457
  border-radius: 999px;
458
  background: linear-gradient(90deg, #0284c7, #7c3aed);
459
  }
460
+ .all-prob-list {
461
+ margin-top: 0.75rem;
462
+ max-height: 420px;
463
+ overflow-y: auto;
464
+ padding-right: 0.25rem;
465
+ }
466
+ .all-prob-row {
467
+ margin-bottom: 0.48rem;
468
+ }
469
+ .all-prob-head {
470
+ display: flex;
471
+ justify-content: space-between;
472
+ font-size: 0.98rem;
473
+ color: #3f5061;
474
+ margin-bottom: 0.16rem;
475
+ }
476
+ .all-prob-bg {
477
+ height: 8px;
478
+ border-radius: 999px;
479
+ background: #d9e0e8;
480
+ overflow: hidden;
481
+ }
482
+ .all-prob-fill {
483
+ height: 8px;
484
+ border-radius: 999px;
485
+ background: #0b86c8;
486
+ }
487
+ @media (max-width: 760px) {
488
+ .section-title { font-size: 1.14rem; }
489
+ .badge { font-size: 0.85rem; }
490
+ .hero p { font-size: 0.98rem; }
491
+ .sample-select { font-size: 1.18rem; }
492
+ .sample-btn { font-size: 0.95rem; }
493
+ .upload-input { min-width: 100%; }
494
+ }
495
  </style>
496
  </head>
497
  <body>
498
  <div class="container">
499
  <div class="hero">
500
+ <h1><span class="hero-icon">🔊</span>SoundEdge</h1>
501
+ <p>Environmental Sound Classification - upload a short audio clip and let the model identify the sound.</p>
502
  </div>
503
 
504
+ <div class="panel section">
505
  <div class="section-title">Supported Sound Classes</div>
506
  <div class="badge-grid">
507
  {% for cls in class_badges %}
 
510
  </div>
511
  </div>
512
 
513
+ <div class="panel section">
514
  <div class="section-title">Select Model</div>
515
+ <div class="model-row">
516
+ <label><input type="radio" name="model" value="Original" checked> Original</label>
517
+ <label><input type="radio" name="model" value="Compressed"> Compressed</label>
518
+ </div>
519
  </div>
520
 
521
+ <div class="panel section">
522
+ <div class="section-title">Choose Audio Input</div>
523
+ <div class="tabs">
524
+ <button id="tabUpload" class="tab-btn active" type="button">⬆️ Upload a File</button>
525
+ <button id="tabSample" class="tab-btn" type="button">🎵 Try a Sample</button>
526
+ </div>
527
+
528
+ <div id="uploadPanel" class="tab-panel active">
529
+ <div class="guide-box">
530
+ <div class="guide-title">Upload Guide</div>
531
+ <div>Format: WAV (.wav) only.</div>
532
+ <div>Duration: Around 5 seconds gives best results.</div>
533
+ <div>Size: Keep under 5 MB.</div>
534
+ </div>
535
+ <div class="input-grid">
536
+ <input id="uploadInput" class="upload-input" type="file" accept=".wav,audio/wav" />
537
+ <button id="uploadBtn" class="btn" type="button">Upload</button>
538
+ <button id="predictUploadedBtn" class="btn secondary" type="button" disabled>Classify Uploaded Audio</button>
539
+ </div>
540
+ <div class="audio-wrap" id="uploadedAudioWrap" style="display:none;">
541
+ <audio id="uploadedAudio" controls></audio>
542
  </div>
 
543
  <div id="uploadStatus" class="status"></div>
544
  <div id="uploadError"></div>
545
  </div>
546
 
547
+ <div id="samplePanel" class="tab-panel">
 
548
  {% if samples %}
549
+ <select id="sampleSelect" class="sample-select">
550
  {% for sample in samples %}
551
+ <option value="{{ sample.name }}">{{ sample.label }}</option>
552
  {% endfor %}
553
  </select>
554
+ <div class="audio-wrap">
555
+ <audio id="sampleAudio" controls></audio>
556
+ </div>
557
+ <button id="predictSampleBtn" class="sample-btn" type="button">Classify this sample ></button>
558
  {% else %}
559
+ <div class="status">No sample files found in the samples folder.</div>
560
  {% endif %}
561
  <div id="sampleStatus" class="status"></div>
562
  </div>
563
  </div>
564
 
565
+ <div id="resultsPanel" class="panel section"></div>
566
  </div>
567
 
568
  <script>
 
578
  const sampleStatus = document.getElementById("sampleStatus");
579
  const predictSampleBtn = document.getElementById("predictSampleBtn");
580
  const resultsPanel = document.getElementById("resultsPanel");
581
+ const uploadedAudioWrap = document.getElementById("uploadedAudioWrap");
582
+ const tabUpload = document.getElementById("tabUpload");
583
+ const tabSample = document.getElementById("tabSample");
584
+ const uploadPanel = document.getElementById("uploadPanel");
585
+ const samplePanel = document.getElementById("samplePanel");
586
 
587
  function currentModel() {
588
  const selected = document.querySelector('input[name="model"]:checked');
 
593
  target.innerHTML = message ? `<div class="error">${message}</div>` : "";
594
  }
595
 
596
+ function setTab(mode) {
597
+ const isUpload = mode === "upload";
598
+ tabUpload?.classList.toggle("active", isUpload);
599
+ tabSample?.classList.toggle("active", !isUpload);
600
+ uploadPanel?.classList.toggle("active", isUpload);
601
+ samplePanel?.classList.toggle("active", !isUpload);
602
+ }
603
+
604
  function renderResult(result) {
605
  resultsPanel.style.display = "block";
606
  if (result.lowConfidence) {
 
624
 
625
  let allHtml = "";
626
  result.allProbs.forEach(item => {
627
+ allHtml += `
628
+ <div class="all-prob-row">
629
+ <div class="all-prob-head"><span>${item.classLabel}</span><span>${item.probabilityPct.toFixed(1)}%</span></div>
630
+ <div class="all-prob-bg"><div class="all-prob-fill" style="width:${item.probabilityPct}%;"></div></div>
631
+ </div>
632
+ `;
633
  });
634
 
635
  resultsPanel.innerHTML = `
636
  <div class="result-card">
637
  <div class="result-label">Predicted Sound</div>
638
  <h2 class="result-class">${result.topClassLabel}</h2>
639
+ <div class="result-prob">Confidence ${result.topProbabilityPct.toFixed(1)}%</div>
640
  </div>
641
  <div class="section-title" style="margin-top:1rem;">Top 3 Predictions</div>
642
  ${top3Html}
643
  <div class="section-title" style="margin-top:1.2rem;">All Class Probabilities</div>
644
+ <div class="all-prob-list">${allHtml}</div>
645
  `;
646
  }
647
 
 
668
 
669
  const file = uploadInput.files && uploadInput.files[0];
670
  if (!file) {
671
+ uploadedAudioWrap.style.display = "none";
672
  return;
673
  }
674
  const objectUrl = URL.createObjectURL(file);
675
  uploadedAudio.src = objectUrl;
676
+ uploadedAudioWrap.style.display = "block";
677
  });
678
 
679
  uploadBtn?.addEventListener("click", async () => {
 
697
  }
698
  uploadedFileId = data.fileId;
699
  uploadedAudio.src = data.audioUrl;
700
+ uploadedAudioWrap.style.display = "block";
701
  predictUploadedBtn.disabled = false;
702
  uploadStatus.textContent = `Uploaded: ${data.filename}`;
703
  } catch (err) {
 
728
  syncSampleAudio();
729
  }
730
 
731
+ tabUpload?.addEventListener("click", () => setTab("upload"));
732
+ tabSample?.addEventListener("click", () => setTab("sample"));
733
+ setTab("sample");
734
+
735
  predictSampleBtn?.addEventListener("click", async () => {
736
  sampleStatus.textContent = "";
737
  try {