Adding custom nodes

This commit is contained in:
Pedro Gengo
2024-09-19 17:53:55 -03:00
parent 774b0361a9
commit 6470848f02
15 changed files with 1508 additions and 1 deletions
+172
View File
@@ -0,0 +1,172 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# VS Code
.vscode/
# macOS
.DS_Store
# Windows
Thumbs.db
config.ini
node.zip
+82 -1
View File
@@ -1 +1,82 @@
# ComfyUI-LumaAI-API
# ComfyUI-LumaAI-API
<p align="center">
<img src="./assets/luma_logo.png" alt="LumaAI Logo" width="200">
</p>
This is a custom node for ComfyUI that allows you to use the Luma AI API directly in ComfyUI. Luma AI API is based on top of [Dream Machine](https://lumalabs.ai/dream-machine/api), which is a video generation model developed by Luma. For more information, see [Luma AI API Documentation](https://docs.lumalabs.ai/docs/api).
## Requirements
Before using this node, you need to have an LumaAI API key. Get one [here](https://lumalabs.ai/dream-machine/api). If you want to use images as input, you will need to host them. As a suggestion, you can use [ImgBB](https://api.imgbb.com/) to host your images.
## Installation
### Installing manually
1. Navigate to the `ComfyUI/custom_nodes` directory.
2. Clone this repository:
```
git clone https://github.com/lumalabs/ComfyUI-LumaAI-API.git
```
The path should be `ComfyUI/custom_nodes/ComfyUI-LumaAI-API/*`, where `*` represents all the files in this repo.
3. Run `cd ComfyUI-LumaAI-API && pip install -r requirements.txt` to install the dependencies.
4. If you don't want to expose your Luma API key, you can add it to the `config.ini` file and keep it empty in the node.
5. Start ComfyUI and enjoy using the LumaAI API node!
### Installing with ComfyUI-Manager
1. Open ComfyUI-Manager and install the LumaAI API node (ComfyUI-LumaAI-API).
### Installing with Comfy Registry
1. Run `comfy node registry-install comfyui-lumaai-api` to install the node.
## Nodes
Most of the nodes allow you to save locally the output video. If you keep the default `filename` (empty string), the video will be saved in the `outputs` folder using the `generation_id` as the name.
### LumaAIClient
This node is used to create a LumaAI client.
### LumaText2Video
This node is used to generate a video from a text prompt.
### LumaImage2Video
This node is used to generate a video from an image. The image can be used as the first or last frame.
### LumaInterpolateGenerations
This node is used to interpolate between two generations.
### LumaExtendGeneration
This node is used to extend a generation. You can choose to extend before or after the generation.
### LumaPreviewVideo
This node is used to preview a video. The video is resized to 768px to look better on ComfyUI.
### ImgBBUpload
This node is used to upload an image to ImgBB and return the URL. We need this because Luma API currently only supports image urls as input.
To use this node, you need to have an ImgBB API key. Create an account and get one [here](https://api.imgbb.com/).
## Examples
For examples, see [workflows folder](./workflows). To use, just download the workflow json and import it into ComfyUI.
## API Documentation
For more information about the Luma AI API, see [Luma AI API Documentation](https://docs.lumalabs.ai/docs/api).
## Pricing
For pricing, see [Luma AI Pricing](https://lumalabs.ai/dream-machine/api/pricing).
+4
View File
@@ -0,0 +1,4 @@
from .py.nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
WEB_DIRECTORY = "./web"
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

+2
View File
@@ -0,0 +1,2 @@
[API]
LUMAAI_API_KEY =
+73
View File
@@ -0,0 +1,73 @@
# Based on https://github.com/revirevy/Comfyui_saveimage_imgbb/blob/main/ImgBBUploader.py
import requests
import base64
from PIL import Image
import io
import numpy as np
class ImgBBUpload:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"api_key": ("STRING", {"default": "", "multiline": False}),
"expire": ("BOOLEAN", {"default": False}),
"expiration_time": (
"INT",
{"default": 60, "min": 60, "max": 15552000, "step": 1},
),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("image_url",)
FUNCTION = "upload"
CATEGORY = "image/upload"
def upload(self, image, api_key, expire, expiration_time):
"""
Upload an image to ImgBB and return the URL.
"""
if not api_key:
raise ValueError("API Key is required")
# Convert image tensor to PIL Image
img = image[0]
img = 255.0 * img.cpu().numpy()
img = Image.fromarray(np.clip(img, 0, 255).astype(np.uint8))
# Save PIL Image to buffer
buffer = io.BytesIO()
img.save(buffer, format="PNG")
buffer.seek(0)
base64_image = base64.b64encode(buffer.getvalue()).decode("utf-8")
url = f"https://api.imgbb.com/1/upload?key={api_key}"
if expire:
url += f"&expiration={expiration_time}"
payload = {
"image": base64_image,
}
try:
response = requests.post(url, data=payload)
result = response.json()
if result.get("success"):
return result["data"]["url"], result["data"]["delete_url"]
else:
error_message = result.get("error", {}).get("message", "Unknown error")
raise ValueError(f"Error: {error_message}")
except Exception as e:
raise ValueError(f"Error: {str(e)}")
NODE_CLASS_MAPPINGS = {"ImgBBUpload": ImgBBUpload}
NODE_DISPLAY_NAME_MAPPINGS = {"ImgBBUpload": "Upload to ImgBB"}
+401
View File
@@ -0,0 +1,401 @@
import configparser
import os
import requests
import time
from lumaai import LumaAI
import folder_paths
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
config_path = os.path.join(parent_dir, "config.ini")
config = configparser.ConfigParser()
config.read(config_path)
try:
luma_api_key = config['API']['LUMAAI_API_KEY']
if luma_api_key != "":
os.environ["LUMAAI_API_KEY"] = luma_api_key
else:
print("Warning: LUMAAI_API_KEY is empty in config.ini")
except KeyError:
print("Error: LUMAAI_API_KEY not found in config.ini")
def download_file(url, file_name):
response = requests.get(url, stream=True)
with open(file_name, "wb") as file:
file.write(response.content)
print(f"File downloaded as {file_name}")
def parse_filename(filename):
# Remove file extension if present
filename = os.path.splitext(filename)[0]
# Check if the filename ends with a directory separator
if filename.endswith(os.path.sep):
# If it does, we'll use the generation_id as the filename later
directory = filename
filename = ""
else:
# Otherwise, split the path into directory and filename
directory, filename = os.path.split(filename)
# Create directories if they don't exist
output_path = folder_paths.get_output_directory()
full_directory = os.path.join(output_path, directory)
if directory and not os.path.exists(full_directory):
os.makedirs(full_directory)
return directory, filename
class LumaAIClient:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_key": (
"STRING",
{
"default": "",
},
)
},
}
RETURN_TYPES = ("LUMACLIENT",)
RETURN_NAMES = ("client",)
FUNCTION = "run"
CATEGORY = "LumaAI"
def run(self, api_key):
"""
Create a LumaAI client with the provided API key.
"""
api_key = api_key if api_key != "" else os.environ.get("LUMAAI_API_KEY", "")
if api_key == "":
raise ValueError("API Key is required")
client = LumaAI(
auth_token=api_key,
)
return (client,)
class Text2Video:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"client": ("LUMACLIENT", {"forceInput": True}),
"prompt": ("STRING", {"multiline": True, "default": ""}),
"loop": ("BOOLEAN", {"default": False}),
"aspect_ratio": (["9:16", "3:4", "1:1", "4:3", "16:9", "21:9"],),
"save": ("BOOLEAN", {"default": True}),
},
"optional": {"filename": ("STRING", {"default": ""})},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("video_url", "generation_id")
OUTPUT_NODE = True
FUNCTION = "run"
CATEGORY = "LumaAI"
def run(self, client, prompt, loop, aspect_ratio, save, filename):
"""
Generate a video from a text prompt.
"""
if prompt == "":
raise ValueError("Prompt is required")
generation = client.generations.create(
prompt=prompt, loop=loop, aspect_ratio=aspect_ratio
)
generation_id = generation.id
completed = False
while not completed:
generation = client.generations.get(id=generation_id)
if generation.state == "completed":
completed = True
elif generation.state == "failed":
raise ValueError(f"Generation failed: {generation.failure_reason}")
time.sleep(3)
video_url = generation.assets.video
if save:
directory, filename = parse_filename(filename)
if filename == "":
filename = generation_id
download_file(video_url, os.path.join(self.output_dir, directory, filename + ".mp4"))
return {
"ui": {"text": [generation_id]},
"result": (
video_url,
generation_id,
),
}
class Image2Video:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"client": ("LUMACLIENT", {"forceInput": True}),
"prompt": ("STRING", {"multiline": True, "default": ""}),
"loop": ("BOOLEAN", {"default": False}),
"save": ("BOOLEAN", {"default": True}),
},
"optional": {
"init_image_url": ("STRING", {"default": "", "forceInput": True}),
"final_image_url": ("STRING", {"default": "", "forceInput": True}),
"filename": ("STRING", {"default": ""}),
},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("video_url", "generation_id")
FUNCTION = "run"
CATEGORY = "LumaAI"
def run(
self,
client,
prompt,
loop,
save,
init_image_url="",
final_image_url="",
filename="",
):
"""
Generate a video from an image prompt.
"""
if init_image_url == "" and final_image_url == "":
raise ValueError("At least one image URL is required")
keyframes = {}
if init_image_url != "":
keyframes["frame0"] = {"type": "image", "url": init_image_url}
if final_image_url != "":
keyframes["frame1"] = {"type": "image", "url": final_image_url}
generation = client.generations.create(
prompt=prompt, loop=loop, keyframes=keyframes
)
generation_id = generation.id
completed = False
while not completed:
generation = client.generations.get(id=generation_id)
if generation.state == "completed":
completed = True
elif generation.state == "failed":
raise ValueError(f"Generation failed: {generation.failure_reason}")
time.sleep(3)
video_url = generation.assets.video
if save:
directory, filename = parse_filename(filename)
if filename == "":
filename = generation_id
download_file(video_url, os.path.join(self.output_dir, directory, filename + ".mp4"))
return {
"ui": {"text": [generation_id]},
"result": (
video_url,
generation_id,
),
}
class InterpolateGenerations:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"client": ("LUMACLIENT", {"forceInput": True}),
"prompt": ("STRING", {"multiline": True, "default": ""}),
"save": ("BOOLEAN", {"default": True}),
"generation_id_1": ("STRING", {"default": "", "forceInput": True}),
"generation_id_2": ("STRING", {"default": "", "forceInput": True}),
},
"optional": {"filename": ("STRING", {"default": ""})},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("video_url", "generation_id")
FUNCTION = "run"
CATEGORY = "LumaAI"
def run(
self,
client,
prompt,
loop,
aspect_ratio,
save,
generation_id_1,
generation_id_2,
filename="",
):
"""
Generate a video by interpolating between two existing generations.
"""
if not generation_id_1 or not generation_id_2:
raise ValueError("Both generation IDs are required")
generation = client.generations.create(
prompt=prompt,
loop=loop,
aspect_ratio=aspect_ratio,
keyframes={
"frame0": {"type": "generation", "id": generation_id_1},
"frame1": {"type": "generation", "id": generation_id_2},
},
)
generation_id = generation.id
completed = False
while not completed:
generation = client.generations.get(id=generation_id)
if generation.state == "completed":
completed = True
elif generation.state == "failed":
raise ValueError(f"Generation failed: {generation.failure_reason}")
time.sleep(3)
video_url = generation.assets.video
if save:
directory, filename = parse_filename(filename)
if filename == "":
filename = generation_id
download_file(video_url, os.path.join(self.output_dir, directory, filename + ".mp4"))
return {
"ui": {"text": [generation_id]},
"result": (
video_url,
generation_id,
),
}
class ExtendGeneration:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"client": ("LUMACLIENT", {"forceInput": True}),
"prompt": ("STRING", {"multiline": True, "default": ""}),
"save": ("BOOLEAN", {"default": True}),
},
"optional": {
"init_image_url": ("STRING", {"default": "", "forceInput": True}),
"final_image_url": ("STRING", {"default": "", "forceInput": True}),
"init_generation_id": ("STRING", {"default": "", "forceInput": True}),
"final_generation_id": ("STRING", {"default": "", "forceInput": True}),
"filename": ("STRING", {"default": ""}),
},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("video_url", "generation_id")
FUNCTION = "run"
CATEGORY = "LumaAI"
def run(
self,
client,
prompt,
save,
init_image_url="",
final_image_url="",
init_generation_id="",
final_generation_id="",
filename="",
):
"""
Generate a video by extending from an image to an existing generation.
"""
if not init_generation_id and not final_generation_id:
raise ValueError("You must provide at least one generation id")
if init_image_url and init_generation_id:
raise ValueError(
"You cannot provide both an init image and a init generation"
)
if final_image_url and final_generation_id:
raise ValueError(
"You cannot provide both a final image and a final generation"
)
keyframes = {}
if init_image_url != "":
keyframes["frame0"] = {"type": "image", "url": init_image_url}
if final_image_url != "":
keyframes["frame1"] = {"type": "image", "url": final_image_url}
if init_generation_id != "":
keyframes["frame0"] = {"type": "generation", "id": init_generation_id}
if final_generation_id != "":
keyframes["frame1"] = {"type": "generation", "id": final_generation_id}
generation = client.generations.create(prompt=prompt, keyframes=keyframes)
new_generation_id = generation.id
completed = False
while not completed:
generation = client.generations.get(id=new_generation_id)
if generation.state == "completed":
completed = True
elif generation.state == "failed":
raise ValueError(f"Generation failed: {generation.failure_reason}")
time.sleep(3)
video_url = generation.assets.video
if save:
directory, filename = parse_filename(filename)
if filename == "":
filename = new_generation_id
download_file(video_url, os.path.join(self.output_dir, directory, filename + ".mp4"))
return {
"ui": {"text": [new_generation_id]},
"result": (
video_url,
new_generation_id,
),
}
class PreviewVideo:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"video_url": ("STRING", {"forceInput": True}),
}
}
OUTPUT_NODE = True
FUNCTION = "run"
CATEGORY = "LumaAI"
RETURN_TYPES = ()
def run(self, video_url):
return {"ui": {"video_url": [video_url]}}
+29
View File
@@ -0,0 +1,29 @@
from .lumaai_api_node import (
LumaAIClient,
Text2Video,
Image2Video,
InterpolateGenerations,
ExtendGeneration,
PreviewVideo,
)
from .imgbb_node import ImgBBUpload
NODE_CLASS_MAPPINGS = {
"LumaAIClient": LumaAIClient,
"ImgBBUpload": ImgBBUpload,
"LumaText2Video": Text2Video,
"LumaImage2Video": Image2Video,
"LumaInterpolateGenerations": InterpolateGenerations,
"LumaExtendGeneration": ExtendGeneration,
"LumaPreviewVideo": PreviewVideo,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LumaAIClient": "LumaAI Client",
"ImgBBUpload": "ImgBB Upload",
"LumaText2Video": "Text to Video",
"LumaImage2Video": "Image to Video",
"LumaInterpolateGenerations": "Interpolate Generations",
"LumaExtendGeneration": "Extend Generation",
"LumaPreviewVideo": "LumaAI Preview Video",
}
+15
View File
@@ -0,0 +1,15 @@
[project]
name = "comfyui-lumaai-api"
description = "This custom node allows you to use the LumaAI API."
version = "1.0.1"
license = {file = "LICENSE"}
dependencies = ["lumaai", "requests", "numpy", "Pillow"]
[project.urls]
Repository = "https://github.com/lumalabs/ComfyUI-LumaAI-API"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "lumaai"
DisplayName = "ComfyUI-LumaAI-API"
Icon = "https://cdn-luma.com/public/lumalabs.ai/images/logo.png"
+4
View File
@@ -0,0 +1,4 @@
lumaai
requests
numpy
Pillow
+154
View File
@@ -0,0 +1,154 @@
// Based on https://github.com/ArtVentureX/comfyui-animatediff/blob/main/web/js/vid_preview.js
import { app, ANIM_PREVIEW_WIDGET } from '../../../scripts/app.js';
import { createImageHost } from "../../../scripts/ui/imagePreview.js"
const BASE_SIZE = 768;
function setVideoDimensions(videoElement, width, height) {
videoElement.style.width = `${width}px`;
videoElement.style.height = `${height}px`;
}
// Resize video maintaining aspect ratio
export function resizeVideoAspectRatio(videoElement, maxWidth, maxHeight) {
const aspectRatio = videoElement.videoWidth / videoElement.videoHeight;
let newWidth, newHeight;
// Check which dimension is the limiting factor
if (videoElement.videoWidth / maxWidth > videoElement.videoHeight / maxHeight) {
// Width is the limiting factor
newWidth = maxWidth;
newHeight = newWidth / aspectRatio;
} else {
// Height is the limiting factor
newHeight = maxHeight;
newWidth = newHeight * aspectRatio;
}
setVideoDimensions(videoElement, newWidth, newHeight);
}
export function chainCallback(object, property, callback) {
if (object == undefined) {
//This should not happen.
console.error("Tried to add callback to non-existant object");
return;
}
if (property in object) {
const callback_orig = object[property];
object[property] = function () {
const r = callback_orig.apply(this, arguments);
callback.apply(this, arguments);
return r;
};
} else {
object[property] = callback;
}
};
export function addVideoPreview(nodeType, options = {}) {
const createVideoNode = (url) => {
return new Promise((cb) => {
const videoEl = document.createElement('video');
videoEl.addEventListener('loadedmetadata', () => {
videoEl.controls = false;
videoEl.loop = true;
videoEl.muted = true;
resizeVideoAspectRatio(videoEl, BASE_SIZE, BASE_SIZE);
cb(videoEl);
});
videoEl.addEventListener('error', () => {
cb();
});
videoEl.src = url;
});
};
nodeType.prototype.onDrawBackground = function (ctx) {
if (this.flags.collapsed) return;
let imageURLs = this.images ?? [];
let imagesChanged = false;
if (JSON.stringify(this.displayingImages) !== JSON.stringify(imageURLs)) {
this.displayingImages = imageURLs;
imagesChanged = true;
}
if (!imagesChanged) {
return;
}
if (!imageURLs.length) {
this.imgs = null;
this.animatedImages = false;
return;
}
const promises = imageURLs.map((url) => {
return createVideoNode(url);
});
Promise.all(promises)
.then((imgs) => {
this.imgs = imgs.filter(Boolean);
})
.then(() => {
if (!this.imgs.length) return;
this.animatedImages = true;
const widgetIdx = this.widgets?.findIndex((w) => w.name === ANIM_PREVIEW_WIDGET);
// Set node size to 1024x1024
this.size[0] = BASE_SIZE;
this.size[1] = BASE_SIZE;
if (widgetIdx > -1) {
// Replace content
const widget = this.widgets[widgetIdx];
widget.options.host.updateImages(this.imgs);
} else {
const host = createImageHost(this);
const widget = this.addDOMWidget(ANIM_PREVIEW_WIDGET, 'img', host.el, {
host,
getHeight: host.getHeight,
onDraw: host.onDraw,
hideOnZoom: false,
});
widget.serializeValue = () => ({
height: BASE_SIZE,
});
widget.options.host.updateImages(this.imgs);
}
this.imgs.forEach((img) => {
if (img instanceof HTMLVideoElement) {
img.muted = true;
img.autoplay = true;
img.play();
}
});
// Force canvas update
this.setDirtyCanvas(true, true);
});
};
chainCallback(nodeType.prototype, "onExecuted", function (message) {
if (message?.video_url) {
this.images = message?.video_url;
this.setDirtyCanvas(true);
}
});
}
app.registerExtension({
name: "VideoPreview",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "LumaPreviewVideo") {
return;
}
addVideoPreview(nodeType);
},
});
+52
View File
@@ -0,0 +1,52 @@
//Based on https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/web/js/showText.js
import { app } from "../../../scripts/app.js";
import { ComfyWidgets } from "../../../scripts/widgets.js";
// Displays input text on a node
app.registerExtension({
name: "lumaai.showgenerationid",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name == "LumaText2Video" || nodeData.name == "LumaImage2Video" || nodeData.name == "LumaInterpolateGenerations" || nodeData.name == "LumaExtendGeneration") {
function populate(text) {
const v = [...text];
if (this.widgets) {
if (this.widgets[this.widgets.length - 1].name == "filename") {
const w = ComfyWidgets["STRING"](this, "gen_output", ["STRING", { multiline: true }], app).widget;
w.inputEl.readOnly = true;
w.inputEl.style.opacity = 0.6;
w.value = v[0];
this.widgets.push(w);
} else {
this.widgets[this.widgets.length - 1].value = v[0];
}
}
requestAnimationFrame(() => {
const sz = this.computeSize();
if (sz[0] < this.size[0]) {
sz[0] = this.size[0];
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1];
}
this.onResize?.(sz);
app.graph.setDirtyCanvas(true, false);
});
}
// When the node is executed we will be sent the input text, display this in the widget
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
populate.call(this, message.text);
};
const onExecutionStart = nodeType.prototype.onExecutionStart;
nodeType.prototype.onExecutionStart = function () {
onExecutionStart?.apply(this, arguments);
populate.call(this, ["generating..."]);
};
}
},
});
+108
View File
@@ -0,0 +1,108 @@
{
"22": {
"inputs": {
"api_key": "",
"expire": false,
"expiration_time": 60,
"image": [
"23",
0
]
},
"class_type": "ImgBBUpload",
"_meta": {
"title": "ImgBB Upload"
}
},
"23": {
"inputs": {
"image": "IMG_9655.JPG",
"upload": "image"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"25": {
"inputs": {
"api_key": ""
},
"class_type": "LumaAIClient",
"_meta": {
"title": "LumaAI Client"
}
},
"26": {
"inputs": {
"video_url": [
"28",
0
],
"$$comfy_animation_preview": {
"height": 768
}
},
"class_type": "LumaPreviewVideo",
"_meta": {
"title": "LumaAI Preview Video"
}
},
"28": {
"inputs": {
"prompt": "",
"loop": false,
"aspect_ratio": "9:16",
"save": true,
"init_image_url": [
"22",
0
],
"filename": "",
"gen_output": "generating...",
"client": [
"25",
0
]
},
"class_type": "LumaImage2Video",
"_meta": {
"title": "Image to Video"
}
},
"29": {
"inputs": {
"prompt": " ",
"save": true,
"init_generation_id": [
"28",
1
],
"filename": "",
"gen_output": "generating...",
"client": [
"25",
0
]
},
"class_type": "LumaExtendGeneration",
"_meta": {
"title": "Extend Generation"
}
},
"31": {
"inputs": {
"video_url": [
"29",
0
],
"$$comfy_animation_preview": {
"height": 768
}
},
"class_type": "LumaPreviewVideo",
"_meta": {
"title": "LumaAI Preview Video"
}
}
}
+263
View File
@@ -0,0 +1,263 @@
{
"last_node_id": 28,
"last_link_id": 17,
"nodes": [
{
"id": 23,
"type": "LoadImage",
"pos": [
18,
590
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
10
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"IMG_9655.JPG",
"image"
]
},
{
"id": 25,
"type": "LumaAIClient",
"pos": [
30,
344
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "client",
"type": "LUMACLIENT",
"links": [
16
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "LumaAIClient"
},
"widgets_values": [
""
]
},
{
"id": 28,
"type": "LumaImage2Video",
"pos": [
727,
344
],
"size": [
382.09642410650736,
212
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "client",
"type": "LUMACLIENT",
"link": 16
},
{
"name": "init_image_url",
"type": "STRING",
"link": 15,
"widget": {
"name": "init_image_url"
}
},
{
"name": "final_image_url",
"type": "STRING",
"link": null,
"widget": {
"name": "final_image_url"
}
}
],
"outputs": [
{
"name": "video_url",
"type": "STRING",
"links": [
17
],
"shape": 3,
"slot_index": 0
},
{
"name": "generation_id",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LumaImage2Video"
},
"widgets_values": [
"",
false,
"9:16",
true,
"",
"",
""
]
},
{
"id": 26,
"type": "LumaPreviewVideo",
"pos": [
1228,
342
],
"size": [
312.26042261370117,
264.63415629022575
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "video_url",
"type": "STRING",
"link": 17,
"widget": {
"name": "video_url"
},
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "LumaPreviewVideo"
},
"widgets_values": [
""
]
},
{
"id": 22,
"type": "ImgBBUpload",
"pos": [
363,
589
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 10
}
],
"outputs": [
{
"name": "image_url",
"type": "STRING",
"links": [
15
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ImgBBUpload"
},
"widgets_values": [
"",
false,
60
]
}
],
"links": [
[
10,
23,
0,
22,
0,
"IMAGE"
],
[
15,
22,
0,
28,
1,
"STRING"
],
[
16,
25,
0,
28,
0,
"LUMACLIENT"
],
[
17,
28,
0,
26,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.6115909044841499,
"offset": [
493.78857374805784,
-7.162115379659536
]
}
},
"version": 0.4
}
+149
View File
@@ -0,0 +1,149 @@
{
"last_node_id": 30,
"last_link_id": 16,
"nodes": [
{
"id": 30,
"type": "LumaPreviewVideo",
"pos": [
1033,
378
],
"size": {
"0": 768,
"1": 768
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "video_url",
"type": "STRING",
"link": 16,
"widget": {
"name": "video_url"
}
}
],
"properties": {
"Node name for S&R": "LumaPreviewVideo"
},
"widgets_values": [
"",
null
]
},
{
"id": 28,
"type": "LumaText2Video",
"pos": [
526,
378
],
"size": [
400.22849849128625,
198.76139540935253
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "client",
"type": "LUMACLIENT",
"link": 15
}
],
"outputs": [
{
"name": "video_url",
"type": "STRING",
"links": [
16
],
"shape": 3,
"slot_index": 0
},
{
"name": "generation_id",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LumaText2Video"
},
"widgets_values": [
"Extreme closeup footage of a young sailor woman with a concerned expression during a rainstorm.",
true,
"9:16",
false,
""
]
},
{
"id": 29,
"type": "LumaAIClient",
"pos": [
124,
375
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "client",
"type": "LUMACLIENT",
"links": [
15
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "LumaAIClient"
},
"widgets_values": [
""
]
}
],
"links": [
[
15,
29,
0,
28,
0,
"LUMACLIENT"
],
[
16,
28,
0,
30,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.8954302432552445,
"offset": [
-56.377797259191,
-151.59915874422396
]
}
},
"version": 0.4
}