176 lines
6.1 KiB
Python
176 lines
6.1 KiB
Python
# (c) godmt || MIT License (https://mit-license.org/)
|
|
# The original project page (https://ip-composer.github.io/IP-Composer/)
|
|
import torch
|
|
import safetensors.torch as st
|
|
import numpy as np
|
|
import open_clip
|
|
import folder_paths
|
|
from pathlib import Path
|
|
|
|
MODEL_DIR_NAME = "ip_concept_subspaces"
|
|
models_dir = Path(__file__).parent.parent.parent / "models"
|
|
root_path = models_dir.resolve() / MODEL_DIR_NAME
|
|
if not root_path.exists():
|
|
root_path.mkdir(parents=True, exist_ok=True)
|
|
folder_paths.folder_names_and_paths[MODEL_DIR_NAME] = ([str(root_path)], [".safetensors"])
|
|
|
|
|
|
CLIP_VISIONS = {
|
|
"ViT-H-14": "laion/CLIP-ViT-H-14-laion2B-s32B-b79K",
|
|
"ViT-bigG-14": "laion/CLIP-ViT-bigG-14-laion2B-39B-b160k"
|
|
}
|
|
|
|
|
|
class IPLoadCLIPVision:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
clip_vision_names = list(CLIP_VISIONS.keys())
|
|
return {"required": {
|
|
"clip_vision": (clip_vision_names,),
|
|
"device": ("STRING", {"default": "cuda:0"})
|
|
}}
|
|
RETURN_TYPES = ("OPEN_CLIP", )
|
|
CATEGORY = "ip_composer"
|
|
TITLE = "IP-Comp Load CLIP Vision"
|
|
FUNCTION = "execute"
|
|
|
|
def execute(self, clip_vision, device):
|
|
hf_name = CLIP_VISIONS[clip_vision]
|
|
model, _, preprocess = open_clip.create_model_and_transforms(f"hf-hub:{hf_name}")
|
|
model.eval()
|
|
model.to(device)
|
|
tokenizer = open_clip.get_tokenizer(f"hf-hub:{hf_name}")
|
|
return ({
|
|
"hf_name": hf_name,
|
|
"device": device,
|
|
"model": model,
|
|
"preprocess": preprocess,
|
|
"tokenizer": tokenizer
|
|
},)
|
|
|
|
|
|
class IPConceptSubspace:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {
|
|
"open_clip_model": ("OPEN_CLIP",),
|
|
"descriptions": ("STRING", {"multiline": True}),
|
|
"rank": ("INT", {"default": 30, "min": 1, "max": 1024}),
|
|
"batch_size": ("INT", {"default": 100, "min": 1, "max": 512})
|
|
}}
|
|
RETURN_TYPES = ("CONCEPT_SUBSPACE",)
|
|
RETURN_NAMES = ("concept_subspace",)
|
|
CATEGORY = "ip_composer"
|
|
TITLE = "IP-Comp Concept Subspace"
|
|
FUNCTION = "execute"
|
|
|
|
def execute(self, open_clip_model, descriptions, rank, batch_size):
|
|
device = open_clip_model["device"]
|
|
model = open_clip_model["model"]
|
|
tokenizer = open_clip_model["tokenizer"]
|
|
# 1. create list of concept descriptions
|
|
lines = [l.strip() for l in descriptions.split("\n") if l.strip()]
|
|
# 2. encode concept descriptions
|
|
with torch.no_grad():
|
|
embs = []
|
|
# batch encoding
|
|
for i in range(0, len(lines), batch_size):
|
|
batch_desc = lines[i:i + batch_size]
|
|
texts = tokenizer(batch_desc).to(device)
|
|
batch_embeddings = model.encode_text(texts)
|
|
# TODO is it necessary to convert to numpy?
|
|
batch_embeddings = batch_embeddings.detach().cpu().numpy()
|
|
embs.append(batch_embeddings)
|
|
del texts, batch_embeddings
|
|
torch.cuda.empty_cache()
|
|
embs = np.vstack(embs)
|
|
# Perform SVD on the combined matrix
|
|
_, _, v = np.linalg.svd(embs, full_matrices=False)
|
|
# specify the recommended top `rank` singular vectors to construct the concept subspace
|
|
concept_subspace = {
|
|
"metadata": {"rank": str(rank), "clip_vision_model": open_clip_model["hf_name"]},
|
|
"concept_subspace": torch.from_numpy(v),
|
|
}
|
|
return (concept_subspace,)
|
|
|
|
|
|
class IPSaveConceptSubspace:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {
|
|
"concept_subspace": ("CONCEPT_SUBSPACE",),
|
|
"concept_name": ("STRING", {"default": "concept"})
|
|
}}
|
|
RETURN_TYPES = ()
|
|
CATEGORY = "ip_composer"
|
|
TITLE = "IP-Comp Save Concept Subspace"
|
|
FUNCTION = "execute"
|
|
OUTPUT_NODE = True
|
|
|
|
def execute(self, concept_subspace, concept_name):
|
|
file_path = str(root_path / f"{concept_name}.safetensors")
|
|
st.save_file({"concept_subspace": concept_subspace["concept_subspace"].cpu()}, file_path, metadata=concept_subspace["metadata"])
|
|
return ()
|
|
|
|
|
|
class IPLoadConceptSubspace:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
concept_names = folder_paths.get_filename_list(MODEL_DIR_NAME)
|
|
return {"required": {
|
|
"concept_name": (concept_names,),
|
|
}}
|
|
RETURN_TYPES = ("CONCEPT_SUBSPACE",)
|
|
RETURN_NAMES = ("concept_subspace",)
|
|
CATEGORY = "ip_composer"
|
|
TITLE = "IP-Comp Load Concept Subspace"
|
|
FUNCTION = "execute"
|
|
|
|
def execute(self, concept_name):
|
|
file_path = folder_paths.get_full_path(MODEL_DIR_NAME, concept_name)
|
|
with st.safe_open(file_path, framework="pt", device="cpu") as f:
|
|
# Load the metadata
|
|
metadata = f.metadata()
|
|
# Load the tensor
|
|
concept_subspace = f.get_tensor("concept_subspace")
|
|
|
|
concept_subspace = {
|
|
"metadata": metadata,
|
|
"concept_subspace": concept_subspace,
|
|
}
|
|
|
|
return (concept_subspace,)
|
|
|
|
|
|
class IPConceptMerge:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {
|
|
"ref_embed": ("EMBEDS",),
|
|
"concept_embed": ("EMBEDS",),
|
|
"concept_subspace": ("CONCEPT_SUBSPACE",),
|
|
"rank": ("INT", {"default": -1, "min": -1, "max": 1024})
|
|
}}
|
|
RETURN_TYPES = ("EMBEDS",) # merged embeddings
|
|
CATEGORY = "ip_composer"
|
|
TITLE = "IP-Comp Concept Merge"
|
|
FUNCTION = "execute"
|
|
|
|
def execute(self, ref_embed, concept_embed, concept_subspace, rank):
|
|
if rank == -1:
|
|
rank = int(concept_subspace["metadata"]["rank"])
|
|
P = concept_subspace["concept_subspace"][:rank, :]
|
|
P = P.T @ P # concept projection matrix, (1024, 1024)
|
|
# merge concept
|
|
e_mix = ref_embed - ref_embed @ P + concept_embed @ P
|
|
return (e_mix,)
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"IPCompLoadOpenCLIP": IPLoadCLIPVision,
|
|
"IPCompConceptSubspace": IPConceptSubspace,
|
|
"IPCompConceptMerge": IPConceptMerge,
|
|
"IPSaveConceptSubspace": IPSaveConceptSubspace,
|
|
"IPLoadConceptSubspace": IPLoadConceptSubspace
|
|
}
|