138 lines
5.4 KiB
Python
138 lines
5.4 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""Small MLX RIFE wrapper for frame interpolation experiments.
|
|
|
|
The backend is the Apple-Silicon-native ``rife-mlx`` package, using the
|
|
``mlx-community/RIFE-4.25`` weights. Frames are HWC RGB ``uint8`` arrays.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterable
|
|
from functools import lru_cache
|
|
|
|
import numpy as np
|
|
from huggingface_hub.utils import LocalEntryNotFoundError
|
|
|
|
|
|
class RIFEBackendError(RuntimeError):
|
|
"""Raised when the MLX RIFE backend cannot be loaded or run."""
|
|
|
|
|
|
class RIFEWeightsUnavailableError(RIFEBackendError):
|
|
"""Raised when uncached RIFE weights cannot be downloaded."""
|
|
|
|
|
|
def aligned_keyframe_count(target_frames: int, factor: int, temporal_compression: int = 4) -> int:
|
|
"""Return the smallest VAE-aligned keyframe count that RIFE can expand to the target."""
|
|
if target_frames < 1:
|
|
raise ValueError(f"target_frames must be >= 1, got {target_frames}")
|
|
if factor < 1:
|
|
raise ValueError(f"factor must be >= 1, got {factor}")
|
|
if temporal_compression < 1:
|
|
raise ValueError(f"temporal_compression must be >= 1, got {temporal_compression}")
|
|
required_intervals = (target_frames - 1 + factor - 1) // factor
|
|
aligned_intervals = ((required_intervals + temporal_compression - 1) // temporal_compression * temporal_compression)
|
|
return aligned_intervals + 1
|
|
|
|
|
|
def _require_hwc_rgb(frame: np.ndarray, index: int) -> np.ndarray:
|
|
array = np.asarray(frame)
|
|
if array.ndim != 3 or array.shape[2] != 3:
|
|
raise ValueError(f"frame {index} must have shape HxWx3, got {array.shape}")
|
|
if array.dtype != np.uint8:
|
|
array = np.clip(array, 0, 255).astype(np.uint8)
|
|
return np.ascontiguousarray(array)
|
|
|
|
|
|
@lru_cache(maxsize=2)
|
|
def load_model(version: str = "4.25", weights_dir: str | None = None):
|
|
"""Load the MLX-native RIFE model.
|
|
|
|
``weights_dir`` is passed through to ``build_model`` in the vendored ``rife_mlx``.
|
|
When it is ``None``, the package downloads/uses the Hugging Face
|
|
``mlx-community/RIFE-4.25`` snapshot.
|
|
"""
|
|
try:
|
|
from fastvideo.third_party.rife_mlx.utils.weights import build_model
|
|
except ImportError:
|
|
# Fall back to a separately installed upstream package, for anyone who
|
|
# already has one in the environment.
|
|
try:
|
|
from rife_mlx.utils.weights import build_model
|
|
except ImportError as exc:
|
|
raise RIFEBackendError("MLX RIFE backend is unavailable. It ships vendored under "
|
|
"fastvideo/third_party/rife_mlx, so this usually means MLX "
|
|
"itself is missing: install with `uv pip install -e '.[mlx]'`.") from exc
|
|
|
|
try:
|
|
return build_model(version, weights_dir=weights_dir)
|
|
except LocalEntryNotFoundError as exc:
|
|
raise RIFEWeightsUnavailableError(f"MLX RIFE {version} weights are unavailable: {exc}") from exc
|
|
except Exception as exc: # noqa: BLE001 - preserve exact backend failure.
|
|
raise RIFEBackendError(f"Failed to load MLX RIFE {version}: {exc}") from exc
|
|
|
|
|
|
def interpolate_pair(
|
|
frame_a: np.ndarray,
|
|
frame_b: np.ndarray,
|
|
timestep: float = 0.5,
|
|
*,
|
|
model=None,
|
|
scale: float = 1.0,
|
|
) -> np.ndarray:
|
|
"""Interpolate one RGB frame between two input RGB frames."""
|
|
if not 0.0 < timestep < 1.0:
|
|
raise ValueError(f"timestep must be inside (0, 1), got {timestep}")
|
|
img0 = _require_hwc_rgb(frame_a, 0)
|
|
img1 = _require_hwc_rgb(frame_b, 1)
|
|
if img0.shape != img1.shape:
|
|
raise ValueError(f"frame shapes must match, got {img0.shape} and {img1.shape}")
|
|
|
|
if model is None:
|
|
model = load_model()
|
|
try:
|
|
try:
|
|
from fastvideo.third_party.rife_mlx.pipeline_mlx import interpolate_pair as _interpolate_pair
|
|
except ImportError:
|
|
from rife_mlx.pipeline_mlx import interpolate_pair as _interpolate_pair
|
|
|
|
return _interpolate_pair(model, img0, img1, timestep=timestep, scale=scale)
|
|
except Exception as exc: # noqa: BLE001 - preserve exact backend failure.
|
|
raise RIFEBackendError(f"MLX RIFE interpolation failed at timestep={timestep}: {exc}") from exc
|
|
|
|
|
|
def interpolate(
|
|
frames: list[np.ndarray] | Iterable[np.ndarray],
|
|
factor: int = 2,
|
|
*,
|
|
model=None,
|
|
scale: float = 1.0,
|
|
) -> list[np.ndarray]:
|
|
"""Return an Nx interpolated frame list.
|
|
|
|
For ``len(frames)=41`` and ``factor=2``, the output length is 81:
|
|
``(41 - 1) * 2 + 1``. Original keyframes are preserved in order and RIFE
|
|
fills ``factor - 1`` intermediate timesteps between each adjacent pair.
|
|
"""
|
|
frame_list = [_require_hwc_rgb(frame, idx) for idx, frame in enumerate(frames)]
|
|
if factor < 1:
|
|
raise ValueError(f"factor must be >= 1, got {factor}")
|
|
if len(frame_list) < 2 or factor == 1:
|
|
return [frame.copy() for frame in frame_list]
|
|
|
|
first_shape = frame_list[0].shape
|
|
for idx, frame in enumerate(frame_list[1:], start=1):
|
|
if frame.shape != first_shape:
|
|
raise ValueError(f"all frames must have the same shape; frame 0={first_shape}, frame {idx}={frame.shape}")
|
|
|
|
if model is None:
|
|
model = load_model()
|
|
|
|
out: list[np.ndarray] = []
|
|
for left, right in zip(frame_list[:-1], frame_list[1:], strict=True):
|
|
out.append(left)
|
|
for step in range(1, factor):
|
|
out.append(interpolate_pair(left, right, step / factor, model=model, scale=scale))
|
|
out.append(frame_list[-1])
|
|
return out
|