Browse Source

First commit

master
Zolfa 6 days ago
commit
fd4c56a3c7
7 changed files with 2068 additions and 0 deletions
  1. +133
    -0
      .gitignore
  2. +162
    -0
      app.js
  3. +1554
    -0
      package-lock.json
  4. +24
    -0
      package.json
  5. +106
    -0
      public/audience.html
  6. +15
    -0
      public/index.html
  7. +74
    -0
      public/speaker.html

+ 133
- 0
.gitignore View File

@ -0,0 +1,133 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
uploads

+ 162
- 0
app.js View File

@ -0,0 +1,162 @@
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const { OBSWebSocket } = require('obs-websocket-js');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
const obs = new OBSWebSocket();
const OBS_HOST = 'localhost'; // If OBS is running on the same machine
const OBS_PORT = 4455; // Default WebSocket port
const OBS_PASSWORD = 'jBHD5lLvKtHIjIwC'; // Add your WebSocket password here if set
// Set up file storage for uploaded speeches
const upload = multer({ dest: 'uploads/' });
let speechText = [];
let currentPosition = 0;
// File to store speech lines
const SPEECH_LINES_FILE = 'speechLines.txt';
const SRT_FILE_NAME = 'captions.srt';
// Serve static files (HTML views)
app.use(express.static(path.join(__dirname, 'public')));
// Connect to OBS WebSocket
async function connectToOBS() {
try {
await obs.connect(`ws://${OBS_HOST}:${OBS_PORT}`, OBS_PASSWORD);
console.log('Connected to OBS WebSocket');
} catch (error) {
console.error('Failed to connect to OBS WebSocket:', error);
}
}
connectToOBS();
// Load speech lines from file
function loadSpeechLines() {
if (fs.existsSync(SPEECH_LINES_FILE)) {
const fileContents = fs.readFileSync(SPEECH_LINES_FILE, 'utf-8');
speechText = fileContents.split('\n').map(line => line.trim()).filter(line => line.length > 0);
currentPosition = 0; // Reset the position to the start
console.log('Loaded speech lines from file.');
}
}
// Load speech lines when the server starts
loadSpeechLines();
// Serve speaker view
app.get('/speaker', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'speaker.html'));
});
// Serve audience/projector view
app.get('/audience', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'audience.html'));
});
// Endpoint to handle file upload
app.post('/upload', upload.single('speech'), (req, res) => {
const filePath = path.join(process.cwd(), req.file.path);
const fileContents = fs.readFileSync(filePath, 'utf-8');
speechText = fileContents.split('\n').map(line => line.trim()).filter(line => line.length > 0);
currentPosition = 0;
// Save the loaded speech lines to a file
fs.writeFileSync(SPEECH_LINES_FILE, speechText.join('\n'));
io.emit('load_speech', speechText);
res.send('File uploaded and speech loaded.');
});
// Handle WebSocket connections
io.on('connection', (socket) => {
console.log('Client connected');
// Send the full speech to any newly connected client
if (speechText.length > 0) {
socket.emit('load_speech', speechText);
}
// Handle position updates from the speaker view
socket.on('update_position', (newPosition) => {
currentPosition = newPosition;
io.emit('update_caption', currentPosition); // Send to all clients
const currentLine = speechText[currentPosition];
updateOBSSubtitle(currentLine);
generateSRT(currentLine);
});
});
async function updateOBSSubtitle(captionText) {
try {
await obs.call('SendStreamCaption', {
captionText: captionText
});
console.log('Updated OBS Text Source:', captionText);
} catch (error) {
console.error('Error updating OBS Text Source:', error);
}0
}
async function generateSRT(captionText) {
try {
// Check if recording is enabled
const recordStatus = await obs.call('GetRecordStatus');
if (recordStatus && recordStatus.outputActive) {
// Get the recording timecode
const seconds = recordStatus.outputDuration / 1000; // Assuming this returns a time in seconds
// Format timecode for SRT (HH:MM:SS,ms)
const startTime = formatTimecode(seconds);
const endTime = formatTimecode(seconds + 5); // Assume each line is displayed for 5 seconds
// Create SRT entry
const srtEntry = `${currentPosition + 1}\n${startTime} --> ${endTime}\n${captionText}\n\n`;
fs.appendFileSync(SRT_FILE_NAME, srtEntry); // Append to SRT file
console.log('Updated SRT file:', SRT_FILE_NAME);
}
} catch (error) {
console.error('Error generating SRT:', error);
}
}
function formatTimecode(seconds) {
const date = new Date(0);
date.setSeconds(seconds);
const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
const secs = String(date.getUTCSeconds()).padStart(2, '0');
const millis = String((seconds % 1) * 1000).padStart(3, '0');
return `${hours}:${minutes}:${secs},${millis}`;
}
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
const ngrok = require("@ngrok/ngrok");
async function startTunnel() {
const listener = await ngrok.forward({
addr: PORT,
authtoken: "2nCZjA9FOMFVdHAhIDyI0Tn8y0A_5oFkSL7kXve8RcCeJMXKU",
domain: "explicitly-trusty-javelin.ngrok-free.app",
proto: "http"
});
console.log(`Ingress established at: ${listener.url()}`);
}
startTunnel();

+ 1554
- 0
package-lock.json
File diff suppressed because it is too large
View File


+ 24
- 0
package.json View File

@ -0,0 +1,24 @@
{
"name": "speechsub",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": "app.js",
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@ngrok/ngrok": "^1.4.1",
"express": "^4.21.0",
"multer": "^1.4.5-lts.1",
"obs-websocket-js": "^5.0.6",
"socket.io": "^4.8.0"
},
"pkg": {
"assets": "public/*",
"outputPath": "dist"
}
}

+ 106
- 0
public/audience.html View File

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Projector View</title>
<style>
body {
font-family: Futura, sans-serif;
font-size: 32px;
background-color: green;
color: white;
text-align: center; /* Align text to the left */
margin: 0;
padding: 0;
-webkit-text-fill-color: white;
-webkit-text-stroke-color: black;
-webkit-text-stroke-width: 6.00px;
paint-order: stroke fill;
}
#container {
overflow: hidden;
height: 200px; /* Ensure container is tall enough for 4 lines */
max-width: 42ch;
position: relative;
padding-left: 20px; /* Add some padding to the left */
margin-left: auto;
margin-right: auto;
}
#content {
position: absolute;
top: 0;
transition: top 0.5s ease; /* Smooth scrolling */
}
.line {
opacity: 1;
white-space: normal; /* Allow lines to wrap */
word-wrap: break-word; /* Break long words if needed */
margin: 5px 0;
}
.currentLine {
font-weight: bold;
opacity: 1;
}
/* Ensure there's space for 4 lines to be displayed */
#container {
max-height: 400px;
}
</style>
</head>
<body>
<div id="container">
<div id="content"></div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
const contentElement = document.getElementById('content');
let speechLines = [];
let currentLineIndex = 0;
// Display 4 lines (previous, current, next, and an extra one above the current line)
function updateDisplay() {
contentElement.innerHTML = ''; // Clear previous lines
const start = Math.max(0, currentLineIndex - 2); // Show 2 lines before current
const end = Math.min(speechLines.length, currentLineIndex + 2); // Show 2 lines after current
// Create a new block of lines
const displayLines = speechLines.slice(start, end).map((line, index) => {
const div = document.createElement('div');
div.classList.add('line');
if (index === 2) div.classList.add('currentLine'); // Highlight the current line
div.textContent = line;
return div;
});
displayLines.forEach(div => contentElement.appendChild(div));
// Scroll animation based on the height of each line
const lineHeight = document.querySelector('.line').offsetHeight;
const previousLinesHeight = (currentLineIndex - start - 1) * lineHeight; // Offset for lines above the current line
contentElement.style.top = `-${previousLinesHeight}px`;
}
// Receive updates from the server
socket.on('update_caption', (newPosition) => {
currentLineIndex = newPosition;
updateDisplay();
});
// Load the speech when a new client connects
socket.on('load_speech', (lines) => {
speechLines = lines;
currentLineIndex = 0;
updateDisplay(); // Display the initial lines
});
</script>
</body>
</html>

+ 15
- 0
public/index.html View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Speech</title>
</head>
<body>
<h1>Upload Speech File</h1>
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="speech" accept=".txt">
<button type="submit">Upload</button>
</form>
</body>
</html>

+ 74
- 0
public/speaker.html View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Speaker View</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background-color: white;
}
.line {
font-size: 20px;
margin: 5px 0;
}
.currentLine {
background-color: yellow;
}
</style>
</head>
<body>
<h1>Speaker View</h1>
<div id="speech"></div>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
const speechDiv = document.getElementById('speech');
let speechLines = [];
let currentLineIndex = 0;
// Render the speech and highlight the current line
function renderSpeech() {
speechDiv.innerHTML = '';
speechLines.forEach((line, index) => {
const div = document.createElement('div');
div.classList.add('line');
if (index === currentLineIndex) {
div.classList.add('currentLine');
}
div.textContent = line;
div.onclick = () => updatePosition(index); // Allow clicking to select a line
speechDiv.appendChild(div);
});
}
// Send updated position to the server
function updatePosition(newPosition) {
currentLineIndex = newPosition;
socket.emit('update_position', currentLineIndex);
renderSpeech();
}
// Listen for speech text and load it
socket.on('load_speech', (lines) => {
speechLines = lines;
currentLineIndex = 0;
renderSpeech();
});
// Add keyboard navigation
document.addEventListener('keydown', (event) => {
if (event.key === 'ArrowDown' && currentLineIndex < speechLines.length - 1) {
updatePosition(currentLineIndex + 1);
} else if (event.key === 'ArrowUp' && currentLineIndex > 0) {
updatePosition(currentLineIndex - 1);
}
});
</script>
</body>
</html>

Loading…
Cancel
Save