Files
church-translate/app/_test-multi.mjs
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

69 lines
3.4 KiB
JavaScript

/* Integration test: fork the REAL mt-service.js the way main.js does and prove
the batched path returns the same text as the one-at-a-time path. */
import { fork } from 'child_process';
import os from 'os'; import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const MODELS_DIR = path.join(os.homedir(), 'Library/Application Support/Church Translate Models');
const LANGS = ['spa_Latn','por_Latn','fra_Latn','zho_Hans','hat_Latn','hin_Deva','kor_Hang','vie_Latn','arb_Arab'];
const TEXTS = [
'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, and that is the kind of faith we are called to',
'Amen',
];
const child = fork(path.join(__dirname, 'mt-service.js'), [], { stdio: ['ignore','pipe','pipe','ipc'] });
child.stderr.on('data', d => process.stderr.write('[mt] ' + d));
let seq = 0; const pending = new Map();
child.on('message', (m) => {
if (m.type === 'ready') { pending.get('ready')?.(); return; }
if (m.type === 'note') { console.log(' note: ' + m.message); return; }
if (m.type === 'progress') return;
const w = pending.get(m.id); if (!w) return; pending.delete(m.id);
if (m.type === 'error') w.reject(new Error(m.message));
else w.resolve(m.type === 'multi' ? m.texts : m.text);
});
const send = (payload) => new Promise((resolve, reject) => {
const id = ++seq; pending.set(id, { resolve, reject });
child.send({ ...payload, id });
});
const ready = new Promise(r => pending.set('ready', r));
child.send({ type: 'load', model: 'Xenova/nllb-200-distilled-600M', dtype: 'fp32', cacheDir: MODELS_DIR });
const t0 = Date.now();
await ready;
console.log('model ready in ' + ((Date.now()-t0)/1000).toFixed(1) + 's\n');
const one = (text, tgt) => send({ type:'translate', text, src:'eng_Latn', tgt });
const many = (text, tgts) => send({ type:'translateMulti', text, src:'eng_Latn', tgts });
await one(TEXTS[0], 'spa_Latn'); await many(TEXTS[0], ['spa_Latn','fra_Latn']); // warm
let mismatches = 0, checks = 0;
console.log('text N | serial | batched | speedup | match');
console.log('-----------------------------+----------+----------+---------+------');
for (const text of TEXTS) {
for (const n of [3, 9]) {
const langs = LANGS.slice(0, n);
let s = Date.now(); const so = {}; for (const l of langs) so[l] = await one(text, l); s = Date.now()-s;
let b = Date.now(); const bo = await many(text, langs); b = Date.now()-b;
let same = 0; for (const l of langs) { checks++; if (so[l] === bo[l]) same++; else { mismatches++; } }
console.log(
(text.slice(0,26)).padEnd(27) + String(n).padStart(2) + ' | ' +
(s+'ms').padStart(8) + ' | ' + (b+'ms').padStart(8) + ' | ' +
((s/b).toFixed(2)+'x').padStart(7) + ' | ' + same + '/' + n);
if (same !== n) for (const l of langs) if (so[l] !== bo[l])
console.log(' ' + l + '\n serial : ' + so[l] + '\n batched: ' + bo[l]);
}
}
console.log('\n' + (checks - mismatches) + '/' + checks + ' identical across ' + TEXTS.length + ' texts');
/* Edge cases */
console.log('\nedge cases:');
console.log(' one target :', JSON.stringify(await many('God is good', ['spa_Latn'])));
console.log(' zero targets :', JSON.stringify(await many('God is good', [])));
console.log(' duplicate :', JSON.stringify(await many('God is good', ['spa_Latn','spa_Latn'])));
child.kill();