From 80830beae28a2b664feecc08e8e47959654f8dcc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 23:33:07 +0000 Subject: [PATCH 1/4] Fix scale-selection 500 error (missing BytesIO import) and compact GPU badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ai_tools.py: add 'from io import BytesIO' — scale_selection and paste endpoints used BytesIO directly but it was only imported locally in one unrelated function, causing NameError on every scale call. provider-badge.js: redesign badge to fit the narrow (~40px) left toolbar. Was rendering 'GPU · sdxl_offload · Quadro RTX 3000' inline which wrapped into multiple lines covering tool icons. Now shows a status dot + short label (SDXL / OAI / Rep…) with all details moved to the hover tooltip. https://claude.ai/code/session_01WVDg7amsy1TTtxvpku7bcM --- backend/app/routers/ai_tools.py | 1 + .../src/js/core/components/provider-badge.js | 83 ++++++++++--------- 2 files changed, 45 insertions(+), 39 deletions(-) diff --git a/backend/app/routers/ai_tools.py b/backend/app/routers/ai_tools.py index dba991e..a5d8095 100644 --- a/backend/app/routers/ai_tools.py +++ b/backend/app/routers/ai_tools.py @@ -10,6 +10,7 @@ from typing import Optional import base64 import asyncio import json +from io import BytesIO from app.services.local_inpaint import ( lama_inpaint, opencv_inpaint, lama_available, gpu_available, rembg_available, diff --git a/frontend/src/js/core/components/provider-badge.js b/frontend/src/js/core/components/provider-badge.js index fc20790..35f0ca0 100644 --- a/frontend/src/js/core/components/provider-badge.js +++ b/frontend/src/js/core/components/provider-badge.js @@ -1,10 +1,6 @@ /** - * ProviderBadge — small DOM element showing the active AI provider. - * Inserted into the toolbar footer on app load. - * - * Green = remote provider healthy (or local_gpu active) - * Yellow = provider configured but unhealthy/unreachable - * Grey = local only (LaMa + OpenCV) + * ProviderBadge — compact status indicator in the left toolbar footer. + * Shows a dot + 3-5 char label; all details in the tooltip. */ import { getCapabilities } from '../../api/capabilities.js'; @@ -15,84 +11,74 @@ export async function mountProviderBadge(container) { var badge = document.createElement('div'); badge.id = 'provider-badge'; badge.style.cssText = [ - 'display:inline-flex', 'align-items:center', 'gap:5px', - 'padding:3px 8px', 'border-radius:10px', - 'font-size:11px', 'font-family:sans-serif', + 'display:flex', 'flex-direction:column', 'align-items:center', 'gap:2px', + 'padding:4px 2px 4px', + 'font-size:9px', 'font-family:sans-serif', 'line-height:1.2', 'cursor:default', 'user-select:none', - 'margin:4px', 'opacity:0.85', + 'width:100%', 'box-sizing:border-box', + 'text-align:center', 'word-break:break-word', ].join(';'); var dot = document.createElement('span'); - dot.style.cssText = 'width:7px;height:7px;border-radius:50%;display:inline-block;'; + dot.style.cssText = 'width:8px;height:8px;border-radius:50%;display:block;flex-shrink:0;'; var label = document.createElement('span'); + label.style.cssText = 'color:inherit;max-width:36px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;'; var remote = caps.remote || {}; var local = caps.local || {}; if (remote.provider === 'local_gpu') { - // Local GPU provider — show GPU name and tier from /api/config local fields var gpuName = _shortGpuName(local.gpu_device); var tier = local.gpu_tier || ''; if (remote.healthy) { dot.style.background = '#44cc44'; - badge.style.background = '#1a2a1a'; badge.style.color = '#aaffaa'; - label.textContent = 'GPU · ' + tier + ' · ' + gpuName; + label.textContent = _shortTier(tier); var flagList = [ local.gpu_fp16 && 'fp16', local.gpu_bf16 && 'bf16', local.gpu_fp8 && 'fp8', - local.gpu_tensor_cores && 'tensor-cores', + local.gpu_tensor_cores && 'TC', ].filter(Boolean).join(' '); badge.title = [ - local.gpu_device || gpuName, - 'VRAM: ' + local.gpu_vram_total + ' GB total ' + local.gpu_vram_free + ' GB free', - 'Compute: CC ' + local.gpu_cc + ' Eff: ' + local.gpu_eff_vram + ' GB', - flagList ? 'Features: ' + flagList : '', - 'Capabilities: ' + (local.local_gpu_capabilities || []).join(', '), + gpuName, + 'VRAM: ' + local.gpu_vram_total + ' GB total / ' + local.gpu_vram_free + ' GB free', + 'CC: ' + local.gpu_cc + ' Eff VRAM: ' + local.gpu_eff_vram + ' GB', + flagList ? 'Flags: ' + flagList : '', + tier ? 'Tier: ' + tier : '', (local.local_gpu_warnings || []).length - ? '\nWarnings:\n' + local.local_gpu_warnings.join('\n') + ? 'Warnings:\n' + local.local_gpu_warnings.join('\n') : '', ].filter(Boolean).join('\n'); } else { dot.style.background = '#ffaa00'; - badge.style.background = '#2a2000'; badge.style.color = '#ffdd88'; - label.textContent = 'Local GPU (not ready)'; - badge.title = 'local_gpu is configured but the diffusers library may not be installed.\nCheck container logs for details.'; + label.textContent = 'GPU?'; + badge.title = 'local_gpu configured but diffusers may not be installed.\nCheck container logs.'; } } else if (remote.provider && remote.healthy) { dot.style.background = '#44cc44'; - badge.style.background = '#1a2a1a'; badge.style.color = '#aaffaa'; - - var overrides = remote.overrides || {}; - var overrideEntries = Object.entries(overrides).filter(([, v]) => v); - var overrideStr = overrideEntries.length - ? ' · ' + overrideEntries.map(([k, v]) => k + '→' + v).join(', ') - : ''; - label.textContent = remote.provider + overrideStr + (local.gpu_detected ? ' · GPU' : ''); + label.textContent = _shortProvider(remote.provider); var opLines = Object.entries(remote.operations || {}) .map(([op, s]) => op + ': ' + (s.provider || remote.provider) + ' ' + (s.healthy ? '✓' : '✗')) .join('\n'); - badge.title = opLines || ('Provider: ' + remote.provider); + badge.title = ('Provider: ' + remote.provider) + (opLines ? '\n' + opLines : ''); } else if (remote.provider && !remote.healthy) { dot.style.background = '#ffaa00'; - badge.style.background = '#2a2000'; badge.style.color = '#ffdd88'; - label.textContent = remote.provider + ' (offline)'; - badge.title = remote.provider + ' is configured but not reachable. Check your .env URL.'; + label.textContent = _shortProvider(remote.provider) + '?'; + badge.title = remote.provider + ' configured but not reachable.\nCheck your .env URL.'; } else { dot.style.background = '#888888'; - badge.style.background = '#1a1a1a'; badge.style.color = '#aaaaaa'; - label.textContent = 'Local' + (local.lama ? ' · LaMa' : '') + (local.gpu_detected ? ' · GPU' : ''); - badge.title = 'Local only. Set AI_PROVIDER in .env to enable generative tools.'; + label.textContent = local.lama ? 'LaMa' : 'Local'; + badge.title = 'Local only (no generative AI).\nSet AI_PROVIDER in .env to enable.'; } badge.appendChild(dot); @@ -108,6 +94,25 @@ export async function mountProviderBadge(container) { function _shortGpuName(name) { return (name || 'GPU') .replace(/^NVIDIA GeForce\s+/i, '') + .replace(/^NVIDIA Quadro\s+/i, '') .replace(/^NVIDIA\s+/i, '') .replace(/^AMD Radeon\s+/i, ''); } + +function _shortTier(tier) { + if (!tier) return 'GPU'; + // sdxl_offload → SDXL, flux → FLUX, sd15 → SD15 + return tier + .replace(/_offload$/, '') + .replace(/_cpu$/, '') + .toUpperCase() + .slice(0, 6); +} + +function _shortProvider(p) { + var map = { + openai: 'OAI', replicate: 'Rep', stability: 'Stab', + invokeai: 'Inv', comfyui: 'CUI', local_gpu: 'GPU', + }; + return map[p] || (p || 'AI').slice(0, 4); +} From d7683b836a555d752af760ad3d91b853cd94a197 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 23:35:12 +0000 Subject: [PATCH 2/4] Try Cloudflare DNS first in GPU container; add troubleshooting note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.1.1.1 uses a different anycast route than 8.8.8.8 so may reach the container even when Google's servers don't. Falls back to Google if Cloudflare is also unreachable. If all fail (Errno -3), port 53 UDP is blocked at the Docker bridge level — comment has the iptables fix. https://claude.ai/code/session_01WVDg7amsy1TTtxvpku7bcM --- docker-compose.gpu.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docker-compose.gpu.yml b/docker-compose.gpu.yml index b44072e..ad583e9 100644 --- a/docker-compose.gpu.yml +++ b/docker-compose.gpu.yml @@ -126,8 +126,12 @@ services: count: 1 capabilities: [gpu] - # Reliable DNS for HuggingFace Hub downloads and external API calls + # DNS: try host resolver first (works on most networks including corporate/VPN), + # fall back to Cloudflare then Google public resolvers. + # If all three fail (Errno -3), your firewall is blocking port 53 UDP from Docker. + # Fix on the host: sudo iptables -I DOCKER-USER -p udp --dport 53 -j ACCEPT dns: + - 1.1.1.1 - 8.8.8.8 - 8.8.4.4 From 59b26b6d5e59d233b41178a3b05df21c19a78f49 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 23:53:21 +0000 Subject: [PATCH 3/4] Fix diffusers/torch compat, SAM docs, and SelectionActions UX - Pin diffusers to >=0.28.0,<0.29.0 to avoid AttributeError on torch.xpu (diffusers 0.29+ requires PyTorch 2.4 but base image ships 2.1.2) - Pin transformers to <4.40.0 to match - README: add SAM offline download troubleshooting for DNS-blocked containers - SelectionActions panel: 'Selection ready' title + subtitle makes clear nothing has fired yet; AI actions show inline description (not just tooltip); section labels 'AI Actions' / 'Classic Tools'; scale hint updates live; Enter key submits AI edit prompt; _actionCard hover border for clickability https://claude.ai/code/session_01WVDg7amsy1TTtxvpku7bcM --- README.md | 19 +++ backend/requirements.gpu.txt | 9 +- frontend/src/js/tools/selection_actions.js | 142 ++++++++++++++------- 3 files changed, 122 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index f912014..23f3163 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,25 @@ docker compose -f docker-compose.gpu.yml logs -f | grep -E "local_gpu|Error|Fail # If a private/gated model: add HF_TOKEN=hf_... to .env ``` +**SAM model fails to download (DNS error / firewall blocking port 53)** + +If the container can't reach `dl.fbaipublicfiles.com` (you'll see `Errno -3 Name or service not known` in the logs), download SAM directly on the host and let the bind mount make it visible to the container — no rebuild needed: + +```bash +mkdir -p ./data/models +# sudo needed if ./data/ was created by Docker (root-owned): +sudo curl -L -o ./data/models/sam_vit_b_01ec64.pth \ + https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth +``` + +The file is ~375 MB. Once it exists at `./data/models/sam_vit_b_01ec64.pth`, the container picks it up on the next startup (no rebuild required). Verify with: +```bash +docker compose -f docker-compose.gpu.yml logs | grep -i sam +# Should show: "SAM model loaded on cuda" (or cpu) +``` + +If Docker created `./data/` as root and you can't write there without `sudo`, you can also use root's curl as above — the container reads the file regardless of owner. + **Out of VRAM during generation** - Reduce `LOCAL_GPU_MAX_PIPELINES=1` in `.env` (default 2) - Or override to a smaller model: `HF_MODEL_TXT2IMG=runwayml/stable-diffusion-v1-5` diff --git a/backend/requirements.gpu.txt b/backend/requirements.gpu.txt index 97bb4df..02af631 100644 --- a/backend/requirements.gpu.txt +++ b/backend/requirements.gpu.txt @@ -9,9 +9,12 @@ # ============================================================================= # HuggingFace Diffusers ecosystem -# 0.29.0+ required for FLUX pipeline support -diffusers>=0.29.0 -transformers>=4.40.0 +# Pinned <0.29.0: diffusers 0.29.0 added torch.xpu (Intel GPU) which fails on +# PyTorch 2.1.x with "AttributeError: module 'torch' has no attribute 'xpu'". +# Upgrade the base image in Dockerfile.gpu to pytorch 2.4+ before lifting this pin. +# (FLUX support requires diffusers>=0.29 + PyTorch>=2.4; SDXL/SD works fine here.) +diffusers>=0.28.0,<0.29.0 +transformers>=4.36.0,<4.40.0 accelerate>=0.27.0 huggingface-hub>=0.23.0 safetensors>=0.4.0 diff --git a/frontend/src/js/tools/selection_actions.js b/frontend/src/js/tools/selection_actions.js index caaa404..b7ec705 100644 --- a/frontend/src/js/tools/selection_actions.js +++ b/frontend/src/js/tools/selection_actions.js @@ -52,7 +52,8 @@ export class SelectionActions { 'font-family:sans-serif', 'font-size:12px', 'color:#d0d0e0', - 'min-width:340px', + 'min-width:360px', + 'max-width:420px', 'box-shadow:0 8px 32px rgba(0,0,0,0.7)', 'display:flex', 'flex-direction:column', @@ -61,37 +62,56 @@ export class SelectionActions { // ── Title row ──────────────────────────────────────────────────────── var titleRow = document.createElement('div'); - titleRow.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:4px'; - var title = document.createElement('span'); - title.textContent = 'Selection Actions'; - title.style.cssText = 'font-size:13px;font-weight:bold;color:#aaaaff'; + titleRow.style.cssText = 'display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:2px'; + var titleBlock = document.createElement('div'); + var title = document.createElement('div'); + title.textContent = 'Selection ready'; + title.style.cssText = 'font-size:13px;font-weight:bold;color:#aaaaff;line-height:1.3'; + var subtitle = document.createElement('div'); + subtitle.textContent = 'Nothing has changed yet — choose an action below'; + subtitle.style.cssText = 'font-size:10px;color:#7777aa;margin-top:1px'; + titleBlock.appendChild(title); + titleBlock.appendChild(subtitle); var closeX = document.createElement('button'); closeX.textContent = '✕'; - closeX.style.cssText = 'background:none;border:none;color:#666;cursor:pointer;font-size:14px;padding:0;line-height:1'; - closeX.title = 'Close panel (keep selection)'; + closeX.style.cssText = 'background:none;border:none;color:#666;cursor:pointer;font-size:14px;padding:0 0 0 8px;line-height:1;flex-shrink:0'; + closeX.title = 'Dismiss (keeps your selection active)'; closeX.onclick = () => this.hide(); - titleRow.appendChild(title); + titleRow.appendChild(titleBlock); titleRow.appendChild(closeX); panel.appendChild(titleRow); - // ── Scale by % ─────────────────────────────────────────────────────── + // ── Section: AI Actions ────────────────────────────────────────────── + panel.appendChild(_sectionLabel('AI Actions')); + + // Scale by % + var scaleWrap = document.createElement('div'); + scaleWrap.style.cssText = 'background:#16213e;border-radius:7px;padding:7px 10px'; var scaleRow = document.createElement('div'); - scaleRow.style.cssText = 'display:flex;align-items:center;gap:6px;background:#16213e;border-radius:7px;padding:7px 10px'; + scaleRow.style.cssText = 'display:flex;align-items:center;gap:6px'; var scaleLabel = document.createElement('span'); - scaleLabel.textContent = 'Scale by'; + scaleLabel.textContent = 'Scale object by'; scaleLabel.style.color = '#aaa'; var scaleInput = document.createElement('input'); scaleInput.type = 'number'; scaleInput.value = '103'; scaleInput.min = '1'; scaleInput.max = '500'; - scaleInput.title = '103 = 3% bigger · 95 = 5% smaller'; scaleInput.style.cssText = 'width:52px;background:#0f0f1a;color:#fff;border:1px solid #4a4a8a;border-radius:4px;padding:2px 5px;font-size:12px'; var scaleUnit = document.createElement('span'); scaleUnit.textContent = '%'; scaleUnit.style.color = '#888'; + var scaleHint = document.createElement('span'); + scaleHint.style.cssText = 'color:#6688aa;font-size:10px;margin-left:2px'; + scaleHint.textContent = '= 3% bigger'; var scaleBtn = _btn('Apply', '#1a2a4a', '#8aacff'); scaleBtn.style.marginLeft = 'auto'; + scaleInput.addEventListener('input', () => { + var v = parseFloat(scaleInput.value); + if (isNaN(v) || v === 100) scaleHint.textContent = '= no change'; + else if (v > 100) scaleHint.textContent = '= ' + (v - 100).toFixed(0) + '% bigger'; + else scaleHint.textContent = '= ' + (100 - v).toFixed(0) + '% smaller'; + }); scaleBtn.onclick = () => { var pct = parseFloat(scaleInput.value) || 103; this._scaleSelection(pct); @@ -99,57 +119,73 @@ export class SelectionActions { scaleRow.appendChild(scaleLabel); scaleRow.appendChild(scaleInput); scaleRow.appendChild(scaleUnit); + scaleRow.appendChild(scaleHint); scaleRow.appendChild(scaleBtn); - panel.appendChild(scaleRow); + var scaleDesc = document.createElement('div'); + scaleDesc.textContent = 'Moves the selected object, then AI fills the vacated area'; + scaleDesc.style.cssText = 'color:#5566aa;font-size:10px;margin-top:4px'; + scaleWrap.appendChild(scaleRow); + scaleWrap.appendChild(scaleDesc); + panel.appendChild(scaleWrap); - // ── AI actions ─────────────────────────────────────────────────────── - panel.appendChild( - _actionBtn('Make less symmetrical', '#1c1a2e', '#cc99ff', - '⟳ AI redraws the region with natural, organic asymmetry', - () => this._makeAsymmetric()) - ); - panel.appendChild( - _actionBtn('Replace with clipboard', '#1a2a1a', '#88dd88', - '📋 Scales your clipboard image into the selection shape', - () => this._pasteFromClipboard()) - ); + // Make less symmetrical + panel.appendChild(_actionCard( + 'Make less symmetrical', + '#1c1a2e', '#cc99ff', + 'AI redraws the selection with subtle, natural imperfections', + () => this._makeAsymmetric() + )); - // ── Custom AI edit prompt ───────────────────────────────────────────── + // Replace with clipboard + panel.appendChild(_actionCard( + 'Replace with clipboard', + '#1a2a1a', '#88dd88', + 'Scales your clipboard image to fit inside the selection shape', + () => this._pasteFromClipboard() + )); + + // Custom AI edit prompt + var aiWrap = document.createElement('div'); + aiWrap.style.cssText = 'background:#16213e;border-radius:7px;padding:7px 10px'; var aiRow = document.createElement('div'); - aiRow.style.cssText = 'display:flex;align-items:center;gap:6px;background:#16213e;border-radius:7px;padding:7px 10px'; + aiRow.style.cssText = 'display:flex;align-items:center;gap:6px'; var aiInput = document.createElement('input'); aiInput.type = 'text'; - aiInput.placeholder = 'AI edit: "add a scar", "make it look aged", …'; + aiInput.placeholder = '"add a scar", "make it look aged", "blue eyes" …'; aiInput.style.cssText = 'flex:1;background:#0f0f1a;color:#fff;border:1px solid #4a4a8a;border-radius:4px;padding:3px 7px;font-size:11px'; - var aiBtn = _btn('Edit', '#1a2a4a', '#8aacff'); + var aiBtn = _btn('AI Edit', '#1a2a4a', '#8aacff'); aiBtn.onclick = () => { var instruction = aiInput.value.trim(); - if (!instruction) { alertify.warning('Enter an AI edit instruction first.'); return; } + if (!instruction) { alertify.warning('Enter an instruction first — describe what to change.'); return; } this._aiEditRegion(instruction); }; + aiInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') aiBtn.click(); + }); + var aiDesc = document.createElement('div'); + aiDesc.textContent = 'Inpaints the selected region according to your description'; + aiDesc.style.cssText = 'color:#5566aa;font-size:10px;margin-top:4px'; aiRow.appendChild(aiInput); aiRow.appendChild(aiBtn); - panel.appendChild(aiRow); + aiWrap.appendChild(aiRow); + aiWrap.appendChild(aiDesc); + panel.appendChild(aiWrap); - // ── Divider ────────────────────────────────────────────────────────── - var hr = document.createElement('div'); - hr.style.cssText = 'border-top:1px solid #2a2a4a;margin:2px 0'; - panel.appendChild(hr); - - // ── Classic selection ops ───────────────────────────────────────────── + // ── Section: Classic Tools ──────────────────────────────────────────── + panel.appendChild(_sectionLabel('Classic Tools')); var classicRow = document.createElement('div'); classicRow.style.cssText = 'display:flex;gap:6px'; var copyBtn = _btn('Copy to layer', '#1a2a1a', '#88cc88'); copyBtn.style.flex = '1'; - copyBtn.title = 'Ctrl+C'; + copyBtn.title = 'Lift a copy of the selection onto a new layer (non-destructive)'; copyBtn.onclick = () => { this.tool.copyToLayer(); this.hide(); }; var cutBtn = _btn('Cut to layer', '#2a1a1a', '#cc8888'); cutBtn.style.flex = '1'; - cutBtn.title = 'Ctrl+X'; + cutBtn.title = 'Cut the selection to a new layer (erases from original)'; cutBtn.onclick = () => { this.tool.cutToLayer(); this.hide(); }; var delBtn = _btn('Erase', '#2a1a1a', '#ff7766'); delBtn.style.flex = '0 0 auto'; - delBtn.title = 'Delete key'; + delBtn.title = 'Delete the selected pixels (transparent / background color)'; delBtn.onclick = () => { this.tool.deleteSelection(); this.hide(); }; classicRow.appendChild(copyBtn); classicRow.appendChild(cutBtn); @@ -330,12 +366,28 @@ function _btn(text, bg, color) { return b; } -function _actionBtn(text, bg, color, tooltip, handler) { - var b = _btn(text, bg, color); - b.style.cssText += ';display:block;width:100%;text-align:left;padding:7px 10px;border-radius:7px;font-size:12px'; - if (tooltip) b.title = tooltip; - b.onclick = handler; - return b; +function _actionCard(text, bg, color, description, handler) { + var wrap = document.createElement('div'); + wrap.style.cssText = 'background:' + bg + ';border-radius:7px;padding:7px 10px;cursor:pointer;border:1px solid transparent'; + wrap.addEventListener('mouseenter', () => { wrap.style.borderColor = color; }); + wrap.addEventListener('mouseleave', () => { wrap.style.borderColor = 'transparent'; }); + wrap.onclick = handler; + var label = document.createElement('div'); + label.textContent = text; + label.style.cssText = 'color:' + color + ';font-size:12px;font-weight:500;pointer-events:none'; + var desc = document.createElement('div'); + desc.textContent = description; + desc.style.cssText = 'color:#5566aa;font-size:10px;margin-top:3px;pointer-events:none'; + wrap.appendChild(label); + wrap.appendChild(desc); + return wrap; +} + +function _sectionLabel(text) { + var el = document.createElement('div'); + el.style.cssText = 'font-size:9px;font-weight:bold;letter-spacing:0.08em;color:#555577;text-transform:uppercase;margin-top:2px'; + el.textContent = text; + return el; } async function _post(path, body) { From 11772e620c88c12767dcdb2db8e91eb20d7b063c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 00:14:34 +0000 Subject: [PATCH 4/4] Fix build cache, DNS/model download, and AI Edit error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build / pip layer fixes: - Add BUILDID ARG to Dockerfile.gpu; pass from docker-compose.gpu.yml build args so pip layers can be force-busted without --no-cache: BUILDID=$(date +%s) docker compose -f docker-compose.gpu.yml up --build Model download (DNS-blocked environments): - Change HF model cache from named volume to ./data/hf_cache bind mount so models can be pre-downloaded on the host (no rebuild needed) - Remove now-unused hf_model_cache named volume - README: add iptables fix + huggingface-cli offline download instructions Error handling improvements: - ai_edit_region: catch ConnectError/Errno-3 → return 503 with exact fix commands - _require_remote: give actionable message when local_gpu provider fails to load - _build_provider: catch AttributeError (torch.xpu from wrong diffusers) not just ImportError - local_diffusion.py: fix docstring to reflect <0.29.0 pin https://claude.ai/code/session_01WVDg7amsy1TTtxvpku7bcM --- Dockerfile.gpu | 7 +++-- README.md | 33 +++++++++++++++++++++ backend/app/routers/ai_tools.py | 39 +++++++++++++++++++++---- backend/app/services/local_diffusion.py | 4 +-- backend/app/services/remote_provider.py | 3 +- docker-compose.gpu.yml | 18 +++++++----- 6 files changed, 86 insertions(+), 18 deletions(-) diff --git a/Dockerfile.gpu b/Dockerfile.gpu index 4516c49..a27584a 100644 --- a/Dockerfile.gpu +++ b/Dockerfile.gpu @@ -42,10 +42,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* # Install Python dependencies — base + GPU extras +# BUILDID forces pip layers to re-run when you need fresh packages without a full --no-cache: +# BUILDID=$(date +%s) docker compose -f docker-compose.gpu.yml up --build +ARG BUILDID=1 COPY backend/requirements.txt . COPY backend/requirements.gpu.txt . -RUN pip install --no-cache-dir -r requirements.txt -RUN pip install --no-cache-dir -r requirements.gpu.txt +RUN echo "BUILDID=$BUILDID" && pip install --no-cache-dir -r requirements.txt +RUN echo "BUILDID=$BUILDID" && pip install --no-cache-dir -r requirements.gpu.txt # Smoke-test rembg (model downloads on first use) RUN python -c "from rembg import remove; print('rembg OK')" \ diff --git a/README.md b/README.md index 23f3163..86186d9 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,12 @@ docker compose -f docker-compose.gpu.yml up -d --build docker compose up -d --build ``` +If pip packages seem stale after a pull (e.g., wrong diffusers version), force a pip layer rebuild without re-downloading the entire PyTorch base image: + +```bash +BUILDID=$(date +%s) docker compose -f docker-compose.gpu.yml up -d --build +``` + --- ## AI Providers @@ -202,6 +208,33 @@ docker compose -f docker-compose.gpu.yml logs | grep -i sam If Docker created `./data/` as root and you can't write there without `sudo`, you can also use root's curl as above — the container reads the file regardless of owner. +**AI Edit returns "model files not yet downloaded" or "Errno -3 / DNS" error** + +The container's DNS is blocked (common on corporate networks or custom iptables rules), so it can't download SDXL models from HuggingFace. Two options: + +*Option A — fix Docker DNS (recommended, one command):* +```bash +sudo iptables -I DOCKER-USER -p udp --dport 53 -j ACCEPT +docker compose -f docker-compose.gpu.yml restart +``` + +*Option B — pre-download models on the host (if iptables fix isn't possible):* +```bash +pip install huggingface-hub + +# Download the inpainting model (~6.5 GB, needed for AI Edit): +huggingface-cli download diffusers/stable-diffusion-xl-1.0-inpainting-0.1 \ + --cache-dir ./data/hf_cache \ + --exclude "*.msgpack" "flax_*" "tf_*" + +# Download the text-to-image model (~6.5 GB, needed for Text → Image): +huggingface-cli download stabilityai/stable-diffusion-xl-base-1.0 \ + --cache-dir ./data/hf_cache \ + --exclude "*.msgpack" "flax_*" "tf_*" +``` + +The models land in `./data/hf_cache/` which is bind-mounted into the container — no rebuild needed. Restart the container and the first AI Edit request loads from local disk. + **Out of VRAM during generation** - Reduce `LOCAL_GPU_MAX_PIPELINES=1` in `.env` (default 2) - Or override to a smaller model: `HF_MODEL_TXT2IMG=runwayml/stable-diffusion-v1-5` diff --git a/backend/app/routers/ai_tools.py b/backend/app/routers/ai_tools.py index a5d8095..7bf0cc1 100644 --- a/backend/app/routers/ai_tools.py +++ b/backend/app/routers/ai_tools.py @@ -80,8 +80,19 @@ def _encode(data: bytes) -> str: def _require_remote(operation: str = None): from app.services.remote_provider import get_remote_provider + from app.config import settings provider = get_remote_provider(operation) if provider is None: + if (settings.ai_provider or "").lower() == "local_gpu": + raise HTTPException( + status_code=503, + detail=( + "local_gpu provider failed to load — diffusers may be incompatible with " + "the installed PyTorch version. Check container logs for details. " + "If you see 'torch has no attribute xpu', rebuild the container from the " + "correct branch so the pinned diffusers<0.29.0 is installed." + ) + ) op_hint = f"AI_PROVIDER_{operation.upper()} or " if operation else "" raise HTTPException( status_code=503, @@ -502,12 +513,28 @@ async def ai_edit_region(req: AiEditRegionRequest): Works with local_gpu, InvokeAI, ComfyUI, or OpenAI. """ provider = _require_remote("inpaint") - result_bytes = await provider.inpaint( - _decode(req.image), - _decode(req.mask), - req.instruction, - {"negative_prompt": req.negative_prompt, "steps": req.steps, "cfg_scale": req.cfg_scale}, - ) + try: + result_bytes = await provider.inpaint( + _decode(req.image), + _decode(req.mask), + req.instruction, + {"negative_prompt": req.negative_prompt, "steps": req.steps, "cfg_scale": req.cfg_scale}, + ) + except Exception as exc: + import traceback; traceback.print_exc() + msg = str(exc) + if "Errno -3" in msg or "Name or service not known" in msg or "ConnectError" in msg: + raise HTTPException( + status_code=503, + detail=( + "AI model files not yet downloaded — container DNS appears to be blocked. " + "Fix: sudo iptables -I DOCKER-USER -p udp --dport 53 -j ACCEPT on the host, " + "or pre-download the model: pip install huggingface-hub && " + "huggingface-cli download diffusers/stable-diffusion-xl-1.0-inpainting-0.1 " + "--cache-dir ./data/hf_cache" + ) + ) + raise HTTPException(status_code=500, detail=msg) return {"result": _encode(result_bytes)} diff --git a/backend/app/services/local_diffusion.py b/backend/app/services/local_diffusion.py index be2c1a1..2e4312e 100644 --- a/backend/app/services/local_diffusion.py +++ b/backend/app/services/local_diffusion.py @@ -11,8 +11,8 @@ Supported model families: sd2x → StableDiffusion2*Pipeline (SD 2.x) sd15 → StableDiffusionPipeline (SD 1.5) -Requires: diffusers>=0.29.0, transformers, accelerate, safetensors - (all in requirements.gpu.txt) +Requires: diffusers>=0.28.0,<0.29.0, transformers, accelerate, safetensors + (all in requirements.gpu.txt — pinned <0.29.0 for PyTorch 2.1.x compatibility) """ from __future__ import annotations diff --git a/backend/app/services/remote_provider.py b/backend/app/services/remote_provider.py index a4107dc..5fcb603 100644 --- a/backend/app/services/remote_provider.py +++ b/backend/app/services/remote_provider.py @@ -432,7 +432,8 @@ def _build_provider(name: str) -> Optional[RemoteAIProvider]: try: from app.services.local_diffusion import get_local_diffusion_provider return get_local_diffusion_provider(max_pipelines=settings.local_gpu_max_pipelines) - except ImportError: + except (ImportError, AttributeError) as exc: + print(f"[local_gpu] Cannot load diffusion provider: {exc}") return None return None diff --git a/docker-compose.gpu.yml b/docker-compose.gpu.yml index ad583e9..02dd9ff 100644 --- a/docker-compose.gpu.yml +++ b/docker-compose.gpu.yml @@ -62,14 +62,23 @@ services: build: context: . dockerfile: Dockerfile.gpu + args: + # 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: ${BUILDID:-1} container_name: editmaskwithai-gpu ports: - "${PORT:-3080}:8000" volumes: # Persistent project data - ./data:/app/data - # HuggingFace model cache — keeps downloaded models across rebuilds (~5-20 GB) - - hf_model_cache:/root/.cache/huggingface + # HuggingFace model cache — bind mount so models can be pre-downloaded on the host. + # If container DNS is blocked, download on the host and the container picks them up: + # pip install huggingface-hub + # huggingface-cli download diffusers/stable-diffusion-xl-1.0-inpainting-0.1 \ + # --cache-dir ./data/hf_cache + # To free disk space: rm -rf ./data/hf_cache + - ./data/hf_cache:/root/.cache/huggingface # Scripts (for exec access) - ./scripts:/scripts environment: @@ -136,8 +145,3 @@ services: - 8.8.4.4 restart: unless-stopped - -volumes: - hf_model_cache: - # Survives docker compose down; delete manually to free disk space: - # docker volume rm editmaskwithai_hf_model_cache