import os
import hashlib
import json
import subprocess
import shutil
import re
import time,math
import numpy as np
from typing import List
import torch
from PIL import Image, ImageOps
from PIL.PngImagePlugin import PngInfo
import cv2,random,string
from pathlib import Path

import folder_paths
from comfy.k_diffusion.utils import FolderOfImages
from comfy.utils import common_upscale

import torchaudio
import base64

import mimetypes

# 使用递归的方法将嵌套的列表展平为一维列表
def flatten_list(nested_list):
    flat_list = []
    for item in nested_list:
        if isinstance(item, list):
            flat_list.extend(flatten_list(item))
        else:
            flat_list.append(item)
    return flat_list

def get_frames(frame_count, frames, revert=False):
    if not revert:
        if frame_count <= len(frames):
            return frames[:frame_count]
        else:
            return [frames[i % len(frames)] for i in range(frame_count)]
    else:
        extended_frames = frames + frames[-2:0:-1]  # 正向加反向中间部分
        if frame_count <= len(extended_frames):
            return extended_frames[:frame_count]
        else:
            return [extended_frames[i % len(extended_frames)] for i in range(frame_count)]

# # 示例用法
# frames = ["frame1", "frame2", "frame3"]
# frame_count = 2

# result = get_frames(frame_count, frames, revert=False)
# print(result)  # 输出: ['frame1', 'frame2', 'frame3', 'frame1', 'frame2', 'frame3', 'frame1']

# result = get_frames(frame_count, frames, revert=True)
# print(result)  # 输出: ['frame1', 'frame2', 'frame3', 'frame2', 'frame1', 'frame2', 'frame3']




def get_mime_type(file_path):
    # 获取文件的 MIME 类型
    mime_type, _ = mimetypes.guess_type(file_path)
    
    # 如果无法猜测类型，返回默认类型
    if mime_type is None:
        return 'application/octet-stream'
    
    return mime_type
# import subprocess
# from imageio_ffmpeg import get_ffmpeg_exe


def save_audio_base64s_to_file(base64_audios, output_folder, file_name):
    # Ensure the output folder exists
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)

    decoded_audios=[]
    for a in base64_audios:
    
        # If the base64 string contains a header, remove it
        if ',' in a:
            a = a.split(',')[1]
        
        # 解码 base64 数据
        a=base64.b64decode(a)
        decoded_audios.append(a)

    # 拼接音频数据
    combined_audio = b''.join(decoded_audios)
    
    # Create the full file path
    file_path = os.path.join(output_folder, file_name)
    
    # Write the decoded audio to the file
    with open(file_path, 'wb') as audio_file:
        audio_file.write(combined_audio)
    
    return file_path

# Example usage
# base64_audio = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAIlYAAESsAAACABAAZGF0YQAAAAA="
# output_folder = "audio_files"
# file_name = "output.wav"

# file_path = save_audio_base64_to_file(base64_audio, output_folder, file_name)
# print(f"Audio saved to: {file_path}")

# 写一个python文件，用来 判断文件夹内命名为 所有chat_tts开头的文件数量（chat_tts_00001），并输出新的编号
def get_new_counter(full_output_folder, filename_prefix):
    # 获取目录中的所有文件
    files = os.listdir(full_output_folder)
    
    # 过滤出以 filename_prefix 开头并且后续部分为数字的文件
    filtered_files = []
    for f in files:
        if f.startswith(filename_prefix):
            # 去掉文件名中的前缀和后缀，只保留中间的数字部分
            base_name = f[len(filename_prefix)+1:]
            number_part = base_name.split('.')[0]  # 假设文件名中只有一个点，即扩展名
            if number_part.isdigit():
                filtered_files.append(int(number_part))

    if not filtered_files:
        return 1

    # 获取最大的编号
    max_number = max(filtered_files)
    
    # 新的编号
    return max_number + 1

def crop_audio(input_file, start_time, duration):
    # Load the audio file
    audio_tensor, sample_rate = torchaudio.load(input_file)
    
    # Convert start_time and duration from seconds to sample indices
    start_sample = int(start_time * sample_rate)
    end_sample = start_sample + int(duration * sample_rate)
    
    # Perform the slicing
    cropped_audio_tensor = audio_tensor[:, start_sample:end_sample]
    
    # Save the cropped audio to a new file
    torchaudio.save(input_file, cropped_audio_tensor, sample_rate)
    
    return input_file

def generate_folder_name(directory,video_path):
    # Get the directory and filename from the video path
    _, filename = os.path.split(video_path)
    # Generate a random string of lowercase letters and digits
    random_string = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
    # Create the folder name by combining the random string and the filename
    folder_name = random_string + '_' + filename
    # Create the full folder path by joining the directory and the folder name
    folder_path = os.path.join(directory, folder_name)
    return folder_path

def create_folder(directory,video_path):
    folder_path = generate_folder_name(directory,video_path)
    os.makedirs(folder_path)
    return folder_path


def split_video(video_path, video_segment_frames, transition_frames, output_dir):
    # 读取视频文件
    video_capture = cv2.VideoCapture(video_path)
    
    # 获取视频的总帧数和帧率
    total_frames = int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT))
    fps = video_capture.get(cv2.CAP_PROP_FPS)
    
    # 计算每个视频片段的总帧数，包括过渡帧
    segment_total_frames = video_segment_frames + transition_frames
    
    # 计算可以分割的片段数量，向上取整
    num_segments = (total_frames + transition_frames - 1) // segment_total_frames
    
    vs=[]
    # 计算每个片段的起始帧和结束帧
    start_frame = 0
    for i in range(num_segments):
        # 计算当前片段的结束帧，注意最后一个片段可能没有过渡帧
        end_frame = min(start_frame + segment_total_frames, total_frames)
        
        # 打印当前片段的起始帧和结束帧
        print(f"Segment {i+1}: Start Frame {start_frame}, End Frame {end_frame}")

        if end_frame<start_frame:
            break
        
        # 保存当前片段为一个视频文件
        segment_video_path = f"{output_dir}/segment_{i+1}.avi"

        fourcc = cv2.VideoWriter_fourcc(*'XVID')
        segment_video = cv2.VideoWriter(segment_video_path, fourcc, fps, (int(video_capture.get(cv2.CAP_PROP_FRAME_WIDTH)),
                                                              int(video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT))))
        

        for frame_num in range(start_frame, end_frame):
            ret, frame = video_capture.read()
            if ret:
                segment_video.write(frame)
            else:
                break  # 如果读取失败，则退出循环
        
        # 更新起始帧为下一个片段的起始位置
        start_frame = end_frame + transition_frames
        vs.append(segment_video_path)
    
    # 释放视频捕获对象
    video_capture.release()
    # print(vs)
    return (vs,total_frames,fps)


folder_paths.folder_names_and_paths["video_formats"] = (
    [
        os.path.join(os.path.dirname(os.path.abspath(__file__)), ".", "video_formats"),
    ],
    [".json"]
)

ffmpeg_path = shutil.which("ffmpeg")
if ffmpeg_path is None:
    print("ffmpeg could not be found. Using ffmpeg from imageio-ffmpeg.")
    from imageio_ffmpeg import get_ffmpeg_exe
    try:
        ffmpeg_path = get_ffmpeg_exe()
    except:
        print("ffmpeg could not be found. Outputs that require it have been disabled")


def combine_audio_video(audio_path, video_path, output_path):
   
    command = [
        ffmpeg_path,
        '-i', video_path,
        '-i', audio_path,
        '-c:v', 'copy',
        '-c:a', 'aac',
        '-shortest',
        output_path
    ]
    
    subprocess.run(command, check=True)
    return output_path




# Tensor to PIL
def tensor2pil(image):
    return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))

# Convert PIL to Tensor
def pil2tensor(image):
    return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)

def count_files(directory):
    count = 0
    for root, dirs, files in os.walk(directory):
        count += len(files)
    return count

def create_temp_file(image):
    output_dir = folder_paths.get_temp_directory()

    c=count_files(output_dir)

    (
            full_output_folder,
            filename,
            counter,
            subfolder,
            _,
        ) = folder_paths.get_save_image_path('temp_', output_dir)

    
    image=tensor2pil(image)
 
    image_file = f"{filename}_{c}_{counter:05}.png"
     
    image_path=os.path.join(full_output_folder, image_file)

    image.save(image_path,compress_level=4)

    return [{
                "filename": image_file,
                "subfolder": subfolder,
                "type": "temp"
                }]


def split_list(lst, chunk_size, transition_size):
    result = []
    for i in range(0, len(lst), chunk_size):
        start = i - transition_size
        end = i + chunk_size + transition_size
        result.append(lst[max(start, 0):end])
    return result

# images = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# chunk_size = 3
# transition_size = 1

# result = split_list(images, chunk_size, transition_size)
# print(result)

class ImageListReplace:
    @classmethod
    def INPUT_TYPES(s):
        return {"required": {
            "images": ("IMAGE",),
            "start_index":("INT", {"default": 0, "min": 0, "step": 1}),
            "end_index":("INT", {"default": 0, "min": 0, "step": 1}),
            "invert": ("BOOLEAN", {"default": False}),
        },
         "optional":{
                    "image_replace": ("IMAGE",),
                    "images_replace": ("IMAGE",),
                    
                    }
        }

    RETURN_TYPES = ("IMAGE","IMAGE",)
    RETURN_NAMES = ("images","select_images",)
    FUNCTION = "run"
    CATEGORY = "♾️Mixlab/Video"

    OUTPUT_NODE = True
    INPUT_IS_LIST = True
    OUTPUT_IS_LIST = (True,True,)

    def run(self, images,start_index=[0],end_index=[0],invert=[False],image_replace=None,images_replace=None):
        start_index=start_index[0]
        end_index=end_index[0]
        invert=invert[0]

        image_rs=[]

        if image_replace!=None:
            for i in range(end_index-start_index+1):
                image_rs.append(image_replace[0])

        if images_replace!=None:
            image_rs=images_replace

        # 如果image replace 为空
        if image_replace==None and images_replace==None:
            # print('如果image replace 为空',images[0])
            # [[tensor(
            # tensor([[[[0.
            first_image=tensor2pil(images[0][0])
            width, height = first_image.size
            image_replace=Image.new("RGB", (width, height), (0, 0, 0))
            image_replace=pil2tensor(image_replace)
            for i in range(end_index-start_index+1):
                image_rs.append(image_replace)

        
        new_images=[]
        select_images=[]
        k=0
        for i in range(len(images)):
            if i>=start_index and i<=end_index:
                if invert:
                    new_images.append(images[i])
                else:
                    new_images.append(image_rs[k])
                    select_images.append(images[i])
                    k+=1
            else:
                if invert:
                    new_images.append(image_rs[k])
                    select_images.append(images[i])
                    k+=1
                else:
                    new_images.append(images[i])

        imss=[]
        # print(len(images))
        for i in range(len(images)):
            t=images[i][0]
            t=tensor2pil(t)
            t = t.convert("RGB")
            original_width, original_height = t.size
            scale = 300 / original_width
            new_height = int(original_height * scale)
            t = t.resize((300, new_height))

            ims=create_temp_file(pil2tensor(t))
            imss.append(ims[0])

        # image_replace=create_temp_file(image_replace)

        return {"ui":{"_images": imss},"result": (new_images,select_images,)}

# The code is based on ComfyUI-VideoHelperSuite modification.
class LoadVideoAndSegment:
    @classmethod
    def INPUT_TYPES(s):
        video_extensions = ['webm', 'mp4', 'mkv', 'gif']
        input_dir = folder_paths.get_input_directory()
        files = []
        for f in os.listdir(input_dir):
            if os.path.isfile(os.path.join(input_dir, f)):
                file_parts = f.split('.')
                if len(file_parts) > 1 and (file_parts[-1] in video_extensions):
                    files.append(f)
        return {"required": {
                    "video": (sorted(files), {"video_upload": True}),
                     "video_segment_frames": ("INT", {"default": 10, "min": -1, "step": 1}),
                     "transition_frames": ("INT", {"default": 0, "min": 0, "step": 1}), 
                     },}

    CATEGORY = "♾️Mixlab/Video"

    RETURN_TYPES = ("SCENE_VIDEO","INT", "INT","INT",)
    RETURN_NAMES = ("scenes_video","scenes_count","frame_count","fps",)
    FUNCTION = "load_video"
    OUTPUT_NODE = True
    OUTPUT_IS_LIST = (True,False,False,False,)


    def is_gif(self, filename):
        file_parts = filename.split('.')
        return len(file_parts) > 1 and file_parts[-1] == "gif"

    def load_video_cv_fallback(self, video, frame_load_cap, skip_first_frames):
        try:
            video_cap = cv2.VideoCapture(folder_paths.get_annotated_filepath(video))
            if not video_cap.isOpened():
                raise ValueError(f"{video} could not be loaded with cv fallback.")
            # set video_cap to look at start_index frame
            images = []
            total_frame_count = 0
            frames_added = 0
            base_frame_time = 1/video_cap.get(cv2.CAP_PROP_FPS)
            
            target_frame_time = base_frame_time

            time_offset=0.0
            while video_cap.isOpened():
                if time_offset < target_frame_time:
                    is_returned, frame = video_cap.read()
                    # if didn't return frame, video has ended
                    if not is_returned:
                        break
                    time_offset += base_frame_time
                if time_offset < target_frame_time:
                    continue
                time_offset -= target_frame_time
                # if not at start_index, skip doing anything with frame
                total_frame_count += 1
                if total_frame_count <= skip_first_frames:
                    continue
                # TODO: do whatever operations need to happen, like force_size, etc

                # opencv loads images in BGR format (yuck), so need to convert to RGB for ComfyUI use
                # follow up: can videos ever have an alpha channel?
                frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                # convert frame to comfyui's expected format (taken from comfy's load image code)
                image = Image.fromarray(frame)
                image = ImageOps.exif_transpose(image)
                image = np.array(image, dtype=np.float32) / 255.0
                image = torch.from_numpy(image)[None,]
                images.append(image)
                frames_added += 1
                # if cap exists and we've reached it, stop processing frames
                if frame_load_cap > 0 and frames_added >= frame_load_cap:
                    break
        finally:
            video_cap.release()
        images = torch.cat(images, dim=0)
       
        return (images, frames_added)

    def load_video(self, video,video_segment_frames,transition_frames ):
        
        video_path = folder_paths.get_annotated_filepath(video)
        
        # temp path 
        tp=folder_paths.get_temp_directory()
        basename = os.path.basename(video_path)  # 获取文件名
        name_without_extension = os.path.splitext(basename)[0]  # 去掉文件后缀

        folder_path = create_folder(tp,name_without_extension)
        
        if video_segment_frames==-1:
            # 不切割视频
            scenes_video=[video_path]
            # 读取视频文件
            video_capture = cv2.VideoCapture(video_path)
            
            # 获取视频的总帧数和帧率
            total_frames = int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT))
            fps = video_capture.get(cv2.CAP_PROP_FPS)

        else:
            # 导出的数据
            scenes_video,total_frames,fps=split_video(video_path,video_segment_frames,
                                    transition_frames,folder_path)
      


        return (scenes_video,len(scenes_video), total_frames,fps,)

    @classmethod
    def IS_CHANGED(s, video, **kwargs):
        image_path = folder_paths.get_annotated_filepath(video)
        m = hashlib.sha256()
        with open(image_path, 'rb') as f:
            m.update(f.read())
        return m.digest().hex()

    @classmethod
    def VALIDATE_INPUTS(s, video, **kwargs):
        if not folder_paths.exists_annotated_filepath(video):
            return "Invalid image file: {}".format(video)

        return True



class LoadAndCombinedAudio_:
    @classmethod
    def INPUT_TYPES(s):
       
        return {"required": {
                    "audios": ("AUDIOBASE64",),
                    "start_time": ("FLOAT" , {"default": 0, "min": 0, "max": 10000000, "step": 0.01}),
                    "duration": ("FLOAT" , {"default": 10, "min": -1, "max": 10000000, "step": 0.01}),
                     },
                }

    CATEGORY = "♾️Mixlab/Audio"

    RETURN_TYPES = ("STRING","AUDIO",)
    RETURN_NAMES = ("audio_file_path","audio",)
    FUNCTION = "run"

    def run(self,audios, start_time, duration):
        output_dir = folder_paths.get_output_directory()
        counter=get_new_counter(output_dir,'audio_')

        audio_file_name = f"audio_{counter:05}.wav"

        audio_file=save_audio_base64s_to_file(audios['base64'],output_dir,audio_file_name)
        # duration == -1 则不裁切
        if duration > -1:
            crop_audio(audio_file, start_time, duration)

        waveform, sample_rate = torchaudio.load(audio_file)
        audio = {
            "filename": audio_file_name,
            "subfolder": "",
            "type": "output",
            "audio_path":audio_file,
            "waveform": waveform.unsqueeze(0), 
            "sample_rate": sample_rate}

        return (audio_file,audio ,)

class CombineAudioVideo:
    @classmethod
    def INPUT_TYPES(s):
       
        return {"required": {
                     "video": ("SCENE_VIDEO",),
                     "audio": ("AUDIO", ), 
                     },
                }

    CATEGORY = "♾️Mixlab/Video"

    OUTPUT_NODE = True
    FUNCTION = "run" 
    RETURN_TYPES = ("SCENE_VIDEO",)
    RETURN_NAMES = ("SCENE_VIDEO",)

    def run(self,video, audio):

        output_dir = folder_paths.get_output_directory()

        # 判断是否是 Tensor 类型
        is_tensor = not isinstance(audio, dict)
        # print('#判断是否是 Tensor 类型',is_tensor,audio)
        if not is_tensor and 'waveform' in audio and 'sample_rate' in audio:
            # {'waveform': tensor([], size=(1, 1, 0)), 'sample_rate': 44100}
            is_tensor=True

        if "audio_path" in audio:
            is_tensor=False
            audio_file_path=audio["audio_path"]

        if is_tensor:
            filename_prefix="audio_tmp"
            full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
                filename_prefix, 
                folder_paths.get_temp_directory())
            
            filename_with_batch_num = filename.replace("%batch_num%", str(1))
            file = f"{filename_with_batch_num}_{counter:05}_.wav"
            
            audio_file_path=os.path.join(full_output_folder, file)

            torchaudio.save(audio_file_path, audio['waveform'].squeeze(0), audio["sample_rate"])
            
        # 获取文件名和扩展名
        base, ext = os.path.splitext(video)
        counter=get_new_counter(output_dir,'video_final_')

        v_file = f"video_final_{counter:05}{ext}"
        
        v_file_path=os.path.join(output_dir, v_file)

        combine_audio_video(audio_file_path,video,v_file_path)

        previews = [
            {
                "filename": v_file,
                "subfolder": "",
                "type": "output",
                "format": get_mime_type(v_file),
            }
        ]

        return {"ui": {"gifs": previews},"result":(v_file_path,)}

# The code is based on ComfyUI-VideoHelperSuite modification.
class VideoCombine_Adv:
    @classmethod
    def INPUT_TYPES(s):
        #Hide ffmpeg formats if ffmpeg isn't available
        if ffmpeg_path is not None:
            ffmpeg_formats = ["video/"+x[:-5] for x in folder_paths.get_filename_list("video_formats")]
        else:
            ffmpeg_formats = []
        # ffmpeg_formats =["video/"+x for x in  ['webm', 'mp4', 'mkv']]
        return {
            "required": {
                "image_batch": ("IMAGE",),
                "frame_rate": (
                    "INT",
                    {"default": 8, "min": 1, "step": 1},
                ),
                "loop_count": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
                "filename_prefix": ("STRING", {"default": "Comfyui"}),
                "format": (["image/gif", "image/webp"] + ffmpeg_formats,),
                "pingpong": ("BOOLEAN", {"default": False}),
                "save_image": ("BOOLEAN", {"default": True}),
                "metadata": ("BOOLEAN", {"default": False}),
            },
            "hidden": {
                "prompt": "PROMPT",
                "extra_pnginfo": "EXTRA_PNGINFO",
            },
        }

    RETURN_TYPES = ("SCENE_VIDEO",)
    RETURN_NAMES = ("scenes_video",)
    OUTPUT_NODE = True
    CATEGORY = "♾️Mixlab/Video"
    FUNCTION = "run"

    def save_with_tempfile(self, args, metadata, file_path, frames, env):
        #Ensure temp directory exists
        os.makedirs(folder_paths.get_temp_directory(), exist_ok=True)

        metadata_path = os.path.join(folder_paths.get_temp_directory(), "metadata.txt")
        #metadata from file should  escape = ; # \ and newline
        #From my testing, though, only backslashes need escapes and = in particular causes problems
        #It is likely better to prioritize future compatibility with containers that don't support
        #or shouldn't use the comment tag for embedding metadata
        metadata = metadata.replace("\\","\\\\")
        metadata = metadata.replace(";","\\;")
        metadata = metadata.replace("#","\\#")
        #metadata = metadata.replace("=","\\=")
        metadata = metadata.replace("\n","\\\n")
        with open(metadata_path, "w") as f:
            f.write(";FFMETADATA1\n")
            f.write(metadata)
        args = args[:1] + ["-i", metadata_path] + args[1:] + [file_path]
        with subprocess.Popen(args, stdin=subprocess.PIPE, env=env) as proc:
            for frame in frames:
                proc.stdin.write(frame.tobytes())

    def run(
        self,
        image_batch,
        frame_rate: int,
        loop_count: int,
        filename_prefix="AnimateDiff",
        format="image/gif",
        pingpong=False,
        save_image=True,
        metadata=False,
        prompt=None,
        extra_pnginfo=None,
    ):
        images=image_batch

        frames: List[Image.Image] = []
        for image in images:
            img = 255.0 * image.cpu().numpy()
            img = Image.fromarray(np.clip(img, 0, 255).astype(np.uint8))
            # resize 保证
            # 检查图像的高度是否是2的倍数，如果不是，则调整高度
            if img.height % 2 != 0:
                img = img.resize((img.width, img.height + 1))

            # 检查图像的宽度是否是2的倍数，如果不是，则调整宽度
            if img.width % 2 != 0:
                img = img.resize((img.width + 1, img.height))
                
            frames.append(img)

        # get output information
        output_dir = (
            folder_paths.get_output_directory()
            if save_image
            else folder_paths.get_temp_directory()
        )
        (
            full_output_folder,
            filename,
            counter,
            subfolder,
            _,
        ) = folder_paths.get_save_image_path(filename_prefix, output_dir)

        metadata = PngInfo()
        video_metadata = {}
        if prompt is not None:
            metadata.add_text("prompt", json.dumps(prompt))
            video_metadata["prompt"] = prompt
        if extra_pnginfo is not None:
            for x in extra_pnginfo:
                metadata.add_text(x, json.dumps(extra_pnginfo[x]))
                video_metadata[x] = extra_pnginfo[x]

        # 取消保存metadata
        if metadata==False:
            metadata = PngInfo()

        # save first frame as png to keep metadata
        file = f"{filename}_{counter:05}_.png"
        file_path = os.path.join(full_output_folder, file)
        frames[0].save(
            file_path,
            pnginfo=metadata,
            compress_level=4,
        )
        if pingpong:
            frames = frames + frames[-2:0:-1]

        format_type, format_ext = format.split("/")
        file = f"{filename}_{counter:05}_.{format_ext}"
        file_path = os.path.join(full_output_folder, file)
        if format_type == "image":
            # Use pillow directly to save an animated image
            frames[0].save(
                file_path,
                format=format_ext.upper(),
                save_all=True,
                append_images=frames[1:],
                duration=round(1000 / frame_rate),
                loop=loop_count,
                compress_level=4,
            )
        else:
            # Use ffmpeg to save a video
            if ffmpeg_path is None:
                #Should never be reachable
                raise ProcessLookupError("Could not find ffmpeg")

            video_format_path = folder_paths.get_full_path("video_formats", format_ext + ".json")
            with open(video_format_path, 'r') as stream:
                video_format = json.load(stream)
            file = f"{filename}_{counter:05}_.{video_format['extension']}"
            file_path = os.path.join(full_output_folder, file)
            dimensions = f"{frames[0].width}x{frames[0].height}"
            metadata_args = ["-metadata", "comment=" + json.dumps(video_metadata)]
            args = [ffmpeg_path, "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24",
                    "-s", dimensions, "-r", str(frame_rate), "-i", "-"] \
                    + video_format['main_pass']
            # On linux, max arg length is Pagesize * 32 -> 131072
            # On windows, this around 32767 but seems to vary wildly by > 500
            # in a manor not solely related to other arguments
            if os.name == 'posix':
                max_arg_length = 4096*32
            else:
                max_arg_length = 32767 - len(" ".join(args + [metadata_args[0]] + [file_path])) - 1
            #test max limit
            #metadata_args[1] = metadata_args[1] + "a"*(max_arg_length - len(metadata_args[1])-1)

            env=os.environ.copy()
            if  "environment" in video_format:
                env.update(video_format["environment"])
            if len(metadata_args[1]) >= max_arg_length:
                print(f"Using fallback file for extremely long metadata: {len(metadata_args[1])}/{max_arg_length}")
                self.save_with_tempfile(args, metadata_args[1], file_path, frames, env)
            else:
                try:
                    with subprocess.Popen(args + metadata_args + [file_path],
                                          stdin=subprocess.PIPE, env=env) as proc:
                        for frame in frames:
                            proc.stdin.write(frame.tobytes())
                except FileNotFoundError as e:
                    if "winerror" in dir(e) and e.winerror == 206:
                        print("Metadata was too long. Retrying with fallback file")
                        self.save_with_tempfile(args, metadata_args[1], file_path, frames, env)
                    else:
                        raise
                except OSError as e:
                    if "errno" in dir(e) and e.errno == 7:
                        print("Metadata was too long. Retrying with fallback file")
                        self.save_with_tempfile(args, metadata_args[1], file_path, frames, env)
                    else:
                        raise

        previews = [
            {
                "filename": file,
                "subfolder": subfolder,
                "type": "output" if save_image else "temp",
                "format": format,
            }
        ]
        return {"ui": {"gifs": previews},"result":(file_path,)}


class VAEEncodeForInpaint_Frames:
    @classmethod
    def INPUT_TYPES(s):
        return {"required": { 
            "vae": ("VAE", ), 
            "images": ("IMAGE", ), 
            "masks": ("MASK", ), 
            "grow_mask_by": ("INT", {"default": 6, "min": 0, "max": 64, "step": 1}),
            }}
    
    FUNCTION = "encode"


    RETURN_TYPES = ("LATENT",)
    RETURN_NAMES = ("LATENT",)
 
    CATEGORY = "♾️Mixlab/Video"

    OUTPUT_NODE = True
    INPUT_IS_LIST = True
    OUTPUT_IS_LIST = (True,)


    def encode(self, vae, images, masks, grow_mask_by=[6]):
        vae=vae[0]
        grow_mask_by=grow_mask_by[0]

        result=[]

        for i in range(len(images)):
            pixels=images[i]
            mask=masks[i]


            x = (pixels.shape[1] // 8) * 8
            y = (pixels.shape[2] // 8) * 8
            mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(pixels.shape[1], pixels.shape[2]), mode="bilinear")

            pixels = pixels.clone()
            if pixels.shape[1] != x or pixels.shape[2] != y:
                x_offset = (pixels.shape[1] % 8) // 2
                y_offset = (pixels.shape[2] % 8) // 2
                pixels = pixels[:,x_offset:x + x_offset, y_offset:y + y_offset,:]
                mask = mask[:,:,x_offset:x + x_offset, y_offset:y + y_offset]

            #grow mask by a few pixels to keep things seamless in latent space
            if grow_mask_by == 0:
                mask_erosion = mask
            else:
                kernel_tensor = torch.ones((1, 1, grow_mask_by, grow_mask_by))
                padding = math.ceil((grow_mask_by - 1) / 2)

                mask_erosion = torch.clamp(torch.nn.functional.conv2d(mask.round(), kernel_tensor, padding=padding), 0, 1)

            m = (1.0 - mask.round()).squeeze(1)
            for i in range(3):
                pixels[:,:,:,i] -= 0.5
                pixels[:,:,:,i] *= m
                pixels[:,:,:,i] += 0.5
            t = vae.encode(pixels)

            result.append({"samples":t, "noise_mask": (mask_erosion[:,:,:x,:y].round())})


        return (result, )
    


class GenerateFramesByCount:
    @classmethod
    def INPUT_TYPES(cls):
        
        return {"required": {
                    "frames": ('IMAGE',),
                    "frame_count": ("INT", {"default": 72, "min": 1, "step": 1}), 
                    "revert" :("BOOLEAN", {"default": True},),
                     },}

    RETURN_TYPES = ('IMAGE',)
    RETURN_NAMES = ("frames",)

    FUNCTION = "r"
    CATEGORY = "♾️Mixlab/Video"
    # INPUT_IS_LIST = True

    def r(self, frames, frame_count, revert):
        
        image_list = [frames[i:i + 1, ...] for i in range(frames.shape[0])]
        print('#image_list',len(image_list),frame_count)
        image_list=get_frames(frame_count,image_list,revert)

        images = torch.cat(image_list, dim=0)

        return (images,)


class scenesNode_:
    @classmethod
    def INPUT_TYPES(cls):
        
        return {"required": {
                    "scenes_video": ('SCENE_VIDEO',),
                    "index": ("INT", {"default": 0, "min": 0, "step": 1}),
                     },}

    RETURN_TYPES = ('IMAGE','INT',)
    RETURN_NAMES = ("video frames (batch)","count",)
    # OUTPUT_IS_LIST = (False,)

    FUNCTION = "run"
    CATEGORY = "♾️Mixlab/Video"
    INPUT_IS_LIST = True

    def load_video_cv_fallback(self, video, frame_load_cap, skip_first_frames):
        
        images = []
        total_frame_count = 0
        video_cap = cv2.VideoCapture(video)
        try:
            if not video_cap.isOpened():
                raise ValueError(f"{video} could not be loaded with cv fallback.")
            # set video_cap to look at start_index frame
            
            frames_added = 0
            base_frame_time = 1/video_cap.get(cv2.CAP_PROP_FPS)
            
            target_frame_time = base_frame_time

            time_offset=0.0
            while video_cap.isOpened():
                if time_offset < target_frame_time:
                    is_returned, frame = video_cap.read()
                    # if didn't return frame, video has ended
                    if not is_returned:
                        break
                    time_offset += base_frame_time
                if time_offset < target_frame_time:
                    continue
                time_offset -= target_frame_time
                # if not at start_index, skip doing anything with frame
                total_frame_count += 1
                if total_frame_count <= skip_first_frames:
                    continue
                # TODO: do whatever operations need to happen, like force_size, etc

                # opencv loads images in BGR format (yuck), so need to convert to RGB for ComfyUI use
                # follow up: can videos ever have an alpha channel?
                frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                # convert frame to comfyui's expected format (taken from comfy's load image code)
                image = Image.fromarray(frame)
                image = ImageOps.exif_transpose(image)
                image = np.array(image, dtype=np.float32) / 255.0
                image = torch.from_numpy(image)[None,]
                images.append(image)
                frames_added += 1
                # if cap exists and we've reached it, stop processing frames
                if frame_load_cap > 0 and frames_added >= frame_load_cap:
                    break
        finally:
            video_cap.release()
        
        print("total_frame_count",total_frame_count)
        images = torch.cat(images, dim=0)
       
        return (images, frames_added,)

    def run(self, scenes_video,index):
        scenes_video=flatten_list(scenes_video)
        print('#scenes_video',index,scenes_video)
        index=index[0]
        if len(scenes_video) > index:
            vp=scenes_video[index]
        else:
            vp=scenes_video[-1]
        
        return self.load_video_cv_fallback(vp,0,0)