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,85 @@
|
||||
/*
|
||||
* Opus-MT bilingual vs NLLB-600M, on THIS Mac, on sermon sentences.
|
||||
*
|
||||
* The claim under test: a 77M bilingual model matches or beats NLLB-200-
|
||||
* distilled-600M on high-resource pairs at ~1/8 the size. Published chrF says
|
||||
* yes; no published CPU latency comparison exists, and quality on preaching
|
||||
* English is not what any leaderboard measures. So: measure both here.
|
||||
*/
|
||||
import { pipeline, env } from '@huggingface/transformers';
|
||||
import os from 'os'; import path from 'path';
|
||||
env.allowLocalModels = false;
|
||||
env.cacheDir = path.join(os.homedir(), 'Library/Application Support/Church Translate Models');
|
||||
|
||||
const SENTENCES = [
|
||||
'And I want you to understand that God has a plan for your life',
|
||||
'Turn with me to the book of Romans chapter eight',
|
||||
'He was faithful even when it cost him everything',
|
||||
'Would you stand with me as we pray',
|
||||
'The grace of God is not something you can earn',
|
||||
'Peter denied him three times before the rooster crowed',
|
||||
];
|
||||
const PAIRS = [
|
||||
{ code: 'spa_Latn', opus: 'Xenova/opus-mt-en-es', name: 'Spanish' },
|
||||
{ code: 'fra_Latn', opus: 'Xenova/opus-mt-en-fr', name: 'French' },
|
||||
{ code: 'zho_Hans', opus: 'Xenova/opus-mt-en-zh', name: 'Chinese' },
|
||||
];
|
||||
|
||||
const t = (f) => { const s = Date.now(); return f().then((r) => [Date.now() - s, r]); };
|
||||
const out = (o) => (Array.isArray(o) ? o[0] : o).translation_text.trim();
|
||||
|
||||
console.log('loading NLLB fp32…');
|
||||
const nllb = await pipeline('translation', 'Xenova/nllb-200-distilled-600M', { dtype: 'fp32' });
|
||||
await nllb(SENTENCES[0], { src_lang: 'eng_Latn', tgt_lang: 'spa_Latn' });
|
||||
|
||||
const results = {};
|
||||
for (const p of PAIRS) {
|
||||
const ts = [], txt = [];
|
||||
for (const s of SENTENCES) {
|
||||
const [ms, o] = await t(() => nllb(s, { src_lang: 'eng_Latn', tgt_lang: p.code }));
|
||||
ts.push(ms); txt.push(out(o));
|
||||
}
|
||||
results[p.name] = { nllb: { ms: ts, txt } };
|
||||
}
|
||||
|
||||
for (const dtype of ['fp32', 'q8']) {
|
||||
for (const p of PAIRS) {
|
||||
let pipe;
|
||||
const t0 = Date.now();
|
||||
try { pipe = await pipeline('translation', p.opus, { dtype }); }
|
||||
catch (e) { console.log(p.name + ' ' + dtype + ': LOAD FAILED — ' + String(e.message).slice(0, 90)); continue; }
|
||||
const load = Date.now() - t0;
|
||||
await pipe(SENTENCES[0]);
|
||||
const ts = [], txt = [];
|
||||
for (const s of SENTENCES) { const [ms, o] = await t(() => pipe(s)); ts.push(ms); txt.push(out(o)); }
|
||||
results[p.name]['opus_' + dtype] = { ms: ts, txt, load };
|
||||
await pipe.dispose?.();
|
||||
}
|
||||
}
|
||||
|
||||
const mean = (a) => a.reduce((x, y) => x + y, 0) / a.length;
|
||||
console.log('\n=== speed, ms per sentence (mean of ' + SENTENCES.length + ') ===');
|
||||
console.log('language | NLLB fp32 | Opus fp32 | Opus q8 | best speedup');
|
||||
for (const p of PAIRS) {
|
||||
const r = results[p.name];
|
||||
const n = mean(r.nllb.ms);
|
||||
const f = r.opus_fp32 ? mean(r.opus_fp32.ms) : NaN;
|
||||
const q = r.opus_q8 ? mean(r.opus_q8.ms) : NaN;
|
||||
const best = Math.min(f || Infinity, q || Infinity);
|
||||
console.log(p.name.padEnd(9) + ' | ' + (n.toFixed(0) + 'ms').padStart(10) + ' | ' +
|
||||
((f ? f.toFixed(0) : '—') + 'ms').padStart(9) + ' | ' + ((q ? q.toFixed(0) : '—') + 'ms').padStart(7) +
|
||||
' | ' + (n / best).toFixed(2) + 'x');
|
||||
}
|
||||
|
||||
console.log('\n=== output, sentence by sentence ===');
|
||||
SENTENCES.forEach((s, i) => {
|
||||
console.log('\n"' + s + '"');
|
||||
for (const p of PAIRS) {
|
||||
const r = results[p.name];
|
||||
console.log(' ' + p.name);
|
||||
console.log(' NLLB : ' + r.nllb.txt[i]);
|
||||
if (r.opus_fp32) console.log(' Opus fp32 : ' + r.opus_fp32.txt[i]);
|
||||
if (r.opus_q8) console.log(' Opus q8 : ' + r.opus_q8.txt[i] +
|
||||
(r.opus_fp32 && r.opus_q8.txt[i] !== r.opus_fp32.txt[i] ? ' <-- differs from fp32' : ''));
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user