Build 126. Two reports, unrelated to each other. IPHONES REPEATING AND SKIPPING PARTS OF EVERY SENTENCE. DRIFT_LAND was 0.6 s and one segment is 0.697 s, so the position the player was being held at sat INSIDE the newest segment — a point it very often did not have yet. It got dragged to the edge, ran out of audio and stalled. Then 'stalled' and 'waiting' were both wired to seekLive(), and a seek on a live HLS stream makes the element re-buffer, which raises those same two events. So the correction caused the stall that caused the correction. On Safari, where HLS is native and a seek lands on a segment boundary, each turn of that loop replays or drops a piece of a sentence — which is exactly what was described. Three changes, all of them about not fighting the player: those two events no longer seek, the target is three segments back (~2.1 s, what an HLS player picks for itself on a live playlist), and a correction now has 5 s to settle before another may run. Costs about 1.5 s of extra delay. A listener a second further behind is following the service; one whose audio keeps jumping backwards is not. NOT YET CONFIRMED ON A REAL IPHONE — the mechanism is read off the code and the measured segment length, and the next service is the test. CANNOT SCROLL THE MONITOR WHILE LIVE. Four boxes ended their update with box.scrollTop = box.scrollHeight, unconditionally, so every sentence yanked the view back to the live edge and reading back was impossible. They now follow the bottom only while the operator is already at the bottom, and resume when they return — nothing to press either way. Trimming the top is held off while they are scrolled away too, since dropping old lines moves the text being read; the cap comes back the moment they return, with a ceiling so nothing grows all service. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Church Translate
Live sermon translation into 100+ languages. One Mac app per campus. No servers, no deployment, no subscription.
Congregants point a phone camera at a QR code, tap their language once, and read along — or let their phone read it aloud in their ear. No app, no account.
Built because Kaleo is $24–149/month and OneAccord starts at $150/month for five hours, and both are wrappers around cloud speech and translation APIs that an M-series Mac runs locally for nothing.
How it works
Everything is inside the one app. Install it on the Mac at a campus, point it at the pastor's microphone, and it serves the congregation page itself.
┌──────────────── one Mac, one campus ─────────────────┐
│ │
│ Dante ─┐ │
│ Waves ─┼─► audio in ──► voice detection │
│ M32 ───┘ │ │
│ ▼ │
│ Whisper (Mac GPU) │
│ │ │
│ NLLB-200 (separate process) │
│ │ │
│ built-in web server ──────────────┼──► phones on
│ │ the WiFi
└───────────────────────────────────────────────────────┘
No audio ever leaves the Mac, and nothing leaves the building unless you choose to publish it. Only finished sentences cross the network, so a full auditorium costs almost nothing to serve.
Two details that are not preference but necessity: translation runs in its own plain-Node child process, because Chromium's allocator refuses the model's gigabyte-sized weight blocks and kills the process; and speech recognition runs on the GPU while translation runs on the CPU, because neither fits in the other's runtime.
What it costs
| Speech recognition | Whisper large-v3-turbo, on the Mac's GPU — $0 |
| Translation, 100+ languages | NLLB-200, on the Mac's CPU — $0 |
| Spoken audio | The listener's own phone — $0, at any scale |
| Serving the page | The app itself — $0 |
| Dante input | Dante Virtual Soundcard, ~$50 once per campus — or free via Waves or console USB |
Performance
Measured on an M2 Max, from the end of a spoken sentence to captions on a phone. Partial text appears well before that, so it feels quicker than the numbers.
| Live preview | updates every 500–800 ms while the sentence is still being spoken |
| Finished sentence | ~1.6 s behind the speaker |
| Speech recognition | 4.9× real time (accurate model) |
| Translation | ~390 ms per language, flat over 240 consecutive runs |
| Memory | plateaus at 10–11 GB; no drift across a long service |
Two speech models run at once: a fast one drives the live preview, an accurate one produces the line the congregation keeps. They run on separate tracks, so the preview costs the finished caption almost nothing.
Getting started
- docs/SETUP.md — installing at a campus
- docs/AUDIO.md — Dante, Waves SoundGrid, console USB
- docs/COMPANION.md — the Stream Deck button
- docs/SUNDAY.md — print this and tape it to the desk
- docs/PUBLIC-ACCESS.md — optional, for people off-site
cd app && npm install && npm start # run from source
cd app && npm run dist # build the .app
Things worth knowing
- Only languages people are actually listening to get translated. Nobody on Icelandic means no CPU spent on Icelandic. Pin a language on the operator page to have it ready before the first person arrives.
- Corrections beat retraining. Operator page → Corrections →
have a cook => Habakkuk. Whole-word, case-insensitive, instant. - A late arrival sees the last 40 lines, so walking in halfway still makes sense.
- Transcripts are written per service, original and every translation, under
~/Library/Application Support/Church Translate/data/transcripts/. - Each campus is independent. Nothing is shared, nothing syncs, and one campus being down has no effect on another.