Compare commits
2
Commits
1.0.1
...
migration_v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab9f2133ef | ||
|
|
6891d35cc1 |
+21
-12
@@ -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"]
|
||||
|
||||
@@ -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))
|
||||
@@ -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
@@ -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")
|
||||
|
||||
+34
-27
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user