Enrell commited on
Commit
c553805
·
verified ·
1 Parent(s): 525d1ff

Upload install.sh with huggingface_hub

Browse files
Files changed (1) hide show
  1. install.sh +172 -45
install.sh CHANGED
@@ -1,21 +1,30 @@
1
  #!/usr/bin/env bash
2
- # Install the Lain parser service: one frozen artifact, a private venv and a
3
- # systemd user unit. Everything lands under --prefix (default
4
- # ~/.local/share/lain-parser) and ~/.config/systemd/user; no sudo anywhere.
 
5
  #
6
- # bash install.sh # install, enable and start the unit
7
- # bash install.sh --no-start # install files only
8
- # bash install.sh --verify # re-check the installed hashes
 
 
9
  #
10
- # Source of truth for the artifact and the vendored app: this repository.
 
 
 
11
  set -euo pipefail
12
 
13
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
  REPO="$(cd "${SCRIPT_DIR}/../.." && pwd)"
15
- ARTIFACT_NAME="e5-s2"
16
- ARTIFACT_SRC="${REPO}/artifacts/e5-s2"
17
- ARTIFACT_SHA="d928668e5ed3c8ea4ac69e42f8d11c90f82120e3307dffaf3a02eb801c077e54"
18
- SNAPSHOT_SRC="${ANIFILEBERT_SNAPSHOT:-${REPO}/artifacts/anifilebert-dapt512-long}"
 
 
 
19
  SNAPSHOT_FILES=(anifilebert config.json model.safetensors tokenizer_config.json \
20
  vocab.json vocab.char.json label_schema.json dapt.json)
21
  PREFIX="${LAIN_PARSER_PREFIX:-${HOME}/.local/share/lain-parser}"
@@ -29,7 +38,6 @@ HOSTS="${LAIN_PARSER_HOST:-127.0.0.1,172.17.0.1}"
29
  SOCKET="${LAIN_PARSER_SOCKET:-${HOME}/.local/share/lain/parser.sock}"
30
  START=1
31
  VERIFY=0
32
- FORCE=0
33
 
34
  while [[ $# -gt 0 ]]; do
35
  case "$1" in
@@ -37,54 +45,163 @@ while [[ $# -gt 0 ]]; do
37
  --port) PORT="$2"; shift 2 ;;
38
  --host) HOSTS="$2"; shift 2 ;;
39
  --socket) SOCKET="$2"; shift 2 ;;
 
 
40
  --no-start) START=0; shift ;;
41
- --force) FORCE=1; shift ;;
42
  --verify) VERIFY=1; shift ;;
43
- -h|--help) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
44
  *) echo "unknown argument: $1" >&2; exit 2 ;;
45
  esac
46
  done
47
 
48
  step() { printf '\n== %s\n' "$*"; }
49
  die() { echo "error: $*" >&2; exit 1; }
50
-
51
  sha_of() { sha256sum "$1" | cut -d' ' -f1; }
52
 
53
- verify_installed() {
54
- step "verifying ${PREFIX}"
55
- local venv="${PREFIX}/venv/bin/python"
56
- [[ -x "$venv" ]] || die "venv missing under ${PREFIX}"
57
- local sum installed
58
- sum="$(sha_of "${PREFIX}/artifact/${ARTIFACT_NAME}/model.int8.pt")"
59
- [[ "$sum" == "$ARTIFACT_SHA" ]] || die "artifact hash mismatch: ${sum}"
60
- echo "artifact ${ARTIFACT_NAME} hash ok"
61
- "$venv" "${PREFIX}/app/serve.py" --check "Example.Show.S01E02.1080p-GROUP.mkv" \
62
- --artifact "${PREFIX}/artifact/${ARTIFACT_NAME}" --snapshot "${PREFIX}/anifilebert" \
63
- >/dev/null
64
- echo "model loads and parses"
 
 
 
 
 
65
  }
66
 
67
  if [[ "$VERIFY" == 1 ]]; then
68
- verify_installed
 
 
 
 
 
 
 
 
 
 
 
 
69
  exit 0
70
  fi
71
 
72
- [[ -f "${ARTIFACT_SRC}/model.int8.pt" ]] || die "artifact not found at ${ARTIFACT_SRC}"
73
- [[ -f "${ARTIFACT_SRC}/config.json" ]] || die "artifact config not found at ${ARTIFACT_SRC}"
74
- sum="$(sha_of "${ARTIFACT_SRC}/model.int8.pt")"
75
- [[ "$sum" == "$ARTIFACT_SHA" ]] || die "source artifact hash mismatch: ${sum}"
76
- [[ -d "$SNAPSHOT_SRC" ]] || die "snapshot not found at ${SNAPSHOT_SRC}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  step "installing into ${PREFIX}"
79
- mkdir -p "${PREFIX}/app/fp" "${PREFIX}/artifact/${ARTIFACT_NAME}" "${PREFIX}/anifilebert"
80
  cp -f "${SCRIPT_DIR}/app/serve.py" "${PREFIX}/app/serve.py"
81
  cp -f "${SCRIPT_DIR}"/app/fp/*.py "${PREFIX}/app/fp/"
82
  cp -f "${SCRIPT_DIR}/requirements.txt" "${PREFIX}/requirements.txt"
83
- cp -f "${ARTIFACT_SRC}/model.int8.pt" "${ARTIFACT_SRC}/config.json" \
84
- "${PREFIX}/artifact/${ARTIFACT_NAME}/"
85
- for entry in "${SNAPSHOT_FILES[@]}"; do
86
- cp -RL "${SNAPSHOT_SRC}/${entry}" "${PREFIX}/anifilebert/"
87
- done
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
  step "recording hashes"
90
  {
@@ -113,6 +230,13 @@ else
113
  fi
114
  "${PREFIX}/venv/bin/python" -c "import torch, transformers; print('torch', torch.__version__, '| transformers', transformers.__version__)"
115
 
 
 
 
 
 
 
 
116
  step "systemd user unit"
117
  mkdir -p "${UNIT_DIR}"
118
  unit_tmp="$(mktemp)"
@@ -137,18 +261,18 @@ if [[ ! -f "${UNIT_DIR}/${UNIT}" ]]; then
137
  echo "wrote ${UNIT_DIR}/${UNIT}"
138
  elif diff -q "$unit_tmp" "${UNIT_DIR}/${UNIT}" >/dev/null; then
139
  echo "unit already up to date"
140
- elif [[ "$FORCE" == 1 ]]; then
141
- install -m 0644 "$unit_tmp" "${UNIT_DIR}/${UNIT}"
142
- echo "unit replaced (--force)"
143
  else
144
- install -m 0644 "$unit_tmp" "${UNIT_DIR}/${UNIT}.new"
145
- echo "unit differs: wrote ${UNIT_DIR}/${UNIT}.new for review (or pass --force)"
 
 
146
  fi
147
  rm -f "$unit_tmp"
148
 
149
  if [[ "$START" == 1 ]] && command -v systemctl >/dev/null 2>&1; then
150
  systemctl --user daemon-reload
151
  systemctl --user enable --now "$UNIT" || true
 
152
  if command -v curl >/dev/null 2>&1; then
153
  ok=""
154
  for _ in $(seq 1 20); do
@@ -168,10 +292,13 @@ fi
168
  step "done"
169
  cat <<SUMMARY
170
  prefix ${PREFIX}
171
- artifact ${ARTIFACT_NAME} (${ARTIFACT_SHA:0:16}…)
 
172
  unit ${UNIT_DIR}/${UNIT}
173
  native URL http://127.0.0.1:${PORT}
174
  docker URL http://172.17.0.1:${PORT} (or host.docker.internal with host-gateway)
175
  docker UDS unix:///data/parser.sock (socket at ${SOCKET})
 
 
176
  verify bash install.sh --verify
177
  SUMMARY
 
1
  #!/usr/bin/env bash
2
+ # Install or update the Lain parser service: one pinned model artifact, a
3
+ # private venv and a systemd user unit. Everything lands under --prefix
4
+ # (default ~/.local/share/lain-parser) and ~/.config/systemd/user; no sudo
5
+ # anywhere.
6
  #
7
+ # bash install.sh # install, or update to the channel default
8
+ # bash install.sh --model e5-s2 # pin a specific model version
9
+ # bash install.sh --local # install from this repo's artifacts/ (dev)
10
+ # bash install.sh --no-start # install files only
11
+ # bash install.sh --verify # re-check installed hashes + a live parse
12
  #
13
+ # Artifacts come from the Hugging Face repo `Enrell/lain-parser`. manifest.json
14
+ # there is the release channel: `default` names the model, and every file's
15
+ # sha256 is verified against it before the running unit is touched. A failed
16
+ # download or hash check leaves the installed service untouched.
17
  set -euo pipefail
18
 
19
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
  REPO="$(cd "${SCRIPT_DIR}/../.." && pwd)"
21
+ HF_REPO="${LAIN_PARSER_HF_REPO:-Enrell/lain-parser}"
22
+ HF_BASE="https://huggingface.co/${HF_REPO}/resolve/main"
23
+ SERVICE_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' \
24
+ "${SCRIPT_DIR}/app/PROVENANCE.json" 2>/dev/null || echo 0.0.0)"
25
+ # --local installs from the repository's artifacts/ tree (development).
26
+ LOCAL=0
27
+ MODEL_ARG=""
28
  SNAPSHOT_FILES=(anifilebert config.json model.safetensors tokenizer_config.json \
29
  vocab.json vocab.char.json label_schema.json dapt.json)
30
  PREFIX="${LAIN_PARSER_PREFIX:-${HOME}/.local/share/lain-parser}"
 
38
  SOCKET="${LAIN_PARSER_SOCKET:-${HOME}/.local/share/lain/parser.sock}"
39
  START=1
40
  VERIFY=0
 
41
 
42
  while [[ $# -gt 0 ]]; do
43
  case "$1" in
 
45
  --port) PORT="$2"; shift 2 ;;
46
  --host) HOSTS="$2"; shift 2 ;;
47
  --socket) SOCKET="$2"; shift 2 ;;
48
+ --model) MODEL_ARG="$2"; shift 2 ;;
49
+ --local) LOCAL=1; shift ;;
50
  --no-start) START=0; shift ;;
 
51
  --verify) VERIFY=1; shift ;;
52
+ -h|--help) sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
53
  *) echo "unknown argument: $1" >&2; exit 2 ;;
54
  esac
55
  done
56
 
57
  step() { printf '\n== %s\n' "$*"; }
58
  die() { echo "error: $*" >&2; exit 1; }
 
59
  sha_of() { sha256sum "$1" | cut -d' ' -f1; }
60
 
61
+ command -v python3 >/dev/null || die "python3 is required"
62
+ command -v curl >/dev/null || [[ "$LOCAL" == 1 ]] || die "curl is required for downloads (or use --local)"
63
+
64
+ # --- resolve the model + snapshot -----------------------------------------
65
+
66
+ MANIFEST_LOCAL="${PREFIX}/manifest.installed.json"
67
+
68
+ hf_fetch() { # hf_fetch <repo-path> <dest> — anonymous resolve-URL download
69
+ curl -fSL --retry 3 --connect-timeout 10 "${HF_BASE}/$1" -o "$2"
70
+ }
71
+
72
+ read_manifest() { # read_manifest <file> <python-expression-over-m>
73
+ python3 - "$1" "$2" <<'PY'
74
+ import json, sys
75
+ m = json.load(open(sys.argv[1]))
76
+ print(eval(sys.argv[2])) # manifest is our own release file; trusted shape
77
+ PY
78
  }
79
 
80
  if [[ "$VERIFY" == 1 ]]; then
81
+ step "verifying ${PREFIX}"
82
+ [[ -x "${PREFIX}/venv/bin/python" ]] || die "venv missing under ${PREFIX}"
83
+ [[ -f "$MANIFEST_LOCAL" ]] || die "installed manifest missing — was this installed by this installer?"
84
+ installed_name="$(read_manifest "$MANIFEST_LOCAL" 'm["installed_model"]')"
85
+ want="$(read_manifest "$MANIFEST_LOCAL" "m['models']['${installed_name}']['files']['model.int8.pt']")"
86
+ sum="$(sha_of "${PREFIX}/artifact/${installed_name}/model.int8.pt")"
87
+ [[ "$sum" == "$want" ]] || die "artifact hash mismatch: ${sum} != ${want}"
88
+ echo "artifact ${installed_name} hash ok"
89
+ "${PREFIX}/venv/bin/python" "${PREFIX}/app/serve.py" --check \
90
+ "Example.Show.S01E02.1080p-GROUP.mkv" \
91
+ --artifact "${PREFIX}/artifact/${installed_name}" \
92
+ --snapshot "${PREFIX}/anifilebert" >/dev/null
93
+ echo "model loads and parses"
94
  exit 0
95
  fi
96
 
97
+ if [[ "$LOCAL" == 1 ]]; then
98
+ ARTIFACT_NAME="${MODEL_ARG:-e5-s2}"
99
+ ARTIFACT_SRC="${REPO}/artifacts/${ARTIFACT_NAME}"
100
+ SNAPSHOT_SRC="${ANIFILEBERT_SNAPSHOT:-${REPO}/artifacts/anifilebert-dapt512-long}"
101
+ [[ -f "${ARTIFACT_SRC}/model.int8.pt" ]] || die "local artifact not found at ${ARTIFACT_SRC}"
102
+ [[ -f "${ARTIFACT_SRC}/config.json" ]] || die "local artifact config not found"
103
+ [[ -d "$SNAPSHOT_SRC" ]] || die "local snapshot not found at ${SNAPSHOT_SRC}"
104
+ MANIFEST_TMP=""
105
+ else
106
+ step "fetching release manifest (${HF_REPO})"
107
+ mkdir -p "${PREFIX}/.download"
108
+ MANIFEST_TMP="${PREFIX}/.download/manifest.json"
109
+ hf_fetch "manifest.json" "$MANIFEST_TMP" || die "manifest download failed"
110
+ ARTIFACT_NAME="${MODEL_ARG:-$(read_manifest "$MANIFEST_TMP" 'm["default"]')}"
111
+ read_manifest "$MANIFEST_TMP" "'${ARTIFACT_NAME}' in m['models']" >/dev/null 2>&1 || \
112
+ die "model ${ARTIFACT_NAME} not in manifest"
113
+ # Refuse cleanly when a model needs a newer service than the vendored app ships.
114
+ need="$(read_manifest "$MANIFEST_TMP" "m['models']['${ARTIFACT_NAME}'].get('min_service','0')")"
115
+ python3 - "$need" "$SERVICE_VERSION" <<'PY' || die "model requires service >= $need; update the installer first"
116
+ import sys
117
+ key = lambda v: [int(x) for x in v.split('.')]
118
+ sys.exit(0 if key(sys.argv[2]) >= key(sys.argv[1]) else 1)
119
+ PY
120
+ SNAP_NAME="$(read_manifest "$MANIFEST_TMP" "m['models']['${ARTIFACT_NAME}']['snapshot']")"
121
+ echo "channel default: $(read_manifest "$MANIFEST_TMP" 'm["default"]') -> installing ${ARTIFACT_NAME} (snapshot ${SNAP_NAME})"
122
+ fi
123
 
124
  step "installing into ${PREFIX}"
125
+ mkdir -p "${PREFIX}/app/fp" "${PREFIX}/artifact" "${PREFIX}/anifilebert"
126
  cp -f "${SCRIPT_DIR}/app/serve.py" "${PREFIX}/app/serve.py"
127
  cp -f "${SCRIPT_DIR}"/app/fp/*.py "${PREFIX}/app/fp/"
128
  cp -f "${SCRIPT_DIR}/requirements.txt" "${PREFIX}/requirements.txt"
129
+
130
+ if [[ "$LOCAL" == 1 ]]; then
131
+ cp -f "${ARTIFACT_SRC}/model.int8.pt" "${ARTIFACT_SRC}/config.json" \
132
+ "${PREFIX}/artifact/${ARTIFACT_NAME}/" 2>/dev/null || {
133
+ mkdir -p "${PREFIX}/artifact/${ARTIFACT_NAME}"
134
+ cp -f "${ARTIFACT_SRC}/model.int8.pt" "${ARTIFACT_SRC}/config.json" \
135
+ "${PREFIX}/artifact/${ARTIFACT_NAME}/"; }
136
+ for entry in "${SNAPSHOT_FILES[@]}"; do
137
+ cp -RL "${SNAPSHOT_SRC}/${entry}" "${PREFIX}/anifilebert/" 2>/dev/null || true
138
+ done
139
+ else
140
+ # Download each needed file to a staging dir, verify sha256 against the
141
+ # manifest, then move into place. Nothing below $PREFIX/artifact or
142
+ # $PREFIX/anifilebert changes until every hash checks out.
143
+ stage="${PREFIX}/.download/${ARTIFACT_NAME}"
144
+ mkdir -p "${stage}/model" "${stage}/snapshot"
145
+ while read -r f sha; do
146
+ [[ "$f" == "model.int8.pt" || "$f" == "config.json" ]] || continue
147
+ dest="${stage}/model/${f}"
148
+ installed="${PREFIX}/artifact/${ARTIFACT_NAME}/${f}"
149
+ if [[ -f "$dest" && "$(sha_of "$dest")" == "$sha" ]] \
150
+ || { [[ -f "$installed" && "$(sha_of "$installed")" == "$sha" ]] \
151
+ && cp -f "$installed" "$dest"; }; then
152
+ echo "already have ${f}"; continue
153
+ fi
154
+ echo "fetching models/${ARTIFACT_NAME}/${f}"
155
+ hf_fetch "models/${ARTIFACT_NAME}/${f}" "$dest" || die "download failed: $f"
156
+ [[ "$(sha_of "$dest")" == "$sha" ]] || die "hash mismatch after download: $f"
157
+ done < <(read_manifest "$MANIFEST_TMP" \
158
+ "'\n'.join(f'{k} {v}' for k,v in m['models']['${ARTIFACT_NAME}']['files'].items())")
159
+ # snapshot files the service needs: the anifilebert/ package plus the
160
+ # top-level files listed in SNAPSHOT_FILES (dapt.json tolerated if absent).
161
+ needed="$(printf '%s\n' "${SNAPSHOT_FILES[@]}" | grep -v '^anifilebert$')"
162
+ while read -r f sha; do
163
+ [[ -n "$f" ]] || continue
164
+ dest="${stage}/snapshot/${f}"
165
+ mkdir -p "$(dirname "$dest")"
166
+ installed="${PREFIX}/anifilebert/${f}"
167
+ if [[ -f "$dest" && "$(sha_of "$dest")" == "$sha" ]] \
168
+ || { [[ -f "$installed" && "$(sha_of "$installed")" == "$sha" ]] \
169
+ && cp -f "$installed" "$dest"; }; then
170
+ echo "already have snapshot/${f}"; continue
171
+ fi
172
+ echo "fetching snapshots/${SNAP_NAME}/${f}"
173
+ if hf_fetch "snapshots/${SNAP_NAME}/${f}" "$dest" && [[ "$(sha_of "$dest")" == "$sha" ]]; then
174
+ continue
175
+ fi
176
+ if [[ "$f" == "dapt.json" ]]; then
177
+ echo "warning: ${f} unavailable — skipping (provenance only)"
178
+ rm -f "$dest"; continue
179
+ fi
180
+ die "snapshot file ${f} failed to fetch or verify"
181
+ done < <(python3 - "$MANIFEST_TMP" "$SNAP_NAME" $needed <<'PY'
182
+ import json, sys
183
+ m = json.load(open(sys.argv[1]))
184
+ snap = m["snapshots"][sys.argv[2]]
185
+ want = set(sys.argv[3:])
186
+ for rel, sha in sorted(snap["sha256"].items()):
187
+ if rel in want or rel.startswith("anifilebert/"):
188
+ print(rel, sha)
189
+ PY
190
+ )
191
+ mkdir -p "${PREFIX}/artifact/${ARTIFACT_NAME}"
192
+ cp -f "${stage}/model/model.int8.pt" "${stage}/model/config.json" \
193
+ "${PREFIX}/artifact/${ARTIFACT_NAME}/"
194
+ cp -Rf "${stage}/snapshot/." "${PREFIX}/anifilebert/"
195
+ fi
196
+
197
+ # Record the manifest used for this install (offline verify + update checks).
198
+ if [[ -n "${MANIFEST_TMP}" ]]; then
199
+ python3 - "$MANIFEST_TMP" "$MANIFEST_LOCAL" "$ARTIFACT_NAME" <<'PY'
200
+ import json, sys
201
+ m = json.load(open(sys.argv[1])); m["installed_model"] = sys.argv[3]
202
+ json.dump(m, open(sys.argv[2], "w"), indent=2)
203
+ PY
204
+ fi
205
 
206
  step "recording hashes"
207
  {
 
230
  fi
231
  "${PREFIX}/venv/bin/python" -c "import torch, transformers; print('torch', torch.__version__, '| transformers', transformers.__version__)"
232
 
233
+ step "smoke test before touching the service"
234
+ "${PREFIX}/venv/bin/python" "${PREFIX}/app/serve.py" --check \
235
+ "Example.Show.S01E02.1080p-GROUP.mkv" \
236
+ --artifact "${PREFIX}/artifact/${ARTIFACT_NAME}" \
237
+ --snapshot "${PREFIX}/anifilebert" >/dev/null \
238
+ || die "new artifact failed the smoke parse — the running service was left alone"
239
+
240
  step "systemd user unit"
241
  mkdir -p "${UNIT_DIR}"
242
  unit_tmp="$(mktemp)"
 
261
  echo "wrote ${UNIT_DIR}/${UNIT}"
262
  elif diff -q "$unit_tmp" "${UNIT_DIR}/${UNIT}" >/dev/null; then
263
  echo "unit already up to date"
 
 
 
264
  else
265
+ # A model bump changes ExecStart; keep the old unit as .bak for inspection.
266
+ cp -f "${UNIT_DIR}/${UNIT}" "${UNIT_DIR}/${UNIT}.bak"
267
+ install -m 0644 "$unit_tmp" "${UNIT_DIR}/${UNIT}"
268
+ echo "unit updated (old copy: ${UNIT}.bak)"
269
  fi
270
  rm -f "$unit_tmp"
271
 
272
  if [[ "$START" == 1 ]] && command -v systemctl >/dev/null 2>&1; then
273
  systemctl --user daemon-reload
274
  systemctl --user enable --now "$UNIT" || true
275
+ systemctl --user restart "$UNIT" || true
276
  if command -v curl >/dev/null 2>&1; then
277
  ok=""
278
  for _ in $(seq 1 20); do
 
292
  step "done"
293
  cat <<SUMMARY
294
  prefix ${PREFIX}
295
+ artifact ${ARTIFACT_NAME}
296
+ source $([[ "$LOCAL" == 1 ]] && echo "local artifacts/" || echo "${HF_REPO} (manifest)")
297
  unit ${UNIT_DIR}/${UNIT}
298
  native URL http://127.0.0.1:${PORT}
299
  docker URL http://172.17.0.1:${PORT} (or host.docker.internal with host-gateway)
300
  docker UDS unix:///data/parser.sock (socket at ${SOCKET})
301
+ update bash install.sh (re-run: installs the channel default)
302
+ rollback bash install.sh --model <previous-name>
303
  verify bash install.sh --verify
304
  SUMMARY