82 lines
2.4 KiB
Python
82 lines
2.4 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import numpy as np
|
|
from moviepy import AudioFileClip, VideoClip
|
|
from PIL import Image
|
|
|
|
from engine.config import AppConfig
|
|
|
|
from .base import BaseVideoGen
|
|
|
|
|
|
class MoviePyVideoGen(BaseVideoGen):
|
|
def __init__(self, cfg: AppConfig):
|
|
self.cfg = cfg
|
|
|
|
def generate(self, image_path: str, prompt: dict, output_path: str | Path) -> str:
|
|
output_path = Path(output_path)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Required prompt fields for shot rendering.
|
|
duration_s = float(prompt.get("duration_s", 3))
|
|
fps = int(prompt.get("fps", self.cfg.get("video.mock_fps", 24)))
|
|
audio_path = prompt.get("audio_path")
|
|
|
|
# Clip resolution.
|
|
size = prompt.get("size")
|
|
if isinstance(size, (list, tuple)) and len(size) == 2:
|
|
w, h = int(size[0]), int(size[1])
|
|
else:
|
|
mock_size = self.cfg.get("video.mock_size", [1024, 576])
|
|
w, h = int(mock_size[0]), int(mock_size[1])
|
|
|
|
base_img = Image.open(image_path).convert("RGB")
|
|
|
|
def make_frame(t: float):
|
|
progress = float(t) / max(duration_s, 1e-6)
|
|
progress = max(0.0, min(1.0, progress))
|
|
scale = 1.0 + 0.03 * progress
|
|
new_w = max(w, int(w * scale))
|
|
new_h = max(h, int(h * scale))
|
|
frame = base_img.resize((new_w, new_h), Image.LANCZOS)
|
|
left = (new_w - w) // 2
|
|
top = (new_h - h) // 2
|
|
frame = frame.crop((left, top, left + w, top + h))
|
|
return np.array(frame)
|
|
|
|
video = VideoClip(make_frame, duration=duration_s, has_constant_size=True)
|
|
|
|
# Optional audio.
|
|
if audio_path and os.path.exists(str(audio_path)):
|
|
a = AudioFileClip(str(audio_path))
|
|
video = video.with_audio(a)
|
|
else:
|
|
a = None
|
|
|
|
try:
|
|
video.write_videofile(
|
|
str(output_path),
|
|
fps=fps,
|
|
codec="libx264",
|
|
audio_codec="aac",
|
|
preset="veryfast",
|
|
threads=2,
|
|
)
|
|
finally:
|
|
try:
|
|
video.close()
|
|
except Exception:
|
|
pass
|
|
if a is not None:
|
|
try:
|
|
a.close()
|
|
except Exception:
|
|
pass
|
|
|
|
return str(output_path)
|
|
|