7 Commits
Author SHA1 Message Date
Laurent Erignoux ab9f2133ef Adding a node to open a generated pecha kucha presentation 2025-10-26 21:46:38 +08:00
Laurent Erignoux 6891d35cc1 Migrating Pecha Kucha node to v3 schema 2025-10-19 21:51:20 +08:00
Laurent Erignoux 96c128673e Updating version to 1.0.1 2025-08-02 10:12:50 +08:00
Laurent Erignoux 5b07327ea0 Merge pull request #2 from ComfyNodePRs/update-publish-yaml
Update Github Action for Publishing to Comfy Registry
2025-08-02 09:50:45 +08:00
snomiao 453c1311e2 chore(publish): update workflow for node publishing
- Add permissions for issue writing
- Update condition to check repository owner
- Change publish-node-action version from main to v1
2025-05-14 02:45:27 +00:00
Laurent Erignoux 606b65af25 Fixing dummy typo for republish 2025-05-12 23:42:08 +08:00
Laurent Erignoux fe26ccbdae Merge pull request #1 from lerignoux/registry
Registry
2025-05-12 23:41:07 +08:00
8 changed files with 301 additions and 117 deletions
+5 -1
View File
@@ -7,14 +7,18 @@ on:
paths: paths:
- "pyproject.toml" - "pyproject.toml"
permissions:
issues: write
jobs: jobs:
publish-node: publish-node:
name: Publish Custom Node to registry name: Publish Custom Node to registry
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'lerignoux' }}
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Publish Custom Node - name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main uses: Comfy-Org/publish-node-action@v1
with: with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here. personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here.
+21 -12
View File
@@ -1,16 +1,25 @@
from comfy_api.latest import ComfyExtension, io
from .download_pecha_kucha import DownloadPechaKucha
from .pecha_kucha import GeneratePowerpoint from .pecha_kucha import GeneratePowerpoint
from .split_prompt import SplitPrompt 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 WEB_DIRECTORY = "./web"
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 [
DownloadPechaKucha,
GeneratePowerpoint,
SplitPrompt,
]
# can be declared async or not, both will work
async def comfy_entrypoint() -> PechaKucha:
return PechaKucha()
__all__ = ["WEB_DIRECTORY"]
+38
View File
@@ -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))
+66 -21
View File
@@ -1,6 +1,8 @@
{ {
"last_node_id": 29, "id": "594262c2-cffc-4068-8fa7-d4ea47a1869d",
"last_link_id": 31, "revision": 0,
"last_node_id": 30,
"last_link_id": 32,
"nodes": [ "nodes": [
{ {
"id": 19, "id": 19,
@@ -72,10 +74,10 @@
{ {
"name": "CONDITIONING", "name": "CONDITIONING",
"type": "CONDITIONING", "type": "CONDITIONING",
"slot_index": 0,
"links": [ "links": [
16 16
], ]
"slot_index": 0
} }
], ],
"properties": { "properties": {
@@ -125,10 +127,10 @@
{ {
"name": "LATENT", "name": "LATENT",
"type": "LATENT", "type": "LATENT",
"slot_index": 0,
"links": [ "links": [
19 19
], ]
"slot_index": 0
} }
], ],
"properties": { "properties": {
@@ -174,10 +176,10 @@
{ {
"name": "IMAGE", "name": "IMAGE",
"type": "IMAGE", "type": "IMAGE",
"slot_index": 0,
"links": [ "links": [
28 28
], ]
"slot_index": 0
} }
], ],
"properties": { "properties": {
@@ -202,14 +204,13 @@
"inputs": [], "inputs": [],
"outputs": [ "outputs": [
{ {
"name": "Split Prompts", "name": "split prompt",
"type": "STRING", "type": "STRING",
"shape": 6, "slot_index": 0,
"links": [ "links": [
24, 24,
31 31
], ]
"slot_index": 0
} }
], ],
"properties": { "properties": {
@@ -273,19 +274,19 @@
{ {
"name": "MODEL", "name": "MODEL",
"type": "MODEL", "type": "MODEL",
"slot_index": 0,
"links": [ "links": [
13 13
], ]
"slot_index": 0
}, },
{ {
"name": "CLIP", "name": "CLIP",
"type": "CLIP", "type": "CLIP",
"slot_index": 1,
"links": [ "links": [
14, 14,
15 15
], ]
"slot_index": 1
}, },
{ {
"name": "VAE", "name": "VAE",
@@ -302,6 +303,39 @@
"epicrealismXL_v5Ultimate.safetensors" "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, "id": 27,
"type": "GeneratePowerpoint", "type": "GeneratePowerpoint",
@@ -335,8 +369,10 @@
{ {
"name": "filename", "name": "filename",
"type": "STRING", "type": "STRING",
"links": null, "slot_index": 0,
"slot_index": 0 "links": [
32
]
} }
], ],
"properties": { "properties": {
@@ -438,6 +474,14 @@
27, 27,
1, 1,
"STRING" "STRING"
],
[
32,
27,
0,
30,
0,
"STRING"
] ]
], ],
"groups": [], "groups": [],
@@ -446,10 +490,11 @@
"ds": { "ds": {
"scale": 0.9090909090909091, "scale": 0.9090909090909091,
"offset": [ "offset": [
812.004781836218, -220.895218163782,
-127.85274297232016 -490.8527429723203
] ]
} },
"frontendVersion": "1.27.10"
}, },
"version": 0.4 "version": 0.4
} }
+65 -54
View File
@@ -1,11 +1,10 @@
import io
import json
import logging import logging
import os import os
import numpy import numpy
from PIL import Image from PIL import Image
from PIL.PngImagePlugin import PngInfo from PIL.PngImagePlugin import PngInfo
from comfy_api.latest import io
import folder_paths import folder_paths
from .presentation_generation.pptx import Pptx from .presentation_generation.pptx import Pptx
@@ -13,61 +12,68 @@ from .presentation_generation.pptx import Pptx
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class GeneratePowerpoint: class GeneratePowerpoint(io.ComfyNode):
""" """
A node to generate a PechaKucha presentation A node to generate a PechaKucha presentation
""" """
def __init__(self):
self.output_dir = folder_paths.get_output_directory() INPUT_IS_LIST = True
self.temp_dir = folder_paths.get_temp_directory() output_dir = folder_paths.get_output_directory()
self.compress_level = 4 temp_dir = folder_paths.get_temp_directory()
pass compress_level = 4
@classmethod @classmethod
def INPUT_TYPES(s): def define_schema(cls) -> io.Schema:
return { return io.Schema(
"required": { node_id="GeneratePowerpoint",
"images": ("IMAGE",), display_name="Generate PechaKucha Powerpoint",
"title": ("STRING", { description="Generates a Pecha Kucha generation from a list of images and prompts",
"multiline": False, category="pecha_kucha",
"default": "My pecha", inputs=[
"tooltip": "The title of this Presentation" io.Image.Input("images"),
}), io.String.Input(
"titles": ("STRING", { "title",
"multiline": False, default="My pecha",
"default": "", multiline=False,
"tooltip": "the titles for each slide." tooltip="The title of this Presentation.",
}), ),
"slide_duration": ("INT", { io.String.Input(
"default": 20, "titles",
"min": 0, default="",
"max": 300, multiline=False,
"step": 1, tooltip="the titles for each slide.",
"display": "number", ),
"tooltip": "Each slide duration in seconds." io.Int.Input(
}), "slide_duration",
"show_titles": ("BOOLEAN", { default=20,
"default": False, min=0,
"tooltip": "Show each prompt as slide title. Needs to provide the titles input." 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" @classmethod
DESCRIPTION = "Generates a Pecha Kucha generation from a list of images and prompts" def save_images(cls, images, filename_prefix="PechaKucha"):
FUNCTION = "generate_pptx" full_output_folder, filename, counter, subfolder, filename_prefix = (
INPUT_IS_LIST = True folder_paths.get_save_image_path(
OUTPUT_NODE = True filename_prefix, cls.temp_dir, images[0].shape[1], images[0].shape[0]
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])
results = list() results = list()
log.debug(f"received {len(images)} images") 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}") 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)) img = Image.fromarray(numpy.clip(i, 0, 255).astype(numpy.uint8))
metadata = None metadata = None
metadata = PngInfo() metadata = PngInfo()
@@ -75,24 +81,29 @@ class GeneratePowerpoint:
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number)) filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
file = f"{filename_with_batch_num}_{counter:05}_.png" file = f"{filename_with_batch_num}_{counter:05}_.png"
path = os.path.join(full_output_folder, file) 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) results.append(path)
counter += 1 counter += 1
return results 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") 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 # The following inputs are also considered as list but only contain a single value
title = title[0] title = title[0]
slide_duration = slide_duration[0] slide_duration = slide_duration[0]
show_titles = show_titles[0] show_titles = show_titles[0]
output_folder = "./output/pecha_kucha" 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}") log.info(f"Generated presentation {filename}")
return (filename,) return io.NodeOutput(
filename,
)
@classmethod @classmethod
def IS_CHANGED(s, images, title, titles, slide_duration, show_titles): def fingerprint_inputs(cls, **kwargs):
return float("NaN") return float("NaN")
+2 -2
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "ComfyUI-PechaKucha" name = "ComfyUI-PechaKucha"
description = "Nodes to generate a pecha kucha presentation in ComfyUI" description = "Nodes to generate a pecha-kucha presentation in ComfyUI"
version = "1.0.0" version = "1.0.1"
license = { file = "LICENSE" } license = { file = "LICENSE" }
dependencies = ["python-pptx >= 1.0.2"] dependencies = ["python-pptx >= 1.0.2"]
+34 -27
View File
@@ -1,47 +1,54 @@
import logging import logging
from comfy_api.latest import io
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class SplitPrompt: class SplitPrompt(io.ComfyNode):
""" """
A simple node to split a prompt into a batch of prompt. A simple node to split a prompt into a batch of prompt.
Inspired from [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) Inspired from [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack)
""" """
@classmethod @classmethod
def INPUT_TYPES(cls): def define_schema(cls) -> io.Schema:
return { return io.Schema(
"required": { node_id="SplitPrompt",
"prompt": ( display_name="Split prompt",
"STRING", description="Split a prompt in a list of prompts",
{"multiline": True, "default": "First Prompt \nSecond Prompt", "tooltip": "A multiline prompt that will be split to batch generate"} category="pecha_kucha",
) inputs=[
}, io.String.Input(
"optional": { "prompt",
"split_on": ( default="First Prompt \nSecond Prompt",
"STRING", multiline=True,
{"default": '\n', "tooltip": "Will split the input prompt on this character"} tooltip="A multiline prompt that will be split to batch generate.",
), ),
"trim": ( io.String.Input(
"BOOLEAN", "split_on",
{"default": True, "tooltip": "Will trim prompts and remove empty ones"} 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,) OUTPUT_IS_LIST = (True,)
RETURN_NAMES = ("Split Prompts",)
RETURN_TYPES = ("STRING",)
@staticmethod @classmethod
def IS_CHANGED(prompt, split_on='\n', trim=True): def fingerprint_inputs(cls, prompt, split_on="\n", trim=True):
return f"trim:{trim}_split:{split_on}_prompt:{prompt}" return f"trim:{trim}_split:{split_on}_prompt:{prompt}"
@staticmethod @classmethod
def split(prompt, split_on='\n', trim=True): def execute(cls, prompt, split_on="\n", trim=True):
prompts = prompt.split(split_on) prompts = prompt.split(split_on)
if trim: if trim:
prompts = [prompt.strip() for prompt in prompts] prompts = [prompt.strip() for prompt in prompts]
+70
View File
@@ -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;
},
});