153 lines
5.3 KiB
Python
153 lines
5.3 KiB
Python
"""
|
||
video_bg.py — Pexels API → скачать видео-клип для фона
|
||
|
||
Если Pexels не доступен → генерируем чёрный видео-файл.
|
||
"""
|
||
|
||
import os
|
||
import subprocess
|
||
import requests
|
||
from pathlib import Path
|
||
from dotenv import load_dotenv
|
||
|
||
load_dotenv(os.path.expanduser("~/.openclaw/.env"))
|
||
|
||
import shutil
|
||
FFMPEG = os.environ.get("FFMPEG_BIN") or shutil.which("ffmpeg") or os.path.expanduser("~/bin/ffmpeg")
|
||
FFPROBE = os.environ.get("FFPROBE_BIN") or shutil.which("ffprobe") or FFMPEG # fallback to ffmpeg
|
||
|
||
|
||
def search_pexels(query: str, per_page: int = 3) -> list[dict]:
|
||
"""Поиск видео на Pexels. Возвращает список видео-объектов."""
|
||
api_key = os.environ.get("PEXELS_API_KEY", "")
|
||
if not api_key:
|
||
print("[video_bg] PEXELS_API_KEY не задан, пропускаем поиск.")
|
||
return []
|
||
|
||
url = "https://api.pexels.com/videos/search"
|
||
params = {
|
||
"query": query,
|
||
"per_page": per_page,
|
||
"orientation": "landscape",
|
||
"size": "medium",
|
||
}
|
||
headers = {"Authorization": api_key}
|
||
|
||
try:
|
||
resp = requests.get(url, params=params, headers=headers, timeout=15)
|
||
resp.raise_for_status()
|
||
data = resp.json()
|
||
except Exception as e:
|
||
print(f"[video_bg] Ошибка Pexels API: {e}")
|
||
return []
|
||
|
||
return data.get("videos", [])
|
||
|
||
|
||
def pick_video(video_list: list[dict]) -> str | None:
|
||
"""Выбрать URL лучшего видео (предпочитаем 720p, берём любое доступное)."""
|
||
for v in video_list:
|
||
files = v.get("video_files", [])
|
||
# Сначала ищем 720p (1280x720)
|
||
for f in files:
|
||
if f.get("width") == 1280:
|
||
return f.get("link") or f.get("file")
|
||
# Потом любое HD ≥ 1280
|
||
for f in sorted(files, key=lambda x: x.get("width", 0)):
|
||
if f.get("width", 0) >= 1280:
|
||
return f.get("link") or f.get("file")
|
||
# Любое что есть
|
||
if files:
|
||
return files[0].get("link") or files[0].get("file")
|
||
return None
|
||
|
||
|
||
def download_url(url: str, dest: str) -> str:
|
||
"""Скачать файл по URL в dest."""
|
||
print(f"[video_bg] Скачиваем {url[:80]}…")
|
||
resp = requests.get(url, stream=True, timeout=60)
|
||
resp.raise_for_status()
|
||
with open(dest, "wb") as f:
|
||
for chunk in resp.iter_content(chunk_size=8192):
|
||
f.write(chunk)
|
||
return dest
|
||
|
||
|
||
def get_duration(path: str) -> float:
|
||
"""Длительность видео в секундах через ffmpeg -i."""
|
||
result = subprocess.run(
|
||
[FFMPEG, "-i", path],
|
||
capture_output=True, text=True
|
||
)
|
||
import re
|
||
m = re.search(r"Duration: (\d+):(\d+):([\d.]+)", result.stderr)
|
||
if m:
|
||
h, mn, s = m.groups()
|
||
return int(h) * 3600 + int(mn) * 60 + float(s)
|
||
raise RuntimeError(f"Не удалось получить длительность: {path}")
|
||
|
||
|
||
def create_black_video(duration: float, dest: str, width: int = 1280, height: int = 720):
|
||
"""Создать чёрное видео заданной длительности."""
|
||
print(f"[video_bg] Создаём чёрный фон {duration:.1f}s…")
|
||
cmd = [
|
||
FFMPEG, "-f", "lavfi", "-i", f"color=c=black:s={width}x{height}:r=30:d={duration}",
|
||
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-y", dest
|
||
]
|
||
subprocess.run(cmd, check=True, capture_output=True)
|
||
return dest
|
||
|
||
|
||
def loop_video(source: str, target_duration: float, dest: str):
|
||
"""Зациклить видео до нужной длительности."""
|
||
src_dur = get_duration(source)
|
||
if src_dur >= target_duration:
|
||
# Обрезать лишнее
|
||
cmd = [
|
||
FFMPEG, "-i", source, "-t", str(target_duration),
|
||
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-an", "-y", dest
|
||
]
|
||
else:
|
||
# Зациклить через stream_loop
|
||
loops = int(target_duration / src_dur) + 1
|
||
cmd = [
|
||
FFMPEG, "-stream_loop", str(loops - 1), "-i", source,
|
||
"-t", str(target_duration),
|
||
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-an", "-y", dest
|
||
]
|
||
subprocess.run(cmd, check=True, capture_output=True)
|
||
return dest
|
||
|
||
|
||
def get_bg_video(search_query: str, audio_duration: float,
|
||
output_dir: str) -> str:
|
||
"""
|
||
Основной entry point.
|
||
|
||
1. Ищет видео на Pexels по query
|
||
2. Скачивает и зацикливает до audio_duration
|
||
3. Если Pexels недоступен — создаёт чёрный фон
|
||
|
||
Возвращает путь к видео-файлу.
|
||
"""
|
||
os.makedirs(output_dir, exist_ok=True)
|
||
|
||
raw_path = os.path.join(output_dir, "raw_bg.mp4")
|
||
bg_path = os.path.join(output_dir, "background.mp4")
|
||
|
||
videos = search_pexels(search_query)
|
||
url = pick_video(videos)
|
||
|
||
if url:
|
||
try:
|
||
download_url(url, raw_path)
|
||
loop_video(raw_path, audio_duration, bg_path)
|
||
print(f"[video_bg] Видео-фон готов: {bg_path}")
|
||
return bg_path
|
||
except Exception as e:
|
||
print(f"[video_bg] Ошибка скачивания: {e}")
|
||
|
||
# Fallback: чёрный фон
|
||
create_black_video(audio_duration, bg_path)
|
||
return bg_path
|