# test_ffmpeg.py - python file
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 select_background_folder(backgrounds_dir):
"""Дозволяє користувачу вибрати папку з фоновими відео"""
print("\n" + "="*60)
print("???? ВИБІР ФОНОВИХ ВІДЕО")
print("="*60)
# Знаходимо всі папки в директорії backgrounds
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"
# 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", "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 process_youtube_story(story_path, horizontal_generator, vertical_generator, selected_template, coqui_tts, asr_model, temp_folder, final_dir):
"""Обробляє 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
audio_path = temp_folder / f"{story_id}_voice.wav"
final_audio.export(audio_path, format="wav")
# Створюємо горизонтальне відео
combined_video, preview_img_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 not chunks:
print("⚠️ Субтитри не знайдено")
final_video = combined_video
else:
final_video = apply_subtitles_ass(combined_video, chunks, story_id, temp_folder=temp_folder)
# Фінальна збірка горизонтального відео
final_path = output_dir / f"{video_title}.mp4"
horizontal_generator.finalize_video(final_video, audio_path, final_path)
# === ВЕРТИКАЛЬНЕ ВІДЕО (SHORTS) ===
try:
print(f"???? Створення Shorts відео...")
combined_shorts_video, shorts_audio_path = vertical_generator.create_shorts_video(
story_txt, preview_txt, story_id, preview_img_path, coqui_tts
)
# Розпізнавання мовлення для Shorts
print(f"???? Розпізнавання мовлення для Shorts...")
segments_shorts, _ = asr_model.transcribe(str(shorts_audio_path), word_timestamps=True)
# Створюємо субтитри тільки для story та ending частин
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)
else:
print("⚠️ Субтитри не знайдено для Shorts")
final_shorts_video = combined_shorts_video
# Фінальна збірка Shorts відео
shorts_path = output_dir / f"short_{video_title}.mp4"
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}")
# Зберігаємо файли
Image.open(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} → {final_path}")
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"
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 відео (горизонтальне + shorts)")
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 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()
# Налаштування 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 функцію з правильною temp папкою
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
)
# --- Обробка відео
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)
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)
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)
# Потім TikTok
tiktok_processed = process_tiktok_videos(TIKTOK_STORIES_DIR, generators['tiktok'], generators.get('horizontal'), DEFAULT_TEMPLATE, TEMPLATE_MODE, preview_generator, tiktok_tts, ASR_MODEL)
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("???? Генератор відео завершив роботу успішно!")
def process_youtube_videos(stories_dir, generators, default_template, template_mode, preview_generator, coqui_tts, asr_model, temp_folder, final_dir):
"""Обробляє 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['horizontal'], generators['vertical'],
selected_template, coqui_tts, asr_model, temp_folder, final_dir
)
processed_count += 1
except Exception as e:
print(f"❌ Помилка з {story.name}: {e}")
return processed_count
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 - python file
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