Adds a comfy-free helper that retimes a source frame sequence to a target fps while preserving real duration (VideoHelperSuite-style force_rate) — 0 means off, dropping/duplicating frames otherwise. Needed by the upcoming Multi Video Loader nodes.
186 lines
5.6 KiB
Python
186 lines
5.6 KiB
Python
# Multi Loader core tests — part of ComfyUI-AI2Go-Utils. GPL-3.0.
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from nodes.multi_loader_core import MAX_FILES, parse_files
|
|
|
|
|
|
def test_parse_valid_list():
|
|
raw = json.dumps([
|
|
{"name": "fox.png", "subfolder": "", "type": "input"},
|
|
{"name": "city.jpg"}, # subfolder/type optional
|
|
])
|
|
files = parse_files(raw)
|
|
assert files == [
|
|
{"name": "fox.png", "subfolder": "", "type": "input", "enabled": True},
|
|
{"name": "city.jpg", "subfolder": "", "type": "input", "enabled": True},
|
|
]
|
|
|
|
|
|
def test_parse_enabled_defaults_true():
|
|
files = parse_files(json.dumps([{"name": "a.png"}]))
|
|
assert files[0]["enabled"] is True
|
|
|
|
|
|
def test_parse_enabled_false_preserved():
|
|
files = parse_files(json.dumps([{"name": "a.png", "enabled": False}]))
|
|
assert files[0]["enabled"] is False
|
|
|
|
|
|
def test_parse_enabled_non_bool_raises():
|
|
with pytest.raises(ValueError, match="File #1"):
|
|
parse_files(json.dumps([{"name": "a.png", "enabled": "true"}]))
|
|
|
|
|
|
def test_parse_keeps_subfolder():
|
|
files = parse_files(json.dumps([{"name": "a.png", "subfolder": "refs"}]))
|
|
assert files[0]["subfolder"] == "refs"
|
|
|
|
|
|
@pytest.mark.parametrize("raw", ["", " ", "[]"])
|
|
def test_parse_empty_raises(raw):
|
|
with pytest.raises(ValueError, match="No files loaded"):
|
|
parse_files(raw)
|
|
|
|
|
|
def test_parse_malformed_json_raises():
|
|
with pytest.raises(ValueError, match="Malformed files_json"):
|
|
parse_files("[{oops")
|
|
|
|
|
|
def test_parse_non_list_raises():
|
|
with pytest.raises(ValueError, match="JSON array"):
|
|
parse_files('{"name": "a.png"}')
|
|
|
|
|
|
@pytest.mark.parametrize("entry", [42, "a.png", {}, {"name": ""}, {"name": 3}])
|
|
def test_parse_bad_entry_raises(entry):
|
|
with pytest.raises(ValueError, match="File #1"):
|
|
parse_files(json.dumps([entry]))
|
|
|
|
|
|
def test_parse_over_ceiling_raises():
|
|
raw = json.dumps([{"name": f"f{i}.png"} for i in range(MAX_FILES + 1)])
|
|
with pytest.raises(ValueError, match="at most 8"):
|
|
parse_files(raw)
|
|
|
|
|
|
from nodes.multi_loader_core import downscale_size
|
|
|
|
|
|
def test_downscale_off_untouched():
|
|
assert downscale_size(2400, 1200, "off", 1200) == ((2400, 1200), None)
|
|
|
|
|
|
def test_downscale_below_threshold_untouched():
|
|
for mode in ("keep aspect ratio", "crop to square", "stretch to square"):
|
|
assert downscale_size(800, 600, mode, 1200) == ((800, 600), None)
|
|
|
|
|
|
def test_downscale_exactly_max_untouched():
|
|
assert downscale_size(1200, 1200, "keep aspect ratio", 1200) == ((1200, 1200), None)
|
|
|
|
|
|
def test_fit_caps_longest_edge():
|
|
assert downscale_size(2400, 1200, "keep aspect ratio", 1200) == ((1200, 600), None)
|
|
assert downscale_size(1200, 2400, "keep aspect ratio", 1200) == ((600, 1200), None)
|
|
|
|
|
|
def test_fit_never_returns_zero():
|
|
(w, h), box = downscale_size(5000, 2, "keep aspect ratio", 1200)
|
|
assert (w, h) == (1200, 1) and box is None
|
|
|
|
|
|
def test_crop_centers_square_then_caps():
|
|
# shorter edge 1200 -> centered 1200x1200 box, already <= max
|
|
assert downscale_size(2400, 1200, "crop to square", 1200) == ((1200, 1200), (600, 0, 1800, 1200))
|
|
# shorter edge 800 < max -> square of 800, no upscale
|
|
assert downscale_size(2400, 800, "crop to square", 1200) == ((800, 800), (800, 0, 1600, 800))
|
|
# shorter edge 2000 > max -> crop 2000-box, downscale to 1200
|
|
assert downscale_size(3000, 2000, "crop to square", 1200) == ((1200, 1200), (500, 0, 2500, 2000))
|
|
|
|
|
|
def test_stretch_forces_square():
|
|
assert downscale_size(2400, 1200, "stretch to square", 1200) == ((1200, 1200), None)
|
|
|
|
|
|
def test_unknown_mode_raises():
|
|
import pytest
|
|
with pytest.raises(ValueError, match="downscale_mode"):
|
|
downscale_size(100, 100, "zoom", 1200)
|
|
|
|
|
|
from nodes.multi_loader_core import remove_file
|
|
|
|
|
|
def _files(*names):
|
|
return [{"name": n, "subfolder": "", "type": "input"} for n in names]
|
|
|
|
|
|
def test_remove_middle_shifts_up():
|
|
new, moved = remove_file(_files("a.png", "b.png", "c.png"), 1)
|
|
assert [f["name"] for f in new] == ["a.png", "c.png"]
|
|
assert moved == ["c.png"]
|
|
|
|
|
|
def test_remove_last_moves_nothing():
|
|
new, moved = remove_file(_files("a.png", "b.png"), 1)
|
|
assert [f["name"] for f in new] == ["a.png"]
|
|
assert moved == []
|
|
|
|
|
|
def test_remove_does_not_mutate_input():
|
|
original = _files("a.png", "b.png")
|
|
remove_file(original, 0)
|
|
assert len(original) == 2
|
|
|
|
|
|
def test_remove_bad_index_raises():
|
|
import pytest
|
|
with pytest.raises(IndexError):
|
|
remove_file(_files("a.png"), 5)
|
|
|
|
|
|
from nodes.multi_loader_core import resample_frame_indices
|
|
|
|
|
|
def test_resample_upsample_24_to_30_preserves_duration():
|
|
# 240 frames @ 24fps = 10s; retimed to 30fps -> 300 frames, still ending at the last
|
|
# source frame (duration preserved, not frame count).
|
|
idx = resample_frame_indices(240, 24, 30)
|
|
assert len(idx) == 300
|
|
assert idx[-1] == 239
|
|
|
|
|
|
def test_resample_downsample_60_to_30_steps_by_two():
|
|
idx = resample_frame_indices(600, 60, 30)
|
|
assert len(idx) == 300
|
|
assert idx == [i * 2 for i in range(300)]
|
|
|
|
|
|
def test_resample_upsample_24_to_48_duplicates_each_frame():
|
|
idx = resample_frame_indices(240, 24, 48)
|
|
assert len(idx) == 480
|
|
for i in range(240):
|
|
assert idx[2 * i] == i
|
|
assert idx[2 * i + 1] == i
|
|
|
|
|
|
def test_resample_identity_when_rates_equal():
|
|
idx = resample_frame_indices(240, 24, 24)
|
|
assert idx == list(range(240))
|
|
|
|
|
|
def test_resample_target_zero_means_no_resampling():
|
|
assert resample_frame_indices(240, 24, 0) is None
|
|
|
|
|
|
def test_resample_src_count_zero_yields_empty():
|
|
assert resample_frame_indices(0, 24, 30) == []
|
|
|
|
|
|
def test_resample_src_fps_zero_raises():
|
|
with pytest.raises(ValueError):
|
|
resample_frame_indices(240, 0, 30)
|