import os import re import json import sys import shutil import yaml from PIL import Image import nodes import torch import folder_paths import comfy import traceback from server import PromptServer from .libs import utils prompt_builder_preset = {} resource_path = os.path.join(os.path.dirname(__file__), "..", "resources") resource_path = os.path.abspath(resource_path) prompts_path = os.path.join(os.path.dirname(__file__), "..", "prompts") prompts_path = os.path.abspath(prompts_path) try: pb_yaml_path = os.path.join(resource_path, 'prompt-builder.yaml') pb_yaml_path_example = os.path.join(resource_path, 'prompt-builder.yaml.example') if not os.path.exists(pb_yaml_path): shutil.copy(pb_yaml_path_example, pb_yaml_path) with open(pb_yaml_path, 'r', encoding="utf-8") as f: prompt_builder_preset = yaml.load(f, Loader=yaml.FullLoader) except Exception as e: print(f"[Inspire Pack] Failed to load 'prompt-builder.yaml'") class LoadPromptsFromDir: @classmethod def INPUT_TYPES(cls): global prompts_path try: prompt_dirs = [d for d in os.listdir(prompts_path) if os.path.isdir(os.path.join(prompts_path, d))] except Exception: prompt_dirs = [] return {"required": {"prompt_dir": (prompt_dirs,)}} RETURN_TYPES = ("ZIPPED_PROMPT",) OUTPUT_IS_LIST = (True,) FUNCTION = "doit" CATEGORY = "InspirePack/prompt" def doit(self, prompt_dir): global prompts_path prompt_dir = os.path.join(prompts_path, prompt_dir) files = [f for f in os.listdir(prompt_dir) if f.endswith(".txt")] files.sort() prompts = [] for file_name in files: print(f"file_name: {file_name}") try: with open(os.path.join(prompt_dir, file_name), "r", encoding="utf-8") as file: prompt_data = file.read() prompt_list = re.split(r'\n\s*-+\s*\n', prompt_data) for prompt in prompt_list: pattern = r"positive:(.*?)(?:\n*|$)negative:(.*)" matches = re.search(pattern, prompt, re.DOTALL) if matches: positive_text = matches.group(1).strip() negative_text = matches.group(2).strip() result_tuple = (positive_text, negative_text, file_name) prompts.append(result_tuple) else: print(f"[WARN] LoadPromptsFromDir: invalid prompt format in '{file_name}'") except Exception as e: print(f"[ERROR] LoadPromptsFromDir: an error occurred while processing '{file_name}': {str(e)}") return (prompts, ) class LoadPromptsFromFile: @classmethod def INPUT_TYPES(cls): global prompts_path try: prompt_files = [] for root, dirs, files in os.walk(prompts_path): for file in files: if file.endswith(".txt"): file_path = os.path.join(root, file) rel_path = os.path.relpath(file_path, prompts_path) prompt_files.append(rel_path) except Exception: prompt_files = [] return {"required": {"prompt_file": (prompt_files,)}} RETURN_TYPES = ("ZIPPED_PROMPT",) OUTPUT_IS_LIST = (True,) FUNCTION = "doit" CATEGORY = "InspirePack/prompt" def doit(self, prompt_file): prompt_path = os.path.join(prompts_path, prompt_file) prompts = [] try: with open(prompt_path, "r", encoding="utf-8") as file: prompt_data = file.read() prompt_list = re.split(r'\n\s*-+\s*\n', prompt_data) pattern = r"positive:(.*?)(?:\n*|$)negative:(.*)" for prompt in prompt_list: matches = re.search(pattern, prompt, re.DOTALL) if matches: positive_text = matches.group(1).strip() negative_text = matches.group(2).strip() result_tuple = (positive_text, negative_text, prompt_file) prompts.append(result_tuple) else: print(f"[WARN] LoadPromptsFromFile: invalid prompt format in '{prompt_file}'") except Exception as e: print(f"[ERROR] LoadPromptsFromFile: an error occurred while processing '{prompt_file}': {str(e)}") return (prompts, ) class UnzipPrompt: @classmethod def INPUT_TYPES(s): return {"required": {"zipped_prompt": ("ZIPPED_PROMPT",), }} RETURN_TYPES = ("STRING", "STRING", "STRING") RETURN_NAMES = ("positive", "negative", "name") FUNCTION = "doit" CATEGORY = "InspirePack/prompt" def doit(self, zipped_prompt): return zipped_prompt class ZipPrompt: @classmethod def INPUT_TYPES(s): return {"required": { "positive": ("STRING", {"forceInput": True, "multiline": True}), "negative": ("STRING", {"forceInput": True, "multiline": True}), }, "optional": { "name_opt": ("STRING", {"forceInput": True, "multiline": False}) } } RETURN_TYPES = ("ZIPPED_PROMPT",) FUNCTION = "doit" CATEGORY = "InspirePack/prompt" def doit(self, positive, negative, name_opt=""): return ((positive, negative, name_opt), ) prompt_blacklist = set([ 'filename_prefix' ]) class PromptExtractor: @classmethod def INPUT_TYPES(s): input_dir = folder_paths.get_input_directory() files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))] return {"required": { "image": (sorted(files), {"image_upload": True}), "positive_id": ("STRING", {}), "negative_id": ("STRING", {}), "info": ("STRING", {"multiline": True}) }, "hidden": {"unique_id": "UNIQUE_ID"}, } CATEGORY = "InspirePack/prompt" RETURN_TYPES = ("STRING", "STRING") RETURN_NAMES = ("positive", "negative") FUNCTION = "doit" OUTPUT_NODE = True def doit(self, image, positive_id, negative_id, info, unique_id): image_path = folder_paths.get_annotated_filepath(image) info = Image.open(image_path).info positive = "" negative = "" text = "" prompt_dicts = {} node_inputs = {} def get_node_inputs(x): if x in node_inputs: return node_inputs[x] else: node_inputs[x] = None obj = nodes.NODE_CLASS_MAPPINGS.get(x, None) if obj is not None: input_types = obj.INPUT_TYPES() node_inputs[x] = input_types return input_types else: return None if isinstance(info, dict) and 'workflow' in info: prompt = json.loads(info['prompt']) for k, v in prompt.items(): input_types = get_node_inputs(v['class_type']) if input_types is not None: inputs = input_types['required'].copy() if 'optional' in input_types: inputs.update(input_types['optional']) for name, value in inputs.items(): if name in prompt_blacklist: continue if value[0] == 'STRING' and name in v['inputs']: prompt_dicts[f"{k}.{name.strip()}"] = (v['class_type'], v['inputs'][name]) for k, v in prompt_dicts.items(): text += f"{k} [{v[0]}] ==> {v[1]}\n" positive = prompt_dicts.get(positive_id.strip(), "") negative = prompt_dicts.get(negative_id.strip(), "") else: text = "There is no prompt information within the image." PromptServer.instance.send_sync("inspire-node-feedback", {"node_id": unique_id, "widget_name": "info", "type": "text", "data": text}) return (positive, negative) class GlobalSeed: @classmethod def INPUT_TYPES(s): return { "required": { "value": ("INT", {"default": 0, "min": 0, "max": 1125899906842624}), "mode": ("BOOLEAN", {"default": True, "label_on": "control_before_generate", "label_off": "control_after_generate"}), "action": (["fixed", "increment", "decrement", "randomize", "increment for each node", "decrement for each node", "randomize for each node"], ), "last_seed": ("STRING", {"default": ""}), } } RETURN_TYPES = () FUNCTION = "doit" CATEGORY = "InspirePack/Prompt" OUTPUT_NODE = True def doit(self, **kwargs): return {} class BindImageListPromptList: @classmethod def INPUT_TYPES(s): return { "required": { "images": ("IMAGE",), "zipped_prompts": ("ZIPPED_PROMPT",), "default_positive": ("STRING", {"multiline": True, "placeholder": "default positive"}), "default_negative": ("STRING", {"multiline": True, "placeholder": "default negative"}), } } INPUT_IS_LIST = True RETURN_TYPES = ("IMAGE", "STRING", "STRING", "STRING") RETURN_NAMES = ("image", "positive", "negative", "prompt_label") OUTPUT_IS_LIST = (True, True, True,) FUNCTION = "doit" CATEGORY = "InspirePack/Prompt" def doit(self, images, zipped_prompts, default_positive, default_negative): positives = [] negatives = [] prompt_labels = [] if len(images) < len(zipped_prompts): zipped_prompts = zipped_prompts[:len(images)] elif len(images) > len(zipped_prompts): lack = len(images) - len(zipped_prompts) default_prompt = (default_positive[0], default_negative[0], "default") zipped_prompts = zipped_prompts[:] for i in range(lack): zipped_prompts.append(default_prompt) for prompt in zipped_prompts: a, b, c = prompt positives.append(a) negatives.append(b) prompt_labels.append(c) return (images, positives, negatives, prompt_labels) class BNK_EncoderWrapper: def __init__(self, token_normalization, weight_interpretation): self.token_normalization = token_normalization self.weight_interpretation = weight_interpretation def encode(self, clip, text): if 'BNK_CLIPTextEncodeAdvanced' not in nodes.NODE_CLASS_MAPPINGS: raise Exception(f"[ERROR] To use MediaPipeFaceMeshDetector, you need to install 'Advanced CLIP Text Encode'") return nodes.NODE_CLASS_MAPPINGS['BNK_CLIPTextEncodeAdvanced']().encode(clip, text, self.token_normalization, self.weight_interpretation) class WildcardEncodeInspire: @classmethod def INPUT_TYPES(s): return {"required": { "model": ("MODEL",), "clip": ("CLIP",), "token_normalization": (["none", "mean", "length", "length+mean"], ), "weight_interpretation": (["comfy", "A1111", "compel", "comfy++", "down_weight"], {'default': 'comfy++'}), "wildcard_text": ("STRING", {"multiline": True, "dynamicPrompts": False, 'placeholder': 'Wildcard Prmopt (User Input)'}), "populated_text": ("STRING", {"multiline": True, "dynamicPrompts": False, 'placeholder': 'Populated Prmopt (Will be generated automatically)'}), "mode": ("BOOLEAN", {"default": True, "label_on": "Populate", "label_off": "Fixed"}), "Select to add LoRA": (["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras"), ), "Select to add Wildcard": (["Select the Wildcard to add to the text"],), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), }, } CATEGORY = "InspirePack/Prompt" RETURN_TYPES = ("MODEL", "CLIP", "CONDITIONING", "STRING") RETURN_NAMES = ("model", "clip", "conditioning", "populated_text") FUNCTION = "doit" def doit(self, *args, **kwargs): populated = kwargs['populated_text'] clip_encoder = BNK_EncoderWrapper(kwargs['token_normalization'], kwargs['weight_interpretation']) if 'ImpactWildcardEncode' not in nodes.NODE_CLASS_MAPPINGS: raise Exception(f"[ERROR] To use WildcardEncodeInspire, you need to install 'Impact Pack'") model, clip, conditioning = nodes.NODE_CLASS_MAPPINGS['ImpactWildcardEncode'].process_with_loras(wildcard_opt=populated, model=kwargs['model'], clip=kwargs['clip'], clip_encoder=clip_encoder) return (model, clip, conditioning, populated) class PromptBuilder: @classmethod def INPUT_TYPES(s): global prompt_builder_preset presets = ["#PRESET"] return {"required": { "category": (list(prompt_builder_preset.keys()), ), "preset": (presets, ), "text": ("STRING", {"multiline": True}), }, } RETURN_TYPES = ("STRING", ) FUNCTION = "doit" CATEGORY = "InspirePack/Prompt" def doit(self, category, preset, text): return (text,) class SeedExplorer: @classmethod def INPUT_TYPES(s): return { "required": { "latent": ("LATENT",), "seed_prompt": ("STRING", {"multiline": True, "dynamicPrompts": False, "pysssss.autocomplete": False}), "enable_additional": ("BOOLEAN", {"default": True, "label_on": "true", "label_off": "false"}), "additional_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), "additional_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), "noise_mode": (["GPU(=A1111)", "CPU"],), "initial_batch_seed_mode": (["incremental", "comfy"],), } } RETURN_TYPES = ("NOISE",) FUNCTION = "doit" CATEGORY = "InspirePack/Prompt" @staticmethod def apply_variation(start_noise, seed_items, noise_device, mask=None): noise = start_noise for x in seed_items: if isinstance(x, str): item = x.split(':') else: item = x if len(item) == 2: try: variation_seed = int(item[0]) variation_strength = float(item[1]) noise = utils.apply_variation_noise(noise, noise_device, variation_seed, variation_strength, mask=mask) except Exception: print(f"[ERROR] IGNORED: SeedExplorer failed to processing '{x}'") traceback.print_exc() return noise def doit(self, latent, seed_prompt, enable_additional, additional_seed, additional_strength, noise_mode, initial_batch_seed_mode): latent_image = latent["samples"] device = comfy.model_management.get_torch_device() noise_device = "cpu" if noise_mode == "CPU" else device seed_prompt = seed_prompt.replace("\n", "") items = seed_prompt.strip().split(",") if items == ['']: items = [] if enable_additional: items.append((additional_seed, additional_strength)) try: hd = items[0] tl = items[1:] if isinstance(hd, tuple): hd_seed = int(hd[0]) else: hd_seed = int(hd) noise = utils.prepare_noise(latent_image, hd_seed, None, noise_device, initial_batch_seed_mode) noise = noise.to(device) noise = SeedExplorer.apply_variation(noise, tl, noise_device) noise = noise.cpu() return (noise,) except Exception: print(f"[ERROR] IGNORED: SeedExplorer failed") traceback.print_exc() noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device=noise_device) return (noise,) list_counter_map = {} class ListCounter: @classmethod def INPUT_TYPES(s): return {"required": { "signal": (utils.any_typ,), "base_value": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), }, "hidden": {"unique_id": "UNIQUE_ID"}, } RETURN_TYPES = ("INT",) FUNCTION = "doit" CATEGORY = "InspirePack/Util" def doit(self, signal, base_value, unique_id): if unique_id not in list_counter_map: count = 0 else: count = list_counter_map[unique_id] list_counter_map[unique_id] = count + 1 return (count + base_value, ) NODE_CLASS_MAPPINGS = { "LoadPromptsFromDir //Inspire": LoadPromptsFromDir, "LoadPromptsFromFile //Inspire": LoadPromptsFromFile, "UnzipPrompt //Inspire": UnzipPrompt, "ZipPrompt //Inspire": ZipPrompt, "PromptExtractor //Inspire": PromptExtractor, "GlobalSeed //Inspire": GlobalSeed, "BindImageListPromptList //Inspire": BindImageListPromptList, "WildcardEncode //Inspire": WildcardEncodeInspire, "PromptBuilder //Inspire": PromptBuilder, "SeedExplorer //Inspire": SeedExplorer, "ListCounter //Inspire": ListCounter, } NODE_DISPLAY_NAME_MAPPINGS = { "LoadPromptsFromDir //Inspire": "Load Prompts From Dir (Inspire)", "LoadPromptsFromFile //Inspire": "Load Prompts From File (Inspire)", "UnzipPrompt //Inspire": "Unzip Prompt (Inspire)", "ZipPrompt //Inspire": "Zip Prompt (Inspire)", "PromptExtractor //Inspire": "Prompt Extractor (Inspire)", "GlobalSeed //Inspire": "Global Seed (Inspire)", "BindImageListPromptList //Inspire": "Bind [ImageList, PromptList] (Inspire)", "WildcardEncode //Inspire": "Wildcard Encode (Inspire)", "PromptBuilder //Inspire": "Prompt Builder (Inspire)", "SeedExplorer //Inspire": "Seed Explorer (Inspire)", "ListCounter //Inspire": "List Counter (Inspire)" }