diff --git a/README.md b/README.md index 8452f60..eaed135 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,12 @@ A sound, or a video (as 24 fps frames with its sound), from the input folder wit ### PromptForge Load Image (`PROMPTFORGE_LoadImage`) 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. + +### 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. + #### 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`. - **Search** looks through names, keywords, captions and descriptions; filters for folder and theme (female, male, group, people, nsfw, animals, scenery, objects, other). diff --git a/moss_nodes.py b/moss_nodes.py index ad9e244..786b71d 100644 --- a/moss_nodes.py +++ b/moss_nodes.py @@ -6,21 +6,10 @@ from comfy_extras.nodes_moss_image import TextEncodeMOSSImageEdit class PROMPTFORGE_TextEncode: + """Positive and negative text in one node, so a workflow needs one encode instead of two.""" CATEGORY = "🔮 PromptForge/Prompts" - RETURN_TYPES = ("CONDITIONING",) - FUNCTION = "encode" - - @classmethod - def INPUT_TYPES(cls): - return {"required": {"clip": ("CLIP",), "text": ("STRING", {"default": "", "multiline": True})}} - - def encode(self, clip, text): - return comfy_nodes.CLIPTextEncode().encode(clip, text) - - -class PROMPTFORGE_MossEditEncode: - CATEGORY = "🔮 PromptForge/Prompts" - RETURN_TYPES = ("CONDITIONING",) + RETURN_TYPES = ("CONDITIONING", "CONDITIONING") + RETURN_NAMES = ("POSITIVE", "NEGATIVE") FUNCTION = "encode" @classmethod @@ -28,7 +17,30 @@ class PROMPTFORGE_MossEditEncode: return { "required": { "clip": ("CLIP",), - "prompt": ("STRING", {"default": "", "multiline": True}), + "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."}), + } + } + + def encode(self, clip, text, negative): + encoder = comfy_nodes.CLIPTextEncode() + return encoder.encode(clip, text)[0], encoder.encode(clip, negative)[0] + + +class PROMPTFORGE_MossEditEncode: + """MOSS edit conditioning for both CFG passes: the prompt and the negative share the reference images.""" + CATEGORY = "🔮 PromptForge/Prompts" + RETURN_TYPES = ("CONDITIONING", "CONDITIONING") + RETURN_NAMES = ("POSITIVE", "NEGATIVE") + FUNCTION = "encode" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "clip": ("CLIP",), + "prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "The edit instruction: what should change."}), + "negative": ("STRING", {"default": "nsfw, worst quality, bad anatomy, watermark, jpeg artifact", "multiline": True, "tooltip": "What the image should avoid. The reference images are attached to it too."}), "edit_format": (["ai-toolkit", "ai-toolkit (kv_cache off)", "Moss-Image-Edit"], {"default": "Moss-Image-Edit"}), "image": ("IMAGE",), }, @@ -36,14 +48,48 @@ class PROMPTFORGE_MossEditEncode: "image2": ("IMAGE",), "image3": ("IMAGE",), "image4": ("IMAGE",), - "image_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.05}), + "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}), "vae": ("VAE",), }, } - def encode(self, clip, prompt, edit_format, image, image2=None, image3=None, image4=None, image_strength=1.0, vae=None): + def encode(self, clip, prompt, negative, edit_format, image, image2=None, image3=None, image4=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} - return TextEncodeMOSSImageEdit.execute(clip, prompt, edit_format, images, vae=vae, image_strength=image_strength).result + positive = TextEncodeMOSSImageEdit.execute(clip, prompt, edit_format, images, vae=vae, image_strength=image_strength).result + negative_out = TextEncodeMOSSImageEdit.execute(clip, negative, edit_format, images, vae=vae, image_strength=negative_image_strength).result + return positive[0], negative_out[0] + + +class PROMPTFORGE_KreaEditEncode: + """Grounded Krea edit positive and negative in one node; both see the source image.""" + CATEGORY = "🔮 PromptForge/Prompts" + RETURN_TYPES = ("CONDITIONING", "CONDITIONING") + RETURN_NAMES = ("POSITIVE", "NEGATIVE") + FUNCTION = "encode" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "clip": ("CLIP",), + "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",), + }, + "optional": { + "grounding_px": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 64}), + "system_prompt": ("STRING", {"default": "", "multiline": True}), + }, + } + + 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) + negative_out = comfy_nodes.NODE_CLASS_MAPPINGS["Krea2EditGroundedEncode"]().encode( + clip, negative, image=image, grounding_px=grounding_px, system_prompt=system_prompt) + return positive[0], negative_out[0] class PROMPTFORGE_SkipLayerGuidance: @@ -118,6 +164,7 @@ class PROMPTFORGE_SaveImage: NODE_CLASS_MAPPINGS = { "PROMPTFORGE_TextEncode": PROMPTFORGE_TextEncode, "PROMPTFORGE_MossEditEncode": PROMPTFORGE_MossEditEncode, + "PROMPTFORGE_KreaEditEncode": PROMPTFORGE_KreaEditEncode, "PROMPTFORGE_SkipLayerGuidance": PROMPTFORGE_SkipLayerGuidance, "PROMPTFORGE_ImageScale": PROMPTFORGE_ImageScale, "PROMPTFORGE_VAEDecode": PROMPTFORGE_VAEDecode, @@ -126,6 +173,7 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "PROMPTFORGE_TextEncode": "PromptForge Text Encode", "PROMPTFORGE_MossEditEncode": "PromptForge MOSS Edit Encode", + "PROMPTFORGE_KreaEditEncode": "PromptForge Krea Edit Encode", "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 94dc910..9bce088 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.7" +version = "1.28.8" license = { file = "LICENSE" } dependencies = [ "transformers",