Settings gains an Updates block: it asks the church Gitea for the newest release, compares the tag against the running build, and if there is a newer one shows what changed and offers to download the installer. Deliberately not electron-updater. That wants a static feed published beside the artifacts, and this build is ad-hoc signed with no Team ID, so a silent in-place swap would meet Gatekeeper on a machine nobody is sitting at. And nothing here happens on its own: a background check that announces itself three minutes into a sermon is a hazard, so the operator presses the button, and presses a second one to download. The file lands in Downloads and is revealed; installing is a drag across when the room is empty. Tags are v<version>-<build>, compared numerically field by field so 3.9.0 does not read as newer than 3.23.2 and build 113 beats build 112. Two things only real testing found. Gitea answers with whatever address it was reached on, so the release API returned asset URLs on 10.3.8.90 while the forge is configured by hostname — the same-host guard refused a perfectly good installer. The URL is now rebuilt on the configured origin, which keeps the property that matters (this only ever fetches from the server the operator configured) and survives a forge that knows itself by more than one name. And a private repository answers 404 rather than 401, so 'no releases published yet' told the operator the opposite of the truth; without a token that case now names the remedy instead. The token is this machine's credential: it stays out of PORTABLE_APP, so it does not ride along in an exported settings file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
461 lines
23 KiB
HTML
461 lines
23 KiB
HTML
<!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>
|
||
|
||
<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">
|
||
<button class="btn" id="updGet">Download it</button>
|
||
<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>
|
||
|
||
<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>
|