147 lines
4.9 KiB
Python
147 lines
4.9 KiB
Python
# SimpleSyrup - workflow-focused ComfyUI extensions for image generation
|
|
# Copyright (C) 2026 Artificial Sweetener and contributors
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
"""Own lifecycle-safe derivation of ComfyUI MODEL and CLIP values."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterable
|
|
from typing import Protocol, TypeVar, cast
|
|
|
|
from .clip_patcher_model_alignment import align_clip_text_encoder_with_patcher
|
|
|
|
|
|
class ModelMutation(Protocol):
|
|
"""Apply one supported mutation to an already-derived MODEL patcher."""
|
|
|
|
def apply(self, model: object) -> None:
|
|
"""Apply the mutation through ComfyUI's public patcher API."""
|
|
|
|
|
|
class ClipMutation(Protocol):
|
|
"""Apply one supported mutation to an already-derived CLIP value."""
|
|
|
|
def apply(self, clip: object) -> None:
|
|
"""Apply the mutation through ComfyUI's public CLIP API."""
|
|
|
|
|
|
PatcherValue = TypeVar("PatcherValue")
|
|
|
|
|
|
class ComfyPatcherLifecycle:
|
|
"""Derive Comfy patchers while preserving their source lineage."""
|
|
|
|
def derive_model(
|
|
self,
|
|
source: PatcherValue,
|
|
mutations: Iterable[ModelMutation],
|
|
*,
|
|
operation: str,
|
|
) -> PatcherValue:
|
|
"""Clone one MODEL, verify its lineage, and apply all mutations."""
|
|
|
|
derived = self._clone(source, operation=operation)
|
|
self._require_direct_parent(source, derived, operation=operation)
|
|
for mutation in mutations:
|
|
mutation.apply(derived)
|
|
return derived
|
|
|
|
def derive_clip(
|
|
self,
|
|
source: PatcherValue,
|
|
mutations: Iterable[ClipMutation],
|
|
*,
|
|
operation: str,
|
|
disable_dynamic: bool = False,
|
|
) -> PatcherValue:
|
|
"""Clone one CLIP, verify patcher lineage, and apply all mutations."""
|
|
|
|
clone = getattr(source, "clone", None)
|
|
if not callable(clone):
|
|
raise TypeError(f"{operation} requires a cloneable CLIP value.")
|
|
derived = cast(
|
|
PatcherValue,
|
|
clone(disable_dynamic=disable_dynamic) if disable_dynamic else clone(),
|
|
)
|
|
if derived is source:
|
|
raise RuntimeError(f"{operation} returned the source CLIP from clone().")
|
|
|
|
source_patcher = self._required_clip_patcher(source, value_name="source CLIP")
|
|
derived_patcher = self._required_clip_patcher(
|
|
derived,
|
|
value_name="derived CLIP",
|
|
)
|
|
self._require_direct_parent(
|
|
source_patcher,
|
|
derived_patcher,
|
|
operation=operation,
|
|
)
|
|
align_clip_text_encoder_with_patcher(derived)
|
|
for mutation in mutations:
|
|
mutation.apply(derived)
|
|
return derived
|
|
|
|
def preserve_vae(self, vae: PatcherValue, *, operation: str) -> PatcherValue:
|
|
"""Return an unmodified VAE and make the no-derivation contract explicit."""
|
|
|
|
if not operation.strip():
|
|
raise ValueError("VAE lifecycle operations require a descriptive name.")
|
|
return vae
|
|
|
|
def clone_hooks(
|
|
self,
|
|
source: PatcherValue,
|
|
*,
|
|
operation: str,
|
|
) -> PatcherValue:
|
|
"""Clone one HookGroup-like value through the lifecycle authority."""
|
|
|
|
if not isinstance(operation, str) or not operation.strip():
|
|
raise ValueError("Hook lifecycle operations require a description.")
|
|
clone = getattr(source, "clone", None)
|
|
if not callable(clone):
|
|
raise TypeError(f"{operation} requires cloneable hooks.")
|
|
derived = cast(PatcherValue, clone())
|
|
if derived is source:
|
|
raise RuntimeError(f"{operation} returned the source hooks from clone().")
|
|
return derived
|
|
|
|
@staticmethod
|
|
def _clone(source: PatcherValue, *, operation: str) -> PatcherValue:
|
|
"""Clone one MODEL through its ComfyUI boundary."""
|
|
|
|
clone = getattr(source, "clone", None)
|
|
if not callable(clone):
|
|
raise TypeError(f"{operation} requires a cloneable MODEL value.")
|
|
derived = cast(PatcherValue, clone())
|
|
if derived is source:
|
|
raise RuntimeError(f"{operation} returned the source MODEL from clone().")
|
|
return derived
|
|
|
|
@staticmethod
|
|
def _require_direct_parent(
|
|
source: object,
|
|
derived: object,
|
|
*,
|
|
operation: str,
|
|
) -> None:
|
|
"""Require Comfy's parent link used by loaded-model cleanup."""
|
|
|
|
if getattr(derived, "parent", None) is not source:
|
|
raise RuntimeError(
|
|
f"{operation} produced a derived patcher without its source as parent."
|
|
)
|
|
|
|
@staticmethod
|
|
def _required_clip_patcher(value: object, *, value_name: str) -> object:
|
|
"""Return the CLIP patcher required for lineage validation."""
|
|
|
|
patcher = getattr(value, "patcher", None)
|
|
if patcher is None:
|
|
raise TypeError(f"{value_name} does not expose patcher.")
|
|
return patcher
|
|
|
|
|
|
PATCHER_LIFECYCLE = ComfyPatcherLifecycle()
|