7 Commits
26 changed files with 9803 additions and 6806 deletions
+5 -2
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from typing import Type
from .base import *
@@ -9,6 +10,7 @@ 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,
@@ -16,12 +18,13 @@ _NODE_CLASSES: List[Type] = [DreamSineWave, DreamLinear, DreamCSVCurve, DreamBea
DreamSimpleFrameCounter, DreamImageSequenceInputWithDefaultFallback,
DreamImageSequenceOutput, DreamCSVGenerator, DreamImageAreaSampler,
DreamVideoEncoder, DreamSequenceTweening, DreamSequenceBlend, DreamColorAlign,
DreamImageSampler, DreamNoiseFromAreaPalettes]
DreamImageSampler, DreamNoiseFromAreaPalettes, DreamVideoEncoderMpegCoder,
DreamInputString, DreamInputFloat, DreamInputInt, DreamInputText]
_SIGNATURE_SUFFIX = " [Dream]"
MANIFEST = {
"name": "Dream Project Animation",
"version": (2, 1, 0),
"version": (3, 0, 1),
"author": "Dream Project",
"project": "https://github.com/alt-key-project/comfyui-dream-project",
"description": "Various utility nodes for creating animations with ComfyUI",
+4 -2
View File
@@ -28,12 +28,14 @@ class DreamDirectoryFileCount:
def IS_CHANGED(cls, *v):
return ALWAYS_CHANGED_FLAG
def result(self, directory_path, patterns, indexing):
def result(self, directory_path, patterns):
if not os.path.isdir(directory_path):
return (0,)
total = 0
for pattern in patterns.split("|"):
total += len(glob.glob(pattern))
files = list(glob.glob(pattern, root_dir=directory_path))
total += len(files)
print("total "+str(total))
return (total,)
+2 -1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from .categories import NodeCategories
from .shared import *
from .types import *
@@ -127,7 +128,7 @@ class DreamColorAlign:
def INPUT_TYPES(cls):
return {
"required": SharedTypes.palette | {
"target_align": (RGBPalette.ID, {"forceInput": True}),
"target_align": (RGBPalette.ID, ),
"alignment_factor": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 10.0, "step": 0.1}),
}
}
+1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import csv
import math
+1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
def run_disable():
pass
+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)
+1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
def run_enable():
pass
+1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
def _get_node_name(cls):
return cls.__dict__.get("NODE_NAME", str(cls))
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

+1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import math
import numpy
+100
View File
@@ -0,0 +1,100 @@
from .categories import *
from .shared import *
class DreamInputText:
NODE_NAME = "Text Input"
ICON = "✍"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("STRING", {"default": "", "multiline": True}),
},
}
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("STRING",)
FUNCTION = "noop"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def noop(self, value):
return (value,)
class DreamInputString:
NODE_NAME = "String Input"
ICON = "✍"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("STRING", {"default": "", "multiline": False}),
},
}
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("STRING",)
FUNCTION = "noop"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def noop(self, value):
return (value,)
class DreamInputFloat:
NODE_NAME = "Float Input"
ICON = "✍"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("FLOAT", {"default": 0.0}),
},
}
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("FLOAT",)
FUNCTION = "noop"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def noop(self, value):
return (value,)
class DreamInputInt:
NODE_NAME = "Int Input"
ICON = "✍"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("INT", {"default": 0}),
},
}
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("INT",)
FUNCTION = "noop"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def noop(self, value):
return (value,)
+1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from .shared import DreamConfig
+1 -1
View File
@@ -1,4 +1,4 @@
from .types import SharedTypes, FrameCounter
# -*- coding: utf-8 -*-
from .categories import NodeCategories
from .shared import ALWAYS_CHANGED_FLAG, list_images_in_directory, DreamImage
from .types import SharedTypes, FrameCounter
+6 -1
View File
@@ -6,6 +6,7 @@
"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",
@@ -14,6 +15,7 @@
"Image Sequence Loader [Dream]": "Loads a frame from a directory of images",
"Image Sequence Saver [Dream]": "Saves a frame to a directory",
"Image Sequence Tweening [Dream]": "Post processing for animation sequences generating blended in-between frames",
"Int Input [Dream]": "Integer input (until primitive routing issues are solved)",
"Linear Curve [Dream]": "Linear interpolation between two value over the full animation",
"Noise from Area Palettes [Dream]": "Generates noise based on the colors of up to nine different palettes",
"Noise from Palette [Dream]": "Generates noise based on the colors in a palette",
@@ -21,5 +23,8 @@
"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"
"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"
}
+11 -10
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import math
from .categories import NodeCategories
@@ -68,15 +69,15 @@ class DreamNoiseFromAreaPalettes:
def INPUT_TYPES(cls):
return {
"optional": {
"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}),
"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}),
@@ -84,7 +85,7 @@ 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}),
},
}
+1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import json
import os
+52 -3
View File
@@ -32,8 +32,21 @@ 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.
@@ -67,6 +80,28 @@ Flags to add an icon before and/or after the node name.
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:
@@ -108,8 +143,11 @@ 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.
### Video Encoder (mpegCoder) [Dream]
Post processing for animation sequences using the python module mpegCoder with ffmpeg library to generate video files.
### File Count [Dream]
Finds the number of files in a directory matching specified patterns.
@@ -180,12 +218,18 @@ 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.
### 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
@@ -200,3 +244,8 @@ are:
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.
+2 -1
View File
@@ -2,4 +2,5 @@ imageio
pilgram
scipy
numpy<1.24>=1.18
torchvision
torchvision
mpegCoder
+72 -5
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import os
import shutil
import subprocess
@@ -8,7 +9,7 @@ from PIL import Image
from .categories import NodeCategories
from .err import on_error
from .shared import DreamConfig
from .shared import DreamConfig, MpegEncoderUtility
from .types import *
CONFIG = DreamConfig()
@@ -126,17 +127,82 @@ def _ffmpeg(config, filenames, fps, output):
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 | {
"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})
},
}
@@ -165,16 +231,17 @@ class DreamVideoEncoder:
filename = self._find_free_filename(filename, os.path.dirname(files[0]))
_ffmpeg(config, files, fps, filename)
def encode(self, sequence: AnimationSequence, filename: str, remove_images, framerate_factor):
def encode(self, sequence: AnimationSequence, name: str, remove_images, framerate_factor):
if not sequence.is_defined:
return ()
config = DreamConfig()
filename = _make_video_filename(name, config.get("ffmpeg.file_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 == "yes":
if remove_images:
for imagepath in images:
if os.path.isfile(imagepath):
os.unlink(imagepath)
+85 -3
View File
@@ -4,6 +4,7 @@ import hashlib
import json
import os
import random
import time
import folder_paths as comfy_paths
import glob
@@ -14,6 +15,8 @@ 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")
@@ -42,13 +45,23 @@ 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", "-i", "%FRAMES%", "-c:v", "libx265", "-pix_fmt",
"arguments": ["-r", "%FPS%", "-f", "concat", "-safe", "0", "-vsync",
"cfr", "-i", "%FRAMES%", "-c:v", "libx265", "-pix_fmt",
"yuv420p", "%OUTPUT%"]
},
"mpeg_coder": {
"encoding_threads": 4,
"bitrate_factor": 1.0,
"max_b_frame": 2,
"file_exension": "mp4",
"codec_name": "libx265"
},
"encoding": {
"jpeg_quality": 95
},
"debug": False,
"ui": {
"top_category": "Dream",
"prepend_icon_to_category": True,
@@ -109,6 +122,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]
@@ -153,20 +171,26 @@ 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):
def __init__(self, tensor_image=None, pil_image=None, file_path=None, with_alpha=False):
if pil_image is not None:
self.pil_image = pil_image
elif tensor_image is not None:
self.pil_image = convertTensorImageToPIL(tensor_image)
else:
self.pil_image = Image.open(file_path)
if self.pil_image.mode not in ("RGB", "RGBA"):
if with_alpha and self.pil_image.mode != "RGBA":
self.pil_image = self.pil_image.convert("RGBA")
else:
if self.pil_image.mode not in ("RGB", "RGBA"):
self.pil_image = self.pil_image.convert("RGB")
self.width = self.pil_image.width
self.height = self.pil_image.height
self.size = self.pil_image.size
self._draw = ImageDraw(self.pil_image)
def numpy_array(self):
return numpy.array(self.pil_image)
def _renew(self, pil_image):
self.pil_image = pil_image
self._draw = ImageDraw(self.pil_image)
@@ -190,6 +214,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)
@@ -232,6 +261,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):
@@ -354,3 +387,52 @@ def hashed_as_strings(*items):
m = hashlib.sha256()
m.update(tokens.encode(encoding="utf-8"))
return m.digest().hex()
class MpegEncoderUtility:
def __init__(self, video_path: str, bit_rate_factor: float, width: int, height: int, files: List[str],
fps: float, encoding_threads: int, codec_name, max_b_frame):
import mpegCoder
self._files = files
self._logger = get_logger()
self._enc = mpegCoder.MpegEncoder()
bit_rate = self._calculate_bit_rate(width, height, fps, bit_rate_factor)
self._logger.info("Bitrate "+str(bit_rate))
self._enc.setParameter(
videoPath=video_path, codecName=codec_name,
nthread=encoding_threads, bitRate=bit_rate, width=width, height=height, widthSrc=width,
heightSrc=height,
GOPSize=len(files), maxBframe=max_b_frame, frameRate=self._fps_to_tuple(fps))
def _calculate_bit_rate(self, width: int, height: int, fps: float, bit_rate_factor: float):
bits_per_pixel_base = 0.075
return round(max(10, float(width * height * fps * bits_per_pixel_base * bit_rate_factor * 0.001)))
def encode(self):
if not self._enc.FFmpegSetup():
raise Exception("Failed to setup MPEG Encoder - check parameters!")
try:
t = time.time()
for filepath in self._files:
self._logger.debug("Encoding frame {}", filepath)
image = DreamImage.from_file(filepath).convert("RGB")
self._enc.EncodeFrame(image.numpy_array())
self._enc.FFmpegClose()
self._logger.info("Completed video encoding of {n} frames in {t} seconds", n=len(self._files),
t=round(time.time() - t))
finally:
self._enc.clear()
def _fps_to_tuple(self, fps: float):
def _is_almost_int(f: float):
return abs(f - int(f)) < 0.001
a = fps
b = 1
while not _is_almost_int(a) and b < 100:
a /= 10
b *= 10
a = round(a)
b = round(b)
self._logger.info("Video specified as {fps} fps - encoder framerate {a}/{b}", fps=fps, a=a, b=b)
return (a, b)
+4 -3
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import random
import time
@@ -126,6 +127,6 @@ class AnimationSequence:
class SharedTypes:
frame_counter = {"frame_counter": (FrameCounter.ID, {"forceInput": True})}
sequence = {"sequence": (AnimationSequence.ID, {"forceInput": True})}
palette = {"palette": (RGBPalette.ID, {"forceInput": True})}
frame_counter = {"frame_counter": (FrameCounter.ID,)}
sequence = {"sequence": (AnimationSequence.ID,)}
palette = {"palette": (RGBPalette.ID,)}
+1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
def run_uninstall():
pass
+3
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import math
from .categories import NodeCategories
@@ -17,6 +18,7 @@ def _align_num(n: int, alignment: int, type: str):
class DreamFrameDimensions:
NODE_NAME = "Common Frame Dimensions"
ICON = "⌗"
@classmethod
def INPUT_TYPES(cls):
@@ -50,3 +52,4 @@ class DreamFrameDimensions:
return (width, height, final_width, final_height)
else:
return (height, width, final_height, final_width)