| |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| 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'); |
|
|
| |
| let isRecording = false; |
| let mediaRecorder = null; |
| let audioChunks = []; |
| let socket = null; |
| let audioContext = null; |
| let silenceTimeout = null; |
| let silenceStart = null; |
|
|
| |
| const SILENCE_THRESHOLD = -50; |
| const SILENCE_DURATION = 1500; |
|
|
| |
| |
| |
| function connectWebSocket() { |
| |
| 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'); |
|
|
| |
| setTimeout(connectWebSocket, 3000); |
| }; |
|
|
| socket.onerror = (error) => { |
| console.error('WebSocket error:', error); |
| }; |
| } |
|
|
| |
| |
| |
| |
| async function handleAudioResponse(data) { |
| |
| const audioBlob = new Blob([data], { type: 'audio/mp3' }); |
|
|
| |
| const audioUrl = URL.createObjectURL(audioBlob); |
|
|
| |
| 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.'; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| function detectSilence(analyser, callback) { |
| const bufferLength = analyser.frequencyBinCount; |
| const dataArray = new Uint8Array(bufferLength); |
|
|
| function checkSilence() { |
| if (!isRecording) return; |
|
|
| analyser.getByteFrequencyData(dataArray); |
|
|
| |
| let sum = 0; |
| for (let i = 0; i < bufferLength; i++) { |
| sum += dataArray[i]; |
| } |
| const average = sum / bufferLength; |
|
|
| |
| 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(); |
| } |
|
|
| |
| |
| |
| async function startRecording() { |
| try { |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
|
|
| |
| 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 = []; |
|
|
| |
| try { |
| const arrayBuffer = await audioBlob.arrayBuffer(); |
| const audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); |
|
|
| |
| const pcmBuffer = convertToPCM(audioBuffer); |
|
|
| |
| 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.'; |
| } |
|
|
| |
| stream.getTracks().forEach(track => track.stop()); |
| }; |
|
|
| |
| mediaRecorder.start(100); |
| isRecording = true; |
| recordButton.classList.add('recording'); |
| statusElement.textContent = 'Recording... (will stop after silence)'; |
|
|
| |
| detectSilence(analyser, () => { |
| if (isRecording) { |
| stopRecording(); |
| } |
| }); |
|
|
| } catch (error) { |
| console.error('Error accessing microphone:', error); |
| statusElement.textContent = 'Error accessing microphone. Check permissions.'; |
| } |
| } |
|
|
| |
| |
| |
| function stopRecording() { |
| if (mediaRecorder && isRecording) { |
| mediaRecorder.stop(); |
| isRecording = false; |
| recordButton.classList.remove('recording'); |
| statusElement.textContent = 'Sending audio to server...'; |
| silenceStart = null; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| function convertToPCM(audioBuffer) { |
| |
| const sampleRate = audioBuffer.sampleRate; |
| const numberOfChannels = audioBuffer.numberOfChannels; |
| const targetSampleRate = 16000; |
| const length = audioBuffer.length; |
|
|
| |
| const channelData = audioBuffer.getChannelData(0); |
|
|
| |
| 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); |
| |
| const sample = Math.max(-1, Math.min(1, channelData[position])); |
| result[i] = sample < 0 ? sample * 0x8000 : sample * 0x7FFF; |
| } |
|
|
| return result.buffer; |
| } |
|
|
| |
| recordButton.addEventListener('click', () => { |
| if (!isRecording) { |
| startRecording(); |
| } else { |
| stopRecording(); |
| } |
| }); |
|
|
| audioPlayer.addEventListener('ended', () => { |
| statusElement.textContent = 'Click to start recording'; |
| }); |
|
|
| |
| connectWebSocket(); |
| }); |
|
|