Files

229 lines
7.2 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Spatial fast mode for the MLX Wan runtime (RIFE's spatial twin).
RIFE ``--fast`` cuts *frames* (temporal). This module cuts *pixels*
(spatial): denoise at ``target // scale``, decode at that size, then
resample the decoded frames up to the target. No second denoise pass —
that is ``--refine`` (quality). The two compose:
* ``--fast-spatial`` alone → speed (≈ scale² fewer tokens)
* ``--refine`` alone → quality two-pass (H3 / LTX-2)
* ``--fast`` + ``--refine`` → fewer frames at base res, full-res refine
* ``--fast`` + ``--fast-spatial`` → fewer frames *and* fewer pixels
The upsample runs in **pixel** space, after the VAE decode. It used to run
in latent space (bilinear over the latent H/W plane, sharing the refine
hand-off primitive) and that is what made spatial fast mode incoherent: an
interpolated Wan latent is off the decoder's manifold, so decode returned
the right silhouette under a smeared veil. ``--refine`` can get away with
the latent-space upsample because a second DMD pass re-denoises the result;
spatial fast mode hands the latent straight to the decoder, so it cannot.
See :mod:`fastvideo.mlx_runtime.frame_upsample` for the full rationale.
MetalFX is intentionally not used: it needs game-engine motion vectors
and depth that diffusion output lacks.
"""
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import dataclass
import numpy as np
from fastvideo.logger import init_logger
from fastvideo.mlx_runtime.frame_upsample import (
DEFAULT_PIXEL_UPSAMPLE_MODE,
PIXEL_UPSAMPLE_MODES,
upsample_frames,
)
from fastvideo.mlx_runtime.refine import RefinePlan, plan_refine_resolutions
logger = init_logger(__name__)
# Resampling from a smaller decode loses high-frequency detail the same way
# RIFE's flow warp does, so spatial fast mode borrows ``--fast``'s remedy: a
# light unsharp pass. 0.4 recovers perceived crispness on Wan2.1 output at 2x
# without the halos that show up by ~0.8.
DEFAULT_FAST_SPATIAL_SHARPEN = 0.4
@dataclass(frozen=True)
class FastSpatialPlan:
"""Resolved geometry for a spatial-fast (upsample-only) run."""
plan: RefinePlan
upsample_mode: str
sharpen: float = DEFAULT_FAST_SPATIAL_SHARPEN
@property
def enabled(self) -> bool:
"""
Determine whether spatial scaling is enabled.
Returns:
`true` if the spatial scale is greater than one, `false` otherwise.
"""
return self.plan.spatial_scale > 1
@property
def scale(self) -> int:
"""Provides the configured spatial scaling factor.
Returns:
int: The spatial scaling factor.
"""
return self.plan.spatial_scale
@property
def target_height(self) -> int:
"""
Return the target output height for the spatial plan.
Returns:
int: Target output height in pixels.
"""
return self.plan.target_height
@property
def target_width(self) -> int:
"""Return the target image width in pixels.
Returns:
int: The target image width.
"""
return self.plan.target_width
@property
def stage1_height(self) -> int:
"""
Provide the stage-one latent height used for reduced-resolution processing.
Returns:
int: The stage-one latent height.
"""
return self.plan.stage1_height
@property
def stage1_width(self) -> int:
"""Get the stage-one latent width.
Returns:
int: The stage-one latent width.
"""
return self.plan.stage1_width
def plan_fast_spatial(
*,
height: int,
width: int,
num_frames: int,
spatial_scale: int = 2,
vae_spatial_compression: int = 8,
vae_temporal_compression: int = 4,
patch_size: tuple[int, int, int] = (1, 2, 2),
upsample_mode: str = DEFAULT_PIXEL_UPSAMPLE_MODE,
sharpen: float = DEFAULT_FAST_SPATIAL_SHARPEN,
enabled: bool = True,
) -> FastSpatialPlan:
"""
Build a plan for reduced-resolution denoising followed by pixel-space upsampling.
Parameters:
upsample_mode (str): Pixel interpolation kernel, one of
:data:`~fastvideo.mlx_runtime.frame_upsample.PIXEL_UPSAMPLE_MODES`.
sharpen (float): Unsharp strength applied after the resize.
Returns:
FastSpatialPlan: The validated spatial-fast processing plan.
Raises:
ValueError: If the upsample mode is unsupported or ``sharpen`` is negative.
"""
if upsample_mode not in PIXEL_UPSAMPLE_MODES:
raise ValueError(f"Unsupported upsample mode: {upsample_mode!r} "
f"(expected one of {', '.join(PIXEL_UPSAMPLE_MODES)})")
if sharpen < 0.0:
raise ValueError(f"sharpen must be >= 0, got {sharpen}")
plan = plan_refine_resolutions(
height=height,
width=width,
num_frames=num_frames,
spatial_scale=spatial_scale,
vae_spatial_compression=vae_spatial_compression,
vae_temporal_compression=vae_temporal_compression,
patch_size=patch_size,
enabled=enabled,
mode_label="fast-spatial",
)
if plan.spatial_scale > 1:
logger.info(
"[MLX fast-spatial] denoise+decode %dx%d → upsample %dx to %dx%d (%s, sharpen=%.2f)",
plan.stage1_width,
plan.stage1_height,
plan.spatial_scale,
plan.target_width,
plan.target_height,
upsample_mode,
sharpen,
)
return FastSpatialPlan(plan=plan, upsample_mode=upsample_mode, sharpen=sharpen)
def apply_fast_spatial_upsample(
frames: Iterable[np.ndarray],
spatial: FastSpatialPlan,
) -> list[np.ndarray]:
"""Resample decoded stage-1 frames up to the target resolution.
This runs on decoded RGB frames, *not* on latents: see the module
docstring for why the latent-space version produced a blurred veil.
Parameters:
frames (Iterable[np.ndarray]): Decoded HxWx3 uint8 RGB frames, produced
by decoding at the stage-one resolution.
spatial (FastSpatialPlan): Plan defining the target size, interpolation
kernel, and unsharp strength.
Returns:
list[np.ndarray]: Frames at the target resolution. When spatial scaling
is disabled the frames are returned unchanged, as a list.
"""
if not spatial.enabled:
return list(frames)
return upsample_frames(
frames,
width=spatial.target_width,
height=spatial.target_height,
mode=spatial.upsample_mode,
sharpen=spatial.sharpen,
)
def resolve_spatial_mode(
*,
refine: bool,
fast_spatial: bool,
) -> str:
"""Select the active spatial processing mode, with refinement taking precedence.
Returns:
str: ``"refine"`` when refinement is enabled, ``"fast_spatial"`` when
spatial-fast processing is enabled, or ``"off"`` otherwise.
"""
if refine:
return "refine"
if fast_spatial:
return "fast_spatial"
return "off"
__all__ = [
"DEFAULT_FAST_SPATIAL_SHARPEN",
"FastSpatialPlan",
"apply_fast_spatial_upsample",
"plan_fast_spatial",
"resolve_spatial_mode",
]