Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
240176df63 |
+4
-41
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Type
|
||||
|
||||
from .base import *
|
||||
@@ -10,7 +9,6 @@ from .noise import *
|
||||
from .output import *
|
||||
from .seq_processing import *
|
||||
from .utility import *
|
||||
from .inputfields import *
|
||||
|
||||
_NODE_CLASSES: List[Type] = [DreamSineWave, DreamLinear, DreamCSVCurve, DreamBeatCurve, DreamFrameDimensions,
|
||||
DreamImageMotion, DreamNoiseFromPalette, DreamAnalyzePalette, DreamColorShift,
|
||||
@@ -18,13 +16,12 @@ _NODE_CLASSES: List[Type] = [DreamSineWave, DreamLinear, DreamCSVCurve, DreamBea
|
||||
DreamSimpleFrameCounter, DreamImageSequenceInputWithDefaultFallback,
|
||||
DreamImageSequenceOutput, DreamCSVGenerator, DreamImageAreaSampler,
|
||||
DreamVideoEncoder, DreamSequenceTweening, DreamSequenceBlend, DreamColorAlign,
|
||||
DreamImageSampler, DreamNoiseFromAreaPalettes, DreamVideoEncoderMpegCoder,
|
||||
DreamInputString, DreamInputFloat, DreamInputInt, DreamInputText]
|
||||
DreamImageSampler, DreamNoiseFromAreaPalettes]
|
||||
_SIGNATURE_SUFFIX = " [Dream]"
|
||||
|
||||
MANIFEST = {
|
||||
"name": "Dream Project Animation",
|
||||
"version": (3, 0, 1),
|
||||
"version": (2, 0, 0),
|
||||
"author": "Dream Project",
|
||||
"project": "https://github.com/alt-key-project/comfyui-dream-project",
|
||||
"description": "Various utility nodes for creating animations with ComfyUI",
|
||||
@@ -34,47 +31,13 @@ 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
|
||||
NODE_DISPLAY_NAME_MAPPINGS[node_name] = update_display_name(cls, category_icon,
|
||||
cls.__dict__.get("DISPLAY_NAME",
|
||||
cls.__dict__["NODE_NAME"]))
|
||||
display_name = cls.__dict__.get("DISPLAY_NAME", cls.__dict__["NODE_NAME"]) + _SIGNATURE_SUFFIX
|
||||
NODE_DISPLAY_NAME_MAPPINGS[node_name] = display_name
|
||||
else:
|
||||
raise Exception("Class {} is missing NODE_NAME!".format(str(cls)))
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import glob
|
||||
|
||||
from .categories import NodeCategories
|
||||
@@ -8,7 +7,6 @@ from .types import *
|
||||
|
||||
class DreamDirectoryFileCount:
|
||||
NODE_NAME = "File Count"
|
||||
ICON = "📂"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -28,22 +26,18 @@ class DreamDirectoryFileCount:
|
||||
def IS_CHANGED(cls, *v):
|
||||
return ALWAYS_CHANGED_FLAG
|
||||
|
||||
def result(self, directory_path, patterns):
|
||||
def result(self, directory_path, patterns, indexing):
|
||||
if not os.path.isdir(directory_path):
|
||||
return (0,)
|
||||
total = 0
|
||||
for pattern in patterns.split("|"):
|
||||
files = list(glob.glob(pattern, root_dir=directory_path))
|
||||
total += len(files)
|
||||
print("total "+str(total))
|
||||
total += len(glob.glob(pattern))
|
||||
return (total,)
|
||||
|
||||
|
||||
class DreamFrameCounterOffset:
|
||||
NODE_NAME = "Frame Counter Offset"
|
||||
|
||||
ICON = "±"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -67,7 +61,6 @@ class DreamFrameCounterOffset:
|
||||
|
||||
class DreamSimpleFrameCounter:
|
||||
NODE_NAME = "Frame Counter (Simple)"
|
||||
ICON = "⚋"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -95,7 +88,6 @@ class DreamSimpleFrameCounter:
|
||||
|
||||
class DreamDirectoryBackedFrameCounter:
|
||||
NODE_NAME = "Frame Counter (Directory)"
|
||||
ICON = "⚋"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
+1
-3
@@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
class NodeCategories:
|
||||
ANIMATION = "animation"
|
||||
ANIMATION_POSTPROCESSING = ANIMATION + "/postprocessing"
|
||||
@@ -10,4 +8,4 @@ class NodeCategories:
|
||||
IMAGE_COLORS = "image/color"
|
||||
IMAGE_GENERATE = "image/generate"
|
||||
IMAGE = "image"
|
||||
UTILS = "utils"
|
||||
UTILS = "utils"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from .categories import NodeCategories
|
||||
from .shared import *
|
||||
from .types import *
|
||||
@@ -128,7 +127,7 @@ class DreamColorAlign:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": SharedTypes.palette | {
|
||||
"target_align": (RGBPalette.ID, ),
|
||||
"target_align": (RGBPalette.ID, {"forceInput": True}),
|
||||
"alignment_factor": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 10.0, "step": 0.1}),
|
||||
}
|
||||
}
|
||||
@@ -216,7 +215,6 @@ class DreamColorShift:
|
||||
|
||||
class DreamAnalyzePalette:
|
||||
NODE_NAME = "Analyze Palette"
|
||||
NODE = "📊"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import csv
|
||||
import math
|
||||
|
||||
@@ -136,7 +135,6 @@ def _is_as_float(s: str):
|
||||
|
||||
class DreamCSVGenerator:
|
||||
NODE_NAME = "CSV Generator"
|
||||
ICON = "⌗"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
def run_disable():
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
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)
|
||||
@@ -1,9 +0,0 @@
|
||||
# -*- 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
+2977
-3547
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import math
|
||||
|
||||
import numpy
|
||||
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
from .categories import *
|
||||
from .shared import *
|
||||
|
||||
class DreamInputText:
|
||||
NODE_NAME = "Text Input"
|
||||
ICON = "✍"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"value": ("STRING", {"default": "", "multiline": True}),
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = NodeCategories.UTILS
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("STRING",)
|
||||
FUNCTION = "noop"
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, *values):
|
||||
return hashed_as_strings(*values)
|
||||
|
||||
def noop(self, value):
|
||||
return (value,)
|
||||
|
||||
class DreamInputString:
|
||||
NODE_NAME = "String Input"
|
||||
ICON = "✍"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"value": ("STRING", {"default": "", "multiline": False}),
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = NodeCategories.UTILS
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("STRING",)
|
||||
FUNCTION = "noop"
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, *values):
|
||||
return hashed_as_strings(*values)
|
||||
|
||||
def noop(self, value):
|
||||
return (value,)
|
||||
|
||||
|
||||
class DreamInputFloat:
|
||||
NODE_NAME = "Float Input"
|
||||
ICON = "✍"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"value": ("FLOAT", {"default": 0.0}),
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = NodeCategories.UTILS
|
||||
RETURN_TYPES = ("FLOAT",)
|
||||
RETURN_NAMES = ("FLOAT",)
|
||||
FUNCTION = "noop"
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, *values):
|
||||
return hashed_as_strings(*values)
|
||||
|
||||
def noop(self, value):
|
||||
return (value,)
|
||||
|
||||
|
||||
class DreamInputInt:
|
||||
NODE_NAME = "Int Input"
|
||||
ICON = "✍"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"value": ("INT", {"default": 0}),
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = NodeCategories.UTILS
|
||||
RETURN_TYPES = ("INT",)
|
||||
RETURN_NAMES = ("INT",)
|
||||
FUNCTION = "noop"
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, *values):
|
||||
return hashed_as_strings(*values)
|
||||
|
||||
def noop(self, value):
|
||||
return (value,)
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from .shared import DreamConfig
|
||||
|
||||
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
+1
-2
@@ -1,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from .types import SharedTypes, FrameCounter
|
||||
from .categories import NodeCategories
|
||||
from .shared import ALWAYS_CHANGED_FLAG, list_images_in_directory, DreamImage
|
||||
from .types import SharedTypes, FrameCounter
|
||||
@@ -6,7 +6,6 @@ from .types import SharedTypes, FrameCounter
|
||||
|
||||
class DreamImageSequenceInputWithDefaultFallback:
|
||||
NODE_NAME = "Image Sequence Loader"
|
||||
ICON = "💾"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
+2
-7
@@ -6,7 +6,6 @@
|
||||
"Common Frame Dimensions [Dream]": "Utility for calculating good width/height based on common video dimensions",
|
||||
"FFMPEG Video Encoder [Dream]": "Post processing for animation sequences calling FFMPEG to generate video file",
|
||||
"File Count [Dream]": "Finds the number of files in a directory matching specified patterns",
|
||||
"Float Input [Dream]": "Float input (until primitive routing issues are solved)",
|
||||
"Frame Counter (Directory) [Dream]": "Directory backed frame counter, for output directories",
|
||||
"Frame Counter (Simple) [Dream]": "Integer value used as frame counter",
|
||||
"Frame Counter Offset [Dream]": "Adds an offset to a frame counter",
|
||||
@@ -15,16 +14,12 @@
|
||||
"Image Sequence Loader [Dream]": "Loads a frame from a directory of images",
|
||||
"Image Sequence Saver [Dream]": "Saves a frame to a directory",
|
||||
"Image Sequence Tweening [Dream]": "Post processing for animation sequences generating blended in-between frames",
|
||||
"Int Input [Dream]": "Integer input (until primitive routing issues are solved)",
|
||||
"Linear Curve [Dream]": "Linear interpolation between two value over the full animation",
|
||||
"Noise from Area Palettes [Dream]": "Generates noise based on the colors of up to nine different palettes",
|
||||
"Noise from Area Palettes [Dream]": "",
|
||||
"Noise from Palette [Dream]": "Generates noise based on the colors in a palette",
|
||||
"Palette Color Align [Dream]": "Shifts the colors of one palette towards another target palette",
|
||||
"Palette Color Shift [Dream]": "Multiplies the color values in a palette",
|
||||
"Sample Image Area as Palette [Dream]": "Samples a palette from an image based on pre-defined areas",
|
||||
"Sample Image as Palette [Dream]": "Randomly samples pixel values to build a palette from an image",
|
||||
"Sine Curve [Dream]": "Simple sine wave curve",
|
||||
"String Input [Dream]": "String input (until primitive routing issues are solved)",
|
||||
"Text Input [Dream]": "Multiline string input (until primitive routing issues are solved)",
|
||||
"Video Encoder (mpegCoder) [Dream]": "Post processing for animation sequences using mpegCoder module to generate video file"
|
||||
"Sine Curve [Dream]": "Simple sine wave curve"
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import math
|
||||
|
||||
from .categories import NodeCategories
|
||||
@@ -25,7 +24,6 @@ def _generate_noise(image: DreamImage, color_function, rng: random.Random, block
|
||||
|
||||
class DreamNoiseFromPalette:
|
||||
NODE_NAME = "Noise from Palette"
|
||||
ICON = "🌫"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -69,15 +67,15 @@ class DreamNoiseFromAreaPalettes:
|
||||
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,),
|
||||
"top_left_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"top_center_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"top_right_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"center_left_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"center_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"center_right_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"bottom_left_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"bottom_center_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"bottom_right_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
},
|
||||
"required": {
|
||||
"area_sharpness": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05}),
|
||||
@@ -85,12 +83,11 @@ class DreamNoiseFromAreaPalettes:
|
||||
"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}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff})
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = NodeCategories.IMAGE_GENERATE
|
||||
ICON = "🌫"
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "result"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import os
|
||||
|
||||
@@ -32,7 +31,6 @@ def _save_jpg(pil_image, filepath, quality):
|
||||
|
||||
class DreamImageSequenceOutput:
|
||||
NODE_NAME = "Image Sequence Saver"
|
||||
ICON = "💾"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
@@ -9,12 +9,6 @@ I have demonstrated the use of these custom nodes in this [youtube video](https:
|
||||
|
||||
## Installation
|
||||
|
||||
### Simple option
|
||||
|
||||
You can install Dream Project Animation Nodes using the ComfyUI Manager.
|
||||
|
||||
### Manual option
|
||||
|
||||
Run within (ComfyUI)/custom_nodes/ folder:
|
||||
|
||||
* git clone https://github.com/alt-key-project/comfyui-dream-project.git
|
||||
@@ -32,76 +26,6 @@ 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:
|
||||
@@ -143,11 +67,8 @@ Recreates file at frame 0 (removing and existing content in the file).
|
||||
### Common Frame Dimensions [Dream]
|
||||
Utility for calculating good width/height based on common video dimensions.
|
||||
|
||||
### Video Encoder (FFMPEG) [Dream]
|
||||
Post processing for animation sequences calling FFMPEG to generate video files.
|
||||
|
||||
### Video Encoder (mpegCoder) [Dream]
|
||||
Post processing for animation sequences using the python module mpegCoder with ffmpeg library to generate video files.
|
||||
### 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.
|
||||
@@ -218,24 +139,12 @@ 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)
|
||||
|
||||
## Known issues
|
||||
|
||||
### FFMPEG
|
||||
|
||||
The call to FFMPEG currently in the default configuration (in config.json) does not seem to work for everyone. The good
|
||||
news is that you can change the arguments to whatever works for you - the node-supplied parameters (that probably all need to be in the call)
|
||||
news is that you can change the arguments to whatever works for you - the fixed parameters (that need to be in the call)
|
||||
are:
|
||||
|
||||
* -i %FRAMES% (the input file listing frames)
|
||||
@@ -243,9 +152,4 @@ 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!
|
||||
|
||||
### 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.
|
||||
know what arguments are causing issues for you!
|
||||
+1
-2
@@ -2,5 +2,4 @@ imageio
|
||||
pilgram
|
||||
scipy
|
||||
numpy<1.24>=1.18
|
||||
torchvision
|
||||
mpegCoder
|
||||
torchvision
|
||||
+9
-76
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -8,8 +8,7 @@ from functools import lru_cache
|
||||
from PIL import Image
|
||||
|
||||
from .categories import NodeCategories
|
||||
from .err import on_error
|
||||
from .shared import DreamConfig, MpegEncoderUtility
|
||||
from .shared import DreamConfig, DreamImage
|
||||
from .types import *
|
||||
|
||||
CONFIG = DreamConfig()
|
||||
@@ -122,87 +121,21 @@ def _ffmpeg(config, filenames, fps, output):
|
||||
for (key, value) in replacements.items():
|
||||
cmd = list(map(lambda s: s.replace(key, value), cmd))
|
||||
|
||||
subprocess.check_output(cmd, shell=True)
|
||||
subprocess.run(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 = ()
|
||||
RETURN_NAMES = ()
|
||||
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 ()
|
||||
config = DreamConfig()
|
||||
filename = _make_video_filename(name, config.get("mpeg_coder.file_exension"))
|
||||
for batch_num in sequence.batches:
|
||||
try:
|
||||
images = list(sequence.get_image_files_of_batch(batch_num))
|
||||
filename = self._find_free_filename(filename, os.path.dirname(images[0]))
|
||||
first_image = DreamImage.from_file(images[0])
|
||||
enc = MpegEncoderUtility(video_path=filename,
|
||||
bit_rate_factor=float(config.get("mpeg_coder.bitrate_factor", 1.0)),
|
||||
encoding_threads=int(config.get("mpeg_coder.encoding_threads", 4)),
|
||||
max_b_frame=int(config.get("mpeg_coder.max_b_frame", 2)),
|
||||
width=first_image.width,
|
||||
height=first_image.height,
|
||||
files=images,
|
||||
fps=sequence.fps * framerate_factor,
|
||||
codec_name=config.get("mpeg_coder.codec_name", "libx265"))
|
||||
enc.encode()
|
||||
if remove_images:
|
||||
for imagepath in images:
|
||||
if os.path.isfile(imagepath):
|
||||
os.unlink(imagepath)
|
||||
except Exception as e:
|
||||
on_error(self.__class__, str(e))
|
||||
return ()
|
||||
|
||||
|
||||
class DreamVideoEncoder:
|
||||
NODE_NAME = "FFMPEG Video Encoder"
|
||||
DISPLAY_NAME = "Video Encoder (FFMPEG)"
|
||||
ICON = "🎬"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": SharedTypes.sequence | {
|
||||
"name": ("STRING", {"default": 'video', "multiline": False}),
|
||||
"filename": ("STRING", {"default": 'video.mp4', "multiline": False}),
|
||||
"framerate_factor": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100.0}),
|
||||
"remove_images": ("BOOLEAN", {"default": True})
|
||||
"remove_images": (["yes", "no"],)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -231,22 +164,22 @@ class DreamVideoEncoder:
|
||||
filename = self._find_free_filename(filename, os.path.dirname(files[0]))
|
||||
_ffmpeg(config, files, fps, filename)
|
||||
|
||||
def encode(self, sequence: AnimationSequence, name: str, remove_images, framerate_factor):
|
||||
def encode(self, sequence: AnimationSequence, filename: str, remove_images, framerate_factor):
|
||||
if not sequence.is_defined:
|
||||
return ()
|
||||
|
||||
config = DreamConfig()
|
||||
filename = _make_video_filename(name, config.get("ffmpeg.file_exension"))
|
||||
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:
|
||||
if remove_images == "yes":
|
||||
for imagepath in images:
|
||||
if os.path.isfile(imagepath):
|
||||
os.unlink(imagepath)
|
||||
except Exception as e:
|
||||
on_error(self.__class__, str(e))
|
||||
print("Failed to encode files in dir {}!".format(os.path.dirname(images[0])))
|
||||
print(str(e))
|
||||
return ()
|
||||
|
||||
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import glob
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
|
||||
import folder_paths as comfy_paths
|
||||
import glob
|
||||
import numpy
|
||||
import random
|
||||
import torch
|
||||
from PIL import Image, ImageFilter
|
||||
from PIL.ImageDraw import ImageDraw
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
from typing import Dict, Tuple, List
|
||||
|
||||
from .dreamlogger import DreamLog
|
||||
|
||||
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")
|
||||
@@ -38,78 +33,25 @@ 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": {
|
||||
"file_exension": "mp4",
|
||||
"path": "ffmpeg",
|
||||
"arguments": ["-r", "%FPS%", "-f", "concat", "-safe", "0", "-vsync",
|
||||
"cfr", "-i", "%FRAMES%", "-c:v", "libx265", "-pix_fmt",
|
||||
"arguments": ["-r", "%FPS%", "-f", "concat", "-safe", "0", "-i", "%FRAMES%", "-c:v", "libx265", "-pix_fmt",
|
||||
"yuv420p", "%OUTPUT%"]
|
||||
},
|
||||
"mpeg_coder": {
|
||||
"encoding_threads": 4,
|
||||
"bitrate_factor": 1.0,
|
||||
"max_b_frame": 2,
|
||||
"file_exension": "mp4",
|
||||
"codec_name": "libx265"
|
||||
},
|
||||
"encoding": {
|
||||
"jpeg_quality": 95
|
||||
},
|
||||
"debug": False,
|
||||
"ui": {
|
||||
"top_category": "Dream",
|
||||
"prepend_icon_to_category": True,
|
||||
"append_icon_to_category": False,
|
||||
"prepend_icon_to_node": True,
|
||||
"append_icon_to_node": False,
|
||||
"category_icons": {
|
||||
"animation": "🎥",
|
||||
"postprocessing": "⚙",
|
||||
"transforms": "🔀",
|
||||
"curves": "📈",
|
||||
"color": "🎨",
|
||||
"generate": "⚡",
|
||||
"utils": "🛠",
|
||||
"image": "🌄",
|
||||
"Dream": "✨"
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
global _config_data
|
||||
if not os.path.isfile(DreamConfig.FILEPATH):
|
||||
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
|
||||
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)
|
||||
|
||||
def get(self, key: str, default=None):
|
||||
key = key.split(".")
|
||||
@@ -122,11 +64,6 @@ 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]
|
||||
@@ -171,26 +108,20 @@ class DreamImage:
|
||||
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, with_alpha=False):
|
||||
def __init__(self, tensor_image=None, pil_image=None, file_path=None):
|
||||
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 with_alpha and self.pil_image.mode != "RGBA":
|
||||
if self.pil_image.mode not in ("RGB", "RGBA"):
|
||||
self.pil_image = self.pil_image.convert("RGBA")
|
||||
else:
|
||||
if self.pil_image.mode not in ("RGB", "RGBA"):
|
||||
self.pil_image = self.pil_image.convert("RGB")
|
||||
self.width = self.pil_image.width
|
||||
self.height = self.pil_image.height
|
||||
self.size = self.pil_image.size
|
||||
self._draw = ImageDraw(self.pil_image)
|
||||
|
||||
def numpy_array(self):
|
||||
return numpy.array(self.pil_image)
|
||||
|
||||
def _renew(self, pil_image):
|
||||
self.pil_image = pil_image
|
||||
self._draw = ImageDraw(self.pil_image)
|
||||
@@ -214,11 +145,6 @@ 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)
|
||||
|
||||
@@ -261,10 +187,6 @@ 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):
|
||||
@@ -387,52 +309,3 @@ def hashed_as_strings(*items):
|
||||
m = hashlib.sha256()
|
||||
m.update(tokens.encode(encoding="utf-8"))
|
||||
return m.digest().hex()
|
||||
|
||||
|
||||
class MpegEncoderUtility:
|
||||
def __init__(self, video_path: str, bit_rate_factor: float, width: int, height: int, files: List[str],
|
||||
fps: float, encoding_threads: int, codec_name, max_b_frame):
|
||||
import mpegCoder
|
||||
self._files = files
|
||||
self._logger = get_logger()
|
||||
self._enc = mpegCoder.MpegEncoder()
|
||||
bit_rate = self._calculate_bit_rate(width, height, fps, bit_rate_factor)
|
||||
self._logger.info("Bitrate "+str(bit_rate))
|
||||
self._enc.setParameter(
|
||||
videoPath=video_path, codecName=codec_name,
|
||||
nthread=encoding_threads, bitRate=bit_rate, width=width, height=height, widthSrc=width,
|
||||
heightSrc=height,
|
||||
GOPSize=len(files), maxBframe=max_b_frame, frameRate=self._fps_to_tuple(fps))
|
||||
|
||||
def _calculate_bit_rate(self, width: int, height: int, fps: float, bit_rate_factor: float):
|
||||
bits_per_pixel_base = 0.075
|
||||
return round(max(10, float(width * height * fps * bits_per_pixel_base * bit_rate_factor * 0.001)))
|
||||
|
||||
def encode(self):
|
||||
if not self._enc.FFmpegSetup():
|
||||
raise Exception("Failed to setup MPEG Encoder - check parameters!")
|
||||
try:
|
||||
t = time.time()
|
||||
|
||||
for filepath in self._files:
|
||||
self._logger.debug("Encoding frame {}", filepath)
|
||||
image = DreamImage.from_file(filepath).convert("RGB")
|
||||
self._enc.EncodeFrame(image.numpy_array())
|
||||
self._enc.FFmpegClose()
|
||||
self._logger.info("Completed video encoding of {n} frames in {t} seconds", n=len(self._files),
|
||||
t=round(time.time() - t))
|
||||
finally:
|
||||
self._enc.clear()
|
||||
|
||||
def _fps_to_tuple(self, fps: float):
|
||||
def _is_almost_int(f: float):
|
||||
return abs(f - int(f)) < 0.001
|
||||
a = fps
|
||||
b = 1
|
||||
while not _is_almost_int(a) and b < 100:
|
||||
a /= 10
|
||||
b *= 10
|
||||
a = round(a)
|
||||
b = round(b)
|
||||
self._logger.info("Video specified as {fps} fps - encoder framerate {a}/{b}", fps=fps, a=a, b=b)
|
||||
return (a, b)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import random
|
||||
import time
|
||||
|
||||
@@ -127,6 +126,6 @@ class AnimationSequence:
|
||||
|
||||
|
||||
class SharedTypes:
|
||||
frame_counter = {"frame_counter": (FrameCounter.ID,)}
|
||||
sequence = {"sequence": (AnimationSequence.ID,)}
|
||||
palette = {"palette": (RGBPalette.ID,)}
|
||||
frame_counter = {"frame_counter": (FrameCounter.ID, {"forceInput": True})}
|
||||
sequence = {"sequence": (AnimationSequence.ID, {"forceInput": True})}
|
||||
palette = {"palette": (RGBPalette.ID, {"forceInput": True})}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
def run_uninstall():
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import math
|
||||
|
||||
from .categories import NodeCategories
|
||||
@@ -18,7 +17,6 @@ def _align_num(n: int, alignment: int, type: str):
|
||||
|
||||
class DreamFrameDimensions:
|
||||
NODE_NAME = "Common Frame Dimensions"
|
||||
ICON = "⌗"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -52,4 +50,3 @@ class DreamFrameDimensions:
|
||||
return (width, height, final_width, final_height)
|
||||
else:
|
||||
return (height, width, final_height, final_width)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user