2 Commits
Author SHA1 Message Date
niknah 312e285e47 A long fix for the warning message when running with image input/output
nodes
It removes the nodes inside the image input/output nodes before running.
2026-08-16 22:27:05 +10:00
niknah 385ee20f78 Update changes list 2026-08-04 08:43:20 +10:00
5 changed files with 256 additions and 168 deletions
+2
View File
@@ -117,6 +117,8 @@ Using flux klein edit(to make new objects appear) + wan video.
## Changes
* v1.0.9: Fixed up warning message for nodes inside the image input / output nodes.
* v1.0.8: Add column node. Adds a column, repeats values until it fills the source spreadsheet's rows
* v1.0.7: Reverted changes for errors/warnings when running. It wasn't working for KJ get/set nodes.
* v1.0.6: Fix missing first image in video. Remove checking errors/warnings when running.
* v1.0.5: File list node now uses the input folder as source. Don't crash when numbers in the columns are not correct.
+216 -110
View File
@@ -6,6 +6,8 @@ from server import PromptServer
import sys
import csv
import hashlib
import base64
import pickle
from io import StringIO
import numpy as np
from PIL import Image
@@ -248,6 +250,7 @@ class GroupInfo():
self.group_name = group_name
self.first_input_node_id = first_input_node_id
self.output_image_link = None
self.restored_nodes = {}
def change_out_node(self, graph):
@@ -265,111 +268,6 @@ class GroupInfo():
# logging.warning("S2V: Could not find output image node")
return found_node
@classmethod
def map_children(cls, prompt):
input_node_ids = []
child_node_ids = {}
for node_id, node in prompt.items():
if "inputs" not in node:
continue
for k, v in node["inputs"].items():
if not is_link(v):
continue
parent_node_id = v[0]
if parent_node_id not in child_node_ids:
child_node_ids[parent_node_id] = {"in":[], "out":[]}
child_node_ids[parent_node_id]["out"].append(node_id)
if node_id not in child_node_ids:
child_node_ids[node_id] = {"in":[], "out":[]}
child_node_ids[node_id]["in"].append(parent_node_id)
if node["class_type"] == "Spreadsheet2VideoInputImage":
input_node_ids.append(node_id)
return (child_node_ids,input_node_ids)
@classmethod
def add_map_output_by_group_name(cls, group_name_by_output_id, child_node_ids, children, group_name):
for child_id in children["out"]:
if child_id in group_name_by_output_id:
existing_group_name = group_name_by_output_id[child_id]
if existing_group_name != group_name:
raise ValueError(f"S2V: Node is in both column1 groups, node id:{child_id}, names:{existing_group_name}, {group_name}")
group_name_by_output_id[child_id] = group_name
if child_id not in child_node_ids:
logging.error(f"S2V: Cannot find output node id: {child_id}")
continue
cls.add_map_output_by_group_name(
group_name_by_output_id,
child_node_ids,
child_node_ids[child_id],
group_name
)
@classmethod
def map_output_by_group_name(cls, group_name_by_output_id, prompt):
(child_node_ids, input_node_ids) = cls.map_children(prompt)
for input_node_id in input_node_ids:
inputImageNode = prompt[input_node_id]
group_name = cls.get_group_name_from_node(inputImageNode)
group_name_by_output_id[input_node_id] = group_name
cls.add_map_output_by_group_name(
group_name_by_output_id,
child_node_ids,
child_node_ids[input_node_id],
group_name
)
return group_name_by_output_id
@classmethod
def get_group_name_from_node(cls, inputImageNode):
if inputImageNode is None or "inputs" not in inputImageNode:
return None
group_name = inputImageNode["inputs"]["COLUMN1"].strip()
if group_name == "":
return None
return group_name
@classmethod
def map_outputs(cls, prompt):
(child_node_ids, input_node_ids) = cls.map_children(prompt)
group_name_by_output_id = {}
cls.map_output_by_group_name(group_name_by_output_id, prompt)
by_group_name = {}
for input_node_id in input_node_ids:
inputImageNode = prompt[input_node_id]
group_name = cls.get_group_name_from_node(inputImageNode)
if group_name is None:
continue
if group_name in by_group_name:
raise ValueError(f"S2V: Duplicate group names: {group_name}")
group_info = GroupInfo(input_node_id, group_name)
by_group_name[group_name] = group_info
group_info.add_orig_node_ids(
prompt,
child_node_ids[input_node_id],
child_node_ids,
group_name_by_output_id,
0
)
logging.info(f"S2V: group: {group_name}, nodes: {group_info.get_orig_node_ids_count()}")
return by_group_name
def add_orig_node_ids(
self,
@@ -377,10 +275,12 @@ class GroupInfo():
children,
child_node_ids,
group_name_by_output_id,
parent_is_out,
depth
):
for child_id in (children["in"] + children["out"]):
for (child_id, node_is_out) in ([ [i,False] for i in children["in"]] + [ [i,True] for i in children["out"]]):
must_be_group = group_name_by_output_id[child_id] if child_id in group_name_by_output_id else None
is_out = parent_is_out and node_is_out
class_type = prompt[child_id]["class_type"]
if (
@@ -391,8 +291,8 @@ class GroupInfo():
(must_be_group is not None and must_be_group != self.group_name)
):
pass
elif (child_id not in self.orig_node_ids):
self.orig_node_ids[child_id] = True
elif(child_id not in self.orig_node_ids):
self.orig_node_ids[child_id] = is_out
if child_id in child_node_ids:
self.add_orig_node_ids(
@@ -400,8 +300,13 @@ class GroupInfo():
child_node_ids[child_id],
child_node_ids,
group_name_by_output_id,
is_out,
depth+1
)
elif(is_out is True):
# if the node is on the output path ever. It is out.
# Cause the path is not processed in any order.
self.orig_node_ids[child_id] = is_out
def get_orig_node_ids_count(self):
return len(self.orig_node_ids.keys())
@@ -411,7 +316,11 @@ class GroupInfo():
node_id_map = {}
for node_id in contained:
original_node = prompt[node_id]
if node_id in prompt:
original_node = prompt[node_id]
else:
original_node = self.restored_nodes[node_id]
new_node = graph.node(original_node["class_type"])
new_node.set_override_display_id(node_id)
node_id_map[node_id] = (original_node, new_node)
@@ -506,6 +415,183 @@ class GroupInfo():
# node['class_type']="Spreadsheet2VideoDoNothing"
# node['inputs']={}
@classmethod
def restore_group_infos_from_prompt(cls, prompt):
by_group_name = {}
for node_id, node in prompt.items():
if "_meta" not in node:
continue
meta = node["_meta"]
if "__group_info" in meta:
group_info = pickle.loads(base64.b64decode(meta["__group_info"]))
if "__removed" in meta:
group_info.restored_nodes = meta["__removed"]
by_group_name[group_info.group_name] = group_info
if(len(by_group_name.keys()) == 0):
return None
return by_group_name
def remove_nodes_from_prompt(self, prompt, exclude_ids):
removed = {}
for node_id, is_out in self.orig_node_ids.items():
if node_id in prompt and node_id not in exclude_ids:
removed[node_id]= prompt[node_id]
del prompt[node_id]
return removed
@classmethod
def remove_nodes_in_group_infos(cls, prompt, by_group_name, exclude_ids):
for name, group_info in by_group_name.items():
input_node = prompt[group_info.first_input_node_id]
raw_bytes = pickle.dumps(group_info)
# base64 encodes the binary data into a clean, portable text string
if "_meta" not in input_node:
input_node["_meta"] = {}
input_node["_meta"]["__group_info"] = base64.b64encode(raw_bytes).decode("utf-8")
removed = group_info.remove_nodes_from_prompt(prompt, exclude_ids)
input_node["_meta"]["__removed"] = removed
class MapOutputs():
def __init__(self):
self.child_node_ids = {}
self.input_node_ids = []
def map_children(self, prompt):
input_node_ids = []
child_node_ids = {}
for node_id, node in prompt.items():
if "inputs" not in node or "class_type" not in node:
continue
for k, v in node["inputs"].items():
if not is_link(v):
continue
parent_node_id = v[0]
if parent_node_id not in child_node_ids:
child_node_ids[parent_node_id] = {"in":[], "out":[]}
child_node_ids[parent_node_id]["out"].append(node_id)
if node_id not in child_node_ids:
child_node_ids[node_id] = {"in":[], "out":[]}
child_node_ids[node_id]["in"].append(parent_node_id)
if node["class_type"] == "Spreadsheet2VideoInputImage":
input_node_ids.append(node_id)
self.child_node_ids = child_node_ids
self.input_node_ids = input_node_ids
return (child_node_ids,input_node_ids)
def get_group_name_from_node(self, inputImageNode):
if inputImageNode is None or "inputs" not in inputImageNode:
return None
group_name = inputImageNode["inputs"]["COLUMN1"].strip()
if group_name == "":
return None
return group_name
def add_map_output_by_group_name(self, group_name_by_output_id, child_node_ids, children, group_name):
for child_id in children["out"]:
if child_id in group_name_by_output_id:
existing_group_name = group_name_by_output_id[child_id]
if existing_group_name != group_name:
raise ValueError(f"S2V: Node is in both column1 groups, node id:{child_id}, names:{existing_group_name}, {group_name}")
group_name_by_output_id[child_id] = group_name
if child_id not in child_node_ids:
logging.error(f"S2V: Cannot find output node id: {child_id}")
continue
self.add_map_output_by_group_name(
group_name_by_output_id,
child_node_ids,
child_node_ids[child_id],
group_name
)
def map_output_by_group_name(self, group_name_by_output_id, prompt):
(child_node_ids, input_node_ids) = self.map_children(prompt)
for input_node_id in input_node_ids:
inputImageNode = prompt[input_node_id]
group_name = self.get_group_name_from_node(inputImageNode)
group_name_by_output_id[input_node_id] = group_name
self.add_map_output_by_group_name(
group_name_by_output_id,
child_node_ids,
child_node_ids[input_node_id],
group_name
)
return group_name_by_output_id
def map_outputs(self, prompt):
(child_node_ids, input_node_ids) = self.map_children(prompt)
group_name_by_output_id = {}
self.map_output_by_group_name(group_name_by_output_id, prompt)
by_group_name = {}
for input_node_id in input_node_ids:
inputImageNode = prompt[input_node_id]
group_name = self.get_group_name_from_node(inputImageNode)
if group_name is None:
continue
if group_name in by_group_name:
raise ValueError(f"S2V: Duplicate group names: {group_name}")
group_info = GroupInfo(input_node_id, group_name)
by_group_name[group_name] = group_info
group_info.add_orig_node_ids(
prompt,
child_node_ids[input_node_id],
child_node_ids,
group_name_by_output_id,
True,
0
)
logging.info(f"S2V: group: {group_name}, nodes: {group_info.get_orig_node_ids_count()}")
return by_group_name
def mark_parent_nodes(self, mark_dict, node):
for parent_id in node["in"]:
if parent_id in mark_dict:
continue
mark_dict[parent_id] = True
for node_id in self.child_node_ids:
node = self.child_node_ids[node_id]
self.mark_parent_nodes(mark_dict, node)
def nodes_used_by_other_nodes(self, prompt, by_group_name) -> dict[str, bool]:
used_by_s2v_ids : dict[str, bool] = {}
not_used_by_s2v_ids : dict[str, bool] = {}
used_by_other_nodes : dict[str, bool] = {}
for name, group_info in by_group_name.items():
for node_id in group_info.orig_node_ids.keys():
used_by_s2v_ids[node_id] = True
not_used_by_s2v_ids = { k: True for k in prompt.keys() if k not in used_by_s2v_ids }
for node_id in not_used_by_s2v_ids:
for node_id in self.child_node_ids:
node = self.child_node_ids[node_id]
self.mark_parent_nodes(used_by_other_nodes, node)
return used_by_other_nodes
class Spreadsheet2VideoFinalVideo(io.ComfyNode):
@classmethod
@@ -928,7 +1014,9 @@ class Spreadsheet2VideoNode(io.ComfyNode):
# Use StringIO to treat the string as a file
f = StringIO(spreadsheet)
by_group_name = GroupInfo.map_outputs(cls.hidden.prompt)
by_group_name = GroupInfo.restore_group_infos_from_prompt(cls.hidden.prompt)
if by_group_name is None:
by_group_name = MapOutputs().map_outputs(cls.hidden.prompt)
used_groups = {}
reader = csv.reader(f, delimiter=',')
@@ -1022,3 +1110,21 @@ class Spreadsheet2VideoNode(io.ComfyNode):
return str(random.random())
def prompt_handler(json_data):
try:
prompt = json_data["prompt"]
map_outputs = MapOutputs()
by_group_name = map_outputs.map_outputs(prompt)
# remove
used_by_other_nodes = map_outputs.nodes_used_by_other_nodes(prompt, by_group_name)
GroupInfo.remove_nodes_in_group_infos(prompt, by_group_name, used_by_other_nodes)
except Exception:
logging.exception("Problem with prompt")
return json_data
return json_data
PromptServer.instance.add_on_prompt_handler(prompt_handler)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "spreadsheet2video-comfyui",
"version": "1.0.0",
"version": "1.0.1",
"description": "",
"license": "ISC",
"author": "",
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "Spreadsheet2Video-ComfyUI"
version = "1.0.8"
version = "1.0.9"
description = "Use a spreadsheet as input for ComfyUI execution"
license = { file = "LICENSE" }
dependencies = [
+36 -56
View File
@@ -1,7 +1,7 @@
{
"id": "8d94793d-f68f-467c-b91c-5a1bad8692ca",
"revision": 0,
"last_node_id": 11,
"last_node_id": 13,
"last_link_id": 5,
"nodes": [
{
@@ -12,8 +12,8 @@
4133.395402282243
],
"size": [
301.040625,
106
301.03125,
142
],
"flags": {},
"order": 0,
@@ -43,12 +43,12 @@
"id": 7,
"type": "Spreadsheet2VideoSequence",
"pos": [
-830.0402038757348,
-832.3603875607462,
4853.891607434261
],
"size": [
301.040625,
106
301.015625,
142
],
"flags": {},
"order": 1,
@@ -82,8 +82,8 @@
4400
],
"size": [
301.040625,
106
301.03125,
142
],
"flags": {},
"order": 2,
@@ -117,8 +117,8 @@
4101.516095855725
],
"size": [
247.78345320808046,
238.2740203378844
247.765625,
238.25
],
"flags": {},
"order": 5,
@@ -142,11 +142,7 @@
"ver": "0.24.0",
"Node name for S&R": "PreviewAny"
},
"widgets_values": [
null,
null,
false
]
"widgets_values": []
},
{
"id": 5,
@@ -156,8 +152,8 @@
4430
],
"size": [
226.80773384673182,
212.1823816679189
226.796875,
212.171875
],
"flags": {
"collapsed": false
@@ -183,22 +179,18 @@
"ver": "0.24.0",
"Node name for S&R": "PreviewAny"
},
"widgets_values": [
null,
null,
null
]
"widgets_values": []
},
{
"id": 8,
"type": "Spreadsheet2VideoSequence",
"pos": [
-827.5216964337976,
-829.841880118809,
5156.965602783869
],
"size": [
301.040625,
106
301.015625,
142
],
"flags": {},
"order": 3,
@@ -228,11 +220,11 @@
"id": 11,
"type": "PreviewAny",
"pos": [
-469.36544276581486,
-471.6856264508262,
5404.420565428257
],
"size": [
210,
225,
166
],
"flags": {},
@@ -257,22 +249,18 @@
"ver": "0.24.0",
"Node name for S&R": "PreviewAny"
},
"widgets_values": [
null,
null,
null
]
"widgets_values": []
},
{
"id": 10,
"type": "Spreadsheet2VideoSequence",
"pos": [
-827.1824057491543,
-829.5025894341657,
5402.336211698525
],
"size": [
301.040625,
106
301.015625,
142
],
"flags": {},
"order": 4,
@@ -302,11 +290,11 @@
"id": 9,
"type": "PreviewAny",
"pos": [
-469.70473345045866,
-472.02491713547,
5159.049956513601
],
"size": [
210,
225,
166
],
"flags": {},
@@ -331,21 +319,17 @@
"ver": "0.24.0",
"Node name for S&R": "PreviewAny"
},
"widgets_values": [
null,
null,
null
]
"widgets_values": []
},
{
"id": 6,
"type": "PreviewAny",
"pos": [
-472.22324089239635,
-474.5434245774077,
4855.975961163992
],
"size": [
210,
225,
166
],
"flags": {},
@@ -370,11 +354,7 @@
"ver": "0.24.0",
"Node name for S&R": "PreviewAny"
},
"widgets_values": [
null,
null,
null
]
"widgets_values": []
}
],
"links": [
@@ -422,14 +402,14 @@
"groups": [
{
"id": 1,
"title": "Will Error",
"title": "Should Error",
"bounding": [
-891.0076443313377,
-893.327828016349,
4744.143303391399,
1146.6399876260243,
883.1031726327501
],
"color": "#3f789e",
"color": "#A88",
"flags": {}
},
{
@@ -448,13 +428,13 @@
"config": {},
"extra": {
"ds": {
"scale": 0.5131581182307067,
"scale": 0.31863081771035706,
"offset": [
2097.969476454836,
-3598.220857052559
2004.6716575603311,
-3393.3513028960815
]
},
"frontendVersion": "1.45.15",
"frontendVersion": "1.48.7",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,