diff --git a/README.md b/README.md index adb7f62..9ea058d 100644 --- a/README.md +++ b/README.md @@ -49,11 +49,22 @@ Related project: https://github.com/FizzleDorf/ComfyUI_FizzNodes ## Prompt Scheduling +see also: [`Nodes > Scheduling`](https://github.com/dmarx/ComfyUI-Keyframed/blob/dev/README.md#scheduling) + + + +![Prompt Scheduling](examples/prompt-scheduling_ezmode.png) This one is probably why you are here. This workflow demonstrates how to use the `keyframed/schedule` nodes to achieve similar behavior as [FizzNodes'](https://github.com/FizzleDorf/ComfyUI_FizzNodes) **PromptSchedule** node, but implemented differently. -This schedule is essentailly a normal AnimateDiff workflow where several nodes have replaced the normal conditioning setup. Rather than a single `CLIP Text Encode` node, we can have multiple prompts which transition sequentially over time. For documentation detailing how this workflow works, see the [`Nodes > Scheduling`](https://github.com/dmarx/ComfyUI-Keyframed/blob/dev/README.md#scheduling) section below. +This schedule is essentailly a normal AnimateDiff workflow where several nodes have replaced the normal conditioning setup. Rather than a single `CLIP Text Encode` node, we can have multiple prompts which transition sequentially over time. + +The workflow above illustrations the most user-friendly interface, but there are nodes for lower level interfaces as well: + +![Prompt Scheduling](examples/prompt-scheduling_old.png) + ## Interleaving Multiple Prompts Simultaneously (aka Prompt Entanglement, aka Prompt Superposition) diff --git a/examples/prompt-scheduling_ezmode.png b/examples/prompt-scheduling_ezmode.png new file mode 100644 index 0000000..4cef56f Binary files /dev/null and b/examples/prompt-scheduling_ezmode.png differ diff --git a/examples/prompt-scheduling.png b/examples/prompt-scheduling_old.png similarity index 100% rename from examples/prompt-scheduling.png rename to examples/prompt-scheduling_old.png diff --git a/nodes/__init__.py b/nodes/__init__.py index 604ba5b..8dd3650 100644 --- a/nodes/__init__.py +++ b/nodes/__init__.py @@ -4,6 +4,7 @@ from .debug import NODE_CLASS_MAPPINGS as ncm0, NODE_DISPLAY_NAME_MAPPINGS as nd from .entangled import NODE_CLASS_MAPPINGS as ncm1, NODE_DISPLAY_NAME_MAPPINGS as ndnm1 from .schedule import NODE_CLASS_MAPPINGS as ncm2, NODE_DISPLAY_NAME_MAPPINGS as ndnm2 from .sinusoidal import NODE_CLASS_MAPPINGS as ncm3, NODE_DISPLAY_NAME_MAPPINGS as ndnm3 +from .pgroup import NODE_CLASS_MAPPINGS as ncm4, NODE_DISPLAY_NAME_MAPPINGS as ndnm4 # there's probably a cleaner, more-dummy-proof way to do this. @@ -12,10 +13,14 @@ NODE_CLASS_MAPPINGS.update(ncm0) NODE_CLASS_MAPPINGS.update(ncm1) NODE_CLASS_MAPPINGS.update(ncm2) NODE_CLASS_MAPPINGS.update(ncm3) +NODE_CLASS_MAPPINGS.update(ncm4) NODE_DISPLAY_NAME_MAPPINGS.update(ndnm0) NODE_DISPLAY_NAME_MAPPINGS.update(ndnm1) NODE_DISPLAY_NAME_MAPPINGS.update(ndnm2) NODE_DISPLAY_NAME_MAPPINGS.update(ndnm3) +NODE_DISPLAY_NAME_MAPPINGS.update(ndnm4) -__all__ =["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] \ No newline at end of file +__all__ =["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] + +# TODO: ensure installed: keyframed toolz \ No newline at end of file diff --git a/nodes/core.py b/nodes/core.py index e380926..dec40c5 100644 --- a/nodes/core.py +++ b/nodes/core.py @@ -248,6 +248,42 @@ class KfConditioningAddx10: return [((cond_t_out, cond_d_out),)] #((cond_t_out, cond_d_out),) + +class KfConditioningAddx10_alt: + CATEGORY = CATEGORY + FUNCTION = "main" + RETURN_TYPES = ("CONDITIONING",) + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "cond_0": ("CONDITIONING",{"forceInput": True,}), + }, + "optional": { + "cond_1": ("CONDITIONING",{"forceInput": True, "default": 0}), + "cond_2": ("CONDITIONING",{"forceInput": True, "default": 0}), + "cond_3": ("CONDITIONING",{"forceInput": True, "default": 0}), + "cond_4": ("CONDITIONING",{"forceInput": True, "default": 0}), + "cond_5": ("CONDITIONING",{"forceInput": True, "default": 0}), + "cond_6": ("CONDITIONING",{"forceInput": True, "default": 0}), + "cond_7": ("CONDITIONING",{"forceInput": True, "default": 0}), + "cond_8": ("CONDITIONING",{"forceInput": True, "default": 0}), + "cond_9": ("CONDITIONING",{"forceInput": True, "default": 0}), + }, + } + + def main(self, cond_0, **kwargs): + ((cond_t_out, cond_d_out),) = deepcopy(cond_0) + for v in kwargs.values(): + if v not in (0, None): + ((cond_t,cond_d),) = v + cond_t, cond_d = deepcopy(cond_t), deepcopy(cond_d) + cond_t_out = cond_t_out + cond_t + cond_d_out["pooled_output"] = cond_d_out["pooled_output"] + cond_d["pooled_output"] + return [((cond_t_out, cond_d_out),)] #((cond_t_out, cond_d_out),) + + # class KfCurveInverse: # CATEGORY = CATEGORY # FUNCTION = "main" @@ -270,7 +306,7 @@ class KfConditioningAddx10: # return (curve,) -def plot_curve(curve, n): +def plot_curve(curve, n, show_legend, is_pgroup=False): """ """ @@ -288,7 +324,7 @@ def plot_curve(curve, n): m=3 if n < m: - n = self.duration + 1 + n = curve.duration + 1 n = max(m, n) @@ -313,12 +349,14 @@ def plot_curve(curve, n): kfy = [curve[x] for x in kfx] plt.scatter(kfx, kfy, color=line[0].get_color()) - if isinstance(curve, kf.ParameterGroup): + #if isinstance(curve, kf.ParameterGroup): # type collision with kf.Composition + if is_pgroup: for c in curve.parameters.values(): draw_curve(c) else: draw_curve(curve) - plt.legend() + if show_legend: + plt.legend() #width, height = 10, 5 #inches @@ -356,11 +394,12 @@ class KfCurveDraw: "required": { "curve": ("KEYFRAMED_CURVE", {"forceInput": True,}), "n": ("INT", {"default": 64}), + "show_legend": ("BOOLEAN", {"default": True}), } } - def main(self, curve, n): - img_tensor = plot_curve(curve, n) + def main(self, curve, n, show_legend): + img_tensor = plot_curve(curve, n, show_legend, is_pgroup=False) return (img_tensor,) class KfPGroupDraw: @@ -374,11 +413,12 @@ class KfPGroupDraw: "required": { "parameter_group": ("PARAMETER_GROUP", {"forceInput": True,}), "n": ("INT", {"default": 64}), + "show_legend": ("BOOLEAN", {"default": True}), } } - def main(self, parameter_group, n): - img_tensor = plot_curve(parameter_group, n) + def main(self, parameter_group, n, show_legend): + img_tensor = plot_curve(parameter_group, n, show_legend, is_pgroup=True) return (img_tensor,) ########################################### @@ -429,7 +469,7 @@ class KfCurvesAddx10: }, } - def main(self, curve_0, curve_1, curve_2, curve_3, curve_4, curve_5, curve_6, curve_7, curve_8, curve_9): + def main(self, curve_0=0, curve_1=0, curve_2=0, curve_3=0, curve_4=0, curve_5=0, curve_6=0, curve_7=0, curve_8=0, curve_9=0): #curve_1 = deepcopy(curve_1) #curve_2 = deepcopy(curve_2) #return (curve_1 + curve_2, ) @@ -499,15 +539,15 @@ class KfCurvesMultiplyx10: "curve_0": ("KEYFRAMED_CURVE",{"forceInput": True,}), }, "optional": { - "curve_1": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 0}), - "curve_2": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 0}), - "curve_3": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 0}), - "curve_4": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 0}), - "curve_5": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 0}), - "curve_6": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 0}), - "curve_7": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 0}), - "curve_8": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 0}), - "curve_9": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 0}), + "curve_1": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 1}), + "curve_2": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 1}), + "curve_3": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 1}), + "curve_4": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 1}), + "curve_5": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 1}), + "curve_6": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 1}), + "curve_7": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 1}), + "curve_8": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 1}), + "curve_9": ("KEYFRAMED_CURVE",{"forceInput": True, "default": 1}), }, } @@ -568,72 +608,6 @@ class KfCurveConstant: return (curve,) -################################################################## - -### TODO: Working with parameter groups - -# add curve(s) to parameter group -## inputs: pgroup, curve -## returns pgroup -## if pgroup not provided, new one created - -class KfAddCurveToPGroup: - CATEGORY = CATEGORY - FUNCTION = "main" - #RETURN_TYPES = ("KEYFRAMED_CURVE",) - RETURN_TYPES = ("PARAMETER_GROUP",) - - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "curve": ("KEYFRAMED_CURVE",{"forceInput": True,}), - }, - "optional": { - "parameter_group": ("PARAMETER_GROUP",{"forceInput": True,}), - }, - } - - def main(self, curve, parameter_group=None): - curve = deepcopy(curve) - if parameter_group is None: - #parameter_group = kf.ParameterGroup({curve.label:curve}) - parameter_group = kf.ParameterGroup([curve]) - else: - parameter_group = deepcopy(parameter_group) - parameter_group.parameters[curve.label] = curve - return (parameter_group,) - - -class KfGetCurveFromPGroup: - CATEGORY = CATEGORY - FUNCTION = "main" - RETURN_TYPES = ("KEYFRAMED_CURVE",) - - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "curve_label": ("STRING",{"default": "My Curve",}), - "parameter_group": ("PARAMETER_GROUP",{"forceInput": True,}), - }, - } - - def main(self, curve_label, parameter_group): - curve = parameter_group.parameters[curve_label] - return (deepcopy(curve),) - - -# get curve from parameter group -## inputs: pgroup, label -## returns curve - -# extract a time slice from the parameter group - - -################################################################## - - ################################################################## @@ -665,14 +639,14 @@ NODE_CLASS_MAPPINGS = { "KfApplyCurveToCond": KfApplyCurveToCond, "KfConditioningAdd": KfConditioningAdd, ####################################### - "KfAddCurveToPGroup": KfAddCurveToPGroup, - "KfGetCurveFromPGroup": KfGetCurveFromPGroup, + "KfCurveDraw": KfCurveDraw, + "KfPGroupDraw": KfPGroupDraw, + "KfSetCurveLabel":KfSetCurveLabel, ####################################### #"KfCurveToAcnLatentKeyframe": KfCurveToAcnLatentKeyframe, ####################################### #"KfCurveInverse": KfCurveInverse, - "KfCurveDraw": KfCurveDraw, - "KfPGroupDraw": KfPGroupDraw, + "KfCurvesAdd": KfCurvesAdd, "KfCurvesSubtract": KfCurvesSubtract, "KfCurvesMultiply": KfCurvesMultiply, @@ -682,7 +656,6 @@ NODE_CLASS_MAPPINGS = { "KfConditioningAddx10":KfConditioningAddx10, "KfCurvesAddx10":KfCurvesAddx10, "KfCurvesMultiplyx10":KfCurvesMultiplyx10, - } # A dictionary that contains the friendly/humanly readable titles for the nodes diff --git a/nodes/pgroup.py b/nodes/pgroup.py new file mode 100644 index 0000000..41bee83 --- /dev/null +++ b/nodes/pgroup.py @@ -0,0 +1,211 @@ +import keyframed as kf +import logging +from copy import deepcopy + +logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + + +CATEGORY = "keyframed/parameter group" + + +class KfAddCurveToPGroup: + CATEGORY = CATEGORY + FUNCTION = "main" + #RETURN_TYPES = ("KEYFRAMED_CURVE",) + RETURN_TYPES = ("PARAMETER_GROUP",) + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "curve": ("KEYFRAMED_CURVE",{"forceInput": True,}), + }, + "optional": { + "parameter_group": ("PARAMETER_GROUP",{"forceInput": True,}), + }, + } + + def main(self, curve, parameter_group=None): + curve = deepcopy(curve) + if parameter_group is None: + #parameter_group = kf.ParameterGroup({curve.label:curve}) + parameter_group = kf.ParameterGroup([curve]) + else: + parameter_group = deepcopy(parameter_group) + parameter_group.parameters[curve.label] = curve + return (parameter_group,) + +class KfAddCurveToPGroupx10: + CATEGORY = CATEGORY + FUNCTION = "main" + #RETURN_TYPES = ("KEYFRAMED_CURVE",) + RETURN_TYPES = ("PARAMETER_GROUP",) + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "curve0": ("KEYFRAMED_CURVE",{"forceInput": True,}), + }, + "optional": { + "parameter_group": ("PARAMETER_GROUP",{"forceInput": True,}), + "curve1": ("KEYFRAMED_CURVE",{"forceInput": True,}), + "curve2": ("KEYFRAMED_CURVE",{"forceInput": True,}), + "curve3": ("KEYFRAMED_CURVE",{"forceInput": True,}), + "curve4": ("KEYFRAMED_CURVE",{"forceInput": True,}), + "curve5": ("KEYFRAMED_CURVE",{"forceInput": True,}), + "curve6": ("KEYFRAMED_CURVE",{"forceInput": True,}), + "curve7": ("KEYFRAMED_CURVE",{"forceInput": True,}), + "curve8": ("KEYFRAMED_CURVE",{"forceInput": True,}), + "curve9": ("KEYFRAMED_CURVE",{"forceInput": True,}), + }, + } + + def main(self, parameter_group=None, **kwargs): + if parameter_group is None: + #parameter_group = kf.ParameterGroup({curve.label:curve}) + parameter_group = kf.ParameterGroup(kwargs) + else: + parameter_group = deepcopy(parameter_group) + for curve in parameter_group.values(): + parameter_group.parameters[curve.label] = curve + return (parameter_group,) + + +class KfGetCurveFromPGroup: + CATEGORY = CATEGORY + FUNCTION = "main" + RETURN_TYPES = ("KEYFRAMED_CURVE",) + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "curve_label": ("STRING",{"default": "My Curve",}), + "parameter_group": ("PARAMETER_GROUP",{"forceInput": True,}), + }, + } + + def main(self, curve_label, parameter_group): + curve = parameter_group.parameters[curve_label] + return (deepcopy(curve),) + + +################################################################## + +# PGroup Arithmetic + +class KfPGroupCurveAdd: + CATEGORY = CATEGORY + FUNCTION = "main" + RETURN_TYPES = ("PARAMETER_GROUP",) + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "parameter_group": ("PARAMETER_GROUP",{"forceInput": True,}), + "curve": ("KEYFRAMED_CURVE",{"forceInput": True,}), + }, + } + + def main(self, parameter_group, curve): + parameter_group = deepcopy(parameter_group) + curve = deepcopy(curve) + return (parameter_group + curve, ) + + +class KfPGroupCurveMultiply: + CATEGORY = CATEGORY + FUNCTION = "main" + RETURN_TYPES = ("PARAMETER_GROUP",) + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "parameter_group": ("PARAMETER_GROUP",{"forceInput": True,}), + "curve": ("KEYFRAMED_CURVE",{"forceInput": True,}), + }, + } + + def main(self, parameter_group, curve): + parameter_group = deepcopy(parameter_group) + curve = deepcopy(curve) + return (parameter_group * curve, ) + + +# not behaving as expected :( + +class KfPGroupSum: + #CATEGORY = CATEGORY + CATEGORY = "keyframed/experimental" + FUNCTION = "main" + RETURN_TYPES = ("KEYFRAMED_CURVE",) + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "parameter_group": ("PARAMETER_GROUP",{"forceInput": True,}), + }, + } + + def main(self, parameter_group): + parameter_group = deepcopy(parameter_group) + #curve = kf.Composition(parameter_group, reduction='sum') + #curve = sum(parameter_group.parameters.values()) + outv = kf.Curve(0) + for curve in parameter_group.parameters.values(): + outv += curve + return (outv,) + #return (curve,) + + +class KfPGroupProd: + pass +# CATEGORY = CATEGORY +# FUNCTION = "main" +# RETURN_TYPES = ("KEYFRAMED_CURVE",) + +# @classmethod +# def INPUT_TYPES(s): +# return { +# "required": { +# "parameter_group": ("PARAMETER_GROUP",{"forceInput": True,}), +# }, +# } + +# def main(self, parameter_group): +# parameter_group = deepcopy(parameter_group) +# curve = kf.Composition(parameter_group, reduction='prod') +# return (curve,) + +################################################################## + +NODE_CLASS_MAPPINGS = { + #"KfCurveDraw": KfCurveDraw, + #"KfPGroupDraw": KfPGroupDraw, + #"KfSetCurveLabel":KfSetCurveLabel, + "KfAddCurveToPGroup": KfAddCurveToPGroup, + "KfGetCurveFromPGroup": KfGetCurveFromPGroup, + "KfAddCurveToPGroupx10": KfAddCurveToPGroupx10, + "KfPGroupCurveAdd":KfPGroupCurveAdd, + "KfPGroupCurveMultiply":KfPGroupCurveMultiply, + "KfPGroupSum": KfPGroupSum, + "KfPGroupProd": KfPGroupProd, +} + +# A dictionary that contains the friendly/humanly readable titles for the nodes +NODE_DISPLAY_NAME_MAPPINGS = { + #"KfSetCurveLabel": "Set Curve Label", + "KfAddCurveToPGroup": "Add Curve To Parameter Group", + "KfGetCurveFromPGroup": "Get Curve From Parameter Group", + "KfAddCurveToPGroupx10": "Add Curve To Parameter Group (x10)", + "KfPGroupCurveAdd": "Parameter Group + Curve (addition)", + "KfPGroupCurveMultiply": "Parameter Group * Curve (multiply)", + "KfPGroupSum": "Sum Over Parameter Group", + "KfPGroupProd": "Product Over Parameter Group", +} \ No newline at end of file diff --git a/nodes/schedule.py b/nodes/schedule.py index ad20acd..349b54f 100644 --- a/nodes/schedule.py +++ b/nodes/schedule.py @@ -27,9 +27,10 @@ class KfKeyframedCondition: return { "required": { "conditioning": ("CONDITIONING", {}), - "time": ("FLOAT", {"default": 0}), + "time": ("FLOAT", {"default": 0, "step": 1}), #"weight": ("FLOAT", {"default": 1}), # maybe i should hide this attribute - "interpolation_method": (list(kf.interpolation.INTERPOLATORS.keys()),), + #"interpolation_method": (list(kf.interpolation.INTERPOLATORS.keys()),), + "interpolation_method": (list(kf.interpolation.EASINGS.keys()), {"default":"linear"}), }, } @@ -52,6 +53,74 @@ class KfKeyframedCondition: return ({"kf_cond_t":kf_cond_t, "kf_cond_pooled":kf_cond_pooled, "cond_dict":cond_dict},) +def set_keyframed_condition(keyframed_condition, schedule=None): + keyframed_condition = deepcopy(keyframed_condition) + cond_dict = keyframed_condition.pop("cond_dict") + #cond_dict = deepcopy(cond_dict) + + if schedule is None: + # get a new copy of the tensor + kf_cond_t = keyframed_condition["kf_cond_t"] + #kf_cond_t.value = kf_cond_t.value.clone() # should be redundant with the deepcopy + curve_tokenized = kf.Curve([kf_cond_t], label="kf_cond_t") + curves = [curve_tokenized] + if keyframed_condition["kf_cond_pooled"] is not None: + kf_cond_pooled = keyframed_condition["kf_cond_pooled"] + curve_pooled = kf.Curve([kf_cond_pooled], label="kf_cond_pooled") + curves.append(curve_pooled) + schedule = (kf.ParameterGroup(curves), cond_dict) + else: + schedule = deepcopy(schedule) + schedule, old_cond_dict = schedule + for k, v in keyframed_condition.items(): + if (v is not None): + # for now, assume we already have a schedule for k. + # Not sure how to handle new conditioning type appearing. + schedule.parameters[k][v.t] = v + old_cond_dict.update(cond_dict) # NB: mutating this is probably bad + schedule = (schedule, old_cond_dict) + return schedule + + +class KfKeyframedConditionWithText(KfKeyframedCondition): + """ + Attaches a condition to a keyframe + """ + CATEGORY=CATEGORY + FUNCTION = 'main' + RETURN_TYPES = ("KEYFRAMED_CONDITION","CONDITIONING", "SCHEDULE") + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + #"conditioning": ("CONDITIONING", {}), + "clip": ("CLIP",), + "text": ("STRING", { + "multiline": True, + "default": "" + }), + "time": ("FLOAT", {"default": 0, "step": 1}), + #"weight": ("FLOAT", {"default": 1}), # maybe i should hide this attribute + #"interpolation_method": (list(kf.interpolation.INTERPOLATORS.keys()),), + "interpolation_method": (list(kf.interpolation.EASINGS.keys()), {"default":"linear"}), + }, + "optional": { + "schedule": ("SCHEDULE", {}), + } + } + + #def main(self, conditioning, time, interpolation_method): + def main(self, clip, text, time, interpolation_method, schedule=None): + tokens = clip.tokenize(text) + cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True) + conditioning = [[cond, {"pooled_output": pooled}]] + keyframed_condition = super().main(conditioning, time, interpolation_method)[0] + keyframed_condition["kf_cond_t"].label = text # attach prompt string as a label to the xattn keyframe for drawing + schedule = set_keyframed_condition(keyframed_condition, schedule) + return (keyframed_condition, conditioning, schedule) + + class KfSetKeyframe: CATEGORY=CATEGORY FUNCTION = 'main' @@ -68,31 +137,7 @@ class KfSetKeyframe: } } def main(self, keyframed_condition, schedule=None): - keyframed_condition = deepcopy(keyframed_condition) - cond_dict = keyframed_condition.pop("cond_dict") - #cond_dict = deepcopy(cond_dict) - - if schedule is None: - # get a new copy of the tensor - kf_cond_t = keyframed_condition["kf_cond_t"] - #kf_cond_t.value = kf_cond_t.value.clone() # should be redundant with the deepcopy - curve_tokenized = kf.Curve([kf_cond_t], label="kf_cond_t") - curves = [curve_tokenized] - if keyframed_condition["kf_cond_pooled"] is not None: - kf_cond_pooled = keyframed_condition["kf_cond_pooled"] - curve_pooled = kf.Curve([kf_cond_pooled], label="kf_cond_pooled") - curves.append(curve_pooled) - schedule = (kf.ParameterGroup(curves), cond_dict) - else: - schedule = deepcopy(schedule) - schedule, old_cond_dict = schedule - for k, v in keyframed_condition.items(): - if (v is not None): - # for now, assume we already have a schedule for k. - # Not sure how to handle new conditioning type appearing. - schedule.parameters[k][v.t] = v - old_cond_dict.update(cond_dict) # NB: mutating this is probably bad - schedule = (schedule, old_cond_dict) + schedule = set_keyframed_condition(keyframed_condition, schedule) return (schedule,) @@ -163,15 +208,78 @@ class KfGetScheduleConditionSlice: ################################################################### +from toolz.itertoolz import sliding_window + +def schedule_to_weight_curves(schedule): + """ + Given a keyfrmaed curve, returns a curve for each keyframe giving the contribution + of that keyframe to the schedule. Motivation here is to facilitate plotting + """ + schedule, _ = schedule + schedule = schedule.parameters["kf_cond_t"] + schedule = deepcopy(schedule) + curves = [] + keyframes = list(schedule._data.values()) + if len(keyframes) == 1: + keyframe = keyframes[0] + #curves = kf.ParameterGroup({kf.label: 1}) + curves = kf.ParameterGroup({keyframe.label: keyframe.Curve(1)}) + return curves + for (frame_in, frame_curr, frame_out) in sliding_window(3, keyframes): + frame_in.value, frame_curr.value, frame_out.value = 0,1,0 + c = kf.Curve({frame_in.t:frame_in, frame_curr.t:frame_curr, frame_out.t:frame_out}, + label=frame_curr.label) + c = deepcopy(c) + curves.append(c) + begin, end = keyframes[:2], keyframes[-2:] + #outv = [begin] + begin[0].value = 1 + begin[1].value = 0 + end[0].value = 0 + end[1].value = 1 + outv = [kf.Curve(begin, label=begin[0].label)] + if len(keyframes) == 2: + return outv + outv += curves + outv += [kf.Curve(end, label=end[1].label)] + return kf.ParameterGroup(outv) + +from .core import plot_curve + +class KfDrawSchedule: + CATEGORY=CATEGORY + FUNCTION = 'main' + RETURN_TYPES = ("IMAGE",) + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "schedule": ("SCHEDULE", {"forceInput": True,}), + "n": ("INT", {"default": 64}), + "show_legend": ("BOOLEAN", {"default": True}), + } + } + + def main(self, schedule, n, show_legend): + curves = schedule_to_weight_curves(schedule) + img_tensor = plot_curve(curves, n, show_legend, is_pgroup=True) + return (img_tensor,) + +################################################################### + NODE_CLASS_MAPPINGS = { "KfKeyframedCondition": KfKeyframedCondition, + "KfKeyframedConditionWithText":KfKeyframedConditionWithText, "KfSetKeyframe": KfSetKeyframe, "KfGetScheduleConditionAtTime": KfGetScheduleConditionAtTime, "KfGetScheduleConditionSlice": KfGetScheduleConditionSlice, + "KfDrawSchedule": KfDrawSchedule, } NODE_DISPLAY_NAME_MAPPINGS = { "KfKeyframedCondition": "Keyframed Condition", + "KfKeyframedConditionWithText": "Schedule Prompt", "KfSetKeyframe": "Set Keyframe", "KfGetScheduleConditionAtTime": "Evaluate Schedule At T", "KfGetScheduleConditionSlice": "Evaluate Schedule At T (Batch)",