Files
wraith_executioner_year_2andCursor 34f88dba6a ✨ Any Split に Send to CPU を追加
分割出力を CPU へ転送する BOOLEAN ウィジェットを追加。旧名 Any List Split の二重登録を削除しノード検索の重複表示を解消。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 13:56:41 +09:00

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,
}