Files
church-translate/app/_bench-opus.mjs
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

86 lines
3.6 KiB
JavaScript

/*
* 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' : ''));
}
});