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>
76 lines
3.0 KiB
JavaScript
76 lines
3.0 KiB
JavaScript
/*
|
|
* app-log.js — the persistent log this app never had.
|
|
*
|
|
* Every child (voice process, congregation server, translation model) writes
|
|
* to stdout, and main.js forwards that to ITS stdout — which, for an app
|
|
* launched from the Finder, is nowhere. So the morning a service went wrong
|
|
* there was a caption transcript and not one line saying what the voice
|
|
* process had decided to do. This module tees everything to
|
|
* <dataDir>/logs/app.log, rotated so it can never fill a disk.
|
|
*
|
|
* Deliberately boring: synchronous appends (a crash must not lose the line
|
|
* that explains it), a size cap, three generations, and every failure
|
|
* swallowed — logging must never be the thing that takes the service down.
|
|
*/
|
|
'use strict';
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const MAX_BYTES = 2 * 1024 * 1024; // rotate past 2 MB
|
|
const KEEP = 3; // app.log, app.log.1, app.log.2
|
|
|
|
function createAppLog(dir, name) {
|
|
name = name || 'app.log';
|
|
const file = path.join(dir, name);
|
|
let size = 0;
|
|
try { fs.mkdirSync(dir, { recursive: true }); } catch (_) {}
|
|
try { size = fs.statSync(file).size; } catch (_) { size = 0; }
|
|
|
|
function rotate() {
|
|
try {
|
|
for (let i = KEEP - 1; i >= 1; i--) {
|
|
const from = file + (i === 1 ? '' : '.' + (i - 1));
|
|
const to = file + '.' + i;
|
|
try { fs.renameSync(from, to); } catch (_) {}
|
|
}
|
|
} catch (_) {}
|
|
size = 0;
|
|
}
|
|
|
|
function stamp() {
|
|
const d = new Date();
|
|
const p = (n, w) => String(n).padStart(w || 2, '0');
|
|
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' +
|
|
p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds()) + '.' + p(d.getMilliseconds(), 3);
|
|
}
|
|
|
|
/* Write one or more lines under a source tag. Multi-line chunks from a
|
|
child's stdout are split so every line carries its own stamp and tag. */
|
|
function write(tag, chunk) {
|
|
try {
|
|
const text = String(chunk == null ? '' : chunk);
|
|
if (!text.trim()) return;
|
|
const lines = text.replace(/\r/g, '').split('\n').filter((l) => l.length);
|
|
const out = lines.map((l) => stamp() + ' [' + tag + '] ' + l).join('\n') + '\n';
|
|
/* Bytes, not characters: accented Spanish and CJK log lines are 2-3
|
|
bytes a character, and a character count let a generation reach 6 MB. */
|
|
const bytes = Buffer.byteLength(out, 'utf8');
|
|
if (size + bytes > MAX_BYTES) rotate();
|
|
fs.appendFileSync(file, out);
|
|
size += bytes;
|
|
} catch (_) { /* never let logging take anything down */ }
|
|
}
|
|
|
|
/* Attach to a child process: tee stdout/stderr to the file. Returns the
|
|
child for chaining. The caller may still forward to its own stdout. */
|
|
function tee(child, tag) {
|
|
if (child && child.stdout) child.stdout.on('data', (d) => write(tag, d));
|
|
if (child && child.stderr) child.stderr.on('data', (d) => write(tag + ':err', d));
|
|
return child;
|
|
}
|
|
|
|
return { write, tee, file, rotate };
|
|
}
|
|
|
|
module.exports = { createAppLog, MAX_BYTES, KEEP };
|