Manjunath Kudlur
Ensure WebGPU enabled onnx runtime is loaded
2be96ef
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Streaming ASR Demo - Moonshine</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #1a1a2e;
color: #eee;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 20px;
color: #00d4ff;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
background: #16213e;
padding: 15px 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 10px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #666;
}
.status-dot.idle { background: #666; }
.status-dot.listening { background: #00ff88; animation: pulse 1s infinite; }
.status-dot.recording { background: #ff4444; animation: pulse 0.5s infinite; }
.backend-badge {
display: none;
padding: 3px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
margin-left: 10px;
background: #444;
color: #ccc;
}
.backend-badge.visible { display: inline-block; }
.backend-badge.wasm { background: #555; color: #aaa; }
.backend-badge.webgl { background: #f90; color: #000; }
.backend-badge.webgpu { background: linear-gradient(90deg, #00d4ff, #00ff88); color: #000; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.controls {
display: flex;
gap: 10px;
}
button {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: #00d4ff;
color: #1a1a2e;
}
.btn-primary:hover:not(:disabled) {
background: #00a8cc;
}
.btn-danger {
background: #ff4444;
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #cc3333;
}
.vad-section {
background: #16213e;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.vad-section h3 {
margin-bottom: 15px;
color: #00d4ff;
}
.vad-graph {
background: #0f0f23;
border-radius: 5px;
padding: 10px;
height: 140px;
position: relative;
overflow: hidden;
}
.vad-canvas {
width: 100%;
height: 100%;
}
.vad-bar {
display: flex;
align-items: center;
gap: 10px;
margin-top: 15px;
}
.vad-bar-container {
flex: 1;
height: 20px;
background: #0f0f23;
border-radius: 10px;
overflow: hidden;
}
.vad-bar-fill {
height: 100%;
background: linear-gradient(90deg, #00ff88, #00d4ff);
width: 0%;
transition: width 0.1s;
}
.vad-value {
min-width: 50px;
text-align: right;
font-family: monospace;
}
.pipeline-status {
display: flex;
gap: 15px;
margin-top: 15px;
font-family: monospace;
font-size: 12px;
color: #888;
}
.transcripts-section {
background: #16213e;
padding: 20px;
border-radius: 10px;
min-height: 300px;
}
.transcripts-section h3 {
margin-bottom: 15px;
color: #00d4ff;
}
.transcripts-list {
max-height: 200px;
overflow-y: auto;
}
.transcript-item {
padding: 10px 15px;
background: #0f0f23;
border-radius: 5px;
margin-bottom: 10px;
display: flex;
gap: 10px;
}
.transcript-duration {
color: #00d4ff;
font-family: monospace;
min-width: 50px;
}
.transcript-text {
flex: 1;
}
.live-caption {
padding: 30px 40px;
background: rgba(0, 0, 0, 0.75);
border-radius: 12px;
margin-top: 25px;
min-height: 100px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
text-align: left;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
.live-caption.active {
background: rgba(0, 0, 0, 0.85);
border-color: rgba(255, 68, 68, 0.3);
box-shadow: 0 8px 32px rgba(255, 68, 68, 0.15);
}
.live-caption-label {
font-size: 11px;
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
letter-spacing: 2px;
margin-bottom: 12px;
}
.live-caption.active .live-caption-label {
color: #ff6b6b;
}
.live-caption-text {
font-size: 28px;
font-weight: 400;
line-height: 1.5;
min-height: 40px;
color: #ffffff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
max-width: 100%;
word-wrap: break-word;
}
.live-caption-text.placeholder {
color: rgba(255, 255, 255, 0.35);
font-style: italic;
font-size: 20px;
font-weight: 300;
}
.config-section {
background: #16213e;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.config-section h3 {
margin-bottom: 15px;
color: #00d4ff;
}
.config-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.config-item {
display: flex;
flex-direction: column;
gap: 5px;
}
.config-item label {
font-size: 12px;
color: #888;
}
.config-item select, .config-item input {
padding: 8px;
border: 1px solid #333;
border-radius: 5px;
background: #0f0f23;
color: #eee;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.loading-overlay.hidden {
display: none;
}
.loading-content {
text-align: center;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 3px solid #333;
border-top-color: #00d4ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
color: #00d4ff;
font-size: 18px;
margin-bottom: 20px;
}
.loading-progress {
width: 300px;
margin: 0 auto;
}
.loading-progress-bar {
height: 8px;
background: #333;
border-radius: 4px;
overflow: hidden;
margin-bottom: 10px;
}
.loading-progress-fill {
height: 100%;
background: linear-gradient(90deg, #00d4ff, #00ff88);
width: 0%;
transition: width 0.3s ease;
border-radius: 4px;
}
.loading-progress-text {
font-size: 13px;
color: #888;
margin-bottom: 15px;
}
.loading-details {
font-size: 12px;
color: #666;
font-family: monospace;
max-height: 60px;
overflow: hidden;
}
.error-message {
background: #ff4444;
color: white;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
display: none;
}
.error-message.visible {
display: block;
}
/* Mobile-first live caption - shown at top on mobile */
.live-caption-mobile {
display: none;
}
/* Mobile styles */
@media (max-width: 768px) {
body {
padding: 10px;
}
h1 {
font-size: 1.4rem;
margin-bottom: 15px;
}
/* Show mobile live caption at top */
.live-caption-mobile {
display: block;
position: sticky;
top: 0;
z-index: 100;
margin: -10px -10px 15px -10px;
padding: 20px 15px;
background: rgba(0, 0, 0, 0.9);
border-radius: 0 0 12px 12px;
min-height: 80px;
text-align: left;
backdrop-filter: blur(10px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
.live-caption-mobile.active {
background: rgba(20, 0, 0, 0.95);
box-shadow: 0 4px 20px rgba(255, 68, 68, 0.2);
}
.live-caption-mobile .live-caption-label {
font-size: 10px;
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
letter-spacing: 2px;
margin-bottom: 8px;
}
.live-caption-mobile.active .live-caption-label {
color: #ff6b6b;
}
.live-caption-mobile .live-caption-text {
font-size: 22px;
font-weight: 400;
line-height: 1.4;
color: #ffffff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.live-caption-mobile .live-caption-text.placeholder {
color: rgba(255, 255, 255, 0.35);
font-style: italic;
font-size: 16px;
font-weight: 300;
}
/* Hide desktop live caption on mobile */
.transcripts-section .live-caption {
display: none;
}
/* Compact status bar */
.status-bar {
flex-direction: column;
gap: 12px;
padding: 12px 15px;
}
.controls {
width: 100%;
justify-content: center;
}
button {
padding: 12px 24px;
font-size: 16px;
min-height: 44px;
}
/* Collapsible config section */
.config-section {
padding: 15px;
}
.config-section h3 {
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.config-section h3::after {
content: '▼';
font-size: 12px;
transition: transform 0.2s;
}
.config-section.collapsed h3::after {
transform: rotate(-90deg);
}
.config-section.collapsed .config-grid {
display: none;
}
.config-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.config-item select,
.config-item input {
padding: 12px;
font-size: 16px;
min-height: 44px;
}
/* Collapsible VAD section */
.vad-section {
padding: 15px;
}
.vad-section h3 {
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.vad-section h3::after {
content: '▼';
font-size: 12px;
transition: transform 0.2s;
}
.vad-section.collapsed h3::after {
transform: rotate(-90deg);
}
.vad-section.collapsed .vad-graph,
.vad-section.collapsed .vad-bar,
.vad-section.collapsed .pipeline-status {
display: none;
}
.vad-graph {
height: 100px;
}
.pipeline-status {
flex-wrap: wrap;
gap: 10px;
}
/* Transcripts section */
.transcripts-section {
padding: 15px;
min-height: auto;
}
.transcripts-list {
max-height: 150px;
}
.transcript-item {
padding: 8px 12px;
font-size: 14px;
}
/* Loading overlay mobile */
.loading-progress {
width: 250px;
}
.loading-text {
font-size: 16px;
}
}
/* Extra small screens */
@media (max-width: 400px) {
.live-caption-mobile .live-caption-text {
font-size: 18px;
}
h1 {
font-size: 1.2rem;
}
.controls {
flex-direction: column;
}
button {
width: 100%;
}
}
</style>
</head>
<body>
<div class="loading-overlay hidden" id="loadingOverlay">
<div class="loading-content">
<div class="loading-spinner"></div>
<div class="loading-text" id="loadingText">Loading models...</div>
<div class="loading-progress">
<div class="loading-progress-bar">
<div class="loading-progress-fill" id="loadingProgressFill"></div>
</div>
<div class="loading-progress-text" id="loadingProgressText">0 / 7 models</div>
</div>
<div class="loading-details" id="loadingDetails"></div>
</div>
</div>
<div class="container">
<!-- Mobile live caption at top (hidden on desktop) -->
<div class="live-caption-mobile" id="liveCaptionMobile">
<div class="live-caption-label">Live Caption</div>
<div class="live-caption-text placeholder" id="liveCaptionTextMobile">
Waiting for speech...
</div>
</div>
<h1>Streaming ASR Demo</h1>
<div class="error-message" id="errorMessage"></div>
<div class="config-section">
<h3>Configuration</h3>
<div class="config-grid">
<div class="config-item">
<label>Model</label>
<select id="modelSelect">
<option value="sleeker">Moonshine Sleeker</option>
<option value="spindlier">Moonshine Spindlier</option>
</select>
</div>
<div class="config-item">
<label>Backend</label>
<select id="backendSelect">
<option value="wasm">WASM (CPU)</option>
<option value="webgl">WebGL (GPU)</option>
<option value="webgpu">WebGPU (GPU)</option>
</select>
</div>
<div class="config-item">
<label>ONNX Files URL</label>
<input type="text" id="onnxUrl" placeholder="e.g., ./models or https://..." value="./models">
</div>
<div class="config-item">
<label>Onset Threshold</label>
<input type="number" id="onsetThreshold" value="0.4" min="0" max="1" step="0.1">
</div>
<div class="config-item">
<label>Offset Threshold</label>
<input type="number" id="offsetThreshold" value="0.3" min="0" max="1" step="0.1">
</div>
</div>
</div>
<div class="status-bar">
<div class="status-indicator">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Ready</span>
<span class="backend-badge" id="backendBadge"></span>
</div>
<div class="controls">
<button class="btn-primary" id="startBtn">Start Listening</button>
<button class="btn-danger" id="stopBtn" disabled>Stop</button>
</div>
</div>
<div class="vad-section">
<h3>Voice Activity Detection</h3>
<div class="vad-graph">
<canvas id="vadCanvas" class="vad-canvas"></canvas>
</div>
<div class="vad-bar">
<span>VAD:</span>
<div class="vad-bar-container">
<div class="vad-bar-fill" id="vadBarFill"></div>
</div>
<span class="vad-value" id="vadValue">0%</span>
</div>
<div class="pipeline-status">
<span>audio_q: <span id="audioQueueSize">0</span></span>
<span>features_q: <span id="featuresQueueSize">0</span></span>
<span>dropped: <span id="droppedChunks">0</span></span>
</div>
</div>
<div class="transcripts-section">
<h3>Transcripts</h3>
<div class="transcripts-list" id="transcriptsList"></div>
<div class="live-caption" id="liveCaption">
<div class="live-caption-label">Live Caption</div>
<div class="live-caption-text placeholder" id="liveCaptionText">
Waiting for speech...
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.21.0/dist/ort.all.min.js"></script>
<script type="module" src="streaming_asr.js"></script>
</body>
</html>