Files
church-translate/app/_test-zh.js
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

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');