more extensions

This commit is contained in:
salt
2024-02-20 08:30:59 -08:00
parent 8048a05d12
commit 7053eec79c
145 changed files with 7747 additions and 2775 deletions
@@ -3,6 +3,8 @@ import torch
import comfy.utils
from .utils import BIGMIN, BIGMAX
class MergeStrategies:
MATCH_A = "match A"
@@ -36,7 +38,7 @@ class SplitLatents:
return {
"required": {
"latents": ("LATENT",),
"split_index": ("INT", {"default": 0, "step": 1, "min": -99999999999}),
"split_index": ("INT", {"default": 0, "step": 1, "min": BIGMIN, "max": BIGMAX}),
},
}
@@ -61,7 +63,7 @@ class SplitImages:
return {
"required": {
"images": ("IMAGE",),
"split_index": ("INT", {"default": 0, "step": 1, "min": -99999999999}),
"split_index": ("INT", {"default": 0, "step": 1, "min": BIGMIN, "max": BIGMAX}),
},
}
@@ -83,7 +85,7 @@ class SplitMasks:
return {
"required": {
"mask": ("MASK",),
"split_index": ("INT", {"default": 0, "step": 1, "min": -99999999999}),
"split_index": ("INT", {"default": 0, "step": 1, "min": BIGMIN, "max": BIGMAX}),
},
}
@@ -258,7 +260,7 @@ class SelectEveryNthLatent:
return {
"required": {
"latents": ("LATENT",),
"select_every_nth": ("INT", {"default": 1, "min": 1, "step": 1}),
"select_every_nth": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1}),
},
}
@@ -279,7 +281,7 @@ class SelectEveryNthImage:
return {
"required": {
"images": ("IMAGE",),
"select_every_nth": ("INT", {"default": 1, "min": 1, "step": 1}),
"select_every_nth": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1}),
},
}
@@ -300,7 +302,7 @@ class SelectEveryNthMask:
return {
"required": {
"mask": ("MASK",),
"select_every_nth": ("INT", {"default": 1, "min": 1, "step": 1}),
"select_every_nth": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1}),
},
}
@@ -378,7 +380,7 @@ class DuplicateLatents:
return {
"required": {
"latents": ("LATENT",),
"multiply_by": ("INT", {"default": 1, "min": 1, "step": 1})
"multiply_by": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1})
}
}
@@ -403,7 +405,7 @@ class DuplicateImages:
return {
"required": {
"images": ("IMAGE",),
"multiply_by": ("INT", {"default": 1, "min": 1, "step": 1})
"multiply_by": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1})
}
}
@@ -427,7 +429,7 @@ class DuplicateMasks:
return {
"required": {
"mask": ("MASK",),
"multiply_by": ("INT", {"default": 1, "min": 1, "step": 1})
"multiply_by": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1})
}
}
@@ -7,7 +7,7 @@ from PIL import Image, ImageOps
import folder_paths
from comfy.k_diffusion.utils import FolderOfImages
from .logger import logger
from .utils import calculate_file_hash, get_sorted_dir_files_from_directory, validate_path
from .utils import BIGMAX, calculate_file_hash, get_sorted_dir_files_from_directory, validate_path
def is_changed_load_images(directory: str, image_load_cap: int = 0, skip_first_images: int = 0, select_every_nth: int = 1):
@@ -93,9 +93,9 @@ class LoadImagesFromDirectoryUpload:
"directory": (directories,),
},
"optional": {
"image_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
"skip_first_images": ("INT", {"default": 0, "min": 0, "step": 1}),
"select_every_nth": ("INT", {"default": 1, "min": 1, "step": 1}),
"image_load_cap": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
"skip_first_images": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
"select_every_nth": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1}),
}
}
@@ -127,9 +127,9 @@ class LoadImagesFromDirectoryPath:
"directory": ("STRING", {"default": "X://path/to/images", "vhs_path_extensions": []}),
},
"optional": {
"image_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
"skip_first_images": ("INT", {"default": 0, "min": 0, "step": 1}),
"select_every_nth": ("INT", {"default": 1, "min": 1, "step": 1}),
"image_load_cap": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
"skip_first_images": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
"select_every_nth": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1}),
}
}
@@ -8,7 +8,7 @@ import cv2
import folder_paths
from comfy.utils import common_upscale
from .logger import logger
from .utils import calculate_file_hash, get_sorted_dir_files_from_directory, get_audio, lazy_eval, hash_path, validate_path
from .utils import BIGMAX, DIMMAX, calculate_file_hash, get_sorted_dir_files_from_directory, get_audio, lazy_eval, hash_path, validate_path
video_extensions = ['webm', 'mp4', 'mkv', 'gif']
@@ -156,13 +156,13 @@ class LoadVideoUpload:
files.append(f)
return {"required": {
"video": (sorted(files),),
"force_rate": ("INT", {"default": 0, "min": 0, "max": 24, "step": 1}),
"force_rate": ("INT", {"default": 0, "min": 0, "max": 60, "step": 1}),
"force_size": (["Disabled", "Custom Height", "Custom Width", "Custom", "256x?", "?x256", "256x256", "512x?", "?x512", "512x512"],),
"custom_width": ("INT", {"default": 512, "min": 0, "step": 8}),
"custom_height": ("INT", {"default": 512, "min": 0, "step": 8}),
"frame_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
"skip_first_frames": ("INT", {"default": 0, "min": 0, "step": 1}),
"select_every_nth": ("INT", {"default": 1, "min": 1, "step": 1}),
"custom_width": ("INT", {"default": 512, "min": 0, "max": DIMMAX, "step": 8}),
"custom_height": ("INT", {"default": 512, "min": 0, "max": DIMMAX, "step": 8}),
"frame_load_cap": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
"skip_first_frames": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
"select_every_nth": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1}),
},
"optional": {
"batch_manager": ("VHS_BatchManager",)
@@ -200,13 +200,13 @@ class LoadVideoPath:
return {
"required": {
"video": ("STRING", {"default": "X://insert/path/here.mp4", "vhs_path_extensions": video_extensions}),
"force_rate": ("INT", {"default": 0, "min": 0, "max": 24, "step": 1}),
"force_rate": ("INT", {"default": 0, "min": 0, "max": 60, "step": 1}),
"force_size": (["Disabled", "Custom Height", "Custom Width", "Custom", "256x?", "?x256", "256x256", "512x?", "?x512", "512x512"],),
"custom_width": ("INT", {"default": 512, "min": 0, "step": 8}),
"custom_height": ("INT", {"default": 512, "min": 0, "step": 8}),
"frame_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
"skip_first_frames": ("INT", {"default": 0, "min": 0, "step": 1}),
"select_every_nth": ("INT", {"default": 1, "min": 1, "step": 1}),
"custom_width": ("INT", {"default": 512, "min": 0, "max": DIMMAX, "step": 8}),
"custom_height": ("INT", {"default": 512, "min": 0, "max": DIMMAX, "step": 8}),
"frame_load_cap": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
"skip_first_frames": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
"select_every_nth": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1}),
},
"optional": {
"batch_manager": ("VHS_BatchManager",)
@@ -4,8 +4,9 @@ import json
import subprocess
import numpy as np
import re
import datetime
from typing import List
from PIL import Image
from PIL import Image, ExifTags
from PIL.PngImagePlugin import PngInfo
from pathlib import Path
@@ -15,7 +16,7 @@ from .image_latent_nodes import *
from .load_video_nodes import LoadVideoUpload, LoadVideoPath
from .load_images_nodes import LoadImagesFromDirectoryUpload, LoadImagesFromDirectoryPath
from .batched_nodes import VAEEncodeBatched, VAEDecodeBatched
from .utils import ffmpeg_path, get_audio, hash_path, validate_path, requeue_workflow
from .utils import ffmpeg_path, get_audio, hash_path, validate_path, requeue_workflow, gifski_path
folder_paths.folder_names_and_paths["VHS_video_formats"] = (
[
@@ -45,6 +46,9 @@ def get_video_formats():
video_format_path = folder_paths.get_full_path("VHS_video_formats", format_name + ".json")
with open(video_format_path, 'r') as stream:
video_format = json.load(stream)
if "gifski_pass" in video_format and gifski_path is None:
#Skip format
continue
widgets = [w[0] for w in gen_format_widgets(video_format)]
if (len(widgets) > 0):
formats.append(["video/" + format_name, widgets])
@@ -106,7 +110,7 @@ def ffmpeg_process(args, video_format, video_metadata, file_path, env):
with open(metadata_path, "w") as f:
f.write(";FFMETADATA1\n")
f.write(metadata)
m_args = args[:1] + ["-i", metadata_path] + args[1:]
m_args = args[:1] + ["-i", metadata_path] + args[1:] + ["-metadata", "creation_time=now"]
with subprocess.Popen(m_args + [file_path], stderr=subprocess.PIPE,
stdin=subprocess.PIPE, env=env) as proc:
try:
@@ -223,6 +227,7 @@ class VideoCombine:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
video_metadata[x] = extra_pnginfo[x]
metadata.add_text("CreationTime", datetime.datetime.now().isoformat(" ")[:19])
if batch_manager is not None and unique_id in batch_manager.outputs:
(counter, output_process) = batch_manager.outputs[unique_id]
@@ -263,6 +268,11 @@ class VideoCombine:
image_kwargs = {}
if format_ext == "gif":
image_kwargs['disposal'] = 2
if format_ext == "webp":
#Save timestamp information
exif = Image.Exif()
exif[ExifTags.IFD.Exif] = {36867: datetime.datetime.now().isoformat(" ")[:19]}
image_kwargs['exif'] = exif
file = f"{filename}_{counter:05}.{format_ext}"
file_path = os.path.join(full_output_folder, file)
images = tensor_to_bytes(images)
@@ -365,11 +375,26 @@ class VideoCombine:
output_files.append(file_path)
if "gifski_pass" in video_format:
gif_output = f"{filename}_{counter:05}.gif"
gif_output_path = os.path.join( full_output_folder, gif_output)
gifski_args = [gifski_path] + video_format["gifski_pass"] \
+ ["-o", gif_output_path, file_path]
try:
res = subprocess.run(gifski_args, env=env, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
raise Exception("An error occured in the gifski subprocess:\n" \
+ e.stderr.decode("utf-8"))
if res.stderr:
print(res.stderr.decode("utf-8"), end="", file=sys.stderr)
#output format is actually an image and should be correctly marked
#TODO: Evaluate a more consistent solution for this
format = "image/gif"
output_files.append(gif_output_path)
file = gif_output
# Audio Injection after video is created, saves additional video with -audio.mp4
# Create audio file if input was provided
if audio:
elif audio is not None and audio() is not False:
# Create audio file if input was provided
output_file_with_audio = f"{filename}_{counter:05}-audio.{video_format['extension']}"
output_file_with_audio_path = os.path.join(full_output_folder, output_file_with_audio)
if "audio_pass" not in video_format:
@@ -8,6 +8,11 @@ import re
import server
from .logger import logger
BIGMIN = -(2**53-1)
BIGMAX = (2**53-1)
DIMMAX = 8192
def ffmpeg_suitability(path):
try:
version = subprocess.run([path, "-version"], check=True,
@@ -31,7 +36,7 @@ def ffmpeg_suitability(path):
if "VHS_FORCE_FFMPEG_PATH" in os.environ:
ffmpeg_path = os.env["VHS_FORCE_FFMPEG_PATH"]
ffmpeg_path = os.environ.get("VHS_FORCE_FFMPEG_PATH")
else:
ffmpeg_paths = []
try:
@@ -51,9 +56,17 @@ else:
if len(ffmpeg_paths) == 0:
logger.error("No valid ffmpeg found.")
ffmpeg_path = None
elif len(ffmpeg_paths) == 1:
#Evaluation of suitability isn't required, can take sole option
#to reduce startup time
ffmpeg_path = ffmpeg_paths[0]
else:
ffmpeg_path = max(ffmpeg_paths, key=ffmpeg_suitability)
gifski_path = os.environ.get("VHS_GIFSKI", None)
if gifski_path is None:
gifski_path = os.environ.get("JOV_GIFSKI", None)
if gifski_path is None:
gifski_path = shutil.which("gifski")
def get_sorted_dir_files_from_directory(directory: str, skip_first_images: int=0, select_every_nth: int=1, extensions: Iterable=None):
directory = directory.strip()
@@ -131,8 +144,13 @@ def get_audio(file, start_time=0, duration=0):
args += ["-ss", str(start_time)]
if duration > 0:
args += ["-t", str(duration)]
return subprocess.run(args + ["-f", "wav", "-"],
stdout=subprocess.PIPE, check=True).stdout
try:
res = subprocess.run(args + ["-f", "wav", "-"],
stdout=subprocess.PIPE, check=True).stdout
except subprocess.CalledProcessError as e:
logger.warning(f"Failed to extract audio from: {file}")
return False
return res
def lazy_eval(func):