Files
church-translate/app/server/public/operator.html
T
christianandClaude Opus 5 c9eef0672c 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>
2026-09-23 15:41:21 -04:00

130 lines
5.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#0b0d12">
<meta name="color-scheme" content="dark">
<title>Translation — Operator</title>
<link rel="stylesheet" href="/operator.css">
</head>
<body>
<!-- PIN gate ------------------------------------------------------------- -->
<section id="gate" class="gate is-active">
<form id="gateForm" class="gate-card">
<h1>Operator</h1>
<p class="muted">Enter the PIN for this church's translation system.</p>
<input id="pin" type="password" inputmode="numeric" autocomplete="current-password"
placeholder="PIN" aria-label="Operator PIN" required>
<button type="submit" class="btn btn-primary">Unlock</button>
<p id="gateErr" class="err" hidden></p>
<p class="muted tiny">The PIN is in <code>data/OPERATOR-PIN.txt</code> on the NAS.</p>
</form>
</section>
<!-- Console -------------------------------------------------------------- -->
<main id="console" class="wrap">
<header class="top">
<div>
<div class="eyebrow">Live Translation</div>
<h1 id="svcName">—</h1>
</div>
<a class="ghost" href="/" target="_blank" rel="noopener">Listener view ↗</a>
</header>
<!-- The one control that matters on a Sunday morning. -->
<section class="card control">
<button id="bigBtn" class="big" data-live="false">
<span class="big-label">START TRANSLATING</span>
<span class="big-sub" id="bigSub">Ready</span>
</button>
<p class="op-health" id="opHealth" hidden></p>
<div class="stats">
<div class="stat"><b id="stListeners">0</b><span>listening</span></div>
<div class="stat"><b id="stLangs">0</b><span>languages</span></div>
<div class="stat"><b id="stElapsed">—</b><span>elapsed</span></div>
</div>
<!-- Who is actually reading, and in what. The total alone does not tell you
whether a language is worth keeping. -->
<div id="whoList" class="wholist" hidden></div>
<div id="engineWarn" class="warn" hidden>
<strong>The Mac engine is not connected.</strong>
<span>Open <em>Church Translate Engine</em> on the backstage Mac Studio. Nothing will translate until it is running.</span>
</div>
<div id="engineInfo" class="enginebar" hidden></div>
</section>
<!-- Languages ---------------------------------------------------------- -->
<section class="card">
<h2>Languages on the front page</h2>
<p class="muted">These show as big buttons before anyone searches. Everything else
is still reachable through search — all <b id="totalLangs">0</b> of them.</p>
<div id="featuredList" class="chips"></div>
<input id="langSearch" type="search" placeholder="Add a language…" autocomplete="off">
<div id="langResults" class="results"></div>
<h2 class="mt">Always translate</h2>
<p class="muted">Normally a language is only translated while someone is actually
listening to it. Pin one here to have it ready before the first person arrives.</p>
<div id="pinnedList" class="chips"></div>
</section>
<!-- Glossary ----------------------------------------------------------- -->
<section class="card">
<h2>Corrections</h2>
<p class="muted">One per line, in the form
<code>what it heard =&gt; what it should say</code>. This is how “have a cook”
becomes “Habakkuk” and your pastor’s name stops being a different word every
week. Matching ignores capitals and only hits whole words.</p>
<textarea id="bias" rows="7" spellcheck="false"></textarea>
<button id="saveGlossary" class="btn">Save corrections</button>
</section>
<!-- Setup -------------------------------------------------------------- -->
<section class="card">
<h2>The QR code</h2>
<div class="qrrow">
<img id="qr" class="qr" alt="QR code to the listener page">
<div class="qrside">
<label>Public address
<input id="publicUrl" type="url" placeholder="https://translate.yourchurch.org" spellcheck="false">
</label>
<p class="muted tiny">Leave blank to use whatever address this page was opened at.
Set it once the Cloudflare tunnel is up so the QR code always points at the public name.</p>
<div class="row">
<button id="savePublic" class="btn">Save</button>
<a id="posterLink" class="btn" href="/poster" target="_blank" rel="noopener">Printable poster ↗</a>
</div>
</div>
</div>
</section>
<section class="card">
<h2>Stream Deck / Companion</h2>
<p class="muted">Add a <b>Generic HTTP</b> action in Companion. Method GET, and one of:</p>
<div class="codeblock"><code id="urlToggle">—</code><button class="copy" data-copy="urlToggle">Copy</button></div>
<div class="codeblock"><code id="urlStart">—</code><button class="copy" data-copy="urlStart">Copy</button></div>
<div class="codeblock"><code id="urlStop">—</code><button class="copy" data-copy="urlStop">Copy</button></div>
<p class="muted tiny">Keep this key off the projector — anyone with it can start and stop translation.</p>
</section>
<section class="card">
<h2>Change the PIN</h2>
<div class="row">
<input id="newPin" type="password" placeholder="New PIN (4+ characters)" autocomplete="new-password">
<button id="savePin" class="btn">Update PIN</button>
</div>
</section>
<div id="toast" class="toast" hidden></div>
</main>
<script src="/languages.js"></script>
<script src="/operator.js"></script>
</body>
</html>