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
+41
View File
@@ -0,0 +1,41 @@
import { pipeline, env } from '@huggingface/transformers';
import { createRequire } from 'module';
import os from 'os'; import path from 'path'; import fs from 'fs';
const require = createRequire(import.meta.url);
env.allowLocalModels=false;
env.cacheDir=path.join(os.homedir(),'Library/Application Support/Church Translate Models');
const pipe=await pipeline('translation','Xenova/nllb-200-distilled-600M',{dtype:'fp32'});
/* The names a preacher actually says, frequency-ranked from the owner's own
Strong's export, plus the Gospel/epistle authors that are rare INSIDE
scripture but constant as book references. */
const ranked=require('/private/tmp/claude-502/-Users-Studio-Documents-BandLineupDisplay/cee248f6-d861-4845-bd13-03867cf46596/scratchpad/proper-nouns.json')
.map(r=>r.english).filter(n=>/^[A-Z][a-z]+$/.test(n));
const NAMES=[...new Set(['Matthew','Mark','Luke','John','Paul','Peter','James','Jude','Timothy','Titus',
'Jesus','Moses','David','Abraham','Isaac','Jacob','Joseph','Mary','Elijah','Isaiah','Jeremiah',
'Daniel','Solomon','Samuel','Saul','Noah','Adam','Eve','Joshua','Thomas','Philip','Andrew',
'Stephen','Barnabas','Silas','Apollos','Lazarus','Nicodemus','Pilate','Herod','Jerusalem',
'Bethlehem','Nazareth','Galilee','Egypt','Babylon','Corinth','Ephesus','Rome','Zion',
...ranked.slice(0,20)])].slice(0,60);
const LANGS=['por_Latn','fra_Latn','zho_Hans','zho_Hant','kor_Hang','vie_Latn','hat_Latn','arb_Arab','hin_Deva'];
const LATIN=/[A-Za-z]/;
const NONLATIN=new Set(['zho_Hans','zho_Hant','kor_Hang','arb_Arab','hin_Deva']);
const out={};
for (const lang of LANGS) {
out[lang]={};
for (const n of NAMES) {
const o=await pipe('We read about '+n+' in the Bible.',{src_lang:'eng_Latn',tgt_lang:lang});
out[lang][n]=(Array.isArray(o)?o[0]:o).translation_text.trim();
}
/* For a non-Latin script, surviving Latin letters mean the name was never
translated at all — an unambiguous, mechanically detectable failure. */
const survived=NONLATIN.has(lang)
? NAMES.filter(n=>LATIN.test(out[lang][n]))
: NAMES.filter(n=>new RegExp('\\b'+n+'\\b').test(out[lang][n]));
console.log(lang+' '+String(survived.length).padStart(2)+'/'+NAMES.length+' names left in ENGLISH: '+survived.slice(0,18).join(', ')+(survived.length>18?' …':''));
out[lang]._survived=survived;
}
fs.writeFileSync('/tmp/name-probe.json',JSON.stringify(out,null,1));
console.log('\nsaved /tmp/name-probe.json');