#!/usr/bin/env bash # Upload the FormScout source tree to both the model repo and the Space. # # Usage: # ./scripts/hf_upload.sh # message from last git commit # ./scripts/hf_upload.sh "feat: my change" # custom message # # Pushes to: # silas-therapy/small-functional-movement-screening (model repo) # spaces/silas-therapy/small-functional-movement-screening (Gradio Space) # # `hf upload` does NOT read .hfignore — it only honors .gitignore, and only at # commit time (after hashing and pre-uploading everything). So we parse # .hfignore ourselves into --exclude globs and pass them explicitly. # # If the filtered file count still exceeds LARGE_THRESHOLD, we fall back to # `hf upload-large-folder` (resumable, multi-threaded). Caveats of that mode: # no --create-pr and no custom commit message — it commits directly to main # in multiple commits. set -euo pipefail cd "$(dirname "$0")/.." REPO_NAME="small-functional-movement-screening" BLADE_OWNER="${FORMSCOUT_HF_BLADE_OWNER:-build-small-hackathon}" MODEL_REPO="silas-therapy/$REPO_NAME" SPACE_REPO="spaces/silas-therapy/$REPO_NAME" SPACE_BLADESZASZA_REPO="spaces/$BLADE_OWNER/$REPO_NAME" MSG="${1:-$(git log -1 --pretty=%s)}" LARGE_THRESHOLD="${FORMSCOUT_HF_LARGE_THRESHOLD:-500}" # Belt-and-suspenders extras on top of .hfignore. `.cache/` is the resume # state upload-large-folder writes into the folder being uploaded. PATTERNS=( "*.pdf" "**/node_modules/**" ".cache/**" ) # Parse .hfignore into fnmatch-style globs. fnmatch's `*` crosses `/`, but a # bare name like `.DS_Store` or `dir/` only matches at the root, so emit both # the rooted and `**/`-prefixed forms. while IFS= read -r line; do line="${line%%#*}" line="${line#"${line%%[![:space:]]*}"}" line="${line%"${line##*[![:space:]]}"}" [[ -z "$line" ]] && continue if [[ "$line" == */ ]]; then PATTERNS+=("${line}**" "**/${line}**") else PATTERNS+=("$line" "**/$line") fi done < .hfignore EXCLUDES=() for p in "${PATTERNS[@]}"; do EXCLUDES+=(--exclude="$p") done # Count what would actually be uploaded, using the same filter the hub client # applies, so the mode decision matches reality. N_FILES=$(python3 - "${PATTERNS[@]}" <<'EOF' import sys from pathlib import Path from huggingface_hub.utils import filter_repo_objects patterns = sys.argv[1:] files = ( str(p) for p in Path(".").rglob("*") if p.is_file() and p.parts[0] != ".git" ) print(len(list(filter_repo_objects(files, ignore_patterns=patterns)))) EOF ) echo "── $N_FILES files to upload after .hfignore filtering" if (( N_FILES == 0 )); then echo "✗ nothing to upload — check .hfignore" >&2 exit 1 fi # upload_repo [pr|direct] # pr — open a PR (shared org repos; review before merge) # direct — commit straight to main (repos you own; deploys immediately) upload_repo() { local repo="$1" local mode="${2:-pr}" if (( N_FILES > LARGE_THRESHOLD )); then echo "── $repo: $N_FILES files > $LARGE_THRESHOLD, using upload-large-folder" echo " (resumable; commits directly to main — no PR, no custom message)" hf upload-large-folder "$repo" . "${EXCLUDES[@]}" elif [[ "$mode" == "direct" ]]; then echo "── uploading (direct → main) to: $repo" hf upload "$repo" . . "${EXCLUDES[@]}" --commit-message="$MSG" else echo "── uploading (PR) to: $repo" hf upload "$repo" . . "${EXCLUDES[@]}" --create-pr --commit-message="$MSG" fi } # Ensure the personal ZeroGPU Space exists. Tries zero-a10g (needs Pro/ZeroGPU); # falls back to cpu-basic so the upload still has a target (set ZeroGPU in # Settings afterward). Idempotent via --exist-ok. ensure_blade_space() { local id="$BLADE_OWNER/$REPO_NAME" if hf repos create "$id" --type space --space-sdk gradio --flavor zero-a10g --exist-ok 2>/dev/null; then echo "── Space ready (ZeroGPU / zero-a10g): $id"; return 0 fi if hf repos create "$id" --type space --space-sdk gradio --exist-ok 2>/dev/null; then echo "── Space created cpu-basic (set ZeroGPU in Settings → Hardware): $id"; return 0 fi return 1 } blade_help() { cat >&2 <