File size: 10,262 Bytes
ee69ab7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39488f9
ee69ab7
39488f9
 
 
ee69ab7
 
 
 
39488f9
ee69ab7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a167323
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee69ab7
a167323
ee69ab7
a167323
 
 
ee69ab7
 
 
a167323
 
ee69ab7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39488f9
ee69ab7
 
 
39488f9
ee69ab7
 
 
 
 
 
 
 
 
 
 
 
39488f9
ee69ab7
39488f9
ee69ab7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39488f9
 
 
ee69ab7
 
 
 
 
 
 
 
 
 
39488f9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
"""FlashVSR+ — one-step video super-resolution demo (turbo preset).

Loads the FlashVSR+ turbo checkpoint (hybrid sliding attention + Triton
block-sparse stream attention + TinyDecoder) and super-resolves an uploaded
video to a target resolution that the user picks. The requested height/width
is the OUTPUT resolution: the input is center-cropped and resized to it, and
the output video is written at that size with the input's FPS (and audio,
when present).
"""

import spaces  # noqa: F401  # must precede any torch / CUDA import
import os
import shutil
import tempfile
import time

import gradio as gr
import torch
from huggingface_hub import snapshot_download

WEIGHTS = snapshot_download(
    "ruchangyao/FlashVSR_plus",
    allow_patterns=["turbo/*", "TCDecoder.ckpt"],
)
CONTEXT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "prompt_tensor", "posi_prompt.pth")

from inference.presets import stream_config_for_mode  # noqa: E402
from inference.loading import ModelPaths  # noqa: E402
from inference.pipeline import build_inference_pipeline  # noqa: E402
from inference.request import InferenceRequest  # noqa: E402

# Output resolutions (both sides divisible by 128 — a hard requirement).
RESOLUTIONS = [
    ("1024 × 1920 (README base example)", 1024, 1920),
    ("512 × 896", 512, 896),
    ("384 × 640", 384, 640),
    ("2048 × 3840 (4K, README turbo example)", 2048, 3840),
    ("2176 × 3840 (4K, README incremental example)", 2176, 3840),
]

DEFAULT_HEIGHT, DEFAULT_WIDTH = 512, 896

# Incremental input/decoding keeps RAM bounded at high resolutions: only one
# decode segment of frames is resident at a time (README's 4K path).
DECODE_EVERY = 28


def _build_pipeline():
    config = stream_config_for_mode(
        "turbo",
        device="cuda",
        seed=2026,
        decode_every=DECODE_EVERY,
        temporal_compression=4,
        attn_kernel="auto",
        color_correction=False,
    )
    paths = ModelPaths(
        dit=os.path.join(WEIGHTS, "turbo", "dit.safetensors"),
        lq_projection=os.path.join(WEIGHTS, "turbo", "lq_proj_in.safetensors"),
        context=CONTEXT_PATH,
        tiny_decoder=os.path.join(WEIGHTS, "TCDecoder.ckpt"),
    )
    return build_inference_pipeline(config, paths)


print("Loading FlashVSR+ turbo weights (~7 GB) ...", flush=True)
t_start = time.perf_counter()
pipeline = _build_pipeline()
print(f"FlashVSR+ pipeline ready in {time.perf_counter() - t_start:.1f}s", flush=True)


# Measured warm-worker GPU cost on ZeroGPU (turbo preset, compression 4):
# (megapixels, fixed seconds, per-frame seconds). Runtime is dominated by a
# fixed per-resolution cost up to ~1 MPix, then a per-frame term that grows
# with resolution (the TinyDecoder decode segments get expensive at 4K).
#   384x640 / 65f  -> 5.6 s    512x896 / 33,97f -> 9.7, 10.0 s
#   1024x1920 / 33f -> 15.4 s  2048x3840 / 33,97f -> 53.3, 112.3 s
_PIXEL_ANCHORS = [
    (384 * 640 / 1e6, 6.3, 0.002),
    (512 * 896 / 1e6, 10.2, 0.008),
    (1024 * 1920 / 1e6, 14.0, 0.073),
    (2048 * 3840 / 1e6, 22.9, 0.922),
    (2176 * 3840 / 1e6, 24.5, 0.98),
]


def _gpu_cost_seconds(height: int, width: int, frames: int, compression: int) -> float:
    """Interpolate the measured GPU cost of one upscale call, in seconds."""
    pixels = height * width / 1e6
    fixed, per_frame = _PIXEL_ANCHORS[-1][1], _PIXEL_ANCHORS[-1][2]
    for i in range(len(_PIXEL_ANCHORS) - 1):
        p0, f0, pf0 = _PIXEL_ANCHORS[i]
        p1, f1, pf1 = _PIXEL_ANCHORS[i + 1]
        if pixels <= p1:
            span = (pixels - p0) / (p1 - p0)
            fixed = f0 + (f1 - f0) * span
            per_frame = pf0 + (pf1 - pf0) * span
            break
    # Compression 1 measured ~1.34x the cost of compression 4 at 512x896;
    # (4/c)^0.25 = 1.41 at c=1, a close, slightly conservative proxy.
    estimate = (fixed + per_frame * frames) * (4.0 / compression) ** 0.25
    # 15% slack + 2 s covers run-to-run noise and a cold worker streaming
    # weights into VRAM (measured cold-vs-warm delta was < 1 s).
    return min(max(estimate * 1.15 + 2.0, 12.0), 300.0)


def _estimate_duration(video, resolution, temporal_compression, color_correction, max_frames, *args, **kwargs):
    """Duration callable for @spaces.GPU: scales with measured cost.

    ZeroGPU calls this with the exact request arguments before scheduling, so
    the quota charged to a visitor tracks what their request really costs —
    calibrated from smoke tests on this Space's hardware.
    """
    del video, color_correction, args, kwargs
    _, height, width = RESOLUTIONS[resolution]
    frames = max(int(max_frames), 17) if max_frames else 128
    return round(_gpu_cost_seconds(height, width, frames, int(temporal_compression)))


@spaces.GPU(duration=_estimate_duration)
def upscale_video(
    video,
    resolution: int,
    temporal_compression: int,
    color_correction: bool,
    max_frames: int,
    progress: gr.Progress = gr.Progress(),
):
    """Super-resolve a video with FlashVSR+ (one-step, turbo preset).

    Args:
        video: Input video path (any resolution/FPS; audio is preserved).
        resolution: Index into RESOLUTIONS; the picked height/width is the
            OUTPUT resolution (input is center-cropped and resized to it).
        temporal_compression: Input frames per latent — 4 (fast, default),
            2 or 1 for large motion (slower).
        color_correction: Match output low-frequency color to the LQ input.
        max_frames: Cap on input frames processed (minimum 9 for one latent).

    Returns:
        The super-resolved MP4 path.
    """
    if video is None:
        raise gr.Error("Please upload or pick an input video first.")
    _, height, width = RESOLUTIONS[resolution]

    def report(message: str) -> None:
        progress(0.5, desc=message)

    frame_cap = max(int(max_frames), 17)
    # Worker threads fork COW from this process; config is rebuilt per call to
    # keep the fields deterministic (seed/compression are request-scoped).
    pipeline.config.seed = 2026
    pipeline.config.temporal_compression = int(temporal_compression)
    pipeline.config.decoder.decode_every = DECODE_EVERY
    if color_correction:
        from inference.postprocessing import WaveletColorCorrector

        if not isinstance(pipeline.color_corrector, WaveletColorCorrector):
            pipeline.color_corrector = WaveletColorCorrector(levels=5, chunk_size=16)
    else:
        pipeline.color_corrector = None

    out_dir = tempfile.mkdtemp(prefix="flashvsr_out_")
    output_path = os.path.join(out_dir, f"flashvsr_{width}x{height}.mp4")
    try:
        result = pipeline.infer(
            InferenceRequest(
                input_path=video,
                output_path=output_path,
                height=height,
                width=width,
                max_frames=frame_cap,
            ),
            progress=report,
            video_preset="veryfast",
        )
    except Exception as error:
        shutil.rmtree(out_dir, ignore_errors=True)
        raise gr.Error(f"Super-resolution failed: {error}") from error
    progress(1.0, desc="Done")
    return result.output_path


WARNING_MD = (
    "**Heads-up:** higher resolutions and lower temporal compression cost "
    "real GPU time. Start with the default (512 × 896, compression 4) or use "
    "the examples; 4K targets can take minutes on ZeroGPU."
)

with gr.Blocks(title="FlashVSR+ Video Super-Resolution") as demo:
    gr.Markdown(
        "# ⚡ FlashVSR+ — Efficient Video Super-Resolution\n"
        "One-step diffusion VSR with hybrid attention and a TinyDecoder. "
        "Pick a target **output** resolution; your video is center-cropped, "
        "resized, and super-resolved to that size at the input's FPS (audio kept)."
    )
    with gr.Row():
        with gr.Column():
            video_in = gr.Video(label="Input video (low resolution)", sources=["upload"])
            resolution = gr.Dropdown(
                choices=[r[0] for r in RESOLUTIONS],
                type="index",
                value=RESOLUTIONS[1][0],
                label="Output resolution",
                info="Target output size (both sides must be multiples of 128); input is center-cropped and resized to it",
            )
            with gr.Accordion("Advanced options", open=False):
                temporal_compression = gr.Radio(
                    choices=[("4 (fast, default)", 4), ("2 (large motion)", 2), ("1 (largest motion, slowest)", 1)],
                    value=4,
                    type="value",
                    label="Temporal compression",
                    info="Input frames per latent frame — lower preserves motion detail at the cost of speed",
                )
                color_correction = gr.Checkbox(
                    value=False,
                    label="Match low-frequency color to input",
                )
                max_frames = gr.Slider(
                    minimum=17,
                    maximum=129,
                    value=97,
                    step=4,
                    label="Max input frames",
                    info="Caps processing time; long videos are truncated to this many frames (17 ≈ 1 latent chunk, minimum)",
                )
            submit = gr.Button("Super-resolve", variant="primary")
            gr.Markdown(WARNING_MD)
        with gr.Column():
            video_out = gr.Video(label="Super-resolved video", autoplay=False)

    submit.click(
        fn=upscale_video,
        inputs=[video_in, resolution, temporal_compression, color_correction, max_frames],
        outputs=video_out,
        api_name="upscale",
    )

    gr.Examples(
        examples=[
            ["examples/case_1.mp4", RESOLUTIONS[1][0], 4, False, 33],
            ["examples/case_0.mp4", RESOLUTIONS[1][0], 4, False, 97],
            ["examples/case_2.mp4", RESOLUTIONS[2][0], 4, False, 65],
        ],
        inputs=[video_in, resolution, temporal_compression, color_correction, max_frames],
        outputs=video_out,
        fn=upscale_video,
        cache_examples=True,
        cache_mode="lazy",
        run_on_click=True,
        label="Example inputs (from the FlashVSR+ repo)",
    )

demo.launch(theme=gr.themes.Citrus(), mcp_server=True)