From eda864f192ead80ae99f28117bfa00a1a6624b15 Mon Sep 17 00:00:00 2001 From: FizzleDorf <1fizzledorf@gmail.com> Date: Wed, 14 Jun 2023 02:23:01 -0400 Subject: [PATCH] ValueScheduleNode added --- ScheduledNodes.py | 237 ++++++++++++++++++++++ WaveNodes.py | 20 +- __init__.py | 10 +- __pycache__/AnimationNodes.cpython-39.pyc | Bin 2314 -> 0 bytes __pycache__/LerpNode.cpython-39.pyc | Bin 977 -> 0 bytes __pycache__/ScheduledNodes.cpython-39.pyc | Bin 0 -> 3006 bytes __pycache__/SinWave.cpython-39.pyc | Bin 2659 -> 0 bytes __pycache__/WaveNodes.cpython-39.pyc | Bin 6203 -> 6213 bytes __pycache__/__init__.cpython-39.pyc | Bin 2021 -> 2134 bytes javascript/animationNodes.js | 0 10 files changed, 250 insertions(+), 17 deletions(-) create mode 100644 ScheduledNodes.py delete mode 100644 __pycache__/AnimationNodes.cpython-39.pyc delete mode 100644 __pycache__/LerpNode.cpython-39.pyc create mode 100644 __pycache__/ScheduledNodes.cpython-39.pyc delete mode 100644 __pycache__/SinWave.cpython-39.pyc delete mode 100644 javascript/animationNodes.js diff --git a/ScheduledNodes.py b/ScheduledNodes.py new file mode 100644 index 0000000..24db078 --- /dev/null +++ b/ScheduledNodes.py @@ -0,0 +1,237 @@ +#These nodes were made using code from the Deforum extension for A1111 webui +#You can find the project here: https://github.com/deforum-art/sd-webui-deforum +import numexpr +#import torch +import numpy as np +import pandas as pd +import re +import simplejson as json + +# used by both nodes +def check_is_number(value): + float_pattern = r'^(?=.)([+-]?([0-9]*)(\.([0-9]+))?)$' + return re.match(float_pattern, value) + +#This node parses the user's formatted prompt, +#sequences the current prompt,next prompt, and +#conditioning strength, evalates expressions in +#the prompts, and then returns either current, +#next or averaged conditioning. +#class PromptSchedule: +# @classmethod +# def INPUT_TYPES(s): +# return {"required": {"text": ("STRING", {"multiline": True}), +# "clip": ("CLIP", ), +# "max_frames": ("INT", {"default": 120.0, "min": 1.0, "max": 9999.0, "step": 1.0}), +# "current_frame": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 9999.0, "step": 1.0}),}} +# +# RETURN_TYPES = ("CONDITIONING",) +# FUNCTION = "animate" +# +# CATEGORY = "FizzNodes/ScheduleNodes" +# +# def animate(self, text, max_frames, current_frame, clip,): +# #format the input so it's valid json +# inputText = self.prepare_prompt(str("{"+text+"}"), max_frames, current_frame) +# animation_prompts = json.loads(inputText.strip()) +# #inputText = str("{"+text+"}") +# #interpolate values in-between keyframes +# #evaluate expressions (if any) in the prompt +# print("animation_prompts", animation_prompts) +# #return a conditioning value +# return self.interpolate_prompts(animation_prompts, max_frames, int(current_frame), clip) +# +# +# def addWeighted(self, conditioning_to, conditioning_from, conditioning_to_strength): +# out = [] +# print(conditioning_to) +# print(conditioning_from) +# if len(conditioning_from) > 1: +# print("Warning: ConditioningAverage conditioning_from contains more than 1 cond, only the first one will actually be applied to conditioning_to.") +# +# cond_from = np.array(conditioning_from[0][0]) # Convert to NumPy array +# +# for i in range(len(conditioning_to)): +# t1 = np.array(conditioning_to[i][0]) # Convert to NumPy array +# t0 = cond_from[:, :t1.shape[1]] +# if t0.shape[1] < t1.shape[1]: +# t0 = np.concatenate([t0] + [np.zeros((1, (t1.shape[1] - t0.shape[1]), t1.shape[2]))], axis=1) +# +# tw = torch.mul(torch.tensor(t1), conditioning_to_strength) + torch.mul(torch.tensor(t0), (1.0 - conditioning_to_strength)) +# n = [tw, conditioning_to[i][1].copy()] +# out.append(n) +# return (out,) +# +# #parse the conditioning strength and determine in-betweens. +# def parse_weight(self, 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 +# print(float(numexpr.evaluate(w_raw[1:-1]))) +# return float(numexpr.evaluate(w_raw[1:-1])) +# +# +# def prepare_prompt(self, prompt_series, max_frames, frame_idx): +# max_f = max_frames - 1 +# pattern = r'`.*?`' +# regex = re.compile(pattern) +# prompt_parsed = str(prompt_series) +# print("prompt_parsed:",prompt_parsed) +# for match in regex.finditer(prompt_parsed): +# matched_string = match.group(0) +# print("matched_string : ", matched_string) +# parsed_string = matched_string.replace('t', f'{frame_idx}').replace("max_f", f"{max_f}").replace('`', '') +# parsed_value = numexpr.evaluate(parsed_string) +# print("value : ", parsed_value) +# prompt_parsed = prompt_parsed.replace(matched_string, str(parsed_value)) +# +# return prompt_parsed.strip() +# +# def interpolate_prompts(self, animation_prompts, max_frames, current_frame, clip): +# # 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 +# curr_prompt_series = [np.nan] * max_frames +# nxt_prompt_series = [np.nan] * max_frames +# weight_series = [np.nan] * max_frames +# +# +# # For every keyframe prompt except the last +# for i in range(0, len(sorted_prompts) - 1): +# # Get current and next keyframe +# current_key = int(sorted_prompts[i][0]) +# print("curKey:"+str(current_key)) +# next_key = int(sorted_prompts[i + 1][0]) +# print("nxtKey:"+str(next_key)) +# # 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_key >= next_key: +# print(f"WARNING: Sequential prompt keyframes {i}:{current_key} 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] +# +# # Calculate how much to shift the weight from current to next prompt at each frame +# weight_step = 1 / (next_key - current_key) +# +# for f in range(current_key, next_key): +# next_weight = weight_step * (f - current_key) +# current_weight = 1 - next_weight +# print(current_key) +# # We will build the prompt incrementally depending on which prompts are present +# curr_prompt_series[f] = '' +# nxt_prompt_series[f] = '' +# weight_series[f] = 0.0 +# +# curr_prompt_series[f] += self.prepare_prompt(current_prompt, max_frames, f) +# nxt_prompt_series[f] += self.prepare_prompt(next_prompt, max_frames, f) +# weight_series[f] += current_weight +# print(curr_prompt_series) +# print(nxt_prompt_series) +# print(weight_series) +# #([[clip.encode(text), {}]], ) +# print(curr_prompt_series[f]) +# print(nxt_prompt_series[f]) +# print(weight_series[f]) +# #([[clip.encode(text), {}]], ) +# # Return the filled series, in case max_frames is greater than the last keyframe or any ranges were skipped. +# if str(curr_prompt_series[current_frame]) == str(nxt_prompt_series[current_frame]): +# print("only curr") +# return ([[clip.encode(str(curr_prompt_series[current_frame])), {}]], ) +# if weight_series[current_frame] == 0: +# print("only curr") +# return ([[clip.encode(str(curr_prompt_series[current_frame])), {}]], ) +# if weight_series[current_frame] == 1: +# print("only next") +# return ([[clip.encode(str(nxt_prompt_series[current_frame])), {}]], ) +# else: +# print("both") +# return self.addWeighted(([[clip.encode(str(nxt_prompt_series[current_frame])), {}]], ), ([[clip.encode(str(curr_prompt_series[current_frame])), {}]], ), weight_series[current_frame]) +# + +#This node parses the user's test input into interpolated floats. +#Expressions can be input and evaluated. +class ValueSchedule: + @classmethod + def INPUT_TYPES(s): + return {"required": {"text": ("STRING", {"multiline": True}), + "max_frames": ("INT", {"default": 120.0, "min": 1.0, "max": 9999.0, "step": 1.0}), + "current_frame": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 9999.0, "step": 1.0}),}} + RETURN_TYPES = ("FLOAT",) + FUNCTION = "animate" + + CATEGORY = "FizzNodes/ScheduleNodes" + + def animate(self, text, max_frames, current_frame,): + t = self.get_inbetweens(self.parse_key_frames(text, max_frames), max_frames) + cFrame = int(current_frame) #current_frame is currently recieved as a float hence the cast to int + return (t[cFrame],) + + def sanitize_value(self, value): + return value.replace("'","").replace('"',"").replace('(',"").replace(')',"") + + def get_inbetweens(self, key_frames, max_frames, integer=False, interp_method='Linear', is_single_string = False): + key_frame_series = pd.Series([np.nan for a in range(max_frames)]) + # get our ui variables set for numexpr.evaluate + max_f = max_frames -1 + value_is_number = False + for i in range(0, max_frames): + if i in key_frames: + value = key_frames[i] + value_is_number = check_is_number(self.sanitize_value(value)) + if value_is_number: # if it's only a number, leave the rest for the default interpolation + key_frame_series[i] = self.sanitize_value(value) + if not value_is_number: + t = i + # workaround for values formatted like 0:("I am test") //used for sampler schedules + key_frame_series[i] = numexpr.evaluate(value) if not is_single_string else self.sanitize_value(value) + elif is_single_string:# take previous string value and replicate it + key_frame_series[i] = key_frame_series[i-1] + key_frame_series = key_frame_series.astype(float) if not is_single_string else key_frame_series # as string + + if interp_method == 'Cubic' and len(key_frames.items()) <= 3: + interp_method = 'Quadratic' + if interp_method == 'Quadratic' and len(key_frames.items()) <= 2: + interp_method = 'Linear' + + key_frame_series[0] = key_frame_series[key_frame_series.first_valid_index()] + key_frame_series[max_frames-1] = key_frame_series[key_frame_series.last_valid_index()] + key_frame_series = key_frame_series.interpolate(method=interp_method.lower(), limit_direction='both') + + if integer: + return key_frame_series.astype(int) + return key_frame_series + + def parse_key_frames(self, string, max_frames): + # because math functions (i.e. sin(t)) can utilize brackets + # it extracts the value in form of some stuff + # which has previously been enclosed with brackets and + # with a comma or end of line existing after the closing one + frames = dict() + for match_object in string.split(","): + frameParam = match_object.split(":") + max_f = max_frames -1 + frame = int(self.sanitize_value(frameParam[0])) if check_is_number(self.sanitize_value(frameParam[0].strip())) else int(numexpr.evaluate(frameParam[0].strip().replace("'","",1).replace('"',"",1)[::-1].replace("'","",1).replace('"',"",1)[::-1])) + frames[frame] = frameParam[1].strip() + if frames == {} and len(string) != 0: + raise RuntimeError('Key Frame string not correctly formatted') + return frames \ No newline at end of file diff --git a/WaveNodes.py b/WaveNodes.py index f45e023..19b810f 100644 --- a/WaveNodes.py +++ b/WaveNodes.py @@ -10,7 +10,7 @@ class Lerp: RETURN_TYPES = ("FLOAT",) FUNCTION = "lerp" - CATEGORY = "FizzNodes" + CATEGORY = "FizzNodes/WaveNodes" def lerp(self, num_Images, strength, current_frame): step = strength/num_Images @@ -29,7 +29,7 @@ class SinWave: RETURN_TYPES = ("FLOAT",) FUNCTION = "Wave" - CATEGORY = "FizzNodes" + CATEGORY = "FizzNodes/WaveNodes" def Wave(self, phase, amplitude, x_translation, y_translation, current_frame): output = (y_translation+(amplitude*(np.sin((2*np.pi*current_frame/phase-x_translation))))) @@ -48,7 +48,7 @@ class InvSinWave: RETURN_TYPES = ("FLOAT",) FUNCTION = "Wave" - CATEGORY = "FizzNodes" + CATEGORY = "FizzNodes/WaveNodes" def Wave(self, phase, amplitude, x_translation, y_translation, current_frame): output = (y_translation+(amplitude*-(np.sin(-1*(2*np.pi*current_frame/phase-x_translation))))) @@ -67,7 +67,7 @@ class CosWave: RETURN_TYPES = ("FLOAT",) FUNCTION = "Wave" - CATEGORY = "FizzNodes" + CATEGORY = "FizzNodes/WaveNodes" def Wave(self, phase, amplitude, x_translation, y_translation, current_frame): output = (y_translation+(amplitude*(np.cos((2*np.pi*current_frame/phase-x_translation))))) @@ -86,7 +86,7 @@ class InvCosWave: RETURN_TYPES = ("FLOAT",) FUNCTION = "Wave" - CATEGORY = "FizzNodes" + CATEGORY = "FizzNodes/WaveNodes" def Wave(self, phase, amplitude, x_translation, y_translation, current_frame): output = (y_translation+(amplitude*-(np.cos(-1*(2*np.pi*current_frame/phase-x_translation))))) @@ -105,7 +105,7 @@ class SquareWave: RETURN_TYPES = ("FLOAT",) FUNCTION = "Wave" - CATEGORY = "FizzNodes" + CATEGORY = "FizzNodes/WaveNodes" def Wave(self, phase, amplitude, x_translation, y_translation, current_frame): output = (y_translation+(step_increment*0**0**(0-np.sin((np.pi*current_frame/phase-x_translation))))) @@ -124,7 +124,7 @@ class SawtoothWave: RETURN_TYPES = ("FLOAT",) FUNCTION = "Wave" - CATEGORY = "FizzNodes" + CATEGORY = "FizzNodes/WaveNodes" def Wave(self, phase, step_increment, x_translation, start_value, current_frame): output = (start_value+(step_increment*(current_frame%phase)-x_translation)) @@ -143,7 +143,7 @@ class TriangleWave: RETURN_TYPES = ("FLOAT",) FUNCTION = "Wave" - CATEGORY = "FizzNodes" + CATEGORY = "FizzNodes/WaveNodes" def Wave(self, phase, amplitude, x_translation, y_translation, current_frame): output = (y_translation+amplitude/np.pi*(np.arcsin(np.sin(2*np.pi/phase*current_frame-x_translation)))) @@ -162,7 +162,7 @@ class AbsCosWave: RETURN_TYPES = ("FLOAT",) FUNCTION = "Wave" - CATEGORY = "FizzNodes" + CATEGORY = "FizzNodes/WaveNodes" def Wave(self, phase, amplitude, x_translation, max_value, current_frame): output = (max_value-(np.abs(np.cos(current_frame/phase))*amplitude)) @@ -181,7 +181,7 @@ class AbsSinWave: RETURN_TYPES = ("FLOAT",) FUNCTION = "Wave" - CATEGORY = "FizzNodes" + CATEGORY = "FizzNodes/WaveNodes" def Wave(self, phase, amplitude, x_translation, max_value, current_frame): output = (max_value-(np.abs(np.sin(current_frame/phase))*amplitude)) diff --git a/__init__.py b/__init__.py index 31584c2..db952a3 100644 --- a/__init__.py +++ b/__init__.py @@ -58,7 +58,7 @@ def is_installed(package, package_overwrite=None): # is_installed("onnxruntime", "onnxruntime-gpu") from .WaveNodes import Lerp, SinWave, InvSinWave, CosWave, InvCosWave, SquareWave, SawtoothWave, TriangleWave, AbsCosWave, AbsSinWave -#from .input import +from .ScheduledNodes import ValueSchedule#, PromptSchedule #from .ABGRemover import ABGRemover NODE_CLASS_MAPPINGS = { @@ -72,12 +72,8 @@ NODE_CLASS_MAPPINGS = { "TriangleWave": TriangleWave, "AbsCosWave": AbsCosWave, "AbsSinWave": AbsSinWave, - #"CLIPTextSchedule": CLIPTextSchedule - #"Input": InputNode - #"MultiAreaConditioning": MultiAreaConditioning, - #"ConditioningUpscale": ConditioningUpscale, - #"ConditioningStretch": ConditioningStretch, - #"ABGRemover": ABGRemover, + #"PromptSchedule": PromptSchedule, + "ValueSchedule": ValueSchedule, } print('\033[34mFizzleDorf Custom Nodes: \033[92mLoaded\033[0m') \ No newline at end of file diff --git a/__pycache__/AnimationNodes.cpython-39.pyc b/__pycache__/AnimationNodes.cpython-39.pyc deleted file mode 100644 index 799a701807fba611e35923f769525e6a41d1291b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2314 zcma)7TW=dh6rS0e*Oy#c0+e3BK!Gf6BVIsI(I!=z1WRR8#m+-rmu0&%wpX#&+u2## z8b?BE5Pu>M&0}8p6a0&LLm;8N^_d6YoN<~aP9K=ne0yf+cFuP$8%|A?2(*jew%DHq zLjJ(P@n*tc33`UW2%|mHr21_%P5ri-mVVn!dyO!InLC7;&y1!6Ba7KEvSCzUv`#$d z08Sx(gm}Ci-EtMaa(zS7;3lKY*dfm-++a3sW;GpV!$}S+?2x9*TsX0)+&f&P{($j_ z9y=1{BJ^eG*>wOsiH)OKxtSC^>Bz2{igE#>4B8^>^L?^U70tvfT%_Ue#_IJ}U4{>PJh&aV z+j$lblAtd0cDuDY5L>O~LBIWUv(}38Ob+@%0wiUvTS?pxC9FiwCWi~@Q&q0{cQ+eB zSev<87H1wntna-cC#l^}SOhrPp@a?}g6tj}e4 zz{DIJrYajNjm-^zOpkI`H~r;CZ5<+H`Br0PZGGdOzWta++Fe>L1NNgvSp~qQ1})hZ zwG7J;I1@oX(U7<$=-F+6|DP1B2+2`oGrYvgyOdSf)Q;UOvuOzASv9+Sr*^l&pU8UD zWR94iBQ|)V2f(#nxz+v>C!PQ53Fm^h6=N$CI zaGf59>%~cfPlm0kEU){wYv@5#bS68bC0I!!NDuKA9C#vv(J5$~rU+wfM4v+x@6nia zj1I-f6>r05u+!oC49^m%miQRqBLqnKQ`&p*?E~l;h5|8%)l+&;s21skxELC0gqmrs zkQYX`4N0C;%&$EITyOZp55JpVys_ZT-@kJ0!Q%Y=&#ry-;8SnDwJ;X1c;2FS*)xOmTp?L*mcf=r1 z#R43uY)z^%dpuEgD&j<{0uacbq(ZqI?+PXN3gZyMza#9xpMmDLX+I7`_!!*<`Jw1! zAykz#6d4c3hFpQ&83r_=CGd$0y+mEZg6fF!@Y1P}JHFfZ4DF+_qbdTcx43|C5#bU7 zrl5Ei;XQ;Z0tU7=ReX-p4TM*j`>Hs;4-=R;8Gh2m(Ha*=*LE#|v!1JLpa%X>Mv^Kg p4HFh-%1D_&%fy!mm=t>P>+VqQ*W)sO6J45NrE|e_c#P>Y{{aDG4W$48 diff --git a/__pycache__/LerpNode.cpython-39.pyc b/__pycache__/LerpNode.cpython-39.pyc deleted file mode 100644 index 21e0132c47989a708b6940d07c93d838d82033a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 977 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 diff --git a/__pycache__/WaveNodes.cpython-39.pyc b/__pycache__/WaveNodes.cpython-39.pyc index d46a8dd5eacf5cb4493cab6b3d609f7351854b67..dabd91e2f7fcdc3e2c1e296b9f3e54c498adceff 100644 GIT binary patch delta 44 ycmdmOaMXY|k(ZZ?0SF>k+fp8EwYZO-z$T0lgy&kN^Mx diff --git a/__pycache__/__init__.cpython-39.pyc b/__pycache__/__init__.cpython-39.pyc index 239a53fe77ad2570e4510d3b7225bc90f8824a44..572b1882be80beb0f1875292f9e3c54695ca2ccc 100644 GIT binary patch delta 200 zcmaFLe@#F;k(ZZ?0SG=scc$n8S&u;+WWWRDH~?|+j*Z%WjEusQs~9b~QutB?a&&X` zqVy(jU=$Ne)k_ha!yTocDx4zJ%*e=)!Whh;DYE$%<0oceO~zZiVTn1Vslmw^sVSv7 zshZrA)!B_$i}ZkoPCmnG%xJKQEs{|viVvnB#V{ha(28=u` NOpF+iiRlX)KL9l(D>MKA delta 151 zcmca6@RVOWk(ZZ?0SH*{H>c=wGcY^`agYH}jKKkji&t&b_G4reoLt3dp^>Vbs*0M)D=YybcN diff --git a/javascript/animationNodes.js b/javascript/animationNodes.js deleted file mode 100644 index e69de29..0000000