Church Translate, as it runs at the campuses
Live sermon translation into 100+ languages, running entirely on one Mac per campus: audio in, voice detection, Whisper on the GPU, NLLB-200 in its own process, and a built-in web server the congregation reads from on their phones. No audio leaves the building. This is the source only. What used to sit beside it — 14 GB of speech and translation models, 8 GB of voice bundles, 6 GB of natural voices, the Electron caches, and the app data holding the operator PIN and every service transcript — is machine state and downloaded assets, and stays in the NAS backup where it belongs. The vendored node, cloudflared and Windows ffmpeg binaries are out too (533 MB, re-fetched for packaging); the browser-side runtime beside them is kept, so a checkout still serves the congregation page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
/* The operator desk must be unreachable from the internet — page, API and
|
||||
socket — and completely normal on the LAN. */
|
||||
import { spawn } from 'child_process';
|
||||
import { WebSocket } from 'ws';
|
||||
import fs from 'fs'; import os from 'os'; import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'ct-oplocal-'));
|
||||
const PORT = 3198;
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const srv = spawn(process.execPath, [path.join(__dirname, 'server', 'server.js')],
|
||||
{ env: { ...process.env, PORT: String(PORT), DATA_DIR: DIR }, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
const logs = []; srv.stdout.on('data', d => logs.push(String(d))); srv.stderr.on('data', d => logs.push(String(d)));
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
const B = 'http://127.0.0.1:' + PORT;
|
||||
const CFG = path.join(DIR, 'config.json');
|
||||
for (let i = 0; i < 80; i++) { if (fs.existsSync(CFG)) { try { await fetch(B + '/healthz'); break; } catch (_) {} } await sleep(150); }
|
||||
const cfg = JSON.parse(fs.readFileSync(CFG, 'utf8'));
|
||||
const PIN = cfg.operatorPin, KEY = cfg.controlToken;
|
||||
console.log('new-install PIN: ' + PIN.length + ' digits (' + PIN + ')\n');
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
const check = (l, ok, d) => { console.log((ok ? ' ok ' : ' FAIL ') + l + (d ? ' ' + d : '')); ok ? pass++ : fail++; };
|
||||
const TUN = { 'x-forwarded-for': '203.0.113.7' };
|
||||
|
||||
const code = async (p, h) => (await fetch(B + p, { headers: h || {} })).status;
|
||||
|
||||
console.log('from the LAN (correct PIN):');
|
||||
check('/operator page loads', await code('/operator') === 200);
|
||||
check('/api/config accepts the PIN', await code('/api/config?pin=' + PIN) === 200);
|
||||
|
||||
console.log('\nfrom the internet (same correct PIN):');
|
||||
check('/operator page is 404', await code('/operator', TUN) === 404, 'not even the login box');
|
||||
const c = await code('/api/config?pin=' + PIN, TUN);
|
||||
check('/api/config is refused', c === 401 || c === 429, 'HTTP ' + c);
|
||||
|
||||
console.log('\nthe listener page must still work from the internet:');
|
||||
check('/ is served', await code('/', TUN) === 200);
|
||||
const lm = await code('/live/eng_Latn.m3u8', TUN);
|
||||
check('/live/*.m3u8 answers', lm === 200 || lm === 404, 'HTTP ' + lm + ' (404 = nothing playing yet)');
|
||||
|
||||
console.log('\noperator WEBSOCKET:');
|
||||
const tryWs = (headers) => new Promise((res) => {
|
||||
const ws = new WebSocket(B.replace('http','ws') + '/ws?role=operator&pin=' + PIN, { headers });
|
||||
const t = setTimeout(() => { try { ws.close(); } catch (_) {} res('timeout'); }, 3000);
|
||||
ws.on('message', () => { clearTimeout(t); ws.close(); res('accepted'); });
|
||||
ws.on('close', (c, r) => { clearTimeout(t); res('closed: ' + String(r || c)); });
|
||||
ws.on('error', () => {});
|
||||
});
|
||||
check('LAN socket is accepted', await tryWs({}) === 'accepted');
|
||||
const w = await tryWs(TUN);
|
||||
check('tunnel socket is closed', w.startsWith('closed'), w);
|
||||
|
||||
console.log('\ncontrol API (Stream Deck) is unaffected — it uses a long random token:');
|
||||
check('control works from the LAN', await code('/api/control/status?key=' + KEY) === 200);
|
||||
|
||||
console.log('\n--- log ---');
|
||||
console.log(logs.join('').split('\n').filter(l => /refused|operator/.test(l)).map(l => ' ' + l).join('\n'));
|
||||
console.log('\n' + pass + ' passed, ' + fail + ' failed');
|
||||
srv.kill(); fs.rmSync(DIR, { recursive: true, force: true });
|
||||
process.exit(fail ? 1 : 0);
|
||||
Reference in New Issue
Block a user