diff --git a/README.md b/README.md index 08410d3..0c2ba8e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Let me know of any major issues by submitting an Issue. I plan to do some fresh install testing when I have time. -## ComfyUI-to-Python-Extension (SaveAsScript version) +## ComfyUI-SaveAsScript This is a fork maintained by Anthony Maranto of the original [ComfyUI-To-Python-Extension](https://github.com/pydn/ComfyUI-to-Python-Extension) by Peyton DeNiro. It provides a more robust command-line interface and the ability to export your current workflow as a script directly from the ComfyUI web interface. @@ -38,15 +38,36 @@ options: Overwrite the output file if it exists ``` +### Arguments + +It is now possible to pass command-line arguments to a generated script file. Any time a **required** input variable for *any* node in your the ComfyUI workflow is left unfilled, SaveAsScript will automatically convert that node into an argument. + +For instance, if you have a simple default workflow, but have converted the text widget of the positive prompt into an input and left it unfilled like so: + +![An altered version of the default ComfyUI workflow](images/default_altered.png) + +Then the unfilled required variable will be available as an argument: +```bash +usage: default-workflow.py [-h] text1 + +A converted ComfyUI workflow. Required inputs listed below. Values passed should be in JSON + +positional arguments: + text1 Argument 0, input `text` for node "CLIP Text Encode (Prompt)" id 6 (autogenerated) + +options: + -h, --help show this help message and exit +``` + +Arguments are new. **If you have any suggestions on how to improve them or on how to effectively specify defaults in the workflow and override in the command-line**, feel free to suggest that in an Issue. + ### Other Changes -I also fixed what seemed to be a minor bug with exporting certain Crystools nodes, possibly due to their unusual name. +#### Bugfixes +- Windows paths are now properly escaped. +- I also fixed what seemed to be a minor bug with exporting certain Crystools nodes, possibly due to their unusual name. -### Improvement TODO - -Disable export of invalid nodes (ones for which required args are not satisfied) and log a warning to the console. - -## Description of ComfyUI-to-Python-Extension (usage altered) +## Old Description of ComfyUI-to-Python-Extension (usage altered) The `ComfyUI-to-Python-Extension` is a powerful tool that translates [ComfyUI](https://github.com/comfyanonymous/ComfyUI) workflows into executable Python code. Designed to bridge the gap between ComfyUI's visual interface and Python's programming environment, this script facilitates the seamless transition from design to code execution. Whether you're a data scientist, a software developer, or an AI enthusiast, this tool streamlines the process of implementing ComfyUI workflows in Python. diff --git a/comfyui_to_python.py b/comfyui_to_python.py index 9ea6487..651345d 100644 --- a/comfyui_to_python.py +++ b/comfyui_to_python.py @@ -11,7 +11,7 @@ from typing import Dict, List, Any, Callable, Tuple, TextIO import black -from comfyui_to_python_utils import import_custom_nodes, find_path, add_comfyui_directory_to_sys_path, add_extra_model_paths, get_value_at_index +from comfyui_to_python_utils import import_custom_nodes, find_path, add_comfyui_directory_to_sys_path, add_extra_model_paths, get_value_at_index, parse_arg add_comfyui_directory_to_sys_path() from nodes import NODE_CLASS_MAPPINGS @@ -202,7 +202,7 @@ class CodeGenerator: arg_inputs.append((input_var, f"Argument {i}, input `{input}` for node \\\"{data['_meta'].get('title', class_type)}\\\" id {idx}")) print("WARNING: Missing required input", input, "for", class_type) print("That will be CLI arg " + str(len(arg_inputs))) - missing.append((input_var, len(arg_inputs))) + missing.append((input, input_var, len(arg_inputs))) class_def = self.node_class_mappings[class_type]() @@ -225,8 +225,8 @@ class CodeGenerator: # Remove any keyword arguments from **inputs if they are not in class_def_params inputs = {key: value for key, value in inputs.items() if key in class_def_params} - for input, arg in missing: - inputs[input] = {"variable_name": f"argv." + input} + for input, input_var, arg in missing: + inputs[input] = {"variable_name": f"parse_arg(args." + input_var + ")"} # Deal with hidden variables if 'unique_id' in class_def_params: inputs['unique_id'] = random.randint(1, 2**64) @@ -303,18 +303,18 @@ class CodeGenerator: """ # Get the source code of the utils functions as a string func_strings = [] - for func in [get_value_at_index, find_path, add_comfyui_directory_to_sys_path, add_extra_model_paths, import_custom_nodes]: + for func in [get_value_at_index, find_path, add_comfyui_directory_to_sys_path, add_extra_model_paths, import_custom_nodes, parse_arg]: func_strings.append(f'\n{inspect.getsource(func)}') argparse_code = "" if arg_inputs: - argparse_code = f'argv = sys.argv[:]\n\nparser = argparse.ArgumentParser(description="A converted ComfyUI workflow. Required inputs listed below. Values passed should be in JSON")\n' + argparse_code = f'\nparser = argparse.ArgumentParser(description="A converted ComfyUI workflow. Required inputs listed below. Values passed should be in JSON")\n' for i, (input_name, arg_desc) in enumerate(arg_inputs): argparse_code += f'parser.add_argument("{input_name}", help="{arg_desc} (autogenerated)")\n' argparse_code += 'args = parser.parse_args()\nsys.argv = [sys.argv[0]]\n' # Define static import statements required for the script - static_imports = ['import os', 'import random', 'import sys', 'import argparse', 'from typing import Sequence, Mapping, Any, Union', + static_imports = ['import os', 'import random', 'import sys', 'import json', 'import argparse', 'from typing import Sequence, Mapping, Any, Union', 'import torch'] + func_strings + [argparse_code] # Check if custom nodes should be included if custom_nodes: diff --git a/comfyui_to_python_utils.py b/comfyui_to_python_utils.py index e55f26e..5fa181d 100644 --- a/comfyui_to_python_utils.py +++ b/comfyui_to_python_utils.py @@ -101,3 +101,10 @@ def get_value_at_index(obj: Union[Sequence, Mapping], index: int) -> Any: return obj[index] except KeyError: return obj['result'][index] + +def parse_arg(s: str): + """ Parses a JSON string, returning it unchanged if the parsing fails. """ + try: + return json.loads(s) + except json.JSONDecodeError: + return s diff --git a/images/default_altered.png b/images/default_altered.png new file mode 100644 index 0000000..190b686 Binary files /dev/null and b/images/default_altered.png differ