Luigi commited on
Commit
bcdf9ee
·
1 Parent(s): 2f0cf23

Improve UI/UX for slow HuggingFace CPU tier

Browse files

- Add visual status text colors (success=green, error=red, warning=yellow, info=blue)
- Restore download button visual feedback with success/error states and icons
- Add progress bar for long-running operations (transcription, summarization)
- Add performance notice in header about expected wait times (2-5 minutes)
- Show progress during transcription stages (diarization, utterance processing)
- Show progress during summary generation (title generation, content streaming)
- Better user expectations management for constrained CPU environment

Files changed (3) hide show
  1. frontend/app.js +50 -1
  2. frontend/index.html +10 -0
  3. frontend/styles.css +55 -0
frontend/app.js CHANGED
@@ -54,6 +54,9 @@ const elements = {
54
  podcastSearch: document.getElementById('podcast-search'),
55
  podcastResults: document.getElementById('podcast-results'),
56
  episodeResults: document.getElementById('episode-results'),
 
 
 
57
  };
58
 
59
  const TRANSCRIPT_FORMATS = [
@@ -89,6 +92,22 @@ function setStatus(message, tone = 'info') {
89
  elements.statusText.dataset.tone = tone;
90
  }
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  function formatTime(seconds) {
93
  const mins = Math.floor(seconds / 60);
94
  const secs = Math.floor(seconds % 60).toString().padStart(2, '0');
@@ -226,6 +245,8 @@ async function handleTranscription() {
226
  resetTranscriptionState();
227
  state.transcribing = true;
228
  setStatus('Starting transcription...', 'info');
 
 
229
 
230
  const formData = new FormData();
231
  if (state.uploadedFile) {
@@ -266,9 +287,11 @@ async function handleTranscription() {
266
  }
267
 
268
  setStatus('Transcription complete', 'success');
 
269
  } catch (err) {
270
  console.error(err);
271
  setStatus(err.message, 'error');
 
272
  } finally {
273
  state.transcribing = false;
274
  }
@@ -289,12 +312,16 @@ function handleTranscriptionEvent(event) {
289
  case 'progress':
290
  if (event.stage === 'diarization') {
291
  setStatus(`Performing speaker diarization... (${event.progress}%)`, 'info');
 
 
 
292
  }
293
  break;
294
  case 'utterance':
295
  state.utterances.push(event.utterance);
296
  const progress = event.progress || 0;
297
  setStatus(`Transcribing audio... (${state.utterances.length} utterances, ${progress}%)`, 'info');
 
298
  renderTranscript();
299
  break;
300
  case 'complete':
@@ -562,6 +589,8 @@ async function handleSummaryGeneration() {
562
  if (state.summarizing || !state.utterances.length) return;
563
  state.summarizing = true;
564
  setStatus('Generating summary...', 'info');
 
 
565
  elements.summaryOutput.textContent = '';
566
  elements.titleOutput.textContent = '';
567
  state.title = '';
@@ -598,16 +627,20 @@ async function handleSummaryGeneration() {
598
  if (event.type === 'title' && event.content) {
599
  state.title = event.content;
600
  elements.titleOutput.textContent = event.content;
 
601
  } else if (event.type === 'partial' && event.content) {
602
  elements.summaryOutput.innerHTML = renderMarkdown(event.content);
 
603
  }
604
  }
605
  }
606
 
607
  setStatus('Summary ready', 'success');
 
608
  } catch (err) {
609
  console.error(err);
610
  setStatus(err.message, 'error');
 
611
  } finally {
612
  state.summarizing = false;
613
  }
@@ -843,13 +876,29 @@ async function downloadEpisode(audioUrl, title, triggerButton = null) {
843
  state.uploadedFile = null;
844
  elements.audioPlayer.src = data.audioUrl;
845
  setStatus('Episode ready', 'success');
 
 
 
 
 
 
 
 
846
  } catch (err) {
847
  console.error(err);
848
  setStatus(err.message, 'error');
 
 
 
 
 
 
 
 
849
  } finally {
850
  if (triggerButton) {
 
851
  triggerButton.classList.remove('loading');
852
- triggerButton.textContent = originalLabel || 'Download';
853
  }
854
  }
855
  }
 
54
  podcastSearch: document.getElementById('podcast-search'),
55
  podcastResults: document.getElementById('podcast-results'),
56
  episodeResults: document.getElementById('episode-results'),
57
+ progressContainer: document.getElementById('progress-container'),
58
+ progressFill: document.getElementById('progress-fill'),
59
+ progressText: document.getElementById('progress-text'),
60
  };
61
 
62
  const TRANSCRIPT_FORMATS = [
 
92
  elements.statusText.dataset.tone = tone;
93
  }
94
 
95
+ function showProgress(visible = true) {
96
+ if (visible) {
97
+ elements.progressContainer.classList.remove('hidden');
98
+ } else {
99
+ elements.progressContainer.classList.add('hidden');
100
+ elements.progressFill.style.width = '0%';
101
+ }
102
+ }
103
+
104
+ function updateProgress(percent, text = null) {
105
+ elements.progressFill.style.width = `${Math.min(100, Math.max(0, percent))}%`;
106
+ if (text) {
107
+ elements.progressText.textContent = text;
108
+ }
109
+ }
110
+
111
  function formatTime(seconds) {
112
  const mins = Math.floor(seconds / 60);
113
  const secs = Math.floor(seconds % 60).toString().padStart(2, '0');
 
245
  resetTranscriptionState();
246
  state.transcribing = true;
247
  setStatus('Starting transcription...', 'info');
248
+ showProgress(true);
249
+ updateProgress(0, 'Initializing...');
250
 
251
  const formData = new FormData();
252
  if (state.uploadedFile) {
 
287
  }
288
 
289
  setStatus('Transcription complete', 'success');
290
+ showProgress(false);
291
  } catch (err) {
292
  console.error(err);
293
  setStatus(err.message, 'error');
294
+ showProgress(false);
295
  } finally {
296
  state.transcribing = false;
297
  }
 
312
  case 'progress':
313
  if (event.stage === 'diarization') {
314
  setStatus(`Performing speaker diarization... (${event.progress}%)`, 'info');
315
+ updateProgress(event.progress, `Diarizing speakers... ${event.progress}%`);
316
+ } else {
317
+ updateProgress(event.progress || 0, `Transcribing... ${event.progress || 0}%`);
318
  }
319
  break;
320
  case 'utterance':
321
  state.utterances.push(event.utterance);
322
  const progress = event.progress || 0;
323
  setStatus(`Transcribing audio... (${state.utterances.length} utterances, ${progress}%)`, 'info');
324
+ updateProgress(progress, `Processing utterances... ${progress}%`);
325
  renderTranscript();
326
  break;
327
  case 'complete':
 
589
  if (state.summarizing || !state.utterances.length) return;
590
  state.summarizing = true;
591
  setStatus('Generating summary...', 'info');
592
+ showProgress(true);
593
+ updateProgress(0, 'Initializing summary generation...');
594
  elements.summaryOutput.textContent = '';
595
  elements.titleOutput.textContent = '';
596
  state.title = '';
 
627
  if (event.type === 'title' && event.content) {
628
  state.title = event.content;
629
  elements.titleOutput.textContent = event.content;
630
+ updateProgress(50, 'Generated title, creating summary...');
631
  } else if (event.type === 'partial' && event.content) {
632
  elements.summaryOutput.innerHTML = renderMarkdown(event.content);
633
+ updateProgress(75, 'Generating summary...');
634
  }
635
  }
636
  }
637
 
638
  setStatus('Summary ready', 'success');
639
+ showProgress(false);
640
  } catch (err) {
641
  console.error(err);
642
  setStatus(err.message, 'error');
643
+ showProgress(false);
644
  } finally {
645
  state.summarizing = false;
646
  }
 
876
  state.uploadedFile = null;
877
  elements.audioPlayer.src = data.audioUrl;
878
  setStatus('Episode ready', 'success');
879
+ if (triggerButton) {
880
+ triggerButton.textContent = '✓ Ready';
881
+ triggerButton.classList.add('success');
882
+ setTimeout(() => {
883
+ triggerButton.classList.remove('success');
884
+ triggerButton.textContent = originalLabel || 'Download';
885
+ }, 3000);
886
+ }
887
  } catch (err) {
888
  console.error(err);
889
  setStatus(err.message, 'error');
890
+ if (triggerButton) {
891
+ triggerButton.textContent = '❌ Retry';
892
+ triggerButton.classList.add('error');
893
+ setTimeout(() => {
894
+ triggerButton.classList.remove('error');
895
+ triggerButton.textContent = originalLabel || 'Download';
896
+ }, 3000);
897
+ }
898
  } finally {
899
  if (triggerButton) {
900
+ triggerButton.disabled = false;
901
  triggerButton.classList.remove('loading');
 
902
  }
903
  }
904
  }
frontend/index.html CHANGED
@@ -11,6 +11,9 @@
11
  <header class="app-header">
12
  <h1>VoxSum Studio</h1>
13
  <p class="tagline">Transform Audio into Insightful Summaries</p>
 
 
 
14
  </header>
15
  <div class="app-shell">
16
  <aside class="sidebar">
@@ -128,6 +131,13 @@
128
  <span id="status-text" class="status-text">Ready</span>
129
  </div>
130
 
 
 
 
 
 
 
 
131
  <section class="panel">
132
  <h2>Audio Player</h2>
133
  <audio id="audio-player" controls preload="auto"></audio>
 
11
  <header class="app-header">
12
  <h1>VoxSum Studio</h1>
13
  <p class="tagline">Transform Audio into Insightful Summaries</p>
14
+ <div class="performance-notice">
15
+ <small>⚡ Running on limited CPU - operations may take 2-5 minutes for large files</small>
16
+ </div>
17
  </header>
18
  <div class="app-shell">
19
  <aside class="sidebar">
 
131
  <span id="status-text" class="status-text">Ready</span>
132
  </div>
133
 
134
+ <div id="progress-container" class="progress-container hidden">
135
+ <div class="progress-bar">
136
+ <div id="progress-fill" class="progress-fill"></div>
137
+ </div>
138
+ <span id="progress-text" class="progress-text">Processing...</span>
139
+ </div>
140
+
141
  <section class="panel">
142
  <h2>Audio Player</h2>
143
  <audio id="audio-player" controls preload="auto"></audio>
frontend/styles.css CHANGED
@@ -28,6 +28,15 @@ body {
28
  color: #94a3b8;
29
  }
30
 
 
 
 
 
 
 
 
 
 
31
  .app-shell {
32
  display: grid;
33
  grid-template-columns: 320px 1fr;
@@ -317,6 +326,52 @@ button:hover {
317
  .status-text {
318
  color: #eab308;
319
  font-size: 0.9rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  }
321
 
322
  #transcript-container {
 
28
  color: #94a3b8;
29
  }
30
 
31
+ .performance-notice {
32
+ margin-top: 0.5rem;
33
+ }
34
+
35
+ .performance-notice small {
36
+ color: #fbbf24;
37
+ font-style: italic;
38
+ }
39
+
40
  .app-shell {
41
  display: grid;
42
  grid-template-columns: 320px 1fr;
 
326
  .status-text {
327
  color: #eab308;
328
  font-size: 0.9rem;
329
+ transition: color 0.3s ease;
330
+ }
331
+
332
+ .status-text[data-tone="success"] {
333
+ color: #22c55e;
334
+ }
335
+
336
+ .status-text[data-tone="error"] {
337
+ color: #ef4444;
338
+ }
339
+
340
+ .status-text[data-tone="warning"] {
341
+ color: #f59e0b;
342
+ }
343
+
344
+ .status-text[data-tone="info"] {
345
+ color: #3b82f6;
346
+ }
347
+
348
+ .progress-container {
349
+ margin-top: 1rem;
350
+ display: flex;
351
+ flex-direction: column;
352
+ gap: 0.5rem;
353
+ }
354
+
355
+ .progress-bar {
356
+ width: 100%;
357
+ height: 8px;
358
+ background: rgba(148, 163, 184, 0.2);
359
+ border-radius: 4px;
360
+ overflow: hidden;
361
+ }
362
+
363
+ .progress-fill {
364
+ height: 100%;
365
+ background: linear-gradient(90deg, #38bdf8 0%, #818cf8 100%);
366
+ width: 0%;
367
+ transition: width 0.3s ease;
368
+ border-radius: 4px;
369
+ }
370
+
371
+ .progress-text {
372
+ font-size: 0.85rem;
373
+ color: #94a3b8;
374
+ text-align: center;
375
  }
376
 
377
  #transcript-container {