Files
melMass-comfy_mtb/nodes/graph_utils.py
T
2023-08-11 22:22:07 +02:00

236 lines
7.1 KiB
Python

from ..log import log
from PIL import Image
import urllib.request
import urllib.parse
import torch
import json
from comfy.cli_args import args
from ..utils import pil2tensor, apply_easing
import io
import numpy as np
def get_image(filename, subfolder, folder_type):
log.debug(f"Getting image {filename} from {subfolder} of {folder_type}")
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
url_values = urllib.parse.urlencode(data)
with urllib.request.urlopen(
f"http://{args.listen}:{args.port}/view?{url_values}"
) as response:
return io.BytesIO(response.read())
class GetBatchFromHistory:
"""Very experimental node to load images from the history of the server.
Queue items without output are ignored in the count."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"enable": ("BOOLEAN", {"default": True}),
"count": ("INT", {"default": 1, "min": 0}),
"offset": ("INT", {"default": 0, "min": -1e9, "max": 1e9}),
"internal_count": ("INT", {"default": 0}),
},
"optional": {
"passthrough_image": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("images",)
CATEGORY = "mtb/animation"
FUNCTION = "load_from_history"
def load_from_history(
self,
enable=True,
count=0,
offset=0,
internal_count=0, # hacky way to invalidate the node
passthrough_image=None,
):
if not enable or count == 0:
if passthrough_image is not None:
log.debug("Using passthrough image")
return (passthrough_image,)
log.debug("Load from history is disabled for this iteration")
return (torch.zeros(0),)
frames = []
with urllib.request.urlopen(
f"http://{args.listen}:{args.port}/history"
) as response:
return self.load_batch_frames(response, offset, count, frames)
def load_batch_frames(self, response, offset, count, frames):
history = json.loads(response.read())
output_images = []
for run in history.values():
for node_output in run["outputs"].values():
if "images" in node_output:
for image in node_output["images"]:
image_data = get_image(
image["filename"], image["subfolder"], image["type"]
)
output_images.append(image_data)
if not output_images:
return (torch.zeros(0),)
# Directly get desired range of images
start_index = max(len(output_images) - offset - count, 0)
end_index = len(output_images) - offset
selected_images = output_images[start_index:end_index]
frames = [Image.open(image) for image in selected_images]
if not frames:
return (torch.zeros(0),)
elif len(frames) != count:
log.warning(f"Expected {count} images, got {len(frames)} instead")
output = pil2tensor(frames)
return (output,)
class AnyToString:
"""Tries to take any input and convert it to a string"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {"input": ("*")},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "do_str"
CATEGORY = "mtb/converters"
def do_str(self, input):
if isinstance(input, str):
return (input,)
elif isinstance(input, torch.Tensor):
return (f"Tensor of shape {input.shape} and dtype {input.dtype}",)
elif isinstance(input, Image.Image):
return (f"PIL Image of size {input.size} and mode {input.mode}",)
elif isinstance(input, np.ndarray):
return (f"Numpy array of shape {input.shape} and dtype {input.dtype}",)
elif isinstance(input, dict):
return (f"Dictionary of {len(input)} items, with keys {input.keys()}",)
else:
log.debug(f"Falling back to string conversion of {input}")
return (str(input),)
class StringReplace:
"""Basic string replacement"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"string": ("STRING", {"forceInput": True}),
"old": ("STRING", {"default": ""}),
"new": ("STRING", {"default": ""}),
}
}
FUNCTION = "replace_str"
RETURN_TYPES = ("STRING",)
CATEGORY = "mtb/string"
def replace_str(self, string: str, old: str, new: str):
log.debug(f"Current string: {string}")
log.debug(f"Find string: {old}")
log.debug(f"Replace string: {new}")
string = string.replace(old, new)
log.debug(f"New string: {string}")
return (string,)
class FitNumber:
"""Fit the input float using a source and target range"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("FLOAT", {"default": 0, "forceInput": True}),
"clamp": ("BOOLEAN", {"default": False}),
"source_min": ("FLOAT", {"default": 0.0}),
"source_max": ("FLOAT", {"default": 1.0}),
"target_min": ("FLOAT", {"default": 0.0}),
"target_max": ("FLOAT", {"default": 1.0}),
"easing": (
[
"Linear",
"Sine In",
"Sine Out",
"Sine In/Out",
"Quart In",
"Quart Out",
"Quart In/Out",
"Cubic In",
"Cubic Out",
"Cubic In/Out",
"Circ In",
"Circ Out",
"Circ In/Out",
"Back In",
"Back Out",
"Back In/Out",
"Elastic In",
"Elastic Out",
"Elastic In/Out",
"Bounce In",
"Bounce Out",
"Bounce In/Out",
],
{"default": "Linear"},
),
}
}
FUNCTION = "set_range"
RETURN_TYPES = ("FLOAT",)
CATEGORY = "mtb/math"
def set_range(
self,
value: float,
clamp: bool,
source_min: float,
source_max: float,
target_min: float,
target_max: float,
easing: str,
):
normalized_value = (value - source_min) / (source_max - source_min)
eased_value = apply_easing(normalized_value, easing)
# - Convert the eased value to the target range
res = target_min + (target_max - target_min) * eased_value
if clamp:
if target_min > target_max:
res = max(min(res, target_min), target_max)
else:
res = max(min(res, target_max), target_min)
return (res,)
__nodes__ = [StringReplace, FitNumber, GetBatchFromHistory, AnyToString]