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:
Tasha Upchurch
2024-09-22 20:05:04 -06:00
parent b57701a669
commit 4a1e6dfb49
8 changed files with 333 additions and 153 deletions
+1 -1
View File
@@ -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
View File
@@ -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))
+4 -3
View File
@@ -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": ""}),
},
}
+98
View File
@@ -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()
+153 -80
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,