16 Commits
Author SHA1 Message Date
Erignoux Laurent bf59ccd9d9 Updating node to 1.1.2 2025-10-30 16:07:56 +08:00
Erignoux Laurent 83ed006088 Using relative path for download. 2025-10-30 16:07:26 +08:00
Erignoux Laurent 4547670d6e Updating project version to 1.1.1 2025-10-30 16:07:26 +08:00
Erignoux Laurent b3fa1129dd Using relative path to fetch app.js to ensure it works behind a url rewritting proxy 2025-10-30 16:07:26 +08:00
Laurent Erignoux f2466984ff Updating version to 1.1.0 2025-10-26 21:52:20 +08:00
Laurent Erignoux 52440aade3 Merge pull request #3 from lerignoux/migration_v3
Migration v3
2025-10-26 21:48:38 +08:00
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
Laurent Erignoux 9bf6b28c3d Adding registry publish github action 2025-05-12 23:40:25 +08:00
Laurent Erignoux b7f745dfcc Adding comfy registry pyproject 2025-05-12 23:36:18 +08:00
Laurent Erignoux 42bb206dd5 Remove un-needed depencency 2025-05-12 23:36:00 +08:00
9 changed files with 332 additions and 115 deletions
+24
View File
@@ -0,0 +1,24 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'lerignoux' }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@v1
with:
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 .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)"
}
WEB_DIRECTORY = "./web"
__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,
"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
}
+65 -54
View File
@@ -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")
+14
View File
@@ -0,0 +1,14 @@
[project]
name = "ComfyUI-PechaKucha"
description = "Nodes to generate a pecha-kucha presentation in ComfyUI"
version = "1.1.2"
license = { file = "LICENSE" }
dependencies = ["python-pptx >= 1.0.2"]
[project.urls]
Repository = "https://github.com/lerignoux/ComfyUI-PechaKucha.git"
[tool.comfy]
PublisherId = "lerignoux"
DisplayName = "ComfyUI Pecha Kucha"
Icon = "https://www.pechakucha.com/static/media/PK_logo_wide_darkmode.b18e1f231e00fe884e4169f95cd0dc26.svg"
-1
View File
@@ -1,2 +1 @@
python-pptx>=1.0.2
reportlab>=4.4.0
+34 -27
View File
@@ -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]
+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;
},
});