# 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: Папка з градієнтами (JPG зображення)
        """
        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.final_resolution = (1920, 1080)  # Фінальний розмір з градієнтним фоном - 16:9
        self.fade_duration = fade_duration
        self.gradients_folder = Path(gradients_folder) if gradients_folder else None
        
        # Перевіряємо наявність папки з градієнтами
        if self.gradients_folder and not self.gradients_folder.exists():
            print(f"⚠️ Папка з градієнтами {self.gradients_folder} не існує!")
            self.gradients_folder = None

    def get_random_gradient(self):
        """
        Вибирає випадковий градієнт з папки
        
        Returns:
            Path: Шлях до випадкового градієнту або None якщо папка порожня
        """
        if not self.gradients_folder:
            return None
            
        # Шукаємо всі зображення в папці градієнтів
        gradient_files = (
            list(self.gradients_folder.glob("*.jpg")) + 
            list(self.gradients_folder.glob("*.jpeg")) +
            list(self.gradients_folder.glob("*.png")) +
            list(self.gradients_folder.glob("*.JPG")) +
            list(self.gradients_folder.glob("*.JPEG")) +
            list(self.gradients_folder.glob("*.PNG"))
        )
        
        if not gradient_files:
            print(f"⚠️ У папці {self.gradients_folder} не знайдено градієнтів (JPG/PNG)!")
            return None
            
        # Вибираємо випадковий градієнт
        selected_gradient = random.choice(gradient_files)
        print(f"???? Вибрано градієнт: {selected_gradient.name}")
        return selected_gradient

    def create_gradient_background(self, story_id, duration):
        """
        Створює градієнтний фон для розміщення вертикального відео
        ОНОВЛЕНО: Використовує випадковий градієнт з папки замість генерації
        
        Args:
            story_id: ID історії
            duration: Тривалість відео в секундах
            
        Returns:
            Path: Шлях до створеного градієнтного відео
        """
        gradient_path = self.temp_folder / f"{story_id}_gradient_bg.mp4"
        
        # Спробуємо взяти випадковий градієнт з папки
        gradient_image = self.get_random_gradient()
        
        if gradient_image:
            # Використовуємо градієнт з папки
            print(f"???? Використовуємо градієнт з файлу: {gradient_image.name}")
            
            try:
                # Масштабуємо градієнт до потрібного розміру
                temp_gradient_path = self.temp_folder / f"{story_id}_scaled_gradient.png"  # Використовуємо PNG замість JPEG
                
                # Завантажуємо та масштабуємо градієнт
                with Image.open(gradient_image) as img:
                    # Конвертуємо в RGB якщо потрібно, але зберігаємо максимальну якість
                    if img.mode in ('RGBA', 'LA', 'P'):
                        # Створюємо чорний фон замість білого для градієнтів
                        rgb_img = Image.new('RGB', img.size, (0, 0, 0))
                        if img.mode == 'P':
                            img = img.convert('RGBA')
                        rgb_img.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None)
                        img = rgb_img
                    elif img.mode != 'RGB':
                        img = img.convert('RGB')
                    
                    # Масштабуємо до фінального розміру з високою якістю
                    scaled_img = img.resize(self.final_resolution, Image.Resampling.LANCZOS)
                    # Зберігаємо як PNG без втрат якості
                    scaled_img.save(temp_gradient_path, "PNG")
                
                # Створюємо відео з високим бітрейтом та без додаткового стиснення
                subprocess.run([
                    "ffmpeg", "-y",
                    "-loop", "1",
                    "-i", str(temp_gradient_path),
                    "-t", str(duration),
                    "-c:v", "h264_nvenc",
                    "-preset", "medium",    # ЗБАЛАНСОВАНИЙ: було "slow"
                    "-b:v", "10M",          # ЗБАЛАНСОВАНИЙ: було "15M"
                    "-crf", "18",           # ЗБАЛАНСОВАНИЙ: було "16"
                    "-pix_fmt", "yuv420p",
                    "-r", "30",
                    str(gradient_path)
                ], check=True)
                
                # Видаляємо тимчасовий файл
                temp_gradient_path.unlink(missing_ok=True)
                
            except Exception as e:
                print(f"❌ Помилка при обробці градієнту {gradient_image.name}: {e}")
                print("???? Використовуємо програмний градієнт замість файлового")
                gradient_image = None  # Перемикаємося на fallback
            
        if not gradient_image:
            # Fallback: створюємо програмний градієнт як було раніше
            print(f"???? Створюємо програмний градієнт (fallback)")
            gradient_img_path = self.temp_folder / f"{story_id}_gradient.png"
            
            img = Image.new('RGB', self.final_resolution, color='black')
            draw = ImageDraw.Draw(img)
            
            # Створюємо вертикальний градієнт від темно-синього до чорного
            for y in range(self.final_resolution[1]):
                # Градієнт від темно-синього (30, 30, 80) до чорного (0, 0, 0)
                progress = y / self.final_resolution[1]
                r = int(30 * (1 - progress))
                g = int(30 * (1 - progress))
                b = int(80 * (1 - progress))
                color = (r, g, b)
                draw.line([(0, y), (self.final_resolution[0], y)], fill=color)
            
            img.save(gradient_img_path)
            
            # Створюємо відео з градієнтного зображення з високою якістю
            subprocess.run([
                "ffmpeg", "-y",
                "-loop", "1",
                "-i", str(gradient_img_path),
                "-t", str(duration),
                "-c:v", "h264_nvenc",
                "-preset", "medium",    # ЗБАЛАНСОВАНИЙ: було "slow"
                "-b:v", "10M",          # ЗБАЛАНСОВАНИЙ: було "15M"
                "-crf", "18",           # ЗБАЛАНСОВАНИЙ: було "16"
                "-pix_fmt", "yuv420p",
                "-r", "30",
                str(gradient_path)
            ], check=True)
            
            # Видаляємо тимчасовий файл
            gradient_img_path.unlink(missing_ok=True)
        
        return gradient_path

    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 combine_video_segments_vertical(self, story_id, preview_video, story_clips):
        """
        Об'єднує всі вертикальні відео сегменти в одне вертикальне відео
        
        Args:
            story_id: ID історії
            preview_video: Шлях до preview відео
            story_clips: Список шляхів до story кліпів
            
        Returns:
            Path: Шлях до об'єднаного ВЕРТИКАЛЬНОГО відео (9:16)
        """
        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")
        
        # Об'єднуємо в ВЕРТИКАЛЬНЕ відео
        vertical_combined_path = self.temp_folder / f"{story_id}_vertical_combined.mp4"
        subprocess.run([
            "ffmpeg", "-y",
            "-f", "concat",
            "-safe", "0",
            "-i", str(concat_list),
            "-c:v", "h264_nvenc",
            "-b:v", "5M",
            "-preset", "p4",
            str(vertical_combined_path)
        ], check=True)
        
        return vertical_combined_path

    def place_vertical_on_gradient(self, story_id, vertical_video, gradient_bg):
        """
        Розміщує вертикальне відео (9:16) на градієнтному фоні (16:9) БЕЗ ОБРІЗАННЯ
        Вертикальне відео масштабується щоб поміститися по висоті, але зберігає пропорції
        
        Args:
            story_id: ID історії
            vertical_video: Шлях до вертикального відео
            gradient_bg: Шлях до градієнтного фону
            
        Returns:
            Path: Шлях до фінального горизонтального відео з вертикальним відео по центру
        """
        final_video_path = self.temp_folder / f"{story_id}_final_with_gradient.mp4"
        
        # ВИПРАВЛЕНО: Масштабуємо вертикальне відео по висоті, зберігаючи пропорції
        # Фон: 1920x1080, вертикальне відео: 1080x1920  
        # Масштабуємо вертикальне відео так, щоб його висота = висоті фону (1080)
        # При цьому ширина стане: 1080 * (1080/1920) = 607 пікселів
        subprocess.run([
            "ffmpeg", "-y",
            "-i", str(gradient_bg),
            "-i", str(vertical_video),
            "-filter_complex", "[1:v]scale=-1:1080[scaled_vertical];[0:v][scaled_vertical]overlay=(W-w)/2:(H-h)/2",
            "-c:v", "h264_nvenc",
            "-preset", "medium",    # ЗБАЛАНСОВАНИЙ: було "slow"
            "-b:v", "12M",          # ЗБАЛАНСОВАНИЙ: було "20M"
            "-crf", "18",           # ЗБАЛАНСОВАНИЙ: було "15"
            "-pix_fmt", "yuv420p",
            "-shortest",
            str(final_video_path)
        ], check=True)
        
        return final_video_path

    def create_full_vertical_video(self, story_id, preview_txt, audio_preview, audio_story, horizontal_preview_path):
        """
        Створює повне вертикальне відео з градієнтним фоном
        НОВА ЛОГІКА:
        1. Створює вертикальне відео (9:16)
        2. Створює градієнтний фон (16:9) - ТЕПЕР З ПАПКИ!
        3. Розміщує вертикальне відео по центру градієнтного фону
        
        Args:
            story_id: ID історії
            preview_txt: Текст preview
            audio_preview: AudioSegment з preview аудіо
            audio_story: AudioSegment з story аудіо
            horizontal_preview_path: Шлях до горизонтального preview зображення
            
        Returns:
            Path: Шлях до створеного фінального відео (16:9 з вертикальним відео по центру)
        """
        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: Створюємо градієнтний фон (ТЕПЕР З ПАПКИ!)
        print(f"???? Створення градієнтного фону...")
        gradient_bg = self.create_gradient_background(story_id, total_duration)
        
        # Етап 2: Створюємо фонові сегменти для preview (ВЕРТИКАЛЬНІ!)
        preview_combined, bg_iter = self.create_preview_video_segments_vertical(story_id, preview_duration)
        
        # Етап 3: Накладаємо preview зображення
        preview_video = self.create_preview_with_overlay_vertical(
            story_id, preview_combined, horizontal_preview_path, preview_duration
        )
        
        print(f"???? Створення основного вертикального відео...")
        
        # Етап 4: Створюємо story сегменти (ВЕРТИКАЛЬНІ!)
        story_clips = self.create_story_video_segments_vertical(
            story_id, bg_iter, total_duration, preview_duration
        )
        
        # Етап 5: Об'єднуємо всі вертикальні сегменти
        vertical_combined = self.combine_video_segments_vertical(story_id, preview_video, story_clips)
        
        print(f"???? Розміщення вертикального відео на градієнтному фоні...")
        
        # Етап 6: Розміщуємо вертикальне відео на градієнтному фоні
        final_video = self.place_vertical_on_gradient(story_id, vertical_combined, gradient_bg)
        
        return final_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",
            "-shortest",
            "-movflags", "+faststart",
            str(output_path)
        ], check=True)
Made on
Tilda