Coverage for src/extratools_av/video/__init__.py: 0%
23 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-16 19:44 -0700
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-16 19:44 -0700
1from collections.abc import Iterable
2from datetime import timedelta
3from typing import Literal, cast
5from moviepy import Effect
6from moviepy.audio.AudioClip import AudioClip
7from moviepy.video.fx.AccelDecel import AccelDecel
8from moviepy.video.fx.Loop import Loop
9from moviepy.video.fx.MakeLoopable import MakeLoopable
10from moviepy.video.VideoClip import VideoClip
13def match_audio_duration(
14 video_clip: VideoClip,
15 audio_clip: AudioClip | float | Iterable[AudioClip | float],
16 *,
17 padding: timedelta = timedelta(seconds=0.25),
18 mode: Literal["scale", "loop"] = "scale",
19 loop_fadein: timedelta | None = timedelta(seconds=1),
20) -> VideoClip:
21 audio_clips: Iterable[AudioClip | float] = (
22 audio_clip if isinstance(audio_clip, Iterable)
23 else [audio_clip]
24 )
26 audio_duration: float = sum(
27 (
28 (
29 audio_clip.duration if isinstance(audio_clip, AudioClip)
30 else audio_clip
31 ) + padding.seconds
32 for audio_clip in audio_clips
33 ),
34 start=padding.seconds,
35 )
37 effects: list[Effect] = []
39 match mode:
40 case "scale":
41 effects.append(
42 AccelDecel(audio_duration),
43 )
44 case "loop":
45 if loop_fadein:
46 effects.append(
47 MakeLoopable(overlap_duration=loop_fadein.seconds),
48 )
50 effects.append(
51 Loop(duration=audio_duration),
52 )
53 case _:
54 raise ValueError
56 return cast(
57 "VideoClip",
58 video_clip.with_effects(effects),
59 )