Files
church-translate/app/_preview-langlist.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

52 lines
3.1 KiB
HTML

<!doctype html><html><head><meta charset="utf-8">
<link rel="stylesheet" href="./engine.css">
<style>body{padding:1.5rem;max-width:44rem;margin:0 auto}</style>
</head><body>
<section class="card">
<h2>Languages on the front page</h2>
<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 id="opLangList" class="langlist"></div>
<label class="oprow"><input type="checkbox" id="opAllowAll" checked><span>Let listeners pick any language</span></label>
<p class="hint" id="opAllowAllHint">On — all 104 languages are offered. 32 of them have a voice; the rest show captions only.</p>
</section>
<script src="./languages.js"></script><script src="./voice-catalog.js"></script>
<script>
/* Same DOM the renderer builds, against the real catalogs. */
const $=(id)=>document.getElementById(id);
const opConfig={featured:["spa_Latn","zho_Hans","por_Latn","por_PT","fra_Latn","kor_Hang","vie_Latn","hat_Latn","arb_Arab"],allowAllLanguages:true};
function render(){
const box=$('opLangList'), q=($('opLangFilter').value||'').trim().toLowerCase();
const voiceOnly=$('opLangVoiceOnly').checked;
const on=new Set(opConfig.featured), spoken=new Set(VoiceCatalog.languages());
const codes=Languages.codes().slice().sort((a,b)=>(Languages.get(a).name||a).localeCompare(Languages.get(b).name||b));
box.textContent=''; let shown=0;
for(const code of codes){
const l=Languages.get(code)||{name:code,native:code}, has=spoken.has(code);
if(voiceOnly&&!has) continue;
if(q&&(l.name+' '+l.native+' '+code).toLowerCase().indexOf(q)===-1) continue;
shown++;
const row=document.createElement('label'); row.className='langrow'+(has?'':' novoice');
const cb=document.createElement('input'); cb.type='checkbox'; cb.checked=on.has(code);
cb.addEventListener('change',()=>{cb.checked?opConfig.featured.push(code):opConfig.featured.splice(opConfig.featured.indexOf(code),1);count();});
row.appendChild(cb);
const nm=document.createElement('span'); nm.className='lname'; nm.textContent=l.name; row.appendChild(nm);
const nat=document.createElement('span'); nat.className='lsub'; nat.textContent=l.native===l.name?'':l.native; row.appendChild(nat);
const tg=document.createElement('span'); tg.className='ltag'; tg.textContent=has?'voice':'captions only'; row.appendChild(tg);
box.appendChild(row);
}
if(!shown){const p=document.createElement('p');p.className='hint';p.textContent='Nothing matches that.';box.appendChild(p);}
count();
}
function count(){$('opLangCount').textContent=opConfig.featured.length+' of '+Languages.codes().length+
' on. These are what a listener sees first — the rest stay reachable by search.';}
$('opLangFilter').addEventListener('input',render);
$('opLangVoiceOnly').addEventListener('change',render);
$('opLangNone').addEventListener('click',()=>{opConfig.featured.length=0;render();});
render();
</script></body></html>