#!/usr/bin/env bash # ============================================================================= # ONE-SHOT: keys-Auto Receipts Studio (iPhone / may add Autonomous Lamp Skill) # 1. Python 3.12 venv + app # 2. Gemma 4 12B-it weights (skip if already on disk) # 3. vLLM serve --gpu-memory-utilization 0.15 FP8 max-model-len 8192 # 4. Gradio UI on the LAN + print phone URL # Idempotent. Re-run anytime. Never raises GPU util above 0.85. # ============================================================================= set -euo pipefail ROOT="$(cd "$(dirname "$0")" && pwd)" cd "$ROOT" MODEL_ID="${RECEIPT_HF_MODEL:-google/gemma-4-12B-it}" MODEL_DIR="${RECEIPT_GEMMA_PATH:-$HOME/models-gemma4-12b-it}" PORT_LLM="${RECEIPT_VLLM_PORT:-8080}" PORT_UI="${RECEIPT_UI_PORT:-7860}" UTIL="${RECEIPT_GPU_MEMORY_UTILIZATION:-0.15}" say(){ printf '\n\033[1;36m==> %s\033[0m\n' "$*"; } die(){ printf '\n\033[1;31mFAILED: %s\033[0m\n' "$*" >&2; exit 1; } python3 -c 'import sys; assert sys.version_info >= (3,12), sys.version' \ || die "Python 3.12+ required" if python3 -c "u=float('$UTIL'); assert u<=0.85" 2>/dev/null; then :; else die "gpu_memory_utilization $UTIL > 0.85 hard cap" fi say "1/5 venv + install" if [[ ! -x .venv/bin/python ]]; then python3 -m venv .venv fi .venv/bin/pip install -q -U pip .venv/bin/pip install -q -e ".[dev]" [[ -f .env ]] || cp .env.example .env HAVE_VLLM=0 if curl -sf -m3 "http://127.0.0.1:$PORT_LLM/v1/models" >/dev/null 2>&1; then HAVE_VLLM=1 elif command -v vllm >/dev/null 2>&1; then HAVE_VLLM=1 fi say "2/5 Gemma 4 12B-it weights → $MODEL_DIR" if [[ "$HAVE_VLLM" != 1 ]]; then echo " skip (no vllm on PATH and nothing on :$PORT_LLM) — UI-only. Point .env at a GPU box." elif [[ -f "$MODEL_DIR/config.json" ]] && ls "$MODEL_DIR"/*.safetensors >/dev/null 2>&1; then echo " present" else .venv/bin/pip install -q huggingface_hub mkdir -p "$MODEL_DIR" .venv/bin/python - "$MODEL_ID" "$MODEL_DIR" <<'PY' || die "weight download failed (run: .venv/bin/hf auth login)" import sys from huggingface_hub import snapshot_download snapshot_download(sys.argv[1], local_dir=sys.argv[2]) print(" downloaded") PY fi say "3/5 vLLM Gemma (util=$UTIL FP8, :$PORT_LLM)" if curl -sf -m3 "http://127.0.0.1:$PORT_LLM/v1/models" >/dev/null 2>&1; then echo " already serving" elif command -v vllm >/dev/null 2>&1 && [[ -f "$MODEL_DIR/config.json" ]]; then mkdir -p data nohup bash "$ROOT/scripts/serve-gemma.sh" >> data/vllm-gemma.log 2>&1 & echo " pid $! log data/vllm-gemma.log" else echo " skip — no local vLLM/Gemma. Set RECEIPT_LLM_BASE_URL in .env to the GPU box." fi say "4/5 wait until Gemma answers /v1/models (first boot compiles kernels)" if curl -sf -m3 "http://127.0.0.1:$PORT_LLM/v1/models" >/dev/null 2>&1; then echo " already healthy" elif command -v vllm >/dev/null 2>&1 && [[ -f "$MODEL_DIR/config.json" ]]; then ok=0 for i in $(seq 1 120); do if curl -sf -m3 "http://127.0.0.1:$PORT_LLM/v1/models" >/dev/null 2>&1; then echo " healthy ($i)" ok=1 break fi sleep 5 done [[ "$ok" = 1 ]] || die "vLLM not healthy — tail data/vllm-gemma.log" else echo " skip wait (no local Gemma). UI will still start." fi say "5/5 UI on LAN :$PORT_UI" mkdir -p data export RECEIPT_UI_SHARE_LAN=true # Only pin LLM URLs to this machine when local Gemma is actually up. # UI-only boxes must keep .env pointing at the GPU box. if curl -sf -m3 "http://127.0.0.1:$PORT_LLM/v1/models" >/dev/null 2>&1; then export RECEIPT_LLM_BASE_URL="http://127.0.0.1:${PORT_LLM}/v1" export RECEIPT_EMBED_BASE_URL="http://127.0.0.1:${PORT_LLM}/v1" export RECEIPT_LLM_MODEL="$MODEL_ID" export RECEIPT_EMBED_MODEL="$MODEL_ID" export RECEIPT_EMBED_DIM=3840 export RECEIPT_EMBED_BACKEND=omni fi if curl -sf -m2 "http://127.0.0.1:$PORT_UI/phone" >/dev/null 2>&1; then echo " UI already up" else nohup .venv/bin/python -m app.cli ui >> data/ui.log 2>&1 & echo " pid $!" for i in $(seq 1 40); do curl -sf -m2 "http://127.0.0.1:$PORT_UI/phone" >/dev/null 2>&1 && break sleep 0.25 done fi LAN="$(python3 - <<'PY' import socket s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: s.connect(("192.0.2.1",1)); print(s.getsockname()[0]) except OSError: print("127.0.0.1") finally: s.close() PY )" printf '\n\033[1;32m✅ READY\033[0m keys-Auto Receipts Studio\n' printf ' Review (this machine): http://127.0.0.1:%s\n' "$PORT_UI" printf ' iPhone Safari: http://%s:%s/phone\n' "$LAN" "$PORT_UI" if curl -sf -m2 "http://127.0.0.1:$PORT_LLM/v1/models" >/dev/null 2>&1; then printf ' Gemma /v1: http://127.0.0.1:%s/v1 model %s util=%s\n' "$PORT_LLM" "$MODEL_ID" "$UTIL" else printf ' Gemma /v1: not local — set RECEIPT_LLM_BASE_URL in .env to the GPU box\n' fi printf ' Desktop launcher: bash scripts/install-launcher.sh\n' printf ' Windows desktop icon: scripts\\install-launcher.bat\n' printf '\n Hold a receipt up → Take photo on the phone page (Safari).\n' printf ' Lamp skill: skills/keys-receipt-scanner/ — 12B does not fit in 6GB RAM.\n'