File size: 5,157 Bytes
2edb151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
676f5d4
 
 
 
 
 
 
2edb151
676f5d4
 
 
2edb151
 
676f5d4
2edb151
676f5d4
2edb151
 
 
 
 
 
 
 
 
 
676f5d4
2edb151
 
 
676f5d4
 
2edb151
 
 
676f5d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2edb151
 
676f5d4
2edb151
676f5d4
 
 
 
 
 
 
 
 
 
2edb151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
676f5d4
 
 
 
 
2edb151
676f5d4
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#!/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'