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,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>
|
||||
Reference in New Issue
Block a user