From 6891d35cc1f19b9bf3d105ded48ee25c69eac783 Mon Sep 17 00:00:00 2001 From: Laurent Erignoux Date: Sat, 18 Oct 2025 23:00:26 +0800 Subject: [PATCH 1/2] Migrating Pecha Kucha node to v3 schema --- __init__.py | 29 +++++++----- pecha_kucha.py | 119 ++++++++++++++++++++++++++---------------------- split_prompt.py | 61 ++++++++++++++----------- 3 files changed, 116 insertions(+), 93 deletions(-) diff --git a/__init__.py b/__init__.py index e2e39a9..8c198b5 100644 --- a/__init__.py +++ b/__init__.py @@ -1,16 +1,21 @@ +from comfy_api.latest import ComfyExtension, io + from .pecha_kucha import GeneratePowerpoint from .split_prompt import SplitPrompt -# A dictionary that contains all nodes you want to export with their names -# NOTE: names should be globally unique -NODE_CLASS_MAPPINGS = { - "GeneratePowerpoint": GeneratePowerpoint, - "SplitPrompt": SplitPrompt -} -# A dictionary that contains the friendly/humanly readable titles for the nodes -NODE_DISPLAY_NAME_MAPPINGS = { - "GeneratePowerpoint": "Generate PechaKucha Powerpoint", - "SplitPrompt": "Split Prompt (To Batch)" -} -__all__ = [NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS] +class PechaKucha(ComfyExtension): + # must be declared as async + async def get_node_list(self) -> list[type[io.ComfyNode]]: + return [ + GeneratePowerpoint, + SplitPrompt, + ] + + +# can be declared async or not, both will work +async def comfy_entrypoint() -> PechaKucha: + return PechaKucha() + + +__all__ = ["WEB_DIRECTORY"] diff --git a/pecha_kucha.py b/pecha_kucha.py index 59ab66b..5e7cc4c 100644 --- a/pecha_kucha.py +++ b/pecha_kucha.py @@ -1,11 +1,10 @@ -import io -import json import logging import os import numpy from PIL import Image from PIL.PngImagePlugin import PngInfo +from comfy_api.latest import io import folder_paths from .presentation_generation.pptx import Pptx @@ -13,61 +12,68 @@ from .presentation_generation.pptx import Pptx log = logging.getLogger(__name__) -class GeneratePowerpoint: +class GeneratePowerpoint(io.ComfyNode): """ A node to generate a PechaKucha presentation """ - def __init__(self): - self.output_dir = folder_paths.get_output_directory() - self.temp_dir = folder_paths.get_temp_directory() - self.compress_level = 4 - pass + + INPUT_IS_LIST = True + output_dir = folder_paths.get_output_directory() + temp_dir = folder_paths.get_temp_directory() + compress_level = 4 @classmethod - def INPUT_TYPES(s): - return { - "required": { - "images": ("IMAGE",), - "title": ("STRING", { - "multiline": False, - "default": "My pecha", - "tooltip": "The title of this Presentation" - }), - "titles": ("STRING", { - "multiline": False, - "default": "", - "tooltip": "the titles for each slide." - }), - "slide_duration": ("INT", { - "default": 20, - "min": 0, - "max": 300, - "step": 1, - "display": "number", - "tooltip": "Each slide duration in seconds." - }), - "show_titles": ("BOOLEAN", { - "default": False, - "tooltip": "Show each prompt as slide title. Needs to provide the titles input." - }) - }, - } + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="GeneratePowerpoint", + display_name="Generate PechaKucha Powerpoint", + description="Generates a Pecha Kucha generation from a list of images and prompts", + category="pecha_kucha", + inputs=[ + io.Image.Input("images"), + io.String.Input( + "title", + default="My pecha", + multiline=False, + tooltip="The title of this Presentation.", + ), + io.String.Input( + "titles", + default="", + multiline=False, + tooltip="the titles for each slide.", + ), + io.Int.Input( + "slide_duration", + default=20, + min=0, + max=300, + step=1, + display_mode=io.NumberDisplay.number, + tooltip="Each slide duration in seconds.", + ), + io.Boolean.Input( + "show_titles", + default=False, + tooltip="Show each prompt as slide title. Needs to provide the titles input.", + ), + ], + is_output_node=True, + outputs=[io.String.Output(display_name="filename")], + ) - CATEGORY = "pecha_kucha" - DESCRIPTION = "Generates a Pecha Kucha generation from a list of images and prompts" - FUNCTION = "generate_pptx" - INPUT_IS_LIST = True - OUTPUT_NODE = True - RETURN_NAMES = ("filename",) - RETURN_TYPES = ("STRING",) - - def save_images(self, images, filename_prefix="PechaKucha"): - full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.temp_dir, images[0].shape[1], images[0].shape[0]) + @classmethod + def save_images(cls, images, filename_prefix="PechaKucha"): + full_output_folder, filename, counter, subfolder, filename_prefix = ( + folder_paths.get_save_image_path( + filename_prefix, cls.temp_dir, images[0].shape[1], images[0].shape[0] + ) + ) results = list() log.debug(f"received {len(images)} images") - for (batch_number, image) in enumerate(images): + for batch_number, image in enumerate(images): log.debug(f"start saving image {batch_number}, shape: {image.shape}") - i = 255. * image.cpu().numpy().squeeze() + i = 255.0 * image.cpu().numpy().squeeze() img = Image.fromarray(numpy.clip(i, 0, 255).astype(numpy.uint8)) metadata = None metadata = PngInfo() @@ -75,24 +81,29 @@ class GeneratePowerpoint: filename_with_batch_num = filename.replace("%batch_num%", str(batch_number)) file = f"{filename_with_batch_num}_{counter:05}_.png" path = os.path.join(full_output_folder, file) - img.save(path, pnginfo=metadata, compress_level=self.compress_level) + img.save(path, pnginfo=metadata, compress_level=cls.compress_level) results.append(path) counter += 1 return results - def generate_pptx(self, images, title, titles, slide_duration, show_titles): + @classmethod + def execute(cls, images, title, titles, slide_duration, show_titles): log.info(f"start pecha_kucha generation") - images = self.save_images(images) + images = cls.save_images(images) # The following inputs are also considered as list but only contain a single value title = title[0] slide_duration = slide_duration[0] show_titles = show_titles[0] output_folder = "./output/pecha_kucha" - filename = Pptx(output_folder).generate(title, slide_duration, images, show_titles, titles) + filename = Pptx(output_folder).generate( + title, slide_duration, images, show_titles, titles + ) log.info(f"Generated presentation {filename}") - return (filename,) + return io.NodeOutput( + filename, + ) @classmethod - def IS_CHANGED(s, images, title, titles, slide_duration, show_titles): + def fingerprint_inputs(cls, **kwargs): return float("NaN") diff --git a/split_prompt.py b/split_prompt.py index 50b8080..662bc53 100644 --- a/split_prompt.py +++ b/split_prompt.py @@ -1,47 +1,54 @@ import logging +from comfy_api.latest import io + log = logging.getLogger(__name__) -class SplitPrompt: +class SplitPrompt(io.ComfyNode): """ A simple node to split a prompt into a batch of prompt. Inspired from [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) """ + @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "prompt": ( - "STRING", - {"multiline": True, "default": "First Prompt \nSecond Prompt", "tooltip": "A multiline prompt that will be split to batch generate"} - ) - }, - "optional": { - "split_on": ( - "STRING", - {"default": '\n', "tooltip": "Will split the input prompt on this character"} + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="SplitPrompt", + display_name="Split prompt", + description="Split a prompt in a list of prompts", + category="pecha_kucha", + inputs=[ + io.String.Input( + "prompt", + default="First Prompt \nSecond Prompt", + multiline=True, + tooltip="A multiline prompt that will be split to batch generate.", ), - "trim": ( - "BOOLEAN", - {"default": True, "tooltip": "Will trim prompts and remove empty ones"} + io.String.Input( + "split_on", + default="\n", + multiline=False, + tooltip="Will split the input prompt on this character.", ), - } - } + io.Boolean.Input( + "trim", + default=True, + tooltip="Will trim prompts and remove empty ones.", + ), + ], + is_output_node=False, + outputs=[io.String.Output(display_name="split prompt")], + ) - CATEGORY = "pecha_kucha" - DESCRIPTION = "Split a prompt in a list of prompts" - FUNCTION = "split" OUTPUT_IS_LIST = (True,) - RETURN_NAMES = ("Split Prompts",) - RETURN_TYPES = ("STRING",) - @staticmethod - def IS_CHANGED(prompt, split_on='\n', trim=True): + @classmethod + def fingerprint_inputs(cls, prompt, split_on="\n", trim=True): return f"trim:{trim}_split:{split_on}_prompt:{prompt}" - @staticmethod - def split(prompt, split_on='\n', trim=True): + @classmethod + def execute(cls, prompt, split_on="\n", trim=True): prompts = prompt.split(split_on) if trim: prompts = [prompt.strip() for prompt in prompts] From ab9f2133ef0e17b73fe074db9dea0c8305e1751e Mon Sep 17 00:00:00 2001 From: Laurent Erignoux Date: Sun, 19 Oct 2025 21:52:03 +0800 Subject: [PATCH 2/2] Adding a node to open a generated pecha kucha presentation --- __init__.py | 4 ++ download_pecha_kucha.py | 38 +++++++++++ example_workflows/SimplePechaKucha.json | 87 +++++++++++++++++++------ web/js/download_pecha_kucha.js | 70 ++++++++++++++++++++ 4 files changed, 178 insertions(+), 21 deletions(-) create mode 100644 download_pecha_kucha.py create mode 100644 web/js/download_pecha_kucha.js diff --git a/__init__.py b/__init__.py index 8c198b5..905cfd0 100644 --- a/__init__.py +++ b/__init__.py @@ -1,13 +1,17 @@ from comfy_api.latest import ComfyExtension, io +from .download_pecha_kucha import DownloadPechaKucha from .pecha_kucha import GeneratePowerpoint from .split_prompt import SplitPrompt +WEB_DIRECTORY = "./web" + class PechaKucha(ComfyExtension): # must be declared as async async def get_node_list(self) -> list[type[io.ComfyNode]]: return [ + DownloadPechaKucha, GeneratePowerpoint, SplitPrompt, ] diff --git a/download_pecha_kucha.py b/download_pecha_kucha.py new file mode 100644 index 0000000..c4bf5e7 --- /dev/null +++ b/download_pecha_kucha.py @@ -0,0 +1,38 @@ +import logging + +from comfy_api.latest import io, ui + +log = logging.getLogger(__name__) + + +class DownloadPechaKucha(io.ComfyNode): + """ + A simple node to view/download a generated PechaKucha Powerpoint. + """ + + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="DownloadPechaKucha", + display_name="Download a generated PechaKucha Powerpoint", + description="Download a generated pecha kucha presentation", + category="pecha_kucha", + inputs=[ + io.String.Input( + "filename", + default="", + multiline=False, + tooltip="The path of this Presentation.", + ) + ], + is_output_node=True, + outputs=[], + ) + + @classmethod + def fingerprint_inputs(cls, filename): + return float("NaN") + + @classmethod + def execute(cls, filename): + return io.NodeOutput(filename, ui=ui.PreviewText(filename, cls=cls)) diff --git a/example_workflows/SimplePechaKucha.json b/example_workflows/SimplePechaKucha.json index 0fad6f5..9949eba 100644 --- a/example_workflows/SimplePechaKucha.json +++ b/example_workflows/SimplePechaKucha.json @@ -1,6 +1,8 @@ { - "last_node_id": 29, - "last_link_id": 31, + "id": "594262c2-cffc-4068-8fa7-d4ea47a1869d", + "revision": 0, + "last_node_id": 30, + "last_link_id": 32, "nodes": [ { "id": 19, @@ -72,10 +74,10 @@ { "name": "CONDITIONING", "type": "CONDITIONING", + "slot_index": 0, "links": [ 16 - ], - "slot_index": 0 + ] } ], "properties": { @@ -125,10 +127,10 @@ { "name": "LATENT", "type": "LATENT", + "slot_index": 0, "links": [ 19 - ], - "slot_index": 0 + ] } ], "properties": { @@ -174,10 +176,10 @@ { "name": "IMAGE", "type": "IMAGE", + "slot_index": 0, "links": [ 28 - ], - "slot_index": 0 + ] } ], "properties": { @@ -202,14 +204,13 @@ "inputs": [], "outputs": [ { - "name": "Split Prompts", + "name": "split prompt", "type": "STRING", - "shape": 6, + "slot_index": 0, "links": [ 24, 31 - ], - "slot_index": 0 + ] } ], "properties": { @@ -273,19 +274,19 @@ { "name": "MODEL", "type": "MODEL", + "slot_index": 0, "links": [ 13 - ], - "slot_index": 0 + ] }, { "name": "CLIP", "type": "CLIP", + "slot_index": 1, "links": [ 14, 15 - ], - "slot_index": 1 + ] }, { "name": "VAE", @@ -302,6 +303,39 @@ "epicrealismXL_v5Ultimate.safetensors" ] }, + { + "id": 30, + "type": "DownloadPechaKucha", + "pos": [ + 1904.9952392578125, + 915.4524536132812 + ], + "size": [ + 397.9166564941406, + 82 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "filename", + "type": "STRING", + "widget": { + "name": "filename" + }, + "link": 32 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "DownloadPechaKucha" + }, + "widgets_values": [ + "", + "" + ] + }, { "id": 27, "type": "GeneratePowerpoint", @@ -335,8 +369,10 @@ { "name": "filename", "type": "STRING", - "links": null, - "slot_index": 0 + "slot_index": 0, + "links": [ + 32 + ] } ], "properties": { @@ -438,6 +474,14 @@ 27, 1, "STRING" + ], + [ + 32, + 27, + 0, + 30, + 0, + "STRING" ] ], "groups": [], @@ -446,10 +490,11 @@ "ds": { "scale": 0.9090909090909091, "offset": [ - 812.004781836218, - -127.85274297232016 + -220.895218163782, + -490.8527429723203 ] - } + }, + "frontendVersion": "1.27.10" }, "version": 0.4 } \ No newline at end of file diff --git a/web/js/download_pecha_kucha.js b/web/js/download_pecha_kucha.js new file mode 100644 index 0000000..b75b2f5 --- /dev/null +++ b/web/js/download_pecha_kucha.js @@ -0,0 +1,70 @@ +import { app } from "/scripts/app.js"; + +app.registerExtension({ + name: "pecha_kucha.DownloadPechaKucha", + nodeCreated(node) { + if (node.comfyClass !== "DownloadPechaKucha") return; + console.log("Creating DownloadPechaKucha node."); + + const onExecuted = node.onExecuted; + + node.onExecuted = function (message) { + onExecuted?.apply(this, arguments); + + let filePath = message.text[0]; + console.log(`Powerpoint path received : ${filePath}`); + + if (!filePath) { + const msg = "Unable to get presentation File path."; + console.error(msg); + useToastStore().addAlert(msg); + } + + const modelWidget = node.widgets?.find( + (w) => w.name === "Open Presentation", + ); + + if (modelWidget) { + modelWidget.value = filePath.replaceAll("\\", "/"); + modelWidget.disabled = false; + } + }; + + const download = () => { + let filePath = ""; + + const fileNameWidget = node.widgets?.find( + (w) => w.name === "Open Presentation", + ); + if (!fileNameWidget) { + console.error("Could not find Open presentation widget."); + } + filePath = fileNameWidget.value; + if (!filePath) { + console.error(`Could not find presentation file: ${filePath}`); + } + + console.log("Downloading presentation:", filePath); + + // Remove any leading ./output/ prefix if present + const cleanFilePath = filePath.replace(/^\.\/output\//, ""); + const folderName = cleanFilePath.replace(/[^\\/]+$/, ""); + const fileNameMatch = cleanFilePath.match(/[^\\/]+$/); + const fileName = fileNameMatch ? fileNameMatch[0] : ""; + const viewUrl = `/api/view?subfolder=${encodeURIComponent(folderName)}&filename=${encodeURIComponent(fileName)}&type=output`; + + // Open the presentation in a new tab + window.open(viewUrl, "_blank"); + }; + + // Create the button widget (initially disabled until we get the presentation file available) + const downloadButton = node.addWidget( + "button", + "Open Presentation", + "", + download, + ); + downloadButton.disabled = true; + node.downloadButton = downloadButton; + }, +});