172 lines
6.7 KiB
Python
172 lines
6.7 KiB
Python
#!/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()
|