From 160b95f75c9cf60b04fbbf4ec0b8f35f474ffb2a Mon Sep 17 00:00:00 2001 From: iChrist Date: Wed, 6 May 2026 05:47:57 +0300 Subject: [PATCH 001/145] Update language options in nodes_ace.py (#12578) * Update language options in nodes_ace.py Modified it to include all 51 language options ace-step1.5 supports instead of the original 23 comfyui had. * re-arrange list by popularity changed order of the languages to be ordered by popularity en is default unknown is last * Update comfy_extras/nodes_ace.py --- comfy_extras/nodes_ace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy_extras/nodes_ace.py b/comfy_extras/nodes_ace.py index 1602add84..affcf3b71 100644 --- a/comfy_extras/nodes_ace.py +++ b/comfy_extras/nodes_ace.py @@ -42,7 +42,7 @@ class TextEncodeAceStepAudio15(IO.ComfyNode): IO.Int.Input("bpm", default=120, min=10, max=300), IO.Float.Input("duration", default=120.0, min=0.0, max=2000.0, step=0.1), IO.Combo.Input("timesignature", options=['2', '3', '4', '6']), - IO.Combo.Input("language", options=["en", "ja", "zh", "es", "de", "fr", "pt", "ru", "it", "nl", "pl", "tr", "vi", "cs", "fa", "id", "ko", "uk", "hu", "ar", "sv", "ro", "el"]), + IO.Combo.Input("language", options=['ar', 'az', 'bg', 'bn', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'fa', 'fi', 'fr', 'he', 'hi', 'hr', 'ht', 'hu', 'id', 'is', 'it', 'ja', 'ko', 'la', 'lt', 'ms', 'ne', 'nl', 'no', 'pa', 'pl', 'pt', 'ro', 'ru', 'sa', 'sk', 'sr', 'sv', 'sw', 'ta', 'te', 'th', 'tl', 'tr', 'uk', 'ur', 'vi', 'yue', 'zh', 'unknown'], default='en'), IO.Combo.Input("keyscale", options=[f"{root} {quality}" for quality in ["major", "minor"] for root in ["C", "C#", "Db", "D", "D#", "Eb", "E", "F", "F#", "Gb", "G", "G#", "Ab", "A", "A#", "Bb", "B"]]), IO.Boolean.Input("generate_audio_codes", default=True, tooltip="Enable the LLM that generates audio codes. This can be slow but will increase the quality of the generated audio. Turn this off if you are giving the model an audio reference.", advanced=True), IO.Float.Input("cfg_scale", default=2.0, min=0.0, max=100.0, step=0.1, advanced=True), From 2b63add0ad975d5f2f0cdc3d4fd8e71ae6553cbf Mon Sep 17 00:00:00 2001 From: Luke Mino-Altherr Date: Tue, 5 May 2026 19:56:09 -0700 Subject: [PATCH 002/145] fix: return millisecond timestamps from get_file_info() (#12996) --- app/user_manager.py | 4 ++-- tests-unit/prompt_server_test/user_manager_test.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/user_manager.py b/app/user_manager.py index e18afb71b..0517b3344 100644 --- a/app/user_manager.py +++ b/app/user_manager.py @@ -28,8 +28,8 @@ def get_file_info(path: str, relative_to: str) -> FileInfo: return { "path": os.path.relpath(path, relative_to).replace(os.sep, '/'), "size": os.path.getsize(path), - "modified": os.path.getmtime(path), - "created": os.path.getctime(path) + "modified": int(os.path.getmtime(path) * 1000), + "created": int(os.path.getctime(path) * 1000), } diff --git a/tests-unit/prompt_server_test/user_manager_test.py b/tests-unit/prompt_server_test/user_manager_test.py index b939d8e68..27118400f 100644 --- a/tests-unit/prompt_server_test/user_manager_test.py +++ b/tests-unit/prompt_server_test/user_manager_test.py @@ -69,7 +69,11 @@ async def test_listuserdata_full_info(aiohttp_client, app, tmp_path): assert len(result) == 1 assert result[0]["path"] == "file1.txt" assert "size" in result[0] - assert "modified" in result[0] + assert isinstance(result[0]["modified"], int) + assert isinstance(result[0]["created"], int) + # Verify millisecond magnitude (timestamps after year 2000 in ms are > 946684800000) + assert result[0]["modified"] > 946684800000 + assert result[0]["created"] > 946684800000 async def test_listuserdata_split_path(aiohttp_client, app, tmp_path): From 78b3096bf36ef32378a3d4299473b820211f1601 Mon Sep 17 00:00:00 2001 From: Talmaj Date: Wed, 6 May 2026 04:59:04 +0200 Subject: [PATCH 003/145] Void model - pass 1 & 2 (CORE-38) (#13403) --- comfy/latent_formats.py | 18 + comfy/sd.py | 5 + comfy/supported_models.py | 23 + comfy/text_encoders/cogvideo.py | 42 ++ comfy_extras/nodes_void.py | 483 +++++++++++++++++ comfy_extras/void_noise_warp.py | 494 ++++++++++++++++++ folder_paths.py | 2 + .../optical_flow/put_optical_flow_models_here | 0 nodes.py | 5 +- 9 files changed, 1070 insertions(+), 2 deletions(-) create mode 100644 comfy_extras/nodes_void.py create mode 100644 comfy_extras/void_noise_warp.py create mode 100644 models/optical_flow/put_optical_flow_models_here diff --git a/comfy/latent_formats.py b/comfy/latent_formats.py index 60c0dfd7e..91bebed3d 100644 --- a/comfy/latent_formats.py +++ b/comfy/latent_formats.py @@ -793,9 +793,27 @@ class ZImagePixelSpace(ChromaRadiance): pass class CogVideoX(LatentFormat): + """Latent format for CogVideoX-2b (THUDM/CogVideoX-2b). + + scale_factor matches the vae/config.json scaling_factor for the 2b variant. + The 5b-class checkpoints (CogVideoX-5b, CogVideoX-1.5-5B, CogVideoX-Fun-V1.5-*) + use a different value; see CogVideoX1_5 below. + """ latent_channels = 16 latent_dimensions = 3 temporal_downscale_ratio = 4 def __init__(self): self.scale_factor = 1.15258426 + + +class CogVideoX1_5(CogVideoX): + """Latent format for 5b-class CogVideoX checkpoints. + + Covers THUDM/CogVideoX-5b, THUDM/CogVideoX-1.5-5B, and the CogVideoX-Fun + V1.5-5b family (including VOID inpainting). All of these have + scaling_factor=0.7 in their vae/config.json. Auto-selected in + supported_models.CogVideoX_T2V based on transformer hidden dim. + """ + def __init__(self): + self.scale_factor = 0.7 diff --git a/comfy/sd.py b/comfy/sd.py index 9fce0e7d0..749bdd710 100644 --- a/comfy/sd.py +++ b/comfy/sd.py @@ -66,6 +66,7 @@ import comfy.text_encoders.longcat_image import comfy.text_encoders.qwen35 import comfy.text_encoders.ernie import comfy.text_encoders.gemma4 +import comfy.text_encoders.cogvideo import comfy.model_patcher import comfy.lora @@ -1224,6 +1225,7 @@ class CLIPType(Enum): NEWBIE = 24 FLUX2 = 25 LONGCAT_IMAGE = 26 + COGVIDEOX = 27 @@ -1428,6 +1430,9 @@ def load_text_encoder_state_dicts(state_dicts=[], embedding_directory=None, clip clip_target.clip = comfy.text_encoders.hidream.hidream_clip(**t5xxl_detect(clip_data), clip_l=False, clip_g=False, t5=True, llama=False, dtype_llama=None) clip_target.tokenizer = comfy.text_encoders.hidream.HiDreamTokenizer + elif clip_type == CLIPType.COGVIDEOX: + clip_target.clip = comfy.text_encoders.cogvideo.cogvideo_te(**t5xxl_detect(clip_data)) + clip_target.tokenizer = comfy.text_encoders.cogvideo.CogVideoXTokenizer else: #CLIPType.MOCHI clip_target.clip = comfy.text_encoders.genmo.mochi_te(**t5xxl_detect(clip_data)) clip_target.tokenizer = comfy.text_encoders.genmo.MochiT5Tokenizer diff --git a/comfy/supported_models.py b/comfy/supported_models.py index dff40461f..6a9613602 100644 --- a/comfy/supported_models.py +++ b/comfy/supported_models.py @@ -1872,6 +1872,14 @@ class CogVideoX_T2V(supported_models_base.BASE): vae_key_prefix = ["vae."] text_encoder_key_prefix = ["text_encoders."] + def __init__(self, unet_config): + # 2b-class (dim=1920, heads=30) uses scale_factor=1.15258426. + # 5b-class (dim=3072, heads=48) — incl. CogVideoX-5b, 1.5-5B, and + # Fun-V1.5 inpainting — uses scale_factor=0.7 per vae/config.json. + if unet_config.get("num_attention_heads", 0) >= 48: + self.latent_format = latent_formats.CogVideoX1_5 + super().__init__(unet_config) + def get_model(self, state_dict, prefix="", device=None): # CogVideoX 1.5 (patch_size_t=2) has different training base dimensions for RoPE if self.unet_config.get("patch_size_t") is not None: @@ -1898,6 +1906,20 @@ class CogVideoX_I2V(CogVideoX_T2V): out = model_base.CogVideoX(self, image_to_video=True, device=device) return out +class CogVideoX_Inpaint(CogVideoX_T2V): + unet_config = { + "image_model": "cogvideox", + "in_channels": 48, + } + + def get_model(self, state_dict, prefix="", device=None): + if self.unet_config.get("patch_size_t") is not None: + self.unet_config.setdefault("sample_height", 96) + self.unet_config.setdefault("sample_width", 170) + self.unet_config.setdefault("sample_frames", 81) + out = model_base.CogVideoX(self, image_to_video=True, device=device) + return out + models = [ LotusD, @@ -1978,6 +2000,7 @@ models = [ ErnieImage, SAM3, SAM31, + CogVideoX_Inpaint, CogVideoX_I2V, CogVideoX_T2V, SVD_img2vid, diff --git a/comfy/text_encoders/cogvideo.py b/comfy/text_encoders/cogvideo.py index f1e8e3f5d..b97310709 100644 --- a/comfy/text_encoders/cogvideo.py +++ b/comfy/text_encoders/cogvideo.py @@ -1,6 +1,48 @@ import comfy.text_encoders.sd3_clip +from comfy import sd1_clip class CogVideoXT5Tokenizer(comfy.text_encoders.sd3_clip.T5XXLTokenizer): + """Inner T5 tokenizer for CogVideoX. + + CogVideoX was trained with T5 embeddings padded to 226 tokens (not 77 like SD3). + Used both directly by supported_models.CogVideoX_T2V.clip_target (paired with + the raw T5XXLModel) and by the CogVideoXTokenizer outer wrapper below. + """ def __init__(self, embedding_directory=None, tokenizer_data={}): super().__init__(embedding_directory=embedding_directory, tokenizer_data=tokenizer_data, min_length=226) + + +class CogVideoXTokenizer(sd1_clip.SD1Tokenizer): + """Outer tokenizer wrapper for CLIPLoader (type="cogvideox").""" + def __init__(self, embedding_directory=None, tokenizer_data={}): + super().__init__(embedding_directory=embedding_directory, tokenizer_data=tokenizer_data, + clip_name="t5xxl", tokenizer=CogVideoXT5Tokenizer) + + +class CogVideoXT5XXL(sd1_clip.SD1ClipModel): + """Outer T5XXL model wrapper for CLIPLoader (type="cogvideox"). + + Wraps the raw T5XXL model in the SD1ClipModel interface so that CLIP.__init__ + (which reads self.dtypes) works correctly. The inner model is the standard + sd3_clip.T5XXLModel (no attention_mask change needed for CogVideoX). + """ + def __init__(self, device="cpu", dtype=None, model_options={}): + super().__init__(device=device, dtype=dtype, name="t5xxl", + clip_model=comfy.text_encoders.sd3_clip.T5XXLModel, + model_options=model_options) + + +def cogvideo_te(dtype_t5=None, t5_quantization_metadata=None): + """Factory that returns a CogVideoXT5XXL class configured with the detected + T5 dtype and optional quantization metadata, for use in load_text_encoder_state_dicts. + """ + class CogVideoXTEModel_(CogVideoXT5XXL): + def __init__(self, device="cpu", dtype=None, model_options={}): + if t5_quantization_metadata is not None: + model_options = model_options.copy() + model_options["t5xxl_quantization_metadata"] = t5_quantization_metadata + if dtype_t5 is not None: + dtype = dtype_t5 + super().__init__(device=device, dtype=dtype, model_options=model_options) + return CogVideoXTEModel_ diff --git a/comfy_extras/nodes_void.py b/comfy_extras/nodes_void.py new file mode 100644 index 000000000..e7a8f3757 --- /dev/null +++ b/comfy_extras/nodes_void.py @@ -0,0 +1,483 @@ +import logging + +import torch + +import comfy +import comfy.model_management +import comfy.model_patcher +import comfy.samplers +import comfy.utils +import folder_paths +import node_helpers +import nodes +from comfy.utils import model_trange as trange +from comfy_api.latest import ComfyExtension, io +from torchvision.models.optical_flow import raft_large +from typing_extensions import override + + +from comfy_extras.void_noise_warp import RaftOpticalFlow, get_noise_from_video + +OpticalFlow = io.Custom("OPTICAL_FLOW") + +TEMPORAL_COMPRESSION = 4 +PATCH_SIZE_T = 2 + + +def _valid_void_length(length: int) -> int: + """Round ``length`` down to a value that produces an even latent_t. + + VOID / CogVideoX-Fun-V1.5 uses patch_size_t=2, so the VAE-encoded latent + must have an even temporal dimension. If latent_t is odd, the transformer + pad_to_patch_size circular-wraps an extra latent frame onto the end; after + the post-transformer crop the last real latent frame has been influenced + by the wrapped phantom frame, producing visible jitter and "disappearing" + subjects near the end of the decoded video. Rounding down fixes this. + """ + latent_t = ((length - 1) // TEMPORAL_COMPRESSION) + 1 + if latent_t % PATCH_SIZE_T == 0: + return length + # Round latent_t down to the nearest multiple of PATCH_SIZE_T, then invert + # the ((length - 1) // TEMPORAL_COMPRESSION) + 1 formula. Floor at 1 frame + # so we never return a non-positive length. + target_latent_t = max(PATCH_SIZE_T, (latent_t // PATCH_SIZE_T) * PATCH_SIZE_T) + return (target_latent_t - 1) * TEMPORAL_COMPRESSION + 1 + + +class OpticalFlowLoader(io.ComfyNode): + """Load an optical flow model from ``models/optical_flow/``. + + Only torchvision's RAFT-large format is recognized today (the model used + by VOIDWarpedNoise). The checkpoint must be placed under + ``models/optical_flow/`` — ComfyUI never downloads optical-flow weights + at runtime. + """ + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="OpticalFlowLoader", + display_name="Load Optical Flow Model", + category="loaders", + inputs=[ + io.Combo.Input( + "model_name", + options=folder_paths.get_filename_list("optical_flow"), + tooltip=( + "Optical flow model to load. Files must be placed in the " + "'optical_flow' folder. Today only torchvision's " + "raft_large.pth is supported." + ), + ), + ], + outputs=[ + OpticalFlow.Output(), + ], + ) + + @classmethod + def execute(cls, model_name) -> io.NodeOutput: + + model_path = folder_paths.get_full_path_or_raise("optical_flow", model_name) + sd = comfy.utils.load_torch_file(model_path, safe_load=True) + + has_raft_keys = ( + any(k.startswith("feature_encoder.") for k in sd) + and any(k.startswith("context_encoder.") for k in sd) + and any(k.startswith("update_block.") for k in sd) + ) + if not has_raft_keys: + raise ValueError( + "Unrecognized optical flow model format: expected a torchvision " + "RAFT-large state dict with 'feature_encoder.', 'context_encoder.' " + "and 'update_block.' prefixes." + ) + + model = raft_large(weights=None, progress=False) + model.load_state_dict(sd) + model.eval().to(torch.float32) + + patcher = comfy.model_patcher.ModelPatcher( + model, + load_device=comfy.model_management.get_torch_device(), + offload_device=comfy.model_management.unet_offload_device(), + ) + return io.NodeOutput(patcher) + + +class VOIDQuadmaskPreprocess(io.ComfyNode): + """Preprocess a quadmask video for VOID inpainting. + + Quantizes mask values to four semantic levels, inverts, and normalizes: + 0 -> primary object to remove + 63 -> overlap of primary + affected + 127 -> affected region (interactions) + 255 -> background (keep) + + After inversion and normalization, the output mask has values in [0, 1] + with four discrete levels: 1.0 (remove), ~0.75, ~0.50, 0.0 (keep). + """ + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="VOIDQuadmaskPreprocess", + category="mask/video", + inputs=[ + io.Mask.Input("mask"), + io.Int.Input("dilate_width", default=0, min=0, max=50, step=1, + tooltip="Dilation radius for the primary mask region (0 = no dilation)"), + ], + outputs=[ + io.Mask.Output(display_name="quadmask"), + ], + ) + + @classmethod + def execute(cls, mask, dilate_width=0) -> io.NodeOutput: + m = mask.clone() + + if m.max() <= 1.0: + m = m * 255.0 + + if dilate_width > 0 and m.ndim >= 3: + binary = (m < 128).float() + kernel_size = dilate_width * 2 + 1 + if binary.ndim == 3: + binary = binary.unsqueeze(1) + dilated = torch.nn.functional.max_pool2d( + binary, kernel_size=kernel_size, stride=1, padding=dilate_width + ) + if dilated.ndim == 4: + dilated = dilated.squeeze(1) + m = torch.where(dilated > 0.5, torch.zeros_like(m), m) + + m = torch.where(m <= 31, torch.zeros_like(m), m) + m = torch.where((m > 31) & (m <= 95), torch.full_like(m, 63), m) + m = torch.where((m > 95) & (m <= 191), torch.full_like(m, 127), m) + m = torch.where(m > 191, torch.full_like(m, 255), m) + + m = (255.0 - m) / 255.0 + + return io.NodeOutput(m) + + +class VOIDInpaintConditioning(io.ComfyNode): + """Build VOID inpainting conditioning for CogVideoX. + + Encodes the processed quadmask and masked source video through the VAE, + producing a 32-channel concat conditioning (16ch mask + 16ch masked video) + that gets concatenated with the 16ch noise latent by the model. + """ + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="VOIDInpaintConditioning", + category="conditioning/video_models", + inputs=[ + io.Conditioning.Input("positive"), + io.Conditioning.Input("negative"), + io.Vae.Input("vae"), + io.Image.Input("video", tooltip="Source video frames [T, H, W, 3]"), + io.Mask.Input("quadmask", tooltip="Preprocessed quadmask from VOIDQuadmaskPreprocess [T, H, W]"), + io.Int.Input("width", default=672, min=16, max=nodes.MAX_RESOLUTION, step=8), + io.Int.Input("height", default=384, min=16, max=nodes.MAX_RESOLUTION, step=8), + io.Int.Input("length", default=45, min=1, max=nodes.MAX_RESOLUTION, step=1, + tooltip="Number of pixel frames to process. For CogVideoX-Fun-V1.5 " + "(patch_size_t=2), latent_t must be even — lengths that " + "produce odd latent_t are rounded down (e.g. 49 → 45)."), + io.Int.Input("batch_size", default=1, min=1, max=64), + ], + outputs=[ + io.Conditioning.Output(display_name="positive"), + io.Conditioning.Output(display_name="negative"), + io.Latent.Output(display_name="latent"), + ], + ) + + @classmethod + def execute(cls, positive, negative, vae, video, quadmask, + width, height, length, batch_size) -> io.NodeOutput: + + adjusted_length = _valid_void_length(length) + if adjusted_length != length: + logging.warning( + "VOIDInpaintConditioning: rounding length %d down to %d so that " + "latent_t is even (required by CogVideoX-Fun-V1.5 patch_size_t=2). " + "Using odd latent_t causes the last frame to be corrupted by " + "circular padding.", length, adjusted_length, + ) + length = adjusted_length + + latent_t = ((length - 1) // TEMPORAL_COMPRESSION) + 1 + latent_h = height // 8 + latent_w = width // 8 + + vid = video[:length] + vid = comfy.utils.common_upscale( + vid.movedim(-1, 1), width, height, "bilinear", "center" + ).movedim(1, -1) + + qm = quadmask[:length] + if qm.ndim == 3: + qm = qm.unsqueeze(-1) + qm = comfy.utils.common_upscale( + qm.movedim(-1, 1), width, height, "bilinear", "center" + ).movedim(1, -1) + if qm.ndim == 4 and qm.shape[-1] == 1: + qm = qm.squeeze(-1) + + mask_condition = qm + if mask_condition.ndim == 3: + mask_condition_3ch = mask_condition.unsqueeze(-1).expand(-1, -1, -1, 3) + else: + mask_condition_3ch = mask_condition + + inverted_mask_3ch = 1.0 - mask_condition_3ch + masked_video = vid[:, :, :, :3] * (1.0 - mask_condition_3ch) + + mask_latents = vae.encode(inverted_mask_3ch) + masked_video_latents = vae.encode(masked_video) + + def _match_temporal(lat, target_t): + if lat.shape[2] > target_t: + return lat[:, :, :target_t] + elif lat.shape[2] < target_t: + pad = target_t - lat.shape[2] + return torch.cat([lat, lat[:, :, -1:].repeat(1, 1, pad, 1, 1)], dim=2) + return lat + + mask_latents = _match_temporal(mask_latents, latent_t) + masked_video_latents = _match_temporal(masked_video_latents, latent_t) + + inpaint_latents = torch.cat([mask_latents, masked_video_latents], dim=1) + + # No explicit scaling needed here: the model's CogVideoX.concat_cond() + # applies process_latent_in (×latent_format.scale_factor) to each 16-ch + # block of the stored conditioning. For 5b-class checkpoints (incl. the + # VOID/CogVideoX-Fun-V1.5 inpainting model) that scale_factor is auto- + # selected as 0.7 in supported_models.CogVideoX_T2V, which matches the + # diffusers vae/config.json scaling_factor VOID was trained with. + + positive = node_helpers.conditioning_set_values( + positive, {"concat_latent_image": inpaint_latents} + ) + negative = node_helpers.conditioning_set_values( + negative, {"concat_latent_image": inpaint_latents} + ) + + noise_latent = torch.zeros( + [batch_size, 16, latent_t, latent_h, latent_w], + device=comfy.model_management.intermediate_device() + ) + + return io.NodeOutput(positive, negative, {"samples": noise_latent}) + + +class VOIDWarpedNoise(io.ComfyNode): + """Generate optical-flow warped noise for VOID Pass 2 refinement. + + Takes the Pass 1 output video and produces temporally-correlated noise + by warping Gaussian noise along optical flow vectors. This noise is used + as the initial latent for Pass 2, resulting in better temporal consistency. + """ + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="VOIDWarpedNoise", + category="latent/video", + inputs=[ + OpticalFlow.Input( + "optical_flow", + tooltip="Optical flow model from OpticalFlowLoader (RAFT-large).", + ), + io.Image.Input("video", tooltip="Pass 1 output video frames [T, H, W, 3]"), + io.Int.Input("width", default=672, min=16, max=nodes.MAX_RESOLUTION, step=8), + io.Int.Input("height", default=384, min=16, max=nodes.MAX_RESOLUTION, step=8), + io.Int.Input("length", default=45, min=1, max=nodes.MAX_RESOLUTION, step=1, + tooltip="Number of pixel frames. Rounded down to make latent_t " + "even (patch_size_t=2 requirement), e.g. 49 → 45."), + io.Int.Input("batch_size", default=1, min=1, max=64), + ], + outputs=[ + io.Latent.Output(display_name="warped_noise"), + ], + ) + + @classmethod + def execute(cls, optical_flow, video, width, height, length, batch_size) -> io.NodeOutput: + + adjusted_length = _valid_void_length(length) + if adjusted_length != length: + logging.warning( + "VOIDWarpedNoise: rounding length %d down to %d so that " + "latent_t is even (required by CogVideoX-Fun-V1.5 patch_size_t=2).", + length, adjusted_length, + ) + length = adjusted_length + + latent_t = ((length - 1) // TEMPORAL_COMPRESSION) + 1 + latent_h = height // 8 + latent_w = width // 8 + + # RAFT + noise warp is real compute, not an "intermediate" buffer, so + # we want the actual torch device (CUDA/MPS). The final latent is + # moved back to intermediate_device() before returning to match the + # rest of the ComfyUI pipeline. + device = comfy.model_management.get_torch_device() + + comfy.model_management.load_model_gpu(optical_flow) + raft = RaftOpticalFlow(optical_flow.model, device=device) + + vid = video[:length].to(device) + vid = comfy.utils.common_upscale( + vid.movedim(-1, 1), width, height, "bilinear", "center" + ).movedim(1, -1) + vid_uint8 = (vid.clamp(0, 1) * 255).to(torch.uint8) + + FRAME = 2**-1 + FLOW = 2**3 + LATENT_SCALE = 8 + + warped = get_noise_from_video( + vid_uint8, + raft, + noise_channels=16, + resize_frames=FRAME, + resize_flow=FLOW, + downscale_factor=round(FRAME * FLOW) * LATENT_SCALE, + device=device, + ) + + if warped.shape[0] != latent_t: + indices = torch.linspace(0, warped.shape[0] - 1, latent_t, + device=device).long() + warped = warped[indices] + + if warped.shape[1] != latent_h or warped.shape[2] != latent_w: + # (T, H, W, C) → (T, C, H, W) → bilinear resize → back + warped = warped.permute(0, 3, 1, 2) + warped = torch.nn.functional.interpolate( + warped, size=(latent_h, latent_w), + mode="bilinear", align_corners=False, + ) + warped = warped.permute(0, 2, 3, 1) + + # (T, H, W, C) → (B, C, T, H, W) + warped_tensor = warped.permute(3, 0, 1, 2).unsqueeze(0) + if batch_size > 1: + warped_tensor = warped_tensor.repeat(batch_size, 1, 1, 1, 1) + + warped_tensor = warped_tensor.to(comfy.model_management.intermediate_device()) + return io.NodeOutput({"samples": warped_tensor}) + + +class Noise_FromLatent: + """Wraps a pre-computed LATENT tensor as a NOISE source.""" + def __init__(self, latent_dict): + self.seed = 0 + self._samples = latent_dict["samples"] + + def generate_noise(self, input_latent): + return self._samples.clone().cpu() + + +class VOIDWarpedNoiseSource(io.ComfyNode): + """Convert a LATENT (e.g. from VOIDWarpedNoise) into a NOISE source + for use with SamplerCustomAdvanced.""" + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="VOIDWarpedNoiseSource", + category="sampling/custom_sampling/noise", + inputs=[ + io.Latent.Input("warped_noise", + tooltip="Warped noise latent from VOIDWarpedNoise"), + ], + outputs=[io.Noise.Output()], + ) + + @classmethod + def execute(cls, warped_noise) -> io.NodeOutput: + return io.NodeOutput(Noise_FromLatent(warped_noise)) + + +class VOID_DDIM(comfy.samplers.Sampler): + """DDIM sampler for VOID inpainting models. + + VOID was trained with the diffusers CogVideoXDDIMScheduler which operates in + alpha-space (input std ≈ 1). The standard KSampler applies noise_scaling that + multiplies by sqrt(1+sigma^2) ≈ 4500x, which is incompatible with VOID's + training. This sampler skips noise_scaling and implements the DDIM update rule + directly using sigma-to-alpha conversion. + """ + + def sample(self, model_wrap, sigmas, extra_args, callback, noise, latent_image=None, denoise_mask=None, disable_pbar=False): + x = noise.to(torch.float32) + model_options = extra_args.get("model_options", {}) + seed = extra_args.get("seed", None) + s_in = x.new_ones([x.shape[0]]) + + for i in trange(len(sigmas) - 1, disable=disable_pbar): + sigma = sigmas[i] + sigma_next = sigmas[i + 1] + + denoised = model_wrap(x, sigma * s_in, model_options=model_options, seed=seed) + + if callback is not None: + callback(i, denoised, x, len(sigmas) - 1) + + if sigma_next == 0: + x = denoised + else: + alpha_t = 1.0 / (1.0 + sigma ** 2) + alpha_prev = 1.0 / (1.0 + sigma_next ** 2) + + pred_eps = (x - (alpha_t ** 0.5) * denoised) / (1.0 - alpha_t) ** 0.5 + x = (alpha_prev ** 0.5) * denoised + (1.0 - alpha_prev) ** 0.5 * pred_eps + + return x + + +class VOIDSampler(io.ComfyNode): + """VOID DDIM sampler for use with SamplerCustom / SamplerCustomAdvanced. + + Required for VOID inpainting models. Implements the same DDIM loop that VOID + was trained with (diffusers CogVideoXDDIMScheduler), without the noise_scaling + that the standard KSampler applies. Use with RandomNoise or VOIDWarpedNoiseSource. + """ + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="VOIDSampler", + category="sampling/custom_sampling/samplers", + inputs=[], + outputs=[io.Sampler.Output()], + ) + + @classmethod + def execute(cls) -> io.NodeOutput: + return io.NodeOutput(VOID_DDIM()) + + get_sampler = execute + + +class VOIDExtension(ComfyExtension): + @override + async def get_node_list(self) -> list[type[io.ComfyNode]]: + return [ + OpticalFlowLoader, + VOIDQuadmaskPreprocess, + VOIDInpaintConditioning, + VOIDWarpedNoise, + VOIDWarpedNoiseSource, + VOIDSampler, + ] + + +async def comfy_entrypoint() -> VOIDExtension: + return VOIDExtension() diff --git a/comfy_extras/void_noise_warp.py b/comfy_extras/void_noise_warp.py new file mode 100644 index 000000000..fcc9a5f8b --- /dev/null +++ b/comfy_extras/void_noise_warp.py @@ -0,0 +1,494 @@ +""" +Optical-flow-warped noise for VOID Pass 2 refinement. + +Adapted from RyannDaGreat/CommonSource (MIT License, Ryan Burgert): + https://github.com/RyannDaGreat/CommonSource + - noise_warp.py (NoiseWarper / warp_xyωc / regaussianize / get_noise_from_video) + - raft.py (RaftOpticalFlow) + +Only the code paths that ``comfy_extras/nodes_void.py::VOIDWarpedNoise`` actually +uses (torch THWC uint8 input, no background removal, no visualization, no disk +I/O, default warp/noise params) have been inlined. External ``rp`` utilities +have been replaced with equivalents from torch.nn.functional / einops. The +RAFT optical-flow model itself is loaded offline via ``OpticalFlowLoader`` in +``nodes_void.py`` and passed into ``get_noise_from_video`` by the caller; this +module never downloads weights at runtime. +""" + +import logging +from typing import Optional + +import torch +import torch.nn.functional as F +from einops import rearrange + +import comfy.model_management + + +# --------------------------------------------------------------------------- +# Low-level torch image helpers (drop-in replacements for rp.torch_* primitives) +# --------------------------------------------------------------------------- + +def _torch_resize_chw(image, size, interp, copy=True): + """Resize a CHW tensor. + + ``size`` is either a scalar factor or a (h, w) tuple. ``interp`` is one + of ``"bilinear"``, ``"nearest"``, ``"area"``. When ``copy`` is False and + the requested size matches the input, returns the input tensor as is + (faster but callers must not mutate the result). + """ + if image.ndim != 3: + raise ValueError( + f"_torch_resize_chw expects a 3D CHW tensor, got shape {tuple(image.shape)}" + ) + _, in_h, in_w = image.shape + if isinstance(size, (int, float)) and not isinstance(size, bool): + new_h = max(1, int(in_h * size)) + new_w = max(1, int(in_w * size)) + else: + new_h, new_w = size + + if (new_h, new_w) == (in_h, in_w): + return image.clone() if copy else image + + kwargs = {} + if interp in ("bilinear", "bicubic"): + kwargs["align_corners"] = False + out = F.interpolate(image[None], size=(new_h, new_w), mode=interp, **kwargs)[0] + return out + + +def _torch_remap_relative(image, dx, dy, interp="bilinear"): + """Relative remap of a CHW image via ``F.grid_sample``. + + Equivalent to ``rp.torch_remap_image(image, dx, dy, relative=True, interp=interp)`` + for ``interp`` in {"bilinear", "nearest"}. Out-of-bounds samples are 0. + """ + if image.ndim != 3: + raise ValueError( + f"_torch_remap_relative expects a 3D CHW tensor, got shape {tuple(image.shape)}" + ) + if dx.shape != dy.shape: + raise ValueError( + f"_torch_remap_relative: dx and dy must match, got {tuple(dx.shape)} vs {tuple(dy.shape)}" + ) + _, h, w = image.shape + + x_abs = dx + torch.arange(w, device=dx.device, dtype=dx.dtype) + y_abs = dy + torch.arange(h, device=dy.device, dtype=dy.dtype)[:, None] + + x_norm = (x_abs / (w - 1)) * 2 - 1 + y_norm = (y_abs / (h - 1)) * 2 - 1 + + grid = torch.stack([x_norm, y_norm], dim=-1)[None].to(image.dtype) + out = F.grid_sample( + image[None], grid, mode=interp, align_corners=True, padding_mode="zeros" + )[0] + return out + + +def _torch_scatter_add_relative(image, dx, dy): + """Scatter-add a CHW image using relative floor-rounded (dx, dy) offsets. + + Equivalent to ``rp.torch_scatter_add_image(image, dx, dy, relative=True, + interp='floor')``. Out-of-bounds targets are dropped. + """ + if image.ndim != 3: + raise ValueError( + f"_torch_scatter_add_relative expects a 3D CHW tensor, got shape {tuple(image.shape)}" + ) + in_c, in_h, in_w = image.shape + if dx.shape != (in_h, in_w) or dy.shape != (in_h, in_w): + raise ValueError( + f"_torch_scatter_add_relative: dx/dy must be ({in_h}, {in_w}), " + f"got dx={tuple(dx.shape)} dy={tuple(dy.shape)}" + ) + + x = dx.long() + torch.arange(in_w, device=dx.device, dtype=torch.long) + y = dy.long() + torch.arange(in_h, device=dy.device, dtype=torch.long)[:, None] + + valid = ((y >= 0) & (y < in_h) & (x >= 0) & (x < in_w)).reshape(-1) + indices = (y * in_w + x).reshape(-1)[valid] + + flat_image = rearrange(image, "c h w -> (h w) c")[valid] + out = torch.zeros((in_h * in_w, in_c), dtype=image.dtype, device=image.device) + out.index_add_(0, indices, flat_image) + return rearrange(out, "(h w) c -> c h w", h=in_h, w=in_w) + + +# --------------------------------------------------------------------------- +# Noise warping primitives (ported from noise_warp.py) +# --------------------------------------------------------------------------- + +def unique_pixels(image): + """Find unique pixel values in a CHW tensor. + + Returns ``(unique_colors [U, C], counts [U], index_matrix [H, W])`` where + ``index_matrix[i, j]`` is the index of the unique color at that pixel. + """ + _, h, w = image.shape + flat = rearrange(image, "c h w -> (h w) c") + unique_colors, inverse_indices, counts = torch.unique( + flat, dim=0, return_inverse=True, return_counts=True, sorted=False, + ) + index_matrix = rearrange(inverse_indices, "(h w) -> h w", h=h, w=w) + return unique_colors, counts, index_matrix + + +def sum_indexed_values(image, index_matrix): + """For each unique index, sum the CHW image values at its pixels.""" + _, h, w = image.shape + u = int(index_matrix.max().item()) + 1 + flat = rearrange(image, "c h w -> (h w) c") + out = torch.zeros((u, flat.shape[1]), dtype=flat.dtype, device=flat.device) + out.index_add_(0, index_matrix.view(-1), flat) + return out + + +def indexed_to_image(index_matrix, unique_colors): + """Build a CHW image from an index matrix and a (U, C) color table.""" + h, w = index_matrix.shape + flat = unique_colors[index_matrix.view(-1)] + return rearrange(flat, "(h w) c -> c h w", h=h, w=w) + + +def regaussianize(noise): + """Variance-preserving re-sampling of a CHW noise tensor. + + Wherever the noise contains groups of identical pixel values (e.g. after + a nearest-neighbor warp that duplicated source pixels), adds zero-mean + foreign noise within each group and scales by ``1/sqrt(count)`` so the + output is unit-variance gaussian again. + """ + _, hs, ws = noise.shape + _, counts, index_matrix = unique_pixels(noise[:1]) + + foreign_noise = torch.randn_like(noise) + summed = sum_indexed_values(foreign_noise, index_matrix) + meaned = indexed_to_image(index_matrix, summed / rearrange(counts, "u -> u 1")) + zeroed_foreign = foreign_noise - meaned + + counts_image = indexed_to_image(index_matrix, rearrange(counts, "u -> u 1")) + + output = noise / counts_image ** 0.5 + zeroed_foreign + return output, counts_image + + +def xy_meshgrid_like_image(image): + """Return a (2, H, W) tensor of (x, y) pixel coordinates matching ``image``.""" + _, h, w = image.shape + y, x = torch.meshgrid( + torch.arange(h, device=image.device, dtype=image.dtype), + torch.arange(w, device=image.device, dtype=image.dtype), + indexing="ij", + ) + return torch.stack([x, y]) + + +def noise_to_state(noise): + """Pack a (C, H, W) noise tensor into a state tensor (3+C, H, W) = [dx, dy, ω, noise].""" + zeros = torch.zeros_like(noise[:1]) + ones = torch.ones_like(noise[:1]) + return torch.cat([zeros, zeros, ones, noise]) + + +def state_to_noise(state): + """Unpack the noise channels from a state tensor.""" + return state[3:] + + +def warp_state(state, flow): + """Warp a noise-warper state tensor along the given optical flow. + + ``state`` has shape ``(3+c, h, w)`` (= dx, dy, ω, c noise channels). + ``flow`` has shape ``(2, h, w)`` (= dx, dy). + """ + if flow.device != state.device: + raise ValueError( + f"warp_state: flow and state must be on the same device, " + f"got flow={flow.device} state={state.device}" + ) + if state.ndim != 3: + raise ValueError( + f"warp_state: state must be 3D (3+C, H, W), got shape {tuple(state.shape)}" + ) + xyoc, h, w = state.shape + if flow.shape != (2, h, w): + raise ValueError( + f"warp_state: flow must have shape (2, {h}, {w}), got {tuple(flow.shape)}" + ) + device = state.device + + x_ch, y_ch = 0, 1 + xy = 2 # state[:xy] = [dx, dy] + xyw = 3 # state[:xyw] = [dx, dy, ω] + w_ch = 2 # state[w_ch] = ω + c = xyoc - xyw + oc = xyoc - xy + if c <= 0: + raise ValueError( + f"warp_state: state has no noise channels (expected 3+C with C>0, got {xyoc} channels)" + ) + if not (state[w_ch] > 0).all(): + raise ValueError("warp_state: all weights in state[2] must be > 0") + + grid = xy_meshgrid_like_image(state) + + init = torch.empty_like(state) + init[:xy] = 0 + init[w_ch] = 1 + init[-c:] = 0 + + # --- Expansion branch: nearest-neighbor remap with negated flow --- + pre_expand = torch.empty_like(state) + pre_expand[:xy] = _torch_remap_relative(state[:xy], -flow[0], -flow[1], "nearest") + pre_expand[-oc:] = _torch_remap_relative(state[-oc:], -flow[0], -flow[1], "nearest") + pre_expand[w_ch][pre_expand[w_ch] == 0] = 1 + + # --- Shrink branch: scatter-add state into new positions --- + pre_shrink = state.clone() + pre_shrink[:xy] += flow + + pos = (grid + pre_shrink[:xy]).round() + in_bounds = (pos[x_ch] >= 0) & (pos[x_ch] < w) & (pos[y_ch] >= 0) & (pos[y_ch] < h) + pre_shrink = torch.where(~in_bounds[None], init, pre_shrink) + + scat_xy = pre_shrink[:xy].round() + pre_shrink[:xy] -= scat_xy + pre_shrink[:xy] = 0 # xy_mode='none' in upstream + + def scat(tensor): + return _torch_scatter_add_relative(tensor, scat_xy[0], scat_xy[1]) + + # rp.torch_scatter_add_image on a bool tensor errors on modern torch; + # scatter-sum a float ones tensor and threshold to get the mask instead. + shrink_mask = scat(torch.ones(1, h, w, dtype=state.dtype, device=device)) > 0 + + # Drop expansion samples at positions that will be filled by shrink. + pre_expand = torch.where(shrink_mask, init, pre_expand) + + # Regaussianize both branches together so duplicated-source groups are + # counted globally, then split back apart. + concat = torch.cat([pre_shrink, pre_expand], dim=2) # along width + concat[-c:], counts_image = regaussianize(concat[-c:]) + concat[w_ch] = concat[w_ch] / counts_image[0] + concat[w_ch] = concat[w_ch].nan_to_num() + pre_shrink, expand = torch.chunk(concat, chunks=2, dim=2) + + shrink = torch.empty_like(pre_shrink) + shrink[w_ch] = scat(pre_shrink[w_ch][None])[0] + shrink[:xy] = scat(pre_shrink[:xy] * pre_shrink[w_ch][None]) / shrink[w_ch][None] + shrink[-c:] = scat(pre_shrink[-c:] * pre_shrink[w_ch][None]) / scat( + pre_shrink[w_ch][None] ** 2 + ).sqrt() + + output = torch.where(shrink_mask, shrink, expand) + output[w_ch] = output[w_ch] / output[w_ch].mean() + output[w_ch] += 1e-5 + output[w_ch] **= 0.9999 + return output + + +class NoiseWarper: + """Maintain a warpable noise state and emit gaussian noise per frame. + + Simplified from RyannDaGreat/CommonSource/noise_warp.py::NoiseWarper: + ``scale_factor``, ``post_noise_alpha``, ``progressive_noise_alpha``, and + ``warp_kwargs`` are all dropped since VOIDWarpedNoise always uses defaults. + """ + + def __init__(self, c, h, w, device, dtype=torch.float32): + if c <= 0 or h <= 0 or w <= 0: + raise ValueError( + f"NoiseWarper: c/h/w must all be positive, got c={c} h={h} w={w}" + ) + self.c = c + self.h = h + self.w = w + self.device = device + self.dtype = dtype + + noise = torch.randn(c, h, w, dtype=dtype, device=device) + self._state = noise_to_state(noise) + + @property + def noise(self): + # With scale_factor=1 the "downsample to respect weights" step is a + # size-preserving no-op; the weight-variance correction math still + # runs to stay faithful to upstream. + n = state_to_noise(self._state) + weights = self._state[2:3] + return n * weights / (weights ** 2).sqrt() + + def __call__(self, dx, dy): + if dx.shape != dy.shape: + raise ValueError( + f"NoiseWarper: dx and dy must match, got {tuple(dx.shape)} vs {tuple(dy.shape)}" + ) + flow = torch.stack([dx, dy]).to(self.device, self.dtype) + _, oflowh, ofloww = flow.shape + + flow = _torch_resize_chw(flow, (self.h, self.w), "bilinear", copy=True) + flowh, floww = flow.shape[-2:] + + # Upstream scales flow[0] by flowh/oflowh and flow[1] by floww/ofloww + # (channel-order appears swapped but harmless when H and W are scaled + # by the same factor, which is always the case for our callers). + flow[0] *= flowh / oflowh + flow[1] *= floww / ofloww + + self._state = warp_state(self._state, flow) + return self + + +# --------------------------------------------------------------------------- +# RAFT optical flow wrapper (ported from raft.py) +# --------------------------------------------------------------------------- + +class RaftOpticalFlow: + """RAFT-large wrapper around a pre-loaded torchvision model. + + ``model`` must be the ``torchvision.models.optical_flow.raft_large`` module + with its weights already populated; this class is load-agnostic so the + caller owns downloading/offload concerns (see ``OpticalFlowLoader`` in + ``nodes_void.py``). ``__call__`` returns a ``(2, H, W)`` flow. + """ + + def __init__(self, model, device=None): + if device is None: + device = comfy.model_management.get_torch_device() + device = torch.device(device) if not isinstance(device, torch.device) else device + + model = model.to(device) + model.eval() + self.device = device + self.model = model + + def _preprocess(self, image_chw): + image = image_chw.to(self.device, torch.float32) + _, h, w = image.shape + new_h = (h // 8) * 8 + new_w = (w // 8) * 8 + image = _torch_resize_chw(image, (new_h, new_w), "bilinear", copy=False) + image = image * 2 - 1 + return image[None] + + def __call__(self, from_image, to_image): + """``from_image``, ``to_image``: CHW float tensors in [0, 1].""" + if from_image.shape != to_image.shape: + raise ValueError( + f"RaftOpticalFlow: from_image and to_image must match, " + f"got {tuple(from_image.shape)} vs {tuple(to_image.shape)}" + ) + _, h, w = from_image.shape + with torch.no_grad(): + img1 = self._preprocess(from_image) + img2 = self._preprocess(to_image) + list_of_flows = self.model(img1, img2) + flow = list_of_flows[-1][0] # (2, new_h, new_w) + if flow.shape[-2:] != (h, w): + flow = _torch_resize_chw(flow, (h, w), "bilinear", copy=False) + return flow + + +# --------------------------------------------------------------------------- +# Narrow entry point used by VOIDWarpedNoise +# --------------------------------------------------------------------------- + +def get_noise_from_video( + video_frames: torch.Tensor, + raft: RaftOpticalFlow, + *, + noise_channels: int = 16, + resize_frames: float = 0.5, + resize_flow: int = 8, + downscale_factor: int = 32, + device: Optional[torch.device] = None, +) -> torch.Tensor: + """Produce optical-flow-warped gaussian noise from a video. + + Args: + video_frames: ``(T, H, W, 3)`` uint8 torch tensor. + raft: Pre-loaded RAFT optical-flow wrapper (see ``RaftOpticalFlow``). + noise_channels: Channels in the output noise. + resize_frames: Pre-RAFT frame scale factor. + resize_flow: Post-flow up-scale factor applied to the optical flow; + the internal noise state is allocated at + ``(resize_flow * resize_frames * H, resize_flow * resize_frames * W)``. + downscale_factor: Area-pool factor applied to the noise before return; + should evenly divide the internal noise resolution. + device: Target device. Defaults to ``comfy.model_management.get_torch_device()``. + + Returns: + ``(T, H', W', noise_channels)`` float32 noise tensor on ``device``. + """ + if not isinstance(resize_flow, int) or resize_flow < 1: + raise ValueError( + f"get_noise_from_video: resize_flow must be a positive int, got {resize_flow!r}" + ) + if video_frames.ndim != 4 or video_frames.shape[-1] != 3: + raise ValueError( + "get_noise_from_video: video_frames must have shape (T, H, W, 3), " + f"got {tuple(video_frames.shape)}" + ) + if video_frames.dtype != torch.uint8: + raise TypeError( + "get_noise_from_video: video_frames must be uint8 in [0, 255], " + f"got dtype {video_frames.dtype}" + ) + + if device is None: + device = comfy.model_management.get_torch_device() + device = torch.device(device) if not isinstance(device, torch.device) else device + + if device.type == "cpu": + logging.warning( + "VOIDWarpedNoise: running get_noise_from_video on CPU; this will be " + "slow (minutes for ~45 frames). Use CUDA for interactive use." + ) + + T = video_frames.shape[0] + frames = video_frames.to(device).permute(0, 3, 1, 2).to(torch.float32) / 255.0 + if resize_frames != 1.0: + new_h = max(1, int(frames.shape[2] * resize_frames)) + new_w = max(1, int(frames.shape[3] * resize_frames)) + frames = F.interpolate(frames, size=(new_h, new_w), mode="area") + + _, _, H, W = frames.shape + internal_h = resize_flow * H + internal_w = resize_flow * W + if internal_h % downscale_factor or internal_w % downscale_factor: + logging.warning( + "VOIDWarpedNoise: internal noise size %dx%d is not divisible by " + "downscale_factor %d; output noise may have artifacts.", + internal_h, internal_w, downscale_factor, + ) + + with torch.no_grad(): + warper = NoiseWarper( + c=noise_channels, h=internal_h, w=internal_w, device=device, + ) + down_h = warper.h // downscale_factor + down_w = warper.w // downscale_factor + output = torch.empty( + (T, down_h, down_w, noise_channels), dtype=torch.float32, device=device, + ) + + def downscale(noise_chw): + # Area-pool to 1/downscale_factor then multiply by downscale_factor + # to adjust std (sqrt of pool area == downscale_factor for a + # square pool). + down = _torch_resize_chw(noise_chw, 1.0 / downscale_factor, "area", copy=False) + return down * downscale_factor + + output[0] = downscale(warper.noise).permute(1, 2, 0) + + prev = frames[0] + for i in range(1, T): + curr = frames[i] + flow = raft(prev, curr).to(device) + warper(flow[0], flow[1]) + output[i] = downscale(warper.noise).permute(1, 2, 0) + prev = curr + + return output diff --git a/folder_paths.py b/folder_paths.py index 039f72636..98d3b1880 100644 --- a/folder_paths.py +++ b/folder_paths.py @@ -54,6 +54,8 @@ folder_names_and_paths["audio_encoders"] = ([os.path.join(models_dir, "audio_enc folder_names_and_paths["frame_interpolation"] = ([os.path.join(models_dir, "frame_interpolation")], supported_pt_extensions) +folder_names_and_paths["optical_flow"] = ([os.path.join(models_dir, "optical_flow")], supported_pt_extensions) + output_directory = os.path.join(base_path, "output") temp_directory = os.path.join(base_path, "temp") input_directory = os.path.join(base_path, "input") diff --git a/models/optical_flow/put_optical_flow_models_here b/models/optical_flow/put_optical_flow_models_here new file mode 100644 index 000000000..e69de29bb diff --git a/nodes.py b/nodes.py index cf61d9df0..ad0cbc675 100644 --- a/nodes.py +++ b/nodes.py @@ -958,7 +958,7 @@ class CLIPLoader: @classmethod def INPUT_TYPES(s): return {"required": { "clip_name": (folder_paths.get_filename_list("text_encoders"), ), - "type": (["stable_diffusion", "stable_cascade", "sd3", "stable_audio", "mochi", "ltxv", "pixart", "cosmos", "lumina2", "wan", "hidream", "chroma", "ace", "omnigen2", "qwen_image", "hunyuan_image", "flux2", "ovis", "longcat_image"], ), + "type": (["stable_diffusion", "stable_cascade", "sd3", "stable_audio", "mochi", "ltxv", "pixart", "cosmos", "lumina2", "wan", "hidream", "chroma", "ace", "omnigen2", "qwen_image", "hunyuan_image", "flux2", "ovis", "longcat_image", "cogvideox"], ), }, "optional": { "device": (["default", "cpu"], {"advanced": True}), @@ -968,7 +968,7 @@ class CLIPLoader: CATEGORY = "advanced/loaders" - DESCRIPTION = "[Recipes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 xxl/ clip-g / clip-l\nstable_audio: t5 base\nmochi: t5 xxl\ncosmos: old t5 xxl\nlumina2: gemma 2 2B\nwan: umt5 xxl\n hidream: llama-3.1 (Recommend) or t5\nomnigen2: qwen vl 2.5 3B" + DESCRIPTION = "[Recipes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 xxl/ clip-g / clip-l\nstable_audio: t5 base\nmochi: t5 xxl\ncogvideox: t5 xxl (226-token padding)\ncosmos: old t5 xxl\nlumina2: gemma 2 2B\nwan: umt5 xxl\n hidream: llama-3.1 (Recommend) or t5\nomnigen2: qwen vl 2.5 3B" def load_clip(self, clip_name, type="stable_diffusion", device="default"): clip_type = getattr(comfy.sd.CLIPType, type.upper(), comfy.sd.CLIPType.STABLE_DIFFUSION) @@ -2430,6 +2430,7 @@ async def init_builtin_extra_nodes(): "nodes_rtdetr.py", "nodes_frame_interpolation.py", "nodes_sam3.py", + "nodes_void.py", ] import_failed = [] From 9c34f5f36a3815af7d21d8b42b0a5776b7406685 Mon Sep 17 00:00:00 2001 From: Comfy Org PR Bot Date: Wed, 6 May 2026 14:22:48 +0900 Subject: [PATCH 004/145] Bump comfyui-frontend-package to 1.43.17 (#13723) Co-authored-by: github-actions[bot] Co-authored-by: Alexander Brown --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e9415f2fd..e7aa92c31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -comfyui-frontend-package==1.42.15 +comfyui-frontend-package==1.43.17 comfyui-workflow-templates==0.9.69 comfyui-embedded-docs==0.4.4 torch From 6bcd8b96ab4650db6e834dcbb54357ebf72edfe6 Mon Sep 17 00:00:00 2001 From: guill Date: Wed, 6 May 2026 10:08:35 -0700 Subject: [PATCH 005/145] Revert "Fix Content-Disposition header missing 'attachment;' prefix (#13093)" (#13733) This reverts commit ea6880b04b88629b9dd07774298bdffea6923f9b. --- server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server.py b/server.py index 0e85635d3..2f3b438bb 100644 --- a/server.py +++ b/server.py @@ -560,7 +560,7 @@ class PromptServer(): buffer.seek(0) return web.Response(body=buffer.read(), content_type=f'image/{image_format}', - headers={"Content-Disposition": f"attachment; filename=\"{filename}\""}) + headers={"Content-Disposition": f"filename=\"{filename}\""}) if 'channel' not in request.rel_url.query: channel = 'rgba' @@ -580,7 +580,7 @@ class PromptServer(): buffer.seek(0) return web.Response(body=buffer.read(), content_type='image/png', - headers={"Content-Disposition": f"attachment; filename=\"{filename}\""}) + headers={"Content-Disposition": f"filename=\"{filename}\""}) elif channel == 'a': with Image.open(file) as img: @@ -597,7 +597,7 @@ class PromptServer(): alpha_buffer.seek(0) return web.Response(body=alpha_buffer.read(), content_type='image/png', - headers={"Content-Disposition": f"attachment; filename=\"{filename}\""}) + headers={"Content-Disposition": f"filename=\"{filename}\""}) else: # Use the content type from asset resolution if available, # otherwise guess from the filename. @@ -614,7 +614,7 @@ class PromptServer(): return web.FileResponse( file, headers={ - "Content-Disposition": f"attachment; filename=\"{filename}\"", + "Content-Disposition": f"filename=\"{filename}\"", "Content-Type": content_type } ) From cd8c7a2306be98bf93cd6632384a675afe750a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Sepp=C3=A4nen?= <40791699+kijai@users.noreply.github.com> Date: Thu, 7 May 2026 05:41:13 +0300 Subject: [PATCH 006/145] Throttle dynamic VRAM prepare logging (#13704) --- comfy/model_patcher.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/comfy/model_patcher.py b/comfy/model_patcher.py index 7d2d6883f..33bdedfb1 100644 --- a/comfy/model_patcher.py +++ b/comfy/model_patcher.py @@ -26,6 +26,7 @@ import uuid from typing import Callable, Optional import torch +import tqdm import comfy.float import comfy.hooks @@ -1651,7 +1652,11 @@ class ModelPatcherDynamic(ModelPatcher): self.model.model_loaded_weight_memory += casted_buf.numel() * casted_buf.element_size() force_load_stat = f" Force pre-loaded {len(self.backup)} weights: {self.model.model_loaded_weight_memory // 1024} KB." if len(self.backup) > 0 else "" - logging.info(f"Model {self.model.__class__.__name__} prepared for dynamic VRAM loading. {allocated_size // (1024 ** 2)}MB Staged. {num_patches} patches attached.{force_load_stat}") + log_key = (self.patches_uuid, allocated_size, num_patches, len(self.backup), self.model.model_loaded_weight_memory) + in_loop = bool(getattr(tqdm.tqdm, "_instances", None)) + level = logging.DEBUG if in_loop and getattr(self, "_last_prepare_log_key", None) == log_key else logging.INFO + self._last_prepare_log_key = log_key + logging.log(level, f"Model {self.model.__class__.__name__} prepared for dynamic VRAM loading. {allocated_size // (1024 ** 2)}MB Staged. {num_patches} patches attached.{force_load_stat}") self.model.device = device_to self.model.current_weight_patches_uuid = self.patches_uuid From e35348aa53563cabdcd9e5f67d0cb77b5259c903 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Wed, 6 May 2026 19:51:01 -0700 Subject: [PATCH 007/145] Add .comfy_environment to portable. (#13746) --- .github/workflows/stable-release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/stable-release.yml b/.github/workflows/stable-release.yml index f501b7b31..bc64ed74d 100644 --- a/.github/workflows/stable-release.yml +++ b/.github/workflows/stable-release.yml @@ -145,6 +145,8 @@ jobs: cp -r ComfyUI/.ci/windows_${{ inputs.rel_name }}_base_files/* ./ cp ../update_comfyui_and_python_dependencies.bat ./update/ + echo 'local-portable' > ComfyUI/.comfy_environment + cd .. "C:\Program Files\7-Zip\7z.exe" a -t7z -m0=lzma2 -mx=9 -mfb=128 -md=768m -ms=on -mf=BCJ2 ComfyUI_windows_portable.7z ComfyUI_windows_portable From 1b25f1289e6f48081b727083425791876ed0f39b Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Thu, 7 May 2026 09:45:59 +0300 Subject: [PATCH 008/145] [Partner Nodes] add grok-imagine-image-quality model (#13725) * feat(api-nodes): add grok-imagine-image-quality model Signed-off-by: bigcat88 * fixed price badges Signed-off-by: bigcat88 * fix: adjust price badges Signed-off-by: bigcat88 --------- Signed-off-by: bigcat88 Co-authored-by: Jedrzej Kosinski --- comfy_api_nodes/nodes_grok.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/comfy_api_nodes/nodes_grok.py b/comfy_api_nodes/nodes_grok.py index f42d84616..dd5d7e249 100644 --- a/comfy_api_nodes/nodes_grok.py +++ b/comfy_api_nodes/nodes_grok.py @@ -54,7 +54,12 @@ class GrokImageNode(IO.ComfyNode): inputs=[ IO.Combo.Input( "model", - options=["grok-imagine-image-pro", "grok-imagine-image", "grok-imagine-image-beta"], + options=[ + "grok-imagine-image-quality", + "grok-imagine-image-pro", + "grok-imagine-image", + "grok-imagine-image-beta", + ], ), IO.String.Input( "prompt", @@ -111,10 +116,12 @@ class GrokImageNode(IO.ComfyNode): ], is_api_node=True, price_badge=IO.PriceBadge( - depends_on=IO.PriceBadgeDepends(widgets=["model", "number_of_images"]), + depends_on=IO.PriceBadgeDepends(widgets=["model", "number_of_images", "resolution"]), expr=""" ( - $rate := $contains(widgets.model, "pro") ? 0.07 : 0.02; + $rate := widgets.model = "grok-imagine-image-quality" + ? (widgets.resolution = "1k" ? 0.05 : 0.07) + : ($contains(widgets.model, "pro") ? 0.07 : 0.02); {"type":"usd","usd": $rate * widgets.number_of_images} ) """, @@ -167,7 +174,12 @@ class GrokImageEditNode(IO.ComfyNode): inputs=[ IO.Combo.Input( "model", - options=["grok-imagine-image-pro", "grok-imagine-image", "grok-imagine-image-beta"], + options=[ + "grok-imagine-image-quality", + "grok-imagine-image-pro", + "grok-imagine-image", + "grok-imagine-image-beta", + ], ), IO.Image.Input("image", display_name="images"), IO.String.Input( @@ -228,11 +240,19 @@ class GrokImageEditNode(IO.ComfyNode): ], is_api_node=True, price_badge=IO.PriceBadge( - depends_on=IO.PriceBadgeDepends(widgets=["model", "number_of_images"]), + depends_on=IO.PriceBadgeDepends(widgets=["model", "number_of_images", "resolution"]), expr=""" ( - $rate := $contains(widgets.model, "pro") ? 0.07 : 0.02; - {"type":"usd","usd": 0.002 + $rate * widgets.number_of_images} + $isQualityModel := widgets.model = "grok-imagine-image-quality"; + $isPro := $contains(widgets.model, "pro"); + $rate := $isQualityModel + ? (widgets.resolution = "1k" ? 0.05 : 0.07) + : ($isPro ? 0.07 : 0.02); + $base := $isQualityModel ? 0.01 : 0.002; + $output := $rate * widgets.number_of_images; + $isPro + ? {"type":"usd","usd": $base + $output} + : {"type":"range_usd","min_usd": $base + $output, "max_usd": 3 * $base + $output} ) """, ), From 25757a53c93281e8e2462ced8795373f09e675bf Mon Sep 17 00:00:00 2001 From: "Daxiong (Lin)" Date: Thu, 7 May 2026 16:28:18 +0900 Subject: [PATCH 009/145] chore: update workflow templates to v0.9.72 (#13732) Co-authored-by: Jedrzej Kosinski --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e7aa92c31..5c7ff76be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ comfyui-frontend-package==1.43.17 -comfyui-workflow-templates==0.9.69 +comfyui-workflow-templates==0.9.72 comfyui-embedded-docs==0.4.4 torch torchsde From c945a433ae09423f7a2a6e9631538e55b9375f78 Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Thu, 7 May 2026 21:55:09 +0300 Subject: [PATCH 010/145] fix(api-nodes): fixed price badge for Kling V3 model in the Motion Control node (#13790) Signed-off-by: bigcat88 --- comfy_api_nodes/nodes_kling.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/comfy_api_nodes/nodes_kling.py b/comfy_api_nodes/nodes_kling.py index efd58fac3..7586f1816 100644 --- a/comfy_api_nodes/nodes_kling.py +++ b/comfy_api_nodes/nodes_kling.py @@ -2787,11 +2787,15 @@ class MotionControl(IO.ComfyNode): ], is_api_node=True, price_badge=IO.PriceBadge( - depends_on=IO.PriceBadgeDepends(widgets=["mode"]), + depends_on=IO.PriceBadgeDepends(widgets=["mode", "model"]), expr=""" ( - $prices := {"std": 0.07, "pro": 0.112}; - {"type":"usd","usd": $lookup($prices, widgets.mode), "format":{"suffix":"/second"}} + $prices := { + "kling-v3": {"std": 0.126, "pro": 0.168}, + "kling-v2-6": {"std": 0.07, "pro": 0.112} + }; + $modelPrices := $lookup($prices, widgets.model); + {"type":"usd","usd": $lookup($modelPrices, widgets.mode), "format":{"suffix":"/second"}} ) """, ), From c011fb520c79b9dfbe7f885d613771774f746eef Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Thu, 7 May 2026 22:19:44 +0300 Subject: [PATCH 011/145] [Partner Nodes] new NanoBanana2 node with DynamicCombo/Autogrow (#13753) * feat(api-nodes): new NanoBanana2 node with DynamicCombo/Autogrow Signed-off-by: bigcat88 * feat: improved status text on uploading Signed-off-by: bigcat88 * feat: improved status text on uploading (2) Signed-off-by: bigcat88 --------- Signed-off-by: bigcat88 --- comfy_api_nodes/nodes_gemini.py | 242 +++++++++++++++++++++++++++++--- 1 file changed, 222 insertions(+), 20 deletions(-) diff --git a/comfy_api_nodes/nodes_gemini.py b/comfy_api_nodes/nodes_gemini.py index 2b77a022e..d18c958a8 100644 --- a/comfy_api_nodes/nodes_gemini.py +++ b/comfy_api_nodes/nodes_gemini.py @@ -83,13 +83,16 @@ class GeminiImageModel(str, Enum): async def create_image_parts( cls: type[IO.ComfyNode], - images: Input.Image, + images: Input.Image | list[Input.Image], image_limit: int = 0, ) -> list[GeminiPart]: image_parts: list[GeminiPart] = [] if image_limit < 0: raise ValueError("image_limit must be greater than or equal to 0 when creating Gemini image parts.") - total_images = get_number_of_images(images) + + # Accept either a single (possibly-batched) tensor or a list of them; share URL budget across all. + images_list: list[Input.Image] = images if isinstance(images, list) else [images] + total_images = sum(get_number_of_images(img) for img in images_list) if total_images <= 0: raise ValueError("No images provided to create_image_parts; at least one image is required.") @@ -98,10 +101,18 @@ async def create_image_parts( # Number of images we'll send as URLs (fileData) num_url_images = min(effective_max, 10) # Vertex API max number of image links + upload_kwargs: dict = {"wait_label": "Uploading reference images"} + if effective_max > num_url_images: + # Split path (e.g. 11+ images): suppress per-image counter to avoid a confusing dual-fraction label. + upload_kwargs = { + "wait_label": f"Uploading reference images ({num_url_images}+)", + "show_batch_index": False, + } reference_images_urls = await upload_images_to_comfyapi( cls, - images, + images_list, max_images=num_url_images, + **upload_kwargs, ) for reference_image_url in reference_images_urls: image_parts.append( @@ -112,15 +123,22 @@ async def create_image_parts( ) ) ) - for idx in range(num_url_images, effective_max): - image_parts.append( - GeminiPart( - inlineData=GeminiInlineData( - mimeType=GeminiMimeType.image_png, - data=tensor_to_base64_string(images[idx]), + if effective_max > num_url_images: + flat: list[torch.Tensor] = [] + for tensor in images_list: + if len(tensor.shape) == 4: + flat.extend(tensor[i] for i in range(tensor.shape[0])) + else: + flat.append(tensor) + for idx in range(num_url_images, effective_max): + image_parts.append( + GeminiPart( + inlineData=GeminiInlineData( + mimeType=GeminiMimeType.image_png, + data=tensor_to_base64_string(flat[idx]), + ) ) ) - ) return image_parts @@ -891,10 +909,6 @@ class GeminiNanoBanana2(IO.ComfyNode): "9:16", "16:9", "21:9", - # "1:4", - # "4:1", - # "8:1", - # "1:8", ], default="auto", tooltip="If set to 'auto', matches your input image's aspect ratio; " @@ -902,12 +916,7 @@ class GeminiNanoBanana2(IO.ComfyNode): ), IO.Combo.Input( "resolution", - options=[ - # "512px", - "1K", - "2K", - "4K", - ], + options=["1K", "2K", "4K"], tooltip="Target output resolution. For 2K/4K the native Gemini upscaler is used.", ), IO.Combo.Input( @@ -956,6 +965,7 @@ class GeminiNanoBanana2(IO.ComfyNode): ], is_api_node=True, price_badge=GEMINI_IMAGE_2_PRICE_BADGE, + is_deprecated=True, ) @classmethod @@ -1016,6 +1026,197 @@ class GeminiNanoBanana2(IO.ComfyNode): ) +def _nano_banana_2_v2_model_inputs(): + return [ + IO.Combo.Input( + "aspect_ratio", + options=[ + "auto", + "1:1", + "2:3", + "3:2", + "3:4", + "4:3", + "4:5", + "5:4", + "9:16", + "16:9", + "21:9", + "1:4", + "4:1", + "8:1", + "1:8", + ], + default="auto", + tooltip="If set to 'auto', matches your input image's aspect ratio; " + "if no image is provided, a 16:9 square is usually generated.", + ), + IO.Combo.Input( + "resolution", + options=["1K", "2K", "4K"], + tooltip="Target output resolution. For 2K/4K the native Gemini upscaler is used.", + ), + IO.Combo.Input( + "thinking_level", + options=["MINIMAL", "HIGH"], + ), + IO.Autogrow.Input( + "images", + template=IO.Autogrow.TemplateNames( + IO.Image.Input("image"), + names=[f"image_{i}" for i in range(1, 15)], + min=0, + ), + tooltip="Optional reference image(s). Up to 14 images total.", + ), + IO.Custom("GEMINI_INPUT_FILES").Input( + "files", + optional=True, + tooltip="Optional file(s) to use as context for the model. " + "Accepts inputs from the Gemini Generate Content Input Files node.", + ), + ] + + +class GeminiNanoBanana2V2(IO.ComfyNode): + + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="GeminiNanoBanana2V2", + display_name="Nano Banana 2", + category="api node/image/Gemini", + description="Generate or edit images synchronously via Google Vertex API.", + inputs=[ + IO.String.Input( + "prompt", + multiline=True, + tooltip="Text prompt describing the image to generate or the edits to apply. " + "Include any constraints, styles, or details the model should follow.", + default="", + ), + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option( + "Nano Banana 2 (Gemini 3.1 Flash Image)", + _nano_banana_2_v2_model_inputs(), + ), + ], + ), + IO.Int.Input( + "seed", + default=42, + min=0, + max=0xFFFFFFFFFFFFFFFF, + control_after_generate=True, + tooltip="When the seed is fixed to a specific value, the model makes a best effort to provide " + "the same response for repeated requests. Deterministic output isn't guaranteed. " + "Also, changing the model or parameter settings, such as the temperature, " + "can cause variations in the response even when you use the same seed value. " + "By default, a random seed value is used.", + ), + IO.Combo.Input( + "response_modalities", + options=["IMAGE", "IMAGE+TEXT"], + advanced=True, + ), + IO.String.Input( + "system_prompt", + multiline=True, + default=GEMINI_IMAGE_SYS_PROMPT, + optional=True, + tooltip="Foundational instructions that dictate an AI's behavior.", + advanced=True, + ), + ], + outputs=[ + IO.Image.Output(), + IO.String.Output(), + IO.Image.Output( + display_name="thought_image", + tooltip="First image from the model's thinking process. " + "Only available with thinking_level HIGH and IMAGE+TEXT modality.", + ), + ], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends(widgets=["model", "model.resolution"]), + expr=""" + ( + $r := $lookup(widgets, "model.resolution"); + $prices := {"1k": 0.0696, "2k": 0.1014, "4k": 0.154}; + {"type":"usd","usd": $lookup($prices, $r), "format":{"suffix":"/Image","approximate":true}} + ) + """, + ), + ) + + @classmethod + async def execute( + cls, + prompt: str, + model: dict, + seed: int, + response_modalities: str, + system_prompt: str = "", + ) -> IO.NodeOutput: + validate_string(prompt, strip_whitespace=True, min_length=1) + model_choice = model["model"] + if model_choice == "Nano Banana 2 (Gemini 3.1 Flash Image)": + model_id = "gemini-3.1-flash-image-preview" + else: + model_id = model_choice + + images = model.get("images") or {} + parts: list[GeminiPart] = [GeminiPart(text=prompt)] + if images: + image_tensors: list[Input.Image] = [t for t in images.values() if t is not None] + if image_tensors: + if sum(get_number_of_images(t) for t in image_tensors) > 14: + raise ValueError("The current maximum number of supported images is 14.") + parts.extend(await create_image_parts(cls, image_tensors)) + files = model.get("files") + if files is not None: + parts.extend(files) + + image_config = GeminiImageConfig(imageSize=model["resolution"]) + if model["aspect_ratio"] != "auto": + image_config.aspectRatio = model["aspect_ratio"] + + gemini_system_prompt = None + if system_prompt: + gemini_system_prompt = GeminiSystemInstructionContent(parts=[GeminiTextPart(text=system_prompt)], role=None) + + response = await sync_op( + cls, + ApiEndpoint(path=f"/proxy/vertexai/gemini/{model_id}", method="POST"), + data=GeminiImageGenerateContentRequest( + contents=[ + GeminiContent(role=GeminiRole.user, parts=parts), + ], + generationConfig=GeminiImageGenerationConfig( + responseModalities=(["IMAGE"] if response_modalities == "IMAGE" else ["TEXT", "IMAGE"]), + imageConfig=image_config, + thinkingConfig=GeminiThinkingConfig(thinkingLevel=model["thinking_level"]), + ), + systemInstruction=gemini_system_prompt, + ), + response_model=GeminiGenerateContentResponse, + price_extractor=calculate_tokens_price, + ) + return IO.NodeOutput( + await get_image_from_response(response), + get_text_from_response(response), + await get_image_from_response(response, thought=True), + ) + + class GeminiExtension(ComfyExtension): @override async def get_node_list(self) -> list[type[IO.ComfyNode]]: @@ -1024,6 +1225,7 @@ class GeminiExtension(ComfyExtension): GeminiImage, GeminiImage2, GeminiNanoBanana2, + GeminiNanoBanana2V2, GeminiInputFiles, ] From 8dc3f3f2094121c0a013e21d89136ebc331d2974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Sepp=C3=A4nen?= <40791699+kijai@users.noreply.github.com> Date: Fri, 8 May 2026 03:18:28 +0300 Subject: [PATCH 012/145] Improve SAM3 large input handling (#13767) --- comfy/ldm/sam3/detector.py | 9 ++++--- comfy/ldm/sam3/tracker.py | 49 +++++++++++++++++++++++++------------- comfy_extras/nodes_sam3.py | 24 +++++++++++-------- 3 files changed, 53 insertions(+), 29 deletions(-) diff --git a/comfy/ldm/sam3/detector.py b/comfy/ldm/sam3/detector.py index 12d3a01ab..23a972ac7 100644 --- a/comfy/ldm/sam3/detector.py +++ b/comfy/ldm/sam3/detector.py @@ -561,7 +561,8 @@ class SAM3Model(nn.Module): return high_res_masks def forward_video(self, images, initial_masks, pbar=None, text_prompts=None, - new_det_thresh=0.5, max_objects=0, detect_interval=1): + new_det_thresh=0.5, max_objects=0, detect_interval=1, + target_device=None, target_dtype=None): """Track video with optional per-frame text-prompted detection.""" bb = self.detector.backbone["vision_backbone"] @@ -589,8 +590,10 @@ class SAM3Model(nn.Module): return self.tracker.track_video_with_detection( backbone_fn, images, initial_masks, detect_fn, new_det_thresh=new_det_thresh, max_objects=max_objects, - detect_interval=detect_interval, backbone_obj=bb, pbar=pbar) + detect_interval=detect_interval, backbone_obj=bb, pbar=pbar, + target_device=target_device, target_dtype=target_dtype) # SAM3 (non-multiplex) — no detection support, requires initial masks if initial_masks is None: raise ValueError("SAM3 (non-multiplex) requires initial_mask for video tracking") - return self.tracker.track_video(backbone_fn, images, initial_masks, pbar=pbar, backbone_obj=bb) + return self.tracker.track_video(backbone_fn, images, initial_masks, pbar=pbar, backbone_obj=bb, + target_device=target_device, target_dtype=target_dtype) diff --git a/comfy/ldm/sam3/tracker.py b/comfy/ldm/sam3/tracker.py index 8f7481003..8456e90a6 100644 --- a/comfy/ldm/sam3/tracker.py +++ b/comfy/ldm/sam3/tracker.py @@ -200,8 +200,13 @@ def pack_masks(masks): def unpack_masks(packed): """Unpack bit-packed [*, H, W//8] uint8 to bool [*, H, W*8].""" - shifts = torch.arange(8, device=packed.device) - return ((packed.unsqueeze(-1) >> shifts) & 1).view(*packed.shape[:-1], -1).bool() + bits = torch.tensor([1, 2, 4, 8, 16, 32, 64, 128], dtype=torch.uint8, device=packed.device) + return (packed.unsqueeze(-1) & bits).bool().view(*packed.shape[:-1], -1) + + +def _prep_frame(images, idx, device, dt, size): + """Slice CPU full-res frames, transfer to GPU in target dtype, and resize to (size, size).""" + return comfy.utils.common_upscale(images[idx].to(device=device, dtype=dt), size, size, "bicubic", crop="disabled") def _compute_backbone(backbone_fn, frame, frame_idx=None): @@ -1078,16 +1083,19 @@ class SAM3Tracker(nn.Module): # SAM3: drop last FPN level return vision_feats[:-1], vision_pos[:-1], feat_sizes[:-1] - def _track_single_object(self, backbone_fn, images, initial_mask, pbar=None): + def _track_single_object(self, backbone_fn, images, initial_mask, pbar=None, + target_device=None, target_dtype=None): """Track one object, computing backbone per frame to save VRAM.""" N = images.shape[0] - device, dt = images.device, images.dtype + device = target_device if target_device is not None else images.device + dt = target_dtype if target_dtype is not None else images.dtype + size = self.image_size output_dict = {"cond_frame_outputs": {}, "non_cond_frame_outputs": {}} all_masks = [] for frame_idx in tqdm(range(N), desc="tracking"): vision_feats, vision_pos, feat_sizes = self._compute_backbone_frame( - backbone_fn, images[frame_idx:frame_idx + 1], frame_idx=frame_idx) + backbone_fn, _prep_frame(images, slice(frame_idx, frame_idx + 1), device, dt, size), frame_idx=frame_idx) mask_input = None if frame_idx == 0: mask_input = F.interpolate(initial_mask.to(device=device, dtype=dt), @@ -1114,12 +1122,13 @@ class SAM3Tracker(nn.Module): return torch.cat(all_masks, dim=0) # [N, 1, H, W] - def track_video(self, backbone_fn, images, initial_masks, pbar=None, **kwargs): + def track_video(self, backbone_fn, images, initial_masks, pbar=None, + target_device=None, target_dtype=None, **kwargs): """Track one or more objects across video frames. Args: backbone_fn: callable that returns (sam2_features, sam2_positions, trunk_out) for a frame - images: [N, 3, 1008, 1008] video frames + images: [N, 3, H, W] CPU full-res video frames (resized per-frame to self.image_size) initial_masks: [N_obj, 1, H, W] binary masks for first frame (one per object) pbar: optional progress bar @@ -1130,7 +1139,8 @@ class SAM3Tracker(nn.Module): per_object = [] for obj_idx in range(N_obj): obj_masks = self._track_single_object( - backbone_fn, images, initial_masks[obj_idx:obj_idx + 1], pbar=pbar) + backbone_fn, images, initial_masks[obj_idx:obj_idx + 1], pbar=pbar, + target_device=target_device, target_dtype=target_dtype) per_object.append(obj_masks) return torch.cat(per_object, dim=1) # [N, N_obj, H, W] @@ -1632,11 +1642,18 @@ class SAM31Tracker(nn.Module): return det_scores[new_dets].tolist() if det_scores is not None else [0.0] * new_dets.sum().item() return [] + INTERNAL_MAX_OBJECTS = 64 # Hard ceiling on accumulated tracks; max_objects=0 or any value above this is clamped here. + def track_video_with_detection(self, backbone_fn, images, initial_masks, detect_fn=None, new_det_thresh=0.5, max_objects=0, detect_interval=1, - backbone_obj=None, pbar=None): + backbone_obj=None, pbar=None, target_device=None, target_dtype=None): """Track with optional per-frame detection. Returns [N, max_N_obj, H, W] mask logits.""" - N, device, dt = images.shape[0], images.device, images.dtype + if max_objects <= 0 or max_objects > self.INTERNAL_MAX_OBJECTS: + max_objects = self.INTERNAL_MAX_OBJECTS + N = images.shape[0] + device = target_device if target_device is not None else images.device + dt = target_dtype if target_dtype is not None else images.dtype + size = self.image_size output_dict = {"cond_frame_outputs": {}, "non_cond_frame_outputs": {}} all_masks = [] idev = comfy.model_management.intermediate_device() @@ -1656,7 +1673,7 @@ class SAM31Tracker(nn.Module): prefetch = True except RuntimeError: pass - cur_bb = self._compute_backbone_frame(backbone_fn, images[0:1], frame_idx=0) + cur_bb = self._compute_backbone_frame(backbone_fn, _prep_frame(images, slice(0, 1), device, dt, size), frame_idx=0) for frame_idx in tqdm(range(N), desc="tracking"): vision_feats, vision_pos, feat_sizes, high_res_prop, trunk_out = cur_bb @@ -1666,7 +1683,7 @@ class SAM31Tracker(nn.Module): backbone_stream.wait_stream(torch.cuda.current_stream(device)) with torch.cuda.stream(backbone_stream): next_bb = self._compute_backbone_frame( - backbone_fn, images[frame_idx + 1:frame_idx + 2], frame_idx=frame_idx + 1) + backbone_fn, _prep_frame(images, slice(frame_idx + 1, frame_idx + 2), device, dt, size), frame_idx=frame_idx + 1) # Per-frame detection with NMS (skip if no detect_fn, or interval/max not met) det_masks = torch.empty(0, device=device) @@ -1687,7 +1704,7 @@ class SAM31Tracker(nn.Module): current_out = self._condition_with_masks( initial_masks.to(device=device, dtype=dt), frame_idx, vision_feats, vision_pos, feat_sizes, high_res_prop, output_dict, N, mux_state, backbone_obj, - images[frame_idx:frame_idx + 1], trunk_out) + _prep_frame(images, slice(frame_idx, frame_idx + 1), device, dt, size), trunk_out) last_occluded = torch.full((mux_state.total_valid_entries,), -1, device=device, dtype=torch.long) obj_scores = [1.0] * mux_state.total_valid_entries if keep_alive is not None: @@ -1702,7 +1719,7 @@ class SAM31Tracker(nn.Module): current_out = self._condition_with_masks( det_masks, frame_idx, vision_feats, vision_pos, feat_sizes, high_res_prop, output_dict, N, mux_state, backbone_obj, - images[frame_idx:frame_idx + 1], trunk_out, threshold=0.0) + _prep_frame(images, slice(frame_idx, frame_idx + 1), device, dt, size), trunk_out, threshold=0.0) last_occluded = torch.full((mux_state.total_valid_entries,), -1, device=device, dtype=torch.long) obj_scores = det_scores[:mux_state.total_valid_entries].tolist() if keep_alive is not None: @@ -1718,7 +1735,7 @@ class SAM31Tracker(nn.Module): torch.cuda.current_stream(device).wait_stream(backbone_stream) cur_bb = next_bb else: - cur_bb = self._compute_backbone_frame(backbone_fn, images[frame_idx + 1:frame_idx + 2], frame_idx=frame_idx + 1) + cur_bb = self._compute_backbone_frame(backbone_fn, _prep_frame(images, slice(frame_idx + 1, frame_idx + 2), device, dt, size), frame_idx=frame_idx + 1) continue else: N_obj = mux_state.total_valid_entries @@ -1768,7 +1785,7 @@ class SAM31Tracker(nn.Module): torch.cuda.current_stream(device).wait_stream(backbone_stream) cur_bb = next_bb else: - cur_bb = self._compute_backbone_frame(backbone_fn, images[frame_idx + 1:frame_idx + 2], frame_idx=frame_idx + 1) + cur_bb = self._compute_backbone_frame(backbone_fn, _prep_frame(images, slice(frame_idx + 1, frame_idx + 2), device, dt, size), frame_idx=frame_idx + 1) if not all_masks or all(m is None for m in all_masks): return {"packed_masks": None, "n_frames": N, "scores": []} diff --git a/comfy_extras/nodes_sam3.py b/comfy_extras/nodes_sam3.py index 5cf92ccb3..c460506bf 100644 --- a/comfy_extras/nodes_sam3.py +++ b/comfy_extras/nodes_sam3.py @@ -272,8 +272,8 @@ class SAM3_VideoTrack(io.ComfyNode): io.Model.Input("model", display_name="model"), io.Mask.Input("initial_mask", display_name="initial_mask", optional=True, tooltip="Mask(s) for the first frame to track (one per object)"), io.Conditioning.Input("conditioning", display_name="conditioning", optional=True, tooltip="Text conditioning for detecting new objects during tracking"), - io.Float.Input("detection_threshold", display_name="detection_threshold", default=0.5, min=0.0, max=1.0, step=0.01, tooltip="Score threshold for text-prompted detection"), - io.Int.Input("max_objects", display_name="max_objects", default=0, min=0, tooltip="Max tracked objects (0=unlimited). Initial masks count toward this limit."), + io.Float.Input("detection_threshold", display_name="detection_threshold", default=0.5, min=0.0, max=1.0, step=0.01, tooltip="Score threshold for text-prompted detection."), + io.Int.Input("max_objects", display_name="max_objects", default=4, min=0, max=64, tooltip="Max tracked objects. Initial masks count toward this limit. 0 uses the internal cap of 64."), io.Int.Input("detect_interval", display_name="detect_interval", default=1, min=1, tooltip="Run detection every N frames (1=every frame). Higher values save compute."), ], outputs=[ @@ -290,8 +290,7 @@ class SAM3_VideoTrack(io.ComfyNode): dtype = model.model.get_dtype() sam3_model = model.model.diffusion_model - frames = images[..., :3].movedim(-1, 1) - frames_in = comfy.utils.common_upscale(frames, 1008, 1008, "bilinear", crop="disabled").to(device=device, dtype=dtype) + frames_in = images[..., :3].movedim(-1, 1) init_masks = None if initial_mask is not None: @@ -308,7 +307,7 @@ class SAM3_VideoTrack(io.ComfyNode): result = sam3_model.forward_video( images=frames_in, initial_masks=init_masks, pbar=pbar, text_prompts=text_prompts, new_det_thresh=detection_threshold, max_objects=max_objects, - detect_interval=detect_interval) + detect_interval=detect_interval, target_device=device, target_dtype=dtype) result["orig_size"] = (H, W) return io.NodeOutput(result) @@ -449,14 +448,18 @@ class SAM3_TrackPreview(io.ComfyNode): cx = (bool_masks * grid_x).sum(dim=(-1, -2)) // area has = area > 1 scores = track_data.get("scores", []) + label_scale = max(3, H // 240) # Scale font with resolutio + size_caps = (area.float().sqrt() / 15).clamp_(min=1).long().tolist() #cap per-object so the number doesn't dwarf small masks for obj_idx in range(N_obj): if has[obj_idx]: _cx, _cy = int(cx[obj_idx]), int(cy[obj_idx]) color = cls.COLORS[obj_idx % len(cls.COLORS)] - SAM3_TrackPreview._draw_number_gpu(frame_gpu, obj_idx, _cx, _cy, color) + obj_scale = min(label_scale, size_caps[obj_idx]) + score_scale = max(1, obj_scale * 2 // 3) + SAM3_TrackPreview._draw_number_gpu(frame_gpu, obj_idx, _cx, _cy, color, scale=obj_scale) if obj_idx < len(scores) and scores[obj_idx] < 1.0: SAM3_TrackPreview._draw_number_gpu(frame_gpu, int(scores[obj_idx] * 100), - _cx, _cy + 5 * 3 + 3, color, scale=2) + _cx, _cy + 5 * obj_scale + 3, color, scale=score_scale) frame_cpu.copy_(frame_gpu.clamp_(0, 1).mul_(255).byte()) else: frame_cpu.copy_(frame.clamp_(0, 1).mul_(255).byte()) @@ -507,9 +510,10 @@ class SAM3_TrackToMask(io.ComfyNode): if not indices: return io.NodeOutput(torch.zeros(N, H, W, device=comfy.model_management.intermediate_device())) - selected = packed[:, indices] - binary = unpack_masks(selected) # [N, len(indices), Hm, Wm] bool - union = binary.any(dim=1, keepdim=True).float() + union_packed = packed[:, indices[0]].clone() + for i in indices[1:]: + union_packed |= packed[:, i] + union = unpack_masks(union_packed).unsqueeze(1).float() # [N, 1, Hm, Wm] mask_out = F.interpolate(union, size=(H, W), mode="bilinear", align_corners=False)[:, 0] return io.NodeOutput(mask_out) From ef8f25601a8504647caf9c9213a7c41a9f414901 Mon Sep 17 00:00:00 2001 From: Talmaj Date: Fri, 8 May 2026 03:38:36 +0200 Subject: [PATCH 013/145] Add I2V for causal forcing model. (#13719) --- comfy/k_diffusion/sampling.py | 17 +++++++++++ comfy_extras/nodes_ar_video.py | 52 ++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/comfy/k_diffusion/sampling.py b/comfy/k_diffusion/sampling.py index d33bc7199..c53ac4b2b 100644 --- a/comfy/k_diffusion/sampling.py +++ b/comfy/k_diffusion/sampling.py @@ -1859,6 +1859,23 @@ def sample_ar_video(model, x, sigmas, extra_args=None, callback=None, disable=No output = torch.zeros_like(x) s_in = x.new_ones([x.shape[0]]) current_start_frame = 0 + + # I2V: seed KV cache with the initial image latent before the denoising loop + initial_latent = transformer_options.get("ar_config", {}).get("initial_latent", None) + if initial_latent is not None: + initial_latent = inner_model.process_latent_in(initial_latent).to(device=device, dtype=model_dtype) + n_init = initial_latent.shape[2] + output[:, :, :n_init] = initial_latent + + ar_state = {"start_frame": 0, "kv_caches": kv_caches, "crossattn_caches": crossattn_caches} + transformer_options["ar_state"] = ar_state + zero_sigma = sigmas.new_zeros([1]) + _ = model(initial_latent, zero_sigma * s_in, **extra_args) + + current_start_frame = n_init + remaining = lat_t - n_init + num_blocks = -(-remaining // num_frame_per_block) + num_sigma_steps = len(sigmas) - 1 total_real_steps = num_blocks * num_sigma_steps step_count = 0 diff --git a/comfy_extras/nodes_ar_video.py b/comfy_extras/nodes_ar_video.py index 09ee886fd..b36588b14 100644 --- a/comfy_extras/nodes_ar_video.py +++ b/comfy_extras/nodes_ar_video.py @@ -2,6 +2,7 @@ ComfyUI nodes for autoregressive video generation (Causal Forcing, Self-Forcing, etc.). - EmptyARVideoLatent: create 5D [B, C, T, H, W] video latent tensors - SamplerARVideo: SAMPLER for the block-by-block autoregressive denoising loop + - ARVideoI2V: image-to-video conditioning for AR models (seeds KV cache with start image) """ import torch @@ -9,6 +10,7 @@ from typing_extensions import override import comfy.model_management import comfy.samplers +import comfy.utils from comfy_api.latest import ComfyExtension, io @@ -71,12 +73,62 @@ class SamplerARVideo(io.ComfyNode): return io.NodeOutput(comfy.samplers.ksampler("ar_video", extra_options)) +class ARVideoI2V(io.ComfyNode): + """Image-to-video setup for AR video models (Causal Forcing, Self-Forcing). + + VAE-encodes the start image and stores it in the model's transformer_options + so that sample_ar_video can seed the KV cache before denoising. + Uses the same T2V model checkpoint -- no separate I2V architecture needed. + """ + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="ARVideoI2V", + category="conditioning/video_models", + inputs=[ + io.Model.Input("model"), + io.Vae.Input("vae"), + io.Image.Input("start_image"), + io.Int.Input("width", default=832, min=16, max=8192, step=16), + io.Int.Input("height", default=480, min=16, max=8192, step=16), + io.Int.Input("length", default=81, min=1, max=1024, step=4), + io.Int.Input("batch_size", default=1, min=1, max=64), + ], + outputs=[ + io.Model.Output(display_name="MODEL"), + io.Latent.Output(display_name="LATENT"), + ], + ) + + @classmethod + def execute(cls, model, vae, start_image, width, height, length, batch_size) -> io.NodeOutput: + start_image = comfy.utils.common_upscale( + start_image[:1].movedim(-1, 1), width, height, "bilinear", "center" + ).movedim(1, -1) + + initial_latent = vae.encode(start_image[:, :, :, :3]) + + m = model.clone() + to = m.model_options.setdefault("transformer_options", {}) + ar_cfg = to.setdefault("ar_config", {}) + ar_cfg["initial_latent"] = initial_latent + + lat_t = ((length - 1) // 4) + 1 + latent = torch.zeros( + [batch_size, 16, lat_t, height // 8, width // 8], + device=comfy.model_management.intermediate_device(), + ) + return io.NodeOutput(m, {"samples": latent}) + + class ARVideoExtension(ComfyExtension): @override async def get_node_list(self) -> list[type[io.ComfyNode]]: return [ EmptyARVideoLatent, SamplerARVideo, + ARVideoI2V, ] From df7bf1d3dc852365593786497123d92440ac1852 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Thu, 7 May 2026 19:04:30 -0700 Subject: [PATCH 014/145] Update warning message for ComfyUI frontend installation. (#13796) --- app/frontend_management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend_management.py b/app/frontend_management.py index f753ef0de..7108bd35a 100644 --- a/app/frontend_management.py +++ b/app/frontend_management.py @@ -27,7 +27,7 @@ def frontend_install_warning_message(): return f""" {get_missing_requirements_message()} -This error is happening because the ComfyUI frontend is no longer shipped as part of the main repo but as a pip package instead. +The ComfyUI frontend is shipped in a pip package so it needs to be updated separately from the ComfyUI code. """.strip() def parse_version(version: str) -> tuple[int, int, int]: From c8673542f762910766691345401e09caef2bc9a6 Mon Sep 17 00:00:00 2001 From: Jedrzej Kosinski Date: Thu, 7 May 2026 19:21:12 -0700 Subject: [PATCH 015/145] fix: make NodeReplaceManager.register() idempotent (#13596) --- app/node_replace_manager.py | 20 ++++- .../app_test/node_replace_manager_test.py | 90 +++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 tests-unit/app_test/node_replace_manager_test.py diff --git a/app/node_replace_manager.py b/app/node_replace_manager.py index d9aab5b22..72e8ac2b1 100644 --- a/app/node_replace_manager.py +++ b/app/node_replace_manager.py @@ -1,5 +1,7 @@ from __future__ import annotations +import logging + from aiohttp import web from typing import TYPE_CHECKING, TypedDict @@ -31,8 +33,22 @@ class NodeReplaceManager: self._replacements: dict[str, list[NodeReplace]] = {} def register(self, node_replace: NodeReplace): - """Register a node replacement mapping.""" - self._replacements.setdefault(node_replace.old_node_id, []).append(node_replace) + """Register a node replacement mapping. + + Idempotent: if a replacement with the same (old_node_id, new_node_id) + is already registered, the duplicate is ignored. This prevents stale + entries from accumulating when custom nodes are reloaded in the same + process (e.g. via ComfyUI-Manager). + """ + existing = self._replacements.setdefault(node_replace.old_node_id, []) + for entry in existing: + if entry.new_node_id == node_replace.new_node_id: + logging.debug( + "Node replacement %s -> %s already registered, ignoring duplicate.", + node_replace.old_node_id, node_replace.new_node_id, + ) + return + existing.append(node_replace) def get_replacement(self, old_node_id: str) -> list[NodeReplace] | None: """Get replacements for an old node ID.""" diff --git a/tests-unit/app_test/node_replace_manager_test.py b/tests-unit/app_test/node_replace_manager_test.py new file mode 100644 index 000000000..8a3fd18bb --- /dev/null +++ b/tests-unit/app_test/node_replace_manager_test.py @@ -0,0 +1,90 @@ +"""Tests for NodeReplaceManager registration behavior.""" +import importlib +import sys +import types + +import pytest + + +@pytest.fixture +def NodeReplaceManager(monkeypatch): + """Provide NodeReplaceManager with `nodes` stubbed. + + `app.node_replace_manager` does `import nodes` at module level, which pulls in + torch + the full ComfyUI graph. register() doesn't actually need it, so we + stub `nodes` per-test (via monkeypatch so it's torn down) and reload the + module so it picks up the stub instead of any cached real import. + """ + fake_nodes = types.ModuleType("nodes") + fake_nodes.NODE_CLASS_MAPPINGS = {} + monkeypatch.setitem(sys.modules, "nodes", fake_nodes) + monkeypatch.delitem(sys.modules, "app.node_replace_manager", raising=False) + module = importlib.import_module("app.node_replace_manager") + yield module.NodeReplaceManager + # Drop the freshly-imported module so the next test (or a later real import + # of `nodes`) starts from a clean slate. + sys.modules.pop("app.node_replace_manager", None) + + +class FakeNodeReplace: + """Lightweight stand-in for comfy_api.latest._io.NodeReplace.""" + def __init__(self, new_node_id, old_node_id, old_widget_ids=None, + input_mapping=None, output_mapping=None): + self.new_node_id = new_node_id + self.old_node_id = old_node_id + self.old_widget_ids = old_widget_ids + self.input_mapping = input_mapping + self.output_mapping = output_mapping + + +def test_register_adds_replacement(NodeReplaceManager): + manager = NodeReplaceManager() + manager.register(FakeNodeReplace(new_node_id="NewNode", old_node_id="OldNode")) + assert manager.has_replacement("OldNode") + assert len(manager.get_replacement("OldNode")) == 1 + + +def test_register_allows_multiple_alternatives_for_same_old_node(NodeReplaceManager): + """Different new_node_ids for the same old_node_id should all be kept.""" + manager = NodeReplaceManager() + manager.register(FakeNodeReplace(new_node_id="AltA", old_node_id="OldNode")) + manager.register(FakeNodeReplace(new_node_id="AltB", old_node_id="OldNode")) + replacements = manager.get_replacement("OldNode") + assert len(replacements) == 2 + assert {r.new_node_id for r in replacements} == {"AltA", "AltB"} + + +def test_register_is_idempotent_for_duplicate_pair(NodeReplaceManager): + """Re-registering the same (old_node_id, new_node_id) should be a no-op.""" + manager = NodeReplaceManager() + manager.register(FakeNodeReplace(new_node_id="NewNode", old_node_id="OldNode")) + manager.register(FakeNodeReplace(new_node_id="NewNode", old_node_id="OldNode")) + manager.register(FakeNodeReplace(new_node_id="NewNode", old_node_id="OldNode")) + assert len(manager.get_replacement("OldNode")) == 1 + + +def test_register_idempotent_preserves_first_registration(NodeReplaceManager): + """First registration wins; later duplicates with different mappings are ignored.""" + manager = NodeReplaceManager() + first = FakeNodeReplace( + new_node_id="NewNode", old_node_id="OldNode", + input_mapping=[{"new_id": "a", "old_id": "x"}], + ) + second = FakeNodeReplace( + new_node_id="NewNode", old_node_id="OldNode", + input_mapping=[{"new_id": "b", "old_id": "y"}], + ) + manager.register(first) + manager.register(second) + replacements = manager.get_replacement("OldNode") + assert len(replacements) == 1 + assert replacements[0] is first + + +def test_register_dedupe_does_not_affect_other_old_nodes(NodeReplaceManager): + manager = NodeReplaceManager() + manager.register(FakeNodeReplace(new_node_id="NewA", old_node_id="OldA")) + manager.register(FakeNodeReplace(new_node_id="NewA", old_node_id="OldA")) + manager.register(FakeNodeReplace(new_node_id="NewB", old_node_id="OldB")) + assert len(manager.get_replacement("OldA")) == 1 + assert len(manager.get_replacement("OldB")) == 1 From 594de378fe1d2e32128338f5cc57864ee1d9d96f Mon Sep 17 00:00:00 2001 From: Alexis Rolland Date: Fri, 8 May 2026 13:02:55 +0800 Subject: [PATCH 016/145] Update nodes categories and display names (CORE-89) (#13786) --- comfy_extras/nodes_advanced_samplers.py | 2 +- comfy_extras/nodes_attention_multiply.py | 8 ++++---- comfy_extras/nodes_audio_encoder.py | 1 + comfy_extras/nodes_camera_trajectory.py | 2 +- comfy_extras/nodes_cond.py | 4 ++-- comfy_extras/nodes_context_windows.py | 2 +- comfy_extras/nodes_custom_sampler.py | 4 ++-- comfy_extras/nodes_differential_diffusion.py | 2 +- comfy_extras/nodes_fresca.py | 2 +- comfy_extras/nodes_hunyuan.py | 4 ++++ comfy_extras/nodes_hunyuan3d.py | 6 ++++-- comfy_extras/nodes_hypernetwork.py | 1 + comfy_extras/nodes_lora_extract.py | 2 +- comfy_extras/nodes_lt.py | 3 ++- comfy_extras/nodes_mahiro.py | 2 +- comfy_extras/nodes_math.py | 2 +- comfy_extras/nodes_number_convert.py | 2 +- comfy_extras/nodes_perpneg.py | 7 ++++--- comfy_extras/nodes_photomaker.py | 4 ++-- comfy_extras/nodes_post_processing.py | 4 +++- comfy_extras/nodes_rtdetr.py | 4 ++-- comfy_extras/nodes_sag.py | 2 +- comfy_extras/nodes_sam3.py | 8 ++++---- comfy_extras/nodes_stable_cascade.py | 2 +- comfy_extras/nodes_textgen.py | 4 +++- comfy_extras/nodes_torch_compile.py | 2 +- comfy_extras/nodes_train.py | 2 +- comfy_extras/nodes_video_model.py | 2 +- custom_nodes/websocket_image_save.py | 6 +++++- nodes.py | 14 +++++++------ .../testing-pack/api_test_nodes.py | 4 ++-- .../testing-pack/async_test_nodes.py | 20 +++++++++---------- .../testing-pack/specific_tests.py | 6 +++--- 33 files changed, 80 insertions(+), 60 deletions(-) diff --git a/comfy_extras/nodes_advanced_samplers.py b/comfy_extras/nodes_advanced_samplers.py index 7f716cd76..7e8411fa4 100644 --- a/comfy_extras/nodes_advanced_samplers.py +++ b/comfy_extras/nodes_advanced_samplers.py @@ -92,7 +92,7 @@ class SamplerEulerCFGpp(io.ComfyNode): return io.Schema( node_id="SamplerEulerCFGpp", display_name="SamplerEulerCFG++", - category="_for_testing", # "sampling/custom_sampling/samplers" + category="experimental", # "sampling/custom_sampling/samplers" inputs=[ io.Combo.Input("version", options=["regular", "alternative"], advanced=True), ], diff --git a/comfy_extras/nodes_attention_multiply.py b/comfy_extras/nodes_attention_multiply.py index 060a5c9be..f4ee6a689 100644 --- a/comfy_extras/nodes_attention_multiply.py +++ b/comfy_extras/nodes_attention_multiply.py @@ -25,7 +25,7 @@ class UNetSelfAttentionMultiply(io.ComfyNode): def define_schema(cls) -> io.Schema: return io.Schema( node_id="UNetSelfAttentionMultiply", - category="_for_testing/attention_experiments", + category="experimental/attention_experiments", inputs=[ io.Model.Input("model"), io.Float.Input("q", default=1.0, min=0.0, max=10.0, step=0.01, advanced=True), @@ -48,7 +48,7 @@ class UNetCrossAttentionMultiply(io.ComfyNode): def define_schema(cls) -> io.Schema: return io.Schema( node_id="UNetCrossAttentionMultiply", - category="_for_testing/attention_experiments", + category="experimental/attention_experiments", inputs=[ io.Model.Input("model"), io.Float.Input("q", default=1.0, min=0.0, max=10.0, step=0.01, advanced=True), @@ -72,7 +72,7 @@ class CLIPAttentionMultiply(io.ComfyNode): return io.Schema( node_id="CLIPAttentionMultiply", search_aliases=["clip attention scale", "text encoder attention"], - category="_for_testing/attention_experiments", + category="experimental/attention_experiments", inputs=[ io.Clip.Input("clip"), io.Float.Input("q", default=1.0, min=0.0, max=10.0, step=0.01, advanced=True), @@ -106,7 +106,7 @@ class UNetTemporalAttentionMultiply(io.ComfyNode): def define_schema(cls) -> io.Schema: return io.Schema( node_id="UNetTemporalAttentionMultiply", - category="_for_testing/attention_experiments", + category="experimental/attention_experiments", inputs=[ io.Model.Input("model"), io.Float.Input("self_structural", default=1.0, min=0.0, max=10.0, step=0.01, advanced=True), diff --git a/comfy_extras/nodes_audio_encoder.py b/comfy_extras/nodes_audio_encoder.py index 13aacd41a..6a85da89b 100644 --- a/comfy_extras/nodes_audio_encoder.py +++ b/comfy_extras/nodes_audio_encoder.py @@ -10,6 +10,7 @@ class AudioEncoderLoader(io.ComfyNode): def define_schema(cls) -> io.Schema: return io.Schema( node_id="AudioEncoderLoader", + display_name="Load Audio Encoder", category="loaders", inputs=[ io.Combo.Input( diff --git a/comfy_extras/nodes_camera_trajectory.py b/comfy_extras/nodes_camera_trajectory.py index e7efa29ba..34b78e81b 100644 --- a/comfy_extras/nodes_camera_trajectory.py +++ b/comfy_extras/nodes_camera_trajectory.py @@ -153,7 +153,7 @@ class WanCameraEmbedding(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="WanCameraEmbedding", - category="camera", + category="conditioning/video_models", inputs=[ io.Combo.Input( "camera_pose", diff --git a/comfy_extras/nodes_cond.py b/comfy_extras/nodes_cond.py index 86426a780..b745a43af 100644 --- a/comfy_extras/nodes_cond.py +++ b/comfy_extras/nodes_cond.py @@ -8,7 +8,7 @@ class CLIPTextEncodeControlnet(io.ComfyNode): def define_schema(cls) -> io.Schema: return io.Schema( node_id="CLIPTextEncodeControlnet", - category="_for_testing/conditioning", + category="experimental/conditioning", inputs=[ io.Clip.Input("clip"), io.Conditioning.Input("conditioning"), @@ -35,7 +35,7 @@ class T5TokenizerOptions(io.ComfyNode): def define_schema(cls) -> io.Schema: return io.Schema( node_id="T5TokenizerOptions", - category="_for_testing/conditioning", + category="experimental/conditioning", inputs=[ io.Clip.Input("clip"), io.Int.Input("min_padding", default=0, min=0, max=10000, step=1, advanced=True), diff --git a/comfy_extras/nodes_context_windows.py b/comfy_extras/nodes_context_windows.py index fefc56d26..f7ca833dc 100644 --- a/comfy_extras/nodes_context_windows.py +++ b/comfy_extras/nodes_context_windows.py @@ -10,7 +10,7 @@ class ContextWindowsManualNode(io.ComfyNode): return io.Schema( node_id="ContextWindowsManual", display_name="Context Windows (Manual)", - category="context", + category="model_patches", description="Manually set context windows.", inputs=[ io.Model.Input("model", tooltip="The model to apply context windows to during sampling."), diff --git a/comfy_extras/nodes_custom_sampler.py b/comfy_extras/nodes_custom_sampler.py index 1e957c09b..c67145d2d 100644 --- a/comfy_extras/nodes_custom_sampler.py +++ b/comfy_extras/nodes_custom_sampler.py @@ -984,7 +984,7 @@ class AddNoise(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="AddNoise", - category="_for_testing/custom_sampling/noise", + category="experimental/custom_sampling/noise", is_experimental=True, inputs=[ io.Model.Input("model"), @@ -1034,7 +1034,7 @@ class ManualSigmas(io.ComfyNode): return io.Schema( node_id="ManualSigmas", search_aliases=["custom noise schedule", "define sigmas"], - category="_for_testing/custom_sampling", + category="experimental/custom_sampling", is_experimental=True, inputs=[ io.String.Input("sigmas", default="1, 0.5", multiline=False) diff --git a/comfy_extras/nodes_differential_diffusion.py b/comfy_extras/nodes_differential_diffusion.py index 34ffb9a89..4fa61ad0e 100644 --- a/comfy_extras/nodes_differential_diffusion.py +++ b/comfy_extras/nodes_differential_diffusion.py @@ -13,7 +13,7 @@ class DifferentialDiffusion(io.ComfyNode): node_id="DifferentialDiffusion", search_aliases=["inpaint gradient", "variable denoise strength"], display_name="Differential Diffusion", - category="_for_testing", + category="experimental", inputs=[ io.Model.Input("model"), io.Float.Input( diff --git a/comfy_extras/nodes_fresca.py b/comfy_extras/nodes_fresca.py index eab4f303f..173f42154 100644 --- a/comfy_extras/nodes_fresca.py +++ b/comfy_extras/nodes_fresca.py @@ -60,7 +60,7 @@ class FreSca(io.ComfyNode): node_id="FreSca", search_aliases=["frequency guidance"], display_name="FreSca", - category="_for_testing", + category="experimental", description="Applies frequency-dependent scaling to the guidance", inputs=[ io.Model.Input("model"), diff --git a/comfy_extras/nodes_hunyuan.py b/comfy_extras/nodes_hunyuan.py index 4ea93a499..9e4873be5 100644 --- a/comfy_extras/nodes_hunyuan.py +++ b/comfy_extras/nodes_hunyuan.py @@ -131,6 +131,8 @@ class HunyuanVideo15SuperResolution(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="HunyuanVideo15SuperResolution", + display_name="Hunyuan Video 1.5 Super Resolution", + category="conditioning/video_models", inputs=[ io.Conditioning.Input("positive"), io.Conditioning.Input("negative"), @@ -381,6 +383,8 @@ class HunyuanRefinerLatent(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="HunyuanRefinerLatent", + display_name="Hunyuan Latent Refiner", + category="conditioning/video_models", inputs=[ io.Conditioning.Input("positive"), io.Conditioning.Input("negative"), diff --git a/comfy_extras/nodes_hunyuan3d.py b/comfy_extras/nodes_hunyuan3d.py index fa55ead59..bf18ecb88 100644 --- a/comfy_extras/nodes_hunyuan3d.py +++ b/comfy_extras/nodes_hunyuan3d.py @@ -40,7 +40,7 @@ class Hunyuan3Dv2Conditioning(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="Hunyuan3Dv2Conditioning", - category="conditioning/video_models", + category="conditioning/3d_models", inputs=[ IO.ClipVisionOutput.Input("clip_vision_output"), ], @@ -65,7 +65,7 @@ class Hunyuan3Dv2ConditioningMultiView(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="Hunyuan3Dv2ConditioningMultiView", - category="conditioning/video_models", + category="conditioning/3d_models", inputs=[ IO.ClipVisionOutput.Input("front", optional=True), IO.ClipVisionOutput.Input("left", optional=True), @@ -424,6 +424,7 @@ class VoxelToMeshBasic(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="VoxelToMeshBasic", + display_name="Voxel to Mesh (Basic)", category="3d", inputs=[ IO.Voxel.Input("voxel"), @@ -453,6 +454,7 @@ class VoxelToMesh(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="VoxelToMesh", + display_name="Voxel to Mesh", category="3d", inputs=[ IO.Voxel.Input("voxel"), diff --git a/comfy_extras/nodes_hypernetwork.py b/comfy_extras/nodes_hypernetwork.py index 2a6a87a81..44a9c6f97 100644 --- a/comfy_extras/nodes_hypernetwork.py +++ b/comfy_extras/nodes_hypernetwork.py @@ -102,6 +102,7 @@ class HypernetworkLoader(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="HypernetworkLoader", + display_name="Load Hypernetwork", category="loaders", inputs=[ IO.Model.Input("model"), diff --git a/comfy_extras/nodes_lora_extract.py b/comfy_extras/nodes_lora_extract.py index 975f90f45..bcd249c29 100644 --- a/comfy_extras/nodes_lora_extract.py +++ b/comfy_extras/nodes_lora_extract.py @@ -91,7 +91,7 @@ class LoraSave(io.ComfyNode): node_id="LoraSave", search_aliases=["export lora"], display_name="Extract and Save Lora", - category="_for_testing", + category="experimental", inputs=[ io.String.Input("filename_prefix", default="loras/ComfyUI_extracted_lora"), io.Int.Input("rank", default=8, min=1, max=4096, step=1, advanced=True), diff --git a/comfy_extras/nodes_lt.py b/comfy_extras/nodes_lt.py index 19d8a387f..ab1359fdb 100644 --- a/comfy_extras/nodes_lt.py +++ b/comfy_extras/nodes_lt.py @@ -594,7 +594,8 @@ class LTXVPreprocess(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="LTXVPreprocess", - category="image", + display_name="LTXV Preprocess", + category="video/preprocessors", inputs=[ io.Image.Input("image"), io.Int.Input( diff --git a/comfy_extras/nodes_mahiro.py b/comfy_extras/nodes_mahiro.py index a25226e6d..7bd5f6652 100644 --- a/comfy_extras/nodes_mahiro.py +++ b/comfy_extras/nodes_mahiro.py @@ -11,7 +11,7 @@ class Mahiro(io.ComfyNode): return io.Schema( node_id="Mahiro", display_name="Positive-Biased Guidance", - category="_for_testing", + category="experimental", description="Modify the guidance to scale more on the 'direction' of the positive prompt rather than the difference between the negative prompt.", inputs=[ io.Model.Input("model"), diff --git a/comfy_extras/nodes_math.py b/comfy_extras/nodes_math.py index 6417bacf1..8f6e687d2 100644 --- a/comfy_extras/nodes_math.py +++ b/comfy_extras/nodes_math.py @@ -70,7 +70,7 @@ class MathExpressionNode(io.ComfyNode): return io.Schema( node_id="ComfyMathExpression", display_name="Math Expression", - category="math", + category="logic", search_aliases=[ "expression", "formula", "calculate", "calculator", "eval", "math", diff --git a/comfy_extras/nodes_number_convert.py b/comfy_extras/nodes_number_convert.py index cac7e736d..ab3f2aa8a 100644 --- a/comfy_extras/nodes_number_convert.py +++ b/comfy_extras/nodes_number_convert.py @@ -21,7 +21,7 @@ class NumberConvertNode(io.ComfyNode): return io.Schema( node_id="ComfyNumberConvert", display_name="Number Convert", - category="math", + category="utils", search_aliases=[ "int to float", "float to int", "number convert", "int2float", "float2int", "cast", "parse number", diff --git a/comfy_extras/nodes_perpneg.py b/comfy_extras/nodes_perpneg.py index ed1467de9..a7a72d1bc 100644 --- a/comfy_extras/nodes_perpneg.py +++ b/comfy_extras/nodes_perpneg.py @@ -24,8 +24,8 @@ class PerpNeg(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="PerpNeg", - display_name="Perp-Neg (DEPRECATED by PerpNegGuider)", - category="_for_testing", + display_name="Perp-Neg (DEPRECATED by Perp-Neg Guider)", + category="experimental", inputs=[ io.Model.Input("model"), io.Conditioning.Input("empty_conditioning"), @@ -127,7 +127,8 @@ class PerpNegGuider(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="PerpNegGuider", - category="_for_testing", + display_name="Perp-Neg Guider", + category="experimental", inputs=[ io.Model.Input("model"), io.Conditioning.Input("positive"), diff --git a/comfy_extras/nodes_photomaker.py b/comfy_extras/nodes_photomaker.py index 228183c07..8a2248572 100644 --- a/comfy_extras/nodes_photomaker.py +++ b/comfy_extras/nodes_photomaker.py @@ -123,7 +123,7 @@ class PhotoMakerLoader(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="PhotoMakerLoader", - category="_for_testing/photomaker", + category="experimental/photomaker", inputs=[ io.Combo.Input("photomaker_model_name", options=folder_paths.get_filename_list("photomaker")), ], @@ -149,7 +149,7 @@ class PhotoMakerEncode(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="PhotoMakerEncode", - category="_for_testing/photomaker", + category="experimental/photomaker", inputs=[ io.Photomaker.Input("photomaker"), io.Image.Input("image"), diff --git a/comfy_extras/nodes_post_processing.py b/comfy_extras/nodes_post_processing.py index d938a2035..1fa14d2d2 100644 --- a/comfy_extras/nodes_post_processing.py +++ b/comfy_extras/nodes_post_processing.py @@ -116,6 +116,7 @@ class Quantize(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="ImageQuantize", + display_name="Quantize Image", category="image/postprocessing", inputs=[ io.Image.Input("image"), @@ -181,6 +182,7 @@ class Sharpen(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="ImageSharpen", + display_name="Sharpen Image", category="image/postprocessing", inputs=[ io.Image.Input("image"), @@ -436,7 +438,7 @@ class ResizeImageMaskNode(io.ComfyNode): node_id="ResizeImageMaskNode", display_name="Resize Image/Mask", description="Resize an image or mask using various scaling methods.", - category="transform", + category="image/transform", search_aliases=["resize", "resize image", "resize mask", "scale", "scale image", "scale mask", "image resize", "change size", "dimensions", "shrink", "enlarge"], inputs=[ io.MatchType.Input("input", template=template), diff --git a/comfy_extras/nodes_rtdetr.py b/comfy_extras/nodes_rtdetr.py index 7feaf3ab3..a321577c7 100644 --- a/comfy_extras/nodes_rtdetr.py +++ b/comfy_extras/nodes_rtdetr.py @@ -15,7 +15,7 @@ class RTDETR_detect(io.ComfyNode): return io.Schema( node_id="RTDETR_detect", display_name="RT-DETR Detect", - category="detection/", + category="detection", search_aliases=["bbox", "bounding box", "object detection", "coco"], inputs=[ io.Model.Input("model", display_name="model"), @@ -71,7 +71,7 @@ class DrawBBoxes(io.ComfyNode): return io.Schema( node_id="DrawBBoxes", display_name="Draw BBoxes", - category="detection/", + category="detection", search_aliases=["bbox", "bounding box", "object detection", "rt_detr", "visualize detections", "coco"], inputs=[ io.Image.Input("image", optional=True), diff --git a/comfy_extras/nodes_sag.py b/comfy_extras/nodes_sag.py index d9c47851c..9dbf1b6f9 100644 --- a/comfy_extras/nodes_sag.py +++ b/comfy_extras/nodes_sag.py @@ -113,7 +113,7 @@ class SelfAttentionGuidance(io.ComfyNode): return io.Schema( node_id="SelfAttentionGuidance", display_name="Self-Attention Guidance", - category="_for_testing", + category="experimental", inputs=[ io.Model.Input("model"), io.Float.Input("scale", default=0.5, min=-2.0, max=5.0, step=0.01), diff --git a/comfy_extras/nodes_sam3.py b/comfy_extras/nodes_sam3.py index c460506bf..4ea9221e9 100644 --- a/comfy_extras/nodes_sam3.py +++ b/comfy_extras/nodes_sam3.py @@ -93,7 +93,7 @@ class SAM3_Detect(io.ComfyNode): return io.Schema( node_id="SAM3_Detect", display_name="SAM3 Detect", - category="detection/", + category="detection", search_aliases=["sam3", "segment anything", "open vocabulary", "text detection", "segment"], inputs=[ io.Model.Input("model", display_name="model"), @@ -265,7 +265,7 @@ class SAM3_VideoTrack(io.ComfyNode): return io.Schema( node_id="SAM3_VideoTrack", display_name="SAM3 Video Track", - category="detection/", + category="detection", search_aliases=["sam3", "video", "track", "propagate"], inputs=[ io.Image.Input("images", display_name="images", tooltip="Video frames as batched images"), @@ -320,7 +320,7 @@ class SAM3_TrackPreview(io.ComfyNode): return io.Schema( node_id="SAM3_TrackPreview", display_name="SAM3 Track Preview", - category="detection/", + category="detection", inputs=[ SAM3TrackData.Input("track_data", display_name="track_data"), io.Image.Input("images", display_name="images", optional=True), @@ -478,7 +478,7 @@ class SAM3_TrackToMask(io.ComfyNode): return io.Schema( node_id="SAM3_TrackToMask", display_name="SAM3 Track to Mask", - category="detection/", + category="detection", inputs=[ SAM3TrackData.Input("track_data", display_name="track_data"), io.String.Input("object_indices", display_name="object_indices", default="", diff --git a/comfy_extras/nodes_stable_cascade.py b/comfy_extras/nodes_stable_cascade.py index 8c1aebca9..0dc6c9fcd 100644 --- a/comfy_extras/nodes_stable_cascade.py +++ b/comfy_extras/nodes_stable_cascade.py @@ -119,7 +119,7 @@ class StableCascade_SuperResolutionControlnet(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="StableCascade_SuperResolutionControlnet", - category="_for_testing/stable_cascade", + category="experimental/stable_cascade", is_experimental=True, inputs=[ io.Image.Input("image"), diff --git a/comfy_extras/nodes_textgen.py b/comfy_extras/nodes_textgen.py index 1661a1011..d52faf815 100644 --- a/comfy_extras/nodes_textgen.py +++ b/comfy_extras/nodes_textgen.py @@ -26,7 +26,8 @@ class TextGenerate(io.ComfyNode): return io.Schema( node_id="TextGenerate", - category="textgen", + display_name="Generate Text", + category="text", search_aliases=["LLM", "gemma"], inputs=[ io.Clip.Input("clip"), @@ -157,6 +158,7 @@ class TextGenerateLTX2Prompt(TextGenerate): parent_schema = super().define_schema() return io.Schema( node_id="TextGenerateLTX2Prompt", + display_name="Generate LTX2 Prompt", category=parent_schema.category, inputs=parent_schema.inputs, outputs=parent_schema.outputs, diff --git a/comfy_extras/nodes_torch_compile.py b/comfy_extras/nodes_torch_compile.py index c9e2e0026..d4506b1a9 100644 --- a/comfy_extras/nodes_torch_compile.py +++ b/comfy_extras/nodes_torch_compile.py @@ -10,7 +10,7 @@ class TorchCompileModel(io.ComfyNode): def define_schema(cls) -> io.Schema: return io.Schema( node_id="TorchCompileModel", - category="_for_testing", + category="experimental", inputs=[ io.Model.Input("model"), io.Combo.Input( diff --git a/comfy_extras/nodes_train.py b/comfy_extras/nodes_train.py index 0616dfc2d..e9871369b 100644 --- a/comfy_extras/nodes_train.py +++ b/comfy_extras/nodes_train.py @@ -1361,7 +1361,7 @@ class SaveLoRA(io.ComfyNode): node_id="SaveLoRA", search_aliases=["export lora"], display_name="Save LoRA Weights", - category="loaders", + category="advanced/model_merging", is_experimental=True, is_output_node=True, inputs=[ diff --git a/comfy_extras/nodes_video_model.py b/comfy_extras/nodes_video_model.py index bf98e6b82..0f3881a24 100644 --- a/comfy_extras/nodes_video_model.py +++ b/comfy_extras/nodes_video_model.py @@ -15,7 +15,7 @@ class ImageOnlyCheckpointLoader: RETURN_TYPES = ("MODEL", "CLIP_VISION", "VAE") FUNCTION = "load_checkpoint" - CATEGORY = "loaders/video_models" + CATEGORY = "loaders" def load_checkpoint(self, ckpt_name, output_vae=True, output_clip=True): ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name) diff --git a/custom_nodes/websocket_image_save.py b/custom_nodes/websocket_image_save.py index 15f87f9f5..6a8646d0e 100644 --- a/custom_nodes/websocket_image_save.py +++ b/custom_nodes/websocket_image_save.py @@ -22,7 +22,7 @@ class SaveImageWebsocket: OUTPUT_NODE = True - CATEGORY = "api/image" + CATEGORY = "image" def save_images(self, images): pbar = comfy.utils.ProgressBar(images.shape[0]) @@ -42,3 +42,7 @@ class SaveImageWebsocket: NODE_CLASS_MAPPINGS = { "SaveImageWebsocket": SaveImageWebsocket, } + +NODE_DISPLAY_NAME_MAPPINGS = { + "SaveImageWebsocket": "Save Image (Websocket)", +} \ No newline at end of file diff --git a/nodes.py b/nodes.py index ad0cbc675..ae9e70cb9 100644 --- a/nodes.py +++ b/nodes.py @@ -330,7 +330,7 @@ class VAEDecodeTiled: RETURN_TYPES = ("IMAGE",) FUNCTION = "decode" - CATEGORY = "_for_testing" + CATEGORY = "experimental" def decode(self, vae, samples, tile_size, overlap=64, temporal_size=64, temporal_overlap=8): if tile_size < overlap * 4: @@ -377,7 +377,7 @@ class VAEEncodeTiled: RETURN_TYPES = ("LATENT",) FUNCTION = "encode" - CATEGORY = "_for_testing" + CATEGORY = "experimental" def encode(self, vae, pixels, tile_size, overlap, temporal_size=64, temporal_overlap=8): t = vae.encode_tiled(pixels, tile_x=tile_size, tile_y=tile_size, overlap=overlap, tile_t=temporal_size, overlap_t=temporal_overlap) @@ -493,7 +493,7 @@ class SaveLatent: OUTPUT_NODE = True - CATEGORY = "_for_testing" + CATEGORY = "experimental" def save(self, samples, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None): full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir) @@ -538,7 +538,7 @@ class LoadLatent: files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f)) and f.endswith(".latent")] return {"required": {"latent": [sorted(files), ]}, } - CATEGORY = "_for_testing" + CATEGORY = "experimental" RETURN_TYPES = ("LATENT", ) FUNCTION = "load" @@ -1443,7 +1443,7 @@ class LatentBlend: RETURN_TYPES = ("LATENT",) FUNCTION = "blend" - CATEGORY = "_for_testing" + CATEGORY = "experimental" def blend(self, samples1, samples2, blend_factor:float, blend_mode: str="normal"): @@ -2092,6 +2092,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "StyleModelLoader": "Load Style Model", "CLIPVisionLoader": "Load CLIP Vision", "UNETLoader": "Load Diffusion Model", + "unCLIPCheckpointLoader": "Load unCLIP Checkpoint", + "GLIGENLoader": "Load GLIGEN Model", # Conditioning "CLIPVisionEncode": "CLIP Vision Encode", "StyleModelApply": "Apply Style Model", @@ -2140,7 +2142,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "ImageSharpen": "Sharpen Image", "ImageScaleToTotalPixels": "Scale Image to Total Pixels", "GetImageSize": "Get Image Size", - # _for_testing + # experimental "VAEDecodeTiled": "VAE Decode (Tiled)", "VAEEncodeTiled": "VAE Encode (Tiled)", } diff --git a/tests/execution/testing_nodes/testing-pack/api_test_nodes.py b/tests/execution/testing_nodes/testing-pack/api_test_nodes.py index b2eaae05e..70c2a9e95 100644 --- a/tests/execution/testing_nodes/testing-pack/api_test_nodes.py +++ b/tests/execution/testing_nodes/testing-pack/api_test_nodes.py @@ -21,7 +21,7 @@ class TestAsyncProgressUpdate(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) FUNCTION = "execute" - CATEGORY = "_for_testing/async" + CATEGORY = "experimental/async" async def execute(self, value, sleep_seconds): start = time.time() @@ -51,7 +51,7 @@ class TestSyncProgressUpdate(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) FUNCTION = "execute" - CATEGORY = "_for_testing/async" + CATEGORY = "experimental/async" def execute(self, value, sleep_seconds): start = time.time() diff --git a/tests/execution/testing_nodes/testing-pack/async_test_nodes.py b/tests/execution/testing_nodes/testing-pack/async_test_nodes.py index 547eea6f4..589dabf17 100644 --- a/tests/execution/testing_nodes/testing-pack/async_test_nodes.py +++ b/tests/execution/testing_nodes/testing-pack/async_test_nodes.py @@ -21,7 +21,7 @@ class TestAsyncValidation(ComfyNodeABC): RETURN_TYPES = ("IMAGE",) FUNCTION = "process" - CATEGORY = "_for_testing/async" + CATEGORY = "experimental/async" @classmethod async def VALIDATE_INPUTS(cls, value, threshold): @@ -53,7 +53,7 @@ class TestAsyncError(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) FUNCTION = "error_execution" - CATEGORY = "_for_testing/async" + CATEGORY = "experimental/async" async def error_execution(self, value, error_after): await asyncio.sleep(error_after) @@ -74,7 +74,7 @@ class TestAsyncValidationError(ComfyNodeABC): RETURN_TYPES = ("IMAGE",) FUNCTION = "process" - CATEGORY = "_for_testing/async" + CATEGORY = "experimental/async" @classmethod async def VALIDATE_INPUTS(cls, value, max_value): @@ -105,7 +105,7 @@ class TestAsyncTimeout(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) FUNCTION = "timeout_execution" - CATEGORY = "_for_testing/async" + CATEGORY = "experimental/async" async def timeout_execution(self, value, timeout, operation_time): try: @@ -129,7 +129,7 @@ class TestSyncError(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) FUNCTION = "sync_error" - CATEGORY = "_for_testing/async" + CATEGORY = "experimental/async" def sync_error(self, value): raise RuntimeError("Intentional sync execution error for testing") @@ -150,7 +150,7 @@ class TestAsyncLazyCheck(ComfyNodeABC): RETURN_TYPES = ("IMAGE",) FUNCTION = "process" - CATEGORY = "_for_testing/async" + CATEGORY = "experimental/async" async def check_lazy_status(self, condition, input1, input2): # Simulate async checking (e.g., querying remote service) @@ -184,7 +184,7 @@ class TestDynamicAsyncGeneration(ComfyNodeABC): RETURN_TYPES = ("IMAGE",) FUNCTION = "generate_async_workflow" - CATEGORY = "_for_testing/async" + CATEGORY = "experimental/async" def generate_async_workflow(self, image1, image2, num_async_nodes, sleep_duration): g = GraphBuilder() @@ -229,7 +229,7 @@ class TestAsyncResourceUser(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) FUNCTION = "use_resource" - CATEGORY = "_for_testing/async" + CATEGORY = "experimental/async" async def use_resource(self, value, resource_id, duration): # Check if resource is already in use @@ -265,7 +265,7 @@ class TestAsyncBatchProcessing(ComfyNodeABC): RETURN_TYPES = ("IMAGE",) FUNCTION = "process_batch" - CATEGORY = "_for_testing/async" + CATEGORY = "experimental/async" async def process_batch(self, images, process_time_per_item, unique_id): batch_size = images.shape[0] @@ -305,7 +305,7 @@ class TestAsyncConcurrentLimit(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) FUNCTION = "limited_execution" - CATEGORY = "_for_testing/async" + CATEGORY = "experimental/async" async def limited_execution(self, value, duration, node_id): async with self._semaphore: diff --git a/tests/execution/testing_nodes/testing-pack/specific_tests.py b/tests/execution/testing_nodes/testing-pack/specific_tests.py index 4f8f01ae4..2eb5d520e 100644 --- a/tests/execution/testing_nodes/testing-pack/specific_tests.py +++ b/tests/execution/testing_nodes/testing-pack/specific_tests.py @@ -409,7 +409,7 @@ class TestSleep(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) FUNCTION = "sleep" - CATEGORY = "_for_testing" + CATEGORY = "experimental" async def sleep(self, value, seconds, unique_id): pbar = ProgressBar(seconds, node_id=unique_id) @@ -440,7 +440,7 @@ class TestParallelSleep(ComfyNodeABC): } RETURN_TYPES = ("IMAGE",) FUNCTION = "parallel_sleep" - CATEGORY = "_for_testing" + CATEGORY = "experimental" OUTPUT_NODE = True def parallel_sleep(self, image1, image2, image3, sleep1, sleep2, sleep3, unique_id): @@ -474,7 +474,7 @@ class TestOutputNodeWithSocketOutput: } RETURN_TYPES = ("IMAGE",) FUNCTION = "process" - CATEGORY = "_for_testing" + CATEGORY = "experimental" OUTPUT_NODE = True def process(self, image, value): From 56c74094c7c2ccbcf23f2aca1e4000199934da13 Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Fri, 8 May 2026 09:39:13 +0300 Subject: [PATCH 017/145] [Partner Nodes] use "adaptive" aspect ratio for SD2 nodes (#13800) Signed-off-by: bigcat88 --- comfy_api_nodes/nodes_bytedance.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/comfy_api_nodes/nodes_bytedance.py b/comfy_api_nodes/nodes_bytedance.py index 2f241a775..5f74f4a14 100644 --- a/comfy_api_nodes/nodes_bytedance.py +++ b/comfy_api_nodes/nodes_bytedance.py @@ -1271,7 +1271,7 @@ PRICE_BADGE_VIDEO = IO.PriceBadge( ) -def _seedance2_text_inputs(resolutions: list[str]): +def _seedance2_text_inputs(resolutions: list[str], default_ratio: str = "16:9"): return [ IO.String.Input( "prompt", @@ -1287,6 +1287,7 @@ def _seedance2_text_inputs(resolutions: list[str]): IO.Combo.Input( "ratio", options=["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], + default=default_ratio, tooltip="Aspect ratio of the output video.", ), IO.Int.Input( @@ -1420,8 +1421,14 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode): IO.DynamicCombo.Input( "model", options=[ - IO.DynamicCombo.Option("Seedance 2.0", _seedance2_text_inputs(["480p", "720p", "1080p"])), - IO.DynamicCombo.Option("Seedance 2.0 Fast", _seedance2_text_inputs(["480p", "720p"])), + IO.DynamicCombo.Option( + "Seedance 2.0", + _seedance2_text_inputs(["480p", "720p", "1080p"], default_ratio="adaptive"), + ), + IO.DynamicCombo.Option( + "Seedance 2.0 Fast", + _seedance2_text_inputs(["480p", "720p"], default_ratio="adaptive"), + ), ], tooltip="Seedance 2.0 for maximum quality; Seedance 2.0 Fast for speed optimization.", ), @@ -1588,9 +1595,9 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode): return IO.NodeOutput(await download_url_to_video_output(response.content.video_url)) -def _seedance2_reference_inputs(resolutions: list[str]): +def _seedance2_reference_inputs(resolutions: list[str], default_ratio: str = "16:9"): return [ - *_seedance2_text_inputs(resolutions), + *_seedance2_text_inputs(resolutions, default_ratio=default_ratio), IO.Autogrow.Input( "reference_images", template=IO.Autogrow.TemplateNames( @@ -1668,8 +1675,14 @@ class ByteDance2ReferenceNode(IO.ComfyNode): IO.DynamicCombo.Input( "model", options=[ - IO.DynamicCombo.Option("Seedance 2.0", _seedance2_reference_inputs(["480p", "720p", "1080p"])), - IO.DynamicCombo.Option("Seedance 2.0 Fast", _seedance2_reference_inputs(["480p", "720p"])), + IO.DynamicCombo.Option( + "Seedance 2.0", + _seedance2_reference_inputs(["480p", "720p", "1080p"], default_ratio="adaptive"), + ), + IO.DynamicCombo.Option( + "Seedance 2.0 Fast", + _seedance2_reference_inputs(["480p", "720p"], default_ratio="adaptive"), + ), ], tooltip="Seedance 2.0 for maximum quality; Seedance 2.0 Fast for speed optimization.", ), From bac6fc35fbf3fb2a6fc7e54fce17203215bcfff5 Mon Sep 17 00:00:00 2001 From: omahs <73983677+omahs@users.noreply.github.com> Date: Fri, 8 May 2026 11:14:45 +0200 Subject: [PATCH 018/145] Fix typos (#10986) --- comfy/hooks.py | 2 +- comfy/ldm/modules/diffusionmodules/util.py | 2 +- comfy_extras/nodes_flux.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/comfy/hooks.py b/comfy/hooks.py index 1a76c7ba4..5458fc3d8 100644 --- a/comfy/hooks.py +++ b/comfy/hooks.py @@ -93,7 +93,7 @@ class Hook: self.hook_scope = hook_scope '''Scope of where this hook should apply in terms of the conds used in sampling run.''' self.custom_should_register = default_should_register - '''Can be overriden with a compatible function to decide if this hook should be registered without the need to override .should_register''' + '''Can be overridden with a compatible function to decide if this hook should be registered without the need to override .should_register''' @property def strength(self): diff --git a/comfy/ldm/modules/diffusionmodules/util.py b/comfy/ldm/modules/diffusionmodules/util.py index 233011dc9..aed5c149c 100644 --- a/comfy/ldm/modules/diffusionmodules/util.py +++ b/comfy/ldm/modules/diffusionmodules/util.py @@ -140,7 +140,7 @@ def make_ddim_sampling_parameters(alphacums, ddim_timesteps, eta, verbose=True): alphas = alphacums[ddim_timesteps] alphas_prev = np.asarray([alphacums[0]] + alphacums[ddim_timesteps[:-1]].tolist()) - # according the the formula provided in https://arxiv.org/abs/2010.02502 + # according to the formula provided in https://arxiv.org/abs/2010.02502 sigmas = eta * np.sqrt((1 - alphas_prev) / (1 - alphas) * (1 - alphas / alphas_prev)) if verbose: logging.info(f'Selected alphas for ddim sampler: a_t: {alphas}; a_(t-1): {alphas_prev}') diff --git a/comfy_extras/nodes_flux.py b/comfy_extras/nodes_flux.py index 3a23c7d04..5e04a5f77 100644 --- a/comfy_extras/nodes_flux.py +++ b/comfy_extras/nodes_flux.py @@ -102,7 +102,7 @@ class FluxDisableGuidance(io.ComfyNode): append = execute # TODO: remove -PREFERED_KONTEXT_RESOLUTIONS = [ +PREFERRED_KONTEXT_RESOLUTIONS = [ (672, 1568), (688, 1504), (720, 1456), @@ -143,7 +143,7 @@ class FluxKontextImageScale(io.ComfyNode): width = image.shape[2] height = image.shape[1] aspect_ratio = width / height - _, width, height = min((abs(aspect_ratio - w / h), w, h) for w, h in PREFERED_KONTEXT_RESOLUTIONS) + _, width, height = min((abs(aspect_ratio - w / h), w, h) for w, h in PREFERRED_KONTEXT_RESOLUTIONS) image = comfy.utils.common_upscale(image.movedim(-1, 1), width, height, "lanczos", "center").movedim(1, -1) return io.NodeOutput(image) From d3c18c163665a6f94e7dc56823aabcb93ebf7e5e Mon Sep 17 00:00:00 2001 From: "Yousef R. Gamaleldin" <81116377+yousef-rafat@users.noreply.github.com> Date: Fri, 8 May 2026 12:59:24 +0300 Subject: [PATCH 019/145] Add support for BiRefNet background remove model (CORE-46) (#12747) --- comfy/background_removal/birefnet.json | 7 + comfy/background_removal/birefnet.py | 689 ++++++++++++++++++ comfy/bg_removal_model.py | 78 ++ comfy/ops.py | 22 + comfy_api/latest/_io.py | 7 + comfy_extras/nodes_bg_removal.py | 60 ++ comfy_extras/nodes_mask.py | 27 +- folder_paths.py | 2 + .../put_background_removal_models_here | 0 nodes.py | 1 + 10 files changed, 887 insertions(+), 6 deletions(-) create mode 100644 comfy/background_removal/birefnet.json create mode 100644 comfy/background_removal/birefnet.py create mode 100644 comfy/bg_removal_model.py create mode 100644 comfy_extras/nodes_bg_removal.py create mode 100644 models/background_removal/put_background_removal_models_here diff --git a/comfy/background_removal/birefnet.json b/comfy/background_removal/birefnet.json new file mode 100644 index 000000000..f0960af39 --- /dev/null +++ b/comfy/background_removal/birefnet.json @@ -0,0 +1,7 @@ +{ + "model_type": "birefnet", + "image_std": [1.0, 1.0, 1.0], + "image_mean": [0.0, 0.0, 0.0], + "image_size": 1024, + "resize_to_original": true +} diff --git a/comfy/background_removal/birefnet.py b/comfy/background_removal/birefnet.py new file mode 100644 index 000000000..df54b2b90 --- /dev/null +++ b/comfy/background_removal/birefnet.py @@ -0,0 +1,689 @@ +import torch +import comfy.ops +import numpy as np +import torch.nn as nn +from functools import partial +import torch.nn.functional as F +from torchvision.ops import deform_conv2d +from comfy.ldm.modules.attention import optimized_attention_for_device + +CXT = [3072, 1536, 768, 384][1:][::-1][-3:] + +class Attention(nn.Module): + def __init__(self, dim, num_heads=8, qkv_bias=False, qk_scale=None, device=None, dtype=None, operations=None): + super().__init__() + + self.dim = dim + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = qk_scale or head_dim ** -0.5 + + self.q = operations.Linear(dim, dim, bias=qkv_bias, device=device, dtype=dtype) + self.kv = operations.Linear(dim, dim * 2, bias=qkv_bias, device=device, dtype=dtype) + self.proj = operations.Linear(dim, dim, device=device, dtype=dtype) + + def forward(self, x): + B, N, C = x.shape + optimized_attention = optimized_attention_for_device(x.device, mask=False, small_input=True) + q = self.q(x).reshape(B, N, self.num_heads, C // self.num_heads).permute(0, 2, 1, 3) + kv = self.kv(x).reshape(B, -1, 2, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4) + k, v = kv[0], kv[1] + + x = optimized_attention( + q, k, v, heads=self.num_heads, skip_output_reshape=True, skip_reshape=True + ).transpose(1, 2).reshape(B, N, C) + x = self.proj(x) + + return x + +class Mlp(nn.Module): + def __init__(self, in_features, hidden_features=None, out_features=None, device=None, dtype=None, operations=None): + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.fc1 = operations.Linear(in_features, hidden_features, device=device, dtype=dtype) + self.act = nn.GELU() + self.fc2 = operations.Linear(hidden_features, out_features, device=device, dtype=dtype) + + def forward(self, x): + x = self.fc1(x) + x = self.act(x) + x = self.fc2(x) + return x + + +def window_partition(x, window_size): + B, H, W, C = x.shape + x = x.view(B, H // window_size, window_size, W // window_size, window_size, C) + windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C) + return windows + + +def window_reverse(windows, window_size, H, W): + B = int(windows.shape[0] / (H * W / window_size / window_size)) + x = windows.view(B, H // window_size, W // window_size, window_size, window_size, -1) + x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, H, W, -1) + return x + + +class WindowAttention(nn.Module): + def __init__(self, dim, window_size, num_heads, qkv_bias=True, qk_scale=None, device=None, dtype=None, operations=None): + + super().__init__() + self.dim = dim + self.window_size = window_size # Wh, Ww + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = qk_scale or head_dim ** -0.5 + + self.relative_position_bias_table = nn.Parameter( + torch.zeros((2 * window_size[0] - 1) * (2 * window_size[1] - 1), num_heads, device=device, dtype=dtype)) + + coords_h = torch.arange(self.window_size[0]) + coords_w = torch.arange(self.window_size[1]) + coords = torch.stack(torch.meshgrid([coords_h, coords_w], indexing='ij')) # 2, Wh, Ww + coords_flatten = torch.flatten(coords, 1) # 2, Wh*Ww + relative_coords = coords_flatten[:, :, None] - coords_flatten[:, None, :] # 2, Wh*Ww, Wh*Ww + relative_coords = relative_coords.permute(1, 2, 0).contiguous() # Wh*Ww, Wh*Ww, 2 + relative_coords[:, :, 0] += self.window_size[0] - 1 + relative_coords[:, :, 1] += self.window_size[1] - 1 + relative_coords[:, :, 0] *= 2 * self.window_size[1] - 1 + relative_position_index = relative_coords.sum(-1) # Wh*Ww, Wh*Ww + self.register_buffer("relative_position_index", relative_position_index) + + self.qkv = operations.Linear(dim, dim * 3, bias=qkv_bias, device=device, dtype=dtype) + self.proj = operations.Linear(dim, dim, device=device, dtype=dtype) + self.softmax = nn.Softmax(dim=-1) + + def forward(self, x, mask=None): + B_, N, C = x.shape + qkv = self.qkv(x).reshape(B_, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4) + q, k, v = qkv[0], qkv[1], qkv[2] + + q = q * self.scale + attn = (q @ k.transpose(-2, -1)) + + relative_position_bias = self.relative_position_bias_table[self.relative_position_index.long().view(-1)].view( + self.window_size[0] * self.window_size[1], self.window_size[0] * self.window_size[1], -1) # Wh*Ww,Wh*Ww,nH + relative_position_bias = relative_position_bias.permute(2, 0, 1).contiguous() # nH, Wh*Ww, Wh*Ww + attn = attn + relative_position_bias.unsqueeze(0) + + if mask is not None: + nW = mask.shape[0] + attn = attn.view(B_ // nW, nW, self.num_heads, N, N) + mask.unsqueeze(1).unsqueeze(0) + attn = attn.view(-1, self.num_heads, N, N) + attn = self.softmax(attn) + else: + attn = self.softmax(attn) + + x = (attn @ v).transpose(1, 2).reshape(B_, N, C) + x = self.proj(x) + return x + + +class SwinTransformerBlock(nn.Module): + def __init__(self, dim, num_heads, window_size=7, shift_size=0, + mlp_ratio=4., qkv_bias=True, qk_scale=None, + norm_layer=nn.LayerNorm, device=None, dtype=None, operations=None): + super().__init__() + self.dim = dim + self.num_heads = num_heads + self.window_size = window_size + self.shift_size = shift_size + self.mlp_ratio = mlp_ratio + + self.norm1 = norm_layer(dim, device=device, dtype=dtype) + self.attn = WindowAttention( + dim, window_size=(self.window_size, self.window_size), num_heads=num_heads, + qkv_bias=qkv_bias, qk_scale=qk_scale, device=device, dtype=dtype, operations=operations) + + self.norm2 = norm_layer(dim, device=device, dtype=dtype) + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, device=device, dtype=dtype, operations=operations) + + self.H = None + self.W = None + + def forward(self, x, mask_matrix): + B, L, C = x.shape + H, W = self.H, self.W + + shortcut = x + x = self.norm1(x) + x = x.view(B, H, W, C) + + pad_l = pad_t = 0 + pad_r = (self.window_size - W % self.window_size) % self.window_size + pad_b = (self.window_size - H % self.window_size) % self.window_size + x = F.pad(x, (0, 0, pad_l, pad_r, pad_t, pad_b)) + _, Hp, Wp, _ = x.shape + + if self.shift_size > 0: + shifted_x = torch.roll(x, shifts=(-self.shift_size, -self.shift_size), dims=(1, 2)) + attn_mask = mask_matrix + else: + shifted_x = x + attn_mask = None + + x_windows = window_partition(shifted_x, self.window_size) + x_windows = x_windows.view(-1, self.window_size * self.window_size, C) + + attn_windows = self.attn(x_windows, mask=attn_mask) + + attn_windows = attn_windows.view(-1, self.window_size, self.window_size, C) + shifted_x = window_reverse(attn_windows, self.window_size, Hp, Wp) # B H' W' C + + if self.shift_size > 0: + x = torch.roll(shifted_x, shifts=(self.shift_size, self.shift_size), dims=(1, 2)) + else: + x = shifted_x + + if pad_r > 0 or pad_b > 0: + x = x[:, :H, :W, :].contiguous() + + x = x.view(B, H * W, C) + + x = shortcut + x + x = x + self.mlp(self.norm2(x)) + + return x + + +class PatchMerging(nn.Module): + def __init__(self, dim, device=None, dtype=None, operations=None): + super().__init__() + self.dim = dim + self.reduction = operations.Linear(4 * dim, 2 * dim, bias=False, device=device, dtype=dtype) + self.norm = operations.LayerNorm(4 * dim, device=device, dtype=dtype) + + def forward(self, x, H, W): + B, L, C = x.shape + x = x.view(B, H, W, C) + + # padding + pad_input = (H % 2 == 1) or (W % 2 == 1) + if pad_input: + x = F.pad(x, (0, 0, 0, W % 2, 0, H % 2)) + + x0 = x[:, 0::2, 0::2, :] # B H/2 W/2 C + x1 = x[:, 1::2, 0::2, :] # B H/2 W/2 C + x2 = x[:, 0::2, 1::2, :] # B H/2 W/2 C + x3 = x[:, 1::2, 1::2, :] # B H/2 W/2 C + x = torch.cat([x0, x1, x2, x3], -1) # B H/2 W/2 4*C + x = x.view(B, -1, 4 * C) # B H/2*W/2 4*C + + x = self.norm(x) + x = self.reduction(x) + + return x + + +class BasicLayer(nn.Module): + def __init__(self, + dim, + depth, + num_heads, + window_size=7, + mlp_ratio=4., + qkv_bias=True, + qk_scale=None, + norm_layer=nn.LayerNorm, + downsample=None, + device=None, dtype=None, operations=None): + super().__init__() + self.window_size = window_size + self.shift_size = window_size // 2 + self.depth = depth + + # build blocks + self.blocks = nn.ModuleList([ + SwinTransformerBlock( + dim=dim, + num_heads=num_heads, + window_size=window_size, + shift_size=0 if (i % 2 == 0) else window_size // 2, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + norm_layer=norm_layer, + device=device, dtype=dtype, operations=operations) + for i in range(depth)]) + + # patch merging layer + if downsample is not None: + self.downsample = downsample(dim=dim, device=device, dtype=dtype, operations=operations) + else: + self.downsample = None + + def forward(self, x, H, W): + Hp = int(np.ceil(H / self.window_size)) * self.window_size + Wp = int(np.ceil(W / self.window_size)) * self.window_size + img_mask = torch.zeros((1, Hp, Wp, 1), device=x.device) # 1 Hp Wp 1 + h_slices = (slice(0, -self.window_size), + slice(-self.window_size, -self.shift_size), + slice(-self.shift_size, None)) + w_slices = (slice(0, -self.window_size), + slice(-self.window_size, -self.shift_size), + slice(-self.shift_size, None)) + cnt = 0 + for h in h_slices: + for w in w_slices: + img_mask[:, h, w, :] = cnt + cnt += 1 + + mask_windows = window_partition(img_mask, self.window_size) + mask_windows = mask_windows.view(-1, self.window_size * self.window_size) + attn_mask = mask_windows.unsqueeze(1) - mask_windows.unsqueeze(2) + attn_mask = attn_mask.masked_fill(attn_mask != 0, float(-100.0)).masked_fill(attn_mask == 0, float(0.0)) + + for blk in self.blocks: + blk.H, blk.W = H, W + x = blk(x, attn_mask) + if self.downsample is not None: + x_down = self.downsample(x, H, W) + Wh, Ww = (H + 1) // 2, (W + 1) // 2 + return x, H, W, x_down, Wh, Ww + else: + return x, H, W, x, H, W + + +class PatchEmbed(nn.Module): + def __init__(self, patch_size=4, in_channels=3, embed_dim=96, norm_layer=None, device=None, dtype=None, operations=None): + super().__init__() + patch_size = (patch_size, patch_size) + self.patch_size = patch_size + + self.in_channels = in_channels + self.embed_dim = embed_dim + + self.proj = operations.Conv2d(in_channels, embed_dim, kernel_size=patch_size, stride=patch_size, device=device, dtype=dtype) + if norm_layer is not None: + self.norm = norm_layer(embed_dim, device=device, dtype=dtype) + else: + self.norm = None + + def forward(self, x): + _, _, H, W = x.size() + if W % self.patch_size[1] != 0: + x = F.pad(x, (0, self.patch_size[1] - W % self.patch_size[1])) + if H % self.patch_size[0] != 0: + x = F.pad(x, (0, 0, 0, self.patch_size[0] - H % self.patch_size[0])) + + x = self.proj(x) # B C Wh Ww + if self.norm is not None: + Wh, Ww = x.size(2), x.size(3) + x = x.flatten(2).transpose(1, 2) + x = self.norm(x) + x = x.transpose(1, 2).view(-1, self.embed_dim, Wh, Ww) + + return x + + +class SwinTransformer(nn.Module): + def __init__(self, + pretrain_img_size=224, + patch_size=4, + in_channels=3, + embed_dim=96, + depths=[2, 2, 6, 2], + num_heads=[3, 6, 12, 24], + window_size=7, + mlp_ratio=4., + qkv_bias=True, + qk_scale=None, + patch_norm=True, + out_indices=(0, 1, 2, 3), + frozen_stages=-1, + device=None, dtype=None, operations=None): + super().__init__() + + norm_layer = partial(operations.LayerNorm, device=device, dtype=dtype) + self.pretrain_img_size = pretrain_img_size + self.num_layers = len(depths) + self.embed_dim = embed_dim + self.patch_norm = patch_norm + self.out_indices = out_indices + self.frozen_stages = frozen_stages + + self.patch_embed = PatchEmbed( + patch_size=patch_size, in_channels=in_channels, embed_dim=embed_dim, + device=device, dtype=dtype, operations=operations, + norm_layer=norm_layer if self.patch_norm else None) + + self.layers = nn.ModuleList() + for i_layer in range(self.num_layers): + layer = BasicLayer( + dim=int(embed_dim * 2 ** i_layer), + depth=depths[i_layer], + num_heads=num_heads[i_layer], + window_size=window_size, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + norm_layer=norm_layer, + downsample=PatchMerging if (i_layer < self.num_layers - 1) else None, + device=device, dtype=dtype, operations=operations) + self.layers.append(layer) + + num_features = [int(embed_dim * 2 ** i) for i in range(self.num_layers)] + self.num_features = num_features + + for i_layer in out_indices: + layer = norm_layer(num_features[i_layer]) + layer_name = f'norm{i_layer}' + self.add_module(layer_name, layer) + + + def forward(self, x): + x = self.patch_embed(x) + + Wh, Ww = x.size(2), x.size(3) + + outs = [] + x = x.flatten(2).transpose(1, 2) + for i in range(self.num_layers): + layer = self.layers[i] + x_out, H, W, x, Wh, Ww = layer(x, Wh, Ww) + + if i in self.out_indices: + norm_layer = getattr(self, f'norm{i}') + x_out = norm_layer(x_out) + + out = x_out.view(-1, H, W, self.num_features[i]).permute(0, 3, 1, 2).contiguous() + outs.append(out) + + return tuple(outs) + +class DeformableConv2d(nn.Module): + def __init__(self, + in_channels, + out_channels, + kernel_size=3, + stride=1, + padding=1, + bias=False, device=None, dtype=None, operations=None): + + super(DeformableConv2d, self).__init__() + + kernel_size = kernel_size if type(kernel_size) is tuple else (kernel_size, kernel_size) + self.stride = stride if type(stride) is tuple else (stride, stride) + self.padding = padding + + self.offset_conv = operations.Conv2d(in_channels, + 2 * kernel_size[0] * kernel_size[1], + kernel_size=kernel_size, + stride=stride, + padding=self.padding, + bias=True, device=device, dtype=dtype) + + self.modulator_conv = operations.Conv2d(in_channels, + 1 * kernel_size[0] * kernel_size[1], + kernel_size=kernel_size, + stride=stride, + padding=self.padding, + bias=True, device=device, dtype=dtype) + + self.regular_conv = operations.Conv2d(in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=self.padding, + bias=bias, device=device, dtype=dtype) + + def forward(self, x): + offset = self.offset_conv(x) + modulator = 2. * torch.sigmoid(self.modulator_conv(x)) + weight, bias, offload_info = comfy.ops.cast_bias_weight(self.regular_conv, x, offloadable=True) + + x = deform_conv2d( + input=x, + offset=offset, + weight=weight, + bias=None, + padding=self.padding, + mask=modulator, + stride=self.stride, + ) + comfy.ops.uncast_bias_weight(self.regular_conv, weight, bias, offload_info) + return x + +class BasicDecBlk(nn.Module): + def __init__(self, in_channels=64, out_channels=64, inter_channels=64, device=None, dtype=None, operations=None): + super(BasicDecBlk, self).__init__() + inter_channels = 64 + self.conv_in = operations.Conv2d(in_channels, inter_channels, 3, 1, padding=1, device=device, dtype=dtype) + self.relu_in = nn.ReLU(inplace=True) + self.dec_att = ASPPDeformable(in_channels=inter_channels, device=device, dtype=dtype, operations=operations) + self.conv_out = operations.Conv2d(inter_channels, out_channels, 3, 1, padding=1, device=device, dtype=dtype) + self.bn_in = operations.BatchNorm2d(inter_channels, device=device, dtype=dtype) + self.bn_out = operations.BatchNorm2d(out_channels, device=device, dtype=dtype) + + def forward(self, x): + x = self.conv_in(x) + x = self.bn_in(x) + x = self.relu_in(x) + x = self.dec_att(x) + x = self.conv_out(x) + x = self.bn_out(x) + return x + + +class BasicLatBlk(nn.Module): + def __init__(self, in_channels=64, out_channels=64, device=None, dtype=None, operations=None): + super(BasicLatBlk, self).__init__() + self.conv = operations.Conv2d(in_channels, out_channels, 1, 1, 0, device=device, dtype=dtype) + + def forward(self, x): + x = self.conv(x) + return x + + +class _ASPPModuleDeformable(nn.Module): + def __init__(self, in_channels, planes, kernel_size, padding, device, dtype, operations): + super(_ASPPModuleDeformable, self).__init__() + self.atrous_conv = DeformableConv2d(in_channels, planes, kernel_size=kernel_size, + stride=1, padding=padding, bias=False, device=device, dtype=dtype, operations=operations) + self.bn = operations.BatchNorm2d(planes, device=device, dtype=dtype) + self.relu = nn.ReLU(inplace=True) + + def forward(self, x): + x = self.atrous_conv(x) + x = self.bn(x) + + return self.relu(x) + + +class ASPPDeformable(nn.Module): + def __init__(self, in_channels, out_channels=None, parallel_block_sizes=[1, 3, 7], device=None, dtype=None, operations=None): + super(ASPPDeformable, self).__init__() + self.down_scale = 1 + if out_channels is None: + out_channels = in_channels + self.in_channelster = 256 // self.down_scale + + self.aspp1 = _ASPPModuleDeformable(in_channels, self.in_channelster, 1, padding=0, device=device, dtype=dtype, operations=operations) + self.aspp_deforms = nn.ModuleList([ + _ASPPModuleDeformable(in_channels, self.in_channelster, conv_size, padding=int(conv_size//2), device=device, dtype=dtype, operations=operations) + for conv_size in parallel_block_sizes + ]) + + self.global_avg_pool = nn.Sequential(nn.AdaptiveAvgPool2d((1, 1)), + operations.Conv2d(in_channels, self.in_channelster, 1, stride=1, bias=False, device=device, dtype=dtype), + operations.BatchNorm2d(self.in_channelster, device=device, dtype=dtype), + nn.ReLU(inplace=True)) + self.conv1 = operations.Conv2d(self.in_channelster * (2 + len(self.aspp_deforms)), out_channels, 1, bias=False, device=device, dtype=dtype) + self.bn1 = operations.BatchNorm2d(out_channels, device=device, dtype=dtype) + self.relu = nn.ReLU(inplace=True) + + def forward(self, x): + x1 = self.aspp1(x) + x_aspp_deforms = [aspp_deform(x) for aspp_deform in self.aspp_deforms] + x5 = self.global_avg_pool(x) + x5 = F.interpolate(x5, size=x1.size()[2:], mode='bilinear', align_corners=True) + x = torch.cat((x1, *x_aspp_deforms, x5), dim=1) + + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + + return x + +class BiRefNet(nn.Module): + def __init__(self, config=None, dtype=None, device=None, operations=None): + super(BiRefNet, self).__init__() + self.bb = SwinTransformer(embed_dim=192, depths=[2, 2, 18, 2], num_heads=[6, 12, 24, 48], window_size=12, device=device, dtype=dtype, operations=operations) + + channels = [1536, 768, 384, 192] + channels = [c * 2 for c in channels] + self.cxt = channels[1:][::-1][-3:] + self.squeeze_module = nn.Sequential(*[ + BasicDecBlk(channels[0]+sum(self.cxt), channels[0], device=device, dtype=dtype, operations=operations) + for _ in range(1) + ]) + + self.decoder = Decoder(channels, device=device, dtype=dtype, operations=operations) + + def forward_enc(self, x): + x1, x2, x3, x4 = self.bb(x) + B, C, H, W = x.shape + x1_, x2_, x3_, x4_ = self.bb(F.interpolate(x, size=(H//2, W//2), mode='bilinear', align_corners=True)) + x1 = torch.cat([x1, F.interpolate(x1_, size=x1.shape[2:], mode='bilinear', align_corners=True)], dim=1) + x2 = torch.cat([x2, F.interpolate(x2_, size=x2.shape[2:], mode='bilinear', align_corners=True)], dim=1) + x3 = torch.cat([x3, F.interpolate(x3_, size=x3.shape[2:], mode='bilinear', align_corners=True)], dim=1) + x4 = torch.cat([x4, F.interpolate(x4_, size=x4.shape[2:], mode='bilinear', align_corners=True)], dim=1) + x4 = torch.cat( + ( + *[ + F.interpolate(x1, size=x4.shape[2:], mode='bilinear', align_corners=True), + F.interpolate(x2, size=x4.shape[2:], mode='bilinear', align_corners=True), + F.interpolate(x3, size=x4.shape[2:], mode='bilinear', align_corners=True), + ][-len(CXT):], + x4 + ), + dim=1 + ) + return (x1, x2, x3, x4) + + def forward_ori(self, x): + (x1, x2, x3, x4) = self.forward_enc(x) + x4 = self.squeeze_module(x4) + features = [x, x1, x2, x3, x4] + scaled_preds = self.decoder(features) + return scaled_preds + + def forward(self, pixel_values, intermediate_output=None): + scaled_preds = self.forward_ori(pixel_values) + return scaled_preds + + +class Decoder(nn.Module): + def __init__(self, channels, device, dtype, operations): + super(Decoder, self).__init__() + # factory kwargs + fk = {"device":device, "dtype":dtype, "operations":operations} + DecoderBlock = partial(BasicDecBlk, **fk) + LateralBlock = partial(BasicLatBlk, **fk) + DBlock = partial(SimpleConvs, **fk) + + self.split = True + N_dec_ipt = 64 + ic = 64 + ipt_cha_opt = 1 + self.ipt_blk5 = DBlock(2**10*3 if self.split else 3, [N_dec_ipt, channels[0]//8][ipt_cha_opt], inter_channels=ic) + self.ipt_blk4 = DBlock(2**8*3 if self.split else 3, [N_dec_ipt, channels[0]//8][ipt_cha_opt], inter_channels=ic) + self.ipt_blk3 = DBlock(2**6*3 if self.split else 3, [N_dec_ipt, channels[1]//8][ipt_cha_opt], inter_channels=ic) + self.ipt_blk2 = DBlock(2**4*3 if self.split else 3, [N_dec_ipt, channels[2]//8][ipt_cha_opt], inter_channels=ic) + self.ipt_blk1 = DBlock(2**0*3 if self.split else 3, [N_dec_ipt, channels[3]//8][ipt_cha_opt], inter_channels=ic) + + self.decoder_block4 = DecoderBlock(channels[0]+([N_dec_ipt, channels[0]//8][ipt_cha_opt]), channels[1]) + self.decoder_block3 = DecoderBlock(channels[1]+([N_dec_ipt, channels[0]//8][ipt_cha_opt]), channels[2]) + self.decoder_block2 = DecoderBlock(channels[2]+([N_dec_ipt, channels[1]//8][ipt_cha_opt]), channels[3]) + self.decoder_block1 = DecoderBlock(channels[3]+([N_dec_ipt, channels[2]//8][ipt_cha_opt]), channels[3]//2) + + fk = {"device":device, "dtype":dtype} + + self.conv_out1 = nn.Sequential(operations.Conv2d(channels[3]//2+([N_dec_ipt, channels[3]//8][ipt_cha_opt]), 1, 1, 1, 0, **fk)) + + self.lateral_block4 = LateralBlock(channels[1], channels[1]) + self.lateral_block3 = LateralBlock(channels[2], channels[2]) + self.lateral_block2 = LateralBlock(channels[3], channels[3]) + + self.conv_ms_spvn_4 = operations.Conv2d(channels[1], 1, 1, 1, 0, **fk) + self.conv_ms_spvn_3 = operations.Conv2d(channels[2], 1, 1, 1, 0, **fk) + self.conv_ms_spvn_2 = operations.Conv2d(channels[3], 1, 1, 1, 0, **fk) + + _N = 16 + + self.gdt_convs_4 = nn.Sequential(operations.Conv2d(channels[0] // 2, _N, 3, 1, 1, **fk), operations.BatchNorm2d(_N, **fk), nn.ReLU(inplace=True)) + self.gdt_convs_3 = nn.Sequential(operations.Conv2d(channels[1] // 2, _N, 3, 1, 1, **fk), operations.BatchNorm2d(_N, **fk), nn.ReLU(inplace=True)) + self.gdt_convs_2 = nn.Sequential(operations.Conv2d(channels[2] // 2, _N, 3, 1, 1, **fk), operations.BatchNorm2d(_N, **fk), nn.ReLU(inplace=True)) + + [setattr(self, f"gdt_convs_pred_{i}", nn.Sequential(operations.Conv2d(_N, 1, 1, 1, 0, **fk))) for i in range(2, 5)] + [setattr(self, f"gdt_convs_attn_{i}", nn.Sequential(operations.Conv2d(_N, 1, 1, 1, 0, **fk))) for i in range(2, 5)] + + def get_patches_batch(self, x, p): + _size_h, _size_w = p.shape[2:] + patches_batch = [] + for idx in range(x.shape[0]): + columns_x = torch.split(x[idx], split_size_or_sections=_size_w, dim=-1) + patches_x = [] + for column_x in columns_x: + patches_x += [p.unsqueeze(0) for p in torch.split(column_x, split_size_or_sections=_size_h, dim=-2)] + patch_sample = torch.cat(patches_x, dim=1) + patches_batch.append(patch_sample) + return torch.cat(patches_batch, dim=0) + + def forward(self, features): + x, x1, x2, x3, x4 = features + + patches_batch = self.get_patches_batch(x, x4) if self.split else x + x4 = torch.cat((x4, self.ipt_blk5(F.interpolate(patches_batch, size=x4.shape[2:], mode='bilinear', align_corners=True))), 1) + p4 = self.decoder_block4(x4) + p4_gdt = self.gdt_convs_4(p4) + gdt_attn_4 = self.gdt_convs_attn_4(p4_gdt).sigmoid() + p4 = p4 * gdt_attn_4 + _p4 = F.interpolate(p4, size=x3.shape[2:], mode='bilinear', align_corners=True) + _p3 = _p4 + self.lateral_block4(x3) + + patches_batch = self.get_patches_batch(x, _p3) if self.split else x + _p3 = torch.cat((_p3, self.ipt_blk4(F.interpolate(patches_batch, size=x3.shape[2:], mode='bilinear', align_corners=True))), 1) + p3 = self.decoder_block3(_p3) + + p3_gdt = self.gdt_convs_3(p3) + gdt_attn_3 = self.gdt_convs_attn_3(p3_gdt).sigmoid() + p3 = p3 * gdt_attn_3 + _p3 = F.interpolate(p3, size=x2.shape[2:], mode='bilinear', align_corners=True) + _p2 = _p3 + self.lateral_block3(x2) + + patches_batch = self.get_patches_batch(x, _p2) if self.split else x + _p2 = torch.cat((_p2, self.ipt_blk3(F.interpolate(patches_batch, size=x2.shape[2:], mode='bilinear', align_corners=True))), 1) + p2 = self.decoder_block2(_p2) + + p2_gdt = self.gdt_convs_2(p2) + gdt_attn_2 = self.gdt_convs_attn_2(p2_gdt).sigmoid() + p2 = p2 * gdt_attn_2 + + _p2 = F.interpolate(p2, size=x1.shape[2:], mode='bilinear', align_corners=True) + _p1 = _p2 + self.lateral_block2(x1) + + patches_batch = self.get_patches_batch(x, _p1) if self.split else x + _p1 = torch.cat((_p1, self.ipt_blk2(F.interpolate(patches_batch, size=x1.shape[2:], mode='bilinear', align_corners=True))), 1) + _p1 = self.decoder_block1(_p1) + _p1 = F.interpolate(_p1, size=x.shape[2:], mode='bilinear', align_corners=True) + + patches_batch = self.get_patches_batch(x, _p1) if self.split else x + _p1 = torch.cat((_p1, self.ipt_blk1(F.interpolate(patches_batch, size=x.shape[2:], mode='bilinear', align_corners=True))), 1) + p1_out = self.conv_out1(_p1) + return p1_out + + +class SimpleConvs(nn.Module): + def __init__( + self, in_channels: int, out_channels: int, inter_channels=64, device=None, dtype=None, operations=None + ) -> None: + super().__init__() + self.conv1 = operations.Conv2d(in_channels, inter_channels, 3, 1, 1, device=device, dtype=dtype) + self.conv_out = operations.Conv2d(inter_channels, out_channels, 3, 1, 1, device=device, dtype=dtype) + + def forward(self, x): + return self.conv_out(self.conv1(x)) diff --git a/comfy/bg_removal_model.py b/comfy/bg_removal_model.py new file mode 100644 index 000000000..cb7c2ee53 --- /dev/null +++ b/comfy/bg_removal_model.py @@ -0,0 +1,78 @@ +from .utils import load_torch_file +import os +import json +import torch +import logging + +import comfy.ops +import comfy.model_patcher +import comfy.model_management +import comfy.clip_model +import comfy.background_removal.birefnet + +BG_REMOVAL_MODELS = { + "birefnet": comfy.background_removal.birefnet.BiRefNet +} + +class BackgroundRemovalModel(): + def __init__(self, json_config): + with open(json_config) as f: + config = json.load(f) + + self.image_size = config.get("image_size", 1024) + self.image_mean = config.get("image_mean", [0.0, 0.0, 0.0]) + self.image_std = config.get("image_std", [1.0, 1.0, 1.0]) + self.model_type = config.get("model_type", "birefnet") + self.config = config.copy() + model_class = BG_REMOVAL_MODELS.get(self.model_type) + + self.load_device = comfy.model_management.text_encoder_device() + offload_device = comfy.model_management.text_encoder_offload_device() + self.dtype = comfy.model_management.text_encoder_dtype(self.load_device) + self.model = model_class(config, self.dtype, offload_device, comfy.ops.manual_cast) + self.model.eval() + + self.patcher = comfy.model_patcher.CoreModelPatcher(self.model, load_device=self.load_device, offload_device=offload_device) + + def load_sd(self, sd): + return self.model.load_state_dict(sd, strict=False, assign=self.patcher.is_dynamic()) + + def get_sd(self): + return self.model.state_dict() + + def encode_image(self, image): + comfy.model_management.load_model_gpu(self.patcher) + H, W = image.shape[1], image.shape[2] + pixel_values = comfy.clip_model.clip_preprocess(image.to(self.load_device), size=self.image_size, mean=self.image_mean, std=self.image_std, crop=False) + out = self.model(pixel_values=pixel_values) + out = torch.nn.functional.interpolate(out, size=(H, W), mode="bicubic", antialias=False) + + mask = out.sigmoid() + if mask.ndim == 3: + mask = mask.unsqueeze(0) + if mask.shape[1] != 1: + mask = mask.movedim(-1, 1) + + return mask + + +def load_background_removal_model(sd): + if "bb.layers.1.blocks.0.attn.relative_position_index" in sd: + json_config = os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "background_removal"), "birefnet.json") + else: + return None + + bg_model = BackgroundRemovalModel(json_config) + m, u = bg_model.load_sd(sd) + if len(m) > 0: + logging.warning("missing background removal: {}".format(m)) + u = set(u) + keys = list(sd.keys()) + for k in keys: + if k not in u: + sd.pop(k) + return bg_model + +def load(ckpt_path): + sd = load_torch_file(ckpt_path) + return load_background_removal_model(sd) diff --git a/comfy/ops.py b/comfy/ops.py index 585c185a3..77ad1d527 100644 --- a/comfy/ops.py +++ b/comfy/ops.py @@ -562,6 +562,25 @@ class disable_weight_init: else: return super().forward(*args, **kwargs) + class BatchNorm2d(torch.nn.BatchNorm2d, CastWeightBiasOp): + def reset_parameters(self): + return None + + def forward_comfy_cast_weights(self, input): + weight, bias, offload_stream = cast_bias_weight(self, input, offloadable=True) + running_mean = self.running_mean.to(device=input.device, dtype=weight.dtype) if self.running_mean is not None else None + running_var = self.running_var.to(device=input.device, dtype=weight.dtype) if self.running_var is not None else None + x = torch.nn.functional.batch_norm(input, running_mean, running_var, weight, bias, self.training, self.momentum, self.eps) + uncast_bias_weight(self, weight, bias, offload_stream) + return x + + def forward(self, *args, **kwargs): + run_every_op() + if self.comfy_cast_weights or len(self.weight_function) > 0 or len(self.bias_function) > 0: + return self.forward_comfy_cast_weights(*args, **kwargs) + else: + return super().forward(*args, **kwargs) + class LayerNorm(torch.nn.LayerNorm, CastWeightBiasOp): def reset_parameters(self): return None @@ -749,6 +768,9 @@ class manual_cast(disable_weight_init): class Conv3d(disable_weight_init.Conv3d): comfy_cast_weights = True + class BatchNorm2d(disable_weight_init.BatchNorm2d): + comfy_cast_weights = True + class GroupNorm(disable_weight_init.GroupNorm): comfy_cast_weights = True diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py index e50266bc5..5ed968960 100644 --- a/comfy_api/latest/_io.py +++ b/comfy_api/latest/_io.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: from spandrel import ImageModelDescriptor from comfy.clip_vision import ClipVisionModel from comfy.clip_vision import Output as ClipVisionOutput_ + from comfy.bg_removal_model import BackgroundRemovalModel from comfy.controlnet import ControlNet from comfy.hooks import HookGroup, HookKeyframeGroup from comfy.model_patcher import ModelPatcher @@ -614,6 +615,11 @@ class Model(ComfyTypeIO): if TYPE_CHECKING: Type = ModelPatcher +@comfytype(io_type="BACKGROUND_REMOVAL") +class BackgroundRemoval(ComfyTypeIO): + if TYPE_CHECKING: + Type = BackgroundRemovalModel + @comfytype(io_type="CLIP_VISION") class ClipVision(ComfyTypeIO): if TYPE_CHECKING: @@ -2257,6 +2263,7 @@ __all__ = [ "ModelPatch", "ClipVision", "ClipVisionOutput", + "BackgroundRemoval", "AudioEncoder", "AudioEncoderOutput", "StyleModel", diff --git a/comfy_extras/nodes_bg_removal.py b/comfy_extras/nodes_bg_removal.py new file mode 100644 index 000000000..8d046b8d4 --- /dev/null +++ b/comfy_extras/nodes_bg_removal.py @@ -0,0 +1,60 @@ +import folder_paths +from typing_extensions import override +from comfy_api.latest import ComfyExtension, IO +from comfy.bg_removal_model import load + + +class LoadBackgroundRemovalModel(IO.ComfyNode): + @classmethod + def define_schema(cls): + files = folder_paths.get_filename_list("background_removal") + return IO.Schema( + node_id="LoadBackgroundRemovalModel", + display_name="Load Background Removal Model", + category="loaders", + inputs=[ + IO.Combo.Input("bg_removal_name", options=sorted(files), tooltip="The model used to remove backgrounds from images"), + ], + outputs=[ + IO.BackgroundRemoval.Output("bg_model") + ] + ) + @classmethod + def execute(cls, bg_removal_name): + path = folder_paths.get_full_path_or_raise("background_removal", bg_removal_name) + bg = load(path) + if bg is None: + raise RuntimeError("ERROR: background model file is invalid and does not contain a valid background removal model.") + return IO.NodeOutput(bg) + +class RemoveBackground(IO.ComfyNode): + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="RemoveBackground", + display_name="Remove Background", + category="image/background removal", + inputs=[ + IO.Image.Input("image", tooltip="Input image to remove the background from"), + IO.BackgroundRemoval.Input("bg_removal_model", tooltip="Background removal model used to generate the mask") + ], + outputs=[ + IO.Mask.Output("mask", tooltip="Generated foreground mask") + ] + ) + @classmethod + def execute(cls, image, bg_removal_model): + mask = bg_removal_model.encode_image(image) + return IO.NodeOutput(mask) + +class BackgroundRemovalExtension(ComfyExtension): + @override + async def get_node_list(self) -> list[type[IO.ComfyNode]]: + return [ + LoadBackgroundRemovalModel, + RemoveBackground + ] + + +async def comfy_entrypoint() -> BackgroundRemovalExtension: + return BackgroundRemovalExtension() diff --git a/comfy_extras/nodes_mask.py b/comfy_extras/nodes_mask.py index 43a933dac..c9b2a84d9 100644 --- a/comfy_extras/nodes_mask.py +++ b/comfy_extras/nodes_mask.py @@ -40,10 +40,21 @@ def composite(destination, source, x, y, mask = None, multiplier = 8, resize_sou inverse_mask = torch.ones_like(mask) - mask - source_portion = mask * source[..., :visible_height, :visible_width] - destination_portion = inverse_mask * destination[..., top:bottom, left:right] + source_rgb = source[:, :3, :visible_height, :visible_width] + dest_slice = destination[..., top:bottom, left:right] + + if destination.shape[1] == 4: + if torch.max(dest_slice) == 0: + destination[:, :3, top:bottom, left:right] = source_rgb + destination[:, 3:4, top:bottom, left:right] = mask + else: + destination[:, :3, top:bottom, left:right] = (mask * source_rgb) + (inverse_mask * dest_slice[:, :3]) + destination[:, 3:4, top:bottom, left:right] = torch.max(mask, dest_slice[:, 3:4]) + else: + source_portion = mask * source_rgb + destination_portion = inverse_mask * dest_slice + destination[..., top:bottom, left:right] = source_portion + destination_portion - destination[..., top:bottom, left:right] = source_portion + destination_portion return destination class LatentCompositeMasked(IO.ComfyNode): @@ -84,18 +95,23 @@ class ImageCompositeMasked(IO.ComfyNode): display_name="Image Composite Masked", category="image", inputs=[ - IO.Image.Input("destination"), IO.Image.Input("source"), IO.Int.Input("x", default=0, min=0, max=nodes.MAX_RESOLUTION, step=1), IO.Int.Input("y", default=0, min=0, max=nodes.MAX_RESOLUTION, step=1), IO.Boolean.Input("resize_source", default=False), + IO.Image.Input("destination", optional=True), IO.Mask.Input("mask", optional=True), ], outputs=[IO.Image.Output()], ) @classmethod - def execute(cls, destination, source, x, y, resize_source, mask = None) -> IO.NodeOutput: + def execute(cls, source, x, y, resize_source, destination = None, mask = None) -> IO.NodeOutput: + if destination is None: # transparent rgba + B, H, W, C = source.shape + destination = torch.zeros((B, H, W, 4), dtype=source.dtype, device=source.device) + if C == 3: + source = torch.nn.functional.pad(source, (0, 1), value=1.0) destination, source = node_helpers.image_alpha_fix(destination, source) destination = destination.clone().movedim(-1, 1) output = composite(destination, source.movedim(-1, 1), x, y, mask, 1, resize_source).movedim(1, -1) @@ -381,7 +397,6 @@ class GrowMask(IO.ComfyNode): expand_mask = execute # TODO: remove - class ThresholdMask(IO.ComfyNode): @classmethod def define_schema(cls): diff --git a/folder_paths.py b/folder_paths.py index 98d3b1880..92e8df3cf 100644 --- a/folder_paths.py +++ b/folder_paths.py @@ -52,6 +52,8 @@ folder_names_and_paths["model_patches"] = ([os.path.join(models_dir, "model_patc folder_names_and_paths["audio_encoders"] = ([os.path.join(models_dir, "audio_encoders")], supported_pt_extensions) +folder_names_and_paths["background_removal"] = ([os.path.join(models_dir, "background_removal")], supported_pt_extensions) + folder_names_and_paths["frame_interpolation"] = ([os.path.join(models_dir, "frame_interpolation")], supported_pt_extensions) folder_names_and_paths["optical_flow"] = ([os.path.join(models_dir, "optical_flow")], supported_pt_extensions) diff --git a/models/background_removal/put_background_removal_models_here b/models/background_removal/put_background_removal_models_here new file mode 100644 index 000000000..e69de29bb diff --git a/nodes.py b/nodes.py index ae9e70cb9..5755f0bb8 100644 --- a/nodes.py +++ b/nodes.py @@ -2429,6 +2429,7 @@ async def init_builtin_extra_nodes(): "nodes_number_convert.py", "nodes_painter.py", "nodes_curve.py", + "nodes_bg_removal.py", "nodes_rtdetr.py", "nodes_frame_interpolation.py", "nodes_sam3.py", From 05cd076bc1d9386ec77414c96d1460008f653f7c Mon Sep 17 00:00:00 2001 From: drozbay <17261091+drozbay@users.noreply.github.com> Date: Fri, 8 May 2026 08:48:59 -0600 Subject: [PATCH 020/145] fix: Make LTXVAddGuide center-crop guide images to match other LTXV nodes (#13794) --- comfy_extras/nodes_lt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy_extras/nodes_lt.py b/comfy_extras/nodes_lt.py index ab1359fdb..f1f4d5319 100644 --- a/comfy_extras/nodes_lt.py +++ b/comfy_extras/nodes_lt.py @@ -236,7 +236,7 @@ class LTXVAddGuide(io.ComfyNode): def encode(cls, vae, latent_width, latent_height, images, scale_factors): time_scale_factor, width_scale_factor, height_scale_factor = scale_factors images = images[:(images.shape[0] - 1) // time_scale_factor * time_scale_factor + 1] - pixels = comfy.utils.common_upscale(images.movedim(-1, 1), latent_width * width_scale_factor, latent_height * height_scale_factor, "bilinear", crop="disabled").movedim(1, -1) + pixels = comfy.utils.common_upscale(images.movedim(-1, 1), latent_width * width_scale_factor, latent_height * height_scale_factor, "bilinear", crop="center").movedim(1, -1) encode_pixels = pixels[:, :, :, :3] t = vae.encode(encode_pixels) return encode_pixels, t From 9864f5ac86778221d730c9626952a1ee15c16994 Mon Sep 17 00:00:00 2001 From: drozbay <17261091+drozbay@users.noreply.github.com> Date: Fri, 8 May 2026 09:02:17 -0600 Subject: [PATCH 021/145] fix: Stop LTXVImgToVideoInplace from mutating input latents and dropping noise_mask (#13793) --- comfy_extras/nodes_lt.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/comfy_extras/nodes_lt.py b/comfy_extras/nodes_lt.py index f1f4d5319..a4c85db77 100644 --- a/comfy_extras/nodes_lt.py +++ b/comfy_extras/nodes_lt.py @@ -106,12 +106,12 @@ class LTXVImgToVideoInplace(io.ComfyNode): if bypass: return (latent,) - samples = latent["samples"] + samples = latent["samples"].clone() _, height_scale_factor, width_scale_factor = ( vae.downscale_index_formula ) - batch, _, latent_frames, latent_height, latent_width = samples.shape + _, _, _, latent_height, latent_width = samples.shape width = latent_width * width_scale_factor height = latent_height * height_scale_factor @@ -124,11 +124,7 @@ class LTXVImgToVideoInplace(io.ComfyNode): samples[:, :, :t.shape[2]] = t - conditioning_latent_frames_mask = torch.ones( - (batch, 1, latent_frames, 1, 1), - dtype=torch.float32, - device=samples.device, - ) + conditioning_latent_frames_mask = get_noise_mask(latent) conditioning_latent_frames_mask[:, :, :t.shape[2]] = 1.0 - strength return io.NodeOutput({"samples": samples, "noise_mask": conditioning_latent_frames_mask}) From c5ecd231a2aa41124ec6a958416d166d7dcb81fb Mon Sep 17 00:00:00 2001 From: Alexis Rolland Date: Fri, 8 May 2026 23:06:29 +0800 Subject: [PATCH 022/145] fix: Fix bug when mask not on same device (CORE-181) (#13801) --- comfy/bg_removal_model.py | 2 +- comfy_extras/nodes_compositing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comfy/bg_removal_model.py b/comfy/bg_removal_model.py index cb7c2ee53..7877afd7f 100644 --- a/comfy/bg_removal_model.py +++ b/comfy/bg_removal_model.py @@ -47,7 +47,7 @@ class BackgroundRemovalModel(): out = self.model(pixel_values=pixel_values) out = torch.nn.functional.interpolate(out, size=(H, W), mode="bicubic", antialias=False) - mask = out.sigmoid() + mask = out.sigmoid().to(device=comfy.model_management.intermediate_device(), dtype=comfy.model_management.intermediate_dtype()) if mask.ndim == 3: mask = mask.unsqueeze(0) if mask.shape[1] != 1: diff --git a/comfy_extras/nodes_compositing.py b/comfy_extras/nodes_compositing.py index 5b4423734..720efc629 100644 --- a/comfy_extras/nodes_compositing.py +++ b/comfy_extras/nodes_compositing.py @@ -203,7 +203,7 @@ class JoinImageWithAlpha(io.ComfyNode): @classmethod def execute(cls, image: torch.Tensor, alpha: torch.Tensor) -> io.NodeOutput: batch_size = max(len(image), len(alpha)) - alpha = 1.0 - resize_mask(alpha, image.shape[1:]) + alpha = 1.0 - resize_mask(alpha.to(image), image.shape[1:]) alpha = comfy.utils.repeat_to_batch_size(alpha, batch_size) image = comfy.utils.repeat_to_batch_size(image, batch_size) return io.NodeOutput(torch.cat((image[..., :3], alpha.unsqueeze(-1)), dim=-1)) From 87878f354f4d49446ed81b5ebfb98b12dda37c7c Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Fri, 8 May 2026 12:39:16 -0700 Subject: [PATCH 023/145] Add cloud-runtime FE-facing operations to spec (#13734) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add cloud-runtime FE-facing operations to openapi.yaml Add ~67 cloud-runtime FE-facing path operations to the core OpenAPI spec, each tagged with x-runtime: [cloud] at the operation level. These operations are served by the cloud runtime; the local runtime returns 404 for all of these paths. Domain groups added: - Jobs / prompts: /api/job/*, /api/jobs/*/cancel, /api/prompt/*, etc. - History v2: /api/history_v2, /api/history_v2/{prompt_id} - Cloud logs: /api/logs - Asset extensions: /api/assets/download, export, import, etc. - Custom nodes: /api/experiment/nodes (cloud install/uninstall) - Hub: /api/hub/profiles, /api/hub/workflows, /api/hub/labels, etc. - Workflows: /api/workflows CRUD, versioning, fork, publish - Auth/session: /api/auth/session, /api/auth/token, /.well-known/jwks.json - Billing: /api/billing/balance, plans, subscribe, topup, etc. - Workspace: /api/workspace/*, /api/workspaces/* - User/settings/misc: /api/user, /api/secrets, /api/feedback, etc. Also adds corresponding cloud-only component schemas (CloudJob, CloudWorkflow, BillingPlan, Workspace, HubProfile, AuthSession, etc.), all tagged with x-runtime: [cloud]. Spectral lint passes under the existing ruleset with zero new warnings. * Add job_id field to Asset schema and deprecate prompt_id (#13736) - Add job_id as a nullable UUID field to the Asset schema - Mark prompt_id as deprecated with note pointing to job_id - No x-runtime tag needed as both runtimes populate the field * Add hash field to Asset schemas and deprecate asset_hash (#13738) - Add 'hash' as a nullable string field to Asset and AssetUpdated schemas - Mark 'asset_hash' as deprecated with a note pointing to 'hash' - AssetCreated inherits 'hash' via allOf from Asset - Spectral lint clean (no new warnings) * Fix method drift on cloud-runtime endpoints Three PUT operations were added that should be PATCH (cloud serves PATCH for partial updates): - /api/workflows/{workflow_id} - /api/workspaces/{id} - /api/workspace/members/{userId} Two POST operations were added that should be GET (cloud serves GET with query params): - /api/assets/remote-metadata (url moves to query param) - /api/files/mask-layers (response shape replaced — operation queries related mask layer filenames, not file uploads) * Add missing cloud-runtime operations and schemas PR review surfaced operations the cloud runtime serves that weren't covered by the initial spec push, plus one path family missed entirely. New methods on existing paths: - /api/auth/session: add POST (create session cookie) and DELETE (logout) - /api/secrets/{id}: add GET (read metadata) and PATCH (update) - /api/hub/profiles: add POST (create profile) - /api/hub/workflows: add POST (publish to hub) - /api/hub/workflows/{share_id}: add DELETE (unpublish) - /api/workspaces/{id}: add DELETE (soft-delete workspace) - /api/workspace/members/{user_id}/api-keys: add DELETE (bulk revoke) - /api/workflows/{workflow_id}/versions: add POST (create new version) - /api/userdata/{file}/publish: add GET (read publish info) New path family: - /api/tasks (GET list) and /api/tasks/{task_id} (GET detail) for the background task framework New component schemas (all tagged x-runtime: [cloud]): CreateSessionResponse, DeleteSessionResponse, UpdateSecretRequest, BulkRevokeAPIKeysResponse, CreateHubProfileRequest, PublishHubWorkflowRequest, HubWorkflowDetail, AssetInfo, CreateWorkflowVersionRequest, WorkflowVersionResponse, WorkflowPublishInfo, TaskEntry, TaskResponse, TasksListResponse. Existing SecretMeta extended with provider and last_used_at fields the cloud runtime actually returns. New tag: task. Spectral lint passes with zero errors. * Add job_id and prompt_id to AssetUpdated schema Mirrors the Asset schema's deprecation pattern: prompt_id is marked deprecated with a description pointing to job_id; job_id is the new preferred field. PUT /api/assets/{id} responses can now carry both fields consistent with the other Asset-returning endpoints. * feat: add width and height fields to Asset schema (#13745) Add nullable integer fields 'width' and 'height' to the Asset schema in openapi.yaml. These expose original image dimensions in pixels for clients that need pre-thumbnail size info. Both fields are null for non-image assets or assets ingested before dimension extraction. Co-authored-by: Matt Miller * Remove /api/job/{job_id} and /api/job/{job_id}/outputs These two paths are not actually served by the cloud runtime — they return 404 with a redirect message pointing callers to the canonical `/api/jobs/{job_id}` (plural). Declaring them with `x-runtime: [cloud]` and a 200 response schema is incorrect. `/api/job/{job_id}/status` stays — it is a real cloud-served endpoint. Also drops the now-orphaned `CloudJob` and `CloudJobOutputs` component schemas. `CloudJobStatus` is retained. --- openapi.yaml | 4716 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 4714 insertions(+), 2 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index 29b5f544b..4216c1a6c 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -62,6 +62,19 @@ tags: - name: assets description: Asset management (feature-gated behind enable-assets) + - name: auth + description: Authentication and session management (cloud-only) + - name: billing + description: Billing, subscriptions, and payment management (cloud-only) + - name: workspace + description: Workspace and team management (cloud-only) + - name: hub + description: "ComfyUI Hub: profiles, shared workflows, and labels (cloud-only)" + - name: workflows + description: Cloud workflow management and versioning (cloud-only) + - name: task + description: Background task management (cloud-only) + paths: # --------------------------------------------------------------------------- # WebSocket @@ -2056,6 +2069,3449 @@ paths: type: integer description: Number of assets marked as missing + + # =========================================================================== + # Cloud-runtime FE-facing operations + # + # These operations are served by the cloud runtime. The local runtime returns + # 404 for all of these paths. Each operation is tagged x-runtime: [cloud]. + # =========================================================================== + + # --------------------------------------------------------------------------- + # Jobs / prompts (cloud) + # --------------------------------------------------------------------------- + /api/jobs/{job_id}/cancel: + post: + operationId: cancelJob + tags: [queue] + summary: Cancel a running or pending job + description: "[cloud-only] Requests cancellation of a job. If the job is currently executing, execution is interrupted. If it is pending in the queue, it is removed." + x-runtime: [cloud] + parameters: + - name: job_id + in: path + required: true + schema: + type: string + format: uuid + description: The job ID to cancel. + responses: + "200": + description: Cancellation accepted + content: + application/json: + schema: + $ref: "#/components/schemas/CloudJobStatus" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/job/{job_id}/status: + get: + operationId: getCloudJobStatus + tags: [queue] + summary: Get status of a cloud job + description: "[cloud-only] Returns the current execution status of a cloud job." + x-runtime: [cloud] + parameters: + - name: job_id + in: path + required: true + schema: + type: string + format: uuid + description: The job ID to check status for. + responses: + "200": + description: Job status + content: + application/json: + schema: + $ref: "#/components/schemas/CloudJobStatus" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/prompt/{prompt_id}: + get: + operationId: getCloudPrompt + tags: [prompt] + summary: Get a cloud prompt by ID + description: "[cloud-only] Returns the full prompt record for a cloud-executed prompt, including the submitted workflow graph and execution metadata." + x-runtime: [cloud] + parameters: + - name: prompt_id + in: path + required: true + schema: + type: string + format: uuid + description: The prompt ID to fetch. + responses: + "200": + description: Cloud prompt detail + content: + application/json: + schema: + $ref: "#/components/schemas/CloudPrompt" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/history_v2: + get: + operationId: getHistoryV2 + tags: [history] + summary: Get paginated execution history (v2) + description: "[cloud-only] Returns a paginated list of execution history entries in the v2 format, with richer metadata than the legacy history endpoint." + x-runtime: [cloud] + parameters: + - name: limit + in: query + schema: + type: integer + default: 20 + description: Maximum number of results + - name: offset + in: query + schema: + type: integer + default: 0 + description: Pagination offset + - name: status + in: query + schema: + type: string + description: Filter by execution status + responses: + "200": + description: History list + content: + application/json: + schema: + $ref: "#/components/schemas/HistoryV2Response" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/history_v2/{prompt_id}: + get: + operationId: getHistoryV2ByPromptId + tags: [history] + summary: Get v2 history for a specific prompt + description: "[cloud-only] Returns the v2 history entry for a specific prompt execution." + x-runtime: [cloud] + parameters: + - name: prompt_id + in: path + required: true + schema: + type: string + format: uuid + description: The prompt ID to fetch history for. + responses: + "200": + description: History entry + content: + application/json: + schema: + $ref: "#/components/schemas/HistoryV2Entry" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/logs: + get: + operationId: getCloudLogs + tags: [system] + summary: Get cloud execution logs + description: "[cloud-only] Returns execution logs for the authenticated user's cloud jobs." + x-runtime: [cloud] + parameters: + - name: job_id + in: query + schema: + type: string + description: Filter logs by job ID + - name: limit + in: query + schema: + type: integer + default: 100 + description: Maximum number of log entries + - name: offset + in: query + schema: + type: integer + default: 0 + description: Pagination offset + responses: + "200": + description: Log entries + content: + application/json: + schema: + $ref: "#/components/schemas/CloudLogsResponse" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + # --------------------------------------------------------------------------- + # Assets extensions (cloud) + # --------------------------------------------------------------------------- + /api/assets/download: + post: + operationId: downloadAssets + tags: [assets] + summary: Download assets to cloud runtime + description: "[cloud-only] Initiates a download of one or more assets to the cloud runtime environment. Returns a task ID for tracking download progress via WebSocket." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - assets + properties: + assets: + type: array + items: + $ref: "#/components/schemas/AssetDownloadRequest" + description: Assets to download + responses: + "200": + description: Download initiated + content: + application/json: + schema: + type: object + properties: + task_id: + type: string + description: Task ID for tracking progress via WebSocket + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/assets/export: + post: + operationId: exportAssets + tags: [assets] + summary: Export assets as a downloadable archive + description: "[cloud-only] Initiates a bulk export of assets. Returns a task ID for tracking progress via WebSocket. When complete, the export can be downloaded via the exports endpoint." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - asset_ids + properties: + asset_ids: + type: array + items: + type: string + format: uuid + description: IDs of assets to export + export_name: + type: string + description: Name for the export archive + responses: + "200": + description: Export initiated + content: + application/json: + schema: + type: object + properties: + task_id: + type: string + export_name: + type: string + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/assets/exports/{exportName}: + get: + operationId: getAssetExport + tags: [assets] + summary: Download a completed asset export + description: "[cloud-only] Returns the archive file for a completed asset export." + x-runtime: [cloud] + parameters: + - name: exportName + in: path + required: true + schema: + type: string + description: Name of the export to download + responses: + "200": + description: Export archive file + content: + application/zip: + schema: + type: string + format: binary + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/assets/from-workflow: + post: + operationId: createAssetsFromWorkflow + tags: [assets] + summary: Create asset records from a workflow execution + description: "[cloud-only] Registers output files from a workflow execution as assets in the asset database." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - prompt_id + properties: + prompt_id: + type: string + format: uuid + description: Prompt ID whose outputs should be registered as assets + tags: + type: array + items: + type: string + description: Tags to apply to the created assets + responses: + "201": + description: Assets created + content: + application/json: + schema: + type: object + properties: + assets: + type: array + items: + $ref: "#/components/schemas/Asset" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/assets/import: + post: + operationId: importAssets + tags: [assets] + summary: Import assets from external URLs + description: "[cloud-only] Imports one or more assets from external URLs into the cloud asset store." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - imports + properties: + imports: + type: array + items: + $ref: "#/components/schemas/AssetImportRequest" + description: Assets to import + responses: + "200": + description: Import initiated + content: + application/json: + schema: + type: object + properties: + assets: + type: array + items: + $ref: "#/components/schemas/Asset" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/assets/remote-metadata: + get: + operationId: getAssetRemoteMetadata + tags: [assets] + summary: Fetch metadata for a remote asset URL + description: "[cloud-only] Fetches and returns metadata (content type, size, filename) for a remote URL without downloading the full content." + x-runtime: [cloud] + parameters: + - name: url + in: query + required: true + schema: + type: string + format: uri + description: URL to inspect + responses: + "200": + description: Remote metadata + content: + application/json: + schema: + $ref: "#/components/schemas/RemoteAssetMetadata" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + # --------------------------------------------------------------------------- + # Custom nodes / hub (cloud) + # --------------------------------------------------------------------------- + /api/experiment/nodes: + get: + operationId: listCloudNodes + tags: [node] + summary: List installed custom nodes + description: "[cloud-only] Returns the list of custom node packages installed in the cloud runtime." + x-runtime: [cloud] + parameters: + - name: limit + in: query + schema: + type: integer + description: Maximum number of results + - name: offset + in: query + schema: + type: integer + description: Pagination offset + responses: + "200": + description: Custom node list + content: + application/json: + schema: + $ref: "#/components/schemas/CloudNodeList" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + post: + operationId: installCloudNode + tags: [node] + summary: Install a custom node package + description: "[cloud-only] Installs a custom node package in the cloud runtime by ID or repository URL." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - id + properties: + id: + type: string + description: Node package ID or repository URL + version: + type: string + description: Specific version to install + responses: + "200": + description: Node installed + content: + application/json: + schema: + $ref: "#/components/schemas/CloudNode" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/experiment/nodes/{id}: + get: + operationId: getCloudNode + tags: [node] + summary: Get details of an installed custom node + description: "[cloud-only] Returns details about a specific installed custom node package." + x-runtime: [cloud] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Custom node package ID + responses: + "200": + description: Node detail + content: + application/json: + schema: + $ref: "#/components/schemas/CloudNode" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + delete: + operationId: uninstallCloudNode + tags: [node] + summary: Uninstall a custom node package + description: "[cloud-only] Removes a custom node package from the cloud runtime." + x-runtime: [cloud] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Custom node package ID + responses: + "204": + description: Node uninstalled + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/hub/assets/upload-url: + post: + operationId: getHubAssetUploadUrl + tags: [hub] + summary: Get a pre-signed upload URL for a hub asset + description: "[cloud-only] Returns a pre-signed URL that can be used to upload an asset file directly to storage." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - filename + - content_type + properties: + filename: + type: string + description: Name of the file to upload + content_type: + type: string + description: MIME type of the file + size: + type: integer + format: int64 + description: File size in bytes + responses: + "200": + description: Upload URL + content: + application/json: + schema: + type: object + properties: + upload_url: + type: string + format: uri + description: Pre-signed upload URL + asset_url: + type: string + format: uri + description: Public URL after upload completes + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/hub/labels: + get: + operationId: listHubLabels + tags: [hub] + summary: List available hub labels + description: "[cloud-only] Returns the list of labels/categories available for tagging hub content." + x-runtime: [cloud] + responses: + "200": + description: Label list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/HubLabel" + + /api/hub/profiles: + get: + operationId: listHubProfiles + tags: [hub] + summary: List hub user profiles + description: "[cloud-only] Returns a paginated list of public hub user profiles." + x-runtime: [cloud] + parameters: + - name: limit + in: query + schema: + type: integer + description: Maximum number of results + - name: offset + in: query + schema: + type: integer + description: Pagination offset + - name: search + in: query + schema: + type: string + description: Search by username or display name + responses: + "200": + description: Profile list + content: + application/json: + schema: + type: object + properties: + profiles: + type: array + items: + $ref: "#/components/schemas/HubProfile" + total: + type: integer + has_more: + type: boolean + post: + operationId: createHubProfile + tags: [hub] + summary: Create a Hub profile + description: "[cloud-only] Creates a hub profile for the specified workspace. Username is immutable after creation." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateHubProfileRequest" + responses: + "201": + description: Hub profile created + content: + application/json: + schema: + $ref: "#/components/schemas/HubProfile" + "400": + description: Bad request (e.g. invalid username) + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "409": + description: Username already taken or profile already exists + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/hub/profiles/{username}: + get: + operationId: getHubProfile + tags: [hub] + summary: Get a hub profile by username + description: "[cloud-only] Returns the public hub profile for the given username." + x-runtime: [cloud] + parameters: + - name: username + in: path + required: true + schema: + type: string + description: Hub username + responses: + "200": + description: Profile + content: + application/json: + schema: + $ref: "#/components/schemas/HubProfile" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/hub/profiles/check: + get: + operationId: checkHubProfileUsername + tags: [hub] + summary: Check if a hub username is available + description: "[cloud-only] Returns whether the given username is available for registration." + x-runtime: [cloud] + parameters: + - name: username + in: query + required: true + schema: + type: string + description: Username to check + responses: + "200": + description: Availability result + content: + application/json: + schema: + type: object + properties: + available: + type: boolean + username: + type: string + + /api/hub/profiles/me: + get: + operationId: getMyHubProfile + tags: [hub] + summary: Get the authenticated user's hub profile + description: "[cloud-only] Returns the hub profile of the currently authenticated user." + x-runtime: [cloud] + responses: + "200": + description: Profile + content: + application/json: + schema: + $ref: "#/components/schemas/HubProfile" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + put: + operationId: updateMyHubProfile + tags: [hub] + summary: Update the authenticated user's hub profile + description: "[cloud-only] Updates the hub profile of the currently authenticated user." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + display_name: + type: string + bio: + type: string + avatar_url: + type: string + format: uri + links: + type: array + items: + type: string + format: uri + responses: + "200": + description: Updated profile + content: + application/json: + schema: + $ref: "#/components/schemas/HubProfile" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "409": + description: Conflict + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/hub/workflows: + get: + operationId: listHubWorkflows + tags: [hub] + summary: List published hub workflows + description: "[cloud-only] Returns a paginated list of publicly shared workflows on the hub." + x-runtime: [cloud] + parameters: + - name: limit + in: query + schema: + type: integer + description: Maximum number of results + - name: offset + in: query + schema: + type: integer + description: Pagination offset + - name: sort + in: query + schema: + type: string + description: Sort field (e.g. created_at, likes) + - name: order + in: query + schema: + type: string + enum: [asc, desc] + description: Sort direction + - name: search + in: query + schema: + type: string + description: Search by title or description + - name: labels + in: query + schema: + type: string + description: Filter by label IDs (comma-separated) + responses: + "200": + description: Hub workflow list + content: + application/json: + schema: + $ref: "#/components/schemas/HubWorkflowList" + post: + operationId: publishHubWorkflow + tags: [hub] + summary: Publish a workflow to the hub + description: "[cloud-only] Publishes a workflow to the hub with metadata, thumbnail, and sample images." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PublishHubWorkflowRequest" + responses: + "200": + description: Workflow published to hub + content: + application/json: + schema: + $ref: "#/components/schemas/HubWorkflowDetail" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Workflow or profile not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/hub/workflows/{share_id}: + get: + operationId: getHubWorkflow + tags: [hub] + summary: Get a published hub workflow by share ID + description: "[cloud-only] Returns the full details of a published workflow on the hub." + x-runtime: [cloud] + parameters: + - name: share_id + in: path + required: true + schema: + type: string + description: Workflow share ID + responses: + "200": + description: Hub workflow + content: + application/json: + schema: + $ref: "#/components/schemas/HubWorkflow" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + delete: + operationId: deleteHubWorkflow + tags: [hub] + summary: Unpublish a workflow from the hub + description: "[cloud-only] Removes a workflow from the hub listing." + x-runtime: [cloud] + parameters: + - name: share_id + in: path + required: true + schema: + type: string + description: Workflow share ID + responses: + "204": + description: Successfully unpublished + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Workflow not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/hub/workflows/index: + get: + operationId: getHubWorkflowIndex + tags: [hub] + summary: Get the hub workflow index + description: "[cloud-only] Returns the lightweight index of all hub workflows for client-side search and navigation." + x-runtime: [cloud] + responses: + "200": + description: Workflow index + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/HubWorkflowIndexEntry" + + # --------------------------------------------------------------------------- + # Workflows (cloud) + # --------------------------------------------------------------------------- + /api/workflows: + get: + operationId: listCloudWorkflows + tags: [workflows] + summary: List cloud workflows + description: "[cloud-only] Returns a paginated list of the authenticated user's cloud workflows." + x-runtime: [cloud] + parameters: + - name: limit + in: query + schema: + type: integer + description: Maximum number of results + - name: offset + in: query + schema: + type: integer + description: Pagination offset + - name: sort + in: query + schema: + type: string + description: Sort field + - name: order + in: query + schema: + type: string + enum: [asc, desc] + description: Sort direction + - name: search + in: query + schema: + type: string + description: Search by workflow name + responses: + "200": + description: Workflow list + content: + application/json: + schema: + $ref: "#/components/schemas/CloudWorkflowList" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + post: + operationId: createCloudWorkflow + tags: [workflows] + summary: Create a new cloud workflow + description: "[cloud-only] Creates a new cloud workflow with the provided name and optional initial content." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + properties: + name: + type: string + description: Workflow name + description: + type: string + description: Workflow description + content: + type: object + additionalProperties: true + description: Initial workflow graph JSON + responses: + "201": + description: Workflow created + content: + application/json: + schema: + $ref: "#/components/schemas/CloudWorkflow" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/workflows/{workflow_id}: + get: + operationId: getCloudWorkflow + tags: [workflows] + summary: Get a cloud workflow by ID + description: "[cloud-only] Returns the metadata for a cloud workflow." + x-runtime: [cloud] + parameters: + - name: workflow_id + in: path + required: true + schema: + type: string + format: uuid + description: The workflow ID. + responses: + "200": + description: Workflow detail + content: + application/json: + schema: + $ref: "#/components/schemas/CloudWorkflow" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + patch: + operationId: updateCloudWorkflow + tags: [workflows] + summary: Update a cloud workflow + description: "[cloud-only] Updates the metadata (name, description) of an existing cloud workflow." + x-runtime: [cloud] + parameters: + - name: workflow_id + in: path + required: true + schema: + type: string + format: uuid + description: The workflow ID. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: + type: string + responses: + "200": + description: Workflow updated + content: + application/json: + schema: + $ref: "#/components/schemas/CloudWorkflow" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + delete: + operationId: deleteCloudWorkflow + tags: [workflows] + summary: Delete a cloud workflow + description: "[cloud-only] Deletes a cloud workflow and all its versions." + x-runtime: [cloud] + parameters: + - name: workflow_id + in: path + required: true + schema: + type: string + format: uuid + description: The workflow ID. + responses: + "204": + description: Workflow deleted + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/workflows/{workflow_id}/content: + get: + operationId: getCloudWorkflowContent + tags: [workflows] + summary: Get the content of a cloud workflow + description: "[cloud-only] Returns the full workflow graph JSON for the latest version of a cloud workflow." + x-runtime: [cloud] + parameters: + - name: workflow_id + in: path + required: true + schema: + type: string + format: uuid + description: The workflow ID. + - name: version_id + in: query + schema: + type: string + description: Specific version ID to fetch + responses: + "200": + description: Workflow content + content: + application/json: + schema: + type: object + additionalProperties: true + description: The full workflow graph JSON + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + put: + operationId: updateCloudWorkflowContent + tags: [workflows] + summary: Update the content of a cloud workflow + description: "[cloud-only] Saves new workflow graph JSON as a new version of the cloud workflow." + x-runtime: [cloud] + parameters: + - name: workflow_id + in: path + required: true + schema: + type: string + format: uuid + description: The workflow ID. + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: true + description: The workflow graph JSON to save + responses: + "200": + description: Content updated + content: + application/json: + schema: + $ref: "#/components/schemas/CloudWorkflowVersion" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/workflows/{workflow_id}/fork: + post: + operationId: forkCloudWorkflow + tags: [workflows] + summary: Fork a cloud workflow + description: "[cloud-only] Creates a copy of a cloud workflow under the authenticated user's account." + x-runtime: [cloud] + parameters: + - name: workflow_id + in: path + required: true + schema: + type: string + format: uuid + description: The workflow ID to fork. + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: Name for the forked workflow (defaults to original name) + responses: + "201": + description: Forked workflow + content: + application/json: + schema: + $ref: "#/components/schemas/CloudWorkflow" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/workflows/{workflow_id}/versions: + get: + operationId: listCloudWorkflowVersions + tags: [workflows] + summary: List versions of a cloud workflow + description: "[cloud-only] Returns the version history of a cloud workflow." + x-runtime: [cloud] + parameters: + - name: workflow_id + in: path + required: true + schema: + type: string + format: uuid + description: The workflow ID. + - name: limit + in: query + schema: + type: integer + description: Maximum number of results + - name: offset + in: query + schema: + type: integer + description: Pagination offset + responses: + "200": + description: Version list + content: + application/json: + schema: + type: object + properties: + versions: + type: array + items: + $ref: "#/components/schemas/CloudWorkflowVersion" + total: + type: integer + has_more: + type: boolean + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + post: + operationId: createCloudWorkflowVersion + tags: [workflows] + summary: Create a new cloud workflow version + description: "[cloud-only] Creates a new workflow version with updated workflow JSON. Uses optimistic concurrency via base_version." + x-runtime: [cloud] + parameters: + - name: workflow_id + in: path + required: true + schema: + type: string + format: uuid + description: The workflow ID. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateWorkflowVersionRequest" + responses: + "201": + description: Version created + content: + application/json: + schema: + $ref: "#/components/schemas/WorkflowVersionResponse" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden — not the workflow owner + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "409": + description: Version conflict — base_version does not match latest + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/workflows/published/{share_id}: + get: + operationId: getPublishedWorkflow + tags: [workflows] + summary: Get a published workflow by share ID + description: "[cloud-only] Returns a publicly published cloud workflow by its share identifier." + x-runtime: [cloud] + parameters: + - name: share_id + in: path + required: true + schema: + type: string + description: The workflow share ID. + responses: + "200": + description: Published workflow + content: + application/json: + schema: + $ref: "#/components/schemas/CloudWorkflow" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + # --------------------------------------------------------------------------- + # Auth / session (cloud) + # --------------------------------------------------------------------------- + /api/auth/session: + get: + operationId: getAuthSession + tags: [auth] + summary: Get the current authentication session + description: "[cloud-only] Returns the current session state for the authenticated user, including user identity and active workspace." + x-runtime: [cloud] + responses: + "200": + description: Session info + content: + application/json: + schema: + $ref: "#/components/schemas/AuthSession" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + post: + operationId: createAuthSession + tags: [auth] + summary: Create a session cookie + description: "[cloud-only] Creates a session cookie from the bearer token in the Authorization header. Returns a Set-Cookie header with a secure HttpOnly session cookie. Cookie authentication is not allowed for this endpoint." + x-runtime: [cloud] + responses: + "200": + description: Session created + content: + application/json: + schema: + $ref: "#/components/schemas/CreateSessionResponse" + "400": + description: Bad request — invalid or expired ID token + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + delete: + operationId: deleteAuthSession + tags: [auth] + summary: Delete session cookie (logout) + description: "[cloud-only] Clears the session cookie and optionally revokes the session on the server." + x-runtime: [cloud] + responses: + "200": + description: Session deleted + content: + application/json: + schema: + $ref: "#/components/schemas/DeleteSessionResponse" + + /api/auth/token: + post: + operationId: createAuthToken + tags: [auth] + summary: Exchange credentials for an access token + description: "[cloud-only] Exchanges authentication credentials (e.g. an authorization code) for an access token." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - grant_type + properties: + grant_type: + type: string + enum: [authorization_code, refresh_token] + description: OAuth2 grant type + code: + type: string + description: Authorization code (for authorization_code grant) + refresh_token: + type: string + description: Refresh token (for refresh_token grant) + redirect_uri: + type: string + format: uri + description: Redirect URI used in the authorization request + responses: + "200": + description: Token response + content: + application/json: + schema: + $ref: "#/components/schemas/AuthTokenResponse" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /.well-known/jwks.json: + get: + operationId: getJwks + tags: [auth] + summary: Get JSON Web Key Set + description: "[cloud-only] Returns the JSON Web Key Set (JWKS) used to verify JWTs issued by the cloud authentication service." + x-runtime: [cloud] + responses: + "200": + description: JWKS + content: + application/json: + schema: + $ref: "#/components/schemas/JwksResponse" + + # --------------------------------------------------------------------------- + # Billing (cloud) + # --------------------------------------------------------------------------- + /api/billing/balance: + get: + operationId: getBillingBalance + tags: [billing] + summary: Get current credit balance + description: "[cloud-only] Returns the authenticated user's current credit balance and usage summary." + x-runtime: [cloud] + responses: + "200": + description: Balance info + content: + application/json: + schema: + $ref: "#/components/schemas/BillingBalance" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/billing/events: + get: + operationId: listBillingEvents + tags: [billing] + summary: List billing events + description: "[cloud-only] Returns a paginated list of billing events (charges, credits, refunds) for the authenticated user." + x-runtime: [cloud] + parameters: + - name: limit + in: query + schema: + type: integer + description: Maximum number of results + - name: offset + in: query + schema: + type: integer + description: Pagination offset + - name: type + in: query + schema: + type: string + description: Filter by event type + responses: + "200": + description: Billing events + content: + application/json: + schema: + $ref: "#/components/schemas/BillingEventList" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/billing/ops/{id}: + get: + operationId: getBillingOp + tags: [billing] + summary: Get a billing operation by ID + description: "[cloud-only] Returns details of a specific billing operation." + x-runtime: [cloud] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The billing operation ID. + responses: + "200": + description: Billing operation + content: + application/json: + schema: + $ref: "#/components/schemas/BillingOp" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/billing/payment-portal: + post: + operationId: createPaymentPortalSession + tags: [billing] + summary: Create a payment portal session + description: "[cloud-only] Creates a Stripe customer portal session for managing payment methods and invoices. Returns a URL to redirect the user to." + x-runtime: [cloud] + responses: + "200": + description: Portal session + content: + application/json: + schema: + type: object + properties: + url: + type: string + format: uri + description: Stripe portal URL + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/billing/plans: + get: + operationId: listBillingPlans + tags: [billing] + summary: List available billing plans + description: "[cloud-only] Returns the list of available subscription plans and their pricing." + x-runtime: [cloud] + responses: + "200": + description: Plan list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/BillingPlan" + + /api/billing/preview-subscribe: + post: + operationId: previewSubscription + tags: [billing] + summary: Preview a subscription change + description: "[cloud-only] Returns a preview of what a subscription change would cost, including prorations." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - plan_id + properties: + plan_id: + type: string + description: ID of the plan to preview + responses: + "200": + description: Subscription preview + content: + application/json: + schema: + $ref: "#/components/schemas/SubscriptionPreview" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/billing/status: + get: + operationId: getBillingStatus + tags: [billing] + summary: Get billing status + description: "[cloud-only] Returns the authenticated user's current billing and subscription status." + x-runtime: [cloud] + responses: + "200": + description: Billing status + content: + application/json: + schema: + $ref: "#/components/schemas/BillingStatus" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/billing/subscribe: + post: + operationId: createSubscription + tags: [billing] + summary: Subscribe to a billing plan + description: "[cloud-only] Creates a new subscription to the specified billing plan." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - plan_id + properties: + plan_id: + type: string + description: ID of the plan to subscribe to + payment_method_id: + type: string + description: Stripe payment method ID + responses: + "200": + description: Subscription created + content: + application/json: + schema: + $ref: "#/components/schemas/BillingSubscription" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/billing/subscription/cancel: + post: + operationId: cancelSubscription + tags: [billing] + summary: Cancel the active subscription + description: "[cloud-only] Cancels the authenticated user's active subscription. The subscription remains active until the end of the current billing period." + x-runtime: [cloud] + responses: + "200": + description: Subscription cancelled + content: + application/json: + schema: + $ref: "#/components/schemas/BillingSubscription" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/billing/subscription/resubscribe: + post: + operationId: resubscribe + tags: [billing] + summary: Resubscribe after cancellation + description: "[cloud-only] Reactivates a subscription that was previously cancelled but has not yet expired." + x-runtime: [cloud] + responses: + "200": + description: Subscription reactivated + content: + application/json: + schema: + $ref: "#/components/schemas/BillingSubscription" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/billing/topup: + post: + operationId: topUpCredits + tags: [billing] + summary: Purchase additional credits + description: "[cloud-only] Purchases a one-time credit top-up using the user's payment method on file." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - amount + properties: + amount: + type: integer + description: Number of credits to purchase + responses: + "200": + description: Top-up successful + content: + application/json: + schema: + $ref: "#/components/schemas/BillingBalance" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + # --------------------------------------------------------------------------- + # Workspace (cloud) + # --------------------------------------------------------------------------- + /api/workspace/api-keys: + get: + operationId: listWorkspaceApiKeys + tags: [workspace] + summary: List workspace API keys + description: "[cloud-only] Returns the list of API keys for the current workspace." + x-runtime: [cloud] + responses: + "200": + description: API key list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/WorkspaceApiKey" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + post: + operationId: createWorkspaceApiKey + tags: [workspace] + summary: Create a workspace API key + description: "[cloud-only] Creates a new API key for the current workspace." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + properties: + name: + type: string + description: Display name for the API key + responses: + "201": + description: API key created + content: + application/json: + schema: + $ref: "#/components/schemas/WorkspaceApiKeyCreated" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/workspace/api-keys/{id}: + delete: + operationId: deleteWorkspaceApiKey + tags: [workspace] + summary: Delete a workspace API key + description: "[cloud-only] Revokes and deletes a workspace API key." + x-runtime: [cloud] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The API key ID. + responses: + "204": + description: API key deleted + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/workspace/invites: + get: + operationId: listWorkspaceInvites + tags: [workspace] + summary: List pending workspace invites + description: "[cloud-only] Returns the list of pending invitations for the current workspace." + x-runtime: [cloud] + responses: + "200": + description: Invite list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/WorkspaceInvite" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + post: + operationId: createWorkspaceInvite + tags: [workspace] + summary: Invite a user to the workspace + description: "[cloud-only] Creates an invitation for a user to join the current workspace." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - email + properties: + email: + type: string + format: email + description: Email address to invite + role: + type: string + enum: [admin, member] + description: Role to assign + responses: + "201": + description: Invite created + content: + application/json: + schema: + $ref: "#/components/schemas/WorkspaceInvite" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "409": + description: Conflict + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/workspace/invites/{inviteId}: + delete: + operationId: deleteWorkspaceInvite + tags: [workspace] + summary: Cancel a workspace invite + description: "[cloud-only] Cancels a pending workspace invitation." + x-runtime: [cloud] + parameters: + - name: inviteId + in: path + required: true + schema: + type: string + description: The invite ID. + responses: + "204": + description: Invite cancelled + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/workspace/leave: + post: + operationId: leaveWorkspace + tags: [workspace] + summary: Leave the current workspace + description: "[cloud-only] Removes the authenticated user from the current workspace." + x-runtime: [cloud] + responses: + "204": + description: Left workspace + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/workspace/members: + get: + operationId: listWorkspaceMembers + tags: [workspace] + summary: List workspace members + description: "[cloud-only] Returns the list of members in the current workspace." + x-runtime: [cloud] + responses: + "200": + description: Member list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/WorkspaceMember" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/workspace/members/{user_id}/api-keys: + get: + operationId: listMemberApiKeys + tags: [workspace] + summary: List API keys for a workspace member + description: "[cloud-only] Returns the API keys belonging to a specific workspace member. Requires admin role." + x-runtime: [cloud] + parameters: + - name: user_id + in: path + required: true + schema: + type: string + description: The member's user ID. + responses: + "200": + description: API key list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/WorkspaceApiKey" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + delete: + operationId: bulkRevokeMemberApiKeys + tags: [workspace] + summary: Bulk revoke a member's API keys + description: "[cloud-only] Revokes all active API keys for a specific workspace member. Only workspace owners can perform this action." + x-runtime: [cloud] + parameters: + - name: user_id + in: path + required: true + schema: + type: string + minLength: 1 + description: The member's user ID. + responses: + "200": + description: Keys revoked + content: + application/json: + schema: + $ref: "#/components/schemas/BulkRevokeAPIKeysResponse" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden — must be workspace owner + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/workspace/members/{userId}: + patch: + operationId: updateWorkspaceMember + tags: [workspace] + summary: Update a workspace member's role + description: "[cloud-only] Updates the role of a workspace member. Requires admin role." + x-runtime: [cloud] + parameters: + - name: userId + in: path + required: true + schema: + type: string + description: The member's user ID. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - role + properties: + role: + type: string + enum: [admin, member] + description: New role to assign + responses: + "200": + description: Member updated + content: + application/json: + schema: + $ref: "#/components/schemas/WorkspaceMember" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + delete: + operationId: removeWorkspaceMember + tags: [workspace] + summary: Remove a member from the workspace + description: "[cloud-only] Removes a member from the current workspace. Requires admin role." + x-runtime: [cloud] + parameters: + - name: userId + in: path + required: true + schema: + type: string + description: The member's user ID. + responses: + "204": + description: Member removed + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/workspaces: + get: + operationId: listWorkspaces + tags: [workspace] + summary: List workspaces the user belongs to + description: "[cloud-only] Returns the list of workspaces the authenticated user is a member of." + x-runtime: [cloud] + responses: + "200": + description: Workspace list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Workspace" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + post: + operationId: createWorkspace + tags: [workspace] + summary: Create a new workspace + description: "[cloud-only] Creates a new workspace. The authenticated user becomes the owner." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + properties: + name: + type: string + description: Workspace name + responses: + "201": + description: Workspace created + content: + application/json: + schema: + $ref: "#/components/schemas/Workspace" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/workspaces/{id}: + get: + operationId: getWorkspace + tags: [workspace] + summary: Get a workspace by ID + description: "[cloud-only] Returns details of a workspace the user is a member of." + x-runtime: [cloud] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The workspace ID. + responses: + "200": + description: Workspace detail + content: + application/json: + schema: + $ref: "#/components/schemas/Workspace" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + patch: + operationId: updateWorkspace + tags: [workspace] + summary: Update workspace settings + description: "[cloud-only] Updates the name or settings of a workspace. Requires admin role." + x-runtime: [cloud] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The workspace ID. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: New workspace name + responses: + "200": + description: Workspace updated + content: + application/json: + schema: + $ref: "#/components/schemas/Workspace" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + delete: + operationId: deleteWorkspace + tags: [workspace] + summary: Delete a workspace + description: "[cloud-only] Soft-deletes a workspace. Requires owner role. Personal workspaces cannot be deleted." + x-runtime: [cloud] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The workspace ID. + responses: + "204": + description: Workspace deleted + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "403": + description: Forbidden — must be workspace owner + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + # --------------------------------------------------------------------------- + # User / settings / misc (cloud) + # --------------------------------------------------------------------------- + /api/feedback: + post: + operationId: submitFeedback + tags: [user] + summary: Submit user feedback + description: "[cloud-only] Submits feedback from the user about their experience with the cloud runtime." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + type: string + description: Feedback message + rating: + type: integer + minimum: 1 + maximum: 5 + description: Optional satisfaction rating + context: + type: object + additionalProperties: true + description: Additional context metadata + responses: + "200": + description: Feedback submitted + content: + application/json: + schema: + type: object + properties: + id: + type: string + status: + type: string + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/files/mask-layers: + get: + operationId: getMaskLayers + tags: [assets] + summary: Get related mask layer filenames + description: "[cloud-only] Given a mask file (any of the 4 layers), returns all related mask layer filenames. Used by the mask editor to load the paint, mask, and painted layers when reopening a previously edited mask." + x-runtime: [cloud] + parameters: + - name: filename + in: query + required: true + schema: + type: string + description: Hash filename of any mask layer file + responses: + "200": + description: Related mask layers + content: + application/json: + schema: + type: object + properties: + mask: + type: string + description: Filename of the mask layer + nullable: true + paint: + type: string + description: Filename of the paint strokes layer + nullable: true + painted: + type: string + description: Filename of the painted image layer + nullable: true + painted_masked: + type: string + description: Filename of the final composite layer + nullable: true + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: File not found or not a mask file + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/internal/cloud_analytics: + post: + operationId: postCloudAnalytics + tags: [internal] + summary: Post client analytics events + description: "[cloud-only] Receives analytics events from the frontend for processing by the cloud analytics pipeline." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - events + properties: + events: + type: array + items: + type: object + required: + - event_name + properties: + event_name: + type: string + timestamp: + type: string + format: date-time + properties: + type: object + additionalProperties: true + responses: + "200": + description: Events accepted + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/invites/{token}/accept: + post: + operationId: acceptInvite + tags: [workspace] + summary: Accept a workspace invitation + description: "[cloud-only] Accepts a workspace invitation using the invite token. The authenticated user is added to the workspace." + x-runtime: [cloud] + parameters: + - name: token + in: path + required: true + schema: + type: string + description: The invitation token. + responses: + "200": + description: Invite accepted + content: + application/json: + schema: + $ref: "#/components/schemas/Workspace" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/secrets: + get: + operationId: listSecrets + tags: [settings] + summary: List user secrets + description: "[cloud-only] Returns the list of secrets (API keys for third-party services) stored for the authenticated user. Secret values are redacted." + x-runtime: [cloud] + responses: + "200": + description: Secret list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/SecretMeta" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + post: + operationId: createSecret + tags: [settings] + summary: Create or update a secret + description: "[cloud-only] Stores a new secret or updates an existing one. Secrets are encrypted at rest." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + - value + properties: + name: + type: string + description: Secret name (unique per user) + value: + type: string + description: Secret value + responses: + "201": + description: Secret created + content: + application/json: + schema: + $ref: "#/components/schemas/SecretMeta" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/secrets/{id}: + get: + operationId: getSecret + tags: [settings] + summary: Get secret metadata + description: "[cloud-only] Returns metadata for a specific secret. Does not return the plaintext secret value." + x-runtime: [cloud] + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + description: The secret ID. + responses: + "200": + description: Secret metadata + content: + application/json: + schema: + $ref: "#/components/schemas/SecretMeta" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + patch: + operationId: updateSecret + tags: [settings] + summary: Update a secret + description: "[cloud-only] Updates an existing secret's name and/or value. Both fields are optional; only provided fields are updated." + x-runtime: [cloud] + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + description: The secret ID. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateSecretRequest" + responses: + "200": + description: Secret updated + content: + application/json: + schema: + $ref: "#/components/schemas/SecretMeta" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "409": + description: Conflict — a secret with this name already exists + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + delete: + operationId: deleteSecret + tags: [settings] + summary: Delete a secret + description: "[cloud-only] Permanently deletes a stored secret." + x-runtime: [cloud] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The secret ID. + responses: + "204": + description: Secret deleted + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/user: + get: + operationId: getCloudUser + tags: [user] + summary: Get the authenticated cloud user + description: "[cloud-only] Returns the profile and account information for the currently authenticated user." + x-runtime: [cloud] + responses: + "200": + description: User profile + content: + application/json: + schema: + $ref: "#/components/schemas/CloudUser" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + put: + operationId: updateCloudUser + tags: [user] + summary: Update the authenticated cloud user profile + description: "[cloud-only] Updates the profile information for the currently authenticated user." + x-runtime: [cloud] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + display_name: + type: string + avatar_url: + type: string + format: uri + responses: + "200": + description: Updated profile + content: + application/json: + schema: + $ref: "#/components/schemas/CloudUser" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/userdata/{file}/publish: + get: + operationId: getUserdataFilePublish + tags: [userdata] + summary: Get publish info for a userdata file + description: "[cloud-only] Returns the publish status and share info for a userdata workflow file." + x-runtime: [cloud] + parameters: + - name: file + in: path + required: true + schema: + type: string + description: File path relative to user data directory + responses: + "200": + description: Publish info (publish_time is null if never published) + content: + application/json: + schema: + $ref: "#/components/schemas/WorkflowPublishInfo" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Workflow not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + post: + operationId: publishUserdataFile + tags: [userdata] + summary: Publish a userdata file to the cloud + description: "[cloud-only] Makes a userdata file available via a public URL for sharing or embedding." + x-runtime: [cloud] + parameters: + - name: file + in: path + required: true + schema: + type: string + description: File path relative to user data directory + responses: + "200": + description: Published file URL + content: + application/json: + schema: + type: object + properties: + url: + type: string + format: uri + description: Public URL of the published file + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/vhs/queryvideo: + get: + operationId: queryVhsVideo + tags: [view] + summary: Query VHS video metadata + description: "[cloud-only] Returns metadata about a video file processed by the VHS (Video Helper Suite) integration." + x-runtime: [cloud] + parameters: + - name: filename + in: query + required: true + schema: + type: string + description: Video filename + - name: type + in: query + schema: + type: string + enum: [input, output, temp] + description: Directory type + - name: subfolder + in: query + schema: + type: string + description: Subfolder within the directory + responses: + "200": + description: Video metadata + content: + application/json: + schema: + type: object + additionalProperties: true + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/vhs/viewaudio: + get: + operationId: viewVhsAudio + tags: [view] + summary: View or download VHS audio + description: "[cloud-only] Returns audio content from a VHS-processed file." + x-runtime: [cloud] + parameters: + - name: filename + in: query + required: true + schema: + type: string + description: Audio filename + - name: type + in: query + schema: + type: string + enum: [input, output, temp] + description: Directory type + - name: subfolder + in: query + schema: + type: string + description: Subfolder within the directory + responses: + "200": + description: Audio content + content: + audio/*: + schema: + type: string + format: binary + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/vhs/viewvideo: + get: + operationId: viewVhsVideo + tags: [view] + summary: View or download VHS video + description: "[cloud-only] Returns video content from a VHS-processed file." + x-runtime: [cloud] + parameters: + - name: filename + in: query + required: true + schema: + type: string + description: Video filename + - name: type + in: query + schema: + type: string + enum: [input, output, temp] + description: Directory type + - name: subfolder + in: query + schema: + type: string + description: Subfolder within the directory + responses: + "200": + description: Video content + content: + video/*: + schema: + type: string + format: binary + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/viewvideo: + get: + operationId: viewVideo + tags: [view] + summary: View or download a video file + description: "[cloud-only] Serves a video file from the output directory. Used by the frontend video player." + x-runtime: [cloud] + parameters: + - name: filename + in: query + required: true + schema: + type: string + description: Video filename + - name: type + in: query + schema: + type: string + enum: [input, output, temp] + description: Directory type + - name: subfolder + in: query + schema: + type: string + description: Subfolder within the directory + responses: + "200": + description: Video content + content: + video/*: + schema: + type: string + format: binary + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/tasks: + get: + operationId: listTasks + tags: [task] + summary: List background tasks + description: "[cloud-only] Retrieve a paginated list of background tasks for the authenticated user. Supports filtering by task type, status, and creation time." + x-runtime: [cloud] + parameters: + - name: task_name + in: query + schema: + type: string + description: Filter by task type name (exact match). + - name: idempotency_key + in: query + schema: + type: string + description: Filter by idempotency key (exact match). + - name: status + in: query + schema: + type: string + description: Filter by one or more statuses (comma-separated). + - name: created_after + in: query + schema: + type: string + format: date-time + description: Filter tasks created after this timestamp. + - name: created_before + in: query + schema: + type: string + format: date-time + description: Filter tasks created before this timestamp. + - name: sort_order + in: query + schema: + type: string + enum: [asc, desc] + default: desc + description: Sort direction by create_time. + - name: offset + in: query + schema: + type: integer + minimum: 0 + default: 0 + description: Pagination offset (0-based). + - name: limit + in: query + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + description: Maximum items per page (1-100). + responses: + "200": + description: Tasks retrieved + content: + application/json: + schema: + $ref: "#/components/schemas/TasksListResponse" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "422": + description: Validation error + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + /api/tasks/{task_id}: + get: + operationId: getTask + tags: [task] + summary: Get task details + description: "[cloud-only] Retrieve full details for a specific background task." + x-runtime: [cloud] + parameters: + - name: task_id + in: path + required: true + schema: + type: string + format: uuid + description: Task identifier (UUID). + responses: + "200": + description: Task details + content: + application/json: + schema: + $ref: "#/components/schemas/TaskResponse" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + "404": + description: Task not found + content: + application/json: + schema: + $ref: "#/components/schemas/CloudError" + + components: parameters: ComfyUserHeader: @@ -2823,14 +6279,29 @@ components: name: type: string description: Name of the asset file + hash: + type: string + nullable: true + description: Blake3 content hash of the asset (preferred over asset_hash) + pattern: "^blake3:[a-f0-9]{64}$" asset_hash: type: string - description: Blake3 hash of the asset content + nullable: true + deprecated: true + description: "Deprecated: use `hash` instead. Blake3 hash of the asset content." pattern: "^blake3:[a-f0-9]{64}$" size: type: integer format: int64 description: Size of the asset in bytes + width: + type: integer + nullable: true + description: "Original image width in pixels. Null for non-image assets or assets ingested before dimension extraction." + height: + type: integer + nullable: true + description: "Original image height in pixels. Null for non-image assets or assets ingested before dimension extraction." mime_type: type: string description: MIME type of the asset @@ -2859,7 +6330,14 @@ components: prompt_id: type: string format: uuid - description: ID of the prompt that created this asset + nullable: true + deprecated: true + description: "Deprecated: use job_id instead. ID of the prompt that created this asset." + job_id: + type: string + format: uuid + nullable: true + description: ID of the job that created this asset created_at: type: string format: date-time @@ -2897,8 +6375,16 @@ components: format: uuid name: type: string + hash: + type: string + nullable: true + description: Blake3 content hash of the asset (preferred over asset_hash) + pattern: "^blake3:[a-f0-9]{64}$" asset_hash: type: string + nullable: true + deprecated: true + description: "Deprecated: use `hash` instead. Blake3 hash of the asset content." pattern: "^blake3:[a-f0-9]{64}$" tags: type: array @@ -2909,6 +6395,17 @@ components: user_metadata: type: object additionalProperties: true + prompt_id: + type: string + format: uuid + nullable: true + deprecated: true + description: "Deprecated: use job_id instead. ID of the prompt that created this asset." + job_id: + type: string + format: uuid + nullable: true + description: ID of the job that created this asset updated_at: type: string format: date-time @@ -3365,3 +6862,1218 @@ components: enum: [created, running, completed, failed] error: type: string + + + # ------------------------------------------------------------------- + # Cloud-runtime schemas + # + # These schemas are exclusively referenced by cloud-runtime operations. + # Tagged x-runtime: [cloud]. + # ------------------------------------------------------------------- + CloudError: + type: object + x-runtime: [cloud] + description: "[cloud-only] Standard error response from cloud endpoints." + required: + - error + properties: + error: + type: string + description: Error message + code: + type: string + description: Machine-readable error code + details: + type: object + additionalProperties: true + description: Additional error context + + CloudJobStatus: + type: object + x-runtime: [cloud] + description: "[cloud-only] Status of a cloud job." + required: + - id + - status + properties: + id: + type: string + format: uuid + status: + type: string + enum: [pending, running, completed, failed, cancelled] + progress: + type: number + minimum: 0 + maximum: 1 + description: "Execution progress (0.0 to 1.0)" + started_at: + type: string + format: date-time + nullable: true + completed_at: + type: string + format: date-time + nullable: true + + CloudPrompt: + type: object + x-runtime: [cloud] + description: "[cloud-only] A cloud-executed prompt record." + required: + - id + - status + properties: + id: + type: string + format: uuid + status: + type: string + workflow: + type: object + additionalProperties: true + outputs: + type: object + additionalProperties: true + created_at: + type: string + format: date-time + completed_at: + type: string + format: date-time + nullable: true + + HistoryV2Response: + type: object + x-runtime: [cloud] + description: "[cloud-only] Paginated execution history in v2 format." + required: + - items + - total + - has_more + properties: + items: + type: array + items: + $ref: "#/components/schemas/HistoryV2Entry" + total: + type: integer + has_more: + type: boolean + + HistoryV2Entry: + type: object + x-runtime: [cloud] + description: "[cloud-only] A single execution history entry in v2 format." + required: + - id + - status + properties: + id: + type: string + format: uuid + status: + type: string + workflow: + type: object + additionalProperties: true + outputs: + type: object + additionalProperties: true + created_at: + type: string + format: date-time + started_at: + type: string + format: date-time + nullable: true + completed_at: + type: string + format: date-time + nullable: true + preview_output: + type: object + additionalProperties: true + + CloudLogsResponse: + type: object + x-runtime: [cloud] + description: "[cloud-only] Paginated cloud execution logs." + required: + - entries + properties: + entries: + type: array + items: + type: object + properties: + timestamp: + type: string + format: date-time + level: + type: string + enum: [debug, info, warn, error] + message: + type: string + job_id: + type: string + format: uuid + total: + type: integer + has_more: + type: boolean + + AssetDownloadRequest: + type: object + x-runtime: [cloud] + description: "[cloud-only] A single asset to download to the cloud runtime." + required: + - asset_id + properties: + asset_id: + type: string + format: uuid + description: ID of the asset to download + target_path: + type: string + description: Target path on the runtime filesystem + + AssetImportRequest: + type: object + x-runtime: [cloud] + description: "[cloud-only] A single asset to import from an external URL." + required: + - url + properties: + url: + type: string + format: uri + description: URL of the asset to import + name: + type: string + description: Display name for the imported asset + tags: + type: array + items: + type: string + + RemoteAssetMetadata: + type: object + x-runtime: [cloud] + description: "[cloud-only] Metadata fetched from a remote asset URL." + properties: + content_type: + type: string + description: MIME type of the remote file + content_length: + type: integer + format: int64 + description: Size in bytes + filename: + type: string + description: Suggested filename from Content-Disposition or URL + + CloudNode: + type: object + x-runtime: [cloud] + description: "[cloud-only] An installed custom node package in the cloud runtime." + required: + - id + - name + properties: + id: + type: string + name: + type: string + version: + type: string + description: + type: string + author: + type: string + repository: + type: string + format: uri + installed_at: + type: string + format: date-time + enabled: + type: boolean + + CloudNodeList: + type: object + x-runtime: [cloud] + description: "[cloud-only] Paginated list of installed custom node packages." + required: + - nodes + properties: + nodes: + type: array + items: + $ref: "#/components/schemas/CloudNode" + total: + type: integer + has_more: + type: boolean + + HubLabel: + type: object + x-runtime: [cloud] + description: "[cloud-only] A label/category used for tagging hub content." + required: + - id + - name + properties: + id: + type: string + name: + type: string + description: + type: string + color: + type: string + description: Hex color code for the label + + HubProfile: + type: object + x-runtime: [cloud] + description: "[cloud-only] A public user profile on the ComfyUI Hub." + required: + - username + properties: + username: + type: string + display_name: + type: string + bio: + type: string + avatar_url: + type: string + format: uri + links: + type: array + items: + type: string + format: uri + workflow_count: + type: integer + created_at: + type: string + format: date-time + + HubWorkflow: + type: object + x-runtime: [cloud] + description: "[cloud-only] A published workflow on the ComfyUI Hub." + required: + - share_id + - name + properties: + share_id: + type: string + name: + type: string + description: + type: string + author: + $ref: "#/components/schemas/HubProfile" + labels: + type: array + items: + $ref: "#/components/schemas/HubLabel" + thumbnail_url: + type: string + format: uri + content: + type: object + additionalProperties: true + description: Workflow graph JSON + likes: + type: integer + views: + type: integer + forks: + type: integer + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + HubWorkflowList: + type: object + x-runtime: [cloud] + description: "[cloud-only] Paginated list of hub workflows." + required: + - workflows + - total + - has_more + properties: + workflows: + type: array + items: + $ref: "#/components/schemas/HubWorkflow" + total: + type: integer + has_more: + type: boolean + + HubWorkflowIndexEntry: + type: object + x-runtime: [cloud] + description: "[cloud-only] Lightweight entry in the hub workflow index for client-side search." + required: + - share_id + - name + properties: + share_id: + type: string + name: + type: string + author_username: + type: string + labels: + type: array + items: + type: string + likes: + type: integer + updated_at: + type: string + format: date-time + + CloudWorkflow: + type: object + x-runtime: [cloud] + description: "[cloud-only] A cloud-managed workflow with version history." + required: + - id + - name + properties: + id: + type: string + format: uuid + name: + type: string + description: + type: string + share_id: + type: string + nullable: true + description: Public share identifier if published + latest_version_id: + type: string + format: uuid + nullable: true + thumbnail_url: + type: string + format: uri + nullable: true + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + CloudWorkflowList: + type: object + x-runtime: [cloud] + description: "[cloud-only] Paginated list of cloud workflows." + required: + - workflows + - total + - has_more + properties: + workflows: + type: array + items: + $ref: "#/components/schemas/CloudWorkflow" + total: + type: integer + has_more: + type: boolean + + CloudWorkflowVersion: + type: object + x-runtime: [cloud] + description: "[cloud-only] A version of a cloud workflow." + required: + - id + - workflow_id + properties: + id: + type: string + format: uuid + workflow_id: + type: string + format: uuid + version_number: + type: integer + created_at: + type: string + format: date-time + + AuthSession: + type: object + x-runtime: [cloud] + description: "[cloud-only] Current authentication session state." + required: + - user + properties: + user: + $ref: "#/components/schemas/CloudUser" + workspace: + $ref: "#/components/schemas/Workspace" + expires_at: + type: string + format: date-time + + AuthTokenResponse: + type: object + x-runtime: [cloud] + description: "[cloud-only] OAuth2 token response." + required: + - access_token + - token_type + properties: + access_token: + type: string + token_type: + type: string + description: Always "Bearer" + expires_in: + type: integer + description: Token lifetime in seconds + refresh_token: + type: string + nullable: true + scope: + type: string + + JwksResponse: + type: object + x-runtime: [cloud] + description: "[cloud-only] JSON Web Key Set for JWT verification." + required: + - keys + properties: + keys: + type: array + items: + type: object + required: + - kty + - kid + - use + properties: + kty: + type: string + description: Key type (e.g. RSA) + kid: + type: string + description: Key ID + use: + type: string + description: Key use (e.g. sig) + alg: + type: string + description: Algorithm (e.g. RS256) + n: + type: string + description: RSA modulus (base64url) + e: + type: string + description: RSA exponent (base64url) + additionalProperties: true + + BillingBalance: + type: object + x-runtime: [cloud] + description: "[cloud-only] Current credit balance and usage summary." + required: + - credits_remaining + properties: + credits_remaining: + type: integer + description: Available credits + credits_used: + type: integer + description: Credits used in current billing period + credits_total: + type: integer + description: Total credits allocated in current period + + BillingEvent: + type: object + x-runtime: [cloud] + description: "[cloud-only] A billing event (charge, credit, refund)." + required: + - id + - type + - amount + - created_at + properties: + id: + type: string + type: + type: string + enum: [charge, credit, refund, topup, subscription] + amount: + type: integer + description: Amount in credits + description: + type: string + job_id: + type: string + format: uuid + nullable: true + created_at: + type: string + format: date-time + + BillingEventList: + type: object + x-runtime: [cloud] + description: "[cloud-only] Paginated list of billing events." + required: + - events + - total + - has_more + properties: + events: + type: array + items: + $ref: "#/components/schemas/BillingEvent" + total: + type: integer + has_more: + type: boolean + + BillingOp: + type: object + x-runtime: [cloud] + description: "[cloud-only] A billing operation record." + required: + - id + - status + properties: + id: + type: string + status: + type: string + enum: [pending, completed, failed] + type: + type: string + amount: + type: integer + created_at: + type: string + format: date-time + completed_at: + type: string + format: date-time + nullable: true + + BillingPlan: + type: object + x-runtime: [cloud] + description: "[cloud-only] A subscription plan with pricing details." + required: + - id + - name + properties: + id: + type: string + name: + type: string + description: + type: string + credits_per_month: + type: integer + price_cents: + type: integer + description: Monthly price in cents (USD) + currency: + type: string + default: usd + features: + type: array + items: + type: string + description: List of plan features + + BillingStatus: + type: object + x-runtime: [cloud] + description: "[cloud-only] Overall billing and subscription status." + properties: + subscription: + $ref: "#/components/schemas/BillingSubscription" + balance: + $ref: "#/components/schemas/BillingBalance" + has_payment_method: + type: boolean + + BillingSubscription: + type: object + x-runtime: [cloud] + description: "[cloud-only] Active subscription details." + required: + - id + - status + - plan_id + properties: + id: + type: string + status: + type: string + enum: [active, cancelled, past_due, trialing] + plan_id: + type: string + plan_name: + type: string + current_period_start: + type: string + format: date-time + current_period_end: + type: string + format: date-time + cancel_at_period_end: + type: boolean + + SubscriptionPreview: + type: object + x-runtime: [cloud] + description: "[cloud-only] Preview of a subscription change including prorations." + properties: + plan_id: + type: string + plan_name: + type: string + amount_due: + type: integer + description: Amount due in cents + proration_amount: + type: integer + description: Proration adjustment in cents + currency: + type: string + next_billing_date: + type: string + format: date-time + + Workspace: + type: object + x-runtime: [cloud] + description: "[cloud-only] A cloud workspace for team collaboration." + required: + - id + - name + properties: + id: + type: string + name: + type: string + owner_id: + type: string + member_count: + type: integer + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + WorkspaceMember: + type: object + x-runtime: [cloud] + description: "[cloud-only] A member of a cloud workspace." + required: + - user_id + - role + properties: + user_id: + type: string + email: + type: string + format: email + display_name: + type: string + avatar_url: + type: string + format: uri + role: + type: string + enum: [owner, admin, member] + joined_at: + type: string + format: date-time + + WorkspaceInvite: + type: object + x-runtime: [cloud] + description: "[cloud-only] A pending workspace invitation." + required: + - id + - email + - role + properties: + id: + type: string + email: + type: string + format: email + role: + type: string + enum: [admin, member] + invited_by: + type: string + created_at: + type: string + format: date-time + expires_at: + type: string + format: date-time + + WorkspaceApiKey: + type: object + x-runtime: [cloud] + description: "[cloud-only] A workspace API key (secret value redacted)." + required: + - id + - name + properties: + id: + type: string + name: + type: string + prefix: + type: string + description: First few characters of the key for identification + created_at: + type: string + format: date-time + last_used_at: + type: string + format: date-time + nullable: true + created_by: + type: string + + WorkspaceApiKeyCreated: + type: object + x-runtime: [cloud] + description: "[cloud-only] A newly created workspace API key, including the full secret value (shown only once)." + required: + - id + - name + - key + properties: + id: + type: string + name: + type: string + key: + type: string + description: Full API key value (only returned on creation) + prefix: + type: string + created_at: + type: string + format: date-time + + CloudUser: + type: object + x-runtime: [cloud] + description: "[cloud-only] A cloud-authenticated user profile." + required: + - id + - email + properties: + id: + type: string + email: + type: string + format: email + display_name: + type: string + avatar_url: + type: string + format: uri + created_at: + type: string + format: date-time + + SecretMeta: + type: object + x-runtime: [cloud] + description: "[cloud-only] Metadata for a stored secret (value is never returned)." + required: + - id + - name + properties: + id: + type: string + name: + type: string + provider: + type: string + description: "[cloud-only] Provider identifier (e.g., huggingface, civitai)." + x-runtime: [cloud] + last_used_at: + type: string + format: date-time + description: "[cloud-only] When the secret was last used for decryption." + x-runtime: [cloud] + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + UpdateSecretRequest: + type: object + x-runtime: [cloud] + description: "[cloud-only] Request body for updating an existing user secret." + properties: + name: + type: string + description: New name for the secret + secret_value: + type: string + description: New secret value (API key, token, etc.) + + CreateSessionResponse: + type: object + x-runtime: [cloud] + description: "[cloud-only] Response after creating a session cookie." + required: + - success + properties: + success: + type: boolean + expiresIn: + type: integer + description: Session expiration time in seconds. + + DeleteSessionResponse: + type: object + x-runtime: [cloud] + description: "[cloud-only] Response after deleting a session cookie." + required: + - success + properties: + success: + type: boolean + + CreateHubProfileRequest: + type: object + x-runtime: [cloud] + description: "[cloud-only] Request body for creating a new Hub profile." + required: + - workspace_id + - username + properties: + workspace_id: + type: string + username: + type: string + description: Unique URL-safe slug. Immutable after creation. + display_name: + type: string + description: + type: string + avatar_token: + type: string + website_urls: + type: array + items: + type: string + + PublishHubWorkflowRequest: + type: object + x-runtime: [cloud] + description: "[cloud-only] Request body for publishing or updating a workflow on the Hub." + required: + - username + - name + - workflow_filename + - asset_ids + properties: + username: + type: string + name: + type: string + workflow_filename: + type: string + asset_ids: + type: array + items: + type: string + description: + type: string + tags: + type: array + items: + type: string + models: + type: array + items: + type: string + custom_nodes: + type: array + items: + type: string + tutorial_url: + type: string + metadata: + type: object + additionalProperties: true + thumbnail_type: + type: string + enum: [image, video, image_comparison] + thumbnail_token_or_url: + type: string + thumbnail_comparison_token_or_url: + type: string + sample_image_tokens_or_urls: + type: array + items: + type: string + + HubWorkflowDetail: + type: object + x-runtime: [cloud] + description: "[cloud-only] Full Hub workflow detail including versions, assets, and statistics." + required: + - share_id + - workflow_id + - name + - workflow_json + - assets + - profile + - status + properties: + share_id: + type: string + workflow_id: + type: string + name: + type: string + status: + type: string + enum: [pending, approved, rejected, deprecated] + description: + type: string + thumbnail_type: + type: string + enum: [image, video, image_comparison] + thumbnail_url: + type: string + thumbnail_comparison_url: + type: string + tutorial_url: + type: string + metadata: + type: object + additionalProperties: true + sample_image_urls: + type: array + items: + type: string + publish_time: + type: string + format: date-time + nullable: true + workflow_json: + type: object + additionalProperties: true + assets: + type: array + items: + $ref: "#/components/schemas/AssetInfo" + profile: + $ref: "#/components/schemas/HubProfile" + + AssetInfo: + type: object + x-runtime: [cloud] + description: "[cloud-only] Lightweight asset reference used in workflow publishing payloads." + required: + - id + - filename + properties: + id: + type: string + filename: + type: string + mime_type: + type: string + size_bytes: + type: integer + format: int64 + + BulkRevokeAPIKeysResponse: + type: object + x-runtime: [cloud] + description: "[cloud-only] Response after bulk-revoking API keys for a workspace member." + required: + - revoked_count + properties: + revoked_count: + type: integer + minimum: 0 + + CreateWorkflowVersionRequest: + type: object + x-runtime: [cloud] + description: "[cloud-only] Request body for creating a new version of a saved workflow." + required: + - base_version + - workflow_json + properties: + base_version: + type: integer + description: Version number this change is based on (for optimistic concurrency). + workflow_json: + type: object + additionalProperties: true + + WorkflowVersionResponse: + type: object + x-runtime: [cloud] + description: "[cloud-only] Metadata for a single workflow version." + required: + - id + - version + - latest_version + - created_by + - created_at + properties: + id: + type: string + version: + type: integer + latest_version: + type: integer + created_by: + type: string + created_at: + type: string + format: date-time + + WorkflowPublishInfo: + type: object + x-runtime: [cloud] + description: "[cloud-only] Publishing metadata for a workflow shared to the Hub." + required: + - workflow_id + - share_id + - listed + - assets + properties: + workflow_id: + type: string + share_id: + type: string + publish_time: + type: string + format: date-time + nullable: true + listed: + type: boolean + assets: + type: array + items: + $ref: "#/components/schemas/AssetInfo" + + TaskEntry: + type: object + x-runtime: [cloud] + description: "[cloud-only] Task data for list views." + required: + - id + - task_name + - status + - create_time + properties: + id: + type: string + format: uuid + task_name: + type: string + status: + type: string + enum: [created, running, completed, failed] + create_time: + type: string + format: date-time + started_at: + type: string + format: date-time + completed_at: + type: string + format: date-time + + TaskResponse: + type: object + x-runtime: [cloud] + description: "[cloud-only] Full task details including payload and result." + required: + - id + - idempotency_key + - task_name + - payload + - status + - create_time + - update_time + properties: + id: + type: string + format: uuid + idempotency_key: + type: string + task_name: + type: string + payload: + type: object + additionalProperties: true + status: + type: string + enum: [created, running, completed, failed] + result: + type: object + additionalProperties: true + create_time: + type: string + format: date-time + update_time: + type: string + format: date-time + started_at: + type: string + format: date-time + completed_at: + type: string + format: date-time + error: + type: string + + TasksListResponse: + type: object + x-runtime: [cloud] + description: "[cloud-only] Paginated list of background tasks for the authenticated user." + required: + - tasks + - pagination + properties: + tasks: + type: array + items: + $ref: "#/components/schemas/TaskEntry" + pagination: + $ref: "#/components/schemas/PaginationInfo" \ No newline at end of file From 65045730a60af0bf75cec2a738555a952da2ea4e Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Fri, 8 May 2026 23:11:52 +0300 Subject: [PATCH 024/145] [Partner Nodes] additionally use Baidu server to detect the accessibility of internet (#13803) Signed-off-by: bigcat88 --- comfy_api_nodes/util/client.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/comfy_api_nodes/util/client.py b/comfy_api_nodes/util/client.py index 8e1ba91ba..052301c33 100644 --- a/comfy_api_nodes/util/client.py +++ b/comfy_api_nodes/util/client.py @@ -488,10 +488,30 @@ async def _diagnose_connectivity() -> dict[str, bool]: "api_accessible": False, } timeout = aiohttp.ClientTimeout(total=5.0) + + # Probe Google and Baidu in parallel: Google is blocked by the GFW in mainland China, so a Baidu probe is required + # to correctly detect that Chinese users with working internet do have working internet. + internet_probe_urls = ("https://www.google.com", "https://www.baidu.com") + async with aiohttp.ClientSession(timeout=timeout) as session: - with contextlib.suppress(ClientError, OSError): - async with session.get("https://www.google.com") as resp: - results["internet_accessible"] = resp.status < 500 + async def _probe(url: str) -> bool: + try: + async with session.get(url) as resp: + return resp.status < 500 + except (ClientError, OSError, asyncio.TimeoutError): + return False + + probe_tasks = [asyncio.create_task(_probe(u)) for u in internet_probe_urls] + try: + for fut in asyncio.as_completed(probe_tasks): + if await fut: + results["internet_accessible"] = True + break + finally: + for t in probe_tasks: + if not t.done(): + t.cancel() + await asyncio.gather(*probe_tasks, return_exceptions=True) if not results["internet_accessible"]: return results From 66669b2ded7d8f362fdf64bb1c77a8df0f684e2f Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Fri, 8 May 2026 17:32:14 -0700 Subject: [PATCH 025/145] I don't think there was any because nobody complained. (#13807) --- comfy/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy/utils.py b/comfy/utils.py index 7b7faad3a..91e1ba3d3 100644 --- a/comfy/utils.py +++ b/comfy/utils.py @@ -1390,7 +1390,7 @@ def convert_old_quants(state_dict, model_prefix="", metadata={}): k_out = "{}.weight_scale".format(layer) if layer is not None: - layer_conf = {"format": "float8_e4m3fn"} # TODO: check if anyone did some non e4m3fn scaled checkpoints + layer_conf = {"format": "float8_e4m3fn"} if full_precision_matrix_mult: layer_conf["full_precision_matrix_mult"] = full_precision_matrix_mult layers[layer] = layer_conf From 4e823431cc8291deced4fc2dcf3967be2549e4c0 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Fri, 8 May 2026 19:14:23 -0700 Subject: [PATCH 026/145] Add cloud-runtime experiment node-schema endpoints to spec (#13806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add cloud-runtime experiment node-schema endpoints to spec Replace the GET operations at /api/experiment/nodes and /api/experiment/nodes/{id} with getNodeInfoSchema and getNodeByID — the optimized, ETag-tagged object_info schema endpoints the cloud frontend depends on for the workflow editor. Each operation is tagged x-runtime: [cloud] and uses the runtime-only tag for cloud-side codegen exclusion. Response headers document the ETag and Cache-Control validators; 304 Not Modified is declared for RFC 7232 conditional GETs. Remove the now-unused CloudNodeList schema to keep Spectral clean. Co-authored-by: Matt Miller * spec: document If-None-Match header on conditional GET endpoints Both `getNodeInfoSchema` and `getNodeByID` advertise `ETag` response headers and a `304 Not Modified` response, but the spec didn't declare the `If-None-Match` request header that triggers conditional validation. Adding it as an optional header parameter on both ops so client codegen exposes the conditional-GET pattern. --- openapi.yaml | 106 +++++++++++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 55 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index 4216c1a6c..d4c9e67ca 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -74,6 +74,8 @@ tags: description: Cloud workflow management and versioning (cloud-only) - name: task description: Background task management (cloud-only) + - name: runtime-only + description: Operations served exclusively by the cloud runtime with no local equivalent paths: # --------------------------------------------------------------------------- @@ -2573,35 +2575,38 @@ paths: # --------------------------------------------------------------------------- /api/experiment/nodes: get: - operationId: listCloudNodes - tags: [node] - summary: List installed custom nodes - description: "[cloud-only] Returns the list of custom node packages installed in the cloud runtime." + operationId: getNodeInfoSchema + tags: [runtime-only] + summary: Get pre-rendered node info schema + description: "[cloud-only] Returns the static ComfyUI object_info schema, identical for every caller, rendered once at startup with empty model/user-file context. Served by a raw HTTP handler that writes pre-rendered bytes with ETag + Cache-Control validators for RFC 7232 conditional GETs." x-runtime: [cloud] parameters: - - name: limit - in: query + - name: If-None-Match + in: header + required: false schema: - type: integer - description: Maximum number of results - - name: offset - in: query - schema: - type: integer - description: Pagination offset + type: string + description: Entity tag previously returned by this endpoint. When present and matching, the server returns 304 Not Modified. responses: "200": - description: Custom node list + description: Node info schema + headers: + ETag: + schema: + type: string + description: Entity tag for conditional request validation + Cache-Control: + schema: + type: string + description: Cache directives for the response content: application/json: schema: - $ref: "#/components/schemas/CloudNodeList" - "401": - description: Unauthorized - content: - application/json: - schema: - $ref: "#/components/schemas/CloudError" + type: object + additionalProperties: + $ref: "#/components/schemas/NodeInfo" + "304": + description: Not Modified — returned when the client sends a matching If-None-Match header post: operationId: installCloudNode tags: [node] @@ -2651,10 +2656,10 @@ paths: /api/experiment/nodes/{id}: get: - operationId: getCloudNode - tags: [node] - summary: Get details of an installed custom node - description: "[cloud-only] Returns details about a specific installed custom node package." + operationId: getNodeByID + tags: [runtime-only] + summary: Get a single node definition by ID + description: "[cloud-only] Returns one node's definition from the pre-indexed object_info schema. Served by a raw HTTP handler that writes pre-rendered bytes with ETag + Cache-Control validators for RFC 7232 conditional GETs." x-runtime: [cloud] parameters: - name: id @@ -2662,26 +2667,33 @@ paths: required: true schema: type: string - description: Custom node package ID + description: Node class identifier + - name: If-None-Match + in: header + required: false + schema: + type: string + description: Entity tag previously returned by this endpoint. When present and matching, the server returns 304 Not Modified. responses: "200": - description: Node detail + description: Single node definition + headers: + ETag: + schema: + type: string + description: Entity tag for conditional request validation + Cache-Control: + schema: + type: string + description: Cache directives for the response content: application/json: schema: - $ref: "#/components/schemas/CloudNode" - "401": - description: Unauthorized - content: - application/json: - schema: - $ref: "#/components/schemas/CloudError" + $ref: "#/components/schemas/NodeInfo" + "304": + description: Not Modified — returned when the client sends a matching If-None-Match header "404": - description: Not found - content: - application/json: - schema: - $ref: "#/components/schemas/CloudError" + description: Node not found delete: operationId: uninstallCloudNode tags: [node] @@ -7100,22 +7112,6 @@ components: enabled: type: boolean - CloudNodeList: - type: object - x-runtime: [cloud] - description: "[cloud-only] Paginated list of installed custom node packages." - required: - - nodes - properties: - nodes: - type: array - items: - $ref: "#/components/schemas/CloudNode" - total: - type: integer - has_more: - type: boolean - HubLabel: type: object x-runtime: [cloud] From 8b08bfdcbe2b4cd8f4426bd1111aaf17b118e33d Mon Sep 17 00:00:00 2001 From: lin-bot23 Date: Sat, 9 May 2026 12:26:13 +0900 Subject: [PATCH 027/145] Add description field to blueprint subgraphs (#13797) * Add description field to all blueprint subgraphs Sets the 'description' field on every subgraph blueprint node, which will show on the node preview and tooltip. Covers all 51 blueprint files under blueprints/. * Update blueprint descriptions with researched model info * Refine blueprint descriptions with researched model specs from docs Updates subgraph descriptions across all 51 blueprints with accurate model details drawn from ComfyUI docs, including: - Flux.1 Dev: 12B open-weights, Pro-level quality - Flux.2 Klein 4B: fastest Flux, distilled architecture - Qwen-Image: 20B MMDiT, multilingual text rendering - Z-Image-Turbo: distilled 6B DiT, sub-second inference - LTX-2/2.3: 19B DiT audio-video foundation model - Wan2.2: open-source, 14B/1.3B variants - ACE-Step 1.5: ~1s full-song generation - GPU shader nodes consistently labeled as fragment shaders * Strip marketing fluff and license info from descriptions * Fix Canny to Video (LTX 2.0) description * Remove 'local-' prefix from subgraph names * Preserve UTF-8 encoding in JSON files (ensure_ascii=False) * Apply review suggestions from alexisrolland - Rename 'Image to Model (Hunyuan3d 2.1)' -> 'Image to 3D Model (Hunyuan3d 2.1)' - Rename 'Image Upscale(Z-image-Turbo)' -> 'Image Upscale (Z-image-Turbo)' - Rename 'Video Inpaint(Wan2.1 VACE)' -> 'Video Inpaint (Wan 2.1 VACE)' - Use 'Black Forest Labs' branding in Flux descriptions - Use 'Google's Gemini' with possessive in captioning nodes - Normalize 'Wan 2.2' and 'Wan 2.1' spacing in descriptions * fix: revert Color Adjustment.json to preserve original GLSL shader content Only adds the 'description' field without modifying the shader code (which contained Unicode escape \\u2192 that should be preserved). * Apply CodeRabbit review suggestions - Color Adjustment: include vibrance in description - Image Blur: expand to Gaussian/Box/Radial modes - Flux.2 Klein 4B: narrow to image edit only (no T2I) - NetaYume Lumina: correct model base (Neta Lumina, not Lumina-Next) --------- Co-authored-by: linmoumou Co-authored-by: Daxiong (Lin) --- blueprints/Brightness and Contrast.json | 5 +++-- blueprints/Canny to Image (Z-Image-Turbo).json | 7 ++++--- blueprints/Canny to Video (LTX 2.0).json | 7 ++++--- blueprints/Chromatic Aberration.json | 5 +++-- blueprints/Color Adjustment.json | 3 ++- blueprints/Color Balance.json | 3 ++- blueprints/Color Curves.json | 3 ++- blueprints/Crop Images 2x2.json | 3 ++- blueprints/Crop Images 3x3.json | 3 ++- blueprints/Depth to Image (Z-Image-Turbo).json | 6 ++++-- blueprints/Depth to Video (ltx 2.0).json | 6 ++++-- blueprints/Edge-Preserving Blur.json | 5 +++-- blueprints/Film Grain.json | 5 +++-- blueprints/First-Last-Frame to Video (LTX-2.3).json | 3 ++- blueprints/Glow.json | 5 +++-- blueprints/Hue and Saturation.json | 5 +++-- blueprints/Image Blur.json | 3 ++- blueprints/Image Captioning (gemini).json | 3 ++- blueprints/Image Channels.json | 5 +++-- blueprints/Image Edit (FireRed Image Edit 1.1).json | 3 ++- blueprints/Image Edit (Flux.2 Klein 4B).json | 8 +++++--- blueprints/Image Edit (LongCat Image Edit).json | 3 ++- blueprints/Image Edit (Qwen 2511).json | 7 ++++--- blueprints/Image Inpainting (Flux.1 Fill Dev).json | 5 +++-- blueprints/Image Inpainting (Qwen-image).json | 6 ++++-- blueprints/Image Levels.json | 5 +++-- blueprints/Image Outpainting (Qwen-Image).json | 9 ++++++--- blueprints/Image Upscale(Z-image-Turbo).json | 5 +++-- blueprints/Image to Depth Map (Lotus).json | 7 ++++--- blueprints/Image to Layers(Qwen-Image-Layered).json | 3 ++- blueprints/Image to Model (Hunyuan3d 2.1).json | 5 +++-- blueprints/Image to Video (LTX-2.3).json | 3 ++- blueprints/Image to Video (Wan 2.2).json | 5 +++-- blueprints/Pose to Image (Z-Image-Turbo).json | 7 ++++--- blueprints/Pose to Video (LTX 2.0).json | 3 ++- blueprints/Prompt Enhance.json | 5 +++-- blueprints/Sharpen.json | 5 +++-- blueprints/Text to Audio (ACE-Step 1.5).json | 7 ++++--- blueprints/Text to Image (Flux.1 Dev).json | 5 +++-- blueprints/Text to Image (Flux.1 Krea Dev).json | 5 +++-- blueprints/Text to Image (NetaYume Lumina).json | 8 +++++--- blueprints/Text to Image (Qwen-Image 2512).json | 3 ++- blueprints/Text to Image (Qwen-Image).json | 3 ++- blueprints/Text to Image (Z-Image-Turbo).json | 7 ++++--- blueprints/Text to Video (LTX-2.3).json | 3 ++- blueprints/Text to Video (Wan 2.2).json | 5 +++-- blueprints/Unsharp Mask.json | 5 +++-- blueprints/Video Captioning (Gemini).json | 3 ++- blueprints/Video Inpaint(Wan2.1 VACE).json | 5 +++-- blueprints/Video Stitch.json | 5 +++-- blueprints/Video Upscale(GAN x4).json | 5 +++-- 51 files changed, 153 insertions(+), 95 deletions(-) diff --git a/blueprints/Brightness and Contrast.json b/blueprints/Brightness and Contrast.json index 90bfe999d..78fc52f29 100644 --- a/blueprints/Brightness and Contrast.json +++ b/blueprints/Brightness and Contrast.json @@ -431,9 +431,10 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Adjusts image brightness and contrast using a real-time GPU fragment shader." } ] }, "extra": {} -} +} \ No newline at end of file diff --git a/blueprints/Canny to Image (Z-Image-Turbo).json b/blueprints/Canny to Image (Z-Image-Turbo).json index ff9717308..14deb64cc 100644 --- a/blueprints/Canny to Image (Z-Image-Turbo).json +++ b/blueprints/Canny to Image (Z-Image-Turbo).json @@ -162,7 +162,7 @@ }, "revision": 0, "config": {}, - "name": "local-Canny to Image (Z-Image-Turbo)", + "name": "Canny to Image (Z-Image-Turbo)", "inputNode": { "id": -10, "bounding": [ @@ -1553,7 +1553,8 @@ "VHS_MetadataImage": true, "VHS_KeepIntermediate": true }, - "category": "Image generation and editing/Canny to image" + "category": "Image generation and editing/Canny to image", + "description": "Generates an image from a Canny edge map using Z-Image-Turbo, with text conditioning." } ] }, @@ -1574,4 +1575,4 @@ } }, "version": 0.4 -} +} \ No newline at end of file diff --git a/blueprints/Canny to Video (LTX 2.0).json b/blueprints/Canny to Video (LTX 2.0).json index fae8321b9..a9682c8a4 100644 --- a/blueprints/Canny to Video (LTX 2.0).json +++ b/blueprints/Canny to Video (LTX 2.0).json @@ -192,7 +192,7 @@ }, "revision": 0, "config": {}, - "name": "local-Canny to Video (LTX 2.0)", + "name": "Canny to Video (LTX 2.0)", "inputNode": { "id": -10, "bounding": [ @@ -3600,7 +3600,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Video generation and editing/Canny to video" + "category": "Video generation and editing/Canny to video", + "description": "Generates video from Canny edge maps using LTX-2, with optional synchronized audio." } ] }, @@ -3616,4 +3617,4 @@ } }, "version": 0.4 -} +} \ No newline at end of file diff --git a/blueprints/Chromatic Aberration.json b/blueprints/Chromatic Aberration.json index ae8037b1b..893fb1190 100644 --- a/blueprints/Chromatic Aberration.json +++ b/blueprints/Chromatic Aberration.json @@ -377,8 +377,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Adds lens-style chromatic aberration (color fringing) using a real-time GPU fragment shader." } ] } -} +} \ No newline at end of file diff --git a/blueprints/Color Adjustment.json b/blueprints/Color Adjustment.json index 622bf28af..5abbf8baa 100644 --- a/blueprints/Color Adjustment.json +++ b/blueprints/Color Adjustment.json @@ -596,7 +596,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Adjusts saturation, temperature, tint, and vibrance using a real-time GPU fragment shader." } ] } diff --git a/blueprints/Color Balance.json b/blueprints/Color Balance.json index 21d6319ed..d921eab37 100644 --- a/blueprints/Color Balance.json +++ b/blueprints/Color Balance.json @@ -1129,7 +1129,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Balances colors across shadows, midtones, and highlights using a real-time GPU fragment shader." } ] } diff --git a/blueprints/Color Curves.json b/blueprints/Color Curves.json index 1461cf396..b9bfb7029 100644 --- a/blueprints/Color Curves.json +++ b/blueprints/Color Curves.json @@ -608,7 +608,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Fine-tunes tone and color with per-channel curve adjustments using a real-time GPU fragment shader." } ] } diff --git a/blueprints/Crop Images 2x2.json b/blueprints/Crop Images 2x2.json index 2aa42cfc3..99b89b608 100644 --- a/blueprints/Crop Images 2x2.json +++ b/blueprints/Crop Images 2x2.json @@ -1609,7 +1609,8 @@ } ], "extra": {}, - "category": "Image Tools/Crop" + "category": "Image Tools/Crop", + "description": "Splits an image into a 2×2 grid of four equal tiles." } ] }, diff --git a/blueprints/Crop Images 3x3.json b/blueprints/Crop Images 3x3.json index 3a3615ac8..6ac636da4 100644 --- a/blueprints/Crop Images 3x3.json +++ b/blueprints/Crop Images 3x3.json @@ -2946,7 +2946,8 @@ } ], "extra": {}, - "category": "Image Tools/Crop" + "category": "Image Tools/Crop", + "description": "Splits an image into a 3×3 grid of nine equal tiles." } ] }, diff --git a/blueprints/Depth to Image (Z-Image-Turbo).json b/blueprints/Depth to Image (Z-Image-Turbo).json index 4f69a8149..fe9ef0f72 100644 --- a/blueprints/Depth to Image (Z-Image-Turbo).json +++ b/blueprints/Depth to Image (Z-Image-Turbo).json @@ -1579,7 +1579,8 @@ "VHS_MetadataImage": true, "VHS_KeepIntermediate": true }, - "category": "Image generation and editing/Depth to image" + "category": "Image generation and editing/Depth to image", + "description": "Generates an image from a depth map using Z-Image-Turbo with text conditioning." }, { "id": "458bdf3c-4b58-421c-af50-c9c663a4d74c", @@ -2461,7 +2462,8 @@ ] }, "workflowRendererVersion": "LG" - } + }, + "description": "Estimates a monocular depth map from an input image using the Lotus depth estimation model." } ] }, diff --git a/blueprints/Depth to Video (ltx 2.0).json b/blueprints/Depth to Video (ltx 2.0).json index f15212520..bb28695a2 100644 --- a/blueprints/Depth to Video (ltx 2.0).json +++ b/blueprints/Depth to Video (ltx 2.0).json @@ -4233,7 +4233,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Video generation and editing/Depth to video" + "category": "Video generation and editing/Depth to video", + "description": "Generates video from depth maps using LTX-2, with optional synchronized audio." }, { "id": "38b60539-50a7-42f9-a5fe-bdeca26272e2", @@ -5192,7 +5193,8 @@ ], "extra": { "workflowRendererVersion": "LG" - } + }, + "description": "Estimates a monocular depth map from an input image using the Lotus depth estimation model." } ] }, diff --git a/blueprints/Edge-Preserving Blur.json b/blueprints/Edge-Preserving Blur.json index 18012beb1..fbda9f126 100644 --- a/blueprints/Edge-Preserving Blur.json +++ b/blueprints/Edge-Preserving Blur.json @@ -450,9 +450,10 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Blur" + "category": "Image Tools/Blur", + "description": "Applies bilateral (edge-preserving) blur to soften images while retaining detail." } ] }, "extra": {} -} +} \ No newline at end of file diff --git a/blueprints/Film Grain.json b/blueprints/Film Grain.json index a680b3ece..3226ea9aa 100644 --- a/blueprints/Film Grain.json +++ b/blueprints/Film Grain.json @@ -580,8 +580,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Adds procedural film grain texture for a cinematic look via GPU fragment shader." } ] } -} +} \ No newline at end of file diff --git a/blueprints/First-Last-Frame to Video (LTX-2.3).json b/blueprints/First-Last-Frame to Video (LTX-2.3).json index 8ec9ed61a..f509aefe0 100644 --- a/blueprints/First-Last-Frame to Video (LTX-2.3).json +++ b/blueprints/First-Last-Frame to Video (LTX-2.3).json @@ -3350,7 +3350,8 @@ } ], "extra": {}, - "category": "Video generation and editing/First-Last-Frame to Video" + "category": "Video generation and editing/First-Last-Frame to Video", + "description": "Generates a video interpolating between first and last keyframes using LTX-2.3." } ] }, diff --git a/blueprints/Glow.json b/blueprints/Glow.json index 1dafb2d35..2bbfdee51 100644 --- a/blueprints/Glow.json +++ b/blueprints/Glow.json @@ -575,8 +575,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Adds a glow/bloom effect around bright image areas via GPU fragment shader." } ] } -} +} \ No newline at end of file diff --git a/blueprints/Hue and Saturation.json b/blueprints/Hue and Saturation.json index 1a2df8937..cddf0154a 100644 --- a/blueprints/Hue and Saturation.json +++ b/blueprints/Hue and Saturation.json @@ -752,8 +752,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Adjusts hue, saturation, and lightness of an image using a real-time GPU fragment shader." } ] } -} +} \ No newline at end of file diff --git a/blueprints/Image Blur.json b/blueprints/Image Blur.json index 3c7a784b0..0ca8d9931 100644 --- a/blueprints/Image Blur.json +++ b/blueprints/Image Blur.json @@ -374,7 +374,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Blur" + "category": "Image Tools/Blur", + "description": "Applies Gaussian, Box, or Radial blur to soften images and create stylized depth or motion effects." } ] } diff --git a/blueprints/Image Captioning (gemini).json b/blueprints/Image Captioning (gemini).json index 98cfb8999..2fc5d6746 100644 --- a/blueprints/Image Captioning (gemini).json +++ b/blueprints/Image Captioning (gemini).json @@ -310,7 +310,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Text generation/Image Captioning" + "category": "Text generation/Image Captioning", + "description": "Generates descriptive captions for images using Google's Gemini multimodal LLM." } ] } diff --git a/blueprints/Image Channels.json b/blueprints/Image Channels.json index 9c7b675b2..b6fdff5be 100644 --- a/blueprints/Image Channels.json +++ b/blueprints/Image Channels.json @@ -315,8 +315,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Manipulates individual RGBA channels for masking, compositing, and channel effects." } ] } -} +} \ No newline at end of file diff --git a/blueprints/Image Edit (FireRed Image Edit 1.1).json b/blueprints/Image Edit (FireRed Image Edit 1.1).json index c34246ce6..14310353c 100644 --- a/blueprints/Image Edit (FireRed Image Edit 1.1).json +++ b/blueprints/Image Edit (FireRed Image Edit 1.1).json @@ -2138,7 +2138,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Edit image" + "category": "Image generation and editing/Edit image", + "description": "Edits images via text instructions using FireRed Image Edit 1.1, a diffusion-based instruction-following editing model." } ] }, diff --git a/blueprints/Image Edit (Flux.2 Klein 4B).json b/blueprints/Image Edit (Flux.2 Klein 4B).json index 6f2f7dc01..7f6fa7a4b 100644 --- a/blueprints/Image Edit (Flux.2 Klein 4B).json +++ b/blueprints/Image Edit (Flux.2 Klein 4B).json @@ -1472,7 +1472,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Edit image" + "category": "Image generation and editing/Edit image", + "description": "Edits an input image via text instructions using FLUX.2 [klein] 4B." }, { "id": "6007e698-2ebd-4917-84d8-299b35d7b7ab", @@ -1821,7 +1822,8 @@ ], "extra": { "workflowRendererVersion": "LG" - } + }, + "description": "Applies reference image conditioning for style/identity transfer (Flux.2 Klein 4B)." } ] }, @@ -1837,4 +1839,4 @@ } }, "version": 0.4 -} \ No newline at end of file +} diff --git a/blueprints/Image Edit (LongCat Image Edit).json b/blueprints/Image Edit (LongCat Image Edit).json index 5b4eb18f0..de1c155a2 100644 --- a/blueprints/Image Edit (LongCat Image Edit).json +++ b/blueprints/Image Edit (LongCat Image Edit).json @@ -1417,7 +1417,8 @@ } ], "extra": {}, - "category": "Image generation and editing/Edit image" + "category": "Image generation and editing/Edit image", + "description": "Edits images via text instructions using LongCat Image Edit, an instruction-following image editing diffusion model." } ] }, diff --git a/blueprints/Image Edit (Qwen 2511).json b/blueprints/Image Edit (Qwen 2511).json index 582171fa0..1aa7e5765 100644 --- a/blueprints/Image Edit (Qwen 2511).json +++ b/blueprints/Image Edit (Qwen 2511).json @@ -132,7 +132,7 @@ }, "revision": 0, "config": {}, - "name": "local-Image Edit (Qwen 2511)", + "name": "Image Edit (Qwen 2511)", "inputNode": { "id": -10, "bounding": [ @@ -1468,7 +1468,8 @@ "VHS_MetadataImage": true, "VHS_KeepIntermediate": true }, - "category": "Image generation and editing/Edit image" + "category": "Image generation and editing/Edit image", + "description": "Edits images via text instructions using Qwen-Image-Edit-2511 with improved character consistency and integrated LoRA." } ] }, @@ -1489,4 +1490,4 @@ } }, "version": 0.4 -} +} \ No newline at end of file diff --git a/blueprints/Image Inpainting (Flux.1 Fill Dev).json b/blueprints/Image Inpainting (Flux.1 Fill Dev).json index d40d63594..c1326ed3d 100644 --- a/blueprints/Image Inpainting (Flux.1 Fill Dev).json +++ b/blueprints/Image Inpainting (Flux.1 Fill Dev).json @@ -1188,7 +1188,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Inpaint image" + "category": "Image generation and editing/Inpaint image", + "description": "Inpaints masked image regions using Flux.1 fill [dev], Black Forest Labs' inpainting/outpainting model." } ] }, @@ -1202,4 +1203,4 @@ }, "ue_links": [] } -} \ No newline at end of file +} diff --git a/blueprints/Image Inpainting (Qwen-image).json b/blueprints/Image Inpainting (Qwen-image).json index 95b2909fa..a06d57e19 100644 --- a/blueprints/Image Inpainting (Qwen-image).json +++ b/blueprints/Image Inpainting (Qwen-image).json @@ -1548,7 +1548,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Inpaint image" + "category": "Image generation and editing/Inpaint image", + "description": "Inpaints masked regions using Qwen-Image, extending its multilingual text rendering to inpainting tasks." }, { "id": "56a1f603-fbd2-40ed-94ef-c9ecbd96aca8", @@ -1907,7 +1908,8 @@ ], "extra": { "workflowRendererVersion": "LG" - } + }, + "description": "Expands and softens mask edges to reduce visible seams after image processing." } ] }, diff --git a/blueprints/Image Levels.json b/blueprints/Image Levels.json index ef256a1aa..1a1b18932 100644 --- a/blueprints/Image Levels.json +++ b/blueprints/Image Levels.json @@ -742,9 +742,10 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Adjusts black point, white point, and gamma for tonal range control via GPU shader." } ] }, "extra": {} -} +} \ No newline at end of file diff --git a/blueprints/Image Outpainting (Qwen-Image).json b/blueprints/Image Outpainting (Qwen-Image).json index 218fdc775..6c07227c0 100644 --- a/blueprints/Image Outpainting (Qwen-Image).json +++ b/blueprints/Image Outpainting (Qwen-Image).json @@ -1919,7 +1919,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Outpaint image" + "category": "Image generation and editing/Outpaint image", + "description": "Outpaints beyond image boundaries using Qwen-Image's outpainting capabilities." }, { "id": "f93c215e-c393-460e-9534-ed2c3d8a652e", @@ -2278,7 +2279,8 @@ ], "extra": { "workflowRendererVersion": "LG" - } + }, + "description": "Expands and softens mask edges to reduce visible seams after image processing." }, { "id": "2a4b2cc0-db37-4302-a067-da392f38f06b", @@ -2733,7 +2735,8 @@ ], "extra": { "workflowRendererVersion": "LG" - } + }, + "description": "Scales both image and mask together while preserving alignment for editing workflows." } ] }, diff --git a/blueprints/Image Upscale(Z-image-Turbo).json b/blueprints/Image Upscale(Z-image-Turbo).json index 0d2b6e240..bd803a0b1 100644 --- a/blueprints/Image Upscale(Z-image-Turbo).json +++ b/blueprints/Image Upscale(Z-image-Turbo).json @@ -141,7 +141,7 @@ }, "revision": 0, "config": {}, - "name": "local-Image Upscale(Z-image-Turbo)", + "name": "Image Upscale (Z-image-Turbo)", "inputNode": { "id": -10, "bounding": [ @@ -1302,7 +1302,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Enhance" + "category": "Image generation and editing/Enhance", + "description": "Upscales images to higher resolution using Z-Image-Turbo." } ] }, diff --git a/blueprints/Image to Depth Map (Lotus).json b/blueprints/Image to Depth Map (Lotus).json index 089f2cd42..12f10ba5b 100644 --- a/blueprints/Image to Depth Map (Lotus).json +++ b/blueprints/Image to Depth Map (Lotus).json @@ -99,7 +99,7 @@ }, "revision": 0, "config": {}, - "name": "local-Image to Depth Map (Lotus)", + "name": "Image to Depth Map (Lotus)", "inputNode": { "id": -10, "bounding": [ @@ -948,7 +948,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Depth to image" + "category": "Image generation and editing/Depth to image", + "description": "Estimates a monocular depth map from an input image using the Lotus depth estimation model." } ] }, @@ -964,4 +965,4 @@ "workflowRendererVersion": "LG" }, "version": 0.4 -} +} \ No newline at end of file diff --git a/blueprints/Image to Layers(Qwen-Image-Layered).json b/blueprints/Image to Layers(Qwen-Image-Layered).json index 8a525e7a5..7b44f0563 100644 --- a/blueprints/Image to Layers(Qwen-Image-Layered).json +++ b/blueprints/Image to Layers(Qwen-Image-Layered).json @@ -1586,7 +1586,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Image to layers" + "category": "Image generation and editing/Image to layers", + "description": "Decomposes an image into variable-resolution RGBA layers for independent editing using Qwen-Image-Layered." } ] }, diff --git a/blueprints/Image to Model (Hunyuan3d 2.1).json b/blueprints/Image to Model (Hunyuan3d 2.1).json index 4705603a8..ee5552656 100644 --- a/blueprints/Image to Model (Hunyuan3d 2.1).json +++ b/blueprints/Image to Model (Hunyuan3d 2.1).json @@ -72,7 +72,7 @@ }, "revision": 0, "config": {}, - "name": "local-Image to Model (Hunyuan3d 2.1)", + "name": "Image to 3D Model (Hunyuan3d 2.1)", "inputNode": { "id": -10, "bounding": [ @@ -765,7 +765,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "3D/Image to 3D Model" + "category": "3D/Image to 3D Model", + "description": "Generates 3D mesh models from a single input image using Hunyuan3D 2.0/2.1." } ] }, diff --git a/blueprints/Image to Video (LTX-2.3).json b/blueprints/Image to Video (LTX-2.3).json index 86a601130..3db524ea0 100644 --- a/blueprints/Image to Video (LTX-2.3).json +++ b/blueprints/Image to Video (LTX-2.3).json @@ -4223,7 +4223,8 @@ "extra": { "workflowRendererVersion": "Vue-corrected" }, - "category": "Video generation and editing/Image to video" + "category": "Video generation and editing/Image to video", + "description": "Generates video from a single input image using LTX-2.3." } ] }, diff --git a/blueprints/Image to Video (Wan 2.2).json b/blueprints/Image to Video (Wan 2.2).json index a8dafd3c9..3510aad18 100644 --- a/blueprints/Image to Video (Wan 2.2).json +++ b/blueprints/Image to Video (Wan 2.2).json @@ -206,7 +206,7 @@ }, "revision": 0, "config": {}, - "name": "local-Image to Video (Wan 2.2)", + "name": "Image to Video (Wan 2.2)", "inputNode": { "id": -10, "bounding": [ @@ -2027,7 +2027,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Video generation and editing/Image to video" + "category": "Video generation and editing/Image to video", + "description": "Generates video from an image and text prompt using Wan 2.2, supporting T2V and I2V." } ] }, diff --git a/blueprints/Pose to Image (Z-Image-Turbo).json b/blueprints/Pose to Image (Z-Image-Turbo).json index a55410ba4..5c2749efe 100644 --- a/blueprints/Pose to Image (Z-Image-Turbo).json +++ b/blueprints/Pose to Image (Z-Image-Turbo).json @@ -134,7 +134,7 @@ }, "revision": 0, "config": {}, - "name": "local-Pose to Image (Z-Image-Turbo)", + "name": "Pose to Image (Z-Image-Turbo)", "inputNode": { "id": -10, "bounding": [ @@ -1298,7 +1298,8 @@ "VHS_MetadataImage": true, "VHS_KeepIntermediate": true }, - "category": "Image generation and editing/Pose to image" + "category": "Image generation and editing/Pose to image", + "description": "Generates an image from pose keypoints using Z-Image-Turbo with text conditioning." } ] }, @@ -1319,4 +1320,4 @@ } }, "version": 0.4 -} +} \ No newline at end of file diff --git a/blueprints/Pose to Video (LTX 2.0).json b/blueprints/Pose to Video (LTX 2.0).json index 580900bc0..1ce49351a 100644 --- a/blueprints/Pose to Video (LTX 2.0).json +++ b/blueprints/Pose to Video (LTX 2.0).json @@ -3870,7 +3870,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Video generation and editing/Pose to video" + "category": "Video generation and editing/Pose to video", + "description": "Generates video from pose reference frames using LTX-2, with optional synchronized audio." } ] }, diff --git a/blueprints/Prompt Enhance.json b/blueprints/Prompt Enhance.json index 5e57548ff..e260b1203 100644 --- a/blueprints/Prompt Enhance.json +++ b/blueprints/Prompt Enhance.json @@ -270,9 +270,10 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Text generation/Prompt enhance" + "category": "Text generation/Prompt enhance", + "description": "Expands short text prompts into detailed descriptions using a text generation model for better generation quality." } ] }, "extra": {} -} +} \ No newline at end of file diff --git a/blueprints/Sharpen.json b/blueprints/Sharpen.json index f332400fd..3c4099c6b 100644 --- a/blueprints/Sharpen.json +++ b/blueprints/Sharpen.json @@ -302,8 +302,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Sharpen" + "category": "Image Tools/Sharpen", + "description": "Sharpens image details using a GPU fragment shader for enhanced clarity." } ] } -} +} \ No newline at end of file diff --git a/blueprints/Text to Audio (ACE-Step 1.5).json b/blueprints/Text to Audio (ACE-Step 1.5).json index 206cf16be..5b8b8626f 100644 --- a/blueprints/Text to Audio (ACE-Step 1.5).json +++ b/blueprints/Text to Audio (ACE-Step 1.5).json @@ -222,7 +222,7 @@ }, "revision": 0, "config": {}, - "name": "local-Text to Audio (ACE-Step 1.5)", + "name": "Text to Audio (ACE-Step 1.5)", "inputNode": { "id": -10, "bounding": [ @@ -1502,7 +1502,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Audio/Music generation" + "category": "Audio/Music generation", + "description": "Generates audio/music from text prompts using ACE-Step 1.5, a diffusion-based audio generation model." } ] }, @@ -1518,4 +1519,4 @@ } }, "version": 0.4 -} +} \ No newline at end of file diff --git a/blueprints/Text to Image (Flux.1 Dev).json b/blueprints/Text to Image (Flux.1 Dev).json index 04c3cb95a..45f68f508 100644 --- a/blueprints/Text to Image (Flux.1 Dev).json +++ b/blueprints/Text to Image (Flux.1 Dev).json @@ -1029,7 +1029,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Text to image" + "category": "Image generation and editing/Text to image", + "description": "Generates images from text prompts using Flux.1 [dev], Black Forest Labs' 12B diffusion model." } ] }, @@ -1043,4 +1044,4 @@ }, "ue_links": [] } -} \ No newline at end of file +} diff --git a/blueprints/Text to Image (Flux.1 Krea Dev).json b/blueprints/Text to Image (Flux.1 Krea Dev).json index fe4db1cfc..30a78dca1 100644 --- a/blueprints/Text to Image (Flux.1 Krea Dev).json +++ b/blueprints/Text to Image (Flux.1 Krea Dev).json @@ -1023,7 +1023,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Text to image" + "category": "Image generation and editing/Text to image", + "description": "Generates images from text prompts using Flux.1 Krea Dev, a Black Forest Labs × Krea collaboration variant." } ] }, @@ -1037,4 +1038,4 @@ }, "ue_links": [] } -} \ No newline at end of file +} diff --git a/blueprints/Text to Image (NetaYume Lumina).json b/blueprints/Text to Image (NetaYume Lumina).json index 394ad1608..9e11b7a86 100644 --- a/blueprints/Text to Image (NetaYume Lumina).json +++ b/blueprints/Text to Image (NetaYume Lumina).json @@ -1104,7 +1104,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Text to image" + "category": "Image generation and editing/Text to image", + "description": "Generates images from text prompts using NetaYume Lumina, fine-tuned from Neta Lumina for anime-style and illustration generation." }, { "id": "a07fdf06-1bda-4dac-bdbd-63ee8ebca1c9", @@ -1458,11 +1459,12 @@ ], "extra": { "workflowRendererVersion": "LG" - } + }, + "description": "Encodes a negative text prompt via CLIP for classifier-free guidance in anime-style generation (NetaYume Lumina)." } ] }, "extra": { "ue_links": [] } -} \ No newline at end of file +} diff --git a/blueprints/Text to Image (Qwen-Image 2512).json b/blueprints/Text to Image (Qwen-Image 2512).json index f52ea2ef2..09612be8b 100644 --- a/blueprints/Text to Image (Qwen-Image 2512).json +++ b/blueprints/Text to Image (Qwen-Image 2512).json @@ -1941,7 +1941,8 @@ "extra": { "workflowRendererVersion": "Vue-corrected" }, - "category": "Image generation and editing/Text to image" + "category": "Image generation and editing/Text to image", + "description": "Generates images from text prompts using Qwen-Image-2512, with enhanced human realism and finer natural detail over the base version." } ] }, diff --git a/blueprints/Text to Image (Qwen-Image).json b/blueprints/Text to Image (Qwen-Image).json index 70b4b44b3..e78d5a962 100644 --- a/blueprints/Text to Image (Qwen-Image).json +++ b/blueprints/Text to Image (Qwen-Image).json @@ -1873,7 +1873,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Text to image" + "category": "Image generation and editing/Text to image", + "description": "Generates images from text prompts using Qwen-Image, Alibaba's 20B MMDiT model with excellent multilingual text rendering." } ] }, diff --git a/blueprints/Text to Image (Z-Image-Turbo).json b/blueprints/Text to Image (Z-Image-Turbo).json index 6aa80e327..6975151ea 100644 --- a/blueprints/Text to Image (Z-Image-Turbo).json +++ b/blueprints/Text to Image (Z-Image-Turbo).json @@ -149,7 +149,7 @@ }, "revision": 0, "config": {}, - "name": "local-Text to Image (Z-Image-Turbo)", + "name": "Text to Image (Z-Image-Turbo)", "inputNode": { "id": -10, "bounding": [ @@ -1054,7 +1054,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Text to image" + "category": "Image generation and editing/Text to image", + "description": "Generates images from text prompts using Z-Image-Turbo, Alibaba's distilled 6B DiT model." } ] }, @@ -1075,4 +1076,4 @@ } }, "version": 0.4 -} +} \ No newline at end of file diff --git a/blueprints/Text to Video (LTX-2.3).json b/blueprints/Text to Video (LTX-2.3).json index ff9bc6ccf..f44a216dd 100644 --- a/blueprints/Text to Video (LTX-2.3).json +++ b/blueprints/Text to Video (LTX-2.3).json @@ -4286,7 +4286,8 @@ "extra": { "workflowRendererVersion": "Vue-corrected" }, - "category": "Video generation and editing/Text to video" + "category": "Video generation and editing/Text to video", + "description": "Generates video from text prompts using LTX-2.3, Lightricks' video diffusion model." } ] }, diff --git a/blueprints/Text to Video (Wan 2.2).json b/blueprints/Text to Video (Wan 2.2).json index 0ce485b67..a264a490d 100644 --- a/blueprints/Text to Video (Wan 2.2).json +++ b/blueprints/Text to Video (Wan 2.2).json @@ -1572,7 +1572,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Video generation and editing/Text to video" + "category": "Video generation and editing/Text to video", + "description": "Generates video from text prompts using Wan2.2, Alibaba's diffusion video model." } ] }, @@ -1586,4 +1587,4 @@ "VHS_KeepIntermediate": true }, "version": 0.4 -} +} \ No newline at end of file diff --git a/blueprints/Unsharp Mask.json b/blueprints/Unsharp Mask.json index 137acaa43..79a4c954f 100644 --- a/blueprints/Unsharp Mask.json +++ b/blueprints/Unsharp Mask.json @@ -434,8 +434,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Sharpen" + "category": "Image Tools/Sharpen", + "description": "Enhances edge contrast via unsharp masking for a sharper image appearance." } ] } -} +} \ No newline at end of file diff --git a/blueprints/Video Captioning (Gemini).json b/blueprints/Video Captioning (Gemini).json index ea6dc8bee..7642b23c1 100644 --- a/blueprints/Video Captioning (Gemini).json +++ b/blueprints/Video Captioning (Gemini).json @@ -307,7 +307,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Text generation/Video Captioning" + "category": "Text generation/Video Captioning", + "description": "Generates descriptive captions for video input using Google's Gemini multimodal LLM." } ] } diff --git a/blueprints/Video Inpaint(Wan2.1 VACE).json b/blueprints/Video Inpaint(Wan2.1 VACE).json index f404e6773..a658be5f8 100644 --- a/blueprints/Video Inpaint(Wan2.1 VACE).json +++ b/blueprints/Video Inpaint(Wan2.1 VACE).json @@ -165,7 +165,7 @@ }, "revision": 0, "config": {}, - "name": "local-Video Inpaint(Wan2.1 VACE)", + "name": "Video Inpaint (Wan 2.1 VACE)", "inputNode": { "id": -10, "bounding": [ @@ -2368,7 +2368,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Video generation and editing/Inpaint video" + "category": "Video generation and editing/Inpaint video", + "description": "Inpaints masked regions in video frames using Wan 2.1 VACE." } ] }, diff --git a/blueprints/Video Stitch.json b/blueprints/Video Stitch.json index 020896d78..6eb0f0bbf 100644 --- a/blueprints/Video Stitch.json +++ b/blueprints/Video Stitch.json @@ -584,8 +584,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Video Tools/Stitch videos" + "category": "Video Tools/Stitch videos", + "description": "Stitches multiple video clips into a single sequential video file." } ] } -} +} \ No newline at end of file diff --git a/blueprints/Video Upscale(GAN x4).json b/blueprints/Video Upscale(GAN x4).json index b61dc88d7..73476e36b 100644 --- a/blueprints/Video Upscale(GAN x4).json +++ b/blueprints/Video Upscale(GAN x4).json @@ -412,9 +412,10 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Video generation and editing/Enhance video" + "category": "Video generation and editing/Enhance video", + "description": "Upscales video to 4× resolution using a GAN-based upscaling model." } ] }, "extra": {} -} +} \ No newline at end of file From 7bbf1e8169fa3080841b83914fa9901793b66b71 Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Sat, 9 May 2026 07:38:17 +0300 Subject: [PATCH 028/145] [Partner Nodes] Tripo3D 3.1 model (#13788) * feat(api-nodes): add Tripo3D 3.1 model Signed-off-by: bigcat88 * fix: price badges algo Signed-off-by: bigcat88 * [Partner Nodes] deprecate "quad" param for the TripoMultiviewToModel node Signed-off-by: bigcat88 --------- Signed-off-by: bigcat88 --- comfy_api_nodes/apis/tripo.py | 30 ++++-------- comfy_api_nodes/nodes_tripo.py | 84 +++++++++++----------------------- 2 files changed, 36 insertions(+), 78 deletions(-) diff --git a/comfy_api_nodes/apis/tripo.py b/comfy_api_nodes/apis/tripo.py index ffaaa7dc1..bce6b0e89 100644 --- a/comfy_api_nodes/apis/tripo.py +++ b/comfy_api_nodes/apis/tripo.py @@ -1,10 +1,11 @@ -from __future__ import annotations from enum import Enum -from typing import Optional, List, Dict, Any, Union +from typing import Optional, Any from pydantic import BaseModel, Field, RootModel + class TripoModelVersion(str, Enum): + v3_1_20260211 = 'v3.1-20260211' v3_0_20250812 = 'v3.0-20250812' v2_5_20250123 = 'v2.5-20250123' v2_0_20240919 = 'v2.0-20240919' @@ -142,7 +143,7 @@ class TripoFileEmptyReference(BaseModel): pass class TripoFileReference(RootModel): - root: Union[TripoFileTokenReference, TripoUrlReference, TripoObjectReference, TripoFileEmptyReference] + root: TripoFileTokenReference | TripoUrlReference | TripoObjectReference | TripoFileEmptyReference class TripoGetStsTokenRequest(BaseModel): format: str = Field(..., description='The format of the image') @@ -183,7 +184,7 @@ class TripoImageToModelRequest(BaseModel): class TripoMultiviewToModelRequest(BaseModel): type: TripoTaskType = TripoTaskType.MULTIVIEW_TO_MODEL - files: List[TripoFileReference] = Field(..., description='The file references to convert to a model') + files: list[TripoFileReference] = Field(..., description='The file references to convert to a model') model_version: Optional[TripoModelVersion] = Field(None, description='The model version to use for generation') orthographic_projection: Optional[bool] = Field(False, description='Whether to use orthographic projection') face_limit: Optional[int] = Field(None, description='The number of faces to limit the generation to') @@ -251,27 +252,13 @@ class TripoConvertModelRequest(BaseModel): with_animation: Optional[bool] = Field(None, description='Whether to include animations') pack_uv: Optional[bool] = Field(None, description='Whether to pack the UVs') bake: Optional[bool] = Field(None, description='Whether to bake the model') - part_names: Optional[List[str]] = Field(None, description='The names of the parts to include') + part_names: Optional[list[str]] = Field(None, description='The names of the parts to include') fbx_preset: Optional[TripoFbxPreset] = Field(None, description='The preset for the FBX export') export_vertex_colors: Optional[bool] = Field(None, description='Whether to export the vertex colors') export_orientation: Optional[TripoOrientation] = Field(None, description='The orientation for the export') animate_in_place: Optional[bool] = Field(None, description='Whether to animate in place') -class TripoTaskRequest(RootModel): - root: Union[ - TripoTextToModelRequest, - TripoImageToModelRequest, - TripoMultiviewToModelRequest, - TripoTextureModelRequest, - TripoRefineModelRequest, - TripoAnimatePrerigcheckRequest, - TripoAnimateRigRequest, - TripoAnimateRetargetRequest, - TripoStylizeModelRequest, - TripoConvertModelRequest - ] - class TripoTaskOutput(BaseModel): model: Optional[str] = Field(None, description='URL to the model') base_model: Optional[str] = Field(None, description='URL to the base model') @@ -283,12 +270,13 @@ class TripoTask(BaseModel): task_id: str = Field(..., description='The task ID') type: Optional[str] = Field(None, description='The type of task') status: Optional[TripoTaskStatus] = Field(None, description='The status of the task') - input: Optional[Dict[str, Any]] = Field(None, description='The input parameters for the task') + input: Optional[dict[str, Any]] = Field(None, description='The input parameters for the task') output: Optional[TripoTaskOutput] = Field(None, description='The output of the task') progress: Optional[int] = Field(None, description='The progress of the task', ge=0, le=100) create_time: Optional[int] = Field(None, description='The creation time of the task') running_left_time: Optional[int] = Field(None, description='The estimated time left for the task') queue_position: Optional[int] = Field(None, description='The position in the queue') + consumed_credit: int | None = Field(None) class TripoTaskResponse(BaseModel): code: int = Field(0, description='The response code') @@ -296,7 +284,7 @@ class TripoTaskResponse(BaseModel): class TripoGeneralResponse(BaseModel): code: int = Field(0, description='The response code') - data: Dict[str, str] = Field(..., description='The task ID data') + data: dict[str, str] = Field(..., description='The task ID data') class TripoBalanceData(BaseModel): balance: float = Field(..., description='The account balance') diff --git a/comfy_api_nodes/nodes_tripo.py b/comfy_api_nodes/nodes_tripo.py index 9f4298dce..d6501dee4 100644 --- a/comfy_api_nodes/nodes_tripo.py +++ b/comfy_api_nodes/nodes_tripo.py @@ -60,6 +60,7 @@ async def poll_until_finished( ], status_extractor=lambda x: x.data.status, progress_extractor=lambda x: x.data.progress, + price_extractor=lambda x: x.data.consumed_credit * 0.01 if x.data.consumed_credit else None, estimated_duration=average_duration, ) if response_poll.data.status == TripoTaskStatus.SUCCESS: @@ -113,7 +114,6 @@ class TripoTextToModelNode(IO.ComfyNode): depends_on=IO.PriceBadgeDepends( widgets=[ "model_version", - "style", "texture", "pbr", "quad", @@ -124,20 +124,17 @@ class TripoTextToModelNode(IO.ComfyNode): expr=""" ( $isV14 := $contains(widgets.model_version,"v1.4"); - $style := widgets.style; - $hasStyle := ($style != "" and $style != "none"); + $isV3OrLater := $contains(widgets.model_version,"v3."); $withTexture := widgets.texture or widgets.pbr; $isHdTexture := (widgets.texture_quality = "detailed"); $isDetailedGeometry := (widgets.geometry_quality = "detailed"); - $baseCredits := - $isV14 ? 20 : ($withTexture ? 20 : 10); - $credits := - $baseCredits - + ($hasStyle ? 5 : 0) + $credits := $isV14 ? 20 : ( + ($withTexture ? 20 : 10) + (widgets.quad ? 5 : 0) + ($isHdTexture ? 10 : 0) - + ($isDetailedGeometry ? 20 : 0); - {"type":"usd","usd": $round($credits * 0.01, 2)} + + (($isDetailedGeometry and $isV3OrLater) ? 20 : 0) + ); + {"type":"usd","usd": $round($credits * 0.01, 2), "format": {"approximate": true}} ) """, ), @@ -239,7 +236,6 @@ class TripoImageToModelNode(IO.ComfyNode): depends_on=IO.PriceBadgeDepends( widgets=[ "model_version", - "style", "texture", "pbr", "quad", @@ -250,20 +246,17 @@ class TripoImageToModelNode(IO.ComfyNode): expr=""" ( $isV14 := $contains(widgets.model_version,"v1.4"); - $style := widgets.style; - $hasStyle := ($style != "" and $style != "none"); + $isV3OrLater := $contains(widgets.model_version,"v3."); $withTexture := widgets.texture or widgets.pbr; $isHdTexture := (widgets.texture_quality = "detailed"); $isDetailedGeometry := (widgets.geometry_quality = "detailed"); - $baseCredits := - $isV14 ? 30 : ($withTexture ? 30 : 20); - $credits := - $baseCredits - + ($hasStyle ? 5 : 0) + $credits := $isV14 ? 30 : ( + ($withTexture ? 30 : 20) + (widgets.quad ? 5 : 0) + ($isHdTexture ? 10 : 0) - + ($isDetailedGeometry ? 20 : 0); - {"type":"usd","usd": $round($credits * 0.01, 2)} + + (($isDetailedGeometry and $isV3OrLater) ? 20 : 0) + ); + {"type":"usd","usd": $round($credits * 0.01, 2), "format": {"approximate": true}} ) """, ), @@ -358,7 +351,7 @@ class TripoMultiviewToModelNode(IO.ComfyNode): "texture_alignment", default="original_image", options=["original_image", "geometry"], optional=True, advanced=True ), IO.Int.Input("face_limit", default=-1, min=-1, max=500000, optional=True, advanced=True), - IO.Boolean.Input("quad", default=False, optional=True, advanced=True), + IO.Boolean.Input("quad", default=False, optional=True, advanced=True, tooltip="This parameter is deprecated and does nothing."), IO.Combo.Input("geometry_quality", default="standard", options=["standard", "detailed"], optional=True, advanced=True), ], outputs=[ @@ -379,7 +372,6 @@ class TripoMultiviewToModelNode(IO.ComfyNode): "model_version", "texture", "pbr", - "quad", "texture_quality", "geometry_quality", ], @@ -387,17 +379,16 @@ class TripoMultiviewToModelNode(IO.ComfyNode): expr=""" ( $isV14 := $contains(widgets.model_version,"v1.4"); + $isV3OrLater := $contains(widgets.model_version,"v3."); $withTexture := widgets.texture or widgets.pbr; $isHdTexture := (widgets.texture_quality = "detailed"); $isDetailedGeometry := (widgets.geometry_quality = "detailed"); - $baseCredits := - $isV14 ? 30 : ($withTexture ? 30 : 20); - $credits := - $baseCredits - + (widgets.quad ? 5 : 0) + $credits := $isV14 ? 30 : ( + ($withTexture ? 30 : 20) + ($isHdTexture ? 10 : 0) - + ($isDetailedGeometry ? 20 : 0); - {"type":"usd","usd": $round($credits * 0.01, 2)} + + (($isDetailedGeometry and $isV3OrLater) ? 20 : 0) + ); + {"type":"usd","usd": $round($credits * 0.01, 2), "format": {"approximate": true}} ) """, ), @@ -457,7 +448,7 @@ class TripoMultiviewToModelNode(IO.ComfyNode): geometry_quality=geometry_quality, texture_alignment=texture_alignment, face_limit=face_limit if face_limit != -1 else None, - quad=quad, + quad=None, ), ) return await poll_until_finished(cls, response, average_duration=80) @@ -498,7 +489,7 @@ class TripoTextureNode(IO.ComfyNode): expr=""" ( $tq := widgets.texture_quality; - {"type":"usd","usd": ($contains($tq,"detailed") ? 0.2 : 0.1)} + {"type":"usd","usd": ($contains($tq,"detailed") ? 0.2 : 0.1), "format": {"approximate": true}} ) """, ), @@ -555,7 +546,7 @@ class TripoRefineNode(IO.ComfyNode): is_api_node=True, is_output_node=True, price_badge=IO.PriceBadge( - expr="""{"type":"usd","usd":0.3}""", + expr="""{"type":"usd","usd":0.3, "format": {"approximate": true}}""", ), ) @@ -592,7 +583,7 @@ class TripoRigNode(IO.ComfyNode): is_api_node=True, is_output_node=True, price_badge=IO.PriceBadge( - expr="""{"type":"usd","usd":0.25}""", + expr="""{"type":"usd","usd":0.25, "format": {"approximate": true}}""", ), ) @@ -652,7 +643,7 @@ class TripoRetargetNode(IO.ComfyNode): is_api_node=True, is_output_node=True, price_badge=IO.PriceBadge( - expr="""{"type":"usd","usd":0.1}""", + expr="""{"type":"usd","usd":0.1, "format": {"approximate": true}}""", ), ) @@ -761,19 +752,10 @@ class TripoConversionNode(IO.ComfyNode): "face_limit", "texture_size", "texture_format", - "force_symmetry", "flatten_bottom", "flatten_bottom_threshold", "pivot_to_center_bottom", "scale_factor", - "with_animation", - "pack_uv", - "bake", - "part_names", - "fbx_preset", - "export_vertex_colors", - "export_orientation", - "animate_in_place", ], ), expr=""" @@ -783,28 +765,16 @@ class TripoConversionNode(IO.ComfyNode): $flatThresh := (widgets.flatten_bottom_threshold != null) ? widgets.flatten_bottom_threshold : 0; $scale := (widgets.scale_factor != null) ? widgets.scale_factor : 1; $texFmt := (widgets.texture_format != "" ? widgets.texture_format : "jpeg"); - $part := widgets.part_names; - $fbx := (widgets.fbx_preset != "" ? widgets.fbx_preset : "blender"); - $orient := (widgets.export_orientation != "" ? widgets.export_orientation : "default"); $advanced := widgets.quad or - widgets.force_symmetry or widgets.flatten_bottom or widgets.pivot_to_center_bottom or - widgets.with_animation or - widgets.pack_uv or - widgets.bake or - widgets.export_vertex_colors or - widgets.animate_in_place or ($face != -1) or ($texSize != 4096) or ($flatThresh != 0) or ($scale != 1) or - ($texFmt != "jpeg") or - ($part != "") or - ($fbx != "blender") or - ($orient != "default"); - {"type":"usd","usd": ($advanced ? 0.1 : 0.05)} + ($texFmt != "jpeg"); + {"type":"usd","usd": ($advanced ? 0.1 : 0.05), "format": {"approximate": true}} ) """, ), From a4b7e3beedda4180cd6a2b319c9805990357ee96 Mon Sep 17 00:00:00 2001 From: Comfy Org PR Bot Date: Sat, 9 May 2026 23:53:10 +0900 Subject: [PATCH 029/145] Bump comfyui-frontend-package to 1.43.18 (#13809) Co-authored-by: github-actions[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5c7ff76be..6fd808772 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -comfyui-frontend-package==1.43.17 +comfyui-frontend-package==1.43.18 comfyui-workflow-templates==0.9.72 comfyui-embedded-docs==0.4.4 torch From 3200f28e3a8663f18b9a9568472ad912ea5c6396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Sepp=C3=A4nen?= <40791699+kijai@users.noreply.github.com> Date: Sun, 10 May 2026 00:02:56 +0300 Subject: [PATCH 030/145] Support Wan-Dancer (#13813) * initial WanDancer support * nodes_wandancer: Add list form of chunker. Create an alternate list form of the node so the chunk gens can be trivially looped by the comfy executor. * Closer match to original soxr resampling * Remove librosa node * Cleanup --------- Co-authored-by: Rattus --- comfy/ldm/wan/model.py | 15 +- comfy/ldm/wan/model_wandancer.py | 251 ++++++++ comfy/model_base.py | 25 + comfy/model_detection.py | 2 + comfy/supported_models.py | 32 + comfy_extras/nodes_wandancer.py | 1002 ++++++++++++++++++++++++++++++ nodes.py | 1 + 7 files changed, 1322 insertions(+), 6 deletions(-) create mode 100644 comfy/ldm/wan/model_wandancer.py create mode 100644 comfy_extras/nodes_wandancer.py diff --git a/comfy/ldm/wan/model.py b/comfy/ldm/wan/model.py index b2287dba9..70dfe7b16 100644 --- a/comfy/ldm/wan/model.py +++ b/comfy/ldm/wan/model.py @@ -1135,7 +1135,7 @@ class AudioInjector_WAN(nn.Module): self.injector_adain_output_layers = nn.ModuleList( [operations.Linear(dim, dim, dtype=dtype, device=device) for _ in range(audio_injector_id)]) - def forward(self, x, block_id, audio_emb, audio_emb_global, seq_len): + def forward(self, x, block_id, audio_emb, audio_emb_global, seq_len, scale=1.0): audio_attn_id = self.injected_block_id.get(block_id, None) if audio_attn_id is None: return x @@ -1148,12 +1148,15 @@ class AudioInjector_WAN(nn.Module): attn_hidden_states = adain_hidden_states else: attn_hidden_states = self.injector_pre_norm_feat[audio_attn_id](input_hidden_states) - audio_emb = rearrange(audio_emb, "b t n c -> (b t) n c", t=num_frames) - attn_audio_emb = audio_emb + + if audio_emb.dim() == 3: # WanDancer case + attn_audio_emb = rearrange(audio_emb, "b t c -> (b t) 1 c", t=num_frames) + else: # S2V case + attn_audio_emb = rearrange(audio_emb, "b t n c -> (b t) n c", t=num_frames) + residual_out = self.injector[audio_attn_id](x=attn_hidden_states, context=attn_audio_emb) - residual_out = rearrange( - residual_out, "(b t) n c -> b (t n) c", t=num_frames) - x[:, :seq_len] = x[:, :seq_len] + residual_out + residual_out = rearrange(residual_out, "(b t) n c -> b (t n) c", t=num_frames) + x[:, :seq_len] = x[:, :seq_len] + residual_out * scale return x diff --git a/comfy/ldm/wan/model_wandancer.py b/comfy/ldm/wan/model_wandancer.py new file mode 100644 index 000000000..3caef6dc5 --- /dev/null +++ b/comfy/ldm/wan/model_wandancer.py @@ -0,0 +1,251 @@ +import torch +import torch.nn as nn +import comfy +from comfy.ldm.modules.attention import optimized_attention +from comfy.ldm.flux.math import apply_rope1 +from comfy.ldm.flux.layers import EmbedND + +from .model import AudioInjector_WAN, WanModel, MLPProj, Head, sinusoidal_embedding_1d + + +class MusicSelfAttention(nn.Module): + def __init__(self, dim, num_heads, device=None, dtype=None, operations=None): + assert dim % num_heads == 0 + super().__init__() + self.embed_dim = dim + self.num_heads = num_heads + self.head_dim = dim // num_heads + + self.q_proj = operations.Linear(dim, dim, device=device, dtype=dtype) + self.k_proj = operations.Linear(dim, dim, device=device, dtype=dtype) + self.v_proj = operations.Linear(dim, dim, device=device, dtype=dtype) + self.out_proj = operations.Linear(dim, dim, device=device, dtype=dtype) + + def forward(self, x, freqs): + b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim + + q = self.q_proj(x).view(b, s, n, d) + q = apply_rope1(q, freqs) + + k = self.k_proj(x).view(b, s, n, d) + k = apply_rope1(k, freqs) + + x = optimized_attention( + q.view(b, s, n * d), + k.view(b, s, n * d), + self.v_proj(x).view(b, s, n * d), + heads=self.num_heads, + ) + + return self.out_proj(x) + + +class MusicEncoderLayer(nn.Module): + def __init__(self, dim: int, num_heads: int, ffn_dim: int, device=None, dtype=None, operations=None): + super().__init__() + self.self_attn = MusicSelfAttention(dim, num_heads, device=device, dtype=dtype, operations=operations) + + self.linear1 = operations.Linear(dim, ffn_dim, device=device, dtype=dtype) + self.linear2 = operations.Linear(ffn_dim, dim, device=device, dtype=dtype) + + self.norm1 = operations.LayerNorm(dim, device=device, dtype=dtype) + self.norm2 = operations.LayerNorm(dim, device=device, dtype=dtype) + + def forward(self, x: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor: + x = x + self.self_attn(self.norm1(x), freqs=freqs) + x = x + self.linear2(torch.nn.functional.gelu(self.linear1(self.norm2(x)))) # ffn + return x + + +class WanDancerModel(WanModel): + def __init__(self, + model_type='wandancer', + patch_size=(1, 2, 2), + text_len=512, + in_dim=16, + dim=5120, + ffn_dim=8192, + freq_dim=256, + text_dim=4096, + out_dim=16, + num_heads=16, + num_layers=40, + window_size=(-1, -1), + qk_norm=True, + cross_attn_norm=True, + eps=1e-6, + in_dim_ref_conv=None, + image_model=None, + device=None, dtype=None, operations=None, + audio_inject_layers=[0, 4, 8, 12, 16, 20, 24, 27], + music_dim = 256, + music_heads = 4, + music_feature_dim = 35, + music_latent_dim = 256 + ): + + super().__init__(model_type='i2v', patch_size=patch_size, text_len=text_len, in_dim=in_dim, dim=dim, ffn_dim=ffn_dim, freq_dim=freq_dim, text_dim=text_dim, out_dim=out_dim, + num_heads=num_heads, num_layers=num_layers, window_size=window_size, qk_norm=qk_norm, cross_attn_norm=cross_attn_norm, eps=eps, image_model=image_model, in_dim_ref_conv=in_dim_ref_conv, + device=device, dtype=dtype, operations=operations) + + self.dtype = dtype + operation_settings = {"operations": operations, "device": device, "dtype": dtype} + + self.patch_embedding_global = operations.Conv3d(in_dim, dim, kernel_size=patch_size, stride=patch_size, device=operation_settings.get("device"), dtype=torch.float32) + self.img_emb_refimage = MLPProj(1280, dim, operation_settings=operation_settings) + self.head_global = Head(dim, out_dim, patch_size, eps, operation_settings=operation_settings) + + self.music_injector = AudioInjector_WAN( + dim=self.dim, + num_heads=self.num_heads, + inject_layer=audio_inject_layers, + root_net=self, + enable_adain=False, + dtype=dtype, device=device, operations=operations + ) + + self.music_projection = operations.Linear(music_feature_dim, music_latent_dim, device=device, dtype=dtype) + self.music_encoder = nn.ModuleList([MusicEncoderLayer(dim=music_dim, num_heads=music_heads, ffn_dim=1024, device=device, dtype=dtype, operations=operations) for _ in range(2)]) + music_head_dim = music_dim // music_heads + self.music_rope_embedder = EmbedND(dim=music_head_dim, theta=10000.0, axes_dim=[music_head_dim]) + + def forward_orig(self, x, t, context, clip_fea=None, clip_fea_ref=None, freqs=None, audio_embed=None, fps=30, audio_inject_scale=1.0, transformer_options={}, **kwargs): + # embeddings + if int(fps + 0.5) != 30: + x = self.patch_embedding_global(x.float()).to(x.dtype) + else: + x = self.patch_embedding(x.float()).to(x.dtype) + + grid_sizes = x.shape[2:] + latent_frames = grid_sizes[0] + transformer_options["grid_sizes"] = grid_sizes + x = x.flatten(2).transpose(1, 2) + seq_len = x.size(1) + + # time embeddings + e = self.time_embedding(sinusoidal_embedding_1d(self.freq_dim, t.flatten()).to(dtype=x[0].dtype)) + e = e.reshape(t.shape[0], -1, e.shape[-1]) + e0 = self.time_projection(e).unflatten(2, (6, self.dim)) + + full_ref = None + if self.ref_conv is not None: # model has the weight, but this wasn't used in the original pipeline + full_ref = kwargs.get("reference_latent", None) + if full_ref is not None: + full_ref = self.ref_conv(full_ref).flatten(2).transpose(1, 2) + x = torch.concat((full_ref, x), dim=1) + + # context + context = self.text_embedding(context) + + audio_emb = None + if audio_embed is not None: # encode music feature,[1, frame_num, 35] -> [1, F*8, dim] + music_feature = self.music_projection(audio_embed) + + music_seq_len = music_feature.shape[1] + music_ids = torch.arange(music_seq_len, device=music_feature.device, dtype=music_feature.dtype).reshape(1, -1, 1) # create 1D position IDs + music_freqs = self.music_rope_embedder(music_ids).movedim(1, 2) + + # apply encoder layers + for layer in self.music_encoder: + music_feature = layer(music_feature, music_freqs) + + # interpolate + audio_emb = torch.nn.functional.interpolate(music_feature.unsqueeze(1), size=(latent_frames * 8, self.dim), mode='bilinear').squeeze(1) + + context_img_len = 0 + if self.img_emb is not None and clip_fea is not None: + context_clip = self.img_emb(clip_fea) # bs x 257 x dim + context = torch.cat([context_clip, context], dim=1) + context_img_len += clip_fea.shape[-2] + if self.img_emb_refimage is not None and clip_fea_ref is not None: + context_clip_ref = self.img_emb_refimage(clip_fea_ref) + context = torch.cat([context_clip_ref, context], dim=1) + context_img_len += clip_fea_ref.shape[-2] + + patches_replace = transformer_options.get("patches_replace", {}) + blocks_replace = patches_replace.get("dit", {}) + transformer_options["total_blocks"] = len(self.blocks) + transformer_options["block_type"] = "double" + for i, block in enumerate(self.blocks): + transformer_options["block_index"] = i + if ("double_block", i) in blocks_replace: + def block_wrap(args): + out = {} + out["img"] = block(args["img"], context=args["txt"], e=args["vec"], freqs=args["pe"], context_img_len=context_img_len, transformer_options=args["transformer_options"]) + return out + out = blocks_replace[("double_block", i)]({"img": x, "txt": context, "vec": e0, "pe": freqs, "transformer_options": transformer_options}, {"original_block": block_wrap}) + x = out["img"] + else: + x = block(x, e=e0, freqs=freqs, context=context, context_img_len=context_img_len, transformer_options=transformer_options) + if audio_emb is not None: + x = self.music_injector(x, i, audio_emb, audio_emb_global=None, seq_len=seq_len, scale=audio_inject_scale) + + # head + if int(fps + 0.5) != 30: + x = self.head_global(x, e) + else: + x = self.head(x, e) + + if full_ref is not None: + x = x[:, full_ref.shape[1]:] + + # unpatchify + x = self.unpatchify(x, grid_sizes) + return x + + def _forward(self, x, timestep, context, clip_fea=None, time_dim_concat=None, transformer_options={}, clip_fea_ref=None, fps=30, audio_inject_scale=1.0, **kwargs): + bs, c, t, h, w = x.shape + x = comfy.ldm.common_dit.pad_to_patch_size(x, self.patch_size) + + t_len = t + if time_dim_concat is not None: + time_dim_concat = comfy.ldm.common_dit.pad_to_patch_size(time_dim_concat, self.patch_size) + x = torch.cat([x, time_dim_concat], dim=2) + t_len = x.shape[2] + + freqs = self.rope_encode(t_len, h, w, device=x.device, dtype=x.dtype, fps=fps, transformer_options=transformer_options) + return self.forward_orig(x, timestep, context, clip_fea=clip_fea, clip_fea_ref=clip_fea_ref, freqs=freqs, fps=fps, audio_inject_scale=audio_inject_scale, transformer_options=transformer_options, **kwargs)[:, :, :t, :h, :w] + + def rope_encode(self, t, h, w, t_start=0, steps_t=None, steps_h=None, steps_w=None, fps=30, device=None, dtype=None, transformer_options={}): + patch_size = self.patch_size + t_len = ((t + (patch_size[0] // 2)) // patch_size[0]) + h_len = ((h + (patch_size[1] // 2)) // patch_size[1]) + w_len = ((w + (patch_size[2] // 2)) // patch_size[2]) + + if steps_t is None: + steps_t = t_len + if steps_h is None: + steps_h = h_len + if steps_w is None: + steps_w = w_len + + h_start = 0 + w_start = 0 + rope_options = transformer_options.get("rope_options", None) + if rope_options is not None: + t_len = (t_len - 1.0) * rope_options.get("scale_t", 1.0) + 1.0 + h_len = (h_len - 1.0) * rope_options.get("scale_y", 1.0) + 1.0 + w_len = (w_len - 1.0) * rope_options.get("scale_x", 1.0) + 1.0 + + t_start += rope_options.get("shift_t", 0.0) + h_start += rope_options.get("shift_y", 0.0) + w_start += rope_options.get("shift_x", 0.0) + + img_ids = torch.zeros((steps_t, steps_h, steps_w, 3), device=device, dtype=dtype) + + if int(fps + 0.5) != 30: + time_scale = 30.0 / fps # how many time units each frame represents relative to 30fps + positions_new = torch.arange(steps_t, device=device, dtype=dtype) * time_scale + t_start + total_frames_at_30fps = int(time_scale * steps_t + 0.5) + positions_new[-1] = t_start + (total_frames_at_30fps - 1) + + img_ids[:, :, :, 0] = img_ids[:, :, :, 0] + positions_new.reshape(-1, 1, 1) + else: + img_ids[:, :, :, 0] = img_ids[:, :, :, 0] + torch.linspace(t_start, t_start + (t_len - 1), steps=steps_t, device=device, dtype=dtype).reshape(-1, 1, 1) + + img_ids[:, :, :, 1] = img_ids[:, :, :, 1] + torch.linspace(h_start, h_start + (h_len - 1), steps=steps_h, device=device, dtype=dtype).reshape(1, -1, 1) + img_ids[:, :, :, 2] = img_ids[:, :, :, 2] + torch.linspace(w_start, w_start + (w_len - 1), steps=steps_w, device=device, dtype=dtype).reshape(1, 1, -1) + img_ids = img_ids.reshape(1, -1, img_ids.shape[-1]) + + freqs = self.rope_embedder(img_ids).movedim(1, 2) + return freqs diff --git a/comfy/model_base.py b/comfy/model_base.py index 57a1e44d2..dbed239e5 100644 --- a/comfy/model_base.py +++ b/comfy/model_base.py @@ -43,6 +43,7 @@ import comfy.ldm.lumina.model import comfy.ldm.wan.model import comfy.ldm.wan.model_animate import comfy.ldm.wan.ar_model +import comfy.ldm.wan.model_wandancer import comfy.ldm.hunyuan3d.model import comfy.ldm.hidream.model import comfy.ldm.chroma.model @@ -1599,6 +1600,30 @@ class WAN21_SCAIL(WAN21): return out +class WAN22_WanDancer(WAN21): + def __init__(self, model_config, model_type=ModelType.FLOW, image_to_video=True, device=None): + super(WAN21, self).__init__(model_config, model_type, device=device, unet_model=comfy.ldm.wan.model_wandancer.WanDancerModel) + self.image_to_video = image_to_video + + def extra_conds(self, **kwargs): + out = super().extra_conds(**kwargs) + audio_embed = kwargs.get("audio_embed", None) + if audio_embed is not None: + out['audio_embed'] = comfy.conds.CONDRegular(audio_embed) + + clip_vision_output_ref = kwargs.get("clip_vision_output_ref", None) + if clip_vision_output_ref is not None: + out['clip_fea_ref'] = comfy.conds.CONDRegular(clip_vision_output_ref.penultimate_hidden_states) + + fps = kwargs.get("fps", None) + if fps is not None: + out['fps'] = comfy.conds.CONDRegular(torch.FloatTensor([fps])) + + audio_inject_scale = kwargs.get("audio_inject_scale", None) + if audio_inject_scale is not None: + out['audio_inject_scale'] = comfy.conds.CONDRegular(torch.FloatTensor([audio_inject_scale])) + return out + class Hunyuan3Dv2(BaseModel): def __init__(self, model_config, model_type=ModelType.FLOW, device=None): super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.hunyuan3d.model.Hunyuan3Dv2) diff --git a/comfy/model_detection.py b/comfy/model_detection.py index d9b67dcdf..8ae456481 100644 --- a/comfy/model_detection.py +++ b/comfy/model_detection.py @@ -572,6 +572,8 @@ def detect_unet_config(state_dict, key_prefix, metadata=None): dit_config["model_type"] = "animate" elif '{}patch_embedding_pose.weight'.format(key_prefix) in state_dict_keys: dit_config["model_type"] = "scail" + elif '{}patch_embedding_global.weight'.format(key_prefix) in state_dict_keys: + dit_config["model_type"] = "wandancer" else: if '{}img_emb.proj.0.bias'.format(key_prefix) in state_dict_keys: dit_config["model_type"] = "i2v" diff --git a/comfy/supported_models.py b/comfy/supported_models.py index 6a9613602..40417f922 100644 --- a/comfy/supported_models.py +++ b/comfy/supported_models.py @@ -1313,6 +1313,37 @@ class WAN21_SCAIL(WAN21_T2V): out = model_base.WAN21_SCAIL(self, image_to_video=False, device=device) return out +class WAN22_WanDancer(WAN21_T2V): + unet_config = { + "image_model": "wan2.1", + "model_type": "wandancer", + "in_dim": 36, + } + + def __init__(self, unet_config): + super().__init__(unet_config) + self.memory_usage_factor = 1.8 + + def get_model(self, state_dict, prefix="", device=None): + out = model_base.WAN22_WanDancer(self, image_to_video=True, device=device) + return out + + def process_unet_state_dict(self, state_dict): + out_sd = {} + for k in list(state_dict.keys()): + # split music_encoder in_proj into q_proj, k_proj, v_proj + if "music_encoder" in k and "self_attn.in_proj" in k: + suffix = "weight" if k.endswith("weight") else "bias" + tensor = state_dict[k] + d = tensor.shape[0] // 3 + prefix = k.replace(f"in_proj_{suffix}", "") + out_sd[f"{prefix}q_proj.{suffix}"] = tensor[:d] + out_sd[f"{prefix}k_proj.{suffix}"] = tensor[d:2*d] + out_sd[f"{prefix}v_proj.{suffix}"] = tensor[2*d:] + else: + out_sd[k] = state_dict[k] + return out_sd + class Hunyuan3Dv2(supported_models_base.BASE): unet_config = { "image_model": "hunyuan3d2", @@ -1982,6 +2013,7 @@ models = [ WAN22_Animate, WAN21_FlowRVS, WAN21_SCAIL, + WAN22_WanDancer, Hunyuan3Dv2mini, Hunyuan3Dv2, Hunyuan3Dv2_1, diff --git a/comfy_extras/nodes_wandancer.py b/comfy_extras/nodes_wandancer.py new file mode 100644 index 000000000..faaeb9020 --- /dev/null +++ b/comfy_extras/nodes_wandancer.py @@ -0,0 +1,1002 @@ +import math +import nodes +import node_helpers +import torch +import torchaudio +import comfy.model_management +import comfy.utils +import numpy as np +import logging +from typing_extensions import override +from comfy_api.latest import ComfyExtension, io + +import scipy.signal +import scipy.ndimage +import scipy.fft +import scipy.sparse + +# Audio Processing Functions - Derived from librosa (https://github.com/librosa/librosa) +# Copyright (c) 2013--2023, librosa development team. + +def mel_to_hz(mels, htk=False): + """Convert mel to Hz (slaney)""" + mels = np.asanyarray(mels) + if htk: + return 700.0 * (10.0 ** (mels / 2595.0) - 1.0) + f_min = 0.0 + f_sp = 200.0 / 3 + freqs = f_min + f_sp * mels + min_log_hz = 1000.0 + min_log_mel = (min_log_hz - f_min) / f_sp + logstep = np.log(6.4) / 27.0 + if mels.ndim: + log_t = mels >= min_log_mel + freqs[log_t] = min_log_hz * np.exp(logstep * (mels[log_t] - min_log_mel)) + elif mels >= min_log_mel: + freqs = min_log_hz * np.exp(logstep * (mels - min_log_mel)) + return freqs + +def hz_to_mel(frequencies, htk=False): + """Convert Hz to mel (slaney)""" + frequencies = np.asanyarray(frequencies) + if htk: + return 2595.0 * np.log10(1.0 + frequencies / 700.0) + f_min = 0.0 + f_sp = 200.0 / 3 + mels = (frequencies - f_min) / f_sp + min_log_hz = 1000.0 + min_log_mel = (min_log_hz - f_min) / f_sp + logstep = np.log(6.4) / 27.0 + if frequencies.ndim: + log_t = frequencies >= min_log_hz + mels[log_t] = min_log_mel + np.log(frequencies[log_t] / min_log_hz) / logstep + elif frequencies >= min_log_hz: + mels = min_log_mel + np.log(frequencies / min_log_hz) / logstep + return mels + +def compute_cqt(y, sr=22050, hop_length=512, fmin=None, n_bins=84, bins_per_octave=12, tuning=0.0): + """Compute Constant-Q Transform (CQT) spectrogram.""" + + def _relative_bandwidth(freqs): + bpo = np.empty_like(freqs) + logf = np.log2(freqs) + bpo[0] = 1.0 / (logf[1] - logf[0]) + bpo[-1] = 1.0 / (logf[-1] - logf[-2]) + bpo[1:-1] = 2.0 / (logf[2:] - logf[:-2]) + return (2.0 ** (2.0 / bpo) - 1.0) / (2.0 ** (2.0 / bpo) + 1.0) + + def _wavelet_lengths(freqs, sr, filter_scale, alpha): + Q = float(filter_scale) / alpha + return Q * sr / freqs # shape (n_bins,) floats + + def _build_wavelet(freqs_oct, sr, filter_scale, alpha_oct): + lengths = _wavelet_lengths(freqs_oct, sr, filter_scale, alpha_oct) + filters = [] + for ilen, freq in zip(lengths, freqs_oct): + t = np.arange(int(-ilen // 2), int(ilen // 2), dtype=float) + sig = (np.cos(t * 2 * np.pi * freq / sr) + + 1j * np.sin(t * 2 * np.pi * freq / sr)).astype(np.complex64) + sig *= scipy.signal.get_window('hann', len(sig), fftbins=True) + l1 = np.sum(np.abs(sig)) + tiny = np.finfo(np.float32).tiny + sig /= max(l1, tiny) + filters.append(sig) + max_len = max(lengths) + n_fft = int(2.0 ** np.ceil(np.log2(max_len))) + out = np.zeros((len(filters), n_fft), dtype=np.complex64) + for k, f in enumerate(filters): + lpad = int((n_fft - len(f)) // 2) + out[k, lpad: lpad + len(f)] = f + return out, lengths + + def _resample_half(y): + ratio = 0.5 + n_samples = int(np.ceil(len(y) * ratio)) + # Kaiser-windowed FIR matches librosa/soxr more closely than scipy's default Hamming filter + L = 2 + h = scipy.signal.firwin(160 * L + 1, 0.96 / L, window=('kaiser', 6.5)) + y_hat = scipy.signal.resample_poly(y.astype(np.float32), 1, 2, window=h) + if len(y_hat) > n_samples: + y_hat = y_hat[:n_samples] + elif len(y_hat) < n_samples: + y_hat = np.pad(y_hat, (0, n_samples - len(y_hat))) + y_hat /= np.sqrt(ratio) + return y_hat.astype(np.float32) + + def _sparsify_rows(x, quantile=0.01): + mags = np.abs(x) + norms = np.sum(mags, axis=1, keepdims=True) + norms = np.where(norms == 0, 1.0, norms) + mag_sort = np.sort(mags, axis=1) + cumulative_mag = np.cumsum(mag_sort / norms, axis=1) + threshold_idx = np.argmin(cumulative_mag < quantile, axis=1) + x_sparse = scipy.sparse.lil_matrix(x.shape, dtype=x.dtype) + for i, j in enumerate(threshold_idx): + idx = np.where(mags[i] >= mag_sort[i, j]) + x_sparse[i, idx] = x[i, idx] + return x_sparse.tocsr() + + if fmin is None: + fmin = 32.70319566257483 # C1 note frequency + + fmin = fmin * (2.0 ** (tuning / bins_per_octave)) + freqs = fmin * (2.0 ** (np.arange(n_bins) / bins_per_octave)) + + alpha = _relative_bandwidth(freqs) + lengths = _wavelet_lengths(freqs, float(sr), 1, alpha) + + n_octaves = int(np.ceil(float(n_bins) / bins_per_octave)) + n_filters = min(bins_per_octave, n_bins) + + cqt_resp = [] + my_y = y.astype(np.float32) + my_sr = float(sr) + my_hop = int(hop_length) + + for i in range(n_octaves): + if i == 0: + sl = slice(-n_filters, None) + else: + sl = slice(-n_filters * (i + 1), -n_filters * i) + + freqs_oct = freqs[sl] + alpha_oct = alpha[sl] + + basis, basis_lengths = _build_wavelet(freqs_oct, my_sr, 1, alpha_oct) + n_fft_oct = basis.shape[1] + + # Frequency-domain normalisation + basis = basis.astype(np.complex64) + basis *= basis_lengths[:, np.newaxis] / float(n_fft_oct) + fft_basis = scipy.fft.fft(basis, n=n_fft_oct, axis=1)[:, :(n_fft_oct // 2) + 1] + fft_basis = _sparsify_rows(fft_basis, quantile=0.01) + fft_basis = fft_basis * np.sqrt(sr / my_sr) + + y_pad = np.pad(my_y, int(n_fft_oct // 2), mode='constant') + n_frames = 1 + (len(y_pad) - n_fft_oct) // my_hop + frames = np.lib.stride_tricks.as_strided( + y_pad, + shape=(n_fft_oct, n_frames), + strides=(y_pad.strides[0], y_pad.strides[0] * my_hop), + ) + stft_result = scipy.fft.rfft(frames, axis=0) + cqt_resp.append(fft_basis.dot(stft_result)) + + if my_hop % 2 == 0: + my_hop //= 2 + my_sr /= 2.0 + my_y = _resample_half(my_y) + + max_col = min(c.shape[-1] for c in cqt_resp) + cqt_out = np.empty((n_bins, max_col), dtype=np.complex64) + end = n_bins + for c_i in cqt_resp: + n_oct = c_i.shape[0] + if end < n_oct: + cqt_out[:end, :] = c_i[-end:, :max_col] + else: + cqt_out[end - n_oct:end, :] = c_i[:, :max_col] + end -= n_oct + + cqt_out /= np.sqrt(lengths)[:, np.newaxis] + return np.abs(cqt_out).astype(np.float32) + + +def cq_to_chroma_mapping(n_input, bins_per_octave=12, n_chroma=12, fmin=None): + """Map CQT bins to chroma bins.""" + + if fmin is None: + fmin = 32.70319566257483 # C1 note frequency + + n_merge = bins_per_octave / n_chroma + cq_to_ch = np.repeat(np.eye(n_chroma), int(n_merge), axis=1) + cq_to_ch = np.roll(cq_to_ch, -int(n_merge // 2), axis=1) + n_octaves = int(np.ceil(n_input / bins_per_octave)) + cq_to_ch = np.tile(cq_to_ch, n_octaves)[:, :n_input] + + midi_0 = np.mod(12 * np.log2(fmin / 440.0) + 69, 12) + roll = int(np.round(midi_0 * (n_chroma / 12.0))) + cq_to_ch = np.roll(cq_to_ch, roll, axis=0) + + return cq_to_ch.astype(np.float32) + + +def _parabolic_interpolation(S, axis=-2): + """Compute parabolic interpolation shift for peak refinement.""" + S_next = np.roll(S, -1, axis=axis) + S_prev = np.roll(S, 1, axis=axis) + + a = S_next + S_prev - 2 * S + b = (S_next - S_prev) / 2.0 + + shifts = np.zeros_like(S) + valid = np.abs(b) < np.abs(a) + shifts[valid] = -b[valid] / a[valid] + + if axis == -2 or axis == S.ndim - 2: + shifts[0, :] = 0 + shifts[-1, :] = 0 + elif axis == 0: + shifts[0, ...] = 0 + shifts[-1, ...] = 0 + + return shifts + + +def _localmax(S, axis=-2): + """Find local maxima along an axis.""" + + S_prev = np.roll(S, 1, axis=axis) + S_next = np.roll(S, -1, axis=axis) + + local_max = (S > S_prev) & (S >= S_next) + + if axis == -2 or axis == S.ndim - 2: + local_max[-1, :] = S[-1, :] > S[-2, :] + # First element is never a local max (strict inequality with previous) + local_max[0, :] = False + elif axis == 0: + local_max[-1, ...] = S[-1, ...] > S[-2, ...] + local_max[0, ...] = False + + return local_max + + +def piptrack(y=None, sr=22050, S=None, n_fft=2048, hop_length=512, + fmin=150.0, fmax=4000.0, threshold=0.1): + """Pitch tracking on thresholded parabolically-interpolated STFT.""" + + # Compute STFT if not provided + if S is None: + if y is None: + raise ValueError("Either y or S must be provided") + + fft_window = scipy.signal.get_window('hann', n_fft, fftbins=True) + if len(fft_window) < n_fft: + lpad = int((n_fft - len(fft_window)) // 2) + fft_window = np.pad(fft_window, (lpad, int(n_fft - len(fft_window) - lpad)), mode='constant') + fft_window = fft_window.reshape((-1, 1)) + + y_pad = np.pad(y, int(n_fft // 2), mode='constant') + n_frames = 1 + (len(y_pad) - n_fft) // hop_length + frames = np.lib.stride_tricks.as_strided( + y_pad, + shape=(n_fft, n_frames), + strides=(y_pad.strides[0], y_pad.strides[0] * hop_length) + ) + + S = scipy.fft.rfft((fft_window * frames).astype(np.float32), axis=0) + + S = np.abs(S) + + fmin = max(fmin, 0) + fmax = min(fmax, float(sr) / 2) + + fft_freqs = np.fft.rfftfreq(S.shape[0] * 2 - 2, 1.0 / sr) + if len(fft_freqs) > S.shape[0]: + fft_freqs = fft_freqs[:S.shape[0]] + + shift = _parabolic_interpolation(S, axis=0) + avg = np.gradient(S, axis=0) + dskew = 0.5 * avg * shift + + pitches = np.zeros_like(S) + mags = np.zeros_like(S) + + freq_mask = (fmin <= fft_freqs) & (fft_freqs < fmax) + freq_mask = freq_mask.reshape(-1, 1) + + ref_value = threshold * np.max(S, axis=0, keepdims=True) + local_max = _localmax(S * (S > ref_value), axis=0) + idx = np.nonzero(freq_mask & local_max) + + pitches[idx] = (idx[0] + shift[idx]) * float(sr) / (S.shape[0] * 2 - 2) + mags[idx] = S[idx] + dskew[idx] + + return pitches, mags + + +def hz_to_octs(frequencies, tuning=0.0, bins_per_octave=12): + """Convert frequencies (Hz) to octave numbers.""" + + A440 = 440.0 * 2.0 ** (tuning / bins_per_octave) + octs = np.log2(np.asanyarray(frequencies) / (float(A440) / 16)) + return octs + + +def pitch_tuning(frequencies, resolution=0.01, bins_per_octave=12): + """Estimate tuning offset from a collection of pitches.""" + + frequencies = np.atleast_1d(frequencies) + frequencies = frequencies[frequencies > 0] + + if not np.any(frequencies): + return 0.0 + + residual = np.mod(bins_per_octave * hz_to_octs(frequencies, tuning=0.0, + bins_per_octave=bins_per_octave), 1.0) + residual[residual >= 0.5] -= 1.0 + + bins = np.linspace(-0.5, 0.5, int(np.ceil(1.0 / resolution)) + 1) + counts, tuning = np.histogram(residual, bins) + tuning_est = tuning[np.argmax(counts)] + return tuning_est + + +def estimate_tuning(y, sr=22050, bins_per_octave=12): + """Estimate global tuning deviation from 12-TET.""" + n_fft = 2048 + hop_length = 512 + + if len(y) < n_fft: + return 0.0 + + pitch, mag = piptrack(y=y, sr=sr, n_fft=n_fft, hop_length=hop_length, + fmin=150.0, fmax=4000.0, threshold=0.1) + + pitch_mask = pitch > 0 + + if not pitch_mask.any(): + return 0.0 + + threshold = np.median(mag[pitch_mask]) + valid_pitches = pitch[(mag >= threshold) & pitch_mask] + + if len(valid_pitches) == 0: + return 0.0 + + tuning = pitch_tuning(valid_pitches, resolution=0.01, bins_per_octave=bins_per_octave) + + return float(tuning) + + +def compute_chroma_cens(y, sr=22050, hop_length=512, n_chroma=12, + n_octaves=7, bins_per_octave=36, + win_len_smooth=41, norm=2): + """Compute Chroma Energy Normalized Statistics (CENS) features.""" + + tuning = estimate_tuning(y, sr, bins_per_octave=bins_per_octave) + + fmin = 32.70319566257483 # C1 note frequency + n_bins = n_octaves * bins_per_octave + cqt_mag = compute_cqt(y, sr=sr, hop_length=hop_length, + fmin=fmin, n_bins=n_bins, + bins_per_octave=bins_per_octave, + tuning=tuning) + + chroma_map = cq_to_chroma_mapping(n_bins, bins_per_octave=bins_per_octave, + n_chroma=n_chroma, fmin=fmin) + chroma = np.dot(chroma_map, cqt_mag) + + threshold = np.finfo(chroma.dtype).tiny + chroma_sum = np.sum(np.abs(chroma), axis=0, keepdims=True) + chroma_sum = np.maximum(chroma_sum, threshold) + chroma = chroma / chroma_sum + + quant_steps = [0.4, 0.2, 0.1, 0.05] + quant_weights = [0.25, 0.25, 0.25, 0.25] + chroma_quant = np.zeros_like(chroma) + for step, weight in zip(quant_steps, quant_weights): + chroma_quant += (chroma > step) * weight + + if win_len_smooth is not None and win_len_smooth > 0: + win = scipy.signal.get_window('hann', win_len_smooth + 2, fftbins=False) + win /= np.sum(win) + win = win.reshape(1, -1) + chroma_smooth = scipy.ndimage.convolve(chroma_quant, win, mode='constant') + else: + chroma_smooth = chroma_quant + + if norm == 2: + threshold = np.finfo(chroma_smooth.dtype).tiny + chroma_norm = np.sqrt(np.sum(chroma_smooth ** 2, axis=0, keepdims=True)) + chroma_norm = np.maximum(chroma_norm, threshold) + chroma_smooth = chroma_smooth / chroma_norm + elif norm == np.inf: + threshold = np.finfo(chroma_smooth.dtype).tiny + chroma_norm = np.max(np.abs(chroma_smooth), axis=0, keepdims=True) + chroma_norm = np.maximum(chroma_norm, threshold) + chroma_smooth = chroma_smooth / chroma_norm + + return chroma_smooth + + +def _create_mel_filterbank(sr, n_fft, n_mels=128, fmin=0.0, fmax=None): + """Create mel-scale filterbank matrix.""" + if fmax is None: + fmax = sr / 2.0 + mel_basis = np.zeros((n_mels, int(1 + n_fft // 2)), dtype=np.float32) + fftfreqs = np.fft.rfftfreq(n=n_fft, d=1.0 / sr) + min_mel = hz_to_mel(fmin) + max_mel = hz_to_mel(fmax) + mels = np.linspace(min_mel, max_mel, n_mels + 2) + mel_f = mel_to_hz(mels) + fdiff = np.diff(mel_f) + ramps = np.subtract.outer(mel_f, fftfreqs) + + for i in range(n_mels): + lower = -ramps[i] / fdiff[i] + upper = ramps[i + 2] / fdiff[i + 1] + mel_basis[i] = np.maximum(0, np.minimum(lower, upper)) + + enorm = 2.0 / (mel_f[2:n_mels + 2] - mel_f[:n_mels]) + mel_basis *= enorm[:, np.newaxis] + return mel_basis + + +def _compute_mel_spectrogram(data, sr, n_fft=2048, hop_length=512, n_mels=128): + """Compute mel spectrogram from audio signal.""" + fft_window = scipy.signal.get_window('hann', n_fft, fftbins=True) + if len(fft_window) < n_fft: + lpad = int((n_fft - len(fft_window)) // 2) + fft_window = np.pad(fft_window, (lpad, int(n_fft - len(fft_window) - lpad)), mode='constant') + + fft_window = fft_window.reshape((-1, 1)) + data_padded = np.pad(data, int(n_fft // 2), mode='constant') + n_frames = 1 + (len(data_padded) - n_fft) // hop_length + shape = (n_fft, n_frames) + strides = (data_padded.strides[0], data_padded.strides[0] * hop_length) + frames = np.lib.stride_tricks.as_strided(data_padded, shape=shape, strides=strides) + + stft_result = scipy.fft.rfft(fft_window * frames, axis=0).astype(np.complex64) + power_spec = np.abs(stft_result) ** 2 + + mel_basis = _create_mel_filterbank(sr, n_fft, n_mels=n_mels, fmin=0.0, fmax=sr / 2.0) + mel_spec = np.dot(mel_basis, power_spec) + return mel_spec.astype(np.float32) + + +def quick_tempo_estimate(audio_np, sr, start_bpm=120.0, std_bpm=1.0, hop_length=512): + """Estimate tempo using autocorrelation tempogram.""" + + if len(audio_np) < hop_length * 10: + logging.warning("Audio too short for tempo estimation, returning default BPM of 120.0") + return 120.0 + + n_fft = 2048 + mel_S = _compute_mel_spectrogram(audio_np, sr, n_fft=n_fft, hop_length=hop_length, n_mels=128) + log_mel_S = 10.0 * np.log10(np.maximum(1e-10, mel_S)) + + lag = 1 + S_diff = log_mel_S[:, lag:] - log_mel_S[:, :-lag] + S_onset = np.maximum(0.0, S_diff) + onset_env_pre = np.mean(S_onset, axis=0) + pad_width = lag + n_fft // (2 * hop_length) + onset_env = np.pad(onset_env_pre, (pad_width, 0), mode='constant') + onset_env = onset_env[:mel_S.shape[1]] + + return estimate_tempo_from_onset(onset_env, sr, hop_length, start_bpm, std_bpm, max_tempo=320.0) + + +def estimate_tempo_from_onset(onset_env, sr, hop_length, start_bpm=120.0, std_bpm=1.0, max_tempo=320.0): + """Estimate tempo from onset strength envelope using autocorrelation tempogram.""" + if len(onset_env) < 20: + return 120.0 + + ac_size = 8.0 + win_length = int(np.round(ac_size * sr / hop_length)) + win_length = min(win_length, len(onset_env)) + + pad_width = win_length // 2 + onset_padded = np.pad(onset_env, (pad_width, pad_width), mode='linear_ramp', end_values=(0, 0)) + + n_frames = len(onset_env) + shape = (win_length, n_frames) + strides = (onset_padded.strides[0], onset_padded.strides[0]) + frames = np.lib.stride_tricks.as_strided(onset_padded, shape=shape, strides=strides) + + hann_window = scipy.signal.get_window('hann', win_length, fftbins=True) + windowed_frames = frames * hann_window[:, np.newaxis] + + tempogram = np.zeros((win_length, n_frames)) + for i in range(n_frames): + frame = windowed_frames[:, i] + n_pad = scipy.fft.next_fast_len(2 * len(frame) - 1) + fft_result = scipy.fft.rfft(frame, n=n_pad) + powspec = np.abs(fft_result) ** 2 + ac = scipy.fft.irfft(powspec, n=n_pad) + tempogram[:, i] = ac[:win_length] + + ac_max = np.max(np.abs(tempogram), axis=0) + mask = ac_max > 0 + tempogram[:, mask] /= ac_max[mask] + + tempogram_mean = np.mean(tempogram, axis=1) + tempogram_mean = np.maximum(tempogram_mean, 0) + + bpms = np.zeros(win_length, dtype=np.float64) + bpms[0] = np.inf + bpms[1:] = 60.0 * sr / (hop_length * np.arange(1.0, win_length)) + + logprior = -0.5 * ((np.log2(bpms) - np.log2(start_bpm)) / std_bpm) ** 2 + + if max_tempo is not None: + max_idx = int(np.argmax(bpms < max_tempo)) + if max_idx > 0: + logprior[:max_idx] = -np.inf + + weighted = np.log1p(1e6 * tempogram_mean) + logprior + best_idx = int(np.argmax(weighted[1:])) + 1 + tempo = bpms[best_idx] + + return tempo + + +def detect_onset_peaks(onset_env, sr=22050, hop_length=512, pre_max=0.03, post_max=0.0, + pre_avg=0.10, post_avg=0.10, wait=0.03, delta=0.07): + """Detect onset peaks using peak picking algorithm.""" + + onset_normalized = onset_env - np.min(onset_env) + onset_max = np.max(onset_normalized) + if onset_max > 0: + onset_normalized = onset_normalized / onset_max + + pre_max_frames = int(pre_max * sr / hop_length) + post_max_frames = int(post_max * sr / hop_length) + 1 + pre_avg_frames = int(pre_avg * sr / hop_length) + post_avg_frames = int(post_avg * sr / hop_length) + 1 + wait_frames = int(wait * sr / hop_length) + + peaks = np.zeros(len(onset_normalized), dtype=bool) + peaks[0] = (onset_normalized[0] >= np.max(onset_normalized[:min(post_max_frames, len(onset_normalized))])) + peaks[0] &= (onset_normalized[0] >= np.mean(onset_normalized[:min(post_avg_frames, len(onset_normalized))]) + delta) + + if peaks[0]: + n = wait_frames + 1 + else: + n = 1 + + while n < len(onset_normalized): + maxn = np.max(onset_normalized[max(0, n - pre_max_frames):min(n + post_max_frames, len(onset_normalized))]) + peaks[n] = (onset_normalized[n] == maxn) + + if not peaks[n]: + n += 1 + continue + + avgn = np.mean(onset_normalized[max(0, n - pre_avg_frames):min(n + post_avg_frames, len(onset_normalized))]) + peaks[n] &= (onset_normalized[n] >= avgn + delta) + + if not peaks[n]: + n += 1 + continue + + n += wait_frames + 1 + + return np.flatnonzero(peaks).astype(np.int32) + + +def track_beats(onset_env, tempo, sr, hop_length, tightness=100, trim=True): + """Track beats using dynamic programming.""" + + frame_rate = sr / hop_length + frames_per_beat = np.round(frame_rate * 60.0 / tempo) + + if frames_per_beat <= 0 or len(onset_env) < 2: + return np.array([], dtype=np.int32) + + onset_std = np.std(onset_env, ddof=1) + if onset_std > 0: + onset_normalized = onset_env / onset_std + else: + onset_normalized = onset_env + + window_range = np.arange(-frames_per_beat, frames_per_beat + 1) + window = np.exp(-0.5 * (window_range * 32.0 / frames_per_beat) ** 2) + + localscore = scipy.signal.convolve(onset_normalized, window, mode='same') + + backlink = np.full(len(localscore), -1, dtype=np.int32) + cumscore = np.zeros(len(localscore), dtype=np.float64) + + score_thresh = 0.01 * localscore.max() + first_beat = True + + backlink[0] = -1 + cumscore[0] = localscore[0] + + fpb = int(frames_per_beat) + + for i in range(1, len(localscore)): + score_i = localscore[i] + best_score = -np.inf + beat_location = -1 + + search_start = int(i - np.round(fpb / 2.0)) + search_end = int(i - 2 * fpb - 1) + + for loc in range(search_start, search_end, -1): + if loc < 0: + break + + score = cumscore[loc] - tightness * (np.log(i - loc) - np.log(fpb)) ** 2 + + if score > best_score: + best_score = score + beat_location = loc + + if beat_location >= 0: + cumscore[i] = score_i + best_score + else: + cumscore[i] = score_i + + if first_beat and score_i < score_thresh: + backlink[i] = -1 + else: + backlink[i] = beat_location + first_beat = False + + local_max_mask = np.zeros(len(cumscore), dtype=bool) + + local_max_mask[0] = False + + for i in range(1, len(cumscore) - 1): + local_max_mask[i] = (cumscore[i] > cumscore[i-1]) and (cumscore[i] >= cumscore[i+1]) + + if len(cumscore) > 1: + local_max_mask[-1] = cumscore[-1] > cumscore[-2] + + if np.any(local_max_mask): + median_max = np.median(cumscore[local_max_mask]) + threshold = 0.5 * median_max + + tail = -1 + for i in range(len(cumscore) - 1, -1, -1): + if local_max_mask[i] and cumscore[i] >= threshold: + tail = i + break + else: + tail = len(cumscore) - 1 + + beats = np.zeros(len(localscore), dtype=bool) + n = tail + visited = set() + while n >= 0 and n not in visited: + beats[n] = True + visited.add(n) + n = backlink[n] + + if trim and np.any(beats): + beat_positions = np.flatnonzero(beats) + + beat_localscores = localscore[beat_positions] + + w = np.hanning(5) + smooth_boe_full = np.convolve(beat_localscores, w) + smooth_boe = smooth_boe_full[len(w)//2 : len(localscore) + len(w)//2] + + threshold = 0.5 * np.sqrt(np.mean(smooth_boe ** 2)) + + start_frame = 0 + while start_frame < len(localscore) and localscore[start_frame] <= threshold: + beats[start_frame] = False + start_frame += 1 + + end_frame = len(localscore) - 1 + while end_frame >= 0 and localscore[end_frame] <= threshold: + beats[end_frame] = False + end_frame -= 1 + + return np.flatnonzero(beats).astype(np.int32) + +def compute_onset_envelope(mel_spec_db, n_fft=2048, hop_length=512): + """Compute onset strength envelope from a log-mel spectrogram (dB).""" + lag = 1 + onset_diff = mel_spec_db[:, lag:] - mel_spec_db[:, :-lag] + onset_diff = np.maximum(0.0, onset_diff) + envelope_pre_pad = np.mean(onset_diff, axis=0) + + pad_width = lag + n_fft // (2 * hop_length) + envelope = np.pad(envelope_pre_pad, (pad_width, 0), mode='constant') + envelope = envelope[:mel_spec_db.shape[1]] + + return envelope + +def compute_mfcc(mel_spec_db, n_mfcc=20): + """Compute MFCC features from a log-mel spectrogram (dB).""" + mfcc = scipy.fft.dct(mel_spec_db, axis=0, type=2, norm='ortho')[:n_mfcc].T + return mfcc.astype(np.float32) + + +def power_to_db(S, amin=1e-10, top_db=80.0, ref=1.0): + """Convert a power spectrogram (amplitude squared) to decibel (dB) units""" + S = np.asarray(S) + log_spec = 10.0 * np.log10(np.maximum(amin, S)) + log_spec -= 10.0 * np.log10(np.maximum(amin, ref)) + if top_db is not None: + log_spec = np.maximum(log_spec, log_spec.max() - top_db) + return log_spec + + +class WanDancerEncodeAudio(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="WanDancerEncodeAudio", + category="conditioning/video_models", + inputs=[ + io.Audio.Input("audio"), + io.Int.Input("video_frames", default=149, min=1, max=nodes.MAX_RESOLUTION, step=4), + io.Float.Input("audio_inject_scale", default=1.0, min=0.0, max=10.0, step=0.01, tooltip="The scale for the audio features when injected into the video model."), + ], + outputs=[ + io.AudioEncoderOutput.Output(display_name="audio_encoder_output"), + io.String.Output(display_name="fps_string", tooltip="The calculated fps based on the audio length and the number of video frames. Used in the prompt."), + ], + ) + + @classmethod + def execute(cls, video_frames, audio_inject_scale, audio) -> io.NodeOutput: + waveform = audio["waveform"][0] + sample_rate = audio["sample_rate"] + base_fps = 30 + hop_length = 512 + model_sr = 22050 + n_fft = 2048 + + # start tempo from original audio (not the resampled one) to match the reference pipeline + if waveform.shape[0] > 1: + waveform = waveform.mean(dim=0, keepdim=False) + + start_bpm = quick_tempo_estimate(waveform.squeeze().cpu().numpy(), sample_rate, hop_length=hop_length) + + # resample to the sample rate used for feature extraction + resample_sr = base_fps * hop_length + waveform = torchaudio.functional.resample(waveform, sample_rate, resample_sr) + + waveform_np = waveform.cpu().numpy().squeeze() + mel_spec = _compute_mel_spectrogram(waveform_np, model_sr, n_fft, hop_length, n_mels=128) + mel_spec_db = power_to_db(mel_spec, amin=1e-10, top_db=80.0, ref=1.0) + envelope = compute_onset_envelope(mel_spec_db, n_fft, hop_length) + mfcc = compute_mfcc(mel_spec_db, n_mfcc=20) + chroma = compute_chroma_cens(y=waveform_np, sr=model_sr, hop_length=hop_length).T + # detect peaks + peak_idxs = detect_onset_peaks(envelope, sr=model_sr, hop_length=hop_length) + peak_onehot = np.zeros_like(envelope, dtype=np.float32) + peak_onehot[peak_idxs] = 1.0 + # detect beats + beat_tracking_tempo = estimate_tempo_from_onset(envelope, sr=model_sr, hop_length=hop_length, start_bpm=start_bpm) + beat_idxs = track_beats(envelope, beat_tracking_tempo, model_sr, hop_length, tightness=100, trim=True) + beat_onehot = np.zeros_like(envelope, dtype=np.float32) + beat_onehot[beat_idxs] = 1.0 + + audio_feature = np.concatenate( + [envelope[:, None], mfcc, chroma, peak_onehot[:, None], beat_onehot[:, None]], + axis=-1, + ) + audio_feature = torch.from_numpy(audio_feature).unsqueeze(0).to(comfy.model_management.intermediate_device()) + + fps = float(base_fps / int(audio_feature.shape[1] / video_frames + 0.5)) + + audio_encoder_output = { + "audio_feature": audio_feature, + "fps": fps, + "audio_inject_scale": audio_inject_scale, + } + + if int(fps + 0.5) != 30: + fps_string = " 帧率是{:.4f}".format(fps) # "frame rate is" in Chinese, as it was in the original pipeline + else: + fps_string = ", 帧率是30fps。" # to match the reference pipeline when the fps is 30 + + return io.NodeOutput(audio_encoder_output, fps_string) + + +class WanDancerVideo(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="WanDancerVideo", + category="conditioning/video_models", + inputs=[ + io.Conditioning.Input("positive"), + io.Conditioning.Input("negative"), + io.Vae.Input("vae"), + io.Int.Input("width", default=480, min=16, max=nodes.MAX_RESOLUTION, step=16), + io.Int.Input("height", default=832, min=16, max=nodes.MAX_RESOLUTION, step=16), + io.Int.Input("length", default=149, min=1, max=nodes.MAX_RESOLUTION, step=4, tooltip="The number of frames in the generated video. Should stay 149 for WanDancer."), + io.ClipVisionOutput.Input("clip_vision_output", optional=True, tooltip="The CLIP vision embeds for the first frame."), + io.ClipVisionOutput.Input("clip_vision_output_ref", optional=True, tooltip="The CLIP vision embeds for the reference image."), + io.Image.Input("start_image", optional=True, tooltip="The initial image(s) to be encoded, can be any number of frames."), + io.Mask.Input("mask", optional=True, tooltip="Image conditioning mask for the start image(s). White is kept, black is generated. Used for the local generations."), + io.AudioEncoderOutput.Input("audio_encoder_output", optional=True), + ], + outputs=[ + io.Conditioning.Output(display_name="positive"), + io.Conditioning.Output(display_name="negative"), + io.Latent.Output(display_name="latent", tooltip="Empty latent."), + ], + ) + + @classmethod + def execute(cls, positive, negative, vae, width, height, length, start_image=None, mask=None, clip_vision_output=None, clip_vision_output_ref=None, audio_encoder_output=None) -> io.NodeOutput: + latent = torch.zeros([1, 16, ((length - 1) // 4) + 1, height // 8, width // 8], device=comfy.model_management.intermediate_device()) + if start_image is not None: + start_image = comfy.utils.common_upscale(start_image[:length].movedim(-1, 1), width, height, "bilinear", "center").movedim(1, -1) + image = torch.zeros((length, height, width, start_image.shape[-1]), device=start_image.device, dtype=start_image.dtype) + image[:start_image.shape[0]] = start_image + + concat_latent_image = vae.encode(image[:, :, :, :3]) + if mask is None: + concat_mask = torch.ones((1, 1, latent.shape[2], concat_latent_image.shape[-2], concat_latent_image.shape[-1]), device=start_image.device, dtype=start_image.dtype) + concat_mask[:, :, :((start_image.shape[0] - 1) // 4) + 1] = 0.0 + else: + concat_mask = 1 - mask[:length].unsqueeze(0) + concat_mask = comfy.utils.common_upscale(concat_mask, concat_latent_image.shape[-2], concat_latent_image.shape[-1], "nearest-exact", "disabled") + concat_mask = torch.cat([torch.repeat_interleave(concat_mask[:, 0:1], repeats=4, dim=1), concat_mask[:, 1:]], dim=1) + concat_mask = concat_mask.view(1, concat_mask.shape[1] // 4, 4, concat_latent_image.shape[-2], concat_latent_image.shape[-1]).transpose(1, 2) + + positive = node_helpers.conditioning_set_values(positive, {"concat_latent_image": concat_latent_image, "concat_mask": concat_mask}) + negative = node_helpers.conditioning_set_values(negative, {"concat_latent_image": concat_latent_image, "concat_mask": concat_mask}) + + if clip_vision_output is not None: + positive = node_helpers.conditioning_set_values(positive, {"clip_vision_output": clip_vision_output, "clip_vision_output_ref": clip_vision_output_ref}) + negative = node_helpers.conditioning_set_values(negative, {"clip_vision_output": clip_vision_output, "clip_vision_output_ref": clip_vision_output_ref}) + + if audio_encoder_output is not None: + positive = node_helpers.conditioning_set_values(positive, {"audio_embed": audio_encoder_output["audio_feature"], "fps": audio_encoder_output["fps"], "audio_inject_scale": audio_encoder_output.get("audio_inject_scale", 1.0)}) + negative = node_helpers.conditioning_set_values(negative, {"audio_embed": audio_encoder_output["audio_feature"], "fps": audio_encoder_output["fps"], "audio_inject_scale": audio_encoder_output.get("audio_inject_scale", 1.0)}) + + out_latent = {} + out_latent["samples"] = latent + return io.NodeOutput(positive, negative, out_latent) + + +class VAEDecodeVideoFramewise(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="VAEDecodeVideoFramewise", + category="latent", + description="Decodes video latents one latent at a time.", + search_aliases=["decode", "decode latent", "latent to image", "render latent"], + inputs=[ + io.Latent.Input("samples", tooltip="The latent to be decoded."), + io.Vae.Input("vae", tooltip="The VAE model used for decoding the latent."), + ], + outputs=[ + io.Image.Output(tooltip="The decoded images."), + ], + ) + + @classmethod + def execute(cls, vae, samples) -> io.NodeOutput: + latent = samples["samples"] + if latent.is_nested: + latent = latent.unbind()[0] + + # reshape temporal dimension into batch + B, C, T, H, W = latent.shape + latent_batched = latent.transpose(1, 2).reshape(B * T, C, 1, H, W) + images = vae.decode(latent_batched).squeeze(1) + + return io.NodeOutput(images) + +class WanDancerPadKeyframes(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="WanDancerPadKeyframes", + category="image/video", + inputs=[ + io.Image.Input("images",), + io.Int.Input("segment_length", default=149, min=1, max=10000, tooltip="Length of this segment (usually 149 frames)"), + io.Int.Input("segment_index", default=0, min=0, max=100, tooltip="Which segment this is (0 for first, 1 for second, etc.)"), + io.Audio.Input("audio", tooltip="Audio to calculate total output frames from and extract segment audio."), + ], + outputs=[ + io.Image.Output(display_name="keyframes_sequence", tooltip="Padded keyframe sequence"), + io.Mask.Output(display_name="keyframes_mask", tooltip="Mask indicating valid frames"), + io.Audio.Output(display_name="audio_segment", tooltip="Audio segment for this video segment"), + ], + ) + + @classmethod + def do_execute(cls, images, segment_length, segment_index, audio): + B, H, W, C = images.shape + fps = 30 + + # calculate total frames + audio_duration = audio["waveform"].shape[-1] / audio["sample_rate"] + segment_duration = segment_length / fps + buffer = 0.2 + num_segments = int((audio_duration - buffer) / segment_duration) + 1 if audio_duration > buffer else 0 + total_frames = num_segments * segment_length + + mask = torch.zeros((segment_length, H, W), device=images.device, dtype=images.dtype) + keyframes = torch.zeros((segment_length, H, W, C), dtype=images.dtype, device=images.device) + + # guard: with no audio or no images, nothing to place — leave keyframes/mask zeroed + if total_frames > 0 and B > 0: + frame_interval = float(total_frames) / B + seg_num = int(math.ceil(total_frames / segment_length)) + is_last_segment = (segment_index == seg_num - 1) + + positions = [] + images_before_this_segment = 0 + + # count images consumed by previous segments + for seg_idx in range(segment_index): + end_idx = (total_frames - segment_length * seg_idx - 1) if seg_idx == seg_num - 1 else (segment_length - 1) + cnt = 0 + while cnt * frame_interval < end_idx - frame_interval: + cnt += 1 + images_before_this_segment += cnt + + # positions for current segment + end_index = (total_frames - segment_length * segment_index - 1) if is_last_segment else (segment_length - 1) + cnt = 0 + while cnt * frame_interval < end_index - frame_interval: + pos = int(math.ceil(frame_interval * cnt)) + positions.append((pos, images_before_this_segment + cnt)) + cnt += 1 + positions.append((end_index, images_before_this_segment + cnt)) + + valid_positions = [(pos, idx) for pos, idx in positions if idx < B and pos < segment_length] + + if valid_positions: + seg_positions, img_indices = zip(*valid_positions) + seg_positions = torch.tensor(seg_positions, dtype=torch.long, device=images.device) + img_indices = torch.tensor(img_indices, dtype=torch.long, device=images.device) + mask[seg_positions] = 1 + keyframes[seg_positions] = images[img_indices] + + # extract audio segment + segment_duration = segment_length / fps + start_time = segment_index * segment_duration + end_time = min(start_time + segment_duration, audio_duration) + + sample_rate = audio["sample_rate"] + start_sample = int(start_time * sample_rate) + end_sample = int(end_time * sample_rate) + + audio_segment_waveform = audio["waveform"][:, :, start_sample:end_sample] + audio_segment = { + "waveform": audio_segment_waveform, + "sample_rate": sample_rate + } + + return keyframes, mask, audio_segment + + @classmethod + def execute(cls, images, segment_length, segment_index, audio=None) -> io.NodeOutput: + return io.NodeOutput(*cls.do_execute(images, segment_length, segment_index, audio)) + +class WanDancerPadKeyframesList(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="WanDancerPadKeyframesList", + category="image/video", + inputs=[ + io.Image.Input("images"), + io.Int.Input("segment_length", default=149, min=1, max=10000, tooltip="Length of each segment (usually 149 frames)"), + io.Int.Input("num_segments", default=1, min=1, max=100, tooltip="How many padded segments to emit as lists."), + io.Audio.Input("audio", tooltip="Audio to slice for each emitted segment."), + ], + outputs=[ + io.Image.Output(display_name="keyframes_sequence", tooltip="Padded keyframe sequences", is_output_list=True), + io.Mask.Output(display_name="keyframes_mask", tooltip="Masks indicating valid frames", is_output_list=True), + io.Audio.Output(display_name="audio_segment", tooltip="Audio segment for each video segment", is_output_list=True), + ], + ) + + @classmethod + def execute(cls, images, segment_length, num_segments, audio=None) -> io.NodeOutput: + outputs = [WanDancerPadKeyframes.do_execute(images, segment_length, i, audio) for i in range(num_segments)] + keyframes, masks, audio_segments = zip(*outputs) + return io.NodeOutput(list(keyframes), list(masks), list(audio_segments)) + +class WanDancerExtension(ComfyExtension): + @override + async def get_node_list(self) -> list[type[io.ComfyNode]]: + return [ + WanDancerVideo, + VAEDecodeVideoFramewise, + WanDancerEncodeAudio, + WanDancerPadKeyframes, + WanDancerPadKeyframesList, + ] + +async def comfy_entrypoint() -> WanDancerExtension: + return WanDancerExtension() diff --git a/nodes.py b/nodes.py index 5755f0bb8..ec66e54d7 100644 --- a/nodes.py +++ b/nodes.py @@ -2434,6 +2434,7 @@ async def init_builtin_extra_nodes(): "nodes_frame_interpolation.py", "nodes_sam3.py", "nodes_void.py", + "nodes_wandancer.py", ] import_failed = [] From 20f5e474da28bd4225ab61b3d5d791e1b32ba069 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Sat, 9 May 2026 14:17:00 -0700 Subject: [PATCH 031/145] Use LatentCutToBatch instead. (#13815) Removed VAEDecodeVideoFramewise from nodes_wandancer.py. --- comfy_extras/nodes_wandancer.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/comfy_extras/nodes_wandancer.py b/comfy_extras/nodes_wandancer.py index faaeb9020..fc005ed4c 100644 --- a/comfy_extras/nodes_wandancer.py +++ b/comfy_extras/nodes_wandancer.py @@ -842,36 +842,6 @@ class WanDancerVideo(io.ComfyNode): return io.NodeOutput(positive, negative, out_latent) -class VAEDecodeVideoFramewise(io.ComfyNode): - @classmethod - def define_schema(cls): - return io.Schema( - node_id="VAEDecodeVideoFramewise", - category="latent", - description="Decodes video latents one latent at a time.", - search_aliases=["decode", "decode latent", "latent to image", "render latent"], - inputs=[ - io.Latent.Input("samples", tooltip="The latent to be decoded."), - io.Vae.Input("vae", tooltip="The VAE model used for decoding the latent."), - ], - outputs=[ - io.Image.Output(tooltip="The decoded images."), - ], - ) - - @classmethod - def execute(cls, vae, samples) -> io.NodeOutput: - latent = samples["samples"] - if latent.is_nested: - latent = latent.unbind()[0] - - # reshape temporal dimension into batch - B, C, T, H, W = latent.shape - latent_batched = latent.transpose(1, 2).reshape(B * T, C, 1, H, W) - images = vae.decode(latent_batched).squeeze(1) - - return io.NodeOutput(images) - class WanDancerPadKeyframes(io.ComfyNode): @classmethod def define_schema(cls): @@ -992,7 +962,6 @@ class WanDancerExtension(ComfyExtension): async def get_node_list(self) -> list[type[io.ComfyNode]]: return [ WanDancerVideo, - VAEDecodeVideoFramewise, WanDancerEncodeAudio, WanDancerPadKeyframes, WanDancerPadKeyframesList, From 95f6652ef5e931af004d23966533aab43a604ed6 Mon Sep 17 00:00:00 2001 From: LaVie024 <62406970+LaVie024@users.noreply.github.com> Date: Sun, 10 May 2026 07:33:47 +0000 Subject: [PATCH 032/145] Add Boolean support to Math Expression Node (#13224) * Add Boolean support to math expressions * Change boolean result test to assert values --------- Co-authored-by: Alexis Rolland --- comfy_extras/nodes_math.py | 7 ++++--- tests-unit/comfy_extras_test/nodes_math_test.py | 8 +++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/comfy_extras/nodes_math.py b/comfy_extras/nodes_math.py index 8f6e687d2..6030ee9d8 100644 --- a/comfy_extras/nodes_math.py +++ b/comfy_extras/nodes_math.py @@ -63,7 +63,7 @@ class MathExpressionNode(io.ComfyNode): @classmethod def define_schema(cls) -> io.Schema: autogrow = io.Autogrow.TemplateNames( - input=io.MultiType.Input("value", [io.Float, io.Int]), + input=io.MultiType.Input("value", [io.Float, io.Int, io.Boolean]), names=list(string.ascii_lowercase), min=1, ) @@ -82,6 +82,7 @@ class MathExpressionNode(io.ComfyNode): outputs=[ io.Float.Output(display_name="FLOAT"), io.Int.Output(display_name="INT"), + io.Boolean.Output(display_name="BOOL"), ], ) @@ -97,7 +98,7 @@ class MathExpressionNode(io.ComfyNode): result = simple_eval(expression, names=context, functions=MATH_FUNCTIONS) # bool check must come first because bool is a subclass of int in Python - if isinstance(result, bool) or not isinstance(result, (int, float)): + if not isinstance(result, (int, float)): raise ValueError( f"Math Expression '{expression}' must evaluate to a numeric result, " f"got {type(result).__name__}: {result!r}" @@ -106,7 +107,7 @@ class MathExpressionNode(io.ComfyNode): raise ValueError( f"Math Expression '{expression}' produced a non-finite result: {result}" ) - return io.NodeOutput(float(result), int(result)) + return io.NodeOutput(float(result), int(result), bool(result)) class MathExtension(ComfyExtension): diff --git a/tests-unit/comfy_extras_test/nodes_math_test.py b/tests-unit/comfy_extras_test/nodes_math_test.py index fa4cdcac3..714e37c32 100644 --- a/tests-unit/comfy_extras_test/nodes_math_test.py +++ b/tests-unit/comfy_extras_test/nodes_math_test.py @@ -124,9 +124,11 @@ class TestMathExpressionExecute: with pytest.raises(Exception, match="not defined"): self._exec("str(a)", a=42) - def test_boolean_result_raises(self): - with pytest.raises(ValueError, match="got bool"): - self._exec("a > b", a=5, b=3) + def test_boolean_result(self): + result = self._exec("a > b", a=5, b=3) + assert result[2] is True + result = self._exec("a > b", a=3, b=5) + assert result[2] is False def test_empty_expression_raises(self): with pytest.raises(ValueError, match="Expression cannot be empty"): From aa9d2fc713664e9ffe37763f4c9240c0c3eda667 Mon Sep 17 00:00:00 2001 From: "Daxiong (Lin)" Date: Sun, 10 May 2026 19:10:13 +0800 Subject: [PATCH 033/145] chore: update workflow templates to v0.9.73 (#13822) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6fd808772..c5a6f4cec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ comfyui-frontend-package==1.43.18 -comfyui-workflow-templates==0.9.72 +comfyui-workflow-templates==0.9.73 comfyui-embedded-docs==0.4.4 torch torchsde From 1eeaf23f207e733e87d0cfe95fe7d6d1a8892c23 Mon Sep 17 00:00:00 2001 From: "Daxiong (Lin)" Date: Mon, 11 May 2026 01:23:04 +0800 Subject: [PATCH 034/145] Remove advanced flag from layers input in EmptyQwenImageLayeredLatentImage node (#13823) --- comfy_extras/nodes_qwen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy_extras/nodes_qwen.py b/comfy_extras/nodes_qwen.py index 6894367be..fde8fac9a 100644 --- a/comfy_extras/nodes_qwen.py +++ b/comfy_extras/nodes_qwen.py @@ -116,7 +116,7 @@ class EmptyQwenImageLayeredLatentImage(io.ComfyNode): inputs=[ io.Int.Input("width", default=640, min=16, max=nodes.MAX_RESOLUTION, step=16), io.Int.Input("height", default=640, min=16, max=nodes.MAX_RESOLUTION, step=16), - io.Int.Input("layers", default=3, min=0, max=nodes.MAX_RESOLUTION, step=1, advanced=True), + io.Int.Input("layers", default=3, min=0, max=nodes.MAX_RESOLUTION, step=1), io.Int.Input("batch_size", default=1, min=1, max=4096), ], outputs=[ From dabfe73dc0e954554fe9632216149964bb9b295f Mon Sep 17 00:00:00 2001 From: "Daxiong (Lin)" Date: Mon, 11 May 2026 04:50:41 +0800 Subject: [PATCH 035/145] Add New Blueprints (#13570) * Add new blueprints * Add Image Segmentation * Add blueprint Get Video Last Frame (#13613) * Add Video segment * Fix Video Stitch subgraph issue * Update get last frame to get any frame * Add Frame Interpolate blueprint * Correct typo * Name blueprints * Update and add new blueprints * blueprints: add subgraph descriptions for previously undocumented workflows Fill missing definitions.subgraphs[].description across ERNIE, Flux.2, Z-Image base/default, Qwen edit 2509, Wan I2V, SAM3 image/video, and align wording with existing blueprint style. * Add new blueprint * remove Image to Video * Update ZIB blueprint * Refine description * Remove duplicate model entries from Image Edit blueprint * Fix typos * Update IDs --- blueprints/ControlNet (Z-Image-Turbo).json | 1412 +++++++ blueprints/Depth to Video (ltx 2.0).json | 2 +- blueprints/First-Last-Frame to Video.json | 3361 +++++++++++++++++ blueprints/Frame Interpolation.json | 858 +++++ blueprints/Get Any Video Frame.json | 485 +++ .../Image Edit (FireRed Image Edit 1.1).json | 478 +-- blueprints/Image Edit (Flux.2 Dev).json | 2050 ++++++++++ blueprints/Image Edit (Qwen 2509).json | 1947 ++++++++++ blueprints/Image Segmentation (SAM3).json | 714 ++++ blueprints/Image to Video (Wan 2.2).json | 2 +- blueprints/Remove Background (BiRefNet).json | 397 ++ .../Text to Image (Ernie Image Turbo).json | 2112 +++++++++++ blueprints/Text to Image (Ernie Image).json | 2190 +++++++++++ blueprints/Text to Image (Flux.1 Dev).json | 2 +- .../Text to Image (Flux.1 Krea Dev).json | 2 +- blueprints/Text to Image (Flux.2 Dev).json | 1870 +++++++++ blueprints/Text to Image (Z-Image-Base).json | 1184 ++++++ blueprints/Text to Image (Z-Image-Turbo).json | 441 ++- blueprints/Text to Image.json | 1132 ++++++ blueprints/Video Segmentation (SAM3).json | 827 ++++ blueprints/Video Stitch.json | 489 ++- 21 files changed, 21418 insertions(+), 537 deletions(-) create mode 100644 blueprints/ControlNet (Z-Image-Turbo).json create mode 100644 blueprints/First-Last-Frame to Video.json create mode 100644 blueprints/Frame Interpolation.json create mode 100644 blueprints/Get Any Video Frame.json create mode 100644 blueprints/Image Edit (Flux.2 Dev).json create mode 100644 blueprints/Image Edit (Qwen 2509).json create mode 100644 blueprints/Image Segmentation (SAM3).json create mode 100644 blueprints/Remove Background (BiRefNet).json create mode 100644 blueprints/Text to Image (Ernie Image Turbo).json create mode 100644 blueprints/Text to Image (Ernie Image).json create mode 100644 blueprints/Text to Image (Flux.2 Dev).json create mode 100644 blueprints/Text to Image (Z-Image-Base).json create mode 100644 blueprints/Text to Image.json create mode 100644 blueprints/Video Segmentation (SAM3).json diff --git a/blueprints/ControlNet (Z-Image-Turbo).json b/blueprints/ControlNet (Z-Image-Turbo).json new file mode 100644 index 000000000..fbec95a97 --- /dev/null +++ b/blueprints/ControlNet (Z-Image-Turbo).json @@ -0,0 +1,1412 @@ +{ + "revision": 0, + "last_node_id": 85, + "last_link_id": 0, + "nodes": [ + { + "id": 85, + "type": "d2e76ecf-6e84-4b8c-8913-48efc09ec1c4", + "pos": [ + 440, + 1220 + ], + "size": [ + 480, + 0 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "label": "control_image", + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": null + }, + { + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + }, + { + "label": "patch_model", + "name": "name", + "type": "COMBO", + "widget": { + "name": "name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "title": "ControlNet (Z-Image-Turbo)", + "properties": { + "proxyWidgets": [ + [ + "83", + "text" + ], + [ + "79", + "seed" + ], + [ + "74", + "unet_name" + ], + [ + "73", + "clip_name" + ], + [ + "75", + "vae_name" + ], + [ + "76", + "name" + ], + [ + "79", + "control_after_generate" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [] + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "d2e76ecf-6e84-4b8c-8913-48efc09ec1c4", + "version": 1, + "state": { + "lastGroupId": 9, + "lastNodeId": 85, + "lastLinkId": 87, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "ControlNet (Z-Image-Turbo)", + "inputNode": { + "id": -10, + "bounding": [ + -500, + 620, + 120, + 180 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1390, + 1100, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "fbbb968e-d3cf-40e4-b3ce-7abb074e5bd8", + "name": "image", + "type": "IMAGE", + "linkIds": [ + 65, + 80 + ], + "localized_name": "image", + "label": "control_image", + "pos": [ + -400, + 640 + ] + }, + { + "id": "c1b19877-5417-4580-aea1-44439c70c1dd", + "name": "text", + "type": "STRING", + "linkIds": [ + 81 + ], + "pos": [ + -400, + 660 + ] + }, + { + "id": "b5671515-bc7a-4be5-b1e7-d4f0f68907d6", + "name": "seed", + "type": "INT", + "linkIds": [ + 83 + ], + "pos": [ + -400, + 680 + ] + }, + { + "id": "2838be23-8034-4f16-87a5-d29d790e8391", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 84 + ], + "pos": [ + -400, + 700 + ] + }, + { + "id": "8a6643b5-8f78-41ff-bbc6-e87b95459706", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 85 + ], + "pos": [ + -400, + 720 + ] + }, + { + "id": "b103dc94-8ca7-456b-a809-414d7e341a1b", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 86 + ], + "pos": [ + -400, + 740 + ] + }, + { + "id": "4a7d65af-f0fd-4a5c-832a-bdc0d15b1f30", + "name": "name", + "type": "COMBO", + "linkIds": [ + 87 + ], + "label": "patch_model", + "pos": [ + -400, + 760 + ] + } + ], + "outputs": [ + { + "id": "ccb7fa39-4a3d-4eb2-8fd2-91d08fad9570", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 45 + ], + "localized_name": "IMAGE", + "pos": [ + 1410, + 1120 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 73, + "type": "CLIPLoader", + "pos": [ + 20, + 500 + ], + "size": [ + 270, + 150 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 85 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 44 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "qwen_3_4b.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/text_encoders/qwen_3_4b.safetensors", + "directory": "text_encoders" + } + ] + }, + "widgets_values": [ + "qwen_3_4b.safetensors", + "lumina2", + "default" + ] + }, + { + "id": 74, + "type": "UNETLoader", + "pos": [ + 20, + 320 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 84 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 79 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "UNETLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "z_image_turbo_bf16.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/diffusion_models/z_image_turbo_bf16.safetensors", + "directory": "diffusion_models" + } + ] + }, + "widgets_values": [ + "z_image_turbo_bf16.safetensors", + "default" + ] + }, + { + "id": 75, + "type": "VAELoader", + "pos": [ + 20, + 760 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 86 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 39, + 70 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAELoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "ae.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/vae/ae.safetensors", + "directory": "vae" + } + ] + }, + "widgets_values": [ + "ae.safetensors" + ] + }, + { + "id": 76, + "type": "ModelPatchLoader", + "pos": [ + 20, + 940 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "name", + "name": "name", + "type": "COMBO", + "widget": { + "name": "name" + }, + "link": 87 + } + ], + "outputs": [ + { + "localized_name": "MODEL_PATCH", + "name": "MODEL_PATCH", + "type": "MODEL_PATCH", + "links": [ + 74 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.51", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ModelPatchLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "Z-Image-Turbo-Fun-Controlnet-Union.safetensors", + "url": "https://huggingface.co/alibaba-pai/Z-Image-Turbo-Fun-Controlnet-Union/resolve/main/Z-Image-Turbo-Fun-Controlnet-Union.safetensors", + "directory": "model_patches" + } + ] + }, + "widgets_values": [ + "Z-Image-Turbo-Fun-Controlnet-Union.safetensors" + ] + }, + { + "id": 77, + "type": "VAEDecode", + "pos": [ + 940, + 1100 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 38 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 39 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 45 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAEDecode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 78, + "type": "ModelSamplingAuraFlow", + "pos": [ + 910, + 270 + ], + "size": [ + 290, + 110 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 69 + }, + { + "localized_name": "shift", + "name": "shift", + "type": "FLOAT", + "widget": { + "name": "shift" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 40 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ModelSamplingAuraFlow", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 3 + ] + }, + { + "id": 79, + "type": "KSampler", + "pos": [ + 910, + 430 + ], + "size": [ + 300, + 570 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 40 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 41 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 42 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 78 + }, + { + "localized_name": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": 83 + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + }, + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 38 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "KSampler", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 729703840979498, + "randomize", + 8, + 1, + "res_multistep", + "simple", + 1 + ] + }, + { + "id": 80, + "type": "ConditioningZeroOut", + "pos": [ + 610, + 830 + ], + "size": [ + 230, + 80 + ], + "flags": { + "collapsed": true + }, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 36 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 42 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ConditioningZeroOut", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 81, + "type": "QwenImageDiffsynthControlnet", + "pos": [ + 490, + 970 + ], + "size": [ + 290, + 200 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 79 + }, + { + "localized_name": "model_patch", + "name": "model_patch", + "type": "MODEL_PATCH", + "link": 74 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 70 + }, + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 65 + }, + { + "localized_name": "mask", + "name": "mask", + "shape": 7, + "type": "MASK", + "link": null + }, + { + "localized_name": "strength", + "name": "strength", + "type": "FLOAT", + "widget": { + "name": "strength" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 69 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.76", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "QwenImageDiffsynthControlnet", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1 + ] + }, + { + "id": 82, + "type": "EmptySD3LatentImage", + "pos": [ + 40, + 1200 + ], + "size": [ + 260, + 170 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 76 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 77 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 78 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "EmptySD3LatentImage", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1024, + 1024, + 1 + ] + }, + { + "id": 83, + "type": "CLIPTextEncode", + "pos": [ + 430, + 310 + ], + "size": [ + 400, + 440 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 44 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 81 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 36, + 41 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPTextEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 84, + "type": "GetImageSize", + "pos": [ + 50, + 1410 + ], + "size": [ + 230, + 120 + ], + "flags": { + "collapsed": true + }, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 80 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": [ + 76 + ] + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": [ + 77 + ] + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.76", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "GetImageSize", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + } + ], + "groups": [ + { + "id": 3, + "title": "Prompt", + "bounding": [ + 410, + 230, + 440, + 630 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 4, + "title": "Model", + "bounding": [ + -50, + 230, + 430, + 840 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 8, + "title": "Apple ControlNet", + "bounding": [ + 410, + 890, + 440, + 330 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 9, + "title": "Image Size", + "bounding": [ + -50, + 1100, + 430, + 350 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "links": [ + { + "id": 38, + "origin_id": 79, + "origin_slot": 0, + "target_id": 77, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 39, + "origin_id": 75, + "origin_slot": 0, + "target_id": 77, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 69, + "origin_id": 81, + "origin_slot": 0, + "target_id": 78, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 40, + "origin_id": 78, + "origin_slot": 0, + "target_id": 79, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 41, + "origin_id": 83, + "origin_slot": 0, + "target_id": 79, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 42, + "origin_id": 80, + "origin_slot": 0, + "target_id": 79, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 78, + "origin_id": 82, + "origin_slot": 0, + "target_id": 79, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 36, + "origin_id": 83, + "origin_slot": 0, + "target_id": 80, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 79, + "origin_id": 74, + "origin_slot": 0, + "target_id": 81, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 74, + "origin_id": 76, + "origin_slot": 0, + "target_id": 81, + "target_slot": 1, + "type": "MODEL_PATCH" + }, + { + "id": 70, + "origin_id": 75, + "origin_slot": 0, + "target_id": 81, + "target_slot": 2, + "type": "VAE" + }, + { + "id": 76, + "origin_id": 84, + "origin_slot": 0, + "target_id": 82, + "target_slot": 0, + "type": "INT" + }, + { + "id": 77, + "origin_id": 84, + "origin_slot": 1, + "target_id": 82, + "target_slot": 1, + "type": "INT" + }, + { + "id": 44, + "origin_id": 73, + "origin_slot": 0, + "target_id": 83, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 65, + "origin_id": -10, + "origin_slot": 0, + "target_id": 81, + "target_slot": 3, + "type": "IMAGE" + }, + { + "id": 80, + "origin_id": -10, + "origin_slot": 0, + "target_id": 84, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 45, + "origin_id": 77, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 81, + "origin_id": -10, + "origin_slot": 1, + "target_id": 83, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 83, + "origin_id": -10, + "origin_slot": 2, + "target_id": 79, + "target_slot": 4, + "type": "INT" + }, + { + "id": 84, + "origin_id": -10, + "origin_slot": 3, + "target_id": 74, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 85, + "origin_id": -10, + "origin_slot": 4, + "target_id": 73, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 86, + "origin_id": -10, + "origin_slot": 5, + "target_id": 75, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 87, + "origin_id": -10, + "origin_slot": 6, + "target_id": 76, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": { + "workflowRendererVersion": "LG" + }, + "category": "Image generation and editing/ControlNet", + "description": "Generates images from a text prompt and ControlNet conditioning (e.g. depth, canny) using Z-Image-Turbo." + } + ] + }, + "extra": { + "ue_links": [] + } +} \ No newline at end of file diff --git a/blueprints/Depth to Video (ltx 2.0).json b/blueprints/Depth to Video (ltx 2.0).json index bb28695a2..bd51e4476 100644 --- a/blueprints/Depth to Video (ltx 2.0).json +++ b/blueprints/Depth to Video (ltx 2.0).json @@ -4234,7 +4234,7 @@ "workflowRendererVersion": "LG" }, "category": "Video generation and editing/Depth to video", - "description": "Generates video from depth maps using LTX-2, with optional synchronized audio." + "description": "Generates depth-controlled video with LTX-2: motion and structure follow a depth-reference video alongside text prompting, optional first-frame image conditioning, with optional synchronized audio." }, { "id": "38b60539-50a7-42f9-a5fe-bdeca26272e2", diff --git a/blueprints/First-Last-Frame to Video.json b/blueprints/First-Last-Frame to Video.json new file mode 100644 index 000000000..84dfafbcd --- /dev/null +++ b/blueprints/First-Last-Frame to Video.json @@ -0,0 +1,3361 @@ +{ + "revision": 0, + "last_node_id": 227, + "last_link_id": 0, + "nodes": [ + { + "id": 227, + "type": "283e4561-61a2-4538-b960-265736eb041f", + "pos": [ + 620, + 3140 + ], + "size": [ + 540, + 0 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "label": "first_frame", + "localized_name": "input", + "name": "input", + "type": "IMAGE,MASK", + "link": null + }, + { + "label": "last_frame", + "localized_name": "input_1", + "name": "input_1", + "type": "IMAGE,MASK", + "link": null + }, + { + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "label": "width", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + }, + { + "label": "height", + "name": "value_1", + "type": "INT", + "widget": { + "name": "value_1" + }, + "link": null + }, + { + "label": "duration", + "name": "value_2", + "type": "INT", + "widget": { + "name": "value_2" + }, + "link": null + }, + { + "label": "fps", + "name": "value_3", + "type": "INT", + "widget": { + "name": "value_3" + }, + "link": null + }, + { + "name": "noise_seed", + "type": "INT", + "widget": { + "name": "noise_seed" + }, + "link": null + }, + { + "label": "ckpt_name", + "name": "ckpt_name_1", + "type": "COMBO", + "widget": { + "name": "ckpt_name_1" + }, + "link": null + }, + { + "name": "text_encoder", + "type": "COMBO", + "widget": { + "name": "text_encoder" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "VIDEO", + "name": "VIDEO", + "type": "VIDEO", + "links": [] + } + ], + "title": "First-Last-Frame to Video", + "properties": { + "proxyWidgets": [ + [ + "222", + "text" + ], + [ + "215", + "value" + ], + [ + "216", + "value" + ], + [ + "198", + "value" + ], + [ + "205", + "value" + ], + [ + "196", + "noise_seed" + ], + [ + "224", + "ckpt_name" + ], + [ + "225", + "text_encoder" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.7" + } + }, + "widgets_values": [] + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "283e4561-61a2-4538-b960-265736eb041f", + "version": 1, + "state": { + "lastGroupId": 22, + "lastNodeId": 227, + "lastLinkId": 276, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "First-Last-Frame to Video", + "inputNode": { + "id": -10, + "bounding": [ + 270, + 3100, + 120, + 240 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 3620, + 3120, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "6fe179c4-d96f-4383-b202-844f6de4922e", + "name": "input", + "type": "IMAGE,MASK", + "linkIds": [ + 251 + ], + "localized_name": "input", + "label": "first_frame", + "pos": [ + 370, + 3120 + ] + }, + { + "id": "e80df1ae-5f39-4f86-91bd-0467635e2f2d", + "name": "input_1", + "type": "IMAGE,MASK", + "linkIds": [ + 253 + ], + "localized_name": "input_1", + "label": "last_frame", + "pos": [ + 370, + 3140 + ] + }, + { + "id": "433148fa-bf73-4ab1-81d9-09e2e38ed861", + "name": "text", + "type": "STRING", + "linkIds": [ + 265 + ], + "pos": [ + 370, + 3160 + ] + }, + { + "id": "36915bc8-a6ed-4d48-8619-e0e8723228e9", + "name": "value", + "type": "INT", + "linkIds": [ + 266 + ], + "label": "width", + "pos": [ + 370, + 3180 + ] + }, + { + "id": "425a36b8-91ab-41b7-81e9-496eba064ec8", + "name": "value_1", + "type": "INT", + "linkIds": [ + 267 + ], + "label": "height", + "pos": [ + 370, + 3200 + ] + }, + { + "id": "0c9e003b-bd07-4b7d-aa6d-789e138ed161", + "name": "value_2", + "type": "INT", + "linkIds": [ + 268 + ], + "label": "duration", + "pos": [ + 370, + 3220 + ] + }, + { + "id": "581b52ff-21c5-4774-ac2a-8f69a7e09e2e", + "name": "value_3", + "type": "INT", + "linkIds": [ + 269 + ], + "label": "fps", + "pos": [ + 370, + 3240 + ] + }, + { + "id": "d03cc171-45da-4658-99aa-77252bbcf522", + "name": "noise_seed", + "type": "INT", + "linkIds": [ + 270 + ], + "pos": [ + 370, + 3260 + ] + }, + { + "id": "e68e61c8-905e-43ac-8c76-65ac52270a08", + "name": "ckpt_name_1", + "type": "COMBO", + "linkIds": [ + 272, + 275, + 276 + ], + "label": "ckpt_name", + "pos": [ + 370, + 3280 + ] + }, + { + "id": "5d065f3b-891b-499f-950b-c2df0be24536", + "name": "text_encoder", + "type": "COMBO", + "linkIds": [ + 273 + ], + "pos": [ + 370, + 3300 + ] + } + ], + "outputs": [ + { + "id": "0c8c2dc0-c67c-4bc2-9e57-6aa00db2e3a9", + "name": "VIDEO", + "type": "VIDEO", + "linkIds": [ + 252 + ], + "localized_name": "VIDEO", + "pos": [ + 3640, + 3140 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 195, + "type": "LTXVPreprocess", + "pos": [ + 1480, + 3780 + ], + "size": [ + 230, + 110 + ], + "flags": { + "collapsed": false + }, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 203 + }, + { + "localized_name": "img_compression", + "name": "img_compression", + "type": "INT", + "widget": { + "name": "img_compression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "output_image", + "name": "output_image", + "type": "IMAGE", + "links": [ + 229 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVPreprocess", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 25 + ] + }, + { + "id": 196, + "type": "RandomNoise", + "pos": [ + 1990, + 2320 + ], + "size": [ + 280, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "noise_seed", + "name": "noise_seed", + "type": "INT", + "widget": { + "name": "noise_seed" + }, + "link": 270 + } + ], + "outputs": [ + { + "localized_name": "NOISE", + "name": "NOISE", + "type": "NOISE", + "links": [ + 246 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": { + "noise_seed": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "RandomNoise", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 315253765879496, + "randomize" + ] + }, + { + "id": 197, + "type": "LTXVEmptyLatentAudio", + "pos": [ + 2090, + 3820 + ], + "size": [ + 280, + 170 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "audio_vae", + "name": "audio_vae", + "type": "VAE", + "link": 205 + }, + { + "localized_name": "frames_number", + "name": "frames_number", + "type": "INT", + "widget": { + "name": "frames_number" + }, + "link": 262 + }, + { + "localized_name": "frame_rate", + "name": "frame_rate", + "type": "INT", + "widget": { + "name": "frame_rate" + }, + "link": 207 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "Latent", + "name": "Latent", + "type": "LATENT", + "links": [ + 245 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.68", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVEmptyLatentAudio", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 97, + 25, + 1 + ] + }, + { + "id": 198, + "type": "PrimitiveInt", + "pos": [ + 760, + 3650 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 268 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 260 + ] + } + ], + "title": "Duration", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 5, + "fixed" + ] + }, + { + "id": 199, + "type": "LTXVPreprocess", + "pos": [ + 1480, + 3340 + ], + "size": [ + 230, + 110 + ], + "flags": { + "collapsed": false + }, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 210 + }, + { + "localized_name": "img_compression", + "name": "img_compression", + "type": "INT", + "widget": { + "name": "img_compression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "output_image", + "name": "output_image", + "type": "IMAGE", + "links": [ + 240 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVPreprocess", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 25 + ] + }, + { + "id": 200, + "type": "LTXVCropGuides", + "pos": [ + 2820, + 2450 + ], + "size": [ + 280, + 120 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 213 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 214 + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "link": 215 + } + ], + "outputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "links": [] + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "links": [] + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "links": [ + 211 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.8.2", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + }, + "Node name for S&R": "LTXVCropGuides", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 201, + "type": "EmptyLTXVLatentVideo", + "pos": [ + 2090, + 3580 + ], + "size": [ + 280, + 200 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 218 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 219 + }, + { + "localized_name": "length", + "name": "length", + "type": "INT", + "widget": { + "name": "length" + }, + "link": 263 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 239 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.60", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "EmptyLTXVLatentVideo", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 768, + 512, + 97, + 1 + ] + }, + { + "id": 202, + "type": "LTXVConditioning", + "pos": [ + 2090, + 3400 + ], + "size": [ + 280, + 130 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 221 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 222 + }, + { + "localized_name": "frame_rate", + "name": "frame_rate", + "type": "FLOAT", + "widget": { + "name": "frame_rate" + }, + "link": 223 + } + ], + "outputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "links": [ + 236 + ] + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "links": [ + 237 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.56", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVConditioning", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 25 + ] + }, + { + "id": 203, + "type": "GetImageSize", + "pos": [ + 1480, + 3500 + ], + "size": [ + 230, + 130 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 224 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": [ + 218 + ] + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": [ + 219 + ] + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": [] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "GetImageSize", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 204, + "type": "LTXVAddGuide", + "pos": [ + 2750, + 3700 + ], + "size": [ + 280, + 240 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 225 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 226 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 227 + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "link": 228 + }, + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 229 + }, + { + "localized_name": "frame_idx", + "name": "frame_idx", + "type": "INT", + "widget": { + "name": "frame_idx" + }, + "link": null + }, + { + "localized_name": "strength", + "name": "strength", + "type": "FLOAT", + "widget": { + "name": "strength" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "links": [ + 213, + 242 + ] + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "links": [ + 214, + 243 + ] + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "links": [ + 244 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.12.3", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVAddGuide", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + -1, + 0.7 + ] + }, + { + "id": 205, + "type": "PrimitiveInt", + "pos": [ + 760, + 3800 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 269 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 207, + 235, + 261 + ] + } + ], + "title": "Frame Rate(int)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 25, + "fixed" + ] + }, + { + "id": 206, + "type": "LTXVAddGuide", + "pos": [ + 2750, + 3430 + ], + "size": [ + 280, + 240 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 236 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 237 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 238 + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "link": 239 + }, + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 240 + }, + { + "localized_name": "frame_idx", + "name": "frame_idx", + "type": "INT", + "widget": { + "name": "frame_idx" + }, + "link": null + }, + { + "localized_name": "strength", + "name": "strength", + "type": "FLOAT", + "widget": { + "name": "strength" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "links": [ + 225 + ] + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "links": [ + 226 + ] + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "links": [ + 228 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.12.3", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVAddGuide", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + 0.7 + ] + }, + { + "id": 207, + "type": "CFGGuider", + "pos": [ + 1990, + 2500 + ], + "size": [ + 280, + 160 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 241 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 242 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 243 + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "GUIDER", + "name": "GUIDER", + "type": "GUIDER", + "links": [ + 247 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CFGGuider", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1 + ] + }, + { + "id": 208, + "type": "SamplerEulerAncestral", + "pos": [ + 1990, + 2720 + ], + "size": [ + 280, + 120 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "eta", + "name": "eta", + "type": "FLOAT", + "widget": { + "name": "eta" + }, + "link": null + }, + { + "localized_name": "s_noise", + "name": "s_noise", + "type": "FLOAT", + "widget": { + "name": "s_noise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "SAMPLER", + "name": "SAMPLER", + "type": "SAMPLER", + "links": [ + 248 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "SamplerEulerAncestral", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + 1 + ] + }, + { + "id": 209, + "type": "ManualSigmas", + "pos": [ + 1990, + 2910 + ], + "size": [ + 280, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "sigmas", + "name": "sigmas", + "type": "STRING", + "widget": { + "name": "sigmas" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "SIGMAS", + "name": "SIGMAS", + "type": "SIGMAS", + "links": [ + 249 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ManualSigmas", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "1., 0.99375, 0.9875, 0.98125, 0.975, 0.909375, 0.725, 0.421875, 0.0" + ] + }, + { + "id": 210, + "type": "LTXVConcatAVLatent", + "pos": [ + 1990, + 3090 + ], + "size": [ + 280, + 100 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "localized_name": "video_latent", + "name": "video_latent", + "type": "LATENT", + "link": 244 + }, + { + "localized_name": "audio_latent", + "name": "audio_latent", + "type": "LATENT", + "link": 245 + } + ], + "outputs": [ + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "links": [ + 250 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVConcatAVLatent", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 211, + "type": "SamplerCustomAdvanced", + "pos": [ + 2460, + 2330 + ], + "size": [ + 230, + 170 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "localized_name": "noise", + "name": "noise", + "type": "NOISE", + "link": 246 + }, + { + "localized_name": "guider", + "name": "guider", + "type": "GUIDER", + "link": 247 + }, + { + "localized_name": "sampler", + "name": "sampler", + "type": "SAMPLER", + "link": 248 + }, + { + "localized_name": "sigmas", + "name": "sigmas", + "type": "SIGMAS", + "link": 249 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 250 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "LATENT", + "links": [] + }, + { + "localized_name": "denoised_output", + "name": "denoised_output", + "type": "LATENT", + "links": [ + 204 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "SamplerCustomAdvanced", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 212, + "type": "ComfyMathExpression", + "pos": [ + 760, + 3970 + ], + "size": [ + 230, + 170 + ], + "flags": { + "collapsed": true + }, + "order": 17, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT", + "link": 235 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 223, + 234 + ] + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.17.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ComfyMathExpression", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "a" + ] + }, + { + "id": 213, + "type": "ResizeImageMaskNode", + "pos": [ + 1130, + 3340 + ], + "size": [ + 280, + 160 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "localized_name": "input", + "name": "input", + "type": "IMAGE,MASK", + "link": 251 + }, + { + "localized_name": "resize_type", + "name": "resize_type", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "resize_type" + }, + "link": null + }, + { + "localized_name": "width", + "name": "resize_type.width", + "type": "INT", + "widget": { + "name": "resize_type.width" + }, + "link": 208 + }, + { + "localized_name": "height", + "name": "resize_type.height", + "type": "INT", + "widget": { + "name": "resize_type.height" + }, + "link": 209 + }, + { + "localized_name": "crop", + "name": "resize_type.crop", + "type": "COMBO", + "widget": { + "name": "resize_type.crop" + }, + "link": null + }, + { + "localized_name": "scale_method", + "name": "scale_method", + "type": "COMBO", + "widget": { + "name": "scale_method" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "resized", + "name": "resized", + "type": "*", + "links": [ + 210, + 224 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": { + "resize_type.width": true, + "resize_type.height": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ResizeImageMaskNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "scale dimensions", + 640, + 360, + "center", + "nearest-exact" + ] + }, + { + "id": 214, + "type": "ResizeImageMaskNode", + "pos": [ + 1130, + 3780 + ], + "size": [ + 280, + 160 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "localized_name": "input", + "name": "input", + "type": "IMAGE,MASK", + "link": 253 + }, + { + "localized_name": "resize_type", + "name": "resize_type", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "resize_type" + }, + "link": null + }, + { + "localized_name": "width", + "name": "resize_type.width", + "type": "INT", + "widget": { + "name": "resize_type.width" + }, + "link": 201 + }, + { + "localized_name": "height", + "name": "resize_type.height", + "type": "INT", + "widget": { + "name": "resize_type.height" + }, + "link": 202 + }, + { + "localized_name": "crop", + "name": "resize_type.crop", + "type": "COMBO", + "widget": { + "name": "resize_type.crop" + }, + "link": null + }, + { + "localized_name": "scale_method", + "name": "scale_method", + "type": "COMBO", + "widget": { + "name": "scale_method" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "resized", + "name": "resized", + "type": "*", + "links": [ + 203 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": { + "resize_type.width": true, + "resize_type.height": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ResizeImageMaskNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "scale dimensions", + 640, + 360, + "center", + "nearest-exact" + ] + }, + { + "id": 215, + "type": "PrimitiveInt", + "pos": [ + 760, + 3340 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 20, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 266 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 201, + 208 + ] + } + ], + "title": "Width", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1280, + "fixed" + ] + }, + { + "id": 216, + "type": "PrimitiveInt", + "pos": [ + 760, + 3490 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 21, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 267 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 202, + 209 + ] + } + ], + "title": "height", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 720, + "fixed" + ] + }, + { + "id": 217, + "type": "CLIPTextEncode", + "pos": [ + 1320, + 2870 + ], + "size": [ + 590, + 200 + ], + "flags": { + "collapsed": false + }, + "order": 22, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 230 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 222 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.56", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPTextEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "blurry, out of focus, overexposed, underexposed, low contrast, washed out colors, excessive noise, grainy texture, poor lighting, flickering, motion blur, distorted proportions, unnatural skin tones, deformed facial features, asymmetrical face, missing facial features, extra limbs, disfigured hands, wrong hand count, artifacts around text, unreadable text on shirt or hat, incorrect lettering on cap (“PNTR”), incorrect t-shirt slogan (“JUST DO IT”), missing microphone, misplaced microphone, inconsistent perspective, camera shake, incorrect depth of field, background too sharp, background clutter, distracting reflections, harsh shadows, inconsistent lighting direction, color banding, cartoonish rendering, 3D CGI look, unrealistic materials, uncanny valley effect, incorrect ethnicity, wrong gender, exaggerated expressions, smiling, laughing, exaggerated sadness, wrong gaze direction, eyes looking at camera, mismatched lip sync, silent or muted audio, distorted voice, robotic voice, echo, background noise, off-sync audio, missing sniff sounds, incorrect dialogue, added dialogue, repetitive speech, jittery movement, awkward pauses, incorrect timing, unnatural transitions, inconsistent framing, tilted camera, missing door or shelves, missing shallow depth of field, flat lighting, inconsistent tone, cinematic oversaturation, stylized filters, or AI artifacts." + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 218, + "type": "CreateVideo", + "pos": [ + 3280, + 2320 + ], + "size": [ + 280, + 130 + ], + "flags": {}, + "order": 23, + "mode": 0, + "inputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 232 + }, + { + "localized_name": "audio", + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": 233 + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "widget": { + "name": "fps" + }, + "link": 234 + } + ], + "outputs": [ + { + "localized_name": "VIDEO", + "name": "VIDEO", + "type": "VIDEO", + "links": [ + 252 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CreateVideo", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 24 + ] + }, + { + "id": 219, + "type": "VAEDecodeTiled", + "pos": [ + 2820, + 2630 + ], + "size": [ + 280, + 200 + ], + "flags": {}, + "order": 24, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 211 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 212 + }, + { + "localized_name": "tile_size", + "name": "tile_size", + "type": "INT", + "widget": { + "name": "tile_size" + }, + "link": null + }, + { + "localized_name": "overlap", + "name": "overlap", + "type": "INT", + "widget": { + "name": "overlap" + }, + "link": null + }, + { + "localized_name": "temporal_size", + "name": "temporal_size", + "type": "INT", + "widget": { + "name": "temporal_size" + }, + "link": null + }, + { + "localized_name": "temporal_overlap", + "name": "temporal_overlap", + "type": "INT", + "widget": { + "name": "temporal_overlap" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 232 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAEDecodeTiled", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 768, + 64, + 4096, + 64 + ] + }, + { + "id": 220, + "type": "LTXVAudioVAEDecode", + "pos": [ + 2820, + 2920 + ], + "size": [ + 280, + 100 + ], + "flags": {}, + "order": 25, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 216 + }, + { + "label": "Audio VAE", + "localized_name": "audio_vae", + "name": "audio_vae", + "type": "VAE", + "link": 217 + } + ], + "outputs": [ + { + "localized_name": "Audio", + "name": "Audio", + "type": "AUDIO", + "links": [ + 233 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVAudioVAEDecode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 221, + "type": "LTXVSeparateAVLatent", + "pos": [ + 2460, + 2580 + ], + "size": [ + 250, + 100 + ], + "flags": {}, + "order": 26, + "mode": 0, + "inputs": [ + { + "localized_name": "av_latent", + "name": "av_latent", + "type": "LATENT", + "link": 204 + } + ], + "outputs": [ + { + "localized_name": "video_latent", + "name": "video_latent", + "type": "LATENT", + "links": [ + 215 + ] + }, + { + "localized_name": "audio_latent", + "name": "audio_latent", + "type": "LATENT", + "links": [ + 216 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.5.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVSeparateAVLatent", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 222, + "type": "CLIPTextEncode", + "pos": [ + 1310, + 2380 + ], + "size": [ + 620, + 420 + ], + "flags": {}, + "order": 27, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 231 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 265 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 221 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.56", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPTextEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 223, + "type": "CheckpointLoaderSimple", + "pos": [ + 770, + 2380 + ], + "size": [ + 420, + 160 + ], + "flags": {}, + "order": 28, + "mode": 0, + "inputs": [ + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 276 + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 241 + ] + }, + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [] + }, + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 212, + 227, + 238 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.10.0", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + }, + "Node name for S&R": "CheckpointLoaderSimple", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "ltx-2.3-22b-distilled-fp8.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-distilled-fp8.safetensors", + "directory": "checkpoints" + } + ] + }, + "widgets_values": [ + "ltx-2.3-22b-distilled-fp8.safetensors" + ] + }, + { + "id": 224, + "type": "LTXVAudioVAELoader", + "pos": [ + 770, + 2660 + ], + "size": [ + 420, + 110 + ], + "flags": {}, + "order": 29, + "mode": 0, + "inputs": [ + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 272 + } + ], + "outputs": [ + { + "localized_name": "Audio VAE", + "name": "Audio VAE", + "type": "VAE", + "links": [ + 205, + 217 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.10.0", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + }, + "Node name for S&R": "LTXVAudioVAELoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "ltx-2.3-22b-distilled-fp8.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-distilled-fp8.safetensors", + "directory": "checkpoints" + } + ] + }, + "widgets_values": [ + "ltx-2.3-22b-distilled-fp8.safetensors" + ] + }, + { + "id": 225, + "type": "LTXAVTextEncoderLoader", + "pos": [ + 770, + 2890 + ], + "size": [ + 410, + 160 + ], + "flags": {}, + "order": 30, + "mode": 0, + "inputs": [ + { + "localized_name": "text_encoder", + "name": "text_encoder", + "type": "COMBO", + "widget": { + "name": "text_encoder" + }, + "link": 273 + }, + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 275 + }, + { + "localized_name": "device", + "name": "device", + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 230, + 231 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.10.0", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + }, + "Node name for S&R": "LTXAVTextEncoderLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "gemma_3_12B_it_fp4_mixed.safetensors", + "url": "https://huggingface.co/Comfy-Org/ltx-2/resolve/main/split_files/text_encoders/gemma_3_12B_it_fp4_mixed.safetensors", + "directory": "text_encoders" + }, + { + "name": "ltx-2.3-22b-distilled-fp8.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-distilled-fp8.safetensors", + "directory": "checkpoints" + } + ] + }, + "widgets_values": [ + "gemma_3_12B_it_fp4_mixed.safetensors", + "ltx-2.3-22b-distilled-fp8.safetensors", + "default" + ] + }, + { + "id": 226, + "type": "ComfyMathExpression", + "pos": [ + 760, + 4020 + ], + "size": [ + 400, + 200 + ], + "flags": { + "collapsed": true + }, + "order": 31, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT", + "link": 260 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT", + "link": 261 + }, + { + "label": "c", + "localized_name": "values.c", + "name": "values.c", + "shape": 7, + "type": "FLOAT,INT", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": null + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 262, + 263 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.7" + }, + "Node name for S&R": "ComfyMathExpression" + }, + "widgets_values": [ + "a * b + 1" + ] + } + ], + "groups": [ + { + "id": 1, + "title": "Conditioning", + "bounding": [ + 1850, + 3250, + 1370, + 800 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 2, + "title": "Settings", + "bounding": [ + 730, + 3250, + 290, + 800 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 3, + "title": "FIrst Frame", + "bounding": [ + 1050, + 3250, + 770, + 400 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 4, + "title": "Last Frame", + "bounding": [ + 1050, + 3680, + 770, + 370 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 5, + "title": "Model", + "bounding": [ + 730, + 2240, + 500, + 980 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 6, + "title": "Prompt", + "bounding": [ + 1260, + 2240, + 680, + 980 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 7, + "title": "Sampling", + "bounding": [ + 1970, + 2240, + 770, + 980 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 8, + "title": "Decoding", + "bounding": [ + 2770, + 2240, + 450, + 980 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "links": [ + { + "id": 203, + "origin_id": 214, + "origin_slot": 0, + "target_id": 195, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 205, + "origin_id": 224, + "origin_slot": 0, + "target_id": 197, + "target_slot": 0, + "type": "VAE" + }, + { + "id": 207, + "origin_id": 205, + "origin_slot": 0, + "target_id": 197, + "target_slot": 2, + "type": "INT" + }, + { + "id": 210, + "origin_id": 213, + "origin_slot": 0, + "target_id": 199, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 213, + "origin_id": 204, + "origin_slot": 0, + "target_id": 200, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 214, + "origin_id": 204, + "origin_slot": 1, + "target_id": 200, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 215, + "origin_id": 221, + "origin_slot": 0, + "target_id": 200, + "target_slot": 2, + "type": "LATENT" + }, + { + "id": 218, + "origin_id": 203, + "origin_slot": 0, + "target_id": 201, + "target_slot": 0, + "type": "INT" + }, + { + "id": 219, + "origin_id": 203, + "origin_slot": 1, + "target_id": 201, + "target_slot": 1, + "type": "INT" + }, + { + "id": 221, + "origin_id": 222, + "origin_slot": 0, + "target_id": 202, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 222, + "origin_id": 217, + "origin_slot": 0, + "target_id": 202, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 223, + "origin_id": 212, + "origin_slot": 0, + "target_id": 202, + "target_slot": 2, + "type": "FLOAT" + }, + { + "id": 224, + "origin_id": 213, + "origin_slot": 0, + "target_id": 203, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 225, + "origin_id": 206, + "origin_slot": 0, + "target_id": 204, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 226, + "origin_id": 206, + "origin_slot": 1, + "target_id": 204, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 227, + "origin_id": 223, + "origin_slot": 2, + "target_id": 204, + "target_slot": 2, + "type": "VAE" + }, + { + "id": 228, + "origin_id": 206, + "origin_slot": 2, + "target_id": 204, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 229, + "origin_id": 195, + "origin_slot": 0, + "target_id": 204, + "target_slot": 4, + "type": "IMAGE" + }, + { + "id": 236, + "origin_id": 202, + "origin_slot": 0, + "target_id": 206, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 237, + "origin_id": 202, + "origin_slot": 1, + "target_id": 206, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 238, + "origin_id": 223, + "origin_slot": 2, + "target_id": 206, + "target_slot": 2, + "type": "VAE" + }, + { + "id": 239, + "origin_id": 201, + "origin_slot": 0, + "target_id": 206, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 240, + "origin_id": 199, + "origin_slot": 0, + "target_id": 206, + "target_slot": 4, + "type": "IMAGE" + }, + { + "id": 241, + "origin_id": 223, + "origin_slot": 0, + "target_id": 207, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 242, + "origin_id": 204, + "origin_slot": 0, + "target_id": 207, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 243, + "origin_id": 204, + "origin_slot": 1, + "target_id": 207, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 244, + "origin_id": 204, + "origin_slot": 2, + "target_id": 210, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 245, + "origin_id": 197, + "origin_slot": 0, + "target_id": 210, + "target_slot": 1, + "type": "LATENT" + }, + { + "id": 246, + "origin_id": 196, + "origin_slot": 0, + "target_id": 211, + "target_slot": 0, + "type": "NOISE" + }, + { + "id": 247, + "origin_id": 207, + "origin_slot": 0, + "target_id": 211, + "target_slot": 1, + "type": "GUIDER" + }, + { + "id": 248, + "origin_id": 208, + "origin_slot": 0, + "target_id": 211, + "target_slot": 2, + "type": "SAMPLER" + }, + { + "id": 249, + "origin_id": 209, + "origin_slot": 0, + "target_id": 211, + "target_slot": 3, + "type": "SIGMAS" + }, + { + "id": 250, + "origin_id": 210, + "origin_slot": 0, + "target_id": 211, + "target_slot": 4, + "type": "LATENT" + }, + { + "id": 235, + "origin_id": 205, + "origin_slot": 0, + "target_id": 212, + "target_slot": 0, + "type": "INT" + }, + { + "id": 208, + "origin_id": 215, + "origin_slot": 0, + "target_id": 213, + "target_slot": 2, + "type": "INT" + }, + { + "id": 209, + "origin_id": 216, + "origin_slot": 0, + "target_id": 213, + "target_slot": 3, + "type": "INT" + }, + { + "id": 201, + "origin_id": 215, + "origin_slot": 0, + "target_id": 214, + "target_slot": 2, + "type": "INT" + }, + { + "id": 202, + "origin_id": 216, + "origin_slot": 0, + "target_id": 214, + "target_slot": 3, + "type": "INT" + }, + { + "id": 230, + "origin_id": 225, + "origin_slot": 0, + "target_id": 217, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 232, + "origin_id": 219, + "origin_slot": 0, + "target_id": 218, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 233, + "origin_id": 220, + "origin_slot": 0, + "target_id": 218, + "target_slot": 1, + "type": "AUDIO" + }, + { + "id": 234, + "origin_id": 212, + "origin_slot": 0, + "target_id": 218, + "target_slot": 2, + "type": "FLOAT" + }, + { + "id": 211, + "origin_id": 200, + "origin_slot": 2, + "target_id": 219, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 212, + "origin_id": 223, + "origin_slot": 2, + "target_id": 219, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 216, + "origin_id": 221, + "origin_slot": 1, + "target_id": 220, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 217, + "origin_id": 224, + "origin_slot": 0, + "target_id": 220, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 204, + "origin_id": 211, + "origin_slot": 1, + "target_id": 221, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 231, + "origin_id": 225, + "origin_slot": 0, + "target_id": 222, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 251, + "origin_id": -10, + "origin_slot": 0, + "target_id": 213, + "target_slot": 0, + "type": "IMAGE,MASK" + }, + { + "id": 253, + "origin_id": -10, + "origin_slot": 1, + "target_id": 214, + "target_slot": 0, + "type": "IMAGE,MASK" + }, + { + "id": 252, + "origin_id": 218, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 260, + "origin_id": 198, + "origin_slot": 0, + "target_id": 226, + "target_slot": 0, + "type": "INT" + }, + { + "id": 261, + "origin_id": 205, + "origin_slot": 0, + "target_id": 226, + "target_slot": 1, + "type": "INT" + }, + { + "id": 262, + "origin_id": 226, + "origin_slot": 1, + "target_id": 197, + "target_slot": 1, + "type": "INT" + }, + { + "id": 263, + "origin_id": 226, + "origin_slot": 1, + "target_id": 201, + "target_slot": 2, + "type": "INT" + }, + { + "id": 265, + "origin_id": -10, + "origin_slot": 2, + "target_id": 222, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 266, + "origin_id": -10, + "origin_slot": 3, + "target_id": 215, + "target_slot": 0, + "type": "INT" + }, + { + "id": 267, + "origin_id": -10, + "origin_slot": 4, + "target_id": 216, + "target_slot": 0, + "type": "INT" + }, + { + "id": 268, + "origin_id": -10, + "origin_slot": 5, + "target_id": 198, + "target_slot": 0, + "type": "INT" + }, + { + "id": 269, + "origin_id": -10, + "origin_slot": 6, + "target_id": 205, + "target_slot": 0, + "type": "INT" + }, + { + "id": 270, + "origin_id": -10, + "origin_slot": 7, + "target_id": 196, + "target_slot": 0, + "type": "INT" + }, + { + "id": 272, + "origin_id": -10, + "origin_slot": 8, + "target_id": 224, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 273, + "origin_id": -10, + "origin_slot": 9, + "target_id": 225, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 275, + "origin_id": -10, + "origin_slot": 8, + "target_id": 225, + "target_slot": 1, + "type": "COMBO" + }, + { + "id": 276, + "origin_id": -10, + "origin_slot": 8, + "target_id": 223, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": {}, + "category": "Video generation and editing/First-Last-Frame to Video", + "description": "Generates a video that interpolates between the first and last keyframes using LTX-2.3, including optional audio." + } + ] + }, + "extra": { + "ue_links": [] + } +} \ No newline at end of file diff --git a/blueprints/Frame Interpolation.json b/blueprints/Frame Interpolation.json new file mode 100644 index 000000000..8e183de7e --- /dev/null +++ b/blueprints/Frame Interpolation.json @@ -0,0 +1,858 @@ +{ + "revision": 0, + "last_node_id": 16, + "last_link_id": 0, + "nodes": [ + { + "id": 16, + "type": "022693be-2baa-4009-870a-28921508a7ef", + "pos": [ + -2990, + -3240 + ], + "size": [ + 410, + 200 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": null + }, + { + "label": "multiplier", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + }, + { + "label": "enable_fps_multiplier", + "name": "value_1", + "type": "BOOLEAN", + "widget": { + "name": "value_1" + }, + "link": null + }, + { + "name": "model_name", + "type": "COMBO", + "widget": { + "name": "model_name" + }, + "link": null + } + ], + "outputs": [ + { + "label": "VIDEO", + "name": "VIDEO_1", + "type": "VIDEO", + "links": [] + }, + { + "name": "IMAGE", + "type": "IMAGE", + "links": null + } + ], + "properties": { + "proxyWidgets": [ + [ + "9", + "value" + ], + [ + "13", + "value" + ], + [ + "1", + "model_name" + ] + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + }, + "widgets_values": [], + "title": "Frame Interpolation" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "022693be-2baa-4009-870a-28921508a7ef", + "version": 1, + "state": { + "lastGroupId": 0, + "lastNodeId": 17, + "lastLinkId": 28, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Frame Interpolation", + "inputNode": { + "id": -10, + "bounding": [ + -2810, + -3070, + 159.7421875, + 120 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -1270, + -3075, + 120, + 80 + ] + }, + "inputs": [ + { + "id": "05e31c51-dcb6-4a1e-9651-1b9ad4f7a287", + "name": "video", + "type": "VIDEO", + "linkIds": [ + 2 + ], + "localized_name": "video", + "pos": [ + -2670.2578125, + -3050 + ] + }, + { + "id": "feecb409-7d1c-4a99-9c63-50c5fecdd3c9", + "name": "value", + "type": "INT", + "linkIds": [ + 22 + ], + "label": "multiplier", + "pos": [ + -2670.2578125, + -3030 + ] + }, + { + "id": "0b8a861b-b581-4068-9e8c-f8d15daf1ca6", + "name": "value_1", + "type": "BOOLEAN", + "linkIds": [ + 23 + ], + "label": "enable_fps_multiplier", + "pos": [ + -2670.2578125, + -3010 + ] + }, + { + "id": "a22b101e-8773-4e17-a297-7ee3aae09162", + "name": "model_name", + "type": "COMBO", + "linkIds": [ + 24 + ], + "pos": [ + -2670.2578125, + -2990 + ] + } + ], + "outputs": [ + { + "id": "ef2ada05-d5aa-492a-9394-6c3e71e39ebb", + "name": "VIDEO_1", + "type": "VIDEO", + "linkIds": [ + 26 + ], + "label": "VIDEO", + "pos": [ + -1250, + -3055 + ] + }, + { + "id": "5aacc622-2a07-4983-b31c-e04461f7f953", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 28 + ], + "pos": [ + -1250, + -3035 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 1, + "type": "FrameInterpolationModelLoader", + "pos": [ + -2510, + -3370 + ], + "size": [ + 370, + 90 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "model_name", + "name": "model_name", + "type": "COMBO", + "widget": { + "name": "model_name" + }, + "link": 24 + } + ], + "outputs": [ + { + "localized_name": "INTERP_MODEL", + "name": "INTERP_MODEL", + "type": "INTERP_MODEL", + "links": [ + 1 + ] + } + ], + "properties": { + "Node name for S&R": "FrameInterpolationModelLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3", + "models": [ + { + "name": "film_net_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/frame_interpolation/resolve/main/frame_interpolation/film_net_fp16.safetensors", + "directory": "frame_interpolation" + } + ] + }, + "widgets_values": [ + "film_net_fp16.safetensors" + ] + }, + { + "id": 2, + "type": "FrameInterpolate", + "pos": [ + -2040, + -3370 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "interp_model", + "name": "interp_model", + "type": "INTERP_MODEL", + "link": 1 + }, + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 3 + }, + { + "localized_name": "multiplier", + "name": "multiplier", + "type": "INT", + "widget": { + "name": "multiplier" + }, + "link": 8 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 4, + 28 + ] + } + ], + "properties": { + "Node name for S&R": "FrameInterpolate", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + }, + "widgets_values": [ + 2 + ] + }, + { + "id": 5, + "type": "CreateVideo", + "pos": [ + -1600, + -3370 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 4 + }, + { + "localized_name": "audio", + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": 5 + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "widget": { + "name": "fps" + }, + "link": 12 + } + ], + "outputs": [ + { + "localized_name": "VIDEO", + "name": "VIDEO", + "type": "VIDEO", + "links": [ + 26 + ] + } + ], + "properties": { + "Node name for S&R": "CreateVideo", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + }, + "widgets_values": [ + 30 + ] + }, + { + "id": 9, + "type": "PrimitiveInt", + "pos": [ + -2500, + -2970 + ], + "size": [ + 270, + 90 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 22 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 8, + 19 + ] + } + ], + "title": "Int (Multiplier)", + "properties": { + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + }, + "widgets_values": [ + 2, + "fixed" + ] + }, + { + "id": 10, + "type": "ComfySwitchNode", + "pos": [ + -1610, + -3120 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 11 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 13 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 15 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 12 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + }, + "widgets_values": [ + true + ] + }, + { + "id": 13, + "type": "PrimitiveBoolean", + "pos": [ + -2500, + -2770 + ], + "size": [ + 310, + 90 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 23 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 15 + ] + } + ], + "title": "Boolean (Apply multiplier to FPS?)", + "properties": { + "Node name for S&R": "PrimitiveBoolean", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + }, + "widgets_values": [ + true + ] + }, + { + "id": 3, + "type": "GetVideoComponents", + "pos": [ + -2500, + -3170 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": 2 + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 3 + ] + }, + { + "localized_name": "audio", + "name": "audio", + "type": "AUDIO", + "links": [ + 5 + ] + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "links": [ + 11, + 18 + ] + } + ], + "properties": { + "Node name for S&R": "GetVideoComponents", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + } + }, + { + "id": 11, + "type": "ComfyMathExpression", + "pos": [ + -2090, + -3070 + ], + "size": [ + 400, + 210 + ], + "flags": { + "collapsed": false + }, + "order": 6, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT", + "link": 18 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT", + "link": 19 + }, + { + "label": "c", + "localized_name": "values.c", + "name": "values.c", + "shape": 7, + "type": "FLOAT,INT", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 13 + ] + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": null + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + }, + "widgets_values": [ + "min(abs(b), 16) * a" + ] + } + ], + "groups": [], + "links": [ + { + "id": 1, + "origin_id": 1, + "origin_slot": 0, + "target_id": 2, + "target_slot": 0, + "type": "INTERP_MODEL" + }, + { + "id": 3, + "origin_id": 3, + "origin_slot": 0, + "target_id": 2, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 8, + "origin_id": 9, + "origin_slot": 0, + "target_id": 2, + "target_slot": 2, + "type": "INT" + }, + { + "id": 4, + "origin_id": 2, + "origin_slot": 0, + "target_id": 5, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 5, + "origin_id": 3, + "origin_slot": 1, + "target_id": 5, + "target_slot": 1, + "type": "AUDIO" + }, + { + "id": 12, + "origin_id": 10, + "origin_slot": 0, + "target_id": 5, + "target_slot": 2, + "type": "FLOAT" + }, + { + "id": 11, + "origin_id": 3, + "origin_slot": 2, + "target_id": 10, + "target_slot": 0, + "type": "FLOAT" + }, + { + "id": 13, + "origin_id": 11, + "origin_slot": 0, + "target_id": 10, + "target_slot": 1, + "type": "FLOAT" + }, + { + "id": 15, + "origin_id": 13, + "origin_slot": 0, + "target_id": 10, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 18, + "origin_id": 3, + "origin_slot": 2, + "target_id": 11, + "target_slot": 0, + "type": "FLOAT" + }, + { + "id": 19, + "origin_id": 9, + "origin_slot": 0, + "target_id": 11, + "target_slot": 1, + "type": "INT" + }, + { + "id": 2, + "origin_id": -10, + "origin_slot": 0, + "target_id": 3, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 22, + "origin_id": -10, + "origin_slot": 1, + "target_id": 9, + "target_slot": 0, + "type": "INT" + }, + { + "id": 23, + "origin_id": -10, + "origin_slot": 2, + "target_id": 13, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 24, + "origin_id": -10, + "origin_slot": 3, + "target_id": 1, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 26, + "origin_id": 5, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 28, + "origin_id": 2, + "origin_slot": 0, + "target_id": -20, + "target_slot": 1, + "type": "IMAGE" + } + ], + "extra": {}, + "category": "Video Tools", + "description": "Increases video frame rate by synthesizing intermediate frames with a frame interpolation model." + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Get Any Video Frame.json b/blueprints/Get Any Video Frame.json new file mode 100644 index 000000000..9ff0f8e6e --- /dev/null +++ b/blueprints/Get Any Video Frame.json @@ -0,0 +1,485 @@ +{ + "revision": 0, + "last_node_id": 98, + "last_link_id": 0, + "nodes": [ + { + "id": 98, + "type": "dca6e78d-fb06-421e-97f7-6ce17a665260", + "pos": [ + -410, + -2230 + ], + "size": [ + 270, + 104 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "video", + "type": "VIDEO", + "link": null + }, + { + "label": "frame_index", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "title": "Get Any Video Frame", + "properties": { + "proxyWidgets": [ + [ + "100", + "value" + ] + ] + }, + "widgets_values": [] + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "dca6e78d-fb06-421e-97f7-6ce17a665260", + "version": 1, + "state": { + "lastGroupId": 1, + "lastNodeId": 136, + "lastLinkId": 302, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Get Any Video Frame", + "inputNode": { + "id": -10, + "bounding": [ + 380, + -57, + 120, + 80 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1460, + -57, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "2ceec378-8dcf-4340-8570-155967f59a93", + "name": "video", + "type": "VIDEO", + "linkIds": [ + 4 + ], + "pos": [ + 480, + -37 + ] + }, + { + "id": "819955f6-c686-4896-8032-ff2d0059109a", + "name": "value", + "type": "INT", + "linkIds": [ + 283 + ], + "label": "frame_index", + "pos": [ + 480, + -17 + ] + } + ], + "outputs": [ + { + "id": "1ab0684d-6a44-45b6-8aa4-a0b971a1d41e", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 5 + ], + "pos": [ + 1480, + -37 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 1, + "type": "GetVideoComponents", + "pos": [ + 560, + -150 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": 4 + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 1, + 2 + ] + }, + { + "localized_name": "audio", + "name": "audio", + "type": "AUDIO", + "links": null + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "links": null + } + ], + "properties": { + "Node name for S&R": "GetVideoComponents" + } + }, + { + "id": 2, + "type": "GetImageSize", + "pos": [ + 560, + 50 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 1 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": null + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": null + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": [ + 285 + ] + } + ], + "properties": { + "Node name for S&R": "GetImageSize" + } + }, + { + "id": 3, + "type": "ImageFromBatch", + "pos": [ + 1130, + -150 + ], + "size": [ + 270, + 140 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 2 + }, + { + "localized_name": "batch_index", + "name": "batch_index", + "type": "INT", + "widget": { + "name": "batch_index" + }, + "link": 286 + }, + { + "localized_name": "length", + "name": "length", + "type": "INT", + "widget": { + "name": "length" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 5 + ] + } + ], + "properties": { + "Node name for S&R": "ImageFromBatch" + }, + "widgets_values": [ + 0, + 1 + ] + }, + { + "id": 99, + "type": "ComfyMathExpression", + "pos": [ + 910, + 100 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT", + "link": 284 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT", + "link": 285 + }, + { + "label": "c", + "localized_name": "values.c", + "name": "values.c", + "shape": 7, + "type": "FLOAT,INT", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": null + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 286 + ] + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression" + }, + "widgets_values": [ + "min(max(int(a if a >= 0 else b + a), 0), b - 1)" + ] + }, + { + "id": 100, + "type": "PrimitiveInt", + "pos": [ + 560, + 250 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 283 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 284 + ] + } + ], + "properties": { + "Node name for S&R": "PrimitiveInt" + }, + "widgets_values": [ + 0, + "fixed" + ] + } + ], + "groups": [], + "links": [ + { + "id": 1, + "origin_id": 1, + "origin_slot": 0, + "target_id": 2, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 2, + "origin_id": 1, + "origin_slot": 0, + "target_id": 3, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 4, + "origin_id": -10, + "origin_slot": 0, + "target_id": 1, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 5, + "origin_id": 3, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 283, + "origin_id": -10, + "origin_slot": 1, + "target_id": 100, + "target_slot": 0, + "type": "INT" + }, + { + "id": 284, + "origin_id": 100, + "origin_slot": 0, + "target_id": 99, + "target_slot": 0, + "type": "INT" + }, + { + "id": 285, + "origin_id": 2, + "origin_slot": 2, + "target_id": 99, + "target_slot": 1, + "type": "INT" + }, + { + "id": 286, + "origin_id": 99, + "origin_slot": 1, + "target_id": 3, + "target_slot": 1, + "type": "INT" + } + ], + "extra": {}, + "category": "Video Tools", + "description": "Extracts one image frame from a video at a chosen index, with optional trim and FPS control." + } + ] + }, + "extra": { + "ds": { + "scale": 1.197015527856339, + "offset": [ + -168.76833554248222, + 540.6638955283997 + ] + }, + "frontendVersion": "1.42.8" + } +} \ No newline at end of file diff --git a/blueprints/Image Edit (FireRed Image Edit 1.1).json b/blueprints/Image Edit (FireRed Image Edit 1.1).json index 14310353c..b82c7d18b 100644 --- a/blueprints/Image Edit (FireRed Image Edit 1.1).json +++ b/blueprints/Image Edit (FireRed Image Edit 1.1).json @@ -1,18 +1,18 @@ { "revision": 0, - "last_node_id": 172, + "last_node_id": 213, "last_link_id": 0, "nodes": [ { - "id": 172, - "type": "edf73971-14ee-4d39-b58e-46ce2a89d3d0", + "id": 213, + "type": "e35fbbeb-d7b1-46d1-a74e-959517d0fb1a", "pos": [ - 30, - 200 + -700, + -470 ], "size": [ 500, - 570 + 0 ], "flags": {}, "order": 2, @@ -105,44 +105,44 @@ "properties": { "proxyWidgets": [ [ - "118", + "208", "prompt" ], [ - "153", + "207", "value" ], [ - "130", + "210", "seed" ], [ - "128", + "205", "unet_name" ], [ - "115", + "203", "clip_name" ], [ - "116", + "202", "vae_name" ], [ - "151", + "204", "lora_name" ], [ - "130", + "210", "control_after_generate" ] ], + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -160,12 +160,12 @@ "definitions": { "subgraphs": [ { - "id": "edf73971-14ee-4d39-b58e-46ce2a89d3d0", + "id": "e35fbbeb-d7b1-46d1-a74e-959517d0fb1a", "version": 1, "state": { "lastGroupId": 8, - "lastNodeId": 174, - "lastLinkId": 376, + "lastNodeId": 213, + "lastLinkId": 378, "lastRerouteId": 0 }, "revision": 0, @@ -183,8 +183,8 @@ "outputNode": { "id": -20, "bounding": [ - 1147.5, - -1215, + 1860, + -1340, 120, 60 ] @@ -327,26 +327,26 @@ ], "localized_name": "IMAGE", "pos": [ - 1167.5, - -1195 + 1880, + -1320 ] } ], "widgets": [], "nodes": [ { - "id": 120, + "id": 193, "type": "ModelSamplingAuraFlow", "pos": [ - 1060, - -1760 + 1010, + -1680 ], "size": [ 290, 110 ], "flags": {}, - "order": 8, + "order": 4, "mode": 0, "inputs": [ { @@ -376,13 +376,13 @@ } ], "properties": { + "Node name for S&R": "ModelSamplingAuraFlow", + "cnr_id": "comfy-core", + "ver": "0.5.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.5.1", - "Node name for S&R": "ModelSamplingAuraFlow", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -396,7 +396,7 @@ ] }, { - "id": 154, + "id": 194, "type": "ComfySwitchNode", "pos": [ 680, @@ -407,7 +407,7 @@ 140 ], "flags": {}, - "order": 16, + "order": 5, "mode": 0, "inputs": [ { @@ -444,13 +444,13 @@ ], "title": "Switch (Model)", "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "ComfySwitchNode", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -464,7 +464,7 @@ ] }, { - "id": 155, + "id": 195, "type": "PrimitiveInt", "pos": [ 190, @@ -500,13 +500,13 @@ ], "title": "Int (Steps)", "properties": { + "Node name for S&R": "PrimitiveInt", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "PrimitiveInt", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -521,18 +521,18 @@ ] }, { - "id": 123, + "id": 196, "type": "CFGNorm", "pos": [ - 1060, - -1590 + 1010, + -1510 ], "size": [ 290, 110 ], "flags": {}, - "order": 9, + "order": 6, "mode": 0, "inputs": [ { @@ -562,13 +562,13 @@ } ], "properties": { + "Node name for S&R": "CFGNorm", + "cnr_id": "comfy-core", + "ver": "0.5.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.5.1", - "Node name for S&R": "CFGNorm", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -582,7 +582,7 @@ ] }, { - "id": 164, + "id": 197, "type": "ComfySwitchNode", "pos": [ 680, @@ -593,7 +593,7 @@ 130 ], "flags": {}, - "order": 18, + "order": 7, "mode": 0, "inputs": [ { @@ -630,13 +630,13 @@ ], "title": "Switch (CFG)", "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "ComfySwitchNode", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -650,7 +650,7 @@ ] }, { - "id": 156, + "id": 198, "type": "PrimitiveInt", "pos": [ 190, @@ -686,13 +686,13 @@ ], "title": "Float (Steps)", "properties": { + "Node name for S&R": "PrimitiveInt", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "PrimitiveInt", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -707,7 +707,7 @@ ] }, { - "id": 162, + "id": 199, "type": "PrimitiveFloat", "pos": [ 190, @@ -743,13 +743,13 @@ ], "title": "Float (CFG)", "properties": { + "Node name for S&R": "PrimitiveFloat", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "PrimitiveFloat", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -763,7 +763,7 @@ ] }, { - "id": 163, + "id": 200, "type": "PrimitiveFloat", "pos": [ 190, @@ -799,13 +799,13 @@ ], "title": "Float (CFG)", "properties": { + "Node name for S&R": "PrimitiveFloat", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "PrimitiveFloat", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -819,7 +819,7 @@ ] }, { - "id": 157, + "id": 201, "type": "ComfySwitchNode", "pos": [ 680, @@ -830,7 +830,7 @@ 130 ], "flags": {}, - "order": 17, + "order": 8, "mode": 0, "inputs": [ { @@ -867,13 +867,13 @@ ], "title": "Switch (Steps)", "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "ComfySwitchNode", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -887,11 +887,11 @@ ] }, { - "id": 116, + "id": 202, "type": "VAELoader", "pos": [ - -950, - -1040 + -960, + -1100 ], "size": [ 400, @@ -900,7 +900,7 @@ "flags": { "collapsed": false }, - "order": 5, + "order": 9, "mode": 0, "inputs": [ { @@ -928,45 +928,45 @@ } ], "properties": { - "ue_properties": { - "widget_ue_connectable": {}, - "input_ue_unconnectable": {} - }, + "Node name for S&R": "VAELoader", "cnr_id": "comfy-core", "ver": "0.5.1", - "Node name for S&R": "VAELoader", - "enableTabs": false, - "tabWidth": 65, - "tabXOffset": 10, - "hasSecondTab": false, - "secondTabText": "Send Back", - "secondTabOffset": 80, - "secondTabWidth": 65, "models": [ { "name": "qwen_image_vae.safetensors", "url": "https://huggingface.co/FireRedTeam/FireRed-Image-Edit-1.0-ComfyUI/resolve/main/qwen_image_vae.safetensors", "directory": "vae" } - ] + ], + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 }, "widgets_values": [ "qwen_image_vae.safetensors" ] }, { - "id": 115, + "id": 203, "type": "CLIPLoader", "pos": [ -960, - -1370 + -1400 ], "size": [ 400, 150 ], "flags": {}, - "order": 4, + "order": 10, "mode": 0, "inputs": [ { @@ -1010,27 +1010,27 @@ } ], "properties": { - "ue_properties": { - "widget_ue_connectable": {}, - "input_ue_unconnectable": {} - }, + "Node name for S&R": "CLIPLoader", "cnr_id": "comfy-core", "ver": "0.5.1", - "Node name for S&R": "CLIPLoader", - "enableTabs": false, - "tabWidth": 65, - "tabXOffset": 10, - "hasSecondTab": false, - "secondTabText": "Send Back", - "secondTabOffset": 80, - "secondTabWidth": 65, "models": [ { "name": "qwen_2.5_vl_7b_fp8_scaled.safetensors", "url": "https://huggingface.co/Comfy-Org/HunyuanVideo_1.5_repackaged/resolve/main/split_files/text_encoders/qwen_2.5_vl_7b_fp8_scaled.safetensors", "directory": "text_encoders" } - ] + ], + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 }, "widgets_values": [ "qwen_2.5_vl_7b_fp8_scaled.safetensors", @@ -1039,7 +1039,7 @@ ] }, { - "id": 151, + "id": 204, "type": "LoraLoaderModelOnly", "pos": [ 100, @@ -1050,7 +1050,7 @@ 140 ], "flags": {}, - "order": 14, + "order": 11, "mode": 0, "inputs": [ { @@ -1089,27 +1089,27 @@ } ], "properties": { - "ue_properties": { - "widget_ue_connectable": {}, - "input_ue_unconnectable": {} - }, + "Node name for S&R": "LoraLoaderModelOnly", "cnr_id": "comfy-core", "ver": "0.15.1", - "Node name for S&R": "LoraLoaderModelOnly", - "enableTabs": false, - "tabWidth": 65, - "tabXOffset": 10, - "hasSecondTab": false, - "secondTabText": "Send Back", - "secondTabOffset": 80, - "secondTabWidth": 65, "models": [ { "name": "FireRed-Image-Edit-1.0-Lightning-8steps-v1.0.safetensors", "url": "https://huggingface.co/FireRedTeam/FireRed-Image-Edit-1.0-ComfyUI/resolve/main/FireRed-Image-Edit-1.0-Lightning-8steps-v1.0.safetensors", "directory": "loras" } - ] + ], + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 }, "widgets_values": [ "FireRed-Image-Edit-1.0-Lightning-8steps-v1.0.safetensors", @@ -1117,7 +1117,7 @@ ] }, { - "id": 128, + "id": 205, "type": "UNETLoader", "pos": [ -960, @@ -1163,27 +1163,27 @@ } ], "properties": { - "ue_properties": { - "widget_ue_connectable": {}, - "input_ue_unconnectable": {} - }, + "Node name for S&R": "UNETLoader", "cnr_id": "comfy-core", "ver": "0.5.1", - "Node name for S&R": "UNETLoader", - "enableTabs": false, - "tabWidth": 65, - "tabXOffset": 10, - "hasSecondTab": false, - "secondTabText": "Send Back", - "secondTabOffset": 80, - "secondTabWidth": 65, "models": [ { "name": "FireRed-Image-Edit-1.1-transformer.safetensors", "url": "https://huggingface.co/FireRedTeam/FireRed-Image-Edit-1.1-ComfyUI/resolve/main/FireRed-Image-Edit-1.1-transformer.safetensors", "directory": "diffusion_models" } - ] + ], + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 }, "widgets_values": [ "FireRed-Image-Edit-1.1-transformer.safetensors", @@ -1191,7 +1191,7 @@ ] }, { - "id": 125, + "id": 206, "type": "VAEEncode", "pos": [ -390, @@ -1202,7 +1202,7 @@ 100 ], "flags": {}, - "order": 10, + "order": 13, "mode": 0, "inputs": [ { @@ -1229,13 +1229,13 @@ } ], "properties": { + "Node name for S&R": "VAEEncode", + "cnr_id": "comfy-core", + "ver": "0.5.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.5.1", - "Node name for S&R": "VAEEncode", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1246,7 +1246,7 @@ } }, { - "id": 153, + "id": 207, "type": "PrimitiveBoolean", "pos": [ 160, @@ -1257,7 +1257,7 @@ 100 ], "flags": {}, - "order": 15, + "order": 14, "mode": 0, "inputs": [ { @@ -1284,13 +1284,13 @@ ], "title": "Enable Lightning LoRA?", "properties": { + "Node name for S&R": "PrimitiveBoolean", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "PrimitiveBoolean", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1304,7 +1304,7 @@ ] }, { - "id": 118, + "id": 208, "type": "TextEncodeQwenImageEditPlus", "pos": [ -480, @@ -1315,7 +1315,7 @@ 370 ], "flags": {}, - "order": 7, + "order": 15, "mode": 0, "inputs": [ { @@ -1374,13 +1374,13 @@ ], "title": "TextEncodeQwenImageEditPlus (Positive)", "properties": { + "Node name for S&R": "TextEncodeQwenImageEditPlus", + "cnr_id": "comfy-core", + "ver": "0.5.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.5.1", - "Node name for S&R": "TextEncodeQwenImageEditPlus", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1396,7 +1396,7 @@ "bgcolor": "#353" }, { - "id": 117, + "id": 209, "type": "TextEncodeQwenImageEditPlus", "pos": [ -470, @@ -1407,7 +1407,7 @@ 290 ], "flags": {}, - "order": 6, + "order": 16, "mode": 0, "inputs": [ { @@ -1465,13 +1465,13 @@ } ], "properties": { + "Node name for S&R": "TextEncodeQwenImageEditPlus", + "cnr_id": "comfy-core", + "ver": "0.5.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.5.1", - "Node name for S&R": "TextEncodeQwenImageEditPlus", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1487,18 +1487,18 @@ "bgcolor": "#535" }, { - "id": 130, + "id": 210, "type": "KSampler", "pos": [ - 1060, - -1420 + 1010, + -1340 ], "size": [ 270, 480 ], "flags": {}, - "order": 13, + "order": 17, "mode": 0, "inputs": [ { @@ -1591,13 +1591,13 @@ } ], "properties": { + "Node name for S&R": "KSampler", + "cnr_id": "comfy-core", + "ver": "0.5.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.5.1", - "Node name for S&R": "KSampler", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1617,11 +1617,11 @@ ] }, { - "id": 126, + "id": 211, "type": "VAEDecode", "pos": [ - 1360, - -1420 + 1440, + -1340 ], "size": [ 230, @@ -1630,7 +1630,7 @@ "flags": { "collapsed": false }, - "order": 11, + "order": 18, "mode": 0, "inputs": [ { @@ -1658,13 +1658,13 @@ } ], "properties": { + "Node name for S&R": "VAEDecode", + "cnr_id": "comfy-core", + "ver": "0.5.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.5.1", - "Node name for S&R": "VAEDecode", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1675,7 +1675,7 @@ } }, { - "id": 174, + "id": 212, "type": "ResizeImageMaskNode", "pos": [ -900, @@ -1736,18 +1736,18 @@ } ], "properties": { + "Node name for S&R": "ResizeImageMaskNode", + "cnr_id": "comfy-core", + "ver": "0.18.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} - }, - "cnr_id": "comfy-core", - "ver": "0.18.1", - "Node name for S&R": "ResizeImageMaskNode" + } }, "widgets_values": [ "scale total pixels", 1, - "area" + "lanczos" ] } ], @@ -1808,207 +1808,207 @@ "links": [ { "id": 326, - "origin_id": 154, + "origin_id": 194, "origin_slot": 0, - "target_id": 120, + "target_id": 193, "target_slot": 0, "type": "MODEL" }, { "id": 324, - "origin_id": 128, + "origin_id": 205, "origin_slot": 0, - "target_id": 154, + "target_id": 194, "target_slot": 0, "type": "MODEL" }, { "id": 325, - "origin_id": 151, + "origin_id": 204, "origin_slot": 0, - "target_id": 154, + "target_id": 194, "target_slot": 1, "type": "MODEL" }, { "id": 323, - "origin_id": 153, + "origin_id": 207, "origin_slot": 0, - "target_id": 154, + "target_id": 194, "target_slot": 2, "type": "BOOLEAN" }, { "id": 294, - "origin_id": 120, + "origin_id": 193, "origin_slot": 0, - "target_id": 123, + "target_id": 196, "target_slot": 0, "type": "MODEL" }, { "id": 333, - "origin_id": 162, + "origin_id": 199, "origin_slot": 0, - "target_id": 164, + "target_id": 197, "target_slot": 0, "type": "FLOAT" }, { "id": 334, - "origin_id": 163, + "origin_id": 200, "origin_slot": 0, - "target_id": 164, + "target_id": 197, "target_slot": 1, "type": "FLOAT" }, { "id": 336, - "origin_id": 153, + "origin_id": 207, "origin_slot": 0, - "target_id": 164, + "target_id": 197, "target_slot": 2, "type": "BOOLEAN" }, { "id": 329, - "origin_id": 155, + "origin_id": 195, "origin_slot": 0, - "target_id": 157, + "target_id": 201, "target_slot": 0, "type": "INT" }, { "id": 337, - "origin_id": 156, + "origin_id": 198, "origin_slot": 0, - "target_id": 157, + "target_id": 201, "target_slot": 1, "type": "INT" }, { "id": 330, - "origin_id": 153, + "origin_id": 207, "origin_slot": 0, - "target_id": 157, + "target_id": 201, "target_slot": 2, "type": "BOOLEAN" }, { "id": 297, - "origin_id": 115, + "origin_id": 203, "origin_slot": 0, - "target_id": 117, + "target_id": 209, "target_slot": 0, "type": "CLIP" }, { "id": 299, - "origin_id": 116, + "origin_id": 202, "origin_slot": 0, - "target_id": 117, + "target_id": 209, "target_slot": 1, "type": "VAE" }, { "id": 316, - "origin_id": 128, + "origin_id": 205, "origin_slot": 0, - "target_id": 151, + "target_id": 204, "target_slot": 0, "type": "MODEL" }, { "id": 296, - "origin_id": 115, + "origin_id": 203, "origin_slot": 0, - "target_id": 118, + "target_id": 208, "target_slot": 0, "type": "CLIP" }, { "id": 298, - "origin_id": 116, + "origin_id": 202, "origin_slot": 0, - "target_id": 118, + "target_id": 208, "target_slot": 1, "type": "VAE" }, { "id": 300, - "origin_id": 116, + "origin_id": 202, "origin_slot": 0, - "target_id": 125, + "target_id": 206, "target_slot": 1, "type": "VAE" }, { "id": 295, - "origin_id": 123, + "origin_id": 196, "origin_slot": 0, - "target_id": 130, + "target_id": 210, "target_slot": 0, "type": "MODEL" }, { "id": 312, - "origin_id": 118, + "origin_id": 208, "origin_slot": 0, - "target_id": 130, + "target_id": 210, "target_slot": 1, "type": "CONDITIONING" }, { "id": 313, - "origin_id": 117, + "origin_id": 209, "origin_slot": 0, - "target_id": 130, + "target_id": 210, "target_slot": 2, "type": "CONDITIONING" }, { "id": 303, - "origin_id": 125, + "origin_id": 206, "origin_slot": 0, - "target_id": 130, + "target_id": 210, "target_slot": 3, "type": "LATENT" }, { "id": 345, - "origin_id": 157, + "origin_id": 201, "origin_slot": 0, - "target_id": 130, + "target_id": 210, "target_slot": 5, "type": "INT" }, { "id": 335, - "origin_id": 164, + "origin_id": 197, "origin_slot": 0, - "target_id": 130, + "target_id": 210, "target_slot": 6, "type": "FLOAT" }, { "id": 273, - "origin_id": 130, + "origin_id": 210, "origin_slot": 0, - "target_id": 126, + "target_id": 211, "target_slot": 0, "type": "LATENT" }, { "id": 314, - "origin_id": 116, + "origin_id": 202, "origin_slot": 0, - "target_id": 126, + "target_id": 211, "target_slot": 1, "type": "VAE" }, { "id": 292, - "origin_id": 126, + "origin_id": 211, "origin_slot": 0, "target_id": -20, "target_slot": 0, @@ -2018,7 +2018,7 @@ "id": 355, "origin_id": -10, "origin_slot": 1, - "target_id": 118, + "target_id": 208, "target_slot": 3, "type": "IMAGE" }, @@ -2026,7 +2026,7 @@ "id": 356, "origin_id": -10, "origin_slot": 1, - "target_id": 117, + "target_id": 209, "target_slot": 3, "type": "IMAGE" }, @@ -2034,7 +2034,7 @@ "id": 357, "origin_id": -10, "origin_slot": 2, - "target_id": 118, + "target_id": 208, "target_slot": 4, "type": "IMAGE" }, @@ -2042,7 +2042,7 @@ "id": 358, "origin_id": -10, "origin_slot": 2, - "target_id": 117, + "target_id": 209, "target_slot": 4, "type": "IMAGE" }, @@ -2050,7 +2050,7 @@ "id": 359, "origin_id": -10, "origin_slot": 3, - "target_id": 118, + "target_id": 208, "target_slot": 5, "type": "STRING" }, @@ -2058,31 +2058,31 @@ "id": 364, "origin_id": -10, "origin_slot": 4, - "target_id": 153, + "target_id": 207, "target_slot": 0, "type": "BOOLEAN" }, { "id": 368, - "origin_id": 174, + "origin_id": 212, "origin_slot": 0, - "target_id": 125, + "target_id": 206, "target_slot": 0, "type": "IMAGE" }, { "id": 369, - "origin_id": 174, + "origin_id": 212, "origin_slot": 0, - "target_id": 118, + "target_id": 208, "target_slot": 2, "type": "IMAGE" }, { "id": 370, - "origin_id": 174, + "origin_id": 212, "origin_slot": 0, - "target_id": 117, + "target_id": 209, "target_slot": 2, "type": "IMAGE" }, @@ -2090,7 +2090,7 @@ "id": 371, "origin_id": -10, "origin_slot": 0, - "target_id": 174, + "target_id": 212, "target_slot": 0, "type": "IMAGE" }, @@ -2098,7 +2098,7 @@ "id": 372, "origin_id": -10, "origin_slot": 5, - "target_id": 130, + "target_id": 210, "target_slot": 4, "type": "INT" }, @@ -2106,7 +2106,7 @@ "id": 373, "origin_id": -10, "origin_slot": 6, - "target_id": 128, + "target_id": 205, "target_slot": 0, "type": "COMBO" }, @@ -2114,7 +2114,7 @@ "id": 374, "origin_id": -10, "origin_slot": 7, - "target_id": 115, + "target_id": 203, "target_slot": 0, "type": "COMBO" }, @@ -2122,7 +2122,7 @@ "id": 375, "origin_id": -10, "origin_slot": 8, - "target_id": 116, + "target_id": 202, "target_slot": 0, "type": "COMBO" }, @@ -2130,7 +2130,7 @@ "id": 376, "origin_id": -10, "origin_slot": 9, - "target_id": 151, + "target_id": 204, "target_slot": 1, "type": "COMBO" } diff --git a/blueprints/Image Edit (Flux.2 Dev).json b/blueprints/Image Edit (Flux.2 Dev).json new file mode 100644 index 000000000..92827bf17 --- /dev/null +++ b/blueprints/Image Edit (Flux.2 Dev).json @@ -0,0 +1,2050 @@ +{ + "revision": 0, + "last_node_id": 139, + "last_link_id": 0, + "nodes": [ + { + "id": 139, + "type": "41b0c117-7470-454c-914e-b8742dc06d62", + "pos": [ + -650, + 570 + ], + "size": [ + 400, + 0 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "label": "image", + "localized_name": "pixels", + "name": "pixels", + "type": "IMAGE", + "link": null + }, + { + "label": "prompt", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + }, + { + "label": "enable_turbo_mode", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": null + }, + { + "label": "turbo_lora", + "name": "lora_name", + "type": "COMBO", + "widget": { + "name": "lora_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "123", + "text" + ], + [ + "129", + "unet_name" + ], + [ + "124", + "clip_name" + ], + [ + "121", + "vae_name" + ], + [ + "138", + "value" + ], + [ + "128", + "lora_name" + ], + [ + "125", + "noise_seed" + ], + [ + "125", + "control_after_generate" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": { + "text": true, + "value": true, + "lora_name": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Image Edit (Flux.2 Dev)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "41b0c117-7470-454c-914e-b8742dc06d62", + "version": 1, + "state": { + "lastGroupId": 8, + "lastNodeId": 139, + "lastLinkId": 194, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Image Edit (Flux.2 Dev)", + "inputNode": { + "id": -10, + "bounding": [ + -1520, + 400, + 151.744140625, + 180 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1240, + 420, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "fc74acd5-30a9-410b-abb5-4a4171ba3d25", + "name": "pixels", + "type": "IMAGE", + "linkIds": [ + 126, + 169 + ], + "localized_name": "pixels", + "label": "image", + "pos": [ + -1388.255859375, + 420 + ] + }, + { + "id": "3e69affa-397b-4d52-82d7-68dfcef9e761", + "name": "text", + "type": "STRING", + "linkIds": [ + 168 + ], + "label": "prompt", + "pos": [ + -1388.255859375, + 440 + ] + }, + { + "id": "2f016a8a-fb3e-4cb9-97f2-a991defe4fa2", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 177 + ], + "pos": [ + -1388.255859375, + 460 + ] + }, + { + "id": "799b9dc7-0c90-4b19-9a13-e01d896bea1f", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 178 + ], + "pos": [ + -1388.255859375, + 480 + ] + }, + { + "id": "e58a83c9-1b93-4378-9598-f24068820313", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 179 + ], + "pos": [ + -1388.255859375, + 500 + ] + }, + { + "id": "8335a4a9-0ce4-4e67-a641-1c9d7a762977", + "name": "value", + "type": "BOOLEAN", + "linkIds": [ + 191 + ], + "label": "enable_turbo_mode", + "pos": [ + -1388.255859375, + 520 + ] + }, + { + "id": "890b22b4-44a7-4707-912a-ca8b4ee7b7c9", + "name": "lora_name", + "type": "COMBO", + "linkIds": [ + 192 + ], + "label": "turbo_lora", + "pos": [ + -1388.255859375, + 540 + ] + } + ], + "outputs": [ + { + "id": "3eaa05d6-4960-4a7c-bf2a-8b585fbb7c9c", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 9 + ], + "localized_name": "IMAGE", + "pos": [ + 1260, + 440 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 118, + "type": "Flux2Scheduler", + "pos": [ + 540, + 430 + ], + "size": [ + 230, + 170 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": 188 + }, + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 170 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 172 + } + ], + "outputs": [ + { + "localized_name": "SIGMAS", + "name": "SIGMAS", + "type": "SIGMAS", + "links": [ + 132 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "Flux2Scheduler", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 20, + 1248, + 832 + ] + }, + { + "id": 119, + "type": "BasicGuider", + "pos": [ + 530, + 120 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 185 + }, + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 166 + } + ], + "outputs": [ + { + "localized_name": "GUIDER", + "name": "GUIDER", + "type": "GUIDER", + "slot_index": 0, + "links": [ + 30 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "BasicGuider", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 120, + "type": "KSamplerSelect", + "pos": [ + 530, + 270 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "SAMPLER", + "name": "SAMPLER", + "type": "SAMPLER", + "links": [ + 19 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "KSamplerSelect", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "euler" + ] + }, + { + "id": 121, + "type": "VAELoader", + "pos": [ + -970, + 390 + ], + "size": [ + 300, + 110 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 179 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "slot_index": 0, + "links": [ + 127, + 159 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAELoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "full_encoder_small_decoder.safetensors", + "url": "https://huggingface.co/black-forest-labs/FLUX.2-small-decoder/resolve/main/full_encoder_small_decoder.safetensors", + "directory": "vae" + } + ] + }, + "widgets_values": [ + "full_encoder_small_decoder.safetensors" + ] + }, + { + "id": 122, + "type": "SamplerCustomAdvanced", + "pos": [ + 790, + -50 + ], + "size": [ + 280, + 170 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "noise", + "name": "noise", + "type": "NOISE", + "link": 37 + }, + { + "localized_name": "guider", + "name": "guider", + "type": "GUIDER", + "link": 30 + }, + { + "localized_name": "sampler", + "name": "sampler", + "type": "SAMPLER", + "link": 19 + }, + { + "localized_name": "sigmas", + "name": "sigmas", + "type": "SIGMAS", + "link": 132 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 161 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "LATENT", + "slot_index": 0, + "links": [ + 24 + ] + }, + { + "localized_name": "denoised_output", + "name": "denoised_output", + "type": "LATENT", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "SamplerCustomAdvanced", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 123, + "type": "CLIPTextEncode", + "pos": [ + -630, + -50 + ], + "size": [ + 430, + 360 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 117 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 168 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 41 + ] + } + ], + "title": "CLIP Text Encode (Positive Prompt)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPTextEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 124, + "type": "CLIPLoader", + "pos": [ + -970, + 160 + ], + "size": [ + 300, + 150 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 178 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 117 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "mistral_3_small_flux2_bf16.safetensors", + "url": "https://huggingface.co/Comfy-Org/flux2-dev/resolve/main/split_files/text_encoders/mistral_3_small_flux2_bf16.safetensors", + "directory": "text_encoders" + } + ] + }, + "widgets_values": [ + "mistral_3_small_flux2_bf16.safetensors", + "flux2", + "default" + ] + }, + { + "id": 125, + "type": "RandomNoise", + "pos": [ + 530, + -50 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "noise_seed", + "name": "noise_seed", + "type": "INT", + "widget": { + "name": "noise_seed" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "NOISE", + "name": "NOISE", + "type": "NOISE", + "links": [ + 37 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "RandomNoise", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 342971778941390, + "randomize" + ] + }, + { + "id": 126, + "type": "VAEDecode", + "pos": [ + 830, + 410 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 24 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 159 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 9 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAEDecode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 127, + "type": "FluxGuidance", + "pos": [ + -520, + 390 + ], + "size": [ + 320, + 110 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 41 + }, + { + "localized_name": "guidance", + "name": "guidance", + "type": "FLOAT", + "widget": { + "name": "guidance" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 144 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "FluxGuidance", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 4 + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 128, + "type": "LoraLoaderModelOnly", + "pos": [ + -150, + 200 + ], + "size": [ + 300, + 140 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 181 + }, + { + "localized_name": "lora_name", + "name": "lora_name", + "type": "COMBO", + "widget": { + "name": "lora_name" + }, + "link": 192 + }, + { + "localized_name": "strength_model", + "name": "strength_model", + "type": "FLOAT", + "widget": { + "name": "strength_model" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 183 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LoraLoaderModelOnly", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "Flux_2-Turbo-LoRA_comfyui.safetensors", + "url": "https://huggingface.co/ByteZSzn/Flux.2-Turbo-ComfyUI/resolve/main/Flux_2-Turbo-LoRA_comfyui.safetensors", + "directory": "loras" + } + ] + }, + "widgets_values": [ + "Flux_2-Turbo-LoRA_comfyui.safetensors", + 1 + ] + }, + { + "id": 129, + "type": "UNETLoader", + "pos": [ + -970, + -40 + ], + "size": [ + 300, + 110 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 177 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 181, + 184 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "UNETLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "flux2_dev_fp8mixed.safetensors", + "url": "https://huggingface.co/Comfy-Org/flux2-dev/resolve/main/split_files/diffusion_models/flux2_dev_fp8mixed.safetensors", + "directory": "diffusion_models" + } + ] + }, + "widgets_values": [ + "flux2_dev_fp8mixed.safetensors", + "default" + ] + }, + { + "id": 130, + "type": "ComfySwitchNode", + "pos": [ + 220, + 10 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 184 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 183 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 190 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 185 + ] + } + ], + "title": "Switch(model)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ComfySwitchNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 131, + "type": "PrimitiveInt", + "pos": [ + -150, + 430 + ], + "size": [ + 300, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 186 + ] + } + ], + "title": "Steps", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 8, + "fixed" + ] + }, + { + "id": 132, + "type": "PrimitiveInt", + "pos": [ + -150, + -50 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 187 + ] + } + ], + "title": "Steps", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 20, + "fixed" + ] + }, + { + "id": 133, + "type": "ComfySwitchNode", + "pos": [ + 220, + 280 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 187 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 186 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 189 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 188 + ] + } + ], + "title": "Switch(steps)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ComfySwitchNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 134, + "type": "EmptyFlux2LatentImage", + "pos": [ + 530, + 790 + ], + "size": [ + 270, + 170 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 171 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 173 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 161 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "EmptyFlux2LatentImage", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1248, + 832, + 1 + ] + }, + { + "id": 135, + "type": "GetImageSize", + "pos": [ + -100, + 810 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 169 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": [ + 170, + 171 + ] + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": [ + 172, + 173 + ] + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "GetImageSize", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 136, + "type": "VAEEncode", + "pos": [ + -910, + 600 + ], + "size": [ + 230, + 100 + ], + "flags": { + "collapsed": true + }, + "order": 18, + "mode": 0, + "inputs": [ + { + "localized_name": "pixels", + "name": "pixels", + "type": "IMAGE", + "link": 126 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 127 + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 125 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAEEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 137, + "type": "ReferenceLatent", + "pos": [ + -470, + 580 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 144 + }, + { + "localized_name": "latent", + "name": "latent", + "shape": 7, + "type": "LATENT", + "link": 125 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 166 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ReferenceLatent", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 138, + "type": "PrimitiveBoolean", + "pos": [ + -130, + 640 + ], + "size": [ + 270, + 100 + ], + "flags": {}, + "order": 20, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 191 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 189, + 190 + ] + } + ], + "title": "Enable 8 steps lora", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveBoolean", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + } + ], + "groups": [ + { + "id": 1, + "title": "Models", + "bounding": [ + -980, + -120, + 320, + 640 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 2, + "title": "Custom sampler", + "bounding": [ + 520, + -120, + 590, + 740 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 3, + "title": "Image size", + "bounding": [ + 510, + 690, + 590, + 290 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 4, + "title": "Prompt", + "bounding": [ + -640, + -120, + 450, + 640 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 7, + "title": "Original", + "bounding": [ + -160, + -120, + 340, + 230 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 8, + "title": "8 Steps LoRA", + "bounding": [ + -160, + 130, + 340, + 430 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "links": [ + { + "id": 41, + "origin_id": 123, + "origin_slot": 0, + "target_id": 127, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 144, + "origin_id": 127, + "origin_slot": 0, + "target_id": 137, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 125, + "origin_id": 136, + "origin_slot": 0, + "target_id": 137, + "target_slot": 1, + "type": "LATENT" + }, + { + "id": 37, + "origin_id": 125, + "origin_slot": 0, + "target_id": 122, + "target_slot": 0, + "type": "NOISE" + }, + { + "id": 30, + "origin_id": 119, + "origin_slot": 0, + "target_id": 122, + "target_slot": 1, + "type": "GUIDER" + }, + { + "id": 19, + "origin_id": 120, + "origin_slot": 0, + "target_id": 122, + "target_slot": 2, + "type": "SAMPLER" + }, + { + "id": 132, + "origin_id": 118, + "origin_slot": 0, + "target_id": 122, + "target_slot": 3, + "type": "SIGMAS" + }, + { + "id": 161, + "origin_id": 134, + "origin_slot": 0, + "target_id": 122, + "target_slot": 4, + "type": "LATENT" + }, + { + "id": 24, + "origin_id": 122, + "origin_slot": 0, + "target_id": 126, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 159, + "origin_id": 121, + "origin_slot": 0, + "target_id": 126, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 117, + "origin_id": 124, + "origin_slot": 0, + "target_id": 123, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 127, + "origin_id": 121, + "origin_slot": 0, + "target_id": 136, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 126, + "origin_id": -10, + "origin_slot": 0, + "target_id": 136, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 9, + "origin_id": 126, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 166, + "origin_id": 137, + "origin_slot": 0, + "target_id": 119, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 168, + "origin_id": -10, + "origin_slot": 1, + "target_id": 123, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 169, + "origin_id": -10, + "origin_slot": 0, + "target_id": 135, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 170, + "origin_id": 135, + "origin_slot": 0, + "target_id": 118, + "target_slot": 1, + "type": "INT" + }, + { + "id": 171, + "origin_id": 135, + "origin_slot": 0, + "target_id": 134, + "target_slot": 0, + "type": "INT" + }, + { + "id": 172, + "origin_id": 135, + "origin_slot": 1, + "target_id": 118, + "target_slot": 2, + "type": "INT" + }, + { + "id": 173, + "origin_id": 135, + "origin_slot": 1, + "target_id": 134, + "target_slot": 1, + "type": "INT" + }, + { + "id": 177, + "origin_id": -10, + "origin_slot": 2, + "target_id": 129, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 178, + "origin_id": -10, + "origin_slot": 3, + "target_id": 124, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 179, + "origin_id": -10, + "origin_slot": 4, + "target_id": 121, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 181, + "origin_id": 129, + "origin_slot": 0, + "target_id": 128, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 183, + "origin_id": 128, + "origin_slot": 0, + "target_id": 130, + "target_slot": 1, + "type": "MODEL" + }, + { + "id": 184, + "origin_id": 129, + "origin_slot": 0, + "target_id": 130, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 185, + "origin_id": 130, + "origin_slot": 0, + "target_id": 119, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 186, + "origin_id": 131, + "origin_slot": 0, + "target_id": 133, + "target_slot": 1, + "type": "INT" + }, + { + "id": 187, + "origin_id": 132, + "origin_slot": 0, + "target_id": 133, + "target_slot": 0, + "type": "INT" + }, + { + "id": 188, + "origin_id": 133, + "origin_slot": 0, + "target_id": 118, + "target_slot": 0, + "type": "INT" + }, + { + "id": 189, + "origin_id": 138, + "origin_slot": 0, + "target_id": 133, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 190, + "origin_id": 138, + "origin_slot": 0, + "target_id": 130, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 191, + "origin_id": -10, + "origin_slot": 5, + "target_id": 138, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 192, + "origin_id": -10, + "origin_slot": 6, + "target_id": 128, + "target_slot": 1, + "type": "COMBO" + } + ], + "extra": { + "workflowRendererVersion": "LG" + }, + "category": "Image generation and editing/Edit image", + "description": "Edits an image from text instructions using Flux.2 [dev], with guidance, schedulers, and optional Turbo LoRAs." + } + ] + }, + "extra": { + "ue_links": [] + } +} \ No newline at end of file diff --git a/blueprints/Image Edit (Qwen 2509).json b/blueprints/Image Edit (Qwen 2509).json new file mode 100644 index 000000000..f7be322a0 --- /dev/null +++ b/blueprints/Image Edit (Qwen 2509).json @@ -0,0 +1,1947 @@ +{ + "revision": 0, + "last_node_id": 433, + "last_link_id": 0, + "nodes": [ + { + "id": 433, + "type": "eba40a3a-f6c5-48ac-b58e-55525d06b373", + "pos": [ + 90, + -160 + ], + "size": [ + 390, + 610 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "label": "image", + "name": "image", + "type": "IMAGE", + "link": null + }, + { + "label": "image2 (optional)", + "name": "image2", + "type": "IMAGE", + "link": null + }, + { + "label": "image3 (optional)", + "name": "image3", + "type": "IMAGE", + "link": null + }, + { + "name": "prompt", + "type": "STRING", + "widget": { + "name": "prompt" + }, + "link": null + }, + { + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "label": "enable_turbo_mode", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "111", + "prompt" + ], + [ + "3", + "seed" + ], + [ + "443", + "value" + ], + [ + "37", + "unet_name" + ], + [ + "38", + "clip_name" + ], + [ + "39", + "vae_name" + ], + [ + "3", + "control_after_generate" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.3.62" + }, + "widgets_values": [], + "title": "Image Edit (Qwen 2509)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "eba40a3a-f6c5-48ac-b58e-55525d06b373", + "version": 1, + "state": { + "lastGroupId": 51, + "lastNodeId": 468, + "lastLinkId": 731, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Image Edit (Qwen 2509)", + "inputNode": { + "id": -10, + "bounding": [ + -1160, + 280, + 151.744140625, + 220 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 2030, + -20, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "d5089bd3-63bc-4a24-b478-6565ed2364e3", + "name": "image", + "type": "IMAGE", + "linkIds": [ + 248 + ], + "label": "image", + "pos": [ + -1028.255859375, + 300 + ] + }, + { + "id": "9e80fff0-ed0a-439f-a16e-a4a6cc1eb601", + "name": "image2", + "type": "IMAGE", + "linkIds": [ + 235, + 236 + ], + "label": "image2 (optional)", + "pos": [ + -1028.255859375, + 320 + ] + }, + { + "id": "49d98fd6-01b5-440b-8603-579252fd7fef", + "name": "image3", + "type": "IMAGE", + "linkIds": [ + 237, + 238 + ], + "label": "image3 (optional)", + "pos": [ + -1028.255859375, + 340 + ] + }, + { + "id": "5de32f24-a7b5-4423-b772-72824005f585", + "name": "prompt", + "type": "STRING", + "linkIds": [ + 244 + ], + "pos": [ + -1028.255859375, + 360 + ] + }, + { + "id": "85fb3d74-7881-4c71-bc8c-624be5eedc3d", + "name": "seed", + "type": "INT", + "linkIds": [ + 718 + ], + "pos": [ + -1028.255859375, + 380 + ] + }, + { + "id": "b0c828de-d7eb-42a3-8dfb-4f53360d4fc9", + "name": "value", + "type": "BOOLEAN", + "linkIds": [ + 719 + ], + "label": "enable_turbo_mode", + "pos": [ + -1028.255859375, + 400 + ] + }, + { + "id": "072baa05-5551-4a98-bd66-015a36833ac2", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 720 + ], + "pos": [ + -1028.255859375, + 420 + ] + }, + { + "id": "d2891d11-b336-4750-9742-b93717c9ae39", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 721 + ], + "pos": [ + -1028.255859375, + 440 + ] + }, + { + "id": "4218135f-5128-4b7e-8572-92cc55615793", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 722 + ], + "pos": [ + -1028.255859375, + 460 + ] + } + ], + "outputs": [ + { + "id": "c4ebfc18-de83-4361-8e42-767c3c8c25c0", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 110 + ], + "localized_name": "IMAGE", + "pos": [ + 2050, + 0 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 75, + "type": "CFGNorm", + "pos": [ + 1080, + 30 + ], + "size": [ + 290, + 110 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 141 + }, + { + "localized_name": "strength", + "name": "strength", + "type": "FLOAT", + "widget": { + "name": "strength" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "patched_model", + "name": "patched_model", + "type": "MODEL", + "links": [ + 186 + ] + } + ], + "properties": { + "Node name for S&R": "CFGNorm", + "cnr_id": "comfy-core", + "ver": "0.3.50", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": { + "strength": true + } + } + }, + "widgets_values": [ + 1 + ] + }, + { + "id": 39, + "type": "VAELoader", + "pos": [ + -730, + 410 + ], + "size": [ + 330, + 110 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 722 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "slot_index": 0, + "links": [ + 76, + 168, + 206, + 207 + ] + } + ], + "properties": { + "Node name for S&R": "VAELoader", + "cnr_id": "comfy-core", + "ver": "0.3.48", + "models": [ + { + "name": "qwen_image_vae.safetensors", + "url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/vae/qwen_image_vae.safetensors", + "directory": "vae" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + "qwen_image_vae.safetensors" + ] + }, + { + "id": 38, + "type": "CLIPLoader", + "pos": [ + -730, + 150 + ], + "size": [ + 330, + 150 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 721 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "slot_index": 0, + "links": [ + 204, + 205 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.3.48", + "models": [ + { + "name": "qwen_2.5_vl_7b_fp8_scaled.safetensors", + "url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/text_encoders/qwen_2.5_vl_7b_fp8_scaled.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + "qwen_2.5_vl_7b_fp8_scaled.safetensors", + "qwen_image", + "default" + ] + }, + { + "id": 37, + "type": "UNETLoader", + "pos": [ + -730, + -60 + ], + "size": [ + 330, + 110 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 720 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 184, + 710 + ] + } + ], + "properties": { + "Node name for S&R": "UNETLoader", + "cnr_id": "comfy-core", + "ver": "0.3.48", + "models": [ + { + "name": "qwen_image_edit_2509_fp8_e4m3fn.safetensors", + "url": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_edit_2509_fp8_e4m3fn.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + "qwen_image_edit_2509_fp8_e4m3fn.safetensors", + "default" + ] + }, + { + "id": 110, + "type": "TextEncodeQwenImageEditPlus", + "pos": [ + -240, + 320 + ], + "size": [ + 400, + 240 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 204 + }, + { + "localized_name": "vae", + "name": "vae", + "shape": 7, + "type": "VAE", + "link": 206 + }, + { + "localized_name": "image1", + "name": "image1", + "shape": 7, + "type": "IMAGE", + "link": 251 + }, + { + "localized_name": "image2", + "name": "image2", + "shape": 7, + "type": "IMAGE", + "link": 236 + }, + { + "localized_name": "image3", + "name": "image3", + "shape": 7, + "type": "IMAGE", + "link": 238 + }, + { + "localized_name": "prompt", + "name": "prompt", + "type": "STRING", + "widget": { + "name": "prompt" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 210 + ] + } + ], + "properties": { + "Node name for S&R": "TextEncodeQwenImageEditPlus", + "cnr_id": "comfy-core", + "ver": "0.3.59" + }, + "widgets_values": [ + "" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 66, + "type": "ModelSamplingAuraFlow", + "pos": [ + 1070, + -120 + ], + "size": [ + 290, + 110 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 708 + }, + { + "localized_name": "shift", + "name": "shift", + "type": "FLOAT", + "widget": { + "name": "shift" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 141 + ] + } + ], + "properties": { + "Node name for S&R": "ModelSamplingAuraFlow", + "cnr_id": "comfy-core", + "ver": "0.3.48", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + 3 + ] + }, + { + "id": 111, + "type": "TextEncodeQwenImageEditPlus", + "pos": [ + -250, + -70 + ], + "size": [ + 410, + 330 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 205 + }, + { + "localized_name": "vae", + "name": "vae", + "shape": 7, + "type": "VAE", + "link": 207 + }, + { + "localized_name": "image1", + "name": "image1", + "shape": 7, + "type": "IMAGE", + "link": 250 + }, + { + "localized_name": "image2", + "name": "image2", + "shape": 7, + "type": "IMAGE", + "link": 235 + }, + { + "localized_name": "image3", + "name": "image3", + "shape": 7, + "type": "IMAGE", + "link": 237 + }, + { + "localized_name": "prompt", + "name": "prompt", + "type": "STRING", + "widget": { + "name": "prompt" + }, + "link": 244 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 211 + ] + } + ], + "properties": { + "Node name for S&R": "TextEncodeQwenImageEditPlus", + "cnr_id": "comfy-core", + "ver": "0.3.59" + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 88, + "type": "VAEEncode", + "pos": [ + -70, + 640 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "localized_name": "pixels", + "name": "pixels", + "type": "IMAGE", + "link": 249 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 168 + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 246 + ] + } + ], + "properties": { + "Node name for S&R": "VAEEncode", + "cnr_id": "comfy-core", + "ver": "0.3.50", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {} + } + } + }, + { + "id": 8, + "type": "VAEDecode", + "pos": [ + 1590, + -60 + ], + "size": [ + 230, + 100 + ], + "flags": { + "collapsed": false + }, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 128 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 76 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 110 + ] + } + ], + "properties": { + "Node name for S&R": "VAEDecode", + "cnr_id": "comfy-core", + "ver": "0.3.48", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + } + }, + { + "id": 89, + "type": "LoraLoaderModelOnly", + "pos": [ + 320, + 300 + ], + "size": [ + 300, + 140 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 184 + }, + { + "localized_name": "lora_name", + "name": "lora_name", + "type": "COMBO", + "widget": { + "name": "lora_name" + }, + "link": null + }, + { + "localized_name": "strength_model", + "name": "strength_model", + "type": "FLOAT", + "widget": { + "name": "strength_model" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 709 + ] + } + ], + "properties": { + "Node name for S&R": "LoraLoaderModelOnly", + "cnr_id": "comfy-core", + "ver": "0.3.50", + "models": [ + { + "name": "Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors", + "url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-2509/Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors", + "directory": "loras" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": { + "lora_name": true, + "strength_model": true + } + } + }, + "widgets_values": [ + "Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors", + 1 + ] + }, + { + "id": 117, + "type": "FluxKontextImageScale", + "pos": [ + -680, + 630 + ], + "size": [ + 230, + 80 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 248 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 249, + 250, + 251 + ] + } + ], + "properties": { + "Node name for S&R": "FluxKontextImageScale" + } + }, + { + "id": 3, + "type": "KSampler", + "pos": [ + 1070, + 210 + ], + "size": [ + 300, + 590 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 186 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 211 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 210 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 246 + }, + { + "localized_name": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": 718 + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": 707 + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": 706 + }, + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 128 + ] + } + ], + "properties": { + "Node name for S&R": "KSampler", + "cnr_id": "comfy-core", + "ver": "0.3.48", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + 973414316252139, + "randomize", + 4, + 1, + "euler", + "simple", + 1 + ] + }, + { + "id": 436, + "type": "PrimitiveInt", + "pos": [ + 320, + 500 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 713 + ] + } + ], + "title": "Steps", + "properties": { + "Node name for S&R": "PrimitiveInt" + }, + "widgets_values": [ + 4, + "fixed" + ] + }, + { + "id": 437, + "type": "PrimitiveFloat", + "pos": [ + 320, + 670 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "FLOAT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 714 + ] + } + ], + "title": "CFG", + "properties": { + "Node name for S&R": "PrimitiveFloat" + }, + "widgets_values": [ + 1 + ] + }, + { + "id": 438, + "type": "PrimitiveInt", + "pos": [ + 320, + -100 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 711 + ] + } + ], + "title": "Steps", + "properties": { + "Node name for S&R": "PrimitiveInt" + }, + "widgets_values": [ + 20, + "fixed" + ] + }, + { + "id": 439, + "type": "PrimitiveFloat", + "pos": [ + 320, + 70 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "FLOAT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 712 + ] + } + ], + "title": "CFG", + "properties": { + "Node name for S&R": "PrimitiveFloat" + }, + "widgets_values": [ + 4 + ] + }, + { + "id": 440, + "type": "ComfySwitchNode", + "pos": [ + 750, + -80 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 710 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 709 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 715 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 708 + ] + } + ], + "title": "Switch (Model)", + "properties": { + "Node name for S&R": "ComfySwitchNode" + }, + "widgets_values": [ + false + ] + }, + { + "id": 441, + "type": "ComfySwitchNode", + "pos": [ + 730, + 340 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 711 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 713 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 716 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 707 + ] + } + ], + "title": "Switch (Steps)", + "properties": { + "Node name for S&R": "ComfySwitchNode" + }, + "widgets_values": [ + false + ] + }, + { + "id": 442, + "type": "ComfySwitchNode", + "pos": [ + 730, + 520 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 712 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 714 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 717 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 706 + ] + } + ], + "title": "Switch (CFG)", + "properties": { + "Node name for S&R": "ComfySwitchNode" + }, + "widgets_values": [ + false + ] + }, + { + "id": 443, + "type": "PrimitiveBoolean", + "pos": [ + 330, + 850 + ], + "size": [ + 270, + 100 + ], + "flags": {}, + "order": 20, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 719 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 715, + 716, + 717 + ] + } + ], + "title": "Enable Lightning LoRA", + "properties": { + "Node name for S&R": "PrimitiveBoolean" + }, + "widgets_values": [ + true + ] + }, + { + "id": 444, + "type": "MarkdownNote", + "pos": [ + 240, + -500 + ], + "size": [ + 450, + 310 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [], + "outputs": [], + "title": "Note: KSampler settings", + "properties": {}, + "widgets_values": [ + "You can test and find the best setting by yourself. The following table is for reference.\n| Parameters | Qwen Team | Comfy Original | with 4steps LoRA |\n|--------|---------|------------|---------------------------|\n| Steps | 50 | 20 | 4 |\n| CFG | 4.0 | 2.5 | 1.0 |" + ], + "color": "#432", + "bgcolor": "#000" + } + ], + "groups": [ + { + "id": 1, + "title": "Step1 - Load models", + "bounding": [ + -770, + -170, + 410, + 750 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 3, + "title": "Step 4 - Prompt", + "bounding": [ + -330, + -170, + 570, + 750 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 50, + "title": "Lightning LoRA", + "bounding": [ + 270, + 220, + 390, + 570 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 51, + "title": "Original Settings", + "bounding": [ + 270, + -170, + 390, + 360 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "links": [ + { + "id": 141, + "origin_id": 66, + "origin_slot": 0, + "target_id": 75, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 128, + "origin_id": 3, + "origin_slot": 0, + "target_id": 8, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 76, + "origin_id": 39, + "origin_slot": 0, + "target_id": 8, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 184, + "origin_id": 37, + "origin_slot": 0, + "target_id": 89, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 186, + "origin_id": 75, + "origin_slot": 0, + "target_id": 3, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 211, + "origin_id": 111, + "origin_slot": 0, + "target_id": 3, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 210, + "origin_id": 110, + "origin_slot": 0, + "target_id": 3, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 168, + "origin_id": 39, + "origin_slot": 0, + "target_id": 88, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 204, + "origin_id": 38, + "origin_slot": 0, + "target_id": 110, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 206, + "origin_id": 39, + "origin_slot": 0, + "target_id": 110, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 205, + "origin_id": 38, + "origin_slot": 0, + "target_id": 111, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 207, + "origin_id": 39, + "origin_slot": 0, + "target_id": 111, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 110, + "origin_id": 8, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 235, + "origin_id": -10, + "origin_slot": 1, + "target_id": 111, + "target_slot": 3, + "type": "IMAGE" + }, + { + "id": 236, + "origin_id": -10, + "origin_slot": 1, + "target_id": 110, + "target_slot": 3, + "type": "IMAGE" + }, + { + "id": 237, + "origin_id": -10, + "origin_slot": 2, + "target_id": 111, + "target_slot": 4, + "type": "IMAGE" + }, + { + "id": 238, + "origin_id": -10, + "origin_slot": 2, + "target_id": 110, + "target_slot": 4, + "type": "IMAGE" + }, + { + "id": 244, + "origin_id": -10, + "origin_slot": 3, + "target_id": 111, + "target_slot": 5, + "type": "STRING" + }, + { + "id": 246, + "origin_id": 88, + "origin_slot": 0, + "target_id": 3, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 248, + "origin_id": -10, + "origin_slot": 0, + "target_id": 117, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 249, + "origin_id": 117, + "origin_slot": 0, + "target_id": 88, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 250, + "origin_id": 117, + "origin_slot": 0, + "target_id": 111, + "target_slot": 2, + "type": "IMAGE" + }, + { + "id": 251, + "origin_id": 117, + "origin_slot": 0, + "target_id": 110, + "target_slot": 2, + "type": "IMAGE" + }, + { + "id": 706, + "origin_id": 442, + "origin_slot": 0, + "target_id": 3, + "target_slot": 6, + "type": "FLOAT" + }, + { + "id": 707, + "origin_id": 441, + "origin_slot": 0, + "target_id": 3, + "target_slot": 5, + "type": "INT" + }, + { + "id": 708, + "origin_id": 440, + "origin_slot": 0, + "target_id": 66, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 709, + "origin_id": 89, + "origin_slot": 0, + "target_id": 440, + "target_slot": 1, + "type": "MODEL" + }, + { + "id": 710, + "origin_id": 37, + "origin_slot": 0, + "target_id": 440, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 711, + "origin_id": 438, + "origin_slot": 0, + "target_id": 441, + "target_slot": 0, + "type": "INT" + }, + { + "id": 712, + "origin_id": 439, + "origin_slot": 0, + "target_id": 442, + "target_slot": 0, + "type": "FLOAT" + }, + { + "id": 713, + "origin_id": 436, + "origin_slot": 0, + "target_id": 441, + "target_slot": 1, + "type": "INT" + }, + { + "id": 714, + "origin_id": 437, + "origin_slot": 0, + "target_id": 442, + "target_slot": 1, + "type": "FLOAT" + }, + { + "id": 715, + "origin_id": 443, + "origin_slot": 0, + "target_id": 440, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 716, + "origin_id": 443, + "origin_slot": 0, + "target_id": 441, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 717, + "origin_id": 443, + "origin_slot": 0, + "target_id": 442, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 718, + "origin_id": -10, + "origin_slot": 4, + "target_id": 3, + "target_slot": 4, + "type": "INT" + }, + { + "id": 719, + "origin_id": -10, + "origin_slot": 5, + "target_id": 443, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 720, + "origin_id": -10, + "origin_slot": 6, + "target_id": 37, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 721, + "origin_id": -10, + "origin_slot": 7, + "target_id": 38, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 722, + "origin_id": -10, + "origin_slot": 8, + "target_id": 39, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": { + "workflowRendererVersion": "LG" + }, + "category": "Image generation and editing/Edit image", + "description": "Edits images from text instructions using Qwen-Image-Edit-2509 with optional Lightning LoRA for few-step sampling." + } + ] + }, + "extra": {} +} diff --git a/blueprints/Image Segmentation (SAM3).json b/blueprints/Image Segmentation (SAM3).json new file mode 100644 index 000000000..b405bf623 --- /dev/null +++ b/blueprints/Image Segmentation (SAM3).json @@ -0,0 +1,714 @@ +{ + "revision": 0, + "last_node_id": 99, + "last_link_id": 0, + "nodes": [ + { + "id": 99, + "type": "6e7ab3ea-96aa-470f-9b94-3d9d0e01f481", + "pos": [ + -1630, + -3270 + ], + "size": [ + 290, + 370 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "label": "image", + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": null + }, + { + "label": "object", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "name": "bboxes", + "type": "BOUNDING_BOX", + "link": null + }, + { + "name": "positive_coords", + "type": "STRING", + "link": null + }, + { + "name": "negative_coords", + "type": "STRING", + "link": null + }, + { + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": null + }, + { + "name": "refine_iterations", + "type": "INT", + "widget": { + "name": "refine_iterations" + }, + "link": null + }, + { + "name": "individual_masks", + "type": "BOOLEAN", + "widget": { + "name": "individual_masks" + }, + "link": null + }, + { + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "masks", + "name": "masks", + "type": "MASK", + "links": [] + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "78", + "text" + ], + [ + "75", + "threshold" + ], + [ + "75", + "refine_iterations" + ], + [ + "75", + "individual_masks" + ], + [ + "77", + "ckpt_name" + ] + ], + "ue_properties": { + "widget_ue_connectable": { + "text": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Image Segmentation (SAM3)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "6e7ab3ea-96aa-470f-9b94-3d9d0e01f481", + "version": 1, + "state": { + "lastGroupId": 0, + "lastNodeId": 113, + "lastLinkId": 283, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Image Segmentation (SAM3)", + "inputNode": { + "id": -10, + "bounding": [ + -2260, + -3450, + 136.369140625, + 220 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -1130, + -3305, + 120, + 80 + ] + }, + "inputs": [ + { + "id": "a6e75fa2-162a-4af0-a2fd-1e9c899a5ab6", + "name": "image", + "type": "IMAGE", + "linkIds": [ + 264 + ], + "localized_name": "image", + "label": "image", + "pos": [ + -2143.630859375, + -3430 + ] + }, + { + "id": "3cefd304-7631-4ff6-a5a0-5a0ffb120745", + "name": "text", + "type": "STRING", + "linkIds": [ + 265 + ], + "label": "object", + "pos": [ + -2143.630859375, + -3410 + ] + }, + { + "id": "1aec91c5-d8d2-441c-928c-49c14e7e80ed", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 266 + ], + "pos": [ + -2143.630859375, + -3390 + ] + }, + { + "id": "1ec7ce1a-8257-4719-8a81-60ebc8a98899", + "name": "positive_coords", + "type": "STRING", + "linkIds": [ + 267 + ], + "pos": [ + -2143.630859375, + -3370 + ] + }, + { + "id": "c65f8b87-9bd7-48be-9fc2-823431e95019", + "name": "negative_coords", + "type": "STRING", + "linkIds": [ + 268 + ], + "pos": [ + -2143.630859375, + -3350 + ] + }, + { + "id": "bb4ba35a-ccfe-4c37-98e5-d9b0d69585fb", + "name": "threshold", + "type": "FLOAT", + "linkIds": [ + 269 + ], + "pos": [ + -2143.630859375, + -3330 + ] + }, + { + "id": "b1439668-b050-490b-a5dc-fc4052c55666", + "name": "refine_iterations", + "type": "INT", + "linkIds": [ + 270 + ], + "pos": [ + -2143.630859375, + -3310 + ] + }, + { + "id": "86e239e5-c098-4302-b54d-d42a38bc0f89", + "name": "individual_masks", + "type": "BOOLEAN", + "linkIds": [ + 271 + ], + "pos": [ + -2143.630859375, + -3290 + ] + }, + { + "id": "f9e0b9d4-b2f1-4907-a4a5-305656576706", + "name": "ckpt_name", + "type": "COMBO", + "linkIds": [ + 272 + ], + "pos": [ + -2143.630859375, + -3270 + ] + } + ], + "outputs": [ + { + "id": "ff50da09-1e59-4a58-9b7f-be1a00aa5913", + "name": "masks", + "type": "MASK", + "linkIds": [ + 231 + ], + "localized_name": "masks", + "pos": [ + -1110, + -3285 + ] + }, + { + "id": "8f622e40-8528-4078-b7d3-147e9f872194", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 232 + ], + "localized_name": "bboxes", + "pos": [ + -1110, + -3265 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 75, + "type": "SAM3_Detect", + "pos": [ + -1470, + -3460 + ], + "size": [ + 270, + 260 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "label": "model", + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 237 + }, + { + "label": "image", + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 264 + }, + { + "label": "conditioning", + "localized_name": "conditioning", + "name": "conditioning", + "shape": 7, + "type": "CONDITIONING", + "link": 200 + }, + { + "label": "bboxes", + "localized_name": "bboxes", + "name": "bboxes", + "shape": 7, + "type": "BOUNDING_BOX", + "link": 266 + }, + { + "label": "positive_coords", + "localized_name": "positive_coords", + "name": "positive_coords", + "shape": 7, + "type": "STRING", + "link": 267 + }, + { + "label": "negative_coords", + "localized_name": "negative_coords", + "name": "negative_coords", + "shape": 7, + "type": "STRING", + "link": 268 + }, + { + "localized_name": "threshold", + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": 269 + }, + { + "localized_name": "refine_iterations", + "name": "refine_iterations", + "type": "INT", + "widget": { + "name": "refine_iterations" + }, + "link": 270 + }, + { + "localized_name": "individual_masks", + "name": "individual_masks", + "type": "BOOLEAN", + "widget": { + "name": "individual_masks" + }, + "link": 271 + } + ], + "outputs": [ + { + "localized_name": "masks", + "name": "masks", + "type": "MASK", + "links": [ + 231 + ] + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [ + 232 + ] + } + ], + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "cnr_id": "comfy-core", + "ver": "0.19.3", + "Node name for S&R": "SAM3_Detect", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0.5, + 2, + false + ] + }, + { + "id": 77, + "type": "CheckpointLoaderSimple", + "pos": [ + -1970, + -3200 + ], + "size": [ + 330, + 140 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 272 + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 237 + ] + }, + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 240 + ] + }, + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": null + } + ], + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "cnr_id": "comfy-core", + "ver": "0.19.3", + "Node name for S&R": "CheckpointLoaderSimple", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "sam3.1_multiplex_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/sam3.1/resolve/main/checkpoints/sam3.1_multiplex_fp16.safetensors", + "directory": "checkpoints" + } + ] + }, + "widgets_values": [ + "sam3.1_multiplex_fp16.safetensors" + ] + }, + { + "id": 78, + "type": "CLIPTextEncode", + "pos": [ + -2000, + -3000 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 240 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 265 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 200 + ] + } + ], + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "cnr_id": "comfy-core", + "ver": "0.19.3", + "Node name for S&R": "CLIPTextEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ] + } + ], + "groups": [], + "links": [ + { + "id": 237, + "origin_id": 77, + "origin_slot": 0, + "target_id": 75, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 200, + "origin_id": 78, + "origin_slot": 0, + "target_id": 75, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 240, + "origin_id": 77, + "origin_slot": 1, + "target_id": 78, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 231, + "origin_id": 75, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "MASK" + }, + { + "id": 232, + "origin_id": 75, + "origin_slot": 1, + "target_id": -20, + "target_slot": 1, + "type": "BOUNDING_BOX" + }, + { + "id": 264, + "origin_id": -10, + "origin_slot": 0, + "target_id": 75, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 265, + "origin_id": -10, + "origin_slot": 1, + "target_id": 78, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 266, + "origin_id": -10, + "origin_slot": 2, + "target_id": 75, + "target_slot": 3, + "type": "BOUNDING_BOX" + }, + { + "id": 267, + "origin_id": -10, + "origin_slot": 3, + "target_id": 75, + "target_slot": 4, + "type": "STRING" + }, + { + "id": 268, + "origin_id": -10, + "origin_slot": 4, + "target_id": 75, + "target_slot": 5, + "type": "STRING" + }, + { + "id": 269, + "origin_id": -10, + "origin_slot": 5, + "target_id": 75, + "target_slot": 6, + "type": "FLOAT" + }, + { + "id": 270, + "origin_id": -10, + "origin_slot": 6, + "target_id": 75, + "target_slot": 7, + "type": "INT" + }, + { + "id": 271, + "origin_id": -10, + "origin_slot": 7, + "target_id": 75, + "target_slot": 8, + "type": "BOOLEAN" + }, + { + "id": 272, + "origin_id": -10, + "origin_slot": 8, + "target_id": 77, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": {}, + "category": "Image Tools/Image Segmentation", + "description": "Segments images into masks using Meta SAM3 from text prompts, points, or boxes." + } + ] + }, + "extra": { + "ue_links": [] + } +} diff --git a/blueprints/Image to Video (Wan 2.2).json b/blueprints/Image to Video (Wan 2.2).json index 3510aad18..a24adcfb6 100644 --- a/blueprints/Image to Video (Wan 2.2).json +++ b/blueprints/Image to Video (Wan 2.2).json @@ -2028,7 +2028,7 @@ "workflowRendererVersion": "LG" }, "category": "Video generation and editing/Image to video", - "description": "Generates video from an image and text prompt using Wan 2.2, supporting T2V and I2V." + "description": "Image-to-video with Wan 2.2 using a start image plus text prompt to extend motion from the still frame." } ] }, diff --git a/blueprints/Remove Background (BiRefNet).json b/blueprints/Remove Background (BiRefNet).json new file mode 100644 index 000000000..732a4adc4 --- /dev/null +++ b/blueprints/Remove Background (BiRefNet).json @@ -0,0 +1,397 @@ +{ + "revision": 0, + "last_node_id": 19, + "last_link_id": 0, + "nodes": [ + { + "id": 19, + "type": "5b40ca21-ba1a-41d5-b403-4d2d7acdc195", + "pos": [ + -6411.330578108367, + 1940.2638932730042 + ], + "size": [ + 349.609375, + 145.9375 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": null + }, + { + "name": "bg_removal_name", + "type": "COMBO", + "widget": { + "name": "bg_removal_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + }, + { + "name": "mask", + "type": "MASK", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "14", + "bg_removal_name" + ] + ] + }, + "widgets_values": [], + "title": "Remove Background (BiRefNet)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "5b40ca21-ba1a-41d5-b403-4d2d7acdc195", + "version": 1, + "state": { + "lastGroupId": 0, + "lastNodeId": 21, + "lastLinkId": 16, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Remove Background (BiRefNet)", + "description": "Removes or replaces image backgrounds using BiRefNet segmentation and alpha compositing.", + "inputNode": { + "id": -10, + "bounding": [ + -6728.534070722246, + 1475.2619799128663, + 150.9140625, + 88 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -6169.049695722246, + 1475.2619799128663, + 128, + 88 + ] + }, + "inputs": [ + { + "id": "7bc321cd-df31-4c39-aaf7-7f0d01326189", + "name": "image", + "type": "IMAGE", + "linkIds": [ + 5, + 7 + ], + "localized_name": "image", + "pos": [ + -6601.620008222246, + 1499.2619799128663 + ] + }, + { + "id": "e89d2cd8-daa3-4e29-8a69-851db85072cb", + "name": "bg_removal_name", + "type": "COMBO", + "linkIds": [ + 12 + ], + "pos": [ + -6601.620008222246, + 1519.2619799128663 + ] + } + ], + "outputs": [ + { + "id": "16e7863c-4c38-46c2-aa74-e82991fbfe8d", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 8 + ], + "localized_name": "IMAGE", + "pos": [ + -6145.049695722246, + 1499.2619799128663 + ] + }, + { + "id": "f7240c19-5b80-406e-a8e2-9b12440ee2d6", + "name": "mask", + "type": "MASK", + "linkIds": [ + 11 + ], + "pos": [ + -6145.049695722246, + 1519.2619799128663 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 13, + "type": "RemoveBackground", + "pos": [ + -6536.764823982709, + 1444.9963409012412 + ], + "size": [ + 302.25, + 72 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 5 + }, + { + "localized_name": "bg_removal_model", + "name": "bg_removal_model", + "type": "BACKGROUND_REMOVAL", + "link": 3 + } + ], + "outputs": [ + { + "localized_name": "mask", + "name": "mask", + "type": "MASK", + "links": [ + 4, + 11 + ] + } + ], + "properties": { + "Node name for S&R": "RemoveBackground" + } + }, + { + "id": 14, + "type": "LoadBackgroundRemovalModel", + "pos": [ + -6540.534070722246, + 1302.223464635445 + ], + "size": [ + 311.484375, + 85.515625 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "bg_removal_name", + "name": "bg_removal_name", + "type": "COMBO", + "widget": { + "name": "bg_removal_name" + }, + "link": 12 + } + ], + "outputs": [ + { + "localized_name": "bg_model", + "name": "bg_model", + "type": "BACKGROUND_REMOVAL", + "links": [ + 3 + ] + } + ], + "properties": { + "Node name for S&R": "LoadBackgroundRemovalModel", + "models": [ + { + "name": "birefnet.safetensors", + "url": "https://huggingface.co/Comfy-Org/BiRefNet/resolve/main/background_removal/birefnet.safetensors", + "directory": "background_removal" + } + ] + }, + "widgets_values": [ + "birefnet.safetensors" + ] + }, + { + "id": 15, + "type": "InvertMask", + "pos": [ + -6532.446160529669, + 1571.1111286839914 + ], + "size": [ + 285.984375, + 48 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "mask", + "name": "mask", + "type": "MASK", + "link": 4 + } + ], + "outputs": [ + { + "localized_name": "MASK", + "name": "MASK", + "type": "MASK", + "links": [ + 6 + ] + } + ], + "properties": { + "Node name for S&R": "InvertMask" + } + }, + { + "id": 16, + "type": "JoinImageWithAlpha", + "pos": [ + -6527.4370171636665, + 1674.3004951902876 + ], + "size": [ + 284.96875, + 72 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 7 + }, + { + "localized_name": "alpha", + "name": "alpha", + "type": "MASK", + "link": 6 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 8 + ] + } + ], + "properties": { + "Node name for S&R": "JoinImageWithAlpha" + } + } + ], + "groups": [], + "links": [ + { + "id": 3, + "origin_id": 14, + "origin_slot": 0, + "target_id": 13, + "target_slot": 1, + "type": "BACKGROUND_REMOVAL" + }, + { + "id": 4, + "origin_id": 13, + "origin_slot": 0, + "target_id": 15, + "target_slot": 0, + "type": "MASK" + }, + { + "id": 6, + "origin_id": 15, + "origin_slot": 0, + "target_id": 16, + "target_slot": 1, + "type": "MASK" + }, + { + "id": 5, + "origin_id": -10, + "origin_slot": 0, + "target_id": 13, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 7, + "origin_id": -10, + "origin_slot": 0, + "target_id": 16, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 8, + "origin_id": 16, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 11, + "origin_id": 13, + "origin_slot": 0, + "target_id": -20, + "target_slot": 1, + "type": "MASK" + }, + { + "id": 12, + "origin_id": -10, + "origin_slot": 1, + "target_id": 14, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": {}, + "category": "Image generation and editing/Background Removal" + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Text to Image (Ernie Image Turbo).json b/blueprints/Text to Image (Ernie Image Turbo).json new file mode 100644 index 000000000..4ecdd1883 --- /dev/null +++ b/blueprints/Text to Image (Ernie Image Turbo).json @@ -0,0 +1,2112 @@ +{ + "revision": 0, + "last_node_id": 88, + "last_link_id": 0, + "nodes": [ + { + "id": 88, + "type": "2a4f0815-c4d2-4e8b-9bdf-991a8403889d", + "pos": [ + -120, + 240 + ], + "size": [ + 400, + 540 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "label": "prompt", + "name": "value", + "type": "STRING", + "widget": { + "name": "value" + }, + "link": null + }, + { + "label": "prompt_enhancement", + "name": "value_1", + "type": "BOOLEAN", + "widget": { + "name": "value_1" + }, + "link": null + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": null + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": null + }, + { + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "label": "prompt_enhancer", + "name": "clip_name_1", + "type": "COMBO", + "widget": { + "name": "clip_name_1" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "94", + "value" + ], + [ + "96", + "value" + ], + [ + "71", + "width" + ], + [ + "71", + "height" + ], + [ + "70", + "seed" + ], + [ + "66", + "unet_name" + ], + [ + "62", + "clip_name" + ], + [ + "98", + "clip_name" + ], + [ + "63", + "vae_name" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": { + "value": true, + "value_1": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [], + "title": "Text to Image (Ernie Image Turbo)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "2a4f0815-c4d2-4e8b-9bdf-991a8403889d", + "version": 1, + "state": { + "lastGroupId": 7, + "lastNodeId": 103, + "lastLinkId": 134, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Text to Image (Ernie Image Turbo)", + "inputNode": { + "id": -10, + "bounding": [ + -1350, + 370, + 163.50390625, + 220 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1110, + 260, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "74a4609c-67df-4ae9-ab96-9ff4e3a1c3b1", + "name": "value", + "type": "STRING", + "linkIds": [ + 128 + ], + "label": "prompt", + "pos": [ + -1206.49609375, + 390 + ] + }, + { + "id": "996f1854-7ae3-450e-821c-a9b5b7c310f9", + "name": "value_1", + "type": "BOOLEAN", + "linkIds": [ + 127 + ], + "label": "prompt_enhancement", + "pos": [ + -1206.49609375, + 410 + ] + }, + { + "id": "71e9c6e8-4285-4543-b1d3-81520088f6a4", + "name": "width", + "type": "INT", + "linkIds": [ + 104, + 129 + ], + "pos": [ + -1206.49609375, + 430 + ] + }, + { + "id": "bdb6cd97-67d9-440c-8c4c-9b7a7540edd0", + "name": "height", + "type": "INT", + "linkIds": [ + 105, + 130 + ], + "pos": [ + -1206.49609375, + 450 + ] + }, + { + "id": "18abb56c-30bf-4de5-83c1-c12376e8d14e", + "name": "seed", + "type": "INT", + "linkIds": [ + 108 + ], + "pos": [ + -1206.49609375, + 470 + ] + }, + { + "id": "e5cd06f9-64ed-4778-97ba-b165f7a79c4e", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 109 + ], + "pos": [ + -1206.49609375, + 490 + ] + }, + { + "id": "06480e4c-4043-489b-ae68-1cf2b4246260", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 110 + ], + "pos": [ + -1206.49609375, + 510 + ] + }, + { + "id": "8d65d01b-16b2-420d-8b7b-42077c2e4976", + "name": "clip_name_1", + "type": "COMBO", + "linkIds": [ + 132 + ], + "label": "prompt_enhancer", + "pos": [ + -1206.49609375, + 530 + ] + }, + { + "id": "697f2fdb-0fd9-4008-a895-0f9ce9e8fd88", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 133 + ], + "pos": [ + -1206.49609375, + 550 + ] + } + ], + "outputs": [ + { + "id": "21d5fbe0-9f91-4d93-8ea8-5bbf2cd5b698", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 84 + ], + "localized_name": "IMAGE", + "pos": [ + 1130, + 280 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 71, + "type": "EmptyFlux2LatentImage", + "pos": [ + -470, + 1050 + ], + "size": [ + 270, + 170 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 104 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 105 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 80 + ] + } + ], + "properties": { + "Node name for S&R": "EmptyFlux2LatentImage", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 1024, + 1024, + 1 + ] + }, + { + "id": 66, + "type": "UNETLoader", + "pos": [ + -470, + 320 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 109 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 85 + ] + } + ], + "properties": { + "Node name for S&R": "UNETLoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "ernie-image-turbo.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/diffusion_models/ernie-image-turbo.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "ernie-image-turbo.safetensors", + "default" + ] + }, + { + "id": 65, + "type": "VAEDecode", + "pos": [ + 710, + 280 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 73 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 74 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 84 + ] + } + ], + "properties": { + "Node name for S&R": "VAEDecode", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + } + }, + { + "id": 70, + "type": "KSampler", + "pos": [ + 350, + 280 + ], + "size": [ + 320, + 350 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 85 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 76 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 113 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 80 + }, + { + "localized_name": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": 108 + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + }, + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 73 + ] + } + ], + "properties": { + "Node name for S&R": "KSampler", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 423299999918804, + "randomize", + 8, + 1, + "euler", + "simple", + 1 + ] + }, + { + "id": 67, + "type": "CLIPTextEncode", + "pos": [ + -140, + 320 + ], + "size": [ + 410, + 370 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 79 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 131 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 76, + 112 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 62, + "type": "CLIPLoader", + "pos": [ + -470, + 530 + ], + "size": [ + 270, + 150 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 110 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 79 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "ministral-3-3b.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/text_encoders/ministral-3-3b.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "ministral-3-3b.safetensors", + "flux2", + "default" + ] + }, + { + "id": 63, + "type": "VAELoader", + "pos": [ + -470, + 780 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 133 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 74 + ] + } + ], + "properties": { + "Node name for S&R": "VAELoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "flux2-vae.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/vae/flux2-vae.safetensors", + "directory": "vae" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "flux2-vae.safetensors" + ] + }, + { + "id": 91, + "type": "ConditioningZeroOut", + "pos": [ + 30, + 760 + ], + "size": [ + 230, + 80 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 112 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 113 + ] + } + ], + "properties": { + "Node name for S&R": "ConditioningZeroOut", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + } + }, + { + "id": 93, + "type": "StringReplace", + "pos": [ + -500, + -650 + ], + "size": [ + 430, + 450 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": null + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 115 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 121 + ] + } + ], + "properties": { + "Node name for S&R": "StringReplace", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "[SYSTEM_PROMPT]你是一个专业的文生图 Prompt 增强助手。你将收到用户的简短图片描述及目标生成分辨率,请据此扩写为一段内容丰富、细节充分的视觉描述,以帮助文生图模型生成高质量的图片。仅输出增强后的描述,不要包含任何解释或前缀。[/SYSTEM_PROMPT][INST]{\"prompt\": \"{prompt}\", \"width\": {width}, \"height\": {height}}[/INST]", + "{prompt}", + "" + ] + }, + { + "id": 94, + "type": "PrimitiveStringMultiline", + "pos": [ + -950, + -660 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "STRING", + "widget": { + "name": "value" + }, + "link": 128 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 115, + 118 + ] + } + ], + "title": "String (Multiline - Prompt)", + "properties": { + "Node name for S&R": "PrimitiveStringMultiline", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "" + ] + }, + { + "id": 95, + "type": "TextGenerate", + "pos": [ + 530, + -660 + ], + "size": [ + 400, + 380 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 116 + }, + { + "localized_name": "image", + "name": "image", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "localized_name": "prompt", + "name": "prompt", + "type": "STRING", + "widget": { + "name": "prompt" + }, + "link": 117 + }, + { + "localized_name": "max_length", + "name": "max_length", + "type": "INT", + "widget": { + "name": "max_length" + }, + "link": null + }, + { + "localized_name": "sampling_mode", + "name": "sampling_mode", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "sampling_mode" + }, + "link": null + }, + { + "localized_name": "temperature", + "name": "sampling_mode.temperature", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.temperature" + }, + "link": null + }, + { + "localized_name": "top_k", + "name": "sampling_mode.top_k", + "type": "INT", + "widget": { + "name": "sampling_mode.top_k" + }, + "link": null + }, + { + "localized_name": "top_p", + "name": "sampling_mode.top_p", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.top_p" + }, + "link": null + }, + { + "localized_name": "min_p", + "name": "sampling_mode.min_p", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.min_p" + }, + "link": null + }, + { + "localized_name": "repetition_penalty", + "name": "sampling_mode.repetition_penalty", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.repetition_penalty" + }, + "link": null + }, + { + "localized_name": "seed", + "name": "sampling_mode.seed", + "type": "INT", + "widget": { + "name": "sampling_mode.seed" + }, + "link": null + }, + { + "localized_name": "sampling_mode.presence_penalty", + "name": "sampling_mode.presence_penalty", + "shape": 7, + "type": "FLOAT", + "widget": { + "name": "sampling_mode.presence_penalty" + }, + "link": null + }, + { + "localized_name": "thinking", + "name": "thinking", + "shape": 7, + "type": "BOOLEAN", + "widget": { + "name": "thinking" + }, + "link": null + }, + { + "localized_name": "use_default_template", + "name": "use_default_template", + "shape": 7, + "type": "BOOLEAN", + "widget": { + "name": "use_default_template" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "generated_text", + "name": "generated_text", + "type": "STRING", + "links": [ + 119 + ] + } + ], + "properties": { + "Node name for S&R": "TextGenerate", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "", + 2048, + "on", + 0.6, + 64, + 0.8, + 0.05, + 1.05, + 0, + 0, + false, + true + ] + }, + { + "id": 96, + "type": "PrimitiveBoolean", + "pos": [ + -490, + 60 + ], + "size": [ + 270, + 100 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 127 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 120 + ] + } + ], + "title": "Enable prompt enhancement?", + "properties": { + "Node name for S&R": "PrimitiveBoolean", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + true + ] + }, + { + "id": 97, + "type": "ComfySwitchNode", + "pos": [ + 550, + -10 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 118 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 119 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 120 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 131, + 134 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + false + ] + }, + { + "id": 98, + "type": "CLIPLoader", + "pos": [ + -490, + -150 + ], + "size": [ + 510, + 150 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 132 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 116 + ] + } + ], + "title": "Load CLIP (PE)", + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "models": [ + { + "name": "ernie-image-prompt-enhancer.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/text_encoders/ernie-image-prompt-enhancer.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "ernie-image-prompt-enhancer.safetensors", + "flux2", + "default" + ] + }, + { + "id": 99, + "type": "PreviewAny", + "pos": [ + -950, + -410 + ], + "size": [ + 400, + 180 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 129 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 122 + ] + } + ], + "title": "Preview as Text (Int to String)", + "properties": { + "Node name for S&R": "PreviewAny", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + null, + null, + null + ] + }, + { + "id": 100, + "type": "PreviewAny", + "pos": [ + -950, + -190 + ], + "size": [ + 400, + 180 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 130 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 124 + ] + } + ], + "title": "Preview as Text (Int to String)", + "properties": { + "Node name for S&R": "PreviewAny", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + null, + null, + null + ] + }, + { + "id": 101, + "type": "StringReplace", + "pos": [ + -30, + -650 + ], + "size": [ + 230, + 450 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": 121 + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 122 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 123 + ] + } + ], + "properties": { + "Node name for S&R": "StringReplace", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "", + "{width}", + "" + ] + }, + { + "id": 102, + "type": "StringReplace", + "pos": [ + 220, + -650 + ], + "size": [ + 250, + 450 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": 123 + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 124 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 117 + ] + } + ], + "properties": { + "Node name for S&R": "StringReplace", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "", + "{height}", + "" + ] + }, + { + "id": 103, + "type": "PreviewAny", + "pos": [ + 970, + -660 + ], + "size": [ + 570, + 790 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 134 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [] + } + ], + "title": "Preview as Text (Int to String)", + "properties": { + "Node name for S&R": "PreviewAny", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + null, + null, + null + ] + } + ], + "groups": [ + { + "id": 6, + "title": "Text to Image", + "bounding": [ + -510, + 200, + 1450, + 1060 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 2, + "title": "Image Size", + "bounding": [ + -490, + 950, + 300, + 290 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 3, + "title": "Prompt", + "bounding": [ + -160, + 250, + 470, + 670 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 4, + "title": "Model", + "bounding": [ + -490, + 250, + 300, + 670 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 7, + "title": "Prompt Enhancement", + "bounding": [ + -510, + -720, + 1450, + 890 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "links": [ + { + "id": 73, + "origin_id": 70, + "origin_slot": 0, + "target_id": 65, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 74, + "origin_id": 63, + "origin_slot": 0, + "target_id": 65, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 85, + "origin_id": 66, + "origin_slot": 0, + "target_id": 70, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 76, + "origin_id": 67, + "origin_slot": 0, + "target_id": 70, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 80, + "origin_id": 71, + "origin_slot": 0, + "target_id": 70, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 79, + "origin_id": 62, + "origin_slot": 0, + "target_id": 67, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 84, + "origin_id": 65, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 104, + "origin_id": -10, + "origin_slot": 2, + "target_id": 71, + "target_slot": 0, + "type": "INT" + }, + { + "id": 105, + "origin_id": -10, + "origin_slot": 3, + "target_id": 71, + "target_slot": 1, + "type": "INT" + }, + { + "id": 108, + "origin_id": -10, + "origin_slot": 4, + "target_id": 70, + "target_slot": 4, + "type": "INT" + }, + { + "id": 109, + "origin_id": -10, + "origin_slot": 5, + "target_id": 66, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 110, + "origin_id": -10, + "origin_slot": 6, + "target_id": 62, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 112, + "origin_id": 67, + "origin_slot": 0, + "target_id": 91, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 113, + "origin_id": 91, + "origin_slot": 0, + "target_id": 70, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 115, + "origin_id": 94, + "origin_slot": 0, + "target_id": 93, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 116, + "origin_id": 98, + "origin_slot": 0, + "target_id": 95, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 117, + "origin_id": 102, + "origin_slot": 0, + "target_id": 95, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 118, + "origin_id": 94, + "origin_slot": 0, + "target_id": 97, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 119, + "origin_id": 95, + "origin_slot": 0, + "target_id": 97, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 120, + "origin_id": 96, + "origin_slot": 0, + "target_id": 97, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 121, + "origin_id": 93, + "origin_slot": 0, + "target_id": 101, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 122, + "origin_id": 99, + "origin_slot": 0, + "target_id": 101, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 123, + "origin_id": 101, + "origin_slot": 0, + "target_id": 102, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 124, + "origin_id": 100, + "origin_slot": 0, + "target_id": 102, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 127, + "origin_id": -10, + "origin_slot": 1, + "target_id": 96, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 128, + "origin_id": -10, + "origin_slot": 0, + "target_id": 94, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 129, + "origin_id": -10, + "origin_slot": 2, + "target_id": 99, + "target_slot": 0, + "type": "*" + }, + { + "id": 130, + "origin_id": -10, + "origin_slot": 3, + "target_id": 100, + "target_slot": 0, + "type": "*" + }, + { + "id": 131, + "origin_id": 97, + "origin_slot": 0, + "target_id": 67, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 132, + "origin_id": -10, + "origin_slot": 7, + "target_id": 98, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 133, + "origin_id": -10, + "origin_slot": 8, + "target_id": 63, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 134, + "origin_id": 97, + "origin_slot": 0, + "target_id": 103, + "target_slot": 0, + "type": "STRING" + } + ], + "extra": {}, + "category": "Image generation and editing/Text to image", + "description": "Faster ERNIE Image Turbo variant (~8B DiT, distilled for fewer sampling steps): same strengths in Chinese/English on-image text and layout-heavy graphics as the base ERNIE Image lineup, with bundled encoders and VAE." + } + ] + }, + "extra": { + "ue_links": [] + } +} diff --git a/blueprints/Text to Image (Ernie Image).json b/blueprints/Text to Image (Ernie Image).json new file mode 100644 index 000000000..2bab20d69 --- /dev/null +++ b/blueprints/Text to Image (Ernie Image).json @@ -0,0 +1,2190 @@ +{ + "revision": 0, + "last_node_id": 88, + "last_link_id": 0, + "nodes": [ + { + "id": 88, + "type": "03921aea-a70e-44b4-bc77-f6bda10f2120", + "pos": [ + -120, + 240 + ], + "size": [ + 400, + 540 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "label": "prompt", + "name": "value", + "type": "STRING", + "widget": { + "name": "value" + }, + "link": null + }, + { + "label": "prompt_enhancement", + "name": "value_1", + "type": "BOOLEAN", + "widget": { + "name": "value_1" + }, + "link": null + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": null + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": null + }, + { + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + }, + { + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + }, + { + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "label": "prompt_enhancer", + "name": "clip_name_1", + "type": "COMBO", + "widget": { + "name": "clip_name_1" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "78", + "value" + ], + [ + "76", + "value" + ], + [ + "71", + "width" + ], + [ + "71", + "height" + ], + [ + "70", + "steps" + ], + [ + "70", + "cfg" + ], + [ + "70", + "seed" + ], + [ + "66", + "unet_name" + ], + [ + "62", + "clip_name" + ], + [ + "91", + "clip_name" + ], + [ + "63", + "vae_name" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": { + "value": true, + "value_1": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [], + "title": "Text to Image (Ernie Image)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "03921aea-a70e-44b4-bc77-f6bda10f2120", + "version": 1, + "state": { + "lastGroupId": 6, + "lastNodeId": 99, + "lastLinkId": 124, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Text to Image (Ernie Image)", + "inputNode": { + "id": -10, + "bounding": [ + -1350, + 370, + 163.50390625, + 260 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1110, + 260, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "504de359-52a4-49aa-b6be-23c1cdb0cbde", + "name": "value", + "type": "STRING", + "linkIds": [ + 102 + ], + "label": "prompt", + "pos": [ + -1206.49609375, + 390 + ] + }, + { + "id": "29f699c6-9263-41f6-b37d-69b9fc3913dd", + "name": "value_1", + "type": "BOOLEAN", + "linkIds": [ + 103 + ], + "label": "prompt_enhancement", + "pos": [ + -1206.49609375, + 410 + ] + }, + { + "id": "968e6213-d1e9-4268-8f47-1d6b9a39a43e", + "name": "width", + "type": "INT", + "linkIds": [ + 104, + 113 + ], + "pos": [ + -1206.49609375, + 430 + ] + }, + { + "id": "181c49ef-740d-4385-aa11-79718951ccb9", + "name": "height", + "type": "INT", + "linkIds": [ + 105, + 114 + ], + "pos": [ + -1206.49609375, + 450 + ] + }, + { + "id": "1e85f808-66a1-41df-be52-334142b35419", + "name": "steps", + "type": "INT", + "linkIds": [ + 106 + ], + "pos": [ + -1206.49609375, + 470 + ] + }, + { + "id": "2806addf-a252-4aa3-a5b7-397ab36dccec", + "name": "cfg", + "type": "FLOAT", + "linkIds": [ + 107 + ], + "pos": [ + -1206.49609375, + 490 + ] + }, + { + "id": "5d036a66-5dc0-4d7c-b9a9-349e454738aa", + "name": "seed", + "type": "INT", + "linkIds": [ + 108 + ], + "pos": [ + -1206.49609375, + 510 + ] + }, + { + "id": "360f9a40-aac5-4e9c-bc98-9d55a4a58be2", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 109 + ], + "pos": [ + -1206.49609375, + 530 + ] + }, + { + "id": "886301c7-6e88-4cec-96fa-8ae20e8340c5", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 110 + ], + "pos": [ + -1206.49609375, + 550 + ] + }, + { + "id": "1d73a545-6d01-462f-bc61-966d4b918ff2", + "name": "clip_name_1", + "type": "COMBO", + "linkIds": [ + 120 + ], + "label": "prompt_enhancer", + "pos": [ + -1206.49609375, + 570 + ] + }, + { + "id": "8c61dc8c-e260-4b36-b73e-d36f90a0bbe3", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 121 + ], + "pos": [ + -1206.49609375, + 590 + ] + } + ], + "outputs": [ + { + "id": "f4cb34c8-4090-4281-b428-7338a339d274", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 84 + ], + "localized_name": "IMAGE", + "pos": [ + 1130, + 280 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 71, + "type": "EmptyFlux2LatentImage", + "pos": [ + -460, + 1040 + ], + "size": [ + 270, + 170 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 104 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 105 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 80 + ] + } + ], + "properties": { + "Node name for S&R": "EmptyFlux2LatentImage", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 1024, + 1024, + 1 + ] + }, + { + "id": 66, + "type": "UNETLoader", + "pos": [ + -470, + 320 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 109 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 85 + ] + } + ], + "properties": { + "Node name for S&R": "UNETLoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "ernie-image.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/diffusion_models/ernie-image.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "ernie-image.safetensors", + "default" + ] + }, + { + "id": 65, + "type": "VAEDecode", + "pos": [ + 710, + 280 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 73 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 74 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 84 + ] + } + ], + "properties": { + "Node name for S&R": "VAEDecode", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + } + }, + { + "id": 70, + "type": "KSampler", + "pos": [ + 350, + 280 + ], + "size": [ + 320, + 350 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 85 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 76 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 83 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 80 + }, + { + "localized_name": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": 108 + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": 106 + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": 107 + }, + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 73 + ] + } + ], + "properties": { + "Node name for S&R": "KSampler", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 182596410725960, + "randomize", + 20, + 4, + "euler", + "simple", + 1 + ] + }, + { + "id": 67, + "type": "CLIPTextEncode", + "pos": [ + -140, + 320 + ], + "size": [ + 410, + 370 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 79 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 100 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 76 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 72, + "type": "CLIPTextEncode", + "pos": [ + -130, + 770 + ], + "size": [ + 390, + 140 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 82 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 83 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 83, + "type": "StringReplace", + "pos": [ + -500, + -640 + ], + "size": [ + 430, + 450 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": null + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 92 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 115 + ] + } + ], + "properties": { + "Node name for S&R": "StringReplace", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "[SYSTEM_PROMPT]你是一个专业的文生图 Prompt 增强助手。你将收到用户的简短图片描述及目标生成分辨率,请据此扩写为一段内容丰富、细节充分的视觉描述,以帮助文生图模型生成高质量的图片。仅输出增强后的描述,不要包含任何解释或前缀。[/SYSTEM_PROMPT][INST]{\"prompt\": \"{prompt}\", \"width\": {width}, \"height\": {height}}[/INST]", + "{prompt}", + "" + ] + }, + { + "id": 78, + "type": "PrimitiveStringMultiline", + "pos": [ + -950, + -650 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "STRING", + "widget": { + "name": "value" + }, + "link": 102 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 87, + 92 + ] + } + ], + "title": "String (Multiline - Prompt)", + "properties": { + "Node name for S&R": "PrimitiveStringMultiline", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "" + ] + }, + { + "id": 74, + "type": "TextGenerate", + "pos": [ + 530, + -650 + ], + "size": [ + 400, + 380 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 112 + }, + { + "localized_name": "image", + "name": "image", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "localized_name": "prompt", + "name": "prompt", + "type": "STRING", + "widget": { + "name": "prompt" + }, + "link": 119 + }, + { + "localized_name": "max_length", + "name": "max_length", + "type": "INT", + "widget": { + "name": "max_length" + }, + "link": null + }, + { + "localized_name": "sampling_mode", + "name": "sampling_mode", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "sampling_mode" + }, + "link": null + }, + { + "localized_name": "temperature", + "name": "sampling_mode.temperature", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.temperature" + }, + "link": null + }, + { + "localized_name": "top_k", + "name": "sampling_mode.top_k", + "type": "INT", + "widget": { + "name": "sampling_mode.top_k" + }, + "link": null + }, + { + "localized_name": "top_p", + "name": "sampling_mode.top_p", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.top_p" + }, + "link": null + }, + { + "localized_name": "min_p", + "name": "sampling_mode.min_p", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.min_p" + }, + "link": null + }, + { + "localized_name": "repetition_penalty", + "name": "sampling_mode.repetition_penalty", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.repetition_penalty" + }, + "link": null + }, + { + "localized_name": "seed", + "name": "sampling_mode.seed", + "type": "INT", + "widget": { + "name": "sampling_mode.seed" + }, + "link": null + }, + { + "localized_name": "sampling_mode.presence_penalty", + "name": "sampling_mode.presence_penalty", + "shape": 7, + "type": "FLOAT", + "widget": { + "name": "sampling_mode.presence_penalty" + }, + "link": null + }, + { + "localized_name": "thinking", + "name": "thinking", + "shape": 7, + "type": "BOOLEAN", + "widget": { + "name": "thinking" + }, + "link": null + }, + { + "localized_name": "use_default_template", + "name": "use_default_template", + "shape": 7, + "type": "BOOLEAN", + "widget": { + "name": "use_default_template" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "generated_text", + "name": "generated_text", + "type": "STRING", + "links": [ + 89 + ] + } + ], + "properties": { + "Node name for S&R": "TextGenerate", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "", + 2048, + "on", + 0.6, + 64, + 0.8, + 0.05, + 1.05, + 0, + 0, + false, + true + ] + }, + { + "id": 76, + "type": "PrimitiveBoolean", + "pos": [ + -500, + 60 + ], + "size": [ + 270, + 100 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 103 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 88 + ] + } + ], + "title": "Enable prompt enhancement?", + "properties": { + "Node name for S&R": "PrimitiveBoolean", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + true + ] + }, + { + "id": 75, + "type": "ComfySwitchNode", + "pos": [ + 530, + 20 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 87 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 89 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 88 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 100, + 124 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + false + ] + }, + { + "id": 62, + "type": "CLIPLoader", + "pos": [ + -460, + 520 + ], + "size": [ + 270, + 150 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 110 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 79, + 82 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "ministral-3-3b.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/text_encoders/ministral-3-3b.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "ministral-3-3b.safetensors", + "flux2", + "default" + ] + }, + { + "id": 63, + "type": "VAELoader", + "pos": [ + -460, + 770 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 121 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 74 + ] + } + ], + "properties": { + "Node name for S&R": "VAELoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "flux2-vae.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/vae/flux2-vae.safetensors", + "directory": "vae" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "flux2-vae.safetensors" + ] + }, + { + "id": 91, + "type": "CLIPLoader", + "pos": [ + -500, + -150 + ], + "size": [ + 510, + 150 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 120 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 112 + ] + } + ], + "title": "Load CLIP (PE)", + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "models": [ + { + "name": "ernie-image-prompt-enhancer.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/text_encoders/ernie-image-prompt-enhancer.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "ernie-image-prompt-enhancer.safetensors", + "flux2", + "default" + ] + }, + { + "id": 92, + "type": "PreviewAny", + "pos": [ + -950, + -400 + ], + "size": [ + 400, + 180 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 113 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 116 + ] + } + ], + "title": "Preview as Text (Int to String)", + "properties": { + "Node name for S&R": "PreviewAny", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + null, + null, + null + ] + }, + { + "id": 93, + "type": "PreviewAny", + "pos": [ + -950, + -180 + ], + "size": [ + 400, + 180 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 114 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 118 + ] + } + ], + "title": "Preview as Text (Int to String)", + "properties": { + "Node name for S&R": "PreviewAny", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + null, + null, + null + ] + }, + { + "id": 94, + "type": "StringReplace", + "pos": [ + -30, + -640 + ], + "size": [ + 230, + 450 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": 115 + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 116 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 117 + ] + } + ], + "properties": { + "Node name for S&R": "StringReplace", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "", + "{width}", + "" + ] + }, + { + "id": 95, + "type": "StringReplace", + "pos": [ + 220, + -640 + ], + "size": [ + 250, + 450 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": 117 + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 118 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 119 + ] + } + ], + "properties": { + "Node name for S&R": "StringReplace", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "", + "{height}", + "" + ] + }, + { + "id": 97, + "type": "PreviewAny", + "pos": [ + 970, + -650 + ], + "size": [ + 570, + 790 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 124 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [] + } + ], + "title": "Preview as Text (Int to String)", + "properties": { + "Node name for S&R": "PreviewAny", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + null, + null, + null + ] + } + ], + "groups": [ + { + "id": 6, + "title": "Text to Image", + "bounding": [ + -510, + 200, + 1450, + 1060 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 2, + "title": "Image Size", + "bounding": [ + -480, + 940, + 310, + 290 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 3, + "title": "Prompt", + "bounding": [ + -160, + 250, + 470, + 670 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 4, + "title": "Model", + "bounding": [ + -490, + 250, + 320, + 670 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 5, + "title": "Prompt Enhancement", + "bounding": [ + -510, + -720, + 1450, + 890 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "links": [ + { + "id": 73, + "origin_id": 70, + "origin_slot": 0, + "target_id": 65, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 74, + "origin_id": 63, + "origin_slot": 0, + "target_id": 65, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 85, + "origin_id": 66, + "origin_slot": 0, + "target_id": 70, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 76, + "origin_id": 67, + "origin_slot": 0, + "target_id": 70, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 83, + "origin_id": 72, + "origin_slot": 0, + "target_id": 70, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 80, + "origin_id": 71, + "origin_slot": 0, + "target_id": 70, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 79, + "origin_id": 62, + "origin_slot": 0, + "target_id": 67, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 100, + "origin_id": 75, + "origin_slot": 0, + "target_id": 67, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 82, + "origin_id": 62, + "origin_slot": 0, + "target_id": 72, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 92, + "origin_id": 78, + "origin_slot": 0, + "target_id": 83, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 87, + "origin_id": 78, + "origin_slot": 0, + "target_id": 75, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 89, + "origin_id": 74, + "origin_slot": 0, + "target_id": 75, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 88, + "origin_id": 76, + "origin_slot": 0, + "target_id": 75, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 84, + "origin_id": 65, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 102, + "origin_id": -10, + "origin_slot": 0, + "target_id": 78, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 103, + "origin_id": -10, + "origin_slot": 1, + "target_id": 76, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 104, + "origin_id": -10, + "origin_slot": 2, + "target_id": 71, + "target_slot": 0, + "type": "INT" + }, + { + "id": 105, + "origin_id": -10, + "origin_slot": 3, + "target_id": 71, + "target_slot": 1, + "type": "INT" + }, + { + "id": 106, + "origin_id": -10, + "origin_slot": 4, + "target_id": 70, + "target_slot": 5, + "type": "INT" + }, + { + "id": 107, + "origin_id": -10, + "origin_slot": 5, + "target_id": 70, + "target_slot": 6, + "type": "FLOAT" + }, + { + "id": 108, + "origin_id": -10, + "origin_slot": 6, + "target_id": 70, + "target_slot": 4, + "type": "INT" + }, + { + "id": 109, + "origin_id": -10, + "origin_slot": 7, + "target_id": 66, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 110, + "origin_id": -10, + "origin_slot": 8, + "target_id": 62, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 112, + "origin_id": 91, + "origin_slot": 0, + "target_id": 74, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 113, + "origin_id": -10, + "origin_slot": 2, + "target_id": 92, + "target_slot": 0, + "type": "*" + }, + { + "id": 114, + "origin_id": -10, + "origin_slot": 3, + "target_id": 93, + "target_slot": 0, + "type": "*" + }, + { + "id": 115, + "origin_id": 83, + "origin_slot": 0, + "target_id": 94, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 116, + "origin_id": 92, + "origin_slot": 0, + "target_id": 94, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 117, + "origin_id": 94, + "origin_slot": 0, + "target_id": 95, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 118, + "origin_id": 93, + "origin_slot": 0, + "target_id": 95, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 119, + "origin_id": 95, + "origin_slot": 0, + "target_id": 74, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 120, + "origin_id": -10, + "origin_slot": 9, + "target_id": 91, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 121, + "origin_id": -10, + "origin_slot": 10, + "target_id": 63, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 124, + "origin_id": 75, + "origin_slot": 0, + "target_id": 97, + "target_slot": 0, + "type": "STRING" + } + ], + "extra": {}, + "category": "Image generation and editing/Text to image", + "description": "Generates images from text prompts using Baidu’s open ERNIE Image (~8B DiT): bilingual in-image typography and layouts (posters, infographics, multi-panel compositions) alongside general scenes, with bundled encoders and VAE." + } + ] + }, + "extra": { + "ue_links": [] + } +} diff --git a/blueprints/Text to Image (Flux.1 Dev).json b/blueprints/Text to Image (Flux.1 Dev).json index 45f68f508..6d8446e81 100644 --- a/blueprints/Text to Image (Flux.1 Dev).json +++ b/blueprints/Text to Image (Flux.1 Dev).json @@ -1030,7 +1030,7 @@ "workflowRendererVersion": "LG" }, "category": "Image generation and editing/Text to image", - "description": "Generates images from text prompts using Flux.1 [dev], Black Forest Labs' 12B diffusion model." + "description": "Generates images from prompts using FLUX.1 [dev]: a 12B rectified-flow MMDiT with dual CLIP plus T5-XXL text encoders and guidance-distilled sampling for sharp prompt following versus classic DDPM diffusion." } ] }, diff --git a/blueprints/Text to Image (Flux.1 Krea Dev).json b/blueprints/Text to Image (Flux.1 Krea Dev).json index 30a78dca1..0d7fa03c4 100644 --- a/blueprints/Text to Image (Flux.1 Krea Dev).json +++ b/blueprints/Text to Image (Flux.1 Krea Dev).json @@ -1024,7 +1024,7 @@ "workflowRendererVersion": "LG" }, "category": "Image generation and editing/Text to image", - "description": "Generates images from text prompts using Flux.1 Krea Dev, a Black Forest Labs × Krea collaboration variant." + "description": "FLUX.1 Krea [dev] (Black Forest Labs × Krea): open-weight 12B rectified-flow text-to-image drop-in alongside FLUX.1 [dev], tuned away from overcooked saturation toward more natural diversity in people, realism, and style while keeping ecosystem compatibility." } ] }, diff --git a/blueprints/Text to Image (Flux.2 Dev).json b/blueprints/Text to Image (Flux.2 Dev).json new file mode 100644 index 000000000..d5ca3077d --- /dev/null +++ b/blueprints/Text to Image (Flux.2 Dev).json @@ -0,0 +1,1870 @@ +{ + "revision": 0, + "last_node_id": 123, + "last_link_id": 0, + "nodes": [ + { + "id": 123, + "type": "85066daf-feda-4c7b-bbc3-d4797e8ccf0f", + "pos": [ + -800, + 640 + ], + "size": [ + 400, + 0 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "label": "prompt", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": null + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + }, + { + "label": "turbo_lora", + "name": "lora_name", + "type": "COMBO", + "widget": { + "name": "lora_name" + }, + "link": null + }, + { + "label": "enable_turbo_mode", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": null + }, + { + "name": "noise_seed", + "type": "INT", + "widget": { + "name": "noise_seed" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "115", + "text" + ], + [ + "113", + "width" + ], + [ + "113", + "height" + ], + [ + "122", + "unet_name" + ], + [ + "111", + "clip_name" + ], + [ + "108", + "vae_name" + ], + [ + "116", + "lora_name" + ], + [ + "121", + "value" + ], + [ + "114", + "noise_seed" + ], + [ + "114", + "control_after_generate" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.15.1", + "ue_properties": { + "widget_ue_connectable": { + "value": true, + "lora_name": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Text to Image (Flux.2 Dev)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "85066daf-feda-4c7b-bbc3-d4797e8ccf0f", + "version": 1, + "state": { + "lastGroupId": 6, + "lastNodeId": 123, + "lastLinkId": 232, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Text to Image (Flux.2 Dev)", + "inputNode": { + "id": -10, + "bounding": [ + -1500, + 250, + 151.744140625, + 220 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1560, + -20, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "1f4f1091-3f97-41d8-8ed8-e8b02260cf3c", + "name": "text", + "type": "STRING", + "linkIds": [ + 206 + ], + "label": "prompt", + "pos": [ + -1368.255859375, + 270 + ] + }, + { + "id": "b9b59411-4f5f-4482-8f78-369e6d50e71c", + "name": "width", + "type": "INT", + "linkIds": [ + 222, + 231 + ], + "pos": [ + -1368.255859375, + 290 + ] + }, + { + "id": "c6de9a28-3bf6-40d0-be16-f75ec517a766", + "name": "height", + "type": "INT", + "linkIds": [ + 223, + 232 + ], + "pos": [ + -1368.255859375, + 310 + ] + }, + { + "id": "8f1b1c75-e47c-45f5-af57-74abcfe8967c", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 225 + ], + "pos": [ + -1368.255859375, + 330 + ] + }, + { + "id": "6ac27631-1bf0-4161-9670-a662f6180b94", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 226 + ], + "pos": [ + -1368.255859375, + 350 + ] + }, + { + "id": "932e6cbe-f716-4905-ae54-d2b3543497bd", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 227 + ], + "pos": [ + -1368.255859375, + 370 + ] + }, + { + "id": "37400048-5e7b-427b-8b79-ea35841d5306", + "name": "lora_name", + "type": "COMBO", + "linkIds": [ + 228 + ], + "label": "turbo_lora", + "pos": [ + -1368.255859375, + 390 + ] + }, + { + "id": "333212d0-f027-476f-8b97-a921e20e340a", + "name": "value", + "type": "BOOLEAN", + "linkIds": [ + 229 + ], + "label": "enable_turbo_mode", + "pos": [ + -1368.255859375, + 410 + ] + }, + { + "id": "e7e73fad-ce6e-48d5-b719-e2abed685185", + "name": "noise_seed", + "type": "INT", + "linkIds": [ + 230 + ], + "pos": [ + -1368.255859375, + 430 + ] + } + ], + "outputs": [ + { + "id": "ed3c0a0f-a39f-453e-907f-8249c8e3335d", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 9 + ], + "localized_name": "IMAGE", + "pos": [ + 1580, + 0 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 105, + "type": "BasicGuider", + "pos": [ + 570, + 170 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 210 + }, + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 165 + } + ], + "outputs": [ + { + "localized_name": "GUIDER", + "name": "GUIDER", + "type": "GUIDER", + "slot_index": 0, + "links": [ + 30 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "BasicGuider", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 106, + "type": "FluxGuidance", + "pos": [ + -510, + 470 + ], + "size": [ + 320, + 110 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 41 + }, + { + "localized_name": "guidance", + "name": "guidance", + "type": "FLOAT", + "widget": { + "name": "guidance" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 165 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "FluxGuidance", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 4 + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 107, + "type": "KSamplerSelect", + "pos": [ + 570, + 350 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "SAMPLER", + "name": "SAMPLER", + "type": "SAMPLER", + "links": [ + 19 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "KSamplerSelect", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "euler" + ] + }, + { + "id": 108, + "type": "VAELoader", + "pos": [ + -1000, + 460 + ], + "size": [ + 300, + 110 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 227 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "slot_index": 0, + "links": [ + 159 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAELoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "full_encoder_small_decoder.safetensors", + "url": "https://huggingface.co/black-forest-labs/FLUX.2-small-decoder/resolve/main/full_encoder_small_decoder.safetensors", + "directory": "vae" + } + ] + }, + "widgets_values": [ + "full_encoder_small_decoder.safetensors" + ] + }, + { + "id": 109, + "type": "SamplerCustomAdvanced", + "pos": [ + 860, + -20 + ], + "size": [ + 280, + 330 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "noise", + "name": "noise", + "type": "NOISE", + "link": 37 + }, + { + "localized_name": "guider", + "name": "guider", + "type": "GUIDER", + "link": 30 + }, + { + "localized_name": "sampler", + "name": "sampler", + "type": "SAMPLER", + "link": 19 + }, + { + "localized_name": "sigmas", + "name": "sigmas", + "type": "SIGMAS", + "link": 132 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 161 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "LATENT", + "slot_index": 0, + "links": [ + 24 + ] + }, + { + "localized_name": "denoised_output", + "name": "denoised_output", + "type": "LATENT", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "SamplerCustomAdvanced", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 110, + "type": "VAEDecode", + "pos": [ + 1220, + -20 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 24 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 159 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 9 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAEDecode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 111, + "type": "CLIPLoader", + "pos": [ + -1000, + 200 + ], + "size": [ + 300, + 150 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 226 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 117 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "mistral_3_small_flux2_bf16.safetensors", + "url": "https://huggingface.co/Comfy-Org/flux2-dev/resolve/main/split_files/text_encoders/mistral_3_small_flux2_bf16.safetensors", + "directory": "text_encoders" + } + ] + }, + "widgets_values": [ + "mistral_3_small_flux2_bf16.safetensors", + "flux2", + "default" + ] + }, + { + "id": 112, + "type": "Flux2Scheduler", + "pos": [ + 570, + 550 + ], + "size": [ + 230, + 170 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": 213 + }, + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 231 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 232 + } + ], + "outputs": [ + { + "localized_name": "SIGMAS", + "name": "SIGMAS", + "type": "SIGMAS", + "links": [ + 132 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "Flux2Scheduler", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 20, + 1024, + 1024 + ] + }, + { + "id": 113, + "type": "EmptyFlux2LatentImage", + "pos": [ + -980, + 660 + ], + "size": [ + 270, + 170 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 222 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 223 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 161 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "EmptyFlux2LatentImage", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1024, + 1024, + 1 + ] + }, + { + "id": 114, + "type": "RandomNoise", + "pos": [ + 570, + -20 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "noise_seed", + "name": "noise_seed", + "type": "INT", + "widget": { + "name": "noise_seed" + }, + "link": 230 + } + ], + "outputs": [ + { + "localized_name": "NOISE", + "name": "NOISE", + "type": "NOISE", + "links": [ + 37 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "RandomNoise", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1027111520328378, + "randomize" + ] + }, + { + "id": 115, + "type": "CLIPTextEncode", + "pos": [ + -630, + -40 + ], + "size": [ + 440, + 450 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 117 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 206 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 41 + ] + } + ], + "title": "CLIP Text Encode (Positive Prompt)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPTextEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 116, + "type": "LoraLoaderModelOnly", + "pos": [ + -150, + 220 + ], + "size": [ + 300, + 140 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 221 + }, + { + "localized_name": "lora_name", + "name": "lora_name", + "type": "COMBO", + "widget": { + "name": "lora_name" + }, + "link": 228 + }, + { + "localized_name": "strength_model", + "name": "strength_model", + "type": "FLOAT", + "widget": { + "name": "strength_model" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 209 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LoraLoaderModelOnly", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "Flux_2-Turbo-LoRA_comfyui.safetensors", + "url": "https://huggingface.co/ByteZSzn/Flux.2-Turbo-ComfyUI/resolve/main/Flux_2-Turbo-LoRA_comfyui.safetensors", + "directory": "loras" + } + ] + }, + "widgets_values": [ + "Flux_2-Turbo-LoRA_comfyui.safetensors", + 1 + ] + }, + { + "id": 117, + "type": "ComfySwitchNode", + "pos": [ + 220, + -30 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 208 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 209 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 215 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 210 + ] + } + ], + "title": "Switch(model)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.15.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ComfySwitchNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 118, + "type": "PrimitiveInt", + "pos": [ + -140, + -30 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 211 + ] + } + ], + "title": "Steps", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.15.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 20, + "fixed" + ] + }, + { + "id": 119, + "type": "PrimitiveInt", + "pos": [ + -150, + 460 + ], + "size": [ + 300, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 212 + ] + } + ], + "title": "Steps", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.15.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 8, + "fixed" + ] + }, + { + "id": 120, + "type": "ComfySwitchNode", + "pos": [ + 220, + 260 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 211 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 212 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 214 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 213 + ] + } + ], + "title": "Switch(steps)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.15.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ComfySwitchNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 121, + "type": "PrimitiveBoolean", + "pos": [ + -110, + 690 + ], + "size": [ + 270, + 100 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 229 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 214, + 215 + ] + } + ], + "title": "Enable Turbo LoRA", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.15.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveBoolean", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 122, + "type": "UNETLoader", + "pos": [ + -1000, + -30 + ], + "size": [ + 300, + 110 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 225 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 208, + 221 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "UNETLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "flux2_dev_fp8mixed.safetensors", + "url": "https://huggingface.co/Comfy-Org/flux2-dev/resolve/main/split_files/diffusion_models/flux2_dev_fp8mixed.safetensors", + "directory": "diffusion_models" + } + ] + }, + "widgets_values": [ + "flux2_dev_fp8mixed.safetensors", + "default" + ] + } + ], + "groups": [ + { + "id": 1, + "title": "Step 1 - Upload models", + "bounding": [ + -1040, + -110, + 380, + 710 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 2, + "title": "Custom sampler", + "bounding": [ + 540, + -110, + 640, + 870 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 4, + "title": "Step2 - Prompt", + "bounding": [ + -640, + -110, + 460, + 710 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 5, + "title": "Original", + "bounding": [ + -160, + -110, + 320, + 230 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 6, + "title": "8 Steps LoRA", + "bounding": [ + -160, + 140, + 320, + 460 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "links": [ + { + "id": 165, + "origin_id": 106, + "origin_slot": 0, + "target_id": 105, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 41, + "origin_id": 115, + "origin_slot": 0, + "target_id": 106, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 37, + "origin_id": 114, + "origin_slot": 0, + "target_id": 109, + "target_slot": 0, + "type": "NOISE" + }, + { + "id": 30, + "origin_id": 105, + "origin_slot": 0, + "target_id": 109, + "target_slot": 1, + "type": "GUIDER" + }, + { + "id": 19, + "origin_id": 107, + "origin_slot": 0, + "target_id": 109, + "target_slot": 2, + "type": "SAMPLER" + }, + { + "id": 132, + "origin_id": 112, + "origin_slot": 0, + "target_id": 109, + "target_slot": 3, + "type": "SIGMAS" + }, + { + "id": 161, + "origin_id": 113, + "origin_slot": 0, + "target_id": 109, + "target_slot": 4, + "type": "LATENT" + }, + { + "id": 117, + "origin_id": 111, + "origin_slot": 0, + "target_id": 115, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 24, + "origin_id": 109, + "origin_slot": 0, + "target_id": 110, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 159, + "origin_id": 108, + "origin_slot": 0, + "target_id": 110, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 9, + "origin_id": 110, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 206, + "origin_id": -10, + "origin_slot": 0, + "target_id": 115, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 208, + "origin_id": 122, + "origin_slot": 0, + "target_id": 117, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 209, + "origin_id": 116, + "origin_slot": 0, + "target_id": 117, + "target_slot": 1, + "type": "MODEL" + }, + { + "id": 210, + "origin_id": 117, + "origin_slot": 0, + "target_id": 105, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 211, + "origin_id": 118, + "origin_slot": 0, + "target_id": 120, + "target_slot": 0, + "type": "INT" + }, + { + "id": 212, + "origin_id": 119, + "origin_slot": 0, + "target_id": 120, + "target_slot": 1, + "type": "INT" + }, + { + "id": 213, + "origin_id": 120, + "origin_slot": 0, + "target_id": 112, + "target_slot": 0, + "type": "INT" + }, + { + "id": 214, + "origin_id": 121, + "origin_slot": 0, + "target_id": 120, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 215, + "origin_id": 121, + "origin_slot": 0, + "target_id": 117, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 221, + "origin_id": 122, + "origin_slot": 0, + "target_id": 116, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 222, + "origin_id": -10, + "origin_slot": 1, + "target_id": 113, + "target_slot": 0, + "type": "INT" + }, + { + "id": 223, + "origin_id": -10, + "origin_slot": 2, + "target_id": 113, + "target_slot": 1, + "type": "INT" + }, + { + "id": 225, + "origin_id": -10, + "origin_slot": 3, + "target_id": 122, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 226, + "origin_id": -10, + "origin_slot": 4, + "target_id": 111, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 227, + "origin_id": -10, + "origin_slot": 5, + "target_id": 108, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 228, + "origin_id": -10, + "origin_slot": 6, + "target_id": 116, + "target_slot": 1, + "type": "COMBO" + }, + { + "id": 229, + "origin_id": -10, + "origin_slot": 7, + "target_id": 121, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 230, + "origin_id": -10, + "origin_slot": 8, + "target_id": 114, + "target_slot": 0, + "type": "INT" + }, + { + "id": 231, + "origin_id": -10, + "origin_slot": 1, + "target_id": 112, + "target_slot": 1, + "type": "INT" + }, + { + "id": 232, + "origin_id": -10, + "origin_slot": 2, + "target_id": 112, + "target_slot": 2, + "type": "INT" + } + ], + "extra": { + "workflowRendererVersion": "LG" + }, + "category": "Image generation and editing/Text to image", + "description": "Generates images from prompts using FLUX.2 [dev]: a newer 32B rectified-flow stack with distilled guidance plus a stronger long-context multimodal encoder for complex scenes, sharper typography/UI text, anatomy, lighting, and high-resolution latent decoding." + } + ] + }, + "extra": { + "ue_links": [] + } +} diff --git a/blueprints/Text to Image (Z-Image-Base).json b/blueprints/Text to Image (Z-Image-Base).json new file mode 100644 index 000000000..169263712 --- /dev/null +++ b/blueprints/Text to Image (Z-Image-Base).json @@ -0,0 +1,1184 @@ +{ + "revision": 0, + "last_node_id": 126, + "last_link_id": 0, + "nodes": [ + { + "id": 126, + "type": "8a2bb267-5858-4aaf-bdcd-61002711af19", + "pos": [ + -2280, + 2850 + ], + "size": [ + 410, + 560 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "label": "prompt", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": null + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": null + }, + { + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + }, + { + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + }, + { + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "67", + "text" + ], + [ + "68", + "width" + ], + [ + "68", + "height" + ], + [ + "69", + "steps" + ], + [ + "69", + "cfg" + ], + [ + "69", + "seed" + ], + [ + "66", + "unet_name" + ], + [ + "62", + "clip_name" + ], + [ + "63", + "vae_name" + ], + [ + "69", + "control_after_generate" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.13.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Text to Image (Z-Image-Base)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "8a2bb267-5858-4aaf-bdcd-61002711af19", + "version": 1, + "state": { + "lastGroupId": 16, + "lastNodeId": 126, + "lastLinkId": 229, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Text to Image (Z-Image-Base)", + "description": "Generates images from text prompts using Z-Image base weights with Qwen3 text encoder and bundled VAE.", + "inputNode": { + "id": -10, + "bounding": [ + -220, + 40, + 120, + 220 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1840, + -150, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "af36fee5-4f8b-4a8e-bfa8-cb8fe7006cc3", + "name": "text", + "type": "STRING", + "linkIds": [ + 108 + ], + "label": "prompt", + "pos": [ + -120, + 60 + ] + }, + { + "id": "357f0059-e8e6-41f6-a290-c53b0a60c0ed", + "name": "width", + "type": "INT", + "linkIds": [ + 114 + ], + "pos": [ + -120, + 80 + ] + }, + { + "id": "4a442743-a9c2-4aa5-9efd-05d43f3322d3", + "name": "height", + "type": "INT", + "linkIds": [ + 115 + ], + "pos": [ + -120, + 100 + ] + }, + { + "id": "a0fc336b-d349-418e-8415-318653f7b6b3", + "name": "steps", + "type": "INT", + "linkIds": [ + 116 + ], + "pos": [ + -120, + 120 + ] + }, + { + "id": "2f253ace-1e1a-415f-9b95-a10430bd5749", + "name": "cfg", + "type": "FLOAT", + "linkIds": [ + 117 + ], + "pos": [ + -120, + 140 + ] + }, + { + "id": "18a6ad37-23aa-4bf7-a0cd-1d6ca6e2a128", + "name": "seed", + "type": "INT", + "linkIds": [ + 118 + ], + "pos": [ + -120, + 160 + ] + }, + { + "id": "d1fc4937-8505-4ec6-9fc4-a33ef7b45eee", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 119 + ], + "pos": [ + -120, + 180 + ] + }, + { + "id": "db45dd49-d990-4ceb-a849-f96341874cdd", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 120 + ], + "pos": [ + -120, + 200 + ] + }, + { + "id": "37b8eac6-9b1b-452b-81f3-0ba9e34a576a", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 121 + ], + "pos": [ + -120, + 220 + ] + } + ], + "outputs": [ + { + "id": "f2bea309-bfe7-4ccb-9ffe-9475bf1da2ae", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 79 + ], + "localized_name": "IMAGE", + "pos": [ + 1860, + -130 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 67, + "type": "CLIPTextEncode", + "pos": [ + 600, + -90 + ], + "size": [ + 410, + 320 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 78 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 108 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 75 + ] + } + ], + "title": "CLIP Text Encode (Positive Prompt)", + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 68, + "type": "EmptySD3LatentImage", + "pos": [ + 240, + 620 + ], + "size": [ + 260, + 170 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 114 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 115 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 77 + ] + } + ], + "properties": { + "Node name for S&R": "EmptySD3LatentImage", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1024, + 1024, + 1 + ] + }, + { + "id": 63, + "type": "VAELoader", + "pos": [ + 230, + 340 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 121 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 73 + ] + } + ], + "properties": { + "Node name for S&R": "VAELoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "ae.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/vae/ae.safetensors", + "directory": "vae" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "ae.safetensors" + ] + }, + { + "id": 62, + "type": "CLIPLoader", + "pos": [ + 230, + 110 + ], + "size": [ + 270, + 150 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 120 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 78, + 82 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "qwen_3_4b.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/text_encoders/qwen_3_4b.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "qwen_3_4b.safetensors", + "lumina2", + "default" + ] + }, + { + "id": 65, + "type": "VAEDecode", + "pos": [ + 1450, + -150 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 72 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 73 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 79 + ] + } + ], + "properties": { + "Node name for S&R": "VAEDecode", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 70, + "type": "ModelSamplingAuraFlow", + "pos": [ + 1100, + -150 + ], + "size": [ + 310, + 110 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 109 + }, + { + "localized_name": "shift", + "name": "shift", + "type": "FLOAT", + "widget": { + "name": "shift" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 74 + ] + } + ], + "properties": { + "Node name for S&R": "ModelSamplingAuraFlow", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 3 + ] + }, + { + "id": 66, + "type": "UNETLoader", + "pos": [ + 230, + -90 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 119 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 109 + ] + } + ], + "properties": { + "Node name for S&R": "UNETLoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "z_image_bf16.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image/resolve/main/split_files/diffusion_models/z_image_bf16.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "z_image_bf16.safetensors", + "default" + ] + }, + { + "id": 71, + "type": "CLIPTextEncode", + "pos": [ + 600, + 310 + ], + "size": [ + 390, + 140 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 82 + }, + { + "label": "prompt", + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 83 + ] + } + ], + "title": "CLIP Text Encode (Negative Prompt)", + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 69, + "type": "KSampler", + "pos": [ + 1100, + 10 + ], + "size": [ + 310, + 440 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 74 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 75 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 83 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 77 + }, + { + "localized_name": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": 118 + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": 116 + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": 117 + }, + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 72 + ] + } + ], + "properties": { + "Node name for S&R": "KSampler", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + "randomize", + 25, + 4, + "res_multistep", + "simple", + 1 + ] + }, + { + "id": 87, + "type": "MarkdownNote", + "pos": [ + 1110, + -360 + ], + "size": [ + 300, + 120 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": {}, + "widgets_values": [ + "- Steps: 30~50\n- cfg: 3~5" + ], + "color": "#222", + "bgcolor": "#000", + "title": "Original Settings" + } + ], + "groups": [ + { + "id": 2, + "title": "Step2 - Image size", + "bounding": [ + 200, + 530, + 330, + 287.9999544955691 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 3, + "title": "Step3 - Prompt", + "bounding": [ + 570, + -200, + 470, + 700 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 4, + "title": "Step1 - Load models", + "bounding": [ + 200, + -200, + 330, + 700 + ], + "color": "#3f789e", + "flags": {} + } + ], + "links": [ + { + "id": 78, + "origin_id": 62, + "origin_slot": 0, + "target_id": 67, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 74, + "origin_id": 70, + "origin_slot": 0, + "target_id": 69, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 75, + "origin_id": 67, + "origin_slot": 0, + "target_id": 69, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 83, + "origin_id": 71, + "origin_slot": 0, + "target_id": 69, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 77, + "origin_id": 68, + "origin_slot": 0, + "target_id": 69, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 82, + "origin_id": 62, + "origin_slot": 0, + "target_id": 71, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 72, + "origin_id": 69, + "origin_slot": 0, + "target_id": 65, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 73, + "origin_id": 63, + "origin_slot": 0, + "target_id": 65, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 79, + "origin_id": 65, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 108, + "origin_id": -10, + "origin_slot": 0, + "target_id": 67, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 109, + "origin_id": 66, + "origin_slot": 0, + "target_id": 70, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 114, + "origin_id": -10, + "origin_slot": 1, + "target_id": 68, + "target_slot": 0, + "type": "INT" + }, + { + "id": 115, + "origin_id": -10, + "origin_slot": 2, + "target_id": 68, + "target_slot": 1, + "type": "INT" + }, + { + "id": 116, + "origin_id": -10, + "origin_slot": 3, + "target_id": 69, + "target_slot": 5, + "type": "INT" + }, + { + "id": 117, + "origin_id": -10, + "origin_slot": 4, + "target_id": 69, + "target_slot": 6, + "type": "FLOAT" + }, + { + "id": 118, + "origin_id": -10, + "origin_slot": 5, + "target_id": 69, + "target_slot": 4, + "type": "INT" + }, + { + "id": 119, + "origin_id": -10, + "origin_slot": 6, + "target_id": 66, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 120, + "origin_id": -10, + "origin_slot": 7, + "target_id": 62, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 121, + "origin_id": -10, + "origin_slot": 8, + "target_id": 63, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": { + "workflowRendererVersion": "LG" + }, + "category": "Image generation and editing/Text to image" + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Text to Image (Z-Image-Turbo).json b/blueprints/Text to Image (Z-Image-Turbo).json index 6975151ea..2501486fa 100644 --- a/blueprints/Text to Image (Z-Image-Turbo).json +++ b/blueprints/Text to Image (Z-Image-Turbo).json @@ -1,22 +1,21 @@ { - "id": "1c3eaa76-5cfa-4dc7-8571-97a570324e01", "revision": 0, - "last_node_id": 34, - "last_link_id": 40, + "last_node_id": 57, + "last_link_id": 0, "nodes": [ { - "id": 5, - "type": "dfe9eb32-97c0-43a5-90d5-4fd37768d91b", + "id": 57, + "type": "f2fdebf6-dfaf-43b6-9eb2-7f70613cfdc1", "pos": [ - -2.5766491043910378e-05, - 1229.999928629805 + 130, + 200 ], "size": [ 400, 470 ], "flags": {}, - "order": 0, + "order": 1, "mode": 0, "inputs": [ { @@ -44,6 +43,22 @@ }, "link": null }, + { + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + }, { "name": "unet_name", "type": "COMBO", @@ -80,15 +95,15 @@ "properties": { "proxyWidgets": [ [ - "-1", + "27", "text" ], [ - "-1", + "13", "width" ], [ - "-1", + "13", "height" ], [ @@ -97,19 +112,23 @@ ], [ "3", - "control_after_generate" + "steps" ], [ - "-1", + "28", "unet_name" ], [ - "-1", + "30", "clip_name" ], [ - "-1", + "29", "vae_name" + ], + [ + "3", + "control_after_generate" ] ], "cnr_id": "comfy-core", @@ -122,29 +141,21 @@ "secondTabOffset": 80, "secondTabWidth": 65 }, - "widgets_values": [ - "", - 1024, - 1024, - null, - null, - "z_image_turbo_bf16.safetensors", - "qwen_3_4b.safetensors", - "ae.safetensors" - ] + "widgets_values": [], + "title": "Text to Image (Z-Image-Turbo)" } ], "links": [], - "groups": [], + "version": 0.4, "definitions": { "subgraphs": [ { - "id": "dfe9eb32-97c0-43a5-90d5-4fd37768d91b", + "id": "f2fdebf6-dfaf-43b6-9eb2-7f70613cfdc1", "version": 1, "state": { "lastGroupId": 4, - "lastNodeId": 34, - "lastLinkId": 40, + "lastNodeId": 61, + "lastLinkId": 75, "lastRerouteId": 0 }, "revision": 0, @@ -153,17 +164,17 @@ "inputNode": { "id": -10, "bounding": [ - -80, - 425, + -560, + 480, 120, - 160 + 200 ] }, "outputNode": { "id": -20, "bounding": [ - 1490, - 415, + 1670, + 320, 120, 60 ] @@ -178,8 +189,8 @@ ], "label": "prompt", "pos": [ - 20, - 445 + -460, + 500 ] }, { @@ -190,8 +201,8 @@ 35 ], "pos": [ - 20, - 465 + -460, + 520 ] }, { @@ -202,44 +213,68 @@ 36 ], "pos": [ - 20, - 485 + -460, + 540 ] }, { - "id": "23087d15-8412-4fbd-b71e-9b6d7ef76de1", + "id": "f77677f7-6bf6-4c19-a71f-c4a553d5981e", + "name": "seed", + "type": "INT", + "linkIds": [ + 71 + ], + "pos": [ + -460, + 560 + ] + }, + { + "id": "ef9a9fb1-5983-4bc9-a60b-cf5aec48bff1", + "name": "steps", + "type": "INT", + "linkIds": [ + 72 + ], + "pos": [ + -460, + 580 + ] + }, + { + "id": "a20a1b30-785f-4a04-bb6d-3d61adab9764", "name": "unet_name", "type": "COMBO", "linkIds": [ - 38 + 73 ], "pos": [ - 20, - 505 + -460, + 600 ] }, { - "id": "0677f5c3-2a3f-43d4-98ac-a4c56d5efdc0", + "id": "4af8fc2b-4655-4086-8240-45f8cb38c6f6", "name": "clip_name", "type": "COMBO", "linkIds": [ - 39 + 74 ], "pos": [ - 20, - 525 + -460, + 620 ] }, { - "id": "c85c0445-2641-48b1-bbca-95057edf2fcf", + "id": "4d518693-2807-439c-9cb6-cffd23ccba2c", "name": "vae_name", "type": "COMBO", "linkIds": [ - 40 + 75 ], "pos": [ - 20, - 545 + -460, + 640 ] } ], @@ -253,8 +288,8 @@ ], "localized_name": "IMAGE", "pos": [ - 1510, - 435 + 1690, + 340 ] } ], @@ -264,15 +299,15 @@ "id": 30, "type": "CLIPLoader", "pos": [ - 109.99997264844609, - 329.99999029608756 + 30, + 420 ], "size": [ - 269.9869791666667, - 106 + 270, + 150 ], "flags": {}, - "order": 0, + "order": 7, "mode": 0, "inputs": [ { @@ -282,7 +317,7 @@ "widget": { "name": "clip_name" }, - "link": 39 + "link": 74 }, { "localized_name": "type", @@ -315,9 +350,9 @@ } ], "properties": { + "Node name for S&R": "CLIPLoader", "cnr_id": "comfy-core", "ver": "0.3.73", - "Node name for S&R": "CLIPLoader", "models": [ { "name": "qwen_3_4b.safetensors", @@ -343,15 +378,15 @@ "id": 29, "type": "VAELoader", "pos": [ - 109.99997264844609, - 479.9999847172637 + 30, + 650 ], "size": [ - 269.9869791666667, - 58 + 270, + 110 ], "flags": {}, - "order": 1, + "order": 6, "mode": 0, "inputs": [ { @@ -361,7 +396,7 @@ "widget": { "name": "vae_name" }, - "link": 40 + "link": 75 } ], "outputs": [ @@ -375,9 +410,9 @@ } ], "properties": { + "Node name for S&R": "VAELoader", "cnr_id": "comfy-core", "ver": "0.3.73", - "Node name for S&R": "VAELoader", "models": [ { "name": "ae.safetensors", @@ -401,12 +436,12 @@ "id": 33, "type": "ConditioningZeroOut", "pos": [ - 639.9999103333332, - 620.0000271257795 + 630, + 960 ], "size": [ - 204.134765625, - 26 + 230, + 80 ], "flags": {}, "order": 8, @@ -430,9 +465,9 @@ } ], "properties": { + "Node name for S&R": "ConditioningZeroOut", "cnr_id": "comfy-core", "ver": "0.3.73", - "Node name for S&R": "ConditioningZeroOut", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -440,22 +475,21 @@ "secondTabText": "Send Back", "secondTabOffset": 80, "secondTabWidth": 65 - }, - "widgets_values": [] + } }, { "id": 8, "type": "VAEDecode", "pos": [ - 1219.9999088104782, - 160.00009184959066 + 1320, + 230 ], "size": [ - 209.98697916666669, - 46 + 230, + 100 ], "flags": {}, - "order": 5, + "order": 1, "mode": 0, "inputs": [ { @@ -483,9 +517,9 @@ } ], "properties": { + "Node name for S&R": "VAEDecode", "cnr_id": "comfy-core", "ver": "0.3.64", - "Node name for S&R": "VAEDecode", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -493,22 +527,21 @@ "secondTabText": "Send Back", "secondTabOffset": 80, "secondTabWidth": 65 - }, - "widgets_values": [] + } }, { "id": 28, "type": "UNETLoader", "pos": [ - 109.99997264844609, - 200.0000502647102 + 30, + 230 ], "size": [ - 269.9869791666667, - 82 + 270, + 110 ], "flags": {}, - "order": 2, + "order": 5, "mode": 0, "inputs": [ { @@ -518,7 +551,7 @@ "widget": { "name": "unet_name" }, - "link": 38 + "link": 73 }, { "localized_name": "weight_dtype", @@ -541,9 +574,9 @@ } ], "properties": { + "Node name for S&R": "UNETLoader", "cnr_id": "comfy-core", "ver": "0.3.73", - "Node name for S&R": "UNETLoader", "models": [ { "name": "z_image_turbo_bf16.safetensors", @@ -568,15 +601,15 @@ "id": 27, "type": "CLIPTextEncode", "pos": [ - 429.99997828947767, - 200.0000502647102 + 400, + 230 ], "size": [ - 409.9869791666667, - 319.9869791666667 + 450, + 650 ], "flags": {}, - "order": 7, + "order": 4, "mode": 0, "inputs": [ { @@ -607,9 +640,9 @@ } ], "properties": { + "Node name for S&R": "CLIPTextEncode", "cnr_id": "comfy-core", "ver": "0.3.73", - "Node name for S&R": "CLIPTextEncode", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -626,15 +659,15 @@ "id": 13, "type": "EmptySD3LatentImage", "pos": [ - 109.99997264844609, - 629.9999791384399 + 40, + 890 ], "size": [ - 259.9869791666667, - 106 + 260, + 170 ], "flags": {}, - "order": 6, + "order": 3, "mode": 0, "inputs": [ { @@ -677,9 +710,9 @@ } ], "properties": { + "Node name for S&R": "EmptySD3LatentImage", "cnr_id": "comfy-core", "ver": "0.3.64", - "Node name for S&R": "EmptySD3LatentImage", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -694,19 +727,77 @@ 1 ] }, + { + "id": 11, + "type": "ModelSamplingAuraFlow", + "pos": [ + 950, + 230 + ], + "size": [ + 310, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 26 + }, + { + "localized_name": "shift", + "name": "shift", + "type": "FLOAT", + "widget": { + "name": "shift" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 13 + ] + } + ], + "properties": { + "Node name for S&R": "ModelSamplingAuraFlow", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 3 + ] + }, { "id": 3, "type": "KSampler", "pos": [ - 879.9999615530063, - 269.9999774911694 + 950, + 400 ], "size": [ - 314.9869791666667, - 262 + 320, + 350 ], "flags": {}, - "order": 4, + "order": 0, "mode": 0, "inputs": [ { @@ -740,7 +831,7 @@ "widget": { "name": "seed" }, - "link": null + "link": 71 }, { "localized_name": "steps", @@ -749,7 +840,7 @@ "widget": { "name": "steps" }, - "link": null + "link": 72 }, { "localized_name": "cfg", @@ -800,9 +891,9 @@ } ], "properties": { + "Node name for S&R": "KSampler", "cnr_id": "comfy-core", "ver": "0.3.64", - "Node name for S&R": "KSampler", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -814,81 +905,23 @@ "widgets_values": [ 0, "randomize", - 4, + 8, 1, "res_multistep", "simple", 1 ] - }, - { - "id": 11, - "type": "ModelSamplingAuraFlow", - "pos": [ - 879.9999615530063, - 160.00009184959066 - ], - "size": [ - 309.9869791666667, - 58 - ], - "flags": {}, - "order": 3, - "mode": 0, - "inputs": [ - { - "localized_name": "model", - "name": "model", - "type": "MODEL", - "link": 26 - }, - { - "localized_name": "shift", - "name": "shift", - "type": "FLOAT", - "widget": { - "name": "shift" - }, - "link": null - } - ], - "outputs": [ - { - "localized_name": "MODEL", - "name": "MODEL", - "type": "MODEL", - "slot_index": 0, - "links": [ - 13 - ] - } - ], - "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.64", - "Node name for S&R": "ModelSamplingAuraFlow", - "enableTabs": false, - "tabWidth": 65, - "tabXOffset": 10, - "hasSecondTab": false, - "secondTabText": "Send Back", - "secondTabOffset": 80, - "secondTabWidth": 65 - }, - "widgets_values": [ - 3 - ] } ], "groups": [ { "id": 2, - "title": "Image size", + "title": "Step2 - Image size", "bounding": [ - 100, - 560, - 290, - 200 + 10, + 820, + 320, + 280 ], "color": "#3f789e", "font_size": 24, @@ -896,12 +929,12 @@ }, { "id": 3, - "title": "Prompt", + "title": "Step3 - Prompt", "bounding": [ - 410, + 360, 130, - 450, - 540 + 530, + 970 ], "color": "#3f789e", "font_size": 24, @@ -909,12 +942,12 @@ }, { "id": 4, - "title": "Models", + "title": "Step1 - Load models", "bounding": [ - 100, + 0, 130, - 290, - 413.6 + 330, + 660 ], "color": "#3f789e", "font_size": 24, @@ -1027,25 +1060,41 @@ "type": "INT" }, { - "id": 38, + "id": 71, "origin_id": -10, "origin_slot": 3, + "target_id": 3, + "target_slot": 4, + "type": "INT" + }, + { + "id": 72, + "origin_id": -10, + "origin_slot": 4, + "target_id": 3, + "target_slot": 5, + "type": "INT" + }, + { + "id": 73, + "origin_id": -10, + "origin_slot": 5, "target_id": 28, "target_slot": 0, "type": "COMBO" }, { - "id": 39, + "id": 74, "origin_id": -10, - "origin_slot": 4, + "origin_slot": 6, "target_id": 30, "target_slot": 0, "type": "COMBO" }, { - "id": 40, + "id": 75, "origin_id": -10, - "origin_slot": 5, + "origin_slot": 7, "target_id": 29, "target_slot": 0, "type": "COMBO" @@ -1059,21 +1108,5 @@ } ] }, - "config": {}, - "extra": { - "frontendVersion": "1.37.10", - "workflowRendererVersion": "LG", - "VHS_latentpreview": false, - "VHS_latentpreviewrate": 0, - "VHS_MetadataImage": true, - "VHS_KeepIntermediate": true, - "ds": { - "scale": 0.8401370345180755, - "offset": [ - 940.0587067393087, - -830.7121087564725 - ] - } - }, - "version": 0.4 + "extra": {} } \ No newline at end of file diff --git a/blueprints/Text to Image.json b/blueprints/Text to Image.json new file mode 100644 index 000000000..ffe3682ff --- /dev/null +++ b/blueprints/Text to Image.json @@ -0,0 +1,1132 @@ +{ + "revision": 0, + "last_node_id": 71, + "last_link_id": 0, + "nodes": [ + { + "id": 71, + "type": "2d5985c9-deef-41ae-9c34-6353d3d7d1ef", + "pos": [ + 90, + 800 + ], + "size": [ + 400, + 80 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "label": "prompt", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": null + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + }, + { + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "title": "Text to Image", + "properties": { + "proxyWidgets": [ + [ + "67", + "text" + ], + [ + "68", + "width" + ], + [ + "68", + "height" + ], + [ + "66", + "unet_name" + ], + [ + "62", + "clip_name" + ], + [ + "63", + "vae_name" + ], + [ + "70", + "steps" + ], + [ + "70", + "control_after_generate" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": { + "text": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [] + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "2d5985c9-deef-41ae-9c34-6353d3d7d1ef", + "version": 1, + "state": { + "lastGroupId": 4, + "lastNodeId": 71, + "lastLinkId": 70, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Text to Image", + "inputNode": { + "id": -10, + "bounding": [ + -80, + 425, + 120, + 180 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1490, + 415, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "fb178669-e742-4a53-8a69-7df59834dfd8", + "name": "text", + "type": "STRING", + "linkIds": [ + 34 + ], + "label": "prompt", + "pos": [ + 20, + 445 + ] + }, + { + "id": "dd780b3c-23e9-46ff-8469-156008f42e5a", + "name": "width", + "type": "INT", + "linkIds": [ + 35 + ], + "pos": [ + 20, + 465 + ] + }, + { + "id": "7b08d546-6bb0-4ef9-82e9-ffae5e1ee6bc", + "name": "height", + "type": "INT", + "linkIds": [ + 36 + ], + "pos": [ + 20, + 485 + ] + }, + { + "id": "8ed4eb73-a2bf-4766-8bf4-c5890b560596", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 38 + ], + "pos": [ + 20, + 505 + ] + }, + { + "id": "f362d639-d412-4b5d-8490-1e9995dc5f82", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 39 + ], + "pos": [ + 20, + 525 + ] + }, + { + "id": "ee25ac16-de63-4b74-bbbb-5b29fdc1efcf", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 40 + ], + "pos": [ + 20, + 545 + ] + }, + { + "id": "51cbcd61-9218-4bcb-89ac-ecdfb1ef8892", + "name": "steps", + "type": "INT", + "linkIds": [ + 70 + ], + "pos": [ + 20, + 565 + ] + } + ], + "outputs": [ + { + "id": "1fa72a21-ce00-4952-814e-1f2ffbe87d1d", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 16 + ], + "localized_name": "IMAGE", + "pos": [ + 1510, + 435 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 62, + "type": "CLIPLoader", + "pos": [ + 110, + 330 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 39 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 28 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPLoader", + "models": [ + { + "name": "qwen_3_4b.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/text_encoders/qwen_3_4b.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "qwen_3_4b.safetensors", + "lumina2", + "default" + ] + }, + { + "id": 63, + "type": "VAELoader", + "pos": [ + 110, + 480 + ], + "size": [ + 270, + 60 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 40 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 27 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAELoader", + "models": [ + { + "name": "ae.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/vae/ae.safetensors", + "directory": "vae" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "ae.safetensors" + ] + }, + { + "id": 64, + "type": "ConditioningZeroOut", + "pos": [ + 640, + 620 + ], + "size": [ + 210, + 30 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 32 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 33 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ConditioningZeroOut", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 65, + "type": "VAEDecode", + "pos": [ + 1220, + 160 + ], + "size": [ + 210, + 50 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 14 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 27 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 16 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAEDecode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 66, + "type": "UNETLoader", + "pos": [ + 110, + 200 + ], + "size": [ + 270, + 90 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 38 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 26 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "UNETLoader", + "models": [ + { + "name": "z_image_turbo_bf16.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/diffusion_models/z_image_turbo_bf16.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "z_image_turbo_bf16.safetensors", + "default" + ] + }, + { + "id": 67, + "type": "CLIPTextEncode", + "pos": [ + 430, + 200 + ], + "size": [ + 410, + 370 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 28 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 34 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 30, + 32 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPTextEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ] + }, + { + "id": 68, + "type": "EmptySD3LatentImage", + "pos": [ + 110, + 630 + ], + "size": [ + 260, + 110 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 35 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 36 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 17 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "EmptySD3LatentImage", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1024, + 1024, + 1 + ] + }, + { + "id": 69, + "type": "ModelSamplingAuraFlow", + "pos": [ + 880, + 160 + ], + "size": [ + 310, + 60 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 26 + }, + { + "localized_name": "shift", + "name": "shift", + "type": "FLOAT", + "widget": { + "name": "shift" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 13 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ModelSamplingAuraFlow", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 3 + ] + }, + { + "id": 70, + "type": "KSampler", + "pos": [ + 880, + 270 + ], + "size": [ + 320, + 270 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 13 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 30 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 33 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 17 + }, + { + "localized_name": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": 70 + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + }, + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 14 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "KSampler", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + "randomize", + 8, + 1, + "res_multistep", + "simple", + 1 + ] + } + ], + "groups": [ + { + "id": 2, + "title": "Step2 - Image size", + "bounding": [ + 100, + 560, + 290, + 200 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 3, + "title": "Step3 - Prompt", + "bounding": [ + 410, + 130, + 450, + 540 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 4, + "title": "Step1 - Load models", + "bounding": [ + 100, + 130, + 290, + 413.6 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "links": [ + { + "id": 32, + "origin_id": 67, + "origin_slot": 0, + "target_id": 64, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 26, + "origin_id": 66, + "origin_slot": 0, + "target_id": 69, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 14, + "origin_id": 70, + "origin_slot": 0, + "target_id": 65, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 27, + "origin_id": 63, + "origin_slot": 0, + "target_id": 65, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 13, + "origin_id": 69, + "origin_slot": 0, + "target_id": 70, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 30, + "origin_id": 67, + "origin_slot": 0, + "target_id": 70, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 33, + "origin_id": 64, + "origin_slot": 0, + "target_id": 70, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 17, + "origin_id": 68, + "origin_slot": 0, + "target_id": 70, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 28, + "origin_id": 62, + "origin_slot": 0, + "target_id": 67, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 16, + "origin_id": 65, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 34, + "origin_id": -10, + "origin_slot": 0, + "target_id": 67, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 35, + "origin_id": -10, + "origin_slot": 1, + "target_id": 68, + "target_slot": 0, + "type": "INT" + }, + { + "id": 36, + "origin_id": -10, + "origin_slot": 2, + "target_id": 68, + "target_slot": 1, + "type": "INT" + }, + { + "id": 38, + "origin_id": -10, + "origin_slot": 3, + "target_id": 66, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 39, + "origin_id": -10, + "origin_slot": 4, + "target_id": 62, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 40, + "origin_id": -10, + "origin_slot": 5, + "target_id": 63, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 70, + "origin_id": -10, + "origin_slot": 6, + "target_id": 70, + "target_slot": 5, + "type": "INT" + } + ], + "extra": { + "workflowRendererVersion": "LG" + }, + "category": "Image generation and editing/Text to image", + "description": "Generates images from text prompts using Z-Image-Turbo defaults with Qwen3 text encoder and VAE." + } + ] + }, + "extra": {} +} diff --git a/blueprints/Video Segmentation (SAM3).json b/blueprints/Video Segmentation (SAM3).json new file mode 100644 index 000000000..4d9a13412 --- /dev/null +++ b/blueprints/Video Segmentation (SAM3).json @@ -0,0 +1,827 @@ +{ + "revision": 0, + "last_node_id": 130, + "last_link_id": 0, + "nodes": [ + { + "id": 130, + "type": "7937cf78-b52b-40a3-93b2-b4e2e5f98df1", + "pos": [ + -1210, + -2780 + ], + "size": [ + 300, + 370 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "video", + "type": "VIDEO", + "link": null + }, + { + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "name": "bboxes", + "type": "BOUNDING_BOX", + "link": null + }, + { + "name": "positive_coords", + "type": "STRING", + "link": null + }, + { + "name": "negative_coords", + "type": "STRING", + "link": null + }, + { + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": null + }, + { + "name": "refine_iterations", + "type": "INT", + "widget": { + "name": "refine_iterations" + }, + "link": null + }, + { + "name": "individual_masks", + "type": "BOOLEAN", + "widget": { + "name": "individual_masks" + }, + "link": null + }, + { + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "masks", + "name": "masks", + "type": "MASK", + "links": [] + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [] + }, + { + "name": "audio", + "type": "AUDIO", + "links": null + }, + { + "name": "fps", + "type": "FLOAT", + "links": null + } + ], + "properties": { + "proxyWidgets": [ + [ + "125", + "text" + ], + [ + "126", + "threshold" + ], + [ + "126", + "refine_iterations" + ], + [ + "126", + "individual_masks" + ], + [ + "127", + "ckpt_name" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Video Segmentation (SAM3)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "7937cf78-b52b-40a3-93b2-b4e2e5f98df1", + "version": 1, + "state": { + "lastGroupId": 0, + "lastNodeId": 130, + "lastLinkId": 299, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Video Segmentation (SAM3)", + "inputNode": { + "id": -10, + "bounding": [ + -2260, + -3450, + 136.369140625, + 220 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -1050, + -3510, + 120, + 120 + ] + }, + "inputs": [ + { + "id": "680ffd88-32fe-48be-88d6-91ea44d5eaee", + "name": "video", + "type": "VIDEO", + "linkIds": [ + 252 + ], + "pos": [ + -2143.630859375, + -3430 + ] + }, + { + "id": "ceaf249c-32d7-4624-8bf6-e590e347ed90", + "name": "text", + "type": "STRING", + "linkIds": [ + 254 + ], + "pos": [ + -2143.630859375, + -3410 + ] + }, + { + "id": "1ffbff36-da0c-4854-8cb4-88ad31e64f99", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 255 + ], + "pos": [ + -2143.630859375, + -3390 + ] + }, + { + "id": "67b7f4c7-cec0-4e00-b154-23cc1abf880e", + "name": "positive_coords", + "type": "STRING", + "linkIds": [ + 256 + ], + "pos": [ + -2143.630859375, + -3370 + ] + }, + { + "id": "b090a498-2bde-46b9-9554-18501401d687", + "name": "negative_coords", + "type": "STRING", + "linkIds": [ + 257 + ], + "pos": [ + -2143.630859375, + -3350 + ] + }, + { + "id": "1a76dfcf-ce95-46af-bba5-c42160c683dd", + "name": "threshold", + "type": "FLOAT", + "linkIds": [ + 261 + ], + "pos": [ + -2143.630859375, + -3330 + ] + }, + { + "id": "999523fa-c476-4c53-80c3-0a2f554d18ab", + "name": "refine_iterations", + "type": "INT", + "linkIds": [ + 262 + ], + "pos": [ + -2143.630859375, + -3310 + ] + }, + { + "id": "d2371011-7fe5-4a39-b0c1-df2e0bbd6ece", + "name": "individual_masks", + "type": "BOOLEAN", + "linkIds": [ + 263 + ], + "pos": [ + -2143.630859375, + -3290 + ] + }, + { + "id": "675a8b37-17db-48d1-853c-2fe5d6a74582", + "name": "ckpt_name", + "type": "COMBO", + "linkIds": [ + 273 + ], + "pos": [ + -2143.630859375, + -3270 + ] + } + ], + "outputs": [ + { + "id": "ff50da09-1e59-4a58-9b7f-be1a00aa5913", + "name": "masks", + "type": "MASK", + "linkIds": [ + 231 + ], + "localized_name": "masks", + "pos": [ + -1030, + -3490 + ] + }, + { + "id": "8f622e40-8528-4078-b7d3-147e9f872194", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 232 + ], + "localized_name": "bboxes", + "pos": [ + -1030, + -3470 + ] + }, + { + "id": "6c9924ec-f0fa-4509-83ea-8f97f5889bcc", + "name": "audio", + "type": "AUDIO", + "linkIds": [ + 259 + ], + "pos": [ + -1030, + -3450 + ] + }, + { + "id": "82c1cddc-ab11-44eb-9e2f-1a5c7ea5645b", + "name": "fps", + "type": "FLOAT", + "linkIds": [ + 260 + ], + "pos": [ + -1030, + -3430 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 125, + "type": "CLIPTextEncode", + "pos": [ + -2010, + -3040 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 240 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 254 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 200 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ] + }, + { + "id": 126, + "type": "SAM3_Detect", + "pos": [ + -1520, + -3520 + ], + "size": [ + 270, + 290 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "label": "model", + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 237 + }, + { + "label": "image", + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 253 + }, + { + "label": "conditioning", + "localized_name": "conditioning", + "name": "conditioning", + "shape": 7, + "type": "CONDITIONING", + "link": 200 + }, + { + "label": "bboxes", + "localized_name": "bboxes", + "name": "bboxes", + "shape": 7, + "type": "BOUNDING_BOX", + "link": 255 + }, + { + "label": "positive_coords", + "localized_name": "positive_coords", + "name": "positive_coords", + "shape": 7, + "type": "STRING", + "link": 256 + }, + { + "label": "negative_coords", + "localized_name": "negative_coords", + "name": "negative_coords", + "shape": 7, + "type": "STRING", + "link": 257 + }, + { + "localized_name": "threshold", + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": 261 + }, + { + "localized_name": "refine_iterations", + "name": "refine_iterations", + "type": "INT", + "widget": { + "name": "refine_iterations" + }, + "link": 262 + }, + { + "localized_name": "individual_masks", + "name": "individual_masks", + "type": "BOOLEAN", + "widget": { + "name": "individual_masks" + }, + "link": 263 + } + ], + "outputs": [ + { + "localized_name": "masks", + "name": "masks", + "type": "MASK", + "links": [ + 231 + ] + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [ + 232 + ] + } + ], + "properties": { + "Node name for S&R": "SAM3_Detect", + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0.5, + 2, + false + ] + }, + { + "id": 127, + "type": "CheckpointLoaderSimple", + "pos": [ + -1970, + -3310 + ], + "size": [ + 330, + 160 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 273 + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 237 + ] + }, + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 240 + ] + }, + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": null + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple", + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "sam3.1_multiplex_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/sam3.1/resolve/main/checkpoints/sam3.1_multiplex_fp16.safetensors", + "directory": "checkpoints" + } + ] + }, + "widgets_values": [ + "sam3.1_multiplex_fp16.safetensors" + ] + }, + { + "id": 128, + "type": "GetVideoComponents", + "pos": [ + -1910, + -3540 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": 252 + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 253 + ] + }, + { + "localized_name": "audio", + "name": "audio", + "type": "AUDIO", + "links": [ + 259 + ] + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "links": [ + 260 + ] + } + ], + "properties": { + "Node name for S&R": "GetVideoComponents", + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 129, + "type": "Note", + "pos": [ + -1980, + -2790 + ], + "size": [ + 370, + 250 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [], + "title": "Note: Prompt format", + "properties": {}, + "widgets_values": [ + "Max tokens for this model is only 32, to separately prompt multiple subjects you can separate prompts with comma, and set the max amount of objects detected for each prompt with :N\n\nFor example above test prompt finds 2 cakes, one apron, 4 window panels" + ], + "color": "#432", + "bgcolor": "#653" + } + ], + "groups": [], + "links": [ + { + "id": 237, + "origin_id": 127, + "origin_slot": 0, + "target_id": 126, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 200, + "origin_id": 125, + "origin_slot": 0, + "target_id": 126, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 240, + "origin_id": 127, + "origin_slot": 1, + "target_id": 125, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 231, + "origin_id": 126, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "MASK" + }, + { + "id": 232, + "origin_id": 126, + "origin_slot": 1, + "target_id": -20, + "target_slot": 1, + "type": "BOUNDING_BOX" + }, + { + "id": 252, + "origin_id": -10, + "origin_slot": 0, + "target_id": 128, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 253, + "origin_id": 128, + "origin_slot": 0, + "target_id": 126, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 254, + "origin_id": -10, + "origin_slot": 1, + "target_id": 125, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 255, + "origin_id": -10, + "origin_slot": 2, + "target_id": 126, + "target_slot": 3, + "type": "BOUNDING_BOX" + }, + { + "id": 256, + "origin_id": -10, + "origin_slot": 3, + "target_id": 126, + "target_slot": 4, + "type": "STRING" + }, + { + "id": 257, + "origin_id": -10, + "origin_slot": 4, + "target_id": 126, + "target_slot": 5, + "type": "STRING" + }, + { + "id": 259, + "origin_id": 128, + "origin_slot": 1, + "target_id": -20, + "target_slot": 2, + "type": "AUDIO" + }, + { + "id": 260, + "origin_id": 128, + "origin_slot": 2, + "target_id": -20, + "target_slot": 3, + "type": "FLOAT" + }, + { + "id": 261, + "origin_id": -10, + "origin_slot": 5, + "target_id": 126, + "target_slot": 6, + "type": "FLOAT" + }, + { + "id": 262, + "origin_id": -10, + "origin_slot": 6, + "target_id": 126, + "target_slot": 7, + "type": "INT" + }, + { + "id": 263, + "origin_id": -10, + "origin_slot": 7, + "target_id": 126, + "target_slot": 8, + "type": "BOOLEAN" + }, + { + "id": 273, + "origin_id": -10, + "origin_slot": 8, + "target_id": 127, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": {}, + "category": "Video Tools", + "description": "Segments video into temporally consistent masks using Meta SAM3 from text or interactive prompts." + } + ] + }, + "extra": {} +} diff --git a/blueprints/Video Stitch.json b/blueprints/Video Stitch.json index 6eb0f0bbf..2ac78b328 100644 --- a/blueprints/Video Stitch.json +++ b/blueprints/Video Stitch.json @@ -1,21 +1,21 @@ { "revision": 0, - "last_node_id": 84, + "last_node_id": 85, "last_link_id": 0, "nodes": [ { - "id": 84, - "type": "8e8aa94a-647e-436d-8440-8ee4691864de", + "id": 85, + "type": "637913e7-0206-46ba-8ded-70ae3a7c2e19", "pos": [ - -6100, - 2620 + -880, + -2260 ], "size": [ 290, 160 ], "flags": {}, - "order": 0, + "order": 2, "mode": 0, "inputs": [ { @@ -76,31 +76,26 @@ "properties": { "proxyWidgets": [ [ - "-1", + "79", "direction" ], [ - "-1", + "79", "match_image_size" ], [ - "-1", + "79", "spacing_width" ], [ - "-1", + "79", "spacing_color" ] ], "cnr_id": "comfy-core", "ver": "0.13.0" }, - "widgets_values": [ - "right", - true, - 0, - "white" - ], + "widgets_values": [], "title": "Video Stitch" } ], @@ -109,12 +104,12 @@ "definitions": { "subgraphs": [ { - "id": "8e8aa94a-647e-436d-8440-8ee4691864de", + "id": "637913e7-0206-46ba-8ded-70ae3a7c2e19", "version": 1, "state": { "lastGroupId": 1, - "lastNodeId": 84, - "lastLinkId": 262, + "lastNodeId": 97, + "lastLinkId": 282, "lastRerouteId": 0 }, "revision": 0, @@ -123,8 +118,8 @@ "inputNode": { "id": -10, "bounding": [ - -6580, - 2649, + -6810, + 2580, 143.55859375, 160 ] @@ -132,8 +127,8 @@ "outputNode": { "id": -20, "bounding": [ - -5720, - 2659, + -4770, + 2600, 120, 60 ] @@ -149,8 +144,8 @@ "localized_name": "video", "label": "Before Video", "pos": [ - -6456.44140625, - 2669 + -6686.44140625, + 2600 ] }, { @@ -163,8 +158,8 @@ "localized_name": "video_1", "label": "After Video", "pos": [ - -6456.44140625, - 2689 + -6686.44140625, + 2620 ] }, { @@ -175,8 +170,8 @@ 259 ], "pos": [ - -6456.44140625, - 2709 + -6686.44140625, + 2640 ] }, { @@ -187,8 +182,8 @@ 260 ], "pos": [ - -6456.44140625, - 2729 + -6686.44140625, + 2660 ] }, { @@ -199,8 +194,8 @@ 261 ], "pos": [ - -6456.44140625, - 2749 + -6686.44140625, + 2680 ] }, { @@ -211,8 +206,8 @@ 262 ], "pos": [ - -6456.44140625, - 2769 + -6686.44140625, + 2700 ] } ], @@ -226,8 +221,8 @@ ], "localized_name": "VIDEO", "pos": [ - -5700, - 2679 + -4750, + 2620 ] } ], @@ -238,11 +233,11 @@ "type": "GetVideoComponents", "pos": [ -6390, - 2560 + 2600 ], "size": [ - 193.530859375, - 66 + 230, + 120 ], "flags": {}, "order": 1, @@ -278,9 +273,9 @@ } ], "properties": { + "Node name for S&R": "GetVideoComponents", "cnr_id": "comfy-core", - "ver": "0.13.0", - "Node name for S&R": "GetVideoComponents" + "ver": "0.13.0" } }, { @@ -291,8 +286,8 @@ 2420 ], "size": [ - 193.530859375, - 66 + 230, + 120 ], "flags": {}, "order": 0, @@ -332,21 +327,254 @@ } ], "properties": { + "Node name for S&R": "GetVideoComponents", "cnr_id": "comfy-core", - "ver": "0.13.0", - "Node name for S&R": "GetVideoComponents" + "ver": "0.13.0" } }, + { + "id": 90, + "type": "GetImageSize", + "pos": [ + -6390, + 3030 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 266 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": [ + 274 + ] + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": [ + 276 + ] + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": null + } + ], + "properties": { + "Node name for S&R": "GetImageSize" + } + }, + { + "id": 80, + "type": "CreateVideo", + "pos": [ + -5190, + 2420 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 282 + }, + { + "localized_name": "audio", + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": 251 + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "widget": { + "name": "fps" + }, + "link": 252 + } + ], + "outputs": [ + { + "localized_name": "VIDEO", + "name": "VIDEO", + "type": "VIDEO", + "links": [ + 255 + ] + } + ], + "properties": { + "Node name for S&R": "CreateVideo", + "cnr_id": "comfy-core", + "ver": "0.13.0" + }, + "widgets_values": [ + 30 + ] + }, + { + "id": 95, + "type": "ComfyMathExpression", + "pos": [ + -6040, + 3020 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT", + "link": 274 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": null + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 279 + ] + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression" + }, + "widgets_values": [ + "a & ~1" + ] + }, + { + "id": 96, + "type": "ComfyMathExpression", + "pos": [ + -6040, + 3290 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT", + "link": 276 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": null + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 280 + ] + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression" + }, + "widgets_values": [ + "a & ~1" + ] + }, { "id": 79, "type": "ImageStitch", "pos": [ -6390, - 2700 + 2780 ], "size": [ 270, - 150 + 160 ], "flags": {}, "order": 2, @@ -408,14 +636,15 @@ "name": "IMAGE", "type": "IMAGE", "links": [ - 250 + 266, + 281 ] } ], "properties": { + "Node name for S&R": "ImageStitch", "cnr_id": "comfy-core", - "ver": "0.13.0", - "Node name for S&R": "ImageStitch" + "ver": "0.13.0" }, "widgets_values": [ "right", @@ -425,60 +654,91 @@ ] }, { - "id": 80, - "type": "CreateVideo", + "id": 97, + "type": "ResizeImageMaskNode", "pos": [ - -6040, - 2610 + -5560, + 2790 ], "size": [ 270, - 78 + 160 ], "flags": {}, - "order": 3, + "order": 7, "mode": 0, "inputs": [ { - "localized_name": "images", - "name": "images", - "type": "IMAGE", - "link": 250 + "localized_name": "input", + "name": "input", + "type": "IMAGE,MASK", + "link": 281 }, { - "localized_name": "audio", - "name": "audio", - "shape": 7, - "type": "AUDIO", - "link": 251 - }, - { - "localized_name": "fps", - "name": "fps", - "type": "FLOAT", + "localized_name": "resize_type", + "name": "resize_type", + "type": "COMFY_DYNAMICCOMBO_V3", "widget": { - "name": "fps" + "name": "resize_type" }, - "link": 252 + "link": null + }, + { + "localized_name": "width", + "name": "resize_type.width", + "type": "INT", + "widget": { + "name": "resize_type.width" + }, + "link": 279 + }, + { + "localized_name": "height", + "name": "resize_type.height", + "type": "INT", + "widget": { + "name": "resize_type.height" + }, + "link": 280 + }, + { + "localized_name": "crop", + "name": "resize_type.crop", + "type": "COMBO", + "widget": { + "name": "resize_type.crop" + }, + "link": null + }, + { + "localized_name": "scale_method", + "name": "scale_method", + "type": "COMBO", + "widget": { + "name": "scale_method" + }, + "link": null } ], "outputs": [ { - "localized_name": "VIDEO", - "name": "VIDEO", - "type": "VIDEO", + "localized_name": "resized", + "name": "resized", + "type": "*", "links": [ - 255 + 282 ] } ], "properties": { - "cnr_id": "comfy-core", - "ver": "0.13.0", - "Node name for S&R": "CreateVideo" + "Node name for S&R": "ResizeImageMaskNode" }, "widgets_values": [ - 30 + "scale dimensions", + 512, + 512, + "center", + "area" ] } ], @@ -500,14 +760,6 @@ "target_slot": 1, "type": "IMAGE" }, - { - "id": 250, - "origin_id": 79, - "origin_slot": 0, - "target_id": 80, - "target_slot": 0, - "type": "IMAGE" - }, { "id": 251, "origin_id": 77, @@ -579,6 +831,62 @@ "target_id": 79, "target_slot": 5, "type": "COMBO" + }, + { + "id": 266, + "origin_id": 79, + "origin_slot": 0, + "target_id": 90, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 274, + "origin_id": 90, + "origin_slot": 0, + "target_id": 95, + "target_slot": 0, + "type": "INT" + }, + { + "id": 276, + "origin_id": 90, + "origin_slot": 1, + "target_id": 96, + "target_slot": 0, + "type": "INT" + }, + { + "id": 279, + "origin_id": 95, + "origin_slot": 1, + "target_id": 97, + "target_slot": 2, + "type": "INT" + }, + { + "id": 280, + "origin_id": 96, + "origin_slot": 1, + "target_id": 97, + "target_slot": 3, + "type": "INT" + }, + { + "id": 281, + "origin_id": 79, + "origin_slot": 0, + "target_id": 97, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 282, + "origin_id": 97, + "origin_slot": 0, + "target_id": 80, + "target_slot": 0, + "type": "IMAGE" } ], "extra": { @@ -588,5 +896,6 @@ "description": "Stitches multiple video clips into a single sequential video file." } ] - } + }, + "extra": {} } \ No newline at end of file From f505cb4070d197f8fc783938319cf49015548e80 Mon Sep 17 00:00:00 2001 From: box4wangjing Date: Mon, 11 May 2026 12:05:09 +0900 Subject: [PATCH 036/145] chore: remove extra word in comment (#13826) --- comfy/utils.py | 2 +- comfy_api_nodes/apis/bria.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comfy/utils.py b/comfy/utils.py index 91e1ba3d3..b75972027 100644 --- a/comfy/utils.py +++ b/comfy/utils.py @@ -1196,7 +1196,7 @@ def model_trange(*args, **kwargs): pbar.i1_time = time.time() pbar.set_postfix_str(" Model Initialization complete! ") elif pbar._i == 2: - #bring forward the effective start time based the the diff between first and second iteration + #bring forward the effective start time based the diff between first and second iteration #to attempt to remove load overhead from the final step rate estimate. pbar.start_t = pbar.i1_time - (time.time() - pbar.i1_time) pbar.set_postfix_str("") diff --git a/comfy_api_nodes/apis/bria.py b/comfy_api_nodes/apis/bria.py index 8c496b56c..e08a519a8 100644 --- a/comfy_api_nodes/apis/bria.py +++ b/comfy_api_nodes/apis/bria.py @@ -23,7 +23,7 @@ class BriaEditImageRequest(BaseModel): None, description="Mask image (black and white). Black areas will be preserved, white areas will be edited. " "If omitted, the edit applies to the entire image. " - "The input image and the the input mask must be of the same size.", + "The input image and the input mask must be of the same size.", ) negative_prompt: str | None = Field(None) guidance_scale: float = Field(...) From 52976f3ea33cc2312c7b5a32e1c7510b203eefb6 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Sun, 10 May 2026 23:32:00 -0400 Subject: [PATCH 037/145] ComfyUI v0.21.0 --- comfyui_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comfyui_version.py b/comfyui_version.py index 53e7156e3..45626792f 100644 --- a/comfyui_version.py +++ b/comfyui_version.py @@ -1,3 +1,3 @@ # This file is automatically generated by the build process when version is # updated in pyproject.toml. -__version__ = "0.20.1" +__version__ = "0.21.0" diff --git a/pyproject.toml b/pyproject.toml index 633dac517..825b492ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ComfyUI" -version = "0.20.1" +version = "0.21.0" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" From b565dc7a6c03d2489b33626ef3d63cd09b912db3 Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Mon, 11 May 2026 11:37:15 +0300 Subject: [PATCH 038/145] [Partner Nodes] new Flux2ImageNode and GrokImageEditNodeV2 nodes with DynamicCombo and Autogrow (#13814) --- comfy_api_nodes/nodes_bfl.py | 171 ++++++++++++++++++++++++++++++ comfy_api_nodes/nodes_grok.py | 194 ++++++++++++++++++++++++++++++++++ 2 files changed, 365 insertions(+) diff --git a/comfy_api_nodes/nodes_bfl.py b/comfy_api_nodes/nodes_bfl.py index 23590bf24..3f0ce29d8 100644 --- a/comfy_api_nodes/nodes_bfl.py +++ b/comfy_api_nodes/nodes_bfl.py @@ -596,6 +596,7 @@ class Flux2ProImageNode(IO.ComfyNode): depends_on=IO.PriceBadgeDepends(widgets=["width", "height"], inputs=["images"]), expr=cls.PRICE_BADGE_EXPR, ), + is_deprecated=True, ) @classmethod @@ -674,6 +675,175 @@ class Flux2MaxImageNode(Flux2ProImageNode): """ +_FLUX2_MODEL_ENDPOINTS = { + "Flux.2 [pro]": "/proxy/bfl/flux-2-pro/generate", + "Flux.2 [max]": "/proxy/bfl/flux-2-max/generate", +} + + +def _flux2_model_inputs(): + return [ + IO.Int.Input( + "width", + default=1024, + min=256, + max=2048, + step=32, + ), + IO.Int.Input( + "height", + default=768, + min=256, + max=2048, + step=32, + ), + IO.Autogrow.Input( + "images", + template=IO.Autogrow.TemplateNames( + IO.Image.Input("image"), + names=[f"image_{i}" for i in range(1, 9)], + min=0, + ), + tooltip="Optional reference image(s) for image-to-image generation. Up to 8 images.", + ), + ] + + +class Flux2ImageNode(IO.ComfyNode): + + @classmethod + def define_schema(cls) -> IO.Schema: + return IO.Schema( + node_id="Flux2ImageNode", + display_name="Flux.2 Image", + category="api node/image/BFL", + description="Generate images via Flux.2 [pro] or Flux.2 [max] from a prompt and optional reference images.", + inputs=[ + IO.String.Input( + "prompt", + multiline=True, + default="", + tooltip="Prompt for the image generation or edit", + ), + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option("Flux.2 [pro]", _flux2_model_inputs()), + IO.DynamicCombo.Option("Flux.2 [max]", _flux2_model_inputs()), + ], + ), + IO.Int.Input( + "seed", + default=0, + min=0, + max=0xFFFFFFFFFFFFFFFF, + control_after_generate=True, + tooltip="The random seed used for creating the noise.", + ), + ], + outputs=[IO.Image.Output()], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends( + widgets=["model", "model.width", "model.height"], + input_groups=["model.images"], + ), + expr=""" + ( + $isMax := widgets.model = "flux.2 [max]"; + $MP := 1024 * 1024; + $w := $lookup(widgets, "model.width"); + $h := $lookup(widgets, "model.height"); + $outMP := $max([1, $floor((($w * $h) + $MP - 1) / $MP)]); + $outputCost := $isMax + ? (0.07 + 0.03 * ($outMP - 1)) + : (0.03 + 0.015 * ($outMP - 1)); + $refMin := $isMax ? 0.03 : 0.015; + $refMax := $isMax ? 0.24 : 0.12; + $hasRefs := $lookup(inputGroups, "model.images") > 0; + $hasRefs + ? { + "type": "range_usd", + "min_usd": $outputCost + $refMin, + "max_usd": $outputCost + $refMax, + "format": { "approximate": true } + } + : {"type": "usd", "usd": $outputCost} + ) + """, + ), + ) + + @classmethod + async def execute( + cls, + prompt: str, + model: dict, + seed: int, + ) -> IO.NodeOutput: + model_choice = model["model"] + endpoint = _FLUX2_MODEL_ENDPOINTS[model_choice] + width = model["width"] + height = model["height"] + images_dict = model.get("images") or {} + + image_tensors: list[Input.Image] = [t for t in images_dict.values() if t is not None] + n_images = sum(get_number_of_images(t) for t in image_tensors) + if n_images > 8: + raise ValueError("The current maximum number of supported images is 8.") + + flat_tensors: list[torch.Tensor] = [] + for tensor in image_tensors: + if len(tensor.shape) == 4: + flat_tensors.extend(tensor[i] for i in range(tensor.shape[0])) + else: + flat_tensors.append(tensor) + + reference_images: dict[str, str] = {} + for idx, tensor in enumerate(flat_tensors): + key_name = f"input_image_{idx + 1}" if idx else "input_image" + reference_images[key_name] = tensor_to_base64_string(tensor, total_pixels=2048 * 2048) + + initial_response = await sync_op( + cls, + ApiEndpoint(path=endpoint, method="POST"), + response_model=BFLFluxProGenerateResponse, + data=Flux2ProGenerateRequest( + prompt=prompt, + width=width, + height=height, + seed=seed, + **reference_images, + ), + ) + + def price_extractor(_r: BaseModel) -> float | None: + return None if initial_response.cost is None else initial_response.cost / 100 + + response = await poll_op( + cls, + ApiEndpoint(initial_response.polling_url), + response_model=BFLFluxStatusResponse, + status_extractor=lambda r: r.status, + progress_extractor=lambda r: r.progress, + price_extractor=price_extractor, + completed_statuses=[BFLStatus.ready], + failed_statuses=[ + BFLStatus.request_moderated, + BFLStatus.content_moderated, + BFLStatus.error, + BFLStatus.task_not_found, + ], + queued_statuses=[], + ) + return IO.NodeOutput(await download_url_to_image_tensor(response.result["sample"])) + + class BFLExtension(ComfyExtension): @override async def get_node_list(self) -> list[type[IO.ComfyNode]]: @@ -685,6 +855,7 @@ class BFLExtension(ComfyExtension): FluxProFillNode, Flux2ProImageNode, Flux2MaxImageNode, + Flux2ImageNode, ] diff --git a/comfy_api_nodes/nodes_grok.py b/comfy_api_nodes/nodes_grok.py index dd5d7e249..a103f24ee 100644 --- a/comfy_api_nodes/nodes_grok.py +++ b/comfy_api_nodes/nodes_grok.py @@ -162,6 +162,61 @@ class GrokImageNode(IO.ComfyNode): ) +_GROK_IMAGE_EDIT_ASPECT_RATIO_OPTIONS = [ + "auto", + "1:1", + "2:3", + "3:2", + "3:4", + "4:3", + "9:16", + "16:9", + "9:19.5", + "19.5:9", + "9:20", + "20:9", + "1:2", + "2:1", +] + + +def _grok_image_edit_model_inputs(*, max_ref_images: int, with_aspect_ratio: bool): + inputs = [ + IO.Autogrow.Input( + "images", + template=IO.Autogrow.TemplateNames( + IO.Image.Input("image"), + names=[f"image_{i}" for i in range(1, max_ref_images + 1)], + min=1, + ), + tooltip=( + "Reference image to edit." + if max_ref_images == 1 + else f"Reference image(s) to edit. Up to {max_ref_images} images." + ), + ), + IO.Combo.Input("resolution", options=["1K", "2K"]), + IO.Int.Input( + "number_of_images", + default=1, + min=1, + max=10, + step=1, + tooltip="Number of edited images to generate", + display_mode=IO.NumberDisplay.number, + ), + ] + if with_aspect_ratio: + inputs.append( + IO.Combo.Input( + "aspect_ratio", + options=_GROK_IMAGE_EDIT_ASPECT_RATIO_OPTIONS, + tooltip="Only allowed when multiple images are connected.", + ) + ) + return inputs + + class GrokImageEditNode(IO.ComfyNode): @classmethod @@ -256,6 +311,7 @@ class GrokImageEditNode(IO.ComfyNode): ) """, ), + is_deprecated=True, ) @classmethod @@ -303,6 +359,143 @@ class GrokImageEditNode(IO.ComfyNode): ) +class GrokImageEditNodeV2(IO.ComfyNode): + + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="GrokImageEditNodeV2", + display_name="Grok Image Edit", + category="api node/image/Grok", + description="Modify an existing image based on a text prompt", + inputs=[ + IO.String.Input( + "prompt", + multiline=True, + default="", + tooltip="The text prompt used to generate the image", + ), + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option( + "grok-imagine-image-quality", + _grok_image_edit_model_inputs(max_ref_images=3, with_aspect_ratio=True), + ), + IO.DynamicCombo.Option( + "grok-imagine-image-pro", + _grok_image_edit_model_inputs(max_ref_images=1, with_aspect_ratio=False), + ), + IO.DynamicCombo.Option( + "grok-imagine-image", + _grok_image_edit_model_inputs(max_ref_images=3, with_aspect_ratio=True), + ), + ], + ), + IO.Int.Input( + "seed", + default=0, + min=0, + max=2147483647, + step=1, + display_mode=IO.NumberDisplay.number, + control_after_generate=True, + tooltip="Seed to determine if node should re-run; " + "actual results are nondeterministic regardless of seed.", + ), + ], + outputs=[ + IO.Image.Output(), + ], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends( + widgets=["model", "model.resolution", "model.number_of_images"], + ), + expr=""" + ( + $isQualityModel := widgets.model = "grok-imagine-image-quality"; + $isPro := $contains(widgets.model, "pro"); + $res := $lookup(widgets, "model.resolution"); + $n := $lookup(widgets, "model.number_of_images"); + $rate := $isQualityModel + ? ($res = "1k" ? 0.05 : 0.07) + : ($isPro ? 0.07 : 0.02); + $base := $isQualityModel ? 0.01 : 0.002; + $output := $rate * $n; + $isPro + ? {"type":"usd","usd": $base + $output} + : {"type":"range_usd","min_usd": $base + $output, "max_usd": 3 * $base + $output} + ) + """, + ), + ) + + @classmethod + async def execute( + cls, + prompt: str, + model: dict, + seed: int, + ) -> IO.NodeOutput: + validate_string(prompt, strip_whitespace=True, min_length=1) + model_id = model["model"] + resolution = model["resolution"] + number_of_images = model["number_of_images"] + images_dict = model.get("images") or {} + aspect_ratio = model.get("aspect_ratio", "auto") + + image_tensors: list[Input.Image] = [t for t in images_dict.values() if t is not None] + n_images = sum(get_number_of_images(t) for t in image_tensors) + if n_images < 1: + raise ValueError("At least one image is required for editing.") + if model_id == "grok-imagine-image-pro" and n_images > 1: + raise ValueError("The pro model supports only 1 input image.") + if model_id != "grok-imagine-image-pro" and n_images > 3: + raise ValueError("A maximum of 3 input images is supported.") + if aspect_ratio != "auto" and n_images == 1: + raise ValueError( + "Custom aspect ratio is only allowed when multiple images are connected to the image input." + ) + + flat_tensors: list[torch.Tensor] = [] + for tensor in image_tensors: + if len(tensor.shape) == 4: + flat_tensors.extend(tensor[i] for i in range(tensor.shape[0])) + else: + flat_tensors.append(tensor) + + response = await sync_op( + cls, + ApiEndpoint(path="/proxy/xai/v1/images/edits", method="POST"), + data=ImageEditRequest( + model=model_id, + images=[ + InputUrlObject(url=f"data:image/png;base64,{tensor_to_base64_string(i)}") for i in flat_tensors + ], + prompt=prompt, + resolution=resolution.lower(), + n=number_of_images, + seed=seed, + aspect_ratio=None if aspect_ratio == "auto" else aspect_ratio, + ), + response_model=ImageGenerationResponse, + price_extractor=_extract_grok_price, + ) + if len(response.data) == 1: + return IO.NodeOutput(await download_url_to_image_tensor(response.data[0].url)) + return IO.NodeOutput( + torch.cat( + [await download_url_to_image_tensor(i) for i in [str(d.url) for d in response.data if d.url]], + ) + ) + + class GrokVideoNode(IO.ComfyNode): @classmethod @@ -737,6 +930,7 @@ class GrokExtension(ComfyExtension): return [ GrokImageNode, GrokImageEditNode, + GrokImageEditNodeV2, GrokVideoNode, GrokVideoReferenceNode, GrokVideoEditNode, From 46063aa9279d0192e6517073ab787330aaa53939 Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Mon, 11 May 2026 12:53:00 +0300 Subject: [PATCH 039/145] [Partner Nodes] new ByteDanceSeedreamNodeV2 node with DynamicCombo and autogrow (#13811) Signed-off-by: bigcat88 --- comfy_api_nodes/apis/bytedance.py | 56 +++++++ comfy_api_nodes/nodes_bytedance.py | 231 +++++++++++++++++++++++++++++ 2 files changed, 287 insertions(+) diff --git a/comfy_api_nodes/apis/bytedance.py b/comfy_api_nodes/apis/bytedance.py index c05bd6893..03f4c445b 100644 --- a/comfy_api_nodes/apis/bytedance.py +++ b/comfy_api_nodes/apis/bytedance.py @@ -198,6 +198,62 @@ RECOMMENDED_PRESETS_SEEDREAM_4 = [ ("Custom", None, None), ] +_PRESETS_SEEDREAM_1K = [ + ("(1K) 1024x1024 (1:1)", 1024, 1024), + ("(1K) 864x1152 (3:4)", 864, 1152), + ("(1K) 1152x864 (4:3)", 1152, 864), + ("(1K) 1312x736 (16:9)", 1312, 736), + ("(1K) 736x1312 (9:16)", 736, 1312), + ("(1K) 832x1248 (2:3)", 832, 1248), + ("(1K) 1248x832 (3:2)", 1248, 832), + ("(1K) 1568x672 (21:9)", 1568, 672), +] + +_PRESETS_SEEDREAM_2K = [ + ("(2K) 2048x2048 (1:1)", 2048, 2048), + ("(2K) 1728x2304 (3:4)", 1728, 2304), + ("(2K) 2304x1728 (4:3)", 2304, 1728), + ("(2K) 2848x1600 (16:9)", 2848, 1600), + ("(2K) 1600x2848 (9:16)", 1600, 2848), + ("(2K) 1664x2496 (2:3)", 1664, 2496), + ("(2K) 2496x1664 (3:2)", 2496, 1664), + ("(2K) 3136x1344 (21:9)", 3136, 1344), +] + +_PRESETS_SEEDREAM_3K = [ + ("(3K) 3072x3072 (1:1)", 3072, 3072), + ("(3K) 2592x3456 (3:4)", 2592, 3456), + ("(3K) 3456x2592 (4:3)", 3456, 2592), + ("(3K) 4096x2304 (16:9)", 4096, 2304), + ("(3K) 2304x4096 (9:16)", 2304, 4096), + ("(3K) 2496x3744 (2:3)", 2496, 3744), + ("(3K) 3744x2496 (3:2)", 3744, 2496), + ("(3K) 4704x2016 (21:9)", 4704, 2016), +] + +_PRESETS_SEEDREAM_4K = [ + ("(4K) 4096x4096 (1:1)", 4096, 4096), + ("(4K) 3520x4704 (3:4)", 3520, 4704), + ("(4K) 4704x3520 (4:3)", 4704, 3520), + ("(4K) 5504x3040 (16:9)", 5504, 3040), + ("(4K) 3040x5504 (9:16)", 3040, 5504), + ("(4K) 3328x4992 (2:3)", 3328, 4992), + ("(4K) 4992x3328 (3:2)", 4992, 3328), + ("(4K) 6240x2656 (21:9)", 6240, 2656), +] + +_CUSTOM_PRESET = [("Custom", None, None)] + +RECOMMENDED_PRESETS_SEEDREAM_5_LITE = ( + _PRESETS_SEEDREAM_2K + _PRESETS_SEEDREAM_3K + _PRESETS_SEEDREAM_4K + _CUSTOM_PRESET +) +RECOMMENDED_PRESETS_SEEDREAM_4_5 = ( + _PRESETS_SEEDREAM_2K + _PRESETS_SEEDREAM_4K + _CUSTOM_PRESET +) +RECOMMENDED_PRESETS_SEEDREAM_4_0 = ( + _PRESETS_SEEDREAM_1K + _PRESETS_SEEDREAM_2K + _PRESETS_SEEDREAM_4K + _CUSTOM_PRESET +) + # Seedance 2.0 reference video pixel count limits per model and output resolution. SEEDANCE2_REF_VIDEO_PIXEL_LIMITS = { "dreamina-seedance-2-0-260128": { diff --git a/comfy_api_nodes/nodes_bytedance.py b/comfy_api_nodes/nodes_bytedance.py index 5f74f4a14..d6b479336 100644 --- a/comfy_api_nodes/nodes_bytedance.py +++ b/comfy_api_nodes/nodes_bytedance.py @@ -10,6 +10,9 @@ from comfy_api.latest import IO, ComfyExtension, Input from comfy_api_nodes.apis.bytedance import ( RECOMMENDED_PRESETS, RECOMMENDED_PRESETS_SEEDREAM_4, + RECOMMENDED_PRESETS_SEEDREAM_4_0, + RECOMMENDED_PRESETS_SEEDREAM_4_5, + RECOMMENDED_PRESETS_SEEDREAM_5_LITE, SEEDANCE2_PRICE_PER_1K_TOKENS, SEEDANCE2_REF_VIDEO_PIXEL_LIMITS, VIDEO_TASKS_EXECUTION_TIME, @@ -68,6 +71,12 @@ SEEDREAM_MODELS = { "seedream-4-0-250828": "seedream-4-0-250828", } +SEEDREAM_PRESETS = { + "seedream-5-0-260128": RECOMMENDED_PRESETS_SEEDREAM_5_LITE, + "seedream-4-5-251128": RECOMMENDED_PRESETS_SEEDREAM_4_5, + "seedream-4-0-250828": RECOMMENDED_PRESETS_SEEDREAM_4_0, +} + # Long-running tasks endpoints(e.g., video) BYTEPLUS_TASK_ENDPOINT = "/proxy/byteplus/api/v3/contents/generations/tasks" BYTEPLUS_TASK_STATUS_ENDPOINT = "/proxy/byteplus/api/v3/contents/generations/tasks" # + /{task_id} @@ -562,6 +571,7 @@ class ByteDanceSeedreamNode(IO.ComfyNode): ) """, ), + is_deprecated=True, ) @classmethod @@ -651,6 +661,226 @@ class ByteDanceSeedreamNode(IO.ComfyNode): return IO.NodeOutput(torch.cat([await download_url_to_image_tensor(i) for i in urls])) +def _seedream_model_inputs(*, max_ref_images: int, presets: list): + return [ + IO.Combo.Input( + "size_preset", + options=[label for label, _, _ in presets], + tooltip="Pick a recommended size. Select Custom to use the width and height below.", + ), + IO.Int.Input( + "width", + default=2048, + min=1024, + max=6240, + step=2, + tooltip="Custom width for image. Value is working only if `size_preset` is set to `Custom`", + ), + IO.Int.Input( + "height", + default=2048, + min=1024, + max=4992, + step=2, + tooltip="Custom height for image. Value is working only if `size_preset` is set to `Custom`", + ), + IO.Int.Input( + "max_images", + default=1, + min=1, + max=max_ref_images, + step=1, + display_mode=IO.NumberDisplay.number, + tooltip="Maximum number of images to generate. With 1, exactly one image is produced. " + "With >1, the model generates between 1 and max_images related images " + "(e.g., story scenes, character variations). " + "Total images (input + generated) cannot exceed 15.", + ), + IO.Autogrow.Input( + "images", + template=IO.Autogrow.TemplateNames( + IO.Image.Input("image"), + names=[f"image_{i}" for i in range(1, max_ref_images + 1)], + min=0, + ), + tooltip=f"Optional reference image(s) for image-to-image or multi-reference generation. " + f"Up to {max_ref_images} images.", + ), + IO.Boolean.Input( + "fail_on_partial", + default=False, + tooltip="If enabled, abort execution if any requested images are missing or return an error.", + advanced=True, + ), + ] + + +class ByteDanceSeedreamNodeV2(IO.ComfyNode): + + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="ByteDanceSeedreamNodeV2", + display_name="ByteDance Seedream 4.5 & 5.0", + category="api node/image/ByteDance", + description="Unified text-to-image generation and precise single-sentence editing at up to 4K resolution.", + inputs=[ + IO.String.Input( + "prompt", + multiline=True, + default="", + tooltip="Text prompt for creating or editing an image.", + ), + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option( + "seedream 5.0 lite", + _seedream_model_inputs(max_ref_images=14, presets=RECOMMENDED_PRESETS_SEEDREAM_5_LITE), + ), + IO.DynamicCombo.Option( + "seedream-4-5-251128", + _seedream_model_inputs(max_ref_images=10, presets=RECOMMENDED_PRESETS_SEEDREAM_4_5), + ), + IO.DynamicCombo.Option( + "seedream-4-0-250828", + _seedream_model_inputs(max_ref_images=10, presets=RECOMMENDED_PRESETS_SEEDREAM_4_0), + ), + ], + ), + IO.Int.Input( + "seed", + default=0, + min=0, + max=2147483647, + step=1, + display_mode=IO.NumberDisplay.number, + control_after_generate=True, + tooltip="Seed to use for generation.", + ), + IO.Boolean.Input( + "watermark", + default=False, + tooltip='Whether to add an "AI generated" watermark to the image.', + advanced=True, + ), + ], + outputs=[ + IO.Image.Output(), + ], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends(widgets=["model"]), + expr=""" + ( + $price := $contains(widgets.model, "5.0 lite") ? 0.035 : + $contains(widgets.model, "4-5") ? 0.04 : 0.03; + { + "type":"usd", + "usd": $price, + "format": { "suffix":" x images/Run", "approximate": true } + } + ) + """, + ), + ) + + @classmethod + async def execute( + cls, + prompt: str, + model: dict, + seed: int = 0, + watermark: bool = False, + ) -> IO.NodeOutput: + validate_string(prompt, strip_whitespace=True, min_length=1) + model_id = SEEDREAM_MODELS[model["model"]] + presets = SEEDREAM_PRESETS[model_id] + + size_preset = model.get("size_preset", presets[0][0]) + width = model.get("width", 2048) + height = model.get("height", 2048) + max_images = model.get("max_images", 1) + sequential_image_generation = "disabled" if max_images == 1 else "auto" + images_dict = model.get("images") or {} + fail_on_partial = model.get("fail_on_partial", False) + + w = h = None + for label, tw, th in presets: + if label == size_preset: + w, h = tw, th + break + if w is None or h is None: + w, h = width, height + + out_num_pixels = w * h + mp_provided = out_num_pixels / 1_000_000.0 + if ("seedream-4-5" in model_id or "seedream-5-0" in model_id) and out_num_pixels < 3686400: + raise ValueError( + f"Minimum image resolution for the selected model is 3.68MP, but {mp_provided:.2f}MP provided." + ) + if "seedream-4-0" in model_id and out_num_pixels < 921600: + raise ValueError( + f"Minimum image resolution that the selected model can generate is 0.92MP, " + f"but {mp_provided:.2f}MP provided." + ) + if out_num_pixels > 16_777_216: + raise ValueError( + f"Maximum image resolution for the selected model is 16.78MP, but {mp_provided:.2f}MP provided." + ) + + image_tensors: list[Input.Image] = [t for t in images_dict.values() if t is not None] + n_input_images = sum(get_number_of_images(t) for t in image_tensors) + max_num_of_images = 14 if model_id == "seedream-5-0-260128" else 10 + if n_input_images > max_num_of_images: + raise ValueError( + f"Maximum of {max_num_of_images} reference images are supported, but {n_input_images} received." + ) + if sequential_image_generation == "auto" and n_input_images + max_images > 15: + raise ValueError( + "The maximum number of generated images plus the number of reference images cannot exceed 15." + ) + + reference_images_urls: list[str] = [] + if image_tensors: + for tensor in image_tensors: + validate_image_aspect_ratio(tensor, (1, 3), (3, 1)) + reference_images_urls = await upload_images_to_comfyapi( + cls, + image_tensors, + max_images=n_input_images, + mime_type="image/png", + wait_label="Uploading reference images", + ) + + response = await sync_op( + cls, + ApiEndpoint(path=BYTEPLUS_IMAGE_ENDPOINT, method="POST"), + response_model=ImageTaskCreationResponse, + data=Seedream4TaskCreationRequest( + model=model_id, + prompt=prompt, + image=reference_images_urls, + size=f"{w}x{h}", + seed=seed, + sequential_image_generation=sequential_image_generation, + sequential_image_generation_options=Seedream4Options(max_images=max_images), + watermark=watermark, + ), + ) + if len(response.data) == 1: + return IO.NodeOutput(await download_url_to_image_tensor(get_image_url_from_response(response))) + urls = [str(d["url"]) for d in response.data if isinstance(d, dict) and "url" in d] + if fail_on_partial and len(urls) < len(response.data): + raise RuntimeError(f"Only {len(urls)} of {len(response.data)} images were generated before error.") + return IO.NodeOutput(torch.cat([await download_url_to_image_tensor(i) for i in urls])) + + class ByteDanceTextToVideoNode(IO.ComfyNode): @classmethod @@ -2105,6 +2335,7 @@ class ByteDanceExtension(ComfyExtension): return [ ByteDanceImageNode, ByteDanceSeedreamNode, + ByteDanceSeedreamNodeV2, ByteDanceTextToVideoNode, ByteDanceImageToVideoNode, ByteDanceFirstLastFrameNode, From 428c323780a7549a4da03b8d282d0064c8e24180 Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Mon, 11 May 2026 16:19:35 +0300 Subject: [PATCH 040/145] [Partner Nodes] new OpenAI Image node with DynamicCombo and Autogrow (#13838) --- comfy_api_nodes/nodes_openai.py | 313 ++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) diff --git a/comfy_api_nodes/nodes_openai.py b/comfy_api_nodes/nodes_openai.py index daed495da..a5a188634 100644 --- a/comfy_api_nodes/nodes_openai.py +++ b/comfy_api_nodes/nodes_openai.py @@ -27,6 +27,7 @@ from comfy_api_nodes.util import ( ApiEndpoint, download_url_to_bytesio, downscale_image_tensor, + get_number_of_images, poll_op, sync_op, tensor_to_base64_string, @@ -372,6 +373,7 @@ class OpenAIGPTImage1(IO.ComfyNode): display_name="OpenAI GPT Image 2", category="api node/image/OpenAI", description="Generates images synchronously via OpenAI's GPT Image endpoint.", + is_deprecated=True, inputs=[ IO.String.Input( "prompt", @@ -640,6 +642,316 @@ class OpenAIGPTImage1(IO.ComfyNode): return IO.NodeOutput(await validate_and_cast_response(response)) +def _gpt_image_shared_inputs(): + """Inputs shared by all GPT Image models (quality + reference images + mask).""" + return [ + IO.Combo.Input( + "quality", + default="low", + options=["low", "medium", "high"], + tooltip="Image quality, affects cost and generation time.", + ), + IO.Autogrow.Input( + "images", + template=IO.Autogrow.TemplateNames( + IO.Image.Input("image"), + names=[f"image_{i}" for i in range(1, 17)], + min=0, + ), + tooltip="Optional reference image(s) for image editing. Up to 16 images.", + ), + IO.Mask.Input( + "mask", + optional=True, + tooltip="Optional mask for inpainting (white areas will be replaced). " + "Requires exactly one reference image.", + ), + ] + + +def _gpt_image_legacy_model_inputs(): + """Per-model widget set for legacy gpt-image-1 / gpt-image-1.5 (4 base sizes, transparent bg allowed).""" + return [ + IO.Combo.Input( + "size", + default="auto", + options=["auto", "1024x1024", "1024x1536", "1536x1024"], + tooltip="Image size.", + ), + IO.Combo.Input( + "background", + default="auto", + options=["auto", "opaque", "transparent"], + tooltip="Return image with or without background.", + ), + *_gpt_image_shared_inputs(), + ] + + +class OpenAIGPTImageNodeV2(IO.ComfyNode): + + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="OpenAIGPTImageNodeV2", + display_name="OpenAI GPT Image 2", + category="api node/image/OpenAI", + description="Generates images via OpenAI's GPT Image endpoint.", + inputs=[ + IO.String.Input( + "prompt", + default="", + multiline=True, + tooltip="Text prompt for GPT Image", + ), + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option( + "gpt-image-2", + [ + IO.Combo.Input( + "size", + default="auto", + options=[ + "auto", + "1024x1024", + "1024x1536", + "1536x1024", + "2048x2048", + "2048x1152", + "1152x2048", + "3840x2160", + "2160x3840", + "Custom", + ], + tooltip="Image size. Select 'Custom' to use the custom width and height.", + ), + IO.Int.Input( + "custom_width", + default=1024, + min=1024, + max=3840, + step=16, + tooltip="Used only when `size` is 'Custom'. Must be a multiple of 16.", + ), + IO.Int.Input( + "custom_height", + default=1024, + min=1024, + max=3840, + step=16, + tooltip="Used only when `size` is 'Custom'. Must be a multiple of 16.", + ), + IO.Combo.Input( + "background", + default="auto", + options=["auto", "opaque"], + tooltip="Return image with or without background.", + ), + *_gpt_image_shared_inputs(), + ], + ), + IO.DynamicCombo.Option("gpt-image-1.5", _gpt_image_legacy_model_inputs()), + IO.DynamicCombo.Option("gpt-image-1", _gpt_image_legacy_model_inputs()), + ], + ), + IO.Int.Input( + "n", + default=1, + min=1, + max=8, + step=1, + tooltip="How many images to generate", + display_mode=IO.NumberDisplay.number, + ), + IO.Int.Input( + "seed", + default=0, + min=0, + max=2147483647, + step=1, + display_mode=IO.NumberDisplay.number, + control_after_generate=True, + tooltip="not implemented yet in backend", + ), + ], + outputs=[IO.Image.Output()], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends(widgets=["model", "model.quality", "n"]), + expr=""" + ( + $ranges := { + "gpt-image-1": { + "low": [0.011, 0.02], + "medium": [0.042, 0.07], + "high": [0.167, 0.25] + }, + "gpt-image-1.5": { + "low": [0.009, 0.02], + "medium": [0.034, 0.062], + "high": [0.133, 0.22] + }, + "gpt-image-2": { + "low": [0.0048, 0.019], + "medium": [0.041, 0.168], + "high": [0.165, 0.67] + } + }; + $range := $lookup($lookup($ranges, widgets.model), $lookup(widgets, "model.quality")); + $nRaw := widgets.n; + $n := ($nRaw != null and $nRaw != 0) ? $nRaw : 1; + ($n = 1) + ? {"type":"range_usd","min_usd": $range[0], "max_usd": $range[1], "format": {"approximate": true}} + : { + "type":"range_usd", + "min_usd": $range[0] * $n, + "max_usd": $range[1] * $n, + "format": { "suffix": "/Run", "approximate": true } + } + ) + """, + ), + ) + + @classmethod + async def execute( + cls, + prompt: str, + model: dict, + n: int, + seed: int, + ) -> IO.NodeOutput: + validate_string(prompt, strip_whitespace=False) + + model_id = model["model"] + size = model["size"] + background = model["background"] + quality = model["quality"] + custom_width = model.get("custom_width", 1024) + custom_height = model.get("custom_height", 1024) + + images_dict = model.get("images") or {} + image_tensors: list[Input.Image] = [t for t in images_dict.values() if t is not None] + n_images = sum(get_number_of_images(t) for t in image_tensors) + mask = model.get("mask") + + if mask is not None and n_images == 0: + raise ValueError("Cannot use a mask without an input image") + + if size == "Custom": + if custom_width % 16 != 0 or custom_height % 16 != 0: + raise ValueError( + f"Custom width and height must be multiples of 16, got {custom_width}x{custom_height}" + ) + if max(custom_width, custom_height) > 3840: + raise ValueError( + f"Custom resolution max edge must be <= 3840, got {custom_width}x{custom_height}" + ) + ratio = max(custom_width, custom_height) / min(custom_width, custom_height) + if ratio > 3: + raise ValueError( + f"Custom resolution aspect ratio must not exceed 3:1, got {custom_width}x{custom_height}" + ) + total_pixels = custom_width * custom_height + if not 655_360 <= total_pixels <= 8_294_400: + raise ValueError( + f"Custom resolution total pixels must be between 655,360 and 8,294,400, got {total_pixels}" + ) + size = f"{custom_width}x{custom_height}" + + if model_id == "gpt-image-1": + price_extractor = calculate_tokens_price_image_1 + elif model_id == "gpt-image-1.5": + price_extractor = calculate_tokens_price_image_1_5 + elif model_id == "gpt-image-2": + price_extractor = calculate_tokens_price_image_2_0 + else: + raise ValueError(f"Unknown model: {model_id}") + + if image_tensors: + flat: list[torch.Tensor] = [] + for tensor in image_tensors: + if len(tensor.shape) == 4: + flat.extend(tensor[i : i + 1] for i in range(tensor.shape[0])) + else: + flat.append(tensor.unsqueeze(0)) + + files = [] + for i, single_image in enumerate(flat): + scaled_image = downscale_image_tensor(single_image, total_pixels=2048 * 2048).squeeze() + image_np = (scaled_image.numpy() * 255).astype(np.uint8) + img = Image.fromarray(image_np) + img_byte_arr = BytesIO() + img.save(img_byte_arr, format="PNG") + img_byte_arr.seek(0) + + if len(flat) == 1: + files.append(("image", (f"image_{i}.png", img_byte_arr, "image/png"))) + else: + files.append(("image[]", (f"image_{i}.png", img_byte_arr, "image/png"))) + + if mask is not None: + if len(flat) != 1: + raise Exception("Cannot use a mask with multiple image") + ref_image = flat[0] + if mask.shape[1:] != ref_image.shape[1:-1]: + raise Exception("Mask and Image must be the same size") + _, height, width = mask.shape + rgba_mask = torch.zeros(height, width, 4, device="cpu") + rgba_mask[:, :, 3] = 1 - mask.squeeze().cpu() + scaled_mask = downscale_image_tensor( + rgba_mask.unsqueeze(0), total_pixels=2048 * 2048 + ).squeeze() + mask_np = (scaled_mask.numpy() * 255).astype(np.uint8) + mask_img = Image.fromarray(mask_np) + mask_img_byte_arr = BytesIO() + mask_img.save(mask_img_byte_arr, format="PNG") + mask_img_byte_arr.seek(0) + files.append(("mask", ("mask.png", mask_img_byte_arr, "image/png"))) + + response = await sync_op( + cls, + ApiEndpoint(path="/proxy/openai/images/edits", method="POST"), + response_model=OpenAIImageGenerationResponse, + data=OpenAIImageEditRequest( + model=model_id, + prompt=prompt, + quality=quality, + background=background, + n=n, + size=size, + moderation="low", + ), + content_type="multipart/form-data", + files=files, + price_extractor=price_extractor, + ) + else: + response = await sync_op( + cls, + ApiEndpoint(path="/proxy/openai/images/generations", method="POST"), + response_model=OpenAIImageGenerationResponse, + data=OpenAIImageGenerationRequest( + model=model_id, + prompt=prompt, + quality=quality, + background=background, + n=n, + size=size, + moderation="low", + ), + price_extractor=price_extractor, + ) + return IO.NodeOutput(await validate_and_cast_response(response)) + + class OpenAIChatNode(IO.ComfyNode): """ Node to generate text responses from an OpenAI model. @@ -999,6 +1311,7 @@ class OpenAIExtension(ComfyExtension): OpenAIDalle2, OpenAIDalle3, OpenAIGPTImage1, + OpenAIGPTImageNodeV2, OpenAIChatNode, OpenAIInputFiles, OpenAIChatConfig, From 20e439419c298e8fef6b4dcba2c62831dcf7722f Mon Sep 17 00:00:00 2001 From: rattus <46076784+rattus128@users.noreply.github.com> Date: Tue, 12 May 2026 05:48:10 +1000 Subject: [PATCH 041/145] model_patcher: Fix safetensors saving of fp8 (#13835) This was missing proper weight scale casting in the saving path. --- comfy/model_patcher.py | 54 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/comfy/model_patcher.py b/comfy/model_patcher.py index 33bdedfb1..2ea14bc2c 100644 --- a/comfy/model_patcher.py +++ b/comfy/model_patcher.py @@ -242,6 +242,37 @@ class LazyCastingParam(torch.nn.Parameter): return self.model.patch_weight_to_device(self.key, device_to=self.model.load_device, return_weight=True).to("cpu") +class LazyCastingQuantizedParam: + def __init__(self, model, key): + self.model = model + self.key = key + self.cpu_state_dict = None + + def state_dict_tensor(self, state_dict_key): + if self.cpu_state_dict is None: + weight = self.model.patch_weight_to_device(self.key, device_to=self.model.load_device, return_weight=True) + self.cpu_state_dict = {k: v.to("cpu") for k, v in weight.state_dict(self.key).items()} + return self.cpu_state_dict[state_dict_key] + + +class LazyCastingParamPiece(torch.nn.Parameter): + def __new__(cls, caster, state_dict_key, tensor): + return super().__new__(cls, tensor) + + def __init__(self, caster, state_dict_key, tensor): + self.caster = caster + self.state_dict_key = state_dict_key + + @property + def device(self): + return CustomTorchDevice + + def to(self, *args, **kwargs): + caster = self.caster + del self.caster + return caster.state_dict_tensor(self.state_dict_key) + + class ModelPatcher: def __init__(self, model, load_device, offload_device, size=0, weight_inplace_update=False): self.size = size @@ -1463,20 +1494,37 @@ class ModelPatcher: self.clear_cached_hook_weights() def state_dict_for_saving(self, clip_state_dict=None, vae_state_dict=None, clip_vision_state_dict=None): - unet_state_dict = self.model.diffusion_model.state_dict() - for k, v in unet_state_dict.items(): + original_state_dict = self.model.diffusion_model.state_dict() + unet_state_dict = {} + keys = list(original_state_dict) + while len(keys) > 0: + k = keys.pop(0) + v = original_state_dict[k] op_keys = k.rsplit('.', 1) if (len(op_keys) < 2) or op_keys[1] not in ["weight", "bias"]: + unet_state_dict[k] = v continue try: op = comfy.utils.get_attr(self.model.diffusion_model, op_keys[0]) except: + unet_state_dict[k] = v continue if not op or not hasattr(op, "comfy_cast_weights") or \ (hasattr(op, "comfy_patched_weights") and op.comfy_patched_weights == True): + unet_state_dict[k] = v continue key = "diffusion_model." + k - unet_state_dict[k] = LazyCastingParam(self, key, comfy.utils.get_attr(self.model, key)) + weight = comfy.utils.get_attr(self.model, key) + if isinstance(weight, QuantizedTensor) and k in original_state_dict: + qt_state_dict = weight.state_dict(k) + caster = LazyCastingQuantizedParam(self, key) + for group_key in (x for x in qt_state_dict if x in original_state_dict): + if group_key in keys: + keys.remove(group_key) + unet_state_dict.pop(group_key, "") + unet_state_dict[group_key] = LazyCastingParamPiece(caster, "diffusion_model." + group_key, original_state_dict[group_key]) + continue + unet_state_dict[k] = LazyCastingParam(self, key, weight) return self.model.state_dict_for_saving(unet_state_dict, clip_state_dict=clip_state_dict, vae_state_dict=vae_state_dict, clip_vision_state_dict=clip_vision_state_dict) def __del__(self): From 0a7d2ffd680111aefa3d538691bce320bda610c4 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Mon, 11 May 2026 20:01:52 -0700 Subject: [PATCH 042/145] Support anima TE lora kohya format. (#13847) --- comfy/lora.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/comfy/lora.py b/comfy/lora.py index db8f16bcb..f11e26ec9 100644 --- a/comfy/lora.py +++ b/comfy/lora.py @@ -97,12 +97,14 @@ def load_lora(lora, to_load, log_missing=True): def model_lora_keys_clip(model, key_map={}): sdk = model.state_dict().keys() + prefix_set = set() for k in sdk: if k.endswith(".weight"): key_map["text_encoders.{}".format(k[:-len(".weight")])] = k #generic lora format without any weird key names tp = k.find(".transformer.") #also map without wrapper prefix for composite text encoder models if tp > 0 and not k.startswith("clip_"): key_map["text_encoders.{}".format(k[tp + 1:-len(".weight")])] = k + prefix_set.add(k.split('.')[0]) text_model_lora_key = "lora_te_text_model_encoder_layers_{}_{}" clip_l_present = False @@ -163,6 +165,13 @@ def model_lora_keys_clip(model, key_map={}): lora_key = "lora_te1_{}".format(l_key.replace(".", "_")) key_map[lora_key] = k + if len(prefix_set) == 1: + full_prefix = "{}.transformer.model.".format(next(iter(prefix_set))) # kohya anima and maybe other single TE models that use a single llama arch based te + for k in sdk: + if k.endswith(".weight"): + if k.startswith(full_prefix): + l_key = k[len(full_prefix):-len(".weight")] + key_map["lora_te_{}".format(l_key.replace(".", "_"))] = k k = "clip_g.transformer.text_projection.weight" if k in sdk: From 8e53f001a492cc818768a308362adbd3d75a1c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Sepp=C3=A4nen?= <40791699+kijai@users.noreply.github.com> Date: Tue, 12 May 2026 06:35:53 +0300 Subject: [PATCH 043/145] feat: Support HiDream-O1-Image (CORE-187) (#13817) * Initial HiDream01-image support * Cleanup nodes * Cleaner handling of empty placeholder models * Remove snap_to_predefined, prefer tooltip for the trained resolutions * Add model and block wrappers * Fix shift tooltip * Add node to work around the patch tile issue Experimental, runs multiple passes with the patch grid offset and blends with various different methods. * Qwen35 vision rotary_pos_emb cast fix * Fix embedding layout type * Some small optimizations * Cleanup, don't need this fallback * Prefix KV cache, cleanup Bit of speed, reduce redundant code * Get rid of redundant custom sampler, refactor noise scaling Our existing lcm sampler is mathematically same, just added the missing options to it instead and a node to control them. Refactored the noise scaling and fix it for the stochastic samplers, add a generic node to control the initial noise scale. * Update nodes_hidream_o1.py * Fix some cache validation cases * Keep existing sampling params * Remove redundant video vision path * Replace some numpy ops with torch * Fx RoPE index for batch size > 1 * Prefer torch preprocessing * Rename block_type to be compatible with existing patch nodes * Fixes and tweaks --- comfy/k_diffusion/sampling.py | 41 +++- comfy/latent_formats.py | 7 + comfy/ldm/hidream_o1/attention.py | 41 ++++ comfy/ldm/hidream_o1/conditioning.py | 230 ++++++++++++++++++ comfy/ldm/hidream_o1/model.py | 306 ++++++++++++++++++++++++ comfy/ldm/hidream_o1/utils.py | 173 ++++++++++++++ comfy/model_base.py | 28 +++ comfy/model_detection.py | 3 + comfy/model_sampling.py | 12 +- comfy/ops.py | 3 +- comfy/sd.py | 4 +- comfy/supported_models.py | 46 ++++ comfy/text_encoders/hidream_o1.py | 119 +++++++++ comfy/text_encoders/llama.py | 32 ++- comfy/text_encoders/qwen35.py | 2 +- comfy_extras/nodes_advanced_samplers.py | 32 +++ comfy_extras/nodes_hidream_o1.py | 256 ++++++++++++++++++++ comfy_extras/nodes_model_advanced.py | 24 ++ nodes.py | 1 + 19 files changed, 1339 insertions(+), 21 deletions(-) create mode 100644 comfy/ldm/hidream_o1/attention.py create mode 100644 comfy/ldm/hidream_o1/conditioning.py create mode 100644 comfy/ldm/hidream_o1/model.py create mode 100644 comfy/ldm/hidream_o1/utils.py create mode 100644 comfy/text_encoders/hidream_o1.py create mode 100644 comfy_extras/nodes_hidream_o1.py diff --git a/comfy/k_diffusion/sampling.py b/comfy/k_diffusion/sampling.py index c53ac4b2b..11db46d94 100644 --- a/comfy/k_diffusion/sampling.py +++ b/comfy/k_diffusion/sampling.py @@ -242,6 +242,7 @@ def sample_euler_ancestral_RF(model, x, sigmas, extra_args=None, callback=None, extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler + s_noise = s_noise * getattr(model.inner_model.model_patcher.get_model_object('model_sampling'), "noise_scale", 1.0) s_in = x.new_ones([x.shape[0]]) for i in trange(len(sigmas) - 1, disable=disable): denoised = model(x, sigmas[i] * s_in, **extra_args) @@ -373,6 +374,7 @@ def sample_dpm_2_ancestral_RF(model, x, sigmas, extra_args=None, callback=None, extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler + s_noise = s_noise * getattr(model.inner_model.model_patcher.get_model_object('model_sampling'), "noise_scale", 1.0) s_in = x.new_ones([x.shape[0]]) for i in trange(len(sigmas) - 1, disable=disable): denoised = model(x, sigmas[i] * s_in, **extra_args) @@ -686,6 +688,7 @@ def sample_dpmpp_2s_ancestral_RF(model, x, sigmas, extra_args=None, callback=Non extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler + s_noise = s_noise * getattr(model.inner_model.model_patcher.get_model_object('model_sampling'), "noise_scale", 1.0) s_in = x.new_ones([x.shape[0]]) sigma_fn = lambda lbda: (lbda.exp() + 1) ** -1 lambda_fn = lambda sigma: ((1-sigma)/sigma).log() @@ -747,6 +750,7 @@ def sample_dpmpp_sde(model, x, sigmas, extra_args=None, callback=None, disable=N sigma_fn = partial(half_log_snr_to_sigma, model_sampling=model_sampling) lambda_fn = partial(sigma_to_half_log_snr, model_sampling=model_sampling) sigmas = offset_first_sigma_for_snr(sigmas, model_sampling) + s_noise = s_noise * getattr(model_sampling, "noise_scale", 1.0) for i in trange(len(sigmas) - 1, disable=disable): denoised = model(x, sigmas[i] * s_in, **extra_args) @@ -832,6 +836,7 @@ def sample_dpmpp_2m_sde(model, x, sigmas, extra_args=None, callback=None, disabl model_sampling = model.inner_model.model_patcher.get_model_object('model_sampling') lambda_fn = partial(sigma_to_half_log_snr, model_sampling=model_sampling) sigmas = offset_first_sigma_for_snr(sigmas, model_sampling) + s_noise = s_noise * getattr(model_sampling, "noise_scale", 1.0) old_denoised = None h, h_last = None, None @@ -889,6 +894,7 @@ def sample_dpmpp_3m_sde(model, x, sigmas, extra_args=None, callback=None, disabl model_sampling = model.inner_model.model_patcher.get_model_object('model_sampling') lambda_fn = partial(sigma_to_half_log_snr, model_sampling=model_sampling) sigmas = offset_first_sigma_for_snr(sigmas, model_sampling) + s_noise = s_noise * getattr(model_sampling, "noise_scale", 1.0) denoised_1, denoised_2 = None, None h, h_1, h_2 = None, None, None @@ -1006,23 +1012,39 @@ def sample_ddpm(model, x, sigmas, extra_args=None, callback=None, disable=None, return generic_step_sampler(model, x, sigmas, extra_args, callback, disable, noise_sampler, DDPMSampler_step) @torch.no_grad() -def sample_lcm(model, x, sigmas, extra_args=None, callback=None, disable=None, noise_sampler=None): +def sample_lcm(model, x, sigmas, extra_args=None, callback=None, disable=None, noise_sampler=None, s_noise=1.0, s_noise_end=None, noise_clip_std=0.0): + + # s_noise / s_noise_end: per-step noise multiplier, linearly interpolated across steps + # noise_clip_std: clamp injected noise to +/- N stddevs (0 disables). + extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler s_in = x.new_ones([x.shape[0]]) - for i in trange(len(sigmas) - 1, disable=disable): + n_steps = max(1, len(sigmas) - 1) + model_sampling = model.inner_model.model_patcher.get_model_object('model_sampling') + + s_start = float(s_noise) + s_end = s_start if s_noise_end is None else float(s_noise_end) + for i in trange(n_steps, disable=disable): denoised = model(x, sigmas[i] * s_in, **extra_args) if callback is not None: callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised}) x = denoised if sigmas[i + 1] > 0: - x = model.inner_model.inner_model.model_sampling.noise_scaling(sigmas[i + 1], noise_sampler(sigmas[i], sigmas[i + 1]), x) + noise = noise_sampler(sigmas[i], sigmas[i + 1]) + if noise_clip_std > 0: + clip_val = noise_clip_std * noise.std() + noise = noise.clamp(min=-clip_val, max=clip_val) + t = (i / (n_steps - 1)) if n_steps > 1 else 0.0 + s_noise_i = s_start + (s_end - s_start) * t + if s_noise_i != 1.0: + noise = noise * s_noise_i + x = model_sampling.noise_scaling(sigmas[i + 1], noise, x) return x - @torch.no_grad() def sample_heunpp2(model, x, sigmas, extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.): # From MIT licensed: https://github.com/Carzit/sd-webui-samplers-scheduler/ @@ -1249,6 +1271,7 @@ def sample_euler_ancestral_cfg_pp(model, x, sigmas, extra_args=None, callback=No model_sampling = model.inner_model.model_patcher.get_model_object("model_sampling") lambda_fn = partial(sigma_to_half_log_snr, model_sampling=model_sampling) + s_noise = s_noise * getattr(model_sampling, "noise_scale", 1.0) uncond_denoised = None @@ -1296,6 +1319,7 @@ def sample_dpmpp_2s_ancestral_cfg_pp(model, x, sigmas, extra_args=None, callback extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler + s_noise = s_noise * getattr(model.inner_model.model_patcher.get_model_object('model_sampling'), "noise_scale", 1.0) temp = [0] def post_cfg_function(args): @@ -1371,6 +1395,7 @@ def res_multistep(model, x, sigmas, extra_args=None, callback=None, disable=None extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler + s_noise = s_noise * getattr(model.inner_model.model_patcher.get_model_object('model_sampling'), "noise_scale", 1.0) s_in = x.new_ones([x.shape[0]]) sigma_fn = lambda t: t.neg().exp() t_fn = lambda sigma: sigma.log().neg() @@ -1504,6 +1529,7 @@ def sample_er_sde(model, x, sigmas, extra_args=None, callback=None, disable=None extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler + s_noise = s_noise * getattr(model.inner_model.model_patcher.get_model_object('model_sampling'), "noise_scale", 1.0) s_in = x.new_ones([x.shape[0]]) def default_er_sde_noise_scaler(x): @@ -1574,9 +1600,10 @@ def sample_seeds_2(model, x, sigmas, extra_args=None, callback=None, disable=Non seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler s_in = x.new_ones([x.shape[0]]) - inject_noise = eta > 0 and s_noise > 0 model_sampling = model.inner_model.model_patcher.get_model_object('model_sampling') + s_noise = s_noise * getattr(model_sampling, "noise_scale", 1.0) + inject_noise = eta > 0 and s_noise > 0 sigma_fn = partial(half_log_snr_to_sigma, model_sampling=model_sampling) lambda_fn = partial(sigma_to_half_log_snr, model_sampling=model_sampling) sigmas = offset_first_sigma_for_snr(sigmas, model_sampling) @@ -1645,9 +1672,10 @@ def sample_seeds_3(model, x, sigmas, extra_args=None, callback=None, disable=Non seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler s_in = x.new_ones([x.shape[0]]) - inject_noise = eta > 0 and s_noise > 0 model_sampling = model.inner_model.model_patcher.get_model_object('model_sampling') + s_noise = s_noise * getattr(model_sampling, "noise_scale", 1.0) + inject_noise = eta > 0 and s_noise > 0 sigma_fn = partial(half_log_snr_to_sigma, model_sampling=model_sampling) lambda_fn = partial(sigma_to_half_log_snr, model_sampling=model_sampling) sigmas = offset_first_sigma_for_snr(sigmas, model_sampling) @@ -1713,6 +1741,7 @@ def sample_sa_solver(model, x, sigmas, extra_args=None, callback=None, disable=F s_in = x.new_ones([x.shape[0]]) model_sampling = model.inner_model.model_patcher.get_model_object("model_sampling") + s_noise = s_noise * getattr(model_sampling, "noise_scale", 1.0) sigmas = offset_first_sigma_for_snr(sigmas, model_sampling) lambdas = sigma_to_half_log_snr(sigmas, model_sampling=model_sampling) diff --git a/comfy/latent_formats.py b/comfy/latent_formats.py index 91bebed3d..d527eec4a 100644 --- a/comfy/latent_formats.py +++ b/comfy/latent_formats.py @@ -792,6 +792,13 @@ class ZImagePixelSpace(ChromaRadiance): """ pass + +class HiDreamO1Pixel(ChromaRadiance): + """Pixel-space latent format for HiDream-O1. + No VAE — model patches/unpatches raw RGB internally with patch_size=32. + """ + pass + class CogVideoX(LatentFormat): """Latent format for CogVideoX-2b (THUDM/CogVideoX-2b). diff --git a/comfy/ldm/hidream_o1/attention.py b/comfy/ldm/hidream_o1/attention.py new file mode 100644 index 000000000..1b68f1771 --- /dev/null +++ b/comfy/ldm/hidream_o1/attention.py @@ -0,0 +1,41 @@ +"""HiDream-O1 two-pass attention: tokens [0, ar_len) are causal, [ar_len, T) +attend full K/V. Splitting Q at the boundary avoids the (B, 1, T, T) additive +mask the general-purpose path would build (~500 MB at T~16K) and lets the +gen half hit the user's preferred backend via optimized_attention. +""" + +import torch + +import comfy.ops +from comfy.ldm.modules.attention import optimized_attention + + +def make_two_pass_attention(ar_len: int, transformer_options=None): + """Build a two-pass attention callable. AR pass uses SDPA-causal directly, gen pass routes through optimized_attention. + The AR pass goes through SDPA directand bypasses wrappers, it is only ~1% of T at typical edit sizes. + """ + + def two_pass_attention(q, k, v, heads, **kwargs): + B, H, T, D = q.shape + + if T < k.shape[2]: # KV-cache hot path: Q is shorter than K/V (cached AR prefix is in K/V only), all fresh Q positions are in the gen region, single full-attention call + out = optimized_attention(q, k, v, heads, mask=None, skip_reshape=True, skip_output_reshape=True, transformer_options=transformer_options) + elif ar_len >= T: + out = comfy.ops.scaled_dot_product_attention(q, k, v, attn_mask=None, dropout_p=0.0, is_causal=True) + elif ar_len <= 0: + out = optimized_attention(q, k, v, heads, mask=None, skip_reshape=True, skip_output_reshape=True, transformer_options=transformer_options) + else: + out_ar = comfy.ops.scaled_dot_product_attention( + q[:, :, :ar_len], k[:, :, :ar_len], v[:, :, :ar_len], + attn_mask=None, dropout_p=0.0, is_causal=True, + ) + out_gen = optimized_attention( + q[:, :, ar_len:], k, v, heads, + mask=None, skip_reshape=True, skip_output_reshape=True, + transformer_options=transformer_options, + ) + out = torch.cat([out_ar, out_gen], dim=2) + + return out.transpose(1, 2).reshape(B, T, H * D) + + return two_pass_attention diff --git a/comfy/ldm/hidream_o1/conditioning.py b/comfy/ldm/hidream_o1/conditioning.py new file mode 100644 index 000000000..7496f0035 --- /dev/null +++ b/comfy/ldm/hidream_o1/conditioning.py @@ -0,0 +1,230 @@ +"""HiDream-O1 conditioning prep — ref-image dual path + extra_conds assembly. + +Each ref image goes through two paths: a 32x32 patchified stream concatenated +to the noised target, and a Qwen3-VL ViT path producing tokens that scatter +into input_ids at <|image_pad|> positions. +""" + +from typing import List + +import torch + +import comfy.utils +from comfy.text_encoders.qwen_vl import process_qwen2vl_images + +from .utils import (PATCH_SIZE, calculate_dimensions, cond_image_size, ref_max_size, resize_tensor) + +# Qwen3-VL ViT preprocessing constants (preprocessor_config.json). +VIT_PATCH = 16 +VIT_MERGE = 2 +VIT_IMAGE_MEAN = [0.5, 0.5, 0.5] +VIT_IMAGE_STD = [0.5, 0.5, 0.5] + + +def prepare_ref_images( + ref_images: List[torch.Tensor], + target_h: int, + target_w: int, + device: torch.device, + dtype: torch.dtype, +): + """Build the dual-path tensors for K reference images at (target_h, target_w). + + Returns None for K=0, else a dict with ref_patches, ref_pixel_values, + ref_image_grid_thw, per_ref_vit_tokens, per_ref_patch_grids. + """ + K = len(ref_images) + if K == 0: + return None + max_size = ref_max_size(max(target_h, target_w), K) + cis = cond_image_size(K) + + refs_t = [img[0].clamp(0, 1).permute(2, 0, 1).unsqueeze(0).contiguous().float() for img in ref_images] + refs_t = [resize_tensor(t, max_size, PATCH_SIZE) for t in refs_t] + + # 32-patch path. + ref_patches_per = [] + per_ref_patch_grids = [] + for t in refs_t: + t_norm = (t.squeeze(0) - 0.5) / 0.5 # (3, H, W) in [-1, 1] + h_p, w_p = t_norm.shape[-2] // PATCH_SIZE, t_norm.shape[-1] // PATCH_SIZE + per_ref_patch_grids.append((h_p, w_p)) + patches = ( + t_norm.reshape(3, h_p, PATCH_SIZE, w_p, PATCH_SIZE) + .permute(1, 3, 0, 2, 4) + .reshape(h_p * w_p, 3 * PATCH_SIZE * PATCH_SIZE) + ) + ref_patches_per.append(patches) + ref_patches = torch.cat(ref_patches_per, dim=0).unsqueeze(0).to(device=device, dtype=dtype) + + # ViT path. + refs_vlm_t = [] + for t in refs_t: + _, _, h, w = t.shape + cond_w, cond_h = calculate_dimensions(cis, w / h) + cond_w = max(cond_w, VIT_PATCH * VIT_MERGE) + cond_h = max(cond_h, VIT_PATCH * VIT_MERGE) + refs_vlm_t.append(comfy.utils.common_upscale(t, cond_w, cond_h, "lanczos", "disabled")) + + pv_list, grid_list, per_ref_vit_tokens = [], [], [] + for t_v in refs_vlm_t: + pv, grid_thw = process_qwen2vl_images( + t_v.permute(0, 2, 3, 1), + min_pixels=0, max_pixels=10**12, + patch_size=VIT_PATCH, merge_size=VIT_MERGE, + image_mean=VIT_IMAGE_MEAN, image_std=VIT_IMAGE_STD, + ) + grid_thw = grid_thw[0] + pv_list.append(pv.to(device=device, dtype=dtype)) + grid_list.append(grid_thw.to(device=device)) + # Post-merge token count = number of <|image_pad|> tokens this image expands to in input_ids. + gh, gw = int(grid_thw[1].item()), int(grid_thw[2].item()) + per_ref_vit_tokens.append((gh // VIT_MERGE) * (gw // VIT_MERGE)) + + return { + "ref_patches": ref_patches, + "ref_pixel_values": torch.cat(pv_list, dim=0), + "ref_image_grid_thw": torch.stack(grid_list, dim=0), + "per_ref_vit_tokens": per_ref_vit_tokens, + "per_ref_patch_grids": per_ref_patch_grids, + } + + +def build_ref_input_ids( + text_input_ids: torch.Tensor, + per_ref_vit_tokens: List[int], + image_token_id: int, + vision_start_id: int, + vision_end_id: int, +): + """Splice [vision_start, image_pad*N, vision_end] blocks into input_ids + after the [im_start, user, \\n] prefix (matches original chat template). + """ + ids = text_input_ids[0].tolist() + inserted = [] + for n_pad in per_ref_vit_tokens: + inserted.extend([vision_start_id] + [image_token_id] * n_pad + [vision_end_id]) + new_ids = ids[:3] + inserted + ids[3:] # 3 = len([im_start, user, \n]) + return torch.tensor([new_ids], dtype=text_input_ids.dtype, device=text_input_ids.device) + + +def build_extra_conds( + text_input_ids: torch.Tensor, + noise: torch.Tensor, + ref_images: List[torch.Tensor] = None, + target_patch_size: int = 32, +): + """Assemble all conditioning tensors for HiDreamO1Transformer.forward: + input_ids (with ref-vision tokens spliced in for the edit/IP path), + position_ids (MRoPE), token_types, vinput_mask, plus the ref + dual-path tensors when refs are provided. + """ + from .utils import get_rope_index_fix_point + from comfy.text_encoders.hidream_o1 import ( + IMAGE_TOKEN_ID, VISION_START_ID, VISION_END_ID, + ) + + if text_input_ids.dim() == 1: + text_input_ids = text_input_ids.unsqueeze(0) + text_input_ids = text_input_ids.long().to(noise.device) + B = noise.shape[0] + if text_input_ids.shape[0] == 1 and B > 1: + text_input_ids = text_input_ids.expand(B, -1) + + H, W = noise.shape[-2], noise.shape[-1] + h_p, w_p = H // target_patch_size, W // target_patch_size + image_len = h_p * w_p + image_grid_thw_tgt = torch.tensor( + [[1, h_p, w_p]], dtype=torch.long, device=text_input_ids.device, + ) + + out = {} + if ref_images: + ref = prepare_ref_images(ref_images, H, W, device=noise.device, dtype=noise.dtype) + text_input_ids = build_ref_input_ids( + text_input_ids, ref["per_ref_vit_tokens"], + IMAGE_TOKEN_ID, VISION_START_ID, VISION_END_ID, + ) + new_txt_len = text_input_ids.shape[1] + + # Each ref's patchified stream gets a [vision_start, image_pad*N-1] + # block in the position-id stream after the noised target. + ref_grid_lengths = [hp * wp for (hp, wp) in ref["per_ref_patch_grids"]] + tgt_vision = torch.full((1, image_len), IMAGE_TOKEN_ID, + dtype=text_input_ids.dtype, device=text_input_ids.device) + tgt_vision[:, 0] = VISION_START_ID + ref_vision_blocks = [] + for rl in ref_grid_lengths: + blk = torch.full((1, rl), IMAGE_TOKEN_ID, + dtype=text_input_ids.dtype, device=text_input_ids.device) + blk[:, 0] = VISION_START_ID + ref_vision_blocks.append(blk) + ref_vision_cat = torch.cat([tgt_vision] + ref_vision_blocks, dim=1) + input_ids_pad = torch.cat([text_input_ids, ref_vision_cat], dim=-1) + total_ref_patches_len = sum(ref_grid_lengths) + total_len = new_txt_len + image_len + total_ref_patches_len + + # K (ViT, post-merge) + 1 (target) + K (ref-patches) image grids. + K = len(ref_images) + igthw_cond = ref["ref_image_grid_thw"].clone() + igthw_cond[:, 1] //= 2 + igthw_cond[:, 2] //= 2 + image_grid_thw_ref = torch.tensor( + [[1, hp, wp] for (hp, wp) in ref["per_ref_patch_grids"]], + dtype=torch.long, device=text_input_ids.device, + ) + igthw_all = torch.cat([ + igthw_cond.to(text_input_ids.device), + image_grid_thw_tgt, + image_grid_thw_ref, + ], dim=0) + position_ids, _ = get_rope_index_fix_point( + spatial_merge_size=1, + image_token_id=IMAGE_TOKEN_ID, + vision_start_token_id=VISION_START_ID, + input_ids=input_ids_pad, image_grid_thw=igthw_all, + attention_mask=None, + skip_vision_start_token=[0] * K + [1] + [1] * K, + fix_point=4096, + ) + + # tms + target_image + ref_patches are all gen. + tms_pos = new_txt_len - 1 + ar_len = tms_pos + token_types = torch.zeros(B, total_len, dtype=torch.long, device=noise.device) + token_types[:, tms_pos:] = 1 + vinput_mask = torch.zeros(B, total_len, dtype=torch.bool, device=noise.device) + vinput_mask[:, new_txt_len:] = True + + # Leading batch dim sidesteps CONDRegular.process_cond's repeat_to_batch_size truncation + out["ref_pixel_values"] = ref["ref_pixel_values"].unsqueeze(0) + out["ref_image_grid_thw"] = ref["ref_image_grid_thw"].unsqueeze(0) + out["ref_patches"] = ref["ref_patches"] + else: + # T2I: text + noised target only, vision_start replaces the first image token + txt_len = text_input_ids.shape[1] + total_len = txt_len + image_len + vision_tokens = torch.full((B, image_len), IMAGE_TOKEN_ID, + dtype=text_input_ids.dtype, device=text_input_ids.device) + vision_tokens[:, 0] = VISION_START_ID + input_ids_pad = torch.cat([text_input_ids, vision_tokens], dim=-1) + position_ids, _ = get_rope_index_fix_point( + spatial_merge_size=1, + image_token_id=IMAGE_TOKEN_ID, + vision_start_token_id=VISION_START_ID, + input_ids=input_ids_pad, image_grid_thw=image_grid_thw_tgt, + attention_mask=None, + skip_vision_start_token=[1], + ) + ar_len = txt_len - 1 + token_types = torch.zeros(B, total_len, dtype=torch.long, device=noise.device) + token_types[:, ar_len:] = 1 + vinput_mask = torch.zeros(B, total_len, dtype=torch.bool, device=noise.device) + vinput_mask[:, txt_len:] = True + + out["input_ids"] = text_input_ids + out["position_ids"] = position_ids[:, 0].unsqueeze(0) # Collapse position_ids batch and add a leading dim so CONDRegular's batch-resize doesn't truncate the 3-axis MRoPE dim + out["token_types"] = token_types + out["vinput_mask"] = vinput_mask + out["ar_len"] = ar_len + return out diff --git a/comfy/ldm/hidream_o1/model.py b/comfy/ldm/hidream_o1/model.py new file mode 100644 index 000000000..a223e706f --- /dev/null +++ b/comfy/ldm/hidream_o1/model.py @@ -0,0 +1,306 @@ +"""HiDream-O1-Image transformer. + +Pixel-space DiT built on Qwen3-VL: the vision tower (Qwen35VisionModel) +encodes ref images, the Qwen3-VL-8B decoder (Llama2_ with interleaved MRoPE) +processes a unified text+image sequence, and 32x32 patch embed/unembed +shims map raw RGB in and out of LLM hidden space. The Qwen3-VL deepstack +mergers go unused — their weights are dropped at load. +""" + +from dataclasses import dataclass, field +from typing import List, Optional + +import einops +import torch +import torch.nn as nn + +import comfy.patcher_extension +from comfy.ldm.modules.diffusionmodules.mmdit import TimestepEmbedder +from comfy.text_encoders.llama import Llama2_ +from comfy.text_encoders.qwen35 import Qwen35VisionModel + +from .attention import make_two_pass_attention + + +IMAGE_TOKEN_ID = 151655 # Qwen3-VL <|image_pad|> +TMS_TOKEN_ID = 151673 # HiDream-O1 <|tms_token|> +PATCH_SIZE = 32 + + +@dataclass +class HiDreamO1TextConfig: + """Qwen3-VL-8B text-decoder dims (matches public Qwen3-VL-8B-Instruct).""" + vocab_size: int = 151936 + hidden_size: int = 4096 + intermediate_size: int = 12288 + num_hidden_layers: int = 36 + num_attention_heads: int = 32 + num_key_value_heads: int = 8 + head_dim: int = 128 + max_position_embeddings: int = 128000 + rms_norm_eps: float = 1e-6 + rope_theta: float = 5000000.0 + rope_scale: Optional[float] = None + rope_dims: List[int] = field(default_factory=lambda: [24, 20, 20]) + interleaved_mrope: bool = True + transformer_type: str = "llama" + rms_norm_add: bool = False + mlp_activation: str = "silu" + qkv_bias: bool = False + q_norm: str = "gemma3" + k_norm: str = "gemma3" + final_norm: bool = True + lm_head: bool = False + stop_tokens: List[int] = field(default_factory=lambda: [151643, 151645]) + + +QWEN3VL_VISION_DEFAULTS = dict( + hidden_size=1152, + num_heads=16, + intermediate_size=4304, + depth=27, + patch_size=16, + temporal_patch_size=2, + in_channels=3, + spatial_merge_size=2, + num_position_embeddings=2304, + deepstack_visual_indexes=(8, 16, 24), + out_hidden_size=4096, # final merger projects directly into LLM hidden +) + + +class BottleneckPatchEmbed(nn.Module): + # 3072 -> 1024 -> 4096 (raw 32x32 RGB patch -> bottleneck -> LLM hidden). + def __init__(self, patch_size=32, in_chans=3, pca_dim=1024, embed_dim=4096, bias=True, device=None, dtype=None, ops=None): + super().__init__() + self.proj1 = ops.Linear(patch_size * patch_size * in_chans, pca_dim, bias=False, device=device, dtype=dtype) + self.proj2 = ops.Linear(pca_dim, embed_dim, bias=bias, device=device, dtype=dtype) + + def forward(self, x): + return self.proj2(self.proj1(x)) + + +class FinalLayer(nn.Module): + # 4096 -> 3072 (LLM hidden -> flat pixel patch). + def __init__(self, hidden_size, patch_size=32, out_channels=3, device=None, dtype=None, ops=None): + super().__init__() + self.linear = ops.Linear(hidden_size, patch_size * patch_size * out_channels, bias=True, device=device, dtype=dtype) + + def forward(self, x): + return self.linear(x) + + +class HiDreamO1Transformer(nn.Module): + """HiDream-O1 unified pixel-level transformer.""" + + def __init__(self, image_model=None, dtype=None, device=None, operations=None, + text_config_overrides=None, vision_config_overrides=None, **kwargs): + super().__init__() + self.dtype = dtype + + text_cfg = HiDreamO1TextConfig(**(text_config_overrides or {})) + vision_cfg = dict(QWEN3VL_VISION_DEFAULTS) + if vision_config_overrides: + vision_cfg.update(vision_config_overrides) + vision_cfg["out_hidden_size"] = text_cfg.hidden_size + + self.text_config = text_cfg + self.vision_config = vision_cfg + self.hidden_size = text_cfg.hidden_size + self.patch_size = PATCH_SIZE + self.in_channels = 3 + self.tms_token_id = TMS_TOKEN_ID + + self.visual = Qwen35VisionModel(vision_cfg, device=device, dtype=dtype, ops=operations) + self.language_model = Llama2_(text_cfg, device=device, dtype=dtype, ops=operations) + self.t_embedder1 = TimestepEmbedder( + text_cfg.hidden_size, device=device, dtype=dtype, operations=operations, + ) + self.x_embedder = BottleneckPatchEmbed( + patch_size=self.patch_size, in_chans=self.in_channels, + pca_dim=text_cfg.hidden_size // 4, embed_dim=text_cfg.hidden_size, + bias=True, device=device, dtype=dtype, ops=operations, + ) + self.final_layer2 = FinalLayer( + text_cfg.hidden_size, patch_size=self.patch_size, + out_channels=self.in_channels, device=device, dtype=dtype, ops=operations, + ) + + self._visual_cache = None + self._kv_cache_entries = [] + + def clear_kv_cache(self): + self._kv_cache_entries = [] + self._visual_cache = None + + def forward(self, x, timesteps, context=None, transformer_options={}, **kwargs): + return comfy.patcher_extension.WrapperExecutor.new_class_executor( + self._forward, + self, + comfy.patcher_extension.get_all_wrappers(comfy.patcher_extension.WrappersMP.DIFFUSION_MODEL, transformer_options) + ).execute(x, timesteps, context, transformer_options, **kwargs) + + def _forward(self, x, timesteps, context=None, transformer_options={}, input_ids=None, attention_mask=None, position_ids=None, + vinput_mask=None, ar_len=None, ref_pixel_values=None, ref_image_grid_thw=None, ref_patches=None, **kwargs): + """Returns flow-match velocity (x - x_pred) / sigma""" + + if input_ids is None or position_ids is None: + raise ValueError("HiDreamO1Transformer requires input_ids and position_ids in conditioning") + + B, _, H, W = x.shape + h_p, w_p = H // self.patch_size, W // self.patch_size + tgt_image_len = h_p * w_p + + z = einops.rearrange( + x, 'B C (H p1) (W p2) -> B (H W) (C p1 p2)', + p1=self.patch_size, p2=self.patch_size, + ) + vinputs = torch.cat([z, ref_patches.to(z.dtype)], dim=1) if ref_patches is not None else z + + inputs_embeds = self.language_model.embed_tokens(input_ids).to(x.dtype) + + if ref_pixel_values is not None and ref_image_grid_thw is not None: + # ViT output is constant across sampling steps within a generation + # identity-key by the input tensor so refs don't recompute every step. + cached = self._visual_cache + if cached is not None and cached[0] is ref_pixel_values: + image_embeds = cached[1] + else: + ref_pv = ref_pixel_values.to(inputs_embeds.device) + ref_grid = ref_image_grid_thw.to(inputs_embeds.device).long() + # extra_conds wraps with a leading batch dim; refs are model-level so [0] always recovers them. + if ref_pv.dim() == 3: + ref_pv = ref_pv[0] + if ref_grid.dim() == 3: + ref_grid = ref_grid[0] + image_embeds = self.visual(ref_pv, ref_grid).to(inputs_embeds.dtype) + self._visual_cache = (ref_pixel_values, image_embeds) + # image_pad positions identical across batch (input_ids shared cond/uncond). + image_idx = (input_ids[0] == IMAGE_TOKEN_ID).nonzero(as_tuple=True)[0] + if image_idx.shape[0] != image_embeds.shape[0]: + raise ValueError( + f"Image-token count {image_idx.shape[0]} != ViT output count " + f"{image_embeds.shape[0]}; check tokenizer/processor alignment." + ) + inputs_embeds[:, image_idx] = image_embeds.unsqueeze(0).expand(B, -1, -1) + + sigma = timesteps.float() / 1000.0 + t_pixeldit = 1.0 - sigma + t_emb = self.t_embedder1(t_pixeldit * 1000, inputs_embeds.dtype) + tms_mask_3d = (input_ids == self.tms_token_id).unsqueeze(-1).expand_as(inputs_embeds) + inputs_embeds = torch.where(tms_mask_3d, t_emb.unsqueeze(1).expand_as(inputs_embeds), inputs_embeds) + + vinputs_embedded = self.x_embedder(vinputs.to(inputs_embeds.dtype)) + inputs_embeds = torch.cat([inputs_embeds, vinputs_embedded], dim=1) + + # extra_conds stores position_ids as (1, 3, T); process_cond repeats dim 0 to B. Take row 0. + freqs_cis = self.language_model.compute_freqs_cis(position_ids[0].to(x.device), x.device) + freqs_cis = tuple(t.to(x.dtype) for t in freqs_cis) + + two_pass_attn = make_two_pass_attention(ar_len, transformer_options=transformer_options) + patches_replace = transformer_options.get("patches_replace", {}) + blocks_replace = patches_replace.get("dit", {}) + transformer_options["total_blocks"] = len(self.language_model.layers) + transformer_options["block_type"] = "double" + + # Cache prefix K/V across steps. Key includes input_ids (prompt), ref_id + # (refs scatter into inputs_embeds), and position_ids (RoPE baked into cached K). + can_cache = not blocks_replace and ar_len > 0 + cache_len = ar_len if can_cache else 0 + ref_id = id(ref_pixel_values) if ref_pixel_values is not None else None + pos_ids_key = position_ids[..., :cache_len] if can_cache else position_ids + cache_entries = self._kv_cache_entries + # Drop stale entries from a previous device (model was unloaded and reloaded). + if cache_entries and cache_entries[0]["input_ids"].device != input_ids.device: + cache_entries = [] + self._kv_cache_entries = [] + kv_cache = None + if can_cache: + for entry in cache_entries: + ck = entry["input_ids"] + ep = entry["position_ids"] + if (entry["cache_len"] == cache_len + and ck.shape == input_ids.shape and torch.equal(ck, input_ids) + and entry["ref_id"] == ref_id + and ep.shape == pos_ids_key.shape and torch.equal(ep, pos_ids_key)): + kv_cache = entry + break + + if kv_cache is not None: + # Hot path: project Q/K/V only for fresh positions; past_key_value prepends cached AR K/V. + hidden_states = inputs_embeds[:, cache_len:] + sliced_freqs = tuple(t[..., cache_len:, :] for t in freqs_cis) + for i, layer in enumerate(self.language_model.layers): + transformer_options["block_index"] = i + K_i, V_i = kv_cache["kv"][i] + hidden_states, _ = layer( + x=hidden_states, attention_mask=None, freqs_cis=sliced_freqs, optimized_attention=two_pass_attn, + past_key_value=(K_i, V_i, cache_len), + ) + else: + # Cold path: run full sequence; if cacheable, snapshot K/V at AR positions. + snapshots = [] if can_cache else None + past_kv_cold = () if can_cache else None + hidden_states = inputs_embeds + for i, layer in enumerate(self.language_model.layers): + transformer_options["block_index"] = i + if ("double_block", i) in blocks_replace: + def block_wrap(args, _layer=layer): + out = {} + out["x"], _ = _layer( + x=args["x"], attention_mask=args.get("attention_mask"), + freqs_cis=args["freqs_cis"], optimized_attention=args["optimized_attention"], + past_key_value=None, + ) + return out + out = blocks_replace[("double_block", i)]( + {"x": hidden_states, "attention_mask": None, + "freqs_cis": freqs_cis, "optimized_attention": two_pass_attn, + "transformer_options": transformer_options}, + {"original_block": block_wrap}, + ) + hidden_states = out["x"] + else: + hidden_states, present_kv = layer( + x=hidden_states, attention_mask=None, + freqs_cis=freqs_cis, optimized_attention=two_pass_attn, + past_key_value=past_kv_cold, + ) + if snapshots is not None: + K, V, _ = present_kv + snapshots.append((K[:, :, :cache_len].contiguous(), + V[:, :, :cache_len].contiguous())) + if snapshots is not None: + # Cap at 2 entries (cond + uncond). Multi-cond workflows LRU-evict. + new_entry = { + "input_ids": input_ids.clone(), + "cache_len": cache_len, + "kv": snapshots, + "ref_id": ref_id, + "position_ids": pos_ids_key.clone(), + } + self._kv_cache_entries = (cache_entries + [new_entry])[-2:] + + if self.language_model.norm is not None: + hidden_states = self.language_model.norm(hidden_states) + + # Slice target-image positions before the final projection so the Linear only runs on tgt_image_len tokens. + # In the hot path hidden_states starts at original position cache_len, so masks/indices shift by cache_len. + sliced_offset = cache_len if kv_cache is not None else 0 + if vinput_mask is not None: + vmask = vinput_mask.to(x.device).bool() + if sliced_offset > 0: + vmask = vmask[:, sliced_offset:] + target_hidden = hidden_states[vmask].view(B, -1, hidden_states.shape[-1])[:, :tgt_image_len] + else: + txt_seq_len = input_ids.shape[1] + start = txt_seq_len - sliced_offset + target_hidden = hidden_states[:, start:start + tgt_image_len] + x_pred_tgt = self.final_layer2(target_hidden) + + # fp32 final subtraction, bf16 here noticeably degrades samples. + x_pred_img = einops.rearrange( + x_pred_tgt, 'B (H W) (C p1 p2) -> B C (H p1) (W p2)', + H=h_p, W=w_p, p1=self.patch_size, p2=self.patch_size, + ) + return (x.float() - x_pred_img.float()) / sigma.view(B, 1, 1, 1).clamp_min(1e-3) diff --git a/comfy/ldm/hidream_o1/utils.py b/comfy/ldm/hidream_o1/utils.py new file mode 100644 index 000000000..5a1249c72 --- /dev/null +++ b/comfy/ldm/hidream_o1/utils.py @@ -0,0 +1,173 @@ +"""HiDream-O1 input-prep helpers: image/resolution math and unified-sequence +RoPE position-id assembly. The fix_point offset in get_rope_index_fix_point +lets the target image and patchified ref images share spatial RoPE positions +despite living at different sequence indices — same 2D image plane. +""" + +import math +from typing import Optional + +import torch + + +PATCH_SIZE = 32 +CONDITION_IMAGE_SIZE = 384 # ViT-side base size for ref images + + +def resize_tensor(img_t, image_size, patch_size=16): + """img_t: (1, 3, H, W) float [0, 1]. Fit to image_size**2 area, patch-aligned, center-cropped.""" + + while min(img_t.shape[-2], img_t.shape[-1]) >= 2 * image_size: # Pre-halves with 2x2 box averaging while the image is still very large + img_t = torch.nn.functional.avg_pool2d(img_t, kernel_size=2, stride=2) + + _, _, height, width = img_t.shape + m = patch_size + s_max = image_size * image_size + scale = math.sqrt(s_max / (width * height)) + + candidates = [ + (round(width * scale) // m * m, round(height * scale) // m * m), + (round(width * scale) // m * m, math.floor(height * scale) // m * m), + (math.floor(width * scale) // m * m, round(height * scale) // m * m), + (math.floor(width * scale) // m * m, math.floor(height * scale) // m * m), + ] + candidates = sorted(candidates, key=lambda x: x[0] * x[1], reverse=True) + new_size = candidates[-1] + for c in candidates: + if c[0] * c[1] <= s_max: + new_size = c + break + + new_w, new_h = new_size + s1 = width / new_w + s2 = height / new_h + if s1 < s2: + resize_w, resize_h = new_w, round(height / s1) + else: + resize_w, resize_h = round(width / s2), new_h + img_t = torch.nn.functional.interpolate(img_t, size=(resize_h, resize_w), mode="bicubic") + top = (resize_h - new_h) // 2 + left = (resize_w - new_w) // 2 + return img_t[..., top:top + new_h, left:left + new_w] + + +def calculate_dimensions(max_size, ratio): + """(W, H) for an aspect ratio fitting in max_size**2 area, 32-aligned.""" + width = math.sqrt(max_size * max_size * ratio) + height = width / ratio + width = int(width / 32) * 32 + height = int(height / 32) * 32 + return width, height + + +def ref_max_size(target_max_dim, k): + """K-dependent ref-image max dim before patchifying.""" + if k == 1: + return target_max_dim + if k == 2: + return target_max_dim * 48 // 64 + if k <= 4: + return target_max_dim // 2 + if k <= 8: + return target_max_dim * 24 // 64 + return target_max_dim // 4 + + +def cond_image_size(k): + """K-dependent ViT-side image size.""" + if k <= 4: + return CONDITION_IMAGE_SIZE + if k <= 8: + return CONDITION_IMAGE_SIZE * 48 // 64 + return CONDITION_IMAGE_SIZE // 2 + + +def get_rope_index_fix_point( + spatial_merge_size: int, + image_token_id: int, + vision_start_token_id: int, + input_ids: Optional[torch.LongTensor] = None, + image_grid_thw: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + skip_vision_start_token=None, + fix_point: int = 4096, +): + mrope_position_deltas = [] + if input_ids is not None and image_grid_thw is not None: + total_input_ids = input_ids + if attention_mask is None: + attention_mask = torch.ones_like(total_input_ids) + position_ids = torch.ones( + 3, input_ids.shape[0], input_ids.shape[1], + dtype=input_ids.dtype, device=input_ids.device, + ) + attention_mask = attention_mask.to(total_input_ids.device) + for i, input_ids_b in enumerate(total_input_ids): + fp = fix_point + image_index = 0 + input_ids_b = input_ids_b[attention_mask[i] == 1] + vision_start_indices = torch.argwhere(input_ids_b == vision_start_token_id).squeeze(1) + vision_tokens = input_ids_b[vision_start_indices + 1] + image_nums = (vision_tokens == image_token_id).sum() + input_tokens = input_ids_b.tolist() + llm_pos_ids_list = [] + st = 0 + remain_images = image_nums + for _ in range(image_nums): + if image_token_id in input_tokens and remain_images > 0: + ed = input_tokens.index(image_token_id, st) + else: + ed = len(input_tokens) + 1 + t = image_grid_thw[image_index][0] + h = image_grid_thw[image_index][1] + w = image_grid_thw[image_index][2] + image_index += 1 + remain_images -= 1 + llm_grid_t = t.item() + llm_grid_h = h.item() // spatial_merge_size + llm_grid_w = w.item() // spatial_merge_size + text_len = ed - st + text_len -= skip_vision_start_token[image_index - 1] + text_len = max(0, text_len) + st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0 + llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx) + + t_index = torch.arange(llm_grid_t).view(-1, 1).expand(-1, llm_grid_h * llm_grid_w).flatten() + h_index = torch.arange(llm_grid_h).view(1, -1, 1).expand(llm_grid_t, -1, llm_grid_w).flatten() + w_index = torch.arange(llm_grid_w).view(1, 1, -1).expand(llm_grid_t, llm_grid_h, -1).flatten() + + if skip_vision_start_token[image_index - 1]: + if fp > 0: + fp = fp - st_idx + llm_pos_ids_list.append(torch.stack([t_index, h_index, w_index]) + fp + st_idx) + fp = 0 + else: + llm_pos_ids_list.append(torch.stack([t_index, h_index, w_index]) + text_len + st_idx) + st = ed + llm_grid_t * llm_grid_h * llm_grid_w + + if st < len(input_tokens): + st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0 + text_len = len(input_tokens) - st + llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx) + + llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1) + position_ids[..., i, attention_mask[i] == 1] = llm_positions.to(position_ids.device) + mrope_position_deltas.append(llm_positions.max() + 1 - len(total_input_ids[i])) + mrope_position_deltas = torch.tensor(mrope_position_deltas, device=input_ids.device).unsqueeze(1) + return position_ids, mrope_position_deltas + + if attention_mask is not None: + position_ids = attention_mask.long().cumsum(-1) - 1 + position_ids.masked_fill_(attention_mask == 0, 1) + position_ids = position_ids.unsqueeze(0).expand(3, -1, -1).to(attention_mask.device) + max_position_ids = position_ids.max(0, keepdim=False)[0].max(-1, keepdim=True)[0] + mrope_position_deltas = max_position_ids + 1 - attention_mask.shape[-1] + else: + position_ids = ( + torch.arange(input_ids.shape[1], device=input_ids.device) + .view(1, 1, -1).expand(3, input_ids.shape[0], -1) + ) + mrope_position_deltas = torch.zeros( + [input_ids.shape[0], 1], device=input_ids.device, dtype=input_ids.dtype, + ) + return position_ids, mrope_position_deltas diff --git a/comfy/model_base.py b/comfy/model_base.py index dbed239e5..0736321b3 100644 --- a/comfy/model_base.py +++ b/comfy/model_base.py @@ -58,6 +58,8 @@ import comfy.ldm.cogvideo.model import comfy.ldm.rt_detr.rtdetr_v4 import comfy.ldm.ernie.model import comfy.ldm.sam3.detector +import comfy.ldm.hidream_o1.model +from comfy.ldm.hidream_o1.conditioning import build_extra_conds import comfy.model_management import comfy.patcher_extension @@ -1674,6 +1676,32 @@ class HiDream(BaseModel): out['image_cond'] = comfy.conds.CONDNoiseShape(self.process_latent_in(image_cond)) return out +class HiDreamO1(BaseModel): + """HiDream-O1-Image: pixel-space DiT (no VAE). Refs from HiDreamO1ReferenceImages and tokens from the stub TE flow through + extra_conds; the heavy preprocessing lives in comfy.ldm.hidream_o1.conditioning.""" + PATCH_SIZE = 32 + + def __init__(self, model_config, model_type=ModelType.FLOW, device=None): + super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.hidream_o1.model.HiDreamO1Transformer) + + def extra_conds(self, **kwargs): + out = super().extra_conds(**kwargs) + text_input_ids = kwargs.get("text_input_ids", None) + noise = kwargs.get("noise", None) + if text_input_ids is None or noise is None: + return out + + conds = build_extra_conds( + text_input_ids, noise, + ref_images=kwargs.get("reference_latents", None), + target_patch_size=self.PATCH_SIZE, + ) + for k, v in conds.items(): + # ar_len is a Python int (precomputed to avoid a GPU sync in forward). + cls = comfy.conds.CONDConstant if k == "ar_len" else comfy.conds.CONDRegular + out[k] = cls(v) + return out + class Chroma(Flux): def __init__(self, model_config, model_type=ModelType.FLUX, device=None, unet_model=comfy.ldm.chroma.model.Chroma): super().__init__(model_config, model_type, device=device, unet_model=unet_model) diff --git a/comfy/model_detection.py b/comfy/model_detection.py index 8ae456481..bc0b933bc 100644 --- a/comfy/model_detection.py +++ b/comfy/model_detection.py @@ -620,6 +620,9 @@ def detect_unet_config(state_dict, key_prefix, metadata=None): dit_config["guidance_cond_proj_dim"] = None#f"{key_prefix}t_embedder.cond_proj.weight" in state_dict_keys return dit_config + if '{}t_embedder1.mlp.0.weight'.format(key_prefix) in state_dict_keys and '{}x_embedder.proj1.weight'.format(key_prefix) in state_dict_keys: # HiDream-O1 + return {"image_model": "hidream_o1"} + if '{}caption_projection.0.linear.weight'.format(key_prefix) in state_dict_keys: # HiDream dit_config = {} dit_config["image_model"] = "hidream" diff --git a/comfy/model_sampling.py b/comfy/model_sampling.py index cf2b5db5f..5af336e76 100644 --- a/comfy/model_sampling.py +++ b/comfy/model_sampling.py @@ -93,7 +93,8 @@ class CONST: def noise_scaling(self, sigma, noise, latent_image, max_denoise=False): sigma = reshape_sigma(sigma, noise.ndim) - return sigma * noise + (1.0 - sigma) * latent_image + s = getattr(self, "noise_scale", 1.0) + return sigma * (s * noise) + (1.0 - sigma) * latent_image def inverse_noise_scaling(self, sigma, latent): sigma = reshape_sigma(sigma, latent.ndim) @@ -288,7 +289,11 @@ class ModelSamplingDiscreteFlow(torch.nn.Module): else: sampling_settings = {} - self.set_parameters(shift=sampling_settings.get("shift", 1.0), multiplier=sampling_settings.get("multiplier", 1000)) + self.set_noise_scale(sampling_settings.get("noise_scale", 1.0)) + self.set_parameters( + shift=sampling_settings.get("shift", 1.0), + multiplier=sampling_settings.get("multiplier", 1000), + ) def set_parameters(self, shift=1.0, timesteps=1000, multiplier=1000): self.shift = shift @@ -296,6 +301,9 @@ class ModelSamplingDiscreteFlow(torch.nn.Module): ts = self.sigma((torch.arange(1, timesteps + 1, 1) / timesteps) * multiplier) self.register_buffer('sigmas', ts) + def set_noise_scale(self, noise_scale): + self.noise_scale = float(noise_scale) + @property def sigma_min(self): return self.sigmas[0] diff --git a/comfy/ops.py b/comfy/ops.py index 77ad1d527..117cdd327 100644 --- a/comfy/ops.py +++ b/comfy/ops.py @@ -1285,7 +1285,8 @@ def mixed_precision_ops(quant_config={}, compute_dtype=torch.bfloat16, full_prec if quant_format in ["float8_e4m3fn", "float8_e5m2"] and weight_key in state_dict: self.quant_format = quant_format qconfig = QUANT_ALGOS[quant_format] - layout_cls = get_layout_class(qconfig["comfy_tensor_layout"]) + self.layout_type = qconfig["comfy_tensor_layout"] + layout_cls = get_layout_class(self.layout_type) weight = state_dict.pop(weight_key) manually_loaded_keys = [weight_key] diff --git a/comfy/sd.py b/comfy/sd.py index 749bdd710..ab2718892 100644 --- a/comfy/sd.py +++ b/comfy/sd.py @@ -239,7 +239,8 @@ class CLIP: model_management.archive_model_dtypes(self.cond_stage_model) self.tokenizer = tokenizer(embedding_directory=embedding_directory, tokenizer_data=tokenizer_data) - ModelPatcher = comfy.model_patcher.ModelPatcher if disable_dynamic else comfy.model_patcher.CoreModelPatcher + te_disable_dynamic = disable_dynamic or getattr(self.cond_stage_model, "disable_offload", False) + ModelPatcher = comfy.model_patcher.ModelPatcher if te_disable_dynamic else comfy.model_patcher.CoreModelPatcher self.patcher = ModelPatcher(self.cond_stage_model, load_device=load_device, offload_device=offload_device) #Match torch.float32 hardcode upcast in TE implemention self.patcher.set_model_compute_dtype(torch.float32) @@ -776,6 +777,7 @@ class VAE: self.latent_channels = 3 self.latent_dim = 2 self.output_channels = 3 + self.disable_offload = True elif "vocoder.activation_post.downsample.lowpass.filter" in sd: #MMAudio VAE sample_rate = 16000 if sample_rate == 16000: diff --git a/comfy/supported_models.py b/comfy/supported_models.py index 40417f922..8d2e02f68 100644 --- a/comfy/supported_models.py +++ b/comfy/supported_models.py @@ -28,6 +28,7 @@ import comfy.text_encoders.ace15 import comfy.text_encoders.longcat_image import comfy.text_encoders.ernie import comfy.text_encoders.cogvideo +import comfy.text_encoders.hidream_o1 from . import supported_models_base from . import latent_formats @@ -1431,6 +1432,50 @@ class HiDream(supported_models_base.BASE): def clip_target(self, state_dict={}): return None # TODO +class HiDreamO1(supported_models_base.BASE): + unet_config = { + "image_model": "hidream_o1", + } + + sampling_settings = { + "shift": 3.0, + "noise_scale": 8.0, + } + + latent_format = latent_formats.HiDreamO1Pixel + memory_usage_factor = 0.6 + # fp16 not supported: LM MLP down_proj activations fp16 overflow, causing NaNs + supported_inference_dtypes = [torch.bfloat16, torch.float32] + + vae_key_prefix = ["vae."] + text_encoder_key_prefix = ["text_encoders."] + + optimizations = {"fp8": False} + + def get_model(self, state_dict, prefix="", device=None): + return model_base.HiDreamO1(self, device=device) + + def process_unet_state_dict(self, state_dict): + # Drop unused Qwen3-VL deepstack merger weights; upstream discards them at inference. + for key in list(state_dict.keys()): + if "visual.deepstack_merger_list" in key: + del state_dict[key] + return state_dict + + def process_vae_state_dict(self, state_dict): + # Pixel-space model: inject sentinel so VAE construction picks PixelspaceConversionVAE. + return {"pixel_space_vae": torch.tensor(1.0)} + + def process_clip_state_dict(self, state_dict): + # Tokenizer-only TE: inject sentinel so load_state_dict_guess_config triggers CLIP init. + return {"_hidream_o1_te_sentinel": torch.zeros(1)} + + def clip_target(self, state_dict={}): + return supported_models_base.ClipTarget( + comfy.text_encoders.hidream_o1.HiDreamO1Tokenizer, + comfy.text_encoders.hidream_o1.HiDreamO1TE, + ) + class Chroma(supported_models_base.BASE): unet_config = { "image_model": "chroma", @@ -2018,6 +2063,7 @@ models = [ Hunyuan3Dv2, Hunyuan3Dv2_1, HiDream, + HiDreamO1, Chroma, ChromaRadiance, ACEStep, diff --git a/comfy/text_encoders/hidream_o1.py b/comfy/text_encoders/hidream_o1.py new file mode 100644 index 000000000..5d287b784 --- /dev/null +++ b/comfy/text_encoders/hidream_o1.py @@ -0,0 +1,119 @@ +"""HiDream-O1-Image tokenizer-only text encoder. + +The real Qwen3-VL backbone runs inside diffusion_model.* every step, so this +module just tokenizes the prompt into text_input_ids and emits them as +conditioning. Position ids / token_types / vinput_mask depend on target H/W +and are built later in model_base.HiDreamO1.extra_conds. +""" + +import os + +import torch +from transformers import Qwen2Tokenizer + +from comfy import sd1_clip + + +# Qwen3-VL special tokens +IM_START_ID = 151644 +IM_END_ID = 151645 +ASSISTANT_ID = 77091 +USER_ID = 872 +NEWLINE_ID = 198 +VISION_START_ID = 151652 +VISION_END_ID = 151653 +IMAGE_TOKEN_ID = 151655 +VIDEO_TOKEN_ID = 151656 +# HiDream-O1-specific tokens +BOI_TOKEN_ID = 151669 +BOR_TOKEN_ID = 151670 +EOR_TOKEN_ID = 151671 +BOT_TOKEN_ID = 151672 +TMS_TOKEN_ID = 151673 + + +class HiDreamO1QwenTokenizer(sd1_clip.SDTokenizer): + def __init__(self, embedding_directory=None, tokenizer_data={}): + tokenizer_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "qwen25_tokenizer" + ) + super().__init__( + tokenizer_path, + pad_with_end=False, + embedding_size=4096, + embedding_key="hidream_o1", + tokenizer_class=Qwen2Tokenizer, + has_start_token=False, + has_end_token=False, + pad_to_max_length=False, + max_length=99999999, + min_length=1, + pad_token=151643, + tokenizer_data=tokenizer_data, + ) + + +class HiDreamO1Tokenizer(sd1_clip.SD1Tokenizer): + """Wraps prompt in the upstream chat template ending with boi/tms markers. + Image tokens get spliced in at sample time once target H/W is known. + """ + + def __init__(self, embedding_directory=None, tokenizer_data={}): + super().__init__( + embedding_directory=embedding_directory, + tokenizer_data=tokenizer_data, + name="hidream_o1", + tokenizer=HiDreamO1QwenTokenizer, + ) + + def tokenize_with_weights(self, text, return_word_ids=False, **kwargs): + text_tokens_dict = super().tokenize_with_weights( + text, return_word_ids=return_word_ids, disable_weights=True, **kwargs + ) + text_tuples = text_tokens_dict["hidream_o1"][0] + text_tuples = [t for t in text_tuples if int(t[0]) != 151643] # strip pad + + # <|im_start|>user\n{text}<|im_end|>\n<|im_start|>assistant\n<|boi|><|tms|> + def tok(tid): + return (tid, 1.0) if not return_word_ids else (tid, 1.0, 0) + + prefix = [tok(IM_START_ID), tok(USER_ID), tok(NEWLINE_ID)] + suffix = [ + tok(IM_END_ID), tok(NEWLINE_ID), + tok(IM_START_ID), tok(ASSISTANT_ID), tok(NEWLINE_ID), + tok(BOI_TOKEN_ID), tok(TMS_TOKEN_ID), + ] + full = prefix + list(text_tuples) + suffix + return {"hidream_o1": [full]} + + +class HiDreamO1TE(torch.nn.Module): + """Passthrough TE: emits int token ids; the Qwen3-VL backbone in diffusion_model does the actual encoding.""" + + def __init__(self, device="cpu", dtype=None, model_options={}): + super().__init__() + self.dtypes = {torch.float32} + self.disable_offload = True # skips dynamic VRAM management for this zero-parameter module + self.device = torch.device("cpu") if device is None else torch.device(device) + + def encode_token_weights(self, token_weight_pairs): + tok_pairs = token_weight_pairs["hidream_o1"][0] + ids = [int(t[0]) for t in tok_pairs] + input_ids = torch.tensor([ids], dtype=torch.long) + # Surrogate keeps the cross_attn slot non-empty for CONDITIONING + # plumbing; the model reads text_input_ids out of `extra` instead. + cross_attn = input_ids.unsqueeze(-1).to(torch.float32) + extra = {"text_input_ids": input_ids} + return cross_attn, None, extra + + def load_sd(self, sd): + return [] + + def get_sd(self): + return {} + + def reset_clip_options(self): + pass + + def set_clip_options(self, options): + pass diff --git a/comfy/text_encoders/llama.py b/comfy/text_encoders/llama.py index a34c41144..5087228ca 100644 --- a/comfy/text_encoders/llama.py +++ b/comfy/text_encoders/llama.py @@ -397,7 +397,7 @@ class RMSNorm(nn.Module): -def precompute_freqs_cis(head_dim, position_ids, theta, rope_scale=None, rope_dims=None, device=None): +def precompute_freqs_cis(head_dim, position_ids, theta, rope_scale=None, rope_dims=None, device=None, interleaved_mrope=False): if not isinstance(theta, list): theta = [theta] @@ -415,16 +415,27 @@ def precompute_freqs_cis(head_dim, position_ids, theta, rope_scale=None, rope_di inv_freq_expanded = inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1) position_ids_expanded = position_ids[:, None, :].float() freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2) - emb = torch.cat((freqs, freqs), dim=-1) - cos = emb.cos() - sin = emb.sin() - if rope_dims is not None and position_ids.shape[0] > 1: - mrope_section = rope_dims * 2 - cos = torch.cat([m[i % 3] for i, m in enumerate(cos.split(mrope_section, dim=-1))], dim=-1).unsqueeze(0) - sin = torch.cat([m[i % 3] for i, m in enumerate(sin.split(mrope_section, dim=-1))], dim=-1).unsqueeze(0) + if rope_dims is not None and position_ids.shape[0] > 1 and interleaved_mrope: + # Qwen3-VL interleaved MRoPE: T-freqs by default, H/W replace every 3rd dim. + freqs_inter = freqs[0].clone() + for axis_idx, offset in ((1, 1), (2, 2)): + length = rope_dims[axis_idx] * 3 + idx = slice(offset, length, 3) + freqs_inter[..., idx] = freqs[axis_idx, ..., idx] + emb = torch.cat((freqs_inter, freqs_inter), dim=-1) + cos = emb.cos().unsqueeze(0) + sin = emb.sin().unsqueeze(0) else: - cos = cos.unsqueeze(1) - sin = sin.unsqueeze(1) + emb = torch.cat((freqs, freqs), dim=-1) + cos = emb.cos() + sin = emb.sin() + if rope_dims is not None and position_ids.shape[0] > 1: + mrope_section = rope_dims * 2 + cos = torch.cat([m[i % 3] for i, m in enumerate(cos.split(mrope_section, dim=-1))], dim=-1).unsqueeze(0) + sin = torch.cat([m[i % 3] for i, m in enumerate(sin.split(mrope_section, dim=-1))], dim=-1).unsqueeze(0) + else: + cos = cos.unsqueeze(1) + sin = sin.unsqueeze(1) sin_split = sin.shape[-1] // 2 out.append((cos, sin[..., : sin_split], -sin[..., sin_split :])) @@ -689,6 +700,7 @@ class Llama2_(nn.Module): self.config.rope_theta, self.config.rope_scale, self.config.rope_dims, + interleaved_mrope=getattr(self.config, "interleaved_mrope", False), device=device) def forward(self, x, attention_mask=None, embeds=None, num_tokens=None, intermediate_output=None, final_layer_norm_intermediate=True, dtype=None, position_ids=None, embeds_info=[], past_key_values=None, input_ids=None): diff --git a/comfy/text_encoders/qwen35.py b/comfy/text_encoders/qwen35.py index d8ed9cd32..d0bbec3e6 100644 --- a/comfy/text_encoders/qwen35.py +++ b/comfy/text_encoders/qwen35.py @@ -651,7 +651,7 @@ class Qwen35VisionModel(nn.Module): x = self.patch_embed(x) pos_embeds = self.fast_pos_embed_interpolate(grid_thw).to(x.device) x = x + pos_embeds - rotary_pos_emb = self.rot_pos_emb(grid_thw) + rotary_pos_emb = self.rot_pos_emb(grid_thw).to(x.device) seq_len = x.shape[0] x = x.reshape(seq_len, -1) rotary_pos_emb = rotary_pos_emb.reshape(seq_len, -1) diff --git a/comfy_extras/nodes_advanced_samplers.py b/comfy_extras/nodes_advanced_samplers.py index 7e8411fa4..567c37be0 100644 --- a/comfy_extras/nodes_advanced_samplers.py +++ b/comfy_extras/nodes_advanced_samplers.py @@ -86,6 +86,37 @@ def sample_euler_pp(model, x, sigmas, extra_args=None, callback=None, disable=No return x +class SamplerLCM(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="SamplerLCM", + category="sampling/samplers", + description=("LCM sampler with tunable per-step noise. s_noise is a multiplier on the model's training noise scale"), + inputs=[ + io.Float.Input("s_noise", default=1.0, min=0.0, max=64.0, step=0.01, + tooltip="Per-step noise multiplier at the first step (1.0 = match training)."), + io.Float.Input("s_noise_end", default=1.0, min=0.0, max=64.0, step=0.01, + tooltip="Per-step noise multiplier at the last step. Set equal to s_noise for a constant schedule."), + io.Float.Input("noise_clip_std", default=0.0, min=0.0, max=10.0, step=0.01, + tooltip="Clamp per-step noise to +/- N*std. 0 disables."), + ], + outputs=[io.Sampler.Output()], + ) + + @classmethod + def execute(cls, s_noise, s_noise_end, noise_clip_std) -> io.NodeOutput: + sampler = comfy.samplers.ksampler( + "lcm", + { + "s_noise": float(s_noise), + "s_noise_end": float(s_noise_end), + "noise_clip_std": float(noise_clip_std), + }, + ) + return io.NodeOutput(sampler) + + class SamplerEulerCFGpp(io.ComfyNode): @classmethod def define_schema(cls) -> io.Schema: @@ -114,6 +145,7 @@ class AdvancedSamplersExtension(ComfyExtension): async def get_node_list(self) -> list[type[io.ComfyNode]]: return [ SamplerLCMUpscale, + SamplerLCM, SamplerEulerCFGpp, ] diff --git a/comfy_extras/nodes_hidream_o1.py b/comfy_extras/nodes_hidream_o1.py new file mode 100644 index 000000000..f393745f6 --- /dev/null +++ b/comfy_extras/nodes_hidream_o1.py @@ -0,0 +1,256 @@ +from typing_extensions import override + +import torch + +import comfy.model_management +import comfy.patcher_extension +import node_helpers +from comfy_api.latest import ComfyExtension, io + + +class EmptyHiDreamO1LatentImage(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="EmptyHiDreamO1LatentImage", + display_name="Empty HiDream-O1 Latent Image", + category="latent/image", + description=( + "Empty pixel-space latent for HiDream-O1-Image. The model was " + "trained at ~4 megapixels; lower resolutions go off-distribution " + "and quality regresses noticeably. Trained resolutions: " + "2048x2048, 2304x1728, 1728x2304, 2560x1440, 1440x2560, " + "2496x1664, 1664x2496, 3104x1312, 1312x3104, 2304x1792, 1792x2304." + ), + inputs=[ + io.Int.Input(id="width", default=2048, min=64, max=4096, step=32), + io.Int.Input(id="height", default=2048, min=64, max=4096, step=32), + io.Int.Input(id="batch_size", default=1, min=1, max=64), + ], + outputs=[io.Latent().Output()], + ) + + @classmethod + def execute(cls, *, width: int, height: int, batch_size: int = 1) -> io.NodeOutput: + latent = torch.zeros( + (batch_size, 3, height, width), + device=comfy.model_management.intermediate_device(), + ) + return io.NodeOutput({"samples": latent}) + + +class HiDreamO1ReferenceImages(io.ComfyNode): + """Attach reference images to both positive and negative conditioning.""" + + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="HiDreamO1ReferenceImages", + display_name="HiDream-O1 Reference Images", + category="conditioning/image", + description=( + "Attach 1-10 reference images to conditioning, one for edit instruction" + "or multiple for subject-driven personalization." + ), + inputs=[ + io.Conditioning.Input(id="positive"), + io.Conditioning.Input(id="negative"), + io.Autogrow.Input( + "images", + template=io.Autogrow.TemplateNames( + io.Image.Input("image"), + names=[f"image_{i}" for i in range(1, 11)], + min=1, + ), + tooltip=("Reference images. 1 image = instruction edit; 2-10 images = multi reference." + ), + ), + ], + outputs=[ + io.Conditioning.Output(display_name="positive"), + io.Conditioning.Output(display_name="negative"), + ], + ) + + @classmethod + def execute(cls, *, positive, negative, images: io.Autogrow.Type) -> io.NodeOutput: + refs = [images[f"image_{i}"] for i in range(1, 11) if f"image_{i}" in images] + positive = node_helpers.conditioning_set_values(positive, {"reference_latents": refs}, append=True) + negative = node_helpers.conditioning_set_values(negative, {"reference_latents": refs}, append=True) + return io.NodeOutput(positive, negative) + + +class HiDreamO1PatchSeamSmoothing(io.ComfyNode): + PATCH_SIZE = 32 + EDGE_FEATHER = 4 + + # Shift presets per (pattern, N). 8-pass = 4-quadrant + 4 quarter-patch offsets. + SHIFTS_BY_PATTERN = { + ("single_shift", 2): [(0, 0), (16, 16)], + ("single_shift", 4): [(0, 0), (16, 0), (0, 16), (16, 16)], + ("single_shift", 8): [(0, 0), (16, 0), (0, 16), (16, 16), + (8, 8), (24, 8), (8, 24), (24, 24)], + ("symmetric", 2): [(-8, -8), (8, 8)], + ("symmetric", 4): [(-8, -8), (8, -8), (-8, 8), (8, 8)], + ("symmetric", 8): [(-12, -12), (4, -12), (-12, 4), (4, 4), + (-4, -4), (12, -4), (-4, 12), (12, 12)], + } + RAMP_LEVELS = { + "2": [2], + "4": [4], + "ramp_2_4": [2, 4], + "ramp_2_4_8": [2, 4, 8], + } + + @staticmethod + def _hann_tile(cy: int, cx: int, size: int = 32) -> torch.Tensor: + """size x size Hann tile peaking at (cy, cx) within a patch.""" + half = size // 2 + yy = torch.arange(size).view(size, 1) + xx = torch.arange(size).view(1, size) + dy = ((yy - cy + half) % size) - half + dx = ((xx - cx + half) % size) - half + return 0.25 * (1 + torch.cos(torch.pi * dy / half)) * (1 + torch.cos(torch.pi * dx / half)) + + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="HiDreamO1PatchSeamSmoothing", + display_name="HiDream-O1 Patch Seam Smoothing", + category="advanced/model", + is_experimental=True, + description=( + "Average the model output across multiple shifted patch-grid " + "positions during the late portion of sampling. Cancels seams." + ), + inputs=[ + io.Model.Input(id="model"), + io.Float.Input(id="start_percent", default=0.8, min=0.0, max=1.0, step=0.01, + tooltip="Sampling progress (0=start, 1=end) at which the blend turns ON.", + ), + io.Float.Input(id="end_percent", default=1.0, min=0.0, max=1.0, step=0.01, + tooltip="Sampling progress at which the blend turns OFF.", + ), + io.Combo.Input( + id="pattern", + options=["single_shift", "symmetric"], + default="single_shift", + tooltip="Shift layout. single_shift: one pass at the natural patch grid + others offset. symmetric: all passes off-grid, shifts split around origin.", + ), + io.Combo.Input( + id="passes", + options=["2", "4", "ramp_2_4", "ramp_2_4_8"], + default="2", + tooltip="Number of passes per gated step. 2/4 = fixed. ramp_*: pass count increases as sampling approaches end (more smoothing where seams are most visible).", + ), + io.Combo.Input( + id="blend", + options=["average", "window", "median"], + default="average", + tooltip="average: equal-weight mean. window: Hann-windowed weighting favoring each pass away from its patch boundaries. median: per-pixel median, rejects wraparound-outlier passes.", + ), + io.Float.Input(id="strength", default=1.0, min=0.0, max=1.0, step=0.01, + tooltip="Interpolation between the natural-grid pred (0) and the averaged result (1).", + ), + ], + outputs=[io.Model.Output()], + ) + + @classmethod + def execute(cls, *, model, start_percent: float, end_percent: float, pattern: str, passes: str, blend: str, strength: float) -> io.NodeOutput: + if strength <= 0.0 or end_percent <= start_percent: + return io.NodeOutput(model) + + P = cls.PATCH_SIZE + half = P // 2 + shift_levels = [cls.SHIFTS_BY_PATTERN[(pattern, n)] for n in cls.RAMP_LEVELS[passes]] + + if blend == "window": + window_tile_levels = [ + torch.stack([cls._hann_tile((half - sy) % P, (half - sx) % P, P) for sy, sx in lst], dim=0) + for lst in shift_levels + ] + else: + window_tile_levels = [None] * len(shift_levels) + + m = model.clone() + model_sampling = m.get_model_object("model_sampling") + multiplier = float(model_sampling.multiplier) + start_t = float(model_sampling.percent_to_sigma(start_percent)) * multiplier + end_t = float(model_sampling.percent_to_sigma(end_percent)) * multiplier + + edge_ramp_cache: dict = {} + + def get_edge_ramp(H: int, W: int, device, dtype) -> torch.Tensor: + key = (H, W, device, dtype) + cached = edge_ramp_cache.get(key) + if cached is not None: + return cached + feather = cls.EDGE_FEATHER + ys = torch.minimum(torch.arange(H, device=device, dtype=torch.float32), + (H - 1) - torch.arange(H, device=device, dtype=torch.float32)) + xs = torch.minimum(torch.arange(W, device=device, dtype=torch.float32), + (W - 1) - torch.arange(W, device=device, dtype=torch.float32)) + y_mask = ((ys - P) / feather).clamp(0, 1) + x_mask = ((xs - P) / feather).clamp(0, 1) + ramp = (y_mask[:, None] * x_mask[None, :]).to(dtype) + edge_ramp_cache[key] = ramp + return ramp + + def smoothing_wrapper(executor, *args, **kwargs): + x = args[0] + t = float(args[1][0]) + pred = executor(*args, **kwargs) + if not (end_t <= t <= start_t): + return pred + # Pick shift-level by sigma phase across the gated range. + if len(shift_levels) == 1: + level_idx = 0 + else: + phase = (start_t - t) / max(start_t - end_t, 1e-8) + level_idx = min(int(phase * len(shift_levels)), len(shift_levels) - 1) + shifts = shift_levels[level_idx] + window_tiles = window_tile_levels[level_idx] + + preds = [] + for sy, sx in shifts: + if sy == 0 and sx == 0: + preds.append(pred) + continue + x_rolled = torch.roll(x, shifts=(sy, sx), dims=(-2, -1)) + pred_rolled = executor(x_rolled, *args[1:], **kwargs) + preds.append(torch.roll(pred_rolled, shifts=(-sy, -sx), dims=(-2, -1))) + stacked = torch.stack(preds, dim=0) # (N, B, C, H, W) + _, _, _, H, W = stacked.shape + if blend == "window": + N = stacked.shape[0] + tiles = window_tiles.to(device=stacked.device, dtype=stacked.dtype) + w = tiles.repeat(1, H // P, W // P)[:, :H, :W] + sum_w = w.sum(dim=0, keepdim=True) + w = torch.where(sum_w < 1e-3, torch.full_like(w, 1.0 / N), w / sum_w.clamp(min=1e-8)) + avg = (stacked * w[:, None, None, :, :]).sum(dim=0) + elif blend == "median": + avg = torch.median(stacked, dim=0).values + else: + avg = stacked.mean(dim=0) + + # Mask out the P-px wraparound contamination strip at each edge. + mask = get_edge_ramp(H, W, pred.device, pred.dtype) + return pred * (1.0 - mask * strength) + avg * (mask * strength) + + m.add_wrapper_with_key(comfy.patcher_extension.WrappersMP.DIFFUSION_MODEL, "hidream_o1_patch_seam_smoothing", smoothing_wrapper) + return io.NodeOutput(m) + + +class HiDreamO1Extension(ComfyExtension): + @override + async def get_node_list(self) -> list[type[io.ComfyNode]]: + return [ + EmptyHiDreamO1LatentImage, + HiDreamO1ReferenceImages, + HiDreamO1PatchSeamSmoothing, + ] + + +async def comfy_entrypoint() -> HiDreamO1Extension: + return HiDreamO1Extension() diff --git a/comfy_extras/nodes_model_advanced.py b/comfy_extras/nodes_model_advanced.py index 8bf6a1afa..33b940a0f 100644 --- a/comfy_extras/nodes_model_advanced.py +++ b/comfy_extras/nodes_model_advanced.py @@ -300,6 +300,29 @@ class RescaleCFG: m.set_model_sampler_cfg_function(rescale_cfg) return (m, ) +class ModelNoiseScale: + @classmethod + def INPUT_TYPES(s): + return {"required": { "model": ("MODEL",), + "noise_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 64.0, "step": 0.01, + "tooltip": "Absolute training noise scale. For example HiDream-O1 base: 8.0, dev: 7.5."}), + }} + + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + + CATEGORY = "advanced/model" + + def patch(self, model, noise_scale): + m = model.clone() + original = m.model.model_sampling + ms = type(original)(m.model.model_config) + ms.set_parameters(shift=original.shift, multiplier=original.multiplier) + ms.set_noise_scale(noise_scale) + m.add_object_patch("model_sampling", ms) + return (m, ) + + class ModelComputeDtype: SEARCH_ALIASES = ["model precision", "change dtype"] @classmethod @@ -327,6 +350,7 @@ NODE_CLASS_MAPPINGS = { "ModelSamplingSD3": ModelSamplingSD3, "ModelSamplingAuraFlow": ModelSamplingAuraFlow, "ModelSamplingFlux": ModelSamplingFlux, + "ModelNoiseScale": ModelNoiseScale, "RescaleCFG": RescaleCFG, "ModelComputeDtype": ModelComputeDtype, } diff --git a/nodes.py b/nodes.py index ec66e54d7..78aaaef74 100644 --- a/nodes.py +++ b/nodes.py @@ -2435,6 +2435,7 @@ async def init_builtin_extra_nodes(): "nodes_sam3.py", "nodes_void.py", "nodes_wandancer.py", + "nodes_hidream_o1.py", ] import_failed = [] From 0155ddcbe32b29cdc3284ec3fbd39edc47d3925e Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Mon, 11 May 2026 20:53:13 -0700 Subject: [PATCH 044/145] Fix dtype issue with hidream o1 (#13849) --- comfy/text_encoders/qwen35.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/comfy/text_encoders/qwen35.py b/comfy/text_encoders/qwen35.py index d0bbec3e6..b022009b1 100644 --- a/comfy/text_encoders/qwen35.py +++ b/comfy/text_encoders/qwen35.py @@ -451,9 +451,8 @@ class Qwen35VisionPatchEmbed(nn.Module): self.proj = ops.Conv3d(self.in_channels, self.embed_dim, kernel_size=kernel_size, stride=kernel_size, bias=True, device=device, dtype=dtype) def forward(self, x): - target_dtype = self.proj.weight.dtype x = x.view(-1, self.in_channels, self.temporal_patch_size, self.patch_size, self.patch_size) - return self.proj(x.to(target_dtype)).view(-1, self.embed_dim) + return self.proj(x).view(-1, self.embed_dim) class Qwen35VisionMLP(nn.Module): From c9589f29b21fc5f73b6eb9d5c98d29a68cf8c392 Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Tue, 12 May 2026 11:40:15 +0300 Subject: [PATCH 045/145] [Partner Nodes] fix Quiver nodes (#13851) Signed-off-by: bigcat88 --- comfy_api_nodes/nodes_quiver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/comfy_api_nodes/nodes_quiver.py b/comfy_api_nodes/nodes_quiver.py index 28862e368..3269c0afe 100644 --- a/comfy_api_nodes/nodes_quiver.py +++ b/comfy_api_nodes/nodes_quiver.py @@ -143,7 +143,7 @@ class QuiverTextToSVGNode(IO.ComfyNode): if reference_images: references = [] for key in reference_images: - url = await upload_image_to_comfyapi(cls, reference_images[key]) + url = await upload_image_to_comfyapi(cls, reference_images[key], mime_type="image/png") references.append(QuiverImageObject(url=url)) if len(references) > 4: raise ValueError("Maximum 4 reference images are allowed.") @@ -252,7 +252,7 @@ class QuiverImageToSVGNode(IO.ComfyNode): model: dict, seed: int, ) -> IO.NodeOutput: - image_url = await upload_image_to_comfyapi(cls, image) + image_url = await upload_image_to_comfyapi(cls, image, mime_type="image/png") response = await sync_op( cls, From fb097bedc2af8cc7499fba8ab6da8811ecc40491 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Tue, 12 May 2026 11:06:28 -0700 Subject: [PATCH 046/145] Mark deprecated cloud-runtime endpoints in spec (#13789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Mark deprecated cloud-runtime endpoints in openapi.yaml Add five cloud-runtime FE-facing endpoints to the OSS spec with deprecated: true and standardized description prefixes: - GET /api/history_v2 — superseded by GET /api/jobs - GET /api/history_v2/{prompt_id} — superseded by GET /api/jobs/{prompt_id} - GET /api/logs — returns static placeholder; no real log data - GET /api/viewvideo — alias of GET /api/view for legacy video playback - GET /api/job/{job_id}/status — superseded by GET /api/jobs/{job_id} Each endpoint is tagged x-runtime: [cloud] and follows the same deprecation convention established for /api/history endpoints. Co-authored-by: Matt Miller * fix(spec): consolidate duplicate path entries on deprecated cloud-runtime endpoints Previous commit added new path entries with `deprecated: true` for `/api/job/{job_id}/status`, `/api/history_v2`, `/api/history_v2/{prompt_id}`, `/api/logs`, and `/api/viewvideo`, but the canonical entries already existed elsewhere in the file. Result: 5 duplicate path keys (Spectral parser errors), and the deprecation flag did not land on the operations that FE clients consume by operationId. This commit moves `deprecated: true` plus the standardized "Deprecated." description onto the canonical operations (`getCloudJobStatus`, `getHistoryV2`, `getHistoryV2ByPromptId`, `getCloudLogs`, `viewVideo`) and removes the duplicate entries. Operation IDs and response schemas are unchanged. Spectral lint passes with zero new warnings. --- openapi.yaml | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index d4c9e67ca..96be4c1d5 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2071,7 +2071,6 @@ paths: type: integer description: Number of assets marked as missing - # =========================================================================== # Cloud-runtime FE-facing operations # @@ -2122,7 +2121,11 @@ paths: operationId: getCloudJobStatus tags: [queue] summary: Get status of a cloud job - description: "[cloud-only] Returns the current execution status of a cloud job." + deprecated: true + description: | + **Deprecated.** This endpoint is superseded by `GET /api/jobs/{job_id}`. + Clients should migrate; the endpoint is retained for backward + compatibility but will be removed in a future release. x-runtime: [cloud] parameters: - name: job_id @@ -2192,7 +2195,11 @@ paths: operationId: getHistoryV2 tags: [history] summary: Get paginated execution history (v2) - description: "[cloud-only] Returns a paginated list of execution history entries in the v2 format, with richer metadata than the legacy history endpoint." + deprecated: true + description: | + **Deprecated.** This endpoint is superseded by `GET /api/jobs`. + Clients should migrate; the endpoint is retained for backward + compatibility but will be removed in a future release. x-runtime: [cloud] parameters: - name: limit @@ -2231,7 +2238,11 @@ paths: operationId: getHistoryV2ByPromptId tags: [history] summary: Get v2 history for a specific prompt - description: "[cloud-only] Returns the v2 history entry for a specific prompt execution." + deprecated: true + description: | + **Deprecated.** This endpoint is superseded by `GET /api/jobs/{prompt_id}`. + Clients should migrate; the endpoint is retained for backward + compatibility but will be removed in a future release. x-runtime: [cloud] parameters: - name: prompt_id @@ -2266,7 +2277,12 @@ paths: operationId: getCloudLogs tags: [system] summary: Get cloud execution logs - description: "[cloud-only] Returns execution logs for the authenticated user's cloud jobs." + deprecated: true + description: | + **Deprecated.** This endpoint returns a static placeholder response and + provides no real log data. It is retained only to avoid breaking clients + that still call it. Clients should remove their dependency; the endpoint + will be removed in a future release. x-runtime: [cloud] parameters: - name: job_id @@ -5370,7 +5386,12 @@ paths: operationId: viewVideo tags: [view] summary: View or download a video file - description: "[cloud-only] Serves a video file from the output directory. Used by the frontend video player." + deprecated: true + description: | + **Deprecated.** This endpoint is an alias of `GET /api/view` added for + legacy history-queue video playback. Callers should use `/api/view` + directly; the endpoint is retained for backward compatibility but will + be removed in a future release. x-runtime: [cloud] parameters: - name: filename @@ -5523,7 +5544,6 @@ paths: schema: $ref: "#/components/schemas/CloudError" - components: parameters: ComfyUserHeader: @@ -6875,7 +6895,6 @@ components: error: type: string - # ------------------------------------------------------------------- # Cloud-runtime schemas # From a5f7bc5658fdc97de7478882b3a0f5aa2d765506 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Tue, 12 May 2026 13:14:50 -0700 Subject: [PATCH 047/145] Suppress false-positive Spectral lint on WebSocket endpoint (#13842) The /ws path uses HTTP 101 (Switching Protocols), which is the correct response for a WebSocket upgrade but not a 2xx. The built-in operation-success-response rule fires as a false positive because OpenAPI 3.x has no native WebSocket support. Add a path-scoped override in .spectral.yaml to disable the rule for /ws only, leaving it active for all other operations. --- .spectral.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.spectral.yaml b/.spectral.yaml index 4bb4a4a94..a4b137628 100644 --- a/.spectral.yaml +++ b/.spectral.yaml @@ -89,3 +89,12 @@ rules: then: field: description function: truthy + +overrides: + # /ws uses HTTP 101 (Switching Protocols) — a legitimate response for a + # WebSocket upgrade, but not a 2xx, so operation-success-response fires + # as a false positive. OpenAPI 3.x has no native WebSocket support. + - files: + - "openapi.yaml#/paths/~1ws" + rules: + operation-success-response: off From 1d95ed211e0d971a1bfa76330a079d03eff9c802 Mon Sep 17 00:00:00 2001 From: drozbay <17261091+drozbay@users.noreply.github.com> Date: Tue, 12 May 2026 16:57:31 -0600 Subject: [PATCH 048/145] Fix LTXV mid-video multi-frame guide alignment (CORE-129) (#13625) --- comfy_extras/nodes_lt.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/comfy_extras/nodes_lt.py b/comfy_extras/nodes_lt.py index a4c85db77..3dc1199c2 100644 --- a/comfy_extras/nodes_lt.py +++ b/comfy_extras/nodes_lt.py @@ -338,8 +338,25 @@ class LTXVAddGuide(io.ComfyNode): noise_mask = get_noise_mask(latent) _, _, latent_length, latent_height, latent_width = latent_image.shape + + # For mid-video multi-frame guides, prepend+strip a throwaway first frame so the VAE's "first latent = 1 pixel frame" asymmetry lands on the discarded slot + time_scale_factor = scale_factors[0] + num_frames_to_keep = ((image.shape[0] - 1) // time_scale_factor) * time_scale_factor + 1 + resolved_frame_idx = frame_idx + if frame_idx < 0: + _, num_keyframes = get_keyframe_idxs(positive) + resolved_frame_idx = max((latent_length - num_keyframes - 1) * time_scale_factor + 1 + frame_idx, 0) + causal_fix = resolved_frame_idx == 0 or num_frames_to_keep == 1 + + if not causal_fix: + image = torch.cat([image[:1], image], dim=0) + image, t = cls.encode(vae, latent_width, latent_height, image, scale_factors) + if not causal_fix: + t = t[:, :, 1:, :, :] + image = image[1:] + frame_idx, latent_idx = cls.get_latent_index(positive, latent_length, len(image), frame_idx, scale_factors) assert latent_idx + t.shape[2] <= latent_length, "Conditioning frames exceed the length of the latent sequence." @@ -352,6 +369,7 @@ class LTXVAddGuide(io.ComfyNode): t, strength, scale_factors, + causal_fix=causal_fix, ) # Track this guide for per-reference attention control. From 300b6c8c9186cfcd4b2b2c51ec0afd4449e7fbb7 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Tue, 12 May 2026 17:28:20 -0700 Subject: [PATCH 049/145] Revert some breaking changes. (#13861) --- comfy_extras/nodes_mask.py | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/comfy_extras/nodes_mask.py b/comfy_extras/nodes_mask.py index c9b2a84d9..96ee1a0f8 100644 --- a/comfy_extras/nodes_mask.py +++ b/comfy_extras/nodes_mask.py @@ -40,23 +40,13 @@ def composite(destination, source, x, y, mask = None, multiplier = 8, resize_sou inverse_mask = torch.ones_like(mask) - mask - source_rgb = source[:, :3, :visible_height, :visible_width] - dest_slice = destination[..., top:bottom, left:right] - - if destination.shape[1] == 4: - if torch.max(dest_slice) == 0: - destination[:, :3, top:bottom, left:right] = source_rgb - destination[:, 3:4, top:bottom, left:right] = mask - else: - destination[:, :3, top:bottom, left:right] = (mask * source_rgb) + (inverse_mask * dest_slice[:, :3]) - destination[:, 3:4, top:bottom, left:right] = torch.max(mask, dest_slice[:, 3:4]) - else: - source_portion = mask * source_rgb - destination_portion = inverse_mask * dest_slice - destination[..., top:bottom, left:right] = source_portion + destination_portion + source_portion = mask * source[..., :visible_height, :visible_width] + destination_portion = inverse_mask * destination[..., top:bottom, left:right] + destination[..., top:bottom, left:right] = source_portion + destination_portion return destination + class LatentCompositeMasked(IO.ComfyNode): @classmethod def define_schema(cls): @@ -95,23 +85,18 @@ class ImageCompositeMasked(IO.ComfyNode): display_name="Image Composite Masked", category="image", inputs=[ + IO.Image.Input("destination"), IO.Image.Input("source"), IO.Int.Input("x", default=0, min=0, max=nodes.MAX_RESOLUTION, step=1), IO.Int.Input("y", default=0, min=0, max=nodes.MAX_RESOLUTION, step=1), IO.Boolean.Input("resize_source", default=False), - IO.Image.Input("destination", optional=True), IO.Mask.Input("mask", optional=True), ], outputs=[IO.Image.Output()], ) @classmethod - def execute(cls, source, x, y, resize_source, destination = None, mask = None) -> IO.NodeOutput: - if destination is None: # transparent rgba - B, H, W, C = source.shape - destination = torch.zeros((B, H, W, 4), dtype=source.dtype, device=source.device) - if C == 3: - source = torch.nn.functional.pad(source, (0, 1), value=1.0) + def execute(cls, destination, source, x, y, resize_source, mask = None) -> IO.NodeOutput: destination, source = node_helpers.image_alpha_fix(destination, source) destination = destination.clone().movedim(-1, 1) output = composite(destination, source.movedim(-1, 1), x, y, mask, 1, resize_source).movedim(1, -1) From cccb697aa3d4f560a45b68d45f12369ca265079e Mon Sep 17 00:00:00 2001 From: angad777 Date: Wed, 13 May 2026 12:41:07 +1000 Subject: [PATCH 050/145] fix: create input directory if missing in LoadAudio define_schema (#13834) --- comfy_extras/nodes_audio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/comfy_extras/nodes_audio.py b/comfy_extras/nodes_audio.py index 5f514716f..6382dd618 100644 --- a/comfy_extras/nodes_audio.py +++ b/comfy_extras/nodes_audio.py @@ -297,6 +297,7 @@ class LoadAudio(IO.ComfyNode): @classmethod def define_schema(cls): input_dir = folder_paths.get_input_directory() + os.makedirs(input_dir, exist_ok=True) files = folder_paths.filter_files_content_types(os.listdir(input_dir), ["audio", "video"]) return IO.Schema( node_id="LoadAudio", From 2bd65f2091f0276e9ff6e18380d452d4f505fc27 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Tue, 12 May 2026 20:55:38 -0700 Subject: [PATCH 051/145] Better Hidream O1 mem usage factor for non dynamic vram. (#13864) --- comfy/supported_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy/supported_models.py b/comfy/supported_models.py index 8d2e02f68..1e4434fd5 100644 --- a/comfy/supported_models.py +++ b/comfy/supported_models.py @@ -1443,7 +1443,7 @@ class HiDreamO1(supported_models_base.BASE): } latent_format = latent_formats.HiDreamO1Pixel - memory_usage_factor = 0.6 + memory_usage_factor = 0.033 # fp16 not supported: LM MLP down_proj activations fp16 overflow, causing NaNs supported_inference_dtypes = [torch.bfloat16, torch.float32] From 240363f11e6605f8e864ff0491297d55b9793e91 Mon Sep 17 00:00:00 2001 From: "Daxiong (Lin)" Date: Wed, 13 May 2026 13:33:29 +0800 Subject: [PATCH 052/145] chore: update embedded docs to v0.5.0 (#13865) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c5a6f4cec..86c0a3c72 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ comfyui-frontend-package==1.43.18 comfyui-workflow-templates==0.9.73 -comfyui-embedded-docs==0.4.4 +comfyui-embedded-docs==0.5.0 torch torchsde torchvision From a5189fed515a96b71cf2b743fb93eaa3d42bc881 Mon Sep 17 00:00:00 2001 From: AustinMroz Date: Tue, 12 May 2026 23:42:31 -0700 Subject: [PATCH 053/145] Add Create Video to the essentials tab (#13863) --- comfy_extras/nodes_video.py | 1 + 1 file changed, 1 insertion(+) diff --git a/comfy_extras/nodes_video.py b/comfy_extras/nodes_video.py index 719acf2f1..78a2a28f8 100644 --- a/comfy_extras/nodes_video.py +++ b/comfy_extras/nodes_video.py @@ -123,6 +123,7 @@ class CreateVideo(io.ComfyNode): search_aliases=["images to video"], display_name="Create Video", category="video", + essentials_category="Video Tools", description="Create a video from images.", inputs=[ io.Image.Input("images", tooltip="The images to create a video from."), From 8505abf52e42f4441d9d53baf4c31a2ec7123400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Sepp=C3=A4nen?= <40791699+kijai@users.noreply.github.com> Date: Wed, 13 May 2026 18:33:53 +0300 Subject: [PATCH 054/145] feat: Extend Save3D to save vertex colors and textures (CORE-189) (#13824) Split GLB save logic out of nodes_hunyuan3d.py into a new nodes_save_3d.py, and extend the writer to support UVs, per-vertex colors, and embedded baseColor textures. Extend the MESH type with optional uvs, vertex_colors, and texture fields so meshes can carry texture data through the graph. Add pack_variable_mesh_batch / get_mesh_batch_item helpers and switch VoxelToMesh / VoxelToMeshBasic to use them so batches with differing vertex/face counts no longer fail at torch.stack. --- comfy_api/latest/_util/geometry_types.py | 21 +- comfy_extras/nodes_hunyuan3d.py | 211 +----------- comfy_extras/nodes_save_3d.py | 396 +++++++++++++++++++++++ nodes.py | 1 + 4 files changed, 422 insertions(+), 207 deletions(-) create mode 100644 comfy_extras/nodes_save_3d.py diff --git a/comfy_api/latest/_util/geometry_types.py b/comfy_api/latest/_util/geometry_types.py index b586fceb3..cdde60b10 100644 --- a/comfy_api/latest/_util/geometry_types.py +++ b/comfy_api/latest/_util/geometry_types.py @@ -12,9 +12,24 @@ class VOXEL: class MESH: - def __init__(self, vertices: torch.Tensor, faces: torch.Tensor): - self.vertices = vertices - self.faces = faces + def __init__(self, vertices: torch.Tensor, faces: torch.Tensor, + uvs: torch.Tensor | None = None, + vertex_colors: torch.Tensor | None = None, + texture: torch.Tensor | None = None, + vertex_counts: torch.Tensor | None = None, + face_counts: torch.Tensor | None = None): + + assert (vertex_counts is None) == (face_counts is None), \ + "vertex_counts and face_counts must be provided together (both or neither)" + self.vertices = vertices # vertices: (B, N, 3) + self.faces = faces # faces: (B, M, 3) + self.uvs = uvs # uvs: (B, N, 2) + self.vertex_colors = vertex_colors # vertex_colors: (B, N, 3 or 4) + self.texture = texture # texture: (B, H, W, 3) + # When vertices/faces are zero-padded to a common N/M across the batch (variable-size mesh batch), + # these hold the real per-item lengths (B,). None means rows are uniform and no slicing is needed. + self.vertex_counts = vertex_counts + self.face_counts = face_counts class File3D: diff --git a/comfy_extras/nodes_hunyuan3d.py b/comfy_extras/nodes_hunyuan3d.py index bf18ecb88..403eb855b 100644 --- a/comfy_extras/nodes_hunyuan3d.py +++ b/comfy_extras/nodes_hunyuan3d.py @@ -1,12 +1,7 @@ import torch -import os -import json -import struct -import numpy as np from comfy.ldm.modules.diffusionmodules.mmdit import get_1d_sincos_pos_embed_from_grid_torch -import folder_paths import comfy.model_management -from comfy.cli_args import args +from comfy_extras.nodes_save_3d import pack_variable_mesh_batch from typing_extensions import override from comfy_api.latest import ComfyExtension, IO, Types from comfy_api.latest._util import MESH, VOXEL # only for backward compatibility if someone import it from this file (will be removed later) # noqa @@ -444,7 +439,9 @@ class VoxelToMeshBasic(IO.ComfyNode): vertices.append(v) faces.append(f) - return IO.NodeOutput(Types.MESH(torch.stack(vertices), torch.stack(faces))) + if vertices and all(v.shape == vertices[0].shape for v in vertices) and all(f.shape == faces[0].shape for f in faces): + return IO.NodeOutput(Types.MESH(torch.stack(vertices), torch.stack(faces))) + return IO.NodeOutput(pack_variable_mesh_batch(vertices, faces)) decode = execute # TODO: remove @@ -481,206 +478,13 @@ class VoxelToMesh(IO.ComfyNode): vertices.append(v) faces.append(f) - return IO.NodeOutput(Types.MESH(torch.stack(vertices), torch.stack(faces))) + if vertices and all(v.shape == vertices[0].shape for v in vertices) and all(f.shape == faces[0].shape for f in faces): + return IO.NodeOutput(Types.MESH(torch.stack(vertices), torch.stack(faces))) + return IO.NodeOutput(pack_variable_mesh_batch(vertices, faces)) decode = execute # TODO: remove -def save_glb(vertices, faces, filepath, metadata=None): - """ - Save PyTorch tensor vertices and faces as a GLB file without external dependencies. - - Parameters: - vertices: torch.Tensor of shape (N, 3) - The vertex coordinates - faces: torch.Tensor of shape (M, 3) - The face indices (triangle faces) - filepath: str - Output filepath (should end with .glb) - """ - - # Convert tensors to numpy arrays - vertices_np = vertices.cpu().numpy().astype(np.float32) - faces_np = faces.cpu().numpy().astype(np.uint32) - - vertices_buffer = vertices_np.tobytes() - indices_buffer = faces_np.tobytes() - - def pad_to_4_bytes(buffer): - padding_length = (4 - (len(buffer) % 4)) % 4 - return buffer + b'\x00' * padding_length - - vertices_buffer_padded = pad_to_4_bytes(vertices_buffer) - indices_buffer_padded = pad_to_4_bytes(indices_buffer) - - buffer_data = vertices_buffer_padded + indices_buffer_padded - - vertices_byte_length = len(vertices_buffer) - vertices_byte_offset = 0 - indices_byte_length = len(indices_buffer) - indices_byte_offset = len(vertices_buffer_padded) - - gltf = { - "asset": {"version": "2.0", "generator": "ComfyUI"}, - "buffers": [ - { - "byteLength": len(buffer_data) - } - ], - "bufferViews": [ - { - "buffer": 0, - "byteOffset": vertices_byte_offset, - "byteLength": vertices_byte_length, - "target": 34962 # ARRAY_BUFFER - }, - { - "buffer": 0, - "byteOffset": indices_byte_offset, - "byteLength": indices_byte_length, - "target": 34963 # ELEMENT_ARRAY_BUFFER - } - ], - "accessors": [ - { - "bufferView": 0, - "byteOffset": 0, - "componentType": 5126, # FLOAT - "count": len(vertices_np), - "type": "VEC3", - "max": vertices_np.max(axis=0).tolist(), - "min": vertices_np.min(axis=0).tolist() - }, - { - "bufferView": 1, - "byteOffset": 0, - "componentType": 5125, # UNSIGNED_INT - "count": faces_np.size, - "type": "SCALAR" - } - ], - "meshes": [ - { - "primitives": [ - { - "attributes": { - "POSITION": 0 - }, - "indices": 1, - "mode": 4 # TRIANGLES - } - ] - } - ], - "nodes": [ - { - "mesh": 0 - } - ], - "scenes": [ - { - "nodes": [0] - } - ], - "scene": 0 - } - - if metadata is not None: - gltf["asset"]["extras"] = metadata - - # Convert the JSON to bytes - gltf_json = json.dumps(gltf).encode('utf8') - - def pad_json_to_4_bytes(buffer): - padding_length = (4 - (len(buffer) % 4)) % 4 - return buffer + b' ' * padding_length - - gltf_json_padded = pad_json_to_4_bytes(gltf_json) - - # Create the GLB header - # Magic glTF - glb_header = struct.pack('<4sII', b'glTF', 2, 12 + 8 + len(gltf_json_padded) + 8 + len(buffer_data)) - - # Create JSON chunk header (chunk type 0) - json_chunk_header = struct.pack('