From 02bcf1a00a912f184d46ea1589e8353d24cc1f0d Mon Sep 17 00:00:00 2001 From: Tom Burrows Date: Sat, 11 Apr 2020 12:52:12 +0100 Subject: [PATCH 01/12] Followup to TextClip.list() fix (#1119) (#1132) --- moviepy/video/VideoClip.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index cce72c78a..b97db58cd 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -13,9 +13,9 @@ import proglog from imageio import imread, imsave -from ..Clip import Clip -from ..config import get_setting -from ..decorators import ( +from moviepy.Clip import Clip +from moviepy.config import get_setting +from moviepy.decorators import ( add_mask_if_none, apply_to_mask, convert_masks_to_RGB, @@ -24,14 +24,18 @@ requires_duration, use_clip_fps_by_default, ) -from ..tools import ( +from moviepy.tools import ( extensions_dict, find_extension, subprocess_call, ) -from .io.ffmpeg_writer import ffmpeg_write_video -from .io.gif_writers import write_gif, write_gif_with_image_io, write_gif_with_tempfiles -from .tools.drawing import blit +from moviepy.video.io.ffmpeg_writer import ffmpeg_write_video +from moviepy.video.io.gif_writers import ( + write_gif, + write_gif_with_image_io, + write_gif_with_tempfiles, +) +from moviepy.video.tools.drawing import blit class VideoClip(Clip): @@ -1231,14 +1235,20 @@ def list(arg): popen_params["creationflags"] = 0x08000000 process = sp.Popen( - [get_setting("IMAGEMAGICK_BINARY"), "-list", arg], **popen_params + [get_setting("IMAGEMAGICK_BINARY"), "-list", arg], + encoding="utf-8", + **popen_params, ) - result = process.communicate()[0].decode() + result = process.communicate()[0] lines = result.splitlines() if arg == "font": + # Slice removes first 8 characters: " Font: " return [l[8:] for l in lines if l.startswith(" Font:")] elif arg == "color": + # Each line is of the format "aqua srgb(0,255,255) SVG" so split on space and take + # the first item to get the color name. + # The first 5 lines are header information, not colors, so ignore return [l.split(" ")[0] for l in lines[5:]] else: raise Exception("Moviepy Error: Argument must equal 'font' or 'color'") From b606067833d95016937fb90732400d93ee5389e8 Mon Sep 17 00:00:00 2001 From: Tom Burrows Date: Tue, 14 Apr 2020 16:41:47 +0100 Subject: [PATCH 02/12] Fix for `ColorClip.rotate()` (#1139) --- CHANGELOG.md | 1 + moviepy/video/fx/rotate.py | 5 ++++- tests/test_fx.py | 10 ++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae1215e13..0069aef8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - When using `VideoClip.write_videofile()` with `write_logfile=True`, errors would not be properly reported [#890] - `TextClip.list("color")` now returns a list of bytes, not strings [#1119] - `TextClip.search("colorname", "color")` does not crash with a TypeError [#1119] +- Using `rotate()` with a `ColorClip` no longer crashes [#1139] ## [v1.0.2](https://github.com/zulko/moviepy/tree/v1.0.2) (2020-03-26) diff --git a/moviepy/video/fx/rotate.py b/moviepy/video/fx/rotate.py index e20623e70..3aeddba38 100644 --- a/moviepy/video/fx/rotate.py +++ b/moviepy/video/fx/rotate.py @@ -8,8 +8,11 @@ PIL_FOUND = True def pil_rotater(pic, angle, resample, expand): + # Ensures that pic is of the correct type return np.array( - Image.fromarray(pic).rotate(angle, expand=expand, resample=resample) + Image.fromarray(np.array(pic).astype(np.uint8)).rotate( + angle, expand=expand, resample=resample + ) ) diff --git a/tests/test_fx.py b/tests/test_fx.py index eb46b5ba7..8de4b6d27 100644 --- a/tests/test_fx.py +++ b/tests/test_fx.py @@ -6,6 +6,7 @@ from moviepy.audio.fx.audio_normalize import audio_normalize from moviepy.audio.io.AudioFileClip import AudioFileClip from moviepy.utils import close_all_clips +from moviepy.video.VideoClip import ColorClip from moviepy.video.fx.blackwhite import blackwhite # from moviepy.video.fx.blink import blink @@ -216,6 +217,15 @@ def test_rotate(): clip4 = rotate(clip, 360) # rotate 90 degrees assert clip4.size == tuple(clip.size) clip4.write_videofile(os.path.join(TMP_DIR, "rotate4.webm")) + + clip5 = rotate(clip, 50) + clip5.write_videofile(os.path.join(TMP_DIR, "rotate5.webm")) + + # Test rotate with color clip + clip = ColorClip([600, 400], [150, 250, 100]).set_duration(1).set_fps(5) + clip = rotate(clip, 20) + clip.write_videofile(os.path.join(TMP_DIR, "color_rotate.webm")) + close_all_clips(locals()) From 00693440aec790b3412088efbe59d358b671ab2c Mon Sep 17 00:00:00 2001 From: Jonne Kaunisto Date: Tue, 14 Apr 2020 12:16:28 -0700 Subject: [PATCH 03/12] Fix undefined variable in Clip.set_end() (#1146) --- moviepy/Clip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moviepy/Clip.py b/moviepy/Clip.py index fb3ea2cb7..51ec13976 100644 --- a/moviepy/Clip.py +++ b/moviepy/Clip.py @@ -262,7 +262,7 @@ def set_end(self, t): return if self.start is None: if self.duration is not None: - self.start = max(0, t - newclip.duration) + self.start = max(0, t - self.duration) else: self.duration = self.end - self.start From 0c0275c8c2f55dfea754f1284458f88c585fbb34 Mon Sep 17 00:00:00 2001 From: Tom Burrows Date: Wed, 15 Apr 2020 10:06:51 +0100 Subject: [PATCH 04/12] Add --diff to black check --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7dfb31cd1..bf126a35b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,4 +11,4 @@ jobs: - name: Black Code Formatter uses: lgeiger/black-action@v1.0.1 with: - args: "--check --target-version py36 $GITHUB_WORKSPACE" \ No newline at end of file + args: "--check --diff --target-version py36 $GITHUB_WORKSPACE" From c62c8b181a7cb9c0a4d89ddb859eee3c0ef6042f Mon Sep 17 00:00:00 2001 From: Roman Scher Date: Fri, 17 Apr 2020 05:44:45 -0400 Subject: [PATCH 05/12] Add test for detect_scenes (#589) --- tests/test_videotools.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/test_videotools.py b/tests/test_videotools.py index 2609341c7..0ed9dda3d 100644 --- a/tests/test_videotools.py +++ b/tests/test_videotools.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- """Video file clip tests meant to be run with pytest.""" import os -import sys +from moviepy.video.VideoClip import ColorClip +from moviepy.video.compositing.concatenate import concatenate_videoclips from moviepy.video.tools.credits import credits1 +from moviepy.video.tools.cuts import detect_scenes from .test_helper import TMP_DIR, FONT @@ -38,3 +40,16 @@ def test_credits(): image = image.set_duration(3) image.write_videofile(vid_location, fps=24) assert os.path.isfile(vid_location) + + +def test_detect_scenes(): + """ + Test that a cut is detected between concatenated red and green clips + """ + red = ColorClip((640, 480), color=(255, 0, 0)).set_duration(1) + green = ColorClip((640, 480), color=(0, 200, 0)).set_duration(1) + video = concatenate_videoclips([red, green]) + + cuts, luminosities = detect_scenes(video, fps=10, logger=None) + + assert len(cuts) == 2 From a996fbaf50ff09103cbd68e614f0d0e225f69336 Mon Sep 17 00:00:00 2001 From: Tom Burrows Date: Fri, 17 Apr 2020 10:48:36 +0100 Subject: [PATCH 06/12] Fix undefined name warning in ez_setup.py (#1149) --- ez_setup.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/ez_setup.py b/ez_setup.py index b0d6f2aab..29bbd6d0f 100644 --- a/ez_setup.py +++ b/ez_setup.py @@ -206,15 +206,7 @@ def _extractall(self, path=".", members=None): self.extract(tarinfo, path) # Reverse sort directories. - if sys.version_info < (2, 4): - - def sorter(dir1, dir2): - return cmp(dir1.name, dir2.name) - - directories.sort(sorter) - directories.reverse() - else: - directories.sort(key=operator.attrgetter("name"), reverse=True) + directories.sort(key=operator.attrgetter("name"), reverse=True) # Set correct owner, mtime and filemode on directories. for tarinfo in directories: From 103b2aa006f5dcc201abb343dc86b14c647d1b5d Mon Sep 17 00:00:00 2001 From: Jonne Kaunisto Date: Fri, 17 Apr 2020 17:54:29 -0700 Subject: [PATCH 07/12] Delete outdated opencv install instructions and fixe english syntax (#1148) --- README.rst | 2 +- docs/FAQ.rst | 2 +- docs/examples/compo_from_image.rst | 2 +- docs/gallery.rst | 2 +- docs/getting_started/compositing.rst | 2 +- docs/getting_started/effects.rst | 4 +-- docs/getting_started/efficient_moviepy.rst | 4 +-- docs/getting_started/quick_presentation.rst | 4 +-- docs/getting_started/videoclips.rst | 8 ++--- docs/index.rst | 5 ++- docs/install.rst | 6 ++-- docs/opencv_instructions.rst | 35 --------------------- examples/headblur.py | 2 +- examples/star_worms.py | 2 +- moviepy/audio/fx/all/__init__.py | 2 +- 15 files changed, 23 insertions(+), 59 deletions(-) delete mode 100644 docs/opencv_instructions.rst diff --git a/README.rst b/README.rst index 62003b4fb..c39d9b0f6 100644 --- a/README.rst +++ b/README.rst @@ -219,7 +219,7 @@ Maintainers .. Software, Tools, Libraries .. _Pillow: https://pillow.readthedocs.org/en/latest/ .. _Scipy: https://www.scipy.org/ -.. _`OpenCV 2.4.6`: https://sourceforge.net/projects/opencvlibrary/files/ +.. _`OpenCV 2.4.6`: https://github.com/skvark/opencv-python .. _Pygame: https://www.pygame.org/download.shtml .. _Numpy: https://www.scipy.org/install.html .. _imageio: https://imageio.github.io/ diff --git a/docs/FAQ.rst b/docs/FAQ.rst index 6eaadf980..2ff04b07e 100644 --- a/docs/FAQ.rst +++ b/docs/FAQ.rst @@ -25,7 +25,7 @@ I can't seem to read any video with MoviePy """""""""""""""""""""""""""""""""""""""""""""" Known reason: you have a deprecated version of FFMPEG, install a recent version from the -website, not from your OS's repositories ! (see :ref:`install`). +website, not from your OS's repositories! (see :ref:`install`). Previewing videos make them slower than they are """"""""""""""""""""""""""""""""""""""""""""""""" diff --git a/docs/examples/compo_from_image.rst b/docs/examples/compo_from_image.rst index c6f31fd08..d542adb3d 100644 --- a/docs/examples/compo_from_image.rst +++ b/docs/examples/compo_from_image.rst @@ -3,7 +3,7 @@ Placing clips according to a picture ====================================== -So how do you do some complex compositing like this ? +So how do you do some complex compositing like this? .. raw:: html diff --git a/docs/gallery.rst b/docs/gallery.rst index fd74ed72e..53473705c 100644 --- a/docs/gallery.rst +++ b/docs/gallery.rst @@ -4,7 +4,7 @@ Gallery ======== -Here are a few projects using MoviePy. The gallery will fill up as more people start using MoviePy (which is currently one year old). If you have a nice project using MoviePy let us know ! +Here are a few projects using MoviePy. The gallery will fill up as more people start using MoviePy (which is currently one year old). If you have a nice project using MoviePy let us know! Videos edited with Moviepy --------------------------- diff --git a/docs/getting_started/compositing.rst b/docs/getting_started/compositing.rst index 082b576bb..6447d40fa 100644 --- a/docs/getting_started/compositing.rst +++ b/docs/getting_started/compositing.rst @@ -103,7 +103,7 @@ There are many ways to specify the position: :: # clip2 is at 40% of the width, 70% of the height of the screen: clip2.set_position((0.4,0.7), relative=True) - # clip2's position is horizontally centered, and moving down ! + # clip2's position is horizontally centered, and moving down! clip2.set_position(lambda t: ('center', 50+t) ) When indicating the position keep in mind that the ``y`` coordinate has its zero at the top of the picture: diff --git a/docs/getting_started/effects.rst b/docs/getting_started/effects.rst index 923c3a15f..1f6241b1f 100644 --- a/docs/getting_started/effects.rst +++ b/docs/getting_started/effects.rst @@ -13,7 +13,7 @@ All these effects have in common that they are **not inplace**: they do NOT modi my_clip = VideoFileClip("some_file.mp4") my_clip.set_start(t=5) # does nothing, changes are lost - my_new_clip = my_clip.set_start(t=5) # good ! + my_new_clip = my_clip.set_start(t=5) # good! Also, when you write ``clip.resize(width=640)``, it does not immediately applies the effect to all the frames of the clip, but only to the first frame: all the other frames will be resized only when required (that is, when you will write the whole clip to a file of when you will preview it). Said otherwise, creating a new clip is neither time nor memory hungry, all the computations happen during the final rendering. @@ -47,7 +47,7 @@ but this is not easy to read. To have a clearer syntax you can use ``clip.fx``: .fx( effect_2, args2) .fx( effect_3, args3)) -Much better ! There are already many effects implemented in the modules ``moviepy.video.fx`` and ``moviepy.audio.fx``. The fx methods in these modules are automatically applied to the sound and the mask of the clip if it is relevant, so that you don't have to worry about modifying these. For practicality, when you use ``from moviepy.editor import *``, these two modules are loaded as ``vfx`` and ``afx``, so you may write something like :: +Much better! There are already many effects implemented in the modules ``moviepy.video.fx`` and ``moviepy.audio.fx``. The fx methods in these modules are automatically applied to the sound and the mask of the clip if it is relevant, so that you don't have to worry about modifying these. For practicality, when you use ``from moviepy.editor import *``, these two modules are loaded as ``vfx`` and ``afx``, so you may write something like :: from moviepy.editor import * clip = (VideoFileClip("myvideo.avi") diff --git a/docs/getting_started/efficient_moviepy.rst b/docs/getting_started/efficient_moviepy.rst index ea328c307..5a8f91383 100644 --- a/docs/getting_started/efficient_moviepy.rst +++ b/docs/getting_started/efficient_moviepy.rst @@ -9,10 +9,10 @@ The best way to start with MoviePy is to use it with the IPython Notebook: it ma .. _should_i_use_moviepy_editor: -Should I use ``moviepy.editor`` ? +Should I use ``moviepy.editor``? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Most examples in this documentation use the submodule ``moviepy.editor``, but this submodule is not adapted to all needs so should *you* use it ? Short answer: if you use MoviePy to edit videos *by hand*, use it, but if you use MoviePy inside a larger library or program or webserver, it is better to avoid it and just load the functions that you need. +Most examples in this documentation use the submodule ``moviepy.editor``, but this submodule is not adapted to all needs so should *you* use it? Short answer: if you use MoviePy to edit videos *by hand*, use it, but if you use MoviePy inside a larger library or program or webserver, it is better to avoid it and just load the functions that you need. The module ``moviepy.editor`` can be loaded using one of the three following methods: :: diff --git a/docs/getting_started/quick_presentation.rst b/docs/getting_started/quick_presentation.rst index 54884da45..2561e4060 100644 --- a/docs/getting_started/quick_presentation.rst +++ b/docs/getting_started/quick_presentation.rst @@ -5,7 +5,7 @@ Quick presentation This section explains when MoviePy can be used and how it works. -Do I need MoviePy ? +Do I need MoviePy? ~~~~~~~~~~~~~~~~~~~ Here are a few reasons why you may want to edit videos in Python: @@ -56,7 +56,7 @@ In a typical MoviePy script, you load video or audio files, modify them, put the # Overlay the text clip on the first video clip video = CompositeVideoClip([clip, txt_clip]) - # Write the result to a file (many options available !) + # Write the result to a file (many options available!) video.write_videofile("myHolidays_edited.webm") diff --git a/docs/getting_started/videoclips.rst b/docs/getting_started/videoclips.rst index 817f74fd4..dbae04b8c 100644 --- a/docs/getting_started/videoclips.rst +++ b/docs/getting_started/videoclips.rst @@ -12,11 +12,11 @@ The following code summarizes the base clips that you can create with moviepy: : clip = VideoFileClip("my_video_file.mp4") # or .avi, .webm, .gif ... clip = ImageSequenceClip(['image_file1.jpeg', ...], fps=24) clip = ImageClip("my_picture.png") # or .jpeg, .tiff, ... - clip = TextClip("Hello !", font="Amiri-Bold", fontsize=70, color="black") + clip = TextClip("Hello!", font="Amiri-Bold", fontsize=70, color="black") clip = ColorClip(size=(460,380), color=[R,G,B]) # AUDIO CLIPS - clip = AudioFileClip("my_audiofile.mp3") # or .ogg, .wav... or a video ! + clip = AudioFileClip("my_audiofile.mp3") # or .ogg, .wav... or a video! clip = AudioArrayClip(numpy_array, fps=44100) # from a numerical array clip = AudioClip(make_frame, duration=3) # uses a function make_frame(t) @@ -170,8 +170,8 @@ Sometimes it is impossible for MoviePy to guess the ``duration`` attribute of th # Make a video showing a flower for 5 seconds my_clip = Image("flower.jpeg") # has infinite duration - my_clip.write_videofile("flower.mp4") # Will fail ! NO DURATION ! - my_clip.set_duration(5).write_videofile("flower.mp4") # works ! + my_clip.write_videofile("flower.mp4") # Will fail! NO DURATION! + my_clip.set_duration(5).write_videofile("flower.mp4") # works! Animated GIFs diff --git a/docs/index.rst b/docs/index.rst index 1e8d21234..5007c37cf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -27,15 +27,14 @@ User Guide gallery examples/examples docker - opencv_instructions FAQ advanced_tools/advanced_tools ref/ref -Contribute ! +Contribute! -------------- -MoviePy is an open source software originally written by Zulko_ and released under the MIT licence. It works on Windows, Mac, and Linux, with Python 2 or Python 3. The code is hosted on Github_, where you can push improvements, report bugs and ask for help. There is also a MoviePy forum on Reddit_ and a mailing list on librelist_ . +MoviePy is an open source software originally written by Zulko_ and released under the MIT licence. It works on Windows, Mac, and Linux, with Python 2 or Python 3. The code is hosted on Github_, where you can push improvements, report bugs and ask for help. There is also a MoviePy forum on Reddit_ and a mailing list on librelist_. .. raw:: html diff --git a/docs/install.rst b/docs/install.rst index 6a9402fcf..f5acd0f28 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -19,7 +19,7 @@ If you have neither ``setuptools`` nor ``ez_setup`` installed the command above (sudo) python setup.py install -MoviePy depends on the Python modules NumPy_, Imageio_, Decorator_, and Proglog_, which will be automatically installed during MoviePy's installation. It should work on Windows/Mac/Linux, with Python 2.7+ and 3 ; if you have trouble installing MoviePy or one of its dependencies, please provide feedback ! +MoviePy depends on the Python modules NumPy_, Imageio_, Decorator_, and Proglog_, which will be automatically installed during MoviePy's installation. It should work on Windows/Mac/Linux, with Python 2.7+ and 3 ; if you have trouble installing MoviePy or one of its dependencies, please provide feedback! MoviePy depends on the software FFMPEG for video reading and writing. You don't need to worry about that, as FFMPEG should be automatically downloaded/installed by ImageIO during your first use of MoviePy (it takes a few seconds). If you want to use a specific version of FFMPEG, you can set the FFMPEG_BINARY environment variable See ``moviepy/config_defaults.py`` for details. @@ -29,7 +29,7 @@ Other optional but useful dependencies ImageMagick_ is not strictly required, only if you want to write texts. It can also be used as a backend for GIFs but you can do GIFs with MoviePy without ImageMagick. -Once you have installed it, ImageMagick will be automatically detected by MoviePy, **except on Windows !**. Windows user, before installing MoviePy by hand, go into the ``moviepy/config_defaults.py`` file and provide the path to the ImageMagick binary called `magick`. It should look like this :: +Once you have installed it, ImageMagick will be automatically detected by MoviePy, **except on Windows!**. Windows user, before installing MoviePy by hand, go into the ``moviepy/config_defaults.py`` file and provide the path to the ImageMagick binary called `magick`. It should look like this :: IMAGEMAGICK_BINARY = "C:\\Program Files\\ImageMagick_VERSION\\magick.exe" @@ -65,6 +65,6 @@ If you are on linux, these packages will likely be in your repos. .. _Github: https://github.com/Zulko/moviepy .. _PyPI: https://pypi.python.org/pypi/moviepy -.. _`OpenCV 2.4.6`: https://sourceforge.net/projects/opencvlibrary/files/ +.. _`OpenCV 2.4.6`: https://github.com/skvark/opencv-python diff --git a/docs/opencv_instructions.rst b/docs/opencv_instructions.rst deleted file mode 100644 index de161762f..000000000 --- a/docs/opencv_instructions.rst +++ /dev/null @@ -1,35 +0,0 @@ -.. _opencv: - -So you want to install OpenCV 2.4.6 ? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -OpenCV is very optional, its installation is not always simple and I found it to be unstable, be warned ! -The installation seems easy for Windows. On linux, here is what I found on the Internet: - -- Remove any other version of OpenCV if you installed it through a package manager. -- Unzip the source code of `OpenCV 2.4.6` in some folder. open a terminal in this folder. -- Make a new directory and go into this directory: :: - - mkdir release - cd release - -- Run ``cmake``. Here is the line I used: :: - - cmake -D WITH_TBB=ON -D BUILD_NEW_PYTHON_SUPPORT=ON -D WITH_V4L=OFF -D INSTALL_C_EXAMPLES=ON -D INSTALL_PYTHON_EXAMPLES=ON -D BUILD_EXAMPLES=ON .. - -- Run ``make``. This may take a few minutes (15 minutes on my computer). :: - - make - -- Finally, install. :: - - sudo make install - -And voilĂ  ! - -You can check if it worked by opeing a Python console and typing :: - - import cv2 - print cv2.__version__ - -Advice: do not throw your ``release`` folder away. If later you have strange bugs with OpenCV involving ``.so`` files, just redo the ``sudo make install`` step. diff --git a/examples/headblur.py b/examples/headblur.py index 4d3193ef2..83cca724e 100644 --- a/examples/headblur.py +++ b/examples/headblur.py @@ -34,7 +34,7 @@ # Generate the text, put in on a grey background txt = TextClip( - "Hey you ! \n You're blurry!", + "Hey you! \n You're blurry!", color="grey70", size=clip.size, bg_color="grey20", diff --git a/examples/star_worms.py b/examples/star_worms.py index 0117fffb6..58c8d736a 100644 --- a/examples/star_worms.py +++ b/examples/star_worms.py @@ -115,7 +115,7 @@ def trapzWarp(pic, cx, cy, ismask=False): We will now code the video tutorial for this video. When you think about it, it is a code for a video explaining how to - make another video using some code (this is so meta !). + make another video using some code (this is so meta!). This code uses the variables of the previous code (it should be placed after that previous code to work). diff --git a/moviepy/audio/fx/all/__init__.py b/moviepy/audio/fx/all/__init__.py index fabfc41e0..02453c497 100644 --- a/moviepy/audio/fx/all/__init__.py +++ b/moviepy/audio/fx/all/__init__.py @@ -1,5 +1,5 @@ """ -Loads all the fx ! +Loads all the fx! Usage: import moviepy.audio.fx.all as afx audio_clip = afx.volume_x(some_clip, .5) From 0f3b41147b5f26cc0113e8518ae7d7defde49306 Mon Sep 17 00:00:00 2001 From: Tom Burrows Date: Sat, 18 Apr 2020 16:29:39 +0100 Subject: [PATCH 08/12] Fix logger in tools.subprocess_call Broken since https://github.com/Zulko/moviepy/commit/bfad5eabfaa32fc11b5056736b7b0f4bb7ccf878 --- moviepy/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moviepy/tools.py b/moviepy/tools.py index 1091091b7..f38b13b5e 100644 --- a/moviepy/tools.py +++ b/moviepy/tools.py @@ -25,7 +25,7 @@ def subprocess_call(cmd, logger="bar", errorprint=True): Set logger to None or a custom Proglog logger to avoid printings. """ logger = proglog.default_bar_logger(logger) - logger(message='Moviepy - Running:\n>>> "+ " ".join(cmd)') + logger(message="Moviepy - Running:\n>>> "+ " ".join(cmd)) popen_params = {"stdout": sp.DEVNULL, "stderr": sp.PIPE, "stdin": sp.DEVNULL} From 57f1110b0f6e9779e6fa9f81a453c7355363aa30 Mon Sep 17 00:00:00 2001 From: Tom Burrows Date: Sat, 18 Apr 2020 16:58:35 +0100 Subject: [PATCH 09/12] Add support for path-like objects with a decorator (#1137) Implement path support by use of `convert_path_to_string` decorator --- CHANGELOG.md | 1 + moviepy/audio/AudioClip.py | 7 +++-- moviepy/audio/io/AudioFileClip.py | 4 +++ moviepy/decorators.py | 11 ++++++-- moviepy/video/VideoClip.py | 18 ++++++++---- moviepy/video/io/VideoFileClip.py | 9 ++++-- moviepy/video/io/ffmpeg_tools.py | 6 ++++ moviepy/video/tools/credits.py | 6 ++-- moviepy/video/tools/subtitles.py | 8 ++++-- tests/test_PR.py | 47 ++++++++++++++++++++++++++++++- 10 files changed, 98 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0069aef8f..542b9d2c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support removed for Python versions 2.7, 3.4 & 3.5 [#1103, #1106] ### Added +- Support for path-like objects as an option wherever filenames are passed in as arguments - Optional `encoding` parameter in `SubtitlesClip` [#1043] ### Changed diff --git a/moviepy/audio/AudioClip.py b/moviepy/audio/AudioClip.py index 508ab7438..a15624a8a 100644 --- a/moviepy/audio/AudioClip.py +++ b/moviepy/audio/AudioClip.py @@ -5,7 +5,7 @@ from moviepy.audio.io.ffmpeg_audiowriter import ffmpeg_audiowrite from moviepy.Clip import Clip -from moviepy.decorators import requires_duration +from moviepy.decorators import requires_duration, convert_path_to_string from moviepy.tools import extensions_dict @@ -159,6 +159,7 @@ def max_volume(self, stereo=False, chunksize=50000, logger=None): return maxi @requires_duration + @convert_path_to_string("filename") def write_audiofile( self, filename, @@ -178,11 +179,11 @@ def write_audiofile( ----------- filename - Name of the output file + Name of the output file, as a string or a path-like object. fps Frames per second. If not set, it will try default to self.fps if - already set, otherwise it will default to 44100 + already set, otherwise it will default to 44100. nbytes Sample width (set to 2 for 16-bit sound, 4 for 32-bit sound) diff --git a/moviepy/audio/io/AudioFileClip.py b/moviepy/audio/io/AudioFileClip.py index 41919c991..58dcacf65 100644 --- a/moviepy/audio/io/AudioFileClip.py +++ b/moviepy/audio/io/AudioFileClip.py @@ -2,6 +2,7 @@ from moviepy.audio.AudioClip import AudioClip from moviepy.audio.io.readers import FFMPEG_AudioReader +from moviepy.decorators import convert_path_to_string class AudioFileClip(AudioClip): @@ -18,6 +19,7 @@ class AudioFileClip(AudioClip): filename Either a soundfile name (of any extension supported by ffmpeg) + as a string or a path-like object, or an array representing a sound. If the soundfile is not a .wav, it will be converted to .wav first, using the ``fps`` and ``bitrate`` arguments. @@ -62,6 +64,7 @@ class AudioFileClip(AudioClip): """ + @convert_path_to_string("filename") def __init__(self, filename, buffersize=200000, nbytes=2, fps=44100): AudioClip.__init__(self) @@ -74,6 +77,7 @@ def __init__(self, filename, buffersize=200000, nbytes=2, fps=44100): self.duration = self.reader.duration self.end = self.reader.duration self.buffersize = self.reader.buffersize + self.filename = filename self.make_frame = lambda t: self.reader.get_frame(t) self.nchannels = self.reader.nchannels diff --git a/moviepy/decorators.py b/moviepy/decorators.py index a994e6469..436e7aba5 100644 --- a/moviepy/decorators.py +++ b/moviepy/decorators.py @@ -1,6 +1,7 @@ """ all decorators used in moviepy go there """ +import os import decorator @@ -81,7 +82,8 @@ def wrapper(f, *a, **kw): names = func_code.co_varnames new_a = [ - fun(arg) if (name in varnames) else arg for (arg, name) in zip(a, names) + fun(arg) if (name in varnames) and (arg is not None) else arg + for (arg, name) in zip(a, names) ] new_kw = {k: fun(v) if k in varnames else v for (k, v) in kw.items()} return f(*new_a, **new_kw) @@ -90,10 +92,15 @@ def wrapper(f, *a, **kw): def convert_to_seconds(varnames): - "Converts the specified variables to seconds" + """Converts the specified variables to seconds""" return preprocess_args(cvsecs, varnames) +def convert_path_to_string(varnames): + """Converts the specified variables to a path string""" + return preprocess_args(os.fspath, varnames) + + @decorator.decorator def add_mask_if_none(f, clip, *a, **k): """ Add a mask to the clip if there is none. """ diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index b97db58cd..4444a117f 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -23,6 +23,7 @@ outplace, requires_duration, use_clip_fps_by_default, + convert_path_to_string, ) from moviepy.tools import ( extensions_dict, @@ -143,6 +144,7 @@ def save_frame(self, filename, t=0, withmask=True): @requires_duration @use_clip_fps_by_default @convert_masks_to_RGB + @convert_path_to_string("filename") def write_videofile( self, filename, @@ -170,7 +172,7 @@ def write_videofile( ----------- filename - Name of the video file to write in. + Name of the video file to write in, as a string or a path-like object. The extension must correspond to the "codec" used (see below), or simply be '.avi' (which will work with any codec). @@ -399,6 +401,7 @@ def write_images_sequence(self, nameformat, fps=None, withmask=True, logger="bar @requires_duration @convert_masks_to_RGB + @convert_path_to_string("filename") def write_gif( self, filename, @@ -421,7 +424,7 @@ def write_gif( ----------- filename - Name of the resulting gif file. + Name of the resulting gif file, as a string or a path-like object. fps Number of frames per second (see note below). If it @@ -930,8 +933,8 @@ class ImageClip(VideoClip): ----------- img - Any picture file (png, tiff, jpeg, etc.) or any array representing - an RGB image (for instance a frame from a VideoClip). + Any picture file (png, tiff, jpeg, etc.) as a string or a path-like object, + or any array representing an RGB image (for instance a frame from a VideoClip). ismask Set this parameter to `True` if the clip is a mask. @@ -953,7 +956,8 @@ def __init__( ): VideoClip.__init__(self, ismask=ismask, duration=duration) - if isinstance(img, str): + if not isinstance(img, np.ndarray): + # img is a string or path-like object, so read it in from disk img = imread(img) if len(img.shape) == 3: # img is (now) a RGB(a) numpy array @@ -1070,7 +1074,8 @@ class TextClip(ImageClip): ``filename``. filename - The name of a file in which there is the text to write. + The name of a file in which there is the text to write, + as a string or a path-like object. Can be provided instead of argument ``txt`` size @@ -1120,6 +1125,7 @@ class TextClip(ImageClip): """ + @convert_path_to_string("filename") def __init__( self, txt=None, diff --git a/moviepy/video/io/VideoFileClip.py b/moviepy/video/io/VideoFileClip.py index 9fa30bc85..db9b9a00c 100644 --- a/moviepy/video/io/VideoFileClip.py +++ b/moviepy/video/io/VideoFileClip.py @@ -2,6 +2,7 @@ from moviepy.audio.io.AudioFileClip import AudioFileClip from moviepy.Clip import Clip +from moviepy.decorators import convert_path_to_string from moviepy.video.io.ffmpeg_reader import FFMPEG_VideoReader from moviepy.video.VideoClip import VideoClip @@ -22,8 +23,9 @@ class VideoFileClip(VideoClip): ------------ filename: - The name of the video file. It can have any extension supported - by ffmpeg: .ogv, .mp4, .mpeg, .avi, .mov etc. + The name of the video file, as a string or a path-like object. + It can have any extension supported by ffmpeg: + .ogv, .mp4, .mpeg, .avi, .mov etc. has_mask: Set this to 'True' if there is a mask included in the videofile. @@ -75,6 +77,7 @@ class VideoFileClip(VideoClip): """ + @convert_path_to_string("filename") def __init__( self, filename, @@ -108,7 +111,7 @@ def __init__( self.size = self.reader.size self.rotation = self.reader.rotation - self.filename = self.reader.filename + self.filename = filename if has_mask: diff --git a/moviepy/video/io/ffmpeg_tools.py b/moviepy/video/io/ffmpeg_tools.py index 8128de903..29806f37b 100644 --- a/moviepy/video/io/ffmpeg_tools.py +++ b/moviepy/video/io/ffmpeg_tools.py @@ -5,9 +5,11 @@ import sys from moviepy.config import get_setting +from moviepy.decorators import convert_path_to_string from moviepy.tools import subprocess_call +@convert_path_to_string("filename") def ffmpeg_movie_from_frames(filename, folder, fps, digits=6, bitrate="v"): """ Writes a movie out of the frames (picture files) in a folder. @@ -33,6 +35,7 @@ def ffmpeg_movie_from_frames(filename, folder, fps, digits=6, bitrate="v"): subprocess_call(cmd) +@convert_path_to_string(("filename", "targetname")) def ffmpeg_extract_subclip(filename, t1, t2, targetname=None): """ Makes a new video file playing video file ``filename`` between the times ``t1`` and ``t2``. """ @@ -62,6 +65,7 @@ def ffmpeg_extract_subclip(filename, t1, t2, targetname=None): subprocess_call(cmd) +@convert_path_to_string(("video", "audio", "output")) def ffmpeg_merge_video_audio( video, audio, @@ -90,6 +94,7 @@ def ffmpeg_merge_video_audio( subprocess_call(cmd, logger=logger) +@convert_path_to_string(("inputfile", "output")) def ffmpeg_extract_audio(inputfile, output, bitrate=3000, fps=44100): """ extract the sound from a video file and save it in ``output`` """ cmd = [ @@ -106,6 +111,7 @@ def ffmpeg_extract_audio(inputfile, output, bitrate=3000, fps=44100): subprocess_call(cmd) +@convert_path_to_string(("video", "output")) def ffmpeg_resize(video, output, size): """ resizes ``video`` to new size ``size`` and write the result in file ``output``. """ diff --git a/moviepy/video/tools/credits.py b/moviepy/video/tools/credits.py index 5403eb41b..edebd9dd9 100644 --- a/moviepy/video/tools/credits.py +++ b/moviepy/video/tools/credits.py @@ -3,12 +3,13 @@ credits, even though it is difficult to fill everyone needs in this matter. """ - +from moviepy.decorators import convert_path_to_string from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip from moviepy.video.fx.resize import resize from moviepy.video.VideoClip import ImageClip, TextClip +@convert_path_to_string("creditfile") def credits1( creditfile, width, @@ -26,7 +27,8 @@ def credits1( ----------- creditfile - A text file whose content must be as follows: :: + A string or path like object pointing to a text file + whose content must be as follows: :: # This is a comment # The next line says : leave 4 blank lines diff --git a/moviepy/video/tools/subtitles.py b/moviepy/video/tools/subtitles.py index d0ae3974c..9ec857d7f 100644 --- a/moviepy/video/tools/subtitles.py +++ b/moviepy/video/tools/subtitles.py @@ -4,6 +4,7 @@ import numpy as np +from moviepy.decorators import convert_path_to_string from moviepy.tools import cvsecs from moviepy.video.VideoClip import TextClip, VideoClip @@ -19,7 +20,8 @@ class SubtitlesClip(VideoClip): ========== subtitles - Either the name of a file, or a list + Either the name of a file as a string or path-like object, or a list + encoding Optional, specifies srt file encoding. Any standard Python encoding is allowed (listed at https://docs.python.org/3.8/library/codecs.html#standard-encodings) @@ -42,7 +44,8 @@ def __init__(self, subtitles, make_textclip=None, encoding=None): VideoClip.__init__(self, has_constant_size=False) - if isinstance(subtitles, str): + if not isinstance(subtitles, list): + # `subtitles` is a string or path-like object subtitles = file_to_subtitles(subtitles, encoding=encoding) # subtitles = [(map(cvsecs, tt),txt) for tt, txt in subtitles] @@ -148,6 +151,7 @@ def write_srt(self, filename): f.write(str(self)) +@convert_path_to_string("filename") def file_to_subtitles(filename, encoding=None): """ Converts a srt file into subtitles. diff --git a/tests/test_PR.py b/tests/test_PR.py index 3cabb8882..4dd8ef5f2 100644 --- a/tests/test_PR.py +++ b/tests/test_PR.py @@ -2,21 +2,23 @@ """Pull request tests meant to be run with pytest.""" import os import sys +from pathlib import Path import pytest +from moviepy.audio.io.AudioFileClip import AudioFileClip from moviepy.utils import close_all_clips from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip from moviepy.video.fx.scroll import scroll from moviepy.video.io.VideoFileClip import VideoFileClip from moviepy.video.tools.interpolators import Trajectory from moviepy.video.VideoClip import ColorClip, ImageClip, TextClip +from moviepy.video.tools.subtitles import SubtitlesClip from .test_helper import FONT, TMP_DIR def test_PR_306(): - assert TextClip.list("font") != [] assert TextClip.list("color") != [] @@ -100,5 +102,48 @@ def test_PR_610(): assert composite.fps == 25 +def test_PR_1137_video(): + """ + Test support for path-like objects as arguments for VideoFileClip. + """ + with VideoFileClip(Path("media/big_buck_bunny_432_433.webm")) as video: + video.write_videofile(Path(TMP_DIR) / "pathlike.mp4") + assert isinstance(video.filename, str) + + +def test_PR_1137_audio(): + """ + Test support for path-like objects as arguments for AudioFileClip. + """ + with AudioFileClip(Path("media/crunching.mp3")) as audio: + audio.write_audiofile(Path(TMP_DIR) / "pathlike.mp3") + assert isinstance(audio.filename, str) + + +def test_PR_1137_image(): + """ + Test support for path-like objects as arguments for ImageClip. + """ + ImageClip(Path("media/vacation_2017.jpg")).close() + + +def test_PR_1137_subtitles(): + """ + Test support for path-like objects as arguments for SubtitlesClip. + """ + + def make_textclip(txt): + return TextClip( + txt, + font=FONT, + fontsize=24, + color="white", + stroke_color="black", + stroke_width=0.5, + ) + + SubtitlesClip(Path("media/subtitles1.srt"), make_textclip=make_textclip).close() + + if __name__ == "__main__": pytest.main() From 467d20c3682f44d921e797d5e9120103a1c436dc Mon Sep 17 00:00:00 2001 From: Tom Burrows Date: Mon, 20 Apr 2020 21:57:00 +0100 Subject: [PATCH 10/12] Apply Black --- moviepy/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moviepy/tools.py b/moviepy/tools.py index f38b13b5e..09d30cced 100644 --- a/moviepy/tools.py +++ b/moviepy/tools.py @@ -25,7 +25,7 @@ def subprocess_call(cmd, logger="bar", errorprint=True): Set logger to None or a custom Proglog logger to avoid printings. """ logger = proglog.default_bar_logger(logger) - logger(message="Moviepy - Running:\n>>> "+ " ".join(cmd)) + logger(message="Moviepy - Running:\n>>> " + " ".join(cmd)) popen_params = {"stdout": sp.DEVNULL, "stderr": sp.PIPE, "stdin": sp.DEVNULL} From 9a328a3df27f829955e6987a606f49bbe996120b Mon Sep 17 00:00:00 2001 From: spollard Date: Mon, 20 Apr 2020 15:01:17 -0600 Subject: [PATCH 11/12] Fixed typo in getting_started/effects (#1153) --- docs/getting_started/effects.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting_started/effects.rst b/docs/getting_started/effects.rst index 1f6241b1f..8afb387f8 100644 --- a/docs/getting_started/effects.rst +++ b/docs/getting_started/effects.rst @@ -15,7 +15,7 @@ All these effects have in common that they are **not inplace**: they do NOT modi my_clip.set_start(t=5) # does nothing, changes are lost my_new_clip = my_clip.set_start(t=5) # good! -Also, when you write ``clip.resize(width=640)``, it does not immediately applies the effect to all the frames of the clip, but only to the first frame: all the other frames will be resized only when required (that is, when you will write the whole clip to a file of when you will preview it). Said otherwise, creating a new clip is neither time nor memory hungry, all the computations happen during the final rendering. +Also, when you write ``clip.resize(width=640)``, it does not immediately apply the effect to all the frames of the clip, but only to the first frame: all the other frames will be resized only when required (that is, when you will write the whole clip to a file of when you will preview it). Said otherwise, creating a new clip is neither time nor memory hungry, all the computations happen during the final rendering. Time representations in MoviePy --------------------------------- From 24ebfdbd2ac63df19bbc05e7039bbb57e3c48493 Mon Sep 17 00:00:00 2001 From: spollard Date: Mon, 20 Apr 2020 17:33:43 -0600 Subject: [PATCH 12/12] Fixing a few typos in getting_started/efficient_moviepy (#1154) Found a few more typos. Simple fixes mostly. --- docs/getting_started/efficient_moviepy.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/getting_started/efficient_moviepy.rst b/docs/getting_started/efficient_moviepy.rst index 5a8f91383..191cb3a2a 100644 --- a/docs/getting_started/efficient_moviepy.rst +++ b/docs/getting_started/efficient_moviepy.rst @@ -21,8 +21,8 @@ The module ``moviepy.editor`` can be loaded using one of the three following met import moviepy.editor as mpy # Clean. Then use mpy.VideoClip, etc. from moviepy.editor import VideoFileClip # just import what you need -With any of these lines, the ``moviepy.editor`` module will actually do a lot of work behind the curtain: It will fetch all the most common classes, functions and subpackages of MoviePy, initialize a PyGame session (if PyGame is installed) to be able to preview video clips, and implement some shortcuts, like adding the ``resize`` transformation to the clips. This way you can use ``clip.resize(width=240)`` instead of the longer ``clip.fx( resize, width=240)``. In short, ``moviepy.editor`` -provides all you need to play around and edit your videos but it will take time to load (circa one second). So if all you need is one or two features inside another library, it is better to import directly what you need, as follows: :: +With any of these lines, the ``moviepy.editor`` module will actually do a lot of work behind the curtain: it will fetch all the most common classes, functions and subpackages of MoviePy, initialize a PyGame session (if PyGame is installed) to be able to preview video clips, and implement some shortcuts, like adding the ``resize`` transformation to the clips. This way you can use ``clip.resize(width=240)`` instead of the longer ``clip.fx( resize, width=240)``. In short, ``moviepy.editor`` +provides all you need to play around and edit your videos, but it will take time to load (circa one second). So if all you need is one or two features inside another library, it is better to import directly what you need, as follows: :: from moviepy.video.io.VideoFileClip import VideoFileClip from moviepy.video.fx.resize import resize @@ -34,13 +34,13 @@ When to close() a clip When you create some types of clip instances - e.g. ``VideoFileClip`` or ``AudioFileClip`` - MoviePy creates a subprocess and locks the file. In order to release those resources when you are finished you should call the ``close()`` method. -This is more important for more complex applications and it particularly important when running on Windows. While Python's garbage collector should eventually clean it the resources for you, clsing them makes them available earlier. +This is more important for more complex applications and is particularly important when running on Windows. While Python's garbage collector should eventually clean up the resources for you, closing them makes them available earlier. However, if you close a clip too early, methods on the clip (and any clips derived from it) become unsafe. So, the rules of thumb are: - * Call ``close()`` on any clip that you **construct** once you have finished using it, and have also finished using any clip that was derived from it. + * Call ``close()`` on any clip that you **construct** once you have finished using it and have also finished using any clip that was derived from it. * Also close any clips you create through ``AudioFileClip.coreader()``. * Even if you close a ``CompositeVideoClip`` instance, you still need to close the clips it was created from. * Otherwise, if you have a clip that was created by deriving it from from another clip (e.g. by calling ``set_mask()``), then generally you shouldn't close it. Closing the original clip will also close the copy. @@ -134,4 +134,4 @@ For instance, when you are editing an animated GIF and want to check that it loo ipython_display(my_clip, autoplay=1, loop=1) -Importantly, ``ipython_display`` actually embeds the clips physically in your notebook. The advantage is that you can move the notebook or put it online and the videos will work. The drawback is that the file size of the notebook can become very large. Depending on your browser, re-computing and displaying at video many times can take some place in the cache and the RAM (it will only be a problem for intensive uses). Restarting your browser solves the problem. \ No newline at end of file +Importantly, ``ipython_display`` actually embeds the clips physically in your notebook. The advantage is that you can move the notebook or put it online and the videos will work. The drawback is that the file size of the notebook can become very large. Depending on your browser, re-computing and displaying at video many times can take some place in the cache and the RAM (it will only be a problem for intensive uses). Restarting your browser solves the problem.