diff --git a/3d.py b/3d.py deleted file mode 100644 index 804d3b2..0000000 --- a/3d.py +++ /dev/null @@ -1,159 +0,0 @@ -NODE_CLASS_MAPPINGS = {} -NODE_DISPLAY_NAME_MAPPINGS = {} - - -def ETK_3d_base(cls): - import functools - cls.CATEGORY = "ETK/3d" - # Add spaces to the camel case class name - pretty_name = cls.__name__ - for i in range(1, len(pretty_name)): - if pretty_name[i].isupper(): - pretty_name = pretty_name[:i] + " " + pretty_name[i:] - cls.DISPLAY_NAME = pretty_name - NODE_CLASS_MAPPINGS[cls.__name__] = cls - NODE_DISPLAY_NAME_MAPPINGS[cls.DISPLAY_NAME] = pretty_name - return cls - - -@ETK_3d_base -class TripoSRNode: - def __init__(self): - import os - from folder_paths import output_directory - self.output_dir = output_directory - self.repo_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "repos", "img23d") - self.repo_url = "https://github.com/VAST-AI-Research/TripoSR.git" - self.venv_dir = os.path.join(self.repo_dir, "venv") - self.python_executable = os.path.join(self.venv_dir, "Scripts", "python") - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "output_dir": ("STRING", {"default": "outputs/TripoSR"}), - "mc_resolution": ("INT", {"default": 256}), - } - } - - RETURN_TYPES = ("STRING",) - FUNCTION = "generate_3d_models" - - CATEGORY = "3D" - - def generate_3d_models(self, image, output_dir=None, mc_resolution=256): - import asyncio - - if output_dir is not None: - #self.output_dir = f"{self.output_dir}/{output_dir}" - self.final_out_dir = f"{self.output_dir}/{output_dir}" - - # Create a new asyncio loop - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - # Run the async function within the loop - output_paths = loop.run_until_complete(self.async_generate_3d_models(image, mc_resolution)) - - return (output_paths,) - - async def async_generate_3d_models(self, image, mc_resolution): - import os - import numpy as np - from PIL import Image - import asyncio - - # Check if the TripoSR repository is installed - if not os.path.exists(self.repo_dir): - # Clone the repository if it doesn't exist - await self.clone_repository() - # Create a virtual environment and install requirements - await self.create_venv_and_install_requirements() - - # Create a temporary directory for saving input images - temp_dir = "temp_images" - os.makedirs(temp_dir, exist_ok=True) - - # Save each image in the batch as a separate file - # each image is a torch tensor with shape (b,h,w,c) - image_paths = [] - for i in range(image.shape[0]): - image_np = image[i].cpu().numpy() - image_pil = Image.fromarray((image_np * 255).astype(np.uint8)) - image_path = os.path.join(temp_dir, f"image_{i}.png") - image_pil.save(image_path) - image_paths.append(image_path) - - # Run inference using the TripoSR CLI for each image simultaneously - output_paths = await asyncio.gather(*[self.run_inference(image_path,mc_resolution) for image_path in image_paths]) - - # Clean up the temporary image files - for image_path in image_paths: - os.remove(image_path) - - return output_paths - - async def run_inference(self, image_path, mc_resolution): - import asyncio - import os - # Create a virtual environment and install requirements if they don't exist - command = [ - self.python_executable, - os.path.join(self.repo_dir, "run.py"), - image_path, - "--mc-resolution", - f"{mc_resolution}", - "--output-dir", - self.final_out_dir, - ] - process = await asyncio.create_subprocess_exec(*command) - await process.wait() - output_path = self.get_output_path(image_path) - return output_path - - async def clone_repository(self): - import asyncio - # Clone the TripoSR repository - command = ["git", "clone", self.repo_url, self.repo_dir] - process = await asyncio.create_subprocess_exec(*command) - await process.wait() - - async def create_venv_and_install_requirements(self): - import asyncio - import os - # Create a virtual environment - command = ["python", "-m", "venv", self.venv_dir] - # use os to execute the command - os.system(" ".join(command)) - - # install pip "python -m ensurepip" - command = [self.python_executable, "-m", "ensurepip"] - os.system(" ".join(command)) - - # install wheel - command = [self.python_executable, "-m", "pip", "install", "wheel"] - os.system(" ".join(command)) - - # install torch - # TODO: detect what version of torch to install based on what is installed on system - command = [self.python_executable, "-m", "pip", "install", "torch", - "--index-url https://download.pytorch.org/whl/cu121"] - - #command = [self.python_executable, "-m", "pip", "install", "torch", - # "--index-url https://download.pytorch.org/whl/cu118"] - - os.system(" ".join(command)) - - # Install requirements - command = [self.python_executable, "-m", "pip", "install", "-r", - os.path.join(self.repo_dir, "requirements.txt")] - os.system(" ".join(command)) - - def get_output_path(self, image_path): - import os - # Extract the base name of the input image file - base_name = os.path.splitext(os.path.basename(image_path))[0] - # Construct the output path based on the base name and output directory - output_path = os.path.join(self.output_dir, base_name, "obj", "mesh_norm.obj") - return output_path diff --git a/AVF/__init__.py b/AVF/__init__.py deleted file mode 100644 index af758f9..0000000 --- a/AVF/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .. import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS -from .audio_nodes import * -from .video_nodes import * -from .common import run_ffmpeg_command - -__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "run_ffmpeg_command"] \ No newline at end of file diff --git a/AVF/audio_nodes.py b/AVF/audio_nodes.py deleted file mode 100644 index ad9c3c4..0000000 --- a/AVF/audio_nodes.py +++ /dev/null @@ -1,342 +0,0 @@ -# audio_module.py -from .common import ETK_AVF_base, duplicate_file, run_ffmpeg_command - - -@ETK_AVF_base -class AudioFolderProvider: - """ - Given a folder path, creates the folder or empties it entirely returns the folder path as - a string but as the node type "AUDIOFOLDER" - """ - - @classmethod - def INPUT_TYPES(cls): - ret = { - "required": { - "folder_path": ("STRING", {"default": ""}), - "clear_folder": ("BOOLEAN", {"default": True}), - }, - "optional": { - "activator:": ("*", {"default": None}) - } - } - - return ret - - RETURN_TYPES = ("AUDIOFOLDER",) - FUNCTION = "audio_folder_provider" - - def audio_folder_provider(self, **kwargs): - import shutil - import os - - # check if the folder_path is none - if kwargs["folder_path"] is None: - raise ValueError("folder_path must be specified") - - # set folder exists flag by using the os.path.exists function - folder_exists = os.path.exists(kwargs["folder_path"]) - - if not folder_exists: - os.makedirs(kwargs["folder_path"]) - folder_exists = True - - # set files in folder flag by using the os.listdir function - files_in_folder = os.listdir(kwargs["folder_path"]) - - # set accessible flag by using the os.access function - accessible = os.access(kwargs["folder_path"], os.W_OK) - if not accessible: - raise ValueError("Folder for audio files is not accessible") - - 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") - - return (kwargs["folder_path"],) - - -@ETK_AVF_base -class DictToComfyAudio: - """ - Given a dictionary, creates a comfy audio node - """ - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"dict": ("DICT", {"default": {}})}} - return ret - - RETURN_TYPES = ("AUDIO",) - FUNCTION = "dict_to_comfy_audio" - - def dict_to_comfy_audio(self, **kwargs): - return (kwargs["dict"],) - - -@ETK_AVF_base -class LoadAudioFromServer: - """ - Given a path on the server loads the audio file from the server and returns it as an AUDIO node - """ - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"server_path": ("STRING", {"default": ""})}} - return ret - - RETURN_TYPES = ("AUDIO",) - FUNCTION = "load_audio_from_server" - - def load_audio_from_server(self, **kwargs): - file_on_server = kwargs["server_path"] - from comfy_extras.nodes_audio import LoadAudio - f = LoadAudio().load - r = f(file_on_server)[0] - - return (r,) - - -@ETK_AVF_base -class AudioDefinitionProvider: - """ - Given a AUDIOFOLDER node, and inputs for sample rate, and channels, creates a AUDIOFOLDERDEF variable - """ - - @classmethod - def INPUT_TYPES(cls): - ret = { - "required": { - "audio_folder": ("AUDIOFOLDER", {"default": None}), - "sample rate": ("INT", {"default": 44100}), - "channels": ("INT", {"default": 2}), - "audio format": (["WAV", "MP3"], {"default": "WAV"}), - }, - } - return ret - - RETURN_TYPES = ("AUDIO_FOLDER_DEF",) - FUNCTION = "audio_definition_provider" - - def get_num_audio_files_that_match(self, audio_folder): - audio_files = self.get_all_audio_files(audio_folder) - return len(audio_files) - - def get_all_audio_files(self, audio_folder): - import os - audio_files = os.listdir(audio_folder) - audio_files = sorted([os.path.join(audio_folder, x) for x in audio_files if "audio_" in x]) - return audio_files - - def audio_definition_provider(self, **kwargs): - from addict import Dict as adict - import os - ret = adict() - ret["audio_folder"] = kwargs["audio_folder"] - ret["sample rate"] = kwargs.get("sample rate") or kwargs.get("sample_rate") - ret["audio format"] = kwargs.get("audio format") or kwargs.get("audio_format") - ret["channels"] = kwargs.get("channels") - # a function that gets the frame count from the disk - ret.get_file_count = lambda: self.get_num_audio_files_that_match(ret["audio_folder"]) - # a function that gets the next file name and full path dont forget to zfill - ret.get_next_file_name = lambda: f"{ret['audio_folder']}{os.sep}audio_{str(ret.get_file_count()).zfill(10)}.{ret['audio format'].lower()}" - ret.get_last_file_name = lambda: f"{ret['audio_folder']}{os.sep}audio_{str(ret.get_file_count() - 1).zfill(10)}.{ret['audio format'].lower()}" - ret.get_all_audio_files = lambda: self.get_all_audio_files(ret["audio_folder"]) - - return (ret,) - - -@ETK_AVF_base -class AddAudioToAudioFolder: - """ - Given a AUDIO_FOLDER_DEF node, and a list of audio files, adds the audio files to the audio folder - """ - - @classmethod - def INPUT_TYPES(cls): - ret = { - "required": { - "audio_folder_def": ("AUDIO_FOLDER_DEF", {"default": None}), - "AUDIO": ("LIST", {"default": None}), - "delete after": ("BOOLEAN", {"default": False}), - }, - } - return ret - - RETURN_TYPES = ("AUDIO_FOLDER_DEF",) - FUNCTION = "add_audio_to_audio_folder" - - def add_audio_to_audio_folder(self, **kwargs): - import os - - audio_folder_def = kwargs["audio_folder_def"] - audio_files = kwargs["AUDIO"] - delete = kwargs["delete after"] - - for audio_file in audio_files: - # use ffmpeg_exe to convert the audio file to the correct format which is audio_folder_def["audio format"] - # use the audio_folder_def["audio format"] to determine the output file name - # use the audio_folder_def["audio folder"] to determine the output folder - # use the audio_folder_def["sample rate"] to determine the sample rate - # use the audio_folder_def["channels"] to determine the number of channels - - # get the output file name - output_file_name = audio_folder_def.get_next_file_name() - input_file_name = audio_file - - CMD_AS_LIST = [ - "-y", - "-i", - input_file_name, - "-ar", - str(audio_folder_def['sample rate']), - "-ac", - str(audio_folder_def['channels']), - output_file_name - ] - - run_ffmpeg_command(CMD_AS_LIST) - if delete: - os.remove(input_file_name) - - return (audio_folder_def,) - - -@ETK_AVF_base -class AddSilenceToAudioFolder: - """Create and add a new audio file that is silence for the specified duration""" - - @classmethod - def INPUT_TYPES(cls): - ret = { - "required": { - "audio_folder_def": ("AUDIO_FOLDER_DEF", {"default": None}), - "duration": ("FLOAT", {"default": 1.0}), - }, - } - return ret - - RETURN_TYPES = ("AUDIO_FOLDER_DEF",) - FUNCTION = "add_silence_to_audio_folder" - - def add_silence_to_audio_folder(self, **kwargs): - audio_folder_def = kwargs["audio_folder_def"] - duration = kwargs["duration"] - - # get the output file name - output_file_name = audio_folder_def.get_next_file_name() - sample_rate = audio_folder_def["sample rate"] - channels = audio_folder_def["channels"] - fmt = audio_folder_def["audio format"] - - # create the silence audio file - - if fmt == "WAV": - fmt_part = "pcm_s16le" - elif fmt == "MP3": - fmt_part = "mp3" - else: - fmt_part = "pcm_s16le" - - # Create the FFmpeg command for generating silence - CMD_AS_LIST = [ - "-f", - "lavfi", - "-i", - f"aevalsrc=0:sample_rate={sample_rate}:channel_layout={channels},atrim=0:{duration}", - "-c:a", - fmt_part, - output_file_name - ] - - run_ffmpeg_command(CMD_AS_LIST) - - return (audio_folder_def,) - - -@ETK_AVF_base -class GetAudioFilesLengthsFromAudioFolder: - """Get the lengths of all audio files in a given audio folder""" - - @classmethod - def INPUT_TYPES(cls): - return {"required": {"audio_folder_def": ("AUDIO_FOLDER_DEF", {"default": None})}} - - RETURN_TYPES = ("AUDIO_FOLDER_DEF", "LIST",) - RETURN_NAMES = ("audio_folder_def", "[lens(seconds)]",) - FUNCTION = "get_audio_files_lengths" - - def get_audio_files_lengths(self, audio_folder_def): - import os - import torchaudio - - audio_folder = audio_folder_def["audio_folder"] - audio_files = os.listdir(audio_folder) - audio_files.sort() - audio_files_lengths = [] - - for audio_file in audio_files: - if audio_file[-3:].lower() not in ["wav", "mp3"]: - continue - audio_path = os.path.join(audio_folder, audio_file) - waveform, sample_rate = torchaudio.load(audio_path) - duration = waveform.shape[1] / sample_rate - audio_files_lengths.append(duration) - - return (audio_folder_def, audio_files_lengths,) - - -@ETK_AVF_base -class AudioFolderRenderAudio: - """Joins all the audio files""" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "audio_folder_def": ("AUDIO_FOLDER_DEF", {"default": None}), - "output_path": ("STRING", {"multiline": False, "default": ""}), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("output_path",) - FUNCTION = "join_audio_files" - CATEGORY = "ETK/audio" - - def join_audio_files(self, **kwargs): - import librosa - import copy - import soundfile as sf - import numpy as np - - audio_folder_def = kwargs['audio_folder_def'] - kwargs = copy.deepcopy(kwargs) - paths = audio_folder_def.get_all_audio_files() - 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) - - # TODO: need to implement writing in the correct format - # Save the concatenated audio - sf.write(output_path, joined_audio_data, sr) - - return (output_path,) \ No newline at end of file diff --git a/AVF/common.py b/AVF/common.py deleted file mode 100644 index 37c43ab..0000000 --- a/AVF/common.py +++ /dev/null @@ -1,45 +0,0 @@ -# common.py -from .. import NODE_CLASS_MAPPINGS -from .. import NODE_DISPLAY_NAME_MAPPINGS - - -def ETK_AVF_base(cls): - cls.CATEGORY = "ETK/AVF" - # Add spaces to the camel case class name - pretty_name = cls.__name__ - for i in range(1, len(pretty_name)): - if pretty_name[i].isupper(): - pretty_name = pretty_name[:i] + " " + pretty_name[i:] - cls.DISPLAY_NAME = pretty_name - NODE_CLASS_MAPPINGS[cls.__name__] = cls - NODE_DISPLAY_NAME_MAPPINGS[cls.DISPLAY_NAME] = pretty_name - - return cls - - -import threading - - -def duplicate_file(src_file, dest_files): - import shutil - threads = [] - for dest_file in dest_files: - thread = threading.Thread(target=shutil.copy2, args=(src_file, dest_file)) - thread.start() - threads.append(thread) - for thread in threads: - thread.join() - -def run_ffmpeg_command(args): - """ - Executes a command using the ffmpeg executable. - - Parameters: - args (list): List of command-line arguments for ffmpeg. - """ - import subprocess - import imageio_ffmpeg as ffmpeg - if isinstance(args, str): - args = args.split() - cmd = [ffmpeg.get_ffmpeg_exe()] + args - subprocess.run(cmd) \ No newline at end of file diff --git a/AVF/video_nodes.py b/AVF/video_nodes.py deleted file mode 100644 index 7b607e4..0000000 --- a/AVF/video_nodes.py +++ /dev/null @@ -1,695 +0,0 @@ -# video_module.py -from .common import ETK_AVF_base, duplicate_file, run_ffmpeg_command - - -@ETK_AVF_base -class VideoFolderFFMPEGVideoInterpolate: - """ - Use run_ffmpeg to interpolate the frames in the video folder - """ - - @classmethod - def INPUT_TYPES(cls): - ret = { - "required": { - "video_folder_def": ("VIDEO_FOLDER_DEF", {"default": None}), - "last_n_frames": ("INT", {"default": 2}), - "target_n_frames": ("INT", {"default": 3}), - }, - } - return ret - - RETURN_TYPES = ("VIDEO_FOLDER_DEF",) - RETURN_NAMES = ("video_folder_def",) - FUNCTION = "video_folder_ffmpeg_video_interpolate" - - def video_folder_ffmpeg_video_interpolate(self, **kwargs): - import os - import shutil - - video_folder_def = kwargs["video_folder_def"] - last_n_frames = kwargs["last_n_frames"] - target_n_frames = kwargs["target_n_frames"] - - # Get the current frame count - current_frame_count = video_folder_def.get_frame_count() - - # Calculate the target frame rate for interpolation - target_fps = video_folder_def["fps"] * (target_n_frames / last_n_frames) - source_fps = video_folder_def["fps"] - - # Get the file names for the last n frames - last_frame_names = [video_folder_def.get_last_file_name(offset=i) for i in range(last_n_frames)] - last_frame_names = list(set(last_frame_names)) - last_frame_names.sort() - - # Create a temporary folder off of the video frame folder's base path - temp_folder = os.path.join(video_folder_def["video_folder"], "temp") - os.makedirs(temp_folder, exist_ok=True) - - # Move the input frames into the new folder - for frame_name in last_frame_names: - # Do this in a way that overwrites the files and MOVES them - shutil.move(frame_name, os.path.join(temp_folder, os.path.basename(frame_name))) - - # New input frame names list - last_frame_names = [os.path.join(temp_folder, os.path.basename(frame_name)) for frame_name in last_frame_names] - last_frame_names.sort() - - # Create a file with the list of input frame paths in the temporary folder - input_frame_list_file = os.path.join(temp_folder, "input_frames.txt") - input_frame_list_file = os.path.abspath(input_frame_list_file) - with open(input_frame_list_file, 'w') as file: - # Need to do this in a manner that puts the first frame at the top of the file. - file.write('\n'.join([f"file '{frame_name}'" for frame_name in last_frame_names])) - - # Get the output frame names for the interpolated frames - output_frame_names = [video_folder_def.get_next_file_name(i) for i in range(target_n_frames)] - - temp_output_frame_pattern = os.path.join(video_folder_def["video_folder"], "temp", - "tmp_frame_%010d." + video_folder_def["frame format"].lower()) - temp_output_frame_pattern = os.path.abspath(temp_output_frame_pattern) - - # Create the FFmpeg command for motion interpolation using the frame list file - ffmpeg_interpolate_cmd = [ - "-f", "concat", "-safe", "0", - "-r", str(source_fps), - "-i", input_frame_list_file, - "-vsync", "1", - "-filter_complex", - f"[0:v]setpts=N/TB,minterpolate=fps={target_fps}:mi_mode=blend,setpts=PTS-STARTPTS[v]", # Reset timestamps - "-map", "[v]", "-q:v", "2", - "-r", str(target_fps), - "-frames:v", str(target_n_frames), - "-threads", "0", - "-y", temp_output_frame_pattern - ] - - run_ffmpeg_command(ffmpeg_interpolate_cmd) - - # Move the interpolated frames to the video folder with the correct names - for i, frame_name in enumerate(output_frame_names): - temp_frame_name = os.path.join(temp_folder, - f"tmp_frame_{i + 1:010d}.{video_folder_def['frame format'].lower()}") - try: - shutil.move(temp_frame_name, frame_name) - except FileNotFoundError: - pass - - # Remove the temporary folder and frame list file - os.remove(input_frame_list_file) - # Remove this without generating an error even if it is not empty - try: - os.rmdir(temp_folder) - except OSError: - shutil.rmtree(temp_folder) - - return (video_folder_def,) - - -@ETK_AVF_base -class VideoDefinitionProvider: - """ - Given a VIDEOFOLDER node, and inputs for height, width, and fps, creates a VIDEOFOLDERDEF variable - """ - - @classmethod - def INPUT_TYPES(cls): - ret = { - "required": { - "video_folder": ("VIDEOFOLDER", {"default": None}), - "height": ("INT", {"default": 1080}), - "width": ("INT", {"default": 1920}), - "fps": ("FLOAT", {"default": 30}), - "frame format": (["JPG", "PNG"], {"default": "JPG"}), - }, - } - return ret - - RETURN_TYPES = ("VIDEO_FOLDER_DEF",) - FUNCTION = "video_definition_provider" - - def video_definition_provider(self, **kwargs): - from addict import Dict as adict - import os - - ret = adict() - ret["video_folder"] = kwargs["video_folder"] - ret["height"] = kwargs["height"] - ret["width"] = kwargs["width"] - ret["fps"] = kwargs["fps"] - ret["frame format"] = kwargs.get("frame format") or kwargs.get("frame_format") - # A function that gets the frame count from the disk - ret.get_frame_count = lambda: len([f for f in os.listdir(ret["video_folder"]) if "frame_" in f]) - # A function that gets the next file name and full path, don't forget to add 10 zeros to the frame count - ret.get_next_file_name = lambda x=0: os.path.join( - ret['video_folder'], - f"frame_{str(ret.get_frame_count() + x).zfill(10)}.{ret['frame format'].lower()}" - ) - ret.get_last_file_name = lambda offset=0: os.path.join( - ret['video_folder'], - f"frame_{str(max(ret.get_frame_count() - offset - 1, 0)).zfill(10)}.{ret['frame format'].lower()}" - ) - ret.get_current_file_name = lambda: os.path.join( - ret['video_folder'], - f"frame_{str(ret.get_frame_count() - 1).zfill(10)}.{ret['frame format'].lower()}" - ) - - def reserve_next_frame(): - fn = ret.get_next_file_name() - open(fn, "w").close() - return fn - - ret.reserve_next_frame = reserve_next_frame - - return (ret,) - - -@ETK_AVF_base -class VideoFolderProvider: - """ - Given a folder path, creates the folder or empties it entirely returns the folder path as - a string but as the node type "VIDEOFOLDER" - """ - - @classmethod - def INPUT_TYPES(cls): - ret = { - "required": { - "folder_path": ("STRING", {"default": ""}), - "clear_folder": ("BOOLEAN", {"default": True}), - }, - "optional": { - "activator:": ("*", {"default": None}) - } - } - - return ret - - RETURN_TYPES = ("VIDEOFOLDER",) - FUNCTION = "video_folder_provider" - - def video_folder_provider(self, **kwargs): - import shutil - import os - - # Check if the folder_path is none - if kwargs["folder_path"] is None: - raise ValueError("folder_path must be specified") - - # Set folder exists flag by using the os.path.exists function - folder_exists = os.path.exists(kwargs["folder_path"]) - - if not folder_exists: - os.makedirs(kwargs["folder_path"]) - folder_exists = True - - # Set files in folder flag by using the os.listdir function - files_in_folder = os.listdir(kwargs["folder_path"]) - - # Set accessible flag by using the os.access function - accessible = os.access(kwargs["folder_path"], os.W_OK) - if not accessible: - raise ValueError("Folder for video files is not accessible") - - 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") - - return (kwargs["folder_path"],) - - -@ETK_AVF_base -class AddFramesToVideoFolder: - """ - Given a VIDEO_FOLDER_DEF node, and a list of frames, adds the frames to the video folder - """ - - @classmethod - def INPUT_TYPES(cls): - ret = { - "required": { - "video_folder_def": ("VIDEO_FOLDER_DEF", {"default": None}), - "IMAGE": ("IMAGE", {"default": None}), - "Rescale method": ( - ["NEAREST", "BOX", "BILINEAR", "HAMMING", "BICUBIC", "LANCZOS"], - {"default": "LANCZOS"}), - "Release vram?": ("BOOLEAN", {"default": True}), - }, - } - return ret - - RETURN_TYPES = ("VIDEO_FOLDER_DEF",) - FUNCTION = "add_frames_to_video_folder" - - @staticmethod - def save_or_copy_frame(current_frame, previous_frame, frame_fp, previous_frame_fp): - import shutil - from PIL import Image - import torch - - if previous_frame is None or not torch.equal(current_frame, previous_frame): - # If the current frame is different from the previous frame, save it to disk - current_frame = current_frame - # expected shape is H, W, C uint8 0-255 - # image.fromarray expects uint8 0-255 and shape H, W, C - - needs_dtype_conversion = current_frame.dtype != torch.uint8 - - if len(current_frame.shape) == 3 and current_frame.dtype == torch.uint8: - needs_dtype_conversion = False - elif len(current_frame.shape) == 2: - # assume grayscale H,W - current_frame = current_frame.unsqueeze(2).repeat(1, 1, 3) - elif len(current_frame.shape) == 4 and current_frame.shape[0] == 1: - # assume B, H, W, C - current_frame = current_frame.squeeze(0) - elif len(current_frame.shape) == 3 and current_frame.shape[-1] == 4: - # assume H, W, C with alpha - current_frame = current_frame[..., :3] - else: # try to move forward - pass - # assume H, W, C - - if needs_dtype_conversion: - current_frame = current_frame.mul(255).byte() - - ndarr = current_frame.cpu().numpy() - im = Image.fromarray(ndarr) - im.save(frame_fp) - - previous_frame = current_frame - else: - # If the current frame is the same as the previous frame, copy the previously saved frame file - shutil.copy2(previous_frame_fp, frame_fp) - return previous_frame - - @staticmethod - def process_frames(frames, rescale_method, video_folder_def): - import gc - import torch - num_frames = frames.shape[0] - previous_frame = None - frame_fp = None - frames_are_all_same = torch.all(frames[0] == frames) - if frames_are_all_same and frames.shape[0] > 1: - # Get a list of file names using reserve_next_frame - frame_f_names = [video_folder_def.reserve_next_frame() for _ in range(num_frames)] - # Save the first frame - AddFramesToVideoFolder.save_or_copy_frame(frames[0], previous_frame, frame_f_names[0], None) - # Copy the first frame to the rest of the frames using os by calling the cli - duplicate_file(frame_f_names[0], frame_f_names[1:]) - - else: - for i in range(num_frames): - next_fn = video_folder_def.get_next_file_name() - - previous_frame_fp = frame_fp - frame_fp = next_fn - - current_frame = frames[i] - - previous_frame = AddFramesToVideoFolder.save_or_copy_frame(current_frame, previous_frame, frame_fp, - previous_frame_fp) - - del frames - gc.collect() - return None - - def add_frames_to_video_folder(self, **kwargs): - from PIL import Image - import torch - 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") - - # Get the video folder path - video_folder = kwargs["video_folder_def"]["video_folder"] - - # Get the frame format - frame_format = kwargs["video_folder_def"]["frame format"] - - # Get the height - height = kwargs["video_folder_def"]["height"] - - # Get the width - width = kwargs["video_folder_def"]["width"] - - # Get the fps - fps = kwargs["video_folder_def"]["fps"] - - # Get the frames - frames = kwargs["IMAGE"] - - # Get the rescale method - rescale_method = kwargs.get("Rescale method", None) - if not rescale_method: - rescale_method = kwargs.get("Rescale_method", None) - - free_vram = kwargs.get("Release vram?", None) - if not free_vram: - free_vram = kwargs.get("Release_vram_", True) - - orig_frames = frames - # num_files = len(glob(os.path.join(video_folder, f"frame_*.{frame_format.lower()}"))) - - # Rescale if needed to the correct size - if height != frames.shape[1] or width != frames.shape[2]: - # First since transforms.Resize requires a tensor of shape B,C, H, W - # We need to permute the tensor to B, C, H, W - frames = frames.permute(0, 3, 1, 2) - frames = frames.to("cuda") - - # Get the right value for interpolation - interpolation_methods = { - "NEAREST": Image.Resampling.NEAREST, - "BOX": Image.Resampling.BOX, - "BILINEAR": Image.Resampling.BILINEAR, - "HAMMING": Image.Resampling.HAMMING, - "BICUBIC": Image.Resampling.BICUBIC, - "LANCZOS": Image.Resampling.LANCZOS - } - - rescale_type = interpolation_methods[rescale_method] - # rescale_method = transforms.Resize((height, width), interpolation=rescale_type) # this doesnt work so well lets use a different function - - if rescale_type != Image.Resampling.LANCZOS: - rescale_method_fn = lambda x: resize(x, (height, width), interpolation=rescale_type) - frames = rescale_method_fn(frames) - else: - rescale_method_fn = lambda x: lanczos_resize(x, (width, height)) - frames = rescale_method_fn(frames) - - # Now we need to permute back to B, H, W, C - frames = frames.permute(0, 2, 3, 1) - self.process_frames(frames, rescale_method_fn, kwargs["video_folder_def"]) - else: - self.process_frames(frames, rescale_method, kwargs["video_folder_def"]) - - gc.collect() - - if free_vram: - import gc - if isinstance(frames, torch.Tensor): - frames.to("cpu").detach().numpy() - del frames - - if isinstance(orig_frames, torch.Tensor): - orig_frames = orig_frames.to("cpu").detach().numpy() - del orig_frames - - 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") - # Return the VIDEO_FOLDER_DEF - return (kwargs["video_folder_def"],) - - -@ETK_AVF_base -class HoldFrameForSecsInVideoFolder: - """ - Given a VIDEO_FOLDER_DEF node, and a list of frames, first uses RepeatFrames to repeat the frames - for the correct amount and then adds the frames to the video folder using AddFramesToVideoFolder - """ - - @classmethod - def INPUT_TYPES(cls): - ret = { - "required": { - "video_folder_def": ("VIDEO_FOLDER_DEF", {"default": None}), - "IMAGE": ("IMAGE", {"default": None}), - "seconds": ("FLOAT", {"default": 1.0}), - "Rescale method": ( - ["NEAREST", "BOX", "BILINEAR", "HAMMING", "BICUBIC", "LANCZOS"], - {"default": "LANCZOS"}), - "Release vram?": ("BOOLEAN", {"default": True}), - }, - } - return ret - - RETURN_TYPES = ("VIDEO_FOLDER_DEF",) - FUNCTION = "hold_frame_for_secs_in_video_folder" - - def saver(self, x, kwargs): - # This just saves in an async way - rescale_method = kwargs.get("Rescale method", None) - if rescale_method is None: - rescale_method = kwargs.get("Rescale_method", None) - - rel_vram = kwargs.get("Release vram?", None) - if rel_vram is None: - rel_vram = kwargs.get("Release_vram_", True) - - AddFramesToVideoFolder().add_frames_to_video_folder( - video_folder_def=kwargs["video_folder_def"], - IMAGE=x, - Rescale_method=rescale_method, - Release_vram_=rel_vram - ) - - def hold_frame_for_secs_in_video_folder(self, **kwargs): - video_folder_ref = kwargs["video_folder_def"] - - video_folder = kwargs["video_folder_def"]["video_folder"] - - # Get the frame format - frame_format = kwargs["video_folder_def"]["frame format"] - - # Get the height - height = kwargs["video_folder_def"]["height"] - - # Get the width - width = kwargs["video_folder_def"]["width"] - - # Get the fps - fps = kwargs["video_folder_def"]["fps"] - - # Get the frames - frames = kwargs["IMAGE"] - - # Get the number of frames - num_frames = frames.shape[0] - - # Get the rescale method - rescale_method = kwargs.get("Rescale method", None) - if rescale_method is None: - rescale_method = kwargs.get("Rescale_method", None) - - # Get the number of seconds - seconds = kwargs["seconds"] - - # Get the number of frames to hold - num_frames_to_hold = int(fps * seconds) - - num_frames_in_this_stack = frames.shape[0] - - # Get the number of repeats needed - num_repeats_needed = num_frames_to_hold // num_frames_in_this_stack - - # Get the number of frames left over - num_frames_left_over = num_frames_to_hold % num_frames_in_this_stack - - # Write the first set of frames to disk - self.saver(frames, kwargs) - - # Get the name of the last frame - source_last_frame_name = video_folder_ref.get_last_file_name() - # From the last frame find the rest of the frames names/paths - source_last_frame_num = int(source_last_frame_name.split("_")[-1].split(".")[0]) - source_first_frame_num = source_last_frame_num - num_frames_in_this_stack + 1 - - template = source_last_frame_name.replace(str(source_last_frame_num).zfill(10), "{}") - - range_of_source_frame_numbers = list(range(source_first_frame_num, source_last_frame_num + 1)) - - source_frame_names = [template.format(str(x).zfill(10)) for x in range_of_source_frame_numbers] - - target_done_frame_num = source_last_frame_num + num_frames_to_hold - num_frames_in_this_stack - num_frames_remaining = num_frames_to_hold - num_frames_in_this_stack - target_start_frame_num = source_last_frame_num + 1 - - target_frame_range = list(range(target_start_frame_num, target_done_frame_num + 1)) - - target_frame_names = [template.format(str(x).zfill(10)) for x in target_frame_range] - - set_todo = {fn: [] for fn in source_frame_names} - # We can now repeat the frames - for i, target_frame_name in enumerate(target_frame_names): - # Keep track of what stack frame we are on - stack_frame_idx = i % num_frames_in_this_stack - stack_frame_name_to_use = source_frame_names[stack_frame_idx] - set_todo[stack_frame_name_to_use].append(target_frame_name) - - # Now we can repeat the frames - for frame_name in set_todo: - src_frame = frame_name - target_frames = set_todo[src_frame] - duplicate_file(src_frame, target_frames) - - return (kwargs["video_folder_def"],) - - -@ETK_AVF_base -class Listed_HoldFramesForSecsInVideoFolder: - """This just calls HoldFrameForSecsInVideoFolder multiple times for a list of frames and a list of durations""" - - @classmethod - def INPUT_TYPES(cls): - ret = { - "required": { - "video_folder_def": ("VIDEO_FOLDER_DEF", {"default": None}), - "IMAGE": ("LIST", {"default": None}), - "seconds": ("LIST", {"default": None}), - "Rescale method": ( - ["NEAREST", "BOX", "BILINEAR", "HAMMING", "BICUBIC", "LANCZOS"], - {"default": "LANCZOS"}), - "Release vram?": ("BOOLEAN", {"default": True}), - }, - } - return ret - - RETURN_TYPES = ("VIDEO_FOLDER_DEF",) - FUNCTION = "listed_hold_frames_for_secs_in_video_folder" - - def listed_hold_frames_for_secs_in_video_folder(self, **kwargs): - # 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"], - IMAGE=frame, - seconds=duration, - Rescale_method=kwargs["Rescale method"], - Release_vram_=kwargs["Release vram?"]) - - return (kwargs["video_folder_def"],) - - -@ETK_AVF_base -class VideoFolderDefInfo: - """ - Returns everything in the VIDEO_FOLDER_DEF, and also returns information about the status of the folder - """ - - @classmethod - def INPUT_TYPES(cls): - ret = { - "required": { - "video_folder_def": ("VIDEO_FOLDER_DEF", {"default": None}), - }, - } - return ret - - RETURN_TYPES = ("VIDEO_FOLDER_DEF", "STRING", "INT", "INT", "FLOAT", "STRING", "INT", "LIST",) - RETURN_NAMES = ("VFD", "video_folder", "height", "width", "fps", "frame format", "num_files", "list of files") - - OUTPUT_NODE = True - FUNCTION = "video_folder_def_info" - - def video_folder_def_info(self, **kwargs): - import os - - video_folder = kwargs["video_folder_def"]["video_folder"] - height = kwargs["video_folder_def"]["height"] - width = kwargs["video_folder_def"]["width"] - fps = kwargs["video_folder_def"]["fps"] - frame_format = kwargs["video_folder_def"]["frame format"] - files = os.listdir(video_folder) - num_files = len(files) - - return (kwargs["video_folder_def"], video_folder, height, width, fps, frame_format, num_files, files,) - - -@ETK_AVF_base -class FadeToInVideoFolder: - """ - Given a VIDEO_FOLDER_DEF node, and a stack of frames, fades the frames in and adds them to the video folder - """ - - @classmethod - def INPUT_TYPES(cls): - ret = { - "required": { - "video_folder_def": ("VIDEO_FOLDER_DEF", {"default": None}), - "IMAGE": ("IMAGE", {"default": None}), - "seconds": ("FLOAT", {"default": 1.0}), - "transition ms": ("FLOAT", {"default": 0.1}), - "Rescale method": ( - ["NEAREST", "BOX", "BILINEAR", "HAMMING", "BICUBIC", "LANCZOS"], - {"default": "LANCZOS"}), - "Release vram?": ("BOOLEAN", {"default": True}), - }, - } - return ret - - RETURN_TYPES = ("VIDEO_FOLDER_DEF",) - FUNCTION = "fade_to_in_video_folder" - - def fade_to_in_video_folder(self, **kwargs): - from PIL import Image - import torch - - # Use HoldFrameForSecsInVideoFolder to hold the frame after creating and AddFramesToVideoFolder the transition - - transition_length_secs = int(kwargs["transition ms"] * kwargs["video_folder_def"]["fps"]) / 1000 - hold_frame_length_secs = int(kwargs["seconds"] * kwargs["video_folder_def"]["fps"]) - transition_length_secs - - transition_frame_count = int(kwargs["video_folder_def"]["fps"] * transition_length_secs) - - base_frame_fn = kwargs["video_folder_def"].get_last_file_name() - # Load it into a pytorch tensor (B, C, H, W) - base_frame = Image.open(base_frame_fn) - base_frame = torch.tensor(base_frame).permute(2, 0, 1).unsqueeze(0).to("cuda") - - transition_stack = torch.zeros( - (transition_frame_count, base_frame.shape[1], base_frame.shape[2], base_frame.shape[3]), - dtype=torch.float16, device="cuda" - ) - - # Fade from base_frame to the first frame in the stack over the transition length - new_frame = kwargs["IMAGE"][0].to("cuda") - for i in range(transition_frame_count): - alpha = i / transition_frame_count - interpolated_frame = (1 - alpha) * base_frame + alpha * new_frame - interpolated_frame = interpolated_frame.to("cpu") - transition_stack[i] = interpolated_frame - - # Add to the video folder - AddFramesToVideoFolder().add_frames_to_video_folder( - video_folder_def=kwargs["video_folder_def"], - IMAGE=transition_stack, - Rescale_method=kwargs["Rescale method"], - Release_vram_=kwargs["Release vram?"] - ) - - # Hold the frame - HoldFrameForSecsInVideoFolder().hold_frame_for_secs_in_video_folder( - video_folder_def=kwargs["video_folder_def"], - IMAGE=kwargs["IMAGE"], - seconds=hold_frame_length_secs, - Rescale_method=kwargs["Rescale method"], - Release_vram_=kwargs["Release vram?"] - ) - - return (kwargs["video_folder_def"],) diff --git a/__init__.py b/__init__.py index 7c62c07..84798d4 100644 --- a/__init__.py +++ b/__init__.py @@ -2,12 +2,8 @@ NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} WEB_DIRECTORY = "./web" -# In /custom_nodes/ETK/__init__.py - import sys from types import ModuleType -import os -import importlib.util def patch_validate_inputs(): @@ -46,32 +42,8 @@ def patch_validate_inputs(): # Apply the patch when this module is imported patch_validate_inputs() -UNIT_MODE = os.environ.get("UNIT_TEST") or importlib.util.find_spec('comfy') is None - -if not UNIT_MODE: - from . import AVF - - # Update NODE_CLASS_MAPPINGS with AVF nodes - NODE_CLASS_MAPPINGS.update(AVF.NODE_CLASS_MAPPINGS) - - # Update NODE_DISPLAY_NAME_MAPPINGS with AVF nodes - NODE_DISPLAY_NAME_MAPPINGS.update(AVF.NODE_DISPLAY_NAME_MAPPINGS) - - - from . import basic - from . import image - from . import video - from . import functional - from . import audio - from . import git - from . import server_endpoints - from . import torchvision_nodes - from . import utils - from . import pytorch_nodes - from . import youtube_nodes - from . import torchvision_nodes - from . import hf_diffusers_nodes - #from . import audio_video_folder +# Import PyTorch nodes to populate node mappings +from . import pytorch_nodes __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/audio.py b/audio.py deleted file mode 100644 index ae6e690..0000000 --- a/audio.py +++ /dev/null @@ -1,742 +0,0 @@ -import shutil -import struct -import io -import copy -import imageio_ffmpeg as ffmpeg -import joblib - - -from .config import config_settings - -temp_dir = config_settings["tmp_dir"] -from . import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS - - -# NODE_CLASS_MAPPINGS = {} -# NODE_DISPLAY_NAME_MAPPINGS = {} - -def create_vorbis_comment_block(comment_dict, last_block): - vendor_string = b'ComfyUI' - vendor_length = len(vendor_string) - - comments = [] - for key, value in comment_dict.items(): - comment = f"{key}={value}".encode('utf-8') - comments.append(struct.pack('I', len(comment_data))[1:] + comment_data - - return comment_block -# Create a memory object for caching -# memory = joblib.Memory(temp_dir, verbose=0) -def insert_or_replace_vorbis_comment(flac_io, comment_dict): - if len(comment_dict) == 0: - return flac_io - - flac_io.seek(4) - - blocks = [] - last_block = False - - while not last_block: - header = flac_io.read(4) - last_block = (header[0] & 0x80) != 0 - block_type = header[0] & 0x7F - block_length = struct.unpack('>I', b'\x00' + header[1:])[0] - block_data = flac_io.read(block_length) - - if block_type == 4 or block_type == 1: - pass - else: - header = bytes([(header[0] & (~0x80))]) + header[1:] - blocks.append(header + block_data) - - blocks.append(create_vorbis_comment_block(comment_dict, last_block=True)) - - new_flac_io = io.BytesIO() - new_flac_io.write(b'fLaC') - for block in blocks: - new_flac_io.write(block) - - new_flac_io.write(flac_io.read()) - return new_flac_io - -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: - if fpath[-4:] in [".wav", ".mp3", ".flac", ".ogg", ".m4a", ".aac", ".wma", ".aiff", ".alac", ".au", ".raw", - ".mp2", ]: - 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 - import tempfile - import uuid - from .config import this_file_path - - # Handle audio_folder_def - audio_folder_def = kwargs.get("AUDIO_FOLDER_DEF", None) - - # Get user-specified output path - base_output_path = kwargs.get('output_path', '.') - - # Generate a random temporary folder using tempfile.mkdtemp (persistent until explicitly removed) - temp_folder = tempfile.mkdtemp(dir=base_output_path) - - 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 = kwargs['[text]'] - else: - texts = [kwargs.get('text', "")] - - # Create a temporary file in the given folder with the speech separated by new lines - fpn = os.path.join(temp_folder, "temp.txt") - with open(fpn, "w") as f: - for text in texts: - f.write(text + "\n") - - # Create the command - command = [ - python_exe, xttscli_path, - "--file", fpn, - "--folder", temp_folder, - "--lang", kwargs.get('lang', 'en'), - "--speaker", kwargs.get('speaker_wav', ''), - "--device", kwargs.get('device', 'cuda:0'), - ] - - # Write the command to "temp_command.txt" in the same folder as this file is in - with open(os.path.join(this_file_path, "temp_command.txt"), "w") as f: - f.write(" ".join(command)) - - # Execute the command in its own full process and receive the output - try: - # Capture both stdout and stderr - output = subprocess.check_output(command, stderr=subprocess.STDOUT) - generated = output.decode("utf-8") - - if "Generated files: (" in generated: - # Extract generated files - generated = str(generated) - generated = generated.split(r"Generated files: (")[1] - generated_files = generated.split(")")[0] - generated_files = eval(generated_files) - - # Get the output folder - output_folder = os.path.dirname(kwargs.get('file', "output.wav")) - - # Move the files to the correct location - out_files = [] - for i, src_file in enumerate(generated_files): - if audio_folder_def: - target_file = audio_folder_def.get_next_file_name() - else: - target_file = os.path.join(temp_folder, f"output_{i}.wav") - shutil.copy(src_file, target_file) - out_files.append(target_file) - - else: - raise Exception(f"Error with xttscli, output was: {generated}") - - except subprocess.CalledProcessError as e: - # Output the captured stdout + stderr - print(f"Command failed with error: {e.output.decode('utf-8')}") - raise e - - # Return values in a consistent manner - return ( - out_files[0], - out_files, - temp_folder, - audio_folder_def if audio_folder_def else None - ) - - -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 EtkSaveAudio: - def __init__(self): - self.type = "output" - self.prefix_append = "" - - @classmethod - def INPUT_TYPES(s): - return {"required": {"audio": ("AUDIO",), - "filename": ("STRING", {"default": ""})}, - } - - RETURN_TYPES = ("STRING",) - FUNCTION = "save_audio" - - OUTPUT_NODE = True - - def save_audio(self, audio, filename=""): - import torchaudio - import io - - - results = list() - filename += self.prefix_append - - - for (batch_number, waveform) in enumerate(audio["waveform"].cpu()): - file = f"{filename}_{batch_number:05}.flac" - - buff = io.BytesIO() - torchaudio.save(buff, waveform, audio["sample_rate"], format="FLAC") - buff = insert_or_replace_vorbis_comment(buff, {}) - - with open(file, 'wb') as f: - f.write(buff.getbuffer()) - - results.append({ - "filename": file, - "type": self.type - }) - - return {"ui": {"audio": results}, "result": (filename,)} - - -@ETK_audio_base -class PreviewAudio(EtkSaveAudio): - - def __init__(self): - import folder_paths - self.output_dir = folder_paths.get_temp_directory() - self.type = "temp" - self.prefix_append = folder_paths.get_temp_directory() + "_temp_" + ''.join( - random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5)) - - @classmethod - def INPUT_TYPES(s): - return {"required": - {"audio": ("AUDIO",), }, - } - - -@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}') diff --git a/audio_video_folder.py b/audio_video_folder.py deleted file mode 100644 index 620155c..0000000 --- a/audio_video_folder.py +++ /dev/null @@ -1,1081 +0,0 @@ -from . import NODE_CLASS_MAPPINGS -from . import NODE_DISPLAY_NAME_MAPPINGS - - -def ETK_AVF_base(cls): - cls.CATEGORY = "ETK/AudioVideoFolders" - # Add spaces to the camel case class name - pretty_name = cls.__name__ - for i in range(1, len(pretty_name)): - if pretty_name[i].isupper(): - pretty_name = pretty_name[:i] + " " + pretty_name[i:] - cls.DISPLAY_NAME = pretty_name - NODE_CLASS_MAPPINGS[cls.__name__] = cls - NODE_DISPLAY_NAME_MAPPINGS[cls.DISPLAY_NAME] = pretty_name - - return cls - - -@ETK_AVF_base -class VideoFolderFFMPEGVideoInterpolate(): - """ - use run_ffmpeg to interpolate the frames in the video folder - """ - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"video_folder_def": ("VIDEO_FOLDER_DEF", {"default": None}), - "last_n_frames": ("INT", {"default": 2}), - "target_n_frames": ("INT", {"default": 3}), - }, - } - return ret - - RETURN_TYPES = ("VIDEO_FOLDER_DEF",) - RETURN_NAMES = ("video_folder_def",) - FUNCTION = "video_folder_ffmpeg_video_interpolate" - - def video_folder_ffmpeg_video_interpolate(self, **kwargs): - import os - import shutil - - video_folder_def = kwargs["video_folder_def"] - last_n_frames = kwargs["last_n_frames"] - target_n_frames = kwargs["target_n_frames"] - - # Get the current frame count - current_frame_count = video_folder_def.get_frame_count() - - # Calculate the target frame rate for interpolation - target_fps = video_folder_def["fps"] * (target_n_frames / last_n_frames) - - # Get the file names for the last n frames - last_frame_names = [video_folder_def.get_last_file_name(offset=i) for i in range(last_n_frames)] - last_frame_names = list(set(last_frame_names)) - last_frame_names.sort() - - # Create a temporary folder off of the video frame folder's base path - temp_folder = os.path.join(video_folder_def["video_folder"], "temp") - os.makedirs(temp_folder, exist_ok=True) - - # put the input frames into the new folder - for frame_name in last_frame_names: - # do this in a way that overwrites the files and MOVES them - shutil.move(frame_name, os.path.join(temp_folder, os.path.basename(frame_name))) - - # new input frame names list - last_frame_names = [os.path.join(temp_folder, os.path.basename(frame_name)) for frame_name in last_frame_names] - last_frame_names.sort() - - # Create a file with the list of input frame paths in the temporary folder - input_frame_list_file = os.path.join(temp_folder, "input_frames.txt") - input_frame_list_file = os.path.abspath(input_frame_list_file) - with open(input_frame_list_file, 'w') as file: - # need to do this in a manner that puts the first frame at the top of the file. - file.write('\n'.join([f"file '{frame_name}'" for frame_name in last_frame_names])) - - # Get the output frame names for the interpolated frames - # output_frame_names = [video_folder_def.reserve_next_frame() for _ in range(target_n_frames)] - output_frame_names = [video_folder_def.get_next_file_name(i) for i in range(target_n_frames)] - - # delete the frames on the list - # for frame_name in output_frame_names: - # os.remove(frame_name) - - temp_output_frame_pattern = os.path.join(video_folder_def["video_folder"], "temp", - "tmp_frame_%010d." + video_folder_def["frame format"].lower()) - temp_output_frame_pattern = os.path.abspath(temp_output_frame_pattern) - - # Create the FFmpeg command for motion interpolation using the frame list file - ffmpeg_interpolate_cmd = [ - "-f", "concat", "-safe", "0", - "-i", input_frame_list_file, - "-vsync", "0", "-filter_complex", - # f"[0:v]minterpolate=fps={target_fps}:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:me=ds:mb_size=16:search_param=8:vsbmc=1[v]", - # f"[0:v]framerate=fps={target_fps}:interp_start=0:interp_end=255:scene=0[v]", - # f"[0:v]minterpolate=fps={target_fps}:vsbmc=1[v]", - f"[0:v]minterpolate=fps={target_fps}:mi_mode=blend[v]", - "-map", "[v]", "-q:v", "2", "-r", str(target_fps), "-frames", str(target_n_frames), - "-threads", "0", - "-y", temp_output_frame_pattern - ] - - run_ffmpeg_command(ffmpeg_interpolate_cmd) - - # Move the interpolated frames to the video folder with the correct names - for i, frame_name in enumerate(output_frame_names): - temp_frame_name = os.path.join(temp_folder, - f"tmp_frame_{i + 1:010d}.{video_folder_def['frame format'].lower()}") - try: - shutil.move(temp_frame_name, frame_name) - except FileNotFoundError: - pass - - # Remove the temporary folder and frame list file - os.remove(input_frame_list_file) - # remove this without generating an error even if it is not empty - try: - os.rmdir(temp_folder) - except OSError: - shutil.rmtree(temp_folder) - - return (video_folder_def,) - - -@ETK_AVF_base -class VideoDefinitionProvider(): - """ - given a VIDEOFOLDER node, and inputs for height, width, and fps, creates a VIDEOFOLDERDEF variable - """ - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"video_folder": ("VIDEOFOLDER", {"default": None}), - "height": ("INT", {"default": 1080}), - "width": ("INT", {"default": 1920}), - "fps": ("FLOAT", {"default": 30}), - "frame format": (["JPG", "PNG"], {"default": "JPG"}), - }, - } - return ret - - RETURN_TYPES = ("VIDEO_FOLDER_DEF",) - FUNCTION = "video_definition_provider" - - def video_definition_provider(self, **kwargs): - from addict import Dict as adict - import os - - ret = adict() - ret["video_folder"] = kwargs["video_folder"] - ret["height"] = kwargs["height"] - ret["width"] = kwargs["width"] - ret["fps"] = kwargs["fps"] - ret["frame format"] = kwargs["frame format"] - # a function that gets the frame count from the disk - ret.get_frame_count = lambda: len([f for f in os.listdir(ret["video_folder"]) if "frame_" in f]) - # a function that gets the next file name and full path dont forget to add 10 zeros to the frame count - ret.get_next_file_name = lambda x=0: os.path.join(ret['video_folder'], - f"frame_{str(ret.get_frame_count() + x).zfill(10)}.{ret['frame format'].lower()}") - ret.get_last_file_name = lambda offset=0: os.path.join(ret['video_folder'], - f"frame_{str(max(ret.get_frame_count() - offset - 1, 0)).zfill(10)}.{ret['frame format'].lower()}") - ret.get_current_file_name = lambda: os.path.join(ret['video_folder'], - f"frame_{str(ret.get_frame_count() - 1).zfill(10)}.{ret['frame format'].lower()}") - - def reserve_next_frame(): - fn = ret.get_next_file_name() - open(fn, "w").close() - return fn - - ret.reserve_next_frame = reserve_next_frame - - return (ret,) - - -@ETK_AVF_base -class VideoFolderProvider(): - """ - given a folder path, creates the folder or empties it entirely returns the folder path as - a string but as the node type "VIDEOFOLDER" - """ - - @classmethod - def INPUT_TYPES(cls): - ret = { - "required": { - "folder_path": ("STRING", {"default": ""}), - "clear_folder": ("BOOLEAN", {"default": True}), - }, - "optional": { - "activator:": ("*", {"default": None}) - } - } - - return ret - - RETURN_TYPES = ("VIDEOFOLDER",) - FUNCTION = "video_folder_provider" - - def video_folder_provider(self, **kwargs): - import shutil - import os - - # check if the folder_path is none - if kwargs["folder_path"] is None: - raise ValueError("folder_path must be specified") - - # set folder exists flag by using the os.path.exists function - folder_exists = os.path.exists(kwargs["folder_path"]) - - if not folder_exists: - os.makedirs(kwargs["folder_path"]) - folder_exists = True - - # set files in folder flag by using the os.listdir function - files_in_folder = os.listdir(kwargs["folder_path"]) - - # set accessible flag by using the os.access function - accessible = os.access(kwargs["folder_path"], os.W_OK) - if not accessible: - raise ValueError("Folder for video files is not accessible") - - 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") - - return (kwargs["folder_path"],) - - -@ETK_AVF_base -class AudioFolderProvider(): - """ - given a folder path, creates the folder or empties it entirely returns the folder path as - a string but as the node type "AUDIOFOLDER" - """ - - @classmethod - def INPUT_TYPES(cls): - ret = { - "required": { - "folder_path": ("STRING", {"default": ""}), - "clear_folder": ("BOOLEAN", {"default": True}), - }, - "optional": { - "activator:": ("*", {"default": None}) - } - } - - return ret - - RETURN_TYPES = ("AUDIOFOLDER",) - FUNCTION = "audio_folder_provider" - - def audio_folder_provider(self, **kwargs): - import shutil - import os - - # check if the folder_path is none - if kwargs["folder_path"] is None: - raise ValueError("folder_path must be specified") - - # set folder exists flag by using the os.path.exists function - folder_exists = os.path.exists(kwargs["folder_path"]) - - if not folder_exists: - os.makedirs(kwargs["folder_path"]) - folder_exists = True - - # set files in folder flag by using the os.listdir function - files_in_folder = os.listdir(kwargs["folder_path"]) - - # set accessible flag by using the os.access function - accessible = os.access(kwargs["folder_path"], os.W_OK) - if not accessible: - raise ValueError("Folder for audio files is not accessible") - - 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") - - return (kwargs["folder_path"],) - - -@ETK_AVF_base -class DictToComfyAudio: - """ - given a dictionary, creates a comfy audio node - """ - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"dict": ("DICT", {"default": {}})}} - return ret - - RETURN_TYPES = ("AUDIO",) - FUNCTION = "dict_to_comfy_audio" - - def dict_to_comfy_audio(self, **kwargs): - return (kwargs["dict"],) - - -@ETK_AVF_base -class LoadAudioFromServer(): - """ - given a path on the server loads the audio file from the server and returns it as an AUDIO node - """ - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"server_path": ("STRING", {"default": ""})}} - return ret - - RETURN_TYPES = ("AUDIO",) - FUNCTION = "load_audio_from_server" - - def load_audio_from_server(self, **kwargs): - file_on_server = kwargs["server_path"] - from comfy_extras.nodes_audio import LoadAudio - f = LoadAudio().load - r = f(file_on_server)[0] - - return (r,) - - -@ETK_AVF_base -class AudioDefinitionProvider(): - """ - given a AUDIOFOLDER node, and inputs for sample rate, and channels, creates a AUDIOFOLDERDEF variable - """ - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"audio_folder": ("AUDIOFOLDER", {"default": None}), - "sample rate": ("INT", {"default": 44100}), - "channels": ("INT", {"default": 2}), - "audio format": (["WAV", "MP3"], {"default": "WAV"}), - }, - } - return ret - - RETURN_TYPES = ("AUDIO_FOLDER_DEF",) - FUNCTION = "audio_definition_provider" - - def get_num_audio_files_that_match(self, audio_folder): - audio_files = self.get_all_audio_files(audio_folder) - return len(audio_files) - - def get_all_audio_files(self, audio_folder): - import os - audio_files = os.listdir(audio_folder) - audio_files = sorted([os.path.join(audio_folder, x) for x in audio_files if "audio_" in x]) - return audio_files - - def audio_definition_provider(self, **kwargs): - from addict import Dict as adict - import os - ret = adict() - ret["audio_folder"] = kwargs["audio_folder"] - ret["sample rate"] = kwargs["sample rate"] - ret["channels"] = kwargs["channels"] - ret["audio format"] = kwargs["audio format"] - # a function that gets the frame count from the disk - ret.get_file_count = lambda: self.get_num_audio_files_that_match(ret["audio_folder"]) - # a function that gets the next file name and full path dont forget to zfill - ret.get_next_file_name = lambda: f"{ret['audio_folder']}{os.sep}audio_{str(ret.get_file_count()).zfill(10)}.{ret['audio format'].lower()}" - ret.get_last_file_name = lambda: f"{ret['audio_folder']}{os.sep}audio_{str(ret.get_file_count() - 1).zfill(10)}.{ret['audio format'].lower()}" - ret.get_all_audio_files = lambda: self.get_all_audio_files(ret["audio_folder"]) - - return (ret,) - - -@ETK_AVF_base -class AddAudioToAudioFolder(): - """ - given a AUDIO_FOLDER_DEF node, and a list of audio files, adds the audio files to the audio folder - """ - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"audio_folder_def": ("AUDIO_FOLDER_DEF", {"default": None}), - "AUDIO": ("LIST", {"default": None}), - "delete after": ("BOOLEAN", {"default": False}), - }, - } - return ret - - RETURN_TYPES = ("AUDIO_FOLDER_DEF",) - FUNCTION = "add_audio_to_audio_folder" - - def add_audio_to_audio_folder(self, **kwargs): - import os - - audio_folder_def = kwargs["audio_folder_def"] - audio_files = kwargs["AUDIO"] - delete = kwargs["delete after"] - - for audio_file in audio_files: - # use ffmpeg_exe to convert the audio file to the correct format which is audio_folder_def["audio format"] - # use the audio_folder_def["audio format"] to determine the output file name - # use the audio_folder_def["audio folder"] to determine the output folder - # use the audio_folder_def["sample rate"] to determine the sample rate - # use the audio_folder_def["channels"] to determine the number of channels - - # get the output file name - output_file_name = audio_folder_def.get_next_file_name() - input_file_name = audio_file - - CMD_AS_LIST = [ - "-y", "-i", input_file_name, "-ar", str(audio_folder_def['sample rate']), - "-ac", str(audio_folder_def['channels']), output_file_name - ] - - run_ffmpeg_command(CMD_AS_LIST) - if delete: - os.remove(input_file_name) - - return (audio_folder_def,) - - -@ETK_AVF_base -class AddSilenceToAudioFolder(): - """create and add a new audio file that is silence for the specified duration""" - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"audio_folder_def": ("AUDIO_FOLDER_DEF", {"default": None}), - "duration": ("FLOAT", {"default": 1.0}), - }, - } - return ret - - RETURN_TYPES = ("AUDIO_FOLDER_DEF",) - FUNCTION = "add_silence_to_audio_folder" - - def add_silence_to_audio_folder(self, **kwargs): - audio_folder_def = kwargs["audio_folder_def"] - duration = kwargs["duration"] - - # get the output file name - output_file_name = audio_folder_def.get_next_file_name() - sample_rate = audio_folder_def["sample rate"] - channels = audio_folder_def["channels"] - fmt = audio_folder_def["audio format"] - - # create the silence audio file - - if fmt == "WAV": - fmt_part = "pcm_s16le" - elif fmt == "MP3": - fmt_part = "mp3" - else: - fmt_part = "pcm_s16le" - - # Create the FFmpeg command for generating silence - CMD_AS_LIST = [ - "-f", "lavfi", "-i", f"aevalsrc=0:sample_rate={sample_rate}:channel_layout={channels},atrim=0:{duration}", - "-c:a", fmt_part, output_file_name - ] - - run_ffmpeg_command(CMD_AS_LIST) - - return (audio_folder_def,) - - -@ETK_AVF_base -class GetAudioFilesLengthsFromAudioFolder(): - """Get the lengths of all audio files in a given audio folder""" - - @classmethod - def INPUT_TYPES(cls): - return {"required": {"audio_folder_def": ("AUDIO_FOLDER_DEF", {"default": None})}} - - RETURN_TYPES = ("AUDIO_FOLDER_DEF", "LIST",) - RETURN_NAMES = ("audio_folder_def", "[lens(seconds)]",) - FUNCTION = "get_audio_files_lengths" - - def get_audio_files_lengths(self, audio_folder_def): - import os - import torchaudio - - audio_folder = audio_folder_def["audio_folder"] - audio_files = os.listdir(audio_folder) - audio_files_lengths = [] - - for audio_file in audio_files: - if audio_file[-3:] not in ["wav", "mp3"]: - continue - audio_path = os.path.join(audio_folder, audio_file) - waveform, sample_rate = torchaudio.load(audio_path) - duration = waveform.shape[1] / sample_rate - audio_files_lengths.append(duration) - - return (audio_folder_def, audio_files_lengths,) - - -@ETK_AVF_base -class AudioFolderRenderAudio(): - """ Joins all the audio files """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "audio_folder_def": ("AUDIO_FOLDER_DEF", {"default": None}), - "output_path": ("STRING", {"multiline": False, "default": ""}), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("output_path",) - FUNCTION = 'join_audio_files' - CATEGORY = 'ETK/audio' - - def join_audio_files(self, **kwargs): - import librosa - import copy - import soundfile as sf - import numpy as np - - audio_folder_def = kwargs['audio_folder_def'] - kwargs = copy.deepcopy(kwargs) - paths = audio_folder_def.get_all_audio_files() - 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) - - # TODO: need to implement writing in the correct format - # Save the concatenated audio - sf.write(output_path, joined_audio_data, sr) - - return (output_path,) - - -@ETK_AVF_base -class AddFramesToVideoFolder(): - """ - given a VIDEO_FOLDER_DEF node, and a list of frames, adds the frames to the video folder - """ - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"video_folder_def": ("VIDEO_FOLDER_DEF", {"default": None}), - "IMAGE": ("IMAGE", {"default": None}), - "Rescale method": ( - ["NEAREST", "BOX", "BILINEAR", "HAMMING", "BICUBIC", "LANCZOS"], - {"default": "LANCZOS"}), - "Release vram?": ("BOOLEAN", {"default": True}), - }, - } - return ret - - RETURN_TYPES = ("VIDEO_FOLDER_DEF",) - FUNCTION = "add_frames_to_video_folder" - - @staticmethod - def save_or_copy_frame(current_frame, previous_frame, frame_fp, previous_frame_fp): - import shutil - from PIL import Image - import torch - - if previous_frame is None or not torch.equal(current_frame, previous_frame): - # If the current frame is different from the previous frame, save it to disk - current_frame = current_frame - - if current_frame.dtype != torch.uint8: # handle float type frames - ndarr = current_frame.permute(2, 0, 1).mul(255).add_(0.5).clamp_(0, 255).permute(1, 2, 0).to("cpu", - torch.uint8).numpy() - else: # handle uint8 frames - ndarr = current_frame.permute(2, 0, 1).permute(1, 2, 0).numpy() - im = Image.fromarray(ndarr) - im.save(frame_fp) - - previous_frame = current_frame - else: - # If the current frame is the same as the previous frame, copy the previously saved frame file - shutil.copy2(previous_frame_fp, frame_fp) - return previous_frame - - @staticmethod - def process_frames(frames, rescale_method, video_folder_def): - import gc - import torch - num_frames = frames.shape[0] - previous_frame = None - frame_fp = None - frames_area_all_the_same = torch.all(frames[0] == frames) - if frames_area_all_the_same and frames.shape[0] > 1: - # get a list of file names using reservenexframe - frame_f_names = [video_folder_def.reserve_next_frame() for _ in range(num_frames)] - # save the first frame - AddFramesToVideoFolder.save_or_copy_frame(frames[0], previous_frame, frame_f_names[0], None) - # copy the first frame to the rest of the frames using os by calling the cli - duplicate_file(frame_f_names[0], frame_f_names[1:]) - - else: - for i in range(num_frames): - next_fn = video_folder_def.get_next_file_name() - - previous_frame_fp = frame_fp - frame_fp = next_fn - - current_frame = frames[i] - - previous_frame = AddFramesToVideoFolder.save_or_copy_frame(current_frame, previous_frame, frame_fp, - previous_frame_fp) - - del frames - gc.collect() - return None - - def add_frames_to_video_folder(self, **kwargs): - from PIL import Image - import torch - 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") - - # get the video folder path - video_folder = kwargs["video_folder_def"]["video_folder"] - - # get the frame format - frame_format = kwargs["video_folder_def"]["frame format"] - - # get the height - height = kwargs["video_folder_def"]["height"] - - # get the width - width = kwargs["video_folder_def"]["width"] - - # get the fps - fps = kwargs["video_folder_def"]["fps"] - - # get the frames - frames = kwargs["IMAGE"] - - # get the rescale method - rescale_method = kwargs.get("Rescale method", None) - if not rescale_method: - rescale_method = kwargs.get("Rescale_method", None) - - free_vram = kwargs.get("Release vram?", None) - if not free_vram: - free_vram = kwargs.get("Release_vram_", True) - - orig_frames = frames - # num_files = len(glob(os.path.join(video_folder, f"frame_*.{frame_format.lower()}"))) - - # rescale if needed to the correct size - if height != frames.shape[1] or width != frames.shape[2]: - # first since transforms.Resize requires a tensor of shape B,C, H, W - # we need to permute the tensor to B, C, H, W - frames = frames.permute(0, 3, 1, 2) - frames = frames.to("cuda") - - # get the right value for interpolation - interpolation_methods = { - "NEAREST": Image.Resampling.NEAREST, - "BOX": Image.Resampling.BOX, - "BILINEAR": Image.Resampling.BILINEAR, - "HAMMING": Image.Resampling.HAMMING, - "BICUBIC": Image.Resampling.BICUBIC, - "LANCZOS": Image.Resampling.LANCZOS - } - - rescale_type = interpolation_methods[rescale_method] - # rescale_method = transforms.Resize((height, width), interpolation=rescale_type) # this doesnt work so well lets use a different function - - if rescale_type != Image.Resampling.LANCZOS: - rescale_method = lambda x: resize(x, (height, width), interpolation=rescale_type) - frames = rescale_method(frames) - else: - rescale_method = lambda x: lanczos_resize(x, (width, height)) - frames = rescale_method(frames) - - # now we need to permute back to B, H, W, C - frames = frames.permute(0, 2, 3, 1) - self.process_frames(frames, rescale_method, kwargs["video_folder_def"]) - else: - self.process_frames(frames, rescale_method, kwargs["video_folder_def"]) - - gc.collect() - - if free_vram: - import gc - if isinstance(frames, torch.Tensor): - frames.to("cpu").detach().numpy() - del frames - - if isinstance(orig_frames, torch.Tensor): - orig_frames = orig_frames.to("cpu").detach().numpy() - del orig_frames - - 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") - # return the VIDEO_FOLDER_DEF - return (kwargs["video_folder_def"],) - - -@ETK_AVF_base -class HoldFrameForSecsInVideoFolder(): - """ - given a VIDEO_FOLDER_DEF node, and a list of frames, first uses RepeatFrames to repeat the frames - for the correct amount and then adds the frames to the video folder using AddFramesToVideoFolder - """ - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"video_folder_def": ("VIDEO_FOLDER_DEF", {"default": None}), - "IMAGE": ("IMAGE", {"default": None}), - "seconds": ("FLOAT", {"default": 1.0}), - "Rescale method": ( - ["NEAREST", "BOX", "BILINEAR", "HAMMING", "BICUBIC", "LANCZOS"], - {"default": "LANCZOS"}), - "Release vram?": ("BOOLEAN", {"default": True}), - }, - } - return ret - - RETURN_TYPES = ("VIDEO_FOLDER_DEF",) - FUNCTION = "hold_frame_for_secs_in_video_folder" - - def saver(self, x, kwargs): - # this just saves in an async way - rescale_method = kwargs.get("Rescale method", None) - if rescale_method is None: - rescale_method = kwargs.get("Rescale_method", None) - - rel_vram = kwargs.get("Release vram?", None) - if rel_vram is None: - rel_vram = kwargs.get("Release_vram_", True) - - AddFramesToVideoFolder().add_frames_to_video_folder( - video_folder_def=kwargs["video_folder_def"], - IMAGE=x, - Rescale_method=rescale_method, - Release_vram_=rel_vram - ) - - def hold_frame_for_secs_in_video_folder(self, **kwargs): - vide_folder_ref = kwargs["video_folder_def"] - - video_folder = kwargs["video_folder_def"]["video_folder"] - - # get the frame format - frame_format = kwargs["video_folder_def"]["frame format"] - - # get the height - height = kwargs["video_folder_def"]["height"] - - # get the width - width = kwargs["video_folder_def"]["width"] - - # get the fps - fps = kwargs["video_folder_def"]["fps"] - - # get the frames - frames = kwargs["IMAGE"] - - # get the number of frames - num_frames = frames.shape[0] - - # get the rescale method - rescale_method = kwargs.get("Rescale method", None) - if rescale_method is None: - rescale_method = kwargs.get("Rescale_method", None) - - # get the number of seconds - seconds = kwargs["seconds"] - - # get the number of frames to hold - num_frames_to_hold = int(fps * seconds) - - num_frames_in_this_stack = frames.shape[0] - - # get the number of repeats needed - num_repeats_needed = num_frames_to_hold // num_frames_in_this_stack - - # get the number of frames left over - num_frames_left_over = num_frames_to_hold % num_frames_in_this_stack - - # write the first set of frames to disk - self.saver(frames, kwargs) - - # get the name of the last frame - source_last_frame_name = vide_folder_ref.get_last_file_name() - # from the last frame find the rest of the frames names/paths - source_last_frame_num = int(source_last_frame_name.split("_")[-1].split(".")[0]) - source_first_frame_num = source_last_frame_num - num_frames_in_this_stack + 1 - - template = source_last_frame_name.replace(str(source_last_frame_num).zfill(10), "{}") - - range_of_source_frame_numbers = list(range(source_first_frame_num, source_last_frame_num + 1)) - - source_frame_names = [template.format(str(x).zfill(10)) for x in range_of_source_frame_numbers] - - target_done_frame_num = source_last_frame_num + num_frames_to_hold - num_frames_in_this_stack - num_frames_remaining = num_frames_to_hold - num_frames_in_this_stack - target_start_frame_num = source_last_frame_num + 1 - - target_frame_range = list(range(target_start_frame_num, target_done_frame_num + 1)) - - target_frame_names = [template.format(str(x).zfill(10)) for x in target_frame_range] - - set_todo = {fn: [] for fn in source_frame_names} - # we can now repeat the frames - for i, target_frame_name in enumerate(target_frame_names): - # keep track of what stack frame we are on - stack_frame_idx = i % num_frames_in_this_stack - stack_frame_name_to_use = source_frame_names[stack_frame_idx] - set_todo[stack_frame_name_to_use].append(target_frame_name) - - # now we can repeat the frames - for frame_name in set_todo: - src_frame = frame_name - target_frames = set_todo[src_frame] - duplicate_file(src_frame, target_frames) - - return (kwargs["video_folder_def"],) - - -@ETK_AVF_base -class Listed_HoldFramesForSecsInVideoFolder(): - """This just calls HoldFrameForSecsInVideoFolder multiple times for a list of frames and a list of durations""" - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"video_folder_def": ("VIDEO_FOLDER_DEF", {"default": None}), - "IMAGE": ("LIST", {"default": None}), - "seconds": ("LIST", {"default": None}), - "Rescale method": ( - ["NEAREST", "BOX", "BILINEAR", "HAMMING", "BICUBIC", "LANCZOS"], - {"default": "LANCZOS"}), - "Release vram?": ("BOOLEAN", {"default": True}), - }, - } - return ret - - RETURN_TYPES = ("VIDEO_FOLDER_DEF",) - FUNCTION = "listed_hold_frames_for_secs_in_video_folder" - - def listed_hold_frames_for_secs_in_video_folder(self, **kwargs): - # 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"], - IMAGE=frame, - seconds=duration, - Rescale_method=kwargs["Rescale method"], - Release_vram_=kwargs["Release vram?"]) - - return (kwargs["video_folder_def"],) - - -@ETK_AVF_base -class VideoFolderDefInfo(): - """ - Returns everything in the VIDEO_FOLDER_DEF, and also returns information about the status of the folder - """ - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"video_folder_def": ("VIDEO_FOLDER_DEF", {"default": None}), - }, - } - return ret - - RETURN_TYPES = ("VIDEO_FOLDER_DEF", "STRING", "INT", "INT", "FLOAT", "STRING", "INT", "LIST",) - RETURN_NAMES = ("VFD", "video_folder", "height", "width", "fps", "frame format", "num_files", "list of files") - - OUTPUT_NODE = True - FUNCTION = "video_folder_def_info" - - def video_folder_def_info(self, **kwargs): - import os - - video_folder = kwargs["video_folder_def"]["video_folder"] - height = kwargs["video_folder_def"]["height"] - width = kwargs["video_folder_def"]["width"] - fps = kwargs["video_folder_def"]["fps"] - frame_format = kwargs["video_folder_def"]["frame format"] - files = os.listdir(video_folder) - num_files = len(files) - - return (kwargs["video_folder_def"], video_folder, height, width, fps, frame_format, num_files, files,) - - -@ETK_AVF_base -class FadeToInVideoFolder(): - """ - given a VIDEO_FOLDER_DEF node, and a stack of frames, fades the frames in and adds them to the video folder - """ - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"video_folder_def": ("VIDEO_FOLDER_DEF", {"default": None}), - "IMAGE": ("IMAGE", {"default": None}), - "seconds": ("FLOAT", {"default": 1.0}), - "transition ms": ("FLOAT", {"default": 0.1}), - "Rescale method": ( - ["NEAREST", "BOX", "BILINEAR", "HAMMING", "BICUBIC", "LANCZOS"], - {"default": "LANCZOS"}), - "Release vram?": ("BOOLEAN", {"default": True}), - }, - } - return ret - - RETURN_TYPES = ("VIDEO_FOLDER_DEF",) - FUNCTION = "fade_to_in_video_folder" - - def fade_to_in_video_folder(self, **kwargs): - from PIL import Image - import torch - - # usee HoldFrameForSecsInVideoFolder to hold the frame after creating and AddFramesToVideoFolder the transition - - transition_length_secs = int(kwargs["transition ms"] * kwargs["video_folder_def"]["fps"]) / 1000 - hold_frame_length_secs = int(kwargs["seconds"] * kwargs["video_folder_def"]["fps"]) - transition_length_secs - - transition_frame_count = int(kwargs["video_folder_def"]["fps"] * transition_length_secs) - - base_frame_fn = kwargs["video_folder_def"].get_last_file_name() - # load it into a pytorch tensor (B, H, W, C) - base_frame = Image.open(base_frame_fn) - base_frame = torch.tensor(base_frame).permute(2, 0, 1).unsqueeze(0).to("cuda") - - transition_stack = torch.zeros((transition_frame_count, - base_frame.shape[1], - base_frame.shape[2], - base_frame.shape[3]), - dtype=torch.float16, device="cuda") - - # fade from base_frame to the first frame in the stack over the transition length - new_frame = kwargs["IMAGE"][0].to("cuda") - for i in range(transition_frame_count): - alpha = i / transition_frame_count - new_frame = (1 - alpha) * base_frame + alpha * new_frame - new_frame = new_frame.to("cpu") - transition_stack[i] = new_frame - - # add to the video folder - AddFramesToVideoFolder().add_frames_to_video_folder( - video_folder_def=kwargs["video_folder_def"], - IMAGE=transition_stack, - Rescale_method=kwargs["Rescale method"], - Release_vram_=kwargs["Release vram?"]) - - # hold the frame - HoldFrameForSecsInVideoFolder().hold_frame_for_secs_in_video_folder( - video_folder_def=kwargs["video_folder_def"], - IMAGE=kwargs["IMAGE"], - seconds=hold_frame_length_secs, - Rescale_method=kwargs["Rescale method"], - Release_vram_=kwargs["Release vram?"]) - - return (kwargs["video_folder_def"],) - - -def duplicate_file(source_file, target_files): - """ - Duplicates a given source file into multiple target files using a single CLI command - in a non-blocking way. The base directory is automatically determined from the - source file's location. - - Args: - - source_file (str): Path to the source file to be duplicated. - - target_files (list of str): A list of target file paths where the source file should be duplicated. - - Returns: - - None - """ - import os - import subprocess - - # Determine the base directory from the source file's directory - base_dir = os.path.dirname(os.path.abspath(source_file)) - - # Change the current working directory to the base directory - os.chdir(base_dir) - - # Get the source file name (relative to the base directory) - source_file_name = os.path.basename(source_file) - - # get the relative paths of the target files - target_files = [os.path.relpath(target_file, base_dir) for target_file in target_files] - - # Determine the shell command based on the operating system - copy_command = 'copy' if os.name == 'nt' else 'cp' - - # Construct the command string - command_parts = [f'{copy_command} "{source_file_name}" "{target}"' for target in target_files] - command = ' && '.join(command_parts) - - command = command.replace("\"", "") - command = command.replace("\'", "") - # print(len(command)) - - try: - # Execute the command in a non-blocking way - process = subprocess.Popen(command, shell=True, stderr=subprocess.DEVNULL) - process.wait() - - except subprocess.CalledProcessError as e: - pass - except ValueError as e: - pass - except Exception as e: - pass - - if "process" not in locals(): - pass - if "process" not in locals() or process.returncode != 0: - # If the command is too long, split it into multiple commands - # split the command into commands that do 64 at a time - command_parts = [command_parts[i:i + 64] for i in range(0, len(command_parts), 64)] - processes = [] - for command_part in command_parts: - command = ' && '.join(command_part) - process = subprocess.Popen(command, shell=True, stderr=subprocess.DEVNULL) - processes.append(process) - - for process in processes: - process.wait() - - return process - - -def run_ffmpeg_command(args): - """ - Executes a command using the ffmpeg executable. - - Parameters: - args (list): List of command-line arguments for ffmpeg. - """ - import subprocess - import imageio_ffmpeg as ffmpeg - if isinstance(args, str): - args = args.split() - cmd = [ffmpeg.get_ffmpeg_exe()] + args - subprocess.run(cmd) diff --git a/basic.py b/basic.py deleted file mode 100644 index 183cf81..0000000 --- a/basic.py +++ /dev/null @@ -1,748 +0,0 @@ -class MultilineStringNode: - """Noe that displays multiline text box""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "Text": ("STRING", { - "default": "", - "multiline": True, - }), - } - } - - RETURN_TYPES = ("STRING", "INT", "FLOAT", "LIST") - FUNCTION = "get_value" - CATEGORY = "text" - - def get_value(self, Text): - return (Text,) - - -class TupleNode: - """Node that displays a tuple""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "Tuple": ("STRING", { - "default": "(None,None,)", - }), - } - } - - RETURN_TYPES = ("TUPLE",) - FUNCTION = "get_value" - CATEGORY = "text" - - def get_value(self, Tuple): - # evaluate the string safely step by step - # type check - if not isinstance(Tuple, str): - raise TypeError("Tuple must be a string") - - # format check - if not Tuple.startswith("(") or not Tuple.endswith(")"): - raise ValueError("Tuple must start with '(' and end with ')'") - - # remove the brackets - Tuple = Tuple.strip("()") - - # check for empty tuple - if len(Tuple) == 0: - return ((),) - # check for commas - if "," not in Tuple: - raise ValueError("Tuple must have at least one comma") - - # split on commas - Tuple = Tuple.split(",") - - # valid values are string,int,float,None, list,tuple,dict - # construct the tuple - ret = [] - for val in Tuple: - val = val.strip() - if val == "None": - ret.append(None) - elif val.startswith("[") and val.endswith("]"): - # list - val = val.strip("[]") - val = val.split(",") - ret.append(val) - elif val.startswith("(") and val.endswith(")"): - # tuple - val = val.strip("()") - val = val.split(",") - ret.append(tuple(val)) - elif val.startswith("{") and val.endswith("}"): - # dict - val = val.strip("{}") - val = val.split(",") - ret.append(dict(val)) - elif val.startswith('"') and val.endswith('"'): - # string - ret.append(val.strip('"')) - elif "." in val: - # float - ret.append(float(val)) - elif val == "": - # empty - pass - else: - # int - ret.append(int(val)) - - return (tuple(ret),) - - -class StrBreakout: - """ node that breaks out a string into a: - - Boolean - - int - - float - - [string] - - splitlines() - - split(",") - - the first value is a description of the transformations tried - """ - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "Text": ("STRING", { - "default": "", - }), - "list_dtype": (["str", "int", "float", ],), - } - } - - RETURN_TYPES = ("STRING", "BOOL", "INT", "FLOAT", "LIST", "LIST", "LIST",) - RETURN_NAMES = ("Notes", "Bool", "Int", "Float", "List by chr", "list by lines", "list by commas",) - FUNCTION = "get_value" - CATEGORY = "text" - - def get_value(self, Text, list_dtype="str"): - out_bool = False - out_int = 0 - out_float = 0.0 - out_list = [] - out_lines = [] - out_comma = [] - notes = [] - try: - out_bool = bool(Text) - notes.append(f"bool worked") - except Exception as e: - notes.append(f"bool failed: {e}") - - try: - out_int = int(Text) - notes.append(f"int worked") - except Exception as e: - notes.append(f"int failed: {e}") - pass - try: - out_float = float(Text) - notes.append(f"float worked") - except Exception as e: - pass - try: - out_list = list(Text) - notes.append(f"list worked") - except Exception as e: - notes.append(f"list failed: {e}") - pass - try: - out_lines = Text.splitlines() - notes.append(f"splitlines worked") - except Exception as e: - notes.append(f"splitlines failed: {e}") - pass - try: - out_comma = Text.split(",") - notes.append(f"split(',') worked") - except Exception as e: - notes.append(f"split(',') failed: {e}") - pass - - if list_dtype == "int": - list_dtype = int - elif list_dtype == "float": - list_dtype = float - else: - list_dtype = str - - try: - out_list = [list_dtype(val) for val in out_list] - notes.append(f"list dtype worked") - except Exception as e: - notes.append(f"list dtype failed: {e}") - pass - try: - out_lines = [list_dtype(val) for val in out_lines] - notes.append(f"splitlines dtype worked") - except Exception as e: - notes.append(f"splitlines dtype failed: {e}") - pass - try: - out_comma = [list_dtype(val) for val in out_comma] - notes.append(f"split(',') dtype worked") - except Exception as e: - notes.append(f"split(',') dtype failed: {e}") - pass - - return ("\n".join(notes), out_bool, out_int, out_float, out_list, out_lines, out_comma,) - - -class IntBreakout: - """ node that breaks out an int into a: - - Boolean - - str(int) - - float - - [int] - - list(range(int)) - - [None] * int - - the first value is a description of the transformations tried - """ - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "Int": ("INT", { - "default": 0, - }), - } - } - - RETURN_TYPES = ("STRING", "BOOL", "STRING", "FLOAT", "LIST", "LIST", "LIST",) - FUNCTION = "get_value" - CATEGORY = "text" - - def get_value(self, Int): - out_bool = False - out_str = "" - out_float = 0.0 - out_list = [] - out_range = [] - out_none = [] - notes = [] - try: - out_bool = bool(Int) - notes.append(f"bool worked") - except Exception as e: - notes.append(f"bool failed: {e}") - - try: - out_str = str(Int) - notes.append(f"str worked") - except Exception as e: - notes.append(f"str failed: {e}") - pass - try: - out_float = float(Int) - notes.append(f"float worked") - except Exception as e: - pass - try: - out_list = list(Int) - notes.append(f"list worked") - except Exception as e: - notes.append(f"list failed: {e}") - pass - try: - out_range = list(range(Int)) - notes.append(f"range worked") - except Exception as e: - notes.append(f"range failed: {e}") - pass - try: - out_none = [None] * Int - notes.append(f"[None] * worked") - except Exception as e: - notes.append(f"[None] * failed: {e}") - pass - return ("\n".join(notes), out_bool, out_str, out_float, out_list, out_range, out_none,) - - -class SelectStrFromDict: - """ - Selects a string from a dictionary - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "dict": ("DICT", { - "default": {}, - }), - "key": ("STRING", { - "default": "", - }), - } - } - - RETURN_TYPES = ("STRING",) - FUNCTION = "get_value" - CATEGORY = "text" - - def get_value(self, dict, key): - if key in dict: - return (dict[key],) - return ("",) - - -class StringToAny: - """takes STRING, and returns the value coded as *""" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "string": ("STRING", {"forceInput": True}), - } - } - - RETURN_TYPES = ("*", "STRING",) - RETURN_NAMES = ("*", "debug info",) - FUNCTION = "get_value" - CATEGORY = "conversion" - - def get_value(self, string): - try: - ret_str = str(string) - dbg = f"found STRING: {string}" - except Exception as e: - ret_str = None - dbg = f"failed to verify STRING: {string} error: {e}" - - return (ret_str, dbg,) - - -class ValueToAny: - """takes INT,STRING,FLOAT,BOOL, and returns the value coded as *""" - - @classmethod - def INPUT_TYPES(cls): - ret = { - "required": {}, - "optional": { - "INT": ("INT", {"forceInput": True}), - "FLOAT": ("FLOAT", {"forceInput": True}), - "STRING": ("STRING", {"forceInput": True}), - "BOOL": ("BOOL", {"forceInput": True}), - }} - return ret - - RETURN_TYPES = ("*", "STRING",) - RETURN_NAMES = ("*", "debug info",) - FUNCTION = "get_value" - CATEGORY = "conversion" - - def get_value(self, INT=None, FLOAT=None, STRING=None, BOOL=None): - if INT: - dbg = f"found INT: {INT}" - return (INT, dbg,) - if FLOAT: - dbg = f"found FLOAT: {FLOAT}" - return (FLOAT, dbg,) - if STRING: - dbg = f"found STRING: {STRING}" - return (STRING, dbg,) - if BOOL: - dbg = f"found BOOL: {BOOL}" - return (BOOL, dbg,) - dbg = f"found nothing, will return the first value that is not None; from INT,FLOAT,STRING,BOOL" - return (None, dbg) - - -class AnyToAny: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "A": ("*", {"forceInput": True}), - } - } - - RETURN_TYPES = ("*",) - FUNCTION = "get_value" - CATEGORY = "conversion" - RETURN_NAMES = ("*",) - - def get_value(self, A): - return (A,) - - -class IterableToAny: - """takes LIST,DICT, and returns the value coded as *""" - - @classmethod - def INPUT_TYPES(cls): - return { - "optional": { - "LIST": ("LIST", {"default": None, }), - "DICT": ("DICT", {"default": None, }), - } - } - - RETURN_TYPES = ("*", "STRING",) - RETURN_NAMES = ("*", "debug info",) - FUNCTION = "get_value" - CATEGORY = "conversion" - - def get_value(self, LIST=None, DICT=None): - - if LIST: - if not isinstance(LIST, list): - return (LIST, f"LIST is not a list: {LIST} is type: {type(LIST)}") - dbg = f"found LIST: {LIST}" - return (LIST, dbg,) - - if DICT: - if not isinstance(DICT, dict): - return (LIST, f"LIST is not a list: {LIST} is type: {type(LIST)}") - - dbg = f"found DICT: {DICT}" - return (DICT, dbg,) - dbg = f"found nothing, will return the first value that is not None; from LIST,DICT" - return (None, dbg) - - -class CallableToAny: - """takes FUNC, (others soon) and returns the value coded as *""" - - @classmethod - def INPUT_TYPES(cls): - return { - "optional": { - "FUNC": ("FUNC", {"default": None, }), - } - } - - RETURN_TYPES = ("*", "STRING",) - RETURN_NAMES = ("*", "debug info",) - FUNCTION = "get_value" - CATEGORY = "conversion" - - def get_value(self, FUNC=None): - if FUNC: - dbg = f"found FUNC: {FUNC}" - return (FUNC, dbg,) - dbg = f"found nothing, will return the first value that is not None; from FUNC" - return (None, dbg) - - -class ObjectToAny: - """ takes non-primitive objects like TENSORS and returns the value coded as *""" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "label": ("STRING", {"default": "0", })}, - "optional": { - "LATENT": ("LATENT", {"default": None, }), - "CONDITIONING": ("CONDITIONING", {"default": None, }), - "IMAGE": ("IMAGE", {"default": None, }), - "MASK": ("MASK", {"default": None, }), - "CLIP": ("CLIP", {"default": None, }), - "MODEL": ("MODEL", {"default": None, }), - } - } - - RETURN_TYPES = ("*", "STRING",) - RETURN_NAMES = ("*", "debug info",) - FUNCTION = "get_value" - CATEGORY = "conversion" - - def get_value(self, label, - LATENT=None, - CONDITIONING=None, - IMAGE=None, - MASK=None, - CLIP=None, - MODEL=None, - ): - if LATENT: - dbg = f"found LATENT: {LATENT}" - return (LATENT, dbg,) - - if CONDITIONING: - dbg = f"found CONDITIONING: {CONDITIONING}" - return (CONDITIONING, dbg,) - - if IMAGE is not None: - dbg = f"found IMAGE: {IMAGE}" - return (IMAGE, dbg,) - - if MASK: - dbg = f"found MASK: {MASK}" - return (MASK, dbg,) - - if MODEL: - dbg = f"found MODEL: {MODEL}" - return (MODEL, dbg,) - if CLIP: - dbg = f"found CLIP: {CLIP}" - return (CLIP, dbg,) - - dbg = f"found nothing, will return the first value that is not None; from LATENT,CONDITIONING,IMAGE,MASK" - return (None, dbg) - - -from torch import Tensor -from comfy.model_patcher import ModelPatcher - -COMFY_PYTHON_CLAS_NAMES = { - "INT": int, - "FLOAT": float, - "STRING": str, - "BOOLEAN": bool, - "TUPLE": tuple, - "LIST": list, - "DICT": dict, - "FUNC": callable, - "LATENT": dict, - "CONDITIONING": dict, - "IMAGE": Tensor, - "MASK": Tensor, - "LLLM_MESSAGES": list, - "LITELLM_MODEL": str, - "MODEL": ModelPatcher, -} - - -def value_try(val, cls): - try: - return cls(val) - except: - return None - - -class AnyToValue: - """ - takes * and returns it in the node systems common value types - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "A": ("*", { - "default": None, - }), - }} - - RETURN_TYPES = ("INT", "FLOAT", "STRING", "BOOLEAN", "TUPLE",) - FUNCTION = "get_value" - CATEGORY = "conversion" - - def get_value(self, A): - int_o = value_try(A, int) - float_o = value_try(A, float) - str_o = value_try(A, str) - bool_o = value_try(A, bool) - tuple_o = value_try(A, tuple) - - return (int_o, float_o, str_o, bool_o, tuple_o,) - - -class AnyToIterable: - """ - takes * and returns it in the node systems common value types - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "Any": ("*", { - "default": None, - }), - }} - - RETURN_TYPES = ("LIST", "DICT",) - FUNCTION = "get_value" - CATEGORY = "conversion" - - def get_value(self, Any): - list_o = None - dict_o = None - - if isinstance(Any, list): - list_o = Any - - if isinstance(Any, dict): - dict_o = Any - - return (list_o, dict_o,) - - -class AnyToObject: - """ - like AnyToIterable but for Image,latent,conditioning,model - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "x": ("*", { - "default": None, - }), - }} - - RETURN_TYPES = ("IMAGE", "LATENT", "CONDITIONING", "MODEL") - FUNCTION = "get_value" - CATEGORY = "conversion" - - def get_value(self, x): - image_o = x - latent_o = x - conditioning_o = x - model_o = x - - return (image_o, latent_o, conditioning_o, model_o,) - - -class TimeThis: - """ - Pass through node that records the current time to a given variable name - then when called again reports the time difference - """ - - start_times = {} - end_times = {} - last_activator = None - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "var_name": ("STRING", {"default": "timer0", }), - "pass_through": ("*", {"default": None, }), - "start": ("BOOLEAN", {"default": True}) - }, - "optional": { - "activator": ("*") - } - - } - - INTERNAL_STATE_DISPLAY = "text_display" - RETURN_TYPES = ("STRING", "FLOAT", "OBJECT") - RETURN_NAMES = ("Notes", "Time", "Object") - FUNCTION = "time_this" - CATEGORY = "utils" - - def time_this(self, var_name, pass_through, start, activator=None): - import time - - if start: - self.start_times[var_name] = time.time() - print(f"Timer started for {var_name}") - ret = ("Timer started", 0.0, pass_through) - else: - if var_name not in self.start_times: - return ("Timer not started", 0.0, pass_through) - self.end_times[var_name] = time.time() - time_diff = self.end_times[var_name] - self.start_times[var_name] - print(f"Timer ended for {var_name} time taken: {time_diff}") - ret = (f"Time taken: {time_diff}", time_diff, pass_through) - - ret = {"ui": {"text": (ret[0],)}, "result": ret} - return ret - - -class Text_ETK: - """ - to provide text to the model - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text": ("STRING", {"multiline": True}), - } - } - - RETURN_TYPES = ("STRING",) - FUNCTION = "fun" - CATEGORY = "text" - - def fun(self, text): - return (text,) - - -class StringsToList: - """ - take two strings and return them as a list - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": ("LIST", {"default": [], "forceInput": True}), - "string1": ("STRING", {"default": "", "forceInput": True}), - }, - "optional": { - "string2": ("STRING", {"default": "", "forceInput": True}), - } - - } - - RETURN_TYPES = ("LIST",) - FUNCTION = "fun" - CATEGORY = "text" - - def fun(self, list, string1, string2=""): - return (list + [string1, string2],) - - -# from __init__ import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS -from . import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS - -NODE_CLASS_MAPPINGS["Text_ETK"] = Text_ETK -NODE_CLASS_MAPPINGS["Text _O"] = Text_ETK -NODE_CLASS_MAPPINGS["MultilineStringNode"] = MultilineStringNode -NODE_CLASS_MAPPINGS["StrBreakout"] = StrBreakout -NODE_CLASS_MAPPINGS["IntBreakout"] = IntBreakout - -NODE_CLASS_MAPPINGS["ValueToAny"] = ValueToAny -NODE_CLASS_MAPPINGS["IterableToAny"] = IterableToAny -NODE_CLASS_MAPPINGS["CallableToAny"] = CallableToAny -NODE_CLASS_MAPPINGS["ObjectToAny"] = ObjectToAny - -NODE_CLASS_MAPPINGS["ETK_Tuple"] = TupleNode -NODE_CLASS_MAPPINGS["AnyToValue"] = AnyToValue -NODE_CLASS_MAPPINGS["AnyToIterable"] = AnyToIterable -NODE_CLASS_MAPPINGS["TimeThis"] = TimeThis -NODE_CLASS_MAPPINGS["AnyToObject"] = AnyToObject - -NODE_CLASS_MAPPINGS["SelectStrFromDict"] = SelectStrFromDict - -NODE_CLASS_MAPPINGS["StringToAny"] = StringToAny -NODE_CLASS_MAPPINGS["StringsToList"] = StringsToList -NODE_CLASS_MAPPINGS["AnyToAny"] = AnyToAny diff --git a/descript.py b/descript.py deleted file mode 100644 index baefb53..0000000 --- a/descript.py +++ /dev/null @@ -1,87 +0,0 @@ -import copy - -NODE_CLASS_MAPPINGS = {} -NODE_DISPLAY_NAME_MAPPINGS = {} - - -def ETK_descript_base(cls): - cls.FUNCTION = "func" - cls.CATEGORY = "ETK/Descript" - NODE_CLASS_MAPPINGS[cls.__name__] = cls - return cls - - -@ETK_descript_base -class DecodeTranscriptNode: - """ - Decode one of descript's transcript formatted files - String input is the files contents - output is 3 lists of strings (speaker,duration,paragraph,) - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "string": ("STRING", {"multiline": True, - "default": "" - }), - } - } - - RETURN_TYPES = ("LIST", "LIST", "LIST") - RETURN_NAMES = ("speakers", "durations", "paragraphs") - OUTPUT_NODE = True - - def func(self, string): - """ - input is like: - [00:00:00] jackie: Let's talk about a - - [00:00:01] jackie: thing - - :param string: - :return: - """ - paras = string.split("\n\n") - speakers = [] - durations = [] - paragraphs = [] - - for para in paras: - if para.strip() == "": - continue - # first find the first ] and split on that - part1 = para.find("]") - time_part = para[:part1 + 1] - remainder = para[part1 + 1:] - - # now find first : in the remainder - part2 = remainder.find(":") - speaker_part = remainder[:part2 + 1].replace(":", "").strip() - paragraph_part = remainder[part2 + 1:] - speakers.append(speaker_part.strip()) - - # now just remove [ and ] from time_part - time_part = time_part.replace("[", "").replace("]", "").strip() - - # split it on : - time_part = time_part.split(":") - - # now convert to seconds - time_part = int(time_part[0]) * 60 * 60 + int(time_part[1]) * 60 + int(time_part[2]) - - # save here for post-processing later - durations.append(time_part) - - paragraphs.append(paragraph_part.strip()) - - import copy - start_seconds = copy.copy(durations) - durations = [] - for i in range(len(start_seconds) - 1): - durations.append(start_seconds[i + 1] - start_seconds[i]) - # now append the last value - durations.append(int(sum(durations) / len(durations))) - - return (speakers, durations, paragraphs,) diff --git a/functional.py b/functional.py deleted file mode 100644 index 85107b0..0000000 --- a/functional.py +++ /dev/null @@ -1,774 +0,0 @@ -import hashlib -from . import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS - -# NODE_CLASS_MAPPINGS = {} -# NODE_DISPLAY_NAME_MAPPINGS = {} -_list = {"list": ("LIST",)} -_func = {"func": ("FUNC", {"default": None})} -_func_extra = {"func_extra": ("FUNC",)} -_text = {"code": ("STRING", {"multiline": True, "default": "y mapped to output"})} -_image = {"image": ("IMAGE",)} -_string = {"string": ("STRING", {"default": ""})} -_any = {"any": ("*", {"default": ""})} - -optional = lambda x: {"optional": {k: v for k, v in x.items()}} -required = lambda x: {"required": {k: v for k, v in x.items()}} -both = lambda a, b: {**a, **b} -many = lambda *dicts: {k: v for d in dicts for k, v in d.items()} # non tested - - -def ETK_functional_base(cls): - cls.FUNCTION = "func" - cls.CATEGORY = "ETK/functional" - NODE_CLASS_MAPPINGS[cls.__name__] = cls - return cls - - -def new_exec(c, g, l): - global old_exec - import os - # if GDE_SECURE_MODE is set to NO_EXEC then we will not execute any code - # this is a security feature to prevent malicious code from being executed - # this is set in the GDE settings - GDE_SECURE_MODE = os.environ.get("GDE_SECURE_MODE", None) - if GDE_SECURE_MODE == "NO_EXEC": - raise Exception("GDE_SECURE_MODE is set to NO_EXEC, not executing code") - - old_exec(c, g, l) - - return locals - - -old_exec = exec -exec = new_exec - - -@ETK_functional_base -class FuncBase: - """ - Base class for func nodes - - Allows for a function to be passed in as a callable - and - for a string to be passed in as code to be executed - also - returns the function as a callable - - """ - - def __init__(self): - from custom_nodes.EternalKernelLiteGraphNodes.image import common_ksampler, TinyTxtToImg - self.locals = locals() - self.globals = globals() - - @classmethod - def INPUT_TYPES(cls): - req = required(_text) - opt = optional( - both( - both( - _func, - _func_extra - ), - _list - ) - ) - use = both(req, opt) - - return use - - RETURN_TYPES = ("FUNC",) - FUNCTION = "func" - CATEGORY = "ETK/func" - - def func(self, **kwargs): - import inspect - self.globals = globals() - self.locals = locals() - self.locals["x1"] = kwargs.get("func", None) - self.locals["x2"] = kwargs.get("func_extra", None) - self.locals["x_list"] = kwargs.get("list", None) - - self.IN_FUNC = kwargs.get("func", None) - self.EXTRA_IN_FUNC = kwargs.get("func_extra", None) - self.exec_code = kwargs.get("code", None) - self.ARGS = kwargs - self.CODE = inspect.getsource(self.func) - - exec(self.exec_code, self.globals, self.locals) - - return (self.func,) - - -@ETK_functional_base -class FuncRender: - """ - as input takes a function and a string of code - optionally takes a list of arguments - returns rendered results of types - float, string, int, list - """ - - def __init__(self): - from custom_nodes.EternalKernelLiteGraphNodes.image import common_ksampler, TinyTxtToImg - self.locals = locals() - self.globals = globals() - - @classmethod - def INPUT_TYPES(cls): - use = both( - required( - _text), - optional(both(_list, _func) - ) - ) - return use - - RETURN_TYPES = ("FLOAT", "STRING", "INT", "LIST") - FUNCTION = "func" - CATEGORY = "ETK/func" - - def func(self, **kwargs): - y_float = None - y_string = None - y_int = None - func = kwargs.get("func", None) - code = kwargs.get("code", None) - - globals = self.globals - locals = self.locals - - locals["y_float"] = y_float - locals["y_string"] = y_string - locals["y_int"] = y_int - locals["y_list"] = None - - locals["x"] = func - locals["x_list"] = kwargs.get("list", None) - # code should do something like: - # func like : lambda x:["what"]*x - # code like : 'f=[x for x in x(2)]' - exec(code, globals, locals) - self.globals = globals - self.locals = locals - - return (locals["y_float"], locals["y_string"], locals["y_int"], locals["y_list"],) - - -@ETK_functional_base -class FuncRenderImage: - def __init__(self): - from custom_nodes.EternalKernelLiteGraphNodes.image import common_ksampler, TinyTxtToImg - self.locals = locals() - self.globals = globals() - - @classmethod - def INPUT_TYPES(cls): - use = both( - required( - both(_func, _text)), - optional(_list) - ) - return use - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "func" - CATEGORY = "ETK/func" - OUTPUT_NODE = True - - def func(self, func, code, **kwargs): - globals = self.globals - locals = self.locals - locals["x"] = func - locals["x_list"] = kwargs.get("list", None) - # code should do something like: - # func like : lambda x:["what"]*x - # code like : 'f=[x for x in x(2)]' - exec(code, globals, locals) - self.globals = globals - self.locals = locals - - return (locals["y"],) - - -@ETK_functional_base -class ExecWidget: - import torch - - """runs eval on the given text""" - - @classmethod - def INPUT_TYPES(cls): - return {"optional": - { - - "code_str_in": ("STRING", {"multiline": True, "default": ""}), - "image1_in": ("IMAGE",), - "float1_in": ("FLOAT", {"multiline": False}), - "string1_in": ("STRING", {"multiline": False}), - "int1_in": ("INT", {"multiline": False}), - - }, - "required": { - "text_to_eval": ("STRING", - {"multiline": True, - "default": "int_out=int_out\n" - "float_out=float_out\n" - "string_out=string_out\n" - "image_out=image_out\n" - }), - } - } - - CATEGORY = "text" - RETURN_TYPES = ("STRING", "IMAGE", "FLOAT", "INT", "TUPLE", "CODE") - FUNCTION = "pre_exec_handler" - INTERNAL_STATE_DISPLAY_CODE = True - OUTPUT_NODE = True - - def pre_exec_handler(self, **kwargs): - image1_in = kwargs.get("image1_in", None) - float1_in = kwargs.get("float1_in", 0.0) - string1_in = kwargs.get("string1_in", "") - int1_in = kwargs.get("int1_in", 0) - tuple1_in = kwargs.get("tuple1_in", None) - name = kwargs.get("name", "exec_func") - text_to_eval = kwargs.get("text_to_eval", "") - code_str_in = kwargs.get("code_str_in", "") - if code_str_in != "": - text_to_eval = code_str_in - else: - code_str_in = None - - ret = self.exec_handler(text_to_eval, image1_in, float1_in, string1_in, int1_in, tuple1_in, name) - - ret_formatted = {"ui": {"text": [ret[-1]]}, "result": ret} - return ret_formatted - - def exec_handler(self, text_to_eval, image_obj: torch.Tensor = None, float_obj: float = 0.0, string_obj: str = "", - int_obj: int = 0, tuple_obj: tuple = None, name: str = "exec_func"): - """ - >>> ExecWidget().exec_handler("2 + 3") - '5' - """ - - new_locals = {"float_out": float_obj, - "image_out": image_obj, - "string_out": string_obj, - "int_out": int_obj, - "tuple_out": tuple_obj, - } - - try: - if isinstance(text_to_eval, list): - text_to_eval = text_to_eval[0] - - # evaluate for code tag - if "" in text_to_eval and "" in text_to_eval: - start = text_to_eval.find("") - end = text_to_eval.find("") - text_to_eval = text_to_eval[start + 6:end] - # also evaluate for ```python tag - - if "```python" in text_to_eval and "```" in text_to_eval: - start = text_to_eval.find("```python") - end = text_to_eval.find("```", start + 8) - text_to_eval = text_to_eval[start + 9:end] - - if "```" in text_to_eval: - start = text_to_eval.find("```") - end = text_to_eval.find("```", start + 2) - text_to_eval = text_to_eval[start + 3:end] - - exec(text_to_eval, globals(), new_locals) - source_code = text_to_eval - - except Exception as e: - string_obj = str(e) - source_code = text_to_eval - - string_obj = new_locals.get("string_out", string_obj) - image_obj = new_locals.get("image_out", image_obj) - float_obj = new_locals.get("float_out", float_obj) - int_obj = new_locals.get("int_out", int_obj) - tuple_obj = new_locals.get("tuple_out", tuple_obj) - - return (string_obj, image_obj, float_obj, int_obj, tuple_obj, text_to_eval) - - @classmethod - def IS_CHANGED_NOPE(s, text_to_eval, image1_in: torch.Tensor = None, float1_in: float = 0.0, string1_in: str = "", - int1_in: int = 0, name: str = "exec_func"): - - m = hashlib.sha256() - m.update(name.encode("utf-8")) - return m.digest().hex() - - -@ETK_functional_base -class FuncListToList: - """runs eval on the given text, with a list as input and output""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - req = required( - both(_list, - _text) - - ) - opt = optional(_func) - use = both(req, opt) - return use - - CATEGORY = "ETK/func" - RETURN_TYPES = ("LIST",) - FUNCTION = "func" - - def func(self, **kwargs): - func = kwargs.get("func", None) - code = kwargs.get("code", None) - my_globals = globals() - my_locals = locals() - - my_locals["x"] = func - my_locals["x_list"] = kwargs.get("list", None) - - exec(code, my_globals, my_locals) - - return (my_locals["y"],) - - -@ETK_functional_base -class FuncStrToStr: - """runs eval on the given text, with a string as input and outputs a string""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - req = required(both(_string, _text)) - return req - - CATEGORY = "ETK/func" - RETURN_TYPES = ("STRING",) - FUNCTION = "func" - - def func(self, **kwargs): - text = kwargs.get("string", None) - x = text - code = kwargs.get("code", None) - my_globals = globals() - my_locals = locals() - - exec(code, my_globals, my_locals) - - return (my_locals["y"],) - - -@ETK_functional_base -class FuncStrToList: - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - out = {"required": { - "string": ("STRING", {"default": "", }), - "code": ("STRING", {"multiline": True, "default": "y mapped to output"}) - }} - - - return out - - CATEGORY = "ETK/func" - RETURN_TYPES = ("LIST",) - FUNCTION = "func" - - def func(self, **kwargs): - text = kwargs.get("string", None) - code = kwargs.get("code", None) - x = text - my_globals = globals() - my_locals = locals() - - exec(code, my_globals, my_locals) - - return (my_locals["y"],) - - -@ETK_functional_base -class FuncListToStr: - # class FuncStrToList: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - req = required(both(_list, _text)) - return req - - CATEGORY = "ETK/func" - RETURN_TYPES = ("STRING",) - FUNCTION = "func" - - def func(self, **kwargs): - lst = kwargs.get("list", None) - code = kwargs.get("code", None) - x = lst - x_list = lst - my_globals = globals() - my_locals = locals() - - exec(code, my_globals, my_locals) - - return (my_locals["y"],) - - -@ETK_functional_base -class GetFirstCodeBlock(): - """ returns the first code block in the text which is surrounded with ``` or - ```python or - tags. If no code block is found, returns an empty string""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - req = required(_text) - return req - - CATEGORY = "ETK/func" - RETURN_TYPES = ("STRING",) - FUNCTION = "func" - - def func(self, **kwargs): - text = kwargs.get("code", None) - # find the code block - if "```python" in text and "```" in text: - start = text.find("```python") - end = text.find("```", start + 8) - text = text[start + 9:end] - elif "```" in text: - start = text.find("```") - end = text.find("```", start + 2) - text = text[start + 3:end] - elif "" in text and "" in text: - start = text.find("") - end = text.find("") - text = text[start + 6:end] - else: - text = "" - return (text,) - - -@ETK_functional_base -class FuncAnysToImage: - """runs eval on the given text, with 5 anys as input and outputs an image""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - req = optional( - both( - both({"any1": ("*", {"default": None})}, {"any2": ("*", {"default": None})}), - both({"any3": ("*", {"default": None})}, {"any4": ("*", {"default": None})}) - ), - ) - req["required"] = {} - req["required"].update(_text) - - return req - - CATEGORY = "ETK/func" - RETURN_TYPES = ("IMAGE",) - FUNCTION = "func" - - def func(self, **kwargs): - import gc - import torch - code = kwargs.get("code", None) - any1 = kwargs.get("any1", None) - any2 = kwargs.get("any2", None) - any3 = kwargs.get("any3", None) - any4 = kwargs.get("any4", None) - - my_globals = globals().copy() - my_locals = {} - - my_locals["any1"] = any1 - my_locals["any2"] = any2 - my_locals["any3"] = any3 - my_locals["any4"] = any4 - - exec(code, my_globals, my_locals) - if "y" in my_locals.keys(): - y = my_locals["y"] - elif "y" in my_globals.keys(): - y = my_globals["y"] - else: - y = None - - del my_locals - del my_globals - del code - del any1, any2, any3, any4 - gc.collect() - torch.cuda.empty_cache() - - return (y,) - - -@ETK_functional_base -class FuncAnysToLatent: - """runs eval on the given text, with 5 anys as input and outputs a latent""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - req = optional( - both( - both({"any1": ("*", {"default": None})}, {"any2": ("*", {"default": None})}), - both({"any3": ("*", {"default": None})}, {"any4": ("*", {"default": None})}) - ), - ) - req["required"] = {} - req["required"].update(_text) - - return req - - CATEGORY = "ETK/func" - RETURN_TYPES = ("LATENT",) - FUNCTION = "func" - - def func(self, **kwargs): - code = kwargs.get("code", None) - any1 = kwargs.get("any1", None) - any2 = kwargs.get("any2", None) - any3 = kwargs.get("any3", None) - any4 = kwargs.get("any4", None) - - my_globals = globals() - my_locals = locals() - - my_locals["any1"] = any1 - my_locals["any2"] = any2 - my_locals["any3"] = any3 - my_locals["any4"] = any4 - - exec(code, my_globals, my_locals) - if "y" in my_locals.keys(): - return (my_locals["y"],) - elif "y" in my_globals.keys(): - return (my_globals["y"],) - else: - return (None,) - - -@ETK_functional_base -class FuncAnysToString: - """runs eval on the given text, with 5 anys as input and outputs a string""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - req = optional( - both( - both({"any1": ("*", {"default": None})}, {"any2": ("*", {"default": None})}), - both({"any3": ("*", {"default": None})}, {"any4": ("*", {"default": None})}) - ), - ) - req["required"] = {} - req["required"].update(_text) - - return req - - CATEGORY = "ETK/func" - RETURN_TYPES = ("STRING",) - FUNCTION = "func" - - def func(self, **kwargs): - code = kwargs.get("code", None) - any1 = kwargs.get("any1", None) - any2 = kwargs.get("any2", None) - any3 = kwargs.get("any3", None) - any4 = kwargs.get("any4", None) - - my_globals = globals() - my_locals = locals() - - my_locals["any1"] = any1 - my_locals["any2"] = any2 - my_locals["any3"] = any3 - my_locals["any4"] = any4 - - exec(code, my_globals, my_locals) - if "y" in my_locals.keys(): - return (my_locals["y"],) - elif "y" in my_globals.keys(): - return (my_globals["y"],) - else: - return (None,) - - -@ETK_functional_base -class FuncAnysToList: - """runs eval on the given text, with 5 anys as input and outputs a list""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - req = optional( - both( - both({"any1": ("*", {"default": None})}, {"any2": ("*", {"default": None})}), - both({"any3": ("*", {"default": None})}, {"any4": ("*", {"default": None})}) - ), - ) - req["required"] = {} - req["required"].update(_text) - - return req - - CATEGORY = "ETK/func" - RETURN_TYPES = ("LIST",) - FUNCTION = "func" - - def func(self, **kwargs): - code = kwargs.get("code", None) - any1 = kwargs.get("any1", None) - any2 = kwargs.get("any2", None) - any3 = kwargs.get("any3", None) - any4 = kwargs.get("any4", None) - - my_globals = globals() - my_locals = {} - - my_locals["any1"] = any1 - my_locals["any2"] = any2 - my_locals["any3"] = any3 - my_locals["any4"] = any4 - - exec(code, my_globals, my_locals) - if "y" in my_locals.keys(): - return (my_locals["y"],) - elif "y" in my_globals.keys(): - return (my_globals["y"],) - else: - return (None,) - -@ETK_functional_base -class FuncAnysToDict: - """runs eval on the given text, with 5 anys as input and outputs a list""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - req = optional( - both( - both({"any1": ("*", {"default": None})}, {"any2": ("*", {"default": None})}), - both({"any3": ("*", {"default": None})}, {"any4": ("*", {"default": None})}) - ), - ) - req["required"] = {} - req["required"].update(_text) - - return req - - CATEGORY = "ETK/func" - RETURN_TYPES = ("DICT",) - FUNCTION = "func" - - def func(self, **kwargs): - code = kwargs.get("code", None) - any1 = kwargs.get("any1", None) - any2 = kwargs.get("any2", None) - any3 = kwargs.get("any3", None) - any4 = kwargs.get("any4", None) - - my_globals = globals() - my_locals = {} - - my_locals["any1"] = any1 - my_locals["any2"] = any2 - my_locals["any3"] = any3 - my_locals["any4"] = any4 - - exec(code, my_globals, my_locals) - if "y" in my_locals.keys(): - return (my_locals["y"],) - elif "y" in my_globals.keys(): - return (my_globals["y"],) - else: - return (None,) - -@ETK_functional_base -class FuncAnysToCond: - """runs eval on the given text, with 5 anys as input and outputs a cond""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - req = optional( - both( - both({"any1": ("*", {"default": None})}, {"any2": ("*", {"default": None})}), - both({"any3": ("*", {"default": None})}, {"any4": ("*", {"default": None})}) - ), - ) - req["required"] = {} - req["required"].update(_text) - - return req - - CATEGORY = "ETK/func" - RETURN_TYPES = ("CONDITIONING",) - FUNCTION = "func" - - def func(self, **kwargs): - import gc - code = kwargs.get("code", None) - any1 = kwargs.get("any1", None) - any2 = kwargs.get("any2", None) - any3 = kwargs.get("any3", None) - any4 = kwargs.get("any4", None) - - my_globals = globals() - my_locals = {} - - my_locals["any1"] = any1 - my_locals["any2"] = any2 - my_locals["any3"] = any3 - my_locals["any4"] = any4 - - exec(code, my_globals, my_locals) - if "y" in my_locals.keys(): - y = (my_locals["y"],) - elif "y" in my_globals.keys(): - y = my_globals["y"], - else: - y = (None,) - del my_locals - gc.collect() - - return (y,) - - -if __name__ == "__main__": - test = {} - - pass diff --git a/git.py b/git.py deleted file mode 100644 index 924c289..0000000 --- a/git.py +++ /dev/null @@ -1,185 +0,0 @@ -from git import Repo -import copy -import re - -from . import NODE_CLASS_MAPPINGS,NODE_DISPLAY_NAME_MAPPINGS -#NODE_CLASS_MAPPINGS = {} -#NODE_DISPLAY_NAME_MAPPINGS = {} - - - - -def ETK_git_base(cls): - cls.FUNCTION = "func" - cls.CATEGORY = "ETK/git" - NODE_CLASS_MAPPINGS[cls.__name__] = cls - return cls - - -@ETK_git_base -class PublicGitRepo: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "repo_url": ("STRING", {"multiline": False, "default": ""}), - "token": ("STRING", {"multiline": False, "default": ""}), - } - } - - RETURN_TYPES = ("bool",) - FUNCTION = 'validate_git_repo_url' - CATEGORY = 'ETK/git' - - -def validate_git_repo_url(self, **kwargs): - kwargs = copy.deepcopy(kwargs) - repo_url = kwargs.get('repo_url') - token = kwargs.get('token') - - # Regular expression to match GitHub repository URLs - pattern = r'https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/?' - match = re.match(pattern, repo_url) - - # The URL is valid if there's a match and it covers the whole URL - return match is not None and match.group() == repo_url - - -@ETK_git_base -class CloneRepoNode: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "repo_url": ("STRING", {"multiline": False, "default": ""}), - "path": ("STRING", {"multiline": False, "default": ""}), - # "path_to_key": ("STRING", {"multiline": False, "default": ""}), - } - } - - RETURN_TYPES = ("STRING", "STRING", "LIST", "STRING",) - RETURN_NAMES = ("path_passthrough", "file_structure", "file_list", "STD_OUT+ERR",) - CATEGORY = 'ETK/git' - - def func(self, **kwargs): - kwargs = copy.deepcopy(kwargs) - repo_url = kwargs.get('repo_url') - path = kwargs.get('path') - - # token = kwargs.get('token') - # lets just do it via calling the terminal - # working = f"GIT_SSH_COMMAND='ssh -i {path_to_key}' git clone https://github.com/story-squad/GDE_Graph_IO.git {path}" - working = f"git clone {repo_url} {path}" - import os - import subprocess - - # execute the command and retrieve the results - process = subprocess.Popen(working, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) - out, err = process.communicate() - - ld = os.listdir(path) - - return (path, "\n".join(ld), ld, out.decode("utf-8") + err.decode("utf-8"),) - - -@ETK_git_base -class PullRepoNode: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "repo_path": ("STRING", {"multiline": False, "default": ""}), - "token": ("STRING", {"multiline": False, "default": ""}), - } - } - - RETURN_TYPES = () - FUNCTION = 'pull_repo' - CATEGORY = 'ETK/git' - - -def pull_repo(self, **kwargs): - kwargs = copy.deepcopy(kwargs) - repo_path = kwargs.get('repo_path') - token = kwargs.get('token') - repo = Repo(repo_path) - origin = repo.remotes.origin - # with repo.git(GIT_ASKPASS='echo', GIT_USERNAME=token): - origin.pull() - - # with repo.git.custom_environment(GIT_ASKPASS='echo', GIT_USERNAME=token): - # origin.pull() - - -@ETK_git_base -class PushRepoNode: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "repo_path": ("STRING", {"multiline": False, "default": ""}), - "token": ("STRING", {"multiline": False, "default": ""}), - } - } - - RETURN_TYPES = () - FUNCTION = 'push_repo' - CATEGORY = 'ETK/git' - - -def push_repo(self, **kwargs): - kwargs = copy.deepcopy(kwargs) - repo_path = kwargs.get('repo_path') - token = kwargs.get('token') - repo = Repo(repo_path) - origin = repo.remotes.origin - # with repo.git.custom_environment(GIT_ASKPASS='echo', GIT_USERNAME=token): - origin.push() - - -@ETK_git_base -class SendCommitNode: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "repo_path": ("STRING", {"multiline": False, "default": ""}), - "commit_message": ("STRING", {"multiline": False, "default": ""}), - "token": ("STRING", {"multiline": False, "default": ""}), - } - } - - RETURN_TYPES = () - FUNCTION = 'send_commit' - CATEGORY = 'ETK/git' - - -def send_commit(self, **kwargs): - kwargs = copy.deepcopy(kwargs) - repo_path = kwargs.get('repo_path') - commit_message = kwargs.get('commit_message') - repo = Repo(repo_path) - repo.git.add(A=True) - repo.index.commit(commit_message) - - -@ETK_git_base -class CreateRepoNode: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "path": ("STRING", {"multiline": False, "default": ""}), - "token": ("STRING", {"multiline": False, "default": ""}), - } - } - - RETURN_TYPES = () - FUNCTION = 'create_repo' - CATEGORY = 'ETK/git' - - -def create_repo(self, **kwargs): - kwargs = copy.deepcopy(kwargs) - path = kwargs.get('path') - Repo.init(path) diff --git a/hf_diffusers_nodes.py b/hf_diffusers_nodes.py deleted file mode 100644 index 0c12581..0000000 --- a/hf_diffusers_nodes.py +++ /dev/null @@ -1,278 +0,0 @@ -import torch -import folder_paths -import torchvision -import torch.nn as nn -import functools -import torch -import json - -try: - from .config import config_settings -except ImportError as e: - from config import config_settings - -#NODE_CLASS_MAPPINGS = {} -#NODE_DISPLAY_NAME_MAPPINGS = {} -from . import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS - - -def ETK_HF_Diffusers_base(cls): - cls.CATEGORY = "ETK/HF/Diffusers" - # Add spaces to the camel case class name - pretty_name = cls.__name__ - for i in range(1, len(pretty_name)): - if pretty_name[i].isupper(): - pretty_name = pretty_name[:i] + " " + pretty_name[i:] - cls.DISPLAY_NAME = pretty_name - NODE_CLASS_MAPPINGS[cls.__name__] = cls - NODE_DISPLAY_NAME_MAPPINGS[cls.DISPLAY_NAME] = pretty_name - - # Wrap the function defined in the FUNCTION attribute - func_name = getattr(cls, "FUNCTION", None) - if func_name and hasattr(cls, func_name): - original_func = getattr(cls, func_name) - - @functools.wraps(original_func) - def wrapped_func(*args, **kwargs): - with torch.inference_mode(False): - return original_func(*args, **kwargs) - - setattr(cls, func_name, wrapped_func) - - return cls - - -@ETK_HF_Diffusers_base -class DDPMPipline: - """A node that loads the specified DDPMPipeline, supports only from_pretrained for now""" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "model_name": ("STRING", {"default": "johnowhitaker/ddpm-butterflies-32px"}), - "device": (["cpu", "cuda"], {"default": "cuda"}), - }, - } - - RETURN_TYPES = ("DDPM_PL",) - FUNCTION = "load_from_pretrained" - - def load_from_pretrained(self, model_name, device): - from diffusers import DDPMPipeline - ret = DDPMPipeline.from_pretrained(model_name, device=device) - return (ret,) - - -@ETK_HF_Diffusers_base -class SampleDDPMPipline: - """A node that samples from the specified DDPMPipeline""" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "model": ("DDPM_PL", {}), - "batch_size": ("INT", {"default": 1}), - "num_timesteps": ("INT", {"default": 1000}), - }, - } - - RETURN_TYPES = ("TORCH_TENSOR",) - FUNCTION = "sample" - - def sample( - self, - model, - batch_size, - num_timesteps, - ): - ret = model( - batch_size=batch_size, - generator=None, - num_inference_steps=num_timesteps, - output_type="tensor", - ) - ret = torch.tensor(ret.images) - return (ret,) - - -@ETK_HF_Diffusers_base -class TensorToImage: - """A node that converts a tensor to an image""" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "tensor": ("TORCH_TENSOR", {}), - "incoming_format": (["BHWC", "BCHW", "HWC"], {"default": "BHWC"} - ), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "tensor_to_image" - - def tensor_to_image(self, tensor, incoming_format): - # outgoing format is BHWC torch tensor float 32 0-1 - - if incoming_format == "BCHW": - tensor = tensor.permute(0, 2, 3, 1) - elif incoming_format == "HWC": - tensor = tensor.unsqueeze(0) - tensor = tensor.clamp(0, 1) - - return (tensor,) - - -@ETK_HF_Diffusers_base -class LoadHFDataset: - """A node that loads the specified HFDataset""" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "dataset_name": ("STRING", {"default": "huggan/smithsonian_butterflies_subset"}), - }, - } - - RETURN_TYPES = ("HF_DATASET",) - FUNCTION = "load_from_hf" - - def load_from_hf(self, dataset_name): - from datasets import load_dataset - ret = load_dataset(dataset_name, - split="train", - ) - return (ret,) - - -@ETK_HF_Diffusers_base -class CreateUnet2dModel: - """A node that creates a unet2d model""" - - @classmethod - def INPUT_TYPES(cls): - down_blocks = ("DownBlock2D", "AttnDownBlock2D", "AttnDownBlock2D", "AttnDownBlock2D") - mid_blocks = ('UNetMidBlock2D', 'UnCLIPUNetMidBlock2D') - up_blocks = ("AttnUpBlock2D", "AttnUpBlock2D", "AttnUpBlock2D", "UpBlock2D") - - return { - "required": { - "name": ("STRING", {"default": "UNet2D"}), - }, - "optional": { - "sample_size": ("TUPLE", {"default": (32, 32, 3,)}), - "in_channels": ("INT", {"default": 3}), - "out_channels": ("INT", {"default": 3}), - "norm_num_groups": ("INT", {"default": 32}), - "layers_per_block": ("INT", {"default": 2}), - "down_block_types": ("TUPLE", {"default": (down_blocks[0],)}), - "mid_block_scale_factor": ("FLOAT", {"default": 1.0}), - "up_block_types": ("TUPLE", {"default": (up_blocks[0],)}), - "block_out_channels": ("TUPLE", {"default": (64, 128, 256, 512,)}), - }, - } - - RETURN_TYPES = ("TORCH_MODEL",) - FUNCTION = "create_unet2d_model" - - def create_unet2d_model(self, name, sample_size, in_channels, out_channels, norm_num_groups, layers_per_block, - down_block_types=("AttnDownBlock2D",), - mid_block_scale_factor=("UNetMidBlock2D",), - up_block_types=("AttnUpBlock2D",), - block_out_channels=(64, 128, 256, 512,) - ): - from diffusers import UNet2DModel - with torch.inference_mode(False): - ret = UNet2DModel( - sample_size=sample_size, - in_channels=in_channels, - out_channels=out_channels, - norm_num_groups=norm_num_groups, - layers_per_block=layers_per_block, - down_block_types=down_block_types, - mid_block_scale_factor=mid_block_scale_factor, - up_block_types=up_block_types, - block_out_channels=block_out_channels, - - ) - for param in ret.parameters(): - param.requires_grad = True - #param.retains_grad = True - - return (ret,) - -@ETK_HF_Diffusers_base -class TrainUnet2dModel: - """A node that trains a unet2d model""" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "model": ("TORCH_MODEL", {}), - "dataset": ("HF_DATASET", {}), - "batch_size": ("INT", {"default": 1}), - "num_epochs": ("INT", {"default": 1}), - "learning_rate": ("FLOAT", {"default": 1e-4}), - }, - } - - RETURN_TYPES = ("TORCH_MODEL",) - FUNCTION = "train_unet2d_model" - - def train_unet2d_model(self, model, dataset, batch_size, num_epochs, learning_rate): - from torch.utils.data import DataLoader - # import mse loss - from torch.nn import MSELoss - from torch.optim import Adam - from diffusers import UNet2DModel - - - model = model.train() - model = model.cuda() - - dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True) - - optimizer = Adam(model.parameters(), lr=learning_rate) - loss_fn = MSELoss() - - for epoch in range(num_epochs): - for batch in dataloader: - batch = batch["image"] - batch = batch.cuda() - optimizer.zero_grad() - loss = loss_fn(model, batch) - loss.backward() - optimizer.step() - print(loss.item()) - - return (model,) - -# @ETK_HF_Diffusers_base -# class LoadModelFromHF: -# """A node that loads the specified model from huggingface""" -# -# @classmethod -# def INPUT_TYPES(cls): -# return { -# "required": { -# "model_name": ("STRING", {"default": "johnowhitaker/ddpm-butterflies-32px"}), -# }, -# } -# -# RETURN_TYPES = ("TORCH_MODEL",) -# FUNCTION = "load_model_from_hf" -# -# def load_model_from_hf(self, model_name): -# from huggingface_hub import hf_hub_url -# from diffusers import DDPMPipeline -# ret = DDPMPipeline.from_pretrained(model_name, device="cuda") -# return (ret,) - - - - diff --git a/image.py b/image.py deleted file mode 100644 index cbdb02a..0000000 --- a/image.py +++ /dev/null @@ -1,3763 +0,0 @@ -import torch -from PIL import Image -import numpy as np -import comfy -import folder_paths -import torch -import torchvision -import os -import hashlib - -# info for code completion AI ### -""" -all of these classes are plugins for comfyui and follow the same pattern -all of the images are torch tensors and it is unknown and unimportant if they are on the cpu or gpu -all image inputs are (B,W,H,C) - -avoid numpy and PIL as much as possible -""" - -try: - from . import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS -except ImportError: - NODE_CLASS_MAPPINGS = {} - NODE_DISPLAY_NAME_MAPPINGS = {} - - -# NODE_CLASS_MAPPINGS = {} -# NODE_DISPLAY_NAME_MAPPINGS = {} - - -# def ETK_image_base(cls): -# # cls.FUNCTION = "func" -# cls.CATEGORY = "ETK/Image" -# NODE_CLASS_MAPPINGS[cls.__name__] = cls -# return cls - -def ETK_image_base(cls): - cls.CATEGORY = "ETK/Image" - # Add spaces to the camel case class name - pretty_name = cls.__name__ - for i in range(1, len(pretty_name)): - if pretty_name[i].isupper(): - pretty_name = pretty_name[:i] + " " + pretty_name[i:] - cls.DISPLAY_NAME = pretty_name - NODE_CLASS_MAPPINGS[cls.__name__] = cls - NODE_DISPLAY_NAME_MAPPINGS[cls.DISPLAY_NAME] = pretty_name - return cls - - -def get_fonts(): - # Directory paths for fonts in Windows and Linux - windows_fonts_dir = 'C:\\Windows\\Fonts\\' - linux_fonts_dir = '/usr/share/fonts/' - - # Use the appropriate directory based on the current platform - if os.name == 'nt': # Windows - fonts_dir = windows_fonts_dir - else: # Linux and others - fonts_dir = linux_fonts_dir - - # Initialize the list of fonts - fonts = [] - - # Check if the directory exists - if os.path.exists(fonts_dir): - try: - # Traverse the directory recursively and add all .ttf and .otf files to the list - for dirpath, dirnames, filenames in os.walk(fonts_dir): - for filename in filenames: - if filename.endswith('.ttf') or filename.endswith('.otf'): - fonts.append(os.path.join(dirpath, filename)) - except PermissionError: - print(f"Permission denied to access the directory: {fonts_dir}") - except Exception as e: - print(f"An error occurred while accessing the directory: {fonts_dir}") - print(e) - else: - print(f"The directory does not exist: {fonts_dir}") - - return fonts - - -fonts = get_fonts() - - -def rgba_per_channel_norm(image): - """ normalize per channel """ - print("normalizing per channel") - - if image[:, :, :, 0].min() != image[:, :, :, 0].max(): - image[:, :, :, 0] = image[:, :, :, 0] - image[:, :, :, 0].min() - image[:, :, :, 0] = image[:, :, :, 0] / image[:, :, :, 0].max() - - if image[:, :, :, 1].min() != image[:, :, :, 1].max(): - image[:, :, :, 1] = image[:, :, :, 1] - image[:, :, :, 1].min() - image[:, :, :, 1] = image[:, :, :, 1] / image[:, :, :, 1].max() - - if image[:, :, :, 2].min() != image[:, :, :, 2].max(): - image[:, :, :, 2] = image[:, :, :, 2] - image[:, :, :, 2].min() - image[:, :, :, 2] = image[:, :, :, 2] / image[:, :, :, 2].max() - - if image[:, :, :, 3].min() != image[:, :, :, 3].max(): - image[:, :, :, 3] = image[:, :, :, 3] - image[:, :, :, 3].min() - image[:, :, :, 3] = image[:, :, :, 3] / image[:, :, :, 3].max() - return image - - -def torch_image_show(image): - """Show a torch image""" - from PIL import Image - - if image.size().__len__() == 4: - image = image[0] - if image.dtype != torch.uint8: - image = torch.mul(image, 255, ) - image = image.to(torch.uint8) # Convert tensor to uint8 data type - - # must have at least 3 channels - if image.shape[-1] == 1: - image = torch.cat((image, image, image), dim=-1) - - image = image.numpy() - try: - image = Image.fromarray(image) # Create a PIL image - except TypeError as e: - # this is a np array not a torch tensor - # try to rearrange the channels using np - image = np.moveaxis(image, 0, -1) - - # image.permute(1, 2, 0) - image = Image.fromarray(image) # Create a PIL image - - image.show() - - -@ETK_image_base -class LoadImageFromPath: - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image_path": ("STRING", {"default": "output/image.png"}), - }, - } - - RETURN_TYPES = ("IMAGE",) - RETURN_NAMES = ("image",) - FUNCTION = "execute" - OUTPUT_NODE = True - - def execute(self, image_path: str): - import torch - from PIL import Image - import numpy as np - from pathlib import Path - - path = Path(image_path) - loaded_image = Image.open(path) - loaded_image = np.array(loaded_image).astype(np.float32) / 255.0 - loaded_image = torch.from_numpy(loaded_image)[None,] - - # torch tensor mus be (B,W,H,C) - if len(loaded_image.shape) == 3: - loaded_image = loaded_image.unsqueeze(0) - return (loaded_image,) - - @classmethod - def IS_CHANGED(s, image_path): - import hashlib - from pathlib import Path - # use the file path and modified date + file size (all as a string) to determine if the file has changed - path = Path(image_path) - m = hashlib.sha256() - m.update(str(path.stat().st_mtime).encode()) - m.update(str(path.stat().st_size).encode()) - return m.digest().hex() - - - - - - -@ETK_image_base -class SaveImageToPath: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "path": ("STRING", {"default": "output/image.png"}), - "save_sequence": ("BOOLEAN", {"default": False}), - "image_format": (["png", "jpg", "bmp"], {"default": "png"}), - "jpg_quality": ("INT", { - "default": 70, - "min": 0, - "max": 100, - "step": 1, - }), - "png_compression": ("INT", { - "default": 5, - "min": 1, - "max": 9, - "step": 1, - }), - }, - } - - RETURN_TYPES = ("LIST",) - RETURN_NAMES = ("saved_paths",) - FUNCTION = "execute" - OUTPUT_NODE = True - - def execute(self, image: torch.Tensor, path: str, save_sequence: bool, image_format: str, jpg_quality: int, - png_compression: int): - import numpy as np - from PIL import Image - from pathlib import Path - - save_paths = [] - path = Path(path) - path.parent.mkdir(parents=True, exist_ok=True) - - for i, img in enumerate(image): - # Convert torch tensor to PIL Image - pil_image = Image.fromarray((img.squeeze().cpu().numpy() * 255).astype(np.uint8)) - - # Determine file path - if save_sequence: - file_path = path.with_stem(f"{path.stem}_{i:06d}") - elif image.shape[0] > 1: - file_path = path.with_stem(f"{path.stem}_{i}") - else: - file_path = path - - # Ensure correct file extension - file_path = file_path.with_suffix(f".{image_format}") - - # Save image - if image_format == "jpg": - pil_image.save(file_path, quality=jpg_quality, optimize=True) - elif image_format == "png": - pil_image.save(file_path, compress_level=png_compression) - else: # bmp - pil_image.save(file_path) - - save_paths.append(str(file_path)) - - return (save_paths,) - - -@ETK_image_base -class TinyTxtToImg: - """small text to image generator""" - share_clip = None - share_mdl = None - share_vae = None - - def __init__(self): - print("ETK> TinyTxtToImg init") - self.mdl = None - self.clp = None - self.vae = None - - self.vision = None - self.steps = 10 - self.cfg = 8 - self.sampler_name = comfy.samplers.KSampler.SAMPLERS[0] - self.scheduler = comfy.samplers.KSampler.SCHEDULERS[0] - self.positive = None - self.negative = None - self.width = 512 - self.height = 512 - self.batch_size = 1 - self.denoise = 1.0 - self.latent_image = None - self.samples = None - - @classmethod - def INPUT_TYPES(cls): - return {"required": - { - "prompt": ("STRING", {"multiline": True}), - "neg_prompt": ("STRING", {"multiline": True}), - "clip_encoder": (["comfy -ignore below", "advanced"],), - "token_normalization": (["none", "mean", "length", "length+mean"],), - "weight_interpretation": (["comfy", "A1111", "compel", "comfy++"],), - "ckpt_name": (folder_paths.get_filename_list("checkpoints"),), - "one_seed": ([True, False], {"default": False}), - "unload_model": ([True, False], {"default": True}), - "func_only": ([True, False], {"default": True}), - - }, - "optional": { - "name": ("STRING", {"multiline": False, - "default": "tinytxt2img"} - ), - "overrides": ("STRING", {"multiline": True, - "default": '{"width":768,"height":768,"steps":20}'}), - "latent": ("LATENT",), - "pos_cond": ("CONDITIONING",), - "neg_cond": ("CONDITIONING",), - "image": ("IMAGE",), - } - } - - CATEGORY = "ETK" - - RETURN_TYPES = ("IMAGE", "FUNC", "LATENT", "CONDITIONING", "CONDITIONING",) - RETURN_NAMES = ("image", "FUNC", "latent", "positive", "negative",) - FUNCTION = "tinytxt2img" - - def tinytxt2img(self, **kwargs): - """ use the imports from nodes to generate an image from text """ - from nodes import CLIPTextEncode - import comfy.model_management as mm - import random - import json - import gc - torch.cuda.empty_cache() - - unload_model = kwargs.get("unload_model", True) - prompt = kwargs.get("prompt", "") - neg_prompt = kwargs.get("neg_prompt", "") - clip_encoder = kwargs.get("clip_encoder", "comfy -ignore below") - token_normalization = kwargs.get("token_normalization", "length+mean") - weight_interpretation = kwargs.get("weight_interpretation", "comfy++") - ckpt_name = kwargs.get("ckpt_name", None) - - model_ref = kwargs.get("model", None) - vae = kwargs.get("vae", None) - clip = kwargs.get("clip", None) - - overrides = kwargs.get("overrides", "") - name = kwargs.get("name", "tinytxt2img") - FUNC = kwargs.get("FUNC", None) - render_what = kwargs.get("render_what", "image") - - self.latent_image = kwargs.get("latent", None) - self.pos_cond = kwargs.get("pos_cond", None) - self.neg_cond = kwargs.get("neg_cond", None) - self.seed = kwargs.get("seed", random.randint(0, 2 ** 32 - 1)) - self.one_seed_per_batch = kwargs.get("one_seed", False) - self.denoise = kwargs.get("denoise", 1.0) - self.steps = kwargs.get("steps", 10) - - if model_ref and vae and clip: - self.mdl = model_ref - self.vae = vae - self.clp = clip - else: - from nodes import CheckpointLoaderSimple - self.mdl, self.clp, self.vae = \ - CheckpointLoaderSimple.load_checkpoint(None, - ckpt_name=ckpt_name - ) - # Check if an input image is provided - input_image = kwargs.get("image", None) - if input_image is not None: - # Encode the input image using the model's VAE - with torch.no_grad(): - self.vae.device = torch.device("cuda:0") - self.vae.first_stage_model = self.vae.first_stage_model.to("cuda:0") - # self.vae.first_stage_model.decoder = self.vae.first_stage_model.decoder.to("cuda:0") - # self.vae.first_stage_model = self.vae.first_stage_model.to("cuda:0") - input_image = input_image.to("cuda") - q = self.vae.encode(input_image) - self.latent_image = {"samples": q} - - self.cfg = kwargs.get("cfg", 8) - - if not "sampler_name" in kwargs and ckpt_name: - if "lcm" in ckpt_name.lower(): - self.sampler_name = comfy.samplers.KSampler.SAMPLERS[18] - self.scheduler = comfy.samplers.KSampler.SCHEDULERS[0] - else: - self.sampler_name = comfy.samplers.KSampler.SAMPLERS[0] - self.scheduler = comfy.samplers.KSampler.SCHEDULERS[0] - self.scheduler = comfy.samplers.KSampler.SCHEDULERS[0] - else: - self.sampler_name = kwargs.get("sampler_name", comfy.samplers.KSampler.SAMPLERS[0]) - self.scheduler = kwargs.get("scheduler", comfy.samplers.KSampler.SCHEDULERS[0]) - - self.positive = prompt - self.negative = neg_prompt - self.width = 512 - self.height = 512 - self.batch_size = 1 - if not self.denoise: - self.denoise = 1.0 - - if overrides: - if overrides != "": - overrides = overrides.replace("'", "\"") - if overrides[0] != "{": - overrides = "{" + overrides + "}" - valid_dict = json.loads(overrides) - for k, v in valid_dict.items(): - if k in self.__dict__: - self.__setattr__(k, v) - else: - print(f"invalid override key: {k}") - # TODO: probably need to prepare the execution better to make sure that applying the incomming func can - # change everything - if FUNC is not None: - self.__dict__.update(FUNC(self.__dict__)) - - if not self.pos_cond: - self.clp.cond_stage_model = self.clp.cond_stage_model.to("cuda") - if clip_encoder == "comfy -ignore below": - self.clp_encode = lambda x: CLIPTextEncode.encode(None, self.clp, x)[0] - self.pos_cond = self.clp_encode(self.positive) - self.neg_cond = self.clp_encode(self.negative) - - elif clip_encoder == "advanced": - self.clp_encode = lambda x: CLIPTextEncodeAdvanced.encode(None, - self.clp, - x, - token_normalization, - weight_interpretation)[0] - try: - self.pos_cond = self.clp_encode(self.positive) - self.neg_cond = self.clp_encode(self.negative) - - except Exception as e: - print(e) - raise ValueError("advanced clip encoder failed") - self.clp.cond_stage_model = self.clp.cond_stage_model.to("cpu") - - torch.cuda.empty_cache() - gc.collect() - if self.latent_image == None: - from nodes import EmptyLatentImage - self.latent_image = EmptyLatentImage().generate(self.width, self.height, self.batch_size)[0] - - if "latent" in render_what or "all" in render_what or "image" in render_what: - self.KSampler = ETKKSampler() - # self.mdl.offload_device = torch.device("cuda:0") - # self.mdl.load_device = torch.device("cuda:0") - self.samples = self.KSampler.sample( - model=self.mdl, - seed=self.seed, - steps=self.steps, - cfg=self.cfg, - sampler_name=self.sampler_name, - scheduler=self.scheduler, - latent_image=self.latent_image, - pos_cond=self.pos_cond, - neg_cond=self.neg_cond, - positive=self.pos_cond, - negative=self.neg_cond, - denoise=self.denoise, - one_seed_per_batch=self.one_seed_per_batch)[0] - - if "image" in render_what or "all" in render_what: - # Move the samples to the GPU - self.samples["samples"] = self.samples["samples"].to("cuda") - - # Move VAE model to the GPU asynchronously - self.vae.first_stage_model = self.vae.first_stage_model.to("cuda:0") - self.vae.device = torch.device(torch.device("cuda:0")) - torch.cuda.empty_cache() - - decoded_images = self.vae.decode(self.samples["samples"]) - image = decoded_images.to("cpu") - - self.vae.first_stage_model.to("cpu") - self.vae.device = torch.device(torch.device("cpu")) - torch.cuda.empty_cache() - gc.collect() - # Perform VAEDecode using samples in the computation stream - # with torch.cpu.stream(cpu_stream): - # decoded_images = self.vae.decode(self.samples["samples"]) - - # Synchronize computation stream before moving model back to CPU - # torch.cuda.synchronize() - - # Move VAE model back to CPU asynchronously - # self.vae.first_stage_model.to("cpu", non_blocking=True) - - # Convert the decoded image to CPU - - self.FUNC = self.tinytxt2img - self.ARGS = kwargs - - self.samples["samples"] = self.samples["samples"].to("cpu") - - ret = (image, - self, - self.samples, - self.pos_cond, - self.neg_cond - ,) - if unload_model: - del self.mdl - del self.clp - del self.vae - if "vision" in self.__dict__: - del self.vision - del self.samples - del self.FUNC - del self.ARGS - del self.pos_cond - del self.neg_cond - del self.KSampler - mm.free_memory(mm.get_total_memory(mm.get_torch_device()), mm.get_torch_device()) - torch.cuda.empty_cache() - gc.collect() - return ret - -@ETK_image_base -class TinyTxtToImgV2: - """small text to image generator""" - share_clip = None - share_mdl = None - share_vae = None - - def __init__(self): - print("ETK> TinyTxtToImgV2 init") - self.mdl = None - self.clp = None - self.vae = None - - self.vision = None - self.steps = 10 - self.cfg = 8 - self.sampler_name = comfy.samplers.KSampler.SAMPLERS[0] - self.scheduler = comfy.samplers.KSampler.SCHEDULERS[0] - self.positive = None - self.negative = None - self.width = 512 - self.height = 512 - self.batch_size = 1 - self.denoise = 1.0 - self.latent_image = None - self.samples = None - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "prompt": ("STRING", {"multiline": True}), - "neg_prompt": ("STRING", {"multiline": True}), - "clip_encoder": (["comfy -ignore below", "advanced"],), - "token_normalization": (["none", "mean", "length", "length+mean"],), - "weight_interpretation": (["comfy", "A1111", "compel", "comfy++"],), - "ckpt_name": (folder_paths.get_filename_list("checkpoints"),), - "one_seed": ([True, False], {"default": False}), - "unload_model": ([True, False], {"default": True}), - "func_only": ([True, False], {"default": True}), - }, - "optional": { - "name": ("STRING", {"multiline": False, "default": "tinytxt2img"}), - "overrides": ("STRING", {"multiline": True, "default": '{"width":768,"height":768,"steps":20}'}), - "latent": ("LATENT",), - "pos_cond": ("CONDITIONING",), - "neg_cond": ("CONDITIONING",), - "image": ("IMAGE",), - } - } - - CATEGORY = "ETK" - - RETURN_TYPES = ("IMAGE", "FUNC", "LATENT", "CONDITIONING", "CONDITIONING",) - RETURN_NAMES = ("image", "FUNC", "latent", "positive", "negative",) - FUNCTION = "tinytxt2img" - - def tinytxt2img(self, **kwargs): - """If func_only is True, return a callable function instead of immediately generating an image.""" - import torch - import comfy.model_management as mm - import random - import json - import gc - from nodes import CLIPTextEncode - - torch.cuda.empty_cache() - - def _do_generation(**override_kwargs): - """This inner function carries out the entire generation logic, using merged arguments.""" - # Merge original kwargs with any overrides passed to _do_generation - all_args = dict(kwargs) - all_args.update(override_kwargs) - - unload_model = all_args.get("unload_model", True) - prompt = all_args.get("prompt", "") - neg_prompt = all_args.get("neg_prompt", "") - clip_encoder = all_args.get("clip_encoder", "comfy -ignore below") - token_normalization = all_args.get("token_normalization", "length+mean") - weight_interpretation = all_args.get("weight_interpretation", "comfy++") - ckpt_name = all_args.get("ckpt_name", None) - - model_ref = all_args.get("model", None) - vae = all_args.get("vae", None) - clip = all_args.get("clip", None) - - overrides = all_args.get("overrides", "") - name = all_args.get("name", "tinytxt2img") - FUNC = all_args.get("FUNC", None) - render_what = all_args.get("render_what", "image") - - self.latent_image = all_args.get("latent", None) - self.pos_cond = all_args.get("pos_cond", None) - self.neg_cond = all_args.get("neg_cond", None) - self.seed = all_args.get("seed", random.randint(0, 2 ** 32 - 1)) - self.one_seed_per_batch = all_args.get("one_seed", False) - self.denoise = all_args.get("denoise", 1.0) - self.steps = all_args.get("steps", 10) - - if model_ref and vae and clip: - self.mdl = model_ref - self.vae = vae - self.clp = clip - else: - from nodes import CheckpointLoaderSimple - self.mdl, self.clp, self.vae = CheckpointLoaderSimple.load_checkpoint( - None, ckpt_name=ckpt_name - ) - - # Check if an input image is provided - input_image = all_args.get("image", None) - if input_image is not None: - with torch.no_grad(): - self.vae.device = torch.device("cuda:0") - self.vae.first_stage_model = self.vae.first_stage_model.to("cuda:0") - input_image = input_image.to("cuda") - q = self.vae.encode(input_image) - self.latent_image = {"samples": q} - - self.cfg = all_args.get("cfg", 8) - - # Sampler logic - if "sampler_name" not in all_args and ckpt_name: - if "lcm" in ckpt_name.lower(): - self.sampler_name = comfy.samplers.KSampler.SAMPLERS[18] - self.scheduler = comfy.samplers.KSampler.SCHEDULERS[0] - else: - self.sampler_name = comfy.samplers.KSampler.SAMPLERS[0] - self.scheduler = comfy.samplers.KSampler.SCHEDULERS[0] - else: - self.sampler_name = all_args.get("sampler_name", comfy.samplers.KSampler.SAMPLERS[0]) - self.scheduler = all_args.get("scheduler", comfy.samplers.KSampler.SCHEDULERS[0]) - - self.positive = prompt - self.negative = neg_prompt - self.width = 512 - self.height = 512 - self.batch_size = 1 - if not self.denoise: - self.denoise = 1.0 - - # Process overrides if provided - if overrides: - if overrides != "": - overrides = overrides.replace("'", "\"") - if overrides[0] != "{": - overrides = "{" + overrides + "}" - valid_dict = json.loads(overrides) - for k, v in valid_dict.items(): - if k in self.__dict__: - self.__setattr__(k, v) - else: - print(f"invalid override key: {k}") - - if FUNC is not None: - self.__dict__.update(FUNC(self.__dict__)) - - # Build positive/negative conditioning if not provided - if not self.pos_cond: - # Possibly advanced clip encode - self.clp.cond_stage_model = self.clp.cond_stage_model.to("cuda") - if clip_encoder == "comfy -ignore below": - self.clp_encode = lambda x: CLIPTextEncode.encode(None, self.clp, x)[0] - self.pos_cond = self.clp_encode(self.positive) - self.neg_cond = self.clp_encode(self.negative) - else: - from nodes import CLIPTextEncodeAdvanced - self.clp_encode = lambda x: CLIPTextEncodeAdvanced.encode( - None, self.clp, x, token_normalization, weight_interpretation - )[0] - try: - self.pos_cond = self.clp_encode(self.positive) - self.neg_cond = self.clp_encode(self.negative) - except Exception as e: - print(e) - raise ValueError("advanced clip encoder failed") - - self.clp.cond_stage_model = self.clp.cond_stage_model.to("cpu") - torch.cuda.empty_cache() - gc.collect() - - if self.latent_image is None: - from nodes import EmptyLatentImage - self.latent_image = EmptyLatentImage().generate( - self.width, self.height, self.batch_size - )[0] - - image = None - # Only sample/decode if the user wants "latent" or "image" or "all" - if "latent" in render_what or "all" in render_what or "image" in render_what: - self.KSampler = ETKKSampler() - self.samples = self.KSampler.sample( - model=self.mdl, - seed=self.seed, - steps=self.steps, - cfg=self.cfg, - sampler_name=self.sampler_name, - scheduler=self.scheduler, - latent_image=self.latent_image, - pos_cond=self.pos_cond, - neg_cond=self.neg_cond, - positive=self.pos_cond, - negative=self.neg_cond, - denoise=self.denoise, - one_seed_per_batch=self.one_seed_per_batch - )[0] - - if "image" in render_what or "all" in render_what: - self.samples["samples"] = self.samples["samples"].to("cuda") - self.vae.first_stage_model = self.vae.first_stage_model.to("cuda:0") - self.vae.device = torch.device("cuda:0") - torch.cuda.empty_cache() - - decoded_images = self.vae.decode(self.samples["samples"]) - image = decoded_images.to("cpu") - - self.vae.first_stage_model.to("cpu") - self.vae.device = torch.device("cpu") - torch.cuda.empty_cache() - gc.collect() - - self.FUNC = self.tinytxt2img - self.ARGS = all_args - self.samples["samples"] = self.samples["samples"].to("cpu") - - ret = ( - image, - self, - self.samples, - self.pos_cond, - self.neg_cond - ) - - # If requested, unload - if unload_model: - del self.mdl - del self.clp - del self.vae - if "vision" in self.__dict__: - del self.vision - del self.samples - del self.FUNC - del self.ARGS - del self.pos_cond - del self.neg_cond - del self.KSampler - mm.free_memory(mm.get_total_memory(mm.get_torch_device()), mm.get_torch_device()) - torch.cuda.empty_cache() - gc.collect() - - return ret - - # If func_only is True, return a placeholder 5-tuple where the second item is our generator - if kwargs.get("func_only", False): - return (None, _do_generation, None, None, None) - else: - # Otherwise, perform the entire generation immediately and return the standard 5-tuple - return _do_generation() - -@ETK_image_base -class PromptTemplate: - """replaces the text in the given text string with a given other text at some key positions""" - - @classmethod - def INPUT_TYPES(s): - return {"required": - { - "text": ("STRING", {"multiline": True}), - "replacement": ("STRING", {"multiline": True}), - "key1": ("STRING", {"multiline": False}), - } - } - - CATEGORY = "text" - RETURN_TYPES = ("STRING",) - FUNCTION = "prompt_template_handler" - - def prompt_template_handler(self, text: str, replacement, key1): - """ - >>> PromptTemplate().prompt_template_handler("hello world", "universe", "world") - 'hello universe' - """ - return (text.replace(key1, replacement),) - - -@ETK_image_base -class LoadImage: - @classmethod - def INPUT_TYPES(s): - input_dir = folder_paths.get_input_directory() - return {"required": - {"image": (sorted(os.listdir(input_dir)),)}, - } - - CATEGORY = "ETK/image" - - RETURN_TYPES = ("IMAGE", "MASK", "IMAGE") - FUNCTION = "load_image_handler" - - def load_image_handler(self, image): - """ - >>> fp = './mouths.tif' - >>> results = LoadImage().load_image_handler(fp) - >>> torch_image_show(results[0][0]) - :param image: - :return: - """ - if image.endswith(".xcf"): - return self.load_xcf(image) - elif image.endswith(".tif"): - return self.load_tif(image) - else: - return self.load_image(image) - - def load_xcf(self, image): - pass - - def load_tif(self, image): - input_dir = folder_paths.get_input_directory() - image_path = os.path.join(input_dir, image) - img = Image.open(image_path) - out = [] - - # first find the maximum width and height - max_width = 0; - max_height = 0 - for i in range(img.n_frames): - img.seek(i) - max_width = max(max_width, img.width) - max_height = max(max_height, img.height) - - # now create a new tensor with the max width and height - out = torch.zeros((img.n_frames, max_height, max_width, 4), dtype=torch.float) - - for i in range(img.n_frames): - img.seek(i) - np_arr = np.array(img).astype(np.float32) / 255.0 - - padx = max_width - img.width - pady = max_height - img.height - np_arr = np.pad(np_arr, ((0, pady), (0, padx), (0, 0)), mode='constant') - - t_arr = torch.tensor(np_arr) - out[i] = t_arr - - mask = out[:, :, :, 2:3] - out_rgb = out[:, :, :, :3] - out_rgba = out - - return (out_rgb, mask, out_rgba,) - - def load_image(self, image): - input_dir = folder_paths.get_input_directory() - image_path = os.path.join(input_dir, image) - i = Image.open(image_path) - - img_rgba = Image.open(image_path) - img_rgba = img_rgba.convert("RGBA") - img_rgba = np.array(img_rgba).astype(np.float32) / 255.0 - img_rgba = torch.from_numpy(img_rgba)[None,] - - image = i.convert("RGB") - image = np.array(image).astype(np.float32) / 255.0 - image = torch.from_numpy(image)[None,] - if 'A' in i.getbands(): - mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 - mask = 1. - torch.from_numpy(mask) - else: - mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") - return (image, mask, img_rgba) - - @classmethod - def IS_CHANGED(s, image): - input_dir = folder_paths.get_input_directory() - image_path = os.path.join(input_dir, image) - m = hashlib.sha256() - try: - with open(image_path, 'rb') as f: - m.update(f.read()) - except FileNotFoundError: - return True - return m.digest().hex() - - -@ETK_image_base -class SelectFromRGBSimilarity: - """ - selects pixel positions based on similarity to a given color - """ - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - - "r": ("FLOAT", { - "default": 0.500, - "min": 0.000, - "max": 1.000, - "step": 0.001 - }), - "g": ("FLOAT", { - "default": 0.500, - "min": 0.000, - "max": 1.000, - "step": 0.001 - }), - "b": ("FLOAT", { - "default": 0.500, - "min": 0.000, - "max": 1.000, - "step": 0.001 - }), - "similarity": ("FLOAT", { - "default": 0.500, - "min": 0.000, - "max": 1.000, - "step": 0.001 - }) - } - } - - CATEGORY = "ETK/image" - - RETURN_TYPES = ("IMAGE",) - - FUNCTION = "select_from_rgb_similarity" - - def select_from_rgb_similarity(self, image: torch.Tensor, r, g, b, similarity: float = 0.5): - """ - >>> image = torch.rand((1, 64, 64,3)) - >>> color = [0.5, 0.5, 0.5] - >>> similarity = 0.5 - >>> mask = SelectFromRGBSimilarity.select_from_rgb_similarity(None,image,color[0],color[1],color[2],similarity)[0] - #>>> torch_image_show(mask) - >>> mask.shape - torch.Size([1, 64, 64, 3]) - >>> mask.mean() - tensor(1.) - - >>> image = torch.rand((2, 64, 64,3)) - >>> color = [0.5, 0.5, 0.5] - >>> similarity = 0.25 - >>> mask = SelectFromRGBSimilarity.select_from_rgb_similarity(None,image,color[0],color[1],color[2],similarity)[0] - #>>> torch_image_show(mask) - >>> mask.shape - torch.Size([1, 64, 64, 3]) - >>> mask.mean() - tensor(0.34) - - :param image: torch tensor of shape (B,W,H,C) - - :param similarity: array of 3 floats - :return: image mask of the positions where the color is similar white=selected black=not selected - """ - - batch_size, height, width, _ = image.shape - color = [r, g, b] - - result = torch.zeros_like(image) - # remember that the input image is BWHC - similarity = torch.tensor(similarity) - out = [] - for b in range(batch_size): - # just use the first 3 channels - use_image = image[b:b + 1, :, :, :3] - white = torch.ones_like(use_image) - - max_distance = torch.norm(torch.tensor([1.0, 1.0, 1.0])) - image_vector = torch.norm(use_image - torch.tensor([0, 0, 0]), dim=-1, keepdim=True) - - color_tensor = torch.tensor(color).to(image.device) - color_tensor = color_tensor[None, None, None, :] # Expand dimensions to match image shape for broadcasting - color_distance = torch.linalg.vector_norm(use_image - color_tensor, dim=-1, - keepdim=True) # Euclidean distance between color and pixels - color_distance = color_distance / max_distance - - mask_bool = color_distance < similarity - mask_white = white * mask_bool - out.append(mask_white) - - out = torch.cat(out, dim=0) - return (out,) - - -@ETK_image_base -class Quantize: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "image": ("IMAGE",), - "colors": ("INT", { - "default": 256, - "min": 1, - "max": 256, - "step": 1 - }), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "quantize" - - CATEGORY = "ETK/image" - - def quantize(self, image: torch.Tensor, colors: int = 256): - """ - >>> image = torch.rand((2, 512, 512,3)) - >>> colors = 256 - >>> quantized = Quantize.quantize(None,image,colors)[0] - >>> quantized.shape - torch.Size([2, 512, 512, 3]) - >>> image = torch.rand((2, 512, 512,4)) - >>> colors = 256 - >>> quantized = Quantize.quantize(None,image,colors)[0] - >>> quantized.shape - torch.Size([2, 512, 512, 4]) - - """ - batch_size, height, width, _ = image.shape - - result = torch.zeros_like(image) - - for b in range(batch_size): - tensor_image_start = image[b] - if tensor_image_start.shape[2] == 4: - tensor_image = image[b, :, :, :3] - else: - tensor_image = image[b] - - img = (tensor_image * 255).to(torch.uint8).numpy() - pil_image = Image.fromarray(img, mode='RGB') - - palette = pil_image.quantize( - colors=colors) # Required as described in https://github.com/python-pillow/Pillow/issues/5836 - quantized_image = pil_image.quantize(colors=colors, palette=palette, dither=0) - - rgb = quantized_image.convert("RGB") - quantized_array = torchvision.transforms.PILToTensor()(rgb).float() / 255 - - # fix the indices to b, w, h, c - quantized_array = quantized_array.permute(1, 2, 0) - - if tensor_image_start.shape[2] == 4: - result[b][:, :, :3] = quantized_array.to(image.dtype) - result[b][:, :, 3] = image[b, :, :, 3] - else: - result[b] = quantized_array.to(image.dtype) - - return (result,) - - -@ETK_image_base -class select_from_batch: - """selects a single image from a batch""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - - "index": ("INT", { - "default": 0, - "min": 0, - "max": 100, - "step": 1 - }), } - - , - "optional": { - "image": ("IMAGE",), - "latent": ("LATENT",), - }, - } - - CATEGORY = "ETK/image" - - RETURN_TYPES = ("IMAGE", "LATENT",) - - FUNCTION = "select_from_batch" - - def select_from_batch(self, index=0, image=None, latent=None): - """ - >>> image = torch.rand((1, 512, 512,3)) - >>> index = 0 - >>> selected = select_from_batch.select_from_batch(None,image,index) - >>> selected.shape - torch.Size([1, 512, 512, 3]) - >>> image = torch.rand((2, 512, 512,3)) - >>> selected = select_from_batch.select_from_batch(None,image,1) - >>> selected.shape - torch.Size([1, 512, 512, 3]) - - """ - olat = None - oimg = None - o_lat_dict = {} - if image is None: - print(latent["samples"].shape) - smpls = latent["samples"][index:index + 1, :, :, :].expand(1, -1, -1, -1).clone() - o_lat_dict["samples"] = smpls - olat = o_lat_dict - print(olat["samples"].shape) - else: - oimg = image[index:index + 1, :, :, :].clone() - print(oimg.shape) - - return (oimg, olat,) - - -@ETK_image_base -class ImageDistanceMask: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "x": ("INT", { - "default": 0, - "min": 0, - "max": 10240, - "step": 1 - }), - "y": ("INT", { - "default": 0, - "min": 0, - "max": 10240, - "step": 1 - }), - - "Z": ("FLOAT", { - "default": 1, - "min": -2, - "max": 2, - "step": .05 - }), - "aspect_ratio": ("FLOAT", { - "default": 1, - "min": 0, - "max": 2, - "step": .05 - }), - - "falloff_method": (["linear", "spherical"],), - - } - } - - CATEGORY = "ETK/image" - - RETURN_TYPES = ("IMAGE",) - - FUNCTION = "distance_mask_image" - - def distance_mask_image(self, image, x, y, falloff_method=None, Z=1, aspect_ratio=1.0): - B, H, W, C = image.shape - - # Create a meshgrid for the coordinates - grid_y, grid_x = torch.meshgrid(torch.arange(H), torch.arange(W)) - - # Adjust aspect ratio - grid_x = grid_x * aspect_ratio - x = x * aspect_ratio - - # Ensure grid_x and grid_y are on the same device and dtype as the image - grid_x, grid_y = grid_x.to(image.device).type(image.dtype), grid_y.to(image.device).type(image.dtype) - - # Calculate distance from (x, y) to each pixel - dist = torch.sqrt((grid_x - x) ** 2 + (grid_y - y) ** 2) - - # Normalize the distance to be between 0 and 1 - dist = (dist - dist.min()) / (dist.max() - dist.min()) - - if falloff_method: - if falloff_method == "linear": - dist = 1 - dist - elif falloff_method == "spherical": - dist = torch.sqrt(1 - dist ** 2) - - if Z < 0: - dist = 1 - dist - Z = abs(Z) - - dist = dist - (1 - Z) - dist = torch.clamp(dist, 0, 1) - - # Normalize again after the modifications - dist = (dist - dist.min()) / (dist.max() - dist.min()) - - # Extend dimensionality to match the input tensor (B, H, W, C) - dist = dist[None, ..., None].expand(B, H, W, C) - - return (dist,) - - -@ETK_image_base -class rgba_lower_clip: - """Clip RGBA values below a threshold""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "nrm_after": ("INT", { - "default": 0, - "min": 0, - "max": 1, - "step": 1 - - }), - "r": ("FLOAT", { - "default": 0.0, - "min": 0.0, - "max": 1.0, - "step": 0.01 - }), - "g": ("FLOAT", { - "default": 0.0, - "min": 0.0, - "max": 1.0, - "step": 0.01 - }), - "b": ("FLOAT", { - "default": 0.0, - "min": 0.0, - "max": 1.0, - "step": 0.01 - }), - "a": ("FLOAT", { - "default": 0.0, - "min": 0.0, - "max": 1.0, - "step": 0.01 - }), - - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "rgba_lower_clip" - - CATEGORY = "ETK/image" - - def rgba_lower_clip(self, image: torch.Tensor, nrm_after, r, g, b, a): - # image is (B,W,H,C) - image = image.clone() - # check for alpha channel, add if not present - - if image.shape[3] == 3: - alpha = torch.ones_like(image[:, :, :, 0]) - image = torch.cat((image, alpha.unsqueeze(3)), dim=3) - - image[:, :, :, 0] = torch.clamp(image[:, :, :, 0], min=r, max=1.0) - image[:, :, :, 1] = torch.clamp(image[:, :, :, 1], min=g, max=1.0) - image[:, :, :, 2] = torch.clamp(image[:, :, :, 2], min=b, max=1.0) - image[:, :, :, 3] = torch.clamp(image[:, :, :, 3], min=a, max=1.0) - - if int(nrm_after) == 1: - image = rgba_per_channel_norm(image) - - return (image,) - - -@ETK_image_base -class ImageStackBlendByMask: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "stack a": ("IMAGE",), - "stack b": ("IMAGE",), - "img mask": ("IMAGE",), - - }, - } - - RETURN_TYPES = ("IMAGE", "IMAGE",) - RETURN_NAMES = ("image", "image rgba") - FUNCTION = "stack_mask" - - CATEGORY = "ETK/Image" - - def stack_mask(self, **kwargs): - import torch - # use torch to blend two images by a mask - # stacks are already (B,W,H,C) - - stack_a = kwargs["stack a"] - stack_b = kwargs["stack b"] - img_mask = kwargs["img mask"] - # check if they all have alpha - if stack_a.shape[3] == 3: - alpha = torch.ones_like(stack_a[:, :, :, 0]) - stack_a = torch.cat((stack_a, alpha.unsqueeze(3)), dim=3) - - if stack_b.shape[3] == 3: - alpha = torch.ones_like(stack_b[:, :, :, 0]) - stack_b = torch.cat((stack_b, alpha.unsqueeze(3)), dim=3) - - # allocate new image - new_image = torch.zeros_like(stack_a) - - # blend rgb channels - - for i in range(3): - new_image[:, :, :, i] = \ - (stack_a[:, :, :, i] * img_mask[:, :, :, 0]) \ - + \ - (stack_b[:, :, :, i] * (1 - img_mask[:, :, :, 0])) - - # alpha should be the mean of the two alphas - new_image[:, :, :, 3] = (stack_a[:, :, :, 3] + stack_b[:, :, :, 3]) / 2 - rgba = new_image - rgb = new_image[:, :, :, 0:3] - - return (rgb, rgba,) - - -@ETK_image_base -class rgba_upper_clip: - """Clip RGBA values above a threshold""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "nrm_after": ("INT", { - "default": 0, - "min": 0, - "max": 1, - "step": 1 - - }), - "r": ("FLOAT", { - "default": 0.0, - "min": 0.0, - "max": 1.0, - "step": 0.01 - }), - "g": ("FLOAT", { - "default": 0.0, - "min": 0.0, - "max": 1.0, - "step": 0.01 - }), - "b": ("FLOAT", { - "default": 0.0, - "min": 0.0, - "max": 1.0, - "step": 0.01 - }), - "a": ("FLOAT", { - "default": 0.0, - "min": 0.0, - "max": 1.0, - "step": 0.01 - }), - - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "rgba_upper_clip" - - CATEGORY = "ETK/image" - - def rgba_upper_clip(self, image: torch.Tensor, nrm_after, r, g, b, a): - # image is (B,W,H,C) - image = image.clone() - # check for alpha channel, add if not present - - if image.shape[3] == 3: - alpha = torch.ones_like(image[:, :, :, 0]) - image = torch.cat((image, alpha.unsqueeze(3)), dim=3) - - image[:, :, :, 0] = torch.clamp(image[:, :, :, 0], min=0, max=r) - image[:, :, :, 1] = torch.clamp(image[:, :, :, 1], min=0, max=g) - image[:, :, :, 2] = torch.clamp(image[:, :, :, 2], min=0, max=b) - image[:, :, :, 3] = torch.clamp(image[:, :, :, 3], min=0, max=a) - - if int(nrm_after) == 1: - image = rgba_per_channel_norm(image) - - return (image,) - - -@ETK_image_base -class RGBA_MOD: - """provide sliders to modify RGBA values""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "r": ("FLOAT", { - "default": 1.0, - "min": 0.0, - "max": 2.0, - "step": 0.01 - }), - "g": ("FLOAT", { - "default": 1.0, - "min": 0.0, - "max": 2.0, - "step": 0.01 - }), - "b": ("FLOAT", { - "default": 1.0, - "min": 0.0, - "max": 2.0, - "step": 0.01 - }), - "a": ("FLOAT", { - "default": 1.0, - "min": 0.0, - "max": 2.0, - "step": 0.01 - }), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "rgba_mod" - - CATEGORY = "ETK/image" - - def rgba_mod(self, image: torch.Tensor, r, g, b, a): - """simply multiply the RGBA values by the sliders - images are (B, W, H, C) - """ - # check for alpha channel - if image.shape[3] == 3: - image = torch.cat((image, torch.ones_like(image[:, :, :, 0:1])), dim=3) - - image = image.clone() - image[:, :, :, 0] = image[:, :, :, 0] * r - image[:, :, :, 1] = image[:, :, :, 1] * g - image[:, :, :, 2] = image[:, :, :, 2] * b - image[:, :, :, 3] = image[:, :, :, 3] * a - - return (image,) - - -@ETK_image_base -class ImageBC: - """Normalize an images brightness,droping lower and higher values of n perctile""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "contrast": ("FLOAT", { - "default": 0.0, - "min": -10.0, - "max": 10.0, - "step": 0.01 - }), - "brightness": ("FLOAT", { - "default": 0.0, - "min": 0.0, - "max": 10.0, - "step": 0.01 - }), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "image_b_c" - - CATEGORY = "ETK/image" - - def image_b_c(self, image, contrast, brightness): - """ - Adjusts the contrast of an image, while preserving the alpha channel - - >>> image = (torch.rand(2, 256, 256, 4)/10)+.45 - >>> image[:,0,0,:] = torch.tensor([0.0,0.0,0.0,0.0]) - >>> image[:,1,1,:] = torch.tensor([1.0,1.0,1.0,1.0]) - >>> torch_image_show(image) - >>> normalized_image = ImageBC.image_b_c(None,image,2,2) - >>> torch_image_show(normalized_image) - >>> normalized_image.shape - torch.Size([2, 256, 256, 4]) - """ - # check for alpha channel - if image.shape[3] == 3: - image = torch.cat((image, torch.ones_like(image[:, :, :, 0:1])), dim=3) - - # get the image size - size = image.shape - - # Separate the alpha channel - img_a = image[..., -1:] - image = image[..., :-1] - - # Permute the dimensions to [..., 1 or 3, H, W] - image = image.permute(0, 3, 1, 2) - - # Adjust the contrast - image = torchvision.transforms.functional.adjust_brightness(image, brightness) - image = torchvision.transforms.functional.adjust_contrast(image, contrast) - - # Permute the dimensions back to [Batch, H, W, Channels] - image = image.permute(0, 2, 3, 1) - - # Concatenate the alpha channel back - image = torch.cat((image, img_a), dim=-1) - - return (image,) - - -@ETK_image_base -class PadToMatch: - """ a node that will pad the smaller of two images to match the size of the larger one """ - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "image1": ("IMAGE",), - "image2": ("IMAGE",), - "im2_x_center": ("FLOAT", { - "default": 0.5, - "min": 0.0, - "max": 1.0, - "step": 0.01 - }), - "im2_y_center": ("FLOAT", { - "default": 0.5, - "min": 0.0, - "max": 1.0, - "step": 0.01 - }), - }, - } - - RETURN_TYPES = ("IMAGE", "IMAGE") - FUNCTION = "padme" - - CATEGORY = "ETK/image" - - def padme(self, image1, image2, im2_x_center, im2_y_center): - """ - pads the smaller of two images to match the size of the larger one - x_center and y_center are how far to the right and down the smaller image should be placed - images are (B, W, H, C) - >>> PadToMatch.padme(None,torch.ones((1, 2, 2, 3)), torch.ones((1, 3, 3, 3)), 0.5, 0.5)[0].mean(axis=(3)) - tensor([[[1., 1., 0.], - [1., 1., 0.], - [0., 0., 0.]]]) - - - """ - target_width = max(image1.shape[1], image2.shape[1]) - target_height = max(image1.shape[2], image2.shape[2]) - - padded1 = torch.zeros((image1.shape[0], target_width, target_height, image1.shape[3])) - padded2 = torch.zeros((image2.shape[0], target_width, target_height, image2.shape[3])) - - pad_w_1 = target_width - image1.shape[1] - pad_h_1 = target_height - image1.shape[2] - - # img 1 is taken as the centered one - xs = int(pad_w_1 / 2) - xe = xs + image1.shape[1] - ys = int(pad_h_1 / 2) - ye = ys + image1.shape[2] - padded1[:, xs:xe, ys:ye, :] = image1 - - # img 2 is placed according to the centering parameters - pad_w_2 = target_width - image2.shape[1] - pad_h_2 = target_height - image2.shape[2] - - xs = int(pad_w_2 * im2_x_center) - ys = int(pad_h_2 * im2_y_center) - xe = xs + image2.shape[1] - ye = ys + image2.shape[2] - - if xe == 0: - xe = None - if ye == 0: - ye = None - padded2[:, xs:xe, ys:ye, :] = image2 - - return (padded1, padded2) - - -@ETK_image_base -class StackImages: - """ a node that will stack two images on top of each other """ - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "image1": ("IMAGE",), - }, - "optional": { - "image2": ("IMAGE",), - "number_of_images": ("INT", {"default": 2, "min": 2, "max": 100}), - # an optionbox with "all" or "upto", model after "one seed per batch" in the sampler - "mode": (["all", "one_for_one", "upto"], {"default": "all"}), - "number_of_repeats": ("INT", {"default": 1, "min": 1, "max": 100}), - - }, } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "stackme" - - CATEGORY = "ETK/image" - - def stackme(self, image1, image2=None, number_of_images=2, mode="all", number_of_repeats=1): - # import modules to help clear memory - import gc - import torch.cuda - - """ - stacks two images on top of each other - images are (B, W, H, C) torch tensors - """ - if image1 is not None: - image1 = image1.clone() - - if image2 is not None: - image2 = image2.clone() - - # gc.collect() - # torch.cuda.empty_cache() - - del gc.garbage[:] - gc.collect() - torch.cuda.empty_cache() - - # apply number of repeats - - if mode == "all" and image2 is not None: - image2 = image2.clone() - out = torch.cat((image1, image2), dim=0) - elif mode == "all" and image2 is None: - out = image1.clone() - elif mode == "one_for_one" and image2 is not None: - # for each image in image1, add each to the output - out_size = min(image1.shape[0], image2.shape[0]) - holder = torch.zeros((out_size, image1.shape[1], image1.shape[2], image1.shape[3])) - for i in range(out_size): - holder[i, :, :, :] = image2[i, :, :, :] - out = holder - elif mode == "one_for_one" and image2 is None: - # this might mean that the user whats each index repeated num times - out_size = number_of_repeats * image1.shape[0] - out = torch.zeros((out_size, image1.shape[1], image1.shape[2], image1.shape[3])) - for i in range(out_size): - out[i, :, :, :] = image1[i % image1.shape[0], :, :, :] - - elif mode == "upto" and image2 is not None: - # stack them all together but then only return the first number_of_images - out = torch.cat((image1, image2), dim=0) - out = out[:number_of_images, :, :, :] - - elif mode == "upto" and image2 is None: - out = image1.clone() - out = out[:number_of_images, :, :, :] - - if number_of_repeats > 1: - out = out.repeat(number_of_repeats, 1, 1, 1) - - # make sure to apply number of images - out = out[:number_of_images, :, :, :] - - torch.cuda.empty_cache() - gc.collect() - - return (out,) - - -@ETK_image_base -class rgba_merge: - """merges the channels of an RGBA image""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "r": ("IMAGE",), - "g": ("IMAGE",), - "b": ("IMAGE",), - "a": ("IMAGE",), - } - } - - CATEGORY = "ETK/image" - - RETURN_TYPES = ("IMAGE",) - - FUNCTION = "merge" - - def merge(self, r, g, b, a): - """ - r,g,b,a all may be 3, 1, or 4 channel images - >>> image = torch.rand((1, 512, 512,3)) - >>> r, g, b, a = rgba_split.split(None,image) - >>> rgba_merge.merge(None,r,g,b,a).shape - torch.Size([1, 512, 512, 4]) - :param r: - :param g: - :param b: - :param a: - :return: - """ - # use channel 0 of each channel in the resulting image - r = r[:, :, :, 0:1] - g = g[:, :, :, 0:1] - b = b[:, :, :, 0:1] - a = a[:, :, :, 0:1] - - # there will always be an alpha channel so just put the channels together - - return (torch.cat((r, g, b, a), dim=3),) - - -@ETK_image_base -class rgba_split: - """splits the channels of an RGBA image""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - } - } - - CATEGORY = "ETK/image" - - RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE") - - FUNCTION = "split" - - def split(self, image): - """ - >>> image = torch.rand((1, 512, 512,3)) - >>> r, g, b, a = rgba_split.split(None,image) - >>> r.shape - torch.Size([1, 1, 512, 512]) - :param image: - :return: - """ - # check for alhpa channel - if image.shape[3] == 3: - # add alpha channel - alpha = torch.ones((image.shape[0], image.shape[1], image.shape[2], 1)) - image = torch.cat((image, alpha), 3) - # for now each channel must be (b,w,h,3) so repeat the channel 3 times - r = image[:, :, :, 0].unsqueeze(3).repeat(1, 1, 1, 3) - g = image[:, :, :, 1].unsqueeze(3).repeat(1, 1, 1, 3) - b = image[:, :, :, 2].unsqueeze(3).repeat(1, 1, 1, 3) - a = image[:, :, :, 3].unsqueeze(3).repeat(1, 1, 1, 3) - - return (r, g, b, a,) - - -def get_image_size(image): - """ returns the width and height of an image """ - size = image.shape - width = int(size[0]) - height = int(size[1]) - return (width * height) - - -@ETK_image_base -class KSamplerCreateDataset: - """ use KSampler to create a dataset """ - - @classmethod - def INPUT_TYPES(cls): - inps = ETKKSampler.INPUT_TYPES() - # make sure to set a default - inps["required"]["dataset_name"] = ("STRING", {"default": "dataset"}) - return inps - - CATEGORY = "ETK/training" - - RETURN_TYPES = ("LATENT", "STRING",) - RETURN_NAMES = ("LATENT", "dataset_path",) - - FUNCTION = "create_dataset" - - def create_dataset(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, - denoise, one_seed_per_batch, dataset_name): - """ - creates a dataset using KSampler - :param model: - :param seed: - :param steps: - :param cfg: - :param sampler_name: - :param scheduler: - :param positive: - :param negative: - :param latent_image: - :param denoise: - :param dataset_name: - :return: - """ - # create the sampler - sampler = ETKKSampler() - - x = {"model": model, "seed": seed, "steps": steps, "cfg": cfg, "sampler_name": sampler_name, - "scheduler": scheduler, "positive": positive, "negative": negative, "latent_image": latent_image, - "denoise": denoise, "one_seed_per_batch": one_seed_per_batch} - - # now save x - # just use torch .save? - import torch - # create a random name - import hashlib - import time - - # create a random name - name = hashlib.md5(str(time.time()).encode()).hexdigest()[0:8] - name = dataset_name + "_" + name - - o = folder_paths.get_output_directory() - import os - - dataset_path = os.path.join(o, name) - torch.save(x, dataset_path + "_x.pt") - - results = sampler.sample(**x) - # save the results - torch.save(results[0], dataset_path + "_y.pt") - # return the dataset path - return (results[0], dataset_path,) - - -@ETK_image_base -class ETKKSampler: - @classmethod - def INPUT_TYPES(s): - from nodes import KSampler - ret = KSampler.INPUT_TYPES() - ret["required"]["one_seed_per_batch"] = ([True, False], {"default": False}) - return ret - - RETURN_TYPES = ("LATENT", "FUNC",) - FUNCTION = "sample" - - CATEGORY = "sampling" - - def sample(self, **kwargs): - from nodes import common_ksampler - # kwargs = deepcopy(kwargs) - - model = kwargs.get("model", None) - seed = kwargs.get("seed", None) - steps = kwargs.get("steps", None) - cfg = kwargs.get("cfg", None) - sampler_name = kwargs.get("sampler_name", None) - scheduler = kwargs.get("scheduler", None) - positive = kwargs.get("positive", None) - negative = kwargs.get("negative", None) - latent_image = kwargs.get("latent_image", None) - denoise = kwargs.get("denoise", None) - one_seed_per_batch = kwargs.get("one_seed_per_batch", None) - - # self.FUNC = lambda x: ETKKSampler.sample(self, **x) - # self.ARGS = kwargs - - try: - - ret = common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, - denoise=denoise, one_seed_per_batch=one_seed_per_batch) - except TypeError as e: - ret = common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, - denoise=denoise) - except RuntimeError as e: - raise e - ret = (ret[0], self,) - return ret - - -@ETK_image_base -class ListKSampler: - """ - A KSampler that takes a list for every input, if the list has one element it will be repeated for every sample - """ - - @classmethod - def INPUT_TYPES(cls): - s1 = ETKKSampler.INPUT_TYPES()["required"]["sampler_name"] - s2 = ETKKSampler.INPUT_TYPES()["required"]["scheduler"] - return { - "required": { - "model": ("MODEL",), - "seed": ("LIST",), - "steps": ("LIST",), - "cfg": ("LIST",), - "sampler_name": s1, - "scheduler": s2, - "positive": ("LIST",), - "negative": ("LIST",), - "latent_image": ("LIST",), - "denoise": ("LIST",), - "one_seed_per_batch": ("LIST",), - } - } - - RETURN_TYPES = ("LIST",) - FUNCTION = "sample" - - CATEGORY = "sampling" - - def sample(self, **kwargs): - import torch - import random - # figure out how many samples we need to generate - defaults = { - "seed": [-1], - "steps": [15], - "cfg": [1.0], - "denoise": [1.0], - "one_seed_per_batch": [False] - } - for k in defaults: - if k not in kwargs: - kwargs[k] = defaults[k] - num_samples = 1 - - for key in kwargs: - if key == "model": - continue - elif key == "sampler_name" or key == "scheduler": - kwargs[key] = [kwargs[key]] - if kwargs[key] is not None: - num_samples = max(num_samples, len(kwargs[key])) - - # create a list of dictionaries - samples = [] - for i in range(num_samples): - sample = {} - for key, v in kwargs.items(): - if v is None: - v = defaults[key] - - if not isinstance(kwargs[key], list) and key != "model": - kwargs[key] = [v] - if key == "model": - sample[key] = v - elif key == "seed": - if v in [[-1], [0], [None]]: - sample[key] = random.randint(0, int(10e10 + 1)) - else: - if isinstance(v,int): - sample[key] = v - if isinstance(v,list): - sample[key] = v[i] - elif len(v) == 1: - sample[key] = v[0] - else: - sample[key] = v[i] - samples.append(sample) - - # create a list of results - results = [] - local_ksampler = ETKKSampler() - for sample in samples: - results.append(local_ksampler.sample(**sample)[0]) - torch.cuda.empty_cache() - - return (results,) - - -@ETK_image_base -class ListCustomSampler: - """ - A Custom sampler that takes a list for every input, if the list has one element it will be repeated for every sample - """ - - @classmethod - def INPUT_TYPES(cls): - s1 = ETKKSampler.INPUT_TYPES()["required"]["sampler_name"] - s2 = ETKKSampler.INPUT_TYPES()["required"]["scheduler"] - return { - "required": { - "model": ("MODEL",), - "add_noise": ("BOOLEAN", {"default": True}), - "sampler": ("SAMPLER",), - "sigmas": ("SIGMAS",), - "noise_seed": ("LIST",), - "cfg": ("LIST",), - "positive": ("LIST",), - "negative": ("LIST",), - "latent_image": ("LIST",), - } - } - - RETURN_TYPES = ("LIST",) - FUNCTION = "sample" - - CATEGORY = "sampling" - - def sample(self, **kwargs): - import torch - # figure out how many samples we need to generate - num_samples = 1 - for key in kwargs: - if key == "model": - continue - elif key == "sampler_name" or key == "scheduler": - continue - if isinstance(kwargs[key], list): - num_samples = max(num_samples, len(kwargs[key])) - else: - pass - - # create a list of dictionaries - samples = [] - for i in range(num_samples): - sample = {} - for key in kwargs: - if not isinstance(kwargs[key], list) and key != "model": - kwargs[key] = [kwargs[key]] - if key == "model": - sample[key] = kwargs[key] - elif kwargs[key] is None: - sample[key] = None - elif len(kwargs[key]) == 1: - sample[key] = kwargs[key][0] - else: - sample[key] = kwargs[key][i] - samples.append(sample) - - # create a list of results - results = [] - from comfy_extras.nodes_custom_sampler import SamplerCustom - local_ksampler = SamplerCustom() - for sample in samples: - results.append(local_ksampler.sample(**sample)[0]) - torch.cuda.empty_cache() - - return (results,) - - -@ETK_image_base -class ListCLIPTextEncode: - """ - A CLIPTextEncode that takes a list for every input, if the list has one element it will be repeated for every sample - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text": ("LIST",), - "clip": ("LIST",), - - } - } - - RETURN_TYPES = ("LIST",) - FUNCTION = "encode" - - CATEGORY = "CLIP" - - def encode(self, **kwargs): - # figure out how many samples we need to generate - num_samples = 1 - for key in kwargs: - num_samples = max(num_samples, len(kwargs[key])) - - # create a list of dictionaries - samples = [] - for i in range(num_samples): - sample = {} - for key in kwargs: - if len(kwargs[key]) == 1: - sample[key] = kwargs[key][0] - else: - sample[key] = kwargs[key][i] - samples.append(sample) - - # create a list of results - results = [] - for sample in samples: - from nodes import CLIPTextEncode - results.append(CLIPTextEncode().encode(**sample)[0]) - - return (results,) - - -@ETK_image_base -class ListVAEDecode: - """ - A VAEDecode that takes a list for every input, if the list has one element it will be repeated for every sample - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "latent": ("LIST",), - - }, - "optional": { - "vae_list": ("LIST",), - "vae": ("VAE",), - - } - } - - RETURN_TYPES = ("LIST", "IMAGE",) - FUNCTION = "decode" - - CATEGORY = "VAE" - - def decode(self, **kwargs): - from nodes import VAEDecode - import torch - # figure out if we are using the list of vae or just one - if kwargs.get("vae_list") is None: - kwargs["vae"] = [kwargs.get("vae")] - else: - kwargs["vae"] = kwargs.pop("vae_list") - - if "vae_list" in kwargs: - kwargs.pop("vae_list") - - # figure out how many samples we need to generate - num_samples = 1 - for key in kwargs: - num_samples = max(num_samples, len(kwargs[key])) - - # create a list of dictionaries - samples = [] - for i in range(num_samples): - sample = {} - for key in kwargs: - if len(kwargs[key]) == 1: - sample[key] = kwargs[key][0] - else: - sample[key] = kwargs[key][i] - samples.append(sample) - - # create a list of results - results = [] - for sample in samples: - sample["samples"] = sample.pop("latent") - - results.append(VAEDecode().decode(**sample)[0]) - - # the results are a list of image tensors (BHWC) float 0-1 - # we need to stack them into a single tensor but first pre-allocte the tensor - # we need to figure out the size of the tensor, we know though that all images are the same size - size = results[0].shape - width = size[1] - height = size[2] - channels = size[3] - num_images = len(results) - - # also need to specify to make the final tensor on the CPU - - final_bsize = 0 - # find the bsize - # sometimes a result will actually be shape (>1, W, H, C) - # sometimes it will be (W, H, C) - # sometimes it will be (1, W, H, C) - - for i in range(num_images): - if len(results[i].shape) == 3: - results[i] = results[i].unsqueeze(0) - final_bsize += 1 - elif len(results[i].shape) == 4: - if results[i].shape[0] == 1: - results[i] = results[i] - final_bsize += 1 - else: - final_bsize += results[i].shape[0] - - # assign - final_tensor = torch.zeros((final_bsize, width, height, channels), device="cpu") - cur_idx = 0 - del i - while True: - - if len(results[cur_idx].shape) == 4: - if results[cur_idx].shape[0] == 1: - final_tensor[cur_idx] = results[cur_idx] - cur_idx += 1 - else: - for j in range(results[cur_idx].shape[0]): - res = results[cur_idx][j] - if len(res.shape) == 3: - res = res.unsqueeze(0) - final_tensor[cur_idx] = res - cur_idx += 1 - if cur_idx >= final_bsize: - break - - return (results, final_tensor,) - - -@ETK_image_base -class ListVAEEncode: - """ - A VAEEncode that takes a list for every input, if the list has one element it will be repeated for every sample - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("LIST",), - - }, - "optional": { - "vae_list": ("LIST",), - "vae": ("VAE",), - - } - } - - RETURN_TYPES = ("LIST",) - FUNCTION = "encode" - - CATEGORY = "VAE" - - def encode(self, **kwargs): - if kwargs.get("vae_list", None) is None: - kwargs["vae"] = [kwargs.get("vae")] - else: - kwargs["vae"] = kwargs.pop("vae_list") - - if "vae_list" in kwargs: - kwargs.pop("vae_list") - - # figure out how many samples we need to generate - num_samples = 1 - for key in kwargs: - num_samples = max(num_samples, len(kwargs[key])) - - # create a list of dictionaries - samples = [] - for i in range(num_samples): - sample = {} - for key in kwargs: - if len(kwargs[key]) == 1: - sample[key] = kwargs[key][0] - else: - sample[key] = kwargs[key][i] - samples.append(sample) - - # create a list of results - results = [] - for sample in samples: - results.append(VAEEncode().encode(**sample)[0]) - - return (results,) - - -@ETK_image_base -class ImageFromListIdx: - """ - A node that takes a list of images and an index and returns the image at that index - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "images": ("LIST", {"default": []}), - "index": ("INT", {"default": 0, "min": 0, "max": 10e10}), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "get_image" - - CATEGORY = "ETK/image" - - def get_image(self, images, index): - return (images[index],) - - -@ETK_image_base -class LatentFromListIdx: - """ - A node that takes a list of latents and an index and returns the latent at that index - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "latents": ("LIST", {"default": []}), - "index": ("INT", {"default": 0, "min": -10e10, "max": 10e10}), - } - } - - RETURN_TYPES = ("LATENT",) - FUNCTION = "get_latent" - - CATEGORY = "ETK/latent" - - def get_latent(self, latents, index): - return (latents[index],) - - -@ETK_image_base -class ListifyAnything: - """ - A node that takes anything and returns it in a list - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "anything": ("*",), - "repeat": ("INT", {"default": 1, "min": 1, "max": 100}), - } - } - - RETURN_TYPES = ("LIST",) - FUNCTION = "listify" - - CATEGORY = "ETK/other" - - def listify(self, anything, repeat): - return ([anything] * repeat,) - - -@ETK_image_base -class TextRender: - """ - renders text to an image - """ - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text": ("STRING", {"default": "Hello World!"}), - "x": ("INT", { - "default": 128, - "min": 0, - "max": 1024, - "step": 1 - }), - "y": ("INT", { - "default": 128, - "min": 0, - "max": 1024, - "step": 1 - }), - "width": ("INT", { - "default": 100, - "min": 1, - "max": 1024, - "step": 32 - }), - "height": ("INT", { - "default": 100, - "min": 1, - "max": 1024, - "step": 32 - }), - "font": (fonts,), - "size": ("INT", { - "default": 16, - "min": 1, - "max": 200, - "step": 1 - }), - "color": ("STRING", { - "default": "#000000" - }), - "func_only": ([True, False], {"default": False}), - "stroke fill": ("STRING", {"default": "#000000"}), - "stroke width": ("INT", {"default": 1, "min": 1, "max": 100, "step": 1}), - }, - "optional": { - "font_override": ("STRING", {"default": ""}), - } - } - - CATEGORY = "ETK/text" - - RETURN_TYPES = ("IMAGE", "IMAGE", "STRING", "FUNC",) - RETURN_NAMES = ("image", "image rgba", "possible_fonts", "FUNC",) - - FUNCTION = "render_text" - - def render_text(self, text, x, y, width, height, font='Arial', size=16, color='#888888', func_only=False, **kwargs): - """ - This function renders the provided text at specified location, with the given width and height. - The text is rendered in the provided font, size, and color. - """ - from PIL import ImageFont, ImageDraw - import psutil - process = psutil.Process(); - memory_info = process.memory_info(); - start_memory = memory_info.rss / (1024 * 1024) - font_override = kwargs.get("font_override", None) - - if font_override: - font = font_override - - sw = kwargs.get("stroke width", None) - sf = kwargs.get("stroke fill", None) - - def _wrap_text(font, line, max_width, d): - words = line.split(' ') - new_line = '' - lines = [] - for word in words: - temp_line = new_line + word + ' ' - - # w, _ = d.textsize(temp_line, font=font) - - # w, h = d.textsize(line, font=font) - (left, top, right, bottom) = d.textbbox((0, 0), text=temp_line, font=font) - w = right - left - h = bottom - top - previous_height = h # Update the previous height - - if w > max_width: - lines.append(new_line.strip()) - new_line = word + ' ' - else: - new_line = temp_line - - lines.append(new_line.strip()) - wrapped_line = '\n'.join(lines) - return wrapped_line - - def _render_text(font_name, image, size, text, x, y, allow_wrap=True, allow_shrink=True, sw=None, sf=None): - width = image.size[0] - d = ImageDraw.Draw(image) - font = ImageFont.truetype(font_name, size) - lines = text.split('\n') - - shrink_threshold = 24 # The minimum font size before we start wrapping text - line_spacing = 5 # The constant space between lines - y_offset = y - previous_height = 0 # Store the height of the previous line - - for i, line in enumerate(lines): - if line.strip() == '' or line.strip() == '\n': - # For blank lines, add a blank line of the previous line's height - y_offset += previous_height + line_spacing - else: - # w, h = d.textsize(line, font=font) - (left, top, right, bottom) = d.textbbox((0, 0), text=line, font=font) - w = right - left - h = bottom - top - previous_height = h # Update the previous height - - if w > width: - if allow_shrink: - new_font_size = int(size * width / w) - if new_font_size >= shrink_threshold: - font = ImageFont.truetype(font_name, new_font_size) - else: - # The font size is too small, start wrapping text - wrapped_line = _wrap_text(font, line, width, d) - y_offset = _render_text(font_name, - image, - size, - wrapped_line, x, y_offset + line_spacing, - allow_wrap=False, - allow_shrink=False, - sf=sf, - sw=sw) - continue # Skip to the next line - - if allow_wrap: - # Wrap the text - wrapped_line = _wrap_text(font, line, width, d) - y_offset = _render_text(font_name, - image, - new_font_size, - wrapped_line, - x, - y_offset + line_spacing, - allow_wrap=False, - allow_shrink=False, - sf=sf, - sw=sw) - continue # Skip to the next line - - # Draw the line - x_offset = x + (width - w) / 2 - if sw or sw: - d.text((x_offset, y_offset), line, font=font, fill="#FFFFFF", stroke_width=sw, stroke_fill=sf) - else: - d.text((x_offset, y_offset), line, font=font, fill="#FFFFFF") - - y_offset += h + line_spacing # Move y_offset to the bottom of the last line drawn plus the line spacing - # debug print the amount of free memory in torch - - return y_offset - - if func_only: - def lll(new_kwargs): - if "font" not in new_kwargs: - new_kwargs["font"] = kwargs.get("font_name", font) - if "color" not in new_kwargs: - new_kwargs["color"] = color - if "size" not in new_kwargs: - new_kwargs["size"] = size - if "text" not in new_kwargs: - new_kwargs["text"] = text - if "x" not in new_kwargs: - new_kwargs["x"] = x - if "y" not in new_kwargs: - new_kwargs["y"] = y - if "width" not in new_kwargs: - new_kwargs["width"] = width - if "height" not in new_kwargs: - new_kwargs["height"] = height - - return TextRender().render_text(**new_kwargs) - - return (None, None, None, lll,) - - font_name = kwargs.get("font_name", font) - - # Create an empty image with RGBA channels - image = Image.new('RGBA', (width, height)) - - _render_text(font_name, image, size, text, x, y, allow_shrink=True, sf=sf, sw=sw) - - # Convert the image to numpy array - image_array = np.array(image) - - # Convert the numpy array to PyTorch tensor and add batch dimension - image_tensor = torch.from_numpy(image_array).unsqueeze(0).float() / 255.0 - - # set the alpha to the first channel to make a mask - image_tensor[..., 3] = image_tensor[..., 0].to("cpu") - - # now set the text to the correct color - color_arr = torch.zeros(1, 1, 1, 3).to("cpu") - color_arr[0, 0, 0, 0] = int(color[1:3], 16) / 255.0 - color_arr[0, 0, 0, 1] = int(color[3:5], 16) / 255.0 - color_arr[0, 0, 0, 2] = int(color[5:7], 16) / 255.0 - # use color_arr to set the color - image_tensor[..., 0:3] *= color_arr - - # make a copy for the RGB image - image_rgb = image_tensor[..., :3] - # print(torch.cuda.memory_stats()) - del image - del image_array - del color_arr - - process = psutil.Process(); - memory_info = process.memory_info(); - end_memory = memory_info.rss / (1024 * 1024) - print("Memory used: {} MB".format(end_memory - start_memory)) - - image_rgb = image_rgb.to("cpu") - image_tensor = image_tensor.to("cpu") - list_of_possible_fonts_str = "\n".join(get_fonts()) - return (image_rgb, image_tensor, list_of_possible_fonts_str,) - - -@ETK_image_base -class StripAlphaChannel: - """ - This function strips the alpha channel from the image. - """ - - @classmethod - def INPUT_TYPES(cls): - reqd = {"required": {"image": ("IMAGE",)}} - return reqd - - RETURN_TYPES = ("IMAGE",) - RETURN_NAMES = ("image",) - - FUNCTION = "strip_alpha_channel" - - def strip_alpha_channel(self, image): - """ - This function strips the alpha channel from the image. - - >>> sac = StripAlphaChannel() - >>> result = sac.strip_alpha_channel(image) - >>> torch_image_show(result[0]) - """ - from copy import deepcopy - - return (deepcopy(image[..., :3]),) - - -@ETK_image_base -class FuncImageStackToImageStack: - """ runs exec on giving the user the input image stack and the ability to define the output image stack""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - req = {"required": { - "image": ("IMAGE",), - "code": ("STRING", {"multiline": True, "default": "y=x()"}) - }} - return req - - CATEGORY = "ETK/func" - RETURN_TYPES = ("IMAGE",) - FUNCTION = "func" - - def func(self, **kwargs): - func = kwargs.get("func", None) - code = kwargs.get("code", None) - my_globals = globals() - my_locals = locals() - - image_x = kwargs.get("image", None) - # gs = image_x.mean(dim=3).repeat(1, 1, 1, 3) - - # my_locals["x"] = func - my_locals["x_image"] = kwargs.get("image", None) - my_locals["x"] = kwargs.get("image", None) - - exec(code, my_globals, my_locals) - - y_image = my_locals.get("y_image", None) - y = my_locals.get("y", None) - - out_y = y_image if y_image is not None else y - - return (out_y,) - - -@ETK_image_base -class ScaleLatentChannelwise: - @classmethod - def INPUT_TYPES(s): - return {"required": { - "latent": ("LATENT",), - "w1": ("FLOAT", {"min": -2, "max": 2, "default": 1, "step": 0.05}), - "w2": ("FLOAT", {"min": -2, "max": 2, "default": 1, "step": 0.05}), - "w3": ("FLOAT", {"min": -2, "max": 2, "default": 1, "step": 0.05}), - "w4": ("FLOAT", {"min": -2, "max": 2, "default": 1, "step": 0.05}), - }} - - RETURN_TYPES = ("LATENT",) - - CATEGORY = "latent" - FUNCTION = "scale" - - def scale(self, latent, w1, w2, w3, w4): - # create a new dictionary to store the scaled latent - scaled_latent = {} - - # get the samples tensor from the latent - samples = latent["samples"] - - # apply scaling to each channel of the samples tensor using the corresponding weight tensor - scaled_samples = samples.clone() - scaled_samples[:, 0, :, :] *= w1 - scaled_samples[:, 1, :, :] *= w2 - scaled_samples[:, 2, :, :] *= w3 - scaled_samples[:, 3, :, :] *= w4 - - # assign the scaled samples tensor to the "samples" key of the scaled_latent dictionary - scaled_latent["samples"] = scaled_samples - - # return the scaled_latent dictionary - return (scaled_latent,) - - -@ETK_image_base -class ImageStackAndMatch: - # Define properties of the node - """ - Maintain the aspect ratio of multiple images by adding padding or by cropping, - then stack the images into a single torch tensor. - Takes up to 4 image stacks as input. - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image1": ("IMAGE",), - "image2": ("IMAGE",), - "scale_interp": ( - ["nearest", "bilinear", "bicubic", "area"], {"default": "area"}), - "pad_color": ("STRING", {"default": "#000000"}), - }, - "optional": { - "image3": ("IMAGE",), - "image4": ("IMAGE",), - "scale_type": (["SCL_UP", "SCL_DOWN"], {"default": "SCL_UP"}), - "pad_or_crop": (["PAD", "CROP"], {"default": "PAD"}), - } - } - - RETURN_TYPES = ("IMAGE",) - CATEGORY = "ETK/func" - FUNCTION = "process_images" - - def hex_to_rgb(self, hex_color): - hex_color = hex_color.lstrip("#") - return tuple(int(hex_color[i:i + 2], 16) / 255.0 for i in (0, 2, 4)) - - def pad_to_max_size(self, image, max_height, max_width, pad_value): - import torch.nn.functional as F - - _, h, w, c = image.shape - padding_left = (max_width - w) // 2 - padding_right = max_width - w - padding_left - padding_top = (max_height - h) // 2 - padding_bottom = max_height - h - padding_top - padding = (padding_left, padding_right, padding_top, padding_bottom) - return F.pad(image, padding, value=pad_value) - - def resize_image(self, image, target_height, target_width, scale_interp): - import torch.nn.functional as F - - _, h, w, _ = image.shape - scale_factor_height = target_height / h - scale_factor_width = target_width / w - if scale_interp == 'area': - scale_factor_min = min(scale_factor_height, scale_factor_width) - image = F.interpolate(image, scale_factor=scale_factor_min, mode='area', recompute_scale_factor=True, - align_corners=None) - else: - image = F.interpolate(image, size=(target_height, target_width), mode=scale_interp, align_corners=True) - return image - - def process_image(self, image, max_height, max_width, scale_type, pad_color, scale_interp, pad_or_crop): - _, h, w, _ = image.shape - pad_value = self.hex_to_rgb(pad_color) - - if scale_type == 'SCL_UP': - target_height = max_height if h < max_height else h - target_width = max_width if w < max_width else w - image = self.resize_image(image, target_height, target_width, scale_interp) - if pad_or_crop == 'PAD': - image = self.pad_to_max_size(image, max_height, max_width, pad_value) - else: - raise NotImplementedError('The scale down functionality is not yet implemented.') - - return image - - def process_images(self, image1, image2, image3=None, image4=None, scale_interp='area', - pad_color="#000000", scale_type='SCL_UP', pad_or_crop='PAD'): - images = [img for img in [image1, image2, image3, image4] if img is not None] - - # Determine max height and width - max_height = max(img.shape[1] for img in images) - max_width = max(img.shape[2] for img in images) - - # Process and resize images - for i, img in enumerate(images): - images[i] = self.process_image(img, max_height, max_width, scale_type, pad_color, scale_interp, pad_or_crop) - - # Stack images into a single tensor - stacked_images = torch.stack(images, dim=0) - return stacked_images - - -# Define a constant for MAX_RESOLUTION if needed -MAX_RESOLUTION = 4096 - - -@ETK_image_base -class TextInImage: - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "scale_factor": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 2.0, "step": 0.1}), - }, - } - - RETURN_TYPES = ("STRING",) - FUNCTION = "detect_text_batch" - - CATEGORY = "OCR" - - def detect_text_batch(self, image, scale_factor): - # calls the detect_text function for each image in the batch - results = [] - for i in range(image.shape[0]): - results.append(self.detect_text(image[i].unsqueeze(0), scale_factor)[0]) - - results = '\n\n'.join(results) - return (results,) - - def detect_text(self, image, scale_factor): - from .config import config_settings - tesse = config_settings.get("tesseract_location", None) - if tesse is None: - raise Exception("Tesseract location not set in config") - - import pytesseract - import cv2 - import numpy as np - pytesseract.pytesseract.tesseract_cmd = tesse - - # incomming tensor is (B, H, W, C) so convert it - image = image.permute(0, 3, 1, 2) - - # Convert the image to a numpy array - image = image[0].cpu().numpy() - image = np.transpose(image, (1, 2, 0)) - image = (image * 255).astype(np.uint8) - - # Resize the image - height, width, _ = image.shape - image = cv2.resize(image, (int(width * scale_factor), int(height * scale_factor))) - - # Convert the image to grayscale - gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) - - # Perform OCR on the image - detected_text = pytesseract.image_to_string(gray) - - return (detected_text,) - - -@ETK_image_base -class DetectTextLines: - @classmethod - def INPUT_TYPES(s): - return {"required": {"image": ("IMAGE",), - "threshold": ("FLOAT", {"default": 0.115, "min": 0.01, "max": 1.0, "step": 0.01})}} - - RETURN_TYPES = ("LIST",) - RETURN_NAMES = ("list of IMAGE",) - FUNCTION = "detect_lines" - - CATEGORY = "OCR" - - def detect_lines(self, image, threshold=0.115): - # Convert the image to (b, h, w, c) format for consistency - # image = image.permute(0, 2, 3, 1) - - # Convert the image to grayscale - grayscale_image = 0.299 * image[..., 0] + 0.587 * image[..., 1] + 0.114 * image[..., 2] - - # Sum the pixel values along the width to get a 1D tensor - sum_along_width = torch.sum(grayscale_image, dim=-1) - - # Normalize the sum to get a clearer indication of lines - normalized_sum = sum_along_width / torch.max(sum_along_width) - - # Find the y-coordinates where the normalized sum is above the threshold - line_coordinates = torch.where(normalized_sum >= threshold)[1] - - # Additional code for converting lines to bounding boxes - bounding_boxes = [] - start_y = None - end_y = None - sorted_coordinates = torch.sort(line_coordinates).values - - for i, coord in enumerate(sorted_coordinates): - if start_y is None: - start_y = coord - if i < len(sorted_coordinates) - 1: - if sorted_coordinates[i + 1] - coord > 1: - end_y = coord - else: - end_y = coord - if end_y is not None: - bounding_boxes.append([start_y.item(), end_y.item()]) - start_y = None - end_y = None - # at this point the channels are in 2nd dimension - # so we need to transpose them to the last dimension - # image = image.permute(0, 3, 1, 2) - - # Create image tensors for each bounding box - line_images = [image[:, y1:y2 + 1, :, :] for y1, y2 in bounding_boxes] - - return (line_images,) - - -@ETK_image_base -class TileImage: - """repeat an image in a 3x3 grid, useful for checking for tiling artifacts""" - - @classmethod - def INPUT_TYPES(self): - return {"required": {"image": ("IMAGE",)}} - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "tile_image" - - CATEGORY = "IMAGE" - - def tile_image(self, image): - import torch - - # image is already B,H,W,C - - # pad the image to make it a multiple of 3 - # height, width = image.shape[1:3] - # new_height = height + (3 - height % 3) - # new_width = width + (3 - width % 3) - # image = F.pad(image, (0, new_width - width, 0, new_height - height)) - - # repeat the image in each dimension - image = torch.cat([image, image, image], dim=1) - image = torch.cat([image, image, image], dim=2) - - # now the image is 3x3 with each 1/3 being the original image - # now the image shape is B,3H,3W,5 - - return (image,) - - -@ETK_image_base -class FloodFillNode: - node_id = "comfyui.flood_fill" - node_name = "Flood Fill" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), # Expecting a NumPy array or tensor - "start_x": ("INT", {"min": 0, "default": 0}), - "start_y": ("INT", {"min": 0, "default": 0}), - "target_color": ("STRING", {"default": "#FFFFFF"}), - "replacement_color": ("STRING", {"default": "#000000"}), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "execute" - - def __init__(self): - self.num_workers = None # Can be set to the number of processes to create - - @staticmethod - def hex_to_rgb(hex_color): - # Convert hex color string to RGB tuple - hex_color = hex_color.lstrip("#") - return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4)) - - def flood_fill(self, image, start_coords, target_color, replacement_color): - # The flood fill algorithm assumes 2D image input (single sample from the batch) - image = image.squeeze(0) # Remove the batch dimension assuming a single sample - height, width, _ = image.shape - - target_color = torch.tensor(target_color, dtype=torch.float32) - replacement_color = torch.tensor(replacement_color, dtype=torch.float32) - target_color = target_color.view(1, 1, 3) - replacement_color = replacement_color.view(1, 1, 3) - - start_x, start_y = start_coords - stack = [(start_x, start_y)] - - # Convert to pixel value range [0, 1] if the max value is greater than 1 - if image.max() > 1.0: - image = image / 255.0 - - visited = torch.zeros(height, width, dtype=torch.bool) - - while len(stack) > 0: - x, y = stack.pop() - if not (0 <= x < width and 0 <= y < height) or visited[y, x] or not torch.allclose(image[y, x], - target_color, atol=1e-6): - continue - visited[y, x] = True - image[y, x] = replacement_color - neighbors = [(x + dx, y + dy) for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]] - stack.extend(neighbors) - - image = image.unsqueeze(0) # Add the batch dimension back - return image - - def execute(self, images, start_x, start_y, target_color_hex, replacement_color_hex): - target_color = self.hex_to_rgb(target_color_hex) - replacement_color = self.hex_to_rgb(replacement_color_hex) - - # Normalize colors to [0, 1] range and convert to the tensor - target_color = [c / 255.0 for c in target_color] - replacement_color = [c / 255.0 for c in replacement_color] - - # Apply flood fill to each image in the batch - for i in range(images.shape[0]): - images[i] = self.flood_fill(images[i], (start_x, start_y), target_color, replacement_color) - - return images - - -@ETK_image_base -class DecorateHeightMap: - """ - Decorate a height map with input images at different heights specified by input list - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "height_map": ("IMAGE",), # Expecting a NumPy array or tensor - "images": ("IMAGE",), # Expecting a NumPy array or tensor - "heights": ("LIST", {"default": [0.5]}), - "feather": ("LIST", {"default": [0.05]}), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "execute" - - def execute(self, height_map, images, heights, feather=0.05): - """ - Args: - height_map: float values 0-1 1,h,w,3 - images: z,x,y,c - heights: list of floats 0-1 length of z-1 - feather: single float or a list of floats specifying the gradient width for feathering effect - Returns: - torch image 1,h,w,3 - """ - heights = heights.copy() - images = images[..., 0:3].clone() - height_map = height_map[..., 0:3].clone() - - # Ensure heights is a list and add boundary values - heights = [0.0] + heights + [1.0] - - # If feather is a single float, create a list of the same feather value for each transition - if isinstance(feather, float): - feather = [feather] * (len(heights) - 1) - - # so the length of th feather list must be at least 1 - # if there is one height then it should be length 1 - # if there are two heights then it should be length 2 - # if there are three heights then it should be length 3 - # etc - if len(feather) != len(heights) - 2: - raise ValueError("feather must be a single float or a list of floats of the same length as heights") - - # Check for dimension match between height_map and images - if height_map.shape[1:3] != images.shape[1:3]: - raise ValueError("height_map and images must have the same height and width") - - # Convert height map to 2D - height_map_2d = height_map.squeeze(0)[..., 0] - - # Initialize output with the first texture - output = images[0].clone() - for i in range(1, len(heights) - 1): - current_feather = feather[i - 1] - # Calculate the mask for the current segment - mask = torch.sigmoid((height_map_2d - heights[i - 1]) / current_feather) * ( - 1 - torch.sigmoid((height_map_2d - heights[i]) / current_feather)) - mask = mask.unsqueeze(-1).repeat(1, 1, 3) # Expand mask to match image dimensions - - # Blend the output with the next texture using the mask - output = output * (1 - mask) + images[i] * mask - - # Normalize the output to ensure it's within the valid range of [0, 1] - output = torch.clamp(output, 0, 1).unsqueeze(0) - return (output,) - - -@ETK_image_base -class RandomImageFromFolder: - """ - Randomly select an image from a folder - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "folder": ("STRING", {"default": "./output"}), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "execute" - - def execute(self, folder): - """ - Args: - folder: string - Returns: - torch image 1,h,w,3 - """ - import random - # Get a list of all files in the folder - files = os.listdir(folder) - - # Filter out non-image files - files = [f for f in files if f.endswith(".jpg") or f.endswith(".png")] - - # Select a random file - file = random.choice(files) - - # Load the image - image = Image.open(os.path.join(folder, file)) - - # Convert to torch tensor - image = torch.from_numpy(np.array(image)).float() / 255.0 - image = image.unsqueeze(0) - - return (image,) - - -@ETK_image_base -class ZoomInImage: - """ - Zoom in on an image by a specified factor and center point - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "zoom_factor": ("FLOAT", {"default": 2.0, "min": .01, "max": 1000.0, "step": 0.1}), - "center_x": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), - "center_y": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), - "interpolation": (["nearest", "bilinear", "bicubic", "area"], {"default": "bilinear"}), - "keep_size": (["True", "False"], {"default": "True"}), - "animate": ("BOOLEAN", {"default": False}), - "exponential_zoom": ("BOOLEAN", {"default": False}), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "zoom_in" - - CATEGORY = "ETK/image" - - def zoom_in(self, image, zoom_factor, center_x, center_y, interpolation, keep_size, animate, exponential_zoom): - batch_size, height, width, channels = image.shape - frames = [] - - for frame in range(batch_size): - percent_done = frame / batch_size - if exponential_zoom: - percent_done = percent_done ** 2 - - # Calculate the zoom factor for this image - frame_zoom_factor = 1 + (zoom_factor * percent_done) - - # Calculate the new dimensions after zooming - new_height = int(height / frame_zoom_factor) - new_width = int(width / frame_zoom_factor) - - # Calculate the coordinates of the top-left corner of the zoomed region - top = int((height - new_height) * center_y) - left = int((width - new_width) * center_x) - - # Extract the zoomed region from the image - zoomed_image = image[frame, top:top + new_height, left:left + new_width, :].unsqueeze(0) - - if keep_size == "True": - # change order of dims for interpolation - zoomed_image = zoomed_image.permute(0, 3, 1, 2) - # Resize the zoomed region back to the original dimensions - zoomed_image = torch.nn.functional.interpolate(zoomed_image, size=(height, width), mode=interpolation, - align_corners=False) - # change order of dims back - zoomed_image = zoomed_image.permute(0, 2, 3, 1) - - frames.append(zoomed_image) - - # Stack all frames into a 4D tensor - frames = torch.cat(frames, dim=0) - - return (frames,) - - -@ETK_image_base -class ListUpscaleLatentBy: - """uses from nodes import LatentUpscaleBy""" - - @classmethod - def INPUT_TYPES(cls): - from nodes import LatentUpscaleBy - tmp = LatentUpscaleBy.INPUT_TYPES()["required"]["upscale_method"] - return { - "required": { - "latents": ("LIST", {"default": None}), - "upscale_method": tmp, - - }, - "optional": { - "factors": ("LIST", {"default": None}), - "factor": ("FLOAT", {"default": 1.5, "min": 0.0, }), - } - } - - RETURN_TYPES = ("LIST",) - FUNCTION = "upscale" - - CATEGORY = "latent" - - def upscale(self, latents, upscale_method, factors=None, factor=1.5): - from nodes import LatentUpscaleBy - - if not factors: - factors = [factor] * len(latents) - - # Check if latents and factors have the same length - if len(latents) != len(factors): - raise ValueError("Latents and factors must have the same length") - - # Apply LatentUpscaleBy to each pair of latent and factor - upscaled_latents = [LatentUpscaleBy().upscale(latent, upscale_method, factor)[0] for latent, factor in - zip(latents, factors)] - - return (upscaled_latents,) - - -@ETK_image_base -class SBSImage: - """ - Combine two images side by side - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image1": ("IMAGE",), - "image2": ("IMAGE",), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "execute" - - CATEGORY = "ETK/image" - - def execute(self, image1, image2): - """ - Args: - image1: torch image b,h,w,3 - image2: torch image b,h,w,3 - Returns: - torch image 1,h,w,3 - """ - - # Check if the images have the same height - if image1.shape[1] != image2.shape[1]: - raise ValueError("Images must have the same height") - - # Check if the images have the same number of channels - if image1.shape[3] != image2.shape[3]: - raise ValueError("Images must have the same number of channels") - - # Stack the images horizontally - output = torch.cat([image1, image2], dim=2) - - return (output,) - - -@ETK_image_base -class TTBImage: - """ - Combine two images top to bottom - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image1": ("IMAGE",), - "image2": ("IMAGE",), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "execute" - - CATEGORY = "ETK/image" - - def execute(self, image1, image2): - """ - Args: - image1: torch image b,h,w,3 - image2: torch image b,h,w,3 - Returns: - torch image 1,h,w,3 - """ - - # Check if the images have the same width - if image1.shape[2] != image2.shape[2]: - raise ValueError("Images must have the same width") - - # Check if the images have the same number of channels - if image1.shape[3] != image2.shape[3]: - raise ValueError("Images must have the same number of channels") - - # Stack the images vertically - output = torch.cat([image1, image2], dim=1) - - return (output,) - - -@ETK_image_base -class ImageGrid: - """ - Combine multiple images into a grid - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "rows": ("INT", {"default": 2, "min": 1}), - "cols": ("INT", {"default": 2, "min": 1}), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "execute" - - CATEGORY = "ETK/image" - - def get_img_idx(self, row, col, rows, cols): - return row * cols + col - - def get_xy_for_idx(self, idx, rows, cols): - row = idx % cols - col = idx // cols - return row, col - - def execute(self, image, rows, cols): - """ - Args: - image: image (r*c),h,w,3 - rows: int - cols: int - Returns: - torch image 1,h*r,w*c,3 - """ - - # Check if the number of images is equal to the number of rows times the number of columns - num_images = rows * cols - if len(image) < num_images: - # Calculate how many noise images are needed - num_noise_images = num_images - len(image) - # Create the required number of noise images - noise_shape = image[0].shape - noise_images = torch.randn((num_noise_images,) + noise_shape, dtype=image.dtype, device=image.device) - # Concatenate the noise images to the original images - image = torch.cat((image, noise_images), dim=0) - - # Check if each row has the same height for each image - for row in range(rows): - row_idxs = [self.get_img_idx(row, col, rows, cols) for col in range(cols)] - heights = [image[idx].shape[1] for idx in row_idxs] - if not all(height == heights[0] for height in heights): - raise ValueError("Images in the same row must have the same height") - - # Check if all columns of images have the same width - for col in range(cols): - col_idxs = [self.get_img_idx(row, col, rows, cols) for row in range(rows)] - widths = [image[idx].shape[2] for idx in col_idxs] - if not all(width == widths[0] for width in widths): - raise ValueError("Images in the same column must have the same width") - - col_widths = [] - for column in range(cols): - idx = self.get_img_idx(0, column, rows, cols) - col_width = image[idx].shape[1] - col_widths.append(col_width) - - col_heights = [] - for row in range(rows): - idx = self.get_img_idx(row, 0, rows, cols) - col_height = image[idx].shape[0] - col_heights.append(col_height) - - col_y_pix = [int(sum(col_heights[:i])) for i in range(len(col_heights))] - col_x_pix = [int(sum(col_widths[:i])) for i in range(len(col_widths))] - - total_height = sum(col_heights) - total_width = sum(col_widths) - - out_tensor = torch.zeros((1, total_height, total_width, image.shape[3]), dtype=image.dtype, - device=image.device) - - # check if there is not enough rows and cols to display all images - max_idx = rows * cols - tot_images = len(image) - if max_idx < tot_images: - raise ValueError("not enough grid space to show all images") - - for idx in range(len(image)): - x, y = self.get_xy_for_idx(idx, rows, cols) - x_pix = col_x_pix[x] - y_pix = col_y_pix[y] - h, w, _ = image[idx].shape - out_tensor[0, y_pix:y_pix + h, x_pix:x_pix + w, :] = image[idx] - - image.to("cpu") - del image - torch.cuda.empty_cache() - - return (out_tensor,) - - -@ETK_image_base -class QueryImage: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "prompt": ("STRING", {"multiline": True}), - "image": ("IMAGE",) - } - } - - FUNCTION = "execute" - - CATEGORY = "image_to_text" - RETURN_TYPES = ("STRING",) - - def execute(self, prompt, image): - from .modules.agent_commands import query_image - result = query_image.execute({"prompt": prompt, "image": image}) - - return (result["result"],) - - -@ETK_image_base -class CLIPSegAdv: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "text": ("STRING", {"default": "", "multiline": False}), - "use_cuda": ("BOOLEAN", {"default": False}), - }, - "optional": { - "clipseg_model": ("CLIPSEG_MODEL",), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "apply_transform" - - CATEGORY = "image/transformation" - - def x28_model(self, model, processor, num): - # takes an initialized model and processor and integrates it into a nn.module to run in paralell - from torch import nn - class x28_model(nn.Module): - def __init__(self, model, processor, overlap=64, slice_size=352): - super(x28_model, self).__init__() - self.model = model - self.processor = processor - self.mask = None - - # Create a blending mask - self.mask = np.ones((slice_size, slice_size)) - self.mask[:overlap, :] *= np.linspace(0, 1, overlap)[:, None] - self.mask[-overlap:, :] *= np.linspace(1, 0, overlap)[:, None] - self.mask[:, :overlap] *= np.linspace(0, 1, overlap)[None, :] - self.mask[:, -overlap:] *= np.linspace(1, 0, overlap)[None, :] - self.mask = torch.tensor(self.mask, dtype=torch.float32).unsqueeze(0).unsqueeze(0) - - def forward(self, image, text, boxs): - import torch - image = image.permute(0, 3, 1, 2).to(torch.float32) * 255 - out_image = image.clone() * 0 - - for i, bc in enumerate(boxs): - with torch.no_grad(): - # img = image[:, :, bc[0]:bc[1], bc[2]:bc[3]] - - inputs = processor(text=text, images=image[:, :, bc[0]:bc[1], bc[2]:bc[3]], return_tensors="pt") - - result = self.model(**inputs) - t = torch.sigmoid(result[0]) - mask = (t - t.min()) / t.max() - mask = mask.unsqueeze(0) - - mask = mask.repeat(1, 3, 1, 1) - out_image[:, :, bc[0]:bc[1], bc[2]:bc[3]] += (mask * self.mask) - - return out_image - - mdl = x28_model(model, processor, num) - return mdl - - def apply_transform(self, image, text, use_cuda, clipseg_model): - import torch - import torch.nn.functional as F - from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation - - B, H, W, C = image.shape - aspect = H / W - - if B != 1: - raise NotImplementedError("Batch size must be 1") - - # Desired slice size and overlap - slice_size = 352 - overlap = slice_size // 2 - overlap = 32 - - # num_slices_w, slices = self.get_slices(image, overlap, slice_size) - # just support landscape, square or portrait images - # 4x7=28, 5x5=25, 7x4=28 - if aspect < 1.25: - num_slices_w = 3 * 2 - num_slices_h = 2 * 2 - elif aspect > 0.75: - num_slices_w = 2 * 2 - num_slices_h = 3 * 2 - else: - num_slices_w = 3 * 2 - num_slices_h = 3 * 2 - - model, processor = self.get_models(clipseg_model, use_cuda) - - image_global = image.permute(0, 3, 1, 2) - image_global = F.interpolate(image_global, size=(num_slices_h * slice_size, num_slices_w * slice_size), - mode='bilinear', - align_corners=False) - image_global = image_global.permute(0, 2, 3, 1) - image_global = self.get_global_prediction(image_global, model, processor, slice_size, text, use_cuda) - - # _, slices = self.get_slices(image_global, overlap, slice_size) - slc_boxs = self.get_slice_boxs(image_global, overlap, slice_size) - - modelx28 = self.x28_model(model, processor, num=len(slc_boxs)) - - # Apply the transformation to each slice - transformed_image = modelx28.forward(image_global, text, slc_boxs) - - transformed_image = transformed_image.permute(0, 2, 3, 1) - - y = transformed_image - - total_power = (y + image_global) / 2 - just_black = image_global < 0.01 - - p1 = total_power > .5 - p2 = y > .5 - p3 = image_global > .5 - - condition = p1 | p2 | p3 - condition = condition & ~just_black - y = torch.where(condition, 1.0, 0.0) - - return (y,) - - def get_global_prediction(self, image, model, processor, slice_size, text, use_cuda): - from torch import nn - import torch.nn.functional as F - B, H, W, C = image.shape - - image_global = image.permute(0, 3, 1, 2) - image_global = F.interpolate(image_global, size=(slice_size, slice_size), mode='bilinear', align_corners=False) - image_global = image_global.permute(0, 2, 3, 1) - _, image_global = self.CLIPSeg_image(image_global.float(), text, processor, model, use_cuda) - image_global = image_global.permute(0, 3, 1, 2) - image_global = F.interpolate(image_global, size=(H, W), mode='bilinear', align_corners=False) - image_global = image_global.permute(0, 2, 3, 1) - return image_global - - def get_models(self, clipseg_model, use_cuda): - from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation - # Initialize CLIPSeg model and processor - if clipseg_model: - processor = clipseg_model[0] - model = clipseg_model[1] - else: - processor = CLIPSegProcessor.from_pretrained("CIDAS/clipseg-rd64-refined") - model = CLIPSegForImageSegmentation.from_pretrained("CIDAS/clipseg-rd64-refined") - # Move model to CUDA if requested - if use_cuda and torch.cuda.is_available(): - model = model.to('cuda') - processor.image_processor.do_rescale = True - processor.image_processor.do_resize = False - return model, processor - - def get_slices(self, image, overlap, slice_size): - B, H, W, C = image.shape - - # Calculate the number of slices needed along each dimension - num_slices_h = (H - overlap) // (slice_size - overlap) + 1 - num_slices_w = (W - overlap) // (slice_size - overlap) + 1 - # Prepare a list to store the slices - slices = [] - # Generate the slices - for i in range(num_slices_h): - for j in range(num_slices_w): - start_h = i * (slice_size - overlap) - start_w = j * (slice_size - overlap) - - end_h = min(start_h + slice_size, H) - end_w = min(start_w + slice_size, W) - - start_h = max(0, end_h - slice_size) - start_w = max(0, end_w - slice_size) - - slice_ = image[:, start_h:end_h, start_w:end_w, :] - print(slice_.shape) - slices.append(slice_) - return num_slices_w, slices - - def get_slice_boxs(self, image, overlap, slice_size): - B, H, W, C = image.shape - - # Calculate the number of slices needed along each dimension - num_slices_h = (H - overlap) // (slice_size - overlap) + 1 - num_slices_w = (W - overlap) // (slice_size - overlap) + 1 - # Prepare a list to store the slices - slices_boxs = [] - # Generate the slices - for i in range(num_slices_h): - for j in range(num_slices_w): - start_h = i * (slice_size - overlap) - start_w = j * (slice_size - overlap) - - end_h = min(start_h + slice_size, H) - end_w = min(start_w + slice_size, W) - - start_h = max(0, end_h - slice_size) - start_w = max(0, end_w - slice_size) - - bx = (start_h, end_h, start_w, end_w) - slices_boxs.append(bx) - print(bx) - return slices_boxs - - def CLIPSeg_image(self, image, text, processor, model, use_cuda): - import torch - import torchvision.transforms.functional as TF - B, H, W, C = image.shape - - import torchvision - with torch.no_grad(): - image = image.permute(0, 3, 1, 2).to(torch.float32) * 255 - - inputs = processor(text=[text] * B, images=image, padding=True, return_tensors="pt") - - # Move model and image tensors to CUDA if requested - if use_cuda and torch.cuda.is_available(): - model = model.to('cuda') - inputs = {k: v.to('cuda') if isinstance(v, torch.Tensor) else v for k, v in inputs.items()} - - result = model(**inputs) - t = torch.sigmoid(result[0]) - mask = (t - t.min()) / t.max() - mask = torchvision.transforms.functional.resize(mask, (H, W)) - mask = mask.unsqueeze(-1) - mask_img = mask.repeat(1, 1, 1, 3) - - # Move mask and mask_img back to CPU if they were moved to CUDA - if use_cuda and torch.cuda.is_available(): - mask = mask.cpu() - mask_img = mask_img.cpu() - - return (mask, mask_img,) - - -from typing import Tuple - - -@ETK_image_base -class PreviewImagePassThrough: # (PreviewImage): - @classmethod - def INPUT_TYPES(s): - from nodes import PreviewImage - # get the input types from the parent class - input_types = PreviewImage.INPUT_TYPES() - - return input_types - - OUTPUT_NODE = True - RETURN_TYPES = ("IMAGE",) - FUNCTION = "pass_through" - - def pass_through(self, **kwargs): - from nodes import PreviewImage - images = kwargs.pop("images", None) - # first call the parent class's save_image method - result = PreviewImage().save_images(images, **kwargs) - result["result"] = (images,) - return result - - -def main(): - t = CLIPSegAdv() - t.apply_transform(torch.rand(1, 1920, 1080, 3), "a", False, None) - - -if __name__ == "__main__": - main() diff --git a/issues/runtime_errors.md b/issues/runtime_errors.md deleted file mode 100644 index 98359db..0000000 --- a/issues/runtime_errors.md +++ /dev/null @@ -1,13 +0,0 @@ -# Runtime errors in PyTorch nodes - -The following problems were discovered: - -1. **Undefined helper method and variable** (#2) - - `TrainModel.train` calls `self.make_image_tensor()` which is not defined. - - Variable `best_model` may be used before assignment. -2. **Dataset handling in inference** (#3) - - `PyTorchInferenceNode.inference` assumes input dataset has a `.dataset` attribute, raising `AttributeError` for regular datasets. -3. **FlattenDataset assumes `.data`** (#4) - - `FlattenDataset.flatten` uses `dataset.data` which is not present in generic datasets. - -These should be addressed to prevent runtime failures. diff --git a/latent.py b/latent.py deleted file mode 100644 index b54b565..0000000 --- a/latent.py +++ /dev/null @@ -1,77 +0,0 @@ -#from nodes import MAX_RESOLUTION -import torch - -#from custom_nodes.EternalKernelLiteGraphNodes.components import fields as field - - -class LatentInterpolation: - def __init__(self) -> None: - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "latent1": ("LATENT",), - "latent2": ("LATENT",), - "t": ("FLOAT", {"default": 0.5}), - } - } - - RETURN_TYPES = ("LATENT",) - FUNCTION = "interpolate" - CATEGORY = "ETK/Latent" - - def interpolate(self, latent1, latent2, t): - # Ensure that the latents are dictionaries - if not (isinstance(latent1, dict) and isinstance(latent2, dict)): - raise ValueError("Latent values must be dictionaries.") - - # Ensure that both latents have the same keys - if set(latent1.keys()) != set(latent2.keys()): - raise ValueError("Latent dictionaries must have the same keys.") - - inter_latent = {} - for key in latent1: - if isinstance(latent1[key], (list, tuple)): - inter_latent[key] = [(1 - t) * l1_val + t * l2_val for l1_val, l2_val in - zip(latent1[key], latent2[key])] - elif isinstance(latent1[key], torch.Tensor): - inter_latent[key] = (1 - t) * latent1[key] + t * latent2[key] - else: - raise ValueError(f"Unsupported value type for key '{key}' in latent dictionaries.") - - return (inter_latent,) - - -def test_latent_interpolation(): - import torch - # Create an instance of the LatentInterpolation class - li = LatentInterpolation() - - # Define test cases for the interpolate method - # latents are at least 4 diminsional - # latents are of the same length - test_cases = [ - { - "input": { - "latent1": torch.tensor([[[[1.0, 2.0], [3.0, 4.0]]]]), - "latent2": torch.tensor([[[[5.0, 6.0], [7.0, 8.0]]]]), - "t": 0.5, - - }, - "expected": torch.tensor([[[[3.0, 4.0], [5.0, 6.0]]]]), - }, - ] - - # Run the test cases - for test_case in test_cases: - result = li.interpolate(**test_case["input"]) - if not (result[0][0] == test_case["expected"]).all().item(): - print(f"Expected {test_case['expected']} but got {result}") - - - - -if __name__ == "__main__": - test_latent_interpolation() \ No newline at end of file diff --git a/local_shared.py b/local_shared.py deleted file mode 100644 index 726ef1f..0000000 --- a/local_shared.py +++ /dev/null @@ -1,7 +0,0 @@ -import os - -# state that is shared, available to all modules - -ETK_PATH = os.path.dirname(os.path.abspath(__file__)) -# this is two levels down -GDE_PATH = os.path.normpath(os.path.join(ETK_PATH, "../../")) diff --git a/modules/config.json b/modules/config.json deleted file mode 100644 index 39c0ca6..0000000 --- a/modules/config.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "output_path": "output", - "logger_uri": null, - "run_name": "run", - "project_name": null, - "run_description": "\ud83d\udc38Coqui trainer run.", - "print_step": 25, - "plot_step": 100, - "model_param_stats": false, - "wandb_entity": null, - "dashboard_logger": "tensorboard", - "save_on_interrupt": true, - "log_model_step": null, - "save_step": 10000, - "save_n_checkpoints": 5, - "save_checkpoints": true, - "save_all_best": false, - "save_best_after": 10000, - "target_loss": null, - "print_eval": false, - "test_delay_epochs": 0, - "run_eval": true, - "run_eval_steps": null, - "distributed_backend": "nccl", - "distributed_url": "tcp://localhost:54321", - "mixed_precision": false, - "precision": "fp16", - "epochs": 1000, - "batch_size": 32, - "eval_batch_size": 16, - "grad_clip": 0.0, - "scheduler_after_epoch": true, - "lr": 0.001, - "optimizer": "radam", - "optimizer_params": null, - "lr_scheduler": null, - "lr_scheduler_params": {}, - "use_grad_scaler": false, - "allow_tf32": false, - "cudnn_enable": true, - "cudnn_deterministic": false, - "cudnn_benchmark": false, - "training_seed": 54321, - "model": "xtts", - "num_loader_workers": 0, - "num_eval_loader_workers": 0, - "use_noise_augment": false, - "audio": { - "sample_rate": 22050, - "output_sample_rate": 24000 - }, - "use_phonemes": false, - "phonemizer": null, - "phoneme_language": null, - "compute_input_seq_cache": false, - "text_cleaner": null, - "enable_eos_bos_chars": false, - "test_sentences_file": "", - "phoneme_cache_path": null, - "characters": null, - "add_blank": false, - "batch_group_size": 0, - "loss_masking": null, - "min_audio_len": 1, - "max_audio_len": Infinity, - "min_text_len": 1, - "max_text_len": Infinity, - "compute_f0": false, - "compute_energy": false, - "compute_linear_spec": false, - "precompute_num_workers": 0, - "start_by_longest": false, - "shuffle": false, - "drop_last": false, - "datasets": [ - { - "formatter": "", - "dataset_name": "", - "path": "", - "meta_file_train": "", - "ignored_speakers": null, - "language": "", - "phonemizer": "", - "meta_file_val": "", - "meta_file_attn_mask": "" - } - ], - "test_sentences": [], - "eval_split_max_size": null, - "eval_split_size": 0.01, - "use_speaker_weighted_sampler": false, - "speaker_weighted_sampler_alpha": 1.0, - "use_language_weighted_sampler": false, - "language_weighted_sampler_alpha": 1.0, - "use_length_weighted_sampler": false, - "length_weighted_sampler_alpha": 1.0, - "model_args": { - "gpt_batch_size": 1, - "enable_redaction": false, - "kv_cache": true, - "gpt_checkpoint": null, - "clvp_checkpoint": null, - "decoder_checkpoint": null, - "num_chars": 255, - "tokenizer_file": "", - "gpt_max_audio_tokens": 605, - "gpt_max_text_tokens": 402, - "gpt_max_prompt_tokens": 70, - "gpt_layers": 30, - "gpt_n_model_channels": 1024, - "gpt_n_heads": 16, - "gpt_number_text_tokens": 6153, - "gpt_start_text_token": null, - "gpt_stop_text_token": null, - "gpt_num_audio_tokens": 1026, - "gpt_start_audio_token": 1024, - "gpt_stop_audio_token": 1025, - "gpt_code_stride_len": 1024, - "gpt_use_masking_gt_prompt_approach": true, - "gpt_use_perceiver_resampler": true, - "input_sample_rate": 22050, - "output_sample_rate": 24000, - "output_hop_length": 256, - "decoder_input_dim": 1024, - "d_vector_dim": 512, - "cond_d_vector_in_each_upsampling_layer": true, - "duration_const": 102400 - }, - "model_dir": null, - "languages": [ - "en", - "es", - "fr", - "de", - "it", - "pt", - "pl", - "tr", - "ru", - "nl", - "cs", - "ar", - "zh-cn", - "hu", - "ko", - "ja" - ], - "temperature": 0.75, - "length_penalty": 1.0, - "repetition_penalty": 10.0, - "top_k": 50, - "top_p": 0.85, - "num_gpt_outputs": 1, - "gpt_cond_len": 30, - "gpt_cond_chunk_len": 4, - "max_ref_len": 30, - "sound_norm_refs": false -} \ No newline at end of file diff --git a/modules/fractal.py b/modules/fractal.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/image_utils.py b/modules/image_utils.py deleted file mode 100644 index 5e93b63..0000000 --- a/modules/image_utils.py +++ /dev/null @@ -1,441 +0,0 @@ -import torch -import torch.nn as nn -import cv2 -import time -from PIL import Image -import numpy as np -import scipy.ndimage - - -class MaskExpansionModel(nn.Module): - """ - A PyTorch model that expands or erodes a mask using the provided function. - """ - - def forward(self, mask, expand, tapered_corners): - assert mask.dim() == 4, "Input mask must have 3 dimensions (batch_size, height, width)." - bs = mask.size(0) - c = 0 if tapered_corners else 1 - kernel = torch.tensor([[c, 1, c], - [1, 1, 1], - [c, 1, c]], dtype=mask.dtype, device=mask.device) - kernel = kernel.view(1, 1, 3, 3) - - if expand < 0: - kernel = -kernel - expand = -expand - mask = 1 - mask - - mask = mask.unsqueeze(1) - # print(mask.shape) - for _ in range(expand): - for i in range(bs): - # print(i) - mask[i] = torch.nn.functional.conv2d(mask[i], kernel, padding=1) - mask[i] = torch.clamp(mask[i], 0, 1) # Clip the mask values to the range [0, 1] - - mask = mask.squeeze(1) - if expand < 0: - mask = 1 - mask - - return mask - - -class LanczosInterpolationLayer(nn.Module): - """ - A PyTorch module that applies Lanczos interpolation to resize a batch of images. - - Args: - target_size (tuple): The target size for resizing the images, in the format (width, height). - """ - - def __init__(self, target_size): - super(LanczosInterpolationLayer, self).__init__() - self.target_size = target_size - - def visualize_tensor(self,tensor, title): - from PIL import Image, ImageDraw, ImageFont - # Convert the tensor to a PIL image - tensor = tensor.squeeze().cpu().numpy() - tensor = (tensor * 255).astype('uint8') - image = Image.fromarray(tensor) - - # Create a new image for the text - text_image = Image.new('RGB', (200, 100), color='white') - draw = ImageDraw.Draw(text_image) - font = ImageFont.truetype("arial.ttf", 16) - - # Add the title, min, and max values to the text image - min_val = tensor.min() - max_val = tensor.max() - text = f"{title}\nMin: {min_val:.6f}\nMax: {max_val:.6f}" - draw.text((10, 10), text, fill='black', font=font) - - # Combine the original image and the text image - combined_image = Image.new('RGB', (image.width + text_image.width, max(image.height, text_image.height)), - color='white') - combined_image.paste(image, (0, 0)) - combined_image.paste(text_image, (image.width, 0)) - - # Display the combined image - combined_image.show() - def forward(self, x): - """ - Performs the forward pass of the Lanczos interpolation layer. - - Args: - x (torch.Tensor): A batch of input images with shape (batch_size, channels, height, width). - - Returns: - torch.Tensor: A batch of resized images with shape (batch_size, channels, target_height, target_width). - """ - device = torch.device("cpu") - interpolated_images = [] - for image in x: - # Convert the PyTorch tensor to a NumPy array - image_np = image.permute(1, 2, 0).to("cpu", torch.float32).numpy() - - # Resize the image using OpenCV's resize function with Lanczos interpolation - interpolated_image = cv2.resize(image_np, self.target_size, interpolation=cv2.INTER_LANCZOS4) - mn = interpolated_image.min() - mx = interpolated_image.max() - - color_range = mx - mn - - under_bottom_target_range = -10 // 255 - over_top_target_range = 10 // 255 - - #self.visualize_tensor(torch.from_numpy(interpolated_image), "Original Image") - - if color_range > 1: - # here we are going to compress the over the top values - over_top = interpolated_image > 1 - not_over_top = interpolated_image <= 1 - over_top_range = mx - 1 - over_top_scale = over_top_target_range / over_top_range - # scale the over the top values to the target range - interpolated_image[over_top] = 1 + (interpolated_image[over_top] - 1) * over_top_scale - #self.visualize_tensor(torch.from_numpy(interpolated_image), "After Over-the-Top Compression") - # scale the non-over-the top to make room - interpolated_image[not_over_top] = interpolated_image[not_over_top] * (1 - over_top_target_range) - #self.visualize_tensor(torch.from_numpy(interpolated_image), "After Non-Over-the-Top Scaling") - - # here we are going to compress the under the bottom values - under_bottom = interpolated_image < 0 - not_under_bottom = interpolated_image >= 0 - under_bottom_range = -mn - under_bottom_scale = under_bottom_target_range / under_bottom_range - # scale the under the bottom values to the target range - - #print("Max value:", interpolated_image.max()) - #print("Min value:", interpolated_image.min()) - - interpolated_image[under_bottom] = interpolated_image[under_bottom] * under_bottom_scale - #self.visualize_tensor(torch.from_numpy(interpolated_image), "After Under-the-Bottom Compression") - - # scale and shift the non-under-the-bottom to make room - #interpolated_image[not_under_bottom] = interpolated_image[not_under_bottom] * ( - # 1 - over_top_target_range) + under_bottom_target_range - - #self.visualize_tensor(torch.from_numpy(interpolated_image),"After Non-Under-the-Bottom Scaling and Shifting") - - #print("Max value:", interpolated_image.max()) - #print("Min value:", interpolated_image.min()) - - # clamp - interpolated_image = np.clip(interpolated_image, 0, 1) - - - else: - pass - - # Convert the resized image back to a PyTorch tensor - # interpolated_image = torch.from_numpy(interpolated_image).to(device, dtype=torch.float16).permute(2, 0, 1) - interpolated_image = torch.from_numpy(interpolated_image) - - # convert fix range to 0-255 - interpolated_image = interpolated_image * 255 - # convert to uint8 - interpolated_image = interpolated_image.to(device, dtype=torch.uint8).permute(2, 0, 1) - - interpolated_images.append(interpolated_image) - del image - del image_np - del interpolated_image - - # transform this list of tensors into a single tensor of shape (batch_size, channels, height, width) - - # pre allocate a tensor of shape (batch_size, channels, target_height, target_width) - y = torch.zeros((x.shape[0], interpolated_images[0].shape[0], self.target_size[1], self.target_size[0]), - device=device, - dtype=interpolated_images[0].dtype) - y = torch.stack(interpolated_images, dim=0, out=y) - del interpolated_images - - return y - - -class LanczosInterpolationModel(nn.Module): - """ - A PyTorch model that applies Lanczos interpolation to resize a batch of images. - - Args: - target_size (tuple): The target size for resizing the images, in the format (width, height). - """ - - def __init__(self, target_size): - super(LanczosInterpolationModel, self).__init__() - self.interpolation_layer = LanczosInterpolationLayer(target_size) - - def forward(self, x): - """ - Performs the forward pass of the Lanczos interpolation model. - - Args: - x (torch.Tensor): A batch of input images with shape (batch_size, channels, height, width). - - Returns: - torch.Tensor: A batch of resized images with shape (batch_size, channels, target_height, target_width). - """ - return self.interpolation_layer(x) - - -def expand_mask(mask, expand, tapered_corners, device=None, dtype=None): - """ - Convenience function for expanding or eroding a mask using the MaskExpansionModel. - - Args: - mask (torch.Tensor): The input mask tensor. - expand (int): The number of iterations to expand (positive) or erode (negative) the mask. - tapered_corners (bool): Whether to use tapered corners when expanding or eroding the mask. - device (torch.device, optional): The device to use for the model and input tensor. - If not provided, it will use the default device (CPU). - dtype (torch.dtype, optional): The data type to use for the input tensor. - If not provided, it will use the default dtype (float32). - - Returns: - torch.Tensor: The expanded or eroded mask tensor. - """ - # Create an instance of the MaskExpansionModel - model = MaskExpansionModel() - - # Move the model to the specified device (or default to CPU) - device = device or torch.device("cpu") - model = model.to(device) - - # Move the input tensor to the specified device and convert to the specified dtype - dtype = dtype or torch.float32 - mask = mask.to(device, dtype=dtype) - - # Expand or erode the mask using the MaskExpansionModel - with torch.no_grad(): - expanded_mask = model(mask, expand, tapered_corners) - - # Clear the GPU cache if using a GPU - if device != torch.device("cpu"): - torch.cuda.empty_cache() - - return expanded_mask - - -def lanczos_resize(image, target_size, device="cpu", dtype=torch.float32, output_type="tensor"): - """ - Convenience function for resizing an image using the Lanczos interpolation model. - - Args: - image (PIL.Image or torch.Tensor): The input image to be resized. - target_size (tuple): The target size for resizing the image, in the format (width, height). - device (torch.device, optional): The device to use for the model and input tensor. - If not provided, it will use the default device (CPU). - dtype (torch.dtype, optional): The data type to use for the input tensor. - If not provided, it will use the default dtype (float32). - output_type (str, optional): The type of the output image. - If "tensor", the function will return a PyTorch tensor. - If "pil", the function will return a PIL Image. - If not provided, it will return a tensor by default. - - Returns: - torch.Tensor or PIL.Image: The resized image. - """ - import gc - import os - import sys - if sys.platform == "win32": - import psutil - print(f"Free memory: {psutil.virtual_memory().available / 1024 / 1024} MB") - else: - # works on linux and mac - print(f"Free memory: {os.popen('free -m').readlines()[1].split()[3]} MB") - - # Create an instance of the LanczosInterpolationModel - model = LanczosInterpolationModel(target_size) - - # Move the model to the specified device (or default to CPU) - device = device or torch.device("cpu") - model = model.to(device) - - # Convert the input image to a PyTorch tensor - if isinstance(image, list): - if isinstance(image[0], Image.Image): - try: - images = np.array(image) - except: - images = np.array([np.array(img) for img in image]) - - images = torch.from_numpy(images).to(device, dtype).permute(0, 3, 1, 2) - elif isinstance(image, torch.Tensor): - images = image.to(device, dtype) - - del image - torch.cuda.empty_cache() - - # Resize the image using the Lanczos interpolation model - with torch.no_grad(): - resized_images = model(images) - - # Convert the resized tensor back to a PIL Image or keep it as a tensor based on the output_type - if output_type.lower() == "pil": - resized_images = resized_images.squeeze(0).permute(0, 2, 3, 1).cpu().numpy() - resized_images = Image.fromarray(resized_images.astype(np.uint8)) - elif output_type.lower() == "tensor": - pass # keep the resized_image as a tensor - else: - raise ValueError(f"Invalid output_type: {output_type}. Valid options are 'tensor' or 'pil'.") - - # Clear the GPU cache if using a GPU - if device != torch.device("cpu"): - torch.cuda.empty_cache() - - del images - del model - gc.collect() - # print current free memory on cpu, in a way that is cross platform 1 way that works on all - - if sys.platform == "win32": - import psutil - print(f"Free memory: {psutil.virtual_memory().available / 1024 / 1024} MB") - else: - # works on linux and mac - print(f"Free memory: {os.popen('free -m').readlines()[1].split()[3]} MB") - - return resized_images - - -def test_lanczos_interpolation_model(): - """ - Tests the Lanczos interpolation model by resizing a batch of random images and benchmarking the performance. - """ - # Set the target sizes for resizing - hd_size = (1920, 1080) - sd_size = (1280, 720) - - # Generate random PIL images - batch_size = 600 - hd_images = [Image.fromarray(np.random.randint(0, 256, size=(hd_size[1], hd_size[0], 3), dtype=np.uint8)) - for _ in range(batch_size)] - sd_images = [Image.fromarray(np.random.randint(0, 256, size=(sd_size[1], sd_size[0], 3), dtype=np.uint8)) - for _ in range(batch_size)] - - # Perform the benchmarks - num_iterations = 3 - - # HD to SD benchmark - start_time = time.time() - for _ in range(num_iterations): - sd_output_images = lanczos_resize(hd_images, sd_size) - end_time = time.time() - hd_to_sd_elapsed_time = end_time - start_time - hd_to_sd_avg_time_per_iteration = hd_to_sd_elapsed_time / num_iterations - - # SD to HD benchmark - start_time = time.time() - for _ in range(num_iterations): - hd_output_images = lanczos_resize(sd_images, hd_size) - end_time = time.time() - sd_to_hd_elapsed_time = end_time - start_time - sd_to_hd_avg_time_per_iteration = sd_to_hd_elapsed_time / num_iterations - - # Check the sizes of the output images - - assert sd_output_images.shape[0] == batch_size - - assert hd_output_images.shape[0] == batch_size - - print(f"Lanczos interpolation test passed!") - print(f"Benchmark results:") - print(f"Number of iterations: {num_iterations}") - print(f"HD to SD:") - print(f" Total elapsed time: {hd_to_sd_elapsed_time:.4f} seconds") - print(f" Average time per iteration: {hd_to_sd_avg_time_per_iteration:.4f} seconds") - print(f" Average time per HD image -> SD image: {hd_to_sd_avg_time_per_iteration / batch_size:.4f} seconds") - print(f"SD to HD:") - print(f" Total elapsed time: {sd_to_hd_elapsed_time:.4f} seconds") - print(f" Average time per iteration: {sd_to_hd_avg_time_per_iteration:.4f} seconds") - print(f" Average time per SD image -> HD image: {sd_to_hd_avg_time_per_iteration / batch_size:.4f} seconds") - - -def test_mask_expansion_model(): - """ - Tests the MaskExpansionModel by expanding and eroding random masks and benchmarking the performance. - """ - # Generate random masks - batch_size = 1000 - mask_size = (512, 512) - # masks are 0-1 tensors float - masks = torch.rand((batch_size, mask_size[0], mask_size[1])).to("cuda") - print(masks.min(), masks.max()) - - # Perform the benchmarks - num_iterations = 1 - expand_steps = 5 - erode_steps = -5 - - # Mask expansion benchmark - start_time = time.time() - for _ in range(num_iterations): - print(masks.mean()) - expanded_masks = expand_mask(masks, expand_steps, tapered_corners=True, device="cuda") - print(masks.mean()) - end_time = time.time() - expansion_elapsed_time = end_time - start_time - expansion_avg_time_per_iteration = expansion_elapsed_time / num_iterations - - # Mask erosion benchmark - start_time = time.time() - for _ in range(num_iterations): - print(masks.mean()) - eroded_masks = expand_mask(masks, erode_steps, tapered_corners=True, device="cuda") - print(eroded_masks.mean()) - end_time = time.time() - erosion_elapsed_time = end_time - start_time - erosion_avg_time_per_iteration = erosion_elapsed_time / num_iterations - - # Check the sizes of the output masks - expected_mask_size = (batch_size, mask_size[0], mask_size[1]) - assert expanded_masks.shape == expected_mask_size, f"Expected expanded mask size: {expected_mask_size}, but got: {expanded_masks.shape}" - assert eroded_masks.shape == expected_mask_size, f"Expected eroded mask size: {expected_mask_size}, but got: {eroded_masks.shape}" - - print(f"Mask expansion/erosion test passed!") - print(f"Benchmark results:") - print(f"Number of iterations: {num_iterations}") - print(f"Mask expansion:") - print(f" Total elapsed time: {expansion_elapsed_time:.4f} seconds") - print(f" Average time per iteration: {expansion_avg_time_per_iteration:.4f} seconds") - print(f" Average time per mask expansion: {expansion_avg_time_per_iteration / batch_size:.4f} seconds") - print(f"Mask erosion:") - print(f" Total elapsed time: {erosion_elapsed_time:.4f} seconds") - print(f" Average time per iteration: {erosion_avg_time_per_iteration:.4f} seconds") - print(f" Average time per mask erosion: {erosion_avg_time_per_iteration / batch_size:.4f} seconds") - - -if __name__ == "image_utils": - print("image_utils module loaded.") - print("Testing Lanczos interpolation model...") - test_lanczos_interpolation_model() - print("Testing Mask expansion model...") - # test_mask_expansion_model() - exit(0) - -if __name__ == "__main__": - test_lanczos_interpolation_model() - # test_mask_expansion_model() diff --git a/modules/musicgencli.py b/modules/musicgencli.py deleted file mode 100644 index 20193f1..0000000 --- a/modules/musicgencli.py +++ /dev/null @@ -1,224 +0,0 @@ -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'] - -import time # Add this at the top of your script - -def generate_music_for_description(description, duration, file, model_name, position): - time.sleep(position * 5) # Wait for position * 5 seconds - from audiocraft.models import MusicGen - from audiocraft.data.audio import audio_write - import torch - model = MusicGen.get_pretrained(model_name) - print(f"Generating music for description: {description}") - print(f"Using model: {model_name}") - print(f"Using duration: {duration}") - model.set_generation_params(duration=duration) - wav = model.generate([description]) - print(f"Writing to {file}") - audio_write(file.replace(".wav", ""), wav[0].cpu(), model.sample_rate, strategy="loudness") - del model - torch.cuda.empty_cache() - - -def generate_music(**kwargs): - from audiocraft.models import MusicGen - from audiocraft.data.audio import audio_write - import torch - import ast - import copy - import random - from concurrent.futures import ProcessPoolExecutor, as_completed - - - - torch.cuda.empty_cache() - - # Set seed, -1 means random seed - seed = kwargs.get('seed', -1) - if seed == -1: - seed = random.randint(0, 100000) - old_seed_values = set_all_seeds(seed) - - kwargs = copy.deepcopy(kwargs) - description = kwargs.get('description') - - file = kwargs.get('file') - # Add .wav if not there - if not file.endswith('.wav'): - file = file + ".wav" - - model_name = kwargs.get('model_name', 'melody') - duration = kwargs.get('duration', 10) - - descriptions = ast.literal_eval(description) - durations = ast.literal_eval(duration) - - - - o_files = [] - batch_size = 3 - with ProcessPoolExecutor(max_workers=batch_size) as executor: - futures = [] - for i, (description, duration) in enumerate(zip(descriptions, durations)): - ii = str(i).zfill(1 + len(str(1 + len(descriptions)))) - file_to_use = file.replace(".wav", f"_{ii}.wav") - # Pass i (the position in the batch) as an additional argument - future = executor.submit(generate_music_for_description, description, duration, file_to_use, model_name, i//batch_size) - futures.append((future, file_to_use)) - - for future, file_to_use in futures: - # Wait for the future to complete, no need to get the result since the function handles file writing - future.result() - o_files.append(file_to_use) - - # Reset seed - reset_seeds(old_seed_values) - torch.cuda.empty_cache() - - return (o_files[0], o_files,) - - -import argparse -import copy - - -def main(): - print("Running main function...") - # Define the model names as a list for the choices parameter - model_names = [ - "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", - ] - - # Parse command line arguments - parser = argparse.ArgumentParser(description='Generate music based on a given description.') - parser.add_argument('--description', help='Description of the music to generate') - parser.add_argument('--file', type=str, default='output.wav', help='Output file name (default: output.wav)') - parser.add_argument('--model_name', type=str, default='facebook/musicgen-large', choices=model_names, - help='Model name to use for generation') - parser.add_argument('--duration', help='Duration of the generated music in seconds (default: 10)') - - parser.add_argument('--seed', type=int, default=-1, - help='Seed for random number generation (default: -1 for random seed)') - parser.add_argument('--test', action='store_true', help='Run the test suite instead of generating music') - args = parser.parse_args() - - # Convert argparse.Namespace to dictionary - kwargs = vars(args) - - if args.test: - run_tests() - else: - print(f"Generating music with the following arguments: {kwargs}") - # Generate music - generated_files = generate_music(**kwargs) - print(f"Generated music files: {generated_files}") - - -def run_tests(): - print("Running test suite...") - # Define your test cases here - test_model_name_validation() - test_file_generation() - # Add more tests as needed - print("All tests passed!") - - -def test_model_name_validation(): - print("Testing model name validation...") - model_names = [ - "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", - ] - - description = "A brief test piece" # Example description for testing - duration = 5 # 5 seconds long - file_template = "test_output_{}.wav" # Template for generated file names - - for i, model_name in enumerate(model_names): - print(f"Testing {model_name}...") - kwargs = { - 'description': description, - 'duration': duration, - 'model_name': model_name, - 'file': file_template.format(i), # Ensure unique file names - 'seed': 42 # Fixed seed for consistency, if needed - } - - try: - # Assuming generate_music is adapted to work with these arguments - generate_music(**kwargs) - print(f"Successfully tested {model_name}") - except Exception as e: - print(f"Error testing {model_name}: {e}") - # Handle the error appropriately - for example, you might want to fail the test or log the error. - - print("Model name validation test completed.") - - -def test_file_generation(): - # Example test function - print("Testing file generation...") - # Implement the logic to test music file generation - # This could involve generating a small piece of music and checking if the file exists - # ... - - -if __name__ == "__main__": - main() diff --git a/modules/noise.py b/modules/noise.py deleted file mode 100644 index 04b33cb..0000000 --- a/modules/noise.py +++ /dev/null @@ -1,87 +0,0 @@ -# TODO: replace pyfastnoisesimd with pyfastnoiselite (works with rpi) -import pyfastnoisesimd as fns - - -def get_perlin_2d(width, height, scale, octaves=6, lacunarity=3, gain=.5, seed=0, offset=0, workers=4) -> any: - """ - Returns a 2D fractal perlin noise array of size (width, height) with the given parameters. - """ - - noise = fns.Noise(seed=seed, numWorkers=workers) - noise.frequency = scale - # noise.fractal.fractalType = fns.FractalType.RigidMulti - noise.fractal.octaves = octaves - noise.fractal.lacunarity = lacunarity - noise.fractal.gain = gain - noise.noiseType = fns.NoiseType.PerlinFractal - noise.fractal.offset = offset - - return noise.genAsGrid([width, height]) - - -def get_perlin_2d_as_torch_image_stack(width, height, scale, octaves=6, lacunarity=3, gain=.5, seed=0, offset=0, - workers=4, norm_zero_to_1=True) -> any: - """ - just uses get_perlin_2d and converts it to a torch image stack - - (B,H,W,C=3) format - """ - - import torch - import numpy as np - - noise = get_perlin_2d(width, height, scale, octaves, lacunarity, gain, seed, offset, workers) - # noise = np.transpose(noise, (2, 0, 1)) - noise = torch.from_numpy(noise) - # noise = noise.unsqueeze(0) - noise = torch.reshape(noise, (1, height, width, 1)) - # now stack it 3 times - # noise = torch.normal(noise) - # use torch to put it from 0-1 - # noise = torch.sigmoid(noise) - - - - # normalize it using torch - - if norm_zero_to_1: - # noise = noise.view(noise.size(0), -1) - noise -= noise.min() - noise /= noise.max() - - # noise = noise + 0.5 - # noise = noise / 1.0 - - return noise - - -if __name__ == '__main__': - import matplotlib.pyplot as plt - import time - - for i in range(10): - start = time.time() - # noise = get_perlin_2d(1024, 1024, 0.001, i + 1, 3, .5, 1, 0) - # noise = get_perlin_2d(1024*10, 1024*10, i/5, i + 1, 3, .5, i, 0) - # print(type(noise)) - # print(f"Time taken: {time.time() - start}") - # print the min and max - # print(f"Min: {noise.min()} Max: {noise.max()}") - # plt.imshow(noise, cmap='gray') - # plt.show() - - start = time.time() - for i in range(10): - # now test the torch version - noise = get_perlin_2d_as_torch_image_stack(1024, - 1024, - 0.001, - 6, - 3, - .5, - 1, - 0, - 4, - 2) - - print(f"Time taken: {time.time() - start}") diff --git a/modules/speaker.mp3 b/modules/speaker.mp3 deleted file mode 100644 index 1d00a90..0000000 Binary files a/modules/speaker.mp3 and /dev/null differ diff --git a/modules/test.speech b/modules/test.speech deleted file mode 100644 index c098f5b..0000000 --- a/modules/test.speech +++ /dev/null @@ -1,3 +0,0 @@ -This is a test. -Of the emergency AI TTS system. -This is only a test. \ No newline at end of file diff --git a/modules/vocab.json b/modules/vocab.json deleted file mode 100644 index 5af6dd3..0000000 --- a/modules/vocab.json +++ /dev/null @@ -1,14470 +0,0 @@ -{ - "version": "1.0", - "truncation": null, - "padding": null, - "added_tokens": [ - { - "id": 0, - "special": true, - "content": "[STOP]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 1, - "special": true, - "content": "[UNK]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 2, - "special": true, - "content": "[SPACE]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 259, - "special": true, - "content": "[en]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 260, - "special": true, - "content": "[de]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 261, - "special": true, - "content": "[START]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 262, - "special": true, - "content": "[fr]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 284, - "special": true, - "content": "[es]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 285, - "special": true, - "content": "[it]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 286, - "special": true, - "content": "[pt]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 294, - "special": true, - "content": "[pl]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 295, - "special": true, - "content": "[tr]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 267, - "special": true, - "content": "[ru]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 293, - "special": true, - "content": "[cs]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 297, - "special": true, - "content": "[nl]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 5022, - "special": true, - "content": "[ar]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 5023, - "special": true, - "content": "[zh-cn]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 5412, - "special": true, - "content": "[ja]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 5753, - "special": true, - "content": "[hu]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - }, - { - "id": 6152, - "special": true, - "content": "[ko]", - "single_word": false, - "lstrip": false, - "rstrip": false, - "normalized": false - } - ], - "normalizer": null, - "pre_tokenizer": { - "type": "Whitespace" - }, - "post_processor": null, - "decoder": null, - "model": { - "type": "BPE", - "dropout": null, - "unk_token": "[UNK]", - "continuing_subword_prefix": null, - "end_of_word_suffix": null, - "fuse_unk": false, - "vocab": { - "[STOP]": 0, - "[UNK]": 1, - "[SPACE]": 2, - "!": 3, - "'": 4, - "(": 5, - ")": 6, - ",": 7, - "-": 8, - ".": 9, - "/": 10, - ":": 11, - ";": 12, - "?": 13, - "a": 14, - "b": 15, - "c": 16, - "d": 17, - "e": 18, - "f": 19, - "g": 20, - "h": 21, - "i": 22, - "j": 23, - "k": 24, - "l": 25, - "m": 26, - "n": 27, - "o": 28, - "p": 29, - "q": 30, - "r": 31, - "s": 32, - "t": 33, - "u": 34, - "v": 35, - "w": 36, - "x": 37, - "y": 38, - "z": 39, - "th": 40, - "in": 41, - "the": 42, - "an": 43, - "er": 44, - "ou": 45, - "re": 46, - "on": 47, - "at": 48, - "ed": 49, - "en": 50, - "to": 51, - "ing": 52, - "and": 53, - "is": 54, - "as": 55, - "al": 56, - "or": 57, - "of": 58, - "ar": 59, - "it": 60, - "es": 61, - "he": 62, - "st": 63, - "le": 64, - "om": 65, - "se": 66, - "be": 67, - "ad": 68, - "ow": 69, - "ly": 70, - "ch": 71, - "wh": 72, - "that": 73, - "you": 74, - "li": 75, - "ve": 76, - "ac": 77, - "ti": 78, - "ld": 79, - "me": 80, - "was": 81, - "gh": 82, - "id": 83, - "ll": 84, - "wi": 85, - "ent": 86, - "for": 87, - "ay": 88, - "ro": 89, - "ver": 90, - "ic": 91, - "her": 92, - "ke": 93, - "his": 94, - "no": 95, - "ut": 96, - "un": 97, - "ir": 98, - "lo": 99, - "we": 100, - "ri": 101, - "ha": 102, - "with": 103, - "ght": 104, - "out": 105, - "im": 106, - "ion": 107, - "all": 108, - "ab": 109, - "one": 110, - "ne": 111, - "ge": 112, - "ould": 113, - "ter": 114, - "mo": 115, - "had": 116, - "ce": 117, - "she": 118, - "go": 119, - "sh": 120, - "ur": 121, - "am": 122, - "so": 123, - "pe": 124, - "my": 125, - "de": 126, - "are": 127, - "but": 128, - "ome": 129, - "fr": 130, - "ther": 131, - "fe": 132, - "su": 133, - "do": 134, - "con": 135, - "te": 136, - "ain": 137, - "ere": 138, - "po": 139, - "if": 140, - "they": 141, - "us": 142, - "ag": 143, - "tr": 144, - "now": 145, - "oun": 146, - "this": 147, - "have": 148, - "not": 149, - "sa": 150, - "il": 151, - "up": 152, - "thing": 153, - "from": 154, - "ap": 155, - "him": 156, - "ack": 157, - "ation": 158, - "ant": 159, - "our": 160, - "op": 161, - "like": 162, - "ust": 163, - "ess": 164, - "bo": 165, - "ok": 166, - "ul": 167, - "ind": 168, - "ex": 169, - "com": 170, - "some": 171, - "there": 172, - "ers": 173, - "co": 174, - "res": 175, - "man": 176, - "ard": 177, - "pl": 178, - "wor": 179, - "way": 180, - "tion": 181, - "fo": 182, - "ca": 183, - "were": 184, - "by": 185, - "ate": 186, - "pro": 187, - "ted": 188, - "ound": 189, - "own": 190, - "would": 191, - "ts": 192, - "what": 193, - "qu": 194, - "ally": 195, - "ight": 196, - "ck": 197, - "gr": 198, - "when": 199, - "ven": 200, - "can": 201, - "ough": 202, - "ine": 203, - "end": 204, - "per": 205, - "ous": 206, - "od": 207, - "ide": 208, - "know": 209, - "ty": 210, - "very": 211, - "si": 212, - "ak": 213, - "who": 214, - "about": 215, - "ill": 216, - "them": 217, - "est": 218, - "red": 219, - "ye": 220, - "could": 221, - "ong": 222, - "your": 223, - "their": 224, - "em": 225, - "just": 226, - "other": 227, - "into": 228, - "any": 229, - "whi": 230, - "um": 231, - "tw": 232, - "ast": 233, - "der": 234, - "did": 235, - "ie": 236, - "been": 237, - "ace": 238, - "ink": 239, - "ity": 240, - "back": 241, - "ting": 242, - "br": 243, - "more": 244, - "ake": 245, - "pp": 246, - "then": 247, - "sp": 248, - "el": 249, - "use": 250, - "bl": 251, - "said": 252, - "over": 253, - "get": 254, - "ß": 255, - "ä": 256, - "ö": 257, - "ü": 258, - "[en]": 259, - "[de]": 260, - "[START]": 261, - "[fr]": 262, - "œ": 263, - "ï": 264, - "ê": 265, - "â": 266, - "[ru]": 267, - "ÿ": 268, - "è": 269, - "à": 270, - "ë": 271, - "ù": 272, - "î": 273, - "ç": 274, - "æ": 275, - "ô": 276, - "û": 277, - "á": 278, - "é": 279, - "í": 280, - "ó": 281, - "ú": 282, - "ñ": 283, - "[es]": 284, - "[it]": 285, - "[pt]": 286, - "ń": 287, - "ś": 288, - "ę": 289, - "ą": 290, - "ż": 291, - "ć": 292, - "[cs]": 293, - "[pl]": 294, - "[tr]": 295, - "ã": 296, - "[nl]": 297, - "ş": 298, - "ğ": 299, - "ı": 300, - "ò": 301, - "ì": 302, - "¿": 303, - "…": 304, - "i̇": 305, - "õ": 306, - "\"": 307, - "´": 308, - "ø": 309, - "č": 310, - "ō": 311, - "š": 312, - "ž": 313, - "̇": 314, - "ei": 315, - "ich": 316, - "ein": 317, - "au": 318, - "sch": 319, - "und": 320, - "die": 321, - "da": 322, - "den": 323, - "gen": 324, - "zu": 325, - "hr": 326, - "ten": 327, - "mi": 328, - "sie": 329, - "das": 330, - "eine": 331, - "icht": 332, - "ber": 333, - "ach": 334, - "auf": 335, - "lich": 336, - "nicht": 337, - "mm": 338, - "ben": 339, - "war": 340, - "mit": 341, - "sich": 342, - "ig": 343, - "aus": 344, - "ist": 345, - "wie": 346, - "och": 347, - "ung": 348, - "ann": 349, - "ür": 350, - "hn": 351, - "ihr": 352, - "sen": 353, - "tz": 354, - "dem": 355, - "eit": 356, - "hat": 357, - "wir": 358, - "von": 359, - "wei": 360, - "ier": 361, - "ra": 362, - "einen": 363, - "vor": 364, - "als": 365, - "wo": 366, - "rei": 367, - "ste": 368, - "lie": 369, - "auch": 370, - "du": 371, - "des": 372, - "ko": 373, - "über": 374, - "bei": 375, - "hen": 376, - "hm": 377, - "lei": 378, - "aber": 379, - "wen": 380, - "hl": 381, - "ger": 382, - "nach": 383, - "ft": 384, - "imm": 385, - "je": 386, - "schen": 387, - "wer": 388, - "ser": 389, - "än": 390, - "sein": 391, - "ol": 392, - "cht": 393, - "für": 394, - "kl": 395, - "ff": 396, - "einem": 397, - "nen": 398, - "ja": 399, - "noch": 400, - "hatte": 401, - "pf": 402, - "hin": 403, - "di": 404, - "chen": 405, - "rü": 406, - "iel": 407, - "sel": 408, - "dass": 409, - "ihn": 410, - "mir": 411, - "schl": 412, - "ön": 413, - "gan": 414, - "gt": 415, - "einer": 416, - "sten": 417, - "mich": 418, - "wenn": 419, - "ell": 420, - "gte": 421, - "mal": 422, - "gel": 423, - "ken": 424, - "nur": 425, - "mmen": 426, - "fü": 427, - "ern": 428, - "ör": 429, - "unter": 430, - "ander": 431, - "dur": 432, - "uch": 433, - "ta": 434, - "men": 435, - "mach": 436, - "doch": 437, - "durch": 438, - "os": 439, - "gl": 440, - "hal": 441, - "ihre": 442, - "wä": 443, - "immer": 444, - "ihm": 445, - "kann": 446, - "ort": 447, - "dann": 448, - "lan": 449, - "tzt": 450, - "oder": 451, - "hren": 452, - "et": 453, - "kön": 454, - "ick": 455, - "fa": 456, - "wieder": 457, - "daß": 458, - "mein": 459, - "fen": 460, - "ganz": 461, - "diese": 462, - "ster": 463, - "dar": 464, - "wa": 465, - "ges": 466, - "na": 467, - "fl": 468, - "igen": 469, - "sche": 470, - "ungen": 471, - "mehr": 472, - "ßen": 473, - "ot": 474, - "kon": 475, - "gew": 476, - "haben": 477, - "geh": 478, - "ät": 479, - "sind": 480, - "dr": 481, - "wel": 482, - "uns": 483, - "vo": 484, - "ma": 485, - "ute": 486, - "schon": 487, - "bes": 488, - "gesch": 489, - "bt": 490, - "che": 491, - "son": 492, - "ob": 493, - "la": 494, - "rück": 495, - "seine": 496, - "kr": 497, - "fre": 498, - "eil": 499, - "zum": 500, - "hier": 501, - "kt": 502, - "ige": 503, - "spr": 504, - "leben": 505, - "bst": 506, - "zeit": 507, - "gro": 508, - "denn": 509, - "ho": 510, - "scha": 511, - "bar": 512, - "alle": 513, - "gegen": 514, - "wür": 515, - "mü": 516, - "ze": 517, - "werden": 518, - "jetzt": 519, - "kommen": 520, - "nie": 521, - "sei": 522, - "heit": 523, - "soll": 524, - "glei": 525, - "meine": 526, - "woll": 527, - "ner": 528, - "habe": 529, - "wur": 530, - "lichen": 531, - "assen": 532, - "nte": 533, - "sehen": 534, - "wird": 535, - "bis": 536, - "gar": 537, - "ien": 538, - "mus": 539, - "uß": 540, - "är": 541, - "stell": 542, - "keit": 543, - "zwei": 544, - "selbst": 545, - "sta": 546, - "pa": 547, - "sagte": 548, - "tet": 549, - "kam": 550, - "ssen": 551, - "viel": 552, - "ug": 553, - "zen": 554, - "hei": 555, - "mann": 556, - "will": 557, - "geb": 558, - "waren": 559, - "ück": 560, - "äch": 561, - "mer": 562, - "ru": 563, - "hau": 564, - "eigen": 565, - "ang": 566, - "weg": 567, - "blick": 568, - "fra": 569, - "alles": 570, - "ka": 571, - "augen": 572, - "fin": 573, - "liche": 574, - "unser": 575, - "dern": 576, - "herr": 577, - "nun": 578, - "vie": 579, - "chte": 580, - "wohl": 581, - "fall": 582, - "ht": 583, - "ün": 584, - "etwas": 585, - "stand": 586, - "äu": 587, - "mö": 588, - "tel": 589, - "rie": 590, - "dich": 591, - "dies": 592, - "hand": 593, - "bin": 594, - "ffen": 595, - "nichts": 596, - "dan": 597, - "hne": 598, - "ihnen": 599, - "esen": 600, - "dieser": 601, - "frau": 602, - "art": 603, - "dir": 604, - "isch": 605, - "erst": 606, - "gleich": 607, - "komm": 608, - "hör": 609, - "ße": 610, - "dig": 611, - "sehr": 612, - "zei": 613, - "sam": 614, - "aum": 615, - "hät": 616, - "ingen": 617, - "gut": 618, - "mut": 619, - "cken": 620, - "konnte": 621, - "stimm": 622, - "zur": 623, - "itz": 624, - "weil": 625, - "würde": 626, - "fä": 627, - "können": 628, - "keine": 629, - "fer": 630, - "ischen": 631, - "voll": 632, - "eines": 633, - "setz": 634, - "zie": 635, - "del": 636, - "tete": 637, - "seiner": 638, - "ieren": 639, - "gest": 640, - "zurück": 641, - "wurde": 642, - "schn": 643, - "pr": 644, - "ließ": 645, - "tra": 646, - "mä": 647, - "gend": 648, - "fol": 649, - "ik": 650, - "schla": 651, - "schaft": 652, - "ater": 653, - "weiß": 654, - "seinen": 655, - "lassen": 656, - "lu": 657, - "unden": 658, - "teil": 659, - "neu": 660, - "iert": 661, - "menschen": 662, - "hmen": 663, - "str": 664, - "gi": 665, - "sah": 666, - "ihren": 667, - "eln": 668, - "weiter": 669, - "gehen": 670, - "iger": 671, - "macht": 672, - "tag": 673, - "also": 674, - "halten": 675, - "nis": 676, - "acht": 677, - "geben": 678, - "og": 679, - "nat": 680, - "mar": 681, - "det": 682, - "ohne": 683, - "haus": 684, - "tro": 685, - "ange": 686, - "lau": 687, - "spiel": 688, - "tre": 689, - "schr": 690, - "inn": 691, - "los": 692, - "machen": 693, - "hätte": 694, - "beg": 695, - "wirk": 696, - "alt": 697, - "glich": 698, - "tes": 699, - "richt": 700, - "freund": 701, - "ihrer": 702, - "fel": 703, - "bel": 704, - "sol": 705, - "einmal": 706, - "eben": 707, - "hol": 708, - "hän": 709, - "tern": 710, - "hö": 711, - "schw": 712, - "recht": 713, - "wahr": 714, - "seinem": 715, - "stehen": 716, - "hlen": 717, - "ins": 718, - "ging": 719, - "wollte": 720, - "wissen": 721, - "ungs": 722, - "ald": 723, - "ass": 724, - "jahr": 725, - "mor": 726, - "welt": 727, - "under": 728, - "zusa": 729, - "kopf": 730, - "lang": 731, - "hinter": 732, - "atz": 733, - "stra": 734, - "angen": 735, - "ank": 736, - "ade": 737, - "glau": 738, - "fach": 739, - "hatten": 740, - "fort": 741, - "eicht": 742, - "iff": 743, - "ler": 744, - "mei": 745, - "diesem": 746, - "kein": 747, - "frei": 748, - "führ": 749, - "vom": 750, - "β": 751, - "ai": 752, - "ait": 753, - "que": 754, - "les": 755, - "av": 756, - "ais": 757, - "oi": 758, - "eu": 759, - "lle": 760, - "par": 761, - "ans": 762, - "ment": 763, - "ét": 764, - "une": 765, - "pas": 766, - "qui": 767, - "elle": 768, - "dé": 769, - "pour": 770, - "dans": 771, - "ré": 772, - "tou": 773, - "vous": 774, - "vi": 775, - "ouv": 776, - "mon": 777, - "sur": 778, - "ci": 779, - "plu": 780, - "ère": 781, - "mais": 782, - "ois": 783, - "plus": 784, - "ée": 785, - "aient": 786, - "mp": 787, - "lui": 788, - "ave": 789, - "était": 790, - "ses": 791, - "tout": 792, - "oir": 793, - "avait": 794, - "és": 795, - "mes": 796, - "nous": 797, - "eux": 798, - "bi": 799, - "ons": 800, - "pu": 801, - "ces": 802, - "tu": 803, - "leur": 804, - "don": 805, - "eur": 806, - "ette": 807, - "aire": 808, - "avec": 809, - "dit": 810, - "té": 811, - "ille": 812, - "comme": 813, - "cr": 814, - "ux": 815, - "ès": 816, - "aux": 817, - "jour": 818, - "ils": 819, - "bien": 820, - "cou": 821, - "quel": 822, - "peu": 823, - "cette": 824, - "cu": 825, - "mê": 826, - "fait": 827, - "gu": 828, - "être": 829, - "ité": 830, - "ens": 831, - "ni": 832, - "lé": 833, - "dis": 834, - "ble": 835, - "né": 836, - "puis": 837, - "même": 838, - "ques": 839, - "fi": 840, - "age": 841, - "moi": 842, - "ence": 843, - "ont": 844, - "main": 845, - "ors": 846, - "aut": 847, - "ance": 848, - "mé": 849, - "sans": 850, - "sé": 851, - "lon": 852, - "hom": 853, - "car": 854, - "able": 855, - "cher": 856, - "deux": 857, - "enf": 858, - "où": 859, - "ph": 860, - "ure": 861, - "temp": 862, - "pos": 863, - "rent": 864, - "pé": 865, - "faire": 866, - "pi": 867, - "tres": 868, - "ça": 869, - "endre": 870, - "bon": 871, - "sou": 872, - "int": 873, - "pré": 874, - "sent": 875, - "tant": 876, - "cer": 877, - "là": 878, - "lais": 879, - "près": 880, - "bre": 881, - "cour": 882, - "pet": 883, - "comp": 884, - "lait": 885, - "trouv": 886, - "entre": 887, - "sont": 888, - "dev": 889, - "nu": 890, - "temps": 891, - "dou": 892, - "rait": 893, - "bou": 894, - "quand": 895, - "jours": 896, - "avoir": 897, - "été": 898, - "ale": 899, - "pre": 900, - "fois": 901, - "orte": 902, - "vé": 903, - "non": 904, - "tous": 905, - "jus": 906, - "coup": 907, - "homme": 908, - "ête": 909, - "aussi": 910, - "urs": 911, - "seu": 912, - "ord": 913, - "min": 914, - "gé": 915, - "core": 916, - "va": 917, - "vre": 918, - "encore": 919, - "sem": 920, - "ite": 921, - "autre": 922, - "pris": 923, - "peut": 924, - "ue": 925, - "ante": 926, - "gn": 927, - "rép": 928, - "hu": 929, - "sion": 930, - "votre": 931, - "dire": 932, - "ez": 933, - "fem": 934, - "leurs": 935, - "met": 936, - "cri": 937, - "mis": 938, - "tour": 939, - "rai": 940, - "jam": 941, - "regar": 942, - "rien": 943, - "vers": 944, - "suis": 945, - "pouv": 946, - "vis": 947, - "grand": 948, - "ants": 949, - "cor": 950, - "rer": 951, - "cé": 952, - "tent": 953, - "pres": 954, - "vou": 955, - "alors": 956, - "sieur": 957, - "aine": 958, - "quoi": 959, - "fon": 960, - "endant": 961, - "arri": 962, - "eure": 963, - "après": 964, - "donc": 965, - "itu": 966, - "lè": 967, - "sait": 968, - "toi": 969, - "cha": 970, - "ail": 971, - "asse": 972, - "imp": 973, - "voy": 974, - "conn": 975, - "pla": 976, - "petit": 977, - "avant": 978, - "nom": 979, - "tin": 980, - "dont": 981, - "sous": 982, - "emp": 983, - "person": 984, - "elles": 985, - "beau": 986, - "parti": 987, - "cho": 988, - "prit": 989, - "toujours": 990, - "rais": 991, - "jamais": 992, - "trav": 993, - "tions": 994, - "très": 995, - "voi": 996, - "ren": 997, - "yeux": 998, - "voir": 999, - "premi": 1000, - "gne": 1001, - "heure": 1002, - "rou": 1003, - "eff": 1004, - "notre": 1005, - "ments": 1006, - "ton": 1007, - "fais": 1008, - "cela": 1009, - "répon": 1010, - "cons": 1011, - "air": 1012, - "ôt": 1013, - "pendant": 1014, - "ici": 1015, - "toute": 1016, - "jet": 1017, - "port": 1018, - "étaient": 1019, - "pen": 1020, - "hé": 1021, - "autres": 1022, - "père": 1023, - "oc": 1024, - "quelques": 1025, - "ique": 1026, - "lis": 1027, - "femme": 1028, - "jou": 1029, - "teur": 1030, - "monde": 1031, - "nes": 1032, - "dre": 1033, - "aff": 1034, - "rap": 1035, - "part": 1036, - "lement": 1037, - "cla": 1038, - "fut": 1039, - "quelque": 1040, - "prendre": 1041, - "rê": 1042, - "aille": 1043, - "sais": 1044, - "ches": 1045, - "let": 1046, - "char": 1047, - "ères": 1048, - "ents": 1049, - "moins": 1050, - "eau": 1051, - "aî": 1052, - "jeu": 1053, - "heur": 1054, - "ées": 1055, - "tri": 1056, - "point": 1057, - "mom": 1058, - "vent": 1059, - "nouv": 1060, - "gran": 1061, - "trois": 1062, - "sant": 1063, - "toutes": 1064, - "contre": 1065, - "èrent": 1066, - "chez": 1067, - "avez": 1068, - "ût": 1069, - "att": 1070, - "pau": 1071, - "porte": 1072, - "ouver": 1073, - "lit": 1074, - "prés": 1075, - "chose": 1076, - "vit": 1077, - "monsieur": 1078, - "hab": 1079, - "tête": 1080, - "ju": 1081, - "tement": 1082, - "ction": 1083, - "vrai": 1084, - "lar": 1085, - "cet": 1086, - "regard": 1087, - "lant": 1088, - "som": 1089, - "moment": 1090, - "illes": 1091, - "ple": 1092, - "ps": 1093, - "mère": 1094, - "cl": 1095, - "sour": 1096, - "ys": 1097, - "trop": 1098, - "enne": 1099, - "jusqu": 1100, - "avaient": 1101, - "avais": 1102, - "jeune": 1103, - "depuis": 1104, - "personne": 1105, - "fit": 1106, - "cert": 1107, - "jo": 1108, - "oui": 1109, - "rest": 1110, - "semb": 1111, - "cap": 1112, - "mat": 1113, - "mu": 1114, - "long": 1115, - "fran": 1116, - "faut": 1117, - "iti": 1118, - "bli": 1119, - "chev": 1120, - "pri": 1121, - "ente": 1122, - "ainsi": 1123, - "cham": 1124, - "lors": 1125, - "cas": 1126, - "ili": 1127, - "bé": 1128, - "nos": 1129, - "sui": 1130, - "rit": 1131, - "cro": 1132, - "gue": 1133, - "ía": 1134, - "por": 1135, - "las": 1136, - "ón": 1137, - "una": 1138, - "aba": 1139, - "dos": 1140, - "era": 1141, - "mb": 1142, - "para": 1143, - "ás": 1144, - "mos": 1145, - "ando": 1146, - "como": 1147, - "más": 1148, - "ción": 1149, - "tan": 1150, - "dad": 1151, - "ado": 1152, - "fu": 1153, - "cia": 1154, - "mente": 1155, - "sus": 1156, - "tar": 1157, - "za": 1158, - "ba": 1159, - "pero": 1160, - "sin": 1161, - "lla": 1162, - "án": 1163, - "ia": 1164, - "ran": 1165, - "ga": 1166, - "yo": 1167, - "tos": 1168, - "cos": 1169, - "ya": 1170, - "ones": 1171, - "había": 1172, - "hi": 1173, - "esta": 1174, - "mas": 1175, - "tor": 1176, - "aban": 1177, - "dor": 1178, - "ían": 1179, - "tas": 1180, - "én": 1181, - "endo": 1182, - "aque": 1183, - "ero": 1184, - "io": 1185, - "qué": 1186, - "cab": 1187, - "tal": 1188, - "señ": 1189, - "ora": 1190, - "todo": 1191, - "sal": 1192, - "cuando": 1193, - "gun": 1194, - "bu": 1195, - "ras": 1196, - "esto": 1197, - "pare": 1198, - "él": 1199, - "tras": 1200, - "jos": 1201, - "mien": 1202, - "pue": 1203, - "cre": 1204, - "pon": 1205, - "día": 1206, - "tros": 1207, - "sab": 1208, - "sobre": 1209, - "ese": 1210, - "mbre": 1211, - "eron": 1212, - "añ": 1213, - "ido": 1214, - "porque": 1215, - "ella": 1216, - "cen": 1217, - "muy": 1218, - "cal": 1219, - "este": 1220, - "has": 1221, - "có": 1222, - "gra": 1223, - "ros": 1224, - "aquel": 1225, - "dijo": 1226, - "cía": 1227, - "zo": 1228, - "ciones": 1229, - "mbi": 1230, - "elo": 1231, - "tó": 1232, - "ina": 1233, - "todos": 1234, - "tien": 1235, - "estaba": 1236, - "deci": 1237, - "cio": 1238, - "ño": 1239, - "lor": 1240, - "nues": 1241, - "medi": 1242, - "len": 1243, - "vida": 1244, - "ali": 1245, - "pues": 1246, - "ales": 1247, - "vol": 1248, - "mí": 1249, - "rar": 1250, - "cion": 1251, - "hasta": 1252, - "señor": 1253, - "cono": 1254, - "ah": 1255, - "dios": 1256, - "esa": 1257, - "ún": 1258, - "var": 1259, - "san": 1260, - "gui": 1261, - "otros": 1262, - "tado": 1263, - "buen": 1264, - "ña": 1265, - "tiemp": 1266, - "hacer": 1267, - "jer": 1268, - "vu": 1269, - "ana": 1270, - "así": 1271, - "antes": 1272, - "vez": 1273, - "miento": 1274, - "jar": 1275, - "lab": 1276, - "casa": 1277, - "eso": 1278, - "ego": 1279, - "dió": 1280, - "está": 1281, - "encia": 1282, - "eli": 1283, - "ías": 1284, - "tiempo": 1285, - "zar": 1286, - "van": 1287, - "mun": 1288, - "erta": 1289, - "tambi": 1290, - "sí": 1291, - "aun": 1292, - "mismo": 1293, - "entes": 1294, - "mano": 1295, - "ele": 1296, - "nada": 1297, - "segu": 1298, - "mej": 1299, - "erra": 1300, - "tir": 1301, - "uno": 1302, - "donde": 1303, - "toda": 1304, - "desde": 1305, - "también": 1306, - "cuer": 1307, - "hombre": 1308, - "otro": 1309, - "lib": 1310, - "trar": 1311, - "cual": 1312, - "hay": 1313, - "cada": 1314, - "taba": 1315, - "mento": 1316, - "tenía": 1317, - "quer": 1318, - "eran": 1319, - "siemp": 1320, - "siempre": 1321, - "erto": 1322, - "quí": 1323, - "gos": 1324, - "pués": 1325, - "ellos": 1326, - "después": 1327, - "nue": 1328, - "llo": 1329, - "inter": 1330, - "cómo": 1331, - "ahora": 1332, - "uste": 1333, - "traba": 1334, - "lado": 1335, - "ino": 1336, - "poco": 1337, - "erte": 1338, - "mujer": 1339, - "quier": 1340, - "algun": 1341, - "fue": 1342, - "ojos": 1343, - "enton": 1344, - "vos": 1345, - "esper": 1346, - "much": 1347, - "otra": 1348, - "az": 1349, - "eza": 1350, - "aquí": 1351, - "cias": 1352, - "gua": 1353, - "mucho": 1354, - "decir": 1355, - "esti": 1356, - "idad": 1357, - "algo": 1358, - "ocu": 1359, - "entonces": 1360, - "dido": 1361, - "entos": 1362, - "gri": 1363, - "dado": 1364, - "ios": 1365, - "dose": 1366, - "usted": 1367, - "quien": 1368, - "ami": 1369, - "unto": 1370, - "mejor": 1371, - "bas": 1372, - "solo": 1373, - "pregun": 1374, - "tur": 1375, - "alg": 1376, - "todas": 1377, - "parte": 1378, - "emb": 1379, - "cto": 1380, - "mundo": 1381, - "tiene": 1382, - "tante": 1383, - "palab": 1384, - "tran": 1385, - "aquella": 1386, - "cios": 1387, - "aunque": 1388, - "cuen": 1389, - "tener": 1390, - "fun": 1391, - "respon": 1392, - "allí": 1393, - "xi": 1394, - "han": 1395, - "pens": 1396, - "contra": 1397, - "tura": 1398, - "val": 1399, - "dio": 1400, - "tanto": 1401, - "camin": 1402, - "mó": 1403, - "esp": 1404, - "ada": 1405, - "ío": 1406, - "hacia": 1407, - "dej": 1408, - "estar": 1409, - "ión": 1410, - "gas": 1411, - "vas": 1412, - "noche": 1413, - "ér": 1414, - "años": 1415, - "padre": 1416, - "gus": 1417, - "ár": 1418, - "sino": 1419, - "manos": 1420, - "cido": 1421, - "estu": 1422, - "hubi": 1423, - "vir": 1424, - "bri": 1425, - "raz": 1426, - "chi": 1427, - "puede": 1428, - "menos": 1429, - "habi": 1430, - "homb": 1431, - "neces": 1432, - "may": 1433, - "eros": 1434, - "ría": 1435, - "hecho": 1436, - "escu": 1437, - "lti": 1438, - "ándo": 1439, - "bus": 1440, - "cosas": 1441, - "tú": 1442, - "espa": 1443, - "reci": 1444, - "ctor": 1445, - "prim": 1446, - "dia": 1447, - "dese": 1448, - "mientras": 1449, - "hor": 1450, - "fuer": 1451, - "ida": 1452, - "posi": 1453, - "lante": 1454, - "ano": 1455, - "estas": 1456, - "pli": 1457, - "luego": 1458, - "sión": 1459, - "cin": 1460, - "tierra": 1461, - "guar": 1462, - "cado": 1463, - "encon": 1464, - "pren": 1465, - "mayor": 1466, - "fal": 1467, - "ð": 1468, - "ħ": 1469, - "ň": 1470, - "ə": 1471, - "θ": 1472, - "’": 1473, - "“": 1474, - "”": 1475, - "zi": 1476, - "gli": 1477, - "tto": 1478, - "ono": 1479, - "nel": 1480, - "tti": 1481, - "della": 1482, - "zione": 1483, - "tta": 1484, - "tà": 1485, - "uo": 1486, - "come": 1487, - "alla": 1488, - "oni": 1489, - "ggi": 1490, - "ssi": 1491, - "più": 1492, - "ini": 1493, - "bb": 1494, - "sto": 1495, - "sono": 1496, - "eri": 1497, - "sse": 1498, - "sc": 1499, - "sul": 1500, - "vano": 1501, - "sti": 1502, - "suo": 1503, - "cchi": 1504, - "zza": 1505, - "anche": 1506, - "tte": 1507, - "sci": 1508, - "col": 1509, - "sso": 1510, - "ssa": 1511, - "dei": 1512, - "aveva": 1513, - "zz": 1514, - "amo": 1515, - "gno": 1516, - "sua": 1517, - "ria": 1518, - "sì": 1519, - "ché": 1520, - "dal": 1521, - "ona": 1522, - "spe": 1523, - "gni": 1524, - "tt": 1525, - "delle": 1526, - "questo": 1527, - "nella": 1528, - "dere": 1529, - "anno": 1530, - "dell": 1531, - "uni": 1532, - "bbe": 1533, - "anti": 1534, - "ene": 1535, - "gio": 1536, - "uto": 1537, - "qual": 1538, - "glia": 1539, - "quando": 1540, - "tutto": 1541, - "glio": 1542, - "zioni": 1543, - "cam": 1544, - "esso": 1545, - "ss": 1546, - "mol": 1547, - "loro": 1548, - "perché": 1549, - "cosa": 1550, - "due": 1551, - "poi": 1552, - "sco": 1553, - "cco": 1554, - "gna": 1555, - "tem": 1556, - "prima": 1557, - "così": 1558, - "essere": 1559, - "ani": 1560, - "bra": 1561, - "rio": 1562, - "anco": 1563, - "cui": 1564, - "spi": 1565, - "via": 1566, - "gior": 1567, - "bile": 1568, - "ggio": 1569, - "mai": 1570, - "tare": 1571, - "indi": 1572, - "rebbe": 1573, - "senza": 1574, - "zio": 1575, - "tutti": 1576, - "stato": 1577, - "zia": 1578, - "dalla": 1579, - "mia": 1580, - "vita": 1581, - "quella": 1582, - "qua": 1583, - "dove": 1584, - "allo": 1585, - "sempre": 1586, - "zzo": 1587, - "sia": 1588, - "dopo": 1589, - "porta": 1590, - "ccia": 1591, - "erano": 1592, - "anni": 1593, - "chia": 1594, - "enza": 1595, - "propri": 1596, - "anda": 1597, - "cca": 1598, - "occhi": 1599, - "questa": 1600, - "ffi": 1601, - "ron": 1602, - "mio": 1603, - "ris": 1604, - "ogni": 1605, - "rin": 1606, - "far": 1607, - "menti": 1608, - "ancora": 1609, - "fatto": 1610, - "mani": 1611, - "senti": 1612, - "pra": 1613, - "tempo": 1614, - "essi": 1615, - "bbi": 1616, - "lare": 1617, - "pers": 1618, - "sor": 1619, - "anza": 1620, - "pie": 1621, - "verso": 1622, - "altro": 1623, - "tato": 1624, - "cato": 1625, - "ato": 1626, - "volta": 1627, - "cc": 1628, - "fare": 1629, - "ciò": 1630, - "bili": 1631, - "nuo": 1632, - "quello": 1633, - "colo": 1634, - "ppo": 1635, - "trova": 1636, - "ore": 1637, - "rono": 1638, - "molto": 1639, - "almente": 1640, - "sca": 1641, - "vole": 1642, - "tali": 1643, - "sulla": 1644, - "sce": 1645, - "meno": 1646, - "anto": 1647, - "pun": 1648, - "stu": 1649, - "capi": 1650, - "giu": 1651, - "mini": 1652, - "pia": 1653, - "lavo": 1654, - "vero": 1655, - "rsi": 1656, - "altri": 1657, - "scia": 1658, - "suoi": 1659, - "glie": 1660, - "sotto": 1661, - "bene": 1662, - "scri": 1663, - "tale": 1664, - "degli": 1665, - "alc": 1666, - "uomo": 1667, - "pel": 1668, - "pote": 1669, - "essa": 1670, - "scu": 1671, - "signo": 1672, - "stro": 1673, - "uti": 1674, - "sione": 1675, - "gre": 1676, - "fini": 1677, - "lun": 1678, - "esi": 1679, - "passa": 1680, - "rà": 1681, - "mentre": 1682, - "hanno": 1683, - "usci": 1684, - "gia": 1685, - "già": 1686, - "mina": 1687, - "tica": 1688, - "giorno": 1689, - "esse": 1690, - "modo": 1691, - "spa": 1692, - "proprio": 1693, - "ori": 1694, - "contro": 1695, - "stru": 1696, - "diven": 1697, - "disse": 1698, - "rato": 1699, - "noi": 1700, - "vere": 1701, - "può": 1702, - "dice": 1703, - "cci": 1704, - "secon": 1705, - "ccio": 1706, - "qualche": 1707, - "tutta": 1708, - "gg": 1709, - "mondo": 1710, - "forma": 1711, - "mma": 1712, - "pensa": 1713, - "deva": 1714, - "fosse": 1715, - "sopra": 1716, - "tamente": 1717, - "ness": 1718, - "quanto": 1719, - "raga": 1720, - "unque": 1721, - "care": 1722, - "stre": 1723, - "grande": 1724, - "picco": 1725, - "guarda": 1726, - "nell": 1727, - "possi": 1728, - "presen": 1729, - "rò": 1730, - "paro": 1731, - "tua": 1732, - "vin": 1733, - "ane": 1734, - "stesso": 1735, - "dav": 1736, - "nei": 1737, - "nelle": 1738, - "ghi": 1739, - "pio": 1740, - "lato": 1741, - "sid": 1742, - "fine": 1743, - "fuo": 1744, - "quasi": 1745, - "ulti": 1746, - "ito": 1747, - "sue": 1748, - "fil": 1749, - "allora": 1750, - "veni": 1751, - "tano": 1752, - "ello": 1753, - "ão": 1754, - "não": 1755, - "uma": 1756, - "ela": 1757, - "lh": 1758, - "ção": 1759, - "cê": 1760, - "inha": 1761, - "você": 1762, - "ec": 1763, - "dade": 1764, - "ao": 1765, - "ram": 1766, - "vel": 1767, - "ém": 1768, - "pode": 1769, - "estava": 1770, - "isso": 1771, - "mui": 1772, - "faz": 1773, - "ões": 1774, - "pes": 1775, - "ix": 1776, - "sim": 1777, - "olh": 1778, - "isa": 1779, - "ên": 1780, - "tinha": 1781, - "meu": 1782, - "são": 1783, - "minha": 1784, - "muito": 1785, - "foi": 1786, - "bem": 1787, - "diz": 1788, - "parec": 1789, - "ço": 1790, - "pesso": 1791, - "pois": 1792, - "mesmo": 1793, - "ções": 1794, - "seus": 1795, - "até": 1796, - "ência": 1797, - "lhe": 1798, - "tiv": 1799, - "mã": 1800, - "só": 1801, - "tão": 1802, - "tudo": 1803, - "então": 1804, - "inda": 1805, - "bal": 1806, - "indo": 1807, - "ndo": 1808, - "já": 1809, - "vam": 1810, - "eito": 1811, - "depois": 1812, - "mel": 1813, - "lha": 1814, - "ainda": 1815, - "fazer": 1816, - "pou": 1817, - "pergun": 1818, - "deix": 1819, - "tamb": 1820, - "ala": 1821, - "pelo": 1822, - "também": 1823, - "fica": 1824, - "prec": 1825, - "eles": 1826, - "havia": 1827, - "lá": 1828, - "nas": 1829, - "gem": 1830, - "mem": 1831, - "ós": 1832, - "deu": 1833, - "eiro": 1834, - "..": 1835, - "assim": 1836, - "ior": 1837, - "har": 1838, - "aqui": 1839, - "cul": 1840, - "sar": 1841, - "outra": 1842, - "olhos": 1843, - "ima": 1844, - "mim": 1845, - "ago": 1846, - "pessoas": 1847, - "eram": 1848, - "eira": 1849, - "pela": 1850, - "coisa": 1851, - "mão": 1852, - "conh": 1853, - "agora": 1854, - "iam": 1855, - "há": 1856, - "suas": 1857, - "guém": 1858, - "cabe": 1859, - "nem": 1860, - "ível": 1861, - "consegu": 1862, - "trabal": 1863, - "lev": 1864, - "lem": 1865, - "vai": 1866, - "tei": 1867, - "pró": 1868, - "quem": 1869, - "onde": 1870, - "cabeça": 1871, - "nunca": 1872, - "mentos": 1873, - "hum": 1874, - "dele": 1875, - "verdade": 1876, - "tá": 1877, - "hos": 1878, - "algum": 1879, - "dizer": 1880, - "penas": 1881, - "nós": 1882, - "enquanto": 1883, - "outro": 1884, - "lho": 1885, - "melhor": 1886, - "primei": 1887, - "iu": 1888, - "apenas": 1889, - "estou": 1890, - "conte": 1891, - "homem": 1892, - "dois": 1893, - "ças": 1894, - "pouco": 1895, - "senhor": 1896, - "tando": 1897, - "espera": 1898, - "pai": 1899, - "rios": 1900, - "baix": 1901, - "ase": 1902, - "isas": 1903, - "hora": 1904, - "ficar": 1905, - "seja": 1906, - "ân": 1907, - "clar": 1908, - "inc": 1909, - "fos": 1910, - "ouvi": 1911, - "vem": 1912, - "tava": 1913, - "ário": 1914, - "sos": 1915, - "inho": 1916, - "rando": 1917, - "ês": 1918, - "coisas": 1919, - "aconte": 1920, - "lher": 1921, - "anos": 1922, - "talvez": 1923, - "estão": 1924, - "liv": 1925, - "outros": 1926, - "qualquer": 1927, - "gou": 1928, - "lí": 1929, - "tivesse": 1930, - "rado": 1931, - "precisa": 1932, - "mãe": 1933, - "dela": 1934, - "entra": 1935, - "maior": 1936, - "noite": 1937, - "tiva": 1938, - "pala": 1939, - "ração": 1940, - "deus": 1941, - "sas": 1942, - "inte": 1943, - "fei": 1944, - "palav": 1945, - "trás": 1946, - "cidade": 1947, - "lugar": 1948, - "vezes": 1949, - "encontra": 1950, - "tru": 1951, - "eci": 1952, - "ın": 1953, - "bir": 1954, - "yor": 1955, - "ek": 1956, - "dı": 1957, - "ey": 1958, - "tı": 1959, - "mı": 1960, - "iz": 1961, - "ır": 1962, - "gö": 1963, - "sı": 1964, - "bil": 1965, - "lı": 1966, - "üz": 1967, - "iç": 1968, - "iy": 1969, - "ım": 1970, - "uz": 1971, - "cak": 1972, - "iş": 1973, - "ını": 1974, - "iyor": 1975, - "baş": 1976, - "dü": 1977, - "değ": 1978, - "kar": 1979, - "ev": 1980, - "öy": 1981, - "bun": 1982, - "yap": 1983, - "sun": 1984, - "gör": 1985, - "yı": 1986, - "ki": 1987, - "ara": 1988, - "alı": 1989, - "onu": 1990, - "çı": 1991, - "şey": 1992, - "sın": 1993, - "kı": 1994, - "kad": 1995, - "ağ": 1996, - "değil": 1997, - "ük": 1998, - "çok": 1999, - "şı": 2000, - "ül": 2001, - "için": 2002, - "eye": 2003, - "oldu": 2004, - "mış": 2005, - "kal": 2006, - "mek": 2007, - "öyle": 2008, - "yordu": 2009, - "yüz": 2010, - "miş": 2011, - "mak": 2012, - "ola": 2013, - "yan": 2014, - "cek": 2015, - "yorum": 2016, - "bak": 2017, - "üm": 2018, - "ları": 2019, - "oğ": 2020, - "kadar": 2021, - "arı": 2022, - "ında": 2023, - "gün": 2024, - "yok": 2025, - "yer": 2026, - "dım": 2027, - "daha": 2028, - "ına": 2029, - "dim": 2030, - "bilir": 2031, - "iki": 2032, - "siz": 2033, - "diğ": 2034, - "bü": 2035, - "düş": 2036, - "üç": 2037, - "unu": 2038, - "aman": 2039, - "fak": 2040, - "ede": 2041, - "sonra": 2042, - "hiç": 2043, - "aki": 2044, - "ğı": 2045, - "bul": 2046, - "maz": 2047, - "anla": 2048, - "bura": 2049, - "geç": 2050, - "maya": 2051, - "konu": 2052, - "din": 2053, - "tek": 2054, - "zaman": 2055, - "eler": 2056, - "öz": 2057, - "dır": 2058, - "gibi": 2059, - "şa": 2060, - "leri": 2061, - "kim": 2062, - "ku": 2063, - "fakat": 2064, - "yar": 2065, - "göz": 2066, - "cı": 2067, - "yorsun": 2068, - "bek": 2069, - "inde": 2070, - "pek": 2071, - "bunu": 2072, - "lik": 2073, - "iler": 2074, - "edi": 2075, - "öl": 2076, - "sür": 2077, - "sır": 2078, - "çık": 2079, - "sıl": 2080, - "alar": 2081, - "kes": 2082, - "yak": 2083, - "çek": 2084, - "yıl": 2085, - "ecek": 2086, - "ız": 2087, - "git": 2088, - "kap": 2089, - "ama": 2090, - "ıl": 2091, - "ların": 2092, - "biz": 2093, - "tır": 2094, - "oy": 2095, - "ancak": 2096, - "doğ": 2097, - "bana": 2098, - "şim": 2099, - "başla": 2100, - "lü": 2101, - "madı": 2102, - "beni": 2103, - "yük": 2104, - "lık": 2105, - "beş": 2106, - "nasıl": 2107, - "tık": 2108, - "tür": 2109, - "daki": 2110, - "ceğ": 2111, - "zı": 2112, - "iyi": 2113, - "dok": 2114, - "benim": 2115, - "cağ": 2116, - "yen": 2117, - "şu": 2118, - "mez": 2119, - "düşün": 2120, - "kendi": 2121, - "şimdi": 2122, - "yol": 2123, - "yu": 2124, - "iste": 2125, - "sek": 2126, - "mam": 2127, - "söyle": 2128, - "dik": 2129, - "kur": 2130, - "olduğ": 2131, - "sını": 2132, - "biliyor": 2133, - "kan": 2134, - "yal": 2135, - "meye": 2136, - "muş": 2137, - "kaç": 2138, - "iye": 2139, - "tü": 2140, - "ef": 2141, - "tım": 2142, - "evet": 2143, - "yet": 2144, - "burada": 2145, - "tim": 2146, - "biraz": 2147, - "kor": 2148, - "doğru": 2149, - "inin": 2150, - "kız": 2151, - "diye": 2152, - "dör": 2153, - "etti": 2154, - "onun": 2155, - "isti": 2156, - "ği": 2157, - "sana": 2158, - "üş": 2159, - "arka": 2160, - "hayır": 2161, - "karşı": 2162, - "ile": 2163, - "hak": 2164, - "ıyor": 2165, - "neden": 2166, - "sev": 2167, - "sız": 2168, - "çocu": 2169, - "çalı": 2170, - "olur": 2171, - "bır": 2172, - "gir": 2173, - "ise": 2174, - "ih": 2175, - "kır": 2176, - "dön": 2177, - "böyle": 2178, - "seni": 2179, - "!\"": 2180, - "dört": 2181, - "söy": 2182, - "oş": 2183, - "musun": 2184, - "laş": 2185, - "ip": 2186, - "kay": 2187, - "hem": 2188, - "büyük": 2189, - "aç": 2190, - "bırak": 2191, - "misin": 2192, - "söz": 2193, - "değiş": 2194, - "ünü": 2195, - "gül": 2196, - "kö": 2197, - "karı": 2198, - "tamam": 2199, - "olu": 2200, - "yeni": 2201, - "lam": 2202, - "mıştı": 2203, - "yaş": 2204, - "iniz": 2205, - "kadın": 2206, - "bunun": 2207, - "mey": 2208, - "altı": 2209, - "yi": 2210, - "inden": 2211, - "senin": 2212, - "yat": 2213, - "top": 2214, - "isi": 2215, - "dün": 2216, - "hiçbir": 2217, - "yon": 2218, - "dın": 2219, - "tün": 2220, - "başka": 2221, - "hep": 2222, - "irmi": 2223, - "devam": 2224, - "olacak": 2225, - "artık": 2226, - "durum": 2227, - "imiz": 2228, - "üzel": 2229, - "lerini": 2230, - "sağ": 2231, - "gerek": 2232, - "yirmi": 2233, - "şek": 2234, - "bağ": 2235, - "lara": 2236, - "yür": 2237, - "ması": 2238, - "katı": 2239, - "dedi": 2240, - "gü": 2241, - "sorun": 2242, - "üne": 2243, - "mız": 2244, - "yapı": 2245, - "mil": 2246, - "ğını": 2247, - "tara": 2248, - "vardı": 2249, - "konuş": 2250, - "arak": 2251, - "larak": 2252, - "çocuk": 2253, - "bütün": 2254, - "ley": 2255, - "dür": 2256, - "güzel": 2257, - "ayı": 2258, - "yapa": 2259, - "nı": 2260, - "ayr": 2261, - "öne": 2262, - "yordum": 2263, - "ban": 2264, - "i̇ş": 2265, - "dum": 2266, - "yorlar": 2267, - "larını": 2268, - "çıkar": 2269, - "zan": 2270, - "seç": 2271, - "liyor": 2272, - "tak": 2273, - "şık": 2274, - "tekrar": 2275, - "aş": 2276, - "eş": 2277, - "mişti": 2278, - "kin": 2279, - "imi": 2280, - "eğ": 2281, - "gidi": 2282, - "leş": 2283, - "başladı": 2284, - "gide": 2285, - "otur": 2286, - "dde": 2287, - "ından": 2288, - "üzer": 2289, - "ının": 2290, - "nız": 2291, - "uy": 2292, - "yedi": 2293, - "kat": 2294, - "olarak": 2295, - "ladı": 2296, - "yalnız": 2297, - "bah": 2298, - "iyet": 2299, - "sak": 2300, - "açık": 2301, - "sında": 2302, - "...": 2303, - "insan": 2304, - "aynı": 2305, - "eder": 2306, - "istan": 2307, - "uzun": 2308, - "geri": 2309, - "erek": 2310, - "olan": 2311, - "gerçek": 2312, - "alan": 2313, - "dış": 2314, - "alık": 2315, - "fark": 2316, - "üst": 2317, - "sade": 2318, - "kiş": 2319, - "ldı": 2320, - "zor": 2321, - "etir": 2322, - "herkes": 2323, - "ömer": 2324, - "unda": 2325, - "haf": 2326, - "buna": 2327, - "ydı": 2328, - "peki": 2329, - "adam": 2330, - "haz": 2331, - "sına": 2332, - "kapı": 2333, - "görüş": 2334, - "sadece": 2335, - "aldı": 2336, - "geldi": 2337, - "rz": 2338, - "sz": 2339, - "cz": 2340, - "ię": 2341, - "dz": 2342, - "ał": 2343, - "się": 2344, - "rze": 2345, - "że": 2346, - "wy": 2347, - "rzy": 2348, - "ła": 2349, - "ło": 2350, - "ny": 2351, - "dzie": 2352, - "dzi": 2353, - "czy": 2354, - "cie": 2355, - "prze": 2356, - "dy": 2357, - "kie": 2358, - "ry": 2359, - "ją": 2360, - "ów": 2361, - "przy": 2362, - "mie": 2363, - "szy": 2364, - "cze": 2365, - "bie": 2366, - "cy": 2367, - "nia": 2368, - "ści": 2369, - "sze": 2370, - "jest": 2371, - "ży": 2372, - "ną": 2373, - "któ": 2374, - "ała": 2375, - "mnie": 2376, - "ły": 2377, - "cza": 2378, - "jak": 2379, - "roz": 2380, - "ró": 2381, - "zna": 2382, - "łu": 2383, - "ść": 2384, - "wia": 2385, - "wszy": 2386, - "spo": 2387, - "gdy": 2388, - "wał": 2389, - "wię": 2390, - "łem": 2391, - "ję": 2392, - "sk": 2393, - "rę": 2394, - "dob": 2395, - "już": 2396, - "bę": 2397, - "ałem": 2398, - "sza": 2399, - "pod": 2400, - "dla": 2401, - "pan": 2402, - "nę": 2403, - "może": 2404, - "śli": 2405, - "ało": 2406, - "lko": 2407, - "nych": 2408, - "powie": 2409, - "cię": 2410, - "tylko": 2411, - "naj": 2412, - "tego": 2413, - "ski": 2414, - "nego": 2415, - "wszyst": 2416, - "szcze": 2417, - "jed": 2418, - "jej": 2419, - "two": 2420, - "ąd": 2421, - "śmy": 2422, - "czę": 2423, - "wać": 2424, - "jego": 2425, - "ża": 2426, - "sy": 2427, - "praw": 2428, - "tym": 2429, - "który": 2430, - "ały": 2431, - "trze": 2432, - "niej": 2433, - "nym": 2434, - "gło": 2435, - "jąc": 2436, - "mówi": 2437, - "ska": 2438, - "nej": 2439, - "słu": 2440, - "wła": 2441, - "będzie": 2442, - "dę": 2443, - "pó": 2444, - "bez": 2445, - "nic": 2446, - "pła": 2447, - "ście": 2448, - "są": 2449, - "trzy": 2450, - "kiem": 2451, - "był": 2452, - "mog": 2453, - "robi": 2454, - "tam": 2455, - "mię": 2456, - "zy": 2457, - "pew": 2458, - "myś": 2459, - "przed": 2460, - "sko": 2461, - "które": 2462, - "lę": 2463, - "wsze": 2464, - "ąc": 2465, - "było": 2466, - "sobie": 2467, - "py": 2468, - "cią": 2469, - "jeszcze": 2470, - "tę": 2471, - "czas": 2472, - "szę": 2473, - "gł": 2474, - "kę": 2475, - "czu": 2476, - "przez": 2477, - "sło": 2478, - "wz": 2479, - "kto": 2480, - "ków": 2481, - "czo": 2482, - "liśmy": 2483, - "więc": 2484, - "rą": 2485, - "wó": 2486, - "rza": 2487, - "ności": 2488, - "wet": 2489, - "nął": 2490, - "śmie": 2491, - "nawet": 2492, - "musi": 2493, - "swo": 2494, - "tej": 2495, - "wą": 2496, - "wu": 2497, - "wią": 2498, - "niu": 2499, - "czą": 2500, - "dzo": 2501, - "skie": 2502, - "jeśli": 2503, - "czego": 2504, - "chy": 2505, - "dł": 2506, - "tych": 2507, - "bym": 2508, - "żo": 2509, - "eś": 2510, - "sią": 2511, - "kiedy": 2512, - "wró": 2513, - "dze": 2514, - "dro": 2515, - "rów": 2516, - "pani": 2517, - "kul": 2518, - "nad": 2519, - "chwi": 2520, - "nim": 2521, - "być": 2522, - "chodzi": 2523, - "nio": 2524, - "dobrze": 2525, - "teraz": 2526, - "wokul": 2527, - "coś": 2528, - "kł": 2529, - "pier": 2530, - "gdzie": 2531, - "dzy": 2532, - "pię": 2533, - "dź": 2534, - "ką": 2535, - "gó": 2536, - "zda": 2537, - "chce": 2538, - "stę": 2539, - "świa": 2540, - "wszystko": 2541, - "peł": 2542, - "wiem": 2543, - "wiel": 2544, - "każ": 2545, - "rzu": 2546, - "sły": 2547, - "jedna": 2548, - "myśl": 2549, - "mój": 2550, - "jestem": 2551, - "óż": 2552, - "miej": 2553, - "moż": 2554, - "kła": 2555, - "resz": 2556, - "dłu": 2557, - "stwo": 2558, - "nię": 2559, - "masz": 2560, - "żeby": 2561, - "niem": 2562, - "jakie": 2563, - "sty": 2564, - "nią": 2565, - "wej": 2566, - "oj": 2567, - "sła": 2568, - "ność": 2569, - "zło": 2570, - "szczę": 2571, - "lej": 2572, - "wego": 2573, - "cał": 2574, - "dział": 2575, - "kich": 2576, - "dza": 2577, - "dzię": 2578, - "oczy": 2579, - "zosta": 2580, - "czło": 2581, - "nam": 2582, - "kil": 2583, - "szu": 2584, - "wę": 2585, - "miał": 2586, - "strze": 2587, - "cej": 2588, - "ej": 2589, - "znaj": 2590, - "dać": 2591, - "miejs": 2592, - "kró": 2593, - "kry": 2594, - "bardzo": 2595, - "śnie": 2596, - "lą": 2597, - "gie": 2598, - "ciebie": 2599, - "dni": 2600, - "potrze": 2601, - "wokulski": 2602, - "uwa": 2603, - "umie": 2604, - "jednak": 2605, - "kra": 2606, - "wróci": 2607, - "człowie": 2608, - "czyć": 2609, - "była": 2610, - "żeli": 2611, - "mę": 2612, - "cę": 2613, - "zrobi": 2614, - "mogę": 2615, - "prowa": 2616, - "rem": 2617, - "niech": 2618, - "cznie": 2619, - "kro": 2620, - "tą": 2621, - "chci": 2622, - "bro": 2623, - "dzieć": 2624, - "szą": 2625, - "pad": 2626, - "trz": 2627, - "jem": 2628, - "tów": 2629, - "dru": 2630, - "taj": 2631, - "rzekł": 2632, - "niego": 2633, - "takie": 2634, - "wała": 2635, - "towa": 2636, - "kapła": 2637, - "widzi": 2638, - "podob": 2639, - "dzę": 2640, - "tał": 2641, - "stęp": 2642, - "bą": 2643, - "poko": 2644, - "wem": 2645, - "gę": 2646, - "aby": 2647, - "albo": 2648, - "spra": 2649, - "zno": 2650, - "smo": 2651, - "jesz": 2652, - "księ": 2653, - "jesteś": 2654, - "poz": 2655, - "nigdy": 2656, - "ksią": 2657, - "cóż": 2658, - "ws": 2659, - "pow": 2660, - "tka": 2661, - "świe": 2662, - "szka": 2663, - "samo": 2664, - "sł": 2665, - "rzę": 2666, - "nale": 2667, - "chcesz": 2668, - "nik": 2669, - "pę": 2670, - "chyba": 2671, - "ciąg": 2672, - "jący": 2673, - "woj": 2674, - "nasze": 2675, - "mniej": 2676, - "więcej": 2677, - "zwy": 2678, - "osta": 2679, - "waż": 2680, - "śmier": 2681, - "wier": 2682, - "dzą": 2683, - "zaś": 2684, - "gdyby": 2685, - "jaki": 2686, - "wol": 2687, - "win": 2688, - "dą": 2689, - "ścia": 2690, - "rozma": 2691, - "wal": 2692, - "panie": 2693, - "star": 2694, - "kaz": 2695, - "jeżeli": 2696, - "wra": 2697, - "koń": 2698, - "siebie": 2699, - "znowu": 2700, - "czem": 2701, - "stwa": 2702, - "isto": 2703, - "pół": 2704, - "dał": 2705, - "kobie": 2706, - "ałam": 2707, - "wych": 2708, - "cesa": 2709, - "nich": 2710, - "zawsze": 2711, - "dzić": 2712, - "też": 2713, - "lepie": 2714, - "proszę": 2715, - "kre": 2716, - "twa": 2717, - "łą": 2718, - "chu": 2719, - "cą": 2720, - "prz": 2721, - "łe": 2722, - "szedł": 2723, - "odpowie": 2724, - "myśli": 2725, - "świą": 2726, - "ź": 2727, - "ł": 2728, - "&": 2729, - "=": 2730, - "ă": 2731, - "đ": 2732, - "ţ": 2733, - "–": 2734, - "‘": 2735, - "ij": 2736, - "aa": 2737, - "een": 2738, - "het": 2739, - "aar": 2740, - "oor": 2741, - "ijn": 2742, - "dat": 2743, - "oe": 2744, - "ijk": 2745, - "aan": 2746, - "voor": 2747, - "iet": 2748, - "zijn": 2749, - "niet": 2750, - "oo": 2751, - "moet": 2752, - "heb": 2753, - "uit": 2754, - "wij": 2755, - "aat": 2756, - "lijk": 2757, - "sl": 2758, - "daar": 2759, - "deze": 2760, - "worden": 2761, - "moeten": 2762, - "onder": 2763, - "hebben": 2764, - "ook": 2765, - "ct": 2766, - "nog": 2767, - "aal": 2768, - "eer": 2769, - "bij": 2770, - "mijn": 2771, - "kom": 2772, - "atie": 2773, - "eft": 2774, - "kel": 2775, - "rij": 2776, - "heid": 2777, - "af": 2778, - "stel": 2779, - "maar": 2780, - "wee": 2781, - "heeft": 2782, - "waar": 2783, - "eren": 2784, - "wat": 2785, - "wil": 2786, - "aag": 2787, - "bet": 2788, - "hij": 2789, - "kun": 2790, - "uw": 2791, - "dt": 2792, - "door": 2793, - "tij": 2794, - "ond": 2795, - "geen": 2796, - "gev": 2797, - "veel": 2798, - "naar": 2799, - "aten": 2800, - "kunnen": 2801, - "echt": 2802, - "goe": 2803, - "twee": 2804, - "delijk": 2805, - "uur": 2806, - "toe": 2807, - "meer": 2808, - "onze": 2809, - "tijd": 2810, - "hoe": 2811, - "tot": 2812, - "zou": 2813, - "aak": 2814, - "amen": 2815, - "woor": 2816, - "wordt": 2817, - "gelijk": 2818, - "gaan": 2819, - "ker": 2820, - "eld": 2821, - "hou": 2822, - "zel": 2823, - "tegen": 2824, - "komen": 2825, - "werk": 2826, - "goed": 2827, - "zal": 2828, - "zij": 2829, - "slag": 2830, - "zien": 2831, - "echter": 2832, - "itie": 2833, - "tie": 2834, - "elijk": 2835, - "ische": 2836, - "belan": 2837, - "haar": 2838, - "vr": 2839, - "grijk": 2840, - "doen": 2841, - "land": 2842, - "belangrijk": 2843, - "open": 2844, - "ctie": 2845, - "zelf": 2846, - "mij": 2847, - "iteit": 2848, - "stem": 2849, - "mee": 2850, - "aren": 2851, - "dien": 2852, - "gaat": 2853, - "prob": 2854, - "moe": 2855, - "ullen": 2856, - "zich": 2857, - "daarom": 2858, - "orm": 2859, - "staat": 2860, - "zit": 2861, - "dui": 2862, - "dus": 2863, - "ds": 2864, - "verslag": 2865, - "kelijk": 2866, - "proble": 2867, - "schap": 2868, - "gd": 2869, - "hun": 2870, - "erd": 2871, - "zet": 2872, - "staan": 2873, - "maal": 2874, - "inder": 2875, - "eid": 2876, - "kken": 2877, - "ged": 2878, - "zullen": 2879, - "mensen": 2880, - "jaar": 2881, - "regel": 2882, - "ieder": 2883, - "volgen": 2884, - "geven": 2885, - "even": 2886, - "blij": 2887, - "ië": 2888, - "uwe": 2889, - "maken": 2890, - "oek": 2891, - "nieuwe": 2892, - "baar": 2893, - "andere": 2894, - "ruik": 2895, - "agen": 2896, - "ouw": 2897, - "willen": 2898, - "aakt": 2899, - "hoo": 2900, - "anden": 2901, - "lig": 2902, - "samen": 2903, - "zeer": 2904, - "duidelijk": 2905, - "antwoor": 2906, - "heel": 2907, - "punt": 2908, - "houden": 2909, - "vraag": 2910, - "gele": 2911, - "eens": 2912, - "besch": 2913, - "omen": 2914, - "erg": 2915, - "doel": 2916, - "dag": 2917, - "uren": 2918, - "ings": 2919, - "oren": 2920, - "delen": 2921, - "steun": 2922, - "innen": 2923, - "pol": 2924, - "oon": 2925, - "sn": 2926, - "zonder": 2927, - "nodig": 2928, - "alleen": 2929, - "mid": 2930, - "ragen": 2931, - "iets": 2932, - "versch": 2933, - "gebruik": 2934, - "rouw": 2935, - "stellen": 2936, - "menten": 2937, - "eerste": 2938, - "laat": 2939, - "groot": 2940, - "ood": 2941, - "toch": 2942, - "laten": 2943, - "aard": 2944, - "sle": 2945, - "deel": 2946, - "plaat": 2947, - "ree": 2948, - "betre": 2949, - "lid": 2950, - "uiten": 2951, - "racht": 2952, - "beleid": 2953, - "stie": 2954, - "staten": 2955, - "ggen": 2956, - "reken": 2957, - "alen": 2958, - "ming": 2959, - "mogelijk": 2960, - "grote": 2961, - "altijd": 2962, - "enkel": 2963, - "wik": 2964, - "politie": 2965, - "elk": 2966, - "handel": 2967, - "kwe": 2968, - "maat": 2969, - "elen": 2970, - "vrij": 2971, - "jes": 2972, - "aam": 2973, - "huis": 2974, - "weer": 2975, - "lidstaten": 2976, - "king": 2977, - "kle": 2978, - "bed": 2979, - "geval": 2980, - "wikkel": 2981, - "kwestie": 2982, - "stee": 2983, - "hel": 2984, - "komst": 2985, - "iden": 2986, - "eerd": 2987, - "tweede": 2988, - "probleem": 2989, - "ussen": 2990, - "snel": 2991, - "tig": 2992, - "ult": 2993, - "nemen": 2994, - "commis": 2995, - "verschil": 2996, - "zoek": 2997, - "krij": 2998, - "graag": 2999, - "denk": 3000, - "landen": 3001, - "reden": 3002, - "besl": 3003, - "oeg": 3004, - "beter": 3005, - "heden": 3006, - "mag": 3007, - "boven": 3008, - "cont": 3009, - "fd": 3010, - "hele": 3011, - "vier": 3012, - "gez": 3013, - "kw": 3014, - "aas": 3015, - "ontwikkel": 3016, - "drie": 3017, - "vaak": 3018, - "plaats": 3019, - "gang": 3020, - "ijf": 3021, - "natuur": 3022, - "tussen": 3023, - "bat": 3024, - "komt": 3025, - "wacht": 3026, - "aad": 3027, - "achter": 3028, - "gebie": 3029, - "verk": 3030, - "ligt": 3031, - "nieuw": 3032, - "vand": 3033, - "ý": 3034, - "ď": 3035, - "ě": 3036, - "ř": 3037, - "ť": 3038, - "ů": 3039, - "„": 3040, - "ní": 3041, - "ně": 3042, - "ře": 3043, - "ná": 3044, - "vě": 3045, - "vá": 3046, - "rá": 3047, - "vy": 3048, - "mě": 3049, - "ři": 3050, - "ří": 3051, - "že": 3052, - "jí": 3053, - "vý": 3054, - "ji": 3055, - "dě": 3056, - "če": 3057, - "tě": 3058, - "ky": 3059, - "še": 3060, - "ké": 3061, - "ší": 3062, - "pře": 3063, - "ví": 3064, - "ný": 3065, - "ži": 3066, - "má": 3067, - "cí": 3068, - "zá": 3069, - "ské": 3070, - "dá": 3071, - "byl": 3072, - "tí": 3073, - "pří": 3074, - "při": 3075, - "či": 3076, - "vní": 3077, - "ča": 3078, - "dí": 3079, - "dní": 3080, - "ká": 3081, - "nou": 3082, - "vět": 3083, - "pě": 3084, - "kou": 3085, - "ých": 3086, - "bě": 3087, - "prá": 3088, - "jako": 3089, - "ží": 3090, - "zí": 3091, - "jsou": 3092, - "jsem": 3093, - "lní": 3094, - "cké": 3095, - "vat": 3096, - "před": 3097, - "hla": 3098, - "stá": 3099, - "čí": 3100, - "ši": 3101, - "kla": 3102, - "ště": 3103, - "lou": 3104, - "mů": 3105, - "chá": 3106, - "pů": 3107, - "také": 3108, - "dů": 3109, - "nost": 3110, - "tře": 3111, - "sku": 3112, - "vše": 3113, - "tní": 3114, - "byla": 3115, - "ční": 3116, - "jeho": 3117, - "bý": 3118, - "vání": 3119, - "ných": 3120, - "tři": 3121, - "vz": 3122, - "stře": 3123, - "dva": 3124, - "hle": 3125, - "čá": 3126, - "nosti": 3127, - "vš": 3128, - "hra": 3129, - "jen": 3130, - "slo": 3131, - "však": 3132, - "kdy": 3133, - "bylo": 3134, - "bude": 3135, - "jší": 3136, - "vých": 3137, - "ním": 3138, - "sm": 3139, - "koli": 3140, - "rů": 3141, - "může": 3142, - "není": 3143, - "hod": 3144, - "bí": 3145, - "tý": 3146, - "stě": 3147, - "uje": 3148, - "sá": 3149, - "pět": 3150, - "krá": 3151, - "tom": 3152, - "ství": 3153, - "vně": 3154, - "sed": 3155, - "své": 3156, - "pí": 3157, - "musí": 3158, - "už": 3159, - "tím": 3160, - "jící": 3161, - "jedno": 3162, - "čas": 3163, - "čty": 3164, - "ský": 3165, - "evro": 3166, - "toho": 3167, - "hy": 3168, - "kter": 3169, - "rní": 3170, - "stí": 3171, - "svě": 3172, - "pak": 3173, - "všech": 3174, - "ků": 3175, - "ng": 3176, - "ád": 3177, - "chází": 3178, - "být": 3179, - "první": 3180, - "mno": 3181, - "ského": 3182, - "pá": 3183, - "nebo": 3184, - "kem": 3185, - "sla": 3186, - "ného": 3187, - "zde": 3188, - "další": 3189, - "řa": 3190, - "čtyři": 3191, - "hrá": 3192, - "druh": 3193, - "lně": 3194, - "vla": 3195, - "ských": 3196, - "ško": 3197, - "půso": 3198, - "proto": 3199, - "vů": 3200, - "ská": 3201, - "šest": 3202, - "dně": 3203, - "ještě": 3204, - "mezi": 3205, - "několi": 3206, - "již": 3207, - "čně": 3208, - "slu": 3209, - "zná": 3210, - "sedm": 3211, - "vlá": 3212, - "osm": 3213, - "byly": 3214, - "vám": 3215, - "cký": 3216, - "tech": 3217, - "ději": 3218, - "velmi": 3219, - "leži": 3220, - "vala": 3221, - "lý": 3222, - "tvo": 3223, - "spole": 3224, - "stup": 3225, - "mož": 3226, - "evrop": 3227, - "stal": 3228, - "jde": 3229, - "rodi": 3230, - "její": 3231, - "poli": 3232, - "devět": 3233, - "sme": 3234, - "až": 3235, - "této": 3236, - "tento": 3237, - "kaž": 3238, - "nula": 3239, - "bych": 3240, - "moc": 3241, - "stou": 3242, - "kdo": 3243, - "zd": 3244, - "praco": 3245, - "tomu": 3246, - "ným": 3247, - "živo": 3248, - "zem": 3249, - "násle": 3250, - "sky": 3251, - "jich": 3252, - "měl": 3253, - "děla": 3254, - "jsme": 3255, - "nice": 3256, - "stej": 3257, - "stní": 3258, - "náro": 3259, - "nit": 3260, - "později": 3261, - "tako": 3262, - "nce": 3263, - "čer": 3264, - "ším": 3265, - "něco": 3266, - "vál": 3267, - "řej": 3268, - "krát": 3269, - "ální": 3270, - "asi": 3271, - "které": 3272, - "stav": 3273, - "mají": 3274, - "mys": 3275, - "době": 3276, - "sně": 3277, - "zku": 3278, - "tů": 3279, - "chod": 3280, - "spě": 3281, - "jejich": 3282, - "součas": 3283, - "vali": 3284, - "kte": 3285, - "prů": 3286, - "zení": 3287, - "pat": 3288, - "potře": 3289, - "dnes": 3290, - "zemí": 3291, - "znam": 3292, - "mám": 3293, - "tedy": 3294, - "hlavní": 3295, - "použí": 3296, - "bní": 3297, - "vede": 3298, - "lep": 3299, - "jek": 3300, - "prav": 3301, - "politi": 3302, - "dne": 3303, - "čení": 3304, - "než": 3305, - "děl": 3306, - "čo": 3307, - "cích": 3308, - "sté": 3309, - "dlou": 3310, - "několik": 3311, - "vyu": 3312, - "ckých": 3313, - "nové": 3314, - "čin": 3315, - "dělá": 3316, - "ký": 3317, - "obla": 3318, - "podle": 3319, - "důleži": 3320, - "poku": 3321, - "kone": 3322, - "dý": 3323, - "dvě": 3324, - "žád": 3325, - "nout": 3326, - "tku": 3327, - "tvr": 3328, - "ckého": 3329, - "rov": 3330, - "tele": 3331, - "psa": 3332, - "svět": 3333, - "tivní": 3334, - "dosta": 3335, - "šel": 3336, - "druhé": 3337, - "skou": 3338, - "žo": 3339, - "jedná": 3340, - "význam": 3341, - "problé": 3342, - "publi": 3343, - "ván": 3344, - "odpo": 3345, - "podpo": 3346, - "dle": 3347, - "jaké": 3348, - "šení": 3349, - "vím": 3350, - "během": 3351, - "nachází": 3352, - "slou": 3353, - "pouze": 3354, - "otá": 3355, - "plo": 3356, - "tové": 3357, - "větši": 3358, - "komi": 3359, - "vají": 3360, - "tyto": 3361, - "zápa": 3362, - "změ": 3363, - "moh": 3364, - "více": 3365, - "společ": 3366, - "auto": 3367, - "proti": 3368, - "dět": 3369, - "cháze": 3370, - "žel": 3371, - "«": 3372, - "»": 3373, - "а": 3374, - "б": 3375, - "в": 3376, - "г": 3377, - "д": 3378, - "е": 3379, - "ж": 3380, - "з": 3381, - "и": 3382, - "й": 3383, - "к": 3384, - "л": 3385, - "м": 3386, - "н": 3387, - "о": 3388, - "п": 3389, - "р": 3390, - "с": 3391, - "т": 3392, - "у": 3393, - "ф": 3394, - "х": 3395, - "ц": 3396, - "ч": 3397, - "ш": 3398, - "щ": 3399, - "ъ": 3400, - "ы": 3401, - "ь": 3402, - "э": 3403, - "ю": 3404, - "я": 3405, - "ё": 3406, - "‑": 3407, - "−": 3408, - "ст": 3409, - "ен": 3410, - "но": 3411, - "на": 3412, - "пр": 3413, - "то": 3414, - "по": 3415, - "ра": 3416, - "го": 3417, - "ко": 3418, - "не": 3419, - "во": 3420, - "ва": 3421, - "ет": 3422, - "ер": 3423, - "ни": 3424, - "ел": 3425, - "ит": 3426, - "ны": 3427, - "за": 3428, - "ро": 3429, - "ени": 3430, - "ка": 3431, - "ли": 3432, - "ем": 3433, - "да": 3434, - "об": 3435, - "ла": 3436, - "до": 3437, - "ся": 3438, - "ть": 3439, - "от": 3440, - "ло": 3441, - "ль": 3442, - "ед": 3443, - "со": 3444, - "ми": 3445, - "ре": 3446, - "мо": 3447, - "ци": 3448, - "про": 3449, - "та": 3450, - "это": 3451, - "ки": 3452, - "ру": 3453, - "при": 3454, - "ти": 3455, - "се": 3456, - "ста": 3457, - "вы": 3458, - "мы": 3459, - "ви": 3460, - "бы": 3461, - "ма": 3462, - "ес": 3463, - "ля": 3464, - "сти": 3465, - "ле": 3466, - "что": 3467, - "ме": 3468, - "ри": 3469, - "ча": 3470, - "од": 3471, - "ей": 3472, - "ель": 3473, - "ения": 3474, - "га": 3475, - "ну": 3476, - "си": 3477, - "па": 3478, - "раз": 3479, - "бо": 3480, - "сто": 3481, - "су": 3482, - "са": 3483, - "ду": 3484, - "его": 3485, - "ест": 3486, - "ин": 3487, - "ить": 3488, - "из": 3489, - "же": 3490, - "му": 3491, - "пер": 3492, - "под": 3493, - "ение": 3494, - "сь": 3495, - "ку": 3496, - "пред": 3497, - "ного": 3498, - "ных": 3499, - "вер": 3500, - "те": 3501, - "ной": 3502, - "ции": 3503, - "де": 3504, - "ры": 3505, - "дел": 3506, - "лю": 3507, - "ве": 3508, - "он": 3509, - "мен": 3510, - "ги": 3511, - "ня": 3512, - "бу": 3513, - "пра": 3514, - "все": 3515, - "ется": 3516, - "сть": 3517, - "жа": 3518, - "дол": 3519, - "жи": 3520, - "бе": 3521, - "кон": 3522, - "сл": 3523, - "ши": 3524, - "ди": 3525, - "ств": 3526, - "ско": 3527, - "ные": 3528, - "чи": 3529, - "ют": 3530, - "дер": 3531, - "стра": 3532, - "ты": 3533, - "ход": 3534, - "щи": 3535, - "зо": 3536, - "зна": 3537, - "ности": 3538, - "чес": 3539, - "вля": 3540, - "вать": 3541, - "ор": 3542, - "пол": 3543, - "вет": 3544, - "так": 3545, - "ша": 3546, - "ту": 3547, - "сво": 3548, - "пре": 3549, - "она": 3550, - "итель": 3551, - "ный": 3552, - "сло": 3553, - "как": 3554, - "вл": 3555, - "ность": 3556, - "хо": 3557, - "мож": 3558, - "пе": 3559, - "для": 3560, - "ния": 3561, - "ное": 3562, - "рас": 3563, - "долж": 3564, - "дар": 3565, - "тель": 3566, - "ска": 3567, - "пу": 3568, - "ство": 3569, - "кото": 3570, - "раб": 3571, - "ее": 3572, - "род": 3573, - "эти": 3574, - "соб": 3575, - "ору": 3576, - "жен": 3577, - "ным": 3578, - "ити": 3579, - "ние": 3580, - "ком": 3581, - "дет": 3582, - "сту": 3583, - "гу": 3584, - "пи": 3585, - "меж": 3586, - "ению": 3587, - "тер": 3588, - "работ": 3589, - "воз": 3590, - "ция": 3591, - "кой": 3592, - "щест": 3593, - "гра": 3594, - "зи": 3595, - "ря": 3596, - "между": 3597, - "ства": 3598, - "вс": 3599, - "ело": 3600, - "ше": 3601, - "мер": 3602, - "ба": 3603, - "зы": 3604, - "лу": 3605, - "аль": 3606, - "дей": 3607, - "гла": 3608, - "народ": 3609, - "кти": 3610, - "предста": 3611, - "лся": 3612, - "явля": 3613, - "ски": 3614, - "нов": 3615, - "един": 3616, - "ров": 3617, - "ис": 3618, - "нима": 3619, - "рем": 3620, - "ходи": 3621, - "также": 3622, - "дру": 3623, - "ать": 3624, - "след": 3625, - "гово": 3626, - "ная": 3627, - "ющи": 3628, - "ень": 3629, - "которы": 3630, - "хот": 3631, - "ву": 3632, - "их": 3633, - "ему": 3634, - "чит": 3635, - "важ": 3636, - "орга": 3637, - "чески": 3638, - "ще": 3639, - "ке": 3640, - "ха": 3641, - "пос": 3642, - "том": 3643, - "боль": 3644, - "мне": 3645, - "пас": 3646, - "объ": 3647, - "прав": 3648, - "конф": 3649, - "слу": 3650, - "поддер": 3651, - "стви": 3652, - "наш": 3653, - "лько": 3654, - "стоя": 3655, - "ную": 3656, - "лем": 3657, - "енных": 3658, - "кра": 3659, - "ды": 3660, - "международ": 3661, - "гда": 3662, - "необ": 3663, - "госу": 3664, - "ству": 3665, - "ении": 3666, - "государ": 3667, - "кто": 3668, - "им": 3669, - "чест": 3670, - "рет": 3671, - "вопро": 3672, - "лен": 3673, - "ели": 3674, - "рова": 3675, - "ций": 3676, - "нам": 3677, - "этой": 3678, - "жения": 3679, - "необходи": 3680, - "меня": 3681, - "было": 3682, - "сили": 3683, - "фи": 3684, - "вя": 3685, - "шь": 3686, - "этого": 3687, - "они": 3688, - "органи": 3689, - "безо": 3690, - "проб": 3691, - "име": 3692, - "реш": 3693, - "би": 3694, - "безопас": 3695, - "ются": 3696, - "оста": 3697, - "енно": 3698, - "год": 3699, - "ела": 3700, - "представ": 3701, - "ться": 3702, - "слово": 3703, - "организа": 3704, - "должны": 3705, - "этом": 3706, - "бла": 3707, - "че": 3708, - "чу": 3709, - "благо": 3710, - "этому": 3711, - "врем": 3712, - "спе": 3713, - "ном": 3714, - "ений": 3715, - "спо": 3716, - "нас": 3717, - "нет": 3718, - "зу": 3719, - "вед": 3720, - "еще": 3721, - "сказа": 3722, - "сей": 3723, - "ерен": 3724, - "дан": 3725, - "сам": 3726, - "еля": 3727, - "ран": 3728, - "зыва": 3729, - "является": 3730, - "будет": 3731, - "ктив": 3732, - "тре": 3733, - "деле": 3734, - "мот": 3735, - "конферен": 3736, - "лась": 3737, - "час": 3738, - "сторо": 3739, - "кого": 3740, - "ез": 3741, - "ней": 3742, - "ос": 3743, - "лись": 3744, - "разору": 3745, - "пере": 3746, - "сси": 3747, - "ными": 3748, - "проц": 3749, - "голо": 3750, - "чело": 3751, - "боле": 3752, - "челове": 3753, - "сер": 3754, - "пл": 3755, - "чет": 3756, - "стран": 3757, - "пя": 3758, - "был": 3759, - "кла": 3760, - "тов": 3761, - "жд": 3762, - "дела": 3763, - "ера": 3764, - "уже": 3765, - "совет": 3766, - "ген": 3767, - "безопасности": 3768, - "ца": 3769, - "седа": 3770, - "поз": 3771, - "ответ": 3772, - "проблем": 3773, - "нако": 3774, - "тем": 3775, - "доста": 3776, - "пы": 3777, - "ща": 3778, - "вой": 3779, - "сущест": 3780, - "необходимо": 3781, - "быть": 3782, - "может": 3783, - "дем": 3784, - "чтобы": 3785, - "ек": 3786, - "чер": 3787, - "усили": 3788, - "рес": 3789, - "руд": 3790, - "единенных": 3791, - "доб": 3792, - "дости": 3793, - "ствен": 3794, - "ядер": 3795, - "годня": 3796, - "каза": 3797, - "сегодня": 3798, - "сейчас": 3799, - "только": 3800, - "вод": 3801, - "есь": 3802, - "много": 3803, - "буду": 3804, - "ев": 3805, - "есть": 3806, - "три": 3807, - "общест": 3808, - "явл": 3809, - "высту": 3810, - "ред": 3811, - "счит": 3812, - "сит": 3813, - "делега": 3814, - "лож": 3815, - "этот": 3816, - "фор": 3817, - "клю": 3818, - "возмож": 3819, - "вания": 3820, - "бли": 3821, - "или": 3822, - "вз": 3823, - "наций": 3824, - "ского": 3825, - "приня": 3826, - "пла": 3827, - "оч": 3828, - "иться": 3829, - "сте": 3830, - "наши": 3831, - "которые": 3832, - "ар": 3833, - "имеет": 3834, - "сот": 3835, - "знач": 3836, - "перь": 3837, - "следу": 3838, - "ены": 3839, - "таки": 3840, - "объединенных": 3841, - "стро": 3842, - "теперь": 3843, - "бле": 3844, - "благодар": 3845, - "разв": 3846, - "ан": 3847, - "жива": 3848, - "очень": 3849, - "ят": 3850, - "без": 3851, - "обес": 3852, - "гро": 3853, - "лось": 3854, - "сы": 3855, - "организации": 3856, - "член": 3857, - "того": 3858, - "ональ": 3859, - "жда": 3860, - "всех": 3861, - "свя": 3862, - "более": 3863, - "сов": 3864, - "когда": 3865, - "вот": 3866, - "кре": 3867, - "кры": 3868, - "поэтому": 3869, - "воль": 3870, - "ой": 3871, - "генера": 3872, - "чем": 3873, - "лы": 3874, - "полити": 3875, - "вен": 3876, - "конференции": 3877, - "процес": 3878, - "бя": 3879, - "ите": 3880, - "отно": 3881, - "развити": 3882, - "аф": 3883, - "ющ": 3884, - "вно": 3885, - "мир": 3886, - "нии": 3887, - "кая": 3888, - "ас": 3889, - "ительно": 3890, - "вто": 3891, - "ением": 3892, - "генераль": 3893, - "прот": 3894, - "всем": 3895, - "самбле": 3896, - "ассамбле": 3897, - "ом": 3898, - "зд": 3899, - "смот": 3900, - "реги": 3901, - "чего": 3902, - "однако": 3903, - "усилия": 3904, - "действи": 3905, - "чно": 3906, - "уча": 3907, - "образ": 3908, - "вос": 3909, - "эта": 3910, - "перего": 3911, - "говор": 3912, - "вам": 3913, - "моло": 3914, - "время": 3915, - "дь": 3916, - "хотел": 3917, - "гру": 3918, - "заявл": 3919, - "предоста": 3920, - "поль": 3921, - "нее": 3922, - "резо": 3923, - "перегово": 3924, - "резолю": 3925, - "крет": 3926, - "поддерж": 3927, - "обеспе": 3928, - "него": 3929, - "представит": 3930, - "наде": 3931, - "кри": 3932, - "чь": 3933, - "проек": 3934, - "лет": 3935, - "други": 3936, - "_": 3937, - "،": 3938, - "؛": 3939, - "؟": 3940, - "ء": 3941, - "آ": 3942, - "أ": 3943, - "ؤ": 3944, - "إ": 3945, - "ئ": 3946, - "ا": 3947, - "ب": 3948, - "ة": 3949, - "ت": 3950, - "ث": 3951, - "ج": 3952, - "ح": 3953, - "خ": 3954, - "د": 3955, - "ذ": 3956, - "ر": 3957, - "ز": 3958, - "س": 3959, - "ش": 3960, - "ص": 3961, - "ض": 3962, - "ط": 3963, - "ظ": 3964, - "ع": 3965, - "غ": 3966, - "ـ": 3967, - "ف": 3968, - "ق": 3969, - "ك": 3970, - "ل": 3971, - "م": 3972, - "ن": 3973, - "ه": 3974, - "و": 3975, - "ى": 3976, - "ي": 3977, - "ً": 3978, - "ٌ": 3979, - "ٍ": 3980, - "َ": 3981, - "ُ": 3982, - "ِ": 3983, - "ّ": 3984, - "ْ": 3985, - "ٰ": 3986, - "چ": 3987, - "ڨ": 3988, - "ک": 3989, - "ھ": 3990, - "ی": 3991, - "ۖ": 3992, - "ۗ": 3993, - "ۘ": 3994, - "ۚ": 3995, - "ۛ": 3996, - "—": 3997, - "☭": 3998, - "ﺃ": 3999, - "ﻻ": 4000, - "ال": 4001, - "َا": 4002, - "وَ": 4003, - "َّ": 4004, - "ِي": 4005, - "أَ": 4006, - "لَ": 4007, - "نَ": 4008, - "الْ": 4009, - "هُ": 4010, - "ُو": 4011, - "ما": 4012, - "نْ": 4013, - "من": 4014, - "عَ": 4015, - "نا": 4016, - "لا": 4017, - "مَ": 4018, - "تَ": 4019, - "فَ": 4020, - "أن": 4021, - "لي": 4022, - "مِ": 4023, - "ان": 4024, - "في": 4025, - "رَ": 4026, - "يَ": 4027, - "هِ": 4028, - "مْ": 4029, - "قَ": 4030, - "بِ": 4031, - "لى": 4032, - "ين": 4033, - "إِ": 4034, - "لِ": 4035, - "وا": 4036, - "كَ": 4037, - "ها": 4038, - "ًا": 4039, - "مُ": 4040, - "ون": 4041, - "الم": 4042, - "بَ": 4043, - "يا": 4044, - "ذا": 4045, - "سا": 4046, - "الل": 4047, - "مي": 4048, - "يْ": 4049, - "را": 4050, - "ري": 4051, - "لك": 4052, - "مَا": 4053, - "نَّ": 4054, - "لم": 4055, - "إن": 4056, - "ست": 4057, - "وم": 4058, - "َّا": 4059, - "لَا": 4060, - "هم": 4061, - "ِّ": 4062, - "كُ": 4063, - "كان": 4064, - "سَ": 4065, - "با": 4066, - "دي": 4067, - "حَ": 4068, - "عْ": 4069, - "بي": 4070, - "الأ": 4071, - "ول": 4072, - "فِي": 4073, - "رِ": 4074, - "دا": 4075, - "مِنْ": 4076, - "ُونَ": 4077, - "وْ": 4078, - "هَا": 4079, - "ُّ": 4080, - "الس": 4081, - "الَ": 4082, - "ني": 4083, - "لْ": 4084, - "تُ": 4085, - "هل": 4086, - "رة": 4087, - "دَ": 4088, - "سْ": 4089, - "تِ": 4090, - "نَا": 4091, - "رْ": 4092, - "اللَّ": 4093, - "سامي": 4094, - "كن": 4095, - "كل": 4096, - "هَ": 4097, - "عَلَ": 4098, - "على": 4099, - "مع": 4100, - "إلى": 4101, - "قد": 4102, - "الر": 4103, - "ُوا": 4104, - "ير": 4105, - "عن": 4106, - "يُ": 4107, - "نِ": 4108, - "بْ": 4109, - "الح": 4110, - "هُمْ": 4111, - "قا": 4112, - "ذه": 4113, - "الت": 4114, - "ِينَ": 4115, - "جَ": 4116, - "هذا": 4117, - "عد": 4118, - "الع": 4119, - "دْ": 4120, - "قَالَ": 4121, - "رُ": 4122, - "يم": 4123, - "ية": 4124, - "نُ": 4125, - "خَ": 4126, - "رب": 4127, - "الك": 4128, - "وَا": 4129, - "أنا": 4130, - "ةِ": 4131, - "الن": 4132, - "حد": 4133, - "عِ": 4134, - "تا": 4135, - "هو": 4136, - "فا": 4137, - "عا": 4138, - "الش": 4139, - "لُ": 4140, - "يت": 4141, - "ذَا": 4142, - "يع": 4143, - "الذ": 4144, - "حْ": 4145, - "الص": 4146, - "إِنَّ": 4147, - "جا": 4148, - "علي": 4149, - "كَا": 4150, - "بُ": 4151, - "تع": 4152, - "وق": 4153, - "مل": 4154, - "لَّ": 4155, - "يد": 4156, - "أخ": 4157, - "رف": 4158, - "تي": 4159, - "الِ": 4160, - "ّا": 4161, - "ذلك": 4162, - "أَنْ": 4163, - "سِ": 4164, - "توم": 4165, - "مر": 4166, - "مَنْ": 4167, - "بل": 4168, - "الق": 4169, - "الله": 4170, - "ِيَ": 4171, - "كم": 4172, - "ذَ": 4173, - "عل": 4174, - "حب": 4175, - "سي": 4176, - "عُ": 4177, - "الج": 4178, - "الد": 4179, - "شَ": 4180, - "تك": 4181, - "فْ": 4182, - "صَ": 4183, - "لل": 4184, - "دِ": 4185, - "بر": 4186, - "فِ": 4187, - "ته": 4188, - "أع": 4189, - "تْ": 4190, - "قْ": 4191, - "الْأَ": 4192, - "ئِ": 4193, - "عَنْ": 4194, - "ور": 4195, - "حا": 4196, - "الَّ": 4197, - "مت": 4198, - "فر": 4199, - "دُ": 4200, - "هنا": 4201, - "وَأَ": 4202, - "تب": 4203, - "ةُ": 4204, - "أي": 4205, - "سب": 4206, - "ريد": 4207, - "وج": 4208, - "كُمْ": 4209, - "حِ": 4210, - "كْ": 4211, - "در": 4212, - "َاء": 4213, - "هذه": 4214, - "الط": 4215, - "الْمُ": 4216, - "دة": 4217, - "قل": 4218, - "غَ": 4219, - "يوم": 4220, - "الَّذ": 4221, - "كر": 4222, - "تر": 4223, - "كِ": 4224, - "كي": 4225, - "عَلَى": 4226, - "رَب": 4227, - "عة": 4228, - "قُ": 4229, - "جْ": 4230, - "فض": 4231, - "لة": 4232, - "هْ": 4233, - "رَا": 4234, - "وَلَ": 4235, - "الْمَ": 4236, - "أَنَّ": 4237, - "يَا": 4238, - "أُ": 4239, - "شي": 4240, - "اللَّهُ": 4241, - "لَى": 4242, - "قِ": 4243, - "أت": 4244, - "عَلَيْ": 4245, - "اللَّهِ": 4246, - "الب": 4247, - "ضَ": 4248, - "ةً": 4249, - "قي": 4250, - "ار": 4251, - "بد": 4252, - "خْ": 4253, - "سْتَ": 4254, - "طَ": 4255, - "قَدْ": 4256, - "ذهب": 4257, - "أم": 4258, - "ماذا": 4259, - "وَإِ": 4260, - "ةٌ": 4261, - "ونَ": 4262, - "ليلى": 4263, - "ولا": 4264, - "حُ": 4265, - "هي": 4266, - "صل": 4267, - "الخ": 4268, - "ود": 4269, - "ليس": 4270, - "لدي": 4271, - "قال": 4272, - "كَانَ": 4273, - "مَّ": 4274, - "حي": 4275, - "تم": 4276, - "لن": 4277, - "وَلَا": 4278, - "بع": 4279, - "يمكن": 4280, - "سُ": 4281, - "ةَ": 4282, - "حت": 4283, - "رًا": 4284, - "كا": 4285, - "شا": 4286, - "هِمْ": 4287, - "لَهُ": 4288, - "زَ": 4289, - "داً": 4290, - "مس": 4291, - "كث": 4292, - "الْعَ": 4293, - "جِ": 4294, - "صْ": 4295, - "فَا": 4296, - "له": 4297, - "وي": 4298, - "عَا": 4299, - "هُوَ": 4300, - "بِي": 4301, - "بَا": 4302, - "أس": 4303, - "ثَ": 4304, - "لِي": 4305, - "رض": 4306, - "الرَّ": 4307, - "لِكَ": 4308, - "تَّ": 4309, - "فُ": 4310, - "قة": 4311, - "فعل": 4312, - "مِن": 4313, - "الآ": 4314, - "ثُ": 4315, - "سم": 4316, - "مَّا": 4317, - "بِهِ": 4318, - "تق": 4319, - "خر": 4320, - "لقد": 4321, - "خل": 4322, - "شر": 4323, - "أنت": 4324, - "لَّا": 4325, - "سن": 4326, - "السَّ": 4327, - "الذي": 4328, - "سَا": 4329, - "وما": 4330, - "زل": 4331, - "وب": 4332, - "أْ": 4333, - "إذا": 4334, - "رِي": 4335, - "حة": 4336, - "نِي": 4337, - "الْحَ": 4338, - "وَقَالَ": 4339, - "به": 4340, - "ةٍ": 4341, - "سأ": 4342, - "رٌ": 4343, - "بال": 4344, - "مة": 4345, - "شْ": 4346, - "وت": 4347, - "عند": 4348, - "فس": 4349, - "بَعْ": 4350, - "هر": 4351, - "قط": 4352, - "أح": 4353, - "إنه": 4354, - "وع": 4355, - "فت": 4356, - "غا": 4357, - "هناك": 4358, - "بت": 4359, - "مِنَ": 4360, - "سر": 4361, - "ذَلِكَ": 4362, - "رس": 4363, - "حدث": 4364, - "غْ": 4365, - "ِّي": 4366, - "الإ": 4367, - "وَيَ": 4368, - "جل": 4369, - "است": 4370, - "قِي": 4371, - "عب": 4372, - "وس": 4373, - "يش": 4374, - "الَّذِينَ": 4375, - "تاب": 4376, - "دِي": 4377, - "جب": 4378, - "كون": 4379, - "بن": 4380, - "الث": 4381, - "لَيْ": 4382, - "بعد": 4383, - "وَالْ": 4384, - "فَأَ": 4385, - "عم": 4386, - "هُم": 4387, - "تن": 4388, - "ذْ": 4389, - "أص": 4390, - "أين": 4391, - "رَبِّ": 4392, - "الذين": 4393, - "إِن": 4394, - "بين": 4395, - "جُ": 4396, - "عَلَيْهِ": 4397, - "حَا": 4398, - "لو": 4399, - "ستط": 4400, - "ظر": 4401, - "لَمْ": 4402, - "ءِ": 4403, - "كُل": 4404, - "طل": 4405, - "تَا": 4406, - "ضُ": 4407, - "كنت": 4408, - "لًا": 4409, - "مٌ": 4410, - "قبل": 4411, - "ــ": 4412, - "ذِ": 4413, - "قَوْ": 4414, - "صِ": 4415, - "مًا": 4416, - "كانت": 4417, - "صا": 4418, - "يق": 4419, - "الف": 4420, - "النا": 4421, - "مٍ": 4422, - "إِنْ": 4423, - "النَّ": 4424, - "جد": 4425, - "وَمَا": 4426, - "تت": 4427, - "بح": 4428, - "مكان": 4429, - "كيف": 4430, - "ّة": 4431, - "الا": 4432, - "جَا": 4433, - "أو": 4434, - "ساعد": 4435, - "ضِ": 4436, - "إلا": 4437, - "راً": 4438, - "قَا": 4439, - "رأ": 4440, - "عت": 4441, - "أحد": 4442, - "هد": 4443, - "ضا": 4444, - "طر": 4445, - "أق": 4446, - "ماء": 4447, - "دَّ": 4448, - "البا": 4449, - "مُو": 4450, - "أَوْ": 4451, - "طا": 4452, - "قُو": 4453, - "خِ": 4454, - "تل": 4455, - "ستطيع": 4456, - "دَا": 4457, - "النَّا": 4458, - "إلَى": 4459, - "وَتَ": 4460, - "هَذَا": 4461, - "بة": 4462, - "عليك": 4463, - "جر": 4464, - "المن": 4465, - "زا": 4466, - "رٍ": 4467, - "دع": 4468, - "ًّا": 4469, - "سة": 4470, - "ثُمَّ": 4471, - "شيء": 4472, - "الغ": 4473, - "تح": 4474, - "رُونَ": 4475, - "اليوم": 4476, - "مِي": 4477, - "نُوا": 4478, - "أر": 4479, - "تُمْ": 4480, - "عر": 4481, - "يف": 4482, - "أب": 4483, - "دًا": 4484, - "صَا": 4485, - "التَّ": 4486, - "أريد": 4487, - "الز": 4488, - "يَوْ": 4489, - "إلي": 4490, - "جي": 4491, - "يَعْ": 4492, - "فضل": 4493, - "الإن": 4494, - "أنه": 4495, - "1": 4496, - "2": 4497, - "3": 4498, - "4": 4499, - "5": 4500, - "·": 4501, - "×": 4502, - "̃": 4503, - "̌": 4504, - "ε": 4505, - "λ": 4506, - "μ": 4507, - "•": 4508, - "‧": 4509, - "─": 4510, - "□": 4511, - "、": 4512, - "。": 4513, - "〈": 4514, - "〉": 4515, - "《": 4516, - "》": 4517, - "「": 4518, - "」": 4519, - "『": 4520, - "』": 4521, - "ア": 4522, - "オ": 4523, - "カ": 4524, - "チ": 4525, - "ド": 4526, - "ベ": 4527, - "ャ": 4528, - "ヤ": 4529, - "ン": 4530, - "・": 4531, - "ー": 4532, - "ㄟ": 4533, - "!": 4534, - "(": 4535, - ")": 4536, - ",": 4537, - "-": 4538, - "/": 4539, - ":": 4540, - ";": 4541, - "?": 4542, - "p": 4543, - "i4": 4544, - "zh": 4545, - "i2": 4546, - "ng1": 4547, - "u4": 4548, - "i1": 4549, - "ng2": 4550, - "u3": 4551, - "de5": 4552, - "e4": 4553, - "i3": 4554, - "ng4": 4555, - "an4": 4556, - "shi4": 4557, - "an2": 4558, - "u2": 4559, - "u1": 4560, - "ng3": 4561, - "a1": 4562, - "an1": 4563, - "e2": 4564, - "a4": 4565, - "ei4": 4566, - "ong1": 4567, - "ai4": 4568, - "ao4": 4569, - "ang1": 4570, - "an3": 4571, - "wei4": 4572, - "uo2": 4573, - "n1": 4574, - "en2": 4575, - "ao3": 4576, - "e1": 4577, - "qi": 4578, - "eng2": 4579, - "zho": 4580, - "ang3": 4581, - "ang4": 4582, - "ang2": 4583, - "uo4": 4584, - "ge4": 4585, - "yi1": 4586, - "guo2": 4587, - "a3": 4588, - "he2": 4589, - "e3": 4590, - "yi2": 4591, - "di4": 4592, - "zhong1": 4593, - "bu4": 4594, - "ai2": 4595, - "n2": 4596, - "zai4": 4597, - "shi2": 4598, - "eng1": 4599, - "ren2": 4600, - "ong2": 4601, - "xian4": 4602, - "xu": 4603, - "n4": 4604, - "li4": 4605, - "en4": 4606, - "yu2": 4607, - "ei2": 4608, - "yi2ge4": 4609, - "ou4": 4610, - "ei3": 4611, - "ui4": 4612, - "a2": 4613, - "you3": 4614, - "ao1": 4615, - "da4": 4616, - "cheng2": 4617, - "en1": 4618, - "eng4": 4619, - "yi4": 4620, - "si1": 4621, - "zhi4": 4622, - "jia1": 4623, - "yuan2": 4624, - "ta1": 4625, - "de5yi2ge4": 4626, - "ke1": 4627, - "shu3": 4628, - "xi1": 4629, - "ji2": 4630, - "ao2": 4631, - "ou3": 4632, - "ong4": 4633, - "xia4": 4634, - "ai1": 4635, - "gong1": 4636, - "zhi1": 4637, - "en3": 4638, - "wei2": 4639, - "xue2": 4640, - "qu1": 4641, - "zhou1": 4642, - "er3": 4643, - "ming2": 4644, - "zhong3": 4645, - "li3": 4646, - "wu4": 4647, - "yi3": 4648, - "uo1": 4649, - "e5": 4650, - "ji4": 4651, - "xing2": 4652, - "jian4": 4653, - "hua4": 4654, - "yu3": 4655, - "uo3": 4656, - "ji1": 4657, - "ai3": 4658, - "zuo4": 4659, - "hou4": 4660, - "hui4": 4661, - "ei1": 4662, - "nian2": 4663, - "qi2": 4664, - "dao4": 4665, - "sheng1": 4666, - "de2": 4667, - "dai4": 4668, - "uan2": 4669, - "zhe4": 4670, - "zheng4": 4671, - "ben3": 4672, - "shang4": 4673, - "zhu3": 4674, - "bei4": 4675, - "ye4": 4676, - "chu1": 4677, - "zhan4": 4678, - "le5": 4679, - "lai2": 4680, - "shi3": 4681, - "nan2": 4682, - "ren4": 4683, - "you2": 4684, - "ke4": 4685, - "ba1": 4686, - "fu4": 4687, - "dui4": 4688, - "ya4": 4689, - "mei3": 4690, - "zi4": 4691, - "xin1": 4692, - "jing1": 4693, - "zhu": 4694, - "n3": 4695, - "yong4": 4696, - "mu4": 4697, - "jiao4": 4698, - "ye3": 4699, - "jin4": 4700, - "bian4": 4701, - "lu4": 4702, - "qi1": 4703, - "she4": 4704, - "xiang1": 4705, - "ong3": 4706, - "shu4": 4707, - "dong4": 4708, - "suo3": 4709, - "guan1": 4710, - "san1": 4711, - "te4": 4712, - "duo1": 4713, - "fu2": 4714, - "min2": 4715, - "la1": 4716, - "zhi2": 4717, - "zhen4": 4718, - "ou1": 4719, - "wu3": 4720, - "ma3": 4721, - "i5": 4722, - "zi5": 4723, - "ju4": 4724, - "er4": 4725, - "yao4": 4726, - "xia4de5yi2ge4": 4727, - "si4": 4728, - "tu2": 4729, - "shan1": 4730, - "zui4": 4731, - "yin1": 4732, - "er2": 4733, - "tong2": 4734, - "dong1": 4735, - "yu4": 4736, - "yan2": 4737, - "qian2": 4738, - "shu3xia4de5yi2ge4": 4739, - "jun1": 4740, - "ke3": 4741, - "wen2": 4742, - "fa3": 4743, - "luo2": 4744, - "zhu4": 4745, - "xi4": 4746, - "kou3": 4747, - "bei3": 4748, - "jian1": 4749, - "fa1": 4750, - "dian4": 4751, - "jiang1": 4752, - "wei4yu2": 4753, - "xiang4": 4754, - "zhi3": 4755, - "eng3": 4756, - "fang1": 4757, - "lan2": 4758, - "shu": 4759, - "ri4": 4760, - "lian2": 4761, - "shou3": 4762, - "qiu2": 4763, - "jin1": 4764, - "huo4": 4765, - "shu3xia4de5yi2ge4zhong3": 4766, - "fen1": 4767, - "nei4": 4768, - "gai1": 4769, - "mei3guo2": 4770, - "un2": 4771, - "ge2": 4772, - "bao3": 4773, - "qing1": 4774, - "gao1": 4775, - "tai2": 4776, - "xiao3": 4777, - "jie2": 4778, - "tian1": 4779, - "chang2": 4780, - "quan2": 4781, - "lie4": 4782, - "hai3": 4783, - "fei1": 4784, - "ti3": 4785, - "jue2": 4786, - "ou2": 4787, - "ci3": 4788, - "zu2": 4789, - "ni2": 4790, - "biao3": 4791, - "zhong1guo2": 4792, - "du4": 4793, - "yue4": 4794, - "xing4": 4795, - "sheng4": 4796, - "che1": 4797, - "dan1": 4798, - "jie1": 4799, - "lin2": 4800, - "ping2": 4801, - "fu3": 4802, - "gu3": 4803, - "jie4": 4804, - "v3": 4805, - "sheng3": 4806, - "na4": 4807, - "yuan4": 4808, - "zhang3": 4809, - "guan3": 4810, - "dao3": 4811, - "zu3": 4812, - "ding4": 4813, - "dian3": 4814, - "ceng2": 4815, - "ren2kou3": 4816, - "tai4": 4817, - "tong1": 4818, - "guo4": 4819, - "neng2": 4820, - "chang3": 4821, - "hua2": 4822, - "liu2": 4823, - "ying1": 4824, - "xiao4": 4825, - "ci4": 4826, - "bian4hua4": 4827, - "liang3": 4828, - "gong4": 4829, - "zhong4": 4830, - "de5yi1": 4831, - "se4": 4832, - "kai1": 4833, - "wang2": 4834, - "jiu4": 4835, - "shi1": 4836, - "shou4": 4837, - "mei2": 4838, - "feng1": 4839, - "ze2": 4840, - "tu2shi4": 4841, - "ti2": 4842, - "qi4": 4843, - "jiu3": 4844, - "shen1": 4845, - "zhe3": 4846, - "ren2kou3bian4hua4": 4847, - "ren2kou3bian4hua4tu2shi4": 4848, - "di4qu1": 4849, - "yang2": 4850, - "men5": 4851, - "long2": 4852, - "bing4": 4853, - "chan3": 4854, - "zhu1": 4855, - "wei3": 4856, - "wai4": 4857, - "xing1": 4858, - "bo1": 4859, - "bi3": 4860, - "tang2": 4861, - "hua1": 4862, - "bo2": 4863, - "shui3": 4864, - "shu1": 4865, - "dou1": 4866, - "sai4": 4867, - "chao2": 4868, - "bi4": 4869, - "ling2": 4870, - "lei4": 4871, - "da4xue2": 4872, - "fen4": 4873, - "shu3de5": 4874, - "mu3": 4875, - "jiao1": 4876, - "dang1": 4877, - "cheng1": 4878, - "tong3": 4879, - "nv3": 4880, - "qi3": 4881, - "yan3": 4882, - "mian4": 4883, - "luo4": 4884, - "jing4": 4885, - "ge1": 4886, - "ru4": 4887, - "dan4": 4888, - "ri4ben3": 4889, - "pu3": 4890, - "yun4": 4891, - "huang2": 4892, - "wo3": 4893, - "lv": 4894, - "hai2": 4895, - "shi4yi1": 4896, - "xie1": 4897, - "ying3": 4898, - "wu2": 4899, - "shen2": 4900, - "wang3": 4901, - "guang3": 4902, - "liu4": 4903, - "su4": 4904, - "shi4zhen4": 4905, - "can1": 4906, - "cao3": 4907, - "xia2": 4908, - "ka3": 4909, - "da2": 4910, - "hu4": 4911, - "ban4": 4912, - "dang3": 4913, - "hu2": 4914, - "zong3": 4915, - "deng3": 4916, - "de5yi2ge4shi4zhen4": 4917, - "chuan2": 4918, - "mo4": 4919, - "zhang1": 4920, - "ban1": 4921, - "mo2": 4922, - "cha2": 4923, - "ce4": 4924, - "zhu3yao4": 4925, - "tou2": 4926, - "ju2": 4927, - "shi4wei4yu2": 4928, - "sa4": 4929, - "un1": 4930, - "ke3yi3": 4931, - "du1": 4932, - "han4": 4933, - "liang4": 4934, - "sha1": 4935, - "jia3": 4936, - "zi1": 4937, - "lv4": 4938, - "fu1": 4939, - "xian1": 4940, - "xu4": 4941, - "guang1": 4942, - "meng2": 4943, - "bao4": 4944, - "you4": 4945, - "rong2": 4946, - "zhi1yi1": 4947, - "wei1": 4948, - "mao2": 4949, - "guo2jia1": 4950, - "cong2": 4951, - "gou4": 4952, - "tie3": 4953, - "zhen1": 4954, - "du2": 4955, - "bian1": 4956, - "ci2": 4957, - "qu3": 4958, - "fan4": 4959, - "xiang3": 4960, - "men2": 4961, - "ju1": 4962, - "hong2": 4963, - "zi3": 4964, - "ta1men5": 4965, - "ji3": 4966, - "zong1": 4967, - "zhou1de5yi2ge4shi4zhen4": 4968, - "tuan2": 4969, - "jing3": 4970, - "gong1si1": 4971, - "xie4": 4972, - "li2": 4973, - "li4shi3": 4974, - "bao1": 4975, - "gang3": 4976, - "gui1": 4977, - "zheng1": 4978, - "zhi2wu4": 4979, - "ta1de5": 4980, - "pin3": 4981, - "zhuan1": 4982, - "chong2": 4983, - "shi3yong4": 4984, - "wa3": 4985, - "shuo1": 4986, - "chuan1": 4987, - "lei2": 4988, - "wan1": 4989, - "huo2": 4990, - "su1": 4991, - "zao3": 4992, - "gai3": 4993, - "qu4": 4994, - "gu4": 4995, - "xi2": 4996, - "hang2": 4997, - "ying4": 4998, - "cun1": 4999, - "gen1": 5000, - "ying2": 5001, - "ting2": 5002, - "cheng2shi4": 5003, - "jiang3": 5004, - "ling3": 5005, - "lun2": 5006, - "bu4fen4": 5007, - "deng1": 5008, - "xuan3": 5009, - "dong4wu4": 5010, - "de2guo2": 5011, - "xian3": 5012, - "fan3": 5013, - "zhe5": 5014, - "han2": 5015, - "hao4": 5016, - "mi4": 5017, - "ran2": 5018, - "qin1": 5019, - "tiao2": 5020, - "zhan3": 5021, - "[ar]": 5022, - "[zh-cn]": 5023, - "¡":5024, - "é":5025, - "shi": 5026, - "tsu": 5027, - "teki": 5028, - "nai": 5029, - "aru": 5030, - "uu": 5031, - "kai": 5032, - "shite": 5033, - "mono": 5034, - "koto": 5035, - "kara": 5036, - "shita": 5037, - "suru": 5038, - "masu": 5039, - "tai": 5040, - "ware": 5041, - "shin": 5042, - "oku": 5043, - "yuu": 5044, - "iru": 5045, - "jiko": 5046, - "desu": 5047, - "rare": 5048, - "shou": 5049, - "sha": 5050, - "sekai": 5051, - "kyou": 5052, - "mashita": 5053, - "nara": 5054, - "kei": 5055, - "ita": 5056, - "ari": 5057, - "itsu": 5058, - "kono": 5059, - "naka": 5060, - "chou": 5061, - "sore": 5062, - "naru": 5063, - "gaku": 5064, - "reba": 5065, - "hito": 5066, - "sai": 5067, - "nan": 5068, - "dai": 5069, - "tsuku": 5070, - "shiki": 5071, - "sare": 5072, - "naku": 5073, - "jun": 5074, - "kaku": 5075, - "zai": 5076, - "wata": 5077, - "shuu": 5078, - "ii": 5079, - "kare": 5080, - "shii": 5081, - "made": 5082, - "sho": 5083, - "kereba": 5084, - "shika": 5085, - "ichi": 5086, - "deki": 5087, - "nin": 5088, - "wareware": 5089, - "nakereba": 5090, - "oite": 5091, - "yaku": 5092, - "mujun": 5093, - "yoku": 5094, - "butsu": 5095, - "omo": 5096, - "gae": 5097, - "naranai": 5098, - "tachi": 5099, - "chuu": 5100, - "kangae": 5101, - "toki": 5102, - "koro": 5103, - "mujunteki": 5104, - "naga": 5105, - "jin": 5106, - "shima": 5107, - "iku": 5108, - "imasu": 5109, - "hon": 5110, - "kae": 5111, - "kore": 5112, - "kita": 5113, - "datta": 5114, - "jitsu": 5115, - "mae": 5116, - "toku": 5117, - "douitsu": 5118, - "ritsu": 5119, - "kyuu": 5120, - "hyou": 5121, - "rareta": 5122, - "keisei": 5123, - "kkan": 5124, - "rareru": 5125, - "mou": 5126, - "doko": 5127, - "ryou": 5128, - "dake": 5129, - "nakatta": 5130, - "soko": 5131, - "tabe": 5132, - "hana": 5133, - "fuku": 5134, - "yasu": 5135, - "wataku": 5136, - "yama": 5137, - "kyo": 5138, - "genzai": 5139, - "boku": 5140, - "ata": 5141, - "kawa": 5142, - "masen": 5143, - "juu": 5144, - "natte": 5145, - "watakushi": 5146, - "yotte": 5147, - "hai": 5148, - "jishin": 5149, - "rete": 5150, - "oka": 5151, - "kagaku": 5152, - "natta": 5153, - "karu": 5154, - "nari": 5155, - "mata": 5156, - "kuru": 5157, - "gai": 5158, - "kari": 5159, - "shakai": 5160, - "koui": 5161, - "yori": 5162, - "setsu": 5163, - "reru": 5164, - "tokoro": 5165, - "jutsu": 5166, - "saku": 5167, - "ttai": 5168, - "ningen": 5169, - "tame": 5170, - "kankyou": 5171, - "ooku": 5172, - "watashi": 5173, - "tsukuru": 5174, - "sugi": 5175, - "jibun": 5176, - "shitsu": 5177, - "keru": 5178, - "kishi": 5179, - "shikashi": 5180, - "moto": 5181, - "mari": 5182, - "itte": 5183, - "deshita": 5184, - "nde": 5185, - "arimasu": 5186, - "koe": 5187, - "zettai": 5188, - "kkanteki": 5189, - "rekishi": 5190, - "dekiru": 5191, - "tsuka": 5192, - "itta": 5193, - "kobutsu": 5194, - "miru": 5195, - "shoku": 5196, - "shimasu": 5197, - "gijutsu": 5198, - "gyou": 5199, - "joushiki": 5200, - "atta": 5201, - "hodo": 5202, - "koko": 5203, - "tsukurareta": 5204, - "zoku": 5205, - "hitei": 5206, - "koku": 5207, - "rekishiteki": 5208, - "kete": 5209, - "kako": 5210, - "nagara": 5211, - "kakaru": 5212, - "shutai": 5213, - "haji": 5214, - "taku": 5215, - "douitsuteki": 5216, - "mete": 5217, - "tsuu": 5218, - "sarete": 5219, - "genjitsu": 5220, - "bai": 5221, - "nawa": 5222, - "jikan": 5223, - "waru": 5224, - "rt": 5225, - "atsu": 5226, - "soku": 5227, - "kouiteki": 5228, - "kata": 5229, - "tetsu": 5230, - "gawa": 5231, - "kedo": 5232, - "reta": 5233, - "sayou": 5234, - "tteru": 5235, - "tori": 5236, - "kimi": 5237, - "mura": 5238, - "sareru": 5239, - "machi": 5240, - "kya": 5241, - "osa": 5242, - "konna": 5243, - "aku": 5244, - "sareta": 5245, - "ipp": 5246, - "shiku": 5247, - "uchi": 5248, - "hitotsu": 5249, - "hatara": 5250, - "tachiba": 5251, - "shiro": 5252, - "katachi": 5253, - "tomo": 5254, - "ete": 5255, - "meru": 5256, - "nichi": 5257, - "dare": 5258, - "katta": 5259, - "eru": 5260, - "suki": 5261, - "ooki": 5262, - "maru": 5263, - "moku": 5264, - "oko": 5265, - "kangaerareru": 5266, - "oto": 5267, - "tanni": 5268, - "tada": 5269, - "taiteki": 5270, - "motte": 5271, - "kinou": 5272, - "shinai": 5273, - "kki": 5274, - "tari": 5275, - "ranai": 5276, - "kkou": 5277, - "mirai": 5278, - "ppon": 5279, - "goto": 5280, - "hitsu": 5281, - "teru": 5282, - "mochi": 5283, - "katsu": 5284, - "nyuu": 5285, - "zuka": 5286, - "tsuite": 5287, - "nomi": 5288, - "sugu": 5289, - "kuda": 5290, - "tetsugaku": 5291, - "ika": 5292, - "ronri": 5293, - "oki": 5294, - "nippon": 5295, - "shimashita": 5296, - "chishiki": 5297, - "chokkanteki": 5298, - "suko": 5299, - "kuu": 5300, - "arou": 5301, - "katte": 5302, - "kuri": 5303, - "inai": 5304, - "hyougen": 5305, - "ishiki": 5306, - "doku": 5307, - "atte": 5308, - "atara": 5309, - "wari": 5310, - "kao": 5311, - "seisan": 5312, - "hanashi": 5313, - "kake": 5314, - "naji": 5315, - "sunawa": 5316, - "sunawachi": 5317, - "ugo": 5318, - "suu": 5319, - "bara": 5320, - "hiro": 5321, - "iwa": 5322, - "betsu": 5323, - "yoi": 5324, - "seru": 5325, - "shiteru": 5326, - "rarete": 5327, - "toshi": 5328, - "seki": 5329, - "tairitsu": 5330, - "wakara": 5331, - "tokyo": 5332, - "kka": 5333, - "kyoku": 5334, - "iro": 5335, - "mite": 5336, - "saki": 5337, - "kanji": 5338, - "mita": 5339, - "sube": 5340, - "ryoku": 5341, - "matta": 5342, - "kudasai": 5343, - "omoi": 5344, - "wareru": 5345, - "hitsuyou": 5346, - "kashi": 5347, - "renai": 5348, - "kankei": 5349, - "gatte": 5350, - "ochi": 5351, - "motsu": 5352, - "sonzai": 5353, - "taishite": 5354, - "ame": 5355, - "seimei": 5356, - "kano": 5357, - "giri": 5358, - "kangaeru": 5359, - "yue": 5360, - "asa": 5361, - "onaji": 5362, - "yoru": 5363, - "niku": 5364, - "osaka": 5365, - "sukoshi": 5366, - "tama": 5367, - "kanojo": 5368, - "kite": 5369, - "mondai": 5370, - "amari": 5371, - "eki": 5372, - "kojin": 5373, - "haya": 5374, - "dete": 5375, - "atarashii": 5376, - "awa": 5377, - "gakkou": 5378, - "tsuzu": 5379, - "shukan": 5380, - "imashita": 5381, - "atae": 5382, - "darou": 5383, - "hataraku": 5384, - "gata": 5385, - "dachi": 5386, - "matsu": 5387, - "arimasen": 5388, - "seibutsu": 5389, - "mitsu": 5390, - "heya": 5391, - "yasui": 5392, - "deni": 5393, - "noko": 5394, - "haha": 5395, - "domo": 5396, - "kami": 5397, - "sudeni": 5398, - "nao": 5399, - "raku": 5400, - "ike": 5401, - "meta": 5402, - "kodomo": 5403, - "soshite": 5404, - "game": 5405, - "bakari": 5406, - "tote": 5407, - "hatsu": 5408, - "mise": 5409, - "mokuteki": 5410, - "dakara": 5411, - "[ja]": 5412, - "ő": 5413, - "ű": 5414, - "そ": 5415, - "な": 5416, - "ん": 5417, - "포": 5418, - "�": 5419, - "gy": 5420, - "eg": 5421, - "cs": 5422, - "ál": 5423, - "egy": 5424, - "át": 5425, - "ott": 5426, - "ett": 5427, - "meg": 5428, - "hogy": 5429, - "ég": 5430, - "ól": 5431, - "nek": 5432, - "volt": 5433, - "ág": 5434, - "nk": 5435, - "ék": 5436, - "ít": 5437, - "ák": 5438, - "ud": 5439, - "szer": 5440, - "mind": 5441, - "oz": 5442, - "ép": 5443, - "ért": 5444, - "mond": 5445, - "szt": 5446, - "nak": 5447, - "ől": 5448, - "csak": 5449, - "oly": 5450, - "áll": 5451, - "ány": 5452, - "mint": 5453, - "már": 5454, - "ött": 5455, - "nagy": 5456, - "ész": 5457, - "azt": 5458, - "elő": 5459, - "tud": 5460, - "ény": 5461, - "áz": 5462, - "még": 5463, - "köz": 5464, - "ely": 5465, - "ség": 5466, - "hoz": 5467, - "uk": 5468, - "kez": 5469, - "ám": 5470, - "aj": 5471, - "unk": 5472, - "vagy": 5473, - "szem": 5474, - "ember": 5475, - "fog": 5476, - "mert": 5477, - "ös": 5478, - "ság": 5479, - "leg": 5480, - "ünk": 5481, - "hát": 5482, - "ony": 5483, - "ezt": 5484, - "minden": 5485, - "ült": 5486, - "jó": 5487, - "kis": 5488, - "áj": 5489, - "úgy": 5490, - "most": 5491, - "ír": 5492, - "itt": 5493, - "elt": 5494, - "mondta": 5495, - "kell": 5496, - "ált": 5497, - "érd": 5498, - "tö": 5499, - "vár": 5500, - "lát": 5501, - "ők": 5502, - "vet": 5503, - "után": 5504, - "két": 5505, - "nap": 5506, - "ív": 5507, - "ály": 5508, - "vég": 5509, - "ök": 5510, - "dul": 5511, - "néz": 5512, - "ában": 5513, - "kül": 5514, - "akkor": 5515, - "szél": 5516, - "új": 5517, - "olyan": 5518, - "ked": 5519, - "hely": 5520, - "tör": 5521, - "ból": 5522, - "elm": 5523, - "ára": 5524, - "ló": 5525, - "volna": 5526, - "lehet": 5527, - "ebb": 5528, - "sok": 5529, - "olt": 5530, - "eket": 5531, - "bor": 5532, - "fej": 5533, - "gond": 5534, - "akar": 5535, - "fél": 5536, - "úl": 5537, - "otta": 5538, - "valami": 5539, - "jel": 5540, - "éd": 5541, - "arc": 5542, - "hall": 5543, - "föl": 5544, - "ába": 5545, - "olg": 5546, - "kir": 5547, - "old": 5548, - "kérd": 5549, - "jár": 5550, - "úr": 5551, - "zs": 5552, - "élet": 5553, - "ját": 5554, - "ov": 5555, - "éz": 5556, - "vil": 5557, - "őr": 5558, - "ög": 5559, - "lesz": 5560, - "koz": 5561, - "ább": 5562, - "király": 5563, - "eng": 5564, - "igaz": 5565, - "haj": 5566, - "kod": 5567, - "ról": 5568, - "több": 5569, - "szó": 5570, - "ében": 5571, - "öt": 5572, - "nyi": 5573, - "szól": 5574, - "gondol": 5575, - "egész": 5576, - "így": 5577, - "ős": 5578, - "obb": 5579, - "osan": 5580, - "ből": 5581, - "abb": 5582, - "őt": 5583, - "nál": 5584, - "kép": 5585, - "aztán": 5586, - "tart": 5587, - "beszél": 5588, - "előtt": 5589, - "aszt": 5590, - "maj": 5591, - "kör": 5592, - "hang": 5593, - "íz": 5594, - "incs": 5595, - "év": 5596, - "ód": 5597, - "ók": 5598, - "hozz": 5599, - "okat": 5600, - "nagyon": 5601, - "ház": 5602, - "ped": 5603, - "ezte": 5604, - "etlen": 5605, - "neki": 5606, - "majd": 5607, - "szony": 5608, - "ának": 5609, - "felé": 5610, - "egyszer": 5611, - "adt": 5612, - "gyer": 5613, - "amikor": 5614, - "foly": 5615, - "szak": 5616, - "őd": 5617, - "hú": 5618, - "ász": 5619, - "amely": 5620, - "ére": 5621, - "ilyen": 5622, - "oda": 5623, - "ják": 5624, - "tár": 5625, - "ával": 5626, - "lak": 5627, - "gyan": 5628, - "ély": 5629, - "út": 5630, - "kezd": 5631, - "mell": 5632, - "mikor": 5633, - "hez": 5634, - "való": 5635, - "szeret": 5636, - "rend": 5637, - "vissza": 5638, - "fő": 5639, - "asszony": 5640, - "ről": 5641, - "pedig": 5642, - "szép": 5643, - "ták": 5644, - "öv": 5645, - "világ": 5646, - "maga": 5647, - "szik": 5648, - "éj": 5649, - "ént": 5650, - "jött": 5651, - "szí": 5652, - "gat": 5653, - "ettem": 5654, - "hány": 5655, - "ást": 5656, - "ahol": 5657, - "őket": 5658, - "hár": 5659, - "nő": 5660, - "csi": 5661, - "talál": 5662, - "elte": 5663, - "látt": 5664, - "tört": 5665, - "hagy": 5666, - "esz": 5667, - "nél": 5668, - "kut": 5669, - "lány": 5670, - "amit": 5671, - "ső": 5672, - "ellen": 5673, - "magát": 5674, - "ugyan": 5675, - "külön": 5676, - "asz": 5677, - "mindig": 5678, - "lép": 5679, - "talán": 5680, - "szor": 5681, - "illan": 5682, - "nincs": 5683, - "vagyok": 5684, - "telen": 5685, - "ismer": 5686, - "isten": 5687, - "ított": 5688, - "jobb": 5689, - "ves": 5690, - "dult": 5691, - "juk": 5692, - "szen": 5693, - "öm": 5694, - "lett": 5695, - "egyik": 5696, - "bár": 5697, - "szi": 5698, - "szív": 5699, - "azon": 5700, - "eszt": 5701, - "föld": 5702, - "kuty": 5703, - "pillan": 5704, - "fér": 5705, - "től": 5706, - "tű": 5707, - "ébe": 5708, - "tött": 5709, - "barát": 5710, - "íg": 5711, - "ahogy": 5712, - "eh": 5713, - "ep": 5714, - "jelent": 5715, - "tat": 5716, - "szeg": 5717, - "mintha": 5718, - "egyen": 5719, - "szab": 5720, - "bizony": 5721, - "jon": 5722, - "öreg": 5723, - "dolg": 5724, - "csap": 5725, - "tiszt": 5726, - "állt": 5727, - "ancs": 5728, - "idő": 5729, - "ügy": 5730, - "miért": 5731, - "ót": 5732, - "csin": 5733, - "ének": 5734, - "vér": 5735, - "jól": 5736, - "alatt": 5737, - "mely": 5738, - "semmi": 5739, - "nyug": 5740, - "vág": 5741, - "követ": 5742, - "össze": 5743, - "mad": 5744, - "acs": 5745, - "fiú": 5746, - "másik": 5747, - "jön": 5748, - "szám": 5749, - "rész": 5750, - "kér": 5751, - "ével": 5752, - "[hu]": 5753, - "%": 5754, - "0": 5755, - "6": 5756, - "7": 5757, - "8": 5758, - "9": 5759, - "A": 5760, - "B": 5761, - "C": 5762, - "D": 5763, - "E": 5764, - "F": 5765, - "G": 5766, - "H": 5767, - "I": 5768, - "J": 5769, - "K": 5770, - "L": 5771, - "M": 5772, - "N": 5773, - "O": 5774, - "P": 5775, - "Q": 5776, - "R": 5777, - "S": 5778, - "T": 5779, - "U": 5780, - "V": 5781, - "W": 5782, - "X": 5783, - "Y": 5784, - "Z": 5785, - "Ł": 5786, - "α": 5787, - "ς": 5788, - "♥": 5789, - "か": 5790, - "ズ": 5791, - "因": 5792, - "国": 5793, - "怎": 5794, - "抱": 5795, - "推": 5796, - "有": 5797, - "樣": 5798, - "為": 5799, - "群": 5800, - "麼": 5801, - "eo": 5802, - "eul": 5803, - "eun": 5804, - "eon": 5805, - "ae": 5806, - "yeon": 5807, - "yeo": 5808, - "ui": 5809, - "hae": 5810, - "geo": 5811, - "neun": 5812, - "ssda": 5813, - "seo": 5814, - "eong": 5815, - "kk": 5816, - "jeo": 5817, - "deul": 5818, - "eum": 5819, - "yeong": 5820, - "geos": 5821, - "hag": 5822, - "aneun": 5823, - "iss": 5824, - "dae": 5825, - "eob": 5826, - "eol": 5827, - "geu": 5828, - "jeong": 5829, - "sae": 5830, - "doe": 5831, - "geul": 5832, - "eulo": 5833, - "bn": 5834, - "sang": 5835, - "bnida": 5836, - "haneun": 5837, - "jeog": 5838, - "saeng": 5839, - "ineun": 5840, - "anh": 5841, - "salam": 5842, - "eom": 5843, - "nae": 5844, - "gwa": 5845, - "yeol": 5846, - "eseo": 5847, - "myeon": 5848, - "ttae": 5849, - "hw": 5850, - "eobs": 5851, - "jang": 5852, - "gw": 5853, - "ileul": 5854, - "yeog": 5855, - "jeon": 5856, - "sig": 5857, - "jag": 5858, - "hago": 5859, - "deun": 5860, - "seong": 5861, - "gag": 5862, - "ham": 5863, - "dang": 5864, - "leul": 5865, - "sil": 5866, - "dong": 5867, - "handa": 5868, - "eossda": 5869, - "aeg": 5870, - "seon": 5871, - "haessda": 5872, - "issda": 5873, - "ege": 5874, - "mul": 5875, - "jung": 5876, - "jig": 5877, - "issneun": 5878, - "geun": 5879, - "seubnida": 5880, - "won": 5881, - "daneun": 5882, - "eoh": 5883, - "deo": 5884, - "gam": 5885, - "jal": 5886, - "haeng": 5887, - "yang": 5888, - "bang": 5889, - "jae": 5890, - "saenggag": 5891, - "hage": 5892, - "sog": 5893, - "eoss": 5894, - "jasin": 5895, - "jil": 5896, - "eog": 5897, - "gyeong": 5898, - "gong": 5899, - "deon": 5900, - "haess": 5901, - "eung": 5902, - "joh": 5903, - "nal": 5904, - "myeong": 5905, - "eona": 5906, - "igo": 5907, - "gyeol": 5908, - "yag": 5909, - "gwan": 5910, - "uli": 5911, - "yong": 5912, - "lyeo": 5913, - "jog": 5914, - "eohge": 5915, - "bog": 5916, - "tong": 5917, - "manh": 5918, - "jeol": 5919, - "geol": 5920, - "aga": 5921, - "naneun": 5922, - "uneun": 5923, - "cheol": 5924, - "dol": 5925, - "bad": 5926, - "hamyeon": 5927, - "yeossda": 5928, - "ibnida": 5929, - "gye": 5930, - "eos": 5931, - "hwal": 5932, - "salamdeul": 5933, - "jiman": 5934, - "dangsin": 5935, - "jib": 5936, - "ttaemun": 5937, - "ib": 5938, - "eneun": 5939, - "eug": 5940, - "jeom": 5941, - "geuleon": 5942, - "hwa": 5943, - "assda": 5944, - "beob": 5945, - "bae": 5946, - "yeoss": 5947, - "chin": 5948, - "chaeg": 5949, - "geon": 5950, - "naega": 5951, - "iga": 5952, - "sigan": 5953, - "gil": 5954, - "hyeon": 5955, - "lyeog": 5956, - "gug": 5957, - "pyeon": 5958, - "wae": 5959, - "jul": 5960, - "seul": 5961, - "deung": 5962, - "hajiman": 5963, - "eumyeon": 5964, - "pil": 5965, - "nyeon": 5966, - "tae": 5967, - "pyo": 5968, - "jineun": 5969, - "beon": 5970, - "hada": 5971, - "seol": 5972, - "sip": 5973, - "daleun": 5974, - "salm": 5975, - "gyo": 5976, - "cheon": 5977, - "hagi": 5978, - "cheoleom": 5979, - "gal": 5980, - "ila": 5981, - "kkaji": 5982, - "anhneun": 5983, - "habnida": 5984, - "tteon": 5985, - "haeseo": 5986, - "doenda": 5987, - "ttal": 5988, - "ilo": 5989, - "seub": 5990, - "byeon": 5991, - "myeo": 5992, - "beol": 5993, - "jeung": 5994, - "chim": 5995, - "hwang": 5996, - "euneun": 5997, - "jong": 5998, - "boda": 5999, - "nol": 6000, - "neom": 6001, - "buteo": 6002, - "jigeum": 6003, - "eobsda": 6004, - "daelo": 6005, - "yul": 6006, - "pyeong": 6007, - "seoneun": 6008, - "salang": 6009, - "seut": 6010, - "heom": 6011, - "hyang": 6012, - "gwang": 6013, - "eobsneun": 6014, - "hwag": 6015, - "gess": 6016, - "jagi": 6017, - "ileon": 6018, - "wihae": 6019, - "daehan": 6020, - "gaji": 6021, - "meog": 6022, - "jyeo": 6023, - "chaj": 6024, - "byeong": 6025, - "eod": 6026, - "gyeo": 6027, - "eoji": 6028, - "gul": 6029, - "modeun": 6030, - "insaeng": 6031, - "geulae": 6032, - "sasil": 6033, - "sib": 6034, - "chal": 6035, - "ilago": 6036, - "geum": 6037, - "doeneun": 6038, - "bol": 6039, - "gajang": 6040, - "geuligo": 6041, - "hyeong": 6042, - "haengbog": 6043, - "chul": 6044, - "chae": 6045, - "mang": 6046, - "dam": 6047, - "choe": 6048, - "sijag": 6049, - "cheong": 6050, - "ilaneun": 6051, - "ulineun": 6052, - "aen": 6053, - "kke": 6054, - "munje": 6055, - "teu": 6056, - "geuneun": 6057, - "bge": 6058, - "cheo": 6059, - "baeg": 6060, - "jug": 6061, - "sangdae": 6062, - "geugeos": 6063, - "dog": 6064, - "eus": 6065, - "jab": 6066, - "hyeo": 6067, - "tteohge": 6068, - "chil": 6069, - "swi": 6070, - "jileul": 6071, - "chang": 6072, - "ganeun": 6073, - "iji": 6074, - "dago": 6075, - "yohan": 6076, - "teug": 6077, - "ppun": 6078, - "aleul": 6079, - "haengdong": 6080, - "sesang": 6081, - "edo": 6082, - "mandeul": 6083, - "amyeon": 6084, - "kkae": 6085, - "bag": 6086, - "ideul": 6087, - "pum": 6088, - "meol": 6089, - "neul": 6090, - "hamkke": 6091, - "chung": 6092, - "dab": 6093, - "yug": 6094, - "sag": 6095, - "gwangye": 6096, - "ileohge": 6097, - "balo": 6098, - "neunde": 6099, - "hamyeo": 6100, - "geuleoh": 6101, - "anila": 6102, - "bangbeob": 6103, - "dasi": 6104, - "byeol": 6105, - "gyeon": 6106, - "gamjeong": 6107, - "oneul": 6108, - "janeun": 6109, - "yeom": 6110, - "lago": 6111, - "igi": 6112, - "hwan": 6113, - "teul": 6114, - "eoseo": 6115, - "sik": 6116, - "jaga": 6117, - "geuleom": 6118, - "geuleona": 6119, - "jeongdo": 6120, - "gyeog": 6121, - "geuleohge": 6122, - "geudeul": 6123, - "eut": 6124, - "imyeon": 6125, - "jjae": 6126, - "keun": 6127, - "isang": 6128, - "malhaessda": 6129, - "euge": 6130, - "nop": 6131, - "ingan": 6132, - "bomyeon": 6133, - "taeg": 6134, - "dwi": 6135, - "saneun": 6136, - "wan": 6137, - "anhgo": 6138, - "nugu": 6139, - "sung": 6140, - "damyeon": 6141, - "adeul": 6142, - "peul": 6143, - "ttala": 6144, - "geosdo": 6145, - "aji": 6146, - "meon": 6147, - "eumyeo": 6148, - "dolog": 6149, - "neung": 6150, - "modu": 6151, - "[ko]": 6152 - }, - "merges": [ - "t h", - "i n", - "th e", - "a n", - "e r", - "o u", - "r e", - "o n", - "a t", - "e d", - "e n", - "t o", - "in g", - "an d", - "i s", - "a s", - "a l", - "o r", - "o f", - "a r", - "i t", - "e s", - "h e", - "s t", - "l e", - "o m", - "s e", - "b e", - "a d", - "o w", - "l y", - "c h", - "w h", - "th at", - "y ou", - "l i", - "v e", - "a c", - "t i", - "l d", - "m e", - "w as", - "g h", - "i d", - "l l", - "w i", - "en t", - "f or", - "a y", - "r o", - "v er", - "i c", - "h er", - "k e", - "h is", - "n o", - "u t", - "u n", - "i r", - "l o", - "w e", - "r i", - "h a", - "wi th", - "gh t", - "ou t", - "i m", - "i on", - "al l", - "a b", - "on e", - "n e", - "g e", - "ou ld", - "t er", - "m o", - "h ad", - "c e", - "s he", - "g o", - "s h", - "u r", - "a m", - "s o", - "p e", - "m y", - "d e", - "a re", - "b ut", - "om e", - "f r", - "the r", - "f e", - "s u", - "d o", - "c on", - "t e", - "a in", - "er e", - "p o", - "i f", - "the y", - "u s", - "a g", - "t r", - "n ow", - "ou n", - "th is", - "ha ve", - "no t", - "s a", - "i l", - "u p", - "th ing", - "fr om", - "a p", - "h im", - "ac k", - "at ion", - "an t", - "ou r", - "o p", - "li ke", - "u st", - "es s", - "b o", - "o k", - "u l", - "in d", - "e x", - "c om", - "s ome", - "the re", - "er s", - "c o", - "re s", - "m an", - "ar d", - "p l", - "w or", - "w ay", - "ti on", - "f o", - "c a", - "w ere", - "b y", - "at e", - "p ro", - "t ed", - "oun d", - "ow n", - "w ould", - "t s", - "wh at", - "q u", - "al ly", - "i ght", - "c k", - "g r", - "wh en", - "v en", - "c an", - "ou gh", - "in e", - "en d", - "p er", - "ou s", - "o d", - "id e", - "k now", - "t y", - "ver y", - "s i", - "a k", - "wh o", - "ab out", - "i ll", - "the m", - "es t", - "re d", - "y e", - "c ould", - "on g", - "you r", - "the ir", - "e m", - "j ust", - "o ther", - "in to", - "an y", - "wh i", - "u m", - "t w", - "as t", - "d er", - "d id", - "i e", - "be en", - "ac e", - "in k", - "it y", - "b ack", - "t ing", - "b r", - "mo re", - "a ke", - "p p", - "the n", - "s p", - "e l", - "u se", - "b l", - "sa id", - "o ver", - "ge t", - "e n", - "e r", - "c h", - "e i", - "i e", - "u n", - "i ch", - "ei n", - "s t", - "a n", - "t e", - "g e", - "a u", - "i n", - "s ch", - "d er", - "un d", - "d ie", - "d a", - "e s", - "a l", - "d en", - "a r", - "g en", - "z u", - "d e", - "h r", - "o n", - "t en", - "e l", - "o r", - "m i", - "s ie", - "da s", - "a t", - "b e", - "ein e", - "ich t", - "b er", - "l e", - "a ch", - "v er", - "s e", - "au f", - "w i", - "s o", - "t er", - "l ich", - "c k", - "u r", - "n icht", - "m m", - "b en", - "a s", - "w ar", - "r e", - "mi t", - "s ich", - "i g", - "l l", - "au s", - "i st", - "w ie", - "o ch", - "un g", - "an n", - "ü r", - "h n", - "i hr", - "s a", - "s en", - "t z", - "de m", - "ei t", - "u m", - "h at", - "wi r", - "v on", - "h a", - "s p", - "w ei", - "i er", - "r o", - "h er", - "r a", - "ein en", - "n e", - "v or", - "al s", - "an d", - "al l", - "w as", - "w o", - "r ei", - "st e", - "l ie", - "au ch", - "d u", - "d es", - "k o", - "ü ber", - "a m", - "b ei", - "h en", - "h m", - "l ei", - "a ber", - "w en", - "h l", - "g er", - "i m", - "u t", - "n ach", - "h e", - "i s", - "b r", - "f t", - "en t", - "i mm", - "j e", - "sch en", - "w er", - "s er", - "a b", - "ä n", - "m e", - "s ein", - "i t", - "o l", - "ch t", - "f ür", - "k l", - "f f", - "eine m", - "n en", - "w e", - "j a", - "u s", - "n och", - "hat te", - "t r", - "p f", - "h in", - "d i", - "ch en", - "b l", - "m an", - "r ü", - "ie l", - "s el", - "das s", - "i hn", - "mi r", - "sch l", - "ö n", - "g an", - "g t", - "ein er", - "st en", - "m ich", - "wen n", - "el l", - "g te", - "in d", - "m al", - "ge l", - "k en", - "n ur", - "mm en", - "f ü", - "er n", - "ö r", - "un ter", - "f r", - "an der", - "g r", - "i l", - "d ur", - "u ch", - "f e", - "t a", - "m en", - "m ach", - "d och", - "t i", - "dur ch", - "o s", - "g l", - "h al", - "ihr e", - "w ä", - "imm er", - "i hm", - "k ann", - "or t", - "d ann", - "l an", - "tz t", - "o der", - "hr en", - "e t", - "k ön", - "i ck", - "f a", - "in g", - "i r", - "wie der", - "da ß", - "m ein", - "f en", - "gan z", - "die se", - "st er", - "da r", - "w a", - "ge s", - "n a", - "f l", - "i gen", - "sch e", - "un gen", - "me hr", - "ß en", - "o t", - "k on", - "ge w", - "ha ben", - "ge h", - "ä t", - "s ind", - "d r", - "w el", - "un s", - "v o", - "m a", - "u te", - "sch on", - "b es", - "ge sch", - "b t", - "ch e", - "s on", - "o b", - "l a", - "p p", - "rü ck", - "s eine", - "k r", - "f re", - "ei l", - "zu m", - "u l", - "h ier", - "k t", - "i ge", - "sp r", - "k e", - "le ben", - "b st", - "z eit", - "i on", - "g ro", - "den n", - "h o", - "sch a", - "b ar", - "al le", - "ge gen", - "w ür", - "m ü", - "z e", - "wer den", - "je tzt", - "ko mmen", - "n ie", - "s ei", - "h eit", - "so ll", - "g lei", - "m eine", - "wo ll", - "n er", - "ha be", - "w ur", - "lich en", - "p er", - "as sen", - "n te", - "se hen", - "wir d", - "b is", - "g ar", - "i en", - "m us", - "u ß", - "ä r", - "st ell", - "k eit", - "z wei", - "sel bst", - "st a", - "p a", - "sa gte", - "te t", - "k am", - "s sen", - "v iel", - "u g", - "z en", - "h ei", - "m ann", - "wi ll", - "ge b", - "war en", - "ü ck", - "ä ch", - "m er", - "r u", - "w or", - "h au", - "ei gen", - "an g", - "we g", - "bl ick", - "f ra", - "all es", - "k a", - "au gen", - "f in", - "lich e", - "t o", - "un ser", - "der n", - "her r", - "n un", - "v ie", - "ch te", - "wo hl", - "f all", - "h t", - "ü n", - "et was", - "st and", - "en d", - "ä u", - "e m", - "m ö", - "te l", - "r ie", - "d ich", - "die s", - "h and", - "b in", - "ff en", - "nicht s", - "d an", - "p l", - "hn e", - "ihn en", - "es en", - "die ser", - "fr au", - "an t", - "ar t", - "di r", - "i sch", - "er st", - "glei ch", - "ko mm", - "h ör", - "ß e", - "d ig", - "se hr", - "z ei", - "sa m", - "au m", - "h ät", - "in gen", - "g ut", - "b o", - "m ut", - "ck en", - "kon nte", - "st imm", - "p ro", - "zu r", - "i tz", - "wei l", - "wür de", - "f ä", - "kön nen", - "k eine", - "f er", - "i schen", - "vo ll", - "ein es", - "se tz", - "z ie", - "de l", - "te te", - "sein er", - "ier en", - "ge st", - "zu rück", - "wur de", - "sch n", - "p r", - "lie ß", - "t ra", - "m ä", - "gen d", - "f ol", - "i k", - "schl a", - "scha ft", - "at er", - "wei ß", - "s einen", - "l assen", - "l u", - "und en", - "t eil", - "ne u", - "ier t", - "men schen", - "hm en", - "st r", - "g i", - "sa h", - "ihr en", - "el n", - "wei ter", - "ge hen", - "ig er", - "mach t", - "ta g", - "al so", - "hal ten", - "n is", - "ach t", - "ge ben", - "f or", - "o g", - "n at", - "m ar", - "de t", - "o hne", - "h aus", - "t ro", - "an ge", - "l au", - "sp iel", - "t re", - "sch r", - "in n", - "s u", - "l os", - "mach en", - "hät te", - "be g", - "wir k", - "al t", - "g lich", - "te s", - "r icht", - "fre und", - "m o", - "ihr er", - "f el", - "b el", - "so l", - "ein mal", - "e ben", - "h ol", - "h än", - "q u", - "ter n", - "h ö", - "sch w", - "re cht", - "wa hr", - "s einem", - "ste hen", - "hl en", - "in s", - "g ing", - "woll te", - "wi ssen", - "ung s", - "al d", - "as s", - "ja hr", - "m or", - "wel t", - "un der", - "zu sa", - "at ion", - "ko pf", - "lan g", - "hin ter", - "at z", - "st ra", - "an gen", - "an k", - "a de", - "gl au", - "f ach", - "hat ten", - "l o", - "f ort", - "ei cht", - "i ff", - "l er", - "m ei", - "diese m", - "k ein", - "f rei", - "fü hr", - "vo m", - "e s", - "e n", - "a i", - "o u", - "o n", - "l e", - "d e", - "r e", - "q u", - "a n", - "e r", - "en t", - "e t", - "l a", - "n e", - "i l", - "a r", - "i s", - "ai t", - "t e", - "a u", - "i n", - "qu e", - "i t", - "u r", - "s e", - "l es", - "c h", - "c e", - "m e", - "o r", - "ou r", - "a s", - "p r", - "a v", - "o m", - "ai s", - "u n", - "an t", - "ou s", - "t r", - "t i", - "l u", - "o i", - "e u", - "l le", - "s i", - "p ar", - "d es", - "an s", - "m ent", - "é t", - "es t", - "j e", - "u ne", - "a l", - "p as", - "t re", - "qu i", - "d u", - "r i", - "c on", - "s on", - "c om", - "e lle", - "d é", - "p our", - "d ans", - "l i", - "s a", - "r é", - "t ou", - "v ous", - "d i", - "v i", - "a g", - "a m", - "a t", - "ou v", - "a p", - "ti on", - "m on", - "s ur", - "c i", - "o s", - "p lu", - "s u", - "en d", - "a b", - "è re", - "ai n", - "m ais", - "o is", - "r es", - "plu s", - "é e", - "ai ent", - "m p", - "ch e", - "lu i", - "av e", - "ét ait", - "m a", - "s es", - "tou t", - "i r", - "v o", - "a c", - "s er", - "an d", - "f f", - "oi r", - "g r", - "av ait", - "é s", - "m es", - "n ous", - "eu x", - "b i", - "t er", - "c o", - "on s", - "p u", - "c es", - "g e", - "t u", - "le ur", - "pr o", - "d on", - "e ur", - "et te", - "ai re", - "ave c", - "d it", - "t é", - "i e", - "u s", - "il le", - "p er", - "com me", - "c r", - "or t", - "m i", - "e x", - "u x", - "v er", - "m o", - "è s", - "v e", - "au x", - "r a", - "j our", - "il s", - "bi en", - "c ou", - "p e", - "que l", - "p eu", - "c ette", - "t es", - "p o", - "in s", - "c u", - "m ê", - "s o", - "f ait", - "g u", - "m ar", - "ê tre", - "l o", - "it é", - "f r", - "a tion", - "en s", - "b r", - "n i", - "l é", - "d is", - "b le", - "m an", - "n é", - "pu is", - "mê me", - "qu es", - "f i", - "e l", - "ag e", - "g ar", - "m oi", - "en ce", - "on t", - "m ain", - "or s", - "au t", - "an ce", - "v en", - "m é", - "s ans", - "e m", - "s é", - "l on", - "h om", - "r o", - "u t", - "c ar", - "ab le", - "i m", - "de r", - "ch er", - "n o", - "vi e", - "au s", - "b e", - "de ux", - "en f", - "o ù", - "t en", - "p h", - "u re", - "te mp", - "p os", - "r ent", - "p é", - "f aire", - "p i", - "tr es", - "ç a", - "an g", - "end re", - "f or", - "p a", - "b on", - "s ou", - "in t", - "pr é", - "s ent", - "t ant", - "n er", - "c er", - "l à", - "l ais", - "pr ès", - "b re", - "c our", - "p et", - "i on", - "i ne", - "com p", - "l ait", - "tr ouv", - "t a", - "ent re", - "son t", - "de v", - "n u", - "temp s", - "d ou", - "r ait", - "b ou", - "qu and", - "jour s", - "l an", - "er s", - "av oir", - "ét é", - "a le", - "p re", - "f ois", - "or te", - "v é", - "m er", - "n on", - "t ous", - "j us", - "cou p", - "t s", - "hom me", - "ê te", - "a d", - "aus si", - "ur s", - "se u", - "or d", - "o b", - "m in", - "g é", - "co re", - "v a", - "v re", - "en core", - "se m", - "i te", - "au tre", - "pr is", - "peu t", - "u e", - "an te", - "m al", - "g n", - "ré p", - "h u", - "si on", - "vo tre", - "di re", - "e z", - "f em", - "leur s", - "m et", - "f in", - "c ri", - "m is", - "t our", - "r ai", - "j am", - "re gar", - "ri en", - "ver s", - "su is", - "p ouv", - "o p", - "v is", - "gr and", - "ant s", - "c or", - "re r", - "ar d", - "c é", - "t ent", - "pr es", - "v ou", - "f a", - "al ors", - "si eur", - "ai ne", - "le r", - "qu oi", - "f on", - "end ant", - "ar ri", - "eu re", - "a près", - "don c", - "it u", - "l è", - "s ait", - "t oi", - "ch a", - "ai l", - "as se", - "i mp", - "vo y", - "con n", - "p la", - "pet it", - "av ant", - "n om", - "t in", - "don t", - "d a", - "s ous", - "e mp", - "per son", - "el les", - "be au", - "par ti", - "ch o", - "pr it", - "tou jours", - "m en", - "r ais", - "jam ais", - "tr av", - "tion s", - "tr ès", - "v oi", - "r en", - "y eux", - "f er", - "v oir", - "pre mi", - "c a", - "g ne", - "h eure", - "r ou", - "e ff", - "no tre", - "ment s", - "t on", - "f ais", - "ce la", - "i er", - "rép on", - "con s", - "ai r", - "ô t", - "p endant", - "i ci", - "tou te", - "j et", - "p ort", - "ét aient", - "p en", - "h é", - "au tres", - "p ère", - "o c", - "quel ques", - "i que", - "l is", - "fem me", - "j ou", - "te ur", - "mon de", - "u se", - "n es", - "d re", - "a ff", - "r ap", - "par t", - "le ment", - "c la", - "f ut", - "quel que", - "pr endre", - "r ê", - "ai lle", - "s ais", - "ch es", - "le t", - "ch ar", - "è res", - "ent s", - "b er", - "g er", - "mo ins", - "e au", - "a î", - "j eu", - "h eur", - "é es", - "tr i", - "po int", - "m om", - "v ent", - "n ouv", - "gr an", - "tr ois", - "s ant", - "tout es", - "con tre", - "è rent", - "che z", - "ave z", - "û t", - "a lle", - "at t", - "p au", - "p orte", - "ouv er", - "b ar", - "l it", - "f ort", - "o t", - "as s", - "pr és", - "cho se", - "v it", - "mon sieur", - "h ab", - "t ête", - "j u", - "te ment", - "c tion", - "v rai", - "la r", - "c et", - "regar d", - "l ant", - "de m", - "s om", - "mom ent", - "il les", - "p le", - "p s", - "b es", - "m ère", - "c l", - "s our", - "y s", - "tr op", - "en ne", - "jus qu", - "av aient", - "av ais", - "jeu ne", - "de puis", - "person ne", - "f it", - "cer t", - "j o", - "g es", - "ou i", - "r est", - "sem b", - "c ap", - "m at", - "m u", - "lon g", - "fr an", - "f aut", - "it i", - "b li", - "che v", - "pr i", - "ent e", - "ain si", - "ch am", - "l ors", - "c as", - "d o", - "il i", - "b é", - "n os", - "an ge", - "su i", - "r it", - "cr o", - "gu e", - "d e", - "e n", - "e s", - "o s", - "l a", - "e r", - "q u", - "a r", - "a n", - "o n", - "qu e", - "a s", - "o r", - "e l", - "d o", - "a l", - "c i", - "u n", - "r e", - "a b", - "i n", - "t e", - "t o", - "s e", - "d i", - "t r", - "d a", - "c on", - "t a", - "s u", - "m i", - "c o", - "t i", - "l e", - "l os", - "n o", - "l o", - "í a", - "c u", - "c a", - "s i", - "v i", - "m e", - "p or", - "m o", - "p ar", - "r a", - "r i", - "la s", - "c h", - "r o", - "m a", - "p er", - "ó n", - "m en", - "de s", - "un a", - "m p", - "s o", - "ab a", - "p u", - "d os", - "t u", - "g u", - "er a", - "de l", - "h a", - "m u", - "l i", - "en t", - "m b", - "h ab", - "es t", - "g o", - "p a", - "r es", - "par a", - "p o", - "á s", - "m os", - "tr a", - "t en", - "an do", - "p i", - "qu i", - "b i", - "m an", - "co mo", - "v e", - "m ás", - "j o", - "ci ón", - "i s", - "t an", - "v o", - "da d", - "c e", - "a do", - "v er", - "f u", - "ci a", - "c er", - "p e", - "c as", - "c ar", - "men te", - "n i", - "su s", - "t ar", - "n a", - "f i", - "t er", - "z a", - "p ro", - "tr o", - "s a", - "l u", - "b a", - "per o", - "s er", - "c es", - "d as", - "d u", - "s in", - "e mp", - "m ar", - "l la", - "e x", - "á n", - "c or", - "i a", - "v a", - "r an", - "ch o", - "g a", - "y o", - "t os", - "c os", - "mi s", - "l es", - "t es", - "v en", - "h o", - "y a", - "en te", - "on es", - "hab ía", - "n u", - "u s", - "p as", - "h i", - "n os", - "es ta", - "la n", - "m as", - "t or", - "l le", - "h e", - "s on", - "b re", - "p re", - "ab an", - "d or", - "í an", - "i r", - "t as", - "é n", - "r u", - "en do", - "a que", - "er o", - "i o", - "qu é", - "m in", - "c ab", - "j a", - "de r", - "t al", - "é s", - "se ñ", - "or a", - "to do", - "la r", - "d on", - "g ar", - "s al", - "p r", - "cu ando", - "j e", - "h u", - "g un", - "b u", - "g i", - "d ar", - "n e", - "r as", - "de n", - "es to", - "par e", - "p en", - "é l", - "tr as", - "c an", - "b o", - "j os", - "mi en", - "pu e", - "c re", - "co mp", - "p on", - "d ía", - "tr os", - "s ab", - "so bre", - "es e", - "mb re", - "er on", - "a ñ", - "m or", - "f or", - "i do", - "por que", - "el la", - "p ri", - "g ran", - "f a", - "c en", - "di s", - "c ri", - "mu y", - "ch a", - "c al", - "es te", - "h as", - "c ó", - "g ra", - "r os", - "p os", - "o b", - "al l", - "aque l", - "j u", - "p res", - "m er", - "di jo", - "c ía", - "ent re", - "z o", - "ci ones", - "bi en", - "mb i", - "el o", - "t ó", - "in a", - "to dos", - "g en", - "ti en", - "est aba", - "de ci", - "ci o", - "h er", - "ñ o", - "l or", - "nu es", - "me di", - "l en", - "vi da", - "f e", - "al i", - "m on", - "c la", - "d re", - "pu es", - "al es", - "vo l", - "m í", - "r ar", - "b le", - "ci on", - "has ta", - "señ or", - "con o", - "a h", - "di os", - "s en", - "es a", - "ú n", - "v ar", - "s an", - "gu i", - "a c", - "o tros", - "ta do", - "bu en", - "ñ a", - "ti emp", - "ha cer", - "j er", - "f er", - "v u", - "f in", - "an a", - "as í", - "an tes", - "t in", - "ve z", - "mien to", - "j ar", - "la b", - "ch e", - "cas a", - "d r", - "es o", - "e go", - "di ó", - "an te", - "est á", - "m al", - "en cia", - "el i", - "í as", - "tiemp o", - "z ar", - "v an", - "m un", - "er ta", - "ta mbi", - "s í", - "b ar", - "a un", - "al e", - "mis mo", - "ent es", - "vi s", - "man o", - "el e", - "na da", - "se gu", - "me j", - "er ra", - "ab le", - "b e", - "ti r", - "un o", - "don de", - "to da", - "des de", - "r en", - "tambi én", - "cu er", - "per son", - "ho mbre", - "o tro", - "li b", - "tr ar", - "cu al", - "ha y", - "a u", - "ca da", - "t aba", - "i mp", - "men to", - "ten ía", - "qu er", - "er an", - "si emp", - "siemp re", - "er to", - "qu í", - "g os", - "pu és", - "el los", - "des pués", - "nu e", - "g an", - "l lo", - "in ter", - "có mo", - "tr i", - "ah ora", - "us te", - "tr aba", - "la do", - "in o", - "po co", - "er te", - "mu jer", - "i m", - "qui er", - "al gun", - "fu e", - "o jos", - "ent on", - "v os", - "es per", - "mu ch", - "o tra", - "a z", - "a d", - "in g", - "e za", - "a quí", - "ci as", - "gu a", - "mu cho", - "deci r", - "es ti", - "i dad", - "al go", - "e z", - "o cu", - "enton ces", - "di do", - "ent os", - "g ri", - "da do", - "i os", - "so l", - "dos e", - "uste d", - "qui en", - "a mi", - "un to", - "f r", - "mi r", - "mej or", - "b as", - "so lo", - "pre gun", - "tu r", - "al g", - "p la", - "to das", - "par te", - "e mb", - "c to", - "mun do", - "tien e", - "tan te", - "pa lab", - "tr an", - "aque lla", - "ci os", - "aun que", - "a y", - "cu en", - "ten er", - "f un", - "res pon", - "all í", - "x i", - "h an", - "pen s", - "con tra", - "tu ra", - "v al", - "di o", - "tr es", - "t re", - "tan to", - "ca min", - "m ó", - "es p", - "a da", - "í o", - "in s", - "ha cia", - "de j", - "est ar", - "i ón", - "g as", - "b er", - "v as", - "no che", - "é r", - "añ os", - "pa dre", - "gu s", - "á r", - "sin o", - "man os", - "ci do", - "es tu", - "a de", - "hu bi", - "vi r", - "b ri", - "ra z", - "ch i", - "pue de", - "men os", - "hab i", - "ho mb", - "ne ces", - "ma y", - "er os", - "r ía", - "he cho", - "es cu", - "l ti", - "án do", - "b us", - "cos as", - "t ú", - "es pa", - "re ci", - "c tor", - "pri m", - "di a", - "de se", - "mien tras", - "h or", - "fu er", - "i da", - "pos i", - "lan te", - "t on", - "an o", - "est as", - "p li", - "ch ar", - "lu ego", - "si ón", - "ci n", - "ti erra", - "m es", - "gu ar", - "ca do", - "en con", - "pr en", - "may or", - "f al", - "e r", - "o n", - "a n", - "t o", - "d i", - "r e", - "l a", - "i n", - "e n", - "a l", - "t a", - "c h", - "e l", - "r i", - "c o", - "t i", - "t e", - "s i", - "r a", - "u n", - "l e", - "l i", - "ch e", - "r o", - "c i", - "c a", - "s e", - "q u", - "m a", - "p o", - "s o", - "i l", - "d o", - "e s", - "v a", - "p er", - "l o", - "c on", - "d el", - "p a", - "m o", - "s a", - "p i", - "d a", - "m i", - "g i", - "s u", - "d e", - "v i", - "z i", - "m e", - "g li", - "n o", - "m en", - "v o", - "t u", - "n on", - "v e", - "t to", - "s t", - "on e", - "an o", - "ch i", - "er a", - "er e", - "f a", - "c e", - "z a", - "un a", - "b i", - "p re", - "s ta", - "o r", - "a r", - "f i", - "on o", - "t ra", - "n a", - "n el", - "n e", - "p ro", - "t ro", - "al e", - "v er", - "n i", - "c u", - "t ti", - "men te", - "del la", - "t er", - "zi one", - "g u", - "p e", - "t ta", - "an do", - "t à", - "al i", - "u o", - "qu el", - "co m", - "s en", - "co me", - "b a", - "al la", - "p ri", - "d u", - "qu es", - "l u", - "on i", - "g gi", - "pa r", - "s si", - "v en", - "in a", - "g a", - "pi ù", - "ci a", - "i m", - "co r", - "m an", - "in o", - "in i", - "t en", - "r an", - "b b", - "g o", - "s to", - "t re", - "a ve", - "a v", - "s ono", - "er i", - "a c", - "s se", - "er o", - "h a", - "s c", - "su l", - "f or", - "v ano", - "po r", - "s ti", - "su o", - "c chi", - "t an", - "z za", - "an che", - "p u", - "i o", - "t te", - "vo l", - "es s", - "s ci", - "co l", - "r u", - "p en", - "f u", - "al l", - "s so", - "s te", - "se m", - "s sa", - "d en", - "a d", - "t ri", - "de i", - "in e", - "ave va", - "men to", - "z z", - "a mo", - "g no", - "f o", - "un o", - "su a", - "g en", - "ri a", - "g e", - "st ra", - "s ì", - "c er", - "ch é", - "b u", - "a p", - "c en", - "d al", - "on a", - "s pe", - "g ni", - "b o", - "t t", - "del le", - "ques to", - "nel la", - "f f", - "d ere", - "an no", - "del l", - "un i", - "bb e", - "an ti", - "g ra", - "s p", - "en e", - "gi o", - "u to", - "qu al", - "gli a", - "qu ando", - "tu tto", - "c an", - "gli o", - "zi oni", - "ca m", - "h o", - "es so", - "s s", - "mo l", - "a t", - "lo ro", - "per ché", - "co sa", - "du e", - "po i", - "ca r", - "s co", - "ci o", - "to r", - "c co", - "c re", - "a m", - "g na", - "te m", - "pri ma", - "lu i", - "co sì", - "qu e", - "gu ar", - "ess ere", - "an i", - "con o", - "b ra", - "al le", - "m on", - "ri o", - "an co", - "cu i", - "s pi", - "vi a", - "g ran", - "gi or", - "a i", - "bi le", - "u l", - "ggi o", - "f e", - "an te", - "ma i", - "ta re", - "in ter", - "in di", - "re bbe", - "sen za", - "so lo", - "zi o", - "e d", - "en te", - "tu tti", - "sta to", - "zi a", - "d alla", - "tu ra", - "mi a", - "vi ta", - "quel la", - "qu a", - "ma r", - "do ve", - "g h", - "al lo", - "sem pre", - "zz o", - "si a", - "mo r", - "do po", - "por ta", - "d re", - "c cia", - "er ano", - "an ni", - "di o", - "chi a", - "en za", - "pro pri", - "qu i", - "m u", - "m b", - "an da", - "c ca", - "o cchi", - "ques ta", - "f fi", - "le i", - "par te", - "d on", - "r on", - "mi o", - "tan to", - "ri s", - "o gni", - "di s", - "r in", - "fa r", - "men ti", - "t el", - "anco ra", - "f ra", - "fa tto", - "man i", - "sen ti", - "p ra", - "tem po", - "es si", - "b bi", - "f in", - "a re", - "la re", - "per s", - "f on", - "b el", - "so r", - "d er", - "pre n", - "an za", - "di re", - "pi e", - "o ra", - "ver so", - "se gu", - "al tro", - "ta to", - "ca to", - "a to", - "vol ta", - "c c", - "fa re", - "pa re", - "ci ò", - "li b", - "bi li", - "n uo", - "s er", - "quel lo", - "co lo", - "p po", - "ca sa", - "tro va", - "o re", - "f er", - "r ono", - "d es", - "mol to", - "al mente", - "s ca", - "vo le", - "t ali", - "sul la", - "s ce", - "men o", - "an to", - "p un", - "s tu", - "ca pi", - "so l", - "gi u", - "m ini", - "m ano", - "z e", - "pi a", - "par ti", - "s al", - "la vo", - "ver o", - "r si", - "al tri", - "es ti", - "s cia", - "suo i", - "gli e", - "so tto", - "b ene", - "sc ri", - "t ale", - "de gli", - "n u", - "al c", - "uo mo", - "p el", - "f re", - "po te", - "es sa", - "s cu", - "si gno", - "el e", - "st ro", - "u ti", - "di a", - "si one", - "g re", - "f ini", - "ar ri", - "l un", - "c ri", - "e si", - "pa ssa", - "r à", - "men tre", - "an d", - "h anno", - "el o", - "u sci", - "gi a", - "gi à", - "di e", - "m ina", - "b e", - "ti ca", - "gior no", - "t in", - "es se", - "mo do", - "c al", - "s pa", - "propri o", - "l en", - "o ri", - "con tro", - "st ru", - "di ven", - "di sse", - "ra to", - "no i", - "v ere", - "pu ò", - "di ce", - "s an", - "es a", - "c ci", - "se con", - "re n", - "c cio", - "qual che", - "tu tta", - "g g", - "mon do", - "for ma", - "p li", - "m ma", - "pen sa", - "de va", - "tu r", - "fo sse", - "so pra", - "ta mente", - "n ess", - "qu anto", - "ra ga", - "un que", - "ca re", - "st re", - "gran de", - "pi cco", - "guar da", - "b en", - "nel l", - "a ff", - "po ssi", - "pre sen", - "r ò", - "pa ro", - "tu a", - "v in", - "an e", - "a s", - "ste sso", - "da v", - "ne i", - "nel le", - "gh i", - "pi o", - "ta r", - "an a", - "la to", - "si d", - "f ine", - "f uo", - "m er", - "z o", - "qua si", - "ul ti", - "i to", - "su e", - "si e", - "f il", - "allo ra", - "m in", - "ven i", - "t ano", - "el lo", - "d e", - "r a", - "e s", - "d o", - "e n", - "q u", - "c o", - "a s", - "o s", - "e r", - "a r", - "s e", - "qu e", - "a n", - "i n", - "i s", - "t o", - "ã o", - "t e", - "d a", - "m a", - "e l", - "t a", - "o r", - "i a", - "r e", - "e m", - "a l", - "co m", - "p a", - "o u", - "c a", - "u m", - "r o", - "v a", - "t i", - "s o", - "m en", - "n ão", - "h a", - "co n", - "m e", - "r i", - "pa ra", - "p o", - "d i", - "s a", - "v o", - "u ma", - "c i", - "n a", - "p or", - "n o", - "g u", - "s u", - "h o", - "an do", - "t ra", - "e i", - "v i", - "e u", - "i m", - "do s", - "el e", - "r es", - "m o", - "en t", - "f i", - "l a", - "e ra", - "l e", - "de s", - "el a", - "men te", - "l h", - "p er", - "l i", - "ç ão", - "m as", - "t er", - "m u", - "es t", - "v e", - "g o", - "l o", - "u s", - "ma is", - "v er", - "c ê", - "in ha", - "vo cê", - "f a", - "t u", - "c u", - "p ar", - "com o", - "p ro", - "s i", - "m os", - "e c", - "p re", - "d as", - "ç a", - "es ta", - "s er", - "u n", - "da de", - "d is", - "f o", - "e x", - "c h", - "i r", - "ra n", - "t ar", - "en te", - "g a", - "t r", - "p e", - "t os", - "b o", - "c ia", - "p en", - "c ar", - "s en", - "su a", - "se m", - "c as", - "f or", - "to u", - "n os", - "te m", - "r ia", - "m es", - "se u", - "co r", - "o n", - "a o", - "p os", - "ra m", - "v el", - "é m", - "t en", - "po de", - "t es", - "esta va", - "c e", - "b a", - "qu ando", - "m i", - "qu er", - "men to", - "se gu", - "t as", - "is so", - "mu i", - "g ar", - "t ro", - "d u", - "fa z", - "õ es", - "p es", - "an to", - "l u", - "p i", - "i x", - "ve z", - "s im", - "j a", - "p r", - "m in", - "b e", - "ra s", - "m an", - "p res", - "est á", - "c er", - "b re", - "p as", - "d ia", - "m b", - "dis se", - "n i", - "r os", - "es se", - "v ia", - "o lh", - "is a", - "an te", - "ê n", - "z a", - "qu i", - "b i", - "t inha", - "me u", - "s ão", - "m inha", - "a c", - "ri o", - "m ar", - "a t", - "p el", - "mui to", - "ta l", - "to r", - "fo i", - "h or", - "j o", - "b em", - "g i", - "f al", - "vo l", - "po n", - "di z", - "l ar", - "gu n", - "m or", - "r u", - "par ec", - "ç o", - "do r", - "pes so", - "n e", - "f er", - "b er", - "p u", - "po is", - "in a", - "es p", - "d ar", - "en do", - "de n", - "so bre", - "co s", - "p ri", - "al i", - "mes mo", - "ç ões", - "g ra", - "se us", - "me i", - "b ra", - "vi da", - "an tes", - "b ri", - "at é", - "ên cia", - "lh e", - "ti v", - "m ã", - "al g", - "qu anto", - "s ó", - "g os", - "de r", - "t ão", - "tu do", - "ent ão", - "r ou", - "es s", - "in da", - "b al", - "in do", - "ci o", - "n do", - "j á", - "va m", - "re i", - "l es", - "ei to", - "v is", - "tem po", - "de pois", - "c ha", - "m el", - "ch e", - "l ha", - "a inda", - "faz er", - "con tra", - "p ou", - "per gun", - "de ix", - "ta mb", - "ra r", - "al a", - "v en", - "t in", - "pel o", - "tamb ém", - "fi ca", - "pre c", - "el es", - "tra n", - "ha via", - "l á", - "to dos", - "j u", - "qu al", - "c an", - "ta do", - "cas a", - "es sa", - "n as", - "g em", - "m em", - "se i", - "na da", - "sen ti", - "c ri", - "ó s", - "de u", - "ei ro", - ". .", - "f un", - "as sim", - "s ou", - "ent re", - "com e", - "i or", - "h ar", - "f e", - "por que", - "s or", - "f in", - "ta mente", - "a qui", - "cu l", - "t ó", - "for ma", - "s ar", - "ou tra", - "olh os", - "i ma", - "m im", - "a go", - "in s", - "co u", - "g ran", - "v al", - "pesso as", - "era m", - "ei ra", - "a que", - "com p", - "de i", - "p ela", - "co isa", - "m ão", - "con h", - "ca da", - "ago ra", - "ia m", - "h á", - "con s", - "su as", - "gu ém", - "o b", - "l an", - "es ti", - "á s", - "la do", - "in ter", - "ca be", - "por ta", - "n em", - "í vel", - "r is", - "j e", - "n un", - "sem pre", - "con segu", - "h as", - "tra bal", - "f u", - "le v", - "l em", - "l as", - "va i", - "tr os", - "t ante", - "te i", - "pr ó", - "que m", - "tu ra", - "on de", - "cabe ça", - "nun ca", - "men tos", - "h um", - "de le", - "ver dade", - "t á", - "h os", - "el i", - "ent es", - "m er", - "alg um", - "diz er", - "s in", - "pen as", - "n ós", - "en quanto", - "ou tro", - "l ho", - "es te", - "mel hor", - "est ar", - "g an", - "b ar", - "pri mei", - "a u", - "i u", - "pen sa", - "a penas", - "p ra", - "es tou", - "con te", - "res pon", - "ho mem", - "do is", - "a do", - "c al", - "a b", - "l os", - "ç as", - "pou co", - "sen hor", - "t ando", - "esp era", - "pa i", - "ri os", - "no i", - "i da", - "ba ix", - "as e", - "is as", - "f r", - "ho ra", - "mu ndo", - "pas sa", - "fi car", - "to do", - "se ja", - "al mente", - "â n", - "c lar", - "a d", - "in c", - "f os", - "lo n", - "g ri", - "ou vi", - "v em", - "g e", - "ta va", - "á rio", - "mo n", - "s os", - "in ho", - "ma l", - "t an", - "t re", - "gran de", - "ran do", - "b u", - "v ou", - "ê s", - "co isas", - "a conte", - "lh er", - "g en", - "ci on", - "an os", - "i do", - "tal vez", - "est ão", - "li v", - "sa b", - "su r", - "ou tros", - "c re", - "qual quer", - "g ou", - "t ri", - "l í", - "tiv esse", - "ra do", - "prec isa", - "mã e", - "su s", - "t anto", - "de la", - "men os", - "s al", - "en tra", - "p é", - "ma ior", - "noi te", - "ti va", - "p ala", - "so n", - "ra ção", - "de us", - "s as", - "un i", - "l or", - "u l", - "in te", - "f ei", - "an o", - "par ti", - "pala v", - "tr ás", - "par te", - "b el", - "ci dade", - "lu gar", - "v os", - "vez es", - "do u", - "en contra", - "tr u", - "e ci", - "a r", - "e r", - "a n", - "e n", - "i n", - "i r", - "o r", - "d e", - "a k", - "ı n", - "a l", - "d i", - "d a", - "b u", - "b ir", - "y or", - "i l", - "e k", - "y a", - "m a", - "l a", - "e l", - "u n", - "k a", - "l ar", - "i m", - "d ı", - "e t", - "o n", - "d u", - "o l", - "e y", - "t ı", - "m i", - "h a", - "b a", - "l er", - "ü n", - "m ı", - "i z", - "l e", - "ı r", - "m e", - "i s", - "n e", - "o k", - "t a", - "s a", - "u m", - "r a", - "g ö", - "i k", - "s ı", - "d en", - "e s", - "b il", - "t i", - "l ı", - "ü z", - "i ç", - "ü r", - "g i", - "u r", - "t e", - "b en", - "d an", - "i y", - "ı m", - "u z", - "v e", - "c ak", - "a y", - "c e", - "i ş", - "ın ı", - "i yor", - "ba ş", - "d ü", - "a t", - "a m", - "g el", - "de ğ", - "k ar", - "i ̇", - "m u", - "e v", - "ö y", - "bu n", - "v ar", - "ya p", - "s en", - "an a", - "s un", - "in i", - "gö r", - "y ı", - "k i", - "l i", - "ar a", - "al ı", - "on u", - "ç ı", - "ş ey", - "s ın", - "k ı", - "ka d", - "s e", - "t an", - "a ğ", - "değ il", - "s in", - "ü k", - "a z", - "ç ok", - "s on", - "ş ı", - "b i", - "ü l", - "t u", - "v er", - "iç in", - "g e", - "k en", - "ey e", - "ol du", - "mı ş", - "y e", - "k al", - "m ek", - "l an", - "öy le", - "yor du", - "er i", - "y üz", - "mi ş", - "b e", - "m ak", - "o la", - "in e", - "y an", - "h er", - "c ek", - "yor um", - "b ak", - "ü m", - "ö n", - "lar ı", - "o ğ", - "d er", - "kad ar", - "h al", - "ar ı", - "s t", - "s an", - "ın da", - "du r", - "g ün", - "v a", - "y ok", - "y er", - "dı m", - "k o", - "da ha", - "l u", - "ın a", - "di m", - "e m", - "bil ir", - "ik i", - "s iz", - "s i", - "n a", - "di ğ", - "s u", - "b ü", - "ha y", - "s or", - "dü ş", - "ü ç", - "un u", - "ö r", - "d ir", - "m ü", - "c a", - "am an", - "f ak", - "a da", - "e de", - "son ra", - "h iç", - "ak i", - "ğ ı", - "bu l", - "r u", - "ma z", - "an la", - "bu ra", - "ge ç", - "ma ya", - "l en", - "k onu", - "c i", - "c u", - "d in", - "t ek", - "z aman", - "el er", - "ö z", - "dı r", - "gi bi", - "o t", - "ş a", - "g er", - "ler i", - "k im", - "k u", - "fak at", - "y ar", - "gö z", - "c ı", - "yor sun", - "b ek", - "in de", - "r o", - "p ek", - "bun u", - "l ik", - "m an", - "il er", - "e di", - "ö l", - "s ür", - "b in", - "s ır", - "çı k", - "sı l", - "al ar", - "k es", - "y ak", - "ç ek", - "yı l", - "e cek", - "ı z", - "gi t", - "ka p", - "a ma", - "ı l", - "lar ın", - "b iz", - "tı r", - "o y", - "an cak", - "d oğ", - "ç a", - "b ana", - "ş im", - "baş la", - "l ü", - "ma dı", - "ben i", - "t ir", - "y ük", - "lı k", - "be ş", - "b el", - "b er", - "m er", - "na sıl", - "tı k", - "k e", - "t ür", - "a v", - ". .", - "d aki", - "p ar", - "t er", - "ce ğ", - "t en", - "z ı", - "iy i", - "d ok", - "ben im", - "c ağ", - "n er", - "y en", - "ş u", - "me z", - "düş ün", - "ken di", - "şim di", - "y ol", - "y u", - "de v", - "is te", - "s ek", - "ma m", - "s öyle", - "di k", - "t o", - "k ur", - "oldu ğ", - "s ını", - "t ar", - "bil iyor", - "k an", - "y al", - "m eye", - "mu ş", - "f a", - "ka ç", - "bil e", - "iy e", - "t ü", - "e f", - "tı m", - "ev et", - "ç o", - "y et", - "g en", - "bura da", - "t im", - "bir az", - "es i", - "k or", - "doğ ru", - "in in", - "kı z", - "di ye", - "d ör", - "et ti", - "on un", - "is ti", - "ğ i", - "h e", - "s ana", - "ü ş", - "ar ka", - "hay ır", - "kar şı", - "h ar", - "il e", - "h ak", - "ı yor", - "ne den", - "s ev", - "sı z", - "ço cu", - "me m", - "ç alı", - "ol ur", - "b ır", - "g ir", - "is e", - "i h", - "c an", - "k ır", - "d ön", - "b öyle", - "sen i", - "! \"", - "al t", - "dör t", - "s öy", - "o ş", - "mu sun", - "la ş", - "h an", - "i p", - "ka y", - "h em", - "bü yük", - "a ç", - "bır ak", - "mi sin", - "s öz", - "u l", - "değ iş", - "ün ü", - "g ül", - "k ö", - "kar ı", - "ta mam", - "ol u", - "r ar", - "yen i", - "la m", - "mış tı", - "ya ş", - "al a", - "in iz", - "kad ın", - "bun un", - "m ey", - "al tı", - "y i", - "s o", - "in den", - "sen in", - "ya t", - "to p", - "s er", - "is i", - "d ün", - "s es", - "hiç bir", - "y on", - "d ın", - "t ün", - "baş ka", - "a s", - "he p", - "i t", - "ir mi", - "dev am", - "ola cak", - "ar tık", - "r e", - "dur um", - "im iz", - "üz el", - "ler ini", - "sa ğ", - "p ro", - "ger ek", - "y irmi", - "ş ek", - "ba ğ", - "me di", - "lar a", - "a h", - "t ur", - "y ür", - "ma sı", - "ka tı", - "de di", - "g ü", - "sor un", - "el i", - "ün e", - "mı z", - "yap ı", - "m il", - "ğ ını", - "t ara", - "m en", - "ha t", - "var dı", - "m et", - "konu ş", - "ar ak", - "lar ak", - "çocu k", - "bü tün", - "l ey", - "d ür", - "g üzel", - "ay ı", - "yap a", - "n ı", - "ay r", - "ö ne", - "yordu m", - "b an", - "i̇ ş", - "du m", - "un a", - "on a", - "yor lar", - "lar ını", - "çı kar", - "z an", - "se ç", - "l iyor", - "t ak", - "şı k", - "tek rar", - "a ş", - "e ş", - "miş ti", - "f ar", - "k in", - "im i", - "i f", - "e ğ", - "gi di", - "le ş", - "başla dı", - "gi de", - "ot ur", - "d de", - "ın dan", - "üz er", - "ın ın", - "n ız", - "u y", - "ye di", - "ka t", - "o larak", - "la dı", - "yal nız", - "ba h", - "iy et", - "m al", - "s ak", - "a çık", - "sın da", - ".. .", - "in san", - "ay nı", - "e der", - "is tan", - "uz un", - "sa h", - "d o", - "g eri", - "er ek", - "ol an", - "ger çek", - "f en", - "al an", - "dı ş", - "alı k", - "far k", - "ü st", - "sa de", - "r i", - "k iş", - "l dı", - "z or", - "et ir", - "her kes", - "s al", - "ö mer", - "s el", - "un da", - "ha f", - "bun a", - "y dı", - "pek i", - "ada m", - "ha z", - "sın a", - "kap ı", - "gör üş", - "sade ce", - "al dı", - "gel di", - "i e", - "n ie", - "n a", - "r z", - "s z", - "c z", - "p o", - "s t", - "c h", - "i ę", - "d z", - "n i", - "a ł", - "r a", - "j e", - "r o", - "d o", - "s ię", - "z a", - "g o", - "e m", - "w i", - "c i", - "rz e", - "k o", - "l e", - "l i", - "w a", - "t o", - "k a", - "m i", - "ż e", - "t a", - "w ie", - "b y", - "m o", - "w y", - "rz y", - "ł a", - "j a", - "n o", - "ł o", - "w o", - "p a", - "m a", - "t e", - "t y", - "n y", - "k i", - "d a", - "n e", - "dz ie", - "dz i", - "cz y", - "c ie", - "m y", - "p rze", - "d y", - "o d", - "l a", - "k ie", - "r y", - "st a", - "j ą", - "ó w", - "c e", - "p rzy", - "c o", - "k u", - "m ie", - "sz y", - "cz e", - "r e", - "b a", - "s i", - "b ie", - "m u", - "w e", - "c y", - "ni a", - "ś ci", - "sz e", - "je st", - "k t", - "s a", - "b o", - "t u", - "ż y", - "n ą", - "b i", - "r u", - "a le", - "kt ó", - "p ra", - "ał a", - "m nie", - "p ie", - "ł y", - "cz a", - "ja k", - "ro z", - "r ó", - "l u", - "z na", - "g a", - "ra z", - "ł u", - "ta k", - "j u", - "p i", - "ś ć", - "s o", - "wi a", - "m ó", - "ch o", - "w szy", - "p e", - "s po", - "c a", - "g dy", - "w ał", - "w ię", - "d e", - "b e", - "p ro", - "ł em", - "j ę", - "s k", - "z e", - "l o", - "g i", - "r ę", - "do b", - "d u", - "ju ż", - "st o", - "b ę", - "ał em", - "sz a", - "m e", - "po d", - "d la", - "pa n", - "n ę", - "z o", - "mo że", - "ś li", - "s ie", - "ał o", - "t em", - "l ko", - "ny ch", - "po wie", - "c ię", - "s u", - "ty lko", - "i n", - "b u", - "na j", - "ch a", - "te go", - "p u", - "s ki", - "ne go", - "wszy st", - "sz cze", - "je d", - "je j", - "t wo", - "ą d", - "ś my", - "cz ę", - "wa ć", - "je go", - "ż a", - "i m", - "s y", - "pra w", - "ty m", - "któ ry", - "ał y", - "t rze", - "nie j", - "s e", - "ny m", - "i ch", - "o b", - ". .", - "g ło", - "ją c", - "mó wi", - "s ka", - "o n", - "ne j", - "s łu", - "w ła", - "bę dzie", - "d ę", - "p ó", - "be z", - "ni c", - "p ła", - "ś cie", - "mi a", - "s ą", - "t rzy", - "kie m", - "by ł", - "mo g", - "ro bi", - "ta m", - "c u", - "te n", - "m ię", - "z y", - "pe w", - "ci a", - "my ś", - "prze d", - "s ko", - "n u", - "któ re", - "a l", - "l ę", - "w sze", - "ą c", - "by ło", - "so bie", - "p y", - "ci ą", - "ba r", - "je szcze", - "h a", - "t ę", - "b ra", - "cza s", - "sz ę", - "g ł", - "k ę", - "ma r", - "cz u", - "prze z", - "f i", - "s ło", - "w z", - "k to", - "k ów", - "cz o", - "li śmy", - "st ra", - "wię c", - "r ą", - "ma m", - "w ó", - "rz a", - "g ro", - "no ści", - "f a", - "we t", - "ną ł", - "ś mie", - "na wet", - "mu si", - "s wo", - "te j", - "w ą", - "w u", - "wi ą", - "ni u", - "cz ą", - "b li", - "dz o", - "s kie", - "n em", - "je śli", - "cze go", - "ch y", - "d ł", - "ty ch", - "by m", - "ż o", - "e ś", - "si ą", - "kie dy", - "na s", - "w ró", - "dz e", - "d ro", - "t ra", - "r ów", - "pa ni", - "z ie", - "ku l", - "na d", - "ch wi", - "ni m", - "t ro", - "by ć", - "cho dzi", - "ni o", - "dob rze", - "te raz", - "wo kul", - "co ś", - "k ł", - "pie r", - "h e", - "g dzie", - "dz y", - "p ię", - "d ź", - "k ą", - "g ó", - "z da", - "ch ce", - "st ę", - "o r", - "ś wia", - "wszyst ko", - "st ro", - "pe ł", - "wie m", - "wie l", - "ka ż", - "ki m", - "rz u", - "s ły", - "jed na", - "z u", - "myś l", - "mó j", - "g u", - "wa r", - "jest em", - "ó ż", - "mie j", - "mo ż", - "k ła", - "re sz", - "d łu", - "st wo", - "n ię", - "ma sz", - "że by", - "nie m", - "ja kie", - "st y", - "ni ą", - "we j", - "o j", - "g ra", - "s ła", - "no ść", - "z ło", - "sz czę", - ".. .", - "r i", - "le j", - "we go", - "c ał", - "dzi ał", - "ki ch", - "dz a", - "dz ię", - "o czy", - "zo sta", - "cz ło", - "na m", - "ki l", - "o na", - "sz u", - "w ę", - "pa r", - "mi ał", - "st rze", - "ce j", - "e j", - "zna j", - "da ć", - "miej s", - "k ró", - "k ry", - "bar dzo", - "si a", - "z i", - "ś nie", - "l ą", - "g ie", - "cie bie", - "d ni", - "st u", - "po trze", - "wokul ski", - "u wa", - "u mie", - "jedna k", - "k ra", - "wró ci", - "czło wie", - "czy ć", - "by ła", - "że li", - "m ę", - "c ę", - "z robi", - "mog ę", - "pro wa", - "r em", - "nie ch", - "cz nie", - "k ro", - "t ą", - "ch ci", - "b ro", - "dzie ć", - "sz ą", - "pa d", - "t rz", - "t ru", - "je m", - "a ni", - "t ów", - "a r", - "d ru", - "ta j", - "rze kł", - "sa m", - "st e", - "nie go", - "ta kie", - "w ała", - "to wa", - "ka pła", - "wi dzi", - "po dob", - "dz ę", - "t ał", - "stę p", - "b ą", - "po ko", - "w em", - "g ę", - "a by", - "g e", - "al bo", - "s pra", - "z no", - "de n", - "s mo", - "je sz", - "k się", - "jest eś", - "po z", - "ni gdy", - "k sią", - "c óż", - "w s", - "po w", - "t ka", - "ś wie", - "sz ka", - "sa mo", - "s ł", - "rz ę", - "na le", - "chce sz", - "ni k", - "p ę", - "chy ba", - "cią g", - "ją cy", - "wo j", - "na sze", - "mnie j", - "wię cej", - "z wy", - "o sta", - "f e", - "wa ż", - "h o", - "se r", - "śmie r", - "wie r", - "dz ą", - "za ś", - "gdy by", - "ja ki", - "wo l", - "wi n", - "d ą", - "ści a", - "roz ma", - "wa l", - "pa nie", - "sta r", - "ka z", - "je żeli", - "d em", - "w ra", - "ko ń", - "sie bie", - "zno wu", - "p ró", - "cz em", - "st wa", - "i sto", - "pó ł", - "d ał", - "ko bie", - "ała m", - "wy ch", - "ce sa", - "ni ch", - "za wsze", - "dzi ć", - "te ż", - "le pie", - "pro szę", - "k re", - "t wa", - "o t", - "ł ą", - "ch u", - "c ą", - "p rz", - "ł e", - "sze dł", - "od powie", - "my śli", - "ś wią", - "e n", - "e r", - "d e", - "a n", - "e t", - "i j", - "i n", - "e l", - "a a", - "s t", - "o r", - "g e", - "i s", - "a t", - "i e", - "c h", - "o n", - "e en", - "h et", - "i t", - "v er", - "aa r", - "a l", - "o or", - "g en", - "v an", - "o p", - "d en", - "h e", - "o m", - "t e", - "w e", - "i k", - "r e", - "z e", - "ij n", - "d at", - "b e", - "d er", - "in g", - "o e", - "ij k", - "a an", - "ch t", - "v oor", - "l e", - "i et", - "r o", - "m o", - "k en", - "z ijn", - "m en", - "i g", - "j e", - "n iet", - "a r", - "o o", - "i d", - "u n", - "i l", - "s ch", - "mo et", - "st e", - "u r", - "o l", - "he b", - "u it", - "g el", - "w ij", - "a s", - "m e", - "t en", - "w or", - "o u", - "v en", - "l en", - "aa t", - "d it", - "m et", - "r a", - "b en", - "s p", - "o ver", - "d ie", - "n o", - "w er", - "l ijk", - "f t", - "s l", - "an d", - "v e", - "t er", - "i er", - "i en", - "t o", - "d aar", - "g r", - "b el", - "de ze", - "d u", - "a g", - "k an", - "wor den", - "in gen", - "moet en", - "n en", - "on der", - "heb ben", - "r u", - "oo k", - "s en", - "c t", - "k t", - "no g", - "aa l", - "w as", - "u l", - "e er", - "b ij", - "m ijn", - "p ro", - "v ol", - "d o", - "k om", - "at ie", - "e ft", - "k el", - "al s", - "r ij", - "he id", - "a f", - "st el", - "m aar", - "a p", - "we e", - "a d", - "he eft", - "w aar", - "i cht", - "d an", - "er en", - "n e", - "w el", - "w at", - "w il", - "a cht", - "aa g", - "ge b", - "c on", - "z o", - "k e", - "b et", - "h ij", - "d ig", - "k un", - "u w", - "d t", - "d oor", - "t ij", - "a m", - "an g", - "on d", - "er s", - "is ch", - "ge en", - "i ge", - "ge v", - "ve el", - "n u", - "m a", - "on s", - "o f", - "b l", - "n aar", - "g ro", - "p l", - "an der", - "at en", - "kun nen", - "e cht", - "h ier", - "g oe", - "an t", - "u s", - "t wee", - "on t", - "de lijk", - "el e", - "u ur", - "al le", - "t oe", - "me er", - "i st", - "n a", - "n ie", - "on ze", - "l o", - "i m", - "p en", - "h ad", - "tij d", - "h oe", - "to t", - "z ou", - "a k", - "aa k", - "a men", - "d r", - "w oor", - "s e", - "wor dt", - "o t", - "gel ijk", - "g aan", - "i c", - "g er", - "k er", - "el d", - "e m", - "h ou", - "de l", - "z en", - "z el", - "te gen", - "b o", - "kom en", - "c om", - "i gen", - "e it", - "wer k", - "goe d", - "z al", - "z ij", - "sl ag", - "e s", - "z ien", - "a st", - "echt er", - "it ie", - "t ie", - "el ijk", - "m is", - "isch e", - "bel an", - "h aar", - "i ch", - "b er", - "h an", - "v r", - "al e", - "c i", - "gr ijk", - "in d", - "do en", - "l and", - "belan grijk", - "p un", - "op en", - "ct ie", - "zel f", - "m ij", - "it eit", - "ste m", - "me e", - "ar en", - "al l", - "b r", - "re cht", - "d ien", - "h u", - "g aat", - "pro b", - "m oe", - "p er", - "a u", - "ul len", - "z ich", - "daar om", - "or m", - "k l", - "v o", - "en t", - "st aat", - "z it", - "du i", - "n at", - "du s", - "d s", - "ver slag", - "kel ijk", - "prob le", - "w et", - "ge m", - "c r", - "i on", - "p r", - "sch ap", - "g d", - "h un", - "z a", - "er d", - "z et", - "st aan", - "st r", - "m aal", - "in der", - "e id", - "st en", - "p ar", - "k ken", - "ge d", - "z ullen", - "re s", - "men sen", - "j aar", - "re gel", - "ie der", - "vol gen", - "ge ven", - "e ven", - "l u", - "bl ij", - "i ë", - "k o", - "u we", - "m an", - "ma ken", - "l ie", - "g a", - "oe k", - "nie uwe", - "b aar", - "h o", - "h er", - "in ter", - "ander e", - "ru ik", - "s u", - "a gen", - "or t", - "m er", - "ou w", - "st er", - "wil len", - "aa kt", - "h oo", - "an den", - "f f", - "l ig", - "t re", - "s amen", - "ze er", - "dui delijk", - "ant woor", - "he el", - "men t", - "pun t", - "hou den", - "we g", - "vr aag", - "gel e", - "een s", - "be sch", - "om en", - "er g", - "do el", - "d ag", - "sp e", - "ur en", - "ing s", - "or en", - "l ang", - "de len", - "m ar", - "ste un", - "in nen", - "p ol", - "o on", - "i de", - "s n", - "s ie", - "r icht", - "z onder", - "no dig", - "all een", - "m id", - "ra gen", - "iet s", - "ver sch", - "geb ruik", - "st u", - "ro uw", - "stel len", - "be g", - "men ten", - "v in", - "eer ste", - "l aat", - "gro ot", - "oo d", - "to ch", - "l aten", - "aar d", - "s le", - "de el", - "st and", - "pl aat", - "re e", - "bet re", - "d i", - "l id", - "uit en", - "ra cht", - "bel eid", - "g et", - "ar t", - "st ie", - "st aten", - "g gen", - "re ken", - "e in", - "al en", - "m ing", - "mo gelijk", - "gro te", - "al tijd", - "z or", - "en kel", - "w ik", - "pol itie", - "e igen", - "el k", - "han del", - "g t", - "k we", - "m aat", - "el en", - "i p", - "v rij", - "s om", - "je s", - "aa m", - "hu is", - "v al", - "we er", - "lid staten", - "k ing", - "k le", - "be d", - "gev al", - "stel l", - "a i", - "wik kel", - "kwe stie", - "t al", - "ste e", - "a b", - "h el", - "kom st", - "p as", - "s s", - "it u", - "i den", - "eer d", - "m in", - "c e", - "p o", - "twee de", - "proble em", - "w aren", - "us sen", - "sn el", - "t ig", - "ge w", - "j u", - "ul t", - "ne men", - "com mis", - "versch il", - "k on", - "z oek", - "k rij", - "gr aag", - "den k", - "l anden", - "re den", - "be sl", - "oe g", - "bet er", - "he den", - "m ag", - "p e", - "bo ven", - "a c", - "con t", - "f d", - "h ele", - "k r", - "v ier", - "w in", - "ge z", - "k w", - "m il", - "v or", - "he m", - "ra m", - "aa s", - "ont wikkel", - "dr ie", - "v aak", - "plaat s", - "l a", - "g ang", - "ij f", - "f in", - "nat uur", - "t ussen", - "u g", - "in e", - "d a", - "b at", - "kom t", - "w acht", - "aa d", - "u t", - "é n", - "acht er", - "geb ie", - "ver k", - "lig t", - "c es", - "nie uw", - "van d", - "s t", - "n í", - "j e", - "p o", - "c h", - "r o", - "n a", - "s e", - "t o", - "n e", - "l e", - "k o", - "l a", - "d o", - "r a", - "n o", - "t e", - "h o", - "n ě", - "v a", - "l i", - "l o", - "ř e", - "c e", - "d e", - "v e", - "b y", - "n i", - "s k", - "t a", - "n á", - "z a", - "p ro", - "v o", - "v ě", - "m e", - "v á", - "s o", - "k a", - "r á", - "v y", - "z e", - "m i", - "p a", - "t i", - "st a", - "m ě", - "n é", - "ř i", - "ř í", - "m o", - "ž e", - "m a", - "j í", - "v ý", - "j i", - "d ě", - "r e", - "d a", - "k u", - "j a", - "c i", - "r u", - "č e", - "o b", - "t ě", - "m u", - "k y", - "d i", - "š e", - "k é", - "š í", - "t u", - "v i", - "p ře", - "v í", - "s i", - "n ý", - "o d", - "so u", - "v é", - "n y", - "r i", - "d y", - "b u", - "b o", - "t y", - "l á", - "l u", - "n u", - "ž i", - "m á", - "st i", - "c í", - "z á", - "p ra", - "sk é", - "m í", - "c o", - "d u", - "d á", - "by l", - "st o", - "s a", - "t í", - "je d", - "p ří", - "p ři", - "t é", - "s í", - "č i", - "v ní", - "č a", - "d í", - "z i", - "st u", - "p e", - "b a", - "d ní", - "ro z", - "va l", - "l í", - "s po", - "k á", - "b e", - "p i", - "no u", - "ta k", - "st e", - "r y", - "l é", - "vě t", - "se m", - "p ě", - "ko n", - "ne j", - "l y", - "ko u", - "ý ch", - "b ě", - "p r", - "f i", - "p rá", - "a le", - "ja ko", - "po d", - "ž í", - "z í", - "j sou", - "j sem", - "ch o", - "l ní", - "c ké", - "t á", - "m y", - "a k", - "h u", - "va t", - "pře d", - "h la", - "k e", - "st á", - "č í", - "š i", - "s le", - "k la", - "š tě", - "lo u", - "m ů", - "z na", - "ch á", - "o r", - "p ů", - "h a", - "b i", - "ta ké", - "d ů", - "no st", - "t ře", - "te r", - "p u", - "i n", - "v r", - "ve l", - "sk u", - "v še", - "t ní", - "do b", - "by la", - "č ní", - "ja k", - "v u", - "je ho", - "b ý", - "vá ní", - "ný ch", - "po u", - "te n", - "t ři", - "v z", - "st ře", - "d va", - "h le", - "č á", - "no sti", - "c k", - "v š", - "vo u", - "s u", - "h e", - "h ra", - "je n", - "s y", - "da l", - "po z", - "s lo", - "te l", - "d ru", - "de n", - "vš ak", - "g i", - "k dy", - "by lo", - "bu de", - "st ra", - "j ší", - "m é", - "me n", - "vý ch", - "ní m", - "s m", - "ko li", - "r ů", - "t ra", - "mů že", - "ne ní", - "ho d", - "b í", - "do u", - "sk a", - "t ý", - "st ě", - "u je", - "s á", - "pě t", - "ne s", - "k rá", - "to m", - "st ví", - "v ně", - "se d", - "s vé", - "p í", - "z o", - "mu sí", - "u ž", - "tí m", - "jí cí", - "jed no", - "t r", - "ča s", - "e v", - "č ty", - "sk ý", - "ni c", - "ev ro", - "to ho", - "h y", - "k ter", - "r ní", - "st í", - "s vě", - "pa k", - "vše ch", - "k ů", - "n g", - "á d", - "chá zí", - "a ni", - "a r", - "jed na", - "bý t", - "t ro", - "k ra", - "pr vní", - "m no", - "ské ho", - "p á", - "p la", - "le m", - "ne bo", - "ke m", - "st ro", - "s la", - "né ho", - "z de", - "dal ší", - "ř a", - "čty ři", - "h rá", - "dru h", - "l ně", - "v la", - "sk ých", - "š ko", - "pů so", - "pro to", - "v ů", - "sk á", - "ve n", - "še st", - "d ně", - "je ště", - "me zi", - "te k", - "s ko", - "ch a", - "ně koli", - "be z", - "g ra", - "ji ž", - "č ně", - "j á", - "s lu", - "z ná", - "ve r", - "sed m", - "k ro", - "ta m", - "a no", - "v lá", - "o sm", - "byl y", - "vá m", - "ck ý", - "te ch", - "dě ji", - "vel mi", - "le ži", - "va la", - "l ý", - "t vo", - "spo le", - "ch u", - "stu p", - "mo ž", - "evro p", - "g e", - "sta l", - "j de", - "ch y", - "ro di", - "je jí", - "po li", - "de vět", - "s me", - "a ž", - "té to", - "re m", - "d é", - "f or", - "u ni", - "f o", - "ten to", - "a u", - "ka ž", - "nu la", - "na d", - "by ch", - "mo c", - "sto u", - "e x", - "le n", - "k do", - "z d", - "pra co", - "to mu", - "ný m", - "ži vo", - "ze m", - "f e", - "f u", - "ná sle", - "j o", - "sk y", - "ji ch", - "h á", - "mě l", - "dě la", - "j sme", - "p re", - "ni ce", - "ste j", - "ne m", - "st ní", - "he m", - "ná ro", - "z u", - "b li", - "ni t", - "pa r", - "a l", - "poz ději", - "ta ko", - "n ce", - "če r", - "ší m", - "ně co", - "vá l", - "ře j", - "krá t", - "á lní", - "u r", - ". .", - "a si", - "kter é", - "sta v", - "ma jí", - "my s", - "do bě", - "s ně", - "ce n", - "z y", - "z ku", - "t ů", - "ch od", - "s pě", - "je jich", - "sou čas", - "d r", - "va li", - "ri e", - "k te", - "pr ů", - "ze ní", - "pa t", - "a n", - "po tře", - "de m", - "d nes", - "ze mí", - "sa mo", - "zna m", - "b ra", - "má m", - "te dy", - "g o", - "hla vní", - "pou ží", - "b ní", - "ve de", - "le p", - "je k", - "pra v", - "poli ti", - "d ne", - "je m", - "le t", - "če ní", - "pro b", - "ne ž", - "dě l", - "fi l", - "č o", - "cí ch", - "st é", - "d lou", - "h i", - "a by", - "to u", - "několi k", - "d la", - "vy u", - "vi t", - "ho u", - "ck ých", - "no vé", - "či n", - "st y", - "dě lá", - "k ý", - "ob la", - "pod le", - "ra n", - "dů leži", - "ta to", - "po ku", - "ko ne", - "d ý", - "d vě", - "ž ád", - "nou t", - "t ku", - "t vr", - "cké ho", - "ro v", - "r é", - "te le", - "p sa", - "s vět", - "ti vní", - "do sta", - "te m", - "še l", - "druh é", - "s kou", - "ž o", - "jed ná", - "vý znam", - "prob lé", - "pu bli", - "vá n", - "od po", - "pod po", - "d le", - "ja ké", - "še ní", - "ví m", - "bě hem", - "na chází", - "s lou", - "pou ze", - "o tá", - "p lo", - "to vé", - "vět ši", - "ko mi", - "va jí", - "ty to", - "zá pa", - "z mě", - "mo h", - "ví ce", - "spole č", - "au to", - "pro ti", - "st ru", - "dě t", - "chá ze", - "že l", - "с т", - "е н", - "н о", - "н а", - "п р", - "т о", - "п о", - "р а", - "г о", - "к о", - "н е", - "в о", - "в а", - "е т", - "е р", - "н и", - "е л", - "и т", - "н ы", - "з а", - "р о", - "ен и", - "к а", - "л и", - "е м", - "д а", - "о б", - "л а", - "д о", - "с я", - "т ь", - "о т", - "л о", - "л ь", - "е д", - "с о", - "м и", - "р е", - "м о", - "ц и", - "пр о", - "т а", - "э то", - "к и", - "р у", - "пр и", - "т и", - "с е", - "ст а", - "в ы", - "м ы", - "в и", - "б ы", - "м а", - "е с", - "л я", - "ст и", - "л е", - "ч то", - "м е", - "р и", - "ч а", - "о д", - "е й", - "ел ь", - "ени я", - "г а", - "н у", - "с и", - "п а", - "ра з", - "б о", - "ст о", - "с у", - "с а", - "д у", - "е го", - "е ст", - "и н", - "ит ь", - "и з", - "ж е", - "м у", - "п ер", - "по д", - "ени е", - "с ь", - "к у", - "пр ед", - "но го", - "ны х", - "в ер", - "т е", - "но й", - "ци и", - "д е", - "р ы", - "д ел", - "л ю", - "в е", - "о н", - "м ен", - "г и", - "н я", - "б у", - "пр а", - "в се", - "ет ся", - "ст ь", - "ж а", - "до л", - "ж и", - "б е", - "ко н", - "с л", - "ш и", - "д и", - "ст в", - "с ко", - "ны е", - "ч и", - "ю т", - "д ер", - "ст ра", - "т ы", - "х од", - "щ и", - "з о", - "з на", - "но сти", - "ч ес", - "в ля", - "ва ть", - "о р", - "по л", - "в ет", - "та к", - "ш а", - "т у", - "с во", - "пр е", - "о на", - "ит ель", - "ны й", - "с ло", - "ка к", - "в л", - "но сть", - "х о", - "мо ж", - "п е", - "д ля", - "ни я", - "но е", - "ра с", - "дол ж", - "да р", - "т ель", - "с ка", - "п у", - "ст во", - "ко то", - "ра б", - "е е", - "ро д", - "э ти", - "с об", - "о ру", - "ж ен", - "ны м", - "ит и", - "ни е", - "ко м", - "д ет", - "ст у", - "г у", - "п и", - "ме ж", - "ени ю", - "т ер", - "раб от", - "во з", - "ци я", - "ко й", - "щ ест", - "г ра", - "з и", - "р я", - "меж ду", - "ст ва", - "в с", - "ел о", - "ш е", - "м ер", - "б а", - "з ы", - "л у", - "а ль", - "д ей", - "г ла", - "на род", - "к ти", - "пред ста", - "л ся", - "я вля", - "с ки", - "но в", - "ед ин", - "ро в", - "и с", - "ни ма", - "р ем", - "ход и", - "так же", - "д ру", - "а ть", - "сл ед", - "го во", - "на я", - "ю щи", - "ен ь", - "кото ры", - "х от", - "в у", - "и х", - "ем у", - "ч ит", - "ва ж", - "ор га", - "чес ки", - "щ е", - "к е", - "х а", - "по с", - "то м", - "бо ль", - "м не", - "па с", - "об ъ", - "пра в", - "кон ф", - "сл у", - "под дер", - "ст ви", - "на ш", - "ль ко", - "сто я", - "ну ю", - "л ем", - "ен ных", - "к ра", - "д ы", - "между народ", - "г да", - "не об", - "го су", - "ств у", - "ени и", - "госу дар", - "к то", - "и м", - "ч ест", - "р ет", - "во про", - "л ен", - "ел и", - "ро ва", - "ци й", - "на м", - "это й", - "ж ения", - "необ ходи", - "мен я", - "бы ло", - "си ли", - "ф и", - "в я", - "ш ь", - "это го", - "о ни", - "орга ни", - "бе зо", - "пр об", - "и ме", - "ре ш", - "б и", - "безо пас", - "ют ся", - "о ста", - "ен но", - "го д", - "ел а", - "предста в", - "ть ся", - "сло во", - "органи за", - "долж ны", - "это м", - "б ла", - "ч е", - "ч у", - "бла го", - "это му", - "в рем", - "с пе", - "но м", - "ени й", - "с по", - "на с", - "не т", - "з у", - "в ед", - "е ще", - "ска за", - "се й", - "ер ен", - "да н", - "са м", - "ел я", - "ра н", - "зы ва", - "явля ется", - "бу дет", - "кти в", - "т ре", - "дел е", - "м от", - "конф ерен", - "ла сь", - "ча с", - "сто ро", - "ко го", - "е з", - "не й", - "о с", - "ли сь", - "раз ору", - "пер е", - "с си", - "ны ми", - "про ц", - "го ло", - "ч ело", - "бо ле", - "чело ве", - "с ер", - "п л", - "ч ет", - "стра н", - "п я", - "бы л", - "к ла", - "то в", - "ж д", - "дел а", - "е ра", - "у же", - "со вет", - "г ен", - "безопас ности", - "ц а", - "се да", - "по з", - "от вет", - "проб лем", - "на ко", - "т ем", - "до ста", - "п ы", - "щ а", - "во й", - "су щест", - "необходи мо", - "бы ть", - "мож ет", - "д ем", - "что бы", - "е к", - "ч ер", - "у сили", - "ре с", - "ру д", - "един енных", - "д об", - "до сти", - "ств ен", - "я дер", - "год ня", - "ка за", - "се годня", - "сей час", - "то лько", - "во д", - "ес ь", - "м ного", - "бу ду", - "е в", - "ест ь", - "т ри", - "об щест", - ". .", - "я вл", - "вы сту", - "р ед", - "с чит", - "с ит", - "деле га", - "ло ж", - "это т", - "ф ор", - "к лю", - "воз мож", - "ва ния", - "б ли", - "и ли", - "в з", - "на ций", - "ско го", - "при ня", - "п ла", - "о ч", - "ить ся", - "ст е", - "на ши", - "которы е", - "а р", - "име ет", - "с от", - "зна ч", - "пер ь", - "след у", - "ен ы", - "та ки", - "объ единенных", - "ст ро", - "те перь", - "б ле", - "благо дар", - "раз в", - "а н", - "жи ва", - "оч ень", - "я т", - "бе з", - "об ес", - "г ро", - "ло сь", - "с ы", - "организа ции", - "ч лен", - "то го", - "она ль", - "ж да", - "все х", - "с вя", - "боле е", - "со в", - "ко гда", - "во т", - "к ре", - "к ры", - "по этому", - "во ль", - "о й", - "ген ера", - "ч ем", - "л ы", - "пол ити", - "в ен", - "конферен ции", - "проц ес", - "б я", - "ит е", - "от но", - "разв ити", - "а ф", - "ю щ", - "в но", - "ми р", - "ни и", - "ка я", - "а с", - "итель но", - "в то", - "ени ем", - "генера ль", - "пр от", - "вс ем", - "сам бле", - "ас самбле", - "о м", - "з д", - "с мот", - "ре ги", - "ч его", - "од нако", - "усили я", - "дей стви", - "ч но", - "у ча", - "об раз", - "во с", - "э та", - "пер его", - "гово р", - "ва м", - "мо ло", - "врем я", - "д ь", - "хот ел", - "г ру", - "за явл", - "пре доста", - "по ль", - "не е", - "ре зо", - "перего во", - "резо лю", - "к рет", - "поддер ж", - "обес пе", - "не го", - "представ ит", - "на де", - "к ри", - "ч ь", - "про ек", - "л ет", - "дру ги", - "ا ل", - "َ ا", - "و َ", - "ّ َ", - "ِ ي", - "أ َ", - "ل َ", - "ن َ", - "ال ْ", - "ه ُ", - "ُ و", - "م ا", - "ن ْ", - "م ن", - "ع َ", - "ن ا", - "ل ا", - "م َ", - "ت َ", - "ف َ", - "أ ن", - "ل ي", - "م ِ", - "ا ن", - "ف ي", - "ر َ", - "ي َ", - "ه ِ", - "م ْ", - "ق َ", - "ب ِ", - "ل ى", - "ي ن", - "إ ِ", - "ل ِ", - "و ا", - "ك َ", - "ه ا", - "ً ا", - "م ُ", - "و ن", - "ال م", - "ب َ", - "ي ا", - "ذ ا", - "س ا", - "ال ل", - "م ي", - "ي ْ", - "ر ا", - "ر ي", - "ل ك", - "م َا", - "ن َّ", - "ل م", - "إ ن", - "س ت", - "و م", - "ّ َا", - "ل َا", - "ه م", - "ّ ِ", - "ك ُ", - "ك ان", - "س َ", - "ب ا", - "د ي", - "ح َ", - "ع ْ", - "ب ي", - "ال أ", - "و ل", - "ف ِي", - "ر ِ", - "د ا", - "مِ نْ", - "ُو نَ", - "و ْ", - "ه َا", - "ّ ُ", - "ال س", - "ال َ", - "ن ي", - "ل ْ", - "ت ُ", - "ه ل", - "ر ة", - "د َ", - "س ْ", - "ت ِ", - "ن َا", - "ر ْ", - "الل َّ", - "سا مي", - "ك ن", - "ك ل", - "ه َ", - "عَ لَ", - "ع لى", - "م ع", - "إ لى", - "ق د", - "ال ر", - "ُو ا", - "ي ر", - "ع ن", - "ي ُ", - "ن ِ", - "ب ْ", - "ال ح", - "هُ مْ", - "ق ا", - "ذ ه", - "ال ت", - "ِي نَ", - "ج َ", - "ه ذا", - "ع د", - "ال ع", - "د ْ", - "قَ الَ", - "ر ُ", - "ي م", - "ي ة", - "ن ُ", - "خ َ", - "ر ب", - "ال ك", - "و َا", - "أ نا", - "ة ِ", - "ال ن", - "ح د", - "ع ِ", - "ت ا", - "ه و", - "ف ا", - "ع ا", - "ال ش", - "ل ُ", - "ي ت", - "ذ َا", - "ي ع", - "ال ذ", - "ح ْ", - "ال ص", - "إِ نَّ", - "ج ا", - "ع لي", - "ك َا", - "ب ُ", - "ت ع", - "و ق", - "م ل", - "ل َّ", - "ي د", - "أ خ", - "ر ف", - "ت ي", - "ال ِ", - "ّ ا", - "ذ لك", - "أَ نْ", - "س ِ", - "ت وم", - "م ر", - "مَ نْ", - "ب ل", - "ال ق", - "الل ه", - "ِي َ", - "ك م", - "ذ َ", - "ع ل", - "ح ب", - "س ي", - "ع ُ", - "ال ج", - "ال د", - "ش َ", - "ت ك", - "ف ْ", - "ص َ", - "ل ل", - "د ِ", - "ب ر", - "ف ِ", - "ت ه", - "أ ع", - "ت ْ", - "ق ْ", - "الْ أَ", - "ئ ِ", - "عَ نْ", - "و ر", - "ح ا", - "ال َّ", - "م ت", - "ف ر", - "د ُ", - "ه نا", - "وَ أَ", - "ت ب", - "ة ُ", - "أ ي", - "س ب", - "ري د", - "و ج", - "كُ مْ", - "ح ِ", - "ك ْ", - "د ر", - "َا ء", - "ه ذه", - "ال ط", - "الْ مُ", - "د ة", - "ق ل", - "غ َ", - "ي وم", - "الَّ ذ", - "ك ر", - "ت ر", - "ك ِ", - "ك ي", - "عَلَ ى", - "رَ ب", - "ع ة", - "ق ُ", - "ج ْ", - "ف ض", - "ل ة", - "ه ْ", - "ر َا", - "وَ لَ", - "الْ مَ", - "أَ نَّ", - "ي َا", - "أ ُ", - "ش ي", - "اللَّ هُ", - "لَ ى", - "ق ِ", - "أ ت", - "عَلَ يْ", - "اللَّ هِ", - "ال ب", - "ض َ", - "ة ً", - "ق ي", - "ا ر", - "ب د", - "خ ْ", - "سْ تَ", - "ط َ", - "قَ دْ", - "ذه ب", - "أ م", - "ما ذا", - "وَ إِ", - "ة ٌ", - "و نَ", - "لي لى", - "و لا", - "ح ُ", - "ه ي", - "ص ل", - "ال خ", - "و د", - "لي س", - "ل دي", - "ق ال", - "كَا نَ", - "م َّ", - "ح ي", - "ت م", - "ل ن", - "وَ لَا", - "ب ع", - "يم كن", - "س ُ", - "ة َ", - "ح ت", - "ر ًا", - "ك ا", - "ش ا", - "هِ مْ", - "لَ هُ", - "ز َ", - "دا ً", - "م س", - "ك ث", - "الْ عَ", - "ج ِ", - "ص ْ", - "ف َا", - "ل ه", - "و ي", - "ع َا", - "هُ وَ", - "ب ِي", - "ب َا", - "أ س", - "ث َ", - "ل ِي", - "ر ض", - "الر َّ", - "لِ كَ", - "ت َّ", - "ف ُ", - "ق ة", - "ف عل", - "مِ ن", - "ال آ", - "ث ُ", - "س م", - "م َّا", - "بِ هِ", - "ت ق", - "خ ر", - "ل قد", - "خ ل", - "ش ر", - "أن ت", - "ل َّا", - "س ن", - "الس َّ", - "الذ ي", - "س َا", - "و ما", - "ز ل", - "و ب", - "أ ْ", - "إ ذا", - "ر ِي", - "ح ة", - "ن ِي", - "الْ حَ", - "وَ قَالَ", - "ب ه", - "ة ٍ", - "س أ", - "ر ٌ", - "ب ال", - "م ة", - "ش ْ", - "و ت", - "عن د", - "ف س", - "بَ عْ", - "ه ر", - "ق ط", - "أ ح", - "إن ه", - "و ع", - "ف ت", - "غ ا", - "هنا ك", - "ب ت", - "مِ نَ", - "س ر", - "ذَ لِكَ", - "ر س", - "حد ث", - "غ ْ", - "ّ ِي", - "ال إ", - "وَ يَ", - "ج ل", - "ا ست", - "ق ِي", - "ع ب", - "و س", - "ي ش", - "الَّذ ِينَ", - "تا ب", - "د ِي", - "ج ب", - "ك ون", - "ب ن", - "ال ث", - "لَ يْ", - "ب عد", - "وَ الْ", - "فَ أَ", - "ع م", - "هُ م", - "ت ن", - "ذ ْ", - "أ ص", - "أ ين", - "رَب ِّ", - "الذ ين", - "إِ ن", - "ب ين", - "ج ُ", - "عَلَيْ هِ", - "ح َا", - "ل و", - "ست ط", - "ظ ر", - "لَ مْ", - "ء ِ", - "كُ ل", - "ط ل", - "ت َا", - "ض ُ", - "كن ت", - "ل ًا", - "م ٌ", - "ق بل", - "ـ ـ", - "ذ ِ", - "قَ وْ", - "ص ِ", - "م ًا", - "كان ت", - "ص ا", - "ي ق", - "ال ف", - "ال نا", - "م ٍ", - "إِ نْ", - "ال نَّ", - "ج د", - "وَ مَا", - "ت ت", - "ب ح", - "م كان", - "كي ف", - "ّ ة", - "ال ا", - "ج َا", - "أ و", - "سا عد", - "ض ِ", - "إ لا", - "را ً", - "ق َا", - "ر أ", - "ع ت", - "أ حد", - "ه د", - "ض ا", - "ط ر", - "أ ق", - "ما ء", - "د َّ", - "ال با", - "م ُو", - "أَ وْ", - "ط ا", - "ق ُو", - "خ ِ", - "ت ل", - "ستط يع", - "د َا", - "الن َّا", - "إ لَى", - "وَ تَ", - "هَ ذَا", - "ب ة", - "علي ك", - "ج ر", - "ال من", - "ز ا", - "ر ٍ", - "د ع", - "ّ ًا", - "س ة", - "ثُ مَّ", - "شي ء", - "ال غ", - "ت ح", - "ر ُونَ", - "ال يوم", - "م ِي", - "ن ُوا", - "أ ر", - "تُ مْ", - "ع ر", - "ي ف", - "أ ب", - "د ًا", - "ص َا", - "الت َّ", - "أ ريد", - "ال ز", - "يَ وْ", - "إ لي", - "ج ي", - "يَ عْ", - "فض ل", - "ال إن", - "أن ه", - "n g", - "i 4", - "a n", - "s h", - "z h", - "i 2", - "ng 1", - "u 4", - "i 1", - "ng 2", - "d e", - "j i", - "a o", - "x i", - "u 3", - "de 5", - "e 4", - "i 3", - "ng 4", - "an 4", - "e n", - "u o", - "sh i4", - "an 2", - "u 2", - "c h", - "u 1", - "ng 3", - "a 1", - "an 1", - "e 2", - "a 4", - "e i4", - "o ng1", - "a i4", - "ao 4", - "h u", - "a ng1", - "l i", - "y o", - "an 3", - "w ei4", - "uo 2", - "n 1", - "en 2", - "ao 3", - "e 1", - "y u", - "q i", - "e ng2", - "zh o", - "a ng3", - "a ng4", - "a ng2", - "uo 4", - "m i", - "g e4", - "y i1", - "g uo2", - "e r", - "b i", - "a 3", - "h e2", - "e 3", - "y i2", - "d i4", - "zh ong1", - "b u4", - "g u", - "a i2", - "n 2", - "z ai4", - "sh i2", - "e ng1", - "r en2", - "o ng2", - "xi an4", - "y i", - "x u", - "n 4", - "l i4", - "en 4", - "y u2", - "e i2", - "yi2 ge4", - "o u4", - "e i3", - "d i", - "u i4", - "a 2", - "yo u3", - "ao 1", - "d a4", - "ch eng2", - "en 1", - "e ng4", - "y i4", - "s i1", - "zh i4", - "ji a1", - "yu an2", - "n i", - "t a1", - "de5 yi2ge4", - "k e1", - "sh u3", - "x i1", - "j i2", - "ao 2", - "t i", - "o u3", - "o ng4", - "xi a4", - "a i1", - "g ong1", - "zh i1", - "en 3", - "w ei2", - "j u", - "xu e2", - "q u1", - "zho u1", - "er 3", - "mi ng2", - "zho ng3", - "l i3", - "w u4", - "y i3", - "uo 1", - "e 5", - "j i4", - "xi ng2", - "ji an4", - "hu a4", - "y u3", - "uo 3", - "j i1", - "a i3", - "z uo4", - "h ou4", - "hu i4", - "e i1", - "ni an2", - "q i2", - "p i", - "d ao4", - "sh eng1", - "de 2", - "d ai4", - "u an2", - "zh e4", - "zh eng4", - "b en3", - "sh ang4", - "zh u3", - "b ei4", - "y e4", - "ch u1", - "zh an4", - "l e5", - "l ai2", - "sh i3", - "n an2", - "r en4", - "yo u2", - "k e4", - "b a1", - "f u4", - "d ui4", - "y a4", - "m ei3", - "z i4", - "xi n1", - "ji ng1", - "zh u", - "n 3", - "yo ng4", - "m u4", - "ji ao4", - "y e3", - "ji n4", - "bi an4", - "l u4", - "q i1", - "sh e4", - "xi ang1", - "o ng3", - "sh u4", - "d ong4", - "s uo3", - "gu an1", - "s an1", - "b o", - "t e4", - "d uo1", - "f u2", - "mi n2", - "l a1", - "zh i2", - "zh en4", - "o u1", - "w u3", - "m a3", - "i 5", - "z i5", - "j u4", - "er 4", - "y ao4", - "xia4 de5yi2ge4", - "s i4", - "t u2", - "sh an1", - "z ui4", - "ch u", - "yi n1", - "er 2", - "t ong2", - "d ong1", - "y u4", - "y an2", - "qi an2", - "shu3 xia4de5yi2ge4", - "ju n1", - "k e3", - "w en2", - "f a3", - "l uo2", - "zh u4", - "x i4", - "k ou3", - "b ei3", - "ji an1", - "f a1", - "di an4", - "ji ang1", - "wei4 yu2", - "xi ang4", - "zh i3", - "e ng3", - "f ang1", - "l an2", - "sh u", - "r i4", - "li an2", - "sh ou3", - "m o", - "qi u2", - "ji n1", - "h uo4", - "shu3xia4de5yi2ge4 zhong3", - "f en1", - "n ei4", - "g ai1", - "mei3 guo2", - "u n2", - "g e2", - "b ao3", - "qi ng1", - "g ao1", - "t ai2", - "d u", - "xi ao3", - "ji e2", - "ti an1", - "ch ang2", - "q uan2", - "li e4", - "h ai3", - "f ei1", - "t i3", - "ju e2", - "o u2", - "c i3", - "z u2", - "n i2", - "bi ao3", - "zhong1 guo2", - "d u4", - "yu e4", - "xi ng4", - "sh eng4", - "ch e1", - "d an1", - "ji e1", - "li n2", - "pi ng2", - "f u3", - "g u3", - "ji e4", - "w o", - "v 3", - "sh eng3", - "n a4", - "yu an4", - "zh ang3", - "gu an3", - "d ao3", - "z u3", - "di ng4", - "di an3", - "c eng2", - "ren2 kou3", - "t ai4", - "t ong1", - "g uo4", - "n eng2", - "ch ang3", - "hu a2", - "li u2", - "yi ng1", - "xi ao4", - "c i4", - "bian4 hua4", - "li ang3", - "g ong4", - "zho ng4", - "de5 yi1", - "s e4", - "k ai1", - "w ang2", - "ji u4", - "sh i1", - "sh ou4", - "m ei2", - "k u", - "s u", - "f eng1", - "z e2", - "tu2 shi4", - "t i2", - "q i4", - "ji u3", - "sh en1", - "zh e3", - "ren2kou3 bian4hua4", - "ren2kou3bian4hua4 tu2shi4", - "di4 qu1", - "y ang2", - "m en", - "men 5", - "l ong2", - "bi ng4", - "ch an3", - "zh u1", - "w ei3", - "w ai4", - "xi ng1", - "bo 1", - "b i3", - "t ang2", - "hu a1", - "bo 2", - "shu i3", - "sh u1", - "d ou1", - "s ai4", - "ch ao2", - "b i4", - "li ng2", - "l ei4", - "da4 xue2", - "f en4", - "shu3 de5", - "m u3", - "ji ao1", - "d ang1", - "ch eng1", - "t ong3", - "n v3", - "q i3", - "y an3", - "mi an4", - "l uo4", - "ji ng4", - "g e1", - "r u4", - "d an4", - "ri4 ben3", - "p u3", - "yu n4", - "hu ang2", - "wo 3", - "l v", - "h ai2", - "shi4 yi1", - "xi e1", - "yi ng3", - "w u2", - "sh en2", - "w ang3", - "gu ang3", - "li u4", - "s u4", - "shi4 zhen4", - "c an1", - "c ao3", - "xi a2", - "k a3", - "d a2", - "h u4", - "b an4", - "d ang3", - "h u2", - "z ong3", - "de ng3", - "de5yi2ge4 shi4zhen4", - "ch uan2", - "mo 4", - "zh ang1", - "b an1", - "mo 2", - "ch a2", - "c e4", - "zhu3 yao4", - "t ou2", - "j u2", - "shi4 wei4yu2", - "s a4", - "u n1", - "ke3 yi3", - "d u1", - "h an4", - "li ang4", - "sh a1", - "ji a3", - "z i1", - "lv 4", - "f u1", - "xi an1", - "x u4", - "gu ang1", - "m eng2", - "b ao4", - "yo u4", - "r ong2", - "zhi1 yi1", - "w ei1", - "m ao2", - "guo2 jia1", - "c ong2", - "g ou4", - "ti e3", - "zh en1", - "d u2", - "bi an1", - "c i2", - "q u3", - "f an4", - "xi ang3", - "m en2", - "j u1", - "h ong2", - "z i3", - "ta1 men5", - "ji 3", - "z ong1", - "zhou1 de5yi2ge4shi4zhen4", - "t uan2", - "ji ng3", - "gong1 si1", - "xi e4", - "l i2", - "li4 shi3", - "b ao1", - "g ang3", - "gu i1", - "zh eng1", - "zhi2 wu4", - "ta1 de5", - "pi n3", - "zhu an1", - "ch ong2", - "shi3 yong4", - "w a3", - "sh uo1", - "chu an1", - "l ei2", - "w an1", - "h uo2", - "q u", - "s u1", - "z ao3", - "g ai3", - "q u4", - "g u4", - "l u", - "x i2", - "h ang2", - "yi ng4", - "c un1", - "g en1", - "yi ng2", - "ti ng2", - "cheng2 shi4", - "ji ang3", - "li ng3", - "l un2", - "bu4 fen4", - "de ng1", - "xu an3", - "dong4 wu4", - "de2 guo2", - "xi an3", - "f an3", - "zh e5", - "h an2", - "h ao4", - "m i4", - "r an2", - "qi n1", - "ti ao2", - "zh an3", - "h i", - "k a", - "n o", - "t e", - "s u", - "s hi", - "t a", - "t o", - "n a", - "w a", - "o u", - "r u", - "n i", - "k u", - "k i", - "g a", - "d e", - "k o", - "m a", - "r e", - "r a", - "m o", - "t su", - "w o", - "e n", - "r i", - "s a", - "d a", - "s e", - "j i", - "h a", - "c hi", - "k e", - "te ki", - "m i", - "y ou", - "s h", - "s o", - "y o", - "y a", - "na i", - "t te", - "a ru", - "b a", - "u u", - "t ta", - "ka i", - "ka n", - "shi te", - "m e", - "d o", - "mo no", - "se i", - "r o", - "ko to", - "ka ra", - "shi ta", - "b u", - "m u", - "c h", - "su ru", - "k ou", - "g o", - "ma su", - "ta i", - "f u", - "k en", - "i u", - "g en", - "wa re", - "shi n", - "z u", - "a i", - "o n", - "o ku", - "g i", - "d ou", - "n e", - "y uu", - "i ru", - "i te", - "ji ko", - "de su", - "j u", - "ra re", - "sh u", - "b e", - "sh ou", - "s ha", - "se kai", - "s ou", - "k you", - "ma shita", - "s en", - "na ra", - "sa n", - "ke i", - "i ta", - "a ri", - "i tsu", - "ko no", - "j ou", - "na ka", - "ch ou", - "so re", - "g u", - "na ru", - "ga ku", - "re ba", - "g e", - "h o", - "i n", - "hi to", - "sa i", - "na n", - "da i", - "tsu ku", - "shi ki", - "sa re", - "na ku", - "p p", - "bu n", - "ju n", - "so no", - "ka ku", - "z ai", - "b i", - "to u", - "wa ta", - "sh uu", - "i i", - "te i", - "ka re", - "y u", - "shi i", - "ma de", - "sh o", - "a n", - "ke reba", - "shi ka", - "i chi", - "ha n", - "de ki", - "ni n", - "ware ware", - "na kereba", - "o ite", - "h ou", - "ya ku", - "ra i", - "mu jun", - "l e", - "yo ku", - "bu tsu", - "o o", - "ko n", - "o mo", - "ga e", - "nara nai", - "ta chi", - "z en", - "ch uu", - "kan gae", - "ta ra", - "to ki", - "ko ro", - "mujun teki", - "z e", - "na ga", - "ji n", - "shi ma", - "te n", - "i ki", - "i ku", - "no u", - "i masu", - "r ou", - "h on", - "ka e", - "t to", - "ko re", - "ta n", - "ki ta", - "i s", - "da tta", - "ji tsu", - "ma e", - "i e", - "me i", - "da n", - "h e", - "to ku", - "dou itsu", - "ri tsu", - "k yuu", - "h you", - "rare ta", - "kei sei", - "k kan", - "rare ru", - "m ou", - "do ko", - "r you", - "da ke", - "naka tta", - "so ko", - "ta be", - "e r", - "ha na", - "c o", - "fu ku", - "p a", - "so n", - "ya su", - "ch o", - "wata ku", - "ya ma", - "z a", - "k yo", - "gen zai", - "b oku", - "a ta", - "j a", - "ka wa", - "ma sen", - "j uu", - "ro n", - "b o", - "na tte", - "wataku shi", - "yo tte", - "ma i", - "g ou", - "ha i", - "mo n", - "ba n", - "ji shin", - "c a", - "re te", - "n en", - "o ka", - "ka gaku", - "na tta", - "p o", - "ka ru", - "na ri", - "m en", - "ma ta", - "e i", - "ku ru", - "ga i", - "ka ri", - "sha kai", - "kou i", - "yo ri", - "se tsu", - "j o", - "re ru", - "to koro", - "ju tsu", - "i on", - "sa ku", - "tta i", - "c ha", - "nin gen", - "n u", - "c e", - "ta me", - "kan kyou", - "de n", - "o oku", - "i ma", - "wata shi", - "tsuku ru", - "su gi", - "b en", - "ji bun", - "shi tsu", - "ke ru", - "ki n", - "ki shi", - "shika shi", - "mo to", - "ma ri", - "i tte", - "de shita", - "n de", - "ari masu", - "te r", - "z ou", - "ko e", - "ze ttai", - "kkan teki", - "h en", - "re kishi", - "deki ru", - "tsu ka", - "l a", - "i tta", - "o i", - "ko butsu", - "mi ru", - "sh oku", - "shi masu", - "gi jutsu", - "g you", - "jou shiki", - "a tta", - "ho do", - "ko ko", - "tsuku rareta", - "z oku", - "hi tei", - "ko ku", - "rekishi teki", - "ke te", - "o ri", - "i mi", - "ka ko", - "naga ra", - "ka karu", - "shu tai", - "ha ji", - "ma n", - "ta ku", - "ra n", - "douitsu teki", - "z o", - "me te", - "re i", - "tsu u", - "sare te", - "gen jitsu", - "p e", - "s t", - "ba i", - "na wa", - "ji kan", - "wa ru", - "r t", - "a tsu", - "so ku", - "koui teki", - "a ra", - "u ma", - "a no", - "i de", - "ka ta", - "te tsu", - "ga wa", - "ke do", - "re ta", - "mi n", - "sa you", - "tte ru", - "to ri", - "p u", - "ki mi", - "b ou", - "mu ra", - "sare ru", - "ma chi", - "k ya", - "o sa", - "kon na", - "a ku", - "a l", - "sare ta", - "i pp", - "shi ku", - "u chi", - "hito tsu", - "ha tara", - "tachi ba", - "shi ro", - "ka tachi", - "to mo", - "e te", - "me ru", - "ni chi", - "da re", - "ka tta", - "e ru", - "su ki", - "a ge", - "oo ki", - "ma ru", - "mo ku", - "o ko", - "kangae rareru", - "o to", - "tan ni", - "ta da", - "tai teki", - "mo tte", - "ki nou", - "shi nai", - "k ki", - "u e", - "ta ri", - "l i", - "ra nai", - "k kou", - "mi rai", - "pp on", - "go to", - "hi n", - "hi tsu", - "te ru", - "mo chi", - "ka tsu", - "re n", - "n yuu", - "su i", - "zu ka", - "tsu ite", - "no mi", - "su gu", - "ku da", - "tetsu gaku", - "i ka", - "ron ri", - "o ki", - "ni ppon", - "p er", - "shi mashita", - "chi shiki", - "cho kkanteki", - "su ko", - "t ion", - "ku u", - "a na", - "a rou", - "ka tte", - "ku ri", - "i nai", - "hyou gen", - "i shiki", - "do ku", - "a tte", - "a tara", - "to n", - "wa ri", - "ka o", - "sei san", - "hana shi", - "s i", - "ka ke", - "na ji", - "su nawa", - "sunawa chi", - "u go", - "su u", - "ba ra", - "le v", - "hi ro", - "i wa", - "be tsu", - "yo i", - "se ru", - "shite ru", - "rare te", - "to shi", - "se ki", - "tai ritsu", - "wa kara", - "to kyo", - "k ka", - "k yoku", - "u n", - "i ro", - "mi te", - "sa ki", - "kan ji", - "mi ta", - "su be", - "r yoku", - "ma tta", - "kuda sai", - "omo i", - "ta no", - "ware ru", - "co m", - "hitsu you", - "ka shi", - "re nai", - "kan kei", - "a to", - "ga tte", - "o chi", - "mo tsu", - "in g", - "son zai", - "l l", - "o re", - "tai shite", - "a me", - "sei mei", - "ka no", - "gi ri", - "kangae ru", - "yu e", - "a sa", - "o naji", - "yo ru", - "ni ku", - "osa ka", - "suko shi", - "c k", - "ta ma", - "kano jo", - "ki te", - "mon dai", - "a mari", - "e ki", - "ko jin", - "ha ya", - "i t", - "de te", - "atara shii", - "a wa", - "ga kkou", - "tsu zu", - "shu kan", - "i mashita", - "mi na", - "ata e", - "da rou", - "hatara ku", - "ga ta", - "da chi", - "ma tsu", - "ari masen", - "sei butsu", - "mi tsu", - "he ya", - "yasu i", - "d i", - "de ni", - "no ko", - "ha ha", - "do mo", - "ka mi", - "su deni", - "na o", - "ra ku", - "i ke", - "a ki", - "me ta", - "l o", - "ko domo", - "so shite", - "ga me", - "ba kari", - "to te", - "ha tsu", - "mi se", - "moku teki", - "da kara", - "s z", - "e l", - "g y", - "e n", - "t t", - "e m", - "a n", - "a k", - "e r", - "a z", - "a l", - "e t", - "o l", - "e g", - "e k", - "m i", - "o n", - "é s", - "c s", - "a t", - "á r", - "h o", - "e z", - "á l", - "i s", - "á n", - "o r", - "a r", - "e gy", - "e s", - "é r", - "á t", - "o tt", - "e tt", - "m eg", - "t a", - "o k", - "o s", - "ho gy", - "n em", - "é g", - "n y", - "k i", - "é l", - "h a", - "á s", - "ü l", - "i n", - "mi n", - "n a", - "e d", - "o m", - "i k", - "k ö", - "m a", - "n i", - "v a", - "v ol", - "é t", - "b b", - "f el", - "i g", - "l e", - "r a", - "é n", - "t e", - "d e", - "a d", - "ó l", - "b e", - "on d", - "j a", - "r e", - "u l", - "b en", - "n ek", - "u t", - "vol t", - "b an", - "ö r", - "o g", - "a p", - "o d", - "á g", - "n k", - "é k", - "v al", - "k or", - "a m", - "i l", - "í t", - "á k", - "b a", - "u d", - "sz er", - "min d", - "o z", - "é p", - "el l", - "ér t", - "m ond", - "i t", - "sz t", - "n ak", - "a mi", - "n e", - "ő l", - "cs ak", - "n é", - "ma g", - "ol y", - "m er", - "ál l", - "án y", - "ö n", - "ö l", - "min t", - "m ár", - "ö tt", - "na gy", - "é sz", - "az t", - "el ő", - "t ud", - "o t", - "é ny", - "á z", - "m ég", - "kö z", - "el y", - "s ég", - "en t", - "s em", - "ta m", - "h et", - "h al", - "f i", - "a s", - "v an", - "ho z", - "v e", - "u k", - "k ez", - "á m", - "v el", - "b er", - "a j", - "u nk", - "i z", - "va gy", - "m os", - "sz em", - "em ber", - "f og", - "mer t", - "ü k", - "l en", - "ö s", - "e j", - "t al", - "h at", - "t ak", - "h i", - "m ás", - "s ág", - "ett e", - "l eg", - "ü nk", - "h át", - "sz a", - "on y", - "ez t", - "mind en", - "en d", - "ül t", - "h an", - "j ó", - "k is", - "á j", - "in t", - "ú gy", - "i d", - "mos t", - "ar t", - "í r", - "k er", - "i tt", - "a tt", - "el t", - "mond ta", - "k ell", - "l á", - "ak i", - "ál t", - "ér d", - "t ö", - "l an", - "v ár", - "h ol", - "t el", - "l át", - "ő k", - "v et", - "s e", - "ut án", - "k ét", - "na p", - "í v", - "ál y", - "v ég", - "ö k", - "i r", - "d ul", - "v is", - "né z", - "t er", - "á ban", - "k ül", - "ak kor", - "k ap", - "sz él", - "y en", - "ú j", - "i m", - "oly an", - "es en", - "k ed", - "h ely", - "t ör", - "b ól", - "el m", - "r á", - "ár a", - "r ó", - "l ó", - "vol na", - "t an", - "le het", - "e bb", - "t en", - "t ek", - "s ok", - "k al", - "f or", - "u g", - "ol t", - "k a", - "ek et", - "b or", - "f ej", - "g ond", - "a g", - "ak ar", - "f él", - "ú l", - "b el", - "ott a", - "mi t", - "val ami", - "j el", - "é d", - "ar c", - "u r", - "hal l", - "t i", - "f öl", - "á ba", - "ol g", - "ki r", - "ol d", - "m ar", - "k érd", - "j ár", - "ú r", - "sz e", - "z s", - "él et", - "j át", - "o v", - "u s", - "é z", - "v il", - "v er", - "ő r", - "á d", - "ö g", - "le sz", - "on t", - "b iz", - "k oz", - "á bb", - "kir ály", - "es t", - "a b", - "en g", - "ig az", - "b ar", - "ha j", - "d i", - "o b", - "k od", - "r ól", - "v ez", - "tö bb", - "sz ó", - "é ben", - "ö t", - "ny i", - "t á", - "sz ól", - "gond ol", - "eg ész", - "í gy", - "ő s", - "o bb", - "os an", - "b ől", - "a bb", - "c i", - "ő t", - "n ál", - "k ép", - "azt án", - "v i", - "t art", - "be szél", - "m en", - "elő tt", - "a szt", - "ma j", - "kö r", - "han g", - "í z", - "in cs", - "a i", - "é v", - "ó d", - "ó k", - "hoz z", - "t em", - "ok at", - "an y", - "nagy on", - "h áz", - "p er", - "p ed", - "ez te", - "et len", - "nek i", - "maj d", - "sz ony", - "án ak", - "fel é", - "egy szer", - "j e", - "ad t", - "gy er", - "ami kor", - "f oly", - "sz ak", - "ő d", - "h ú", - "á sz", - "am ely", - "h ar", - "ér e", - "il yen", - "od a", - "j ák", - "t ár", - "á val", - "l ak", - "t ó", - "m ent", - "gy an", - "él y", - "ú t", - "v ar", - "kez d", - "m ell", - "mi kor", - "h ez", - "val ó", - "k o", - "m es", - "szer et", - "r end", - "l et", - "vis sza", - "ig en", - "f ő", - "va s", - "as szony", - "r ől", - "ped ig", - "p i", - "sz ép", - "t ák", - "ö v", - "an i", - "vil ág", - "p en", - "mag a", - "t et", - "sz ik", - "é j", - "én t", - "j ött", - "s an", - "sz í", - "i de", - "g at", - "ett em", - "ul t", - "h ány", - "ás t", - "a hol", - "ők et", - "h ár", - "k el", - "n ő", - "cs i", - "tal ál", - "el te", - "lá tt", - "tör t", - "ha gy", - "e sz", - "s en", - "n él", - "p ar", - "v ál", - "k ut", - "l ány", - "ami t", - "s ő", - "ell en", - "mag át", - "in k", - "u gyan", - "kül ön", - "a sz", - "mind ig", - "l ép", - "tal án", - "u n", - "sz or", - "k e", - "il lan", - "n incs", - "z et", - "vagy ok", - "tel en", - "is mer", - "s or", - "is ten", - "ít ott", - "j obb", - "v es", - "dul t", - "j uk", - "sz en", - "r o", - "ö m", - "l ett", - "k ar", - "egy ik", - "b ár", - "sz i", - "sz ív", - "az on", - "e szt", - "föl d", - "kut y", - "p illan", - "f ér", - "k om", - "t ől", - "t ű", - "é be", - "t ött", - "bar át", - "í g", - "a hogy", - "e h", - "e p", - "s o", - "v en", - "jel ent", - "t at", - "sz eg", - "mint ha", - "f al", - "egy en", - "mi l", - "sza b", - "r i", - "é m", - "biz ony", - "j on", - "ör eg", - "d olg", - "cs ap", - "ti szt", - "áll t", - "an cs", - "id ő", - "k at", - "ü gy", - "mi ért", - "ó t", - "ü r", - "cs in", - "h az", - "b et", - "én ek", - "v ér", - "j ól", - "al att", - "m ely", - "l o", - "sem mi", - "ny ug", - "v ág", - "kö vet", - "ös sze", - "ma d", - "l i", - "a cs", - "fi ú", - "kö n", - "más ik", - "j ön", - "sz ám", - "g er", - "s ó", - "r ész", - "k ér", - "z el", - "é vel", - "e o", - "e u", - "a n", - "eu l", - "eu n", - "eo n", - "a e", - "d a", - "a l", - "s s", - "i n", - "i l", - "a g", - "an g", - "y eon", - "y eo", - "d o", - "c h", - "n g", - "j i", - "h an", - "g a", - "g o", - "u i", - "h ae", - "a m", - "u l", - "u n", - "g eo", - "s i", - "n eun", - "ss da", - "s eo", - "eon g", - "y o", - "i da", - "t t", - "k k", - "j eo", - "d eul", - "w a", - "eu m", - "g e", - "o n", - "o g", - "s al", - "m an", - "yeon g", - "geo s", - "h ag", - "an eun", - "j a", - "g i", - "s u", - "i ss", - "o l", - "d ae", - "eo b", - "h a", - "j u", - "eo l", - "g eu", - "j eong", - "s ae", - "do e", - "g eul", - "s eu", - "s in", - "eul o", - "b n", - "s ang", - "bn ida", - "h al", - "b o", - "han eun", - "m al", - "i m", - "m o", - "b u", - "jeo g", - "sae ng", - "in eun", - "an h", - "m a", - "sal am", - "j o", - "s a", - "eo m", - "n ae", - "w i", - "l o", - "g wa", - "yeo l", - "n a", - "e seo", - "y e", - "m yeon", - "tt ae", - "h w", - "j e", - "eob s", - "j ang", - "g u", - "g w", - "il eul", - "yeo g", - "j eon", - "si g", - "j ag", - "j in", - "y u", - "o e", - "s e", - "hag o", - "d eun", - "y a", - "m un", - "s eong", - "g ag", - "h am", - "d ang", - "b a", - "l eul", - "s il", - "do ng", - "kk a", - "b al", - "da l", - "han da", - "eo ssda", - "ae g", - "l i", - "ha ji", - "s eon", - "o ng", - "hae ssda", - "d e", - "i ssda", - "e ge", - "b un", - "m ul", - "ju ng", - "ji g", - "m u", - "iss neun", - "b i", - "g eun", - "seu bnida", - "w on", - "p p", - "d aneun", - "eo h", - "d eo", - "ga m", - "j al", - "hae ng", - "ag o", - "y ang", - "b ul", - "b ang", - "u m", - "s o", - "h i", - "j ae", - "si m", - "saeng gag", - "hag e", - "s og", - "eo ss", - "d an", - "ja sin", - "j il", - "eo g", - "g yeong", - "doe n", - "go ng", - "m i", - "ch i", - "d eu", - "d eon", - "hae ss", - "d u", - "n am", - "eun g", - "jo h", - "n al", - "m yeong", - "w o", - "eon a", - "i go", - "g yeol", - "y ag", - "gw an", - "ul i", - "yo ng", - "n o", - "l yeo", - "j og", - "eoh ge", - "ga t", - "b og", - "mo s", - "t ong", - "ch a", - "man h", - "jeo l", - "geo l", - "h oe", - "ag a", - "n aneun", - "g an", - "un eun", - "ch eol", - "ch e", - "do l", - "b on", - "b an", - "ba d", - "ch u", - "ham yeon", - "yeo ssda", - "i bnida", - "g ye", - "eo s", - "hw al", - "salam deul", - "ji man", - "dang sin", - "ji b", - "ttae mun", - "m ae", - "i b", - "e neun", - "eu g", - "jeo m", - "geul eon", - "h wa", - "a ssda", - "b eob", - "bu t", - "b ae", - "yeo ss", - "ch in", - "ch aeg", - "g eon", - "g ae", - "nae ga", - "i ga", - "m og", - "sig an", - "g il", - "h yeon", - "l yeog", - "gu g", - "p yeon", - "s an", - "w ae", - "j ul", - "s eul", - "deun g", - "haji man", - "eum yeon", - "p il", - "m ol", - "n eu", - "a ss", - "n yeon", - "t ae", - "h u", - "p yo", - "s ul", - "g ang", - "j ineun", - "b eon", - "ha da", - "seo l", - "si p", - "dal eun", - "a p", - "sal m", - "g yo", - "ch eon", - "hag i", - "in a", - "cheol eom", - "g al", - "il a", - "kka ji", - "anh neun", - "ha bnida", - "tt eon", - "n u", - "hae seo", - "doen da", - "s ol", - "tt al", - "l a", - "il o", - "seu b", - "b yeon", - "m yeo", - "b eol", - "s on", - "n un", - "j un", - "j am", - "j eung", - "tt o", - "e n", - "mo m", - "h o", - "ch im", - "hw ang", - "eun eun", - "jo ng", - "bo da", - "n ol", - "n eom", - "but eo", - "jig eum", - "eobs da", - "dae lo", - "i g", - "y ul", - "p yeong", - "seon eun", - "sal ang", - "seu t", - "h im", - "n an", - "h eom", - "h yang", - "p i", - "gw ang", - "eobs neun", - "hw ag", - "ge ss", - "jag i", - "il eon", - "wi hae", - "dae han", - "ga ji", - "m eog", - "j yeo", - "cha j", - "b yeong", - "eo d", - "g yeo", - "do n", - "eo ji", - "g ul", - "mo deun", - "j on", - "in saeng", - "geul ae", - "h ang", - "sa sil", - "si b", - "ch al", - "il ago", - "doe l", - "g eum", - "doe neun", - "b ol", - "ga jang", - "geul igo", - "e l", - "h yeong", - "haeng bog", - "ch ul", - "h on", - "ch ae", - "s am", - "m ang", - "in da", - "da m", - "w ol", - "ch oe", - "d ul", - "si jag", - "ch eong", - "il aneun", - "ul ineun", - "ae n", - "kk e", - "mun je", - "a do", - "t eu", - "g un", - "geun eun", - "b ge", - "ch eo", - "b aeg", - "ju g", - "t a", - "sang dae", - "geu geos", - "do g", - "eu s", - "deu s", - "ja b", - "h yeo", - "tt eohge", - "u g", - "ma j", - "ch il", - "s wi", - "j ileul", - "ch ang", - "g aneun", - "m ag", - "i ji", - "da go", - "m in", - "yo han", - "t eug", - "pp un", - "al eul", - "haeng dong", - "p o", - "m il", - "ch am", - "se sang", - "e do", - "p an", - "man deul", - "am yeon", - "a b", - "kk ae", - "b ag", - "i deul", - "p um", - "m eol", - "s un", - "n eul", - "ham kke", - "chu ng", - "da b", - "yu g", - "s ag", - "gwang ye", - "il eohge", - "bal o", - "neun de", - "ham yeo", - "go s", - "geul eoh", - "an ila", - "bang beob", - "da si", - "b yeol", - "g yeon", - "gam jeong", - "on eul", - "j aneun", - "yeo m", - "l ago", - "i gi", - "hw an", - "t eul", - "eo seo", - "si k", - "ch o", - "jag a", - "geul eom", - "geul eona", - "jeong do", - "g yeog", - "geul eohge", - "geu deul", - "eu t", - "im yeon", - "j jae", - "k eun", - "i sang", - "mal haessda", - "eu ge", - "no p", - "in gan", - "bo myeon", - "t aeg", - "seu s", - "d wi", - "s aneun", - "w an", - "anh go", - "t an", - "nu gu", - "su ng", - "da myeon", - "a deul", - "p eul", - "ttal a", - "d i", - "geos do", - "a ji", - "m eon", - "eum yeo", - "dol og", - "neun g", - "mo du" - ] - } -} \ No newline at end of file diff --git a/modules/xttscli.py b/modules/xttscli.py deleted file mode 100644 index 03df733..0000000 --- a/modules/xttscli.py +++ /dev/null @@ -1,219 +0,0 @@ -import sys -import os -import argparse -import logging -import traceback -import torch -import torchaudio -import tempfile -from pathlib import Path -from TTS.tts.configs.xtts_config import XttsConfig -from TTS.tts.models.xtts import Xtts - -XTTS_MODEL = None - -def prepare_speaker_audio(speaker): - """ - Prepares the speaker audio file and returns the speaker file path. - For .wav and .mp3 files, it directly returns the file path for model processing. - """ - if speaker.endswith(".wav") or speaker.endswith(".mp3"): - print(f"Using speaker audio file: {speaker}") - return speaker, None # No precomputed speaker embedding - elif speaker.endswith(".pth"): - print(f"Loading preprocessed speaker embedding from .pth file: {speaker}") - speaker_embedding = torch.load(speaker) - return None, speaker_embedding # Return precomputed speaker embedding - else: - raise ValueError("Unsupported speaker file format. Please provide a .wav, .mp3, or .pth file.") - - -def run_tts(lang, tts_text, speaker_audio_file, speaker_embedding, device): - """ - Generates TTS using the XTTS model, either from speaker audio or precomputed speaker embedding. - """ - if XTTS_MODEL is None: - raise ValueError("You need to load the model.") - - # Generate conditioning latents from speaker audio if provided - if speaker_audio_file is not None: - print(f"Using speaker audio file: {speaker_audio_file}") - gpt_cond_latent, speaker_embedding = XTTS_MODEL.get_conditioning_latents( - audio_path=speaker_audio_file, - gpt_cond_len=XTTS_MODEL.config.gpt_cond_len, - max_ref_length=XTTS_MODEL.config.max_ref_len, - sound_norm_refs=XTTS_MODEL.config.sound_norm_refs - ) - elif speaker_embedding is not None: - raise ValueError("Precomputed speaker embeddings are not directly usable without gpt_cond_latent.") - - # Perform inference - out = XTTS_MODEL.inference( - text=tts_text, - language=lang, - gpt_cond_latent=gpt_cond_latent, - speaker_embedding=speaker_embedding, - temperature=XTTS_MODEL.config.temperature, - length_penalty=XTTS_MODEL.config.length_penalty, - repetition_penalty=XTTS_MODEL.config.repetition_penalty, - top_k=XTTS_MODEL.config.top_k, - top_p=XTTS_MODEL.config.top_p, - enable_text_splitting=True - ) - - # Move the output tensor to CPU before saving - out["wav"] = torch.tensor(out["wav"]).unsqueeze(0).to("cpu") # Ensure tensor is on CPU - - # Save the generated speech to a temporary file - with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as fp: - out_path = fp.name - torchaudio.save(out_path, out["wav"], 24000) # Save the tensor to disk - - return out_path - - -def clear_gpu_cache(): - if torch.cuda.is_available(): - torch.cuda.empty_cache() - - -def load_model(xtts_checkpoint, xtts_config, xtts_vocab, device): - """ - Loads the XTTS model from checkpoint, config, and vocab files. - """ - global XTTS_MODEL - clear_gpu_cache() - - if not xtts_checkpoint or not xtts_config or not xtts_vocab: - raise ValueError("You need to set the XTTS checkpoint path, XTTS config path, and XTTS vocab path!") - - config = XttsConfig() - config.load_json(xtts_config) - - XTTS_MODEL = Xtts.init_from_config(config) - print("Loading XTTS model!") - - XTTS_MODEL.load_checkpoint(config, checkpoint_path=xtts_checkpoint, vocab_path=xtts_vocab, use_deepspeed=False) - - if "cuda" in device and torch.cuda.is_available(): - XTTS_MODEL.cuda(device) - print(f"Model moved to GPU: {device}") - else: - print(f"Using device: {device}") - - -def main(): - # Set up logging - log_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'xtts_cli.log') - logging.basicConfig(filename=log_file, level=logging.ERROR, format='%(asctime)s %(levelname)s %(message)s') - - # Default paths for the config, checkpoint, and vocab - script_dir = os.path.dirname(os.path.abspath(__file__)) - default_config_path = os.path.join(script_dir, 'config.json') - default_checkpoint_path = os.path.join(script_dir, 'model.pth') - default_vocab_path = os.path.join(script_dir, 'vocab.json') - default_speaker_path = os.path.join(script_dir, 'speaker.mp3') # Default speaker audio file - default_test_file_path = os.path.join(script_dir, 'test.speech') # Default text file for --test - - parser = argparse.ArgumentParser(description="CLI for XTTS model loading and TTS generation.") - parser.add_argument('--checkpoint', default=default_checkpoint_path, - help=f"Path to XTTS checkpoint file. Default: {default_checkpoint_path}") - parser.add_argument('--config', default=default_config_path, - help=f"Path to XTTS config file. Default: {default_config_path}") - parser.add_argument('--vocab', default=default_vocab_path, - help=f"Path to XTTS vocab file. Default: {default_vocab_path}") - parser.add_argument('--speaker', default=default_speaker_path, - help=f"Path to speaker audio file (raw .wav, .mp3, or preprocessed .pth). Default: {default_speaker_path}") - parser.add_argument('--text', help="Text to synthesize.") - parser.add_argument('--file', help="File containing text to synthesize (one text per line).") - parser.add_argument('--lang', default='en', help="Language for the TTS.") - parser.add_argument('--folder', help="Path to save the output TTS file.") - parser.add_argument('--device', default='cuda:0', help="Device to run the model on. Default: cuda:0") - parser.add_argument('--test', action='store_true', help="Run a test TTS using the test.speech file.") - - args = parser.parse_args() - - try: - # Preprocess the speaker file if it's a .wav or .mp3 file - args.speaker, speaker_embedding = prepare_speaker_audio(args.speaker) - - # Handle test case - if args.test: - print("Running test with default parameters...") - if not os.path.exists(default_test_file_path): - raise FileNotFoundError(f"The test file '{default_test_file_path}' does not exist. Please create it.") - - args.file = default_test_file_path - args.folder = os.path.join(script_dir, 'test_output.wav') - print(f"Test file: {args.file}") - print(f"Output will be saved to: {args.folder}") - - # Handle the --file argument - if args.file: - print(f"Reading text from file: {args.file}") - with open(args.file, 'r') as f: - args.text = f.read().splitlines() - - elif not args.text: - raise ValueError("Text or a file with text must be provided unless running in test mode.") - - # Load the XTTS model - load_model(args.checkpoint, args.config, args.vocab, args.device) - - # Prepare to store generated files - all_output_files = [] - - # Process multiple lines of text (if from file) - if isinstance(args.text, list): - for i, tts_text in enumerate(args.text): - output_path = run_tts( - lang=args.lang, - tts_text=tts_text, - speaker_audio_file=args.speaker, - speaker_embedding=speaker_embedding, - device=args.device - ) - - output_file = f"{args.folder}_{i}.wav" if args.folder else f'./output_{i}.wav' - try: - os.rename(output_path, output_file) - except Exception as e: - import shutil - # try copy then delete - shutil.copy(output_path, output_file) - os.remove(output_path) - - all_output_files.append(output_file) - - else: - # Generate TTS for a single text input - output_path = run_tts( - lang=args.lang, - tts_text=args.text, - speaker_audio_file=args.speaker, - speaker_embedding=speaker_embedding, - device=args.device - ) - - output_file = args.folder if args.folder else './output.wav' - try: - os.rename(output_path, output_file) - except Exception as e: - import shutil - # try copy then delete - shutil.copy(output_path, output_file) - os.remove(output_path) - - all_output_files.append(output_file) - - # Final output format to match expected behavior - main_output_file = all_output_files[0] - print(f"Generated files: ({all_output_files})") - - except Exception as e: - logging.error("An error occurred: %s", traceback.format_exc()) - print(f"An error occurred. Check the log file at {log_file} for details.") - - -if __name__ == '__main__': - main() diff --git a/modules/xttscli_old.py b/modules/xttscli_old.py deleted file mode 100644 index 75771ac..0000000 --- a/modules/xttscli_old.py +++ /dev/null @@ -1,219 +0,0 @@ -import argparse -import torch.multiprocessing as mp -from concurrent.futures import ProcessPoolExecutor, as_completed -import unittest -from unittest.mock import patch, MagicMock - - -def generate_xtts_for_text(text, file, lang, speaker): - from TTS.api import TTS - import torch - tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to("cuda") - tts.tts_to_file( - text=text, - speaker_wav=speaker, - language=lang, - file_path=file, - ) - - -def generate_xtts(**kwargs): - import torch - import ast - import uuid - import os - from concurrent.futures import ProcessPoolExecutor, as_completed - - torch.cuda.empty_cache() - - text = kwargs.get('text') - folder = kwargs.get('folder', '.') - folder = os.path.abspath(folder) - lang = kwargs.get('lang', 'en') - speaker = kwargs.get('speaker', '') - - if isinstance(text, str): - if text != None and text.startswith("[") and text.endswith("]"): - try: - texts = ast.literal_eval(text) - except SyntaxError: - texts = [text] - elif isinstance(text, list): - texts = text - - o_files = [] - batch_size = 4 - with ProcessPoolExecutor(max_workers=batch_size) as executor: - futures = [] - for i, text in enumerate(texts): - if len(text) > 1: - ii = str(i).zfill(1 + len(str(1 + len(texts)))) - file_to_use = f"{folder}/tts_{ii}_{str(uuid.uuid4())}.wav" - future = executor.submit(generate_xtts_for_text, text, file_to_use, lang, speaker) - futures.append((future, file_to_use)) - - for future, file_to_use in futures: - future.result() - o_files.append(file_to_use) - - torch.cuda.empty_cache() - - return (o_files[0], o_files,) - - -def cache_parameters(cache_file, params): - """Cache the input parameters to a JSON file.""" - import json - with open(cache_file, 'w') as f: - json.dump(params, f) - - -def load_cached_parameters(cache_file): - """Load the cached parameters from the JSON file.""" - import json - import os - if os.path.exists(cache_file): - with open(cache_file, 'r') as f: - return json.load(f) - return None - - -def run_test(): - """Run unit tests for the module.""" - unittest.main(argv=[''], exit=False) - - -class TestGenerateXTTS(unittest.TestCase): - """Unit tests for generate_xtts functions.""" - - @patch('os.path.exists', return_value=True) - @patch('TTS.api.TTS.tts_to_file') - def test_generate_xtts_single_text(self, mock_tts_to_file, mock_exists): - """Test generating xtts with a single text input.""" - mock_tts_to_file.return_value = None - kwargs = { - 'text': 'Hello, this is a test.', - 'folder': './output', - 'lang': 'en', - 'speaker': '/path/to/speaker.wav' - } - - result = generate_xtts(**kwargs) - - # Assertions - self.assertEqual(len(result[1]), 1) - mock_tts_to_file.assert_called_once() - - @patch('os.path.exists', return_value=True) - @patch('TTS.api.TTS.tts_to_file') - def test_generate_xtts_multiple_texts(self, mock_tts_to_file, mock_exists): - """Test generating xtts with multiple texts.""" - mock_tts_to_file.return_value = None - kwargs = { - 'text': ['Hello, this is a test.', 'This is another test.'], - 'folder': './output', - 'lang': 'en', - 'speaker': '/path/to/speaker.wav' - } - - result = generate_xtts(**kwargs) - - # Assertions - self.assertEqual(len(result[1]), 2) - self.assertEqual(mock_tts_to_file.call_count, 2) - - def test_cache_parameters(self): - """Test caching parameters to a file.""" - import os - import json - test_cache_file = './test_cache.json' - test_params = {'text': 'sample text', 'lang': 'en'} - - cache_parameters(test_cache_file, test_params) - - # Verify file content - with open(test_cache_file, 'r') as f: - data = json.load(f) - self.assertEqual(data, test_params) - - # Clean up - os.remove(test_cache_file) - - def test_load_cached_parameters(self): - """Test loading cached parameters from a file.""" - import os - import json - test_cache_file = './test_cache.json' - test_params = {'text': 'sample text', 'lang': 'en'} - - # Write test data to the file - with open(test_cache_file, 'w') as f: - json.dump(test_params, f) - - # Load the parameters - loaded_params = load_cached_parameters(test_cache_file) - self.assertEqual(loaded_params, test_params) - - # Clean up - os.remove(test_cache_file) - - -def main(): - import sys - import os - import argparse - import logging - import traceback - - log_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'xttscli.log') - logging.basicConfig(filename=log_file, level=logging.ERROR, - format='%(asctime)s %(levelname)s %(message)s') - - cache_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'params_cache.json') - - try: - parser = argparse.ArgumentParser(description='Generate from given text.') - parser.add_argument('--text', help='Text to generate for') - parser.add_argument('--folder', type=str, default='.', help='Output folder') - parser.add_argument('--lang', type=str, default='en') - parser.add_argument('--speaker', type=str, default='', help='Speaker wav to use for generation') - parser.add_argument('--file', type=str, default="", help='Newline separated file.') - parser.add_argument('--rerun', action='store_true', help='Rerun with the last cached parameters.') - parser.add_argument('--test', action='store_true', help='Run the test.') - - args = parser.parse_args() - kwargs = vars(args) - - if args.test: - run_test() - sys.exit(0) - - if args.rerun: - cached_params = load_cached_parameters(cache_file) - if cached_params: - print(f"Rerunning with cached parameters: {cached_params}") - kwargs.update(cached_params) - else: - print("No cached parameters found. Exiting.") - sys.exit(1) - else: - cache_parameters(cache_file, kwargs) - - if kwargs['file']: - with open(kwargs['file']) as f: - kwargs['text'] = f.read().split('\n') - - if kwargs['speaker']: - if not os.path.exists(kwargs['speaker']): - raise ValueError(f"Speaker file {kwargs['speaker']} does not exist.") - - generated_files = generate_xtts(**kwargs) - print(f"Generated files: {generated_files}") - - except Exception as e: - logging.error("An error occurred: %s", traceback.format_exc()) - print(f"An error occurred. Check the log file at {log_file} for details.") - - -if __name__ == '__main__': - main() diff --git a/modules/youtube.py b/modules/youtube.py deleted file mode 100644 index f8b902d..0000000 --- a/modules/youtube.py +++ /dev/null @@ -1,184 +0,0 @@ -import httplib2 -import os -import random -import sys -import time - -from apiclient.discovery import build -from apiclient.errors import HttpError -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. -httplib2.RETRIES = 1 - -# Maximum number of times to retry before giving up. -MAX_RETRIES = 10 - -# Always retry when these exceptions are raised. -RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError) - -# Always retry when an apiclient.errors.HttpError with one of these status -# codes is raised. -RETRIABLE_STATUS_CODES = [500, 502, 503, 504] - -try: - import comfy -except ModuleNotFoundError: - print("comfy not found, using fallback") - comfy = lambda: None - comfy.__path__ = lambda: None - comfy.__path__._path = ["."] - -comfy_dir = comfy.__path__._path[0] -base_dir = os.path.dirname(comfy_dir) - -# this_dir -this_file_path = os.path.dirname(os.path.abspath(__file__)) - -CLIENT_SECRETS_FILE = os.path.join(base_dir, "google-credentials.json") -CLIENT_SECRETS_FILE = os.path.join(this_file_path, "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" -YOUTUBE_API_SERVICE_NAME = "youtube" -YOUTUBE_API_VERSION = "v3" - -# This variable defines a message to display if the CLIENT_SECRETS_FILE is -# missing. -MISSING_CLIENT_SECRETS_MESSAGE = """ -WARNING: Please configure OAuth 2.0 - -To make this sample run you will need to populate the client_secrets.json file -found at: - - %s - -with information from the API Console -https://console.cloud.google.com/ - -For more information about the client_secrets.json file format, please visit: -https://developers.google.com/api-client-library/python/guide/aaa_client_secrets -""" % os.path.abspath(os.path.join(os.path.dirname(__file__), - CLIENT_SECRETS_FILE)) - -VALID_PRIVACY_STATUSES = ("public", "private", "unlisted") - - -def get_authenticated_service(args): - flow = flow_from_clientsecrets(CLIENT_SECRETS_FILE, - scope=YOUTUBE_UPLOAD_SCOPE, - message=MISSING_CLIENT_SECRETS_MESSAGE) - - storage = Storage("%s-oauth2.json" % sys.argv[0]) - credentials = storage.get() - - if credentials is None or credentials.invalid: - credentials = run_flow(flow, storage, args) - - return build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, - http=credentials.authorize(httplib2.Http())) - - -def initialize_upload(youtube_api, options): - tags = None - if options.keywords: - tags = options.keywords.split(",") - - body = dict( - snippet=dict( - title=options.title, - description=options.description, - tags=tags, - categoryId=options.category - ), - status=dict( - privacyStatus=options.privacyStatus - ) - ) - - # Call the API's videos.insert method to create and upload the video. - insert_request = youtube_api.videos().insert( - part=",".join(body.keys()), - body=body, - media_body=MediaFileUpload(options.file, chunksize=-1, resumable=True) - ) - - video_id = resumable_upload(insert_request) - return video_id - - -def resumable_upload(insert_request): - response = None - error = None - retry = 0 - while response is None: - try: - print("Uploading file...") - status, response = insert_request.next_chunk() - if response is not None: - if 'id' in response: - 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) - else: - raise - except RETRIABLE_EXCEPTIONS as e: - error = "A retriable error occurred: %s" % e - - if error is not None: - print(error) - retry += 1 - if retry > MAX_RETRIES: - exit("No longer attempting to retry.") - - max_sleep = 2 ** retry - sleep_seconds = random.random() * max_sleep - print("Sleeping %f seconds and then retrying..." % sleep_seconds) - 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("--title", help="Video title", default="Test Title") - 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="") - 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): - exit("Please specify a valid file using the --file= parameter.") - - youtube = get_authenticated_service(args) - try: - 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)) diff --git a/noise_nodes.py b/noise_nodes.py deleted file mode 100644 index 1899f6e..0000000 --- a/noise_nodes.py +++ /dev/null @@ -1,95 +0,0 @@ -import torch -import folder_paths -import torchvision -import torch.nn as nn -import functools -import torch - -NODE_CLASS_MAPPINGS = {} -NODE_DISPLAY_NAME_MAPPINGS = {} - - -def ETK_noise_base(cls): - cls.CATEGORY = "ETK/noise" - # Add spaces to the camel case class name - pretty_name = cls.__name__ - for i in range(1, len(pretty_name)): - if pretty_name[i].isupper(): - pretty_name = pretty_name[:i] + " " + pretty_name[i:] - cls.DISPLAY_NAME = pretty_name - NODE_CLASS_MAPPINGS[cls.__name__] = cls - NODE_DISPLAY_NAME_MAPPINGS[cls.DISPLAY_NAME] = pretty_name - - # Wrap the function defined in the FUNCTION attribute - func_name = getattr(cls, "FUNCTION", None) - if func_name and hasattr(cls, func_name): - original_func = getattr(cls, func_name) - - @functools.wraps(original_func) - def wrapped_func(*args, **kwargs): - with torch.inference_mode(False): - return original_func(*args, **kwargs) - - setattr(cls, func_name, wrapped_func) - - return cls - - -@ETK_noise_base -class PerlinFractalImage: - """ - use noise.perlin_fractal_image to generate a torch tensor (b,h,w,c=3) - """ - - # noise = torch.cat((noise, noise, noise), dim=3) - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "width": ("INT", {"default": 256}), - "height": ("INT", {"default": 256}), - "scale": ("FLOAT", {"default": 0.01}), - "octaves": ("INT", {"default": 6}), - "lacunarity": ("FLOAT", {"default": 3}), - "gain": ("FLOAT", {"default": 0.5}), - "seed": ("INT", {"default": 1}), - "offset": ("FLOAT", {"default": 0}), - "workers": ("INT", {"default": 4}), - "norm_zero_to_1": ([True, False], {"default": True}), - - } - } - - # Define the return types - RETURN_TYPES = ("IMAGE", "IMAGE", "MASK",) - RETURN_NAMES = ("Image", "Grayscale Image", "Mask",) - FUNCTION = "handler" - - # Method to download the dataset - def handler(self, **kwargs): - from .modules import noise - width = kwargs["width"] - height = kwargs["height"] - scale = kwargs["scale"] - octaves = kwargs["octaves"] - lacunarity = kwargs["lacunarity"] - gain = kwargs["gain"] - seed = kwargs["seed"] - offset = kwargs["offset"] - workers = kwargs["workers"] - norm_zero_to_1 = kwargs["norm_zero_to_1"] - - noise = noise.get_perlin_2d_as_torch_image_stack(width, height, scale, octaves, lacunarity, gain, seed, offset, - workers, norm_zero_to_1) - - noise_image = torch.cat((noise, noise, noise), dim=3) - - # format for this according to comfyui is unknown atm - noise_grayscale = noise - - # format for this according to comfyui is unknown atm - noise_mask = noise_grayscale > torch.median(noise_grayscale) - noise_mask = noise_mask.float() - - return (noise_image, noise_grayscale, noise_mask,) diff --git a/server_endpoints.py b/server_endpoints.py deleted file mode 100644 index 7dbae09..0000000 --- a/server_endpoints.py +++ /dev/null @@ -1,687 +0,0 @@ -from .config import config_settings -from .utils import FileBackedDict -import json -TEST= False -try: - from server import PromptServer -except ImportError: - TEST=True - # for now just exit - -if not TEST: - - from aiohttp import web - from importlib import reload - import logging - import os - from folder_paths import output_directory, input_directory, temp_directory - # from custom_nodes.EternalKernelLiteGraphNodes.local_shared import GDE_PATH, ETK_PATH - from .local_shared import GDE_PATH, ETK_PATH - from .config import config_settings - - fake_git = None - - - # real_git_url = "https://github.com/story-squad/GDE_Graph_IO.git" # ssh url - # real_git_url = "https://github.com/Hopping-Mad-Games/COTRF.git" # ssh url - - - class GitIO: - def __init__(self, i_real_git_url: str, i_gde_path: str): - self.gde_path = i_gde_path - self.real_git_io_path = self.setup_git_io(i_real_git_url) - - def setup_git_io(self, l_real_git_url: str): - git_name = l_real_git_url.split("/")[-1].split(".")[0] - - real_git_path = os.path.join(self.gde_path, "data", "gitio", git_name) - - # check to see if it exists and is a git repo, if not then clone it - if os.path.exists(real_git_path): - # check to see if it is a git repo - if os.path.exists(os.path.join(real_git_path, ".git")): - print("git io found") - else: - # not a git repo, delete it and clone it - print("git io error, not a git repo") - else: - # clone it - print("git io not found") - print("cloning git io repo") - os.system("git clone " + l_real_git_url + " " + real_git_path) - - return real_git_path - - def ensure_git_state(self, user: str): - import subprocess - """works in windows or linux, ensures that the git repo is in the correct state""" - if not os.path.exists(self.real_git_io_path): - raise Exception("git io path not found") - if not os.path.exists(os.path.join(self.real_git_io_path, ".git")): - raise Exception("git io error, not a git repo") - - def run_git_command(command: str): - """Helper function to run a git command in the correct directory""" - return subprocess.check_output(command, cwd=self.real_git_io_path, text=True).strip() - - # fetch first? - run_git_command(["git", "fetch"]) - - # get the current branch - current_branch = run_git_command(["git", "branch", "--show-current"]) - - ################## - - try: - origin_branch_exists = not run_git_command(["git", "rev-parse", "--verify", "--quiet", "origin/" + user]) - origin_branch_exists = origin_branch_exists != "" - except subprocess.CalledProcessError: - origin_branch_exists = False - - try: - local_branch_exists = run_git_command(["git", "rev-parse", "--verify", "--quiet", user]) - local_branch_exists = local_branch_exists != "" - except subprocess.CalledProcessError: - local_branch_exists = False - - if not origin_branch_exists and not local_branch_exists: - print(f"git branch {user} does not exist locally or on the remote") - # the branch does not exist locally or on the remote so create it here first - # checkout new_user - run_git_command(["git", "checkout", "new_user"]) - # create new branch - run_git_command(["git", "branch", user]) # creates and checks out new branch - # git checkout feature-branch - run_git_command(["git", "checkout", user]) - # set the upstream to the remote - # run_git_command(["git", "branch", "--set-upstream-to", "origin/" + user]) - # push the branch to the remote - run_git_command(["git", "push", "-u", "origin", user]) - # fetch and pull it back down for good measure - run_git_command(["git", "fetch"]) - run_git_command(["git", "pull"]) - - elif not origin_branch_exists and local_branch_exists: - print(f"git branch {user} exists locally but not on the remote") - # the branch does not exist on the remote but does locally - # set the upstream - # run_git_command(["git", "branch", "--set-upstream-to", "origin/" + user]) - # push the branch to the remote - run_git_command(["git", "push", "-u", "origin", user]) - # fetch and pull it back down for good measure - run_git_command(["git", "fetch"]) - run_git_command(["git", "pull"]) - - elif origin_branch_exists and not local_branch_exists: - print(f"git branch {user} exists on the remote but not locally") - # the branch does not exist locally but does on the remote - # fetch and pull it down - run_git_command(["git", "checkout", user]) - # pull - run_git_command(["git", "pull"]) - - elif origin_branch_exists and local_branch_exists: - print(f"git branch {user} exists locally and on the remote") - # the branch exists locally and on the remote - # pull it down - run_git_command(["git", "checkout", user]) - # set the upstream to the remote - run_git_command(["git", "branch", "--set-upstream-to", "origin/" + user]) - - current_branch = run_git_command(["git", "branch", "--show-current"]) - # if the current branch is not the user's branch, then switch to it - if current_branch != user: - print(f"git branch {user} is not the current branch, switching to it") - # also check if the branch exists, if not then create it - branches = run_git_command(["git", "branch"]) - if user not in branches: - run_git_command(["git", "branch", user]) - # be verbose about what exactly is happening where - print(f"git branch {user} does not exist locally, creating it, and pushing it to the remote") - run_git_command(["git", "push", "--set-upstream", "origin", user]) - # switch to the user's branch - run_git_command(["git", "checkout", user]) - - # pull the latest from the user's branch - run_git_command(["git", "pull"]) - - def git_save_version_1(self, user: str, file_name: str, file_data: str): - """ - for git save version 1 each user will have their own branch, - and the data will be saved to a file in the user's branch - version > 1 may include other strategies - """ - self.ensure_git_state(user) - # change to correct directory - os.chdir(self.real_git_io_path) - # switch to the users branch - os.system("git checkout " + user) - # save the file data as the file to the disk - with open(os.path.join(self.real_git_io_path, file_name), "w", encoding="utf-8") as f: - f.write(file_data) - # add the file to the git repo - os.system("git add " + file_name) - # commit the file to the git repo - os.system("git commit -m \"saving file " + file_name + "\"") - # push the file to the git repo - os.system("git push") - - def git_load_version_1(self, user_name, file_name): - """ - for git load version 1 each user will have their own branch, - and the data will be loaded from a file in the user's branch - version > 1 may include other strategies - """ - self.ensure_git_state(user_name) - # change to correct directory - os.chdir(self.real_git_io_path) - # switch to the users branch - os.system("git checkout " + user_name) - # pull the latest from the user's branch - os.system("git pull") - # read the file from the disk - with open(os.path.join(self.real_git_io_path, file_name), "r", encoding="utf-8") as f: - file_data = f.read() - return file_data - - - git_io = None - - user_data_base = { - "tasha_StSq": { - "password": "[REDACTED]", - "security": None, - "client_id": None, - "git_io": "StSq" - }, - "tasha_COTRF": { - "password": "[REDACTED]", - "security": None, - "client_id": None, - "git_io": "COTRF" - }, - "HMG": { - "password": "[REDACTED]", - "security": None, - "client_id": None, - "git_io": "HMG" - }, - "API": { - "password": "[REDACTED]", - "security": None, - "client_id": None, - "git_io": "HMG" - }, - } - tmp_dir = config_settings["tmp_dir"] - user_data_file = os.path.join(tmp_dir, 'user_data_tmp.json') - user_data = FileBackedDict(user_data_file) - - if len(user_data.keys()) == 0: - user_data.update(user_data_base) - - - def get_user_by_client_id(client_id): - for user in user_data: - if user_data[user]["client_id"] == client_id: - return user - return None - - - async def get_user_if_client_id_logged_in(client_id): - try: - user = get_user_by_client_id(client_id) - except KeyError: - user = None - return user - - - def get_user_by_security(security): - for user in user_data: - if user_data[user]["security"] == security: - return user - return None - - - @PromptServer.instance.routes.post("/gde/logout") - async def logout(request): - """ - This function is called when the user logs out - """ - print("logout request") - - # get the user and pass from the json - data = await request.json() - client_id = data["client_id"] - - # find the user with the client id - user = get_user_by_client_id(client_id) - - if user is None: - # use json to send the data back - return web.json_response({"error": "invalid client_id"}, status=401) - - # clear the security and client id - user_data[user]["security"] = None - user_data[user]["client_id"] = None - global git_io - git_io = None - - # use json to send the data back - return web.json_response({"success": True}) - - - @PromptServer.instance.routes.post("/gde/login") - async def login(request): - """ - This function is called when the attemnpts to log in: - - validates login - - sets up security - - user and pass are in the json - """ - - print("login request") - - # get the user and pass from the json - data = await request.json() - try: - client_id = data["client_id"] - has_client_id = True - except: - # this means the client id is not set yet so return logged_in False - # use json to send the data back - has_client_id = False - return web.json_response({"logged_in": False}) - - try: - user = data["username"] - password = data["password"] - has_user_and_pass = True - except: - has_user_and_pass = False - # this means the client is just checking to see if it is already logged into the server - # so check - user = get_user_by_client_id(client_id) - if user is None: - print("user is none") - return web.json_response({"logged_in": False}, status=401) - else: - # send the client back its user so it can handle api calls correctly - print("user recconected " + user) - return web.json_response({"logged_in": True, "user": user}, status=200) - - # validate the user and pass - # for now the users and passes are right here in the code - - if user not in user_data: - # use json to send the data back - return web.json_response({"error": "invalid user"}, status=401) - - if password != user_data[user]["password"]: - # use json to send the data back - return web.json_response({"error": "invalid password"}, status=401) - - # if the user and pass are valid, then set up security - # for now the security is just a random string that will be sent back to the user - # in the future this will be a token that will be used to authenticate the user - import uuid - security = str(uuid.uuid4()) - - # there is no middleware installed - # so we will just store the security in memory - # this is not secure, but it is good enough for now - # store the security in memory - user_data[user]["security"] = security - user_data[user]["client_id"] = client_id - - global git_io - - # from . import config - # config_settings = config.config_settings - - users_gitio = user_data[user]["git_io"] - - # users_gitio_path = os.path.abspath(os.path.join( - # GDE_PATH, - # config_settings["gitio"]["base"], - # users_gitio) - # ) - - try: - users_gittio_url = config_settings["gitio"][users_gitio] - except KeyError: - return web.json_response({"error": "invalid gitio"}, status=401) - - git_io = GitIO(users_gittio_url, GDE_PATH) - - # return json with the security - return web.json_response({"security": security}) - - - def hijack_prompt_server(route_path, - before_get_func: callable = None, - after_get_func: callable = None, - before_post_func: callable = None, - after_post_func: callable = None, - ): - """ - This function is called by the GDE when it starts up, it hijacks the prompt server - for added security. - """ - from server import PromptServer - - # find the existing /prompt route - rts = [i for i, s in enumerate(PromptServer.instance.routes) if (route_path == s.path)] - print(f"found {len(rts)} routes to remove: {rts}") - - # remove the existing prompt route - for i, rt_num in enumerate(rts): - rt_to_pop = rt_num - i - route = PromptServer.instance.routes._items.pop(rt_to_pop) - - # route = PromptServer.instance.routes._items[rt_to_pop] - # myr = web.RouteTableDef() - - method = route.method - path = route.path - kwargs = route.kwargs - - if method == "GET": - print(f"Hijacking route {method} {path} {kwargs}") - get_handler = route.handler - if not before_get_func: - if not after_get_func: - raise NotImplementedError - - if before_get_func and after_get_func: - raise NotImplementedError - - if before_get_func: - @PromptServer.instance.routes.get(path) - async def get_prompt(request): - part1 = await before_get_func(request) - if isinstance(part1, web.Response): - return part1 - if isinstance(part1, web.Request): - modified_func_response = await get_handler(part1) - return modified_func_response - elif after_get_func: - @PromptServer.instance.routes.get(path) - async def get_prompt(request): - normal_handler_response = await get_handler(request) - modified_func_response = await after_get_func(normal_handler_response) - return modified_func_response - - elif method == "POST": - print(f"Hijacking route {method} {path} {kwargs}") - post_handler = route.handler - if not before_post_func: - if not after_post_func: - raise NotImplementedError - - if before_post_func and after_post_func: - raise NotImplementedError - - if before_post_func: - @PromptServer.instance.routes.post(path) - async def post_prompt(request): - part1 = await before_post_func(request) - if isinstance(part1, web.Response): - return part1 - if isinstance(part1, web.Request): - # modified_func_response = await post_handler(part1) - # if it has a json() method then it is a request, careful it might not have the attribute at all - if hasattr(part1, "json"): - modified_func_response = await post_handler(part1) - if isinstance(part1, dict): - a = await post_handler(part1) - b = part1 - return a - return modified_func_response - elif after_post_func: - @PromptServer.instance.routes.post(path) - async def post_prompt(request): - normal_handler_response = await post_handler(request) - modified_func_response = await after_post_func(normal_handler_response) - return modified_func_response - - - from aiohttp import web - - - @PromptServer.instance.routes.post("/gde/git/user_graphs") - async def get_user_graphs(request): - """ - Returns a list of available user graphs (dropdown options). - """ - import os - import subprocess - - if git_io == None: - return web.json_response({"error": "git_io not set"}, status=401) - - def run_git_command(command: list, dir_path: str): - """Helper function to run a git command in the specified directory""" - return subprocess.call(command, cwd=dir_path) - - data = await request.json() - client_id = data.get("client_id", None) - user = data.get("user", None) - if user not in user_data: - user = get_user_by_client_id(client_id) - - if await get_user_if_client_id_logged_in(client_id) is None: - return web.json_response({"error": "user not found"}, status=401) - - # user = data.get("user", None) - print("getting user graphs for user:", user) - - # Directory where the user graphs are stored - user_graphs_dir = git_io.real_git_io_path - - # Ensure that the git repo is in the correct state - git_io.ensure_git_state(user) - - run_git_command(["git", "pull", "origin", user], user_graphs_dir) - import os - - user_graph_files = [ - f for f in os.listdir(user_graphs_dir) - if os.path.isfile(os.path.join(user_graphs_dir, f)) and f.endswith(".json") - ] - - # Sort files by modification time, newest first - user_graph_files.sort( - key=lambda x: os.path.getmtime(os.path.join(user_graphs_dir, x)), - reverse=True - ) - - options_data = [{"value": f, "label": f} for f in user_graph_files if f != ""] - # options_data = [] - - return web.json_response(options_data) - - - @PromptServer.instance.routes.post("/gde/git/users") - async def get_users(request): - """ the list of users is just the list of branches in the git repo""" - import subprocess - print("getting users") - - # Ensure that the git repo is in the correct state - git_io.ensure_git_state("main") - - # Change to correct directory - os.chdir(git_io.real_git_io_path) - - # Fetch updates from the remote - subprocess.call(["git", "fetch", "--prune"]) - - # List all remote branches - remote_branches_raw = subprocess.check_output(["git", "branch", "-r"]).decode("utf-8").split("\n") - remote_branches = [b.replace("origin/", "").strip() for b in remote_branches_raw] - - # List all local branches - local_branches_raw = subprocess.check_output(["git", "branch"]).decode("utf-8").split("\n") - local_branches = [b.replace("*", "").strip() for b in local_branches_raw] - - # Identify local branches not present in the remote branches - branches_to_delete = [b for b in local_branches if b not in remote_branches] - - # Delete these local branches - for branch in branches_to_delete: - subprocess.call(["git", "branch", "-D", branch]) - - # Now, get the updated list of local branches - branches = subprocess.check_output(["git", "branch"]).decode("utf-8").split("\n") - branches = [b.replace("*", "").strip() for b in branches] - - # Convert the list of branches to the desired format - options_data = [{"value": b, "label": b} for b in branches if b != ""] - - return web.json_response(options_data) - - - @PromptServer.instance.routes.post("/gde/git/save") - async def save_to_git(request): - """ - for git save version 1 each user will have their own branch, - and the data will be saved to a file in the user's branch - version > 1 may include other strategies - """ - - git_save_version = 1 - req_json = await request.json() - print("saving to git") - - req_json = await request.json() - data = req_json.get("data", None) - user_name = req_json.get("user_name", None) - file_name = req_json.get("file_name", None) - - git_io.git_save_version_1(user_name, file_name, data) - # Implement your actual Git saving logic here - return web.Response(text="Saved to Git successfully.") - - - @PromptServer.instance.routes.post("/gde/git/load") - async def load_from_git(request): - """use git_io to load the file from git""" - print("loading from git") - req_json = await request.json() - user_name = req_json.get("user_name", None) - file_name = req_json.get("file_name", None) - - # Implement your actual Git loading logic here - file_data = git_io.git_load_version_1(user_name, file_name) - return web.json_response({"data": file_data}) - - - # print(f"Hijacking route {method} {path} {kwargs}") - # post_handler = route.handler - # - - async def my_post_hijack_before(request: web.Request): - json_data = await request.json() - client_id = json_data.get("client_id", None) - - ret = request - - print("client_id:", client_id) - if not isinstance(request, web.Request): - print("request is not a web.Request") - return request - - user = await get_user_if_client_id_logged_in(client_id) - skp_lcl = config_settings["skip_login_for_local"] - is_local_request = request.transport.get_extra_info('peername')[0] in ( - '127.0.0.1', '::1') if request.transport.get_extra_info('peername') else False - - if user: - print("user:", user) - ret = request - elif skp_lcl and is_local_request: - ret = request - else: - ret = web.json_response({"error": "user not found"}, status=401) - - return ret - - - async def my_get_hijack_after(response): - return response - - # ------------------------------------ - # WRAPPER: so we can override json() - # ------------------------------------ - class ModifiedRequestWrapper: - """ - A wrapper around the original `web.Request` that overrides - the `json()` method to return our custom dictionary. - """ - def __init__(self, original_request: web.Request, new_json: dict): - self._original_request = original_request - self._new_json = new_json - - def __getattr__(self, name): - # Delegate everything except .json() to the original request - return getattr(self._original_request, name) - - async def json(self): - # Return our overridden data - return self._new_json - - def fill_prompt(raw_json,**kwargs): - for node_num,node in raw_json.items(): - inputs = node["inputs"] - if node["class_type"]=="RequestInput": - key = inputs["key"] - if key in kwargs: - raw_json[node_num]["inputs"]["overridden_value"] = kwargs[key] - return raw_json - - - # ------------------------------------------------------------------ - # This function checks for "api_file" and if found, injects its JSON - # into the request by returning a ModifiedRequestWrapper. - # ------------------------------------------------------------------ - async def my_post_hijack_before_api_file(request: web.Request): - import os - import json - - # Convert the request body into a dict - data = await request.json() - api_file = data.get("api_file", None) - - if api_file: - kwargs = data.get("kwargs", {}) - # Build path to the file in ./api_graphs - current_dir = os.path.dirname(os.path.abspath(__file__)) - api_graphs_dir = os.path.join(current_dir, "api_graphs") - file_path = os.path.join(api_graphs_dir, api_file) - - if not os.path.isfile(file_path): - return web.json_response({"error": f"api_file '{api_file}' not found"}, status=400) - - with open(file_path, "r", encoding="utf-8") as f: - loaded_json = json.load(f) - - filled_json = fill_prompt(loaded_json,**kwargs) - - # Insert loaded_json into the "prompt" key - data["prompt"] = filled_json - - # Return a new request-like object that yields `data` from .json() - return ModifiedRequestWrapper(request, data) - - # If "api_file" isn't there, just return the original request - return request - - if __name__ == 'EternalKernelLiteGraphNodes.server_endpoints': - async def both_post_hijack_before(request: web.Request): - thing1 = await my_post_hijack_before(request) - thing2 = await my_post_hijack_before_api_file(thing1) - return thing2 - - hijack_prompt_server("/prompt", after_get_func=my_get_hijack_after, before_post_func=both_post_hijack_before) - pass diff --git a/tests/test_descript.py b/tests/test_descript.py deleted file mode 100644 index 14e0925..0000000 --- a/tests/test_descript.py +++ /dev/null @@ -1,16 +0,0 @@ -from unittest import TestCase - - -class TestDecodeTranscriptNode(TestCase): - def test_func(self): - from custom_nodes.EternalKernelLiteGraphNodes.descript import DecodeTranscriptNode - node = DecodeTranscriptNode() - string = """[00:00:00] jackie: Let's talk about a - - [00:00:01] jackie: thing\n\n[00:00:05] jackie: Let's talk about an apple - - """ - speakers, durations, paragraphs = node.func(string) - self.assertEqual(speakers, ["jackie", "jackie", "jackie"]) - self.assertEqual(durations, [1, 4, 2]) - self.assertEqual(paragraphs, ["Let's talk about a", 'thing', "Let's talk about an apple"]) \ No newline at end of file diff --git a/tests/test_git.py b/tests/test_git.py deleted file mode 100644 index 925470d..0000000 --- a/tests/test_git.py +++ /dev/null @@ -1,120 +0,0 @@ -import unittest -from unittest.mock import patch, Mock -from git import Repo -from git.exc import GitCommandError -import os - -os.putenv('UNIT_TEST', 'True') -from ..git import PublicGitRepo, CloneRepoNode, PullRepoNode, PushRepoNode, SendCommitNode, CreateRepoNode - -repo_path = './tmp/test_repo' -commit_message = 'message' -token = 'token' -# use tashaskyups's repo on github for testing -repo_url = "https://github.com/TashaSkyUp/public_repo.git" - - -class BaseTestCase(unittest.TestCase): - pass - - -class TestPublicGitRepo(BaseTestCase): - def test_validate_git_repo_url(self): - repo = PublicGitRepo() - # Test with a valid URL - result = repo.validate_git_repo_url(repo_url=repo_url, token=token) - self.assertTrue(result) - - # Test with an invalid URL - result = repo.validate_git_repo_url(repo_url='https://github.com', token=token) - self.assertFalse(result) - - -class TestCloneRepoNode(BaseTestCase): - # @patch.object(Repo, 'clone_from') - def test_clone_repo(self): - import shutil - # delete the repo if it exists first - if os.path.exists(repo_path): - # this fails so - # shutil.rmtree(repo_path) - # use this instead - os.system(f'rmdir "{repo_path}" /S /Q ') - - node = CloneRepoNode() - node.clone_repo(repo_url=repo_url, path=repo_path, token=token) - # shutil.rmtree(repo_path) - # os.system(f'rmdir "{repo_path}" /S /Q ') - # mock_clone_from.assert_called_once_with(repo_url, repo_path, - # env={'GIT_ASKPASS': 'echo', 'GIT_USERNAME': 'token'}) - - -class TestPullRepoNode(BaseTestCase): - # @patch.object(Repo, 'remotes') - # @patch.object(Repo, '__init__', return_value=None) - def test_pull_repo(self): - # mock_origin = Mock() - # mock_remotes.origin = mock_origin - node = PullRepoNode() - node.pull_repo(repo_path=repo_path, token=token) - # mock_origin.pull.assert_called_once() - - -class TestSendCommitNode(BaseTestCase): - # @patch.object(Repo, 'git') - # @patch.object(Repo, 'index') - # @patch.object(Repo, '__init__', return_value=None) - def test_send_commit(self): - import hashlib - data = hashlib.md5().hexdigest() - # write to a file - with open(f'{repo_path}/test.txt', 'w') as f: - f.write(data) - # now commit the file - node = SendCommitNode() - node.send_commit(repo_path=repo_path, commit_message=commit_message, token=token) - # mock_git.add.assert_called_once_with(A=True) - # mock_index.commit.assert_called_once_with(commit_message) - - -class TestPushRepoNode(BaseTestCase): - # @patch.object(Repo, 'remotes') - # @patch.object(Repo, '__init__', return_value=None) - def test_push_repo(self): - # mock_origin = Mock() - # mock_remotes.origin = mock_origin - node = PushRepoNode() - node.push_repo(repo_path=repo_path, token=token) - # mock_origin.push.assert_called_once() - - -class TestCreateRepoNode(BaseTestCase): - @patch.object(Repo, 'init') - def test_create_repo(self, mock_init): - node = CreateRepoNode() - node.create_repo(path=repo_path, token=token) - mock_init.assert_called_once_with(repo_path) - - -if __name__ == '__main__': - # run the tests one at a time in a n order that makes sense - runner = unittest.TextTestRunner() - - suite = unittest.TestSuite() - suite.addTest(TestCloneRepoNode('test_clone_repo')) - runner.run(suite) - - suite = unittest.TestSuite() - suite.addTest(TestSendCommitNode('test_send_commit')) - runner.run(suite) - - suite = unittest.TestSuite() - suite.addTest(TestPushRepoNode('test_push_repo')) - runner.run(suite) - - # suite.addTest(TestPublicGitRepo('test_validate_git_repo_url')) - # suite.addTest(TestCreateRepoNode('test_create_repo')) - # suite.addTest(TestPullRepoNode('test_pull_repo')) - - # run all the tests - # unittest.main() diff --git a/tests/test_hf_diffusers_nodes.py b/tests/test_hf_diffusers_nodes.py deleted file mode 100644 index 43a7bbf..0000000 --- a/tests/test_hf_diffusers_nodes.py +++ /dev/null @@ -1,84 +0,0 @@ -import pytest -from ..hf_diffusers_nodes import DDPMPipline, SampleDDPMPipline, TensorToImage -from ..hf_diffusers_nodes import LoadHFDataset - - -# when testing set "UNIT_TEST" to true in the environment to avoid loading all of ComfyUI -def test_DDPMPipline(): - p_node = DDPMPipline() - ret = p_node.load_from_pretrained("johnowhitaker/ddpm-butterflies-32px", - "cuda") - assert ret is not None - - -def test_SampleDDPMPipline(): - p_node = DDPMPipline() - s_node = SampleDDPMPipline() - ret = p_node.load_from_pretrained("johnowhitaker/ddpm-butterflies-32px", - "cuda") - ret = ret[0] - assert ret is not None - ret = s_node.sample(ret, - 1, - 25 - ) - assert ret is not None - - -def test_TensorToImage(): - p_node = DDPMPipline() - s_node = SampleDDPMPipline() - t_node = TensorToImage() - ret = p_node.load_from_pretrained("johnowhitaker/ddpm-butterflies-32px", - "cuda") - ret = ret[0] - assert ret is not None - ret = s_node.sample(ret, - 1, - 25 - ) - ret = ret[0] - assert ret is not None - ret = t_node.tensor_to_image(ret, "HWC") - ret = ret[0] - assert ret is not None - # now convert to pillow image - from PIL import Image - import numpy as np - # ret is float32 0-1 torch tensor we need to convert it to numpy and then to a pillow image - - ret = ret.squeeze(0) - ret = ret.squeeze(0) - ret = ret.numpy() - ret = ret * 255 - ret = np.uint8(ret) - ret = Image.fromarray(ret) - - ret.show() - - -def test_LoadHFDataset(): - p_node = LoadHFDataset() - ret = p_node.load_from_hf("huggan/smithsonian_butterflies_subset") - assert ret is not None - - -if __name__ == "__main__": - pytest.main() - - -def test_create_unet2d_model(): - from ..hf_diffusers_nodes import CreateUnet2dModel - p_node = CreateUnet2dModel() - ret = p_node.create_unet2d_model(3, - 3, - 1, - 1, - 1, - 1, - ) - assert ret is not None - - - - diff --git a/tests/test_image.py b/tests/test_image.py deleted file mode 100644 index 50d309a..0000000 --- a/tests/test_image.py +++ /dev/null @@ -1,48 +0,0 @@ -import unittest -from PIL import Image -from custom_nodes.EternalKernelLiteGraphNodes.image import TextRender as Renderer -import os -hard_test = """Chuck Norris needs no mouse; - -He stares down the computer - -THE END. - -SchwaaWeb, trained by Chuck!""" -hard_test2 ="""Chuck Norris can catch a data breach before it even happens. - -SchwaaWeb, vigilant - -like Chuck!""" -# set an env var to tell so we know we are in test mode -os.environ["ETERNAL_KERNEL_LITEGRAPH_NODES_TEST"] = "True" - -class TestRenderText(unittest.TestCase): - def test_render_text(self): - # Initialize object that includes _render_text and _wrap_text methods - renderer = Renderer() - - # Create a blank image - image = Image.new('RGB', (500, 500), color=(73, 109, 137)) - - # Define parameters for _render_text - font_name = 'arial' - size = 24 - text = hard_test2 - x, y = 0, 0 - - try: - # Call the function under test - renderer._render_text(font_name, image, size, text, x, y, allow_wrap=True, allow_shrink=True) - - # Check that the image is not empty (i.e., it has been changed by the drawing operation) - self.assertNotEqual(image.tobytes(), Image.new('RGB', (500, 500), color=(73, 109, 137)).tobytes()) - finally: - # Show the image whether the test passed or failed - image.show() - -if __name__ == '__main__': - unittest.main() - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_torchvision_nodes.py b/tests/test_torchvision_nodes.py deleted file mode 100644 index 12c7408..0000000 --- a/tests/test_torchvision_nodes.py +++ /dev/null @@ -1,62 +0,0 @@ -import pytest -from ..torchvision_nodes import ( - TorchVisionTransformCompositionList, - TorchVisionTransformStringParser, - TorchVisionTransformNode, - TorchVisionCallComposition, -) - - -# when testing set "UNIT_TEST" to true in the environment to avoid loading all of ComfyUI - - -def test_TorchVisionTransformCompositionList(): - tvtcl_node = TorchVisionTransformCompositionList() - ret = tvtcl_node.compose("test") - ret = ret[0] - assert ret is not None - - -def test_TorchVisionTransformStringParser(): - node = TorchVisionTransformStringParser() - ret = node.compose("Resize(64,64)\nGaussianBlur(1)", "test") - transforms, name = ret - assert name == "test" - assert transforms[0][0] == "Resize" - assert transforms[0][1] == ["64", "64"] - assert transforms[1][0] == "GaussianBlur" - assert transforms[1][1] == ["1"] - -def test_TorchVisionTransformNode(): - node1 = TorchVisionTransformCompositionList() - node2 = TorchVisionTransformNode() - - ret = node1.compose("test") - ret = ret[0] - assert ret is not None - - ret = node2.compose(ret,"GaussianBlur","1") - ret = ret[0] - assert ret is not None - -def test_TorchVisionCallComposition(): - node1 = TorchVisionTransformCompositionList() - node2 = TorchVisionTransformNode() - node3 = TorchVisionCallComposition() - - ret = node1.compose("test") - ret = ret[0] - assert ret is not None - - ret = node2.compose(ret,"GaussianBlur","1") - ret = ret[0] - assert ret is not None - - import torch - test_tensor = torch.rand(1,256,256,3) - ret = node3.compose(ret,test_tensor) - ret = ret[0] - assert ret is not None - -if __name__ == "__main__": - pytest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 703f24b..0000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,41 +0,0 @@ -import torch -import numpy as np -from custom_nodes.EternalKernelLiteGraphNodes.utils import etk_deep_copy - - -def test_etk_deep_copy(): - original = { - "list": [1, 2, 3, [4, 5, 6], {"inner_list": [7, 8, 9]}], - "dict": {"inner_dict": {"key": "value"}} - } - torch_test = {"torch_tensor": torch.tensor([1, 2, 3])} - np_test = {"np_array": np.array([1, 2, 3])} - - test(original) - test(torch_test) - test(np_test) - - -def test(original): - # Creating a deep copy - copy = etk_deep_copy(original) - - # Modifying the original structure - if "list" in original: - original["list"][0] = 100 - if isinstance(original["list"][3], list): - original["list"][3][0] = 200 - if "inner_list" in original["list"][4]: - original["list"][4]["inner_list"][0] = 300 - - if "dict" in original and "inner_dict" in original["dict"]: - original["dict"]["inner_dict"]["key"] = "modified" - - if "torch_tensor" in original: - original["torch_tensor"][0] = 100 - - if "np_array" in original: - original["np_array"][0] = 200 - - # Returning both the original and the copy for inspection - return original, copy diff --git a/tests/test_video.py b/tests/test_video.py deleted file mode 100644 index e663b51..0000000 --- a/tests/test_video.py +++ /dev/null @@ -1,277 +0,0 @@ -from unittest import TestCase -import os -import torch -import os -import shutil -import tempfile -import pytest -import torch -from PIL import Image -from ..audio_video_folder import AddFramesToVideoFolder, VideoFolderProvider, VideoDefinitionProvider - -os.environ["ETERNAL_KERNEL_LITEGRAPH_NODES_TEST"] = "True" - - -class TestImageStackToVideoFile(TestCase): - def test_handler(self): - # test with random frames of noise - import numpy as np - from custom_nodes.EternalKernelLiteGraphNodes.video import ImageStackToVideoFile - - # create a random stack of images - stack = np.random.randint(0, 255, (100, 100, 100, 3), dtype=np.uint8) - # it expects a tensor - stack = torch.from_numpy(stack) - # create the node - node = ImageStackToVideoFile() - # run the node - node.handler(image_stack=stack, video_out="test.mp4") - # check that the file exists - # find the actual filepath for the generated file, will be ../../../video/test.mp4 - test_video_file_fullpath = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))), "video", - "test.mp4") - - self.assertTrue(os.path.exists(test_video_file_fullpath)) - # remove the file - os.remove(test_video_file_fullpath) - - -class TestTemporalSpatialSmoothing(TestCase): - def test_handler(self): - # test with random frames of noise - - from custom_nodes.EternalKernelLiteGraphNodes.video import TemporalSpatialSmoothing, ImageStackToVideoFile - from custom_nodes.EternalKernelLiteGraphNodes.image import torch_image_show - - # use only torch - # create a random stack of images - # use floats - stack = torch.rand((48, 512, 512, 3)) - # _,path= ImageStackToVideoFile().handler(image_stack=stack, video_out="test_start.mp4",fps=24) - # launch the default video player - # os.system(f"start {path}") - - # create the node - node = TemporalSpatialSmoothing() - # run the node - result = node.handler(image_stack=stack, spatial_kernel_size=3, temporal_kernel_size=3, spatial_sigma=.5, - temporal_sigma=.5) - # check that the file exists - _, path = ImageStackToVideoFile().handler(image_stack=result[0], video_out="test_result.mp4", fps=24) - # launch the default video player - os.system(f"start {path}") - - # torch_image_show(result[0]) - - -class TestTemporalOpticalFlowSmoothing(TestCase): - def test_handler(self): - from custom_nodes.EternalKernelLiteGraphNodes.video import TemporalOpticalFlowSmoothing, ImageStackToVideoFile - import time - import numpy as np - # create a random stack of images - # stack = torch.rand((24*4, 512, 512, 3)) - h = 256 - w = 512 - fps = 24 - seconds = 2 - stack = torch.rand((fps * seconds, h, w, 3)) - # now add a box that moves from left to right with a constant speed - # the box will be 20% of the width and 20% of the height - # the box will move from left to right - - # create the box - box = torch.zeros((fps * seconds, int(h), int(w), 3)) - # fill the box with ones - box = box + 1 - # the box width - bw = min(int(w * .2), int(h * .2)) - # the box height - bh = int(h * .2) - # the box x position - - t = stack.shape[0] - for i in range(t): - pt = i / t - - # the box x position - bx = int((w - bw) * pt) - # now add a jitter - bx += int(np.random.randint(-10, 10, 1)) - # the box y position - by = int(h * .333) - # fill the box with ones - box[i, :, :, :] = 0 - box[i, by:by + bh, bx:bx + bw, :] = 1 - - # create a mask for the box region - # box_mask = torch.zeros_like(stack) - # box_mask[:, by:by + bh, bx:bx + bw, :] = 1 - - # set the color of the stack to blue where the box mask is 1 - stack = torch.where(box == 1, torch.tensor([.24, .5, .7], dtype=torch.float32), stack) - - nm = "box1" - _, path = ImageStackToVideoFile().handler(image_stack=stack, video_out=nm, fps=fps) - assert os.path.exists(path), f"Output video file doesn't exist: {path}" - os.system(f"start {path}") - time.sleep(1) - - # create the node - node = TemporalOpticalFlowSmoothing() - # run the node - result = node.handler(image_stack=stack, device="cuda") - result = result[0] - - # result = torch.cat(result[0], dim=0) - # print(result.shape) - # now we can permute the dimensions - # result = result.permute(0, 2,3, 1) - - _, path = ImageStackToVideoFile().handler(image_stack=result, video_out="test_result_optical_flow.mp4", fps=fps) - assert os.path.exists(path), f"Output video file doesn't exist: {path}" - os.system(f"start {path}") - - ## Check the shape of the output - # assert len(result) == stack.shape[0] - 1 - - # Each element in the result should have the shape of [1, 2, H, W] - # for flow in result: - # assert flow.shape == torch.Size([1, 2, 512, 512]) - - # check the file generation and video play - - -from ..audio_video_folder import AddFramesToVideoFolder, VideoFolderProvider, VideoDefinitionProvider - - -@pytest.fixture(scope="function") -def video_folder_def(): - this_file_dir = os.path.dirname(os.path.abspath(__file__)) - temp_dir = os.path.join(this_file_dir, "temp") - os.makedirs(temp_dir, exist_ok=True) - vfp = VideoFolderProvider().video_folder_provider(folder_path=temp_dir)[0] - d = { - "video_folder": vfp, - "fps": 24, - "width": 256, - "height": 256, - "frame format": "jpg", - } - VFD = VideoDefinitionProvider().video_definition_provider(**d)[0] - yield VFD - # shutil.rmtree(temp_dir) - - -def test_add_frames_to_video_folder(video_folder_def): - # Generate synthetic inputs - num_frames = 10 - image_size = (256, 256) - frames = torch.randint(0, 256, (num_frames, *image_size, 3), dtype=torch.uint8) - rescale_method = "LANCZOS" - release_vram = True - - # Call the function - result = AddFramesToVideoFolder().add_frames_to_video_folder(video_folder_def=video_folder_def, - IMAGE=frames, - Rescale_method=rescale_method, - Release_vram=release_vram - )[0] - - # Check the output - assert len(os.listdir(result["video_folder"])) == num_frames - - # Check the saved frames - for i in range(num_frames): - frame_path = video_folder_def["get_last_file_name"](i + 1) - saved_frame = Image.open(frame_path) - assert saved_frame.size == image_size - - -def test_save_or_copy_frame(video_folder_def): - # Test case 1: Different frames - # Generate synthetic inputs - current_frame = torch.randint(0, 256, (256, 256, 3), dtype=torch.uint8) - previous_frame = torch.randint(0, 256, (256, 256, 3), dtype=torch.uint8) - frame_fp = video_folder_def.get_next_file_name() - previous_frame_fp = video_folder_def.get_next_file_name() - - # Save the previous frame - Image.fromarray(previous_frame.permute(2, 0, 1).permute(1, 2, 0).numpy()).save(previous_frame_fp) - - # Call the function - result = AddFramesToVideoFolder.save_or_copy_frame(current_frame, previous_frame, frame_fp, previous_frame_fp) - - # Check the output - assert torch.equal(result, current_frame) - assert os.path.exists(frame_fp) - - # Check the saved frame - saved_frame = Image.open(frame_fp) - assert saved_frame.size == (256, 256) - saved_frame.close() - # clean up - os.remove(frame_fp) - - - # Test case 2: Duplicate frames - # Generate synthetic inputs - duplicate_frames = [torch.randint(0, 256, (256, 256, 3), dtype=torch.uint8)] - duplicate_frame = duplicate_frames * 2 - - - previous_duplicate_frame_fp = video_folder_def.get_next_file_name(0) - # need to check the file names for 0000000000 and 0000000001 - assert "0000000000" in previous_duplicate_frame_fp - #assert "0000000001" in duplicate_frame_fp - - # Save the previous duplicate frame - Image.fromarray(duplicate_frames[0].permute(2, 0, 1).permute(1, 2, 0).numpy()).save(previous_duplicate_frame_fp) - - - # Call the function 100 times - for i in range(100): - duplicate_frame_fp = video_folder_def.get_next_file_name(0) - result = AddFramesToVideoFolder.save_or_copy_frame(duplicate_frames[0], duplicate_frame[1], duplicate_frame_fp, - previous_duplicate_frame_fp) - - # Check the output - assert torch.equal(result, duplicate_frame[0]) - assert os.path.exists(duplicate_frame_fp) - - # check that here are 101 files - assert len(os.listdir(video_folder_def["video_folder"])) == 101 - - # clean up - for i in range(101): - os.remove(video_folder_def.get_last_file_name(0)) - - - - -def test_process_frames(video_folder_def): - # Generate synthetic inputs - num_frames = 10 - image_size = (256, 256) - frames = torch.randint(0, 256, (num_frames, *image_size, 3), dtype=torch.uint8) - rescale_method = "LANCZOS" - - # Call the function - result = AddFramesToVideoFolder.process_frames(frames, rescale_method, video_folder_def) - - # Check the output - # import pdb; - # pdb.set_trace() - - assert result is None - test = video_folder_def["video_folder"] - print("path: ", test) - assert len(os.listdir(test)) == num_frames - - # Check the saved frames - assert video_folder_def["get_frame_count"]() == num_frames - for i in range(num_frames): - frame_path = video_folder_def["get_last_file_name"](i + 1) - saved_frame = Image.open(frame_path) - assert saved_frame.size == image_size diff --git a/tests/test_youtube_nodes.py b/tests/test_youtube_nodes.py deleted file mode 100644 index c2cf1cd..0000000 --- a/tests/test_youtube_nodes.py +++ /dev/null @@ -1,17 +0,0 @@ -import os - -os.environ["UNIT_TEST"] = "True" -from custom_nodes.EternalKernelLiteGraphNodes.youtube_nodes import YoutubeUploadNode - - -def test_upload(): - node = YoutubeUploadNode() - output = node.upload_video( - file_path=os.path.join("tests", "example.mp4"), - category=22, - title="Test Video", - description="This is a test video for the Youtube Upload Node", - keywords="test, youtube, node", - privacy_status="unlisted" - ) - print (output) diff --git a/torchvision_nodes.py b/torchvision_nodes.py deleted file mode 100644 index 0591983..0000000 --- a/torchvision_nodes.py +++ /dev/null @@ -1,235 +0,0 @@ -try: - from .config import config_settings -except ImportError as e: - from config import config_settings - -import functools -import torch -import torchvision - -#NODE_CLASS_MAPPINGS = {} -#NODE_DISPLAY_NAME_MAPPINGS = {} -from . import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS - - -def ETK_torchvision_base(cls): - cls.CATEGORY = "ETK/torchvision" - # Add spaces to the camel case class name - pretty_name = cls.__name__ - for i in range(1, len(pretty_name)): - if pretty_name[i].isupper(): - pretty_name = pretty_name[:i] + " " + pretty_name[i:] - cls.DISPLAY_NAME = pretty_name - NODE_CLASS_MAPPINGS[cls.__name__] = cls - NODE_DISPLAY_NAME_MAPPINGS[cls.DISPLAY_NAME] = pretty_name - - # Wrap the function defined in the FUNCTION attribute - func_name = getattr(cls, "FUNCTION", None) - if func_name and hasattr(cls, func_name): - original_func = getattr(cls, func_name) - - @functools.wraps(original_func) - def wrapped_func(*args, **kwargs): - with torch.inference_mode(False): - return original_func(*args, **kwargs) - - setattr(cls, func_name, wrapped_func) - - return cls - - -@ETK_torchvision_base -class TorchVisionTransformCompositionList: - """Provides the base list that helps define a composition pipeline for torchvision transforms""" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "name": ("STRING", {"default": "t_comp_00"}), - }, - } - - RETURN_TYPES = ("TV_COMPOSITION_LIST", "STRING") - RETURN_NAMES = ("transforms_list", "name") - FUNCTION = "compose" - - def compose(self, name): - transforms = [] - return (transforms, name,) - - -@ETK_torchvision_base -class TorchVisionTransformStringParser: - """Parses a newline separated list of torchvision transform calls. - - Each line should be either ``TransformName`` or ``TransformName(arg1, arg2)``. - This node helps users quickly define a composition pipeline without having - to add a separate node for each transform. - """ - - @classmethod - def INPUT_TYPES(cls): - import inspect - import torchvision.transforms as transforms - import torch.nn as nn - - # build default definition text listing all available transform - # classes and their constructor parameters for quick reference - lines = [] - for name, obj in inspect.getmembers(transforms, inspect.isclass): - if obj.__module__.startswith("torchvision.transforms") and issubclass(obj, nn.Module): - sig = inspect.signature(obj.__init__) - params = [p for p in sig.parameters.values() if p.name != "self"] - p_names = ", ".join(p.name for p in params) - lines.append(f"{name}({p_names})") - - default_def = "\n".join(lines) - - return { - "required": { - "definition": ("STRING", {"default": default_def, "multiline": True}), - "name": ("STRING", {"default": "t_comp_00"}), - }, - } - - RETURN_TYPES = ("TV_COMPOSITION_LIST", "STRING") - RETURN_NAMES = ("transforms_list", "name") - FUNCTION = "compose" - - def compose(self, definition, name): - import re - - lines = [l.strip() for l in definition.splitlines() if l.strip()] - transforms = [] - for line in lines: - m = re.match(r"(\w+)(?:\((.*)\))?", line) - if not m: - continue - t_name = m.group(1) - args_str = m.group(2) - if args_str: - args = [a.strip() for a in args_str.split(',') if a.strip()] - else: - args = None - transforms.append([t_name, args]) - - return (transforms, name,) - - -@ETK_torchvision_base -class TorchVisionTransformNode: - """Provides the base class for torchvision transforms""" - - @classmethod - def INPUT_TYPES(cls): - import torchvision.transforms as transforms - import inspect - import torch.nn as nn - - # gather torchvision transform classes for dropdown selection - t_list = [] - c_list = [] - for name, obj in inspect.getmembers(transforms, inspect.isclass): - if obj.__module__.startswith("torchvision.transforms") and issubclass(obj, nn.Module): - t_list.append([name, obj]) - c_list.append(name) - - return { - "required": { - "composition": ("TV_COMPOSITION_LIST", {"default": None}), - "transform": (c_list,), - }, "optional": { - "args": ("STRING", {"default": ""}), - }, - } - - RETURN_TYPES = ("TV_COMPOSITION_LIST",) - - FUNCTION = "compose" - - def compose(self, composition: list, transform, args=None): - composition = composition.copy() - - if args: - if not "(" in args[0] and not ")" in args[-1]: - args = args.split(",") - else: - args = [args] - - composition.append([transform, args]) - - return (composition,) - - -@ETK_torchvision_base -class TorchVisionCallComposition: - """ - creates the composition object and calls it on the input TORCH_TENSOR or IMAGE - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "composition": ("TV_COMPOSITION_LIST", {"default": None}), - }, "optional": { - "tensor": ("TORCH_TENSOR",), - "image": ("IMAGE",), - }, - } - - RETURN_TYPES = ("TORCH_TENSOR", "IMAGE", "TV_COMPOSITION") - FUNCTION = "compose" - - def compose(self, composition, tensor=None, image=None): - # we assume that both tensor and image are fit for the transform and that the transform is valid - if tensor == None and image == None: - raise ValueError("Must provide either a tensor or an image") - if tensor != None and image != None: - raise ValueError("Must provide either a tensor or an image, not both") - - if tensor == None and image != None: - tensor = image - - # transform args that might be ints or floats - composition = composition.copy() - - for c_name, c_args in composition: - if c_args: - - if isinstance(c_args, list): - for i in range(len(c_args)): - try: - if "(" in c_args[i][0] and ")" in c_args[i][-1]: - c_args[i] = [eval(c_args[i])][0] - - elif "." in c_args[i]: - c_args[i] = float(c_args[i]) - - else: - c_args[i] = int(c_args[i]) - except: - pass - - # create the transform composition list that will be used with transforms.Compose - transform_composition = [] - for l_c_name, l_c_args in composition: - if l_c_name: - if l_c_args is None: - l_c_args = [] - transform_composition.append(getattr(torchvision.transforms, l_c_name)(*l_c_args)) - - # now that the list is composed use it to create the compostion object - transform_composition = torchvision.transforms.Compose(transform_composition) - - # apply the transform to each item in the batch - # many torchvision transforms expect a single image tensor of shape C,H,W - tensor = tensor.permute(0, 3, 1, 2) # B,C,H,W - out_list = [] - for img in tensor: - out_list.append(transform_composition(img)) - tensor = torch.stack(out_list, dim=0) - tensor = tensor.permute(0, 2, 3, 1) - - return (tensor, tensor, transform_composition,) diff --git a/utils.py b/utils.py deleted file mode 100644 index 59a11d2..0000000 --- a/utils.py +++ /dev/null @@ -1,155 +0,0 @@ -import copy - -from collections import OrderedDict - - -def etk_deep_copy(x, verbose=False): - if type(x).__module__ == "numpy": - return x.copy() - elif type(x).__module__ == "torch": - return x.clone() - elif isinstance(x, list): - return [etk_deep_copy(item) for item in x] - elif isinstance(x, tuple): - return tuple(etk_deep_copy(item) for item in x) - elif isinstance(x, dict): - return {k: etk_deep_copy(v) for k, v in x.items()} - elif isinstance(x, set): - return set(etk_deep_copy(item) for item in x) - elif isinstance(x, OrderedDict): - return OrderedDict((k, etk_deep_copy(v)) for k, v in x.items()) - else: - if verbose: - 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) - - # check if the file exists, if it doesnt create it with the current data - if not os.path.exists(self.file_path): - self._data = {} - self._save() - else: - 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, debug=False): - """Saves the current data to the file.""" - # Debug print to check if the directory and file path exist - if debug: - print(f"Saving file to: {self.file_path}") - directory = os.path.dirname(self.file_path) - - # Ensure the directory exists, create if it doesn't - if not os.path.exists(directory): - if debug: - print(f"Directory does not exist, creating: {directory}") - os.makedirs(directory, exist_ok=True) - else: - if debug: - print(f"Directory exists: {directory}") - - # Check if the file exists - if not os.path.exists(self.file_path): - if debug: - print(f"File does not exist, it will be created: {self.file_path}") - else: - if debug: - print(f"File already exists: {self.file_path}") - - # 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, creating the file if it does not exist using utf-8 encoding - with open(self.file_path, 'w', encoding='utf-8') as file: - if debug: - print(f"Writing data to file: {self.file_path}") - json.dump(data_to_save, file, ensure_ascii=False, indent=4) - if debug: - print("File written successfully.") - - 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() diff --git a/video.py b/video.py deleted file mode 100644 index bfe8ebf..0000000 --- a/video.py +++ /dev/null @@ -1,2282 +0,0 @@ -### info for code completion AI ### -""" -all of these classes are plugins for comfyui and follow the same pattern -all of the images are torch tensors and it is unknown and unimportant if they are on the cpu or gpu -all image inputs are (B,W,H,C) - -avoid numpy and PIL as much as possible -""" -import shutil -import fractions -from abc import ABC, ABCMeta, abstractmethod -from os.path import splitext, join -import torchaudio -import os -import torch -from torchvision.utils import draw_keypoints -import numpy as np -import cv2 -import imageio_ffmpeg as ffmpeg - -from . import NODE_CLASS_MAPPINGS - -# from . import 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__)))) -# input_dir = os.path.join(root_p, 'input', 'video') -# output_dir = os.path.join(root_p, 'output', 'video') - -video_dir = os.path.join(root_p, 'video') - -if not os.path.exists(video_dir): - os.makedirs(video_dir) - - -# video_folders = lambda s: sorted( -# [name for name in os.listdir(s.input_dir) if os.path.isdir(os.path.join(s.input_dir, name))]) -# turn the above in the a full function -def video_folders(s): - ret = sorted( - [name for name in os.listdir(s.input_dir) if os.path.isdir(os.path.join(s.input_dir, name))]) - return ret - - -def video_files(s): - # any files in in_video_folders ending in .mp4 - ret = sorted([name for name in os.listdir(s.input_dir) if - os.path.isfile(os.path.join(s.input_dir, name)) and name.endswith('.mp4')]) - return ret - - -def run_ffmpeg_command(args): - """ - Executes a command using the ffmpeg executable. - - Parameters: - args (list): List of command-line arguments for ffmpeg. - """ - if isinstance(args, str): - args = args.split() - cmd = [ffmpeg.get_ffmpeg_exe()] + args - subprocess.run(cmd) - - -# start with an ABC to define the common widget interface - -class ABCWidgetMetaclass(ABCMeta): - """A metaclass that automatically registers classes.""" - - def __init__(cls, name, bases, attrs): - if (ABC in bases) or (ABCVideoWidget in bases) or ("ABC" in name): - pass - else: - NODE_CLASS_MAPPINGS[str(name)] = cls - cls.CATEGORY = "ETK/video" - super().__init__(name, bases, attrs) - - -class ABCVideoWidget(ABC, metaclass=ABCWidgetMetaclass): - """Abstract base class for simple construction""" - - input_dir = video_dir - output_dir = video_dir - - CATEGORY = "video" - RETURN_TYPES = ("VIDEO",) - FUNCTION = "handler" - - @classmethod - @abstractmethod - def INPUT_TYPES(cls): - """All subclasses must provide a INPUT_TYPES classmethod""" - pass - - @abstractmethod - def handler(self, one: object, two: object): - """All subclasses must provide a handler method""" - pass - - -class ABCVideoFileToABCVideoFolder(ABCVideoWidget, metaclass=ABCWidgetMetaclass): - """Abstract base class for simple construction""" - - @classmethod - def INPUT_TYPES(cls): - return {"required": - { - "video_in": (video_files(cls),), - "folder_out": (video_folders(cls),), - }, - - "optional": - {"text": - ("STRING", {"multiline": False}), - } - } - - @abstractmethod - def handler(self, one, two, text): - return (one, two,) - - -class ABCABCVideoFolderToImage(ABCVideoWidget, metaclass=ABCWidgetMetaclass): - """Abstract base class for simple construction""" - RETURN_TYPES = ("IMAGE",) - - @classmethod - def INPUT_TYPES(cls): - return {"required": - { - "folder_in": ("STRING", {"default": "full_path_to_folder"}), - "idx_slice_start": ("INT", {"min": 0, "max": 99999, "step": 1, "default": 0}), - "idx_slice_stop": ("INT", {"min": 0, "max": 99999, "step": 1, "default": 1}), - "slice_idx_step": ("INT", {"min": 0, "max": 9999, "step": 1, "default": 1}), - "idx": ("INT", {"min": 0, "max": 99999, "step": 1, "default": 0}), - }, - - "optional": - {"text": - ("STRING", {"multiline": False}), - } - } - - @abstractmethod - def handler(self, one, two, text): - return (one,) - - -class ABCABCVideoFileToImage(ABCVideoWidget, metaclass=ABCWidgetMetaclass): - """Abstract base class for simple construction""" - RETURN_TYPES = ("IMAGE",) - - @classmethod - def INPUT_TYPES(cls): - return {"required": - { - "video_in": ("STRING", {"default": "full_path_to_video"}), - "idx_slice_start": ("INT", {"min": 0, "max": 99999, "step": 1, "default": 0}), - "idx_slice_stop": ("INT", {"min": 0, "max": 99999, "step": 1, "default": 1}), - "slice_idx_step": ("INT", {"min": 0, "max": 9999, "step": 1, "default": 1}), - "idx": ("INT", {"min": 0, "max": 99999, "step": 1, "default": 0}), - "func_only": ([False, True], {"default": False}), - }, - - } - - @abstractmethod - def handler(self, one, two, text): - return (one,) - - -## end of abstract base classes ## - -class VideoToFramesFolder(ABCVideoFileToABCVideoFolder, metaclass=ABCWidgetMetaclass): - """use cv2 to open the video and use it as a source for image stacks""" - - RETURN_TYPES = ("IMAGE",) - - def __init__(self): - super().__init__() - self.old = self.INPUT_TYPES - # self.INPUT_TYPES = lambda s: self.old().update({}) - - def handler(self, video_in, folder_out, text): - import time - start = time.time() - video_in_path = os.path.join(self.input_dir, video_in) - video_out_path = os.path.join(self.output_dir, folder_out) - - vidcap = cv2.VideoCapture(video_in_path) - success, image = vidcap.read() - count = 0 - while success: - cv2.imwrite(os.path.join(video_out_path, "frame%d.jpg" % count), image) - success, image = vidcap.read() - count += 1 - - vidcap.release() - end = time.time() - print("time elapsed: ", end - start) - return (folder_out,) - - -class VideoToFramesFolderFFMPEG(ABCVideoFileToABCVideoFolder, metaclass=ABCWidgetMetaclass): - """use ffmpeg to open the video and use it as a source for image stacks, use PNG or jpeg with quality 100""" - - @classmethod - def INPUT_TYPES(cls): - return {"required": - { - "video_in": ("STRING", {"default": "full_path_to_folder"}), - "folder_out": ("STRING", {"default": "full_path_to_folder"}), - "format": (["PNG", "JPEG"], {"default": "PNG"}), - }, - - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("folder_out",) - - def __init__(self): - super().__init__() - self.old = self.INPUT_TYPES - # self.INPUT_TYPES = lambda s: self.old().update({}) - - def handler(self, video_in, folder_out, format): - import time - start = time.time() - video_in_path = os.path.join(self.input_dir, video_in) - video_out_path = os.path.join(self.output_dir, folder_out) - - if not os.path.exists(video_out_path): - os.makedirs(video_out_path) - - # specifically this calls ffmpeg with the following arguments: - # -i means input file - # -qscale:v means quality scale video, default is 2, which is a good compromise between size and quality - # 1 is the quality scale value, which is a value between 1 and 31, where 1 is the highest quality - - start = time.time() - - if format == "JPEG": - subprocess.call( - ["ffmpeg", "-i", video_in_path, "-qscale:v", "1", os.path.join(video_out_path, "frame%05d.jpg")]) - end = time.time() - print("ffmpeg jpg process took {} seconds".format(end - start)) - - elif format == "PNG": - subprocess.call(["ffmpeg", "-i", video_in_path, os.path.join(video_out_path, "frame%05d.png")]) - end = time.time() - print("ffmpeg png process took {} seconds".format(end - start)) - - else: - raise ValueError("format must be either PNG or JPEG") - - return (folder_out,) - - -class VideoFramesFolderToImageStack(metaclass=ABCWidgetMetaclass): - """Use cv2 to open the video and save the videos individual frames""" - - @classmethod - def INPUT_TYPES(cls): - from addict import Dict - ret = Dict() - ret.required = Dict() - ret.required.folder_in = ("STRING", {"default": "full_path_to_folder"}) - ret.required.use_subfolders = ([True, False], {"default": False}) - ret.required.by_date_or_name = (["DATE", "NAME"], {"default": "DATE"}) - ret.required.idx_slice_start = ("INT", {"min": 0, "max": 99999, "step": 1, "default": 0}) - ret.required.idx_slice_stop = ("INT", {"min": 0, "max": 99999, "step": 1, "default": 1}) - ret.required.slice_idx_step = ("INT", {"min": 0, "max": 9999, "step": 1, "default": 1}) - ret.required.idx = ("INT", {"min": 0, "max": 99999, "step": 1, "default": 0}) - ret.required.use_float16 = ([True, False], {"default": False}) - - return ret - - CATEGORY = "video" - RETURN_TYPES = ("IMAGE", "LIST",) - RETURN_NAMES = ("image_stack", "[img_fname]",) - - FUNCTION = "handler" - - @torch.inference_mode() - def handler(self, folder_in, - use_subfolders, - by_date_or_name, - idx_slice_start, - idx_slice_stop, - slice_idx_step, - idx, - use_float16 - ): - """ - Return slices of time from the video, sequences of images, defined by the start, stop and step - - torch tensors (T,H,W,C) - """ - - import glob - idx = idx * slice_idx_step - - folder_in_path = folder_in - image_list = [] - image_fname_list = [] - - p = os.path.abspath(folder_in_path) - # List and sort all PNG or JPG files in the folder - if not use_subfolders and by_date_or_name == "NAME": - file_fps = sorted( - glob.glob(os.path.join(p, '*.png')) + glob.glob(os.path.join(p, '*.jpg'))) - elif not use_subfolders and by_date_or_name == "DATE": - file_fps = sorted( - glob.glob(os.path.join(p, '*.png')) + glob.glob(os.path.join(p, '*.jpg')), - key=os.path.getmtime) - elif use_subfolders and by_date_or_name == "NAME": - file_fps = sorted( - glob.glob(os.path.join(p, '**/*.png'), recursive=True) + - glob.glob(os.path.join(p, '**/*.jpg'), recursive=True)) - elif use_subfolders and by_date_or_name == "DATE": - file_fps = sorted( - glob.glob(os.path.join(p, '**/*.png'), recursive=True) + - glob.glob(os.path.join(p, '**/*.jpg'), recursive=True), - key=os.path.getmtime) - - # Ensure the slice doesn't go beyond the end of the file list - - if idx_slice_stop + idx > len(file_fps): - idx_slice_stop = len(file_fps) - idx - - slice_size = idx_slice_stop - idx_slice_start - # for i in range(idx_slice_start + idx, idx_slice_stop + idx): - for fn in file_fps[idx_slice_start + idx:idx_slice_start + idx + slice_size]: - image = cv2.imread(fn) - image_fname_list.append(os.path.basename(fn)) - if image is not None: - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # converting to RGB format - image_list.append(image) - - # Convert list of images to tensor (T,H,W,C) - if use_float16: - tensor = torch.stack([torch.tensor(img, dtype=torch.float16) for img in image_list]) - else: - tensor = torch.stack([torch.tensor(img, dtype=torch.float32) for img in image_list]) - del image_list - tensor = tensor / 255.0 - return (tensor, image_fname_list,) - - -class SmoothStackTemporalByDistance2(ABCABCVideoFolderToImage, metaclass=ABCWidgetMetaclass): - @classmethod - def INPUT_TYPES(cls): - return {"required": - { - "image_stack": ("IMAGE",), - "top_thresh_normed": ("FLOAT", {"min": 0.0, "max": 100000.0, "step": 0.01, "default": 1.0}), - "bottom_thresh_normed": ("FLOAT", {"min": 0.0, "max": 100000.0, "step": 0.01, "default": 1.0}), - "max_iterations": ("INT", {"min": 0, "max": 1000, "step": 1, "default": 1}), - "target_deviation": ("FLOAT", {"min": 0.0, "max": 100000.0, "step": 0.01, "default": 0.0}), - } - } - - RETURN_TYPES = ("IMAGE", "STRING") - - def handler(self, image_stack, top_thresh_normed=0.9, bottom_thresh_normed=0.10, max_iterations=1, - target_deviation=0.0): - import torch - # Code for printing memory size and available GPU memory stays the same - - image_stack = image_stack.to(torch.float16) - info = "" - iteration = 0 - while True: - # Compute the first-order differences (gradient) along the time dimension (0) - first_order_diff = torch.gradient(image_stack, dim=0)[0] - - # Compute the Euclidean norm of the first-order differences - euclidean_diff_first_order = torch.norm(first_order_diff.view(first_order_diff.shape[0], -1), dim=1).clone() - - euclidean_diff_first_order_range = euclidean_diff_first_order.max() - euclidean_diff_first_order.min() - euclidean_diff_first_order_normed = (euclidean_diff_first_order - euclidean_diff_first_order.min()) / \ - euclidean_diff_first_order_range - - euclidean_diff_first_order_normed = euclidean_diff_first_order_normed[:-1] - - start_mean = euclidean_diff_first_order.mean() - - start_std = euclidean_diff_first_order.std() - - if start_std <= target_deviation or iteration >= max_iterations: - break - - # Identify the frames with differences greater than the top threshold, excluding the last index - outlier_indices_top = torch.where(euclidean_diff_first_order_normed > top_thresh_normed)[0] - - # Interpolate frames where the differences are greater than the top threshold - interpolated_frames_top = 0.5 * (image_stack[outlier_indices_top] + image_stack[outlier_indices_top + 1]) - image_stack[outlier_indices_top + 1] = interpolated_frames_top - - # Identify the frames with differences less than the bottom threshold, excluding the last index - outlier_indices_bottom = torch.where(euclidean_diff_first_order_normed < bottom_thresh_normed)[0] - - # Create a mask to delete frames where the differences are less than the bottom threshold - mask = torch.ones(image_stack.size(0), dtype=torch.bool) - mask[outlier_indices_bottom + 1] = False - image_stack = image_stack[mask] - # now remove the first and last frames - # image_stack = image_stack[1:-1] - # print the shape and stats for image_stack - - # relate how many frames where changed via top threshold and bottom threshold - # also report on the mean and std of the euclidean_diff_first_order - - info += f"Number of frames changed via top threshold: {len(outlier_indices_top)}\n " \ - f"Number of frames changed via bottom threshold: {len(outlier_indices_bottom)}\n " \ - f"Mean of euclidean_diff_first_order: {start_mean}\n " \ - f"Std of euclidean_diff_first_order: {start_std}\n " \ - f"\n\n" - print(info) - iteration += 1 - - if image_stack.shape[0] == 0: - print("image_stack is empty, returning None") - return (None, None,) - print( - f"image_stack shape: {image_stack.shape}, image_stack stats: {image_stack.min()}, {image_stack.max()}") - - return (image_stack.to(torch.float32), info,) - - -class SmoothStackTemporalByDistance(ABCABCVideoFolderToImage, metaclass=ABCWidgetMetaclass): - """ - Smooth the temporal dimension of the image stack by calculating the distance between each frame - and the next frame and then averaging the frames that are within a certain distance of each other. - """ - - @classmethod - def INPUT_TYPES(cls): - return {"required": - { - "image_stack": ("IMAGE",), - "top_thresh_normed": ("FLOAT", {"min": 0.0, "max": 100000.0, "step": 0.1, "default": 1.0}), - "bottom_thresh_normed": ("FLOAT", {"min": 0.0, "max": 100000.0, "step": 0.1, "default": 1.0}), - "iterations": ("INT", {"min": 0, "max": 1000, "step": 1, "default": 1}), - } - } - - RETURN_TYPES = ("IMAGE", "STRING") - - def handler(self, image_stack, - top_thresh_normed=0.9, bottom_thresh_normed=0.10, iterations=1): - import torch - # calculate and print the total size in memory in bytes of the tensor - print( - f"Size of tensor in memory in GB: {image_stack.element_size() * image_stack.nelement() / (1024 ** 3)} GB, dtype: {image_stack.dtype}") - # print the available memory on the GPU in GB - print(f"Available memory on GPU in GB: {torch.cuda.get_device_properties(0).total_memory / (1024 ** 3)} GB") - - # image_stack = image_stack.clone() - image_stack = image_stack.to(torch.float16) - info = "" - for _ in range(iterations): - # Compute the first-order differences (gradient) along the time dimension (0) - first_order_diff = torch.gradient(image_stack, dim=0)[0] - - # Compute the Euclidean norm of the first-order differences - euclidean_diff_first_order = torch.norm(first_order_diff.view(first_order_diff.shape[0], -1), dim=1).clone() - - euclidean_diff_first_order_range = euclidean_diff_first_order.max() - euclidean_diff_first_order.min() - euclidean_diff_first_order_normed = (euclidean_diff_first_order - euclidean_diff_first_order.min()) / \ - euclidean_diff_first_order_range - - euclidean_diff_first_order_normed = euclidean_diff_first_order_normed[:-1] - - start_mean = euclidean_diff_first_order.mean() - start_std = euclidean_diff_first_order.std() - - # Identify the frames with differences greater than the top threshold, excluding the last index - outlier_indices_top = torch.where(euclidean_diff_first_order_normed > top_thresh_normed)[0] - - # Interpolate frames where the differences are greater than the top threshold - interpolated_frames_top = 0.5 * (image_stack[outlier_indices_top] + image_stack[outlier_indices_top + 1]) - image_stack[outlier_indices_top + 1] = interpolated_frames_top - - # Identify the frames with differences less than the bottom threshold, excluding the last index - outlier_indices_bottom = torch.where(euclidean_diff_first_order_normed < bottom_thresh_normed)[0] - - # Create a mask to delete frames where the differences are less than the bottom threshold - mask = torch.ones(image_stack.size(0), dtype=torch.bool) - mask[outlier_indices_bottom + 1] = False - image_stack = image_stack[mask] - # now remove the first and last frames - # image_stack = image_stack[1:-1] - # print the shape and stats for image_stack - - # relate how many frames where changed via top threshold and bottom threshold - # also report on the mean and std of the euclidean_diff_first_order - info += f"Number of frames changed via top threshold: {len(outlier_indices_top)}\n " \ - f"Number of frames changed via bottom threshold: {len(outlier_indices_bottom)}\n " \ - f"Mean of euclidean_diff_first_order: {start_mean}\n " \ - f"Std of euclidean_diff_first_order: {start_std}\n " \ - f"\n\n" - print(info) - - if image_stack.shape[0] == 0: - print("image_stack is empty, returning None") - return (None, None,) - print( - f"image_stack shape: {image_stack.shape}, image_stack stats: {image_stack.min()}, {image_stack.max()}") - - return (image_stack.to(torch.float32), info,) - - -class GetImageStackStatistics(ABCABCVideoFolderToImage, metaclass=ABCWidgetMetaclass): - """Get statistics of an image stack""" - - @classmethod - def INPUT_TYPES(cls): - return {"required": - { - "image_stack": ("IMAGE",), - "method": (["SSIM", "MSE", "REGIONAL"], {"default": "SSIM"}), - }, - - } - - RETURN_TYPES = ("STRING", "LIST", "IMAGE",) - - def handler(self, image_stack, method): - import numpy as np - import torch - # if the dtype is not float32 - if image_stack.dtype != torch.float32: - image_stack = image_stack.to(torch.float32) - - image_stack_BCWH = image_stack.permute(0, 3, 1, 2) - image_stack_BCWH_DIFF = image_stack_BCWH[1:] - image_stack_BCWH[:-1] - - # we want to calculate the mean squared error - # of the image stack - # so we need to calculate the difference between each image in the stack and the previous image - # we have that in image_stack_BCWH_DIFF - # now we need to calculate the mean squared error of each image in the stack - # which will give us a tensor of shape (B) - # where B is the number of images in the stack - - if method == "MSE": - stack_mse_B = torch.mean(image_stack_BCWH_DIFF ** 2, dim=(1, 2, 3)) - # find the max possible value of the mean squared error - - elif method == "REGIONAL": - """ - this method resamples the image stack of B,W,H,C to B,8,8,C - then calculates the mean squared error of each region of the image - by further slicing into [b,1,1,C] x 64 regional representations - the greatest mean squared from each region is returned - """ - # the input image stack is defined as "image_stack" and it is a torch tensor, B,H,W,C - - B, H, W, C = image_stack.shape - levels = [3, 4, 5, 6, 7, 8, 16, 32, 64] - max_mse_value_in_all_regions_and_frames_for_this_level = 0 - level_stats = {k: {"max_mse": None, "max_std_dev": None} for k in levels} - for size in levels: - # need to re order the channels for the interpolation - resampled_stack = image_stack.permute(0, 3, 1, 2) - - # Resample the image stack to 8x8 pixels - resampled_stack = torch.nn.functional.interpolate(resampled_stack, size=(size, size), mode='bilinear', - align_corners=False) - - # but the channel order back to the original - resampled_stack = resampled_stack.permute(0, 2, 3, 1) - - # Reshape the resampled stack to (B, 64, C) - regional_stack = resampled_stack.view(B, size ** 2, C) - - # Calculate the mean squared difference between consecutive frames for each region - mse_values = torch.mean((regional_stack[1:] - regional_stack[:-1]) ** 2, dim=2) - - # max mse for each frame - max_mse_ea_frame_at_level = torch.max(mse_values, dim=1)[0] - max_dev_ea_frame_at_level = torch.std(mse_values, dim=1) - - # Calculate the standard deviation of the mean squared error for each region along the time dimension - max_std_dev_of_all_regions_over_time = torch.max(torch.std(mse_values, dim=0)) - - # in terms of video - # represents the maximum amount of change in the image stack, basically the maximum amount of motion - max_mse_value_in_all_regions_and_frames_for_this_level = torch.max(mse_values.view(-1)) - - level_stats[size]["max_r_mse_ea_frame"] = max_mse_ea_frame_at_level - level_stats[size]["max_r_dev_ea_frame"] = max_dev_ea_frame_at_level - level_stats[size]["max_mse"] = max_mse_value_in_all_regions_and_frames_for_this_level - level_stats[size]["max_std_dev"] = max_std_dev_of_all_regions_over_time - - all_l_max_mse_ea_frame = [level_stats[k]["max_r_mse_ea_frame"].tolist() for k in levels] - all_l_max_std_ea_frame = [level_stats[k]["max_r_dev_ea_frame"].tolist() for k in levels] - - max_l_r_mse_per_batch = [] # Initialize list to store the maximum mean squared error for each batch_num - for batch_num in range(B - 1): - max_value = float('-inf') # Initialize max_value to negative infinity for each batch_num - for level in levels: - mse_for_level_and_batch = level_stats[level]["max_r_mse_ea_frame"][batch_num] - # Update max_value if the current mse_for_level_and_batch is greater - if mse_for_level_and_batch > max_value: - max_value = mse_for_level_and_batch - max_l_r_mse_per_batch.append( - max_value) # Append the maximum value for the current batch_num to the list - - max_l_r_dev_per_batch = [] # Initialize list to store the maximum standard deviation for each batch_num - for batch_num in range(B - 1): - max_value = float('-inf') - for level in levels: - dev_for_level_and_batch = level_stats[level]["max_r_dev_ea_frame"][batch_num] - if dev_for_level_and_batch > max_value: - max_value = dev_for_level_and_batch - max_l_r_dev_per_batch.append(max_value) - - # all_f_max_mse_ea_frame = [level_stats[k]["max_mse_ea_frame"] for k in levels] - - max_mse_value_in_all_regions_and_frames_for_all_levels = torch.max(torch.tensor(all_l_max_mse_ea_frame)) - max_std_dev_value_in_all_regions_and_frames_for_all_levels = torch.max(torch.tensor(all_l_max_std_ea_frame)) - - out_str = f"max difference between any region over time: {max_mse_value_in_all_regions_and_frames_for_this_level}\nmax of how much the amount of motion changes over time: {max_std_dev_of_all_regions_over_time}" - out_list = [max_mse_value_in_all_regions_and_frames_for_all_levels, - max_std_dev_value_in_all_regions_and_frames_for_all_levels, - max_l_r_mse_per_batch, - max_l_r_dev_per_batch - ] - - # return the resampled image stack - - out_image = None - - return (out_str, out_list, out_image,) - - - - else: - # not implimented - raise NotImplementedError - - image_stack_diff_len = len(stack_mse_B) - required_image_info_width = 512 + image_stack_diff_len - - mean = torch.mean(stack_mse_B) - std = torch.std(stack_mse_B) - median = torch.median(stack_mse_B) - max_mse = torch.max(stack_mse_B) - min_mse = torch.min(stack_mse_B) - - out_str = f"mean: {mean}\n std: {std}\n median: {median}\n max: {max_mse}\n min: {min_mse}\n len: {image_stack_diff_len}" - - # create the histogram - hist_values, _ = np.histogram(stack_mse_B.numpy(), bins=min(100, image_stack_diff_len)) - hist_values = hist_values / hist_values.max() # Normalize to [0, 1] - - # Create an image tensor with shape (1, 512, required_width, 3) filled with zeros - out_hist_image = torch.zeros((1, 512, required_image_info_width, 3)) - - # Determine the scaling factors for width and height for the histogram - scale_width = 512 // len(hist_values) - scale_height = out_hist_image.shape[1] - - # Fill the image tensor with the histogram - for i, value in enumerate(hist_values): - height = 512 - int(value * scale_height) - start_x = int(i * scale_width) - end_x = int((i + 1) * scale_width) - out_hist_image[0, height:, start_x:end_x - 1, :] = 1 # Color as white - - # repeat each element of the stack_mse_B tensor by scale_width - stack_mse_B = stack_mse_B.repeat_interleave(scale_width) - - # Create a plot for the difference for each frame pair - plot_offset = 512 - for i, value in enumerate(stack_mse_B): - height = 512 - int(value * (scale_height / .5)) # Normalize by the max value - start_x = plot_offset + i - end_x = plot_offset + i + 1 - out_hist_image[0, height:, start_x:end_x, :] = (1 * height) / 512 # Color as white - - return (out_str, out_hist_image,) - - -# @profile -class VideoFileToImageStack(ABCABCVideoFileToImage, metaclass=ABCWidgetMetaclass): - """Use cv2 to open a video and save the videos individual frames""" - RETURN_TYPES = ("IMAGE", "FUNC",) - RETURN_NAMES = ("image_stack", "FUNC(**kwargs)",) - - def handler(self, video_in, idx_slice_start, idx_slice_stop, slice_idx_step, idx, func_only=False): - """ - Return slices of time from the video, sequences of images, defined by the start, stop and step - - torch tensors (T,H,W,C) - """ - import sys - import gc - # print memory usage - print(f"Memory usage: {sys.getsizeof(self)} bytes") - - if func_only: - return (None, VideoFileToImageStack.handler,) - - image_list = [] - video_in_fp = video_in - vidcap = cv2.VideoCapture(video_in_fp) - max_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT)) - - if not vidcap.isOpened(): - print("Error: Unable to open video file") - return - - start = (idx * slice_idx_step) + idx_slice_start - stop = (idx * slice_idx_step) + idx_slice_stop - slice_length = stop - start - - if (start + slice_length) > max_frames: - raise ValueError(f"idx_slice_stop ({idx_slice_stop}) is greater than the max frames ({max_frames})") - - for frame_count in range(start, start + slice_length): - # Seek to frame_count frame - vidcap.set(cv2.CAP_PROP_POS_FRAMES, frame_count) - - success, image = vidcap.read() - if not success: - print("Error: Unable to read frame from video file") - break - # image_list.append(image) - - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) - image_list.append(image) - print(f"Memory usage: {sys.getsizeof(self)} bytes") - - vidcap.release() - # Convert list of images to tensor (T,H,W,C) - try: - tensor = torch.stack([torch.tensor(img, dtype=torch.float32) for img in image_list]) - except RuntimeError as e: - print("Error: Unable to convert image list to tensor") - raise e - - tensor = tensor / 255.0 - del image_list - del vidcap - del image - gc.collect() - # print memory usage - print(f"Memory usage: {sys.getsizeof(self)} bytes") - return (tensor, VideoFileToImageStack.handler,) - - -class ImageStackToVideoFramesFolder(metaclass=ABCWidgetMetaclass): - """Really just saves the image stack to a folder""" - - @classmethod - def INPUT_TYPES(self): - return {"required": - { - "image_stack": ("IMAGE",), - "folder_out": ("STRING", {"multiline": False, "default": "video01"}), - }, - } - - RETURN_TYPES = ("STRING",) - CATEGORY = "video" - OUTPUT_NODE = True - FUNCTION = "handler" - - def handler(self, **kwargs): - import uuid - import shutil - - image_stack = kwargs["image_stack"] - folder_out = kwargs["folder_out"] - folder_out_path = video_dir - - # full path to the output folder - folder_out_path = os.path.join(video_dir, folder_out) - - # Create the output folder if it doesn't exist - if not os.path.exists(folder_out_path): - os.makedirs(folder_out_path) - - # move any current files in the output folder to a random folder inside the video folder - random_folder = os.path.join(folder_out_path, str(uuid.uuid4())) - os.makedirs(random_folder) - for file in os.listdir(folder_out_path): - file_path = os.path.join(folder_out_path, file) - if os.path.isfile(file_path): - shutil.move(file_path, random_folder) - - # Save each image in the image stack to the output folder - for i in range(image_stack.shape[0]): - image = image_stack[i].numpy() - # make sure the image is in the correct color format - image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) - # convert to 0-255 - image = image * 255 - # use zfill to get a good ordering of the frames - file_name = os.path.join(folder_out_path, "frame" + str(i).zfill(6) + ".jpg") - # use the best compression nearly lossless - cv2.imwrite(file_name, image, [cv2.IMWRITE_JPEG_QUALITY, 99]) - - image_stack = None - del image_stack - return (folder_out,) - - -class SmoothStackTemporal(ABCABCVideoFolderToImage, metaclass=ABCWidgetMetaclass): - """ - Smooth the temporal dimension of the image stack by calculating the distance between each frame - and the next frame and then averaging the frames that are within a certain distance of each other. - Supports adaptive mode with target deviation and baseline mode. - Can also perform frame interpolation using RIFE VFI. - """ - - rife_kwargs = { - "ckpt_name": "rife49.pth", - "clear_cache_after_n_frames": 1000, - "multiplier": 2, - "fast_mode": False, - "ensemble": False, - "scale_factor": 1.0, - "optional_interpolation_states": None, - } - - @classmethod - def INPUT_TYPES(cls): - return {"required": - { - "image_stack": ("IMAGE",), - "top_threshold": ("FLOAT", {"min": 0.0, "max": 100000.0, "step": 0.01, "default": 1.0}), - "bottom_threshold": ("FLOAT", {"min": 0.0, "max": 100000.0, "step": 0.01, "default": 1.0}), - "max_iters": ("INT", {"min": 0, "max": 1000, "step": 1, "default": 1}), - "target_deviation": ("FLOAT", {"min": 0.0, "max": 100000.0, "step": 0.01, "default": 0.0}), - "interp_frames": ("BOOLEAN", {"default": False}), - "mode": (["adaptive", "baseline"],), - } - } - - RETURN_TYPES = ("IMAGE", "STRING") - - def compute_stats(self, image_stack): - stats_handler = GetImageStackStatistics().handler - res = stats_handler(image_stack=image_stack, method="REGIONAL") - stats_str, stats_list, _ = res - region_diff = torch.tensor(stats_list[2]) # The list of each frame pairs maximum regional difference - max_diff = stats_list[0] # Overall maximum difference - max_dev = stats_list[1] # Overall regional deviation - return stats_str, region_diff, max_diff, max_dev - - def compute_baseline_motion(self, region_diff): - values, counts = torch.unique(region_diff, return_counts=True) - baseline_motion = values[torch.argmax(counts)].item() - return baseline_motion - - def normalize_diff(self, region_diff): - region_diff_range = region_diff.max() - region_diff.min() - if region_diff_range == 0: - region_diff_range = 1 # Avoid division by zero - region_diff_norm = (region_diff - region_diff.min()) / region_diff_range - return region_diff_norm - - def process_top_outliers(self, image_stack, region_diff_norm, top_threshold, rife_vfi): - import torch.nn.functional as F - - top_outliers = torch.where(region_diff_norm > top_threshold)[0] - new_image_stack = [] - - for idx in range(image_stack.shape[0]): - new_image_stack.append(image_stack[idx]) - if idx in top_outliers and idx < image_stack.shape[0] - 1: # Ensure we don't go out of bounds - frame_pair = image_stack[idx:idx + 2] - interpolated_frames = rife_vfi(frames=frame_pair, **self.rife_kwargs)[0] - - # Calculate the MSE for each interpolated frame with the first and last frames of the pair - mse_first = [F.mse_loss(interpolated_frame, frame_pair[0]) for interpolated_frame in - interpolated_frames] - mse_last = [F.mse_loss(interpolated_frame, frame_pair[1]) for interpolated_frame in interpolated_frames] - - # Find the interpolated frame with the smallest difference between mse_first and mse_last - mse_diff = [abs(mse_f - mse_l) for mse_f, mse_l in zip(mse_first, mse_last)] - middle_frame_index = torch.argmin(torch.tensor(mse_diff)).item() - - choice = interpolated_frames[middle_frame_index] - new_image_stack.append(choice) - - new_image_stack = torch.stack(new_image_stack) - return new_image_stack, len(top_outliers) - - def drop_bottom_outliers(self, image_stack, region_diff_norm, bottom_threshold): - bottom_outliers = torch.where(region_diff_norm < bottom_threshold)[0] - if len(bottom_outliers) > 0: - mask = torch.ones(image_stack.size(0), dtype=torch.bool) - mask[bottom_outliers + 1] = False - image_stack = image_stack[mask] - return image_stack, len(bottom_outliers) - - def log_info(self, iteration, top_outliers_count, bottom_outliers_count, mean_diff, std_diff, max_diff, max_dev, - stats_str): - info = f"Iteration {iteration}:\n" - info += f"Frames changed by top threshold: {top_outliers_count}\n" - info += f"Frames dropped by bottom threshold: {bottom_outliers_count}\n" - info += f"Mean of region_diff: {mean_diff}\n" - info += f"Std of region_diff: {std_diff}\n" - info += f"Overall max difference: {max_diff}\n" - info += f"Overall max deviation: {max_dev}\n" - info += f"Image stack statistics: {stats_str}\n\n" - return info - - def handler(self, image_stack, top_threshold=0.9, bottom_threshold=0.10, max_iters=1, target_deviation=0.0, - interp_frames=False, mode="adaptive"): - import torch - import nodes - - image_stack = image_stack.to(torch.float16) - log = "" - iteration = 0 - - if interp_frames: - rife_vfi = nodes.NODE_CLASS_MAPPINGS["RIFE VFI"]().vfi - - if mode == "baseline": - # Compute initial statistics and baseline motion - stats_str, region_diff, max_diff, max_dev = self.compute_stats(image_stack) - baseline_motion = self.compute_baseline_motion(region_diff) - target_deviation = baseline_motion - log += f"Baseline motion (mode of max regional differences): {baseline_motion}\n" - - while True: - # Step 1: Compute statistics - stats_str, region_diff, max_diff, max_dev = self.compute_stats(image_stack) - - # Step 2: Normalize differences - region_diff_norm = self.normalize_diff(region_diff) - - mean_diff = region_diff.mean().item() - std_diff = region_diff.std().item() - - # Step 3: Check if the target deviation is met or if max iterations are reached - if std_diff <= target_deviation or iteration >= max_iters: - log += f"std_diff <= target_deviation or iteration >= max_iters" - break - - # Step 4: Identify and process top outliers - image_stack, top_outliers_count = self.process_top_outliers(image_stack, region_diff_norm, top_threshold, - rife_vfi) - - # Step 5: Identify and drop bottom outliers - image_stack, bottom_outliers_count = self.drop_bottom_outliers(image_stack, region_diff_norm, - bottom_threshold) - - # Log information - log += self.log_info(iteration, top_outliers_count, bottom_outliers_count, mean_diff, std_diff, max_diff, - max_dev, stats_str) - - # Increment the iteration counter - iteration += 1 - - # Step 6: Check if the image stack is empty - if image_stack.shape[0] == 0: - return (None, "Image stack is empty after processing. No frames left.") - - return (image_stack.to(torch.float32), log) - - -class MovingCircle(metaclass=ABCWidgetMetaclass): - # VideoWidget class definition - - def calculate_start(self, x1, y1, width, height): - # Convert to 0-1 range - start_x = x1 * width - start_y = y1 * height - return (start_x, start_y) - - def calculate_end(self, x2, y2, width, height): - # Convert to 0-1 range - end_x = x2 * width - end_y = y2 * height - return (end_x, end_y) - - def generate_frames(self, num_frames, width, height): - # Match tensor shape from example - frames = torch.zeros(num_frames, width, height, 3) - return frames - - def draw_circle_or_oval(self, kp, width, height, radius, aspect_ratio, color=(255, 255, 255)): - import torch.nn.functional as F - # Initialize black background - img = torch.zeros((3, height, width), dtype=torch.uint8) - - # Draw circle as keypoints - keypoints = torch.tensor([[kp[0], kp[1]]]).unsqueeze(0) - img = draw_keypoints(img, keypoints, colors=color, radius=radius) - - # convert to (w,h,c) - # img = img.permute(1, 2, 0).unsqueeze(0) - img = img.unsqueeze(0) - - # Calculate new size - if aspect_ratio < 1: - new_size = (height, round(width / aspect_ratio)) - else: - new_size = (round(height * aspect_ratio), width) - - # Stretch the image - img_stretched = F.interpolate(img, size=new_size, mode='bilinear', align_corners=False) - - # Shrink it back - img_shrunk = F.interpolate(img_stretched, size=(height, width), mode='bilinear', align_corners=False) - - # Convert back to float values - - img_shrunk = img_shrunk.float() / 255 - - img_shrunk = img_shrunk.permute(0, 2, 3, 1) - return img_shrunk - - def draw_circle(self, kp, width, height, radius, color=(255, 255, 255)): - # Initialize black background - img = torch.zeros((3, height, width), dtype=torch.uint8) - # Draw circle as keypoints - keypoints = torch.tensor([[kp[0], kp[1]]]).unsqueeze(0) - img = draw_keypoints(img, keypoints, colors=color, radius=radius) - # convert to (w,h,c) - img = img.permute(1, 2, 0).unsqueeze(0) - # convert back to float values - img = img.float() / 255 - - return img - - # Metadata for documentation - RETURN_TYPES = ("IMAGE",) - CATEGORY = "ETK/video" - FUNCTION = "handler" - - # Match parent class method - @classmethod - def INPUT_TYPES(cls): - # Validate types, ranges - return {"required": - { - "x1": ("FLOAT", {"min": 0, "max": 1, "step": 0.1}), - "y1": ("FLOAT", {"min": 0, "max": 1, "step": 0.1}), - "x2": ("FLOAT", {"min": 0, "max": 1, "step": 0.1}), - "y2": ("FLOAT", {"min": 0, "max": 1, "step": 0.1}), - "num_frames": ("INT", {"min": 1}), - "width": ("INT", {"min": 1}), - "height": ("INT", {"min": 1}), - "radius": ("INT", {"min": 1}), - "color": ("STRING", {"default": "255 255 255"}), - "aspect_ratio": ("FLOAT", {"min": 0.1, "max": 10}) - - }} - - def handler(self, x1, y1, x2, y2, num_frames, width, height, radius, color="255 255 255", aspect_ratio=1): - import torch - # Avoid errors on bad values - assert 0 <= x1 <= 1 - assert 0 <= y1 <= 1 - - # Split calculations - start = torch.tensor(self.calculate_start(x1, y1, width, height)) - - # Split calculations - end = torch.tensor(self.calculate_end(x2, y2, width, height)) - - # color is supposed to be a tuple - color = tuple(map(lambda x: int(x), color.split())) - # Initialize for compositing - - frames = [] - for i in range(num_frames): - # Leverage PyTorch - cur_pos = start + (end - start) * i / num_frames - circle = self.draw_circle_or_oval(cur_pos, width, height, radius, aspect_ratio, color) - # Use broadcasting to overlay - frames.append(circle) - - # cat - frames = torch.cat(frames, dim=0) - - return (frames,) - - if __name__ == "__main__": - import doctest - - doctest.testmod() - - -class FindFrameFolders(metaclass=ABCWidgetMetaclass): - """Finds all folders directly off a given root path containing sequential image files""" - - @classmethod - def INPUT_TYPES(self): - return { - "required": { - "root_path": ("STRING", {"default": "path to root of project"}) - } - } - - RETURN_TYPES = ("LIST",) - RETURN_NAMES = ("image_sequence_folders",) - CATEGORY = "video" - OUTPUT_NODE = True - FUNCTION = "handler" - - def handler(self, **kwargs): - root_path = kwargs["root_path"] - image_sequence_folders = [] - - for folder_name in os.listdir(root_path): - folder_path = os.path.join(root_path, folder_name) - if os.path.isdir(folder_path): - image_files = [file for file in os.listdir(folder_path) if file.endswith(('.png', '.jpg', '.jpeg'))] - if len(image_files) >= 2: - image_sequence_folders.append(folder_path) - - return (image_sequence_folders,) - - -class CombineAudioAndVideoFiles(metaclass=ABCWidgetMetaclass): - """Combines lists of audio files and video files using imageio-ffmpeg""" - - @classmethod - 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": { - "move_to_folder": ("STRING", {"default": ""}), - } - } - - RETURN_TYPES = ("LIST",) - RETURN_NAMES = ("combined_video_paths",) - CATEGORY = "video" - OUTPUT_NODE = True - FUNCTION = "handler" - - @classmethod - def has_audio_stream(cls, video_file): - """Check if the video file has an audio stream.""" - import subprocess - command = ['ffprobe', '-v', 'error', '-select_streams', 'a', '-show_entries', 'stream=codec_name', '-of', - 'default=nw=1:nk=1', video_file] - try: - output = subprocess.check_output(command, stderr=subprocess.STDOUT) - return bool(output.strip()) - except subprocess.CalledProcessError: - return False - - def get_duration(self, file): - """Get the duration of a file in seconds.""" - import subprocess - - 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 as e: - print (f"Could not determine duration of file: {file}") - print (f"Command: {command}") - raise e - - def handler(self, **kwargs): - import subprocess - - 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 = 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 = 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 = '[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 = '' - output_args = ['-c:v', 'copy', '-map', '0:v:0', '-map', '1:a:0', '-c:a', 'aac', '-strict', - 'experimental', '-y', video_out_file_fp] - - 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: - print(f"audio duration: {audio_duration}, video duration: {video_duration}") - 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)) - - if video_out_file_fp != destination_path: - shutil.move(video_out_file_fp, destination_path) - - combined_video_paths.append(destination_path) - else: - combined_video_paths.append(video_out_file_fp) - - os.chdir("..") - os.chdir("..") - os.chdir("..") - - return (combined_video_paths,) - - -class CombineFoldersWithFramesToVideoFiles(metaclass=ABCWidgetMetaclass): - """Combines lists of folders containing frames of individual video files using imageio and ffmpeg to create video files""" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "folders_with_frames": ("LIST", {"default": []}), - "fps/length": ("FLOAT", {"default": 30, "min": 1, "max": 10000000000}), - "bitrate": ("STRING", {"default": "20000k"}), - "codec": ( - ["libx264", "libx265", "mpeg4", "vp9", "huffyuv", "flv1", "h264_nvenc", "hevc_nvenc", "rawvideo"], - {"default": "libx264"} - ), - "file_extension": ( - [".mp4", ".mov", ".avi", ".flv", ".mkv", ".webm"], - {"default": ".mp4"} - ), - "fps or length": (["fps", "length"], {"default": "fps"}), - }, - "optional": { - "[duration or fps]": ("LIST", {"default": []}), - "q": ("INT", {"default": 20, "min": 0, "max": 31}), - "pix_fmt": ("STRING", {"default": "yuv420p"}), - }, - } - - RETURN_TYPES = ("LIST", "STRING",) - RETURN_NAMES = ("combined_video_paths",) - CATEGORY = "video" - OUTPUT_NODE = True - FUNCTION = "handler" - - def handler(self, **kwargs): - 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: - 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: - fps = total_frames / duration_or_fps_list[i] - - create_video_from_images(folder_path, output_file, fps, codec, q, pix_fmt) - combined_video_paths.append(output_file) - except Exception as e: - import traceback - - # Capture full traceback information - full_traceback = traceback.format_exc() - messages.append(f"Error processing folder '{folder_path}': {str(e)}\nFull Traceback:\n{full_traceback}") - - 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 - - -def create_video_from_images(folder_path, output_file, fps, codec, q, pix_fmt): - import os - import subprocess - import shutil - import traceback - - # Assuming create_file_list is part of the larger framework, not redefining it. - list_file_path = create_file_list(folder_path) - - # Ensure ffmpeg is available in PATH - ffmpeg_exe = shutil.which('ffmpeg') - if ffmpeg_exe is None: - raise RuntimeError("FFmpeg is not found. Please ensure it is installed and available in the system's PATH.") - - # Normalize paths for cross-platform compatibility - list_file_path = os.path.normpath(list_file_path) - output_file = os.path.normpath(output_file) - - # FFmpeg command to create the video - 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: - # Capture both stdout and stderr to diagnose errors - subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - except subprocess.CalledProcessError as e: - # Capture the full traceback and the error output from the ffmpeg process - error_message = e.stderr.decode() if e.stderr else "No additional error information" - full_traceback = traceback.format_exc() - raise RuntimeError(f"ffmpeg failed with error: {error_message}\nFull Traceback:\n{full_traceback}") - - -class ImageStackToVideoFile(metaclass=ABCWidgetMetaclass): - """Really just saves the image stack to a video file""" - - @classmethod - def INPUT_TYPES(self): - return {"required": - { - "image_stack": ("IMAGE",), - "video_out": ("STRING", {"multiline": False, "default": "video01.mp4"}), - "fps or length": (["fps", "length"], {"default": "fps"}), - "fps/length": ("FLOAT", {"default": 30, "min": 1, "max": 10000000000}), - "bitrate": ("STRING", {"default": "20000k"}), - "codec": ( - ["libx264", "libx265", "mpeg4", "libvpx-vp9", "huffyuv", "flv1"], - {"default": "libx264"} - ), - }, - "optional": - { - "audio_file": ("STRING", {"default": "NONE"}), - }, - } - - RETURN_TYPES = ("FUNC", "STRING",) - RETURN_NAMES = ("FUNC", "video_out_path",) - CATEGORY = "video" - OUTPUT_NODE = True - FUNCTION = "handler" - - def handler(self, **kwargs): - import os - import numpy as np - import uuid - import shutil - import imageio - import audioread - import cv2 - import torchvision - - use_torchvision = False - - # video_dir = kwargs["video_dir"] - - audio_file = kwargs.get("audio_file") - if audio_file == "NONE": - audio_file = None - - image_stack = kwargs["image_stack"] - - fps_or_length = kwargs["fps or length"] - fps_length_value = kwargs["fps/length"] - bitrate = kwargs["bitrate"] - codec = kwargs["codec"] - video_out = kwargs["video_out"] - - if fps_or_length == "fps": - fps_length_value = np.array([fps_length_value], dtype=np.float16)[0] - - video_out_path = video_out - - if os.path.exists(video_out_path): - random_folder = os.path.join(os.path.dirname(video_out_path), str(uuid.uuid4())) - os.makedirs(random_folder) - shutil.move(video_out_path, random_folder) - - if audio_file is not None: - audio_file = audio_file - if not os.path.exists(audio_file): - # raise Exception(f"Audio file {audio_file} does not exist") - audio_length = 0 - else: - with audioread.audio_open(audio_file) as audio_info: - audio_length = audio_info.duration - - frames_length = image_stack.shape[0] - if fps_or_length == "fps": - video_length = frames_length / fps_length_value - else: - video_length = fps_length_value - - if fps_or_length == "fps": - video_fps = fps_length_value - else: - video_fps = frames_length / fps_length_value - - if use_torchvision == False: - writer = imageio.get_writer(video_out_path, fps=float(video_fps)) - - # convert the image stack to uint8 - image_stack = (image_stack * 255).to(torch.uint8) - - for frame_data in image_stack: - frame = np.array(frame_data) # Convert torch tensor to numpy array if needed - frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - writer.append_data(frame) - - if audio_file is not None: - difference = video_length - audio_length - - needed_extra_frames = int(difference * video_fps) - - if needed_extra_frames > 0: - for i in range(needed_extra_frames): - writer.append_data(frame) - - writer.close() - - args = [ - "-i", video_out_path, - "-i", audio_file, - "-c:v", codec, - "-b:v", str(bitrate), - "-c:a", "aac", - "-strict", "experimental", - "-y", - video_out_path.replace(".mp4", "_with_audio.mp4") - ] - run_ffmpeg_command(args) - else: - writer.close() - - if use_torchvision: - audio_tensor = None - if audio_file is not None: - audio_tensor, aud_sr = torchaudio.load(audio_file) - fps_frac = fractions.Fraction(float(video_fps)) - torchvision.io.write_video(filename=video_out_path, - video_array=image_stack * 255, - fps=fps_frac, - audio_array=audio_tensor, - video_codec=codec, - audio_codec="aac", - audio_fps=aud_sr - ) - - return (self, video_out_path,) - - -class CreateConsistentVideo(metaclass=ABCWidgetMetaclass): - """Creates a single video from a folder of videos with a consistent rate of change""" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "video_folder": ("STRING", {"default": ""}), - "rate_of_change_target": ("FLOAT", {"default": 0.5}), - } - } - - RETURN_TYPES = ("STRING", "LIST", "STRING",) - RETURN_NAMES = ("joined_video_path", "individual_video_paths", "error_messages") - CATEGORY = "video" - OUTPUT_NODE = True - FUNCTION = "handler" - - def handler(self, **kwargs): - import asyncio - import os - - video_folder = kwargs["video_folder"] - rate_of_change_target = kwargs["rate_of_change_target"] - - individual_video_paths = [] - error_messages = [] - - async def process_video(file_path): - try: - # Calculate visual differences - difference = calculate_visual_difference(file_path) - - # Adjust frame rate based on the rate of change target - adjusted_video_path = adjust_frame_rate( - video_path=file_path, - target_rate_of_change=rate_of_change_target, - visual_difference=difference - - ) - - return adjusted_video_path - - except Exception as e: - error_messages.append(str(e)) - return None - - # Gather video files - - video_files = [f for f in os.listdir(video_folder) if - f.lower().endswith(('.mp4', '.mov', '.avi', '.flv', '.mkv', '.webm'))] - # now get their full path - video_files = [os.path.join(video_folder, f) for f in video_files] - - # Use Asyncio to process videos in parallel - # There is no current loop so we need to create a NEW one - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - processed_videos = loop.run_until_complete(asyncio.gather( - *[process_video(file_path) for file_path in video_files] - )) - - # Combine processed videos into the final video - joined_video_path = combine_videos(processed_videos, video_folder, video_folder) - - return (joined_video_path, individual_video_paths, "\n".join(error_messages),) - - -def calculate_visual_difference(video_path): - # Open the video file - cap = cv2.VideoCapture(video_path) - if not cap.isOpened(): - raise Exception("Could not open the video file") - - # Initialize variables to hold previous frame - prev_frame_tensor = None - total_difference = 0 - - # Iterate through frames - while True: - ret, frame = cap.read() - if not ret: - break - - # Convert frame to a PyTorch tensor and normalize - frame_tensor = torch.from_numpy(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)).permute(2, 0, 1).float() / 255 - - # If previous frame exists, calculate the difference using SSIM - if prev_frame_tensor is not None: - difference = get_img_diff_torch(prev_frame_tensor, frame_tensor) - total_difference += difference - - # Update previous frame - prev_frame_tensor = frame_tensor - - # Close the video file - cap.release() - - return total_difference - - -def get_img_diff_torch(image1: torch.Tensor, image2: torch.Tensor, K1=0.01, K2=0.03, L=255): - # Convert images to grayscale using weighted average of color channels - gray1 = 0.2989 * image1[0] + 0.5870 * image1[1] + 0.1140 * image1[2] - gray2 = 0.2989 * image2[0] + 0.5870 * image2[1] + 0.1140 * image2[2] - - # Calculate mean and variance of input images - mu1 = torch.mean(gray1) - mu2 = torch.mean(gray2) - var1 = torch.var(gray1) - var2 = torch.var(gray2) - covar = np.cov(gray1.cpu().numpy().ravel(), gray2.cpu().numpy().ravel())[0][1] - - # Set constants - C1 = (K1 * L) ** 2 - C2 = (K2 * L) ** 2 - - # Calculate SSIM - numerator = (2 * mu1 * mu2 + C1) * (2 * covar + C2) - denominator = (mu1 ** 2 + mu2 ** 2 + C1) * (var1 + var2 + C2) - ssim = numerator / denominator - - # Return difference score - return 1 - ssim - - -def adjust_frame_rate(video_path, target_rate_of_change, visual_difference): - filename, _ = os.path.splitext(os.path.basename(video_path)) - base_dir = os.path.dirname(video_path) - output_filename = f"{filename}_adjusted.mp4" - output_filename = os.path.join(base_dir, output_filename) - - # use the full path - - cap = cv2.VideoCapture(video_path) - if not cap.isOpened(): - raise Exception("Could not open the video file") - - original_fps = int(cap.get(cv2.CAP_PROP_FPS)) - frame_size = (int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))) - - if visual_difference == 0: - desired_fps = float(original_fps) - else: - desired_fps = max(float(original_fps * target_rate_of_change / visual_difference), 1.0) - - desired_fps = int(desired_fps * 8) / 8 - - fourcc = cv2.VideoWriter_fourcc(*'mp4v') - - out = cv2.VideoWriter(output_filename, fourcc, desired_fps, frame_size) - - while True: - ret, frame = cap.read() - if not ret: - break - out.write(frame) - - cap.release() - out.release() - - return output_filename - - -def combine_videos(video_paths, base_input_directory, output_path): - import subprocess - import tempfile - - # Create a temporary text file - with tempfile.NamedTemporaryFile(mode="w+t", delete=False) as temp_file: - for video_path in video_paths: - temp_file.write(f"file '{video_path}'\n") - - # TODO: Check cross-platform compatibility (linux) - cmd = f"ffmpeg -f concat -safe 0 -i \"{temp_file.name}\" \"{output_path}combined{os.path.splitext(video_paths[0])[1]}\"" - - try: - subprocess.run(cmd, shell=True, check=True) - return output_path - except subprocess.CalledProcessError as e: - return str(e) - finally: - # Clean up the temporary file - temp_file.close() - os.remove(temp_file.name) - - -def transcribe_to_segs(input_filename): - import whisper as wh - import folder_paths as fp - - if not os.sep in input_filename: - input_dir = fp.input_directory - full_path = os.path.join(input_dir, input_filename) - else: - full_path = input_filename - # check that the file exists - - if not os.path.exists(full_path): - raise ValueError(f"File {full_path} does not exist.") - # check that it is a file - if not os.path.isfile(full_path): - raise ValueError(f"{full_path} is not a file.") - - model = wh.load_model("medium.en") - - res = wh.transcribe(model=model, audio=full_path, word_timestamps=True) - segs = res["segments"] - - return segs - - -class WhisperTranscription(metaclass=ABCWidgetMetaclass): - @classmethod - def INPUT_TYPES(cls): - return {"required": {"input_filename": ("STRING", {"default": "fullpath"})}} - - RETURN_TYPES = ("STRING", "STRING",) - RETURN_NAMES = ("lines_out", "words_out",) - FUNCTION = "transcribe_movie" - CATEGORY = "transcription" - - def transcribe_movie(self, input_filename): - - full_path = os.path.abspath(input_filename) - - try: - segs = transcribe_to_segs(full_path) - - lines_out = [] - for seg in segs: - lines_out.append(f"{seg['start']:3.3f} - {seg['end']:3.3f}: {seg['text']}") - - lines_out = '\n'.join(lines_out) - - words_out = [] - for seg in segs: - words_out.append([]) - for word in seg['words']: - line = f"{word['start']:3.3f} - {word['end']:3.3f}: {word['word']}" - words_out[-1].append(line) - - words_out = '\n'.join(['\n'.join(x) for x in words_out]) - - return (lines_out, words_out,) - - except Exception as e: - print(e) - return ("Error", str(e)) - - -class WhisperTranscribeToFrameIDXandWords(metaclass=ABCWidgetMetaclass): - """Just like WhisperTranscription, but returns a list of frame numbers and words instead of a string.""" - - @classmethod - def INPUT_TYPES(cls): - # needs full path to video file - # needs fps for conversion - ret = {"required": {"input_filename": ("STRING", {"default": "fullpath"}), - "fps": ("INT", {"default": 30})}} - return ret - - RETURN_TYPES = ("LIST", "LIST", "LIST",) - RETURN_NAMES = ("frame_idx", "words_out", "combined",) - FUNCTION = "transcribe_movie" - CATEGORY = "transcription" - - def transcribe_movie(self, input_filename, fps): - import folder_paths as fp - input_dir = fp.input_directory - full_path = os.path.join(input_dir, input_filename) - try: - segs = transcribe_to_segs(full_path) - frame_idx = [] - words_out = [] - combined = [] - for seg in segs: - for word in seg['words']: - start_frame = int(word['start'] * fps) - end_frame = int(word['end'] * fps) - word_text = word['word'] - - frame_idx.append((start_frame, end_frame)) - words_out.append(word_text) - combined.append((start_frame, end_frame, word_text)) - - # now we fill in the gaps so the entire video is covered by some word - # we do this by finding if there is a gap then changing the frame stamps of the last word and the next word - # to cover the gap - for i in range(len(frame_idx) - 1): - if frame_idx[i][1] < frame_idx[i + 1][0]: - # there is a gap - gap_len = frame_idx[i + 1][0] - frame_idx[i][1] - to_previous_word = gap_len // 2 - to_next_word = gap_len - to_previous_word - - frame_idx[i] = (frame_idx[i][0], frame_idx[i][1] + to_previous_word) - frame_idx[i + 1] = (frame_idx[i + 1][0] - to_next_word, frame_idx[i + 1][1]) - - combined[i] = (combined[i][0], combined[i][1] + to_previous_word, combined[i][2]) - combined[i + 1] = (combined[i + 1][0] - to_next_word, combined[i + 1][1], combined[i + 1][2]) - - # leave words_out alone - - return (frame_idx, words_out, combined,) - except Exception as e: - print(e) - return ("Error",) - - -# @profile -def get_text_frames(word_idx, words_data, x: callable, y: callable, size: (), text_kwargs: dict, style="default"): - from .image import TextRender as text_render_class - text_render = text_render_class().render_text - - start = words_data[word_idx][0] - end = words_data[word_idx][1] - word_text = words_data[word_idx][2] - frame_length = end - start - - # if x and y are not callables - if not callable(x): - x = lambda i: x - if not callable(y): - y = lambda i: y - - out_frames = [] - for i in range(start, end): - it_dict = dict() - it_dict["text"] = word_text - it_dict["x"] = x(i / float(frame_length)) - it_dict["y"] = y(i / float(frame_length)) - it_dict["width"] = size[0] - it_dict["height"] = size[1] - - it_dict.update(text_kwargs) - # it_dict["size"] = 128 - # it_dict["color"] = "#ffffff" - # it_dict["func_only"] = False - # it_dict["stroke fill"] = "#050505" - # it_dict["stroke width"] = 5 - - text_img = text_render(**it_dict) - out_frames.append(text_img[0]) - return out_frames - - -class AddTranscriptionToVideo(metaclass=ABCWidgetMetaclass): - """ - Add a transcription to a video. Requires a video and transcription data. - transcription data must be [(start_frame, end_frame, word), ...] - """ - - @classmethod - def INPUT_TYPES(cls): - # needs full path to video file - # needs fps for conversion - ret = {"required": {"video_file": ("STRING", {"default": "fullpath"}), - "words": ("LIST", {"default": None}), - } - } - return ret - - RETURN_TYPES = ("IMAGE", "IMAGE",) - RETURN_NAMES = ("1 fps preview", "Full Video") - FUNCTION = "add_transcription_to_video" - CATEGORY = "transcription" - - # @profile - def add_transcription_to_video(self, **kwargs): - import torch - import gc - - video_file = kwargs["video_file"] - words = kwargs["words"] - - def create_video_frames_folder(video_file): - fnc = VideoToFramesFolderFFMPEG().handler - call_dict = dict() - call_dict["video_in"] = video_file - call_dict["folder_out"] = os.path.join(video_file, os.sep, "frames_tmp") - call_dict["format"] = "PNG" - - tmp_video_folder = fnc(**call_dict)[0] - del fnc - del call_dict - gc.collect() - return tmp_video_folder - - def get_video_frames(start, end): - fnc = VideoFramesFolderToImageStack().handler - call_dict = dict() - call_dict["use_subfolders"] = False - call_dict["by_date_or_name"] = "NAME" - call_dict["folder_in"] = os.path.join(video_file, os.sep, "frames_tmp") - call_dict["idx_slice_start"] = 0 - call_dict["idx_slice_stop"] = end - start - call_dict["slice_idx_step"] = 1 - call_dict["idx"] = start - call_dict["use_float16"] = False - - ret = fnc(**call_dict)[0] - - return ret - - def overlay_text_on_video(video_frames, text_frames): - # Create a mask for the text frames (where text is black) - text_mask = (text_frames == 0) * 1 # Assuming black is represented as 0 in text_frames - - # Use the ~ operator to invert the boolean mask - # inverted_mask = text_mask - - # Overlay text_frames on video_frames using the inverted_mask - overlaid_frames = video_frames * text_mask + text_frames - - return overlaid_frames - - frames_out = [] - num_frames = 0 - frames_folder = create_video_frames_folder(video_file) - for w_idx in range(len(words)): - word_start = words[w_idx][0] - word_end = words[w_idx][1] - word = words[w_idx][2].strip() - print(f"word_start: {word_start}, word_end: {word_end}, word: {word}") - - words_video_frames = get_video_frames(word_start, word_end) - i_width = words_video_frames.shape[2] - i_height = words_video_frames.shape[1] - text_frames = get_text_frames(word_idx=w_idx, - words_data=words, - x=lambda i: 0, - y=lambda i: 0, - size=(i_width, i_height), - text_kwargs={"size": 128, "color": "#ffffff", "func_only": False, - "stroke fill": "#050505", "stroke width": 5, - "font_name": "c:/Windows/Fonts/arial.ttf"}) - - text_frames = torch.cat(text_frames) - - # Overlay text on video frames - video_frames_with_text = overlay_text_on_video(words_video_frames, text_frames) - frames_out.append(video_frames_with_text) - num_frames += video_frames_with_text.shape[0] - gc.collect() - - # free memory - del words_video_frames - del text_frames - del video_frames_with_text - - num_stks = len(frames_out) - # write to disk - for i, stk in enumerate(frames_out): - torch.save(stk, f"tmp_str_{i}.pt") - del frames_out - gc.collect() - torch.cuda.empty_cache() - - # pre allocate tensor - y = torch.zeros((num_frames, i_height, i_width, 3), dtype=torch.float32, device="cpu") - # load each save and put into tensor - new_start = 0 - for i in range(num_stks): - stk = torch.load(f"tmp_str_{i}.pt") - y[new_start:new_start + stk.shape[0]] = stk - new_start = new_start + stk.shape[0] - del stk - gc.collect() - torch.cuda.empty_cache() - - preview = y[::30].clone() - - return (preview, y,) - - -class TransformImageStack(metaclass=ABCWidgetMetaclass): - """ - Transform a stack of images - given x change (in pixels) - given y change (in pixels) - - - """ - - # TODO: support sheer,rotate,scale - # TODO: support transparancy - # TODO: support fill color specification - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"ImageStack": ("IMAGE", {"default": None}), - "pan x": ("INT", {"default": 0, "min": -2048}), - "pan y": ("INT", {"default": 0, "min": -2048}), - }, - } - return ret - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "transform_image_stack" - - def transform_image_stack(self, **kwargs): - import torchvision.transforms.functional as TF - - x_image = kwargs["ImageStack"] - x_image = x_image.detach().clone() - x = kwargs["pan x"] - y = kwargs["pan y"] - - time_index, H, W, C = x_image.shape # Assuming x_image has shape (time_index, H, W, C) - out_list = [] - - stop_x = x - stop_y = y - dx_step = stop_x / time_index - 1 - dy_step = stop_y / time_index - 1 - - for t in range(time_index): - tx = dx_step * t - ty = dy_step * t - - current_frame = x_image[t].detach().clone() - # Change from (H, W, C) to (C, H, W) - current_frame = current_frame.permute(2, 0, 1) - - transformed_frame = TF.affine(current_frame, angle=0, translate=(tx, ty), scale=1, - shear=[0, 0]).detach().clone() - - # Convert back to (H, W, C) - transformed_frame = transformed_frame.permute(1, 2, 0) - - # Store in the list - x_image[t] = transformed_frame - - return (x_image.detach().clone(),) - - -class PanByStep(metaclass=ABCWidgetMetaclass): - """ - Transform a stack of images - given x change (in pixels) - given y change (in pixels) - """ - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"ImageStack": ("IMAGE", {"default": None}), - "pan x": ("INT", {"default": 0, "min": -2048}), - "pan y": ("INT", {"default": 0, "min": -2048}), - "wrap": ("BOOLEAN", {"default": False}), - }, - } - return ret - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "pan_by_step" - - def pan_by_step(self, **kwargs): - import torchvision.transforms.functional as TF - - x_image = kwargs["ImageStack"] - x_image = x_image.detach().clone() - x = kwargs["pan x"] - y = kwargs["pan y"] - - wrap = kwargs["wrap"] - - time_index, H, W, C = x_image.shape # Assuming x_image has shape (time_index, H, W, C) - out_list = [] - - stop_x = x - stop_y = y - dx_step = 0 if x == 0 else stop_x / time_index - 1 - dy_step = stop_y / time_index - 1 - - for t in range(time_index): - tx = dx_step * t - ty = dy_step * t - - current_frame = x_image[t].detach().clone() - # Change from (H, W, C) to (C, H, W) - current_frame = current_frame.permute(2, 0, 1) - - # transformed_frame = TF.affine(current_frame, angle=0, translate=(tx, ty), scale=1, - # shear=[0, 0]).detach().clone() - - # If wrap is True, apply the wrapping operation - if wrap: - transformed_frame = current_frame.roll( - (int(tx) % W, int(ty) % H), dims=(2, 1) - ) - else: - transformed_frame = TF.affine(current_frame, angle=0, translate=(tx, ty), scale=1, - shear=[0, 0]).detach().clone() - - # Convert back to (H, W, C) - transformed_frame = transformed_frame.permute(1, 2, 0) - - # Store in the list - x_image[t] = transformed_frame - - return (x_image.detach().clone(),) - - -class RepeatFrames(metaclass=ABCWidgetMetaclass): - """ - does nothing more than repeat the frames a number of times - """ - - @classmethod - def INPUT_TYPES(cls): - ret = {"required": {"IMAGE": ("IMAGE", {"default": None}), - "repeat": ("INT", {"default": 1}), - }, - } - return ret - - RETURN_TYPES = ("IMAGE",) - RETURN_NAMES = ("repeated images",) - FUNCTION = "repeat_frames" - - def repeat_frames(self, **kwargs): - frames = kwargs["IMAGE"] - repeat = kwargs["repeat"] - return (frames.repeat(repeat, 1, 1, 1),) - - -class ListedSVDConditioning(metaclass=ABCWidgetMetaclass): - """ - repeats SVD_img2vid_Conditioning for each set of params - """ - - @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}) - base["required"]["video_frames"] = ("LIST", {"default": None}) - - return base - - RETURN_TYPES = ("LIST", "LIST", "LIST") - RETURN_NAMES = ("positive", "negative", "latent") - - FUNCTION = "listed_SVD_conditioning" - - def listed_SVD_conditioning(self, **kwargs): - from comfy_extras.nodes_video_model import SVD_img2vid_Conditioning - - # just like Listed_HoldFramesForSecsInVideoFolder, we will call SVD_img2vid_Conditioning for each set of params - - positives = [] - negatives = [] - latents = [] - - init_images = kwargs["init_image"] - clip_vision = kwargs["clip_vision"] - # vae - vae = kwargs["vae"] - # width and height - width = kwargs["width"] - height = kwargs["height"] - video_frames = kwargs["video_frames"] - motion_bucket_id = kwargs["motion_bucket_id"] - fps = kwargs["fps"] - augmentation_level = kwargs["augmentation_level"] - - lens = [len(init_images), len(video_frames)] - - # check if one length is divisible by the other - if lens[0] % lens[1] != 0 and lens[1] % lens[0] != 0: - raise ValueError("The length of init_image and video_frames must be divisible by each other") - - # repeat the shorter - if lens[0] < lens[1]: - init_images = init_images * (lens[1] // lens[0]) - else: - video_frames = video_frames * (lens[0] // lens[1]) - - # at this point the lengths should be the same - - for init_image, video_frame in zip(init_images, video_frames): - pos, neg, latent = SVD_img2vid_Conditioning().encode( - clip_vision=clip_vision, - init_image=init_image, - video_frames=video_frame, - vae=vae, - width=width, - height=height, - motion_bucket_id=motion_bucket_id, - fps=fps, - augmentation_level=augmentation_level - ) - - positives.append(pos) - negatives.append(neg) - latents.append(latent) - - return (positives, negatives, latents,) - - -class AddMetadataNode(metaclass=ABCWidgetMetaclass): - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "input_file": ("STRING", {"default": "input.mp4"}), - "output_file": ("STRING", {"default": "output.mp4"}), - "metadata": ("STRING", {"default": "title:My Video"}), - } - } - - RETURN_TYPES = ("STRING",) # This node returns a string message indicating success or failure - RETURN_NAMES = ("status_message",) # Providing a name for the return type - FUNCTION = "add_metadata" - CATEGORY = "video processing" - - def add_metadata(self, input_file, output_file, metadata): - from ffmpeg import FFmpeg - import shutil - - import torch # Importing torch as it seems to be a convention in your framework - # Parse the metadata from string to dictionary - # strip input and output file - input_file = input_file.strip() - output_file = output_file.strip() - - # so actually if the input_file and output_file are the same - # it will by default have an error - # co if this is the case copy then input file to a tempfile then use that as the input file - import tempfile - - if input_file == output_file: - # copy the input file to a temp file - with tempfile.NamedTemporaryFile(delete=False) as temp_file: - temp_file_name = temp_file.name - shutil.copy(input_file, temp_file_name) - input_file = temp_file_name - - metadata_dict = {} - try: - for line in metadata.splitlines(): - key, value = line.split(':', 1) # Split on the first colon - mdkey = "metadata" - # metadata_dict.append(f'metadata:{key.strip()}={value.strip()}') - metadata_dict[mdkey] = f'{key.strip()}={value.strip()}' - - # Run ffmpeg to add metadata - ffmpeg = FFmpeg().option("y") - try: - # make sure to tell it to overwrite outputfile - - ffmpeg.input(input_file).output(output_file, **metadata_dict).execute() - - return (f"status_message : Metadata added successfully to {output_file}.",) - except Exception as e: - return (f"status_message : Error adding metadata to {output_file}.{str(e)}",) - - return (f"status_message : Metadata added successfully to {output_file}.",) - except Exception as e: - return (f"status_message : Error adding metadata to {output_file}.",) - - @staticmethod - def describe(): - return "This node adds metadata to video files using ffmpeg. Metadata should be provided as 'key: value' pairs separated by newlines." diff --git a/video_v2.py b/video_v2.py deleted file mode 100644 index e69de29..0000000 diff --git a/web/marked.js b/web/marked.js deleted file mode 100644 index 3128e26..0000000 --- a/web/marked.js +++ /dev/null @@ -1,1191 +0,0 @@ -/** - * marked v12.0.1 - a markdown parser - * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) - * https://github.com/markedjs/marked - */ -!function (e, t) { - "object" == typeof exports && "undefined" != typeof module ? t(exports) : "function" == typeof define && define.amd ? define(["exports"], t) : t((e = "undefined" != typeof globalThis ? globalThis : e || self).marked = {}) -}(this, (function (e) { - "use strict"; - - function t() { - return { - async: !1, - breaks: !1, - extensions: null, - gfm: !0, - hooks: null, - pedantic: !1, - renderer: null, - silent: !1, - tokenizer: null, - walkTokens: null - } - } - - function n(t) { - e.defaults = t - } - - e.defaults = { - async: !1, - breaks: !1, - extensions: null, - gfm: !0, - hooks: null, - pedantic: !1, - renderer: null, - silent: !1, - tokenizer: null, - walkTokens: null - }; - const s = /[&<>"']/, r = new RegExp(s.source, "g"), i = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/, - l = new RegExp(i.source, "g"), o = {"&": "&", "<": "<", ">": ">", '"': """, "'": "'"}, - a = e => o[e]; - - function c(e, t) { - if (t) { - if (s.test(e)) return e.replace(r, a) - } else if (i.test(e)) return e.replace(l, a); - return e - } - - const h = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi; - - function p(e) { - return e.replace(h, ((e, t) => "colon" === (t = t.toLowerCase()) ? ":" : "#" === t.charAt(0) ? "x" === t.charAt(1) ? String.fromCharCode(parseInt(t.substring(2), 16)) : String.fromCharCode(+t.substring(1)) : "")) - } - - const u = /(^|[^\[])\^/g; - - function k(e, t) { - let n = "string" == typeof e ? e : e.source; - t = t || ""; - const s = { - replace: (e, t) => { - let r = "string" == typeof t ? t : t.source; - return r = r.replace(u, "$1"), n = n.replace(e, r), s - }, getRegex: () => new RegExp(n, t) - }; - return s - } - - function g(e) { - try { - e = encodeURI(e).replace(/%25/g, "%") - } catch (e) { - return null - } - return e - } - - const f = {exec: () => null}; - - function d(e, t) { - const n = e.replace(/\|/g, ((e, t, n) => { - let s = !1, r = t; - for (; --r >= 0 && "\\" === n[r];) s = !s; - return s ? "|" : " |" - })).split(/ \|/); - let s = 0; - if (n[0].trim() || n.shift(), n.length > 0 && !n[n.length - 1].trim() && n.pop(), t) if (n.length > t) n.splice(t); else for (; n.length < t;) n.push(""); - for (; s < n.length; s++) n[s] = n[s].trim().replace(/\\\|/g, "|"); - return n - } - - function x(e, t, n) { - const s = e.length; - if (0 === s) return ""; - let r = 0; - for (; r < s;) { - const i = e.charAt(s - r - 1); - if (i !== t || n) { - if (i === t || !n) break; - r++ - } else r++ - } - return e.slice(0, s - r) - } - - function b(e, t, n, s) { - const r = t.href, i = t.title ? c(t.title) : null, l = e[1].replace(/\\([\[\]])/g, "$1"); - if ("!" !== e[0].charAt(0)) { - s.state.inLink = !0; - const e = {type: "link", raw: n, href: r, title: i, text: l, tokens: s.inlineTokens(l)}; - return s.state.inLink = !1, e - } - return {type: "image", raw: n, href: r, title: i, text: c(l)} - } - - class w { - options; - rules; - lexer; - - constructor(t) { - this.options = t || e.defaults - } - - space(e) { - const t = this.rules.block.newline.exec(e); - if (t && t[0].length > 0) return {type: "space", raw: t[0]} - } - - code(e) { - const t = this.rules.block.code.exec(e); - if (t) { - const e = t[0].replace(/^ {1,4}/gm, ""); - return { - type: "code", - raw: t[0], - codeBlockStyle: "indented", - text: this.options.pedantic ? e : x(e, "\n") - } - } - } - - fences(e) { - const t = this.rules.block.fences.exec(e); - if (t) { - const e = t[0], n = function (e, t) { - const n = e.match(/^(\s+)(?:```)/); - if (null === n) return t; - const s = n[1]; - return t.split("\n").map((e => { - const t = e.match(/^\s+/); - if (null === t) return e; - const [n] = t; - return n.length >= s.length ? e.slice(s.length) : e - })).join("\n") - }(e, t[3] || ""); - return { - type: "code", - raw: e, - lang: t[2] ? t[2].trim().replace(this.rules.inline.anyPunctuation, "$1") : t[2], - text: n - } - } - } - - heading(e) { - const t = this.rules.block.heading.exec(e); - if (t) { - let e = t[2].trim(); - if (/#$/.test(e)) { - const t = x(e, "#"); - this.options.pedantic ? e = t.trim() : t && !/ $/.test(t) || (e = t.trim()) - } - return {type: "heading", raw: t[0], depth: t[1].length, text: e, tokens: this.lexer.inline(e)} - } - } - - hr(e) { - const t = this.rules.block.hr.exec(e); - if (t) return {type: "hr", raw: t[0]} - } - - blockquote(e) { - const t = this.rules.block.blockquote.exec(e); - if (t) { - const e = x(t[0].replace(/^ *>[ \t]?/gm, ""), "\n"), n = this.lexer.state.top; - this.lexer.state.top = !0; - const s = this.lexer.blockTokens(e); - return this.lexer.state.top = n, {type: "blockquote", raw: t[0], tokens: s, text: e} - } - } - - list(e) { - let t = this.rules.block.list.exec(e); - if (t) { - let n = t[1].trim(); - const s = n.length > 1, - r = {type: "list", raw: "", ordered: s, start: s ? +n.slice(0, -1) : "", loose: !1, items: []}; - n = s ? `\\d{1,9}\\${n.slice(-1)}` : `\\${n}`, this.options.pedantic && (n = s ? n : "[*+-]"); - const i = new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`); - let l = "", o = "", a = !1; - for (; e;) { - let n = !1; - if (!(t = i.exec(e))) break; - if (this.rules.block.hr.test(e)) break; - l = t[0], e = e.substring(l.length); - let s = t[2].split("\n", 1)[0].replace(/^\t+/, (e => " ".repeat(3 * e.length))), - c = e.split("\n", 1)[0], h = 0; - this.options.pedantic ? (h = 2, o = s.trimStart()) : (h = t[2].search(/[^ ]/), h = h > 4 ? 1 : h, o = s.slice(h), h += t[1].length); - let p = !1; - if (!s && /^ *$/.test(c) && (l += c + "\n", e = e.substring(c.length + 1), n = !0), !n) { - const t = new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`), - n = new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`), - r = new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`), - i = new RegExp(`^ {0,${Math.min(3,h-1)}}#`); - for (; e;) { - const a = e.split("\n", 1)[0]; - if (c = a, this.options.pedantic && (c = c.replace(/^ {1,4}(?=( {4})*[^ ])/g, " ")), r.test(c)) break; - if (i.test(c)) break; - if (t.test(c)) break; - if (n.test(e)) break; - if (c.search(/[^ ]/) >= h || !c.trim()) o += "\n" + c.slice(h); else { - if (p) break; - if (s.search(/[^ ]/) >= 4) break; - if (r.test(s)) break; - if (i.test(s)) break; - if (n.test(s)) break; - o += "\n" + c - } - p || c.trim() || (p = !0), l += a + "\n", e = e.substring(a.length + 1), s = c.slice(h) - } - } - r.loose || (a ? r.loose = !0 : /\n *\n *$/.test(l) && (a = !0)); - let u, k = null; - this.options.gfm && (k = /^\[[ xX]\] /.exec(o), k && (u = "[ ] " !== k[0], o = o.replace(/^\[[ xX]\] +/, ""))), r.items.push({ - type: "list_item", - raw: l, - task: !!k, - checked: u, - loose: !1, - text: o, - tokens: [] - }), r.raw += l - } - r.items[r.items.length - 1].raw = l.trimEnd(), r.items[r.items.length - 1].text = o.trimEnd(), r.raw = r.raw.trimEnd(); - for (let e = 0; e < r.items.length; e++) if (this.lexer.state.top = !1, r.items[e].tokens = this.lexer.blockTokens(r.items[e].text, []), !r.loose) { - const t = r.items[e].tokens.filter((e => "space" === e.type)), - n = t.length > 0 && t.some((e => /\n.*\n/.test(e.raw))); - r.loose = n - } - if (r.loose) for (let e = 0; e < r.items.length; e++) r.items[e].loose = !0; - return r - } - } - - html(e) { - const t = this.rules.block.html.exec(e); - if (t) { - return { - type: "html", - block: !0, - raw: t[0], - pre: "pre" === t[1] || "script" === t[1] || "style" === t[1], - text: t[0] - } - } - } - - def(e) { - const t = this.rules.block.def.exec(e); - if (t) { - const e = t[1].toLowerCase().replace(/\s+/g, " "), - n = t[2] ? t[2].replace(/^<(.*)>$/, "$1").replace(this.rules.inline.anyPunctuation, "$1") : "", - s = t[3] ? t[3].substring(1, t[3].length - 1).replace(this.rules.inline.anyPunctuation, "$1") : t[3]; - return {type: "def", tag: e, raw: t[0], href: n, title: s} - } - } - - table(e) { - const t = this.rules.block.table.exec(e); - if (!t) return; - if (!/[:|]/.test(t[2])) return; - const n = d(t[1]), s = t[2].replace(/^\||\| *$/g, "").split("|"), - r = t[3] && t[3].trim() ? t[3].replace(/\n[ \t]*$/, "").split("\n") : [], - i = {type: "table", raw: t[0], header: [], align: [], rows: []}; - if (n.length === s.length) { - for (const e of s) /^ *-+: *$/.test(e) ? i.align.push("right") : /^ *:-+: *$/.test(e) ? i.align.push("center") : /^ *:-+ *$/.test(e) ? i.align.push("left") : i.align.push(null); - for (const e of n) i.header.push({text: e, tokens: this.lexer.inline(e)}); - for (const e of r) i.rows.push(d(e, i.header.length).map((e => ({ - text: e, - tokens: this.lexer.inline(e) - })))); - return i - } - } - - lheading(e) { - const t = this.rules.block.lheading.exec(e); - if (t) return { - type: "heading", - raw: t[0], - depth: "=" === t[2].charAt(0) ? 1 : 2, - text: t[1], - tokens: this.lexer.inline(t[1]) - } - } - - paragraph(e) { - const t = this.rules.block.paragraph.exec(e); - if (t) { - const e = "\n" === t[1].charAt(t[1].length - 1) ? t[1].slice(0, -1) : t[1]; - return {type: "paragraph", raw: t[0], text: e, tokens: this.lexer.inline(e)} - } - } - - text(e) { - const t = this.rules.block.text.exec(e); - if (t) return {type: "text", raw: t[0], text: t[0], tokens: this.lexer.inline(t[0])} - } - - escape(e) { - const t = this.rules.inline.escape.exec(e); - if (t) return {type: "escape", raw: t[0], text: c(t[1])} - } - - tag(e) { - const t = this.rules.inline.tag.exec(e); - if (t) return !this.lexer.state.inLink && /^/i.test(t[0]) && (this.lexer.state.inLink = !1), !this.lexer.state.inRawBlock && /^<(pre|code|kbd|script)(\s|>)/i.test(t[0]) ? this.lexer.state.inRawBlock = !0 : this.lexer.state.inRawBlock && /^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0]) && (this.lexer.state.inRawBlock = !1), { - type: "html", - raw: t[0], - inLink: this.lexer.state.inLink, - inRawBlock: this.lexer.state.inRawBlock, - block: !1, - text: t[0] - } - } - - link(e) { - const t = this.rules.inline.link.exec(e); - if (t) { - const e = t[2].trim(); - if (!this.options.pedantic && /^$/.test(e)) return; - const t = x(e.slice(0, -1), "\\"); - if ((e.length - t.length) % 2 == 0) return - } else { - const e = function (e, t) { - if (-1 === e.indexOf(t[1])) return -1; - let n = 0; - for (let s = 0; s < e.length; s++) if ("\\" === e[s]) s++; else if (e[s] === t[0]) n++; else if (e[s] === t[1] && (n--, n < 0)) return s; - return -1 - }(t[2], "()"); - if (e > -1) { - const n = (0 === t[0].indexOf("!") ? 5 : 4) + t[1].length + e; - t[2] = t[2].substring(0, e), t[0] = t[0].substring(0, n).trim(), t[3] = "" - } - } - let n = t[2], s = ""; - if (this.options.pedantic) { - const e = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n); - e && (n = e[1], s = e[3]) - } else s = t[3] ? t[3].slice(1, -1) : ""; - return n = n.trim(), /^$/.test(e) ? n.slice(1) : n.slice(1, -1)), b(t, { - href: n ? n.replace(this.rules.inline.anyPunctuation, "$1") : n, - title: s ? s.replace(this.rules.inline.anyPunctuation, "$1") : s - }, t[0], this.lexer) - } - } - - reflink(e, t) { - let n; - if ((n = this.rules.inline.reflink.exec(e)) || (n = this.rules.inline.nolink.exec(e))) { - const e = t[(n[2] || n[1]).replace(/\s+/g, " ").toLowerCase()]; - if (!e) { - const e = n[0].charAt(0); - return {type: "text", raw: e, text: e} - } - return b(n, e, n[0], this.lexer) - } - } - - emStrong(e, t, n = "") { - let s = this.rules.inline.emStrongLDelim.exec(e); - if (!s) return; - if (s[3] && n.match(/[\p{L}\p{N}]/u)) return; - if (!(s[1] || s[2] || "") || !n || this.rules.inline.punctuation.exec(n)) { - const n = [...s[0]].length - 1; - let r, i, l = n, o = 0; - const a = "*" === s[0][0] ? this.rules.inline.emStrongRDelimAst : this.rules.inline.emStrongRDelimUnd; - for (a.lastIndex = 0, t = t.slice(-1 * e.length + n); null != (s = a.exec(t));) { - if (r = s[1] || s[2] || s[3] || s[4] || s[5] || s[6], !r) continue; - if (i = [...r].length, s[3] || s[4]) { - l += i; - continue - } - if ((s[5] || s[6]) && n % 3 && !((n + i) % 3)) { - o += i; - continue - } - if (l -= i, l > 0) continue; - i = Math.min(i, i + l + o); - const t = [...s[0]][0].length, a = e.slice(0, n + s.index + t + i); - if (Math.min(n, i) % 2) { - const e = a.slice(1, -1); - return {type: "em", raw: a, text: e, tokens: this.lexer.inlineTokens(e)} - } - const c = a.slice(2, -2); - return {type: "strong", raw: a, text: c, tokens: this.lexer.inlineTokens(c)} - } - } - } - - codespan(e) { - const t = this.rules.inline.code.exec(e); - if (t) { - let e = t[2].replace(/\n/g, " "); - const n = /[^ ]/.test(e), s = /^ /.test(e) && / $/.test(e); - return n && s && (e = e.substring(1, e.length - 1)), e = c(e, !0), { - type: "codespan", - raw: t[0], - text: e - } - } - } - - br(e) { - const t = this.rules.inline.br.exec(e); - if (t) return {type: "br", raw: t[0]} - } - - del(e) { - const t = this.rules.inline.del.exec(e); - if (t) return {type: "del", raw: t[0], text: t[2], tokens: this.lexer.inlineTokens(t[2])} - } - - autolink(e) { - const t = this.rules.inline.autolink.exec(e); - if (t) { - let e, n; - return "@" === t[2] ? (e = c(t[1]), n = "mailto:" + e) : (e = c(t[1]), n = e), { - type: "link", - raw: t[0], - text: e, - href: n, - tokens: [{type: "text", raw: e, text: e}] - } - } - } - - url(e) { - let t; - if (t = this.rules.inline.url.exec(e)) { - let e, n; - if ("@" === t[2]) e = c(t[0]), n = "mailto:" + e; else { - let s; - do { - s = t[0], t[0] = this.rules.inline._backpedal.exec(t[0])?.[0] ?? "" - } while (s !== t[0]); - e = c(t[0]), n = "www." === t[1] ? "http://" + t[0] : t[0] - } - return {type: "link", raw: t[0], text: e, href: n, tokens: [{type: "text", raw: e, text: e}]} - } - } - - inlineText(e) { - const t = this.rules.inline.text.exec(e); - if (t) { - let e; - return e = this.lexer.state.inRawBlock ? t[0] : c(t[0]), {type: "text", raw: t[0], text: e} - } - } - } - - const m = /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/, y = /(?:[*+-]|\d{1,9}[.)])/, - $ = k(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g, y).replace(/blockCode/g, / {4}/).replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g, / {0,3}>/).replace(/heading/g, / {0,3}#{1,6}/).replace(/html/g, / {0,3}<[^\n>]+>\n/).getRegex(), - z = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/, - T = /(?!\s*\])(?:\\.|[^\[\]\\])+/, - R = k(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label", T).replace("title", /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(), - _ = k(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g, y).getRegex(), - A = "address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul", - S = /|$))/, - I = k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))", "i").replace("comment", S).replace("tag", A).replace("attribute", / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(), - E = k(z).replace("hr", m).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("|lheading", "").replace("|table", "").replace("blockquote", " {0,3}>").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)]) ").replace("html", ")|<(?:script|pre|style|textarea|!--)").replace("tag", A).getRegex(), - q = { - blockquote: k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph", E).getRegex(), - code: /^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/, - def: R, - fences: /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/, - heading: /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/, - hr: m, - html: I, - lheading: $, - list: _, - newline: /^(?: *(?:\n|$))+/, - paragraph: E, - table: f, - text: /^[^\n]+/ - }, - Z = k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr", m).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("blockquote", " {0,3}>").replace("code", " {4}[^\\n]").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)]) ").replace("html", ")|<(?:script|pre|style|textarea|!--)").replace("tag", A).getRegex(), - L = { - ...q, - table: Z, - paragraph: k(z).replace("hr", m).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("|lheading", "").replace("table", Z).replace("blockquote", " {0,3}>").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)]) ").replace("html", ")|<(?:script|pre|style|textarea|!--)").replace("tag", A).getRegex() - }, P = { - ...q, - html: k("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment", S).replace(/tag/g, "(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(), - def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, - heading: /^(#{1,6})(.*)(?:\n+|$)/, - fences: f, - lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, - paragraph: k(z).replace("hr", m).replace("heading", " *#{1,6} *[^\n]").replace("lheading", $).replace("|table", "").replace("blockquote", " {0,3}>").replace("|fences", "").replace("|list", "").replace("|html", "").replace("|tag", "").getRegex() - }, Q = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/, v = /^( {2,}|\\)\n(?!\s*$)/, B = "\\p{P}\\p{S}", - C = k(/^((?![*_])[\spunctuation])/, "u").replace(/punctuation/g, B).getRegex(), - M = k(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/, "u").replace(/punct/g, B).getRegex(), - O = k("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])", "gu").replace(/punct/g, B).getRegex(), - D = k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])", "gu").replace(/punct/g, B).getRegex(), - j = k(/\\([punct])/, "gu").replace(/punct/g, B).getRegex(), - H = k(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme", /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email", /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(), - U = k(S).replace("(?:--\x3e|$)", "--\x3e").getRegex(), - X = k("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment", U).replace("attribute", /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(), - F = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/, - N = k(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label", F).replace("href", /<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title", /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(), - G = k(/^!?\[(label)\]\[(ref)\]/).replace("label", F).replace("ref", T).getRegex(), - J = k(/^!?\[(ref)\](?:\[\])?/).replace("ref", T).getRegex(), K = { - _backpedal: f, - anyPunctuation: j, - autolink: H, - blockSkip: /\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g, - br: v, - code: /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/, - del: f, - emStrongLDelim: M, - emStrongRDelimAst: O, - emStrongRDelimUnd: D, - escape: Q, - link: N, - nolink: J, - punctuation: C, - reflink: G, - reflinkSearch: k("reflink|nolink(?!\\()", "g").replace("reflink", G).replace("nolink", J).getRegex(), - tag: X, - text: /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\ t + " ".repeat(n.length))); e;) if (!(this.options.extensions && this.options.extensions.block && this.options.extensions.block.some((s => !!(n = s.call({lexer: this}, e, t)) && (e = e.substring(n.raw.length), t.push(n), !0))))) if (n = this.tokenizer.space(e)) e = e.substring(n.raw.length), 1 === n.raw.length && t.length > 0 ? t[t.length - 1].raw += "\n" : t.push(n); else if (n = this.tokenizer.code(e)) e = e.substring(n.raw.length), s = t[t.length - 1], !s || "paragraph" !== s.type && "text" !== s.type ? t.push(n) : (s.raw += "\n" + n.raw, s.text += "\n" + n.text, this.inlineQueue[this.inlineQueue.length - 1].src = s.text); else if (n = this.tokenizer.fences(e)) e = e.substring(n.raw.length), t.push(n); else if (n = this.tokenizer.heading(e)) e = e.substring(n.raw.length), t.push(n); else if (n = this.tokenizer.hr(e)) e = e.substring(n.raw.length), t.push(n); else if (n = this.tokenizer.blockquote(e)) e = e.substring(n.raw.length), t.push(n); else if (n = this.tokenizer.list(e)) e = e.substring(n.raw.length), t.push(n); else if (n = this.tokenizer.html(e)) e = e.substring(n.raw.length), t.push(n); else if (n = this.tokenizer.def(e)) e = e.substring(n.raw.length), s = t[t.length - 1], !s || "paragraph" !== s.type && "text" !== s.type ? this.tokens.links[n.tag] || (this.tokens.links[n.tag] = { - href: n.href, - title: n.title - }) : (s.raw += "\n" + n.raw, s.text += "\n" + n.raw, this.inlineQueue[this.inlineQueue.length - 1].src = s.text); else if (n = this.tokenizer.table(e)) e = e.substring(n.raw.length), t.push(n); else if (n = this.tokenizer.lheading(e)) e = e.substring(n.raw.length), t.push(n); else { - if (r = e, this.options.extensions && this.options.extensions.startBlock) { - let t = 1 / 0; - const n = e.slice(1); - let s; - this.options.extensions.startBlock.forEach((e => { - s = e.call({lexer: this}, n), "number" == typeof s && s >= 0 && (t = Math.min(t, s)) - })), t < 1 / 0 && t >= 0 && (r = e.substring(0, t + 1)) - } - if (this.state.top && (n = this.tokenizer.paragraph(r))) s = t[t.length - 1], i && "paragraph" === s.type ? (s.raw += "\n" + n.raw, s.text += "\n" + n.text, this.inlineQueue.pop(), this.inlineQueue[this.inlineQueue.length - 1].src = s.text) : t.push(n), i = r.length !== e.length, e = e.substring(n.raw.length); else if (n = this.tokenizer.text(e)) e = e.substring(n.raw.length), s = t[t.length - 1], s && "text" === s.type ? (s.raw += "\n" + n.raw, s.text += "\n" + n.text, this.inlineQueue.pop(), this.inlineQueue[this.inlineQueue.length - 1].src = s.text) : t.push(n); else if (e) { - const t = "Infinite loop on byte: " + e.charCodeAt(0); - if (this.options.silent) { - console.error(t); - break - } - throw new Error(t) - } - } - return this.state.top = !0, t - } - - inline(e, t = []) { - return this.inlineQueue.push({src: e, tokens: t}), t - } - - inlineTokens(e, t = []) { - let n, s, r, i, l, o, a = e; - if (this.tokens.links) { - const e = Object.keys(this.tokens.links); - if (e.length > 0) for (; null != (i = this.tokenizer.rules.inline.reflinkSearch.exec(a));) e.includes(i[0].slice(i[0].lastIndexOf("[") + 1, -1)) && (a = a.slice(0, i.index) + "[" + "a".repeat(i[0].length - 2) + "]" + a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex)) - } - for (; null != (i = this.tokenizer.rules.inline.blockSkip.exec(a));) a = a.slice(0, i.index) + "[" + "a".repeat(i[0].length - 2) + "]" + a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex); - for (; null != (i = this.tokenizer.rules.inline.anyPunctuation.exec(a));) a = a.slice(0, i.index) + "++" + a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex); - for (; e;) if (l || (o = ""), l = !1, !(this.options.extensions && this.options.extensions.inline && this.options.extensions.inline.some((s => !!(n = s.call({lexer: this}, e, t)) && (e = e.substring(n.raw.length), t.push(n), !0))))) if (n = this.tokenizer.escape(e)) e = e.substring(n.raw.length), t.push(n); else if (n = this.tokenizer.tag(e)) e = e.substring(n.raw.length), s = t[t.length - 1], s && "text" === n.type && "text" === s.type ? (s.raw += n.raw, s.text += n.text) : t.push(n); else if (n = this.tokenizer.link(e)) e = e.substring(n.raw.length), t.push(n); else if (n = this.tokenizer.reflink(e, this.tokens.links)) e = e.substring(n.raw.length), s = t[t.length - 1], s && "text" === n.type && "text" === s.type ? (s.raw += n.raw, s.text += n.text) : t.push(n); else if (n = this.tokenizer.emStrong(e, a, o)) e = e.substring(n.raw.length), t.push(n); else if (n = this.tokenizer.codespan(e)) e = e.substring(n.raw.length), t.push(n); else if (n = this.tokenizer.br(e)) e = e.substring(n.raw.length), t.push(n); else if (n = this.tokenizer.del(e)) e = e.substring(n.raw.length), t.push(n); else if (n = this.tokenizer.autolink(e)) e = e.substring(n.raw.length), t.push(n); else if (this.state.inLink || !(n = this.tokenizer.url(e))) { - if (r = e, this.options.extensions && this.options.extensions.startInline) { - let t = 1 / 0; - const n = e.slice(1); - let s; - this.options.extensions.startInline.forEach((e => { - s = e.call({lexer: this}, n), "number" == typeof s && s >= 0 && (t = Math.min(t, s)) - })), t < 1 / 0 && t >= 0 && (r = e.substring(0, t + 1)) - } - if (n = this.tokenizer.inlineText(r)) e = e.substring(n.raw.length), "_" !== n.raw.slice(-1) && (o = n.raw.slice(-1)), l = !0, s = t[t.length - 1], s && "text" === s.type ? (s.raw += n.raw, s.text += n.text) : t.push(n); else if (e) { - const t = "Infinite loop on byte: " + e.charCodeAt(0); - if (this.options.silent) { - console.error(t); - break - } - throw new Error(t) - } - } else e = e.substring(n.raw.length), t.push(n); - return t - } - } - - class se { - options; - - constructor(t) { - this.options = t || e.defaults - } - - code(e, t, n) { - const s = (t || "").match(/^\S*/)?.[0]; - return e = e.replace(/\n$/, "") + "\n", s ? '
' + (n ? e : c(e, !0)) + "
\n" : "
" + (n ? e : c(e, !0)) + "
\n" - } - - blockquote(e) { - return `
\n${e}
\n` - } - - html(e, t) { - return e - } - - heading(e, t, n) { - return `${e}\n` - } - - hr() { - return "
\n" - } - - list(e, t, n) { - const s = t ? "ol" : "ul"; - return "<" + s + (t && 1 !== n ? ' start="' + n + '"' : "") + ">\n" + e + "\n" - } - - listitem(e, t, n) { - return `
  • ${e}
  • \n` - } - - checkbox(e) { - return "' - } - - paragraph(e) { - return `

    ${e}

    \n` - } - - table(e, t) { - return t && (t = `${t}`), "\n\n" + e + "\n" + t + "
    \n" - } - - tablerow(e) { - return `\n${e}\n` - } - - tablecell(e, t) { - const n = t.header ? "th" : "td"; - return (t.align ? `<${n} align="${t.align}">` : `<${n}>`) + e + `\n` - } - - strong(e) { - return `${e}` - } - - em(e) { - return `${e}` - } - - codespan(e) { - return `${e}` - } - - br() { - return "
    " - } - - del(e) { - return `${e}` - } - - link(e, t, n) { - const s = g(e); - if (null === s) return n; - let r = '
    ", r - } - - image(e, t, n) { - const s = g(e); - if (null === s) return n; - let r = `${n} 0 && "paragraph" === n.tokens[0].type ? (n.tokens[0].text = e + " " + n.tokens[0].text, n.tokens[0].tokens && n.tokens[0].tokens.length > 0 && "text" === n.tokens[0].tokens[0].type && (n.tokens[0].tokens[0].text = e + " " + n.tokens[0].tokens[0].text)) : n.tokens.unshift({ - type: "text", - text: e + " " - }) : o += e + " " - } - o += this.parse(n.tokens, i), l += this.renderer.listitem(o, r, !!s) - } - n += this.renderer.list(l, t, s); - continue - } - case"html": { - const e = r; - n += this.renderer.html(e.text, e.block); - continue - } - case"paragraph": { - const e = r; - n += this.renderer.paragraph(this.parseInline(e.tokens)); - continue - } - case"text": { - let i = r, l = i.tokens ? this.parseInline(i.tokens) : i.text; - for (; s + 1 < e.length && "text" === e[s + 1].type;) i = e[++s], l += "\n" + (i.tokens ? this.parseInline(i.tokens) : i.text); - n += t ? this.renderer.paragraph(l) : l; - continue - } - default: { - const e = 'Token with "' + r.type + '" type was not found.'; - if (this.options.silent) return console.error(e), ""; - throw new Error(e) - } - } - } - return n - } - - parseInline(e, t) { - t = t || this.renderer; - let n = ""; - for (let s = 0; s < e.length; s++) { - const r = e[s]; - if (this.options.extensions && this.options.extensions.renderers && this.options.extensions.renderers[r.type]) { - const e = this.options.extensions.renderers[r.type].call({parser: this}, r); - if (!1 !== e || !["escape", "html", "link", "image", "strong", "em", "codespan", "br", "del", "text"].includes(r.type)) { - n += e || ""; - continue - } - } - switch (r.type) { - case"escape": { - const e = r; - n += t.text(e.text); - break - } - case"html": { - const e = r; - n += t.html(e.text); - break - } - case"link": { - const e = r; - n += t.link(e.href, e.title, this.parseInline(e.tokens, t)); - break - } - case"image": { - const e = r; - n += t.image(e.href, e.title, e.text); - break - } - case"strong": { - const e = r; - n += t.strong(this.parseInline(e.tokens, t)); - break - } - case"em": { - const e = r; - n += t.em(this.parseInline(e.tokens, t)); - break - } - case"codespan": { - const e = r; - n += t.codespan(e.text); - break - } - case"br": - n += t.br(); - break; - case"del": { - const e = r; - n += t.del(this.parseInline(e.tokens, t)); - break - } - case"text": { - const e = r; - n += t.text(e.text); - break - } - default: { - const e = 'Token with "' + r.type + '" type was not found.'; - if (this.options.silent) return console.error(e), ""; - throw new Error(e) - } - } - } - return n - } - } - - class le { - options; - - constructor(t) { - this.options = t || e.defaults - } - - static passThroughHooks = new Set(["preprocess", "postprocess", "processAllTokens"]); - - preprocess(e) { - return e - } - - postprocess(e) { - return e - } - - processAllTokens(e) { - return e - } - } - - class oe { - defaults = { - async: !1, - breaks: !1, - extensions: null, - gfm: !0, - hooks: null, - pedantic: !1, - renderer: null, - silent: !1, - tokenizer: null, - walkTokens: null - }; - options = this.setOptions; - parse = this.#e(ne.lex, ie.parse); - parseInline = this.#e(ne.lexInline, ie.parseInline); - Parser = ie; - Renderer = se; - TextRenderer = re; - Lexer = ne; - Tokenizer = w; - Hooks = le; - - constructor(...e) { - this.use(...e) - } - - walkTokens(e, t) { - let n = []; - for (const s of e) switch (n = n.concat(t.call(this, s)), s.type) { - case"table": { - const e = s; - for (const s of e.header) n = n.concat(this.walkTokens(s.tokens, t)); - for (const s of e.rows) for (const e of s) n = n.concat(this.walkTokens(e.tokens, t)); - break - } - case"list": { - const e = s; - n = n.concat(this.walkTokens(e.items, t)); - break - } - default: { - const e = s; - this.defaults.extensions?.childTokens?.[e.type] ? this.defaults.extensions.childTokens[e.type].forEach((s => { - const r = e[s].flat(1 / 0); - n = n.concat(this.walkTokens(r, t)) - })) : e.tokens && (n = n.concat(this.walkTokens(e.tokens, t))) - } - } - return n - } - - use(...e) { - const t = this.defaults.extensions || {renderers: {}, childTokens: {}}; - return e.forEach((e => { - const n = {...e}; - if (n.async = this.defaults.async || n.async || !1, e.extensions && (e.extensions.forEach((e => { - if (!e.name) throw new Error("extension name required"); - if ("renderer" in e) { - const n = t.renderers[e.name]; - t.renderers[e.name] = n ? function (...t) { - let s = e.renderer.apply(this, t); - return !1 === s && (s = n.apply(this, t)), s - } : e.renderer - } - if ("tokenizer" in e) { - if (!e.level || "block" !== e.level && "inline" !== e.level) throw new Error("extension level must be 'block' or 'inline'"); - const n = t[e.level]; - n ? n.unshift(e.tokenizer) : t[e.level] = [e.tokenizer], e.start && ("block" === e.level ? t.startBlock ? t.startBlock.push(e.start) : t.startBlock = [e.start] : "inline" === e.level && (t.startInline ? t.startInline.push(e.start) : t.startInline = [e.start])) - } - "childTokens" in e && e.childTokens && (t.childTokens[e.name] = e.childTokens) - })), n.extensions = t), e.renderer) { - const t = this.defaults.renderer || new se(this.defaults); - for (const n in e.renderer) { - if (!(n in t)) throw new Error(`renderer '${n}' does not exist`); - if ("options" === n) continue; - const s = n, r = e.renderer[s], i = t[s]; - t[s] = (...e) => { - let n = r.apply(t, e); - return !1 === n && (n = i.apply(t, e)), n || "" - } - } - n.renderer = t - } - if (e.tokenizer) { - const t = this.defaults.tokenizer || new w(this.defaults); - for (const n in e.tokenizer) { - if (!(n in t)) throw new Error(`tokenizer '${n}' does not exist`); - if (["options", "rules", "lexer"].includes(n)) continue; - const s = n, r = e.tokenizer[s], i = t[s]; - t[s] = (...e) => { - let n = r.apply(t, e); - return !1 === n && (n = i.apply(t, e)), n - } - } - n.tokenizer = t - } - if (e.hooks) { - const t = this.defaults.hooks || new le; - for (const n in e.hooks) { - if (!(n in t)) throw new Error(`hook '${n}' does not exist`); - if ("options" === n) continue; - const s = n, r = e.hooks[s], i = t[s]; - le.passThroughHooks.has(n) ? t[s] = e => { - if (this.defaults.async) return Promise.resolve(r.call(t, e)).then((e => i.call(t, e))); - const n = r.call(t, e); - return i.call(t, n) - } : t[s] = (...e) => { - let n = r.apply(t, e); - return !1 === n && (n = i.apply(t, e)), n - } - } - n.hooks = t - } - if (e.walkTokens) { - const t = this.defaults.walkTokens, s = e.walkTokens; - n.walkTokens = function (e) { - let n = []; - return n.push(s.call(this, e)), t && (n = n.concat(t.call(this, e))), n - } - } - this.defaults = {...this.defaults, ...n} - })), this - } - - setOptions(e) { - return this.defaults = {...this.defaults, ...e}, this - } - - lexer(e, t) { - return ne.lex(e, t ?? this.defaults) - } - - parser(e, t) { - return ie.parse(e, t ?? this.defaults) - } - - #e(e, t) { - return (n, s) => { - const r = {...s}, i = {...this.defaults, ...r}; - !0 === this.defaults.async && !1 === r.async && (i.silent || console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."), i.async = !0); - const l = this.#t(!!i.silent, !!i.async); - if (null == n) return l(new Error("marked(): input parameter is undefined or null")); - if ("string" != typeof n) return l(new Error("marked(): input parameter is of type " + Object.prototype.toString.call(n) + ", string expected")); - if (i.hooks && (i.hooks.options = i), i.async) return Promise.resolve(i.hooks ? i.hooks.preprocess(n) : n).then((t => e(t, i))).then((e => i.hooks ? i.hooks.processAllTokens(e) : e)).then((e => i.walkTokens ? Promise.all(this.walkTokens(e, i.walkTokens)).then((() => e)) : e)).then((e => t(e, i))).then((e => i.hooks ? i.hooks.postprocess(e) : e)).catch(l); - try { - i.hooks && (n = i.hooks.preprocess(n)); - let s = e(n, i); - i.hooks && (s = i.hooks.processAllTokens(s)), i.walkTokens && this.walkTokens(s, i.walkTokens); - let r = t(s, i); - return i.hooks && (r = i.hooks.postprocess(r)), r - } catch (e) { - return l(e) - } - } - } - - #t(e, t) { - return n => { - if (n.message += "\nPlease report this to https://github.com/markedjs/marked.", e) { - const e = "

    An error occurred:

    " + c(n.message + "", !0) + "
    "; - return t ? Promise.resolve(e) : e - } - if (t) return Promise.reject(n); - throw n - } - } - } - - const ae = new oe; - - function ce(e, t) { - return ae.parse(e, t) - } - - ce.options = ce.setOptions = function (e) { - return ae.setOptions(e), ce.defaults = ae.defaults, n(ce.defaults), ce - }, ce.getDefaults = t, ce.defaults = e.defaults, ce.use = function (...e) { - return ae.use(...e), ce.defaults = ae.defaults, n(ce.defaults), ce - }, ce.walkTokens = function (e, t) { - return ae.walkTokens(e, t) - }, ce.parseInline = ae.parseInline, ce.Parser = ie, ce.parser = ie.parse, ce.Renderer = se, ce.TextRenderer = re, ce.Lexer = ne, ce.lexer = ne.lex, ce.Tokenizer = w, ce.Hooks = le, ce.parse = ce; - const he = ce.options, pe = ce.setOptions, ue = ce.use, ke = ce.walkTokens, ge = ce.parseInline, fe = ce, - de = ie.parse, xe = ne.lex; - e.Hooks = le, e.Lexer = ne, e.Marked = oe, e.Parser = ie, e.Renderer = se, e.TextRenderer = re, e.Tokenizer = w, e.getDefaults = t, e.lexer = xe, e.marked = ce, e.options = he, e.parse = fe, e.parseInline = ge, e.parser = de, e.setOptions = pe, e.use = ue, e.walkTokens = ke -})); diff --git a/web/moviemain.js b/web/moviemain.js deleted file mode 100644 index 1bde862..0000000 --- a/web/moviemain.js +++ /dev/null @@ -1,107 +0,0 @@ -import {app} from "../../scripts/app.js"; -import {api} from "../../scripts/api.js"; - -app.registerExtension({ - name: "movienodes.MovieMain", - async init(app) { - const canvas = document.querySelector("#graph-canvas"); - // Add event listener for the drop event - canvas.addEventListener("drop", this.handleDrop.bind(this), false); - - //canvas.addEventListener("dragover", (e) => { - // e.preventDefault() - // console.log(e), false - //}); - // Allow drop events on the app canvas - canvas.addEventListener("dragover", this.handleDragOver.bind(this), false); - canvas.addEventListener("dragenter", this.handleDragEnter.bind(this), false); - canvas.addEventListener("dragleave", this.handleDragLeave.bind(this), false); - }, - async handleDrop(event) { - // Prevent default behavior to prevent the browser from opening the dropped file - event.preventDefault(); - const supportsFileSystemAccessAPI = - 'getAsFileSystemHandle' in DataTransferItem.prototype; - // Get the file(s) from the dropped event - const files = event.dataTransfer.files; - - // Check if there are any files dropped - if (files.length > 0) { - // Iterate through the dropped files - for (let i = 0; i < files.length; i++) { - const fileHandlesPromises = [...event.dataTransfer.items] - // …by including only files (where file misleadingly means actual file _or_ - // directory)… - //.filter((item) => item.kind === 'file') - // …and, depending on previous feature detection… - .map((item) => - supportsFileSystemAccessAPI - // …either get a modern `FileSystemHandle`… - ? item.getAsFileSystemHandle() - // …or a classic `File`. - : item.getAsFile(), - ); - - for await (const handle of fileHandlesPromises) { - // This is where we can actually exclusively act on the files. - console.log(`File: ${handle.name}`); - } - - const file = fileHandlesPromises[i]; - const formData = new FormData(); - formData.append("name", file.name); - formData.append("video", file); - - - try { - const resp = await api.fetchApi("/SWAIN/SaveFile", { - method: "POST", - body: formData - }); - - if (resp.status === 200) { - // File uploaded successfully - - // Get uploaded filename - const filename = await resp.json(); - - // Create node - const node = LiteGraph.createNode("VideoFileToImageStack"); - app.graph.add(node); - - // Set widget value to filename - // filter to the widget that has the name "vide_in" - const widget = node.widgets.find(w => w.name === "video_in"); - widget.value = file.name; - - - - } else { - console.error("Upload failed"); - } - - } catch (err) { - console.error("Error uploading file", err); - } - - - } - } - }, - handleDragOver(event) { - // Allow dropping on the document - event.preventDefault(); - } - , - handleDragEnter(event) { - // Add a class to the document when a file is dragged over it - document.body.classList.add("dragover"); - } - , - handleDragLeave(event) { - // Remove the class from the document when a file is dragged out of it - document.body.classList.remove("dragover"); - } - , -}) -; \ No newline at end of file diff --git a/web/showMarkdown.js b/web/showMarkdown.js deleted file mode 100644 index ce2bea7..0000000 --- a/web/showMarkdown.js +++ /dev/null @@ -1,47 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { ComfyWidgets } from "../../scripts/widgets.js"; -import Marked from "./marked.js"; - - -// Displays input markdown on a node - -app.registerExtension({ - name: "pysssss.ShowMarkdown", - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.internal_state_display || nodeData.name == "ShowMarkdown") { - // When the node is executed we will be sent the input markdown, display this in the widget - const onExecuted = nodeType.prototype.onExecuted; - nodeType.prototype.onExecuted = function (message) { - onExecuted?.apply(this, arguments); - - if (this.widgets) { - const pos = this.widgets.findIndex((w) => w.name === "markdown"); - if (pos !== -1) { - for (let i = pos; i < this.widgets.length; i++) { - this.widgets[i].onRemove?.(); - } - this.widgets.length = pos; - } - } - - for (const list of message.text) { - const w = ComfyWidgets["STRING"](this, "markdown", ["STRING", { multiline: true }], app).widget; - - // Convert markdown to HTML - const html = Marked(list); - - // Create a new div element to hold the rendered markdown - const div = document.createElement("div"); - div.classList.add("markdown-widget"); - div.style.opacity = 0.6; - div.innerHTML = html; - - // Replace the input element with the div containing the rendered markdown - w.inputEl.parentNode.replaceChild(div, w.inputEl); - } - - this.onResize?.(this.size); - }; - } - }, -}); \ No newline at end of file diff --git a/web/showText.js b/web/showText.js deleted file mode 100644 index 2bca3b2..0000000 --- a/web/showText.js +++ /dev/null @@ -1,118 +0,0 @@ -import {app} from "/scripts/app.js"; -import {ComfyWidgets} from "/scripts/widgets.js"; -//import {createAddNodeWidget} from "./AddNodeWidget.js"; - -app.registerExtension({ - name: "etk.ShowText", - async beforeRegisterNodeDef(nodeType, nodeData, app) { - const use_name = "ETK Show Text"; - const use_name_2 = "ShowList"; - // create an array with these two - const use_names = - [ - "ETK Show Text", - "ShowList", - "ShowDict" - ]; - // if nodeData.name in use_names - if (use_names.includes(nodeData.name)) { - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - onNodeCreated?.apply(this, arguments); - - // const nodeParams = { - // buttonLabel: "Add Node", - // nodeName: "Show Text", - // outputType: "STRING", - // inputSlotName: "text" - // }; - // createAddNodeWidget(this, app, nodeParams); - - if (!nodeData.widgets) { - return; - } else { - const widget = this.widgets.find((w) => w.name === "text_display"); - if (widget) { - widget.inputEl.readOnly = true; - widget.inputEl.style.opacity = 0.6; - widget.inputEl.style.userSelect = "text"; - widget.inputEl.style.webkitUserSelect = "text"; - widget.inputEl.style.msUserSelect = "text"; - widget.inputEl.style.mozUserSelect = "text"; - } - } - - } - } - ; - - // Check if nodeData has a property 'internal_state_display' to identify nodes used for display. - if (use_names.includes(nodeData.name)) { - // Debugging: print the nodeData to the console. - console.log(nodeData); - - // Function to populate the node with text widgets. - function populate(text) { - this.display_widget_idx = this.widgets.findIndex((w) => w.name === "text_display"); - // Remove the widget with the name of the text display widget - const widget = this.widgets.find((w) => w.name === "text_display"); - if (widget) { - widget.onRemove?.(); - this.widgets.splice(this.widgets.indexOf(widget), 1); - } - - // Create a new widget with the name of the text display widget and the correct text - const w = ComfyWidgets["STRING"](this, "text_display", ["STRING", {multiline: true}], app).widget; - // Make the widget's input element read-only and semi-transparent. - w.inputEl.readOnly = true; - w.inputEl.style.opacity = 0.6; - w.inputEl.style.userSelect = "text"; - w.inputEl.style.webkitUserSelect = "text"; - w.inputEl.style.msUserSelect = "text"; - w.inputEl.style.mozUserSelect = "text"; - - // If text has only one item, update the w.value directly without indexing - if (text.length === 1) { - w.value = text[0]; - } else { - // Set the value of the widget to the current text item, use the correct item index - w.value = text[this.display_widget_idx]; - } - - // Adjust the node size after adding widgets. - requestAnimationFrame(() => { - const sz = this.computeSize(); - // Ensure the node size is not smaller than its initial size. - if (sz[0] < this.size[0]) { - sz[0] = this.size[0]; - } - if (sz[1] < this.size[1]) { - sz[1] = this.size[1]; - } - // Trigger a resize event with the new size. - this.onResize?.(sz); - // Mark the canvas as dirty to update the UI. - app.graph.setDirtyCanvas(true, false); - }); - } - - // Override the 'onExecuted' method to display text when the node is executed. - const onExecuted = nodeType.prototype.onExecuted; - nodeType.prototype.onExecuted = function (message) { - // Call the original 'onExecuted' method. - onExecuted?.apply(this, arguments); - // Populate the node with the text from the execution message. - populate.call(this, message.text); - }; - - // Override the 'onConfigure' method to display stored widget values. - const onConfigure = nodeType.prototype.onConfigure; - nodeType.prototype.onConfigure = function () { - // If there are stored widget values, populate the node with these values. - if (this.widgets_values?.length) { - populate.call(this, this.widgets_values); - } - }; - } - }, -}); \ No newline at end of file diff --git a/web/ui_helpers.js b/web/ui_helpers.js deleted file mode 100644 index 873e236..0000000 --- a/web/ui_helpers.js +++ /dev/null @@ -1,225 +0,0 @@ -import {app} from "../../../scripts/app.js"; -import {api} from "../../../scripts/api.js"; - -export function setupButtons(gdeState, app, api) { - // Save To Git Button - const menu = document.querySelector(".comfy-menu"); - const saveButton = document.createElement("button"); - saveButton.textContent = "Save To Cloud"; - saveButton.onclick = async () => { - try { - const json = JSON.stringify(app.graph.serialize(), null, 2); - const userNameValue = gdeState.user; - const graphSelected = gdeState.graph; - - await api.fetchApi("/gde/git/save", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - data: json, - file_name: graphSelected, - user_name: userNameValue - }), - }); - } catch (error) { - console.error(error); - } - }; - menu.append(saveButton); - - // Load From Git Button - const loadButton = document.createElement("button"); - loadButton.textContent = "Load From Cloud"; - loadButton.onclick = async () => { - try { - const userNameValue = gdeState.user; - const graphSelected = gdeState.graph; - const response = await api.fetchApi("/gde/git/load", { - // needs user_name file_name - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - user_name: userNameValue, - file_name: graphSelected - } - ), - - }); - const data = await response.json(); - app.loadGraphData(JSON.parse(data.data)); - } catch (error) { - console.error(error); - } - }; - menu.append(loadButton); -} - - -export function setupModal(gdeState) { - // Modal - const modal = document.createElement("div"); - modal.id = "graph-name-modal"; - modal.style.display = "none"; // Initially hidden - modal.style.position = "fixed"; - modal.style.top = "50%"; - modal.style.left = "50%"; - modal.style.transform = "translate(-50%, -50%)"; - modal.style.padding = "20px"; - modal.style.backgroundColor = "#fff"; - modal.style.border = "1px solid #000"; - modal.style.zIndex = "1000"; // Ensure it's on top of other elements - document.body.append(modal); - - // Input field inside the modal - const modalInput = document.createElement("input"); - modalInput.type = "text"; - modalInput.placeholder = "Enter graph name"; - modal.append(modalInput); - - // Submit button inside the modal - const submitButton = document.createElement("button"); - submitButton.textContent = "Submit"; - let targetDropdownId = ''; // This will store the ID of the dropdown we're targeting - - modal.submit = () => { - // this is the function that will be called when the submit button is clicked - // it will add the new option to the dropdown and select it - const dropdown = document.querySelector(`#${targetDropdownId}`); - const newOption = document.createElement("option"); - newOption.value = modalInput.value; - newOption.textContent = modalInput.value; - dropdown.append(newOption); - dropdown.value = modalInput.value; - console.log("Submitted value:", modalInput.value); - modal.style.display = "none"; // Hide the modal - return modalInput.value; - } - submitButton.onclick = () => { - modal.submit() - }; - modal.append(submitButton); - - // Function to show the modal (this can be used outside as well) - window.showModal = function (dropdownId) { - targetDropdownId = dropdownId; // Store the passed dropdown ID - modal.style.display = "block"; - }; -} - -HTMLSelectElement.prototype.clearDropDown = function (placeHolder = "") { - // remove everything - while (this.options.length > 0) { - this.remove(0); - } - // only add if there is a placeholder - if (placeHolder != "") { - //add select+type - const defaultOption = document.createElement("option"); - defaultOption.value = ""; - defaultOption.textContent = placeHolder - this.append(defaultOption); - - // Add "Create New" option - const createNewOption = document.createElement("option"); - createNewOption.value = "create_new"; - createNewOption.textContent = "Create New"; - this.append(createNewOption); - } - -}; - - -export function setupDropdown(menu, api, placeholder, apiEndpoint, dropdownId, - user = null, onCreateNew = null, - onChanged = null) { - let dropdown = document.getElementById(dropdownId); - - // If dropdown already exists, clear its options first - if (dropdown) { - dropdown.clearDropDown(placeholder) - } else { - // Create new dropdown if it doesn't exist - dropdown = document.createElement("select"); - dropdown.style.marginTop = "2px"; - dropdown.style.width = "100%"; - dropdown.style.borderRadius = "8px"; - dropdown.style.fontSize = "18px"; - dropdown.style.backgroundColor = "#222222"; - dropdown.style.color = "#EEEEEE"; - - dropdown.id = dropdownId; - //use comfy-list style - dropdown.clearDropDown(placeholder) - menu.append(dropdown); - - // Add event listener to the dropdown - dropdown.addEventListener("change", (event) => { - // Check if "Create New" option was selected - if (event.target.value === "create_new" && onCreateNew) { - onCreateNew(event, dropdown); - } - if (onChanged) { - onChanged(event, dropdown); - } - - } - ); - } - - - // dropdown.addEventListener("change", (event) => { - // // Check if "Create New" option was selected - // if (event.target.value === "create_new" && onCreateNew) { - // onCreateNew(event, dropdown); - // } - // - // }); - - setTimeout(async () => { - dropdown.clearDropDown(placeholder) - await someFunction(dropdownId, api, apiEndpoint, placeholder, user) - }, 50) - return dropdown; -} - -export async function someFunction(dropdownId, api, apiEndpoint, placeholder, user = null) { - // this is the function that will be called to fetch the options for the dropdown - try { - const dropdown = document.getElementById(dropdownId); - dropdown.disabled = true; // Disable the file dropdown - const clientId = api.socket.url.split("=")[1] - const queryParams = { - user: user || "", - client_id: clientId - }; - - const response = await api.fetchApi(apiEndpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(queryParams) - }); - const optionsData = await response.json(); - optionsData.forEach(optionItem => { - // if the option already exists, don't add it again - if (dropdown.querySelector(`option[value="${optionItem.value}"]`)) { - return; - } - const optionElement = document.createElement("option"); - optionElement.value = optionItem.value; - optionElement.textContent = optionItem.label; - dropdown.append(optionElement); - }); - - dropdown.disabled = false; // Enable the file dropdown - - } catch (error) { - console.error(`Error fetching options for ${placeholder}:`, error); - } -} - diff --git a/web/ui_modifications.js b/web/ui_modifications.js deleted file mode 100644 index e07650e..0000000 --- a/web/ui_modifications.js +++ /dev/null @@ -1,413 +0,0 @@ -import {app} from "../../../scripts/app.js"; -import {api} from "../../../scripts/api.js"; -import {setupButtons, setupModal, setupDropdown} from './ui_helpers.js'; - - -let loggedInEvent = null; -let loggedOutEvent = new Event('loggedOutEvent'); -export let selectedGraph = null; - -//const graphCreateNewEvent = new Event('graphCreateNew'); -// create a place to store some variables, including security_token - -class GDELogin { - constructor(api) { - this.api = api; - this.security_token = null; - this.initElements(); - } - - initElements() { - 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)); - this.logoutButton = this.createButton("logout-button", "Logout", this.handleLogout.bind(this)); - } - - createInputField(id, placeholder, type = "text") { - return this.createElement("input", { - id: id, - placeholder: placeholder, - type: type, - }); - } - - createButton(id, text, clickHandler) { - const button = this.createElement("button", { - id: id, - innerText: text, - }); - button.addEventListener("click", clickHandler); - return button; - } - - createElement(tag, attributes) { - const element = document.createElement(tag); - for (let attr in attributes) { - element[attr] = attributes[attr]; - } - return element; - } - - async handleLogin() { - const username = this.usernameField.value; - const password = this.passwordField.value; - - gdeState.user = username; - - const res = await this.api.fetchApi("/gde/login", { - method: "POST", - body: JSON.stringify({ - username: username, - password: password, - client_id: this.api.clientId, - }), - }); - - if (res.status !== 200) { - //if res.logged_in is false then the login failed but only need to alert if this.api.clientId is set - if (res.logged_in === false && this.api.clientId) { - alert("Login failed. Please check your username and password and try again."); - } - if (res.logged_in === true) { - this.username = res.user; - gdeState.user = username; - } - - } else { - const security_json = await res.json(); - this.security_token = security_json.security; - this.switchToLogoutView(); - - if (username) { - const event = new CustomEvent('loggedInEvent', - {detail: {'user': username}}); - //loggedInEvent.target = {value:json.user}; - document.dispatchEvent(event); - } else { // client was refreshed user was lost but client_id was not - this.usernameField.value = security_json.user; - loggedInEvent.target.value = security_json.user; - document.dispatchEvent(loggedInEvent); - - } - const welcomeMessage = this.createElement("div", { - id: "welcome-message", - innerText: `Welcome ${this.usernameField.value}` - }); - this.login_form.append(welcomeMessage); - } - - - } - - async handleLogout() { - await this.api.fetchApi("/gde/logout", { - method: "POST", - body: JSON.stringify({ - client_id: this.api.clientId, - }), - }); - document.dispatchEvent(loggedOutEvent); - this.clearDropdowns(); - this.switchToLoginView(); - - const welcomeMessage = document.getElementById("welcome-message"); - if (welcomeMessage) welcomeMessage.remove(); - - } - - switchToLogoutView() { - this.usernameField.remove(); - this.passwordField.remove(); - this.loginButton.remove(); - this.login_form.append(this.logoutButton); - } - - switchToLoginView() { - this.logoutButton.remove(); - this.login_form.append(this.usernameField, this.passwordField, this.loginButton); - } - - clearDropdowns() { - this.clearDropdown("graph-name-field"); - //this.clearDropdown("branch-name-field"); - } - - clearDropdown(id) { - const dropdown = document.getElementById(id); - if (dropdown) { - while (dropdown.options.length > 2) { - dropdown.remove(2); - } - } - } - - - async getServerState(client_id) { - // check with the server to see if we have a valid client_id - const response = await api.fetchApi("/gde/login", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - client_id: client_id, - }), - }); - - const data = await response.json(); - return data; - } - - async setupLoginForm() { - const menu = document.querySelector(".comfy-menu"); - // create a div in the menu for this extension - - //need to check with server for the status of our current api.clientId - const cid = api.clientId || api.socket.url.split("=")[1]; - const server_state = await this.getServerState(cid).then((server_state) => { - const logged_in = server_state.logged_in; - gdeState.user = server_state.user; - - const separator = document.createElement("hr"); - separator.style.margin = "20px 0"; - separator.style.width = "100%"; - menu.append(separator); - menu.append(this.login_form); - - if (logged_in) { - this.login_form.append(this.logoutButton); - - } else { - this.login_form.append(this.usernameField); - this.login_form.append(this.passwordField); - this.login_form.append(this.loginButton); - - } - }); - } -} - -const gdeLogin = new GDELogin(api); -let gdeState = { - // this is for holding the state of the ui like what is selected etc - graph: null, - branch: null, - user: null, -} - -// create a dummy function -// gde.setup_login_form = async () => { -// const menu = document.querySelector(".comfy-menu"); -// // create a div in the menu for this extension -// -// -// //need to check with server for the status of our current api.clientId -// const cid = api.clientId || api.socket.url.split("=")[1]; -// const server_state = await gde.get_server_state(cid).then((server_state) => { -// const logged_in = server_state.logged_in; -// -// const separator = document.createElement("hr"); -// separator.style.margin = "20px 0"; -// separator.style.width = "100%"; -// menu.append(separator); -// menu.append(gde.login_div); -// -// if (logged_in) { -// gde.login_div.append(gde.logoutButton); -// -// } else { -// gde.login_div.append(gde.usernameField); -// gde.login_div.append(gde.passwordField); -// gde.login_div.append(gde.loginButton); -// -// } -// }); -// -// } -// gde.hijack_queueprompt = async () => { -// // is this actually needed? -// } -app.registerExtension({ - name: "GDE.UI.RemoveDefaultButtons", - async setup(app) { - document.getElementById("comfy-load-default-button").remove(); - document.getElementById("comfy-clipspace-button").remove(); - }, -}); - -app.registerExtension({ - name: "GDE.UI.Login", - async setup() { - setTimeout(async () => { - await gdeLogin.setupLoginForm(); - //await gde.hijack_queueprompt(); - }, 10); - } - - -}); - -function SetupGraphDropDown(graphDropdown, - event, - menu, - handleGraphCreateNew, - handleGraphSelect) { - // event may come in as null, so we need to populate it with default values - if (!event) { - event = { - detail: { - user: "main", - graph: "default.json", - branch: "main" - }, - target: { - value: "main" - } - } - gdeState.user = "main" - } - // graphDropdown.clearDropDown(); - graphDropdown.clearDropDown("Select Graph"); - const selectedGraph = null - graphDropdown = setupDropdown(menu, api, - "Select Graph", - "/gde/git/user_graphs", - "graph-name-field", - gdeState.user, - handleGraphCreateNew, - handleGraphSelect); - // modify graphDropdown to change gdeState.graph when it is changed - graphDropdown.addEventListener("change", (event) => { - const selectedGraph = event.target.value; - if (selectedGraph && selectedGraph !== gdeState.graph) { - gdeState.graph = selectedGraph; - const graphChangeEvent = new CustomEvent('graphChangeEvent', { - detail: { - graph: selectedGraph - } - }); - document.dispatchEvent(graphChangeEvent); - } - }); - return graphDropdown; -} - -app.registerExtension({ - name: "GDE.UI.Git", - async setup() { - setupModal(); - setupButtons(gdeState, app, api); - - - const separator = document.createElement("hr"); - const menu = document.querySelector(".comfy-menu"); - separator.style.margin = "20px 0"; - separator.style.width = "100%"; - menu.append(separator); - - - // const handleBranchCreateNew = (event, dropdown) => { - // // Custom logic for when "Create New" is selected in the branch dropdown - // showModal('branch-name-field'); // Show the modal for creating a new branch - // dropdown.value = ""; // Reset the dropdown value - // // Clear the file dropdown - // const fileDropdown = document.getElementById("graph-name-field"); - // - // - // }; - - document.addEventListener('loggedInEvent', async (event) => { - gdeState.user = event.detail.user; - //gdeState.graph = event.detail.graph; - graphDropdown = SetupGraphDropDown( - graphDropdown, - event, - menu, - handleGraphCreateNew, - handleGraphSelect); - - }); - - // no lets call clear dropdown when there is a loggout - document.addEventListener('loggedOutEvent', async (event) => { - graphDropdown.clearDropDown(); - } - ); - - const handleGraphCreateNew = (event, dropdown) => { - // Custom logic for when "Create New" is selected in the branch dropdown - showModal('graph-name-field'); - dropdown.disabled = true; - - const modal = document.getElementById("graph-name-modal"); - // Hijack its onsubmit function - const prev_submit = modal.submit; - - modal.submit = () => { - let graphName = prev_submit(); - 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 - const selectedGraph = event.target.value; - gdeState.graph = selectedGraph; - - }; - - - //setupDropdown(menu, api, "Select Branch", "/gde/git/users", "branch-name-field", null, null, handleBranchCreateNew); - //const branchDropdown = document.getElementById("branch-name-field"); - - // branchDropdown.addEventListener("change", (event) => { - // const selectedBranch = event.target.value; - // if (selectedBranch && selectedBranch !== "Create New") { - // // Repopulate the file/graph dropdown based on the selected branch - // setupDropdown(menu, api, "Select Graph", "/gde/git/user_graphs", "graph-name-field", selectedBranch, null, handleGraphCreateNew); - // } - // - // }); - - - let tmp = setupDropdown(menu, - api, - "Select Graph", - "/gde/git/user_graphs", - "graph-name-field", - gdeState.user, - handleGraphCreateNew, - handleGraphSelect); - let graphDropdown = SetupGraphDropDown(tmp, - null, - menu, - handleGraphCreateNew, - handleGraphSelect); - - } -}); diff --git a/youtube_nodes.py b/youtube_nodes.py deleted file mode 100644 index 1b54a49..0000000 --- a/youtube_nodes.py +++ /dev/null @@ -1,76 +0,0 @@ -import subprocess -import os - -NODE_CLASS_MAPPINGS = {} -NODE_DISPLAY_NAME_MAPPINGS = {} -from . import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS - - -def ETK_youtube_base(cls): - cls.CATEGORY = "ETK/Youtube" - NODE_CLASS_MAPPINGS[cls.__name__] = cls - return cls - - -@ETK_youtube_base -class YoutubeUploadNode: - VALID_PRIVACY_STATUSES = ["public", "private", "unlisted"] - VALID_CATEGORIES = [1, 2, 10, 15, 17, 19, 20, 22, 23, 24, 25, 26, 27, 28, 29] - CATEGORY_NAMES = ['Film & Animation', 'Autos & Vehicles', 'Music', 'Pets & Animals', 'Sports', 'Travel & Events', - 'Gaming', 'People & Blogs', 'Comedy', 'Entertainment', 'News & Politics', 'Howto & Style', - 'Education', 'Science & Technology', 'Nonprofits & Activism'] - - OUTPUT_NODE = True - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "file_path": ("STRING", {"description": "Path to the video file to upload"}), - "category": (cls.CATEGORY_NAMES,), - "title": ("STRING", {"default": "My video", "description": "Title of the video"}), - "description": ("STRING", {"default": "My video description", "multiline": True, - "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"}) - } - } - - RETURN_TYPES = ('STRING',) - FUNCTION = "upload_video" - CATEGORY = "ETK/Youtube" - - def upload_video(self, file_path, category, title, description, keywords, privacy_status, thumbnail_path=""): - import sys - my_python_exe = sys.executable - - category_idx = self.CATEGORY_NAMES.index(category) - category = self.VALID_CATEGORIES[category_idx] - - 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, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True) - - os.chdir(current_dir) - error = result.stderr - if error: - raise Exception(error) - - return (result.stdout,) - - -if __name__ == "__main__": - pass