import os import re import torch from torchvision.transforms import InterpolationMode import torchvision.transforms.v2.functional as F import torchvision as tv from scipy import ndimage import numpy as np import node_helpers import folder_paths import comfy class PercentPadding: @classmethod def INPUT_TYPES(cls): return { 'required': { 'image': ('IMAGE',), 'left': ('FLOAT', {'min': 0, 'step': .01, 'default': 0}), 'top': ('FLOAT', {'min': 0, 'step': .01, 'default': 0}), 'right': ('FLOAT', {'min': 0, 'step': .01, 'default': 0}), 'bottom': ('FLOAT', {'min': 0, 'step': .01, 'default': 0}), } } RETURN_TYPES = ('IMAGE',) FUNCTION = 'run' CATEGORY = 'davcha' def run(self, image, left, top, right, bottom): image = image.permute(0,3,1,2) b, c, h, w = image.shape left, right = [int(x * w) for x in [left, right]] top, bottom = [int(x * h) for x in [top, bottom]] mask = torch.ones(b, h, w, dtype=torch.float32) mask = F.pad(mask, (left, top, right, bottom), 0, padding_mode='constant') dist = ndimage.distance_transform_edt(mask) soft_m = np.minimum(dist / 32, 1) soft_m = torch.from_numpy(soft_m).type(torch.float32) mask = soft_m image = F.pad(image, (left, top, right, bottom), 0.5, padding_mode='constant') if c != 4: image = torch.cat((image, mask.unsqueeze(1)), 1) else: image[:,3,...] *= mask image = image.permute(0,2,3,1) return (image, ) class StringScheduleHelper: @classmethod def INPUT_TYPES(cls): return { 'required': { 'text': ('STRING', {'multiline': True, 'dynamicPrompts': True}), } } RETURN_NAMES = ('base_prompt', 'schedule', 'has_schedule') RETURN_TYPES = ('STRING', 'STRING', 'BOOLEAN') FUNCTION = 'run' CATEGORY = 'davcha' def run(self, text): base = [] sched = [] for a,b,c in re.findall(r"(\d+):(.+)|(.+)", text): if a == '': base.append(c.strip()) else: sched.append(f'"{a}": "{b.strip()}"') base_prompt = '\n'.join(base) schedule = ',\n'.join(sched) has_schedule = len(sched) > 0 if not has_schedule: schedule = '"0": ""' return (base_prompt, schedule, has_schedule) class SmartMask: @classmethod def INPUT_TYPES(cls): return { 'required': { 'mask': ('MASK',), } } RETURN_TYPES = ('MASK',) FUNCTION = 'mask' CATEGORY = 'davcha' def mask(self, mask): if torch.sum(mask) > 0: return (mask, ) else: return (1 - mask, ) class ResizeCropFit: RETURN_TYPES = ('IMAGE',) FUNCTION = 'run' CATEGORY = 'davcha' @classmethod def INPUT_TYPES(cls): return { 'required': { 'image': ('IMAGE',), 'latent': ('LATENT',), 'mode': (['resize', 'crop', 'fit'],) } } def run(self, image, latent, mode): image = image.permute(0,3,1,2) b, c, sh, sw = image.shape _, _, th, tw = latent['samples'].shape th, tw = th * 8, tw * 8 if (th, tw) == (sh, sw): return (image.permute(0,2,3,1), ) if mode == 'resize': result = F.resize(image, (th, tw), InterpolationMode.NEAREST_EXACT) else: fun = max if mode == 'crop' else min scale = fun(torch.tensor((th/sh,tw/sw))) shape = tuple(int(x * scale) for x in [sh,sw]) mask = torch.ones((1, sh, sw)) mask = F.resize(mask, shape, InterpolationMode.NEAREST_EXACT) result = F.resize(image, shape, InterpolationMode.NEAREST_EXACT) phw = ( (tw - int(sw*scale))//2, (th - int(sh*scale))//2, (tw - int(sw*scale)+1)//2, (th - int(sh*scale)+1)//2, ) mask = F.pad(mask, phw, fill=0, padding_mode='constant') dist = ndimage.distance_transform_edt(mask) soft_m = np.minimum(dist / 32, 1) soft_m = torch.from_numpy(soft_m).type(torch.float32) mask = soft_m result = F.pad(result, phw, fill=0.5, padding_mode='constant') if c != 4: result = torch.cat((result, mask.unsqueeze(1)), 1) # result[:,3,:,:] = 1 else: result[:,3,:,:] *= mask result = result.permute(0,2,3,1) return (result, ) class SoftErosion: RETURN_TYPES = ('MASK',) FUNCTION = 'run' CATEGORY = 'davcha' @classmethod def INPUT_TYPES(cls): return { 'required': { 'mask': ('MASK',), 'size': ('INT', {'min': 0, 'default': 5}), } } def run(self, mask, size): if size == 0: return (mask, ) soft_mask = [] for m in mask: dist = ndimage.distance_transform_edt(m) soft_m = np.minimum(dist / size, 1) soft_m = torch.from_numpy(soft_m) soft_mask.append(soft_m) soft_mask = torch.from_numpy(np.array(soft_mask)).type(torch.FloatTensor) return (soft_mask, ) class ApplyMask: RETURN_TYPES = ('IMAGE',) FUNCTION = 'run' CATEGORY = 'davcha' @classmethod def INPUT_TYPES(cls): return { 'required': { 'image': ('IMAGE',), 'mask': ('MASK',), } } def run(self, image, mask): image = image.permute(0,3,1,2) _, _, h, w = image.shape mask = F.resize(mask, (h, w), InterpolationMode.BICUBIC) image = image.permute(0,2,3,1)[:,:,:,:3] image *= mask[...,None] return (image, ) class DStack: RETURN_TYPES = ('IMAGE',) FUNCTION = 'run' CATEGORY = 'davcha' @classmethod def INPUT_TYPES(cls): return { 'required': { 'image1': ('IMAGE',), 'image2': ('IMAGE',), } } def run(self, image1, image2): stack = torch.dstack((image1, image2)) return (stack, ) ratio_merge = ("FLOAT", {"default": 1.0, "min": -5.0, "max": 6.0, "step": 0.001}) class DavchaConditioningConcat: @classmethod def INPUT_TYPES(s): return {"required": { "conditioning_to": ("CONDITIONING",), "conditioning_from": ("CONDITIONING",), }} RETURN_TYPES = ("CONDITIONING",) FUNCTION = "concat" CATEGORY = "conditioning" def concat(self, conditioning_to, conditioning_from): out = [] if len(conditioning_from) < 1: return (conditioning_to,) if len(conditioning_to) < 1: return (conditioning_from,) cond_from = conditioning_from[0][0].to(conditioning_to[0][0].device) for i in range(len(conditioning_to)): t1 = conditioning_to[i][0] mb = max(t1.shape[0], cond_from.shape[0]) tw = torch.cat((t1.repeat(mb // t1.shape[0],1,1), cond_from.repeat(mb // cond_from.shape[0],1,1)), 1) n = [tw, conditioning_to[i][1].copy()] out.append(n) return (out, ) class DavchaModelMergeSimple: @classmethod def INPUT_TYPES(s): return {"required": { "model1": ("MODEL",), "model2": ("MODEL",), "ratio": ratio_merge, }} RETURN_TYPES = ("MODEL",) FUNCTION = "merge" CATEGORY = "advanced/model_merging" def merge(self, model1, model2, ratio): m = model1.clone() kp = model2.get_key_patches("diffusion_model.") for k in kp: m.add_patches({k: kp[k]}, 1.0 - ratio, ratio) return (m, ) class DavchaCLIPMergeSimple: @classmethod def INPUT_TYPES(s): return {"required": { "clip1": ("CLIP",), "clip2": ("CLIP",), "ratio": ratio_merge, }} RETURN_TYPES = ("CLIP",) FUNCTION = "merge" CATEGORY = "advanced/model_merging" def merge(self, clip1, clip2, ratio): m = clip1.clone() kp = clip2.get_key_patches() for k in kp: if k.endswith(".position_ids") or k.endswith(".logit_scale"): continue m.add_patches({k: kp[k]}, 1.0 - ratio, ratio) return (m, ) import comfy_extras.nodes_model_merging class DavchaModelMergeSD1(comfy_extras.nodes_model_merging.ModelMergeBlocks): CATEGORY = "advanced/model_merging/model_specific" @classmethod def INPUT_TYPES(s): arg_dict = { "model1": ("MODEL",), "model2": ("MODEL",)} argument = ratio_merge arg_dict["time_embed."] = argument arg_dict["label_emb."] = argument for i in range(12): arg_dict["input_blocks.{}.".format(i)] = argument for i in range(3): arg_dict["middle_block.{}.".format(i)] = argument for i in range(12): arg_dict["output_blocks.{}.".format(i)] = argument arg_dict["out."] = argument return {"required": arg_dict} class DavchaModelMergeSDXL(comfy_extras.nodes_model_merging.ModelMergeBlocks): CATEGORY = "advanced/model_merging/model_specific" @classmethod def INPUT_TYPES(s): arg_dict = { "model1": ("MODEL",), "model2": ("MODEL",)} argument = ratio_merge arg_dict["time_embed."] = argument arg_dict["label_emb."] = argument for i in range(9): arg_dict["input_blocks.{}".format(i)] = argument for i in range(3): arg_dict["middle_block.{}".format(i)] = argument for i in range(9): arg_dict["output_blocks.{}".format(i)] = argument arg_dict["out."] = argument return {"required": arg_dict} class ConditioningCompress: CATEGORY = "davcha" @classmethod def INPUT_TYPES(s): return {"required": { "conditioning": ("CONDITIONING",), "max_tokens": ("INT", {"default": 75, "min": 1, "step": 1}) }} RETURN_TYPES = ("CONDITIONING",) FUNCTION = "run" def run(self, conditioning, max_tokens): def reduc(o): u,s,v = torch.svd(o) return (u[0,:max_tokens,:max_tokens]@s[0,:max_tokens].diag()@v[0,:,:max_tokens].T)[None] out = [[c[0].clone(), c[1]] for c in conditioning] for o in out: o[0] = reduc(o[0]) return (out, ) ################ DavchaCLIPTextEncode import regex from itertools import product def parse(text): def _inner(text, start=0, end=1): matches = regex.findall(r'(\[((?:(?:embedding:)?[^\[\]])*?):((?:(?:embedding:)?[^\[\]])*?):(\d*\.?\d+)\])', text, regex.DOTALL) if len(matches) == 0: return [(text, (start, end))] x = [m[1:3] for m in matches] prod = list(product(*x)) results = {} for p in prod: if p not in results: results[p] = [] for f, b, a, t in matches: t = float(t) if b in p: results[p].append((f, b, (start, t))) if a in p: results[p].append((f, a, (t, end))) final = [] for items in results.values(): txt = text st, en = start, end for f, r, (s, e) in items: txt = txt.replace(f, r) st = max(st, s) en = min(en, e) if st <= en: for prompt, (a, b) in _inner(txt, st, en): final.append((prompt, (a, b))) return final candidates = _inner(text) dic = {} for prompt, (s, e) in candidates: if prompt not in dic: dic[prompt] = (s, e) else: dic[prompt] = (min(s, dic[prompt][0]), max(e, dic[prompt][1])) return [(prompt, (s,e)) for prompt, (s, e) in dic.items()] class DavchaCLIPTextEncode: @classmethod def INPUT_TYPES(s): return { "required": { "text": ("STRING", {"multiline": True, "dynamicPrompts": True, "tooltip": "The text to be encoded."}), "clip": ("CLIP", {"tooltip": "The CLIP model used for encoding the text."}) } } RETURN_TYPES = ("CONDITIONING",) OUTPUT_IS_LIST = (True,) FUNCTION = "run" CATEGORY = "davcha" def run(self, clip, text): text = text.split('|') result = [] for txt in text: texts = re.split(r"\b(AREA\(\s*(?:\d*\.?\d+\s*){4,5}\))", txt) areas = [] current_area = None for item in texts: if item.startswith('AREA'): current_area = item else: prompt = parse(item) if current_area is None: areas.append((prompt, None)) else: values = re.findall(r"(\d*\.?\d+)", current_area) x, y, w, h, s = [float(i) for i in values + [1.0] * (5 - len(values))] areas.append((prompt, (x, y, w, h, s))) current_area = None for schedule, area in areas: for prompt, (start, end) in schedule: print(f'{area} {start}-{end}: {prompt}') cs = [] for prompts, area in areas: for txt, (lb, ub) in prompts: if len(txt) > 0: tokens = clip.tokenize(txt) output = clip.encode_from_tokens(tokens, return_pooled=True, return_dict=True) cond = output.pop("cond") c = [[cond, output]] c = node_helpers.conditioning_set_values(c, {"start_percent": lb, "end_percent": ub}) if area is not None: x, y, w, h, s = area c = node_helpers.conditioning_set_values(c, {"area": ("percentage", h, w, y, x), "strength": s, "set_area_to_bounds": False}) cs += c result.append(cs) return (result, ) from pathlib import Path from webp import WebPData, WebPAnimDecoderOptions, WebPAnimDecoder, mimread def video_reader(file, start=0, count=0, target_fps=0): if Path(file).suffix.lower() == '.webp': return _webp_reader(file, start, count, target_fps) else: return _ffmpeg_reader(file, start, count, target_fps) def _webp_reader(file, start, count, target_fps): with open(file, 'rb') as f: webp_data = WebPData.from_buffer(f.read()) dec_opts = WebPAnimDecoderOptions.new(use_threads=True) dec = WebPAnimDecoder.new(webp_data, dec_opts) eps = 1e-7 frames_data = list(dec.frames()) frames = [arr[:,:,:3] for arr, _ in frames_data] fps = 1000 * len(frames_data) / frames_data[-1][1] if target_fps > 0: if fps > target_fps: frames = [frame[:,:,:3] for frame in mimread(file, fps=target_fps)] else: target_fps = fps if count > 0: frames = frames[int(start):int(start+count)] else: frames = frames[int(start):] frames = torch.from_numpy(np.array(frames)).float() / 255.0 return frames, target_fps import cv2 import numpy as np def _ffmpeg_reader(file, start, count, target_fps): cap = cv2.VideoCapture(file) # Get video properties fps = int(cap.get(cv2.CAP_PROP_FPS)) frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) if target_fps == 0: target_fps = fps start_frame = int(min(max(0, start), frame_count - 1) * fps / target_fps) if count == 0: count = int(frame_count * target_fps / fps) count = min(int(frame_count * target_fps / fps - start), count) frames = np.empty((count, frame_height, frame_width, 3), dtype=np.uint8) for i in range(count): cap.set(cv2.CAP_PROP_POS_FRAMES, int(start_frame + i * fps / target_fps)) ret, frame = cap.read() if not ret: break frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) frames[i] = frame_rgb cap.release() frames = torch.from_numpy(frames).float() / 255.0 return frames, target_fps class DavchaLoadVideo: @classmethod def INPUT_TYPES(s): return { "required": { "path": ("STRING", {"multiline": False}), "start": ("INT", {"default": 0, "min": 0, "step": 1}), "count": ("INT", {"default": 0, "min": 0, "step": 1}), "target_fps": ("FLOAT", {"default": 0, "min": 0, "max": 60.0, "step": 0.01}), } } RETURN_TYPES = ("IMAGE", "INT", "FLOAT") RETURN_NAMES = ("frames", "frame_count", "fps") FUNCTION = "run" CATEGORY = "davcha" def run(self, path, start, count, target_fps): fullpath = os.path.join(folder_paths.get_input_directory(), path) frames, fps = video_reader(fullpath, start, count, target_fps) return (frames, len(frames), fps) MAX_RESOLUTION = 16384 class AnyType(str): def __eq__(self, _) -> bool: return True def __ne__(self, __value: object) -> bool: return False class DavchaEmptyLatentImage: def __init__(self): self.device = comfy.model_management.intermediate_device() @classmethod def INPUT_TYPES(s): return { "required": { "width": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8, "tooltip": "The width of the latent images in pixels."}), "height": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8, "tooltip": "The height of the latent images in pixels."}), "option": ([AnyType("*")], ), "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096, "tooltip": "The number of latent images in the batch."}) } } RETURN_TYPES = ("LATENT", "FLOAT", "INT") RETURN_NAMES = ("latent", "upscale_factor", "batch_size") OUTPUT_TOOLTIPS = ("The empty latent image batch.",) FUNCTION = "generate" CATEGORY = "davcha" DESCRIPTION = "Create a new batch of empty latent images to be denoised via sampling." def generate(self, width, height, option, batch_size=1): width, height, upscale_factor = re.findall("(\d+)x(\d+): (\d+\.?\d*)", option)[0] width, height, upscale_factor = int(width), int(height), float(upscale_factor) latent = torch.zeros([batch_size, 4, height // 8, width // 8], device=self.device) return ({"samples":latent}, upscale_factor, batch_size) class DavchaMaskImage: @classmethod def INPUT_TYPES(s): return {"required": { "image": ("IMAGE",), "mask": ("MASK",), }} RETURN_TYPES = ("IMAGE",) FUNCTION = "run" CATEGORY = "davcha" def run(self, image, mask): b, w, h, c = image.shape mask = F.resize(mask, (w, h), InterpolationMode.BICUBIC) image = image.permute(0, 3, 1, 2) if c != 4: image = torch.cat((image, torch.ones((b, 1, w, h), dtype=torch.float32)), 1)# mask.unsqueeze(1)), 1) image = image * mask.unsqueeze(1) image = image.permute(0, 2, 3, 1) return (image, ) class DavchaPop: @classmethod def INPUT_TYPES(s): return {"required": { "items": (AnyType("*"),), }} INPUT_IS_LIST = (True, ) OUTPUT_IS_LIST = (False, True) RETURN_TYPES = (AnyType("*"), AnyType("*")) FUNCTION = "run" CATEGORY = "davcha" def run(self, items): return (items[0], items[1:]) NODE_CLASS_MAPPINGS = { 'SmartMask': SmartMask, 'ResizeCropFit': ResizeCropFit, 'PercentPadding': PercentPadding, 'SoftErosion': SoftErosion, 'ApplyMask': ApplyMask, 'StringScheduleHelper': StringScheduleHelper, 'DStack': DStack, 'DavchaConditioningConcat': DavchaConditioningConcat, 'DavchaModelMergeSimple' : DavchaModelMergeSimple, 'DavchaCLIPMergeSimple' : DavchaCLIPMergeSimple, 'DavchaModelMergeSD1' : DavchaModelMergeSD1, 'DavchaModelMergeSDXL' : DavchaModelMergeSDXL, 'ConditioningCompress': ConditioningCompress, 'DavchaCLIPTextEncode': DavchaCLIPTextEncode, 'DavchaLoadVideo': DavchaLoadVideo, 'DavchaEmptyLatentImage': DavchaEmptyLatentImage, 'DavchaMaskImage': DavchaMaskImage, 'DavchaPop': DavchaPop, } NODE_DISPLAY_NAME_MAPPINGS = { 'SmartMask': 'SmartMask', 'ResizeCropFit': 'Resize, Crop or Fit', 'PercentPadding': 'Percent Padding', 'SoftErosion': 'SoftErosion', 'ApplyMask': 'ApplyMask', 'StringScheduleHelper': 'StringScheduleHelper', 'DStack': 'DStack', 'DavchaConditioningConcat': 'DavchaConditioningConcat', 'DavchaModelMergeSimple': 'DavchaModelMergeSimple', 'DavchaCLIPMergeSimple': 'DavchaCLIPMergeSimple', 'DavchaModelMergeSD1': 'DavchaModelMergeSD1', 'DavchaModelMergeSDXL': 'DavchaModelMergeSDXL', 'ConditioningCompress': 'ConditioningCompress', 'DavchaCLIPTextEncode': 'CLIP Text Encode (Davcha)', 'DavchaLoadVideo': 'Load Video (Davcha)', 'DavchaEmptyLatentImage': 'Empty Latent Image (Davcha)', 'DavchaMaskImage': 'Mask Image (Davcha)', 'DavchaPop': 'Pop', }