/* * 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 }; }));