diff --git a/README.md b/README.md index de298c6..7bb1c06 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,10 @@ Use the `Krea` presets in the sampler list: FinePorn 8-10 steps, cfg 1, `euler`/ ### Negative prompts at cfg 1 -`use_negative` on the encode node is the NAG switch. Off: the negative is ignored and the run takes the fast -cfg-1 path. On: the sampler runs the uncond pass and applies normalized attention guidance at cfg 1, without -changing cfg. Krea edit no longer uses a separate NAG node; the encoder and sampler handle it. +The NAG switch is `use_nag` on the model patch: `PromptForge Krea NAG` (text-to-image) and +`PromptForge Krea Edit Model Patch` (edit and transfer). Off passes the model through; on applies normalized +attention guidance from the negative prompt at cfg 1, without changing cfg. The encoders always encode the +negative, which costs nothing when NAG is off. ## MOSS Image @@ -200,10 +201,16 @@ A sound, or a video (as 24 fps frames with its sound), from the input folder wit Loads an image from the input folder through a thumbnail browser (search, folder filter, upload). Each image can have a stored description (`user/promptforge/image_descriptions.json`): *Save description* stores the text typed in the node (picking another image never changes that box; the stored text shows as its placeholder), and the node outputs it as `DESCRIPTION` next to `IMAGE`, `MASK` and `NAME`. Text typed in the node is used as is; an empty box falls back to the stored description. Handy for switching a character and its description together in reference-to-video workflows. ### PromptForge Text Encode (`PROMPTFORGE_TextEncode`) -The positive prompt and the negative prompt in one node: `text` becomes POSITIVE and `negative` becomes NEGATIVE on the same CLIP, so a graph needs one encode instead of two. `use_negative` (off by default) decides whether the negative is real; off returns the positive for both outputs and keeps a cfg-1 run fast, on runs the negative through normalized attention guidance at cfg 1 without touching cfg. +The positive prompt and the negative prompt in one node: `text` becomes POSITIVE and `negative` becomes NEGATIVE on the same CLIP, so a graph needs one encode instead of two. The negative is always encoded; the model patch's `use_nag` decides whether it is used. + +### PromptForge Krea NAG / Krea Edit Model Patch (`PROMPTFORGE_KreaNAG`, `PROMPTFORGE_KreaEditModelPatch`) + +Model patches for Krea 2. `PromptForge Krea NAG` is the text-to-image NAG path; `PromptForge Krea Edit Model Patch` +adds the source-reference path and can also apply NAG for the edit and transfer workflows. Both have `use_nag` +(off for the NAG node, on for the edit patch) and work at cfg 1 without changing cfg. ### PromptForge MOSS Edit Encode / Krea Edit Encode (`PROMPTFORGE_MossEditEncode`, `PROMPTFORGE_KreaEditEncode`) -Edit conditioning for both CFG passes in one node. The MOSS node attaches the reference images and VAE to the positive and to the negative, each with its own image strength. The Krea node grounds the positive and the negative on the source image through the `Krea2EditGroundedEncode` path. The prompt is the positive instruction, the negative is what to avoid. `use_negative` (off by default) enables the negative: off returns the positive for both outputs and keeps the fast cfg-1 path; on makes the sampler run the uncond pass and apply normalized attention guidance, so the negative works **at cfg 1 without changing cfg**. Leave it off on turbo models unless the negative matters. +Edit conditioning for both CFG passes in one node. The MOSS node attaches the reference images and VAE to the positive and to the negative, each with its own image strength. The Krea node grounds the positive and the negative on the source image through the `Krea2EditGroundedEncode` path. The prompt is the positive instruction, the negative is what to avoid. The negative is always encoded; the model patch's `use_nag` decides whether NAG uses it at cfg 1. #### Image library (picker and sidebar) The Choose image picker, and an **Images** tab in the left sidebar, share one library of the input folder. **Tools** runs a local vision model (a GGUF with its mmproj, found in your text_encoders / LLM folders) that gives every image keywords, a caption, a people count and an NSFW flag; results are stored in `user/promptforge/image_tags.json`. diff --git a/moss_nodes.py b/moss_nodes.py index 5fcca96..b3ec9d6 100644 --- a/moss_nodes.py +++ b/moss_nodes.py @@ -20,17 +20,12 @@ class PROMPTFORGE_TextEncode: "clip": ("CLIP",), "text": ("STRING", {"default": "", "multiline": True, "tooltip": "The prompt."}), "negative": ("STRING", {"default": "worst quality, bad anatomy, watermark, jpeg artifact", "multiline": True, "tooltip": "What the image should avoid."}), - "use_negative": ("BOOLEAN", {"default": False, "tooltip": "Use the negative prompt. Off at low cfg (1) keeps the fast path; on raises cfg 1 to 1.5 in the sampler. At higher cfg, off makes the negative identical to the positive."}), } } - def encode(self, clip, text, negative, use_negative=False): + def encode(self, clip, text, negative): encoder = comfy_nodes.CLIPTextEncode() - positive = encoder.encode(clip, text)[0] - if not use_negative: - return positive, positive - negative_out = encoder.encode(clip, negative)[0] - return positive, node_helpers.conditioning_set_values(negative_out, {"promptforge_use_negative": True}) + return encoder.encode(clip, text)[0], encoder.encode(clip, negative)[0] class PROMPTFORGE_MossEditEncode: @@ -56,19 +51,16 @@ class PROMPTFORGE_MossEditEncode: "image4": ("IMAGE",), "image_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.05, "tooltip": "How hard the prompt listens to the images."}), "negative_image_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.05}), - "use_negative": ("BOOLEAN", {"default": False, "tooltip": "Use the negative prompt. Off keeps the fast path and makes the negative identical to the positive; on raises cfg 1 to 1.5 in the sampler."}), "vae": ("VAE",), }, } def encode(self, clip, prompt, negative, edit_format, image, image2=None, image3=None, image4=None, - image_strength=1.0, negative_image_strength=1.0, use_negative=False, vae=None): + image_strength=1.0, negative_image_strength=1.0, vae=None): images = {f"image_{index}": value for index, value in enumerate((image, image2, image3, image4)) if value is not None} positive = TextEncodeMOSSImageEdit.execute(clip, prompt, edit_format, images, vae=vae, image_strength=image_strength).result[0] - if not use_negative: - return positive, positive negative_out = TextEncodeMOSSImageEdit.execute(clip, negative, edit_format, images, vae=vae, image_strength=negative_image_strength).result[0] - return positive, node_helpers.conditioning_set_values(negative_out, {"promptforge_use_negative": True}) + return positive, negative_out class PROMPTFORGE_KreaEditEncode: @@ -86,7 +78,6 @@ class PROMPTFORGE_KreaEditEncode: "prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "The edit instruction: what should change."}), "negative": ("STRING", {"default": "deformed, blurry, bad anatomy, bad hands, low quality", "multiline": True}), "image": ("IMAGE",), - "use_negative": ("BOOLEAN", {"default": False, "tooltip": "Use the negative prompt. Off keeps the fast path and makes the negative identical to the positive; on raises cfg 1 to 1.5 in the sampler."}), }, "optional": { "grounding_px": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 64}), @@ -94,14 +85,82 @@ class PROMPTFORGE_KreaEditEncode: }, } - def encode(self, clip, prompt, negative, image, use_negative=False, grounding_px=0, system_prompt=""): + def encode(self, clip, prompt, negative, image, grounding_px=0, system_prompt=""): grounded = comfy_nodes.NODE_CLASS_MAPPINGS["Krea2EditGroundedEncode"]() positive = grounded.encode(clip, prompt, image=image, grounding_px=grounding_px, system_prompt=system_prompt)[0] - if not use_negative: - return positive, positive negative_out = comfy_nodes.NODE_CLASS_MAPPINGS["Krea2EditGroundedEncode"]().encode( clip, negative, image=image, grounding_px=grounding_px, system_prompt=system_prompt)[0] - return positive, node_helpers.conditioning_set_values(negative_out, {"promptforge_use_negative": True}) + return positive, negative_out + + +class PROMPTFORGE_KreaNAG: + """Krea2 text-to-image NAG as a model patch, with an off switch.""" + CATEGORY = "🔮 PromptForge/Sampling" + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL",), + "use_nag": ("BOOLEAN", {"default": False, "tooltip": "Apply normalized attention guidance from the negative prompt. Works at cfg 1 and does not change cfg."}), + }, + "optional": { + "nag_negative": ("CONDITIONING",), + "phi": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 20.0, "step": 0.1, "advanced": True}), + "tau": ("FLOAT", {"default": 2.5, "min": 0.01, "max": 20.0, "step": 0.05, "advanced": True}), + "alpha": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01, "advanced": True}), + }, + } + + def patch(self, model, use_nag, nag_negative=None, phi=4.0, tau=2.5, alpha=0.25): + if not use_nag or nag_negative is None: + return (model,) + return comfy_nodes.NODE_CLASS_MAPPINGS["Krea2NormalizedAttentionGuidance"]().patch( + model, nag_negative, phi, tau, alpha, 1000.0, 0.0) + + +class PROMPTFORGE_KreaEditModelPatch: + """Krea2 edit source patch; the same node can also apply NAG to the negative prompt.""" + CATEGORY = "🔮 PromptForge/Sampling" + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL",), + "source_latent": ("LATENT",), + "use_nag": ("BOOLEAN", {"default": True, "tooltip": "Apply normalized attention guidance from the negative prompt. Works at cfg 1 and does not change cfg."}), + }, + "optional": { + "nag_negative": ("CONDITIONING",), + "source_latent_b": ("LATENT",), + "ref_boost": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1000.0, "step": 0.01}), + "ref_boost_a": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1000.0, "step": 0.01}), + "fit_mode": (["fit", "crop (legacy)"], {"default": "fit"}), + "vae": ("VAE",), + "source_image": ("IMAGE",), + "target_latent": ("LATENT",), + "phi": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 20.0, "step": 0.1, "advanced": True}), + "tau": ("FLOAT", {"default": 2.5, "min": 0.01, "max": 20.0, "step": 0.05, "advanced": True}), + "alpha": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01, "advanced": True}), + }, + } + + def patch(self, model, source_latent, use_nag, nag_negative=None, source_latent_b=None, ref_boost=1.0, + ref_boost_a=1.0, fit_mode="fit", vae=None, source_image=None, target_latent=None, + phi=4.0, tau=2.5, alpha=0.25): + common = dict(source_latent=source_latent, source_latent_b=source_latent_b, ref_boost=ref_boost, + ref_boost_a=ref_boost_a, fit_mode=fit_mode, vae=vae, source_image=source_image, + target_latent=target_latent) + if use_nag and nag_negative is not None: + rest = {key: value for key, value in common.items() if key != "source_latent"} + return comfy_nodes.NODE_CLASS_MAPPINGS["Krea2EditNormalizedAttentionGuidance"]().patch( + model, nag_negative, source_latent, phi, tau, alpha, 1000.0, 0.0, **rest) + return comfy_nodes.NODE_CLASS_MAPPINGS["Krea2EditModelPatch"]().patch(model, source_latent, **common) class PROMPTFORGE_SkipLayerGuidance: @@ -177,6 +236,8 @@ NODE_CLASS_MAPPINGS = { "PROMPTFORGE_TextEncode": PROMPTFORGE_TextEncode, "PROMPTFORGE_MossEditEncode": PROMPTFORGE_MossEditEncode, "PROMPTFORGE_KreaEditEncode": PROMPTFORGE_KreaEditEncode, + "PROMPTFORGE_KreaNAG": PROMPTFORGE_KreaNAG, + "PROMPTFORGE_KreaEditModelPatch": PROMPTFORGE_KreaEditModelPatch, "PROMPTFORGE_SkipLayerGuidance": PROMPTFORGE_SkipLayerGuidance, "PROMPTFORGE_ImageScale": PROMPTFORGE_ImageScale, "PROMPTFORGE_VAEDecode": PROMPTFORGE_VAEDecode, @@ -186,6 +247,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "PROMPTFORGE_TextEncode": "PromptForge Text Encode", "PROMPTFORGE_MossEditEncode": "PromptForge MOSS Edit Encode", "PROMPTFORGE_KreaEditEncode": "PromptForge Krea Edit Encode", + "PROMPTFORGE_KreaNAG": "PromptForge Krea NAG", + "PROMPTFORGE_KreaEditModelPatch": "PromptForge Krea Edit Model Patch", "PROMPTFORGE_SkipLayerGuidance": "PromptForge Skip Layer Guidance", "PROMPTFORGE_ImageScale": "PromptForge Image Scale", "PROMPTFORGE_VAEDecode": "PromptForge VAE Decode", diff --git a/pyproject.toml b/pyproject.toml index 886ebb2..c584244 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI-PromptForge" description = "ComfyUI prompt enhancement suite: model/config node with GPU-aware auto settings, prompt enhancer with a preset catalog, and a standalone config finder." -version = "1.28.18" +version = "1.28.19" license = { file = "LICENSE" } dependencies = [ "transformers", diff --git a/sampler.py b/sampler.py index 5caedd9..d43a226 100644 --- a/sampler.py +++ b/sampler.py @@ -83,22 +83,6 @@ async def _recommend_route(request): return web.json_response({"settings": settings, "source": source}) -def _nag_pre_cfg(args): - """Normalized attention guidance on the conditioning batch, so a negative works at cfg 1.""" - conds = args["conds_out"] - if len(conds) < 2 or conds[1] is None: - return conds - positive, negative = conds[0], conds[1] - scale, alpha, tau = 4.0, 0.25, 2.5 - guided = positive * scale - negative * (scale - 1.0) - norm_positive = torch.norm(positive, p=1, dim=-1, keepdim=True).clamp_min(1e-6) - norm_guided = torch.norm(guided, p=1, dim=-1, keepdim=True).clamp_min(1e-6) - ratio = norm_guided / norm_positive - guided = guided * (torch.minimum(ratio, torch.full_like(ratio, tau)) / ratio) - conds[0] = guided * alpha + positive * (1.0 - alpha) - return conds - - def parse_sigmas(text): """'1, 0.8, 0.5' -> descending sigma tensor ending in 0.""" values = [float(part) for part in text.replace("\n", ",").split(",") if part.strip()] @@ -167,12 +151,6 @@ class PROMPTFORGE_Sampler: pipe=None, sigmas=None, custom_sigmas="", step_cache="off", cache_threshold=0.2, cache_start=0.15, cache_end=0.95, start_at_step=0, end_at_step=10000, add_noise=True, preview="default"): family = self._family(model, pipe) - if cfg <= 1.0 and any(len(cond) > 1 and cond[1].get("promptforge_use_negative") for cond in negative): - # the negative is real: force the uncond pass and guide with NAG instead of changing cfg - model = model.clone() - model.model_options = dict(model.model_options) - model.model_options["disable_cfg1_optimization"] = True - model.model_options["sampler_pre_cfg_function"] = [_nag_pre_cfg] + list(model.model_options.get("sampler_pre_cfg_function", [])) if step_cache == "auto": step_cache = "easycache" if steps >= AUTO_CACHE_MIN_STEPS else "off" if step_cache != "off":