diff --git a/Example_Workflows/How_to_use_RandomizedUtilityNodes.png b/Example_Workflows/How_to_use_RandomizedUtilityNodes.png new file mode 100644 index 0000000..ec91a74 Binary files /dev/null and b/Example_Workflows/How_to_use_RandomizedUtilityNodes.png differ diff --git a/Example_Workflows/How_to_use_addParameters.png.png b/Example_Workflows/How_to_use_addParameters.png.png new file mode 100644 index 0000000..408e3e0 Binary files /dev/null and b/Example_Workflows/How_to_use_addParameters.png.png differ diff --git a/UtilNodes.py b/UtilNodes.py index fa9be59..beac111 100644 --- a/UtilNodes.py +++ b/UtilNodes.py @@ -1,5 +1,6 @@ from .mng_json import json_manager, helpSgltn, TroubleSgltn +import random class Tagger: def __init__(self)-> None: @@ -160,6 +161,96 @@ class Tagger: return(output, _help, self.trbl.get_troubles()) +class randomOut: + + def __init__(self)-> None: + self.trbl = TroubleSgltn() + self.j_mngr = json_manager() + self.help_data = helpSgltn() + + @classmethod + def INPUT_TYPES(cls): + + return { + "required": { + "Text": ("STRING", {"multiline": True, "forceInput": True}), + "randomized_outputs": (['1','2','3','4','5'], {"default": "2"}), + "seed": ("INT", {"default": 9, "min": 0, "max": 0xffffffffffffffff}) + }, + "hidden": { + "unique_id": "UNIQUE_ID", + } + } + + RETURN_TYPES = ("STRING","STRING","STRING","STRING","STRING") + RETURN_NAMES = ("Text_1", "Text_2","Text_3","Text_4","Text_5") + + FUNCTION = "gogo" + + OUTPUT_NODE = False + + CATEGORY = "Plush/Utils" + + def gogo(self, Text, unique_id, seed, randomized_outputs=0): + random.seed(seed) + rnd_out = int(randomized_outputs) + int_rnd = random.randint(1, rnd_out) + outputs = ["","","","",""] + if 1 <= int_rnd <= 5: + outputs[int_rnd-1] = Text + out_tuple = tuple(outputs[:5]) + + return out_tuple + + +class mixer: + + def __init__(self)-> None: + self.trbl = TroubleSgltn() + self.j_mngr = json_manager() + self.help_data = helpSgltn() + + @classmethod + def INPUT_TYPES(cls): + + return { + "optional": { + "Text_1": ("STRING", {"multiline": True, "forceInput": True}), + "Text_2": ("STRING", {"multiline": True, "forceInput": True}), + "Text_3": ("STRING", {"multiline": True, "forceInput": True}), + "Text_4": ("STRING", {"multiline": True, "forceInput": True}), + "Text_5": ("STRING", {"multiline": True, "forceInput": True}), + + "seed": ("INT", {"default": 9, "min": 0, "max": 0xffffffffffffffff}) + }, + "hidden": { + "unique_id": "UNIQUE_ID", + } + } + + RETURN_TYPES = ("STRING","STRING","STRING","STRING","STRING") + RETURN_NAMES = ("Output_1", "Output_2","Output_3","Output_4","Output_5") + + FUNCTION = "gogo" + + OUTPUT_NODE = False + + CATEGORY = "Plush/Utils" + + def gogo(self, unique_id, seed, Text_1:str="", Text_2:str="", Text_3:str="", Text_4:str="", Text_5:str=""): + + valid_input = [text for text in [Text_1, Text_2, Text_3, Text_4, Text_5] if text] #Fill the list only with actual input data + random.seed(seed) + random.shuffle(valid_input) #randomize list order + + padding = 5 - len(valid_input) + valid_input.extend([""] * padding) #pad the outputs with no valid input data with empty strings + out_tuple = tuple(valid_input[:5]) + + return out_tuple + + + class mulTextSwitch: @@ -361,8 +452,9 @@ NODE_CLASS_MAPPINGS = { "mulTextSwitch": mulTextSwitch, "ImgTextSwitch": ImgTextSwitch, "Tagger": Tagger, - "ShowInfo_md": ShowInfo_md, - "ParseJSON": jsonParse + "ParseJSON": jsonParse, + "Random Output": randomOut, + "Random Mixer": mixer } @@ -371,7 +463,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "mulTextSwitch": "MultiLine Text Switch", "ImgTextSwitch": "Image & Text Switch", "Tagger": "Tagger", -"ShowInfo_md": "Display Markdown Text", -"ParseJSON": "Extract JSON data" +"ParseJSON": "Extract JSON data", +"Random Output": "Random Output", +"Random Mixer": "Random Mixer" } diff --git a/__init__.py b/__init__.py index 42079db..e792067 100644 --- a/__init__.py +++ b/__init__.py @@ -17,7 +17,7 @@ if jmanager.on_startup(False): else: jmanager.log_events("config.json was not updated") -__version__ ="1.21.18" +__version__ ="1.21.18.1" print('Plush - Version:', __version__) diff --git a/help.json b/help.json index 29924e8..842fd51 100644 --- a/help.json +++ b/help.json @@ -5,5 +5,6 @@ "adv_prompt_help": "• Advanced Prompt Enhancer (APE) uses AI Models to generate text output from any combination of: Instruction, Example_or_Context, Image and Prompt you provide. No API key is needed for Open source Models. This node can use various remote models, ChatGPT, Groq and Anthropic Claude if you have an API key and have stored it in an environment variable (see ReadMe file). With or without a key it can also connect to various local apps and models e.g.: LM Studio, Oobabooga, Koboldcpp, etc.\n\n• image input: Advanced Prompt Enhancer can send image data (in the form of a b64 image file) to AI vision capable models. If you're sending an image to an AI model be sure both the model and the app or remote service have vision capabilities and can handle image files.\n\n• Examples_or_Context: APE can send example(s) and/or context along with your instructions to the LLM. Examples and Context *always* need to be in the form of: User input, then the delimiter, followed by the model's response. Delimited text entered in this field will automatically create alternatating input to the model for each delimited segment using this pattern. If you want to explicitly tag your text as being user or model input you can preface each delimited segment with {{user}} or {{model}}. (There's an workflow file: 'How_To_Use_Examples.png' in the 'Example_Worflows' folder with details about using the Examples_or_Context input.)\n\n• API Keys: API keys need to be kept in environment variables. The Environment Variable names that Advanced Prompt Enhancer looks for are: ✦ChatGPT: OPENAI_API_KEY or OAI_KEY; ✦Groq: GROQ_API_KEY; ✦Anthropic: ANTHROPIC_API_KEY. Find instructions on how to create the Enviroment Variable here: https://github.com/glibsonoran/Plush-for-ComfyUI?tab=readme-ov-file#requirements . \n\n**************\n\n• AI_service: This indicates the type of AI service and connection you're going to send your data to. If you're using a local AI app you'll need to provide a valid URL in the LLM_URL field near the bottom of the node. If you're using 'Oobabooga API' make sure you read the LLM_URL help below. 'OpenAI compatible http POST' uses a web POST action rather than the OpenAI API Object to communicate with the local or remote AI server, typically this requires a 'v1/chat/completions' path in the URL. 'http POST Simplified Data' also uses a web POST action and presents a simplified data structure. Try this if the other AI service methods don't work, it will also require a: 'v1/chat/completions' path. \n\n• GPTmodel: This field only applies when the LLM field is set to 'ChatGPT'. Select the specific OpenAI ChatGPT model you want to use. If you're inputting an image, make sure the model you choose is vision capable.\n\n• Groq_model: This only applies when you select 'Groq' in the AI_service field. Choose the Groq model you want to use. \n\n• Anthropic_model: This only applies when you select 'Anthropic' from the AI_service field. Choose the Anthopic model you want to use. \n\n• Ollama_model: This will display the model(s) currently loaded in the Ollama front end. In order for models to show up in the drop down Ollama will have to be running with the models you intend to use loaded *before* starting ComfyUI. Note that APE looks for the standard url: http://localhost:11434/api/tags when retrieving the model names. If you've setup Ollama with another url (e.g. different port), you'll need to modify the 'urls.json' file. \n\n• Optional_model: This is a list of models extracted from the text file: '/custom_nodes/Plush-for-ComfyUI/Opt_models.txt'. This is a user configurable file that's initially empty. It's meant to hold model names for unique remote or local AI services that require a model name to be included with the inference request. These model names only apply to AI_Services that end in '(URL)'. If you enter or remove model names from this file, the changes will only show up after you reboot ComfyUI. Instructions on how to enter these model names is in the comments header of the 'Opt_models.txt' text file. \n\n• creative_latitude: (Temperature) This will set how strictly the LLM adheres to common word relationships and how closely it will follow your instruction and prompt. Setting this value higher allows more creative freedom in interpreting your input and generating its ouptput.\n\n• tokens: The maximum number of tokens that the LLM can use in processing your prompt and return text. This is not the number of tokens it 'will' use, it's the number available that it 'can' use.\n\n• seed: This is a pseudo or mock seed, it has no effect on the text generated, and it's not passed to the LLM. It's used here solely to control when the node will run. It works the same as a KSampler, set it to 'fixed' if you want the node to run only once each time you change your inputs, set it to random or increment/decrement if you want it run with each Queue.\n\n• example_delimiter: You can provide multiple examples or context to the LLM. Providing multiple examples for a given instruction is a type of 'Few Shot Prompting', which can be effective with some LLM's. This field indicates how the node will distinguish each separate example, each separate example or context item will be presented as originating from the User then the Model alternating in that order for as many as you enter. You can choose to separate your examples with a pipe '|' character, two newlines (i.e.: carriage returns) or two colons '::', these are called delimiters and they denote where these separations will occur.\n\n• LLM_URL: When using an LLM other than ChatGPT, Anthropic or Groq you'll need to provide a URL in this field. Typically the AI application you're using (e.g. LM Studio, Oobabooga), will indicate the URL to use after you startup its server. It may be in the terminal output or in the UI. Some local AI apps will specify that a particular URL is OpenAI compatible, if so this is the one you want to use. Typically the URLs for local apps have this general format: http://localhost:5001/v1 where '5001' is the port and 'localhost' is interchangable with '127.0.0.1'. If you're using the Oobabooga API or 'OpenAI compatible http POST' selection your url will need to have /chat/completions appended as part of the url: http://127.0.0.1:5000/v1/chat/completions\n\n**************\n\n• Use the troubleshooting output if you have issues with model connections, or if you want to see exactly which model was used to produce your output (some ChatGPT model names are actually only pointers to the latest specific model in that category) and how many tokens were used.", "tagger_help": "• Tagger adds tags to the beginning, middle or end of a text block. Tagger can be used whenever you want to add text that needs to appear exactly as written. \n\n**************\n\n• Beginning_tags: The text (tags) you want to appear at the very beginning of the input text block. It will preface all other text in the block. \n\n• Middle_tags: The text (tags) you want to appear in the middle of the text block. These tags will always appear immediately after a comma or period. \n\n• Prefer_middle_tag_after_period: You can indicate a preference for the tags to follow a period by clicking this button. Otherwise the tags may follow a period or a comma whichever is closest to the middle of the text. \n\n• End_tags: Tags that will be appended to the end of the input text block.\n\n• Examples: Beginning_tags: '[An Abstract Painting:| Digital Art:]', Middle_tags: '(Big Black Hat:1.4)', End_tags: 'In the style of Piet Mondrian' ", "add_param_help": "• BE AWARE THAT CERTAIN PARAMETERS MAY NOT WORK WITH ALL MODELS OR SERVICES. You should display Advanced Prompt Enhancer's 'Troubleshooting' output when testing parameters on a model so you can quickly diagnose issues. Additional Parameters allows you to add parameters to your LLM completions request using Advanced Prompt Enhancer (APE). These parameters affect the way the LLM handles your input data. You're probably already familiar with 'temperature' (which is shown as 'creative_latitude' in APE), this node allows you to add other parameters that aren't available in the APE user interface. If you want to see an example of how this node is used I have an example workflow in '/custom_nodes/Plush-For-ComfyUI/Example_Workflows/How_to_use_additionalParameters.png'. You can find a list of parameters for OpenAI models at this address: https://platform.openai.com/docs/api-reference/chat \n***************\n\n• The 'Add_Parameter(s)' input and output: These handle LIST data and will only connect to other nodes that can handle LIST data. The 'Add_Parameter' input on APE is compatible with these input/outputs. Plus 'Additional Parameter' nodes can be daisy chained, the result being that you can provide several different parameters at once.\n**************** \n\n• Parameter type: This dropdown list contains three elements: 'none', 'OpenAI JSON Format' and 'User Defined'. 'User Defined' is the only mode that will process the other fields in the node. If you select 'User Defined' you'll need to fill out the additional fields below. If you select 'none' then that particular node will provide no output, even if the Param_Name and Param_Value fields are filled out. If a node is set to none in a set of Daisy chained nodes, it will just pass the information from the other nodes along without contributing anything. 'OpenAI JSON Format' is a special format that tells the LLM to create its output as a JSON. Along with selecting this you must mention 'JSON' in the prompt and the prompt must instruct the node as to the structure of the JSON. This parameter only works with OpenAI models ‘chatgpt-4’ and later.\n\n✦ Param_Name: This field is only accessed when Parameter-type is set to: 'User Defined'. This is the name of the parameter to be added, see the link in the beginning of this help text for a web address where you can see a list of available parameters for OpenAI. \n\n✦ Param_Value: This field is only accessed when Parameter_type is set to: 'User Defined'. This is the value that will be passed to the parameter. Parameter values are passed through a data type inference algorithm to convert them to the proper data type, if you want to ensure your Param_Value will be passed as a string, enclose it in double quotes. \n\n✦ Is_JSON: This field is only accessed when Parameter_type is set to: 'User Defined'. If the parameter value needs to be in the form of a JSON, check this box. Additional Parameters will then validate the input and convert it to a JSON.", + "add_params_help": "• BE AWARE THAT CERTAIN PARAMETERS MAY NOT WORK WITH ALL MODELS OR SERVICES. You should display Advanced Prompt Enhancer's 'Troubleshooting' output when testing parameters on a model so you can quickly diagnose issues. Add Parameters allows you to add parameters to your LLM completions request using Advanced Prompt Enhancer (APE). These parameters affect the way the LLM handles your input data. You're probably already familiar with 'temperature' (which is shown as 'creative_latitude' in APE), this node allows you to add other parameters that aren't available in the APE user interface. If you want to see an example of how this node is used I have an example workflow in '/custom_nodes/Plush-For-ComfyUI/Example_Workflows/How_to_use_addParameters.png'. You can find a list of parameters for OpenAI models at this address: https://platform.openai.com/docs/api-reference/chat \n***************\n\n• The 'Add_Parameter(s)' output: This output provides LIST data and will only connect to other nodes that can handle LIST data. The 'Add_Parameter' input on APE is compatible with this output. \n**************** \n\n• Parameter: List your parameters in this text area using the format 'parameter name::value' e.g. 'top_p::0.9' make sure to place two colons between the parameter name and the value. Place each parameter::value pair on a separate line. You don't need commas or semicolons between lines, just a newline. You can add comments in this text area by prefacing each comment line with a '#' character, e.g.:'# my comment'.\n\n✦ Save_to_file: Check this box if you want to save your parameter list and comments to a text file. The file will be placed in: [...ComfyUI/output/PlushFiles]. You'll need to provide a file name also. \n\n✦ File_name: Enter the name of the file you want to save. The file name will begin with the text you provide and also have a unique identifier added. The program automatically adds the .txt extension.", "extract_json_help": "• Extract JSON lets you extract values from a string JSON that correspond to the JSON keys you enter. If there are duplicate keys in the JSON, the multiple values will be extracted in a list, e.g.: “[‘value1’, ‘value2’]”. If you want to see an example of how this node is used I have an example workflow in '/custom_nodes/Plush-For-ComfyUI/Example_Workflows/How_to_use_additionalParameters.png'. \n***************\n\n✦ The ‘json_string’ input accepts text (string) data that is properly formatted as a JSON. JSON objects or dictionaries will not work as input for this node. If you want to validate that your JSON string is properly formed I recommend using this website: https://jsonformatter.org. Only text(string) data is output from this node. If the output data is contained in a list, per the earlier example, the list will be presented as text (string). The ‘JSON_Obj’ output will not necessarily produce the same JSON that was input. Instead it is a JSON the node assembles that holds only the data associated with the keys you entered. This output is in the form of a JSON Object/dictionary, not text (string).. \n**************** \n\n✦ key_1..2..3 etc: These are the keys you want to retrieve value data from. The node won’t return the keys themselves (except in the JSON_Obj output). It will return the values that are associated with the keys. It’s like if you were accessing an employee database record and you looked up the ‘name’. ‘Name’ would be the key and the employee’s actual first and last name would be the value. The keys correspond numerically to the outputs (e.g. key_1 will output data to string_1, etc.)." } \ No newline at end of file diff --git a/mng_json.py b/mng_json.py index b034843..413df77 100644 --- a/mng_json.py +++ b/mng_json.py @@ -114,6 +114,7 @@ class helpSgltn: self._adv_prompt_help ='' self._tagger_help = "" self._add_param_help = "" + self._add_params_help = "" self._extract_json_help = "" # Empty help text is not a critical issue for the app if not help_data: @@ -127,6 +128,7 @@ class helpSgltn: self._adv_prompt_help = help_data.get('adv_prompt_help', '') self._tagger_help = help_data.get('tagger_help', '') self._add_param_help = help_data.get('add_param_help', '') + self._add_params_help = help_data.get('add_params_help', '') self._extract_json_help = help_data.get('extract_json_help', '') @property @@ -153,6 +155,10 @@ class helpSgltn: def add_param_help(self)->str: return self._add_param_help + @property + def add_params_help(self)->str: + return self._add_params_help + @property def extract_json_help (self)->str: return self._extract_json_help @@ -1306,6 +1312,67 @@ class json_manager: self.remove_keys_from_dict(item, keys_to_remove) + + def positional_str_to_dict(self, input_data: str, template_dict: Dict[str, Any], comment_char: str = None, delimiter: str = ",") -> List[Dict[str, Any]]: + """ + Parses the input data string to create a list of dictionaries based on a provided template dictionary. + + Args: + input_data (str): The raw input string with items separated by newline characters and fields within + each item separated by the specified delimiter. Each item will be parsed and mapped to the keys + in the template dictionary according to their order. + template_dict (Dict[str, Any]): A dictionary template that defines the structure for each output entry. + The order of the keys in this template dictates the assignment of parsed fields from each item + in the input data. + comment_char (str, optional): A character indicating that a line should be treated as a comment and ignored. + Any line beginning with this character will not be processed. If None, all lines are processed. + Default is None. + delimiter (str, optional): The character(s) used to separate fields within each item in input_data. + Default is ",". + + Returns: + List[Dict[str, Any]]: A list of dictionaries that match the template structure, with values populated + based on the parsed input data. + + Note: + Each field in an item (separated by the specified delimiter) + is mapped to the corresponding key in `template_dict` based on their order. Ensure that the template + dictionary and input data lines are structured consistently with each other. + """ + # Extract the keys from the template dict to map data + template_keys = list(template_dict.keys()) + + # Split input into lines + lines = input_data.split('\n') + + # Initialize the list to hold the resulting dictionaries + result_list = [] + self.log_events(f"Received input data of {len(lines)} items.", is_trouble=True) + + # Process each line + for line in lines: + # Skip empty lines or lines that begin with the comment character (if set) + line = line.strip() # First remove general whitespace, including newlines + if not line or (comment_char and line.startswith(comment_char)): + continue + line = line.strip(",.;") # Then remove specific characters as needed + + # Split line into fields by the specified delimiter + fields = line.split(delimiter) + + # Create the dictionary structure based on the template + entry = template_dict.copy() + for i, key in enumerate(template_keys): + if i < len(fields): + # Assign the parsed value to the corresponding template key + entry[key] = self.infer_type(fields[i].strip()) + + result_list.append(entry) + + return result_list + + + def insert_text_into_dict(self, input_data:Union[str, list[str]], template_dict:dict, insert_key:str, delimiter:str="|"): """ Processes input_data to create a list of dictionaries based on the template_dict. The input_data can be a diff --git a/style_prompt.py b/style_prompt.py index c42d23a..44674eb 100644 --- a/style_prompt.py +++ b/style_prompt.py @@ -640,6 +640,95 @@ class Enhancer: return (CGPT_prompt, instruction, CGPT_styleInfo, _help, self.trbl.get_troubles()) +class addParameters: + ##New## + def __init__(self): + #instantiate Configuration and Help data classes + self.cFig = cFigSingleton() + self.help_data = helpSgltn() + self.j_mngr = json_manager() + self.trbl = TroubleSgltn() + + @classmethod + def INPUT_TYPES(cls): + + return { + "required": { + "Parameters": ("STRING", {"multiline": True, "default": ""}), + "Save_to_file": ("BOOLEAN", {"default": False}), + "File_name": ("STRING", {"default": ""}) + + }, + + "hidden": { + "unique_id": "UNIQUE_ID", + } + + } + + RETURN_TYPES = ("LIST", "STRING", "STRING") + RETURN_NAMES = ("Add_Parameter","Help","Troubleshooting") + + FUNCTION = "gogo" + + OUTPUT_NODE = False + + CATEGORY = "Plush/Prompt" + + def gogo(self, Parameters, Save_to_file, File_name:bool, unique_id=None): + + self.trbl.reset(f"Add Parameters, Node: {unique_id}") + _help = self.help_data.add_params_help + param_list = [] + + #Create path and dir for saved .txt files + write_dir = '' + comfy_dir = self.j_mngr.comfy_dir + if comfy_dir: + output_dir = self.j_mngr.find_child_directory(comfy_dir,'output') + if output_dir: + write_dir = self.j_mngr.find_child_directory(output_dir, 'PlushFiles',True) #Setting argument to True means dir will be created if not present + if not write_dir: + self.j_mngr.log_events('Unable to find or create PlushFiles directory. Unable to write files', + TroubleSgltn.Severity.WARNING, + True) + else: + self.j_mngr.log_events('Unable to find output directory, Unable to write files', + TroubleSgltn.Severity.WARNING, + True) + else: + self.j_mngr.log_events('Unable to find ComfyUI directory. Unable to write files.', + TroubleSgltn.Severity.WARNING, + True) + + if Save_to_file and write_dir: + + working_file_name = self.j_mngr.generate_unique_filename("txt", File_name + '_param_') + working_file_path = self.j_mngr.append_filename_to_path(write_dir,working_file_name) + self.j_mngr.write_string_to_file(Parameters,working_file_path) + self.j_mngr.log_events(f"Parameter file: '{working_file_name}' successfully written to directory [{write_dir}]", + is_trouble=True) + + + if Parameters: + template_dict = {"param": None, "value": None} + param_list = self.j_mngr.positional_str_to_dict(Parameters,template_dict,"#","::") #Parses the Parameters data and puts the result in a list of dicts + if not param_list: + self.j_mngr.log_events("Parameters not processed", + TroubleSgltn.Severity.ERROR, + True) + return(param_list,_help,self.trbl.get_troubles()) + else: + self.j_mngr.log_events("No paramter data provided.", + TroubleSgltn.Severity.INFO, + True) + return(param_list,_help,self.trbl.get_troubles()) + + self.j_mngr.log_events(f"Parameters added: {str(param_list)}", + is_trouble=True) + return(param_list,_help,self.trbl.get_troubles()) + + class addParams: def __init__(self): #instantiate Configuration and Help data classes @@ -1455,7 +1544,8 @@ NODE_CLASS_MAPPINGS = { "AdvPromptEnhancer": AdvPromptEnhancer, "DalleImage": DalleImage, "Plush-Exif Wrangler" :ImageInfoExtractor, - "Additional Parameter" :addParams + "Additional Parameter" :addParams, + "Add Parameters": addParameters } # A dictionary that contains the friendly/humanly readable titles for the nodes @@ -1465,6 +1555,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "AdvPromptEnhancer": "Advanced Prompt Enhancer", "DalleImage": "OAI Dall_e Image", "ImageInfoExtractor": "Exif Wrangler", - "Additional Parameter": "Additional Parameter" + "Additional Parameter": "Additional Parameter", + "Add Parameters": "Add Parameters" }