Add files via upload

This commit is contained in:
Fictiverse
2026-08-08 11:57:59 +02:00
committed by GitHub
parent e5396c65b8
commit 4d01e2c6d5
2 changed files with 312 additions and 47 deletions
+221
View File
@@ -0,0 +1,221 @@
import os
import numpy as np
import torch
from PIL import Image
import folder_paths
RATIOS = ["1:1", "5:4", "4:3", "3:2", "16:9", "2:1", "21:9", "32:9"]
def ensure_multiple_of_32(img):
"""Ajuste les dimensions d'une image PIL pour qu'elles soient multiples de 32."""
w, h = img.size
if w % 32 != 0 or h % 32 != 0:
w_new = max(32, int(round(w / 32.0) * 32))
h_new = max(32, int(round(h / 32.0) * 32))
return img.resize((w_new, h_new), Image.Resampling.LANCZOS)
return img
def calculate_dimensions(ratio_str, portrait, target_megapixel):
"""Calcule les dimensions (largeur, hauteur) arrondies à un multiple de 32."""
try:
w_ratio, h_ratio = map(float, ratio_str.split(":"))
except ValueError:
w_ratio, h_ratio = 1.0, 1.0
if portrait:
w_ratio, h_ratio = h_ratio, w_ratio
aspect_ratio = w_ratio / h_ratio
target_pixels = target_megapixel * 1_000_000
h = np.sqrt(target_pixels / aspect_ratio)
w = aspect_ratio * h
w = max(32, int(round(w / 32.0) * 32))
h = max(32, int(round(h / 32.0) * 32))
return w, h
def scale_to_megapixel(img, target_megapixel):
"""Redimensionne une image PIL selon un nombre de mégapixels cible, multiple de 32."""
w_orig, h_orig = img.size
aspect_ratio = w_orig / h_orig
target_pixels = target_megapixel * 1_000_000
h = np.sqrt(target_pixels / aspect_ratio)
w = aspect_ratio * h
w = max(32, int(round(w / 32.0) * 32))
h = max(32, int(round(h / 32.0) * 32))
return img.resize((w, h), Image.Resampling.LANCZOS)
class Load_Image_Advanced:
@classmethod
def INPUT_TYPES(cls):
input_dir = folder_paths.get_input_directory()
files = [
f
for f in os.listdir(input_dir)
if os.path.isfile(os.path.join(input_dir, f))
]
return {
"required": {
"image_file": (sorted(files), {"image_upload": True}),
"enable": (
"BOOLEAN",
{
"default": True,
"label_on": "Enabled 🟢",
"label_off": "Disabled 🔴",
},
),
"scale_image": ("BOOLEAN", {"default": False}),
"megapixel": (
"FLOAT",
{"default": 1.0, "min": 0.1, "max": 16.0, "step": 0.1},
),
"ratio": (RATIOS, {"default": "1:1"}),
"orientation": (
"BOOLEAN",
{
"default": False,
"label_on": "Portrait ▯",
"label_off": "Landscape ▭",
},
),
}
}
RETURN_TYPES = ("IMAGE_PARAMS",)
RETURN_NAMES = ("_",)
FUNCTION = "run"
CATEGORY = "Fictiverse/Image"
def run(self, image_file, enable, scale_image, megapixel, ratio, orientation):
if enable:
image_path = folder_paths.get_annotated_filepath(image_file)
img = Image.open(image_path)
img = img.convert("RGB")
if scale_image:
img = scale_to_megapixel(img, megapixel)
else:
img = ensure_multiple_of_32(img)
w, h = img.size
else:
w, h = calculate_dimensions(ratio, orientation, megapixel)
img = Image.new("RGB", (w, h), (0, 0, 0))
image_tensor = torch.from_numpy(
np.array(img).astype(np.float32) / 255.0
)[None,]
# Compactage de tous les paramètres et de l'image
params = {
"image": image_tensor,
"width": w,
"height": h,
"megapixel": megapixel,
"enabled": enable,
}
return (params,)
class Load_Image_Bypass:
@classmethod
def INPUT_TYPES(cls):
input_dir = folder_paths.get_input_directory()
# Extensions d'images et de GIFs supportées par PIL
valid_extensions = {'.png', '.jpg', '.jpeg', '.webp', '.bmp', '.tiff', '.gif', '.apng'}
files = [
f for f in os.listdir(input_dir)
if os.path.isfile(os.path.join(input_dir, f))
and os.path.splitext(f)[1].lower() in valid_extensions
]
return {
"required": {
"image_file": (sorted(files), {"image_upload": True}),
"enable": (
"BOOLEAN",
{
"default": True,
"label_on": "Active 🟢",
"label_off": "Disabled 🔴",
},
),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "run"
CATEGORY = "Fictiverse/Image"
def run(self, image_file, enable):
# Si désactivé, on retourne (None,) pour interrompre la branche dans ComfyUI
if not enable:
return (None,)
image_path = folder_paths.get_annotated_filepath(image_file)
img = Image.open(image_path)
img = img.convert("RGB")
img = ensure_multiple_of_32(img)
image_tensor = torch.from_numpy(
np.array(img).astype(np.float32) / 255.0
)[None,]
return (image_tensor,)
class Unpack_Image_Params:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"params": ("IMAGE_PARAMS",),
}
}
RETURN_TYPES = ("IMAGE", "INT", "INT", "FLOAT", "BOOLEAN")
RETURN_NAMES = ("image", "width", "height", "megapixel", "enabled")
FUNCTION = "unpack"
CATEGORY = "Fictiverse/Image"
def unpack(self, params):
image = params.get("image")
if image is None:
image = torch.zeros((1, 512, 512, 3), dtype=torch.float32)
return (
image,
params.get("width", 512),
params.get("height", 512),
params.get("megapixel", 1.0),
params.get("enabled", True),
)
# ==========================================
# MAPPINGS COMFYUI
# ==========================================
NODE_CLASS_MAPPINGS = {
"LoadImageAdvanced": Load_Image_Advanced,
"LoadImageBypass": Load_Image_Bypass,
"UnpackImageParams": Unpack_Image_Params,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LoadImageAdvanced": "Load Image Advanced",
"LoadImageBypass": "Load Image Bypass",
"UnpackImageParams": "Unpack Image Params",
}
+91 -47
View File
@@ -1,78 +1,122 @@
#Code inspired by sv-nodes, thanks to him.
import math
# ==========================================
# CLASSSES PARAMS VIDÉO
# ==========================================
class VideoParams:
RATIOS = ["1:1", "5:4", "4:3", "3:2", "16:9", "21:9"]
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
def INPUT_TYPES(cls):
return {
"required": {
"base": ("INT", {"default": 768, "min": 128, "max": 4096, "step": 128}),
"ratio": (VideoParams.RATIOS,),
"base": (
"INT",
{"default": 768, "min": 128, "max": 4096, "step": 128},
),
"ratio": (cls.RATIOS,),
"orientation": ("BOOLEAN", {"default": False, "label_on": "Portrait ▯", "label_off": "Landscape ▭"}),
"Frames": ("INT", {"min": 1, "max": 600, "step": 1, "default": 81}),
"FPS": ("INT", {"min": 1, "max": 120, "step": 1, "default": 16})
"duration": (
"FLOAT",
{"default": 10.0, "min": 1.0, "max": 60.0, "step": 1.0},
),
}
}
RETURN_TYPES = ("VParams",)
RETURN_NAMES = ("_",)
FUNCTION = "run"
CATEGORY = "Fictiverse/Params"
def run(self, base, ratio, orientation, Frames, FPS,):
ratio = ratio.split(":")
if len(ratio) != 2:
raise ValueError("Invalid ratio")
ratio = math.sqrt(float(ratio[0]) / float(ratio[1]))
width = math.floor(base * ratio / 64) * 64
height = math.floor(base / ratio / 64) * 64
#Frames = max(7, (math.floor(Frames / 6) * 6) + 1)
megapixels = (base*base)/1000000
def run(self, base, ratio, orientation, duration):
ratio_parts = ratio.split(":")
if len(ratio_parts) != 2:
raise ValueError("Invalid ratio format")
ratio_val = math.sqrt(float(ratio_parts[0]) / float(ratio_parts[1]))
# Calcul des dimensions multiples de 32
width = math.floor(base * ratio_val / 32) * 32
height = math.floor(base / ratio_val / 32) * 32
if orientation:
width, height = height, width
return ((width, height, Frames, float(FPS), megapixels),)
NODE_CLASS_MAPPINGS = {
"Video Params": VideoParams,
}
# Calcul plus précis des mégapixels réels en sortie
megapixels = (width * height) / 1_000_000.0
return ((width, height, duration, megapixels),)
class VideoParamsExpand:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
def INPUT_TYPES(cls):
return {"required": {"_": ("VParams",)}}
RETURN_TYPES = ("INT", "INT", "FLOAT", "FLOAT")
RETURN_NAMES = ("width", "height", "duration", "Megapixels")
FUNCTION = "run"
CATEGORY = "Fictiverse/Params"
def run(self, _):
if not isinstance(_, (tuple, list)):
raise TypeError("Invalid packet input type")
if len(_) != 4:
raise ValueError(
f"Invalid packet length (expected 4, got {len(_)})"
)
return _
# ==========================================
# NOUVEAU NODE : DURATION TO FRAMES
# ==========================================
class DurationToFrames:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"_": ("VParams",)
"duration": (
"FLOAT",
{"default": 10.0, "min": 0.1, "max": 1000.0, "step": 0.1},
),
"fps": (
"INT",
{"default": 24, "min": 1, "max": 240, "step": 1},
),
}
}
RETURN_TYPES = ("INT", "INT", "INT", "FLOAT", "FLOAT")
RETURN_NAMES = ("width", "height", "frames", "fps", "Megapixels")
RETURN_TYPES = ("INT", "FLOAT")
RETURN_NAMES = ("int", "float")
FUNCTION = "run"
CATEGORY = "Fictiverse/Params"
def run(self, _):
if not isinstance(_, tuple):
raise TypeError("Invalid packet input type")
if len(_) != 5:
raise ValueError("Invalid packet length")
return _
NODE_CLASS_MAPPINGS["Video Params Expand"] = VideoParamsExpand
def run(self, duration, fps):
# Traduction de l'expression :
# a -> duration
# 24 -> fps
raw_frames = max(5, round(duration * fps))
frame_count = raw_frames + (5 - (raw_frames % 17)) % 17
return (frame_count, float(frame_count))
# ==========================================
# MAPPINGS COMFYUI
# ==========================================
NODE_CLASS_MAPPINGS = {
"Video Params": VideoParams,
"Video Params Expand": VideoParamsExpand,
"Duration To Frames": DurationToFrames,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Video Params": "Video Params",
"Video Params Expand": "Video Params Expand",
"Duration To Frames": "Duration To Frames",
}