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>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
/* Model the cut rules with the SHIPPED constants and drive them with synthetic
|
||||
speech patterns. The question is: how long does a segment get, per style? */
|
||||
const fs=require('fs');
|
||||
const src=fs.readFileSync('engine.js','utf8');
|
||||
const g=(n)=>Number(src.match(new RegExp('const '+n+'\\s*=\\s*Math\\.round\\((\\d+) / FRAME_MS\\)'))[1]);
|
||||
const FRAME=Number(src.match(/const FRAME\s*=\s*(\d+)/)[1]);
|
||||
const SAMPLE_RATE=Number(src.match(/const SAMPLE_RATE\s*=\s*(\d+)/)[1]);
|
||||
const FRAME_MS=(FRAME/SAMPLE_RATE)*1000;
|
||||
const F=(n)=>Math.round(g(n)/FRAME_MS); // source stores ms; the loop counts frames
|
||||
const C={SOFT_CUT:F('SOFT_CUT'),SOFT_DIP:F('SOFT_DIP'),HARD_CUT:F('HARD_CUT'),
|
||||
HARD_CUT_MAX:F('HARD_CUT_MAX'),EARLY_CLOSE:F('EARLY_CLOSE'),
|
||||
EARLY_MIN_SEG:F('EARLY_MIN_SEG'),SILENCE_TO_END:F('SILENCE_TO_END')};
|
||||
console.log('FRAME_MS='+FRAME_MS+' constants (ms): '+
|
||||
Object.entries(C).map(([k,v])=>k+'='+Math.round(v*FRAME_MS)).join(' ')+'\n');
|
||||
|
||||
/* pattern: fn(frameIndex) -> {speech:bool, dipish:bool, sentenceEnded:bool} */
|
||||
function run(name, pattern, maxFrames){
|
||||
let len=0,silence=0,dip=0,cuts=[];
|
||||
for (let i=0;i<maxFrames;i++){
|
||||
const p=pattern(i);
|
||||
len++;
|
||||
if (p.speech){ silence=0; dip=0; }
|
||||
else { silence++; dip++; }
|
||||
if (!p.speech && p.dipish) dip++;
|
||||
let why=null;
|
||||
if (len>=C.HARD_CUT_MAX) why='ceiling';
|
||||
else if (silence>=C.SILENCE_TO_END) why='silence';
|
||||
else if (len>=C.SOFT_CUT && dip>=C.SOFT_DIP) why=(len>=C.HARD_CUT?'hard':'soft');
|
||||
else if (p.sentenceEnded && len>=C.EARLY_MIN_SEG && silence>=C.EARLY_CLOSE) why='early';
|
||||
else if (p.sentenceEnded && len>=C.SOFT_CUT) why='sentence';
|
||||
if (why){ cuts.push({ms:len*FRAME_MS,why}); len=0;silence=0;dip=0; }
|
||||
}
|
||||
const avg=cuts.length?Math.round(cuts.reduce((a,c)=>a+c.ms,0)/cuts.length):0;
|
||||
const worst=cuts.length?Math.max(...cuts.map(c=>c.ms)):0;
|
||||
console.log(name);
|
||||
console.log(' segments: '+cuts.length+' avg '+avg+'ms worst '+worst+'ms');
|
||||
console.log(' reasons : '+JSON.stringify(cuts.reduce((a,c)=>(a[c.why]=(a[c.why]||0)+1,a),{})));
|
||||
return {avg,worst};
|
||||
}
|
||||
|
||||
const S=(ms)=>Math.round(ms/FRAME_MS);
|
||||
/* A fast preacher: continuous speech, a sentence boundary every ~4s, and no
|
||||
pause anywhere long enough for the old rules to notice. */
|
||||
run('FAST preacher — no pauses, sentence every 4s', (i)=>({
|
||||
speech:true, dipish:false, sentenceEnded: i%S(4000)===S(3999)
|
||||
}), S(60000));
|
||||
|
||||
/* Same speaker but Whisper never reports a sentence end (worst case). */
|
||||
run('FAST preacher — no pauses, no punctuation at all', (i)=>({
|
||||
speech:true, dipish:false, sentenceEnded:false
|
||||
}), S(60000));
|
||||
|
||||
/* A normal speaker: 150ms micro-gaps between phrases every 2s. */
|
||||
run('NORMAL speaker — 150ms gap every 2s', (i)=>{
|
||||
const inGap = (i%S(2000)) >= S(1850);
|
||||
return { speech:!inGap, dipish:inGap, sentenceEnded:false };
|
||||
}, S(60000));
|
||||
|
||||
/* Someone who stops properly at the end of each sentence. */
|
||||
run('DELIBERATE speaker — 600ms full stop every 5s', (i)=>{
|
||||
const inGap = (i%S(5000)) >= S(4400);
|
||||
return { speech:!inGap, dipish:inGap, sentenceEnded:false };
|
||||
}, S(60000));
|
||||
Reference in New Issue
Block a user