From a31a564d2ef0a5da530eaa921363d869220feb64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Gait=C3=A1n?= Date: Sat, 4 Apr 2020 16:29:13 -0300 Subject: [PATCH 01/12] apply patch --- docs/getting_started/quick_presentation.rst | 7 +- moviepy/Clip.py | 122 ++++++++++++++++++-- moviepy/audio/AudioClip.py | 5 + moviepy/audio/io/AudioFileClip.py | 3 - moviepy/video/VideoClip.py | 15 ++- moviepy/video/compositing/concatenate.py | 1 - moviepy/video/fx/freeze.py | 5 +- moviepy/video/fx/speedx.py | 11 +- moviepy/video/fx/time_mirror.py | 6 +- moviepy/video/fx/time_symmetrize.py | 8 +- tests/test_VideoClip.py | 32 +++-- tests/test_issues.py | 2 +- 12 files changed, 164 insertions(+), 53 deletions(-) diff --git a/docs/getting_started/quick_presentation.rst b/docs/getting_started/quick_presentation.rst index 54884da45..754b52a7f 100644 --- a/docs/getting_started/quick_presentation.rst +++ b/docs/getting_started/quick_presentation.rst @@ -42,16 +42,17 @@ In a typical MoviePy script, you load video or audio files, modify them, put the from moviepy.editor import * # Load myHolidays.mp4 and select the subclip 00:00:50 - 00:00:60 - clip = VideoFileClip("myHolidays.mp4").subclip(50,60) + clip = VideoFileClip("myHolidays.mp4") + clip = clip.subclip(50, 60) # or just clip[50:60] # Reduce the audio volume (volume x 0.8) clip = clip.volumex(0.8) # Generate a text clip. You can customize the font, color, etc. - txt_clip = TextClip("My Holidays 2013",fontsize=70,color='white') + txt_clip = TextClip("My Holidays 2013", fontsize=70, color="white") # Say that you want it to appear 10s at the center of the screen - txt_clip = txt_clip.set_pos('center').set_duration(10) + txt_clip = txt_clip.set_pos("center").set_duration(10) # Overlay the text clip on the first video clip video = CompositeVideoClip([clip, txt_clip]) diff --git a/moviepy/Clip.py b/moviepy/Clip.py index fb3ea2cb7..0959f1f3b 100644 --- a/moviepy/Clip.py +++ b/moviepy/Clip.py @@ -5,6 +5,9 @@ """ from copy import copy +from functools import reduce +from operator import add +from numbers import Real import numpy as np import proglog @@ -20,7 +23,6 @@ class Clip: - """ Base class of all clips (VideoClips and AudioClips). @@ -69,8 +71,10 @@ def copy(self): there is an outplace transformation of the clip (clip.resize, clip.subclip, etc.) """ + return copy(self) - newclip = copy(self) + def __copy__(self): + newclip = copy(super(Clip, self)) if hasattr(self, "audio"): newclip.audio = copy(self.audio) if hasattr(self, "mask"): @@ -179,10 +183,10 @@ def fl_time(self, t_func, apply_to=None, keep_duration=False): -------- >>> # plays the clip (and its mask and sound) twice faster - >>> newclip = clip.fl_time(lambda: 2*t, apply_to=['mask', 'audio']) + >>> newclip = clip.fl_time(lambda t: 2*t, apply_to=['mask', 'audio']) >>> >>> # plays the clip starting at t=3, and backwards: - >>> newclip = clip.fl_time(lambda: 3-t) + >>> newclip = clip.fl_time(lambda t: 3-t) """ if apply_to is None: @@ -213,7 +217,6 @@ def fx(self, func, *args, **kwargs): >>> resize( volumex( mirrorx( clip ), 0.5), 0.3) """ - return func(self, *args, **kwargs) @apply_to_mask @@ -226,7 +229,6 @@ def set_start(self, t, change_end=True): to ``t``, which can be expressed in seconds (15.35), in (min, sec), in (hour, min, sec), or as a string: '01:03:05.35'. - If ``change_end=True`` and the clip has a ``duration`` attribute, the ``end`` atrribute of the clip will be updated to ``start+duration``. @@ -317,13 +319,13 @@ def set_memoize(self, memoize): @convert_to_seconds(["t"]) def is_playing(self, t): """ - If t is a time, returns true if t is between the start and the end of the clip. t can be expressed in seconds (15.35), - in (min, sec), in (hour, min, sec), or as a string: '01:03:05.35'. + in (min, sec), in (hour, min, sec), or as a string: "01:03:05.35". + If t is a numpy array, returns False if none of the t is in - theclip, else returns a vector [b_1, b_2, b_3...] where b_i - is true iff tti is in the clip. + the clip, else returns a vector [b_1, b_2, b_3...] where b_i + is true if tti is in the clip. """ if isinstance(t, np.ndarray): @@ -355,8 +357,13 @@ def subclip(self, t_start=0, t_end=None): between times ``t_start`` and ``t_end``, which can be expressed in seconds (15.35), in (min, sec), in (hour, min, sec), or as a string: '01:03:05.35'. + + It's equivalent to slice the clip as a sequence, like + ``clip[t_start:t_end]`` + If ``t_end`` is not provided, it is assumed to be the duration of the clip (potentially infinite). + If ``t_end`` is a negative value, it is reset to ``clip.duration + t_end. ``. For instance: :: @@ -490,10 +497,103 @@ def close(self): # * Therefore, should NOT be called by __del__(). pass - # Support the Context Manager protocol, to ensure that resources are cleaned up. + # helper private methods + def __unsupported(self, other, operator): + self_type = type(self).__name__ + other_type = type(other).__name__ + message = "unsupported operand type(s) for {}: '{}' and '{}'" + raise TypeError(message.format(operator, self_type, other_type)) + + @staticmethod + def __apply_to(clip): + apply_to = [] + if getattr(clip, "mask", None): + apply_to.append("mask") + if getattr(clip, "audio", None): + apply_to.append("audio") + return apply_to def __enter__(self): + """ + Support the Context Manager protocol, + to ensure that resources are cleaned up. + """ return self def __exit__(self, exc_type, exc_value, traceback): self.close() + + def __getitem__(self, key): + """ + Support extended slice and index operations over + a clip object. + + Simple slicing is implemented via :meth:`subclip`. + So, ``clip[t_start:t_end]`` is equivalent to + ``clip.subclip(t_start, t_end)``. If ``t_start`` is not + given, default to ``0``, if ``t_end`` is not given, + default to ``self.duration``. + + The slice object optionally support a third argument as + a ``speed`` coefficient (that could be negative), + ``clip[t_start:t_end:speed]``. + + For example ``clip[::-1]`` returns a reversed (a time_mirror fx) + the video and ``clip[:5:2]`` returns the segment from 0 to 5s + accelerated to 2x (ie. resulted duration would be 2.5s) + + In addition, a tuple of slices is supported, resulting in the concatenation + of each segment. For example ``clip[(:1, 2:)]`` return a clip + with the segment from 1 to 2s removed. + + If ``key`` is not a slice or tuple, we assume it's a time + value (expressed in any format supported by :func:`cvsec`) + and return the frame at that time, passing the key + to :meth:`get_frame`. + """ + if isinstance(key, slice): + # support for [start:end:speed] slicing. If speed is negative + # a time mirror is applied. + clip = self.subclip(key.start or 0, key.stop or self.duration) + + if key.step: + # change speed of the subclip + apply_to = self.__apply_to(clip) + factor = abs(key.step) + if factor != 1: + # change speed + clip = clip.fl_time( + lambda t: factor * t, apply_to=apply_to, keep_duration=True + ) + clip = clip.set_duration(1.0 * clip.duration / factor) + if key.step < 0: + # time mirror + clip = clip.fl_time( + lambda t: clip.duration - t, + keep_duration=True, + apply_to=apply_to, + ) + return clip + elif isinstance(key, tuple): + # get a concatenation of subclips + return reduce(add, (self[k] for k in key)) + else: + return self.get_frame(key) + + def __del__(self): + self.close() + + def __add__(self, other): + # concatenate. implemented in specialized classes + self.__unsupported(other, "+") + + def __mul__(self, n): + # loop n times + if not isinstance(n, Real): + self.__unsupported(n, "*") + + apply_to = self.__apply_to(self) + clip = self.fl_time( + lambda t: t % self.duration, apply_to=apply_to, keep_duration=True + ) + return clip.set_duration(clip.duration * n) diff --git a/moviepy/audio/AudioClip.py b/moviepy/audio/AudioClip.py index e722d4dd8..a1eaab603 100644 --- a/moviepy/audio/AudioClip.py +++ b/moviepy/audio/AudioClip.py @@ -245,6 +245,11 @@ def write_audiofile( logger=logger, ) + def __add__(self, other): + if isinstance(other, AudioClip): + return concatenate_audioclips([self, other]) + return super(AudioClip, self).__add__(other) + # The to_audiofile method is replaced by the more explicit write_audiofile. AudioClip.to_audiofile = deprecated_version_of( diff --git a/moviepy/audio/io/AudioFileClip.py b/moviepy/audio/io/AudioFileClip.py index 41919c991..6609f5a32 100644 --- a/moviepy/audio/io/AudioFileClip.py +++ b/moviepy/audio/io/AudioFileClip.py @@ -89,6 +89,3 @@ def close(self): if self.reader: self.reader.close_proc() self.reader = None - - def __del__(self): - self.close() diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index 6021d791a..8e2e05ce4 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -835,6 +835,17 @@ def afx(self, fun, *a, **k): """ self.audio = self.audio.fx(fun, *a, **k) + def __add__(self, other): + if isinstance(other, VideoClip): + from moviepy.video.compositing.concatenate import concatenate_videoclips + + method = "chain" if self.size == other.size else "compose" + return concatenate_videoclips([self, other], method=method) + return super(VideoClip, self).__add__(other) + + def __and__(self, mask): + return self.set_mask(mask) + class DataVideoClip(VideoClip): """ @@ -911,9 +922,7 @@ def make_frame(t): world.update() return world.to_frame() - VideoClip.__init__( - self, make_frame=make_frame, ismask=ismask, duration=duration - ) + super().__init__(make_frame=make_frame, ismask=ismask, duration=duration) """--------------------------------------------------------------------- diff --git a/moviepy/video/compositing/concatenate.py b/moviepy/video/compositing/concatenate.py index 0bc99b88e..7d6f206f6 100644 --- a/moviepy/video/compositing/concatenate.py +++ b/moviepy/video/compositing/concatenate.py @@ -4,7 +4,6 @@ from moviepy.audio.AudioClip import CompositeAudioClip from moviepy.tools import deprecated_version_of from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip -from moviepy.video.compositing.on_color import on_color from moviepy.video.VideoClip import ColorClip, VideoClip diff --git a/moviepy/video/fx/freeze.py b/moviepy/video/fx/freeze.py index 7d831d77d..494d6c0f2 100644 --- a/moviepy/video/fx/freeze.py +++ b/moviepy/video/fx/freeze.py @@ -1,6 +1,5 @@ from moviepy.decorators import requires_duration from moviepy.video.compositing.concatenate import concatenate_videoclips -from moviepy.video.VideoClip import ImageClip @requires_duration @@ -21,7 +20,7 @@ def freeze(clip, t=0, freeze_duration=None, total_duration=None, padding_end=0): if freeze_duration is None: freeze_duration = total_duration - clip.duration - before = [clip.subclip(0, t)] if (t != 0) else [] + before = [clip[:t]] if (t != 0) else [] freeze = [clip.to_ImageClip(t).set_duration(freeze_duration)] - after = [clip.subclip(t)] if (t != clip.duration) else [] + after = [clip[t:]] if (t != clip.duration) else [] return concatenate_videoclips(before + freeze + after) diff --git a/moviepy/video/fx/speedx.py b/moviepy/video/fx/speedx.py index ce4c76955..d1d5abd90 100644 --- a/moviepy/video/fx/speedx.py +++ b/moviepy/video/fx/speedx.py @@ -1,6 +1,3 @@ -from moviepy.decorators import apply_to_audio, apply_to_mask - - def speedx(clip, factor=None, final_duration=None): """ Returns a clip playing the current clip but at a speed multiplied @@ -12,10 +9,4 @@ def speedx(clip, factor=None, final_duration=None): if final_duration: factor = 1.0 * clip.duration / final_duration - - newclip = clip.fl_time(lambda t: factor * t, apply_to=["mask", "audio"]) - - if clip.duration is not None: - newclip = newclip.set_duration(1.0 * clip.duration / factor) - - return newclip + return clip[::factor] diff --git a/moviepy/video/fx/time_mirror.py b/moviepy/video/fx/time_mirror.py index 1c044e3e3..59f90838f 100644 --- a/moviepy/video/fx/time_mirror.py +++ b/moviepy/video/fx/time_mirror.py @@ -1,13 +1,11 @@ -from moviepy.decorators import apply_to_audio, apply_to_mask, requires_duration +from moviepy.decorators import requires_duration @requires_duration -@apply_to_mask -@apply_to_audio def time_mirror(self): """ Returns a clip that plays the current clip backwards. The clip must have its ``duration`` attribute set. The same effect is applied to the clip's audio and mask if any. """ - return self.fl_time(lambda t: self.duration - t, keep_duration=True) + return self[::-1] diff --git a/moviepy/video/fx/time_symmetrize.py b/moviepy/video/fx/time_symmetrize.py index e4ebb8b58..65cfdf511 100644 --- a/moviepy/video/fx/time_symmetrize.py +++ b/moviepy/video/fx/time_symmetrize.py @@ -1,11 +1,7 @@ -from moviepy.decorators import apply_to_audio, apply_to_mask, requires_duration -from moviepy.video.compositing.concatenate import concatenate_videoclips - -from .time_mirror import time_mirror +from moviepy.decorators import requires_duration @requires_duration -@apply_to_mask def time_symmetrize(clip): """ Returns a clip that plays the current clip once forwards and @@ -14,4 +10,4 @@ def time_symmetrize(clip): This effect is automatically applied to the clip's mask and audio if they exist. """ - return concatenate_videoclips([clip, clip.fx(time_mirror)]) + return clip + clip[::-1] diff --git a/tests/test_VideoClip.py b/tests/test_VideoClip.py index a9b56830c..39d4a866c 100644 --- a/tests/test_VideoClip.py +++ b/tests/test_VideoClip.py @@ -1,16 +1,11 @@ import os -import sys import pytest -from numpy import pi, sin +import numpy as np -from moviepy.audio.AudioClip import AudioClip -from moviepy.audio.io.AudioFileClip import AudioFileClip from moviepy.utils import close_all_clips from moviepy.video.fx.speedx import speedx -from moviepy.video.io.VideoFileClip import VideoFileClip -from moviepy.video.VideoClip import ColorClip, VideoClip - +from moviepy.editor import VideoFileClip, ImageClip, ColorClip, AudioClip, AudioFileClip from .test_helper import TMP_DIR @@ -117,7 +112,7 @@ def test_oncolor(): def test_setaudio(): clip = ColorClip(size=(100, 60), color=(255, 0, 0), duration=0.5) - make_frame_440 = lambda t: [sin(440 * 2 * pi * t)] + make_frame_440 = lambda t: [np.sin(440 * 2 * np.pi * t)] audio = AudioClip(make_frame_440, duration=0.5) audio.fps = 44100 clip = clip.set_audio(audio) @@ -163,5 +158,26 @@ def test_withoutaudio(): close_all_clips(locals()) +def test_add(): + clip = VideoFileClip("media/fire2.mp4") + new_clip = clip[0:1] + clip[2:3.2] + assert new_clip.duration == 2.2 + assert np.array_equal(new_clip[1.1], clip[2.1]) + + +def test_mul(): + clip = VideoFileClip("media/fire2.mp4") + new_clip = clip[0:1] * 2.5 + assert new_clip.duration == 2.5 + assert np.array_equal(new_clip[1.1], clip[0.1]) + + +def test_and(): + clip = VideoFileClip("media/fire2.mp4") + maskclip = ImageClip("media/afterimage.png", ismask=True, transparent=True) + clip_with_mask = clip & maskclip + assert clip_with_mask.mask is maskclip + + if __name__ == "__main__": pytest.main() diff --git a/tests/test_issues.py b/tests/test_issues.py index 9b048da41..1237b31d2 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -225,7 +225,7 @@ def size(t): avatar.audio = None maskclip = ImageClip("media/afterimage.png", ismask=True, transparent=True) avatar.set_mask(maskclip) # must set maskclip here.. - concatenated = concatenate_videoclips([avatar] * 3) + concatenated = avatar * 3 tt = VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0, 3) # TODO: Setting mask here does not work: From 36bdf1e65f711aca2b07da9f53f6d1b5f0d55876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Gait=C3=A1n?= Date: Thu, 30 Mar 2023 09:39:23 -0300 Subject: [PATCH 02/12] lint --- moviepy/Clip.py | 47 +++++++++++++++------------------ moviepy/video/fx/time_mirror.py | 2 +- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/moviepy/Clip.py b/moviepy/Clip.py index 9b95d8821..d842ccb80 100644 --- a/moviepy/Clip.py +++ b/moviepy/Clip.py @@ -55,7 +55,6 @@ def copy(self): """Allows the usage of ``.copy()`` in clips as chained methods invocation.""" return _copy.copy(self) - @convert_parameter_to_seconds(["t"]) def get_frame(self, t): """Gets a numpy array representing the RGB picture of the clip, @@ -389,8 +388,8 @@ def subclip(self, start_time=0, end_time=None): The ``mask`` and ``audio`` of the resulting subclip will be subclips of ``mask`` and ``audio`` the original clip, if they exist. - It's equivalent to slice the clip as a sequence, like - ``clip[t_start:t_end]``. + It's equivalent to slice the clip as a sequence, like + ``clip[t_start:t_end]``. Parameters ---------- @@ -411,7 +410,6 @@ def subclip(self, start_time=0, end_time=None): If ``end_time`` is provided or if the clip has a duration attribute, the duration of the returned clip is set automatically. """ - if start_time < 0: # Make this more Python-like, a negative value means to move # backward from the end of the clip @@ -550,7 +548,6 @@ def close(self): # * Therefore, should NOT be called by __del__(). pass - def __eq__(self, other): if not isinstance(other, Clip): return NotImplemented @@ -570,7 +567,7 @@ def __eq__(self, other): def __enter__(self): """ - Support the Context Manager protocol, + Support the Context Manager protocol, to ensure that resources are cleaned up. """ return self @@ -580,31 +577,31 @@ def __exit__(self, exc_type, exc_value, traceback): def __getitem__(self, key): """ - Support extended slice and index operations over - a clip object. - - Simple slicing is implemented via :meth:`subclip`. - So, ``clip[t_start:t_end]`` is equivalent to - ``clip.subclip(t_start, t_end)``. If ``t_start`` is not - given, default to ``0``, if ``t_end`` is not given, - default to ``self.duration``. - - The slice object optionally support a third argument as - a ``speed`` coefficient (that could be negative), - ``clip[t_start:t_end:speed]``. + Support extended slice and index operations over + a clip object. + + Simple slicing is implemented via `subclip`. + So, ``clip[t_start:t_end]`` is equivalent to + ``clip.subclip(t_start, t_end)``. If ``t_start`` is not + given, default to ``0``, if ``t_end`` is not given, + default to ``self.duration``. + + The slice object optionally support a third argument as + a ``speed`` coefficient (that could be negative), + ``clip[t_start:t_end:speed]``. For example ``clip[::-1]`` returns a reversed (a time_mirror fx) the video and ``clip[:5:2]`` returns the segment from 0 to 5s - accelerated to 2x (ie. resulted duration would be 2.5s) - + accelerated to 2x (ie. resulted duration would be 2.5s) + In addition, a tuple of slices is supported, resulting in the concatenation of each segment. For example ``clip[(:1, 2:)]`` return a clip - with the segment from 1 to 2s removed. + with the segment from 1 to 2s removed. - If ``key`` is not a slice or tuple, we assume it's a time - value (expressed in any format supported by :func:`cvsec`) - and return the frame at that time, passing the key - to :meth:`get_frame`. + If ``key`` is not a slice or tuple, we assume it's a time + value (expressed in any format supported by `cvsec`) + and return the frame at that time, passing the key + to ``get_frame``. """ apply_to = ["mask", "audio"] if isinstance(key, slice): diff --git a/moviepy/video/fx/time_mirror.py b/moviepy/video/fx/time_mirror.py index 42799ba84..93e1d521f 100644 --- a/moviepy/video/fx/time_mirror.py +++ b/moviepy/video/fx/time_mirror.py @@ -1,4 +1,4 @@ -from moviepy.decorators import requires_duration, apply_to_mask, apply_to_audio +from moviepy.decorators import apply_to_audio, apply_to_mask, requires_duration @requires_duration From 3ef62194a9bb64b6b85229636a0b244374e8c869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Gait=C3=A1n?= Date: Sat, 1 Apr 2023 15:59:37 -0300 Subject: [PATCH 03/12] fix time mirror --- moviepy/Clip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moviepy/Clip.py b/moviepy/Clip.py index d842ccb80..bf8bf4df4 100644 --- a/moviepy/Clip.py +++ b/moviepy/Clip.py @@ -621,7 +621,7 @@ def __getitem__(self, key): if key.step < 0: # time mirror clip = clip.time_transform( - lambda t: clip.duration - t, + lambda t: clip.duration - t - 1, keep_duration=True, apply_to=apply_to, ) From 4b259f47288ca0bc345117a9894e056d50b926cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Gait=C3=A1n?= Date: Sat, 1 Apr 2023 15:59:56 -0300 Subject: [PATCH 04/12] undo change --- tests/test_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index 3a5a5155f..552d29c4e 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -100,7 +100,7 @@ def to_file(*args, **kwargs): ("url", "expected_result"), ( ( - "http://localhost:8001/media/chaplin.mp4", + "http://localhost:8000/media/chaplin.mp4", os.path.join("media", "chaplin.mp4"), ), ("foobarbazimpossiblecode", OSError), From eb55938db5f7094e31206b0467176488db9be973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Gait=C3=A1n?= Date: Sat, 1 Apr 2023 17:31:00 -0300 Subject: [PATCH 05/12] merge #1943 --- .github/workflows/test_suite.yml | 18 +++--------------- setup.py | 13 +++++-------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 2f7c0a311..30f9d0f23 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -15,24 +15,12 @@ jobs: runs-on: macos-latest strategy: matrix: - python-version: [3.7] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 - uses: s-weigand/setup-conda@v1 with: activate-conda: true - # TODO: fix adding the different versions - # Pick the installation of Python Framework build from conda, because they do not support versioning - # https://anaconda.org/anaconda/python.app/files - #- name: Install conda python version 3.6 - # if: matrix.python-version == '3.6' - # run: conda install http://repo.continuum.io/pkgs/main/osx-64/python.app-2-py36_10.tar.bz2 - #- name: Install conda python version 3.7 - # if: matrix.python-version == '3.7' - # run: conda install http://repo.continuum.io/pkgs/main/osx-64/python.app-2-py37_10.tar.bz2 - #- name: Install conda python version 3.8 - # if: matrix.python-version == '3.8' - # run: conda install http://repo.continuum.io/pkgs/main/osx-64/python.app-2-py38_10.tar.bz2 - name: Install pythonw run: conda install python.app - name: Python Version Info @@ -65,7 +53,7 @@ jobs: runs-on: windows-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] fail-fast: false steps: - uses: actions/checkout@v3 @@ -130,7 +118,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] fail-fast: false steps: - uses: actions/checkout@v3 diff --git a/setup.py b/setup.py index ceaa96635..751ee8351 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,8 @@ #!/usr/bin/env python """MoviePy setup script.""" -import os import sys -from codecs import open +from pathlib import Path try: @@ -66,9 +65,7 @@ def run_tests(self): cmdclass["build_docs"] = BuildDoc -__version__ = None -with open(os.path.join("moviepy", "version.py"), "r", "utf-8") as f: - __version__ = f.read().split(" ")[2].strip("\n").strip('"') +__version__ = Path("moviepy/version.py").read_text().strip().split('"')[1][:-1] # Define the requirements for specific execution needs. @@ -122,8 +119,7 @@ def run_tests(self): } # Load the README. -with open("README.rst", "r", "utf-8") as file: - readme = file.read() +readme = Path("README.rst").read_text() setup( name="moviepy", @@ -143,10 +139,11 @@ def run_tests(self): "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Multimedia", "Topic :: Multimedia :: Sound/Audio", "Topic :: Multimedia :: Sound/Audio :: Analysis", From 215fe1c7c58e294c3699d18adb3422365c50a088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Gait=C3=A1n?= Date: Sat, 1 Apr 2023 17:36:25 -0300 Subject: [PATCH 06/12] undo change --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9673851f2..e06263375 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -59,7 +59,7 @@ def make_mono_frame(t): @contextlib.contextmanager -def get_static_files_server(port=8001): +def get_static_files_server(port=8000): my_server = socketserver.TCPServer(("", port), http.server.SimpleHTTPRequestHandler) thread = threading.Thread(target=my_server.serve_forever, daemon=True) thread.start() From 9d43dd6aa7b9f8fbd57844292d624f9883e5fa4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Gait=C3=A1n?= Date: Sun, 2 Apr 2023 13:00:15 -0300 Subject: [PATCH 07/12] new implementation for test_blit_with_opacity --- tests/test_compositing.py | 42 +++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/tests/test_compositing.py b/tests/test_compositing.py index 58240347f..052b54fc0 100644 --- a/tests/test_compositing.py +++ b/tests/test_compositing.py @@ -93,22 +93,44 @@ def test_concatenate_floating_point(util): concat.write_videofile(os.path.join(util.TMP_DIR, "concat.mp4"), preset="ultrafast") +# def test_blit_with_opacity(): +# # bitmap.mp4 has one second R, one second G, one second B +# clip1 = VideoFileClip("media/bitmap.mp4") +# # overlay same clip, shifted by 1 second, at half opacity +# clip2 = ( +# VideoFileClip("media/bitmap.mp4") +# .subclip(1, 2) +# .with_start(0) +# .with_end(2) +# .with_opacity(0.5) +# ) +# composite = CompositeVideoClip([clip1, clip2]) +# bt = ClipPixelTest(composite) + +# bt.expect_color_at(0.5, (0x7F, 0x7F, 0x00)) +# bt.expect_color_at(1.5, (0x00, 0x7F, 0x7F)) +# bt.expect_color_at(2.5, (0x00, 0x00, 0xFF)) + + def test_blit_with_opacity(): - # bitmap.mp4 has one second R, one second G, one second B - clip1 = VideoFileClip("media/bitmap.mp4") - # overlay same clip, shifted by 1 second, at half opacity - clip2 = ( - VideoFileClip("media/bitmap.mp4") - .subclip(1, 2) - .with_start(0) - .with_end(2) - .with_opacity(0.5) + # has one second R, one second G, one second B + size = (2, 2) + clip1 = ( + ColorClip(size, color=(255, 0, 0), duration=1) + + ColorClip(size, color=(0, 255, 0), duration=1) + + ColorClip(size, color=(0, 0, 255), duration=1) ) + + # overlay green at half opacity during first 2 sec + clip2 = ColorClip(size, color=(0, 255, 0), duration=2).with_opacity(0.5) composite = CompositeVideoClip([clip1, clip2]) bt = ClipPixelTest(composite) + # red + 50% green bt.expect_color_at(0.5, (0x7F, 0x7F, 0x00)) - bt.expect_color_at(1.5, (0x00, 0x7F, 0x7F)) + # green + 50% green + bt.expect_color_at(1.5, (0x00, 0xFF, 0x00)) + # blue is after 2s, so keep untouched bt.expect_color_at(2.5, (0x00, 0x00, 0xFF)) From 728dde7aca9dd91c073b604ee9c628a4ced3f87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Gait=C3=A1n?= Date: Sun, 2 Apr 2023 20:20:17 -0300 Subject: [PATCH 08/12] add more tests for slicing operations --- moviepy/Clip.py | 4 +++- tests/test_VideoClip.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/moviepy/Clip.py b/moviepy/Clip.py index bf8bf4df4..ea684946a 100644 --- a/moviepy/Clip.py +++ b/moviepy/Clip.py @@ -2,7 +2,9 @@ are common to the two subclasses of Clip, VideoClip and AudioClip. """ import copy as _copy +from functools import reduce from numbers import Real +from operator import add import numpy as np import proglog @@ -628,7 +630,7 @@ def __getitem__(self, key): return clip elif isinstance(key, tuple): # get a concatenation of subclips - return sum(self[k] for k in key) + return reduce(add, (self[k] for k in key)) else: return self.get_frame(key) diff --git a/tests/test_VideoClip.py b/tests/test_VideoClip.py index 137a1b438..b290b7efe 100644 --- a/tests/test_VideoClip.py +++ b/tests/test_VideoClip.py @@ -448,6 +448,29 @@ def test_add(): assert np.array_equal(new_clip[1.1], clip[2.1]) +def test_slice_tuples(): + clip = VideoFileClip("media/fire2.mp4") + new_clip = clip[0:1, 2:3.2] + assert new_clip.duration == 2.2 + assert np.array_equal(new_clip[1.1], clip[2.1]) + + +def test_slice_mirror(): + clip = VideoFileClip("media/fire2.mp4") + new_clip = clip[::-1] + assert new_clip.duration == clip.duration + assert np.array_equal(new_clip[0], clip[clip.duration]) + + +def test_slice_speed(): + clip = BitmapClip([["A"], ["B"], ["C"], ["D"]], fps=1) + clip1 = clip[::0.5] # 1/2x speed + target1 = BitmapClip( + [["A"], ["A"], ["B"], ["B"], ["C"], ["C"], ["D"], ["D"]], fps=1 + ) + assert clip1 == target1 + + def test_mul(): clip = VideoFileClip("media/fire2.mp4") new_clip = clip[0:1] * 2.5 From b8b6c07296c0aee163c91b0b85f905a29340291c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Gait=C3=A1n?= Date: Sun, 2 Apr 2023 20:22:32 -0300 Subject: [PATCH 09/12] implement __mul__ using loop function as recommended --- moviepy/Clip.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/moviepy/Clip.py b/moviepy/Clip.py index ea684946a..cf822518c 100644 --- a/moviepy/Clip.py +++ b/moviepy/Clip.py @@ -646,7 +646,5 @@ def __mul__(self, n): if not isinstance(n, Real): return NotImplemented - clip = self.time_transform( - lambda t: t % self.duration, apply_to=["mask", "audio"], keep_duration=True - ) - return clip.with_duration(clip.duration * n) + from moviepy.video.fx.loop import loop + return loop(self, n) From 18f4ebb70a2c868a7593771023a9f0fb8adab07f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Gait=C3=A1n?= Date: Sun, 2 Apr 2023 22:06:58 -0300 Subject: [PATCH 10/12] or and div --- moviepy/video/VideoClip.py | 19 +++++++++++++++++++ tests/test_VideoClip.py | 15 +++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index 975eb44a7..6b6c6053d 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -917,6 +917,25 @@ def __add__(self, other): return concatenate_videoclips([self, other], method=method) return super(VideoClip, self).__add__(other) + def __or__(self, other): + """ + self | other produces a video with self and other placed side by side + horizontally + """ + if isinstance(other, VideoClip): + from moviepy.video.compositing.CompositeVideoClip import clips_array + return clips_array([[self, other]]) + return super(VideoClip, self).__or__(other) + + def __truediv__(self, other): + """ + self / other produces a video with self placed on top of other + """ + if isinstance(other, VideoClip): + from moviepy.video.compositing.CompositeVideoClip import clips_array + return clips_array([[self], [other]]) + return super(VideoClip, self).__or__(other) + def __and__(self, mask): return self.with_mask(mask) diff --git a/tests/test_VideoClip.py b/tests/test_VideoClip.py index b290b7efe..80084c8a2 100644 --- a/tests/test_VideoClip.py +++ b/tests/test_VideoClip.py @@ -485,5 +485,20 @@ def test_and(): assert clip_with_mask.mask is maskclip +def test_or(util): + clip1 = BitmapClip([["R"]], fps=1) + clip2 = BitmapClip([["G"]], fps=1) + target = BitmapClip([["RG"]], fps=1) + result = clip1 | clip2 + assert result == target + + +def test_truediv(util): + clip1 = BitmapClip([["R"]], fps=1) + clip2 = BitmapClip([["G"]], fps=1) + target = BitmapClip([["R", "G"]], fps=1) + result = clip1 / clip2 + assert result == target + if __name__ == "__main__": pytest.main() From f4775b311f5fbb5236abc3573b94df4faadbb779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Gait=C3=A1n?= Date: Sun, 2 Apr 2023 23:00:29 -0300 Subject: [PATCH 11/12] clip @ angle is a rotation --- moviepy/video/VideoClip.py | 7 +++++++ tests/test_VideoClip.py | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index 6b6c6053d..cad726115 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -6,6 +6,7 @@ import copy as _copy import os +from numbers import Real import subprocess as sp import tempfile @@ -936,6 +937,12 @@ def __truediv__(self, other): return clips_array([[self], [other]]) return super(VideoClip, self).__or__(other) + def __matmul__(self, n): + if not isinstance(n, Real): + return NotImplemented + from moviepy.video.fx.rotate import rotate + return rotate(self, n) + def __and__(self, mask): return self.with_mask(mask) diff --git a/tests/test_VideoClip.py b/tests/test_VideoClip.py index 80084c8a2..787cfc51a 100644 --- a/tests/test_VideoClip.py +++ b/tests/test_VideoClip.py @@ -500,5 +500,13 @@ def test_truediv(util): result = clip1 / clip2 assert result == target + +def test_matmul(util): + clip1 = BitmapClip([["RG"]], fps=1) + target = BitmapClip([["R", "G"]], fps=1) + result = clip1 @ 270 + assert result == target + + if __name__ == "__main__": pytest.main() From 21cbf323ac8cd999407dbd27005d97052f4d267f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Gait=C3=A1n?= Date: Sun, 2 Apr 2023 23:07:11 -0300 Subject: [PATCH 12/12] linters happy --- moviepy/Clip.py | 1 + moviepy/video/VideoClip.py | 12 ++++++++---- tests/test_VideoClip.py | 2 +- tests/test_compositing.py | 1 - 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/moviepy/Clip.py b/moviepy/Clip.py index cf822518c..71c13f64f 100644 --- a/moviepy/Clip.py +++ b/moviepy/Clip.py @@ -647,4 +647,5 @@ def __mul__(self, n): return NotImplemented from moviepy.video.fx.loop import loop + return loop(self, n) diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index cad726115..caa26828e 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -6,9 +6,9 @@ import copy as _copy import os -from numbers import Real import subprocess as sp import tempfile +from numbers import Real import numpy as np import proglog @@ -920,20 +920,23 @@ def __add__(self, other): def __or__(self, other): """ - self | other produces a video with self and other placed side by side - horizontally + Implement the or (self | other) to produce a video with self and other + placed side by side horizontally. """ if isinstance(other, VideoClip): from moviepy.video.compositing.CompositeVideoClip import clips_array + return clips_array([[self, other]]) return super(VideoClip, self).__or__(other) def __truediv__(self, other): """ - self / other produces a video with self placed on top of other + Implement division (self / other) to produce a video with self + placed on top of other. """ if isinstance(other, VideoClip): from moviepy.video.compositing.CompositeVideoClip import clips_array + return clips_array([[self], [other]]) return super(VideoClip, self).__or__(other) @@ -941,6 +944,7 @@ def __matmul__(self, n): if not isinstance(n, Real): return NotImplemented from moviepy.video.fx.rotate import rotate + return rotate(self, n) def __and__(self, mask): diff --git a/tests/test_VideoClip.py b/tests/test_VideoClip.py index 787cfc51a..151049ba3 100644 --- a/tests/test_VideoClip.py +++ b/tests/test_VideoClip.py @@ -464,7 +464,7 @@ def test_slice_mirror(): def test_slice_speed(): clip = BitmapClip([["A"], ["B"], ["C"], ["D"]], fps=1) - clip1 = clip[::0.5] # 1/2x speed + clip1 = clip[::0.5] # 1/2x speed target1 = BitmapClip( [["A"], ["A"], ["B"], ["B"], ["C"], ["C"], ["D"], ["D"]], fps=1 ) diff --git a/tests/test_compositing.py b/tests/test_compositing.py index 052b54fc0..365b1ad4f 100644 --- a/tests/test_compositing.py +++ b/tests/test_compositing.py @@ -10,7 +10,6 @@ from moviepy.video.compositing.concatenate import concatenate_videoclips from moviepy.video.compositing.transitions import slide_in, slide_out from moviepy.video.fx.resize import resize -from moviepy.video.io.VideoFileClip import VideoFileClip from moviepy.video.VideoClip import BitmapClip, ColorClip