diff --git a/README.md b/README.md index bc45daf..6830bac 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,10 @@ Available V4+ models include: - `nai-diffusion-4-full` - `nai-diffusion-4-5-curated` - `nai-diffusion-4-5-full` +- `nai-diffusion-5-curated` +- `nai-diffusion-5-full` + +**Note:** NovelAI Diffusion V5 launched on 2026-08-21. The `nai-diffusion-5-*` identifiers above follow NovelAI's established naming convention but have not been independently confirmed against the live API yet — if generation fails with an unrecognized model error, please open an issue so the identifier can be corrected. ![ModelOption](https://github.com/bedovyy/ComfyUI_NAIDGenerator/assets/137917911/0b484edb-bcb5-428a-b2af-1372a9d7a34f) @@ -130,20 +134,20 @@ You can find director tools like `LineArtNAID`, `EmotionNAID`, and `RemoveBGNAID ![augment_example](https://github.com/user-attachments/assets/5833e9fb-f92e-4d53-9069-58ca8503a3e7) -### V4 / V4.5 Support +### V4 / V4.5 / V5 Support -The nodes fully support NAI's V4 and V4.5 model architecture. +The nodes fully support NAI's V4, V4.5 and V5 model architecture. #### V4 Prompt Handling -Two new nodes have been added in `NovelAI/v4` for V4/V4.5 prompt handling: +Two new nodes have been added in `NovelAI/v4` for V4/V4.5/V5 prompt handling: - **`V4BasePrompt`**: Handles the positive prompt. - **`V4NegativePrompt`**: Handles the negative prompt. -#### Example V4 / V4.5 Workflow +#### Example V4 / V4.5 / V5 Workflow -Here's a basic setup for a V4/V4.5 model: +Here's a basic setup for a V4/V4.5/V5 model: ``` V4BasePrompt -----> positive @@ -151,4 +155,14 @@ V4BasePrompt -----> positive V4NegativePrompt -> negative ``` -**Note:** Basic `img2img`, `vibe transfer` and `inpainting` functionality works with V4/V4.5. +**Note:** Basic `img2img`, `vibe transfer` and `inpainting` functionality works with V4/V4.5/V5. + +### Character Prompt (Multi-Character Text Prompting) + +Connect one or more `CharacterPromptOptionNAID` nodes to the `option` input of `GenerateNAID` to give individual characters their own positive/negative captions, mirroring NAI's multi-character prompting feature (V4+). Chain multiple nodes together, one per character, the same way you would chain `VibeTransferOptionNAID` nodes. + +- **positive / negative:** Per-character captions, merged into the request's `char_captions`. +- **use_position / x / y:** Enable to pin the character to a specific normalized position (`0.0`-`1.0`) instead of letting the model place it automatically. If any chained character enables this, positioning is used for the whole group. +- **use_order:** Treat the connection order of the chained character nodes as a priority hint for overlapping characters. If any chained character enables this, it applies to the whole group. + +**Note:** This is independent of `CharacterReferenceOptionNAID`, which guides generation from a reference *image* rather than text. diff --git a/nodes.py b/nodes.py index cec25de..766d56d 100644 --- a/nodes.py +++ b/nodes.py @@ -130,7 +130,9 @@ class ModelOption: "nai-diffusion-4-curated-preview", "nai-diffusion-4-full", "nai-diffusion-4-5-curated", - "nai-diffusion-4-5-full" + "nai-diffusion-4-5-full", + "nai-diffusion-5-curated", + "nai-diffusion-5-full" ], { "default": "nai-diffusion-4-5-full" }), }, "optional": { "option": ("NAID_OPTION",) }, @@ -254,6 +256,38 @@ class CharacterReferenceOption: } return (option,) +# ------------------------------------------------- +# Character Prompt (Multi-Character Text Prompting) +# ------------------------------------------------- + +class CharacterPromptOption: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "positive": ("STRING", { "multiline": True, "dynamicPrompts": False, "tooltip": "This character's positive caption." }), + "negative": ("STRING", { "default": "", "multiline": True, "dynamicPrompts": False, "tooltip": "This character's negative caption." }), + "use_position": ("BOOLEAN", { "default": False, "tooltip": "Pin this character to x/y instead of letting the model place it automatically." }), + "x": ("FLOAT", { "default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "display": "number", "tooltip": "Horizontal position (0=left, 1=right). Only used when use_position is enabled." }), + "y": ("FLOAT", { "default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "display": "number", "tooltip": "Vertical position (0=top, 1=bottom). Only used when use_position is enabled." }), + "use_order": ("BOOLEAN", { "default": False, "tooltip": "Use the connection order of character prompt nodes as a priority when characters overlap. Applies to the whole group of chained character prompts." }), + }, + "optional": { "option": ("NAID_OPTION",) }, + } + RETURN_TYPES = ("NAID_OPTION",) + FUNCTION = "set_option" + CATEGORY = "NovelAI" + def set_option(self, positive, negative, use_position, x, y, use_order, option=None): + option = copy.deepcopy(option) if option else {} + if "characters" not in option: + option["characters"] = [] + option["characters"].append({ + "positive": positive, "negative": negative, + "use_position": use_position, "x": x, "y": y, + "use_order": use_order, + }) + return (option,) + # ------------------------------------------------- # Generation Node # ------------------------------------------------- @@ -358,7 +392,7 @@ class GenerateNAID: action = "infill" image, mask, add_original_image = option["infill"] params["image"] = image_to_base64(resize_image(image, (width, height))) - params["mask"] = naimask_to_base64(resize_to_naimask(mask, (width, height), "4" in model)) + params["mask"] = naimask_to_base64(resize_to_naimask(mask, (width, height), "nai-diffusion-4" in model or "nai-diffusion-5" in model)) params["add_original_image"] = add_original_image if "vibe" in option: @@ -371,6 +405,21 @@ class GenerateNAID: if "model" in option: model = option["model"] if "v4_prompt" in option: params["v4_prompt"].update(option["v4_prompt"]) + if "characters" in option: + chars = option["characters"] + use_coords = any(c["use_position"] for c in chars) + use_order = any(c["use_order"] for c in chars) + params["v4_prompt"]["caption"]["char_captions"] = [ + {"char_caption": c["positive"], "centers": [{"x": c["x"], "y": c["y"]}]} for c in chars + ] + params["v4_prompt"]["use_coords"] = use_coords + params["v4_prompt"]["use_order"] = use_order + params["v4_negative_prompt"]["caption"]["char_captions"] = [ + {"char_caption": c["negative"], "centers": [{"x": c["x"], "y": c["y"]}]} for c in chars + ] + params["v4_negative_prompt"]["use_coords"] = use_coords + params["v4_negative_prompt"]["use_order"] = use_order + if "character_reference_single" in option: ref = option["character_reference_single"] base_caption = "character&style" if ref["style_aware"] else "character" @@ -626,6 +675,7 @@ NODE_CLASS_MAPPINGS = { "VibeTransferOptionNAID": VibeTransferOption, "NetworkOptionNAID": NetworkOption, "CharacterReferenceOptionNAID": CharacterReferenceOption, + "CharacterPromptOptionNAID": CharacterPromptOption, "AnlasTrackerNAID": AnlasTrackerNAID, # New node "MaskImageToNAID": ImageToNAIMask, "PromptToNAID": PromptToNAID, @@ -647,6 +697,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "VibeTransferOptionNAID": "VibeTransferOption ✒️🅝🅐🅘", "NetworkOptionNAID": "NetworkOption ✒️🅝🅐🅘", "CharacterReferenceOptionNAID": "Character Reference ✒️🅝🅐🅘", + "CharacterPromptOptionNAID": "Character Prompt ✒️🅝🅐🅘", "AnlasTrackerNAID": "Anlas Tracker ✒️🅝🅐🅘", # New node "MaskImageToNAID": "Convert Mask Image ✒️🅝🅐🅘", "PromptToNAID": "Convert Prompt ✒️🅝🅐🅘",