import os import torch import numpy as np from PIL import Image import folder_paths from nodes import SaveImage import server # Import server import asyncio # Import Import asyncio from comfy.comfy_types import node_typing class _CozyGenDynamicTypes(str): basic_types = node_typing.IO.PRIMITIVE.split(",") def __eq__(self, other): return other in self.basic_types or isinstance(other, (list, _CozyGenDynamicTypes)) def __ne__(self, other): return not self.__eq__(other) CozyGenDynamicTypes = _CozyGenDynamicTypes("COZYGEN_DYNAMIC_TYPE") class CozyGenDynamicInput: _NODE_CLASS_NAME = "CozyGenDynamicInput" # Link to custom JavaScript @classmethod def INPUT_TYPES(cls): return { "required": { "param_name": ("STRING", {"default": "Dynamic Parameter"}), "priority": ("INT", {"default": 10}), "param_type": (["STRING", "INT", "FLOAT", "BOOLEAN", "DROPDOWN"], {"default": "STRING"}), "default_value": ("STRING", {"default": ""}), }, "optional": { "add_randomize_toggle": ("BOOLEAN", {"default": False}), "choice_type": ("STRING", {"default": ""}), "display_bypass": ("BOOLEAN", {"default": False}), }, "hidden": { "choices": ("STRING", {"default": ""}), # Used by JS for dropdowns "multiline": ("BOOLEAN", {"default": False}), # Used by JS for strings "min_value": ("FLOAT", {"default": 0.0}), # Used by JS for numbers "max_value": ("FLOAT", {"default": 1.0}), # Used by JS for numbers "step": ("FLOAT", {"default": 0.0}), # Used by JS for numbers } } RETURN_TYPES = (node_typing.IO.ANY,) # Can return any type FUNCTION = "get_dynamic_value" CATEGORY = "CozyGen" def get_dynamic_value(self, param_name, priority, param_type, default_value, add_randomize_toggle=False, choice_type="", min_value=0.0, max_value=1.0, choices="", multiline=False, step=None, display_bypass=False): # Convert default_value based on param_type if param_type == "INT": try: value = int(default_value) except (ValueError, TypeError): value = 0 # Default to 0 if conversion fails elif param_type == "FLOAT": try: value = float(default_value) except (ValueError, TypeError): value = 0.0 # Default to 0.0 if conversion fails elif param_type == "BOOLEAN": value = str(default_value).lower() == "true" elif param_type == "DROPDOWN": value = default_value # For dropdowns, default_value is already the selected string else: # STRING or any other type value = default_value return (value, ) class CozyGenImageInput: @classmethod def INPUT_TYPES(s): # This input now correctly accepts a STRING, which will be our Base64 data. return { "required": { "param_name": ("STRING", {"default": "Image Input"}), "image_filename": ("STRING", {"default": ""}), } } # The return types are now the standard IMAGE and MASK for ComfyUI image loaders. RETURN_TYPES = ("IMAGE", "MASK") FUNCTION = "load_image" CATEGORY = "CozyGen" def load_image(self, param_name, image_filename): if not image_filename: return (None, None) image_path = folder_paths.get_input_directory() + os.sep + image_filename img = Image.open(image_path) image_np = np.array(img).astype(np.float32) / 255.0 image_tensor = torch.from_numpy(image_np)[None,] # Handle images with an alpha channel (transparency) to create a mask if 'A' in img.getbands(): mask = image_tensor[:, :, :, 3] image = image_tensor[:, :, :, :3] # Keep only the RGB channels for the image else: # If no alpha channel, the mask is all white (fully opaque) mask = torch.ones_like(image_tensor[:, :, :, 0]) image = image_tensor return (image, mask) from .api import server_queue, queue_lock class CozyGenOutput(SaveImage): def __init__(self): super().__init__() self.output_dir = folder_paths.get_output_directory() @classmethod def INPUT_TYPES(cls): return { "required": { "images": ("IMAGE", ), }, "optional": { "filename_prefix": ("STRING", {"default": "CozyGen/output"}), }, "hidden": { "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "job_id": ("STRING", {"default": ""}), # For tracking }, } FUNCTION = "save_images" CATEGORY = "CozyGen" def save_images(self, images, filename_prefix="CozyGen/output", prompt=None, extra_pnginfo=None, job_id=""): results = super().save_images(images, filename_prefix, prompt, extra_pnginfo) server_instance = server.PromptServer.instance # Extract job_id from the prompt's extra_data if not passed directly if not job_id and extra_pnginfo and 'job_id' in extra_pnginfo: job_id = extra_pnginfo['job_id'] if server_instance and results and 'ui' in results and 'images' in results['ui']: batch_images_data = [] thumbnail_url = None for i, saved_image in enumerate(results['ui']['images']): image_url = f"/view?filename={saved_image['filename']}&subfolder={saved_image['subfolder']}&type={saved_image['type']}" if i == 0: thumbnail_url = image_url batch_images_data.append({ "url": image_url, "filename": saved_image['filename'], "subfolder": saved_image['subfolder'], "type": saved_image['type'] }) if batch_images_data: message_data = { "status": "images_generated", "images": batch_images_data, "job_id": job_id } server_instance.send_sync("cozygen_batch_ready", message_data) # Update server-side queue if job_id: asyncio.run_coroutine_threadsafe(self.update_job_status(job_id, 'completed', thumbnail_url), server_instance.loop) return results async def update_job_status(self, job_id, status, thumbnail_url=None): async with queue_lock: for job in server_queue: if job['id'] == job_id: job['status'] = status if thumbnail_url: job['thumbnailUrl'] = thumbnail_url break import imageio class CozyGenVideoOutput: def __init__(self): self.output_dir = folder_paths.get_output_directory() self.type = "output" self.prefix_append = "" @classmethod def INPUT_TYPES(s): return {"required": {"images": ("IMAGE", ), "frame_rate": ("INT", {"default": 8, "min": 1, "max": 24}), "loop_count": ("INT", {"default": 0, "min": 0, "max": 100}), "filename_prefix": ("STRING", {"default": "CozyGen/video"}), "format": (["video/webm", "video/mp4", "image/gif"],), "pingpong": ("BOOLEAN", {"default": False}), }, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "job_id": ("STRING", {"default": ""})}, } RETURN_TYPES = () FUNCTION = "save_video" OUTPUT_NODE = True CATEGORY = "CozyGen" def save_video(self, images, frame_rate, loop_count, filename_prefix="CozyGen/video", format="video/webm", pingpong=False, prompt=None, extra_pnginfo=None, job_id=""): filename_prefix += self.prefix_append full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]) results = list() if format == "image/gif": ext = "gif" elif format == "video/mp4": ext = "mp4" else: ext = "webm" file = f"{filename}_{counter:05}_.{ext}" video_data = (images.cpu().numpy() * 255).astype(np.uint8) if pingpong: video_data = np.concatenate((video_data, video_data[-2:0:-1]), axis=0) if format == "image/gif": imageio.mimsave(os.path.join(full_output_folder, file), video_data, duration=(1000/frame_rate)/1000, loop=loop_count) else: imageio.mimsave(os.path.join(full_output_folder, file), video_data, fps=frame_rate) results.append({ "filename": file, "subfolder": subfolder, "type": self.type }) if not job_id and extra_pnginfo and 'job_id' in extra_pnginfo: job_id = extra_pnginfo['job_id'] server_instance = server.PromptServer.instance if server_instance: for result in results: video_url = f"/view?filename={result['filename']}&subfolder={result['subfolder']}&type={result['type']}" message_data = { "status": "video_generated", "url": video_url, "job_id": job_id } server_instance.send_sync("cozygen_video_ready", message_data) if job_id: asyncio.run_coroutine_threadsafe(self.update_job_status(job_id, 'completed', video_url), server_instance.loop) return { "ui": { "videos": results } } async def update_job_status(self, job_id, status, thumbnail_url=None): async with queue_lock: for job in server_queue: if job['id'] == job_id: job['status'] = status if thumbnail_url: job['thumbnailUrl'] = thumbnail_url break import comfy.samplers # Dynamically get model folder names models_path = folder_paths.models_dir model_folders = sorted([d.name for d in os.scandir(models_path) if d.is_dir()]) static_choices = ["sampler", "scheduler"] all_choice_types = model_folders + static_choices class CozyGenFloatInput: @classmethod def INPUT_TYPES(cls): return { "required": { "param_name": ("STRING", {"default": "Float Parameter"}), "priority": ("INT", {"default": 10}), "default_value": ("FLOAT", {"default": 1.0}), "min_value": ("FLOAT", {"default": 0.0}), "max_value": ("FLOAT", {"default": 1024.0}), "step": ("FLOAT", {"default": 0.1}), "add_randomize_toggle": ("BOOLEAN", {"default": False}), } } RETURN_TYPES = ("FLOAT",) FUNCTION = "get_value" CATEGORY = "CozyGen/Static" def get_value(self, param_name, priority, default_value, min_value, max_value, step, add_randomize_toggle): return (default_value,) class CozyGenIntInput: @classmethod def INPUT_TYPES(cls): return { "required": { "param_name": ("STRING", {"default": "Int Parameter"}), "priority": ("INT", {"default": 10}), "default_value": ("INT", {"default": 1}), "min_value": ("INT", {"default": 0}), "max_value": ("INT", {"default": 9999999999, "max": 9999999999}), "step": ("INT", {"default": 1}), "add_randomize_toggle": ("BOOLEAN", {"default": False}), } } RETURN_TYPES = ("INT",) FUNCTION = "get_value" CATEGORY = "CozyGen/Static" def get_value(self, param_name, priority, default_value, min_value, max_value, step, add_randomize_toggle): return (default_value,) class CozyGenStringInput: @classmethod def INPUT_TYPES(cls): return { "required": { "param_name": ("STRING", {"default": "String Parameter"}), "priority": ("INT", {"default": 10}), "default_value": ("STRING", {"default": ""}), "display_multiline": ("BOOLEAN", {"default": False}), } } RETURN_TYPES = ("STRING",) FUNCTION = "get_value" CATEGORY = "CozyGen/Static" def get_value(self, param_name, priority, default_value, display_multiline): return (default_value,) class CozyGenChoiceInput: _NODE_CLASS_NAME = "CozyGenChoiceInput" @classmethod def INPUT_TYPES(cls): # Create a flat list of all possible choices for the initial dropdown all_choices = [] for choice_type in all_choice_types: if choice_type == "sampler": all_choices.extend(comfy.samplers.KSampler.SAMPLERS) elif choice_type == "scheduler": all_choices.extend(comfy.samplers.KSampler.SCHEDULERS) else: try: all_choices.extend(folder_paths.get_filename_list(choice_type)) except KeyError: pass # Ignore choice types that don't have a corresponding folder # Add a "None" option to be safe all_choices = ["None"] + sorted(list(set(all_choices))) return { "required": { "param_name": ("STRING", {"default": "Choice Parameter"}), "priority": ("INT", {"default": 10}), "choice_type": (all_choice_types,), "default_choice": (all_choices,), "display_bypass": ("BOOLEAN", {"default": False}), }, "hidden": { "value": ("STRING", { "default": "" }) # This is the value from the web UI } } RETURN_TYPES = (node_typing.IO.ANY,) FUNCTION = "get_value" CATEGORY = "CozyGen/Static" def get_value(self, param_name, priority, choice_type, default_choice, display_bypass, value): # The `value` parameter comes from the frontend UI on generation. # If it's present, we use it. Otherwise, we use the default set in the node graph. final_value = value if value and value != "None" else default_choice # If the final value is still None or empty, try to get a fallback if not final_value or final_value == "None": if choice_type == "sampler": return (comfy.samplers.KSampler.SAMPLERS[0],) elif choice_type == "scheduler": return (comfy.samplers.KSampler.SCHEDULERS[0],) else: choices = folder_paths.get_filename_list(choice_type) if choices: return (choices[0],) return (final_value,) class CozyGenJobID: @classmethod def INPUT_TYPES(cls): return {"required": {"job_id": ("STRING", {"default": "", "multiline": False})}} RETURN_TYPES = ("STRING",) FUNCTION = "get_job_id" CATEGORY = "CozyGen/Internal" def get_job_id(self, job_id): return (job_id,) NODE_CLASS_MAPPINGS = { "CozyGenOutput": CozyGenOutput, "CozyGenVideoOutput": CozyGenVideoOutput, "CozyGenDynamicInput": CozyGenDynamicInput, "CozyGenImageInput": CozyGenImageInput, "CozyGenFloatInput": CozyGenFloatInput, "CozyGenIntInput": CozyGenIntInput, "CozyGenStringInput": CozyGenStringInput, "CozyGenChoiceInput": CozyGenChoiceInput, "CozyGenJobID": CozyGenJobID, } NODE_DISPLAY_NAME_MAPPINGS = { "CozyGenOutput": "CozyGen Output", "CozyGenVideoOutput": "CozyGen Video Output", "CozyGenDynamicInput": "CozyGen Dynamic Input", "CozyGenImageInput": "CozyGen Image Input", "CozyGenFloatInput": "CozyGen Float Input", "CozyGenIntInput": "CozyGen Int Input", "CozyGenStringInput": "CozyGen String Input", "CozyGenChoiceInput": "CozyGen Choice Input", }