atharvak30 commited on
Commit
2b0106d
Β·
verified Β·
1 Parent(s): 0681e9e

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +280 -0
  2. requirements.txt +8 -0
app.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ # AI Video Enhancer 4K - Gradio app for Hugging Face Spaces
3
+ # MCP-enabled version
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import tempfile
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Tuple
12
+
13
+ import gradio as gr
14
+ import spaces
15
+ import torch
16
+ import numpy as np
17
+ from PIL import Image
18
+ import cv2
19
+ from huggingface_hub import hf_hub_download
20
+
21
+ # Config
22
+ TEMP_DIR = Path(tempfile.gettempdir()) / "hf_video_enhancer"
23
+ TEMP_DIR.mkdir(parents=True, exist_ok=True)
24
+
25
+
26
+ def run_cmd(cmd):
27
+ p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
28
+ if p.returncode != 0:
29
+ raise RuntimeError(f"Command failed: {p.stderr.decode()}")
30
+ return p.stdout.decode()
31
+
32
+
33
+ def probe_video(video_path: str) -> Tuple[float, int, int, float]:
34
+ cmd = [
35
+ "ffprobe", "-v", "error",
36
+ "-select_streams", "v:0",
37
+ "-show_entries", "stream=width,height,duration,r_frame_rate",
38
+ "-of", "default=noprint_wrappers=1:nokey=0",
39
+ video_path
40
+ ]
41
+ p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
42
+ out = p.stdout.decode()
43
+ width = height = 0
44
+ duration = 0.0
45
+ fps = 30.0
46
+
47
+ for line in out.splitlines():
48
+ if line.startswith("width="):
49
+ width = int(line.split("=")[1])
50
+ elif line.startswith("height="):
51
+ height = int(line.split("=")[1])
52
+ elif line.startswith("duration="):
53
+ try:
54
+ duration = float(line.split("=")[1])
55
+ except:
56
+ pass
57
+ elif line.startswith("r_frame_rate="):
58
+ try:
59
+ fps_str = line.split("=")[1]
60
+ if "/" in fps_str:
61
+ num, den = fps_str.split("/")
62
+ fps = float(num) / float(den)
63
+ else:
64
+ fps = float(fps_str)
65
+ except:
66
+ pass
67
+
68
+ return duration, width, height, fps
69
+
70
+
71
+ def extract_frames(video_path: str, frames_dir: Path):
72
+ frames_dir.mkdir(parents=True, exist_ok=True)
73
+ run_cmd([
74
+ "ffmpeg", "-y", "-i", video_path,
75
+ "-vsync", "0",
76
+ str(frames_dir / "%06d.png")
77
+ ])
78
+
79
+
80
+ def reassemble_video(frames_dir: Path, audio_src: str, out_path: str, fps: float = 30.0):
81
+ tmp_video = str(frames_dir.parent / "tmp_video.mp4")
82
+ run_cmd([
83
+ "ffmpeg", "-y", "-framerate", str(fps),
84
+ "-i", str(frames_dir / "%06d.png"),
85
+ "-c:v", "libx264", "-preset", "veryfast", "-pix_fmt", "yuv420p",
86
+ "-crf", "18", tmp_video
87
+ ])
88
+
89
+ p = subprocess.run(
90
+ ["ffprobe", "-v", "error", "-select_streams", "a", "-show_entries",
91
+ "stream=codec_type", "-of", "default=noprint_wrappers=1", audio_src],
92
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE
93
+ )
94
+
95
+ if p.stdout.decode().strip():
96
+ run_cmd([
97
+ "ffmpeg", "-y", "-i", tmp_video, "-i", audio_src,
98
+ "-c:v", "copy", "-c:a", "aac",
99
+ "-map", "0:v:0", "-map", "1:a:0", out_path
100
+ ])
101
+ os.remove(tmp_video)
102
+ else:
103
+ shutil.move(tmp_video, out_path)
104
+
105
+
106
+ def simple_upscale(img: np.ndarray, scale: int) -> np.ndarray:
107
+ """Simple bicubic upscaling using OpenCV"""
108
+ h, w = img.shape[:2]
109
+ return cv2.resize(img, (w * scale, h * scale), interpolation=cv2.INTER_CUBIC)
110
+
111
+
112
+ @spaces.GPU(duration=120)
113
+ def enhance_with_realesrgan(frames_dir: str, scale: int = 4) -> int:
114
+ """
115
+ Enhance frames using Real-ESRGAN via Spandrel.
116
+ Separated function with GPU decorator for cleaner ZeroGPU handling.
117
+ """
118
+ from spandrel import ImageModelDescriptor, ModelLoader
119
+
120
+ frames_path = Path(frames_dir)
121
+ frame_files = sorted(frames_path.glob("*.png"))
122
+ total = len(frame_files)
123
+
124
+ if total == 0:
125
+ return 0
126
+
127
+ if scale == 2:
128
+ model_path = hf_hub_download(repo_id="ai-forever/Real-ESRGAN", filename="RealESRGAN_x2.pth")
129
+ else:
130
+ model_path = hf_hub_download(repo_id="ai-forever/Real-ESRGAN", filename="RealESRGAN_x4.pth")
131
+
132
+ model = ModelLoader().load_from_file(model_path)
133
+ assert isinstance(model, ImageModelDescriptor)
134
+
135
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
136
+ model = model.to(device).eval()
137
+
138
+ print(f"Model loaded on {device}, processing {total} frames...")
139
+
140
+ for idx, frame_path in enumerate(frame_files):
141
+ img = cv2.imread(str(frame_path))
142
+ if img is None:
143
+ continue
144
+
145
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
146
+
147
+ tensor = torch.from_numpy(img_rgb).permute(2, 0, 1).float().div(255.0)
148
+ tensor = tensor.unsqueeze(0).to(device)
149
+
150
+ with torch.no_grad():
151
+ output = model(tensor)
152
+
153
+ output = output.squeeze(0).cpu().clamp(0, 1).mul(255).byte()
154
+ output = output.permute(1, 2, 0).numpy()
155
+ output_bgr = cv2.cvtColor(output, cv2.COLOR_RGB2BGR)
156
+
157
+ cv2.imwrite(str(frame_path), output_bgr)
158
+
159
+ if (idx + 1) % 5 == 0:
160
+ print(f"Processed {idx + 1}/{total}")
161
+
162
+ return total
163
+
164
+
165
+ def process_video(video_file: str, scale: int = 4) -> Tuple[str, str]:
166
+ """
167
+ Upscale and enhance a video using Real-ESRGAN AI super-resolution.
168
+
169
+ Takes a low-resolution or standard video and outputs a sharper, higher-resolution
170
+ version using the Real-ESRGAN model. Audio is preserved. Processing is limited
171
+ to approximately 30 seconds of video due to GPU constraints.
172
+
173
+ Args:
174
+ video_file: Path to the input video file. Supported formats: mp4, avi, mov, mkv, webm.
175
+ scale: Upscaling factor. Use 2 for 2x resolution boost or 4 for 4x (default: 4).
176
+
177
+ Returns:
178
+ A tuple of (status_message, output_video_path). The status message describes
179
+ the resolution change (e.g. '480x270 β†’ 1920x1080'). The output_video_path
180
+ is the local path to the enhanced MP4 file.
181
+ """
182
+ if video_file is None:
183
+ return "⚠️ Please upload a video file.", None
184
+
185
+ ts = int(time.time() * 1000)
186
+ base_dir = TEMP_DIR / f"job_{ts}"
187
+ base_dir.mkdir(parents=True, exist_ok=True)
188
+ in_path = base_dir / "input_video"
189
+
190
+ try:
191
+ shutil.copy(video_file, in_path)
192
+ except Exception as e:
193
+ return f"Error: {e}", None
194
+
195
+ try:
196
+ duration, w, h, fps = probe_video(str(in_path))
197
+ except Exception as e:
198
+ shutil.rmtree(base_dir, ignore_errors=True)
199
+ return f"Error probing video: {e}", None
200
+
201
+ if duration <= 0:
202
+ shutil.rmtree(base_dir, ignore_errors=True)
203
+ return "Could not determine video duration.", None
204
+
205
+ max_frames = int(fps * 30)
206
+
207
+ print(f"Video: {w}x{h}, {duration:.1f}s, {fps:.1f}fps")
208
+
209
+ frames_dir = base_dir / "frames"
210
+ try:
211
+ extract_frames(str(in_path), frames_dir)
212
+ except Exception as e:
213
+ shutil.rmtree(base_dir, ignore_errors=True)
214
+ return f"Failed extracting frames: {e}", None
215
+
216
+ frame_files = sorted(frames_dir.glob("*.png"))
217
+ num_frames = len(frame_files)
218
+
219
+ if num_frames > max_frames:
220
+ print(f"Limiting from {num_frames} to {max_frames} frames")
221
+ for f in frame_files[max_frames:]:
222
+ f.unlink()
223
+ num_frames = max_frames
224
+
225
+ print(f"Processing {num_frames} frames...")
226
+
227
+ try:
228
+ enhanced = enhance_with_realesrgan(str(frames_dir), scale)
229
+ print(f"Enhanced {enhanced} frames")
230
+ except Exception as e:
231
+ print(f"Enhancement failed: {e}")
232
+ print("Using fallback bicubic upscaling...")
233
+ try:
234
+ for fp in sorted(frames_dir.glob("*.png")):
235
+ img = cv2.imread(str(fp))
236
+ if img is not None:
237
+ upscaled = simple_upscale(img, scale)
238
+ cv2.imwrite(str(fp), upscaled)
239
+ except Exception as e2:
240
+ shutil.rmtree(base_dir, ignore_errors=True)
241
+ return f"Enhancement failed: {e}", None
242
+
243
+ out_video = base_dir / "enhanced_output.mp4"
244
+ try:
245
+ reassemble_video(frames_dir, str(in_path), str(out_video), fps)
246
+ except Exception as e:
247
+ shutil.rmtree(base_dir, ignore_errors=True)
248
+ return f"Failed reassembling: {e}", None
249
+
250
+ shutil.rmtree(frames_dir, ignore_errors=True)
251
+
252
+ try:
253
+ _, out_w, out_h, _ = probe_video(str(out_video))
254
+ return f"βœ… Done! {w}x{h} β†’ {out_w}x{out_h}", str(out_video)
255
+ except:
256
+ return "βœ… Done!", str(out_video)
257
+
258
+
259
+ # Gradio UI
260
+ with gr.Blocks(title="AI Video Enhancer", theme=gr.themes.Soft()) as demo:
261
+ gr.Markdown("# 🎬 AI Video Enhancer")
262
+ gr.Markdown("Upscale videos using Real-ESRGAN AI enhancement.")
263
+
264
+ gr.LoginButton()
265
+
266
+ with gr.Row():
267
+ with gr.Column(scale=2):
268
+ video_in = gr.File(label="Upload video", file_types=[".mp4", ".avi", ".mov", ".mkv", ".webm"])
269
+ scale_choice = gr.Radio(choices=[2, 4], value=4, label="Upscale Factor")
270
+ btn = gr.Button("πŸš€ Enhance", variant="primary")
271
+ status = gr.Textbox(label="Status", interactive=False)
272
+ with gr.Column(scale=1):
273
+ out_video = gr.Video(label="Result")
274
+
275
+ gr.Markdown("**Note:** Limited to ~30 seconds for ZeroGPU. Longer videos will be truncated.")
276
+
277
+ btn.click(fn=process_video, inputs=[video_in, scale_choice], outputs=[status, out_video])
278
+
279
+ if __name__ == "__main__":
280
+ demo.launch(mcp_server=True)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio[mcp]
2
+ spaces
3
+ torch
4
+ numpy
5
+ Pillow
6
+ opencv-python-headless
7
+ huggingface_hub
8
+ spandrel