Files
2025-10-17 17:34:17 -05:00

419 lines
16 KiB
Python

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",
}