CoolFace
Apppublic

fred-dev/comfy_ui_ali

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
hooks.py786 linesDownload Raw Back to comfy
1from __future__ import annotations2from typing import TYPE_CHECKING, Callable3import enum4import math5import torch6import numpy as np7import itertools8import logging9 10if TYPE_CHECKING:11    from comfy.model_patcher import ModelPatcher, PatcherInjection12    from comfy.model_base import BaseModel13    from comfy.sd import CLIP14import comfy.lora15import comfy.model_management16import comfy.patcher_extension17from node_helpers import conditioning_set_values18 19# #######################################################################################################20# Hooks explanation21# -------------------22# The purpose of hooks is to allow conds to influence sampling without the need for ComfyUI core code to23# make explicit special cases like it does for ControlNet and GLIGEN.24#25# This is necessary for nodes/features that are intended for use with masked or scheduled conds, or those26# that should run special code when a 'marked' cond is used in sampling.27# #######################################################################################################28 29class EnumHookMode(enum.Enum):30    '''31    Priority of hook memory optimization vs. speed, mostly related to WeightHooks.32 33    MinVram: No caching will occur for any operations related to hooks.34    MaxSpeed: Excess VRAM (and RAM, once VRAM is sufficiently depleted) will be used to cache hook weights when switching hook groups.35    '''36    MinVram = "minvram"37    MaxSpeed = "maxspeed"38 39class EnumHookType(enum.Enum):40    '''41    Hook types, each of which has different expected behavior.42    '''43    Weight = "weight"44    ObjectPatch = "object_patch"45    AdditionalModels = "add_models"46    TransformerOptions = "transformer_options"47    Injections = "add_injections"48 49class EnumWeightTarget(enum.Enum):50    Model = "model"51    Clip = "clip"52 53class EnumHookScope(enum.Enum):54    '''55    Determines if hook should be limited in its influence over sampling.56 57    AllConditioning: hook will affect all conds used in sampling.58    HookedOnly: hook will only affect the conds it was attached to.59    '''60    AllConditioning = "all_conditioning"61    HookedOnly = "hooked_only"62 63 64class _HookRef:65    pass66 67 68def default_should_register(hook: Hook, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):69    '''Example for how custom_should_register function can look like.'''70    return True71 72 73def create_target_dict(target: EnumWeightTarget=None, **kwargs) -> dict[str]:74    '''Creates base dictionary for use with Hooks' target param.'''75    d = {}76    if target is not None:77        d['target'] = target78    d.update(kwargs)79    return d80 81 82class Hook:83    def __init__(self, hook_type: EnumHookType=None, hook_ref: _HookRef=None, hook_id: str=None,84                 hook_keyframe: HookKeyframeGroup=None, hook_scope=EnumHookScope.AllConditioning):85        self.hook_type = hook_type86        '''Enum identifying the general class of this hook.'''87        self.hook_ref = hook_ref if hook_ref else _HookRef()88        '''Reference shared between hook clones that have the same value. Should NOT be modified.'''89        self.hook_id = hook_id90        '''Optional string ID to identify hook; useful if need to consolidate duplicates at registration time.'''91        self.hook_keyframe = hook_keyframe if hook_keyframe else HookKeyframeGroup()92        '''Keyframe storage that can be referenced to get strength for current sampling step.'''93        self.hook_scope = hook_scope94        '''Scope of where this hook should apply in terms of the conds used in sampling run.'''95        self.custom_should_register = default_should_register96        '''Can be overriden with a compatible function to decide if this hook should be registered without the need to override .should_register'''97 98    @property99    def strength(self):100        return self.hook_keyframe.strength101 102    def initialize_timesteps(self, model: BaseModel):103        self.reset()104        self.hook_keyframe.initialize_timesteps(model)105 106    def reset(self):107        self.hook_keyframe.reset()108 109    def clone(self):110        c: Hook = self.__class__()111        c.hook_type = self.hook_type112        c.hook_ref = self.hook_ref113        c.hook_id = self.hook_id114        c.hook_keyframe = self.hook_keyframe115        c.hook_scope = self.hook_scope116        c.custom_should_register = self.custom_should_register117        return c118 119    def should_register(self, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):120        return self.custom_should_register(self, model, model_options, target_dict, registered)121 122    def add_hook_patches(self, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):123        raise NotImplementedError("add_hook_patches should be defined for Hook subclasses")124 125    def __eq__(self, other: Hook):126        return self.__class__ == other.__class__ and self.hook_ref == other.hook_ref127 128    def __hash__(self):129        return hash(self.hook_ref)130 131class WeightHook(Hook):132    '''133    Hook responsible for tracking weights to be applied to some model/clip.134 135    Note, value of hook_scope is ignored and is treated as HookedOnly.136    '''137    def __init__(self, strength_model=1.0, strength_clip=1.0):138        super().__init__(hook_type=EnumHookType.Weight, hook_scope=EnumHookScope.HookedOnly)139        self.weights: dict = None140        self.weights_clip: dict = None141        self.need_weight_init = True142        self._strength_model = strength_model143        self._strength_clip = strength_clip144        self.hook_scope = EnumHookScope.HookedOnly # this value does not matter for WeightHooks, just for docs145 146    @property147    def strength_model(self):148        return self._strength_model * self.strength149 150    @property151    def strength_clip(self):152        return self._strength_clip * self.strength153 154    def add_hook_patches(self, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):155        if not self.should_register(model, model_options, target_dict, registered):156            return False157        weights = None158 159        target = target_dict.get('target', None)160        if target == EnumWeightTarget.Clip:161            strength = self._strength_clip162        else:163            strength = self._strength_model164 165        if self.need_weight_init:166            key_map = {}167            if target == EnumWeightTarget.Clip:168                key_map = comfy.lora.model_lora_keys_clip(model.model, key_map)169            else:170                key_map = comfy.lora.model_lora_keys_unet(model.model, key_map)171            weights = comfy.lora.load_lora(self.weights, key_map, log_missing=False)172        else:173            if target == EnumWeightTarget.Clip:174                weights = self.weights_clip175            else:176                weights = self.weights177        model.add_hook_patches(hook=self, patches=weights, strength_patch=strength)178        registered.add(self)179        return True180        # TODO: add logs about any keys that were not applied181 182    def clone(self):183        c: WeightHook = super().clone()184        c.weights = self.weights185        c.weights_clip = self.weights_clip186        c.need_weight_init = self.need_weight_init187        c._strength_model = self._strength_model188        c._strength_clip = self._strength_clip189        return c190 191class ObjectPatchHook(Hook):192    def __init__(self, object_patches: dict[str]=None,193                 hook_scope=EnumHookScope.AllConditioning):194        super().__init__(hook_type=EnumHookType.ObjectPatch)195        self.object_patches = object_patches196        self.hook_scope = hook_scope197 198    def clone(self):199        c: ObjectPatchHook = super().clone()200        c.object_patches = self.object_patches201        return c202 203    def add_hook_patches(self, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):204        raise NotImplementedError("ObjectPatchHook is not supported yet in ComfyUI.")205 206class AdditionalModelsHook(Hook):207    '''208    Hook responsible for telling model management any additional models that should be loaded.209 210    Note, value of hook_scope is ignored and is treated as AllConditioning.211    '''212    def __init__(self, models: list[ModelPatcher]=None, key: str=None):213        super().__init__(hook_type=EnumHookType.AdditionalModels)214        self.models = models215        self.key = key216 217    def clone(self):218        c: AdditionalModelsHook = super().clone()219        c.models = self.models.copy() if self.models else self.models220        c.key = self.key221        return c222 223    def add_hook_patches(self, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):224        if not self.should_register(model, model_options, target_dict, registered):225            return False226        registered.add(self)227        return True228 229class TransformerOptionsHook(Hook):230    '''231    Hook responsible for adding wrappers, callbacks, patches, or anything else related to transformer_options.232    '''233    def __init__(self, transformers_dict: dict[str, dict[str, dict[str, list[Callable]]]]=None,234                 hook_scope=EnumHookScope.AllConditioning):235        super().__init__(hook_type=EnumHookType.TransformerOptions)236        self.transformers_dict = transformers_dict237        self.hook_scope = hook_scope238        self._skip_adding = False239        '''Internal value used to avoid double load of transformer_options when hook_scope is AllConditioning.'''240 241    def clone(self):242        c: TransformerOptionsHook = super().clone()243        c.transformers_dict = self.transformers_dict244        c._skip_adding = self._skip_adding245        return c246 247    def add_hook_patches(self, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):248        if not self.should_register(model, model_options, target_dict, registered):249            return False250        # NOTE: to_load_options will be used to manually load patches/wrappers/callbacks from hooks251        self._skip_adding = False252        if self.hook_scope == EnumHookScope.AllConditioning:253            add_model_options = {"transformer_options": self.transformers_dict,254                                 "to_load_options": self.transformers_dict}255            # skip_adding if included in AllConditioning to avoid double loading256            self._skip_adding = True257        else:258            add_model_options = {"to_load_options": self.transformers_dict}259        registered.add(self)260        comfy.patcher_extension.merge_nested_dicts(model_options, add_model_options, copy_dict1=False)261        return True262 263    def on_apply_hooks(self, model: ModelPatcher, transformer_options: dict[str]):264        if not self._skip_adding:265            comfy.patcher_extension.merge_nested_dicts(transformer_options, self.transformers_dict, copy_dict1=False)266 267WrapperHook = TransformerOptionsHook268'''Only here for backwards compatibility, WrapperHook is identical to TransformerOptionsHook.'''269 270class InjectionsHook(Hook):271    def __init__(self, key: str=None, injections: list[PatcherInjection]=None,272                 hook_scope=EnumHookScope.AllConditioning):273        super().__init__(hook_type=EnumHookType.Injections)274        self.key = key275        self.injections = injections276        self.hook_scope = hook_scope277 278    def clone(self):279        c: InjectionsHook = super().clone()280        c.key = self.key281        c.injections = self.injections.copy() if self.injections else self.injections282        return c283 284    def add_hook_patches(self, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):285        raise NotImplementedError("InjectionsHook is not supported yet in ComfyUI.")286 287class HookGroup:288    '''289    Stores groups of hooks, and allows them to be queried by type.290 291    To prevent breaking their functionality, never modify the underlying self.hooks or self._hook_dict vars directly;292    always use the provided functions on HookGroup.293    '''294    def __init__(self):295        self.hooks: list[Hook] = []296        self._hook_dict: dict[EnumHookType, list[Hook]] = {}297 298    def __len__(self):299        return len(self.hooks)300 301    def add(self, hook: Hook):302        if hook not in self.hooks:303            self.hooks.append(hook)304            self._hook_dict.setdefault(hook.hook_type, []).append(hook)305 306    def remove(self, hook: Hook):307        if hook in self.hooks:308            self.hooks.remove(hook)309            self._hook_dict[hook.hook_type].remove(hook)310 311    def get_type(self, hook_type: EnumHookType):312        return self._hook_dict.get(hook_type, [])313 314    def contains(self, hook: Hook):315        return hook in self.hooks316 317    def is_subset_of(self, other: HookGroup):318        self_hooks = set(self.hooks)319        other_hooks = set(other.hooks)320        return self_hooks.issubset(other_hooks)321 322    def new_with_common_hooks(self, other: HookGroup):323        c = HookGroup()324        for hook in self.hooks:325            if other.contains(hook):326                c.add(hook.clone())327        return c328 329    def clone(self):330        c = HookGroup()331        for hook in self.hooks:332            c.add(hook.clone())333        return c334 335    def clone_and_combine(self, other: HookGroup):336        c = self.clone()337        if other is not None:338            for hook in other.hooks:339                c.add(hook.clone())340        return c341 342    def set_keyframes_on_hooks(self, hook_kf: HookKeyframeGroup):343        if hook_kf is None:344            hook_kf = HookKeyframeGroup()345        else:346            hook_kf = hook_kf.clone()347        for hook in self.hooks:348            hook.hook_keyframe = hook_kf349 350    def get_hooks_for_clip_schedule(self):351        scheduled_hooks: dict[WeightHook, list[tuple[tuple[float,float], HookKeyframe]]] = {}352        # only care about WeightHooks, for now353        for hook in self.get_type(EnumHookType.Weight):354            hook: WeightHook355            hook_schedule = []356            # if no hook keyframes, assign default value357            if len(hook.hook_keyframe.keyframes) == 0:358                hook_schedule.append(((0.0, 1.0), None))359                scheduled_hooks[hook] = hook_schedule360                continue361            # find ranges of values362            prev_keyframe = hook.hook_keyframe.keyframes[0]363            for keyframe in hook.hook_keyframe.keyframes:364                if keyframe.start_percent > prev_keyframe.start_percent and not math.isclose(keyframe.strength, prev_keyframe.strength):365                    hook_schedule.append(((prev_keyframe.start_percent, keyframe.start_percent), prev_keyframe))366                    prev_keyframe = keyframe367                elif keyframe.start_percent == prev_keyframe.start_percent:368                    prev_keyframe = keyframe369            # create final range, assuming last start_percent was not 1.0370            if not math.isclose(prev_keyframe.start_percent, 1.0):371                hook_schedule.append(((prev_keyframe.start_percent, 1.0), prev_keyframe))372            scheduled_hooks[hook] = hook_schedule373        # hooks should not have their schedules in a list of tuples374        all_ranges: list[tuple[float, float]] = []375        for range_kfs in scheduled_hooks.values():376            for t_range, keyframe in range_kfs:377                all_ranges.append(t_range)378        # turn list of ranges into boundaries379        boundaries_set = set(itertools.chain.from_iterable(all_ranges))380        boundaries_set.add(0.0)381        boundaries = sorted(boundaries_set)382        real_ranges = [(boundaries[i], boundaries[i + 1]) for i in range(len(boundaries) - 1)]383        # with real ranges defined, give appropriate hooks w/ keyframes for each range384        scheduled_keyframes: list[tuple[tuple[float,float], list[tuple[WeightHook, HookKeyframe]]]] = []385        for t_range in real_ranges:386            hooks_schedule = []387            for hook, val in scheduled_hooks.items():388                keyframe = None389                # check if is a keyframe that works for the current t_range390                for stored_range, stored_kf in val:391                    # if stored start is less than current end, then fits - give it assigned keyframe392                    if stored_range[0] < t_range[1] and stored_range[1] > t_range[0]:393                        keyframe = stored_kf394                        break395                hooks_schedule.append((hook, keyframe))396            scheduled_keyframes.append((t_range, hooks_schedule))397        return scheduled_keyframes398 399    def reset(self):400        for hook in self.hooks:401            hook.reset()402 403    @staticmethod404    def combine_all_hooks(hooks_list: list[HookGroup], require_count=0) -> HookGroup:405        actual: list[HookGroup] = []406        for group in hooks_list:407            if group is not None:408                actual.append(group)409        if len(actual) < require_count:410            raise Exception(f"Need at least {require_count} hooks to combine, but only had {len(actual)}.")411        # if no hooks, then return None412        if len(actual) == 0:413            return None414        # if only 1 hook, just return itself without cloning415        elif len(actual) == 1:416            return actual[0]417        final_hook: HookGroup = None418        for hook in actual:419            if final_hook is None:420                final_hook = hook.clone()421            else:422                final_hook = final_hook.clone_and_combine(hook)423        return final_hook424 425 426class HookKeyframe:427    def __init__(self, strength: float, start_percent=0.0, guarantee_steps=1):428        self.strength = strength429        # scheduling430        self.start_percent = float(start_percent)431        self.start_t = 999999999.9432        self.guarantee_steps = guarantee_steps433 434    def get_effective_guarantee_steps(self, max_sigma: torch.Tensor):435        '''If keyframe starts before current sampling range (max_sigma), treat as 0.'''436        if self.start_t > max_sigma:437            return 0438        return self.guarantee_steps439 440    def clone(self):441        c = HookKeyframe(strength=self.strength,442                         start_percent=self.start_percent, guarantee_steps=self.guarantee_steps)443        c.start_t = self.start_t444        return c445 446class HookKeyframeGroup:447    def __init__(self):448        self.keyframes: list[HookKeyframe] = []449        self._current_keyframe: HookKeyframe = None450        self._current_used_steps = 0451        self._current_index = 0452        self._current_strength = None453        self._curr_t = -1.454 455    # properties shadow those of HookWeightsKeyframe456    @property457    def strength(self):458        if self._current_keyframe is not None:459            return self._current_keyframe.strength460        return 1.0461 462    def reset(self):463        self._current_keyframe = None464        self._current_used_steps = 0465        self._current_index = 0466        self._current_strength = None467        self.curr_t = -1.468        self._set_first_as_current()469 470    def add(self, keyframe: HookKeyframe):471        # add to end of list, then sort472        self.keyframes.append(keyframe)473        self.keyframes = get_sorted_list_via_attr(self.keyframes, "start_percent")474        self._set_first_as_current()475 476    def _set_first_as_current(self):477        if len(self.keyframes) > 0:478            self._current_keyframe = self.keyframes[0]479        else:480            self._current_keyframe = None481 482    def has_guarantee_steps(self):483        for kf in self.keyframes:484            if kf.guarantee_steps > 0:485                return True486        return False487 488    def has_index(self, index: int):489        return index >= 0 and index < len(self.keyframes)490 491    def is_empty(self):492        return len(self.keyframes) == 0493 494    def clone(self):495        c = HookKeyframeGroup()496        for keyframe in self.keyframes:497            c.keyframes.append(keyframe.clone())498        c._set_first_as_current()499        return c500 501    def initialize_timesteps(self, model: BaseModel):502        for keyframe in self.keyframes:503            keyframe.start_t = model.model_sampling.percent_to_sigma(keyframe.start_percent)504 505    def prepare_current_keyframe(self, curr_t: float, transformer_options: dict[str, torch.Tensor]) -> bool:506        if self.is_empty():507            return False508        if curr_t == self._curr_t:509            return False510        max_sigma = torch.max(transformer_options["sample_sigmas"])511        prev_index = self._current_index512        prev_strength = self._current_strength513        # if met guaranteed steps, look for next keyframe in case need to switch514        if self._current_used_steps >= self._current_keyframe.get_effective_guarantee_steps(max_sigma):515            # if has next index, loop through and see if need to switch516            if self.has_index(self._current_index+1):517                for i in range(self._current_index+1, len(self.keyframes)):518                    eval_c = self.keyframes[i]519                    # check if start_t is greater or equal to curr_t520                    # NOTE: t is in terms of sigmas, not percent, so bigger number = earlier step in sampling521                    if eval_c.start_t >= curr_t:522                        self._current_index = i523                        self._current_strength = eval_c.strength524                        self._current_keyframe = eval_c525                        self._current_used_steps = 0526                        # if guarantee_steps greater than zero, stop searching for other keyframes527                        if self._current_keyframe.get_effective_guarantee_steps(max_sigma) > 0:528                            break529                    # if eval_c is outside the percent range, stop looking further530                    else: break531        # update steps current context is used532        self._current_used_steps += 1533        # update current timestep this was performed on534        self._curr_t = curr_t535        # return True if keyframe changed, False if no change536        return prev_index != self._current_index and prev_strength != self._current_strength537 538 539class InterpolationMethod:540    LINEAR = "linear"541    EASE_IN = "ease_in"542    EASE_OUT = "ease_out"543    EASE_IN_OUT = "ease_in_out"544 545    _LIST = [LINEAR, EASE_IN, EASE_OUT, EASE_IN_OUT]546 547    @classmethod548    def get_weights(cls, num_from: float, num_to: float, length: int, method: str, reverse=False):549        diff = num_to - num_from550        if method == cls.LINEAR:551            weights = torch.linspace(num_from, num_to, length)552        elif method == cls.EASE_IN:553            index = torch.linspace(0, 1, length)554            weights = diff * np.power(index, 2) + num_from555        elif method == cls.EASE_OUT:556            index = torch.linspace(0, 1, length)557            weights = diff * (1 - np.power(1 - index, 2)) + num_from558        elif method == cls.EASE_IN_OUT:559            index = torch.linspace(0, 1, length)560            weights = diff * ((1 - np.cos(index * np.pi)) / 2) + num_from561        else:562            raise ValueError(f"Unrecognized interpolation method '{method}'.")563        if reverse:564            weights = weights.flip(dims=(0,))565        return weights566 567def get_sorted_list_via_attr(objects: list, attr: str) -> list:568    if not objects:569        return objects570    elif len(objects) <= 1:571        return [x for x in objects]572    # now that we know we have to sort, do it following these rules:573    # a) if objects have same value of attribute, maintain their relative order574    # b) perform sorting of the groups of objects with same attributes575    unique_attrs = {}576    for o in objects:577        val_attr = getattr(o, attr)578        attr_list: list = unique_attrs.get(val_attr, list())579        attr_list.append(o)580        if val_attr not in unique_attrs:581            unique_attrs[val_attr] = attr_list582    # now that we have the unique attr values grouped together in relative order, sort them by key583    sorted_attrs = dict(sorted(unique_attrs.items()))584    # now flatten out the dict into a list to return585    sorted_list = []586    for object_list in sorted_attrs.values():587        sorted_list.extend(object_list)588    return sorted_list589 590def create_transformer_options_from_hooks(model: ModelPatcher, hooks: HookGroup,  transformer_options: dict[str]=None):591    # if no hooks or is not a ModelPatcher for sampling, return empty dict592    if hooks is None or model.is_clip:593        return {}594    if transformer_options is None:595        transformer_options = {}596    for hook in hooks.get_type(EnumHookType.TransformerOptions):597        hook: TransformerOptionsHook598        hook.on_apply_hooks(model, transformer_options)599    return transformer_options600 601def create_hook_lora(lora: dict[str, torch.Tensor], strength_model: float, strength_clip: float):602    hook_group = HookGroup()603    hook = WeightHook(strength_model=strength_model, strength_clip=strength_clip)604    hook_group.add(hook)605    hook.weights = lora606    return hook_group607 608def create_hook_model_as_lora(weights_model, weights_clip, strength_model: float, strength_clip: float):609    hook_group = HookGroup()610    hook = WeightHook(strength_model=strength_model, strength_clip=strength_clip)611    hook_group.add(hook)612    patches_model = None613    patches_clip = None614    if weights_model is not None:615        patches_model = {}616        for key in weights_model:617            patches_model[key] = ("model_as_lora", (weights_model[key],))618    if weights_clip is not None:619        patches_clip = {}620        for key in weights_clip:621            patches_clip[key] = ("model_as_lora", (weights_clip[key],))622    hook.weights = patches_model623    hook.weights_clip = patches_clip624    hook.need_weight_init = False625    return hook_group626 627def get_patch_weights_from_model(model: ModelPatcher, discard_model_sampling=True):628    if model is None:629        return None630    patches_model: dict[str, torch.Tensor] = model.model.state_dict()631    if discard_model_sampling:632        # do not include ANY model_sampling components of the model that should act as a patch633        for key in list(patches_model.keys()):634            if key.startswith("model_sampling"):635                patches_model.pop(key, None)636    return patches_model637 638# NOTE: this function shows how to register weight hooks directly on the ModelPatchers639def load_hook_lora_for_models(model: ModelPatcher, clip: CLIP, lora: dict[str, torch.Tensor],640                              strength_model: float, strength_clip: float):641    key_map = {}642    if model is not None:643        key_map = comfy.lora.model_lora_keys_unet(model.model, key_map)644    if clip is not None:645        key_map = comfy.lora.model_lora_keys_clip(clip.cond_stage_model, key_map)646 647    hook_group = HookGroup()648    hook = WeightHook()649    hook_group.add(hook)650    loaded: dict[str] = comfy.lora.load_lora(lora, key_map)651    if model is not None:652        new_modelpatcher = model.clone()653        k = new_modelpatcher.add_hook_patches(hook=hook, patches=loaded, strength_patch=strength_model)654    else:655        k = ()656        new_modelpatcher = None657 658    if clip is not None:659        new_clip = clip.clone()660        k1 = new_clip.patcher.add_hook_patches(hook=hook, patches=loaded, strength_patch=strength_clip)661    else:662        k1 = ()663        new_clip = None664    k = set(k)665    k1 = set(k1)666    for x in loaded:667        if (x not in k) and (x not in k1):668            logging.warning(f"NOT LOADED {x}")669    return (new_modelpatcher, new_clip, hook_group)670 671def _combine_hooks_from_values(c_dict: dict[str, HookGroup], values: dict[str, HookGroup], cache: dict[tuple[HookGroup, HookGroup], HookGroup]):672    hooks_key = 'hooks'673    # if hooks only exist in one dict, do what's needed so that it ends up in c_dict674    if hooks_key not in values:675        return676    if hooks_key not in c_dict:677        hooks_value = values.get(hooks_key, None)678        if hooks_value is not None:679            c_dict[hooks_key] = hooks_value680        return681    # otherwise, need to combine with minimum duplication via cache682    hooks_tuple = (c_dict[hooks_key], values[hooks_key])683    cached_hooks = cache.get(hooks_tuple, None)684    if cached_hooks is None:685        new_hooks = hooks_tuple[0].clone_and_combine(hooks_tuple[1])686        cache[hooks_tuple] = new_hooks687        c_dict[hooks_key] = new_hooks688    else:689        c_dict[hooks_key] = cache[hooks_tuple]690 691def conditioning_set_values_with_hooks(conditioning, values={}, append_hooks=True,692                                       cache: dict[tuple[HookGroup, HookGroup], HookGroup]=None):693    c = []694    if cache is None:695        cache = {}696    for t in conditioning:697        n = [t[0], t[1].copy()]698        for k in values:699            if append_hooks and k == 'hooks':700                _combine_hooks_from_values(n[1], values, cache)701            else:702                n[1][k] = values[k]703        c.append(n)704 705    return c706 707def set_hooks_for_conditioning(cond, hooks: HookGroup, append_hooks=True, cache: dict[tuple[HookGroup, HookGroup], HookGroup]=None):708    if hooks is None:709        return cond710    return conditioning_set_values_with_hooks(cond, {'hooks': hooks}, append_hooks=append_hooks, cache=cache)711 712def set_timesteps_for_conditioning(cond, timestep_range: tuple[float,float]):713    if timestep_range is None:714        return cond715    return conditioning_set_values(cond, {"start_percent": timestep_range[0],716                                          "end_percent": timestep_range[1]})717 718def set_mask_for_conditioning(cond, mask: torch.Tensor, set_cond_area: str, strength: float):719    if mask is None:720        return cond721    set_area_to_bounds = False722    if set_cond_area != 'default':723        set_area_to_bounds = True724    if len(mask.shape) < 3:725        mask = mask.unsqueeze(0)726    return conditioning_set_values(cond, {'mask': mask,727                                          'set_area_to_bounds': set_area_to_bounds,728                                          'mask_strength': strength})729 730def combine_conditioning(conds: list):731    combined_conds = []732    for cond in conds:733        combined_conds.extend(cond)734    return combined_conds735 736def combine_with_new_conds(conds: list, new_conds: list):737    combined_conds = []738    for c, new_c in zip(conds, new_conds):739        combined_conds.append(combine_conditioning([c, new_c]))740    return combined_conds741 742def set_conds_props(conds: list, strength: float, set_cond_area: str,743                   mask: torch.Tensor=None, hooks: HookGroup=None, timesteps_range: tuple[float,float]=None, append_hooks=True):744    final_conds = []745    cache = {}746    for c in conds:747        # first, apply lora_hook to conditioning, if provided748        c = set_hooks_for_conditioning(c, hooks, append_hooks=append_hooks, cache=cache)749        # next, apply mask to conditioning750        c = set_mask_for_conditioning(cond=c, mask=mask, strength=strength, set_cond_area=set_cond_area)751        # apply timesteps, if present752        c = set_timesteps_for_conditioning(cond=c, timestep_range=timesteps_range)753        # finally, apply mask to conditioning and store754        final_conds.append(c)755    return final_conds756 757def set_conds_props_and_combine(conds: list, new_conds: list, strength: float=1.0, set_cond_area: str="default",758                               mask: torch.Tensor=None, hooks: HookGroup=None, timesteps_range: tuple[float,float]=None, append_hooks=True):759    combined_conds = []760    cache = {}761    for c, masked_c in zip(conds, new_conds):762        # first, apply lora_hook to new conditioning, if provided763        masked_c = set_hooks_for_conditioning(masked_c, hooks, append_hooks=append_hooks, cache=cache)764        # next, apply mask to new conditioning, if provided765        masked_c = set_mask_for_conditioning(cond=masked_c, mask=mask, set_cond_area=set_cond_area, strength=strength)766        # apply timesteps, if present767        masked_c = set_timesteps_for_conditioning(cond=masked_c, timestep_range=timesteps_range)768        # finally, combine with existing conditioning and store769        combined_conds.append(combine_conditioning([c, masked_c]))770    return combined_conds771 772def set_default_conds_and_combine(conds: list, new_conds: list,773                                   hooks: HookGroup=None, timesteps_range: tuple[float,float]=None, append_hooks=True):774    combined_conds = []775    cache = {}776    for c, new_c in zip(conds, new_conds):777        # first, apply lora_hook to new conditioning, if provided778        new_c = set_hooks_for_conditioning(new_c, hooks, append_hooks=append_hooks, cache=cache)779        # next, add default_cond key to cond so that during sampling, it can be identified780        new_c = conditioning_set_values(new_c, {'default': True})781        # apply timesteps, if present782        new_c = set_timesteps_for_conditioning(cond=new_c, timestep_range=timesteps_range)783        # finally, combine with existing conditioning and store784        combined_conds.append(combine_conditioning([c, new_c]))785    return combined_conds786