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>
33 lines
1.6 KiB
JavaScript
33 lines
1.6 KiB
JavaScript
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');
|
|
|
|
/* Partial-length text: a rolling 6s window is roughly this long. */
|
|
const TEXT = 'And I want you to understand that God has a plan for your life';
|
|
const LANGS = ['spa_Latn','por_Latn','fra_Latn','zho_Hans','hat_Latn','hin_Deva','kor_Hang','vie_Latn','arb_Arab'];
|
|
|
|
for (const dtype of ['fp32','q8']) {
|
|
let pipe;
|
|
const t0 = Date.now();
|
|
try { pipe = await pipeline('translation','Xenova/nllb-200-distilled-600M',{ dtype }); }
|
|
catch (e) { console.log(dtype + ': FAILED to load — ' + String(e.message).slice(0,80)); continue; }
|
|
const load = Date.now() - t0;
|
|
/* warm */
|
|
await pipe(TEXT, { src_lang:'eng_Latn', tgt_lang:'spa_Latn' });
|
|
const times = []; const outs = {};
|
|
for (const l of LANGS) {
|
|
const s = Date.now();
|
|
const o = await pipe(TEXT, { src_lang:'eng_Latn', tgt_lang:l });
|
|
times.push(Date.now() - s);
|
|
outs[l] = (Array.isArray(o)?o[0]:o).translation_text.trim();
|
|
}
|
|
const mean = times.reduce((a,b)=>a+b,0)/times.length;
|
|
console.log('\n=== ' + dtype + ' === load ' + (load/1000).toFixed(1) + 's');
|
|
console.log(' per language: ' + times.map(t=>t+'ms').join(' '));
|
|
console.log(' mean ' + mean.toFixed(0) + 'ms | 1 lang ' + mean.toFixed(0) + 'ms | 2 langs ' + (mean*2).toFixed(0) + 'ms | 9 langs ' + (mean*9/1000).toFixed(1) + 's');
|
|
console.log(' spa: ' + outs.spa_Latn);
|
|
console.log(' fra: ' + outs.fra_Latn);
|
|
console.log(' zho: ' + outs.zho_Hans);
|
|
}
|