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
+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?