diff --git a/pyclip/__init__.py b/pyclip/__init__.py index e69de29..073fed9 100644 --- a/pyclip/__init__.py +++ b/pyclip/__init__.py @@ -0,0 +1,2 @@ + +from .core.video import Video \ No newline at end of file diff --git a/pyclip/core/video.py b/pyclip/core/video.py index df6cab8..0597972 100644 --- a/pyclip/core/video.py +++ b/pyclip/core/video.py @@ -1,7 +1,17 @@ +from __future__ import annotations + +from pathlib import Path + +class Model: + pass class Video(Model): - def trim(self): + def trim(self, start: float, end: float | None = None, unit: str = "ms") -> Video: + """ + Returns a clip playing the content of the current clip + between times `start` and `end` + """ ... def mute(self, channels: int | list[int] | None = None): @@ -11,4 +21,13 @@ def resize(self, **kwargs): ... def upscale(self, **kwargs): + ... + + def rand(self): + ... + + def open(self, path: str | Path): + ... + + def save(self, path: str | Path): ... \ No newline at end of file diff --git a/tests/test_conversion.py b/tests/test_conversion.py new file mode 100644 index 0000000..a94a8f6 --- /dev/null +++ b/tests/test_conversion.py @@ -0,0 +1,17 @@ +"""Conversion operation tests + +References: + - https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.save + +""" + +from pyclip import Video +import tempfile + +import pytest + +@pytest.mark.parametrize("file_format", [("mp4"), ("avi")]) +def test_video_conversion(mock_video: Video, file_format: str): + with tempfile.NamedTemporaryFile(suffix=f".{file_format}") as f: + mock_video.save(f.name, format=file_format.upper()) + assert Video.open(f.name) == mock_video \ No newline at end of file diff --git a/tests/test_flip.py b/tests/test_flip.py new file mode 100644 index 0000000..c43b47e --- /dev/null +++ b/tests/test_flip.py @@ -0,0 +1,84 @@ +"""Tests for the rotate and flip methods of the Video class. + +References: + - https://kornia.readthedocs.io/en/latest/geometry.transform.html#kornia.geometry.transform.hflip + - https://kornia.readthedocs.io/en/latest/geometry.transform.html#kornia.geometry.transform.vflip + +""" + +import pyclip +import pytest + +def test_flip_horizontal(mock_video: pyclip.Video): + """Test flipping a video horizontally.""" + flipped_once = mock_video.fliph() + assert flipped_once != mock_video + + flipped_twice = flipped_once.fliph() + assert flipped_twice == mock_video + +def test_flip_vertical(mock_video: pyclip.Video): + """Test flipping a video vertically.""" + flipped_once = mock_video.flipv() + assert flipped_once != mock_video + + flipped_twice = flipped_once.flipv() + assert flipped_twice == mock_video + +def test_flip_both(mock_video: pyclip.Video): + """Test flipping a video both horizontally and vertically.""" + assert mock_video.flip(horizontal=True, vertical=True) == mock_video.flipv().fliph() == mock_video.fliph().flipv() + +def test_flip_without_args(mock_video: pyclip.Video): + """Test flipping without any arguments should raise a ValueError.""" + with pytest.raises(ValueError): + mock_video.flip() + +def test_flip_audio_sync(mock_video: pyclip.Video): + """Test audio synchronization after flipping.""" + flipped = mock_video.fliph() + assert flipped.audio == mock_video.audio + +def test_flip_horizontal_and_vertical(mock_video: pyclip.Video): + """Test flipping a video horizontally and then vertically should equal flipping both at once.""" + flipped_sequentially = mock_video.fliph().flipv() + flipped_simultaneously = mock_video.flip(horizontal=True, vertical=True) + assert flipped_sequentially == flipped_simultaneously + +def test_flip_vertical_audio_sync(mock_video: pyclip.Video): + """Test audio synchronization after vertical flipping.""" + flipped = mock_video.flipv() + assert flipped.audio == mock_video.audio + +def test_flip_invalid_arguments(mock_video: pyclip.Video): + """Test flipping with an invalid argument type should raise a TypeError.""" + with pytest.raises(TypeError): + mock_video.flip(horizontal="True", vertical=True) + +def test_fliph_method_vs_flip_function(mock_video: pyclip.Video): + """Test equivalence of fliph method vs flip function with horizontal=True.""" + assert mock_video.fliph() == mock_video.flip(horizontal=True) + +def test_flipv_method_vs_flip_function(mock_video: pyclip.Video): + """Test equivalence of flipv method vs flip function with vertical=True.""" + assert mock_video.flipv() == mock_video.flip(vertical=True) + +def test_flip_with_both_false(mock_video: pyclip.Video): + """Test flipping with both horizontal and vertical set to False should return the original.""" + assert mock_video.flip(horizontal=False, vertical=False) == mock_video + +def test_flip_idempotence(mock_video: pyclip.Video): + """Test that flipping twice in any direction should return the original video.""" + assert mock_video.flip(horizontal=True, vertical=True).flip(horizontal=True, vertical=True) == mock_video.fliph().fliph() == mock_video.flipv().flipv() == mock_video + +def test_flip_chain_operations(mock_video: pyclip.Video): + """Test chaining multiple flip operations.""" + result = mock_video.fliph().flipv().fliph() + assert result == mock_video.flipv() + +def test_flip_no_audio(mock_video: pyclip.Video): + """Test flipping a video with no audio.""" + # Assuming the Video class has a method to remove audio + video_no_audio = mock_video.remove_audio() + flipped = video_no_audio.fliph() + assert flipped.audio is None diff --git a/tests/test_rotate.py b/tests/test_rotate.py new file mode 100644 index 0000000..a3fa488 --- /dev/null +++ b/tests/test_rotate.py @@ -0,0 +1,72 @@ +"""Tests for the rotate and flip methods of the Video class. + +References: + - https://kornia.readthedocs.io/en/latest/geometry.transform.html#kornia.geometry.transform.rotate + +""" + +import pyclip +import pytest + +def test_rotate_90_degrees(mock_video: pyclip.Video): + """Test rotating a video 90 degrees counter-clockwise.""" + rotated = mock_video.rotate(90) + assert rotated.width == mock_video.width + assert rotated.height == mock_video.height + +def test_rotate_with_center(mock_video: pyclip.Video): + """Test rotating a video about a specific center.""" + center = [mock_video.width // 2, mock_video.height // 2] + assert mock_video.rotate(45, center=center) == mock_video.rotate(45) + +def test_rotate_180_degrees(mock_video: pyclip.Video): + """Test rotating a video 180 degrees counter-clockwise and checking its properties.""" + assert mock_video.rotate(180) == mock_video.flipv() + +def test_rotate_360_degrees(mock_video: pyclip.Video): + """Test rotating a video 360 degrees should be equal to the original.""" + assert mock_video.rotate(360) == mock_video + +def test_rotate_with_interpolation(mock_video: pyclip.Video): + """Test rotating a video with a specific interpolation.""" + rotated_bilinear = mock_video.rotate(45, mode='bilinear') + rotated_nearest = mock_video.rotate(45, mode='nearest') + + assert rotated_bilinear != rotated_nearest + assert rotated_bilinear.is_close(rotated_nearest) + +def test_rotate_with_padding_mode(mock_video: pyclip.Video): + """Test rotating a video with different padding modes.""" + rotated_zeros = mock_video.rotate(45, padding_mode='zeros') + rotated_border = mock_video.rotate(45, padding_mode='border') + rotated_reflection = mock_video.rotate(45, padding_mode='reflection') + + # Assuming Video class has a method to compare video content, e.g., is_close() + assert not rotated_zeros.is_close(rotated_border) + assert not rotated_zeros.is_close(rotated_reflection) + assert not rotated_border.is_close(rotated_reflection) + +def test_rotate_audio_sync(mock_video: pyclip.Video): + """Test audio synchronization after rotation.""" + rotated = mock_video.rotate(45) + assert rotated.audio == mock_video.audio + +def test_rotate_invalid_angle(mock_video: pyclip.Video): + """Test rotating with an invalid angle type should raise a TypeError.""" + with pytest.raises(TypeError): + mock_video.rotate("invalid_angle") + +def test_rotate_invalid_interpolation(mock_video: pyclip.Video): + """Test rotating with an invalid interpolation mode should raise a ValueError.""" + with pytest.raises(ValueError): + mock_video.rotate(45, mode='invalid_mode') + +def test_rotate_invalid_padding_mode(mock_video: pyclip.Video): + """Test rotating with an invalid padding mode should raise a ValueError.""" + with pytest.raises(ValueError): + mock_video.rotate(45, padding_mode='invalid_padding') + +def test_rotate_invalid_center(mock_video: pyclip.Video): + """Test rotating with an invalid center type should raise a TypeError.""" + with pytest.raises(TypeError): + mock_video.rotate(45, center="invalid_center") diff --git a/tests/test_trim.py b/tests/test_trim.py index c6eba2b..d404b96 100644 --- a/tests/test_trim.py +++ b/tests/test_trim.py @@ -1,22 +1,59 @@ +""" +Tests for the trim method of the Video class. + +References: + - https://zulko.github.io/moviepy/ref/Clip.html?highlight=subclip#moviepy.Clip.Clip.subclip + +""" + import pyclip +from pyclip import Trim import pytest -def test_sanity(): - assert 1 == 1 - @pytest.fixture -def mock_video(): - return pyclip.RandomVideo() +def mock_video() -> pyclip.Video: + """Fixture to generate a mock video for testing.""" + return pyclip.Video.rand() -def test_trim(mock_video): +def test_trim(mock_video: pyclip.Video): + """Test trimming a video using seconds as unit.""" video = mock_video.trim(50, 1000) assert video.duration == 950 -def test_trim_by_frames(mock_video): +def test_trim_by_frames(mock_video: pyclip.Video): + """Test trimming a video using frames as unit.""" assert mock_video.trim(50, 100, unit="f") == mock_video[50:100] -def test_trim_negative(mock_video): +def test_trim_negative(mock_video: pyclip.Video): + """Test trimming with negative start frame raises a ValueError.""" with pytest.raises(ValueError): mock_video.trim(-100, 1000, unit="f") +def test_trim_class_operation(mock_video: pyclip.Video): + """Test using Trim class operation to trim a video.""" + transforms = [ + Trim(50, 100, unit="f") + ] + + assert mock_video.apply(transforms) == mock_video.trim(50, 100, unit="f") + +def test_trim_start_greater_than_end(mock_video: pyclip.Video): + """Test trimming with start time greater than end time raises a ValueError.""" + with pytest.raises(ValueError): + mock_video.trim(1000, 100) + +def test_trim_exceed_duration(mock_video: pyclip.Video): + """Test trimming with time range exceeding video duration raises an IndexError.""" + with pytest.raises(IndexError): + mock_video.trim(0, mock_video.duration + 1000) + +def test_trim_invalid_unit(mock_video: pyclip.Video): + """Test trimming with invalid unit raises a ValueError.""" + with pytest.raises(ValueError): + mock_video.trim(0, 100, unit="z") + +def test_trim_without_end(mock_video: pyclip.Video): + """Test trimming without specifying end trims to the end of the video.""" + trimmed = mock_video.trim(50) + assert trimmed.duration == mock_video.duration - 50