diff --git a/Spreadsheet2VideoNodes.py b/Spreadsheet2VideoNodes.py index fc16d90..1405711 100644 --- a/Spreadsheet2VideoNodes.py +++ b/Spreadsheet2VideoNodes.py @@ -62,6 +62,14 @@ class S2VComfy(): return cls.get_prompt_dir() / ("%s%06d" % (prefix, max_num+1) ) + # Add empty columns if the row does not have the same amount of columns as the header + @classmethod + def fill_empty_columns(cls, headerRow, from_rows): + for from_row in from_rows: + if len(from_row) < len(headerRow): + from_row += [None] * (len(headerRow) - len(from_row)) + + last_processed_time = None # Executed after each row class Spreadsheet2VideoProcessImage(io.ComfyNode): @@ -135,24 +143,29 @@ class Spreadsheet2VideoProcessImage(io.ComfyNode): # The original nodes are replaced with this one so they won't do anything. -class Spreadsheet2VideoDoNothing(io.ComfyNode): - @classmethod - def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="Spreadsheet2VideoDoNothing", - display_name="Internal S2V Do nothing", - description="internal node, ignore", - category="S2V Internal", - inputs=[ - ], - outputs=[ - ], - hidden=[], - ) +# class Spreadsheet2VideoDoNothing(io.ComfyNode): +# @classmethod +# def define_schema(cls) -> io.Schema: +# return io.Schema( +# node_id="Spreadsheet2VideoDoNothing", +# display_name="Internal S2V Do nothing", +# description="internal node, ignore", +# category="S2V Internal", +# inputs=[ +# ], +# outputs=[ +# ], +# hidden=[], +# ) - @classmethod - def execute(cls, **kwargs) -> io.NodeOutput: - return io.NodeOutput() +# @classmethod +# def execute(cls, **kwargs) -> io.NodeOutput: +# return io.NodeOutput( +# ExecutionBlocker(None) +# ) + +# def check_lazy_status(self): +# return [] # Not executed @@ -487,9 +500,11 @@ class GroupInfo(): logging.warning(f"S2V: not enough columns in row, column: {v[1]}, row: {','.join(row)}") return foundImageInput - def replace_with_do_nothing(self, prompt): - for remove_node_id in self.orig_node_ids.keys(): - prompt[remove_node_id]['class_type']="Spreadsheet2VideoDoNothing" + # def replace_with_do_nothing(self, prompt): + # for remove_node_id in self.orig_node_ids.keys(): + # node = prompt[remove_node_id] + # node['class_type']="Spreadsheet2VideoDoNothing" + # node['inputs']={} class Spreadsheet2VideoFinalVideo(io.ComfyNode): @@ -578,6 +593,7 @@ class Spreadsheet2VideoFinalVideo(io.ComfyNode): if len(waveforms)==0: output_audio["waveform"] = torch.empty(1, 1, 0) + output_audio["sample_rate"] = 44100 else: output_audio["waveform"] = torch.cat(waveforms, dim=1).unsqueeze(0) @@ -675,6 +691,57 @@ class Spreadsheet2VideoFilesList(io.ComfyNode): ) +class Spreadsheet2VideoAddColumnSpreadsheet(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="Spreadsheet2VideoAddColumnSpreadsheet", + display_name="Spreadsheet2Video Add column to Spreadsheet", + category="Spreadsheet2Video", + description="Adds columns to a spreadsheet. Repeats rows if there are not enough. Maximum rows to add is the source spreadsheet", + search_aliases=["spreadsheet", "loop", "add", "column"], + inputs=[ + io.String.Input("spreadsheet", multiline=True, tooltip="Comma separated values with a header row. Can use Spreadsheet2Video Load Spreadsheet node."), + io.String.Input("spreadsheet_to_add", multiline=True, tooltip="Comma separated values with a header row. The columns to add"), + ], + outputs=[ + io.String.Output(), + ], + ) + + + @classmethod + def execute(cls, spreadsheet, spreadsheet_to_add) -> io.NodeOutput: + f = StringIO(spreadsheet) + reader = csv.reader(f, delimiter=',') + headerRow = next(reader) + from_rows = list(reader) + + S2VComfy.fill_empty_columns(headerRow, from_rows) + + f = StringIO(spreadsheet_to_add) + reader = csv.reader(f, delimiter=',') + + output = StringIO() + writer = csv.writer(output) + + addHeaderRow = next(reader) + add_rows = list(reader) + + writer.writerow(headerRow + addHeaderRow) + + add_row_upto = 0 + for from_row in from_rows: + add_row_upto = add_row_upto % len(add_rows) + add_row = add_rows[add_row_upto] + writer.writerow(from_row + add_row) + add_row_upto += 1 + + return io.NodeOutput( + output.getvalue() + ) + + class Spreadsheet2VideoMultiplySpreadsheet(io.ComfyNode): @classmethod def define_schema(cls) -> io.Schema: @@ -700,9 +767,7 @@ class Spreadsheet2VideoMultiplySpreadsheet(io.ComfyNode): headerRow = next(reader) from_rows = list(reader) - for from_row in from_rows: - if len(from_row) < len(headerRow): - from_row += [None] * (len(headerRow) - len(from_row)) + S2VComfy.fill_empty_columns(headerRow, from_rows) f = StringIO(spreadsheet_to_add) @@ -931,10 +996,10 @@ class Spreadsheet2VideoNode(io.ComfyNode): ) # Disabled: If we make the original nodes that are not in the loop DoNothing -# If there are KJ get / set disconnected nodes. The get nodes will return None instead of the value -# for group_name in used_groups: -# used_group = by_group_name[group_name] -# used_group.replace_with_do_nothing(cls.hidden.prompt) +# Some workflows won't work with this. + # for group_name in used_groups: + # used_group = by_group_name[group_name] + # used_group.replace_with_do_nothing(cls.hidden.prompt) return io.NodeOutput( finalVideoNode.out(0), diff --git a/__init__.py b/__init__.py index f2d2356..5077208 100644 --- a/__init__.py +++ b/__init__.py @@ -10,7 +10,8 @@ from .Spreadsheet2VideoNodes import ( Spreadsheet2VideoLoadText, Spreadsheet2VideoSequence, Spreadsheet2VideoFilesList, - Spreadsheet2VideoDoNothing, +# Spreadsheet2VideoDoNothing, + Spreadsheet2VideoAddColumnSpreadsheet, Spreadsheet2VideoMultiplySpreadsheet ) @@ -41,7 +42,8 @@ class Spreadsheet2VideoExtension(ComfyExtension): Spreadsheet2VideoLoadText, Spreadsheet2VideoSequence, Spreadsheet2VideoFilesList, - Spreadsheet2VideoDoNothing, +# Spreadsheet2VideoDoNothing, + Spreadsheet2VideoAddColumnSpreadsheet, Spreadsheet2VideoMultiplySpreadsheet, ] diff --git a/pyproject.toml b/pyproject.toml index bbe4ca3..ab05cb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "Spreadsheet2Video-ComfyUI" -version = "1.0.7" +version = "1.0.8" description = "Use a spreadsheet as input for ComfyUI execution" license = { file = "LICENSE" } dependencies = [ diff --git a/tests/test_boolean.json b/tests/test_boolean.json index 384b105..5024a96 100644 --- a/tests/test_boolean.json +++ b/tests/test_boolean.json @@ -12,8 +12,8 @@ 1166.8738027901068 ], "size": [ - 312.483984375, - 158 + 312.453125, + 193.984375 ], "flags": {}, "order": 0, @@ -54,6 +54,8 @@ } ], "properties": { + "cnr_id": "Spreadsheet2Video-ComfyUI", + "ver": "145e15a6fd7c2f746444869550ee3c7a25e17126", "Node name for S&R": "Spreadsheet2VideoInputImage" }, "widgets_values": [ @@ -69,7 +71,7 @@ ], "size": [ 270, - 78 + 106 ], "flags": {}, "order": 4, @@ -120,23 +122,32 @@ 1234.79316455834 ], "size": [ - 264.0162109375, - 26 + 264, + 78 ], "flags": {}, - "order": 8, + "order": 7, "mode": 0, "inputs": [ { "name": "images", "type": "IMAGE", "link": 72 + }, + { + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": null } ], "outputs": [], "properties": { + "cnr_id": "Spreadsheet2Video-ComfyUI", + "ver": "145e15a6fd7c2f746444869550ee3c7a25e17126", "Node name for S&R": "Spreadsheet2VideoOutputImage" - } + }, + "widgets_values": [] }, { "id": 6, @@ -146,8 +157,8 @@ 1046.018577649322 ], "size": [ - 300.440416046162, - 58 + 300.40625, + 93.984375 ], "flags": {}, "order": 1, @@ -179,8 +190,8 @@ 1254.2865711858988 ], "size": [ - 286.73896268632825, - 58.36471886178333 + 286.71875, + 93.984375 ], "flags": {}, "order": 2, @@ -239,40 +250,6 @@ } ], "properties": { - "proxyWidgets": [ - [ - "27", - "text" - ], - [ - "13", - "width" - ], - [ - "13", - "height" - ], - [ - "28", - "unet_name" - ], - [ - "30", - "clip_name" - ], - [ - "29", - "vae_name" - ], - [ - "3", - "steps" - ], - [ - "3", - "control_after_generate" - ] - ], "cnr_id": "comfy-core", "ver": "0.3.73", "enableTabs": false, @@ -281,44 +258,33 @@ "hasSecondTab": false, "secondTabText": "Send Back", "secondTabOffset": 80, - "secondTabWidth": 65 - }, - "widgets_values": [] - }, - { - "id": 7, - "type": "EmptyImage", - "pos": [ - 1207.965006424334, - 719.2113395590205 - ], - "size": [ - 270, - 130 - ], - "flags": {}, - "order": 3, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 3 - ] - } - ], - "properties": { - "cnr_id": "comfy-core", - "ver": "0.19.3", - "Node name for S&R": "EmptyImage" + "secondTabWidth": 65, + "proxyWidgetErrorQuarantine": [ + { + "originalEntry": [ + "3", + "control_after_generate" + ], + "reason": "missingSubgraphInput", + "attemptedAtVersion": 1 + } + ], + "previewExposures": [ + { + "name": "$$canvas-image-preview", + "sourceNodeId": "3", + "sourcePreviewName": "$$canvas-image-preview" + } + ] }, "widgets_values": [ + "Latina female with thick wavy hair, harbor boats and pastel houses behind. Breezy seaside light, warm tones, cinematic close-up. ", 1024, 1024, - 1, - 0 + "z_image_turbo_bf16.safetensors", + "qwen_3_4b.safetensors", + "FLUX1\\ae.safetensors", + 8 ] }, { @@ -329,11 +295,11 @@ 704.5518187413677 ], "size": [ - 140, - 246 + 225, + 290 ], "flags": {}, - "order": 7, + "order": 5, "mode": 0, "inputs": [ { @@ -342,7 +308,13 @@ "link": 4 } ], - "outputs": [], + "outputs": [ + { + "name": "images", + "type": "IMAGE", + "links": null + } + ], "properties": { "cnr_id": "comfy-core", "ver": "0.19.3", @@ -362,14 +334,14 @@ 200 ], "flags": {}, - "order": 5, + "order": 3, "mode": 0, "inputs": [ { "name": "first_image", "shape": 7, "type": "IMAGE", - "link": 3 + "link": null } ], "outputs": [ @@ -379,9 +351,16 @@ "links": [ 4 ] + }, + { + "name": "AUDIO", + "type": "AUDIO", + "links": null } ], "properties": { + "cnr_id": "Spreadsheet2Video-ComfyUI", + "ver": "145e15a6fd7c2f746444869550ee3c7a25e17126", "Node name for S&R": "Spreadsheet2Video" }, "widgets_values": [ @@ -390,14 +369,6 @@ } ], "links": [ - [ - 3, - 7, - 0, - 1, - 0, - "IMAGE" - ], [ 4, 1, @@ -1045,7 +1016,7 @@ "secondTabWidth": 65 }, "widgets_values": [ - 1007068742280709, + 1045136092634709, "randomize", 8, 1, @@ -1124,7 +1095,6 @@ 200 ], "color": "#3f789e", - "font_size": 24, "flags": {} }, { @@ -1137,7 +1107,6 @@ 540 ], "color": "#3f789e", - "font_size": 24, "flags": {} }, { @@ -1150,7 +1119,6 @@ 413.6 ], "color": "#3f789e", - "font_size": 24, "flags": {} } ], @@ -1301,13 +1269,13 @@ "config": {}, "extra": { "ds": { - "scale": 1.1556864978176087, + "scale": 1.0506240889250995, "offset": [ - -1268.0988326073784, - -547.481125026469 + -1381.2506133337172, + -628.9646201878417 ] }, - "frontendVersion": "1.42.11", + "frontendVersion": "1.47.12", "VHS_latentpreview": false, "VHS_latentpreviewrate": 0, "VHS_MetadataImage": true, diff --git a/tests/test_spreadsheet.json b/tests/test_spreadsheet.json new file mode 100644 index 0000000..b3b99a0 --- /dev/null +++ b/tests/test_spreadsheet.json @@ -0,0 +1,181 @@ +{ + "id": "844d351e-ec61-4bdf-b8d9-26dfd091434b", + "revision": 0, + "last_node_id": 96, + "last_link_id": 189, + "nodes": [ + { + "id": 91, + "type": "PreviewAny", + "pos": [ + -1.841804357108301, + 1074.0426332603915 + ], + "size": [ + 364.28125, + 330.375 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "source", + "type": "*", + "link": 178 + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.19.3", + "Node name for S&R": "PreviewAny" + }, + "widgets_values": [] + }, + { + "id": 89, + "type": "Spreadsheet2VideoMultiplySpreadsheet", + "pos": [ + -585.6675584600498, + 1069.7731994235164 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 178 + ] + } + ], + "properties": { + "cnr_id": "Spreadsheet2Video-ComfyUI", + "ver": "7342d3ec4657599baab5be5dd4414663e9d0da1a", + "Node name for S&R": "Spreadsheet2VideoMultiplySpreadsheet" + }, + "widgets_values": [ + "column1,prompt\nworkflow1,One\nworkflow1,Two\nworkflow1,Three\nworkflow1", + "sampler\neuler\nheun" + ] + }, + { + "id": 95, + "type": "PreviewAny", + "pos": [ + 85.70426079452947, + 615.0534104526303 + ], + "size": [ + 406.703125, + 308.359375 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "source", + "type": "*", + "link": 189 + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.19.3", + "Node name for S&R": "PreviewAny" + }, + "widgets_values": [] + }, + { + "id": 96, + "type": "Spreadsheet2VideoAddColumnSpreadsheet", + "pos": [ + -569.2349843700941, + 598.1665514472247 + ], + "size": [ + 433.046875, + 327.765625 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 189 + ] + } + ], + "properties": { + "cnr_id": "Spreadsheet2Video-ComfyUI", + "ver": "145e15a6fd7c2f746444869550ee3c7a25e17126", + "Node name for S&R": "Spreadsheet2VideoAddColumnSpreadsheet" + }, + "widgets_values": [ + "column1,prompt\nworkflow1,One\nworkflow1,Two\nworkflow1,Three\nworkflow1,Four\nworkflow1", + "sampler\neuler\nheun" + ] + } + ], + "links": [ + [ + 178, + 89, + 0, + 91, + 0, + "STRING" + ], + [ + 189, + 96, + 0, + 95, + 0, + "STRING" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 0.7751878709002787, + "offset": [ + 835.409880916017, + -212.2218877684238 + ] + }, + "frontendVersion": "1.47.12", + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4 +} \ No newline at end of file