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,123 @@
|
||||
/*
|
||||
* Does the stream delay hold back the INTERNET without holding back the ROOM?
|
||||
*
|
||||
* Starts the real server on a spare port with a throwaway DATA_DIR, connects
|
||||
* two listeners — one plain (the room), one carrying x-forwarded-for (through
|
||||
* the tunnel) — plus a fake engine, and times when each one is told things.
|
||||
*/
|
||||
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-delay-'));
|
||||
const PORT = 3199;
|
||||
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('ERR ' + d));
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const B = 'http://127.0.0.1:' + PORT;
|
||||
/* Wait for OUR server, identified by the config it writes into OUR dir — a
|
||||
stray copy left on this port by an earlier run would answer /healthz and
|
||||
then be tested instead. */
|
||||
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);
|
||||
}
|
||||
if (!fs.existsSync(CFG)) {
|
||||
console.error('server never started. log:\n' + logs.join(''));
|
||||
srv.kill(); process.exit(1);
|
||||
}
|
||||
const cfg = JSON.parse(fs.readFileSync(CFG, 'utf8'));
|
||||
const KEY = cfg.controlToken, PIN = cfg.operatorPin, EKEY = cfg.engineKey;
|
||||
console.log('server up on ' + PORT + '; new PIN is ' + PIN.length + ' digits');
|
||||
|
||||
const DELAY = 4;
|
||||
await fetch(B + '/api/control/delay-set?s=' + DELAY + '&key=' + KEY);
|
||||
console.log('stream delay set to ' + DELAY + 's\n');
|
||||
|
||||
function listener(label, headers) {
|
||||
const ws = new WebSocket(B.replace('http', 'ws') + '/ws?role=listener&lang=eng_Latn', { headers });
|
||||
const got = [];
|
||||
ws.on('message', (raw) => {
|
||||
const m = JSON.parse(raw);
|
||||
got.push({ t: m.t, at: Date.now(), text: m.text, delaySec: m.delaySec });
|
||||
});
|
||||
return { label, ws, got, ready: new Promise((r) => ws.on('open', r)) };
|
||||
}
|
||||
const room = listener('room ', {});
|
||||
const remote = listener('remote', { 'x-forwarded-for': '203.0.113.7' });
|
||||
await Promise.all([room.ready, remote.ready]);
|
||||
|
||||
const eng = new WebSocket(B.replace('http', 'ws') + '/ws?role=engine&key=' + encodeURIComponent(EKEY));
|
||||
await new Promise((r) => eng.on('open', r));
|
||||
await fetch(B + '/api/control/start?key=' + KEY);
|
||||
await sleep(300);
|
||||
|
||||
const hello = (l) => l.got.find((g) => g.t === 'hello');
|
||||
console.log('hello.delaySec room=' + hello(room).delaySec + ' remote=' + hello(remote).delaySec);
|
||||
|
||||
const t0 = Date.now();
|
||||
eng.send(JSON.stringify({ t: 'final', text: 'God has a plan for your life', cid: 'c1', tr: {} }));
|
||||
await sleep(DELAY * 1000 + 1200);
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
const check = (label, ok, detail) => {
|
||||
console.log((ok ? ' ok ' : ' FAIL ') + label + (detail ? ' ' + detail : ''));
|
||||
ok ? pass++ : fail++;
|
||||
};
|
||||
console.log('\ncaption arrival, measured from the moment the engine sent it:');
|
||||
for (const l of [room, remote]) {
|
||||
const c = l.got.find((g) => g.t === 'caption');
|
||||
const dt = c ? (c.at - t0) / 1000 : null;
|
||||
console.log(' ' + l.label + ' ' + (c ? dt.toFixed(2) + 's "' + c.text + '"' : 'NEVER ARRIVED'));
|
||||
}
|
||||
const rc = room.got.find((g) => g.t === 'caption'), xc = remote.got.find((g) => g.t === 'caption');
|
||||
check('the room gets it immediately', rc && (rc.at - t0) < 700, rc ? ((rc.at-t0)/1000).toFixed(2) + 's' : 'missing');
|
||||
check('the internet waits the delay', xc && (xc.at - t0) >= DELAY * 1000 - 300, xc ? ((xc.at-t0)/1000).toFixed(2) + 's' : 'missing');
|
||||
check('both got the same words', rc && xc && rc.text === xc.text);
|
||||
check('hello tells the room 0', hello(room).delaySec === 0);
|
||||
check('hello tells the internet ' + DELAY, hello(remote).delaySec === DELAY);
|
||||
|
||||
console.log('\nlate joiner backlog (scrollback in the hello frame):');
|
||||
function joiner(headers) {
|
||||
const ws = new WebSocket(B.replace('http', 'ws') + '/ws?role=listener&lang=eng_Latn', { headers });
|
||||
return new Promise((res) => {
|
||||
ws.on('message', (raw) => { const m = JSON.parse(raw); if (m.t === 'hello') { ws.close(); res(m); } });
|
||||
});
|
||||
}
|
||||
const n = (h) => (h.scrollback || []).length;
|
||||
const jr = await joiner({});
|
||||
const jx = await joiner({ 'x-forwarded-for': '203.0.113.8' });
|
||||
console.log(' a sentence spoken ' + DELAY + '+s ago:');
|
||||
console.log(' room backlog ' + n(jr) + ' · remote backlog ' + n(jx));
|
||||
check('the room sees it', n(jr) >= 1);
|
||||
check('the internet sees it too — it is past their clock', n(jx) >= 1);
|
||||
|
||||
/* Now the case that matters: something said JUST NOW must not reach a remote
|
||||
joiner's backlog, or the delay is defeated by refreshing the page. */
|
||||
const before = n(jx);
|
||||
eng.send(JSON.stringify({ t: 'final', text: 'a secret just spoken', cid: 'c2', tr: {} }));
|
||||
await sleep(400);
|
||||
const jr2 = await joiner({});
|
||||
const jx2 = await joiner({ 'x-forwarded-for': '203.0.113.9' });
|
||||
const txt = (h) => (h.scrollback || []).map((r) => r.text).join(' | ');
|
||||
console.log(' a sentence spoken 0.4s ago:');
|
||||
console.log(' room backlog: ' + txt(jr2));
|
||||
console.log(' remote backlog: ' + txt(jx2));
|
||||
check('the room sees the new sentence', txt(jr2).includes('a secret just spoken'));
|
||||
check('the internet does NOT — refreshing cannot skip the delay',
|
||||
!txt(jx2).includes('a secret just spoken'));
|
||||
check('server sized the audio window for the delay', logs.join('').includes('audio window now'));
|
||||
|
||||
console.log('\n--- server log ---');
|
||||
console.log(logs.join('').split('\n').filter((l) => /delay|window|START/.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