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
- frontend/app.js +50 -1
- frontend/index.html +10 -0
- 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 {
|