Files
christianandClaude Opus 5 c9eef0672c 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>
2026-09-23 15:41:21 -04:00

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"