Files
wiki/tasks/karaoke/karaoke.py
2026-04-30 02:10:01 +03:00

172 lines
6.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()