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
+53
View File
@@ -0,0 +1,53 @@
/* Measure the hallucination and what actually stops it. Runs the same model,
dtype and cache the app uses, in a plain node process like mt-service.js. */
import { pipeline, env } from '@huggingface/transformers';
import os from 'os';
import path from 'path';
env.allowLocalModels = false;
env.cacheDir = path.join(os.homedir(), 'Library/Application Support/Church Translate Models');
const t0 = Date.now();
const pipe = await pipeline('translation', 'Xenova/nllb-200-distilled-600M', { dtype: 'fp32' });
console.log('loaded in ' + ((Date.now() - t0) / 1000).toFixed(1) + 's\n');
/* The exact lines from the operator's screenshot, plus stress cases. */
const CASES = [
'Donald Trump.',
'Hello, hello.',
'His name was John.',
'Open up to the book, Matthew.',
'Today we are going to talk about Peter in the Bible.',
'Turn with me to John chapter 3.',
'Amen.',
'Jesus wept.',
'Paul wrote to the church at Corinth.',
'How many countries in the world?',
'And God said, let there be light, and there was light.'
];
const CONFIGS = [
{ name: 'current (no params)', opts: {} },
{ name: 'cap 2x+8', opts: null, cap: (n) => Math.ceil(n * 2) + 8 },
{ name: 'cap 2x+8 +nrng3', opts: { no_repeat_ngram_size: 3 }, cap: (n) => Math.ceil(n * 2) + 8 },
{ name: 'beams2 cap2x+8', opts: { num_beams: 2, no_repeat_ngram_size: 3 }, cap: (n) => Math.ceil(n * 2) + 8 }
];
const tok = pipe.tokenizer;
for (const cfg of CONFIGS) {
console.log('=== ' + cfg.name + ' ===');
let total = 0;
for (const text of CASES) {
const nIn = tok(text).input_ids.dims.at(-1);
const opts = Object.assign({ src_lang: 'eng_Latn', tgt_lang: 'spa_Latn' }, cfg.opts || {});
if (cfg.cap) opts.max_new_tokens = cfg.cap(nIn);
const s = Date.now();
const out = await pipe(text, opts);
const ms = Date.now() - s;
total += ms;
const got = (Array.isArray(out) ? out[0] : out).translation_text.trim();
const flag = (!/[??]/.test(text) && /[??]/.test(got)) ? ' <-- INVENTED A QUESTION' : '';
console.log(' ' + String(ms).padStart(5) + 'ms in' + String(nIn).padStart(3) + ' ' + got + flag);
}
console.log(' total ' + total + 'ms\n');
}