From 851e2556415c9fd749e795544e2cc0b878e2505c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 16:33:17 +0000 Subject: [PATCH] Fix SAM symlink crash and make prefetch-models.sh resilient per-step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit download_sam_model.py crashed with an uncaught PermissionError when data/models/ is root-owned (common after a prior Docker run) and a non-root host user tries to recreate the convenience sam_model.pth symlink — observed in the wild, and it aborted the whole prefetch run before U2Net/BEN2/BiRefNet-HR were ever attempted. Worse, the same unguarded symlink call sat inside the post-download try/except, so a successful download could get deleted just because the symlink step failed afterward. Wrapped symlink creation in a shared helper that warns and continues instead of raising — the real model file already satisfies entrypoint.sh's checks regardless of the symlink. prefetch-models.sh now treats SAM, U2Net, and the HuggingFace models as independent steps (one failing no longer aborts the rest) and prints a summary of which steps failed, so a single run gives full diagnostic signal instead of stopping at the first error. --- prefetch-models.sh | 52 +++++++++++++++++++++++------------ scripts/download_sam_model.py | 27 ++++++++++-------- 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/prefetch-models.sh b/prefetch-models.sh index 0a39bb8..2eb1faa 100755 --- a/prefetch-models.sh +++ b/prefetch-models.sh @@ -12,9 +12,11 @@ # ./prefetch-models.sh --sdxl # also prefetch SDXL base + inpaint (~13GB) # # Safe to re-run: every download here skips files that already exist -# (HuggingFace Hub) or are already present (SAM/U2Net). +# (HuggingFace Hub) or are already present (SAM/U2Net). Each model is +# independent — one failing (e.g. no network reachable at all) doesn't +# block the others from being attempted. -set -euo pipefail +set -uo pipefail cd "$(dirname "$0")" @@ -36,36 +38,39 @@ if [ "${1:-}" = "--sdxl" ]; then PREFETCH_SDXL=1 fi +FAILED=() + echo "==================================================" echo " Prefetching AI models (host-side, no Docker)" echo "==================================================" echo "" echo "── SAM (Smart Select) ───────────────────────────────" -python3 scripts/download_sam_model.py vit_b +python3 scripts/download_sam_model.py vit_b || FAILED+=("SAM") echo "" echo "── U2Net (Remove Background fallback) ──────────────" -python3 scripts/download_u2net_model.py u2net +python3 scripts/download_u2net_model.py u2net || FAILED+=("U2Net") echo "" echo "── HuggingFace Hub models (BEN2, BiRefNet-HR) ───────" if ! python3 -c "import huggingface_hub" &>/dev/null; then echo "Installing huggingface_hub (lightweight — no torch/GPU needed for this step)..." - python3 -m pip install --quiet --user "huggingface_hub>=0.23.0" + python3 -m pip install --quiet --user "huggingface_hub>=0.23.0" || FAILED+=("huggingface_hub install") fi -# HF_HOME must match what the container resolves by default: the bind mount -# maps ./data/hf_cache -> /root/.cache/huggingface, and the container never -# sets HF_HOME explicitly, so it defaults to ~/.cache/huggingface there. -# huggingface_hub itself appends "/hub" to HF_HOME to get the actual cache -# root (HF_HUB_CACHE) — setting HF_HOME here (instead of passing --cache-dir -# or cache_dir=... directly) lets both sides derive that "/hub" nesting the -# same way, rather than us hardcoding it and risking a mismatch. -export HF_HOME="$(pwd)/data/hf_cache" +if python3 -c "import huggingface_hub" &>/dev/null; then + # HF_HOME must match what the container resolves by default: the bind mount + # maps ./data/hf_cache -> /root/.cache/huggingface, and the container never + # sets HF_HOME explicitly, so it defaults to ~/.cache/huggingface there. + # huggingface_hub itself appends "/hub" to HF_HOME to get the actual cache + # root (HF_HUB_CACHE) — setting HF_HOME here (instead of passing --cache-dir + # or cache_dir=... directly) lets both sides derive that "/hub" nesting the + # same way, rather than us hardcoding it and risking a mismatch. + export HF_HOME="$(pwd)/data/hf_cache" -PREFETCH_SDXL="$PREFETCH_SDXL" python3 - << 'PYEOF' + PREFETCH_SDXL="$PREFETCH_SDXL" python3 - << 'PYEOF' || FAILED+=("HuggingFace models") import os from huggingface_hub import snapshot_download @@ -81,12 +86,23 @@ for repo_id in repos: snapshot_download(repo_id=repo_id, ignore_patterns=["*.msgpack", "flax_*", "tf_*"]) print(f" done: {repo_id}") PYEOF +else + echo "⚠ Skipping BEN2/BiRefNet-HR — huggingface_hub unavailable (install failed above)" + FAILED+=("HuggingFace models") +fi echo "" echo "==================================================" -echo " Done. Models cached under ./data/models and ./data/hf_cache" -if [ "$PREFETCH_SDXL" != "1" ]; then -echo " (SDXL not included — re-run with --sdxl to also prefetch txt2img/inpaint, ~13GB)" +if [ ${#FAILED[@]} -eq 0 ]; then + echo " Done. Models cached under ./data/models and ./data/hf_cache" + if [ "$PREFETCH_SDXL" != "1" ]; then + echo " (SDXL not included — re-run with --sdxl to also prefetch txt2img/inpaint, ~13GB)" + fi + echo " Start the app: ./bring-up-local-gpu.sh" +else + echo " Finished with failures: ${FAILED[*]}" + echo " If ALL of the above failed, this host can't reach the internet right now" + echo " (check: curl -v https://github.com) — that's a host/network issue, not Docker." + echo " If only some failed, re-run this script to retry just those." fi -echo " Start the app: ./bring-up-local-gpu.sh" echo "==================================================" diff --git a/scripts/download_sam_model.py b/scripts/download_sam_model.py index a09c2de..749d277 100644 --- a/scripts/download_sam_model.py +++ b/scripts/download_sam_model.py @@ -40,6 +40,20 @@ SAM_MODELS = { } } +def create_symlink(symlink_path: Path, target_name: str): + """Best-effort convenience symlink. Never raises — a missing/stale + symlink is harmless (callers also check the real filename directly), + but data/models/ is often root-owned from a prior Docker run, which + makes unlink/symlink_to fail with PermissionError for other users.""" + try: + if symlink_path.exists() or symlink_path.is_symlink(): + symlink_path.unlink() + symlink_path.symlink_to(target_name) + print(f"Symlink created: {symlink_path} -> {target_name}") + except OSError as e: + print(f"(skipping symlink: {e})") + + def download_with_progress(url: str, dest_path: Path): """Download file with progress indicator""" print(f"Downloading to: {dest_path}") @@ -88,12 +102,7 @@ def main(): print(f"\nModel already exists at: {dest_path}") print("To re-download, delete the file first.") - # Create symlink for easy access - symlink_path = models_dir / 'sam_model.pth' - if symlink_path.exists() or symlink_path.is_symlink(): - symlink_path.unlink() - symlink_path.symlink_to(dest_path.name) - print(f"Symlink created: {symlink_path} -> {dest_path.name}") + create_symlink(models_dir / 'sam_model.pth', dest_path.name) return print(f"\nDownloading SAM {model_type.upper()} ({model_info['size']})...") @@ -103,17 +112,13 @@ def main(): try: download_with_progress(model_info['url'], dest_path) - # Create symlink for easy access symlink_path = models_dir / 'sam_model.pth' - if symlink_path.exists() or symlink_path.is_symlink(): - symlink_path.unlink() - symlink_path.symlink_to(dest_path.name) + create_symlink(symlink_path, dest_path.name) print() print("=" * 60) print("SUCCESS!") print(f"Model saved to: {dest_path}") - print(f"Symlink: {symlink_path}") print("=" * 60) except Exception as e: