Files
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

270 lines
12 KiB
JavaScript
Raw Permalink 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.
/*
* mt-guard.js — catch what NLLB invents, before a congregation hears it.
*
* WHY THIS IS NOT A GENERATION-PARAMETER FIX. Measured against the shipped
* model (nllb-200-distilled-600M fp32), "Donald Trump." produces
* "Donald Trump. ¿Qué es eso?" under ALL of: default params, an input-derived
* max_new_tokens cap, no_repeat_ngram_size:3, and num_beams:2. The model
* genuinely believes that is the translation. Decoding cannot be tuned out of
* it; the output has to be checked.
*
* WHY THE OBVIOUS RULE IS WRONG. "Source has no '?', target has one, so strip
* the question" corrupts real output: Whisper routinely drops the question mark
* on a rhetorical aside, so "Are you with me" arrives unpunctuated and a
* correct Spanish rendering is "¿Estás conmigo?" — which that rule would
* delete. The reliable signal is not the question mark, it is the SENTENCE
* COUNT: one sentence in, two out, and the second one invented.
*
* WHY ONLY SHORT INPUTS. Hallucination is a short-input failure — the model has
* no context and fills the gap. Long sentences are also the ones that
* legitimately split in the target (many languages prefer shorter sentences
* than English), so clamping them would delete real scripture. The length gate
* keeps the clamp where the evidence is.
*/
(function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory();
else root.MtGuard = factory();
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
/* Sentence-final punctuation across the scripts this app serves: Latin,
CJK ideographic stop, fullwidth marks, Arabic question mark, Devanagari
danda, Armenian, Greek, Thai has none. */
const TERMINATORS = '.!?。!?؟۔।॥…՜՞;';
const TERM_RE = new RegExp('[' + TERMINATORS.replace(/[.\-\]\\]/g, '\\$&') + ']');
const QUESTION_RE = /[??؟՞]/;
/* Beyond this the input carries enough context that the model stops
inventing, and legitimate sentence-splitting starts. Measured failures all
sit far below it. */
/* Narrow deliberately. Every hallucination measured against this model sat
at one or two words; a wider gate starts reaching utterances that split
into two target sentences legitimately, and deleting real scripture is a
far worse failure than leaving one invented clause in. */
const SHORT_WORDS = 6;
function words(s) {
return String(s || '').trim().split(/\s+/).filter(Boolean).length;
}
/* Count sentences by terminator runs. "Mr. Smith went." must not read as two,
so a terminator followed by a lowercase letter does not end a sentence. */
function sentenceCount(s) {
const parts = splitSentences(s);
return parts.length || 1;
}
function splitSentences(s) {
const text = String(s || '').trim();
if (!text) return [];
const out = [];
let cur = '';
for (let i = 0; i < text.length; i++) {
const ch = text[i];
cur += ch;
if (!TERM_RE.test(ch)) continue;
/* Absorb a run of terminators and any closing quote/bracket. */
while (i + 1 < text.length && (TERM_RE.test(text[i + 1]) || /["'\)\]”’」』]/.test(text[i + 1]))) {
cur += text[++i];
}
const rest = text.slice(i + 1);
/* An abbreviation or a decimal: "Dr. Smith", "3.5" — not a boundary. */
if (/^\s*[a-zà-ÿ0-9]/.test(rest)) continue;
out.push(cur.trim());
cur = '';
}
if (cur.trim()) out.push(cur.trim());
return out;
}
/*
* NLLB was trained on a lot of subtitles and emits their dialogue dash. It is
* never correct here — nothing in this app is a screenplay.
*/
function stripDash(s) {
return String(s || '').replace(/^\s*[-‐-―−]\s+/, '').trim();
}
/*
* The main entry. Returns { text, trimmed, reason } — `trimmed` is true when
* something was removed, so callers can log it and the operator can see how
* often the model is inventing.
*/
/*
* A ONE-WORD source that comes back with a comma-clause. Measured across the
* archive, 13.4% of everything the preacher says is one or two words, and the
* model answers the short ones conversationally:
*
* "Hello." -> "Hola, ¿qué pasa?" "Yeah." -> "Sí, es cierto."
* "No." -> "No, no." "Good." -> "Bien, muy bien."
* "Come." -> "Ven, ven." "Look." -> "Mira, ¿qué?"
*
* One English word cannot mean two clauses, so the tail is invented. Keeping
* only the head fixes all of these and touches nothing that was already
* right ("La fe.", "Sin embargo.", "Por lo tanto." have no comma).
*
* Restricted to EXACTLY one word deliberately — at two words a comma can be
* legitimate. Handles the CJK and ideographic commas too, since this is the
* one output rule that needs no per-language knowledge.
*/
function trimInventedClause(src, out) {
const words = String(src == null ? '' : src).replace(/[.!?,;:]+$/, '').split(/\s+/).filter(Boolean);
if (words.length !== 1) return out;
const m = String(out).match(/^([^,\uff0c\u3001]+)[,\uff0c\u3001]/u);
if (!m) return out;
const head = m[1].trim();
if (!head) return out;
const end = (String(src).match(/[.!?]$/) || [''])[0];
return /[.!?\u3002\uff01\uff1f]$/.test(head) ? head : head + end;
}
/*
* THE WHOLE OUTPUT IS AN INVENTED QUESTION.
*
* The sentence-count rule above catches an invented question APPENDED to a
* real translation ("Donald Trump. ¿Qué es eso?"). It cannot catch the case
* where the invention IS the output: one sentence in, one sentence out, and
* the one out is "¿Qué es eso?" for a source of "Mmm" or "them" — MEASURED
* in a live service, spoken aloud to the Spanish congregation three times in
* one morning, and produced by 34 distinct short sources in the archive.
*
* Narrow on purpose. Source: at most two words and not itself a question.
* Output: every sentence is a question. And the source must not be an
* English interrogative or a tag — "Why.", "Who.", "Right?", "You know." —
* because for those a Spanish question IS the correct rendering (measured:
* 'why.' -> '¿Por qué?', 'Isn't it.' -> '¿No es cierto?'). Whisper drops
* question marks routinely, so the word list, not the punctuation, is the
* exemption.
*/
/*
* Widened with the auxiliaries and contractions that FRONT a real English
* question, because the rule below now reads every word of a short source
* rather than the single word it used to be limited to.
*
* MEASURED: without `could` and `you're`, widening the rule deleted
* 'Could it be' -> '¿Podría ser' and "You're interested in" -> '¿Te interesa?'
* — both correct renderings of questions Whisper handed over unpunctuated.
* With them the widened rule breaks NOTHING in 22,526 real short lines.
*/
const INTERROGATIVE_SRC = /^(?:why|who|whom|whose|what|where|when|when'?s|how|which|isn'?t|right|okay|ok|see|really|huh|anybody|anyone|somebody|someone|guess|any|says|you|you'?re|we'?re|they'?re|there'?s|it'?s|are|am|is|was|were|do|does|did|can|could|will|would|shall|should|may|might|must|have|has|had|ready|let'?s)$/i;
function isQuestionSentence(p) {
const t = String(p || '').trim();
return /^[¿]/.test(t) || /[??؟՞]["'\)\]”’」』]*$/.test(t);
}
function bareSource(src) {
return String(src || '').replace(/^[\s"'“‘(\[]+/u, '').replace(/[.!,;:…\-\s"'”’)\]]+$/u, '').trim();
}
/* Short enough that the model had no context to work from. Four, measured:
see the numbers below. */
const INVENTED_MAX_WORDS = 4;
/*
* UP TO FOUR words, not one.
*
* This was restricted to a single word because a two-word rule was measured
* blanking 'Why not.' -> '¿Por qué no?', 'Guess what.' -> '¿Adivina qué?',
* 'You ready.' -> '¿Están listos?'. That finding was right; the conclusion
* drawn from it was too narrow. What those cases have in common is not their
* LENGTH, it is that they contain an interrogative word — so the exemption
* below, not the word count, is what protects them. Testing every word of
* the source instead of only a lone one keeps all of them and frees the
* count to rise.
*
* MEASURED over this church's own archive — 349 confirmed inventions and
* 22,526 legitimate short translations from ten real services:
*
* one word (before) 198/349 caught 57% 0 legitimate lines broken
* <= 4 words 254/349 caught 73% 0 legitimate lines broken
*
* The congregation was hearing 'Check, check.' as '¿Qué quieres?', 'Holy
* Spirit.' as '我們必須要做什麼?' and 'Hello.' as '¿Qué pasa?' — all of them
* two words, all of them one word past where this rule used to stop looking.
*
* A source of NO words reaches here too ('-', '...', a stray dash Whisper
* emits between speakers). Nothing cannot mean a question, so it is not
* exempted: an empty source with an all-question output is an invention by
* definition.
*/
function inventedQuestion(src, out) {
if (QUESTION_RE.test(src)) return false;
const words = bareSource(src).split(/\s+/).filter(Boolean);
if (words.length > INVENTED_MAX_WORDS) return false;
/* ANY interrogative word, anywhere in the source — 'Guess what.' and
'Why not.' put it first, 'How about now' and 'And why' do not. */
if (words.some(function (w) { return INTERROGATIVE_SRC.test(w); })) return false;
const parts = splitSentences(out);
return parts.length > 0 && parts.every(isQuestionSentence);
}
/*
* THE PORTUGUESE TWIN. NLLB's stock invention for a fragment in Portuguese is
* not a question but the statement "Não." ('See.' -> 'Não.', 'Wow.' ->
* 'Não.', 'way.' -> 'Não.', 'You understand?' -> '- Não.' — measured in
* the archive), and the Spanish "No, no lo sé." family. A short source with
* no negation in it cannot mean "No".
*/
const NEGATION_SRC = /\b(?:no|not|never|nope|nah|nay|nix|negative|nothing|none|nobody|neither|nor|don'?t|can'?t|won'?t|isn'?t|ain'?t|didn'?t|doesn'?t|wasn'?t|aren'?t|couldn'?t|wouldn'?t|shouldn'?t)\b|\b(?:uh|nuh)-?uh\b/i;
const NEGATION_OUT = /^[\s¿¡\-‐-―−"'“”]*(?:não|nao|no|non|nein|nee|niet|nej|nie|нет|いいえ|不|아니요|لا)[\s.!?…"'”]*$/iu;
function inventedNegation(src, out) {
const bare = bareSource(src);
const words = bare.split(/\s+/).filter(Boolean);
if (!words.length || words.length > 2) return false;
if (NEGATION_SRC.test(bare)) return false;
return NEGATION_OUT.test(String(out || ''));
}
function clean(source, target) {
const src = String(source == null ? '' : source).trim();
let out = stripDash(target);
if (!out) return { text: '', trimmed: false, reason: '' };
const dashed = out !== String(target || '').trim();
const oneWord = trimInventedClause(src, out);
if (oneWord !== out) {
return { text: oneWord, trimmed: true,
reason: 'dropped invented clause after a one-word line \u2014 "' + out + '"' };
}
if (inventedQuestion(src, out)) {
return { text: '', trimmed: true,
reason: 'dropped an invented question for a short line \u2014 "' + out + '"' };
}
if (inventedNegation(src, out)) {
return { text: '', trimmed: true,
reason: 'dropped an invented negation for a short line \u2014 "' + out + '"' };
}
if (words(src) > SHORT_WORDS) {
return { text: out, trimmed: dashed, reason: dashed ? 'subtitle dash' : '' };
}
const nSrc = sentenceCount(src);
const parts = splitSentences(out);
if (parts.length <= nSrc) {
return { text: out, trimmed: dashed, reason: dashed ? 'subtitle dash' : '' };
}
/*
* More sentences came out than went in, on an input too short to justify
* it. Keep the first nSrc — NLLB translates in order, so the real content
* is at the front and the invention is appended.
*/
const kept = parts.slice(0, nSrc).join(' ').trim();
const dropped = parts.slice(nSrc).join(' ').trim();
if (!kept) return { text: out, trimmed: dashed, reason: '' };
return {
text: kept,
trimmed: true,
reason: 'dropped invented ' + (QUESTION_RE.test(dropped) ? 'question' : 'text') +
' "' + dropped + '"'
};
}
return { clean, splitSentences, sentenceCount, stripDash, trimInventedClause, inventedQuestion, inventedNegation, SHORT_WORDS };
}));