mirror of
https://github.com/markuryy/comfyui-conditioning-converter.git
synced 2026-06-25 18:11:02 +00:00
initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
3
__init__.py
Normal file
3
__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||||
|
|
||||||
|
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||||
228
nodes.py
Normal file
228
nodes.py
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import torch
|
||||||
|
import torch.nn.functional as F
|
||||||
|
|
||||||
|
import comfy.sd1_clip
|
||||||
|
import comfy.model_management
|
||||||
|
|
||||||
|
|
||||||
|
PRESETS = {
|
||||||
|
"custom": None,
|
||||||
|
"klein_9b_to_krea2": (3, 4096, 12, 2560),
|
||||||
|
"klein_4b_to_krea2": (3, 2560, 12, 2560),
|
||||||
|
"krea2_to_klein_9b": (12, 2560, 3, 4096),
|
||||||
|
"flux2_mistral_to_krea2": (3, 5120, 12, 2560),
|
||||||
|
}
|
||||||
|
|
||||||
|
METHODS = ["interpolate", "repeat_nearest", "truncate_pad"]
|
||||||
|
|
||||||
|
# (layer_indices, target_hidden_dim or 0 for "keep native")
|
||||||
|
ENCODE_PRESETS = {
|
||||||
|
"klein_for_krea2": ([2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35], 2560),
|
||||||
|
"krea2_12_layers": ([2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35], 0),
|
||||||
|
"klein_3_layers": ([9, 18, 27], 0),
|
||||||
|
"flux2_3_layers": ([10, 20, 30], 0),
|
||||||
|
"uniform_6_layers": ([5, 11, 17, 23, 29, 35], 0),
|
||||||
|
"deep_3_layers": ([24, 30, 35], 0),
|
||||||
|
"custom": ([], 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _resize_dim(tensor, dim, old_size, new_size, method):
|
||||||
|
if method == "truncate_pad":
|
||||||
|
if new_size <= old_size:
|
||||||
|
slices = [slice(None)] * tensor.ndim
|
||||||
|
slices[dim] = slice(0, new_size)
|
||||||
|
return tensor[tuple(slices)]
|
||||||
|
pad_size = new_size - old_size
|
||||||
|
pad_shape = list(tensor.shape)
|
||||||
|
pad_shape[dim] = pad_size
|
||||||
|
padding = torch.zeros(pad_shape, dtype=tensor.dtype, device=tensor.device)
|
||||||
|
return torch.cat([tensor, padding], dim=dim)
|
||||||
|
|
||||||
|
if method == "repeat_nearest":
|
||||||
|
repeats = -(-new_size // old_size)
|
||||||
|
tensor = tensor.repeat_interleave(repeats, dim=dim)
|
||||||
|
slices = [slice(None)] * tensor.ndim
|
||||||
|
slices[dim] = slice(0, new_size)
|
||||||
|
return tensor[tuple(slices)]
|
||||||
|
|
||||||
|
perm = list(range(tensor.ndim))
|
||||||
|
perm.remove(dim)
|
||||||
|
perm.append(dim)
|
||||||
|
t = tensor.permute(*perm)
|
||||||
|
original_shape = t.shape
|
||||||
|
t = t.reshape(-1, 1, old_size)
|
||||||
|
t = F.interpolate(t, size=new_size, mode="linear", align_corners=False)
|
||||||
|
new_shape = list(original_shape[:-1]) + [new_size]
|
||||||
|
t = t.reshape(*new_shape)
|
||||||
|
|
||||||
|
inv_perm = [0] * tensor.ndim
|
||||||
|
for i, p in enumerate(perm):
|
||||||
|
inv_perm[p] = i
|
||||||
|
return t.permute(*inv_perm)
|
||||||
|
|
||||||
|
|
||||||
|
class ConditioningConverter:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"conditioning": ("CONDITIONING",),
|
||||||
|
"preset": (list(PRESETS.keys()),),
|
||||||
|
"source_layers": ("INT", {"default": 3, "min": 1, "max": 64}),
|
||||||
|
"source_hidden_dim": ("INT", {"default": 4096, "min": 1, "max": 65536}),
|
||||||
|
"target_layers": ("INT", {"default": 12, "min": 1, "max": 64}),
|
||||||
|
"target_hidden_dim": ("INT", {"default": 2560, "min": 1, "max": 65536}),
|
||||||
|
"method": (METHODS,),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("CONDITIONING",)
|
||||||
|
FUNCTION = "convert"
|
||||||
|
CATEGORY = "model/conditioning/transform"
|
||||||
|
|
||||||
|
def convert(self, conditioning, preset, source_layers, source_hidden_dim,
|
||||||
|
target_layers, target_hidden_dim, method):
|
||||||
|
if preset != "custom":
|
||||||
|
source_layers, source_hidden_dim, target_layers, target_hidden_dim = PRESETS[preset]
|
||||||
|
|
||||||
|
source_features = source_layers * source_hidden_dim
|
||||||
|
target_features = target_layers * target_hidden_dim
|
||||||
|
|
||||||
|
if source_features == target_features:
|
||||||
|
return (conditioning,)
|
||||||
|
|
||||||
|
out = []
|
||||||
|
for t in conditioning:
|
||||||
|
cond_tensor = t[0]
|
||||||
|
meta = t[1].copy()
|
||||||
|
|
||||||
|
B, seq, features = cond_tensor.shape
|
||||||
|
if features != source_features:
|
||||||
|
raise ValueError(
|
||||||
|
f"ConditioningConverter: expected feature dim {source_features} "
|
||||||
|
f"({source_layers}×{source_hidden_dim}) but got {features}. "
|
||||||
|
f"Check source_layers and source_hidden_dim."
|
||||||
|
)
|
||||||
|
|
||||||
|
tensor = cond_tensor.reshape(B, seq, source_layers, source_hidden_dim)
|
||||||
|
|
||||||
|
if source_hidden_dim != target_hidden_dim:
|
||||||
|
tensor = _resize_dim(tensor, 3, source_hidden_dim, target_hidden_dim, method)
|
||||||
|
|
||||||
|
if source_layers != target_layers:
|
||||||
|
tensor = _resize_dim(tensor, 2, source_layers, target_layers, method)
|
||||||
|
|
||||||
|
tensor = tensor.reshape(B, seq, target_features)
|
||||||
|
out.append([tensor, meta])
|
||||||
|
|
||||||
|
return (out,)
|
||||||
|
|
||||||
|
|
||||||
|
class FlexibleCLIPTextEncode:
|
||||||
|
"""Encode text with a custom layer tap list, bypassing the model's
|
||||||
|
hardcoded layer reshape. Produces conditioning with real hidden
|
||||||
|
states from arbitrary transformer layers."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"clip": ("CLIP",),
|
||||||
|
"text": ("STRING", {"multiline": True, "dynamicPrompts": True}),
|
||||||
|
"preset": (list(ENCODE_PRESETS.keys()),),
|
||||||
|
"custom_layers": ("STRING", {
|
||||||
|
"default": "2,5,8,11,14,17,20,23,26,29,32,35",
|
||||||
|
"tooltip": "Comma-separated layer indices (used when preset is 'custom')",
|
||||||
|
}),
|
||||||
|
"target_hidden_dim": ("INT", {
|
||||||
|
"default": 0, "min": 0, "max": 65536,
|
||||||
|
"tooltip": "Project per-layer hidden dim to this size. 0 = use preset value or keep native.",
|
||||||
|
}),
|
||||||
|
"projection_method": (["interpolate", "truncate_pad"],),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("CONDITIONING",)
|
||||||
|
FUNCTION = "encode"
|
||||||
|
CATEGORY = "model/conditioning/transform"
|
||||||
|
|
||||||
|
def encode(self, clip, text, preset, custom_layers,
|
||||||
|
target_hidden_dim, projection_method):
|
||||||
|
preset_layers, preset_hidden = ENCODE_PRESETS[preset]
|
||||||
|
|
||||||
|
if preset == "custom":
|
||||||
|
layer_list = [int(x.strip()) for x in custom_layers.split(",") if x.strip()]
|
||||||
|
else:
|
||||||
|
layer_list = list(preset_layers)
|
||||||
|
|
||||||
|
if target_hidden_dim == 0:
|
||||||
|
target_hidden_dim = preset_hidden
|
||||||
|
|
||||||
|
if not layer_list:
|
||||||
|
raise ValueError("FlexibleCLIPTextEncode: layer list is empty.")
|
||||||
|
|
||||||
|
clip = clip.clone()
|
||||||
|
cond_stage = clip.cond_stage_model
|
||||||
|
|
||||||
|
if not isinstance(cond_stage, comfy.sd1_clip.SD1ClipModel):
|
||||||
|
raise TypeError(
|
||||||
|
f"FlexibleCLIPTextEncode: expected an SD1ClipModel-based encoder, "
|
||||||
|
f"got {type(cond_stage).__name__}. This node works with single-encoder "
|
||||||
|
f"models (Klein, Flux2, Krea2)."
|
||||||
|
)
|
||||||
|
|
||||||
|
inner_clip = getattr(cond_stage, cond_stage.clip)
|
||||||
|
original_layer = inner_clip.layer
|
||||||
|
|
||||||
|
tokens = clip.tokenize(text)
|
||||||
|
clip.load_model(tokens)
|
||||||
|
device = clip.patcher.load_device
|
||||||
|
inner_clip.execution_device = device
|
||||||
|
|
||||||
|
inner_clip.layer = layer_list
|
||||||
|
try:
|
||||||
|
with comfy.model_management.cuda_device_context(device):
|
||||||
|
out = comfy.sd1_clip.SD1ClipModel.encode_token_weights(cond_stage, tokens)
|
||||||
|
finally:
|
||||||
|
inner_clip.layer = original_layer
|
||||||
|
inner_clip.execution_device = None
|
||||||
|
|
||||||
|
z = out[0]
|
||||||
|
pooled = out[1] if len(out) > 1 else None
|
||||||
|
extra = out[2] if len(out) > 2 else {}
|
||||||
|
|
||||||
|
if z.ndim == 4:
|
||||||
|
B, n_layers, seq, hidden = z.shape
|
||||||
|
z = z.permute(0, 2, 1, 3) # (B, seq, n_layers, hidden)
|
||||||
|
|
||||||
|
if target_hidden_dim > 0 and target_hidden_dim != hidden:
|
||||||
|
z = _resize_dim(z, 3, hidden, target_hidden_dim, projection_method)
|
||||||
|
hidden = target_hidden_dim
|
||||||
|
|
||||||
|
z = z.reshape(B, seq, n_layers * hidden)
|
||||||
|
elif z.ndim == 3 and target_hidden_dim > 0:
|
||||||
|
B, seq, hidden = z.shape
|
||||||
|
if target_hidden_dim != hidden:
|
||||||
|
z = z.unsqueeze(2)
|
||||||
|
z = _resize_dim(z, 3, hidden, target_hidden_dim, projection_method)
|
||||||
|
z = z.squeeze(2)
|
||||||
|
|
||||||
|
pooled_dict = {}
|
||||||
|
if pooled is not None:
|
||||||
|
pooled_dict["pooled_output"] = pooled
|
||||||
|
if isinstance(extra, dict) and "attention_mask" in extra:
|
||||||
|
pooled_dict["attention_mask"] = extra["attention_mask"]
|
||||||
|
|
||||||
|
return ([[z, pooled_dict]],)
|
||||||
|
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"ConditioningConverter": ConditioningConverter,
|
||||||
|
"FlexibleCLIPTextEncode": FlexibleCLIPTextEncode,
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"ConditioningConverter": "Conditioning Converter (Experimental)",
|
||||||
|
"FlexibleCLIPTextEncode": "Flexible CLIP Text Encode (Experimental)",
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user