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
+94
View File
@@ -0,0 +1,94 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Translation — Poster</title>
<style>
:root { --ink: #10131a; --muted: #5b6478; }
* { box-sizing: border-box; }
body {
margin: 0; background: #f4f5f8; color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
display: grid; place-items: center; min-height: 100vh; padding: 1.5rem;
}
.sheet {
width: 100%; max-width: 46rem; aspect-ratio: 8.5 / 11;
background: #fff; border-radius: 12px; box-shadow: 0 1.5rem 3rem rgba(0,0,0,.12);
padding: 3.5rem 3rem; display: flex; flex-direction: column; align-items: center;
text-align: center; gap: 1.25rem;
}
.church { font-size: .875rem; letter-spacing: .18em; text-transform: uppercase; color: var(--muted); }
h1 { font-size: 2.75rem; line-height: 1.1; margin: 0; font-weight: 750; letter-spacing: -.01em; }
.sub { font-size: 1.25rem; color: var(--muted); margin: 0; }
.qr { width: 17rem; height: 17rem; margin: .5rem 0; }
.url { font-size: 1.125rem; font-weight: 600; letter-spacing: .01em; word-break: break-all; }
.langs {
display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem .875rem;
font-size: 1.0625rem; color: var(--ink); margin-top: auto; padding-top: 1.25rem;
border-top: 1px solid #e3e6ec; width: 100%;
}
.langs span { white-space: nowrap; }
.foot { font-size: .8125rem; color: var(--muted); }
.bar { max-width: 46rem; width: 100%; display: flex; gap: .5rem; justify-content: flex-end; margin-bottom: .75rem; }
.bar button {
border: 1px solid #d3d8e2; background: #fff; color: var(--ink);
padding: .5rem 1rem; border-radius: 8px; font: inherit; font-size: .875rem; cursor: pointer;
}
@media print {
body { background: #fff; padding: 0; display: block; }
.bar { display: none; }
.sheet { box-shadow: none; border-radius: 0; max-width: none; width: 100%; aspect-ratio: auto; min-height: 100vh; }
}
</style>
</head>
<body>
<div class="bar"><button onclick="window.print()">Print</button></div>
<div class="sheet">
<div class="church" id="church">—</div>
<h1 id="head">Listen in your language</h1>
<p class="sub" id="sub">Scan with your phone camera</p>
<img class="qr" id="qr" alt="QR code">
<div class="url" id="url">—</div>
<div class="langs" id="langs"></div>
<div class="foot">Free · No app to download · Works on any phone</div>
</div>
<script src="/languages.js"></script>
<script>
(function () {
var params = new URLSearchParams(location.search);
var pin = params.get('pin');
/* "Listen in your language" written in the languages most likely to be in the
room — a poster nobody can read is just decoration. */
var INVITES = ['Escucha en tu idioma', '用您的语言收听', 'Écoutez dans votre langue',
'Ouça no seu idioma', '당신의 언어로 들으세요', 'Nghe bằng ngôn ngữ của bạn',
'Koute nan lang ou', 'استمع بلغتك', 'Слухайте своєю мовою', 'Sikiliza kwa lugha yako'];
function render(cfg, base) {
document.getElementById('church').textContent = cfg.serviceName || '';
document.getElementById('url').textContent = base.replace(/^https?:\/\//, '');
document.getElementById('qr').src = '/qr.svg?url=' + encodeURIComponent(base + '/');
var box = document.getElementById('langs');
box.textContent = '';
INVITES.forEach(function (t) {
var s = document.createElement('span');
s.textContent = t;
box.appendChild(s);
});
}
var url = pin ? '/api/config' : '/api/languages';
var opts = pin ? { headers: { 'X-Operator-Pin': pin } } : {};
fetch(url, opts).then(function (r) { return r.json(); }).then(function (j) {
var cfg = j.config || j;
var base = (cfg.publicUrl || j.publicBase || location.origin).replace(/\/+$/, '');
render(cfg, base);
}).catch(function () {
render({ serviceName: '' }, location.origin);
});
})();
</script>
</body>
</html>