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>
54 lines
2.1 KiB
JavaScript
54 lines
2.1 KiB
JavaScript
/* Measure the hallucination and what actually stops it. Runs the same model,
|
||
dtype and cache the app uses, in a plain node process like mt-service.js. */
|
||
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 t0 = Date.now();
|
||
const pipe = await pipeline('translation', 'Xenova/nllb-200-distilled-600M', { dtype: 'fp32' });
|
||
console.log('loaded in ' + ((Date.now() - t0) / 1000).toFixed(1) + 's\n');
|
||
|
||
/* The exact lines from the operator's screenshot, plus stress cases. */
|
||
const CASES = [
|
||
'Donald Trump.',
|
||
'Hello, hello.',
|
||
'His name was John.',
|
||
'Open up to the book, Matthew.',
|
||
'Today we are going to talk about Peter in the Bible.',
|
||
'Turn with me to John chapter 3.',
|
||
'Amen.',
|
||
'Jesus wept.',
|
||
'Paul wrote to the church at Corinth.',
|
||
'How many countries in the world?',
|
||
'And God said, let there be light, and there was light.'
|
||
];
|
||
|
||
const CONFIGS = [
|
||
{ name: 'current (no params)', opts: {} },
|
||
{ name: 'cap 2x+8', opts: null, cap: (n) => Math.ceil(n * 2) + 8 },
|
||
{ name: 'cap 2x+8 +nrng3', opts: { no_repeat_ngram_size: 3 }, cap: (n) => Math.ceil(n * 2) + 8 },
|
||
{ name: 'beams2 cap2x+8', opts: { num_beams: 2, no_repeat_ngram_size: 3 }, cap: (n) => Math.ceil(n * 2) + 8 }
|
||
];
|
||
|
||
const tok = pipe.tokenizer;
|
||
for (const cfg of CONFIGS) {
|
||
console.log('=== ' + cfg.name + ' ===');
|
||
let total = 0;
|
||
for (const text of CASES) {
|
||
const nIn = tok(text).input_ids.dims.at(-1);
|
||
const opts = Object.assign({ src_lang: 'eng_Latn', tgt_lang: 'spa_Latn' }, cfg.opts || {});
|
||
if (cfg.cap) opts.max_new_tokens = cfg.cap(nIn);
|
||
const s = Date.now();
|
||
const out = await pipe(text, opts);
|
||
const ms = Date.now() - s;
|
||
total += ms;
|
||
const got = (Array.isArray(out) ? out[0] : out).translation_text.trim();
|
||
const flag = (!/[??]/.test(text) && /[??]/.test(got)) ? ' <-- INVENTED A QUESTION' : '';
|
||
console.log(' ' + String(ms).padStart(5) + 'ms in' + String(nIn).padStart(3) + ' ' + got + flag);
|
||
}
|
||
console.log(' total ' + total + 'ms\n');
|
||
}
|