AI:adding prisma, redesign

This commit is contained in:
Dobromir Popov 2024-07-06 17:41:22 +03:00
parent a454f219b7
commit 355e81399a
7 changed files with 445 additions and 592 deletions

18
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "kevin-ai", "name": "kevin-ai",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@prisma/client": "^5.16.1",
"axios": "^1.7.2", "axios": "^1.7.2",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
@ -21,6 +22,23 @@
"ws": "^8.12.1" "ws": "^8.12.1"
} }
}, },
"node_modules/@prisma/client": {
"version": "5.16.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.16.1.tgz",
"integrity": "sha512-wM9SKQjF0qLxdnOZIVAIMKiz6Hu7vDt4FFAih85K1dk/Rr2mdahy6d3QP41K62N9O0DJJA//gUDA3Mp49xsKIg==",
"hasInstallScript": true,
"engines": {
"node": ">=16.13"
},
"peerDependencies": {
"prisma": "*"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
}
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "18.19.34", "version": "18.19.34",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz",

View File

@ -7,12 +7,12 @@
"start:demo": "NODE_ENV=demo node web/server.js", "start:demo": "NODE_ENV=demo node web/server.js",
"start:demo-chat": "node web/chat-server.js", "start:demo-chat": "node web/chat-server.js",
"start:tele": "python agent-py-bot/agent.py" "start:tele": "python agent-py-bot/agent.py"
}, },
"env": { "env": {
"NODE_ENV": "demo" "NODE_ENV": "demo"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^5.16.1",
"axios": "^1.7.2", "axios": "^1.7.2",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",

View File

@ -14,10 +14,19 @@ TTS_API_URL=https://api.tts.d-popov.com/asr
LLN_MODEL=qwen2 LLN_MODEL=qwen2
LNN_API_URL=https://ollama.d-popov.com/api/generate LNN_API_URL=https://ollama.d-popov.com/api/generate
GROQ_API_KEY=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 # GROQ_API_KEY=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
OPENAI_API_KEY=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 # OPENAI_API_KEY=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
WS_URL=wss://tts.d-popov.com # WS_URL=wss://tts.d-popov.com
PUBLIC_HOSTNAME=tts.d-popov.com # PUBLIC_HOSTNAME=tts.d-popov.com
SERVER_PORT_WS=8080 # SERVER_PORT_WS=8080
SERVER_PORT_HTTP=8080 # SERVER_PORT_HTTP=8080
# This was inserted by `prisma init`:
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

3
web/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
# Keep environment variables out of version control
.env

View File

@ -1,615 +1,354 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<title>Real-time Voice Chat</title> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css"> <title>Voice Chat Messenger</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
</head> </head>
<body class="bg-gray-100"> <body class="bg-gray-100">
<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-3xl font-bold mb-8 text-center text-blue-600">Voice Chat Messenger</h1>
<div class="flex justify-center items-center mb-4"> <!-- Login/Register Form -->
<!-- Username Input --> <div id="auth-container" class="max-w-md mx-auto bg-white p-6 rounded-lg shadow-md mb-8">
<input type="text" id="username" class="border rounded p-2 mr-4" placeholder="Enter your username"> <h2 class="text-2xl font-semibold mb-4">Login or Register</h2>
<div id="join-container" class="hidden"> <input type="text" id="username" class="w-full border rounded p-2 mb-4" placeholder="Username">
<button id="btn-join" onclick="logInAndStoreSession()" <input type="password" id="password" class="w-full border rounded p-2 mb-4" placeholder="Password">
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Join Chat</button> <div class="flex justify-between">
<select id="language-select"> <button onclick="login()"
<option value="auto">Auto</option> class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Login</button>
<option value="en">English</option> <button onclick="register()"
<option value="bg">Български</option> class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">Register</button>
<option value="fr">Français</option> </div>
</select>
</div> </div>
<!-- Main Chat Interface (initially hidden) -->
<!-- Clear Session Option --> <div id="chat-interface" class="hidden">
<button id="btn-disconnect" onclick="clearSession()" <div class="flex">
class="hidden bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">Clear Session</button> <!-- Sidebar -->
<div class="w-1/4 bg-white rounded-lg shadow-md mr-4 p-4">
<h2 class="text-xl font-semibold mb-4">Chats</h2>
<div id="chat-list" class="space-y-2">
<!-- Chat list items will be inserted here -->
</div> </div>
<button onclick="showNewChatModal()"
<!-- Active Users List --> class="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded w-full">New
<div id="active-users-container" class="hidden flex justify-center items-center mb-4">
<div class="w-1/3">
<h2 class="text-xl font-bold mb-2">Active Users</h2>
<select id="users-list" class="w-full bg-white p-4 rounded shadow" multiple size="10">
<!-- Dynamic list of users -->
</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> Chat</button>
</div> </div>
</div>
<div id="previous-chats-container" class="hidden w-2/3 mx-auto"> <!-- Main Chat Area -->
<h2 class="text-xl font-bold mb-2">Previous Chats</h2> <div class="w-3/4 bg-white rounded-lg shadow-md p-4">
<div id="previous-chats" class="bg-white p-4 rounded shadow"> <div id="current-chat-info" class="mb-4">
<!-- Previous chats content --> <h2 class="text-xl font-semibold">Current Chat</h2>
<div id="chat-participants" class="text-sm text-gray-600"></div>
</div> </div>
<div id="messages" class="h-96 overflow-y-auto mb-4 p-2 border rounded">
<!-- Messages will be inserted here -->
</div> </div>
<div class="flex items-center">
<!-- Chat Room --> <button id="record-button"
<div id="chat-room-container" class="hidden w-2/3 mx-auto"> class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-2">Push to
<h2 class="text-xl font-bold mb-2">Chat Room</h2> Talk</button>
<div id="chat-room" class="bg-white p-4 rounded shadow mb-4"> <div id="status-recording" class="text-sm"></div>
<!-- Chat room content -->
<div>
<div id="chat-room-users" class="flex flex-wrap mb-4">
<!-- Participants list -->
</div> </div>
</div> <div class="mt-2">
<div class="mb-4"> <label class="inline-flex items-center">
<label class="flex items-center space-x-2"> <input type="checkbox" id="autosend" class="form-checkbox">
<input type="checkbox" id="autosend" class="mr-2"> <span class="ml-2">Continuous Mode</span>
<span>Continuous</span>
</label> </label>
</div> </div>
<div class="mb-4">
<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>
</div>
<div id="status-recording" class="flex justify-center items-center mb-4">
</div>
<div id="transcription" class="border rounded p-4 h-48 overflow-y-scroll mb-4">
<!-- Transcription content -->
</div>
<canvas id="canvas" class="w-full mb-4"></canvas>
<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="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>
</div> </div>
<!-- Connection Status and Info --> <!-- New Chat Modal (initially hidden) -->
<div class="flex justify-center items-center mb-4"> <div id="new-chat-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full">
<div id="connection-status" class="mr-4"></div> <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<h3 class="text-lg font-semibold mb-4">Start a New Chat</h3>
<select id="users-list" class="w-full p-2 border rounded mb-4" multiple>
<!-- User options will be inserted here -->
</select>
<div class="flex justify-end">
<button onclick="startNewChat()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-2">Start
Chat</button>
<button onclick="hideNewChatModal()"
class="bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 rounded">Cancel</button>
</div> </div>
<div class="flex justify-center items-center mb-4">
<div id="info"></div>
</div> </div>
<div id="status-recording" class="flex justify-center items-center mb-4"> status</div>
</div> </div>
<!-- Connection Status -->
<div id="connection-status" class="fixed bottom-4 right-4 bg-gray-800 text-white p-2 rounded"></div>
</div>
<script>
// Declare these variables and functions in the global scope
var socket;
var sessionId;
var currentUser;
var users = [];
var chats = [];
var currentChatId;
<script type="module"> // Make these functions global
// import * as audio from './audio.js'; window.login = function() {
const username = document.getElementById('username').value;
let socket; const password = document.getElementById('password').value;
let sessionId; fetch('/login', {
let username;
let users = [];
let selectedUsers = [];
let chats = [];
let recordButton;
let connectionStatus;
let statusRecording;
let connected = false;
document.getElementById('autosend').addEventListener('change', (event) => {
const autosend = event.target.checked;
fetch('/settings', {
method: 'POST', method: 'POST',
body: JSON.stringify({ autosend, sessionId }),
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin' body: JSON.stringify({ username, password })
}); })
.then(response => response.json())
.then(data => {
if (data.sessionId) {
sessionId = data.sessionId;
currentUser = { id: data.userId, username };
showChatInterface();
connect();
} else {
alert('Login failed. Please try again.');
}
}); });
};
window.register = function() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
fetch('/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
.then(response => response.json())
.then(data => {
if (data.userId) {
alert('Registration successful. Please login.');
} else {
alert('Registration failed. Please try again.');
}
});
};
window.showNewChatModal = function() {
document.getElementById('new-chat-modal').classList.remove('hidden');
};
window.hideNewChatModal = function() {
document.getElementById('new-chat-modal').classList.add('hidden');
};
window.startNewChat = function() {
const selectedUsers = Array.from(document.getElementById('users-list').selectedOptions).map(option => option.value);
if (selectedUsers.length > 0) {
socket.send(JSON.stringify({ type: 'startChat', users: selectedUsers }));
hideNewChatModal();
} else {
alert('Please select at least one user to start a chat.');
}
};
// Connect to WebSocket
function connect() { function connect() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connectionStatus.innerHTML = "Connecting to WS..."; const connectionStatus = document.getElementById("connection-status");
let wsurl = "ws://localhost:8080"; connectionStatus.textContent = "Connecting...";
fetch("/wsurl") fetch("/wsurl")
.then((response) => response.text()) .then(response => response.text())
.then((data) => { .then(wsurl => {
wsurl = data;
console.log("Got ws url: '" + wsurl + "'");
})
.then(() => {
socket = new WebSocket(wsurl); socket = new WebSocket(wsurl);
// audio.setSocket(socket); // Set the socket in the audio module
socket.onopen = () => { socket.onopen = () => {
connectionStatus.innerHTML = "Connected to " + wsurl; connectionStatus.textContent = "Connected";
recordButton.disabled = false;
connected = true;
//if we stored a session id in a cookie, reconnect
const sessionId = getCookie("sessionId");
if (sessionId) {
socket.send(JSON.stringify({ type: 'reconnect', sessionId }));
}
else {
socket.send(JSON.stringify({ type: 'sessionId' }));
}
resolve(socket); resolve(socket);
}; };
socket.onmessage = onmessage; socket.onmessage = handleMessage;
socket.onclose = () => { socket.onclose = () => {
connectionStatus.innerHTML = "Disconnected"; connectionStatus.textContent = "Disconnected";
recordButton.disabled = true; setTimeout(() => connect().then(resolve).catch(reject), 5000);
connected = false;
setTimeout(() => {
connect().then(resolve).catch(reject);
}, 5000);
}; };
}) })
.catch((error) => { .catch(error => {
connectionStatus.innerHTML = "Error getting ws url: " + error; connectionStatus.textContent = "Connection error";
reject(error); reject(error);
}); });
}); });
};
function onmessage(event) {
try {
let json = JSON.parse(event.data);
switch (json.type) {
case "sessionId":
sessionId = json.sessionId;
//set the session id in the cookie
document.cookie = `sessionId=${sessionId}; path=/;`;
console.log("Got session id: " + sessionId);
break;
case "languageDetected":
statusRecording.innerHTML = "Detected language: " + json.languageDetected;
break;
case "text":
case "transcriptionResult":
transcription.innerHTML += "<br />" + json.text;
let latency = Date.now() - serverTime;
if (autosend.checked) {
// const arr = event.data.split(/[(\)]/);
// let queue = arr[1];
// let text = arr[2].trim();
// info.innerHTML = "latency: " + latency + "ms; server queue: " + queue + " requests";
//transcription.value += event.data + " ";
statusRecording.innerHTML = "Listening...";
statusRecording.style.color = "black";
} else {
//transcription.innerHTML = event.data;
} }
break;
case 'audio':
const audioBuffer = Uint8Array.from(atob(json.audio), char => char.charCodeAt(0));
const audioBlob = new Blob([audioBuffer], { type: 'audio/mp3' });
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.play();
break;
case "userList": function handleMessage(event) {
users = json.users; const data = JSON.parse(event.data);
switch (data.type) {
case 'sessionId':
sessionId = data.sessionId;
break;
case 'userList':
users = data.users;
updateUserList(); updateUserList();
break; break;
case "chats": case 'chats':
chats = json.chats; chats = data.chats;
updateChatList(); updateChatList();
break; break;
case "chat": case 'chat':
displayChatParticipants(json.chat.id, json.chat.participants); displayChat(data.chat);
break;
case 'text':
case 'transcriptionResult':
addMessage(data.text);
break;
case 'audio':
playAudio(data.audio);
break; break;
default:
console.log("Unknown message type:", json.type);
}
} catch (e) {
console.error("Failed to parse message", e);
}
}
function logInAndStoreSession() {
username = document.getElementById('username').value;
if (username.trim() === "") {
alert("Please enter a username");
return;
}
if (!socket || socket.readyState !== WebSocket.OPEN) {
connect().then(() => {
userJoin(sessionId, username, document.getElementById('language-select').value);
});
} else {
userJoin(sessionId, username, document.getElementById('language-select').value);
} }
} }
function showChatInterface() {
function userJoin(sessionId, username, language) { document.getElementById('auth-container').classList.add('hidden');
socket.send(JSON.stringify({ type: 'join', username, language })); document.getElementById('chat-interface').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((s) => {
s.send(JSON.stringify({ type: 'reconnect', sessionId }));
});
}
document.getElementById('btn-disconnect').classList.remove('hidden');
document.getElementById('join-container').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('join-container').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() {
const usersList = document.getElementById('users-list'); const usersList = document.getElementById('users-list');
usersList.innerHTML = ''; usersList.innerHTML = '';
users.forEach(user => { users.forEach(user => {
if (user.id !== currentUser.id) {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = user.sessionId; option.value = user.id;
option.innerText = "[" + user.language + "] " + user.username; option.textContent = user.username;
if (user.username === username) {
option.innerText += " (me)";
}
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');
}
function fetchPreviousChats(username) {
fetch(`/chats?username=${username}`)
.then(response => response.json())
.then(data => {
chats = data.chats;
updateChatList();
});
}
function updateChatList() { function updateChatList() {
const previousChats = document.getElementById('previous-chats'); const chatList = document.getElementById('chat-list');
previousChats.innerHTML = ''; chatList.innerHTML = '';
chats.forEach(chat => { chats.forEach(chat => {
const chatDiv = document.createElement('div'); const chatItem = document.createElement('div');
chatDiv.classList.add('border', 'rounded', 'p-2', 'mb-2', 'cursor-pointer'); chatItem.className = 'p-2 hover:bg-gray-100 cursor-pointer';
chatDiv.setAttribute('data-chat-id', chat.id); // Store chat ID in data attribute chatItem.textContent = chat.participants.map(p => p.username).join(', ');
chatItem.onclick = () => selectChat(chat.id);
const participants = chat.participants.join(', '); chatList.appendChild(chatItem);
const status = chat.participants.map(participant => {
const user = users.find(u => u.username === participant);
return user ? `${participant} (online)` : `${participant} (offline)`;
}).join(', ');
chatDiv.innerHTML = `${status}`;
chatDiv.addEventListener('click', () => {
// Remove highlight from all chat divs
document.querySelectorAll('#previous-chats > div').forEach(div => {
div.classList.remove('bg-blue-100');
});
// Highlight selected chat div
chatDiv.classList.add('bg-blue-100');
selectChatRoom(chat.id);
});
previousChats.appendChild(chatDiv);
}); });
} }
function selectChat(chatId) {
function selectChatRoom(chatId) { currentChatId = chatId;
const chat = chats.find(c => c.id === chatId);
if (!chat) return;
const chatRoomUsers = document.getElementById('chat-room-users');
chatRoomUsers.innerHTML = ''; // Clear existing content
socket.send(JSON.stringify({ type: 'enterChat', chatId })); socket.send(JSON.stringify({ type: 'enterChat', chatId }));
document.getElementById('chat-room-container').classList.remove('hidden');
// displayChatParticipants(chatId, chat.participants);
} }
function displayChat(chat) {
function displayChatParticipants(chatId, participants) { const messagesContainer = document.getElementById('messages');
const chatRoomUsers = document.getElementById('chat-room-users'); messagesContainer.innerHTML = '';
let participantsHtml = '<div class="flex flex-wrap mb-4">'; chat.messages.forEach(addMessage);
participants.forEach(participantId => { const participantsContainer = document.getElementById('chat-participants');
const user = users.find(u => u.sessionId === participantId); participantsContainer.textContent = Participants: ${ chat.participants.map(p => p.username).join(', ') };
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>';
chatRoomUsers.innerHTML = participantsHtml;
} }
function addMessage(message) {
// REGION AUDIO RECORDING const messagesContainer = document.getElementById('messages');
let selectedDeviceId = "default"; const messageElement = document.createElement('div');
export let serverTime; messageElement.className = 'mb-2';
// export let recordButton; messageElement.textContent = ${ message.sender }: ${ message.text };
// export let socket; messagesContainer.appendChild(messageElement);
// let connectionStatus; messagesContainer.scrollTop = messagesContainer.scrollHeight;
// let statusRecording;
let audioRecorder;
let audioStream;
let recording = false;
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 = [];
function setSocket(newSocket) {
socket = newSocket;
} }
export function setRecordButton(newRecordButton) { function playAudio(audioBase64) {
recordButton = newRecordButton; const audio = new Audio(data: audio / mp3; base64, ${ audioBase64 });
recordButton.addEventListener("click", toggleListening); audio.play();
} }
// Audio recording logic
let mediaRecorder;
let audioChunks = [];
document.getElementById('record-button').addEventListener('mousedown', startRecording);
document.getElementById('record-button').addEventListener('mouseup', stopRecording);
document.getElementById('record-button').addEventListener('mouseleave', stopRecording);
function startRecording() {
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
mediaRecorder = new MediaRecorder(stream);
export function InitAudioAnalyser(stream) { mediaRecorder.ondataavailable = event => {
audioContext = new AudioContext(); audioChunks.push(event.data);
const source = audioContext.createMediaStreamSource(stream); };
analyser = audioContext.createAnalyser(); mediaRecorder.onstop = sendAudioToServer;
analyser.fftSize = 2048; mediaRecorder.start();
analyser.smoothingTimeConstant = 0.8; document.getElementById('status-recording').textContent = 'Recording...';
source.connect(analyser);
}
export function startListening() {
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');
}
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 sendAudioToServerJson(data) { function stopRecording() {
if (socket && socket.readyState === WebSocket.OPEN) { if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
const binaryData = Buffer.from(base64AudioData, 'base64'); document.getElementById('status-recording').textContent = 'Processing...';
socket.send(JSON.stringify({ type: 'audio', audiobase64: binaryData }));
serverTime = Date.now();
if (!autosend.checked) {
transcription.placeholder = "Processing audio...";
}
}
}
export function sendAudioToServer(data) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(data);
serverTime = Date.now();
if (!autosend.checked) {
transcription.placeholder = "Processing audio...";
}
} }
} }
export function toggleListening() { function sendAudioToServer() {
if (socket.readyState === WebSocket.OPEN) { const audioBlob = new Blob(audioChunks, { type: 'audio/ogg; codecs=opus' });
if (recording) { audioChunks = [];
stopListening();
const reader = new FileReader();
reader.onloadend = () => {
const base64Audio = reader.result.split(',')[1];
socket.send(JSON.stringify({
type: 'audio',
chatId: currentChatId,
audio: base64Audio
}));
};
reader.readAsDataURL(audioBlob);
}
// Initialize the app
document.addEventListener('DOMContentLoaded', () => {
const storedSessionId = localStorage.getItem('sessionId');
if (storedSessionId) {
sessionId = storedSessionId;
showChatInterface();
connect();
}
});
// Continuous mode toggle
document.getElementById('autosend').addEventListener('change', (event) => {
const autosend = event.target.checked;
if (autosend) {
startContinuousRecording();
} else { } else {
startListening(); stopContinuousRecording();
} }
});
let continuousRecorder;
function startContinuousRecording() {
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
continuousRecorder = new MediaRecorder(stream);
continuousRecorder.ondataavailable = event => {
sendAudioToServer();
};
continuousRecorder.start(1000); // Send audio every second
document.getElementById('status-recording').textContent = 'Continuous mode active';
});
}
function stopContinuousRecording() {
if (continuousRecorder && continuousRecorder.state !== 'inactive') {
continuousRecorder.stop();
document.getElementById('status-recording').textContent = '';
} }
} }
export function initializeVolumeChecker() { // Logout function
volumeChecker = setInterval(() => { function logout() {
if (!audioContext) { localStorage.removeItem('sessionId');
console.log("No audio context"); location.reload();
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; // Add logout button to the UI
const isSilent = averageVolume < threshold; const logoutButton = document.createElement('button');
logoutButton.textContent = 'Logout';
if (averageVolume > threshold) { logoutButton.className = 'bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded mt-4';
if (autosend.checked && speakingCount == 0 && audioRecorder) { logoutButton.onclick = logout;
soundDetected = false; document.querySelector('.container').appendChild(logoutButton);
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);
}
// Expose functions to global scope
window.logInAndStoreSession = logInAndStoreSession;
window.clearSession = clearSession;
window.copyToClipboard = copyToClipboard;
window.clearTranscription = clearTranscription;
window.startChat = startChat;
</script> </script>
<script src="https://cdn.webrtc-experiment.com/MediaStreamRecorder.js"></script>
</body> </body>
</html> </html>

View File

@ -11,6 +11,9 @@ const axios = require('axios');
const OpenAI = require('openai'); const OpenAI = require('openai');
const Groq = require('groq-sdk'); const Groq = require('groq-sdk');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// Load environment variables // Load environment variables
dotenv.config({ path: `.env${process.env.NODE_ENV === 'development' ? '.development' :'.'+ process.env.NODE_ENV }` }); dotenv.config({ path: `.env${process.env.NODE_ENV === 'development' ? '.development' :'.'+ process.env.NODE_ENV }` });
console.log(`loaded env file: ${process.env.NODE_ENV}`) console.log(`loaded env file: ${process.env.NODE_ENV}`)
@ -37,20 +40,36 @@ let queueCounter = 0;
const sessions = new Map(); const sessions = new Map();
const chats = new Map(); // Store chat rooms const chats = new Map(); // Store chat rooms
// Initialize storage and load initial values
async function initStorage() {
await storage.init();
language = await storage.getItem('language') || language;
storeRecordings = await storage.getItem('storeRecordings') || storeRecordings;
const storedChats = await storage.getItem('chats') || [];
storedChats.forEach(chat => chats.set(chat.id, chat));
const storedSessions = await storage.getItem('sessions') || []; // User registration
storedSessions.forEach(session => sessions.set(session.sessionId, session)); app.post('/register', async (req, res) => {
} const { username, password } = req.body;
try {
const user = await prisma.user.create({
data: { username, password: hashPassword(password) },
});
res.status(201).json({ message: 'User registered successfully', userId: user.id });
} catch (error) {
res.status(400).json({ error: 'Username already exists' });
}
});
// User login
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await prisma.user.findUnique({ where: { username } });
if (user && verifyPassword(password, user.password)) {
const session = await prisma.session.create({
data: { userId: user.id, lastLogin: new Date() },
});
res.json({ sessionId: session.id, userId: user.id });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
initStorage();
// WebSocket Server // WebSocket Server
const wss = new WebSocket.Server({ port: PORT_WS }); const wss = new WebSocket.Server({ port: PORT_WS });
@ -105,33 +124,55 @@ async function handleSessionId(ws) {
await storage.setItem('sessions', Array.from(sessions.values())); await storage.setItem('sessions', Array.from(sessions.values()));
} }
async function handleJoin(ws, { username, language }) { // Modified handleJoin function
sessions.set(ws.sessionId, { username, sessionId: ws.sessionId, language }); async function handleJoin(ws, { username, language, sessionId }) {
ws.send(JSON.stringify({ type: 'sessionId', sessionId: ws.sessionId, language, storeRecordings })); const session = await prisma.session.update({
where: { id: sessionId },
data: { language },
include: { user: true },
});
ws.sessionId = sessionId;
ws.userId = session.userId;
ws.send(JSON.stringify({ type: 'sessionId', sessionId, language, storeRecordings }));
const userChats = Array.from(chats.values()).filter(chat => chat.participants.includes(ws.sessionId)); const userChats = await prisma.chat.findMany({
where: { participants: { some: { userId: session.userId } } },
include: { participants: true },
});
ws.send(JSON.stringify({ type: 'chats', chats: userChats })); ws.send(JSON.stringify({ type: 'chats', chats: userChats }));
broadcastUserList(); broadcastUserList();
} }
async function handleStartChat(ws, { users }) { async function handleStartChat(ws, { users }) {
const chatId = generateChatId(); const chatId = generateChatId();
let participants = [ws.sessionId, ...users]; let participants = [ws.userId, ...users];
participants = [...new Set(participants)]; participants = [...new Set(participants)];
chats.set(chatId, { participants, messages: [] }); const chat = await prisma.chat.create({
await storage.setItem('chats', Array.from(chats.values())); data: {
id: chatId,
participants: {
connect: participants.map(userId => ({ id: userId })),
},
},
include: { participants: true },
});
notifyParticipants(participants); notifyParticipants(participants);
broadcastUserList(); broadcastUserList();
} }
async function handleEnterChat(ws, { chatId }) { async function handleEnterChat(ws, { chatId }) {
const enteredChat = chats.get(chatId); const enteredChat = await prisma.chat.findUnique({
const currentSession = sessions.get(ws.sessionId); where: { id: chatId },
currentSession.currentChat = chatId; include: { participants: true, messages: true },
if (enteredChat && enteredChat.participants.includes(ws.sessionId)) { });
if (enteredChat && enteredChat.participants.some(p => p.id === ws.userId)) {
await prisma.session.update({
where: { id: ws.sessionId },
data: { currentChatId: chatId },
});
ws.send(JSON.stringify({ type: 'chat', chat: enteredChat })); ws.send(JSON.stringify({ type: 'chat', chat: enteredChat }));
} }
} }
@ -158,12 +199,15 @@ function generateChatId() {
return Math.random().toString(36).substring(2); return Math.random().toString(36).substring(2);
} }
function broadcastUserList() { async function broadcastUserList() {
const userList = Array.from(sessions.values()).map(user => ({ const users = await prisma.session.findMany({
username: user.username, include: { user: true },
sessionId: user.sessionId, });
currentChat: user.currentChat, const userList = users.map(session => ({
language: user.language username: session.user.username,
sessionId: session.id,
currentChat: session.currentChatId,
language: session.language,
})); }));
wss.clients.forEach(client => { wss.clients.forEach(client => {
@ -171,7 +215,7 @@ function broadcastUserList() {
client.send(JSON.stringify({ type: 'userList', users: userList })); client.send(JSON.stringify({ type: 'userList', users: userList }));
} }
}); });
} }
function notifyParticipants(participants) { function notifyParticipants(participants) {
participants.forEach(sessionId => { participants.forEach(sessionId => {

40
web/prisma/schema.prisma Normal file
View File

@ -0,0 +1,40 @@
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(uuid())
username String @unique
password String
sessions Session[]
chats Chat[]
}
model Session {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
language String?
lastLogin DateTime
currentChatId String?
}
model Chat {
id String @id
participants User[]
messages Message[]
}
model Message {
id String @id @default(uuid())
chatId String
chat Chat @relation(fields: [chatId], references: [id])
senderId String
content String
timestamp DateTime @default(now())
}