Files

104 lines
4.3 KiB
Python

"""Video preparation: the loader choices (rate, skip, cut, every Nth, cap, crop, size), on made-up frames and a real file."""
import json
import pytest
import torch
from modules.media.load import prepare as prep
def _clip(n=48, h=40, w=80):
"""Frame i is filled with the value i/255, so which frames survived can be read off the pixels."""
return torch.stack([torch.full((h, w, 3), i / 255.0) for i in range(n)])
def _ids(frames):
return [round(float(f[0, 0, 0]) * 255) for f in frames]
def test_frames_are_picked_in_loader_order():
frames, fps, start, span = prep.prepare(_clip(), 24, {"skip_first": 4, "cut_last": 4, "every_nth": 2, "frame_cap": 10})
assert _ids(frames) == [4, 6, 8, 10, 12, 14, 16, 18, 20, 22]
assert fps == 12 and start == pytest.approx(4 / 24) and span == pytest.approx(20 / 24)
def test_target_rate_picks_the_frame_showing_at_each_tick():
frames, fps, *_ = prep.prepare(_clip(48), 48, {"fps": 24})
assert fps == 24 and _ids(frames) == list(range(0, 48, 2))
def test_nothing_left_is_refused():
with pytest.raises(ValueError, match="nothing is left"):
prep.prepare(_clip(10), 24, {"skip_first": 6, "cut_last": 4})
def test_crop_fit_fills_the_target_and_loses_the_overflow():
out = prep.shape(_clip(2, 40, 80), 32, 32, "crop")
assert out.shape == (2, 32, 32, 3)
def test_pad_fit_keeps_everything_with_black_bars():
out = prep.shape(_clip(1, 40, 80) + 0.5, 64, 64, "pad")
assert out.shape == (1, 64, 64, 3)
assert float(out[0, 0, 0, 0]) == 0.0 and float(out[0, 32, 32, 0]) > 0.4
def test_stretch_and_one_sided_target_and_multiple():
assert prep.shape(_clip(1, 40, 80), 64, 64, "stretch").shape[1:3] == (64, 64)
assert prep.shape(_clip(1, 40, 80), 64, 0).shape[1:3] == (32, 64) # height follows the shape
assert prep.shape(_clip(1, 50, 90), 0, 0, multiple_of=32).shape[1:3] == (32, 64)
def test_margin_crop_and_a_crop_that_removes_everything():
assert prep.shape(_clip(1, 40, 80), crop_left=25, crop_right=25).shape[1:3] == (40, 40)
with pytest.raises(ValueError, match="no picture"):
prep.shape(_clip(1, 4, 4), crop_left=50, crop_right=50)
def test_sound_follows_the_picked_stretch():
wave = torch.arange(48000, dtype=torch.float32).reshape(1, 1, -1)
cut = prep.cut_audio({"waveform": wave, "sample_rate": 48000}, 0.25, 0.5)
assert cut["waveform"].shape[-1] == 24000 and float(cut["waveform"][0, 0, 0]) == 12000
# --- the node, on a real encoded file --------------------------------------------------------
@pytest.fixture
def stored(comfyui, tmp_path, monkeypatch):
from core import config, media
monkeypatch.setattr(config, "MEDIA_DIR", tmp_path / "media")
import av
path = tmp_path / "clip.mp4"
with av.open(str(path), "w") as out:
stream = out.add_stream("libx264", rate=24)
stream.width, stream.height, stream.pix_fmt = 64, 32, "yuv420p"
for i in range(48):
frame = av.VideoFrame.from_ndarray(torch.full((32, 64, 3), i * 5, dtype=torch.uint8).numpy(), format="rgb24")
for packet in stream.encode(frame):
out.mux(packet)
for packet in stream.encode():
out.mux(packet)
return media.save_upload("clip.mp4", path.read_bytes())
def test_the_node_prepares_a_real_video(stored):
from modules.media.load.nodes import FunUILoadMedia
options = {"skip_first": 8, "cut_last": 8, "every_nth": 2, "width": 32, "height": 32, "fit": "crop", "multiple_of": 16}
first, frames, _audio = FunUILoadMedia.execute(stored["id"], full=True, prepare=json.dumps(options)).result
assert frames.shape == (16, 32, 32, 3) and first.shape[0] == 1
def test_no_options_keeps_the_old_behaviour(stored):
from modules.media.load.nodes import FunUILoadMedia
_first, frames, _audio = FunUILoadMedia.execute(stored["id"], full=True).result
assert frames.shape[0] == 48 and frames.shape[1:3] == (32, 64)
def test_a_cut_that_leaves_nothing_is_said_in_plain_words(stored):
from modules.media.load.nodes import FunUILoadMedia
with pytest.raises(RuntimeError, match="cannot be prepared"):
FunUILoadMedia.execute(stored["id"], full=True, prepare=json.dumps({"skip_first": 40, "cut_last": 20})).result
with pytest.raises(RuntimeError, match="cannot be prepared"):
FunUILoadMedia.execute(stored["id"], full=True, prepare="{not json").result