84 Commits
Author SHA1 Message Date
Morgan Johansson f48bed5b8a Removed resampling from Pillow access. 2025-02-16 15:45:39 +01:00
Morgan Johansson b09b8a79eb Removed resampling from Pillow access. 2025-02-16 15:45:15 +01:00
Morgan Johansson 44afbb07c6 Node list fix. 2025-02-15 19:13:25 +01:00
Morgan Johansson a8071b2089 Fix for IS_CHANGED. 2025-02-15 18:49:35 +01:00
Morgan Johansson 12d7ef2a54 Fix for IS_CHANGED 2025-02-12 18:09:16 +01:00
Morgan Johansson a94e21dbdd Fix for IS_CHANGED 2025-02-12 18:03:06 +01:00
Morgan Johansson ae524cd593 Fix for node defaults. 2025-02-11 21:58:33 +01:00
Morgan Johansson d45fffff23 Update check fix. 2025-02-11 21:48:17 +01:00
Morgan Johansson 411948551a Compatibility fix. 2025-02-11 21:41:14 +01:00
Morgan Johansson d87e5d8a7c Compatibility fix. 2025-02-11 21:33:05 +01:00
Morgan Johansson 9e87f209aa Compatibility fix. 2025-02-11 21:27:59 +01:00
Morgan Johansson f09e0f3e36 Compatibility fix. 2025-02-11 21:24:33 +01:00
Morgan Johansson b2dfaea97c Compatibility fix. 2025-02-11 21:18:25 +01:00
Morgan Johansson 1e50facb9e Install fix. 2025-02-11 21:09:48 +01:00
Morgan Johansson b63340f4b7 Version fix. 2025-02-11 21:07:03 +01:00
Morgan Johansson f77719bf34 Version fix. 2025-02-11 21:04:30 +01:00
Morgan Johansson c8fe03350e Version fix. 2025-02-11 20:38:32 +01:00
Morgan Johansson 96258138b6 Re-added random prompt node. 2025-02-11 20:30:11 +01:00
Morgan Johansson 98776cccb6 numpy req versions 2025-02-11 19:47:31 +01:00
Morgan Johansson 942d7d8931 Fix for change check warnings. 2025-02-11 19:46:06 +01:00
Morgan Johansson 1d38975ce3 prompt builder input range fix 2025-01-17 22:00:38 +01:00
Morgan Johansson 403095b513 Random Prompt Words fix. 2025-01-15 21:29:25 +01:00
Morgan Johansson 170664c592 Lazy switches added. 2025-01-15 19:05:13 +01:00
Morgan Johansson 7b73e65cec DreamRandomPromptWords fix. 2025-01-14 17:38:09 +01:00
Morgan Johansson d5c107e541 DreamRandomPromptWords fix. 2025-01-14 17:32:06 +01:00
Morgan Johansson a22b11b43c DreamRandomPromptWords separator added 2025-01-14 17:09:09 +01:00
Morgan Johansson 4b059e5095 DreamRandomPromptWords added 2025-01-14 17:03:29 +01:00
Morgan Johansson b5c804a332 Version bump. 2024-12-07 21:07:28 +01:00
Morgan Johansson 801bcaf1d7 Fixed requirements.txt that could cause parse error on some pip versions 2024-12-07 21:02:26 +01:00
Morgan Johansson b9e6959c1f 14: Switches will now return a None value if there is no valid input. 2024-12-01 10:09:08 +01:00
Morgan Johansson ed2c58cf2f 14: Switches will now return a None value if there is no valid input. 2024-12-01 09:58:49 +01:00
Morgan Johansson f44a70900a Version bump to 1.0.2 2024-12-01 09:50:42 +01:00
Morgan Johansson bff5ca4fbe Version bump to 1.0.1 2024-12-01 09:12:03 +01:00
Morgan Johansson a81c591e9d 14: resolved issue with inf loop in switches when there are inputs but none is valid. 2024-12-01 09:10:09 +01:00
alt-key-project 426a6e85e9 Merge pull request #12 from ComfyNodePRs/licence-update
Update PyProject Toml - License
2024-11-17 07:16:01 +01:00
snomiao e1a7ec58ed chore(licence-update): Update PyProject Toml - License 2024-07-31 15:02:24 +00:00
Morgan Johansson b2ddca87a9 fixed github action branch 2024-07-01 07:34:10 +02:00
Morgan Johansson 6bc4c1616f Merge branch 'master' of https://github.com/alt-key-project/comfyui-dream-project 2024-07-01 07:30:35 +02:00
Morgan Johansson bff8f66047 Version bump 2024-07-01 07:30:30 +02:00
alt-key-project 8babb241dd Merge pull request #10 from haohaocreates/publish
Add Github Action for Publishing to Comfy Registry
2024-07-01 07:27:24 +02:00
Morgan Johansson f456af2ec5 publisher id added 2024-07-01 07:18:55 +02:00
alt-key-project c3c7a4ccfd Merge pull request #11 from haohaocreates/pyproject
Add pyproject.toml for Custom Node Registry
2024-07-01 07:15:25 +02:00
haohaocreates b56e007c57 chore(pyproject): Add pyproject.toml for Custom Node Registry 2024-05-22 16:51:45 -04:00
haohaocreates e6545ec3c9 chore(publish): Add Github Action for Publishing to Comfy Registry 2024-05-22 16:51:43 -04:00
Morgan Johansson 2d642150da 5.0.1 2023-12-21 20:36:49 +01:00
Morgan Johansson c34e9f5213 Fixed issues with big switches. 2023-12-21 20:35:09 +01:00
Morgan Johansson 68246e3adf Added readme notice. 2023-11-25 12:27:15 +01:00
Morgan Johansson ab73436fc2 Added readme notice. 2023-11-25 12:25:42 +01:00
Morgan Johansson d9efc1516a Added readme notice. 2023-11-25 12:25:04 +01:00
Morgan Johansson 6880bde78c Minor update for changed python version. 2023-11-25 12:18:36 +01:00
Morgan Johansson 25cbdff39c Removed mpegCoder encoding as the module does not support newer versions of python. 2023-10-22 18:17:28 +02:00
Morgan Johansson 07bfda783d rename of types.py to dreamtypes.py 2023-10-22 12:08:37 +02:00
alt-key-project d74fdb8de7 Merge pull request #6 from linnkoln/master
Allows to output name of frame file
2023-10-17 21:22:49 +02:00
linnkoln 5def36f87f Allows to output name of frame file 2023-10-12 17:11:54 +03:00
Morgan Johansson 08a0a06820 Removed debug logging. 2023-09-19 20:39:38 +02:00
Morgan Johansson 5ce2e63f01 WAV curves. 2023-09-19 20:19:49 +02:00
Morgan Johansson ebed0e60d9 Update to examples. 2023-09-18 19:29:24 +02:00
Morgan Johansson f65df84de6 New example. 2023-09-18 19:22:54 +02:00
Morgan Johansson c5579afb12 minor fix 2023-09-18 06:59:15 +02:00
Morgan Johansson 7d5b6d8f4a Added string tokenizer utility 2023-09-18 06:54:59 +02:00
Morgan Johansson af492b7669 Example update. 2023-09-17 16:44:40 +02:00
Morgan Johansson 15084a9264 Label on string log entry. 2023-09-17 16:12:59 +02:00
Morgan Johansson 01a9d17b42 4.0 release - laboratory and logging 2023-09-17 10:08:45 +02:00
Morgan Johansson f4f5a5c7c5 Changed default codec to H.264 2023-09-16 10:49:40 +02:00
Morgan Johansson bcac6e1a33 New example + increased default bitrate for mpeg 2023-09-16 09:31:40 +02:00
Morgan Johansson 96e5fa3ae8 Contrast analysis/adjustment 2023-09-16 06:24:56 +02:00
Morgan Johansson 4418ba9454 version 2023-09-13 18:54:10 +02:00
Morgan Johansson fefe43ebe3 removed junk text 2023-09-13 18:53:08 +02:00
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
40 changed files with 24479 additions and 3421 deletions
+21
View File
@@ -0,0 +1,21 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- master
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+2 -1
View File
@@ -1,3 +1,4 @@
.idea
__pycache__
config.json
config.json
*.cmd
+65 -13
View File
@@ -1,27 +1,45 @@
from typing import List, Type
# -*- coding: utf-8 -*-
from typing import Type
import sys,os
sys.path.append(str(os.path.dirname(os.path.abspath(__file__))))
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 *
from .laboratory import *
#from .lazyswitches 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,
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,
DreamStringTokenizer, DreamWavCurve, DreamFrameCounterTimeOffset, DreamRandomPromptWords]
_SIGNATURE_SUFFIX = " [Dream]"
MANIFEST = {
"name": "Dream Project Animation",
"version": (1, 1, 0),
"version": (5, 1, 2),
"author": "Dream Project",
"project": "https://github.com/alt-key-project/comfyui-dream-project",
"description": "Various utility nodes for creating animations with ComfyUI",
@@ -31,13 +49,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)))
+104 -17
View File
@@ -1,11 +1,41 @@
# -*- coding: utf-8 -*-
import glob
from .categories import NodeCategories
from .shared import *
from .types import *
import glob
from .dreamtypes 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"
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):
@@ -22,21 +52,30 @@ class DreamDirectoryFileCount:
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *v):
return ALWAYS_CHANGED_FLAG
def IS_CHANGED(cls, directory_path, patterns):
if not os.path.isdir(directory_path):
return ""
total = 0
for pattern in patterns.split("|"):
files = list(glob.glob(pattern, root_dir=directory_path))
total += len(files)
return total
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)
return (total,)
class DreamFrameCounterOffset:
NODE_NAME = "Frame Counter Offset"
ICON = "±"
@classmethod
def INPUT_TYPES(cls):
return {
@@ -50,16 +89,35 @@ class DreamFrameCounterOffset:
RETURN_NAMES = ("frame_counter",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, frame_counter, offset):
return hashed_as_strings(frame_counter, offset)
def result(self, frame_counter: FrameCounter, offset):
return (frame_counter.incremented(offset),)
class DreamFrameCounterTimeOffset:
NODE_NAME = "Frame Counter Time Offset"
ICON = "±"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.frame_counter | {
"offset_seconds": ("FLOAT", {"default": 0.0}),
},
}
CATEGORY = NodeCategories.ANIMATION
RETURN_TYPES = (FrameCounter.ID,)
RETURN_NAMES = ("frame_counter",)
FUNCTION = "result"
def result(self, frame_counter: FrameCounter, offset_seconds):
offset = offset_seconds * frame_counter.frames_per_second
return (frame_counter.incremented(offset),)
class DreamSimpleFrameCounter:
NODE_NAME = "Frame Counter (Simple)"
ICON = "⚋"
@classmethod
def INPUT_TYPES(cls):
@@ -76,10 +134,6 @@ class DreamSimpleFrameCounter:
RETURN_NAMES = ("frame_counter",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def result(self, frame_index, total_frames, frames_per_second):
n = frame_index
return (FrameCounter(n, total_frames, frames_per_second),)
@@ -87,6 +141,7 @@ class DreamSimpleFrameCounter:
class DreamDirectoryBackedFrameCounter:
NODE_NAME = "Frame Counter (Directory)"
ICON = "⚋"
@classmethod
def INPUT_TYPES(cls):
@@ -96,7 +151,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}),
},
}
@@ -106,8 +161,14 @@ class DreamDirectoryBackedFrameCounter:
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def IS_CHANGED(cls, directory_path, patterns, indexing, total_frames, frames_per_second):
if not os.path.isdir(directory_path):
return ""
total = 0
for pattern in patterns.split("|"):
files = list(glob.glob(pattern, root_dir=directory_path))
total += len(files)
return (total, indexing, total_frames, frames_per_second)
def result(self, directory_path, pattern, indexing, total_frames, frames_per_second):
results = list_images_in_directory(directory_path, pattern, indexing == "alphabetic order")
@@ -115,3 +176,29 @@ 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"
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),)
+95
View File
@@ -0,0 +1,95 @@
# -*- 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"
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"
+215 -27
View File
@@ -1,6 +1,83 @@
from .shared import *
from .types import *
# -*- coding: utf-8 -*-
from .categories import NodeCategories
from .shared import *
from .dreamtypes import *
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"
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 +88,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})
},
}
@@ -21,10 +98,6 @@ class DreamImageSampler:
RETURN_NAMES = ("palette",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def result(self, image, samples, seed):
result = list()
r = random.Random()
@@ -48,7 +121,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}),
}
}
@@ -58,10 +131,6 @@ class DreamColorAlign:
RETURN_NAMES = ("palette",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def result(self, palette: Tuple[RGBPalette], target_align: Tuple[RGBPalette], alignment_factor: float):
results = list()
@@ -71,8 +140,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
@@ -106,10 +175,6 @@ class DreamColorShift:
RETURN_NAMES = ("palette",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def result(self, palette, red_multiplier, green_multiplier, blue_multiplier, fixed_brightness):
results = list()
@@ -134,8 +199,134 @@ 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"
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"
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"
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"
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"
ICON = "📊"
@classmethod
def INPUT_TYPES(cls):
@@ -145,22 +336,19 @@ 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
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
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
+217 -29
View File
@@ -1,8 +1,28 @@
import math, csv
# -*- coding: utf-8 -*-
import csv
import functools
import math
import os
from scipy.io.wavfile import read as wav_read
from .types import SharedTypes, FrameCounter
from .shared import hashed_as_strings
from .categories import NodeCategories
from .shared import hashed_as_strings
from .dreamtypes 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:
@@ -24,10 +44,6 @@ class DreamSineWave:
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
a = (max_value - min_value) * 0.5
@@ -35,7 +51,193 @@ 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"
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"
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 WavData:
def __init__(self, sampling_rate: float, single_channel_samples, fps: float):
self._length_in_seconds = len(single_channel_samples) / sampling_rate
self._num_buckets = round(self._length_in_seconds * fps * 3)
self._bucket_size = len(single_channel_samples) / float(self._num_buckets)
self._buckets = list()
self._rate = sampling_rate
self._max_bucket_value = 0
for i in range(self._num_buckets):
start_index = round(i * self._bucket_size)
end_index = round((i + 1) * self._bucket_size) - 1
samples = list(map(lambda n: abs(n), single_channel_samples[start_index:end_index]))
bucket_total = sum(samples)
self._buckets.append(bucket_total)
self._max_bucket_value=max(bucket_total, self._max_bucket_value)
for i in range(self._num_buckets):
self._buckets[i] = float(self._buckets[i]) / self._max_bucket_value
def value_at_time(self, second: float) -> float:
if second < 0.0 or second > self._length_in_seconds:
return 0.0
nsample = second * self._rate
nbucket = min(max(0, round(nsample / self._bucket_size)), self._num_buckets - 1)
return self._buckets[nbucket]
@functools.lru_cache(4)
def _wav_loader(filepath, fps):
sampling_rate, samples = wav_read(filepath)
single_channel = samples[:, 0]
return WavData(sampling_rate, single_channel, fps)
class DreamWavCurve:
NODE_NAME = "WAV Curve"
CATEGORY = NodeCategories.ANIMATION_CURVES
RETURN_TYPES = ("FLOAT", "INT")
RETURN_NAMES = ("FLOAT", "INT")
FUNCTION = "result"
ICON = "∿"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.frame_counter | {
"wav_path": ("STRING", {"default": "audio.wav"}),
"scale": ("FLOAT", {"default": 1.0, "multiline": False})
},
}
def result(self, frame_counter: FrameCounter, wav_path, scale):
if not os.path.isfile(wav_path):
return (0.0, 0)
data = _wav_loader(wav_path, frame_counter.frames_per_second)
frame_counter.current_time_in_seconds
v = data.value_at_time(frame_counter.current_time_in_seconds)
return (v * scale, round(v * scale))
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"
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"
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:
@@ -55,9 +257,9 @@ class DreamBeatCurve:
"accent_1": ("INT", {"default": 1, "min": 1, "max": 24}),
},
"optional": {
"accent_2": ("INT", {"default": 3, "min": 1, "max": 24}),
"accent_3": ("INT", {"default": 0}),
"accent_4": ("INT", {"default": 0}),
"accent_2": ("INT", {"default": 0, "min": 0, "max": 24}),
"accent_3": ("INT", {"default": 0, "min": 0, "max": 24}),
"accent_4": ("INT", {"default": 0, "min": 0, "max": 24}),
}
}
@@ -66,10 +268,6 @@ class DreamBeatCurve:
RETURN_NAMES = ("FLOAT", "INT")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def _get_value_for_accent(self, accent, measure_length, bpm, frame_counter: FrameCounter, frame_offset):
current_frame = frame_counter.current_frame + frame_offset
frames_per_minute = frame_counter.frames_per_second * 60.0
@@ -82,7 +280,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 +292,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:
@@ -113,10 +312,6 @@ class DreamLinear:
RETURN_NAMES = ("FLOAT", "INT")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def result(self, initial_value, final_value, frame_counter: FrameCounter):
d = final_value - initial_value
v = initial_value + frame_counter.progress * d
@@ -133,6 +328,7 @@ def _is_as_float(s: str):
class DreamCSVGenerator:
NODE_NAME = "CSV Generator"
ICON = "⌗"
@classmethod
def INPUT_TYPES(cls):
@@ -150,10 +346,6 @@ class DreamCSVGenerator:
FUNCTION = "write"
OUTPUT_NODE = True
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def write(self, csvfile, frame_counter: FrameCounter, value, csv_dialect):
if frame_counter.is_first_frame and csvfile:
with open(csvfile, 'w', newline='') as csvfile:
@@ -186,10 +378,6 @@ class DreamCSVCurve:
RETURN_NAMES = ("FLOAT", "INT")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def _row_yield(self, file, csv_dialect):
prev_row = None
for row in csv.reader(file, dialect=csv_dialect):
+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)
+238
View File
@@ -0,0 +1,238 @@
# -*- coding: utf-8 -*-
import random
import time
from typing import List, Dict, Tuple
from .shared import DreamImage
class RGBPalette:
ID = "RGB_PALETTE"
def __init__(self, colors: List[tuple[int, int, int]] = None, image: DreamImage = None):
self._colors = []
def _fix_tuple(t):
if len(t) < 3:
return (t[0], t[0], t[0])
else:
return t
if image:
for p, _, _ in image:
self._colors.append(_fix_tuple(p))
if colors:
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
total_green = 0
for pixel in self:
total_red += pixel[0]
total_green += pixel[1]
total_blue += pixel[2]
n = len(self._colors)
r = float(total_red) / (255 * n)
g = float(total_green) / (255 * n)
b = float(total_blue) / (255 * n)
return ((r + g + b) / 3.0, self._calculate_combined_contrast(), r, g, b)
def __len__(self):
return len(self._colors)
def __iter__(self):
return iter(self._colors)
def random_iteration(self, seed=None):
s = seed if seed is not None else int(time.time() * 1000)
n = len(self._colors) - 1
c = self._colors
class _ColorIterator:
def __init__(self):
self._r = random.Random()
self._r.seed(s)
self._n = n
self._c = c
def __next__(self):
return self._c[self._r.randint(0, self._n)]
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"
def __init__(self, current_frame=0, total_frames=1, frames_per_second=25.0):
self.current_frame = max(0, current_frame)
self.total_frames = max(total_frames, 1)
self.frames_per_second = float(max(1.0, frames_per_second))
def incremented(self, amount: int):
return FrameCounter(self.current_frame + amount, self.total_frames, self.frames_per_second)
@property
def is_first_frame(self):
return self.current_frame == 0
@property
def is_final_frame(self):
return (self.current_frame + 1) == self.total_frames
@property
def is_after_last_frame(self):
return self.current_frame >= self.total_frames
@property
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)
class AnimationSequence:
ID = "ANIMATION_SEQUENCE"
def __init__(self, frame_counter: FrameCounter, frames: Dict[int, List[str]] = None):
self.frames = frames
self.fps = frame_counter.frames_per_second
self.frame_counter = frame_counter
if self.is_defined:
self.keys_in_order = sorted(frames.keys())
self.num_batches = min(map(len, self.frames.values()))
else:
self.keys_in_order = []
self.num_batches = 0
@property
def batches(self):
return range(self.num_batches)
def get_image_files_of_batch(self, batch_num):
for key in self.keys_in_order:
yield self.frames[key][batch_num]
@property
def is_defined(self):
if self.frames:
return True
else:
return False
class SharedTypes:
frame_counter = {"frame_counter": (FrameCounter.ID,)}
sequence = {"sequence": (AnimationSequence.ID,)}
palette = {"palette": (RGBPalette.ID,)}
+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": "✨"
}
},
}
+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
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

+10 -13
View File
@@ -1,14 +1,14 @@
# -*- 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 .shared import ALWAYS_CHANGED_FLAG, convertTensorImageToPIL, DreamImageProcessor, \
from .shared import convertTensorImageToPIL, DreamImageProcessor, \
DreamImage, DreamMask
from .dreamtypes import SharedTypes, FrameCounter
class DreamImageMotion:
NODE_NAME = "Image Motion"
@@ -40,10 +40,6 @@ class DreamImageMotion:
RETURN_NAMES = ("image", "mask1", "mask2", "mask3")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def _mk_PIL_image(self, size, color=None, mode="RGB") -> Image:
im = Image.new(mode=mode, size=size)
if color:
@@ -75,7 +71,7 @@ class DreamImageMotion:
return min(max(i, 1), 32767)
if output_resize_height and output_resize_width:
return lambda img: img.resize((bound(output_resize_width), bound(output_resize_height)), Resampling.NEAREST)
return lambda img: img.resize((bound(output_resize_width), bound(output_resize_height)))
else:
return lambda img: img
@@ -85,7 +81,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)
@@ -95,12 +92,12 @@ class DreamImageMotion:
noise = other.get("noise", None)
multiplier = math.pow(2, zoom)
resized_image = pil_image.resize((round(pil_image.width * multiplier),
round(pil_image.height * multiplier)), Resampling.BILINEAR)
round(pil_image.height * multiplier)))
if noise is None:
base_image = self._mk_PIL_image(pil_image.size, "black")
else:
base_image = convertTensorImageToPIL(noise).resize(pil_image.size, Resampling.BILINEAR)
base_image = convertTensorImageToPIL(noise).resize(pil_image.size)
selection_offset = (round(x_translation * pil_image.width), round(y_translation * pil_image.height))
selection = ((pil_image.width - resized_image.width) // 2 + selection_offset[0],
+84
View File
@@ -0,0 +1,84 @@
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"
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"
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"
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"
def noop(self, value):
return (value,)
+8 -3
View File
@@ -1,9 +1,14 @@
from .shared import DreamConfig
# -*- coding: utf-8 -*-
import sys, os
#sys.path.append(str(os.path.dirname(os.path.abspath(__file__))))
#from . import shared
def setup_default_config():
DreamConfig()
#shared.DreamConfig()
pass
def run_install():
setup_default_config()
+96
View File
@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
import json
from .categories import *
from .shared import DreamStateFile
from .dreamtypes 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"
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)))
+151
View File
@@ -0,0 +1,151 @@
from .categories import NodeCategories
from .dreamtypes import RGBPalette
_NOT_A_VALUE_I = 9223372036854775807
_NOT_A_VALUE_F = float(_NOT_A_VALUE_I)
_NOT_A_VALUE_S = "⭆"
def _generate_switch_input(type_nm: str, default_value=None):
d = dict()
for i in range(10):
if default_value is None:
d["input_" + str(i)] = (type_nm, {"lazy": True})
else:
d["input_" + str(i)] = (type_nm, {"default": default_value, "forceInput": True, "lazy": True})
return {
"required": {
"select": ("INT", {"default": 0, "min": 0, "max": 9})
},
"optional": d
}
def _check_big_switch_lazy_status(*args, **kwargs):
n = int(kwargs['select'])
input_name = f"input_{n}"
print(f"SELECTED: {input_name}")
if input_name in kwargs:
return [input_name]
else:
return []
class DreamLazyImageSwitch:
_switch_type = "IMAGE"
NODE_NAME = "Lazy 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)
def check_lazy_status(self, *args, **kwargs):
return _check_big_switch_lazy_status(*args, **kwargs)
def pick(self, select, **args):
return (args.get("input_"+str(select), None),)
class DreamLazyLatentSwitch:
_switch_type = "LATENT"
NODE_NAME = "Lazy 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)
def check_lazy_status(self, *args, **kwargs):
return _check_big_switch_lazy_status(*args, **kwargs)
def pick(self, select, **args):
return (args.get("input_" + str(select), None),)
class DreamLazyTextSwitch:
_switch_type = "STRING"
NODE_NAME = "Lazy 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, _NOT_A_VALUE_S)
def check_lazy_status(self, *args, **kwargs):
return _check_big_switch_lazy_status(*args, **kwargs)
def pick(self, select, **args):
return (args.get("input_" + str(select), None),)
class DreamLazyPaletteSwitch:
_switch_type = RGBPalette.ID
NODE_NAME = "Lazy 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)
def check_lazy_status(self, *args, **kwargs):
return _check_big_switch_lazy_status(*args, **kwargs)
def pick(self, select, **args):
return (args.get("input_" + str(select), None),)
class DreamLazyFloatSwitch:
_switch_type = "FLOAT"
NODE_NAME = "Lazy 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, _NOT_A_VALUE_F)
def check_lazy_status(self, *args, **kwargs):
return _check_big_switch_lazy_status(*args, **kwargs)
def pick(self, select, **args):
return (args.get("input_" + str(select), None),)
class DreamLazyIntSwitch:
_switch_type = "INT"
NODE_NAME = "Lazy 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, _NOT_A_VALUE_I)
def check_lazy_status(self, *args, **kwargs):
return _check_big_switch_lazy_status(*args, **kwargs)
def pick(self, select, **args):
return (args.get("input_" + str(select), None),)
+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.
+10 -8
View File
@@ -1,12 +1,13 @@
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
from .shared import list_images_in_directory, DreamImage
from .dreamtypes import SharedTypes, FrameCounter
import os
class DreamImageSequenceInputWithDefaultFallback:
NODE_NAME = "Image Sequence Loader"
ICON = "💾"
@classmethod
def INPUT_TYPES(cls):
@@ -22,13 +23,13 @@ class DreamImageSequenceInputWithDefaultFallback:
}
CATEGORY = NodeCategories.IMAGE_ANIMATION
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
RETURN_TYPES = ("IMAGE","STRING")
RETURN_NAMES = ("image","frame_name")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def IS_CHANGED(cls, *values, **kwargs):
return float("NaN")
def result(self, frame_counter: FrameCounter, directory_path, pattern, indexing, **other):
default_image = other.get("default_image", None)
@@ -37,5 +38,6 @@ class DreamImageSequenceInputWithDefaultFallback:
if not entry:
return (default_image, "")
else:
image_names = [os.path.basename(file_path) for file_path in entry]
images = map(lambda f: DreamImage(file_path=f), entry)
return (DreamImage.join_to_tensor_data(images),)
return (DreamImage.join_to_tensor_data(images), image_names[0])
+39 -2
View File
@@ -1,23 +1,60 @@
{
"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",
"Frame Counter Time Offset [Dream]": "Adds an offset to a frame counter in seconds",
"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",
"Random Prompt Words [Dream]": "Picks random words from input",
"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)",
"String Tokenizer [Dream]": "Extract individual words or phrases from a text as tokens",
"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",
"WAV Curve [Dream]": "WAV audio file as a curve"
}
+125 -24
View File
@@ -1,10 +1,31 @@
from .shared import *
from .types import *
# -*- coding: utf-8 -*-
import math
from .categories import NodeCategories
from .shared import *
from .dreamtypes import *
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})
},
}
@@ -23,32 +44,112 @@ class DreamNoiseFromPalette:
RETURN_NAMES = ("image",)
FUNCTION = "result"
@classmethod
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"
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),)
+32 -20
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 DreamImageProcessor, DreamImage, \
list_images_in_directory, DreamConfig
from .dreamtypes import SharedTypes, FrameCounter, AnimationSequence, LogEntry
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):
@@ -37,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": {
@@ -47,23 +52,24 @@ 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
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
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)
@@ -76,7 +82,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):
@@ -88,13 +94,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"]
frame_counter = 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)
+100
View File
@@ -0,0 +1,100 @@
from .categories import NodeCategories
from .dreamtypes import PartialPrompt
import random
class DreamRandomPromptWords:
NODE_NAME = "Random Prompt Words"
ICON = "⚅"
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"partial_prompt": (PartialPrompt.ID,)
},
"required": {
"words": ("STRING", {"default": "", "multiline": True}),
"separator": ("STRING", {"default": ",", "multiline": False}),
"samples": ("INT", {"default": 1, "min": 1, "max": 100}),
"min_weight": ("FLOAT", {"default": 1.0, "min": -10, "max": 10}),
"max_weight": ("FLOAT", {"default": 1.0, "min": -10, "max": 10}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
},
}
CATEGORY = NodeCategories.CONDITIONING
RETURN_TYPES = (PartialPrompt.ID,)
RETURN_NAMES = ("partial_prompt",)
FUNCTION = "result"
def result(self, words: str, separator, samples, min_weight, max_weight, seed, **args):
p = args.get("partial_prompt", PartialPrompt())
rnd = random.Random()
rnd.seed(seed)
words = list(set(map(lambda s: s.strip(), filter(lambda s: s.strip() != "", words.split(separator)))))
samples = min(samples, len(words))
for i in range(samples):
picked_word = words[rnd.randint(0, len(words)-1)]
words = list(filter(lambda s: s!=picked_word, words))
weight = rnd.uniform(min_weight, max_weight)
p = p.add(picked_word, weight)
return (p,)
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"
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"
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)
+15
View File
@@ -0,0 +1,15 @@
[project]
name = "comfyui-dream-project"
description = "This extension offers various nodes that are useful for Deforum-like animations in ComfyUI."
version = "5.1.2"
license = { text = "MIT License" }
dependencies = ["imageio", "pilgram", "scipy", "numpy<2.0,>=1.18", "torchvision", "evalidate"]
[project.urls]
Repository = "https://github.com/alt-key-project/comfyui-dream-project"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "altkeyproject"
DisplayName = "comfyui-dream-project"
Icon = ""
+201 -5
View File
@@ -7,8 +7,25 @@ and useful to many ComfyUI users.
I have demonstrated the use of these custom nodes in this [youtube video](https://youtu.be/pZ6Li3qF-Kk).
# Notice!
This custom node pack is currently not being updated. Stable Diffusion video generation is moving towards a different
workflow with AnimateDiff and Stable Video Diffusion. I decided to not try to update this node pack, but I am instead
creating a separate custom node pack here:
[github](https://github.com/alt-key-project/comfyui-dream-video-batches)
This new node pack will be getting my attention from now on (at least as long as stable diffusion video generation is done mostly
in batches).
## 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 +43,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:
@@ -57,6 +144,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.
@@ -67,12 +171,18 @@ 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.
### Video Encoder (FFMPEG) [Dream]
Post processing for animation sequences calling FFMPEG to generate video files.
### 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.
@@ -80,8 +190,23 @@ 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.
Adds an offset (in frames) to a frame counter.
### Frame Counter Time Offset [Dream]
Adds an offset in seconds 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.
@@ -98,9 +223,26 @@ 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.
### Lazy *** Switch [Dream]
Switch nodes for different type for up to ten inputs. The lazy version only evaluates the
selected input, but first/next of the big switch is unsupported.
### 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.
### 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.
@@ -112,12 +254,37 @@ 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.
### String Tokenizer [Dream]
Splits a text into tokens by a separator and returns one of the tokens based on a given index.
### Random Prompt Words [Dream]
Randomly picks words/tokens/phrases from an input text.
### Triangle Curve [Dream]
Triangle wave curve.
### Triangle Event Curve [Dream]
Single event/peak curve with triangular shape.
### WAV Curve [Dream]
Use an uncompressed WAV audio file as a 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
@@ -131,12 +298,36 @@ This example should be a starting point for anyone wanting to build with the Dre
[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)
### Prompt Morphing
This flow demonstrates prompt building with weights based on curves and brightness and contrast control.
[prompt-morphing](examples/prompt-morphing.json)
### Laboratory
This flow demonstrates use of the Laboratory and Logging nodes.
[laboratory](examples/laboratory.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 fixed parameters (that need to be in the call)
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)
@@ -144,4 +335,9 @@ are:
* %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!
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 -2
View File
@@ -1,5 +1,6 @@
imageio
pilgram
scipy
numpy<1.24>=1.18
torchvision
numpy<2.0,>=1.18
torchvision
evalidate
+99 -21
View File
@@ -1,18 +1,24 @@
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 PIL import Image as PilImage
from .categories import NodeCategories
from .err import on_error
from .shared import DreamConfig
#from .shared import MpegEncoderUtility
from .dreamtypes import *
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:
@@ -117,27 +123,95 @@ 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)
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 = (LogEntry.ID,)
# RETURN_NAMES = ("log_entry",)
# OUTPUT_NODE = True
# FUNCTION = "encode"
#
# @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})
# },
# }
#
# 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 (LogEntry([]),)
# config = DreamConfig()
# 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))
# 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()
# 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 (log_entry,)
#
class DreamVideoEncoder:
NODE_NAME = "FFMPEG Video Encoder"
DISPLAY_NAME = "Video Encoder (FFMPEG)"
ICON = "🎬"
@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})
},
}
CATEGORY = NodeCategories.ANIMATION_POSTPROCESSING
RETURN_TYPES = ()
RETURN_NAMES = ()
RETURN_TYPES = (LogEntry.ID,)
RETURN_NAMES = ("log_entry",)
OUTPUT_NODE = True
FUNCTION = "encode"
@@ -159,24 +233,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, filename: str, remove_images, framerate_factor):
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_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)
if remove_images == "yes":
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:
print("Failed to encode files in dir {}!".format(os.path.dirname(images[0])))
print(str(e))
return ()
on_error(self.__class__, str(e))
return (log_entry,)
class DreamSequenceTweening:
+121 -29
View File
@@ -1,18 +1,28 @@
import hashlib, os, json, glob
# -*- coding: utf-8 -*-
import hashlib
import json
import os
import random
import tempfile
import glob
from io import BytesIO
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
import folder_paths as comfy_paths
from .dreamlogger import DreamLog
from .embedded_config import EMBEDDED_CONFIGURATION
tmpDir = tempfile.TemporaryDirectory("Dream_Anim")
NODE_FILE = os.path.abspath(__file__)
DREAM_NODES_SOURCE_ROOT = os.path.dirname(NODE_FILE)
TEMP_PATH = os.path.join(os.path.abspath(comfy_paths.temp_directory), "Dream_Anim")
ALWAYS_CHANGED_FLAG = float("NaN")
TEMP_PATH = os.path.join(os.path.abspath(tempfile.gettempdir()), "Dream_Anim")
def convertTensorImageToPIL(tensor_image) -> Image:
@@ -30,25 +40,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 +86,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 +119,54 @@ 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 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)
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 +186,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)
@@ -146,6 +204,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:
@@ -175,6 +242,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):
@@ -204,7 +275,7 @@ def list_images_in_directory(directory_path: str, pattern: str, alphabetic_index
def _num_from_filename(fn):
(text, _) = os.path.splitext(fn)
token: str = text.split("_")[-1]
token = text.split("_")[-1]
if token.isdigit():
return int(token)
else:
@@ -262,13 +333,13 @@ 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):
self._data: dict = {}
self._data = {}
else:
with open(self._filepath, encoding="utf-8") as f:
self._data = json.load(f)
@@ -288,12 +359,33 @@ 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
def hashed_as_strings(*items):
def hash_tensor_data(image, hasher = None):
m = hashlib.sha256() if hasher is None else hasher
if isinstance(image, torch.Tensor):
buff = BytesIO()
torch.save(image, buff)
print("HASHING TENSOR")
m.update(buff.getvalue())
elif isinstance(image, bytes) or isinstance(image, bytearray):
print("HASHING BYTES")
m.update(image)
elif isinstance(image, list) or isinstance(image, tuple):
print("HASHING ITERABLE")
for item in image:
hash_tensor_data(item, m)
else:
print("HASING_AS_TEXT - "+str(type(image)))
m.update(str(image).encode(encoding="utf-8"))
return m.digest().hex()
def hashed_as_strings(*items, **kwargs):
tokens = "|".join(list(map(str, items)))
m = hashlib.sha256()
m.update(tokens.encode(encoding="utf-8"))
for pair in kwargs.items():
m.update(str(pair).encode(encoding="utf-8"))
return m.digest().hex()
+190
View File
@@ -0,0 +1,190 @@
from .categories import NodeCategories
from .dreamtypes import RGBPalette
from .err import *
from .shared import hashed_as_strings, hash_tensor_data
_NOT_A_VALUE_I = 9223372036854775807
_NOT_A_VALUE_F = float(_NOT_A_VALUE_I)
_NOT_A_VALUE_S = "⭆"
def _generate_switch_input(type_nm: str, default_value=None):
d = dict()
for i in range(10):
if default_value is None:
d["input_" + str(i)] = (type_nm,)
else:
d["input_" + str(i)] = (type_nm, {"default": default_value, "forceInput": True})
return {
"required": {
"select": ("INT", {"default": 0, "min": 0, "max": 9}),
"on_missing": (["previous", "next"],)
},
"optional": d
}
def _do_pick(cls, select, test_val, on_missing, **args):
direction = 1
if on_missing == "previous":
direction = -1
if len(args) == 0:
on_error(cls, "No inputs provided!")
n = len(args)
while not test_val(args.get("input_" + str(select), None)):
if n<0:
return (None,)
select = (select + direction) % 10
n = n - 1
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)
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, lambda n: n is not None, 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)
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, lambda n: n is not None, 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, _NOT_A_VALUE_S)
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, lambda n: (n is not None) and (n != _NOT_A_VALUE_S), 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)
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, lambda n: (n is not None), 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, _NOT_A_VALUE_F)
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, lambda n: (n is not None) and (n != _NOT_A_VALUE_F), 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, _NOT_A_VALUE_I)
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, lambda n: (n is not None) and (n != _NOT_A_VALUE_I), 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})
}
}
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})
}
}
def pick(self, boolean, on_true, on_false):
if boolean:
return (on_true,)
else:
return (on_false,)
-128
View File
@@ -1,128 +0,0 @@
from typing import List, Dict
from .shared import DreamImage
import random, time
class RGBPalette:
ID = "RGB_PALETTE"
def __init__(self, colors: List[tuple[int, int, int]] = None, image: DreamImage = None):
self._colors = []
def _fix_tuple(t):
if len(t) < 3:
return (t[0], t[0], t[0])
else:
return t
if image:
for p, _, _ in image:
self._colors.append(_fix_tuple(p))
if colors:
for c in colors:
self._colors.append(_fix_tuple(c))
def analyze(self):
total_red = 0
total_blue = 0
total_green = 0
for pixel in self:
total_red += pixel[0]
total_green += pixel[1]
total_blue += pixel[2]
n = len(self._colors)
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)
def __len__(self):
return len(self._colors)
def __iter__(self):
return iter(self._colors)
def random_iteration(self, seed=None):
s = seed if seed is not None else int(time.time() * 1000)
n = len(self._colors) - 1
c = self._colors
class _ColorIterator:
def __init__(self):
self._r = random.Random()
self._r.seed(s)
self._n = n
self._c = c
def __next__(self):
return self._c[self._r.randint(0, self._n)]
return _ColorIterator()
class FrameCounter:
ID = "FRAME_COUNTER"
def __init__(self, current_frame=0, total_frames=1, frames_per_second=25.0):
self.current_frame = max(0, current_frame)
self.total_frames = max(total_frames, 1)
self.frames_per_second = float(max(1.0, frames_per_second))
def incremented(self, amount: int):
return FrameCounter(self.current_frame + amount, self.total_frames, self.frames_per_second)
@property
def is_first_frame(self):
return self.current_frame == 0
@property
def is_final_frame(self):
return (self.current_frame + 1) == self.total_frames
@property
def is_after_last_frame(self):
return self.current_frame >= self.total_frames
@property
def current_time_in_seconds(self):
return float(self.current_frame) / self.frames_per_second
@property
def progress(self):
return float(self.current_frame) / (max(2, self.total_frames) - 1)
class AnimationSequence:
ID = "ANIMATION_SEQUENCE"
def __init__(self, frame_counter: FrameCounter, frames: Dict[int, List[str]] = None):
self.frames = frames
self.fps = frame_counter.frames_per_second
self.frame_counter = frame_counter
if self.is_defined:
self.keys_in_order = sorted(frames.keys())
self.num_batches = min(map(len, self.frames.values()))
else:
self.keys_in_order = []
self.num_batches = 0
@property
def batches(self):
return range(self.num_batches)
def get_image_files_of_batch(self, batch_num):
for key in self.keys_in_order:
yield self.frames[key][batch_num]
@property
def is_defined(self):
if self.frames:
return True
else:
return False
class SharedTypes:
frame_counter = {"frame_counter": (FrameCounter.ID, {"forceInput": True})}
sequence = {"sequence": (AnimationSequence.ID, {"forceInput": True})}
palette = {"palette": (RGBPalette.ID, {"forceInput": True})}
+1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
def run_uninstall():
pass
+218 -6
View File
@@ -1,6 +1,220 @@
from .shared import hashed_as_strings
from .categories import NodeCategories
# -*- 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, DreamStateFile
from .dreamtypes 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": ""}),
},
"optional": {
"label": ("STRING", {"default": ""}),
}
}
def convert(self, text, **values):
label = values.get("label", "")
if label:
return (LogEntry.new(label + ": " + text),)
else:
return (LogEntry.new(text),)
class DreamStringTokenizer:
NODE_NAME = "String Tokenizer"
ICON = "🪙"
OUTPUT_NODE = True
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("token",)
FUNCTION = "exec"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"default": "", "multiline": True}),
"separator": ("STRING", {"default": ","}),
"selected": ("INT", {"default": 0, "min": 0})
},
}
def exec(self, text: str, separator: str, selected: int):
if separator is None or separator == "":
separator = " "
parts = text.split(sep=separator)
return (parts[abs(selected) % len(parts)].strip(),)
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):
@@ -16,6 +230,8 @@ def _align_num(n: int, alignment: int, type: str):
class DreamFrameDimensions:
NODE_NAME = "Common Frame Dimensions"
ICON = "⌗"
@classmethod
def INPUT_TYPES(cls):
return {
@@ -34,10 +250,6 @@ class DreamFrameDimensions:
RETURN_NAMES = ("width", "height", "final_width", "final_height")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def result(self, size, aspect_ratio, orientation, divisor, alignment, alignment_type):
ratio = tuple(map(int, aspect_ratio.split(":")))
final_width = int(size)