Skip to content

Commit af9a0df

Browse files
authored
Merge pull request #74 from pydn/atmaranto-main
Add UI button to save script directly form ComfyUI web app
2 parents d0c9b00 + aa54166 commit af9a0df

7 files changed

+193
-68
lines changed

__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

js/save-as-script.js

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { app } from "../../scripts/app.js";
2+
import { api } from "../../scripts/api.js";
3+
import { $el } from "../../scripts/ui.js";
4+
5+
app.registerExtension({
6+
name: "Comfy.SaveAsScript",
7+
init() {
8+
$el("style", {
9+
parent: document.head,
10+
});
11+
},
12+
async setup() {
13+
const menu = document.querySelector(".comfy-menu");
14+
const separator = document.createElement("hr");
15+
16+
separator.style.margin = "20px 0";
17+
separator.style.width = "100%";
18+
menu.append(separator);
19+
20+
const saveButton = document.createElement("button");
21+
saveButton.textContent = "Save as Script";
22+
saveButton.onclick = () => {
23+
var filename = prompt("Save script as:");
24+
if(filename === undefined || filename === null || filename === "") {
25+
return
26+
}
27+
28+
app.graphToPrompt().then(async (p) => {
29+
const json = JSON.stringify({name: filename + ".json", workflow: JSON.stringify(p.output, null, 2)}, null, 2); // convert the data to a JSON string
30+
var response = await api.fetchApi(`/saveasscript`, { method: "POST", body: json });
31+
if(response.status == 200) {
32+
const blob = new Blob([await response.text()], {type: "text/python;charset=utf-8"});
33+
const url = URL.createObjectURL(blob);
34+
if(!filename.endsWith(".py")) {
35+
filename += ".py";
36+
}
37+
38+
const a = $el("a", {
39+
href: url,
40+
download: filename,
41+
style: {display: "none"},
42+
parent: document.body,
43+
});
44+
a.click();
45+
setTimeout(function () {
46+
a.remove();
47+
window.URL.revokeObjectURL(url);
48+
}, 0);
49+
}
50+
});
51+
}
52+
menu.append(saveButton);
53+
54+
console.log("SaveAsScript loaded");
55+
}
56+
});

requirements.txt

-12
Original file line numberDiff line numberDiff line change
@@ -1,13 +1 @@
1-
torch
2-
torchdiffeq
3-
torchsde
4-
einops
5-
transformers>=4.25.1
6-
safetensors>=0.3.0
7-
aiohttp
8-
accelerate
9-
pyyaml
10-
Pillow
11-
scipy
12-
tqdm
131
black

0 commit comments

Comments
 (0)