# 'Deforum' plugin for Automatic1111's Stable Diffusion WebUI. # Copyright (C) 2023 Artem Khrapov (kabachuha) and Deforum team listed in AUTHORS.md # # 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 Affero 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 . # Contact the dev team: https://discord.gg/deforum 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 from .rich import console logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO) class ParseqAnimKeys(): def __init__(self, parseq_args, anim_args, video_args, mute=False): # Resolve manifest either directly from supplied value # or via supplied URL 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}") self.parseq_json = json.loads(body) # 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(self.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'] except Exception as e: logging.error(f"Unable to load Parseq manifest from URL: {manifestOrUrl}") raise e else: self.parseq_json = json.loads(manifestOrUrl) self.default_anim_keys = DeformAnimKeys(anim_args) self.rendered_frames = self.parseq_json['rendered_frames'] self.max_frame = self.get_max('frame') self.required_frames = anim_args.max_frames # TODO these values are currently only used to emit a subtle warning. User must ensure the output FPS set in parseq # matches the one set in Deforum to avoid unexpected results. # In the future we may wish to override video_args.fps value with the one from parseq. self.required_fps = video_args.fps self.config_output_fps = self.parseq_json['options']['output_fps'] if not mute: self.print_parseq_table() 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.") # 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 parseq_args.parseq_use_deltas else '' self.angle_series = self.parseq_to_anim_series('angle' + optional_delta) self.zoom_series = self.parseq_to_anim_series('zoom' + optional_delta) self.translation_x_series = self.parseq_to_anim_series('translation_x' + optional_delta) self.translation_y_series = self.parseq_to_anim_series('translation_y' + optional_delta) self.translation_z_series = self.parseq_to_anim_series('translation_z' + optional_delta) self.rotation_3d_x_series = self.parseq_to_anim_series('rotation_3d_x' + optional_delta) self.rotation_3d_y_series = self.parseq_to_anim_series('rotation_3d_y' + optional_delta) self.rotation_3d_z_series = self.parseq_to_anim_series('rotation_3d_z' + optional_delta) self.perspective_flip_theta_series = self.parseq_to_anim_series('perspective_flip_theta' + optional_delta) self.perspective_flip_phi_series = self.parseq_to_anim_series('perspective_flip_phi' + optional_delta) self.perspective_flip_gamma_series = self.parseq_to_anim_series('perspective_flip_gamma' + optional_delta) # Non-motion animation args self.perspective_flip_fv_series = self.parseq_to_anim_series('perspective_flip_fv') self.noise_schedule_series = self.parseq_to_anim_series('noise') self.strength_schedule_series = self.parseq_to_anim_series('strength') self.sampler_schedule_series = self.parseq_to_anim_series('sampler_schedule') self.contrast_schedule_series = self.parseq_to_anim_series('contrast') self.cfg_scale_schedule_series = self.parseq_to_anim_series('scale') self.steps_schedule_series = self.parseq_to_anim_series("steps_schedule") self.seed_schedule_series = self.parseq_to_anim_series('seed') self.fov_series = self.parseq_to_anim_series('fov') self.near_series = self.parseq_to_anim_series('near') self.far_series = self.parseq_to_anim_series('far') self.prompts = self.parseq_to_anim_series('deforum_prompt') # formatted as "{positive} --neg {negative}" self.subseed_schedule_series = self.parseq_to_anim_series('subseed') self.subseed_strength_schedule_series = self.parseq_to_anim_series('subseed_strength') self.kernel_schedule_series = self.parseq_to_anim_series('antiblur_kernel') self.sigma_schedule_series = self.parseq_to_anim_series('antiblur_sigma') self.amount_schedule_series = self.parseq_to_anim_series('antiblur_amount') self.threshold_schedule_series = self.parseq_to_anim_series('antiblur_threshold') 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("Fields", '\n'.join(self.managed_fields()), '\n'.join(self.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 self.required_frames != len(self.rendered_frames) else "")) table.add_row("FPS", str(self.config_output_fps), str(self.required_fps) + (" ⚠️" if self.required_fps != self.config_output_fps else "")) console.print("\nUse this table to validate your Parseq & Deforum setup:") console.print(table) def manages_prompts(self): return 'deforum_prompt' in self.rendered_frames[0].keys() def managed_fields(self): return [field for field in self.rendered_frames[0].keys() if (field not in ['frame', 'deforum_prompt'] and not field.endswith('_delta') and not field.endswith('_pc'))] def unmanaged_fields(self): managed_fields = self.managed_fields() all_fields = [self.strip_suffixes(property) for property, _ in vars(self.default_anim_keys).items() if property not in ['fi'] and not property.startswith('_')] return [field for field in all_fields if field not in managed_fields] def get_max(self, seriesName): return max(self.rendered_frames, key=itemgetter(seriesName))[seriesName] def parseq_to_anim_series(self, seriesName): # Check if valus 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.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.required_frames)]) for frame in self.rendered_frames: frame_idx = frame['frame'] if frame_idx < self.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.required_frames): key_frame_series[frame_idx] = operator.itemgetter(-1)(self.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(ParseqAnimKeys, 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_anim_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.default_anim_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