|
|
<!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; |
|
|
} |
|
|
|
|
|
|
|
|
.live-caption-mobile { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
|
|
|
@media (max-width: 768px) { |
|
|
body { |
|
|
padding: 10px; |
|
|
} |
|
|
|
|
|
h1 { |
|
|
font-size: 1.4rem; |
|
|
margin-bottom: 15px; |
|
|
} |
|
|
|
|
|
|
|
|
.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; |
|
|
} |
|
|
|
|
|
|
|
|
.transcripts-section .live-caption { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
|
|
|
.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; |
|
|
} |
|
|
|
|
|
|
|
|
.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; |
|
|
} |
|
|
|
|
|
|
|
|
.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 { |
|
|
padding: 15px; |
|
|
min-height: auto; |
|
|
} |
|
|
|
|
|
.transcripts-list { |
|
|
max-height: 150px; |
|
|
} |
|
|
|
|
|
.transcript-item { |
|
|
padding: 8px 12px; |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
|
|
|
.loading-progress { |
|
|
width: 250px; |
|
|
} |
|
|
|
|
|
.loading-text { |
|
|
font-size: 16px; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@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"> |
|
|
|
|
|
<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> |
|
|
|