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:
@@ -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?
|
||||
Reference in New Issue
Block a user