Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92e86e415f | ||
|
|
73ec7f1436 |
+38
-2
@@ -1794,6 +1794,7 @@ async function pump() {
|
|||||||
|
|
||||||
state.busy = true;
|
state.busy = true;
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
|
let jobAudioMs = 0;
|
||||||
try {
|
try {
|
||||||
/* job.at is when the audio ENDED. Live translation of stale audio is
|
/* job.at is when the audio ENDED. Live translation of stale audio is
|
||||||
worthless, but the WORDS are not: however old the job, it still gets
|
worthless, but the WORDS are not: however old the job, it still gets
|
||||||
@@ -1805,6 +1806,9 @@ async function pump() {
|
|||||||
/* Near-silence telemetry (log-only this release): a chunk whose average
|
/* Near-silence telemetry (log-only this release): a chunk whose average
|
||||||
level barely clears the noise floor is hallucination fuel. */
|
level barely clears the noise floor is hallucination fuel. */
|
||||||
const audio = flatten(job.frames);
|
const audio = flatten(job.frames);
|
||||||
|
/* How much speech this chunk actually holds. The step-down below compares
|
||||||
|
against it, because "slow" only means anything relative to the audio. */
|
||||||
|
jobAudioMs = (audio.length / SAMPLE_RATE) * 1000;
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
for (let i = 0; i < audio.length; i += 4) sum += audio[i] * audio[i];
|
for (let i = 0; i < audio.length; i += 4) sum += audio[i] * audio[i];
|
||||||
const meanRms = Math.sqrt(sum / Math.ceil(audio.length / 4));
|
const meanRms = Math.sqrt(sum / Math.ceil(audio.length / 4));
|
||||||
@@ -1908,11 +1912,43 @@ async function pump() {
|
|||||||
* talk, and the backlog only grows — step down before the service is lost
|
* talk, and the backlog only grows — step down before the service is lost
|
||||||
* rather than after. Measured on the Intel laptop: 12-25 s, every sentence.
|
* rather than after. Measured on the Intel laptop: 12-25 s, every sentence.
|
||||||
*/
|
*/
|
||||||
if (took > 8000) {
|
/*
|
||||||
|
* SLOWER THAN THE AUDIO IT COVERS — not slower than some constant.
|
||||||
|
*
|
||||||
|
* This used to fire only past 8 s, on the reasoning that 8 s meant slower
|
||||||
|
* than people talk. That was true when a chunk could be 8 s long. Chunks
|
||||||
|
* are capped at HARD_CUT_MAX, 5.5 s, so a 4.5 s chunk taking 4.3 s is
|
||||||
|
* already losing ground and never came near the old threshold.
|
||||||
|
*
|
||||||
|
* MEASURED on a campus M4 Pro: every sentence 4.2-4.7 s against roughly
|
||||||
|
* 4-5 s of audio, a queue 46 deep and a lag of 152 s still climbing — with
|
||||||
|
* nobody listening and no language being translated, so this was
|
||||||
|
* transcription alone. The one mechanism built to catch exactly that could
|
||||||
|
* not fire, because 4.4 s is not 8 s.
|
||||||
|
*
|
||||||
|
* The queue test is what separates "this machine cannot keep up" from one
|
||||||
|
* slow sentence: a backlog that is three deep and being fed faster than it
|
||||||
|
* drains does not recover on its own.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* 0.85 and a backlog of five, both measured rather than chosen.
|
||||||
|
*
|
||||||
|
* The Mini's sentences ran 4.4 s against 4.5 s of audio — nominally FASTER
|
||||||
|
* than real time, while the queue sat at 46 and the lag climbed past 150 s.
|
||||||
|
* Per-sentence time alone therefore misses it: the work outside this timer
|
||||||
|
* (segmenting, the preview model sharing the GPU, the scheduler) is enough
|
||||||
|
* to lose on its own. The margin catches "near enough to real time to be
|
||||||
|
* losing"; the queue depth is the proof that it actually is, and keeps one
|
||||||
|
* slow sentence on a healthy machine from touching anything.
|
||||||
|
*/
|
||||||
|
const losingGround = jobAudioMs > 0 && took > jobAudioMs * 0.85 && queue.length >= 5;
|
||||||
|
if (took > 8000 || losingGround) {
|
||||||
state.slowRun = (state.slowRun || 0) + 1;
|
state.slowRun = (state.slowRun || 0) + 1;
|
||||||
if (state.slowRun >= 3 && !respawning && lighterAsr(state.asrModelInUse || state.cfg.asrModel)) {
|
if (state.slowRun >= 3 && !respawning && lighterAsr(state.asrModelInUse || state.cfg.asrModel)) {
|
||||||
state.slowRun = 0;
|
state.slowRun = 0;
|
||||||
log('transcription is slower than people are speaking — stepping down to a lighter model', 'err');
|
log('transcription is slower than people are speaking (' + took + ' ms for ' +
|
||||||
|
Math.round(jobAudioMs) + ' ms of audio, ' + queue.length + ' waiting) — ' +
|
||||||
|
'stepping down to a lighter model', 'err');
|
||||||
respawnWorker(state.asrWorker);
|
respawnWorker(state.asrWorker);
|
||||||
}
|
}
|
||||||
} else state.slowRun = 0;
|
} else state.slowRun = 0;
|
||||||
|
|||||||
@@ -440,6 +440,11 @@ function startServer() {
|
|||||||
execPath: nodeBin,
|
execPath: nodeBin,
|
||||||
env: Object.assign({}, process.env, {
|
env: Object.assign({}, process.env, {
|
||||||
CT_FFMPEG: ffmpegPathForChildren(),
|
CT_FFMPEG: ffmpegPathForChildren(),
|
||||||
|
/* So /healthz can report the APP’s version rather than only an asset
|
||||||
|
hash. A campus Mac’s build was otherwise unknowable from the network,
|
||||||
|
which made confirming an update a matter of inference. */
|
||||||
|
CT_APP_VERSION: appBuild().version,
|
||||||
|
CT_APP_BUILD: appBuild().build,
|
||||||
PORT: String(port),
|
PORT: String(port),
|
||||||
DATA_DIR: dataDir(),
|
DATA_DIR: dataDir(),
|
||||||
/* Where the spoken clips land, so the server can hand them to phones. */
|
/* Where the spoken clips land, so the server can hand them to phones. */
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
"name": "church-translate-engine",
|
"name": "church-translate-engine",
|
||||||
"productName": "Church Translate",
|
"productName": "Church Translate",
|
||||||
"version": "3.23.2",
|
"version": "3.23.2",
|
||||||
"buildNumber": 115,
|
"buildNumber": 117,
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Live sermon translation — self-contained: captures audio, transcribes, translates, and serves the congregation page itself",
|
"description": "Live sermon translation — self-contained: captures audio, transcribes, translates, and serves the congregation page itself",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"asar": false,
|
"asar": false,
|
||||||
"buildVersion": "115",
|
"buildVersion": "117",
|
||||||
"win": {
|
"win": {
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
|
|||||||
+16
-1
@@ -1095,7 +1095,22 @@ const server = http.createServer(async (req, res) => {
|
|||||||
}
|
}
|
||||||
if (req.method === 'OPTIONS') { res.writeHead(origin ? 204 : 405).end(); return; }
|
if (req.method === 'OPTIONS') { res.writeHead(origin ? 204 : 405).end(); return; }
|
||||||
|
|
||||||
if (p === '/healthz') { sendJSON(res, 200, { ok: true, live: state.live, build: BUILD }); return; }
|
/*
|
||||||
|
* The APP's version, plainly, to anyone who can reach this port.
|
||||||
|
*
|
||||||
|
* `build` here has always been a hash of the listener-page assets — a
|
||||||
|
* cache-busting token, not a version — so the only way to tell what a campus
|
||||||
|
* Mac was running was to install something and watch the hash move. With an
|
||||||
|
* updater in the field that is the first question anyone asks, so answer it.
|
||||||
|
*/
|
||||||
|
if (p === '/healthz') {
|
||||||
|
sendJSON(res, 200, {
|
||||||
|
ok: true, live: state.live, build: BUILD,
|
||||||
|
version: process.env.CT_APP_VERSION || '',
|
||||||
|
appBuild: process.env.CT_APP_BUILD || ''
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Companion / Stream Deck control -------------------------------
|
// --- Companion / Stream Deck control -------------------------------
|
||||||
// GET or POST, token in ?key= or the X-Control-Token header.
|
// GET or POST, token in ?key= or the X-Control-Token header.
|
||||||
|
|||||||
Reference in New Issue
Block a user