Skip to content

Commit 4f689e1

Browse files
authored
Merge pull request #75 from pydn/dev
Add UI button to save script directly form ComfyUI web app
2 parents d0c9b00 + fa57d09 commit 4f689e1

8 files changed

+231
-80
lines changed

README.md

+38-12
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,27 @@ if __name__ == "__main__":
116116
- Creating large queues for image generation (For example, you could adjust the script to generate 1000 images without clicking ctrl+enter 1000 times)
117117
- Easily expanding or iterating on your architecture in Python once a foundational workflow is in place in the GUI
118118

119+
## V1.3.0 Release Notes
120+
- Generate .py file directly from the ComfyUI Web App
121+
122+
![Save As Script](images/save_as_script.png)
123+
124+
## V1.2.1 Release Notes
125+
- Dynamically change `comfyui_to_python.py` parameters with CLI arguments
126+
- Hotfix to handle nodes that accept kwargs.
127+
128+
## V1.2.0 Release Notes
129+
- Updates to adhere to latest changes from `ComfyUI`
130+
119131
## V1.0.0 Release Notes
120132
- **Use all the custom nodes!**
121133
- Custom nodes are now supported. If you run into any issues with code execution, first ensure that the each node works as expected in the GUI. If it works in the GUI, but not in the generated script, please submit an issue.
122134

123135

124-
## Usage
136+
## Installation
125137

126138

127-
1. Navigate to your `ComfyUI` directory
139+
1. Navigate to your `ComfyUI/custom_nodes` directory
128140

129141
2. Clone this repo
130142
```bash
@@ -135,8 +147,8 @@ if __name__ == "__main__":
135147
```
136148
/comfy
137149
/comfy_extras
138-
/ComfyUI-to-Python-Extension
139150
/custom_nodes
151+
--/ComfyUI-to-Python-Extension
140152
/input
141153
/models
142154
/output
@@ -157,30 +169,44 @@ if __name__ == "__main__":
157169
server.py
158170
```
159171

160-
3. Navigate to the `ComfyUI-to-Python-Extension` folder and install requirements
172+
## Web App Use
173+
1. Launch ComfyUI
174+
175+
2. Load your favorite workflow and click `Save As Script`
176+
177+
![Save As Script](images/save_as_script.png)
178+
179+
3. Type your desired file name into the pop up screen.
180+
181+
4. Move .py file from your downloads folder to your `ComfyUI` directory.
182+
183+
5. Now you can execute the newly created .py file to generate images without launching a server.
184+
185+
## CLI Usage
186+
1. Navigate to the `ComfyUI-to-Python-Extension` folder and install requirements
161187
```bash
162188
pip install -r requirements.txt
163189
```
164190

165-
4. Launch ComfyUI, click the gear icon over `Queue Prompt`, then check `Enable Dev mode Options`. **THE SCRIPT WILL NOT WORK IF YOU DO NOT ENABLE THIS OPTION!**
191+
2. Launch ComfyUI, click the gear icon over `Queue Prompt`, then check `Enable Dev mode Options`. **THE SCRIPT WILL NOT WORK IF YOU DO NOT ENABLE THIS OPTION!**
166192

167193
![Enable Dev Mode Options](images/dev_mode_options.jpg)
168194

169-
5. Load up your favorite workflows, then click the newly enabled `Save (API Format)` button under Queue Prompt
195+
3. Load up your favorite workflows, then click the newly enabled `Save (API Format)` button under Queue Prompt
170196

171-
6. Move the downloaded .json workflow file to your `ComfyUI/ComfyUI-to-Python-Extension` folder
197+
4. Move the downloaded .json workflow file to your `ComfyUI/ComfyUI-to-Python-Extension` folder
172198

173-
7. If needed, add arguments when executing `comfyui_to_python.py` to update the default `input_file` and `output_file` to match your .json workflow file and desired .py file name. By default, the script will look for a file called `workflow_api.json`. You can also update the `queue_size` variable to your desired number of images that you want to generate in a single script execution. By default, the scripts will generate 10 images. Run `python comfyui_to_python.py --help` for more details.
199+
5. If needed, add arguments when executing `comfyui_to_python.py` to update the default `input_file` and `output_file` to match your .json workflow file and desired .py file name. By default, the script will look for a file called `workflow_api.json`. You can also update the `queue_size` variable to your desired number of images that you want to generate in a single script execution. By default, the scripts will generate 10 images. Run `python comfyui_to_python.py --help` for more details.
174200

175-
8a. Run the script with default arguments:
201+
6a. Run the script with default arguments:
176202
```bash
177203
python comfyui_to_python.py
178204
```
179-
8b. Run the script with optional arguments:
205+
6b. Run the script with optional arguments:
180206
```bash
181207
python comfyui_to_python.py --input_file "workflow_api (2).json" --output_file my_workflow.py --queue_size 100
182208
```
183209

184-
9. After running `comfyui_to_python.py`, a new .py file will be created in the current working directory. If you made no changes, look for `workflow_api.py`.
210+
7. After running `comfyui_to_python.py`, a new .py file will be created in the current working directory. If you made no changes, look for `workflow_api.py`.
185211

186-
10. Now you can execute the newly created .py file to generate images without launching a server.
212+
8. Now you can execute the newly created .py file to generate images without launching a server.

__init__.py

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import sys
2+
import os
3+
4+
from io import StringIO
5+
6+
import traceback
7+
8+
from aiohttp import web
9+
10+
ext_dir = os.path.dirname(__file__)
11+
sys.path.append(ext_dir)
12+
13+
try:
14+
import black
15+
except ImportError:
16+
print("Unable to import requirements for ComfyUI-SaveAsScript.")
17+
print("Installing...")
18+
19+
import importlib
20+
21+
spec = importlib.util.spec_from_file_location(
22+
"impact_install", os.path.join(os.path.dirname(__file__), "install.py")
23+
)
24+
impact_install = importlib.util.module_from_spec(spec)
25+
spec.loader.exec_module(impact_install)
26+
27+
print("Successfully installed. Hopefully, at least.")
28+
29+
# Prevent reimporting of custom nodes
30+
os.environ["RUNNING_IN_COMFYUI"] = "TRUE"
31+
32+
from comfyui_to_python import ComfyUItoPython
33+
34+
sys.path.append(os.path.dirname(os.path.dirname(ext_dir)))
35+
36+
import server
37+
38+
WEB_DIRECTORY = "js"
39+
NODE_CLASS_MAPPINGS = {}
40+
41+
42+
@server.PromptServer.instance.routes.post("/saveasscript")
43+
async def save_as_script(request):
44+
try:
45+
data = await request.json()
46+
name = data["name"]
47+
workflow = data["workflow"]
48+
49+
sio = StringIO()
50+
ComfyUItoPython(workflow=workflow, output_file=sio)
51+
52+
sio.seek(0)
53+
data = sio.read()
54+
55+
return web.Response(text=data, status=200)
56+
except Exception as e:
57+
traceback.print_exc()
58+
return web.Response(text=str(e), status=500)

comfyui_to_python.py

+59-54
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import random
77
import sys
88
import re
9-
from typing import Dict, List, Callable, Tuple
9+
from typing import Dict, List, Any, Callable, Tuple, TextIO
1010
from argparse import ArgumentParser
1111

1212
import black
@@ -20,7 +20,7 @@
2020
get_value_at_index,
2121
)
2222

23-
sys.path.append("../")
23+
add_comfyui_directory_to_sys_path()
2424
from nodes import NODE_CLASS_MAPPINGS
2525

2626

@@ -36,7 +36,7 @@ class FileHandler:
3636
"""
3737

3838
@staticmethod
39-
def read_json_file(file_path: str) -> dict:
39+
def read_json_file(file_path: str | TextIO, encoding: str = "utf-8") -> dict:
4040
"""
4141
Reads a JSON file and returns its contents as a dictionary.
4242
@@ -51,35 +51,14 @@ def read_json_file(file_path: str) -> dict:
5151
ValueError: If the file is not a valid JSON.
5252
"""
5353

54-
try:
55-
with open(file_path, "r") as file:
56-
data = json.load(file)
57-
return data
58-
59-
except FileNotFoundError:
60-
# Get the directory from the file_path
61-
directory = os.path.dirname(file_path)
62-
63-
# If the directory is an empty string (which means file is in the current directory),
64-
# get the current working directory
65-
if not directory:
66-
directory = os.getcwd()
67-
68-
# Find all JSON files in the directory
69-
json_files = glob.glob(f"{directory}/*.json")
70-
71-
# Format the list of JSON files as a string
72-
json_files_str = "\n".join(json_files)
73-
74-
raise FileNotFoundError(
75-
f"\n\nFile not found: {file_path}. JSON files in the directory:\n{json_files_str}"
76-
)
77-
78-
except json.JSONDecodeError:
79-
raise ValueError(f"Invalid JSON format in file: {file_path}")
54+
if hasattr(file_path, "read"):
55+
return json.load(file_path)
56+
with open(file_path, "r", encoding="utf-8") as file:
57+
data = json.load(file)
58+
return data
8059

8160
@staticmethod
82-
def write_code_to_file(file_path: str, code: str) -> None:
61+
def write_code_to_file(file_path: str | TextIO, code: str) -> None:
8362
"""Write the specified code to a Python file.
8463
8564
Args:
@@ -89,16 +68,19 @@ def write_code_to_file(file_path: str, code: str) -> None:
8968
Returns:
9069
None
9170
"""
92-
# Extract directory from the filename
93-
directory = os.path.dirname(file_path)
71+
if isinstance(file_path, str):
72+
# Extract directory from the filename
73+
directory = os.path.dirname(file_path)
9474

95-
# If the directory does not exist, create it
96-
if directory and not os.path.exists(directory):
97-
os.makedirs(directory)
75+
# If the directory does not exist, create it
76+
if directory and not os.path.exists(directory):
77+
os.makedirs(directory)
9878

99-
# Save the code to a .py file
100-
with open(file_path, "w") as file:
101-
file.write(code)
79+
# Save the code to a .py file
80+
with open(file_path, "w", encoding="utf-8") as file:
81+
file.write(code)
82+
else:
83+
file_path.write(code)
10284

10385

10486
class LoadOrderDeterminer:
@@ -203,15 +185,12 @@ def __init__(self, node_class_mappings: Dict, base_node_class_mappings: Dict):
203185
def generate_workflow(
204186
self,
205187
load_order: List,
206-
filename: str = "generated_code_workflow.py",
207188
queue_size: int = 10,
208189
) -> str:
209190
"""Generate the execution code based on the load order.
210191
211192
Args:
212193
load_order (List): A list of tuples representing the load order.
213-
filename (str): The name of the Python file to which the code should be saved.
214-
Defaults to 'generated_code_workflow.py'.
215194
queue_size (int): The number of photos that will be created by the script.
216195
217196
Returns:
@@ -515,23 +494,37 @@ class ComfyUItoPython:
515494

516495
def __init__(
517496
self,
518-
input_file: str,
519-
output_file: str,
520-
queue_size: int = 10,
497+
workflow: str = "",
498+
input_file: str = "",
499+
output_file: str | TextIO = "",
500+
queue_size: int = 1,
521501
node_class_mappings: Dict = NODE_CLASS_MAPPINGS,
502+
needs_init_custom_nodes: bool = False,
522503
):
523-
"""Initialize the ComfyUItoPython class with the given parameters.
524-
504+
"""Initialize the ComfyUItoPython class with the given parameters. Exactly one of workflow or input_file must be specified.
525505
Args:
506+
workflow (str): The workflow's JSON.
526507
input_file (str): Path to the input JSON file.
527-
output_file (str): Path to the output Python file.
528-
queue_size (int): The number of times a workflow will be executed by the script. Defaults to 10.
508+
output_file (str | TextIO): Path to the output file or a file-like object.
509+
queue_size (int): The number of times a workflow will be executed by the script. Defaults to 1.
529510
node_class_mappings (Dict): Mappings of node classes. Defaults to NODE_CLASS_MAPPINGS.
511+
needs_init_custom_nodes (bool): Whether to initialize custom nodes. Defaults to False.
530512
"""
513+
if input_file and workflow:
514+
raise ValueError("Can't provide both input_file and workflow")
515+
elif not input_file and not workflow:
516+
raise ValueError("Needs input_file or workflow")
517+
518+
if not output_file:
519+
raise ValueError("Needs output_file")
520+
521+
self.workflow = workflow
531522
self.input_file = input_file
532523
self.output_file = output_file
533524
self.queue_size = queue_size
534525
self.node_class_mappings = node_class_mappings
526+
self.needs_init_custom_nodes = needs_init_custom_nodes
527+
535528
self.base_node_class_mappings = copy.deepcopy(self.node_class_mappings)
536529
self.execute()
537530

@@ -541,11 +534,18 @@ def execute(self):
541534
Returns:
542535
None
543536
"""
544-
# Step 1: Import all custom nodes
545-
import_custom_nodes()
537+
# Step 1: Import all custom nodes if we need to
538+
if self.needs_init_custom_nodes:
539+
import_custom_nodes()
540+
else:
541+
# If they're already imported, we don't know which nodes are custom nodes, so we need to import all of them
542+
self.base_node_class_mappings = {}
546543

547544
# Step 2: Read JSON data from the input file
548-
data = FileHandler.read_json_file(self.input_file)
545+
if self.input_file:
546+
data = FileHandler.read_json_file(self.input_file)
547+
else:
548+
data = json.loads(self.workflow)
549549

550550
# Step 3: Determine the load order
551551
load_order_determiner = LoadOrderDeterminer(data, self.node_class_mappings)
@@ -556,7 +556,7 @@ def execute(self):
556556
self.node_class_mappings, self.base_node_class_mappings
557557
)
558558
generated_code = code_generator.generate_workflow(
559-
load_order, filename=self.output_file, queue_size=self.queue_size
559+
load_order, queue_size=self.queue_size
560560
)
561561

562562
# Step 5: Write the generated code to a file
@@ -582,7 +582,12 @@ def run(
582582
Returns:
583583
None
584584
"""
585-
ComfyUItoPython(input_file, output_file, queue_size)
585+
ComfyUItoPython(
586+
input_file=input_file,
587+
output_file=output_file,
588+
queue_size=queue_size,
589+
needs_init_custom_nodes=True,
590+
)
586591

587592

588593
def main() -> None:
@@ -612,7 +617,7 @@ def main() -> None:
612617
default=DEFAULT_QUEUE_SIZE,
613618
)
614619
pargs = parser.parse_args()
615-
ComfyUItoPython(**vars(pargs))
620+
run(**vars(pargs))
616621
print("Done.")
617622

618623

comfyui_to_python_utils.py

-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
from typing import Sequence, Mapping, Any, Union
33
import sys
44

5-
sys.path.append("../")
6-
75

86
def import_custom_nodes() -> None:
97
"""Find all custom nodes in the custom_nodes folder and add those node objects to NODE_CLASS_MAPPINGS

images/save_as_script.png

57.5 KB
Loading

install.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import os
2+
import sys
3+
4+
from subprocess import Popen, check_output, PIPE
5+
6+
requirements = open(os.path.join(os.path.dirname(__file__), "requirements.txt")).read().split("\n")
7+
8+
installed_packages = check_output(
9+
[sys.executable, "-m", "pip", "list"],
10+
universal_newlines=True
11+
).split("\n")
12+
13+
installed_packages = set([package.split(" ")[0].lower() for package in installed_packages if package.strip()])
14+
15+
for requirement in requirements:
16+
if requirement.lower() not in installed_packages:
17+
print(f"Installing requirements...")
18+
Popen([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], stdout=PIPE, stderr=PIPE, cwd=os.path.dirname(__file__)).communicate()
19+
print(f"Installed.")
20+
break

0 commit comments

Comments
 (0)