Files
church-translate/docs/PLAN-bible-fidelity.md
T

106 lines
5.7 KiB
Markdown
Raw Normal View History

# Plan — Bible-faithful translation, "bulletproof for live preaching"
## The complaint
Operator, watching the Spanish listener page during a live test:
- "why is the voice asking questions to me? it keeps saying '¿Qué es eso?'"
- "if i say 'john' it must be translated properly"
- "make this bulletproof so native speakers in any language can completely
understand what is being said in english. this will primarily be used for
live preaching of the christian bible."
## Measured, not assumed
Ran `Xenova/nllb-200-distilled-600M` fp32 (the shipped model, shipped dtype,
shipped cache) against the exact lines from the screenshot plus stress cases,
under four generation configs. Results:
| English in | Spanish out | verdict |
|---|---|---|
| `Donald Trump.` | `Donald Trump. ¿Qué es eso?` | invented question |
| `Amen.` | `- Sí, estoy bien.` | "Yes, I'm fine." — catastrophic |
| `Hello, hello.` | `- Hola, está bien.` | invented content + subtitle dash |
| `His name was John.` | `Se llamaba John.` | name left in English |
| `Turn with me to John chapter 3.` | `…a Juan capítulo 3.` | same name, translated |
| `Open up to the book, Matthew.` | `Abre al libro, Matthew.` | name left in English |
**The decisive finding:** `¿Qué es eso?` appears under ALL FOUR configs —
no params, `max_new_tokens` cap, `no_repeat_ngram_size:3`, and `num_beams:2`.
It is not a decoding-budget artifact. Generation parameters cannot fix it.
Post-processing is the only reliable lever. (My first hypothesis was wrong;
the probe is why this plan is not built on it.)
Secondary finding: cap + `no_repeat_ngram_size:3` cut the 11-case suite from
5897ms to 3924ms. Strictly faster AND safer, so it goes in regardless.
## Existing machinery to build on (do not reinvent)
- `applyTable()` / `termMatcher()` in engine.js — a single-pass, longest-first
find-and-replace that already survived three real bugs: terms ending in
punctuation, CJK word boundaries, and case-sensitivity ("job" → "Job").
- `glossary.fixes` — source-side transcript corrections, operator-editable.
- `glossary.overrides[lang]` — target-side per-language pins. Currently `{}`.
- Both MT paths converge on `applyOverrides(text, lang)`:
`translateSentence()` (live, per sentence) and `translateAll()` (batch).
- `translateSentence` receives ONE sentence, so a 1-in-1-out clamp is safe there.
## The five layers
### L1 — source-side Bible vocabulary (before MT)
Built-in `fixes` for common Whisper mishearings of scripture vocabulary
("have a cook" → "Habakkuk" already exists as the only entry). Ship a real
list. Must NOT clobber the operator's own fixes.
### L2 — generation parameters (mt-service.js)
`max_new_tokens` derived from input token count, `no_repeat_ngram_size: 3`.
Measured: faster and safer. Does NOT fix the hallucination — that is L3.
### L3 — post-MT hallucination guard ← the fix for the actual complaint
Applied to every language, no per-language data needed:
1. **Invented-question rule.** Source has no `?`/`?`, output contains `?`/`?`
→ the question is hallucinated. Drop the offending clause. This is the
direct fix for `¿Qué es eso?`. Precision is high: real scripture questions
carry a `?` in the source.
2. **Sentence-count clamp.** N sentence terminators in → at most N out
(min 1). Kills appended-sentence hallucination generally.
3. **Subtitle-dash strip.** NLLB emits leading `- ` / `– ` from its subtitle
training data.
4. **Length-ratio guard.** Output wildly longer than input → keep first
sentence only. Needs a per-script ratio (CJK is far shorter per character).
### L4 — Bible lexicon, target side (per language)
Canonical forms for the 66 book names, ~120 proper nouns (people, places),
and key doctrinal terms. Applied through `applyTable` — the machinery is
already hardened. Built-in table, SEPARATE from operator overrides, with the
operator's entries winning so an app update never clobbers their work.
**Honesty constraint:** ship a language only where the canonical forms are
verifiable. A wrong Korean Bible name is worse than NLLB's output. Structure
the data so unverified languages simply have no entry and fall through to NLLB.
### L5 — liturgical phrasebook (skip MT entirely)
`Amen.` must never reach NLLB. Short liturgical constants — Amen, Hallelujah,
Hosanna, Glory to God, Praise the Lord — have stable known forms and are the
highest-frequency utterances in preaching. Direct lookup, no inference. This
is also the class that produces the worst failures, because NLLB has almost no
context to work from on a 1-3 word input.
## What must not regress
- Latency: EN 2-4s / ES 4-7s worst case. Every layer here is string work except
L2, which is faster. No layer may add an inference pass.
- `mtWorkers` stays 1 (benchmarked: 2 workers = 0.92x, slower).
- Portuguese variants (`PtVariants`) run BEFORE `applyOverrides`; ordering with
the new lexicon must be decided explicitly.
- Operator's saved glossary must survive app updates.
- `por_PT` is a pseudo-code aliased to `por_Latn` at the MT boundary only.
## Open questions for the audit
1. Does the invented-question rule ever fire on legitimate output? Spanish
inverts questions (`¿…?`); other languages may add a question particle where
English used intonation with no `?`.
2. Ordering: lexicon before or after the hallucination guard? Before or after
PtVariants?
3. Can Whisper be biased toward scripture vocabulary in transformers.js
(an initial prompt), or is L1 find-and-replace the only available lever?
4. Is a length-ratio guard safe across scripts, or does it need per-script
calibration that we cannot verify for 200 languages?
5. Which languages can we honestly ship a lexicon for, and how should the
unverified ones behave?