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

1from collections.abc import Iterable 

2from datetime import timedelta 

3from typing import Literal, cast 

4 

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 

11 

12 

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 ) 

25 

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 ) 

36 

37 effects: list[Effect] = [] 

38 

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 ) 

49 

50 effects.append( 

51 Loop(duration=audio_duration), 

52 ) 

53 case _: 

54 raise ValueError 

55 

56 return cast( 

57 "VideoClip", 

58 video_clip.with_effects(effects), 

59 )