# test_ffmpeg.py
import os, re, shutil, subprocess, hashlib, random, time
from pathlib import Path
from datetime import datetime
from moviepy.editor import VideoFileClip
from TTS.api import TTS
from pydub import AudioSegment
from PIL import Image, ImageDraw, ImageFont
from faster_whisper import WhisperModel
import torch
import asyncio
import mouse
import json

# Імпорти модулів
from telegram_bot import send_video_preview
from preview_generator import PreviewGenerator
from horizontal_video import HorizontalVideoGenerator
from vertical_video import VerticalVideoGenerator
from tiktok_processor import TikTokVideoGenerator, process_tiktok_videos, create_tts_with_censoring, get_tiktok_duration_settings

def safe_filename(name):
    """Створює безпечну назву файлу"""
    name = re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name)
    name = re.sub(r'\s+','_', name).strip("_")
    name = name.rstrip(". ")
    return name[:50]

def get_speed_settings():
    """Дозволяє користувачу налаштувати прискорення відео"""
    print("\n" + "="*60)
    print("⚡ НАЛАШТУВАННЯ ПРИСКОРЕННЯ ВІДЕО")
    print("="*60)
    print("Виберіть режим прискорення:")
    print("1. ⚡ Без прискорення (оригінальна швидкість)")
    print("2. ???? Фіксоване прискорення для всіх відео")
    print("3. ???? Прискорення до цільової тривалості")
    print("4. ⚙️ Інтерактивне налаштування для кожного відео")
    print("="*60)
    
    while True:
        choice = input("Ваш вибір (1-4): ").strip()
        if choice in ["1", "2", "3", "4"]:
            break
        print("❌ Невірний вибір. Введіть 1, 2, 3 або 4")
    
    speed_config = {
        'mode': choice,
        'youtube_horizontal': {'enabled': False, 'speed': 1.0, 'target_duration': None},
        'youtube_vertical': {'enabled': False, 'speed': 1.0, 'target_duration': None},
        'tiktok': {'enabled': False, 'speed': 1.0, 'target_duration': None}
    }
    
    if choice == "1":
        # Без прискорення
        print("✅ Вибрано режим без прискорення")
        return speed_config
        
    elif choice == "2":
        # Фіксоване прискорення
        print("\n???? Налаштування фіксованого прискорення:")
        while True:
            try:
                speed = float(input("Введіть коефіцієнт прискорення (1.0-4.0, наприклад 1.5): "))
                if 1.0 <= speed <= 4.0:
                    break
                print("❌ Коефіцієнт має бути від 1.0 до 4.0")
            except ValueError:
                print("❌ Введіть дійсне число!")
        
        speed_config['youtube_horizontal'] = {'enabled': True, 'speed': speed, 'target_duration': None}
        speed_config['youtube_vertical'] = {'enabled': True, 'speed': speed, 'target_duration': None}
        speed_config['tiktok'] = {'enabled': True, 'speed': speed, 'target_duration': None}
        print(f"✅ Всі відео будуть прискорені в {speed}x")
        
    elif choice == "3":
        # Прискорення до цільової тривалості
        print("\n???? Налаштування прискорення до цільової тривалості:")
        
        # YouTube горизонтальне
        print("\n????️ YouTube горизонтальне відео:")
        enable = input("Прискорювати YouTube горизонтальні відео? (y/n): ").lower() == 'y'
        if enable:
            while True:
                try:
                    target = float(input("Цільова тривалість (секунди): "))
                    if target > 10:
                        break
                    print("❌ Тривалість має бути більше 10 секунд")
                except ValueError:
                    print("❌ Введіть дійсне число!")
            speed_config['youtube_horizontal'] = {'enabled': True, 'speed': None, 'target_duration': target}
        
        # YouTube вертикальне
        print("\n???? YouTube вертикальне відео:")
        enable = input("Прискорювати YouTube вертикальні відео? (y/n): ").lower() == 'y'
        if enable:
            while True:
                try:
                    target = float(input("Цільова тривалість (секунди): "))
                    if target > 10:
                        break
                    print("❌ Тривалість має бути більше 10 секунд")
                except ValueError:
                    print("❌ Введіть дійсне число!")
            speed_config['youtube_vertical'] = {'enabled': True, 'speed': None, 'target_duration': target}
        
        # TikTok
        print("\n???? TikTok відео:")
        enable = input("Прискорювати TikTok відео? (y/n): ").lower() == 'y'
        if enable:
            while True:
                try:
                    target = float(input("Цільова тривалість кожної частини (секунди): "))
                    if target > 15:
                        break
                    print("❌ Тривалість має бути більше 15 секунд")
                except ValueError:
                    print("❌ Введіть дійсне число!")
            speed_config['tiktok'] = {'enabled': True, 'speed': None, 'target_duration': target}
            
    elif choice == "4":
        # Інтерактивне налаштування
        print("✅ Вибрано інтерактивний режим")
        print("Для кожного відео буде запропоновано налаштування прискорення")
        speed_config['mode'] = 'interactive'
    
    return speed_config

def get_interactive_speed_settings(video_type, original_duration):
    """Запитує у користувача налаштування прискорення для конкретного відео"""
    type_names = {
        'youtube_horizontal': 'YouTube горизонтальне',
        'youtube_vertical': 'YouTube вертикальне',
        'tiktok': 'TikTok частина'
    }
    
    print(f"\n⚙️ Налаштування для {type_names.get(video_type, video_type)}")
    print(f"⏱️ Поточна тривалість: {original_duration:.1f} секунд")
    
    choice = input("Прискорювати це відео? (y/n): ").lower()
    if choice != 'y':
        return {'enabled': False, 'speed': 1.0, 'target_duration': None}
    
    print("Виберіть спосіб:")
    print("1. Фіксоване прискорення (наприклад, 1.5x)")
    print("2. До цільової тривалості")
    
    while True:
        method = input("Ваш вибір (1-2): ").strip()
        if method in ["1", "2"]:
            break
        print("❌ Введіть 1 або 2")
    
    if method == "1":
        while True:
            try:
                speed = float(input("Коефіцієнт прискорення (1.0-4.0): "))
                if 1.0 <= speed <= 4.0:
                    break
                print("❌ Коефіцієнт має бути від 1.0 до 4.0")
            except ValueError:
                print("❌ Введіть дійсне число!")
        
        final_duration = original_duration / speed
        print(f"✅ Швидкість {speed}x, фінальна тривалість: {final_duration:.1f}с")
        return {'enabled': True, 'speed': speed, 'target_duration': None}
    else:
        max_duration = 300 if video_type == 'tiktok' else 600
        while True:
            try:
                target = float(input(f"Цільова тривалість (10-{max_duration}с): "))
                if 10 <= target <= max_duration:
                    break
                print(f"❌ Тривалість має бути від 10 до {max_duration} секунд")
            except ValueError:
                print("❌ Введіть дійсне число!")
        
        required_speed = original_duration / target
        if required_speed > 4.0:
            print(f"⚠️ Потрібне прискорення {required_speed:.1f}x перевищує максимум 4.0x")
            required_speed = 4.0
            final_duration = original_duration / required_speed
            print(f"Буде використано максимальне прискорення 4.0x, тривалість: {final_duration:.1f}с")
        else:
            print(f"✅ Прискорення {required_speed:.1f}x до {target}с")
        
        return {'enabled': True, 'speed': required_speed, 'target_duration': target}

def apply_video_speedup(input_video_path, output_video_path, speed_factor, temp_folder):
    """Прискорює відео зі збереженням якості"""
    if speed_factor <= 1.0:
        shutil.copy2(input_video_path, output_video_path)
        return Path(output_video_path)
    
    print(f"⚡ Прискорення відео в {speed_factor:.2f}x...")
    temp_video = temp_folder / f"speedup_temp_{random.randint(1000, 9999)}.mp4"
    
    try:
        cmd = [
            "ffmpeg", "-y", "-i", str(input_video_path),
            "-filter_complex", f"[0:v]setpts={1/speed_factor}*PTS[v];[0:a]atempo={speed_factor}[a]",
            "-map", "[v]", "-map", "[a]", "-c:v", "h264_nvenc", "-preset", "p4", "-b:v", "8M",
            "-c:a", "aac", "-b:a", "128k", "-movflags", "+faststart", str(temp_video)
        ]
        
        result = subprocess.run(cmd, check=True, capture_output=True, text=True)
        shutil.move(str(temp_video), str(output_video_path))
        print(f"✅ Відео прискорено успішно")
        return Path(output_video_path)
        
    except subprocess.CalledProcessError as e:
        print(f"❌ Помилка при прискоренні відео: {e}")
        print(f"FFmpeg stderr: {e.stderr}")
        shutil.copy2(input_video_path, output_video_path)
        return Path(output_video_path)
    finally:
        if temp_video.exists():
            temp_video.unlink()

def select_background_folder(backgrounds_dir):
    """Дозволяє користувачу вибрати папку з фоновими відео"""
    print("\n" + "="*60)
    print("???? ВИБІР ФОНОВИХ ВІДЕО")
    print("="*60)
    
    bg_folders = [f for f in backgrounds_dir.iterdir() if f.is_dir()]
    if not bg_folders:
        print("❌ Папки з фоновими відео не знайдено!")
        print(f"Створіть папки в: {backgrounds_dir}")
        print("Наприклад: minecraft, cakes, future, тощо")
        return None
    
    for i, folder in enumerate(bg_folders, 1):
        video_files = list(folder.glob("*.mp4")) + list(folder.glob("*.mov")) + list(folder.glob("*.avi"))
        print(f"{i}. ???? {folder.name} ({len(video_files)} відео)")
    print("="*60)
    
    while True:
        try:
            choice = input(f"Виберіть папку (1-{len(bg_folders)}): ").strip()
            choice_idx = int(choice) - 1
            if 0 <= choice_idx < len(bg_folders):
                selected_folder = bg_folders[choice_idx]
                video_files = list(selected_folder.glob("*.mp4")) + list(selected_folder.glob("*.mov")) + list(selected_folder.glob("*.avi"))
                if not video_files:
                    print(f"❌ У папці {selected_folder.name} немає відео файлів!")
                    continue
                print(f"✅ Вибрано папку: {selected_folder.name} ({len(video_files)} відео)")
                return selected_folder
            else:
                print(f"❌ Невірний вибір. Введіть число від 1 до {len(bg_folders)}")
        except ValueError:
            print(f"❌ Невірний вибір. Введіть число від 1 до {len(bg_folders)}")

def chunk_words(words, n=2):
    """Групує слова для субтитрів"""
    chunks = []
    i = 0
    while i < len(words):
        group = words[i:i+n]
        text = " ".join([w["word"] for w in group])
        start = group[0]["start"]
        end = group[-1]["end"]
        chunks.append({"text": text, "start": start, "end": end})
        i += n
    return chunks

def create_ass_subtitles(chunks, ass_path, font_path=None, font_size=48, vertical=False):
    """Створює ASS файл субтитрів з центрованим текстом та обводкою"""
    font_name = "Komika Axis"
    if vertical:
        font_size = 28
        margin_v = 250
        margin_lr = 100
    else:
        margin_v = 0
        margin_lr = 30
    
    ass_content = f"""[Script Info]
Title: Generated Subtitles
ScriptType: v4.00+

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,{font_name},{font_size},&H00FFFFFF,&H000000FF,&H00000000,&H80000000,1,0,0,0,100,100,0,0,1,3,0,5,{margin_lr},{margin_lr},{margin_v},1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
"""
    
    for chunk in chunks:
        start_time = format_ass_time(chunk["start"])
        end_time = format_ass_time(chunk["end"])
        text = chunk["text"].replace("\\", "\\\\").replace("{", "\\{").replace("}", "\\}")
        
        if vertical:
            words = text.split()
            lines = []
            current_line = ""
            max_chars = 20
            
            for word in words:
                if len(word) > max_chars:
                    if current_line:
                        lines.append(current_line.strip())
                        current_line = ""
                    while len(word) > max_chars:
                        lines.append(word[:max_chars-1] + "-")
                        word = word[max_chars-1:]
                    if word:
                        current_line = word
                elif len(current_line + " " + word) <= max_chars:
                    current_line += (" " + word if current_line else word)
                else:
                    if current_line:
                        lines.append(current_line.strip())
                    current_line = word
            
            if current_line:
                lines.append(current_line.strip())
            
            text = "\\N".join(lines)
        
        ass_content += f"Dialogue: 0,{start_time},{end_time},Default,,0,0,0,,{text}\n"
    
    with open(ass_path, 'w', encoding='utf-8-sig') as f:
        f.write(ass_content)

def format_ass_time(seconds):
    """Конвертує секунди в формат часу ASS (H:MM:SS.CC)"""
    hours = int(seconds // 3600)
    minutes = int((seconds % 3600) // 60)
    secs = seconds % 60
    return f"{hours}:{minutes:02d}:{secs:05.2f}"

def apply_subtitles_ass(video_path, chunks, story_id, vertical=False, temp_folder=None):
    """Додає субтитри використовуючи ASS файл"""
    if not chunks:
        return video_path
    
    print(f"???? Створення ASS субтитрів для {len(chunks)} фрагментів...")
    suffix = "_vertical" if vertical else ""
    ass_path = temp_folder / f"{story_id}_subtitles{suffix}.ass"
    create_ass_subtitles(chunks, ass_path, font_size=56, vertical=vertical)
    
    output_video = temp_folder / f"{story_id}_with_subtitles{suffix}.mp4"
    ass_path_escaped = str(ass_path).replace("\\", "/").replace(":", "\\:")
    
    cmd = [
        "ffmpeg", "-y", "-i", str(video_path), "-vf", f"ass='{ass_path_escaped}'",
        "-c:v", "h264_nvenc", "-preset", "p4", "-b:v", "5M", "-c:a", "copy", str(output_video)
    ]
    
    try:
        print(f" ???? Додавання субтитрів...")
        subprocess.run(cmd, check=True, capture_output=True, text=True)
        print(f" ✅ Субтитри успішно додано")
        return output_video
    except subprocess.CalledProcessError as e:
        print(f"❌ Помилка при додаванні субтитрів: {e}")
        return video_path

def select_youtube_video_format():
    """Дозволяє користувачу вибрати формат YouTube відео"""
    print("\n" + "="*60)
    print("???? ВИБІР ФОРМАТУ YOUTUBE ВІДЕО")
    print("="*60)
    print("1. ????️ Горизонтальне відео (1920x1080) - класичний YouTube")
    print("2. ???? Вертикальне відео (1080x1920) - повна версія для Shorts")
    print("3. ???? Обидва формати")
    print("="*60)
    
    while True:
        choice = input("Ваш вибір (1-3): ").strip()
        if choice in ["1", "2", "3"]:
            return choice
        print("❌ Невірний вибір. Введіть 1, 2 або 3")

def create_horizontal_video_with_speedup(story_id, preview_txt, audio_preview, final_audio, selected_template, horizontal_generator, chunks, speed_config, output_dir, video_title, temp_folder):
    """Створює горизонтальне відео з прискоренням"""
    combined_video, preview_img_path = horizontal_generator.create_horizontal_video(
        story_id, preview_txt, audio_preview, final_audio, selected_template
    )
    horizontal_generator.preview_img_path = preview_img_path
    
    if chunks:
        final_video = apply_subtitles_ass(combined_video, chunks, story_id, temp_folder=temp_folder)
    else:
        print("⚠️ Субтитри не знайдено")
        final_video = combined_video
    
    horizontal_output_path = output_dir / f"{video_title}.mp4"
    horizontal_speed_settings = speed_config['youtube_horizontal']
    
    if speed_config['mode'] == 'interactive':
        original_duration = final_audio.duration_seconds
        horizontal_speed_settings = get_interactive_speed_settings('youtube_horizontal', original_duration)
    
    if horizontal_speed_settings['enabled'] and horizontal_speed_settings.get('speed', 1.0) > 1.0:
        print(f"⚡ Прискорення горизонтального відео...")
        speed_factor = horizontal_speed_settings.get('speed')
        if horizontal_speed_settings.get('target_duration'):
            speed_factor = final_audio.duration_seconds / horizontal_speed_settings['target_duration']
            speed_factor = min(speed_factor, 4.0)
        
        horizontal_generator.finalize_video(final_video, temp_folder / f"{story_id}_voice.wav", temp_folder / "temp_horizontal.mp4")
        apply_video_speedup(temp_folder / "temp_horizontal.mp4", horizontal_output_path, speed_factor, temp_folder)
    else:
        horizontal_generator.finalize_video(final_video, temp_folder / f"{story_id}_voice.wav", horizontal_output_path)
    
    print(f"????️ Горизонтальне відео створено: {horizontal_output_path}")

def create_full_vertical_video_with_speedup(story_id, story_txt, preview_txt, horizontal_generator, vertical_generator, selected_template, coqui_tts, asr_model, chunks, speed_config, output_dir, video_title, temp_folder):
    """Створює повне вертикальне відео з прискоренням"""
    audio_preview = coqui_tts(preview_txt)
    preview_img_path = temp_folder / f"{story_id}_preview.png"
    horizontal_generator.preview_generator.draw_horizontal_preview(preview_txt, preview_img_path, selected_template)
    
    combined_vertical_video, vertical_audio_path = vertical_generator.create_full_vertical_video(
        story_txt, preview_txt, story_id, preview_img_path, coqui_tts
    )
    
    print(f"???? Розпізнавання мовлення для вертикального відео...")
    segments_vertical, _ = asr_model.transcribe(str(vertical_audio_path), word_timestamps=True)
    
    story_start_time = audio_preview.duration_seconds + 0.5
    story_words = []
    for seg in segments_vertical:
        for w in seg.words:
            if w.start >= story_start_time:
                story_words.append({"word": w.word.strip(), "start": w.start, "end": w.end})
    
    chunks_vertical = chunk_words(story_words)
    
    if chunks_vertical:
        final_vertical_video = apply_subtitles_ass(combined_vertical_video, chunks_vertical, f"{story_id}_full_vertical", vertical=True, temp_folder=temp_folder)
    else:
        print("⚠️ Субтитри не знайдено для вертикального відео")
        final_vertical_video = combined_vertical_video
    
    vertical_path = output_dir / f"vertical_{video_title}.mp4"
    vertical_speed_settings = speed_config['youtube_vertical']
    
    if speed_config['mode'] == 'interactive':
        vertical_duration = AudioSegment.from_wav(vertical_audio_path).duration_seconds
        vertical_speed_settings = get_interactive_speed_settings('youtube_vertical', vertical_duration)
    
    if vertical_speed_settings['enabled'] and vertical_speed_settings.get('speed', 1.0) > 1.0:
        print(f"⚡ Прискорення вертикального відео...")
        speed_factor = vertical_speed_settings.get('speed')
        if vertical_speed_settings.get('target_duration'):
            speed_factor = AudioSegment.from_wav(vertical_audio_path).duration_seconds / vertical_speed_settings['target_duration']
            speed_factor = min(speed_factor, 4.0)
        
        vertical_generator.finalize_shorts_video(final_vertical_video, vertical_audio_path, temp_folder / "temp_vertical.mp4")
        apply_video_speedup(temp_folder / "temp_vertical.mp4", vertical_path, speed_factor, temp_folder)
    else:
        vertical_generator.finalize_shorts_video(final_vertical_video, vertical_audio_path, vertical_path)
    
    print(f"???? Повне вертикальне відео створено: {vertical_path}")

def process_youtube_story(story_path, horizontal_generator, vertical_generator, selected_template, coqui_tts, asr_model, temp_folder, final_dir, speed_config):
    """Обробляє YouTube історію з вибором формату відео"""
    story_txt = story_path.read_text(encoding="utf-8").strip()
    preview_path = story_path.with_name(f"{story_path.stem}_preview.txt")
    description_path = story_path.with_name(f"{story_path.stem}_description.txt")
    
    if not preview_path.exists() or not description_path.exists():
        print(f"❌ Не знайдено preview або description для {story_path.name}")
        return
    
    preview_txt = preview_path.read_text(encoding="utf-8").strip()
    description_txt = description_path.read_text(encoding="utf-8").strip()
    
    if not story_txt or not preview_txt or not description_txt:
        print(f"❌ Порожні файли для {story_path.name}")
        return
    
    story_id = hashlib.md5(story_txt.encode()).hexdigest()
    video_title = safe_filename(preview_txt)
    output_dir = final_dir / video_title
    output_dir.mkdir(parents=True, exist_ok=True)
    
    print(f"???? Використовується template: {selected_template}")
    
    # НОВИЙ ВИБІР ФОРМАТУ
    format_choice = select_youtube_video_format()
    
    print(f"???? Генерація аудіо...")
    audio_preview = coqui_tts(preview_txt)
    audio_story = coqui_tts(story_txt)
    final_audio = audio_preview + AudioSegment.silent(duration=500) + audio_story
    
    original_duration = final_audio.duration_seconds
    audio_path = temp_folder / f"{story_id}_voice.wav"
    final_audio.export(audio_path, format="wav")
    
    print(f"???? Розпізнавання мовлення...")
    story_offset = audio_preview.duration_seconds + 0.5
    segments, _ = asr_model.transcribe(str(audio_path), word_timestamps=True)
    words = [{"word": w.word.strip(), "start": w.start, "end": w.end} 
             for seg in segments for w in seg.words if w.start >= story_offset]
    chunks = chunk_words(words)
    
    # Створюємо відео залежно від вибору
    if format_choice == "1":  # Тільки горизонтальне
        print(f"????️ Створення горизонтального відео...")
        create_horizontal_video_with_speedup(
            story_id, preview_txt, audio_preview, final_audio, selected_template,
            horizontal_generator, chunks, speed_config, output_dir, video_title, temp_folder
        )
        
    elif format_choice == "2":  # Тільки вертикальне (повне)
        print(f"???? Створення повного вертикального відео...")
        create_full_vertical_video_with_speedup(
            story_id, story_txt, preview_txt, horizontal_generator, vertical_generator,
            selected_template, coqui_tts, asr_model, chunks, speed_config, 
            output_dir, video_title, temp_folder
        )
        
    elif format_choice == "3":  # Обидва формати
        print(f"???? Створення обох форматів...")
        create_horizontal_video_with_speedup(
            story_id, preview_txt, audio_preview, final_audio, selected_template,
            horizontal_generator, chunks, speed_config, output_dir, video_title, temp_folder
        )
        create_full_vertical_video_with_speedup(
            story_id, story_txt, preview_txt, horizontal_generator, vertical_generator,
            selected_template, coqui_tts, asr_model, chunks, speed_config, 
            output_dir, video_title, temp_folder
        )
    
    # Зберігаємо додаткові файли
    if hasattr(horizontal_generator, 'preview_img_path'):
        Image.open(horizontal_generator.preview_img_path).convert("RGB").save(output_dir / "preview.jpg", "JPEG")
    (output_dir / "description.txt").write_text(description_txt, encoding="utf-8")
    
    # Очищуємо оригінальні файли
    for f in [story_path, preview_path, description_path]:
        f.write_text("", encoding="utf-8")
    
    print(f"\n✅ {story_path.name} успішно оброблено!")
    
    # Telegram preview
    try:
        print(f"???? Надсилання превью в Telegram...")
        preview_jpg_path = output_dir / "preview.jpg"
        if preview_jpg_path.exists():
            asyncio.run(send_video_preview(preview_txt, str(preview_jpg_path)))
            print(f"✅ Превью успішно надіслано в Telegram")
    except Exception as e:
        print(f"❌ Помилка при надсиланні превью: {e}")

def select_video_type():
    """Дозволяє користувачу вибрати тип відео для генерації"""
    print("\n" + "="*60)
    print("???? ВИБІР ТИПУ ВІДЕО")
    print("="*60)
    print("1. ???? YouTube відео (з вибором формату)")
    print("2. ???? TikTok відео (розділені на частини)")
    print("3. ???? Обидва типи")
    print("="*60)
    
    while True:
        choice = input("Ваш вибір (1-3): ").strip()
        if choice in ["1", "2", "3"]:
            return choice
        print("❌ Невірний вибір. Введіть 1, 2 або 3")

def select_template_for_video(template_mode, default_template, preview_generator):
    """Вибирає template для поточного відео залежно від режиму"""
    if template_mode == "fixed":
        return default_template
    elif template_mode == "interactive":
        template_name, _ = preview_generator.select_template()
        return template_name
    else: # auto
        return default_template

def process_youtube_videos(stories_dir, generators, default_template, template_mode, preview_generator, coqui_tts, asr_model, temp_folder, final_dir, speed_config):
    """Обробляє YouTube відео з прискоренням"""
    story_files = list(stories_dir.glob("*.txt"))
    story_files = [f for f in story_files if not f.name.endswith("_preview.txt") and not f.name.endswith("_description.txt")]
    
    if not story_files:
        print("❌ YouTube історій не знайдено")
        return 0
    
    print(f"???? Знайдено {len(story_files)} YouTube історій для обробки")
    
    processed_count = 0
    for i, story in enumerate(story_files, 1):
        print(f"\n{'='*60}")
        print(f"???? [{i}/{len(story_files)}] Обробка YouTube історії → {story.name}")
        print(f"{'='*60}")
        
        try:
            selected_template = select_template_for_video(template_mode, default_template, preview_generator)
            if selected_template is None:
                print("❌ Template не вибрано, пропускаємо відео")
                continue
            
            process_youtube_story(
                story, generators['horizontal'], generators['vertical'], selected_template,
                coqui_tts, asr_model, temp_folder, final_dir, speed_config
            )
            processed_count += 1
        except Exception as e:
            print(f"❌ Помилка з {story.name}: {e}")
    
    return processed_count

def main():
    """Головна функція з вибором типу відео та налаштуванням прискорення"""
    # Запитуємо про вимкнення ПК
    off = int(input("Введіть 1 щоб вимкнути ПК після завершення скрипта: "))
    
    # --- Шляхи ---
    ROOT = Path(r"K:/test_ffmpeg_drama")
    BACKGROUNDS_DIR = ROOT / "backgrounds"
    STORIES_DIR = ROOT / "stories"
    TIKTOK_STORIES_DIR = ROOT / "tiktok_stories"
    TEMP_FOLDER = ROOT / "temp"
    TEMP_TIKTOK_FOLDER = ROOT / "temp_tiktok"
    FINAL_DIR = ROOT / "final_videos"
    FINAL_TIKTOK_DIR = ROOT / "final_tiktok_videos"
    TEMPLATES_DIR = ROOT / "templates"
    FONT_PREVIEW = ROOT / "fonts/NotoSans-ExtraBold.ttf"
    SUB_FONT_PATH = ROOT / "fonts/KOMIKAX_.ttf"
    BEEP_SOUND_PATH = ROOT / "beep.wav"
    
    # --- Ініціалізація папок ---
    BACKGROUNDS_DIR.mkdir(parents=True, exist_ok=True)
    STORIES_DIR.mkdir(parents=True, exist_ok=True)
    TIKTOK_STORIES_DIR.mkdir(parents=True, exist_ok=True)
    FINAL_DIR.mkdir(parents=True, exist_ok=True)
    FINAL_TIKTOK_DIR.mkdir(parents=True, exist_ok=True)
    TEMPLATES_DIR.mkdir(parents=True, exist_ok=True)
    
    # --- Вибір папки з фоновими відео ---
    SOURCE_FOLDER = select_background_folder(BACKGROUNDS_DIR)
    if SOURCE_FOLDER is None:
        print("❌ Неможливо продовжити без фонових відео")
        return
    
    # Вибір типу відео
    video_type = select_video_type()
    
    # Налаштування прискорення відео
    speed_config = get_speed_settings()
    
    # Налаштування TikTok тривалості (якщо потрібно)
    tiktok_min_duration = 63
    tiktok_target_duration = 105
    if video_type in ["2", "3"]:  # TikTok або обидва
        tiktok_min_duration, tiktok_target_duration = get_tiktok_duration_settings()
    
    # --- Параметри ---
    DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
    if DEVICE == "cuda":
        torch.cuda.empty_cache()
        torch.backends.cudnn.benchmark = True
        torch.set_float32_matmul_precision("high")
        torch.set_default_device(DEVICE)
    
    RESOLUTION = (1920, 1080)
    SHORTS_RESOLUTION = (1080, 1920)
    TIKTOK_RESOLUTION = (1080, 1920)
    FADE_DURATION = 1
    SHORTS_TARGET_DURATION = 59
    
    # Очищуємо temp папки залежно від типу відео
    if video_type in ["1", "3"]:  # YouTube або обидва
        shutil.rmtree(TEMP_FOLDER, ignore_errors=True)
        TEMP_FOLDER.mkdir(parents=True, exist_ok=True)
    
    if video_type in ["2", "3"]:  # TikTok або обидва
        shutil.rmtree(TEMP_TIKTOK_FOLDER, ignore_errors=True)
        TEMP_TIKTOK_FOLDER.mkdir(parents=True, exist_ok=True)
    
    # --- Ініціалізація TTS та ASR ---
    MODEL = "tts_models/en/vctk/vits"
    VOICE = "p312"
    tts = TTS(model_name=MODEL, progress_bar=False)
    ASR_MODEL = WhisperModel("small", device=DEVICE, compute_type="float16" if DEVICE == "cuda" else "int8")
    
    # --- Ініціалізація генераторів ---
    try:
        preview_generator = PreviewGenerator(
            templates_dir=TEMPLATES_DIR,
            font_path=FONT_PREVIEW,
            shorts_resolution=SHORTS_RESOLUTION
        )
        
        # Питаємо користувача який template використовувати
        print("\n???? Налаштування template для відео:")
        print("Ви можете:")
        print("1. Вибрати один template для всіх відео")
        print("2. Вибирати template для кожного відео окремо")
        print("3. Використовувати перший доступний template")
        
        template_choice = input("Ваш вибір (1-3): ").strip()
        
        if template_choice == "1":
            DEFAULT_TEMPLATE = preview_generator.select_template()[0]
            TEMPLATE_MODE = "fixed"
            print(f"✅ Вибрано template: {DEFAULT_TEMPLATE}")
        elif template_choice == "2":
            DEFAULT_TEMPLATE = None
            TEMPLATE_MODE = "interactive"
        else:
            DEFAULT_TEMPLATE = list(preview_generator.templates.keys())[0]
            TEMPLATE_MODE = "auto"
            print(f"✅ Буде використовуватись template: {DEFAULT_TEMPLATE}")
            
    except FileNotFoundError as e:
        print(f"❌ Помилка з template: {e}")
        # Створюємо приклад template
        example_template = TEMPLATES_DIR / "template_default_x40_y224_w1837_h751.png"
        if not example_template.exists():
            example_img = Image.new('RGBA', (1920, 1080), (255, 255, 255, 0))
            example_img.save(example_template)
        
        preview_generator = PreviewGenerator(
            templates_dir=TEMPLATES_DIR,
            font_path=FONT_PREVIEW,
            shorts_resolution=SHORTS_RESOLUTION
        )
        DEFAULT_TEMPLATE = "template_default_x40_y224_w1837_h751"
        TEMPLATE_MODE = "auto"
    
    # --- Створення TTS функції з цензурою ---
    coqui_tts = create_tts_with_censoring(tts, TEMP_FOLDER, BEEP_SOUND_PATH, VOICE)
    
    # --- Ініціалізація генераторів відео ---
    generators = {}
    if video_type in ["1", "3"]:  # YouTube або обидва
        generators['horizontal'] = HorizontalVideoGenerator(
            source_folder=SOURCE_FOLDER,
            temp_folder=TEMP_FOLDER,
            preview_generator=preview_generator,
            resolution=RESOLUTION,
            fade_duration=FADE_DURATION
        )
        generators['vertical'] = VerticalVideoGenerator(
            source_folder=SOURCE_FOLDER,
            temp_folder=TEMP_FOLDER,
            preview_generator=preview_generator,
            shorts_resolution=SHORTS_RESOLUTION,
            target_duration=SHORTS_TARGET_DURATION
        )
    
    if video_type in ["2", "3"]:  # TikTok або обидва
        tiktok_tts = create_tts_with_censoring(tts, TEMP_TIKTOK_FOLDER, BEEP_SOUND_PATH, VOICE)
        generators['tiktok'] = TikTokVideoGenerator(
            source_folder=SOURCE_FOLDER,
            temp_folder=TEMP_TIKTOK_FOLDER,
            preview_generator=preview_generator,
            tts_func=tiktok_tts,
            asr_model=ASR_MODEL,
            tiktok_resolution=TIKTOK_RESOLUTION,
            target_part_duration=tiktok_target_duration,
            min_part_duration=tiktok_min_duration
        )
    
    # Показуємо налаштування прискорення
    print(f"\n⚡ НАЛАШТУВАННЯ ПРИСКОРЕННЯ:")
    if speed_config['mode'] == '1':
        print("⚡ Режим: Без прискорення")
    elif speed_config['mode'] == '2':
        print(f"⚡ Режим: Фіксоване прискорення")
        for video_type_name, settings in speed_config.items():
            if video_type_name != 'mode' and settings['enabled']:
                print(f"  • {video_type_name}: {settings['speed']}x")
    elif speed_config['mode'] == '3':
        print(f"⚡ Режим: До цільової тривалості")
        for video_type_name, settings in speed_config.items():
            if video_type_name != 'mode' and settings['enabled']:
                print(f"  • {video_type_name}: до {settings['target_duration']}с")
    elif speed_config['mode'] == '4':
        print(f"⚡ Режим: Інтерактивне налаштування")
    
    # --- Обробка відео ---
    if video_type == "1":  # Тільки YouTube
        print(f"\n???? РЕЖИМ: Тільки YouTube відео")
        print(f"???? Використовуються фонові відео з: {SOURCE_FOLDER.name}")
        process_youtube_videos(STORIES_DIR, generators, DEFAULT_TEMPLATE, TEMPLATE_MODE, preview_generator, coqui_tts, ASR_MODEL, TEMP_FOLDER, FINAL_DIR, speed_config)
        
    elif video_type == "2":  # Тільки TikTok
        print(f"\n???? РЕЖИМ: Тільки TikTok відео")
        print(f"???? Використовуються фонові відео з: {SOURCE_FOLDER.name}")
        print(f"⚙️ Налаштування: мінімум {tiktok_min_duration}с, ціль {tiktok_target_duration}с")
        process_tiktok_videos(TIKTOK_STORIES_DIR, generators['tiktok'], generators.get('horizontal'), DEFAULT_TEMPLATE, TEMPLATE_MODE, preview_generator, tiktok_tts, ASR_MODEL, speed_config)
        
    elif video_type == "3":  # Обидва типи
        print(f"\n???????? РЕЖИМ: YouTube + TikTok відео")
        print(f"???? Використовуються фонові відео з: {SOURCE_FOLDER.name}")
        print(f"⚙️ TikTok налаштування: мінімум {tiktok_min_duration}с, ціль {tiktok_target_duration}с")
        
        # Спочатку YouTube
        youtube_processed = process_youtube_videos(STORIES_DIR, generators, DEFAULT_TEMPLATE, TEMPLATE_MODE, preview_generator, coqui_tts, ASR_MODEL, TEMP_FOLDER, FINAL_DIR, speed_config)
        
        # Потім TikTok
        tiktok_processed = process_tiktok_videos(TIKTOK_STORIES_DIR, generators['tiktok'], generators.get('horizontal'), DEFAULT_TEMPLATE, TEMPLATE_MODE, preview_generator, tiktok_tts, ASR_MODEL, speed_config)
        
        print(f"\n???? Загальний підсумок:")
        print(f"???? YouTube відео оброблено: {youtube_processed}")
        print(f"???? TikTok відео оброблено: {tiktok_processed}")
    
    print(f"\n???? Всі відео оброблено!")
    
    # Вимкнення ПК
    if off == 1:
        print("???? Вимкнення комп'ютера через 3 хвилини...")
        time.sleep(180)
        os.system("shutdown /s /t 1")
    else:
        print("???? Генератор відео завершив роботу успішно!")

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n⚠️ Скрипт перервано користувачем")
    except Exception as e:
        print(f"\n❌ Критична помилка: {e}")
        import traceback
        traceback.print_exc()
#tiktok_precessor.py
import os, re, shutil, subprocess, hashlib, random, time
from pathlib import Path
from datetime import datetime
from moviepy.editor import VideoFileClip
from TTS.api import TTS
from pydub import AudioSegment
from PIL import Image, ImageDraw, ImageFont
from faster_whisper import WhisperModel
import torch
import asyncio
import mouse
import json

# Імпорти модулів
from telegram_bot import send_video_preview
from preview_generator import PreviewGenerator


class TikTokVideoGenerator:
  def __init__(self, source_folder, temp_folder, preview_generator, tts_func, asr_model,
         tiktok_resolution=(1080, 1920), target_part_duration=105, min_part_duration=63):
    """
    Ініціалізація генератора TikTok відео

    Args:
      source_folder: Папка з фоновими відео
      temp_folder: Папка для тимчасових файлів
      preview_generator: Об'єкт PreviewGenerator для створення превью
      tts_func: Функція для TTS (передаємо coqui_tts)
      asr_model: Модель для розпізнавання мовлення
      tiktok_resolution: Розмір TikTok відео (ширина, висота)
      target_part_duration: Цільова тривалість кожної частини у секундах
      min_part_duration: Мінімальна тривалість частини (налаштовується користувачем)
    """
    self.source_folder = Path(source_folder)
    self.temp_folder = Path(temp_folder)
    self.preview_generator = preview_generator
    self.tts_func = tts_func
    self.asr_model = asr_model
    self.tiktok_resolution = tiktok_resolution
    self.target_part_duration = target_part_duration
    self.min_part_duration = min_part_duration
    self.fade_duration = 0.3 # Короткі fade для TikTok

    print(f"📱 TikTok генератор ініціалізовано:")
    print(f" • Мінімальна тривалість частини: {self.min_part_duration} секунд")
    print(f" • Цільова тривалість частини: {self.target_part_duration} секунд")
    print(f" • Без додавання мовчання - природна тривалість")

  def split_story_into_parts(self, story_text, preview_text):
    """
    Розділяє історію на частини оптимальної тривалості

    Args:
      story_text: Повний текст історії
      preview_text: Текст preview (тільки для part1)

    Returns:
      list: Список частин з текстом і номером
    """
    print(f"📝 Розділення історії на TikTok частини (без додавання мовчання)...")

    # Розділяємо історію на речення (покращена регулярка)
    sentence_pattern = r'(?<=[.!?])\s+'
    sentences = re.split(sentence_pattern, story_text.strip())

    # Очищаємо та фільтруємо речення
    clean_sentences = []
    for sentence in sentences:
      sentence = sentence.strip()
      if sentence and len(sentence) > 5: # Мінімум 5 символів
        # Додаємо крапку в кінці якщо немає розділового знаку
        if not sentence[-1] in '.!?':
          sentence += '.'
        clean_sentences.append(sentence)

    if not clean_sentences:
      # Якщо не знайшли речення, розділяємо по параграфах
      paragraphs = story_text.split('\n\n')
      clean_sentences = [p.strip() for p in paragraphs if p.strip()]

    if not clean_sentences:
      # Останній варіант - розділити на блоки по словах
      words = story_text.split()
      clean_sentences = [' '.join(words[i:i+15]) + '.' for i in range(0, len(words), 15)]

    print(f"📊 Знайдено {len(clean_sentences)} речень для розділення")

    # Розраховуємо тривалість preview (тільки для part1)
    preview_audio = self.tts_func(preview_text)
    preview_duration = preview_audio.duration_seconds + 0.5 # preview + пауза

    # Формуємо частини з перевіркою мінімальної тривалості
    parts = []
    current_sentences = []
    current_duration = 0
    part_number = 1

    for i, sentence in enumerate(clean_sentences):
      # Оцінюємо тривалість речення (більш консервативна оцінка)
      words_count = len(sentence.split())
      chars_count = len(sentence)
      # Більш точна оцінка тривалості для українського тексту
      sentence_duration = max(words_count / 2.0, chars_count / 10.0)

      # Додаємо речення до поточної частини
      current_sentences.append(sentence)
      current_duration += sentence_duration

      # Для part1 враховуємо preview
      base_duration = preview_duration if part_number == 1 else 0
      total_duration = current_duration + base_duration

      # Перевіряємо умови для завершення частини
      is_last_sentence = (i == len(clean_sentences) - 1)
      exceeds_target = total_duration >= self.target_part_duration
      meets_minimum = total_duration >= self.min_part_duration

      # Завершуємо частину якщо:
      # 1. Досягли цільової тривалості І маємо мінімум
      # 2. Це остання речення (НЕЗАЛЕЖНО від тривалості)
      if (exceeds_target and meets_minimum) or is_last_sentence:
        part_text = ' '.join(current_sentences)

        parts.append({
          'number': part_number,
          'text': part_text,
          'preview_text': preview_text if part_number == 1 else None,
          'estimated_duration': total_duration,
          'has_preview': part_number == 1,
          'sentence_count': len(current_sentences),
          'is_natural_duration': True # Без штучного подовження
        })

        duration_status = "✅" if total_duration >= self.min_part_duration else "⚠️"
        print(f" {duration_status} Частина {part_number}: {len(current_sentences)} речень, {total_duration:.1f}с (природна тривалість)")

        # Починаємо нову частину
        part_number += 1
        current_sentences = []
        current_duration = 0

    print(f"✅ Історія розділена на {len(parts)} частин (без штучного подовження):")
    for part in parts:
      words_count = len(part['text'].split())
      preview_marker = " (з preview)" if part['has_preview'] else ""
      duration_status = "✅" if part['estimated_duration'] >= self.min_part_duration else "⚠️"
      print(f" {duration_status} Частина {part['number']}: {words_count} слів, {part.get('sentence_count', 0)} речень, ~{part['estimated_duration']:.1f} сек{preview_marker}")

    return parts

  def merge_short_parts(self, parts):
    """Об'єднує тільки дуже короткі частини (менше 30 секунд)"""
    print(f"🔄 Об'єднання лише критично коротких частин (менше 30с)...")
    merged_parts = []
    i = 0

    critical_min_duration = 30 # Критично мінімальна тривалість - 30 секунд

    while i < len(parts):
      current_part = parts[i].copy()
      original_number = current_part['number']

      # Об'єднуємо тільки критично короткі частини
      if current_part['estimated_duration'] < critical_min_duration and i + 1 < len(parts):
        next_part = parts[i + 1]

        # Об'єднуємо тексти
        current_part['text'] += ' ' + next_part['text']

        # Перерахуємо тривалість для об'єднаного тексту
        words_count = len(current_part['text'].split())
        chars_count = len(current_part['text'])
        new_duration = max(words_count / 2.0, chars_count / 10.0)

        # Додаємо preview тільки для першої частини
        if current_part['has_preview']:
          preview_audio = self.tts_func(current_part['preview_text'])
          preview_duration = preview_audio.duration_seconds + 0.5
          new_duration += preview_duration

        current_part['estimated_duration'] = new_duration
        current_part['sentence_count'] = current_part.get('sentence_count', 1) + next_part.get('sentence_count', 1)

        # Preview залишається тільки в першій частині
        if not current_part['has_preview'] and next_part['has_preview']:
          current_part['preview_text'] = next_part['preview_text']
          current_part['has_preview'] = True

        i += 1
        print(f" 📎 Об'єднано критично короткі частини {original_number} і {next_part['number']} → {new_duration:.1f}с")

      # Перенумеровуємо частини
      current_part['number'] = len(merged_parts) + 1
      merged_parts.append(current_part)
      i += 1

    print(f"✅ Після об'єднання: {len(merged_parts)} частин")
    for part in merged_parts:
      duration_status = "✅" if part['estimated_duration'] >= 30 else "⚠️"
      print(f" {duration_status} Частина {part['number']}: ~{part['estimated_duration']:.1f}с")

    return merged_parts

  def create_background_segments_tiktok(self, story_id, part_number, duration):
    """Створює фонові відео сегменти для TikTok частини з 60 FPS"""
    bg_videos = list(self.source_folder.glob("*.mp4"))
    random.shuffle(bg_videos)
    bg_iter = iter(bg_videos)

    clips = []
    duration_collected = 0

    while duration_collected < 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_duration = duration - duration_collected
      use_duration = min(clip.duration, remaining_duration)
      clip.close()

      if use_duration <= 0:
        break

      clip_output = self.temp_folder / f"{story_id}_part{part_number}_bg_{len(clips):02d}.mp4"

      # Базові фільтри для вертикального відео з 60 FPS
      base_filters = f"scale='max({self.tiktok_resolution[0]},iw*{self.tiktok_resolution[1]}/ih)':'max({self.tiktok_resolution[1]},ih*{self.tiktok_resolution[0]}/iw)',crop={self.tiktok_resolution[0]}:{self.tiktok_resolution[1]},fps=60"

      # Додаємо fade ефекти
      fade_filters = self.apply_safe_fade_filters(use_duration, base_filters)

      subprocess.run([
        "ffmpeg", "-y", "-i", str(bg_video), "-ss", "0", "-t", str(use_duration),
        "-vf", fade_filters,
        "-an", "-c:v", "h264_nvenc", "-preset", "p4", "-b:v", "8M", "-r", "60", str(clip_output)
      ], check=True)

      clips.append(clip_output)
      duration_collected += use_duration

    return clips

  def apply_safe_fade_filters(self, use_dur, base_filters):
    """Застосовує fade фільтри безпечно"""
    fade_in_duration = min(self.fade_duration, use_dur * 0.3)
    fade_out_duration = min(self.fade_duration, 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_with_overlay_tiktok(self, story_id, part_number, preview_clips, horizontal_preview_path, preview_duration):
    """Створює preview відео з накладеним вертикальним preview зображенням для TikTok з 60 FPS"""
    # Об'єднуємо всі preview кліпи
    if len(preview_clips) > 1:
      preview_concat_list = self.temp_folder / f"{story_id}_part{part_number}_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}_part{part_number}_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", "-r", "60", str(preview_combined)
      ], check=True)
    else:
      preview_combined = preview_clips[0]

    # Створюємо вертикальне preview зображення
    preview_img_path = self.temp_folder / f"{story_id}_part{part_number}_preview_vertical.png"
    self.preview_generator.draw_vertical_preview(horizontal_preview_path, preview_img_path)

    # Накладаємо preview зображення на відео
    preview_video = self.temp_folder / f"{story_id}_part{part_number}_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", "-r", "60", str(preview_video)
    ], check=True)

    return preview_video

  def combine_tiktok_segments(self, story_id, part_number, preview_video, story_clips):
    """Об'єднує всі сегменти TikTok відео з 60 FPS"""
    if preview_video:
      used_clips = [preview_video] + story_clips
    else:
      used_clips = story_clips

    concat_list = self.temp_folder / f"{story_id}_part{part_number}_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}_part{part_number}_combined.mp4"
    subprocess.run([
      "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", str(concat_list),
      "-c:v", "h264_nvenc", "-b:v", "8M", "-preset", "p4", "-r", "60", str(combined_path)
    ], check=True)

    return combined_path

  def create_tiktok_part(self, story_part, story_id, horizontal_preview_path, selected_template):
    """
    Створює одну частину TikTok відео БЕЗ ДОДАВАННЯ МОВЧАННЯ

    Args:
      story_part: Словник з інформацією про частину
      story_id: ID історії
      horizontal_preview_path: Шлях до горизонтального preview
      selected_template: Вибраний template

    Returns:
      tuple: (video_path, audio_path, total_duration)
    """
    part_number = story_part['number']
    story_text = story_part['text']
    has_preview = story_part['has_preview']

    print(f"🎬 Створення TikTok частини {part_number} (без додавання мовчання)...")
    print(f"📝 Текст частини ({len(story_text)} символів): {story_text[:100]}...")

    # Генеруємо аудіо для цієї частини
    if has_preview:
      # Тільки для part1 додаємо preview
      preview_text = story_part['preview_text']
      audio_preview = self.tts_func(preview_text)
      audio_story = self.tts_func(story_text)
      final_audio = audio_preview + AudioSegment.silent(duration=500) + audio_story
      preview_duration = audio_preview.duration_seconds
    else:
      # Для інших частин тільки story
      audio_story = self.tts_func(story_text)
      final_audio = audio_story
      preview_duration = 0

    audio_path = self.temp_folder / f"{story_id}_part{part_number}_voice.wav"
    final_audio.export(audio_path, format="wav")

    # Розраховуємо тривалості
    story_duration = audio_story.duration_seconds
    total_duration = final_audio.duration_seconds # ПРИРОДНА тривалість без додавання

    print(f"⏱️ Природна тривалість частини {part_number}: story={story_duration:.1f}с, загалом={total_duration:.1f}с")

    # НЕ ДОДАЄМО МОВЧАННЯ! Залишаємо природну тривалість

    # Створюємо відео сегменти
    preview_video = None
    if has_preview:
      # Створюємо preview сегменти тільки для part1
      preview_clips = self.create_background_segments_tiktok(story_id, part_number, preview_duration)
      preview_video = self.create_preview_with_overlay_tiktok(
        story_id, part_number, preview_clips, horizontal_preview_path, preview_duration
      )

    # Створюємо story сегменти (точна тривалість story аудіо)
    story_video_duration = story_duration # Використовуємо точну тривалість story
    story_clips = self.create_background_segments_tiktok(story_id, f"{part_number}_story", story_video_duration)

    # Об'єднуємо сегменти
    combined_video = self.combine_tiktok_segments(story_id, part_number, preview_video, story_clips)

    return combined_video, audio_path, total_duration

  def apply_subtitles_ass_tiktok(self, video_path, chunks, story_id, part_number):
    """Додає субтитри для TikTok відео використовуючи ASS файл"""
    if not chunks:
      return video_path

    print(f"🔤 Створення ASS субтитрів для частини {part_number}...")

    # Створюємо ASS файл
    ass_path = self.temp_folder / f"{story_id}_part{part_number}_subtitles.ass"
    self.create_ass_subtitles_tiktok(chunks, ass_path)

    # Застосовуємо субтитри з 60 FPS
    output_video = self.temp_folder / f"{story_id}_part{part_number}_with_subtitles.mp4"

    # Escape шлях для Windows
    ass_path_escaped = str(ass_path).replace("\\", "/").replace(":", "\\:")

    cmd = [
      "ffmpeg", "-y",
      "-i", str(video_path),
      "-vf", f"ass='{ass_path_escaped}'",
      "-c:v", "h264_nvenc", "-preset", "p4", "-b:v", "8M", "-r", "60",
      "-c:a", "copy",
      str(output_video)
    ]

    try:
      subprocess.run(cmd, check=True, capture_output=True, text=True)
      print(f" ✅ Субтитри для частини {part_number} успішно додано")
      return output_video
    except subprocess.CalledProcessError as e:
      print(f"❌ Помилка при додаванні субтитрів для частини {part_number}: {e}")
      return video_path

  def create_ass_subtitles_tiktok(self, chunks, ass_path):
    """Створює ASS файл субтитрів для TikTok з оптимізованими налаштуваннями"""
    font_name = "Komika Axis"
    font_size = 28 # Менший шрифт для TikTok
    margin_v = 250 # Вертикальні відступи
    margin_lr = 100 # Горизонтальні відступи

    # ASS заголовок з налаштуваннями стилю
    ass_content = f"""[Script Info]
Title: TikTok Subtitles
ScriptType: v4.00+

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,{font_name},{font_size},&H00FFFFFF,&H000000FF,&H00000000,&H80000000,1,0,0,0,100,100,0,0,1,3,0,5,{margin_lr},{margin_lr},{margin_v},1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
"""

    # Додаємо субтитри
    for chunk in chunks:
      start_time = self.format_ass_time(chunk["start"])
      end_time = self.format_ass_time(chunk["end"])

      # Екрануємо спеціальні символи ASS
      text = chunk["text"].replace("\\", "\\\\").replace("{", "\\{").replace("}", "\\}")

      # Розбиваємо довгий текст на коротші рядки
      words = text.split()
      lines = []
      current_line = ""
      max_chars = 20 # Максимум символів на рядок

      for word in words:
        # Перевіряємо чи довге слово поміститься окремо
        if len(word) > max_chars:
          if current_line:
            lines.append(current_line.strip())
            current_line = ""
          # Розбиваємо довге слово
          while len(word) > max_chars:
            lines.append(word[:max_chars-1] + "-")
            word = word[max_chars-1:]
          if word:
            current_line = word
        elif len(current_line + " " + word) <= max_chars:
          current_line += (" " + word if current_line else word)
        else:
          if current_line:
            lines.append(current_line.strip())
          current_line = word

      if current_line:
        lines.append(current_line.strip())

      text = "\\N".join(lines) # \\N - перенос рядка в ASS

      ass_content += f"Dialogue: 0,{start_time},{end_time},Default,,0,0,0,,{text}\n"

    # Записуємо файл
    with open(ass_path, 'w', encoding='utf-8-sig') as f:
      f.write(ass_content)

  def format_ass_time(self, seconds):
    """Конвертує секунди в формат часу ASS (H:MM:SS.CC)"""
    hours = int(seconds // 3600)
    minutes = int((seconds % 3600) // 60)
    secs = seconds % 60
    return f"{hours}:{minutes:02d}:{secs:05.2f}"

  def chunk_words(self, words, n=2):
    """Групує слова для субтитрів"""
    chunks = []
    i = 0
    while i < len(words):
      group = words[i:i+n]
      text = " ".join([w["word"] for w in group])
      start = group[0]["start"]
      end = group[-1]["end"]
      chunks.append({"text": text, "start": start, "end": end})
      i += n
    return chunks

  def finalize_tiktok_video(self, combined_video, audio_path, output_path):
    """
    Фінальна збірка TikTok відео з аудіо та 60 FPS

    Args:
      combined_video: Шлях до відео без аудіо
      audio_path: Шлях до аудіо файлу
      output_path: Шлях для збереження фінального відео
    """
    print(f"🎬 Фінальна збірка TikTok відео з 60 FPS (природна тривалість)...")
    subprocess.run([
      "ffmpeg", "-y", "-i", str(combined_video), "-i", str(audio_path),
      "-c:v", "h264_nvenc", "-preset", "p4", "-b:v", "8M", "-r", "60",
      "-c:a", "aac", "-shortest", "-movflags", "+faststart",
      str(output_path)
    ], check=True)


def safe_filename(name):
  """Створює безпечну назву файлу"""
  name = re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name)
  name = re.sub(r'\s+','_', name).strip("_")
  name = name.rstrip(". ")
  return name[:50]


def detect_censored_words(text):
  """Знаходить слова з зірочками для цензури"""
  pattern = r'\b\w*\*+\w*\b'
  return re.findall(pattern, text)


def split_text_with_censoring(text):
  """Розділяє текст на частини, виділяючи цензуровані слова"""
  parts = []
  pattern = r'(\b\w*\*+\w*\b)'
  split_parts = re.split(pattern, text)

  for part in split_parts:
    if part.strip():
      if re.match(r'\b\w*\*+\w*\b', part):
        parts.append({"text": part, "is_censored": True})
      else:
        parts.append({"text": part, "is_censored": False})
  return parts


def generate_beep_sound(beep_sound_path):
  """Генерує звук запікування якщо файл не існує"""
  if not beep_sound_path.exists():
    print("🔊 Генерація звуку запікування...")
    beep = AudioSegment.sine(1000, duration=800)
    beep = beep - 10
    beep.export(beep_sound_path, format="wav")
    print("✅ Звук запікування створено")


def create_tts_with_censoring(tts, temp_folder, beep_sound_path, voice):
  """Створює функцію TTS з підтримкою цензури"""

  def coqui_tts_simple(text: str) -> AudioSegment:
    """Простий TTS без цензури"""
    raw_path = temp_folder / f"raw_{hashlib.md5(text.encode()).hexdigest()}.wav"
    clean_path = temp_folder / f"audio_{hashlib.md5(text.encode()).hexdigest()}.wav"
    if clean_path.exists():
      return AudioSegment.from_wav(clean_path)
    tts.tts_to_file(text=text, speaker=voice, file_path=raw_path)
    subprocess.run(["ffmpeg", "-y", "-i", str(raw_path), "-af", "afftdn,volume=3dB", str(clean_path)], check=True)
    return AudioSegment.from_wav(clean_path)

  def coqui_tts_with_censoring(text: str) -> AudioSegment:
    """TTS з підтримкою цензури"""
    text_hash = hashlib.md5(text.encode()).hexdigest()
    final_audio_path = temp_folder / f"final_audio_{text_hash}.wav"

    if final_audio_path.exists():
      return AudioSegment.from_wav(final_audio_path)

    censored_words = detect_censored_words(text)

    if not censored_words:
      return coqui_tts_simple(text)

    print(f"🔇 Знайдено цензуровані слова: {censored_words}")

    generate_beep_sound(beep_sound_path)
    beep_audio = AudioSegment.from_wav(beep_sound_path)
    text_parts = split_text_with_censoring(text)
    final_audio = AudioSegment.empty()

    for part in text_parts:
      if part["is_censored"]:
        print(f" 🔇 Цензура: {part['text']} → *БІП*")
        final_audio += beep_audio
      else:
        text_content = part["text"].strip()
        if text_content:
          part_audio = coqui_tts_simple(text_content)
          final_audio += part_audio

    if len(final_audio) == 0:
      print("⚠️ Порожнє аудіо після цензури, використовуємо оригінальний TTS")
      return coqui_tts_simple(text)

    final_audio.export(final_audio_path, format="wav")
    return final_audio

  return coqui_tts_with_censoring


def select_template_for_video(template_mode, default_template, preview_generator):
  """Вибирає template для поточного відео залежно від режиму"""
  if template_mode == "fixed":
    return default_template
  elif template_mode == "interactive":
    template_name, _ = preview_generator.select_template()
    return template_name
  else: # auto
    return default_template


def get_tiktok_duration_settings():
  """
  Дозволяє користувачу налаштувати мінімальну довжину TikTok відео

  Returns:
    tuple: (min_duration, target_duration)
  """
  print("\n" + "="*60)
  print("📱 НАЛАШТУВАННЯ ТРИВАЛОСТІ TIKTOK ВІДЕО")
  print("="*60)
  print("Налаштуйте мінімальну тривалість частин TikTok відео:")
  print("• Рекомендовано: 30-60 секунд (без додавання мовчання)")
  print("• Мінімум: 15 секунд")
  print("• Максимум: 300 секунд (5 хвилин)")
  print("• ✨ НОВА ПОВЕДІНКА: Мовчання НЕ додається - тільки природна тривалість!")
  print("="*60)

  while True:
    try:
      min_duration_input = input("Введіть мінімальну тривалість частини (секунди, за замовчуванням 30): ").strip()

      if not min_duration_input:
        min_duration = 30 # Зменшено за замовчуванням
        break

      min_duration = int(min_duration_input)

      if min_duration < 15:
        print("❌ Мінімальна тривалість не може бути менше 15 секунд!")
        continue
      elif min_duration > 300:
        print("❌ Мінімальна тривалість не може бути більше 300 секунд!")
        continue
      else:
        break

    except ValueError:
      print("❌ Введіть число!")

  # Автоматично розраховуємо цільову тривалість
  target_duration = min(min_duration + 40, min_duration * 1.5)
  target_duration = max(target_duration, min_duration + 10) # Мінімум +10 секунд

  print(f"✅ Налаштування TikTok відео:")
  print(f" • Мінімальна тривалість частини: {min_duration} секунд")
  print(f" • Цільова тривалість частини: {target_duration:.0f} секунд")
  print(f" • ⚡ ВАЖЛИВО: Мовчання НЕ додається - тільки природна тривалість аудіо!")
  print(f" • Частини коротші за 30с будуть об'єднані (критично короткі)")

  return min_duration, int(target_duration)


def process_tiktok_story(story_path, tiktok_generator, horizontal_generator, selected_template, tts_func, asr_model):
  """
  Обробляє одну TikTok історію БЕЗ ДОДАВАННЯ МОВЧАННЯ

  Args:
    story_path: Шлях до файлу з історією
    tiktok_generator: Генератор TikTok відео
    horizontal_generator: Генератор горизонтальних відео (для preview)
    selected_template: Вибраний template
    tts_func: Функція TTS
    asr_model: Модель ASR
  """
  # Читаємо файли
  story_txt = story_path.read_text(encoding="utf-8").strip()
  preview_path = story_path.with_name(f"{story_path.stem}_preview.txt")
  description_path = story_path.with_name(f"{story_path.stem}_description.txt")

  if not preview_path.exists() or not description_path.exists():
    print(f"❌ Не знайдено preview або description для {story_path.name}")
    return

  preview_txt = preview_path.read_text(encoding="utf-8").strip()
  description_txt = description_path.read_text(encoding="utf-8").strip()

  if not story_txt or not preview_txt or not description_txt:
    print(f"❌ Порожні файли для {story_path.name}")
    return

  story_id = hashlib.md5(story_txt.encode()).hexdigest()
  video_title = safe_filename(preview_txt)

  # Створюємо папку для результатів
  output_dir = Path("K:/test_ffmpeg_drama/final_tiktok_videos") / video_title
  output_dir.mkdir(parents=True, exist_ok=True)

  print(f"🎬 Обробка TikTok історії: {story_path.name}")
  print(f"📁 Папка результатів: {output_dir}")
  print(f"⚡ РЕЖИМ: Природна тривалість (без додавання мовчання)")

  # Створюємо горизонтальне preview (потрібне для вертикального)
  print(f"🖼️ Створення превью...")
  audio_preview = tts_func(preview_txt)

  # Створюємо preview зображення
  horizontal_preview_path = tiktok_generator.temp_folder / f"{story_id}_tiktok_preview.png"
  tiktok_generator.preview_generator.draw_horizontal_preview(
    preview_txt, horizontal_preview_path, selected_template
  )

  # Розділяємо історію на частини (БЕЗ ШТУЧНОГО ПОДОВЖЕННЯ)
  story_parts = tiktok_generator.split_story_into_parts(story_txt, preview_txt)

  if not story_parts:
    print(f"❌ Не вдалося розділити історію на частини")
    return

  # Об'єднуємо тільки критично короткі частини (менше 30 секунд)
  print(f"🔍 Перевірка критично коротких частин (менше 30 секунд)...")
  very_short_parts = [p for p in story_parts if p['estimated_duration'] < 30]

  if very_short_parts:
    print(f"⚠️ Знайдено {len(very_short_parts)} критично коротких частин, об'єднуємо...")
    for part in very_short_parts:
      print(f" ⚠️ Частина {part['number']}: {part['estimated_duration']:.1f}с (критично коротка)")
    story_parts = tiktok_generator.merge_short_parts(story_parts)
  else:
    print(f"✅ Всі частини довші за 30 секунд")

  # Створюємо кожну частину
  created_parts = []
  for part in story_parts:
    try:
      print(f"\n🎥 Створення частини {part['number']}/{len(story_parts)} (природна тривалість)...")

      # Створюємо TikTok відео для цієї частини БЕЗ ДОДАВАННЯ МОВЧАННЯ
      combined_video, audio_path, total_duration = tiktok_generator.create_tiktok_part(
        part, story_id, horizontal_preview_path, selected_template
      )

      # Розпізнавання мовлення для субтитрів
      print(f"🔤 Розпізнавання мовлення для частини {part['number']}...")
      segments, _ = asr_model.transcribe(str(audio_path), word_timestamps=True)

      # Створюємо субтитри
      if part['has_preview']:
        # Для part1 субтитри тільки для story частини (після preview та паузи)
        story_start_time = audio_preview.duration_seconds + 0.5
        story_words = []

        for seg in segments:
          for w in seg.words:
            if w.start >= story_start_time:
              story_words.append({"word": w.word.strip(), "start": w.start, "end": w.end})
      else:
        # Для інших частин субтитри для всього тексту
        story_words = []
        for seg in segments:
          for w in seg.words:
            story_words.append({"word": w.word.strip(), "start": w.start, "end": w.end})

      chunks = tiktok_generator.chunk_words(story_words)

      if chunks:
        final_video = tiktok_generator.apply_subtitles_ass_tiktok(
          combined_video, chunks, story_id, part['number']
        )
      else:
        print(f"⚠️ Субтитри не знайдено для частини {part['number']}")
        final_video = combined_video

      # Фінальна збірка частини
      part_filename = f"part{part['number']}.mp4"
      part_output_path = output_dir / part_filename
      tiktok_generator.finalize_tiktok_video(final_video, audio_path, part_output_path)

      created_parts.append({
        'number': part['number'],
        'path': part_output_path,
        'duration': total_duration
      })

      print(f"✅ Частина {part['number']} створена: {part_output_path}")
      print(f"⏱️ Природна тривалість частини: {total_duration:.1f} секунд (без мовчання)")

    except Exception as e:
      print(f"❌ Помилка при створенні частини {part['number']}: {e}")
      import traceback
      traceback.print_exc()

  # Зберігаємо допоміжні файли
  if created_parts:
    # Зберігаємо оригінальне preview
    Image.open(horizontal_preview_path).convert("RGB").save(output_dir / "preview.jpg", "JPEG")

    # Зберігаємо опис
    (output_dir / "description.txt").write_text(description_txt, encoding="utf-8")

    # Створюємо інформаційний файл про частини
    parts_info = {
      'total_parts': len(created_parts),
      'story_title': video_title,
      'min_duration_setting': tiktok_generator.min_part_duration,
      'target_duration_setting': tiktok_generator.target_part_duration,
      'natural_duration_mode': True, # Позначаємо що використовується природна тривалість
      'parts': [
        {
          'number': part['number'],
          'filename': f"part{part['number']}.mp4",
          'duration': part['duration']
        }
        for part in created_parts
      ]
    }

    (output_dir / "parts_info.json").write_text(
      json.dumps(parts_info, indent=2, ensure_ascii=False),
      encoding="utf-8"
    )

    # Очищаємо оригінальні файли
    for f in [story_path, preview_path, description_path]:
      f.write_text("", encoding="utf-8")

    print(f"✅ TikTok історія завершена: {len(created_parts)} частин створено")
    print(f"📁 Результати збережено в: {output_dir}")

    # Сумарна тривалість
    total_duration = sum(part['duration'] for part in created_parts)
    print(f"⏱️ Загальна природна тривалість: {total_duration:.1f} секунд (без додавання мовчання)")

    # Перевіряємо які частини коротші за мінімум
    short_count = sum(1 for part in created_parts if part['duration'] < tiktok_generator.min_part_duration)
    if short_count > 0:
      print(f"📊 Статистика: {short_count} частин коротше {tiktok_generator.min_part_duration}с (це нормально для природної тривалості)")

    # Надсилаємо превью в Telegram (тільки для першої частини)
    try:
      print(f"📤 Надсилання превью в Telegram...")
      preview_jpg_path = output_dir / "preview.jpg"
      asyncio.run(send_video_preview(preview_txt, str(preview_jpg_path)))
      print(f"✅ Превью успішно надіслано в Telegram")
    except Exception as e:
      print(f"❌ Помилка при надсиланні превью: {e}")

    return created_parts
  else:
    print(f"❌ Жодної частини не створено для {story_path.name}")
    return []


def process_tiktok_videos(tiktok_stories_dir, tiktok_generator, horizontal_generator, default_template, template_mode, preview_generator, tiktok_tts, asr_model):
  """Обробляє TikTok відео БЕЗ ДОДАВАННЯ МОВЧАННЯ"""
  story_files = list(tiktok_stories_dir.glob("*.txt"))
  story_files = [f for f in story_files if not f.name.endswith("_preview.txt") and not f.name.endswith("_description.txt")]

  if not story_files:
    print(f"❌ TikTok історій не знайдено в {tiktok_stories_dir}")
    print("📝 Переконайтесь що файли мають формат:")
    print(" - story1.txt")
    print(" - story1_preview.txt")
    print(" - story1_description.txt")
    return 0

  print(f"📚 Знайдено {len(story_files)} TikTok історій для обробки")
  print(f"⚡ НОВИЙ РЕЖИМ: Природна тривалість без додавання мовчання!")
  print(f"⚙️ Налаштування: мінімум {tiktok_generator.min_part_duration}с (для об'єднання), ціль {tiktok_generator.target_part_duration}с")

  processed_count = 0
  for i, story in enumerate(story_files, 1):
    print(f"\n{'='*60}")
    print(f"📜 [{i}/{len(story_files)}] Обробка TikTok історії → {story.name}")
    print(f"⚡ Режим: Природна тривалість (без мовчання)")
    print(f"{'='*60}")

    try:
      # Вибираємо template для цього відео
      selected_template = select_template_for_video(template_mode, default_template, preview_generator)
      if selected_template is None:
        print("❌ Template не вибрано, пропускаємо відео")
        continue

      parts_created = process_tiktok_story(
        story, tiktok_generator, horizontal_generator,
        selected_template, tiktok_tts, asr_model
      )

      if parts_created:
        print(f"🎉 Історія {story.name} успішно оброблена!")
        print(f"📊 Створено частин: {len(parts_created)}")

        # Статистика тривалості
        durations = [part['duration'] for part in parts_created]
        avg_duration = sum(durations) / len(durations)
        min_duration = min(durations)
        max_duration = max(durations)

        print(f"⏱️ Статистика тривалості:")
        print(f" • Середня: {avg_duration:.1f}с")
        print(f" • Мінімальна: {min_duration:.1f}с")
        print(f" • Максимальна: {max_duration:.1f}с")

        processed_count += 1

        # Рух миші для запобігання сну комп'ютера
        time.sleep(2)
        mouse.move(random.randint(0, 1920), random.randint(0, 1080), duration=1)
        time.sleep(2)
      else:
        print(f"❌ Не вдалося створити жодної частини для {story.name}")

    except Exception as e:
      print(f"❌ Критична помилка з {story.name}: {e}")
      import traceback
      traceback.print_exc()

  return processed_count
vertical_video.py
#vertical_video.py
import subprocess
import random
import re
from pathlib import Path
from moviepy.editor import VideoFileClip
from pydub import AudioSegment

class VerticalVideoGenerator:
  def __init__(self, source_folder, temp_folder, preview_generator, shorts_resolution=(1080, 1920), target_duration=59):
    """
    Ініціалізація генератора вертикальних відео (Shorts)

    Args:
      source_folder: Папка з фоновими відео
      temp_folder: Папка для тимчасових файлів
      preview_generator: Об'єкт PreviewGenerator для створення превью
      shorts_resolution: Розмір Shorts відео (ширина, висота)
      target_duration: Цільова тривалість Shorts у секундах
    """
    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 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:
      # Приблизно розраховуємо тривалість речення (середня швидкість мовлення ~150 слів/хв)
      words_count = len(sentence.split())
      sentence_duration = words_count / 2.5 # приблизно 2.5 слова в секунду

      if current_duration + sentence_duration > target_duration - 5: # залишаємо 5 секунд на кінцівку
        break

      truncated_text += sentence + ". "
      current_duration += sentence_duration

    return truncated_text.strip()

  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_tmp = self.temp_folder / f"{story_id}_shorts_preview_bg_{len(preview_clips):02d}.mp4"

      # Створюємо вертикальний кліп з потрібною тривалістю
      subprocess.run([
        "ffmpeg", "-y", "-i", str(bg_video), "-ss", "0", "-t", str(use_duration),
        "-vf", f"scale='max({self.shorts_resolution[0]},iw*{self.shorts_resolution[1]}/ih)':'max({self.shorts_resolution[1]},ih*{self.shorts_resolution[0]}/iw)',crop={self.shorts_resolution[0]}:{self.shorts_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

    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}_shorts_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}_shorts_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]

    # Створюємо вертикальне 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}_shorts_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_segments_vertical(self, story_id, bg_iter, story_duration):
    """Створює вертикальні відео сегменти для основної частини історії"""
    used_clips = []
    remaining_story_duration = story_duration

    for idx, bg in enumerate(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

      temp_out = self.temp_folder / f"shorts_clip_{idx:02d}.mp4"

      base_filters = f"scale='max({self.shorts_resolution[0]},iw*{self.shorts_resolution[1]}/ih)':'max({self.shorts_resolution[1]},ih*{self.shorts_resolution[0]}/iw)',crop={self.shorts_resolution[0]}:{self.shorts_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)
      remaining_story_duration -= use_dur

    # Якщо все ще не достатньо відео для story, додаємо ще
    if remaining_story_duration > 0:
      bg_videos_extra = list(self.source_folder.glob("*.mp4"))
      random.shuffle(bg_videos_extra)

      for idx, bg in enumerate(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

        temp_out = self.temp_folder / f"shorts_clip_extra_{idx:02d}.mp4"

        base_filters = f"scale='max({self.shorts_resolution[0]},iw*{self.shorts_resolution[1]}/ih)':'max({self.shorts_resolution[1]},ih*{self.shorts_resolution[0]}/iw)',crop={self.shorts_resolution[0]}:{self.shorts_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)
        remaining_story_duration -= use_dur

    return used_clips

  def create_ending_segment(self, story_id, bg_iter, ending_duration):
    """Створює сегмент кінцівки з фоновим відео"""
    ending_bg = self.temp_folder / f"{story_id}_ending_bg.mp4"

    # Беремо наступне фонове відео для кінцівки
    try:
      ending_bg_source = next(bg_iter)
    except StopIteration:
      # Якщо відео закінчились, беремо перше знову
      bg_videos_reset = list(self.source_folder.glob("*.mp4"))
      random.shuffle(bg_videos_reset)
      ending_bg_source = bg_videos_reset[0]

    # Створюємо фонове відео для кінцівки з правильним масштабуванням
    subprocess.run([
      "ffmpeg", "-y", "-i", str(ending_bg_source), "-ss", "0", "-t", str(ending_duration),
      "-vf", f"scale='max({self.shorts_resolution[0]},iw*{self.shorts_resolution[1]}/ih)':'max({self.shorts_resolution[1]},ih*{self.shorts_resolution[0]}/iw)',crop={self.shorts_resolution[0]}:{self.shorts_resolution[1]},fps=30",
      "-an", "-c:v", "h264_nvenc", "-preset", "p4", "-b:v", "5M", str(ending_bg)
    ], check=True)

    return ending_bg

  def combine_shorts_segments(self, story_id, preview_video, story_clips, ending_clip):
    """Об'єднує всі сегменти Shorts відео"""
    used_clips = [preview_video] + story_clips + [ending_clip]

    concat_list = self.temp_folder / f"{story_id}_shorts_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}_shorts_combined.mp4"
    subprocess.run([
      "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", str(concat_list),
      "-c:v", "h264_nvenc", "-b:v", "5M", "-preset", "p4", str(combined_path)
    ], check=True)

    return combined_path

  def create_shorts_video(self, story_txt, preview_txt, story_id, horizontal_preview_path, coqui_tts):
    """
    Створює вертикальне відео для Shorts

    Args:
      story_txt: Текст історії
      preview_txt: Текст preview
      story_id: ID історії
      horizontal_preview_path: Шлях до горизонтального preview
      coqui_tts: Функція TTS для генерації аудіо

    Returns:
      Path: Шлях до створеного Shorts відео
    """
    print(f"📱 Створення Shorts відео...")

    # Генеруємо аудіо
    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)

    # Створюємо кінцівку англійською з паузою ПЕРЕД нею
    ending_text = "Full video on the channel. Subscribe for more stories"
    audio_ending = coqui_tts(ending_text)
    # Додаємо паузу ПЕРЕД кінцівкою (0.5 сек) та в кінці (1 сек)
    audio_ending = AudioSegment.silent(duration=500) + audio_ending + AudioSegment.silent(duration=1000)

    # Збираємо фінальне аудіо
    final_audio = audio_preview + AudioSegment.silent(duration=500) + audio_story + audio_ending
    audio_path = self.temp_folder / f"{story_id}_shorts_voice.wav"
    final_audio.export(audio_path, format="wav")

    # Створюємо preview сегменти
    preview_duration = audio_preview.duration_seconds
    story_duration = audio_story.duration_seconds
    ending_duration = audio_ending.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)

    # Створюємо кінцівку
    ending_clip = self.create_ending_segment(story_id, bg_iter, ending_duration)

    # Об'єднуємо всі сегменти
    combined_video = self.combine_shorts_segments(story_id, preview_video, story_clips, ending_clip)

    return combined_video, audio_path

  def finalize_shorts_video(self, combined_video, audio_path, output_path):
    """
    Фінальна збірка Shorts відео з аудіо

    Args:
      combined_video: Шлях до відео без аудіо
      audio_path: Шлях до аудіо файлу
      output_path: Шлях для збереження фінального відео
    """
    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)
horizontal_video.py
#horizontal_video.py
import subprocess
import random
from pathlib import Path
from moviepy.editor import VideoFileClip
from pydub import AudioSegment

class HorizontalVideoGenerator:
  def __init__(self, source_folder, temp_folder, preview_generator, resolution=(1920, 1080), fade_duration=1):
    """
    Ініціалізація генератора горизонтальних відео

    Args:
      source_folder: Папка з фоновими відео
      temp_folder: Папка для тимчасових файлів
      preview_generator: Об'єкт PreviewGenerator для створення превью
      resolution: Розмір відео (ширина, висота)
      fade_duration: Тривалість fade ефектів
    """
    self.source_folder = Path(source_folder)
    self.temp_folder = Path(temp_folder)
    self.preview_generator = preview_generator
    self.resolution = resolution
    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) # Максимум 30% від тривалості кліпу
    fade_out_duration = min(0.3, use_dur * 0.3)

    # Переконуємося, що fade_in + fade_out не перевищує тривалість кліпу
    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
    fade_out_start = max(0, use_dur - fade_out_duration)

    # Створюємо fade фільтри тільки якщо вони мають сенс
    fade_filters = []
    if fade_in_duration > 0.05: # Мінімум 0.05 секунди для fade-in
      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: # Мінімум 0.05 секунди для fade-out
      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(self, story_id, preview_duration):
    """
    Створює фонові відео сегменти для preview частини

    Args:
      story_id: ID історії для іменування файлів
      preview_duration: Тривалість preview в секундах

    Returns:
      Path: Шлях до об'єднаного preview відео
    """
    bg_videos = list(self.source_folder.glob("*.mp4"))
    random.shuffle(bg_videos)
    bg_iter = iter(bg_videos)

    # Створюємо достатньо фонових відео для покриття всієї preview тривалості
    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}_preview_bg_{len(preview_clips):02d}.mp4"

      # Створюємо кліп з потрібною тривалістю
      subprocess.run([
        "ffmpeg", "-y", "-i", str(bg_video), "-ss", "0", "-t", str(use_duration),
        "-vf", f"scale={self.resolution[0]}:{self.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}_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}_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(self, story_id, preview_combined, preview_img_path, preview_duration):
    """
    Накладає preview зображення на фонове відео

    Args:
      story_id: ID історії
      preview_combined: Шлях до об'єднаного фонового відео
      preview_img_path: Шлях до preview зображення
      preview_duration: Тривалість preview

    Returns:
      Path: Шлях до preview відео з накладеним зображенням
    """
    preview_video = self.temp_folder / f"{story_id}_preview_video.mp4"
    subprocess.run([
      "ffmpeg", "-y", "-i", str(preview_combined), "-i", str(preview_img_path),
      "-filter_complex",
      "[1:v]format=rgba,scale=iw*0.7:ih*0.7[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(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):
      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"clip_{idx:02d}.mp4"

      # Використовуємо безпечну функцію для fade фільтрів
      base_filters = f"scale={self.resolution[0]}:{self.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"clip_extra_{idx:02d}.mp4"

        # Використовуємо ту ж безпечну функцію для fade
        base_filters = f"scale={self.resolution[0]}:{self.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(self, story_id, preview_video, story_clips):
    """
    Об'єднує всі відео сегменти в одне відео

    Args:
      story_id: ID історії
      preview_video: Шлях до preview відео
      story_clips: Список шляхів до story кліпів

    Returns:
      Path: Шлях до об'єднаного відео
    """
    all_clips = [preview_video] + story_clips

    concat_list = self.temp_folder / f"{story_id}_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")

    combined_path = self.temp_folder / f"{story_id}_combined.mp4"
    subprocess.run([
      "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", str(concat_list),
      "-c:v", "h264_nvenc", "-b:v", "5M", "-preset", "p4", str(combined_path)
    ], check=True)

    return combined_path

  def create_horizontal_video(self, story_id, preview_txt, audio_preview, audio_story, selected_template):
    """
    Створює повне горизонтальне відео

    Args:
      story_id: ID історії
      preview_txt: Текст preview
      audio_preview: AudioSegment з preview аудіо
      audio_story: AudioSegment з story аудіо
      selected_template: Назва вибраного template

    Returns:
      Path: Шлях до створеного відео
    """
    print(f"🖼️ Створення preview зображення з template: {selected_template}...")
    preview_img_path = self.temp_folder / f"{story_id}_preview.png"
    self.preview_generator.draw_horizontal_preview(preview_txt, preview_img_path, selected_template)

    print(f"🎬 Створення preview відео...")
    preview_duration = audio_preview.duration_seconds

    # Створюємо фонові сегменти для preview
    preview_combined, bg_iter = self.create_preview_video_segments(story_id, preview_duration)

    # Накладаємо preview зображення
    preview_video = self.create_preview_with_overlay(story_id, preview_combined, preview_img_path, preview_duration)

    print(f"🎥 Створення основного відео...")
    # Створюємо story сегменти
    final_audio = audio_preview + AudioSegment.silent(duration=500) + audio_story
    story_clips = self.create_story_video_segments(story_id, bg_iter, final_audio.duration_seconds, preview_duration)

    # Об'єднуємо всі сегменти
    combined_video = self.combine_video_segments(story_id, preview_video, story_clips)

    return combined_video, preview_img_path

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