christianandClaude Opus 5 7291ff48f7 Save QR image stranded the window on a picture of itself
Build 124. Reported from the booth: the button did not download anything and
there was no way back off the QR.

Both halves are the same fault. The window is served from the asset port and
the code comes off the congregation port, so the link the button built was
CROSS-ORIGIN — and a cross-origin <a download> has its download attribute
ignored and NAVIGATES instead. So the file never saved, and the window went to
the image. The app has no back button, no reload item and no address bar, so
that is a one-way trip: a headless booth Mac showing a full-screen QR with the
operator locked out of the desk. It fetches the code and downloads a blob now,
which is same-origin and actually saves.

The second change is the one that matters more. Nothing was stopping the app
window being replaced by any navigation at all, and the only reason this had
never bitten before is that nothing had tried. will-navigate now refuses to
replace the app page, logs what it refused, and sends anything external to the
browser. Our own pages are unaffected — the operator console and the poster
open as their own windows through setWindowOpenHandler, which is a different
path entirely.

Found because the build number went to the wrong field: buildNumber is the
source of truth and build.buildVersion is synced from it by predist, which
caught it and rebuilt correctly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 14:14:45 -04:00

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

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.
S
Description
Live sermon translation into 100+ languages. One Mac app per campus — captures audio, transcribes, translates, speaks, and serves the congregation page itself. No servers, no subscription.
Readme
6 MiB
2026-10-15 16:05:50 -04:00
Languages
JavaScript 90.2%
CSS 4.8%
HTML 4%
Shell 1%