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>
91 lines
4.3 KiB
Bash
Executable File
91 lines
4.3 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# install-latest.sh — quit, replace, relaunch. One step, repeatable.
|
|
#
|
|
# Doing this by hand meant three builds in a row were staged and never
|
|
# installed, so the app under test was two versions behind what the log said
|
|
# was fixed. That is worse than no build at all.
|
|
#
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
REL="$ROOT/Releases"
|
|
DIST="$ROOT/app/dist"
|
|
APPS="/Applications"
|
|
NAME="Church Translate.app"
|
|
|
|
# Both places, newest first. `npm run dist` writes to app/dist and only a
|
|
# release copy lands in Releases/ — looking in one of them installed a build
|
|
# that was two versions behind the one just compiled, and said "3.8.0 -> 3.8.0"
|
|
# while doing it. That is the exact failure this script was written to stop.
|
|
DMG="$(ls -t "$REL"/*.dmg "$DIST"/*.dmg 2>/dev/null | head -1)"
|
|
[ -n "$DMG" ] || { echo "No .dmg in $REL or $DIST"; exit 1; }
|
|
echo "installing: $(basename "$DMG")"
|
|
|
|
# Stale volumes from earlier installs keep the old version's name around and
|
|
# make it genuinely hard to tell what is mounted.
|
|
for v in /Volumes/*Church\ Translate*; do
|
|
[ -d "$v" ] && { echo " ejecting stale $v"; hdiutil detach "$v" -quiet -force || true; }
|
|
done
|
|
|
|
# NEVER install over a live service. main.js deliberately blocks its own window
|
|
# close with a confirmation dialog while translation is live, so that nobody
|
|
# takes the captions down mid-sermon — and this script's force-terminate would
|
|
# walk straight through that protection. Ask the server instead.
|
|
if pgrep -f "$NAME/Contents/MacOS" >/dev/null; then
|
|
CFG="$HOME/Library/Application Support/Church Translate/data/config.json"
|
|
KEY="$(/usr/bin/python3 -c "import json,sys;print(json.load(open(sys.argv[1])).get('controlToken',''))" "$CFG" 2>/dev/null || true)"
|
|
if [ -n "$KEY" ]; then
|
|
ST="$(curl -s --max-time 4 "http://127.0.0.1:3100/api/status.txt?key=$KEY" 2>/dev/null || true)"
|
|
if [ "$ST" = "live" ]; then
|
|
echo "REFUSING TO INSTALL: translation is LIVE right now."
|
|
echo "Stop the service first — installing would cut the captions for everyone listening."
|
|
exit 2
|
|
fi
|
|
[ -n "$ST" ] && echo " service is '$ST' (not live) — safe to replace"
|
|
fi
|
|
echo " quitting the running app"
|
|
osascript -e 'tell application "Church Translate" to quit' 2>/dev/null || true
|
|
for _ in $(seq 1 50); do # 25s: it shuts three model children down first
|
|
pgrep -f "$NAME/Contents/MacOS" >/dev/null || break
|
|
sleep 0.5
|
|
done
|
|
# The forked children (server, mt, tts) exit with the parent; if the parent
|
|
# is wedged, take it down rather than installing under a live process.
|
|
pgrep -f "$NAME/Contents/MacOS" >/dev/null && {
|
|
echo " did not quit in 25s — terminating"
|
|
pkill -f "$NAME/Contents/MacOS" || true
|
|
sleep 2
|
|
}
|
|
fi
|
|
|
|
MNT="$(mktemp -d /tmp/ct-mnt.XXXXXX)"
|
|
hdiutil attach "$DMG" -nobrowse -quiet -mountpoint "$MNT"
|
|
trap 'hdiutil detach "$MNT" -quiet -force >/dev/null 2>&1 || true; rmdir "$MNT" 2>/dev/null || true' EXIT
|
|
|
|
# Prove the replacement exists BEFORE removing what works.
|
|
[ -d "$MNT/$NAME" ] || { echo "no $NAME inside the dmg"; exit 1; }
|
|
NEW="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$MNT/$NAME/Contents/Info.plist")"
|
|
NEWB="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$MNT/$NAME/Contents/Info.plist" 2>/dev/null || echo '?')"
|
|
OLD="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APPS/$NAME/Contents/Info.plist" 2>/dev/null || echo 'none')"
|
|
OLDB="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$APPS/$NAME/Contents/Info.plist" 2>/dev/null || echo '0')"
|
|
echo " $OLD (build $OLDB) -> $NEW (build $NEWB)"
|
|
|
|
# Installing the same build over itself looks identical to a successful
|
|
# install and is how a fix gets reported as shipped when it is not on disk.
|
|
if [ "$OLD" = "$NEW" ] && [ "$OLDB" = "$NEWB" ]; then
|
|
echo " NOTE: that is the build already installed — nothing changed."
|
|
echo " Did you forget to bump version/buildNumber, or to run npm run dist?"
|
|
fi
|
|
|
|
rm -rf "$APPS/$NAME"
|
|
ditto "$MNT/$NAME" "$APPS/$NAME"
|
|
|
|
# The build is deliberately unsigned, so a fresh copy off a disk image carries
|
|
# a quarantine flag and Gatekeeper refuses to open it without a right-click.
|
|
# This is the user's own locally built app being installed at their request.
|
|
xattr -dr com.apple.quarantine "$APPS/$NAME" 2>/dev/null || true
|
|
|
|
open -a "$APPS/$NAME"
|
|
echo " launched"
|