From 59b26b6d5e59d233b41178a3b05df21c19a78f49 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 23:53:21 +0000 Subject: [PATCH] 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) {