8 Commits
19 changed files with 8349 additions and 92 deletions
+13 -3
View File
@@ -5,12 +5,16 @@ from .base import *
from .colors 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 .inputfields import *
from .calculate import *
from .laboratory import *
_NODE_CLASSES: List[Type] = [DreamSineWave, DreamLinear, DreamCSVCurve, DreamBeatCurve, DreamFrameDimensions,
DreamImageMotion, DreamNoiseFromPalette, DreamAnalyzePalette, DreamColorShift,
@@ -19,12 +23,18 @@ _NODE_CLASSES: List[Type] = [DreamSineWave, DreamLinear, DreamCSVCurve, DreamBea
DreamImageSequenceOutput, DreamCSVGenerator, DreamImageAreaSampler,
DreamVideoEncoder, DreamSequenceTweening, DreamSequenceBlend, DreamColorAlign,
DreamImageSampler, DreamNoiseFromAreaPalettes, DreamVideoEncoderMpegCoder,
DreamInputString, DreamInputFloat, DreamInputInt, DreamInputText]
DreamInputString, DreamInputFloat, DreamInputInt, DreamInputText, DreamBigLatentSwitch,
DreamFrameCountCalculator, DreamBigImageSwitch, DreamBigTextSwitch, DreamBigFloatSwitch,
DreamBigIntSwitch, DreamBigPaletteSwitch, DreamWeightedPromptBuilder, DreamPromptFinalizer,
DreamFrameCounterInfo, DreamBoolToFloat, DreamBoolToInt, DreamSawWave, DreamTriangleWave,
DreamTriangleEvent, DreamSmoothEvent, DreamCalculation, DreamImageColorShift,
DreamComparePalette, DreamImageContrast, DreamImageBrightness, DreamLogFile,
DreamLaboratory, DreamStringToLog, DreamIntToLog, DreamFloatToLog, DreamJoinLog]
_SIGNATURE_SUFFIX = " [Dream]"
MANIFEST = {
"name": "Dream Project Animation",
"version": (3, 0, 1),
"version": (4, 0, 0),
"author": "Dream Project",
"project": "https://github.com/alt-key-project/comfyui-dream-project",
"description": "Various utility nodes for creating animations with ComfyUI",
+63 -2
View File
@@ -6,6 +6,37 @@ from .shared import *
from .types import *
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 = "📂"
@@ -35,7 +66,7 @@ class DreamDirectoryFileCount:
for pattern in patterns.split("|"):
files = list(glob.glob(pattern, root_dir=directory_path))
total += len(files)
print("total "+str(total))
print("total " + str(total))
return (total,)
@@ -105,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}),
},
}
@@ -124,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),)
+99
View File
@@ -0,0 +1,99 @@
# -*- 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"
ICON = "🖩"
@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.UTILS
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))
+3 -1
View File
@@ -5,9 +5,11 @@ class NodeCategories:
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 = "utils"
UTILS_SWITCHES = "utils/switches"
+152 -9
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from .categories import NodeCategories
from .shared import *
from .types import *
@@ -128,7 +129,7 @@ class DreamColorAlign:
def INPUT_TYPES(cls):
return {
"required": SharedTypes.palette | {
"target_align": (RGBPalette.ID, ),
"target_align": (RGBPalette.ID,),
"alignment_factor": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 10.0, "step": 0.1}),
}
}
@@ -151,8 +152,8 @@ class DreamColorAlign:
for i in range(len(palette)):
p = palette[i]
t = target_align[i]
(_, r1, g1, b1) = p.analyze()
(_, r2, g2, b2) = t.analyze()
(_, _, r1, g1, b1) = p.analyze()
(_, _, r2, g2, b2) = t.analyze()
dr = (r2 - r1) * alignment_factor
dg = (g2 - g1) * alignment_factor
@@ -214,9 +215,150 @@ class DreamColorShift:
return (tuple(results),)
class DreamImageColorShift:
NODE_NAME = "Image Color Shift"
ICON = "🖼"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {"image": ("IMAGE",),
"red_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0}),
"green_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0}),
"blue_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0}),
},
}
CATEGORY = NodeCategories.IMAGE_COLORS
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def result(self, image, red_multiplier, green_multiplier, blue_multiplier):
proc = DreamImageProcessor(inputs=image)
def recolor(im: DreamImage, *a, **args):
return (im.adjust_colors(red_multiplier, green_multiplier, blue_multiplier),)
return proc.process(recolor)
class DreamImageBrightness:
NODE_NAME = "Image Brightness Adjustment"
ICON = "☼"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {"image": ("IMAGE",),
"factor": ("FLOAT", {"default": 1.0, "min": 0.0}),
},
}
CATEGORY = NodeCategories.IMAGE_COLORS
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def result(self, image, factor):
proc = DreamImageProcessor(inputs=image)
def change(im: DreamImage, *a, **args):
return (im.change_brightness(factor),)
return proc.process(change)
class DreamImageContrast:
NODE_NAME = "Image Contrast Adjustment"
ICON = "◐"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {"image": ("IMAGE",),
"factor": ("FLOAT", {"default": 1.0, "min": 0.0}),
},
}
CATEGORY = NodeCategories.IMAGE_COLORS
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def result(self, image, factor):
proc = DreamImageProcessor(inputs=image)
def change(im: DreamImage, *a, **args):
return (im.change_contrast(factor),)
return proc.process(change)
class DreamComparePalette:
NODE_NAME = "Compare Palettes"
ICON = "📊"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": (RGBPalette.ID,),
"b": (RGBPalette.ID,),
},
}
CATEGORY = NodeCategories.IMAGE_COLORS
RETURN_TYPES = ("FLOAT", "FLOAT", "FLOAT", "FLOAT")
RETURN_NAMES = (
"brightness_multiplier", "contrast_multiplier", "red_multiplier", "green_multiplier", "blue_multiplier")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def result(self, a, b):
MIN_VALUE = 1 / 255.0
brightness = list()
contrasts = list()
reds = list()
greens = list()
blues = list()
for i in range(min(len(a), len(b))):
(bright, ctr, red, green, blue) = a[i].analyze()
(bright2, ctr2, red2, green2, blue2) = b[i].analyze()
brightness.append(bright2 / max(MIN_VALUE, bright))
contrasts.append(ctr2 / max(MIN_VALUE, ctr))
reds.append(red2 / max(MIN_VALUE, red))
greens.append(green2 / max(MIN_VALUE, green))
blues.append(blue2 / max(MIN_VALUE, blue))
n = len(brightness)
return (sum(brightness) / n, sum(contrasts) / n, sum(reds) / n,
sum(greens) / n, sum(blues) / n)
class DreamAnalyzePalette:
NODE_NAME = "Analyze Palette"
NODE = "📊"
ICON = "📊"
@classmethod
def INPUT_TYPES(cls):
@@ -226,8 +368,8 @@ class DreamAnalyzePalette:
}
CATEGORY = NodeCategories.IMAGE_COLORS
RETURN_TYPES = ("FLOAT", "FLOAT", "FLOAT", "FLOAT")
RETURN_NAMES = ("brightness", "redness", "greenness", "blueness")
RETURN_TYPES = ("FLOAT", "FLOAT", "FLOAT", "FLOAT", "FLOAT")
RETURN_NAMES = ("brightness", "contrast", "redness", "greenness", "blueness")
FUNCTION = "result"
@classmethod
@@ -236,12 +378,13 @@ class DreamAnalyzePalette:
def result(self, palette):
f = 1.0 / len(palette)
(w, r, g, b) = (0, 0, 0, 0)
(w, c, r, g, b) = (0, 0, 0, 0, 0)
for p in palette:
(brightness, red, green, blue) = p.analyze()
(brightness, contrast, red, green, blue) = p.analyze()
w += brightness
c += contrast
r += red
g += green
b += blue
return w * f, r * f, g * f, b * f
return w * f, c * f, r * f, g * f, b * f
+158 -2
View File
@@ -7,6 +7,20 @@ 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:
NODE_NAME = "Sine Curve"
@@ -37,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:
@@ -96,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:
+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", "libx264", "-pix_fmt",
"yuv420p", "%OUTPUT%"]
},
"mpeg_coder": {
"encoding_threads": 4,
"bitrate_factor": 1.0,
"max_b_frame": 2,
"file_extension": "mp4",
"codec_name": "libx264"
},
"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": "✨"
}
},
}
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
import json
from .categories import *
from .shared import ALWAYS_CHANGED_FLAG, DreamStateFile
from .types import *
_laboratory_state = DreamStateFile("laboratory")
class DreamLaboratory:
NODE_NAME = "Laboratory"
ICON = "🧪"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.frame_counter | {
"key": ("STRING", {"default": "Random value " + str(random.randint(0, 1000000))}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"renew_policy": (["every frame", "first frame"],),
"min_value": ("FLOAT", {"default": 0.0}),
"max_value": ("FLOAT", {"default": 1.0}),
"mode": (["random uniform", "random bell", "ladder", "random walk"],),
},
"optional": {
"step_size": ("FLOAT", {"default": 0.1}),
},
}
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = ("FLOAT", "INT", LogEntry.ID)
RETURN_NAMES = ("FLOAT", "INT", "log_entry")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def _generate(self, seed, last_value, min_value, max_value, mode, step_size):
rnd = random.Random()
rnd.seed(seed)
def jsonify(v: float):
return json.loads(json.dumps(v))
if mode == "random uniform":
return jsonify(self._mode_uniform(rnd, last_value, min_value, max_value, step_size))
elif mode == "random bell":
return jsonify(self._mode_bell(rnd, last_value, min_value, max_value, step_size))
elif mode == "ladder":
return jsonify(self._mode_ladder(rnd, last_value, min_value, max_value, step_size))
else:
return jsonify(self._mode_walk(rnd, last_value, min_value, max_value, step_size))
def _mode_uniform(self, rnd: random.Random, last_value: float, min_value: float, max_value: float, step_size):
return rnd.random() * (max_value - min_value) + min_value
def _mode_bell(self, rnd: random.Random, last_value: float, min_value: float, max_value: float, step_size):
s = 0.0
for i in range(3):
s += rnd.random() * (max_value - min_value) + min_value
return s / 3.0
def _mode_ladder(self, rnd: random.Random, last_value: float, min_value: float, max_value: float, step_size):
if last_value is None:
last_value = min_value - step_size
next_value = last_value + step_size
if next_value > max_value:
d = abs(max_value - min_value)
next_value = (next_value - min_value) % d + min_value
return next_value
def _mode_walk(self, rnd: random.Random, last_value: float, min_value: float, max_value: float, step_size):
if last_value is None:
last_value = (max_value - min_value) * 0.5
if rnd.random() >= 0.5:
return min(max_value, last_value + step_size)
else:
return max(min_value, last_value - step_size)
def result(self, key, frame_counter: FrameCounter, seed, renew_policy, min_value, max_value, mode, **values):
if min_value > max_value:
t = max_value
max_value = min_value
min_value = t
step_size = values.get("step_size", abs(max_value - min_value) * 0.1)
last_value = _laboratory_state.get_section("values").get(key, None)
if (last_value is None) or (renew_policy == "every frame") or frame_counter.is_first_frame:
v = _laboratory_state.get_section("values") \
.update(key, 0, lambda old: self._generate(seed, last_value, min_value, max_value, mode, step_size))
return v, round(v), LogEntry.new(
"Laboratory generated new value for '{}': {} ({})".format(key, v, round(v)))
else:
return last_value, round(last_value), LogEntry.new("Laboratory reused value for '{}': {} ({})"
.format(key, last_value, round(last_value)))
+28 -1
View File
@@ -1,30 +1,57 @@
{
"Analyze Palette [Dream]": "Output brightness, red, green and blue averages of a palette",
"Analyze Palette [Dream]": "Output brightness, contrast, 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",
"Compare Palettes [Dream]": "Analyses two palettes producing the factor for each color channel",
"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)",
"Float to Log Entry [Dream]": "Logging for float values",
"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 Brightness Adjustment [Dream]": "Adjusts the brightness of an image by a factor",
"Image Color Shift [Dream]": "Adjust the colors (or brightness) of an image",
"Image Contrast Adjustment [Dream]": "Adjusts the contrast of an image by a factor",
"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)",
"Int to Log Entry [Dream]": "Logging for int values",
"Laboratory [Dream]": "Super-charged number generator for experimenting with ComfyUI",
"Linear Curve [Dream]": "Linear interpolation between two value over the full animation",
"Log Entry Joiner [Dream]": "Merges multiple log entries (reduces noodling)",
"Log File [Dream]": "Logging node for output to file",
"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",
"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)",
"String to Log Entry [Dream]": "Use any string as a log entry",
"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"
}
+21 -10
View File
@@ -8,7 +8,7 @@ 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
from .types import SharedTypes, FrameCounter, AnimationSequence, LogEntry
CONFIG = DreamConfig()
@@ -42,7 +42,7 @@ class DreamImageSequenceOutput:
"directory_path": ("STRING", {"default": comfy_paths.output_directory, "multiline": False}),
"prefix": ("STRING", {"default": 'frame', "multiline": False}),
"digits": ("INT", {"default": 5}),
"at_end": (["stop output", "keep going"],),
"at_end": (["stop output", "raise error", "keep going"],),
"filetype": (['png with embedded workflow', "png", 'jpg'],),
},
"hidden": {
@@ -52,9 +52,9 @@ class DreamImageSequenceOutput:
}
CATEGORY = NodeCategories.IMAGE_ANIMATION
RETURN_TYPES = (AnimationSequence.ID,)
RETURN_TYPES = (AnimationSequence.ID, LogEntry.ID)
OUTPUT_NODE = True
RETURN_NAMES = ("sequence",)
RETURN_NAMES = ("sequence", "log_entry")
FUNCTION = "save"
@classmethod
@@ -64,11 +64,16 @@ class DreamImageSequenceOutput:
def _get_new_filename(self, current_frame, prefix, digits, filetype):
return prefix + "_" + str(current_frame).zfill(digits) + "." + filetype.split(" ")[0]
def _save_single_image(self, dream_image: DreamImage, batch_counter, frame_counter: FrameCounter, directory_path,
prefix, digits, filetype, prompt, extra_pnginfo, at_end):
def _save_single_image(self, dream_image: DreamImage, batch_counter, frame_counter: FrameCounter,
directory_path,
prefix, digits, filetype, prompt, extra_pnginfo, at_end, logger):
if at_end == "stop output" and frame_counter.is_after_last_frame:
print("Reached end of animation - not saving output!")
logger("Reached end of animation - not saving output!")
return ()
if at_end == "raise error" and frame_counter.is_after_last_frame:
logger("Reached end of animation - raising error to stop processing!")
raise Exception("Reached end of animation!")
filename = self._get_new_filename(frame_counter.current_frame, prefix, digits, filetype)
if batch_counter >= 0:
filepath = os.path.join(directory_path, "batch_" + (str(batch_counter).zfill(4)), filename)
@@ -81,7 +86,7 @@ class DreamImageSequenceOutput:
dream_image.save_png(filepath, filetype == 'png with embedded workflow', prompt, extra_pnginfo)
elif filetype == "jpg":
dream_image.save_jpg(filepath, int(CONFIG.get("encoding.jpeg_quality", 95)))
print("Saved {} in {}".format(filename, os.path.abspath(save_dir)))
logger("Saved {} in {}".format(filename, os.path.abspath(save_dir)))
return ()
def _generate_animation_sequence(self, filetype, directory_path, frame_counter):
@@ -93,13 +98,19 @@ class DreamImageSequenceOutput:
return AnimationSequence(frame_counter, frames)
def save(self, image, **args):
log_texts = list()
logger = lambda s: log_texts.append(s)
if not args.get("directory_path", ""):
args["directory_path"] = comfy_paths.output_directory
args["logger"] = logger
proc = DreamImageProcessor(image, **args)
proc.process(self._save_single_image)
frame_counter: FrameCounter = args["frame_counter"]
log_entry = LogEntry([])
for text in log_texts:
log_entry = log_entry.add(text)
if frame_counter.is_final_frame:
return (self._generate_animation_sequence(args["filetype"], args["directory_path"],
frame_counter),)
frame_counter), log_entry)
else:
return (AnimationSequence(frame_counter),)
return (AnimationSequence(frame_counter), log_entry)
+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)
+62
View File
@@ -133,6 +133,23 @@ Output brightness, red, green and blue averages of a palette. Useful to control
### Beat Curve [Dream]
Beat pattern curve with impulses at specified beats of a measure.
### Big *** Switch [Dream]
Switch nodes for different type for up to ten inputs.
### 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.
### Compare Palettes [Dream]
Analyses two palettes and produces the quotient for each individual channel (b/a) and brightness.
### CSV Curve [Dream]
CSV input curve where first column is frame or second and second column is value.
@@ -152,6 +169,12 @@ Post processing for animation sequences using the python module mpegCoder with f
### File Count [Dream]
Finds the number of files in a directory matching specified patterns.
### Float/Int/string to Log Entry [Dream]
Logging for float/int/string values.
### 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.
@@ -159,9 +182,21 @@ Directory backed frame counter, for output directories.
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 Brightness Adjustment [Dream]
Adjusts the brightness of an image by a factor.
### Image Color Shift [Dream]
Allows changing the colors of an image with a multiplier for each channel (RGB).
### Image Contrast Adjustment [Dream]
Adjusts the contrast of an image by a factor.
### Image Motion [Dream]
Node supporting zooming in/out and translating an image.
@@ -177,6 +212,15 @@ Saves a frame to a directory.
### Image Sequence Tweening [Dream]
Post processing for animation sequences generating blended in-between frames.
### Laboratory [Dream]
Super-charged number generator for experimenting with ComfyUI.
### Log Entry Joiner [Dream]
Merges multiple log entries (reduces noodling).
### Log File [Dream]
The text logging facility for the Dream Project Animation nodes.
### Linear Curve [Dream]
Linear interpolation between two values over the full animation.
@@ -202,9 +246,21 @@ 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
@@ -230,6 +286,12 @@ This flow demonstrates sampling image areas into palettes and generating noise f
[area-sampled-noise](examples/area-sampled-noise.json)
### Prompt Morphing
This flow demonstrates prompt building with weights based on curves and brightness and contrast control.
[prompt-morphing](examples/prompt-morphing.json)
## Known issues
### FFMPEG
+1
View File
@@ -4,3 +4,4 @@ scipy
numpy<1.24>=1.18
torchvision
mpegCoder
evalidate
+20 -14
View File
@@ -5,7 +5,7 @@ import subprocess
import tempfile
from functools import lru_cache
from PIL import Image
from PIL import Image as PilImage
from .categories import NodeCategories
from .err import on_error
@@ -16,8 +16,8 @@ CONFIG = DreamConfig()
@lru_cache(5)
def _load_image_cached(filename) -> Image:
return Image.open(filename)
def _load_image_cached(filename):
return PilImage.open(filename)
class TempFileSet:
@@ -136,8 +136,8 @@ class DreamVideoEncoderMpegCoder:
NODE_NAME = "Video Encoder (mpegCoder)"
ICON = "🎬"
CATEGORY = NodeCategories.ANIMATION_POSTPROCESSING
RETURN_TYPES = ()
RETURN_NAMES = ()
RETURN_TYPES = (LogEntry.ID,)
RETURN_NAMES = ("log_entry",)
OUTPUT_NODE = True
FUNCTION = "encode"
@@ -164,9 +164,10 @@ class DreamVideoEncoderMpegCoder:
def encode(self, sequence, name, framerate_factor, remove_images):
if not sequence.is_defined:
return ()
return (LogEntry([]),)
config = DreamConfig()
filename = _make_video_filename(name, config.get("mpeg_coder.file_exension"))
filename = _make_video_filename(name, config.get("mpeg_coder.file_extension", "mp4"))
log_entry = LogEntry([])
for batch_num in sequence.batches:
try:
images = list(sequence.get_image_files_of_batch(batch_num))
@@ -182,13 +183,14 @@ class DreamVideoEncoderMpegCoder:
fps=sequence.fps * framerate_factor,
codec_name=config.get("mpeg_coder.codec_name", "libx265"))
enc.encode()
log_entry = log_entry.add("Generated video '{}'".format(filename))
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 ()
return (log_entry,)
class DreamVideoEncoder:
@@ -207,8 +209,8 @@ class DreamVideoEncoder:
}
CATEGORY = NodeCategories.ANIMATION_POSTPROCESSING
RETURN_TYPES = ()
RETURN_NAMES = ()
RETURN_TYPES = (LogEntry.ID,)
RETURN_NAMES = ("log_entry",)
OUTPUT_NODE = True
FUNCTION = "encode"
@@ -230,24 +232,28 @@ class DreamVideoEncoder:
def generate_video(self, files, fps, filename, config):
filename = self._find_free_filename(filename, os.path.dirname(files[0]))
_ffmpeg(config, files, fps, filename)
return filename
def encode(self, sequence: AnimationSequence, name: str, remove_images, framerate_factor):
if not sequence.is_defined:
return ()
return (LogEntry([]),)
config = DreamConfig()
filename = _make_video_filename(name, config.get("ffmpeg.file_exension"))
filename = _make_video_filename(name, config.get("ffmpeg.file_extension", "mp4"))
log_entry = LogEntry([])
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)
actual_filename = self.generate_video(images, sequence.fps * framerate_factor, filename, config)
log_entry = log_entry.add("Generated video '{}'".format(actual_filename))
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 ()
return (log_entry,)
class DreamSequenceTweening:
+26 -46
View File
@@ -10,12 +10,13 @@ import folder_paths as comfy_paths
import glob
import numpy
import torch
from PIL import Image, ImageFilter
from PIL import Image, ImageFilter, ImageEnhance
from PIL.ImageDraw import ImageDraw
from PIL.PngImagePlugin import PngInfo
from typing import Dict, Tuple, List
from .dreamlogger import DreamLog
from .embedded_config import EMBEDDED_CONFIGURATION
NODE_FILE = os.path.abspath(__file__)
DREAM_NODES_SOURCE_ROOT = os.path.dirname(NODE_FILE)
@@ -43,45 +44,7 @@ _config_data = None
class DreamConfig:
FILEPATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json")
DEFAULT_CONFIG = {
"ffmpeg": {
"file_exension": "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_exension": "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": "🌄",
"Dream": "✨"
}
},
}
DEFAULT_CONFIG = EMBEDDED_CONFIGURATION
def __init__(self):
global _config_data
@@ -188,6 +151,14 @@ class DreamImage:
self.size = self.pil_image.size
self._draw = ImageDraw(self.pil_image)
def change_brightness(self, factor):
enhancer = ImageEnhance.Brightness(self.pil_image)
return DreamImage(pil_image=enhancer.enhance(factor))
def change_contrast(self, factor):
enhancer = ImageEnhance.Contrast(self.pil_image)
return DreamImage(pil_image=enhancer.enhance(factor))
def numpy_array(self):
return numpy.array(self.pil_image)
@@ -232,6 +203,15 @@ class DreamImage:
def blur(self, amount):
return DreamImage(pil_image=self.pil_image.filter(ImageFilter.GaussianBlur(amount)))
def adjust_colors(self, red_factor=1.0, green_factor=1.0, blue_factor=1.0):
# newRed = 1.1*oldRed + 0*oldGreen + 0*oldBlue + constant
# newGreen = 0*oldRed + 0.9*OldGreen + 0*OldBlue + constant
# newBlue = 0*oldRed + 0*OldGreen + 1*OldBlue + constant
matrix = (red_factor, 0, 0, 0,
0, green_factor, 0, 0,
0, 0, blue_factor, 0)
return DreamImage(pil_image=self.pil_image.convert("RGB", matrix))
def get_pixel(self, x, y):
p = self.pil_image.getpixel((x, y))
if len(p) == 4:
@@ -352,9 +332,9 @@ class DreamStateStore:
class DreamStateFile:
def __init__(self, state_file_path=os.path.join(TEMP_PATH, "state.json")):
self._dirname = os.path.dirname(state_file_path)
self._filepath = state_file_path
def __init__(self, state_collection_name="state"):
self._filepath = os.path.join(TEMP_PATH, state_collection_name+".json")
self._dirname = os.path.dirname(self._filepath)
if not os.path.isdir(self._dirname):
os.makedirs(self._dirname)
if not os.path.isfile(self._filepath):
@@ -378,7 +358,6 @@ class DreamStateFile:
self._data[key] = value
with open(self._filepath, "w", encoding="utf-8") as f:
json.dump(self._data, f)
print("* {} -> {}".format(key, value))
return previous
@@ -397,7 +376,7 @@ class MpegEncoderUtility:
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._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,
@@ -405,7 +384,7 @@ class MpegEncoderUtility:
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
bits_per_pixel_base = 0.5
return round(max(10, float(width * height * fps * bits_per_pixel_base * bit_rate_factor * 0.001)))
def encode(self):
@@ -427,6 +406,7 @@ class MpegEncoderUtility:
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:
+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,)
+108 -2
View File
@@ -2,7 +2,7 @@
import random
import time
from typing import List, Dict
from typing import List, Dict, Tuple
from .shared import DreamImage
@@ -26,6 +26,24 @@ class RGBPalette:
for c in colors:
self._colors.append(_fix_tuple(c))
def _calculate_channel_contrast(self, c):
hist = list(map(lambda _: 0, range(16)))
for pixel in self._colors:
hist[pixel[c] // 16] += 1
s = 0
max_possible = (15 - 0) * (len(self) // 2) * (len(self) // 2)
for i in range(16):
for j in range(i):
if i != j:
s += abs(i - j) * hist[i] * hist[j]
return s / max_possible
def _calculate_combined_contrast(self):
s = 0
for c in range(3):
s += self._calculate_channel_contrast(c)
return s / 3
def analyze(self):
total_red = 0
total_blue = 0
@@ -38,7 +56,7 @@ class RGBPalette:
r = float(total_red) / (255 * n)
g = float(total_green) / (255 * n)
b = float(total_blue) / (255 * n)
return ((r + g + b) / 3.0, r, g, b)
return ((r + g + b) / 3.0, self._calculate_combined_contrast(), r, g, b)
def __len__(self):
return len(self._colors)
@@ -64,6 +82,86 @@ 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)
for parts in text.split(","):
parts = parts.strip()
if " " in parts:
output._data["(" + parts + ")"] = weight
else:
output._data[parts] = 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 LogEntry:
ID = "LOG_ENTRY"
@classmethod
def new(cls, text):
return LogEntry([(time.time(), text)])
def __init__(self, data: List[Tuple[float, str]] = None):
if data is None:
self._data = list()
else:
self._data = list(data)
def add(self, text: str):
new_data = list(self._data)
new_data.append((time.time(), text))
return LogEntry(new_data)
def merge(self, log_entry):
new_data = list(self._data)
new_data.extend(log_entry._data)
return LogEntry(new_data)
def get_filtered_entries(self, t: float):
for d in sorted(self._data):
if d[0] > t:
yield d
class FrameCounter:
ID = "FRAME_COUNTER"
@@ -91,6 +189,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)
+180 -2
View File
@@ -1,8 +1,187 @@
# -*- coding: utf-8 -*-
import datetime
import math
import os
import folder_paths as comfy_paths
from .categories import NodeCategories
from .shared import hashed_as_strings
from .shared import hashed_as_strings, DreamStateFile
from .types import LogEntry, SharedTypes, FrameCounter
_logfile_state = DreamStateFile("logging")
class DreamJoinLog:
NODE_NAME = "Log Entry Joiner"
ICON = "🗎"
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = (LogEntry.ID,)
RETURN_NAMES = ("log_entry",)
FUNCTION = "convert"
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"entry_0": (LogEntry.ID,),
"entry_1": (LogEntry.ID,),
"entry_2": (LogEntry.ID,),
"entry_3": (LogEntry.ID,),
}
}
def convert(self, **values):
entry = LogEntry([])
for i in range(4):
txt = values.get("entry_" + str(i), None)
if txt:
entry = entry.merge(txt)
return (entry,)
class DreamFloatToLog:
NODE_NAME = "Float to Log Entry"
ICON = "🗎"
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = (LogEntry.ID,)
RETURN_NAMES = ("log_entry",)
FUNCTION = "convert"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("FLOAT", {"default": 0}),
"label": ("STRING", {"default": ""}),
},
}
def convert(self, label, value):
return (LogEntry.new(label + ": " + str(value)),)
class DreamIntToLog:
NODE_NAME = "Int to Log Entry"
ICON = "🗎"
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = (LogEntry.ID,)
RETURN_NAMES = ("log_entry",)
FUNCTION = "convert"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("INT", {"default": 0}),
"label": ("STRING", {"default": ""}),
},
}
def convert(self, label, value):
return (LogEntry.new(label + ": " + str(value)),)
class DreamStringToLog:
NODE_NAME = "String to Log Entry"
ICON = "🗎"
OUTPUT_NODE = True
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = (LogEntry.ID,)
RETURN_NAMES = ("log_entry",)
FUNCTION = "convert"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"default": ""}),
},
}
def convert(self, text):
return (LogEntry.new(text),)
class DreamLogFile:
NODE_NAME = "Log File"
ICON = "🗎"
OUTPUT_NODE = True
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = ()
RETURN_NAMES = ()
FUNCTION = "write"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.frame_counter | {
"log_directory": ("STRING", {"default": comfy_paths.output_directory}),
"log_filename": ("STRING", {"default": "dreamlog.txt"}),
"stdout": ("BOOLEAN", {"default": True}),
"active": ("BOOLEAN", {"default": True}),
"clock_has_24_hours": ("BOOLEAN", {"default": True}),
},
"optional": {
"entry_0": (LogEntry.ID,),
"entry_1": (LogEntry.ID,),
"entry_2": (LogEntry.ID,),
"entry_3": (LogEntry.ID,),
"entry_4": (LogEntry.ID,),
"entry_5": (LogEntry.ID,),
"entry_6": (LogEntry.ID,),
"entry_7": (LogEntry.ID,),
},
}
def _path_to_log_file(self, log_directory, logfile):
if os.path.isabs(logfile):
return os.path.normpath(os.path.abspath(logfile))
elif os.path.isabs(log_directory):
return os.path.normpath(os.path.abspath(os.path.join(log_directory, logfile)))
elif log_directory:
return os.path.normpath(os.path.abspath(os.path.join(comfy_paths.output_directory, log_directory, logfile)))
else:
return os.path.normpath(os.path.abspath(os.path.join(comfy_paths.output_directory, logfile)))
def _get_tm_format(self, clock_has_24_hours):
if clock_has_24_hours:
return "%a %H:%M:%S"
else:
return "%a %I:%M:%S %p"
def write(self, frame_counter: FrameCounter, log_directory, log_filename, stdout, active, clock_has_24_hours,
**entries):
if not active:
return ()
log_entry = None
for i in range(8):
e = entries.get("entry_" + str(i), None)
if e is not None:
if log_entry is None:
log_entry = e
else:
log_entry = log_entry.merge(e)
log_file_path = self._path_to_log_file(log_directory, log_filename)
ts = _logfile_state.get_section("timestamps").get(log_file_path, 0)
output_text = list()
last_t = 0
for (t, text) in log_entry.get_filtered_entries(ts):
dt = datetime.datetime.fromtimestamp(t)
output_text.append("[frame {}/{} (~{}%), timestamp {}]\n{}".format(frame_counter.current_frame + 1,
frame_counter.total_frames,
round(frame_counter.progress * 100),
dt.strftime(self._get_tm_format(
clock_has_24_hours)), text.rstrip()))
output_text.append("---")
last_t = max(t, last_t)
output_text = "\n".join(output_text) + "\n"
if stdout:
print(output_text)
with open(log_file_path, "a", encoding="utf-8") as f:
f.write(output_text)
_logfile_state.get_section("timestamps").update(log_file_path, 0, lambda _: last_t)
return ()
def _align_num(n: int, alignment: int, type: str):
@@ -52,4 +231,3 @@ class DreamFrameDimensions:
return (width, height, final_width, final_height)
else:
return (height, width, final_height, final_width)