Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90d96366c8 | ||
|
|
605c8db320 | ||
|
|
cf965727e8 | ||
|
|
12b134ab4c | ||
|
|
dd27f990c7 | ||
|
|
16c1a59312 | ||
|
|
59a361af58 | ||
|
|
e4da832b99 | ||
|
|
14ee9e23c0 | ||
|
|
53cb503866 | ||
|
|
d5c4c5f264 |
@@ -42,6 +42,7 @@
|
|||||||
"Mask To Image (mtb)": "Converts a mask (alpha) to an RGB image with a color and background",
|
"Mask To Image (mtb)": "Converts a mask (alpha) to an RGB image with a color and background",
|
||||||
"Math Expression (mtb)": "Node to evaluate a simple math expression string",
|
"Math Expression (mtb)": "Node to evaluate a simple math expression string",
|
||||||
"Model Patch Seamless (mtb)": "Uses the stable diffusion 'hack' to infer seamless images by setting the model layers padding mode to circular (experimental)",
|
"Model Patch Seamless (mtb)": "Uses the stable diffusion 'hack' to infer seamless images by setting the model layers padding mode to circular (experimental)",
|
||||||
|
"Pick From Batch (mtb)": "Pick a specific number of images from a batch, either from the start or end.",
|
||||||
"Qr Code (mtb)": "Basic QR Code generator",
|
"Qr Code (mtb)": "Basic QR Code generator",
|
||||||
"Restore Face (mtb)": "Uses GFPGan to restore faces",
|
"Restore Face (mtb)": "Uses GFPGan to restore faces",
|
||||||
"Save Gif (mtb)": "Save the images from the batch as a GIF",
|
"Save Gif (mtb)": "Save the images from the batch as a GIF",
|
||||||
|
|||||||
+13
-6
@@ -1,5 +1,4 @@
|
|||||||
import csv
|
import csv, shutil
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import folder_paths
|
import folder_paths
|
||||||
@@ -157,13 +156,21 @@ class StylesLoader:
|
|||||||
for file in files:
|
for file in files:
|
||||||
with open(file, "r", encoding="utf8") as f:
|
with open(file, "r", encoding="utf8") as f:
|
||||||
parsed = csv.reader(f)
|
parsed = csv.reader(f)
|
||||||
for row in parsed:
|
for i, row in enumerate(parsed):
|
||||||
log.debug(f"Adding style {row[0]}")
|
log.debug(f"Adding style {row[0]}")
|
||||||
try:
|
try:
|
||||||
cls.options[row[0]] = (row[1], row[2])
|
name, positive, negative = (row + [None] * 3)[:3]
|
||||||
except Exception:
|
positive = positive or ""
|
||||||
|
negative = negative or ""
|
||||||
|
if name is not None:
|
||||||
|
cls.options[name] = (positive, negative)
|
||||||
|
else:
|
||||||
|
# Handle the case where 'name' is None
|
||||||
|
log.warning(f"Missing 'name' in row {i}.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
log.warning(
|
log.warning(
|
||||||
f"There was an error while parsing {file}, make sure it respects A1111 format, i.e 3 columns name, positive, negative"
|
f"There was an error while parsing {file}, make sure it respects A1111 format, i.e 3 columns name, positive, negative:\n{e}"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
+11
-7
@@ -44,6 +44,7 @@ class BboxFromMask:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"mask": ("MASK",),
|
"mask": ("MASK",),
|
||||||
|
"invert": ("BOOLEAN", {"default": False}),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"image": ("IMAGE",),
|
"image": ("IMAGE",),
|
||||||
@@ -61,7 +62,7 @@ class BboxFromMask:
|
|||||||
FUNCTION = "extract_bounding_box"
|
FUNCTION = "extract_bounding_box"
|
||||||
CATEGORY = "mtb/crop"
|
CATEGORY = "mtb/crop"
|
||||||
|
|
||||||
def extract_bounding_box(self, mask: torch.Tensor, image=None):
|
def extract_bounding_box(self, mask: torch.Tensor, invert: bool, image=None):
|
||||||
# if image != None:
|
# if image != None:
|
||||||
# if mask.size(0) != image.size(0):
|
# if mask.size(0) != image.size(0):
|
||||||
# if mask.size(0) != 1:
|
# if mask.size(0) != 1:
|
||||||
@@ -73,9 +74,8 @@ class BboxFromMask:
|
|||||||
# f"Batch count mismatch for mask and image, it can either be 1 mask for X images, or X masks for X images (mask: {mask.shape} | image: {image.shape})"
|
# f"Batch count mismatch for mask and image, it can either be 1 mask for X images, or X masks for X images (mask: {mask.shape} | image: {image.shape})"
|
||||||
# )
|
# )
|
||||||
|
|
||||||
_mask = tensor2pil(1.0 - mask)[0]
|
|
||||||
|
|
||||||
# we invert it
|
# we invert it
|
||||||
|
_mask = tensor2pil(1.0 - mask)[0] if invert else tensor2pil(mask)[0]
|
||||||
alpha_channel = np.array(_mask)
|
alpha_channel = np.array(_mask)
|
||||||
|
|
||||||
non_zero_indices = np.nonzero(alpha_channel)
|
non_zero_indices = np.nonzero(alpha_channel)
|
||||||
@@ -141,19 +141,23 @@ class Crop:
|
|||||||
self, image: torch.Tensor, mask=None, x=0, y=0, width=256, height=256, bbox=None
|
self, image: torch.Tensor, mask=None, x=0, y=0, width=256, height=256, bbox=None
|
||||||
):
|
):
|
||||||
image = image.numpy()
|
image = image.numpy()
|
||||||
if mask:
|
if mask is not None:
|
||||||
mask = mask.numpy()
|
mask = mask.numpy()
|
||||||
|
|
||||||
if bbox != None:
|
if bbox is not None:
|
||||||
x, y, width, height = bbox
|
x, y, width, height = bbox
|
||||||
|
|
||||||
cropped_image = image[:, y : y + height, x : x + width, :]
|
cropped_image = image[:, y : y + height, x : x + width, :]
|
||||||
cropped_mask = mask[y : y + height, x : x + width] if mask != None else None
|
cropped_mask = None
|
||||||
|
if mask is not None:
|
||||||
|
cropped_mask = (
|
||||||
|
mask[:, y : y + height, x : x + width] if mask is not None else None
|
||||||
|
)
|
||||||
crop_data = (x, y, width, height)
|
crop_data = (x, y, width, height)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
torch.from_numpy(cropped_image),
|
torch.from_numpy(cropped_image),
|
||||||
torch.from_numpy(cropped_mask) if mask != None else None,
|
torch.from_numpy(cropped_mask) if cropped_mask is not None else None,
|
||||||
crop_data,
|
crop_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+61
-23
@@ -1,9 +1,11 @@
|
|||||||
import qrcode
|
import threading
|
||||||
from ..utils import pil2tensor
|
|
||||||
from ..utils import comfy_dir
|
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
|
import qrcode
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from ..log import log
|
from ..log import log
|
||||||
|
from ..utils import comfy_dir, pil2tensor
|
||||||
|
|
||||||
# class MtbExamples:
|
# class MtbExamples:
|
||||||
# """MTB Example Images"""
|
# """MTB Example Images"""
|
||||||
@@ -74,9 +76,10 @@ class UnsplashImage:
|
|||||||
CATEGORY = "mtb/generate"
|
CATEGORY = "mtb/generate"
|
||||||
|
|
||||||
def do_unsplash_image(self, width, height, random_seed, keyword=None):
|
def do_unsplash_image(self, width, height, random_seed, keyword=None):
|
||||||
import requests
|
|
||||||
import io
|
import io
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
base_url = "https://source.unsplash.com/random/"
|
base_url = "https://source.unsplash.com/random/"
|
||||||
|
|
||||||
if width and height:
|
if width and height:
|
||||||
@@ -201,12 +204,13 @@ class TextToImage:
|
|||||||
|
|
||||||
for font in fonts:
|
for font in fonts:
|
||||||
log.debug(f"Adding font {font}")
|
log.debug(f"Adding font {font}")
|
||||||
cls.fonts[font.stem] = font.as_posix()
|
TextToImage.fonts[font.stem] = font.as_posix()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
if not cls.fonts:
|
if not cls.fonts:
|
||||||
cls.CACHE_FONTS()
|
thread = threading.Thread(target=cls.CACHE_FONTS)
|
||||||
|
thread.start()
|
||||||
else:
|
else:
|
||||||
log.debug(f"Using cached fonts (count: {len(cls.fonts)})")
|
log.debug(f"Using cached fonts (count: {len(cls.fonts)})")
|
||||||
return {
|
return {
|
||||||
@@ -232,7 +236,6 @@ class TextToImage:
|
|||||||
"INT",
|
"INT",
|
||||||
{"default": 512, "min": 1, "max": 8096, "step": 1},
|
{"default": 512, "min": 1, "max": 8096, "step": 1},
|
||||||
),
|
),
|
||||||
# "position": (["INT"], {"default": 0, "min": 0, "max": 100, "step": 1}),
|
|
||||||
"color": (
|
"color": (
|
||||||
"COLOR",
|
"COLOR",
|
||||||
{"default": "black"},
|
{"default": "black"},
|
||||||
@@ -241,6 +244,8 @@ class TextToImage:
|
|||||||
"COLOR",
|
"COLOR",
|
||||||
{"default": "white"},
|
{"default": "white"},
|
||||||
),
|
),
|
||||||
|
"h_align": (("left", "center", "right"), {"default": "left"}),
|
||||||
|
"v_align": (("top", "center", "bottom"), {"default": "top"}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,29 +255,62 @@ class TextToImage:
|
|||||||
CATEGORY = "mtb/generate"
|
CATEGORY = "mtb/generate"
|
||||||
|
|
||||||
def text_to_image(
|
def text_to_image(
|
||||||
self, text, font, wrap, font_size, width, height, color, background
|
self,
|
||||||
|
text,
|
||||||
|
font,
|
||||||
|
wrap,
|
||||||
|
font_size,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
color,
|
||||||
|
background,
|
||||||
|
h_align="left",
|
||||||
|
v_align="top",
|
||||||
):
|
):
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
font = self.fonts[font]
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
font = cast(ImageFont.FreeTypeFont, ImageFont.truetype(font, font_size))
|
|
||||||
if wrap == 0:
|
|
||||||
wrap = width / font_size
|
|
||||||
lines = textwrap.wrap(text, width=wrap)
|
|
||||||
log.debug(f"Lines: {lines}")
|
|
||||||
line_height = bbox_dim(font.getbbox("hg"))[1]
|
|
||||||
img_height = height # line_height * len(lines)
|
|
||||||
img_width = width # max(font.getsize(line)[0] for line in lines)
|
|
||||||
|
|
||||||
img = Image.new("RGBA", (img_width, img_height), background)
|
font_path = self.fonts[font]
|
||||||
|
|
||||||
|
# Handle word wrapping
|
||||||
|
if wrap:
|
||||||
|
lines = textwrap.wrap(text, width=wrap)
|
||||||
|
else:
|
||||||
|
lines = [text]
|
||||||
|
font = ImageFont.truetype(font_path, font_size)
|
||||||
|
# font = ImageFont.truetype(font_path, font_size)
|
||||||
|
# if wrap == 0:
|
||||||
|
# wrap = width / font_size
|
||||||
|
|
||||||
|
log.debug(f"Lines: {lines}")
|
||||||
|
img = Image.new("RGBA", (width, height), background)
|
||||||
draw = ImageDraw.Draw(img)
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
text_height = sum(font.getsize(line)[1] for line in lines)
|
||||||
|
|
||||||
|
# Vertical alignment
|
||||||
|
if v_align == "top":
|
||||||
y_text = 0
|
y_text = 0
|
||||||
# - bbox is [left, upper, right, lower]
|
elif v_align == "center":
|
||||||
|
y_text = (height - text_height) // 2
|
||||||
|
else: # bottom
|
||||||
|
y_text = height - text_height
|
||||||
|
|
||||||
|
# Draw each line of text
|
||||||
for line in lines:
|
for line in lines:
|
||||||
width, height = bbox_dim(font.getbbox(line))
|
line_width, line_height = font.getsize(line)
|
||||||
draw.text((0, y_text), line, color, font=font)
|
|
||||||
y_text += height
|
# Horizontal alignment
|
||||||
|
if h_align == "left":
|
||||||
|
x_text = 0
|
||||||
|
elif h_align == "center":
|
||||||
|
x_text = (width - line_width) // 2
|
||||||
|
else: # right
|
||||||
|
x_text = width - line_width
|
||||||
|
|
||||||
|
draw.text((x_text, y_text), line, color, font=font)
|
||||||
|
y_text += line_height
|
||||||
|
|
||||||
# img.save(os.path.join(folder_paths.base_path, f'{str(uuid.uuid4())}.png'))
|
# img.save(os.path.join(folder_paths.base_path, f'{str(uuid.uuid4())}.png'))
|
||||||
return (pil2tensor(img),)
|
return (pil2tensor(img),)
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import io
|
import io, json, urllib.parse, urllib.request
|
||||||
import json
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
|
|||||||
+36
-1
@@ -38,4 +38,39 @@ class StackImages:
|
|||||||
return (stacked_tensor,)
|
return (stacked_tensor,)
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [StackImages]
|
class PickFromBatch:
|
||||||
|
"""Pick a specific number of images from a batch, either from the start or end."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"image": ("IMAGE",),
|
||||||
|
"from_direction": (["end", "start"], {"default": "start"}),
|
||||||
|
"count": ("INT", {"default": 1}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
FUNCTION = "pick_from_batch"
|
||||||
|
CATEGORY = "mtb/image utils"
|
||||||
|
|
||||||
|
def pick_from_batch(self, image, from_direction, count):
|
||||||
|
batch_size = image.size(0)
|
||||||
|
|
||||||
|
# Limit count to the available number of images in the batch
|
||||||
|
count = min(count, batch_size)
|
||||||
|
if count < batch_size:
|
||||||
|
log.warning(
|
||||||
|
f"Requested {count} images, but only {batch_size} are available."
|
||||||
|
)
|
||||||
|
|
||||||
|
if from_direction == "end":
|
||||||
|
selected_tensors = image[-count:]
|
||||||
|
else:
|
||||||
|
selected_tensors = image[:count]
|
||||||
|
|
||||||
|
return (selected_tensors,)
|
||||||
|
|
||||||
|
|
||||||
|
__nodes__ = [StackImages, PickFromBatch]
|
||||||
|
|||||||
+153
-16
@@ -1,14 +1,107 @@
|
|||||||
from ..utils import tensor2np, PIL_FILTER_MAP
|
import json, subprocess, uuid
|
||||||
import uuid
|
|
||||||
import folder_paths
|
|
||||||
from ..log import log
|
|
||||||
import comfy.model_management as model_management
|
|
||||||
import subprocess
|
|
||||||
import torch
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import comfy.model_management as model_management
|
||||||
|
import folder_paths
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import torch
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from typing import Optional, List
|
|
||||||
|
from ..log import log
|
||||||
|
from ..utils import PIL_FILTER_MAP, output_dir, session_id, tensor2np
|
||||||
|
|
||||||
|
|
||||||
|
def get_playlist_path(playlist_name: str, persistant_playlist=False):
|
||||||
|
if persistant_playlist:
|
||||||
|
return output_dir / "playlists" / f"{playlist_name}.json"
|
||||||
|
|
||||||
|
return output_dir / "playlists" / session_id / f"{playlist_name}.json"
|
||||||
|
|
||||||
|
|
||||||
|
class ReadPlaylist:
|
||||||
|
"""Read a playlist"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"enable": ("BOOLEAN", {"default": True}),
|
||||||
|
"persistant_playlist": ("BOOLEAN", {"default": False}),
|
||||||
|
"playlist_name": ("STRING", {"default": "playlist_{index:04d}"}),
|
||||||
|
"index": ("INT", {"default": 0, "min": 0}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("PLAYLIST",)
|
||||||
|
FUNCTION = "read_playlist"
|
||||||
|
CATEGORY = "mtb/IO"
|
||||||
|
|
||||||
|
def read_playlist(
|
||||||
|
self, enable: bool, persistant_playlist: bool, playlist_name: str, index: int
|
||||||
|
):
|
||||||
|
playlist_name = playlist_name.format(index=index)
|
||||||
|
playlist_path = get_playlist_path(playlist_name, persistant_playlist)
|
||||||
|
if not enable:
|
||||||
|
return (None,)
|
||||||
|
|
||||||
|
if not playlist_path.exists():
|
||||||
|
log.warning(f"Playlist {playlist_path} does not exist, skipping")
|
||||||
|
return (None,)
|
||||||
|
|
||||||
|
log.debug(f"Reading playlist {playlist_path}")
|
||||||
|
return (json.loads(playlist_path.read_text(encoding="utf-8")),)
|
||||||
|
|
||||||
|
|
||||||
|
class AddToPlaylist:
|
||||||
|
"""Add a video to the playlist"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"relative_paths": ("BOOLEAN", {"default": False}),
|
||||||
|
"persistant_playlist": ("BOOLEAN", {"default": False}),
|
||||||
|
"playlist_name": ("STRING", {"default": "playlist_{index:04d}"}),
|
||||||
|
"index": ("INT", {"default": 0, "min": 0}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ()
|
||||||
|
OUTPUT_NODE = True
|
||||||
|
FUNCTION = "add_to_playlist"
|
||||||
|
CATEGORY = "mtb/IO"
|
||||||
|
|
||||||
|
def add_to_playlist(
|
||||||
|
self,
|
||||||
|
relative_paths: bool,
|
||||||
|
persistant_playlist: bool,
|
||||||
|
playlist_name: str,
|
||||||
|
index: int,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
playlist_name = playlist_name.format(index=index)
|
||||||
|
playlist_path = get_playlist_path(playlist_name, persistant_playlist)
|
||||||
|
|
||||||
|
if not playlist_path.parent.exists():
|
||||||
|
playlist_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
playlist = []
|
||||||
|
if not playlist_path.exists():
|
||||||
|
playlist_path.write_text("[]")
|
||||||
|
else:
|
||||||
|
playlist = json.loads(playlist_path.read_text())
|
||||||
|
log.debug(f"Playlist {playlist_path} has {len(playlist)} items")
|
||||||
|
for video in kwargs.values():
|
||||||
|
if relative_paths:
|
||||||
|
video = Path(video).relative_to(output_dir).as_posix()
|
||||||
|
|
||||||
|
log.debug(f"Adding {video} to playlist")
|
||||||
|
playlist.append(video)
|
||||||
|
|
||||||
|
log.debug(f"Writing playlist {playlist_path}")
|
||||||
|
playlist_path.write_text(json.dumps(playlist), encoding="utf-8")
|
||||||
|
return ()
|
||||||
|
|
||||||
|
|
||||||
class ExportWithFfmpeg:
|
class ExportWithFfmpeg:
|
||||||
@@ -17,8 +110,11 @@ class ExportWithFfmpeg:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
"required": {
|
"optional": {
|
||||||
"images": ("IMAGE",),
|
"images": ("IMAGE",),
|
||||||
|
"playlist": ("PLAYLIST",),
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
# "frames": ("FRAMES",),
|
# "frames": ("FRAMES",),
|
||||||
"fps": ("FLOAT", {"default": 24, "min": 1}),
|
"fps": ("FLOAT", {"default": 24, "min": 1}),
|
||||||
"prefix": ("STRING", {"default": "export"}),
|
"prefix": ("STRING", {"default": "export"}),
|
||||||
@@ -27,7 +123,7 @@ class ExportWithFfmpeg:
|
|||||||
["prores_ks", "libx264", "libx265"],
|
["prores_ks", "libx264", "libx265"],
|
||||||
{"default": "prores_ks"},
|
{"default": "prores_ks"},
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("VIDEO",)
|
RETURN_TYPES = ("VIDEO",)
|
||||||
@@ -37,20 +133,61 @@ class ExportWithFfmpeg:
|
|||||||
|
|
||||||
def export_prores(
|
def export_prores(
|
||||||
self,
|
self,
|
||||||
images: torch.Tensor,
|
|
||||||
fps: float,
|
fps: float,
|
||||||
prefix: str,
|
prefix: str,
|
||||||
format: str,
|
format: str,
|
||||||
codec: str,
|
codec: str,
|
||||||
|
images: Optional[torch.Tensor] = None,
|
||||||
|
playlist: Optional[List[str]] = None,
|
||||||
):
|
):
|
||||||
if images.size(0) == 0:
|
|
||||||
return ("",)
|
|
||||||
output_dir = Path(folder_paths.get_output_directory())
|
|
||||||
pix_fmt = "rgb48le" if codec == "prores_ks" else "yuv420p"
|
pix_fmt = "rgb48le" if codec == "prores_ks" else "yuv420p"
|
||||||
file_ext = format
|
file_ext = format
|
||||||
file_id = f"{prefix}_{uuid.uuid4()}.{file_ext}"
|
file_id = f"{prefix}_{uuid.uuid4()}.{file_ext}"
|
||||||
|
|
||||||
log.debug(f"Exporting to {output_dir / file_id}")
|
if playlist is not None and images is not None:
|
||||||
|
log.info(f"Exporting to {output_dir / file_id}")
|
||||||
|
|
||||||
|
if playlist is not None:
|
||||||
|
if len(playlist) == 0:
|
||||||
|
log.debug("Playlist is empty, skipping")
|
||||||
|
return ("",)
|
||||||
|
|
||||||
|
temp_playlist_path = output_dir / f"temp_playlist_{uuid.uuid4()}.txt"
|
||||||
|
log.debug(
|
||||||
|
f"Create a temporary file to list the videos for concatenation to {temp_playlist_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(temp_playlist_path, "w") as f:
|
||||||
|
for video_path in playlist:
|
||||||
|
f.write(f"file '{video_path}'\n")
|
||||||
|
|
||||||
|
out_path = (output_dir / file_id).as_posix()
|
||||||
|
|
||||||
|
# Prepare the FFmpeg command for concatenating videos from the playlist
|
||||||
|
command = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-f",
|
||||||
|
"concat",
|
||||||
|
"-safe",
|
||||||
|
"0",
|
||||||
|
"-i",
|
||||||
|
temp_playlist_path.as_posix(),
|
||||||
|
"-c",
|
||||||
|
"copy",
|
||||||
|
"-y",
|
||||||
|
out_path,
|
||||||
|
]
|
||||||
|
log.debug(f"Executing {command}")
|
||||||
|
subprocess.run(command)
|
||||||
|
|
||||||
|
temp_playlist_path.unlink()
|
||||||
|
|
||||||
|
return (out_path,)
|
||||||
|
|
||||||
|
if (
|
||||||
|
images is None or images.size(0) == 0
|
||||||
|
): # the is None check is just for the type checker
|
||||||
|
return ("",)
|
||||||
|
|
||||||
frames = tensor2np(images)
|
frames = tensor2np(images)
|
||||||
log.debug(f"Frames type {type(frames[0])}")
|
log.debug(f"Frames type {type(frames[0])}")
|
||||||
@@ -192,4 +329,4 @@ class SaveGif:
|
|||||||
return {"ui": {"gif": results}}
|
return {"ui": {"gif": results}}
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [SaveGif, ExportWithFfmpeg]
|
__nodes__ = [SaveGif, ExportWithFfmpeg, AddToPlaylist, ReadPlaylist]
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
import contextlib
|
import contextlib, functools, math, os, shlex, shutil, socket, subprocess, sys, uuid
|
||||||
import functools
|
|
||||||
import math
|
|
||||||
import os
|
|
||||||
import shlex
|
|
||||||
import shutil
|
|
||||||
import socket
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
@@ -238,8 +229,9 @@ here = Path(__file__).parent.absolute()
|
|||||||
# - Construct the absolute path to the ComfyUI directory
|
# - Construct the absolute path to the ComfyUI directory
|
||||||
comfy_dir = Path(folder_paths.base_path)
|
comfy_dir = Path(folder_paths.base_path)
|
||||||
models_dir = Path(folder_paths.models_dir)
|
models_dir = Path(folder_paths.models_dir)
|
||||||
|
output_dir = Path(folder_paths.output_directory)
|
||||||
styles_dir = comfy_dir / "styles"
|
styles_dir = comfy_dir / "styles"
|
||||||
|
session_id = str(uuid.uuid4())
|
||||||
# - Construct the path to the font file
|
# - Construct the path to the font file
|
||||||
font_path = here / "font.ttf"
|
font_path = here / "font.ttf"
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ data otherwise:
|
|||||||

|

|
||||||
|
|
||||||
|
|
||||||
|
**note +**
|
||||||
|
A basic HTML note mainly to add better looking notes/instructions for workflow makers:
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
## Standalone
|
## Standalone
|
||||||
These scripts can be taken and placed independently of `comfy_mtb` or any other files, mimicking what pythongosss did for their
|
These scripts can be taken and placed independently of `comfy_mtb` or any other files, mimicking what pythongosss did for their
|
||||||
|
|||||||
@@ -121,6 +121,9 @@ export const dynamic_connection = (
|
|||||||
connectionType = 'PSDLAYER',
|
connectionType = 'PSDLAYER',
|
||||||
nameArray = []
|
nameArray = []
|
||||||
) => {
|
) => {
|
||||||
|
if (!node.inputs[index].name.startsWith(connectionPrefix)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
// remove all non connected inputs
|
// remove all non connected inputs
|
||||||
if (!connected && node.inputs.length > 1) {
|
if (!connected && node.inputs.length > 1) {
|
||||||
log(`Removing input ${index} (${node.inputs[index].name})`)
|
log(`Removing input ${index} (${node.inputs[index].name})`)
|
||||||
@@ -331,6 +334,43 @@ function getBrightness(rgbObj) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//- HTML / CSS UTILS
|
//- HTML / CSS UTILS
|
||||||
|
export const loadScript = (
|
||||||
|
FILE_URL,
|
||||||
|
async = true,
|
||||||
|
type = 'text/javascript'
|
||||||
|
) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Check if the script already exists
|
||||||
|
const existingScript = document.querySelector(`script[src="${FILE_URL}"]`)
|
||||||
|
if (existingScript) {
|
||||||
|
resolve({ status: true, message: 'Script already loaded' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scriptEle = document.createElement('script')
|
||||||
|
scriptEle.type = type
|
||||||
|
scriptEle.async = async
|
||||||
|
scriptEle.src = FILE_URL
|
||||||
|
|
||||||
|
scriptEle.addEventListener('load', (ev) => {
|
||||||
|
resolve({ status: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
scriptEle.addEventListener('error', (ev) => {
|
||||||
|
reject({
|
||||||
|
status: false,
|
||||||
|
message: `Failed to load the script ${FILE_URL}`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
document.body.appendChild(scriptEle)
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function defineClass(className, classStyles) {
|
export function defineClass(className, classStyles) {
|
||||||
const styleSheets = document.styleSheets
|
const styleSheets = document.styleSheets
|
||||||
|
|
||||||
|
|||||||
+11
-4
@@ -7,6 +7,8 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// TODO: Use the builtin addDOMWidget everywhere appropriate
|
||||||
|
|
||||||
import { app } from '../../scripts/app.js'
|
import { app } from '../../scripts/app.js'
|
||||||
import { api } from '../../scripts/api.js'
|
import { api } from '../../scripts/api.js'
|
||||||
|
|
||||||
@@ -494,10 +496,6 @@ const mtb_widgets = {
|
|||||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||||
// const rinputs = nodeData.input?.required
|
// const rinputs = nodeData.input?.required
|
||||||
|
|
||||||
if (!nodeData.name.endsWith('(mtb)')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let has_custom = false
|
let has_custom = false
|
||||||
if (nodeData.input && nodeData.input.required) {
|
if (nodeData.input && nodeData.input.required) {
|
||||||
for (const i of Object.keys(nodeData.input.required)) {
|
for (const i of Object.keys(nodeData.input.required)) {
|
||||||
@@ -565,6 +563,10 @@ const mtb_widgets = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!nodeData.name.endsWith('(mtb)')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
//- Extending Python Nodes
|
//- Extending Python Nodes
|
||||||
switch (nodeData.name) {
|
switch (nodeData.name) {
|
||||||
case 'Psd Save (mtb)': {
|
case 'Psd Save (mtb)': {
|
||||||
@@ -881,6 +883,10 @@ const mtb_widgets = {
|
|||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 'Add To Playlist (mtb)': {
|
||||||
|
shared.setupDynamicConnections(nodeType, 'video', 'VIDEO')
|
||||||
|
break
|
||||||
|
}
|
||||||
case 'Stack Images (mtb)':
|
case 'Stack Images (mtb)':
|
||||||
case 'Concat Images (mtb)': {
|
case 'Concat Images (mtb)': {
|
||||||
shared.setupDynamicConnections(nodeType, 'image', 'IMAGE')
|
shared.setupDynamicConnections(nodeType, 'image', 'IMAGE')
|
||||||
@@ -896,6 +902,7 @@ const mtb_widgets = {
|
|||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
// TODO: remove this, recommend pythongoss's version that is much better
|
||||||
case 'Math Expression (mtb)': {
|
case 'Math Expression (mtb)': {
|
||||||
const onNodeCreated = nodeType.prototype.onNodeCreated
|
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||||
nodeType.prototype.onNodeCreated = function () {
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { app } from '../../scripts/app.js'
|
||||||
|
import { api } from '../../scripts/api.js'
|
||||||
|
import * as shared from './comfy_shared.js'
|
||||||
|
|
||||||
|
class NotePlus extends LiteGraph.LGraphNode {
|
||||||
|
title = 'Note+ (mtb)'
|
||||||
|
category = 'mtb/utils'
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.isVirtualNode = true
|
||||||
|
this.serialize_widgets = true
|
||||||
|
|
||||||
|
this.editing = false
|
||||||
|
this.live = true
|
||||||
|
this.rawVal = "<p style='color:red;font-family:monospace'\n> Note+\n</p>"
|
||||||
|
|
||||||
|
this.calculated_height = 36
|
||||||
|
|
||||||
|
const inner = document.createElement('div')
|
||||||
|
inner.style.margin = '0'
|
||||||
|
inner.style.padding = '0'
|
||||||
|
this.html_widget = this.addDOMWidget('HTML', 'html', inner, {
|
||||||
|
setValue: (v) => {
|
||||||
|
// update our widget preview
|
||||||
|
this.html_widget.element.innerHTML = v
|
||||||
|
// calculate height
|
||||||
|
this.calculated_height = this.html_widget.element.scrollHeight + 36
|
||||||
|
},
|
||||||
|
getValue: () => this.rawVal,
|
||||||
|
getMinHeight: () => this.calculated_height, // (the edit button),
|
||||||
|
})
|
||||||
|
|
||||||
|
// console.log(`Value of HTML: ${this.html_widget.value}`)
|
||||||
|
this.html_widget.element.innerHTML = this.html_widget.value
|
||||||
|
|
||||||
|
//- ace based editor
|
||||||
|
this.addWidget('button', 'Edit', 'Edit', () => {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
Object.assign(container.style, {
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
})
|
||||||
|
|
||||||
|
dialog.show('')
|
||||||
|
dialog.textElement.append(container)
|
||||||
|
|
||||||
|
const value = document.createElement('div')
|
||||||
|
value.id = 'noteplus-editor'
|
||||||
|
Object.assign(value.style, {
|
||||||
|
width: '300px',
|
||||||
|
height: '200px',
|
||||||
|
backgroundColor: 'rgb(30,30,30)',
|
||||||
|
color: 'whitesmoke',
|
||||||
|
})
|
||||||
|
|
||||||
|
container.append(value)
|
||||||
|
|
||||||
|
const live_edit = document.createElement('input')
|
||||||
|
live_edit.type = 'checkbox'
|
||||||
|
live_edit.checked = this.live
|
||||||
|
live_edit.onchange = () => {
|
||||||
|
this.live = live_edit.checked
|
||||||
|
}
|
||||||
|
|
||||||
|
const live_edit_label = document.createElement('label')
|
||||||
|
live_edit_label.textContent = 'Live Edit'
|
||||||
|
live_edit_label.append(live_edit)
|
||||||
|
|
||||||
|
value.after(live_edit_label)
|
||||||
|
|
||||||
|
this.setupEditor()
|
||||||
|
this.editor.setValue(this.html_widget.element.innerHTML)
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialog = new app.ui.dialog.constructor()
|
||||||
|
dialog.element.classList.add('comfy-settings')
|
||||||
|
|
||||||
|
const closeButton = dialog.element.querySelector('button')
|
||||||
|
closeButton.textContent = 'CANCEL'
|
||||||
|
const saveButton = document.createElement('button')
|
||||||
|
saveButton.textContent = 'SAVE'
|
||||||
|
saveButton.onclick = () => {
|
||||||
|
this.updateHTML(this.editor.getValue())
|
||||||
|
|
||||||
|
this.editor.destroy()
|
||||||
|
this.editor.container.remove()
|
||||||
|
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
closeButton.before(saveButton)
|
||||||
|
|
||||||
|
shared
|
||||||
|
.loadScript(
|
||||||
|
'https://cdn.jsdelivr.net/npm/ace-builds@1.16.0/src-min-noconflict/ace.min.js'
|
||||||
|
)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEditor() {
|
||||||
|
this.editor = ace.edit('noteplus-editor')
|
||||||
|
this.editor.setTheme('ace/theme/dracula')
|
||||||
|
this.editor.session.setMode('ace/mode/html')
|
||||||
|
|
||||||
|
this.editor.setShowPrintMargin(false)
|
||||||
|
this.editor.session.setUseWrapMode(true)
|
||||||
|
this.editor.renderer.setShowGutter(false)
|
||||||
|
this.editor.session.setTabSize(4)
|
||||||
|
this.editor.session.setUseSoftTabs(true)
|
||||||
|
this.editor.setFontSize(14)
|
||||||
|
this.editor.setReadOnly(false)
|
||||||
|
this.editor.setHighlightActiveLine(false)
|
||||||
|
this.editor.setShowFoldWidgets(true)
|
||||||
|
|
||||||
|
this.editor.session.on('change', (delta) => {
|
||||||
|
// delta.start, delta.end, delta.lines, delta.action
|
||||||
|
if (this.live) {
|
||||||
|
this.updateHTML(this.editor.getValue())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHTML(val) {
|
||||||
|
// if (CONTAINER_HTML.includes('${html}')) {
|
||||||
|
// console.log('found template')
|
||||||
|
// val = CONTAINER_HTML.replace('${html}', val)
|
||||||
|
// }
|
||||||
|
|
||||||
|
this.html_widget.value = val
|
||||||
|
this.rawVal = val
|
||||||
|
|
||||||
|
this.calculated_height = this.html_widget.element.scrollHeight
|
||||||
|
|
||||||
|
this.setSize(this.computeSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
// // onRemoved() {
|
||||||
|
// // console.log('Removing', this)
|
||||||
|
// // for (const w of this.widgets) {
|
||||||
|
// // console.log('Removing', w)
|
||||||
|
// // w.onRemove?.()
|
||||||
|
// // w.onRemoved?.()
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
}
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: 'mtb.noteplus',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
// app.ui.settings.addSetting({
|
||||||
|
// id: "mtb.noteplus.Container",
|
||||||
|
// name: "📦 HTML container",
|
||||||
|
// type: "text",
|
||||||
|
// defaultValue: "<div>${html}</div>",
|
||||||
|
// tooltip:
|
||||||
|
// "This defines the wrapper for the noteplus html content, use '${html}' to define the location of the placeholder",
|
||||||
|
// attrs: {
|
||||||
|
// style: {
|
||||||
|
// fontFamily: "monospace",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// onChange(value) {
|
||||||
|
// if (!value) {
|
||||||
|
// CONTAINER_HTML = null;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// console.log(`NOTEPLUS| value changed: ${value}`)
|
||||||
|
// CONTAINER_HTML = value
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
},
|
||||||
|
|
||||||
|
registerCustomNodes() {
|
||||||
|
LiteGraph.registerNodeType('Note Plus (mtb)', NotePlus)
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user