Compare commits
13
Commits
3a977c7438
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bd87dbf23 | ||
|
|
608d6958b1 | ||
|
|
4e43728583 | ||
|
|
3f4a6b754c | ||
|
|
f49718a260 | ||
|
|
b4a790473c | ||
|
|
e96d7cc204 | ||
|
|
eca834a7f0 | ||
|
|
b8523cb729 | ||
|
|
851e255641 | ||
|
|
b70f95ede5 | ||
|
|
f58c96c20f | ||
|
|
668728348b |
@@ -1,4 +1,4 @@
|
|||||||
# Caddy 2 Configuration for EditmaskwithAI
|
# Caddy 2 Configuration for PaintPlus
|
||||||
# ==========================================
|
# ==========================================
|
||||||
#
|
#
|
||||||
# SETUP INSTRUCTIONS:
|
# SETUP INSTRUCTIONS:
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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 "$@"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 "=================================================="
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user