# 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)
Made on
Tilda