6 Commits
11 changed files with 7641 additions and 31 deletions
+5 -2
View File
@@ -14,6 +14,7 @@ from .seq_processing import *
from .switches import *
from .utility import *
from .calculate import *
from .laboratory import *
_NODE_CLASSES: List[Type] = [DreamSineWave, DreamLinear, DreamCSVCurve, DreamBeatCurve, DreamFrameDimensions,
DreamImageMotion, DreamNoiseFromPalette, DreamAnalyzePalette, DreamColorShift,
@@ -27,12 +28,14 @@ _NODE_CLASSES: List[Type] = [DreamSineWave, DreamLinear, DreamCSVCurve, DreamBea
DreamBigIntSwitch, DreamBigPaletteSwitch, DreamWeightedPromptBuilder, DreamPromptFinalizer,
DreamFrameCounterInfo, DreamBoolToFloat, DreamBoolToInt, DreamSawWave, DreamTriangleWave,
DreamTriangleEvent, DreamSmoothEvent, DreamCalculation, DreamImageColorShift,
DreamComparePalette, DreamImageContrast, DreamImageBrightness]
DreamComparePalette, DreamImageContrast, DreamImageBrightness, DreamLogFile,
DreamLaboratory, DreamStringToLog, DreamIntToLog, DreamFloatToLog, DreamJoinLog,
DreamStringTokenizer]
_SIGNATURE_SUFFIX = " [Dream]"
MANIFEST = {
"name": "Dream Project Animation",
"version": (3, 2, 0),
"version": (4, 2, 0),
"author": "Dream Project",
"project": "https://github.com/alt-key-project/comfyui-dream-project",
"description": "Various utility nodes for creating animations with ComfyUI",
+2 -2
View File
@@ -3,7 +3,7 @@ EMBEDDED_CONFIGURATION = {
"file_extension": "mp4",
"path": "ffmpeg",
"arguments": ["-r", "%FPS%", "-f", "concat", "-safe", "0", "-vsync",
"cfr", "-i", "%FRAMES%", "-c:v", "libx265", "-pix_fmt",
"cfr", "-i", "%FRAMES%", "-c:v", "libx264", "-pix_fmt",
"yuv420p", "%OUTPUT%"]
},
"mpeg_coder": {
@@ -11,7 +11,7 @@ EMBEDDED_CONFIGURATION = {
"bitrate_factor": 1.0,
"max_b_frame": 2,
"file_extension": "mp4",
"codec_name": "libx265"
"codec_name": "libx264"
},
"encoding": {
"jpeg_quality": 95
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
import json
from .categories import *
from .shared import ALWAYS_CHANGED_FLAG, DreamStateFile
from .types import *
_laboratory_state = DreamStateFile("laboratory")
class DreamLaboratory:
NODE_NAME = "Laboratory"
ICON = "🧪"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.frame_counter | {
"key": ("STRING", {"default": "Random value " + str(random.randint(0, 1000000))}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"renew_policy": (["every frame", "first frame"],),
"min_value": ("FLOAT", {"default": 0.0}),
"max_value": ("FLOAT", {"default": 1.0}),
"mode": (["random uniform", "random bell", "ladder", "random walk"],),
},
"optional": {
"step_size": ("FLOAT", {"default": 0.1}),
},
}
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = ("FLOAT", "INT", LogEntry.ID)
RETURN_NAMES = ("FLOAT", "INT", "log_entry")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def _generate(self, seed, last_value, min_value, max_value, mode, step_size):
rnd = random.Random()
rnd.seed(seed)
def jsonify(v: float):
return json.loads(json.dumps(v))
if mode == "random uniform":
return jsonify(self._mode_uniform(rnd, last_value, min_value, max_value, step_size))
elif mode == "random bell":
return jsonify(self._mode_bell(rnd, last_value, min_value, max_value, step_size))
elif mode == "ladder":
return jsonify(self._mode_ladder(rnd, last_value, min_value, max_value, step_size))
else:
return jsonify(self._mode_walk(rnd, last_value, min_value, max_value, step_size))
def _mode_uniform(self, rnd: random.Random, last_value: float, min_value: float, max_value: float, step_size):
return rnd.random() * (max_value - min_value) + min_value
def _mode_bell(self, rnd: random.Random, last_value: float, min_value: float, max_value: float, step_size):
s = 0.0
for i in range(3):
s += rnd.random() * (max_value - min_value) + min_value
return s / 3.0
def _mode_ladder(self, rnd: random.Random, last_value: float, min_value: float, max_value: float, step_size):
if last_value is None:
last_value = min_value - step_size
next_value = last_value + step_size
if next_value > max_value:
d = abs(max_value - min_value)
next_value = (next_value - min_value) % d + min_value
return next_value
def _mode_walk(self, rnd: random.Random, last_value: float, min_value: float, max_value: float, step_size):
if last_value is None:
last_value = (max_value - min_value) * 0.5
if rnd.random() >= 0.5:
return min(max_value, last_value + step_size)
else:
return max(min_value, last_value - step_size)
def result(self, key, frame_counter: FrameCounter, seed, renew_policy, min_value, max_value, mode, **values):
if min_value > max_value:
t = max_value
max_value = min_value
min_value = t
step_size = values.get("step_size", abs(max_value - min_value) * 0.1)
last_value = _laboratory_state.get_section("values").get(key, None)
if (last_value is None) or (renew_policy == "every frame") or frame_counter.is_first_frame:
v = _laboratory_state.get_section("values") \
.update(key, 0, lambda old: self._generate(seed, last_value, min_value, max_value, mode, step_size))
return v, round(v), LogEntry.new(
"Laboratory generated new value for '{}': {} ({})".format(key, v, round(v)))
else:
return last_value, round(last_value), LogEntry.new("Laboratory reused value for '{}': {} ({})"
.format(key, last_value, round(last_value)))
+7
View File
@@ -19,6 +19,7 @@
"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",
@@ -33,7 +34,11 @@
"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",
@@ -44,6 +49,8 @@
"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",
+21 -10
View File
@@ -8,7 +8,7 @@ from PIL.PngImagePlugin import PngInfo
from .categories import NodeCategories
from .shared import hashed_as_strings, DreamImageProcessor, DreamImage, \
list_images_in_directory, DreamConfig
from .types import SharedTypes, FrameCounter, AnimationSequence
from .types import SharedTypes, FrameCounter, AnimationSequence, LogEntry
CONFIG = DreamConfig()
@@ -42,7 +42,7 @@ class DreamImageSequenceOutput:
"directory_path": ("STRING", {"default": comfy_paths.output_directory, "multiline": False}),
"prefix": ("STRING", {"default": 'frame', "multiline": False}),
"digits": ("INT", {"default": 5}),
"at_end": (["stop output", "keep going"],),
"at_end": (["stop output", "raise error", "keep going"],),
"filetype": (['png with embedded workflow', "png", 'jpg'],),
},
"hidden": {
@@ -52,9 +52,9 @@ class DreamImageSequenceOutput:
}
CATEGORY = NodeCategories.IMAGE_ANIMATION
RETURN_TYPES = (AnimationSequence.ID,)
RETURN_TYPES = (AnimationSequence.ID, LogEntry.ID)
OUTPUT_NODE = True
RETURN_NAMES = ("sequence",)
RETURN_NAMES = ("sequence", "log_entry")
FUNCTION = "save"
@classmethod
@@ -64,11 +64,16 @@ class DreamImageSequenceOutput:
def _get_new_filename(self, current_frame, prefix, digits, filetype):
return prefix + "_" + str(current_frame).zfill(digits) + "." + filetype.split(" ")[0]
def _save_single_image(self, dream_image: DreamImage, batch_counter, frame_counter: FrameCounter, directory_path,
prefix, digits, filetype, prompt, extra_pnginfo, at_end):
def _save_single_image(self, dream_image: DreamImage, batch_counter, frame_counter: FrameCounter,
directory_path,
prefix, digits, filetype, prompt, extra_pnginfo, at_end, logger):
if at_end == "stop output" and frame_counter.is_after_last_frame:
print("Reached end of animation - not saving output!")
logger("Reached end of animation - not saving output!")
return ()
if at_end == "raise error" and frame_counter.is_after_last_frame:
logger("Reached end of animation - raising error to stop processing!")
raise Exception("Reached end of animation!")
filename = self._get_new_filename(frame_counter.current_frame, prefix, digits, filetype)
if batch_counter >= 0:
filepath = os.path.join(directory_path, "batch_" + (str(batch_counter).zfill(4)), filename)
@@ -81,7 +86,7 @@ class DreamImageSequenceOutput:
dream_image.save_png(filepath, filetype == 'png with embedded workflow', prompt, extra_pnginfo)
elif filetype == "jpg":
dream_image.save_jpg(filepath, int(CONFIG.get("encoding.jpeg_quality", 95)))
print("Saved {} in {}".format(filename, os.path.abspath(save_dir)))
logger("Saved {} in {}".format(filename, os.path.abspath(save_dir)))
return ()
def _generate_animation_sequence(self, filetype, directory_path, frame_counter):
@@ -93,13 +98,19 @@ class DreamImageSequenceOutput:
return AnimationSequence(frame_counter, frames)
def save(self, image, **args):
log_texts = list()
logger = lambda s: log_texts.append(s)
if not args.get("directory_path", ""):
args["directory_path"] = comfy_paths.output_directory
args["logger"] = logger
proc = DreamImageProcessor(image, **args)
proc.process(self._save_single_image)
frame_counter: FrameCounter = args["frame_counter"]
log_entry = LogEntry([])
for text in log_texts:
log_entry = log_entry.add(text)
if frame_counter.is_final_frame:
return (self._generate_animation_sequence(args["filetype"], args["directory_path"],
frame_counter),)
frame_counter), log_entry)
else:
return (AnimationSequence(frame_counter),)
return (AnimationSequence(frame_counter), log_entry)
+21
View File
@@ -169,6 +169,9 @@ Post processing for animation sequences using the python module mpegCoder with f
### File Count [Dream]
Finds the number of files in a directory matching specified patterns.
### Float/Int/string to Log Entry [Dream]
Logging for float/int/string values.
### Frame Count Calculator [Dream]
Simple utility to calculate number of frames based on time and framerate.
@@ -209,6 +212,15 @@ Saves a frame to a directory.
### Image Sequence Tweening [Dream]
Post processing for animation sequences generating blended in-between frames.
### Laboratory [Dream]
Super-charged number generator for experimenting with ComfyUI.
### Log Entry Joiner [Dream]
Merges multiple log entries (reduces noodling).
### Log File [Dream]
The text logging facility for the Dream Project Animation nodes.
### Linear Curve [Dream]
Linear interpolation between two values over the full animation.
@@ -243,6 +255,9 @@ 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.
### Triangle Curve [Dream]
Triangle wave curve.
@@ -274,6 +289,12 @@ This flow demonstrates sampling image areas into palettes and generating noise f
[area-sampled-noise](examples/area-sampled-noise.json)
### Prompt Morphing
This flow demonstrates prompt building with weights based on curves and brightness and contrast control.
[prompt-morphing](examples/prompt-morphing.json)
## Known issues
### FFMPEG
+17 -9
View File
@@ -4,7 +4,9 @@ import shutil
import subprocess
import tempfile
from functools import lru_cache
from PIL import Image as PilImage
from .categories import NodeCategories
from .err import on_error
from .shared import DreamConfig, MpegEncoderUtility
@@ -134,8 +136,8 @@ class DreamVideoEncoderMpegCoder:
NODE_NAME = "Video Encoder (mpegCoder)"
ICON = "🎬"
CATEGORY = NodeCategories.ANIMATION_POSTPROCESSING
RETURN_TYPES = ()
RETURN_NAMES = ()
RETURN_TYPES = (LogEntry.ID,)
RETURN_NAMES = ("log_entry",)
OUTPUT_NODE = True
FUNCTION = "encode"
@@ -162,9 +164,10 @@ class DreamVideoEncoderMpegCoder:
def encode(self, sequence, name, framerate_factor, remove_images):
if not sequence.is_defined:
return ()
return (LogEntry([]),)
config = DreamConfig()
filename = _make_video_filename(name, config.get("mpeg_coder.file_extension", "mp4"))
log_entry = LogEntry([])
for batch_num in sequence.batches:
try:
images = list(sequence.get_image_files_of_batch(batch_num))
@@ -180,13 +183,14 @@ class DreamVideoEncoderMpegCoder:
fps=sequence.fps * framerate_factor,
codec_name=config.get("mpeg_coder.codec_name", "libx265"))
enc.encode()
log_entry = log_entry.add("Generated video '{}'".format(filename))
if remove_images:
for imagepath in images:
if os.path.isfile(imagepath):
os.unlink(imagepath)
except Exception as e:
on_error(self.__class__, str(e))
return ()
return (log_entry,)
class DreamVideoEncoder:
@@ -205,8 +209,8 @@ class DreamVideoEncoder:
}
CATEGORY = NodeCategories.ANIMATION_POSTPROCESSING
RETURN_TYPES = ()
RETURN_NAMES = ()
RETURN_TYPES = (LogEntry.ID,)
RETURN_NAMES = ("log_entry",)
OUTPUT_NODE = True
FUNCTION = "encode"
@@ -228,24 +232,28 @@ class DreamVideoEncoder:
def generate_video(self, files, fps, filename, config):
filename = self._find_free_filename(filename, os.path.dirname(files[0]))
_ffmpeg(config, files, fps, filename)
return filename
def encode(self, sequence: AnimationSequence, name: str, remove_images, framerate_factor):
if not sequence.is_defined:
return ()
return (LogEntry([]),)
config = DreamConfig()
filename = _make_video_filename(name, config.get("ffmpeg.file_extension", "mp4"))
log_entry = LogEntry([])
for batch_num in sequence.batches:
try:
images = list(sequence.get_image_files_of_batch(batch_num))
self.generate_video(images, sequence.fps * framerate_factor, filename, config)
actual_filename = self.generate_video(images, sequence.fps * framerate_factor, filename, config)
log_entry = log_entry.add("Generated video '{}'".format(actual_filename))
if remove_images:
for imagepath in images:
if os.path.isfile(imagepath):
os.unlink(imagepath)
except Exception as e:
on_error(self.__class__, str(e))
return ()
return (log_entry,)
class DreamSequenceTweening:
+4 -5
View File
@@ -332,9 +332,9 @@ class DreamStateStore:
class DreamStateFile:
def __init__(self, state_file_path=os.path.join(TEMP_PATH, "state.json")):
self._dirname = os.path.dirname(state_file_path)
self._filepath = state_file_path
def __init__(self, state_collection_name="state"):
self._filepath = os.path.join(TEMP_PATH, state_collection_name+".json")
self._dirname = os.path.dirname(self._filepath)
if not os.path.isdir(self._dirname):
os.makedirs(self._dirname)
if not os.path.isfile(self._filepath):
@@ -358,7 +358,6 @@ class DreamStateFile:
self._data[key] = value
with open(self._filepath, "w", encoding="utf-8") as f:
json.dump(self._data, f)
print("* {} -> {}".format(key, value))
return previous
@@ -385,7 +384,7 @@ class MpegEncoderUtility:
GOPSize=len(files), maxBframe=max_b_frame, frameRate=self._fps_to_tuple(fps))
def _calculate_bit_rate(self, width: int, height: int, fps: float, bit_rate_factor: float):
bits_per_pixel_base = 0.125
bits_per_pixel_base = 0.5
return round(max(10, float(width * height * fps * bits_per_pixel_base * bit_rate_factor * 0.001)))
def encode(self):
+30 -1
View File
@@ -2,7 +2,7 @@
import random
import time
from typing import List, Dict
from typing import List, Dict, Tuple
from .shared import DreamImage
@@ -133,6 +133,35 @@ class PartialPrompt:
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"
+213 -2
View File
@@ -1,8 +1,220 @@
# -*- coding: utf-8 -*-
import datetime
import math
import os
import folder_paths as comfy_paths
from .categories import NodeCategories
from .shared import hashed_as_strings
from .shared import hashed_as_strings, DreamStateFile
from .types import LogEntry, SharedTypes, FrameCounter
_logfile_state = DreamStateFile("logging")
class DreamJoinLog:
NODE_NAME = "Log Entry Joiner"
ICON = "🗎"
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = (LogEntry.ID,)
RETURN_NAMES = ("log_entry",)
FUNCTION = "convert"
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"entry_0": (LogEntry.ID,),
"entry_1": (LogEntry.ID,),
"entry_2": (LogEntry.ID,),
"entry_3": (LogEntry.ID,),
}
}
def convert(self, **values):
entry = LogEntry([])
for i in range(4):
txt = values.get("entry_" + str(i), None)
if txt:
entry = entry.merge(txt)
return (entry,)
class DreamFloatToLog:
NODE_NAME = "Float to Log Entry"
ICON = "🗎"
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = (LogEntry.ID,)
RETURN_NAMES = ("log_entry",)
FUNCTION = "convert"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("FLOAT", {"default": 0}),
"label": ("STRING", {"default": ""}),
},
}
def convert(self, label, value):
return (LogEntry.new(label + ": " + str(value)),)
class DreamIntToLog:
NODE_NAME = "Int to Log Entry"
ICON = "🗎"
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = (LogEntry.ID,)
RETURN_NAMES = ("log_entry",)
FUNCTION = "convert"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("INT", {"default": 0}),
"label": ("STRING", {"default": ""}),
},
}
def convert(self, label, value):
return (LogEntry.new(label + ": " + str(value)),)
class DreamStringToLog:
NODE_NAME = "String to Log Entry"
ICON = "🗎"
OUTPUT_NODE = True
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = (LogEntry.ID,)
RETURN_NAMES = ("log_entry",)
FUNCTION = "convert"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"default": ""}),
},
"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[max(0, 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):
@@ -52,4 +264,3 @@ class DreamFrameDimensions:
return (width, height, final_width, final_height)
else:
return (height, width, final_height, final_width)