diff --git a/README.md b/README.md index f2795ac..591af34 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ The usual way to run a workflow over a folder of images is to swap the file in * - **Pick your filetype** — `png`, `jpg`, `jpeg`, `webp`, `bmp`, `gif`, `tiff`, or `all` for every supported type at once. - **Reverse** runs the sequence last→first. - **Live status line** shows exactly where you are: `Next: 3 / 240 · frame_003.png`. +- **Thumbnail previews** of the current and next image at the bottom of the node, so you can see what's coming before you Queue. ## Install @@ -60,6 +61,8 @@ Restart ComfyUI. No extra Python dependencies — it uses Pillow and torch, whic The status line shows `Next: N / TOTAL · filename`. Hit **⏮ Reset to start** any time to begin again from the first file. +At the bottom of the node, two thumbnails show what's coming: **Next** (the image that loads on your next Queue) and **Next + 1** (the one after it, looping back to the start at the end). + ## Controls | Control | What it does | diff --git a/memory/MEMORY.md b/memory/MEMORY.md new file mode 100644 index 0000000..c1db2e1 --- /dev/null +++ b/memory/MEMORY.md @@ -0,0 +1 @@ +- [Publishing ComfyUI nodes](publishing-comfyui-nodes.md) — two-target publish flow: Comfy Registry (comfy node publish) + legacy Manager PR (Comfy-Org/ComfyUI-Manager) diff --git a/memory/publishing-comfyui-nodes.md b/memory/publishing-comfyui-nodes.md new file mode 100644 index 0000000..5e00e10 --- /dev/null +++ b/memory/publishing-comfyui-nodes.md @@ -0,0 +1,16 @@ +--- +name: publishing-comfyui-nodes +description: How the user publishes ComfyUI custom nodes (Comfy Registry + legacy Manager) +metadata: + type: project +--- + +The user (shootthesound) publishes ComfyUI custom nodes to two places: + +1. **Comfy Registry** — needs a `pyproject.toml` with `[project]` (name = lowercase-hyphen id, version, license, description) and `[tool.comfy]` (`PublisherId = "shootthesound"`, `DisplayName`). Publish with `comfy node publish --token ` (comfy-cli installed, v1.10.3). Registry URL: `https://registry.comfy.org/nodes/`. + +2. **Legacy ComfyUI-Manager** — PR adding an entry to `custom-node-list.json` in **Comfy-Org/ComfyUI-Manager** (moved from ltdrdata). New entries go at the TOP of the `custom_nodes` array. The user already has a fork `shootthesound/ComfyUI-Manager` (default branch `main`); GitHub allows only one fork per repo, so reuse it — branch off upstream `main`, push branch to the fork, open PR against `Comfy-Org:main`. Entry shape: `author, title, reference, files[], install_type:"git-clone", description`. + +**Why:** The user asked to publish to "both the legacy manager and the comfy registry" — this is a recurring two-target workflow for them. + +**How to apply:** Never write the Comfy API key into any file; pass it inline to the publish command only. Advise rotating the key if it was pasted in chat. diff --git a/node.zip b/node.zip new file mode 100644 index 0000000..6954b01 Binary files /dev/null and b/node.zip differ diff --git a/sequential_loader.py b/sequential_loader.py index dcac65a..d90fb49 100644 --- a/sequential_loader.py +++ b/sequential_loader.py @@ -8,6 +8,7 @@ resolves index → file → tensors and exposes a listing route the UI uses to show progress. """ +import io import os import numpy as np @@ -250,8 +251,26 @@ try: return web.json_response({ "count": len(files), "files": [os.path.basename(p) for p in files], + "paths": files, }) + @PromptServer.instance.routes.get("/seqloader/thumb") + async def _seqloader_thumb(request): + path = (request.query.get("path", "") or "").strip() + if (not path or not os.path.isfile(path) + or not path.lower().endswith(_ALL_EXTS)): + return web.Response(status=404) + try: + img = Image.open(path) + img = ImageOps.exif_transpose(img) + img = img.convert("RGB") + img.thumbnail((256, 256)) + buf = io.BytesIO() + img.save(buf, format="JPEG", quality=85) + return web.Response(body=buf.getvalue(), content_type="image/jpeg") + except Exception as e: + return web.Response(status=500, text=str(e)) + @PromptServer.instance.routes.post("/seqloader/browse") async def _seqloader_browse(request): try: diff --git a/web/seqloader.js b/web/seqloader.js index 35b8698..4eaf41a 100644 --- a/web/seqloader.js +++ b/web/seqloader.js @@ -82,13 +82,67 @@ async function refreshFileList(node) { }); const data = await res.json(); node._seqFiles = Array.isArray(data.files) ? data.files : []; + node._seqPaths = Array.isArray(data.paths) ? data.paths : []; } catch (e) { node._seqFiles = []; + node._seqPaths = []; } updateStatus(node); } +// --- Thumbnail previews of the current + next image. Fetched from the +// server (the files live there) as blobs so base-path / auth are +// handled by api.fetchApi. Skips refetching when the path is unchanged. +async function setThumb(img, path) { + if (!img) return; + if (!path) { + img._path = ""; + img.removeAttribute("src"); + return; + } + if (img._path === path) return; + img._path = path; + try { + const res = await api.fetchApi( + "/seqloader/thumb?path=" + encodeURIComponent(path)); + if (!res.ok) { img.removeAttribute("src"); return; } + const blob = await res.blob(); + if (img._path !== path) return; // a newer request superseded this one + if (img._url) URL.revokeObjectURL(img._url); + img._url = URL.createObjectURL(blob); + img.src = img._url; + } catch (e) { + img.removeAttribute("src"); + } +} + +function updatePreviews(node) { + const paths = node._seqPaths || []; + const total = paths.length; + const cur = node._seqCurImg, nxt = node._seqNextImg; + if (total === 0) { + setThumb(cur, ""); + setThumb(nxt, ""); + if (node._seqCurLab) node._seqCurLab.textContent = "Next"; + if (node._seqNextLab) node._seqNextLab.textContent = "Next + 1"; + return; + } + const idx = findWidget(node, "index")?.value | 0; + const pos = ((idx % total) + total) % total; + const nextPos = (pos + 1) % total; + setThumb(cur, paths[pos]); + setThumb(nxt, paths[nextPos]); + const baseName = (p) => (p || "").split(/[\\/]/).pop(); + if (node._seqCurImg) node._seqCurImg.title = baseName(paths[pos]); + if (node._seqNextImg) node._seqNextImg.title = baseName(paths[nextPos]); + if (node._seqCurLab) node._seqCurLab.textContent = "Next"; + if (node._seqNextLab) { + node._seqNextLab.textContent = (total === 1) ? "Next + 1 (loops)" : "Next + 1"; + } +} + function updateStatus(node) { + updatePreviews(node); const el = node._seqStatusEl; if (!el) return; const files = node._seqFiles || []; @@ -166,14 +220,44 @@ function attachUI(node) { })); container.appendChild(row); + // Image previews of the current + next file, at the bottom. + const preview = document.createElement("div"); + preview.style.cssText = "display:flex; gap:6px; margin-top:2px;"; + const makeCell = (labelText) => { + const cell = document.createElement("div"); + cell.style.cssText = + "flex:1 1 0; min-width:0; display:flex; flex-direction:column; gap:3px;"; + const lab = document.createElement("div"); + lab.style.cssText = + "font-size:10px; color:#9cf; text-align:center; white-space:nowrap; " + + "overflow:hidden; text-overflow:ellipsis;"; + lab.textContent = labelText; + const img = document.createElement("img"); + img.style.cssText = + "width:100%; height:96px; object-fit:contain; background:#111; " + + "border:1px solid #3a3a3a; border-radius:4px;"; + cell.appendChild(lab); + cell.appendChild(img); + return { cell, lab, img }; + }; + const curCell = makeCell("Next"); + const nextCell = makeCell("Next + 1"); + preview.appendChild(curCell.cell); + preview.appendChild(nextCell.cell); + container.appendChild(preview); + node._seqCurImg = curCell.img; + node._seqNextImg = nextCell.img; + node._seqCurLab = curCell.lab; + node._seqNextLab = nextCell.lab; + // Attach as a DOM widget so LiteGraph manages its layout. The panel is - // three fixed rows (status + browse + button row) at ~28px each with two - // 6px gaps and 6px top+bottom padding — a constant floor of 112 fits the + // status + browse + button row (~28px each) plus a ~115px preview row + // (two 96px thumbnails with labels). A constant floor of 238 fits the // content without clipping or leaving a gap. const widget = node.addDOMWidget("seqloader_ui", "seqloader_panel", container, { serialize: false, hideOnZoom: false, - getMinHeight: () => 112, + getMinHeight: () => 238, }); // Make the panel fill the node's full width: define `width` as a getter @@ -398,7 +482,7 @@ app.registerExtension({ attachUI(this); // Enforce a sensible minimum node size once. Width ~320; height // accommodates the standard widgets plus the DOM panel. - const min = [320, 370]; + const min = [320, 500]; if (this.size[0] < min[0]) this.size[0] = min[0]; if (this.size[1] < min[1]) this.size[1] = min[1]; };