Delete comfy_api
Browse files- comfy_api/feature_flags.py +0 -69
- comfy_api/input/__init__.py +0 -8
- comfy_api/input/basic_types.py +0 -20
- comfy_api/input/video_types.py +0 -72
- comfy_api/input_impl/__init__.py +0 -7
- comfy_api/input_impl/video_types.py +0 -312
- comfy_api/torch_helpers/__init__.py +0 -5
- comfy_api/torch_helpers/torch_compile.py +0 -69
- comfy_api/util/__init__.py +0 -8
- comfy_api/util/video_types.py +0 -51
comfy_api/feature_flags.py
DELETED
|
@@ -1,69 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Feature flags module for ComfyUI WebSocket protocol negotiation.
|
| 3 |
-
|
| 4 |
-
This module handles capability negotiation between frontend and backend,
|
| 5 |
-
allowing graceful protocol evolution while maintaining backward compatibility.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from typing import Any, Dict
|
| 9 |
-
|
| 10 |
-
from comfy.cli_args import args
|
| 11 |
-
|
| 12 |
-
# Default server capabilities
|
| 13 |
-
SERVER_FEATURE_FLAGS: Dict[str, Any] = {
|
| 14 |
-
"supports_preview_metadata": True,
|
| 15 |
-
"max_upload_size": args.max_upload_size * 1024 * 1024, # Convert MB to bytes
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def get_connection_feature(
|
| 20 |
-
sockets_metadata: Dict[str, Dict[str, Any]],
|
| 21 |
-
sid: str,
|
| 22 |
-
feature_name: str,
|
| 23 |
-
default: Any = False
|
| 24 |
-
) -> Any:
|
| 25 |
-
"""
|
| 26 |
-
Get a feature flag value for a specific connection.
|
| 27 |
-
|
| 28 |
-
Args:
|
| 29 |
-
sockets_metadata: Dictionary of socket metadata
|
| 30 |
-
sid: Session ID of the connection
|
| 31 |
-
feature_name: Name of the feature to check
|
| 32 |
-
default: Default value if feature not found
|
| 33 |
-
|
| 34 |
-
Returns:
|
| 35 |
-
Feature value or default if not found
|
| 36 |
-
"""
|
| 37 |
-
if sid not in sockets_metadata:
|
| 38 |
-
return default
|
| 39 |
-
|
| 40 |
-
return sockets_metadata[sid].get("feature_flags", {}).get(feature_name, default)
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def supports_feature(
|
| 44 |
-
sockets_metadata: Dict[str, Dict[str, Any]],
|
| 45 |
-
sid: str,
|
| 46 |
-
feature_name: str
|
| 47 |
-
) -> bool:
|
| 48 |
-
"""
|
| 49 |
-
Check if a connection supports a specific feature.
|
| 50 |
-
|
| 51 |
-
Args:
|
| 52 |
-
sockets_metadata: Dictionary of socket metadata
|
| 53 |
-
sid: Session ID of the connection
|
| 54 |
-
feature_name: Name of the feature to check
|
| 55 |
-
|
| 56 |
-
Returns:
|
| 57 |
-
Boolean indicating if feature is supported
|
| 58 |
-
"""
|
| 59 |
-
return get_connection_feature(sockets_metadata, sid, feature_name, False) is True
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def get_server_features() -> Dict[str, Any]:
|
| 63 |
-
"""
|
| 64 |
-
Get the server's feature flags.
|
| 65 |
-
|
| 66 |
-
Returns:
|
| 67 |
-
Dictionary of server feature flags
|
| 68 |
-
"""
|
| 69 |
-
return SERVER_FEATURE_FLAGS.copy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
comfy_api/input/__init__.py
DELETED
|
@@ -1,8 +0,0 @@
|
|
| 1 |
-
from .basic_types import ImageInput, AudioInput
|
| 2 |
-
from .video_types import VideoInput
|
| 3 |
-
|
| 4 |
-
__all__ = [
|
| 5 |
-
"ImageInput",
|
| 6 |
-
"AudioInput",
|
| 7 |
-
"VideoInput",
|
| 8 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
comfy_api/input/basic_types.py
DELETED
|
@@ -1,20 +0,0 @@
|
|
| 1 |
-
import torch
|
| 2 |
-
from typing import TypedDict
|
| 3 |
-
|
| 4 |
-
ImageInput = torch.Tensor
|
| 5 |
-
"""
|
| 6 |
-
An image in format [B, H, W, C] where B is the batch size, C is the number of channels,
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
class AudioInput(TypedDict):
|
| 10 |
-
"""
|
| 11 |
-
TypedDict representing audio input.
|
| 12 |
-
"""
|
| 13 |
-
|
| 14 |
-
waveform: torch.Tensor
|
| 15 |
-
"""
|
| 16 |
-
Tensor in the format [B, C, T] where B is the batch size, C is the number of channels,
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
sample_rate: int
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
comfy_api/input/video_types.py
DELETED
|
@@ -1,72 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
from abc import ABC, abstractmethod
|
| 3 |
-
from typing import Optional, Union
|
| 4 |
-
import io
|
| 5 |
-
from comfy_api.util import VideoContainer, VideoCodec, VideoComponents
|
| 6 |
-
|
| 7 |
-
class VideoInput(ABC):
|
| 8 |
-
"""
|
| 9 |
-
Abstract base class for video input types.
|
| 10 |
-
"""
|
| 11 |
-
|
| 12 |
-
@abstractmethod
|
| 13 |
-
def get_components(self) -> VideoComponents:
|
| 14 |
-
"""
|
| 15 |
-
Abstract method to get the video components (images, audio, and frame rate).
|
| 16 |
-
|
| 17 |
-
Returns:
|
| 18 |
-
VideoComponents containing images, audio, and frame rate
|
| 19 |
-
"""
|
| 20 |
-
pass
|
| 21 |
-
|
| 22 |
-
@abstractmethod
|
| 23 |
-
def save_to(
|
| 24 |
-
self,
|
| 25 |
-
path: str,
|
| 26 |
-
format: VideoContainer = VideoContainer.AUTO,
|
| 27 |
-
codec: VideoCodec = VideoCodec.AUTO,
|
| 28 |
-
metadata: Optional[dict] = None
|
| 29 |
-
):
|
| 30 |
-
"""
|
| 31 |
-
Abstract method to save the video input to a file.
|
| 32 |
-
"""
|
| 33 |
-
pass
|
| 34 |
-
|
| 35 |
-
def get_stream_source(self) -> Union[str, io.BytesIO]:
|
| 36 |
-
"""
|
| 37 |
-
Get a streamable source for the video. This allows processing without
|
| 38 |
-
loading the entire video into memory.
|
| 39 |
-
|
| 40 |
-
Returns:
|
| 41 |
-
Either a file path (str) or a BytesIO object that can be opened with av.
|
| 42 |
-
|
| 43 |
-
Default implementation creates a BytesIO buffer, but subclasses should
|
| 44 |
-
override this for better performance when possible.
|
| 45 |
-
"""
|
| 46 |
-
buffer = io.BytesIO()
|
| 47 |
-
self.save_to(buffer)
|
| 48 |
-
buffer.seek(0)
|
| 49 |
-
return buffer
|
| 50 |
-
|
| 51 |
-
# Provide a default implementation, but subclasses can provide optimized versions
|
| 52 |
-
# if possible.
|
| 53 |
-
def get_dimensions(self) -> tuple[int, int]:
|
| 54 |
-
"""
|
| 55 |
-
Returns the dimensions of the video input.
|
| 56 |
-
|
| 57 |
-
Returns:
|
| 58 |
-
Tuple of (width, height)
|
| 59 |
-
"""
|
| 60 |
-
components = self.get_components()
|
| 61 |
-
return components.images.shape[2], components.images.shape[1]
|
| 62 |
-
|
| 63 |
-
def get_duration(self) -> float:
|
| 64 |
-
"""
|
| 65 |
-
Returns the duration of the video in seconds.
|
| 66 |
-
|
| 67 |
-
Returns:
|
| 68 |
-
Duration in seconds
|
| 69 |
-
"""
|
| 70 |
-
components = self.get_components()
|
| 71 |
-
frame_count = components.images.shape[0]
|
| 72 |
-
return float(frame_count / components.frame_rate)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
comfy_api/input_impl/__init__.py
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
from .video_types import VideoFromFile, VideoFromComponents
|
| 2 |
-
|
| 3 |
-
__all__ = [
|
| 4 |
-
# Implementations
|
| 5 |
-
"VideoFromFile",
|
| 6 |
-
"VideoFromComponents",
|
| 7 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
comfy_api/input_impl/video_types.py
DELETED
|
@@ -1,312 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
from av.container import InputContainer
|
| 3 |
-
from av.subtitles.stream import SubtitleStream
|
| 4 |
-
from fractions import Fraction
|
| 5 |
-
from typing import Optional
|
| 6 |
-
from comfy_api.input import AudioInput
|
| 7 |
-
import av
|
| 8 |
-
import io
|
| 9 |
-
import json
|
| 10 |
-
import numpy as np
|
| 11 |
-
import torch
|
| 12 |
-
from comfy_api.input import VideoInput
|
| 13 |
-
from comfy_api.util import VideoContainer, VideoCodec, VideoComponents
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
def container_to_output_format(container_format: str | None) -> str | None:
|
| 17 |
-
"""
|
| 18 |
-
A container's `format` may be a comma-separated list of formats.
|
| 19 |
-
E.g., iso container's `format` may be `mov,mp4,m4a,3gp,3g2,mj2`.
|
| 20 |
-
However, writing to a file/stream with `av.open` requires a single format,
|
| 21 |
-
or `None` to auto-detect.
|
| 22 |
-
"""
|
| 23 |
-
if not container_format:
|
| 24 |
-
return None # Auto-detect
|
| 25 |
-
|
| 26 |
-
if "," not in container_format:
|
| 27 |
-
return container_format
|
| 28 |
-
|
| 29 |
-
formats = container_format.split(",")
|
| 30 |
-
return formats[0]
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def get_open_write_kwargs(
|
| 34 |
-
dest: str | io.BytesIO, container_format: str, to_format: str | None
|
| 35 |
-
) -> dict:
|
| 36 |
-
"""Get kwargs for writing a `VideoFromFile` to a file/stream with `av.open`"""
|
| 37 |
-
open_kwargs = {
|
| 38 |
-
"mode": "w",
|
| 39 |
-
# If isobmff, preserve custom metadata tags (workflow, prompt, extra_pnginfo)
|
| 40 |
-
"options": {"movflags": "use_metadata_tags"},
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
is_write_to_buffer = isinstance(dest, io.BytesIO)
|
| 44 |
-
if is_write_to_buffer:
|
| 45 |
-
# Set output format explicitly, since it cannot be inferred from file extension
|
| 46 |
-
if to_format == VideoContainer.AUTO:
|
| 47 |
-
to_format = container_format.lower()
|
| 48 |
-
elif isinstance(to_format, str):
|
| 49 |
-
to_format = to_format.lower()
|
| 50 |
-
open_kwargs["format"] = container_to_output_format(to_format)
|
| 51 |
-
|
| 52 |
-
return open_kwargs
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
class VideoFromFile(VideoInput):
|
| 56 |
-
"""
|
| 57 |
-
Class representing video input from a file.
|
| 58 |
-
"""
|
| 59 |
-
|
| 60 |
-
def __init__(self, file: str | io.BytesIO):
|
| 61 |
-
"""
|
| 62 |
-
Initialize the VideoFromFile object based off of either a path on disk or a BytesIO object
|
| 63 |
-
containing the file contents.
|
| 64 |
-
"""
|
| 65 |
-
self.__file = file
|
| 66 |
-
|
| 67 |
-
def get_stream_source(self) -> str | io.BytesIO:
|
| 68 |
-
"""
|
| 69 |
-
Return the underlying file source for efficient streaming.
|
| 70 |
-
This avoids unnecessary memory copies when the source is already a file path.
|
| 71 |
-
"""
|
| 72 |
-
if isinstance(self.__file, io.BytesIO):
|
| 73 |
-
self.__file.seek(0)
|
| 74 |
-
return self.__file
|
| 75 |
-
|
| 76 |
-
def get_dimensions(self) -> tuple[int, int]:
|
| 77 |
-
"""
|
| 78 |
-
Returns the dimensions of the video input.
|
| 79 |
-
|
| 80 |
-
Returns:
|
| 81 |
-
Tuple of (width, height)
|
| 82 |
-
"""
|
| 83 |
-
if isinstance(self.__file, io.BytesIO):
|
| 84 |
-
self.__file.seek(0) # Reset the BytesIO object to the beginning
|
| 85 |
-
with av.open(self.__file, mode='r') as container:
|
| 86 |
-
for stream in container.streams:
|
| 87 |
-
if stream.type == 'video':
|
| 88 |
-
assert isinstance(stream, av.VideoStream)
|
| 89 |
-
return stream.width, stream.height
|
| 90 |
-
raise ValueError(f"No video stream found in file '{self.__file}'")
|
| 91 |
-
|
| 92 |
-
def get_duration(self) -> float:
|
| 93 |
-
"""
|
| 94 |
-
Returns the duration of the video in seconds.
|
| 95 |
-
|
| 96 |
-
Returns:
|
| 97 |
-
Duration in seconds
|
| 98 |
-
"""
|
| 99 |
-
if isinstance(self.__file, io.BytesIO):
|
| 100 |
-
self.__file.seek(0)
|
| 101 |
-
with av.open(self.__file, mode="r") as container:
|
| 102 |
-
if container.duration is not None:
|
| 103 |
-
return float(container.duration / av.time_base)
|
| 104 |
-
|
| 105 |
-
# Fallback: calculate from frame count and frame rate
|
| 106 |
-
video_stream = next(
|
| 107 |
-
(s for s in container.streams if s.type == "video"), None
|
| 108 |
-
)
|
| 109 |
-
if video_stream and video_stream.frames and video_stream.average_rate:
|
| 110 |
-
return float(video_stream.frames / video_stream.average_rate)
|
| 111 |
-
|
| 112 |
-
# Last resort: decode frames to count them
|
| 113 |
-
if video_stream and video_stream.average_rate:
|
| 114 |
-
frame_count = 0
|
| 115 |
-
container.seek(0)
|
| 116 |
-
for packet in container.demux(video_stream):
|
| 117 |
-
for _ in packet.decode():
|
| 118 |
-
frame_count += 1
|
| 119 |
-
if frame_count > 0:
|
| 120 |
-
return float(frame_count / video_stream.average_rate)
|
| 121 |
-
|
| 122 |
-
raise ValueError(f"Could not determine duration for file '{self.__file}'")
|
| 123 |
-
|
| 124 |
-
def get_components_internal(self, container: InputContainer) -> VideoComponents:
|
| 125 |
-
# Get video frames
|
| 126 |
-
frames = []
|
| 127 |
-
for frame in container.decode(video=0):
|
| 128 |
-
img = frame.to_ndarray(format='rgb24') # shape: (H, W, 3)
|
| 129 |
-
img = torch.from_numpy(img) / 255.0 # shape: (H, W, 3)
|
| 130 |
-
frames.append(img)
|
| 131 |
-
|
| 132 |
-
images = torch.stack(frames) if len(frames) > 0 else torch.zeros(0, 3, 0, 0)
|
| 133 |
-
|
| 134 |
-
# Get frame rate
|
| 135 |
-
video_stream = next(s for s in container.streams if s.type == 'video')
|
| 136 |
-
frame_rate = Fraction(video_stream.average_rate) if video_stream and video_stream.average_rate else Fraction(1)
|
| 137 |
-
|
| 138 |
-
# Get audio if available
|
| 139 |
-
audio = None
|
| 140 |
-
try:
|
| 141 |
-
container.seek(0) # Reset the container to the beginning
|
| 142 |
-
for stream in container.streams:
|
| 143 |
-
if stream.type != 'audio':
|
| 144 |
-
continue
|
| 145 |
-
assert isinstance(stream, av.AudioStream)
|
| 146 |
-
audio_frames = []
|
| 147 |
-
for packet in container.demux(stream):
|
| 148 |
-
for frame in packet.decode():
|
| 149 |
-
assert isinstance(frame, av.AudioFrame)
|
| 150 |
-
audio_frames.append(frame.to_ndarray()) # shape: (channels, samples)
|
| 151 |
-
if len(audio_frames) > 0:
|
| 152 |
-
audio_data = np.concatenate(audio_frames, axis=1) # shape: (channels, total_samples)
|
| 153 |
-
audio_tensor = torch.from_numpy(audio_data).unsqueeze(0) # shape: (1, channels, total_samples)
|
| 154 |
-
audio = AudioInput({
|
| 155 |
-
"waveform": audio_tensor,
|
| 156 |
-
"sample_rate": int(stream.sample_rate) if stream.sample_rate else 1,
|
| 157 |
-
})
|
| 158 |
-
except StopIteration:
|
| 159 |
-
pass # No audio stream
|
| 160 |
-
|
| 161 |
-
metadata = container.metadata
|
| 162 |
-
return VideoComponents(images=images, audio=audio, frame_rate=frame_rate, metadata=metadata)
|
| 163 |
-
|
| 164 |
-
def get_components(self) -> VideoComponents:
|
| 165 |
-
if isinstance(self.__file, io.BytesIO):
|
| 166 |
-
self.__file.seek(0) # Reset the BytesIO object to the beginning
|
| 167 |
-
with av.open(self.__file, mode='r') as container:
|
| 168 |
-
return self.get_components_internal(container)
|
| 169 |
-
raise ValueError(f"No video stream found in file '{self.__file}'")
|
| 170 |
-
|
| 171 |
-
def save_to(
|
| 172 |
-
self,
|
| 173 |
-
path: str | io.BytesIO,
|
| 174 |
-
format: VideoContainer = VideoContainer.AUTO,
|
| 175 |
-
codec: VideoCodec = VideoCodec.AUTO,
|
| 176 |
-
metadata: Optional[dict] = None
|
| 177 |
-
):
|
| 178 |
-
if isinstance(self.__file, io.BytesIO):
|
| 179 |
-
self.__file.seek(0) # Reset the BytesIO object to the beginning
|
| 180 |
-
with av.open(self.__file, mode='r') as container:
|
| 181 |
-
container_format = container.format.name
|
| 182 |
-
video_encoding = container.streams.video[0].codec.name if len(container.streams.video) > 0 else None
|
| 183 |
-
reuse_streams = True
|
| 184 |
-
if format != VideoContainer.AUTO and format not in container_format.split(","):
|
| 185 |
-
reuse_streams = False
|
| 186 |
-
if codec != VideoCodec.AUTO and codec != video_encoding and video_encoding is not None:
|
| 187 |
-
reuse_streams = False
|
| 188 |
-
|
| 189 |
-
if not reuse_streams:
|
| 190 |
-
components = self.get_components_internal(container)
|
| 191 |
-
video = VideoFromComponents(components)
|
| 192 |
-
return video.save_to(
|
| 193 |
-
path,
|
| 194 |
-
format=format,
|
| 195 |
-
codec=codec,
|
| 196 |
-
metadata=metadata
|
| 197 |
-
)
|
| 198 |
-
|
| 199 |
-
streams = container.streams
|
| 200 |
-
|
| 201 |
-
open_kwargs = get_open_write_kwargs(path, container_format, format)
|
| 202 |
-
with av.open(path, **open_kwargs) as output_container:
|
| 203 |
-
# Copy over the original metadata
|
| 204 |
-
for key, value in container.metadata.items():
|
| 205 |
-
if metadata is None or key not in metadata:
|
| 206 |
-
output_container.metadata[key] = value
|
| 207 |
-
|
| 208 |
-
# Add our new metadata
|
| 209 |
-
if metadata is not None:
|
| 210 |
-
for key, value in metadata.items():
|
| 211 |
-
if isinstance(value, str):
|
| 212 |
-
output_container.metadata[key] = value
|
| 213 |
-
else:
|
| 214 |
-
output_container.metadata[key] = json.dumps(value)
|
| 215 |
-
|
| 216 |
-
# Add streams to the new container
|
| 217 |
-
stream_map = {}
|
| 218 |
-
for stream in streams:
|
| 219 |
-
if isinstance(stream, (av.VideoStream, av.AudioStream, SubtitleStream)):
|
| 220 |
-
out_stream = output_container.add_stream_from_template(template=stream, opaque=True)
|
| 221 |
-
stream_map[stream] = out_stream
|
| 222 |
-
|
| 223 |
-
# Write packets to the new container
|
| 224 |
-
for packet in container.demux():
|
| 225 |
-
if packet.stream in stream_map and packet.dts is not None:
|
| 226 |
-
packet.stream = stream_map[packet.stream]
|
| 227 |
-
output_container.mux(packet)
|
| 228 |
-
|
| 229 |
-
class VideoFromComponents(VideoInput):
|
| 230 |
-
"""
|
| 231 |
-
Class representing video input from tensors.
|
| 232 |
-
"""
|
| 233 |
-
|
| 234 |
-
def __init__(self, components: VideoComponents):
|
| 235 |
-
self.__components = components
|
| 236 |
-
|
| 237 |
-
def get_components(self) -> VideoComponents:
|
| 238 |
-
return VideoComponents(
|
| 239 |
-
images=self.__components.images,
|
| 240 |
-
audio=self.__components.audio,
|
| 241 |
-
frame_rate=self.__components.frame_rate
|
| 242 |
-
)
|
| 243 |
-
|
| 244 |
-
def save_to(
|
| 245 |
-
self,
|
| 246 |
-
path: str,
|
| 247 |
-
format: VideoContainer = VideoContainer.AUTO,
|
| 248 |
-
codec: VideoCodec = VideoCodec.AUTO,
|
| 249 |
-
metadata: Optional[dict] = None
|
| 250 |
-
):
|
| 251 |
-
if format != VideoContainer.AUTO and format != VideoContainer.MP4:
|
| 252 |
-
raise ValueError("Only MP4 format is supported for now")
|
| 253 |
-
if codec != VideoCodec.AUTO and codec != VideoCodec.H264:
|
| 254 |
-
raise ValueError("Only H264 codec is supported for now")
|
| 255 |
-
with av.open(path, mode='w', options={'movflags': 'use_metadata_tags'}) as output:
|
| 256 |
-
# Add metadata before writing any streams
|
| 257 |
-
if metadata is not None:
|
| 258 |
-
for key, value in metadata.items():
|
| 259 |
-
output.metadata[key] = json.dumps(value)
|
| 260 |
-
|
| 261 |
-
frame_rate = Fraction(round(self.__components.frame_rate * 1000), 1000)
|
| 262 |
-
# Create a video stream
|
| 263 |
-
video_stream = output.add_stream('h264', rate=frame_rate)
|
| 264 |
-
video_stream.width = self.__components.images.shape[2]
|
| 265 |
-
video_stream.height = self.__components.images.shape[1]
|
| 266 |
-
video_stream.pix_fmt = 'yuv420p'
|
| 267 |
-
|
| 268 |
-
# Create an audio stream
|
| 269 |
-
audio_sample_rate = 1
|
| 270 |
-
audio_stream: Optional[av.AudioStream] = None
|
| 271 |
-
if self.__components.audio:
|
| 272 |
-
audio_sample_rate = int(self.__components.audio['sample_rate'])
|
| 273 |
-
audio_stream = output.add_stream('aac', rate=audio_sample_rate)
|
| 274 |
-
audio_stream.sample_rate = audio_sample_rate
|
| 275 |
-
audio_stream.format = 'fltp'
|
| 276 |
-
|
| 277 |
-
# Encode video
|
| 278 |
-
for i, frame in enumerate(self.__components.images):
|
| 279 |
-
img = (frame * 255).clamp(0, 255).byte().cpu().numpy() # shape: (H, W, 3)
|
| 280 |
-
frame = av.VideoFrame.from_ndarray(img, format='rgb24')
|
| 281 |
-
frame = frame.reformat(format='yuv420p') # Convert to YUV420P as required by h264
|
| 282 |
-
packet = video_stream.encode(frame)
|
| 283 |
-
output.mux(packet)
|
| 284 |
-
|
| 285 |
-
# Flush video
|
| 286 |
-
packet = video_stream.encode(None)
|
| 287 |
-
output.mux(packet)
|
| 288 |
-
|
| 289 |
-
if audio_stream and self.__components.audio:
|
| 290 |
-
# Encode audio
|
| 291 |
-
samples_per_frame = int(audio_sample_rate / frame_rate)
|
| 292 |
-
num_frames = self.__components.audio['waveform'].shape[2] // samples_per_frame
|
| 293 |
-
for i in range(num_frames):
|
| 294 |
-
start = i * samples_per_frame
|
| 295 |
-
end = start + samples_per_frame
|
| 296 |
-
# TODO(Feature) - Add support for stereo audio
|
| 297 |
-
chunk = (
|
| 298 |
-
self.__components.audio["waveform"][0, 0, start:end]
|
| 299 |
-
.unsqueeze(0)
|
| 300 |
-
.contiguous()
|
| 301 |
-
.numpy()
|
| 302 |
-
)
|
| 303 |
-
audio_frame = av.AudioFrame.from_ndarray(chunk, format='fltp', layout='mono')
|
| 304 |
-
audio_frame.sample_rate = audio_sample_rate
|
| 305 |
-
audio_frame.pts = i * samples_per_frame
|
| 306 |
-
for packet in audio_stream.encode(audio_frame):
|
| 307 |
-
output.mux(packet)
|
| 308 |
-
|
| 309 |
-
# Flush audio
|
| 310 |
-
for packet in audio_stream.encode(None):
|
| 311 |
-
output.mux(packet)
|
| 312 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
comfy_api/torch_helpers/__init__.py
DELETED
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
from .torch_compile import set_torch_compile_wrapper
|
| 2 |
-
|
| 3 |
-
__all__ = [
|
| 4 |
-
"set_torch_compile_wrapper",
|
| 5 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
comfy_api/torch_helpers/torch_compile.py
DELETED
|
@@ -1,69 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
import torch
|
| 3 |
-
|
| 4 |
-
import comfy.utils
|
| 5 |
-
from comfy.patcher_extension import WrappersMP
|
| 6 |
-
from typing import TYPE_CHECKING, Callable, Optional
|
| 7 |
-
if TYPE_CHECKING:
|
| 8 |
-
from comfy.model_patcher import ModelPatcher
|
| 9 |
-
from comfy.patcher_extension import WrapperExecutor
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
COMPILE_KEY = "torch.compile"
|
| 13 |
-
TORCH_COMPILE_KWARGS = "torch_compile_kwargs"
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
def apply_torch_compile_factory(compiled_module_dict: dict[str, Callable]) -> Callable:
|
| 17 |
-
'''
|
| 18 |
-
Create a wrapper that will refer to the compiled_diffusion_model.
|
| 19 |
-
'''
|
| 20 |
-
def apply_torch_compile_wrapper(executor: WrapperExecutor, *args, **kwargs):
|
| 21 |
-
try:
|
| 22 |
-
orig_modules = {}
|
| 23 |
-
for key, value in compiled_module_dict.items():
|
| 24 |
-
orig_modules[key] = comfy.utils.get_attr(executor.class_obj, key)
|
| 25 |
-
comfy.utils.set_attr(executor.class_obj, key, value)
|
| 26 |
-
return executor(*args, **kwargs)
|
| 27 |
-
finally:
|
| 28 |
-
for key, value in orig_modules.items():
|
| 29 |
-
comfy.utils.set_attr(executor.class_obj, key, value)
|
| 30 |
-
return apply_torch_compile_wrapper
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def set_torch_compile_wrapper(model: ModelPatcher, backend: str, options: Optional[dict[str,str]]=None,
|
| 34 |
-
mode: Optional[str]=None, fullgraph=False, dynamic: Optional[bool]=None,
|
| 35 |
-
keys: list[str]=["diffusion_model"], *args, **kwargs):
|
| 36 |
-
'''
|
| 37 |
-
Perform torch.compile that will be applied at sample time for either the whole model or specific params of the BaseModel instance.
|
| 38 |
-
|
| 39 |
-
When keys is None, it will default to using ["diffusion_model"], compiling the whole diffusion_model.
|
| 40 |
-
When a list of keys is provided, it will perform torch.compile on only the selected modules.
|
| 41 |
-
'''
|
| 42 |
-
# clear out any other torch.compile wrappers
|
| 43 |
-
model.remove_wrappers_with_key(WrappersMP.APPLY_MODEL, COMPILE_KEY)
|
| 44 |
-
# if no keys, default to 'diffusion_model'
|
| 45 |
-
if not keys:
|
| 46 |
-
keys = ["diffusion_model"]
|
| 47 |
-
# create kwargs dict that can be referenced later
|
| 48 |
-
compile_kwargs = {
|
| 49 |
-
"backend": backend,
|
| 50 |
-
"options": options,
|
| 51 |
-
"mode": mode,
|
| 52 |
-
"fullgraph": fullgraph,
|
| 53 |
-
"dynamic": dynamic,
|
| 54 |
-
}
|
| 55 |
-
# get a dict of compiled keys
|
| 56 |
-
compiled_modules = {}
|
| 57 |
-
for key in keys:
|
| 58 |
-
compiled_modules[key] = torch.compile(
|
| 59 |
-
model=model.get_model_object(key),
|
| 60 |
-
**compile_kwargs,
|
| 61 |
-
)
|
| 62 |
-
# add torch.compile wrapper
|
| 63 |
-
wrapper_func = apply_torch_compile_factory(
|
| 64 |
-
compiled_module_dict=compiled_modules,
|
| 65 |
-
)
|
| 66 |
-
# store wrapper to run on BaseModel's apply_model function
|
| 67 |
-
model.add_wrapper_with_key(WrappersMP.APPLY_MODEL, COMPILE_KEY, wrapper_func)
|
| 68 |
-
# keep compile kwargs for reference
|
| 69 |
-
model.model_options[TORCH_COMPILE_KWARGS] = compile_kwargs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
comfy_api/util/__init__.py
DELETED
|
@@ -1,8 +0,0 @@
|
|
| 1 |
-
from .video_types import VideoContainer, VideoCodec, VideoComponents
|
| 2 |
-
|
| 3 |
-
__all__ = [
|
| 4 |
-
# Utility Types
|
| 5 |
-
"VideoContainer",
|
| 6 |
-
"VideoCodec",
|
| 7 |
-
"VideoComponents",
|
| 8 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
comfy_api/util/video_types.py
DELETED
|
@@ -1,51 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
from dataclasses import dataclass
|
| 3 |
-
from enum import Enum
|
| 4 |
-
from fractions import Fraction
|
| 5 |
-
from typing import Optional
|
| 6 |
-
from comfy_api.input import ImageInput, AudioInput
|
| 7 |
-
|
| 8 |
-
class VideoCodec(str, Enum):
|
| 9 |
-
AUTO = "auto"
|
| 10 |
-
H264 = "h264"
|
| 11 |
-
|
| 12 |
-
@classmethod
|
| 13 |
-
def as_input(cls) -> list[str]:
|
| 14 |
-
"""
|
| 15 |
-
Returns a list of codec names that can be used as node input.
|
| 16 |
-
"""
|
| 17 |
-
return [member.value for member in cls]
|
| 18 |
-
|
| 19 |
-
class VideoContainer(str, Enum):
|
| 20 |
-
AUTO = "auto"
|
| 21 |
-
MP4 = "mp4"
|
| 22 |
-
|
| 23 |
-
@classmethod
|
| 24 |
-
def as_input(cls) -> list[str]:
|
| 25 |
-
"""
|
| 26 |
-
Returns a list of container names that can be used as node input.
|
| 27 |
-
"""
|
| 28 |
-
return [member.value for member in cls]
|
| 29 |
-
|
| 30 |
-
@classmethod
|
| 31 |
-
def get_extension(cls, value) -> str:
|
| 32 |
-
"""
|
| 33 |
-
Returns the file extension for the container.
|
| 34 |
-
"""
|
| 35 |
-
if isinstance(value, str):
|
| 36 |
-
value = cls(value)
|
| 37 |
-
if value == VideoContainer.MP4 or value == VideoContainer.AUTO:
|
| 38 |
-
return "mp4"
|
| 39 |
-
return ""
|
| 40 |
-
|
| 41 |
-
@dataclass
|
| 42 |
-
class VideoComponents:
|
| 43 |
-
"""
|
| 44 |
-
Dataclass representing the components of a video.
|
| 45 |
-
"""
|
| 46 |
-
|
| 47 |
-
images: ImageInput
|
| 48 |
-
frame_rate: Fraction
|
| 49 |
-
audio: Optional[AudioInput] = None
|
| 50 |
-
metadata: Optional[dict] = None
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|