Updated protocol version and added error handling for packet size limits. Enhanced connection handling with new timeout and retry parameters.
550 lines
23 KiB
Python
550 lines
23 KiB
Python
import os
|
|
import math
|
|
import json
|
|
import time
|
|
import hmac
|
|
import socket
|
|
import struct
|
|
import threading
|
|
from collections import OrderedDict
|
|
import torch
|
|
import folder_paths
|
|
import comfy.sd
|
|
import comfy.utils
|
|
DEFAULT_PORT = 8181
|
|
PROTOCOL_VERSION = 3
|
|
HEADER_LEN_FMT = ">Q"
|
|
HEADER_LEN_SIZE = struct.calcsize(HEADER_LEN_FMT)
|
|
SOCKET_TIMEOUT = 120
|
|
GENERATE_TIMEOUT = 3600
|
|
CONNECT_RETRIES = 3
|
|
CONNECT_BACKOFF = 1.0
|
|
RECV_CHUNK = 1 << 20
|
|
MAX_HEADER_BYTES = 8 * 1024 * 1024
|
|
MAX_BLOB_BYTES = 512 * 1024 * 1024
|
|
PATCHED_CLIP_CACHE = 4
|
|
ALLOWED_DTYPES = {
|
|
"torch.float32": torch.float32,
|
|
"torch.float16": torch.float16,
|
|
"torch.bfloat16": torch.bfloat16,
|
|
"torch.int64": torch.int64,
|
|
"torch.int32": torch.int32,
|
|
"torch.uint8": torch.uint8,
|
|
"torch.bool": torch.bool,
|
|
}
|
|
def log(msg):
|
|
print(f"[RemoteCLIP {time.strftime('%H:%M:%S')}] {msg}", flush=True)
|
|
_LOCAL_HOSTS = {"127.0.0.1", "localhost", "::1", "0.0.0.0"}
|
|
def resolve_transport_dtype(mode, ip):
|
|
if mode == "fp16":
|
|
return "torch.float16"
|
|
if mode == "fp32":
|
|
return "torch.float32"
|
|
is_local = ip in _LOCAL_HOSTS
|
|
return None if is_local else "torch.float16"
|
|
def _recv_exact(sock, size):
|
|
chunks = []
|
|
remaining = size
|
|
while remaining > 0:
|
|
chunk = sock.recv(min(remaining, RECV_CHUNK))
|
|
if not chunk:
|
|
raise ConnectionError("Socket closed while reading")
|
|
chunks.append(chunk)
|
|
remaining -= len(chunk)
|
|
return b"".join(chunks)
|
|
def send_packet(sock, header, blob=b""):
|
|
header_bytes = json.dumps(header).encode("utf-8")
|
|
if len(header_bytes) > MAX_HEADER_BYTES or len(blob) > MAX_BLOB_BYTES:
|
|
raise ValueError("Packet exceeds protocol size limits")
|
|
prefix = struct.pack(HEADER_LEN_FMT, len(header_bytes))
|
|
sock.sendall(prefix + header_bytes)
|
|
if blob:
|
|
sock.sendall(blob)
|
|
def recv_header(sock):
|
|
header_len = struct.unpack(HEADER_LEN_FMT, _recv_exact(sock, HEADER_LEN_SIZE))[0]
|
|
if header_len > MAX_HEADER_BYTES:
|
|
raise ValueError(f"Header too large: {header_len} bytes")
|
|
header = json.loads(_recv_exact(sock, header_len).decode("utf-8"))
|
|
if not isinstance(header, dict):
|
|
raise ValueError("Packet header must be an object")
|
|
return header
|
|
def recv_blob(sock, header):
|
|
blob_size = header.get("blob_size", 0)
|
|
if type(blob_size) is not int or not 0 <= blob_size <= MAX_BLOB_BYTES:
|
|
raise ValueError(f"Blob too large: {blob_size} bytes")
|
|
return _recv_exact(sock, blob_size) if blob_size else b""
|
|
def recv_packet(sock):
|
|
header = recv_header(sock)
|
|
blob = recv_blob(sock, header)
|
|
return header, blob
|
|
def _set_socket_opts(sock):
|
|
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
sock.settimeout(SOCKET_TIMEOUT)
|
|
def pack_tensors(tensors, transport_dtype=None):
|
|
meta = {}
|
|
blobs = []
|
|
offset = 0
|
|
for name, t in tensors.items():
|
|
if t is None:
|
|
raise ValueError(f"Tensor '{name}' is None")
|
|
t = t.detach().cpu()
|
|
orig_dtype = str(t.dtype)
|
|
if t.is_floating_point():
|
|
if transport_dtype is not None:
|
|
t = t.to(transport_dtype)
|
|
elif t.dtype not in (torch.float16, torch.float32):
|
|
t = t.to(torch.float32)
|
|
t = t.contiguous()
|
|
raw = t.numpy().tobytes()
|
|
meta[name] = {
|
|
"dtype": str(t.dtype),
|
|
"orig_dtype": orig_dtype,
|
|
"shape": list(t.shape),
|
|
"offset": offset,
|
|
"size": len(raw),
|
|
}
|
|
offset += len(raw)
|
|
blobs.append(raw)
|
|
return meta, b"".join(blobs)
|
|
def unpack_tensors(meta, blob):
|
|
out = {}
|
|
view = memoryview(blob)
|
|
for name, info in meta.items():
|
|
dtype_name = info["dtype"]
|
|
if dtype_name not in ALLOWED_DTYPES:
|
|
raise ValueError(f"Refusing to decode disallowed dtype: {dtype_name}")
|
|
dtype = ALLOWED_DTYPES[dtype_name]
|
|
start, size, shape = info["offset"], info["size"], info["shape"]
|
|
if (type(start) is not int or type(size) is not int or
|
|
start < 0 or size < 0 or start + size > len(blob)):
|
|
raise ValueError("Invalid tensor byte range")
|
|
if not isinstance(shape, list) or any(type(d) is not int or d < 0 for d in shape):
|
|
raise ValueError("Invalid tensor shape")
|
|
count = math.prod(shape)
|
|
if count * torch.empty((), dtype=dtype).element_size() != size:
|
|
raise ValueError("Tensor shape does not match byte size")
|
|
if count == 0:
|
|
t = torch.empty(shape, dtype=dtype)
|
|
else:
|
|
raw = bytearray(view[start:start + size])
|
|
t = torch.frombuffer(raw, dtype=dtype).reshape(shape).clone()
|
|
orig = info.get("orig_dtype")
|
|
if orig in ALLOWED_DTYPES and ALLOWED_DTYPES[orig] != dtype:
|
|
t = t.to(ALLOWED_DTYPES[orig])
|
|
out[name] = t
|
|
return out
|
|
def extract_tensors(obj, prefix, out):
|
|
# Tag every container so user dictionaries cannot mimic tensor references.
|
|
if isinstance(obj, torch.Tensor):
|
|
key = f"tensor_{len(out)}"
|
|
out[key] = obj
|
|
return {"kind": "tensor", "value": key}
|
|
if isinstance(obj, dict):
|
|
return {"kind": "dict", "value": [
|
|
[extract_tensors(k, prefix, out), extract_tensors(v, prefix, out)]
|
|
for k, v in obj.items()]}
|
|
if isinstance(obj, (list, tuple)):
|
|
return {"kind": "tuple" if isinstance(obj, tuple) else "list",
|
|
"value": [extract_tensors(v, prefix, out) for v in obj]}
|
|
return {"kind": "value", "value": obj}
|
|
|
|
|
|
def restore_tensors(obj, tensors):
|
|
kind, value = obj["kind"], obj["value"]
|
|
if kind == "tensor":
|
|
return tensors[value]
|
|
if kind == "dict":
|
|
return {restore_tensors(k, tensors): restore_tensors(v, tensors) for k, v in value}
|
|
if kind in ("list", "tuple"):
|
|
values = [restore_tensors(v, tensors) for v in value]
|
|
return tuple(values) if kind == "tuple" else values
|
|
if kind != "value":
|
|
raise ValueError(f"Invalid structure kind: {kind}")
|
|
return value
|
|
|
|
|
|
def pack_object(obj, transport_dtype=None):
|
|
tensors = {}
|
|
structure = extract_tensors(obj, "", tensors)
|
|
meta, blob = pack_tensors(tensors, transport_dtype)
|
|
return {"struct": structure, "tensors": meta, "blob_size": len(blob)}, blob
|
|
|
|
|
|
def unpack_object(header, blob):
|
|
return restore_tensors(header["struct"], unpack_tensors(header["tensors"], blob))
|
|
|
|
|
|
class _Connection:
|
|
def __init__(self, ip, port, auth_token="", connect_timeout=SOCKET_TIMEOUT,
|
|
request_attempts=CONNECT_RETRIES, connect_backoff=CONNECT_BACKOFF,
|
|
encode_timeout=SOCKET_TIMEOUT):
|
|
self.ip, self.port, self.auth_token = ip, port, auth_token
|
|
self.connect_timeout = float(connect_timeout)
|
|
self.request_attempts = int(request_attempts)
|
|
self.connect_backoff = float(connect_backoff)
|
|
self.encode_timeout = float(encode_timeout)
|
|
for value in (self.connect_timeout, self.encode_timeout):
|
|
if not math.isfinite(value) or value <= 0:
|
|
raise ValueError("Timeouts must be finite and positive")
|
|
if self.request_attempts < 1:
|
|
raise ValueError("Request attempts must be at least 1")
|
|
if not math.isfinite(self.connect_backoff) or self.connect_backoff < 0:
|
|
raise ValueError("Retry delay must be finite and nonnegative")
|
|
self._sock = None
|
|
self._lock = threading.Lock()
|
|
|
|
def _connect(self):
|
|
sock = socket.create_connection((self.ip, self.port), timeout=self.connect_timeout)
|
|
try:
|
|
_set_socket_opts(sock)
|
|
except Exception:
|
|
sock.close()
|
|
raise
|
|
self._sock = sock
|
|
log(f"Connected to worker {self.ip}:{self.port}")
|
|
|
|
def _close(self):
|
|
if self._sock is not None:
|
|
try:
|
|
self._sock.close()
|
|
except OSError:
|
|
pass
|
|
self._sock = None
|
|
|
|
def request(self, header, blob=b"", response_timeout=None, replay_safe=True):
|
|
header = {**header, "auth": self.auth_token}
|
|
timeout = self.encode_timeout if response_timeout is None else response_timeout
|
|
if not math.isfinite(timeout) or timeout <= 0:
|
|
raise ValueError("Response timeout must be finite and positive")
|
|
with self._lock:
|
|
backoff = self.connect_backoff
|
|
for attempt in range(1, self.request_attempts + 1):
|
|
sending = False
|
|
try:
|
|
if self._sock is None:
|
|
self._connect()
|
|
self._sock.settimeout(timeout)
|
|
sending = True
|
|
send_packet(self._sock, header, blob)
|
|
result = recv_packet(self._sock)
|
|
self._sock.settimeout(self.encode_timeout)
|
|
return result
|
|
except (OSError, ConnectionError, struct.error, ValueError):
|
|
self._close()
|
|
# Generation may have executed even when its response was lost.
|
|
if attempt == self.request_attempts or (sending and not replay_safe):
|
|
raise
|
|
log(f"Request attempt {attempt}/{self.request_attempts} failed; retrying")
|
|
time.sleep(backoff)
|
|
backoff *= 2
|
|
|
|
|
|
class RemoteCLIPProxy:
|
|
def __init__(self, ip, port, auth_token="", transport_mode="auto",
|
|
connection=None, lora_stack=None, connect_timeout=SOCKET_TIMEOUT,
|
|
request_attempts=CONNECT_RETRIES, connect_backoff=CONNECT_BACKOFF,
|
|
encode_timeout=SOCKET_TIMEOUT, generate_timeout=GENERATE_TIMEOUT,
|
|
layer_idx=None):
|
|
self.ip, self.port = ip, port
|
|
self.transport_mode = transport_mode
|
|
self.transport_dtype_name = resolve_transport_dtype(transport_mode, ip)
|
|
self.transport_dtype = ALLOWED_DTYPES.get(self.transport_dtype_name)
|
|
self.generate_timeout = float(generate_timeout)
|
|
if not math.isfinite(self.generate_timeout) or self.generate_timeout <= 0:
|
|
raise ValueError("Generation timeout must be finite and positive")
|
|
self._conn = connection or _Connection(
|
|
ip, port, auth_token, connect_timeout, request_attempts,
|
|
connect_backoff, encode_timeout)
|
|
self.lora_stack = list(lora_stack or [])
|
|
self.layer_idx = layer_idx
|
|
|
|
def clone(self, disable_dynamic=False):
|
|
return RemoteCLIPProxy(
|
|
self.ip, self.port, transport_mode=self.transport_mode,
|
|
connection=self._conn, lora_stack=self.lora_stack,
|
|
generate_timeout=self.generate_timeout, layer_idx=self.layer_idx)
|
|
|
|
def clip_layer(self, layer_idx):
|
|
self.layer_idx = layer_idx
|
|
|
|
def with_lora(self, lora_name, strength_clip):
|
|
c = self.clone()
|
|
c.lora_stack = self.lora_stack + [[lora_name, float(strength_clip)]]
|
|
return c
|
|
|
|
def add_patches(self, *_args, **_kwargs):
|
|
raise NotImplementedError(
|
|
"Apply LoRAs to a remote CLIP with the 'LoraLoaderCLIPOnly' node.")
|
|
|
|
def _rpc(self, cmd, payload, generation=False):
|
|
# Token tensors retain their original numerical values. Transport precision
|
|
# applies to image/tokenizer inputs and embedding responses, not token IDs.
|
|
header, blob = pack_object(payload)
|
|
header.update(cmd=cmd, proto=PROTOCOL_VERSION,
|
|
lora_stack=self.lora_stack, layer_idx=self.layer_idx,
|
|
transport_dtype=self.transport_dtype_name)
|
|
response, data = self._conn.request(
|
|
header, blob,
|
|
response_timeout=self.generate_timeout if generation else None,
|
|
replay_safe=not generation)
|
|
if response.get("error"):
|
|
raise RuntimeError(f"Remote CLIP worker error: {response['error']}")
|
|
return unpack_object(response, data)
|
|
|
|
def tokenize(self, text, return_word_ids=False, **kwargs):
|
|
# Return actual token dictionaries: callers such as SDXL edit l/g streams.
|
|
return self._rpc("tokenize", {"text": text, "kwargs": {
|
|
**kwargs, "return_word_ids": return_word_ids}})
|
|
|
|
def encode_from_tokens(self, tokens, return_pooled=False, return_dict=False):
|
|
out = self._rpc("encode", {"tokens": tokens, "return_pooled": return_pooled})
|
|
if return_dict:
|
|
return out
|
|
if return_pooled:
|
|
return out["cond"], out.get("pooled_output")
|
|
return out["cond"]
|
|
|
|
def encode_from_tokens_scheduled(self, tokens, unprojected=False, add_dict=None, show_pbar=True):
|
|
return self._rpc("encode_scheduled", {
|
|
"tokens": tokens, "unprojected": unprojected,
|
|
"add_dict": {} if add_dict is None else add_dict, "show_pbar": show_pbar})
|
|
|
|
def generate(self, tokens, **gen_kwargs):
|
|
return self._rpc("generate", {"tokens": tokens, "gen_kwargs": gen_kwargs}, generation=True)
|
|
|
|
def decode(self, generated, skip_special_tokens=True):
|
|
return generated
|
|
|
|
|
|
class _Worker:
|
|
def __init__(self, clip, auth_token=""):
|
|
self.base_clip = clip
|
|
self.auth_token = auth_token
|
|
self._infer_lock = threading.Lock()
|
|
self._patched_clips = OrderedDict()
|
|
|
|
def _get_clip(self, lora_stack, layer_idx):
|
|
sig = json.dumps([lora_stack, layer_idx], sort_keys=True)
|
|
if sig in self._patched_clips:
|
|
self._patched_clips.move_to_end(sig)
|
|
return self._patched_clips[sig]
|
|
clip = self.base_clip.clone()
|
|
if layer_idx is not None:
|
|
clip.clip_layer(layer_idx)
|
|
for lora_name, strength in lora_stack:
|
|
if strength == 0:
|
|
continue
|
|
path = folder_paths.get_full_path("loras", lora_name)
|
|
if path is None:
|
|
raise FileNotFoundError(f"LoRA not found on worker: {lora_name}")
|
|
lora = comfy.utils.load_torch_file(path, safe_load=True)
|
|
_, clip = comfy.sd.load_lora_for_models(None, clip, lora, 0, strength)
|
|
self._patched_clips[sig] = clip
|
|
while len(self._patched_clips) > PATCHED_CLIP_CACHE:
|
|
self._patched_clips.popitem(last=False)
|
|
return clip
|
|
|
|
def execute(self, cmd, payload, lora_stack, layer_idx):
|
|
with self._infer_lock, torch.inference_mode():
|
|
clip = self._get_clip(lora_stack, layer_idx)
|
|
if cmd == "tokenize":
|
|
return clip.tokenize(payload["text"], **payload["kwargs"])
|
|
if cmd == "generate":
|
|
return clip.decode(clip.generate(payload["tokens"], **payload["gen_kwargs"]))
|
|
if cmd == "encode":
|
|
out = clip.encode_from_tokens(payload["tokens"],
|
|
return_pooled=payload["return_pooled"], return_dict=True)
|
|
elif cmd == "encode_scheduled":
|
|
out = clip.encode_from_tokens_scheduled(**payload)
|
|
else:
|
|
raise ValueError(f"Unknown command: {cmd}")
|
|
return out
|
|
|
|
|
|
class _Server:
|
|
def __init__(self, clip, host, port, token):
|
|
self.worker = _Worker(clip, token)
|
|
self.token = token
|
|
self.stopped = threading.Event()
|
|
self._clients = set()
|
|
self._clients_lock = threading.Lock()
|
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
try:
|
|
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
self.socket.bind((host, port))
|
|
self.socket.listen(16)
|
|
self.socket.settimeout(0.5)
|
|
except Exception:
|
|
self.socket.close()
|
|
raise
|
|
self.thread = threading.Thread(target=self._accept_loop, daemon=True)
|
|
self.thread.start()
|
|
|
|
@staticmethod
|
|
def _close_socket(sock):
|
|
try:
|
|
sock.shutdown(socket.SHUT_RDWR)
|
|
except OSError:
|
|
pass
|
|
sock.close()
|
|
|
|
def close(self):
|
|
self.stopped.set()
|
|
self._close_socket(self.socket)
|
|
with self._clients_lock:
|
|
clients = list(self._clients)
|
|
for conn in clients:
|
|
self._close_socket(conn)
|
|
self.thread.join(timeout=1)
|
|
|
|
def _accept_loop(self):
|
|
while not self.stopped.is_set():
|
|
try:
|
|
conn, addr = self.socket.accept()
|
|
except socket.timeout:
|
|
continue
|
|
except OSError:
|
|
break
|
|
with self._clients_lock:
|
|
if self.stopped.is_set():
|
|
conn.close()
|
|
break
|
|
self._clients.add(conn)
|
|
threading.Thread(target=self._handle, args=(conn, addr), daemon=True).start()
|
|
|
|
def _handle(self, conn, addr):
|
|
try:
|
|
_set_socket_opts(conn)
|
|
while not self.stopped.is_set():
|
|
header = recv_header(conn)
|
|
auth = header.get("auth", "")
|
|
if self.token and (not isinstance(auth, str) or not hmac.compare_digest(
|
|
auth.encode("utf-8"), self.token.encode("utf-8"))):
|
|
send_packet(conn, {"error": "unauthorized", "blob_size": 0})
|
|
break
|
|
if header.get("proto") != PROTOCOL_VERSION:
|
|
send_packet(conn, {"error": f"Protocol mismatch: install v{PROTOCOL_VERSION} on both ends", "blob_size": 0})
|
|
break
|
|
blob = recv_blob(conn, header)
|
|
try:
|
|
payload = unpack_object(header, blob)
|
|
if self.stopped.is_set():
|
|
break
|
|
out = self.worker.execute(header["cmd"], payload,
|
|
header.get("lora_stack", []), header.get("layer_idx"))
|
|
if self.stopped.is_set():
|
|
break
|
|
transport = header.get("transport_dtype")
|
|
if transport not in (None, "torch.float16", "torch.float32"):
|
|
raise ValueError("Invalid transport precision")
|
|
dtype = ALLOWED_DTYPES.get(transport) if header["cmd"] in ("encode", "encode_scheduled") else None
|
|
response, data = pack_object(out, dtype)
|
|
send_packet(conn, response, data)
|
|
except Exception as e:
|
|
if self.stopped.is_set():
|
|
break
|
|
log(f"Worker request failed: {e}")
|
|
send_packet(conn, {"error": str(e), "blob_size": 0})
|
|
except (ConnectionError, OSError, ValueError, TypeError, KeyError, struct.error) as e:
|
|
log(f"Client {addr} disconnected: {e}")
|
|
finally:
|
|
with self._clients_lock:
|
|
self._clients.discard(conn)
|
|
self._close_socket(conn)
|
|
|
|
|
|
class SendRemoteCLIP:
|
|
_servers = {}
|
|
_servers_lock = threading.Lock()
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {
|
|
"CLIP": ("CLIP",),
|
|
"listen_port": ("INT", {"default": DEFAULT_PORT, "min": 1, "max": 65535}),
|
|
}, "optional": {
|
|
"bind_host": ("STRING", {"default": "0.0.0.0"}),
|
|
"auth_token": ("STRING", {"default": ""}),
|
|
}}
|
|
|
|
RETURN_TYPES = ()
|
|
FUNCTION = "start_worker"
|
|
OUTPUT_NODE = True
|
|
CATEGORY = "Remote CLIP"
|
|
|
|
def start_worker(self, CLIP, listen_port, bind_host="0.0.0.0", auth_token=""):
|
|
token = auth_token or os.environ.get("REMOTE_CLIP_TOKEN", "")
|
|
with self._servers_lock:
|
|
previous = self._servers.pop(listen_port, None)
|
|
if previous is not None:
|
|
previous.close()
|
|
self._servers[listen_port] = _Server(CLIP, bind_host, listen_port, token)
|
|
if bind_host == "0.0.0.0" and not token:
|
|
log("WARNING: worker is accessible on all interfaces without authentication")
|
|
log(f"Worker listening on {bind_host}:{listen_port}")
|
|
return {"ui": {"text": [f"Remote CLIP worker on {bind_host}:{listen_port}"]}}
|
|
|
|
|
|
class LoadRemoteCLIP:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {
|
|
"worker_ip": ("STRING", {"default": "127.0.0.1"}),
|
|
"port": ("INT", {"default": DEFAULT_PORT, "min": 1, "max": 65535}),
|
|
}, "optional": {
|
|
"auth_token": ("STRING", {"default": ""}),
|
|
"transport_precision": (["auto", "fp16", "fp32"], {"default": "auto"}),
|
|
"connect_timeout": ("FLOAT", {"default": SOCKET_TIMEOUT, "min": 0.1, "max": 3600.0, "step": 0.1, "tooltip": "Seconds per connection attempt."}),
|
|
"request_attempts": ("INT", {"default": CONNECT_RETRIES, "min": 1, "max": 10, "tooltip": "Total attempts per operation, including the first. Connection failures and safe request retries share this single limit. Generation is never replayed after sending starts."}),
|
|
"connect_backoff": ("FLOAT", {"default": CONNECT_BACKOFF, "min": 0.0, "max": 60.0, "step": 0.1, "tooltip": "Initial retry delay in seconds; doubles after each failure."}),
|
|
"encode_timeout": ("FLOAT", {"default": SOCKET_TIMEOUT, "min": 0.1, "max": 86400.0, "step": 0.1, "tooltip": "Socket timeout for tokenization and encoding; not a total operation deadline."}),
|
|
"generate_timeout": ("FLOAT", {"default": GENERATE_TIMEOUT, "min": 0.1, "max": 86400.0, "step": 0.1, "tooltip": "Generation socket timeout; not a total operation deadline."}),
|
|
}}
|
|
|
|
RETURN_TYPES = ("CLIP",)
|
|
FUNCTION = "load_remote"
|
|
CATEGORY = "Remote CLIP"
|
|
|
|
def load_remote(self, worker_ip, port, auth_token="", transport_precision="auto",
|
|
connect_timeout=SOCKET_TIMEOUT, request_attempts=CONNECT_RETRIES,
|
|
connect_backoff=CONNECT_BACKOFF, encode_timeout=SOCKET_TIMEOUT,
|
|
generate_timeout=GENERATE_TIMEOUT):
|
|
token = auth_token or os.environ.get("REMOTE_CLIP_TOKEN", "")
|
|
return (RemoteCLIPProxy(worker_ip, port, token, transport_precision,
|
|
connect_timeout=connect_timeout, request_attempts=request_attempts,
|
|
connect_backoff=connect_backoff, encode_timeout=encode_timeout,
|
|
generate_timeout=generate_timeout),)
|
|
|
|
class LoraLoaderCLIPOnly:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"CLIP": ("CLIP",),
|
|
"lora_name": (folder_paths.get_filename_list("loras"),),
|
|
"strength_clip": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
|
}
|
|
}
|
|
RETURN_TYPES = ("CLIP",)
|
|
FUNCTION = "load_lora"
|
|
CATEGORY = "Remote CLIP"
|
|
def load_lora(self, CLIP, lora_name, strength_clip):
|
|
if isinstance(CLIP, RemoteCLIPProxy):
|
|
return (CLIP.with_lora(lora_name, strength_clip),)
|
|
if strength_clip == 0:
|
|
return (CLIP,)
|
|
lora_path = folder_paths.get_full_path("loras", lora_name)
|
|
lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
|
|
_, clip_lora = comfy.sd.load_lora_for_models(None, CLIP, lora, 0, strength_clip)
|
|
return (clip_lora,)
|
|
NODE_CLASS_MAPPINGS = {
|
|
"SendRemoteCLIP": SendRemoteCLIP,
|
|
"LoadRemoteCLIP": LoadRemoteCLIP,
|
|
"LoraLoaderCLIPOnly": LoraLoaderCLIPOnly,
|
|
}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"SendRemoteCLIP": "Send Remote CLIP",
|
|
"LoadRemoteCLIP": "Load Remote CLIP",
|
|
"LoraLoaderCLIPOnly": "LoraLoaderCLIPOnly",
|
|
}
|