feat: apply local modifications to WhisperLive-Server
This commit is contained in:
@@ -0,0 +1,727 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WhisperLive Dashboard</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary: #4f46e5;
|
||||
--primary-hover: #4338ca;
|
||||
--bg-color: #0f172a;
|
||||
--card-bg: rgba(30, 41, 59, 0.7);
|
||||
--text-main: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
--border: rgba(255, 255, 255, 0.1);
|
||||
--success: #10b981;
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-main);
|
||||
min-height: 100vh;
|
||||
background-image:
|
||||
radial-gradient(at 0% 0%, rgba(79, 70, 229, 0.15) 0px, transparent 50%),
|
||||
radial-gradient(at 100% 100%, rgba(16, 185, 129, 0.1) 0px, transparent 50%);
|
||||
background-attachment: fixed;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(to right, #818cf8, #34d399);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: var(--card-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Config Section */
|
||||
.config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.config-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="file"],
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--text-main);
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2);
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: var(--text-main);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: var(--text-main);
|
||||
background: var(--primary);
|
||||
box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.4);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: var(--success);
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
/* Results / Live View */
|
||||
.transcript-box {
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
margin-top: 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.segment {
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.segment:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.segment-time {
|
||||
font-size: 0.75rem;
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
.status-recording {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #fca5a5;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(239, 68, 68, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Code snippets */
|
||||
pre {
|
||||
background: #1e293b;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #e2e8f0;
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>WhisperLive</h1>
|
||||
<p>High-Performance Real-Time Audio Transcription</p>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Panel -->
|
||||
<div class="glass-panel">
|
||||
<h3 style="margin-bottom: 1rem; font-size: 1.1rem;">Connection Settings</h3>
|
||||
<div class="config-grid">
|
||||
<div class="form-group">
|
||||
<label>HTTP API URL (For File Upload & API)</label>
|
||||
<input type="text" id="httpUrl" value="https://whisperlive.classroomcopilot.ai">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>WebSocket URL (For Live Audio)</label>
|
||||
<input type="text" id="wsUrl" value="wss://whisperlive.classroomcopilot.ai/ws">
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 0.5rem; font-size: 0.8rem; color: var(--text-muted);">
|
||||
HTTP Status: <span id="httpStatus" class="status-badge status-offline">Checking...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Workspace -->
|
||||
<div class="glass-panel">
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" onclick="switchTab('file-tab')">File Upload</button>
|
||||
<button class="tab-btn" onclick="switchTab('live-tab')">Live Microphone</button>
|
||||
<button class="tab-btn" onclick="switchTab('api-tab')">API Usage</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 1: File Upload -->
|
||||
<div id="file-tab" class="tab-content active">
|
||||
<form id="fileForm">
|
||||
<div class="form-group">
|
||||
<label>Audio File</label>
|
||||
<input type="file" id="audioFile" accept=".wav,.mp3,.flac,.m4a,.ogg,.webm" required>
|
||||
</div>
|
||||
|
||||
<div class="config-grid">
|
||||
<div class="form-group">
|
||||
<label>Language</label>
|
||||
<select id="fileLanguage">
|
||||
<option value="">Auto-detect</option>
|
||||
<option value="en">English</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="fr">French</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Task</label>
|
||||
<select id="fileTask">
|
||||
<option value="transcribe">Transcribe</option>
|
||||
<option value="translate">Translate to English</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn" id="fileSubmitBtn">
|
||||
<span>Transcribe File</span>
|
||||
<div class="loading-spinner" id="fileSpinner"></div>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div id="fileResult" style="display: none;">
|
||||
<div class="transcript-box" id="fileTranscript"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Live Recording -->
|
||||
<div id="live-tab" class="tab-content">
|
||||
<div class="config-grid" style="margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label>Language</label>
|
||||
<select id="liveLanguage">
|
||||
<option value="en">English</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="fr">French</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Task</label>
|
||||
<select id="liveTask">
|
||||
<option value="transcribe">Transcribe</option>
|
||||
<option value="translate">Translate to English</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; align-items: center;">
|
||||
<button id="recordBtn" class="btn btn-success" style="width: auto;">
|
||||
<span id="recordIcon">🎤</span> <span id="recordText">Start Recording</span>
|
||||
</button>
|
||||
<span id="liveStatus" class="status-badge status-offline" style="display: none;">Not
|
||||
connected</span>
|
||||
</div>
|
||||
|
||||
<div class="transcript-box" id="liveTranscript">
|
||||
<div style="color: var(--text-muted); text-align: center; margin-top: 3rem;">
|
||||
Click Start Recording to begin live transcription...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 3: API Usage -->
|
||||
<div id="api-tab" class="tab-content">
|
||||
<h3 style="margin-bottom: 1rem;">OpenAI Compatible API</h3>
|
||||
<p style="color: var(--text-muted); margin-bottom: 1rem; font-size: 0.9rem;">
|
||||
WhisperLive acts as a drop-in replacement for OpenAI's Whisper API. You can use any standard OpenAI
|
||||
client by changing the base URL.
|
||||
</p>
|
||||
|
||||
<h4 style="margin-bottom: 0.5rem; color: #cbd5e1;">Python (openai package)</h4>
|
||||
<pre><code id="pythonSnippet">from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key="sk-no-key-required",
|
||||
base_url="https://whisperlive.classroomcopilot.ai/v1/"
|
||||
)
|
||||
|
||||
with open("audio.wav", "rb") as file:
|
||||
transcription = client.audio.transcriptions.create(
|
||||
file=file,
|
||||
model="base",
|
||||
response_format="verbose_json"
|
||||
)
|
||||
|
||||
print(transcription.text)</code></pre>
|
||||
|
||||
<h4 style="margin-bottom: 0.5rem; color: #cbd5e1;">cURL</h4>
|
||||
<pre><code id="curlSnippet">curl https://whisperlive.classroomcopilot.ai/v1/audio/transcriptions \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F file="@audio.wav" \
|
||||
-F model="base" \
|
||||
-F response_format="verbose_json"</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// DOM Elements
|
||||
const httpUrlInput = document.getElementById('httpUrl');
|
||||
const wsUrlInput = document.getElementById('wsUrl');
|
||||
const httpStatus = document.getElementById('httpStatus');
|
||||
|
||||
// Initialization
|
||||
window.onload = () => {
|
||||
// Check if on same domain to set default URL intelligently, else leave defaults
|
||||
if (window.location.hostname !== '' && window.location.hostname !== 'localhost') {
|
||||
httpUrlInput.value = window.location.origin;
|
||||
wsUrlInput.value = window.location.origin.replace(/^http/, 'ws') + '/ws';
|
||||
}
|
||||
checkHealth();
|
||||
updateSnippets();
|
||||
};
|
||||
|
||||
httpUrlInput.addEventListener('change', () => { checkHealth(); updateSnippets(); });
|
||||
|
||||
// Tab Switching
|
||||
function switchTab(tabId) {
|
||||
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.getElementById(tabId).classList.add('active');
|
||||
event.target.classList.add('active');
|
||||
}
|
||||
|
||||
// Health Check
|
||||
async function checkHealth() {
|
||||
try {
|
||||
const res = await fetch(`${httpUrlInput.value}/health`);
|
||||
if (res.ok) {
|
||||
httpStatus.className = 'status-badge status-online';
|
||||
httpStatus.textContent = '✅ Online';
|
||||
} else throw new Error();
|
||||
} catch (e) {
|
||||
httpStatus.className = 'status-badge status-offline';
|
||||
httpStatus.textContent = '❌ Offline';
|
||||
}
|
||||
}
|
||||
|
||||
// Update Code Snippets
|
||||
function updateSnippets() {
|
||||
const baseUrl = httpUrlInput.value.endsWith('/') ? httpUrlInput.value.slice(0, -1) : httpUrlInput.value;
|
||||
document.getElementById('pythonSnippet').textContent = `from openai import OpenAI\n\nclient = OpenAI(\n api_key="sk-no-key-required",\n base_url="${baseUrl}/v1/"\n)\n\nwith open("audio.wav", "rb") as file:\n transcription = client.audio.transcriptions.create(\n file=file,\n model="base",\n response_format="verbose_json"\n )\n \nprint(transcription.text)`;
|
||||
document.getElementById('curlSnippet').textContent = `curl ${baseUrl}/v1/audio/transcriptions \\\n -H "Content-Type: multipart/form-data" \\\n -F file="@audio.wav" \\\n -F model="base" \\\n -F response_format="verbose_json"`;
|
||||
}
|
||||
|
||||
// Utility: Format Time
|
||||
function formatTime(seconds) {
|
||||
if (!seconds) return "0:00";
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = (seconds % 60).toFixed(2);
|
||||
return `${mins}:${secs.padStart(5, '0')}`;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// FEATURE 1: FILE TRANSCRIPTION
|
||||
// ==========================================
|
||||
document.getElementById('fileForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const file = document.getElementById('audioFile').files[0];
|
||||
if (!file) return;
|
||||
|
||||
const btn = document.getElementById('fileSubmitBtn');
|
||||
const spinner = document.getElementById('fileSpinner');
|
||||
const resultBox = document.getElementById('fileResult');
|
||||
const transcriptBox = document.getElementById('fileTranscript');
|
||||
|
||||
btn.disabled = true;
|
||||
spinner.style.display = 'block';
|
||||
resultBox.style.display = 'none';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('model', 'base');
|
||||
formData.append('response_format', 'verbose_json');
|
||||
|
||||
const lang = document.getElementById('fileLanguage').value;
|
||||
if (lang) formData.append('language', lang);
|
||||
|
||||
const task = document.getElementById('fileTask').value;
|
||||
const baseUrl = httpUrlInput.value.endsWith('/') ? httpUrlInput.value.slice(0, -1) : httpUrlInput.value;
|
||||
const endpoint = task === 'translate' ? `${baseUrl}/v1/audio/translations` : `${baseUrl}/v1/audio/transcriptions`;
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, { method: 'POST', body: formData });
|
||||
const data = await response.json();
|
||||
|
||||
resultBox.style.display = 'block';
|
||||
if (response.ok) {
|
||||
let html = '';
|
||||
if (data.segments && data.segments.length > 0) {
|
||||
data.segments.forEach(seg => {
|
||||
html += `<div class="segment"><div class="segment-time">${formatTime(seg.start)} - ${formatTime(seg.end)}</div><div class="segment-text">${seg.text}</div></div>`;
|
||||
});
|
||||
} else if (data.text) {
|
||||
html += `<div class="segment"><div class="segment-text">${data.text}</div></div>`;
|
||||
}
|
||||
transcriptBox.innerHTML = html;
|
||||
} else {
|
||||
transcriptBox.innerHTML = `<div style="color: var(--danger)">Error: ${data.error?.message || JSON.stringify(data.error)}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultBox.style.display = 'block';
|
||||
transcriptBox.innerHTML = `<div style="color: var(--danger)">Network Error: ${error.message}</div>`;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
spinner.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// FEATURE 2: LIVE WEBSOCKET TRANSCRIPTION
|
||||
// ==========================================
|
||||
let ws = null;
|
||||
let audioContext = null;
|
||||
let mediaStream = null;
|
||||
let processor = null;
|
||||
let isRecording = false;
|
||||
|
||||
const recordBtn = document.getElementById('recordBtn');
|
||||
const liveStatus = document.getElementById('liveStatus');
|
||||
const liveTranscript = document.getElementById('liveTranscript');
|
||||
|
||||
recordBtn.addEventListener('click', async () => {
|
||||
if (isRecording) {
|
||||
stopRecording();
|
||||
} else {
|
||||
startRecording();
|
||||
}
|
||||
});
|
||||
|
||||
async function startRecording() {
|
||||
liveTranscript.innerHTML = '';
|
||||
liveStatus.style.display = 'inline-flex';
|
||||
liveStatus.className = 'status-badge status-offline';
|
||||
liveStatus.textContent = 'Connecting...';
|
||||
|
||||
try {
|
||||
// 1. Connect WebSocket
|
||||
ws = new WebSocket(wsUrlInput.value);
|
||||
|
||||
ws.onopen = () => {
|
||||
// Send options to server
|
||||
const options = {
|
||||
uid: "web-" + Math.random().toString(36).substring(7),
|
||||
language: document.getElementById('liveLanguage').value,
|
||||
task: document.getElementById('liveTask').value,
|
||||
model: "base",
|
||||
use_vad: true
|
||||
};
|
||||
ws.send(JSON.stringify(options));
|
||||
};
|
||||
|
||||
ws.onmessage = async (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.message === "SERVER_READY") {
|
||||
liveStatus.className = 'status-badge status-recording';
|
||||
liveStatus.innerHTML = '🔴 Recording';
|
||||
await startAudioCapture();
|
||||
} else if (data.segments) {
|
||||
renderLiveSegments(data.segments);
|
||||
} else if (data.status === "WAIT") {
|
||||
liveStatus.textContent = `Waiting in queue (Est: ${data.message} min)`;
|
||||
} else if (data.message === "DISCONNECT") {
|
||||
stopRecording();
|
||||
liveStatus.className = 'status-badge status-offline';
|
||||
liveStatus.textContent = 'Disconnected by server';
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (err) => {
|
||||
console.error('WebSocket Error', err);
|
||||
stopRecording();
|
||||
liveStatus.className = 'status-badge status-offline';
|
||||
liveStatus.textContent = 'Connection Error';
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
stopRecording();
|
||||
};
|
||||
|
||||
// Update UI
|
||||
isRecording = true;
|
||||
recordBtn.className = 'btn btn-danger';
|
||||
document.getElementById('recordIcon').textContent = '⏹';
|
||||
document.getElementById('recordText').textContent = 'Stop Recording';
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
liveStatus.className = 'status-badge status-offline';
|
||||
liveStatus.textContent = 'Microphone Error';
|
||||
stopRecording();
|
||||
}
|
||||
}
|
||||
|
||||
async function startAudioCapture() {
|
||||
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
||||
const source = audioContext.createMediaStreamSource(mediaStream);
|
||||
|
||||
// Create a ScriptProcessorNode with bufferSize of 4096 and a single input/output channel
|
||||
processor = audioContext.createScriptProcessor(4096, 1, 1);
|
||||
|
||||
processor.onaudioprocess = function (e) {
|
||||
if (!isRecording || ws.readyState !== WebSocket.OPEN) return;
|
||||
const float32Array = e.inputBuffer.getChannelData(0);
|
||||
ws.send(float32Array.buffer);
|
||||
};
|
||||
|
||||
source.connect(processor);
|
||||
processor.connect(audioContext.destination);
|
||||
}
|
||||
|
||||
function stopRecording() {
|
||||
isRecording = false;
|
||||
|
||||
if (processor) {
|
||||
processor.disconnect();
|
||||
processor = null;
|
||||
}
|
||||
if (mediaStream) {
|
||||
mediaStream.getTracks().forEach(track => track.stop());
|
||||
mediaStream = null;
|
||||
}
|
||||
if (audioContext) {
|
||||
audioContext.close();
|
||||
audioContext = null;
|
||||
}
|
||||
if (ws) {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send("END_OF_AUDIO");
|
||||
setTimeout(() => ws.close(), 1000);
|
||||
}
|
||||
ws = null;
|
||||
}
|
||||
|
||||
recordBtn.className = 'btn btn-success';
|
||||
document.getElementById('recordIcon').textContent = '🎤';
|
||||
document.getElementById('recordText').textContent = 'Start Recording';
|
||||
|
||||
if (liveStatus.textContent === '🔴 Recording') {
|
||||
liveStatus.className = 'status-badge status-offline';
|
||||
liveStatus.textContent = 'Stopped';
|
||||
}
|
||||
}
|
||||
|
||||
let liveSegments = [];
|
||||
function renderLiveSegments(segments) {
|
||||
let html = '';
|
||||
segments.forEach(seg => {
|
||||
const timeHtml = (seg.start !== undefined && seg.end !== undefined)
|
||||
? `<div class="segment-time">${formatTime(seg.start)} - ${formatTime(seg.end)}</div>`
|
||||
: '';
|
||||
html += `<div class="segment">${timeHtml}<div class="segment-text">${seg.text}</div></div>`;
|
||||
});
|
||||
liveTranscript.innerHTML = html;
|
||||
liveTranscript.scrollTop = liveTranscript.scrollHeight;
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user