Spaces:
Build error
Build error
/** | |
* WebSocket Connection | |
* The client sends and receives messages through this WebSocket connection. | |
*/ | |
const connectButton = document.getElementById('connect'); | |
const disconnectButton = document.getElementById('disconnect'); | |
const devicesContainer = document.getElementById('devices-container'); | |
let socket; | |
let clientId = Math.floor(Math.random() * 10000000); | |
function connectSocket() { | |
chatWindow.value = ""; | |
var clientId = Math.floor(Math.random() * 1010000); | |
var ws_scheme = window.location.protocol == "https:" ? "wss" : "ws"; | |
var ws_path = ws_scheme + '://' + window.location.host + `/ws/${clientId}`; | |
socket = new WebSocket(ws_path); | |
socket.binaryType = 'arraybuffer'; | |
socket.onopen = (event) => { | |
console.log("successfully connected"); | |
connectMicrophone(audioDeviceSelection.value); | |
speechRecognition(); | |
socket.send("web"); // select web as the platform | |
}; | |
socket.onmessage = (event) => { | |
if (typeof event.data === 'string') { | |
const message = event.data; | |
if (message == '[end]\n') { | |
chatWindow.value += "\n\n"; | |
chatWindow.scrollTop = chatWindow.scrollHeight; | |
} else if (message.startsWith('[+]')) { | |
// [+] indicates the transcription is done. stop playing audio | |
chatWindow.value += `\nYou> ${message}\n`; | |
stopAudioPlayback(); | |
} else if (message.startsWith('[=]')) { | |
// [=] indicates the response is done | |
chatWindow.value += "\n\n"; | |
chatWindow.scrollTop = chatWindow.scrollHeight; | |
} else if (message.startsWith('Select')) { | |
createCharacterGroups(message); | |
} else { | |
chatWindow.value += `${event.data}`; | |
chatWindow.scrollTop = chatWindow.scrollHeight; | |
// if user interrupts the previous response, should be able to play audios of new response | |
shouldPlayAudio=true; | |
} | |
} else { // binary data | |
if (!shouldPlayAudio) { | |
return; | |
} | |
audioQueue.push(event.data); | |
if (audioQueue.length === 1) { | |
playAudios(); | |
} | |
} | |
}; | |
socket.onerror = (error) => { | |
console.log(`WebSocket Error: ${error}`); | |
}; | |
socket.onclose = (event) => { | |
console.log("Socket closed"); | |
}; | |
} | |
connectButton.addEventListener("click", function() { | |
connectButton.style.display = "none"; | |
textContainer.textContent = "Select a character"; | |
devicesContainer.style.display = "none"; | |
connectSocket(); | |
talkButton.style.display = 'flex'; | |
textButton.style.display = 'flex'; | |
}); | |
disconnectButton.addEventListener("click", function() { | |
stopAudioPlayback(); | |
if (radioGroupsCreated) { | |
destroyRadioGroups(); | |
} | |
if (mediaRecorder) { | |
mediaRecorder.stop(); | |
} | |
if (recognition) { | |
recognition.stop(); | |
} | |
textContainer.textContent = ""; | |
disconnectButton.style.display = "none"; | |
playerContainer.style.display = "none"; | |
stopCallButton.style.display = "none"; | |
continueCallButton.style.display = "none"; | |
messageButton.style.display = "none"; | |
sendButton.style.display = "none"; | |
messageInput.style.display = "none"; | |
chatWindow.style.display = "none"; | |
callButton.style.display = "none"; | |
connectButton.style.display = "flex"; | |
devicesContainer.style.display = "flex"; | |
talkButton.disabled = true; | |
textButton.disabled = true; | |
chatWindow.value = ""; | |
selectedCharacter = null; | |
characterSent = false; | |
callActive = false; | |
showRecordingStatus(); | |
socket.close(); | |
}); | |
/** | |
* Devices | |
* Get the list of media devices | |
*/ | |
const audioDeviceSelection = document.getElementById('audio-device-selection'); | |
window.addEventListener("load", function() { | |
navigator.mediaDevices.enumerateDevices() | |
.then(function(devices) { | |
// Filter out the audio input devices | |
let audioInputDevices = devices.filter(function(device) { | |
return device.kind === 'audioinput'; | |
}); | |
// If there are no audio input devices, display an error and return | |
if (audioInputDevices.length === 0) { | |
console.log('No audio input devices found'); | |
return; | |
} | |
// Add the audio input devices to the dropdown | |
audioInputDevices.forEach(function(device, index) { | |
let option = document.createElement('option'); | |
option.value = device.deviceId; | |
option.textContent = device.label || `Microphone ${index + 1}`; | |
audioDeviceSelection.appendChild(option); | |
}); | |
}) | |
.catch(function(err) { | |
console.log('An error occurred: ' + err); | |
}); | |
}); | |
audioDeviceSelection.addEventListener('change', function(e) { | |
connectMicrophone(e.target.value); | |
}); | |
/** | |
* Audio Recording and Transmission | |
* captures audio from the user's microphone, which is then sent over the | |
* WebSocket connection then sent over the WebSocket connection to the server | |
* when the recording stops. | |
*/ | |
let mediaRecorder; | |
let chunks = []; | |
let finalTranscripts = []; | |
let debug = false; | |
let audioSent = false; | |
function connectMicrophone(deviceId) { | |
navigator.mediaDevices.getUserMedia({ | |
audio: { | |
deviceId: deviceId ? {exact: deviceId} : undefined, | |
echoCancellation: true | |
} | |
}).then(function(stream) { | |
mediaRecorder = new MediaRecorder(stream); | |
mediaRecorder.ondataavailable = function(e) { | |
chunks.push(e.data); | |
} | |
mediaRecorder.onstart = function() { | |
console.log("recorder starts"); | |
} | |
mediaRecorder.onstop = function(e) { | |
console.log("recorder stops"); | |
let blob = new Blob(chunks, {'type' : 'audio/webm'}); | |
chunks = []; | |
if (debug) { | |
// Save the audio | |
let url = URL.createObjectURL(blob); | |
let a = document.createElement("a"); | |
document.body.appendChild(a); | |
a.style = "display: none"; | |
a.href = url; | |
a.download = 'test.webm'; | |
a.click(); | |
} | |
if (socket && socket.readyState === WebSocket.OPEN) { | |
if (!audioSent && callActive) { | |
console.log("sending audio"); | |
socket.send(blob); | |
} | |
audioSent = false; | |
if (callActive) { | |
mediaRecorder.start(); | |
} | |
} | |
} | |
}) | |
.catch(function(err) { | |
console.log('An error occurred: ' + err); | |
}); | |
} | |
/** | |
* Speech Recognition | |
* listens for when the user's speech ends and stops the recording. | |
*/ | |
let recognition; | |
let onresultTimeout; | |
let onspeechTimeout; | |
let confidence; | |
function speechRecognition() { | |
// Initialize SpeechRecognition | |
window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
recognition = new SpeechRecognition(); | |
recognition.interimResults = true; | |
recognition.maxAlternatives = 1; | |
recognition.continuous = true; | |
recognition.onstart = function() { | |
console.log("recognition starts"); | |
} | |
recognition.onresult = function(event) { | |
// Clear the timeout if a result is received | |
clearTimeout(onresultTimeout); | |
clearTimeout(onspeechTimeout); | |
stopAudioPlayback() | |
const result = event.results[event.results.length - 1]; | |
const transcriptObj = result[0]; | |
const transcript = transcriptObj.transcript; | |
const ifFinal = result.isFinal; | |
if (ifFinal) { | |
console.log(`final transcript: {${transcript}}`); | |
finalTranscripts.push(transcript); | |
confidence = transcriptObj.confidence; | |
socket.send(`[&]${transcript}`); | |
} else { | |
console.log(`interim transcript: {${transcript}}`); | |
} | |
// Set a new timeout | |
onresultTimeout = setTimeout(() => { | |
if (ifFinal) { | |
return; | |
} | |
// If the timeout is reached, send the interim transcript | |
console.log(`TIMEOUT: interim transcript: {${transcript}}`); | |
socket.send(`[&]${transcript}`); | |
}, 500); // 500 ms | |
onspeechTimeout = setTimeout(() => { | |
recognition.stop(); | |
}, 2000); // 2 seconds | |
} | |
recognition.onspeechend = function() { | |
console.log("speech ends"); | |
if (socket && socket.readyState === WebSocket.OPEN){ | |
audioSent = true; | |
mediaRecorder.stop(); | |
if (confidence > 0.8 && finalTranscripts.length > 0) { | |
console.log("send final transcript"); | |
let message = finalTranscripts.join(' '); | |
socket.send(message); | |
chatWindow.value += `\nYou> ${message}\n`; | |
chatWindow.scrollTop = chatWindow.scrollHeight; | |
shouldPlayAudio = true; | |
} | |
} | |
finalTranscripts = []; | |
}; | |
recognition.onend = function() { | |
console.log("recognition ends"); | |
if (socket && socket.readyState === WebSocket.OPEN && callActive){ | |
recognition.start(); | |
} | |
}; | |
} | |
/** | |
* Voice-based Chatting | |
* allows users to start a voice chat. | |
*/ | |
const talkButton = document.getElementById('talk-btn'); | |
const textButton = document.getElementById('text-btn'); | |
const callButton = document.getElementById('call'); | |
const textContainer = document.querySelector('.header p'); | |
const playerContainer = document.getElementById('player-container'); | |
const soundWave = document.getElementById('sound-wave'); | |
const stopCallButton = document.getElementById('stop-call'); | |
const continueCallButton = document.getElementById('continue-call'); | |
let callActive = false; | |
callButton.addEventListener("click", () => { | |
playerContainer.style.display = 'flex'; | |
chatWindow.style.display = 'none'; | |
sendButton.style.display = 'none'; | |
messageInput.style.display = "none"; | |
callButton.style.display = "none"; | |
messageButton.style.display = 'flex'; | |
if (callActive) { | |
stopCallButton.style.display = 'flex'; | |
soundWave.style.display = 'flex'; | |
} else { | |
continueCallButton.style.display = 'flex'; | |
} | |
showRecordingStatus(); | |
}); | |
stopCallButton.addEventListener("click", () => { | |
soundWave.style.display = "none"; | |
stopCallButton.style.display = "none"; | |
continueCallButton.style.display = "flex"; | |
callActive = false; | |
mediaRecorder.stop(); | |
recognition.stop(); | |
stopAudioPlayback(); | |
showRecordingStatus(); | |
}) | |
continueCallButton.addEventListener("click", () => { | |
stopCallButton.style.display = "flex"; | |
continueCallButton.style.display = "none"; | |
soundWave.style.display = "flex"; | |
mediaRecorder.start(); | |
recognition.start(); | |
callActive = true; | |
showRecordingStatus(); | |
}); | |
function showRecordingStatus() { | |
// show recording status | |
if (mediaRecorder.state == "recording") { | |
recordingStatus.style.display = "inline-block"; | |
} else { | |
recordingStatus.style.display = "none"; | |
} | |
} | |
talkButton.addEventListener("click", function() { | |
if (socket && socket.readyState === WebSocket.OPEN && mediaRecorder && selectedCharacter) { | |
playerContainer.style.display = "flex"; | |
talkButton.style.display = "none"; | |
textButton.style.display = 'none'; | |
disconnectButton.style.display = "flex"; | |
messageButton.style.display = "flex"; | |
stopCallButton.style.display = "flex"; | |
soundWave.style.display = "flex"; | |
textContainer.textContent = "Hi, my friend, what brings you here today?"; | |
shouldPlayAudio=true; | |
socket.send(selectedCharacter); | |
hideOtherCharacters(); | |
mediaRecorder.start(); | |
recognition.start(); | |
callActive = true; | |
showRecordingStatus(); | |
} | |
}); | |
textButton.addEventListener("click", function() { | |
if (socket && socket.readyState === WebSocket.OPEN && mediaRecorder && selectedCharacter) { | |
messageButton.click(); | |
disconnectButton.style.display = "flex"; | |
textContainer.textContent = ""; | |
shouldPlayAudio=true; | |
socket.send(selectedCharacter); | |
hideOtherCharacters(); | |
showRecordingStatus(); | |
} | |
}); | |
function hideOtherCharacters() { | |
// Hide the radio buttons that are not selected | |
const radioButtons = document.querySelectorAll('.radio-buttons input[type="radio"]'); | |
radioButtons.forEach(radioButton => { | |
if (radioButton.value != selectedCharacter) { | |
radioButton.parentElement.style.display = 'none'; | |
} | |
}); | |
} | |
/** | |
* Text-based Chatting | |
* allow users to send text-based messages through the WebSocket connection. | |
*/ | |
const messageInput = document.getElementById('message-input'); | |
const sendButton = document.getElementById('send-btn'); | |
const messageButton = document.getElementById('message'); | |
const chatWindow = document.getElementById('chat-window'); | |
const recordingStatus = document.getElementById("recording"); | |
let characterSent = false; | |
messageButton.addEventListener('click', function() { | |
playerContainer.style.display = 'none'; | |
chatWindow.style.display = 'block'; | |
talkButton.style.display = 'none'; | |
textButton.style.display = 'none'; | |
sendButton.style.display = 'block'; | |
messageInput.style.display = "block"; | |
callButton.style.display = "flex"; | |
messageButton.style.display = 'none'; | |
continueCallButton.style.display = 'none'; | |
stopCallButton.style.display = 'none'; | |
soundWave.style.display = "none"; | |
showRecordingStatus(); | |
}); | |
const sendMessage = () => { | |
if (socket && socket.readyState === WebSocket.OPEN) { | |
const message = messageInput.value; | |
chatWindow.value += `\nYou> ${message}\n`; | |
chatWindow.scrollTop = chatWindow.scrollHeight; | |
socket.send(message); | |
messageInput.value = ""; | |
if (isPlaying) { | |
stopAudioPlayback(); | |
} | |
} | |
} | |
sendButton.addEventListener("click", sendMessage); | |
messageInput.addEventListener("keydown", (event) => { | |
if (event.key === "Enter") { | |
event.preventDefault(); | |
sendMessage(); | |
} | |
}); | |
/** | |
* Character Selection | |
* parses the initial message from the server that asks the user to select a | |
* character for the chat. creates radio buttons for the character selection. | |
*/ | |
let selectedCharacter; | |
let radioGroupsCreated = false; | |
function createCharacterGroups(message) { | |
const options = message.split('\n').slice(1); | |
// Create a map from character name to image URL | |
// TODO: store image in database and let server send the image url to client. | |
const imageMap = { | |
'Raiden Shogun And Ei': '/static/raiden.svg', | |
'Loki': '/static/loki.svg', | |
'Ai Character Helper': '/static/ai_helper.png', | |
'Reflection Pi': '/static/pi.jpeg', | |
'Elon Musk': '/static/elon.png', | |
'Bruce Wayne': '/static/bruce.png', | |
'Steve Jobs': '/static/jobs.png', | |
}; | |
const radioButtonDiv = document.getElementsByClassName('radio-buttons')[0]; | |
options.forEach(option => { | |
const match = option.match(/^(\d+)\s-\s(.+)$/); | |
if (match) { | |
const label = document.createElement('label'); | |
label.className = 'custom-radio'; | |
const input = document.createElement('input'); | |
input.type = 'radio'; | |
input.name = 'radio'; | |
input.value = match[1]; // The option number is the value | |
const span = document.createElement('span'); | |
span.className = 'radio-btn'; | |
span.innerHTML = '<i class="las la-check"></i>'; | |
const hobbiesIcon = document.createElement('div'); | |
hobbiesIcon.className = 'hobbies-icon'; | |
const img = document.createElement('img'); | |
let src = imageMap[match[2]]; | |
if (!src) { | |
src = '/static/realchar.svg'; | |
} | |
img.src = src; | |
// Create a h3 element | |
const h3 = document.createElement('h4'); | |
h3.textContent = match[2]; // The option name is the text | |
hobbiesIcon.appendChild(img); | |
hobbiesIcon.appendChild(h3); | |
span.appendChild(hobbiesIcon); | |
label.appendChild(input); | |
label.appendChild(span); | |
radioButtonDiv.appendChild(label); | |
} | |
}); | |
radioButtonDiv.addEventListener('change', (event) => { | |
if (event.target.value != "") { | |
selectedCharacter = event.target.value; | |
} | |
talkButton.disabled = false; | |
textButton.disabled = false; | |
}); | |
radioGroupsCreated = true; | |
} | |
function destroyRadioGroups() { | |
const radioButtonDiv = document.getElementsByClassName('radio-buttons')[0]; | |
while (radioButtonDiv.firstChild) { | |
radioButtonDiv.removeChild(radioButtonDiv.firstChild); | |
} | |
selectedCharacter = null; | |
radioGroupsCreated = false; | |
} | |
// This function will add or remove the pulse animation | |
function togglePulseAnimation() { | |
const selectedRadioButton = document.querySelector('.custom-radio input:checked + .radio-btn'); | |
if (isPlaying && selectedRadioButton) { | |
// Remove existing pulse animations | |
selectedRadioButton.classList.remove("pulse-animation-1"); | |
selectedRadioButton.classList.remove("pulse-animation-2"); | |
// Add a new pulse animation, randomly choosing between the two speeds | |
const animationClass = Math.random() > 0.5 ? "pulse-animation-1" : "pulse-animation-2"; | |
selectedRadioButton.classList.add(animationClass); | |
} else if (selectedRadioButton) { | |
selectedRadioButton.classList.remove("pulse-animation-1"); | |
selectedRadioButton.classList.remove("pulse-animation-2"); | |
} | |
} | |
/** | |
* Audio Playback | |
* playing back audio received from the server. | |
*/ | |
const audioPlayer = document.getElementById('audio-player') | |
let audioQueue = []; | |
let audioContext; | |
let shouldPlayAudio = false; | |
let isPlaying = false; | |
// Function to unlock the AudioContext | |
function unlockAudioContext(audioContext) { | |
if (audioContext.state === 'suspended') { | |
var unlock = function() { | |
audioContext.resume().then(function() { | |
document.body.removeEventListener('touchstart', unlock); | |
document.body.removeEventListener('touchend', unlock); | |
}); | |
}; | |
document.body.addEventListener('touchstart', unlock, false); | |
document.body.addEventListener('touchend', unlock, false); | |
} | |
} | |
async function playAudios() { | |
isPlaying = true; | |
togglePulseAnimation(); | |
while (audioQueue.length > 0) { | |
let data = audioQueue[0]; | |
let blob = new Blob([data], { type: 'audio/mp3' }); | |
let audioUrl = URL.createObjectURL(blob); | |
await playAudio(audioUrl); | |
audioQueue.shift(); | |
} | |
isPlaying = false; | |
togglePulseAnimation(); | |
} | |
function playAudio(url) { | |
if (!audioContext) { | |
audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
unlockAudioContext(audioContext); | |
} | |
if (!audioPlayer) { | |
audioPlayer = document.getElementById('audio-player'); | |
} | |
return new Promise((resolve) => { | |
audioPlayer.src = url; | |
audioPlayer.muted = true; // Start muted | |
audioPlayer.play(); | |
audioPlayer.onended = resolve; | |
audioPlayer.play().then(() => { | |
audioPlayer.muted = false; // Unmute after playback starts | |
}).catch(error => alert(`Playback failed because: ${error}`)); | |
}); | |
} | |
function stopAudioPlayback() { | |
if (audioPlayer) { | |
audioPlayer.pause(); | |
shouldPlayAudio = false; | |
} | |
audioQueue = []; | |
isPlaying = false; | |
togglePulseAnimation(); | |
} | |