Jambonz_impl / improved_asr_web_ui.html
alaatiger989's picture
Upload folder using huggingface_hub
04c4cd1 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASR WebSocket Testing Client with Sample Rate Analysis</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
max-width: 800px;
margin: 0 auto;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #333;
font-size: 2.5em;
font-weight: 300;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 1.1em;
}
.connection-section {
margin-bottom: 30px;
}
.input-group {
margin-bottom: 20px;
}
.input-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
}
.input-group input, .input-group select {
width: 100%;
padding: 12px 16px;
border: 2px solid #e1e5e9;
border-radius: 10px;
font-size: 16px;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.8);
}
.input-group input:focus, .input-group select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-connect {
background: linear-gradient(135deg, #4CAF50, #45a049);
color: white;
width: 100%;
}
.btn-connect:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3);
}
.btn-disconnect {
background: linear-gradient(135deg, #f44336, #da190b);
color: white;
width: 100%;
}
.audio-controls {
display: flex;
justify-content: center;
gap: 20px;
margin: 30px 0;
}
.btn-mic {
background: linear-gradient(135deg, #2196F3, #1976D2);
color: white;
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.btn-mic:hover:not(:disabled) {
transform: scale(1.1);
box-shadow: 0 10px 25px rgba(33, 150, 243, 0.3);
}
.btn-mic.recording {
background: linear-gradient(135deg, #f44336, #da190b);
animation: pulse 1.5s infinite;
}
.btn-stop {
background: linear-gradient(135deg, #FF9800, #F57C00);
color: white;
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.status {
text-align: center;
margin: 20px 0;
padding: 12px;
border-radius: 10px;
font-weight: 500;
}
.status.connected {
background: rgba(76, 175, 80, 0.1);
color: #4CAF50;
border: 1px solid rgba(76, 175, 80, 0.3);
}
.status.disconnected {
background: rgba(244, 67, 54, 0.1);
color: #f44336;
border: 1px solid rgba(244, 67, 54, 0.3);
}
.status.recording {
background: rgba(33, 150, 243, 0.1);
color: #2196F3;
border: 1px solid rgba(33, 150, 243, 0.3);
}
.stats-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin: 20px 0;
}
.stat-box {
background: rgba(0, 0, 0, 0.05);
border-radius: 10px;
padding: 15px;
text-align: center;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.stat-value {
font-size: 2em;
font-weight: bold;
color: #667eea;
margin-bottom: 5px;
}
.stat-label {
color: #666;
font-size: 0.9em;
}
.response-section {
margin-top: 30px;
}
.response-box {
background: rgba(0, 0, 0, 0.05);
border-radius: 10px;
padding: 20px;
min-height: 200px;
max-height: 400px;
overflow-y: auto;
border: 1px solid rgba(0, 0, 0, 0.1);
font-family: 'Courier New', monospace;
font-size: 12px;
white-space: pre-wrap;
word-wrap: break-word;
}
.debug-section {
margin-top: 20px;
}
.debug-box {
background: rgba(0, 0, 0, 0.8);
color: #00ff00;
border-radius: 10px;
padding: 15px;
min-height: 150px;
max-height: 300px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 11px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>๐ŸŽค ASR Sample Rate Analyzer</h1>
<p>WebSocket-based Speech Recognition with Audio Analysis</p>
</div>
<div class="connection-section">
<div class="input-group">
<label for="websocketUrl">WebSocket URL:</label>
<input type="text" id="websocketUrl" value="ws://185.208.206.135:5005" placeholder="ws://185.208.206.135:5005">
</div>
<div class="input-group">
<label for="targetSampleRate">Target Sample Rate (Hz):</label>
<select id="targetSampleRate">
<option value="8000">8000 Hz (Default)</option>
<option value="16000">16000 Hz</option>
<option value="22050">22050 Hz</option>
<option value="44100">44100 Hz</option>
</select>
</div>
<div class="input-group">
<label for="chunkSize">Audio Chunk Size (samples):</label>
<select id="chunkSize">
<option value="1024">1024 samples</option>
<option value="2048">2048 samples</option>
<option value="4096" selected>4096 samples</option>
<option value="8192">8192 samples</option>
</select>
</div>
<div class="input-group">
<label for="Interim-Results"> Interim-Results:</label>
<select id="interimResults">
<option value="true">True</option>
<option value="false">False</option>
</select>
</div>
<div class="input-group">
<label for="language">Language Code:</label>
<input type="text" id="language_code" value="en-US" placeholder="en-US">
</div>
<button id="connectBtn" class="btn btn-connect">Connect to Debug Server</button>
<button id="disconnectBtn" class="btn btn-disconnect" style="display: none;">Disconnect</button>
</div>
<div id="status" class="status disconnected">Disconnected</div>
<div class="stats-section">
<div class="stat-box">
<div class="stat-value" id="actualSampleRate">0</div>
<div class="stat-label">Calculated Sample Rate (Hz)</div>
</div>
<div class="stat-box">
<div class="stat-value" id="bytesSent">0</div>
<div class="stat-label">Total Bytes Sent</div>
</div>
<div class="stat-box">
<div class="stat-value" id="chunksSent">0</div>
<div class="stat-label">Audio Chunks Sent</div>
</div>
<div class="stat-box">
<div class="stat-value" id="duration">0.0s</div>
<div class="stat-label">Recording Duration</div>
</div>
</div>
<div class="audio-controls">
<button id="micBtn" class="btn btn-mic" disabled title="Start Recording">๐ŸŽค</button>
<button id="stopBtn" class="btn btn-stop" disabled title="Stop Recording">โน๏ธ</button>
</div>
<div class="response-section">
<h3>Server Responses:</h3>
<div id="responseBox" class="response-box">Waiting for connection...</div>
</div>
<div class="debug-section">
<h3>Debug Console:</h3>
<div id="debugBox" class="debug-box">Ready to connect...</div>
</div>
</div>
<script>
class SampleRateAnalyzer {
constructor() {
this.websocket = null;
this.audioContext = null;
this.mediaRecorder = null;
this.audioStream = null;
this.processor = null;
this.isRecording = false;
this.isConnected = false;
// Audio analysis variables
this.startTime = null;
this.totalBytesSent = 0;
this.chunksSent = 0;
this.targetSampleRate = 8000;
this.chunkSize = 4096;
this.initializeElements();
this.attachEventListeners();
}
initializeElements() {
this.elements = {
websocketUrl: document.getElementById('websocketUrl'),
targetSampleRate: document.getElementById('targetSampleRate'),
chunkSize: document.getElementById('chunkSize'),
connectBtn: document.getElementById('connectBtn'),
disconnectBtn: document.getElementById('disconnectBtn'),
micBtn: document.getElementById('micBtn'),
stopBtn: document.getElementById('stopBtn'),
status: document.getElementById('status'),
responseBox: document.getElementById('responseBox'),
debugBox: document.getElementById('debugBox'),
actualSampleRate: document.getElementById('actualSampleRate'),
bytesSent: document.getElementById('bytesSent'),
chunksSent: document.getElementById('chunksSent'),
duration: document.getElementById('duration')
};
}
attachEventListeners() {
this.elements.connectBtn.addEventListener('click', () => this.connect());
this.elements.disconnectBtn.addEventListener('click', () => this.disconnect());
this.elements.micBtn.addEventListener('click', () => this.startRecording());
this.elements.stopBtn.addEventListener('click', () => this.stopRecording());
this.elements.targetSampleRate.addEventListener('change', (e) => {
this.targetSampleRate = parseInt(e.target.value);
this.debugLog(`Target sample rate changed to: ${this.targetSampleRate} Hz`);
});
this.elements.chunkSize.addEventListener('change', (e) => {
this.chunkSize = parseInt(e.target.value);
this.debugLog(`Chunk size changed to: ${this.chunkSize} samples`);
});
}
debugLog(message) {
const timestamp = new Date().toLocaleTimeString();
const debugBox = this.elements.debugBox;
debugBox.innerHTML += `[${timestamp}] ${message}\n`;
debugBox.scrollTop = debugBox.scrollHeight;
}
updateStatus(message, type) {
this.elements.status.textContent = message;
this.elements.status.className = `status ${type}`;
}
updateStats() {
if (this.startTime) {
const elapsed = (Date.now() - this.startTime) / 1000;
this.elements.duration.textContent = `${elapsed.toFixed(1)}s`;
// Calculate actual sample rate
if (elapsed > 0.5) {
const totalSamples = this.totalBytesSent / 2; // 16-bit samples
const calculatedRate = totalSamples / elapsed;
this.elements.actualSampleRate.textContent = Math.round(calculatedRate);
// Log if there's a significant difference
const difference = Math.abs(calculatedRate - this.targetSampleRate);
if (difference > 100) {
this.debugLog(`โš ๏ธ Sample rate mismatch! Target: ${this.targetSampleRate}Hz, Actual: ${Math.round(calculatedRate)}Hz`);
}
}
}
this.elements.bytesSent.textContent = this.totalBytesSent.toLocaleString();
this.elements.chunksSent.textContent = this.chunksSent.toLocaleString();
}
async connect() {
const url = this.elements.websocketUrl.value.trim();
if (!url) {
alert('Please enter a WebSocket URL');
return;
}
try {
this.updateStatus('Connecting...', 'disconnected');
this.elements.connectBtn.disabled = true;
this.debugLog(`Attempting to connect to: ${url}`);
this.websocket = new WebSocket(url);
this.websocket.binaryType = 'arraybuffer';
// Set a connection timeout
const connectionTimeout = setTimeout(() => {
if (this.websocket.readyState === WebSocket.CONNECTING) {
this.websocket.close();
this.debugLog('โŒ Connection timeout');
this.updateStatus('Connection Timeout', 'disconnected');
this.resetConnection();
}
}, 10000); // 10 second timeout
this.websocket.onopen = () => {
clearTimeout(connectionTimeout);
this.isConnected = true;
this.updateStatus('Connected to Debug Server', 'connected');
this.elements.connectBtn.style.display = 'none';
this.elements.disconnectBtn.style.display = 'block';
this.elements.micBtn.disabled = false;
this.elements.responseBox.textContent = 'Connected to debug server. Ready to test sample rate...';
this.debugLog('โœ… WebSocket connected successfully');
};
this.websocket.onmessage = (event) => {
if (typeof event.data === 'string') {
try {
const response = JSON.parse(event.data);
this.displayResponse('JSON Response', response);
this.debugLog(`๐Ÿ“จ Received JSON: ${JSON.stringify(response)}`);
} catch (e) {
this.displayResponse('Text Response', event.data);
this.debugLog(`๐Ÿ“จ Received Text: ${event.data}`);
}
} else {
this.displayResponse('Binary Response', `Received binary data: ${event.data.byteLength} bytes`);
this.debugLog(`๐Ÿ“จ Received Binary: ${event.data.byteLength} bytes`);
}
};
this.websocket.onerror = (error) => {
clearTimeout(connectionTimeout);
console.error('WebSocket error:', error);
this.debugLog(`โŒ WebSocket error: ${error.message || 'Unknown error'}`);
this.updateStatus('Connection Error', 'disconnected');
this.resetConnection();
};
this.websocket.onclose = (event) => {
clearTimeout(connectionTimeout);
this.isConnected = false;
const reason = event.reason || 'No reason provided';
this.debugLog(`๐Ÿ”Œ WebSocket closed: Code ${event.code}, Reason: ${reason}`);
this.updateStatus(`Disconnected (Code: ${event.code})`, 'disconnected');
this.resetConnection();
this.displayResponse('Connection Closed', `Code: ${event.code}, Reason: ${reason}`);
};
} catch (error) {
console.error('Connection failed:', error);
this.debugLog(`โŒ Connection failed: ${error.message}`);
this.updateStatus('Connection Failed', 'disconnected');
this.resetConnection();
}
}
disconnect() {
if (this.isRecording) {
this.stopRecording();
}
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
this.debugLog('๐Ÿ”Œ Manually disconnecting...');
this.websocket.close(1000, 'Client disconnect');
}
this.resetConnection();
}
resetConnection() {
this.isConnected = false;
this.elements.connectBtn.disabled = false;
this.elements.connectBtn.style.display = 'block';
this.elements.disconnectBtn.style.display = 'none';
this.elements.micBtn.disabled = true;
this.elements.stopBtn.disabled = true;
this.stopRecording();
// Reset stats
this.totalBytesSent = 0;
this.chunksSent = 0;
this.startTime = null;
this.updateStats();
}
// Convert Float32Array to Int16Array (LINEAR16 PCM)
floatTo16BitPCM(float32Array) {
const int16Array = new Int16Array(float32Array.length);
for (let i = 0; i < float32Array.length; i++) {
const clipped = Math.max(-1, Math.min(1, float32Array[i]));
int16Array[i] = clipped * 0x7FFF;
}
return int16Array;
}
// Resample audio to target sample rate
resampleAudio(audioBuffer, sourceSampleRate, targetSampleRate) {
if (sourceSampleRate === targetSampleRate) {
return audioBuffer;
}
const ratio = sourceSampleRate / targetSampleRate;
const targetLength = Math.round(audioBuffer.length / ratio);
const resampled = new Float32Array(targetLength);
for (let i = 0; i < targetLength; i++) {
const sourceIndex = i * ratio;
const sourceIndexFloor = Math.floor(sourceIndex);
const sourceIndexCeil = Math.min(sourceIndexFloor + 1, audioBuffer.length - 1);
const weight = sourceIndex - sourceIndexFloor;
resampled[i] = audioBuffer[sourceIndexFloor] * (1 - weight) +
audioBuffer[sourceIndexCeil] * weight;
}
return resampled;
}
async startRecording() {
if (!this.isConnected) {
alert('Please connect to debug server first');
return;
}
try {
this.debugLog(`๐ŸŽค Starting recording with target sample rate: ${this.targetSampleRate} Hz`);
// Initialize audio context
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.debugLog(`๐Ÿ”Š Audio context created with sample rate: ${this.audioContext.sampleRate} Hz`);
// Get microphone stream
this.audioStream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: false,
noiseSuppression: false,
autoGainControl: false,
channelCount: 1
}
});
const source = this.audioContext.createMediaStreamSource(this.audioStream);
// Create processor with specified chunk size
this.processor = this.audioContext.createScriptProcessor(this.chunkSize, 1, 1);
this.processor.onaudioprocess = (event) => {
if (!this.isRecording || !this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
return;
}
const inputBuffer = event.inputBuffer;
const audioData = inputBuffer.getChannelData(0);
// Resample to target sample rate
const resampled = this.resampleAudio(audioData, this.audioContext.sampleRate, this.targetSampleRate);
// Convert to LINEAR16 PCM
const pcmData = this.floatTo16BitPCM(resampled);
// Send binary audio data
this.websocket.send(pcmData.buffer);
// Update stats
this.totalBytesSent += pcmData.buffer.byteLength;
this.chunksSent += 1;
this.updateStats();
if (this.chunksSent % 10 === 0) { // Log every 10 chunks
this.debugLog(`๐Ÿ“Š Sent ${this.chunksSent} chunks, ${this.totalBytesSent} bytes`);
}
};
// Connect audio nodes
source.connect(this.processor);
this.processor.connect(this.audioContext.destination);
// Send START message
const startMessage = {
type: "start",
language: this.language_code,
format: "raw",
encoding: "LINEAR16",
interimResults: this.interimResults,
sampleRateHz: this.targetSampleRate,
options: {
testClient: true,
chunkSize: this.chunkSize
}
};
this.websocket.send(JSON.stringify(startMessage));
this.displayResponse('Sent START', startMessage);
this.debugLog(`๐Ÿ“ค Sent START message: ${JSON.stringify(startMessage)}`);
this.isRecording = true;
this.startTime = Date.now();
// Update UI
this.elements.micBtn.classList.add('recording');
this.elements.micBtn.disabled = true;
this.elements.stopBtn.disabled = false;
this.updateStatus(`Recording @ ${this.targetSampleRate}Hz`, 'recording');
} catch (error) {
console.error('Failed to start recording:', error);
this.debugLog(`โŒ Recording failed: ${error.message}`);
alert('Failed to access microphone. Please check permissions.');
this.stopRecording();
}
}
stopRecording() {
if (this.isRecording) {
this.isRecording = false;
this.debugLog('๐Ÿ›‘ Stopping recording...');
// Send STOP message
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
const stopMessage = { type: "stop" };
this.websocket.send(JSON.stringify(stopMessage));
this.displayResponse('Sent STOP', stopMessage);
this.debugLog(`๐Ÿ“ค Sent STOP message`);
}
}
// Clean up audio resources
if (this.processor) {
this.processor.disconnect();
this.processor = null;
}
if (this.audioContext) {
this.audioContext.close().then(() => {
this.audioContext = null;
this.debugLog('๐Ÿ”Š Audio context closed');
});
}
if (this.audioStream) {
this.audioStream.getTracks().forEach(track => track.stop());
this.audioStream = null;
this.debugLog('๐ŸŽค Microphone stream stopped');
}
// Update UI
this.elements.micBtn.classList.remove('recording');
this.elements.micBtn.disabled = false;
this.elements.stopBtn.disabled = true;
if (this.isConnected) {
this.updateStatus('Connected - Recording stopped', 'connected');
}
// Final stats update
this.updateStats();
}
displayResponse(messageType, response) {
const responseBox = this.elements.responseBox;
const timestamp = new Date().toLocaleTimeString();
let content = `[${timestamp}] ${messageType}:\n`;
if (typeof response === 'object') {
content += JSON.stringify(response, null, 2);
} else {
content += response;
}
responseBox.innerHTML += content + '\n\n';
responseBox.scrollTop = responseBox.scrollHeight;
}
}
// Initialize when page loads
document.addEventListener('DOMContentLoaded', () => {
new SampleRateAnalyzer();
});
</script>
</body>
</html>