278 lines
7.2 KiB
Bash
Executable File
278 lines
7.2 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
PYTHON_BIN="${PYTHON_BIN:-python3.13}"
|
|
VENV_DIR="backend/.venv"
|
|
TORCH_FLAVOR_FILE="$VENV_DIR/.heimgeist-torch-flavor"
|
|
TORCH_STATE_FILE="$VENV_DIR/.heimgeist-torch-state"
|
|
PYTHON_DEPS_STATE_FILE="$VENV_DIR/.heimgeist-python-deps-state"
|
|
NODE_DEPS_STATE_FILE="node_modules/.heimgeist-node-deps-state"
|
|
HEIMGEIST_TORCH_FLAVOR="${HEIMGEIST_TORCH_FLAVOR:-auto}"
|
|
HEIMGEIST_TORCH_INDEX_URL="${HEIMGEIST_TORCH_INDEX_URL:-}"
|
|
HEIMGEIST_FORCE_BOOTSTRAP="${HEIMGEIST_FORCE_BOOTSTRAP:-0}"
|
|
|
|
manifest_state() {
|
|
for manifest_path in "$@"; do
|
|
if [ -f "$manifest_path" ]; then
|
|
checksum="$(cksum "$manifest_path" | awk '{print $1 ":" $2}')"
|
|
printf '%s %s\n' "$manifest_path" "$checksum"
|
|
else
|
|
printf '%s missing\n' "$manifest_path"
|
|
fi
|
|
done
|
|
}
|
|
|
|
state_matches() {
|
|
state_file="$1"
|
|
shift
|
|
[ -r "$state_file" ] || return 1
|
|
current_state="$(manifest_state "$@")"
|
|
saved_state="$(cat "$state_file")"
|
|
[ "$saved_state" = "$current_state" ]
|
|
}
|
|
|
|
write_state() {
|
|
state_file="$1"
|
|
shift
|
|
manifest_state "$@" > "$state_file"
|
|
}
|
|
|
|
current_torch_state() {
|
|
printf 'flavor=%s\nindex=%s\n' "$TORCH_FLAVOR" "$HEIMGEIST_TORCH_INDEX_URL"
|
|
}
|
|
|
|
torch_state_matches() {
|
|
[ -r "$TORCH_STATE_FILE" ] || return 1
|
|
saved_state="$(cat "$TORCH_STATE_FILE")"
|
|
[ "$saved_state" = "$(current_torch_state)" ]
|
|
}
|
|
|
|
write_torch_state() {
|
|
current_torch_state > "$TORCH_STATE_FILE"
|
|
}
|
|
|
|
python_deps_usable() {
|
|
"$VENV_DIR/bin/python" - <<'PY' >/dev/null 2>&1
|
|
import fastapi
|
|
import httpx
|
|
import pydantic
|
|
import sqlalchemy
|
|
import uvicorn
|
|
import whisper
|
|
PY
|
|
}
|
|
|
|
node_deps_usable() {
|
|
node - <<'NODE' >/dev/null 2>&1
|
|
require('concurrently')
|
|
require('electron')
|
|
require('react')
|
|
require('vite')
|
|
require('wait-on')
|
|
NODE
|
|
}
|
|
|
|
is_linux_x86_64() {
|
|
[ "$(uname -s)" = "Linux" ] && [ "$(uname -m)" = "x86_64" ]
|
|
}
|
|
|
|
has_nvidia_gpu() {
|
|
if command -v nvidia-smi >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
for vendor_file in /sys/class/drm/card*/device/vendor; do
|
|
[ -r "$vendor_file" ] || continue
|
|
if grep -qi "0x10de" "$vendor_file"; then
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
is_steam_deck() {
|
|
if [ -r /etc/os-release ] && grep -Eiq '(^ID=steamos$|^NAME=.*SteamOS|^PRETTY_NAME=.*SteamOS)' /etc/os-release; then
|
|
return 0
|
|
fi
|
|
|
|
for dmi_file in \
|
|
/sys/devices/virtual/dmi/id/product_name \
|
|
/sys/devices/virtual/dmi/id/product_version \
|
|
/sys/devices/virtual/dmi/id/board_name
|
|
do
|
|
[ -r "$dmi_file" ] || continue
|
|
if grep -Eiq 'steam deck|jupiter|galileo' "$dmi_file"; then
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
resolve_torch_flavor() {
|
|
case "$HEIMGEIST_TORCH_FLAVOR" in
|
|
auto)
|
|
if is_linux_x86_64; then
|
|
if is_steam_deck; then
|
|
printf '%s\n' "cpu"
|
|
return
|
|
fi
|
|
if has_nvidia_gpu; then
|
|
printf '%s\n' "default"
|
|
return
|
|
fi
|
|
printf '%s\n' "cpu"
|
|
return
|
|
fi
|
|
printf '%s\n' "default"
|
|
;;
|
|
default|cpu|cuda|rocm|rocm6.4)
|
|
printf '%s\n' "$HEIMGEIST_TORCH_FLAVOR"
|
|
;;
|
|
*)
|
|
echo "Unsupported HEIMGEIST_TORCH_FLAVOR '$HEIMGEIST_TORCH_FLAVOR'. Use auto, default, cpu, cuda, rocm, or rocm6.4." >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
install_selected_torch() {
|
|
torch_flavor="$1"
|
|
|
|
if [ -n "$HEIMGEIST_TORCH_INDEX_URL" ]; then
|
|
echo "Installing PyTorch from custom index: $HEIMGEIST_TORCH_INDEX_URL"
|
|
"$VENV_DIR/bin/python" -m pip install --upgrade --index-url "$HEIMGEIST_TORCH_INDEX_URL" torch
|
|
return
|
|
fi
|
|
|
|
case "$torch_flavor" in
|
|
default|cuda)
|
|
return
|
|
;;
|
|
cpu)
|
|
echo "Installing CPU-only PyTorch for Whisper"
|
|
"$VENV_DIR/bin/python" -m pip install --upgrade --index-url https://download.pytorch.org/whl/cpu torch
|
|
;;
|
|
rocm|rocm6.4)
|
|
echo "Installing ROCm PyTorch for Whisper"
|
|
"$VENV_DIR/bin/python" -m pip install --upgrade --index-url https://download.pytorch.org/whl/rocm6.4 torch
|
|
;;
|
|
esac
|
|
}
|
|
|
|
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
|
|
echo "Python 3.13 is required. Set PYTHON_BIN to a Python 3.13 executable if needed." >&2
|
|
exit 1
|
|
fi
|
|
|
|
TORCH_FLAVOR="$(resolve_torch_flavor)"
|
|
RECREATE_VENV=0
|
|
|
|
if [ -z "${HEIMGEIST_SETTINGS_FILE:-}" ]; then
|
|
case "$(uname -s)" in
|
|
Darwin)
|
|
HEIMGEIST_SETTINGS_FILE="${HOME}/Library/Application Support/Heimgeist/settings.json"
|
|
;;
|
|
Linux)
|
|
HEIMGEIST_SETTINGS_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/Heimgeist/settings.json"
|
|
;;
|
|
esac
|
|
if [ -n "${HEIMGEIST_SETTINGS_FILE:-}" ]; then
|
|
export HEIMGEIST_SETTINGS_FILE
|
|
mkdir -p "$(dirname "$HEIMGEIST_SETTINGS_FILE")"
|
|
fi
|
|
fi
|
|
|
|
if [ ! -x "$VENV_DIR/bin/python" ] || ! "$VENV_DIR/bin/python" -c 'import sys; raise SystemExit(0 if sys.version_info[:2] == (3, 13) else 1)'; then
|
|
RECREATE_VENV=1
|
|
fi
|
|
|
|
if [ "$RECREATE_VENV" -eq 0 ]; then
|
|
PREVIOUS_TORCH_FLAVOR=""
|
|
if [ -r "$TORCH_FLAVOR_FILE" ]; then
|
|
PREVIOUS_TORCH_FLAVOR="$(cat "$TORCH_FLAVOR_FILE")"
|
|
fi
|
|
if [ -n "$PREVIOUS_TORCH_FLAVOR" ] && [ "$PREVIOUS_TORCH_FLAVOR" != "$TORCH_FLAVOR" ]; then
|
|
RECREATE_VENV=1
|
|
elif [ -z "$PREVIOUS_TORCH_FLAVOR" ] && [ "$TORCH_FLAVOR" != "default" ]; then
|
|
RECREATE_VENV=1
|
|
fi
|
|
fi
|
|
|
|
if [ "$RECREATE_VENV" -eq 1 ]; then
|
|
rm -rf "$VENV_DIR"
|
|
"$PYTHON_BIN" -m venv "$VENV_DIR"
|
|
fi
|
|
|
|
printf '%s\n' "$TORCH_FLAVOR" > "$TORCH_FLAVOR_FILE"
|
|
echo "Using PyTorch flavor: $TORCH_FLAVOR"
|
|
|
|
NEEDS_LEGACY_PYTHON_STATE=0
|
|
if [ ! -r "$PYTHON_DEPS_STATE_FILE" ]; then
|
|
NEEDS_LEGACY_PYTHON_STATE=1
|
|
fi
|
|
if [ -z "$HEIMGEIST_TORCH_INDEX_URL" ] && [ ! -r "$TORCH_STATE_FILE" ]; then
|
|
NEEDS_LEGACY_PYTHON_STATE=1
|
|
fi
|
|
|
|
if [ "$RECREATE_VENV" -eq 0 ] && [ "$NEEDS_LEGACY_PYTHON_STATE" -eq 1 ] && python_deps_usable; then
|
|
if [ ! -r "$PYTHON_DEPS_STATE_FILE" ]; then
|
|
write_state "$PYTHON_DEPS_STATE_FILE" backend/requirements.txt
|
|
fi
|
|
if [ -z "$HEIMGEIST_TORCH_INDEX_URL" ] && [ ! -r "$TORCH_STATE_FILE" ]; then
|
|
write_torch_state
|
|
fi
|
|
fi
|
|
|
|
if [ -d node_modules ] && [ ! -r "$NODE_DEPS_STATE_FILE" ] && node_deps_usable; then
|
|
write_state "$NODE_DEPS_STATE_FILE" package.json package-lock.json
|
|
fi
|
|
|
|
NEED_PYTHON_DEPS_INSTALL="$RECREATE_VENV"
|
|
NEED_TORCH_INSTALL="$RECREATE_VENV"
|
|
NEED_NODE_DEPS_INSTALL=0
|
|
|
|
if [ "$HEIMGEIST_FORCE_BOOTSTRAP" = "1" ]; then
|
|
NEED_PYTHON_DEPS_INSTALL=1
|
|
NEED_TORCH_INSTALL=1
|
|
NEED_NODE_DEPS_INSTALL=1
|
|
fi
|
|
|
|
if [ "$NEED_PYTHON_DEPS_INSTALL" -eq 0 ] && ! state_matches "$PYTHON_DEPS_STATE_FILE" backend/requirements.txt; then
|
|
NEED_PYTHON_DEPS_INSTALL=1
|
|
fi
|
|
|
|
if [ "$NEED_TORCH_INSTALL" -eq 0 ] && ! torch_state_matches; then
|
|
NEED_TORCH_INSTALL=1
|
|
fi
|
|
|
|
if [ "$NEED_PYTHON_DEPS_INSTALL" -eq 1 ]; then
|
|
echo "Installing Python dependencies"
|
|
"$VENV_DIR/bin/python" -m pip install --upgrade pip
|
|
if [ "$NEED_TORCH_INSTALL" -eq 1 ]; then
|
|
install_selected_torch "$TORCH_FLAVOR"
|
|
write_torch_state
|
|
fi
|
|
"$VENV_DIR/bin/python" -m pip install -r backend/requirements.txt
|
|
write_state "$PYTHON_DEPS_STATE_FILE" backend/requirements.txt
|
|
elif [ "$NEED_TORCH_INSTALL" -eq 1 ]; then
|
|
install_selected_torch "$TORCH_FLAVOR"
|
|
write_torch_state
|
|
fi
|
|
|
|
if [ ! -d node_modules ]; then
|
|
NEED_NODE_DEPS_INSTALL=1
|
|
elif ! state_matches "$NODE_DEPS_STATE_FILE" package.json package-lock.json; then
|
|
NEED_NODE_DEPS_INSTALL=1
|
|
fi
|
|
|
|
if [ "$NEED_NODE_DEPS_INSTALL" -eq 1 ]; then
|
|
echo "Installing Node dependencies"
|
|
npm install --no-fund --no-audit
|
|
mkdir -p node_modules
|
|
write_state "$NODE_DEPS_STATE_FILE" package.json package-lock.json
|
|
fi
|
|
|
|
npm run dev
|