Files
Meisoftcoltd-comfyui-sequen…/batch.py
T
google-labs-jules[bot] 27bf0d70e3 Rename 'Job' to 'Batch' and add LoadCSV node
- Renamed all 'Job' terminology to 'Batch' across the codebase.
- Renamed job.py to batch.py.
- Added LoadCSV node to support loading batch parameters from CSV files.
- Maintained backward compatibility for existing workflows using legacy names.
- Updated README.md and README_ES.md with new version and documentation.
- Fixed a potential index-out-of-bounds bug in GetBatchStep clamping logic.
2026-01-26 02:50:13 +00:00

226 lines
5.9 KiB
Python

import collections
import itertools
import csv
import os
from . import register_node
@register_node
class MakeBatch:
"""Turns a sequence into a batch with one attribute."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"sequence": ("SEQUENCE", ),
"name": ("STRING", {"default": ''}),
},
}
RETURN_TYPES = ("BATCH", "INT")
RETURN_NAMES = ("batch", "count")
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Batch"
def merge_dicts(self, *dicts):
#return collections.ChainMap(*reversed(dicts))
return dict(itertools.chain.from_iterable(d.items() for d in dicts))
def go(self, sequence, name):
result = [{name: value} for value in sequence]
return (result, len(result))
@register_node
class CombineBatches(MakeBatch):
"""Combines multiple batches."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"a": ("BATCH", ),
"method": (("zip", "product"), {"default": "zip"}),
},
"optional": {
x: ("BATCH", ) for x in ('b', 'c', 'd', 'e')
}
}
def go(self, method, **kwargs):
method = {'product': itertools.product, 'zip': zip}[method]
result = [self.merge_dicts(*steps) for steps in method(*kwargs.values())]
return (result, len(result))
@register_node
class EnumerateBatch(MakeBatch):
"""Combines multiple batches."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"batch": ("BATCH", ),
"name": ("STRING", {"default": ''}),
},
}
def go(self, batch, name):
result = [self.merge_dicts(step, {name: n}) for n, step in enumerate(batch)]
return (result, len(result))
@register_node
class GetBatchStep:
"""Gets the batch step by number."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"batch": ("BATCH", ),
"step": ("INT", {"default": 0}),
"wrap": (("repeat", "clamp"), {"default": "repeat"}),
},
}
RETURN_TYPES = ("ATTRIBUTES", )
RETURN_NAMES = ("attributes", )
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Batch"
def go(self, batch, step, wrap):
if wrap == 'repeat':
while step < 0:
step += len(batch)
step = step % len(batch)
elif wrap == 'clamp':
step = max(min(step, len(batch)-1), 0)
return (batch[step], )
@register_node
class FormatAttributes:
"""Applies attributes to a format string."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"attributes": ("ATTRIBUTES",),
"format": ("STRING", {'default': '', 'multiline': True, "dynamicPrompts": False})
},
}
RETURN_TYPES = ("STRING", )
RETURN_NAMES = ("string", )
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Batch"
def go(self, attributes, format):
return (format.format(**attributes), )
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
@register_node
class GetAttribute:
"""Gets a named attribute from a step."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"attributes": ("ATTRIBUTES", ),
"name": ("STRING", {"default": ''}),
},
}
RETURN_TYPES = (AnyType("*"), )
RETURN_NAMES = ("value", )
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Batch"
def go(self, attributes, name):
return (attributes[name], )
# Dynamically register typed attribute getters to avoid wildcard bug
# https://github.com/comfyanonymous/ComfyUI/pull/770
for t in ('INT', 'FLOAT', 'STRING'):
register_node(type(
'GetAttribute'+t.title(),
(GetAttribute, ),
{
'RETURN_TYPES': (t, ),
}
))
@register_node
class LoadCSV:
"""Loads a CSV file into a batch."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"path": ("STRING", {"default": "prompt.csv"}),
"delimiter": (["comma", "tab", "semicolon"], {"default": "comma"}),
"quotechar": (["\"", "'"], {"default": "\""}),
},
"optional": {
"index": ("INT", {"default": -1, "min": -1, "max": 999999}),
}
}
RETURN_TYPES = ("BATCH", "ATTRIBUTES", "INT")
RETURN_NAMES = ("batch", "current_attributes", "count")
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Batch"
def go(self, path, delimiter, quotechar, index=-1):
if not os.path.exists(path):
raise FileNotFoundError(f"CSV file not found: {path}")
delim = {
"comma": ",",
"tab": "\t",
"semicolon": ";",
}[delimiter]
batch = []
with open(path, mode='r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f, delimiter=delim, quotechar=quotechar)
for row in reader:
# Clean up keys and values
clean_row = {k.strip() if k else k: v.strip() if v else v for k, v in row.items()}
batch.append(clean_row)
count = len(batch)
current_attributes = batch[index % count] if count > 0 and index != -1 else (batch[0] if count > 0 else {})
return (batch, current_attributes, count)
@register_node
class BatchToList:
"""Converts a batch into a list."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"batch": ("BATCH",),
},
}
RETURN_TYPES = ("ATTRIBUTES",)
RETURN_NAMES = ("attributes",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Batch"
def go(self, batch):
return (batch,)