Files
JaredTherriault-ComfyUI-JNodes/py/deprecated_nodes.py
T
JaredTherriault 6838fd084a Generify Media Upload nodes, fix video previews, image drawer drag and drop to node support
-Generify video loading nodes to work for many visual media types, still images, animated images, some video types, etc. 'Video' nodes and scripts and params have been renamed to 'media' to reflect the change.
-Added some accessors for MediaInfo
-Replaced OutVideoInfo with BreakMediaInfo
-Added some more information to MediaInfo
-Stashed renamed nodes into deprecated_nodes so they're still accessible for now
-Cleaned up some unnecessary variables and params in the media loading code, now runs a tiny bit faster
-Now possible to sample media by seconds or frames
-Added some filetype support in js, still need to just pull this directly from python
-Renamed some js variables to be less parsimonious and more clear
-Fix video preview in SaveVideo
-Encode text in video preview uri (so spaces in filenames work, et al)
-Add support for images and videos from the image drawer to be dragged onto UploadMedia node
-Formatting
2024-04-22 13:32:25 -07:00

323 lines
13 KiB
Python

import os
import folder_paths
from .logger import logger
from .misc import *
from .utils import *
import cv2
import re
import torch
import numpy as np
import comfy.sd
from comfy.utils import common_upscale
from PIL import Image, ImageSequence, ImageOps
from typing import List
class VideoInfo:
def __init__(self):
self.original_frame_count = 0
self.original_fps = 0.0
self.original_video_duration = 0.0
self.original_frame_time = 0.0
self.actual_frame_count = 0
self.actual_fps = 0.0
self.actual_video_duration = 0.0
self.actual_frame_time = 0.0
@staticmethod
def build_video_info(original_fps : float, original_frame_count : int, desired_frame_rate : float, frames_added : int):
video_info = VideoInfo()
video_info.original_fps = float(original_fps)
video_info.original_frame_count = int(original_frame_count)
video_info.original_video_duration = float(original_frame_count / original_fps)
video_info.original_frame_time = float(1 / original_fps)
actual_fps = desired_frame_rate if desired_frame_rate > 0.001 else original_fps
video_info.actual_fps = float(actual_fps)
video_info.actual_frame_count = int(frames_added)
video_info.actual_video_duration = float(frames_added / actual_fps)
video_info.actual_frame_time = float(1 / actual_fps)
return video_info
class OutVideoInfo:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"in_video_info": ("JNODES_VIDEO_INFO",),
},
}
RETURN_TYPES = (
"INT", "FLOAT",
"FLOAT", "FLOAT",
"INT", "FLOAT",
"FLOAT", "FLOAT",
)
RETURN_NAMES = (
"original_frame_count", "original_fps",
"original_video_duration", "original_frame_time",
"actual_frame_count", "actual_fps",
"actual_video_duration", "actual_frame_time",
)
FUNCTION = "output_video_info"
def output_video_info(self, in_video_info : VideoInfo):
out = (
in_video_info.original_frame_count, in_video_info.original_fps,
in_video_info.original_video_duration, in_video_info.original_frame_time,
in_video_info.actual_frame_count, in_video_info.actual_fps,
in_video_info.actual_video_duration, in_video_info.actual_frame_time
)
#print(out)
return out
class LoadVideo:
"""
A mutation from Kosinkadink's VideoHelperSuite, credits to his repository!
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"video": ("STRING", {"default": "X://insert/path/here.mp4"}),
"desired_frame_rate": ("FLOAT", {"default": 0, "min": 0, "step": 1}),
"force_size": (["Disabled", "256x?", "?x256", "256x256", "512x?", "?x512", "512x512"],),
"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}),
"discard_transparency": ("BOOLEAN", {"default": True})
},
}
RETURN_TYPES = ("IMAGE", "JNODES_VIDEO_INFO",)
RETURN_NAMES = ("IMAGE", "out_video_info",)
FUNCTION = "load_video"
known_exceptions = []
def load_video(self, **kwargs):
try:
return LoadVideo.load_video_cv(**kwargs)
except Exception as e:
raise RuntimeError(f"Failed to load video: {kwargs['video']}\ndue to: {e.__str__()}")
@classmethod
def VALIDATE_INPUTS(s, video, **kwargs):
if video is not None and not os.path.isfile(video.strip("\"")):
return "Invalid video file: {}".format(video)
return True
def target_size(width, height, force_size) -> tuple[int, int]:
if force_size != "Disabled":
force_size = force_size.split("x")
if force_size[0] == "?":
width = (width*int(force_size[1]))//height
#Limit to a multple of 8 for latent conversion
#TODO: Consider instead cropping and centering to main aspect ratio
width = int(width)+4 & ~7
height = int(force_size[1])
elif force_size[1] == "?":
height = (height*int(force_size[0]))//width
height = int(height)+4 & ~7
width = int(force_size[0])
return (width, height)
def force_size(force_size, width, height, images):
if force_size != "Disabled":
new_size = LoadVideo.target_size(width, height, force_size)
if new_size[0] != width or new_size[1] != height:
s = images.movedim(-1,1)
s = common_upscale(s, new_size[0], new_size[1], "lanczos", "disabled")
return s.movedim(1,-1)
return images
def load_video_pil(
video: str, desired_frame_rate: float, force_size: str, frame_load_cap: int, skip_first_frames: int, select_every_nth: int, discard_transparency):
"""
For any other animated type, such as webp, apng, or mjpeg.
Can't really use the force_rate param since we're not sampling the video,
we're just loading the frames directly.
"""
logger.info(
'Falling back to load_video_pil. desired_frame_rate will be ignored, and the original frame rate will be used. ' +
'If desired_frame_rate is important, please consider converting this media to another file type.')
try:
loaded_video = Image.open(video)
except Exception as e:
raise Exception(f"Unable to open video with pil fallback: {video}")
frames = ImageSequence.Iterator(loaded_video)
images = []
original_frame_time = None
for image in frames:
if original_frame_time is None:
if 'duration' not in image.info:
image.load()
original_frame_time = image.info.get('duration', None)
# Ensure the image does not have an alpha channel
if discard_transparency and image.mode == 'RGBA':
image = image.convert('RGB')
image = ImageOps.exif_transpose(image)
image = np.array(image, dtype=np.float32) / 255.0
image = torch.from_numpy(image)[None,]
images.append(image)
if original_frame_time is None:
raise Exception(f"Could not get original_frame_time from video: {video}")
original_frame_count = len(images)
original_fps = 1000 / original_frame_time
width = loaded_video.width
height = loaded_video.height
# Remove images before the skip and beyond the cap
if skip_first_frames != 0:
images = images[skip_first_frames:]
if frame_load_cap != 0:
images = images[skip_first_frames:frame_load_cap]
out_images = []
for i, image in enumerate(images):
if i % select_every_nth == 0:
out_images.append(image)
out_images = torch.cat(out_images, dim=0)
out_images = LoadVideo.force_size(force_size, width, height, out_images)
return (out_images, VideoInfo.build_video_info(original_fps, original_frame_count, original_fps, len(out_images)))
def load_video_cv(
video: str, desired_frame_rate: float, force_size: str, frame_load_cap: int, skip_first_frames: int, select_every_nth: int, discard_transparency):
def retry_with_pil(video, desired_frame_rate, force_size, frame_load_cap, skip_first_frames, select_every_nth, discard_transparency):
logger.info(f"Retrying with pil due to opencv error")
return LoadVideo.load_video_pil(video, desired_frame_rate, force_size, frame_load_cap, skip_first_frames, select_every_nth, discard_transparency)
try:
video_cap = cv2.VideoCapture(video)
if not video_cap.isOpened():
return retry_with_pil(video, desired_frame_rate, force_size, frame_load_cap, skip_first_frames, select_every_nth, discard_transparency)
# set video_cap to look at start_index frame
images = []
original_frame_count = video_cap.get(cv2.CAP_PROP_FRAME_COUNT)
total_frame_count = 0
total_frames_evaluated = -1
frames_added = 0
original_fps = video_cap.get(cv2.CAP_PROP_FPS)
original_frame_time = 1/original_fps
width = video_cap.get(cv2.CAP_PROP_FRAME_WIDTH)
height = video_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
if desired_frame_rate < 0.001:
target_frame_time = original_frame_time
else:
target_frame_time = 1/desired_frame_rate
time_offset=target_frame_time - original_frame_time
while video_cap.isOpened():
if time_offset < target_frame_time:
is_returned, frame = video_cap.read()
# if didn't return frame, video has ended
if not is_returned:
break
time_offset += original_frame_time
if time_offset < target_frame_time:
continue
time_offset -= target_frame_time
# if not at start_index, skip doing anything with frame
total_frame_count += 1
if total_frame_count <= skip_first_frames:
continue
else:
total_frames_evaluated += 1
# if should not be selected, skip doing anything with frame
if total_frames_evaluated%select_every_nth != 0:
continue
# opencv loads images in BGR format (yuck), so need to convert to RGB for ComfyUI use
# follow up: can videos ever have an alpha channel?
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# convert frame to comfyui's expected format (taken from comfy's load image code)
image = Image.fromarray(frame)
image = ImageOps.exif_transpose(image)
image = np.array(image, dtype=np.float32) / 255.0
image = torch.from_numpy(image)[None,]
images.append(image)
frames_added += 1
# if cap exists and we've reached it, stop processing frames
if frame_load_cap > 0 and frames_added >= frame_load_cap:
break
finally:
if video_cap:
video_cap.release()
if len(images) > 0:
images = torch.cat(images, dim=0)
else:
return retry_with_pil(video, desired_frame_rate, force_size, frame_load_cap, skip_first_frames, select_every_nth, discard_transparency)
images = LoadVideo.force_size(force_size, width, height, images)
return (images, VideoInfo.build_video_info(original_fps, original_frame_count, desired_frame_rate, frames_added))
class UploadVideo:
"""
This is also found in Kosinkadink's VideoHelperSuite, but that was something I contributed.
"""
UPLOAD_SUBDIRECTORY = "upload_video"
INPUT_DIR_TYPE_NAMES = ["input", "temp"]
@classmethod
def INPUT_TYPES(s):
files = []
# This just pulls in whatever's in the /input/upload_video folder on start
for input_type in s.INPUT_DIR_TYPE_NAMES:
input_dir = convert_relative_comfyui_path_to_full_path(input_type)
if not os.path.isdir(input_dir):
continue
for f in os.listdir(input_dir):
if os.path.isfile(os.path.join(input_dir, f)):
file_parts = f.split('.')
if len(file_parts) > 1 and (file_parts[-1] in ACCEPTED_UPLOAD_VIDEO_EXTENSIONS + ACCEPTED_ANIMATED_IMAGE_EXTENSIONS):
files.append(f"{input_type}/{s.UPLOAD_SUBDIRECTORY}/{f}")
return {"required": {
"video": (sorted(files), {"video_upload": True}),
"upload_to_directory": (s.INPUT_DIR_TYPE_NAMES,),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("video_path",)
FUNCTION = "upload_video"
def upload_video(self, video, upload_to_directory):
return (convert_relative_comfyui_path_to_full_path(video),)
@classmethod
def VALIDATE_INPUTS(s, video, upload_to_directory, **kwargs):
full_path = convert_relative_comfyui_path_to_full_path(video)
if not os.path.isfile(full_path):
return f"Invalid video file: {full_path}"
return True