File size: 15,393 Bytes
81f4d3a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 |
# Copyright (C) 2023 Deforum LLC
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# Contact the authors: https://deforum.github.io/
import copy
import json
import logging
import operator
from operator import itemgetter
import numpy as np
import pandas as pd
import requests
from .animation_key_frames import DeformAnimKeys, ControlNetKeys, LooperAnimKeys
from .rich import console
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
IGNORED_FIELDS = ['fi', 'use_looper', 'imagesToKeyframe', 'schedules']
class ParseqAdapter():
def __init__(self, parseq_args, anim_args, video_args, controlnet_args, loop_args, mute=False):
# Basic data extraction
self.use_parseq = parseq_args.parseq_manifest and parseq_args.parseq_manifest.strip()
self.use_deltas = parseq_args.parseq_use_deltas
self.parseq_json = self.load_manifest(parseq_args) if self.use_parseq else json.loads('{ "rendered_frames": [{"frame": 0}] }')
self.rendered_frames = self.parseq_json['rendered_frames']
self.max_frame = self.get_max('frame')
self.required_frames = anim_args.max_frames
# Wrap the original schedules with Parseq decorators, so that Parseq values will override the original values IFF appropriate.
self.anim_keys = ParseqAnimKeysDecorator(self, DeformAnimKeys(anim_args))
self.cn_keys = ParseqControlNetKeysDecorator(self, ControlNetKeys(anim_args, controlnet_args)) if controlnet_args else None
# -1 because seed seems to be unused in LooperAnimKeys
self.looper_keys = ParseqLooperKeysDecorator(self, LooperAnimKeys(loop_args, anim_args, -1)) if loop_args else None
# Validation
if (self.use_parseq):
self.required_fps = video_args.fps
self.config_output_fps = self.parseq_json['options']['output_fps']
count_defined_frames = len(self.rendered_frames)
expected_defined_frames = self.max_frame+1 # frames are 0-indexed
if (expected_defined_frames != count_defined_frames):
logging.warning(f"There may be duplicated or missing frame data in the Parseq input: expected {expected_defined_frames} frames including frame 0 because the highest frame number is {self.max_frame}, but there are {count_defined_frames} frames defined.")
if not mute:
self.print_parseq_table()
# Resolve manifest either directly from supplied value or via supplied URL
def load_manifest(self, parseq_args):
manifestOrUrl = parseq_args.parseq_manifest.strip()
if (manifestOrUrl.startswith('http')):
logging.info(f"Loading Parseq manifest from URL: {manifestOrUrl}")
try:
body = requests.get(manifestOrUrl).text
logging.debug(f"Loaded remote manifest: {body}")
parseq_json = json.loads(body)
if not parseq_json or not 'rendered_frames' in parseq_json:
raise Exception(f"The JSON data does not look like a Parseq manifest (missing field 'rendered_frames').")
# SIDE EFFECT!
# Add the parseq manifest without the detailed frame data to parseq_args.
# This ensures it will be saved in the settings file, so that you can always
# see exactly what parseq prompts and keyframes were used, even if what the URL
# points to changes.
parseq_args.fetched_parseq_manifest_summary = copy.deepcopy(parseq_json)
if parseq_args.fetched_parseq_manifest_summary['rendered_frames']:
del parseq_args.fetched_parseq_manifest_summary['rendered_frames']
if parseq_args.fetched_parseq_manifest_summary['rendered_frames_meta']:
del parseq_args.fetched_parseq_manifest_summary['rendered_frames_meta']
return parseq_json
except Exception as e:
logging.error(f"Unable to load Parseq manifest from URL: {manifestOrUrl}")
raise e
else:
return json.loads(manifestOrUrl)
def print_parseq_table(self):
from rich.table import Table
from rich import box
table = Table(padding=0, box=box.ROUNDED, show_lines=True)
table.add_column("", style="white bold")
table.add_column("Parseq", style="cyan")
table.add_column("Deforum", style="green")
table.add_row("Animation", '\n'.join(self.anim_keys.managed_fields()), '\n'.join(self.anim_keys.unmanaged_fields()))
if self.cn_keys:
table.add_row("ControlNet", '\n'.join(self.cn_keys.managed_fields()), '\n'.join(self.cn_keys.unmanaged_fields()))
if self.looper_keys:
table.add_row("Guided Images", '\n'.join(self.looper_keys.managed_fields()), '\n'.join(self.looper_keys.unmanaged_fields()))
table.add_row("Prompts", "✅" if self.manages_prompts() else "❌", "✅" if not self.manages_prompts() else "❌")
table.add_row("Frames", str(len(self.rendered_frames)), str(self.required_frames) + (" ⚠️" if str(self.required_frames) != str(len(self.rendered_frames))+"" else ""))
table.add_row("FPS", str(self.config_output_fps), str(self.required_fps) + (" ⚠️" if str(self.required_fps) != str(self.config_output_fps) else ""))
console.print("\nUse this table to validate your Parseq & Deforum setup:")
console.print(table)
def manages_prompts(self):
return self.use_parseq and 'deforum_prompt' in self.rendered_frames[0].keys()
def manages_seed(self):
return self.use_parseq and 'seed' in self.rendered_frames[0].keys()
def get_max(self, seriesName):
return max(self.rendered_frames, key=itemgetter(seriesName))[seriesName]
class ParseqAbstractDecorator():
def __init__(self, adapter: ParseqAdapter, fallback_keys):
self.adapter = adapter
self.fallback_keys = fallback_keys
def parseq_to_series(self, seriesName):
# Check if value is present in first frame of JSON data. If not, assume it's undefined.
# The Parseq contract is that the first frame (at least) must define values for all fields.
try:
if self.adapter.rendered_frames[0][seriesName] is not None:
logging.debug(f"Found {seriesName} in first frame of Parseq data. Assuming it's defined.")
except KeyError:
return None
key_frame_series = pd.Series([np.nan for a in range(self.adapter.required_frames)])
for frame in self.adapter.rendered_frames:
frame_idx = frame['frame']
if frame_idx < self.adapter.required_frames:
if not np.isnan(key_frame_series[frame_idx]):
logging.warning(f"Duplicate frame definition {frame_idx} detected for data {seriesName}. Latest wins.")
key_frame_series[frame_idx] = frame[seriesName]
# If the animation will have more frames than Parseq defines,
# duplicate final value to match the required frame count.
while (frame_idx < self.adapter.required_frames):
key_frame_series[frame_idx] = operator.itemgetter(-1)(self.adapter.rendered_frames)[seriesName]
frame_idx += 1
return key_frame_series
# fallback to anim_args if the series is not defined in the Parseq data
def __getattribute__(inst, name):
try:
definedField = super(ParseqAbstractDecorator, inst).__getattribute__(name)
except AttributeError:
# No field with this name has been explicitly extracted from the JSON data.
# It must be a new parameter. Let's see if it's in the raw JSON.
parseqName = inst.strip_suffixes(name)
# returns None if not defined in Parseq JSON data
definedField = inst.parseq_to_series(parseqName)
if (definedField is not None):
# add the field to the instance so we don't compute it again.
setattr(inst, name, definedField)
if (definedField is not None):
return definedField
else:
logging.debug(f"Data for {name} not defined in Parseq data. Falling back to standard Deforum values.")
return getattr(inst.fallback_keys, name)
# parseq doesn't use _series, _schedule or _schedule_series suffixes in the
# JSON data - remove them.
def strip_suffixes(self, name):
strippableSuffixes = ['_series', '_schedule']
parseqName = name
while any(parseqName.endswith(suffix) for suffix in strippableSuffixes):
for suffix in strippableSuffixes:
if parseqName.endswith(suffix):
parseqName = parseqName[:-len(suffix)]
return parseqName
# parseq prefixes some field names for clarity. These prefixes are not present in the original Deforum names.
def strip_parseq_prefixes(self, name):
strippablePrefixes = ['guided_']
parseqName = name
while any(parseqName.startswith(prefix) for prefix in strippablePrefixes):
for prefix in strippablePrefixes:
if parseqName.startswith(prefix):
parseqName = parseqName[len(prefix):]
return parseqName
def all_parseq_fields(self):
return [self.strip_parseq_prefixes(field) for field in self.adapter.rendered_frames[0].keys() if (not field.endswith('_delta') and not field.endswith('_pc'))]
def managed_fields(self):
all_parseq_fields = self.all_parseq_fields()
deforum_fields = [self.strip_suffixes(property) for property, _ in vars(self.fallback_keys).items() if property not in IGNORED_FIELDS and not property.startswith('_')]
return [field for field in deforum_fields if field in all_parseq_fields]
def unmanaged_fields(self):
all_parseq_fields = self.all_parseq_fields()
deforum_fields = [self.strip_suffixes(property) for property, _ in vars(self.fallback_keys).items() if property not in IGNORED_FIELDS and not property.startswith('_')]
return [field for field in deforum_fields if field not in all_parseq_fields]
class ParseqControlNetKeysDecorator(ParseqAbstractDecorator):
def __init__(self, adapter: ParseqAdapter, cn_keys):
super().__init__(adapter, cn_keys)
class ParseqAnimKeysDecorator(ParseqAbstractDecorator):
def __init__(self, adapter: ParseqAdapter, anim_keys):
super().__init__(adapter, anim_keys)
# Parseq treats input values as absolute values. So if you want to
# progressively rotate 180 degrees over 4 frames, you specify: 45, 90, 135, 180.
# However, many animation parameters are relative to the previous frame if there is enough
# loopback strength. So if you want to rotate 180 degrees over 5 frames, the animation engine expects:
# 45, 45, 45, 45. Therefore, for such parameter, we use the fact that Parseq supplies delta values.
optional_delta = '_delta' if self.adapter.use_deltas else ''
self.angle_series = super().parseq_to_series('angle' + optional_delta)
self.zoom_series = super().parseq_to_series('zoom' + optional_delta)
self.translation_x_series = super().parseq_to_series('translation_x' + optional_delta)
self.translation_y_series = super().parseq_to_series('translation_y' + optional_delta)
self.translation_z_series = super().parseq_to_series('translation_z' + optional_delta)
self.rotation_3d_x_series = super().parseq_to_series('rotation_3d_x' + optional_delta)
self.rotation_3d_y_series = super().parseq_to_series('rotation_3d_y' + optional_delta)
self.rotation_3d_z_series = super().parseq_to_series('rotation_3d_z' + optional_delta)
self.perspective_flip_theta_series = super().parseq_to_series('perspective_flip_theta' + optional_delta)
self.perspective_flip_phi_series = super().parseq_to_series('perspective_flip_phi' + optional_delta)
self.perspective_flip_gamma_series = super().parseq_to_series('perspective_flip_gamma' + optional_delta)
# Non-motion animation args - never use deltas for these.
self.perspective_flip_fv_series = super().parseq_to_series('perspective_flip_fv')
self.noise_schedule_series = super().parseq_to_series('noise')
self.strength_schedule_series = super().parseq_to_series('strength')
self.sampler_schedule_series = super().parseq_to_series('sampler_schedule')
self.contrast_schedule_series = super().parseq_to_series('contrast')
self.cfg_scale_schedule_series = super().parseq_to_series('scale')
self.steps_schedule_series = super().parseq_to_series("steps_schedule")
self.seed_schedule_series = super().parseq_to_series('seed')
self.fov_series = super().parseq_to_series('fov')
self.near_series = super().parseq_to_series('near')
self.far_series = super().parseq_to_series('far')
self.subseed_schedule_series = super().parseq_to_series('subseed')
self.subseed_strength_schedule_series = super().parseq_to_series('subseed_strength')
self.kernel_schedule_series = super().parseq_to_series('antiblur_kernel')
self.sigma_schedule_series = super().parseq_to_series('antiblur_sigma')
self.amount_schedule_series = super().parseq_to_series('antiblur_amount')
self.threshold_schedule_series = super().parseq_to_series('antiblur_threshold')
# TODO - move to a different decorator?
self.prompts = super().parseq_to_series('deforum_prompt') # formatted as "{positive} --neg {negative}"
class ParseqLooperKeysDecorator(ParseqAbstractDecorator):
def __init__(self, adapter: ParseqAdapter, looper_keys):
super().__init__(adapter, looper_keys)
# The Deforum UI offers an "Image strength schedule" in the Guided Images section,
# which simply overrides the strength schedule if guided images is enabled.
# In Parseq, we just re-use the same strength schedule.
self.image_strength_schedule_series = super().parseq_to_series('strength')
# We explicitly state the mapping for all other guided images fields so we can strip the prefix
# that we use in Parseq.
self.blendFactorMax_series = super().parseq_to_series('guided_blendFactorMax')
self.blendFactorSlope_series = super().parseq_to_series('guided_blendFactorSlope')
self.tweening_frames_schedule_series = super().parseq_to_series('guided_tweening_frames')
self.color_correction_factor_series = super().parseq_to_series('guided_color_correction_factor')
|