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:
2026-09-23 15:41:21 -04:00
co-authored by Claude Opus 5
commit c9eef0672c
118 changed files with 36031 additions and 0 deletions
+113
View File
@@ -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.
+99
View File
@@ -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.
+105
View File
@@ -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?
+77
View File
@@ -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?
+90
View File
@@ -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?
+115
View File
@@ -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.
+88
View File
@@ -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.
+63
View File
@@ -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.