initial commit and version 1.0

This commit is contained in:
2025-04-21 15:14:03 +02:00
commit ae6b2bbf44
82 changed files with 10782 additions and 0 deletions

684
moviepy/Clip.py Normal file
View File

@@ -0,0 +1,684 @@
"""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 functools import reduce
from numbers import Real
from operator import add
from typing import TYPE_CHECKING, List
import numpy as np
import proglog
if TYPE_CHECKING:
from moviepy.Effect import Effect
from moviepy.decorators import (
apply_to_audio,
apply_to_mask,
convert_parameter_to_seconds,
outplace,
requires_duration,
use_clip_fps_by_default,
)
class Clip:
"""Base class of all clips (VideoClips and AudioClips).
Attributes
----------
start : float
When the clip is included in a composition, time of the
composition at which the clip starts playing (in seconds).
end : float
When the clip is included in a composition, time of the
composition at which the clip stops playing (in seconds).
duration : float
Duration of the clip (in seconds). Some clips are infinite, in
this case their duration will be ``None``.
"""
# prefix for all temporary video and audio files.
# You can overwrite it with
# >>> Clip._TEMP_FILES_PREFIX = "temp_"
_TEMP_FILES_PREFIX = "TEMP_MPY_"
def __init__(self):
self.start = 0
self.end = None
self.duration = None
self.memoize = False
self.memoized_t = None
self.memoized_frame = None
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,
or (mono or stereo) value for a sound clip, at time ``t``.
Parameters
----------
t : float or tuple or str
Moment of the clip whose frame will be returned.
"""
# Coming soon: smart error handling for debugging at this point
if self.memoize:
if t == self.memoized_t:
return self.memoized_frame
else:
frame = self.frame_function(t)
self.memoized_t = t
self.memoized_frame = frame
return frame
else:
return self.frame_function(t)
def transform(self, func, apply_to=None, keep_duration=True):
"""General processing of a clip.
Returns a new Clip whose frames are a transformation
(through function ``func``) of the frames of the current clip.
Parameters
----------
func : function
A function with signature (gf,t -> frame) where ``gf`` will
represent the current clip's ``get_frame`` method,
i.e. ``gf`` is a function (t->image). Parameter `t` is a time
in seconds, `frame` is a picture (=Numpy array) which will be
returned by the transformed clip (see examples below).
apply_to : {"mask", "audio", ["mask", "audio"]}, optional
Can be either ``'mask'``, or ``'audio'``, or
``['mask','audio']``.
Specifies if the filter should also be applied to the
audio or the mask of the clip, if any.
keep_duration : bool, optional
Set to True if the transformation does not change the
``duration`` of the clip.
Examples
--------
In the following ``new_clip`` a 100 pixels-high clip whose video
content scrolls from the top to the bottom of the frames of
``clip`` at 50 pixels per second.
>>> filter = lambda get_frame,t : get_frame(t)[int(t):int(t)+50, :]
>>> new_clip = clip.transform(filter, apply_to='mask')
"""
if apply_to is None:
apply_to = []
# mf = copy(self.frame_function)
new_clip = self.with_updated_frame_function(lambda t: func(self.get_frame, t))
if not keep_duration:
new_clip.duration = None
new_clip.end = None
if isinstance(apply_to, str):
apply_to = [apply_to]
for attribute in apply_to:
attribute_value = getattr(new_clip, attribute, None)
if attribute_value is not None:
new_attribute_value = attribute_value.transform(
func, keep_duration=keep_duration
)
setattr(new_clip, attribute, new_attribute_value)
return new_clip
def time_transform(self, time_func, apply_to=None, keep_duration=False):
"""
Returns a Clip instance playing the content of the current clip
but with a modified timeline, time ``t`` being replaced by the return
of `time_func(t)`.
Parameters
----------
time_func : function
A function ``t -> new_t``.
apply_to : {"mask", "audio", ["mask", "audio"]}, optional
Can be either 'mask', or 'audio', or ['mask','audio'].
Specifies if the filter ``transform`` should also be applied to the
audio or the mask of the clip, if any.
keep_duration : bool, optional
``False`` (default) if the transformation modifies the
``duration`` of the clip.
Examples
--------
.. code:: python
# plays the clip (and its mask and sound) twice faster
new_clip = clip.time_transform(lambda t: 2*t, apply_to=['mask', 'audio'])
# plays the clip starting at t=3, and backwards:
new_clip = clip.time_transform(lambda t: 3-t)
"""
if apply_to is None:
apply_to = []
return self.transform(
lambda get_frame, t: get_frame(time_func(t)),
apply_to,
keep_duration=keep_duration,
)
def with_effects(self, effects: List["Effect"]):
"""Return a copy of the current clip with the effects applied
>>> new_clip = clip.with_effects([vfx.Resize(0.2, method="bilinear")])
You can also pass multiple effect as a list
>>> clip.with_effects([afx.VolumeX(0.5), vfx.Resize(0.3), vfx.Mirrorx()])
"""
new_clip = self.copy()
for effect in effects:
# We always copy effect before using it, see Effect.copy
# to see why we need to
effect_copy = effect.copy()
new_clip = effect_copy.apply(new_clip)
return new_clip
@apply_to_mask
@apply_to_audio
@convert_parameter_to_seconds(["t"])
@outplace
def with_start(self, t, change_end=True):
"""Returns a copy of the clip, with the ``start`` attribute set
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'.
These changes are also applied to the ``audio`` and ``mask``
clips of the current clip, if they exist.
Parameters
----------
t : float or tuple or str
New ``start`` attribute value for the clip.
change_end : bool optional
Indicates if the ``end`` attribute value must be changed accordingly,
if possible. If ``change_end=True`` and the clip has a ``duration``
attribute, the ``end`` attribute of the clip will be updated to
``start + duration``. If ``change_end=False`` and the clip has a
``end`` attribute, the ``duration`` attribute of the clip will be
updated to ``end - start``.
"""
self.start = t
if (self.duration is not None) and change_end:
self.end = t + self.duration
elif self.end is not None:
self.duration = self.end - self.start
@apply_to_mask
@apply_to_audio
@convert_parameter_to_seconds(["t"])
@outplace
def with_end(self, t):
"""Returns a copy of the clip, with the ``end`` attribute set 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'. Also sets the duration
of the mask and audio, if any, of the returned clip.
Parameters
----------
t : float or tuple or str
New ``end`` attribute value for the clip.
"""
self.end = t
if self.end is None:
return
if self.start is None:
if self.duration is not None:
self.start = max(0, t - self.duration)
else:
self.duration = self.end - self.start
@apply_to_mask
@apply_to_audio
@convert_parameter_to_seconds(["duration"])
@outplace
def with_duration(self, duration, change_end=True):
"""Returns a copy of the clip, with the ``duration`` attribute set 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'. Also sets the duration
of the mask and audio, if any, of the returned clip.
If ``change_end is False``, the start attribute of the clip will be
modified in function of the duration and the preset end of the clip.
Parameters
----------
duration : float
New duration attribute value for the clip.
change_end : bool, optional
If ``True``, the ``end`` attribute value of the clip will be adjusted
accordingly to the new duration using ``clip.start + duration``.
"""
self.duration = duration
if change_end:
self.end = None if (duration is None) else (self.start + duration)
else:
if self.duration is None:
raise ValueError("Cannot change clip start when new duration is None")
self.start = self.end - duration
@outplace
def with_updated_frame_function(self, frame_function):
"""Sets a ``frame_function`` attribute for the clip. Useful for setting
arbitrary/complicated videoclips.
Parameters
----------
frame_function : function
New frame creator function for the clip.
"""
self.frame_function = frame_function
def with_fps(self, fps, change_duration=False):
"""Returns a copy of the clip with a new default fps for functions like
write_videofile, iterframe, etc.
Parameters
----------
fps : int
New ``fps`` attribute value for the clip.
change_duration : bool, optional
If ``change_duration=True``, then the video speed will change to
match the new fps (conserving all frames 1:1). For example, if the
fps is halved in this mode, the duration will be doubled.
"""
if change_duration:
from moviepy.video.fx.MultiplySpeed import MultiplySpeed
newclip = self.with_effects([MultiplySpeed(fps / self.fps)])
else:
newclip = self.copy()
newclip.fps = fps
return newclip
@outplace
def with_is_mask(self, is_mask):
"""Says whether the clip is a mask or not.
Parameters
----------
is_mask : bool
New ``is_mask`` attribute value for the clip.
"""
self.is_mask = is_mask
@outplace
def with_memoize(self, memoize):
"""Sets whether the clip should keep the last frame read in memory.
Parameters
----------
memoize : bool
Indicates if the clip should keep the last frame read in memory.
"""
self.memoize = memoize
@convert_parameter_to_seconds(["start_time", "end_time"])
@apply_to_mask
@apply_to_audio
def subclipped(self, start_time=0, end_time=None):
"""Returns a clip playing the content of the current clip between times
``start_time`` and ``end_time``, which can be expressed in seconds
(15.35), in (min, sec), in (hour, min, sec), or as a string:
'01:03:05.35'.
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
----------
start_time : float or tuple or str, optional
Moment that will be chosen as the beginning of the produced clip. If
is negative, it is reset to ``clip.duration + start_time``.
end_time : float or tuple or str, optional
Moment that will be chosen as the end of the produced clip. If not
provided, it is assumed to be the duration of the clip (potentially
infinite). If is negative, it is reset to ``clip.duration + end_time``.
For instance:
>>> # cut the last two seconds of the clip:
>>> new_clip = clip.subclipped(0, -2)
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
start_time = self.duration + start_time # Remember start_time is negative
if (self.duration is not None) and (start_time >= self.duration):
raise ValueError(
"start_time (%.02f) " % start_time
+ "should be smaller than the clip's "
+ "duration (%.02f)." % self.duration
)
new_clip = self.time_transform(lambda t: t + start_time, apply_to=[])
if (end_time is None) and (self.duration is not None):
end_time = self.duration
elif (end_time is not None) and (end_time < 0):
if self.duration is None:
raise ValueError(
(
"Subclip with negative times (here %s)"
" can only be extracted from clips with a ``duration``"
)
% (str((start_time, end_time)))
)
else:
end_time = self.duration + end_time
if end_time is not None:
new_clip.duration = end_time - start_time
new_clip.end = new_clip.start + new_clip.duration
return new_clip
@convert_parameter_to_seconds(["start_time", "end_time"])
def with_section_cut_out(self, start_time, end_time):
"""
Returns a clip playing the content of the current clip but
skips the extract between ``start_time`` and ``end_time``, which can be
expressed in seconds (15.35), in (min, sec), in (hour, min, sec),
or as a string: '01:03:05.35'.
If the original clip has a ``duration`` attribute set,
the duration of the returned clip is automatically computed as
`` duration - (end_time - start_time)``.
The resulting clip's ``audio`` and ``mask`` will also be cutout
if they exist.
Parameters
----------
start_time : float or tuple or str
Moment from which frames will be ignored in the resulting output.
end_time : float or tuple or str
Moment until which frames will be ignored in the resulting output.
"""
new_clip = self.time_transform(
lambda t: t + (t >= start_time) * (end_time - start_time),
apply_to=["audio", "mask"],
)
if self.duration is not None:
return new_clip.with_duration(self.duration - (end_time - start_time))
else: # pragma: no cover
return new_clip
def with_speed_scaled(self, factor: float = None, final_duration: float = None):
"""Returns a clip playing the current clip but at a speed multiplied
by ``factor``. For info on the parameters, please see ``vfx.MultiplySpeed``.
"""
from moviepy.video.fx.MultiplySpeed import MultiplySpeed
return self.with_effects(
[MultiplySpeed(factor=factor, final_duration=final_duration)]
)
def with_volume_scaled(self, factor: float, start_time=None, end_time=None):
"""Returns a new clip with audio volume multiplied by the value `factor`.
For info on the parameters, please see ``afx.MultiplyVolume``
"""
from moviepy.audio.fx.MultiplyVolume import MultiplyVolume
return self.with_effects(
[MultiplyVolume(factor=factor, start_time=start_time, end_time=end_time)]
)
@requires_duration
@use_clip_fps_by_default
def iter_frames(self, fps=None, with_times=False, logger=None, dtype=None):
"""Iterates over all the frames of the clip.
Returns each frame of the clip as a HxWxN Numpy array,
where N=1 for mask clips and N=3 for RGB clips.
This function is not really meant for video editing. It provides an
easy way to do frame-by-frame treatment of a video, for fields like
science, computer vision...
Parameters
----------
fps : int, optional
Frames per second for clip iteration. Is optional if the clip already
has a ``fps`` attribute.
with_times : bool, optional
Ff ``True`` yield tuples of ``(t, frame)`` where ``t`` is the current
time for the frame, otherwise only a ``frame`` object.
logger : str, optional
Either ``"bar"`` for progress bar or ``None`` or any Proglog logger.
dtype : type, optional
Type to cast Numpy array frames. Use ``dtype="uint8"`` when using the
pictures to write video, images..
Examples
--------
.. code:: python
# prints the maximum of red that is contained
# on the first line of each frame of the clip.
from moviepy import VideoFileClip
myclip = VideoFileClip('myvideo.mp4')
print([frame[0,:,0].max()
for frame in myclip.iter_frames()])
"""
logger = proglog.default_bar_logger(logger)
for frame_index in logger.iter_bar(
frame_index=np.arange(0, int(self.duration * fps))
):
# int is used to ensure that floating point errors are rounded
# down to the nearest integer
t = frame_index / fps
frame = self.get_frame(t)
if (dtype is not None) and (frame.dtype != dtype):
frame = frame.astype(dtype)
if with_times:
yield t, frame
else:
yield frame
@convert_parameter_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'. If ``t`` is a numpy
array, returns False if none of the ``t`` is in 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):
# is the whole list of t outside the clip ?
tmin, tmax = t.min(), t.max()
if (self.end is not None) and (tmin >= self.end):
return False
if tmax < self.start:
return False
# If we arrive here, a part of t falls in the clip
result = 1 * (t >= self.start)
if self.end is not None:
result *= t <= self.end
return result
else:
return (t >= self.start) and ((self.end is None) or (t < self.end))
def close(self):
"""Release any resources that are in use."""
# Implementation note for subclasses:
#
# * Memory-based resources can be left to the garbage-collector.
# * However, any open files should be closed, and subprocesses
# should be terminated.
# * Be wary that shallow copies are frequently used.
# Closing a Clip may affect its copies.
# * Therefore, should NOT be called by __del__().
pass
def __eq__(self, other):
if not isinstance(other, Clip):
return NotImplemented
# Make sure that the total number of frames is the same
self_length = self.duration * self.fps
other_length = other.duration * other.fps
if self_length != other_length:
return False
# Make sure that each frame is the same
for frame1, frame2 in zip(self.iter_frames(), other.iter_frames()):
if not np.array_equal(frame1, frame2):
return False
return True
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.subclipped(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.subclipped(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 reduce(add, (self[k] for k in key))
else:
return self.get_frame(key)
def __del__(self):
# WARNING: as stated in close() above, if we call close, it closes clips
# even if shallow copies are still in used, leading to some bugs, see:
# https://github.com/Zulko/moviepy/issues/1994
# so don't call self.close() here, rather do it manually in the code.
pass
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
from moviepy.video.fx.Loop import Loop
return self.with_effects([Loop(n)])

42
moviepy/Effect.py Normal file
View File

@@ -0,0 +1,42 @@
"""Defines the base class for all effects in MoviePy."""
import copy as _copy
from abc import ABCMeta, abstractmethod
from moviepy.Clip import Clip
class Effect(metaclass=ABCMeta):
"""Base abstract class for all effects in MoviePy.
Any new effect have to extend this base class.
"""
def copy(self):
"""Return a shallow copy of an Effect.
You must *always* copy an ``Effect`` before applying,
because some of them will modify their own attributes when applied.
For example, setting a previously unset property by using target clip property.
If we was to use the original effect, calling the same effect multiple times
could lead to different properties, and different results for equivalent clips.
By using copy, we ensure we can use the same effect object multiple times while
maintaining the same behavior/result.
In a way, copy makes the effect himself being kind of idempotent.
"""
return _copy.copy(self)
@abstractmethod
def apply(self, clip: Clip) -> Clip:
"""Apply the current effect on a clip
Parameters
----------
clip
The target clip to apply the effect on.
(Internally, MoviePy will always pass a copy of the original clip)
"""
pass

67
moviepy/__init__.py Normal file
View File

@@ -0,0 +1,67 @@
"""Imports everything that you need from the MoviePy submodules so that every thing
can be directly imported with ``from moviepy import *``.
"""
from moviepy.audio import fx as afx
from moviepy.audio.AudioClip import (
AudioArrayClip,
AudioClip,
CompositeAudioClip,
concatenate_audioclips,
)
from moviepy.audio.io.AudioFileClip import AudioFileClip
from moviepy.Effect import Effect
from moviepy.tools import convert_to_seconds
from moviepy.version import __version__
from moviepy.video import fx as vfx, tools as videotools
from moviepy.video.compositing.CompositeVideoClip import (
CompositeVideoClip,
clips_array,
concatenate_videoclips,
)
from moviepy.video.io import ffmpeg_tools
from moviepy.video.io.display_in_notebook import display_in_notebook
from moviepy.video.io.ImageSequenceClip import ImageSequenceClip
from moviepy.video.io.VideoFileClip import VideoFileClip
from moviepy.video.VideoClip import (
BitmapClip,
ColorClip,
DataVideoClip,
ImageClip,
TextClip,
UpdatedVideoClip,
VideoClip,
)
# Add display in notebook to video and audioclip
VideoClip.display_in_notebook = display_in_notebook
AudioClip.display_in_notebook = display_in_notebook
# Importing with `from moviepy import *` will only import these names
__all__ = [
"__version__",
"VideoClip",
"DataVideoClip",
"UpdatedVideoClip",
"ImageClip",
"ColorClip",
"TextClip",
"BitmapClip",
"VideoFileClip",
"CompositeVideoClip",
"clips_array",
"ImageSequenceClip",
"concatenate_videoclips",
"AudioClip",
"AudioArrayClip",
"CompositeAudioClip",
"concatenate_audioclips",
"AudioFileClip",
"Effect",
"vfx",
"afx",
"videotools",
"ffmpeg_tools",
"convert_to_seconds",
]

441
moviepy/audio/AudioClip.py Normal file
View File

@@ -0,0 +1,441 @@
"""Implements AudioClip (base class for audio clips) and its main subclasses:
- Audio clips: AudioClip, AudioFileClip, AudioArrayClip
- Composition: CompositeAudioClip
"""
import numbers
import os
import numpy as np
import proglog
from moviepy.audio.io.ffmpeg_audiowriter import ffmpeg_audiowrite
from moviepy.audio.io.ffplay_audiopreviewer import ffplay_audiopreview
from moviepy.Clip import Clip
from moviepy.decorators import convert_path_to_string, requires_duration
from moviepy.tools import extensions_dict
class AudioClip(Clip):
"""Base class for audio clips.
See ``AudioFileClip`` and ``CompositeAudioClip`` for usable classes.
An AudioClip is a Clip with a ``frame_function`` attribute of
the form `` t -> [ f_t ]`` for mono sound and
``t-> [ f1_t, f2_t ]`` for stereo sound (the arrays are Numpy arrays).
The `f_t` are floats between -1 and 1. These bounds can be
trespassed without problems (the program will put the
sound back into the bounds at conversion time, without much impact).
Parameters
----------
frame_function
A function `t-> frame at time t`. The frame does not mean much
for a sound, it is just a float. What 'makes' the sound are
the variations of that float in the time.
duration
Duration of the clip (in seconds). Some clips are infinite, in
this case their duration will be ``None``.
nchannels
Number of channels (one or two for mono or stereo).
Examples
--------
.. code:: python
# Plays the note A in mono (a sine wave of frequency 440 Hz)
import numpy as np
frame_function = lambda t: np.sin(440 * 2 * np.pi * t)
clip = AudioClip(frame_function, duration=5, fps=44100)
clip.preview()
# Plays the note A in stereo (two sine waves of frequencies 440 and 880 Hz)
frame_function = lambda t: np.array([
np.sin(440 * 2 * np.pi * t),
np.sin(880 * 2 * np.pi * t)
]).T.copy(order="C")
clip = AudioClip(frame_function, duration=3, fps=44100)
clip.preview()
"""
def __init__(self, frame_function=None, duration=None, fps=None):
super().__init__()
if fps is not None:
self.fps = fps
if frame_function is not None:
self.frame_function = frame_function
frame0 = self.get_frame(0)
if hasattr(frame0, "__iter__"):
self.nchannels = len(list(frame0))
else:
self.nchannels = 1
if duration is not None:
self.duration = duration
self.end = duration
@requires_duration
def iter_chunks(
self,
chunksize=None,
chunk_duration=None,
fps=None,
quantize=False,
nbytes=2,
logger=None,
):
"""Iterator that returns the whole sound array of the clip by chunks"""
if fps is None:
fps = self.fps
logger = proglog.default_bar_logger(logger)
if chunk_duration is not None:
chunksize = int(chunk_duration * fps)
total_size = int(fps * self.duration)
nchunks = total_size // chunksize + 1
positions = np.linspace(0, total_size, nchunks + 1, endpoint=True, dtype=int)
for i in logger.iter_bar(chunk=list(range(nchunks))):
size = positions[i + 1] - positions[i]
assert size <= chunksize
timings = (1.0 / fps) * np.arange(positions[i], positions[i + 1])
yield self.to_soundarray(
timings, nbytes=nbytes, quantize=quantize, fps=fps, buffersize=chunksize
)
@requires_duration
def to_soundarray(
self, tt=None, fps=None, quantize=False, nbytes=2, buffersize=50000
):
"""
Transforms the sound into an array that can be played by pygame
or written in a wav file. See ``AudioClip.preview``.
Parameters
----------
fps
Frame rate of the sound for the conversion.
44100 for top quality.
nbytes
Number of bytes to encode the sound: 1 for 8bit sound,
2 for 16bit, 4 for 32bit sound.
"""
if tt is None:
if fps is None:
fps = self.fps
max_duration = 1 * buffersize / fps
if self.duration > max_duration:
stacker = np.vstack if self.nchannels == 2 else np.hstack
return stacker(
tuple(
self.iter_chunks(
fps=fps, quantize=quantize, nbytes=2, chunksize=buffersize
)
)
)
else:
tt = np.arange(0, self.duration, 1.0 / fps)
"""
elif len(tt)> 1.5*buffersize:
nchunks = int(len(tt)/buffersize+1)
tt_chunks = np.array_split(tt, nchunks)
return stacker([self.to_soundarray(tt=ttc, buffersize=buffersize, fps=fps,
quantize=quantize, nbytes=nbytes)
for ttc in tt_chunks])
"""
snd_array = self.get_frame(tt)
if quantize:
snd_array = np.maximum(-0.99, np.minimum(0.99, snd_array))
inttype = {1: "int8", 2: "int16", 4: "int32"}[nbytes]
snd_array = (2 ** (8 * nbytes - 1) * snd_array).astype(inttype)
return snd_array
def max_volume(self, stereo=False, chunksize=50000, logger=None):
"""Returns the maximum volume level of the clip."""
# max volume separated by channels if ``stereo`` and not mono
stereo = stereo and self.nchannels > 1
# zero for each channel
maxi = np.zeros(self.nchannels)
for chunk in self.iter_chunks(chunksize=chunksize, logger=logger):
maxi = np.maximum(maxi, abs(chunk).max(axis=0))
# if mono returns float, otherwise array of volumes by channel
return maxi if stereo else maxi[0]
@requires_duration
@convert_path_to_string("filename")
def write_audiofile(
self,
filename,
fps=None,
nbytes=2,
buffersize=2000,
codec=None,
bitrate=None,
ffmpeg_params=None,
write_logfile=False,
logger="bar",
):
"""Writes an audio file from the AudioClip.
Parameters
----------
filename
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.
nbytes
Sample width (set to 2 for 16-bit sound, 4 for 32-bit sound)
buffersize
The sound is not generated all at once, but rather made by bunches
of frames (chunks). ``buffersize`` is the size of such a chunk.
Try varying it if you meet audio problems (but you shouldn't
have to). Default to 2000
codec
Which audio codec should be used. If None provided, the codec is
determined based on the extension of the filename. Choose
'pcm_s16le' for 16-bit wav and 'pcm_s32le' for 32-bit wav.
bitrate
Audio bitrate, given as a string like '50k', '500k', '3000k'.
Will determine the size and quality of the output file.
Note that it mainly an indicative goal, the bitrate won't
necessarily be the this in the output file.
ffmpeg_params
Any additional parameters you would like to pass, as a list
of terms, like ['-option1', 'value1', '-option2', 'value2']
write_logfile
If true, produces a detailed logfile named filename + '.log'
when writing the file
logger
Either ``"bar"`` for progress bar or ``None`` or any Proglog logger.
"""
# print(filename)
if not fps:
if not self.fps:
fps = 44100
else:
fps = self.fps
if codec is None:
name, ext = os.path.splitext(os.path.basename(filename))
try:
codec = extensions_dict[ext[1:]]["codec"][0]
except KeyError:
raise ValueError(
"MoviePy couldn't find the codec associated "
"with the filename. Provide the 'codec' "
"parameter in write_audiofile."
)
return ffmpeg_audiowrite(
self,
filename,
fps,
nbytes,
buffersize,
codec=codec,
bitrate=bitrate,
write_logfile=write_logfile,
ffmpeg_params=ffmpeg_params,
logger=logger,
)
@requires_duration
def audiopreview(
self, fps=None, buffersize=2000, nbytes=2, audio_flag=None, video_flag=None
):
"""
Preview an AudioClip using ffplay
Parameters
----------
fps
Frame rate of the sound. 44100 gives top quality, but may cause
problems if your computer is not fast enough and your clip is
complicated. If the sound jumps during the preview, lower it
(11025 is still fine, 5000 is tolerable).
buffersize
The sound is not generated all at once, but rather made by bunches
of frames (chunks). ``buffersize`` is the size of such a chunk.
Try varying it if you meet audio problems (but you shouldn't
have to).
nbytes:
Number of bytes to encode the sound: 1 for 8bit sound, 2 for
16bit, 4 for 32bit sound. 2 bytes is fine.
audio_flag, video_flag:
Instances of class threading events that are used to synchronize
video and audio during ``VideoClip.preview()``.
"""
ffplay_audiopreview(
clip=self,
fps=fps,
buffersize=buffersize,
nbytes=nbytes,
audio_flag=audio_flag,
video_flag=video_flag,
)
def __add__(self, other):
if isinstance(other, AudioClip):
return concatenate_audioclips([self, other])
return super(AudioClip, self).__add__(other)
class AudioArrayClip(AudioClip):
"""
An audio clip made from a sound array.
Parameters
----------
array
A Numpy array representing the sound, of size Nx1 for mono,
Nx2 for stereo.
fps
Frames per second : speed at which the sound is supposed to be
played.
"""
def __init__(self, array, fps):
Clip.__init__(self)
self.array = array
self.fps = fps
self.duration = 1.0 * len(array) / fps
def frame_function(t):
"""Complicated, but must be able to handle the case where t
is a list of the form sin(t).
"""
if isinstance(t, np.ndarray):
array_inds = np.round(self.fps * t).astype(int)
in_array = (array_inds >= 0) & (array_inds < len(self.array))
result = np.zeros((len(t), 2))
result[in_array] = self.array[array_inds[in_array]]
return result
else:
i = int(self.fps * t)
if i < 0 or i >= len(self.array):
return 0 * self.array[0]
else:
return self.array[i]
self.frame_function = frame_function
self.nchannels = len(list(self.get_frame(0)))
class CompositeAudioClip(AudioClip):
"""Clip made by composing several AudioClips.
An audio clip made by putting together several audio clips.
Parameters
----------
clips
List of audio clips, which may start playing at different times or
together, depends on their ``start`` attributes. If all have their
``duration`` attribute set, the duration of the composite clip is
computed automatically.
"""
def __init__(self, clips):
self.clips = clips
self.nchannels = max(clip.nchannels for clip in self.clips)
# self.duration is set at AudioClip
duration = None
for end in self.ends:
if end is None:
break
duration = max(end, duration or 0)
# self.fps is set at AudioClip
fps = None
for clip in self.clips:
if hasattr(clip, "fps") and isinstance(clip.fps, numbers.Number):
fps = max(clip.fps, fps or 0)
super().__init__(duration=duration, fps=fps)
@property
def starts(self):
"""Returns starting times for all clips in the composition."""
return (clip.start for clip in self.clips)
@property
def ends(self):
"""Returns ending times for all clips in the composition."""
return (clip.end for clip in self.clips)
def frame_function(self, t):
"""Renders a frame for the composition for the time ``t``."""
played_parts = [clip.is_playing(t) for clip in self.clips]
sounds = [
clip.get_frame(t - clip.start) * np.array([part]).T
for clip, part in zip(self.clips, played_parts)
if (part is not False)
]
if isinstance(t, np.ndarray):
zero = np.zeros((len(t), self.nchannels))
else:
zero = np.zeros(self.nchannels)
return zero + sum(sounds)
def concatenate_audioclips(clips):
"""Concatenates one AudioClip after another, in the order that are passed
to ``clips`` parameter.
Parameters
----------
clips
List of audio clips, which will be played one after other.
"""
# start, end/start2, end2/start3... end
starts_end = np.cumsum([0, *[clip.duration for clip in clips]])
newclips = [clip.with_start(t) for clip, t in zip(clips, starts_end[:-1])]
return CompositeAudioClip(newclips).with_duration(starts_end[-1])

View File

@@ -0,0 +1 @@
"""Everything about audio manipulation."""

View File

@@ -0,0 +1,70 @@
from dataclasses import dataclass
import numpy as np
from moviepy.audio.AudioClip import CompositeAudioClip
from moviepy.audio.fx.MultiplyVolume import MultiplyVolume
from moviepy.Clip import Clip
from moviepy.decorators import audio_video_effect
from moviepy.Effect import Effect
@dataclass
class AudioDelay(Effect):
"""Repeats audio certain number of times at constant intervals multiplying
their volume levels using a linear space in the range 1 to ``decay`` argument
value.
Parameters
----------
offset : float, optional
Gap between repetitions start times, in seconds.
n_repeats : int, optional
Number of repetitions (without including the clip itself).
decay : float, optional
Multiplication factor for the volume level of the last repetition. Each
repetition will have a value in the linear function between 1 and this value,
increasing or decreasing constantly. Keep in mind that the last repetition
will be muted if this is 0, and if is greater than 1, the volume will increase
for each repetition.
Examples
--------
.. code:: python
from moviepy import *
videoclip = AudioFileClip('myaudio.wav').with_effects([
afx.AudioDelay(offset=.2, n_repeats=10, decayment=.2)
])
# stereo A note
frame_function = lambda t: np.array(
[np.sin(440 * 2 * np.pi * t), np.sin(880 * 2 * np.pi * t)]
).T
clip = AudioClip(frame_function=frame_function, duration=0.1, fps=44100)
clip = clip.with_effects([afx.AudioDelay(offset=.2, n_repeats=11, decay=0)])
"""
offset: float = 0.2
n_repeats: int = 8
decay: float = 1
@audio_video_effect
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
decayments = np.linspace(1, max(0, self.decay), self.n_repeats + 1)
return CompositeAudioClip(
[
clip.copy(),
*[
clip.with_start((rep + 1) * self.offset).with_effects(
[MultiplyVolume(decayments[rep + 1])]
)
for rep in range(self.n_repeats)
],
]
)

View File

@@ -0,0 +1,60 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Clip import Clip
from moviepy.decorators import audio_video_effect
from moviepy.Effect import Effect
from moviepy.tools import convert_to_seconds
@dataclass
class AudioFadeIn(Effect):
"""Return an audio (or video) clip that is first mute, then the
sound arrives progressively over ``duration`` seconds.
Parameters
----------
duration : float
How long does it take for the sound to return to its normal level.
Examples
--------
.. code:: python
clip = VideoFileClip("media/chaplin.mp4")
clip.with_effects([afx.AudioFadeIn("00:00:06")])
"""
duration: float
def __post_init__(self):
self.duration = convert_to_seconds(self.duration)
def _mono_factor_getter(self):
return lambda t, duration: np.minimum(t / duration, 1)
def _stereo_factor_getter(self, nchannels):
def getter(t, duration):
factor = np.minimum(t / duration, 1)
return np.array([factor for _ in range(nchannels)]).T
return getter
@audio_video_effect
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
get_factor = (
self._mono_factor_getter()
if clip.nchannels == 1
else self._stereo_factor_getter(clip.nchannels)
)
return clip.transform(
lambda get_frame, t: get_factor(t, self.duration) * get_frame(t),
)

View File

@@ -0,0 +1,62 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Clip import Clip
from moviepy.decorators import audio_video_effect
from moviepy.Effect import Effect
from moviepy.tools import convert_to_seconds
@dataclass
class AudioFadeOut(Effect):
"""Return a sound clip where the sound fades out progressively
over ``duration`` seconds at the end of the clip.
Parameters
----------
duration : float
How long does it take for the sound to reach the zero level at the end
of the clip.
Examples
--------
.. code:: python
clip = VideoFileClip("media/chaplin.mp4")
clip.with_effects([afx.AudioFadeOut("00:00:06")])
"""
duration: float
def __post_init__(self):
self.duration = convert_to_seconds(self.duration)
def _mono_factor_getter(self, clip_duration):
return lambda t, duration: np.minimum(1.0 * (clip_duration - t) / duration, 1)
def _stereo_factor_getter(self, clip_duration, nchannels):
def getter(t, duration):
factor = np.minimum(1.0 * (clip_duration - t) / duration, 1)
return np.array([factor for _ in range(nchannels)]).T
return getter
@audio_video_effect
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
get_factor = (
self._mono_factor_getter(clip.duration)
if clip.nchannels == 1
else self._stereo_factor_getter(clip.duration, clip.nchannels)
)
return clip.transform(
lambda get_frame, t: get_factor(t, self.duration) * get_frame(t),
keep_duration=True,
)

View File

@@ -0,0 +1,41 @@
from dataclasses import dataclass
from moviepy.audio.AudioClip import concatenate_audioclips
from moviepy.Clip import Clip
from moviepy.decorators import audio_video_effect
from moviepy.Effect import Effect
@dataclass
class AudioLoop(Effect):
"""Loops over an audio clip.
Returns an audio clip that plays the given clip either
`n_loops` times, or during `duration` seconds.
Examples
--------
.. code:: python
from moviepy import *
videoclip = VideoFileClip('myvideo.mp4')
music = AudioFileClip('music.ogg')
audio = music.with_effects([afx.AudioLoop(duration=videoclip.duration)])
videoclip.with_audio(audio)
"""
n_loops: int = None
duration: float = None
@audio_video_effect
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if self.duration is not None:
self.n_loops = int(self.duration / clip.duration) + 1
return concatenate_audioclips(self.n_loops * [clip]).with_duration(
self.duration
)
return concatenate_audioclips(self.n_loops * [clip])

View File

@@ -0,0 +1,31 @@
from dataclasses import dataclass
from moviepy.audio.fx.MultiplyVolume import MultiplyVolume
from moviepy.Clip import Clip
from moviepy.decorators import audio_video_effect
from moviepy.Effect import Effect
@dataclass
class AudioNormalize(Effect):
"""Return a clip whose volume is normalized to 0db.
Return an audio (or video) clip whose audio volume is normalized
so that the maximum volume is at 0db, the maximum achievable volume.
Examples
--------
>>> from moviepy import *
>>> videoclip = VideoFileClip('myvideo.mp4').with_effects([afx.AudioNormalize()])
"""
@audio_video_effect
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
max_volume = clip.max_volume()
if max_volume == 0:
return clip
else:
return clip.with_effects([MultiplyVolume(1 / max_volume)])

View File

@@ -0,0 +1,44 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.decorators import audio_video_effect
from moviepy.Effect import Effect
@dataclass
class MultiplyStereoVolume(Effect):
"""For a stereo audioclip, this function enables to change the volume
of the left and right channel separately (with the factors `left`
and `right`). Makes a stereo audio clip in which the volume of left
and right is controllable.
Examples
--------
.. code:: python
from moviepy import AudioFileClip
music = AudioFileClip('music.ogg')
# mutes left channel
audio_r = music.with_effects([afx.MultiplyStereoVolume(left=0, right=1)])
# halves audio volume
audio_h = music.with_effects([afx.MultiplyStereoVolume(left=0.5, right=0.5)])
"""
left: float = 1
right: float = 1
@audio_video_effect
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
def stereo_volume(get_frame, t):
frame = get_frame(t)
if len(frame) == 1: # mono
frame *= self.left if self.left is not None else self.right
else: # stereo, stereo surround...
for i in range(len(frame[0])): # odd channels are left
frame[:, i] *= self.left if i % 2 == 0 else self.right
return frame
return clip.transform(stereo_volume)

View File

@@ -0,0 +1,90 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Clip import Clip
from moviepy.decorators import audio_video_effect
from moviepy.Effect import Effect
from moviepy.tools import convert_to_seconds
@dataclass
class MultiplyVolume(Effect):
"""Returns a clip with audio volume multiplied by the
value `factor`. Can be applied to both audio and video clips.
Parameters
----------
factor : float
Volume multiplication factor.
start_time : float, optional
Time from the beginning of the clip until the volume transformation
begins to take effect, in seconds. By default at the beginning.
end_time : float, optional
Time from the beginning of the clip until the volume transformation
ends to take effect, in seconds. By default at the end.
Examples
--------
.. code:: python
from moviepy import AudioFileClip
music = AudioFileClip("music.ogg")
# doubles audio volume
doubled_audio_clip = music.with_effects([afx.MultiplyVolume(2)])
# halves audio volume
half_audio_clip = music.with_effects([afx.MultiplyVolume(0.5)])
# silences clip during one second at third
effect = afx.MultiplyVolume(0, start_time=2, end_time=3)
silenced_clip = clip.with_effects([effect])
"""
factor: float
start_time: float = None
end_time: float = None
def __post_init__(self):
if self.start_time is not None:
self.start_time = convert_to_seconds(self.start_time)
if self.end_time is not None:
self.end_time = convert_to_seconds(self.end_time)
def _multiply_volume_in_range(self, factor, start_time, end_time, nchannels):
def factors_filter(factor, t):
return np.array([factor if start_time <= t_ <= end_time else 1 for t_ in t])
def multiply_stereo_volume(get_frame, t):
return np.multiply(
get_frame(t),
np.array([factors_filter(factor, t) for _ in range(nchannels)]).T,
)
def multiply_mono_volume(get_frame, t):
return np.multiply(get_frame(t), factors_filter(factor, t))
return multiply_mono_volume if nchannels == 1 else multiply_stereo_volume
@audio_video_effect
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if self.start_time is None and self.end_time is None:
return clip.transform(
lambda get_frame, t: self.factor * get_frame(t),
keep_duration=True,
)
return clip.transform(
self._multiply_volume_in_range(
self.factor,
clip.start if self.start_time is None else self.start_time,
clip.end if self.end_time is None else self.end_time,
clip.nchannels,
),
keep_duration=True,
)

View File

@@ -0,0 +1,22 @@
"""All the audio effects that can be applied to AudioClip and VideoClip."""
# import every video fx function
from moviepy.audio.fx.AudioDelay import AudioDelay
from moviepy.audio.fx.AudioFadeIn import AudioFadeIn
from moviepy.audio.fx.AudioFadeOut import AudioFadeOut
from moviepy.audio.fx.AudioLoop import AudioLoop
from moviepy.audio.fx.AudioNormalize import AudioNormalize
from moviepy.audio.fx.MultiplyStereoVolume import MultiplyStereoVolume
from moviepy.audio.fx.MultiplyVolume import MultiplyVolume
__all__ = (
"AudioDelay",
"AudioFadeIn",
"AudioFadeOut",
"AudioLoop",
"AudioNormalize",
"MultiplyStereoVolume",
"MultiplyVolume",
)

View File

@@ -0,0 +1,85 @@
"""Implements AudioFileClip, a class for audio clips creation using audio files."""
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):
"""
An audio clip read from a sound file, or an array.
The whole file is not loaded in memory. Instead, only a portion is
read and stored in memory. this portion includes frames before
and after the last frames read, so that it is fast to read the sound
backward and forward.
Parameters
----------
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.
buffersize:
Size to load in memory (in number of frames)
Attributes
----------
nbytes
Number of bits per frame of the original audio file.
fps
Number of frames per second in the audio file
buffersize
See Parameters.
Lifetime
--------
Note that this creates subprocesses and locks files. If you construct one
of these instances, you must call close() afterwards, or the subresources
will not be cleaned up until the process ends.
Examples
--------
.. code:: python
snd = AudioFileClip("song.wav")
snd.close()
"""
@convert_path_to_string("filename")
def __init__(
self, filename, decode_file=False, buffersize=200000, nbytes=2, fps=44100
):
AudioClip.__init__(self)
self.filename = filename
self.reader = FFMPEG_AudioReader(
filename,
decode_file=decode_file,
fps=fps,
nbytes=nbytes,
buffersize=buffersize,
)
self.fps = fps
self.duration = self.reader.duration
self.end = self.reader.duration
self.buffersize = self.reader.buffersize
self.filename = filename
self.frame_function = lambda t: self.reader.get_frame(t)
self.nchannels = self.reader.nchannels
def close(self):
"""Close the internal reader."""
if self.reader:
self.reader.close()
self.reader = None

View File

@@ -0,0 +1 @@
"""Class and methods to read, write, preview audiofiles."""

View File

@@ -0,0 +1,240 @@
"""MoviePy audio writing with ffmpeg."""
import subprocess as sp
from log import log_step
import proglog
from moviepy.config import FFMPEG_BINARY
from moviepy.decorators import requires_duration
from moviepy.tools import cross_platform_popen_params, ffmpeg_escape_filename
class FFMPEG_AudioWriter:
"""
A class to write an AudioClip into an audio file.
Parameters
----------
filename
Name of any video or audio file, like ``video.mp4`` or ``sound.wav`` etc.
size
Size (width,height) in pixels of the output video.
fps_input
Frames per second of the input audio (given by the AudioClip being
written down).
nbytes : int, optional
Number of bytes per sample. Default is 2 (16-bit audio).
nchannels : int, optional
Number of audio channels. Default is 2 (stereo).
codec : str, optional
The codec to use for the output. Default is ``libfdk_aac``.
bitrate:
A string indicating the bitrate of the final video. Only
relevant for codecs which accept a bitrate.
input_video : str, optional
Path to an input video file. If provided, the audio will be muxed with this video.
If not provided, the output will be audio-only.
logfile : file-like object or None, optional
A file object where FFMPEG logs will be written. If None, logs are suppressed.
ffmpeg_params : list of str, optional
Additional FFMPEG command-line parameters to customize the output.
"""
def __init__(
self,
filename,
fps_input,
nbytes=2,
nchannels=2,
codec="libfdk_aac",
bitrate=None,
input_video=None,
logfile=None,
ffmpeg_params=None,
):
if logfile is None:
logfile = sp.PIPE
self.logfile = logfile
self.filename = filename
self.codec = codec
self.ext = self.filename.split(".")[-1]
# order is important
cmd = [
FFMPEG_BINARY,
"-y",
"-loglevel",
"error" if logfile == sp.PIPE else "info",
"-f",
"s%dle" % (8 * nbytes),
"-acodec",
"pcm_s%dle" % (8 * nbytes),
"-ar",
"%d" % fps_input,
"-ac",
"%d" % nchannels,
"-i",
"-",
]
if input_video is None:
cmd.extend(["-vn"])
else:
cmd.extend(["-i", ffmpeg_escape_filename(input_video), "-vcodec", "copy"])
cmd.extend(["-acodec", codec] + ["-ar", "%d" % fps_input])
cmd.extend(["-strict", "-2"]) # needed to support codec 'aac'
if bitrate is not None:
cmd.extend(["-ab", bitrate])
if ffmpeg_params is not None:
cmd.extend(ffmpeg_params)
cmd.extend([ffmpeg_escape_filename(filename)])
popen_params = cross_platform_popen_params(
{"stdout": sp.DEVNULL, "stderr": logfile, "stdin": sp.PIPE}
)
self.proc = sp.Popen(cmd, **popen_params)
def write_frames(self, frames_array):
"""Send the audio frame (a chunck of ``AudioClip``) to ffmpeg for writting"""
try:
self.proc.stdin.write(frames_array.tobytes())
except IOError as err:
_, ffmpeg_error = self.proc.communicate()
if ffmpeg_error is not None:
ffmpeg_error = ffmpeg_error.decode()
else:
# The error was redirected to a logfile with `write_logfile=True`,
# so read the error from that file instead
self.logfile.seek(0)
ffmpeg_error = self.logfile.read()
error = (
f"{err}\n\nMoviePy error: FFMPEG encountered the following error while "
f"writing file {self.filename}:\n\n {ffmpeg_error}"
)
if "Unknown encoder" in ffmpeg_error:
error += (
"\n\nThe audio export failed because FFMPEG didn't find the "
f"specified codec for audio encoding {self.codec}. "
"Please install this codec or change the codec when calling "
"write_videofile or write_audiofile.\nFor instance for mp3:\n"
" >>> write_videofile('myvid.mp4', audio_codec='libmp3lame')"
)
elif "incorrect codec parameters ?" in ffmpeg_error:
error += (
"\n\nThe audio export failed, possibly because the "
f"codec specified for the video {self.codec} is not compatible"
f" with the given extension {self.ext}. Please specify a "
"valid 'codec' argument in write_audiofile or 'audio_codoc'"
"argument in write_videofile. This would be "
"'libmp3lame' for mp3, 'libvorbis' for ogg..."
)
elif "bitrate not specified" in ffmpeg_error:
error += (
"\n\nThe audio export failed, possibly because the "
"bitrate you specified was too high or too low for "
"the audio codec."
)
elif "Invalid encoder type" in ffmpeg_error:
error += (
"\n\nThe audio export failed because the codec "
"or file extension you provided is not suitable for audio"
)
raise IOError(error)
def close(self):
"""Closes the writer, terminating the subprocess if is still alive."""
if hasattr(self, "proc") and self.proc:
self.proc.stdin.close()
self.proc.stdin = None
if self.proc.stderr is not None:
self.proc.stderr.close()
self.proc.stderr = None
# If this causes deadlocks, consider terminating instead.
self.proc.wait()
self.proc = None
def __del__(self):
# If the garbage collector comes, make sure the subprocess is terminated.
self.close()
# Support the Context Manager protocol, to ensure that resources are cleaned up.
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
@requires_duration
def ffmpeg_audiowrite(
clip,
filename,
fps,
nbytes,
buffersize,
codec="libvorbis",
bitrate=None,
write_logfile=False,
ffmpeg_params=None,
logger=None,
):
"""
A function that wraps the FFMPEG_AudioWriter to write an AudioClip
to a file.
"""
if write_logfile:
logfile = open(filename + ".log", "w+")
else:
logfile = None
# logger = proglog.default_bar_logger(logger)
# logger(message="MoviePy - Writing audio in %s" % filename)
writer = FFMPEG_AudioWriter(
filename,
fps,
nbytes,
clip.nchannels,
codec=codec,
bitrate=bitrate,
logfile=logfile,
ffmpeg_params=ffmpeg_params,
)
total_chunks = len(list(clip.iter_chunks(chunksize=buffersize, quantize=True, nbytes=nbytes, fps=fps)))
old_progress = -1
for i, chunk in enumerate(clip.iter_chunks(chunksize=buffersize, quantize=True, nbytes=nbytes, fps=fps)):
# Calculate the progress
progress = (i + 1) / total_chunks * 100
int_progress = int(progress)
# Display the progress if it has changed from the last time
if int_progress != old_progress:
old_progress = int_progress
log_step("audio_extraction", old_progress, "extracting the audio from the video")
# Écrire le chunk audio
writer.write_frames(chunk)
writer.close()
if write_logfile:
logfile.close()
# logger(message="MoviePy - Done.")

View File

@@ -0,0 +1,154 @@
"""MoviePy audio writing with ffmpeg."""
import subprocess as sp
from moviepy.config import FFPLAY_BINARY
from moviepy.decorators import requires_duration
from moviepy.tools import cross_platform_popen_params
class FFPLAY_AudioPreviewer:
"""
A class to preview an AudioClip.
Parameters
----------
fps_input
Frames per second of the input audio (given by the AudioClip being
written down).
nbytes:
Number of bytes to encode the sound: 1 for 8bit sound, 2 for
16bit, 4 for 32bit sound. Default is 2 bytes, it's fine.
nchannels:
Number of audio channels in the clip. Default to 2 channels.
"""
def __init__(
self,
fps_input,
nbytes=2,
nchannels=2,
):
# order is important
cmd = [
FFPLAY_BINARY,
"-autoexit", # If you don't precise, ffplay won't stop at end
"-nodisp", # If you don't precise a window is
"-f",
"s%dle" % (8 * nbytes),
"-ar",
"%d" % fps_input,
"-ac",
"%d" % nchannels,
"-i",
"-",
]
popen_params = cross_platform_popen_params(
{"stdout": sp.DEVNULL, "stderr": sp.STDOUT, "stdin": sp.PIPE}
)
self.proc = sp.Popen(cmd, **popen_params)
def write_frames(self, frames_array):
"""Send a raw audio frame (a chunck of audio) to ffplay to be played"""
try:
self.proc.stdin.write(frames_array.tobytes())
except IOError as err:
_, ffplay_error = self.proc.communicate()
if ffplay_error is not None:
ffplay_error = ffplay_error.decode()
else:
# The error was redirected to a logfile with `write_logfile=True`,
# so read the error from that file instead
self.logfile.seek(0)
ffplay_error = self.logfile.read()
error = (
f"{err}\n\nMoviePy error: FFPLAY encountered the following error while "
f":\n\n {ffplay_error}"
)
raise IOError(error)
def close(self):
"""Closes the writer, terminating the subprocess if is still alive."""
if hasattr(self, "proc") and self.proc:
self.proc.stdin.close()
self.proc.stdin = None
if self.proc.stderr is not None:
self.proc.stderr.close()
self.proc.stderr = None
# If this causes deadlocks, consider terminating instead.
self.proc.wait()
self.proc = None
def __del__(self):
# If the garbage collector comes, make sure the subprocess is terminated.
self.close()
# Support the Context Manager protocol, to ensure that resources are cleaned up.
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
@requires_duration
def ffplay_audiopreview(
clip, fps=None, buffersize=2000, nbytes=2, audio_flag=None, video_flag=None
):
"""
A function that wraps the FFPLAY_AudioPreviewer to preview an AudioClip
Parameters
----------
fps
Frame rate of the sound. 44100 gives top quality, but may cause
problems if your computer is not fast enough and your clip is
complicated. If the sound jumps during the preview, lower it
(11025 is still fine, 5000 is tolerable).
buffersize
The sound is not generated all at once, but rather made by bunches
of frames (chunks). ``buffersize`` is the size of such a chunk.
Try varying it if you meet audio problems (but you shouldn't
have to).
nbytes:
Number of bytes to encode the sound: 1 for 8bit sound, 2 for
16bit, 4 for 32bit sound. 2 bytes is fine.
audio_flag, video_flag:
Instances of class threading events that are used to synchronize
video and audio during ``VideoClip.preview()``.
"""
if not fps:
if not clip.fps:
fps = 44100
else:
fps = clip.fps
with FFPLAY_AudioPreviewer(fps, nbytes, clip.nchannels) as previewer:
first_frame = True
for chunk in clip.iter_chunks(
chunksize=buffersize, quantize=True, nbytes=nbytes, fps=fps
):
# On first frame, wait for video
if first_frame:
first_frame = False
if audio_flag is not None:
audio_flag.set() # Say to video that audio is ready
if video_flag is not None:
video_flag.wait() # Wait for video to be ready
previewer.write_frames(chunk)

304
moviepy/audio/io/readers.py Normal file
View File

@@ -0,0 +1,304 @@
"""MoviePy audio reading with ffmpeg."""
import subprocess as sp
import warnings
import numpy as np
from moviepy.config import FFMPEG_BINARY
from moviepy.tools import cross_platform_popen_params, ffmpeg_escape_filename
from moviepy.video.io.ffmpeg_reader import ffmpeg_parse_infos
class FFMPEG_AudioReader:
"""A class to read the audio in either video files or audio files
using ffmpeg. ffmpeg will read any audio and transform them into
raw data.
Parameters
----------
filename
Name of any video or audio file, like ``video.mp4`` or
``sound.wav`` etc.
buffersize
The size of the buffer to use. Should be bigger than the buffer
used by ``write_audiofile``
print_infos
Print the ffmpeg infos on the file being read (for debugging)
fps
Desired frames per second in the decoded signal that will be
received from ffmpeg
nbytes
Desired number of bytes (1,2,4) in the signal that will be
received from ffmpeg
"""
def __init__(
self,
filename,
buffersize,
decode_file=False,
print_infos=False,
fps=44100,
nbytes=2,
nchannels=2,
):
# TODO bring FFMPEG_AudioReader more in line with FFMPEG_VideoReader
# E.g. here self.pos is still 1-indexed.
# (or have them inherit from a shared parent class)
self.filename = filename
self.nbytes = nbytes
self.fps = fps
self.format = "s%dle" % (8 * nbytes)
self.codec = "pcm_s%dle" % (8 * nbytes)
self.nchannels = nchannels
infos = ffmpeg_parse_infos(filename, decode_file=decode_file)
self.duration = infos["duration"]
self.bitrate = infos["audio_bitrate"]
self.infos = infos
self.proc = None
self.n_frames = int(self.fps * self.duration)
self.buffersize = min(self.n_frames + 1, buffersize)
self.buffer = None
self.buffer_startframe = 1
self.initialize()
self.buffer_around(1)
def initialize(self, start_time=0):
"""Opens the file, creates the pipe."""
self.close() # if any
if start_time != 0:
offset = min(1, start_time)
i_arg = [
"-ss",
"%.05f" % (start_time - offset),
"-i",
ffmpeg_escape_filename(self.filename),
"-vn",
"-ss",
"%.05f" % offset,
]
else:
i_arg = ["-i", ffmpeg_escape_filename(self.filename), "-vn"]
cmd = (
[FFMPEG_BINARY]
+ i_arg
+ [
"-loglevel",
"error",
"-f",
self.format,
"-acodec",
self.codec,
"-ar",
"%d" % self.fps,
"-ac",
"%d" % self.nchannels,
"-",
]
)
popen_params = cross_platform_popen_params(
{
"bufsize": self.buffersize,
"stdout": sp.PIPE,
"stderr": sp.PIPE,
"stdin": sp.DEVNULL,
}
)
self.proc = sp.Popen(cmd, **popen_params)
self.pos = np.round(self.fps * start_time)
def skip_chunk(self, chunksize):
"""Skip a chunk of audio data by reading and discarding the specified number of
frames from the audio stream. The audio stream is read from the `proc` stdout.
After skipping the chunk, the `pos` attribute is updated accordingly.
Parameters
----------
chunksize (int):
The number of audio frames to skip.
"""
_ = self.proc.stdout.read(self.nchannels * chunksize * self.nbytes)
self.proc.stdout.flush()
self.pos = self.pos + chunksize
def read_chunk(self, chunksize):
"""Read a chunk of audio data from the audio stream.
This method reads a chunk of audio data from the audio stream. The
specified number of frames, given by `chunksize`, is read from the
`proc` stdout. The audio data is returned as a NumPy array, where
each row corresponds to a frame and each column corresponds to a
channel. If there is not enough audio left to read, the remaining
portion is padded with zeros, ensuring that the returned array has
the desired length. The `pos` attribute is updated accordingly.
Parameters
----------
chunksize (float):
The desired number of audio frames to read.
"""
# chunksize is not being autoconverted from float to int
chunksize = int(round(chunksize))
s = self.proc.stdout.read(self.nchannels * chunksize * self.nbytes)
data_type = {1: "int8", 2: "int16", 4: "int32"}[self.nbytes]
if hasattr(np, "frombuffer"):
result = np.frombuffer(s, dtype=data_type)
else:
result = np.fromstring(s, dtype=data_type)
result = (1.0 * result / 2 ** (8 * self.nbytes - 1)).reshape(
(int(len(result) / self.nchannels), self.nchannels)
)
# Pad the read chunk with zeros when there isn't enough audio
# left to read, so the buffer is always at full length.
pad = np.zeros((chunksize - len(result), self.nchannels), dtype=result.dtype)
result = np.concatenate([result, pad])
# self.proc.stdout.flush()
self.pos = self.pos + chunksize
return result
def seek(self, pos):
"""Read a frame at time t. Note for coders: getting an arbitrary
frame in the video with ffmpeg can be painfully slow if some
decoding has to be done. This function tries to avoid fectching
arbitrary frames whenever possible, by moving between adjacent
frames.
"""
if (pos < self.pos) or (pos > (self.pos + 1000000)):
t = 1.0 * pos / self.fps
self.initialize(t)
elif pos > self.pos:
self.skip_chunk(pos - self.pos)
# last case standing: pos = current pos
self.pos = pos
def get_frame(self, tt):
"""Retrieve the audio frame(s) corresponding to the given timestamp(s).
Parameters
----------
tt (float or numpy.ndarray):
The timestamp(s) at which to retrieve the audio frame(s).
If `tt` is a single float value, the frame corresponding to that
timestamp is returned. If `tt` is a NumPy array of timestamps, an
array of frames corresponding to each timestamp is returned.
"""
if isinstance(tt, np.ndarray):
# lazy implementation, but should not cause problems in
# 99.99 % of the cases
# elements of t that are actually in the range of the
# audio file.
in_time = (tt >= 0) & (tt < self.duration)
# Check that the requested time is in the valid range
if not in_time.any():
raise IOError(
"Error in file %s, " % (self.filename)
+ "Accessing time t=%.02f-%.02f seconds, " % (tt[0], tt[-1])
+ "with clip duration=%f seconds, " % self.duration
)
# The np.round in the next line is super-important.
# Removing it results in artifacts in the noise.
frames = np.round((self.fps * tt)).astype(int)[in_time]
fr_min, fr_max = frames.min(), frames.max()
# if min and max frames don't fit the buffer, it results in IndexError
# we avoid that by recursively calling this function on smaller length
# and concatenate the results:w
max_frame_threshold = fr_min + self.buffersize // 2
threshold_idx = np.searchsorted(frames, max_frame_threshold, side="right")
if threshold_idx != len(frames):
in_time_head = in_time[0:threshold_idx]
in_time_tail = in_time[threshold_idx:]
return np.concatenate(
[self.get_frame(in_time_head), self.get_frame(in_time_tail)]
)
if not (0 <= (fr_min - self.buffer_startframe) < len(self.buffer)):
self.buffer_around(fr_min)
elif not (0 <= (fr_max - self.buffer_startframe) < len(self.buffer)):
self.buffer_around(fr_max)
try:
result = np.zeros((len(tt), self.nchannels))
indices = frames - self.buffer_startframe
result[in_time] = self.buffer[indices]
return result
except IndexError as error:
warnings.warn(
"Error in file %s, " % (self.filename)
+ "At time t=%.02f-%.02f seconds, " % (tt[0], tt[-1])
+ "indices wanted: %d-%d, " % (indices.min(), indices.max())
+ "but len(buffer)=%d\n" % (len(self.buffer))
+ str(error),
UserWarning,
)
# repeat the last frame instead
indices[indices >= len(self.buffer)] = len(self.buffer) - 1
result[in_time] = self.buffer[indices]
return result
else:
ind = int(self.fps * tt)
if ind < 0 or ind > self.n_frames: # out of time: return 0
return np.zeros(self.nchannels)
if not (0 <= (ind - self.buffer_startframe) < len(self.buffer)):
# out of the buffer: recenter the buffer
self.buffer_around(ind)
# read the frame in the buffer
return self.buffer[ind - self.buffer_startframe]
def buffer_around(self, frame_number):
"""Fill the buffer with frames, centered on frame_number if possible."""
# start-frame for the buffer
new_bufferstart = max(0, frame_number - self.buffersize // 2)
if self.buffer is not None:
current_f_end = self.buffer_startframe + self.buffersize
if new_bufferstart < current_f_end < new_bufferstart + self.buffersize:
# We already have part of what must be read
conserved = current_f_end - new_bufferstart
chunksize = self.buffersize - conserved
array = self.read_chunk(chunksize)
self.buffer = np.vstack([self.buffer[-conserved:], array])
else:
self.seek(new_bufferstart)
self.buffer = self.read_chunk(self.buffersize)
else:
self.seek(new_bufferstart)
self.buffer = self.read_chunk(self.buffersize)
self.buffer_startframe = new_bufferstart
def close(self):
"""Closes the reader, terminating the subprocess if is still alive."""
if self.proc:
if self.proc.poll() is None:
self.proc.terminate()
self.proc.stdout.close()
self.proc.stderr.close()
self.proc.wait()
self.proc = None
def __del__(self):
# If the garbage collector comes, make sure the subprocess is terminated.
self.close()

View File

@@ -0,0 +1 @@
"""Tools to better processing and edition of audio."""

View File

@@ -0,0 +1,29 @@
"""Cutting utilities working with audio."""
import numpy as np
def find_audio_period(clip, min_time=0.1, max_time=2, time_resolution=0.01):
"""Finds the period, in seconds of an audioclip.
Parameters
----------
min_time : float, optional
Minimum bound for the returned value.
max_time : float, optional
Maximum bound for the returned value.
time_resolution : float, optional
Numerical precision.
"""
chunksize = int(time_resolution * clip.fps)
chunk_duration = 1.0 * chunksize / clip.fps
# v denotes the list of volumes
v = np.array([(chunk**2).sum() for chunk in clip.iter_chunks(chunksize)])
v = v - v.mean()
corrs = np.correlate(v, v, mode="full")[-len(v) :]
corrs[: int(min_time / chunk_duration)] = 0
corrs[int(max_time / chunk_duration) :] = 0
return chunk_duration * np.argmax(corrs)

91
moviepy/config.py Normal file
View File

@@ -0,0 +1,91 @@
"""Third party programs configuration for MoviePy."""
import os
import subprocess as sp
from pathlib import Path
from log import log_step
from moviepy.tools import cross_platform_popen_params
try:
from dotenv import find_dotenv, load_dotenv
DOTENV = find_dotenv()
load_dotenv(DOTENV)
except ImportError:
DOTENV = None
FFMPEG_BINARY = os.getenv("FFMPEG_BINARY", "ffmpeg-imageio")
FFPLAY_BINARY = os.getenv("FFPLAY_BINARY", "auto-detect")
IS_POSIX_OS = os.name == "posix"
def try_cmd(cmd):
"""Verify if the OS support command invocation as expected by moviepy"""
try:
popen_params = cross_platform_popen_params(
{"stdout": sp.PIPE, "stderr": sp.PIPE, "stdin": sp.DEVNULL}
)
proc = sp.Popen(cmd, **popen_params)
proc.communicate()
except Exception as err:
return False, err
else:
return True, None
if FFMPEG_BINARY == "ffmpeg-imageio":
from imageio.plugins.ffmpeg import get_exe
FFMPEG_BINARY = get_exe()
elif FFMPEG_BINARY == "auto-detect":
if try_cmd(["ffmpeg"])[0]:
FFMPEG_BINARY = "ffmpeg"
elif not IS_POSIX_OS and try_cmd(["ffmpeg.exe"])[0]:
FFMPEG_BINARY = "ffmpeg.exe"
else: # pragma: no cover
FFMPEG_BINARY = "unset"
else:
success, err = try_cmd([FFMPEG_BINARY])
if not success:
raise IOError(
f"{err} - The path specified for the ffmpeg binary might be wrong"
)
if FFPLAY_BINARY == "auto-detect":
if try_cmd(["ffplay"])[0]:
FFPLAY_BINARY = "ffplay"
elif not IS_POSIX_OS and try_cmd(["ffplay.exe"])[0]:
FFPLAY_BINARY = "ffplay.exe"
else: # pragma: no cover
FFPLAY_BINARY = "unset"
else:
success, err = try_cmd([FFPLAY_BINARY])
if not success:
raise IOError(
f"{err} - The path specified for the ffmpeg binary might be wrong"
)
def check():
"""Check if moviepy has found the binaries for FFmpeg."""
if try_cmd([FFMPEG_BINARY])[0]:
log_step("info", 100, f"MoviePy: ffmpeg successfully found in '{FFMPEG_BINARY}'.")
else: # pragma: no cover
raise Exception(f"MoviePy: can't find or access ffmpeg in '{FFMPEG_BINARY}'.")
if try_cmd([FFPLAY_BINARY])[0]:
log_step("info", 100, f"MoviePy: ffmpeg successfully found in '{FFPLAY_BINARY}'.")
else: # pragma: no cover
raise Exception(f"MoviePy: can't find or access ffmpeg in '{FFPLAY_BINARY}'.")
# if DOTENV:
# print(f"\n.env file content at {DOTENV}:\n")
# print(Path(DOTENV).read_text())
if __name__ == "__main__": # pragma: no cover
check()

143
moviepy/decorators.py Normal file
View File

@@ -0,0 +1,143 @@
"""Decorators used by moviepy."""
import inspect
import os
import decorator
from moviepy.tools import convert_to_seconds
@decorator.decorator
def outplace(func, clip, *args, **kwargs):
"""Applies ``func(clip.copy(), *args, **kwargs)`` and returns ``clip.copy()``."""
new_clip = clip.copy()
func(new_clip, *args, **kwargs)
return new_clip
@decorator.decorator
def convert_masks_to_RGB(func, clip, *args, **kwargs):
"""If the clip is a mask, convert it to RGB before running the function."""
if clip.is_mask:
clip = clip.to_RGB()
return func(clip, *args, **kwargs)
@decorator.decorator
def apply_to_mask(func, clip, *args, **kwargs):
"""Applies the same function ``func`` to the mask of the clip created with
``func``.
"""
new_clip = func(clip, *args, **kwargs)
if getattr(new_clip, "mask", None):
new_clip.mask = func(new_clip.mask, *args, **kwargs)
return new_clip
@decorator.decorator
def apply_to_audio(func, clip, *args, **kwargs):
"""Applies the function ``func`` to the audio of the clip created with ``func``."""
new_clip = func(clip, *args, **kwargs)
if getattr(new_clip, "audio", None):
new_clip.audio = func(new_clip.audio, *args, **kwargs)
return new_clip
@decorator.decorator
def requires_duration(func, clip, *args, **kwargs):
"""Raises an error if the clip has no duration."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
else:
return func(clip, *args, **kwargs)
@decorator.decorator
def requires_fps(func, clip, *args, **kwargs):
"""Raises an error if the clip has no fps."""
if not hasattr(clip, "fps") or clip.fps is None:
raise ValueError("Attribute 'fps' not set")
else:
return func(clip, *args, **kwargs)
@decorator.decorator
def audio_video_effect(func, effect, clip, *args, **kwargs):
"""Use an audio function on a video/audio clip.
This decorator tells that the function func (audioclip -> audioclip)
can be also used on a video clip, at which case it returns a
videoclip with unmodified video and modified audio.
"""
if hasattr(clip, "audio"):
if clip.audio is not None:
clip.audio = func(effect, clip.audio, *args, **kwargs)
return clip
else:
return func(effect, clip, *args, **kwargs)
def preprocess_args(fun, varnames):
"""Applies fun to variables in varnames before launching the function."""
def wrapper(func, *args, **kwargs):
names = inspect.getfullargspec(func).args
new_args = [
fun(arg) if (name in varnames) and (arg is not None) else arg
for (arg, name) in zip(args, names)
]
new_kwargs = {
kwarg: fun(value) if kwarg in varnames else value
for (kwarg, value) in kwargs.items()
}
return func(*new_args, **new_kwargs)
return decorator.decorator(wrapper)
def convert_parameter_to_seconds(varnames):
"""Converts the specified variables to seconds."""
return preprocess_args(convert_to_seconds, 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(func, clip, *args, **kwargs):
"""Add a mask to the clip if there is none."""
if clip.mask is None:
clip = clip.with_mask()
return func(clip, *args, **kwargs)
@decorator.decorator
def use_clip_fps_by_default(func, clip, *args, **kwargs):
"""Will use ``clip.fps`` if no ``fps=...`` is provided in **kwargs**."""
def find_fps(fps):
if fps is not None:
return fps
elif getattr(clip, "fps", None):
return clip.fps
raise AttributeError(
"No 'fps' (frames per second) attribute specified"
" for function %s and the clip has no 'fps' attribute. Either"
" provide e.g. fps=24 in the arguments of the function, or define"
" the clip's fps with `clip.fps=24`" % func.__name__
)
names = inspect.getfullargspec(func).args[1:]
new_args = [
find_fps(arg) if (name == "fps") else arg for (arg, name) in zip(args, names)
]
new_kwargs = {
kwarg: find_fps(value) if kwarg == "fps" else value
for (kwarg, value) in kwargs.items()
}
return func(clip, *new_args, **new_kwargs)

313
moviepy/tools.py Normal file
View File

@@ -0,0 +1,313 @@
"""Misc. useful functions that can be used at many places in the program."""
import os
import platform
import subprocess as sp
import warnings
import proglog
OS_NAME = os.name
def cross_platform_popen_params(popen_params):
"""Wrap with this function a dictionary of ``subprocess.Popen`` kwargs and
will be ready to work without unexpected behaviours in any platform.
Currently, the implementation will add to them:
- ``creationflags=0x08000000``: no extra unwanted window opens on Windows
when the child process is created. Only added on Windows.
"""
if OS_NAME == "nt":
popen_params["creationflags"] = 0x08000000
return popen_params
def subprocess_call(cmd, logger="bar"):
"""Executes the given subprocess command.
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))
popen_params = cross_platform_popen_params(
{"stdout": sp.DEVNULL, "stderr": sp.PIPE, "stdin": sp.DEVNULL}
)
proc = sp.Popen(cmd, **popen_params)
out, err = proc.communicate() # proc.wait()
proc.stderr.close()
if proc.returncode:
logger(message="MoviePy - Command returned an error")
raise IOError(err.decode("utf8"))
else:
logger(message="MoviePy - Command successful")
del proc
def ffmpeg_escape_filename(filename):
"""Escape a filename that we want to pass to the ffmpeg command line
That will ensure the filename doesn't start with a '-' (which would raise an error)
"""
if filename.startswith("-"):
filename = "./" + filename
return filename
def convert_to_seconds(time):
"""Will convert any time into seconds.
If the type of `time` is not valid,
it's returned as is.
Here are the accepted formats:
.. code:: python
convert_to_seconds(15.4) # seconds
15.4
convert_to_seconds((1, 21.5)) # (min,sec)
81.5
convert_to_seconds((1, 1, 2)) # (hr, min, sec)
3662
convert_to_seconds('01:01:33.045')
3693.045
convert_to_seconds('01:01:33,5') # coma works too
3693.5
convert_to_seconds('1:33,5') # only minutes and secs
99.5
convert_to_seconds('33.5') # only secs
33.5
"""
factors = (1, 60, 3600)
if isinstance(time, str):
time = [float(part.replace(",", ".")) for part in time.split(":")]
if not isinstance(time, (tuple, list)):
return time
return sum(mult * part for mult, part in zip(factors, reversed(time)))
def deprecated_version_of(func, old_name):
"""Indicates that a function is deprecated and has a new name.
`func` is the new function and `old_name` is the name of the deprecated
function.
Returns
-------
deprecated_func
A function that does the same thing as `func`, but with a docstring
and a printed message on call which say that the function is
deprecated and that you should use `func` instead.
Examples
--------
.. code:: python
# The badly named method 'to_file' is replaced by 'write_file'
class Clip:
def write_file(self, some args):
# blablabla
Clip.to_file = deprecated_version_of(Clip.write_file, 'to_file')
"""
# Detect new name of func
new_name = func.__name__
warning = (
"The function ``%s`` is deprecated and is kept temporarily "
"for backwards compatibility.\nPlease use the new name, "
"``%s``, instead."
) % (old_name, new_name)
def deprecated_func(*args, **kwargs):
warnings.warn("MoviePy: " + warning, PendingDeprecationWarning)
return func(*args, **kwargs)
deprecated_func.__doc__ = warning
return deprecated_func
# Non-exhaustive dictionary to store default information.
# Any addition is most welcome.
# Note that 'gif' is complicated to place. From a VideoFileClip point of view,
# it is a video, but from a HTML5 point of view, it is an image.
extensions_dict = {
"mp4": {"type": "video", "codec": ["libx264", "libmpeg4", "aac"]},
"mkv": {"type": "video", "codec": ["libx264", "libmpeg4", "aac"]},
"ogv": {"type": "video", "codec": ["libtheora"]},
"webm": {"type": "video", "codec": ["libvpx"]},
"avi": {"type": "video"},
"mov": {"type": "video"},
"ogg": {"type": "audio", "codec": ["libvorbis"]},
"mp3": {"type": "audio", "codec": ["libmp3lame"]},
"wav": {"type": "audio", "codec": ["pcm_s16le", "pcm_s24le", "pcm_s32le"]},
"m4a": {"type": "audio", "codec": ["libfdk_aac"]},
}
for ext in ["jpg", "jpeg", "png", "bmp", "tiff"]:
extensions_dict[ext] = {"type": "image"}
def find_extension(codec):
"""Returns the correspondent file extension for a codec.
Parameters
----------
codec : str
Video or audio codec name.
"""
if codec in extensions_dict:
# codec is already the extension
return codec
for ext, infos in extensions_dict.items():
if codec in infos.get("codec", []):
return ext
raise ValueError(
"The audio_codec you chose is unknown by MoviePy. "
"You should report this. In the meantime, you can "
"specify a temp_audiofile with the right extension "
"in write_videofile."
)
def close_all_clips(objects="globals", types=("audio", "video", "image")):
"""Closes all clips in a context.
Follows different strategies retrieving the namespace from which the clips
to close will be retrieved depending on the ``objects`` argument, and filtering
by type of clips depending on the ``types`` argument.
Parameters
----------
objects : str or dict, optional
- If is a string an the value is ``"globals"``, will close all the clips
contained by the ``globals()`` namespace.
- If is a dictionary, the values of the dictionary could be clips to close,
useful if you want to use ``locals()``.
types : Iterable, optional
Set of types of clips to close, being "audio", "video" or "image" the supported
values.
"""
from moviepy.audio.io.AudioFileClip import AudioFileClip
from moviepy.video.io.VideoFileClip import VideoFileClip
from moviepy.video.VideoClip import ImageClip
CLIP_TYPES = {
"audio": AudioFileClip,
"video": VideoFileClip,
"image": ImageClip,
}
if objects == "globals": # pragma: no cover
objects = globals()
if hasattr(objects, "values"):
objects = objects.values()
types_tuple = tuple(CLIP_TYPES[key] for key in types)
for obj in objects:
if isinstance(obj, types_tuple):
obj.close()
def no_display_available() -> bool:
"""Return True if we determine the host system has no graphical environment.
This is usefull to remove tests requiring display, like preview
..info::
Currently this only works for Linux/BSD systems with X11 or wayland.
It probably works for SunOS, AIX and CYGWIN
"""
system = platform.system()
if system in ["Linux", "FreeBSD", "NetBSD", "OpenBSD", "SunOS", "AIX"]:
if ("DISPLAY" not in os.environ) and ("WAYLAND_DISPLAY" not in os.environ):
return True
if "CYGWIN_NT" in system:
if ("DISPLAY" not in os.environ) and ("WAYLAND_DISPLAY" not in os.environ):
return True
return False
def compute_position(
clip1_size: tuple, clip2_size: tuple, pos: any, relative: bool = False
) -> tuple[int, int]:
"""Return the position to put clip 1 on clip 2 based on both clip size
and the position of clip 1, as return by clip1.pos() method
Parameters
----------
clip1_size : tuple
The width and height of clip1 (e.g., (width, height)).
clip2_size : tuple
The width and height of clip2 (e.g., (width, height)).
pos : Any
The position of clip1 as returned by the `clip1.pos()` method.
relative: bool
Is the position relative (% of clip size), default False.
Returns
-------
tuple[int, int]
A tuple (x, y) representing the top-left corner of clip1 relative to clip2.
Notes
-----
For more information on `pos`, see the documentation for `VideoClip.with_position`.
"""
if pos is None:
pos = (0, 0)
# preprocess short writings of the position
if isinstance(pos, str):
pos = {
"center": ["center", "center"],
"left": ["left", "center"],
"right": ["right", "center"],
"top": ["center", "top"],
"bottom": ["center", "bottom"],
}[pos]
else:
pos = list(pos)
# is the position relative (given in % of the clip's size) ?
if relative:
for i, dim in enumerate(clip2_size):
if not isinstance(pos[i], str):
pos[i] = dim * pos[i]
if isinstance(pos[0], str):
D = {
"left": 0,
"center": (clip2_size[0] - clip1_size[0]) / 2,
"right": clip2_size[0] - clip1_size[0],
}
pos[0] = D[pos[0]]
if isinstance(pos[1], str):
D = {
"top": 0,
"center": (clip2_size[1] - clip1_size[1]) / 2,
"bottom": clip2_size[1] - clip1_size[1],
}
pos[1] = D[pos[1]]
# Return as int, rounding if necessary
return (int(pos[0]), int(pos[1]))

1
moviepy/version.py Normal file
View File

@@ -0,0 +1 @@
__version__ = "2.1.1"

1985
moviepy/video/VideoClip.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
"""Everything about video manipulation."""

View File

@@ -0,0 +1,378 @@
"""Main video composition interface of MoviePy."""
from functools import reduce
import numpy as np
from PIL import Image
from moviepy.audio.AudioClip import CompositeAudioClip
from moviepy.video.VideoClip import ColorClip, VideoClip
class CompositeVideoClip(VideoClip):
"""
A VideoClip made of other videoclips displayed together. This is the
base class for most compositions.
Parameters
----------
size
The size (width, height) of the final clip.
clips
A list of videoclips.
Clips with a higher ``layer`` attribute will be displayed
on top of other clips in a lower layer.
If two or more clips share the same ``layer``,
then the one appearing latest in ``clips`` will be displayed
on top (i.e. it has the higher layer).
For each clip:
- The attribute ``pos`` determines where the clip is placed.
See ``VideoClip.set_pos``
- The mask of the clip determines which parts are visible.
Finally, if all the clips in the list have their ``duration``
attribute set, then the duration of the composite video clip
is computed automatically
bg_color
Color for the unmasked and unfilled regions. Set to None for these
regions to be transparent (will be slower).
use_bgclip
Set to True if the first clip in the list should be used as the
'background' on which all other clips are blitted. That first clip must
have the same size as the final clip. If it has no transparency, the final
clip will have no mask.
The clip with the highest FPS will be the FPS of the composite clip.
"""
def __init__(
self, clips, size=None, bg_color=None, use_bgclip=False, is_mask=False
):
if size is None:
size = clips[0].size
if use_bgclip and (clips[0].mask is None):
transparent = False
else:
transparent = True if bg_color is None else False
# If we must not use first clip as background and we dont have a color
# we generate a black background if clip should not be transparent and
# a transparent background if transparent
if (not use_bgclip) and bg_color is None:
if transparent:
bg_color = 0.0 if is_mask else (0, 0, 0, 0)
else:
bg_color = 0.0 if is_mask else (0, 0, 0)
fpss = [clip.fps for clip in clips if getattr(clip, "fps", None)]
self.fps = max(fpss) if fpss else None
VideoClip.__init__(self)
self.size = size
self.is_mask = is_mask
self.clips = clips
self.bg_color = bg_color
# Use first clip as background if necessary, else use color
# either set by user or previously generated
if use_bgclip:
self.bg = clips[0]
self.clips = clips[1:]
self.created_bg = False
else:
self.clips = clips
self.bg = ColorClip(size, color=self.bg_color, is_mask=is_mask)
self.created_bg = True
# order self.clips by layer
self.clips = sorted(self.clips, key=lambda clip: clip.layer_index)
# compute duration
ends = [clip.end for clip in self.clips]
if None not in ends:
duration = max(ends)
self.duration = duration
self.end = duration
# compute audio
audioclips = [v.audio for v in self.clips if v.audio is not None]
if audioclips:
self.audio = CompositeAudioClip(audioclips)
# compute mask if necessary
if transparent:
maskclips = [
(clip.mask if (clip.mask is not None) else clip.with_mask().mask)
.with_position(clip.pos)
.with_end(clip.end)
.with_start(clip.start, change_end=False)
.with_layer_index(clip.layer_index)
for clip in self.clips
]
if use_bgclip and self.bg.mask:
maskclips = [self.bg.mask] + maskclips
self.mask = CompositeVideoClip(
maskclips, self.size, is_mask=True, bg_color=0.0
)
def frame_function(self, t):
"""The clips playing at time `t` are blitted over one another."""
# For the mask we recalculate the final transparency we'll need
# to apply on the result image
if self.is_mask:
mask = np.zeros((self.size[1], self.size[0]), dtype=float)
for clip in self.playing_clips(t):
mask = clip.compose_mask(mask, t)
return mask
# Try doing clip merging with pillow
bg_t = t - self.bg.start
bg_frame = self.bg.get_frame(bg_t).astype("uint8")
bg_img = Image.fromarray(bg_frame)
if self.bg.mask:
bgm_t = t - self.bg.mask.start
bg_mask = (self.bg.mask.get_frame(bgm_t) * 255).astype("uint8")
bg_mask_img = Image.fromarray(bg_mask).convert("L")
# Resize bg_mask_img to match bg_img, always use top left corner
if bg_mask_img.size != bg_img.size:
mask_width, mask_height = bg_mask_img.size
img_width, img_height = bg_img.size
if mask_width > img_width or mask_height > img_height:
bg_mask_img = bg_mask_img.crop((0, 0, img_width, img_height))
else:
new_mask = Image.new("L", (img_width, img_height), 0)
new_mask.paste(bg_mask_img, (0, 0))
bg_mask_img = new_mask
bg_img = bg_img.convert("RGBA")
bg_img.putalpha(bg_mask_img)
# For each clip apply on top of current img
current_img = bg_img
for clip in self.playing_clips(t):
current_img = clip.compose_on(current_img, t)
# Turn Pillow image into a numpy array
frame = np.array(current_img)
# If frame have transparency, remove it
# our mask will take care of it during rendering
if frame.shape[2] == 4:
return frame[:, :, :3]
return frame
def playing_clips(self, t=0):
"""Returns a list of the clips in the composite clips that are
actually playing at the given time `t`.
"""
return [clip for clip in self.clips if clip.is_playing(t)]
def close(self):
"""Closes the instance, releasing all the resources."""
if self.created_bg and self.bg:
# Only close the background clip if it was locally created.
# Otherwise, it remains the job of whoever created it.
self.bg.close()
self.bg = None
if hasattr(self, "audio") and self.audio:
self.audio.close()
self.audio = None
def clips_array(array, rows_widths=None, cols_heights=None, bg_color=None):
"""Given a matrix whose rows are clips, creates a CompositeVideoClip where
all clips are placed side by side horizontally for each clip in each row
and one row on top of the other for each row. So given next matrix of clips
with same size:
```python
clips_array([[clip1, clip2, clip3], [clip4, clip5, clip6]])
```
the result will be a CompositeVideoClip with a layout displayed like:
```
┏━━━━━━━┳━━━━━━━┳━━━━━━━┓
┃ ┃ ┃ ┃
┃ clip1 ┃ clip2 ┃ clip3 ┃
┃ ┃ ┃ ┃
┣━━━━━━━╋━━━━━━━╋━━━━━━━┫
┃ ┃ ┃ ┃
┃ clip4 ┃ clip5 ┃ clip6 ┃
┃ ┃ ┃ ┃
┗━━━━━━━┻━━━━━━━┻━━━━━━━┛
```
If some clips doesn't fulfill the space required by the rows or columns
in which are placed, that space will be filled by the color defined in
``bg_color``.
array
Matrix of clips included in the returned composited video clip.
rows_widths
Widths of the different rows in pixels. If ``None``, is set automatically.
cols_heights
Heights of the different columns in pixels. If ``None``, is set automatically.
bg_color
Fill color for the masked and unfilled regions. Set to ``None`` for these
regions to be transparent (processing will be slower).
"""
array = np.array(array)
sizes_array = np.array([[clip.size for clip in line] for line in array])
# find row width and col_widths automatically if not provided
if rows_widths is None:
rows_widths = sizes_array[:, :, 1].max(axis=1)
if cols_heights is None:
cols_heights = sizes_array[:, :, 0].max(axis=0)
# compute start positions of X for rows and Y for columns
xs = np.cumsum([0] + list(cols_heights))
ys = np.cumsum([0] + list(rows_widths))
for j, (x, ch) in enumerate(zip(xs[:-1], cols_heights)):
for i, (y, rw) in enumerate(zip(ys[:-1], rows_widths)):
clip = array[i, j]
w, h = clip.size
# if clip not fulfill row width or column height
if (w < ch) or (h < rw):
clip = CompositeVideoClip(
[clip.with_position("center")], size=(ch, rw), bg_color=bg_color
).with_duration(clip.duration)
array[i, j] = clip.with_position((x, y))
return CompositeVideoClip(array.flatten(), size=(xs[-1], ys[-1]), bg_color=bg_color)
def concatenate_videoclips(
clips, method="chain", transition=None, bg_color=None, is_mask=False, padding=0
):
"""Concatenates several video clips.
Returns a video clip made by clip by concatenating several video clips.
(Concatenated means that they will be played one after another).
There are two methods:
- method="chain": will produce a clip that simply outputs
the frames of the successive clips, without any correction if they are
not of the same size of anything. If none of the clips have masks the
resulting clip has no mask, else the mask is a concatenation of masks
(using completely opaque for clips that don't have masks, obviously).
If you have clips of different size and you want to write directly the
result of the concatenation to a file, use the method "compose" instead.
- method="compose", if the clips do not have the same resolution, the final
resolution will be such that no clip has to be resized.
As a consequence the final clip has the height of the highest clip and the
width of the widest clip of the list. All the clips with smaller dimensions
will appear centered. The border will be transparent if mask=True, else it
will be of the color specified by ``bg_color``.
The clip with the highest FPS will be the FPS of the result clip.
Parameters
----------
clips
A list of video clips which must all have their ``duration``
attributes set.
method
"chain" or "compose": see above.
transition
A clip that will be played between each two clips of the list.
bg_color
Only for method='compose'. Color of the background.
Set to None for a transparent clip
padding
Only for method='compose'. Duration during two consecutive clips.
Note that for negative padding, a clip will partly play at the same
time as the clip it follows (negative padding is cool for clips who fade
in on one another). A non-null padding automatically sets the method to
`compose`.
"""
if transition is not None:
clip_transition_pairs = [[v, transition] for v in clips[:-1]]
clips = reduce(lambda x, y: x + y, clip_transition_pairs) + [clips[-1]]
transition = None
timings = np.cumsum([0] + [clip.duration for clip in clips])
sizes = [clip.size for clip in clips]
w = max(size[0] for size in sizes)
h = max(size[1] for size in sizes)
timings = np.maximum(0, timings + padding * np.arange(len(timings)))
timings[-1] -= padding # Last element is the duration of the whole
if method == "chain":
def frame_function(t):
i = max([i for i, e in enumerate(timings) if e <= t])
return clips[i].get_frame(t - timings[i])
def get_mask(clip):
mask = clip.mask or ColorClip(clip.size, color=1, is_mask=True)
if mask.duration is None:
mask.duration = clip.duration
return mask
result = VideoClip(is_mask=is_mask, frame_function=frame_function)
if any([clip.mask is not None for clip in clips]):
masks = [get_mask(clip) for clip in clips]
result.mask = concatenate_videoclips(masks, method="chain", is_mask=True)
result.clips = clips
elif method == "compose":
result = CompositeVideoClip(
[
clip.with_start(t).with_position("center")
for (clip, t) in zip(clips, timings)
],
size=(w, h),
bg_color=bg_color,
is_mask=is_mask,
)
else:
raise Exception(
"MoviePy Error: The 'method' argument of "
"concatenate_videoclips must be 'chain' or 'compose'"
)
result.timings = timings
result.start_times = timings[:-1]
result.start, result.duration, result.end = 0, timings[-1], timings[-1]
audio_t = [
(clip.audio, t) for clip, t in zip(clips, timings) if clip.audio is not None
]
if audio_t:
result.audio = CompositeAudioClip([a.with_start(t) for a, t in audio_t])
fpss = [clip.fps for clip in clips if getattr(clip, "fps", None) is not None]
result.fps = max(fpss) if fpss else None
return result

View File

@@ -0,0 +1 @@
"""All for compositing video clips."""

View File

@@ -0,0 +1,83 @@
from dataclasses import dataclass
from moviepy.Effect import Effect
@dataclass
class AccelDecel(Effect):
"""Accelerates and decelerates a clip, useful for GIF making.
Parameters
----------
new_duration : float
Duration for the new transformed clip. If None, will be that of the
current clip.
abruptness : float
Slope shape in the acceleration-deceleration function. It will depend
on the value of the parameter:
* ``-1 < abruptness < 0``: speed up, down, up.
* ``abruptness == 0``: no effect.
* ``abruptness > 0``: speed down, up, down.
soonness : float
For positive abruptness, determines how soon the transformation occurs.
Should be a positive number.
Raises
------
ValueError
When ``sooness`` argument is lower than 0.
Examples
--------
The following graphs show functions generated by different combinations
of arguments, where the value of the slopes represents the speed of the
videos generated, being the linear function (in red) a combination that
does not produce any transformation.
.. image:: /_static/medias/accel_decel-fx-params.png
:alt: acced_decel FX parameters combinations
"""
new_duration: float = None
abruptness: float = 1.0
soonness: float = 1.0
def _f_accel_decel(
self, t, old_duration, new_duration, abruptness=1.0, soonness=1.0
):
a = 1.0 + abruptness
def _f(t):
def f1(t):
return (0.5) ** (1 - a) * (t**a)
def f2(t):
return 1 - f1(1 - t)
return (t < 0.5) * f1(t) + (t >= 0.5) * f2(t)
return old_duration * _f((t / new_duration) ** soonness)
def apply(self, clip):
"""Apply the effect to the clip."""
if self.new_duration is None:
self.new_duration = clip.duration
if self.soonness < 0:
raise ValueError("'sooness' should be a positive number")
return clip.time_transform(
lambda t: self._f_accel_decel(
t=t,
old_duration=clip.duration,
new_duration=self.new_duration,
abruptness=self.abruptness,
soonness=self.soonness,
)
).with_duration(self.new_duration)

View File

@@ -0,0 +1,38 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Effect import Effect
@dataclass
class BlackAndWhite(Effect):
"""Desaturates the picture, makes it black and white.
Parameter RGB allows to set weights for the different color
channels.
If RBG is 'CRT_phosphor' a special set of values is used.
preserve_luminosity maintains the sum of RGB to 1.
"""
RGB: str = None
preserve_luminosity: bool = True
def apply(self, clip):
"""Apply the effect to the clip."""
if self.RGB is None:
self.RGB = [1, 1, 1]
if self.RGB == "CRT_phosphor":
self.RGB = [0.2125, 0.7154, 0.0721]
R, G, B = (
1.0
* np.array(self.RGB)
/ (sum(self.RGB) if self.preserve_luminosity else 1)
)
def filter(im):
im = R * im[:, :, 0] + G * im[:, :, 1] + B * im[:, :, 2]
return np.dstack(3 * [im]).astype("uint8")
return clip.image_transform(filter)

27
moviepy/video/fx/Blink.py Normal file
View File

@@ -0,0 +1,27 @@
from dataclasses import dataclass
from moviepy.Effect import Effect
@dataclass
class Blink(Effect):
"""
Makes the clip blink. At each blink it will be displayed ``duration_on``
seconds and disappear ``duration_off`` seconds. Will only work in
composite clips.
"""
duration_on: float
duration_off: float
def apply(self, clip):
"""Apply the effect to the clip."""
if clip.mask is None:
clip = clip.with_mask()
duration = self.duration_on + self.duration_off
clip.mask = clip.mask.transform(
lambda get_frame, t: get_frame(t) * ((t % duration) < self.duration_on)
)
return clip

80
moviepy/video/fx/Crop.py Normal file
View File

@@ -0,0 +1,80 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class Crop(Effect):
"""Effect to crop a clip to get a new clip in which just a rectangular
subregion of the original clip is conserved. `x1,y1` indicates the top left
corner and `x2,y2` is the lower right corner of the cropped region. All
coordinates are in pixels. Float numbers are accepted.
To crop an arbitrary rectangle:
>>> Crop(x1=50, y1=60, x2=460, y2=275)
Only remove the part above y=30:
>>> Crop(y1=30)
Crop a rectangle that starts 10 pixels left and is 200px wide
>>> Crop(x1=10, width=200)
Crop a rectangle centered in x,y=(300,400), width=50, height=150 :
>>> Crop(x_center=300, y_center=400, width=50, height=150)
Any combination of the above should work, like for this rectangle
centered in x=300, with explicit y-boundaries:
>>> Crop(x_center=300, width=400, y1=100, y2=600)
"""
x1: int = None
y1: int = None
x2: int = None
y2: int = None
width: int = None
height: int = None
x_center: int = None
y_center: int = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if self.width and self.x1 is not None:
self.x2 = self.x1 + self.width
elif self.width and self.x2 is not None:
self.x1 = self.x2 - self.width
if self.height and self.y1 is not None:
self.y2 = self.y1 + self.height
elif self.height and self.y2 is not None:
self.y1 = self.y2 - self.height
if self.x_center:
self.x1, self.x2 = (
self.x_center - self.width / 2,
self.x_center + self.width / 2,
)
if self.y_center:
self.y1, self.y2 = (
self.y_center - self.height / 2,
self.y_center + self.height / 2,
)
self.x1 = self.x1 or 0
self.y1 = self.y1 or 0
self.x2 = self.x2 or clip.size[0]
self.y2 = self.y2 or clip.size[1]
return clip.image_transform(
lambda frame: frame[
int(self.y1) : int(self.y2), int(self.x1) : int(self.x2)
],
apply_to=["mask"],
)

View File

@@ -0,0 +1,27 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.fx.FadeIn import FadeIn
@dataclass
class CrossFadeIn(Effect):
"""Makes the clip appear progressively, over ``duration`` seconds.
Only works when the clip is included in a CompositeVideoClip.
"""
duration: float
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
if clip.mask is None:
clip = clip.with_mask()
clip.mask.duration = clip.duration
clip.mask = clip.mask.with_effects([FadeIn(self.duration)])
return clip

View File

@@ -0,0 +1,27 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.fx.FadeOut import FadeOut
@dataclass
class CrossFadeOut(Effect):
"""Makes the clip disappear progressively, over ``duration`` seconds.
Only works when the clip is included in a CompositeVideoClip.
"""
duration: float
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
if clip.mask is None:
clip = clip.with_mask()
clip.mask.duration = clip.duration
clip.mask = clip.mask.with_effects([FadeOut(self.duration)])
return clip

View File

@@ -0,0 +1,34 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class EvenSize(Effect):
"""Crops the clip to make dimensions even."""
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
w, h = clip.size
w_even = w % 2 == 0
h_even = h % 2 == 0
if w_even and h_even:
return clip
if not w_even and not h_even:
def image_filter(a):
return a[:-1, :-1, :]
elif h_even:
def image_filter(a):
return a[:, :-1, :]
else:
def image_filter(a):
return a[:-1, :, :]
return clip.image_transform(image_filter, apply_to=["mask"])

View File

@@ -0,0 +1,36 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class FadeIn(Effect):
"""Makes the clip progressively appear from some color (black by default),
over ``duration`` seconds at the beginning of the clip. Can be used for
masks too, where the initial color must be a number between 0 and 1.
For cross-fading (progressive appearance or disappearance of a clip
over another clip, see ``CrossFadeIn``
"""
duration: float
initial_color: list = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if self.initial_color is None:
self.initial_color = 0 if clip.is_mask else [0, 0, 0]
self.initial_color = np.array(self.initial_color)
def filter(get_frame, t):
if t >= self.duration:
return get_frame(t)
else:
fading = 1.0 * t / self.duration
return fading * get_frame(t) + (1 - fading) * self.initial_color
return clip.transform(filter)

View File

@@ -0,0 +1,39 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class FadeOut(Effect):
"""Makes the clip progressively fade to some color (black by default),
over ``duration`` seconds at the end of the clip. Can be used for masks too,
where the final color must be a number between 0 and 1.
For cross-fading (progressive appearance or disappearance of a clip over another
clip), see ``CrossFadeOut``
"""
duration: float
final_color: list = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
if self.final_color is None:
self.final_color = 0 if clip.is_mask else [0, 0, 0]
self.final_color = np.array(self.final_color)
def filter(get_frame, t):
if (clip.duration - t) >= self.duration:
return get_frame(t)
else:
fading = 1.0 * (clip.duration - t) / self.duration
return fading * get_frame(t) + (1 - fading) * self.final_color
return clip.transform(filter)

View File

@@ -0,0 +1,43 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.compositing.CompositeVideoClip import concatenate_videoclips
@dataclass
class Freeze(Effect):
"""Momentarily freeze the clip at time t.
Set `t='end'` to freeze the clip at the end (actually it will freeze on the
frame at time clip.duration - padding_end seconds - 1 / clip_fps).
With ``duration`` you can specify the duration of the freeze.
With ``total_duration`` you can specify the total duration of
the clip and the freeze (i.e. the duration of the freeze is
automatically computed). One of them must be provided.
"""
t: float = 0
freeze_duration: float = None
total_duration: float = None
padding_end: float = 0
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
if self.t == "end":
self.t = clip.duration - self.padding_end - 1 / clip.fps
if self.freeze_duration is None:
if self.total_duration is None:
raise ValueError(
"You must provide either 'freeze_duration' or 'total_duration'"
)
self.freeze_duration = self.total_duration - clip.duration
before = [clip[: self.t]] if (self.t != 0) else []
freeze = [clip.to_ImageClip(self.t).with_duration(self.freeze_duration)]
after = [clip[self.t :]] if (self.t != clip.duration) else []
return concatenate_videoclips(before + freeze + after)

View File

@@ -0,0 +1,68 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
from moviepy.video.fx.Crop import Crop
@dataclass
class FreezeRegion(Effect):
"""Freezes one region of the clip while the rest remains animated.
You can choose one of three methods by providing either `region`,
`outside_region`, or `mask`.
Parameters
----------
t
Time at which to freeze the freezed region.
region
A tuple (x1, y1, x2, y2) defining the region of the screen (in pixels)
which will be freezed. You can provide outside_region or mask instead.
outside_region
A tuple (x1, y1, x2, y2) defining the region of the screen (in pixels)
which will be the only non-freezed region.
mask
If not None, will overlay a freezed version of the clip on the current clip,
with the provided mask. In other words, the "visible" pixels in the mask
indicate the freezed region in the final picture.
"""
t: float = 0
region: tuple = None
outside_region: tuple = None
mask: Clip = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if self.region is not None:
x1, y1, _x2, _y2 = self.region
freeze = (
clip.with_effects([Crop(*self.region)])
.to_ImageClip(t=self.t)
.with_duration(clip.duration)
.with_position((x1, y1))
)
return CompositeVideoClip([clip, freeze])
elif self.outside_region is not None:
x1, y1, x2, y2 = self.outside_region
animated_region = clip.with_effects(
[Crop(*self.outside_region)]
).with_position((x1, y1))
freeze = clip.to_ImageClip(t=self.t).with_duration(clip.duration)
return CompositeVideoClip([freeze, animated_region])
elif self.mask is not None:
freeze = (
clip.to_ImageClip(t=self.t)
.with_duration(clip.duration)
.with_mask(self.mask)
)
return CompositeVideoClip([clip, freeze])

View File

@@ -0,0 +1,20 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class GammaCorrection(Effect):
"""Gamma-correction of a video clip."""
gamma: float
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
def filter(im):
corrected = 255 * (1.0 * im / 255) ** self.gamma
return corrected.astype("uint8")
return clip.image_transform(filter)

View File

@@ -0,0 +1,45 @@
from dataclasses import dataclass
import numpy as np
from PIL import Image, ImageDraw, ImageFilter
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class HeadBlur(Effect):
"""Returns a filter that will blur a moving part (a head ?) of the frames.
The position of the blur at time t is defined by (fx(t), fy(t)), the radius
of the blurring by ``radius`` and the intensity of the blurring by ``intensity``.
"""
fx: callable
fy: callable
radius: float
intensity: float = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if self.intensity is None:
self.intensity = int(2 * self.radius / 3)
def filter(gf, t):
im = gf(t).copy()
h, w, d = im.shape
x, y = int(self.fx(t)), int(self.fy(t))
x1, x2 = max(0, x - self.radius), min(x + self.radius, w)
y1, y2 = max(0, y - self.radius), min(y + self.radius, h)
image = Image.fromarray(im)
mask = Image.new("RGB", image.size)
draw = ImageDraw.Draw(mask)
draw.ellipse([x1, y1, x2, y2], fill=(255, 255, 255))
blurred = image.filter(ImageFilter.GaussianBlur(radius=self.intensity))
res = np.where(np.array(mask) > 0, np.array(blurred), np.array(image))
return res
return clip.transform(filter)

View File

@@ -0,0 +1,18 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class InvertColors(Effect):
"""Returns the color-inversed clip.
The values of all pixels are replaced with (255-v) or (1-v) for masks
Black becomes white, green becomes purple, etc.
"""
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
maxi = 1.0 if clip.is_mask else 255
return clip.image_transform(lambda f: maxi - f)

43
moviepy/video/fx/Loop.py Normal file
View File

@@ -0,0 +1,43 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class Loop(Effect):
"""
Returns a clip that plays the current clip in an infinite loop.
Ideal for clips coming from GIFs.
Parameters
----------
n
Number of times the clip should be played. If `None` the
the clip will loop indefinitely (i.e. with no set duration).
duration
Total duration of the clip. Can be specified instead of n.
"""
n: int = None
duration: float = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
previous_duration = clip.duration
clip = clip.time_transform(
lambda t: t % previous_duration, apply_to=["mask", "audio"]
)
if self.n:
self.duration = self.n * previous_duration
if self.duration:
clip = clip.with_duration(self.duration)
return clip

View File

@@ -0,0 +1,27 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class LumContrast(Effect):
"""Luminosity-contrast correction of a clip."""
lum: float = 0
contrast: float = 0
contrast_threshold: float = 127
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
def image_filter(im):
im = 1.0 * im # float conversion
corrected = (
im + self.lum + self.contrast * (im - float(self.contrast_threshold))
)
corrected[corrected < 0] = 0
corrected[corrected > 255] = 255
return corrected.astype("uint8")
return clip.image_transform(image_filter)

View File

@@ -0,0 +1,30 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
from moviepy.video.fx.CrossFadeIn import CrossFadeIn
@dataclass
class MakeLoopable(Effect):
"""Makes the clip fade in progressively at its own end, this way it can be
looped indefinitely.
Parameters
----------
overlap_duration : float
Duration of the fade-in (in seconds).
"""
overlap_duration: float
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
clip2 = clip.with_effects([CrossFadeIn(self.overlap_duration)]).with_start(
clip.duration - self.overlap_duration
)
return CompositeVideoClip([clip, clip2]).subclipped(
self.overlap_duration, clip.duration
)

View File

@@ -0,0 +1,90 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.VideoClip import ImageClip
@dataclass
class Margin(Effect):
"""Draws an external margin all around the frame.
Parameters
----------
margin_size : int, optional
If not ``None``, then the new clip has a margin size of
size ``margin_size`` in pixels on the left, right, top, and bottom.
left : int, optional
If ``margin_size=None``, margin size for the new clip in left direction.
right : int, optional
If ``margin_size=None``, margin size for the new clip in right direction.
top : int, optional
If ``margin_size=None``, margin size for the new clip in top direction.
bottom : int, optional
If ``margin_size=None``, margin size for the new clip in bottom direction.
color : tuple, optional
Color of the margin.
opacity : float, optional
Opacity of the margin. Setting this value to 0 yields transparent margins.
"""
margin_size: int = None
left: int = 0
right: int = 0
top: int = 0
bottom: int = 0
color: tuple = (0, 0, 0)
opacity: float = 1.0
def add_margin(self, clip: Clip):
"""Add margins to the clip."""
if (self.opacity != 1.0) and (clip.mask is None) and not (clip.is_mask):
clip = clip.with_mask()
if self.margin_size is not None:
self.left = self.right = self.top = self.bottom = self.margin_size
def make_bg(w, h):
new_w, new_h = w + self.left + self.right, h + self.top + self.bottom
if clip.is_mask:
shape = (new_h, new_w)
bg = np.tile(self.opacity, (new_h, new_w)).astype(float).reshape(shape)
else:
shape = (new_h, new_w, 3)
bg = np.tile(self.color, (new_h, new_w)).reshape(shape)
return bg
if isinstance(clip, ImageClip):
im = make_bg(clip.w, clip.h)
im[self.top : self.top + clip.h, self.left : self.left + clip.w] = clip.img
return clip.image_transform(lambda pic: im)
else:
def filter(get_frame, t):
pic = get_frame(t)
h, w = pic.shape[:2]
im = make_bg(w, h)
im[self.top : self.top + h, self.left : self.left + w] = pic
return im
return clip.transform(filter)
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
# We apply once on clip and once on mask if we have one
clip = self.add_margin(clip=clip)
if clip.mask:
clip.mask = self.add_margin(clip=clip.mask)
return clip

View File

@@ -0,0 +1,45 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class MaskColor(Effect):
"""Returns a new clip with a mask for transparency where the original
clip is of the given color.
You can also have a "progressive" mask by specifying a non-null distance
threshold ``threshold``. In this case, if the distance between a pixel and
the given color is d, the transparency will be
d**stiffness / (threshold**stiffness + d**stiffness)
which is 1 when d>>threshold and 0 for d<<threshold, the stiffness of the
effect being parametrized by ``stiffness``
"""
color: tuple = (0, 0, 0)
threshold: float = 0
stiffness: float = 1
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
color = np.array(self.color)
def hill(x):
if self.threshold:
return x**self.stiffness / (
self.threshold**self.stiffness + x**self.stiffness
)
else:
return 1.0 * (x != 0)
def flim(im):
return hill(np.sqrt(((im - color) ** 2).sum(axis=2)))
mask = clip.image_transform(flim)
mask.is_mask = True
return clip.with_mask(mask)

View File

@@ -0,0 +1,52 @@
from dataclasses import dataclass
from typing import Union
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.VideoClip import ImageClip
@dataclass
class MasksAnd(Effect):
"""Returns the logical 'and' (minimum pixel color values) between two masks.
The result has the duration of the clip to which has been applied, if it has any.
Parameters
----------
other_clip ImageClip or np.ndarray
Clip used to mask the original clip.
Examples
--------
.. code:: python
clip = ColorClip(color=(255, 0, 0), size=(1, 1)) # red
mask = ColorClip(color=(0, 255, 0), size=(1, 1)) # green
masked_clip = clip.with_effects([vfx.MasksAnd(mask)]) # black
masked_clip.get_frame(0)
[[[0 0 0]]]
"""
other_clip: Union[Clip, np.ndarray]
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
# to ensure that 'and' of two ImageClips will be an ImageClip
if isinstance(self.other_clip, ImageClip):
self.other_clip = self.other_clip.img
if isinstance(self.other_clip, np.ndarray):
return clip.image_transform(
lambda frame: np.minimum(frame, self.other_clip)
)
else:
return clip.transform(
lambda get_frame, t: np.minimum(
get_frame(t), self.other_clip.get_frame(t)
)
)

View File

@@ -0,0 +1,52 @@
from dataclasses import dataclass
from typing import Union
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.VideoClip import ImageClip
@dataclass
class MasksOr(Effect):
"""Returns the logical 'or' (maximum pixel color values) between two masks.
The result has the duration of the clip to which has been applied, if it has any.
Parameters
----------
other_clip ImageClip or np.ndarray
Clip used to mask the original clip.
Examples
--------
.. code:: python
clip = ColorClip(color=(255, 0, 0), size=(1, 1)) # red
mask = ColorClip(color=(0, 255, 0), size=(1, 1)) # green
masked_clip = clip.with_effects([vfx.MasksOr(mask)]) # yellow
masked_clip.get_frame(0)
[[[255 255 0]]]
"""
other_clip: Union[Clip, np.ndarray]
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
# to ensure that 'or' of two ImageClips will be an ImageClip
if isinstance(self.other_clip, ImageClip):
self.other_clip = self.other_clip.img
if isinstance(self.other_clip, np.ndarray):
return clip.image_transform(
lambda frame: np.maximum(frame, self.other_clip)
)
else:
return clip.transform(
lambda get_frame, t: np.maximum(
get_frame(t), self.other_clip.get_frame(t)
)
)

View File

@@ -0,0 +1,16 @@
from dataclasses import dataclass
from typing import List, Union
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class MirrorX(Effect):
"""Flips the clip horizontally (and its mask too, by default)."""
apply_to: Union[List, str] = "mask"
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
return clip.image_transform(lambda img: img[:, ::-1], apply_to=self.apply_to)

View File

@@ -0,0 +1,16 @@
from dataclasses import dataclass
from typing import List, Union
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class MirrorY(Effect):
"""Flips the clip vertically (and its mask too, by default)."""
apply_to: Union[List, str] = "mask"
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
return clip.image_transform(lambda img: img[::-1], apply_to=self.apply_to)

View File

@@ -0,0 +1,23 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class MultiplyColor(Effect):
"""
Multiplies the clip's colors by the given factor, can be used
to decrease or increase the clip's brightness (is that the
right word ?)
"""
factor: float
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
return clip.image_transform(
lambda frame: np.minimum(255, (self.factor * frame)).astype("uint8")
)

View File

@@ -0,0 +1,31 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class MultiplySpeed(Effect):
"""Returns a clip playing the current clip but at a speed multiplied by ``factor``.
Instead of factor one can indicate the desired ``final_duration`` of the clip, and
the factor will be automatically computed. The same effect is applied to the clip's
audio and mask if any.
"""
factor: float = None
final_duration: float = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if self.final_duration:
self.factor = 1.0 * clip.duration / self.final_duration
new_clip = clip.time_transform(
lambda t: self.factor * t, apply_to=["mask", "audio"]
)
if clip.duration is not None:
new_clip = new_clip.with_duration(1.0 * clip.duration / self.factor)
return new_clip

View File

@@ -0,0 +1,63 @@
from dataclasses import dataclass
import numpy as np
from PIL import Image, ImageFilter
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class Painting(Effect):
"""Transforms any photo into some kind of painting.
Transforms any photo into some kind of painting. Saturation
tells at which point the colors of the result should be
flashy. ``black`` gives the amount of black lines wanted.
np_image : a numpy image
"""
saturation: float = 1.4
black: float = 0.006
def to_painting(self, np_image, saturation=1.4, black=0.006):
"""Transforms any photo into some kind of painting.
Transforms any photo into some kind of painting. Saturation
tells at which point the colors of the result should be
flashy. ``black`` gives the amount of black lines wanted.
np_image : a numpy image
"""
image = Image.fromarray(np_image)
image = image.filter(ImageFilter.EDGE_ENHANCE_MORE)
# Convert the image to grayscale
grayscale_image = image.convert("L")
# Find the image edges
edges_image = grayscale_image.filter(ImageFilter.FIND_EDGES)
# Convert the edges image to a numpy array
edges = np.array(edges_image)
# Create the darkening effect
darkening = black * (255 * np.dstack(3 * [edges]))
# Apply the painting effect
painting = saturation * np.array(image) - darkening
# Clip the pixel values to the valid range of 0-255
painting = np.maximum(0, np.minimum(255, painting))
# Convert the pixel values to unsigned 8-bit integers
painting = painting.astype("uint8")
return painting
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
return clip.image_transform(
lambda im: self.to_painting(im, self.saturation, self.black)
)

158
moviepy/video/fx/Resize.py Normal file
View File

@@ -0,0 +1,158 @@
import numbers
from dataclasses import dataclass
from typing import Union
import numpy as np
from PIL import Image
from moviepy.Effect import Effect
@dataclass
class Resize(Effect):
"""Effect returning a video clip that is a resized version of the clip.
Parameters
----------
new_size : tuple or float or function, optional
Can be either
- ``(width, height)`` in pixels or a float representing
- A scaling factor, like ``0.5``.
- A function of time returning one of these.
height : int, optional
Height of the new clip in pixels. The width is then computed so
that the width/height ratio is conserved.
width : int, optional
Width of the new clip in pixels. The height is then computed so
that the width/height ratio is conserved.
Examples
--------
.. code:: python
clip.with_effects([vfx.Resize((460,720))]) # New resolution: (460,720)
clip.with_effects([vfx.Resize(0.6)]) # width and height multiplied by 0.6
clip.with_effects([vfx.Resize(width=800)]) # height computed automatically.
clip.with_effects([vfx.Resize(lambda t : 1+0.02*t)]) # slow clip swelling
"""
new_size: Union[tuple, float, callable] = None
height: int = None
width: int = None
apply_to_mask: bool = True
def resizer(self, pic, new_size):
"""Resize the image using PIL."""
new_size = list(map(int, new_size))
pil_img = Image.fromarray(pic)
resized_pil = pil_img.resize(new_size, Image.Resampling.LANCZOS)
return np.array(resized_pil)
def apply(self, clip):
"""Apply the effect to the clip."""
w, h = clip.size
if self.new_size is not None:
def translate_new_size(new_size_):
"""Returns a [w, h] pair from `new_size_`. If `new_size_` is a
scalar, then work out the correct pair using the clip's size.
Otherwise just return `new_size_`
"""
if isinstance(new_size_, numbers.Number):
return [new_size_ * w, new_size_ * h]
else:
return new_size_
if hasattr(self.new_size, "__call__"):
# The resizing is a function of time
def get_new_size(t):
return translate_new_size(self.new_size(t))
if clip.is_mask:
def filter(get_frame, t):
return (
self.resizer(
(255 * get_frame(t)).astype("uint8"), get_new_size(t)
)
/ 255.0
)
else:
def filter(get_frame, t):
return self.resizer(
get_frame(t).astype("uint8"), get_new_size(t)
)
newclip = clip.transform(
filter,
keep_duration=True,
apply_to=(["mask"] if self.apply_to_mask else []),
)
if self.apply_to_mask and clip.mask is not None:
newclip.mask = clip.mask.with_effects(
[Resize(self.new_size, apply_to_mask=False)]
)
return newclip
else:
self.new_size = translate_new_size(self.new_size)
elif self.height is not None:
if hasattr(self.height, "__call__"):
def func(t):
return 1.0 * int(self.height(t)) / h
return clip.with_effects([Resize(func)])
else:
self.new_size = [w * self.height / h, self.height]
elif self.width is not None:
if hasattr(self.width, "__call__"):
def func(t):
return 1.0 * self.width(t) / w
return clip.with_effects([Resize(func)])
else:
self.new_size = [self.width, h * self.width / w]
else:
raise ValueError(
"You must provide either 'new_size' or 'height' or 'width'"
)
# From here, the resizing is constant (not a function of time), size=newsize
if clip.is_mask:
def image_filter(pic):
return (
1.0
* self.resizer((255 * pic).astype("uint8"), self.new_size)
/ 255.0
)
else:
def image_filter(pic):
return self.resizer(pic.astype("uint8"), self.new_size)
new_clip = clip.image_transform(image_filter)
if self.apply_to_mask and clip.mask is not None:
new_clip.mask = clip.mask.with_effects(
[Resize(self.new_size, apply_to_mask=False)]
)
return new_clip

128
moviepy/video/fx/Rotate.py Normal file
View File

@@ -0,0 +1,128 @@
import math
from dataclasses import dataclass
import numpy as np
from PIL import Image
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class Rotate(Effect):
"""
Rotates the specified clip by ``angle`` degrees (or radians) anticlockwise
If the angle is not a multiple of 90 (degrees) or ``center``, ``translate``,
and ``bg_color`` are not ``None``, there will be black borders.
You can make them transparent with:
>>> new_clip = clip.with_mask().rotate(72)
Parameters
----------
clip : VideoClip
A video clip.
angle : float
Either a value or a function angle(t) representing the angle of rotation.
unit : str, optional
Unit of parameter `angle` (either "deg" for degrees or "rad" for radians).
resample : str, optional
An optional resampling filter. One of "nearest", "bilinear", or "bicubic".
expand : bool, optional
If true, expands the output image to make it large enough to hold the
entire rotated image. If false or omitted, make the output image the same
size as the input image.
translate : tuple, optional
An optional post-rotate translation (a 2-tuple).
center : tuple, optional
Optional center of rotation (a 2-tuple). Origin is the upper left corner.
bg_color : tuple, optional
An optional color for area outside the rotated image. Only has effect if
``expand`` is true.
"""
angle: float
unit: str = "deg"
resample: str = "bicubic"
expand: bool = True
center: tuple = None
translate: tuple = None
bg_color: tuple = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
try:
resample = {
"bilinear": Image.BILINEAR,
"nearest": Image.NEAREST,
"bicubic": Image.BICUBIC,
}[self.resample]
except KeyError:
raise ValueError(
"'resample' argument must be either 'bilinear', 'nearest' or 'bicubic'"
)
if hasattr(self.angle, "__call__"):
get_angle = self.angle
else:
get_angle = lambda t: self.angle
def filter(get_frame, t):
angle = get_angle(t)
im = get_frame(t)
if self.unit == "rad":
angle = math.degrees(angle)
angle %= 360
if not self.center and not self.translate and not self.bg_color:
if (angle == 0) and self.expand:
return im
if (angle == 90) and self.expand:
transpose = [1, 0] if len(im.shape) == 2 else [1, 0, 2]
return np.transpose(im, axes=transpose)[::-1]
elif (angle == 270) and self.expand:
transpose = [1, 0] if len(im.shape) == 2 else [1, 0, 2]
return np.transpose(im, axes=transpose)[:, ::-1]
elif (angle == 180) and self.expand:
return im[::-1, ::-1]
pillow_kwargs = {}
if self.bg_color is not None:
pillow_kwargs["fillcolor"] = self.bg_color
if self.center is not None:
pillow_kwargs["center"] = self.center
if self.translate is not None:
pillow_kwargs["translate"] = self.translate
# PIL expects uint8 type data. However a mask image has values in the
# range [0, 1] and is of float type. To handle this we scale it up by
# a factor 'a' for use with PIL and then back again by 'a' afterwards.
if im.dtype == "float64":
# this is a mask image
a = 255.0
else:
a = 1
# call PIL.rotate
return (
np.array(
Image.fromarray(np.array(a * im).astype(np.uint8)).rotate(
angle, expand=self.expand, resample=resample, **pillow_kwargs
)
)
/ a
)
return clip.transform(filter, apply_to=["mask"])

View File

@@ -0,0 +1,57 @@
from moviepy.Effect import Effect
class Scroll(Effect):
"""Effect that scrolls horizontally or vertically a clip, e.g. to make end credits
Parameters
----------
w, h
The width and height of the final clip. Default to clip.w and clip.h
x_speed, y_speed
The speed of the scroll in the x and y directions.
x_start, y_start
The starting position of the scroll in the x and y directions.
apply_to
Whether to apply the effect to the mask too.
"""
def __init__(
self,
w=None,
h=None,
x_speed=0,
y_speed=0,
x_start=0,
y_start=0,
apply_to="mask",
):
self.w = w
self.h = h
self.x_speed = x_speed
self.y_speed = y_speed
self.x_start = x_start
self.y_start = y_start
self.apply_to = apply_to
def apply(self, clip):
"""Apply the effect to the clip."""
if self.h is None:
self.h = clip.h
if self.w is None:
self.w = clip.w
x_max = self.w - 1
y_max = self.h - 1
def filter(get_frame, t):
x = int(max(0, min(x_max, self.x_start + round(self.x_speed * t))))
y = int(max(0, min(y_max, self.y_start + round(self.y_speed * t))))
return get_frame(t)[y : y + self.h, x : x + self.w]
return clip.transform(filter, apply_to=self.apply_to)

View File

@@ -0,0 +1,60 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class SlideIn(Effect):
"""Makes the clip arrive from one side of the screen.
Only works when the clip is included in a CompositeVideoClip,
and if the clip has the same size as the whole composition.
Parameters
----------
clip : moviepy.Clip.Clip
A video clip.
duration : float
Time taken for the clip to be fully visible
side : str
Side of the screen where the clip comes from. One of
'top', 'bottom', 'left' or 'right'.
Examples
--------
.. code:: python
from moviepy import *
clips = [... make a list of clips]
slided_clips = [
CompositeVideoClip([clip.with_effects([vfx.SlideIn(1, "left")])])
for clip in clips
]
final_clip = concatenate_videoclips(slided_clips, padding=-1)
clip = ColorClip(
color=(255, 0, 0), duration=1, size=(300, 300)
).with_fps(60)
final_clip = CompositeVideoClip([clip.with_effects([vfx.SlideIn(1, "right")])])
"""
duration: float
side: str
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
w, h = clip.size
pos_dict = {
"left": lambda t: (min(0, w * (t / self.duration - 1)), "center"),
"right": lambda t: (max(0, w * (1 - t / self.duration)), "center"),
"top": lambda t: ("center", min(0, h * (t / self.duration - 1))),
"bottom": lambda t: ("center", max(0, h * (1 - t / self.duration))),
}
return clip.with_position(pos_dict[self.side])

View File

@@ -0,0 +1,64 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class SlideOut(Effect):
"""Makes the clip goes away by one side of the screen.
Only works when the clip is included in a CompositeVideoClip,
and if the clip has the same size as the whole composition.
Parameters
----------
clip : moviepy.Clip.Clip
A video clip.
duration : float
Time taken for the clip to be fully visible
side : str
Side of the screen where the clip goes. One of
'top', 'bottom', 'left' or 'right'.
Examples
--------
.. code:: python
from moviepy import *
clips = [... make a list of clips]
slided_clips = [
CompositeVideoClip([clip.with_effects([vfx.SlideOut(1, "left")])])
for clip in clips
]
final_clip = concatenate_videoclips(slided_clips, padding=-1)
clip = ColorClip(
color=(255, 0, 0), duration=1, size=(300, 300)
).with_fps(60)
final_clip = CompositeVideoClip([clip.with_effects([vfx.SlideOut(1, "right")])])
"""
duration: float
side: str
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
w, h = clip.size
ts = clip.duration - self.duration # start time of the effect.
pos_dict = {
"left": lambda t: (min(0, w * (-(t - ts) / self.duration)), "center"),
"right": lambda t: (max(0, w * ((t - ts) / self.duration)), "center"),
"top": lambda t: ("center", min(0, h * (-(t - ts) / self.duration))),
"bottom": lambda t: ("center", max(0, h * ((t - ts) / self.duration))),
}
return clip.with_position(pos_dict[self.side])

View File

@@ -0,0 +1,29 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class SuperSample(Effect):
"""Replaces each frame at time t by the mean of `n_frames` equally spaced frames
taken in the interval [t-d, t+d]. This results in motion blur.
"""
d: float
n_frames: int
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
def filter(get_frame, t):
timings = np.linspace(t - self.d, t + self.d, self.n_frames)
frame_average = np.mean(
1.0 * np.array([get_frame(t_) for t_ in timings], dtype="uint16"),
axis=0,
)
return frame_average.astype("uint8")
return clip.transform(filter)

View File

@@ -0,0 +1,20 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class TimeMirror(Effect):
"""
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.
"""
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
return clip[::-1]

View File

@@ -0,0 +1,22 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class TimeSymmetrize(Effect):
"""
Returns a clip that plays the current clip once forwards and
then once backwards. This is very practival to make video that
loop well, e.g. to create animated GIFs.
This effect is automatically applied to the clip's mask and audio
if they exist.
"""
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
return clip + clip[::-1]

View File

@@ -0,0 +1,76 @@
"""All the visual effects that can be applied to VideoClip."""
# import every video fx function
from moviepy.video.fx.AccelDecel import AccelDecel
from moviepy.video.fx.BlackAndWhite import BlackAndWhite
from moviepy.video.fx.Blink import Blink
from moviepy.video.fx.Crop import Crop
from moviepy.video.fx.CrossFadeIn import CrossFadeIn
from moviepy.video.fx.CrossFadeOut import CrossFadeOut
from moviepy.video.fx.EvenSize import EvenSize
from moviepy.video.fx.FadeIn import FadeIn
from moviepy.video.fx.FadeOut import FadeOut
from moviepy.video.fx.Freeze import Freeze
from moviepy.video.fx.FreezeRegion import FreezeRegion
from moviepy.video.fx.GammaCorrection import GammaCorrection
from moviepy.video.fx.HeadBlur import HeadBlur
from moviepy.video.fx.InvertColors import InvertColors
from moviepy.video.fx.Loop import Loop
from moviepy.video.fx.LumContrast import LumContrast
from moviepy.video.fx.MakeLoopable import MakeLoopable
from moviepy.video.fx.Margin import Margin
from moviepy.video.fx.MaskColor import MaskColor
from moviepy.video.fx.MasksAnd import MasksAnd
from moviepy.video.fx.MasksOr import MasksOr
from moviepy.video.fx.MirrorX import MirrorX
from moviepy.video.fx.MirrorY import MirrorY
from moviepy.video.fx.MultiplyColor import MultiplyColor
from moviepy.video.fx.MultiplySpeed import MultiplySpeed
from moviepy.video.fx.Painting import Painting
from moviepy.video.fx.Resize import Resize
from moviepy.video.fx.Rotate import Rotate
from moviepy.video.fx.Scroll import Scroll
from moviepy.video.fx.SlideIn import SlideIn
from moviepy.video.fx.SlideOut import SlideOut
from moviepy.video.fx.SuperSample import SuperSample
from moviepy.video.fx.TimeMirror import TimeMirror
from moviepy.video.fx.TimeSymmetrize import TimeSymmetrize
__all__ = (
"AccelDecel",
"BlackAndWhite",
"Blink",
"Crop",
"CrossFadeIn",
"CrossFadeOut",
"EvenSize",
"FadeIn",
"FadeOut",
"Freeze",
"FreezeRegion",
"GammaCorrection",
"HeadBlur",
"InvertColors",
"Loop",
"LumContrast",
"MakeLoopable",
"Margin",
"MasksAnd",
"MaskColor",
"MasksOr",
"MirrorX",
"MirrorY",
"MultiplyColor",
"MultiplySpeed",
"Painting",
"Resize",
"Rotate",
"Scroll",
"SlideIn",
"SlideOut",
"SuperSample",
"TimeMirror",
"TimeSymmetrize",
)

View File

@@ -0,0 +1,167 @@
"""Implements ImageSequenceClip, a class to create a video clip from a set
of image files.
"""
import os
import numpy as np
from imageio.v2 import imread
from moviepy.video.VideoClip import VideoClip
class ImageSequenceClip(VideoClip):
"""A VideoClip made from a series of images.
Parameters
----------
sequence
Can be one of these:
- The name of a folder (containing only pictures). The pictures
will be considered in alphanumerical order.
- A list of names of image files. In this case you can choose to
load the pictures in memory pictures
- A list of Numpy arrays representing images. In this last case,
masks are not supported currently.
fps
Number of picture frames to read per second. Instead, you can provide
the duration of each image with durations (see below)
durations
List of the duration of each picture.
with_mask
Should the alpha layer of PNG images be considered as a mask ?
is_mask
Will this sequence of pictures be used as an animated mask.
load_images
Specify that all images should be loaded into the RAM. This is only
interesting if you have a small number of images that will be used
more than once.
"""
def __init__(
self,
sequence,
fps=None,
durations=None,
with_mask=True,
is_mask=False,
load_images=False,
):
# CODE WRITTEN AS IT CAME, MAY BE IMPROVED IN THE FUTURE
if (fps is None) and (durations is None):
raise ValueError("Please provide either 'fps' or 'durations'.")
VideoClip.__init__(self, is_mask=is_mask)
# Parse the data
fromfiles = True
if isinstance(sequence, list):
if isinstance(sequence[0], str):
if load_images:
sequence = [imread(file) for file in sequence]
fromfiles = False
else:
fromfiles = True
else:
# sequence is already a list of numpy arrays
fromfiles = False
else:
# sequence is a folder name, make it a list of files:
fromfiles = True
sequence = sorted(
[os.path.join(sequence, file) for file in os.listdir(sequence)]
)
# check that all the images are of the same size
if isinstance(sequence[0], str):
size = imread(sequence[0]).shape
else:
size = sequence[0].shape
for image in sequence:
image1 = image
if isinstance(image, str):
image1 = imread(image)
if size != image1.shape:
raise Exception(
"MoviePy: ImageSequenceClip requires all images to be the same size"
)
self.fps = fps
if fps is not None:
durations = [1.0 / fps for image in sequence]
self.images_starts = [
1.0 * i / fps - np.finfo(np.float32).eps for i in range(len(sequence))
]
else:
self.images_starts = [0] + list(np.cumsum(durations))
self.durations = durations
self.duration = sum(durations)
self.end = self.duration
self.sequence = sequence
if fps is None:
self.fps = self.duration / len(sequence)
def find_image_index(t):
return max(
[i for i in range(len(self.sequence)) if self.images_starts[i] <= t]
)
if fromfiles:
self.last_index = None
self.last_image = None
def frame_function(t):
index = find_image_index(t)
if index != self.last_index:
self.last_image = imread(self.sequence[index])[:, :, :3]
self.last_index = index
return self.last_image
if with_mask and (imread(self.sequence[0]).shape[2] == 4):
self.mask = VideoClip(is_mask=True)
self.mask.last_index = None
self.mask.last_image = None
def mask_frame_function(t):
index = find_image_index(t)
if index != self.mask.last_index:
frame = imread(self.sequence[index])[:, :, 3]
self.mask.last_image = frame.astype(float) / 255
self.mask.last_index = index
return self.mask.last_image
self.mask.frame_function = mask_frame_function
self.mask.size = mask_frame_function(0).shape[:2][::-1]
else:
def frame_function(t):
index = find_image_index(t)
return self.sequence[index][:, :, :3]
if with_mask and (self.sequence[0].shape[2] == 4):
self.mask = VideoClip(is_mask=True)
def mask_frame_function(t):
index = find_image_index(t)
return 1.0 * self.sequence[index][:, :, 3] / 255
self.mask.frame_function = mask_frame_function
self.mask.size = mask_frame_function(0).shape[:2][::-1]
self.frame_function = frame_function
self.size = frame_function(0).shape[:2][::-1]

View File

@@ -0,0 +1,175 @@
"""Implements VideoFileClip, a class for video clips creation using video files."""
from moviepy.audio.io.AudioFileClip import AudioFileClip
from moviepy.decorators import convert_path_to_string
from moviepy.video.io.ffmpeg_reader import FFMPEG_VideoReader
from moviepy.video.VideoClip import VideoClip
class VideoFileClip(VideoClip):
"""A video clip originating from a movie file. For instance:
.. code:: python
clip = VideoFileClip("myHolidays.mp4")
clip.close()
with VideoFileClip("myMaskVideo.avi") as clip2:
pass # Implicit close called by context manager.
Parameters
----------
filename:
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.
Video files rarely contain masks, but some video codecs enable
that. For instance if you have a MoviePy VideoClip with a mask you
can save it to a videofile with a mask. (see also
``VideoClip.write_videofile`` for more details).
audio:
Set to `False` if the clip doesn't have any audio or if you do not
wish to read the audio.
target_resolution:
Set to (desired_width, desired_height) to have ffmpeg resize the frames
before returning them. This is much faster than streaming in high-res
and then resizing. If either dimension is None, the frames are resized
by keeping the existing aspect ratio.
resize_algorithm:
The algorithm used for resizing. Default: "bicubic", other popular
options include "bilinear" and "fast_bilinear". For more information, see
https://ffmpeg.org/ffmpeg-scaler.html
fps_source:
The fps value to collect from the metadata. Set by default to 'fps', but
can be set to 'tbr', which may be helpful if you are finding that it is reading
the incorrect fps from the file.
pixel_format
Optional: Pixel format for the video to read. If is not specified
'rgb24' will be used as the default format unless ``has_mask`` is set
as ``True``, then 'rgba' will be used.
is_mask
`True` if the clip is going to be used as a mask.
Attributes
----------
filename:
Name of the original video file.
fps:
Frames per second in the original file.
Read docs for Clip() and VideoClip() for other, more generic, attributes.
Lifetime
--------
Note that this creates subprocesses and locks files. If you construct one
of these instances, you must call close() afterwards, or the subresources
will not be cleaned up until the process ends.
If copies are made, and close() is called on one, it may cause methods on
the other copies to fail.
"""
@convert_path_to_string("filename")
def __init__(
self,
filename,
decode_file=False,
has_mask=False,
audio=True,
audio_buffersize=200000,
target_resolution=None,
resize_algorithm="bicubic",
audio_fps=44100,
audio_nbytes=2,
fps_source="fps",
pixel_format=None,
is_mask=False,
):
VideoClip.__init__(self, is_mask=is_mask)
# Make a reader
if not pixel_format:
pixel_format = "rgba" if has_mask else "rgb24"
self.reader = FFMPEG_VideoReader(
filename,
decode_file=decode_file,
pixel_format=pixel_format,
target_resolution=target_resolution,
resize_algo=resize_algorithm,
fps_source=fps_source,
)
# Make some of the reader's attributes accessible from the clip
self.duration = self.reader.duration
self.end = self.reader.duration
self.fps = self.reader.fps
self.size = self.reader.size
self.rotation = self.reader.rotation
self.filename = filename
if has_mask:
self.frame_function = lambda t: self.reader.get_frame(t)[:, :, :3]
def mask_frame_function(t):
return self.reader.get_frame(t)[:, :, 3] / 255.0
self.mask = VideoClip(
is_mask=True, frame_function=mask_frame_function
).with_duration(self.duration)
self.mask.fps = self.fps
else:
self.frame_function = lambda t: self.reader.get_frame(t)
# Make a reader for the audio, if any.
if audio and self.reader.infos["audio_found"]:
self.audio = AudioFileClip(
filename,
buffersize=audio_buffersize,
fps=audio_fps,
nbytes=audio_nbytes,
)
def __deepcopy__(self, memo):
"""Implements ``copy.deepcopy(clip)`` behaviour as ``copy.copy(clip)``.
VideoFileClip class instances can't be deeply copied because the locked Thread
of ``proc`` isn't pickleable. Without this override, calls to
``copy.deepcopy(clip)`` would raise a ``TypeError``:
```
TypeError: cannot pickle '_thread.lock' object
```
"""
return self.__copy__()
def close(self):
"""Close the internal reader."""
if self.reader:
self.reader.close()
self.reader = None
try:
if self.audio:
self.audio.close()
self.audio = None
except AttributeError: # pragma: no cover
pass

View File

@@ -0,0 +1 @@
"""Classes and methods for reading, writing and previewing video files."""

View File

@@ -0,0 +1,284 @@
"""Implements ``display_in_notebook``, a function to embed images/videos/audio in the
Jupyter Notebook.
"""
# Notes:
# All media are physically embedded in the Jupyter Notebook
# (instead of simple links to the original files)
# That is because most browsers use a cache system and they won't
# properly refresh the media when the original files are changed.
import inspect
import os
from base64 import b64encode
from moviepy.audio.AudioClip import AudioClip
from moviepy.tools import extensions_dict
from moviepy.video.io.ffmpeg_reader import ffmpeg_parse_infos
from moviepy.video.VideoClip import ImageClip, VideoClip
try: # pragma: no cover
from IPython.display import HTML
ipython_available = True
class HTML2(HTML): # noqa D101
def __add__(self, other):
return HTML2(self.data + other.data)
except ImportError:
def HTML2(content): # noqa D103
return content
ipython_available = False
sorry = "Sorry, seems like your browser doesn't support HTML5 audio/video"
templates = {
"audio": (
"<audio controls>"
"<source %(options)s src='data:audio/%(ext)s;base64,%(data)s'>"
+ sorry
+ "</audio>"
),
"image": "<img %(options)s src='data:image/%(ext)s;base64,%(data)s'>",
"video": (
"<video %(options)s"
"src='data:video/%(ext)s;base64,%(data)s' controls>" + sorry + "</video>"
),
}
def html_embed(
clip, filetype=None, maxduration=60, rd_kwargs=None, center=True, **html_kwargs
):
"""Returns HTML5 code embedding the clip.
Parameters
----------
clip : moviepy.Clip.Clip
Either a file name, or a clip to preview.
Either an image, a sound or a video. Clips will actually be
written to a file and embedded as if a filename was provided.
filetype : str, optional
One of 'video','image','audio'. If None is given, it is determined
based on the extension of ``filename``, but this can bug.
maxduration : float, optional
An error will be raised if the clip's duration is more than the indicated
value (in seconds), to avoid spoiling the browser's cache and the RAM.
rd_kwargs : dict, optional
Keyword arguments for the rendering, like ``dict(fps=15, bitrate="50k")``.
Allow you to give some options to the render process. You can, for
example, disable the logger bar passing ``dict(logger=None)``.
center : bool, optional
If true (default), the content will be wrapped in a
``<div align=middle>`` HTML container, so the content will be displayed
at the center.
html_kwargs
Allow you to give some options, like ``width=260``, ``autoplay=True``,
``loop=1`` etc.
Examples
--------
.. code:: python
from moviepy import *
# later ...
html_embed(clip, width=360)
html_embed(clip.audio)
clip.write_gif("test.gif")
html_embed('test.gif')
clip.save_frame("first_frame.jpeg")
html_embed("first_frame.jpeg")
"""
if rd_kwargs is None: # pragma: no cover
rd_kwargs = {}
if "Clip" in str(clip.__class__):
TEMP_PREFIX = "__temp__"
if isinstance(clip, ImageClip):
filename = TEMP_PREFIX + ".png"
kwargs = {"filename": filename, "with_mask": True}
argnames = inspect.getfullargspec(clip.save_frame).args
kwargs.update(
{key: value for key, value in rd_kwargs.items() if key in argnames}
)
clip.save_frame(**kwargs)
elif isinstance(clip, VideoClip):
filename = TEMP_PREFIX + ".mp4"
kwargs = {"filename": filename, "preset": "ultrafast"}
kwargs.update(rd_kwargs)
clip.write_videofile(**kwargs)
elif isinstance(clip, AudioClip):
filename = TEMP_PREFIX + ".mp3"
kwargs = {"filename": filename}
kwargs.update(rd_kwargs)
clip.write_audiofile(**kwargs)
else:
raise ValueError("Unknown class for the clip. Cannot embed and preview.")
return html_embed(
filename,
maxduration=maxduration,
rd_kwargs=rd_kwargs,
center=center,
**html_kwargs,
)
filename = clip
options = " ".join(["%s='%s'" % (str(k), str(v)) for k, v in html_kwargs.items()])
name, ext = os.path.splitext(filename)
ext = ext[1:]
if filetype is None:
ext = filename.split(".")[-1].lower()
if ext == "gif":
filetype = "image"
elif ext in extensions_dict:
filetype = extensions_dict[ext]["type"]
else:
raise ValueError(
"No file type is known for the provided file. Please provide "
"argument `filetype` (one of 'image', 'video', 'sound') to the "
"display_in_notebook function."
)
if filetype == "video":
# The next lines set the HTML5-cvompatible extension and check that the
# extension is HTML5-valid
exts_htmltype = {"mp4": "mp4", "webm": "webm", "ogv": "ogg"}
allowed_exts = " ".join(exts_htmltype.keys())
try:
ext = exts_htmltype[ext]
except Exception:
raise ValueError(
"This video extension cannot be displayed in the "
"Jupyter Notebook. Allowed extensions: " + allowed_exts
)
if filetype in ["audio", "video"]:
duration = ffmpeg_parse_infos(filename, decode_file=True)["duration"]
if duration > maxduration:
raise ValueError(
(
"The duration of video %s (%.1f) exceeds the 'maxduration'"
" attribute. You can increase 'maxduration', by passing"
" 'maxduration' parameter to display_in_notebook function."
" But note that embedding large videos may take all the memory"
" away!"
)
% (filename, duration)
)
with open(filename, "rb") as file:
data = b64encode(file.read()).decode("utf-8")
template = templates[filetype]
result = template % {"data": data, "options": options, "ext": ext}
if center:
result = r"<div align=middle>%s</div>" % result
return result
def display_in_notebook(
clip,
filetype=None,
maxduration=60,
t=None,
fps=None,
rd_kwargs=None,
center=True,
**html_kwargs,
):
"""Displays clip content in an Jupyter Notebook.
Remarks: If your browser doesn't support HTML5, this should warn you.
If nothing is displayed, maybe your file or filename is wrong.
Important: The media will be physically embedded in the notebook.
Parameters
----------
clip : moviepy.Clip.Clip
Either the name of a file, or a clip to preview. The clip will actually
be written to a file and embedded as if a filename was provided.
filetype : str, optional
One of ``"video"``, ``"image"`` or ``"audio"``. If None is given, it is
determined based on the extension of ``filename``, but this can bug.
maxduration : float, optional
An error will be raised if the clip's duration is more than the indicated
value (in seconds), to avoid spoiling the browser's cache and the RAM.
t : float, optional
If not None, only the frame at time t will be displayed in the notebook,
instead of a video of the clip.
fps : int, optional
Enables to specify an fps, as required for clips whose fps is unknown.
rd_kwargs : dict, optional
Keyword arguments for the rendering, like ``dict(fps=15, bitrate="50k")``.
Allow you to give some options to the render process. You can, for
example, disable the logger bar passing ``dict(logger=None)``.
center : bool, optional
If true (default), the content will be wrapped in a
``<div align=middle>`` HTML container, so the content will be displayed
at the center.
kwargs
Allow you to give some options, like ``width=260``, etc. When editing
looping gifs, a good choice is ``loop=1, autoplay=1``.
Examples
--------
.. code:: python
from moviepy import *
# later ...
clip.display_in_notebook(width=360)
clip.audio.display_in_notebook()
clip.write_gif("test.gif")
display_in_notebook('test.gif')
clip.save_frame("first_frame.jpeg")
display_in_notebook("first_frame.jpeg")
"""
if not ipython_available:
raise ImportError("Only works inside an Jupyter Notebook")
if rd_kwargs is None:
rd_kwargs = {}
if fps is not None:
rd_kwargs["fps"] = fps
if t is not None:
clip = clip.to_ImageClip(t)
return HTML2(
html_embed(
clip,
filetype=filetype,
maxduration=maxduration,
center=center,
rd_kwargs=rd_kwargs,
**html_kwargs,
)
)

View File

@@ -0,0 +1,882 @@
"""Implements all the functions to read a video or a picture using ffmpeg."""
import os
import re
import subprocess as sp
import warnings
from log import log_step
import numpy as np
from moviepy.config import FFMPEG_BINARY # ffmpeg, ffmpeg.exe, etc...
from moviepy.tools import (
convert_to_seconds,
cross_platform_popen_params,
ffmpeg_escape_filename,
)
class FFMPEG_VideoReader:
"""Class for video byte-level reading with ffmpeg."""
def __init__(
self,
filename,
decode_file=True,
print_infos=False,
bufsize=None,
pixel_format="rgb24",
check_duration=True,
target_resolution=None,
resize_algo="bicubic",
fps_source="fps",
):
self.filename = filename
self.proc = None
infos = ffmpeg_parse_infos(
filename,
check_duration=check_duration,
fps_source=fps_source,
decode_file=decode_file,
print_infos=print_infos,
)
# If framerate is unavailable, assume 1.0 FPS to avoid divide-by-zero errors.
self.fps = infos.get("video_fps", 1.0)
# If frame size is unavailable, set 1x1 divide-by-zero errors.
self.size = infos.get("video_size", (1, 1))
# ffmpeg automatically rotates videos if rotation information is
# available, so exchange width and height
self.rotation = abs(infos.get("video_rotation", 0))
if self.rotation in [90, 270]:
self.size = [self.size[1], self.size[0]]
if target_resolution:
if None in target_resolution:
ratio = 1
for idx, target in enumerate(target_resolution):
if target:
ratio = target / self.size[idx]
self.size = (int(self.size[0] * ratio), int(self.size[1] * ratio))
else:
self.size = target_resolution
self.resize_algo = resize_algo
self.duration = infos.get("video_duration", 0.0)
self.ffmpeg_duration = infos.get("duration", 0.0)
self.n_frames = infos.get("video_n_frames", 0)
self.bitrate = infos.get("video_bitrate", 0)
self.infos = infos
self.pixel_format = pixel_format
self.depth = 4 if pixel_format[-1] == "a" else 3
# 'a' represents 'alpha' which means that each pixel has 4 values instead of 3.
# See https://github.com/Zulko/moviepy/issues/1070#issuecomment-644457274
if bufsize is None:
w, h = self.size
bufsize = self.depth * w * h + 100
self.bufsize = bufsize
self.initialize()
def initialize(self, start_time=0):
"""
Opens the file, creates the pipe.
Sets self.pos to the appropriate value (1 if start_time == 0 because
it pre-reads the first frame).
"""
self.close(delete_lastread=False) # if any
if start_time != 0:
offset = min(1, start_time)
i_arg = [
"-ss",
"%.06f" % (start_time - offset),
"-i",
ffmpeg_escape_filename(self.filename),
"-ss",
"%.06f" % offset,
]
else:
i_arg = ["-i", ffmpeg_escape_filename(self.filename)]
# For webm video (vp8 and vp9) with transparent layer, force libvpx/libvpx-vp9
# as ffmpeg native webm decoder dont decode alpha layer
# (see
# https://www.reddit.com/r/ffmpeg/comments/fgpyfb/help_with_webm_with_alpha_channel/
# )
if self.depth == 4:
codec_name = self.infos.get("video_codec_name")
if codec_name == "vp9":
i_arg = ["-c:v", "libvpx-vp9"] + i_arg
elif codec_name == "vp8":
i_arg = ["-c:v", "libvpx"] + i_arg
# print(self.infos)
log_step("init", 100, self.infos)
cmd = (
[FFMPEG_BINARY]
+ i_arg
+ [
"-loglevel",
"error",
"-f",
"image2pipe",
"-vf",
"scale=%d:%d" % tuple(self.size),
"-sws_flags",
self.resize_algo,
"-pix_fmt",
self.pixel_format,
"-vcodec",
"rawvideo",
"-",
]
)
# print(" ".join(cmd))
popen_params = cross_platform_popen_params(
{
"bufsize": self.bufsize,
"stdout": sp.PIPE,
"stderr": sp.PIPE,
"stdin": sp.DEVNULL,
}
)
self.proc = sp.Popen(cmd, **popen_params)
# self.pos represents the (0-indexed) index of the frame that is next in line
# to be read by self.read_frame().
# Eg when self.pos is 1, the 2nd frame will be read next.
self.pos = self.get_frame_number(start_time)
self.last_read = self.read_frame()
def skip_frames(self, n=1):
"""Reads and throws away n frames"""
w, h = self.size
for i in range(n):
self.proc.stdout.read(self.depth * w * h)
# self.proc.stdout.flush()
self.pos += n
def read_frame(self):
"""
Reads the next frame from the file.
Note that upon (re)initialization, the first frame will already have been read
and stored in ``self.last_read``.
"""
w, h = self.size
nbytes = self.depth * w * h
s = self.proc.stdout.read(nbytes)
if len(s) != nbytes:
warnings.warn(
(
"In file %s, %d bytes wanted but %d bytes read at frame index"
" %d (out of a total %d frames), at time %.02f/%.02f sec."
" Using the last valid frame instead."
)
% (
self.filename,
nbytes,
len(s),
self.pos,
self.n_frames,
1.0 * self.pos / self.fps,
self.duration,
),
UserWarning,
)
if not hasattr(self, "last_read"):
raise IOError(
(
"MoviePy error: failed to read the first frame of "
f"video file {self.filename}. That might mean that the file is "
"corrupted. That may also mean that you are using "
"a deprecated version of FFMPEG. On Ubuntu/Debian "
"for instance the version in the repos is deprecated. "
"Please update to a recent version from the website."
)
)
result = self.last_read
else:
if hasattr(np, "frombuffer"):
result = np.frombuffer(s, dtype="uint8")
else:
result = np.fromstring(s, dtype="uint8")
result.shape = (h, w, len(s) // (w * h)) # reshape((h, w, len(s)//(w*h)))
self.last_read = result
# We have to do this down here because `self.pos` is used in the warning above
self.pos += 1
return result
def get_frame(self, t):
"""Read a file video frame at time t.
Note for coders: getting an arbitrary frame in the video with
ffmpeg can be painfully slow if some decoding has to be done.
This function tries to avoid fetching arbitrary frames
whenever possible, by moving between adjacent frames.
"""
# + 1 so that it represents the frame position that it will be
# after the frame is read. This makes the later comparisons easier.
pos = self.get_frame_number(t) + 1
# Initialize proc if it is not open
if not self.proc:
# raise Exception("Proc not detected")
self.initialize(t)
return self.last_read
if pos == self.pos:
return self.last_read
elif (pos < self.pos) or (pos > self.pos + 100):
# We can't just skip forward to `pos` or it would take too long
self.initialize(t)
return self.last_read
else:
# If pos == self.pos + 1, this line has no effect
self.skip_frames(pos - self.pos - 1)
result = self.read_frame()
return result
@property
def lastread(self):
"""Alias of `self.last_read` for backwards compatibility with MoviePy 1.x."""
return self.last_read
def get_frame_number(self, t):
"""Helper method to return the frame number at time ``t``"""
# I used this horrible '+0.00001' hack because sometimes due to numerical
# imprecisions a 3.0 can become a 2.99999999... which makes the int()
# go to the previous integer. This makes the fetching more robust when you
# are getting the nth frame by writing get_frame(n/fps).
return int(self.fps * t + 0.00001)
def close(self, delete_lastread=True):
"""Closes the reader terminating the process, if is still open."""
if self.proc:
if self.proc.poll() is None:
self.proc.terminate()
self.proc.stdout.close()
self.proc.stderr.close()
self.proc.wait()
self.proc = None
if delete_lastread and hasattr(self, "last_read"):
del self.last_read
def __del__(self):
self.close()
def ffmpeg_read_image(filename, with_mask=True, pixel_format=None):
"""Read an image file (PNG, BMP, JPEG...).
Wraps FFMPEG_Videoreader to read just one image.
Returns an ImageClip.
This function is not meant to be used directly in MoviePy.
Use ImageClip instead to make clips out of image files.
Parameters
----------
filename
Name of the image file. Can be of any format supported by ffmpeg.
with_mask
If the image has a transparency layer, ``with_mask=true`` will save
this layer as the mask of the returned ImageClip
pixel_format
Optional: Pixel format for the image to read. If is not specified
'rgb24' will be used as the default format unless ``with_mask`` is set
as ``True``, then 'rgba' will be used.
"""
if not pixel_format:
pixel_format = "rgba" if with_mask else "rgb24"
reader = FFMPEG_VideoReader(
filename, pixel_format=pixel_format, check_duration=False
)
im = reader.last_read
del reader
return im
class FFmpegInfosParser:
"""Finite state ffmpeg `-i` command option file information parser.
Is designed to parse the output fast, in one loop. Iterates line by
line of the `ffmpeg -i <filename> [-f null -]` command output changing
the internal state of the parser.
Parameters
----------
filename
Name of the file parsed, only used to raise accurate error messages.
infos
Information returned by FFmpeg.
fps_source
Indicates what source data will be preferably used to retrieve fps data.
check_duration
Enable or disable the parsing of the duration of the file. Useful to
skip the duration check, for example, for images.
decode_file
Indicates if the whole file has been decoded. The duration parsing strategy
will differ depending on this argument.
"""
def __init__(
self,
infos,
filename,
fps_source="fps",
check_duration=True,
decode_file=False,
):
self.infos = infos
self.filename = filename
self.check_duration = check_duration
self.fps_source = fps_source
self.duration_tag_separator = "time=" if decode_file else "Duration: "
self._reset_state()
def _reset_state(self):
"""Reinitializes the state of the parser. Used internally at
initialization and at the end of the parsing process.
"""
# could be 2 possible types of metadata:
# - file_metadata: Metadata of the container. Here are the tags set
# by the user using `-metadata` ffmpeg option
# - stream_metadata: Metadata for each stream of the container.
self._inside_file_metadata = False
# this state is needed if `duration_tag_separator == "time="` because
# execution of ffmpeg decoding the whole file using `-f null -` appends
# to the output the blocks "Stream mapping:" and "Output:", which
# should be ignored
self._inside_output = False
# flag which indicates that a default stream has not been found yet
self._default_stream_found = False
# current input file, stream and chapter, which will be built at runtime
self._current_input_file = {"streams": []}
self._current_stream = None
self._current_chapter = None
# resulting data of the parsing process
self.result = {
"video_found": False,
"audio_found": False,
"metadata": {},
"inputs": [],
}
# keep the value of latest metadata value parsed so we can build
# at next lines a multiline metadata value
self._last_metadata_field_added = None
def parse(self):
"""Parses the information returned by FFmpeg in stderr executing their binary
for a file with ``-i`` option and returns a dictionary with all data needed
by MoviePy.
"""
# chapters by input file
input_chapters = []
for line in self.infos.splitlines()[1:]:
if (
self.duration_tag_separator == "time="
and self.check_duration
and "time=" in line
):
# parse duration using file decodification
self.result["duration"] = self.parse_duration(line)
elif self._inside_output or line[0] != " ":
if self.duration_tag_separator == "time=" and not self._inside_output:
self._inside_output = True
# skip lines like "At least one output file must be specified"
elif not self._inside_file_metadata and line.startswith(" Metadata:"):
# enter " Metadata:" group
self._inside_file_metadata = True
elif line.startswith(" Duration:"):
# exit " Metadata:" group
self._inside_file_metadata = False
if self.check_duration and self.duration_tag_separator == "Duration: ":
self.result["duration"] = self.parse_duration(line)
# parse global bitrate (in kb/s)
bitrate_match = re.search(r"bitrate: (\d+) kb/s", line)
self.result["bitrate"] = (
int(bitrate_match.group(1)) if bitrate_match else None
)
# parse start time (in seconds)
start_match = re.search(r"start: (\d+\.?\d+)", line)
self.result["start"] = (
float(start_match.group(1)) if start_match else None
)
elif self._inside_file_metadata:
# file metadata line
field, value = self.parse_metadata_field_value(line)
# multiline metadata value parsing
if field == "":
field = self._last_metadata_field_added
value = self.result["metadata"][field] + "\n" + value
else:
self._last_metadata_field_added = field
self.result["metadata"][field] = value
elif line.lstrip().startswith("Stream "):
# exit stream " Metadata:"
if self._current_stream:
self._current_input_file["streams"].append(self._current_stream)
# get input number, stream number, language and type
main_info_match = re.search(
r"^Stream\s#(\d+):(\d+)(?:\[\w+\])?\(?(\w+)?\)?:\s(\w+):",
line.lstrip(),
)
(
input_number,
stream_number,
language,
stream_type,
) = main_info_match.groups()
input_number = int(input_number)
stream_number = int(stream_number)
stream_type_lower = stream_type.lower()
if language == "und":
language = None
# start builiding the current stream
self._current_stream = {
"input_number": input_number,
"stream_number": stream_number,
"stream_type": stream_type_lower,
"language": language,
"default": not self._default_stream_found
or line.endswith("(default)"),
}
self._default_stream_found = True
# for default streams, set their numbers globally, so it's
# easy to get without iterating all
if self._current_stream["default"]:
self.result[
f"default_{stream_type_lower}_input_number"
] = input_number
self.result[
f"default_{stream_type_lower}_stream_number"
] = stream_number
# exit chapter
if self._current_chapter:
input_chapters[input_number].append(self._current_chapter)
self._current_chapter = None
if "input_number" not in self._current_input_file:
# first input file
self._current_input_file["input_number"] = input_number
elif self._current_input_file["input_number"] != input_number:
# new input file
# include their chapters if there are for this input file
if len(input_chapters) >= input_number + 1:
self._current_input_file["chapters"] = input_chapters[
input_number
]
# add new input file to self.result
self.result["inputs"].append(self._current_input_file)
self._current_input_file = {"input_number": input_number}
# parse relevant data by stream type
try:
global_data, stream_data = self.parse_data_by_stream_type(
stream_type, line
)
except NotImplementedError as exc:
warnings.warn(
f"{str(exc)}\nffmpeg output:\n\n{self.infos}", UserWarning
)
else:
self.result.update(global_data)
self._current_stream.update(stream_data)
elif line.startswith(" Metadata:"):
# enter group " Metadata:"
continue
elif self._current_stream:
# stream metadata line
if "metadata" not in self._current_stream:
self._current_stream["metadata"] = {}
field, value = self.parse_metadata_field_value(line)
if self._current_stream["stream_type"] == "video":
field, value = self.video_metadata_type_casting(field, value)
if field == "rotate":
self.result["video_rotation"] = value
# multiline metadata value parsing
if field == "":
field = self._last_metadata_field_added
value = self._current_stream["metadata"][field] + "\n" + value
else:
self._last_metadata_field_added = field
self._current_stream["metadata"][field] = value
elif line.startswith(" Chapter"):
# Chapter data line
if self._current_chapter:
# there is a previews chapter?
if len(input_chapters) < self._current_chapter["input_number"] + 1:
input_chapters.append([])
# include in the chapters by input matrix
input_chapters[self._current_chapter["input_number"]].append(
self._current_chapter
)
# extract chapter data
chapter_data_match = re.search(
r"^ Chapter #(\d+):(\d+): start (\d+\.?\d+?), end (\d+\.?\d+?)",
line,
)
input_number, chapter_number, start, end = chapter_data_match.groups()
# start building the chapter
self._current_chapter = {
"input_number": int(input_number),
"chapter_number": int(chapter_number),
"start": float(start),
"end": float(end),
}
elif self._current_chapter:
# inside chapter metadata
if "metadata" not in self._current_chapter:
self._current_chapter["metadata"] = {}
field, value = self.parse_metadata_field_value(line)
# multiline metadata value parsing
if field == "":
field = self._last_metadata_field_added
value = self._current_chapter["metadata"][field] + "\n" + value
else:
self._last_metadata_field_added = field
self._current_chapter["metadata"][field] = value
# last input file, must be included in self.result
if self._current_input_file:
self._current_input_file["streams"].append(self._current_stream)
# include their chapters, if there are any
if (
"input_number" in self._current_input_file
and len(input_chapters) == self._current_input_file["input_number"] + 1
):
self._current_input_file["chapters"] = input_chapters[
self._current_input_file["input_number"]
]
self.result["inputs"].append(self._current_input_file)
# some video duration utilities
if self.result["video_found"] and self.check_duration:
self.result["video_duration"] = self.result["duration"]
self.result["video_n_frames"] = int(
self.result["duration"] * self.result.get("video_fps", 0)
)
else:
self.result["video_n_frames"] = 0
self.result["video_duration"] = 0.0
# We could have also recomputed duration from the number of frames, as follows:
# >>> result['video_duration'] = result['video_n_frames'] / result['video_fps']
# not default audio found, assume first audio stream is the default
if self.result["audio_found"] and not self.result.get("audio_bitrate"):
self.result["audio_bitrate"] = None
for streams_input in self.result["inputs"]:
for stream in streams_input["streams"]:
if stream["stream_type"] == "audio" and stream.get("bitrate"):
self.result["audio_bitrate"] = stream["bitrate"]
break
if self.result["audio_bitrate"] is not None:
break
result = self.result
# reset state of the parser
self._reset_state()
return result
def parse_data_by_stream_type(self, stream_type, line):
"""Parses data from "Stream ... {stream_type}" line."""
try:
return {
"Audio": self.parse_audio_stream_data,
"Video": self.parse_video_stream_data,
"Data": lambda _line: ({}, {}),
}[stream_type](line)
except KeyError:
raise NotImplementedError(
f"{stream_type} stream parsing is not supported by moviepy and"
" will be ignored"
)
def parse_audio_stream_data(self, line):
"""Parses data from "Stream ... Audio" line."""
global_data, stream_data = ({"audio_found": True}, {})
try:
stream_data["fps"] = int(re.search(r" (\d+) Hz", line).group(1))
except (AttributeError, ValueError):
# AttributeError: 'NoneType' object has no attribute 'group'
# ValueError: invalid literal for int() with base 10: '<string>'
stream_data["fps"] = "unknown"
match_audio_bitrate = re.search(r"(\d+) kb/s", line)
stream_data["bitrate"] = (
int(match_audio_bitrate.group(1)) if match_audio_bitrate else None
)
if self._current_stream["default"]:
global_data["audio_fps"] = stream_data["fps"]
global_data["audio_bitrate"] = stream_data["bitrate"]
return (global_data, stream_data)
def parse_video_stream_data(self, line):
"""Parses data from "Stream ... Video" line."""
global_data, stream_data = ({"video_found": True}, {})
try:
match_video_size = re.search(r" (\d+)x(\d+)[,\s]", line)
if match_video_size:
# size, of the form 460x320 (w x h)
stream_data["size"] = [int(num) for num in match_video_size.groups()]
except Exception:
raise IOError(
(
"MoviePy error: failed to read video dimensions in"
" file '%s'.\nHere are the file infos returned by"
"ffmpeg:\n\n%s"
)
% (self.filename, self.infos)
)
match_bitrate = re.search(r"(\d+) kb/s", line)
stream_data["bitrate"] = int(match_bitrate.group(1)) if match_bitrate else None
# Get the frame rate. Sometimes it's 'tbr', sometimes 'fps', sometimes
# tbc, and sometimes tbc/2...
# Current policy: Trust fps first, then tbr unless fps_source is
# specified as 'tbr' in which case try tbr then fps
# If result is near from x*1000/1001 where x is 23,24,25,50,
# replace by x*1000/1001 (very common case for the fps).
if self.fps_source == "fps":
try:
fps = self.parse_fps(line)
except (AttributeError, ValueError):
fps = self.parse_tbr(line)
elif self.fps_source == "tbr":
try:
fps = self.parse_tbr(line)
except (AttributeError, ValueError):
fps = self.parse_fps(line)
else:
raise ValueError(
("fps source '%s' not supported parsing the video '%s'")
% (self.fps_source, self.filename)
)
# It is known that a fps of 24 is often written as 24000/1001
# but then ffmpeg nicely rounds it to 23.98, which we hate.
coef = 1000.0 / 1001.0
for x in [23, 24, 25, 30, 50]:
if (fps != x) and abs(fps - x * coef) < 0.01:
fps = x * coef
stream_data["fps"] = fps
# Try to extract video codec and profile
main_info_match = re.search(
r"Video:\s(\w+)?\s?(\([^)]+\))?",
line.lstrip(),
)
if main_info_match is not None:
(codec_name, profile) = main_info_match.groups()
stream_data["codec_name"] = codec_name
stream_data["profile"] = profile
if self._current_stream["default"] or "video_codec_name" not in self.result:
global_data["video_codec_name"] = stream_data.get("codec_name", None)
if self._current_stream["default"] or "video_profile" not in self.result:
global_data["video_profile"] = stream_data.get("profile", None)
if self._current_stream["default"] or "video_size" not in self.result:
global_data["video_size"] = stream_data.get("size", None)
if self._current_stream["default"] or "video_bitrate" not in self.result:
global_data["video_bitrate"] = stream_data.get("bitrate", None)
if self._current_stream["default"] or "video_fps" not in self.result:
global_data["video_fps"] = stream_data["fps"]
return (global_data, stream_data)
def parse_fps(self, line):
"""Parses number of FPS from a line of the ``ffmpeg -i`` command output."""
return float(re.search(r" (\d+.?\d*) fps", line).group(1))
def parse_tbr(self, line):
"""Parses number of TBS from a line of the ``ffmpeg -i`` command output."""
s_tbr = re.search(r" (\d+.?\d*k?) tbr", line).group(1)
# Sometimes comes as e.g. 12k. We need to replace that with 12000.
if s_tbr[-1] == "k":
tbr = float(s_tbr[:-1]) * 1000
else:
tbr = float(s_tbr)
return tbr
def parse_duration(self, line):
"""Parse the duration from the line that outputs the duration of
the container.
"""
try:
time_raw_string = line.split(self.duration_tag_separator)[-1]
match_duration = re.search(
r"([0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9])",
time_raw_string,
)
return convert_to_seconds(match_duration.group(1))
except Exception:
raise IOError(
(
"MoviePy error: failed to read the duration of file '%s'.\n"
"Here are the file infos returned by ffmpeg:\n\n%s"
)
% (self.filename, self.infos)
)
def parse_metadata_field_value(
self,
line,
):
"""Returns a tuple with a metadata field-value pair given a ffmpeg `-i`
command output line.
"""
raw_field, raw_value = line.split(":", 1)
return (raw_field.strip(" "), raw_value.strip(" "))
def video_metadata_type_casting(self, field, value):
"""Cast needed video metadata fields to other types than the default str."""
if field == "rotate":
return (field, float(value))
return (field, value)
def ffmpeg_parse_infos(
filename,
check_duration=True,
fps_source="fps",
decode_file=False,
print_infos=False,
):
"""Get the information of a file using ffmpeg.
Returns a dictionary with next fields:
- ``"duration"``
- ``"metadata"``
- ``"inputs"``
- ``"video_found"``
- ``"video_fps"``
- ``"video_n_frames"``
- ``"video_duration"``
- ``"video_bitrate"``
- ``"video_metadata"``
- ``"audio_found"``
- ``"audio_fps"``
- ``"audio_bitrate"``
- ``"audio_metadata"``
- ``"video_codec_name"``
- ``"video_profile"``
Note that "video_duration" is slightly smaller than "duration" to avoid
fetching the incomplete frames at the end, which raises an error.
Parameters
----------
filename
Name of the file parsed, only used to raise accurate error messages.
infos
Information returned by FFmpeg.
fps_source
Indicates what source data will be preferably used to retrieve fps data.
check_duration
Enable or disable the parsing of the duration of the file. Useful to
skip the duration check, for example, for images.
decode_file
Indicates if the whole file must be read to retrieve their duration.
This is needed for some files in order to get the correct duration (see
https://github.com/Zulko/moviepy/pull/1222).
"""
# Open the file in a pipe, read output
cmd = [FFMPEG_BINARY, "-hide_banner", "-i", ffmpeg_escape_filename(filename)]
if decode_file:
cmd.extend(["-f", "null", "-"])
popen_params = cross_platform_popen_params(
{
"bufsize": 10**5,
"stdout": sp.PIPE,
"stderr": sp.PIPE,
"stdin": sp.DEVNULL,
}
)
proc = sp.Popen(cmd, **popen_params)
(output, error) = proc.communicate()
infos = error.decode("utf8", errors="ignore")
proc.terminate()
del proc
if print_infos:
# print the whole info text returned by FFMPEG
print(infos)
try:
return FFmpegInfosParser(
infos,
filename,
fps_source=fps_source,
check_duration=check_duration,
decode_file=decode_file,
).parse()
except Exception as exc:
if os.path.isdir(filename):
raise IsADirectoryError(f"'{filename}' is a directory")
elif not os.path.exists(filename):
raise FileNotFoundError(f"'{filename}' not found")
raise IOError(f"Error passing `ffmpeg -i` command output:\n\n{infos}") from exc

View File

@@ -0,0 +1,209 @@
"""Miscellaneous bindings to ffmpeg."""
import os
from moviepy.config import FFMPEG_BINARY
from moviepy.decorators import convert_parameter_to_seconds, convert_path_to_string
from moviepy.tools import ffmpeg_escape_filename, subprocess_call
@convert_path_to_string(("inputfile", "outputfile"))
@convert_parameter_to_seconds(("start_time", "end_time"))
def ffmpeg_extract_subclip(
inputfile, start_time, end_time, outputfile=None, logger="bar"
):
"""Makes a new video file playing video file between two times.
Parameters
----------
inputfile : str
Path to the file from which the subclip will be extracted.
start_time : float
Moment of the input clip that marks the start of the produced subclip.
end_time : float
Moment of the input clip that marks the end of the produced subclip.
outputfile : str, optional
Path to the output file. Defaults to
``<inputfile_name>SUB<start_time>_<end_time><ext>``.
"""
if not outputfile:
name, ext = os.path.splitext(inputfile)
t1, t2 = [int(1000 * t) for t in [start_time, end_time]]
outputfile = "%sSUB%d_%d%s" % (name, t1, t2, ext)
cmd = [
FFMPEG_BINARY,
"-y",
"-ss",
"%0.2f" % start_time,
"-i",
ffmpeg_escape_filename(inputfile),
"-t",
"%0.2f" % (end_time - start_time),
"-map",
"0",
"-vcodec",
"copy",
"-acodec",
"copy",
"-copyts",
ffmpeg_escape_filename(outputfile),
]
subprocess_call(cmd, logger=logger)
@convert_path_to_string(("videofile", "audiofile", "outputfile"))
def ffmpeg_merge_video_audio(
videofile,
audiofile,
outputfile,
video_codec="copy",
audio_codec="copy",
logger="bar",
):
"""Merges video file and audio file into one movie file.
Parameters
----------
videofile : str
Path to the video file used in the merge.
audiofile : str
Path to the audio file used in the merge.
outputfile : str
Path to the output file.
video_codec : str, optional
Video codec used by FFmpeg in the merge.
audio_codec : str, optional
Audio codec used by FFmpeg in the merge.
"""
cmd = [
FFMPEG_BINARY,
"-y",
"-i",
ffmpeg_escape_filename(audiofile),
"-i",
ffmpeg_escape_filename(videofile),
"-vcodec",
video_codec,
"-acodec",
audio_codec,
ffmpeg_escape_filename(outputfile),
]
subprocess_call(cmd, logger=logger)
@convert_path_to_string(("inputfile", "outputfile"))
def ffmpeg_extract_audio(inputfile, outputfile, bitrate=3000, fps=44100, logger="bar"):
"""Extract the sound from a video file and save it in ``outputfile``.
Parameters
----------
inputfile : str
The path to the file from which the audio will be extracted.
outputfile : str
The path to the file to which the audio will be stored.
bitrate : int, optional
Bitrate for the new audio file.
fps : int, optional
Frame rate for the new audio file.
"""
cmd = [
FFMPEG_BINARY,
"-y",
"-i",
ffmpeg_escape_filename(inputfile),
"-ab",
"%dk" % bitrate,
"-ar",
"%d" % fps,
ffmpeg_escape_filename(outputfile),
]
subprocess_call(cmd, logger=logger)
@convert_path_to_string(("inputfile", "outputfile"))
def ffmpeg_resize(inputfile, outputfile, size, logger="bar"):
"""Resizes a file to new size and write the result in another.
Parameters
----------
inputfile : str
Path to the file to be resized.
outputfile : str
Path to the output file.
size : list or tuple
New size in format ``[width, height]`` for the output file.
"""
cmd = [
FFMPEG_BINARY,
"-i",
ffmpeg_escape_filename(inputfile),
"-vf",
"scale=%d:%d" % (size[0], size[1]),
ffmpeg_escape_filename(outputfile),
]
subprocess_call(cmd, logger=logger)
@convert_path_to_string(("inputfile", "outputfile", "output_dir"))
def ffmpeg_stabilize_video(
inputfile, outputfile=None, output_dir="", overwrite_file=True, logger="bar"
):
"""
Stabilizes ``filename`` and write the result to ``output``.
Parameters
----------
inputfile : str
The name of the shaky video.
outputfile : str, optional
The name of new stabilized video. Defaults to appending '_stabilized' to
the input file name.
output_dir : str, optional
The directory to place the output video in. Defaults to the current
working directory.
overwrite_file : bool, optional
If ``outputfile`` already exists in ``output_dir``, then overwrite
``outputfile`` Defaults to True.
"""
if not outputfile:
without_dir = os.path.basename(inputfile)
name, ext = os.path.splitext(without_dir)
outputfile = f"{name}_stabilized{ext}"
outputfile = os.path.join(output_dir, outputfile)
cmd = [
FFMPEG_BINARY,
"-i",
ffmpeg_escape_filename(inputfile),
"-vf",
"deshake",
ffmpeg_escape_filename(outputfile),
]
if overwrite_file:
cmd.append("-y")
subprocess_call(cmd, logger=logger)

View File

@@ -0,0 +1,344 @@
"""
On the long term this will implement several methods to make videos
out of VideoClips
"""
import subprocess as sp
import numpy as np
from proglog import proglog
from moviepy.config import FFMPEG_BINARY
from moviepy.tools import cross_platform_popen_params, ffmpeg_escape_filename
class FFMPEG_VideoWriter:
"""A class for FFMPEG-based video writing.
Parameters
----------
filename : str
Any filename like ``"video.mp4"`` etc. but if you want to avoid
complications it is recommended to use the generic extension ``".avi"``
for all your videos.
size : tuple or list
Size of the output video in pixels (width, height).
fps : int
Frames per second in the output video file.
codec : str, optional
FFMPEG codec. It seems that in terms of quality the hierarchy is
'rawvideo' = 'png' > 'mpeg4' > 'libx264'
'png' manages the same lossless quality as 'rawvideo' but yields
smaller files. Type ``ffmpeg -codecs`` in a terminal to get a list
of accepted codecs.
Note for default 'libx264': by default the pixel format yuv420p
is used. If the video dimensions are not both even (e.g. 720x405)
another pixel format is used, and this can cause problem in some
video readers.
audiofile : str, optional
The name of an audio file that will be incorporated to the video.
preset : str, optional
Sets the time that FFMPEG will take to compress the video. The slower,
the better the compression rate. Possibilities are: ``"ultrafast"``,
``"superfast"``, ``"veryfast"``, ``"faster"``, ``"fast"``, ``"medium"``
(default), ``"slow"``, ``"slower"``, ``"veryslow"``, ``"placebo"``.
bitrate : str, optional
Only relevant for codecs which accept a bitrate. "5000k" offers
nice results in general.
with_mask : bool, optional
Set to ``True`` if there is a mask in the video to be encoded.
pixel_format : str, optional
Optional: Pixel format for the output video file. If is not specified
``"rgb24"`` will be used as the default format unless ``with_mask`` is
set as ``True``, then ``"rgba"`` will be used.
logfile : int, optional
File descriptor for logging output. If not defined, ``subprocess.PIPE``
will be used. Defined using another value, the log level of the ffmpeg
command will be "info", otherwise "error".
threads : int, optional
Number of threads used to write the output with ffmpeg.
ffmpeg_params : list, optional
Additional parameters passed to ffmpeg command.
"""
def __init__(
self,
filename,
size,
fps,
codec="libx264",
audiofile=None,
preset="medium",
bitrate=None,
with_mask=False,
logfile=None,
threads=None,
ffmpeg_params=None,
pixel_format=None,
):
if logfile is None:
logfile = sp.PIPE
self.logfile = logfile
self.filename = filename
self.codec = codec
self.ext = self.filename.split(".")[-1]
pixel_format = "rgba" if with_mask else "rgb24"
# order is important
cmd = [
FFMPEG_BINARY,
"-y",
"-loglevel",
"error" if logfile == sp.PIPE else "info",
"-f",
"rawvideo",
"-vcodec",
"rawvideo",
"-s",
"%dx%d" % (size[0], size[1]),
"-pix_fmt",
pixel_format,
"-r",
"%.02f" % fps,
"-an",
"-i",
"-",
]
if audiofile is not None:
cmd.extend(["-i", audiofile, "-acodec", "copy"])
cmd.extend(["-vcodec", codec, "-preset", preset])
if ffmpeg_params is not None:
cmd.extend(ffmpeg_params)
if bitrate is not None:
cmd.extend(["-b", bitrate])
if threads is not None:
cmd.extend(["-threads", str(threads)])
# Disable auto alt ref for transparent webm and set pix format yo yuva420p
if codec == "libvpx" and with_mask:
cmd.extend(["-pix_fmt", "yuva420p"])
cmd.extend(["-auto-alt-ref", "0"])
elif (codec == "libx264") and (size[0] % 2 == 0) and (size[1] % 2 == 0):
cmd.extend(["-pix_fmt", "yuva420p"])
cmd.extend([ffmpeg_escape_filename(filename)])
popen_params = cross_platform_popen_params(
{"stdout": sp.DEVNULL, "stderr": logfile, "stdin": sp.PIPE}
)
self.proc = sp.Popen(cmd, **popen_params)
def write_frame(self, img_array):
"""Writes one frame in the file."""
try:
self.proc.stdin.write(img_array.tobytes())
except IOError as err:
_, ffmpeg_error = self.proc.communicate()
if ffmpeg_error is not None:
ffmpeg_error = ffmpeg_error.decode()
else:
# The error was redirected to a logfile with `write_logfile=True`,
# so read the error from that file instead
self.logfile.seek(0)
ffmpeg_error = self.logfile.read()
error = (
f"{err}\n\nMoviePy error: FFMPEG encountered the following error while "
f"writing file {self.filename}:\n\n {ffmpeg_error}"
)
if "Unknown encoder" in ffmpeg_error:
error += (
"\n\nThe video export failed because FFMPEG didn't find the "
f"specified codec for video encoding {self.codec}. "
"Please install this codec or change the codec when calling "
"write_videofile.\nFor instance:\n"
" >>> clip.write_videofile('myvid.webm', codec='libvpx')"
)
elif "incorrect codec parameters ?" in ffmpeg_error:
error += (
"\n\nThe video export failed, possibly because the codec "
f"specified for the video {self.codec} is not compatible with "
f"the given extension {self.ext}.\n"
"Please specify a valid 'codec' argument in write_videofile.\n"
"This would be 'libx264' or 'mpeg4' for mp4, "
"'libtheora' for ogv, 'libvpx for webm.\n"
"Another possible reason is that the audio codec was not "
"compatible with the video codec. For instance, the video "
"extensions 'ogv' and 'webm' only allow 'libvorbis' (default) as a"
"video codec."
)
elif "bitrate not specified" in ffmpeg_error:
error += (
"\n\nThe video export failed, possibly because the bitrate "
"specified was too high or too low for the video codec."
)
elif "Invalid encoder type" in ffmpeg_error:
error += (
"\n\nThe video export failed because the codec "
"or file extension you provided is not suitable for video"
)
raise IOError(error)
def close(self):
"""Closes the writer, terminating the subprocess if is still alive."""
if self.proc:
self.proc.stdin.close()
if self.proc.stderr is not None:
self.proc.stderr.close()
self.proc.wait()
self.proc = None
# Support the Context Manager protocol, to ensure that resources are cleaned up.
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
def ffmpeg_write_video(
clip,
filename,
fps,
codec="libx264",
bitrate=None,
preset="medium",
write_logfile=False,
audiofile=None,
threads=None,
ffmpeg_params=None,
logger="bar",
pixel_format=None,
):
"""Write the clip to a videofile. See VideoClip.write_videofile for details
on the parameters.
"""
logger = proglog.default_bar_logger(logger)
if write_logfile:
logfile = open(filename + ".log", "w+")
else:
logfile = None
logger(message="MoviePy - Writing video %s\n" % filename)
has_mask = clip.mask is not None
with FFMPEG_VideoWriter(
filename,
clip.size,
fps,
codec=codec,
preset=preset,
bitrate=bitrate,
with_mask=has_mask,
logfile=logfile,
audiofile=audiofile,
threads=threads,
ffmpeg_params=ffmpeg_params,
pixel_format=pixel_format,
) as writer:
for t, frame in clip.iter_frames(
logger=logger, with_times=True, fps=fps, dtype="uint8"
):
if clip.mask is not None:
mask = 255 * clip.mask.get_frame(t)
if mask.dtype != "uint8":
mask = mask.astype("uint8")
frame = np.dstack([frame, mask])
writer.write_frame(frame)
if write_logfile:
logfile.close()
logger(message="MoviePy - Done !")
def ffmpeg_write_image(filename, image, logfile=False, pixel_format=None):
"""Writes an image (HxWx3 or HxWx4 numpy array) to a file, using ffmpeg.
Parameters
----------
filename : str
Path to the output file.
image : np.ndarray
Numpy array with the image data.
logfile : bool, optional
Writes the ffmpeg output inside a logging file (``True``) or not
(``False``).
pixel_format : str, optional
Pixel format for ffmpeg. If not defined, it will be discovered checking
if the image data contains an alpha channel (``"rgba"``) or not
(``"rgb24"``).
"""
if image.dtype != "uint8":
image = image.astype("uint8")
if not pixel_format:
pixel_format = "rgba" if (image.shape[2] == 4) else "rgb24"
cmd = [
FFMPEG_BINARY,
"-y",
"-s",
"%dx%d" % (image.shape[:2][::-1]),
"-f",
"rawvideo",
"-pix_fmt",
pixel_format,
"-i",
"-",
ffmpeg_escape_filename(filename),
]
if logfile:
log_file = open(filename + ".log", "w+")
else:
log_file = sp.PIPE
popen_params = cross_platform_popen_params(
{"stdout": sp.DEVNULL, "stderr": log_file, "stdin": sp.PIPE}
)
proc = sp.Popen(cmd, **popen_params)
out, err = proc.communicate(image.tobytes())
if proc.returncode:
error = (
f"{err}\n\nMoviePy error: FFMPEG encountered the following error while "
f"writing file {filename} with command {cmd}:\n\n {err.decode()}"
)
raise IOError(error)
del proc

View File

@@ -0,0 +1,137 @@
"""
On the long term this will implement several methods to make videos
out of VideoClips
"""
import subprocess as sp
from moviepy.config import FFPLAY_BINARY
from moviepy.tools import cross_platform_popen_params
class FFPLAY_VideoPreviewer:
"""A class for FFPLAY-based video preview.
Parameters
----------
size : tuple or list
Size of the output video in pixels (width, height).
fps : int
Frames per second in the output video file.
pixel_format : str
Pixel format for the output video file, ``rgb24`` for normal video, ``rgba``
if video with mask.
"""
def __init__(
self,
size,
fps,
pixel_format,
):
# order is important
cmd = [
FFPLAY_BINARY,
"-autoexit", # If you don't precise, ffplay won't stop at end
"-f",
"rawvideo",
"-pixel_format",
pixel_format,
"-video_size",
"%dx%d" % (size[0], size[1]),
"-framerate",
"%.02f" % fps,
"-",
]
popen_params = cross_platform_popen_params(
{"stdout": sp.DEVNULL, "stderr": sp.STDOUT, "stdin": sp.PIPE}
)
self.proc = sp.Popen(cmd, **popen_params)
def show_frame(self, img_array):
"""Writes one frame in the file."""
try:
self.proc.stdin.write(img_array.tobytes())
except IOError as err:
_, ffplay_error = self.proc.communicate()
if ffplay_error is not None:
ffplay_error = ffplay_error.decode()
error = (
f"{err}\n\nMoviePy error: FFPLAY encountered the following error while "
f"previewing clip :\n\n {ffplay_error}"
)
raise IOError(error)
def close(self):
"""Closes the writer, terminating the subprocess if is still alive."""
if self.proc:
self.proc.stdin.close()
if self.proc.stderr is not None:
self.proc.stderr.close()
self.proc.wait()
self.proc = None
# Support the Context Manager protocol, to ensure that resources are cleaned up.
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
def ffplay_preview_video(
clip, fps, pixel_format="rgb24", audio_flag=None, video_flag=None
):
"""Preview the clip using ffplay. See VideoClip.preview for details
on the parameters.
Parameters
----------
clip : VideoClip
The clip to preview
fps : int
Number of frames per seconds in the displayed video.
pixel_format : str, optional
Warning: This is not used anywhere in the code and should probably
be removed.
It is believed pixel format rgb24 does not work properly for now because
it requires applying a mask on CompositeVideoClip and that is believed to
not be working.
Pixel format for the output video file, ``rgb24`` for normal video, ``rgba``
if video with mask
audio_flag : Thread.Event, optional
A thread event that video will wait for. If not provided we ignore audio
video_flag : Thread.Event, optional
A thread event that video will set after first frame has been shown. If not
provided, we simply ignore
"""
with FFPLAY_VideoPreviewer(clip.size, fps, pixel_format) as previewer:
first_frame = True
for t, frame in clip.iter_frames(with_times=True, fps=fps, dtype="uint8"):
previewer.show_frame(frame)
# After first frame is shown, if we have audio/video flag, set video ready
# and wait for audio
if first_frame:
first_frame = False
if video_flag:
video_flag.set() # say to the audio: video is ready
if audio_flag:
audio_flag.wait() # wait for the audio to be ready

View File

@@ -0,0 +1,20 @@
"""MoviePy video GIFs writing."""
import imageio.v3 as iio
import proglog
from moviepy.decorators import requires_duration, use_clip_fps_by_default
@requires_duration
@use_clip_fps_by_default
def write_gif_with_imageio(clip, filename, fps=None, loop=0, logger="bar"):
"""Writes the gif with the Python library ImageIO (calls FreeImage)."""
logger = proglog.default_bar_logger(logger)
with iio.imopen(filename, "w", plugin="pillow") as writer:
logger(message="MoviePy - Building file %s with imageio." % filename)
for frame in clip.iter_frames(fps=fps, logger=logger, dtype="uint8"):
writer.write(
frame, duration=1000 / fps, loop=loop
) # Duration is in ms not s

View File

View File

@@ -0,0 +1,142 @@
"""Contains different functions to make end and opening 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
class CreditsClip(TextClip):
"""Credits clip.
Parameters
----------
creditfile
A string or path like object pointing to a text file
whose content must be as follows:
..code:: python
# This is a comment
# The next line says : leave 4 blank lines
.blank 4
..Executive Story Editor
MARCEL DURAND
..Associate Producers
MARTIN MARCEL
DIDIER MARTIN
..Music Supervisor
JEAN DIDIER
width
Total width of the credits text in pixels
gap
Horizontal gap in pixels between the jobs and the names
color
Color of the text. See ``TextClip.list('color')``
for a list of acceptable names.
font
Name of the font to use. See ``TextClip.list('font')`` for
the list of fonts you can use on your computer.
font_size
Size of font to use
stroke_color
Color of the stroke (=contour line) of the text. If ``None``,
there will be no stroke.
stroke_width
Width of the stroke, in pixels. Can be a float, like 1.5.
bg_color
Color of the background. If ``None``, the background will be transparent.
Returns
-------
image
An ImageClip instance that looks like this and can be scrolled
to make some credits: ::
Executive Story Editor MARCEL DURAND
Associate Producers MARTIN MARCEL
DIDIER MARTIN
Music Supervisor JEAN DIDIER
"""
@convert_path_to_string("creditfile")
def __init__(
self,
creditfile,
width,
color="white",
stroke_color="black",
stroke_width=2,
font="Impact-Normal",
font_size=60,
bg_color=None,
gap=0,
):
# Parse the .txt file
texts = []
one_line = True
with open(creditfile) as file:
for line in file:
if line.startswith(("\n", "#")):
# exclude blank lines or comments
continue
elif line.startswith(".blank"):
# ..blank n
for i in range(int(line.split(" ")[1])):
texts.append(["\n", "\n"])
elif line.startswith(".."):
texts.append([line[2:], ""])
one_line = True
elif one_line:
texts.append(["", line])
one_line = False
else:
texts.append(["\n", line])
left, right = ("".join(line) for line in zip(*texts))
# Make two columns for the credits
left, right = [
TextClip(
text=txt,
color=color,
stroke_color=stroke_color,
stroke_width=stroke_width,
font=font,
font_size=font_size,
text_align=align,
)
for txt, align in [(left, "left"), (right, "right")]
]
both_columns = CompositeVideoClip(
[left, right.with_position((left.w + gap, 0))],
size=(left.w + right.w + gap, right.h),
bg_color=bg_color,
)
# Scale to the required size
scaled = both_columns.with_effects([Resize(width=width)])
# Transform the CompositeVideoClip into an ImageClip
# Calls ImageClip.__init__()
super(TextClip, self).__init__(scaled.get_frame(0))
self.mask = ImageClip(scaled.mask.get_frame(0), is_mask=True)

522
moviepy/video/tools/cuts.py Normal file
View File

@@ -0,0 +1,522 @@
"""Contains everything that can help automate the cuts in MoviePy."""
from collections import defaultdict
import numpy as np
from moviepy.decorators import convert_parameter_to_seconds, use_clip_fps_by_default
@use_clip_fps_by_default
@convert_parameter_to_seconds(["start_time"])
def find_video_period(clip, fps=None, start_time=0.3):
"""Find the period of a video based on frames correlation.
Parameters
----------
clip : moviepy.Clip.Clip
Clip for which the video period will be computed.
fps : int, optional
Number of frames per second used computing the period. Higher values will
produce more accurate periods, but the execution time will be longer.
start_time : float, optional
First timeframe used to calculate the period of the clip.
Examples
--------
.. code:: python
from moviepy import *
from moviepy.video.tools.cuts import find_video_period
clip = VideoFileClip("media/chaplin.mp4").subclipped(0, 1).loop(2)
round(videotools.find_video_period(clip, fps=80), 6)
1
"""
def frame(t):
return clip.get_frame(t).flatten()
timings = np.arange(start_time, clip.duration, 1 / fps)[1:]
ref = frame(0)
corrs = [np.corrcoef(ref, frame(t))[0, 1] for t in timings]
return timings[np.argmax(corrs)]
class FramesMatch:
"""Frames match inside a set of frames.
Parameters
----------
start_time : float
Starting time.
end_time : float
End time.
min_distance : float
Lower bound on the distance between the first and last frames
max_distance : float
Upper bound on the distance between the first and last frames
"""
def __init__(self, start_time, end_time, min_distance, max_distance):
self.start_time = start_time
self.end_time = end_time
self.min_distance = min_distance
self.max_distance = max_distance
self.time_span = end_time - start_time
def __str__(self): # pragma: no cover
return "(%.04f, %.04f, %.04f, %.04f)" % (
self.start_time,
self.end_time,
self.min_distance,
self.max_distance,
)
def __repr__(self): # pragma: no cover
return self.__str__()
def __iter__(self): # pragma: no cover
return iter(
(self.start_time, self.end_time, self.min_distance, self.max_distance)
)
def __eq__(self, other):
return (
other.start_time == self.start_time
and other.end_time == self.end_time
and other.min_distance == self.min_distance
and other.max_distance == self.max_distance
)
class FramesMatches(list):
"""Frames matches inside a set of frames.
You can instantiate it passing a list of FramesMatch objects or
using the class methods ``load`` and ``from_clip``.
Parameters
----------
lst : list
Iterable of FramesMatch objects.
"""
def __init__(self, lst):
list.__init__(self, sorted(lst, key=lambda e: e.max_distance))
def best(self, n=1, percent=None):
"""Returns a new instance of FramesMatches object or a FramesMatch
from the current class instance given different conditions.
By default returns the first FramesMatch that the current instance
stores.
Parameters
----------
n : int, optional
Number of matches to retrieve from the current FramesMatches object.
Only has effect when ``percent=None``.
percent : float, optional
Percent of the current match to retrieve.
Returns
-------
FramesMatch or FramesMatches : If the number of matches to retrieve is
greater than 1 returns a FramesMatches object, otherwise a
FramesMatch.
"""
if percent is not None:
n = len(self) * percent / 100
return self[0] if n == 1 else FramesMatches(self[: int(n)])
def filter(self, condition):
"""Return a FramesMatches object obtained by filtering out the
FramesMatch which do not satistify a condition.
Parameters
----------
condition : func
Function which takes a FrameMatch object as parameter and returns a
bool.
Examples
--------
.. code:: python
# Only keep the matches corresponding to (> 1 second) sequences.
new_matches = matches.filter(lambda match: match.time_span > 1)
"""
return FramesMatches(filter(condition, self))
def save(self, filename):
"""Save a FramesMatches object to a file.
Parameters
----------
filename : str
Path to the file in which will be dumped the FramesMatches object data.
"""
np.savetxt(
filename,
np.array([np.array(list(e)) for e in self]),
fmt="%.03f",
delimiter="\t",
)
@staticmethod
def load(filename):
"""Load a FramesMatches object from a file.
Parameters
----------
filename : str
Path to the file to use loading a FramesMatches object.
Examples
--------
>>> matching_frames = FramesMatches.load("somefile")
"""
arr = np.loadtxt(filename)
mfs = [FramesMatch(*e) for e in arr]
return FramesMatches(mfs)
@staticmethod
def from_clip(clip, distance_threshold, max_duration, fps=None, logger="bar"):
"""Finds all the frames that look alike in a clip, for instance to make
a looping GIF.
Parameters
----------
clip : moviepy.video.VideoClip.VideoClip
A MoviePy video clip.
distance_threshold : float
Distance above which a match is rejected.
max_duration : float
Maximal duration (in seconds) between two matching frames.
fps : int, optional
Frames per second (default will be ``clip.fps``).
logger : str, optional
Either ``"bar"`` for progress bar or ``None`` or any Proglog logger.
Returns
-------
FramesMatches
All pairs of frames with ``end_time - start_time < max_duration``
and whose distance is under ``distance_threshold``.
Examples
--------
We find all matching frames in a given video and turn the best match
with a duration of 1.5 seconds or more into a GIF:
.. code:: python
from moviepy import VideoFileClip
from moviepy.video.tools.cuts import FramesMatches
clip = VideoFileClip("foo.mp4").resize(width=200)
matches = FramesMatches.from_clip(
clip, distance_threshold=10, max_duration=3, # will take time
)
best = matches.filter(lambda m: m.time_span > 1.5).best()
clip.subclipped(best.start_time, best.end_time).write_gif("foo.gif")
"""
N_pixels = clip.w * clip.h * 3
def dot_product(F1, F2):
return (F1 * F2).sum() / N_pixels
frame_dict = {} # will store the frames and their mutual distances
def distance(t1, t2):
uv = dot_product(frame_dict[t1]["frame"], frame_dict[t2]["frame"])
u, v = frame_dict[t1]["|F|sq"], frame_dict[t2]["|F|sq"]
return np.sqrt(u + v - 2 * uv)
matching_frames = [] # the final result.
for t, frame in clip.iter_frames(with_times=True, logger=logger):
flat_frame = 1.0 * frame.flatten()
F_norm_sq = dot_product(flat_frame, flat_frame)
F_norm = np.sqrt(F_norm_sq)
for t2 in list(frame_dict.keys()):
# forget old frames, add 't' to the others frames
# check for early rejections based on differing norms
if (t - t2) > max_duration:
frame_dict.pop(t2)
else:
frame_dict[t2][t] = {
"min": abs(frame_dict[t2]["|F|"] - F_norm),
"max": frame_dict[t2]["|F|"] + F_norm,
}
frame_dict[t2][t]["rejected"] = (
frame_dict[t2][t]["min"] > distance_threshold
)
t_F = sorted(frame_dict.keys())
frame_dict[t] = {"frame": flat_frame, "|F|sq": F_norm_sq, "|F|": F_norm}
for i, t2 in enumerate(t_F):
# Compare F(t) to all the previous frames
if frame_dict[t2][t]["rejected"]:
continue
dist = distance(t, t2)
frame_dict[t2][t]["min"] = frame_dict[t2][t]["max"] = dist
frame_dict[t2][t]["rejected"] = dist >= distance_threshold
for t3 in t_F[i + 1 :]:
# For all the next times t3, use d(F(t), F(end_time)) to
# update the bounds on d(F(t), F(t3)). See if you can
# conclude on whether F(t) and F(t3) match.
t3t, t2t3 = frame_dict[t3][t], frame_dict[t2][t3]
t3t["max"] = min(t3t["max"], dist + t2t3["max"])
t3t["min"] = max(t3t["min"], dist - t2t3["max"], t2t3["min"] - dist)
if t3t["min"] > distance_threshold:
t3t["rejected"] = True
# Store all the good matches (end_time,t)
matching_frames += [
(t1, t, frame_dict[t1][t]["min"], frame_dict[t1][t]["max"])
for t1 in frame_dict
if (t1 != t) and not frame_dict[t1][t]["rejected"]
]
return FramesMatches([FramesMatch(*e) for e in matching_frames])
def select_scenes(
self, match_threshold, min_time_span, nomatch_threshold=None, time_distance=0
):
"""Select the scenes at which a video clip can be reproduced as the
smoothest possible way, mainly oriented for the creation of GIF images.
Parameters
----------
match_threshold : float
Maximum distance possible between frames. The smaller, the
better-looping the GIFs are.
min_time_span : float
Minimum duration for a scene. Only matches with a duration longer
than the value passed to this parameters will be extracted.
nomatch_threshold : float, optional
Minimum distance possible between frames. If is ``None``, then it is
chosen equal to ``match_threshold``.
time_distance : float, optional
Minimum time offset possible between matches.
Returns
-------
FramesMatches : New instance of the class with the selected scenes.
Examples
--------
.. code:: python
from pprint import pprint
from moviepy import *
from moviepy.video.tools.cuts import FramesMatches
ch_clip = VideoFileClip("media/chaplin.mp4").subclipped(1, 4)
mirror_and_clip = [ch_clip.with_effects([vfx.TimeMirror()]), ch_clip]
clip = concatenate_videoclips(mirror_and_clip)
result = FramesMatches.from_clip(clip, 10, 3).select_scenes(
1, 2, nomatch_threshold=0,
)
print(result)
# [(1.0000, 4.0000, 0.0000, 0.0000),
# (1.1600, 3.8400, 0.0000, 0.0000),
# (1.2800, 3.7200, 0.0000, 0.0000),
# (1.4000, 3.6000, 0.0000, 0.0000)]
"""
if nomatch_threshold is None:
nomatch_threshold = match_threshold
dict_starts = defaultdict(lambda: [])
for start, end, min_distance, max_distance in self:
dict_starts[start].append([end, min_distance, max_distance])
starts_ends = sorted(dict_starts.items(), key=lambda k: k[0])
result = []
min_start = 0
for start, ends_distances in starts_ends:
if start < min_start:
continue
ends = [end for (end, min_distance, max_distance) in ends_distances]
great_matches = [
(end, min_distance, max_distance)
for (end, min_distance, max_distance) in ends_distances
if max_distance < match_threshold
]
great_long_matches = [
(end, min_distance, max_distance)
for (end, min_distance, max_distance) in great_matches
if (end - start) > min_time_span
]
if not great_long_matches:
continue # No GIF can be made starting at this time
poor_matches = {
end
for (end, min_distance, max_distance) in ends_distances
if min_distance > nomatch_threshold
}
short_matches = {end for end in ends if (end - start) <= 0.6}
if not poor_matches.intersection(short_matches):
continue
end = max(end for (end, min_distance, max_distance) in great_long_matches)
end, min_distance, max_distance = next(
e for e in great_long_matches if e[0] == end
)
result.append(FramesMatch(start, end, min_distance, max_distance))
min_start = start + time_distance
return FramesMatches(result)
def write_gifs(self, clip, gifs_dir, **kwargs):
"""Extract the matching frames represented by the instance from a clip
and write them as GIFs in a directory, one GIF for each matching frame.
Parameters
----------
clip : video.VideoClip.VideoClip
A video clip whose frames scenes you want to obtain as GIF images.
gif_dir : str
Directory in which the GIF images will be written.
kwargs
Passed as ``clip.write_gif`` optional arguments.
Examples
--------
.. code:: python
import os
from pprint import pprint
from moviepy import *
from moviepy.video.tools.cuts import FramesMatches
ch_clip = VideoFileClip("media/chaplin.mp4").subclipped(1, 4)
clip = concatenate_videoclips([ch_clip.time_mirror(), ch_clip])
result = FramesMatches.from_clip(clip, 10, 3).select_scenes(
1, 2, nomatch_threshold=0,
)
os.mkdir("foo")
result.write_gifs(clip, "foo")
# MoviePy - Building file foo/00000100_00000400.gif with imageio.
# MoviePy - Building file foo/00000115_00000384.gif with imageio.
# MoviePy - Building file foo/00000128_00000372.gif with imageio.
# MoviePy - Building file foo/00000140_00000360.gif with imageio.
"""
for start, end, _, _ in self:
name = "%s/%08d_%08d.gif" % (gifs_dir, 100 * start, 100 * end)
clip.subclipped(start, end).write_gif(name, **kwargs)
@use_clip_fps_by_default
def detect_scenes(
clip=None, luminosities=None, luminosity_threshold=10, logger="bar", fps=None
):
"""Detects scenes of a clip based on luminosity changes.
Note that for large clip this may take some time.
Returns
-------
tuple : cuts, luminosities
cuts is a series of cuts [(0,t1), (t1,t2),...(...,tf)]
luminosities are the luminosities computed for each
frame of the clip.
Parameters
----------
clip : video.VideoClip.VideoClip, optional
A video clip. Can be None if a list of luminosities is
provided instead. If provided, the luminosity of each
frame of the clip will be computed. If the clip has no
'fps' attribute, you must provide it.
luminosities : list, optional
A list of luminosities, e.g. returned by detect_scenes
in a previous run.
luminosity_threshold : float, optional
Determines a threshold above which the 'luminosity jumps'
will be considered as scene changes. A scene change is defined
as a change between 2 consecutive frames that is larger than
(avg * thr) where avg is the average of the absolute changes
between consecutive frames.
logger : str, optional
Either ``"bar"`` for progress bar or ``None`` or any Proglog logger.
fps : int, optional
Frames per second value. Must be provided if you provide
no clip or a clip without fps attribute.
"""
if luminosities is None:
luminosities = [
f.sum() for f in clip.iter_frames(fps=fps, dtype="uint32", logger=logger)
]
luminosities = np.array(luminosities, dtype=float)
if clip is not None:
end = clip.duration
else:
end = len(luminosities) * (1.0 / fps)
luminosity_diffs = abs(np.diff(luminosities))
avg = luminosity_diffs.mean()
luminosity_jumps = (
1 + np.array(np.nonzero(luminosity_diffs > luminosity_threshold * avg))[0]
)
timings = [0] + list((1.0 / fps) * luminosity_jumps) + [end]
cuts = [(t1, t2) for t1, t2 in zip(timings, timings[1:])]
return cuts, luminosities

View File

@@ -0,0 +1,319 @@
"""Deals with making images (np arrays). It provides drawing
methods that are difficult to do with the existing Python libraries.
"""
import numpy as np
def color_gradient(
size,
p1,
p2=None,
vector=None,
radius=None,
color_1=0.0,
color_2=1.0,
shape="linear",
offset=0,
):
"""Draw a linear, bilinear, or radial gradient.
The result is a picture of size ``size``, whose color varies
gradually from color `color_1` in position ``p1`` to color ``color_2``
in position ``p2``.
If it is a RGB picture the result must be transformed into
a 'uint8' array to be displayed normally:
Parameters
----------
size : tuple or list
Size (width, height) in pixels of the final image array.
p1 : tuple or list
Position for the first coordinate of the gradient in pixels (x, y).
The color 'before' ``p1`` is ``color_1`` and it gradually changes in
the direction of ``p2`` until it is ``color_2`` when it reaches ``p2``.
p2 : tuple or list, optional
Position for the second coordinate of the gradient in pixels (x, y).
Coordinates (x, y) of the limit point for ``color_1``
and ``color_2``.
vector : tuple or list, optional
A vector (x, y) in pixels that can be provided instead of ``p2``.
``p2`` is then defined as (p1 + vector).
color_1 : tuple or list, optional
Starting color for the gradient. As default, black. Either floats
between 0 and 1 (for gradients used in masks) or [R, G, B] arrays
(for colored gradients).
color_2 : tuple or list, optional
Color for the second point in the gradient. As default, white. Either
floats between 0 and 1 (for gradients used in masks) or [R, G, B]
arrays (for colored gradients).
shape : str, optional
Shape of the gradient. Can be either ``"linear"``, ``"bilinear"`` or
``"circular"``. In a linear gradient the color varies in one direction,
from point ``p1`` to point ``p2``. In a bilinear gradient it also
varies symmetrically from ``p1`` in the other direction. In a circular
gradient it goes from ``color_1`` to ``color_2`` in all directions.
radius : float, optional
If ``shape="radial"``, the radius of the gradient is defined with the
parameter ``radius``, in pixels.
offset : float, optional
Real number between 0 and 1 indicating the fraction of the vector
at which the gradient actually starts. For instance if ``offset``
is 0.9 in a gradient going from p1 to p2, then the gradient will
only occur near p2 (before that everything is of color ``color_1``)
If the offset is 0.9 in a radial gradient, the gradient will
occur in the region located between 90% and 100% of the radius,
this creates a blurry disc of radius ``d(p1, p2)``.
Returns
-------
image
An Numpy array of dimensions (width, height, n_colors) of type float
representing the image of the gradient.
Examples
--------
.. code:: python
color_gradient((10, 1), (0, 0), p2=(10, 0)) # from white to black
#[[1. 0.9 0.8 0.7 0.6 0.5 0.4 0.3 0.2 0.1]]
# from red to green
color_gradient(
(10, 1), (0, 0),
p2=(10, 0),
color_1=(255, 0, 0),
color_2=(0, 255, 0)
)
# [[[ 0. 255. 0. ]
# [ 25.5 229.5 0. ]
# [ 51. 204. 0. ]
# [ 76.5 178.5 0. ]
# [102. 153. 0. ]
# [127.5 127.5 0. ]
# [153. 102. 0. ]
# [178.5 76.5 0. ]
# [204. 51. 0. ]
# [229.5 25.5 0. ]]]
"""
# np-arrayize and change x,y coordinates to y,x
w, h = size
color_1 = np.array(color_1).astype(float)
color_2 = np.array(color_2).astype(float)
if shape == "bilinear":
if vector is None:
if p2 is None:
raise ValueError("You must provide either 'p2' or 'vector'")
vector = np.array(p2) - np.array(p1)
m1, m2 = [
color_gradient(
size,
p1,
vector=v,
color_1=1.0,
color_2=0.0,
shape="linear",
offset=offset,
)
for v in [vector, [-v for v in vector]]
]
arr = np.maximum(m1, m2)
if color_1.size > 1:
arr = np.dstack(3 * [arr])
return arr * color_1 + (1 - arr) * color_2
p1 = np.array(p1[::-1]).astype(float)
M = np.dstack(np.meshgrid(range(w), range(h))[::-1]).astype(float)
if shape == "linear":
if vector is None:
if p2 is not None:
vector = np.array(p2[::-1]) - p1
else:
raise ValueError("You must provide either 'p2' or 'vector'")
else:
vector = np.array(vector[::-1])
norm = np.linalg.norm(vector)
n_vec = vector / norm**2 # norm 1/norm(vector)
p1 = p1 + offset * vector
arr = (M - p1).dot(n_vec) / (1 - offset)
arr = np.minimum(1, np.maximum(0, arr))
if color_1.size > 1:
arr = np.dstack(3 * [arr])
return arr * color_1 + (1 - arr) * color_2
elif shape == "radial":
if (radius or 0) == 0:
arr = np.ones((h, w))
else:
arr = (np.sqrt(((M - p1) ** 2).sum(axis=2))) - offset * radius
arr = arr / ((1 - offset) * radius)
arr = np.minimum(1.0, np.maximum(0, arr))
if color_1.size > 1:
arr = np.dstack(3 * [arr])
return (1 - arr) * color_1 + arr * color_2
raise ValueError("Invalid shape, should be either 'radial', 'linear' or 'bilinear'")
def color_split(
size,
x=None,
y=None,
p1=None,
p2=None,
vector=None,
color_1=0,
color_2=1.0,
gradient_width=0,
):
"""Make an image split in 2 colored regions.
Returns an array of size ``size`` divided in two regions called 1 and
2 in what follows, and which will have colors color_1 and color_2
respectively.
Parameters
----------
x : int, optional
If provided, the image is split horizontally in x, the left
region being region 1.
y : int, optional
If provided, the image is split vertically in y, the top region
being region 1.
p1, p2: tuple or list, optional
Positions (x1, y1), (x2, y2) in pixels, where the numbers can be
floats. Region 1 is defined as the whole region on the left when
going from ``p1`` to ``p2``.
p1, vector: tuple or list, optional
``p1`` is (x1,y1) and vector (v1,v2), where the numbers can be
floats. Region 1 is then the region on the left when starting
in position ``p1`` and going in the direction given by ``vector``.
gradient_width : float, optional
If not zero, the split is not sharp, but gradual over a region of
width ``gradient_width`` (in pixels). This is preferable in many
situations (for instance for antialiasing).
Examples
--------
.. code:: python
size = [200, 200]
# an image with all pixels with x<50 =0, the others =1
color_split(size, x=50, color_1=0, color_2=1)
# an image with all pixels with y<50 red, the others green
color_split(size, x=50, color_1=[255, 0, 0], color_2=[0, 255, 0])
# An image split along an arbitrary line (see below)
color_split(size, p1=[20, 50], p2=[25, 70], color_1=0, color_2=1)
"""
if gradient_width or ((x is None) and (y is None)):
if p2 is not None:
vector = np.array(p2) - np.array(p1)
elif x is not None:
vector = np.array([0, -1.0])
p1 = np.array([x, 0])
elif y is not None:
vector = np.array([1.0, 0.0])
p1 = np.array([0, y])
x, y = vector
vector = np.array([y, -x]).astype("float")
norm = np.linalg.norm(vector)
vector = max(0.1, gradient_width) * vector / norm
return color_gradient(
size, p1, vector=vector, color_1=color_1, color_2=color_2, shape="linear"
)
else:
w, h = size
shape = (h, w) if np.isscalar(color_1) else (h, w, len(color_1))
arr = np.zeros(shape)
if x:
arr[:, :x] = color_1
arr[:, x:] = color_2
elif y:
arr[:y] = color_1
arr[y:] = color_2
return arr
def circle(screensize, center, radius, color=1.0, bg_color=0, blur=1):
"""Draw an image with a circle.
Draws a circle of color ``color``, on a background of color ``bg_color``,
on a screen of size ``screensize`` at the position ``center=(x, y)``,
with a radius ``radius`` but slightly blurred on the border by ``blur``
pixels.
Parameters
----------
screensize : tuple or list
Size of the canvas.
center : tuple or list
Center of the circle.
radius : float
Radius of the circle, in pixels.
bg_color : tuple or float, optional
Color for the background of the canvas. As default, black.
blur : float, optional
Blur for the border of the circle.
Examples
--------
.. code:: python
from moviepy.video.tools.drawing import circle
circle(
(5, 5), # size
(2, 2), # center
2, # radius
)
# array([[0. , 0. , 0. , 0. , 0. ],
# [0. , 0.58578644, 1. , 0.58578644, 0. ],
# [0. , 1. , 1. , 1. , 0. ],
# [0. , 0.58578644, 1. , 0.58578644, 0. ],
# [0. , 0. , 0. , 0. , 0. ]])
"""
offset = 1.0 * (radius - blur) / radius if radius else 0
return color_gradient(
screensize,
p1=center,
radius=radius,
color_1=color,
color_2=bg_color,
shape="radial",
offset=offset,
)

View File

@@ -0,0 +1,238 @@
"""Classes for easy interpolation of trajectories and curves."""
import numpy as np
class Interpolator:
"""Poorman's linear interpolator.
Parameters
----------
tt : list, optional
List of time frames for the interpolator.
ss : list, optional
List of values for the interpolator.
ttss : list, optional
Lists of time frames and their correspondients values for the
interpolator. This argument can be used instead of ``tt`` and ``ss``
to instantiate the interpolator using an unique argument.
left : float, optional
Value to return when ``t < tt[0]``.
right : float, optional
Value to return when ``t > tt[-1]``.
Examples
--------
.. code:: python
# instantiate using `tt` and `ss`
interpolator = Interpolator(tt=[0, 1, 2], ss=[3, 4, 5])
# instantiate using `ttss`
interpolator = Interpolator(ttss=[[0, 3], [1, 4], [2, 5]]) # [t, value]
"""
def __init__(self, tt=None, ss=None, ttss=None, left=None, right=None):
if ttss is not None:
tt, ss = zip(*ttss)
self.tt = 1.0 * np.array(tt)
self.ss = 1.0 * np.array(ss)
self.left = left
self.right = right
self.tmin, self.tmax = min(tt), max(tt)
def __call__(self, t):
"""Interpolates ``t``.
Parameters
----------
t : float
Time frame for which the correspondent value will be returned.
"""
return np.interp(t, self.tt, self.ss, self.left, self.right)
class Trajectory:
"""Trajectory compound by time frames and (x, y) pixels.
It's designed as an interpolator, so you can get the position at a given
time ``t``. You can instantiate it from a file using the methods
``from_file`` and ``load_list``.
Parameters
----------
tt : list or numpy.ndarray
Time frames.
xx : list or numpy.ndarray
X positions in the trajectory.
yy : list or numpy.ndarray
Y positions in the trajectory.
Examples
--------
>>> trajectory = Trajectory([0, .166, .333], [554, 474, 384], [100, 90, 91])
"""
def __init__(self, tt, xx, yy):
self.tt = 1.0 * np.array(tt)
self.xx = np.array(xx)
self.yy = np.array(yy)
self.update_interpolators()
def __call__(self, t):
"""Interpolates the trajectory at the given time ``t``.
Parameters
----------
t : float
Time for which to the corresponding position will be returned.
"""
return np.array([self.xi(t), self.yi(t)])
def addx(self, x):
"""Adds a value to the ``xx`` position of the trajectory.
Parameters
----------
x : int
Value added to ``xx`` in the trajectory.
Returns
-------
Trajectory : new instance with the new X position included.
"""
return Trajectory(self.tt, self.xx + x, self.yy)
def addy(self, y):
"""Adds a value to the ``yy`` position of the trajectory.
Parameters
----------
y : int
Value added to ``yy`` in the trajectory.
Returns
-------
Trajectory : new instance with the new Y position included.
"""
return Trajectory(self.tt, self.xx, self.yy + y)
def update_interpolators(self):
"""Updates the internal X and Y position interpolators for the instance."""
self.xi = Interpolator(self.tt, self.xx)
self.yi = Interpolator(self.tt, self.yy)
def txy(self, tms=False):
"""Returns all times with the X and Y values of each position.
Parameters
----------
tms : bool, optional
If is ``True``, the time will be returned in milliseconds.
"""
return zip((1000 if tms else 1) * self.tt, self.xx, self.yy)
def to_file(self, filename):
"""Saves the trajectory data in a text file.
Parameters
----------
filename : str
Path to the location of the new trajectory text file.
"""
np.savetxt(
filename,
np.array(list(self.txy(tms=True))),
fmt="%d",
delimiter="\t",
)
@staticmethod
def from_file(filename):
"""Instantiates an object of Trajectory using a data text file.
Parameters
----------
filename : str
Path to the location of trajectory text file to load.
Returns
-------
Trajectory : new instance loaded from text file.
"""
arr = np.loadtxt(filename, delimiter="\t")
tt, xx, yy = arr.T
return Trajectory(1.0 * tt / 1000, xx, yy)
@staticmethod
def save_list(trajs, filename):
"""Saves a set of trajectories into a text file.
Parameters
----------
trajs : list
List of trajectories to be saved.
filename : str
Path of the text file that will store the trajectories data.
"""
N = len(trajs)
arr = np.hstack([np.array(list(t.txy(tms=True))) for t in trajs])
np.savetxt(
filename,
arr,
fmt="%d",
delimiter="\t",
header="\t".join(N * ["t(ms)", "x", "y"]),
)
@staticmethod
def load_list(filename):
"""Loads a list of trajectories from a data text file.
Parameters
----------
filename : str
Path of the text file that stores the data of a set of trajectories.
Returns
-------
list : List of trajectories loaded from the file.
"""
arr = np.loadtxt(filename, delimiter="\t").T
Nlines = arr.shape[0]
return [
Trajectory(tt=1.0 * a[0] / 1000, xx=a[1], yy=a[2])
for a in np.split(arr, Nlines / 3)
]

View File

@@ -0,0 +1,198 @@
"""Experimental module for subtitles support."""
import re
import numpy as np
from moviepy.decorators import convert_path_to_string
from moviepy.tools import convert_to_seconds
from moviepy.video.VideoClip import TextClip, VideoClip
class SubtitlesClip(VideoClip):
"""A Clip that serves as "subtitle track" in videos.
One particularity of this class is that the images of the
subtitle texts are not generated beforehand, but only if
needed.
Parameters
----------
subtitles
Either the name of a file as a string or path-like object, or a list
font
Path to a font file to be used. Optional if make_textclip is provided.
make_textclip
A custom function to use for text clip generation. If None, a TextClip
will be generated.
The function must take a text as argument and return a VideoClip
to be used as caption
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)
Examples
--------
.. code:: python
from moviepy.video.tools.subtitles import SubtitlesClip
from moviepy.video.io.VideoFileClip import VideoFileClip
generator = lambda text: TextClip(text, font='./path/to/font.ttf',
font_size=24, color='white')
sub = SubtitlesClip("subtitles.srt", make_textclip=generator, encoding='utf-8')
myvideo = VideoFileClip("myvideo.avi")
final = CompositeVideoClip([clip, subtitles])
final.write_videofile("final.mp4", fps=myvideo.fps)
"""
def __init__(self, subtitles, font=None, make_textclip=None, encoding=None):
VideoClip.__init__(self, has_constant_size=False)
if not isinstance(subtitles, list):
# `subtitles` is a string or path-like object
subtitles = file_to_subtitles(subtitles, encoding=encoding)
# subtitles = [(map(convert_to_seconds, times), text)
# for times, text in subtitles]
self.subtitles = subtitles
self.textclips = dict()
self.font = font
if make_textclip is None:
if self.font is None:
raise ValueError("Argument font is required if make_textclip is None.")
def make_textclip(txt):
return TextClip(
font=self.font,
text=txt,
font_size=24,
color="#ffffff",
stroke_color="#000000",
stroke_width=1,
)
self.make_textclip = make_textclip
self.start = 0
self.duration = max([tb for ((ta, tb), txt) in self.subtitles])
self.end = self.duration
def add_textclip_if_none(t):
"""Will generate a textclip if it hasn't been generated asked
to generate it yet. If there is no subtitle to show at t, return
false.
"""
sub = [
((text_start, text_end), text)
for ((text_start, text_end), text) in self.textclips.keys()
if (text_start <= t < text_end)
]
if not sub:
sub = [
((text_start, text_end), text)
for ((text_start, text_end), text) in self.subtitles
if (text_start <= t < text_end)
]
if not sub:
return False
sub = sub[0]
if sub not in self.textclips.keys():
self.textclips[sub] = self.make_textclip(sub[1])
return sub
def frame_function(t):
sub = add_textclip_if_none(t)
return self.textclips[sub].get_frame(t) if sub else np.array([[[0, 0, 0]]])
def make_mask_frame(t):
sub = add_textclip_if_none(t)
return self.textclips[sub].mask.get_frame(t) if sub else np.array([[0]])
self.frame_function = frame_function
hasmask = bool(self.make_textclip("T").mask)
self.mask = VideoClip(make_mask_frame, is_mask=True) if hasmask else None
def in_subclip(self, start_time=None, end_time=None):
"""Returns a sequence of [(t1,t2), text] covering all the given subclip
from start_time to end_time. The first and last times will be cropped so as
to be exactly start_time and end_time if possible.
"""
def is_in_subclip(t1, t2):
try:
return (start_time <= t1 < end_time) or (start_time < t2 <= end_time)
except Exception:
return False
def try_cropping(t1, t2):
try:
return max(t1, start_time), min(t2, end_time)
except Exception:
return t1, t2
return [
(try_cropping(t1, t2), txt)
for ((t1, t2), txt) in self.subtitles
if is_in_subclip(t1, t2)
]
def __iter__(self):
return iter(self.subtitles)
def __getitem__(self, k):
return self.subtitles[k]
def __str__(self):
def to_srt(sub_element):
(start_time, end_time), text = sub_element
formatted_start_time = convert_to_seconds(start_time)
formatted_end_time = convert_to_seconds(end_time)
return "%s - %s\n%s" % (formatted_start_time, formatted_end_time, text)
return "\n\n".join(to_srt(sub) for sub in self.subtitles)
def match_expr(self, expr):
"""Matches a regular expression against the subtitles of the clip."""
return SubtitlesClip(
[sub for sub in self.subtitles if re.findall(expr, sub[1]) != []]
)
def write_srt(self, filename):
"""Writes an ``.srt`` file with the content of the clip."""
with open(filename, "w+") as file:
file.write(str(self))
@convert_path_to_string("filename")
def file_to_subtitles(filename, encoding=None):
"""Converts a srt file into subtitles.
The returned list is of the form ``[((start_time,end_time),'some text'),...]``
and can be fed to SubtitlesClip.
Only works for '.srt' format for the moment.
"""
times_texts = []
current_times = None
current_text = ""
with open(filename, "r", encoding=encoding) as file:
for line in file:
times = re.findall("([0-9]*:[0-9]*:[0-9]*,[0-9]*)", line)
if times:
current_times = [convert_to_seconds(t) for t in times]
elif line.strip() == "":
times_texts.append((current_times, current_text.strip("\n")))
current_times, current_text = None, ""
elif current_times:
current_text += line
return times_texts