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,147 @@
|
||||
/*
|
||||
* Regenerates bible-lexicon.js from every bible/lexicon-*.json.
|
||||
* Run after editing any lexicon file.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const files = fs.readdirSync('bible').filter((f) => /^lexicon-.*\.json$/.test(f)).sort();
|
||||
const english = {}; // lang -> { English term: local form }
|
||||
const fixes = {}; // lang -> { wrong local form: right local form }
|
||||
const refs = {};
|
||||
let total = 0;
|
||||
|
||||
/* A variant can INHERIT its base language's table and then diverge. Portugal
|
||||
keeps "David" where Brazil writes "Davi", so por_PT cannot simply reuse
|
||||
por_Latn — but it should not have to restate 85 shared entries either. */
|
||||
const inheritQueue = [];
|
||||
|
||||
for (const f of files) {
|
||||
const j = JSON.parse(fs.readFileSync(path.join('bible', f), 'utf8'));
|
||||
const lang = j.language;
|
||||
if (j.inherits) { inheritQueue.push(j); refs[lang] = j.reference; continue; }
|
||||
refs[lang] = j.reference;
|
||||
const t = {};
|
||||
for (const grp of ['books', 'people', 'places', 'terms']) {
|
||||
for (const [en, loc] of Object.entries(j[grp] || {})) {
|
||||
if (en.startsWith('_') || en === loc) continue; // no-ops earn nothing
|
||||
t[en] = loc;
|
||||
}
|
||||
}
|
||||
english[lang] = t;
|
||||
total += Object.keys(t).length;
|
||||
const tf = {};
|
||||
for (const [bad, good] of Object.entries(j._target_fixes || {})) {
|
||||
if (bad.startsWith('_')) continue;
|
||||
tf[bad] = good;
|
||||
}
|
||||
if (Object.keys(tf).length) fixes[lang] = tf;
|
||||
}
|
||||
|
||||
/* Resolve inheritance after every base table is built. */
|
||||
for (const j of inheritQueue) {
|
||||
const from = english[j.inherits];
|
||||
if (!from) throw new Error(j.language + ' inherits ' + j.inherits + ', which does not exist');
|
||||
const t = Object.assign({}, from);
|
||||
for (const k of (j.remove || [])) delete t[k];
|
||||
for (const [en, loc] of Object.entries(j.overrides || {})) {
|
||||
if (en === loc) delete t[en]; // "same as English" means: do not touch it
|
||||
else t[en] = loc;
|
||||
}
|
||||
english[j.language] = t;
|
||||
total += Object.keys(t).length;
|
||||
const fb = fixes[j.inherits];
|
||||
if (fb) fixes[j.language] = Object.assign({}, fb, j.targetFixes || {});
|
||||
else if (j.targetFixes) fixes[j.language] = j.targetFixes;
|
||||
}
|
||||
|
||||
const j2 = (o) => JSON.stringify(o, null, 2).split('\n').map((l, i) => i ? ' ' + l : l).join('\n');
|
||||
|
||||
const OUT = [
|
||||
'/*',
|
||||
' * bible-lexicon.js — GENERATED from bible/lexicon-*.json by _gen-lexicon.js.',
|
||||
' * Do not hand-edit; edit the JSON and regenerate.',
|
||||
' *',
|
||||
' * TWO TABLES, and the difference is the whole safety argument.',
|
||||
' *',
|
||||
' * ENGLISH[] maps an ENGLISH term to the local form. It only ever fires on',
|
||||
' * English text that survived translation, so there is no target-language',
|
||||
' * homograph to collide with — the trap that makes a target-side lexicon',
|
||||
' * dangerous (a Spanish entry for "Marca" would turn "la marca de la bestia"',
|
||||
' * into "la Marcos de la bestia"). No CJK word-boundary problem either, and',
|
||||
' * no noun gender or grammatical case to agree with.',
|
||||
' *',
|
||||
' * FIXES[] is target-side and therefore restricted to forms MEASURED wrong',
|
||||
' * against a named published edition, each one checked for not being an',
|
||||
' * ordinary word in that language. Never add to it by intuition.',
|
||||
' *',
|
||||
' * Naming authorities: ' + Object.entries(refs).map(([k, v]) => k + ' = ' + v).join('; ') + '.',
|
||||
' * Verse TEXT from any copyrighted edition appears nowhere in this app; proper',
|
||||
' * nouns and book titles are naming conventions, not copyrightable subject',
|
||||
' * matter.',
|
||||
' */',
|
||||
'(function (root, factory) {',
|
||||
" if (typeof module === 'object' && module.exports) module.exports = factory();",
|
||||
' else root.BibleLexicon = factory();',
|
||||
"}(typeof self !== 'undefined' ? self : this, function () {",
|
||||
" 'use strict';",
|
||||
'',
|
||||
' const ENGLISH = ' + j2(english) + ';',
|
||||
'',
|
||||
' const FIXES = ' + j2(fixes) + ';',
|
||||
'',
|
||||
' /* A regional variant inherits its base language. Canonical list: NLLB_ALIAS. */',
|
||||
" const VARIANT_OF = { spa_ES: 'spa_Latn', spa_MX: 'spa_Latn', por_PT: 'por_Latn', zho_Hant: 'zho_Hans' };",
|
||||
' const base = (lang) => (ENGLISH[lang] ? lang : VARIANT_OF[lang]);',
|
||||
'',
|
||||
' function forLanguage(lang) { return ENGLISH[base(lang)] || null; }',
|
||||
'',
|
||||
' /*',
|
||||
' * WHOLE-UTTERANCE NAME LOOKUP — the deterministic guarantee.',
|
||||
' *',
|
||||
' * A bare name is the worst input this model receives. MEASURED into',
|
||||
' * Spanish: "Peter." came back as "\u00bfQu\u00e9 es eso?" and "Jesus." as',
|
||||
' * "\u00bfQu\u00e9 quieres?" — the name GONE, replaced by invented dialogue.',
|
||||
' * Into French, "Habakkuk." became "Habakuk est mort" (Habakkuk is dead).',
|
||||
' * A preacher listing names one at a time produced a run of questions with',
|
||||
' * no names in them at all.',
|
||||
' *',
|
||||
' * The cause is ONE CHARACTER: the terminal period. Without it the model',
|
||||
' * translates the term ("Peter" -> "Pedro"); with it, it reads a complete',
|
||||
' * conversational turn and invents a reply. preach.js strips the period for',
|
||||
' * short utterances, which fixes most of it — this is the guarantee for the',
|
||||
' * rest, because a name we know should never depend on the model at all.',
|
||||
' *',
|
||||
' * Matched on the bare word, punctuation and case stripped, and ONLY when',
|
||||
' * the utterance is that name and nothing else — an ordinary sentence',
|
||||
' * containing the name is untouched.',
|
||||
' */',
|
||||
' function wholeUtterance(text, lang) {',
|
||||
' const t = ENGLISH[base(lang)];',
|
||||
' if (!t) return null;',
|
||||
' const bare = String(text == null ? \'\' : text)',
|
||||
' .replace(/^[\\s"\\u2018\\u201c(\\[\\\']+/, \'\')',
|
||||
' .replace(/[\\s.!?,;:"\\u2019\\u201d)\\]\\\']+$/, \'\')',
|
||||
' .trim();',
|
||||
' if (!bare) return null;',
|
||||
' if (Object.prototype.hasOwnProperty.call(t, bare)) return t[bare];',
|
||||
' const lower = bare.toLowerCase();',
|
||||
' for (const k of Object.keys(t)) if (k.toLowerCase() === lower) return t[k];',
|
||||
' return null;',
|
||||
' }',
|
||||
'',
|
||||
' function fixesFor(lang) { return FIXES[base(lang)] || null; }',
|
||||
' function languages() { return Object.keys(ENGLISH); }',
|
||||
'',
|
||||
' return { forLanguage: forLanguage, fixesFor: fixesFor,',
|
||||
' wholeUtterance: wholeUtterance, languages: languages,',
|
||||
' ENGLISH: ENGLISH, FIXES: FIXES };',
|
||||
'}));'
|
||||
].join('\n');
|
||||
|
||||
fs.writeFileSync('bible-lexicon.js', OUT);
|
||||
console.log('wrote bible-lexicon.js — ' + files.length + ' languages, ' + total + ' English entries');
|
||||
for (const [l, t] of Object.entries(english)) {
|
||||
console.log(' ' + l.padEnd(10) + String(Object.keys(t).length).padStart(4) + ' entries, ' +
|
||||
Object.keys(fixes[l] || {}).length + ' target fixes (' + refs[l] + ')');
|
||||
}
|
||||
Reference in New Issue
Block a user