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
+60
View File
@@ -0,0 +1,60 @@
<!doctype html><html lang="en"><head><meta charset="utf-8">
<title>group + channel CSS check</title><link rel="stylesheet" href="./engine.css"></head>
<body>
<header class="head"><div class="brand"><span class="mark"></span><span>Church&nbsp;Translate</span>
<span class="build" id="buildTag">3.11.0 · 75</span></div></header>
<main class="page">
<section class="hero" data-state="ready"><h1>Hero spans both columns</h1></section>
<div class="col-main">
<section class="group" id="g1" data-open="true">
<button class="group-head" type="button"><span class="chev"></span><h2>Input</h2>
<span class="group-sub">Dante Virtual Soundcard · ch 1</span></button>
<div class="group-body" id="g1b"><div class="group-inner">
<div class="chanbox"><div class="chanhead"><label style="margin:0">Channels</label>
<span class="hint" id="chanNote" style="margin:0">channel 1</span>
<button class="btn">Mix all</button></div>
<div class="chanlist" id="chanList"></div></div>
<p class="hint">Some content so the section has height.</p>
</div></div>
</section>
<section class="group" id="g2"><button class="group-head" type="button"><span class="chev"></span>
<h2>Languages and voices</h2><span class="group-sub">9 on · 1 pinned</span></button>
<div class="group-body" id="g2b"><div class="group-inner">
<p>Line</p><p>Line</p><p>Line</p><p>Line</p><p>Line</p><p>Line</p><p>Line</p><p>Line</p>
</div></div></section>
<section class="group" id="g3"><button class="group-head" type="button"><span class="chev"></span>
<h2>Settings</h2><span class="group-sub">no stream delay</span></button>
<div class="group-body"><div class="group-inner"><p>Line</p><p>Line</p></div></div></section>
<section class="card"><h2>Log</h2><div class="log"></div></section>
</div>
<aside class="col-rail"><section class="card rail-card"><h2>What the congregation opens</h2>
<div class="qr" style="width:11rem;height:11rem;background:#fff;margin:0 auto .625rem"></div>
<div class="railbtns"><button class="btn btn-primary">Copy address</button>
<button class="btn">Printable poster</button></div>
<h3 class="subhead">Operator page</h3>
<div class="codeblock"><code>http://10.6.3.222:3100/operator</code><button class="btn">Copy</button></div>
<button class="btn railwide">Open operator page</button></section></aside>
</main>
<script>
for (const sec of document.querySelectorAll('.group')) {
sec.querySelector('.group-head').addEventListener('click', () => {
sec.dataset.open = sec.dataset.open === 'true' ? 'false' : 'true';
});
}
const list = document.getElementById('chanList');
for (let i = 0; i < 16; i++) {
const r = document.createElement('button');
r.type = 'button'; r.className = 'chanrow' + (i === 0 ? ' on' : ''); r.dataset.ch = i;
r.innerHTML = '<b>' + (i+1) + '</b><span class="chanbar"><i class="chanbar-fill"></i></span>';
list.appendChild(r);
}
let t = 0;
setInterval(() => { t += .3;
[...list.children].forEach((row, i) => {
const v = i === 0 ? Math.abs(Math.sin(t)) : i === 5 ? Math.abs(Math.cos(t*1.7))*.6 : Math.random()*.05;
const f = row.querySelector('.chanbar-fill');
f.style.width = (v*100).toFixed(0) + '%'; f.classList.toggle('hot', v > .35);
});
}, 120);
</script>
</body></html>