Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4aa30699c9 | ||
|
|
6753a94e1c |
+18
-6
@@ -2983,14 +2983,26 @@ async function downloadUpdate() {
|
|||||||
try {
|
try {
|
||||||
const r = await window.engineHost.updateDownload(updFound.asset.url, updFound.asset.name);
|
const r = await window.engineHost.updateDownload(updFound.asset.url, updFound.asset.name);
|
||||||
if (!r.ok) { $('updProgress').textContent = r.error || 'Download failed.'; btn.disabled = false; return; }
|
if (!r.ok) { $('updProgress').textContent = r.error || 'Download failed.'; btn.disabled = false; return; }
|
||||||
$('updProgress').textContent = 'Saved to Downloads.';
|
$('updProgress').textContent = 'Downloaded. Installing\u2026';
|
||||||
/* Say what is left to do. Nothing installs itself here on purpose — see
|
|
||||||
the note on the main-process side. */
|
|
||||||
$('updWhat').textContent = 'Downloaded ' + updFound.tag + ' (' + fmtMB(r.bytes) + '). ' +
|
|
||||||
'Quit Church Translate, open the disk image, and drag the app over the one in ' +
|
|
||||||
'Applications. Your settings, models and voices are untouched.';
|
|
||||||
log('update ' + updFound.tag + ' downloaded to ' + r.path, 'ok');
|
log('update ' + updFound.tag + ' downloaded to ' + r.path, 'ok');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Straight into the install. The app quits, a helper swaps the bundle and
|
||||||
|
* reopens it — so from here the window closing IS the update working, and
|
||||||
|
* saying so before it happens stops that looking like a crash.
|
||||||
|
*/
|
||||||
|
$('updWhat').textContent = 'Installing ' + updFound.tag + '. Church Translate will close and ' +
|
||||||
|
'reopen by itself in a few seconds. Your settings, models and voices are untouched.';
|
||||||
|
const ins = await window.engineHost.updateInstall(r.path);
|
||||||
|
if (!ins || !ins.ok) {
|
||||||
|
/* Still useful: the file is downloaded, so fall back to the manual route
|
||||||
|
rather than leaving them with nothing. */
|
||||||
|
$('updProgress').textContent = (ins && ins.error) || 'Could not install.';
|
||||||
|
$('updWhat').textContent = 'Downloaded, but it could not install itself. Quit Church ' +
|
||||||
|
'Translate, open the disk image in Downloads, and drag the app over the one in Applications.';
|
||||||
try { await window.engineHost.updateReveal(r.path); } catch (_) {}
|
try { await window.engineHost.updateReveal(r.path); } catch (_) {}
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
$('updProgress').textContent = 'Download failed: ' + e.message;
|
$('updProgress').textContent = 'Download failed: ' + e.message;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
|
|||||||
+1
-1
@@ -391,7 +391,7 @@
|
|||||||
<div id="updFound" hidden>
|
<div id="updFound" hidden>
|
||||||
<p class="hint" id="updWhat"></p>
|
<p class="hint" id="updWhat"></p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button class="btn" id="updGet">Download it</button>
|
<button class="btn" id="updGet">Download and install</button>
|
||||||
<span class="hint" id="updProgress"></span>
|
<span class="hint" id="updProgress"></span>
|
||||||
</div>
|
</div>
|
||||||
<details id="updNotesBox"><summary class="hint">What changed</summary>
|
<details id="updNotesBox"><summary class="hint">What changed</summary>
|
||||||
|
|||||||
+91
@@ -2259,6 +2259,97 @@ ipcMain.handle('update:download', async (_e, url, name) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ─────────────────────────── installing it, in place ──────────────────────
|
||||||
|
*
|
||||||
|
* An app cannot replace itself while it is running, so the swap is done by a
|
||||||
|
* small script that outlives us: it waits for this process to exit, moves the
|
||||||
|
* old bundle aside, copies the new one in, and relaunches.
|
||||||
|
*
|
||||||
|
* MOVED ASIDE, NOT DELETED. If the copy fails half way — a full disk, a bad
|
||||||
|
* download, the volume going away — the old app is put straight back and
|
||||||
|
* relaunched. The failure mode this is written against is a campus Mac with no
|
||||||
|
* working app and a service in an hour, which is far worse than not updating.
|
||||||
|
*
|
||||||
|
* No sudo: /Applications is admin-writable on macOS and the bundle belongs to
|
||||||
|
* the operator, so nothing here needs a password. If that is ever not true the
|
||||||
|
* copy fails, the old app comes back, and the log says so.
|
||||||
|
*/
|
||||||
|
ipcMain.handle('update:install', async (_e, dmgPath) => {
|
||||||
|
const dmg = String(dmgPath || '');
|
||||||
|
if (!dmg || !fs.existsSync(dmg)) return { ok: false, error: 'That installer is no longer on disk.' };
|
||||||
|
|
||||||
|
/* Never mid-service. Quitting the app stops every listener's captions, and an
|
||||||
|
update is never worth that — the operator stops translation first. */
|
||||||
|
if (engineIsLive) {
|
||||||
|
return { ok: false, error: 'Translation is live. Press Stop translating first, then install.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const appPath = app.getPath('exe').replace(/\/Contents\/MacOS\/[^/]+$/, '');
|
||||||
|
if (!/\.app$/.test(appPath)) {
|
||||||
|
return { ok: false, error: 'This copy is not running from an .app bundle, so it cannot replace itself.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmp = path.join(require('os').tmpdir(), 'ct-install-' + process.pid + '.sh');
|
||||||
|
const mount = '/Volumes/ChurchTranslateUpdate';
|
||||||
|
const aside = appPath + '.replacing-' + process.pid;
|
||||||
|
const script = [
|
||||||
|
'#!/bin/bash',
|
||||||
|
'# Written by Church Translate to swap itself. Safe to delete.',
|
||||||
|
'set -u',
|
||||||
|
'DMG=' + JSON.stringify(dmg),
|
||||||
|
'APP=' + JSON.stringify(appPath),
|
||||||
|
'ASIDE=' + JSON.stringify(aside),
|
||||||
|
'MNT=' + JSON.stringify(mount),
|
||||||
|
'PID=' + process.pid,
|
||||||
|
'LOG="$HOME/Library/Application Support/Church Translate/data/logs/install.log"',
|
||||||
|
'mkdir -p "$(dirname "$LOG")"',
|
||||||
|
'say(){ echo "$(date "+%Y-%m-%d %H:%M:%S") $*" >> "$LOG"; }',
|
||||||
|
'say "installing $DMG into $APP"',
|
||||||
|
'# wait for the app to actually be gone (up to 30s)',
|
||||||
|
'for i in $(seq 1 60); do kill -0 "$PID" 2>/dev/null || break; sleep 0.5; done',
|
||||||
|
'if kill -0 "$PID" 2>/dev/null; then say "app did not exit; aborting"; exit 1; fi',
|
||||||
|
'hdiutil detach "$MNT" -quiet 2>/dev/null',
|
||||||
|
'if ! hdiutil attach "$DMG" -nobrowse -quiet -mountpoint "$MNT"; then say "could not mount"; open -a "$APP"; exit 1; fi',
|
||||||
|
'NEW="$MNT/Church Translate.app"',
|
||||||
|
'# refuse to touch the installed app unless the replacement is really there',
|
||||||
|
'if [ ! -f "$NEW/Contents/Info.plist" ]; then say "disk image has no app"; hdiutil detach "$MNT" -quiet; open -a "$APP"; exit 1; fi',
|
||||||
|
'rm -rf "$ASIDE"',
|
||||||
|
'if ! mv "$APP" "$ASIDE"; then say "could not move the old app aside"; hdiutil detach "$MNT" -quiet; open -a "$APP"; exit 1; fi',
|
||||||
|
'if cp -R "$NEW" "$APP"; then',
|
||||||
|
' xattr -dr com.apple.quarantine "$APP" 2>/dev/null',
|
||||||
|
' say "installed $( /usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$APP/Contents/Info.plist" 2>/dev/null )"',
|
||||||
|
' rm -rf "$ASIDE"',
|
||||||
|
'else',
|
||||||
|
' say "copy failed — putting the old app back"',
|
||||||
|
' rm -rf "$APP"; mv "$ASIDE" "$APP"',
|
||||||
|
'fi',
|
||||||
|
'hdiutil detach "$MNT" -quiet 2>/dev/null',
|
||||||
|
'open -a "$APP"',
|
||||||
|
'say "relaunched"',
|
||||||
|
'rm -f "$0"'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(tmp, script, { mode: 0o755 });
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: 'Could not prepare the installer: ' + e.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detached and unref'd, or it dies with us the moment we quit. */
|
||||||
|
try {
|
||||||
|
const child = spawn('/bin/bash', [tmp], { detached: true, stdio: 'ignore' });
|
||||||
|
child.unref();
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: 'Could not start the installer: ' + e.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Give the renderer a moment to show the message before we go. */
|
||||||
|
forceQuit = true;
|
||||||
|
setTimeout(() => { try { app.quit(); } catch (_) {} }, 600);
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('update:reveal', (_e, p2) => { try { shell.showItemInFolder(String(p2)); return true; } catch (_) { return false; } });
|
ipcMain.handle('update:reveal', (_e, p2) => { try { shell.showItemInFolder(String(p2)); return true; } catch (_) { return false; } });
|
||||||
|
|
||||||
ipcMain.handle('config:get', () => loadConfig());
|
ipcMain.handle('config:get', () => loadConfig());
|
||||||
|
|||||||
+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": 113,
|
"buildNumber": 115,
|
||||||
"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": "113",
|
"buildVersion": "115",
|
||||||
"win": {
|
"win": {
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ contextBridge.exposeInMainWorld('engineHost', {
|
|||||||
updateCheck: () => ipcRenderer.invoke('update:check'),
|
updateCheck: () => ipcRenderer.invoke('update:check'),
|
||||||
updateDownload: (u, n) => ipcRenderer.invoke('update:download', u, n),
|
updateDownload: (u, n) => ipcRenderer.invoke('update:download', u, n),
|
||||||
updateReveal: (p) => ipcRenderer.invoke('update:reveal', p),
|
updateReveal: (p) => ipcRenderer.invoke('update:reveal', p),
|
||||||
|
updateInstall: (p) => ipcRenderer.invoke('update:install', p),
|
||||||
onUpdateProgress: (fn) => ipcRenderer.on('update:progress', (_e, p) => fn(p)),
|
onUpdateProgress: (fn) => ipcRenderer.on('update:progress', (_e, p) => fn(p)),
|
||||||
settingsExport: () => ipcRenderer.invoke('settings:export'),
|
settingsExport: () => ipcRenderer.invoke('settings:export'),
|
||||||
settingsImport: () => ipcRenderer.invoke('settings:import'),
|
settingsImport: () => ipcRenderer.invoke('settings:import'),
|
||||||
|
|||||||
+21
-2
@@ -500,6 +500,23 @@ const ENGINE_STALE_MS = 12000;
|
|||||||
* with one plain sentence does. Computed here so every operator surface —
|
* with one plain sentence does. Computed here so every operator surface —
|
||||||
* in-app pane, browser page — shows the same verdict.
|
* in-app pane, browser page — shows the same verdict.
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
|
* WHEN IT IS BEHIND, SAY WHAT IS LOADING IT.
|
||||||
|
*
|
||||||
|
* Translation cost is roughly linear in the number of active languages, through
|
||||||
|
* ONE NLLB process — measured, five languages in 1.9 s. So the single most
|
||||||
|
* useful fact about a machine that is falling behind is how many languages it
|
||||||
|
* is carrying, and that number was computed one function away and never shown.
|
||||||
|
* A campus reported "constantly behind, in the red" and the cause took an
|
||||||
|
* afternoon to find; it was eight languages on hardware tuned against one.
|
||||||
|
*/
|
||||||
|
function loadNote() {
|
||||||
|
const n = activeLanguages().length;
|
||||||
|
if (n < 2) return '';
|
||||||
|
return ' Carrying ' + n + ' languages' +
|
||||||
|
(config.allowAllLanguages ? ', and listeners may add more' : '') + '.';
|
||||||
|
}
|
||||||
|
|
||||||
function healthVerdict(connected, stale) {
|
function healthVerdict(connected, stale) {
|
||||||
const es = state.engineStatus || {};
|
const es = state.engineStatus || {};
|
||||||
if (!connected) return { level: 'red', detail: 'The translation app is not connected.' };
|
if (!connected) return { level: 'red', detail: 'The translation app is not connected.' };
|
||||||
@@ -507,9 +524,11 @@ function healthVerdict(connected, stale) {
|
|||||||
if (!state.live) return { level: 'green', detail: 'Ready.' };
|
if (!state.live) return { level: 'green', detail: 'Ready.' };
|
||||||
if (!es.modelsReady) return { level: 'amber', detail: 'Models are still loading.' };
|
if (!es.modelsReady) return { level: 'amber', detail: 'Models are still loading.' };
|
||||||
if ((es.lagMs || 0) > 12000 || (es.captionOnlyFinals || 0) > 0)
|
if ((es.lagMs || 0) > 12000 || (es.captionOnlyFinals || 0) > 0)
|
||||||
return { level: 'red', detail: 'Falling badly behind — some sentences are captions only.' };
|
return { level: 'red',
|
||||||
|
detail: 'Falling badly behind — some sentences are captions only.' + loadNote() };
|
||||||
if ((es.lagMs || 0) > 6000 || (es.queue || 0) >= 2 || (es.degradedFinals || 0) > 0)
|
if ((es.lagMs || 0) > 6000 || (es.queue || 0) >= 2 || (es.degradedFinals || 0) > 0)
|
||||||
return { level: 'amber', detail: 'Running behind — idle languages may be trimmed to catch up.' };
|
return { level: 'amber',
|
||||||
|
detail: 'Running behind — idle languages may be trimmed to catch up.' + loadNote() };
|
||||||
if (es.cloud && es.cloud.parked)
|
if (es.cloud && es.cloud.parked)
|
||||||
return { level: 'amber', detail: 'Natural voices are down — the built-in voices have taken over.' };
|
return { level: 'amber', detail: 'Natural voices are down — the built-in voices have taken over.' };
|
||||||
/* A fallback in the last minute means one language just changed voice
|
/* A fallback in the last minute means one language just changed voice
|
||||||
|
|||||||
Reference in New Issue
Block a user