Spaces:
Running on Zero
Running on Zero
| import os | |
| import subprocess | |
| import tempfile | |
| import cv2 | |
| import gradio as gr | |
| import numpy as np | |
| import spaces | |
| import torch | |
| import torch.nn.functional as F | |
| from huggingface_hub import hf_hub_download | |
| from imageio_ffmpeg import get_ffmpeg_exe | |
| from train_log.IFNet_HDv3 import IFNet | |
| from upsampler_theme import UPSAMPLER_THEME, UPSAMPLER_CSS, footer_html, header_html | |
| # Official Practical-RIFE v4.25 weights (MIT), mirrored unchanged on our org | |
| # so no third-party repo can drift underneath the Space. | |
| WEIGHTS = hf_hub_download("Upsampler/rife-4-25", "flownet.pkl") | |
| MAX_SECONDS = 6.5 | |
| MAX_SIDE = 1280 | |
| # Consecutive frames more different than this (mean abs diff on gray | |
| # thumbnails) are treated as a hard cut: duplicate instead of interpolate, | |
| # so cuts don't produce ghost blends. | |
| SCENE_CUT_DIFF = 60.0 | |
| flownet = IFNet() | |
| state = torch.load(WEIGHTS, map_location="cpu") | |
| flownet.load_state_dict( | |
| {k.replace("module.", ""): v for k, v in state.items() if "module." in k}, | |
| strict=False, | |
| ) | |
| flownet = flownet.to("cuda").eval() | |
| def probe(path): | |
| cap = cv2.VideoCapture(path) | |
| fps = cap.get(cv2.CAP_PROP_FPS) or 30 | |
| n = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) | |
| w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) | |
| h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |
| cap.release() | |
| return fps, n, w, h | |
| def get_duration(video, factor, progress=None): | |
| if not video: | |
| return 10 | |
| try: | |
| fps, n, w, h = probe(video) | |
| except Exception: | |
| return 60 | |
| mult = int(factor[0]) if factor else 2 | |
| # ~35ms per generated frame at 720p on the ZeroGPU slice, plus decode, | |
| # encode, and model-to-GPU overhead; never over-request. | |
| est = 20 + int(n * (mult - 1) * 0.06 * max(1.0, (w * h) / (1280 * 720))) | |
| return min(110, est) | |
| def midpoint(img0, img1): | |
| imgs = torch.cat((img0, img1), 1) | |
| scale_list = [16, 8, 4, 2, 1] | |
| _, _, merged = flownet(imgs, 0.5, scale_list) | |
| return merged[-1] | |
| def interpolate(video, factor, progress=gr.Progress()): | |
| if not video: | |
| raise gr.Error("Please upload a video first.") | |
| fps, n, w, h = probe(video) | |
| seconds = n / max(fps, 1) | |
| if seconds > MAX_SECONDS: | |
| raise gr.Error( | |
| f"This clip is {seconds:.1f}s; the free tool accepts up to {MAX_SECONDS:.0f}s. " | |
| "Trim it first and try again." | |
| ) | |
| if max(w, h) > MAX_SIDE: | |
| raise gr.Error( | |
| f"This clip is {w}x{h}; the free tool accepts up to {MAX_SIDE}px on the longest side." | |
| ) | |
| mult = int(factor[0]) | |
| cap = cv2.VideoCapture(video) | |
| frames = [] | |
| ok, frame = cap.read() | |
| while ok: | |
| frames.append(frame) | |
| ok, frame = cap.read() | |
| cap.release() | |
| if len(frames) < 2: | |
| raise gr.Error("Could not read enough frames from this video.") | |
| ph = ((h - 1) // 64 + 1) * 64 | |
| pw = ((w - 1) // 64 + 1) * 64 | |
| def to_tensor(frame): | |
| t = torch.from_numpy(frame).to("cuda", non_blocking=True) | |
| t = t.permute(2, 0, 1).float().unsqueeze(0) / 255.0 | |
| return F.pad(t, (0, pw - w, 0, ph - h)) | |
| def to_frame(t): | |
| out = (t[0][:, :h, :w].permute(1, 2, 0) * 255.0).clamp(0, 255) | |
| return out.byte().cpu().numpy() | |
| thumbs = [ | |
| cv2.cvtColor(cv2.resize(f, (64, 36)), cv2.COLOR_BGR2GRAY).astype(np.float32) | |
| for f in frames | |
| ] | |
| out_path = os.path.join(tempfile.mkdtemp(), "interpolated.mp4") | |
| ffmpeg = get_ffmpeg_exe() | |
| enc = subprocess.Popen( | |
| [ | |
| ffmpeg, "-y", "-f", "rawvideo", "-pix_fmt", "bgr24", | |
| "-s", f"{w}x{h}", "-r", str(fps * mult), "-i", "-", | |
| "-i", video, "-map", "0:v", "-map", "1:a?", "-c:a", "copy", | |
| "-c:v", "libx264", "-preset", "fast", "-crf", "18", | |
| "-pix_fmt", "yuv420p", "-shortest", out_path, | |
| ], | |
| stdin=subprocess.PIPE, | |
| stderr=subprocess.DEVNULL, | |
| ) | |
| with torch.inference_mode(): | |
| for i in range(len(frames) - 1): | |
| progress((i + 1) / len(frames), desc="Interpolating frames") | |
| enc.stdin.write(frames[i].tobytes()) | |
| is_cut = float(np.abs(thumbs[i] - thumbs[i + 1]).mean()) > SCENE_CUT_DIFF | |
| if is_cut: | |
| for _ in range(mult - 1): | |
| enc.stdin.write(frames[i].tobytes()) | |
| continue | |
| t0, t1 = to_tensor(frames[i]), to_tensor(frames[i + 1]) | |
| if mult == 2: | |
| enc.stdin.write(to_frame(midpoint(t0, t1)).tobytes()) | |
| else: # 4x: recursive midpoints -> quarter timesteps | |
| mid = midpoint(t0, t1) | |
| for t in (midpoint(t0, mid), mid, midpoint(mid, t1)): | |
| enc.stdin.write(to_frame(t).tobytes()) | |
| enc.stdin.write(frames[-1].tobytes()) | |
| enc.stdin.close() | |
| if enc.wait() != 0: | |
| raise gr.Error("Video encoding failed. Please try a different clip.") | |
| return out_path | |
| with gr.Blocks(theme=UPSAMPLER_THEME, css=UPSAMPLER_CSS) as demo: | |
| gr.HTML( | |
| header_html( | |
| "RIFE Video Frame Interpolation", | |
| "Double or quadruple your video's frame rate for smooth slow motion", | |
| ) | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| input_video = gr.Video(label="Video (up to 6s, 1280px)") | |
| factor = gr.Radio(["2x frames", "4x frames"], value="2x frames", label="Interpolation") | |
| run = gr.Button("Interpolate", variant="primary") | |
| with gr.Column(scale=1): | |
| output_video = gr.Video(label="Result", autoplay=True) | |
| run.click(interpolate, inputs=[input_video, factor], outputs=output_video, api_name="interpolate") | |
| gr.HTML( | |
| footer_html( | |
| "RIFE (Real-Time Intermediate Flow Estimation, Practical-RIFE v4.25) " | |
| "generates new in-between frames for any video, turning choppy clips " | |
| "into smooth slow motion and converting frame rates like 30fps to " | |
| "60fps or 120fps, with scene-cut detection to avoid ghosting.", | |
| "https://upsampler.com/free-video-frame-interpolation-no-signup", | |
| "free video frame interpolation tool", | |
| ) | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(ssr_mode=False) | |