diff --git a/README.md b/README.md index 081d582..c60664f 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ KSampler, KSampler (Advanced) and custom sigmas in one node, built on `comfy.sam - **Schedules**: `scheduler = auto` uses LTX Video's token-count dependent schedule (max_shift, base_shift, terminal exposed) for LTX models and `simple` otherwise. Sigmas can also come from a SIGMAS input or a typed list (`custom_sigmas`), and `denoise` works with all of them. The sigmas used are an output. - **runs**: samples seed, seed+1 ... in one node and returns one batch. -- **Advanced**: `start_at_step`, `end_at_step`, `add_noise`, `use_negative`. Distilled models run at cfg 1, where the negative prompt is ignored; `use_negative` raises a cfg of 1 to 1.5 for the guider so the negative pass runs. Off by default, because it can soften or over-bake a turbo result; raise cfg for more effect. +- **Advanced**: `start_at_step`, `end_at_step`, `add_noise`. - **Recommended settings**: press `Use recommended settings` with a Loader's PIPE connected to fill sampler, scheduler, steps and cfg (nothing changes when you only connect the pipe). They come from the model's sidecar (the most common sampler, scheduler, steps and cfg of the author's example images; prompts are never read), then name hints (turbo / distilled -> 8 steps, cfg 1), then family defaults from community documentation (Krea 2: res_multistep/simple; MiniMax H3: euler/simple; LTX: the LTXV schedule). These are starting points, not official values. - **Step cache**: `step_cache` (off / auto / easycache / lazycache) wraps ComfyUI's own EasyCache and LazyCache, which skip model calls on steps where the output barely changes and work across architectures (including audio+video models). `auto` turns on easycache from 15 steps up; distilled 4-8 step runs should stay off. `cache_threshold` (higher = faster, softer), `cache_start` and `cache_end` set when it may skip. Measured on Anima, 30 steps, RTX 5080: easycache 0.1 skipped 15/30 steps (1.8x, similarity to uncached 0.991), 0.2 skipped 17 (2.05x, 0.978), 0.4 skipped 19 (2.4x, 0.971); lazycache 0.2 gave 2.05x at 0.988. Results vary by model. - **Presets**: save, apply and delete named presets (steps, cfg, sampler, scheduler, denoise, runs, step cache). They are stored in ComfyUI's user folder under `promptforge/sampler_presets.json`. Connect the Loader's PIPE to the sampler and the preset list shows only presets for that model family and model, plus generic presets; `show all presets` reveals the rest. A saved preset records the connected family and model, so it keeps matching that model and does not clutter other models. @@ -140,10 +140,10 @@ 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. +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 lets the sampler raise cfg 1 to 1.5 for the negative pass. ### 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. +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 raise a cfg of 1 to 1.5 so the negative pass runs. Leave it off on turbo/distilled models unless the negative matters, then raise cfg for more effect. #### 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 786b71d..5fcca96 100644 --- a/moss_nodes.py +++ b/moss_nodes.py @@ -1,5 +1,6 @@ """PromptForge wrappers for the core nodes the image workflows need.""" import nodes as comfy_nodes +import node_helpers from comfy_extras.nodes_slg import SkipLayerGuidanceDiTSimple from comfy_extras.nodes_post_processing import ImageScaleToTotalPixels from comfy_extras.nodes_moss_image import TextEncodeMOSSImageEdit @@ -19,12 +20,17 @@ 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): + def encode(self, clip, text, negative, use_negative=False): encoder = comfy_nodes.CLIPTextEncode() - return encoder.encode(clip, text)[0], encoder.encode(clip, negative)[0] + 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}) class PROMPTFORGE_MossEditEncode: @@ -50,16 +56,19 @@ 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, vae=None): + image_strength=1.0, negative_image_strength=1.0, use_negative=False, 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 - negative_out = TextEncodeMOSSImageEdit.execute(clip, negative, edit_format, images, vae=vae, image_strength=negative_image_strength).result - return positive[0], negative_out[0] + 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}) class PROMPTFORGE_KreaEditEncode: @@ -77,6 +86,7 @@ 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}), @@ -84,12 +94,14 @@ class PROMPTFORGE_KreaEditEncode: }, } - def encode(self, clip, prompt, negative, image, grounding_px=0, system_prompt=""): + def encode(self, clip, prompt, negative, image, use_negative=False, 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) + 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) - return positive[0], negative_out[0] + 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}) class PROMPTFORGE_SkipLayerGuidance: diff --git a/pyproject.toml b/pyproject.toml index bc328e2..4fe39bd 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.13" +version = "1.28.14" license = { file = "LICENSE" } dependencies = [ "transformers", diff --git a/sampler.py b/sampler.py index ad76e50..4fb89db 100644 --- a/sampler.py +++ b/sampler.py @@ -28,7 +28,7 @@ from . import families, sampler_defaults AUTO = "auto" LTXV = "ltxv" -PRESET_FIELDS = ("steps", "cfg", "sampler_name", "scheduler", "denoise", "runs", "step_cache", "cache_threshold", "use_negative") +PRESET_FIELDS = ("steps", "cfg", "sampler_name", "scheduler", "denoise", "runs", "step_cache", "cache_threshold") # step caching skips whole model calls, which only pays off (and only stays clean) with enough steps AUTO_CACHE_MIN_STEPS = 15 CACHES = {"easycache": nodes_easycache.EasyCacheNode, "lazycache": nodes_easycache.LazyCacheNode} @@ -125,7 +125,6 @@ class PROMPTFORGE_Sampler: "ltxv_base_shift": ("FLOAT", {"default": 0.95, "min": 0.0, "max": 100.0, "step": 0.01, "advanced": True}), "ltxv_terminal": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 0.99, "step": 0.01, "advanced": True}), "preview": (["default", "taesd", "latent2rgb", "none"], {"default": "default", "tooltip": "Live preview while sampling. default follows ComfyUI's --preview-method; taesd uses the model's tiny decoder when it is in models/vae_approx (taeh3 for MiniMax H3) and latent2rgb otherwise."}), - "use_negative": ("BOOLEAN", {"default": False, "advanced": True, "tooltip": "Use the negative prompt on distilled models. A cfg of 1 or less is raised to 1.5 for the guider, so the negative pass runs; raise cfg further for a stronger effect. Off keeps the fast cfg-1 path."}), }, } @@ -157,10 +156,9 @@ class PROMPTFORGE_Sampler: def sample(self, model, positive, negative, latent_image, seed, steps, cfg, sampler_name, scheduler, denoise, runs, 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, ltxv_max_shift=2.05, ltxv_base_shift=0.95, ltxv_terminal=0.1, preview="default", - use_negative=False): + start_at_step=0, end_at_step=10000, add_noise=True, ltxv_max_shift=2.05, ltxv_base_shift=0.95, ltxv_terminal=0.1, preview="default"): family = self._family(model, pipe) - if use_negative and cfg <= 1.0: + if cfg <= 1.0 and any(len(cond) > 1 and cond[1].get("promptforge_use_negative") for cond in negative): cfg = 1.5 if step_cache == "auto": step_cache = "easycache" if steps >= AUTO_CACHE_MIN_STEPS else "off" diff --git a/web/promptforge_sampler.js b/web/promptforge_sampler.js index e43b9a1..06d9019 100644 --- a/web/promptforge_sampler.js +++ b/web/promptforge_sampler.js @@ -2,7 +2,7 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; const NODE = "PROMPTFORGE_Sampler"; -const PRESET_FIELDS = ["steps", "cfg", "sampler_name", "scheduler", "denoise", "runs", "step_cache", "cache_threshold", "use_negative"]; +const PRESET_FIELDS = ["steps", "cfg", "sampler_name", "scheduler", "denoise", "runs", "step_cache", "cache_threshold"]; const NO_PRESET = "(no preset)"; app.registerExtension({