Skip to content

Add UI button to save script directly form ComfyUI web app #74

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9aaee0b
Initial SaveAsScript changes
atmaranto Mar 7, 2024
5fe795c
Reduce requirements, use install script
atmaranto Mar 7, 2024
3ad6de7
Updated README and comments
atmaranto Mar 7, 2024
77cd75a
Download as Python file
atmaranto Mar 7, 2024
16b4858
Fix CLI bugs
atmaranto Mar 7, 2024
5564e0b
Add TODO and fork status
atmaranto Mar 7, 2024
5b00e27
Alphabetize Names
atmaranto Mar 7, 2024
38d941b
Update README.md
atmaranto Mar 7, 2024
58ea247
Update README.md
atmaranto Mar 7, 2024
ac6e6b6
Fix const/var
atmaranto Mar 7, 2024
8f71ab9
Update usage info
atmaranto Mar 7, 2024
f0bc65e
Fix punctuation
atmaranto Mar 7, 2024
793854d
Fixed Windows paths, missing inputs used as args
atmaranto Mar 9, 2024
0b4788d
Bugfixes for args, updated the README
atmaranto Mar 9, 2024
8f0a473
Update README, add template args
atmaranto Mar 9, 2024
507e096
Fixed code block
atmaranto Mar 10, 2024
8c7cbf6
Only randomize seed if it's a set value
atmaranto Mar 10, 2024
a0f665c
Allow argument to determine save location
atmaranto Mar 18, 2024
fc76d34
Update comfyui_to_python_utils.py
atmaranto Mar 18, 2024
e42b980
Module API and --output
atmaranto Mar 19, 2024
e53f61c
Update README.md
atmaranto Mar 19, 2024
fa4d363
Set __main__ path to ComfyUI's main.py
atmaranto Mar 20, 2024
a548adf
Add prompt data and handle kwargs
atmaranto May 4, 2024
26ed53e
Don't send prompt, unique_id
atmaranto May 4, 2024
9694b84
Don't alter sys.argv when imported
atmaranto May 15, 2024
be237bd
Ignore missing metadata
atmaranto May 23, 2024
ef9637f
Modifications to code generation match current state of main repo.
pydn Sep 14, 2024
b360c53
Add banner image.
pydn Sep 14, 2024
d5b1896
Merge branch 'dev' into atmaranto-main
pydn Sep 14, 2024
aa54166
Small update to allow comfyui_to_python.py to run from any subdirecto…
pydn Sep 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import sys
import os

from io import StringIO

import traceback

from aiohttp import web

ext_dir = os.path.dirname(__file__)
sys.path.append(ext_dir)

try:
import black
except ImportError:
print("Unable to import requirements for ComfyUI-SaveAsScript.")
print("Installing...")

import importlib

spec = importlib.util.spec_from_file_location(
"impact_install", os.path.join(os.path.dirname(__file__), "install.py")
)
impact_install = importlib.util.module_from_spec(spec)
spec.loader.exec_module(impact_install)

print("Successfully installed. Hopefully, at least.")

# Prevent reimporting of custom nodes
os.environ["RUNNING_IN_COMFYUI"] = "TRUE"

from comfyui_to_python import ComfyUItoPython

sys.path.append(os.path.dirname(os.path.dirname(ext_dir)))

import server

WEB_DIRECTORY = "js"
NODE_CLASS_MAPPINGS = {}


@server.PromptServer.instance.routes.post("/saveasscript")
async def save_as_script(request):
try:
data = await request.json()
name = data["name"]
workflow = data["workflow"]

sio = StringIO()
ComfyUItoPython(workflow=workflow, output_file=sio)

sio.seek(0)
data = sio.read()

return web.Response(text=data, status=200)
except Exception as e:
traceback.print_exc()
return web.Response(text=str(e), status=500)
113 changes: 59 additions & 54 deletions comfyui_to_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import random
import sys
import re
from typing import Dict, List, Callable, Tuple
from typing import Dict, List, Any, Callable, Tuple, TextIO
from argparse import ArgumentParser

import black
Expand All @@ -20,7 +20,7 @@
get_value_at_index,
)

sys.path.append("../")
add_comfyui_directory_to_sys_path()
from nodes import NODE_CLASS_MAPPINGS


Expand All @@ -36,7 +36,7 @@ class FileHandler:
"""

@staticmethod
def read_json_file(file_path: str) -> dict:
def read_json_file(file_path: str | TextIO, encoding: str = "utf-8") -> dict:
"""
Reads a JSON file and returns its contents as a dictionary.

Expand All @@ -51,35 +51,14 @@ def read_json_file(file_path: str) -> dict:
ValueError: If the file is not a valid JSON.
"""

try:
with open(file_path, "r") as file:
data = json.load(file)
return data

except FileNotFoundError:
# Get the directory from the file_path
directory = os.path.dirname(file_path)

# If the directory is an empty string (which means file is in the current directory),
# get the current working directory
if not directory:
directory = os.getcwd()

# Find all JSON files in the directory
json_files = glob.glob(f"{directory}/*.json")

# Format the list of JSON files as a string
json_files_str = "\n".join(json_files)

raise FileNotFoundError(
f"\n\nFile not found: {file_path}. JSON files in the directory:\n{json_files_str}"
)

except json.JSONDecodeError:
raise ValueError(f"Invalid JSON format in file: {file_path}")
if hasattr(file_path, "read"):
return json.load(file_path)
with open(file_path, "r", encoding="utf-8") as file:
data = json.load(file)
return data

@staticmethod
def write_code_to_file(file_path: str, code: str) -> None:
def write_code_to_file(file_path: str | TextIO, code: str) -> None:
"""Write the specified code to a Python file.

Args:
Expand All @@ -89,16 +68,19 @@ def write_code_to_file(file_path: str, code: str) -> None:
Returns:
None
"""
# Extract directory from the filename
directory = os.path.dirname(file_path)
if isinstance(file_path, str):
# Extract directory from the filename
directory = os.path.dirname(file_path)

# If the directory does not exist, create it
if directory and not os.path.exists(directory):
os.makedirs(directory)
# If the directory does not exist, create it
if directory and not os.path.exists(directory):
os.makedirs(directory)

# Save the code to a .py file
with open(file_path, "w") as file:
file.write(code)
# Save the code to a .py file
with open(file_path, "w", encoding="utf-8") as file:
file.write(code)
else:
file_path.write(code)


class LoadOrderDeterminer:
Expand Down Expand Up @@ -203,15 +185,12 @@ def __init__(self, node_class_mappings: Dict, base_node_class_mappings: Dict):
def generate_workflow(
self,
load_order: List,
filename: str = "generated_code_workflow.py",
queue_size: int = 10,
) -> str:
"""Generate the execution code based on the load order.

Args:
load_order (List): A list of tuples representing the load order.
filename (str): The name of the Python file to which the code should be saved.
Defaults to 'generated_code_workflow.py'.
queue_size (int): The number of photos that will be created by the script.

Returns:
Expand Down Expand Up @@ -515,23 +494,37 @@ class ComfyUItoPython:

def __init__(
self,
input_file: str,
output_file: str,
queue_size: int = 10,
workflow: str = "",
input_file: str = "",
output_file: str | TextIO = "",
queue_size: int = 1,
node_class_mappings: Dict = NODE_CLASS_MAPPINGS,
needs_init_custom_nodes: bool = False,
):
"""Initialize the ComfyUItoPython class with the given parameters.

"""Initialize the ComfyUItoPython class with the given parameters. Exactly one of workflow or input_file must be specified.
Args:
workflow (str): The workflow's JSON.
input_file (str): Path to the input JSON file.
output_file (str): Path to the output Python file.
queue_size (int): The number of times a workflow will be executed by the script. Defaults to 10.
output_file (str | TextIO): Path to the output file or a file-like object.
queue_size (int): The number of times a workflow will be executed by the script. Defaults to 1.
node_class_mappings (Dict): Mappings of node classes. Defaults to NODE_CLASS_MAPPINGS.
needs_init_custom_nodes (bool): Whether to initialize custom nodes. Defaults to False.
"""
if input_file and workflow:
raise ValueError("Can't provide both input_file and workflow")
elif not input_file and not workflow:
raise ValueError("Needs input_file or workflow")

if not output_file:
raise ValueError("Needs output_file")

self.workflow = workflow
self.input_file = input_file
self.output_file = output_file
self.queue_size = queue_size
self.node_class_mappings = node_class_mappings
self.needs_init_custom_nodes = needs_init_custom_nodes

self.base_node_class_mappings = copy.deepcopy(self.node_class_mappings)
self.execute()

Expand All @@ -541,11 +534,18 @@ def execute(self):
Returns:
None
"""
# Step 1: Import all custom nodes
import_custom_nodes()
# Step 1: Import all custom nodes if we need to
if self.needs_init_custom_nodes:
import_custom_nodes()
else:
# If they're already imported, we don't know which nodes are custom nodes, so we need to import all of them
self.base_node_class_mappings = {}

# Step 2: Read JSON data from the input file
data = FileHandler.read_json_file(self.input_file)
if self.input_file:
data = FileHandler.read_json_file(self.input_file)
else:
data = json.loads(self.workflow)

# Step 3: Determine the load order
load_order_determiner = LoadOrderDeterminer(data, self.node_class_mappings)
Expand All @@ -556,7 +556,7 @@ def execute(self):
self.node_class_mappings, self.base_node_class_mappings
)
generated_code = code_generator.generate_workflow(
load_order, filename=self.output_file, queue_size=self.queue_size
load_order, queue_size=self.queue_size
)

# Step 5: Write the generated code to a file
Expand All @@ -582,7 +582,12 @@ def run(
Returns:
None
"""
ComfyUItoPython(input_file, output_file, queue_size)
ComfyUItoPython(
input_file=input_file,
output_file=output_file,
queue_size=queue_size,
needs_init_custom_nodes=True,
)


def main() -> None:
Expand Down Expand Up @@ -612,7 +617,7 @@ def main() -> None:
default=DEFAULT_QUEUE_SIZE,
)
pargs = parser.parse_args()
ComfyUItoPython(**vars(pargs))
run(**vars(pargs))
print("Done.")


Expand Down
2 changes: 0 additions & 2 deletions comfyui_to_python_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
from typing import Sequence, Mapping, Any, Union
import sys

sys.path.append("../")


def import_custom_nodes() -> None:
"""Find all custom nodes in the custom_nodes folder and add those node objects to NODE_CLASS_MAPPINGS
Expand Down
Binary file added images/save_as_script.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import os
import sys

from subprocess import Popen, check_output, PIPE

requirements = open(os.path.join(os.path.dirname(__file__), "requirements.txt")).read().split("\n")

installed_packages = check_output(
[sys.executable, "-m", "pip", "list"],
universal_newlines=True
).split("\n")

installed_packages = set([package.split(" ")[0].lower() for package in installed_packages if package.strip()])

for requirement in requirements:
if requirement.lower() not in installed_packages:
print(f"Installing requirements...")
Popen([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], stdout=PIPE, stderr=PIPE, cwd=os.path.dirname(__file__)).communicate()
print(f"Installed.")
break
56 changes: 56 additions & 0 deletions js/save-as-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { $el } from "../../scripts/ui.js";

app.registerExtension({
name: "Comfy.SaveAsScript",
init() {
$el("style", {
parent: document.head,
});
},
async setup() {
const menu = document.querySelector(".comfy-menu");
const separator = document.createElement("hr");

separator.style.margin = "20px 0";
separator.style.width = "100%";
menu.append(separator);

const saveButton = document.createElement("button");
saveButton.textContent = "Save as Script";
saveButton.onclick = () => {
var filename = prompt("Save script as:");
if(filename === undefined || filename === null || filename === "") {
return
}

app.graphToPrompt().then(async (p) => {
const json = JSON.stringify({name: filename + ".json", workflow: JSON.stringify(p.output, null, 2)}, null, 2); // convert the data to a JSON string
var response = await api.fetchApi(`/saveasscript`, { method: "POST", body: json });
if(response.status == 200) {
const blob = new Blob([await response.text()], {type: "text/python;charset=utf-8"});
const url = URL.createObjectURL(blob);
if(!filename.endsWith(".py")) {
filename += ".py";
}

const a = $el("a", {
href: url,
download: filename,
style: {display: "none"},
parent: document.body,
});
a.click();
setTimeout(function () {
a.remove();
window.URL.revokeObjectURL(url);
}, 0);
}
});
}
menu.append(saveButton);

console.log("SaveAsScript loaded");
}
});
12 changes: 0 additions & 12 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1 @@
torch
torchdiffeq
torchsde
einops
transformers>=4.25.1
safetensors>=0.3.0
aiohttp
accelerate
pyyaml
Pillow
scipy
tqdm
black