Omani-AI / static /script.js
EbukaGaus's picture
backend
b6b326b
// Omani Arabic Speech Interface - WebSocket Client
document.addEventListener('DOMContentLoaded', () => {
// DOM Elements
const recordButton = document.getElementById('recordButton');
const recordIcon = document.getElementById('recordIcon');
const statusElement = document.getElementById('status');
const transcriptElement = document.getElementById('transcript');
const audioPlayer = document.getElementById('audioPlayer');
const connectionStatus = document.getElementById('connectionStatus');
// Audio Recording State
let isRecording = false;
let mediaRecorder = null;
let audioChunks = [];
let socket = null;
let audioContext = null;
let silenceTimeout = null;
let silenceStart = null;
// Constants
const SILENCE_THRESHOLD = -50; // dB
const SILENCE_DURATION = 1500; // ms
/**
* Establishes WebSocket connection to the server
*/
function connectWebSocket() {
// Get the current hostname and protocol
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/talk`;
socket = new WebSocket(wsUrl);
socket.onopen = () => {
connectionStatus.textContent = 'Connected';
connectionStatus.classList.remove('disconnected');
connectionStatus.classList.add('connected');
console.log('WebSocket connection established');
};
socket.onmessage = (event) => {
handleAudioResponse(event.data);
};
socket.onclose = () => {
connectionStatus.textContent = 'Disconnected';
connectionStatus.classList.remove('connected');
connectionStatus.classList.add('disconnected');
console.log('WebSocket connection closed');
// Try to reconnect after a delay
setTimeout(connectWebSocket, 3000);
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
/**
* Handles audio response from the server
* @param {Blob} data Binary audio data from the server
*/
async function handleAudioResponse(data) {
// Convert the binary audio data to a Blob
const audioBlob = new Blob([data], { type: 'audio/mp3' });
// Create a URL for the Blob
const audioUrl = URL.createObjectURL(audioBlob);
// Set the audio source and play it
audioPlayer.src = audioUrl;
audioPlayer.classList.remove('hidden');
try {
await audioPlayer.play();
statusElement.textContent = 'Playing response...';
} catch (err) {
console.error('Error playing audio:', err);
statusElement.textContent = 'Error playing audio. Click to play manually.';
}
}
/**
* Detects silence in the audio stream
* @param {AnalyserNode} analyser Audio analyser node
* @param {Function} callback Function to call when silence is detected
*/
function detectSilence(analyser, callback) {
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
function checkSilence() {
if (!isRecording) return;
analyser.getByteFrequencyData(dataArray);
// Calculate average volume
let sum = 0;
for (let i = 0; i < bufferLength; i++) {
sum += dataArray[i];
}
const average = sum / bufferLength;
// Convert to dB
const dB = 20 * Math.log10(average / 255);
if (dB < SILENCE_THRESHOLD) {
if (!silenceStart) {
silenceStart = Date.now();
} else if (Date.now() - silenceStart > SILENCE_DURATION) {
callback();
return;
}
} else {
silenceStart = null;
}
requestAnimationFrame(checkSilence);
}
checkSilence();
}
/**
* Starts recording audio from the microphone
*/
async function startRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Set up AudioContext for silence detection
audioContext = new (window.AudioContext || window.webkitAudioContext)();
const source = audioContext.createMediaStreamSource(stream);
const analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
source.connect(analyser);
mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus',
audioBitsPerSecond: 16000
});
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data);
}
};
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
audioChunks = [];
// Convert to the format expected by the server (LINEAR16 16kHz)
try {
const arrayBuffer = await audioBlob.arrayBuffer();
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
// Convert to raw PCM
const pcmBuffer = convertToPCM(audioBuffer);
// Send the audio data to the server
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(pcmBuffer);
statusElement.textContent = 'Processing...';
} else {
statusElement.textContent = 'Connection lost. Try again.';
connectWebSocket();
}
} catch (error) {
console.error('Error processing audio:', error);
statusElement.textContent = 'Error processing audio.';
}
// Stop all tracks
stream.getTracks().forEach(track => track.stop());
};
// Start recording
mediaRecorder.start(100);
isRecording = true;
recordButton.classList.add('recording');
statusElement.textContent = 'Recording... (will stop after silence)';
// Set up silence detection
detectSilence(analyser, () => {
if (isRecording) {
stopRecording();
}
});
} catch (error) {
console.error('Error accessing microphone:', error);
statusElement.textContent = 'Error accessing microphone. Check permissions.';
}
}
/**
* Stops recording audio
*/
function stopRecording() {
if (mediaRecorder && isRecording) {
mediaRecorder.stop();
isRecording = false;
recordButton.classList.remove('recording');
statusElement.textContent = 'Sending audio to server...';
silenceStart = null;
}
}
/**
* Converts AudioBuffer to 16-bit PCM at 16kHz
* @param {AudioBuffer} audioBuffer Audio buffer to convert
* @returns {ArrayBuffer} PCM audio buffer
*/
function convertToPCM(audioBuffer) {
// Convert AudioBuffer to 16-bit PCM at 16kHz
const sampleRate = audioBuffer.sampleRate;
const numberOfChannels = audioBuffer.numberOfChannels;
const targetSampleRate = 16000;
const length = audioBuffer.length;
// Get the audio data from the first channel
const channelData = audioBuffer.getChannelData(0);
// Simple resampling
const resamplingRatio = sampleRate / targetSampleRate;
const newLength = Math.floor(length / resamplingRatio);
const result = new Int16Array(newLength);
for (let i = 0; i < newLength; i++) {
const position = Math.floor(i * resamplingRatio);
// Convert float32 [-1.0, 1.0] to int16 [-32768, 32767]
const sample = Math.max(-1, Math.min(1, channelData[position]));
result[i] = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
}
return result.buffer;
}
// Event listeners
recordButton.addEventListener('click', () => {
if (!isRecording) {
startRecording();
} else {
stopRecording();
}
});
audioPlayer.addEventListener('ended', () => {
statusElement.textContent = 'Click to start recording';
});
// Connect to WebSocket when the page loads
connectWebSocket();
});