2025-03-05 15:35:26 -05:00
from __future__ import annotations
2025-02-19 07:11:49 -05:00
import os
import av
import torch
import folder_paths
import json
2025-08-31 06:19:54 +03:00
from typing import Optional
from typing_extensions import override
2025-02-19 07:11:49 -05:00
from fractions import Fraction
2025-08-31 06:19:54 +03:00
from comfy_api . input import AudioInput , ImageInput , VideoInput
from comfy_api . input_impl import VideoFromComponents , VideoFromFile
from comfy_api . util import VideoCodec , VideoComponents , VideoContainer
from comfy_api . latest import ComfyExtension , io , ui
2025-04-29 02:58:00 -07:00
from comfy . cli_args import args
2025-02-19 07:11:49 -05:00
2025-08-31 06:19:54 +03:00
class SaveWEBM ( io . ComfyNode ) :
2025-02-19 07:11:49 -05:00
@classmethod
2025-08-31 06:19:54 +03:00
def define_schema ( cls ) :
return io . Schema (
node_id = " SaveWEBM " ,
category = " image/video " ,
is_experimental = True ,
inputs = [
io . Image . Input ( " images " ) ,
io . String . Input ( " filename_prefix " , default = " ComfyUI " ) ,
io . Combo . Input ( " codec " , options = [ " vp9 " , " av1 " ] ) ,
io . Float . Input ( " fps " , default = 24.0 , min = 0.01 , max = 1000.0 , step = 0.01 ) ,
io . Float . Input ( " crf " , default = 32.0 , min = 0 , max = 63.0 , step = 1 , tooltip = " Higher crf means lower quality with a smaller file size, lower crf means higher quality higher filesize. " ) ,
] ,
outputs = [ ] ,
hidden = [ io . Hidden . prompt , io . Hidden . extra_pnginfo ] ,
is_output_node = True ,
)
2025-02-19 07:11:49 -05:00
2025-08-31 06:19:54 +03:00
@classmethod
def execute ( cls , images , codec , fps , filename_prefix , crf ) - > io . NodeOutput :
full_output_folder , filename , counter , subfolder , filename_prefix = folder_paths . get_save_image_path (
filename_prefix , folder_paths . get_output_directory ( ) , images [ 0 ] . shape [ 1 ] , images [ 0 ] . shape [ 0 ]
)
2025-02-19 07:11:49 -05:00
file = f " { filename } _ { counter : 05 } _.webm "
container = av . open ( os . path . join ( full_output_folder , file ) , mode = " w " )
2025-08-31 06:19:54 +03:00
if cls . hidden . prompt is not None :
container . metadata [ " prompt " ] = json . dumps ( cls . hidden . prompt )
2025-02-19 07:11:49 -05:00
2025-08-31 06:19:54 +03:00
if cls . hidden . extra_pnginfo is not None :
for x in cls . hidden . extra_pnginfo :
container . metadata [ x ] = json . dumps ( cls . hidden . extra_pnginfo [ x ] )
2025-02-19 07:11:49 -05:00
2025-04-22 22:57:17 +01:00
codec_map = { " vp9 " : " libvpx-vp9 " , " av1 " : " libsvtav1 " }
2025-02-19 07:11:49 -05:00
stream = container . add_stream ( codec_map [ codec ] , rate = Fraction ( round ( fps * 1000 ) , 1000 ) )
stream . width = images . shape [ - 2 ]
stream . height = images . shape [ - 3 ]
2025-04-22 22:57:17 +01:00
stream . pix_fmt = " yuv420p10le " if codec == " av1 " else " yuv420p "
2025-02-19 07:11:49 -05:00
stream . bit_rate = 0
stream . options = { ' crf ' : str ( crf ) }
2025-04-22 22:57:17 +01:00
if codec == " av1 " :
stream . options [ " preset " ] = " 6 "
2025-02-19 07:11:49 -05:00
for frame in images :
frame = av . VideoFrame . from_ndarray ( torch . clamp ( frame [ . . . , : 3 ] * 255 , min = 0 , max = 255 ) . to ( device = torch . device ( " cpu " ) , dtype = torch . uint8 ) . numpy ( ) , format = " rgb24 " )
for packet in stream . encode ( frame ) :
container . mux ( packet )
2025-02-25 20:21:03 -05:00
container . mux ( stream . encode ( ) )
2025-02-19 07:11:49 -05:00
container . close ( )
2025-08-31 06:19:54 +03:00
return io . NodeOutput ( ui = ui . PreviewVideo ( [ ui . SavedResult ( file , subfolder , io . FolderType . output ) ] ) )
2025-02-19 07:11:49 -05:00
2025-08-31 06:19:54 +03:00
class SaveVideo ( io . ComfyNode ) :
@classmethod
def define_schema ( cls ) :
return io . Schema (
node_id = " SaveVideo " ,
display_name = " Save Video " ,
category = " image/video " ,
description = " Saves the input images to your ComfyUI output directory. " ,
inputs = [
io . Video . Input ( " video " , tooltip = " The video to save. " ) ,
io . String . Input ( " filename_prefix " , default = " video/ComfyUI " , tooltip = " The prefix for the file to save. This may include formatting information such as %d ate:yyyy-MM-dd % o r %E mpty Latent Image.width % to include values from nodes. " ) ,
io . Combo . Input ( " format " , options = VideoContainer . as_input ( ) , default = " auto " , tooltip = " The format to save the video as. " ) ,
io . Combo . Input ( " codec " , options = VideoCodec . as_input ( ) , default = " auto " , tooltip = " The codec to use for the video. " ) ,
] ,
outputs = [ ] ,
hidden = [ io . Hidden . prompt , io . Hidden . extra_pnginfo ] ,
is_output_node = True ,
)
2025-04-29 02:58:00 -07:00
@classmethod
2025-08-31 06:19:54 +03:00
def execute ( cls , video : VideoInput , filename_prefix , format , codec ) - > io . NodeOutput :
2025-04-29 02:58:00 -07:00
width , height = video . get_dimensions ( )
full_output_folder , filename , counter , subfolder , filename_prefix = folder_paths . get_save_image_path (
filename_prefix ,
2025-08-31 06:19:54 +03:00
folder_paths . get_output_directory ( ) ,
2025-04-29 02:58:00 -07:00
width ,
height
)
saved_metadata = None
if not args . disable_metadata :
metadata = { }
2025-08-31 06:19:54 +03:00
if cls . hidden . extra_pnginfo is not None :
metadata . update ( cls . hidden . extra_pnginfo )
if cls . hidden . prompt is not None :
metadata [ " prompt " ] = cls . hidden . prompt
2025-04-29 02:58:00 -07:00
if len ( metadata ) > 0 :
saved_metadata = metadata
2025-08-31 06:19:54 +03:00
file = f " { filename } _ { counter : 05 } _. { VideoContainer . get_extension ( format ) } "
2025-04-29 02:58:00 -07:00
video . save_to (
os . path . join ( full_output_folder , file ) ,
format = format ,
codec = codec ,
metadata = saved_metadata
)
2025-08-31 06:19:54 +03:00
return io . NodeOutput ( ui = ui . PreviewVideo ( [ ui . SavedResult ( file , subfolder , io . FolderType . output ) ] ) )
2025-04-29 02:58:00 -07:00
2025-08-31 06:19:54 +03:00
class CreateVideo ( io . ComfyNode ) :
2025-04-29 02:58:00 -07:00
@classmethod
2025-08-31 06:19:54 +03:00
def define_schema ( cls ) :
return io . Schema (
node_id = " CreateVideo " ,
display_name = " Create Video " ,
category = " image/video " ,
description = " Create a video from images. " ,
inputs = [
io . Image . Input ( " images " , tooltip = " The images to create a video from. " ) ,
io . Float . Input ( " fps " , default = 30.0 , min = 1.0 , max = 120.0 , step = 1.0 ) ,
io . Audio . Input ( " audio " , optional = True , tooltip = " The audio to add to the video. " ) ,
] ,
outputs = [
io . Video . Output ( ) ,
] ,
)
@classmethod
def execute ( cls , images : ImageInput , fps : float , audio : Optional [ AudioInput ] = None ) - > io . NodeOutput :
return io . NodeOutput (
VideoFromComponents ( VideoComponents ( images = images , audio = audio , frame_rate = Fraction ( fps ) ) )
)
class GetVideoComponents ( io . ComfyNode ) :
@classmethod
def define_schema ( cls ) :
return io . Schema (
node_id = " GetVideoComponents " ,
display_name = " Get Video Components " ,
category = " image/video " ,
description = " Extracts all components from a video: frames, audio, and framerate. " ,
inputs = [
io . Video . Input ( " video " , tooltip = " The video to extract components from. " ) ,
] ,
outputs = [
io . Image . Output ( display_name = " images " ) ,
io . Audio . Output ( display_name = " audio " ) ,
io . Float . Output ( display_name = " fps " ) ,
] ,
)
2025-04-29 02:58:00 -07:00
@classmethod
2025-08-31 06:19:54 +03:00
def execute ( cls , video : VideoInput ) - > io . NodeOutput :
2025-04-29 02:58:00 -07:00
components = video . get_components ( )
2025-08-31 06:19:54 +03:00
return io . NodeOutput ( components . images , components . audio , float ( components . frame_rate ) )
2025-04-29 02:58:00 -07:00
2025-08-31 06:19:54 +03:00
class LoadVideo ( io . ComfyNode ) :
2025-04-29 02:58:00 -07:00
@classmethod
2025-08-31 06:19:54 +03:00
def define_schema ( cls ) :
2025-04-29 02:58:00 -07:00
input_dir = folder_paths . get_input_directory ( )
files = [ f for f in os . listdir ( input_dir ) if os . path . isfile ( os . path . join ( input_dir , f ) ) ]
files = folder_paths . filter_files_content_types ( files , [ " video " ] )
2025-08-31 06:19:54 +03:00
return io . Schema (
node_id = " LoadVideo " ,
display_name = " Load Video " ,
category = " image/video " ,
inputs = [
io . Combo . Input ( " file " , options = sorted ( files ) , upload = io . UploadType . video ) ,
] ,
outputs = [
io . Video . Output ( ) ,
] ,
)
2025-04-29 02:58:00 -07:00
2025-08-31 06:19:54 +03:00
@classmethod
def execute ( cls , file ) - > io . NodeOutput :
2025-04-29 02:58:00 -07:00
video_path = folder_paths . get_annotated_filepath ( file )
2025-08-31 06:19:54 +03:00
return io . NodeOutput ( VideoFromFile ( video_path ) )
2025-04-29 02:58:00 -07:00
@classmethod
2025-08-31 06:19:54 +03:00
def fingerprint_inputs ( s , file ) :
2025-04-29 02:58:00 -07:00
video_path = folder_paths . get_annotated_filepath ( file )
mod_time = os . path . getmtime ( video_path )
# Instead of hashing the file, we can just use the modification time to avoid
# rehashing large files.
return mod_time
@classmethod
2025-08-31 06:19:54 +03:00
def validate_inputs ( s , file ) :
2025-04-29 02:58:00 -07:00
if not folder_paths . exists_annotated_filepath ( file ) :
return " Invalid video file: {} " . format ( file )
return True
2025-02-19 07:11:49 -05:00
2025-07-29 19:17:22 -07:00
2025-08-31 06:19:54 +03:00
class VideoExtension ( ComfyExtension ) :
@override
async def get_node_list ( self ) - > list [ type [ io . ComfyNode ] ] :
return [
SaveWEBM ,
SaveVideo ,
CreateVideo ,
GetVideoComponents ,
LoadVideo ,
]
async def comfy_entrypoint ( ) - > VideoExtension :
return VideoExtension ( )