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,80 @@
|
||||
/* Drive the REAL live-audio module the way the mic feed does, and see whether
|
||||
the stream it produces is continuous. */
|
||||
import { execFileSync } from 'child_process';
|
||||
import fs from 'fs'; import os from 'os'; import path from 'path';
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
const { LiveAudio } = require('./server/live-audio.js');
|
||||
|
||||
const RATE = 16000, CHUNK = 11200; // MIC_CHUNK
|
||||
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'micfeed-'));
|
||||
|
||||
function encode(samples, hz) {
|
||||
const pcm = Buffer.alloc(samples * 2);
|
||||
for (let i = 0; i < samples; i++) pcm.writeInt16LE(Math.round(9000 * Math.sin(i / hz)), i * 2);
|
||||
const wav = Buffer.alloc(44 + pcm.length);
|
||||
wav.write('RIFF', 0); wav.writeUInt32LE(36 + pcm.length, 4);
|
||||
wav.write('WAVE', 8); wav.write('fmt ', 12);
|
||||
wav.writeUInt32LE(16, 16); wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22);
|
||||
wav.writeUInt32LE(RATE, 24); wav.writeUInt32LE(RATE * 2, 28);
|
||||
wav.writeUInt16LE(2, 32); wav.writeUInt16LE(16, 34);
|
||||
wav.write('data', 36); wav.writeUInt32LE(pcm.length, 40);
|
||||
pcm.copy(wav, 44);
|
||||
const w = path.join(d, 'c.wav'), a = path.join(d, 'c.aac');
|
||||
fs.writeFileSync(w, wav);
|
||||
if (fs.existsSync(a)) fs.unlinkSync(a);
|
||||
execFileSync('/usr/bin/afconvert', ['-f', 'adts', '-d', 'aac@' + RATE, '-b', '48000', w, a]);
|
||||
return fs.readFileSync(a);
|
||||
}
|
||||
|
||||
const live = new LiveAudio((m) => console.log(' [live] ' + m));
|
||||
const st = live.ensure('eng_Latn', RATE);
|
||||
st.start();
|
||||
/* Count silence frames the pump invents because the queue ran dry. */
|
||||
const SIL = require('./server/live-audio.js').silenceFrame(RATE);
|
||||
let underrun = 0, real = 0, counting = false;
|
||||
const origClose = st.closeSegment.bind(st);
|
||||
st.closeSegment = function () {
|
||||
if (counting) for (const f of this.building) { if (f === SIL) underrun++; else real++; }
|
||||
return origClose();
|
||||
};
|
||||
console.log('pushing 700ms chunks every 700ms for 14s, exactly as the mic feed does…\n');
|
||||
|
||||
const clip = encode(CHUNK, 9);
|
||||
let pushed = 0;
|
||||
const t0 = Date.now();
|
||||
let stalls = 0;
|
||||
function schedule(delay) { setTimeout(tickPush, delay); }
|
||||
function tickPush() {
|
||||
st.pushContinuous(clip);
|
||||
pushed++;
|
||||
if (pushed === 2) counting = true; // skip the startup seed
|
||||
const backlogMs = (st.pendingFrames * 1024) / RATE * 1000;
|
||||
const wall = Date.now() - t0;
|
||||
const audioIn = pushed * 700;
|
||||
console.log(' chunk ' + String(pushed).padStart(2) +
|
||||
' wall ' + String(wall).padStart(5) + 'ms' +
|
||||
' audio pushed ' + String(audioIn).padStart(5) + 'ms' +
|
||||
' backlog ' + backlogMs.toFixed(0).padStart(4) + 'ms' +
|
||||
' segments ' + st.segments.length);
|
||||
/* Two deliberate stalls: 500ms and 900ms late, modelling a blocked event
|
||||
loop or two chunks bunched by the socket. */
|
||||
let nextIn = 700;
|
||||
if (pushed === 8) { nextIn = 1200; stalls++; }
|
||||
if (pushed === 14) { nextIn = 1600; stalls++; }
|
||||
if (pushed < 20) { schedule(nextIn); return; }
|
||||
if (pushed >= 20) {
|
||||
const held = st.segments.length * (st.framesPerSegment * 1024 / RATE);
|
||||
console.log('\n produced ' + st.segments.length + ' segments (' + held.toFixed(1) + 's held)');
|
||||
console.log(' real audio frames : ' + real);
|
||||
console.log(' SILENCE frames the pump invented on underrun : ' + underrun +
|
||||
' (' + (underrun * 64) + ' ms of gaps)');
|
||||
console.log(' audio in: ' + (pushed * 0.7).toFixed(1) + 's wall: ' + ((Date.now()-t0)/1000).toFixed(1) + 's');
|
||||
console.log(' deliberate stalls injected: ' + stalls + ' (one 500ms late, one 900ms late)');
|
||||
console.log(' UNDERRUNS reported by the stream: ' + (st.underruns || 0));
|
||||
fs.rmSync(d, { recursive: true, force: true });
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
schedule(700);
|
||||
|
||||
Reference in New Issue
Block a user