# 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 . # Contact the authors: https://deforum.github.io/ import re import numexpr def check_is_number(value): float_pattern = r'^(?=.)([+-]?([0-9]*)(\.([0-9]+))?)$' return re.match(float_pattern, value) def parse_weight(match, frame=0, max_frames=0) -> float: w_raw = match.group("weight") max_f = max_frames # this line has to be left intact as it's in use by numexpr even though it looks like it doesn't if w_raw is None: return 1 if check_is_number(w_raw): return float(w_raw) else: t = frame if len(w_raw) < 3: print('the value inside `-characters cannot represent a math function') return 1 return float(numexpr.evaluate(w_raw[1:-1])) def split_weighted_subprompts(text, frame=0, max_frames=0): """ splits the prompt based on deforum webui implementation, moved from generate.py """ math_parser = re.compile("(?P(`[\S\s]*?`))", re.VERBOSE) parsed_prompt = re.sub(math_parser, lambda m: str(parse_weight(m, frame)), text) negative_prompts = [] positive_prompts = [] prompt_split = parsed_prompt.split("--neg") if len(prompt_split) > 1: positive_prompts, negative_prompts = parsed_prompt.split("--neg") # TODO: add --neg to vanilla Deforum for compat else: positive_prompts = prompt_split[0] negative_prompts = "" return positive_prompts, negative_prompts def interpolate_prompts(animation_prompts, max_frames): import numpy as np import pandas as pd # Get prompts sorted by keyframe max_f = max_frames parsed_animation_prompts = {} for key, value in animation_prompts.items(): if check_is_number(key): # default case 0:(1 + t %5), 30:(5-t%2) parsed_animation_prompts[key] = value else: # math on the left hand side case 0:(1 + t %5), maxKeyframes/2:(5-t%2) parsed_animation_prompts[int(numexpr.evaluate(key))] = value sorted_prompts = sorted(parsed_animation_prompts.items(), key=lambda item: int(item[0])) # Setup container for interpolated prompts prompt_series = pd.Series([np.nan for a in range(max_frames)]) # For every keyframe prompt except the last for i in range(0, len(sorted_prompts) - 1): # Get current and next keyframe current_frame = int(sorted_prompts[i][0]) next_frame = int(sorted_prompts[i + 1][0]) # Ensure there's no weird ordering issues or duplication in the animation prompts # (unlikely because we sort above, and the json parser will strip dupes) if current_frame >= next_frame: print(f"WARNING: Sequential prompt keyframes {i}:{current_frame} and {i + 1}:{next_frame} are not monotonously increasing; skipping interpolation.") continue # Get current and next keyframes' positive and negative prompts (if any) current_prompt = sorted_prompts[i][1] next_prompt = sorted_prompts[i + 1][1] current_positive, current_negative, *_ = current_prompt.split("--neg") + [None] next_positive, next_negative, *_ = next_prompt.split("--neg") + [None] # Calculate how much to shift the weight from current to next prompt at each frame weight_step = 1 / (next_frame - current_frame) # Apply weighted prompt interpolation for each frame between current and next keyframe # using the syntax: prompt1 :weight1 AND prompt1 :weight2 --neg nprompt1 :weight1 AND nprompt1 :weight2 # (See: https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features#composable-diffusion ) for f in range(current_frame, next_frame): next_weight = weight_step * (f - current_frame) current_weight = 1 - next_weight # We will build the prompt incrementally depending on which prompts are present prompt_series[f] = '' # Cater for the case where neither, either or both current & next have positive prompts: if current_positive: prompt_series[f] += f" ({current_positive}):{current_weight}" if current_positive and next_positive: prompt_series[f] += f" AND " if next_positive: prompt_series[f] += f" ({next_positive}):{next_weight}" # Cater for the case where neither, either or both current & next have negative prompts: if len(current_negative) > 1 or len(next_negative) > 1: prompt_series[f] += " --neg " if len(current_negative) > 1: prompt_series[f] += f" ({current_negative}):{current_weight}" if len(current_negative) > 1 and len(next_negative) > 1: prompt_series[f] += f" AND " if len(next_negative) > 1: prompt_series[f] += f" ({next_negative}):{next_weight}" # Set explicitly declared keyframe prompts (overwriting interpolated values at the keyframe idx). This ensures: # - That final prompt is set, and # - Gives us a chance to emit warnings if any keyframe prompts are already using composable diffusion for i, prompt in parsed_animation_prompts.items(): prompt_series[int(i)] = prompt if ' AND ' in prompt: print(f"WARNING: keyframe {i}'s prompt is using composable diffusion (aka the 'AND' keyword). This will cause unexpected behaviour with interpolation.") # Return the filled series, in case max_frames is greater than the last keyframe or any ranges were skipped. return prompt_series.ffill().bfill() def prepare_prompt(prompt_series, max_frames, seed, frame_idx): max_f = max_frames - 1 pattern = r'`.*?`' regex = re.compile(pattern) prompt_parsed = prompt_series for match in regex.finditer(prompt_parsed): matched_string = match.group(0) parsed_string = matched_string.replace('t', f'{frame_idx}').replace("max_f", f"{max_f}").replace('`', '') parsed_value = numexpr.evaluate(parsed_string) prompt_parsed = prompt_parsed.replace(matched_string, str(parsed_value)) prompt_to_print, *after_neg = prompt_parsed.strip().split("--neg") prompt_to_print = prompt_to_print.strip() after_neg = "".join(after_neg).strip() print(f"\033[32mSeed: \033[0m{seed}") print(f"\033[35mPrompt: \033[0m{prompt_to_print}") if after_neg and after_neg.strip(): print(f"\033[91mNeg Prompt: \033[0m{after_neg}") prompt_to_print += f"--neg {after_neg}" # set value back into the prompt return prompt_to_print