Files
harness-tools/services/dictate/install.sh
T

184 lines
6.0 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
MODEL_REVISION="541d1f99c6b0c3cd0b11a95167540bb8edefd82b"
MODEL_SHA256="3cbdc85877e668ca7b82d0d56770eb1fac76691f55d6b97545e8d61ca588d10d"
MODEL_FILENAME="parakeet-tdt-0.6b-v3.nemo"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPOSITORY_ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
INSTALL_ROOT="$HOME/.local/share/pi-dictate"
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
MODEL_DIR="$INSTALL_ROOT/models/parakeet-tdt-0.6b-v3"
MODEL_PATH="$MODEL_DIR/$MODEL_FILENAME"
DICTIONARY_SOURCE="$SCRIPT_DIR/coding-terms.txt"
DICTIONARY_PATH="$INSTALL_ROOT/config/coding-terms.txt"
DRY_RUN=false
MODEL_SOURCE=""
ACTION=install
usage() {
cat <<'EOF'
Usage: install.sh [--dry-run] [--model-source PATH]
install.sh uninstall [--dry-run]
Installs the pinned service environment, model snapshot, and systemd user units.
Without --model-source, installation downloads the exact pinned model revision.
EOF
}
run() {
printf '+ '
printf '%q ' "$@"
printf '\n'
if ! $DRY_RUN; then
"$@"
fi
}
is_owned_unit() {
local destination="$1"
local source="$2"
[[ -L "$destination" && "$(readlink -f -- "$destination")" == "$(readlink -f -- "$source")" ]] ||
{ [[ -f "$destination" ]] && grep -qxF '# Managed by pi-dictate install.sh' "$destination"; }
}
install_owned_unit() {
local source="$1"
local destination="$2"
if [[ -L "$destination" && "$(readlink -f -- "$destination")" == "$(readlink -f -- "$source")" ]]; then
run rm "$destination"
elif [[ -e "$destination" || -L "$destination" ]]; then
if [[ ! -f "$destination" ]] || ! grep -qxF '# Managed by pi-dictate install.sh' "$destination"; then
printf 'Refusing to replace unmanaged systemd unit: %s\n' "$destination" >&2
return 1
fi
fi
run install -m 0644 "$source" "$destination"
}
remove_owned_unit() {
local destination="$1"
local source="$2"
if [[ -L "$destination" && "$(readlink -f -- "$destination")" == "$(readlink -f -- "$source")" ]]; then
run rm "$destination"
elif [[ -f "$destination" ]] && grep -qxF '# Managed by pi-dictate install.sh' "$destination"; then
run rm "$destination"
elif [[ -e "$destination" || -L "$destination" ]]; then
printf 'Leaving unmanaged unit in place: %s\n' "$destination" >&2
fi
}
while (($#)); do
case "$1" in
uninstall) ACTION=uninstall ;;
--dry-run) DRY_RUN=true ;;
--model-source)
shift
(($#)) || { printf '%s\n' '--model-source requires a path' >&2; exit 2; }
MODEL_SOURCE="$1"
;;
-h|--help) usage; exit 0 ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; usage >&2; exit 2 ;;
esac
shift
done
socket_source="$REPOSITORY_ROOT/systemd/user/pi-dictate.socket"
service_source="$REPOSITORY_ROOT/systemd/user/pi-dictate.service"
socket_link="$UNIT_DIR/pi-dictate.socket"
service_link="$UNIT_DIR/pi-dictate.service"
if [[ "$ACTION" == uninstall ]]; then
for pair in "$socket_link:$socket_source" "$service_link:$service_source"; do
destination="${pair%%:*}"
source="${pair#*:}"
if [[ ( -e "$destination" || -L "$destination" ) ]] && ! is_owned_unit "$destination" "$source"; then
printf 'Refusing to disable or remove unmanaged systemd unit: %s\n' "$destination" >&2
exit 1
fi
done
run systemctl --user disable --now pi-dictate.socket
remove_owned_unit "$socket_link" "$socket_source"
remove_owned_unit "$service_link" "$service_source"
run systemctl --user daemon-reload
run rm -rf "$INSTALL_ROOT"
exit 0
fi
command -v uv >/dev/null || { printf '%s\n' 'uv is required' >&2; exit 1; }
command -v systemctl >/dev/null || { printf '%s\n' 'systemctl is required' >&2; exit 1; }
run mkdir -p "$INSTALL_ROOT" "$MODEL_DIR" "$(dirname "$DICTIONARY_PATH")" "$UNIT_DIR"
run chmod 0700 "$INSTALL_ROOT" "$(dirname "$MODEL_DIR")" "$MODEL_DIR" "$(dirname "$DICTIONARY_PATH")"
if $DRY_RUN; then
printf '+ UV_PROJECT_ENVIRONMENT=%q uv sync --frozen --no-dev --no-editable --project %q\n' "$INSTALL_ROOT/venv" "$SCRIPT_DIR"
else
UV_PROJECT_ENVIRONMENT="$INSTALL_ROOT/venv" uv sync --frozen --no-dev --no-editable --project "$SCRIPT_DIR"
fi
MODEL_TEMP="$MODEL_DIR/.${MODEL_FILENAME}.install.$$"
cleanup_model_temp() {
if ! $DRY_RUN; then
rm -f -- "$MODEL_TEMP"
fi
}
trap cleanup_model_temp EXIT
if [[ -n "$MODEL_SOURCE" ]]; then
[[ -f "$MODEL_SOURCE" ]] || { printf 'Model source not found: %s\n' "$MODEL_SOURCE" >&2; exit 1; }
run cp --reflink=auto "$MODEL_SOURCE" "$MODEL_TEMP"
else
if $DRY_RUN; then
printf '+ download nvidia/parakeet-tdt-0.6b-v3@%s to %q\n' "$MODEL_REVISION" "$MODEL_TEMP"
else
MODEL_REVISION="$MODEL_REVISION" MODEL_PATH="$MODEL_TEMP" \
"$INSTALL_ROOT/venv/bin/python" - <<'PY'
import os
import shutil
from pathlib import Path
from huggingface_hub import hf_hub_download
source = hf_hub_download(
repo_id="nvidia/parakeet-tdt-0.6b-v3",
filename="parakeet-tdt-0.6b-v3.nemo",
revision=os.environ["MODEL_REVISION"],
)
destination = Path(os.environ["MODEL_PATH"])
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, destination)
PY
fi
fi
if ! $DRY_RUN; then
actual_sha256="$(sha256sum "$MODEL_TEMP" | cut -d' ' -f1)"
if [[ "$actual_sha256" != "$MODEL_SHA256" ]]; then
rm -f "$MODEL_TEMP"
printf 'Model checksum mismatch: expected %s, got %s\n' "$MODEL_SHA256" "$actual_sha256" >&2
exit 1
fi
fi
run chmod 0400 "$MODEL_TEMP"
run mv -f "$MODEL_TEMP" "$MODEL_PATH"
trap - EXIT
if [[ ! -e "$DICTIONARY_PATH" ]]; then
run cp "$DICTIONARY_SOURCE" "$DICTIONARY_PATH"
run chmod 0600 "$DICTIONARY_PATH"
else
printf 'Keeping existing coding dictionary: %s\n' "$DICTIONARY_PATH"
fi
install_owned_unit "$socket_source" "$socket_link"
install_owned_unit "$service_source" "$service_link"
run systemctl --user daemon-reload
run systemctl --user enable --now pi-dictate.socket
if $DRY_RUN; then
printf 'Would install pi-dictate with model revision %s.\n' "$MODEL_REVISION"
else
printf 'Installed pi-dictate with model revision %s.\n' "$MODEL_REVISION"
printf 'Check it with: systemctl --user status pi-dictate.socket pi-dictate.service\n'
fi