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>
119 lines
5.6 KiB
JavaScript
119 lines
5.6 KiB
JavaScript
/*
|
||
* liturgy.js — the handful of words that must never be guessed at.
|
||
*
|
||
* Measured against the shipped NLLB, the single most frequent word in a service
|
||
* translates as follows:
|
||
*
|
||
* "Amen." → spa "- Sí, estoy bien." ("yes, I'm fine")
|
||
* fra "Je vous en prie." ("you're welcome")
|
||
* deu "- Das ist doch gut." ("well, that's good")
|
||
* zho "现在,我们要去." ("now, we're going to go")
|
||
* rus "- Да, да." ("yes, yes")
|
||
*
|
||
* Of ten languages tested, four were right. This is not a translation problem
|
||
* that better decoding fixes — one word carries no context for a context model
|
||
* to work from. So these bypass the model completely.
|
||
*
|
||
* SCOPE IS DELIBERATELY TINY. Only Hebrew LOANWORDS are here — amen, hallelujah,
|
||
* hosanna — words every tradition transliterated rather than translated, so
|
||
* there is one right answer and no denominational choice hiding in it.
|
||
* "Praise the Lord" and "Glory to God" are NOT here: they are translated
|
||
* phrases, and their wording differs between Protestant and Catholic editions
|
||
* of the same language. Guessing those would be the exact silent, confident
|
||
* error this file exists to prevent.
|
||
*
|
||
* Matching is WHOLE-UTTERANCE ONLY. "Amen" is a title of Christ in Revelation
|
||
* 3:14 and a deity in an Exodus sermon; "can I get an amen?" is a sentence.
|
||
* None of those may be touched, and equality matching means none of them are.
|
||
*/
|
||
(function (root, factory) {
|
||
if (typeof module === 'object' && module.exports) module.exports = factory();
|
||
else root.Liturgy = factory();
|
||
}(typeof self !== 'undefined' ? self : this, function () {
|
||
'use strict';
|
||
|
||
/*
|
||
* Whisper writes these many ways: "Amen", "amen!", "A-men.", "Amen...".
|
||
*
|
||
* Hyphens are removed (A-men -> amen) but whitespace is COLLAPSED, not
|
||
* deleted. Deleting it turned "praise god" into "praisegod", which could
|
||
* never match a table key — so every multi-word entry would have been a
|
||
* silent no-op. Only single words are in the table today, so this was latent;
|
||
* fixing it now means a multi-word entry added later actually works.
|
||
*/
|
||
function key(text) {
|
||
return String(text || '')
|
||
.toLowerCase()
|
||
.replace(/[‘’']/g, '')
|
||
.replace(/[-‐-―]+/g, '') // A-men -> amen
|
||
.replace(/\s+/g, ' ') // collapse, do NOT delete
|
||
.replace(/[.!?,;:…。!?]+$/g, '')
|
||
.trim();
|
||
}
|
||
|
||
/*
|
||
* Confidence is recorded per language, not assumed. HIGH = a form I can state
|
||
* without hedging. REVIEW = believed correct and worth a native speaker's
|
||
* glance before a service. A language absent from a table falls through to
|
||
* NLLB — no entry is better than a confident wrong one.
|
||
*/
|
||
const REVIEW = { vie_Latn: 1, hin_Deva: 1, hat_Latn: 1, arb_Arab: 1, kor_Hang: 1, npi_Deva: 1 };
|
||
|
||
const AMEN = {
|
||
spa_Latn: 'Amén.', por_Latn: 'Amém.', por_PT: 'Amém.',
|
||
fra_Latn: 'Amen.', deu_Latn: 'Amen.', ita_Latn: 'Amen.',
|
||
nld_Latn: 'Amen.', pol_Latn: 'Amen.', ces_Latn: 'Amen.',
|
||
swe_Latn: 'Amen.', dan_Latn: 'Amen.', fin_Latn: 'Aamen.',
|
||
hun_Latn: 'Ámen.', cat_Latn: 'Amén.', ron_Latn: 'Amin.',
|
||
tur_Latn: 'Âmin.', ell_Grek: 'Αμήν.', rus_Cyrl: 'Аминь.',
|
||
ukr_Cyrl: 'Амінь.', srp_Cyrl: 'Амин.', swh_Latn: 'Amina.',
|
||
zho_Hans: '阿们。', zho_Hant: '阿們。', kor_Hang: '아멘.',
|
||
vie_Latn: 'A-men.', hat_Latn: 'Amèn.', arb_Arab: 'آمين.',
|
||
hin_Deva: 'आमीन।', urd_Arab: 'آمین۔', pes_Arab: 'آمین.'
|
||
};
|
||
|
||
const HALLELUJAH = {
|
||
spa_Latn: '¡Aleluya!', por_Latn: 'Aleluia!', por_PT: 'Aleluia!',
|
||
fra_Latn: 'Alléluia !', deu_Latn: 'Halleluja!', ita_Latn: 'Alleluia!',
|
||
nld_Latn: 'Halleluja!', pol_Latn: 'Alleluja!', ces_Latn: 'Aleluja!',
|
||
swe_Latn: 'Halleluja!', dan_Latn: 'Halleluja!', fin_Latn: 'Halleluja!',
|
||
hun_Latn: 'Halleluja!', cat_Latn: 'Al·leluia!', ron_Latn: 'Aleluia!',
|
||
ell_Grek: 'Αλληλούια!', rus_Cyrl: 'Аллилуйя!', ukr_Cyrl: 'Алілуя!',
|
||
srp_Cyrl: 'Алилуја!', zho_Hans: '哈利路亚!', zho_Hant: '哈利路亞!',
|
||
kor_Hang: '할렐루야!', vie_Latn: 'Ha-lê-lu-gia!', hat_Latn: 'Alelouya!',
|
||
arb_Arab: 'هللويا!'
|
||
};
|
||
|
||
const HOSANNA = {
|
||
spa_Latn: '¡Hosanna!', por_Latn: 'Hosana!', por_PT: 'Hosana!',
|
||
fra_Latn: 'Hosanna !', deu_Latn: 'Hosianna!', ita_Latn: 'Osanna!',
|
||
nld_Latn: 'Hosanna!', pol_Latn: 'Hosanna!', rus_Cyrl: 'Осанна!',
|
||
ell_Grek: 'Ωσαννά!', zho_Hans: '和散那!', zho_Hant: '和散那!',
|
||
kor_Hang: '호산나!', hat_Latn: 'Ozana!'
|
||
};
|
||
|
||
const PHRASES = {
|
||
amen: AMEN,
|
||
hallelujah: HALLELUJAH, halleluyah: HALLELUJAH, alleluia: HALLELUJAH,
|
||
hosanna: HOSANNA, hosana: HOSANNA
|
||
};
|
||
|
||
/* The whole utterance, or nothing. Returns null so callers fall through to
|
||
normal translation, which must stay the default path. */
|
||
/* A regional variant inherits its base language's forms. Without this a
|
||
Spain or Portugal listener falls through to NLLB for the single most
|
||
frequent utterance in the service. Canonical list: NLLB_ALIAS. */
|
||
const VARIANT_OF = { por_PT: 'por_Latn', spa_ES: 'spa_Latn', spa_MX: 'spa_Latn' };
|
||
|
||
function lookup(text, lang) {
|
||
const table = PHRASES[key(text)];
|
||
if (!table) return null;
|
||
return table[lang] || table[VARIANT_OF[lang]] || null;
|
||
}
|
||
|
||
function needsReview(lang) { return !!REVIEW[lang]; }
|
||
function phrases() { return Object.keys(PHRASES); }
|
||
|
||
return { lookup, key, needsReview, phrases, PHRASES };
|
||
}));
|