Files
2026-02-20 00:28:25 +07:00

212 lines
7.9 KiB
Python

import torch
import comfy.sd
import comfy.utils
import folder_paths
import os
import json
def get_presets():
presets_path = os.path.join(os.path.dirname(__file__), "Prompt_presets", "base_preset.json")
if not os.path.exists(presets_path):
return {}
try:
with open(presets_path, "r", encoding="utf-8") as f:
data = json.load(f)
return {item["name"]: item for item in data}
except Exception as e:
print(f"Error loading presets: {e}")
return {}
class DJ_cliploader:
@classmethod
def INPUT_TYPES(s):
return {"required": {"clip_name": (folder_paths.get_filename_list("clip"), )}}
RETURN_TYPES = ("CLIP",)
FUNCTION = "load_dual_clip"
CATEGORY = "custom_loaders"
def load_dual_clip(self, clip_name):
clip_path = folder_paths.get_full_path("clip", clip_name)
sd = comfy.utils.load_torch_file(clip_path)
# DEBUG: Print keys to console to diagnose mismatches
print(f"DEBUG: Total keys in file: {len(sd)}")
if len(sd) > 0:
print(f"DEBUG: First 5 keys: {list(sd.keys())[:5]}")
# 1. SPLIT BY PREFIX: Identify CLIP-L vs CLIP-G
sd_l = {}
sd_g = {}
for k, v in sd.items():
# CLIP-L Logic
if "conditioner.embedders.1" in k:
# Standard SDXL checkpoint format
if "conditioner.embedders.1.model." in k:
new_k = k.replace("conditioner.embedders.1.model.", "")
else:
new_k = k.replace("conditioner.embedders.1.", "")
# Fix for OpenCLIP format missing 'transformer.' prefix for blocks
if new_k.startswith("resblocks."):
new_k = "transformer." + new_k
sd_l[new_k] = v
elif "clip_l." in k:
# Extracted with simple prefixes
new_k = k.replace("clip_l.", "")
if new_k.startswith("resblocks."):
new_k = "transformer." + new_k
sd_l[new_k] = v
# CLIP-G Logic
elif "conditioner.embedders.0" in k:
# Standard SDXL checkpoint format
if "conditioner.embedders.0.model." in k:
new_k = k.replace("conditioner.embedders.0.model.", "")
else:
new_k = k.replace("conditioner.embedders.0.", "")
# Fix for CLIP-G having extra 'transformer.' prefix
if new_k.startswith("transformer."):
new_k = new_k.replace("transformer.", "")
sd_g[new_k] = v
elif "clip_g." in k:
# Extracted with simple prefixes
new_k = k.replace("clip_g.", "")
if new_k.startswith("transformer."):
new_k = new_k.replace("transformer.", "")
sd_g[new_k] = v
print(f"DEBUG: keys in sd_l: {len(sd_l)}")
if len(sd_l) > 0: print(f"DEBUG: sd_l sample: {list(sd_l.keys())[:3]}")
print(f"DEBUG: keys in sd_g: {len(sd_g)}")
if len(sd_g) > 0: print(f"DEBUG: sd_g sample: {list(sd_g.keys())[:3]}")
# 2. INJECT MISSING KEYS FOR CLIP-L (Bypass strict check)
# CLIP-L (sd_l) typically lacks these keys, but generic loading might expect them
# Only inject if we actually have a CLIP-L (sd_l is not empty)
if len(sd_l) > 0:
if "text_projection.weight" not in sd_l and "text_projection" not in sd_l:
sd_l["text_projection.weight"] = torch.eye(768, dtype=torch.float32)
if "logit_scale" not in sd_l:
sd_l["logit_scale"] = torch.tensor(4.6055, dtype=torch.float32)
# 3. LOAD USING CORRECT API
# We pass both dictionaries; ComfyUI will detect TEModel.CLIP_L and TEModel.CLIP_G
# and construct an SDXLClipModel or appropriate equivalent.
clip = comfy.sd.load_text_encoder_state_dicts(
[sd_l, sd_g],
embedding_directory=folder_paths.get_folder_paths("embeddings"),
clip_type=comfy.sd.CLIPType.STABLE_DIFFUSION
)
return (clip,)
class DJ_V_Prediction:
parameterization_options = ["epsilon", "v_prediction"] # Both options in the dropdown
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL",),
"parameterization": (s.parameterization_options,), # Required input
}
}
RETURN_TYPES = ("MODEL",)
FUNCTION = "add_parameterization"
CATEGORY = "advanced/model"
def add_parameterization(self, model, parameterization):
m = model.clone()
if parameterization == "v_prediction":
internal_parameterization = "v" # Set internal value to "v"
setattr(m.model, "parameterization", internal_parameterization)
from comfy.model_sampling import V_PREDICTION, ModelSamplingDiscrete
class ModelSamplingAdvanced(ModelSamplingDiscrete, V_PREDICTION):
pass
m.add_object_patch("model_sampling", ModelSamplingAdvanced(m.model.model_config))
# No 'elif' for epsilon - this is the key for the bypass
return (m,) # Return the (potentially modified) model
class DJ_PromptPresets:
@classmethod
@classmethod
def INPUT_TYPES(s):
presets = get_presets()
preset_names = ["None"] + list(presets.keys())
return {
"required": {
"clip": ("CLIP",),
"preset_1": (preset_names,),
"preset_2": (preset_names,),
"preset_3": (preset_names,),
"preset_4": (preset_names,),
},
"optional": {
"generated_positive": ("STRING", {"multiline": True, "default": "", "forceInput": False}),
"generated_negative": ("STRING", {"multiline": True, "default": "", "forceInput": False}),
}
}
RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "STRING", "STRING")
RETURN_NAMES = ("POSITIVE", "NEGATIVE", "pos_text", "neg_text")
FUNCTION = "apply_presets"
CATEGORY = "DJ_nodes/Prompting"
def apply_presets(self, clip, preset_1, preset_2, preset_3, preset_4, generated_positive="", generated_negative=""):
presets = get_presets()
selected_presets = [preset_1, preset_2, preset_3, preset_4]
final_pos_str = ""
final_neg_str = ""
# Concatenate strings first
for p_name in selected_presets:
if p_name == "None" or p_name not in presets:
continue
p_data = presets[p_name]
p_pos = p_data.get("prompt", "").strip()
p_neg = p_data.get("negative_prompt", "").strip()
if p_pos:
final_pos_str = (final_pos_str + ", " + p_pos) if final_pos_str else p_pos
if p_neg:
final_neg_str = (final_neg_str + ", " + p_neg) if final_neg_str else p_neg
def encode_text(text, clip):
tokens = clip.tokenize(text)
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
return [[cond, {"pooled_output": pooled}]]
# Encode once
pos_cond = encode_text(final_pos_str, clip)
neg_cond = encode_text(final_neg_str, clip)
return {
"ui": {
"generated_positive": [final_pos_str],
"generated_negative": [final_neg_str]
},
"result": (pos_cond, neg_cond, final_pos_str, final_neg_str)
}
# Ensure nodes are registered for ComfyUI
NODE_CLASS_MAPPINGS = {
"DJ_V_Prediction": DJ_V_Prediction,
"DJ_cliploader": DJ_cliploader,
"DJ_PromptPresets": DJ_PromptPresets
}
NODE_DISPLAY_NAME_MAPPINGS = {
"DJ_V_Prediction": "DJ V Prediction Param",
"DJ_cliploader": "DJ Load SDXL CLIPs",
"DJ_PromptPresets": "DJ Prompt Presets"
}