# full_vertical_video.py import subprocess import random from pathlib import Path from moviepy.editor import VideoFileClip from pydub import AudioSegment from PIL import Image, ImageDraw class FullVerticalVideoGenerator: def __init__(self, source_folder, temp_folder, preview_generator, vertical_resolution=(1080, 1920), fade_duration=1, gradients_folder=None): """ Ініціалізація генератора вертикальних відео Args: source_folder: Папка з фоновими відео temp_folder: Папка для тимчасових файлів preview_generator: Об'єкт PreviewGenerator для створення превью vertical_resolution: Розмір вертикального відео (ширина, висота) fade_duration: Тривалість fade ефектів gradients_folder: Не використовується (залишено для сумісності) """ self.source_folder = Path(source_folder) self.temp_folder = Path(temp_folder) self.preview_generator = preview_generator self.vertical_resolution = vertical_resolution # (1080, 1920) - 9:16 self.fade_duration = fade_duration def apply_safe_fade_filters(self, use_dur, base_filters): """Застосовує fade фільтри безпечно для вертикального відео""" fade_in_duration = min(0.3, use_dur * 0.3) fade_out_duration = min(0.3, use_dur * 0.3) if fade_in_duration + fade_out_duration > use_dur: fade_in_duration = use_dur * 0.4 fade_out_duration = use_dur * 0.4 fade_out_start = max(0, use_dur - fade_out_duration) fade_filters = [] if fade_in_duration > 0.05: fade_filters.append(f"fade=t=in:st=0:d={fade_in_duration}") if fade_out_duration > 0.05 and fade_out_start > fade_in_duration: fade_filters.append(f"fade=t=out:st={fade_out_start}:d={fade_out_duration}") all_filters = [base_filters] + fade_filters return ",".join(all_filters) def create_preview_video_segments_vertical(self, story_id, preview_duration): """ Створює вертикальні фонові відео сегменти для preview частини Args: story_id: ID історії для іменування файлів preview_duration: Тривалість preview в секундах Returns: tuple: (шлях до об'єднаного preview відео, ітератор фонових відео) """ bg_videos = list(self.source_folder.glob("*.mp4")) random.shuffle(bg_videos) bg_iter = iter(bg_videos) preview_clips = [] preview_duration_collected = 0 while preview_duration_collected < preview_duration: try: bg_video = next(bg_iter) except StopIteration: # Якщо відео закінчились, починаємо знову bg_videos_reset = list(self.source_folder.glob("*.mp4")) random.shuffle(bg_videos_reset) bg_iter = iter(bg_videos_reset) bg_video = next(bg_iter) clip = VideoFileClip(str(bg_video)) remaining_preview_duration = preview_duration - preview_duration_collected use_duration = min(clip.duration, remaining_preview_duration) clip.close() if use_duration <= 0: break preview_clip_tmp = self.temp_folder / f"{story_id}_full_v_preview_bg_{len(preview_clips):02d}.mp4" # Створюємо вертикальний кліп (9:16) subprocess.run([ "ffmpeg", "-y", "-i", str(bg_video), "-ss", "0", "-t", str(use_duration), "-vf", f"scale='max({self.vertical_resolution[0]},iw*{self.vertical_resolution[1]}/ih)':'max({self.vertical_resolution[1]},ih*{self.vertical_resolution[0]}/iw)',crop={self.vertical_resolution[0]}:{self.vertical_resolution[1]},fps=30", "-an", "-c:v", "h264_nvenc", "-preset", "p4", "-b:v", "5M", str(preview_clip_tmp) ], check=True) preview_clips.append(preview_clip_tmp) preview_duration_collected += use_duration # Об'єднуємо всі preview кліпи if len(preview_clips) > 1: preview_concat_list = self.temp_folder / f"{story_id}_full_v_preview_concat.txt" with open(preview_concat_list, "w", encoding="utf-8") as f: for clip in preview_clips: f.write(f"file '{clip.as_posix()}'\n") preview_combined = self.temp_folder / f"{story_id}_full_v_preview_combined.mp4" subprocess.run([ "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", str(preview_concat_list), "-c:v", "h264_nvenc", "-b:v", "5M", "-preset", "p4", str(preview_combined) ], check=True) else: preview_combined = preview_clips[0] return preview_combined, bg_iter def create_preview_with_overlay_vertical(self, story_id, preview_combined, horizontal_preview_path, preview_duration): """ Накладає вертикальне preview зображення на фонове відео Args: story_id: ID історії preview_combined: Шлях до об'єднаного фонового відео horizontal_preview_path: Шлях до горизонтального preview зображення preview_duration: Тривалість preview Returns: Path: Шлях до preview відео з накладеним зображенням """ # Створюємо вертикальне preview зображення preview_img_path = self.temp_folder / f"{story_id}_preview_full_vertical.png" self.preview_generator.draw_vertical_preview(horizontal_preview_path, preview_img_path) # Накладаємо preview зображення на відео preview_video = self.temp_folder / f"{story_id}_full_v_preview_video.mp4" subprocess.run([ "ffmpeg", "-y", "-i", str(preview_combined), "-i", str(preview_img_path), "-filter_complex", "[1:v]format=rgba[overlay];[0:v][overlay]overlay=(W-w)/2:(H-h)/2:format=auto", "-t", str(preview_duration), "-c:v", "h264_nvenc", "-preset", "p4", "-b:v", "5M", str(preview_video) ], check=True) return preview_video def create_story_video_segments_vertical(self, story_id, bg_iter, target_duration, duration_collected): """ Створює вертикальні відео сегменти для основної частини історії Args: story_id: ID історії bg_iter: Ітератор фонових відео target_duration: Цільова тривалість всього відео duration_collected: Вже зібрана тривалість Returns: list: Список шляхів до створених відео сегментів """ used_clips = [] # Продовжуємо з того місця де зупинились в bg_iter для story частини for idx, bg in enumerate(bg_iter): use_dur = target_duration - duration_collected if use_dur <= 0: break clip = VideoFileClip(str(bg)) use_dur = min(clip.duration, use_dur) clip.close() if use_dur <= 0: break temp_out = self.temp_folder / f"full_v_clip_{idx:02d}.mp4" # Створюємо вертикальний відеокліп (9:16) base_filters = f"scale='max({self.vertical_resolution[0]},iw*{self.vertical_resolution[1]}/ih)':'max({self.vertical_resolution[1]},ih*{self.vertical_resolution[0]}/iw)',crop={self.vertical_resolution[0]}:{self.vertical_resolution[1]},fps=30" filter_string = self.apply_safe_fade_filters(use_dur, base_filters) subprocess.run([ "ffmpeg", "-y", "-i", str(bg), "-ss", "0", "-t", str(use_dur), "-vf", filter_string, "-an", "-c:v", "h264_nvenc", "-preset", "p4", "-b:v", "5M", str(temp_out) ], check=True) used_clips.append(temp_out) duration_collected += use_dur if duration_collected >= target_duration: break # Якщо все ще не достатньо відео, додаємо ще if duration_collected < target_duration: bg_videos_extra = list(self.source_folder.glob("*.mp4")) random.shuffle(bg_videos_extra) for idx, bg in enumerate(bg_videos_extra): if duration_collected >= target_duration: break clip = VideoFileClip(str(bg)) use_dur = min(clip.duration, target_duration - duration_collected) clip.close() if use_dur <= 0: break temp_out = self.temp_folder / f"full_v_clip_extra_{idx:02d}.mp4" # Створюємо вертикальний відеокліп base_filters = f"scale='max({self.vertical_resolution[0]},iw*{self.vertical_resolution[1]}/ih)':'max({self.vertical_resolution[1]},ih*{self.vertical_resolution[0]}/iw)',crop={self.vertical_resolution[0]}:{self.vertical_resolution[1]},fps=30" filter_string = self.apply_safe_fade_filters(use_dur, base_filters) subprocess.run([ "ffmpeg", "-y", "-i", str(bg), "-ss", "0", "-t", str(use_dur), "-vf", filter_string, "-an", "-c:v", "h264_nvenc", "-preset", "p4", "-b:v", "5M", str(temp_out) ], check=True) used_clips.append(temp_out) duration_collected += use_dur return used_clips def create_full_vertical_video(self, story_id, preview_txt, audio_preview, audio_story, horizontal_preview_path): """ Створює повне вертикальне відео (9:16) СПРОЩЕНА ЛОГІКА: Створює тільки вертикальне відео без градієнтного фону Args: story_id: ID історії preview_txt: Текст preview audio_preview: AudioSegment з preview аудіо audio_story: AudioSegment з story аудіо horizontal_preview_path: Шлях до горизонтального preview зображення Returns: Path: Шлях до створеного вертикального відео (9:16) """ print(f"???? Створення вертикального відео (9:16)...") preview_duration = audio_preview.duration_seconds final_audio = audio_preview + AudioSegment.silent(duration=500) + audio_story total_duration = final_audio.duration_seconds # Етап 1: Створюємо фонові сегменти для preview preview_combined, bg_iter = self.create_preview_video_segments_vertical(story_id, preview_duration) # Етап 2: Накладаємо preview зображення preview_video = self.create_preview_with_overlay_vertical( story_id, preview_combined, horizontal_preview_path, preview_duration ) print(f"???? Створення основного вертикального відео...") # Етап 3: Створюємо story сегменти story_clips = self.create_story_video_segments_vertical( story_id, bg_iter, total_duration, preview_duration ) # Етап 4: Об'єднуємо всі вертикальні сегменти all_clips = [preview_video] + story_clips concat_list = self.temp_folder / f"{story_id}_full_v_concat.txt" with open(concat_list, "w", encoding="utf-8") as f: for clip in all_clips: f.write(f"file '{clip.as_posix()}'\n") # Фінальне вертикальне відео final_vertical_video = self.temp_folder / f"{story_id}_final_vertical.mp4" subprocess.run([ "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", str(concat_list), "-c:v", "h264_nvenc", "-b:v", "8M", "-preset", "p4", "-pix_fmt", "yuv420p", str(final_vertical_video) ], check=True) return final_vertical_video def finalize_full_vertical_video(self, combined_video, audio_path, output_path): """ Фінальна збірка вертикального відео з аудіо Args: combined_video: Шлях до відео без аудіо audio_path: Шлях до аудіо файлу output_path: Шлях для збереження фінального відео """ print(f"???? Фінальна збірка вертикального відео з аудіо...") subprocess.run([ "ffmpeg", "-y", "-i", str(combined_video), "-i", str(audio_path), "-c:v", "copy", "-c:a", "aac", "-b:a", "128k", "-shortest", "-movflags", "+faststart", str(output_path) ], check=True)