Files
church-translate/app/liturgy.js
T
christianandClaude Opus 5 c9eef0672c 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>
2026-09-23 15:41:21 -04:00

119 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 };
}));