|
|
from PIL import Image
|
|
|
import numpy as np
|
|
|
import comfy.utils
|
|
|
from server import PromptServer, BinaryEventTypes
|
|
|
import time, io, struct
|
|
|
|
|
|
SPECIAL_ID = 12345
|
|
|
VIDEO_ID = 12346
|
|
|
TEXT_ID = 12347
|
|
|
|
|
|
def send_image_to_server_raw(type_num: int, save_me: callable, id: int, event_type: int = BinaryEventTypes.PREVIEW_IMAGE):
|
|
|
out = io.BytesIO()
|
|
|
header = struct.pack(">I", type_num)
|
|
|
out.write(header)
|
|
|
save_me(out)
|
|
|
out.seek(0)
|
|
|
preview_bytes = out.getvalue()
|
|
|
server = PromptServer.instance
|
|
|
server.send_sync("progress", {"value": id, "max": id}, sid=server.client_id)
|
|
|
server.send_sync(event_type, preview_bytes, sid=server.client_id)
|
|
|
|
|
|
class SwarmSaveImageWS:
|
|
|
@classmethod
|
|
|
def INPUT_TYPES(s):
|
|
|
return {
|
|
|
"required": {
|
|
|
"images": ("IMAGE", ),
|
|
|
},
|
|
|
"optional": {
|
|
|
"bit_depth": (["8bit", "16bit", "raw"], {"default": "8bit"})
|
|
|
}
|
|
|
}
|
|
|
|
|
|
CATEGORY = "SwarmUI/images"
|
|
|
RETURN_TYPES = ()
|
|
|
FUNCTION = "save_images"
|
|
|
OUTPUT_NODE = True
|
|
|
DESCRIPTION = "Acts like a special version of 'SaveImage' that doesn't actual save to disk, instead it sends directly over websocket. This is intended so that SwarmUI can save the image itself rather than having Comfy's Core save it."
|
|
|
|
|
|
def save_images(self, images, bit_depth = "8bit"):
|
|
|
pbar = comfy.utils.ProgressBar(SPECIAL_ID)
|
|
|
step = 0
|
|
|
for image in images:
|
|
|
if bit_depth == "raw":
|
|
|
i = 255.0 * image.cpu().numpy()
|
|
|
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
|
|
def do_save(out):
|
|
|
img.save(out, format='BMP')
|
|
|
send_image_to_server_raw(1, do_save, SPECIAL_ID, event_type=10)
|
|
|
elif bit_depth == "16bit":
|
|
|
i = 65535.0 * image.cpu().numpy()
|
|
|
img = self.convert_img_16bit(np.clip(i, 0, 65535).astype(np.uint16))
|
|
|
send_image_to_server_raw(2, lambda out: out.write(img), SPECIAL_ID)
|
|
|
else:
|
|
|
i = 255.0 * image.cpu().numpy()
|
|
|
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
|
|
pbar.update_absolute(step, SPECIAL_ID, ("PNG", img, None))
|
|
|
step += 1
|
|
|
|
|
|
return {}
|
|
|
|
|
|
def convert_img_16bit(self, img_np):
|
|
|
try:
|
|
|
import cv2
|
|
|
img_np = cv2.cvtColor(img_np, cv2.COLOR_BGR2RGB)
|
|
|
success, img_encoded = cv2.imencode('.png', img_np)
|
|
|
|
|
|
if img_encoded is None or not success:
|
|
|
raise RuntimeError("OpenCV failed to encode image.")
|
|
|
|
|
|
return img_encoded.tobytes()
|
|
|
except Exception as e:
|
|
|
print(f"Error converting OpenCV image to PIL: {e}")
|
|
|
raise
|
|
|
|
|
|
@classmethod
|
|
|
def IS_CHANGED(s, images, bit_depth = "8bit"):
|
|
|
return time.time()
|
|
|
|
|
|
|
|
|
class SwarmSaveAnimatedWebpWS:
|
|
|
methods = {"default": 4, "fastest": 0, "slowest": 6}
|
|
|
|
|
|
@classmethod
|
|
|
def INPUT_TYPES(s):
|
|
|
return {
|
|
|
"required": {
|
|
|
"images": ("IMAGE", ),
|
|
|
"fps": ("FLOAT", {"default": 6.0, "min": 0.01, "max": 1000.0, "step": 0.01, "tooltip": "Frames per second, must match the actual generated speed or else you will get slow/fast motion."}),
|
|
|
"lossless": ("BOOLEAN", {"default": True, "tooltip": "If true, the image will be saved losslessly, otherwise it will be saved with the quality specified. Lossless is best quality, but takes more file space."}),
|
|
|
"quality": ("INT", {"default": 80, "min": 0, "max": 100, "tooltip": "Quality of the image as a percentage, only used if lossless is false. Smaller values save more space but look worse. 80 is a fine general value."}),
|
|
|
"method": (list(s.methods.keys()),),
|
|
|
},
|
|
|
}
|
|
|
|
|
|
CATEGORY = "SwarmUI/video"
|
|
|
RETURN_TYPES = ()
|
|
|
FUNCTION = "save_images"
|
|
|
OUTPUT_NODE = True
|
|
|
DESCRIPTION = "Acts like a special version of 'SaveAnimatedWEBP' that doesn't actual save to disk, instead it sends directly over websocket. This is intended so that SwarmUI can save the image itself rather than having Comfy's Core save it."
|
|
|
|
|
|
def save_images(self, images, fps, lossless, quality, method):
|
|
|
method = self.methods.get(method)
|
|
|
pil_images = []
|
|
|
for image in images:
|
|
|
i = 255. * image.cpu().numpy()
|
|
|
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
|
|
pil_images.append(img)
|
|
|
|
|
|
def do_save(out):
|
|
|
pil_images[0].save(out, save_all=True, duration=int(1000.0/fps), append_images=pil_images[1 : len(pil_images)], lossless=lossless, quality=quality, method=method, format='WEBP')
|
|
|
send_image_to_server_raw(3, do_save, VIDEO_ID)
|
|
|
|
|
|
return { }
|
|
|
|
|
|
@classmethod
|
|
|
def IS_CHANGED(s, images, fps, lossless, quality, method):
|
|
|
return time.time()
|
|
|
|
|
|
|
|
|
class SwarmAddSaveMetadataWS:
|
|
|
@classmethod
|
|
|
def INPUT_TYPES(s):
|
|
|
return {
|
|
|
"required": {
|
|
|
"key": ("STRING", {"tooltip": "The key to add to the metadata tracker. Must be simple A-Z plain text or underscores."}),
|
|
|
"value": ("STRING", {"tooltip": "The value to add to the metadata tracker."}),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
CATEGORY = "SwarmUI/images"
|
|
|
RETURN_TYPES = ()
|
|
|
FUNCTION = "add_save_metadata"
|
|
|
OUTPUT_NODE = True
|
|
|
DESCRIPTION = "Adds a metadata key/value pair to SwarmUI's metadata tracker for this generation, which will be appended to any images saved after this node triggers. Note that keys overwrite, not add. Any key can have only one value."
|
|
|
|
|
|
def add_save_metadata(self, key, value):
|
|
|
full_text = f"{key}:{value}"
|
|
|
full_text_bytes = full_text.encode('utf-8')
|
|
|
send_image_to_server_raw(0, lambda out: out.write(full_text_bytes), TEXT_ID, event_type=BinaryEventTypes.TEXT)
|
|
|
return {}
|
|
|
|
|
|
@classmethod
|
|
|
def IS_CHANGED(s, key, value):
|
|
|
return time.time()
|
|
|
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
|
"SwarmSaveImageWS": SwarmSaveImageWS,
|
|
|
"SwarmSaveAnimatedWebpWS": SwarmSaveAnimatedWebpWS,
|
|
|
"SwarmAddSaveMetadataWS": SwarmAddSaveMetadataWS,
|
|
|
}
|
|
|
|