# test_ffmpeg.py (UPDATED VERSION WITH FONT SELECTION) 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 from full_vertical_video import FullVerticalVideoGenerator 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_available_fonts(fonts_dir): """Сканує папку fonts і повертає словник з доступними шрифтами (TTF та OTF)""" fonts = {} # Словник для маппінгу файлів шрифтів до їх системних назв # Ключ: ім'я файлу, значення: (назва для ASS, опис для користувача) font_mapping = { # TTF файли "NotoSans-ExtraBold.ttf": ("Noto Sans ExtraBold", "Noto Sans ExtraBold (жирний, читабельний)"), "KOMIKAX_.ttf": ("Komika Axis", "Komika Axis (комічний стиль)"), "Arial.ttf": ("Arial", "Arial (стандартний)"), "arial.ttf": ("Arial", "Arial (стандартний)"), "calibri.ttf": ("Calibri", "Calibri (сучасний)"), "Calibri.ttf": ("Calibri", "Calibri (сучасний)"), "times.ttf": ("Times New Roman", "Times New Roman (класичний)"), "TimesNewRoman.ttf": ("Times New Roman", "Times New Roman (класичний)"), "OpenSans-Bold.ttf": ("Open Sans Bold", "Open Sans Bold (веб-шрифт)"), "Roboto-Bold.ttf": ("Roboto Bold", "Roboto Bold (Google шрифт)"), "Montserrat-Bold.ttf": ("Montserrat Bold", "Montserrat Bold (сучасний)"), "Poppins-Bold.ttf": ("Poppins Bold", "Poppins Bold (трендовий)"), "Inter-Bold.ttf": ("Inter Bold", "Inter Bold (UI шрифт)"), # OTF файли "NotoSans-ExtraBold.otf": ("Noto Sans ExtraBold", "Noto Sans ExtraBold (жирний, OTF)"), "KOMIKAX_.otf": ("Komika Axis", "Komika Axis (комічний стиль, OTF)"), "Arial.otf": ("Arial", "Arial (стандартний, OTF)"), "arial.otf": ("Arial", "Arial (стандартний, OTF)"), "calibri.otf": ("Calibri", "Calibri (сучасний, OTF)"), "Calibri.otf": ("Calibri", "Calibri (сучасний, OTF)"), "OpenSans-Bold.otf": ("Open Sans Bold", "Open Sans Bold (веб-шрифт, OTF)"), "Roboto-Bold.otf": ("Roboto Bold", "Roboto Bold (Google шрифт, OTF)"), "Montserrat-Bold.otf": ("Montserrat Bold", "Montserrat Bold (сучасний, OTF)"), "Poppins-Bold.otf": ("Poppins Bold", "Poppins Bold (трендовий, OTF)"), "Inter-Bold.otf": ("Inter Bold", "Inter Bold (UI шрифт, OTF)"), "Proxima-Nova-Bold.otf": ("Proxima Nova Bold", "Proxima Nova Bold (преміум, OTF)"), "Futura-Bold.otf": ("Futura Bold", "Futura Bold (геометричний, OTF)"), "Helvetica-Bold.otf": ("Helvetica Bold", "Helvetica Bold (класичний, OTF)"), "Gotham-Bold.otf": ("Gotham Bold", "Gotham Bold (сучасний, OTF)"), "Avenir-Heavy.otf": ("Avenir Heavy", "Avenir Heavy (елегантний, OTF)"), # Додайте більше шрифтів за потребою } if not fonts_dir.exists(): print(f"⚠️ Папка шрифтів не існує: {fonts_dir}") return fonts # Скануємо як TTF, так і OTF файли font_extensions = ["*.ttf", "*.otf", "*.TTF", "*.OTF"] for pattern in font_extensions: for font_file in fonts_dir.glob(pattern): filename = font_file.name if filename in font_mapping: ass_name, description = font_mapping[filename] fonts[filename] = { 'path': font_file, 'ass_name': ass_name, 'description': description, 'type': font_file.suffix.upper()[1:] # TTF або OTF } else: # Для невідомих шрифтів використовуємо ім'я файлу без розширення font_name = font_file.stem.replace("_", " ").replace("-", " ") font_type = font_file.suffix.upper()[1:] # TTF або OTF fonts[filename] = { 'path': font_file, 'ass_name': font_name, 'description': f"{font_name} (автовизначення, {font_type})", 'type': font_type } return fonts def select_fonts(fonts_dir): """Дозволяє користувачу вибрати шрифти для preview та субтитрів""" print("\n" + "="*60) print("???? ВИБІР ШРИФТІВ") print("="*60) available_fonts = get_available_fonts(fonts_dir) if not available_fonts: print("❌ Шрифти не знайдено в папці fonts!") print("Перевірте, що у папці є файли .ttf або .otf") return { 'preview': { 'path': fonts_dir / "NotoSans-ExtraBold.ttf", 'name': "Noto Sans ExtraBold" }, 'subtitles': { 'path': fonts_dir / "KOMIKAX_.ttf", 'name': "Komika Axis" } } print("Доступні шрифти:") font_list = list(available_fonts.items()) for i, (filename, info) in enumerate(font_list, 1): type_icon = "????" if info['type'] == "TTF" else "????" if info['type'] == "OTF" else "????" print(f"{i}. {type_icon} {info['description']}") print("="*60) # Вибір шрифту для preview print("????️ Виберіть шрифт для PREVIEW (заголовків на превью):") while True: try: choice = input(f"Ваш вибір (1-{len(font_list)}): ").strip() choice_idx = int(choice) - 1 if 0 <= choice_idx < len(font_list): preview_font = font_list[choice_idx] break print(f"❌ Введіть число від 1 до {len(font_list)}") except ValueError: print("❌ Введіть дійсне число!") print(f"✅ Шрифт для preview: {preview_font[1]['description']}") # Вибір шрифту для субтитрів print("\n???? Виберіть шрифт для СУБТИТРІВ:") print("(Можете вибрати той же шрифт або інший)") while True: try: choice = input(f"Ваш вибір (1-{len(font_list)}): ").strip() choice_idx = int(choice) - 1 if 0 <= choice_idx < len(font_list): subtitles_font = font_list[choice_idx] break print(f"❌ Введіть число від 1 до {len(font_list)}") except ValueError: print("❌ Введіть дійсне число!") print(f"✅ Шрифт для субтитрів: {subtitles_font[1]['description']}") return { 'preview': { 'path': preview_font[1]['path'], 'name': preview_font[1]['ass_name'] }, 'subtitles': { 'path': subtitles_font[1]['path'], 'name': subtitles_font[1]['ass_name'] } } 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}, 'youtube_full_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['youtube_full_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 вертикальне (Shorts) print("\n???? YouTube Shorts відео:") enable = input("Прискорювати YouTube Shorts? (y/n): ").lower() == 'y' if enable: while True: try: target = float(input("Цільова тривалість (секунди, макс 60): ")) if 10 <= target <= 60: break print("❌ Тривалість має бути від 10 до 60 секунд") except ValueError: print("❌ Введіть дійсне число!") speed_config['youtube_vertical'] = {'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_full_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 Shorts', 'youtube_full_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 (60 if 'vertical' in video_type and video_type != 'youtube_full_vertical' 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}") 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_name="Komika Axis", font_size=48, vertical=False): """Створює ASS файл субтитрів з вказаним шрифтом""" 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, font_name="Komika Axis"): """Додає субтитри використовуючи ASS файл з вказаним шрифтом""" if not chunks: return video_path print(f"???? Створення ASS субтитрів для {len(chunks)} фрагментів...") print(f"???? Використовується шрифт: {font_name}") suffix = "_vertical" if vertical else "" ass_path = temp_folder / f"{story_id}_subtitles{suffix}.ass" create_ass_subtitles(chunks, ass_path, font_name=font_name, 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_video_type(): """Дозволяє користувачу вибрати тип відео для генерації""" print("\n" + "="*60) print("???? ВИБІР ТИПУ ВІДЕО") print("="*60) print("1. ???? YouTube відео (горизонтальне + shorts)") print("2. ???? TikTok відео (розділені на частини)") print("3. ???? Обидва типи") print("4. ???? YouTube з градієнтним фоном (вертикальне на горизонтальному фоні)") print("5. ???? Горизонтальне + градієнтне + shorts") print("6. ???? Градієнтне + shorts") print("7. ???? Всі типи YouTube відео") print("="*60) while True: choice = input("Ваш вибір (1-7): ").strip() if choice in ["1", "2", "3", "4", "5", "6", "7"]: return choice print("❌ Невірний вибір. Введіть число від 1 до 7") 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_story(story_path, horizontal_generator, vertical_generator, full_vertical_generator, selected_template, coqui_tts, asr_model, temp_folder, final_dir, speed_config, video_type, sub_font_name="Komika Axis"): """Обробляє 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}") # === ГЕНЕРАЦІЯ АУДІО === 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") horizontal_preview_path = None # === ГОРИЗОНТАЛЬНЕ ВІДЕО === if video_type in ["1", "3", "5", "7"] and horizontal_generator: # Включає горизонтальне print(f"???? Створення горизонтального відео...") horizontal_speed_settings = speed_config['youtube_horizontal'] if speed_config['mode'] == 'interactive': horizontal_speed_settings = get_interactive_speed_settings('youtube_horizontal', original_duration) combined_video, horizontal_preview_path = horizontal_generator.create_horizontal_video( story_id, preview_txt, audio_preview, audio_story, selected_template ) # Субтитри 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 chunks: final_video = apply_subtitles_ass(combined_video, chunks, story_id, temp_folder=temp_folder, font_name=sub_font_name) else: print("⚠️ Субтитри не знайдено") final_video = combined_video # Застосовуємо прискорення horizontal_output_path = output_dir / f"{video_title}.mp4" 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 = original_duration / horizontal_speed_settings['target_duration'] speed_factor = min(speed_factor, 4.0) horizontal_generator.finalize_video(final_video, audio_path, 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, audio_path, horizontal_output_path) print(f"✅ Горизонтальне відео створено: {horizontal_output_path}") # === ВЕРТИКАЛЬНЕ ВІДЕО (SHORTS) === if video_type in ["1", "3", "5", "6", "7"] and vertical_generator: # Включає shorts try: print(f"???? Створення Shorts відео...") vertical_speed_settings = speed_config['youtube_vertical'] combined_shorts_video, shorts_audio_path = vertical_generator.create_shorts_video( story_txt, preview_txt, story_id, horizontal_preview_path, coqui_tts ) if speed_config['mode'] == 'interactive': shorts_duration = AudioSegment.from_wav(shorts_audio_path).duration_seconds vertical_speed_settings = get_interactive_speed_settings('youtube_vertical', shorts_duration) # Розпізнавання мовлення для Shorts print(f"???? Розпізнавання мовлення для Shorts...") segments_shorts, _ = asr_model.transcribe(str(shorts_audio_path), word_timestamps=True) audio_preview_shorts = coqui_tts(preview_txt) story_start_time = audio_preview_shorts.duration_seconds + 0.5 story_words = [] for seg in segments_shorts: 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_shorts = chunk_words(story_words) if chunks_shorts: final_shorts_video = apply_subtitles_ass(combined_shorts_video, chunks_shorts, f"{story_id}_shorts", vertical=True, temp_folder=temp_folder, font_name=sub_font_name) else: print("⚠️ Субтитри не знайдено для Shorts") final_shorts_video = combined_shorts_video # Застосовуємо прискорення shorts_path = output_dir / f"short_{video_title}.mp4" 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(shorts_audio_path).duration_seconds / vertical_speed_settings['target_duration'] speed_factor = min(speed_factor, 4.0) vertical_generator.finalize_shorts_video(final_shorts_video, shorts_audio_path, temp_folder / "temp_shorts.mp4") apply_video_speedup( temp_folder / "temp_shorts.mp4", shorts_path, speed_factor, temp_folder ) else: vertical_generator.finalize_shorts_video(final_shorts_video, shorts_audio_path, shorts_path) print(f"???? Shorts відео створено: {shorts_path}") except Exception as e: print(f"❌ Помилка при створенні Shorts відео: {e}") # === ВІДЕО З ГРАДІЄНТНИМ ФОНОМ === if video_type in ["4", "5", "6", "7"] and full_vertical_generator: # Включає градієнтний фон try: print(f"???? Створення відео з градієнтним фоном...") full_vertical_speed_settings = speed_config['youtube_full_vertical'] if speed_config['mode'] == 'interactive': full_vertical_speed_settings = get_interactive_speed_settings('youtube_full_vertical', original_duration) # Створюємо горизонтальне preview якщо ще не створено if horizontal_preview_path is None: temp_preview_path = temp_folder / f"{story_id}_temp_preview.png" from preview_generator import PreviewGenerator preview_gen = PreviewGenerator( templates_dir=temp_folder.parent / "templates", font_path=temp_folder.parent / "fonts/NotoSans-ExtraBold.ttf", shorts_resolution=(1080, 1920) ) preview_gen.draw_horizontal_preview(preview_txt, temp_preview_path, selected_template) horizontal_preview_path = temp_preview_path combined_gradient_video = full_vertical_generator.create_full_vertical_video( story_id, preview_txt, audio_preview, audio_story, horizontal_preview_path ) # Додаємо субтитри (використовуємо ті ж chunks що й для горизонтального) if 'chunks' in locals(): final_gradient_video = apply_subtitles_ass(combined_gradient_video, chunks, f"{story_id}_gradient", vertical=True, temp_folder=temp_folder, font_name=sub_font_name) else: # Якщо горизонтального відео не було, створюємо субтитри знову 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 chunks: final_gradient_video = apply_subtitles_ass(combined_gradient_video, chunks, f"{story_id}_gradient", vertical=True, temp_folder=temp_folder, font_name=sub_font_name) else: print("⚠️ Субтитри не знайдено для відео з градієнтним фоном") final_gradient_video = combined_gradient_video # Застосовуємо прискорення gradient_path = output_dir / f"gradient_{video_title}.mp4" if full_vertical_speed_settings['enabled'] and full_vertical_speed_settings.get('speed', 1.0) > 1.0: print(f"⚡ Прискорення відео з градієнтним фоном...") speed_factor = full_vertical_speed_settings.get('speed') if full_vertical_speed_settings.get('target_duration'): speed_factor = original_duration / full_vertical_speed_settings['target_duration'] speed_factor = min(speed_factor, 4.0) full_vertical_generator.finalize_full_vertical_video(final_gradient_video, audio_path, temp_folder / "temp_gradient.mp4") apply_video_speedup( temp_folder / "temp_gradient.mp4", gradient_path, speed_factor, temp_folder ) else: full_vertical_generator.finalize_full_vertical_video(final_gradient_video, audio_path, gradient_path) print(f"???? Відео з градієнтним фоном створено: {gradient_path}") except Exception as e: print(f"❌ Помилка при створенні відео з градієнтним фоном: {e}") # Зберігаємо файли if horizontal_preview_path and horizontal_preview_path.exists(): Image.open(horizontal_preview_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} → відео створено успішно") time.sleep(2) mouse.move(random.randint(0, 1920), random.randint(0, 1080), duration=1) time.sleep(2) # Надсилаємо превью в Telegram 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 process_youtube_videos(stories_dir, generators, default_template, template_mode, preview_generator, coqui_tts, asr_model, temp_folder, final_dir, speed_config, video_type, sub_font_name="Komika Axis"): """Обробляє 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: # Вибираємо template для цього відео 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.get('horizontal'), generators.get('vertical'), generators.get('full_vertical'), selected_template, coqui_tts, asr_model, temp_folder, final_dir, speed_config, video_type, sub_font_name # Передаємо назву шрифту ) processed_count += 1 except Exception as e: print(f"❌ Помилка з {story.name}: {e}") import traceback traceback.print_exc() 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" FONTS_DIR = ROOT / "fonts" BEEP_SOUND_PATH = ROOT / "beep.wav" GRADIENTS_DIR = ROOT / "gradients" # --- Ініціалізація папок --- for folder in [BACKGROUNDS_DIR, STORIES_DIR, TIKTOK_STORIES_DIR, FINAL_DIR, FINAL_TIKTOK_DIR, TEMPLATES_DIR, FONTS_DIR, GRADIENTS_DIR]: folder.mkdir(parents=True, exist_ok=True) # === ВИБІР ШРИФТІВ === font_config = select_fonts(FONTS_DIR) FONT_PREVIEW = font_config['preview']['path'] SUB_FONT_NAME = font_config['subtitles']['name'] print(f"\n???? Налаштування шрифтів:") print(f" ????️ Preview: {FONT_PREVIEW.name}") print(f" ???? Субтитри: {SUB_FONT_NAME}") # --- Вибір фонових відео --- 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) VERTICAL_RESOLUTION = (1080, 1920) FADE_DURATION = 1 SHORTS_TARGET_DURATION = 59 # Очищуємо temp папки залежно від типу відео if video_type in ["1", "3", "4", "5", "6", "7"]: # 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 ) 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", "4", "5", "6", "7"]: # YouTube варіанти # Горизонтальний генератор (потрібен для preview) generators['horizontal'] = HorizontalVideoGenerator( source_folder=SOURCE_FOLDER, temp_folder=TEMP_FOLDER, preview_generator=preview_generator, resolution=RESOLUTION, fade_duration=FADE_DURATION ) # Вертикальний генератор (для shorts) if video_type in ["1", "3", "5", "6", "7"]: # Варіанти що включають shorts 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 ["4", "5", "6", "7"]: # Варіанти що включають градієнтний фон generators['full_vertical'] = FullVerticalVideoGenerator( source_folder=SOURCE_FOLDER, temp_folder=TEMP_FOLDER, preview_generator=preview_generator, vertical_resolution=VERTICAL_RESOLUTION, fade_duration=FADE_DURATION, gradients_folder=GRADIENTS_DIR ) 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=SHORTS_RESOLUTION, target_part_duration=tiktok_target_duration, min_part_duration=tiktok_min_duration ) # Показуємо налаштування print(f"\n⚡ НАЛАШТУВАННЯ:") video_type_names = { "1": "???? YouTube відео (горизонтальне + shorts)", "2": "???? TikTok відео", "3": "???? YouTube + TikTok відео", "4": "???? YouTube з градієнтним фоном", "5": "???? Горизонтальне + градієнтне + shorts", "6": "???? Градієнтне + shorts", "7": "???? Всі типи YouTube відео" } print(f"Режим: {video_type_names[video_type]}") print(f"???? Фонові відео: {SOURCE_FOLDER.name}") # --- Обробка відео --- if 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, "1", SUB_FONT_NAME ) # Потім 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}") else: # YouTube варіанти (1, 4, 5, 6, 7) mode_names = { "1": "???? YouTube відео (горизонтальне + shorts)", "4": "???? YouTube з градієнтним фоном", "5": "???? Горизонтальне + градієнтне + shorts", "6": "???? Градієнтне + shorts", "7": "???? Всі типи YouTube відео" } print(f"\n{mode_names[video_type]}") print(f"???? Використовуються фонові відео з: {SOURCE_FOLDER.name}") processed = process_youtube_videos( STORIES_DIR, generators, DEFAULT_TEMPLATE, TEMPLATE_MODE, preview_generator, coqui_tts, ASR_MODEL, TEMP_FOLDER, FINAL_DIR, speed_config, video_type, SUB_FONT_NAME ) print(f"\n???? Підсумок: оброблено {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()