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:
parent
737c158763
commit
f7a8218814
|
@ -11,6 +11,7 @@ import torch
|
||||||
import nodes
|
import nodes
|
||||||
|
|
||||||
import comfy.model_management
|
import comfy.model_management
|
||||||
|
import folder_paths
|
||||||
|
|
||||||
def get_input_data(inputs, class_def, unique_id, outputs={}, prompt={}, extra_data={}):
|
def get_input_data(inputs, class_def, unique_id, outputs={}, prompt={}, extra_data={}):
|
||||||
valid_inputs = class_def.INPUT_TYPES()
|
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))
|
return (False, "Value bigger than max. {}, {}".format(class_type, x))
|
||||||
|
|
||||||
if isinstance(type_input, list):
|
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 (False, "Value not in list. {}, {}: {} not in {}".format(class_type, x, val, type_input))
|
||||||
return (True, "")
|
return (True, "")
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,46 @@ def get_directory_by_type(type_name):
|
||||||
return None
|
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):
|
def add_model_folder_path(folder_name, full_folder_path):
|
||||||
global folder_names_and_paths
|
global folder_names_and_paths
|
||||||
if folder_name in folder_names_and_paths:
|
if folder_name in folder_names_and_paths:
|
||||||
|
|
8
nodes.py
8
nodes.py
|
@ -975,7 +975,7 @@ class LoadImage:
|
||||||
FUNCTION = "load_image"
|
FUNCTION = "load_image"
|
||||||
def load_image(self, image):
|
def load_image(self, image):
|
||||||
input_dir = folder_paths.get_input_directory()
|
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)
|
i = Image.open(image_path)
|
||||||
image = i.convert("RGB")
|
image = i.convert("RGB")
|
||||||
image = np.array(image).astype(np.float32) / 255.0
|
image = np.array(image).astype(np.float32) / 255.0
|
||||||
|
@ -990,7 +990,7 @@ class LoadImage:
|
||||||
@classmethod
|
@classmethod
|
||||||
def IS_CHANGED(s, image):
|
def IS_CHANGED(s, image):
|
||||||
input_dir = folder_paths.get_input_directory()
|
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()
|
m = hashlib.sha256()
|
||||||
with open(image_path, 'rb') as f:
|
with open(image_path, 'rb') as f:
|
||||||
m.update(f.read())
|
m.update(f.read())
|
||||||
|
@ -1011,7 +1011,7 @@ class LoadImageMask:
|
||||||
FUNCTION = "load_image"
|
FUNCTION = "load_image"
|
||||||
def load_image(self, image, channel):
|
def load_image(self, image, channel):
|
||||||
input_dir = folder_paths.get_input_directory()
|
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)
|
i = Image.open(image_path)
|
||||||
if i.getbands() != ("R", "G", "B", "A"):
|
if i.getbands() != ("R", "G", "B", "A"):
|
||||||
i = i.convert("RGBA")
|
i = i.convert("RGBA")
|
||||||
|
@ -1029,7 +1029,7 @@ class LoadImageMask:
|
||||||
@classmethod
|
@classmethod
|
||||||
def IS_CHANGED(s, image, channel):
|
def IS_CHANGED(s, image, channel):
|
||||||
input_dir = folder_paths.get_input_directory()
|
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()
|
m = hashlib.sha256()
|
||||||
with open(image_path, 'rb') as f:
|
with open(image_path, 'rb') as f:
|
||||||
m.update(f.read())
|
m.update(f.read())
|
||||||
|
|
15
server.py
15
server.py
|
@ -112,14 +112,21 @@ class PromptServer():
|
||||||
|
|
||||||
@routes.post("/upload/image")
|
@routes.post("/upload/image")
|
||||||
async def upload_image(request):
|
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):
|
if not os.path.exists(upload_dir):
|
||||||
os.makedirs(upload_dir)
|
os.makedirs(upload_dir)
|
||||||
|
|
||||||
post = await request.post()
|
|
||||||
image = post.get("image")
|
|
||||||
|
|
||||||
if image and image.file:
|
if image and image.file:
|
||||||
filename = image.filename
|
filename = image.filename
|
||||||
if not filename:
|
if not filename:
|
||||||
|
|
|
@ -20,6 +20,12 @@ export class ComfyApp {
|
||||||
*/
|
*/
|
||||||
#processingQueue = false;
|
#processingQueue = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content Clipboard
|
||||||
|
* @type {serialized node object}
|
||||||
|
*/
|
||||||
|
static clipspace = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.ui = new ComfyUI(this);
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue