diff --git a/app/models/schema.py b/app/models/schema.py index 2a688bd..3696fa3 100644 --- a/app/models/schema.py +++ b/app/models/schema.py @@ -18,6 +18,15 @@ class VideoConcatMode(str, Enum): sequential = "sequential" +class VideoTransitionMode(str, Enum): + none = None + shuffle = "Shuffle" + fade_in = "FadeIn" + fade_out = "FadeOut" + slide_in = "SlideIn" + slide_out = "SlideOut" + + class VideoAspect(str, Enum): landscape = "16:9" portrait = "9:16" @@ -44,44 +53,6 @@ class MaterialInfo: duration: int = 0 -# VoiceNames = [ -# # zh-CN -# "female-zh-CN-XiaoxiaoNeural", -# "female-zh-CN-XiaoyiNeural", -# "female-zh-CN-liaoning-XiaobeiNeural", -# "female-zh-CN-shaanxi-XiaoniNeural", -# -# "male-zh-CN-YunjianNeural", -# "male-zh-CN-YunxiNeural", -# "male-zh-CN-YunxiaNeural", -# "male-zh-CN-YunyangNeural", -# -# # "female-zh-HK-HiuGaaiNeural", -# # "female-zh-HK-HiuMaanNeural", -# # "male-zh-HK-WanLungNeural", -# # -# # "female-zh-TW-HsiaoChenNeural", -# # "female-zh-TW-HsiaoYuNeural", -# # "male-zh-TW-YunJheNeural", -# -# # en-US -# "female-en-US-AnaNeural", -# "female-en-US-AriaNeural", -# "female-en-US-AvaNeural", -# "female-en-US-EmmaNeural", -# "female-en-US-JennyNeural", -# "female-en-US-MichelleNeural", -# -# "male-en-US-AndrewNeural", -# "male-en-US-BrianNeural", -# "male-en-US-ChristopherNeural", -# "male-en-US-EricNeural", -# "male-en-US-GuyNeural", -# "male-en-US-RogerNeural", -# "male-en-US-SteffanNeural", -# ] - - class VideoParams(BaseModel): """ { @@ -102,11 +73,14 @@ class VideoParams(BaseModel): video_terms: Optional[str | list] = None # Keywords used to generate the video video_aspect: Optional[VideoAspect] = VideoAspect.portrait.value video_concat_mode: Optional[VideoConcatMode] = VideoConcatMode.random.value + video_transition_mode: Optional[VideoTransitionMode] = None video_clip_duration: Optional[int] = 5 video_count: Optional[int] = 1 video_source: Optional[str] = "pexels" - video_materials: Optional[List[MaterialInfo]] = None # Materials used to generate the video + video_materials: Optional[List[MaterialInfo]] = ( + None # Materials used to generate the video + ) video_language: Optional[str] = "" # auto detect diff --git a/app/services/task.py b/app/services/task.py index e3d9eb5..27b01a9 100644 --- a/app/services/task.py +++ b/app/services/task.py @@ -164,6 +164,7 @@ def generate_final_videos( video_concat_mode = ( params.video_concat_mode if params.video_count == 1 else VideoConcatMode.random ) + video_transition_mode = params.video_transition_mode _progress = 50 for i in range(params.video_count): @@ -178,6 +179,7 @@ def generate_final_videos( audio_file=audio_file, video_aspect=params.video_aspect, video_concat_mode=video_concat_mode, + video_transition_mode=video_transition_mode, max_clip_duration=params.video_clip_duration, threads=params.n_threads, ) diff --git a/app/services/utils/video_effects.py b/app/services/utils/video_effects.py index 3022cfc..6cba8eb 100644 --- a/app/services/utils/video_effects.py +++ b/app/services/utils/video_effects.py @@ -9,3 +9,13 @@ def fadein_transition(clip: Clip, t: float) -> Clip: # FadeOut def fadeout_transition(clip: Clip, t: float) -> Clip: return clip.with_effects([vfx.FadeOut(t)]) + + +# SlideIn +def slidein_transition(clip: Clip, t: float, side: str) -> Clip: + return clip.with_effects([vfx.SlideIn(t, side)]) + + +# SlideOut +def slideout_transition(clip: Clip, t: float, side: str) -> Clip: + return clip.with_effects([vfx.SlideOut(t, side)]) diff --git a/app/services/video.py b/app/services/video.py index b0d4732..f21dad2 100644 --- a/app/services/video.py +++ b/app/services/video.py @@ -19,7 +19,14 @@ from moviepy.video.tools.subtitles import SubtitlesClip from PIL import ImageFont from app.models import const -from app.models.schema import MaterialInfo, VideoAspect, VideoConcatMode, VideoParams +from app.models.schema import ( + MaterialInfo, + VideoAspect, + VideoConcatMode, + VideoParams, + VideoTransitionMode, +) +from app.services.utils import video_effects from app.utils import utils @@ -45,6 +52,7 @@ def combine_videos( audio_file: str, video_aspect: VideoAspect = VideoAspect.portrait, video_concat_mode: VideoConcatMode = VideoConcatMode.random, + video_transition_mode: VideoTransitionMode = None, max_clip_duration: int = 5, threads: int = 2, ) -> str: @@ -129,12 +137,34 @@ def combine_videos( f"resizing video to {video_width} x {video_height}, clip size: {clip_w} x {clip_h}" ) + shuffle_side = random.choice(["left", "right", "top", "bottom"]) + logger.info(f"Using transition mode: {video_transition_mode}") + if video_transition_mode.value == VideoTransitionMode.none.value: + clip = clip + elif video_transition_mode.value == VideoTransitionMode.fade_in.value: + clip = video_effects.fadein_transition(clip, 1) + elif video_transition_mode.value == VideoTransitionMode.fade_out.value: + clip = video_effects.fadeout_transition(clip, 1) + elif video_transition_mode.value == VideoTransitionMode.slide_in.value: + clip = video_effects.slidein_transition(clip, 1, shuffle_side) + elif video_transition_mode.value == VideoTransitionMode.slide_out.value: + clip = video_effects.slideout_transition(clip, 1, shuffle_side) + elif video_transition_mode.value == VideoTransitionMode.shuffle.value: + transition_funcs = [ + lambda c: video_effects.fadein_transition(c, 1), + lambda c: video_effects.fadeout_transition(c, 1), + lambda c: video_effects.slidein_transition(c, 1, shuffle_side), + lambda c: video_effects.slideout_transition(c, 1, shuffle_side), + ] + shuffle_transition = random.choice(transition_funcs) + clip = shuffle_transition(clip) + if clip.duration > max_clip_duration: clip = clip.subclipped(0, max_clip_duration) clips.append(clip) video_duration += clip.duration - + clips = [CompositeVideoClip([clip]) for clip in clips] video_clip = concatenate_videoclips(clips) video_clip = video_clip.with_fps(30) logger.info("writing") diff --git a/webui/Main.py b/webui/Main.py index 244c7e1..9338bce 100644 --- a/webui/Main.py +++ b/webui/Main.py @@ -8,7 +8,13 @@ from loguru import logger from app.config import config from app.models.const import FILE_TYPE_IMAGES, FILE_TYPE_VIDEOS -from app.models.schema import MaterialInfo, VideoAspect, VideoConcatMode, VideoParams +from app.models.schema import ( + MaterialInfo, + VideoAspect, + VideoConcatMode, + VideoParams, + VideoTransitionMode, +) from app.services import llm, voice from app.services import task as tm from app.utils import utils @@ -544,6 +550,25 @@ with middle_panel: video_concat_modes[selected_index][1] ) + # 视频转场模式 + video_transition_modes = [ + (tr("None"), VideoTransitionMode.none.value), + (tr("Shuffle"), VideoTransitionMode.shuffle.value), + (tr("FadeIn"), VideoTransitionMode.fade_in.value), + (tr("FadeOut"), VideoTransitionMode.fade_out.value), + (tr("SlideIn"), VideoTransitionMode.slide_in.value), + (tr("SlideOut"), VideoTransitionMode.slide_out.value), + ] + selected_index = st.selectbox( + tr("Video Transition Mode"), + options=range(len(video_transition_modes)), + format_func=lambda x: video_transition_modes[x][0], + index=0, + ) + params.video_transition_mode = VideoTransitionMode( + video_transition_modes[selected_index][1] + ) + video_aspect_ratios = [ (tr("Portrait"), VideoAspect.portrait.value), (tr("Landscape"), VideoAspect.landscape.value), diff --git a/webui/i18n/de.json b/webui/i18n/de.json index 506ff36..05c589c 100644 --- a/webui/i18n/de.json +++ b/webui/i18n/de.json @@ -16,6 +16,13 @@ "Video Concat Mode": "Videoverkettungsmodus", "Random": "Zufällige Verkettung (empfohlen)", "Sequential": "Sequentielle Verkettung", + "Video Transition Mode": "Video Übergangsmodus", + "None": "Kein Übergang", + "Shuffle": "Zufällige Übergänge", + "FadeIn": "FadeIn", + "FadeOut": "FadeOut", + "SlideIn": "SlideIn", + "SlideOut": "SlideOut", "Video Ratio": "Video-Seitenverhältnis", "Portrait": "Portrait 9:16", "Landscape": "Landschaft 16:9", diff --git a/webui/i18n/en.json b/webui/i18n/en.json index 37fadc7..a30063f 100644 --- a/webui/i18n/en.json +++ b/webui/i18n/en.json @@ -16,6 +16,13 @@ "Video Concat Mode": "Video Concatenation Mode", "Random": "Random Concatenation (Recommended)", "Sequential": "Sequential Concatenation", + "Video Transition Mode": "Video Transition Mode", + "None": "None", + "Shuffle": "Shuffle", + "FadeIn": "FadeIn", + "FadeOut": "FadeOut", + "SlideIn": "SlideIn", + "SlideOut": "SlideOut", "Video Ratio": "Video Aspect Ratio", "Portrait": "Portrait 9:16", "Landscape": "Landscape 16:9", diff --git a/webui/i18n/pt.json b/webui/i18n/pt.json index 7910168..06bded2 100644 --- a/webui/i18n/pt.json +++ b/webui/i18n/pt.json @@ -16,6 +16,13 @@ "Video Concat Mode": "Modo de Concatenação de Vídeo", "Random": "Concatenação Aleatória (Recomendado)", "Sequential": "Concatenação Sequencial", + "Video Transition Mode": "Modo de Transição de Vídeo", + "None": "Nenhuma Transição", + "Shuffle": "Transição Aleatória", + "FadeIn": "FadeIn", + "FadeOut": "FadeOut", + "SlideIn": "SlideIn", + "SlideOut": "SlideOut", "Video Ratio": "Proporção do Vídeo", "Portrait": "Retrato 9:16", "Landscape": "Paisagem 16:9", diff --git a/webui/i18n/vi.json b/webui/i18n/vi.json index 651706d..e5847c8 100644 --- a/webui/i18n/vi.json +++ b/webui/i18n/vi.json @@ -16,6 +16,13 @@ "Video Concat Mode": "Chế Độ Nối Video", "Random": "Nối Ngẫu Nhiên (Được Khuyến Nghị)", "Sequential": "Nối Theo Thứ Tự", + "Video Transition Mode": "Chế Độ Chuyển Đổi Video", + "None": "Không Có Chuyển Đổi", + "Shuffle": "Chuyển Đổi Ngẫu Nhiên", + "FadeIn": "FadeIn", + "FadeOut": "FadeOut", + "SlideIn": "SlideIn", + "SlideOut": "SlideOut", "Video Ratio": "Tỷ Lệ Khung Hình Video", "Portrait": "Dọc 9:16", "Landscape": "Ngang 16:9", diff --git a/webui/i18n/zh.json b/webui/i18n/zh.json index d3e7077..5cabd19 100644 --- a/webui/i18n/zh.json +++ b/webui/i18n/zh.json @@ -16,6 +16,13 @@ "Video Concat Mode": "视频拼接模式", "Random": "随机拼接(推荐)", "Sequential": "顺序拼接", + "Video Transition Mode": "视频转场模式", + "None": "无转场", + "Shuffle": "随机转场", + "FadeIn": "渐入", + "FadeOut": "渐出", + "SlideIn": "滑动入", + "SlideOut": "滑动出", "Video Ratio": "视频比例", "Portrait": "竖屏 9:16(抖音视频)", "Landscape": "横屏 16:9(西瓜视频)",