Let a connected video encode without a frames connection
This commit is contained in:
@@ -12,7 +12,8 @@ exactly one saved-video surface per node.
|
||||
## Controls
|
||||
|
||||
- **frames**: BHWC frame batch to encode. Odd dimensions are cropped down one
|
||||
pixel to the even sizes H.264 requires.
|
||||
pixel to the even sizes H.264 requires. Optional only because a connected
|
||||
`video` brings its own frames — connect one or the other.
|
||||
- **fps**: Playback frame rate. Wire Load Video's `fps` output to keep the
|
||||
source timing, including fractional rates like 29.97.
|
||||
- **filename_prefix**: Path under the output folder; subfolders are created
|
||||
|
||||
@@ -35,10 +35,6 @@ class AusBossSaveVideo:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"frames": (
|
||||
"IMAGE",
|
||||
{"tooltip": "BHWC frame batch to encode."},
|
||||
),
|
||||
"fps": (
|
||||
"FLOAT",
|
||||
{
|
||||
@@ -71,7 +67,15 @@ class AusBossSaveVideo:
|
||||
},
|
||||
),
|
||||
},
|
||||
# frames leads the optional group so the socket order the frontend
|
||||
# draws — frames, audio, video — is the order saved workflows
|
||||
# already link against. It is optional only because a connected
|
||||
# video carries its own frames; one of the two must be present.
|
||||
"optional": {
|
||||
"frames": (
|
||||
"IMAGE",
|
||||
{"tooltip": "BHWC frame batch to encode. Not needed when a video is connected."},
|
||||
),
|
||||
"audio": (
|
||||
"AUDIO",
|
||||
{"tooltip": "Optional track muxed into the file, e.g. Load Video's audio output."},
|
||||
@@ -96,7 +100,7 @@ class AusBossSaveVideo:
|
||||
FUNCTION = "save"
|
||||
|
||||
async def save(
|
||||
self, frames, fps, filename_prefix, crf, audio=None, video=None, prompt=None, extra_pnginfo=None
|
||||
self, fps, filename_prefix, crf, frames=None, audio=None, video=None, prompt=None, extra_pnginfo=None
|
||||
):
|
||||
if folder_paths is None:
|
||||
raise RuntimeError("Save Video requires ComfyUI's folder_paths at runtime.")
|
||||
@@ -111,6 +115,10 @@ class AusBossSaveVideo:
|
||||
fps, notice = resolve_encode_fps(float(fps), video_fps)
|
||||
if notice:
|
||||
print(notice)
|
||||
elif frames is None:
|
||||
raise ValueError(
|
||||
"Save Video: connect either a frames batch or a video to encode."
|
||||
)
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = (
|
||||
folder_paths.get_save_image_path(
|
||||
filename_prefix,
|
||||
|
||||
@@ -191,7 +191,7 @@ class SaveVideoHelperTests(unittest.TestCase):
|
||||
patch.object(node_save_video, "encode_video", return_value=(576, 1024, 188)),
|
||||
):
|
||||
result = run_node(node_save_video.AusBossSaveVideo().save(
|
||||
gradient_batch(1, 32, 32), 24.0, "AusBoss/video", 19
|
||||
frames=gradient_batch(1, 32, 32), fps=24.0, filename_prefix="AusBoss/video", crf=19
|
||||
))
|
||||
|
||||
self.assertEqual(result["ui"]["images"], [{
|
||||
@@ -262,7 +262,8 @@ class CoreVideoInputTests(unittest.TestCase):
|
||||
) as encode,
|
||||
):
|
||||
result = run_node(node_save_video.AusBossSaveVideo().save(
|
||||
gradient_batch(1, 8, 8), 16.0, "AusBoss/video", 19, video=connected
|
||||
frames=gradient_batch(1, 8, 8), fps=16.0, filename_prefix="AusBoss/video", crf=19,
|
||||
video=connected,
|
||||
))
|
||||
|
||||
_path, encoded_frames, encoded_fps, encoded_audio, _crf, _metadata = encode.call_args.args
|
||||
@@ -279,18 +280,53 @@ class CoreVideoInputTests(unittest.TestCase):
|
||||
node_save_video, "encode_video", return_value=(8, 8, 2)
|
||||
) as encode,
|
||||
):
|
||||
run_node(node_save_video.AusBossSaveVideo().save(images, 12.0, "AusBoss/video", 19))
|
||||
run_node(node_save_video.AusBossSaveVideo().save(
|
||||
frames=images, fps=12.0, filename_prefix="AusBoss/video", crf=19
|
||||
))
|
||||
|
||||
_path, encoded_frames, encoded_fps, _audio, _crf, _metadata = encode.call_args.args
|
||||
self.assertIs(encoded_frames, images)
|
||||
self.assertAlmostEqual(encoded_fps, 12.0)
|
||||
|
||||
def test_the_video_input_is_optional_and_declared_after_audio(self):
|
||||
optional = node_save_video.AusBossSaveVideo.INPUT_TYPES()["optional"]
|
||||
self.assertEqual(list(optional), ["audio", "video"])
|
||||
self.assertEqual(optional["video"][0], "VIDEO")
|
||||
required = node_save_video.AusBossSaveVideo.INPUT_TYPES()["required"]
|
||||
self.assertEqual(list(required), ["frames", "fps", "filename_prefix", "crf"])
|
||||
def test_the_link_slot_order_saved_workflows_rely_on_is_unchanged(self):
|
||||
# Saved workflows address inputs by slot index, and the frontend numbers
|
||||
# them by walking required then optional and keeping the link types. A
|
||||
# connected video carries its own frames, so frames leads the optional
|
||||
# group rather than sitting in required — the slot order must not move.
|
||||
spec = node_save_video.AusBossSaveVideo.INPUT_TYPES()
|
||||
widget_types = {"INT", "FLOAT", "STRING", "BOOLEAN"}
|
||||
sockets = [
|
||||
name
|
||||
for group in ("required", "optional")
|
||||
for name, definition in spec.get(group, {}).items()
|
||||
if isinstance(definition[0], str) and definition[0] not in widget_types
|
||||
]
|
||||
self.assertEqual(sockets, ["frames", "audio", "video"])
|
||||
self.assertEqual(spec["optional"]["frames"][0], "IMAGE")
|
||||
self.assertEqual(spec["optional"]["video"][0], "VIDEO")
|
||||
self.assertEqual(list(spec["required"]), ["fps", "filename_prefix", "crf"])
|
||||
|
||||
def test_a_video_alone_encodes_without_a_frames_connection(self):
|
||||
images = gradient_batch(4, 16, 16)
|
||||
connected = FakeCoreVideo(images, None, Fraction(12, 1))
|
||||
with (
|
||||
patch.object(node_save_video, "folder_paths", FakeFolderPaths),
|
||||
patch.object(node_save_video, "encode_video", return_value=(16, 16, 4)) as encode,
|
||||
):
|
||||
run_node(node_save_video.AusBossSaveVideo().save(
|
||||
fps=16.0, filename_prefix="AusBoss/video", crf=19, video=connected
|
||||
))
|
||||
|
||||
_path, encoded_frames, encoded_fps, _audio, _crf, _metadata = encode.call_args.args
|
||||
self.assertIs(encoded_frames, images)
|
||||
self.assertAlmostEqual(encoded_fps, 12.0)
|
||||
|
||||
def test_neither_frames_nor_video_reports_what_to_connect(self):
|
||||
with patch.object(node_save_video, "folder_paths", FakeFolderPaths):
|
||||
with self.assertRaisesRegex(ValueError, "frames batch or a video"):
|
||||
run_node(node_save_video.AusBossSaveVideo().save(
|
||||
fps=16.0, filename_prefix="AusBoss/video", crf=19
|
||||
))
|
||||
|
||||
|
||||
class AsyncSaveTests(unittest.TestCase):
|
||||
@@ -323,7 +359,8 @@ class AsyncSaveTests(unittest.TestCase):
|
||||
patch.object(node_save_video, "encode_video", slow_encode),
|
||||
):
|
||||
await node_save_video.AusBossSaveVideo().save(
|
||||
gradient_batch(1, 32, 32), 24.0, "AusBoss/video", 19
|
||||
frames=gradient_batch(1, 32, 32), fps=24.0,
|
||||
filename_prefix="AusBoss/video", crf=19,
|
||||
)
|
||||
finally:
|
||||
beat.cancel()
|
||||
|
||||
Reference in New Issue
Block a user