Files
church-translate/app/_test-delay.mjs
T
christianandClaude Opus 5 c9eef0672c 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>
2026-09-23 15:41:21 -04:00

124 lines
5.8 KiB
JavaScript

/*
* 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);