File size: 7,473 Bytes
fbaeba7
 
 
 
 
 
 
b9917c2
 
 
fbaeba7
 
b9917c2
4d02e5a
fbaeba7
 
 
 
 
4d02e5a
fbaeba7
4d02e5a
 
 
fbaeba7
 
4d02e5a
 
 
fbaeba7
4d02e5a
 
 
 
 
 
fbaeba7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9917c2
 
fbaeba7
 
4d02e5a
 
 
 
884f557
fbaeba7
 
15bf580
fbaeba7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d02e5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fbaeba7
4d02e5a
fbaeba7
b9917c2
 
 
fbaeba7
 
 
 
 
 
 
b9917c2
fbaeba7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15bf580
fbaeba7
15bf580
fbaeba7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15bf580
fbaeba7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9917c2
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
"""SwiftVR: Real-Time One-Step Generative Video Restoration.

Gradio Space demo that loads the SwiftVR model from H-oliday/SwiftVR on the
Hugging Face Hub and restores / upscales user-uploaded videos in real time.
"""

import os
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")

import spaces
import sys
import tempfile
import traceback
import math

# Make the bundled swiftvr package importable
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

import torch
import torch.nn.functional as F
import gradio as gr
import numpy as np
import imageio
from PIL import Image
from huggingface_hub import snapshot_download

import decord
decord.bridge.set_bridge("torch")

from swiftvr import SwiftVRPipeline
from swiftvr.io import (
    get_video_info, iter_video_clips_fixed_scheme,
    preprocess_clip_uint8, crop_spatial_padding_ntchw,
    ntchw_to_uint8_frames, open_stream_video_writer,
)
from swiftvr.streaming.chunk import ChunkType

CHECKPOINT_REPO = "H-oliday/SwiftVR"
CHECKPOINT_DIR = "/tmp/swiftvr_checkpoints"

# --- Load model at module scope (ZeroGPU rule) --- #

os.makedirs(CHECKPOINT_DIR, exist_ok=True)

print("Downloading SwiftVR checkpoints...")
snapshot_download(
    repo_id=CHECKPOINT_REPO,
    local_dir=CHECKPOINT_DIR,
    repo_type="model",
)
print("Checkpoints downloaded. Loading model...")

pipe = SwiftVRPipeline.from_pretrained(CHECKPOINT_DIR)
pipe.to("cuda", dtype="bfloat16", attention_backend="sdpa")
print("SwiftVR model loaded and moved to CUDA with SDPA attention.")


def _aligned_pad(size, multiple=32):
    return (multiple - size % multiple) % multiple


@spaces.GPU(duration=90)
def restore_video(
    input_video: str,
    upscale: int = 2,
    clip_len: int = 24,
    quality: int = 60,
):
    """Restore a low-quality video using SwiftVR.

    Args:
        input_video: Path to the input low-quality video file.
        upscale: Upscale factor (2, 4, or 8).
        clip_len: Processing chunk size (must be multiple of 4).
        quality: Output quality 0-100 (maps to x265 CRF).

    Returns:
        Path to the restored output video, or an error message.
    """
    if input_video is None:
        return None, "Please upload or select a video first."

    tmp_dir = tempfile.mkdtemp()
    output_path = os.path.join(tmp_dir, "restored.mp4")

    try:
        # Single-threaded pipeline to avoid ZeroGPU fork conflicts
        device = pipe.device
        dtype = pipe.dtype

        # Get video info
        raw_total, lq_h, lq_w, src_fps = get_video_info(input_video, fallback_fps=30)
        total_frames = 4 * ((raw_total - 1) // 4) + 1

        # Compute target size
        out_h = lq_h * upscale
        out_w = lq_w * upscale
        pad_h = _aligned_pad(out_h)
        pad_w = _aligned_pad(out_w)

        # Reset streaming state
        pipe.tae_stream.reset()
        pipe.dit_stream.reset()
        pipe.dit_stream.overlap = 0

        n_lat = clip_len // 4
        prev_dit_out_cpu = None

        # Open video writer
        writer = open_stream_video_writer(
            output_path, fps=src_fps, video_format="",
            preset="", quality=quality)

        # Process chunks (single-threaded, no pipeline threads)
        clips = iter_video_clips_fixed_scheme(
            input_video, clip_len=clip_len,
            total_frames=total_frames,
            crop_h=lq_h, crop_w=lq_w)

        total_written = 0
        for spec, cpu_rgb in clips:
            # Move to GPU
            gpu_rgb = cpu_rgb.to(device=device)

            # Preprocess
            clip_rgb = preprocess_clip_uint8(
                gpu_rgb, out_h=out_h, out_w=out_w,
                mode=pipe.upscale_mode, pad_h=pad_h, pad_w=pad_w,
                dtype=dtype)

            # Encode
            z = pipe.tae_stream.encode_chunk_fixed(clip_rgb, spec)

            # Denoise
            if spec.ctype == ChunkType.LAST:
                z_ntchw = pipe.dit_stream.denoise_last_chunk(
                    z, spec, pipe.prompt_emb, prev_dit_out_cpu,
                    n_lat, device, dtype)
            else:
                z_bcfhw = z.permute(0, 2, 1, 3, 4).contiguous()
                z_den = pipe.dit_stream.denoise(z_bcfhw, pipe.prompt_emb)
                z_ntchw = z_den.permute(0, 2, 1, 3, 4).contiguous()
                prev_dit_out_cpu = z_bcfhw[:, :, -n_lat:].detach().cpu().clone()

            # Decode
            rgb_out = pipe.tae_stream.decode_chunk_fixed(z_ntchw, spec)
            if rgb_out is not None and rgb_out.shape[1] > 0:
                rgb_out = crop_spatial_padding_ntchw(rgb_out, pad_h, pad_w).detach()
                frames = ntchw_to_uint8_frames(rgb_out)
                if frames is not None:
                    for frame in frames:
                        writer.append_data(frame)
                    total_written += frames.shape[0]

            # Clean up GPU memory
            del gpu_rgb, clip_rgb, z, z_ntchw, rgb_out
            torch.cuda.empty_cache()

        writer.close()
        msg = f"Restored {total_written} frames from {raw_total} input frames at {src_fps:.1f} fps"
        return output_path, msg

    except Exception as e:
        err_msg = f"Error: {e}\n{traceback.format_exc()}"
        print(err_msg)
        return None, err_msg


CSS = """
#col-container { max-width: 1100px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
"""

with gr.Blocks() as demo:
    with gr.Column(elem_id="col-container"):
        gr.Markdown(
            """
            # SwiftVR: Real-Time One-Step Generative Video Restoration
            Upload a low-quality video to restore and upscale it in real time using the SwiftVR model.
            [Paper](https://arxiv.org/abs/2606.09516) | [GitHub](https://github.com/H-oliday/SwiftVR) | [Model](https://huggingface.co/H-oliday/SwiftVR)
            """
        )

        with gr.Row():
            input_video = gr.Video(label="Input Video (low quality)", sources=["upload"])
            output_video = gr.Video(label="Restored Video", interactive=False)

        status = gr.Textbox(label="Status", interactive=False)

        with gr.Accordion("Advanced Settings", open=False):
            upscale = gr.Slider(
                minimum=1, maximum=4, step=1, value=2,
                label="Upscale Factor",
                info="How many times to upscale the input video resolution. Use 2 for faster processing."
            )
            clip_len = gr.Slider(
                minimum=4, maximum=48, step=4, value=24,
                label="Clip Length",
                info="Processing chunk size (must be multiple of 4). Larger = more context, slower."
            )
            quality = gr.Slider(
                minimum=0, maximum=100, step=5, value=60,
                label="Output Quality",
                info="0-100, maps to x265 CRF. Higher = better quality, larger file."
            )

        run_btn = gr.Button("Restore Video", variant="primary")

    gr.Examples(
        examples=[
            ["examples/cat.mp4", 2, 24, 60],
        ],
        inputs=[input_video, upscale, clip_len, quality],
        outputs=[output_video, status],
        fn=restore_video,
        cache_examples=True,
        cache_mode="lazy",
    )

    run_btn.click(
        fn=restore_video,
        inputs=[input_video, upscale, clip_len, quality],
        outputs=[output_video, status],
    )

if __name__ == "__main__":
    demo.launch(mcp_server=True, show_error=True)