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:
2026-09-23 15:41:21 -04:00
co-authored by Claude Opus 5
commit c9eef0672c
118 changed files with 36031 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
/* Drive the REAL live-audio module the way the mic feed does, and see whether
the stream it produces is continuous. */
import { execFileSync } from 'child_process';
import fs from 'fs'; import os from 'os'; import path from 'path';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const { LiveAudio } = require('./server/live-audio.js');
const RATE = 16000, CHUNK = 11200; // MIC_CHUNK
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'micfeed-'));
function encode(samples, hz) {
const pcm = Buffer.alloc(samples * 2);
for (let i = 0; i < samples; i++) pcm.writeInt16LE(Math.round(9000 * Math.sin(i / hz)), i * 2);
const wav = Buffer.alloc(44 + pcm.length);
wav.write('RIFF', 0); wav.writeUInt32LE(36 + pcm.length, 4);
wav.write('WAVE', 8); wav.write('fmt ', 12);
wav.writeUInt32LE(16, 16); wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22);
wav.writeUInt32LE(RATE, 24); wav.writeUInt32LE(RATE * 2, 28);
wav.writeUInt16LE(2, 32); wav.writeUInt16LE(16, 34);
wav.write('data', 36); wav.writeUInt32LE(pcm.length, 40);
pcm.copy(wav, 44);
const w = path.join(d, 'c.wav'), a = path.join(d, 'c.aac');
fs.writeFileSync(w, wav);
if (fs.existsSync(a)) fs.unlinkSync(a);
execFileSync('/usr/bin/afconvert', ['-f', 'adts', '-d', 'aac@' + RATE, '-b', '48000', w, a]);
return fs.readFileSync(a);
}
const live = new LiveAudio((m) => console.log(' [live] ' + m));
const st = live.ensure('eng_Latn', RATE);
st.start();
/* Count silence frames the pump invents because the queue ran dry. */
const SIL = require('./server/live-audio.js').silenceFrame(RATE);
let underrun = 0, real = 0, counting = false;
const origClose = st.closeSegment.bind(st);
st.closeSegment = function () {
if (counting) for (const f of this.building) { if (f === SIL) underrun++; else real++; }
return origClose();
};
console.log('pushing 700ms chunks every 700ms for 14s, exactly as the mic feed does…\n');
const clip = encode(CHUNK, 9);
let pushed = 0;
const t0 = Date.now();
let stalls = 0;
function schedule(delay) { setTimeout(tickPush, delay); }
function tickPush() {
st.pushContinuous(clip);
pushed++;
if (pushed === 2) counting = true; // skip the startup seed
const backlogMs = (st.pendingFrames * 1024) / RATE * 1000;
const wall = Date.now() - t0;
const audioIn = pushed * 700;
console.log(' chunk ' + String(pushed).padStart(2) +
' wall ' + String(wall).padStart(5) + 'ms' +
' audio pushed ' + String(audioIn).padStart(5) + 'ms' +
' backlog ' + backlogMs.toFixed(0).padStart(4) + 'ms' +
' segments ' + st.segments.length);
/* Two deliberate stalls: 500ms and 900ms late, modelling a blocked event
loop or two chunks bunched by the socket. */
let nextIn = 700;
if (pushed === 8) { nextIn = 1200; stalls++; }
if (pushed === 14) { nextIn = 1600; stalls++; }
if (pushed < 20) { schedule(nextIn); return; }
if (pushed >= 20) {
const held = st.segments.length * (st.framesPerSegment * 1024 / RATE);
console.log('\n produced ' + st.segments.length + ' segments (' + held.toFixed(1) + 's held)');
console.log(' real audio frames : ' + real);
console.log(' SILENCE frames the pump invented on underrun : ' + underrun +
' (' + (underrun * 64) + ' ms of gaps)');
console.log(' audio in: ' + (pushed * 0.7).toFixed(1) + 's wall: ' + ((Date.now()-t0)/1000).toFixed(1) + 's');
console.log(' deliberate stalls injected: ' + stalls + ' (one 500ms late, one 900ms late)');
console.log(' UNDERRUNS reported by the stream: ' + (st.underruns || 0));
fs.rmSync(d, { recursive: true, force: true });
process.exit(0);
}
}
schedule(700);