Compare commits

...
13 Commits
Author SHA1 Message Date
Outis 2bd87dbf23 Merge pull request #69 from outis1one/claude/rename-paintplus-z9eg86
Rename remaining EditmaskwithAI references to PaintPlus
2026-07-30 19:51:20 -04:00
Claude 608d6958b1 Rename remaining EditmaskwithAI references to PaintPlus
Updates README, Docker/Compose comments and container name, install
script banner, about dialog GitHub link, and a script's cd path to
match the repo's new name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011g84QwMKwPMFQPwLVnPrQ1
2026-07-30 23:44:43 +00:00
Outis 4e43728583 Merge pull request #68 from outis1one/claude/focused-maxwell-3kgh3x
Make model prefetching automatic in install/run scripts
2026-06-18 13:58:54 -04:00
Claude 3f4a6b754c Make model prefetching automatic in install/run scripts
install-local-gpu.sh and bring-up-local-gpu.sh now call
prefetch-models.sh --sdxl (host-side download, all models including
SDXL/inpaint, ~13GB) automatically rather than treating it as a manual
DNS-troubleshooting step. install-local-gpu.sh drops back to the
invoking user via sudo -u "$SUDO_USER" -H before running it, since
that script itself runs as root and would otherwise leave downloaded
files root-owned. Both call sites are non-fatal: a prefetch failure
logs a warning and the container still starts, falling back to its
own in-container download attempt.

Updated README to match.
2026-06-18 17:47:18 +00:00
Outis f49718a260 Merge pull request #67 from outis1one/claude/focused-maxwell-3kgh3x
Fix stale UI cache, BEN2 edge quality, and fresh-install permissions
2026-06-18 13:23:20 -04:00
Claude b4a790473c Fix stale UI cache, BEN2 edge quality, and fresh-install permissions
- Serve /dist and /src with Cache-Control: no-cache so browsers always
  revalidate the webpack bundle (it has a fixed filename with no content
  hash, so a stale browser cache was hiding the new Remove Background
  model dropdown after the rebuild).
- Enable BEN2's refine_foreground pass by default - it's the model's own
  documented option for preserving fine/semi-transparent edge detail
  (text borders, light rays) instead of a harder cutout, at a small
  speed cost.
- Make install-local-gpu.sh and bring-up-local-gpu.sh pre-create ./data
  as the invoking non-root user before Docker ever runs, so its daemon
  never gets a chance to auto-create those bind-mount dirs as root.
- Make prefetch-models.sh self-heal a root-owned ./data automatically
  (sudo chown) instead of erroring out with a manual fix-it command.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ro4PwQKvSc3CH19LSN21Ht
2026-06-18 17:09:23 +00:00
Outis e96d7cc204 Merge pull request #66 from outis1one/claude/focused-maxwell-3kgh3x
Fix prefetch-models.sh write-permission detection and PEP 668 pip ins…
2026-06-18 12:44:50 -04:00
Claude eca834a7f0 Fix prefetch-models.sh write-permission detection and PEP 668 pip install
The mkdir -p preflight check only verified that data/models and
data/hf_cache could be created, which succeeds vacuously on a directory
that already exists — exactly the case when Docker created ./data as
root on a prior run. Confirmed in practice: U2Net's download reached the
network fine but failed writing the file with Permission denied. Now
test actual writability and point at the one-time fix (chown ./data back
to the calling user) instead of suggesting sudo re-runs that would leave
the ownership problem in place permanently.

Also handle pip's PEP 668 "externally-managed-environment" error
(Debian/Ubuntu 12+) when installing huggingface_hub, retrying with
--break-system-packages — safe here since --user already keeps the
install out of apt-managed system site-packages.
2026-06-18 16:43:55 +00:00
Outis b8523cb729 Merge pull request #65 from outis1one/claude/focused-maxwell-3kgh3x
Fix SAM symlink crash and make prefetch-models.sh resilient per-step
2026-06-18 12:40:34 -04:00
Claude 851e255641 Fix SAM symlink crash and make prefetch-models.sh resilient per-step
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.
2026-06-18 16:33:17 +00:00
Outis b70f95ede5 Merge pull request #64 from outis1one/claude/focused-maxwell-3kgh3x
Add prefetch-models.sh to download AI models on host when container D…
2026-06-18 12:22:14 -04:00
Outis f58c96c20f Merge pull request #63 from outis1one/claude/focused-maxwell-3kgh3x
Claude/focused maxwell 3kgh3x
2026-06-18 10:39:24 -04:00
Outis 668728348b Merge pull request #62 from outis1one/claude/focused-maxwell-3kgh3x
Auto-download U2Net model on startup; remove dead PyTorch U2Net module
2026-06-18 09:29:16 -04:00
12 changed files with 176 additions and 64 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# Caddy 2 Configuration for EditmaskwithAI # Caddy 2 Configuration for PaintPlus
# ========================================== # ==========================================
# #
# SETUP INSTRUCTIONS: # SETUP INSTRUCTIONS:
+1 -1
View File
@@ -1,5 +1,5 @@
# ============================================================================= # =============================================================================
# EditmaskwithAI — GPU Container (NVIDIA CUDA) # PaintPlus — GPU Container (NVIDIA CUDA)
# #
# Usage: # Usage:
# docker compose -f docker-compose.gpu.yml up --build # docker compose -f docker-compose.gpu.yml up --build
+10 -10
View File
@@ -1,4 +1,4 @@
# EditmaskwithAI # PaintPlus
A self-hosted, web-based AI photo editor. Paint over any object, describe what you want, and the AI replaces just that region — every pixel outside your selection stays untouched. A self-hosted, web-based AI photo editor. Paint over any object, describe what you want, and the AI replaces just that region — every pixel outside your selection stays untouched.
@@ -7,11 +7,11 @@ A self-hosted, web-based AI photo editor. Paint over any object, describe what y
### GPU machine (recommended — free inference, best quality) ### GPU machine (recommended — free inference, best quality)
```bash ```bash
git clone https://github.com/outis1one/editmaskwithai git clone https://github.com/outis1one/paintplus
cd editmaskwithai cd paintplus
# One-time setup: installs nvidia-container-toolkit, configures Docker, # One-time setup: installs nvidia-container-toolkit, configures Docker,
# and sets up a permanent DNS fix so the container can download models. # sets up a permanent DNS fix, and prefetches all AI models on the host.
chmod +x install-local-gpu.sh chmod +x install-local-gpu.sh
./install-local-gpu.sh ./install-local-gpu.sh
@@ -22,15 +22,15 @@ chmod +x bring-up-local-gpu.sh
Open **http://localhost:3080** Open **http://localhost:3080**
**First startup downloads the AI model for your GPU (~13 GB, one time).** Models are cached in `./data/hf_cache/` and survive rebuilds. **Models (~13 GB total, one time) download automatically on the host**, outside Docker — both scripts call `./prefetch-models.sh` for you, since in-container DNS is unreliable on some hosts. They're cached in `./data/hf_cache/` and `./data/models/`, and survive rebuilds.
--- ---
### Cloud API (no GPU required) ### Cloud API (no GPU required)
```bash ```bash
git clone https://github.com/outis1one/editmaskwithai git clone https://github.com/outis1one/paintplus
cd editmaskwithai cd paintplus
cp .env.example .env cp .env.example .env
# Edit .env: set AI_PROVIDER and your API key (see .env.example for options) # Edit .env: set AI_PROVIDER and your API key (see .env.example for options)
docker compose up -d --build docker compose up -d --build
@@ -140,7 +140,7 @@ docker compose logs -f
## File structure ## File structure
``` ```
EditmaskwithAI/ PaintPlus/
├── backend/ ├── backend/
│ ├── app/ │ ├── app/
│ │ ├── routers/ # API endpoints (ai_tools, print_tools, …) │ │ ├── routers/ # API endpoints (ai_tools, print_tools, …)
@@ -163,7 +163,7 @@ EditmaskwithAI/
├── Dockerfile.gpu ├── Dockerfile.gpu
├── install-local-gpu.sh # One-time GPU host setup ├── install-local-gpu.sh # One-time GPU host setup
├── bring-up-local-gpu.sh # Start/stop the GPU container ├── bring-up-local-gpu.sh # Start/stop the GPU container
├── prefetch-models.sh # Download AI models on the host (DNS-blocked workaround) ├── prefetch-models.sh # Download AI models on the host (called automatically; also runnable standalone)
└── .env.example └── .env.example
``` ```
@@ -232,7 +232,7 @@ You can also pick a specific model per-edit from the Remove Background dialog's
**AI models not downloading (container DNS blocked)** **AI models not downloading (container DNS blocked)**
Easiest fix: download the models on the host instead of inside the container — they land in `./data/`, which is already bind-mounted into the container, so it picks them up with no rebuild: `install-local-gpu.sh` and `bring-up-local-gpu.sh` already run this for you automatically on every start, so you normally don't need to think about it. If a model still didn't download (no network at the time, etc.), re-run it manually — it lands in `./data/`, which is already bind-mounted into the container, so it's picked up with no rebuild:
```bash ```bash
./prefetch-models.sh # SAM + U2Net + BEN2 + BiRefNet-HR (~1.5GB) ./prefetch-models.sh # SAM + U2Net + BEN2 + BiRefNet-HR (~1.5GB)
+17 -6
View File
@@ -103,20 +103,31 @@ def health():
STATIC_DIR = Path("/app/static") STATIC_DIR = Path("/app/static")
class NoCacheStaticFiles(StaticFiles):
"""webpack outputs a fixed 'bundle.js' filename (no content hash), so
browsers can keep serving a stale cached copy after a rebuild unless
forced to revalidate on every request."""
def file_response(self, *args, **kwargs):
response = super().file_response(*args, **kwargs)
response.headers["Cache-Control"] = "no-cache"
return response
# Serve static assets - mount subdirectories if they exist # Serve static assets - mount subdirectories if they exist
if STATIC_DIR.exists(): if STATIC_DIR.exists():
# React-style assets folder # React-style assets folder
if (STATIC_DIR / "assets").exists(): if (STATIC_DIR / "assets").exists():
app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets") app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets")
# miniPaint dist folder (webpack bundle) # miniPaint dist folder (webpack bundle) - no-cache so code updates are picked up immediately
if (STATIC_DIR / "dist").exists(): if (STATIC_DIR / "dist").exists():
app.mount("/dist", StaticFiles(directory=STATIC_DIR / "dist"), name="dist") app.mount("/dist", NoCacheStaticFiles(directory=STATIC_DIR / "dist"), name="dist")
# miniPaint images folder # miniPaint images folder
if (STATIC_DIR / "images").exists(): if (STATIC_DIR / "images").exists():
app.mount("/images", StaticFiles(directory=STATIC_DIR / "images"), name="images") app.mount("/images", StaticFiles(directory=STATIC_DIR / "images"), name="images")
# miniPaint CSS folder # miniPaint CSS folder - no-cache, same reasoning as /dist
if (STATIC_DIR / "src").exists(): if (STATIC_DIR / "src").exists():
app.mount("/src", StaticFiles(directory=STATIC_DIR / "src"), name="src") app.mount("/src", NoCacheStaticFiles(directory=STATIC_DIR / "src"), name="src")
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
@@ -124,7 +135,7 @@ async def serve_spa():
"""Serve miniPaint index.html""" """Serve miniPaint index.html"""
index_path = STATIC_DIR / "index.html" index_path = STATIC_DIR / "index.html"
if index_path.exists(): if index_path.exists():
return FileResponse(index_path) return FileResponse(index_path, headers={"Cache-Control": "no-cache"})
return HTMLResponse("<h1>Frontend not built. Run npm build in frontend/</h1>") return HTMLResponse("<h1>Frontend not built. Run npm build in frontend/</h1>")
@@ -146,6 +157,6 @@ async def serve_spa_routes(request: Request, full_path: str):
# Otherwise serve index.html # Otherwise serve index.html
index_path = STATIC_DIR / "index.html" index_path = STATIC_DIR / "index.html"
if index_path.exists(): if index_path.exists():
return FileResponse(index_path) return FileResponse(index_path, headers={"Cache-Control": "no-cache"})
return HTMLResponse("<h1>Frontend not built</h1>", status_code=404) return HTMLResponse("<h1>Frontend not built</h1>", status_code=404)
+4 -1
View File
@@ -369,7 +369,10 @@ async def _remove_background_ben2(img: Image.Image) -> bytes:
_ben2_model.to(device).eval() _ben2_model.to(device).eval()
print("BEN2_Base model loaded") print("BEN2_Base model loaded")
result = _ben2_model.inference(img.convert('RGB'), refine_foreground=False) # refine_foreground=True runs BEN2's extra foreground-color refinement pass
# (slower, but recovers fine/semi-transparent edge detail instead of a hard
# cutout — matters for things like lace, light rays, or fine text borders).
result = _ben2_model.inference(img.convert('RGB'), refine_foreground=True)
buffer = BytesIO() buffer = BytesIO()
result.save(buffer, format='PNG') result.save(buffer, format='PNG')
+17 -3
View File
@@ -4,9 +4,11 @@
# Run this each time you want to start the app. # Run this each time you want to start the app.
# Run ./install-local-gpu.sh once first on a new machine. # Run ./install-local-gpu.sh once first on a new machine.
# #
# If models fail to download inside the container (DNS/firewall blocked), # Before starting, this fetches any missing models on the host (outside
# run ./prefetch-models.sh first — it downloads them on the host into # Docker) via ./prefetch-models.sh — in-container DNS/network is unreliable
# ./data/, which this container already bind-mounts, so no rebuild needed. # on some hosts, so this is the default now, not a manual troubleshooting
# step. It never blocks startup: if it fails (no network, no python3, etc.)
# the container still starts and falls back to its own in-container download.
# #
# Usage: # Usage:
# ./bring-up-local-gpu.sh # start (detached, rebuild if needed) # ./bring-up-local-gpu.sh # start (detached, rebuild if needed)
@@ -19,7 +21,19 @@
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")"
# Pre-create ./data as the current (non-root) user. Otherwise, on a fresh
# checkout, Docker's daemon (root) auto-creates these bind-mount sources on
# the first 'up' — leaving them root-owned and blocking this same user from
# later writing to them without sudo (e.g. ./prefetch-models.sh). No-op if
# they already exist, regardless of current ownership.
mkdir -p data/models data/hf_cache data/projects data/patches
if [ $# -eq 0 ]; then if [ $# -eq 0 ]; then
# Best-effort: fetch any missing models on the host first (see header).
./prefetch-models.sh --sdxl \
|| echo "⚠ Model prefetch had failures (see above) — continuing anyway, the container will retry in-container."
exec docker compose -f docker-compose.gpu.yml up -d --build exec docker compose -f docker-compose.gpu.yml up -d --build
else else
exec docker compose -f docker-compose.gpu.yml "$@" exec docker compose -f docker-compose.gpu.yml "$@"
+2 -2
View File
@@ -1,5 +1,5 @@
# ============================================================================= # =============================================================================
# EditmaskwithAI — GPU Docker Compose (NVIDIA CUDA) # PaintPlus — GPU Docker Compose (NVIDIA CUDA)
# #
# ── PREREQUISITES ───────────────────────────────────────────────────────────── # ── PREREQUISITES ─────────────────────────────────────────────────────────────
# #
@@ -66,7 +66,7 @@ services:
# Increment BUILDID to force pip layers to re-run without full --no-cache: # Increment BUILDID to force pip layers to re-run without full --no-cache:
# BUILDID=$(date +%s) docker compose -f docker-compose.gpu.yml up --build # BUILDID=$(date +%s) docker compose -f docker-compose.gpu.yml up --build
BUILDID: ${BUILDID:-1} BUILDID: ${BUILDID:-1}
container_name: editmaskwithai-gpu container_name: paintplus-gpu
ports: ports:
- "${PORT:-3080}:8000" - "${PORT:-3080}:8000"
volumes: volumes:
+1 -1
View File
@@ -25,7 +25,7 @@ class Help_about_class {
{title: "Smart Select:", html: '<a href="https://github.com/facebookresearch/segment-anything" target="_blank">SAM</a> (Meta AI)'}, {title: "Smart Select:", html: '<a href="https://github.com/facebookresearch/segment-anything" target="_blank">SAM</a> (Meta AI)'},
{title: "Remote AI:", html: 'InvokeAI · ComfyUI · OpenAI (user-configured)'}, {title: "Remote AI:", html: 'InvokeAI · ComfyUI · OpenAI (user-configured)'},
{title: "", html: '<hr style="margin:8px 0;border-color:#444;">'}, {title: "", html: '<hr style="margin:8px 0;border-color:#444;">'},
{title: "GitHub:", html: '<a href="https://github.com/outis1one/EditmaskwithAI" target="_blank">outis1one/EditmaskwithAI</a>'}, {title: "GitHub:", html: '<a href="https://github.com/outis1one/PaintPlus" target="_blank">outis1one/PaintPlus</a>'},
], ],
}; };
this.POP.show(settings); this.POP.show(settings);
+35 -4
View File
@@ -19,7 +19,7 @@ if [ "$EUID" -ne 0 ]; then
fi fi
echo "==================================================" echo "=================================================="
echo " EditmaskwithAI — Local GPU one-time setup" echo " PaintPlus — Local GPU one-time setup"
echo "==================================================" echo "=================================================="
echo "" echo ""
@@ -80,6 +80,18 @@ fi
echo "" echo ""
# ── 0.5. Pre-create ./data with correct ownership ────────────────────────────
# Docker's daemon (always root) auto-creates bind-mount source directories on
# the first 'compose up' if they don't exist yet — leaving ./data root-owned
# and blocking the invoking user from later running ./prefetch-models.sh
# without a manual 'sudo chown'. Create it now, owned by the real (non-root)
# user, so that problem has no chance to happen on a fresh checkout.
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
mkdir -p "$REPO_DIR"/data/{models,hf_cache,projects,patches}
chown -R "${SUDO_UID:-$(id -u)}:${SUDO_GID:-$(id -g)}" "$REPO_DIR/data"
echo "✓ ./data prepared (writable without sudo)"
echo ""
# ── 1. NVIDIA container toolkit ────────────────────────────────────────────── # ── 1. NVIDIA container toolkit ──────────────────────────────────────────────
if command -v nvidia-ctk &>/dev/null; then if command -v nvidia-ctk &>/dev/null; then
echo "✓ nvidia-container-toolkit already installed — skipping" echo "✓ nvidia-container-toolkit already installed — skipping"
@@ -158,12 +170,31 @@ else
echo " Minimum driver version: 525" echo " Minimum driver version: 525"
fi fi
# ── 6. Prefetch all AI models (host-side, outside Docker) ───────────────────
# In-container DNS/network is unreliable on some hosts, so downloading on the
# host up front (including SDXL/inpaint, ~13GB) is the default now rather
# than a manual troubleshooting step. Best-effort: this script must finish
# (and leave the GPU/Docker setup done) even if prefetch fails outright.
# Run as the real invoking user, not root, so downloaded files (and any
# `pip install --user` side effects) end up owned by that user — this script
# itself is already running as root via the sudo re-exec above.
echo ""
echo "Prefetching AI models (this can take a while for SDXL, ~13GB)..."
if [ -n "${SUDO_USER:-}" ]; then
sudo -u "$SUDO_USER" -H "$REPO_DIR/prefetch-models.sh" --sdxl \
|| echo "⚠ Model prefetch had failures (see above) — continuing anyway, the container will retry in-container."
else
"$REPO_DIR/prefetch-models.sh" --sdxl \
|| echo "⚠ Model prefetch had failures (see above) — continuing anyway, the container will retry in-container."
fi
echo "" echo ""
echo "==================================================" echo "=================================================="
echo " Setup complete." echo " Setup complete."
echo " Start the app with: ./bring-up-local-gpu.sh" echo " Start the app with: ./bring-up-local-gpu.sh"
echo "" echo ""
echo " If models fail to download inside the container (DNS/firewall" echo " Models are prefetched automatically by this script and by"
echo " blocked), fetch them on the host first instead:" echo " bring-up-local-gpu.sh on every start. If any failed above (no"
echo " ./prefetch-models.sh" echo " network, etc.), re-run manually any time:"
echo " ./prefetch-models.sh --sdxl"
echo "==================================================" echo "=================================================="
+71 -23
View File
@@ -12,9 +12,11 @@
# ./prefetch-models.sh --sdxl # also prefetch SDXL base + inpaint (~13GB) # ./prefetch-models.sh --sdxl # also prefetch SDXL base + inpaint (~13GB)
# #
# Safe to re-run: every download here skips files that already exist # 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")" cd "$(dirname "$0")"
@@ -24,11 +26,32 @@ if ! command -v python3 &>/dev/null; then
exit 1 exit 1
fi fi
if ! mkdir -p data/models data/hf_cache 2>/dev/null; then # mkdir -p succeeds silently on an already-existing directory even when we
echo "✗ Could not create ./data/models or ./data/hf_cache." >&2 # can't write into it, so actually test writability rather than trusting that.
echo " If ./data/ was already created by Docker (root-owned), re-run with sudo:" >&2 check_writable() {
echo " sudo ./prefetch-models.sh $*" >&2 mkdir -p "$1" 2>/dev/null
exit 1 touch "$1/.write_test" 2>/dev/null && rm -f "$1/.write_test"
}
NEED_CHOWN=0
for d in data/models data/hf_cache; do
check_writable "$d" || NEED_CHOWN=1
done
if [ "$NEED_CHOWN" -eq 1 ]; then
echo "⚠ ./data isn't writable by $(id -un) — this usually means Docker created it as root on a previous run."
echo " Fixing ownership (the container runs as root and will still work fine afterward):"
echo " sudo chown -R $(id -u):$(id -g) ./data"
if ! sudo chown -R "$(id -u):$(id -g)" ./data; then
echo "✗ Could not fix ownership automatically (sudo failed or unavailable)." >&2
echo " Run this manually, then re-run this script:" >&2
echo " sudo chown -R \$(id -u):\$(id -g) ./data" >&2
exit 1
fi
for d in data/models data/hf_cache; do
check_writable "$d" || { echo "✗ Still no write permission in ./$d after chown." >&2; exit 1; }
done
echo "✓ Fixed."
fi fi
PREFETCH_SDXL=0 PREFETCH_SDXL=0
@@ -36,36 +59,50 @@ if [ "${1:-}" = "--sdxl" ]; then
PREFETCH_SDXL=1 PREFETCH_SDXL=1
fi fi
FAILED=()
echo "==================================================" echo "=================================================="
echo " Prefetching AI models (host-side, no Docker)" echo " Prefetching AI models (host-side, no Docker)"
echo "==================================================" echo "=================================================="
echo "" echo ""
echo "── SAM (Smart Select) ───────────────────────────────" echo "── SAM (Smart Select) ───────────────────────────────"
python3 scripts/download_sam_model.py vit_b python3 scripts/download_sam_model.py vit_b || FAILED+=("SAM")
echo "" echo ""
echo "── U2Net (Remove Background fallback) ──────────────" echo "── U2Net (Remove Background fallback) ──────────────"
python3 scripts/download_u2net_model.py u2net python3 scripts/download_u2net_model.py u2net || FAILED+=("U2Net")
echo "" echo ""
echo "── HuggingFace Hub models (BEN2, BiRefNet-HR) ───────" echo "── HuggingFace Hub models (BEN2, BiRefNet-HR) ───────"
if ! python3 -c "import huggingface_hub" &>/dev/null; then if ! python3 -c "import huggingface_hub" &>/dev/null; then
echo "Installing huggingface_hub (lightweight — no torch/GPU needed for this step)..." echo "Installing huggingface_hub (lightweight — no torch/GPU needed for this step)..."
python3 -m pip install --quiet --user "huggingface_hub>=0.23.0" PIP_ERR=$(python3 -m pip install --quiet --user "huggingface_hub>=0.23.0" 2>&1) || {
if echo "$PIP_ERR" | grep -q "externally-managed-environment"; then
# PEP 668 (Debian/Ubuntu 12+): --user already keeps this out of
# apt-managed system site-packages, so overriding here is safe.
echo "System Python is externally managed — retrying with --break-system-packages"
python3 -m pip install --quiet --user --break-system-packages "huggingface_hub>=0.23.0" \
|| FAILED+=("huggingface_hub install")
else
echo "$PIP_ERR" >&2
FAILED+=("huggingface_hub install")
fi
}
fi fi
# HF_HOME must match what the container resolves by default: the bind mount if python3 -c "import huggingface_hub" &>/dev/null; then
# maps ./data/hf_cache -> /root/.cache/huggingface, and the container never # HF_HOME must match what the container resolves by default: the bind mount
# sets HF_HOME explicitly, so it defaults to ~/.cache/huggingface there. # maps ./data/hf_cache -> /root/.cache/huggingface, and the container never
# huggingface_hub itself appends "/hub" to HF_HOME to get the actual cache # sets HF_HOME explicitly, so it defaults to ~/.cache/huggingface there.
# root (HF_HUB_CACHE) — setting HF_HOME here (instead of passing --cache-dir # huggingface_hub itself appends "/hub" to HF_HOME to get the actual cache
# or cache_dir=... directly) lets both sides derive that "/hub" nesting the # root (HF_HUB_CACHE) — setting HF_HOME here (instead of passing --cache-dir
# same way, rather than us hardcoding it and risking a mismatch. # or cache_dir=... directly) lets both sides derive that "/hub" nesting the
export HF_HOME="$(pwd)/data/hf_cache" # 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 import os
from huggingface_hub import snapshot_download from huggingface_hub import snapshot_download
@@ -81,12 +118,23 @@ for repo_id in repos:
snapshot_download(repo_id=repo_id, ignore_patterns=["*.msgpack", "flax_*", "tf_*"]) snapshot_download(repo_id=repo_id, ignore_patterns=["*.msgpack", "flax_*", "tf_*"])
print(f" done: {repo_id}") print(f" done: {repo_id}")
PYEOF PYEOF
else
echo "⚠ Skipping BEN2/BiRefNet-HR — huggingface_hub unavailable (install failed above)"
FAILED+=("HuggingFace models")
fi
echo "" echo ""
echo "==================================================" echo "=================================================="
echo " Done. Models cached under ./data/models and ./data/hf_cache" if [ ${#FAILED[@]} -eq 0 ]; then
if [ "$PREFETCH_SDXL" != "1" ]; then echo " Done. Models cached under ./data/models and ./data/hf_cache"
echo " (SDXL not included — re-run with --sdxl to also prefetch txt2img/inpaint, ~13GB)" 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 fi
echo " Start the app: ./bring-up-local-gpu.sh"
echo "==================================================" echo "=================================================="
+16 -11
View File
@@ -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): def download_with_progress(url: str, dest_path: Path):
"""Download file with progress indicator""" """Download file with progress indicator"""
print(f"Downloading to: {dest_path}") print(f"Downloading to: {dest_path}")
@@ -88,12 +102,7 @@ def main():
print(f"\nModel already exists at: {dest_path}") print(f"\nModel already exists at: {dest_path}")
print("To re-download, delete the file first.") print("To re-download, delete the file first.")
# Create symlink for easy access create_symlink(models_dir / 'sam_model.pth', dest_path.name)
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}")
return return
print(f"\nDownloading SAM {model_type.upper()} ({model_info['size']})...") print(f"\nDownloading SAM {model_type.upper()} ({model_info['size']})...")
@@ -103,17 +112,13 @@ def main():
try: try:
download_with_progress(model_info['url'], dest_path) download_with_progress(model_info['url'], dest_path)
# Create symlink for easy access
symlink_path = models_dir / 'sam_model.pth' symlink_path = models_dir / 'sam_model.pth'
if symlink_path.exists() or symlink_path.is_symlink(): create_symlink(symlink_path, dest_path.name)
symlink_path.unlink()
symlink_path.symlink_to(dest_path.name)
print() print()
print("=" * 60) print("=" * 60)
print("SUCCESS!") print("SUCCESS!")
print(f"Model saved to: {dest_path}") print(f"Model saved to: {dest_path}")
print(f"Symlink: {symlink_path}")
print("=" * 60) print("=" * 60)
except Exception as e: except Exception as e:
+1 -1
View File
@@ -6,7 +6,7 @@ These are public domain images from Wikimedia Commons of classical sculptures.
Run this script to populate the eye catalog with example eyes. Run this script to populate the eye catalog with example eyes.
Usage: Usage:
cd /home/user/EditmaskwithAI cd /home/user/PaintPlus
python scripts/download_sample_eyes.py python scripts/download_sample_eyes.py
""" """