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>
This commit is contained in:
+113
@@ -0,0 +1,113 @@
|
||||
# Getting the pastor's mic into the Mac
|
||||
|
||||
The engine takes **any CoreAudio input**. It does not care how the audio got
|
||||
there. Pick whichever of these matches the room, and it shows up in the
|
||||
**Sermon audio → Input device** list like any other sound card.
|
||||
|
||||
---
|
||||
|
||||
## The one rule
|
||||
|
||||
Feed it **the pastor's microphone**, not the main mix.
|
||||
|
||||
A direct out, a matrix, an aux, a bus — anything that is that one voice. Music,
|
||||
congregational singing and room noise all make the transcription worse, and the
|
||||
voice-detection has to work much harder to find the speech.
|
||||
|
||||
Post-EQ and post-compression is good. Post-fader is fine. Post-reverb is not.
|
||||
|
||||
---
|
||||
|
||||
## Dante
|
||||
|
||||
Dante Controller is already installed on this Mac, but a Mac cannot receive
|
||||
Dante audio without something to receive it *with*. Two options:
|
||||
|
||||
### Dante Virtual Soundcard (about $50, one time)
|
||||
|
||||
The normal answer. It turns the Mac itself into a Dante device.
|
||||
|
||||
1. Buy and install **Dante Virtual Soundcard** from audinate.com.
|
||||
2. Start it. Set it to **2 channels**, sample rate **48 kHz**.
|
||||
3. Open **Dante Controller**, find the row for this Mac, and subscribe
|
||||
channel 1 to the pastor's mic transmit channel.
|
||||
4. In the engine, pick **Dante Virtual Soundcard (1-2)**.
|
||||
|
||||
There is no subscription. It is the only money this whole system costs.
|
||||
|
||||
### RedNet hardware you may already own
|
||||
|
||||
This Mac has **RedNet Control 2** and the Focusrite PCIe driver installed, but
|
||||
no RedNet device is currently attached. If there is a RedNet box or a PCIeNX
|
||||
card in a Thunderbolt chassis somewhere in the building, reconnecting it gives
|
||||
you Dante input for free and with lower latency than DVS.
|
||||
|
||||
---
|
||||
|
||||
## Waves SoundGrid
|
||||
|
||||
For the locations on a Waves network instead of Dante.
|
||||
|
||||
1. Install **Waves SoundGrid Studio** (free from waves.com) on the Mac.
|
||||
2. Connect the Mac to the SoundGrid network. SoundGrid Studio will find the
|
||||
server and I/O.
|
||||
3. In SoundGrid Studio, patch the pastor's mic channel to the **driver** —
|
||||
that is the thing that appears to macOS as a sound card.
|
||||
4. In the engine, pick **Waves SoundGrid** as the input device.
|
||||
|
||||
The Mac needs a wired Ethernet connection to the SoundGrid network, and it
|
||||
should be a **separate** interface from the one reaching the NAS if the two
|
||||
networks are separate VLANs.
|
||||
|
||||
---
|
||||
|
||||
## Console USB (free, works today)
|
||||
|
||||
Your M32 does 32 channels of USB audio to a Mac with no extra hardware.
|
||||
|
||||
1. USB cable from the console's **USB** port to the Mac.
|
||||
2. On the console, route the pastor's mic channel to a USB send.
|
||||
3. In the engine, pick the console as the input device.
|
||||
|
||||
Least elegant, zero cost, and completely reliable. A good way to prove the whole
|
||||
system works before spending anything on Dante.
|
||||
|
||||
---
|
||||
|
||||
## BlackHole (for testing)
|
||||
|
||||
BlackHole 2ch is already installed. Anything playing on the Mac can be routed
|
||||
into the engine through it, which is useful for rehearsing with a recording of
|
||||
last week's sermon before you trust it live.
|
||||
|
||||
---
|
||||
|
||||
## If Dante is not in the list
|
||||
|
||||
**Restart the app.** Chromium reads the audio devices when it starts, and if
|
||||
Dante Virtual Soundcard was launched afterwards it will not be there. The app
|
||||
now watches for devices appearing and picks them up on its own, but a restart is
|
||||
the certain fix.
|
||||
|
||||
Also worth checking: DVS running, set to Core Audio, and at least one channel
|
||||
subscribed in Dante Controller. If the app lists the device but the meter never
|
||||
moves, the device is there and the subscription is not.
|
||||
|
||||
## Level
|
||||
|
||||
Aim for speech peaking around **-20 dB** on the app's meter. Measured on this
|
||||
rig the Dante feed peaked at -44 dB, which works but leaves only about 14 dB
|
||||
above the speech/silence line — quiet passages can slip under it. Raising the
|
||||
send at the console is better than winding Sensitivity down, because Sensitivity
|
||||
lowers the bar for room noise too.
|
||||
|
||||
## Checking it works
|
||||
|
||||
The engine has a level meter with a notch on it.
|
||||
|
||||
- The **bar** is the incoming level.
|
||||
- The **notch** is the line between silence and speech.
|
||||
|
||||
Speech should push the bar clearly past the notch and turn it green. Room noise
|
||||
between sentences should sit below it. If it doesn't, move the **Sensitivity**
|
||||
slider — left if quiet speech is being missed, right if noise is triggering it.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Stream Deck / Bitfocus Companion
|
||||
|
||||
One button starts and stops translation. That is the whole integration.
|
||||
|
||||
---
|
||||
|
||||
## The button
|
||||
|
||||
In Companion:
|
||||
|
||||
1. **Connections** → add **Generic HTTP**. No settings needed.
|
||||
2. Make a button. Add a **press action**: *Generic HTTP → GET*.
|
||||
3. Paste the toggle URL from the operator page (**Stream Deck / Companion**
|
||||
section — there is a Copy button):
|
||||
|
||||
```
|
||||
http://127.0.0.1:3100/api/control/toggle?key=YOUR_TOKEN
|
||||
```
|
||||
|
||||
Use `127.0.0.1` when Companion runs on the same Mac as the app, which is the
|
||||
normal case. If Companion is on a different machine, use that Mac's LAN address
|
||||
instead — the app shows it under *What the congregation opens*.
|
||||
|
||||
**The token is per-campus.** Copy it from the operator page → *Stream Deck /
|
||||
Companion*, which has a copy button for exactly this.
|
||||
|
||||
Set the button text to **TRANSLATE**. Done.
|
||||
|
||||
Separate start and stop URLs are on the operator page too if you'd rather have
|
||||
two buttons than one toggle.
|
||||
|
||||
> The key is a password. Anyone who has it can start and stop translation for
|
||||
> the whole congregation. Don't put it on a slide, and don't paste it into a
|
||||
> group chat.
|
||||
|
||||
---
|
||||
|
||||
## Making the button change colour
|
||||
|
||||
Worth doing — a volunteer should be able to tell at a glance whether it is
|
||||
running.
|
||||
|
||||
The server pushes its state into a Companion **custom variable**, so the button
|
||||
is always right the moment anything changes, including when translation was
|
||||
started from the operator page or stopped because the Mac engine quit.
|
||||
|
||||
**First, switch Companion's HTTP API on.** It ships disabled — checked on your
|
||||
Companion 5.0.4 and every API endpoint returns 404 until you enable it. In
|
||||
Companion: **Settings → HTTP API → enable**. (The button itself works without
|
||||
this; only the colour feedback needs it, because that is the server calling
|
||||
*into* Companion rather than Companion calling out.)
|
||||
|
||||
**Then:** Variables → Custom Variables → create one called `translate_state`.
|
||||
|
||||
**On the operator page:** set **Companion address**. When Companion runs on the
|
||||
same Mac as the app — the normal case — that is `http://127.0.0.1:8000`.
|
||||
|
||||
The variable will then hold one of:
|
||||
|
||||
| Value | Meaning | Suggested colour |
|
||||
| --- | --- | --- |
|
||||
| `live` | Translating right now | Red |
|
||||
| `ready` | Engine connected, standing by | Green |
|
||||
| `offline` | The Mac engine is not running | Dark grey / amber |
|
||||
|
||||
Add feedbacks on the button: *Internal → Custom variable value* — one rule per
|
||||
value, each setting the background colour.
|
||||
|
||||
`offline` is the useful one. It tells the volunteer the Mac app is closed
|
||||
**before** the sermon starts, instead of after.
|
||||
|
||||
---
|
||||
|
||||
## If you'd rather poll
|
||||
|
||||
There is also a plain-text endpoint that returns exactly one word:
|
||||
|
||||
```
|
||||
http://127.0.0.1:3100/api/status.txt?key=YOUR_TOKEN
|
||||
```
|
||||
|
||||
Returns `live`, `ready`, or `offline`. Use it with any control system that can
|
||||
fetch a string — it needs no JSON parsing.
|
||||
|
||||
---
|
||||
|
||||
## Other control systems
|
||||
|
||||
Everything is plain HTTP GET, so anything that can fetch a URL can drive this:
|
||||
a browser bookmark, a shortcut on a phone, an AMX/Crestron panel, a `curl` in a
|
||||
script.
|
||||
|
||||
```bash
|
||||
curl "http://127.0.0.1:3100/api/control/start?key=YOUR_TOKEN"
|
||||
curl "http://127.0.0.1:3100/api/control/stop?key=YOUR_TOKEN"
|
||||
```
|
||||
|
||||
Both return JSON describing the new state, including how many people are
|
||||
listening and in which languages.
|
||||
@@ -0,0 +1,105 @@
|
||||
# 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?
|
||||
@@ -0,0 +1,77 @@
|
||||
# Plan — side-by-side language columns with a real move animation
|
||||
|
||||
## The ask
|
||||
"Make this portion side by side with one side being the active languages and the
|
||||
other side being the disabled languages. There needs to be a nice animation of
|
||||
the languages moving sides, so that they don't seem like they are teleporting
|
||||
instantly to the other side."
|
||||
|
||||
## What exists now
|
||||
One scrolling list of 105 rows, each a `<label class="langrow">` with a
|
||||
checkbox, English name, native name and a voice/captions tag. Toggling calls
|
||||
`toggleFeatured()` which mutates `opConfig.featured`, re-renders the whole list
|
||||
with `renderOpLangList()`, and debounces a save. The row does not move — only
|
||||
its checkbox state changes. Filter and "with a voice only" narrow the list;
|
||||
"Turn all off" empties it.
|
||||
|
||||
## Design
|
||||
|
||||
┌─ ON (3) ───────────────┐ ┌─ OFF (102) ─────────────┐
|
||||
│ Español (América Latina)│ │ Afrikaans │
|
||||
│ Português (Brasil) │ │ Albanian Shqip │
|
||||
│ Português (Portugal) │ │ Amharic አማርኛ │
|
||||
└─────────────────────────┘ └─────────────────────────┘
|
||||
|
||||
- Two columns, each independently scrollable, each with a count in its header.
|
||||
- One filter box and one "with a voice only" toggle drive BOTH columns.
|
||||
- Clicking a row moves it to the other column. The checkbox goes away: in a
|
||||
two-column layout the column IS the state, and a checkbox next to it would
|
||||
say the same thing twice.
|
||||
- Empty column shows a short instruction rather than blank space.
|
||||
|
||||
## The animation — FLIP, because it is the only technique that is not a lie
|
||||
|
||||
Naively re-rendering makes a row vanish from one column and appear in the other:
|
||||
the "teleporting" the ask names. FLIP animates the real transition:
|
||||
|
||||
1. **First** — before mutating state, record `getBoundingClientRect()` for every
|
||||
currently visible row, keyed by language code.
|
||||
2. **Last** — mutate and re-render. Rows are now in their final positions.
|
||||
3. **Invert** — for each row that both existed before and exists now, apply
|
||||
`transform: translate(dx, dy)` putting it visually back where it was.
|
||||
4. **Play** — on the next frame, clear the transform with a transition. The row
|
||||
travels from its old position to its new one.
|
||||
|
||||
Constraints taken from the UX guidance:
|
||||
- **transform/opacity ONLY.** Never animate width/height/top/left — they force
|
||||
layout on every frame.
|
||||
- **Batch reads then writes.** 105 rows × 2 measurements will thrash layout if
|
||||
interleaved. All reads first, then all writes.
|
||||
- **Only animate rows that are actually on screen.** A row scrolled far out of
|
||||
view should not fly hundreds of pixels; it just appears in place.
|
||||
- **Respect `prefers-reduced-motion`** — skip the animation entirely, keep the
|
||||
move. Rated High severity.
|
||||
- **Interruptible.** Clicking three languages quickly must not queue three
|
||||
animations or leave a row stuck mid-flight.
|
||||
- **~260ms, ease-out**, matching the app's existing `paneIn` feel.
|
||||
- The row in flight gets a raised z-index so it passes over its neighbours
|
||||
rather than under them.
|
||||
|
||||
## What must not regress
|
||||
- `toggleFeatured()`'s optimistic update + 500ms debounced save, and its revert
|
||||
on failure. Ticking twenty languages must stay one write.
|
||||
- The filter, the "with a voice only" toggle, "Turn all off", and the count line.
|
||||
- `opConfig.featured` stays the single source of truth; the columns are a view.
|
||||
- Alphabetical order inside each column, and the voice/captions tag per row.
|
||||
- No horizontal overflow — the card is ~470px wide inside the settings column.
|
||||
|
||||
## Open questions for the audit
|
||||
1. FLIP across two DIFFERENT scroll containers — what breaks?
|
||||
2. What happens when the destination column is scrolled such that the row's
|
||||
final position is outside the visible area?
|
||||
3. Rapid clicking: how do in-flight transforms interact with a re-render?
|
||||
4. Is 105 rows × 2 rect reads per toggle acceptable on this machine?
|
||||
5. Does removing the checkbox lose anything — keyboard operability, screen
|
||||
reader state, the "I can see what's off" affordance?
|
||||
6. Two independently scrolling columns inside a page that also scrolls — does
|
||||
that create a scroll trap?
|
||||
@@ -0,0 +1,90 @@
|
||||
# Plan — three Spanish variants for live preaching
|
||||
|
||||
## The ask
|
||||
Operator, relaying a native Spanish speaker: the Spanish output is Latin
|
||||
American, and three variants are wanted — **Latin American, Spain, Mexican** —
|
||||
"fine tuned exactly to how it's supposed to be". No native speaker will be
|
||||
provided; research from the web instead. Nothing may be left out.
|
||||
|
||||
## Finding 1 — the taxonomy overlaps
|
||||
es-MX is INSIDE es-419, not beside it. Their differences are lexical only
|
||||
(*computadora/ordenador*, *carro/coche*, *ícono/icono*). es-ES is the one that
|
||||
differs GRAMMATICALLY. So the three deliverables are not three equal problems:
|
||||
|
||||
| variant | differs from es-419 by | difficulty |
|
||||
|---|---|---|
|
||||
| es_419 Latin American | — (this is today's output) | none |
|
||||
| es_MX Mexican | word choice | low, deterministic |
|
||||
| es_ES Spain | **the whole 2nd-person-plural system** | high |
|
||||
|
||||
Reference editions, so each variant has an authority rather than my preference:
|
||||
- **es_ES** → Reina-Valera 1960 (uses `vosotros`) / LBLA
|
||||
- **es_419** → NBLA (explicitly uses `ustedes` for Latin America) / NVI
|
||||
- **es_MX** → NBLA + Mexican lexicon
|
||||
|
||||
## Finding 2 — why es-ES cannot be a find-and-replace
|
||||
Spanish 3rd-person-plural is IDENTICAL for "ustedes" (you all) and "ellos"
|
||||
(they). `hablan` is both. So a target-side rule `hablan → habláis` corrupts
|
||||
every genuine third-person sentence: "they were there" becomes "you all were
|
||||
there". In scripture. Silently. This is the exact failure class the whole
|
||||
fidelity effort exists to prevent, so a naive transform is not shippable.
|
||||
|
||||
## Finding 3 — the English source resolves the ambiguity
|
||||
This is the key idea and it may remove the need for any model change.
|
||||
|
||||
The ambiguity is unresolvable in the Spanish. It is already resolved in the
|
||||
ENGLISH, which we still have:
|
||||
|
||||
"Open YOUR Bibles." -> 2nd person -> ustedes-form -> convert
|
||||
"THEY were there." -> 3rd person -> ellos-form -> leave alone
|
||||
|
||||
So: convert to `vosotros` ONLY when the English sentence contains 2nd-person
|
||||
address AND contains NO 3rd-person-plural marker. Mixed sentences ("tell them
|
||||
that you are loved") are left in es-419 form — which a Spaniard understands
|
||||
perfectly, it just reads Latin American. **Conservative by construction: the
|
||||
failure mode is "not converted", never "wrongly converted".**
|
||||
|
||||
## Design
|
||||
|
||||
NLLB -> guard -> cache -> [es-419 base]
|
||||
|-- es_MX : lexical layer
|
||||
|-- es_ES : gated vosotros transform
|
||||
`-- es_419: as-is
|
||||
|
||||
Same shape as `pt-variants.js` (por_Latn / por_PT share one NLLB result), and it
|
||||
reuses `NLLB_ALIAS` so the extra variants cost no extra inference.
|
||||
|
||||
New pseudo-codes `spa_MX` and `spa_ES`, both aliased to `spa_Latn` at the MT
|
||||
boundary only — exactly how `por_PT` already works.
|
||||
|
||||
### es_ES transform components
|
||||
1. **Gate** (on the English source): 2nd-person markers present, 3rd-person-plural
|
||||
markers absent.
|
||||
2. **Pronouns/possessives**: ustedes→vosotros, su(s)→vuestro(s)/vuestra(s),
|
||||
les→os, se (reflexive 2pl)→os, a ustedes→a vosotros.
|
||||
3. **Verb morphology**: 3pl→2pl. `-an→-áis`; `-en→-éis` for -er verbs but
|
||||
`-en→-ís` for -ir verbs, so the verb's class must be known — a table, not a
|
||||
rule. Irregulars (ser, ir, tener, hacer, poder, querer, decir, venir, saber,
|
||||
dar, ver, poner, salir, traer, oír, haber) need explicit entries.
|
||||
4. **Imperatives**: `abran→abrid`, `vengan→venid`, `miren→mirad` — highest
|
||||
frequency in preaching and the most audible difference.
|
||||
|
||||
### es_MX layer
|
||||
Lexical only. Word-level, no morphology, no gate needed.
|
||||
|
||||
## What must not regress
|
||||
- Latency: no extra inference. All layers are string work over a shared result.
|
||||
- `applyOverrides` still runs last so an operator pin outranks everything.
|
||||
- The English caption and archive keep what was actually said.
|
||||
- Voice: `speech-text.js` has Spanish book-name rules (`ES_BOOKS`) that fire on
|
||||
`1 Corintios` → `Primera de Corintios`. New variants must not produce forms
|
||||
outside that list or the voice reads "uno Juan".
|
||||
- Each variant needs its own voice choice — es_ES should get a Castilian voice
|
||||
(miro/sharvard, measured male), es_MX the Mexican `ald`.
|
||||
|
||||
## Open questions for the audit
|
||||
1. Is the source-side gate actually safe? Where does it misfire?
|
||||
2. Verb-table coverage: what fraction of real sermon Spanish is reachable?
|
||||
3. Does adding two pseudo-languages break the shared-cache invariant?
|
||||
4. Is `vosotros` even wanted? RVR1960 uses it and is read across Latin America.
|
||||
5. Does the listener page cope with three Spanish entries in the picker?
|
||||
@@ -0,0 +1,115 @@
|
||||
# Letting people off-site watch
|
||||
|
||||
**Optional.** Everything works on the building's WiFi without any of this. Read
|
||||
on only if you want people on cell data, or watching from home, to follow along.
|
||||
|
||||
Because each campus runs its own copy, this is a per-campus decision — you can
|
||||
publish one campus and not another.
|
||||
|
||||
---
|
||||
|
||||
## The easy way: Cloudflare Tunnel
|
||||
|
||||
Free, needs no port forwarding, no firewall changes, no certificate, and no
|
||||
static IP. It runs on the same Mac as the app.
|
||||
|
||||
1. **Install cloudflared**
|
||||
|
||||
```bash
|
||||
brew install cloudflared
|
||||
```
|
||||
|
||||
No Homebrew on that Mac? Download the `cloudflared-darwin-arm64.tgz` binary
|
||||
from Cloudflare's releases page and drop it in `/usr/local/bin`.
|
||||
|
||||
2. **Log in and create the tunnel**
|
||||
|
||||
```bash
|
||||
cloudflared tunnel login
|
||||
cloudflared tunnel create translate-main
|
||||
```
|
||||
|
||||
3. **Point a hostname at the app**
|
||||
|
||||
```bash
|
||||
cloudflared tunnel route dns translate-main translate.watersmedia.org
|
||||
```
|
||||
|
||||
4. **Run it**
|
||||
|
||||
```bash
|
||||
cloudflared tunnel run --url http://127.0.0.1:3100 translate-main
|
||||
```
|
||||
|
||||
To keep it running after a reboot, `sudo cloudflared service install`.
|
||||
|
||||
5. **Tell the app its public name**
|
||||
|
||||
Operator page → **Public address** → `https://translate.watersmedia.org` →
|
||||
Save. That is what the QR code encodes, so it must be the public name or the
|
||||
code only works for people already on your WiFi.
|
||||
|
||||
WebSockets pass through Cloudflare tunnels with nothing extra to configure,
|
||||
which is the main reason this is the lower-risk option.
|
||||
|
||||
For a second campus, repeat with its own name — `translate-north`,
|
||||
`north.translate.watersmedia.org`.
|
||||
|
||||
---
|
||||
|
||||
## The other way: your existing nginx proxy
|
||||
|
||||
You already run a reverse proxy at `10.1.1.4` fronting `check.watersmedia.org`,
|
||||
with split-horizon DNS. It can front this too, though it means the traffic goes
|
||||
through the NAS network to reach the booth Mac.
|
||||
|
||||
Add a DNS record for `translate.watersmedia.org` — public to your WAN address,
|
||||
internal to `10.1.1.4` — and a server block. Copy the one that already serves
|
||||
the checklist app, change the name, and point it at the **booth Mac**, not the
|
||||
NAS:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name translate.watersmedia.org;
|
||||
|
||||
ssl_certificate /path/to/fullchain.pem;
|
||||
ssl_certificate_key /path/to/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://10.6.3.222:3100; # the booth Mac
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
# ---- required: captions ride a WebSocket ----
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# A sermon is long and mostly silent between sentences. Without this the
|
||||
# proxy closes the caption socket after 60s of quiet and every phone in
|
||||
# the room reconnects at once.
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This has one real drawback the tunnel does not: the booth Mac needs a fixed
|
||||
address on the network, or the proxy breaks the next time DHCP moves it.
|
||||
|
||||
---
|
||||
|
||||
## Checking it worked
|
||||
|
||||
```bash
|
||||
curl -s https://translate.watersmedia.org/healthz
|
||||
```
|
||||
|
||||
Should return `{"ok":true,…}`. Then open the page on a phone **with WiFi turned
|
||||
off** — that is the real test. If the page loads but captions never appear, the
|
||||
WebSocket headers are missing.
|
||||
@@ -0,0 +1,88 @@
|
||||
# Setting it up at a campus
|
||||
|
||||
Once per Mac. Most of the time is the app downloading its models.
|
||||
|
||||
There is no server to deploy, nothing to configure on a NAS, and no address to
|
||||
type in. The app carries everything.
|
||||
|
||||
---
|
||||
|
||||
## 1. Install the app
|
||||
|
||||
Copy **Church Translate.app** to `/Applications` on the campus Mac and open it.
|
||||
|
||||
Two things happen on a first run and never again:
|
||||
|
||||
- **macOS asks for microphone access.** Click Allow. Without it the app can see
|
||||
audio devices but hears silence from all of them.
|
||||
- **It downloads about 1.5 GB of models.** Leave it running; the progress bar is
|
||||
approximate because Hugging Face does not send a size for every file. After
|
||||
this, opening the app takes seconds.
|
||||
|
||||
The box at the top turns blue and says **Ready** when it is done.
|
||||
|
||||
> The app is unsigned, so the first open needs **right-click → Open** rather
|
||||
> than a double-click. macOS only asks once.
|
||||
|
||||
## 2. Point it at the pastor's microphone
|
||||
|
||||
**Sermon audio → Input device.** Dante Virtual Soundcard, Waves SoundGrid and a
|
||||
console USB feed all appear here as ordinary inputs — see [AUDIO.md](AUDIO.md).
|
||||
|
||||
Watch the meter while someone talks. Speech should push the bar past the notch
|
||||
and turn it green; room noise between sentences should sit below it. The
|
||||
**Sensitivity** slider moves the notch.
|
||||
|
||||
## 3. Tell the congregation where to go
|
||||
|
||||
The **What the congregation opens** panel shows every address this Mac can be
|
||||
reached at. If the Mac is on more than one network, click the address that is on
|
||||
the same network as people's phones — the QR code follows your choice.
|
||||
|
||||
Then: **Printable poster** → print it → put it where people sit.
|
||||
|
||||
## 4. Add the Stream Deck button
|
||||
|
||||
One Generic HTTP action. See [COMPANION.md](COMPANION.md).
|
||||
|
||||
## 5. Before you announce it
|
||||
|
||||
- Press **START TRANSLATING** on the operator page and talk into the mic. Watch
|
||||
the words appear under *What it is hearing* in the app. If those words are
|
||||
right, everything downstream is right.
|
||||
- Scan the QR with your own phone, pick a language, confirm captions arrive.
|
||||
- Tap the speaker icon and confirm your phone reads them aloud.
|
||||
- Add anything it mishears to **Corrections** — your pastor's name, your church's
|
||||
name, the book of Habakkuk.
|
||||
|
||||
---
|
||||
|
||||
## Where things live
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| The app | `/Applications/Church Translate.app` |
|
||||
| Config, PIN, transcripts | `~/Library/Application Support/Church Translate/data/` |
|
||||
| Model weights (~7 GB) | `~/Library/Application Support/Church Translate Models/` |
|
||||
| Opens at login | `~/Library/LaunchAgents/org.waterschurch.translate.plist` |
|
||||
|
||||
To stop it opening at login:
|
||||
|
||||
```bash
|
||||
launchctl unload ~/Library/LaunchAgents/org.waterschurch.translate.plist
|
||||
```
|
||||
|
||||
## Another campus
|
||||
|
||||
Copy the same `.app` across and repeat. Each campus is completely independent —
|
||||
its own settings, its own PIN, its own transcripts. Nothing syncs and nothing
|
||||
depends on anything else being up.
|
||||
|
||||
The models download again on each new Mac. If bandwidth is tight, copy
|
||||
`~/Library/Application Support/Church Translate Models/` across by hand and the
|
||||
app will use it as-is.
|
||||
|
||||
## What it costs per campus
|
||||
|
||||
Nothing recurring. Dante Virtual Soundcard is about $50 once, and only if you go
|
||||
the Dante route — Waves SoundGrid and a console USB feed cost nothing.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Sunday morning
|
||||
|
||||
Print this. Tape it to the sound desk. It is the whole job.
|
||||
|
||||
---
|
||||
|
||||
## Before the service
|
||||
|
||||
1. **On the booth Mac**, check **Church Translate** is open.
|
||||
It starts by itself when the Mac logs in, so usually it already is.
|
||||
Wait until the box at the top turns blue and says **Ready**.
|
||||
- If it says *Downloading the language models*, leave it alone — that only
|
||||
happens the first time, and after that it starts in a few seconds.
|
||||
2. Check the **green bar moves** when someone talks into the pastor's mic.
|
||||
That is the only audio check that matters.
|
||||
3. Leave the app open. Minimise it if you like. **Do not quit it.**
|
||||
|
||||
## When the sermon starts
|
||||
|
||||
Press the **TRANSLATE** button on the Stream Deck.
|
||||
|
||||
That is it. The button turns red while it is running.
|
||||
|
||||
## When the sermon ends
|
||||
|
||||
Press it again. It goes back to green.
|
||||
|
||||
---
|
||||
|
||||
## The addresses
|
||||
|
||||
Open the app and look at **What the congregation opens** — it shows the address
|
||||
and the QR code, and the buttons there open the operator page and the poster.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Operator page | the **Operator page** button in the app |
|
||||
| Operator PIN | shown in the app, under the QR code |
|
||||
| What congregants scan | the **Printable poster** button in the app |
|
||||
|
||||
## What the congregation does
|
||||
|
||||
They point their phone camera at the QR code and tap the link.
|
||||
Then they tap their language once. That is all — no app, no account, no sign-in.
|
||||
|
||||
Their phone remembers next week.
|
||||
|
||||
---
|
||||
|
||||
## If something looks wrong
|
||||
|
||||
| What you see | What to do |
|
||||
| --- | --- |
|
||||
| Operator page won't load at all | The app is closed. Open **Church Translate** on the booth Mac. |
|
||||
| Operator page says **Waiting for the Mac engine** | The app is still starting — give it a minute. |
|
||||
| Engine says **Ready** but nothing appears on phones | You haven't pressed the Stream Deck button. |
|
||||
| Green bar never moves | Wrong input picked. Change **Sermon audio → Input device**. |
|
||||
| Words are appearing but they're wrong | Check you picked the pastor's mic, not the room mic or the program mix. |
|
||||
| Someone's phone shows **Reconnecting…** | Their phone lost signal. It fixes itself. |
|
||||
| A name is spelled wrong every week | Operator page → **Corrections** → add `what it heard => what it should say` |
|
||||
|
||||
**Nobody needs to be at the Mac during the service.** If it was Ready before the
|
||||
service, it stays Ready.
|
||||
Reference in New Issue
Block a user