Add clipspace feature. (#541)

* Add clipspace feature.
* feat: copy content to clipspace
* feat: paste content from clipspace

Extend validation to allow for validating annotated_path in addition to other parameters.

Add support for annotated_filepath in folder_paths function.

Generalize the '/upload/image' API to allow for uploading images to the 'input', 'temp', or 'output' directories.

* rename contentClipboard -> clipspace

* Do deep copy for imgs on copy to clipspace.

* add original_imgs into clipspace
* Preserve the original image when 'imgs' are modified

* robust patch & refactoring folder_paths about annotated_filepath

* Only show the Paste menu if the ComfyApp.clipspace is not empty

* instant refresh on paste

force triggering 'changed' on paste action

* subfolder fix on paste logic

attach subfolder if subfolder isn't empty

---------

Co-authored-by: Lt.Dr.Data <lt.dr.data@gmail.com>
This commit is contained in:
ltdrdata 2023-04-24 04:58:55 +09:00 committed by GitHub
parent 737c158763
commit f7a8218814
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 145 additions and 9 deletions

View File

@ -11,6 +11,7 @@ import torch
import nodes
import comfy.model_management
import folder_paths
def get_input_data(inputs, class_def, unique_id, outputs={}, prompt={}, extra_data={}):
valid_inputs = class_def.INPUT_TYPES()
@ -250,7 +251,12 @@ def validate_inputs(prompt, item):
return (False, "Value bigger than max. {}, {}".format(class_type, x))
if isinstance(type_input, list):
if val not in type_input:
is_annotated_path = val.endswith("[temp]") or val.endswith("[input]") or val.endswith("[output]")
if is_annotated_path:
if not folder_paths.exists_annotated_filepath(val):
return (False, "Invalid file path. {}, {}: {}".format(class_type, x, val))
elif val not in type_input:
return (False, "Value not in list. {}, {}: {} not in {}".format(class_type, x, val, type_input))
return (True, "")

View File

@ -69,6 +69,46 @@ def get_directory_by_type(type_name):
return None
# determine base_dir rely on annotation if name is 'filename.ext [annotation]' format
# otherwise use default_path as base_dir
def touch_annotated_filepath(name):
if name.endswith("[output]"):
base_dir = get_output_directory()
name = name[:-9]
elif name.endswith("[input]"):
base_dir = get_input_directory()
name = name[:-8]
elif name.endswith("[temp]"):
base_dir = get_temp_directory()
name = name[:-7]
else:
return name, None
return name, base_dir
def get_annotated_filepath(name, default_dir=None):
name, base_dir = touch_annotated_filepath(name)
if base_dir is None:
if default_dir is not None:
base_dir = default_dir
else:
base_dir = get_input_directory() # fallback path
return os.path.join(base_dir, name)
def exists_annotated_filepath(name):
name, base_dir = touch_annotated_filepath(name)
if base_dir is None:
base_dir = get_input_directory() # fallback path
filepath = os.path.join(base_dir, name)
return os.path.exists(filepath)
def add_model_folder_path(folder_name, full_folder_path):
global folder_names_and_paths
if folder_name in folder_names_and_paths:

View File

@ -975,7 +975,7 @@ class LoadImage:
FUNCTION = "load_image"
def load_image(self, image):
input_dir = folder_paths.get_input_directory()
image_path = os.path.join(input_dir, image)
image_path = folder_paths.get_annotated_filepath(image, input_dir)
i = Image.open(image_path)
image = i.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
@ -990,7 +990,7 @@ class LoadImage:
@classmethod
def IS_CHANGED(s, image):
input_dir = folder_paths.get_input_directory()
image_path = os.path.join(input_dir, image)
image_path = folder_paths.get_annotated_filepath(image, input_dir)
m = hashlib.sha256()
with open(image_path, 'rb') as f:
m.update(f.read())
@ -1011,7 +1011,7 @@ class LoadImageMask:
FUNCTION = "load_image"
def load_image(self, image, channel):
input_dir = folder_paths.get_input_directory()
image_path = os.path.join(input_dir, image)
image_path = folder_paths.get_annotated_filepath(image, input_dir)
i = Image.open(image_path)
if i.getbands() != ("R", "G", "B", "A"):
i = i.convert("RGBA")
@ -1029,7 +1029,7 @@ class LoadImageMask:
@classmethod
def IS_CHANGED(s, image, channel):
input_dir = folder_paths.get_input_directory()
image_path = os.path.join(input_dir, image)
image_path = folder_paths.get_annotated_filepath(image, input_dir)
m = hashlib.sha256()
with open(image_path, 'rb') as f:
m.update(f.read())

View File

@ -112,13 +112,20 @@ class PromptServer():
@routes.post("/upload/image")
async def upload_image(request):
upload_dir = folder_paths.get_input_directory()
post = await request.post()
image = post.get("image")
if post.get("type") is None:
upload_dir = folder_paths.get_input_directory()
elif post.get("type") == "input":
upload_dir = folder_paths.get_input_directory()
elif post.get("type") == "temp":
upload_dir = folder_paths.get_temp_directory()
elif post.get("type") == "output":
upload_dir = folder_paths.get_output_directory()
if not os.path.exists(upload_dir):
os.makedirs(upload_dir)
post = await request.post()
image = post.get("image")
if image and image.file:
filename = image.filename

View File

@ -20,6 +20,12 @@ export class ComfyApp {
*/
#processingQueue = false;
/**
* Content Clipboard
* @type {serialized node object}
*/
static clipspace = null;
constructor() {
this.ui = new ComfyUI(this);
@ -130,6 +136,83 @@ export class ComfyApp {
);
}
}
options.push(
{
content: "Copy (Clipspace)",
callback: (obj) => {
var widgets = null;
if(this.widgets) {
widgets = this.widgets.map(({ type, name, value }) => ({ type, name, value }));
}
let img = new Image();
var imgs = undefined;
if(this.imgs != undefined) {
img.src = this.imgs[0].src;
imgs = [img];
}
ComfyApp.clipspace = {
'widgets': widgets,
'imgs': imgs,
'original_imgs': imgs,
'images': this.images
};
}
});
if(ComfyApp.clipspace != null) {
options.push(
{
content: "Paste (Clipspace)",
callback: () => {
if(ComfyApp.clipspace != null) {
if(ComfyApp.clipspace.widgets != null && this.widgets != null) {
ComfyApp.clipspace.widgets.forEach(({ type, name, value }) => {
const prop = Object.values(this.widgets).find(obj => obj.type === type && obj.name === name);
if (prop) {
prop.value = value;
}
});
}
// image paste
if(ComfyApp.clipspace.imgs != undefined && this.imgs != undefined && this.widgets != null) {
var filename = "";
if(this.images && ComfyApp.clipspace.images) {
this.images = ComfyApp.clipspace.images;
}
if(ComfyApp.clipspace.images != undefined) {
const clip_image = ComfyApp.clipspace.images[0];
if(clip_image.subfolder != '')
filename = `${clip_image.subfolder}/`;
filename += `${clip_image.filename} [${clip_image.type}]`;
}
else if(ComfyApp.clipspace.widgets != undefined) {
const index_in_clip = ComfyApp.clipspace.widgets.findIndex(obj => obj.name === 'image');
if(index_in_clip >= 0) {
filename = `${ComfyApp.clipspace.widgets[index_in_clip].value}`;
}
}
const index = this.widgets.findIndex(obj => obj.name === 'image');
if(index >= 0 && filename != "" && ComfyApp.clipspace.imgs != undefined) {
this.imgs = ComfyApp.clipspace.imgs;
this.widgets[index].value = filename;
if(this.widgets_values != undefined) {
this.widgets_values[index] = filename;
}
}
}
this.trigger('changed');
}
}
}
);
}
};
}