#!/usr/bin/env node /** * Multi-User Transcription Server (Node.js) * * Much better than PHP for real-time applications: * - Native WebSocket support * - No buffering issues * - Better for long-lived connections * - Lower resource usage * * Install: npm install express ws body-parser * Run: node server.js */ const express = require('express'); const WebSocket = require('ws'); const http = require('http'); const bodyParser = require('body-parser'); const fs = require('fs').promises; const path = require('path'); const crypto = require('crypto'); const app = express(); const server = http.createServer(app); const wss = new WebSocket.Server({ server }); // Configuration const PORT = process.env.PORT || 3000; const DATA_DIR = path.join(__dirname, 'data'); const MAX_TRANSCRIPTIONS = 100; const CLEANUP_INTERVAL = 2 * 60 * 60 * 1000; // 2 hours // Middleware app.use(bodyParser.json()); app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); }); // In-memory cache of rooms (reduces file I/O) const rooms = new Map(); // Track WebSocket connections by room const roomConnections = new Map(); // Ensure data directory exists async function ensureDataDir() { try { await fs.mkdir(DATA_DIR, { recursive: true }); } catch (err) { console.error('Error creating data directory:', err); } } // Get room file path function getRoomFile(room) { const hash = crypto.createHash('md5').update(room).digest('hex'); return path.join(DATA_DIR, `room_${hash}.json`); } // Load room data async function loadRoom(room) { if (rooms.has(room)) { return rooms.get(room); } const file = getRoomFile(room); try { const data = await fs.readFile(file, 'utf8'); const roomData = JSON.parse(data); rooms.set(room, roomData); return roomData; } catch (err) { return null; } } // Save room data async function saveRoom(room, roomData) { rooms.set(room, roomData); const file = getRoomFile(room); await fs.writeFile(file, JSON.stringify(roomData, null, 2)); } // Verify passphrase async function verifyPassphrase(room, passphrase) { let roomData = await loadRoom(room); // If room doesn't exist, create it if (!roomData) { const bcrypt = require('bcrypt'); const hash = await bcrypt.hash(passphrase, 10); roomData = { passphrase_hash: hash, created_at: Date.now(), last_activity: Date.now(), transcriptions: [] }; await saveRoom(room, roomData); return true; } // Verify passphrase const bcrypt = require('bcrypt'); return await bcrypt.compare(passphrase, roomData.passphrase_hash); } // Add transcription async function addTranscription(room, transcription) { let roomData = await loadRoom(room); if (!roomData) { throw new Error('Room not found'); } roomData.transcriptions.push(transcription); // Limit transcriptions if (roomData.transcriptions.length > MAX_TRANSCRIPTIONS) { roomData.transcriptions = roomData.transcriptions.slice(-MAX_TRANSCRIPTIONS); } roomData.last_activity = Date.now(); await saveRoom(room, roomData); // Broadcast to all connected clients in this room broadcastToRoom(room, transcription); } // Broadcast to all clients in a room function broadcastToRoom(room, data) { const connections = roomConnections.get(room) || new Set(); const message = JSON.stringify(data); connections.forEach(ws => { if (ws.readyState === WebSocket.OPEN) { ws.send(message); } }); } // Cleanup old rooms async function cleanupOldRooms() { const now = Date.now(); const files = await fs.readdir(DATA_DIR); for (const file of files) { if (!file.startsWith('room_') || !file.endsWith('.json')) { continue; } const filepath = path.join(DATA_DIR, file); try { const data = JSON.parse(await fs.readFile(filepath, 'utf8')); const lastActivity = data.last_activity || data.created_at || 0; if (now - lastActivity > CLEANUP_INTERVAL) { await fs.unlink(filepath); console.log(`Cleaned up old room: ${file}`); } } catch (err) { console.error(`Error processing ${file}:`, err); } } } // Routes // Server info / landing page app.get('/', (req, res) => { res.send(` Local Transcription Multi-User Server

🎤 Local Transcription

Multi-User Server (Node.js)

đŸŸĸ Server Running

🚀 Quick Start

Generate a unique room with random credentials:

📡 API Endpoints

POST /api/send
Send a transcription to a room
GET /api/list?room=ROOM
List recent transcriptions from a room
WS /ws?room=ROOM
WebSocket connection for real-time updates
GET /display?room=ROOM
Web display page for OBS

🔗 Quick Links

💡 Example: Send a Transcription

Try this curl command to send a test message:

curl -X POST "http://${req.headers.host}/api/send" \\
  -H "Content-Type: application/json" \\
  -d '{
    "room": "demo",
    "passphrase": "demopass",
    "user_name": "TestUser",
    "text": "Hello from the API!",
    "timestamp": "12:34:56"
  }'

Then view it at: /display?room=demo

â„šī¸ Server Information

Node.js
Runtime
v1.0.0
Version
<100ms
Latency
WebSocket
Protocol
`); }); // Send transcription app.post('/api/send', async (req, res) => { try { const { room, passphrase, user_name, text, timestamp } = req.body; if (!room || !passphrase || !user_name || !text) { return res.status(400).json({ error: 'Missing required fields' }); } // Verify passphrase const valid = await verifyPassphrase(room, passphrase); if (!valid) { return res.status(401).json({ error: 'Invalid passphrase' }); } // Create transcription const transcription = { user_name: user_name.trim(), text: text.trim(), timestamp: timestamp || new Date().toLocaleTimeString('en-US', { hour12: false }), created_at: Date.now() }; await addTranscription(room, transcription); res.json({ status: 'ok', message: 'Transcription added' }); } catch (err) { console.error('Error in /api/send:', err); res.status(500).json({ error: err.message }); } }); // List transcriptions app.get('/api/list', async (req, res) => { try { const { room } = req.query; if (!room) { return res.status(400).json({ error: 'Missing room parameter' }); } const roomData = await loadRoom(room); const transcriptions = roomData ? roomData.transcriptions : []; res.json({ transcriptions }); } catch (err) { console.error('Error in /api/list:', err); res.status(500).json({ error: err.message }); } }); // Serve display page app.get('/display', (req, res) => { const { room = 'default', fade = '10', timestamps = 'true' } = req.query; res.send(` Multi-User Transcription Display
âšĢ Connecting...
`); }); // WebSocket handler wss.on('connection', (ws, req) => { const params = new URLSearchParams(req.url.split('?')[1]); const room = params.get('room') || 'default'; console.log(`WebSocket connected to room: ${room}`); // Add to room connections if (!roomConnections.has(room)) { roomConnections.set(room, new Set()); } roomConnections.get(room).add(ws); ws.on('close', () => { const connections = roomConnections.get(room); if (connections) { connections.delete(ws); if (connections.size === 0) { roomConnections.delete(room); } } console.log(`WebSocket disconnected from room: ${room}`); }); }); // Start server async function start() { await ensureDataDir(); // Run cleanup periodically setInterval(cleanupOldRooms, CLEANUP_INTERVAL); server.listen(PORT, () => { console.log(`✅ Multi-User Transcription Server running on port ${PORT}`); console.log(` Display URL: http://localhost:${PORT}/display?room=YOUR_ROOM`); console.log(` API endpoint: http://localhost:${PORT}/api/send`); }); } start().catch(err => { console.error('Failed to start server:', err); process.exit(1); });