import subprocess
import random
import re
from pathlib import Path
from moviepy.editor import VideoFileClip
from pydub import AudioSegment


class EnhancedVerticalVideoGenerator:
    def __init__(self, source_folder, temp_folder, preview_generator, vertical_resolution=(1920, 1080), target_duration=59):
        """
        Ініціалізація генератора вертикальних відео з покращеним форматом
        Args:
            source_folder: Папка з фоновими відео
            temp_folder: Папка для тимчасових файлів
            preview_generator: Об'єкт PreviewGenerator для створення превью
            vertical_resolution: Розмір вертикального відео (ширина, висота) - 1920x1080
            target_duration: Цільова тривалість відео у секундах
        """
        self.source_folder = Path(source_folder)
        self.temp_folder = Path(temp_folder)
        self.preview_generator = preview_generator
        self.vertical_resolution = vertical_resolution
        self.target_duration = target_duration

    def create_gradient_background(self, story_id, width, height, duration):
        """
        Створює приємний градієнт фон для заповнення чорних областей
        """
        gradient_path = self.temp_folder / f"{story_id}_gradient.mp4"
        
        # Створюємо градієнт від темно-синього до фіолетового
        gradient_colors = [
            "#1a1a2e",  # темно-синій
            "#16213e",  # синій
            "#0f4c75",  # середній синій
            "#3282b8",  # світліший синій
            "#533483"   # фіолетовий
        ]
        
        # Створюємо градієнт фон
        gradient_filter = f"color=c=#1a1a2e:size={width}x{height}:duration={duration}:rate=30"
        gradient_overlay = f"geq=r='128+64*sin((X+Y)*0.01+T*0.5)':g='64+32*sin((X-Y)*0.01+T*0.3)':b='128+96*sin((X*Y)*0.001+T*0.2)'"
        
        subprocess.run([
            "ffmpeg", "-y",
            "-f", "lavfi",
            "-i", gradient_filter,
            "-vf", gradient_overlay,
            "-c:v", "h264_nvenc",
            "-preset", "p4",
            "-b:v", "3M",
            str(gradient_path)
        ], check=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 truncate_story_for_duration(self, story_text, target_duration, preview_duration):
        """Обрізає історію до цільової тривалості, завершуючи на кінці речення"""
        sentences = re.split(r'[.!?]+', story_text)
        sentences = [s.strip() for s in sentences if s.strip()]
        
        truncated_text = ""
        current_duration = preview_duration + 0.5  # preview + пауза
        
        for sentence in sentences:
            words_count = len(sentence.split())
            sentence_duration = words_count / 2.5  # приблизно 2.5 слова в секунду
            
            if current_duration + sentence_duration > target_duration:
                break
                
            truncated_text += sentence + ". "
            current_duration += sentence_duration
            
        return truncated_text.strip()

    def create_vertical_video_segment(self, bg_video_path, duration, story_id, segment_idx):
        """
        Створює вертикальний відео сегмент з градієнтом замість чорних областей
        """
        temp_segment = self.temp_folder / f"{story_id}_vertical_segment_{segment_idx:02d}.mp4"
        
        # Створюємо градієнт фон
        gradient_bg = self.create_gradient_background(story_id + f"_seg{segment_idx}", 
                                                    self.vertical_resolution[0], 
                                                    self.vertical_resolution[1], 
                                                    duration)
        
        # Масштабуємо оригінальне відео щоб воно поміщалось в центр
        # Оригінальне відео буде в центрі, а градієнт буде по краях
        video_scale = f"scale='min({self.vertical_resolution[0]},iw)':'min({self.vertical_resolution[1]},ih)':force_original_aspect_ratio=decrease"
        
        # Комбінуємо градієнт і відео
        filter_complex = f"""
        [0:v]{video_scale}[scaled];
        [1:v][scaled]overlay=(W-w)/2:(H-h)/2:format=auto[final];
        [final]fps=30
        """
        
        subprocess.run([
            "ffmpeg", "-y",
            "-i", str(bg_video_path),
            "-i", str(gradient_bg),
            "-filter_complex", filter_complex.strip(),
            "-t", str(duration),
            "-c:v", "h264_nvenc",
            "-preset", "p4",
            "-b:v", "8M",
            str(temp_segment)
        ], check=True)
        
        return temp_segment

    def create_preview_segments_vertical(self, story_id, preview_duration):
        """Створює вертикальні фонові сегменти для 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 = self.create_vertical_video_segment(
                bg_video, use_duration, story_id, len(preview_clips)
            )
            
            preview_clips.append(preview_clip)
            preview_duration_collected += use_duration
            
        return preview_clips, bg_iter

    def create_preview_with_overlay_vertical(self, story_id, preview_clips, horizontal_preview_path, preview_duration):
        """Створює preview відео з накладеним вертикальним preview зображенням"""
        # Об'єднуємо всі preview кліпи
        if len(preview_clips) > 1:
            preview_concat_list = self.temp_folder / f"{story_id}_vertical_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}_vertical_preview_combined.mp4"
            subprocess.run([
                "ffmpeg", "-y",
                "-f", "concat", "-safe", "0",
                "-i", str(preview_concat_list),
                "-c:v", "h264_nvenc", "-b:v", "8M", "-preset", "p4",
                str(preview_combined)
            ], check=True)
        else:
            preview_combined = preview_clips[0]
            
        # Створюємо вертикальне preview зображення
        preview_img_path = self.temp_folder / f"{story_id}_preview_vertical.png"
        self.preview_generator.draw_vertical_preview(horizontal_preview_path, preview_img_path)
        
        # Накладаємо preview зображення на відео
        preview_video = self.temp_folder / f"{story_id}_vertical_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", "8M",
            str(preview_video)
        ], check=True)
        
        return preview_video

    def create_story_segments_vertical(self, story_id, bg_iter, story_duration):
        """Створює вертикальні відео сегменти для основної частини історії"""
        used_clips = []
        remaining_story_duration = story_duration
        segment_idx = 0
        
        for bg in bg_iter:
            if remaining_story_duration <= 0:
                break
                
            clip = VideoFileClip(str(bg))
            use_dur = min(clip.duration, remaining_story_duration)
            clip.close()
            
            if use_dur <= 0:
                break
                
            story_segment = self.create_vertical_video_segment(
                bg, use_dur, story_id, segment_idx + 100  # +100 щоб не перетинатись з preview
            )
            
            used_clips.append(story_segment)
            remaining_story_duration -= use_dur
            segment_idx += 1
            
        # Якщо все ще не достатньо відео для story, додаємо ще
        if remaining_story_duration > 0:
            bg_videos_extra = list(self.source_folder.glob("*.mp4"))
            random.shuffle(bg_videos_extra)
            
            for bg in bg_videos_extra:
                if remaining_story_duration <= 0:
                    break
                    
                clip = VideoFileClip(str(bg))
                use_dur = min(clip.duration, remaining_story_duration)
                clip.close()
                
                if use_dur <= 0:
                    break
                    
                story_segment = self.create_vertical_video_segment(
                    bg, use_dur, story_id, segment_idx + 100
                )
                
                used_clips.append(story_segment)
                remaining_story_duration -= use_dur
                segment_idx += 1
                
        return used_clips

    def combine_vertical_segments(self, story_id, preview_video, story_clips):
        """Об'єднує всі сегменти вертикального відео (БЕЗ кінцівки про підписку)"""
        used_clips = [preview_video] + story_clips
        
        concat_list = self.temp_folder / f"{story_id}_vertical_concat.txt"
        with open(concat_list, "w", encoding="utf-8") as f:
            for clip in used_clips:
                f.write(f"file '{clip.as_posix()}'\n")
                
        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", "8M",
            "-preset", "p4",
            str(combined_path)
        ], check=True)
        
        return combined_path

    def create_vertical_video(self, story_txt, preview_txt, story_id, horizontal_preview_path, coqui_tts):
        """
        Створює вертикальне відео (1920x1080) БЕЗ заклику підписки
        Args:
            story_txt: Текст історії
            preview_txt: Текст preview
            story_id: ID історії
            horizontal_preview_path: Шлях до горизонтального preview
            coqui_tts: Функція TTS для генерації аудіо
        Returns:
            tuple: (Path до відео, Path до аудіо)
        """
        print(f"???? Створення вертикального відео (1920x1080)...")
        
        # Генеруємо аудіо
        audio_preview = coqui_tts(preview_txt)
        
        # Обрізаємо історію для цільової тривалості
        truncated_story = self.truncate_story_for_duration(
            story_txt, self.target_duration, audio_preview.duration_seconds
        )
        audio_story = coqui_tts(truncated_story)
        
        # Збираємо фінальне аудіо БЕЗ кінцівки про підписку
        final_audio = audio_preview + AudioSegment.silent(duration=500) + audio_story
        
        audio_path = self.temp_folder / f"{story_id}_vertical_voice.wav"
        final_audio.export(audio_path, format="wav")
        
        # Створюємо preview сегменти
        preview_duration = audio_preview.duration_seconds
        story_duration = audio_story.duration_seconds
        
        preview_clips, bg_iter = self.create_preview_segments_vertical(story_id, preview_duration)
        preview_video = self.create_preview_with_overlay_vertical(
            story_id, preview_clips, horizontal_preview_path, preview_duration
        )
        
        # Створюємо story сегменти
        story_clips = self.create_story_segments_vertical(story_id, bg_iter, story_duration)
        
        # Об'єднуємо всі сегменти (БЕЗ кінцівки)
        combined_video = self.combine_vertical_segments(story_id, preview_video, story_clips)
        
        return combined_video, audio_path

    def finalize_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)


class ShortsVideoGenerator:
    """Генератор для коротких відео (Shorts) з покращеннями"""
    
    def __init__(self, source_folder, temp_folder, preview_generator, shorts_resolution=(1080, 1920), target_duration=59):
        self.source_folder = Path(source_folder)
        self.temp_folder = Path(temp_folder)
        self.preview_generator = preview_generator
        self.shorts_resolution = shorts_resolution
        self.target_duration = target_duration

    def create_gradient_background_shorts(self, story_id, width, height, duration):
        """Створює приємний градієнт фон для Shorts"""
        gradient_path = self.temp_folder / f"{story_id}_shorts_gradient.mp4"
        
        # Створюємо вертикальний градієнт для Shorts
        gradient_filter = f"color=c=#0f0f23:size={width}x{height}:duration={duration}:rate=30"
        gradient_overlay = f"geq=r='96+48*sin((Y)*0.003+T*0.4)':g='48+24*sin((Y)*0.002+T*0.2)':b='96+72*sin((Y)*0.004+T*0.6)'"
        
        subprocess.run([
            "ffmpeg", "-y",
            "-f", "lavfi",
            "-i", gradient_filter,
            "-vf", gradient_overlay,
            "-c:v", "h264_nvenc",
            "-preset", "p4",
            "-b:v", "5M",
            str(gradient_path)
        ], check=True)
        
        return gradient_path

    def create_shorts_video_segment(self, bg_video_path, duration, story_id, segment_idx):
        """Створює сегмент відео для Shorts з градієнтом"""
        temp_segment = self.temp_folder / f"{story_id}_shorts_segment_{segment_idx:02d}.mp4"
        
        # Створюємо градієнт фон
        gradient_bg = self.create_gradient_background_shorts(
            story_id + f"_shorts_seg{segment_idx}",
            self.shorts_resolution[0],
            self.shorts_resolution[1],
            duration
        )
        
        # Масштабуємо відео для Shorts формату
        video_scale = f"scale='min({self.shorts_resolution[0]},iw)':'min({self.shorts_resolution[1]},ih)':force_original_aspect_ratio=decrease"
        
        filter_complex = f"""
        [0:v]{video_scale}[scaled];
        [1:v][scaled]overlay=(W-w)/2:(H-h)/2:format=auto[final];
        [final]fps=30
        """
        
        subprocess.run([
            "ffmpeg", "-y",
            "-i", str(bg_video_path),
            "-i", str(gradient_bg),
            "-filter_complex", filter_complex.strip(),
            "-t", str(duration),
            "-c:v", "h264_nvenc",
            "-preset", "p4",
            "-b:v", "8M",
            str(temp_segment)
        ], check=True)
        
        return temp_segment

    def truncate_story_for_duration(self, story_text, target_duration, preview_duration):
        """Обрізає історію до цільової тривалості для Shorts"""
        sentences = re.split(r'[.!?]+', story_text)
        sentences = [s.strip() for s in sentences if s.strip()]
        
        truncated_text = ""
        current_duration = preview_duration + 0.5
        
        for sentence in sentences:
            words_count = len(sentence.split())
            sentence_duration = words_count / 2.5
            
            if current_duration + sentence_duration > target_duration:
                break
                
            truncated_text += sentence + ". "
            current_duration += sentence_duration
            
        return truncated_text.strip()

    def create_shorts_video(self, story_txt, preview_txt, story_id, horizontal_preview_path, coqui_tts):
        """
        Створює Shorts відео БЕЗ заклику підписки в кінці
        """
        print(f"???? Створення Shorts відео (БЕЗ заклику підписки)...")
        
        # Генеруємо аудіо
        audio_preview = coqui_tts(preview_txt)
        
        # Обрізаємо історію для Shorts
        truncated_story = self.truncate_story_for_duration(
            story_txt, self.target_duration, audio_preview.duration_seconds
        )
        audio_story = coqui_tts(truncated_story)
        
        # БЕЗ кінцівки про підписку
        final_audio = audio_preview + AudioSegment.silent(duration=500) + audio_story
        
        audio_path = self.temp_folder / f"{story_id}_shorts_voice.wav"
        final_audio.export(audio_path, format="wav")
        
        # Створюємо відео сегменти
        bg_videos = list(self.source_folder.glob("*.mp4"))
        random.shuffle(bg_videos)
        
        clips = []
        remaining_duration = final_audio.duration_seconds
        segment_idx = 0
        
        for bg_video in bg_videos:
            if remaining_duration <= 0:
                break
                
            clip = VideoFileClip(str(bg_video))
            use_duration = min(clip.duration, remaining_duration)
            clip.close()
            
            if use_duration <= 0:
                break
                
            shorts_segment = self.create_shorts_video_segment(
                bg_video, use_duration, story_id, segment_idx
            )
            
            clips.append(shorts_segment)
            remaining_duration -= use_duration
            segment_idx += 1
        
        # Об'єднуємо сегменти
        concat_list = self.temp_folder / f"{story_id}_shorts_concat.txt"
        with open(concat_list, "w", encoding="utf-8") as f:
            for clip in clips:
                f.write(f"file '{clip.as_posix()}'\n")
                
        combined_video = self.temp_folder / f"{story_id}_shorts_combined.mp4"
        subprocess.run([
            "ffmpeg", "-y",
            "-f", "concat", "-safe", "0",
            "-i", str(concat_list),
            "-c:v", "h264_nvenc",
            "-b:v", "8M",
            "-preset", "p4",
            str(combined_video)
        ], check=True)
        
        return combined_video, audio_path

    def finalize_shorts_video(self, combined_video, audio_path, output_path):
        """Фінальна збірка Shorts відео"""
        print(f"???? Фінальна збірка Shorts відео...")
        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