utils.py:
Added FileBackedDict, which provides a dictionary-like interface with file-backed persistence. Supports nested dictionaries and ensures data is stored and retrieved from disk. video.py: Implemented audio resampling in CombineAudioVideoFiles, including video and audio resampling logic. Refined create_video_from_images to use ffmpeg for combining image sequences into video. Updated CombineFoldersWithFramesToVideoFiles to handle both duration and fps-based input. Added additional parameters for video quality and pixel format. torchvision_nodes.py: Centralized the imports for NODE_CLASS_MAPPINGS and NODE_DISPLAY_NAME_MAPPINGS across the module. Adjusted default values for certain fields to improve user experience. youtube_nodes.py & modules/youtube.py: Extended the YouTube upload functionality to support optional thumbnail uploads. Improved initialize_upload function to return video_id after a successful upload. Introduced the ability to upload a thumbnail alongside the video. web/ui_modifications.js: Reworked the login form handling to prevent page reload and improved element management. Added dynamic graph creation in the handleGraphCreateNew function. web/showMarkdown.js: Fixed the import of marked.js, using Marked as the module name to avoid potential conflicts. modules/xttscli.py: Increased the batch_size for text-to-speech processing from 2 to 4, enabling faster concurrent execution.
This commit is contained in:
+1
-1
@@ -43,7 +43,7 @@ def generate_xtts(**kwargs):
|
||||
texts = text
|
||||
|
||||
o_files = []
|
||||
batch_size = 2
|
||||
batch_size = 4
|
||||
with ProcessPoolExecutor(max_workers=batch_size) as executor:
|
||||
futures = []
|
||||
for i, text in enumerate(texts):
|
||||
|
||||
+30
-44
@@ -10,6 +10,7 @@ from apiclient.http import MediaFileUpload
|
||||
from oauth2client.client import flow_from_clientsecrets
|
||||
from oauth2client.file import Storage
|
||||
from oauth2client.tools import argparser, run_flow
|
||||
from googleapiclient.http import MediaFileUpload
|
||||
|
||||
# Explicitly tell the underlying HTTP transport library not to retry, since
|
||||
# we are handling retry logic ourselves.
|
||||
@@ -25,16 +26,6 @@ RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError)
|
||||
# codes is raised.
|
||||
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
|
||||
|
||||
# The CLIENT_SECRETS_FILE variable specifies the name of a file that contains
|
||||
# the OAuth 2.0 information for this application, including its client_id and
|
||||
# client_secret. You can acquire an OAuth 2.0 client ID and client secret from
|
||||
# the Google API Console at
|
||||
# https://console.cloud.google.com/.
|
||||
# Please ensure that you have enabled the YouTube Data API for your project.
|
||||
# For more information about using OAuth2 to access the YouTube Data API, see:
|
||||
# https://developers.google.com/youtube/v3/guides/authentication
|
||||
# For more information about the client_secrets.json file format, see:
|
||||
# https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
|
||||
try:
|
||||
import comfy
|
||||
except ModuleNotFoundError:
|
||||
@@ -44,11 +35,9 @@ except ModuleNotFoundError:
|
||||
comfy.__path__._path = ["."]
|
||||
|
||||
comfy_dir = comfy.__path__._path[0]
|
||||
# the base dir we need is one level down use os.path.dirname to get it
|
||||
base_dir = os.path.dirname(comfy_dir)
|
||||
# now we can use os.path.join to get the full path to the file
|
||||
CLIENT_SECRETS_FILE = os.path.join(base_dir, "google-credentials.json")
|
||||
|
||||
CLIENT_SECRETS_FILE = r"Q:\GDE\custom_nodes\EternalKernelLiteGraphNodes\modules\youtube.py-oauth2-secrets.json"
|
||||
# This OAuth 2.0 access scope allows an application to upload files to the
|
||||
# authenticated user's YouTube channel, but doesn't allow other types of access.
|
||||
YOUTUBE_UPLOAD_SCOPE = "https://www.googleapis.com/auth/youtube.upload"
|
||||
@@ -91,7 +80,7 @@ def get_authenticated_service(args):
|
||||
http=credentials.authorize(httplib2.Http()))
|
||||
|
||||
|
||||
def initialize_upload(youtube, options):
|
||||
def initialize_upload(youtube_api, options):
|
||||
tags = None
|
||||
if options.keywords:
|
||||
tags = options.keywords.split(",")
|
||||
@@ -109,28 +98,14 @@ def initialize_upload(youtube, options):
|
||||
)
|
||||
|
||||
# Call the API's videos.insert method to create and upload the video.
|
||||
insert_request = youtube.videos().insert(
|
||||
insert_request = youtube_api.videos().insert(
|
||||
part=",".join(body.keys()),
|
||||
body=body,
|
||||
# The chunksize parameter specifies the size of each chunk of data, in
|
||||
# bytes, that will be uploaded at a time. Set a higher value for
|
||||
# reliable connections as fewer chunks lead to faster uploads. Set a lower
|
||||
# value for better recovery on less reliable connections.
|
||||
#
|
||||
# Setting "chunksize" equal to -1 in the code below means that the entire
|
||||
# file will be uploaded in a single HTTP request. (If the upload fails,
|
||||
# it will still be retried where it left off.) This is usually a best
|
||||
# practice, but if you're using Python older than 2.6 or if you're
|
||||
# running on App Engine, you should set the chunksize to something like
|
||||
# 1024 * 1024 (1 megabyte).
|
||||
media_body=MediaFileUpload(options.file, chunksize=-1, resumable=True)
|
||||
)
|
||||
|
||||
resumable_upload(insert_request)
|
||||
|
||||
|
||||
# This method implements an exponential backoff strategy to resume a
|
||||
# failed upload.
|
||||
video_id = resumable_upload(insert_request)
|
||||
return video_id
|
||||
|
||||
|
||||
def resumable_upload(insert_request):
|
||||
@@ -143,14 +118,13 @@ def resumable_upload(insert_request):
|
||||
status, response = insert_request.next_chunk()
|
||||
if response is not None:
|
||||
if 'id' in response:
|
||||
print("Video id '%s' was successfully uploaded." %
|
||||
response['id'])
|
||||
print("Video id '%s' was successfully uploaded." % response['id'])
|
||||
return response['id']
|
||||
else:
|
||||
exit("The upload failed with an unexpected response: %s" % response)
|
||||
except HttpError as e:
|
||||
if e.resp.status in RETRIABLE_STATUS_CODES:
|
||||
error = "A retriable HTTP error %d occurred:\n%s" % (e.resp.status,
|
||||
e.content)
|
||||
error = "A retriable HTTP error %d occurred:\n%s" % (e.resp.status, e.content)
|
||||
else:
|
||||
raise
|
||||
except RETRIABLE_EXCEPTIONS as e:
|
||||
@@ -168,19 +142,26 @@ def resumable_upload(insert_request):
|
||||
time.sleep(sleep_seconds)
|
||||
|
||||
|
||||
def upload_thumbnail(youtube, video_id, thumbnail_path):
|
||||
print("Uploading thumbnail...")
|
||||
request = youtube.thumbnails().set(
|
||||
videoId=video_id,
|
||||
media_body=MediaFileUpload(thumbnail_path)
|
||||
)
|
||||
response = request.execute()
|
||||
print("Thumbnail uploaded successfully.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
argparser.add_argument("--file", required=True,
|
||||
help="Video file to upload")
|
||||
argparser.add_argument("--file", required=True, help="Video file to upload")
|
||||
argparser.add_argument("--title", help="Video title", default="Test Title")
|
||||
argparser.add_argument("--description", help="Video description",
|
||||
default="Test Description")
|
||||
argparser.add_argument("--description", help="Video description", default="Test Description")
|
||||
argparser.add_argument("--category", default="22",
|
||||
help="Numeric video category. " +
|
||||
"See https://developers.google.com/youtube/v3/docs/videoCategories/list")
|
||||
argparser.add_argument("--keywords", help="Video keywords, comma separated",
|
||||
default="")
|
||||
help="Numeric video category. See https://developers.google.com/youtube/v3/docs/videoCategories/list")
|
||||
argparser.add_argument("--keywords", help="Video keywords, comma separated", default="")
|
||||
argparser.add_argument("--privacyStatus", choices=VALID_PRIVACY_STATUSES,
|
||||
default=VALID_PRIVACY_STATUSES[0], help="Video privacy status.")
|
||||
argparser.add_argument("--thumbnail", help="Path to thumbnail image file")
|
||||
args = argparser.parse_args()
|
||||
|
||||
if not os.path.exists(args.file):
|
||||
@@ -188,6 +169,11 @@ if __name__ == '__main__':
|
||||
|
||||
youtube = get_authenticated_service(args)
|
||||
try:
|
||||
initialize_upload(youtube, args)
|
||||
video_id = initialize_upload(youtube, args)
|
||||
if args.thumbnail:
|
||||
if not os.path.exists(args.thumbnail):
|
||||
print("Thumbnail file does not exist. Skipping thumbnail upload.")
|
||||
else:
|
||||
upload_thumbnail(youtube, video_id, args.thumbnail)
|
||||
except HttpError as e:
|
||||
print("An HTTP error %d occurred:\n%s" % (e.resp.status, e.content))
|
||||
|
||||
@@ -7,8 +7,9 @@ import functools
|
||||
import torch
|
||||
import torchvision
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
#NODE_CLASS_MAPPINGS = {}
|
||||
#NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
from . import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
|
||||
def ETK_torchvision_base(cls):
|
||||
@@ -80,7 +81,7 @@ class TorchVisionTransformNode:
|
||||
"composition": ("TV_COMPOSITION_LIST", {"default": None}),
|
||||
"transform": (c_list,),
|
||||
}, "optional": {
|
||||
"args": ("STRING", {"default": None}),
|
||||
"args": ("STRING", {"default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -23,3 +23,101 @@ def etk_deep_copy(x, verbose=False):
|
||||
print(f"copying {x}, of type {type(x)}")
|
||||
|
||||
return copy.deepcopy(x)
|
||||
|
||||
|
||||
import json
|
||||
import os
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
|
||||
class FileBackedDict(MutableMapping):
|
||||
FBD_MARKER = "__is_fbd__" # Marker to indicate nested FileBackedDicts
|
||||
|
||||
def __init__(self, file_path):
|
||||
# Ensure the provided path is absolute, raise an error if not
|
||||
if not os.path.isabs(file_path):
|
||||
raise ValueError(f"The path '{file_path}' is not an absolute path.")
|
||||
|
||||
# Convert file_path to an absolute path (if needed)
|
||||
self.file_path = os.path.abspath(file_path)
|
||||
self._data = {}
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
"""Loads the data from the file into memory."""
|
||||
if os.path.exists(self.file_path):
|
||||
with open(self.file_path, 'r', encoding='utf-8') as file:
|
||||
try:
|
||||
raw_data = json.load(file)
|
||||
# Convert any sub-dictionaries marked as FileBackedDicts
|
||||
for key, value in raw_data.items():
|
||||
if isinstance(value, dict) and value.get(self.FBD_MARKER, False):
|
||||
# Create a nested FileBackedDict for each sub-dictionary
|
||||
sub_dict_file = f"{self.file_path}_{key}.json"
|
||||
self._data[key] = FileBackedDict(sub_dict_file)
|
||||
else:
|
||||
self._data[key] = value
|
||||
except json.JSONDecodeError:
|
||||
self._data = {}
|
||||
else:
|
||||
self._data = {}
|
||||
|
||||
def _save(self):
|
||||
"""Saves the current data to the file."""
|
||||
# Save only the top-level keys that are not FileBackedDicts
|
||||
data_to_save = {}
|
||||
for key, value in self._data.items():
|
||||
if isinstance(value, FileBackedDict):
|
||||
# Mark this key as being backed by a FileBackedDict
|
||||
data_to_save[key] = {self.FBD_MARKER: True}
|
||||
value._save() # Trigger save for the sub-dictionaries
|
||||
else:
|
||||
data_to_save[key] = value
|
||||
|
||||
# Save the metadata about the keys
|
||||
with open(self.file_path, 'w', encoding='utf-8') as file:
|
||||
json.dump(data_to_save, file, ensure_ascii=False, indent=4)
|
||||
|
||||
def __getitem__(self, key):
|
||||
self._load() # Ensure the data is up-to-date
|
||||
return self._data[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._load() # Ensure the data is up-to-date
|
||||
if isinstance(value, dict):
|
||||
# Create a nested FileBackedDict for each sub-dictionary
|
||||
sub_dict_file = f"{self.file_path}_{key}.json"
|
||||
self._data[key] = FileBackedDict(sub_dict_file)
|
||||
self._data[key].update(value)
|
||||
else:
|
||||
self._data[key] = value
|
||||
self._save()
|
||||
|
||||
def __delitem__(self, key):
|
||||
self._load() # Ensure the data is up-to-date
|
||||
del self._data[key]
|
||||
self._save()
|
||||
|
||||
def __iter__(self):
|
||||
self._load() # Ensure the data is up-to-date
|
||||
return iter(self._data)
|
||||
|
||||
def __len__(self):
|
||||
self._load() # Ensure the data is up-to-date
|
||||
return len(self._data)
|
||||
|
||||
def __repr__(self):
|
||||
self._load() # Ensure the data is up-to-date
|
||||
return repr(self._data)
|
||||
|
||||
def clear(self):
|
||||
"""Clear the dictionary and save the changes to the file."""
|
||||
self._data.clear()
|
||||
self._save()
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
self._load() # Ensure the data is up-to-date
|
||||
"""Update the dictionary with new data and save the changes to the file."""
|
||||
for key, value in dict(*args, **kwargs).items():
|
||||
self.__setitem__(key, value)
|
||||
self._save()
|
||||
@@ -11,6 +11,7 @@ import shutil
|
||||
import fractions
|
||||
import os
|
||||
from abc import ABC, ABCMeta, abstractmethod
|
||||
from os.path import splitext, join
|
||||
from typing import List, Tuple, Union
|
||||
import cv2
|
||||
import torch
|
||||
@@ -20,7 +21,7 @@ import torchaudio
|
||||
from torch import dtype
|
||||
# from custom_nodes.EternalKernelLiteGraphNodes.image import torch_image_show
|
||||
from torchvision.models.optical_flow import raft_large, Raft_Large_Weights, raft_small, Raft_Small_Weights
|
||||
from custom_nodes.EternalKernelLiteGraphNodes.modules.image_utils import lanczos_resize
|
||||
|
||||
import os
|
||||
import torch
|
||||
from torchvision.utils import draw_keypoints
|
||||
@@ -30,9 +31,11 @@ import numpy as np
|
||||
import cv2
|
||||
import subprocess
|
||||
import imageio_ffmpeg as ffmpeg
|
||||
from comfy_extras.nodes_video_model import SVD_img2vid_Conditioning
|
||||
# from comfy_extras.nodes_video_model import SVD_img2vid_Conditioning
|
||||
|
||||
NODE_CLASS_MAPPINGS = {} # this is the dictionary that will be used to register the nodes
|
||||
from . import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
# NODE_CLASS_MAPPINGS = {} # this is the dictionary that will be used to register the nodes
|
||||
|
||||
# setup directories
|
||||
root_p = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
|
||||
@@ -1137,11 +1140,12 @@ class CombineAudioAndVideoFiles(metaclass=ABCWidgetMetaclass):
|
||||
"""Combines lists of audio files and video files using imageio-ffmpeg"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(self):
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"audio_files": ("LIST", {"default": []}),
|
||||
"video_files": ("LIST", {"default": []}),
|
||||
"resample": (["None", "video", "audio", ],),
|
||||
"fps": ("FLOAT", {"default": 30, "min": 1, "max": 120}),
|
||||
},
|
||||
"optional": {
|
||||
@@ -1166,41 +1170,81 @@ class CombineAudioAndVideoFiles(metaclass=ABCWidgetMetaclass):
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
from os.path import splitext, join
|
||||
import shutil
|
||||
|
||||
def get_duration(self, file):
|
||||
"""Get the duration of a file in seconds."""
|
||||
command = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of',
|
||||
'default=nw=1:nk=1', file]
|
||||
try:
|
||||
output = subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
return float(output.strip())
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
|
||||
def handler(self, **kwargs):
|
||||
from os.path import splitext, join
|
||||
import shutil
|
||||
audio_files = kwargs["audio_files"]
|
||||
video_files = kwargs["video_files"]
|
||||
move_to_folder = kwargs.get("move_to_folder", "")
|
||||
resample = kwargs.get("resample", "None")
|
||||
fps = kwargs.get("fps", 30.0)
|
||||
|
||||
combined_video_paths = []
|
||||
video_audio_pairs = zip(video_files, audio_files)
|
||||
video_audio_pairs = list(zip(video_files, audio_files))
|
||||
|
||||
ffmpeg_exe = ffmpeg.get_ffmpeg_exe()
|
||||
if ffmpeg_exe is None:
|
||||
raise RuntimeError("FFmpeg could not be found.")
|
||||
|
||||
for video_file, audio_file in video_audio_pairs:
|
||||
video_file = f'{video_file}'
|
||||
audio_file = f'{audio_file}'
|
||||
|
||||
video_out_file_fp = splitext(video_file)[0] + "_v_and_a.mp4"
|
||||
video_out_file_fp = f'{splitext(video_file)[0]}_v_and_a.mp4'
|
||||
input_args = ['-i', video_file, '-i', audio_file]
|
||||
|
||||
if self.has_audio_stream(video_file):
|
||||
# The video has an audio stream, proceed with mixing
|
||||
filter_complex_arg = ['-filter_complex', '[0:a][1:a]amix=inputs=2:duration=longest[a]']
|
||||
filter_complex_arg = '[0:a][1:a]amix=inputs=2:duration=longest[a]'
|
||||
output_args = ['-c:v', 'copy', '-map', '0:v:0', '-map', '[a]', '-c:a', 'aac', '-strict', 'experimental',
|
||||
'-y', video_out_file_fp]
|
||||
|
||||
|
||||
else:
|
||||
filter_complex_arg = []
|
||||
filter_complex_arg = ''
|
||||
output_args = ['-c:v', 'copy', '-map', '0:v:0', '-map', '1:a:0', '-c:a', 'aac', '-strict',
|
||||
'experimental', '-y', video_out_file_fp]
|
||||
|
||||
command = [ffmpeg_exe] + input_args + filter_complex_arg + output_args
|
||||
cmd = ' '.join(command)
|
||||
print(cmd)
|
||||
os.system(cmd)
|
||||
if resample == "video":
|
||||
video_duration = self.get_duration(video_file)
|
||||
audio_duration = self.get_duration(audio_file)
|
||||
|
||||
if video_duration and audio_duration:
|
||||
speed = audio_duration / video_duration
|
||||
filter_complex_arg = f'[0:v]setpts={speed}*PTS[v];[v][1:a]concat=n=1:v=1:a=1[v][a]'
|
||||
output_args = ['-map', '[v]', '-map', '[a]', '-c:v', 'libx264', '-c:a', 'aac', '-strict',
|
||||
'experimental', '-y', video_out_file_fp]
|
||||
else:
|
||||
raise RuntimeError("Could not determine duration of video or audio file.")
|
||||
elif resample == "None":
|
||||
output_args.extend(['-r', str(fps)])
|
||||
elif resample == "audio":
|
||||
# Not implemented: Finding silence in audio and removing it
|
||||
raise NotImplementedError(
|
||||
"Resampling audio is not implemented. Consider finding silence in the audio and removing it.")
|
||||
|
||||
try:
|
||||
# try to run it with the list of parts, using the list
|
||||
if filter_complex_arg:
|
||||
commands_to_run = [ffmpeg_exe, *input_args, '-filter_complex', filter_complex_arg, *output_args]
|
||||
else:
|
||||
commands_to_run = [ffmpeg_exe, *input_args, *output_args]
|
||||
|
||||
commands_to_run = [str(c).replace("\"", "") for c in commands_to_run]
|
||||
subprocess.run(commands_to_run, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"An error occurred: {e}")
|
||||
|
||||
if move_to_folder:
|
||||
destination_path = join(move_to_folder, os.path.basename(video_out_file_fp))
|
||||
@@ -1212,6 +1256,10 @@ class CombineAudioAndVideoFiles(metaclass=ABCWidgetMetaclass):
|
||||
else:
|
||||
combined_video_paths.append(video_out_file_fp)
|
||||
|
||||
os.chdir("..")
|
||||
os.chdir("..")
|
||||
os.chdir("..")
|
||||
|
||||
return (combined_video_paths,)
|
||||
|
||||
|
||||
@@ -1226,7 +1274,7 @@ class CombineFoldersWithFramesToVideoFiles(metaclass=ABCWidgetMetaclass):
|
||||
"fps/length": ("FLOAT", {"default": 30, "min": 1, "max": 10000000000}),
|
||||
"bitrate": ("STRING", {"default": "20000k"}),
|
||||
"codec": (
|
||||
["libx264", "libx265", "mpeg4", "vp9", "huffyuv", "flv1", "h264_nvenc", "hevc_nvenc"],
|
||||
["libx264", "libx265", "mpeg4", "vp9", "huffyuv", "flv1", "h264_nvenc", "hevc_nvenc", "rawvideo"],
|
||||
{"default": "libx264"}
|
||||
),
|
||||
"file_extension": (
|
||||
@@ -1237,6 +1285,8 @@ class CombineFoldersWithFramesToVideoFiles(metaclass=ABCWidgetMetaclass):
|
||||
},
|
||||
"optional": {
|
||||
"[duration or fps]": ("LIST", {"default": []}),
|
||||
"q": ("INT", {"default": 20, "min": 0, "max": 31}),
|
||||
"pix_fmt": ("STRING", {"default": "yuv420p"}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1247,63 +1297,76 @@ class CombineFoldersWithFramesToVideoFiles(metaclass=ABCWidgetMetaclass):
|
||||
FUNCTION = "handler"
|
||||
|
||||
def handler(self, **kwargs):
|
||||
import imageio
|
||||
import os
|
||||
import time
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
folders_with_frames = kwargs["folders_with_frames"]
|
||||
fps_length_value = kwargs["fps/length"]
|
||||
bitrate = kwargs["bitrate"]
|
||||
codec = kwargs["codec"]
|
||||
file_extension = kwargs["file_extension"]
|
||||
fps_or_length = kwargs["fps or length"]
|
||||
folders_with_frames = kwargs.get("folders_with_frames", [])
|
||||
fps_length_value = kwargs.get("fps/length", 30)
|
||||
file_extension = kwargs.get("file_extension", ".mkv")
|
||||
fps_or_length = kwargs.get("fps or length", "fps")
|
||||
duration_or_fps_list = kwargs.get("[duration or fps]", [])
|
||||
codec = kwargs.get("codec", "libx264")
|
||||
q = kwargs.get("q", 20)
|
||||
pix_fmt = kwargs.get("pix_fmt", "yuv420p")
|
||||
|
||||
combined_video_paths = []
|
||||
messages = []
|
||||
|
||||
ffmpeg_exe = 'ffmpeg' # Ensure ffmpeg is in your PATH or provide the full path to the ffmpeg executable
|
||||
if not shutil.which(ffmpeg_exe):
|
||||
raise RuntimeError("FFmpeg could not be found.")
|
||||
|
||||
for i, folder_path in enumerate(folders_with_frames):
|
||||
try:
|
||||
out_file = os.path.join(os.path.dirname(folder_path),
|
||||
f"{os.path.basename(folder_path)}tmp_{i}{file_extension}")
|
||||
frame_files = sorted(
|
||||
[f for f in os.listdir(folder_path) if f.lower().endswith(('.jpg', '.jpeg', '.png'))])
|
||||
|
||||
if not frame_files:
|
||||
messages.append(f"No frame files found in folder: {folder_path}")
|
||||
continue
|
||||
|
||||
if fps_or_length == "fps":
|
||||
output_file = os.path.join(os.path.dirname(folder_path),
|
||||
f"{os.path.basename(folder_path)}tmp_{i}{file_extension}")
|
||||
if fps_or_length == 'fps':
|
||||
fps = fps_length_value
|
||||
if duration_or_fps_list:
|
||||
fps = duration_or_fps_list[i]
|
||||
|
||||
else:
|
||||
frame_files = sorted(
|
||||
[f for f in os.listdir(folder_path) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]
|
||||
)
|
||||
total_frames = len(frame_files)
|
||||
fps = total_frames / fps_length_value if fps_length_value != 0 else 30
|
||||
if duration_or_fps_list:
|
||||
# calculate the fps based on the duration
|
||||
fps = total_frames / duration_or_fps_list[i]
|
||||
|
||||
writer = imageio.get_writer(out_file, fps=fps, codec=codec, bitrate=bitrate)
|
||||
|
||||
for frame_file in frame_files:
|
||||
frame_path = os.path.join(folder_path, frame_file)
|
||||
frame = imageio.imread(frame_path)
|
||||
writer.append_data(frame)
|
||||
|
||||
writer.close()
|
||||
combined_video_paths.append(out_file)
|
||||
create_video_from_images(folder_path, output_file, fps, codec, q, pix_fmt)
|
||||
combined_video_paths.append(output_file)
|
||||
except Exception as e:
|
||||
messages.append(f"Error processing folder {folder_path}: {str(e)}")
|
||||
|
||||
end_time = time.time()
|
||||
print(f"Time taken for CombineFoldersWithFramesToVideoFiles: {end_time - start_time} seconds")
|
||||
return (combined_video_paths, "\n".join(messages),)
|
||||
|
||||
|
||||
def create_file_list(folder_path):
|
||||
frame_files = sorted(
|
||||
[f for f in os.listdir(folder_path) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]
|
||||
)
|
||||
list_file_path = os.path.join(folder_path, 'filelist.txt')
|
||||
with open(list_file_path, 'w') as f:
|
||||
for frame_file in frame_files:
|
||||
f.write(f"file '{os.path.join(folder_path, frame_file)}'\n")
|
||||
return list_file_path
|
||||
|
||||
|
||||
import subprocess
|
||||
|
||||
|
||||
def create_video_from_images(folder_path, output_file, fps, codec, q, pix_fmt):
|
||||
list_file_path = create_file_list(folder_path)
|
||||
ffmpeg_exe = 'ffmpeg' # Ensure ffmpeg is in your PATH or provide the full path to the ffmpeg executable
|
||||
|
||||
command = [
|
||||
ffmpeg_exe, '-y', '-f', 'concat', '-safe', '0', '-i', list_file_path,
|
||||
'-vf', f"fps={fps}", '-c:v', codec, '-pix_fmt', pix_fmt, "-qp", str(q), output_file
|
||||
]
|
||||
|
||||
try:
|
||||
subprocess.run(command, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise RuntimeError(f"ffmpeg failed with error: {e.stderr.decode()}")
|
||||
|
||||
|
||||
class ImageStackToVideoFile(metaclass=ABCWidgetMetaclass):
|
||||
"""Really just saves the image stack to a video file"""
|
||||
|
||||
@@ -2065,7 +2128,8 @@ class VideoFolderProvider(metaclass=ABCWidgetMetaclass):
|
||||
def INPUT_TYPES(cls):
|
||||
ret = {
|
||||
"required": {
|
||||
"folder_path": ("STRING", {"default": None}),
|
||||
"folder_path": ("STRING", {"default": ""}),
|
||||
"clear_folder": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
"activator:": ("*", {"default": None})
|
||||
@@ -2099,19 +2163,20 @@ class VideoFolderProvider(metaclass=ABCWidgetMetaclass):
|
||||
if not accessible:
|
||||
raise ValueError("Folder for video files is not accessible")
|
||||
|
||||
# if the folder exists, delete all files in the folder
|
||||
if folder_exists:
|
||||
for file in files_in_folder:
|
||||
# use shutil to remove the file
|
||||
fl = os.path.join(kwargs["folder_path"], file)
|
||||
if os.path.isfile(fl):
|
||||
os.remove(fl)
|
||||
# use shutil to remove the entire path
|
||||
check_this_path = file
|
||||
if len(check_this_path) > 3:
|
||||
shutil.rmtree(check_this_path, ignore_errors=True)
|
||||
else:
|
||||
raise ValueError("Folder path is too short")
|
||||
if kwargs["clear_folder"]:
|
||||
# if the folder exists, delete all files in the folder
|
||||
if folder_exists:
|
||||
for file in files_in_folder:
|
||||
# use shutil to remove the file
|
||||
fl = os.path.join(kwargs["folder_path"], file)
|
||||
if os.path.isfile(fl):
|
||||
os.remove(fl)
|
||||
# use shutil to remove the entire path
|
||||
check_this_path = file
|
||||
if len(check_this_path) > 3:
|
||||
shutil.rmtree(check_this_path, ignore_errors=True)
|
||||
else:
|
||||
raise ValueError("Folder path is too short")
|
||||
|
||||
# if the folder does not exist, create the folder
|
||||
|
||||
@@ -2128,7 +2193,8 @@ class AudioFolderProvider(metaclass=ABCWidgetMetaclass):
|
||||
def INPUT_TYPES(cls):
|
||||
ret = {
|
||||
"required": {
|
||||
"folder_path": ("STRING", {"default": None}),
|
||||
"folder_path": ("STRING", {"default": ""}),
|
||||
"clear_folder": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
"activator:": ("*", {"default": None})
|
||||
@@ -2162,15 +2228,14 @@ class AudioFolderProvider(metaclass=ABCWidgetMetaclass):
|
||||
if not accessible:
|
||||
raise ValueError("Folder for audio files is not accessible")
|
||||
|
||||
# if the folder exists, delete all files in the folder
|
||||
if folder_exists:
|
||||
for file in files_in_folder:
|
||||
# use shutil to remove the file
|
||||
fl = os.path.join(kwargs["folder_path"], file)
|
||||
if os.path.isfile(fl):
|
||||
os.remove(fl)
|
||||
|
||||
# if the folder does not exist, create the folder
|
||||
if kwargs["clear_folder"]:
|
||||
# if the folder exists, delete all files in the folder
|
||||
if folder_exists:
|
||||
for file in files_in_folder:
|
||||
# use shutil to remove the file
|
||||
fl = os.path.join(kwargs["folder_path"], file)
|
||||
if os.path.isfile(fl):
|
||||
os.remove(fl)
|
||||
|
||||
return (kwargs["folder_path"],)
|
||||
|
||||
@@ -2288,6 +2353,7 @@ class AddAudioToAudioFolder(metaclass=ABCWidgetMetaclass):
|
||||
# check each file in the list to make sure we can convert it to the correct format
|
||||
import shutil
|
||||
import os
|
||||
from subprocess import run
|
||||
|
||||
ffmpeg_exe = ffmpeg.get_ffmpeg_exe()
|
||||
|
||||
@@ -2307,9 +2373,11 @@ class AddAudioToAudioFolder(metaclass=ABCWidgetMetaclass):
|
||||
output_file_name = audio_folder_def.get_next_file_name()
|
||||
input_file_name = audio_file
|
||||
|
||||
CMD = f"{ffmpeg_exe} -y -i {input_file_name} -ar {audio_folder_def['sample rate']} -ac {audio_folder_def['channels']} {output_file_name}"
|
||||
CMD = f"{ffmpeg_exe} -y -i {input_file_name} -ar {audio_folder_def['sample rate']} -ac {audio_folder_def['channels']} '{output_file_name}'"
|
||||
CMD_AS_LIST = [ffmpeg_exe, "-y", "-i", input_file_name, "-ar", str(audio_folder_def['sample rate']), "-ac",
|
||||
str(audio_folder_def['channels']), output_file_name]
|
||||
|
||||
os.system(CMD)
|
||||
run(CMD_AS_LIST)
|
||||
if delete:
|
||||
os.remove(input_file_name)
|
||||
|
||||
@@ -2605,6 +2673,7 @@ class AddFramesToVideoFolder(metaclass=ABCWidgetMetaclass):
|
||||
import gc
|
||||
from torchvision.transforms.functional import resize
|
||||
import psutil
|
||||
from custom_nodes.EternalKernelLiteGraphNodes.modules.image_utils import lanczos_resize
|
||||
|
||||
initial_memory = psutil.virtual_memory().available
|
||||
print(f"Initial free memory: {initial_memory / (1024 * 1024):.2f} MB")
|
||||
@@ -2688,8 +2757,8 @@ class AddFramesToVideoFolder(metaclass=ABCWidgetMetaclass):
|
||||
|
||||
torch.cuda.empty_cache()
|
||||
gc.collect()
|
||||
torch.cuda.empty_cache()
|
||||
gc.collect()
|
||||
# torch.cuda.empty_cache()
|
||||
# gc.collect()
|
||||
|
||||
initial_memory = psutil.virtual_memory().available
|
||||
print(f"after adding frames free memory: {initial_memory / (1024 * 1024):.2f} MB")
|
||||
@@ -2868,6 +2937,9 @@ class Listed_HoldFramesForSecsInVideoFolder(metaclass=ABCWidgetMetaclass):
|
||||
# the plan is to call HoldFrameForSecsInVideoFolder for each frame and duration
|
||||
# and then return the video_folder_def
|
||||
|
||||
if "IMAGE" not in kwargs or "seconds" not in kwargs:
|
||||
return (kwargs["video_folder_def"],)
|
||||
|
||||
for frame, duration in zip(kwargs["IMAGE"], kwargs["seconds"]):
|
||||
HoldFrameForSecsInVideoFolder().hold_frame_for_secs_in_video_folder(
|
||||
video_folder_def=kwargs["video_folder_def"],
|
||||
@@ -2988,6 +3060,7 @@ class ListedSVDConditioning(metaclass=ABCWidgetMetaclass):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
from comfy_extras.nodes_video_model import SVD_img2vid_Conditioning
|
||||
base = SVD_img2vid_Conditioning.INPUT_TYPES()
|
||||
base["required"]["init_image"] = ("LIST", {"default": None})
|
||||
# base["required"]["clip_vision"] = ("LIST", {"default": None})
|
||||
|
||||
+2
-3
@@ -1,7 +1,6 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
import marked from "./marked.js";
|
||||
|
||||
import Marked from "./marked.js";
|
||||
|
||||
|
||||
// Displays input markdown on a node
|
||||
@@ -29,7 +28,7 @@ app.registerExtension({
|
||||
const w = ComfyWidgets["STRING"](this, "markdown", ["STRING", { multiline: true }], app).widget;
|
||||
|
||||
// Convert markdown to HTML
|
||||
const html = marked(list);
|
||||
const html = Marked(list);
|
||||
|
||||
// Create a new div element to hold the rendered markdown
|
||||
const div = document.createElement("div");
|
||||
|
||||
+37
-16
@@ -18,7 +18,12 @@ class GDELogin {
|
||||
}
|
||||
|
||||
initElements() {
|
||||
this.login_div = this.createElement("div", {id: "gde-login"});
|
||||
this.login_form = this.createElement("form", {id: "gde-login"});
|
||||
// make sure the form doesnt cause the page to reload
|
||||
this.login_form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
//this.login_div = this.createElement("div", {id: "gde-login"});
|
||||
this.usernameField = this.createInputField("username-field", "Username");
|
||||
this.passwordField = this.createInputField("password-field", "Password", "password");
|
||||
this.loginButton = this.createButton("login-button", "Login", this.handleLogin.bind(this));
|
||||
@@ -95,7 +100,7 @@ class GDELogin {
|
||||
id: "welcome-message",
|
||||
innerText: `Welcome ${this.usernameField.value}`
|
||||
});
|
||||
this.login_div.append(welcomeMessage);
|
||||
this.login_form.append(welcomeMessage);
|
||||
}
|
||||
|
||||
|
||||
@@ -121,12 +126,12 @@ class GDELogin {
|
||||
this.usernameField.remove();
|
||||
this.passwordField.remove();
|
||||
this.loginButton.remove();
|
||||
this.login_div.append(this.logoutButton);
|
||||
this.login_form.append(this.logoutButton);
|
||||
}
|
||||
|
||||
switchToLoginView() {
|
||||
this.logoutButton.remove();
|
||||
this.login_div.append(this.usernameField, this.passwordField, this.loginButton);
|
||||
this.login_form.append(this.usernameField, this.passwordField, this.loginButton);
|
||||
}
|
||||
|
||||
clearDropdowns() {
|
||||
@@ -174,15 +179,15 @@ class GDELogin {
|
||||
separator.style.margin = "20px 0";
|
||||
separator.style.width = "100%";
|
||||
menu.append(separator);
|
||||
menu.append(this.login_div);
|
||||
menu.append(this.login_form);
|
||||
|
||||
if (logged_in) {
|
||||
this.login_div.append(this.logoutButton);
|
||||
this.login_form.append(this.logoutButton);
|
||||
|
||||
} else {
|
||||
this.login_div.append(this.usernameField);
|
||||
this.login_div.append(this.passwordField);
|
||||
this.login_div.append(this.loginButton);
|
||||
this.login_form.append(this.usernameField);
|
||||
this.login_form.append(this.passwordField);
|
||||
this.login_form.append(this.loginButton);
|
||||
|
||||
}
|
||||
});
|
||||
@@ -338,20 +343,36 @@ app.registerExtension({
|
||||
|
||||
const handleGraphCreateNew = (event, dropdown) => {
|
||||
// Custom logic for when "Create New" is selected in the branch dropdown
|
||||
|
||||
showModal('graph-name-field')
|
||||
showModal('graph-name-field');
|
||||
dropdown.disabled = true;
|
||||
|
||||
const modal = document.getElementById("graph-name-modal");
|
||||
// hijack its onsubmit function
|
||||
// Hijack its onsubmit function
|
||||
const prev_submit = modal.submit;
|
||||
//submitButton.onclick
|
||||
|
||||
modal.submit = () => {
|
||||
let graphName = prev_submit();
|
||||
gdeState.graph = graphName;
|
||||
dropdown.disabled = false;
|
||||
if (graphName) {
|
||||
// Add the new graph to the dropdown
|
||||
const optionElement = document.createElement("option");
|
||||
optionElement.value = graphName;
|
||||
optionElement.textContent = graphName;
|
||||
dropdown.append(optionElement);
|
||||
|
||||
}
|
||||
// Set the new graph as the selected value
|
||||
dropdown.value = graphName;
|
||||
gdeState.graph = graphName;
|
||||
|
||||
// Dispatch a custom event to notify other parts of the application
|
||||
const graphChangeEvent = new CustomEvent('graphChangeEvent', {
|
||||
detail: {
|
||||
graph: graphName
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(graphChangeEvent);
|
||||
}
|
||||
dropdown.disabled = false;
|
||||
};
|
||||
};
|
||||
const handleGraphSelect = (event, dropdown) => {
|
||||
// change gdeState.graph when it is changed
|
||||
|
||||
+8
-6
@@ -3,6 +3,7 @@ import os
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
from . import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
|
||||
def ETK_youtube_base(cls):
|
||||
@@ -32,6 +33,9 @@ class YoutubeUploadNode:
|
||||
"description": "Description of the video"}),
|
||||
"keywords": ("STRING", {"default": "tag1, tag2, tag3", "description": "Keywords for the video"}),
|
||||
"privacy_status": (cls.VALID_PRIVACY_STATUSES,)
|
||||
},
|
||||
"optional": {
|
||||
"thumbnail_path": ("STRING", {"default": "", "description": "Path to the thumbnail image file"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +43,7 @@ class YoutubeUploadNode:
|
||||
FUNCTION = "upload_video"
|
||||
CATEGORY = "ETK/Youtube"
|
||||
|
||||
def upload_video(self, file_path, category, title, description, keywords, privacy_status):
|
||||
def upload_video(self, file_path, category, title, description, keywords, privacy_status, thumbnail_path=""):
|
||||
import sys
|
||||
my_python_exe = sys.executable
|
||||
|
||||
@@ -49,14 +53,12 @@ class YoutubeUploadNode:
|
||||
args = ["--file", file_path, "--category", str(category), "--title", title, "--description", description,
|
||||
"--keywords", keywords, "--privacyStatus", privacy_status]
|
||||
|
||||
if thumbnail_path:
|
||||
args.extend(["--thumbnail", thumbnail_path])
|
||||
|
||||
current_dir = os.getcwd()
|
||||
os.chdir(os.path.dirname(os.path.realpath(__file__)))
|
||||
|
||||
# result = subprocess.run([my_python_exe, "./modules/youtube.py"] + args,
|
||||
# capture_output=True,
|
||||
# text=True)
|
||||
|
||||
# the above does not capture the output, so we use the below instead
|
||||
result = subprocess.run([my_python_exe, "./modules/youtube.py"] + args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
|
||||
Reference in New Issue
Block a user