Skip to content

base

Classes:

Functions:

BaseEffect

BaseEffect(**kwargs: Any)

Bases: ABC

kwargs: Effect-specific keyword arguments

Methods:

Source code in src/movfx/effects/base.py
36
37
38
39
40
41
def __init__(self, **kwargs: Any) -> None:
    """
    title: Initialize the effect with optional keyword arguments
    parameters:
        kwargs: Effect-specific keyword arguments
    """

build_clip

build_clip(
    img_from: ndarray,
    img_to: ndarray,
    duration: float,
    fps: int = 30,
) -> VideoClip
Source code in src/movfx/effects/base.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def build_clip(
    self,
    img_from: np.ndarray,
    img_to: np.ndarray,
    duration: float,
    fps: int = 30,
) -> VideoClip:
    """
    title: Build a moviepy VideoClip from the effect
    parameters:
        img_from: Source image as a NumPy RGB array
        img_to: Destination image as a NumPy RGB array
        duration: Duration of the clip in seconds
        fps: Frames per second
    returns: A moviepy VideoClip with the transition
    """

    def make_frame(t: float) -> np.ndarray:
        progress = t / duration if duration > 0 else 1.0
        progress = max(0.0, min(1.0, progress))
        return self.render_frame(img_from, img_to, progress)

    clip = VideoClip(make_frame, duration=duration)
    clip = clip.with_fps(fps)
    return clip

render_frame abstractmethod

render_frame(
    img_from: ndarray, img_to: ndarray, progress: float
) -> ndarray
Source code in src/movfx/effects/base.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@abstractmethod
def render_frame(
    self,
    img_from: np.ndarray,
    img_to: np.ndarray,
    progress: float,
) -> np.ndarray:
    """
    title: Render a single transition frame
    parameters:
        img_from: Source image as a NumPy RGB array
        img_to: Destination image as a NumPy RGB array
        progress: >
            Transition progress from 0.0 (fully source)
            to 1.0 (fully destination)
    returns: Blended frame as a NumPy RGB uint8 array
    """

load_image

load_image(path: str | Path) -> ndarray
Source code in src/movfx/effects/base.py
132
133
134
135
136
137
138
139
140
def load_image(path: str | Path) -> np.ndarray:
    """
    title: Load an image file as a NumPy RGB array
    parameters:
        path: Path to the image file
    returns: NumPy array of shape (H, W, 3) with dtype uint8
    """
    img = Image.open(path).convert("RGB")
    return np.array(img)

resize_to_match

resize_to_match(
    img_a: ndarray, img_b: ndarray
) -> tuple[ndarray, ndarray]
Source code in src/movfx/effects/base.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def resize_to_match(
    img_a: np.ndarray,
    img_b: np.ndarray,
) -> tuple[np.ndarray, np.ndarray]:
    """
    title: Resize two images to the same dimensions
    summary: |
        Uses the maximum width and height from both images.
        Images are resized with Lanczos resampling.
    parameters:
        img_a: First image array
        img_b: Second image array
    returns: Tuple of resized image arrays with matching dimensions
    """
    h = max(img_a.shape[0], img_b.shape[0])
    w = max(img_a.shape[1], img_b.shape[1])

    def _resize(arr: np.ndarray) -> np.ndarray:
        if arr.shape[0] == h and arr.shape[1] == w:
            return arr
        pil = Image.fromarray(arr)
        pil = pil.resize((w, h), Image.Resampling.LANCZOS)
        return np.array(pil)

    return _resize(img_a), _resize(img_b)

resolve_duration

resolve_duration(
    duration: float,
    effect: BaseEffect,
    sound_path: str | Path | None = None,
) -> float
Source code in src/movfx/effects/base.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def resolve_duration(
    duration: float,
    effect: BaseEffect,
    sound_path: str | Path | None = None,
) -> float:
    """
    title: Resolve the final duration for a transition
    summary: |
        Applies the duration resolution logic:
        1. Clamp to effect min_duration if needed (with warning).
        2. Extend to sound duration if sound is longer (with warning).
    parameters:
        duration: Requested duration in seconds
        effect: The effect instance (may define min_duration)
        sound_path: Optional path to an audio file
    returns: Resolved duration in seconds
    """
    final = duration

    if effect.min_duration is not None and final < effect.min_duration:
        warnings.warn(
            f"Requested duration {final}s is below the minimum "
            f"({effect.min_duration}s) for effect '{effect.name}'. "
            f"Using {effect.min_duration}s.",
            stacklevel=2,
        )
        final = effect.min_duration

    if sound_path is not None:
        audio = AudioFileClip(str(sound_path))
        sound_dur = audio.duration
        audio.close()
        if final < sound_dur:
            warnings.warn(
                f"Requested duration {final}s is shorter than the "
                f"sound duration ({sound_dur:.2f}s). "
                f"Using sound duration.",
                stacklevel=2,
            )
            final = sound_dur

    return final