2026-09-23 15:41:21 -04:00
|
|
|
|
<!doctype html>
|
|
|
|
|
|
<html lang="en">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
|
<meta name="color-scheme" content="dark">
|
|
|
|
|
|
<!--
|
|
|
|
|
|
Model weights come from Hugging Face on first run. The congregation server is
|
|
|
|
|
|
this app's own child process on another localhost port, which is why img-src
|
|
|
|
|
|
admits 127.0.0.1 — that is where the QR code is rendered. Everything
|
|
|
|
|
|
executable stays 'self' plus what the wasm ONNX Runtime needs.
|
|
|
|
|
|
|
|
|
|
|
|
media-src carries data:/blob: because voice previews are synthesized inside
|
|
|
|
|
|
this app and handed straight to an <audio> element. NOTE: the policy below is
|
|
|
|
|
|
a plain directive list — CSP has no comment syntax, and a /* */ comment placed
|
|
|
|
|
|
inside it silently swallows the next directive (that is exactly how media-src
|
|
|
|
|
|
went missing once, and every voice preview failed with "no supported source").
|
|
|
|
|
|
-->
|
|
|
|
|
|
<meta http-equiv="Content-Security-Policy" content="
|
|
|
|
|
|
default-src 'self';
|
|
|
|
|
|
script-src 'self' 'wasm-unsafe-eval' blob:;
|
|
|
|
|
|
worker-src 'self' blob:;
|
|
|
|
|
|
connect-src 'self' https: ws: wss: data: blob: http://127.0.0.1:*;
|
|
|
|
|
|
img-src 'self' data: blob: http://127.0.0.1:*;
|
|
|
|
|
|
media-src 'self' data: blob: http://127.0.0.1:*;
|
|
|
|
|
|
style-src 'self' 'unsafe-inline';
|
|
|
|
|
|
font-src 'self' data:;
|
|
|
|
|
|
">
|
|
|
|
|
|
<title>Church Translate</title>
|
|
|
|
|
|
<link rel="stylesheet" href="./engine.css">
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
|
|
|
|
|
|
<header class="head">
|
|
|
|
|
|
<div class="brand">
|
|
|
|
|
|
<span class="mark"></span>
|
|
|
|
|
|
<span>Church Translate</span>
|
|
|
|
|
|
<span class="build" id="buildTag" title="Version and build number">—</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="head-right">
|
|
|
|
|
|
<span id="srvDot" class="pip" title="Church server"></span>
|
|
|
|
|
|
<span id="srvText" class="pip-label">Not connected</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<main class="page">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Always first, always full width. Nothing above it, nothing beside it. -->
|
|
|
|
|
|
<section class="hero" id="hero" data-state="setup">
|
|
|
|
|
|
<div class="hero-icon" id="heroIcon"></div>
|
|
|
|
|
|
<h1 id="heroTitle">Getting ready…</h1>
|
|
|
|
|
|
<p id="heroBody">Checking your setup.</p>
|
|
|
|
|
|
<button class="hero-go" id="heroGo" hidden></button>
|
|
|
|
|
|
<button class="btn hold-btn" id="heroHold" hidden></button>
|
|
|
|
|
|
<p class="holdbar" id="holdBar" hidden>
|
|
|
|
|
|
<b>ON HOLD</b> — translation is running and you can see every word, but
|
|
|
|
|
|
nothing is reaching the congregation.</p>
|
|
|
|
|
|
<p class="op-health" id="opHealth" hidden></p>
|
|
|
|
|
|
<p class="voice-warn" id="voiceWarn" hidden></p>
|
|
|
|
|
|
<div class="stats" id="opStats" hidden>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<div class="listenwho" id="opWho"></div>
|
|
|
|
|
|
<div class="bar" id="progWrap" hidden><div class="bar-fill" id="progFill"></div></div>
|
|
|
|
|
|
<p class="prog-note" id="progNote" hidden></p>
|
|
|
|
|
|
<button class="btn btn-primary" id="heroBtn" hidden></button>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ============================ centre column ============================ -->
|
|
|
|
|
|
<div class="col-main">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 1. Input ---------------------------------------------------------- -->
|
|
|
|
|
|
<section class="group" id="grpInput" data-open="true">
|
|
|
|
|
|
<button class="group-head" type="button" aria-expanded="true" aria-controls="grpInputBody">
|
|
|
|
|
|
<span class="chev" aria-hidden="true"></span>
|
|
|
|
|
|
<h2>Input</h2>
|
|
|
|
|
|
<span class="group-sub" id="inputSummary"></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div class="group-body" id="grpInputBody">
|
|
|
|
|
|
<div class="group-inner">
|
|
|
|
|
|
|
|
|
|
|
|
<label for="device">Input device</label>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<select id="device"></select>
|
|
|
|
|
|
<button class="btn" id="refreshDevices" title="Rescan inputs">Rescan</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="chanbox" id="channelBox" hidden>
|
|
|
|
|
|
<div class="chanhead">
|
|
|
|
|
|
<label style="margin:0">Channels</label>
|
|
|
|
|
|
<span class="hint" id="chanNote" style="margin:0"></span>
|
|
|
|
|
|
<button class="btn" id="chanMixAll" type="button">Mix all</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="chanlist" id="chanList"></div>
|
|
|
|
|
|
<p class="hint">Pick the channel the speaker is on — watch for the bar that moves.
|
|
|
|
|
|
Pick more than one only if two people talk on separate channels.</p>
|
|
|
|
|
|
<p class="qrwarn" id="chanTruth" hidden></p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="meter-row">
|
|
|
|
|
|
<div class="meter"><div class="meter-fill" id="meterFill"></div>
|
|
|
|
|
|
<div class="meter-thresh" id="meterThresh"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="meter-label" id="meterLabel">—</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="hint" id="audioHint">Green = speech. Voice should cross the notch; room noise should not.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="threshrow">
|
|
|
|
|
|
<label class="oprow" style="margin:0">
|
|
|
|
|
|
<input type="checkbox" id="threshManual">
|
|
|
|
|
|
<span>Set the notch myself</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input type="range" class="outtrim" id="threshSlider" min="-60" max="-20" step="1" disabled>
|
|
|
|
|
|
<span class="outdb" id="threshDb">auto</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="hint" id="threshHint">Following the room: the notch sits above whatever the input
|
|
|
|
|
|
is hearing when nobody is talking. Right on a console or Dante feed. On a preamp with
|
|
|
|
|
|
real hiss it can settle above a quiet talker, and the mic goes dead while the bar still
|
|
|
|
|
|
moves — set it by hand and put the notch just above the room, under the voice.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<h3 class="subhead">What it is hearing</h3>
|
|
|
|
|
|
<div class="monitor" id="monitor"><div class="mon-empty">Nothing yet.</div></div>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 1b. Output routing -------------------------------------------------- -->
|
|
|
|
|
|
<section class="group" id="grpOut">
|
|
|
|
|
|
<button class="group-head" type="button" aria-expanded="false" aria-controls="grpOutBody">
|
|
|
|
|
|
<span class="chev" aria-hidden="true"></span>
|
|
|
|
|
|
<h2>Output routing</h2>
|
|
|
|
|
|
<span class="group-sub" id="outSummary"></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div class="group-body" id="grpOutBody">
|
|
|
|
|
|
<div class="group-inner">
|
|
|
|
|
|
<p class="hint">Send each language's voice to its own physical channels — English on
|
|
|
|
|
|
1+2, Spanish on 3, and so on — into Dante, Waves SoundGrid, a console over USB,
|
|
|
|
|
|
anything the Mac can play to. Phones keep working exactly as they do now; this is
|
|
|
|
|
|
an extra output, not a replacement.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<label for="outDevice">Output device</label>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<select id="outDevice"></select>
|
|
|
|
|
|
<button class="btn" id="outRefresh" title="Rescan outputs">Rescan</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="hint" id="outState"></p>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="outMapBox" hidden>
|
|
|
|
|
|
<label style="margin-top:.75rem">Channels</label>
|
|
|
|
|
|
<p class="hint">Click a number to send that language there. Two channels side by
|
|
|
|
|
|
side join into a stereo pair — the voice is mono, so both carry the same signal,
|
|
|
|
|
|
which is what fills a stereo pair on a console. A language with nothing selected is
|
|
|
|
|
|
not sent to hardware at all.</p>
|
|
|
|
|
|
<p class="hint">The source language carries the <b>live room microphone</b>, not a
|
|
|
|
|
|
translation — there is nothing to translate English into during an English service.
|
|
|
|
|
|
Give it channels and it feeds an assistive-listening transmitter. Keep those
|
|
|
|
|
|
channels off whatever feeds the app's input, or you will build a loop.</p>
|
|
|
|
|
|
<div id="outMap" class="outmap"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 2. Languages and voices ------------------------------------------- -->
|
|
|
|
|
|
<section class="group" id="grpLang">
|
|
|
|
|
|
<button class="group-head" type="button" aria-expanded="false" aria-controls="grpLangBody">
|
|
|
|
|
|
<span class="chev" aria-hidden="true"></span>
|
|
|
|
|
|
<h2>Languages and voices</h2>
|
|
|
|
|
|
<span class="group-sub" id="langSummary"></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div class="group-body" id="grpLangBody">
|
|
|
|
|
|
<div class="group-inner">
|
|
|
|
|
|
|
|
|
|
|
|
<h3 class="subhead">Languages on the front page</h3>
|
|
|
|
|
|
<p class="hint" id="opLangCount"></p>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<input id="opLangFilter" type="search" placeholder="Filter languages…" autocomplete="off">
|
|
|
|
|
|
<label class="oprow"><input type="checkbox" id="opLangVoiceOnly"><span>With a voice only</span></label>
|
|
|
|
|
|
<button class="btn" id="opLangNone">Turn all off</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="langcols">
|
|
|
|
|
|
<div class="langcol">
|
|
|
|
|
|
<div class="langcolhead">On <b id="opLangNOn">0</b></div>
|
|
|
|
|
|
<div id="opLangOn" class="langlist" role="group" aria-label="Languages on the front page"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="langcol">
|
|
|
|
|
|
<div class="langcolhead">Off <b id="opLangNOff">0</b></div>
|
|
|
|
|
|
<div id="opLangOff" class="langlist" role="group" aria-label="Languages not on the front page"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<label class="oprow">
|
|
|
|
|
|
<input type="checkbox" id="opAllowAll">
|
|
|
|
|
|
<span>Let listeners pick any language</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<p class="hint" id="opAllowAllHint"></p>
|
|
|
|
|
|
|
|
|
|
|
|
<h3 class="subhead">Always translate</h3>
|
|
|
|
|
|
<p class="hint">Pinned languages are translated before anyone arrives.</p>
|
|
|
|
|
|
<div id="opPinned" class="chips"></div>
|
|
|
|
|
|
<input id="opPinSearch" type="search" placeholder="Pin a language…" autocomplete="off">
|
|
|
|
|
|
<div id="opPinResults" class="opresults"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<hr>
|
|
|
|
|
|
|
|
|
|
|
|
<h3 class="subhead">Corrections</h3>
|
|
|
|
|
|
<p class="hint">One per line: <code>what it heard => what it should say</code></p>
|
|
|
|
|
|
<textarea id="opFixes" rows="8" spellcheck="false"></textarea>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<button class="btn" id="opSaveFixes">Save corrections</button>
|
|
|
|
|
|
<span class="hint" id="opFixHint"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<hr>
|
|
|
|
|
|
|
|
|
|
|
|
<h3 class="subhead">Voices</h3>
|
|
|
|
|
|
<p class="hint">Pick a language, press ▶ to hear a voice read scripture, then Use it.
|
|
|
|
|
|
Built-in voices download the first time you play them.</p>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<select id="voiceLang"></select>
|
|
|
|
|
|
<span class="hint" id="voiceNow"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="hint" id="voiceCoverage"></p>
|
|
|
|
|
|
|
|
|
|
|
|
<label for="voiceInstruct" style="margin-top:.5rem">Delivery direction <span class="hint" style="display:inline">— local voices only</span></label>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<input id="voiceInstruct" type="text" autocomplete="off" spellcheck="false"
|
|
|
|
|
|
placeholder="e.g. whisper this softly and intimately, breathy">
|
|
|
|
|
|
<button class="btn" id="voiceInstructClear">Clear</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="hint">Plain English, told to the voice before it speaks. Only the
|
|
|
|
|
|
<b>Local (this Mac)</b> voices listen to it — Piper built-ins have no such control and
|
|
|
|
|
|
will ignore it. It changes <b>delivery</b>, not volume: measured, "whisper this softly
|
|
|
|
|
|
and intimately" stretched the same Spanish line from 4.0s to 6.2s. Wording is not
|
|
|
|
|
|
predictable — "speak loudly and forcefully" came out <i>quieter</i> than plain — so try
|
|
|
|
|
|
a few and judge by ear. Press ▶ on a voice below to hear it with this direction.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="voiceList" class="voicelist"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<hr>
|
|
|
|
|
|
|
|
|
|
|
|
<h3 class="subhead">Premium voices</h3>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<select id="cloudProvider" style="max-width:11rem">
|
|
|
|
|
|
<option value="off">Off — built-in voices</option>
|
|
|
|
|
|
<option value="kokoro">Kokoro (this Mac — free, fast)</option>
|
|
|
|
|
|
<option value="local">Qwen3 (this Mac — free, expressive)</option>
|
|
|
|
|
|
<option value="fish">fish.audio</option>
|
|
|
|
|
|
<option value="google">Google Cloud TTS</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<span class="hint" id="cloudState"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="cloudFields" hidden>
|
|
|
|
|
|
<label for="cloudKey">API key</label>
|
|
|
|
|
|
<input id="cloudKey" type="password" autocomplete="off" spellcheck="false">
|
|
|
|
|
|
<label for="cloudVoice">Voice</label>
|
|
|
|
|
|
<input id="cloudVoice" type="text" autocomplete="off" spellcheck="false"
|
|
|
|
|
|
placeholder="fish: default reference id · leave empty for Google">
|
|
|
|
|
|
<label for="cloudPerLang">Per-language voices (one per line: code = voice id)</label>
|
|
|
|
|
|
<textarea id="cloudPerLang" rows="4" spellcheck="false"
|
|
|
|
|
|
placeholder="spa_Latn = es-US-Chirp3-HD-Charon kor_Hang = ko-KR-Chirp3-HD-Charon"></textarea>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<button class="btn" id="cloudSave">Save</button>
|
|
|
|
|
|
<button class="btn" id="cloudTest">Test</button>
|
|
|
|
|
|
<span class="hint" id="cloudHint"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="hint">Human-sounding voices from a paid service. Keys stay on this Mac.
|
|
|
|
|
|
If the service ever fails, the built-in voices take over automatically.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<hr>
|
|
|
|
|
|
|
|
|
|
|
|
<h3 class="subhead">Voice dynamics</h3>
|
|
|
|
|
|
<label class="oprow">
|
|
|
|
|
|
<input type="checkbox" id="optDynamics">
|
|
|
|
|
|
<span>Follow the speaker's loudness</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<p class="hint">The translated voice gets quieter when the preacher drops to a whisper
|
|
|
|
|
|
and louder when he leans on a line. Applies to built-in and premium voices alike.</p>
|
|
|
|
|
|
<p class="hint"><b>Compressed on purpose.</b> The room spans about 40 dB between a
|
|
|
|
|
|
whisper and a shout; reproduced faithfully into headphones or a hearing-aid pack the
|
|
|
|
|
|
whisper is inaudible and the shout is painful. This keeps the shape and holds the
|
|
|
|
|
|
range to about 14 dB. Level only — the voice cannot actually whisper.</p>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 3. Settings -------------------------------------------------------- -->
|
|
|
|
|
|
<section class="group" id="grpSettings">
|
|
|
|
|
|
<button class="group-head" type="button" aria-expanded="false" aria-controls="grpSettingsBody">
|
|
|
|
|
|
<span class="chev" aria-hidden="true"></span>
|
|
|
|
|
|
<h2>Settings</h2>
|
|
|
|
|
|
<span class="group-sub" id="settingsSummary"></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div class="group-body" id="grpSettingsBody">
|
|
|
|
|
|
<div class="group-inner">
|
|
|
|
|
|
|
|
|
|
|
|
<h3 class="subhead">Stream delay</h3>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<input id="opDelay" type="number" min="0" max="120" step="1" style="max-width:6rem">
|
|
|
|
|
|
<span class="hint" style="margin:0">seconds</span>
|
|
|
|
|
|
<button class="btn" id="opSaveDelay">Set</button>
|
|
|
|
|
|
<span class="hint" id="opDelayHint"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="hint">For livestream viewers: hold captions and audio so they line up
|
|
|
|
|
|
with the stream's own delay. The room always sees live. 0 = off.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<hr>
|
|
|
|
|
|
|
|
|
|
|
|
<h3 class="subhead">Bitfocus Companion</h3>
|
|
|
|
|
|
<div class="codeblock"><span class="urlname">Start</span><code id="opStartUrl">—</code><button class="btn" data-copy="opStartUrl">Copy</button></div>
|
|
|
|
|
|
<div class="codeblock"><span class="urlname">Stop</span><code id="opStopUrl">—</code><button class="btn" data-copy="opStopUrl">Copy</button></div>
|
|
|
|
|
|
<div class="codeblock"><span class="urlname">Toggle</span><code id="opToggleUrl">—</code><button class="btn" data-copy="opToggleUrl">Copy</button></div>
|
|
|
|
|
|
|
|
|
|
|
|
<h4 class="subhead2">Cough button</h4>
|
|
|
|
|
|
<p class="hint">Keeps translating and keeps you watching it — the congregation just
|
|
|
|
|
|
stops receiving. For a prayer request with a name in it, or anything said on a live
|
|
|
|
|
|
mic that was not meant to leave the room. Starting a service always clears it.</p>
|
|
|
|
|
|
<div class="codeblock"><span class="urlname">Hold</span><code id="opHoldUrl">—</code><button class="btn" data-copy="opHoldUrl">Copy</button></div>
|
|
|
|
|
|
<div class="codeblock"><span class="urlname">Resume</span><code id="opResumeUrl">—</code><button class="btn" data-copy="opResumeUrl">Copy</button></div>
|
|
|
|
|
|
<div class="codeblock"><span class="urlname">Hold toggle</span><code id="opHoldToggleUrl">—</code><button class="btn" data-copy="opHoldToggleUrl">Copy</button></div>
|
|
|
|
|
|
|
|
|
|
|
|
<h4 class="subhead2">Stream delay</h4>
|
|
|
|
|
|
<p class="hint">Nudge while watching the stream until the captions line up. Change
|
|
|
|
|
|
<code>by=</code> for a bigger step — one button at 1s for fine work, another at 5s.</p>
|
|
|
|
|
|
<div class="codeblock"><span class="urlname">+1 second</span><code id="opDelayUpUrl">—</code><button class="btn" data-copy="opDelayUpUrl">Copy</button></div>
|
|
|
|
|
|
<div class="codeblock"><span class="urlname">−1 second</span><code id="opDelayDownUrl">—</code><button class="btn" data-copy="opDelayDownUrl">Copy</button></div>
|
|
|
|
|
|
<div class="codeblock"><span class="urlname">Off</span><code id="opDelayOffUrl">—</code><button class="btn" data-copy="opDelayOffUrl">Copy</button></div>
|
|
|
|
|
|
|
|
|
|
|
|
<h4 class="subhead2">For button text and colour</h4>
|
|
|
|
|
|
<div class="codeblock"><span class="urlname">Status</span><code id="opStatusUrl">—</code><button class="btn" data-copy="opStatusUrl">Copy</button></div>
|
|
|
|
|
|
<div class="codeblock"><span class="urlname">Hold state</span><code id="opHoldStateUrl">—</code><button class="btn" data-copy="opHoldStateUrl">Copy</button></div>
|
|
|
|
|
|
<div class="codeblock"><span class="urlname">Delay now</span><code id="opDelayStateUrl">—</code><button class="btn" data-copy="opDelayStateUrl">Copy</button></div>
|
|
|
|
|
|
<p class="hint">Generic HTTP → GET. One word each: Status is <code>live</code>,
|
|
|
|
|
|
<code>ready</code> or <code>offline</code> (a held service still reads
|
|
|
|
|
|
<code>live</code> — it is live), Hold state is <code>held</code> or <code>open</code>,
|
|
|
|
|
|
Delay now is a number of seconds. Keep the key off the projector.</p>
|
|
|
|
|
|
<p class="hint">Set a Companion URL below and the server pushes
|
|
|
|
|
|
<code>translate_state</code>, <code>_hold</code>, <code>_delay</code> and
|
|
|
|
|
|
<code>_listeners</code> straight into custom variables — no polling.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<hr>
|
|
|
|
|
|
|
|
|
|
|
|
<h3 class="subhead">Operator page PIN</h3>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<input id="opPin" type="text" inputmode="numeric" autocomplete="off" spellcheck="false">
|
|
|
|
|
|
<button class="btn" id="opSavePin">Change</button>
|
|
|
|
|
|
<span class="hint" id="opPinHint"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="hint">For running the desk from a phone or tablet <b>on the church
|
|
|
|
|
|
network</b>. This tab never asks for it. The operator page is refused outright to
|
|
|
|
|
|
anything reaching the server from the internet — there is no setting to change that,
|
|
|
|
|
|
so the PIN only ever faces someone already in the building.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<hr>
|
|
|
|
|
|
|
|
|
|
|
|
<h3 class="subhead">Advanced</h3>
|
|
|
|
|
|
<label for="port">Port</label>
|
|
|
|
|
|
<input id="port" type="number" min="1024" max="65535">
|
|
|
|
|
|
<p class="hint">Only if something else already uses it.</p>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<button class="btn" id="savePort">Save & restart server</button>
|
|
|
|
|
|
<span class="hint" id="portHint"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<hr>
|
|
|
|
|
|
|
|
|
|
|
|
<h3 class="subhead">Another campus</h3>
|
|
|
|
|
|
<p class="hint">Carries the work: corrections, languages, pinned languages, voices,
|
|
|
|
|
|
output channel map and trims. Does <b>not</b> carry this Mac's PIN, control token or
|
|
|
|
|
|
device ids — each campus keeps its own, and premium-voice API keys stay on the machine
|
|
|
|
|
|
they were typed into.</p>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<button class="btn" id="setExport">Save settings to a file</button>
|
|
|
|
|
|
<button class="btn" id="setImport">Load settings from a file</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="hint" id="setHint"></p>
|
|
|
|
|
|
|
|
|
|
|
|
<hr>
|
|
|
|
|
|
|
2026-09-24 12:09:08 -04:00
|
|
|
|
<h3 class="subhead">Updates</h3>
|
|
|
|
|
|
<p class="hint">Checks this church’s own server, not the internet — so it works at a venue
|
|
|
|
|
|
with no route out. Nothing downloads on its own and nothing installs itself: this app is
|
|
|
|
|
|
often live in front of a congregation.</p>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<button class="btn" id="updCheck">Check for an update</button>
|
|
|
|
|
|
<span class="hint" id="updState">—</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="updFound" hidden>
|
|
|
|
|
|
<p class="hint" id="updWhat"></p>
|
|
|
|
|
|
<div class="row">
|
2026-09-24 12:50:05 -04:00
|
|
|
|
<button class="btn" id="updGet">Download and install</button>
|
2026-09-24 12:09:08 -04:00
|
|
|
|
<span class="hint" id="updProgress"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<details id="updNotesBox"><summary class="hint">What changed</summary>
|
|
|
|
|
|
<pre class="updnotes" id="updNotes"></pre></details>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<label for="updToken" style="margin-top:.75rem">Access token</label>
|
|
|
|
|
|
<input id="updToken" type="password" placeholder="only while the repository is private"
|
|
|
|
|
|
autocomplete="off" spellcheck="false">
|
|
|
|
|
|
<p class="hint">Stays on this Mac. It is not carried by <b>Save settings to a file</b>.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<hr>
|
|
|
|
|
|
|
2026-09-23 15:41:21 -04:00
|
|
|
|
<label style="margin-top:.75rem">Models</label>
|
|
|
|
|
|
<p class="hint" id="modelInfo">—</p>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<button class="btn" id="revealCache">Show model folder</button>
|
|
|
|
|
|
<span class="hint" id="cacheSize"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="card">
|
|
|
|
|
|
<h2>Log</h2>
|
|
|
|
|
|
<div class="log" id="log"></div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
</div><!-- /col-main -->
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ============================= right rail ============================= -->
|
|
|
|
|
|
<aside class="col-rail">
|
|
|
|
|
|
<section class="card rail-card">
|
|
|
|
|
|
<h2>What the congregation opens</h2>
|
|
|
|
|
|
|
|
|
|
|
|
<p class="qrwarn" id="qrWarn" hidden></p>
|
|
|
|
|
|
|
|
|
|
|
|
<img id="qr" class="qr" alt="QR code to the translation page">
|
|
|
|
|
|
<div id="addrList" class="addrlist"></div>
|
|
|
|
|
|
<div class="railbtns">
|
|
|
|
|
|
<button class="btn btn-primary" id="copyAddr">Copy address</button>
|
|
|
|
|
|
<button class="btn" id="openPoster">Printable poster</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<h3 class="subhead">Operator page</h3>
|
|
|
|
|
|
<p class="hint">Run the desk from a phone or tablet. Needs the PIN.</p>
|
|
|
|
|
|
<div class="codeblock"><code id="opPageUrl">—</code><button class="btn" data-copy="opPageUrl">Copy</button></div>
|
|
|
|
|
|
<button class="btn railwide" id="openOperator">Open operator page</button>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
<script src="./server/public/languages.js"></script>
|
|
|
|
|
|
<script src="./pt-variants.js"></script>
|
|
|
|
|
|
<script src="./voice-catalog.js"></script>
|
|
|
|
|
|
<script src="./out-routing.js"></script>
|
|
|
|
|
|
<script src="./mt-guard.js"></script>
|
|
|
|
|
|
<script src="./liturgy.js"></script>
|
|
|
|
|
|
<script src="./short-questions.js"></script>
|
|
|
|
|
|
<script src="./preach.js"></script>
|
|
|
|
|
|
<script src="./spa-variants.js"></script>
|
|
|
|
|
|
<script src="./bible-lexicon.js"></script>
|
|
|
|
|
|
<script type="module" src="./engine.js"></script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|