atharvak30 commited on
Commit
64fa463
·
verified ·
1 Parent(s): 46c1bd4

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +301 -0
app.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ # AI Video Enhancer 4K - Optimized single GPU call with in-memory processing
3
+
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ import tempfile
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Tuple
11
+
12
+ import gradio as gr
13
+ import spaces
14
+ import torch
15
+ import numpy as np
16
+ from PIL import Image
17
+ import cv2
18
+ from huggingface_hub import hf_hub_download
19
+ from spandrel import ImageModelDescriptor, ModelLoader
20
+
21
+ # Config
22
+ TEMP_DIR = Path(tempfile.gettempdir()) / "hf_video_enhancer"
23
+ TEMP_DIR.mkdir(parents=True, exist_ok=True)
24
+
25
+ # Pre-download models at startup
26
+ MODEL_PATHS = {}
27
+
28
+ def ensure_model(scale: int) -> str:
29
+ """Download model weights (CPU/network only)."""
30
+ if scale not in MODEL_PATHS:
31
+ if scale == 2:
32
+ MODEL_PATHS[scale] = hf_hub_download(
33
+ repo_id="ai-forever/Real-ESRGAN", filename="RealESRGAN_x2.pth"
34
+ )
35
+ else:
36
+ MODEL_PATHS[scale] = hf_hub_download(
37
+ repo_id="ai-forever/Real-ESRGAN", filename="RealESRGAN_x4.pth"
38
+ )
39
+ return MODEL_PATHS[scale]
40
+
41
+ try:
42
+ ensure_model(2)
43
+ ensure_model(4)
44
+ print("Models pre-downloaded successfully.")
45
+ except Exception as e:
46
+ print(f"Model pre-download skipped: {e}")
47
+
48
+
49
+ def run_cmd(cmd):
50
+ p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
51
+ if p.returncode != 0:
52
+ raise RuntimeError(f"Command failed: {p.stderr.decode()}")
53
+ return p.stdout.decode()
54
+
55
+
56
+ def probe_video(video_path: str) -> Tuple[float, int, int, float]:
57
+ cmd = [
58
+ "ffprobe", "-v", "error",
59
+ "-select_streams", "v:0",
60
+ "-show_entries", "stream=width,height,duration,r_frame_rate",
61
+ "-of", "default=noprint_wrappers=1:nokey=0",
62
+ video_path
63
+ ]
64
+ p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
65
+ out = p.stdout.decode()
66
+ width = height = 0
67
+ duration = 0.0
68
+ fps = 30.0
69
+
70
+ for line in out.splitlines():
71
+ if line.startswith("width="):
72
+ width = int(line.split("=")[1])
73
+ elif line.startswith("height="):
74
+ height = int(line.split("=")[1])
75
+ elif line.startswith("duration="):
76
+ try:
77
+ duration = float(line.split("=")[1])
78
+ except:
79
+ pass
80
+ elif line.startswith("r_frame_rate="):
81
+ try:
82
+ fps_str = line.split("=")[1]
83
+ if "/" in fps_str:
84
+ num, den = fps_str.split("/")
85
+ fps = float(num) / float(den)
86
+ else:
87
+ fps = float(fps_str)
88
+ except:
89
+ pass
90
+
91
+ return duration, width, height, fps
92
+
93
+
94
+ def extract_frames(video_path: str, frames_dir: Path, max_frames: int = None):
95
+ frames_dir.mkdir(parents=True, exist_ok=True)
96
+ cmd = ["ffmpeg", "-y", "-i", video_path, "-vsync", "0"]
97
+ if max_frames:
98
+ cmd.extend(["-vframes", str(max_frames)])
99
+ cmd.append(str(frames_dir / "%06d.png"))
100
+ run_cmd(cmd)
101
+
102
+
103
+ def reassemble_video(frames_dir: Path, audio_src: str, out_path: str, fps: float = 30.0):
104
+ tmp_video = str(frames_dir.parent / "tmp_video.mp4")
105
+ run_cmd([
106
+ "ffmpeg", "-y", "-framerate", str(fps),
107
+ "-i", str(frames_dir / "%06d.png"),
108
+ "-c:v", "libx264", "-preset", "veryfast", "-pix_fmt", "yuv420p",
109
+ "-crf", "18", tmp_video
110
+ ])
111
+
112
+ p = subprocess.run(
113
+ ["ffprobe", "-v", "error", "-select_streams", "a", "-show_entries",
114
+ "stream=codec_type", "-of", "default=noprint_wrappers=1", audio_src],
115
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE
116
+ )
117
+
118
+ if p.stdout.decode().strip():
119
+ run_cmd([
120
+ "ffmpeg", "-y", "-i", tmp_video, "-i", audio_src,
121
+ "-c:v", "copy", "-c:a", "aac",
122
+ "-map", "0:v:0", "-map", "1:a:0", out_path
123
+ ])
124
+ os.remove(tmp_video)
125
+ else:
126
+ shutil.move(tmp_video, out_path)
127
+
128
+
129
+ def simple_upscale(img: np.ndarray, scale: int) -> np.ndarray:
130
+ h, w = img.shape[:2]
131
+ return cv2.resize(img, (w * scale, h * scale), interpolation=cv2.INTER_CUBIC)
132
+
133
+
134
+ def load_frames_to_memory(frames_dir: Path) -> list:
135
+ """Load all frames into RAM (CPU work, not billed)."""
136
+ frame_files = sorted(frames_dir.glob("*.png"))
137
+ frames = []
138
+ for fp in frame_files:
139
+ img = cv2.imread(str(fp))
140
+ if img is not None:
141
+ frames.append(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
142
+ return frames
143
+
144
+
145
+ def save_frames_from_memory(frames: list, frames_dir: Path):
146
+ """Write enhanced frames back to disk (CPU work, not billed)."""
147
+ for idx, img_rgb in enumerate(frames):
148
+ out_path = frames_dir / f"{idx + 1:06d}.png"
149
+ cv2.imwrite(str(out_path), cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR))
150
+
151
+
152
+ @spaces.GPU(duration=180)
153
+ def enhance_all_frames_gpu(frames_rgb: list, model_path: str, scale: int = 4) -> list:
154
+ """
155
+ Enhance ALL frames in a SINGLE GPU call.
156
+ Frames are already in memory (no disk I/O here).
157
+ Uses torch.inference_mode() for faster inference with identical quality.
158
+ """
159
+ model = ModelLoader().load_from_file(model_path)
160
+ assert isinstance(model, ImageModelDescriptor)
161
+
162
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
163
+ model = model.to(device).eval()
164
+
165
+ total = len(frames_rgb)
166
+ print(f"Model loaded on {device}, processing {total} frames in memory...")
167
+
168
+ enhanced = []
169
+ with torch.inference_mode():
170
+ for idx, img_rgb in enumerate(frames_rgb):
171
+ tensor = torch.from_numpy(img_rgb).permute(2, 0, 1).float().div(255.0)
172
+ tensor = tensor.unsqueeze(0).to(device)
173
+
174
+ output = model(tensor)
175
+ output = output.squeeze(0).cpu().clamp(0, 1).mul(255).byte()
176
+ output = output.permute(1, 2, 0).numpy()
177
+ enhanced.append(output)
178
+
179
+ if (idx + 1) % 10 == 0:
180
+ print(f"Processed {idx + 1}/{total}")
181
+
182
+ return enhanced
183
+
184
+
185
+ def process_video(video_file, scale: int = 4, progress=gr.Progress()) -> Tuple[str, str]:
186
+ """Main video processing - single GPU call with in-memory frame processing."""
187
+ if video_file is None:
188
+ return "Please upload a video file.", None
189
+
190
+ ts = int(time.time() * 1000)
191
+ base_dir = TEMP_DIR / f"job_{ts}"
192
+ base_dir.mkdir(parents=True, exist_ok=True)
193
+ in_path = base_dir / "input_video"
194
+
195
+ try:
196
+ shutil.copy(video_file, in_path)
197
+ except Exception as e:
198
+ return f"Error: {e}", None
199
+
200
+ try:
201
+ duration, w, h, fps = probe_video(str(in_path))
202
+ except Exception as e:
203
+ shutil.rmtree(base_dir, ignore_errors=True)
204
+ return f"Error probing video: {e}", None
205
+
206
+ if duration <= 0:
207
+ shutil.rmtree(base_dir, ignore_errors=True)
208
+ return "Could not determine video duration.", None
209
+
210
+ max_seconds = 10
211
+ max_frames = int(fps * max_seconds)
212
+
213
+ progress(0.05, f"Video: {w}x{h} @ {fps:.1f}fps, extracting up to {max_seconds}s...")
214
+
215
+ frames_dir = base_dir / "frames"
216
+ try:
217
+ extract_frames(str(in_path), frames_dir, max_frames)
218
+ except Exception as e:
219
+ shutil.rmtree(base_dir, ignore_errors=True)
220
+ return f"Failed extracting frames: {e}", None
221
+
222
+ num_frames = len(list(frames_dir.glob("*.png")))
223
+ progress(0.15, f"Loading {num_frames} frames into memory...")
224
+
225
+ # Load all frames into RAM (CPU work, no GPU needed)
226
+ frames_rgb = load_frames_to_memory(frames_dir)
227
+ if not frames_rgb:
228
+ shutil.rmtree(base_dir, ignore_errors=True)
229
+ return "No frames extracted.", None
230
+
231
+ # Ensure model weights are cached (CPU/network only)
232
+ progress(0.20, "Preparing model...")
233
+ model_path = ensure_model(scale)
234
+
235
+ progress(0.25, f"Enhancing {len(frames_rgb)} frames on GPU (single call)...")
236
+
237
+ use_fallback = False
238
+ try:
239
+ # === THE SINGLE GPU CALL ===
240
+ enhanced_frames = enhance_all_frames_gpu(frames_rgb, model_path, scale)
241
+ print(f"Enhanced {len(enhanced_frames)} frames with Real-ESRGAN")
242
+ except Exception as e:
243
+ print(f"GPU enhancement failed: {e}")
244
+ print("Using fallback bicubic upscaling...")
245
+ use_fallback = True
246
+ enhanced_frames = [simple_upscale(f, scale) for f in frames_rgb]
247
+
248
+ progress(0.80, "Writing enhanced frames...")
249
+ save_frames_from_memory(enhanced_frames, frames_dir)
250
+
251
+ # Free memory
252
+ del frames_rgb, enhanced_frames
253
+
254
+ progress(0.85, "Reassembling video...")
255
+ out_video = base_dir / "enhanced_output.mp4"
256
+ try:
257
+ reassemble_video(frames_dir, str(in_path), str(out_video), fps)
258
+ except Exception as e:
259
+ shutil.rmtree(base_dir, ignore_errors=True)
260
+ return f"Failed reassembling: {e}", None
261
+
262
+ shutil.rmtree(frames_dir, ignore_errors=True)
263
+
264
+ try:
265
+ _, out_w, out_h, _ = probe_video(str(out_video))
266
+ method = "bicubic" if use_fallback else "Real-ESRGAN"
267
+ progress(1.0, "Done!")
268
+ return f"Done: {w}x{h} -> {out_w}x{out_h} ({method}, {num_frames} frames)", str(out_video)
269
+ except:
270
+ return "Done!", str(out_video)
271
+
272
+
273
+ # Gradio UI
274
+ with gr.Blocks(title="AI Video Enhancer", theme=gr.themes.Soft()) as demo:
275
+ gr.Markdown("# AI Video Enhancer")
276
+ gr.Markdown("Upscale videos using Real-ESRGAN AI. **Log in above for more GPU quota!**")
277
+
278
+ # LOGIN BUTTON - This allows ZeroGPU to recognize your Pro account
279
+ gr.LoginButton()
280
+
281
+ with gr.Row():
282
+ with gr.Column(scale=2):
283
+ video_in = gr.File(
284
+ label="Upload video (max 10 sec processed)",
285
+ file_types=[".mp4", ".avi", ".mov", ".mkv", ".webm"]
286
+ )
287
+ scale_choice = gr.Radio(choices=[2, 4], value=4, label="Upscale Factor")
288
+ btn = gr.Button("Enhance", variant="primary")
289
+ status = gr.Textbox(label="Status", interactive=False)
290
+ with gr.Column(scale=1):
291
+ out_video = gr.Video(label="Result")
292
+
293
+ gr.Markdown(
294
+ "**Limit: 10 seconds of video** (ZeroGPU quota). "
295
+ "Log in to HuggingFace for more!"
296
+ )
297
+
298
+ btn.click(fn=process_video, inputs=[video_in, scale_choice], outputs=[status, out_video])
299
+
300
+ if __name__ == "__main__":
301
+ demo.launch()