diff --git a/.vscode/launch.json b/.vscode/launch.json index a4ddc9f..7c011b9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -27,28 +27,35 @@ } }, { - "name": "node: Launch server.js", + "name": "Launch chat-server.js", "type": "node", "request": "launch", - "program": "conda activate node && ${workspaceFolder}/web/server.js", + "program": "${workspaceFolder}/web/chat-server.js", "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "env": { + "CONDA_ENV": "node", //? + "NODE_ENV": "development" + }, "skipFiles": [ "/**" ] }, { - "name": "conda task: Launch server.js", + "name": "Launch server.js", "type": "node", "request": "launch", + // "program": "conda activate node && ${workspaceFolder}/web/server.js", "program": "${workspaceFolder}/web/server.js", - // "preLaunchTask": "conda-activate", "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "env": { "CONDA_ENV": "node", //? - //set env to dev "NODE_ENV": "development" - } + }, + "skipFiles": [ + "/**" + ] }, { "name": "Python Debugger: Python File", diff --git a/web/chat-client.html b/web/chat-client.html new file mode 100644 index 0000000..ec5b768 --- /dev/null +++ b/web/chat-client.html @@ -0,0 +1,382 @@ + + + + + Real-time Voice Chat + + + + + +
+

Real-time Voice Chat

+ + +
+ + +
+ + +
+
+

Active Users

+
    + +
+
+
+

Chat Room

+
+ +
+ +
+
+ +
+
+ +
+ +
+ + +
+
+
+
+ + +
+
+
+
+
+
+
+
+ + + + + + diff --git a/web/chat-server.js b/web/chat-server.js new file mode 100644 index 0000000..fb4b04c --- /dev/null +++ b/web/chat-server.js @@ -0,0 +1,192 @@ +// server.js +if (require('dotenv')) { + const envFile = process.env.NODE_ENV === 'development' ? '.env.development' : '.env'; + require('dotenv').config({ path: envFile }); +} +const express = require('express'); +const bodyParser = require('body-parser'); +const WebSocket = require('ws'); +const storage = require('node-persist'); +const request = require('request'); +const fs = require('fs'); +const path = require('path'); + +const app = express(); +app.use(bodyParser.json()); + +const PORT_HTTP = process.env.SERVER_PORT_HTTP || 3000; +const PORT_WS = process.env.SERVER_PORT_WS || 8080; +const TTS_API_URL = process.env.TTS_API_URL; + +let language = "en"; +let storeRecordings = false; +let queueCounter = 0; + +const sessions = new Map(); +const users = new Map(); // Store users with their usernames and session IDs + +storage.init().then(() => { + storage.getItem('language').then((value) => { + if (value !== undefined) language = value; + else storage.setItem('language', language); + }); + storage.getItem('storeRecordings').then((value) => { + if (value !== undefined) storeRecordings = value; + else storage.setItem('storeRecordings', storeRecordings); + }); +}); + +// WebSocket Server +const wss = new WebSocket.Server({ port: PORT_WS }); +wss.on('connection', (ws) => { + ws.sessionId = Math.random().toString(36).substring(2); + sessions.set(ws.sessionId, { language: 'en' }); + + ws.send(JSON.stringify({ sessionId: ws.sessionId, language, storeRecordings })); + + ws.on('message', (message) => { + try { + const data = JSON.parse(message); + + if (data.type === 'join') { + const { username } = data; + users.set(ws.sessionId, { username, sessionId: ws.sessionId }); + broadcastUserList(); + } else if (data.type === 'audio') { + handleAudioData(ws, data.audio); + } + } catch (err) { + console.error('Failed to parse message', err); + } + }); + + ws.on('close', () => { + users.delete(ws.sessionId); + sessions.delete(ws.sessionId); + broadcastUserList(); + }); +}); + +function handleAudioData(ws, data) { + const sessionData = sessions.get(ws.sessionId); + let language = sessionData.language || 'en'; + let task = sessionData.task || 'transcribe'; + + const formData = { + task, + language, + vad_filter: 'true', + output: 'json', + audio_file: { + value: data, + options: { filename: 'audio.ogg', contentType: 'audio/ogg' } + } + }; + + if (language === 'auto' || language === '') { + detectLanguage(ws, formData); + } else { + transcribeAudio(ws, formData, sessionData); + } +} + +function detectLanguage(ws, formData) { + request.post({ url: TTS_API_URL.replace('/asr', '/detect-language'), formData }, (err, httpResponse, body) => { + if (err) return console.error('Language detection failed:', err); + const result = JSON.parse(body); + if (result && result.language_code) { + const language = result.language_code; + const sessionData = sessions.get(ws.sessionId); + sessionData.language = language; + ws.send(JSON.stringify({ languageDetected: result.detected_language })); + transcribeAudio(ws, formData, sessionData); + } + }); +} + +function transcribeAudio(ws, formData, sessionData) { + const start = new Date().getTime(); + queueCounter++; + + request.post({ url: TTS_API_URL, formData }, (err, httpResponse, body) => { + queueCounter--; + if (err) return console.error('Transcription failed:', err); + + const duration = new Date().getTime() - start; + ws.send(JSON.stringify({ + queueCounter, + duration, + language: sessionData.language, + text: body + })); + }); + + if (storeRecordings) { + const timestamp = Date.now(); + fs.mkdir('rec', { recursive: true }, (err) => { + if (err) throw err; + }); + fs.writeFile(`rec/audio${timestamp}.ogg`, formData.audio_file.value, (err) => { + if (err) console.log(err); + else console.log('Audio data saved to rec/audio' + timestamp + '.ogg'); + }); + } +} + +function broadcastUserList() { + const userList = Array.from(users.values()).map(user => ({ username: user.username, sessionId: user.sessionId })); + wss.clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify({ type: 'userList', users: userList })); + } + }); +} + +// HTTP Server +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, 'chat-client.html')); +}); + +app.post('/log', (req, res) => { + console.log(`[LOG ${new Date().toISOString()}] ${req.body.message}`); + res.status(200).send('OK'); +}); + +app.get('/wsurl', (req, res) => { + res.status(200).send(process.env.WS_URL); +}); + +app.get('/settings', (req, res) => { + if (req.query.language) { + language = req.query.language; + storage.setItem('language', language); + } + if (req.query.storeRecordings) { + storeRecordings = req.query.storeRecordings; + storage.setItem('storeRecordings', storeRecordings); + } + res.status(200).send({ language, storeRecordings }); +}); + +app.post('/settings', (req, res) => { + const { sessionId, language, storeRecordings, task } = req.body; + const sessionData = sessions.get(sessionId); + if (language) sessionData.language = language; + if (storeRecordings) sessionData.storeRecordings = storeRecordings; + if (task) sessionData.task = task; + res.status(200).send('OK'); +}); + +app.post('/upload', (req, res) => { + const timestamp = Date.now(); + fs.mkdir('rec', { recursive: true }, (err) => { + if (err) return res.status(500).send('ERROR'); + const file = fs.createWriteStream(`rec/audio_slice_${timestamp}.ogg`); + req.pipe(file); + file.on('finish', () => res.status(200).send('OK')); + }); +}); + +app.listen(PORT_HTTP, () => { + console.log(`Server listening on port ${PORT_HTTP}`); +});