分割出力を CPU へ転送する BOOLEAN ウィジェットを追加。旧名 Any List Split の二重登録を削除しノード検索の重複表示を解消。 Co-authored-by: Cursor <cursoragent@cursor.com>
229 lines
7.2 KiB
Python
229 lines
7.2 KiB
Python
"""
|
|
Any Split (misc)
|
|
|
|
LIST / BATCH 入力 * を any_01, any_02 … と length (INT) に分解する。型チェックなし。
|
|
"""
|
|
|
|
try:
|
|
import torch
|
|
except ImportError:
|
|
torch = None
|
|
|
|
from .utils import ByPassNamesTuple, SplitReturnTypes, any_type, get_category, get_name
|
|
|
|
MIN_DATA_OUTPUTS = 1
|
|
SEND_TO_CPU_KEY = "Send to CPU"
|
|
_LEGACY_SEND_TO_CUDA_KEY = "Send to CUDA"
|
|
_CPU_DEVICE = "cpu"
|
|
|
|
|
|
def _unwrap_singleton(value):
|
|
"""INPUT_IS_LIST 時の hidden 入力ラップを外す。"""
|
|
if isinstance(value, list) and len(value) == 1:
|
|
return value[0]
|
|
return value
|
|
|
|
|
|
def _unwrap_bool(value, default=False):
|
|
"""INPUT_IS_LIST 時の BOOLEAN ウィジェット値を bool にする。"""
|
|
value = _unwrap_singleton(value)
|
|
if value is None:
|
|
return default
|
|
return bool(value)
|
|
|
|
|
|
def _send_value_to_cpu(value):
|
|
"""テンソル・.to 可能オブジェクト・入れ子 dict/list を CPU へ送る。"""
|
|
if torch is None or value is None:
|
|
return value
|
|
if isinstance(value, torch.Tensor):
|
|
return value.to(_CPU_DEVICE)
|
|
if isinstance(value, dict):
|
|
return {key: _send_value_to_cpu(item) for key, item in value.items()}
|
|
if isinstance(value, list):
|
|
return [_send_value_to_cpu(item) for item in value]
|
|
if isinstance(value, tuple):
|
|
return tuple(_send_value_to_cpu(item) for item in value)
|
|
to_device = getattr(value, "to", None)
|
|
if callable(to_device):
|
|
try:
|
|
return to_device(_CPU_DEVICE)
|
|
except (RuntimeError, TypeError, AttributeError):
|
|
return value
|
|
return value
|
|
|
|
|
|
def _normalize_list_input(value):
|
|
"""INPUT_IS_LIST で渡る * を平坦な要素列にする。"""
|
|
if value is None:
|
|
return []
|
|
if isinstance(value, (list, tuple)):
|
|
out = []
|
|
for item in value:
|
|
if isinstance(item, (list, tuple)):
|
|
out.extend(_normalize_list_input(item))
|
|
else:
|
|
out.append(item)
|
|
return out
|
|
return [value]
|
|
|
|
|
|
def _canonicalize_batch_tensor(value):
|
|
"""[B, 1, H, W] 形式の MASK バッチを ComfyUI 標準の [B, H, W] に揃える。"""
|
|
if torch is None or not isinstance(value, torch.Tensor):
|
|
return value
|
|
if value.ndim == 4 and int(value.shape[1]) == 1:
|
|
return value.squeeze(1)
|
|
return value
|
|
|
|
|
|
def _batch_size(value):
|
|
"""BATCH 入力の要素数。判定不能なら None。"""
|
|
value = _canonicalize_batch_tensor(value)
|
|
|
|
if torch is not None and isinstance(value, torch.Tensor):
|
|
if value.ndim == 2:
|
|
return 1
|
|
if value.ndim >= 1:
|
|
return int(value.shape[0])
|
|
|
|
if isinstance(value, dict):
|
|
samples = value.get("samples")
|
|
if torch is not None and isinstance(samples, torch.Tensor) and samples.ndim >= 1:
|
|
return int(samples.shape[0])
|
|
batch_index = value.get("batch_index")
|
|
if isinstance(batch_index, list) and batch_index:
|
|
return len(batch_index)
|
|
|
|
return None
|
|
|
|
|
|
def _slice_batch(value, index):
|
|
"""BATCH 値の index 番目(バッチ次元 1 を維持)。"""
|
|
value = _canonicalize_batch_tensor(value)
|
|
|
|
if torch is not None and isinstance(value, torch.Tensor):
|
|
if value.ndim == 2:
|
|
return value.unsqueeze(0)
|
|
return value[index : index + 1]
|
|
|
|
if isinstance(value, dict) and "samples" in value:
|
|
out = value.copy()
|
|
samples = value["samples"]
|
|
if torch is not None and isinstance(samples, torch.Tensor):
|
|
out["samples"] = samples[index : index + 1].clone()
|
|
if "noise_mask" in value and torch is not None:
|
|
masks = value["noise_mask"]
|
|
if isinstance(masks, torch.Tensor):
|
|
if masks.shape[0] == 1:
|
|
out["noise_mask"] = masks.clone()
|
|
elif masks.shape[0] >= samples.shape[0]:
|
|
out["noise_mask"] = masks[index : index + 1].clone()
|
|
if "batch_index" in value and isinstance(value["batch_index"], list):
|
|
out["batch_index"] = value["batch_index"][index : index + 1]
|
|
return out
|
|
|
|
return value
|
|
|
|
|
|
def _expand_batch_input(items):
|
|
"""LIST はそのまま。単一 BATCH 値は要素列へ展開。"""
|
|
if not items or len(items) != 1:
|
|
return items
|
|
|
|
size = _batch_size(items[0])
|
|
if size is None:
|
|
return items
|
|
if size <= 1:
|
|
return [_slice_batch(items[0], 0)]
|
|
return [_slice_batch(items[0], i) for i in range(size)]
|
|
|
|
|
|
def _linked_output_slots(prompt, unique_id):
|
|
"""ワークフロー JSON から接続済み出力スロット番号を集める。"""
|
|
uid = str(unique_id)
|
|
slots = []
|
|
for node in prompt.values():
|
|
for value in node.get("inputs", {}).values():
|
|
if isinstance(value, list) and len(value) == 2 and str(value[0]) == uid:
|
|
slots.append(int(value[1]))
|
|
return slots
|
|
|
|
|
|
def _compute_split_num_data(linked_slots, min_data=MIN_DATA_OUTPUTS):
|
|
"""接続状況から data 出力本数を決める(web/logic/any-split-layout.js と同等)。"""
|
|
if not linked_slots:
|
|
return min_data
|
|
|
|
slots = sorted(set(int(s) for s in linked_slots))
|
|
|
|
if len(slots) == 1 and slots[0] == min_data:
|
|
return min_data
|
|
|
|
length_candidates = [slot for slot in slots if any(s < slot for s in slots)]
|
|
length_slot = max(length_candidates) if length_candidates else -1
|
|
data_slots = [slot for slot in slots if slot != length_slot] if length_slot >= 0 else slots
|
|
|
|
max_data = max(data_slots) if data_slots else -1
|
|
return min_data if max_data < 0 else max(min_data, max_data + 2)
|
|
|
|
|
|
class MiscAnySplit:
|
|
"""LIST / BATCH 入力を可変 any_* 出力に分解する。"""
|
|
|
|
NAME = get_name("Any Split")
|
|
CATEGORY = get_category()
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls): # pylint: disable=invalid-name
|
|
return {
|
|
"required": {
|
|
"*": (any_type,),
|
|
SEND_TO_CPU_KEY: ("BOOLEAN", {"default": False}),
|
|
},
|
|
"hidden": {
|
|
"prompt": "PROMPT",
|
|
"unique_id": "UNIQUE_ID",
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = SplitReturnTypes()
|
|
RETURN_NAMES = ByPassNamesTuple(("any_01", "length"))
|
|
INPUT_IS_LIST = True
|
|
FUNCTION = "split"
|
|
|
|
def split(self, *, prompt=None, unique_id=None, **kwargs):
|
|
prompt = _unwrap_singleton(prompt)
|
|
unique_id = _unwrap_singleton(unique_id)
|
|
send_to_cpu = _unwrap_bool(
|
|
kwargs.get(SEND_TO_CPU_KEY, kwargs.get(_LEGACY_SEND_TO_CUDA_KEY, False)),
|
|
)
|
|
|
|
items = _expand_batch_input(_normalize_list_input(kwargs.get("*")))
|
|
if send_to_cpu:
|
|
items = [_send_value_to_cpu(item) for item in items]
|
|
length = len(items)
|
|
|
|
if not items:
|
|
return (length,)
|
|
|
|
linked = (
|
|
_linked_output_slots(prompt, unique_id)
|
|
if prompt is not None and unique_id is not None
|
|
else []
|
|
)
|
|
num_data = _compute_split_num_data(linked, MIN_DATA_OUTPUTS)
|
|
|
|
result = [items[i] if i < length else None for i in range(num_data)]
|
|
result.append(length)
|
|
return tuple(result)
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
MiscAnySplit.NAME: MiscAnySplit,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
MiscAnySplit.NAME: MiscAnySplit.NAME,
|
|
}
|