chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Frame interpolation and upscaling support for SGLang diffusion pipelines."""
|
||||
|
||||
from sglang.multimodal_gen.runtime.postprocess.realesrgan_upscaler import (
|
||||
ImageUpscaler,
|
||||
batch_upscale_frames,
|
||||
upscale_frames,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.postprocess.rife_interpolator import (
|
||||
FrameInterpolator,
|
||||
interpolate_video_frames,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"FrameInterpolator",
|
||||
"interpolate_video_frames",
|
||||
"ImageUpscaler",
|
||||
"batch_upscale_frames",
|
||||
"upscale_frames",
|
||||
]
|
||||
@@ -0,0 +1,822 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
Real-ESRGAN upscaling for SGLang diffusion pipelines.
|
||||
|
||||
Real-ESRGAN model code is vendored and adapted from:
|
||||
- https://github.com/xinntao/Real-ESRGAN (BSD-3-Clause License)
|
||||
Copyright (c) 2021 xinntao
|
||||
|
||||
The ImageUpscaler wrapper and integration code are original work.
|
||||
"""
|
||||
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
from hashlib import sha256
|
||||
from typing import Optional
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
# Default HuggingFace repo and filename for Real-ESRGAN weights
|
||||
_DEFAULT_REALESRGAN_HF_REPO = "ai-forever/Real-ESRGAN"
|
||||
_DEFAULT_REALESRGAN_FILENAME = "RealESRGAN_x4.pth"
|
||||
_DEFAULT_REALESRGAN_FILENAMES_BY_SCALE = {
|
||||
2: "RealESRGAN_x2.pth",
|
||||
4: "RealESRGAN_x4.pth",
|
||||
8: "RealESRGAN_x8.pth",
|
||||
}
|
||||
_LOW_MEMORY_TILED_UPSCALE_FREE_BYTES = 2 * 1024**3
|
||||
_REALESRGAN_TILE_SIZE = 256
|
||||
_REALESRGAN_TILE_PAD = 32
|
||||
|
||||
# Module-level cache: model_path -> UpscalerModel instance
|
||||
_MODEL_CACHE: dict[str, "UpscalerModel"] = {}
|
||||
_RESOLVED_MODEL_PATH_CACHE: dict[str, str] = {}
|
||||
|
||||
|
||||
def _default_model_path_for_scale(scale: int) -> str:
|
||||
filename = _DEFAULT_REALESRGAN_FILENAMES_BY_SCALE.get(
|
||||
int(scale),
|
||||
_DEFAULT_REALESRGAN_FILENAME,
|
||||
)
|
||||
return f"{_DEFAULT_REALESRGAN_HF_REPO}:{filename}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vendored Real-ESRGAN architecture code
|
||||
# (SRVGGNetCompact, ResidualDenseBlock, RRDB, RRDBNet)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SRVGGNetCompact(nn.Module):
|
||||
"""Compact VGG-style network for super resolution.
|
||||
|
||||
Corresponds to ``realesr-animevideov3`` and ``realesr-general-x4v3``.
|
||||
Reference: xinntao/Real-ESRGAN (BSD-3-Clause).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_in_ch: int = 3,
|
||||
num_out_ch: int = 3,
|
||||
num_feat: int = 64,
|
||||
num_conv: int = 16,
|
||||
upscale: int = 4,
|
||||
act_type: str = "prelu",
|
||||
):
|
||||
super().__init__()
|
||||
self.num_in_ch = num_in_ch
|
||||
self.num_out_ch = num_out_ch
|
||||
self.num_feat = num_feat
|
||||
self.num_conv = num_conv
|
||||
self.upscale = upscale
|
||||
self.act_type = act_type
|
||||
|
||||
self.body = nn.ModuleList()
|
||||
# first conv
|
||||
self.body.append(nn.Conv2d(num_in_ch, num_feat, 3, 1, 1))
|
||||
# first activation
|
||||
self.body.append(self._make_act(act_type, num_feat))
|
||||
# body convs + activations
|
||||
for _ in range(num_conv):
|
||||
self.body.append(nn.Conv2d(num_feat, num_feat, 3, 1, 1))
|
||||
self.body.append(self._make_act(act_type, num_feat))
|
||||
# last conv: maps to out_ch * upscale^2 for pixel shuffle
|
||||
self.body.append(nn.Conv2d(num_feat, num_out_ch * upscale * upscale, 3, 1, 1))
|
||||
self.upsampler = nn.PixelShuffle(upscale)
|
||||
|
||||
@staticmethod
|
||||
def _make_act(act_type: str, num_feat: int) -> nn.Module:
|
||||
if act_type == "relu":
|
||||
return nn.ReLU(inplace=True)
|
||||
elif act_type == "prelu":
|
||||
return nn.PReLU(num_parameters=num_feat)
|
||||
elif act_type == "leakyrelu":
|
||||
return nn.LeakyReLU(negative_slope=0.1, inplace=True)
|
||||
else:
|
||||
raise ValueError(f"Unsupported activation type: {act_type}")
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
out = x
|
||||
for layer in self.body:
|
||||
out = layer(out)
|
||||
out = self.upsampler(out)
|
||||
# residual addition with nearest upsampled input
|
||||
base = F.interpolate(x, scale_factor=self.upscale, mode="nearest")
|
||||
return out + base
|
||||
|
||||
|
||||
class ResidualDenseBlock(nn.Module):
|
||||
"""Residual Dense Block used in RRDB (RealESRGAN_x4plus)."""
|
||||
|
||||
def __init__(self, num_feat: int = 64, num_grow_ch: int = 32):
|
||||
super().__init__()
|
||||
self.conv1 = nn.Conv2d(num_feat, num_grow_ch, 3, 1, 1)
|
||||
self.conv2 = nn.Conv2d(num_feat + num_grow_ch, num_grow_ch, 3, 1, 1)
|
||||
self.conv3 = nn.Conv2d(num_feat + 2 * num_grow_ch, num_grow_ch, 3, 1, 1)
|
||||
self.conv4 = nn.Conv2d(num_feat + 3 * num_grow_ch, num_grow_ch, 3, 1, 1)
|
||||
self.conv5 = nn.Conv2d(num_feat + 4 * num_grow_ch, num_feat, 3, 1, 1)
|
||||
self.lrelu = nn.LeakyReLU(negative_slope=0.2, inplace=True)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
x1 = self.lrelu(self.conv1(x))
|
||||
x2 = self.lrelu(self.conv2(torch.cat((x, x1), 1)))
|
||||
x3 = self.lrelu(self.conv3(torch.cat((x, x1, x2), 1)))
|
||||
x4 = self.lrelu(self.conv4(torch.cat((x, x1, x2, x3), 1)))
|
||||
x5 = self.conv5(torch.cat((x, x1, x2, x3, x4), 1))
|
||||
return x5 * 0.2 + x
|
||||
|
||||
|
||||
class RRDB(nn.Module):
|
||||
"""Residual in Residual Dense Block."""
|
||||
|
||||
def __init__(self, num_feat: int, num_grow_ch: int = 32):
|
||||
super().__init__()
|
||||
self.rdb1 = ResidualDenseBlock(num_feat, num_grow_ch)
|
||||
self.rdb2 = ResidualDenseBlock(num_feat, num_grow_ch)
|
||||
self.rdb3 = ResidualDenseBlock(num_feat, num_grow_ch)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
out = self.rdb1(x)
|
||||
out = self.rdb2(out)
|
||||
out = self.rdb3(out)
|
||||
return out * 0.2 + x
|
||||
|
||||
|
||||
class RRDBNet(nn.Module):
|
||||
"""RRDB network for RealESRGAN_x4plus (heavier, higher quality for photos)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_in_ch: int = 3,
|
||||
num_out_ch: int = 3,
|
||||
scale: int = 4,
|
||||
num_feat: int = 64,
|
||||
num_block: int = 23,
|
||||
num_grow_ch: int = 32,
|
||||
):
|
||||
super().__init__()
|
||||
self.scale = scale
|
||||
in_ch = num_in_ch
|
||||
if scale == 2:
|
||||
in_ch = num_in_ch * 4
|
||||
elif scale == 1:
|
||||
in_ch = num_in_ch * 16
|
||||
self.conv_first = nn.Conv2d(in_ch, num_feat, 3, 1, 1)
|
||||
self.body = nn.Sequential(
|
||||
*[RRDB(num_feat, num_grow_ch) for _ in range(num_block)]
|
||||
)
|
||||
self.conv_body = nn.Conv2d(num_feat, num_feat, 3, 1, 1)
|
||||
# upsample
|
||||
self.conv_up1 = nn.Conv2d(num_feat, num_feat, 3, 1, 1)
|
||||
self.conv_up2 = nn.Conv2d(num_feat, num_feat, 3, 1, 1)
|
||||
self.conv_hr = nn.Conv2d(num_feat, num_feat, 3, 1, 1)
|
||||
self.conv_last = nn.Conv2d(num_feat, num_out_ch, 3, 1, 1)
|
||||
self.lrelu = nn.LeakyReLU(negative_slope=0.2, inplace=True)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
if self.scale == 2:
|
||||
feat = F.pixel_unshuffle(x, 2)
|
||||
elif self.scale == 1:
|
||||
feat = F.pixel_unshuffle(x, 4)
|
||||
else:
|
||||
feat = x
|
||||
feat = self.conv_first(feat)
|
||||
body_feat = self.conv_body(self.body(feat))
|
||||
feat = feat + body_feat
|
||||
feat = self.lrelu(
|
||||
self.conv_up1(F.interpolate(feat, scale_factor=2, mode="nearest"))
|
||||
)
|
||||
feat = self.lrelu(
|
||||
self.conv_up2(F.interpolate(feat, scale_factor=2, mode="nearest"))
|
||||
)
|
||||
return self.conv_last(self.lrelu(self.conv_hr(feat)))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Architecture auto-detection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _build_net_from_state_dict(state_dict: dict) -> nn.Module:
|
||||
"""Detect architecture from checkpoint keys and return an unloaded network."""
|
||||
if "conv_first.weight" in state_dict:
|
||||
# RRDBNet (e.g., RealESRGAN_x4plus)
|
||||
num_feat = state_dict["conv_first.weight"].shape[0]
|
||||
in_channels = state_dict["conv_first.weight"].shape[1]
|
||||
if in_channels == 3:
|
||||
scale = 4
|
||||
elif in_channels == 12:
|
||||
scale = 2
|
||||
elif in_channels == 48:
|
||||
scale = 1
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported RRDBNet conv_first input channels: {in_channels}"
|
||||
)
|
||||
num_block = sum(
|
||||
1
|
||||
for k in state_dict
|
||||
if k.startswith("body.") and k.endswith(".rdb1.conv1.weight")
|
||||
)
|
||||
num_grow_ch = state_dict["body.0.rdb1.conv1.weight"].shape[0]
|
||||
logger.info(
|
||||
"Detected RRDBNet: num_feat=%d, num_block=%d, num_grow_ch=%d, scale=%d",
|
||||
num_feat,
|
||||
num_block,
|
||||
num_grow_ch,
|
||||
scale,
|
||||
)
|
||||
return RRDBNet(
|
||||
num_in_ch=3,
|
||||
num_out_ch=3,
|
||||
scale=scale,
|
||||
num_feat=num_feat,
|
||||
num_block=num_block,
|
||||
num_grow_ch=num_grow_ch,
|
||||
)
|
||||
else:
|
||||
# SRVGGNetCompact (e.g., realesr-animevideov3)
|
||||
num_feat = state_dict["body.0.weight"].shape[0]
|
||||
# body layout: [first_conv, first_act, (conv, act)*num_conv, last_conv]
|
||||
# count 4-D weight tensors = first_conv + loop_convs + last_conv = num_conv + 2
|
||||
conv_keys = sorted(
|
||||
[
|
||||
k
|
||||
for k in state_dict
|
||||
if k.startswith("body.")
|
||||
and k.endswith(".weight")
|
||||
and state_dict[k].dim() == 4
|
||||
],
|
||||
key=lambda k: int(k.split(".")[1]),
|
||||
)
|
||||
num_conv = len(conv_keys) - 2 # subtract first and last
|
||||
# upscale from last conv output channels: out_ch = num_out_ch * upscale^2
|
||||
last_out_ch = state_dict[conv_keys[-1]].shape[0]
|
||||
upscale = int(math.sqrt(last_out_ch / 3))
|
||||
logger.info(
|
||||
"Detected SRVGGNetCompact: num_feat=%d, num_conv=%d, upscale=%d",
|
||||
num_feat,
|
||||
num_conv,
|
||||
upscale,
|
||||
)
|
||||
return SRVGGNetCompact(
|
||||
num_in_ch=3,
|
||||
num_out_ch=3,
|
||||
num_feat=num_feat,
|
||||
num_conv=num_conv,
|
||||
upscale=upscale,
|
||||
act_type="prelu",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UpscalerModel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class UpscalerModel:
|
||||
"""Wraps a Real-ESRGAN network, provides load() and upscale() API."""
|
||||
|
||||
def __init__(self, net: nn.Module, scale: int):
|
||||
self.net = net
|
||||
self.scale = scale # the model's native upscaling factor (e.g. 4)
|
||||
|
||||
@property
|
||||
def device(self) -> torch.device:
|
||||
return next(self.net.parameters()).device
|
||||
|
||||
@property
|
||||
def dtype(self) -> torch.dtype:
|
||||
return next(self.net.parameters()).dtype
|
||||
|
||||
def _copy_input_to_device(self, frames: np.ndarray) -> torch.Tensor:
|
||||
return torch.from_numpy(frames).to(self.device)
|
||||
|
||||
def _preprocess_input_tensor(self, imgs_t: torch.Tensor) -> torch.Tensor:
|
||||
imgs_t = imgs_t.permute(0, 3, 1, 2).to(dtype=self.dtype).mul_(1.0 / 255.0)
|
||||
if self.device.type == "cuda":
|
||||
imgs_t = imgs_t.contiguous(memory_format=torch.channels_last)
|
||||
return imgs_t
|
||||
|
||||
@staticmethod
|
||||
def _postprocess_output_tensor(out: torch.Tensor) -> torch.Tensor:
|
||||
out = out.permute(0, 2, 3, 1).clamp(0.0, 1.0).mul_(255.0)
|
||||
return out.to(torch.uint8).contiguous()
|
||||
|
||||
@staticmethod
|
||||
def _copy_output_to_host(out: torch.Tensor) -> np.ndarray:
|
||||
return out.cpu().numpy()
|
||||
|
||||
def _start_cuda_timer(self):
|
||||
if self.device.type != "cuda":
|
||||
return None
|
||||
start = torch.cuda.Event(enable_timing=True)
|
||||
end = torch.cuda.Event(enable_timing=True)
|
||||
start.record()
|
||||
return start, end
|
||||
|
||||
@staticmethod
|
||||
def _stop_cuda_timer(timer) -> None:
|
||||
if timer is not None:
|
||||
timer[1].record()
|
||||
|
||||
@staticmethod
|
||||
def _cuda_elapsed_s(timer, fallback_s: float) -> float:
|
||||
if timer is None:
|
||||
return fallback_s
|
||||
timer[1].synchronize()
|
||||
return timer[0].elapsed_time(timer[1]) / 1000.0
|
||||
|
||||
def _should_use_tiled_upscale(self, h: int, w: int) -> bool:
|
||||
if self.device.type != "cuda":
|
||||
return False
|
||||
free_bytes, _ = torch.cuda.mem_get_info(self.device)
|
||||
output_bytes = h * w * self.scale * self.scale * 3 * 4
|
||||
required_free_bytes = max(
|
||||
_LOW_MEMORY_TILED_UPSCALE_FREE_BYTES,
|
||||
output_bytes * 4,
|
||||
)
|
||||
return free_bytes < required_free_bytes
|
||||
|
||||
def _upscale_tiled_to_cpu(
|
||||
self,
|
||||
img_t: torch.Tensor,
|
||||
tile_size: int = _REALESRGAN_TILE_SIZE,
|
||||
tile_pad: int = _REALESRGAN_TILE_PAD,
|
||||
) -> torch.Tensor:
|
||||
_, channels, h, w = img_t.shape
|
||||
scale = self.scale
|
||||
output = torch.empty(
|
||||
(1, channels, h * scale, w * scale),
|
||||
dtype=torch.float32,
|
||||
device="cpu",
|
||||
)
|
||||
|
||||
for y in range(0, h, tile_size):
|
||||
tile_h = min(tile_size, h - y)
|
||||
in_y0 = max(y - tile_pad, 0)
|
||||
in_y1 = min(y + tile_h + tile_pad, h)
|
||||
out_y0 = y * scale
|
||||
out_y1 = (y + tile_h) * scale
|
||||
crop_y0 = (y - in_y0) * scale
|
||||
crop_y1 = crop_y0 + tile_h * scale
|
||||
|
||||
for x in range(0, w, tile_size):
|
||||
tile_w = min(tile_size, w - x)
|
||||
in_x0 = max(x - tile_pad, 0)
|
||||
in_x1 = min(x + tile_w + tile_pad, w)
|
||||
out_x0 = x * scale
|
||||
out_x1 = (x + tile_w) * scale
|
||||
crop_x0 = (x - in_x0) * scale
|
||||
crop_x1 = crop_x0 + tile_w * scale
|
||||
|
||||
tile = img_t[..., in_y0:in_y1, in_x0:in_x1]
|
||||
out_tile = self.net(tile)
|
||||
out_tile = out_tile[..., crop_y0:crop_y1, crop_x0:crop_x1].float()
|
||||
output[..., out_y0:out_y1, out_x0:out_x1].copy_(out_tile.cpu())
|
||||
|
||||
return output
|
||||
|
||||
def upscale(self, frame: np.ndarray, outscale: float | None = None) -> np.ndarray:
|
||||
"""Upscale a single HWC uint8 frame → HWC uint8 frame.
|
||||
|
||||
Args:
|
||||
frame: Input HWC uint8 numpy array.
|
||||
outscale: Desired final upscaling factor. If different from the
|
||||
model's native scale, a cheap resize is applied after
|
||||
the network output (same approach as the official
|
||||
Real-ESRGAN ``inference_realesrgan.py --outscale``).
|
||||
``None`` means use the model's native scale as-is.
|
||||
"""
|
||||
h, w = frame.shape[:2]
|
||||
img = frame.astype(np.float32) / 255.0
|
||||
img_t = (
|
||||
torch.from_numpy(img)
|
||||
.permute(2, 0, 1)
|
||||
.unsqueeze(0)
|
||||
.to(device=self.device, dtype=self.dtype)
|
||||
)
|
||||
with torch.no_grad():
|
||||
if self._should_use_tiled_upscale(h, w):
|
||||
logger.info(
|
||||
"Using tiled Real-ESRGAN upscale for low GPU memory: "
|
||||
"frame=%dx%d, tile_size=%d, tile_pad=%d",
|
||||
w,
|
||||
h,
|
||||
_REALESRGAN_TILE_SIZE,
|
||||
_REALESRGAN_TILE_PAD,
|
||||
)
|
||||
out = self._upscale_tiled_to_cpu(img_t)
|
||||
else:
|
||||
try:
|
||||
out = self.net(img_t)
|
||||
except torch.cuda.OutOfMemoryError:
|
||||
if self.device.type != "cuda":
|
||||
raise
|
||||
torch.cuda.empty_cache()
|
||||
logger.warning(
|
||||
"Real-ESRGAN full-frame upscale OOM; retrying with tiled upscale"
|
||||
)
|
||||
out = self._upscale_tiled_to_cpu(img_t)
|
||||
|
||||
# If the desired outscale differs from the model's native scale,
|
||||
# resize to (h * outscale, w * outscale).
|
||||
if outscale is not None and outscale != self.scale:
|
||||
target_h = int(h * outscale)
|
||||
target_w = int(w * outscale)
|
||||
out = F.interpolate(
|
||||
out, size=(target_h, target_w), mode="bicubic", align_corners=False
|
||||
)
|
||||
|
||||
out_np = out.squeeze(0).permute(1, 2, 0).clamp(0.0, 1.0).cpu().numpy()
|
||||
return (out_np * 255.0).astype(np.uint8)
|
||||
|
||||
def upscale_batch(
|
||||
self, frames: list[np.ndarray], outscale: float | None = None
|
||||
) -> list[np.ndarray]:
|
||||
"""Upscale same-resolution HWC uint8 frames in one batched forward pass."""
|
||||
if not frames:
|
||||
return []
|
||||
|
||||
h, w = frames[0].shape[:2]
|
||||
if any(frame.shape[:2] != (h, w) for frame in frames):
|
||||
raise ValueError("All frames in a batch must have the same resolution")
|
||||
|
||||
total_start_time = time.perf_counter()
|
||||
|
||||
start_time = time.perf_counter()
|
||||
imgs = np.stack(frames, axis=0)
|
||||
stack_duration_s = time.perf_counter() - start_time
|
||||
|
||||
start_time = time.perf_counter()
|
||||
h2d_timer = self._start_cuda_timer()
|
||||
imgs_t = self._copy_input_to_device(imgs)
|
||||
self._stop_cuda_timer(h2d_timer)
|
||||
h2d_wall_duration_s = time.perf_counter() - start_time
|
||||
|
||||
start_time = time.perf_counter()
|
||||
input_preprocess_timer = self._start_cuda_timer()
|
||||
imgs_t = self._preprocess_input_tensor(imgs_t)
|
||||
self._stop_cuda_timer(input_preprocess_timer)
|
||||
input_preprocess_wall_duration_s = time.perf_counter() - start_time
|
||||
|
||||
start_time = time.perf_counter()
|
||||
forward_timer = self._start_cuda_timer()
|
||||
with torch.inference_mode():
|
||||
out = self.net(imgs_t)
|
||||
self._stop_cuda_timer(forward_timer)
|
||||
forward_wall_duration_s = time.perf_counter() - start_time
|
||||
|
||||
resize_timer = None
|
||||
resize_wall_duration_s = 0.0
|
||||
if outscale is not None and outscale != self.scale:
|
||||
start_time = time.perf_counter()
|
||||
resize_timer = self._start_cuda_timer()
|
||||
target_h = int(h * outscale)
|
||||
target_w = int(w * outscale)
|
||||
out = F.interpolate(
|
||||
out, size=(target_h, target_w), mode="bicubic", align_corners=False
|
||||
)
|
||||
self._stop_cuda_timer(resize_timer)
|
||||
resize_wall_duration_s = time.perf_counter() - start_time
|
||||
|
||||
start_time = time.perf_counter()
|
||||
output_postprocess_timer = self._start_cuda_timer()
|
||||
out = self._postprocess_output_tensor(out)
|
||||
self._stop_cuda_timer(output_postprocess_timer)
|
||||
output_postprocess_wall_duration_s = time.perf_counter() - start_time
|
||||
|
||||
start_time = time.perf_counter()
|
||||
output_d2h_timer = self._start_cuda_timer()
|
||||
out_np = self._copy_output_to_host(out)
|
||||
self._stop_cuda_timer(output_d2h_timer)
|
||||
output_d2h_wall_duration_s = time.perf_counter() - start_time
|
||||
|
||||
start_time = time.perf_counter()
|
||||
outputs = [frame for frame in out_np]
|
||||
post_duration_s = time.perf_counter() - start_time
|
||||
|
||||
h2d_duration_s = self._cuda_elapsed_s(h2d_timer, h2d_wall_duration_s)
|
||||
input_preprocess_duration_s = self._cuda_elapsed_s(
|
||||
input_preprocess_timer, input_preprocess_wall_duration_s
|
||||
)
|
||||
forward_duration_s = self._cuda_elapsed_s(
|
||||
forward_timer, forward_wall_duration_s
|
||||
)
|
||||
resize_duration_s = self._cuda_elapsed_s(resize_timer, resize_wall_duration_s)
|
||||
output_postprocess_duration_s = self._cuda_elapsed_s(
|
||||
output_postprocess_timer, output_postprocess_wall_duration_s
|
||||
)
|
||||
output_d2h_duration_s = self._cuda_elapsed_s(
|
||||
output_d2h_timer, output_d2h_wall_duration_s
|
||||
)
|
||||
total_duration_s = time.perf_counter() - total_start_time
|
||||
timing_source = "cuda_event" if self.device.type == "cuda" else "wall"
|
||||
logger.info(
|
||||
"RealESRGAN batch upscale: batch=%d input=%dx%d native_scale=%dx outscale=%s "
|
||||
"dtype=%s timing=%s total=%.3fs stack=%.3fs input_h2d=%.3fs "
|
||||
"input_pre=%.3fs forward=%.3fs resize=%.3fs output_post=%.3fs "
|
||||
"output_d2h=%.3fs python_post=%.3fs",
|
||||
len(frames),
|
||||
w,
|
||||
h,
|
||||
self.scale,
|
||||
outscale if outscale is not None else self.scale,
|
||||
self.dtype,
|
||||
timing_source,
|
||||
total_duration_s,
|
||||
stack_duration_s,
|
||||
h2d_duration_s,
|
||||
input_preprocess_duration_s,
|
||||
forward_duration_s,
|
||||
resize_duration_s,
|
||||
output_postprocess_duration_s,
|
||||
output_d2h_duration_s,
|
||||
post_duration_s,
|
||||
)
|
||||
return outputs
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ImageUpscaler public class
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ImageUpscaler:
|
||||
"""
|
||||
Lazy-loaded Real-ESRGAN upscaler.
|
||||
|
||||
Weights are downloaded and cached on first call to `.upscale()`.
|
||||
Supports both SRVGGNetCompact (lightweight, default) and RRDBNet (heavier).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_path: Optional[str] = None,
|
||||
scale: int = 4,
|
||||
half_precision: bool = False,
|
||||
):
|
||||
self._model_path = model_path
|
||||
self._scale = scale
|
||||
self._half_precision = half_precision
|
||||
|
||||
def _ensure_model_loaded(self) -> UpscalerModel:
|
||||
"""Download/load Real-ESRGAN weights, detect arch, and cache globally."""
|
||||
model_path = self._model_path or _default_model_path_for_scale(self._scale)
|
||||
|
||||
# Resolve: local .pth pass-through, or HF repo → download single file
|
||||
resolved_path = _resolve_model_path(model_path)
|
||||
|
||||
if resolved_path in _MODEL_CACHE:
|
||||
return _MODEL_CACHE[resolved_path]
|
||||
|
||||
logger.info("Loading Real-ESRGAN weights from %s", resolved_path)
|
||||
try:
|
||||
state_dict = torch.load(
|
||||
resolved_path, map_location="cpu", weights_only=True
|
||||
)
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
f"Failed to load Real-ESRGAN checkpoint from '{resolved_path}'. "
|
||||
f"The file may be corrupted or not a valid PyTorch checkpoint. "
|
||||
f"Original error: {e}"
|
||||
) from e
|
||||
|
||||
# Some checkpoints wrap weights under a 'params' or 'params_ema' key
|
||||
if "params_ema" in state_dict:
|
||||
state_dict = state_dict["params_ema"]
|
||||
elif "params" in state_dict:
|
||||
state_dict = state_dict["params"]
|
||||
|
||||
try:
|
||||
net = _build_net_from_state_dict(state_dict)
|
||||
net.load_state_dict(state_dict, strict=True)
|
||||
except (RuntimeError, KeyError) as e:
|
||||
raise RuntimeError(
|
||||
f"Real-ESRGAN weight file '{resolved_path}' is not compatible "
|
||||
f"with the supported architectures (SRVGGNetCompact / RRDBNet). "
|
||||
f"Please ensure you are using a valid Real-ESRGAN checkpoint. "
|
||||
f"Original error: {e}"
|
||||
) from e
|
||||
net.eval()
|
||||
|
||||
device = current_platform.get_local_torch_device()
|
||||
if self._half_precision:
|
||||
net = net.half()
|
||||
net = net.to(device)
|
||||
|
||||
# Detect the model's native scale from network architecture
|
||||
native_scale = 4 # sensible default
|
||||
if hasattr(net, "upscale"):
|
||||
native_scale = net.upscale
|
||||
elif hasattr(net, "scale"):
|
||||
native_scale = net.scale
|
||||
|
||||
model = UpscalerModel(net=net, scale=native_scale)
|
||||
_MODEL_CACHE[resolved_path] = model
|
||||
logger.info(
|
||||
"Real-ESRGAN model loaded on device: %s (native_scale=%dx, outscale=%s)",
|
||||
device,
|
||||
native_scale,
|
||||
f"{self._scale}x" if self._scale != native_scale else "native",
|
||||
)
|
||||
return model
|
||||
|
||||
def upscale(self, frames: list[np.ndarray]) -> list[np.ndarray]:
|
||||
"""Upscale a list of HWC uint8 frames.
|
||||
|
||||
Uses the model's native scale for super-resolution, then resizes to
|
||||
the desired ``outscale`` if it differs (cheap bicubic resize).
|
||||
"""
|
||||
if not frames:
|
||||
return frames
|
||||
model = self._ensure_model_loaded()
|
||||
outscale = self._scale if self._scale != model.scale else None
|
||||
return [model.upscale(frame, outscale=outscale) for frame in frames]
|
||||
|
||||
def upscale_batched(self, frames: list[np.ndarray]) -> list[np.ndarray]:
|
||||
"""Upscale HWC uint8 frames with batched forwards grouped by resolution."""
|
||||
if not frames:
|
||||
return frames
|
||||
total_start_time = time.perf_counter()
|
||||
model = self._ensure_model_loaded()
|
||||
outscale = self._scale if self._scale != model.scale else None
|
||||
output_frames: list[np.ndarray | None] = [None] * len(frames)
|
||||
groups: dict[tuple[int, ...], list[int]] = {}
|
||||
for idx, frame in enumerate(frames):
|
||||
groups.setdefault(tuple(frame.shape), []).append(idx)
|
||||
|
||||
for shape, indices in groups.items():
|
||||
logger.info(
|
||||
"RealESRGAN upscale group: frames=%d shape=%s indices=%s",
|
||||
len(indices),
|
||||
shape,
|
||||
indices,
|
||||
)
|
||||
group_frames = [frames[idx] for idx in indices]
|
||||
group_outputs = model.upscale_batch(group_frames, outscale=outscale)
|
||||
for idx, output in zip(indices, group_outputs):
|
||||
output_frames[idx] = output
|
||||
|
||||
if any(frame is None for frame in output_frames):
|
||||
raise RuntimeError("RealESRGAN batch upscale did not produce all frames")
|
||||
|
||||
total_duration_s = time.perf_counter() - total_start_time
|
||||
logger.info(
|
||||
"RealESRGAN batch_upscale_frames completed in %.3f seconds for %d frames across %d groups",
|
||||
total_duration_s,
|
||||
len(frames),
|
||||
len(groups),
|
||||
)
|
||||
return [frame for frame in output_frames if frame is not None]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HF download helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _resolve_model_path(model_path: str) -> str:
|
||||
"""Return a local .pth file path.
|
||||
|
||||
Accepts:
|
||||
- An existing local file path (pass-through).
|
||||
- An http(s) URL to a .pth file, downloaded into the local cache.
|
||||
- A HuggingFace ``repo_id`` → downloads the default weight file
|
||||
(``RealESRGAN_x4.pth``).
|
||||
- A HuggingFace ``repo_id:filename`` → downloads *filename* from *repo_id*,
|
||||
allowing users to specify custom weight files hosted on HF.
|
||||
"""
|
||||
cached_path = _RESOLVED_MODEL_PATH_CACHE.get(model_path)
|
||||
if cached_path is not None:
|
||||
return cached_path
|
||||
|
||||
if os.path.isfile(model_path):
|
||||
_RESOLVED_MODEL_PATH_CACHE[model_path] = model_path
|
||||
return model_path
|
||||
|
||||
parsed_url = urlparse(model_path)
|
||||
if parsed_url.scheme in ("http", "https"):
|
||||
filename = (
|
||||
os.path.basename(unquote(parsed_url.path)) or _DEFAULT_REALESRGAN_FILENAME
|
||||
)
|
||||
cache_dir = os.path.join(
|
||||
os.path.expanduser("~"), ".cache", "sglang", "realesrgan"
|
||||
)
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
cache_key = sha256(model_path.encode("utf-8")).hexdigest()[:12]
|
||||
local_path = os.path.join(cache_dir, f"{cache_key}-{filename}")
|
||||
if not os.path.isfile(local_path):
|
||||
tmp_path = f"{local_path}.tmp"
|
||||
logger.info("Downloading Real-ESRGAN weights from URL %s", model_path)
|
||||
torch.hub.download_url_to_file(model_path, tmp_path, progress=False)
|
||||
os.replace(tmp_path, local_path)
|
||||
_RESOLVED_MODEL_PATH_CACHE[model_path] = local_path
|
||||
return local_path
|
||||
|
||||
# Parse optional "repo_id:filename" syntax; fall back to default filename.
|
||||
if ":" in model_path and not model_path.startswith("/"):
|
||||
repo_id, filename = model_path.split(":", 1)
|
||||
else:
|
||||
repo_id = model_path
|
||||
filename = _DEFAULT_REALESRGAN_FILENAME
|
||||
|
||||
try:
|
||||
from huggingface_hub import hf_hub_download
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"huggingface_hub is required to download Real-ESRGAN weights. "
|
||||
"Install it with: pip install huggingface_hub"
|
||||
) from e
|
||||
|
||||
logger.info(
|
||||
"Downloading Real-ESRGAN weights from HF repo %s (file: %s)",
|
||||
repo_id,
|
||||
filename,
|
||||
)
|
||||
try:
|
||||
local_path = hf_hub_download(
|
||||
repo_id=repo_id,
|
||||
filename=filename,
|
||||
)
|
||||
except Exception as e:
|
||||
raise FileNotFoundError(
|
||||
f"Failed to download Real-ESRGAN weights from HuggingFace repo "
|
||||
f"'{repo_id}' (file: '{filename}'). If you are using a custom "
|
||||
f"model, provide either a local .pth file path or use the "
|
||||
f"'repo_id:filename' format (e.g. 'my-org/my-esrgan:weights.pth'). "
|
||||
f"Original error: {e}"
|
||||
) from e
|
||||
_RESOLVED_MODEL_PATH_CACHE[model_path] = local_path
|
||||
return local_path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Module-level convenience function
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def upscale_frames(
|
||||
frames: list[np.ndarray],
|
||||
model_path: Optional[str] = None,
|
||||
scale: int = 4,
|
||||
half_precision: bool = False,
|
||||
) -> list[np.ndarray]:
|
||||
"""
|
||||
Convenience wrapper around ImageUpscaler.
|
||||
|
||||
The model always runs at its native resolution (e.g. 4× for
|
||||
``RealESRGAN_x4.pth``). If *scale* differs from the native factor,
|
||||
a cheap bicubic resize is applied after the network output – the same
|
||||
approach used by the official Real-ESRGAN ``--outscale`` flag.
|
||||
|
||||
Args:
|
||||
frames: List of uint8 HWC numpy frames.
|
||||
model_path: Local .pth file, HuggingFace repo ID, or
|
||||
``repo_id:filename`` for a custom weight file.
|
||||
None → default ``ai-forever/Real-ESRGAN`` with
|
||||
``RealESRGAN_x4.pth``.
|
||||
scale: Desired final upscaling factor (e.g. 2, 3, 4).
|
||||
The 4× model is used internally; the output is
|
||||
resized to match *scale* when it differs.
|
||||
half_precision: Use fp16 inference (faster on supported GPUs).
|
||||
|
||||
Returns:
|
||||
List of upscaled uint8 HWC numpy frames.
|
||||
"""
|
||||
upscaler = ImageUpscaler(
|
||||
model_path=model_path,
|
||||
scale=scale,
|
||||
half_precision=half_precision,
|
||||
)
|
||||
return upscaler.upscale(frames)
|
||||
|
||||
|
||||
def batch_upscale_frames(
|
||||
frames: list[np.ndarray],
|
||||
model_path: Optional[str] = None,
|
||||
scale: int = 4,
|
||||
) -> list[np.ndarray]:
|
||||
"""
|
||||
Batched Real-ESRGAN upscaling for realtime video paths.
|
||||
|
||||
The default ``upscale_frames`` API intentionally keeps its original
|
||||
per-frame behavior. Call this helper only when the caller can tolerate
|
||||
batched execution and same-shape grouping semantics.
|
||||
"""
|
||||
upscaler = ImageUpscaler(
|
||||
model_path=model_path,
|
||||
scale=scale,
|
||||
half_precision=current_platform.is_cuda(),
|
||||
)
|
||||
return upscaler.upscale_batched(frames)
|
||||
@@ -0,0 +1,538 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
RIFE 4.22.lite frame interpolation for SGLang diffusion pipelines.
|
||||
|
||||
RIFE model code is vendored and adapted from:
|
||||
- https://github.com/hzwer/ECCV2022-RIFE (MIT License)
|
||||
- https://github.com/hzwer/Practical-RIFE (MIT License)
|
||||
Copyright (c) 2021 Zhewei Huang
|
||||
|
||||
The FrameInterpolator wrapper and integration code are original work.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
# Default HuggingFace repo for RIFE 4.22.lite weights
|
||||
_DEFAULT_RIFE_HF_REPO = "elfgum/RIFE-4.22.lite"
|
||||
|
||||
# Module-level cache: model_path -> Model instance
|
||||
_MODEL_CACHE: dict[str, "Model"] = {}
|
||||
_MAX_RIFE_BATCH_PAIRS = 16
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vendored RIFE 4.22.lite model code
|
||||
# (IFBlock, IFNet_HDv3 backbone, Model wrapper)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def warp(tenInput: torch.Tensor, tenFlow: torch.Tensor) -> torch.Tensor:
|
||||
"""Warp tenInput by tenFlow using grid_sample."""
|
||||
# Build base grid for the current size
|
||||
tenHorizontal = (
|
||||
torch.linspace(-1.0, 1.0, tenFlow.shape[3], device=tenFlow.device)
|
||||
.view(1, 1, 1, tenFlow.shape[3])
|
||||
.expand(tenFlow.shape[0], -1, tenFlow.shape[2], -1)
|
||||
)
|
||||
tenVertical = (
|
||||
torch.linspace(-1.0, 1.0, tenFlow.shape[2], device=tenFlow.device)
|
||||
.view(1, 1, tenFlow.shape[2], 1)
|
||||
.expand(tenFlow.shape[0], -1, -1, tenFlow.shape[3])
|
||||
)
|
||||
tenGrid = torch.cat([tenHorizontal, tenVertical], dim=1)
|
||||
|
||||
tenFlow = torch.cat(
|
||||
[
|
||||
tenFlow[:, 0:1, :, :] / ((tenInput.shape[3] - 1.0) / 2.0),
|
||||
tenFlow[:, 1:2, :, :] / ((tenInput.shape[2] - 1.0) / 2.0),
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
|
||||
grid = (tenGrid + tenFlow).permute(0, 2, 3, 1)
|
||||
return F.grid_sample(
|
||||
input=tenInput,
|
||||
grid=grid,
|
||||
mode="bilinear",
|
||||
padding_mode="border",
|
||||
align_corners=True,
|
||||
)
|
||||
|
||||
|
||||
def _conv(in_planes, out_planes, kernel_size=3, stride=1, padding=1, dilation=1):
|
||||
"""Conv2d + LeakyReLU helper (matches RIFE 4.22 conv())."""
|
||||
return nn.Sequential(
|
||||
nn.Conv2d(
|
||||
in_planes,
|
||||
out_planes,
|
||||
kernel_size=kernel_size,
|
||||
stride=stride,
|
||||
padding=padding,
|
||||
dilation=dilation,
|
||||
bias=True,
|
||||
),
|
||||
nn.LeakyReLU(0.2, True),
|
||||
)
|
||||
|
||||
|
||||
class ResConv(nn.Module):
|
||||
"""Residual convolution block with learnable beta scaling (RIFE 4.22)."""
|
||||
|
||||
def __init__(self, c: int, dilation: int = 1):
|
||||
super().__init__()
|
||||
self.conv = nn.Conv2d(c, c, 3, 1, dilation, dilation=dilation, groups=1)
|
||||
self.beta = nn.Parameter(torch.ones((1, c, 1, 1)), requires_grad=True)
|
||||
self.relu = nn.LeakyReLU(0.2, True)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
return self.relu(self.conv(x) * self.beta + x)
|
||||
|
||||
|
||||
class IFBlock(nn.Module):
|
||||
"""Single-scale optical flow + mask + feature block (RIFE 4.22)."""
|
||||
|
||||
def __init__(self, in_planes: int, c: int = 64):
|
||||
super().__init__()
|
||||
self.conv0 = nn.Sequential(
|
||||
_conv(in_planes, c // 2, 3, 2, 1),
|
||||
_conv(c // 2, c, 3, 2, 1),
|
||||
)
|
||||
self.convblock = nn.Sequential(
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
)
|
||||
self.lastconv = nn.Sequential(
|
||||
nn.ConvTranspose2d(c, 4 * 13, 4, 2, 1),
|
||||
nn.PixelShuffle(2),
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
flow: Optional[torch.Tensor] = None,
|
||||
scale: float = 1.0,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
x = F.interpolate(
|
||||
x, scale_factor=1.0 / scale, mode="bilinear", align_corners=False
|
||||
)
|
||||
if flow is not None:
|
||||
flow = (
|
||||
F.interpolate(
|
||||
flow,
|
||||
scale_factor=1.0 / scale,
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
)
|
||||
* 1.0
|
||||
/ scale
|
||||
)
|
||||
x = torch.cat((x, flow), 1)
|
||||
feat = self.conv0(x)
|
||||
feat = self.convblock(feat)
|
||||
tmp = self.lastconv(feat)
|
||||
tmp = F.interpolate(
|
||||
tmp, scale_factor=scale, mode="bilinear", align_corners=False
|
||||
)
|
||||
flow = tmp[:, :4] * scale
|
||||
mask = tmp[:, 4:5]
|
||||
feat = tmp[:, 5:]
|
||||
return flow, mask, feat
|
||||
|
||||
|
||||
class Head(nn.Module):
|
||||
"""Feature encoder producing 4-channel features at full resolution (RIFE 4.22)."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.cnn0 = nn.Conv2d(3, 16, 3, 2, 1)
|
||||
self.cnn1 = nn.Conv2d(16, 16, 3, 1, 1)
|
||||
self.cnn2 = nn.Conv2d(16, 16, 3, 1, 1)
|
||||
self.cnn3 = nn.ConvTranspose2d(16, 4, 4, 2, 1)
|
||||
self.relu = nn.LeakyReLU(0.2, True)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
x0 = self.cnn0(x)
|
||||
x = self.relu(x0)
|
||||
x1 = self.cnn1(x)
|
||||
x = self.relu(x1)
|
||||
x2 = self.cnn2(x)
|
||||
x = self.relu(x2)
|
||||
x3 = self.cnn3(x)
|
||||
return x3
|
||||
|
||||
|
||||
class IFNet(nn.Module):
|
||||
"""4-scale IFNet optical flow network (RIFE 4.22 backbone)."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.block0 = IFBlock(7 + 8, c=192)
|
||||
self.block1 = IFBlock(8 + 4 + 8 + 8, c=128)
|
||||
self.block2 = IFBlock(8 + 4 + 8 + 8, c=64)
|
||||
self.block3 = IFBlock(8 + 4 + 8 + 8, c=32)
|
||||
self.encode = Head()
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
timestep: float = 0.5,
|
||||
scale_list: Optional[list] = None,
|
||||
) -> tuple[list, torch.Tensor, list]:
|
||||
if scale_list is None:
|
||||
scale_list = [8, 4, 2, 1]
|
||||
|
||||
channel = x.shape[1] // 2
|
||||
img0 = x[:, :channel]
|
||||
img1 = x[:, channel:]
|
||||
|
||||
if not torch.is_tensor(timestep):
|
||||
timestep = (x[:, :1].clone() * 0 + 1) * timestep
|
||||
else:
|
||||
timestep = timestep.repeat(1, 1, img0.shape[2], img0.shape[3])
|
||||
|
||||
f0 = self.encode(img0[:, :3])
|
||||
f1 = self.encode(img1[:, :3])
|
||||
|
||||
flow_list = []
|
||||
merged = []
|
||||
mask_list = []
|
||||
warped_img0 = img0
|
||||
warped_img1 = img1
|
||||
flow = None
|
||||
mask = None
|
||||
|
||||
block = [self.block0, self.block1, self.block2, self.block3]
|
||||
for i in range(4):
|
||||
if flow is None:
|
||||
flow, mask, feat = block[i](
|
||||
torch.cat((img0[:, :3], img1[:, :3], f0, f1, timestep), 1),
|
||||
None,
|
||||
scale=scale_list[i],
|
||||
)
|
||||
else:
|
||||
wf0 = warp(f0, flow[:, :2])
|
||||
wf1 = warp(f1, flow[:, 2:4])
|
||||
fd, m0, feat = block[i](
|
||||
torch.cat(
|
||||
(
|
||||
warped_img0[:, :3],
|
||||
warped_img1[:, :3],
|
||||
wf0,
|
||||
wf1,
|
||||
timestep,
|
||||
mask,
|
||||
feat,
|
||||
),
|
||||
1,
|
||||
),
|
||||
flow,
|
||||
scale=scale_list[i],
|
||||
)
|
||||
mask = m0
|
||||
flow = flow + fd
|
||||
|
||||
mask_list.append(mask)
|
||||
flow_list.append(flow)
|
||||
warped_img0 = warp(img0, flow[:, :2])
|
||||
warped_img1 = warp(img1, flow[:, 2:4])
|
||||
merged.append((warped_img0, warped_img1))
|
||||
|
||||
mask = torch.sigmoid(mask)
|
||||
merged[3] = warped_img0 * mask + warped_img1 * (1 - mask)
|
||||
|
||||
return flow_list, mask_list[3], merged
|
||||
|
||||
|
||||
class Model:
|
||||
"""Wraps IFNet, provides load_model() and inference() API."""
|
||||
|
||||
def __init__(self):
|
||||
self.flownet = IFNet()
|
||||
self.device_type: str = "cpu"
|
||||
|
||||
def eval(self) -> "Model":
|
||||
self.flownet.eval()
|
||||
return self
|
||||
|
||||
def device(self) -> torch.device:
|
||||
return next(self.flownet.parameters()).device
|
||||
|
||||
def load_model(self, path: str, strip_module_prefix: bool = True) -> None:
|
||||
"""Load weights from {path}/flownet.pkl.
|
||||
|
||||
Args:
|
||||
path: Directory containing ``flownet.pkl``.
|
||||
strip_module_prefix: If True, strip the ``module.`` prefix that
|
||||
``DataParallel`` / ``DistributedDataParallel`` adds to keys.
|
||||
"""
|
||||
flownet_path = os.path.join(path, "flownet.pkl")
|
||||
if not os.path.isfile(flownet_path):
|
||||
raise FileNotFoundError(
|
||||
f"RIFE weight file not found: {flownet_path}\n"
|
||||
"Expected layout: <model_path>/flownet.pkl"
|
||||
)
|
||||
|
||||
def convert(param):
|
||||
if strip_module_prefix:
|
||||
return {
|
||||
k.replace("module.", ""): v
|
||||
for k, v in param.items()
|
||||
if "module." in k
|
||||
}
|
||||
else:
|
||||
return {k: v for k, v in param.items() if "module." not in k}
|
||||
|
||||
state = torch.load(flownet_path, map_location="cpu", weights_only=False)
|
||||
self.flownet.load_state_dict(convert(state), strict=False)
|
||||
logger.info("Loaded RIFE weights from %s", flownet_path)
|
||||
|
||||
def inference(
|
||||
self,
|
||||
img0: torch.Tensor,
|
||||
img1: torch.Tensor,
|
||||
scale: float = 1.0,
|
||||
timestep: float = 0.5,
|
||||
) -> torch.Tensor:
|
||||
"""Interpolate a single intermediate frame between img0 and img1."""
|
||||
n, c, h, w = img0.shape
|
||||
|
||||
# Pad to multiples of 32 so that RIFE's downsample/upsample round-trips
|
||||
# preserve spatial dimensions exactly.
|
||||
ph = ((h - 1) // 32 + 1) * 32
|
||||
pw = ((w - 1) // 32 + 1) * 32
|
||||
pad = (0, pw - w, 0, ph - h)
|
||||
img0 = F.pad(img0, pad)
|
||||
img1 = F.pad(img1, pad)
|
||||
|
||||
imgs = torch.cat((img0, img1), 1)
|
||||
scale_list = [8 / scale, 4 / scale, 2 / scale, 1 / scale]
|
||||
with torch.no_grad():
|
||||
flow_list, mask, merged = self.flownet(
|
||||
imgs,
|
||||
timestep=timestep,
|
||||
scale_list=scale_list,
|
||||
)
|
||||
|
||||
# Crop back to original resolution
|
||||
return merged[3][:, :, :h, :w]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FrameInterpolator public class
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class FrameInterpolator:
|
||||
"""
|
||||
Lazy-loaded RIFE 4.22.lite frame interpolator.
|
||||
|
||||
Weights are loaded on first call to `.interpolate()` and cached globally
|
||||
per model_path to avoid reloading across requests.
|
||||
"""
|
||||
|
||||
def __init__(self, model_path: Optional[str] = None):
|
||||
self._model_path = model_path
|
||||
self._resolved_path: Optional[str] = None
|
||||
|
||||
def _ensure_model_loaded(self) -> Model:
|
||||
"""Load RIFE model weights.
|
||||
|
||||
Accepts a local directory **or** a HuggingFace repo ID. When *None*
|
||||
(the default) the weights are downloaded (and cached) automatically
|
||||
from ``elfgum/RIFE-4.22.lite`` via ``maybe_download_model()``.
|
||||
"""
|
||||
from sglang.multimodal_gen.runtime.utils.hf_diffusers_utils import (
|
||||
maybe_download_model,
|
||||
)
|
||||
|
||||
model_path = self._model_path or _DEFAULT_RIFE_HF_REPO
|
||||
|
||||
# Resolve: local path pass-through, HF repo ID → download & cache
|
||||
model_path = maybe_download_model(model_path)
|
||||
|
||||
self._resolved_path = model_path
|
||||
|
||||
if model_path in _MODEL_CACHE:
|
||||
return _MODEL_CACHE[model_path]
|
||||
|
||||
device = current_platform.get_local_torch_device()
|
||||
model = Model()
|
||||
model.load_model(model_path, strip_module_prefix=True)
|
||||
model.eval()
|
||||
model.flownet = model.flownet.to(device)
|
||||
_MODEL_CACHE[model_path] = model
|
||||
logger.info("RIFE model loaded on device: %s", device)
|
||||
return model
|
||||
|
||||
@staticmethod
|
||||
def _frame_to_tensor(frame: np.ndarray, device: torch.device) -> torch.Tensor:
|
||||
"""Convert uint8 HWC numpy frame to float32 CHW tensor on device."""
|
||||
t = torch.from_numpy(frame).permute(2, 0, 1).unsqueeze(0).float() / 255.0
|
||||
return t.to(device)
|
||||
|
||||
@staticmethod
|
||||
def _tensor_to_frame(t: torch.Tensor) -> np.ndarray:
|
||||
"""Convert float32 CHW tensor (batch=1) to uint8 HWC numpy frame."""
|
||||
arr = t.squeeze(0).permute(1, 2, 0).clamp(0.0, 1.0).cpu().numpy()
|
||||
return (arr * 255.0).astype(np.uint8)
|
||||
|
||||
@staticmethod
|
||||
def _frames_to_tensor(
|
||||
frames: list[np.ndarray], device: torch.device
|
||||
) -> torch.Tensor:
|
||||
t = torch.from_numpy(np.stack(frames, axis=0))
|
||||
t = t.permute(0, 3, 1, 2).contiguous().float() / 255.0
|
||||
return t.to(device, non_blocking=True)
|
||||
|
||||
@staticmethod
|
||||
def _tensor_to_frames(t: torch.Tensor) -> list[np.ndarray]:
|
||||
arr = t.permute(0, 2, 3, 1).clamp(0.0, 1.0).cpu().numpy()
|
||||
arr = (arr * 255.0).astype(np.uint8)
|
||||
return [arr[i] for i in range(arr.shape[0])]
|
||||
|
||||
def _make_inference(
|
||||
self, model: Model, I0: torch.Tensor, I1: torch.Tensor, n: int, scale: float
|
||||
) -> list[torch.Tensor]:
|
||||
"""
|
||||
Recursively generate n-1 intermediate frames between I0 and I1.
|
||||
|
||||
Returns a list of intermediate frame tensors (not including I0 or I1).
|
||||
"""
|
||||
if n == 1:
|
||||
return [model.inference(I0, I1, scale=scale)]
|
||||
mid = model.inference(I0, I1, scale=scale)
|
||||
return (
|
||||
self._make_inference(model, I0, mid, n // 2, scale)
|
||||
+ [mid]
|
||||
+ self._make_inference(model, mid, I1, n // 2, scale)
|
||||
)
|
||||
|
||||
def _interpolate_2x_batched(
|
||||
self, model: Model, frames: list[np.ndarray], scale: float
|
||||
) -> list[np.ndarray]:
|
||||
device = model.device()
|
||||
source = self._frames_to_tensor(frames, device)
|
||||
intermediate_frames: list[np.ndarray] = []
|
||||
|
||||
with torch.inference_mode():
|
||||
for start in range(0, len(frames) - 1, _MAX_RIFE_BATCH_PAIRS):
|
||||
end = min(start + _MAX_RIFE_BATCH_PAIRS, len(frames) - 1)
|
||||
mids = model.inference(
|
||||
source[start:end],
|
||||
source[start + 1 : end + 1],
|
||||
scale=scale,
|
||||
)
|
||||
intermediate_frames.extend(self._tensor_to_frames(mids))
|
||||
|
||||
result: list[np.ndarray] = []
|
||||
for i, mid in enumerate(intermediate_frames):
|
||||
result.append(frames[i])
|
||||
result.append(mid)
|
||||
result.append(frames[-1])
|
||||
return result
|
||||
|
||||
def interpolate(
|
||||
self,
|
||||
frames: list[np.ndarray],
|
||||
exp: int = 1,
|
||||
scale: float = 1.0,
|
||||
) -> tuple[list[np.ndarray], int]:
|
||||
"""
|
||||
Interpolate frames using RIFE.
|
||||
|
||||
Args:
|
||||
frames: List of uint8 numpy arrays with shape [H, W, 3].
|
||||
exp: Exponent for interpolation factor. 1 → 2×, 2 → 4×.
|
||||
scale: RIFE inference scale. Use 0.5 for high-resolution inputs.
|
||||
|
||||
Returns:
|
||||
(interpolated_frames, multiplier) where multiplier = 2**exp.
|
||||
"""
|
||||
if len(frames) < 2:
|
||||
logger.warning(
|
||||
"Frame interpolation requires at least 2 frames; returning input unchanged."
|
||||
)
|
||||
return frames, 1
|
||||
|
||||
model = self._ensure_model_loaded()
|
||||
device = model.device()
|
||||
|
||||
n_intermediate = 2**exp // 2 # intermediates per adjacent pair
|
||||
start_time = time.perf_counter()
|
||||
|
||||
if n_intermediate == 1:
|
||||
result = self._interpolate_2x_batched(model, frames, scale)
|
||||
logger.info(
|
||||
"RIFE batched interpolation completed in %.3f seconds for %d frames",
|
||||
time.perf_counter() - start_time,
|
||||
len(frames),
|
||||
)
|
||||
return result, 2**exp
|
||||
|
||||
result: list[np.ndarray] = []
|
||||
for i in range(len(frames) - 1):
|
||||
I0 = self._frame_to_tensor(frames[i], device)
|
||||
I1 = self._frame_to_tensor(frames[i + 1], device)
|
||||
|
||||
intermediate_tensors = self._make_inference(
|
||||
model, I0, I1, n_intermediate, scale
|
||||
)
|
||||
|
||||
result.append(frames[i])
|
||||
for t in intermediate_tensors:
|
||||
result.append(self._tensor_to_frame(t))
|
||||
|
||||
result.append(frames[-1])
|
||||
multiplier = 2**exp
|
||||
logger.info(
|
||||
"RIFE interpolation completed in %.3f seconds for %d frames",
|
||||
time.perf_counter() - start_time,
|
||||
len(frames),
|
||||
)
|
||||
return result, multiplier
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Module-level convenience function
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def interpolate_video_frames(
|
||||
frames: list[np.ndarray],
|
||||
exp: int = 1,
|
||||
scale: float = 1.0,
|
||||
model_path: Optional[str] = None,
|
||||
) -> tuple[list[np.ndarray], int]:
|
||||
"""
|
||||
Convenience wrapper around FrameInterpolator.
|
||||
|
||||
Args:
|
||||
frames: List of uint8 HWC numpy frames.
|
||||
exp: Interpolation exponent (1=2×, 2=4×).
|
||||
scale: RIFE inference scale (default 1.0; use 0.5 for high-res).
|
||||
model_path: Local directory or HuggingFace repo ID containing
|
||||
``flownet.pkl``. *None* → default ``elfgum/RIFE-4.22.lite``.
|
||||
|
||||
Returns:
|
||||
(interpolated_frames, multiplier)
|
||||
"""
|
||||
interpolator = FrameInterpolator(model_path=model_path)
|
||||
return interpolator.interpolate(frames, exp=exp, scale=scale)
|
||||
Reference in New Issue
Block a user