Files
TashaSkyUp-EternalKernelPyt…/audio.py
T
Tasha Upchurch d9170f804b New Features: Added nodes for advanced video and audio processing
Key updates:

Added AddFramesToVideoFolder node to add frames to a video folder
Introduced VideoFolderProvider and VideoDefinitionProvider nodes to manage video folders and video definitions
Added FadeToInVideoFolder node for fading between frames in a video folder
Implemented ListedSVDConditioning node for applying SVD conditioning to a list of parameters
Added VideoFolderFFMPEGVideoInterpolate node to interpolate frames in a video folder using FFmpeg
Introduced AudioFolderProvider and AudioDefinitionProvider nodes for managing audio folders and definitions
Added AddAudioToAudioFolder and AddSilenceToAudioFolder nodes for adding audio files and silence to audio folders
Implemented GetAudioFilesLengthsFromAudioFolder node to retrieve lengths of audio files in a folder
Added AudioFolderRenderAudio node to join audio files in an audio folder
Introduced GenerateMusic node for generating music from descriptions using the AudioCraft library
Added XttsNode for text-to-speech conversion using the XTTS library
Implemented AdjustAudioVolumeForFiles node to adjust volume for a list of audio files
The changes also include the addition of LiteLLM nodes for text generation:

Added FuncAnysToText node that runs Python code with multiple input types and outputs a string
Introduced FuncAnysToLatent node for running code that outputs a latent representation
Added FuncAnysToString node for executing code with various inputs and producing a string output
Implemented FuncAnysToList node to run code that takes different input types and returns a list
Added FuncAnysToCond node for running code with multiple inputs and generating a conditioning output

These new nodes expand the capabilities for video and audio processing, allowing more advanced manipulations and generation of video and audio content within the graph.
2024-04-15 13:29:31 -04:00

580 lines
19 KiB
Python

import shutil
import copy
import imageio_ffmpeg as ffmpeg
import joblib
from config import config_settings
temp_dir = config_settings["tmp_dir"]
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
# Create a memory object for caching
# memory = joblib.Memory(temp_dir, verbose=0)
def ETK_audio_base(cls):
cls.CATEGORY = "ETK/audio"
NODE_CLASS_MAPPINGS[cls.__name__] = cls
return cls
@ETK_audio_base
class GetAudioMetaData:
""" Returns a dictionary of audio metadata """
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"path": ("STRING", {"multiline": False, "default": ""}),
}
}
RETURN_TYPES = ("DICT",)
FUNCTION = 'get_audio_metadata'
CATEGORY = 'ETK/audio'
def get_audio_metadata(self, **kwargs):
import audioread
kwargs = copy.deepcopy(kwargs)
path = kwargs.get('path')
metadata = {}
with audioread.audio_open(path) as f:
metadata['duration'] = f.duration
metadata['samplerate'] = f.samplerate
metadata['channels'] = f.channels
return (metadata,)
@ETK_audio_base
class AudioFileListFromPathPattern:
""" Returns a list of audio files from a path pattern """
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"path": ("STRING", {"multiline": False, "default": ""}),
}
}
RETURN_TYPES = ("LIST", "LIST", "LIST",)
RETURN_NAMES = ("list_of_fpaths", "list_of_bytes", "list_of_metadata")
FUNCTION = 'get_audio_file_list_from_path_pattern'
CATEGORY = 'ETK/audio'
def get_audio_file_list_from_path_pattern(self, **kwargs):
import glob
kwargs = copy.deepcopy(kwargs)
path = kwargs.get('path')
l_fpaths = glob.glob(path)
l_bytes = []
l_metadata = []
for fpath in l_fpaths:
with open(fpath, "rb") as f:
l_bytes.append(f.read())
l_metadata.append(GetAudioMetaData().get_audio_metadata(path=fpath)[0])
return (l_fpaths, l_bytes, l_metadata,)
import numpy as np
@ETK_audio_base
class JoinAudioFiles:
""" Joins a list of audio files into a single audio file """
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"paths": ("LIST", {"default": []}),
"output_path": ("STRING", {"multiline": False, "default": ""}),
}
}
RETURN_TYPES = ("STRING", "BYTES", "DICT",)
FUNCTION = 'join_audio_files'
CATEGORY = 'ETK/audio'
def join_audio_files(self, **kwargs):
import librosa
import soundfile as sf
kwargs = copy.deepcopy(kwargs)
paths = kwargs['paths']
output_path = kwargs['output_path']
# Read the first file to get metadata
audio_data_list = []
for path in paths:
audio_data, sr = librosa.load(path, sr=None)
audio_data_list.append(audio_data)
# Concatenate all audio data
joined_audio_data = np.concatenate(audio_data_list)
# Save the concatenated audio
sf.write(output_path, joined_audio_data, sr)
# Get metadata
metadata = {
'duration': librosa.get_duration(y=joined_audio_data, sr=sr),
'samplerate': sr
}
# Get bytes
with open(output_path, "rb") as f:
file_bytes = f.read()
return (output_path, file_bytes, metadata,)
@ETK_audio_base
class ExtractAudioFromVideo:
""" Extracts audio from a video file and saves it as an audio file with a specified format and bitrate """
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"video_path": ("STRING", {"multiline": False, "default": ""}),
"format": (["wav", "mp3"], {"default": "wav"}),
"output_path": ("STRING", {"multiline": False, "default": ""}),
},
"optional": {
"bitrate (MP3)": ("INT", {"default": 192}) # Default bitrate value (change as needed)
}
}
RETURN_TYPES = ("STRING", "BYTES")
RETURN_NAMES = ("output_wave_file_path", "audio_bytes")
FUNCTION = 'extract_audio_from_video'
CATEGORY = 'ETK/audio'
def extract_audio_from_video(self, **kwargs):
import moviepy.editor as mp
import io
kwargs = copy.deepcopy(kwargs)
video_path = kwargs.get('video_path')
output_path = kwargs.get('output_path', "tmp.wav")
audio_format = kwargs.get('format', 'wav')
bitrate = kwargs.get('bitrate', None) # Default bitrate is None
# Load the video clip
video_clip = mp.VideoFileClip(video_path)
# Determine the appropriate codec based on the audio format
if audio_format == 'wav':
codec = "pcm_s16le" # For WAV format
elif audio_format == 'mp3':
codec = "mp3" # For MP3 format
# Extract audio and save it with the specified format and bitrate
audio_clip = video_clip.audio
audio_clip.write_audiofile(output_path, codec=codec, bitrate=bitrate)
# Read the generated audio file as bytes
audio_bytes = None
with open(output_path, 'rb') as audio_file:
audio_bytes = audio_file.read()
return (output_path, audio_bytes,)
# import torchaudio
# from audiocraft.models import MusicGen
# from audiocraft.data.audio import audio_write
#
# model = MusicGen.get_pretrained('melody')
# model.set_generation_params(duration=8) # generate 8 seconds.
#
# descriptions = ['happy rock', 'energetic EDM', 'sad jazz']
#
# melody, sr = torchaudio.load('./assets/bach.mp3')
# # generates using the melody from the given audio and the provided descriptions.
# wav = model.generate_with_chroma(descriptions, melody[None].expand(3, -1, -1), sr)
#
# for idx, one_wav in enumerate(wav):
# # Will save under {idx}.wav, with loudness normalization at -14 db LUFS.
# audio_write(f'{idx}', one_wav.cpu(), model.sample_rate, strategy="loudness")
@ETK_audio_base
class GenerateMusic:
""" Generates music from a given melody and descriptions """
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"description": ("STRING", {"default": ""}),
"[description]": ("LIST", {"default": None}),
},
"required": {
"model_name": (["facebook/musicgen-melody-large",
"facebook/musicgen-melody-small",
"facebook/musicgen-melody-medium",
"facebook/musicgen-stereo-melody-large",
"facebook/musicgen-stereo-melody-small",
"facebook/musicgen-stereo-melody-medium",
"facebook/musicgen-large",
"facebook/musicgen-small",
"facebook/musicgen-medium",
"facebook/musicgen-stereo-large",
"facebook/musicgen-stereo-small",
"facebook/musicgen-stereo-medium",
],),
"duration": ("INT", {"default": None}),
"[duration]": ("LIST", {"default": None}),
"file": ("STRING", {"default": ""}),
"seed": ("INT", {"default": -1}),
}
}
RETURN_TYPES = ("STRING", "LIST",)
RETURN_NAMES = ("file", "files_list",)
FUNCTION = 'generate_music'
CATEGORY = 'ETK/audio'
def create_command(self, kwargs):
import os
musicgencli_path = os.path.join(os.path.dirname(__file__), "modules", "musicgencli.py")
ukwargs = copy.deepcopy(kwargs)
nl = "\n"
ukwargs["description"] = kwargs.get("[description]", [])
ukwargs.pop("[description]")
ukwargs["duration"] = kwargs.get("[duration]", [])
ukwargs.pop("[duration]")
ukwargs["description"] = str([i.replace("'", "") for i in ukwargs["description"]])
command = " ".join([f"--{k} \"{str(v).replace(nl, ' ')}\"" for k, v in ukwargs.items()])
command = f"python {musicgencli_path} {command}"
return command
def generate_music(self, **kwargs):
# use the cli to call the cli music generator at ./modules/musicgencli.py
import os
import subprocess
command = self.create_command(kwargs)
# Execute the command caputring the output
try:
output = subprocess.check_output(command, shell=True)
except subprocess.CalledProcessError as e:
print(f"Error: {e}")
# use the os to find the new files
new_files = []
for file in os.listdir(os.path.dirname(kwargs['file'])):
if file.endswith(".wav"):
new_files.append(file)
return (kwargs['file'], new_files,)
def generate_music_old(self, **kwargs):
from audiocraft.models import MusicGen
from audiocraft.data.audio import audio_write
import torch
torch.cuda.empty_cache()
import random
# Set seed, -1 means random seed
seed = kwargs.get('seed', -1)
if kwargs.get('seed') == -1:
seed = random.randint(0, 100000)
old_seed_values = set_all_seeds(seed)
kwargs = copy.deepcopy(kwargs)
description = kwargs.get('description')
descriptions = kwargs.get('[description]')
if description and not descriptions:
descriptions = [description]
file = kwargs.get('file')
# add .wave if not there
if not file.endswith('.wav'):
file = file + ".wav"
model_name = kwargs.get('model_name', 'melody')
duration = kwargs.get('duration', 10)
durations = kwargs.get('[duration]', None)
if duration:
durations = [duration]
model = MusicGen.get_pretrained(model_name)
o_files = []
for i, description in enumerate(descriptions):
ii = str(i).zfill(1 + len(str(1 + len(descriptions))))
file_to_use = file.replace(".wav", f"_{ii}.wav")
duration_to_use = durations[i]
model.set_generation_params(duration=duration_to_use)
wav = model.generate([description])
# q = MusicGen.set_generation_params()
audio_write(file_to_use.replace(".wav", ""), wav[0].cpu(), model.sample_rate, strategy="loudness")
o_files.append(file_to_use)
# Reset seed
reset_seeds(old_seed_values)
torch.cuda.empty_cache()
del model
torch.cuda.empty_cache()
return (o_files[0], o_files,)
def set_all_seeds(seed):
import random
import os
import numpy as np
import torch
old_values = {}
old_values['random'] = random.getstate()
old_values['numpy'] = np.random.get_state()
old_values['torch'] = torch.get_rng_state()
old_values['torch_cuda'] = torch.cuda.get_rng_state()
old_values['torch_cuda_all'] = torch.cuda.get_rng_state_all()
old_values['torch_deterministic'] = torch.backends.cudnn.deterministic
# From https://gist.github.com/gatheluck/c57e2a40e3122028ceaecc3cb0d152ac
random.seed(seed)
os.environ["PYTHONHASHSEED"] = str(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.backends.cudnn.deterministic = True
return old_values
def reset_seeds(old_values):
import random
import numpy as np
import torch
random.setstate(old_values['random'])
np.random.set_state(old_values['numpy'])
torch.set_rng_state(old_values['torch'])
torch.cuda.set_rng_state(old_values['torch_cuda'])
torch.cuda.set_rng_state_all(old_values['torch_cuda_all'])
torch.backends.cudnn.deterministic = old_values['torch_deterministic']
@ETK_audio_base
class XttsNode:
""" Converts text to speech """
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"device": ("STRING", {"default": "cuda"}),
},
"optional": {
"text": ("STRING", {"multiline": False, "default": ""}),
"[text]": ("LIST", {"multiline": False, "default": ""}),
"speaker_wav": ("STRING", {"multiline": False, "default": ""}),
"language_code": (
['en', 'es', 'fr', 'de', 'it', 'pt', 'pl', 'tr', 'ru', 'nl', 'cs', 'ar', 'zh-cn', 'hu', 'ko', 'ja',
'hi'], {"default": "en"}),
"output_path": ("STRING", {"multiline": False, "default": ""}),
"AUDIO_FOLDER_DEF": ("AUDIO_FOLDER_DEF", {"default": None}),
}
}
RETURN_TYPES = ("STRING", "LIST", "STRING", "AUDIO_FOLDER_DEF",)
RETURN_NAMES = ("output file", "output files list", "output folder", "AUDIO_FOLDER_DEF",)
FUNCTION = 'xtts'
CATEGORY = 'ETK/audio'
def xtts(self=None, **kwargs):
import os
import subprocess
import sys
import shutil
from .config import this_file_path
audio_folder_def = kwargs.get("AUDIO_FOLDER_DEF", None)
python_exe = sys.executable
# Path to the xttscli.py script
xttscli_path = os.path.join(this_file_path, "modules", "xttscli.py")
# Check if '[text]' is in kwargs and if it is a list
if '[text]' in kwargs and isinstance(kwargs['[text]'], list):
texts = str(kwargs['[text]'])
else:
texts = str([kwargs.get('text', "")])
# Create the command
command = [
python_exe, xttscli_path,
"--text", texts,
"--folder", kwargs.get('output_path', "."),
"--lang", kwargs.get('lang', 'en'),
"--speaker", kwargs.get('speaker_wav', ''),
]
# execute the command in its own full porcess and recieve the output
output = subprocess.check_output(command, shell=True)
generated = output.decode("utf-8")
generated_files = generated.split("Generated files: (")[1].split(")")[0]
generated_files = eval(generated_files)
generated_files = generated_files[1]
# get the output folder
output_folder = os.path.dirname(kwargs.get('file', "output.wav"))
# move the files to the correct files
out_files = []
for src_file in generated_files:
target_file = audio_folder_def.get_next_file_name()
shutil.copy(src_file, target_file)
out_files.append(target_file)
return (out_files[0], out_files, output_folder, audio_folder_def,)
import moviepy.editor as mpy
class MovieCrossfadeMultiAudio:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"video_path1": ("STRING", {}),
"video_path2": ("STRING", {}),
"crossfade_duration": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1})
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "crossfade_videos"
CATEGORY = "video_processing"
def crossfade_videos(self, video_path1, video_path2, crossfade_duration):
# Load video clips
clip1 = mpy.VideoFileClip(video_path1)
clip2 = mpy.VideoFileClip(video_path2)
# Extract primary audio tracks (assuming the first track is primary)
audio1 = clip1.audio
audio2 = clip2.audio
# Crossfade audio tracks
audio_crossfade = mpy.CompositeAudioClip([audio1, audio2.set_start(clip1.duration - crossfade_duration)])
# Crossfade video clips
video_crossfade = mpy.concatenate_videoclips([clip1, clip2.set_start(clip1.duration - crossfade_duration)],
method="compose", padding=-crossfade_duration)
# Set the crossfaded audio to the crossfaded video
final_clip = video_crossfade.set_audio(audio_crossfade)
# Output attributes
self.video = final_clip
return {
"video": self.video
}
@ETK_audio_base
class AdjustAudioVolumeForFiles():
"""Adjusts the volume of a list of audio files."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"audio_files": ("LIST", {"default": []}),
"volume": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1}),
},
"optional": {
"output_folder": ("STRING", {"default": ""}),
}
}
RETURN_TYPES = ("LIST",)
RETURN_NAMES = ("modified_audio_paths",)
CATEGORY = "audio_processing"
OUTPUT_NODE = True
FUNCTION = "handler"
def handler(self, **kwargs):
import os
from os.path import splitext, join
audio_files = kwargs["audio_files"]
volume = kwargs["volume"]
output_folder = kwargs.get("output_folder", "")
modified_audio_paths = []
ffmpeg_exe = ffmpeg.get_ffmpeg_exe()
if ffmpeg_exe is None:
raise RuntimeError("FFmpeg could not be found.")
for audio_file in audio_files:
output_file = splitext(audio_file)[0] + f"_adjusted_volume.mp3"
if output_folder:
output_file = join(output_folder, os.path.basename(output_file))
command = [
ffmpeg_exe, '-i', audio_file,
'-filter:a', f'volume={volume}',
'-y', output_file
]
cmd = ' '.join(command)
print(cmd)
os.system(cmd)
modified_audio_paths.append(output_file)
return (modified_audio_paths,)
if __name__ == '__main__':
# do some testing
wave_file = """C:\\Users\\Tasha\\Nextcloud\\ETK\\YT\\Monsters\\The Haunter of the Ring\\The Haunter of the Ring real.wav"""
xtts_node = XttsNode()
xtts_node.xtts(speaker_wav=wave_file,
text="Hello world! I'm alive, you had better believe it! muahahaha ---- I'm alive! HAHAHAHA! hahahahahahahaha",
language_code="en",
output_path="output.wav")
# use windows to play the file
import os
os.system("start output.wav")
# Paths to the video files to crossfade
video_path1 = 'path/to/your/first/video.mp4'
video_path2 = 'path/to/your/second/video.mp4'
# Duration of the crossfade in seconds
crossfade_duration = 2.0 # Example duration
# Create an instance of the MovieCrossfadeMultiAudio node
crossfade_node = MovieCrossfadeMultiAudio()
# Execute the node's crossfade function
output_video = crossfade_node.crossfade_videos(video_path1, video_path2, crossfade_duration)
# Save the output video to a file
output_video_path = 'path/to/your/output/crossfaded_video.mp4'
output_video['video'].write_videofile(output_video_path, codec='libx264', audio_codec='aac')
print(f'Crossfaded video saved to {output_video_path}')