wip
This commit is contained in:
parent
43f3f9a281
commit
364df3d891
192
web/audio.js
Normal file
192
web/audio.js
Normal 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);
|
||||||
|
}
|
@ -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>
|
||||||
|
|
||||||
|
@ -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`);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user