Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- """ [COPY AND PASTE THE ENTIRE CODE INTO __init__.py in the Krea 2 Edit Custom Node folder ]
- ComfyUI-Krea2Edit — in-context edit forward for the Krea2 model (upgraded).
- ComfyUI's native Krea2 `_forward` is text-to-image only: it builds the sequence
- `[text | target]`. The krea2_edit LoRA (trained in ai-toolkit) needs the *appearance
- path*: the VAE-encoded SOURCE latent prepended as a block of clean tokens, distinguished
- from the (noisy) target purely by the 3-axis RoPE frame index (source=1..N, target=0, h/w
- aligned). This node adds that by wrapping the model's DIFFUSION_MODEL forward and rebuilding
- the sequence as `[text | source(s) | target]`, keeping only the target tokens out.
- Upgraded with Project-2 latent handling:
- - Pixel-space fit geometry (blur-proof, AR-safe) matching v1.2 training
- - Stride-1 centered RoPE offsets for native-fit refs
- - Clean multi-ref ordering (scene first, subject last)
- - Regional ref_boost_mask (last ref only)
- Wiring: LoadImage -> VAEEncode(source) --\
- Krea2EditModelPatch(model, source_latent) -> KSampler
- UNETLoader -> LoraLoaderModelOnly -/
- KSampler.latent_image <- EmptySD3LatentImage (noise). Text: NATIVE krea2 CLIP + CLIPTextEncode
- (or Krea2EditGroundedEncode for the semantic path).
- """
- import math
- import torch
- import torch.nn.functional as F
- from einops import rearrange
- import comfy.patcher_extension
- import comfy.utils
- import comfy.ldm.common_dit
- from comfy.ldm.flux.layers import timestep_embedding
- from comfy.ldm.flux.math import apply_rope
- from comfy.ldm.modules.attention import optimized_attention_masked
- CROP_TOL = 0.08 # near-matched-AR tolerance for the "fit" geometry (v1.2 value)
- def _imgids(bs, frame, h_, w_, device):
- ids = torch.zeros(h_, w_, 3, device=device, dtype=torch.float32)
- ids[..., 0] = frame
- ids[..., 1] = torch.arange(h_, device=device, dtype=torch.float32)[:, None]
- ids[..., 2] = torch.arange(w_, device=device, dtype=torch.float32)[None, :]
- return ids.reshape(1, h_ * w_, 3).repeat(bs, 1, 1)
- def _imgids_offset(bs, frame, gh, gw, th, tw, device):
- """Stride-1 integer positions at a centered integer offset.
- For fit-mode refs the pixels are already resampled to target grid density,
- so the position grid stays stride-1 by construction."""
- off_h, off_w = max(0, (th - gh) // 2), max(0, (tw - gw) // 2)
- ids = torch.zeros(gh, gw, 3, device=device, dtype=torch.float32)
- ids[..., 0] = frame
- ids[..., 1] = (torch.arange(gh, device=device, dtype=torch.float32) + off_h)[:, None]
- ids[..., 2] = (torch.arange(gw, device=device, dtype=torch.float32) + off_w)[None, :]
- return ids.reshape(1, gh * gw, 3).repeat(bs, 1, 1)
- def _to_4d(v):
- """(B,C,T,H,W) -> (B*T,C,H,W); pass 4D through. Images use T=1."""
- if v.ndim == 5:
- b, c, t, h, w = v.shape
- return v.reshape(b * t, c, h, w)
- return v
- def _fit_latent(src, H, W):
- """Latent-space fallback: center-crop to target AR then resize.
- Equals plain bilinear at matched AR; crops instead of stretching at mismatch."""
- sh, sw = src.shape[-2:]
- if (sh, sw) == (H, W):
- return src
- s = max(H / sh, W / sw)
- ch, cw = min(sh, int(round(H / s))), min(sw, int(round(W / s)))
- y0, x0 = (sh - ch) // 2, (sw - cw) // 2
- src = src[..., y0:y0 + ch, x0:x0 + cw]
- return F.interpolate(src.float(), size=(H, W), mode="bilinear")
- def fit_image_to_latent(image, lat_h, lat_w, fit_mode="fit"):
- """Pixel-space source prep (v1.2 'fit' geometry — the blur/stretch fix).
- Resample the IMAGE to the target grid BEFORE VAE-encoding so latents are
- never resized.
- fit: AR-preserving fit-inside at the target's grid density, /16 floor-snapped
- and capped at the target's /16 floor (training-matched). Near-matched AR
- fills the target grid exactly via a minimal center-crop.
- crop: center-crop to the target AR then resize (v1/v1.1 legacy).
- """
- px_h, px_w = lat_h * 8, lat_w * 8
- img = image[..., :3].movedim(-1, 1).float()
- ih, iw = img.shape[-2:]
- if fit_mode == "fit":
- sc = min(px_h / ih, px_w / iw)
- if ih * sc >= px_h * (1 - CROP_TOL) and iw * sc >= px_w * (1 - CROP_TOL):
- s = max(px_h / ih, px_w / iw)
- ch, cw = min(ih, int(round(px_h / s))), min(iw, int(round(px_w / s)))
- y0, x0 = (ih - ch) // 2, (iw - cw) // 2
- img = img[..., y0:y0 + ch, x0:x0 + cw]
- nh, nw = px_h, px_w
- else:
- nh = min(max(16, int(ih * sc) // 16 * 16), max(16, px_h // 16 * 16))
- nw = min(max(16, int(iw * sc) // 16 * 16), max(16, px_w // 16 * 16))
- img = F.interpolate(img, size=(nh, nw), mode="bicubic", antialias=True)
- else:
- s = max(px_h / ih, px_w / iw)
- ch, cw = min(ih, int(round(px_h / s))), min(iw, int(round(px_w / s)))
- y0, x0 = (ih - ch) // 2, (iw - cw) // 2
- img = img[..., y0:y0 + ch, x0:x0 + cw]
- img = F.interpolate(img, size=(px_h, px_w), mode="bicubic", antialias=True)
- return img.movedim(1, -1).clamp(0, 1)
- def _ref_attn_bias(boosts, boost_mask, txtlen, slens, tgtlen, mask_hw, device, dtype):
- """Additive attention-logit bias on the [text | refs... | target] sequence.
- boosts: per-ref factor on target->ref attention (last entry = last ref = subject).
- boost_mask (ComfyUI MASK) restricts the LAST ref's boost to a region (e.g. face).
- """
- nsrc = len(slens)
- offs = [txtlen]
- for sl in slens:
- offs.append(offs[-1] + sl)
- rows0 = offs[-1]
- L = rows0 + tgtlen
- bias = torch.zeros(1, 1, L, L, device=device, dtype=dtype)
- for i, b in enumerate(boosts):
- if b == 1.0:
- continue
- off, sl = offs[i], slens[i]
- if boost_mask is not None and i == nsrc - 1 and mask_hw is not None and len(mask_hw) > i:
- mask = boost_mask[:1]
- if mask.ndim == 2:
- mask = mask[None]
- mask = F.interpolate(mask[None].float(), size=mask_hw[i], mode="area")[0, 0]
- cols = off + torch.nonzero(mask.reshape(-1) > 0.5, as_tuple=True)[0].to(device)
- else:
- cols = torch.arange(off, off + sl, device=device)
- bias[:, :, rows0:, cols] = math.log(max(b, 1e-4))
- return bias
- def krea2_edit_forward(m, x, timesteps, context, src_latent, transformer_options,
- ref_boost=1.0, ref_boost_a=1.0, ref_boost_mask=None,
- ref_native=False, pos_mode="anchor"):
- """Krea2 SingleStreamDiT._forward with source block(s) prepended.
- m : the SingleStreamDiT (LoRA-patched at sample time)
- x : (B,C,H,W) or (B,C,T,H,W) noisy TARGET latent
- src_latent : clean SOURCE latent (VAE-encoded), 4D/5D — or a LIST of them
- (multi-ref: [scene, subject], frames 1..N)
- context : (B, seq, txtlayers*txtdim) — the 12-layer Qwen3-VL stack
- """
- patch = m.patch
- temporal = x.ndim == 5
- if temporal:
- b5, c5, t5, h5, w5 = x.shape
- x = _to_4d(x)
- bs, c, H_orig, W_orig = x.shape
- x = comfy.ldm.common_dit.pad_to_patch_size(x, (patch, patch), padding_mode="replicate")
- H, W = x.shape[-2], x.shape[-1]
- h_, w_ = H // patch, W // patch
- # source(s) -> list of (bs, C, H', W')
- src_list = src_latent if isinstance(src_latent, (list, tuple)) else [src_latent]
- srcs = []
- for sl in src_list:
- src = _to_4d(sl).to(x.device, x.dtype)
- if src.shape[0] != bs:
- src = src[:1].expand(bs, *src.shape[1:])
- # Native-fit refs that already fit inside the target keep their own grid
- # (stride-1 offset positions). Everything else is fitted in latent space.
- native = ref_native and src.shape[-2] <= H and src.shape[-1] <= W
- if src.shape[-2:] != (H, W) and not native:
- src = _fit_latent(src, H, W).to(x.dtype)
- srcs.append(comfy.ldm.common_dit.pad_to_patch_size(src, (patch, patch), padding_mode="replicate"))
- src_grids = [(s_.shape[-2] // patch, s_.shape[-1] // patch) for s_ in srcs]
- context = m._unpack_context(context)
- tgt_img = m.first(rearrange(x, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=patch, pw=patch))
- src_imgs = [m.first(rearrange(s_, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=patch, pw=patch))
- for s_ in srcs]
- t = m.tmlp(timestep_embedding(timesteps, m.tdim).unsqueeze(1).to(tgt_img.dtype))
- tvec = m.tproj(t)
- context = m.txtfusion(context, mask=None, transformer_options=transformer_options)
- context = m.txtmlp(context)
- txtlen, tgtlen = context.shape[1], tgt_img.shape[1]
- srclen = sum(si.shape[1] for si in src_imgs)
- combined = torch.cat([context] + src_imgs + [tgt_img], dim=1)
- device = combined.device
- if pos_mode == "stride1" and ref_native:
- ref_ids = [_imgids_offset(bs, i + 1, gh, gw, h_, w_, device)
- for i, (gh, gw) in enumerate(src_grids)]
- else:
- ref_ids = [_imgids(bs, i + 1, gh, gw, device) for i, (gh, gw) in enumerate(src_grids)]
- pos = torch.cat([
- torch.zeros(bs, txtlen, 3, device=device, dtype=torch.float32)]
- + ref_ids
- + [_imgids(bs, 0, h_, w_, device)],
- dim=1)
- freqs = m.pe_embedder(pos)
- attn_bias = None
- if ref_boost != 1.0 or ref_boost_a != 1.0:
- # last ref = subject; earlier refs (scene) get ref_boost_a
- boosts = [ref_boost_a] * (len(src_imgs) - 1) + [ref_boost]
- attn_bias = _ref_attn_bias(boosts, ref_boost_mask, txtlen,
- [si.shape[1] for si in src_imgs], tgtlen,
- src_grids, combined.device, combined.dtype)
- for block in m.blocks:
- combined = block(combined, tvec, freqs, attn_bias, transformer_options=transformer_options)
- final = m.last(combined, t)
- out = final[:, txtlen + srclen: txtlen + srclen + tgtlen, :]
- out = rearrange(out, "b (h w) (c ph pw) -> b c (h ph) (w pw)",
- h=h_, w=w_, ph=patch, pw=patch, c=m.channels)
- out = out[:, :, :H_orig, :W_orig]
- if temporal:
- out = out.reshape(b5, t5, m.channels, H_orig, W_orig).movedim(1, 2)
- return out
- class Krea2EditModelPatch:
- @classmethod
- def INPUT_TYPES(cls):
- return {"required": {
- "model": ("MODEL",),
- "source_latent": ("LATENT",),
- }, "optional": {
- "source_latent_b": ("LATENT", {
- "tooltip": "2nd reference (subject photo) for multi-ref LoRAs → RoPE frame=2. "
- "Training-matched order: scene first, subject second."}),
- "ref_boost": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1000.0, "step": 0.01, "round": 0.001,
- "tooltip": "reference-fidelity dial: multiplies target→reference attention. "
- "Applies to the LAST ref (= subject in two-ref workflows). "
- "1.0 = off; >1 pulls harder (v1.2 author suggests 2–6)."}),
- "ref_boost_a": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1000.0, "step": 0.01, "round": 0.001,
- "tooltip": "same dial for the FIRST ref (= scene in two-ref workflows). "
- "No effect in single-ref. 1.0 = off."}),
- "ref_boost_mask": ("MASK", {
- "tooltip": "optional region on the (last) reference to boost, e.g. the face; "
- "empty = whole reference. Only affects ref_boost."}),
- "fit_mode": (["fit", "crop (legacy)"], {"default": "fit",
- "tooltip": "how an image source fits a mismatched output AR "
- "(needs vae + source_image): fit = v1.2 pixel-space geometry "
- "(blur-proof); crop = v1/v1.1 legacy center-crop + resize."}),
- "vae": ("VAE", {
- "tooltip": "RECOMMENDED with source_image: enables the blur-proof pixel-space path "
- "(crop/resize in pixels, encode internally)."}),
- "source_image": ("IMAGE", {
- "tooltip": "source as IMAGE (with vae): overrides source_latent with exact "
- "pixel-space fitting — fixes blurry results from resolution mismatch."}),
- "source_image_b": ("IMAGE", {
- "tooltip": "2nd reference as IMAGE (with vae)."}),
- }}
- RETURN_TYPES = ("MODEL",)
- FUNCTION = "patch"
- CATEGORY = "krea2edit"
- DESCRIPTION = ("[MODDED] Adds the krea2_edit in-context source-preservation path "
- "(source latent as frame=1+ tokens) to a Krea2 model. "
- "Upgraded with v1.2 pixel-space fit geometry and regional boost mask.")
- def patch(self, model, source_latent, source_latent_b=None, ref_boost=1.0, ref_boost_a=1.0,
- ref_boost_mask=None, vae=None, source_image=None,
- source_image_b=None, fit_mode="fit", **_future):
- if _future:
- print(f"[krea2edit] WARNING: workflow provides inputs this node version does not "
- f"know ({', '.join(sorted(_future))}). Update the node pack and restart "
- f"ComfyUI. Continuing without them.", flush=True)
- m = model.clone()
- # Target latent reaches the diffusion model already scaled (process_latent_in);
- # scale the source(s) the same way so all share one latent space.
- src_samples = model.model.process_latent_in(source_latent["samples"])
- if source_latent_b is not None:
- src_samples = [src_samples, model.model.process_latent_in(source_latent_b["samples"])]
- px_cache = {} # pixel-path encoded sources, keyed per target resolution
- mm = model.model
- if fit_mode == "fit" and (vae is None or source_image is None):
- print(f"[krea2edit] WARNING: fit_mode='fit' has limited effect without both "
- f"'vae' and 'source_image' connected (pixel path). Falling back to latent "
- f"crop path where needed.", flush=True)
- def wrapper(executor, x, timesteps, context, *wargs, **kwargs):
- # Signature-drift guard (ComfyUI has added/removed positional args over time).
- transformer_options = kwargs.pop("transformer_options", None)
- if transformer_options is None:
- transformer_options = {}
- for a in reversed(wargs):
- if isinstance(a, dict):
- transformer_options = a
- break
- dm = executor.class_obj # the SingleStreamDiT instance
- src = src_samples
- use_pixel = vae is not None and source_image is not None
- if use_pixel:
- if not px_cache:
- print(f"[krea2edit] pixel path ACTIVE (fit_mode={fit_mode})", flush=True)
- xx = _to_4d(x)
- Hh, Ww = xx.shape[-2], xx.shape[-1]
- mode = "crop" if fit_mode.startswith("crop") else "fit"
- key_a = ("a", Hh, Ww, mode)
- if key_a not in px_cache:
- fitted = fit_image_to_latent(source_image[:1], Hh, Ww, mode)
- px_cache[key_a] = mm.process_latent_in(vae.encode(fitted))
- lat = px_cache[key_a]
- if source_image_b is not None:
- key_b = ("b", Hh, Ww, mode)
- if key_b not in px_cache:
- fitted_b = fit_image_to_latent(source_image_b[:1], Hh, Ww, mode)
- px_cache[key_b] = mm.process_latent_in(vae.encode(fitted_b))
- lat = [lat, px_cache[key_b]]
- src = lat
- v = krea2_edit_forward(
- dm, x, timesteps, context, src, transformer_options,
- ref_boost=ref_boost, ref_boost_a=ref_boost_a,
- ref_boost_mask=ref_boost_mask,
- ref_native=use_pixel and not fit_mode.startswith("crop"),
- pos_mode=("stride1" if (use_pixel and not fit_mode.startswith("crop")) else "anchor"),
- )
- return v
- to = m.model_options.setdefault("transformer_options", {})
- comfy.patcher_extension.add_wrapper_with_key(
- comfy.patcher_extension.WrappersMP.DIFFUSION_MODEL, "krea2_edit", wrapper, to
- )
- return (m,)
- class Krea2EditGroundedEncode:
- """Image-grounded instruction encode — the SEMANTIC path of krea2_edit.
- Training always encodes the instruction TOGETHER with the source image through
- Qwen3-VL (user turn = <vision tokens: source> + instruction) and taps 12 layers.
- Stock CLIPTextEncode is text-only, so inference was running with the grounding
- half of the recipe missing (the VAE source tokens carry appearance; THIS carries
- scene semantics: "the man on the left", "the sign in the back").
- Requires a qwen3vl TE checkpoint WITH the vision tower.
- grounding_px caps the longest side fed to the VLM — the identity LoRA trained
- with 384-768px jitter, so 640-768 is in-distribution; 0 = native res.
- For CFG, ground the NEGATIVE too: second node, empty prompt, same image.
- """
- DEFAULT_SYSTEM = (
- "Describe the image by detailing the color, shape, size, "
- "texture, quantity, text, spatial relationships of the objects and background:"
- )
- @classmethod
- def _template(cls, nimg, system_prompt=""):
- sp = system_prompt.strip() or cls.DEFAULT_SYSTEM
- vis = "<|vision_start|><|image_pad|><|vision_end|>" * nimg
- return ("<|im_start|>system\n" + sp + "<|im_end|>\n<|im_start|>user\n"
- + vis + "{}<|im_end|>\n<|im_start|>assistant\n")
- @classmethod
- def INPUT_TYPES(cls):
- return {
- "required": {
- "clip": ("CLIP",),
- "prompt": ("STRING", {"multiline": True, "default": ""}),
- },
- "optional": {
- "image": ("IMAGE",),
- "image_b": ("IMAGE", {
- "tooltip": "2nd reference (subject) for multi-ref LoRAs; "
- "vision blocks in training order: scene, subject"}),
- "grounding_px": ("INT", {"default": 768, "min": 0, "max": 4096, "step": 64,
- "tooltip": "cap longest side fed to Qwen3-VL; 0 = native"}),
- "system_prompt": ("STRING", {"multiline": True, "default": "",
- "tooltip": "advanced: override the grounding system prompt "
- "(empty = training default)."}),
- },
- }
- RETURN_TYPES = ("CONDITIONING",)
- FUNCTION = "encode"
- CATEGORY = "krea2edit"
- DESCRIPTION = "[MODDED] Encodes the edit instruction grounded on the source image (training-matched semantic path)."
- def _prep(self, image, grounding_px):
- samples = image.movedim(-1, 1)
- h, w = samples.shape[2], samples.shape[3]
- if grounding_px and max(h, w) > grounding_px:
- s = grounding_px / max(h, w)
- samples = comfy.utils.common_upscale(samples, round(w * s), round(h * s), "area", "disabled")
- return samples.movedim(1, -1)[:, :, :, :3]
- def encode(self, clip, prompt, image=None, image_b=None, grounding_px=768, system_prompt="", **_future):
- if _future:
- print(f"[krea2edit] WARNING: workflow provides inputs this node version does not "
- f"know ({', '.join(sorted(_future))}). Update and restart ComfyUI.", flush=True)
- if image is None:
- tokens = clip.tokenize(prompt)
- return (clip.encode_from_tokens_scheduled(tokens),)
- imgs = [self._prep(image, grounding_px)]
- if image_b is not None:
- imgs.append(self._prep(image_b, grounding_px))
- template = self._template(len(imgs), system_prompt)
- tokens = clip.tokenize(prompt, images=imgs, llama_template=template)
- return (clip.encode_from_tokens_scheduled(tokens),)
- NODE_CLASS_MAPPINGS = {
- "Krea2EditModelPatch": Krea2EditModelPatch,
- "Krea2EditGroundedEncode": Krea2EditGroundedEncode,
- }
- NODE_DISPLAY_NAME_MAPPINGS = {
- "Krea2EditModelPatch": "Krea2 Edit [MODDED] (source patch)",
- "Krea2EditGroundedEncode": "Krea2 Edit [MODDED] (grounded encode)",
- }
- def _pack_version():
- try:
- import os, re
- p = os.path.join(os.path.dirname(__file__), "pyproject.toml")
- with open(p) as f:
- m = re.search(r'^version\s*=\s*"([^"]+)"', f.read(), re.M)
- return m.group(1) if m else "2.0-upgraded"
- except Exception:
- return "2.0-upgraded"
- print(f"[krea2edit] nodes v{_pack_version()} loaded (ModelPatch + v1.2 latent handling + mask)", flush=True)
Advertisement