Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement basic data model #1076

Merged
merged 13 commits into from
May 26, 2023
18 changes: 3 additions & 15 deletions .github/workflows/test_suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions docs/getting_started/quick_presentation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,14 @@ In a typical MoviePy script, you load video or audio files, modify them, put the
from moviepy 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.multiply_volume(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.with_position('center').with_duration(10)
Expand Down
85 changes: 82 additions & 3 deletions moviepy/Clip.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Implements the central object of MoviePy, the Clip, and all the methods that
are common to the two subclasses of Clip, VideoClip and AudioClip.
"""

import copy as _copy
from numbers import Real

import numpy as np
import proglog
Expand Down Expand Up @@ -388,6 +388,9 @@ 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]``.

Parameters
----------

Expand Down Expand Up @@ -562,10 +565,86 @@ def __eq__(self, other):

return True

# Support the Context Manager protocol, to ensure that resources are cleaned up.

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 `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 `cvsec`)
and return the frame at that time, passing the key
to ``get_frame``.
"""
apply_to = ["mask", "audio"]
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
factor = abs(key.step)
if factor != 1:
# change speed
clip = clip.time_transform(
lambda t: factor * t, apply_to=apply_to, keep_duration=True
)
clip = clip.with_duration(1.0 * clip.duration / factor)
if key.step < 0:
# time mirror
clip = clip.time_transform(
lambda t: clip.duration - t - 1,
keep_duration=True,
apply_to=apply_to,
)
return clip
elif isinstance(key, tuple):
# get a concatenation of subclips
return sum(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
return NotImplemented

def __mul__(self, n):
# loop n times where N is a real
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)
5 changes: 5 additions & 0 deletions moviepy/audio/AudioClip.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,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)


class AudioArrayClip(AudioClip):
"""
Expand Down
13 changes: 12 additions & 1 deletion moviepy/video/VideoClip.py
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,17 @@ def afx(self, fun, *args, **kwargs):
"""
self.audio = self.audio.fx(fun, *args, **kwargs)

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.with_mask(mask)


class DataVideoClip(VideoClip):
"""
Expand All @@ -918,7 +929,7 @@ class DataVideoClip(VideoClip):
Parameters
----------
data
A liste of datasets, each dataset being used for one frame of the clip
A list of datasets, each dataset being used for one frame of the clip

data_to_frame
A function d -> video frame, where d is one element of the list `data`
Expand Down
4 changes: 2 additions & 2 deletions moviepy/video/fx/freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def freeze(clip, t=0, freeze_duration=None, total_duration=None, padding_end=0):
)
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).with_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)
2 changes: 1 addition & 1 deletion moviepy/video/fx/time_mirror.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ def time_mirror(clip):
The clip must have its ``duration`` attribute set.
The same effect is applied to the clip's audio and mask if any.
"""
return clip.time_transform(lambda t: clip.duration - t - 1, keep_duration=True)
return clip[::-1]
7 changes: 2 additions & 5 deletions moviepy/video/fx/time_symmetrize.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
from moviepy.decorators import apply_to_mask, requires_duration
from moviepy.video.compositing.concatenate import concatenate_videoclips
from moviepy.video.fx.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
Expand All @@ -13,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]
13 changes: 5 additions & 8 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
#!/usr/bin/env python
"""MoviePy setup script."""

import os
import sys
from codecs import open
from pathlib import Path


try:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
21 changes: 21 additions & 0 deletions tests/test_VideoClip.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,5 +441,26 @@ def test_afterimage(util):
final_clip.write_videofile(filename, fps=30, logger=None)


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", is_mask=True, transparent=True)
clip_with_mask = clip & maskclip
assert clip_with_mask.mask is maskclip


if __name__ == "__main__":
pytest.main()
42 changes: 32 additions & 10 deletions tests/test_compositing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't really understand this test nor its regression, as it's not affected by my new methods . It was failing like this

E       AssertionError: Expected (7f,7f,00) but got (00,80,80) at timestamp 0.5
E       assert not True

which is the same error that I get in master if I apply this diff to the test

(lab) tin@morocha:~/lab/moviepy$ git diff
diff --git a/tests/test_compositing.py b/tests/test_compositing.py
index 5824034..d3bdee2 100644
--- a/tests/test_compositing.py
+++ b/tests/test_compositing.py
@@ -105,6 +105,7 @@ def test_blit_with_opacity():
         .with_opacity(0.5)
     )
     composite = CompositeVideoClip([clip1, clip2])
+    composite.write_videofile("compo.mp4")
     bt = ClipPixelTest(composite)
 
     bt.expect_color_at(0.5, (0x7F, 0x7F, 0x00))

ie,write de composed videofile before the assert, it shouldn't affect, right? what's happening?

As an alternative, I rewrote the test as I understood below using ColorClip. Is it correct?

# # 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))


Expand Down
2 changes: 1 addition & 1 deletion tests/test_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ def size(t):
avatar.audio = None
maskclip = ImageClip("media/afterimage.png", is_mask=True, transparent=True)
avatar.with_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:
Expand Down