This commit is contained in:
Dobromir Popov 2024-06-10 17:32:00 +03:00
parent 43f3f9a281
commit 364df3d891
3 changed files with 448 additions and 302 deletions

192
web/audio.js Normal file
View File

@ -0,0 +1,192 @@
let selectedDeviceId = "default";
export let serverTime;
export let recordButton;
let socket;
let audioRecorder;
let audioStream;
let recording = false;
let connectionStatus;
let statusRecording;
let audioContext;
let volumeChecker;
let lastVolumes = new Array(5);
let averageVolume;
let silenceCount = 0;
let isSpeaking = false;
let soundDetected = false;
let speakingCount = 0;
let analyser = null;
let SILENCE_DELAY_MS = 50;
let preDetect_IncludedAudio = 400; //ms
let soundCount_Threshold = 10;
let silenceCount_Threshold = 10;
const volumeHistory = [];
export function setSocket(newSocket) {
socket = newSocket;
}
export function setRecordButton(newRecordButton) {
recordButton = newRecordButton;
recordButton.addEventListener("click", toggleListening);
}
export function InitAudioAnalyser(stream) {
audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(stream);
analyser = audioContext.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.8;
source.connect(analyser);
}
export function startListening() {
//canvasCtx.fillStyle = "green";
recording = true;
navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 16000 } })
.then((stream) => {
audioStream = stream;
const audioContext = new AudioContext();
const sourceNode = audioContext.createMediaStreamSource(audioStream);
const audioSampleRate = sourceNode.context.sampleRate;
info.innerHTML = "Sample rate: " + audioSampleRate + " Hz";
var preBuffer = [];
const channelSplitter = audioContext.createChannelSplitter(2);
const channelMerger = audioContext.createChannelMerger(1);
sourceNode.connect(channelSplitter);
channelSplitter.connect(channelMerger, 0, 0);
const outputNode = channelMerger;
const mediaStreamDestination = audioContext.createMediaStreamDestination();
outputNode.connect(mediaStreamDestination);
const singleChannelStream = mediaStreamDestination.stream;
audioRecorder = new MediaRecorder(singleChannelStream);
audioRecorder.start();
audioRecorder.addEventListener("dataavailable", (event) => {
if (!soundDetected && autosend.checked) {
preBuffer = [];
preBuffer.push(event.data);
return;
}
if (event.data.size > 0) {
let data = event.data;
console.log("audio data size: " + data.size);
if (preBuffer.length > 0) {
sendAudioToServerPost(preBuffer);
}
sendAudioToServer(data);
soundDetected = false;
}
});
InitAudioAnalyser(stream);
});
recordButton.innerHTML = "Stop Talking";
recordButton.classList.toggle('bg-red-500');
recordButton.classList.toggle('bg-blue-500');
recordButton.classList.toggle('hover:bg-blue-700');
}
export function stopListening() {
recording = false;
audioRecorder.stop();
recordButton.innerHTML = "Push to Talk";
recordButton.classList.toggle('bg-blue-500');
recordButton.classList.toggle('bg-red-500');
recordButton.classList.toggle('hover:bg-blue-700');
clearInterval(volumeChecker);
if (audioStream) {
audioStream.getTracks().forEach(track => track.stop());
audioStream = null;
}
}
export function sendAudioToServerPost(data) {
const blob = new Blob(data, { type: "audio/ogg; codecs=opus" });
var formData = new FormData();
formData.append('file', data);
fetch('/upload', {
method: 'POST',
body: formData
});
}
export function sendAudioToServer(data) {
//if (connected) {
socket.send(JSON.stringify({ type: 'audio', task:"transcribe", audio: data }));
serverTime = Date.now();
if (!autosend.checked) {
transcription.innerHTML = "Processing audio...";
}
//}
}
export function toggleListening() {
if (socket.readyState === WebSocket.OPEN) {
if (recording) {
stopListening();
} else {
startListening();
}
}
}
export function initializeVolumeChecker() {
volumeChecker = setInterval(() => {
if (!audioContext) {
console.log("No audio context");
return;
}
const frequencyData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(frequencyData);
let totalVolume = 0;
for (let i = 0; i < frequencyData.length; i++) {
totalVolume += frequencyData[i];
}
averageVolume = totalVolume / frequencyData.length;
volumeHistory.push(averageVolume);
if (volumeHistory.length > 100) {
volumeHistory.shift();
}
const threshold = volumeHistory.reduce((acc, curr) => acc + curr) / volumeHistory.length + 5;
const isSilent = averageVolume < threshold;
if (averageVolume > threshold) {
if (autosend.checked && speakingCount == 0 && audioRecorder) {
soundDetected = false;
audioRecorder.stop();
audioRecorder.start();
}
speakingCount++;
if (speakingCount > soundCount_Threshold) {
statusRecording.innerHTML = "Listening...";
statusRecording.style.color = "green";
isSpeaking = true;
}
} else if (averageVolume - 5 < threshold) {
speakingCount = 0;
if (isSpeaking) {
silenceCount++;
if (silenceCount > silenceCount_Threshold) {
if (autosend.checked) {
soundDetected = true;
audioRecorder.stop();
audioRecorder.start();
}
isSpeaking = false;
statusRecording.innerHTML = "Silence detected...";
statusRecording.style.color = "orange";
}
}
}
}, SILENCE_DELAY_MS);
}

View File

@ -11,10 +11,16 @@
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-4 text-center">Real-time Voice Chat</h1> <h1 class="text-2xl font-bold mb-4 text-center">Real-time Voice Chat</h1>
<!-- Username Input -->
<div class="flex justify-center items-center mb-4"> <div class="flex justify-center items-center mb-4">
<!-- Username Input -->
<input type="text" id="username" class="border rounded p-2 mr-4" placeholder="Enter your username"> <input type="text" id="username" class="border rounded p-2 mr-4" placeholder="Enter your username">
<button onclick="joinChat()" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Join Chat</button> <button id="btn-join" onclick="joinChat()"
class="hidden bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Join Chat</button>
<!-- Clear Session Option -->
<button id="btn-disconnect" onclick="clearSession()"
class="hidden bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">Clear Session</button>
</div> </div>
<!-- Active Users List --> <!-- Active Users List -->
@ -24,7 +30,16 @@
<select id="users-list" class="w-full bg-white p-4 rounded shadow" multiple size="10"> <select id="users-list" class="w-full bg-white p-4 rounded shadow" multiple size="10">
<!-- Dynamic list of users --> <!-- Dynamic list of users -->
</select> </select>
<button onclick="startChat()" class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded mt-4">Start Chat</button> <button onclick="startChat()"
class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded mt-4">Start
Chat</button>
</div>
</div>
<div id="previous-chats-container" class="hidden w-2/3 mx-auto">
<h2 class="text-xl font-bold mb-2">Previous Chats</h2>
<div id="previous-chats" class="bg-white p-4 rounded shadow">
<!-- Previous chats content -->
</div> </div>
</div> </div>
@ -41,21 +56,22 @@
</div> </div>
<div class="mb-4"> <div class="mb-4">
<button id="record-button" disabled <button id="record-button" disabled
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-4">Push to Talk</button> class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-4">Push to
Talk</button>
</div> </div>
<div id="transcription" class="border rounded p-4 h-48 overflow-y-scroll mb-4"> <div id="transcription" class="border rounded p-4 h-48 overflow-y-scroll mb-4">
<!-- Transcription content --> <!-- Transcription content -->
</div> </div>
<canvas id="canvas" class="w-full mb-4"></canvas> <canvas id="canvas" class="w-full mb-4"></canvas>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<button id="copyButton" class="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-2 px-4 rounded focus:outline-none" onclick="copyToClipboard('transcription')">Copy</button> <button id="copyButton"
<button id="clearButton" class="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-2 px-4 rounded focus:outline-none" onclick="clearTranscription()">Clear</button> class="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-2 px-4 rounded focus:outline-none"
onclick="copyToClipboard('transcription')">Copy</button>
<button id="clearButton"
class="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-2 px-4 rounded focus:outline-none"
onclick="clearTranscription()">Clear</button>
</div> </div>
</div> </div>
<h2 class="text-xl font-bold mb-2">Previous Chats</h2>
<div id="previous-chats" class="bg-white p-4 rounded shadow">
<!-- Previous chats content -->
</div>
</div> </div>
<!-- Connection Status and Info --> <!-- Connection Status and Info -->
@ -68,42 +84,19 @@
<div id="status-recording" class="flex justify-center items-center mb-4"></div> <div id="status-recording" class="flex justify-center items-center mb-4"></div>
</div> </div>
<script> <script type="module">
import { startListening, stopListening, toggleListening, InitAudioAnalyser, sendAudioToServerPost, sendAudioToServer, initializeVolumeChecker, serverTime, setSocket, setRecordButton } from './audio.js';
let socket;
let sessionId; let sessionId;
let username; let username;
let selectedDeviceId = "default";
let socket;
let audioRecorder;
let audioStream;
let recording = false;
let recordButton;
let connected = false;
let connectionStatus;
let statusRecording;
let audioContext;
let serverTime;
let users = []; let users = [];
let selectedUsers = []; let selectedUsers = [];
let chats = []; let chats = [];
let volumeChecker; let recordButton;
let lastVolumes = new Array(5); let connectionStatus;
let averageVolume; let statusRecording;
let silenceCount = 0; let connected = false;
let isSpeaking = false;
let soundDetected = false;
let speakingCount = 0;
let SILENCE_DELAY_MS = 50;
let preDetect_IncludedAudio = 400; //ms
let soundCount_Threshold = 10;
let silenceCount_Threshold = 10;
const volumeHistory = [];
let canvas = document.getElementById("canvas");
let canvasCtx = canvas.getContext("2d");
let barWidth = 10;
let barSpacing = 5;
document.getElementById('autosend').addEventListener('change', (event) => { document.getElementById('autosend').addEventListener('change', (event) => {
const autosend = event.target.checked; const autosend = event.target.checked;
@ -115,79 +108,8 @@
}); });
}); });
function drawSlidingBarGraph(lastVolumes) {
canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < lastVolumes.length; i++) {
let value = lastVolumes[i];
let barHeight = (value / 255) * canvas.height;
let x = i * (barWidth + barSpacing);
let y = canvas.height - barHeight;
canvasCtx.fillRect(x, y, barWidth, barHeight);
}
}
volumeChecker = setInterval(() => {
if (!audioContext) {
console.log("No audio context");
return;
}
const frequencyData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(frequencyData);
let totalVolume = 0;
for (let i = 0; i < frequencyData.length; i++) {
totalVolume += frequencyData[i];
}
averageVolume = totalVolume / frequencyData.length;
volumeHistory.push(averageVolume);
if (volumeHistory.length > 100) {
volumeHistory.shift();
}
const threshold = volumeHistory.reduce((acc, curr) => acc + curr) / volumeHistory.length + 5;
const isSilent = averageVolume < threshold;
if (averageVolume > threshold) {
if (autosend.checked && speakingCount == 0 && audioRecorder) {
soundDetected = false;
audioRecorder.stop();
audioRecorder.start();
}
speakingCount++;
if (speakingCount > soundCount_Threshold) {
statusRecording.innerHTML = "Listening...";
statusRecording.style.color = "green";
isSpeaking = true;
}
} else if (averageVolume - 5 < threshold) {
speakingCount = 0;
if (isSpeaking) {
silenceCount++;
if (silenceCount > silenceCount_Threshold) {
if (autosend.checked) {
soundDetected = true;
audioRecorder.stop();
audioRecorder.start();
}
isSpeaking = false;
statusRecording.innerHTML = "Silence detected...";
statusRecording.style.color = "orange";
}
}
}
}, SILENCE_DELAY_MS);
function InitAudioAnalyser(stream) {
audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(stream);
analyser = audioContext.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.8;
source.connect(analyser);
}
function connect() { function connect() {
return new Promise((resolve, reject) => {
connectionStatus.innerHTML = "Connecting to WS..."; connectionStatus.innerHTML = "Connecting to WS...";
let wsurl = "ws://localhost:8081"; let wsurl = "ws://localhost:8081";
fetch("/wsurl") fetch("/wsurl")
@ -202,6 +124,7 @@
connectionStatus.innerHTML = "Connected to " + wsurl; connectionStatus.innerHTML = "Connected to " + wsurl;
recordButton.disabled = false; recordButton.disabled = false;
connected = true; connected = true;
resolve();
}; };
socket.onmessage = onmessage; socket.onmessage = onmessage;
socket.onclose = () => { socket.onclose = () => {
@ -209,38 +132,49 @@
recordButton.disabled = true; recordButton.disabled = true;
connected = false; connected = false;
setTimeout(() => { setTimeout(() => {
connect(); connect().then(resolve).catch(reject);
}, 5000); }, 5000);
}; };
setSocket(socket);
}) })
.catch((error) => { .catch((error) => {
connectionStatus.innerHTML = "Error getting ws url: " + error; connectionStatus.innerHTML = "Error getting ws url: " + error;
reject(error);
});
}); });
}; };
function onmessage(event) { function onmessage(event) {
try { try {
let json = JSON.parse(event.data); let json = JSON.parse(event.data);
if (json.hasOwnProperty("sessionId")) { switch (json.type) {
case "sessionId":
sessionId = json.sessionId; sessionId = json.sessionId;
console.log("Got session id: " + sessionId); console.log("Got session id: " + sessionId);
} break;
if (json.hasOwnProperty("languageDetected")) { case "languageDetected":
statusRecording.innerHTML = "Detected language: " + json.languageDetected; statusRecording.innerHTML = "Detected language: " + json.languageDetected;
} break;
if (json.hasOwnProperty("text")) { case "text":
transcription.innerHTML += "\r\n" + json.text; transcription.innerHTML += "\r\n" + json.text;
} break;
if (json.hasOwnProperty("users")) { case "userList":
users = json.users; users = json.users;
updateUserList(); updateUserList();
} break;
if (json.hasOwnProperty("chats")) { case "chats":
chats = json.chats; chats = json.chats;
updateChatList(); updateChatList();
break;
case "newChatRoom":
chats.push(json.newChatRoom);
updateChatList();
break;
default:
console.log("Unknown message type:", json.type);
} }
return;
} catch (e) { } catch (e) {
console.error("Failed to parse message", e);
} }
let latency = Date.now() - serverTime; let latency = Date.now() - serverTime;
@ -257,121 +191,76 @@
} }
} }
function startListening() {
canvasCtx.fillStyle = "green";
recording = true;
navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 16000 } })
.then((stream) => {
audioStream = stream;
const audioContext = new AudioContext();
const sourceNode = audioContext.createMediaStreamSource(audioStream);
const audioSampleRate = sourceNode.context.sampleRate;
info.innerHTML = "Sample rate: " + audioSampleRate + " Hz";
var preBuffer = [];
const channelSplitter = audioContext.createChannelSplitter(2);
const channelMerger = audioContext.createChannelMerger(1);
sourceNode.connect(channelSplitter);
channelSplitter.connect(channelMerger, 0, 0);
const outputNode = channelMerger;
const mediaStreamDestination = audioContext.createMediaStreamDestination();
outputNode.connect(mediaStreamDestination);
const singleChannelStream = mediaStreamDestination.stream;
audioRecorder = new MediaRecorder(singleChannelStream);
audioRecorder.start();
audioRecorder.addEventListener("dataavailable", (event) => {
if (!soundDetected && autosend.checked) {
preBuffer = [];
preBuffer.push(event.data);
return;
}
if (event.data.size > 0) {
let data = event.data;
if (preBuffer.length > 0) {
sendAudioToServerPost(preBuffer);
}
sendAudioToServer(data);
soundDetected = false;
}
});
InitAudioAnalyser(stream);
});
recordButton.innerHTML = "Stop Talking";
recordButton.classList.toggle('bg-red-500');
recordButton.classList.toggle('bg-blue-500');
recordButton.classList.toggle('hover:bg-blue-700');
}
function stopListening() {
recording = false;
audioRecorder.stop();
recordButton.innerHTML = "Push to Talk";
recordButton.classList.toggle('bg-blue-500');
recordButton.classList.toggle('bg-red-500');
recordButton.classList.toggle('hover:bg-blue-700');
clearInterval(volumeChecker);
if (audioStream) {
audioStream.getTracks().forEach(track => track.stop());
audioStream = null;
}
}
function sendAudioToServerPost(data) {
const blob = new Blob(data, { type: "audio/ogg; codecs=opus" });
var formData = new FormData();
formData.append('file', data);
fetch('/upload', {
method: 'POST',
body: formData
});
}
function sendAudioToServer(data) {
if (connected) {
socket.send(JSON.stringify({ type: 'audio', audio: data }));
serverTime = Date.now();
if (!autosend.checked) {
transcription.innerHTML = "Processing audio...";
}
}
}
function toggleListening() {
if (socket.readyState === WebSocket.OPEN) {
if (recording) {
stopListening();
} else {
startListening();
}
}
}
function joinChat() { function joinChat() {
username = document.getElementById('username').value; username = document.getElementById('username').value;
if (username.trim() === "") { if (username.trim() === "") {
alert("Please enter a username"); alert("Please enter a username");
return; return;
} }
socket.send(JSON.stringify({ type: 'join', username })); if (!socket || socket.readyState !== WebSocket.OPEN) {
document.getElementById('active-users-container').classList.remove('hidden'); alert("WebSocket connection is not open. Please wait and try again.");
fetchPreviousChats(username);
}
function startChat() {
const selectedOptions = Array.from(document.querySelectorAll('#users-list option:checked'));
selectedUsers = selectedOptions.map(option => option.value);
if (selectedUsers.length === 0) {
alert("Please select at least one user to start a chat.");
return; return;
} }
socket.send(JSON.stringify({ type: 'startChat', users: selectedUsers })); socket.send(JSON.stringify({ type: 'join', username }));
document.getElementById('chat-room-container').classList.remove('hidden'); document.cookie = `sessionId=${sessionId}; path=/;`;
document.cookie = `username=${username}; path=/;`;
showClearSessionOption();
}
function clearSession() {
document.cookie = "sessionId=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
location.reload();
}
function showClearSessionOption() {
const sessionId = getCookie("sessionId");
if (sessionId) {
if (!socket || socket.readyState !== WebSocket.OPEN) {
connect().then(() => {
socket.send(JSON.stringify({ type: 'reconnect', sessionId }));
//initializeVolumeChecker();
});
}
document.getElementById('btn-disconnect').classList.remove('hidden');
document.getElementById('btn-join').classList.add('hidden');
document.getElementById('active-users-container').classList.remove('hidden');
document.getElementById('previous-chats-container').classList.remove('hidden');
} else {
document.getElementById('btn-disconnect').classList.add('hidden');
document.getElementById('btn-join').classList.remove('hidden');
}
}
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
window.onload = () => {
recordButton = document.getElementById("record-button");
setRecordButton(recordButton);
connectionStatus = document.getElementById("connection-status");
statusRecording = document.getElementById("status-recording");
showClearSessionOption();
connect().then(() => {
initializeVolumeChecker();
});
};
function copyToClipboard(id) {
var textarea = document.getElementById(id);
textarea.select();
document.execCommand('copy');
}
function clearTranscription() {
document.getElementById('transcription').innerText = '';
} }
function updateUserList() { function updateUserList() {
@ -383,12 +272,39 @@
option.innerText = user.username; option.innerText = user.username;
if (user.username === username) { if (user.username === username) {
option.innerText += " (me)"; option.innerText += " (me)";
// option.disabled = true;
} }
usersList.appendChild(option); usersList.appendChild(option);
}); });
} }
function startChat() {
const selectedOptions = Array.from(document.querySelectorAll('#users-list option:checked'));
selectedUsers = selectedOptions.map(option => option.value);
if (selectedUsers.length === 0) {
alert("Please select at least one user to start a chat.");
return;
}
selectedUsers.push(sessionId); // Add self to the selected users list for self-chat
socket.send(JSON.stringify({ type: 'startChat', users: selectedUsers }));
document.getElementById('chat-room-container').classList.remove('hidden');
displayChatParticipants(selectedUsers);
}
function displayChatParticipants(participants) {
const chatRoom = document.getElementById('chat-room');
let participantsHtml = '<div class="flex flex-wrap mb-4">';
participants.forEach(participantId => {
const user = users.find(u => u.sessionId === participantId);
const status = user ? "online" : "offline";
const username = user ? user.username : "Unknown User";
participantsHtml += `<span class="inline-flex items-center px-3 py-0.5 rounded-full text-sm font-medium bg-${status === 'online' ? 'green' : 'gray'}-100 text-${status === 'online' ? 'green' : 'gray'}-800 mr-2 mb-2">
${username} (${status})
</span>`;
});
participantsHtml += '</div>';
chatRoom.insertAdjacentHTML('afterbegin', participantsHtml);
}
function fetchPreviousChats(username) { function fetchPreviousChats(username) {
fetch(`/chats?username=${username}`) fetch(`/chats?username=${username}`)
.then(response => response.json()) .then(response => response.json())
@ -397,6 +313,11 @@
updateChatList(); updateChatList();
}); });
} }
function sendSocketMessage(message) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message));
}
}
function updateChatList() { function updateChatList() {
const previousChats = document.getElementById('previous-chats'); const previousChats = document.getElementById('previous-chats');
@ -414,25 +335,14 @@
}); });
} }
window.onload = () => { // Expose functions to global scope
recordButton = document.getElementById("record-button"); window.joinChat = joinChat;
recordButton.addEventListener("click", toggleListening); window.clearSession = clearSession;
connectionStatus = document.getElementById("connection-status"); window.copyToClipboard = copyToClipboard;
statusRecording = document.getElementById("status-recording"); window.clearTranscription = clearTranscription;
window.startChat = startChat;
connect(socket);
};
function copyToClipboard(id) {
var textarea = document.getElementById(id);
textarea.select();
document.execCommand('copy');
}
function clearTranscription() {
document.getElementById('transcription').innerText = '';
}
</script> </script>
<script src="https://cdn.webrtc-experiment.com/MediaStreamRecorder.js"></script> <script src="https://cdn.webrtc-experiment.com/MediaStreamRecorder.js"></script>
</body> </body>

View File

@ -23,7 +23,6 @@ let storeRecordings = false;
let queueCounter = 0; let queueCounter = 0;
const sessions = new Map(); const sessions = new Map();
const users = new Map();
const chats = new Map(); // Store chat rooms const chats = new Map(); // Store chat rooms
storage.init().then(() => { storage.init().then(() => {
@ -48,18 +47,57 @@ wss.on('connection', (ws) => {
ws.on('message', (message) => { ws.on('message', (message) => {
try { try {
const data = JSON.parse(message); const data = JSON.parse(message);
console.log('Received message:', data.type);
if (data.type === 'join') { switch (data.type) {
case 'join':
const { username } = data; const { username } = data;
users.set(ws.sessionId, { username, sessionId: ws.sessionId }); sessions.set(ws.sessionId, { username, sessionId: ws.sessionId });
broadcastUserList(); broadcastUserList();
} else if (data.type === 'startChat') { break;
case 'startChat':
const { users: chatUsers } = data; const { users: chatUsers } = data;
const chatId = Math.random().toString(36).substring(2); const chatId = Math.random().toString(36).substring(2);
chats.set(chatId, { participants: [ws.sessionId, ...chatUsers], messages: [] }); let participants = [ws.sessionId, ...chatUsers];
// Deduplicate participants
participants = [...new Set(participants)];
chats.set(chatId, { participants, messages: [] });
const userNames = participants.map(sessionId => {
const user = sessions.get(sessionId);
return user ? `${user.username}(${user.sessionId})` : 'Unknown User';
});
console.log('Creating chat room. Users:', userNames);
participants.forEach(sessionId => {
const participantSocket = Array.from(wss.clients).find(client => client.sessionId === sessionId);
if (participantSocket && participantSocket.readyState === WebSocket.OPEN) {
participantSocket.send(JSON.stringify({ newChatRoom: { id: chatId, participants: userNames } }));
}
});
broadcastUserList(); broadcastUserList();
} else if (data.type === 'audio') { break;
case 'audio':
console.log('(queue ' + queueCounter + ') Received ' + (data.audio.length / 1024 / 1024).toFixed(3) + ' MB audio from client. Crrent language: ' + language, 'task: ' + data.task);
handleAudioData(ws, data.audio); handleAudioData(ws, data.audio);
break;
case 'reconnect':
const userSession = sessions.get(data.sessionId);
if (userSession) {
sessions.set(ws.sessionId, userSession);
ws.sessionId = data.sessionId;
broadcastUserList();
// Send existing chats
const userChats = Array.from(chats.values()).filter(chat => chat.participants.includes(ws.sessionId));
ws.send(JSON.stringify({ type: 'chats', chats: userChats }));
ws.send(JSON.stringify({ type: 'userList', users: userList }));
}
break;
default:
console.log('Unknown message type:', data.type);
} }
} catch (err) { } catch (err) {
console.error('Failed to parse message', err); console.error('Failed to parse message', err);
@ -67,7 +105,7 @@ wss.on('connection', (ws) => {
}); });
ws.on('close', () => { ws.on('close', () => {
users.delete(ws.sessionId); // allUsers.delete(ws.sessionId);
sessions.delete(ws.sessionId); sessions.delete(ws.sessionId);
broadcastUserList(); broadcastUserList();
}); });
@ -140,7 +178,7 @@ function transcribeAudio(ws, formData, sessionData) {
} }
function broadcastUserList() { function broadcastUserList() {
const userList = Array.from(users.values()).map(user => ({ username: user.username, sessionId: user.sessionId })); const userList = Array.from(sessions.values()).map(user => ({ username: user.username, sessionId: user.sessionId }));
wss.clients.forEach(client => { wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) { if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ type: 'userList', users: userList })); client.send(JSON.stringify({ type: 'userList', users: userList }));
@ -153,6 +191,11 @@ app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'chat-client.html')); res.sendFile(path.join(__dirname, 'chat-client.html'));
}); });
app.get('/audio.js', (req, res) => {
res.sendFile(path.join(__dirname, 'audio.js'));
});
app.post('/log', (req, res) => { app.post('/log', (req, res) => {
console.log(`[LOG ${new Date().toISOString()}] ${req.body.message}`); console.log(`[LOG ${new Date().toISOString()}] ${req.body.message}`);
res.status(200).send('OK'); res.status(200).send('OK');
@ -185,6 +228,7 @@ app.post('/settings', (req, res) => {
app.post('/upload', (req, res) => { app.post('/upload', (req, res) => {
const timestamp = Date.now(); const timestamp = Date.now();
console.log('Received audio data:', timestamp);
fs.mkdir('rec', { recursive: true }, (err) => { fs.mkdir('rec', { recursive: true }, (err) => {
if (err) return res.status(500).send('ERROR'); if (err) return res.status(500).send('ERROR');
const file = fs.createWriteStream(`rec/audio_slice_${timestamp}.ogg`); const file = fs.createWriteStream(`rec/audio_slice_${timestamp}.ogg`);