├── .github └── workflows │ └── publish.yml ├── .gitignore ├── __init__.py ├── base.py ├── calculate.py ├── categories.py ├── colors.py ├── curves.py ├── disable.py ├── dreamlogger.py ├── dreamtypes.py ├── embedded_config.py ├── enable.py ├── err.py ├── examples ├── area-sampled-noise.json ├── laboratory.json ├── motion-workflow-example.json ├── motion-workflow-with-color-coherence.json ├── prompt-morphing.json └── test_colors.png ├── image_processing.py ├── inputfields.py ├── install.py ├── laboratory.py ├── lazyswitches.py ├── license.txt ├── loaders.py ├── node_list.json ├── noise.py ├── output.py ├── prompting.py ├── pyproject.toml ├── readme.md ├── requirements.txt ├── seq_processing.py ├── shared.py ├── switches.py ├── uninstall.py └── utility.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - "pyproject.toml" 9 | 10 | jobs: 11 | publish-node: 12 | name: Publish Custom Node to registry 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out code 16 | uses: actions/checkout@v4 17 | - name: Publish Custom Node 18 | uses: Comfy-Org/publish-node-action@main 19 | with: 20 | ## Add your own personal access token to your Github Repository secrets and reference it here. 21 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__ 3 | config.json 4 | *.cmd -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import Type 3 | import sys,os 4 | 5 | sys.path.append(str(os.path.dirname(os.path.abspath(__file__)))) 6 | 7 | from .base import * 8 | from .colors import * 9 | from .curves import * 10 | from .image_processing import * 11 | from .inputfields import * 12 | from .loaders import * 13 | from .noise import * 14 | from .output import * 15 | from .prompting import * 16 | from .seq_processing import * 17 | from .switches import * 18 | from .utility import * 19 | from .calculate import * 20 | from .laboratory import * 21 | #from .lazyswitches import * 22 | 23 | _NODE_CLASSES: List[Type] = [DreamSineWave, DreamLinear, DreamCSVCurve, DreamBeatCurve, DreamFrameDimensions, 24 | DreamImageMotion, DreamNoiseFromPalette, DreamAnalyzePalette, DreamColorShift, 25 | DreamDirectoryFileCount, DreamFrameCounterOffset, DreamDirectoryBackedFrameCounter, 26 | DreamSimpleFrameCounter, DreamImageSequenceInputWithDefaultFallback, 27 | DreamImageSequenceOutput, DreamCSVGenerator, DreamImageAreaSampler, 28 | DreamVideoEncoder, DreamSequenceTweening, DreamSequenceBlend, DreamColorAlign, 29 | DreamImageSampler, DreamNoiseFromAreaPalettes, 30 | DreamInputString, DreamInputFloat, DreamInputInt, DreamInputText, DreamBigLatentSwitch, 31 | DreamFrameCountCalculator, DreamBigImageSwitch, DreamBigTextSwitch, DreamBigFloatSwitch, 32 | DreamBigIntSwitch, DreamBigPaletteSwitch, DreamWeightedPromptBuilder, DreamPromptFinalizer, 33 | DreamFrameCounterInfo, DreamBoolToFloat, DreamBoolToInt, DreamSawWave, DreamTriangleWave, 34 | DreamTriangleEvent, DreamSmoothEvent, DreamCalculation, DreamImageColorShift, 35 | DreamComparePalette, DreamImageContrast, DreamImageBrightness, DreamLogFile, 36 | DreamLaboratory, DreamStringToLog, DreamIntToLog, DreamFloatToLog, DreamJoinLog, 37 | DreamStringTokenizer, DreamWavCurve, DreamFrameCounterTimeOffset, DreamRandomPromptWords] 38 | _SIGNATURE_SUFFIX = " [Dream]" 39 | 40 | MANIFEST = { 41 | "name": "Dream Project Animation", 42 | "version": (5, 1, 2), 43 | "author": "Dream Project", 44 | "project": "https://github.com/alt-key-project/comfyui-dream-project", 45 | "description": "Various utility nodes for creating animations with ComfyUI", 46 | } 47 | 48 | NODE_CLASS_MAPPINGS = {} 49 | 50 | NODE_DISPLAY_NAME_MAPPINGS = {} 51 | 52 | config = DreamConfig() 53 | 54 | 55 | def update_category(cls): 56 | top = config.get("ui.top_category", "").strip().strip("/") 57 | leaf_icon = "" 58 | if top and "CATEGORY" in cls.__dict__: 59 | cls.CATEGORY = top + "/" + cls.CATEGORY.lstrip("/") 60 | if "CATEGORY" in cls.__dict__: 61 | joined = [] 62 | for partial in cls.CATEGORY.split("/"): 63 | icon = config.get("ui.category_icons." + partial, "") 64 | if icon: 65 | leaf_icon = icon 66 | if config.get("ui.prepend_icon_to_category", False): 67 | partial = icon.lstrip() + " " + partial 68 | if config.get("ui.append_icon_to_category", False): 69 | partial = partial + " " + icon.rstrip() 70 | joined.append(partial) 71 | cls.CATEGORY = "/".join(joined) 72 | return leaf_icon 73 | 74 | 75 | def update_display_name(cls, category_icon, display_name): 76 | icon = cls.__dict__.get("ICON", category_icon) 77 | if config.get("ui.prepend_icon_to_node", False): 78 | display_name = icon.lstrip() + " " + display_name 79 | if config.get("ui.append_icon_to_node", False): 80 | display_name = display_name + " " + icon.rstrip() 81 | return display_name 82 | 83 | 84 | for cls in _NODE_CLASSES: 85 | category_icon = update_category(cls) 86 | clsname = cls.__name__ 87 | if "NODE_NAME" in cls.__dict__: 88 | node_name = cls.__dict__["NODE_NAME"] + _SIGNATURE_SUFFIX 89 | NODE_CLASS_MAPPINGS[node_name] = cls 90 | NODE_DISPLAY_NAME_MAPPINGS[node_name] = update_display_name(cls, category_icon, 91 | cls.__dict__.get("DISPLAY_NAME", 92 | cls.__dict__["NODE_NAME"])) 93 | else: 94 | raise Exception("Class {} is missing NODE_NAME!".format(str(cls))) 95 | 96 | 97 | def update_node_index(): 98 | node_list_path = os.path.join(os.path.dirname(__file__), "node_list.json") 99 | with open(node_list_path) as f: 100 | node_list = json.loads(f.read()) 101 | updated = False 102 | for nodename in NODE_CLASS_MAPPINGS.keys(): 103 | if nodename not in node_list: 104 | node_list[nodename] = "" 105 | updated = True 106 | if updated or True: 107 | with open(node_list_path, "w") as f: 108 | f.write(json.dumps(node_list, indent=2, sort_keys=True)) 109 | 110 | 111 | update_node_index() 112 | -------------------------------------------------------------------------------- /base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import glob 3 | 4 | from .categories import NodeCategories 5 | from .shared import * 6 | from .dreamtypes import * 7 | 8 | 9 | class DreamFrameCounterInfo: 10 | NODE_NAME = "Frame Counter Info" 11 | ICON = "⚋" 12 | 13 | @classmethod 14 | def INPUT_TYPES(cls): 15 | return { 16 | "required": SharedTypes.frame_counter 17 | } 18 | 19 | CATEGORY = NodeCategories.ANIMATION 20 | RETURN_TYPES = ("INT", "INT", "BOOLEAN", "BOOLEAN", "FLOAT", "FLOAT", "FLOAT", "FLOAT") 21 | RETURN_NAMES = ("frames_completed", "total_frames", "first_frame", "last_frame", 22 | "elapsed_seconds", "remaining_seconds", "total_seconds", "completion") 23 | FUNCTION = "result" 24 | 25 | def result(self, frame_counter: FrameCounter): 26 | return (frame_counter.current_frame, 27 | frame_counter.total_frames, 28 | frame_counter.is_first_frame, 29 | frame_counter.is_final_frame, 30 | frame_counter.current_time_in_seconds, 31 | frame_counter.remaining_time_in_seconds, 32 | frame_counter.total_time_in_seconds, 33 | frame_counter.current_time_in_seconds / max(0.01, frame_counter.total_time_in_seconds)) 34 | 35 | 36 | class DreamDirectoryFileCount: 37 | NODE_NAME = "File Count" 38 | ICON = "📂" 39 | 40 | @classmethod 41 | def INPUT_TYPES(cls): 42 | return { 43 | "required": { 44 | "directory_path": ("STRING", {"default": '', "multiline": False}), 45 | "patterns": ("STRING", {"default": '*.jpg|*.png|*.jpeg', "multiline": False}), 46 | }, 47 | } 48 | 49 | CATEGORY = NodeCategories.ANIMATION 50 | RETURN_TYPES = ("INT",) 51 | RETURN_NAMES = ("TOTAL",) 52 | FUNCTION = "result" 53 | 54 | @classmethod 55 | def IS_CHANGED(cls, directory_path, patterns): 56 | if not os.path.isdir(directory_path): 57 | return "" 58 | total = 0 59 | for pattern in patterns.split("|"): 60 | files = list(glob.glob(pattern, root_dir=directory_path)) 61 | total += len(files) 62 | return total 63 | 64 | def result(self, directory_path, patterns): 65 | if not os.path.isdir(directory_path): 66 | return (0,) 67 | total = 0 68 | for pattern in patterns.split("|"): 69 | files = list(glob.glob(pattern, root_dir=directory_path)) 70 | total += len(files) 71 | return (total,) 72 | 73 | 74 | class DreamFrameCounterOffset: 75 | NODE_NAME = "Frame Counter Offset" 76 | 77 | ICON = "±" 78 | 79 | @classmethod 80 | def INPUT_TYPES(cls): 81 | return { 82 | "required": SharedTypes.frame_counter | { 83 | "offset": ("INT", {"default": -1}), 84 | }, 85 | } 86 | 87 | CATEGORY = NodeCategories.ANIMATION 88 | RETURN_TYPES = (FrameCounter.ID,) 89 | RETURN_NAMES = ("frame_counter",) 90 | FUNCTION = "result" 91 | 92 | def result(self, frame_counter: FrameCounter, offset): 93 | return (frame_counter.incremented(offset),) 94 | 95 | class DreamFrameCounterTimeOffset: 96 | NODE_NAME = "Frame Counter Time Offset" 97 | 98 | ICON = "±" 99 | 100 | @classmethod 101 | def INPUT_TYPES(cls): 102 | return { 103 | "required": SharedTypes.frame_counter | { 104 | "offset_seconds": ("FLOAT", {"default": 0.0}), 105 | }, 106 | } 107 | 108 | CATEGORY = NodeCategories.ANIMATION 109 | RETURN_TYPES = (FrameCounter.ID,) 110 | RETURN_NAMES = ("frame_counter",) 111 | FUNCTION = "result" 112 | 113 | def result(self, frame_counter: FrameCounter, offset_seconds): 114 | offset = offset_seconds * frame_counter.frames_per_second 115 | return (frame_counter.incremented(offset),) 116 | 117 | 118 | class DreamSimpleFrameCounter: 119 | NODE_NAME = "Frame Counter (Simple)" 120 | ICON = "⚋" 121 | 122 | @classmethod 123 | def INPUT_TYPES(cls): 124 | return { 125 | "required": { 126 | "frame_index": ("INT", {"min": 0, "default": 0}), 127 | "total_frames": ("INT", {"default": 100, "min": 1, "max": 24 * 3600 * 60}), 128 | "frames_per_second": ("INT", {"min": 1, "default": 25}), 129 | }, 130 | } 131 | 132 | CATEGORY = NodeCategories.ANIMATION 133 | RETURN_TYPES = (FrameCounter.ID,) 134 | RETURN_NAMES = ("frame_counter",) 135 | FUNCTION = "result" 136 | 137 | def result(self, frame_index, total_frames, frames_per_second): 138 | n = frame_index 139 | return (FrameCounter(n, total_frames, frames_per_second),) 140 | 141 | 142 | class DreamDirectoryBackedFrameCounter: 143 | NODE_NAME = "Frame Counter (Directory)" 144 | ICON = "⚋" 145 | 146 | @classmethod 147 | def INPUT_TYPES(cls): 148 | return { 149 | "required": { 150 | "directory_path": ("STRING", {"default": '', "multiline": False}), 151 | "pattern": ("STRING", {"default": '*', "multiline": False}), 152 | "indexing": (["numeric", "alphabetic order"],), 153 | "total_frames": ("INT", {"default": 100, "min": 2, "max": 24 * 3600 * 60}), 154 | "frames_per_second": ("INT", {"min": 1, "default": 30}), 155 | }, 156 | } 157 | 158 | CATEGORY = NodeCategories.ANIMATION 159 | RETURN_TYPES = (FrameCounter.ID,) 160 | RETURN_NAMES = ("frame_counter",) 161 | FUNCTION = "result" 162 | 163 | @classmethod 164 | def IS_CHANGED(cls, directory_path, patterns, indexing, total_frames, frames_per_second): 165 | if not os.path.isdir(directory_path): 166 | return "" 167 | total = 0 168 | for pattern in patterns.split("|"): 169 | files = list(glob.glob(pattern, root_dir=directory_path)) 170 | total += len(files) 171 | return (total, indexing, total_frames, frames_per_second) 172 | 173 | def result(self, directory_path, pattern, indexing, total_frames, frames_per_second): 174 | results = list_images_in_directory(directory_path, pattern, indexing == "alphabetic order") 175 | if not results: 176 | return (FrameCounter(0, total_frames, frames_per_second),) 177 | n = max(results.keys()) + 1 178 | return (FrameCounter(n, total_frames, frames_per_second),) 179 | 180 | 181 | class DreamFrameCountCalculator: 182 | NODE_NAME = "Frame Count Calculator" 183 | ICON = "⌛" 184 | 185 | @classmethod 186 | def INPUT_TYPES(cls): 187 | return { 188 | "required": { 189 | "hours": ("INT", {"min": 0, "default": 0, "max": 23}), 190 | "minutes": ("INT", {"min": 0, "default": 0, "max": 59}), 191 | "seconds": ("INT", {"min": 0, "default": 10, "max": 59}), 192 | "milliseconds": ("INT", {"min": 0, "default": 0, "max": 59}), 193 | "frames_per_second": ("INT", {"min": 1, "default": 30}) 194 | }, 195 | } 196 | 197 | CATEGORY = NodeCategories.ANIMATION 198 | RETURN_TYPES = ("INT",) 199 | RETURN_NAMES = ("TOTAL",) 200 | FUNCTION = "result" 201 | 202 | def result(self, hours, minutes, seconds, milliseconds, frames_per_second): 203 | total_s = seconds + 0.001 * milliseconds + minutes * 60 + hours * 3600 204 | return (round(total_s * frames_per_second),) 205 | -------------------------------------------------------------------------------- /calculate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import math 3 | 4 | from evalidate import Expr, EvalException, base_eval_model 5 | 6 | from .categories import * 7 | from .err import on_error 8 | from .shared import hashed_as_strings 9 | 10 | 11 | class DreamCalculation: 12 | NODE_NAME = "Calculation" 13 | ICON = "🖩" 14 | 15 | @classmethod 16 | def INPUT_TYPES(cls): 17 | return { 18 | "required": { 19 | "expression": ("STRING", {"default": "a + b + c - (r * s * t)", "multiline": True}) 20 | }, 21 | "optional": { 22 | "a_int": ("INT", {"default": 0, "multiline": False}), 23 | "b_int": ("INT", {"default": 0, "multiline": False}), 24 | "c_int": ("INT", {"default": 0, "multiline": False}), 25 | "r_float": ("FLOAT", {"default": 0.0, "multiline": False}), 26 | "s_float": ("FLOAT", {"default": 0.0, "multiline": False}), 27 | "t_float": ("FLOAT", {"default": 0.0, "multiline": False}) 28 | } 29 | } 30 | 31 | CATEGORY = NodeCategories.UTILS 32 | RETURN_TYPES = ("FLOAT", "INT") 33 | RETURN_NAMES = ("FLOAT", "INT") 34 | FUNCTION = "result" 35 | 36 | def _make_model(self): 37 | funcs = self._make_functions() 38 | m = base_eval_model.clone() 39 | m.nodes.append('Mult') 40 | m.nodes.append('Call') 41 | for funname in funcs.keys(): 42 | m.allowed_functions.append(funname) 43 | return (m, funcs) 44 | 45 | def _make_functions(self): 46 | return { 47 | "round": round, 48 | "float": float, 49 | "int": int, 50 | "abs": abs, 51 | "min": min, 52 | "max": max, 53 | "tan": math.tan, 54 | "tanh": math.tanh, 55 | "sin": math.sin, 56 | "sinh": math.sinh, 57 | "cos": math.cos, 58 | "cosh": math.cosh, 59 | "pow": math.pow, 60 | "sqrt": math.sqrt, 61 | "ceil": math.ceil, 62 | "floor": math.floor, 63 | "pi": math.pi, 64 | "log": math.log, 65 | "log2": math.log2, 66 | "acos": math.acos, 67 | "asin": math.asin, 68 | "acosh": math.acosh, 69 | "asinh": math.asinh, 70 | "atan": math.atan, 71 | "atanh": math.atanh, 72 | "exp": math.exp, 73 | "fmod": math.fmod, 74 | "factorial": math.factorial, 75 | "dist": math.dist, 76 | "atan2": math.atan2, 77 | "log10": math.log10 78 | } 79 | 80 | def result(self, expression, **values): 81 | model, funcs = self._make_model() 82 | vars = funcs 83 | for key in ("a_int", "b_int", "c_int", "r_float", "s_float", "t_float"): 84 | nm = key.split("_")[0] 85 | v = values.get(key, None) 86 | if v is not None: 87 | vars[nm] = v 88 | try: 89 | data = Expr(expression, model=model).eval(vars) 90 | if isinstance(data, (int, float)): 91 | return float(data), int(round(data)) 92 | else: 93 | return 0.0, 0 94 | except EvalException as e: 95 | on_error(DreamCalculation, str(e)) 96 | -------------------------------------------------------------------------------- /categories.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | class NodeCategories: 4 | ANIMATION = "animation" 5 | ANIMATION_POSTPROCESSING = ANIMATION + "/postprocessing" 6 | ANIMATION_TRANSFORMS = ANIMATION + "/transforms" 7 | ANIMATION_CURVES = "animation/curves" 8 | CONDITIONING = "conditioning" 9 | IMAGE_POSTPROCESSING = "image/postprocessing" 10 | IMAGE_ANIMATION = "image/animation" 11 | IMAGE_COLORS = "image/color" 12 | IMAGE_GENERATE = "image/generate" 13 | IMAGE = "image" 14 | UTILS = "utils" 15 | UTILS_SWITCHES = "utils/switches" -------------------------------------------------------------------------------- /colors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .categories import NodeCategories 4 | from .shared import * 5 | from .dreamtypes import * 6 | 7 | 8 | class DreamImageAreaSampler: 9 | NODE_NAME = "Sample Image Area as Palette" 10 | 11 | @classmethod 12 | def INPUT_TYPES(cls): 13 | return { 14 | "required": { 15 | "image": ("IMAGE",), 16 | "samples": ("INT", {"default": 256, "min": 1, "max": 1024 * 4}), 17 | "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), 18 | "area": (["top-left", "top-center", "top-right", 19 | "center-left", "center", "center-right", 20 | "bottom-left", "bottom-center", "bottom-right"],) 21 | }, 22 | } 23 | 24 | CATEGORY = NodeCategories.IMAGE_COLORS 25 | RETURN_TYPES = (RGBPalette.ID,) 26 | RETURN_NAMES = ("palette",) 27 | FUNCTION = "result" 28 | 29 | def _get_pixel_area(self, img: DreamImage, area): 30 | w = img.width 31 | h = img.height 32 | wpart = round(w / 3) 33 | hpart = round(h / 3) 34 | x0 = 0 35 | x1 = wpart - 1 36 | x2 = wpart 37 | x3 = wpart + wpart - 1 38 | x4 = wpart + wpart 39 | x5 = w - 1 40 | y0 = 0 41 | y1 = hpart - 1 42 | y2 = hpart 43 | y3 = hpart + hpart - 1 44 | y4 = hpart + hpart 45 | y5 = h - 1 46 | if area == "center": 47 | return (x2, y2, x3, y3) 48 | elif area == "top-center": 49 | return (x2, y0, x3, y1) 50 | elif area == "bottom-center": 51 | return (x2, y4, x3, y5) 52 | elif area == "center-left": 53 | return (x0, y2, x1, y3) 54 | elif area == "top-left": 55 | return (x0, y0, x1, y1) 56 | elif area == "bottom-left": 57 | return (x0, y4, x1, y5) 58 | elif area == "center-right": 59 | return (x4, y2, x5, y3) 60 | elif area == "top-right": 61 | return (x4, y0, x5, y1) 62 | elif area == "bottom-right": 63 | return (x4, y4, x5, y5) 64 | 65 | def result(self, image, samples, seed, area): 66 | result = list() 67 | r = random.Random() 68 | r.seed(seed) 69 | for data in image: 70 | di = DreamImage(tensor_image=data) 71 | area = self._get_pixel_area(di, area) 72 | 73 | pixels = list() 74 | for i in range(samples): 75 | x = r.randint(area[0], area[2]) 76 | y = r.randint(area[1], area[3]) 77 | pixels.append(di.get_pixel(x, y)) 78 | result.append(RGBPalette(colors=pixels)) 79 | 80 | return (tuple(result),) 81 | 82 | 83 | class DreamImageSampler: 84 | NODE_NAME = "Sample Image as Palette" 85 | 86 | @classmethod 87 | def INPUT_TYPES(cls): 88 | return { 89 | "required": { 90 | "image": ("IMAGE",), 91 | "samples": ("INT", {"default": 1024, "min": 1, "max": 1024 * 4}), 92 | "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}) 93 | }, 94 | } 95 | 96 | CATEGORY = NodeCategories.IMAGE_COLORS 97 | RETURN_TYPES = (RGBPalette.ID,) 98 | RETURN_NAMES = ("palette",) 99 | FUNCTION = "result" 100 | 101 | def result(self, image, samples, seed): 102 | result = list() 103 | r = random.Random() 104 | r.seed(seed) 105 | for data in image: 106 | di = DreamImage(tensor_image=data) 107 | pixels = list() 108 | for i in range(samples): 109 | x = r.randint(0, di.width - 1) 110 | y = r.randint(0, di.height - 1) 111 | pixels.append(di.get_pixel(x, y)) 112 | result.append(RGBPalette(colors=pixels)) 113 | 114 | return (tuple(result),) 115 | 116 | 117 | class DreamColorAlign: 118 | NODE_NAME = "Palette Color Align" 119 | 120 | @classmethod 121 | def INPUT_TYPES(cls): 122 | return { 123 | "required": SharedTypes.palette | { 124 | "target_align": (RGBPalette.ID,), 125 | "alignment_factor": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 10.0, "step": 0.1}), 126 | } 127 | } 128 | 129 | CATEGORY = NodeCategories.IMAGE_COLORS 130 | RETURN_TYPES = (RGBPalette.ID,) 131 | RETURN_NAMES = ("palette",) 132 | FUNCTION = "result" 133 | 134 | def result(self, palette: Tuple[RGBPalette], target_align: Tuple[RGBPalette], alignment_factor: float): 135 | results = list() 136 | 137 | def _limit(c): 138 | return max(min(c, 255), 0) 139 | 140 | for i in range(len(palette)): 141 | p = palette[i] 142 | t = target_align[i] 143 | (_, _, r1, g1, b1) = p.analyze() 144 | (_, _, r2, g2, b2) = t.analyze() 145 | 146 | dr = (r2 - r1) * alignment_factor 147 | dg = (g2 - g1) * alignment_factor 148 | db = (b2 - b1) * alignment_factor 149 | new_pixels = list() 150 | for pixel in p: 151 | r = _limit(round(pixel[0] + (255 * dr))) 152 | g = _limit(round(pixel[1] + (255 * dg))) 153 | b = _limit(round(pixel[1] + (255 * db))) 154 | new_pixels.append((r, g, b)) 155 | results.append(RGBPalette(colors=new_pixels)) 156 | return (tuple(results),) 157 | 158 | 159 | class DreamColorShift: 160 | NODE_NAME = "Palette Color Shift" 161 | 162 | @classmethod 163 | def INPUT_TYPES(cls): 164 | return { 165 | "required": SharedTypes.palette | { 166 | "red_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.1}), 167 | "green_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.1}), 168 | "blue_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.1}), 169 | "fixed_brightness": (["yes", "no"],), 170 | } 171 | } 172 | 173 | CATEGORY = NodeCategories.IMAGE_COLORS 174 | RETURN_TYPES = (RGBPalette.ID,) 175 | RETURN_NAMES = ("palette",) 176 | FUNCTION = "result" 177 | 178 | def result(self, palette, red_multiplier, green_multiplier, blue_multiplier, fixed_brightness): 179 | results = list() 180 | 181 | def _limit(c): 182 | return max(min(c, 255), 0) 183 | 184 | for p in palette: 185 | new_pixels = list() 186 | for pixel in p: 187 | s = pixel[0] + pixel[1] + pixel[2] 188 | r = _limit(round(pixel[0] * red_multiplier)) 189 | g = _limit(round(pixel[1] * green_multiplier)) 190 | b = _limit(round(pixel[2] * blue_multiplier)) 191 | if fixed_brightness == "yes": 192 | brightness_factor = max(s, 1) / float(max(r + g + b, 1)) 193 | r = _limit(round(r * brightness_factor)) 194 | g = _limit(round(g * brightness_factor)) 195 | b = _limit(round(b * brightness_factor)) 196 | 197 | new_pixels.append((r, g, b)) 198 | results.append(RGBPalette(colors=new_pixels)) 199 | return (tuple(results),) 200 | 201 | 202 | class DreamImageColorShift: 203 | NODE_NAME = "Image Color Shift" 204 | ICON = "🖼" 205 | @classmethod 206 | def INPUT_TYPES(cls): 207 | return { 208 | "required": {"image": ("IMAGE",), 209 | "red_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0}), 210 | "green_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0}), 211 | "blue_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0}), 212 | }, 213 | 214 | } 215 | 216 | CATEGORY = NodeCategories.IMAGE_COLORS 217 | RETURN_TYPES = ("IMAGE",) 218 | RETURN_NAMES = ("image",) 219 | FUNCTION = "result" 220 | 221 | def result(self, image, red_multiplier, green_multiplier, blue_multiplier): 222 | proc = DreamImageProcessor(inputs=image) 223 | 224 | def recolor(im: DreamImage, *a, **args): 225 | return (im.adjust_colors(red_multiplier, green_multiplier, blue_multiplier),) 226 | 227 | return proc.process(recolor) 228 | 229 | 230 | class DreamImageBrightness: 231 | NODE_NAME = "Image Brightness Adjustment" 232 | ICON = "☼" 233 | 234 | @classmethod 235 | def INPUT_TYPES(cls): 236 | return { 237 | "required": {"image": ("IMAGE",), 238 | "factor": ("FLOAT", {"default": 1.0, "min": 0.0}), 239 | }, 240 | 241 | } 242 | 243 | CATEGORY = NodeCategories.IMAGE_COLORS 244 | RETURN_TYPES = ("IMAGE",) 245 | RETURN_NAMES = ("image",) 246 | FUNCTION = "result" 247 | 248 | def result(self, image, factor): 249 | proc = DreamImageProcessor(inputs=image) 250 | 251 | def change(im: DreamImage, *a, **args): 252 | return (im.change_brightness(factor),) 253 | 254 | return proc.process(change) 255 | 256 | 257 | class DreamImageContrast: 258 | NODE_NAME = "Image Contrast Adjustment" 259 | ICON = "◐" 260 | 261 | @classmethod 262 | def INPUT_TYPES(cls): 263 | return { 264 | "required": {"image": ("IMAGE",), 265 | "factor": ("FLOAT", {"default": 1.0, "min": 0.0}), 266 | }, 267 | 268 | } 269 | 270 | CATEGORY = NodeCategories.IMAGE_COLORS 271 | RETURN_TYPES = ("IMAGE",) 272 | RETURN_NAMES = ("image",) 273 | FUNCTION = "result" 274 | 275 | def result(self, image, factor): 276 | proc = DreamImageProcessor(inputs=image) 277 | 278 | def change(im: DreamImage, *a, **args): 279 | return (im.change_contrast(factor),) 280 | 281 | return proc.process(change) 282 | 283 | 284 | class DreamComparePalette: 285 | NODE_NAME = "Compare Palettes" 286 | ICON = "📊" 287 | 288 | @classmethod 289 | def INPUT_TYPES(cls): 290 | return { 291 | "required": { 292 | "a": (RGBPalette.ID,), 293 | "b": (RGBPalette.ID,), 294 | }, 295 | } 296 | 297 | CATEGORY = NodeCategories.IMAGE_COLORS 298 | RETURN_TYPES = ("FLOAT", "FLOAT", "FLOAT", "FLOAT") 299 | RETURN_NAMES = ( 300 | "brightness_multiplier", "contrast_multiplier", "red_multiplier", "green_multiplier", "blue_multiplier") 301 | FUNCTION = "result" 302 | 303 | def result(self, a, b): 304 | MIN_VALUE = 1 / 255.0 305 | 306 | brightness = list() 307 | contrasts = list() 308 | reds = list() 309 | greens = list() 310 | blues = list() 311 | 312 | for i in range(min(len(a), len(b))): 313 | (bright, ctr, red, green, blue) = a[i].analyze() 314 | (bright2, ctr2, red2, green2, blue2) = b[i].analyze() 315 | brightness.append(bright2 / max(MIN_VALUE, bright)) 316 | contrasts.append(ctr2 / max(MIN_VALUE, ctr)) 317 | reds.append(red2 / max(MIN_VALUE, red)) 318 | greens.append(green2 / max(MIN_VALUE, green)) 319 | blues.append(blue2 / max(MIN_VALUE, blue)) 320 | 321 | n = len(brightness) 322 | 323 | return (sum(brightness) / n, sum(contrasts) / n, sum(reds) / n, 324 | sum(greens) / n, sum(blues) / n) 325 | 326 | 327 | class DreamAnalyzePalette: 328 | NODE_NAME = "Analyze Palette" 329 | ICON = "📊" 330 | 331 | @classmethod 332 | def INPUT_TYPES(cls): 333 | return { 334 | "required": SharedTypes.palette 335 | , 336 | } 337 | 338 | CATEGORY = NodeCategories.IMAGE_COLORS 339 | RETURN_TYPES = ("FLOAT", "FLOAT", "FLOAT", "FLOAT", "FLOAT") 340 | RETURN_NAMES = ("brightness", "contrast", "redness", "greenness", "blueness") 341 | FUNCTION = "result" 342 | 343 | def result(self, palette): 344 | f = 1.0 / len(palette) 345 | (w, c, r, g, b) = (0, 0, 0, 0, 0) 346 | for p in palette: 347 | (brightness, contrast, red, green, blue) = p.analyze() 348 | w += brightness 349 | c += contrast 350 | r += red 351 | g += green 352 | b += blue 353 | 354 | return w * f, c * f, r * f, g * f, b * f 355 | -------------------------------------------------------------------------------- /curves.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import csv 3 | import functools 4 | import math 5 | import os 6 | 7 | from scipy.io.wavfile import read as wav_read 8 | 9 | from .categories import NodeCategories 10 | from .shared import hashed_as_strings 11 | from .dreamtypes import SharedTypes, FrameCounter 12 | 13 | 14 | def _linear_value_calc(x, x_start, x_end, y_start, y_end): 15 | if x <= x_start: 16 | return y_start 17 | if x >= x_end: 18 | return y_end 19 | dx = max(x_end - x_start, 0.0001) 20 | n = (x - x_start) / dx 21 | return (y_end - y_start) * n + y_start 22 | 23 | 24 | def _curve_result(f: float): 25 | return (f, int(round(f))) 26 | 27 | 28 | class DreamSineWave: 29 | NODE_NAME = "Sine Curve" 30 | 31 | @classmethod 32 | def INPUT_TYPES(cls): 33 | return { 34 | "required": SharedTypes.frame_counter | { 35 | "max_value": ("FLOAT", {"default": 1.0, "multiline": False}), 36 | "min_value": ("FLOAT", {"default": 0.0, "multiline": False}), 37 | "periodicity_seconds": ("FLOAT", {"default": 10.0, "multiline": False, "min": 0.01}), 38 | "phase": ("FLOAT", {"default": 0.0, "multiline": False, "min": -1, "max": 1}), 39 | }, 40 | } 41 | 42 | CATEGORY = NodeCategories.ANIMATION_CURVES 43 | RETURN_TYPES = ("FLOAT", "INT") 44 | RETURN_NAMES = ("FLOAT", "INT") 45 | FUNCTION = "result" 46 | 47 | def result(self, frame_counter: FrameCounter, max_value, min_value, periodicity_seconds, phase): 48 | x = frame_counter.current_time_in_seconds 49 | a = (max_value - min_value) * 0.5 50 | c = phase 51 | b = 2 * math.pi / periodicity_seconds 52 | d = (max_value + min_value) / 2 53 | y = a * math.sin(b * (x + c)) + d 54 | return _curve_result(y) 55 | 56 | 57 | class DreamSawWave: 58 | NODE_NAME = "Saw Curve" 59 | 60 | @classmethod 61 | def INPUT_TYPES(cls): 62 | return { 63 | "required": SharedTypes.frame_counter | { 64 | "max_value": ("FLOAT", {"default": 1.0, "multiline": False}), 65 | "min_value": ("FLOAT", {"default": 0.0, "multiline": False}), 66 | "periodicity_seconds": ("FLOAT", {"default": 10.0, "multiline": False, "min": 0.01}), 67 | "phase": ("FLOAT", {"default": 0.0, "multiline": False, "min": -1, "max": 1}), 68 | }, 69 | } 70 | 71 | CATEGORY = NodeCategories.ANIMATION_CURVES 72 | RETURN_TYPES = ("FLOAT", "INT") 73 | RETURN_NAMES = ("FLOAT", "INT") 74 | FUNCTION = "result" 75 | 76 | def result(self, frame_counter: FrameCounter, max_value, min_value, periodicity_seconds, phase): 77 | x = frame_counter.current_time_in_seconds 78 | x = ((x + periodicity_seconds * phase) % periodicity_seconds) / periodicity_seconds 79 | y = x * (max_value - min_value) + min_value 80 | return _curve_result(y) 81 | 82 | 83 | class DreamTriangleWave: 84 | NODE_NAME = "Triangle Curve" 85 | 86 | @classmethod 87 | def INPUT_TYPES(cls): 88 | return { 89 | "required": SharedTypes.frame_counter | { 90 | "max_value": ("FLOAT", {"default": 1.0, "multiline": False}), 91 | "min_value": ("FLOAT", {"default": 0.0, "multiline": False}), 92 | "periodicity_seconds": ("FLOAT", {"default": 10.0, "multiline": False, "min": 0.01}), 93 | "phase": ("FLOAT", {"default": 0.0, "multiline": False, "min": -1, "max": 1}), 94 | }, 95 | } 96 | 97 | CATEGORY = NodeCategories.ANIMATION_CURVES 98 | RETURN_TYPES = ("FLOAT", "INT") 99 | RETURN_NAMES = ("FLOAT", "INT") 100 | FUNCTION = "result" 101 | 102 | def result(self, frame_counter: FrameCounter, max_value, min_value, periodicity_seconds, phase): 103 | x = frame_counter.current_time_in_seconds 104 | x = ((x + periodicity_seconds * phase) % periodicity_seconds) / periodicity_seconds 105 | if x <= 0.5: 106 | x *= 2 107 | y = x * (max_value - min_value) + min_value 108 | else: 109 | x = (x - 0.5) * 2 110 | y = max_value - x * (max_value - min_value) 111 | return _curve_result(y) 112 | 113 | 114 | class WavData: 115 | def __init__(self, sampling_rate: float, single_channel_samples, fps: float): 116 | self._length_in_seconds = len(single_channel_samples) / sampling_rate 117 | self._num_buckets = round(self._length_in_seconds * fps * 3) 118 | self._bucket_size = len(single_channel_samples) / float(self._num_buckets) 119 | self._buckets = list() 120 | self._rate = sampling_rate 121 | self._max_bucket_value = 0 122 | for i in range(self._num_buckets): 123 | start_index = round(i * self._bucket_size) 124 | end_index = round((i + 1) * self._bucket_size) - 1 125 | samples = list(map(lambda n: abs(n), single_channel_samples[start_index:end_index])) 126 | bucket_total = sum(samples) 127 | self._buckets.append(bucket_total) 128 | self._max_bucket_value=max(bucket_total, self._max_bucket_value) 129 | 130 | for i in range(self._num_buckets): 131 | self._buckets[i] = float(self._buckets[i]) / self._max_bucket_value 132 | 133 | def value_at_time(self, second: float) -> float: 134 | if second < 0.0 or second > self._length_in_seconds: 135 | return 0.0 136 | nsample = second * self._rate 137 | nbucket = min(max(0, round(nsample / self._bucket_size)), self._num_buckets - 1) 138 | return self._buckets[nbucket] 139 | 140 | 141 | @functools.lru_cache(4) 142 | def _wav_loader(filepath, fps): 143 | sampling_rate, samples = wav_read(filepath) 144 | single_channel = samples[:, 0] 145 | return WavData(sampling_rate, single_channel, fps) 146 | 147 | 148 | class DreamWavCurve: 149 | NODE_NAME = "WAV Curve" 150 | CATEGORY = NodeCategories.ANIMATION_CURVES 151 | RETURN_TYPES = ("FLOAT", "INT") 152 | RETURN_NAMES = ("FLOAT", "INT") 153 | FUNCTION = "result" 154 | ICON = "∿" 155 | 156 | @classmethod 157 | def INPUT_TYPES(cls): 158 | return { 159 | "required": SharedTypes.frame_counter | { 160 | "wav_path": ("STRING", {"default": "audio.wav"}), 161 | "scale": ("FLOAT", {"default": 1.0, "multiline": False}) 162 | }, 163 | } 164 | 165 | def result(self, frame_counter: FrameCounter, wav_path, scale): 166 | if not os.path.isfile(wav_path): 167 | return (0.0, 0) 168 | data = _wav_loader(wav_path, frame_counter.frames_per_second) 169 | frame_counter.current_time_in_seconds 170 | v = data.value_at_time(frame_counter.current_time_in_seconds) 171 | return (v * scale, round(v * scale)) 172 | 173 | 174 | class DreamTriangleEvent: 175 | NODE_NAME = "Triangle Event Curve" 176 | 177 | @classmethod 178 | def INPUT_TYPES(cls): 179 | return { 180 | "required": SharedTypes.frame_counter | { 181 | "max_value": ("FLOAT", {"default": 1.0, "multiline": False}), 182 | "min_value": ("FLOAT", {"default": 0.0, "multiline": False}), 183 | "width_seconds": ("FLOAT", {"default": 1.0, "multiline": False, "min": 0.1}), 184 | "center_seconds": ("FLOAT", {"default": 10.0, "multiline": False, "min": 0.0}), 185 | }, 186 | } 187 | 188 | CATEGORY = NodeCategories.ANIMATION_CURVES 189 | RETURN_TYPES = ("FLOAT", "INT") 190 | RETURN_NAMES = ("FLOAT", "INT") 191 | FUNCTION = "result" 192 | 193 | def result(self, frame_counter: FrameCounter, max_value, min_value, width_seconds, center_seconds): 194 | x = frame_counter.current_time_in_seconds 195 | start = center_seconds - width_seconds * 0.5 196 | end = center_seconds + width_seconds * 0.5 197 | if start <= x <= center_seconds: 198 | y = _linear_value_calc(x, start, center_seconds, min_value, max_value) 199 | elif center_seconds < x <= end: 200 | y = _linear_value_calc(x, center_seconds, end, max_value, min_value) 201 | else: 202 | y = min_value 203 | return _curve_result(y) 204 | 205 | 206 | class DreamSmoothEvent: 207 | NODE_NAME = "Smooth Event Curve" 208 | 209 | @classmethod 210 | def INPUT_TYPES(cls): 211 | return { 212 | "required": SharedTypes.frame_counter | { 213 | "max_value": ("FLOAT", {"default": 1.0, "multiline": False}), 214 | "min_value": ("FLOAT", {"default": 0.0, "multiline": False}), 215 | "width_seconds": ("FLOAT", {"default": 1.0, "multiline": False, "min": 0.1}), 216 | "center_seconds": ("FLOAT", {"default": 10.0, "multiline": False, "min": 0.0}), 217 | }, 218 | } 219 | 220 | CATEGORY = NodeCategories.ANIMATION_CURVES 221 | RETURN_TYPES = ("FLOAT", "INT") 222 | RETURN_NAMES = ("FLOAT", "INT") 223 | FUNCTION = "result" 224 | 225 | def result(self, frame_counter: FrameCounter, max_value, min_value, width_seconds, center_seconds): 226 | x = frame_counter.current_time_in_seconds 227 | start = center_seconds - width_seconds * 0.5 228 | end = center_seconds + width_seconds * 0.5 229 | if start <= x <= center_seconds: 230 | y = _linear_value_calc(x, start, center_seconds, 0.0, 1.0) 231 | elif center_seconds < x <= end: 232 | y = _linear_value_calc(x, center_seconds, end, 1.0, 0.0) 233 | else: 234 | y = 0.0 235 | if y < 0.5: 236 | y = ((y + y) * (y + y)) * 0.5 237 | else: 238 | a = (y - 0.5) * 2 239 | y = math.pow(a, 0.25) * 0.5 + 0.5 240 | return _curve_result(y * (max_value - min_value) + min_value) 241 | 242 | 243 | class DreamBeatCurve: 244 | NODE_NAME = "Beat Curve" 245 | 246 | @classmethod 247 | def INPUT_TYPES(cls): 248 | return { 249 | "required": SharedTypes.frame_counter | { 250 | "bpm": ("FLOAT", {"default": 100.0, "multiline": False}), 251 | "time_offset": ("FLOAT", {"default": 0.0, "multiline": False}), 252 | "measure_length": ("INT", {"default": 4, "min": 1}), 253 | "low_value": ("FLOAT", {"default": 0.0}), 254 | "high_value": ("FLOAT", {"default": 1.0}), 255 | "invert": (["no", "yes"],), 256 | "power": ("FLOAT", {"default": 2.0, "min": 0.25, "max": 4}), 257 | "accent_1": ("INT", {"default": 1, "min": 1, "max": 24}), 258 | }, 259 | "optional": { 260 | "accent_2": ("INT", {"default": 0, "min": 0, "max": 24}), 261 | "accent_3": ("INT", {"default": 0, "min": 0, "max": 24}), 262 | "accent_4": ("INT", {"default": 0, "min": 0, "max": 24}), 263 | } 264 | } 265 | 266 | CATEGORY = NodeCategories.ANIMATION_CURVES 267 | RETURN_TYPES = ("FLOAT", "INT") 268 | RETURN_NAMES = ("FLOAT", "INT") 269 | FUNCTION = "result" 270 | 271 | def _get_value_for_accent(self, accent, measure_length, bpm, frame_counter: FrameCounter, frame_offset): 272 | current_frame = frame_counter.current_frame + frame_offset 273 | frames_per_minute = frame_counter.frames_per_second * 60.0 274 | frames_per_beat = frames_per_minute / bpm 275 | frames_per_measure = frames_per_beat * measure_length 276 | frame = (current_frame % frames_per_measure) 277 | accent_start = (accent - 1) * frames_per_beat 278 | accent_end = accent * frames_per_beat 279 | if frame >= accent_start and frame < accent_end: 280 | return 1.0 - ((frame - accent_start) / frames_per_beat) 281 | return 0 282 | 283 | def result(self, bpm, frame_counter: FrameCounter, measure_length, low_value, high_value, power, invert, 284 | time_offset, **accents): 285 | frame_offset = int(round(time_offset * frame_counter.frames_per_second)) 286 | accents_set = set(filter(lambda v: v >= 1 and v <= measure_length, 287 | map(lambda i: accents.get("accent_" + str(i), -1), range(30)))) 288 | v = 0.0 289 | for a in accents_set: 290 | v += math.pow(self._get_value_for_accent(a, measure_length, bpm, frame_counter, frame_offset), power) 291 | if invert == "yes": 292 | v = 1.0 - v 293 | 294 | r = low_value + v * (high_value - low_value) 295 | return _curve_result(r) 296 | 297 | 298 | class DreamLinear: 299 | NODE_NAME = "Linear Curve" 300 | 301 | @classmethod 302 | def INPUT_TYPES(cls): 303 | return { 304 | "required": SharedTypes.frame_counter | { 305 | "initial_value": ("FLOAT", {"default": 0.0, "multiline": False}), 306 | "final_value": ("FLOAT", {"default": 100.0, "multiline": False}), 307 | }, 308 | } 309 | 310 | CATEGORY = NodeCategories.ANIMATION_CURVES 311 | RETURN_TYPES = ("FLOAT", "INT") 312 | RETURN_NAMES = ("FLOAT", "INT") 313 | FUNCTION = "result" 314 | 315 | def result(self, initial_value, final_value, frame_counter: FrameCounter): 316 | d = final_value - initial_value 317 | v = initial_value + frame_counter.progress * d 318 | return (v, int(round(v))) 319 | 320 | 321 | def _is_as_float(s: str): 322 | try: 323 | float(s) 324 | return True 325 | except ValueError: 326 | return False 327 | 328 | 329 | class DreamCSVGenerator: 330 | NODE_NAME = "CSV Generator" 331 | ICON = "⌗" 332 | 333 | @classmethod 334 | def INPUT_TYPES(cls): 335 | return { 336 | "required": SharedTypes.frame_counter | { 337 | "value": ("FLOAT", {"forceInput": True, "default": 0.0}), 338 | "csvfile": ("STRING", {"default": "", "multiline": False}), 339 | "csv_dialect": (csv.list_dialects(),) 340 | }, 341 | } 342 | 343 | CATEGORY = NodeCategories.ANIMATION_CURVES 344 | RETURN_TYPES = () 345 | RETURN_NAMES = () 346 | FUNCTION = "write" 347 | OUTPUT_NODE = True 348 | 349 | def write(self, csvfile, frame_counter: FrameCounter, value, csv_dialect): 350 | if frame_counter.is_first_frame and csvfile: 351 | with open(csvfile, 'w', newline='') as csvfile: 352 | csvwriter = csv.writer(csvfile, dialect=csv_dialect) 353 | csvwriter.writerow(['Frame', 'Value']) 354 | csvwriter.writerow([frame_counter.current_frame, str(value)]) 355 | else: 356 | with open(csvfile, 'a', newline='') as csvfile: 357 | csvwriter = csv.writer(csvfile, dialect=csv_dialect) 358 | csvwriter.writerow([frame_counter.current_frame, str(value)]) 359 | return () 360 | 361 | 362 | class DreamCSVCurve: 363 | NODE_NAME = "CSV Curve" 364 | 365 | @classmethod 366 | def INPUT_TYPES(cls): 367 | return { 368 | "required": SharedTypes.frame_counter | { 369 | "csvfile": ("STRING", {"default": "", "multiline": False}), 370 | "first_column_type": (["seconds", "frames"],), 371 | "interpolate": (["true", "false"],), 372 | "csv_dialect": (csv.list_dialects(),) 373 | }, 374 | } 375 | 376 | CATEGORY = NodeCategories.ANIMATION_CURVES 377 | RETURN_TYPES = ("FLOAT", "INT") 378 | RETURN_NAMES = ("FLOAT", "INT") 379 | FUNCTION = "result" 380 | 381 | def _row_yield(self, file, csv_dialect): 382 | prev_row = None 383 | for row in csv.reader(file, dialect=csv_dialect): 384 | if len(row) == 2 and _is_as_float(row[0]) and _is_as_float(row[1]): 385 | row = list(map(float, row)) 386 | yield (prev_row, row) 387 | prev_row = row 388 | if prev_row is not None: 389 | yield (prev_row, None) 390 | 391 | def result(self, csvfile, frame_counter: FrameCounter, first_column_type, interpolate, csv_dialect): 392 | interpolate = interpolate == "true" 393 | 394 | def _first_col_to_frame(v: float): 395 | if first_column_type == "frames": 396 | return round(v) 397 | else: 398 | return round(v * frame_counter.frames_per_second) 399 | 400 | with open(csvfile) as f: 401 | for (prev, current) in self._row_yield(f, csv_dialect): 402 | if prev is None and frame_counter.current_frame < _first_col_to_frame(current[0]): 403 | # before first row 404 | return (current[1], int(round(current[1]))) 405 | if current is None: 406 | # after last row 407 | return (prev[1], int(round(prev[1]))) 408 | if prev is not None and current is not None: 409 | frame1 = _first_col_to_frame(prev[0]) 410 | value1 = prev[1] 411 | frame2 = _first_col_to_frame(current[0]) 412 | value2 = current[1] 413 | if frame1 <= frame_counter.current_frame and interpolate and frame2 > frame_counter.current_frame: 414 | offset = (frame_counter.current_frame - frame1) / float(frame2 - frame1) 415 | v = value1 * (1.0 - offset) + value2 * offset 416 | return (v, int(round(v))) 417 | elif frame1 <= frame_counter.current_frame and frame2 > frame_counter.current_frame: 418 | return (value1, int(round(value1))) 419 | return (0.0, 0) 420 | -------------------------------------------------------------------------------- /disable.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | def run_disable(): 3 | pass 4 | 5 | 6 | if __name__ == "__main__": 7 | run_disable() 8 | -------------------------------------------------------------------------------- /dreamlogger.py: -------------------------------------------------------------------------------- 1 | class DreamLog: 2 | def __init__(self, debug_active=False): 3 | self._debug = debug_active 4 | 5 | def _print(self, text: str, *args, **kwargs): 6 | if args or kwargs: 7 | text = text.format(*args, **kwargs) 8 | print("[DREAM] " + text) 9 | 10 | def error(self, text: str, *args, **kwargs): 11 | self._print(text, *args, **kwargs) 12 | 13 | def info(self, text: str, *args, **kwargs): 14 | self._print(text, *args, **kwargs) 15 | 16 | def debug(self, text: str, *args, **kwargs): 17 | if self._debug: 18 | self._print(text, *args, **kwargs) 19 | -------------------------------------------------------------------------------- /dreamtypes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import random 3 | import time 4 | 5 | from typing import List, Dict, Tuple 6 | 7 | from .shared import DreamImage 8 | 9 | 10 | class RGBPalette: 11 | ID = "RGB_PALETTE" 12 | 13 | def __init__(self, colors: List[tuple[int, int, int]] = None, image: DreamImage = None): 14 | self._colors = [] 15 | 16 | def _fix_tuple(t): 17 | if len(t) < 3: 18 | return (t[0], t[0], t[0]) 19 | else: 20 | return t 21 | 22 | if image: 23 | for p, _, _ in image: 24 | self._colors.append(_fix_tuple(p)) 25 | if colors: 26 | for c in colors: 27 | self._colors.append(_fix_tuple(c)) 28 | 29 | def _calculate_channel_contrast(self, c): 30 | hist = list(map(lambda _: 0, range(16))) 31 | for pixel in self._colors: 32 | hist[pixel[c] // 16] += 1 33 | s = 0 34 | max_possible = (15 - 0) * (len(self) // 2) * (len(self) // 2) 35 | for i in range(16): 36 | for j in range(i): 37 | if i != j: 38 | s += abs(i - j) * hist[i] * hist[j] 39 | return s / max_possible 40 | 41 | def _calculate_combined_contrast(self): 42 | s = 0 43 | for c in range(3): 44 | s += self._calculate_channel_contrast(c) 45 | return s / 3 46 | 47 | def analyze(self): 48 | total_red = 0 49 | total_blue = 0 50 | total_green = 0 51 | for pixel in self: 52 | total_red += pixel[0] 53 | total_green += pixel[1] 54 | total_blue += pixel[2] 55 | n = len(self._colors) 56 | r = float(total_red) / (255 * n) 57 | g = float(total_green) / (255 * n) 58 | b = float(total_blue) / (255 * n) 59 | return ((r + g + b) / 3.0, self._calculate_combined_contrast(), r, g, b) 60 | 61 | def __len__(self): 62 | return len(self._colors) 63 | 64 | def __iter__(self): 65 | return iter(self._colors) 66 | 67 | def random_iteration(self, seed=None): 68 | s = seed if seed is not None else int(time.time() * 1000) 69 | n = len(self._colors) - 1 70 | c = self._colors 71 | 72 | class _ColorIterator: 73 | def __init__(self): 74 | self._r = random.Random() 75 | self._r.seed(s) 76 | self._n = n 77 | self._c = c 78 | 79 | def __next__(self): 80 | return self._c[self._r.randint(0, self._n)] 81 | 82 | return _ColorIterator() 83 | 84 | 85 | class PartialPrompt: 86 | ID = "PARTIAL_PROMPT" 87 | 88 | def __init__(self): 89 | self._data = {} 90 | 91 | def add(self, text: str, weight: float): 92 | output = PartialPrompt() 93 | output._data = dict(self._data) 94 | for parts in text.split(","): 95 | parts = parts.strip() 96 | if " " in parts: 97 | output._data["(" + parts + ")"] = weight 98 | else: 99 | output._data[parts] = weight 100 | return output 101 | 102 | def is_empty(self): 103 | return not self._data 104 | 105 | def abs_sum(self): 106 | if not self._data: 107 | return 0.0 108 | return sum(map(abs, self._data.values())) 109 | 110 | def abs_max(self): 111 | if not self._data: 112 | return 0.0 113 | return max(map(abs, self._data.values())) 114 | 115 | def scaled_by(self, f: float): 116 | new_data = PartialPrompt() 117 | new_data._data = dict(self._data) 118 | for text, weight in new_data._data.items(): 119 | new_data._data[text] = weight * f 120 | return new_data 121 | 122 | def finalize(self, clamp: float): 123 | items = self._data.items() 124 | items = sorted(items, key=lambda pair: (pair[1], pair[0])) 125 | pos = list() 126 | neg = list() 127 | for text, w in sorted(items, key=lambda pair: (-pair[1], pair[0])): 128 | if w >= 0.0001: 129 | pos.append("({}:{:.3f})".format(text, min(clamp, w))) 130 | for text, w in sorted(items, key=lambda pair: (pair[1], pair[0])): 131 | if w <= -0.0001: 132 | neg.append("({}:{:.3f})".format(text, min(clamp, -w))) 133 | return ", ".join(pos), ", ".join(neg) 134 | 135 | 136 | class LogEntry: 137 | ID = "LOG_ENTRY" 138 | 139 | @classmethod 140 | def new(cls, text): 141 | return LogEntry([(time.time(), text)]) 142 | 143 | def __init__(self, data: List[Tuple[float, str]] = None): 144 | if data is None: 145 | self._data = list() 146 | else: 147 | self._data = list(data) 148 | 149 | def add(self, text: str): 150 | new_data = list(self._data) 151 | new_data.append((time.time(), text)) 152 | return LogEntry(new_data) 153 | 154 | def merge(self, log_entry): 155 | new_data = list(self._data) 156 | new_data.extend(log_entry._data) 157 | return LogEntry(new_data) 158 | 159 | def get_filtered_entries(self, t: float): 160 | for d in sorted(self._data): 161 | if d[0] > t: 162 | yield d 163 | 164 | 165 | class FrameCounter: 166 | ID = "FRAME_COUNTER" 167 | 168 | def __init__(self, current_frame=0, total_frames=1, frames_per_second=25.0): 169 | self.current_frame = max(0, current_frame) 170 | self.total_frames = max(total_frames, 1) 171 | self.frames_per_second = float(max(1.0, frames_per_second)) 172 | 173 | def incremented(self, amount: int): 174 | return FrameCounter(self.current_frame + amount, self.total_frames, self.frames_per_second) 175 | 176 | @property 177 | def is_first_frame(self): 178 | return self.current_frame == 0 179 | 180 | @property 181 | def is_final_frame(self): 182 | return (self.current_frame + 1) == self.total_frames 183 | 184 | @property 185 | def is_after_last_frame(self): 186 | return self.current_frame >= self.total_frames 187 | 188 | @property 189 | def current_time_in_seconds(self): 190 | return float(self.current_frame) / self.frames_per_second 191 | 192 | @property 193 | def total_time_in_seconds(self): 194 | return float(self.total_frames) / self.frames_per_second 195 | 196 | @property 197 | def remaining_time_in_seconds(self): 198 | return self.total_time_in_seconds - self.current_time_in_seconds 199 | 200 | @property 201 | def progress(self): 202 | return float(self.current_frame) / (max(2, self.total_frames) - 1) 203 | 204 | 205 | class AnimationSequence: 206 | ID = "ANIMATION_SEQUENCE" 207 | 208 | def __init__(self, frame_counter: FrameCounter, frames: Dict[int, List[str]] = None): 209 | self.frames = frames 210 | self.fps = frame_counter.frames_per_second 211 | self.frame_counter = frame_counter 212 | if self.is_defined: 213 | self.keys_in_order = sorted(frames.keys()) 214 | self.num_batches = min(map(len, self.frames.values())) 215 | else: 216 | self.keys_in_order = [] 217 | self.num_batches = 0 218 | 219 | @property 220 | def batches(self): 221 | return range(self.num_batches) 222 | 223 | def get_image_files_of_batch(self, batch_num): 224 | for key in self.keys_in_order: 225 | yield self.frames[key][batch_num] 226 | 227 | @property 228 | def is_defined(self): 229 | if self.frames: 230 | return True 231 | else: 232 | return False 233 | 234 | 235 | class SharedTypes: 236 | frame_counter = {"frame_counter": (FrameCounter.ID,)} 237 | sequence = {"sequence": (AnimationSequence.ID,)} 238 | palette = {"palette": (RGBPalette.ID,)} 239 | -------------------------------------------------------------------------------- /embedded_config.py: -------------------------------------------------------------------------------- 1 | EMBEDDED_CONFIGURATION = { 2 | "ffmpeg": { 3 | "file_extension": "mp4", 4 | "path": "ffmpeg", 5 | "arguments": ["-r", "%FPS%", "-f", "concat", "-safe", "0", "-vsync", 6 | "cfr", "-i", "%FRAMES%", "-c:v", "libx264", "-pix_fmt", 7 | "yuv420p", "%OUTPUT%"] 8 | }, 9 | "mpeg_coder": { 10 | "encoding_threads": 4, 11 | "bitrate_factor": 1.0, 12 | "max_b_frame": 2, 13 | "file_extension": "mp4", 14 | "codec_name": "libx264" 15 | }, 16 | "encoding": { 17 | "jpeg_quality": 95 18 | }, 19 | "debug": False, 20 | "ui": { 21 | "top_category": "Dream", 22 | "prepend_icon_to_category": True, 23 | "append_icon_to_category": False, 24 | "prepend_icon_to_node": True, 25 | "append_icon_to_node": False, 26 | "category_icons": { 27 | "animation": "🎥", 28 | "postprocessing": "⚙", 29 | "transforms": "🔀", 30 | "curves": "📈", 31 | "color": "🎨", 32 | "generate": "⚡", 33 | "utils": "🛠", 34 | "image": "🌄", 35 | "switches": "⭆", 36 | "conditioning": "☯", 37 | "Dream": "✨" 38 | } 39 | }, 40 | 41 | } 42 | -------------------------------------------------------------------------------- /enable.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | def run_enable(): 3 | pass 4 | 5 | 6 | if __name__ == "__main__": 7 | run_enable() 8 | -------------------------------------------------------------------------------- /err.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | def _get_node_name(cls): 3 | return cls.__dict__.get("NODE_NAME", str(cls)) 4 | 5 | 6 | def on_error(node_cls: type, message: str): 7 | msg = "Failure in [" + _get_node_name(node_cls) + "]:" + message 8 | print(msg) 9 | raise Exception(msg) 10 | -------------------------------------------------------------------------------- /examples/area-sampled-noise.json: -------------------------------------------------------------------------------- 1 | { 2 | "last_node_id": 32, 3 | "last_link_id": 37, 4 | "nodes": [ 5 | { 6 | "id": 1, 7 | "type": "LoadImage", 8 | "pos": [ 9 | -290, 10 | 250 11 | ], 12 | "size": { 13 | "0": 430, 14 | "1": 530 15 | }, 16 | "flags": {}, 17 | "order": 0, 18 | "mode": 0, 19 | "outputs": [ 20 | { 21 | "name": "IMAGE", 22 | "type": "IMAGE", 23 | "links": [ 24 | 1, 25 | 2, 26 | 3, 27 | 4, 28 | 5, 29 | 6, 30 | 7, 31 | 8, 32 | 9 33 | ], 34 | "shape": 3, 35 | "slot_index": 0 36 | }, 37 | { 38 | "name": "MASK", 39 | "type": "MASK", 40 | "links": null, 41 | "shape": 3, 42 | "slot_index": 1 43 | } 44 | ], 45 | "properties": { 46 | "Node name for S&R": "LoadImage" 47 | }, 48 | "widgets_values": [ 49 | "forest (2).jpg", 50 | "image" 51 | ] 52 | }, 53 | { 54 | "id": 4, 55 | "type": "Sample Image Area as Palette [Dream]", 56 | "pos": [ 57 | 530, 58 | 440 59 | ], 60 | "size": { 61 | "0": 315, 62 | "1": 130 63 | }, 64 | "flags": {}, 65 | "order": 1, 66 | "mode": 0, 67 | "inputs": [ 68 | { 69 | "name": "image", 70 | "type": "IMAGE", 71 | "link": 1 72 | } 73 | ], 74 | "outputs": [ 75 | { 76 | "name": "palette", 77 | "type": "RGB_PALETTE", 78 | "links": [ 79 | 18, 80 | 32 81 | ], 82 | "shape": 3, 83 | "slot_index": 0 84 | } 85 | ], 86 | "properties": { 87 | "Node name for S&R": "Sample Image Area as Palette [Dream]" 88 | }, 89 | "widgets_values": [ 90 | 256, 91 | 6263246444646, 92 | "randomize", 93 | "center" 94 | ] 95 | }, 96 | { 97 | "id": 12, 98 | "type": "Sample Image Area as Palette [Dream]", 99 | "pos": [ 100 | 530, 101 | 70 102 | ], 103 | "size": { 104 | "0": 315, 105 | "1": 130 106 | }, 107 | "flags": {}, 108 | "order": 2, 109 | "mode": 0, 110 | "inputs": [ 111 | { 112 | "name": "image", 113 | "type": "IMAGE", 114 | "link": 2 115 | } 116 | ], 117 | "outputs": [ 118 | { 119 | "name": "palette", 120 | "type": "RGB_PALETTE", 121 | "links": [ 122 | 15, 123 | 30 124 | ], 125 | "shape": 3, 126 | "slot_index": 0 127 | } 128 | ], 129 | "properties": { 130 | "Node name for S&R": "Sample Image Area as Palette [Dream]" 131 | }, 132 | "widgets_values": [ 133 | 256, 134 | 602439956783214, 135 | "randomize", 136 | "top-right" 137 | ] 138 | }, 139 | { 140 | "id": 5, 141 | "type": "Sample Image Area as Palette [Dream]", 142 | "pos": [ 143 | 530, 144 | 250 145 | ], 146 | "size": { 147 | "0": 315, 148 | "1": 130 149 | }, 150 | "flags": {}, 151 | "order": 3, 152 | "mode": 0, 153 | "inputs": [ 154 | { 155 | "name": "image", 156 | "type": "IMAGE", 157 | "link": 3 158 | } 159 | ], 160 | "outputs": [ 161 | { 162 | "name": "palette", 163 | "type": "RGB_PALETTE", 164 | "links": [ 165 | 17, 166 | 31 167 | ], 168 | "shape": 3, 169 | "slot_index": 0 170 | } 171 | ], 172 | "properties": { 173 | "Node name for S&R": "Sample Image Area as Palette [Dream]" 174 | }, 175 | "widgets_values": [ 176 | 256, 177 | 347895810515905, 178 | "randomize", 179 | "center-left" 180 | ] 181 | }, 182 | { 183 | "id": 6, 184 | "type": "Sample Image Area as Palette [Dream]", 185 | "pos": [ 186 | 530, 187 | 640 188 | ], 189 | "size": { 190 | "0": 315, 191 | "1": 130 192 | }, 193 | "flags": {}, 194 | "order": 4, 195 | "mode": 0, 196 | "inputs": [ 197 | { 198 | "name": "image", 199 | "type": "IMAGE", 200 | "link": 4 201 | } 202 | ], 203 | "outputs": [ 204 | { 205 | "name": "palette", 206 | "type": "RGB_PALETTE", 207 | "links": [ 208 | 19, 209 | 33 210 | ], 211 | "shape": 3, 212 | "slot_index": 0 213 | } 214 | ], 215 | "properties": { 216 | "Node name for S&R": "Sample Image Area as Palette [Dream]" 217 | }, 218 | "widgets_values": [ 219 | 256, 220 | 442018658189454, 221 | "randomize", 222 | "center-right" 223 | ] 224 | }, 225 | { 226 | "id": 7, 227 | "type": "Sample Image Area as Palette [Dream]", 228 | "pos": [ 229 | 530, 230 | 810 231 | ], 232 | "size": { 233 | "0": 315, 234 | "1": 130 235 | }, 236 | "flags": {}, 237 | "order": 5, 238 | "mode": 0, 239 | "inputs": [ 240 | { 241 | "name": "image", 242 | "type": "IMAGE", 243 | "link": 5 244 | } 245 | ], 246 | "outputs": [ 247 | { 248 | "name": "palette", 249 | "type": "RGB_PALETTE", 250 | "links": [ 251 | 20, 252 | 34 253 | ], 254 | "shape": 3, 255 | "slot_index": 0 256 | } 257 | ], 258 | "properties": { 259 | "Node name for S&R": "Sample Image Area as Palette [Dream]" 260 | }, 261 | "widgets_values": [ 262 | 256, 263 | 369707362068911, 264 | "randomize", 265 | "bottom-left" 266 | ] 267 | }, 268 | { 269 | "id": 10, 270 | "type": "Sample Image Area as Palette [Dream]", 271 | "pos": [ 272 | 530, 273 | 1010 274 | ], 275 | "size": { 276 | "0": 315, 277 | "1": 130 278 | }, 279 | "flags": {}, 280 | "order": 6, 281 | "mode": 0, 282 | "inputs": [ 283 | { 284 | "name": "image", 285 | "type": "IMAGE", 286 | "link": 6 287 | } 288 | ], 289 | "outputs": [ 290 | { 291 | "name": "palette", 292 | "type": "RGB_PALETTE", 293 | "links": [ 294 | 21, 295 | 35 296 | ], 297 | "shape": 3, 298 | "slot_index": 0 299 | } 300 | ], 301 | "properties": { 302 | "Node name for S&R": "Sample Image Area as Palette [Dream]" 303 | }, 304 | "widgets_values": [ 305 | 256, 306 | 495981514872635, 307 | "randomize", 308 | "bottom-center" 309 | ] 310 | }, 311 | { 312 | "id": 8, 313 | "type": "Sample Image Area as Palette [Dream]", 314 | "pos": [ 315 | 530, 316 | 1190 317 | ], 318 | "size": { 319 | "0": 315, 320 | "1": 130 321 | }, 322 | "flags": {}, 323 | "order": 7, 324 | "mode": 0, 325 | "inputs": [ 326 | { 327 | "name": "image", 328 | "type": "IMAGE", 329 | "link": 7 330 | } 331 | ], 332 | "outputs": [ 333 | { 334 | "name": "palette", 335 | "type": "RGB_PALETTE", 336 | "links": [ 337 | 22, 338 | 36 339 | ], 340 | "shape": 3, 341 | "slot_index": 0 342 | } 343 | ], 344 | "properties": { 345 | "Node name for S&R": "Sample Image Area as Palette [Dream]" 346 | }, 347 | "widgets_values": [ 348 | 256, 349 | 531491245299573, 350 | "randomize", 351 | "bottom-right" 352 | ] 353 | }, 354 | { 355 | "id": 9, 356 | "type": "Sample Image Area as Palette [Dream]", 357 | "pos": [ 358 | 530, 359 | -110 360 | ], 361 | "size": { 362 | "0": 315, 363 | "1": 130 364 | }, 365 | "flags": {}, 366 | "order": 8, 367 | "mode": 0, 368 | "inputs": [ 369 | { 370 | "name": "image", 371 | "type": "IMAGE", 372 | "link": 8 373 | } 374 | ], 375 | "outputs": [ 376 | { 377 | "name": "palette", 378 | "type": "RGB_PALETTE", 379 | "links": [ 380 | 14, 381 | 29 382 | ], 383 | "shape": 3, 384 | "slot_index": 0 385 | } 386 | ], 387 | "properties": { 388 | "Node name for S&R": "Sample Image Area as Palette [Dream]" 389 | }, 390 | "widgets_values": [ 391 | 256, 392 | 275748471798978, 393 | "randomize", 394 | "top-center" 395 | ] 396 | }, 397 | { 398 | "id": 11, 399 | "type": "Sample Image Area as Palette [Dream]", 400 | "pos": [ 401 | 530, 402 | -300 403 | ], 404 | "size": { 405 | "0": 315, 406 | "1": 130 407 | }, 408 | "flags": {}, 409 | "order": 9, 410 | "mode": 0, 411 | "inputs": [ 412 | { 413 | "name": "image", 414 | "type": "IMAGE", 415 | "link": 9, 416 | "slot_index": 0 417 | } 418 | ], 419 | "outputs": [ 420 | { 421 | "name": "palette", 422 | "type": "RGB_PALETTE", 423 | "links": [ 424 | 10, 425 | 28 426 | ], 427 | "shape": 3, 428 | "slot_index": 0 429 | } 430 | ], 431 | "properties": { 432 | "Node name for S&R": "Sample Image Area as Palette [Dream]" 433 | }, 434 | "widgets_values": [ 435 | 256, 436 | 1078875074929860, 437 | "randomize", 438 | "top-left" 439 | ] 440 | }, 441 | { 442 | "id": 21, 443 | "type": "Noise from Palette [Dream]", 444 | "pos": [ 445 | 1050, 446 | 620 447 | ], 448 | "size": { 449 | "0": 315, 450 | "1": 178 451 | }, 452 | "flags": {}, 453 | "order": 10, 454 | "mode": 0, 455 | "inputs": [ 456 | { 457 | "name": "palette", 458 | "type": "RGB_PALETTE", 459 | "link": 18 460 | } 461 | ], 462 | "outputs": [ 463 | { 464 | "name": "image", 465 | "type": "IMAGE", 466 | "links": [ 467 | 23 468 | ], 469 | "shape": 3, 470 | "slot_index": 0 471 | } 472 | ], 473 | "properties": { 474 | "Node name for S&R": "Noise from Palette [Dream]" 475 | }, 476 | "widgets_values": [ 477 | 256, 478 | 256, 479 | 0.3, 480 | 0.5, 481 | 524929391212381, 482 | "randomize" 483 | ] 484 | }, 485 | { 486 | "id": 17, 487 | "type": "Noise from Palette [Dream]", 488 | "pos": [ 489 | 1050, 490 | 80 491 | ], 492 | "size": { 493 | "0": 315, 494 | "1": 178 495 | }, 496 | "flags": {}, 497 | "order": 11, 498 | "mode": 0, 499 | "inputs": [ 500 | { 501 | "name": "palette", 502 | "type": "RGB_PALETTE", 503 | "link": 15 504 | } 505 | ], 506 | "outputs": [ 507 | { 508 | "name": "image", 509 | "type": "IMAGE", 510 | "links": [ 511 | 13 512 | ], 513 | "shape": 3, 514 | "slot_index": 0 515 | } 516 | ], 517 | "properties": { 518 | "Node name for S&R": "Noise from Palette [Dream]" 519 | }, 520 | "widgets_values": [ 521 | 256, 522 | 256, 523 | 0.3, 524 | 0.5, 525 | 78435709751137, 526 | "randomize" 527 | ] 528 | }, 529 | { 530 | "id": 19, 531 | "type": "Noise from Palette [Dream]", 532 | "pos": [ 533 | 1050, 534 | 330 535 | ], 536 | "size": { 537 | "0": 315, 538 | "1": 178 539 | }, 540 | "flags": {}, 541 | "order": 12, 542 | "mode": 0, 543 | "inputs": [ 544 | { 545 | "name": "palette", 546 | "type": "RGB_PALETTE", 547 | "link": 17 548 | } 549 | ], 550 | "outputs": [ 551 | { 552 | "name": "image", 553 | "type": "IMAGE", 554 | "links": [ 555 | 16 556 | ], 557 | "shape": 3, 558 | "slot_index": 0 559 | } 560 | ], 561 | "properties": { 562 | "Node name for S&R": "Noise from Palette [Dream]" 563 | }, 564 | "widgets_values": [ 565 | 256, 566 | 256, 567 | 0.3, 568 | 0.5, 569 | 525418659561865, 570 | "randomize" 571 | ] 572 | }, 573 | { 574 | "id": 23, 575 | "type": "Noise from Palette [Dream]", 576 | "pos": [ 577 | 1050, 578 | 870 579 | ], 580 | "size": { 581 | "0": 315, 582 | "1": 178 583 | }, 584 | "flags": {}, 585 | "order": 13, 586 | "mode": 0, 587 | "inputs": [ 588 | { 589 | "name": "palette", 590 | "type": "RGB_PALETTE", 591 | "link": 19 592 | } 593 | ], 594 | "outputs": [ 595 | { 596 | "name": "image", 597 | "type": "IMAGE", 598 | "links": [ 599 | 24 600 | ], 601 | "shape": 3, 602 | "slot_index": 0 603 | } 604 | ], 605 | "properties": { 606 | "Node name for S&R": "Noise from Palette [Dream]" 607 | }, 608 | "widgets_values": [ 609 | 256, 610 | 256, 611 | 0.3, 612 | 0.5, 613 | 635328469053725, 614 | "randomize" 615 | ] 616 | }, 617 | { 618 | "id": 25, 619 | "type": "Noise from Palette [Dream]", 620 | "pos": [ 621 | 1050, 622 | 1150 623 | ], 624 | "size": { 625 | "0": 315, 626 | "1": 178 627 | }, 628 | "flags": {}, 629 | "order": 14, 630 | "mode": 0, 631 | "inputs": [ 632 | { 633 | "name": "palette", 634 | "type": "RGB_PALETTE", 635 | "link": 20 636 | } 637 | ], 638 | "outputs": [ 639 | { 640 | "name": "image", 641 | "type": "IMAGE", 642 | "links": [ 643 | 25 644 | ], 645 | "shape": 3, 646 | "slot_index": 0 647 | } 648 | ], 649 | "properties": { 650 | "Node name for S&R": "Noise from Palette [Dream]" 651 | }, 652 | "widgets_values": [ 653 | 256, 654 | 256, 655 | 0.3, 656 | 0.5, 657 | 383192376875704, 658 | "randomize" 659 | ] 660 | }, 661 | { 662 | "id": 27, 663 | "type": "Noise from Palette [Dream]", 664 | "pos": [ 665 | 1050, 666 | 1440 667 | ], 668 | "size": { 669 | "0": 315, 670 | "1": 178 671 | }, 672 | "flags": {}, 673 | "order": 15, 674 | "mode": 0, 675 | "inputs": [ 676 | { 677 | "name": "palette", 678 | "type": "RGB_PALETTE", 679 | "link": 21 680 | } 681 | ], 682 | "outputs": [ 683 | { 684 | "name": "image", 685 | "type": "IMAGE", 686 | "links": [ 687 | 26 688 | ], 689 | "shape": 3, 690 | "slot_index": 0 691 | } 692 | ], 693 | "properties": { 694 | "Node name for S&R": "Noise from Palette [Dream]" 695 | }, 696 | "widgets_values": [ 697 | 256, 698 | 256, 699 | 0.3, 700 | 0.5, 701 | 1096999909838714, 702 | "randomize" 703 | ] 704 | }, 705 | { 706 | "id": 29, 707 | "type": "Noise from Palette [Dream]", 708 | "pos": [ 709 | 1050, 710 | 1690 711 | ], 712 | "size": { 713 | "0": 315, 714 | "1": 178 715 | }, 716 | "flags": {}, 717 | "order": 16, 718 | "mode": 0, 719 | "inputs": [ 720 | { 721 | "name": "palette", 722 | "type": "RGB_PALETTE", 723 | "link": 22 724 | } 725 | ], 726 | "outputs": [ 727 | { 728 | "name": "image", 729 | "type": "IMAGE", 730 | "links": [ 731 | 27 732 | ], 733 | "shape": 3, 734 | "slot_index": 0 735 | } 736 | ], 737 | "properties": { 738 | "Node name for S&R": "Noise from Palette [Dream]" 739 | }, 740 | "widgets_values": [ 741 | 256, 742 | 256, 743 | 0.3, 744 | 0.5, 745 | 945264383034375, 746 | "randomize" 747 | ] 748 | }, 749 | { 750 | "id": 15, 751 | "type": "Noise from Palette [Dream]", 752 | "pos": [ 753 | 1050, 754 | -210 755 | ], 756 | "size": { 757 | "0": 315, 758 | "1": 178 759 | }, 760 | "flags": {}, 761 | "order": 17, 762 | "mode": 0, 763 | "inputs": [ 764 | { 765 | "name": "palette", 766 | "type": "RGB_PALETTE", 767 | "link": 14 768 | } 769 | ], 770 | "outputs": [ 771 | { 772 | "name": "image", 773 | "type": "IMAGE", 774 | "links": [ 775 | 12 776 | ], 777 | "shape": 3, 778 | "slot_index": 0 779 | } 780 | ], 781 | "properties": { 782 | "Node name for S&R": "Noise from Palette [Dream]" 783 | }, 784 | "widgets_values": [ 785 | 256, 786 | 256, 787 | 0.3, 788 | 0.5, 789 | 385670812107820, 790 | "randomize" 791 | ] 792 | }, 793 | { 794 | "id": 13, 795 | "type": "Noise from Palette [Dream]", 796 | "pos": [ 797 | 1050, 798 | -490 799 | ], 800 | "size": { 801 | "0": 315, 802 | "1": 178 803 | }, 804 | "flags": {}, 805 | "order": 18, 806 | "mode": 0, 807 | "inputs": [ 808 | { 809 | "name": "palette", 810 | "type": "RGB_PALETTE", 811 | "link": 10 812 | } 813 | ], 814 | "outputs": [ 815 | { 816 | "name": "image", 817 | "type": "IMAGE", 818 | "links": [ 819 | 11 820 | ], 821 | "shape": 3, 822 | "slot_index": 0 823 | } 824 | ], 825 | "properties": { 826 | "Node name for S&R": "Noise from Palette [Dream]" 827 | }, 828 | "widgets_values": [ 829 | 256, 830 | 256, 831 | 0.3, 832 | 0.5, 833 | 869209746177441, 834 | "randomize" 835 | ] 836 | }, 837 | { 838 | "id": 31, 839 | "type": "Noise from Area Palettes [Dream]", 840 | "pos": [ 841 | 1810, 842 | 140 843 | ], 844 | "size": { 845 | "0": 342.5999755859375, 846 | "1": 362 847 | }, 848 | "flags": {}, 849 | "order": 19, 850 | "mode": 0, 851 | "inputs": [ 852 | { 853 | "name": "top_left_palette", 854 | "type": "RGB_PALETTE", 855 | "link": 28 856 | }, 857 | { 858 | "name": "top_center_palette", 859 | "type": "RGB_PALETTE", 860 | "link": 29 861 | }, 862 | { 863 | "name": "top_right_palette", 864 | "type": "RGB_PALETTE", 865 | "link": 30 866 | }, 867 | { 868 | "name": "center_left_palette", 869 | "type": "RGB_PALETTE", 870 | "link": 31 871 | }, 872 | { 873 | "name": "center_palette", 874 | "type": "RGB_PALETTE", 875 | "link": 32 876 | }, 877 | { 878 | "name": "center_right_palette", 879 | "type": "RGB_PALETTE", 880 | "link": 33 881 | }, 882 | { 883 | "name": "bottom_left_palette", 884 | "type": "RGB_PALETTE", 885 | "link": 34 886 | }, 887 | { 888 | "name": "bottom_center_palette", 889 | "type": "RGB_PALETTE", 890 | "link": 35 891 | }, 892 | { 893 | "name": "bottom_right_palette", 894 | "type": "RGB_PALETTE", 895 | "link": 36 896 | } 897 | ], 898 | "outputs": [ 899 | { 900 | "name": "image", 901 | "type": "IMAGE", 902 | "links": [ 903 | 37 904 | ], 905 | "shape": 3, 906 | "slot_index": 0 907 | } 908 | ], 909 | "properties": { 910 | "Node name for S&R": "Noise from Area Palettes [Dream]" 911 | }, 912 | "widgets_values": [ 913 | 0.5, 914 | 512, 915 | 512, 916 | 0.22727050781249997, 917 | 0.5, 918 | 336106403318857, 919 | "randomize" 920 | ] 921 | }, 922 | { 923 | "id": 22, 924 | "type": "PreviewImage", 925 | "pos": [ 926 | 1580, 927 | 540 928 | ], 929 | "size": [ 930 | 140, 931 | 250 932 | ], 933 | "flags": {}, 934 | "order": 20, 935 | "mode": 0, 936 | "inputs": [ 937 | { 938 | "name": "images", 939 | "type": "IMAGE", 940 | "link": 23 941 | } 942 | ], 943 | "properties": { 944 | "Node name for S&R": "PreviewImage" 945 | } 946 | }, 947 | { 948 | "id": 18, 949 | "type": "PreviewImage", 950 | "pos": [ 951 | 1580, 952 | 60 953 | ], 954 | "size": [ 955 | 140, 956 | 190 957 | ], 958 | "flags": {}, 959 | "order": 21, 960 | "mode": 0, 961 | "inputs": [ 962 | { 963 | "name": "images", 964 | "type": "IMAGE", 965 | "link": 13 966 | } 967 | ], 968 | "properties": { 969 | "Node name for S&R": "PreviewImage" 970 | } 971 | }, 972 | { 973 | "id": 20, 974 | "type": "PreviewImage", 975 | "pos": [ 976 | 1580, 977 | 310 978 | ], 979 | "size": [ 980 | 140, 981 | 180 982 | ], 983 | "flags": {}, 984 | "order": 22, 985 | "mode": 0, 986 | "inputs": [ 987 | { 988 | "name": "images", 989 | "type": "IMAGE", 990 | "link": 16 991 | } 992 | ], 993 | "properties": { 994 | "Node name for S&R": "PreviewImage" 995 | } 996 | }, 997 | { 998 | "id": 24, 999 | "type": "PreviewImage", 1000 | "pos": [ 1001 | 1580, 1002 | 840 1003 | ], 1004 | "size": [ 1005 | 140, 1006 | 180 1007 | ], 1008 | "flags": {}, 1009 | "order": 23, 1010 | "mode": 0, 1011 | "inputs": [ 1012 | { 1013 | "name": "images", 1014 | "type": "IMAGE", 1015 | "link": 24 1016 | } 1017 | ], 1018 | "properties": { 1019 | "Node name for S&R": "PreviewImage" 1020 | } 1021 | }, 1022 | { 1023 | "id": 26, 1024 | "type": "PreviewImage", 1025 | "pos": [ 1026 | 1580, 1027 | 1070 1028 | ], 1029 | "size": [ 1030 | 140, 1031 | 250 1032 | ], 1033 | "flags": {}, 1034 | "order": 24, 1035 | "mode": 0, 1036 | "inputs": [ 1037 | { 1038 | "name": "images", 1039 | "type": "IMAGE", 1040 | "link": 25 1041 | } 1042 | ], 1043 | "properties": { 1044 | "Node name for S&R": "PreviewImage" 1045 | } 1046 | }, 1047 | { 1048 | "id": 28, 1049 | "type": "PreviewImage", 1050 | "pos": [ 1051 | 1580, 1052 | 1390 1053 | ], 1054 | "size": [ 1055 | 140, 1056 | 250 1057 | ], 1058 | "flags": {}, 1059 | "order": 25, 1060 | "mode": 0, 1061 | "inputs": [ 1062 | { 1063 | "name": "images", 1064 | "type": "IMAGE", 1065 | "link": 26 1066 | } 1067 | ], 1068 | "properties": { 1069 | "Node name for S&R": "PreviewImage" 1070 | } 1071 | }, 1072 | { 1073 | "id": 30, 1074 | "type": "PreviewImage", 1075 | "pos": [ 1076 | 1580, 1077 | 1690 1078 | ], 1079 | "size": [ 1080 | 140, 1081 | 250 1082 | ], 1083 | "flags": {}, 1084 | "order": 26, 1085 | "mode": 0, 1086 | "inputs": [ 1087 | { 1088 | "name": "images", 1089 | "type": "IMAGE", 1090 | "link": 27 1091 | } 1092 | ], 1093 | "properties": { 1094 | "Node name for S&R": "PreviewImage" 1095 | } 1096 | }, 1097 | { 1098 | "id": 16, 1099 | "type": "PreviewImage", 1100 | "pos": [ 1101 | 1580, 1102 | -240 1103 | ], 1104 | "size": [ 1105 | 140, 1106 | 250 1107 | ], 1108 | "flags": {}, 1109 | "order": 27, 1110 | "mode": 0, 1111 | "inputs": [ 1112 | { 1113 | "name": "images", 1114 | "type": "IMAGE", 1115 | "link": 12 1116 | } 1117 | ], 1118 | "properties": { 1119 | "Node name for S&R": "PreviewImage" 1120 | } 1121 | }, 1122 | { 1123 | "id": 14, 1124 | "type": "PreviewImage", 1125 | "pos": [ 1126 | 1580, 1127 | -540 1128 | ], 1129 | "size": [ 1130 | 140, 1131 | 250 1132 | ], 1133 | "flags": {}, 1134 | "order": 28, 1135 | "mode": 0, 1136 | "inputs": [ 1137 | { 1138 | "name": "images", 1139 | "type": "IMAGE", 1140 | "link": 11 1141 | } 1142 | ], 1143 | "properties": { 1144 | "Node name for S&R": "PreviewImage" 1145 | } 1146 | }, 1147 | { 1148 | "id": 32, 1149 | "type": "PreviewImage", 1150 | "pos": [ 1151 | 1790, 1152 | 600 1153 | ], 1154 | "size": [ 1155 | 460, 1156 | 510 1157 | ], 1158 | "flags": {}, 1159 | "order": 29, 1160 | "mode": 0, 1161 | "inputs": [ 1162 | { 1163 | "name": "images", 1164 | "type": "IMAGE", 1165 | "link": 37 1166 | } 1167 | ], 1168 | "properties": { 1169 | "Node name for S&R": "PreviewImage" 1170 | } 1171 | } 1172 | ], 1173 | "links": [ 1174 | [ 1175 | 1, 1176 | 1, 1177 | 0, 1178 | 4, 1179 | 0, 1180 | "IMAGE" 1181 | ], 1182 | [ 1183 | 2, 1184 | 1, 1185 | 0, 1186 | 12, 1187 | 0, 1188 | "IMAGE" 1189 | ], 1190 | [ 1191 | 3, 1192 | 1, 1193 | 0, 1194 | 5, 1195 | 0, 1196 | "IMAGE" 1197 | ], 1198 | [ 1199 | 4, 1200 | 1, 1201 | 0, 1202 | 6, 1203 | 0, 1204 | "IMAGE" 1205 | ], 1206 | [ 1207 | 5, 1208 | 1, 1209 | 0, 1210 | 7, 1211 | 0, 1212 | "IMAGE" 1213 | ], 1214 | [ 1215 | 6, 1216 | 1, 1217 | 0, 1218 | 10, 1219 | 0, 1220 | "IMAGE" 1221 | ], 1222 | [ 1223 | 7, 1224 | 1, 1225 | 0, 1226 | 8, 1227 | 0, 1228 | "IMAGE" 1229 | ], 1230 | [ 1231 | 8, 1232 | 1, 1233 | 0, 1234 | 9, 1235 | 0, 1236 | "IMAGE" 1237 | ], 1238 | [ 1239 | 9, 1240 | 1, 1241 | 0, 1242 | 11, 1243 | 0, 1244 | "IMAGE" 1245 | ], 1246 | [ 1247 | 10, 1248 | 11, 1249 | 0, 1250 | 13, 1251 | 0, 1252 | "RGB_PALETTE" 1253 | ], 1254 | [ 1255 | 11, 1256 | 13, 1257 | 0, 1258 | 14, 1259 | 0, 1260 | "IMAGE" 1261 | ], 1262 | [ 1263 | 12, 1264 | 15, 1265 | 0, 1266 | 16, 1267 | 0, 1268 | "IMAGE" 1269 | ], 1270 | [ 1271 | 13, 1272 | 17, 1273 | 0, 1274 | 18, 1275 | 0, 1276 | "IMAGE" 1277 | ], 1278 | [ 1279 | 14, 1280 | 9, 1281 | 0, 1282 | 15, 1283 | 0, 1284 | "RGB_PALETTE" 1285 | ], 1286 | [ 1287 | 15, 1288 | 12, 1289 | 0, 1290 | 17, 1291 | 0, 1292 | "RGB_PALETTE" 1293 | ], 1294 | [ 1295 | 16, 1296 | 19, 1297 | 0, 1298 | 20, 1299 | 0, 1300 | "IMAGE" 1301 | ], 1302 | [ 1303 | 17, 1304 | 5, 1305 | 0, 1306 | 19, 1307 | 0, 1308 | "RGB_PALETTE" 1309 | ], 1310 | [ 1311 | 18, 1312 | 4, 1313 | 0, 1314 | 21, 1315 | 0, 1316 | "RGB_PALETTE" 1317 | ], 1318 | [ 1319 | 19, 1320 | 6, 1321 | 0, 1322 | 23, 1323 | 0, 1324 | "RGB_PALETTE" 1325 | ], 1326 | [ 1327 | 20, 1328 | 7, 1329 | 0, 1330 | 25, 1331 | 0, 1332 | "RGB_PALETTE" 1333 | ], 1334 | [ 1335 | 21, 1336 | 10, 1337 | 0, 1338 | 27, 1339 | 0, 1340 | "RGB_PALETTE" 1341 | ], 1342 | [ 1343 | 22, 1344 | 8, 1345 | 0, 1346 | 29, 1347 | 0, 1348 | "RGB_PALETTE" 1349 | ], 1350 | [ 1351 | 23, 1352 | 21, 1353 | 0, 1354 | 22, 1355 | 0, 1356 | "IMAGE" 1357 | ], 1358 | [ 1359 | 24, 1360 | 23, 1361 | 0, 1362 | 24, 1363 | 0, 1364 | "IMAGE" 1365 | ], 1366 | [ 1367 | 25, 1368 | 25, 1369 | 0, 1370 | 26, 1371 | 0, 1372 | "IMAGE" 1373 | ], 1374 | [ 1375 | 26, 1376 | 27, 1377 | 0, 1378 | 28, 1379 | 0, 1380 | "IMAGE" 1381 | ], 1382 | [ 1383 | 27, 1384 | 29, 1385 | 0, 1386 | 30, 1387 | 0, 1388 | "IMAGE" 1389 | ], 1390 | [ 1391 | 28, 1392 | 11, 1393 | 0, 1394 | 31, 1395 | 0, 1396 | "RGB_PALETTE" 1397 | ], 1398 | [ 1399 | 29, 1400 | 9, 1401 | 0, 1402 | 31, 1403 | 1, 1404 | "RGB_PALETTE" 1405 | ], 1406 | [ 1407 | 30, 1408 | 12, 1409 | 0, 1410 | 31, 1411 | 2, 1412 | "RGB_PALETTE" 1413 | ], 1414 | [ 1415 | 31, 1416 | 5, 1417 | 0, 1418 | 31, 1419 | 3, 1420 | "RGB_PALETTE" 1421 | ], 1422 | [ 1423 | 32, 1424 | 4, 1425 | 0, 1426 | 31, 1427 | 4, 1428 | "RGB_PALETTE" 1429 | ], 1430 | [ 1431 | 33, 1432 | 6, 1433 | 0, 1434 | 31, 1435 | 5, 1436 | "RGB_PALETTE" 1437 | ], 1438 | [ 1439 | 34, 1440 | 7, 1441 | 0, 1442 | 31, 1443 | 6, 1444 | "RGB_PALETTE" 1445 | ], 1446 | [ 1447 | 35, 1448 | 10, 1449 | 0, 1450 | 31, 1451 | 7, 1452 | "RGB_PALETTE" 1453 | ], 1454 | [ 1455 | 36, 1456 | 8, 1457 | 0, 1458 | 31, 1459 | 8, 1460 | "RGB_PALETTE" 1461 | ], 1462 | [ 1463 | 37, 1464 | 31, 1465 | 0, 1466 | 32, 1467 | 0, 1468 | "IMAGE" 1469 | ] 1470 | ], 1471 | "groups": [], 1472 | "config": {}, 1473 | "extra": {}, 1474 | "version": 0.4 1475 | } -------------------------------------------------------------------------------- /examples/test_colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt-key-project/comfyui-dream-project/f48bed5b8ae866b3dad33fb811d712d45205f117/examples/test_colors.png -------------------------------------------------------------------------------- /image_processing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import math 3 | 4 | import numpy 5 | import torch 6 | from PIL import Image, ImageDraw 7 | 8 | from .categories import * 9 | from .shared import convertTensorImageToPIL, DreamImageProcessor, \ 10 | DreamImage, DreamMask 11 | from .dreamtypes import SharedTypes, FrameCounter 12 | 13 | class DreamImageMotion: 14 | NODE_NAME = "Image Motion" 15 | 16 | @classmethod 17 | def INPUT_TYPES(cls): 18 | return { 19 | "required": { 20 | "image": ("IMAGE",), 21 | "zoom": ("FLOAT", {"default": 0.0, "min": -10, "max": 10, "step": 0.01}), 22 | "mask_1_feather": ("INT", {"default": 0, "min": 0}), 23 | "mask_1_overlap": ("INT", {"default": 0, "min": 0}), 24 | "mask_2_feather": ("INT", {"default": 10, "min": 0}), 25 | "mask_2_overlap": ("INT", {"default": 5, "min": 0}), 26 | "mask_3_feather": ("INT", {"default": 15, "min": 0}), 27 | "mask_3_overlap": ("INT", {"default": 5, "min": 0}), 28 | "x_translation": ("FLOAT", {"default": 0.0, "min": -10, "max": 10, "step": 0.01}), 29 | "y_translation": ("FLOAT", {"default": 0.0, "min": -10, "max": 10, "step": 0.01}), 30 | } | SharedTypes.frame_counter, 31 | "optional": { 32 | "noise": ("IMAGE",), 33 | "output_resize_width": ("INT", {"default": 0, "min": 0}), 34 | "output_resize_height": ("INT", {"default": 0, "min": 0}) 35 | } 36 | } 37 | 38 | CATEGORY = NodeCategories.ANIMATION_TRANSFORMS 39 | RETURN_TYPES = ("IMAGE", "MASK", "MASK", "MASK") 40 | RETURN_NAMES = ("image", "mask1", "mask2", "mask3") 41 | FUNCTION = "result" 42 | 43 | def _mk_PIL_image(self, size, color=None, mode="RGB") -> Image: 44 | im = Image.new(mode=mode, size=size) 45 | if color: 46 | im.paste(color, (0, 0, size[0], size[1])) 47 | return im 48 | 49 | def _convertPILToMask(self, image): 50 | return torch.from_numpy(numpy.array(image.convert("L")).astype(numpy.float32) / 255.0) 51 | 52 | def _apply_feather(self, pil_image, area, feather): 53 | feather = min((area[2] - area[0]) // 2 - 1, feather) 54 | draw = ImageDraw.Draw(pil_image) 55 | for i in range(1, feather + 1): 56 | rect = [(area[0] + i - 1, area[1] + i - 1), (area[2] - i + 1, area[3] - i + 1)] 57 | c = 255 - int(round(255.0 * (i / (feather + 1)))) 58 | draw.rectangle(rect, fill=None, outline=(c, c, c)) 59 | return pil_image 60 | 61 | def _make_mask(self, width, height, selection_area, feather, overlap): 62 | complete_area = self._mk_PIL_image((width, height), "white") 63 | draw = ImageDraw.Draw(complete_area) 64 | (left, top, right, bottom) = selection_area 65 | area = (left + overlap, top + overlap, right - overlap - 1, bottom - overlap - 1) 66 | draw.rectangle(area, fill="black", width=0) 67 | return self._apply_feather(complete_area, area, feather) 68 | 69 | def _make_resizer(self, output_resize_width, output_resize_height): 70 | def bound(i): 71 | return min(max(i, 1), 32767) 72 | 73 | if output_resize_height and output_resize_width: 74 | return lambda img: img.resize((bound(output_resize_width), bound(output_resize_height))) 75 | else: 76 | return lambda img: img 77 | 78 | def result(self, image: torch.Tensor, zoom, x_translation, y_translation, mask_1_feather, mask_1_overlap, 79 | mask_2_feather, mask_2_overlap, mask_3_feather, mask_3_overlap, frame_counter: FrameCounter, 80 | **other): 81 | def _limit_range(f): 82 | return max(-1.0, min(1.0, f)) 83 | 84 | def _motion(image: DreamImage, batch_counter, zoom, x_translation, y_translation, mask_1_overlap, 85 | mask_2_overlap, 86 | mask_3_overlap): 87 | zoom = _limit_range(zoom / frame_counter.frames_per_second) 88 | x_translation = _limit_range(x_translation / frame_counter.frames_per_second) 89 | y_translation = _limit_range(y_translation / frame_counter.frames_per_second) 90 | pil_image = image.pil_image 91 | sz = self._make_resizer(other.get("output_resize_width", None), other.get("output_resize_height", None)) 92 | noise = other.get("noise", None) 93 | multiplier = math.pow(2, zoom) 94 | resized_image = pil_image.resize((round(pil_image.width * multiplier), 95 | round(pil_image.height * multiplier))) 96 | 97 | if noise is None: 98 | base_image = self._mk_PIL_image(pil_image.size, "black") 99 | else: 100 | base_image = convertTensorImageToPIL(noise).resize(pil_image.size) 101 | 102 | selection_offset = (round(x_translation * pil_image.width), round(y_translation * pil_image.height)) 103 | selection = ((pil_image.width - resized_image.width) // 2 + selection_offset[0], 104 | (pil_image.height - resized_image.height) // 2 + selection_offset[1], 105 | (pil_image.width - resized_image.width) // 2 + selection_offset[0] + resized_image.width, 106 | (pil_image.height - resized_image.height) // 2 + selection_offset[1] + resized_image.height) 107 | base_image.paste(resized_image, selection) 108 | 109 | mask_1_overlap = min(pil_image.width // 3, min(mask_1_overlap, pil_image.height // 3)) 110 | mask_2_overlap = min(pil_image.width // 3, min(mask_2_overlap, pil_image.height // 3)) 111 | mask_3_overlap = min(pil_image.width // 3, min(mask_3_overlap, pil_image.height // 3)) 112 | mask1 = self._make_mask(pil_image.width, pil_image.height, selection, mask_1_feather, mask_1_overlap) 113 | mask2 = self._make_mask(pil_image.width, pil_image.height, selection, mask_2_feather, mask_2_overlap) 114 | mask3 = self._make_mask(pil_image.width, pil_image.height, selection, mask_3_feather, mask_3_overlap) 115 | 116 | return (DreamImage(pil_image=sz(base_image)), 117 | DreamMask(pil_image=sz(mask1)), 118 | DreamMask(pil_image=sz(mask2)), 119 | DreamMask(pil_image=sz(mask3))) 120 | 121 | proc = DreamImageProcessor(image, 122 | zoom=zoom, 123 | x_translation=x_translation, 124 | y_translation=y_translation, 125 | mask_1_overlap=mask_1_overlap, 126 | mask_2_overlap=mask_2_overlap, 127 | mask_3_overlap=mask_3_overlap) 128 | return proc.process(_motion) 129 | -------------------------------------------------------------------------------- /inputfields.py: -------------------------------------------------------------------------------- 1 | from .categories import * 2 | from .shared import * 3 | 4 | class DreamInputText: 5 | NODE_NAME = "Text Input" 6 | ICON = "✍" 7 | 8 | @classmethod 9 | def INPUT_TYPES(cls): 10 | return { 11 | "required": { 12 | "value": ("STRING", {"default": "", "multiline": True}), 13 | }, 14 | } 15 | 16 | CATEGORY = NodeCategories.UTILS 17 | RETURN_TYPES = ("STRING",) 18 | RETURN_NAMES = ("STRING",) 19 | FUNCTION = "noop" 20 | 21 | def noop(self, value): 22 | return (value,) 23 | 24 | class DreamInputString: 25 | NODE_NAME = "String Input" 26 | ICON = "✍" 27 | 28 | @classmethod 29 | def INPUT_TYPES(cls): 30 | return { 31 | "required": { 32 | "value": ("STRING", {"default": "", "multiline": False}), 33 | }, 34 | } 35 | 36 | CATEGORY = NodeCategories.UTILS 37 | RETURN_TYPES = ("STRING",) 38 | RETURN_NAMES = ("STRING",) 39 | FUNCTION = "noop" 40 | 41 | def noop(self, value): 42 | return (value,) 43 | 44 | 45 | class DreamInputFloat: 46 | NODE_NAME = "Float Input" 47 | ICON = "✍" 48 | 49 | @classmethod 50 | def INPUT_TYPES(cls): 51 | return { 52 | "required": { 53 | "value": ("FLOAT", {"default": 0.0}), 54 | }, 55 | } 56 | 57 | CATEGORY = NodeCategories.UTILS 58 | RETURN_TYPES = ("FLOAT",) 59 | RETURN_NAMES = ("FLOAT",) 60 | FUNCTION = "noop" 61 | 62 | def noop(self, value): 63 | return (value,) 64 | 65 | 66 | class DreamInputInt: 67 | NODE_NAME = "Int Input" 68 | ICON = "✍" 69 | 70 | @classmethod 71 | def INPUT_TYPES(cls): 72 | return { 73 | "required": { 74 | "value": ("INT", {"default": 0}), 75 | }, 76 | } 77 | 78 | CATEGORY = NodeCategories.UTILS 79 | RETURN_TYPES = ("INT",) 80 | RETURN_NAMES = ("INT",) 81 | FUNCTION = "noop" 82 | 83 | def noop(self, value): 84 | return (value,) 85 | -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys, os 3 | 4 | #sys.path.append(str(os.path.dirname(os.path.abspath(__file__)))) 5 | 6 | #from . import shared 7 | 8 | 9 | def setup_default_config(): 10 | #shared.DreamConfig() 11 | pass 12 | 13 | def run_install(): 14 | setup_default_config() 15 | 16 | 17 | if __name__ == "__main__": 18 | run_install() 19 | -------------------------------------------------------------------------------- /laboratory.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # -*- coding: utf-8 -*- 4 | 5 | import json 6 | 7 | from .categories import * 8 | from .shared import DreamStateFile 9 | from .dreamtypes import * 10 | 11 | _laboratory_state = DreamStateFile("laboratory") 12 | 13 | 14 | class DreamLaboratory: 15 | NODE_NAME = "Laboratory" 16 | ICON = "🧪" 17 | 18 | @classmethod 19 | def INPUT_TYPES(cls): 20 | return { 21 | "required": SharedTypes.frame_counter | { 22 | "key": ("STRING", {"default": "Random value " + str(random.randint(0, 1000000))}), 23 | "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), 24 | "renew_policy": (["every frame", "first frame"],), 25 | "min_value": ("FLOAT", {"default": 0.0}), 26 | "max_value": ("FLOAT", {"default": 1.0}), 27 | "mode": (["random uniform", "random bell", "ladder", "random walk"],), 28 | }, 29 | "optional": { 30 | "step_size": ("FLOAT", {"default": 0.1}), 31 | }, 32 | } 33 | 34 | CATEGORY = NodeCategories.UTILS 35 | RETURN_TYPES = ("FLOAT", "INT", LogEntry.ID) 36 | RETURN_NAMES = ("FLOAT", "INT", "log_entry") 37 | FUNCTION = "result" 38 | 39 | def _generate(self, seed, last_value, min_value, max_value, mode, step_size): 40 | rnd = random.Random() 41 | rnd.seed(seed) 42 | 43 | def jsonify(v: float): 44 | return json.loads(json.dumps(v)) 45 | 46 | if mode == "random uniform": 47 | return jsonify(self._mode_uniform(rnd, last_value, min_value, max_value, step_size)) 48 | elif mode == "random bell": 49 | return jsonify(self._mode_bell(rnd, last_value, min_value, max_value, step_size)) 50 | elif mode == "ladder": 51 | return jsonify(self._mode_ladder(rnd, last_value, min_value, max_value, step_size)) 52 | else: 53 | return jsonify(self._mode_walk(rnd, last_value, min_value, max_value, step_size)) 54 | 55 | def _mode_uniform(self, rnd: random.Random, last_value: float, min_value: float, max_value: float, step_size): 56 | return rnd.random() * (max_value - min_value) + min_value 57 | 58 | def _mode_bell(self, rnd: random.Random, last_value: float, min_value: float, max_value: float, step_size): 59 | s = 0.0 60 | for i in range(3): 61 | s += rnd.random() * (max_value - min_value) + min_value 62 | return s / 3.0 63 | 64 | def _mode_ladder(self, rnd: random.Random, last_value: float, min_value: float, max_value: float, step_size): 65 | if last_value is None: 66 | last_value = min_value - step_size 67 | next_value = last_value + step_size 68 | if next_value > max_value: 69 | d = abs(max_value - min_value) 70 | next_value = (next_value - min_value) % d + min_value 71 | return next_value 72 | 73 | def _mode_walk(self, rnd: random.Random, last_value: float, min_value: float, max_value: float, step_size): 74 | if last_value is None: 75 | last_value = (max_value - min_value) * 0.5 76 | if rnd.random() >= 0.5: 77 | return min(max_value, last_value + step_size) 78 | else: 79 | return max(min_value, last_value - step_size) 80 | 81 | def result(self, key, frame_counter: FrameCounter, seed, renew_policy, min_value, max_value, mode, **values): 82 | if min_value > max_value: 83 | t = max_value 84 | max_value = min_value 85 | min_value = t 86 | step_size = values.get("step_size", abs(max_value - min_value) * 0.1) 87 | last_value = _laboratory_state.get_section("values").get(key, None) 88 | 89 | if (last_value is None) or (renew_policy == "every frame") or frame_counter.is_first_frame: 90 | v = _laboratory_state.get_section("values") \ 91 | .update(key, 0, lambda old: self._generate(seed, last_value, min_value, max_value, mode, step_size)) 92 | return v, round(v), LogEntry.new( 93 | "Laboratory generated new value for '{}': {} ({})".format(key, v, round(v))) 94 | else: 95 | return last_value, round(last_value), LogEntry.new("Laboratory reused value for '{}': {} ({})" 96 | .format(key, last_value, round(last_value))) 97 | -------------------------------------------------------------------------------- /lazyswitches.py: -------------------------------------------------------------------------------- 1 | from .categories import NodeCategories 2 | from .dreamtypes import RGBPalette 3 | 4 | _NOT_A_VALUE_I = 9223372036854775807 5 | _NOT_A_VALUE_F = float(_NOT_A_VALUE_I) 6 | _NOT_A_VALUE_S = "⭆" 7 | 8 | def _generate_switch_input(type_nm: str, default_value=None): 9 | d = dict() 10 | for i in range(10): 11 | if default_value is None: 12 | d["input_" + str(i)] = (type_nm, {"lazy": True}) 13 | else: 14 | d["input_" + str(i)] = (type_nm, {"default": default_value, "forceInput": True, "lazy": True}) 15 | 16 | return { 17 | "required": { 18 | "select": ("INT", {"default": 0, "min": 0, "max": 9}) 19 | }, 20 | "optional": d 21 | } 22 | 23 | 24 | def _check_big_switch_lazy_status(*args, **kwargs): 25 | n = int(kwargs['select']) 26 | input_name = f"input_{n}" 27 | print(f"SELECTED: {input_name}") 28 | if input_name in kwargs: 29 | return [input_name] 30 | else: 31 | return [] 32 | 33 | 34 | class DreamLazyImageSwitch: 35 | _switch_type = "IMAGE" 36 | NODE_NAME = "Lazy Image Switch" 37 | ICON = "⭆" 38 | CATEGORY = NodeCategories.UTILS_SWITCHES 39 | RETURN_TYPES = (_switch_type,) 40 | RETURN_NAMES = ("selected",) 41 | FUNCTION = "pick" 42 | 43 | @classmethod 44 | def INPUT_TYPES(cls): 45 | return _generate_switch_input(cls._switch_type) 46 | 47 | def check_lazy_status(self, *args, **kwargs): 48 | return _check_big_switch_lazy_status(*args, **kwargs) 49 | 50 | def pick(self, select, **args): 51 | return (args.get("input_"+str(select), None),) 52 | 53 | 54 | class DreamLazyLatentSwitch: 55 | _switch_type = "LATENT" 56 | NODE_NAME = "Lazy Latent Switch" 57 | ICON = "⭆" 58 | CATEGORY = NodeCategories.UTILS_SWITCHES 59 | RETURN_TYPES = (_switch_type,) 60 | RETURN_NAMES = ("selected",) 61 | FUNCTION = "pick" 62 | 63 | @classmethod 64 | def INPUT_TYPES(cls): 65 | return _generate_switch_input(cls._switch_type) 66 | 67 | def check_lazy_status(self, *args, **kwargs): 68 | return _check_big_switch_lazy_status(*args, **kwargs) 69 | 70 | def pick(self, select, **args): 71 | return (args.get("input_" + str(select), None),) 72 | 73 | 74 | class DreamLazyTextSwitch: 75 | _switch_type = "STRING" 76 | NODE_NAME = "Lazy Text Switch" 77 | ICON = "⭆" 78 | CATEGORY = NodeCategories.UTILS_SWITCHES 79 | RETURN_TYPES = (_switch_type,) 80 | RETURN_NAMES = ("selected",) 81 | FUNCTION = "pick" 82 | 83 | @classmethod 84 | def INPUT_TYPES(cls): 85 | return _generate_switch_input(cls._switch_type, _NOT_A_VALUE_S) 86 | 87 | def check_lazy_status(self, *args, **kwargs): 88 | return _check_big_switch_lazy_status(*args, **kwargs) 89 | 90 | def pick(self, select, **args): 91 | return (args.get("input_" + str(select), None),) 92 | 93 | 94 | class DreamLazyPaletteSwitch: 95 | _switch_type = RGBPalette.ID 96 | NODE_NAME = "Lazy Palette Switch" 97 | ICON = "⭆" 98 | CATEGORY = NodeCategories.UTILS_SWITCHES 99 | RETURN_TYPES = (_switch_type,) 100 | RETURN_NAMES = ("selected",) 101 | FUNCTION = "pick" 102 | 103 | @classmethod 104 | def INPUT_TYPES(cls): 105 | return _generate_switch_input(cls._switch_type) 106 | 107 | def check_lazy_status(self, *args, **kwargs): 108 | return _check_big_switch_lazy_status(*args, **kwargs) 109 | 110 | def pick(self, select, **args): 111 | return (args.get("input_" + str(select), None),) 112 | 113 | 114 | class DreamLazyFloatSwitch: 115 | _switch_type = "FLOAT" 116 | NODE_NAME = "Lazy Float Switch" 117 | ICON = "⭆" 118 | CATEGORY = NodeCategories.UTILS_SWITCHES 119 | RETURN_TYPES = (_switch_type,) 120 | RETURN_NAMES = ("selected",) 121 | FUNCTION = "pick" 122 | 123 | @classmethod 124 | def INPUT_TYPES(cls): 125 | return _generate_switch_input(cls._switch_type, _NOT_A_VALUE_F) 126 | 127 | def check_lazy_status(self, *args, **kwargs): 128 | return _check_big_switch_lazy_status(*args, **kwargs) 129 | 130 | def pick(self, select, **args): 131 | return (args.get("input_" + str(select), None),) 132 | 133 | 134 | class DreamLazyIntSwitch: 135 | _switch_type = "INT" 136 | NODE_NAME = "Lazy Int Switch" 137 | ICON = "⭆" 138 | CATEGORY = NodeCategories.UTILS_SWITCHES 139 | RETURN_TYPES = (_switch_type,) 140 | RETURN_NAMES = ("selected",) 141 | FUNCTION = "pick" 142 | 143 | @classmethod 144 | def INPUT_TYPES(cls): 145 | return _generate_switch_input(cls._switch_type, _NOT_A_VALUE_I) 146 | 147 | def check_lazy_status(self, *args, **kwargs): 148 | return _check_big_switch_lazy_status(*args, **kwargs) 149 | 150 | def pick(self, select, **args): 151 | return (args.get("input_" + str(select), None),) 152 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Morgan Johansson/Dream Project 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /loaders.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .categories import NodeCategories 3 | from .shared import list_images_in_directory, DreamImage 4 | from .dreamtypes import SharedTypes, FrameCounter 5 | import os 6 | 7 | 8 | class DreamImageSequenceInputWithDefaultFallback: 9 | NODE_NAME = "Image Sequence Loader" 10 | ICON = "💾" 11 | 12 | @classmethod 13 | def INPUT_TYPES(cls): 14 | return { 15 | "required": SharedTypes.frame_counter | { 16 | "directory_path": ("STRING", {"default": '', "multiline": False}), 17 | "pattern": ("STRING", {"default": '*', "multiline": False}), 18 | "indexing": (["numeric", "alphabetic order"],) 19 | }, 20 | "optional": { 21 | "default_image": ("IMAGE", {"default": None}) 22 | } 23 | } 24 | 25 | CATEGORY = NodeCategories.IMAGE_ANIMATION 26 | RETURN_TYPES = ("IMAGE","STRING") 27 | RETURN_NAMES = ("image","frame_name") 28 | FUNCTION = "result" 29 | 30 | @classmethod 31 | def IS_CHANGED(cls, *values, **kwargs): 32 | return float("NaN") 33 | 34 | def result(self, frame_counter: FrameCounter, directory_path, pattern, indexing, **other): 35 | default_image = other.get("default_image", None) 36 | entries = list_images_in_directory(directory_path, pattern, indexing == "alphabetic order") 37 | entry = entries.get(frame_counter.current_frame, None) 38 | if not entry: 39 | return (default_image, "") 40 | else: 41 | image_names = [os.path.basename(file_path) for file_path in entry] 42 | images = map(lambda f: DreamImage(file_path=f), entry) 43 | return (DreamImage.join_to_tensor_data(images), image_names[0]) 44 | -------------------------------------------------------------------------------- /node_list.json: -------------------------------------------------------------------------------- 1 | { 2 | "Analyze Palette [Dream]": "Output brightness, contrast, red, green and blue averages of a palette", 3 | "Beat Curve [Dream]": "Beat pattern curve with impulses at specified beats of a measure", 4 | "Big Float Switch [Dream]": "Switch for up to 10 inputs", 5 | "Big Image Switch [Dream]": "Switch for up to 10 inputs", 6 | "Big Int Switch [Dream]": "Switch for up to 10 inputs", 7 | "Big Latent Switch [Dream]": "Switch for up to 10 inputs", 8 | "Big Palette Switch [Dream]": "Switch for up to 10 inputs", 9 | "Big Text Switch [Dream]": "Switch for up to 10 inputs", 10 | "Boolean To Float [Dream]": "Converts a boolean value to two different float values", 11 | "Boolean To Int [Dream]": "Converts a boolean value to two different int values", 12 | "Build Prompt [Dream]": "Weighted text prompt builder utility", 13 | "CSV Curve [Dream]": "CSV input curve where first column is frame or second and second column is value", 14 | "CSV Generator [Dream]": "CSV output, mainly for debugging purposes", 15 | "Calculation [Dream]": "Mathematical calculation node", 16 | "Common Frame Dimensions [Dream]": "Utility for calculating good width/height based on common video dimensions", 17 | "Compare Palettes [Dream]": "Analyses two palettes producing the factor for each color channel", 18 | "FFMPEG Video Encoder [Dream]": "Post processing for animation sequences calling FFMPEG to generate video file", 19 | "File Count [Dream]": "Finds the number of files in a directory matching specified patterns", 20 | "Finalize Prompt [Dream]": "Used in conjunction with 'Build Prompt'", 21 | "Float Input [Dream]": "Float input (until primitive routing issues are solved)", 22 | "Float to Log Entry [Dream]": "Logging for float values", 23 | "Frame Count Calculator [Dream]": "Simple utility to calculate number of frames based on duration and framerate", 24 | "Frame Counter (Directory) [Dream]": "Directory backed frame counter, for output directories", 25 | "Frame Counter (Simple) [Dream]": "Integer value used as frame counter", 26 | "Frame Counter Info [Dream]": "Extracts information from the frame counter", 27 | "Frame Counter Offset [Dream]": "Adds an offset to a frame counter", 28 | "Frame Counter Time Offset [Dream]": "Adds an offset to a frame counter in seconds", 29 | "Image Brightness Adjustment [Dream]": "Adjusts the brightness of an image by a factor", 30 | "Image Color Shift [Dream]": "Adjust the colors (or brightness) of an image", 31 | "Image Contrast Adjustment [Dream]": "Adjusts the contrast of an image by a factor", 32 | "Image Motion [Dream]": "Node supporting zooming in/out and translating an image", 33 | "Image Sequence Blend [Dream]": "Post processing for animation sequences blending frame for a smoother blurred effect", 34 | "Image Sequence Loader [Dream]": "Loads a frame from a directory of images", 35 | "Image Sequence Saver [Dream]": "Saves a frame to a directory", 36 | "Image Sequence Tweening [Dream]": "Post processing for animation sequences generating blended in-between frames", 37 | "Int Input [Dream]": "Integer input (until primitive routing issues are solved)", 38 | "Int to Log Entry [Dream]": "Logging for int values", 39 | "Laboratory [Dream]": "Super-charged number generator for experimenting with ComfyUI", 40 | "Linear Curve [Dream]": "Linear interpolation between two value over the full animation", 41 | "Log Entry Joiner [Dream]": "Merges multiple log entries (reduces noodling)", 42 | "Log File [Dream]": "Logging node for output to file", 43 | "Noise from Area Palettes [Dream]": "Generates noise based on the colors of up to nine different palettes", 44 | "Noise from Palette [Dream]": "Generates noise based on the colors in a palette", 45 | "Palette Color Align [Dream]": "Shifts the colors of one palette towards another target palette", 46 | "Palette Color Shift [Dream]": "Multiplies the color values in a palette", 47 | "Random Prompt Words [Dream]": "Picks random words from input", 48 | "Sample Image Area as Palette [Dream]": "Samples a palette from an image based on pre-defined areas", 49 | "Sample Image as Palette [Dream]": "Randomly samples pixel values to build a palette from an image", 50 | "Saw Curve [Dream]": "Saw wave curve", 51 | "Sine Curve [Dream]": "Simple sine wave curve", 52 | "Smooth Event Curve [Dream]": "Single event/peak curve with a slight bell-shape", 53 | "String Input [Dream]": "String input (until primitive routing issues are solved)", 54 | "String Tokenizer [Dream]": "Extract individual words or phrases from a text as tokens", 55 | "String to Log Entry [Dream]": "Use any string as a log entry", 56 | "Text Input [Dream]": "Multiline string input (until primitive routing issues are solved)", 57 | "Triangle Curve [Dream]": "Triangle wave curve", 58 | "Triangle Event Curve [Dream]": "Single event/peak curve with triangular shape", 59 | "WAV Curve [Dream]": "WAV audio file as a curve" 60 | } -------------------------------------------------------------------------------- /noise.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import math 3 | 4 | from .categories import NodeCategories 5 | from .shared import * 6 | from .dreamtypes import * 7 | 8 | 9 | def _generate_noise(image: DreamImage, color_function, rng: random.Random, block_size, blur_amount, 10 | density) -> DreamImage: 11 | w = block_size[0] 12 | h = block_size[1] 13 | blur_radius = round(max(image.width, image.height) * blur_amount * 0.25) 14 | if w <= (image.width // 128) or h <= (image.height // 128): 15 | return image 16 | max_placements = round(density * (image.width * image.height)) 17 | num = min(max_placements, round((image.width * image.height * 2) / (w * h))) 18 | for i in range(num): 19 | x = rng.randint(-w + 1, image.width - 1) 20 | y = rng.randint(-h + 1, image.height - 1) 21 | image.color_area(x, y, w, h, color_function(x + (w >> 1), y + (h >> 1))) 22 | image = image.blur(blur_radius) 23 | return _generate_noise(image, color_function, rng, (w >> 1, h >> 1), blur_amount, density) 24 | 25 | 26 | class DreamNoiseFromPalette: 27 | NODE_NAME = "Noise from Palette" 28 | ICON = "🌫" 29 | 30 | @classmethod 31 | def INPUT_TYPES(cls): 32 | return { 33 | "required": SharedTypes.palette | { 34 | "width": ("INT", {"default": 512, "min": 1, "max": 8192}), 35 | "height": ("INT", {"default": 512, "min": 1, "max": 8192}), 36 | "blur_amount": ("FLOAT", {"default": 0.3, "min": 0, "max": 1.0, "step": 0.05}), 37 | "density": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 1.0, "step": 0.025}), 38 | "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}) 39 | }, 40 | } 41 | 42 | CATEGORY = NodeCategories.IMAGE_GENERATE 43 | RETURN_TYPES = ("IMAGE",) 44 | RETURN_NAMES = ("image",) 45 | FUNCTION = "result" 46 | 47 | def result(self, palette: Tuple[RGBPalette], width, height, seed, blur_amount, density): 48 | outputs = list() 49 | rng = random.Random() 50 | for p in palette: 51 | seed += 1 52 | color_iterator = p.random_iteration(seed) 53 | image = DreamImage(pil_image=Image.new("RGB", (width, height), color=next(color_iterator))) 54 | image = _generate_noise(image, lambda x, y: next(color_iterator), rng, 55 | (image.width >> 1, image.height >> 1), blur_amount, density) 56 | outputs.append(image) 57 | 58 | return (DreamImage.join_to_tensor_data(outputs),) 59 | 60 | 61 | class DreamNoiseFromAreaPalettes: 62 | NODE_NAME = "Noise from Area Palettes" 63 | 64 | @classmethod 65 | def INPUT_TYPES(cls): 66 | return { 67 | "optional": { 68 | "top_left_palette": (RGBPalette.ID,), 69 | "top_center_palette": (RGBPalette.ID,), 70 | "top_right_palette": (RGBPalette.ID,), 71 | "center_left_palette": (RGBPalette.ID,), 72 | "center_palette": (RGBPalette.ID,), 73 | "center_right_palette": (RGBPalette.ID,), 74 | "bottom_left_palette": (RGBPalette.ID,), 75 | "bottom_center_palette": (RGBPalette.ID,), 76 | "bottom_right_palette": (RGBPalette.ID,), 77 | }, 78 | "required": { 79 | "area_sharpness": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05}), 80 | "width": ("INT", {"default": 512, "min": 1, "max": 8192}), 81 | "height": ("INT", {"default": 512, "min": 1, "max": 8192}), 82 | "blur_amount": ("FLOAT", {"default": 0.3, "min": 0, "max": 1.0, "step": 0.05}), 83 | "density": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 1.0, "step": 0.025}), 84 | "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), 85 | }, 86 | } 87 | 88 | CATEGORY = NodeCategories.IMAGE_GENERATE 89 | ICON = "🌫" 90 | RETURN_TYPES = ("IMAGE",) 91 | RETURN_NAMES = ("image",) 92 | FUNCTION = "result" 93 | 94 | def _area_coordinates(self, width, height): 95 | dx = width / 6 96 | dy = height / 6 97 | return { 98 | "top_left_palette": (dx, dy), 99 | "top_center_palette": (dx * 3, dy), 100 | "top_right_palette": (dx * 5, dy), 101 | "center_left_palette": (dx, dy * 3), 102 | "center_palette": (dx * 3, dy * 3), 103 | "center_right_palette": (dx * 5, dy * 3), 104 | "bottom_left_palette": (dx * 1, dy * 5), 105 | "bottom_center_palette": (dx * 3, dy * 5), 106 | "bottom_right_palette": (dx * 5, dy * 5), 107 | } 108 | 109 | def _pick_random_area(self, active_coordinates, x, y, rng, area_sharpness): 110 | def _dst(x1, y1, x2, y2): 111 | a = x1 - x2 112 | b = y1 - y2 113 | return math.sqrt(a * a + b * b) 114 | 115 | distances = list(map(lambda item: (item[0], _dst(item[1][0], item[1][1], x, y)), active_coordinates)) 116 | areas_by_weight = list( 117 | map(lambda item: (math.pow((1.0 / max(1, item[1])), 0.5 + 4.5 * area_sharpness), item[0]), distances)) 118 | return pick_random_by_weight(areas_by_weight, rng) 119 | 120 | def _setup_initial_colors(self, image: DreamImage, color_func): 121 | w = image.width 122 | h = image.height 123 | wpart = round(w / 3) 124 | hpart = round(h / 3) 125 | for i in range(3): 126 | for j in range(3): 127 | image.color_area(wpart * i, hpart * j, w, h, 128 | color_func(wpart * i + w // 2, hpart * j + h // 2)) 129 | 130 | def result(self, width, height, seed, blur_amount, density, area_sharpness, **palettes): 131 | outputs = list() 132 | rng = random.Random() 133 | coordinates = self._area_coordinates(width, height) 134 | active_palettes = list(filter(lambda pair: pair[1] is not None and len(pair[1]) > 0, palettes.items())) 135 | active_coordinates = list(map(lambda item: (item[0], coordinates[item[0]]), active_palettes)) 136 | 137 | n = max(list(map(len, palettes.values())) + [0]) 138 | for b in range(n): 139 | batch_palettes = dict(map(lambda item: (item[0], item[1][b].random_iteration(seed)), active_palettes)) 140 | 141 | def _color_func(x, y): 142 | name = self._pick_random_area(active_coordinates, x, y, rng, area_sharpness) 143 | rgb = batch_palettes[name] 144 | return next(rgb) 145 | 146 | image = DreamImage(pil_image=Image.new("RGB", (width, height))) 147 | self._setup_initial_colors(image, _color_func) 148 | image = _generate_noise(image, _color_func, rng, (round(image.width / 3), round(image.height / 3)), 149 | blur_amount, density) 150 | outputs.append(image) 151 | 152 | if not outputs: 153 | outputs.append(DreamImage(pil_image=Image.new("RGB", (width, height)))) 154 | 155 | return (DreamImage.join_to_tensor_data(outputs),) 156 | -------------------------------------------------------------------------------- /output.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import os 4 | 5 | import folder_paths as comfy_paths 6 | from PIL.PngImagePlugin import PngInfo 7 | 8 | from .categories import NodeCategories 9 | from .shared import DreamImageProcessor, DreamImage, \ 10 | list_images_in_directory, DreamConfig 11 | from .dreamtypes import SharedTypes, FrameCounter, AnimationSequence, LogEntry 12 | 13 | CONFIG = DreamConfig() 14 | 15 | 16 | def _save_png(pil_image, filepath, embed_info, prompt, extra_pnginfo): 17 | info = PngInfo() 18 | if extra_pnginfo is not None: 19 | for item in extra_pnginfo: 20 | info.add_text(item, json.dumps(extra_pnginfo[item])) 21 | if prompt is not None: 22 | info.add_text("prompt", json.dumps(prompt)) 23 | if embed_info: 24 | pil_image.save(filepath, pnginfo=info, optimize=True) 25 | else: 26 | pil_image.save(filepath, optimize=True) 27 | 28 | 29 | def _save_jpg(pil_image, filepath, quality): 30 | pil_image.save(filepath, quality=quality, optimize=True) 31 | 32 | 33 | class DreamImageSequenceOutput: 34 | NODE_NAME = "Image Sequence Saver" 35 | ICON = "💾" 36 | 37 | @classmethod 38 | def INPUT_TYPES(cls): 39 | return { 40 | "required": SharedTypes.frame_counter | { 41 | "image": ("IMAGE",), 42 | "directory_path": ("STRING", {"default": comfy_paths.output_directory, "multiline": False}), 43 | "prefix": ("STRING", {"default": 'frame', "multiline": False}), 44 | "digits": ("INT", {"default": 5}), 45 | "at_end": (["stop output", "raise error", "keep going"],), 46 | "filetype": (['png with embedded workflow', "png", 'jpg'],), 47 | }, 48 | "hidden": { 49 | "prompt": "PROMPT", 50 | "extra_pnginfo": "EXTRA_PNGINFO" 51 | }, 52 | } 53 | 54 | CATEGORY = NodeCategories.IMAGE_ANIMATION 55 | RETURN_TYPES = (AnimationSequence.ID, LogEntry.ID) 56 | OUTPUT_NODE = True 57 | RETURN_NAMES = ("sequence", "log_entry") 58 | FUNCTION = "save" 59 | 60 | def _get_new_filename(self, current_frame, prefix, digits, filetype): 61 | return prefix + "_" + str(current_frame).zfill(digits) + "." + filetype.split(" ")[0] 62 | 63 | def _save_single_image(self, dream_image: DreamImage, batch_counter, frame_counter: FrameCounter, 64 | directory_path, 65 | prefix, digits, filetype, prompt, extra_pnginfo, at_end, logger): 66 | 67 | if at_end == "stop output" and frame_counter.is_after_last_frame: 68 | logger("Reached end of animation - not saving output!") 69 | return () 70 | if at_end == "raise error" and frame_counter.is_after_last_frame: 71 | logger("Reached end of animation - raising error to stop processing!") 72 | raise Exception("Reached end of animation!") 73 | filename = self._get_new_filename(frame_counter.current_frame, prefix, digits, filetype) 74 | if batch_counter >= 0: 75 | filepath = os.path.join(directory_path, "batch_" + (str(batch_counter).zfill(4)), filename) 76 | else: 77 | filepath = os.path.join(directory_path, filename) 78 | save_dir = os.path.dirname(filepath) 79 | if not os.path.isdir(save_dir): 80 | os.makedirs(save_dir) 81 | if filetype.startswith("png"): 82 | dream_image.save_png(filepath, filetype == 'png with embedded workflow', prompt, extra_pnginfo) 83 | elif filetype == "jpg": 84 | dream_image.save_jpg(filepath, int(CONFIG.get("encoding.jpeg_quality", 95))) 85 | logger("Saved {} in {}".format(filename, os.path.abspath(save_dir))) 86 | return () 87 | 88 | def _generate_animation_sequence(self, filetype, directory_path, frame_counter): 89 | if filetype.startswith("png"): 90 | pattern = "*.png" 91 | else: 92 | pattern = "*.jpg" 93 | frames = list_images_in_directory(directory_path, pattern, False) 94 | return AnimationSequence(frame_counter, frames) 95 | 96 | def save(self, image, **args): 97 | log_texts = list() 98 | logger = lambda s: log_texts.append(s) 99 | if not args.get("directory_path", ""): 100 | args["directory_path"] = comfy_paths.output_directory 101 | args["logger"] = logger 102 | proc = DreamImageProcessor(image, **args) 103 | proc.process(self._save_single_image) 104 | frame_counter = args["frame_counter"] 105 | log_entry = LogEntry([]) 106 | for text in log_texts: 107 | log_entry = log_entry.add(text) 108 | if frame_counter.is_final_frame: 109 | return (self._generate_animation_sequence(args["filetype"], args["directory_path"], 110 | frame_counter), log_entry) 111 | else: 112 | return (AnimationSequence(frame_counter), log_entry) 113 | -------------------------------------------------------------------------------- /prompting.py: -------------------------------------------------------------------------------- 1 | from .categories import NodeCategories 2 | from .dreamtypes import PartialPrompt 3 | import random 4 | 5 | class DreamRandomPromptWords: 6 | NODE_NAME = "Random Prompt Words" 7 | ICON = "⚅" 8 | 9 | @classmethod 10 | def INPUT_TYPES(cls): 11 | return { 12 | "optional": { 13 | "partial_prompt": (PartialPrompt.ID,) 14 | }, 15 | "required": { 16 | "words": ("STRING", {"default": "", "multiline": True}), 17 | "separator": ("STRING", {"default": ",", "multiline": False}), 18 | "samples": ("INT", {"default": 1, "min": 1, "max": 100}), 19 | "min_weight": ("FLOAT", {"default": 1.0, "min": -10, "max": 10}), 20 | "max_weight": ("FLOAT", {"default": 1.0, "min": -10, "max": 10}), 21 | "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), 22 | }, 23 | } 24 | 25 | CATEGORY = NodeCategories.CONDITIONING 26 | RETURN_TYPES = (PartialPrompt.ID,) 27 | RETURN_NAMES = ("partial_prompt",) 28 | FUNCTION = "result" 29 | 30 | def result(self, words: str, separator, samples, min_weight, max_weight, seed, **args): 31 | p = args.get("partial_prompt", PartialPrompt()) 32 | rnd = random.Random() 33 | rnd.seed(seed) 34 | words = list(set(map(lambda s: s.strip(), filter(lambda s: s.strip() != "", words.split(separator))))) 35 | samples = min(samples, len(words)) 36 | for i in range(samples): 37 | picked_word = words[rnd.randint(0, len(words)-1)] 38 | words = list(filter(lambda s: s!=picked_word, words)) 39 | weight = rnd.uniform(min_weight, max_weight) 40 | p = p.add(picked_word, weight) 41 | return (p,) 42 | 43 | 44 | class DreamWeightedPromptBuilder: 45 | NODE_NAME = "Build Prompt" 46 | ICON = "⚖" 47 | 48 | @classmethod 49 | def INPUT_TYPES(cls): 50 | return { 51 | "optional": { 52 | "partial_prompt": (PartialPrompt.ID,) 53 | }, 54 | "required": { 55 | "added_prompt": ("STRING", {"default": "", "multiline": True}), 56 | "weight": ("FLOAT", {"default": 1.0}), 57 | }, 58 | } 59 | 60 | CATEGORY = NodeCategories.CONDITIONING 61 | RETURN_TYPES = (PartialPrompt.ID,) 62 | RETURN_NAMES = ("partial_prompt",) 63 | FUNCTION = "result" 64 | 65 | def result(self, added_prompt, weight, **args): 66 | input = args.get("partial_prompt", PartialPrompt()) 67 | p = input.add(added_prompt, weight) 68 | return (p,) 69 | 70 | 71 | class DreamPromptFinalizer: 72 | NODE_NAME = "Finalize Prompt" 73 | ICON = "🗫" 74 | 75 | @classmethod 76 | def INPUT_TYPES(cls): 77 | return { 78 | "required": { 79 | "partial_prompt": (PartialPrompt.ID,), 80 | "adjustment": (["raw", "by_abs_max", "by_abs_sum"],), 81 | "clamp": ("FLOAT", {"default": 2.0, "min": 0.1, "step": 0.1}), 82 | "adjustment_reference": ("FLOAT", {"default": 1.0, "min": 0.1}), 83 | }, 84 | } 85 | 86 | CATEGORY = NodeCategories.CONDITIONING 87 | RETURN_TYPES = ("STRING", "STRING") 88 | RETURN_NAMES = ("positive", "negative") 89 | FUNCTION = "result" 90 | 91 | 92 | def result(self, partial_prompt: PartialPrompt, adjustment, adjustment_reference, clamp): 93 | if adjustment == "raw" or partial_prompt.is_empty(): 94 | return partial_prompt.finalize(clamp) 95 | elif adjustment == "by_abs_sum": 96 | f = adjustment_reference / partial_prompt.abs_sum() 97 | return partial_prompt.scaled_by(f).finalize(clamp) 98 | else: 99 | f = adjustment_reference / partial_prompt.abs_max() 100 | return partial_prompt.scaled_by(f).finalize(clamp) 101 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui-dream-project" 3 | description = "This extension offers various nodes that are useful for Deforum-like animations in ComfyUI." 4 | version = "5.1.2" 5 | license = { text = "MIT License" } 6 | dependencies = ["imageio", "pilgram", "scipy", "numpy<2.0,>=1.18", "torchvision", "evalidate"] 7 | 8 | [project.urls] 9 | Repository = "https://github.com/alt-key-project/comfyui-dream-project" 10 | # Used by Comfy Registry https://comfyregistry.org 11 | 12 | [tool.comfy] 13 | PublisherId = "altkeyproject" 14 | DisplayName = "comfyui-dream-project" 15 | Icon = "" 16 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Dream Project Animation Nodes for ComfyUI 2 | 3 | This repository contains various nodes for supporting Deforum-style animation generation with ComfyUI. I created these 4 | for my own use (producing videos for my "Alt Key Project" music - 5 | [youtube channel](https://www.youtube.com/channel/UC4cKvJ4hia7zULxeCc-7OcQ)), but I think they should be generic enough 6 | and useful to many ComfyUI users. 7 | 8 | I have demonstrated the use of these custom nodes in this [youtube video](https://youtu.be/pZ6Li3qF-Kk). 9 | 10 | # Notice! 11 | 12 | This custom node pack is currently not being updated. Stable Diffusion video generation is moving towards a different 13 | workflow with AnimateDiff and Stable Video Diffusion. I decided to not try to update this node pack, but I am instead 14 | creating a separate custom node pack here: 15 | 16 | [github](https://github.com/alt-key-project/comfyui-dream-video-batches) 17 | 18 | This new node pack will be getting my attention from now on (at least as long as stable diffusion video generation is done mostly 19 | in batches). 20 | 21 | ## Installation 22 | 23 | ### Simple option 24 | 25 | You can install Dream Project Animation Nodes using the ComfyUI Manager. 26 | 27 | ### Manual option 28 | 29 | Run within (ComfyUI)/custom_nodes/ folder: 30 | 31 | * git clone https://github.com/alt-key-project/comfyui-dream-project.git 32 | * cd comfyui-dream-project 33 | 34 | Then, if you are using the python embedded in ComfyUI: 35 | * (ComfyUI)/python_embedded/python.exe -s -m pip install -r requirements.txt 36 | 37 | With your system-wide python: 38 | * pip install -r requirements.txt 39 | 40 | Finally: 41 | * Start ComfyUI. 42 | 43 | After startup, a configuration file 'config.json' should have been created in the 'comfyui-dream-project' directory. 44 | Specifically check that the path of ffmpeg works in your system (add full path to the command if needed). 45 | 46 | ## Upgrade 47 | 48 | When upgrading, it is good to re-run the pip install command as specified in the install section. This will install any 49 | new dependencies. 50 | 51 | ## Configuration 52 | 53 | ### debug 54 | 55 | Setting this to true will enable some trace-level logging. 56 | 57 | ### ffmpeg.file_extension 58 | 59 | Sets the output file extension and with that the envelope used. 60 | 61 | ### ffmpeg.path 62 | 63 | Path to the ffmpeg executable or just the command if ffmpeg is in PATH. 64 | 65 | ### ffmpeg.arguments 66 | 67 | The arguments sent to FFMPEG. A few of the values are provided by the node: 68 | 69 | * %FPS% the target framerate 70 | * %FRAMES% a frame ionput file 71 | * %OUTPUT% output video file path 72 | 73 | ### encoding.jpeg__quality 74 | 75 | Sets the encoding quality of jpeg images. 76 | 77 | ### ui.top_category 78 | 79 | Sets the name of the top level category on the menu. Set to empty string "" to remove the top level. If the top level 80 | is removed you may also want to disable the category icons to get nodes into existing category folders. 81 | 82 | ### prepend_icon_to_category / append_icon_to_category 83 | 84 | Flags to add a icon before and/or after the category name at each level. 85 | 86 | ### prepend_icon_icon_to_node / append_icon_icon_to_node 87 | 88 | Flags to add an icon before and/or after the node name. 89 | 90 | ### ui.category_icons 91 | 92 | Each key defines a unicode symbol as an icon used for the specified category. 93 | 94 | ### mpeg_coder.bitrate_factor 95 | 96 | This factor allows changing the bitrate to better fit the required quality and codec. A value of 1 is typically 97 | suitable for H.265. 98 | 99 | ### mpeg_coder.codec_name 100 | 101 | Codec names as specified by ffmpeg. Some common options include "libx264", "libx264" and "mpeg2video". 102 | 103 | ### mpeg_coder.encoding_threads 104 | 105 | Increasing the number of encoding threads in mpegCoder will generally reduce the overall encoding time, but it will also 106 | increase the load on the computer. 107 | 108 | ### mpeg_coder.file_extension 109 | 110 | Sets the output file extension and with that the envelope used. 111 | 112 | ### mpeg_coder.max_b_frame 113 | 114 | Sets the max-b-frames parameter for as specified in ffmpeg. 115 | 116 | ## Concepts used 117 | 118 | These are some concepts used in nodes: 119 | 120 | ### Frame Counter 121 | 122 | The frame counter is an abstraction that keeps track of where we are in the animation - what frame is rendered 123 | and how does the current frame fit into the current animation. 124 | 125 | ### Curves 126 | 127 | A curve is simply a node that produces a value based on the frame counter (changing over time). 128 | 129 | ### Palette 130 | 131 | A palette is a collection of color values. 132 | 133 | ### Sequence 134 | 135 | A sequence is a full set of animation frames and a corresponding timeline for these frames. The sequence is 136 | created by the 'Image Sequence Saver' node and it may be used to trigger post processing tasks such as generating the 137 | video file using ffmpeg. These nodes should be seen as a convenience and they are severely limited. Never put sequence 138 | nodes in parallel - they will not work as intended! 139 | 140 | ## The nodes 141 | ### Analyze Palette [Dream] 142 | Output brightness, red, green and blue averages of a palette. Useful to control other processing. 143 | 144 | ### Beat Curve [Dream] 145 | Beat pattern curve with impulses at specified beats of a measure. 146 | 147 | ### Big *** Switch [Dream] 148 | Switch nodes for different type for up to ten inputs. 149 | 150 | ### Boolean To Float/Int [Dream] 151 | Converts a boolean value to two different numeric values. 152 | 153 | ### Build Prompt [Dream] (and Finalize Prompt [Dream]) 154 | Weighted text prompt builder utility. Chain any number of these nodes and terminate with 'Finalize Prompt'. 155 | 156 | ### Calculation [Dream] 157 | Mathematical calculation node. Exposes most of the mathematical functions in the python 158 | [math module](https://docs.python.org/3/library/math.html), mathematical operators as well as round, abs, int, 159 | float, max and min. 160 | 161 | ### Compare Palettes [Dream] 162 | Analyses two palettes and produces the quotient for each individual channel (b/a) and brightness. 163 | 164 | ### CSV Curve [Dream] 165 | CSV input curve where first column is frame or second and second column is value. 166 | 167 | ### CSV Generator [Dream] 168 | CSV output, mainly for debugging purposes. First column is frame number and second is value. 169 | Recreates file at frame 0 (removing and existing content in the file). 170 | 171 | ### Common Frame Dimensions [Dream] 172 | Utility for calculating good width/height based on common video dimensions. 173 | 174 | ### Video Encoder (FFMPEG) [Dream] 175 | Post processing for animation sequences calling FFMPEG to generate video files. 176 | 177 | ### File Count [Dream] 178 | Finds the number of files in a directory matching specified patterns. 179 | 180 | ### Float/Int/string to Log Entry [Dream] 181 | Logging for float/int/string values. 182 | 183 | ### Frame Count Calculator [Dream] 184 | Simple utility to calculate number of frames based on time and framerate. 185 | 186 | ### Frame Counter (Directory) [Dream] 187 | Directory backed frame counter, for output directories. 188 | 189 | ### Frame Counter (Simple) [Dream] 190 | Integer value used as frame counter. Useful for testing or if an auto-incrementing primitive is used as a frame 191 | counter. 192 | 193 | ### Frame Counter Info [Dream] 194 | Extracts information from the frame counter. 195 | 196 | ### Frame Counter Offset [Dream] 197 | Adds an offset (in frames) to a frame counter. 198 | 199 | ### Frame Counter Time Offset [Dream] 200 | Adds an offset in seconds to a frame counter. 201 | 202 | ### Image Brightness Adjustment [Dream] 203 | Adjusts the brightness of an image by a factor. 204 | 205 | ### Image Color Shift [Dream] 206 | Allows changing the colors of an image with a multiplier for each channel (RGB). 207 | 208 | ### Image Contrast Adjustment [Dream] 209 | Adjusts the contrast of an image by a factor. 210 | 211 | ### Image Motion [Dream] 212 | Node supporting zooming in/out and translating an image. 213 | 214 | ### Image Sequence Blend [Dream] 215 | Post processing for animation sequences blending frame for a smoother blurred effect. 216 | 217 | ### Image Sequence Loader [Dream] 218 | Loads a frame from a directory of images. 219 | 220 | ### Image Sequence Saver [Dream] 221 | Saves a frame to a directory. 222 | 223 | ### Image Sequence Tweening [Dream] 224 | Post processing for animation sequences generating blended in-between frames. 225 | 226 | ### Laboratory [Dream] 227 | Super-charged number generator for experimenting with ComfyUI. 228 | 229 | ### Lazy *** Switch [Dream] 230 | Switch nodes for different type for up to ten inputs. The lazy version only evaluates the 231 | selected input, but first/next of the big switch is unsupported. 232 | 233 | ### Log Entry Joiner [Dream] 234 | Merges multiple log entries (reduces noodling). 235 | 236 | ### Log File [Dream] 237 | The text logging facility for the Dream Project Animation nodes. 238 | 239 | ### Linear Curve [Dream] 240 | Linear interpolation between two values over the full animation. 241 | 242 | ### Noise from Area Palettes [Dream] 243 | Generates noise based on the colors of up to nine different palettes, each connected to position/area of the 244 | image. Although the palettes are optional, at least one palette should be provided. 245 | 246 | ### Noise from Palette [Dream] 247 | Generates noise based on the colors in a palette. 248 | 249 | ### Palette Color Align [Dream] 250 | Shifts the colors of one palette towards another target palette. If the alignment factor 251 | is 0.5 the result is nearly an average of the two palettes. At 0 no alignment is done and at 1 we get a close 252 | alignment to the target. Above one we will overshoot the alignment. 253 | 254 | ### Palette Color Shift [Dream] 255 | Multiplies the color values in a palette to shift the color balance or brightness. 256 | 257 | ### Sample Image Area as Palette [Dream] 258 | Randomly samples a palette from an image based on pre-defined areas. The image is separated into nine rectangular areas 259 | of equal size and each node may sample one of these. 260 | 261 | ### Sample Image as Palette [Dream] 262 | Randomly samples pixels from a source image to build a palette from it. 263 | 264 | ### Saw Curve [Dream] 265 | Saw wave curve. 266 | 267 | ### Sine Curve [Dream] 268 | Simple sine wave curve. 269 | 270 | ### Smooth Event Curve [Dream] 271 | Single event/peak curve with a slight bell-shape. 272 | 273 | ### String Tokenizer [Dream] 274 | Splits a text into tokens by a separator and returns one of the tokens based on a given index. 275 | 276 | ### Random Prompt Words [Dream] 277 | Randomly picks words/tokens/phrases from an input text. 278 | 279 | ### Triangle Curve [Dream] 280 | Triangle wave curve. 281 | 282 | ### Triangle Event Curve [Dream] 283 | Single event/peak curve with triangular shape. 284 | 285 | ### WAV Curve [Dream] 286 | Use an uncompressed WAV audio file as a curve. 287 | 288 | ### Other custom nodes 289 | 290 | Many of the nodes found in 'WAS Node Suite' are useful the Dream Project Animation nodes - I suggest you install those 291 | custom nodes as well! 292 | 293 | ## Examples 294 | 295 | ### Image Motion with Curves 296 | 297 | This example should be a starting point for anyone wanting to build with the Dream Project Animation nodes. 298 | 299 | [motion-workflow-example](examples/motion-workflow-example.json) 300 | 301 | ### Image Motion with Color Coherence 302 | 303 | Same as above but with added color coherence through palettes. 304 | 305 | [motion-workflow-with-color-coherence](examples/motion-workflow-with-color-coherence.json) 306 | 307 | ### Area Sampled Noise 308 | 309 | This flow demonstrates sampling image areas into palettes and generating noise for these areas. 310 | 311 | [area-sampled-noise](examples/area-sampled-noise.json) 312 | 313 | ### Prompt Morphing 314 | 315 | This flow demonstrates prompt building with weights based on curves and brightness and contrast control. 316 | 317 | [prompt-morphing](examples/prompt-morphing.json) 318 | 319 | ### Laboratory 320 | 321 | This flow demonstrates use of the Laboratory and Logging nodes. 322 | 323 | [laboratory](examples/laboratory.json) 324 | 325 | ## Known issues 326 | 327 | ### FFMPEG 328 | 329 | The call to FFMPEG currently in the default configuration (in config.json) does not seem to work for everyone. The good 330 | news is that you can change the arguments to whatever works for you - the node-supplied parameters (that probably all need to be in the call) 331 | are: 332 | 333 | * -i %FRAMES% (the input file listing frames) 334 | * -r %FPS% (sets the frame rate) 335 | * %OUTPUT% (the path to the video file) 336 | 337 | If possible, I will change the default configuration to one that more versions/builds of ffmpeg will accept. Do let me 338 | know what arguments are causing issues for you! 339 | 340 | ### Framerate is not always right with mpegCoder encoding node 341 | 342 | The mpegCoder library will always use variable frame rate encoding if it is available in the output format. With most 343 | outputs this means that your actual framerate will differ slightly from the requested one. 344 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | imageio 2 | pilgram 3 | scipy 4 | numpy<2.0,>=1.18 5 | torchvision 6 | evalidate 7 | -------------------------------------------------------------------------------- /seq_processing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import shutil 4 | import subprocess 5 | import tempfile 6 | from functools import lru_cache 7 | 8 | from PIL import Image as PilImage 9 | 10 | from .categories import NodeCategories 11 | from .err import on_error 12 | from .shared import DreamConfig 13 | #from .shared import MpegEncoderUtility 14 | from .dreamtypes import * 15 | 16 | CONFIG = DreamConfig() 17 | 18 | 19 | @lru_cache(5) 20 | def _load_image_cached(filename): 21 | return PilImage.open(filename) 22 | 23 | 24 | class TempFileSet: 25 | def __init__(self): 26 | self._files = dict() 27 | 28 | def add(self, temppath, finalpath): 29 | self._files[temppath] = finalpath 30 | 31 | def remove(self): 32 | for f in self._files.keys(): 33 | os.unlink(f) 34 | 35 | def finalize(self): 36 | for a, b in self._files.items(): 37 | shutil.move(a, b) 38 | self._files = dict() 39 | 40 | 41 | class AnimationSeqProcessor: 42 | def __init__(self, sequence: AnimationSequence): 43 | self._sequence = sequence 44 | self._input_cache = {} 45 | self._inputs = {} 46 | self._output_dirs = {} 47 | for b in self._sequence.batches: 48 | self._inputs[b] = list(self._sequence.get_image_files_of_batch(b)) 49 | self._output_dirs[b] = os.path.dirname(os.path.abspath(self._inputs[b][0])) 50 | self._ext = os.path.splitext(self._inputs[0][0])[1].lower() 51 | self._length = len(self._inputs[0]) 52 | 53 | def _load_input(self, batch_id, index) -> DreamImage: 54 | files = self._inputs[batch_id] 55 | index = min(max(0, index), len(files) - 1) 56 | filename = files[index] 57 | return DreamImage(pil_image=_load_image_cached(filename)) 58 | 59 | def _process_single_batch(self, batch_id, indices, index_offsets: List[int], fun, output_dir) -> List[str]: 60 | all_indices = list(indices) 61 | last_index = max(all_indices) 62 | workset = TempFileSet() 63 | rnd = random.randint(0, 1000000) 64 | result_files = list() 65 | try: 66 | for index in all_indices: 67 | images = list(map(lambda offset: self._load_input(batch_id, index + offset), index_offsets)) 68 | 69 | result: Dict[int, DreamImage] = fun(index, last_index, images) 70 | for (result_index, img) in result.items(): 71 | filepath = os.path.join(output_dir, 72 | "tmp_" + str(rnd) + "_" + (str(result_index).zfill(8)) + self._ext) 73 | filepath_final = os.path.join(output_dir, "seq_" + (str(result_index).zfill(8)) + self._ext) 74 | if self._ext == ".png": 75 | img.save_png(filepath) 76 | else: 77 | img.save_jpg(filepath, quality=CONFIG.get("encoding.jpeg_quality", 98)) 78 | workset.add(filepath, filepath_final) 79 | result_files.append(filepath_final) 80 | # all done with batch - remove input files 81 | for oldfile in self._inputs[batch_id]: 82 | os.unlink(oldfile) 83 | workset.finalize() 84 | return result_files 85 | finally: 86 | workset.remove() 87 | 88 | def process(self, index_offsets: List[int], fun): 89 | results = dict() 90 | new_length = 0 91 | for batch_id in self._sequence.batches: 92 | resulting_filenames = self._process_single_batch(batch_id, range(len(self._inputs[batch_id])), 93 | index_offsets, fun, 94 | self._output_dirs[batch_id]) 95 | for (index, filename) in enumerate(resulting_filenames): 96 | l = results.get(index, []) 97 | l.append(filename) 98 | results[index] = l 99 | new_length = len(resulting_filenames) 100 | new_fps = self._sequence.frame_counter.frames_per_second * (float(new_length) / self._length) 101 | counter = FrameCounter(new_length - 1, new_length, new_fps) 102 | return AnimationSequence(counter, results) 103 | 104 | 105 | def _ffmpeg(config, filenames, fps, output): 106 | fps = float(fps) 107 | duration = 1.0 / fps 108 | tmp = tempfile.NamedTemporaryFile(delete=False, mode="wb") 109 | tempfilepath = tmp.name 110 | try: 111 | for filename in filenames: 112 | filename = filename.replace("\\", "/") 113 | tmp.write(f"file '{filename}'\n".encode()) 114 | tmp.write(f"duration {duration}\n".encode()) 115 | finally: 116 | tmp.close() 117 | 118 | try: 119 | cmd = [config.get("ffmpeg.path", "ffmpeg")] 120 | cmd.extend(config.get("ffmpeg.arguments")) 121 | replacements = {"%FPS%": str(fps), "%FRAMES%": tempfilepath, "%OUTPUT%": output} 122 | 123 | for (key, value) in replacements.items(): 124 | cmd = list(map(lambda s: s.replace(key, value), cmd)) 125 | 126 | subprocess.check_output(cmd, shell=True) 127 | finally: 128 | os.unlink(tempfilepath) 129 | 130 | 131 | def _make_video_filename(name, file_ext): 132 | (b, _) = os.path.splitext(name) 133 | return b + "." + file_ext.strip(".") 134 | 135 | # 136 | # class DreamVideoEncoderMpegCoder: 137 | # NODE_NAME = "Video Encoder (mpegCoder)" 138 | # ICON = "🎬" 139 | # CATEGORY = NodeCategories.ANIMATION_POSTPROCESSING 140 | # RETURN_TYPES = (LogEntry.ID,) 141 | # RETURN_NAMES = ("log_entry",) 142 | # OUTPUT_NODE = True 143 | # FUNCTION = "encode" 144 | # 145 | # @classmethod 146 | # def INPUT_TYPES(cls): 147 | # return { 148 | # "required": SharedTypes.sequence | { 149 | # "name": ("STRING", {"default": 'video', "multiline": False}), 150 | # "framerate_factor": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100.0}), 151 | # "remove_images": ("BOOLEAN", {"default": True}) 152 | # }, 153 | # } 154 | # 155 | # def _find_free_filename(self, filename, defaultdir): 156 | # if os.path.basename(filename) == filename: 157 | # filename = os.path.join(defaultdir, filename) 158 | # n = 1 159 | # tested = filename 160 | # while os.path.exists(tested): 161 | # n += 1 162 | # (b, ext) = os.path.splitext(filename) 163 | # tested = b + "_" + str(n) + ext 164 | # return tested 165 | # 166 | # def encode(self, sequence, name, framerate_factor, remove_images): 167 | # if not sequence.is_defined: 168 | # return (LogEntry([]),) 169 | # config = DreamConfig() 170 | # filename = _make_video_filename(name, config.get("mpeg_coder.file_extension", "mp4")) 171 | # log_entry = LogEntry([]) 172 | # for batch_num in sequence.batches: 173 | # try: 174 | # images = list(sequence.get_image_files_of_batch(batch_num)) 175 | # filename = self._find_free_filename(filename, os.path.dirname(images[0])) 176 | # first_image = DreamImage.from_file(images[0]) 177 | # enc = MpegEncoderUtility(video_path=filename, 178 | # bit_rate_factor=float(config.get("mpeg_coder.bitrate_factor", 1.0)), 179 | # encoding_threads=int(config.get("mpeg_coder.encoding_threads", 4)), 180 | # max_b_frame=int(config.get("mpeg_coder.max_b_frame", 2)), 181 | # width=first_image.width, 182 | # height=first_image.height, 183 | # files=images, 184 | # fps=sequence.fps * framerate_factor, 185 | # codec_name=config.get("mpeg_coder.codec_name", "libx265")) 186 | # enc.encode() 187 | # log_entry = log_entry.add("Generated video '{}'".format(filename)) 188 | # if remove_images: 189 | # for imagepath in images: 190 | # if os.path.isfile(imagepath): 191 | # os.unlink(imagepath) 192 | # except Exception as e: 193 | # on_error(self.__class__, str(e)) 194 | # return (log_entry,) 195 | # 196 | 197 | class DreamVideoEncoder: 198 | NODE_NAME = "FFMPEG Video Encoder" 199 | DISPLAY_NAME = "Video Encoder (FFMPEG)" 200 | ICON = "🎬" 201 | 202 | @classmethod 203 | def INPUT_TYPES(cls): 204 | return { 205 | "required": SharedTypes.sequence | { 206 | "name": ("STRING", {"default": 'video', "multiline": False}), 207 | "framerate_factor": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100.0}), 208 | "remove_images": ("BOOLEAN", {"default": True}) 209 | }, 210 | } 211 | 212 | CATEGORY = NodeCategories.ANIMATION_POSTPROCESSING 213 | RETURN_TYPES = (LogEntry.ID,) 214 | RETURN_NAMES = ("log_entry",) 215 | OUTPUT_NODE = True 216 | FUNCTION = "encode" 217 | 218 | @classmethod 219 | def IS_CHANGED(cls, sequence: AnimationSequence, **kwargs): 220 | return sequence.is_defined 221 | 222 | def _find_free_filename(self, filename, defaultdir): 223 | if os.path.basename(filename) == filename: 224 | filename = os.path.join(defaultdir, filename) 225 | n = 1 226 | tested = filename 227 | while os.path.exists(tested): 228 | n += 1 229 | (b, ext) = os.path.splitext(filename) 230 | tested = b + "_" + str(n) + ext 231 | return tested 232 | 233 | def generate_video(self, files, fps, filename, config): 234 | filename = self._find_free_filename(filename, os.path.dirname(files[0])) 235 | _ffmpeg(config, files, fps, filename) 236 | return filename 237 | 238 | def encode(self, sequence: AnimationSequence, name: str, remove_images, framerate_factor): 239 | if not sequence.is_defined: 240 | return (LogEntry([]),) 241 | 242 | config = DreamConfig() 243 | filename = _make_video_filename(name, config.get("ffmpeg.file_extension", "mp4")) 244 | log_entry = LogEntry([]) 245 | for batch_num in sequence.batches: 246 | try: 247 | images = list(sequence.get_image_files_of_batch(batch_num)) 248 | actual_filename = self.generate_video(images, sequence.fps * framerate_factor, filename, config) 249 | 250 | log_entry = log_entry.add("Generated video '{}'".format(actual_filename)) 251 | if remove_images: 252 | for imagepath in images: 253 | if os.path.isfile(imagepath): 254 | os.unlink(imagepath) 255 | except Exception as e: 256 | on_error(self.__class__, str(e)) 257 | return (log_entry,) 258 | 259 | 260 | class DreamSequenceTweening: 261 | NODE_NAME = "Image Sequence Tweening" 262 | 263 | @classmethod 264 | def INPUT_TYPES(cls): 265 | return { 266 | "required": SharedTypes.sequence | { 267 | "multiplier": ("INT", {"default": 2, "min": 2, "max": 10}), 268 | }, 269 | } 270 | 271 | CATEGORY = NodeCategories.ANIMATION_POSTPROCESSING 272 | RETURN_TYPES = (AnimationSequence.ID,) 273 | RETURN_NAMES = ("sequence",) 274 | OUTPUT_NODE = False 275 | FUNCTION = "process" 276 | 277 | @classmethod 278 | def IS_CHANGED(cls, sequence: AnimationSequence, **kwargs): 279 | return sequence.is_defined 280 | 281 | def process(self, sequence: AnimationSequence, multiplier): 282 | if not sequence.is_defined: 283 | return (sequence,) 284 | 285 | def _generate_extra_frames(input_index, last_index, images): 286 | results = {} 287 | if input_index == last_index: 288 | # special case 289 | for i in range(multiplier): 290 | results[input_index * multiplier + i] = images[0] 291 | return results 292 | 293 | # normal case 294 | current_frame = images[0] 295 | next_frame = images[1] 296 | for i in range(multiplier): 297 | alpha = float(i + 1) / multiplier 298 | results[multiplier * input_index + i] = current_frame.blend(next_frame, 1.0 - alpha, alpha) 299 | return results 300 | 301 | proc = AnimationSeqProcessor(sequence) 302 | return (proc.process([0, 1], _generate_extra_frames),) 303 | 304 | 305 | class DreamSequenceBlend: 306 | NODE_NAME = "Image Sequence Blend" 307 | 308 | @classmethod 309 | def INPUT_TYPES(cls): 310 | return { 311 | "required": SharedTypes.sequence | { 312 | "fade_in": ("FLOAT", {"default": 0.1, "min": 0.01, "max": 0.5}), 313 | "fade_out": ("FLOAT", {"default": 0.1, "min": 0.01, "max": 0.5}), 314 | "iterations": ("INT", {"default": 1, "min": 1, "max": 10}), 315 | }, 316 | } 317 | 318 | CATEGORY = NodeCategories.ANIMATION_POSTPROCESSING 319 | RETURN_TYPES = (AnimationSequence.ID,) 320 | RETURN_NAMES = ("sequence",) 321 | OUTPUT_NODE = False 322 | FUNCTION = "process" 323 | 324 | @classmethod 325 | def IS_CHANGED(cls, sequence: AnimationSequence, **kwargs): 326 | return sequence.is_defined 327 | 328 | def process(self, sequence: AnimationSequence, fade_in, fade_out, iterations): 329 | if not sequence.is_defined: 330 | return (sequence,) 331 | 332 | current_sequence = sequence 333 | for i in range(iterations): 334 | proc = AnimationSeqProcessor(current_sequence) 335 | 336 | def _blur(index: int, last_index: int, images: List[DreamImage]): 337 | pre_frame = images[0].blend(images[1], fade_in, 1.0) 338 | post_frame = images[2].blend(images[1], fade_out, 1.0) 339 | return {index: pre_frame.blend(post_frame)} 340 | 341 | current_sequence = proc.process([-1, 0, 1], _blur) 342 | 343 | return (current_sequence,) 344 | -------------------------------------------------------------------------------- /shared.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import hashlib 4 | import json 5 | import os 6 | import random 7 | import tempfile 8 | import glob 9 | from io import BytesIO 10 | 11 | import numpy 12 | import torch 13 | from PIL import Image, ImageFilter, ImageEnhance 14 | from PIL.ImageDraw import ImageDraw 15 | from PIL.PngImagePlugin import PngInfo 16 | from typing import Dict, Tuple, List 17 | 18 | from .dreamlogger import DreamLog 19 | from .embedded_config import EMBEDDED_CONFIGURATION 20 | 21 | tmpDir = tempfile.TemporaryDirectory("Dream_Anim") 22 | 23 | NODE_FILE = os.path.abspath(__file__) 24 | DREAM_NODES_SOURCE_ROOT = os.path.dirname(NODE_FILE) 25 | TEMP_PATH = os.path.join(os.path.abspath(tempfile.gettempdir()), "Dream_Anim") 26 | 27 | 28 | def convertTensorImageToPIL(tensor_image) -> Image: 29 | return Image.fromarray(numpy.clip(255. * tensor_image.cpu().numpy().squeeze(), 0, 255).astype(numpy.uint8)) 30 | 31 | 32 | def convertFromPILToTensorImage(pil_image): 33 | return torch.from_numpy(numpy.array(pil_image).astype(numpy.float32) / 255.0).unsqueeze(0) 34 | 35 | 36 | def _replace_pil_image(data): 37 | if isinstance(data, Image.Image): 38 | return DreamImage(pil_image=data) 39 | else: 40 | return data 41 | 42 | 43 | _config_data = None 44 | 45 | 46 | class DreamConfig: 47 | FILEPATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") 48 | DEFAULT_CONFIG = EMBEDDED_CONFIGURATION 49 | 50 | def __init__(self): 51 | global _config_data 52 | if not os.path.isfile(DreamConfig.FILEPATH): 53 | self._data = DreamConfig.DEFAULT_CONFIG 54 | self._save() 55 | if _config_data is None: 56 | with open(DreamConfig.FILEPATH, encoding="utf-8") as f: 57 | self._data = json.load(f) 58 | if self._merge_with_defaults(self._data, DreamConfig.DEFAULT_CONFIG): 59 | self._save() 60 | _config_data = self._data 61 | else: 62 | self._data = _config_data 63 | 64 | def _save(self): 65 | with open(DreamConfig.FILEPATH, "w", encoding="utf-8") as f: 66 | json.dump(self._data, f, indent=2) 67 | 68 | def _merge_with_defaults(self, config: dict, default_config: dict) -> bool: 69 | changed = False 70 | for key in default_config.keys(): 71 | if key not in config: 72 | changed = True 73 | config[key] = default_config[key] 74 | elif isinstance(default_config[key], dict): 75 | changed = changed or self._merge_with_defaults(config[key], default_config[key]) 76 | return changed 77 | 78 | def get(self, key: str, default=None): 79 | key = key.split(".") 80 | d = self._data 81 | for part in key: 82 | d = d.get(part, {}) 83 | if isinstance(d, dict) and not d: 84 | return default 85 | else: 86 | return d 87 | 88 | 89 | def get_logger(): 90 | config = DreamConfig() 91 | return DreamLog(config.get("debug", False)) 92 | 93 | 94 | class DreamImageProcessor: 95 | def __init__(self, inputs: torch.Tensor, **extra_args): 96 | self._images_in_batch = [convertTensorImageToPIL(tensor) for tensor in inputs] 97 | self._extra_args = extra_args 98 | self.is_batch = len(self._images_in_batch) > 1 99 | 100 | def process_PIL(self, fun): 101 | def _wrap(dream_image): 102 | pil_outputs = fun(dream_image.pil_image) 103 | return list(map(_replace_pil_image, pil_outputs)) 104 | 105 | return self.process(_wrap) 106 | 107 | def process(self, fun): 108 | output = [] 109 | batch_counter = 0 if self.is_batch else -1 110 | for pil_image in self._images_in_batch: 111 | exec_result = fun(DreamImage(pil_image=pil_image), batch_counter, **self._extra_args) 112 | exec_result = list(map(_replace_pil_image, exec_result)) 113 | if not output: 114 | output = [list() for i in range(len(exec_result))] 115 | for i in range(len(exec_result)): 116 | output[i].append(exec_result[i].create_tensor_image()) 117 | if batch_counter >= 0: 118 | batch_counter += 1 119 | return tuple(map(lambda l: torch.cat(l, dim=0), output)) 120 | 121 | 122 | def pick_random_by_weight(data: List[Tuple[float, object]], rng: random.Random): 123 | total_weight = sum(map(lambda item: item[0], data)) 124 | r = rng.random() 125 | for (weight, obj) in data: 126 | r -= weight / total_weight 127 | if r <= 0: 128 | return obj 129 | return data[0][1] 130 | 131 | 132 | class DreamImage: 133 | @classmethod 134 | def join_to_tensor_data(cls, images): 135 | l = list(map(lambda i: i.create_tensor_image(), images)) 136 | return torch.cat(l, dim=0) 137 | 138 | def __init__(self, tensor_image=None, pil_image=None, file_path=None, with_alpha=False): 139 | if pil_image is not None: 140 | self.pil_image = pil_image 141 | elif tensor_image is not None: 142 | self.pil_image = convertTensorImageToPIL(tensor_image) 143 | else: 144 | self.pil_image = Image.open(file_path) 145 | if with_alpha and self.pil_image.mode != "RGBA": 146 | self.pil_image = self.pil_image.convert("RGBA") 147 | else: 148 | if self.pil_image.mode not in ("RGB", "RGBA"): 149 | self.pil_image = self.pil_image.convert("RGB") 150 | self.width = self.pil_image.width 151 | self.height = self.pil_image.height 152 | self.size = self.pil_image.size 153 | self._draw = ImageDraw(self.pil_image) 154 | 155 | def change_brightness(self, factor): 156 | enhancer = ImageEnhance.Brightness(self.pil_image) 157 | return DreamImage(pil_image=enhancer.enhance(factor)) 158 | 159 | def change_contrast(self, factor): 160 | enhancer = ImageEnhance.Contrast(self.pil_image) 161 | return DreamImage(pil_image=enhancer.enhance(factor)) 162 | 163 | def numpy_array(self): 164 | return numpy.array(self.pil_image) 165 | 166 | def _renew(self, pil_image): 167 | self.pil_image = pil_image 168 | self._draw = ImageDraw(self.pil_image) 169 | 170 | def __iter__(self): 171 | class _Pixels: 172 | def __init__(self, image: DreamImage): 173 | self.x = 0 174 | self.y = 0 175 | self._img = image 176 | 177 | def __next__(self) -> Tuple[int, int, int, int]: 178 | if self.x >= self._img.width: 179 | self.y += 1 180 | self.x = 1 181 | if self.y >= self._img.height: 182 | raise StopIteration 183 | p = self._img.get_pixel(self.x, self.y) 184 | self.x += 1 185 | return (p, self.x, self.y) 186 | 187 | return _Pixels(self) 188 | 189 | def convert(self, mode="RGB"): 190 | if self.pil_image.mode == mode: 191 | return self 192 | return DreamImage(pil_image=self.pil_image.convert(mode)) 193 | 194 | def create_tensor_image(self): 195 | return convertFromPILToTensorImage(self.pil_image) 196 | 197 | def blend(self, other, weight_self: float = 0.5, weight_other: float = 0.5): 198 | alpha = 1.0 - weight_self / (weight_other + weight_self) 199 | return DreamImage(pil_image=Image.blend(self.pil_image, other.pil_image, alpha)) 200 | 201 | def color_area(self, x, y, w, h, col): 202 | self._draw.rectangle((x, y, x + w - 1, y + h - 1), fill=col, outline=col) 203 | 204 | def blur(self, amount): 205 | return DreamImage(pil_image=self.pil_image.filter(ImageFilter.GaussianBlur(amount))) 206 | 207 | def adjust_colors(self, red_factor=1.0, green_factor=1.0, blue_factor=1.0): 208 | # newRed = 1.1*oldRed + 0*oldGreen + 0*oldBlue + constant 209 | # newGreen = 0*oldRed + 0.9*OldGreen + 0*OldBlue + constant 210 | # newBlue = 0*oldRed + 0*OldGreen + 1*OldBlue + constant 211 | matrix = (red_factor, 0, 0, 0, 212 | 0, green_factor, 0, 0, 213 | 0, 0, blue_factor, 0) 214 | return DreamImage(pil_image=self.pil_image.convert("RGB", matrix)) 215 | 216 | def get_pixel(self, x, y): 217 | p = self.pil_image.getpixel((x, y)) 218 | if len(p) == 4: 219 | return p 220 | else: 221 | return (p[0], p[1], p[2], 255) 222 | 223 | def set_pixel(self, x, y, pixelvalue): 224 | if len(pixelvalue) == 4: 225 | self.pil_image.putpixel((x, y), pixelvalue) 226 | else: 227 | self.pil_image.putpixel((x, y), (pixelvalue[0], pixelvalue[1], pixelvalue[2], 255)) 228 | 229 | def save_png(self, filepath, embed_info=False, prompt=None, extra_pnginfo=None): 230 | info = PngInfo() 231 | print(filepath) 232 | if extra_pnginfo is not None: 233 | for item in extra_pnginfo: 234 | info.add_text(item, json.dumps(extra_pnginfo[item])) 235 | if prompt is not None: 236 | info.add_text("prompt", json.dumps(prompt)) 237 | if embed_info: 238 | self.pil_image.save(filepath, pnginfo=info, optimize=True) 239 | else: 240 | self.pil_image.save(filepath, optimize=True) 241 | 242 | def save_jpg(self, filepath, quality=98): 243 | self.pil_image.save(filepath, quality=quality, optimize=True) 244 | 245 | @classmethod 246 | def from_file(cls, file_path): 247 | return DreamImage(pil_image=Image.open(file_path)) 248 | 249 | 250 | class DreamMask: 251 | def __init__(self, tensor_image=None, pil_image=None): 252 | if pil_image: 253 | self.pil_image = pil_image 254 | else: 255 | self.pil_image = convertTensorImageToPIL(tensor_image) 256 | if self.pil_image.mode != "L": 257 | self.pil_image = self.pil_image.convert("L") 258 | 259 | def create_tensor_image(self): 260 | return torch.from_numpy(numpy.array(self.pil_image).astype(numpy.float32) / 255.0) 261 | 262 | 263 | def list_images_in_directory(directory_path: str, pattern: str, alphabetic_index: bool) -> Dict[int, List[str]]: 264 | if not os.path.isdir(directory_path): 265 | return {} 266 | dirs_to_search = [directory_path] 267 | if os.path.isdir(os.path.join(directory_path, "batch_0001")): 268 | dirs_to_search = list() 269 | for i in range(10000): 270 | dirpath = os.path.join(directory_path, "batch_" + (str(i).zfill(4))) 271 | if not os.path.isdir(dirpath): 272 | break 273 | else: 274 | dirs_to_search.append(dirpath) 275 | 276 | def _num_from_filename(fn): 277 | (text, _) = os.path.splitext(fn) 278 | token = text.split("_")[-1] 279 | if token.isdigit(): 280 | return int(token) 281 | else: 282 | return -1 283 | 284 | result = dict() 285 | for search_path in dirs_to_search: 286 | files = [] 287 | for file_name in glob.glob(os.path.join(search_path, pattern), recursive=False): 288 | if file_name.lower().endswith(('.jpeg', '.jpg', '.png', '.tiff', '.gif', '.bmp', '.webp')): 289 | files.append(os.path.abspath(file_name)) 290 | 291 | if alphabetic_index: 292 | files.sort() 293 | for idx, item in enumerate(files): 294 | lst = result.get(idx, []) 295 | lst.append(item) 296 | result[idx] = lst 297 | else: 298 | for filepath in files: 299 | idx = _num_from_filename(os.path.basename(filepath)) 300 | lst = result.get(idx, []) 301 | lst.append(filepath) 302 | result[idx] = lst 303 | return result 304 | 305 | 306 | class DreamStateStore: 307 | def __init__(self, name, read_fun, write_fun): 308 | self._read = read_fun 309 | self._write = write_fun 310 | self._name = name 311 | 312 | def _as_key(self, k): 313 | return self._name + "_" + k 314 | 315 | def get(self, key, default): 316 | v = self[key] 317 | if v is None: 318 | return default 319 | else: 320 | return v 321 | 322 | def update(self, key, default, f): 323 | prev = self.get(key, default) 324 | v = f(prev) 325 | self[key] = v 326 | return v 327 | 328 | def __getitem__(self, item): 329 | return self._read(self._as_key(item)) 330 | 331 | def __setitem__(self, key, value): 332 | return self._write(self._as_key(key), value) 333 | 334 | 335 | class DreamStateFile: 336 | def __init__(self, state_collection_name="state"): 337 | self._filepath = os.path.join(TEMP_PATH, state_collection_name+".json") 338 | self._dirname = os.path.dirname(self._filepath) 339 | if not os.path.isdir(self._dirname): 340 | os.makedirs(self._dirname) 341 | if not os.path.isfile(self._filepath): 342 | self._data = {} 343 | else: 344 | with open(self._filepath, encoding="utf-8") as f: 345 | self._data = json.load(f) 346 | 347 | def get_section(self, name: str) -> DreamStateStore: 348 | return DreamStateStore(name, self._read, self._write) 349 | 350 | def _read(self, key): 351 | return self._data.get(key, None) 352 | 353 | def _write(self, key, value): 354 | previous = self._data.get(key, None) 355 | if value is None: 356 | if key in self._data: 357 | del self._data[key] 358 | else: 359 | self._data[key] = value 360 | with open(self._filepath, "w", encoding="utf-8") as f: 361 | json.dump(self._data, f) 362 | return previous 363 | 364 | 365 | def hash_tensor_data(image, hasher = None): 366 | m = hashlib.sha256() if hasher is None else hasher 367 | if isinstance(image, torch.Tensor): 368 | buff = BytesIO() 369 | torch.save(image, buff) 370 | print("HASHING TENSOR") 371 | m.update(buff.getvalue()) 372 | elif isinstance(image, bytes) or isinstance(image, bytearray): 373 | print("HASHING BYTES") 374 | m.update(image) 375 | elif isinstance(image, list) or isinstance(image, tuple): 376 | print("HASHING ITERABLE") 377 | for item in image: 378 | hash_tensor_data(item, m) 379 | else: 380 | print("HASING_AS_TEXT - "+str(type(image))) 381 | m.update(str(image).encode(encoding="utf-8")) 382 | return m.digest().hex() 383 | 384 | def hashed_as_strings(*items, **kwargs): 385 | tokens = "|".join(list(map(str, items))) 386 | m = hashlib.sha256() 387 | m.update(tokens.encode(encoding="utf-8")) 388 | for pair in kwargs.items(): 389 | m.update(str(pair).encode(encoding="utf-8")) 390 | return m.digest().hex() 391 | 392 | -------------------------------------------------------------------------------- /switches.py: -------------------------------------------------------------------------------- 1 | from .categories import NodeCategories 2 | from .dreamtypes import RGBPalette 3 | from .err import * 4 | from .shared import hashed_as_strings, hash_tensor_data 5 | 6 | _NOT_A_VALUE_I = 9223372036854775807 7 | _NOT_A_VALUE_F = float(_NOT_A_VALUE_I) 8 | _NOT_A_VALUE_S = "⭆" 9 | 10 | def _generate_switch_input(type_nm: str, default_value=None): 11 | d = dict() 12 | for i in range(10): 13 | if default_value is None: 14 | d["input_" + str(i)] = (type_nm,) 15 | else: 16 | d["input_" + str(i)] = (type_nm, {"default": default_value, "forceInput": True}) 17 | 18 | return { 19 | "required": { 20 | "select": ("INT", {"default": 0, "min": 0, "max": 9}), 21 | "on_missing": (["previous", "next"],) 22 | }, 23 | "optional": d 24 | } 25 | 26 | def _do_pick(cls, select, test_val, on_missing, **args): 27 | direction = 1 28 | if on_missing == "previous": 29 | direction = -1 30 | if len(args) == 0: 31 | on_error(cls, "No inputs provided!") 32 | n = len(args) 33 | while not test_val(args.get("input_" + str(select), None)): 34 | if n<0: 35 | return (None,) 36 | select = (select + direction) % 10 37 | n = n - 1 38 | return args["input_" + str(select)], 39 | 40 | 41 | class DreamBigImageSwitch: 42 | _switch_type = "IMAGE" 43 | NODE_NAME = "Big Image Switch" 44 | ICON = "⭆" 45 | CATEGORY = NodeCategories.UTILS_SWITCHES 46 | RETURN_TYPES = (_switch_type,) 47 | RETURN_NAMES = ("selected",) 48 | FUNCTION = "pick" 49 | 50 | @classmethod 51 | def INPUT_TYPES(cls): 52 | return _generate_switch_input(cls._switch_type) 53 | 54 | def pick(self, select, on_missing, **args): 55 | return _do_pick(self.__class__, select, lambda n: n is not None, on_missing, **args) 56 | 57 | 58 | class DreamBigLatentSwitch: 59 | _switch_type = "LATENT" 60 | NODE_NAME = "Big Latent Switch" 61 | ICON = "⭆" 62 | CATEGORY = NodeCategories.UTILS_SWITCHES 63 | RETURN_TYPES = (_switch_type,) 64 | RETURN_NAMES = ("selected",) 65 | FUNCTION = "pick" 66 | 67 | @classmethod 68 | def INPUT_TYPES(cls): 69 | return _generate_switch_input(cls._switch_type) 70 | 71 | def pick(self, select, on_missing, **args): 72 | return _do_pick(self.__class__, select, lambda n: n is not None, on_missing, **args) 73 | 74 | 75 | class DreamBigTextSwitch: 76 | _switch_type = "STRING" 77 | NODE_NAME = "Big Text Switch" 78 | ICON = "⭆" 79 | CATEGORY = NodeCategories.UTILS_SWITCHES 80 | RETURN_TYPES = (_switch_type,) 81 | RETURN_NAMES = ("selected",) 82 | FUNCTION = "pick" 83 | 84 | @classmethod 85 | def INPUT_TYPES(cls): 86 | return _generate_switch_input(cls._switch_type, _NOT_A_VALUE_S) 87 | 88 | def pick(self, select, on_missing, **args): 89 | return _do_pick(self.__class__, select, lambda n: (n is not None) and (n != _NOT_A_VALUE_S), on_missing, **args) 90 | 91 | 92 | class DreamBigPaletteSwitch: 93 | _switch_type = RGBPalette.ID 94 | NODE_NAME = "Big Palette Switch" 95 | ICON = "⭆" 96 | CATEGORY = NodeCategories.UTILS_SWITCHES 97 | RETURN_TYPES = (_switch_type,) 98 | RETURN_NAMES = ("selected",) 99 | FUNCTION = "pick" 100 | 101 | @classmethod 102 | def INPUT_TYPES(cls): 103 | return _generate_switch_input(cls._switch_type) 104 | 105 | def pick(self, select, on_missing, **args): 106 | return _do_pick(self.__class__, select, lambda n: (n is not None), on_missing, **args) 107 | 108 | 109 | class DreamBigFloatSwitch: 110 | _switch_type = "FLOAT" 111 | NODE_NAME = "Big Float Switch" 112 | ICON = "⭆" 113 | CATEGORY = NodeCategories.UTILS_SWITCHES 114 | RETURN_TYPES = (_switch_type,) 115 | RETURN_NAMES = ("selected",) 116 | FUNCTION = "pick" 117 | 118 | @classmethod 119 | def INPUT_TYPES(cls): 120 | return _generate_switch_input(cls._switch_type, _NOT_A_VALUE_F) 121 | 122 | def pick(self, select, on_missing, **args): 123 | return _do_pick(self.__class__, select, lambda n: (n is not None) and (n != _NOT_A_VALUE_F), on_missing, **args) 124 | 125 | 126 | class DreamBigIntSwitch: 127 | _switch_type = "INT" 128 | NODE_NAME = "Big Int Switch" 129 | ICON = "⭆" 130 | CATEGORY = NodeCategories.UTILS_SWITCHES 131 | RETURN_TYPES = (_switch_type,) 132 | RETURN_NAMES = ("selected",) 133 | FUNCTION = "pick" 134 | 135 | @classmethod 136 | def INPUT_TYPES(cls): 137 | return _generate_switch_input(cls._switch_type, _NOT_A_VALUE_I) 138 | 139 | def pick(self, select, on_missing, **args): 140 | return _do_pick(self.__class__, select, lambda n: (n is not None) and (n != _NOT_A_VALUE_I), on_missing, **args) 141 | 142 | 143 | class DreamBoolToFloat: 144 | NODE_NAME = "Boolean To Float" 145 | ICON = "⬖" 146 | CATEGORY = NodeCategories.UTILS_SWITCHES 147 | RETURN_TYPES = ("FLOAT",) 148 | RETURN_NAMES = ("result",) 149 | FUNCTION = "pick" 150 | 151 | @classmethod 152 | def INPUT_TYPES(cls): 153 | return { 154 | "required": { 155 | "boolean": ("BOOLEAN", {"default": False}), 156 | "on_true": ("FLOAT", {"default": 1.0}), 157 | "on_false": ("FLOAT", {"default": 0.0}) 158 | } 159 | } 160 | 161 | def pick(self, boolean, on_true, on_false): 162 | if boolean: 163 | return (on_true,) 164 | else: 165 | return (on_false,) 166 | 167 | 168 | class DreamBoolToInt: 169 | NODE_NAME = "Boolean To Int" 170 | ICON = "⬖" 171 | CATEGORY = NodeCategories.UTILS_SWITCHES 172 | RETURN_TYPES = ("INT",) 173 | RETURN_NAMES = ("result",) 174 | FUNCTION = "pick" 175 | 176 | @classmethod 177 | def INPUT_TYPES(cls): 178 | return { 179 | "required": { 180 | "boolean": ("BOOLEAN", {"default": False}), 181 | "on_true": ("INT", {"default": 1}), 182 | "on_false": ("INT", {"default": 0}) 183 | } 184 | } 185 | 186 | def pick(self, boolean, on_true, on_false): 187 | if boolean: 188 | return (on_true,) 189 | else: 190 | return (on_false,) 191 | -------------------------------------------------------------------------------- /uninstall.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | def run_uninstall(): 3 | pass 4 | 5 | 6 | if __name__ == "__main__": 7 | run_uninstall() 8 | -------------------------------------------------------------------------------- /utility.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | import math 4 | import os 5 | 6 | import folder_paths as comfy_paths 7 | 8 | from .categories import NodeCategories 9 | from .shared import hashed_as_strings, DreamStateFile 10 | from .dreamtypes import LogEntry, SharedTypes, FrameCounter 11 | 12 | _logfile_state = DreamStateFile("logging") 13 | 14 | 15 | class DreamJoinLog: 16 | NODE_NAME = "Log Entry Joiner" 17 | ICON = "🗎" 18 | CATEGORY = NodeCategories.UTILS 19 | RETURN_TYPES = (LogEntry.ID,) 20 | RETURN_NAMES = ("log_entry",) 21 | FUNCTION = "convert" 22 | 23 | @classmethod 24 | def INPUT_TYPES(cls): 25 | return { 26 | "optional": { 27 | "entry_0": (LogEntry.ID,), 28 | "entry_1": (LogEntry.ID,), 29 | "entry_2": (LogEntry.ID,), 30 | "entry_3": (LogEntry.ID,), 31 | } 32 | } 33 | 34 | def convert(self, **values): 35 | entry = LogEntry([]) 36 | for i in range(4): 37 | txt = values.get("entry_" + str(i), None) 38 | if txt: 39 | entry = entry.merge(txt) 40 | return (entry,) 41 | 42 | 43 | class DreamFloatToLog: 44 | NODE_NAME = "Float to Log Entry" 45 | ICON = "🗎" 46 | CATEGORY = NodeCategories.UTILS 47 | RETURN_TYPES = (LogEntry.ID,) 48 | RETURN_NAMES = ("log_entry",) 49 | FUNCTION = "convert" 50 | 51 | @classmethod 52 | def INPUT_TYPES(cls): 53 | return { 54 | "required": { 55 | "value": ("FLOAT", {"default": 0}), 56 | "label": ("STRING", {"default": ""}), 57 | }, 58 | } 59 | 60 | def convert(self, label, value): 61 | return (LogEntry.new(label + ": " + str(value)),) 62 | 63 | 64 | class DreamIntToLog: 65 | NODE_NAME = "Int to Log Entry" 66 | ICON = "🗎" 67 | CATEGORY = NodeCategories.UTILS 68 | RETURN_TYPES = (LogEntry.ID,) 69 | RETURN_NAMES = ("log_entry",) 70 | FUNCTION = "convert" 71 | 72 | @classmethod 73 | def INPUT_TYPES(cls): 74 | return { 75 | "required": { 76 | "value": ("INT", {"default": 0}), 77 | "label": ("STRING", {"default": ""}), 78 | }, 79 | } 80 | 81 | def convert(self, label, value): 82 | return (LogEntry.new(label + ": " + str(value)),) 83 | 84 | 85 | class DreamStringToLog: 86 | NODE_NAME = "String to Log Entry" 87 | ICON = "🗎" 88 | OUTPUT_NODE = True 89 | CATEGORY = NodeCategories.UTILS 90 | RETURN_TYPES = (LogEntry.ID,) 91 | RETURN_NAMES = ("log_entry",) 92 | FUNCTION = "convert" 93 | 94 | @classmethod 95 | def INPUT_TYPES(cls): 96 | return { 97 | "required": { 98 | "text": ("STRING", {"default": ""}), 99 | }, 100 | "optional": { 101 | "label": ("STRING", {"default": ""}), 102 | } 103 | } 104 | 105 | def convert(self, text, **values): 106 | label = values.get("label", "") 107 | if label: 108 | return (LogEntry.new(label + ": " + text),) 109 | else: 110 | return (LogEntry.new(text),) 111 | 112 | 113 | class DreamStringTokenizer: 114 | NODE_NAME = "String Tokenizer" 115 | ICON = "🪙" 116 | OUTPUT_NODE = True 117 | CATEGORY = NodeCategories.UTILS 118 | RETURN_TYPES = ("STRING",) 119 | RETURN_NAMES = ("token",) 120 | FUNCTION = "exec" 121 | 122 | @classmethod 123 | def INPUT_TYPES(cls): 124 | return { 125 | "required": { 126 | "text": ("STRING", {"default": "", "multiline": True}), 127 | "separator": ("STRING", {"default": ","}), 128 | "selected": ("INT", {"default": 0, "min": 0}) 129 | }, 130 | } 131 | 132 | def exec(self, text: str, separator: str, selected: int): 133 | if separator is None or separator == "": 134 | separator = " " 135 | parts = text.split(sep=separator) 136 | return (parts[abs(selected) % len(parts)].strip(),) 137 | 138 | 139 | class DreamLogFile: 140 | NODE_NAME = "Log File" 141 | ICON = "🗎" 142 | OUTPUT_NODE = True 143 | CATEGORY = NodeCategories.UTILS 144 | RETURN_TYPES = () 145 | RETURN_NAMES = () 146 | FUNCTION = "write" 147 | 148 | @classmethod 149 | def INPUT_TYPES(cls): 150 | return { 151 | "required": SharedTypes.frame_counter | { 152 | "log_directory": ("STRING", {"default": comfy_paths.output_directory}), 153 | "log_filename": ("STRING", {"default": "dreamlog.txt"}), 154 | "stdout": ("BOOLEAN", {"default": True}), 155 | "active": ("BOOLEAN", {"default": True}), 156 | "clock_has_24_hours": ("BOOLEAN", {"default": True}), 157 | }, 158 | "optional": { 159 | "entry_0": (LogEntry.ID,), 160 | "entry_1": (LogEntry.ID,), 161 | "entry_2": (LogEntry.ID,), 162 | "entry_3": (LogEntry.ID,), 163 | "entry_4": (LogEntry.ID,), 164 | "entry_5": (LogEntry.ID,), 165 | "entry_6": (LogEntry.ID,), 166 | "entry_7": (LogEntry.ID,), 167 | }, 168 | } 169 | 170 | def _path_to_log_file(self, log_directory, logfile): 171 | if os.path.isabs(logfile): 172 | return os.path.normpath(os.path.abspath(logfile)) 173 | elif os.path.isabs(log_directory): 174 | return os.path.normpath(os.path.abspath(os.path.join(log_directory, logfile))) 175 | elif log_directory: 176 | return os.path.normpath(os.path.abspath(os.path.join(comfy_paths.output_directory, log_directory, logfile))) 177 | else: 178 | return os.path.normpath(os.path.abspath(os.path.join(comfy_paths.output_directory, logfile))) 179 | 180 | def _get_tm_format(self, clock_has_24_hours): 181 | if clock_has_24_hours: 182 | return "%a %H:%M:%S" 183 | else: 184 | return "%a %I:%M:%S %p" 185 | 186 | def write(self, frame_counter: FrameCounter, log_directory, log_filename, stdout, active, clock_has_24_hours, 187 | **entries): 188 | if not active: 189 | return () 190 | log_entry = None 191 | for i in range(8): 192 | e = entries.get("entry_" + str(i), None) 193 | if e is not None: 194 | if log_entry is None: 195 | log_entry = e 196 | else: 197 | log_entry = log_entry.merge(e) 198 | log_file_path = self._path_to_log_file(log_directory, log_filename) 199 | ts = _logfile_state.get_section("timestamps").get(log_file_path, 0) 200 | output_text = list() 201 | last_t = 0 202 | for (t, text) in log_entry.get_filtered_entries(ts): 203 | dt = datetime.datetime.fromtimestamp(t) 204 | output_text.append("[frame {}/{} (~{}%), timestamp {}]\n{}".format(frame_counter.current_frame + 1, 205 | frame_counter.total_frames, 206 | round(frame_counter.progress * 100), 207 | dt.strftime(self._get_tm_format( 208 | clock_has_24_hours)), text.rstrip())) 209 | output_text.append("---") 210 | last_t = max(t, last_t) 211 | output_text = "\n".join(output_text) + "\n" 212 | if stdout: 213 | print(output_text) 214 | with open(log_file_path, "a", encoding="utf-8") as f: 215 | f.write(output_text) 216 | _logfile_state.get_section("timestamps").update(log_file_path, 0, lambda _: last_t) 217 | return () 218 | 219 | 220 | def _align_num(n: int, alignment: int, type: str): 221 | if alignment <= 1: 222 | return n 223 | if type == "ceil": 224 | return int(math.ceil(float(n) / alignment)) * alignment 225 | elif type == "floor": 226 | return int(math.floor(float(n) / alignment)) * alignment 227 | else: 228 | return int(round(float(n) / alignment)) * alignment 229 | 230 | 231 | class DreamFrameDimensions: 232 | NODE_NAME = "Common Frame Dimensions" 233 | ICON = "⌗" 234 | 235 | @classmethod 236 | def INPUT_TYPES(cls): 237 | return { 238 | "required": { 239 | "size": (["3840", "1920", "1440", "1280", "768", "720", "640", "512"],), 240 | "aspect_ratio": (["16:9", "16:10", "4:3", "1:1", "5:4", "3:2", "21:9", "14:9"],), 241 | "orientation": (["wide", "tall"],), 242 | "divisor": (["8", "4", "2", "1"],), 243 | "alignment": ("INT", {"default": 64, "min": 1, "max": 512}), 244 | "alignment_type": (["ceil", "floor", "nearest"],), 245 | }, 246 | } 247 | 248 | CATEGORY = NodeCategories.UTILS 249 | RETURN_TYPES = ("INT", "INT", "INT", "INT") 250 | RETURN_NAMES = ("width", "height", "final_width", "final_height") 251 | FUNCTION = "result" 252 | 253 | def result(self, size, aspect_ratio, orientation, divisor, alignment, alignment_type): 254 | ratio = tuple(map(int, aspect_ratio.split(":"))) 255 | final_width = int(size) 256 | final_height = int(round((float(final_width) * ratio[1]) / ratio[0])) 257 | width = _align_num(int(round(final_width / float(divisor))), alignment, alignment_type) 258 | height = _align_num(int(round((float(width) * ratio[1]) / ratio[0])), alignment, alignment_type) 259 | if orientation == "wide": 260 | return (width, height, final_width, final_height) 261 | else: 262 | return (height, width, final_height, final_width) 263 | --------------------------------------------------------------------------------