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>
36 lines
1.6 KiB
JavaScript
36 lines
1.6 KiB
JavaScript
/* Load and speak with every Chinese bundle the picker offers, plus a Piper
|
|
voice to prove the espeak path did not regress. */
|
|
const path=require('path'), os=require('os');
|
|
const voices=require('./voices.js');
|
|
const sherpa=require('sherpa-onnx-node');
|
|
const DIR=path.join(os.homedir(),'Library/Application Support/Church Translate Voices');
|
|
const C=require('./voice-catalog.js');
|
|
|
|
const TEXT='耶稣说,我就是道路、真理、生命。';
|
|
const cases=C.forLanguage('zho_Hans').filter(v=>v.engine==='builtin')
|
|
.map(v=>({bundle:v.bundle,model:v.model,text:TEXT}));
|
|
cases.push({bundle:'vits-piper-de_DE-thorsten-medium',model:'de_DE-thorsten-medium.onnx',
|
|
text:'Jesus sprach: Ich bin der Weg.'});
|
|
|
|
let ok=0,bad=0;
|
|
for (const c of cases) {
|
|
const dir=path.join(DIR,c.bundle);
|
|
try {
|
|
const cfg=voices.ttsConfig(dir,{model:c.model});
|
|
const which=cfg.model.vits.lexicon?'lexicon':'espeak';
|
|
const t=new sherpa.OfflineTts(cfg);
|
|
const a=t.generate({text:c.text,sid:0,speed:1.0});
|
|
const secs=a.samples.length/a.sampleRate;
|
|
/* Silence means it "worked" but produced nothing — that is still a failure. */
|
|
let peak=0; for(let i=0;i<a.samples.length;i++){const v=Math.abs(a.samples[i]); if(v>peak)peak=v;}
|
|
const dead=peak<0.01;
|
|
console.log((dead?' SILENT ':' OK ')+c.bundle.padEnd(38)+which.padEnd(9)+
|
|
secs.toFixed(1)+'s peak '+peak.toFixed(3)+' @'+a.sampleRate);
|
|
dead?bad++:ok++;
|
|
} catch(e) {
|
|
console.log(' FAIL '+c.bundle.padEnd(38)+String(e.message).split('\n')[0].slice(0,70));
|
|
bad++;
|
|
}
|
|
}
|
|
console.log('\n'+ok+' usable, '+bad+' not');
|