Files
2026-03-24 13:20:34 -05:00

145 lines
4.9 KiB
Python

import os
import re
from dataclasses import dataclass
import safetensors.torch
import torch
from safetensors import safe_open
@dataclass
class EmbeddingBundle:
clip_l: torch.Tensor | None
clip_g: torch.Tensor | None
metadata: dict[str, str]
source_path: str
@property
def token_count(self) -> int:
if self.clip_l is not None:
return int(self.clip_l.shape[0])
if self.clip_g is not None:
return int(self.clip_g.shape[0])
return 0
@property
def is_sdxl(self) -> bool:
return self.clip_l is not None and self.clip_g is not None
def load_embedding_bundle(embedding_path: str) -> EmbeddingBundle:
if embedding_path.lower().endswith((".safetensors", ".sft")):
return _load_safetensors_embedding(embedding_path)
return _load_pt_embedding(embedding_path)
def normalize_output_name(output_name: str, fallback_stem: str) -> str:
name = (output_name or "").strip()
if not name:
name = fallback_stem
stem = os.path.splitext(os.path.basename(name))[0]
return normalize_output_component(stem, fallback_stem)
def normalize_output_component(name: str, fallback_component: str) -> str:
component = re.sub(r"[^A-Za-z0-9._-]+", "_", (name or "").strip()).strip("._")
return component or fallback_component
def build_output_path(output_dir: str, output_name: str, fallback_stem: str, overwrite: bool) -> tuple[str, str]:
embedding_name = normalize_output_name(output_name, fallback_stem)
output_path = os.path.join(output_dir, f"{embedding_name}.safetensors")
if os.path.exists(output_path) and not overwrite:
raise FileExistsError(f"Output embedding already exists: {output_path}")
return output_path, embedding_name
def save_sdxl_embedding(
output_path: str,
clip_l: torch.Tensor,
clip_g: torch.Tensor,
metadata: dict[str, str],
) -> None:
os.makedirs(os.path.dirname(output_path), exist_ok=True)
safetensors.torch.save_file(
{"clip_l": clip_l.contiguous().cpu(), "clip_g": clip_g.contiguous().cpu()},
output_path,
metadata=metadata,
)
def _load_safetensors_embedding(embedding_path: str) -> EmbeddingBundle:
tensors = {}
with safe_open(embedding_path, framework="pt", device="cpu") as handle:
metadata = handle.metadata() or {}
for key in handle.keys():
tensors[key] = handle.get_tensor(key).clone()
clip_l = _coerce_tensor(tensors.get("clip_l"))
clip_g = _coerce_tensor(tensors.get("clip_g"))
if clip_l is None and clip_g is None:
inferred_key, inferred_tensor = _infer_single_tensor(tensors)
if inferred_key == "clip_l":
clip_l = inferred_tensor
elif inferred_key == "clip_g":
clip_g = inferred_tensor
return EmbeddingBundle(clip_l=clip_l, clip_g=clip_g, metadata=dict(metadata), source_path=embedding_path)
def _load_pt_embedding(embedding_path: str) -> EmbeddingBundle:
import comfy.utils
payload = comfy.utils.load_torch_file(embedding_path, safe_load=True, device=torch.device("cpu"))
metadata = {}
clip_l = None
clip_g = None
if isinstance(payload, dict):
for key in ("name", "step", "sd_checkpoint", "sd_checkpoint_name"):
value = payload.get(key)
if value is not None:
metadata[f"legacy.{key}"] = str(value)
if "clip_l" in payload:
clip_l = _coerce_tensor(payload["clip_l"])
if "clip_g" in payload:
clip_g = _coerce_tensor(payload["clip_g"])
string_to_param = payload.get("string_to_param")
if isinstance(string_to_param, dict) and (clip_l is None and clip_g is None):
first_tensor = next(iter(string_to_param.values()), None)
inferred = _coerce_tensor(first_tensor)
if inferred is not None:
if inferred.shape[-1] == 768:
clip_l = inferred
elif inferred.shape[-1] == 1280:
clip_g = inferred
if clip_l is None and clip_g is None:
raise RuntimeError(f"Unsupported embedding format: {embedding_path}")
return EmbeddingBundle(clip_l=clip_l, clip_g=clip_g, metadata=metadata, source_path=embedding_path)
def _infer_single_tensor(tensors: dict[str, torch.Tensor]) -> tuple[str | None, torch.Tensor | None]:
for tensor in tensors.values():
inferred = _coerce_tensor(tensor)
if inferred is None:
continue
if inferred.shape[-1] == 768:
return "clip_l", inferred
if inferred.shape[-1] == 1280:
return "clip_g", inferred
return None, None
def _coerce_tensor(tensor: torch.Tensor | None) -> torch.Tensor | None:
if tensor is None or not torch.is_tensor(tensor):
return None
if tensor.ndim == 1:
return tensor.view(1, -1)
if tensor.ndim >= 2:
return tensor.reshape(-1, tensor.shape[-1])
return None