From 36d1667eb09093ef13de300aeb64492a024f5301 Mon Sep 17 00:00:00 2001 From: Budi Hartono Date: Fri, 14 Aug 2026 17:29:40 +0700 Subject: [PATCH] Add model-filtered preset picker and per-model latent channel count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EmptyLatentAspectPreset gains a "model" widget; web/aspect_ratio_filter.js filters the "preset" combo to that model client-side, parsed from each preset's label so presets.py stays the single source of truth. - Fix latent channel count: SD1.5/SDXL use 4-channel latents, but Flux/HiDream/Krea/Qwen-Image use 16-channel latents (same family as ComfyUI's EmptySD3LatentImage) — was previously hardcoded to 4 for all models, breaking/corrupting generations on the newer models. - Drop Ideogram and ERNIE from presets.py: both are hosted API models with no local LATENT/diffusion pipeline in ComfyUI, so listing them implied false compatibility. --- .gitignore | 1 + __init__.py | 6 +++++- nodes.py | 25 ++++++++++++++++++++++-- presets.py | 19 ++---------------- web/aspect_ratio_filter.js | 40 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 web/aspect_ratio_filter.js diff --git a/.gitignore b/.gitignore index 0abaf46..8d1940e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node.zip # Claude Code / AI assistant local state .claude/ .serena/ +.tokensave/ CLAUDE.md CLAUDE.local.md diff --git a/__init__.py b/__init__.py index 84291c3..ba97acd 100644 --- a/__init__.py +++ b/__init__.py @@ -5,4 +5,8 @@ NODE_CLASS_MAPPINGS = { "CAS Empty Latent Aspect Ratio Axis": EmptyLatentAspectByAxis, } -NODE_DISPLAY_NAME = "latent" \ No newline at end of file +NODE_DISPLAY_NAME = "latent" + +WEB_DIRECTORY = "web" + +__all__ = ["NODE_CLASS_MAPPINGS", "WEB_DIRECTORY"] \ No newline at end of file diff --git a/nodes.py b/nodes.py index 207e49b..70b83bc 100644 --- a/nodes.py +++ b/nodes.py @@ -13,11 +13,31 @@ class EmptyLatentAspectPreset: f"{w}x{h} - {lbl} - {model}": (w, h) for model, lbl, w, h in PRESETS } + # Unique models in first-appearance order; the "model" widget below is purely a + # client-side filter (see web/aspect_ratio_filter.js) for the "preset" dropdown, + # which already encodes the model in its label — parsed there, not duplicated. + MODELS = list(dict.fromkeys(model for model, _, _, _ in PRESETS)) + + # Latent channel count per model family. SD1.5/SDXL use the legacy 4-channel VAE; + # Flux/HiDream/Qwen-Image/Krea use a 16-channel VAE (same family as ComfyUI's own + # EmptySD3LatentImage) — feeding a 4-channel latent into those samplers fails or + # silently produces garbage. + LATENT_CHANNELS = { + "SD15": 4, + "SDXL": 4, + "HiDream": 16, + "Flux.1": 16, + "Flux.2": 16, + "Krea": 16, + "Qwen-Image": 16, + } + DEFAULT_LATENT_CHANNELS = 4 @classmethod def INPUT_TYPES(cls): return { "required": { + "model": (cls.MODELS,), "preset": (list(cls.PRESET_MAP.keys()),), "batch_size": ("INT", {"default": 1, "min": 1}) } @@ -28,7 +48,7 @@ class EmptyLatentAspectPreset: FUNCTION = "generate" CATEGORY = "latent" # moved into ComfyUI's built-in "latent" category - def generate(self, preset: str, batch_size: int): + def generate(self, model: str, preset: str, batch_size: int): if preset not in self.PRESET_MAP: raise ValueError(f"Unknown preset: {preset}") w, h = self.PRESET_MAP[preset] @@ -36,7 +56,8 @@ class EmptyLatentAspectPreset: _validate_dim(w) _validate_dim(h) - latent = torch.zeros([batch_size, 4, h // 8, w // 8], dtype=torch.float32) + channels = self.LATENT_CHANNELS.get(model, self.DEFAULT_LATENT_CHANNELS) + latent = torch.zeros([batch_size, channels, h // 8, w // 8], dtype=torch.float32) return ({"samples": latent}, w, h) diff --git a/presets.py b/presets.py index 4ca0ed1..3824da3 100644 --- a/presets.py +++ b/presets.py @@ -67,21 +67,6 @@ PRESETS = [ ("Qwen-Image", "3:4 Portrait", 1136, 1472), ("Qwen-Image", "9:16 Portrait", 928, 1664), - # --- Ideogram 4.0 (broad native AR support incl. panoramic) --- - ("Ideogram", "1:1 Square", 1024, 1024), - ("Ideogram", "3:2 Landscape", 1216, 832), - ("Ideogram", "4:3 Landscape", 1152, 896), - ("Ideogram", "16:9 Landscape", 1344, 768), - ("Ideogram", "3:1 Landscape", 1728, 576), - ("Ideogram", "2:3 Portrait", 832, 1216), - ("Ideogram", "3:4 Portrait", 896, 1152), - ("Ideogram", "9:16 Portrait", 768, 1344), - ("Ideogram", "1:3 Portrait", 576, 1728), - - # --- ERNIE (Baidu ERNIE-ViLG / iRAG, native 1024) --- - ("Ernie", "1:1 Square", 1024, 1024), - ("Ernie", "4:3 Landscape", 1152, 896), - ("Ernie", "16:9 Landscape", 1344, 768), - ("Ernie", "3:4 Portrait", 896, 1152), - ("Ernie", "9:16 Portrait", 768, 1344), + # Ideogram 4.0 and ERNIE are hosted API models (no local LATENT/diffusion sampling + # in ComfyUI) — deliberately excluded so this node never implies false compatibility. ] diff --git a/web/aspect_ratio_filter.js b/web/aspect_ratio_filter.js new file mode 100644 index 0000000..7e668fe --- /dev/null +++ b/web/aspect_ratio_filter.js @@ -0,0 +1,40 @@ +import { app } from "../../scripts/app.js"; + +// Filters the "preset" combo of the "CAS Empty Latent Aspect Ratio Preset" node down to +// entries matching the selected "model" widget. The model is parsed from each preset's +// label ("WxH - label - model"), so presets.py stays the single source of truth. +app.registerExtension({ + name: "ComfyUI.AspectRatioPresets.ModelFilter", + beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name !== "CAS Empty Latent Aspect Ratio Preset") return; + + const allPresets = nodeData.input.required.preset[0]; + const presetsByModel = (model) => + allPresets.filter((p) => p.endsWith(` - ${model}`)); + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + onNodeCreated?.apply(this, arguments); + + const modelWidget = this.widgets.find((w) => w.name === "model"); + const presetWidget = this.widgets.find((w) => w.name === "preset"); + if (!modelWidget || !presetWidget) return; + + const applyFilter = () => { + const filtered = presetsByModel(modelWidget.value); + presetWidget.options.values = filtered; + if (!filtered.includes(presetWidget.value)) { + presetWidget.value = filtered[0]; + } + }; + + const origCallback = modelWidget.callback; + modelWidget.callback = (...args) => { + origCallback?.apply(modelWidget, args); + applyFilter(); + }; + + applyFilter(); + }; + }, +});