From f2952d4634df972a21163e70d471bbc7aabb0816 Mon Sep 17 00:00:00 2001 From: bymyself Date: Fri, 27 Feb 2026 17:15:06 -0800 Subject: [PATCH] fix: resolve linting issues in SaveVideo codec options - Add missing 'logging' import used in speed preset error handling - Apply ruff format to all PR-changed files Amp-Thread-ID: https://ampcode.com/threads/T-019ca1cb-0150-7549-8b1b-6713060d3408 --- comfy_api/latest/__init__.py | 18 ++- comfy_api/latest/_input_impl/video_types.py | 145 +++++++++++++------ comfy_api/latest/_util/__init__.py | 8 +- comfy_api/latest/_util/video_types.py | 5 +- comfy_extras/nodes_video.py | 147 ++++++++++++++------ 5 files changed, 232 insertions(+), 91 deletions(-) diff --git a/comfy_api/latest/__init__.py b/comfy_api/latest/__init__.py index 7932efe76..dcf7d41de 100644 --- a/comfy_api/latest/__init__.py +++ b/comfy_api/latest/__init__.py @@ -7,7 +7,14 @@ from comfy_api.internal.singleton import ProxiedSingleton from comfy_api.internal.async_to_sync import create_sync_class from ._input import ImageInput, AudioInput, MaskInput, LatentInput, VideoInput from ._input_impl import VideoFromFile, VideoFromComponents -from ._util import VideoCodec, VideoContainer, VideoComponents, VideoSpeedPreset, MESH, VOXEL +from ._util import ( + VideoCodec, + VideoContainer, + VideoComponents, + VideoSpeedPreset, + MESH, + VOXEL, +) from . import _io_public as io from . import _ui_public as ui from comfy_execution.utils import get_executing_context @@ -45,7 +52,9 @@ class ComfyAPI_latest(ComfyAPIBase): raise ValueError("node_id must be provided if not in executing context") # Convert preview_image to PreviewImageTuple if needed - to_display: PreviewImageTuple | Image.Image | ImageInput | None = preview_image + to_display: PreviewImageTuple | Image.Image | ImageInput | None = ( + preview_image + ) if to_display is not None: # First convert to PIL Image if needed if isinstance(to_display, ImageInput): @@ -75,6 +84,7 @@ class ComfyAPI_latest(ComfyAPIBase): execution: Execution + class ComfyExtension(ABC): async def on_load(self) -> None: """ @@ -88,6 +98,7 @@ class ComfyExtension(ABC): Returns a list of nodes that this extension provides. """ + class Input: Image = ImageInput Audio = AudioInput @@ -95,10 +106,12 @@ class Input: Latent = LatentInput Video = VideoInput + class InputImpl: VideoFromFile = VideoFromFile VideoFromComponents = VideoFromComponents + class Types: VideoCodec = VideoCodec VideoContainer = VideoContainer @@ -107,6 +120,7 @@ class Types: MESH = MESH VOXEL = VOXEL + ComfyAPI = ComfyAPI_latest # Create a synchronous version of the API diff --git a/comfy_api/latest/_input_impl/video_types.py b/comfy_api/latest/_input_impl/video_types.py index 6a8fbe713..3d5000325 100644 --- a/comfy_api/latest/_input_impl/video_types.py +++ b/comfy_api/latest/_input_impl/video_types.py @@ -10,7 +10,13 @@ import json import numpy as np import math import torch -from .._util import VideoContainer, VideoCodec, VideoComponents, VideoSpeedPreset, quality_to_crf +from .._util import ( + VideoContainer, + VideoCodec, + VideoComponents, + VideoSpeedPreset, + quality_to_crf, +) def container_to_output_format(container_format: str | None) -> str | None: @@ -82,9 +88,9 @@ class VideoFromFile(VideoInput): """ if isinstance(self.__file, io.BytesIO): self.__file.seek(0) # Reset the BytesIO object to the beginning - with av.open(self.__file, mode='r') as container: + with av.open(self.__file, mode="r") as container: for stream in container.streams: - if stream.type == 'video': + if stream.type == "video": assert isinstance(stream, av.VideoStream) return stream.width, stream.height raise ValueError(f"No video stream found in file '{self.__file}'") @@ -138,7 +144,9 @@ class VideoFromFile(VideoInput): # 2. Try to estimate from duration and average_rate using only metadata if container.duration is not None and video_stream.average_rate: duration_seconds = float(container.duration / av.time_base) - estimated_frames = int(round(duration_seconds * float(video_stream.average_rate))) + estimated_frames = int( + round(duration_seconds * float(video_stream.average_rate)) + ) if estimated_frames > 0: return estimated_frames @@ -148,7 +156,9 @@ class VideoFromFile(VideoInput): and video_stream.average_rate ): duration_seconds = float(video_stream.duration * video_stream.time_base) - estimated_frames = int(round(duration_seconds * float(video_stream.average_rate))) + estimated_frames = int( + round(duration_seconds * float(video_stream.average_rate)) + ) if estimated_frames > 0: return estimated_frames @@ -160,7 +170,9 @@ class VideoFromFile(VideoInput): frame_count += 1 if frame_count == 0: - raise ValueError(f"Could not determine frame count for file '{self.__file}'") + raise ValueError( + f"Could not determine frame count for file '{self.__file}'" + ) return frame_count def get_frame_rate(self) -> Fraction: @@ -181,7 +193,9 @@ class VideoFromFile(VideoInput): if video_stream.frames and container.duration: duration_seconds = float(container.duration / av.time_base) if duration_seconds > 0: - return Fraction(video_stream.frames / duration_seconds).limit_denominator() + return Fraction( + video_stream.frames / duration_seconds + ).limit_denominator() # Last resort: match get_components_internal default return Fraction(1) @@ -195,53 +209,69 @@ class VideoFromFile(VideoInput): """ if isinstance(self.__file, io.BytesIO): self.__file.seek(0) - with av.open(self.__file, mode='r') as container: + with av.open(self.__file, mode="r") as container: return container.format.name def get_components_internal(self, container: InputContainer) -> VideoComponents: # Get video frames frames = [] for frame in container.decode(video=0): - img = frame.to_ndarray(format='rgb24') # shape: (H, W, 3) + img = frame.to_ndarray(format="rgb24") # shape: (H, W, 3) img = torch.from_numpy(img) / 255.0 # shape: (H, W, 3) frames.append(img) images = torch.stack(frames) if len(frames) > 0 else torch.zeros(0, 3, 0, 0) # Get frame rate - video_stream = next(s for s in container.streams if s.type == 'video') - frame_rate = Fraction(video_stream.average_rate) if video_stream and video_stream.average_rate else Fraction(1) + video_stream = next(s for s in container.streams if s.type == "video") + frame_rate = ( + Fraction(video_stream.average_rate) + if video_stream and video_stream.average_rate + else Fraction(1) + ) # Get audio if available audio = None try: container.seek(0) # Reset the container to the beginning for stream in container.streams: - if stream.type != 'audio': + if stream.type != "audio": continue assert isinstance(stream, av.AudioStream) audio_frames = [] for packet in container.demux(stream): for frame in packet.decode(): assert isinstance(frame, av.AudioFrame) - audio_frames.append(frame.to_ndarray()) # shape: (channels, samples) + audio_frames.append( + frame.to_ndarray() + ) # shape: (channels, samples) if len(audio_frames) > 0: - audio_data = np.concatenate(audio_frames, axis=1) # shape: (channels, total_samples) - audio_tensor = torch.from_numpy(audio_data).unsqueeze(0) # shape: (1, channels, total_samples) - audio = AudioInput({ - "waveform": audio_tensor, - "sample_rate": int(stream.sample_rate) if stream.sample_rate else 1, - }) + audio_data = np.concatenate( + audio_frames, axis=1 + ) # shape: (channels, total_samples) + audio_tensor = torch.from_numpy(audio_data).unsqueeze( + 0 + ) # shape: (1, channels, total_samples) + audio = AudioInput( + { + "waveform": audio_tensor, + "sample_rate": int(stream.sample_rate) + if stream.sample_rate + else 1, + } + ) except StopIteration: pass # No audio stream metadata = container.metadata - return VideoComponents(images=images, audio=audio, frame_rate=frame_rate, metadata=metadata) + return VideoComponents( + images=images, audio=audio, frame_rate=frame_rate, metadata=metadata + ) def get_components(self) -> VideoComponents: if isinstance(self.__file, io.BytesIO): self.__file.seek(0) # Reset the BytesIO object to the beginning - with av.open(self.__file, mode='r') as container: + with av.open(self.__file, mode="r") as container: return self.get_components_internal(container) raise ValueError(f"No video stream found in file '{self.__file}'") @@ -260,13 +290,23 @@ class VideoFromFile(VideoInput): ): if isinstance(self.__file, io.BytesIO): self.__file.seek(0) - with av.open(self.__file, mode='r') as container: + with av.open(self.__file, mode="r") as container: container_format = container.format.name - video_encoding = container.streams.video[0].codec.name if len(container.streams.video) > 0 else None + video_encoding = ( + container.streams.video[0].codec.name + if len(container.streams.video) > 0 + else None + ) reuse_streams = True - if format != VideoContainer.AUTO and format not in container_format.split(","): + if format != VideoContainer.AUTO and format not in container_format.split( + "," + ): reuse_streams = False - if codec != VideoCodec.AUTO and codec != video_encoding and video_encoding is not None: + if ( + codec != VideoCodec.AUTO + and codec != video_encoding + and video_encoding is not None + ): reuse_streams = False if quality is not None or speed is not None: reuse_streams = False @@ -309,8 +349,12 @@ class VideoFromFile(VideoInput): # Add streams to the new container stream_map = {} for stream in streams: - if isinstance(stream, (av.VideoStream, av.AudioStream, SubtitleStream)): - out_stream = output_container.add_stream_from_template(template=stream, opaque=True) + if isinstance( + stream, (av.VideoStream, av.AudioStream, SubtitleStream) + ): + out_stream = output_container.add_stream_from_template( + template=stream, opaque=True + ) stream_map[stream] = out_stream # Write packets to the new container @@ -338,7 +382,7 @@ class VideoFromComponents(VideoInput): return VideoComponents( images=self.__components.images, audio=self.__components.audio, - frame_rate=self.__components.frame_rate + frame_rate=self.__components.frame_rate, ) def save_to( @@ -399,7 +443,9 @@ class VideoFromComponents(VideoInput): if resolved_format == VideoContainer.MP4: container_options["movflags"] = "use_metadata_tags" - with av.open(path, mode='w', options=container_options, **extra_kwargs) as output: + with av.open( + path, mode="w", options=container_options, **extra_kwargs + ) as output: if metadata is not None: for key, value in metadata.items(): output.metadata[key] = json.dumps(value) @@ -409,48 +455,50 @@ class VideoFromComponents(VideoInput): video_stream.width = self.__components.images.shape[2] video_stream.height = self.__components.images.shape[1] - video_stream.pix_fmt = 'yuv420p' + video_stream.pix_fmt = "yuv420p" if resolved_codec == VideoCodec.VP9: video_stream.bit_rate = 0 if quality is not None: crf = quality_to_crf(quality, ffmpeg_codec) - video_stream.options['crf'] = str(crf) + video_stream.options["crf"] = str(crf) if speed is not None and speed != VideoSpeedPreset.AUTO: if isinstance(speed, str): speed = VideoSpeedPreset(speed) preset = speed.to_ffmpeg_preset(ffmpeg_codec) if resolved_codec == VideoCodec.VP9: - video_stream.options['cpu-used'] = preset + video_stream.options["cpu-used"] = preset else: - video_stream.options['preset'] = preset + video_stream.options["preset"] = preset # H.264-specific options if resolved_codec == VideoCodec.H264: if profile is not None: - video_stream.options['profile'] = profile + video_stream.options["profile"] = profile if tune is not None: - video_stream.options['tune'] = tune + video_stream.options["tune"] = tune # VP9-specific options if resolved_codec == VideoCodec.VP9: if row_mt: - video_stream.options['row-mt'] = '1' + video_stream.options["row-mt"] = "1" if tile_columns is not None: - video_stream.options['tile-columns'] = str(tile_columns) + video_stream.options["tile-columns"] = str(tile_columns) audio_sample_rate = 1 audio_stream: Optional[av.AudioStream] = None if self.__components.audio: - audio_sample_rate = int(self.__components.audio['sample_rate']) - audio_codec = 'libopus' if resolved_format == VideoContainer.WEBM else 'aac' + audio_sample_rate = int(self.__components.audio["sample_rate"]) + audio_codec = ( + "libopus" if resolved_format == VideoContainer.WEBM else "aac" + ) audio_stream = output.add_stream(audio_codec, rate=audio_sample_rate) for i, frame in enumerate(self.__components.images): img = (frame * 255).clamp(0, 255).byte().cpu().numpy() - video_frame = av.VideoFrame.from_ndarray(img, format='rgb24') - video_frame = video_frame.reformat(format='yuv420p') + video_frame = av.VideoFrame.from_ndarray(img, format="rgb24") + video_frame = video_frame.reformat(format="yuv420p") packet = video_stream.encode(video_frame) output.mux(packet) @@ -458,12 +506,19 @@ class VideoFromComponents(VideoInput): output.mux(packet) if audio_stream and self.__components.audio: - waveform = self.__components.audio['waveform'] - waveform = waveform[:, :, :math.ceil((audio_sample_rate / frame_rate) * self.__components.images.shape[0])] + waveform = self.__components.audio["waveform"] + waveform = waveform[ + :, + :, + : math.ceil( + (audio_sample_rate / frame_rate) + * self.__components.images.shape[0] + ), + ] audio_frame = av.AudioFrame.from_ndarray( waveform.movedim(2, 1).reshape(1, -1).float().numpy(), - format='flt', - layout='mono' if waveform.shape[1] == 1 else 'stereo' + format="flt", + layout="mono" if waveform.shape[1] == 1 else "stereo", ) audio_frame.sample_rate = audio_sample_rate audio_frame.pts = 0 diff --git a/comfy_api/latest/_util/__init__.py b/comfy_api/latest/_util/__init__.py index b93bca1aa..7c8060a1d 100644 --- a/comfy_api/latest/_util/__init__.py +++ b/comfy_api/latest/_util/__init__.py @@ -1,4 +1,10 @@ -from .video_types import VideoContainer, VideoCodec, VideoComponents, VideoSpeedPreset, quality_to_crf +from .video_types import ( + VideoContainer, + VideoCodec, + VideoComponents, + VideoSpeedPreset, + quality_to_crf, +) from .geometry_types import VOXEL, MESH from .image_types import SVG diff --git a/comfy_api/latest/_util/video_types.py b/comfy_api/latest/_util/video_types.py index b4a63ac16..c7f77f5c4 100644 --- a/comfy_api/latest/_util/video_types.py +++ b/comfy_api/latest/_util/video_types.py @@ -5,6 +5,7 @@ from fractions import Fraction from typing import Optional from .._input import ImageInput, AudioInput + class VideoCodec(str, Enum): AUTO = "auto" H264 = "h264" @@ -46,6 +47,7 @@ class VideoContainer(str, Enum): class VideoSpeedPreset(str, Enum): """Encoding speed presets - slower = better compression at same quality.""" + AUTO = "auto" FASTEST = "Fastest" FAST = "Fast" @@ -104,6 +106,7 @@ def quality_to_crf(quality: int, codec: str = "h264") -> int: # Default fallback return 23 + @dataclass class VideoComponents: """ @@ -114,5 +117,3 @@ class VideoComponents: frame_rate: Fraction audio: Optional[AudioInput] = None metadata: Optional[dict] = None - - diff --git a/comfy_extras/nodes_video.py b/comfy_extras/nodes_video.py index 8f0d76c8a..3b679a949 100644 --- a/comfy_extras/nodes_video.py +++ b/comfy_extras/nodes_video.py @@ -1,16 +1,19 @@ from __future__ import annotations -import os -import av -import torch -import folder_paths import json +import logging +import os from typing import Optional + +import av +import folder_paths +import torch from typing_extensions import override from fractions import Fraction from comfy_api.latest import ComfyExtension, io, ui, Input, InputImpl, Types from comfy.cli_args import args + class SaveWEBM(io.ComfyNode): @classmethod def define_schema(cls): @@ -23,7 +26,14 @@ class SaveWEBM(io.ComfyNode): 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."), + 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.", + ), ], hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo], is_output_node=True, @@ -31,8 +41,13 @@ class SaveWEBM(io.ComfyNode): @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] + 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], + ) ) file = f"{filename}_{counter:05}_.webm" @@ -46,23 +61,33 @@ class SaveWEBM(io.ComfyNode): container.metadata[x] = json.dumps(cls.hidden.extra_pnginfo[x]) codec_map = {"vp9": "libvpx-vp9", "av1": "libsvtav1"} - stream = container.add_stream(codec_map[codec], rate=Fraction(round(fps * 1000), 1000)) + stream = container.add_stream( + codec_map[codec], rate=Fraction(round(fps * 1000), 1000) + ) stream.width = images.shape[-2] stream.height = images.shape[-3] stream.pix_fmt = "yuv420p10le" if codec == "av1" else "yuv420p" stream.bit_rate = 0 - stream.options = {'crf': str(crf)} + stream.options = {"crf": str(crf)} if codec == "av1": stream.options["preset"] = "6" 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") + 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) container.mux(stream.encode()) container.close() - return io.NodeOutput(ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)])) + return io.NodeOutput( + ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)]) + ) + class SaveVideo(io.ComfyNode): @classmethod @@ -76,7 +101,7 @@ class SaveVideo(io.ComfyNode): step=1, display_name="Quality", tooltip="Output quality (0-100). Higher = better quality, larger files. " - "Internally maps to CRF: 100→CRF 12, 50→CRF 23, 0→CRF 40.", + "Internally maps to CRF: 100→CRF 12, 50→CRF 23, 0→CRF 40.", ) h264_speed = io.Combo.Input( "speed", @@ -84,7 +109,7 @@ class SaveVideo(io.ComfyNode): default="auto", display_name="Encoding Speed", tooltip="Encoding speed preset. Slower = better compression at same quality. " - "Maps to FFmpeg presets: Fastest=ultrafast, Balanced=medium, Best=veryslow.", + "Maps to FFmpeg presets: Fastest=ultrafast, Balanced=medium, Best=veryslow.", ) h264_profile = io.Combo.Input( "profile", @@ -92,16 +117,24 @@ class SaveVideo(io.ComfyNode): default="auto", display_name="Profile", tooltip="H.264 profile. 'baseline' for max compatibility (older devices), " - "'main' for standard use, 'high' for best quality/compression.", + "'main' for standard use, 'high' for best quality/compression.", advanced=True, ) h264_tune = io.Combo.Input( "tune", - options=["auto", "film", "animation", "grain", "stillimage", "fastdecode", "zerolatency"], + options=[ + "auto", + "film", + "animation", + "grain", + "stillimage", + "fastdecode", + "zerolatency", + ], default="auto", display_name="Tune", tooltip="Optimize encoding for specific content types. " - "'film' for live action, 'animation' for cartoons/anime, 'grain' to preserve film grain.", + "'film' for live action, 'animation' for cartoons/anime, 'grain' to preserve film grain.", advanced=True, ) @@ -114,7 +147,7 @@ class SaveVideo(io.ComfyNode): step=1, display_name="Quality", tooltip="Output quality (0-100). Higher = better quality, larger files. " - "Internally maps to CRF: 100→CRF 15, 50→CRF 33, 0→CRF 50.", + "Internally maps to CRF: 100→CRF 15, 50→CRF 33, 0→CRF 50.", ) vp9_speed = io.Combo.Input( "speed", @@ -122,7 +155,7 @@ class SaveVideo(io.ComfyNode): default="auto", display_name="Encoding Speed", tooltip="Encoding speed. Slower = better compression. " - "Maps to VP9 cpu-used: Fastest=0, Balanced=2, Best=4.", + "Maps to VP9 cpu-used: Fastest=0, Balanced=2, Best=4.", ) vp9_row_mt = io.Boolean.Input( "row_mt", @@ -137,7 +170,7 @@ class SaveVideo(io.ComfyNode): default="auto", display_name="Tile Columns", tooltip="Number of tile columns (as power of 2). More tiles = faster encoding " - "but slightly worse compression. 'auto' picks based on resolution.", + "but slightly worse compression. 'auto' picks based on resolution.", advanced=True, ) @@ -146,28 +179,39 @@ class SaveVideo(io.ComfyNode): display_name="Save Video", category="image/video", description="Saves video to the output directory. " - "When format/codec/quality differ from source, the video is re-encoded.", + "When format/codec/quality differ from source, the video is re-encoded.", 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. " - "Supports formatting like %date:yyyy-MM-dd%.", + "Supports formatting like %date:yyyy-MM-dd%.", + ), + io.DynamicCombo.Input( + "codec", + options=[ + io.DynamicCombo.Option("auto", []), + io.DynamicCombo.Option( + "h264", [h264_quality, h264_speed, h264_profile, h264_tune] + ), + io.DynamicCombo.Option( + "vp9", + [vp9_quality, vp9_speed, vp9_row_mt, vp9_tile_columns], + ), + ], + tooltip="Video codec. 'auto' preserves source when possible. " + "h264 outputs MP4, vp9 outputs WebM.", ), - io.DynamicCombo.Input("codec", options=[ - io.DynamicCombo.Option("auto", []), - io.DynamicCombo.Option("h264", [h264_quality, h264_speed, h264_profile, h264_tune]), - io.DynamicCombo.Option("vp9", [vp9_quality, vp9_speed, vp9_row_mt, vp9_tile_columns]), - ], tooltip="Video codec. 'auto' preserves source when possible. " - "h264 outputs MP4, vp9 outputs WebM."), ], hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo], is_output_node=True, ) @classmethod - def execute(cls, video: Input.Video, filename_prefix: str, codec: dict) -> io.NodeOutput: + def execute( + cls, video: Input.Video, filename_prefix: str, codec: dict + ) -> io.NodeOutput: selected_codec = codec.get("codec", "auto") quality = codec.get("quality") speed_str = codec.get("speed", "auto") @@ -201,11 +245,10 @@ class SaveVideo(io.ComfyNode): logging.warning(f"Invalid speed preset '{speed_str}', using default") width, height = video.get_dimensions() - full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path( - filename_prefix, - folder_paths.get_output_directory(), - width, - height + full_output_folder, filename, counter, subfolder, filename_prefix = ( + folder_paths.get_save_image_path( + filename_prefix, folder_paths.get_output_directory(), width, height + ) ) saved_metadata = None @@ -233,7 +276,9 @@ class SaveVideo(io.ComfyNode): tile_columns=int(tile_columns) if tile_columns != "auto" else None, ) - return io.NodeOutput(ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)])) + return io.NodeOutput( + ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)]) + ) class CreateVideo(io.ComfyNode): @@ -247,7 +292,9 @@ class CreateVideo(io.ComfyNode): 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."), + io.Audio.Input( + "audio", optional=True, tooltip="The audio to add to the video." + ), ], outputs=[ io.Video.Output(), @@ -255,11 +302,18 @@ class CreateVideo(io.ComfyNode): ) @classmethod - def execute(cls, images: Input.Image, fps: float, audio: Optional[Input.Audio] = None) -> io.NodeOutput: + def execute( + cls, images: Input.Image, fps: float, audio: Optional[Input.Audio] = None + ) -> io.NodeOutput: return io.NodeOutput( - InputImpl.VideoFromComponents(Types.VideoComponents(images=images, audio=audio, frame_rate=Fraction(fps))) + InputImpl.VideoFromComponents( + Types.VideoComponents( + images=images, audio=audio, frame_rate=Fraction(fps) + ) + ) ) + class GetVideoComponents(io.ComfyNode): @classmethod def define_schema(cls): @@ -269,7 +323,9 @@ class GetVideoComponents(io.ComfyNode): 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."), + io.Video.Input( + "video", tooltip="The video to extract components from." + ), ], outputs=[ io.Image.Output(display_name="images"), @@ -281,21 +337,29 @@ class GetVideoComponents(io.ComfyNode): @classmethod def execute(cls, video: Input.Video) -> io.NodeOutput: components = video.get_components() - return io.NodeOutput(components.images, components.audio, float(components.frame_rate)) + return io.NodeOutput( + components.images, components.audio, float(components.frame_rate) + ) class LoadVideo(io.ComfyNode): @classmethod def define_schema(cls): 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 = [ + 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"]) 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), + io.Combo.Input( + "file", options=sorted(files), upload=io.UploadType.video + ), ], outputs=[ io.Video.Output(), @@ -334,5 +398,6 @@ class VideoExtension(ComfyExtension): LoadVideo, ] + async def comfy_entrypoint() -> VideoExtension: return VideoExtension()