17 Commits
Author SHA1 Message Date
Morgan Johansson 533386aff5 3.1 - curves, calculations, prompt building and switches. 2023-09-13 18:52:07 +02:00
Morgan Johansson 84bd74c652 Fixed typo in config - file extension. 2023-09-10 19:41:01 +02:00
Morgan Johansson a6cf30f2ef Fixed examples to use the mpegCoder version. 2023-09-10 18:14:45 +02:00
Morgan Johansson cbe86aa142 Added support for mpegCoder as an alternative to ffmpeg CLI. 2023-09-10 17:47:38 +02:00
Morgan Johansson 6cccb2f495 Adding opencv as dependency. 2023-09-10 06:53:24 +02:00
Morgan Johansson 5540287e6f Improved examples. 2023-09-09 19:40:46 +02:00
Morgan Johansson 0f115c43c0 Fixed issue due to changes to parameter declaration in latest version of ComfyUI. 2023-09-09 19:37:49 +02:00
Morgan Johansson 6cf1d8049a Doc update. 2023-09-09 09:35:24 +02:00
Morgan Johansson 56549fb6e6 example update 2023-09-09 09:32:03 +02:00
Morgan Johansson 983b5f361c Fixed issue with error handling in ffmpeg and improved UI integration. 2023-09-09 08:59:25 +02:00
alt-key-project 88b41ce5a6 Update readme.md 2023-09-08 16:59:08 +02:00
alt-key-project 1d65a0df67 Update readme.md 2023-09-08 16:58:53 +02:00
alt-key-project 510799aba7 Update readme.md 2023-09-08 16:38:07 +02:00
Morgan Johansson 82387cf301 Merge branch 'colors2' 2023-09-08 07:04:32 +02:00
Morgan Johansson e9639a58c3 Color sampling/noise for areas. 2023-09-08 07:04:06 +02:00
alt-key-project 36ca96ff96 Update readme.md 2023-09-07 18:51:23 +02:00
Morgan Johansson 5e92827e0a Readme update - known issue. 2023-09-07 18:35:32 +02:00
32 changed files with 13004 additions and 3149 deletions
+57 -13
View File
@@ -1,27 +1,37 @@
from typing import List, Type
# -*- coding: utf-8 -*-
from typing import Type
from .seq_processing import *
from .base import *
from .curves import *
from .loaders import *
from .output import *
from .utility import *
from .colors import *
from .noise import *
from .curves import *
from .image_processing import *
from .inputfields import *
from .loaders import *
from .noise import *
from .output import *
from .prompting import *
from .seq_processing import *
from .switches import *
from .utility import *
from .calculate import *
_NODE_CLASSES: List[Type] = [DreamSineWave, DreamLinear, DreamCSVCurve, DreamBeatCurve, DreamFrameDimensions,
DreamImageMotion,
DreamImageMotion, DreamNoiseFromPalette, DreamAnalyzePalette, DreamColorShift,
DreamDirectoryFileCount, DreamFrameCounterOffset, DreamDirectoryBackedFrameCounter,
DreamSimpleFrameCounter, DreamImageSequenceInputWithDefaultFallback,
DreamImageSequenceOutput, DreamCSVGenerator,
DreamImageSequenceOutput, DreamCSVGenerator, DreamImageAreaSampler,
DreamVideoEncoder, DreamSequenceTweening, DreamSequenceBlend, DreamColorAlign,
DreamImageSampler, DreamNoiseFromPalette, DreamAnalyzePalette, DreamColorShift]
DreamImageSampler, DreamNoiseFromAreaPalettes, DreamVideoEncoderMpegCoder,
DreamInputString, DreamInputFloat, DreamInputInt, DreamInputText, DreamBigLatentSwitch,
DreamFrameCountCalculator, DreamBigImageSwitch, DreamBigTextSwitch, DreamBigFloatSwitch,
DreamBigIntSwitch, DreamBigPaletteSwitch, DreamWeightedPromptBuilder, DreamPromptFinalizer,
DreamFrameCounterInfo, DreamBoolToFloat, DreamBoolToInt, DreamSawWave, DreamTriangleWave,
DreamTriangleEvent, DreamSmoothEvent, DreamCalculation]
_SIGNATURE_SUFFIX = " [Dream]"
MANIFEST = {
"name": "Dream Project Animation",
"version": (1, 1, 0),
"version": (3, 1, 0),
"author": "Dream Project",
"project": "https://github.com/alt-key-project/comfyui-dream-project",
"description": "Various utility nodes for creating animations with ComfyUI",
@@ -31,13 +41,47 @@ NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
config = DreamConfig()
def update_category(cls):
top = config.get("ui.top_category", "").strip().strip("/")
leaf_icon = ""
if top and "CATEGORY" in cls.__dict__:
cls.CATEGORY = top + "/" + cls.CATEGORY.lstrip("/")
if "CATEGORY" in cls.__dict__:
joined = []
for partial in cls.CATEGORY.split("/"):
icon = config.get("ui.category_icons." + partial, "")
if icon:
leaf_icon = icon
if config.get("ui.prepend_icon_to_category", False):
partial = icon.lstrip() + " " + partial
if config.get("ui.append_icon_to_category", False):
partial = partial + " " + icon.rstrip()
joined.append(partial)
cls.CATEGORY = "/".join(joined)
return leaf_icon
def update_display_name(cls, category_icon, display_name):
icon = cls.__dict__.get("ICON", category_icon)
if config.get("ui.prepend_icon_to_node", False):
display_name = icon.lstrip() + " " + display_name
if config.get("ui.append_icon_to_node", False):
display_name = display_name + " " + icon.rstrip()
return display_name
for cls in _NODE_CLASSES:
category_icon = update_category(cls)
clsname = cls.__name__
if "NODE_NAME" in cls.__dict__:
node_name = cls.__dict__["NODE_NAME"] + _SIGNATURE_SUFFIX
NODE_CLASS_MAPPINGS[node_name] = cls
display_name = cls.__dict__.get("DISPLAY_NAME", cls.__dict__["NODE_NAME"]) + _SIGNATURE_SUFFIX
NODE_DISPLAY_NAME_MAPPINGS[node_name] = display_name
NODE_DISPLAY_NAME_MAPPINGS[node_name] = update_display_name(cls, category_icon,
cls.__dict__.get("DISPLAY_NAME",
cls.__dict__["NODE_NAME"]))
else:
raise Exception("Class {} is missing NODE_NAME!".format(str(cls)))
+74 -4
View File
@@ -1,11 +1,45 @@
# -*- coding: utf-8 -*-
import glob
from .categories import NodeCategories
from .shared import *
from .types import *
import glob
class DreamFrameCounterInfo:
NODE_NAME = "Frame Counter Info"
ICON = "⚋"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.frame_counter
}
CATEGORY = NodeCategories.ANIMATION
RETURN_TYPES = ("INT", "INT", "BOOLEAN", "BOOLEAN", "FLOAT", "FLOAT", "FLOAT", "FLOAT")
RETURN_NAMES = ("frames_completed", "total_frames", "first_frame", "last_frame",
"elapsed_seconds", "remaining_seconds", "total_seconds", "completion")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *v):
return ALWAYS_CHANGED_FLAG
def result(self, frame_counter: FrameCounter):
return (frame_counter.current_frame,
frame_counter.total_frames,
frame_counter.is_first_frame,
frame_counter.is_final_frame,
frame_counter.current_time_in_seconds,
frame_counter.remaining_time_in_seconds,
frame_counter.total_time_in_seconds,
frame_counter.current_time_in_seconds / max(0.01, frame_counter.total_time_in_seconds))
class DreamDirectoryFileCount:
NODE_NAME = "File Count"
ICON = "📂"
@classmethod
def INPUT_TYPES(cls):
@@ -25,18 +59,22 @@ class DreamDirectoryFileCount:
def IS_CHANGED(cls, *v):
return ALWAYS_CHANGED_FLAG
def result(self, directory_path, patterns, indexing):
def result(self, directory_path, patterns):
if not os.path.isdir(directory_path):
return (0,)
total = 0
for pattern in patterns.split("|"):
total += len(glob.glob(pattern))
files = list(glob.glob(pattern, root_dir=directory_path))
total += len(files)
print("total " + str(total))
return (total,)
class DreamFrameCounterOffset:
NODE_NAME = "Frame Counter Offset"
ICON = "±"
@classmethod
def INPUT_TYPES(cls):
return {
@@ -60,6 +98,7 @@ class DreamFrameCounterOffset:
class DreamSimpleFrameCounter:
NODE_NAME = "Frame Counter (Simple)"
ICON = "⚋"
@classmethod
def INPUT_TYPES(cls):
@@ -87,6 +126,7 @@ class DreamSimpleFrameCounter:
class DreamDirectoryBackedFrameCounter:
NODE_NAME = "Frame Counter (Directory)"
ICON = "⚋"
@classmethod
def INPUT_TYPES(cls):
@@ -96,7 +136,7 @@ class DreamDirectoryBackedFrameCounter:
"pattern": ("STRING", {"default": '*', "multiline": False}),
"indexing": (["numeric", "alphabetic order"],),
"total_frames": ("INT", {"default": 100, "min": 2, "max": 24 * 3600 * 60}),
"frames_per_second": ("INT", {"min": 1, "default": 25}),
"frames_per_second": ("INT", {"min": 1, "default": 30}),
},
}
@@ -115,3 +155,33 @@ class DreamDirectoryBackedFrameCounter:
return (FrameCounter(0, total_frames, frames_per_second),)
n = max(results.keys()) + 1
return (FrameCounter(n, total_frames, frames_per_second),)
class DreamFrameCountCalculator:
NODE_NAME = "Frame Count Calculator"
ICON = "⌛"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"hours": ("INT", {"min": 0, "default": 0, "max": 23}),
"minutes": ("INT", {"min": 0, "default": 0, "max": 59}),
"seconds": ("INT", {"min": 0, "default": 10, "max": 59}),
"milliseconds": ("INT", {"min": 0, "default": 0, "max": 59}),
"frames_per_second": ("INT", {"min": 1, "default": 30})
},
}
CATEGORY = NodeCategories.ANIMATION
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("TOTAL",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *v):
return ALWAYS_CHANGED_FLAG
def result(self, hours, minutes, seconds, milliseconds, frames_per_second):
total_s = seconds + 0.001 * milliseconds + minutes * 60 + hours * 3600
return (round(total_s * frames_per_second),)
+98
View File
@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
import math
from evalidate import Expr, EvalException, base_eval_model
from .categories import *
from .err import on_error
from .shared import hashed_as_strings
class DreamCalculation:
NODE_NAME = "Calculation"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"expression": ("STRING", {"default": "a + b + c - (r * s * t)", "multiline": True})
},
"optional": {
"a_int": ("INT", {"default": 0, "multiline": False}),
"b_int": ("INT", {"default": 0, "multiline": False}),
"c_int": ("INT", {"default": 0, "multiline": False}),
"r_float": ("FLOAT", {"default": 0.0, "multiline": False}),
"s_float": ("FLOAT", {"default": 0.0, "multiline": False}),
"t_float": ("FLOAT", {"default": 0.0, "multiline": False})
}
}
CATEGORY = NodeCategories.ANIMATION_CURVES
RETURN_TYPES = ("FLOAT", "INT")
RETURN_NAMES = ("FLOAT", "INT")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def _make_model(self):
funcs = self._make_functions()
m = base_eval_model.clone()
m.nodes.append('Mult')
m.nodes.append('Call')
for funname in funcs.keys():
m.allowed_functions.append(funname)
return (m, funcs)
def _make_functions(self):
return {
"round": round,
"float": float,
"int": int,
"abs": abs,
"min": min,
"max": max,
"tan": math.tan,
"tanh": math.tanh,
"sin": math.sin,
"sinh": math.sinh,
"cos": math.cos,
"cosh": math.cosh,
"pow": math.pow,
"sqrt": math.sqrt,
"ceil": math.ceil,
"floor": math.floor,
"pi": math.pi,
"log": math.log,
"log2": math.log2,
"acos": math.acos,
"asin": math.asin,
"acosh": math.acosh,
"asinh": math.asinh,
"atan": math.atan,
"atanh": math.atanh,
"exp": math.exp,
"fmod": math.fmod,
"factorial": math.factorial,
"dist": math.dist,
"atan2": math.atan2,
"log10": math.log10
}
def result(self, expression, **values):
model, funcs = self._make_model()
vars = funcs
for key in ("a_int", "b_int", "c_int", "r_float", "s_float", "t_float"):
nm = key.split("_")[0]
v = values.get(key, None)
if v is not None:
vars[nm] = v
try:
data = Expr(expression, model=model).eval(vars)
if isinstance(data, (int, float)):
return float(data), int(round(data))
else:
return 0.0, 0
except EvalException as e:
on_error(DreamCalculation, str(e))
+4
View File
@@ -1,11 +1,15 @@
# -*- coding: utf-8 -*-
class NodeCategories:
ANIMATION = "animation"
ANIMATION_POSTPROCESSING = ANIMATION + "/postprocessing"
ANIMATION_TRANSFORMS = ANIMATION + "/transforms"
ANIMATION_CURVES = "animation/curves"
CONDITIONING = "conditioning"
IMAGE_POSTPROCESSING = "image/postprocessing"
IMAGE_ANIMATION = "image/animation"
IMAGE_COLORS = "image/color"
IMAGE_GENERATE = "image/generate"
IMAGE = "image"
UTILS = "utils"
UTILS_SWITCHES = "utils/switches"
+84 -3
View File
@@ -1,6 +1,86 @@
# -*- coding: utf-8 -*-
from .categories import NodeCategories
from .shared import *
from .types import *
from .categories import NodeCategories
class DreamImageAreaSampler:
NODE_NAME = "Sample Image Area as Palette"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"samples": ("INT", {"default": 256, "min": 1, "max": 1024 * 4}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"area": (["top-left", "top-center", "top-right",
"center-left", "center", "center-right",
"bottom-left", "bottom-center", "bottom-right"],)
},
}
CATEGORY = NodeCategories.IMAGE_COLORS
RETURN_TYPES = (RGBPalette.ID,)
RETURN_NAMES = ("palette",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def _get_pixel_area(self, img: DreamImage, area):
w = img.width
h = img.height
wpart = round(w / 3)
hpart = round(h / 3)
x0 = 0
x1 = wpart - 1
x2 = wpart
x3 = wpart + wpart - 1
x4 = wpart + wpart
x5 = w - 1
y0 = 0
y1 = hpart - 1
y2 = hpart
y3 = hpart + hpart - 1
y4 = hpart + hpart
y5 = h - 1
if area == "center":
return (x2, y2, x3, y3)
elif area == "top-center":
return (x2, y0, x3, y1)
elif area == "bottom-center":
return (x2, y4, x3, y5)
elif area == "center-left":
return (x0, y2, x1, y3)
elif area == "top-left":
return (x0, y0, x1, y1)
elif area == "bottom-left":
return (x0, y4, x1, y5)
elif area == "center-right":
return (x4, y2, x5, y3)
elif area == "top-right":
return (x4, y0, x5, y1)
elif area == "bottom-right":
return (x4, y4, x5, y5)
def result(self, image, samples, seed, area):
result = list()
r = random.Random()
r.seed(seed)
for data in image:
di = DreamImage(tensor_image=data)
area = self._get_pixel_area(di, area)
pixels = list()
for i in range(samples):
x = r.randint(area[0], area[2])
y = r.randint(area[1], area[3])
pixels.append(di.get_pixel(x, y))
result.append(RGBPalette(colors=pixels))
return (tuple(result),)
class DreamImageSampler:
@@ -11,7 +91,7 @@ class DreamImageSampler:
return {
"required": {
"image": ("IMAGE",),
"samples": ("INT", {"default": 1024, "min": 1, "max": 1024 * 64}),
"samples": ("INT", {"default": 1024, "min": 1, "max": 1024 * 4}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff})
},
}
@@ -48,7 +128,7 @@ class DreamColorAlign:
def INPUT_TYPES(cls):
return {
"required": SharedTypes.palette | {
"target_align": (RGBPalette.ID, {"forceInput": True}),
"target_align": (RGBPalette.ID, ),
"alignment_factor": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 10.0, "step": 0.1}),
}
}
@@ -136,6 +216,7 @@ class DreamColorShift:
class DreamAnalyzePalette:
NODE_NAME = "Analyze Palette"
NODE = "📊"
@classmethod
def INPUT_TYPES(cls):
+166 -6
View File
@@ -1,8 +1,24 @@
import math, csv
# -*- coding: utf-8 -*-
import csv
import math
from .types import SharedTypes, FrameCounter
from .shared import hashed_as_strings
from .categories import NodeCategories
from .shared import hashed_as_strings
from .types import SharedTypes, FrameCounter
def _linear_value_calc(x, x_start, x_end, y_start, y_end):
if x <= x_start:
return y_start
if x >= x_end:
return y_end
dx = max(x_end - x_start, 0.0001)
n = (x - x_start) / dx
return (y_end - y_start) * n + y_start
def _curve_result(f: float):
return (f, int(round(f)))
class DreamSineWave:
@@ -35,7 +51,149 @@ class DreamSineWave:
b = 2 * math.pi / periodicity_seconds
d = (max_value + min_value) / 2
y = a * math.sin(b * (x + c)) + d
return (y, int(round(y)))
return _curve_result(y)
class DreamSawWave:
NODE_NAME = "Saw Curve"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.frame_counter | {
"max_value": ("FLOAT", {"default": 1.0, "multiline": False}),
"min_value": ("FLOAT", {"default": 0.0, "multiline": False}),
"periodicity_seconds": ("FLOAT", {"default": 10.0, "multiline": False, "min": 0.01}),
"phase": ("FLOAT", {"default": 0.0, "multiline": False, "min": -1, "max": 1}),
},
}
CATEGORY = NodeCategories.ANIMATION_CURVES
RETURN_TYPES = ("FLOAT", "INT")
RETURN_NAMES = ("FLOAT", "INT")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def result(self, frame_counter: FrameCounter, max_value, min_value, periodicity_seconds, phase):
x = frame_counter.current_time_in_seconds
x = ((x + periodicity_seconds * phase) % periodicity_seconds) / periodicity_seconds
y = x * (max_value - min_value) + min_value
return _curve_result(y)
class DreamTriangleWave:
NODE_NAME = "Triangle Curve"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.frame_counter | {
"max_value": ("FLOAT", {"default": 1.0, "multiline": False}),
"min_value": ("FLOAT", {"default": 0.0, "multiline": False}),
"periodicity_seconds": ("FLOAT", {"default": 10.0, "multiline": False, "min": 0.01}),
"phase": ("FLOAT", {"default": 0.0, "multiline": False, "min": -1, "max": 1}),
},
}
CATEGORY = NodeCategories.ANIMATION_CURVES
RETURN_TYPES = ("FLOAT", "INT")
RETURN_NAMES = ("FLOAT", "INT")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def result(self, frame_counter: FrameCounter, max_value, min_value, periodicity_seconds, phase):
x = frame_counter.current_time_in_seconds
x = ((x + periodicity_seconds * phase) % periodicity_seconds) / periodicity_seconds
if x <= 0.5:
x *= 2
y = x * (max_value - min_value) + min_value
else:
x = (x - 0.5) * 2
y = max_value - x * (max_value - min_value)
return _curve_result(y)
class DreamTriangleEvent:
NODE_NAME = "Triangle Event Curve"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.frame_counter | {
"max_value": ("FLOAT", {"default": 1.0, "multiline": False}),
"min_value": ("FLOAT", {"default": 0.0, "multiline": False}),
"width_seconds": ("FLOAT", {"default": 1.0, "multiline": False, "min": 0.1}),
"center_seconds": ("FLOAT", {"default": 10.0, "multiline": False, "min": 0.0}),
},
}
CATEGORY = NodeCategories.ANIMATION_CURVES
RETURN_TYPES = ("FLOAT", "INT")
RETURN_NAMES = ("FLOAT", "INT")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def result(self, frame_counter: FrameCounter, max_value, min_value, width_seconds, center_seconds):
x = frame_counter.current_time_in_seconds
start = center_seconds - width_seconds * 0.5
end = center_seconds + width_seconds * 0.5
if start <= x <= center_seconds:
y = _linear_value_calc(x, start, center_seconds, min_value, max_value)
elif center_seconds < x <= end:
y = _linear_value_calc(x, center_seconds, end, max_value, min_value)
else:
y = min_value
return _curve_result(y)
class DreamSmoothEvent:
NODE_NAME = "Smooth Event Curve"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.frame_counter | {
"max_value": ("FLOAT", {"default": 1.0, "multiline": False}),
"min_value": ("FLOAT", {"default": 0.0, "multiline": False}),
"width_seconds": ("FLOAT", {"default": 1.0, "multiline": False, "min": 0.1}),
"center_seconds": ("FLOAT", {"default": 10.0, "multiline": False, "min": 0.0}),
},
}
CATEGORY = NodeCategories.ANIMATION_CURVES
RETURN_TYPES = ("FLOAT", "INT")
RETURN_NAMES = ("FLOAT", "INT")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def result(self, frame_counter: FrameCounter, max_value, min_value, width_seconds, center_seconds):
x = frame_counter.current_time_in_seconds
start = center_seconds - width_seconds * 0.5
end = center_seconds + width_seconds * 0.5
if start <= x <= center_seconds:
y = _linear_value_calc(x, start, center_seconds, 0.0, 1.0)
elif center_seconds < x <= end:
y = _linear_value_calc(x, center_seconds, end, 1.0, 0.0)
else:
y = 0.0
if y < 0.5:
y = ((y + y) * (y + y)) * 0.5
else:
a = (y - 0.5) * 2
y = math.pow(a, 0.25) * 0.5 + 0.5
return _curve_result(y * (max_value - min_value) + min_value)
class DreamBeatCurve:
@@ -82,7 +240,8 @@ class DreamBeatCurve:
return 1.0 - ((frame - accent_start) / frames_per_beat)
return 0
def result(self, bpm, frame_counter: FrameCounter, measure_length, low_value, high_value, power, invert, time_offset, **accents):
def result(self, bpm, frame_counter: FrameCounter, measure_length, low_value, high_value, power, invert,
time_offset, **accents):
frame_offset = int(round(time_offset * frame_counter.frames_per_second))
accents_set = set(filter(lambda v: v >= 1 and v <= measure_length,
map(lambda i: accents.get("accent_" + str(i), -1), range(30))))
@@ -93,7 +252,7 @@ class DreamBeatCurve:
v = 1.0 - v
r = low_value + v * (high_value - low_value)
return (r, int(round(r)))
return _curve_result(r)
class DreamLinear:
@@ -133,6 +292,7 @@ def _is_as_float(s: str):
class DreamCSVGenerator:
NODE_NAME = "CSV Generator"
ICON = "⌗"
@classmethod
def INPUT_TYPES(cls):
+3 -2
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
def run_disable():
pass
if __name__ == "__main__":
run_disable()
run_disable()
+18
View File
@@ -0,0 +1,18 @@
class DreamLog:
def __init__(self, debug_active=False):
self._debug = debug_active
def _print(self, text: str, *args, **kwargs):
if args or kwargs:
text = text.format(*args, **kwargs)
print("[DREAM] " + text)
def error(self, text: str, *args, **kwargs):
self._print(text, *args, **kwargs)
def info(self, text: str, *args, **kwargs):
self._print(text, *args, **kwargs)
def debug(self, text: str, *args, **kwargs):
if self._debug:
self._print(text, *args, **kwargs)
+41
View File
@@ -0,0 +1,41 @@
EMBEDDED_CONFIGURATION = {
"ffmpeg": {
"file_extension": "mp4",
"path": "ffmpeg",
"arguments": ["-r", "%FPS%", "-f", "concat", "-safe", "0", "-vsync",
"cfr", "-i", "%FRAMES%", "-c:v", "libx265", "-pix_fmt",
"yuv420p", "%OUTPUT%"]
},
"mpeg_coder": {
"encoding_threads": 4,
"bitrate_factor": 1.0,
"max_b_frame": 2,
"file_extension": "mp4",
"codec_name": "libx265"
},
"encoding": {
"jpeg_quality": 95
},
"debug": False,
"ui": {
"top_category": "Dream",
"prepend_icon_to_category": True,
"append_icon_to_category": False,
"prepend_icon_to_node": True,
"append_icon_to_node": False,
"category_icons": {
"animation": "🎥",
"postprocessing": "⚙",
"transforms": "🔀",
"curves": "📈",
"color": "🎨",
"generate": "⚡",
"utils": "🛠",
"image": "🌄",
"switches": "⭆",
"conditioning": "☯",
"Dream": "✨"
}
},
}
+2
View File
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
def run_enable():
pass
if __name__ == "__main__":
run_enable()
+9
View File
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
def _get_node_name(cls):
return cls.__dict__.get("NODE_NAME", str(cls))
def on_error(node_cls: type, message: str):
msg = "Failure in [" + _get_node_name(node_cls) + "]:" + message
print(msg)
raise Exception(msg)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

+7 -4
View File
@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
import math
import numpy
import torch
from PIL.Image import Resampling
from PIL import Image, ImageDraw
from .categories import *
from .types import SharedTypes, FrameCounter
from PIL.Image import Resampling
from .categories import *
from .shared import ALWAYS_CHANGED_FLAG, convertTensorImageToPIL, DreamImageProcessor, \
DreamImage, DreamMask
from .types import SharedTypes, FrameCounter
class DreamImageMotion:
@@ -85,7 +87,8 @@ class DreamImageMotion:
def _limit_range(f):
return max(-1.0, min(1.0, f))
def _motion(image: DreamImage, batch_counter, zoom, x_translation, y_translation, mask_1_overlap, mask_2_overlap,
def _motion(image: DreamImage, batch_counter, zoom, x_translation, y_translation, mask_1_overlap,
mask_2_overlap,
mask_3_overlap):
zoom = _limit_range(zoom / frame_counter.frames_per_second)
x_translation = _limit_range(x_translation / frame_counter.frames_per_second)
+100
View File
@@ -0,0 +1,100 @@
from .categories import *
from .shared import *
class DreamInputText:
NODE_NAME = "Text Input"
ICON = "✍"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("STRING", {"default": "", "multiline": True}),
},
}
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("STRING",)
FUNCTION = "noop"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def noop(self, value):
return (value,)
class DreamInputString:
NODE_NAME = "String Input"
ICON = "✍"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("STRING", {"default": "", "multiline": False}),
},
}
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("STRING",)
FUNCTION = "noop"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def noop(self, value):
return (value,)
class DreamInputFloat:
NODE_NAME = "Float Input"
ICON = "✍"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("FLOAT", {"default": 0.0}),
},
}
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("FLOAT",)
FUNCTION = "noop"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def noop(self, value):
return (value,)
class DreamInputInt:
NODE_NAME = "Int Input"
ICON = "✍"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("INT", {"default": 0}),
},
}
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("INT",)
FUNCTION = "noop"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def noop(self, value):
return (value,)
+1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from .shared import DreamConfig
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Morgan Johansson/Dream Project
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+4 -4
View File
@@ -1,12 +1,12 @@
from PIL import Image
from .types import SharedTypes, FrameCounter
from .shared import ALWAYS_CHANGED_FLAG, list_images_in_directory, convertFromPILToTensorImage, DreamImage
# -*- coding: utf-8 -*-
from .categories import NodeCategories
import os
from .shared import ALWAYS_CHANGED_FLAG, list_images_in_directory, DreamImage
from .types import SharedTypes, FrameCounter
class DreamImageSequenceInputWithDefaultFallback:
NODE_NAME = "Image Sequence Loader"
ICON = "💾"
@classmethod
def INPUT_TYPES(cls):
+26 -2
View File
@@ -1,23 +1,47 @@
{
"Analyze Palette [Dream]": "Output brightness, red, green and blue averages of a palette",
"Beat Curve [Dream]": "Beat pattern curve with impulses at specified beats of a measure",
"Big Float Switch [Dream]": "Switch for up to 10 inputs",
"Big Image Switch [Dream]": "Switch for up to 10 inputs",
"Big Int Switch [Dream]": "Switch for up to 10 inputs",
"Big Latent Switch [Dream]": "Switch for up to 10 inputs",
"Big Palette Switch [Dream]": "Switch for up to 10 inputs",
"Big Text Switch [Dream]": "Switch for up to 10 inputs",
"Boolean To Float [Dream]": "Converts a boolean value to two different float values",
"Boolean To Int [Dream]": "Converts a boolean value to two different int values",
"Build Prompt [Dream]": "Weighted text prompt builder utility",
"CSV Curve [Dream]": "CSV input curve where first column is frame or second and second column is value",
"CSV Generator [Dream]": "CSV output, mainly for debugging purposes",
"Calculation [Dream]": "Mathematical calculation node",
"Common Frame Dimensions [Dream]": "Utility for calculating good width/height based on common video dimensions",
"FFMPEG Video Encoder [Dream]": "Post processing for animation sequences calling FFMPEG to generate video file",
"File Count [Dream]": "Finds the number of files in a directory matching specified patterns",
"Finalize Prompt [Dream]": "Used in conjunction with 'Build Prompt'",
"Float Input [Dream]": "Float input (until primitive routing issues are solved)",
"Frame Count Calculator [Dream]": "Simple utility to calculate number of frames based on duration and framerate",
"Frame Counter (Directory) [Dream]": "Directory backed frame counter, for output directories",
"Frame Counter (Simple) [Dream]": "Integer value used as frame counter",
"Frame Counter Info [Dream]": "Extracts information from the frame counter",
"Frame Counter Offset [Dream]": "Adds an offset to a frame counter",
"Image Motion [Dream]": "Node supporting zooming in/out and translating an image",
"Image Sequence Blend [Dream]": "Post processing for animation sequences blending frame for a smoother blurred effect",
"Image Sequence Loader [Dream]": "Loads a frame from a directory of images",
"Image Sequence Saver [Dream]": "Saves a frame to a directory",
"Image Sequence Tweening [Dream]": "Post processing for animation sequences generating blended in-between frames",
"Int Input [Dream]": "Integer input (until primitive routing issues are solved)",
"Linear Curve [Dream]": "Linear interpolation between two value over the full animation",
"Noise from Area Palettes [Dream]": "Generates noise based on the colors of up to nine different palettes",
"Noise from Palette [Dream]": "Generates noise based on the colors in a palette",
"Palette Color Align [Dream]": "Shifts the colors of one palette towards another target palette",
"Palette Color Shift [Dream]": "Multiplies the color values in a palette",
"Sample Image Area as Palette [Dream]": "Samples a palette from an image based on pre-defined areas",
"Sample Image as Palette [Dream]": "Randomly samples pixel values to build a palette from an image",
"Sine Curve [Dream]": "Simple sine wave curve"
}
"Saw Curve [Dream]": "Saw wave curve",
"Sine Curve [Dream]": "Simple sine wave curve",
"Smooth Event Curve [Dream]": "Single event/peak curve with a slight bell-shape",
"String Input [Dream]": "String input (until primitive routing issues are solved)",
"Text Input [Dream]": "Multiline string input (until primitive routing issues are solved)",
"Triangle Curve [Dream]": "Triangle wave curve",
"Triangle Event Curve [Dream]": "Single event/peak curve with triangular shape",
"Video Encoder (mpegCoder) [Dream]": "Post processing for animation sequences using mpegCoder module to generate video file"
} and switches.
+128 -19
View File
@@ -1,10 +1,31 @@
# -*- coding: utf-8 -*-
import math
from .categories import NodeCategories
from .shared import *
from .types import *
from .categories import NodeCategories
def _generate_noise(image: DreamImage, color_function, rng: random.Random, block_size, blur_amount,
density) -> DreamImage:
w = block_size[0]
h = block_size[1]
blur_radius = round(max(image.width, image.height) * blur_amount * 0.25)
if w <= (image.width // 128) or h <= (image.height // 128):
return image
max_placements = round(density * (image.width * image.height))
num = min(max_placements, round((image.width * image.height * 2) / (w * h)))
for i in range(num):
x = rng.randint(-w + 1, image.width - 1)
y = rng.randint(-h + 1, image.height - 1)
image.color_area(x, y, w, h, color_function(x + (w >> 1), y + (h >> 1)))
image = image.blur(blur_radius)
return _generate_noise(image, color_function, rng, (w >> 1, h >> 1), blur_amount, density)
class DreamNoiseFromPalette:
NODE_NAME = "Noise from Palette"
ICON = "🌫"
@classmethod
def INPUT_TYPES(cls):
@@ -12,8 +33,8 @@ class DreamNoiseFromPalette:
"required": SharedTypes.palette | {
"width": ("INT", {"default": 512, "min": 1, "max": 8192}),
"height": ("INT", {"default": 512, "min": 1, "max": 8192}),
"blur_amount": ("FLOAT", {"default": 0.1, "min": 0, "max": 1.0, "step": 0.05}),
"iterations": ("INT", {"default": 4, "min": 1, "max": 64}),
"blur_amount": ("FLOAT", {"default": 0.3, "min": 0, "max": 1.0, "step": 0.05}),
"density": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 1.0, "step": 0.025}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff})
},
}
@@ -27,28 +48,116 @@ class DreamNoiseFromPalette:
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def generate_noise(self, image: DreamImage, color_function, rng: random.Random, i: int, blur_amount) -> DreamImage:
w = image.width >> i
h = image.height >> i
blur_radius = round(max(image.width, image.height) * blur_amount * 0.25)
if w <= 1 or h <= 1:
return image
for i in range(1 << (i*2)):
x = rng.randint(-w+1, image.width - 1)
y = rng.randint(-h+1, image.height - 1)
image.color_area(x, y, w, h, color_function())
image = image.blur(blur_radius)
return self.generate_noise(image, color_function, rng, i + 1, blur_amount)
def result(self, palette: Tuple[RGBPalette], width, height, seed, blur_amount, iterations):
def result(self, palette: Tuple[RGBPalette], width, height, seed, blur_amount, density):
outputs = list()
rng = random.Random()
for p in palette:
seed += 1
color_iterator = p.random_iteration(seed)
image = DreamImage(pil_image=Image.new("RGB", (width, height), color=next(color_iterator)))
for n in range(iterations):
image = self.generate_noise(image, lambda: next(color_iterator), rng, 1, blur_amount)
image = _generate_noise(image, lambda x, y: next(color_iterator), rng,
(image.width >> 1, image.height >> 1), blur_amount, density)
outputs.append(image)
return (DreamImage.join_to_tensor_data(outputs),)
class DreamNoiseFromAreaPalettes:
NODE_NAME = "Noise from Area Palettes"
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"top_left_palette": (RGBPalette.ID,),
"top_center_palette": (RGBPalette.ID,),
"top_right_palette": (RGBPalette.ID,),
"center_left_palette": (RGBPalette.ID,),
"center_palette": (RGBPalette.ID,),
"center_right_palette": (RGBPalette.ID,),
"bottom_left_palette": (RGBPalette.ID,),
"bottom_center_palette": (RGBPalette.ID,),
"bottom_right_palette": (RGBPalette.ID,),
},
"required": {
"area_sharpness": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05}),
"width": ("INT", {"default": 512, "min": 1, "max": 8192}),
"height": ("INT", {"default": 512, "min": 1, "max": 8192}),
"blur_amount": ("FLOAT", {"default": 0.3, "min": 0, "max": 1.0, "step": 0.05}),
"density": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 1.0, "step": 0.025}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
},
}
CATEGORY = NodeCategories.IMAGE_GENERATE
ICON = "🌫"
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def _area_coordinates(self, width, height):
dx = width / 6
dy = height / 6
return {
"top_left_palette": (dx, dy),
"top_center_palette": (dx * 3, dy),
"top_right_palette": (dx * 5, dy),
"center_left_palette": (dx, dy * 3),
"center_palette": (dx * 3, dy * 3),
"center_right_palette": (dx * 5, dy * 3),
"bottom_left_palette": (dx * 1, dy * 5),
"bottom_center_palette": (dx * 3, dy * 5),
"bottom_right_palette": (dx * 5, dy * 5),
}
def _pick_random_area(self, active_coordinates, x, y, rng, area_sharpness):
def _dst(x1, y1, x2, y2):
a = x1 - x2
b = y1 - y2
return math.sqrt(a * a + b * b)
distances = list(map(lambda item: (item[0], _dst(item[1][0], item[1][1], x, y)), active_coordinates))
areas_by_weight = list(
map(lambda item: (math.pow((1.0 / max(1, item[1])), 0.5 + 4.5 * area_sharpness), item[0]), distances))
return pick_random_by_weight(areas_by_weight, rng)
def _setup_initial_colors(self, image: DreamImage, color_func):
w = image.width
h = image.height
wpart = round(w / 3)
hpart = round(h / 3)
for i in range(3):
for j in range(3):
image.color_area(wpart * i, hpart * j, w, h,
color_func(wpart * i + w // 2, hpart * j + h // 2))
def result(self, width, height, seed, blur_amount, density, area_sharpness, **palettes):
outputs = list()
rng = random.Random()
coordinates = self._area_coordinates(width, height)
active_palettes = list(filter(lambda pair: pair[1] is not None and len(pair[1]) > 0, palettes.items()))
active_coordinates = list(map(lambda item: (item[0], coordinates[item[0]]), active_palettes))
n = max(list(map(len, palettes.values())) + [0])
for b in range(n):
batch_palettes = dict(map(lambda item: (item[0], item[1][b].random_iteration(seed)), active_palettes))
def _color_func(x, y):
name = self._pick_random_area(active_coordinates, x, y, rng, area_sharpness)
rgb = batch_palettes[name]
return next(rgb)
image = DreamImage(pil_image=Image.new("RGB", (width, height)))
self._setup_initial_colors(image, _color_func)
image = _generate_noise(image, _color_func, rng, (round(image.width / 3), round(image.height / 3)),
blur_amount, density)
outputs.append(image)
if not outputs:
outputs.append(DreamImage(pil_image=Image.new("RGB", (width, height))))
return (DreamImage.join_to_tensor_data(outputs),)
+11 -6
View File
@@ -1,14 +1,18 @@
# -*- coding: utf-8 -*-
import json
from PIL.PngImagePlugin import PngInfo
from .categories import NodeCategories
import folder_paths as comfy_paths
from .types import SharedTypes, FrameCounter, AnimationSequence
from .shared import hashed_as_strings, DreamImageProcessor, DreamImage, \
list_images_in_directory, DreamConfig
import os
import folder_paths as comfy_paths
from PIL.PngImagePlugin import PngInfo
from .categories import NodeCategories
from .shared import hashed_as_strings, DreamImageProcessor, DreamImage, \
list_images_in_directory, DreamConfig
from .types import SharedTypes, FrameCounter, AnimationSequence
CONFIG = DreamConfig()
def _save_png(pil_image, filepath, embed_info, prompt, extra_pnginfo):
info = PngInfo()
if extra_pnginfo is not None:
@@ -28,6 +32,7 @@ def _save_jpg(pil_image, filepath, quality):
class DreamImageSequenceOutput:
NODE_NAME = "Image Sequence Saver"
ICON = "💾"
@classmethod
def INPUT_TYPES(cls):
+69
View File
@@ -0,0 +1,69 @@
from .categories import NodeCategories
from .shared import hashed_as_strings
from .types import PartialPrompt
class DreamWeightedPromptBuilder:
NODE_NAME = "Build Prompt"
ICON = "⚖"
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"partial_prompt": (PartialPrompt.ID,)
},
"required": {
"added_prompt": ("STRING", {"default": "", "multiline": True}),
"weight": ("FLOAT", {"default": 1.0}),
},
}
CATEGORY = NodeCategories.CONDITIONING
RETURN_TYPES = (PartialPrompt.ID,)
RETURN_NAMES = ("partial_prompt",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def result(self, added_prompt, weight, **args):
input = args.get("partial_prompt", PartialPrompt())
p = input.add(added_prompt, weight)
return (p,)
class DreamPromptFinalizer:
NODE_NAME = "Finalize Prompt"
ICON = "🗫"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"partial_prompt": (PartialPrompt.ID,),
"adjustment": (["raw", "by_abs_max", "by_abs_sum"],),
"clamp": ("FLOAT", {"default": 2.0, "min": 0.1, "step": 0.1}),
"adjustment_reference": ("FLOAT", {"default": 1.0, "min": 0.1}),
},
}
CATEGORY = NodeCategories.CONDITIONING
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("positive", "negative")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def result(self, partial_prompt: PartialPrompt, adjustment, adjustment_reference, clamp):
if adjustment == "raw" or partial_prompt.is_empty():
return partial_prompt.finalize(clamp)
elif adjustment == "by_abs_sum":
f = adjustment_reference / partial_prompt.abs_sum()
return partial_prompt.scaled_by(f).finalize(clamp)
else:
f = adjustment_reference / partial_prompt.abs_max()
return partial_prompt.scaled_by(f).finalize(clamp)
+222 -72
View File
@@ -9,6 +9,12 @@ I have demonstrated the use of these custom nodes in this [youtube video](https:
## Installation
### Simple option
You can install Dream Project Animation Nodes using the ComfyUI Manager.
### Manual option
Run within (ComfyUI)/custom_nodes/ folder:
* git clone https://github.com/alt-key-project/comfyui-dream-project.git
@@ -26,6 +32,76 @@ Finally:
After startup, a configuration file 'config.json' should have been created in the 'comfyui-dream-project' directory.
Specifically check that the path of ffmpeg works in your system (add full path to the command if needed).
## Upgrade
When upgrading, it is good to re-run the pip install command as specified in the install section. This will install any
new dependencies.
## Configuration
### debug
Setting this to true will enable some trace-level logging.
### ffmpeg.file_extension
Sets the output file extension and with that the envelope used.
### ffmpeg.path
Path to the ffmpeg executable or just the command if ffmpeg is in PATH.
### ffmpeg.arguments
The arguments sent to FFMPEG. A few of the values are provided by the node:
* %FPS% the target framerate
* %FRAMES% a frame ionput file
* %OUTPUT% output video file path
### encoding.jpeg__quality
Sets the encoding quality of jpeg images.
### ui.top_category
Sets the name of the top level category on the menu. Set to empty string "" to remove the top level. If the top level
is removed you may also want to disable the category icons to get nodes into existing category folders.
### prepend_icon_to_category / append_icon_to_category
Flags to add a icon before and/or after the category name at each level.
### prepend_icon_icon_to_node / append_icon_icon_to_node
Flags to add an icon before and/or after the node name.
### ui.category_icons
Each key defines a unicode symbol as an icon used for the specified category.
### mpeg_coder.bitrate_factor
This factor allows changing the bitrate to better fit the required quality and codec. A value of 1 is typically
suitable for H.265.
### mpeg_coder.codec_name
Codec names as specified by ffmpeg. Some common options include "libx264", "libx264" and "mpeg2video".
### mpeg_coder.encoding_threads
Increasing the number of encoding threads in mpegCoder will generally reduce the overall encoding time, but it will also
increase the load on the computer.
### mpeg_coder.file_extension
Sets the output file extension and with that the envelope used.
### mpeg_coder.max_b_frame
Sets the max-b-frames parameter for as specified in ffmpeg.
## Concepts used
These are some concepts used in nodes:
@@ -51,83 +127,157 @@ video file using ffmpeg. These nodes should be seen as a convenience and they ar
nodes in parallel - they will not work as intended!
## The nodes
### Analyze Palette [Dream]
Output brightness, red, green and blue averages of a palette. Useful to control other processing.
### Analyze Palette [Dream]
Output brightness, red, green and blue averages of a palette. Useful to control other processing.
### Beat Curve [Dream]
Beat pattern curve with impulses at specified beats of a measure.
### CSV Curve [Dream]
CSV input curve where first column is frame or second and second column is value.
### CSV Generator [Dream]
CSV output, mainly for debugging purposes. First column is frame number and second is value.
Recreates file at frame 0 (removing and existing content in the file).
### Common Frame Dimensions [Dream]
Utility for calculating good width/height based on common video dimensions.
### FFMPEG Video Encoder [Dream]
Post processing for animation sequences calling FFMPEG to generate video file.
### File Count [Dream]
Finds the number of files in a directory matching specified patterns.
### Frame Counter (Directory) [Dream]
Directory backed frame counter, for output directories.
### Frame Counter (Simple) [Dream]
Integer value used as frame counter. Useful for testing or if an auto-incrementing primitive is used as a frame
counter.
### Frame Counter Offset [Dream]
Adds an offset to a frame counter.
### Image Motion [Dream]
Node supporting zooming in/out and translating an image.
### Image Sequence Blend [Dream]
Post processing for animation sequences blending frame for a smoother blurred effect.
### Image Sequence Loader [Dream]
Loads a frame from a directory of images.
### Image Sequence Saver [Dream]
Saves a frame to a directory.
### Image Sequence Tweening [Dream]
Post processing for animation sequences generating blended in-between frames.
### Linear Curve [Dream]
Linear interpolation between two values over the full animation.
### Noise from Palette [Dream]
Generates noise based on the colors in a palette.
### Palette Color Align [Dream]
Shifts the colors of one palette towards another target palette. If the alignment factor
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
alignment to the target. Above one we will overshoot the alignment.
### Palette Color Shift [Dream]
Multiplies the color values in a palette to shift the color balance or brightness.
### Sample Image as Palette [Dream]
Randomly samples pixels from a source image to build a palette from it.
### Sine Curve [Dream]
Simple sine wave curve.
### Other custom nodes
Many of the nodes found in 'WAS Node Suite' are useful the Dream Project Animation nodes - I suggest you install those
custom nodes as well!
### Beat Curve [Dream]
Beat pattern curve with impulses at specified beats of a measure.
## Examples
### Big *** Switch [Dream]
Switch nodes for different type for up to ten inputs.
### Image Motion with Curves
### Boolean To Float/Int [Dream]
Converts a boolean value to two different numeric values.
### Build Prompt [Dream] (and Finalize Prompt [Dream])
Weighted text prompt builder utility. Chain any number of these nodes and terminate with 'Finalize Prompt'.
### Calculation [Dream]
Mathematical calculation node. Exposes most of the mathematical functions in the python
[math module](https://docs.python.org/3/library/math.html), mathematical operators as well as round, abs, int,
float, max and min.
### CSV Curve [Dream]
CSV input curve where first column is frame or second and second column is value.
### CSV Generator [Dream]
CSV output, mainly for debugging purposes. First column is frame number and second is value.
Recreates file at frame 0 (removing and existing content in the file).
### Common Frame Dimensions [Dream]
Utility for calculating good width/height based on common video dimensions.
### Video Encoder (FFMPEG) [Dream]
Post processing for animation sequences calling FFMPEG to generate video files.
### Video Encoder (mpegCoder) [Dream]
Post processing for animation sequences using the python module mpegCoder with ffmpeg library to generate video files.
### File Count [Dream]
Finds the number of files in a directory matching specified patterns.
### Frame Count Calculator [Dream]
Simple utility to calculate number of frames based on time and framerate.
### Frame Counter (Directory) [Dream]
Directory backed frame counter, for output directories.
### Frame Counter (Simple) [Dream]
Integer value used as frame counter. Useful for testing or if an auto-incrementing primitive is used as a frame
counter.
### Frame Counter Info [Dream]
Extracts information from the frame counter.
### Frame Counter Offset [Dream]
Adds an offset to a frame counter.
### Image Motion [Dream]
Node supporting zooming in/out and translating an image.
### Image Sequence Blend [Dream]
Post processing for animation sequences blending frame for a smoother blurred effect.
### Image Sequence Loader [Dream]
Loads a frame from a directory of images.
### Image Sequence Saver [Dream]
Saves a frame to a directory.
### Image Sequence Tweening [Dream]
Post processing for animation sequences generating blended in-between frames.
### Linear Curve [Dream]
Linear interpolation between two values over the full animation.
### Noise from Area Palettes [Dream]
Generates noise based on the colors of up to nine different palettes, each connected to position/area of the
image. Although the palettes are optional, at least one palette should be provided.
### Noise from Palette [Dream]
Generates noise based on the colors in a palette.
### Palette Color Align [Dream]
Shifts the colors of one palette towards another target palette. If the alignment factor
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
alignment to the target. Above one we will overshoot the alignment.
### Palette Color Shift [Dream]
Multiplies the color values in a palette to shift the color balance or brightness.
### Sample Image Area as Palette [Dream]
Randomly samples a palette from an image based on pre-defined areas. The image is separated into nine rectangular areas
of equal size and each node may sample one of these.
### Sample Image as Palette [Dream]
Randomly samples pixels from a source image to build a palette from it.
### Saw Curve [Dream]
Saw wave curve.
### Sine Curve [Dream]
Simple sine wave curve.
### Smooth Event Curve [Dream]
Single event/peak curve with a slight bell-shape.
### Triangle Curve [Dream]
Triangle wave curve.
### Triangle Event Curve [Dream]
Single event/peak curve with triangular shape.
### Other custom nodes
Many of the nodes found in 'WAS Node Suite' are useful the Dream Project Animation nodes - I suggest you install those
custom nodes as well!
## Examples
### Image Motion with Curves
This example should be a starting point for anyone wanting to build with the Dream Project Animation nodes.
[motion-workflow-example](examples/motion-workflow-example.json)
### Image Motion with Color Coherence
Same as above but with added color coherence through palettes.
[motion-workflow-with-color-coherence](examples/motion-workflow-with-color-coherence.json)
### Area Sampled Noise
This flow demonstrates sampling image areas into palettes and generating noise for these areas.
[area-sampled-noise](examples/area-sampled-noise.json)
## Known issues
### FFMPEG
The call to FFMPEG currently in the default configuration (in config.json) does not seem to work for everyone. The good
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)
are:
* -i %FRAMES% (the input file listing frames)
* -r %FPS% (sets the frame rate)
* %OUTPUT% (the path to the video file)
If possible, I will change the default configuration to one that more versions/builds of ffmpeg will accept. Do let me
know what arguments are causing issues for you!
### Framerate is not always right with mpegCoder encoding node
The mpegCoder library will always use variable frame rate encoding if it is available in the output format. With most
outputs this means that your actual framerate will differ slightly from the requested one.
+3 -1
View File
@@ -2,4 +2,6 @@ imageio
pilgram
scipy
numpy<1.24>=1.18
torchvision
torchvision
mpegCoder
evalidate
+86 -15
View File
@@ -1,12 +1,17 @@
from typing import Iterable, Tuple
from .types import *
from .categories import NodeCategories
from .shared import DreamConfig, DreamImage
import os, tempfile, subprocess, shutil, random
# -*- coding: utf-8 -*-
import os
import shutil
import subprocess
import tempfile
from functools import lru_cache
from PIL import Image
from .categories import NodeCategories
from .err import on_error
from .shared import DreamConfig, MpegEncoderUtility
from .types import *
CONFIG = DreamConfig()
@@ -117,21 +122,87 @@ def _ffmpeg(config, filenames, fps, output):
for (key, value) in replacements.items():
cmd = list(map(lambda s: s.replace(key, value), cmd))
subprocess.run(cmd, shell=True)
subprocess.check_output(cmd, shell=True)
finally:
os.unlink(tempfilepath)
class DreamVideoEncoder:
NODE_NAME = "FFMPEG Video Encoder"
def _make_video_filename(name, file_ext):
(b, _) = os.path.splitext(name)
return b + "." + file_ext.strip(".")
class DreamVideoEncoderMpegCoder:
NODE_NAME = "Video Encoder (mpegCoder)"
ICON = "🎬"
CATEGORY = NodeCategories.ANIMATION_POSTPROCESSING
RETURN_TYPES = ()
RETURN_NAMES = ()
OUTPUT_NODE = True
FUNCTION = "encode"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.sequence | {
"filename": ("STRING", {"default": 'video.mp4', "multiline": False}),
"name": ("STRING", {"default": 'video', "multiline": False}),
"framerate_factor": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100.0}),
"remove_images": (["yes", "no"],)
"remove_images": ("BOOLEAN", {"default": True})
},
}
def _find_free_filename(self, filename, defaultdir):
if os.path.basename(filename) == filename:
filename = os.path.join(defaultdir, filename)
n = 1
tested = filename
while os.path.exists(tested):
n += 1
(b, ext) = os.path.splitext(filename)
tested = b + "_" + str(n) + ext
return tested
def encode(self, sequence, name, framerate_factor, remove_images):
if not sequence.is_defined:
return ()
config = DreamConfig()
filename = _make_video_filename(name, config.get("mpeg_coder.file_extension", "mp4"))
for batch_num in sequence.batches:
try:
images = list(sequence.get_image_files_of_batch(batch_num))
filename = self._find_free_filename(filename, os.path.dirname(images[0]))
first_image = DreamImage.from_file(images[0])
enc = MpegEncoderUtility(video_path=filename,
bit_rate_factor=float(config.get("mpeg_coder.bitrate_factor", 1.0)),
encoding_threads=int(config.get("mpeg_coder.encoding_threads", 4)),
max_b_frame=int(config.get("mpeg_coder.max_b_frame", 2)),
width=first_image.width,
height=first_image.height,
files=images,
fps=sequence.fps * framerate_factor,
codec_name=config.get("mpeg_coder.codec_name", "libx265"))
enc.encode()
if remove_images:
for imagepath in images:
if os.path.isfile(imagepath):
os.unlink(imagepath)
except Exception as e:
on_error(self.__class__, str(e))
return ()
class DreamVideoEncoder:
NODE_NAME = "FFMPEG Video Encoder"
DISPLAY_NAME = "Video Encoder (FFMPEG)"
ICON = "🎬"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.sequence | {
"name": ("STRING", {"default": 'video', "multiline": False}),
"framerate_factor": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100.0}),
"remove_images": ("BOOLEAN", {"default": True})
},
}
@@ -160,22 +231,22 @@ class DreamVideoEncoder:
filename = self._find_free_filename(filename, os.path.dirname(files[0]))
_ffmpeg(config, files, fps, filename)
def encode(self, sequence: AnimationSequence, filename: str, remove_images, framerate_factor):
def encode(self, sequence: AnimationSequence, name: str, remove_images, framerate_factor):
if not sequence.is_defined:
return ()
config = DreamConfig()
filename = _make_video_filename(name, config.get("ffmpeg.file_extension", "mp4"))
for batch_num in sequence.batches:
try:
images = list(sequence.get_image_files_of_batch(batch_num))
self.generate_video(images, sequence.fps * framerate_factor, filename, config)
if remove_images == "yes":
if remove_images:
for imagepath in images:
if os.path.isfile(imagepath):
os.unlink(imagepath)
except Exception as e:
print("Failed to encode files in dir {}!".format(os.path.dirname(images[0])))
print(str(e))
on_error(self.__class__, str(e))
return ()
+121 -19
View File
@@ -1,13 +1,22 @@
import hashlib, os, json, glob
# -*- coding: utf-8 -*-
import hashlib
import json
import os
import random
import time
import folder_paths as comfy_paths
import glob
import numpy
import torch
from PIL import Image, ImageFilter
from PIL.ImageDraw import ImageDraw
from PIL.PngImagePlugin import PngInfo
from .embedded_config import EMBEDDED_CONFIGURATION
from typing import Dict, Tuple, List
import folder_paths as comfy_paths
from .dreamlogger import DreamLog
NODE_FILE = os.path.abspath(__file__)
DREAM_NODES_SOURCE_ROOT = os.path.dirname(NODE_FILE)
@@ -30,25 +39,40 @@ def _replace_pil_image(data):
return data
_config_data = None
class DreamConfig:
FILEPATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json")
DEFAULT_CONFIG = {
"ffmpeg": {
"path": "ffmpeg",
"arguments": ["-r", "%FPS%", "-f", "concat", "-safe", "0", "-i", "%FRAMES%", "-c:v", "libx265", "-pix_fmt",
"yuv420p", "%OUTPUT%"]
},
"encoding": {
"jpeg_quality": 95
}
}
DEFAULT_CONFIG = EMBEDDED_CONFIGURATION
def __init__(self):
global _config_data
if not os.path.isfile(DreamConfig.FILEPATH):
with open(DreamConfig.FILEPATH, "w") as f:
json.dump(DreamConfig.DEFAULT_CONFIG, f, indent=2)
with open(DreamConfig.FILEPATH) as f:
self._data = json.load(f)
self._data = DreamConfig.DEFAULT_CONFIG
self._save()
if _config_data is None:
with open(DreamConfig.FILEPATH, encoding="utf-8") as f:
self._data = json.load(f)
if self._merge_with_defaults(self._data, DreamConfig.DEFAULT_CONFIG):
self._save()
_config_data = self._data
else:
self._data = _config_data
def _save(self):
with open(DreamConfig.FILEPATH, "w", encoding="utf-8") as f:
json.dump(self._data, f, indent=2)
def _merge_with_defaults(self, config: dict, default_config: dict) -> bool:
changed = False
for key in default_config.keys():
if key not in config:
changed = True
config[key] = default_config[key]
elif isinstance(default_config[key], dict):
changed = changed or self._merge_with_defaults(config[key], default_config[key])
return changed
def get(self, key: str, default=None):
key = key.split(".")
@@ -61,6 +85,11 @@ class DreamConfig:
return d
def get_logger():
config = DreamConfig()
return DreamLog(config.get("debug", False))
class DreamImageProcessor:
def __init__(self, inputs: torch.Tensor, **extra_args):
self._images_in_batch = [convertTensorImageToPIL(tensor) for tensor in inputs]
@@ -89,31 +118,46 @@ class DreamImageProcessor:
return tuple(map(lambda l: torch.cat(l, dim=0), output))
def pick_random_by_weight(data: List[Tuple[float, object]], rng: random.Random):
total_weight = sum(map(lambda item: item[0], data))
r = rng.random()
for (weight, obj) in data:
r -= weight / total_weight
if r <= 0:
return obj
return data[0][1]
class DreamImage:
@classmethod
def join_to_tensor_data(cls, images):
l = list(map(lambda i: i.create_tensor_image(), images))
return torch.cat(l, dim=0)
def __init__(self, tensor_image=None, pil_image=None, file_path=None):
def __init__(self, tensor_image=None, pil_image=None, file_path=None, with_alpha=False):
if pil_image is not None:
self.pil_image = pil_image
elif tensor_image is not None:
self.pil_image = convertTensorImageToPIL(tensor_image)
else:
self.pil_image = Image.open(file_path)
if self.pil_image.mode not in ("RGB", "RGBA"):
if with_alpha and self.pil_image.mode != "RGBA":
self.pil_image = self.pil_image.convert("RGBA")
else:
if self.pil_image.mode not in ("RGB", "RGBA"):
self.pil_image = self.pil_image.convert("RGB")
self.width = self.pil_image.width
self.height = self.pil_image.height
self.size = self.pil_image.size
self._draw = ImageDraw(self.pil_image)
def numpy_array(self):
return numpy.array(self.pil_image)
def _renew(self, pil_image):
self.pil_image = pil_image
self._draw = ImageDraw(self.pil_image)
def __iter__(self):
class _Pixels:
def __init__(self, image: DreamImage):
@@ -133,6 +177,11 @@ class DreamImage:
return _Pixels(self)
def convert(self, mode="RGB"):
if self.pil_image.mode == mode:
return self
return DreamImage(pil_image=self.pil_image.convert(mode))
def create_tensor_image(self):
return convertFromPILToTensorImage(self.pil_image)
@@ -175,6 +224,10 @@ class DreamImage:
def save_jpg(self, filepath, quality=98):
self.pil_image.save(filepath, quality=quality, optimize=True)
@classmethod
def from_file(cls, file_path):
return DreamImage(pil_image=Image.open(file_path))
class DreamMask:
def __init__(self, tensor_image=None, pil_image=None):
@@ -297,3 +350,52 @@ def hashed_as_strings(*items):
m = hashlib.sha256()
m.update(tokens.encode(encoding="utf-8"))
return m.digest().hex()
class MpegEncoderUtility:
def __init__(self, video_path: str, bit_rate_factor: float, width: int, height: int, files: List[str],
fps: float, encoding_threads: int, codec_name, max_b_frame):
import mpegCoder
self._files = files
self._logger = get_logger()
self._enc = mpegCoder.MpegEncoder()
bit_rate = self._calculate_bit_rate(width, height, fps, bit_rate_factor)
self._logger.info("Bitrate "+str(bit_rate))
self._enc.setParameter(
videoPath=video_path, codecName=codec_name,
nthread=encoding_threads, bitRate=bit_rate, width=width, height=height, widthSrc=width,
heightSrc=height,
GOPSize=len(files), maxBframe=max_b_frame, frameRate=self._fps_to_tuple(fps))
def _calculate_bit_rate(self, width: int, height: int, fps: float, bit_rate_factor: float):
bits_per_pixel_base = 0.075
return round(max(10, float(width * height * fps * bits_per_pixel_base * bit_rate_factor * 0.001)))
def encode(self):
if not self._enc.FFmpegSetup():
raise Exception("Failed to setup MPEG Encoder - check parameters!")
try:
t = time.time()
for filepath in self._files:
self._logger.debug("Encoding frame {}", filepath)
image = DreamImage.from_file(filepath).convert("RGB")
self._enc.EncodeFrame(image.numpy_array())
self._enc.FFmpegClose()
self._logger.info("Completed video encoding of {n} frames in {t} seconds", n=len(self._files),
t=round(time.time() - t))
finally:
self._enc.clear()
def _fps_to_tuple(self, fps: float):
def _is_almost_int(f: float):
return abs(f - int(f)) < 0.001
a = fps
b = 1
while not _is_almost_int(a) and b < 100:
a /= 10
b *= 10
a = round(a)
b = round(b)
self._logger.info("Video specified as {fps} fps - encoder framerate {a}/{b}", fps=fps, a=a, b=b)
return (a, b)
+212
View File
@@ -0,0 +1,212 @@
from .categories import NodeCategories
from .err import *
from .shared import ALWAYS_CHANGED_FLAG, hashed_as_strings
from .types import RGBPalette
def _generate_switch_input(type: str):
d = dict()
for i in range(10):
d["input_" + str(i)] = (type,)
return {
"required": {
"select": ("INT", {"defualt": 0, "min": 0, "max": 9}),
"on_missing": (["previous", "next"],)
},
"optional": d
}
def _do_pick(cls, select, on_missing, **args):
direction = 1
if on_missing == "previous":
direction = -1
if len(args) == 0:
on_error(cls, "No inputs provided!")
while args.get("input_" + str(select), None) is None:
select = (select + direction) % 10
return args["input_" + str(select)],
class DreamBigImageSwitch:
_switch_type = "IMAGE"
NODE_NAME = "Big Image Switch"
ICON = "⭆"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = (_switch_type,)
RETURN_NAMES = ("selected",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return _generate_switch_input(cls._switch_type)
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, on_missing, **args)
class DreamBigLatentSwitch:
_switch_type = "LATENT"
NODE_NAME = "Big Latent Switch"
ICON = "⭆"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = (_switch_type,)
RETURN_NAMES = ("selected",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return _generate_switch_input(cls._switch_type)
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, on_missing, **args)
class DreamBigTextSwitch:
_switch_type = "STRING"
NODE_NAME = "Big Text Switch"
ICON = "⭆"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = (_switch_type,)
RETURN_NAMES = ("selected",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return _generate_switch_input(cls._switch_type)
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(values)
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, on_missing, **args)
class DreamBigPaletteSwitch:
_switch_type = RGBPalette.ID
NODE_NAME = "Big Palette Switch"
ICON = "⭆"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = (_switch_type,)
RETURN_NAMES = ("selected",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return _generate_switch_input(cls._switch_type)
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, on_missing, **args)
class DreamBigFloatSwitch:
_switch_type = "FLOAT"
NODE_NAME = "Big Float Switch"
ICON = "⭆"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = (_switch_type,)
RETURN_NAMES = ("selected",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return _generate_switch_input(cls._switch_type)
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(values)
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, on_missing, **args)
class DreamBigIntSwitch:
_switch_type = "INT"
NODE_NAME = "Big Int Switch"
ICON = "⭆"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = (_switch_type,)
RETURN_NAMES = ("selected",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return _generate_switch_input(cls._switch_type)
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(values)
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, on_missing, **args)
class DreamBoolToFloat:
NODE_NAME = "Boolean To Float"
ICON = "⬖"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("result",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"boolean": ("BOOLEAN", {"default": False}),
"on_true": ("FLOAT", {"default": 1.0}),
"on_false": ("FLOAT", {"default": 0.0})
}
}
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(values)
def pick(self, boolean, on_true, on_false):
if boolean:
return (on_true,)
else:
return (on_false,)
class DreamBoolToInt:
NODE_NAME = "Boolean To Int"
ICON = "⬖"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("result",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"boolean": ("BOOLEAN", {"default": False}),
"on_true": ("INT", {"default": 1}),
"on_false": ("INT", {"default": 0})
}
}
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(values)
def pick(self, boolean, on_true, on_false):
if boolean:
return (on_true,)
else:
return (on_false,)
+62 -4
View File
@@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-
import random
import time
from typing import List, Dict
from .shared import DreamImage
import random, time
class RGBPalette:
@@ -60,6 +64,52 @@ class RGBPalette:
return _ColorIterator()
class PartialPrompt:
ID = "PARTIAL_PROMPT"
def __init__(self):
self._data = {}
def add(self, text: str, weight: float):
output = PartialPrompt()
output._data = dict(self._data)
output._data[text.strip()] = weight
return output
def is_empty(self):
return not self._data
def abs_sum(self):
if not self._data:
return 0.0
return sum(map(abs, self._data.values()))
def abs_max(self):
if not self._data:
return 0.0
return max(map(abs, self._data.values()))
def scaled_by(self, f: float):
new_data = PartialPrompt()
new_data._data = dict(self._data)
for text, weight in new_data._data.items():
new_data._data[text] = weight * f
return new_data
def finalize(self, clamp: float):
items = self._data.items()
items = sorted(items, key=lambda pair: (pair[1], pair[0]))
pos = list()
neg = list()
for text, w in sorted(items, key=lambda pair: (-pair[1], pair[0])):
if w >= 0.0001:
pos.append("({}:{:.3f})".format(text, min(clamp, w)))
for text, w in sorted(items, key=lambda pair: (pair[1], pair[0])):
if w <= -0.0001:
neg.append("({}:{:.3f})".format(text, min(clamp, -w)))
return ", ".join(pos), ", ".join(neg)
class FrameCounter:
ID = "FRAME_COUNTER"
@@ -87,6 +137,14 @@ class FrameCounter:
def current_time_in_seconds(self):
return float(self.current_frame) / self.frames_per_second
@property
def total_time_in_seconds(self):
return float(self.total_frames) / self.frames_per_second
@property
def remaining_time_in_seconds(self):
return self.total_time_in_seconds - self.current_time_in_seconds
@property
def progress(self):
return float(self.current_frame) / (max(2, self.total_frames) - 1)
@@ -123,6 +181,6 @@ class AnimationSequence:
class SharedTypes:
frame_counter = {"frame_counter": (FrameCounter.ID, {"forceInput": True})}
sequence = {"sequence": (AnimationSequence.ID, {"forceInput": True})}
palette = {"palette": (RGBPalette.ID, {"forceInput": True})}
frame_counter = {"frame_counter": (FrameCounter.ID,)}
sequence = {"sequence": (AnimationSequence.ID,)}
palette = {"palette": (RGBPalette.ID,)}
+1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
def run_uninstall():
pass
+7 -2
View File
@@ -1,7 +1,9 @@
from .shared import hashed_as_strings
from .categories import NodeCategories
# -*- coding: utf-8 -*-
import math
from .categories import NodeCategories
from .shared import hashed_as_strings
def _align_num(n: int, alignment: int, type: str):
if alignment <= 1:
@@ -16,6 +18,8 @@ def _align_num(n: int, alignment: int, type: str):
class DreamFrameDimensions:
NODE_NAME = "Common Frame Dimensions"
ICON = "⌗"
@classmethod
def INPUT_TYPES(cls):
return {
@@ -48,3 +52,4 @@ class DreamFrameDimensions:
return (width, height, final_width, final_height)
else:
return (height, width, final_height, final_width)