atharvak30 commited on
Commit
22d966c
Β·
verified Β·
1 Parent(s): 23e41f5

Upload 4 files

Browse files
Files changed (4) hide show
  1. README.md +13 -0
  2. app.py +276 -0
  3. gitattributes +35 -0
  4. requirements.txt +14 -0
README.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AI Video Enhancer 4K
3
+ emoji: πŸ‘
4
+ colorFrom: yellow
5
+ colorTo: pink
6
+ sdk: gradio
7
+ sdk_version: 5.49.0
8
+ app_file: app.py
9
+ pinned: false
10
+ hf_oauth: true
11
+ ---
12
+
13
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ # AI Video Enhancer 4K - Gradio app for Hugging Face Spaces
3
+ # Simplified version for better ZeroGPU compatibility
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
+ # Simple upscaling function using torch interpolation as fallback
107
+ def simple_upscale(img: np.ndarray, scale: int) -> np.ndarray:
108
+ """Simple bicubic upscaling using OpenCV"""
109
+ h, w = img.shape[:2]
110
+ return cv2.resize(img, (w * scale, h * scale), interpolation=cv2.INTER_CUBIC)
111
+
112
+
113
+ @spaces.GPU(duration=120)
114
+ def enhance_with_realesrgan(frames_dir: str, scale: int = 4) -> int:
115
+ """
116
+ Enhance frames using Real-ESRGAN via Spandrel.
117
+ Separated function with GPU decorator for cleaner ZeroGPU handling.
118
+ """
119
+ from spandrel import ImageModelDescriptor, ModelLoader
120
+
121
+ frames_path = Path(frames_dir)
122
+ frame_files = sorted(frames_path.glob("*.png"))
123
+ total = len(frame_files)
124
+
125
+ if total == 0:
126
+ return 0
127
+
128
+ # Download and load model
129
+ if scale == 2:
130
+ model_path = hf_hub_download(repo_id="ai-forever/Real-ESRGAN", filename="RealESRGAN_x2.pth")
131
+ else:
132
+ model_path = hf_hub_download(repo_id="ai-forever/Real-ESRGAN", filename="RealESRGAN_x4.pth")
133
+
134
+ model = ModelLoader().load_from_file(model_path)
135
+ assert isinstance(model, ImageModelDescriptor)
136
+
137
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
138
+ model = model.to(device).eval()
139
+
140
+ print(f"Model loaded on {device}, processing {total} frames...")
141
+
142
+ for idx, frame_path in enumerate(frame_files):
143
+ # Read image
144
+ img = cv2.imread(str(frame_path))
145
+ if img is None:
146
+ continue
147
+
148
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
149
+
150
+ # Convert to tensor
151
+ tensor = torch.from_numpy(img_rgb).permute(2, 0, 1).float().div(255.0)
152
+ tensor = tensor.unsqueeze(0).to(device)
153
+
154
+ # Process
155
+ with torch.no_grad():
156
+ output = model(tensor)
157
+
158
+ # Convert back
159
+ output = output.squeeze(0).cpu().clamp(0, 1).mul(255).byte()
160
+ output = output.permute(1, 2, 0).numpy()
161
+ output_bgr = cv2.cvtColor(output, cv2.COLOR_RGB2BGR)
162
+
163
+ # Save
164
+ cv2.imwrite(str(frame_path), output_bgr)
165
+
166
+ if (idx + 1) % 5 == 0:
167
+ print(f"Processed {idx + 1}/{total}")
168
+
169
+ return total
170
+
171
+
172
+ def process_video(video_file, scale: int = 4) -> Tuple[str, str]:
173
+ """Main video processing - handles file I/O outside GPU function"""
174
+ if video_file is None:
175
+ return "⚠️ Please upload a video file.", None
176
+
177
+ ts = int(time.time() * 1000)
178
+ base_dir = TEMP_DIR / f"job_{ts}"
179
+ base_dir.mkdir(parents=True, exist_ok=True)
180
+ in_path = base_dir / "input_video"
181
+
182
+ try:
183
+ shutil.copy(video_file, in_path)
184
+ except Exception as e:
185
+ return f"Error: {e}", None
186
+
187
+ try:
188
+ duration, w, h, fps = probe_video(str(in_path))
189
+ except Exception as e:
190
+ shutil.rmtree(base_dir, ignore_errors=True)
191
+ return f"Error probing video: {e}", None
192
+
193
+ if duration <= 0:
194
+ shutil.rmtree(base_dir, ignore_errors=True)
195
+ return "Could not determine video duration.", None
196
+
197
+ # Limit for ZeroGPU - process max ~30 seconds of video
198
+ max_frames = int(fps * 30) # ~30 seconds worth
199
+
200
+ print(f"Video: {w}x{h}, {duration:.1f}s, {fps:.1f}fps")
201
+
202
+ frames_dir = base_dir / "frames"
203
+ try:
204
+ extract_frames(str(in_path), frames_dir)
205
+ except Exception as e:
206
+ shutil.rmtree(base_dir, ignore_errors=True)
207
+ return f"Failed extracting frames: {e}", None
208
+
209
+ frame_files = sorted(frames_dir.glob("*.png"))
210
+ num_frames = len(frame_files)
211
+
212
+ # Limit frames if too many
213
+ if num_frames > max_frames:
214
+ print(f"Limiting from {num_frames} to {max_frames} frames")
215
+ for f in frame_files[max_frames:]:
216
+ f.unlink()
217
+ num_frames = max_frames
218
+
219
+ print(f"Processing {num_frames} frames...")
220
+
221
+ try:
222
+ enhanced = enhance_with_realesrgan(str(frames_dir), scale)
223
+ print(f"Enhanced {enhanced} frames")
224
+ except Exception as e:
225
+ print(f"Enhancement failed: {e}")
226
+ # Fallback to simple upscaling
227
+ print("Using fallback bicubic upscaling...")
228
+ try:
229
+ for fp in sorted(frames_dir.glob("*.png")):
230
+ img = cv2.imread(str(fp))
231
+ if img is not None:
232
+ upscaled = simple_upscale(img, scale)
233
+ cv2.imwrite(str(fp), upscaled)
234
+ except Exception as e2:
235
+ shutil.rmtree(base_dir, ignore_errors=True)
236
+ return f"Enhancement failed: {e}", None
237
+
238
+ out_video = base_dir / "enhanced_output.mp4"
239
+ try:
240
+ reassemble_video(frames_dir, str(in_path), str(out_video), fps)
241
+ except Exception as e:
242
+ shutil.rmtree(base_dir, ignore_errors=True)
243
+ return f"Failed reassembling: {e}", None
244
+
245
+ shutil.rmtree(frames_dir, ignore_errors=True)
246
+
247
+ try:
248
+ _, out_w, out_h, _ = probe_video(str(out_video))
249
+ return f"βœ… Done! {w}x{h} β†’ {out_w}x{out_h}", str(out_video)
250
+ except:
251
+ return "βœ… Done!", str(out_video)
252
+
253
+
254
+ # Gradio UI
255
+ with gr.Blocks(title="AI Video Enhancer", theme=gr.themes.Soft()) as demo:
256
+ gr.Markdown("# 🎬 AI Video Enhancer")
257
+ gr.Markdown("Upscale videos using Real-ESRGAN AI enhancement.")
258
+
259
+ # LOGIN BUTTON - This allows ZeroGPU to recognize your Pro account
260
+ gr.LoginButton()
261
+
262
+ with gr.Row():
263
+ with gr.Column(scale=2):
264
+ video_in = gr.File(label="Upload video", file_types=[".mp4", ".avi", ".mov", ".mkv", ".webm"])
265
+ scale_choice = gr.Radio(choices=[2, 4], value=4, label="Upscale Factor")
266
+ btn = gr.Button("πŸš€ Enhance", variant="primary")
267
+ status = gr.Textbox(label="Status", interactive=False)
268
+ with gr.Column(scale=1):
269
+ out_video = gr.Video(label="Result")
270
+
271
+ gr.Markdown("**Note:** Limited to ~30 seconds for ZeroGPU. Longer videos will be truncated.")
272
+
273
+ btn.click(fn=process_video, inputs=[video_in, scale_choice], outputs=[status, out_video])
274
+
275
+ if __name__ == "__main__":
276
+ demo.launch()
gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies
2
+ gradio>=4.0.0
3
+ spaces
4
+ numpy
5
+ Pillow
6
+ opencv-python-headless
7
+ huggingface_hub
8
+
9
+ # PyTorch
10
+ torch
11
+ torchvision
12
+
13
+ # Spandrel - clean model loader for Real-ESRGAN (no basicsr dependency!)
14
+ spandrel