#!/usr/bin/env python3 """ karaoke.py — Караоке-генератор v0.1 (MVP) Принимает аудиофайл (и опционально текст), генерирует видео с синхронизированным текстом. Пример: python karaoke.py --audio song.mp3 python karaoke.py --audio song.mp3 --text lyrics.lrc python karaoke.py --audio song.mp3 --text lyrics.txt --output my_video.mp4 """ import argparse import os import sys from pathlib import Path from dotenv import load_dotenv # Загружаем ~/.openclaw/.env load_dotenv(os.path.expanduser("~/.openclaw/.env")) # Путь к директории проекта PROJECT_DIR = Path(__file__).parent # Добавляем в path для импорта модулей sys.path.insert(0, str(PROJECT_DIR)) from transcribe import transcribe from nlp import analyze from video_bg import get_bg_video from render import render_with_bg, render from video_bg import FFMPEG def get_audio_duration(audio_path: str) -> float: """Длительность аудио через ffprobe.""" import subprocess FFPROBE = os.environ.get("FFPROBE_BIN", os.path.expanduser("~/bin/ffmpeg-7.0.2-amd64-static/ffprobe")) cmd = [ FFPROBE, "-v", "quiet", "-show_entries", "format=duration", "-of", "csv=p=0", audio_path ] out = subprocess.check_output(cmd, text=True).strip() return float(out) def main(): parser = argparse.ArgumentParser( description="Караоке-генератор: аудио → видео с текстом", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Примеры: python karaoke.py --audio song.mp3 python karaoke.py --audio song.mp3 --text lyrics.lrc python karaoke.py --audio song.mp3 --text lyrics.srt --output out.mp4 python karaoke.py --audio song.mp3 --whisper-model small """ ) parser.add_argument("--audio", required=True, help="Путь к аудиофайлу (mp3, wav, ogg, m4a)") parser.add_argument("--text", default=None, help="Путь к файлу с текстом (lrc, srt, txt)") parser.add_argument("--output", default="output.mp4", help="Путь к выходному видео") parser.add_argument("--whisper-model", default="base", choices=["tiny", "base", "small", "medium", "large"], help="Размер Whisper-модели (по умолчанию: base)") parser.add_argument("--model", default=None, choices=["tiny", "base", "small", "medium", "large"], help="Алиас для --whisper-model (удобнее для .txt)") parser.add_argument("--device", default="cpu", choices=["cpu", "cuda"], help="Устройство для Whisper (по умолчанию: cpu)") parser.add_argument("--language", default=None, help="Код языка (напр. ru, en) для Whisper (None = авто)") parser.add_argument("--no-api", action="store_true", help="Не использовать OpenAI API, только локальный faster-whisper") args = parser.parse_args() # --model алиас для --whisper-model model_size = args.model if args.model else args.whisper_model # Валидация audio_path = os.path.abspath(args.audio) if not os.path.isfile(audio_path): print(f"❌ Аудиофайл не найден: {audio_path}") sys.exit(1) output_path = os.path.abspath(args.output) output_dir = os.path.dirname(output_path) if not output_dir: output_dir = "." tmp_dir = os.path.join(output_dir, ".karaoke_tmp") os.makedirs(tmp_dir, exist_ok=True) print("=" * 60) print("🎤 Караоке-генератор v0.1") print("=" * 60) # --- Шаг 1: Транскрипция --- print("\n📝 Шаг 1: Транскрипция…") if args.text: ext = Path(args.text).suffix.lower() print(f" Используем готовый текст: {args.text} ({ext})") segments = transcribe(audio_path, text_path=args.text, model_size=model_size, device=args.device, language=args.language, use_api=not args.no_api) else: segments = transcribe(audio_path, model_size=model_size, device=args.device, language=args.language, use_api=not args.no_api) if not segments: print("❌ Не удалось получить сегменты текста!") sys.exit(1) print(f" Получено {len(segments)} сегментов.") full_text = " ".join(s["text"] for s in segments) audio_duration = get_audio_duration(audio_path) print(f" Длительность аудио: {audio_duration:.1f}s") # --- Шаг 2: NLP-анализ --- print("\n🧠 Шаг 2: NLP-анализ текста…") nlp_result = analyze(full_text) mood = nlp_result.get("mood", "neutral") scenes = nlp_result.get("scenes", ["abstract"]) search_query = scenes[0] if scenes else "abstract" print(f" Настроение: {mood}") print(f" Сцена для фона: {search_query}") # --- Шаг 3: Видео-фон --- print(f"\n🎬 Шаг 3: Подбор видео-фона…") bg_video = get_bg_video(search_query, audio_duration, tmp_dir) print(f" Видео-фон: {bg_video}") # --- Шаг 4: Рендер --- print(f"\n🎞️ Шаг 4: Рендер видео…") is_black_bg = (bg_video.split("/")[-1] == "background.mp4" and get_bg_video.__doc__) # Проверяем, чёрный фон или реальный видео # Проще: всегда пытаемся render_with_bg — он сам упадёт на fallback try: render_with_bg(segments, audio_path, bg_video, output_path) except Exception as e: print(f"[karaoke] Render с фоном не удался: {e}") print("[karaoke] Пробуем fallback…") render(segments, audio_path, bg_video, output_path) # Финал print("\n" + "=" * 60) if os.path.isfile(output_path): size_mb = os.path.getsize(output_path) / (1024 * 1024) print(f"✅ Готово: {output_path} ({size_mb:.1f} MB)") else: print("❌ Ошибка: выходной файл не создан!") sys.exit(1) print("=" * 60) # Чистим tmp try: import shutil shutil.rmtree(tmp_dir) except Exception: pass if __name__ == "__main__": main()