calebmills99 commited on
Commit
fb59dc6
ยท
verified ยท
1 Parent(s): 93d2fe7

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +755 -0
app.py ADDED
@@ -0,0 +1,755 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from spandrel import ModelLoader
2
+ import torch
3
+ from pathlib import Path
4
+ import gradio as App
5
+ import logging
6
+ import time
7
+ import cv2
8
+ import os
9
+
10
+ # Conditional import for Hugging Face Spaces
11
+ try:
12
+ import spaces
13
+
14
+ HF_SPACES_AVAILABLE = True
15
+ except ImportError:
16
+ # Create a dummy decorator for local development
17
+ class DummySpaces:
18
+ @staticmethod
19
+ def GPU(func):
20
+ return func
21
+
22
+ spaces = DummySpaces()
23
+ HF_SPACES_AVAILABLE = False
24
+
25
+ from gradio import themes
26
+ from rich.console import Console
27
+ from rich.logging import RichHandler
28
+
29
+ from Scripts.SAD import GetDifferenceRectangles
30
+ from Scripts.ORB import DetectMotionWithOrb
31
+
32
+ # ============================== #
33
+ # Core Settings #
34
+ # ============================== #
35
+
36
+ # Use default theme for HF compatibility
37
+ Theme = None # Will use Gradio's default theme
38
+ ModelDir = Path("./Models")
39
+ TempDir = Path("./Temp")
40
+ os.environ["GRADIO_TEMP_DIR"] = str(TempDir)
41
+ ModelFileType = ".pth"
42
+
43
+ # ============================== #
44
+ # Logging #
45
+ # ============================== #
46
+
47
+ logging.basicConfig(
48
+ level=logging.INFO,
49
+ format="%(message)s",
50
+ datefmt="[%X]",
51
+ handlers=[
52
+ RichHandler(
53
+ console=Console(),
54
+ rich_tracebacks=True,
55
+ omit_repeated_times=False,
56
+ markup=True,
57
+ show_path=False,
58
+ )
59
+ ],
60
+ )
61
+ Logger = logging.getLogger("Zero2x")
62
+ logging.getLogger("httpx").setLevel(logging.WARNING)
63
+
64
+ # ============================== #
65
+ # Device Configuration #
66
+ # ============================== #
67
+
68
+
69
+ @spaces.GPU
70
+ def GetDeviceName():
71
+ Device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
72
+ Logger.info(f"๐Ÿงช Using device: {str(Device).upper()}")
73
+ return Device
74
+
75
+
76
+ Device = GetDeviceName()
77
+
78
+ # ============================== #
79
+ # Utility Functions #
80
+ # ============================== #
81
+
82
+
83
+ def HumanizeSeconds(Seconds):
84
+ Hours = int(Seconds // 3600)
85
+ Minutes = int((Seconds % 3600) // 60)
86
+ Seconds = int(Seconds % 60)
87
+
88
+ if Hours > 0:
89
+ return f"{Hours}h {Minutes}m {Seconds}s"
90
+ elif Minutes > 0:
91
+ return f"{Minutes}m {Seconds}s"
92
+ else:
93
+ return f"{Seconds}s"
94
+
95
+
96
+ def HumanizedBytes(Size):
97
+ Units = ["B", "KB", "MB", "GB", "TB"]
98
+ Index = 0
99
+ while Size >= 1024 and Index < len(Units) - 1:
100
+ Size /= 1024.0
101
+ Index += 1
102
+ return f"{Size:.2f} {Units[Index]}"
103
+
104
+
105
+ # ============================== #
106
+ # Main Processing Logic #
107
+ # ============================== #
108
+
109
+
110
+ class Upscaler:
111
+ def __init__(self, preload=False):
112
+ self.loaded_models = {}
113
+ if preload:
114
+ self.preload_models()
115
+ else:
116
+ Logger.info(
117
+ f"๐Ÿ“š Found {len(self.ListModelsFromDisk())} Models In Directory (lazy loading enabled)"
118
+ )
119
+
120
+ def preload_models(self):
121
+ """Preload all models during initialization for faster access"""
122
+ model_files = sorted(
123
+ [File.stem for File in ModelDir.glob("*" + ModelFileType) if File.is_file()]
124
+ )
125
+ Logger.info(f"๐Ÿ“š Found {len(model_files)} Models In Directory")
126
+ Logger.info("๐Ÿš€ Preloading all models for faster access...")
127
+
128
+ for i, model_name in enumerate(model_files):
129
+ try:
130
+ Logger.info(f"๐Ÿ“ฆ Loading model {i+1}/{len(model_files)}: {model_name}")
131
+ model = (
132
+ ModelLoader()
133
+ .load_from_file(ModelDir / (model_name + ModelFileType))
134
+ .to(Device)
135
+ .eval()
136
+ )
137
+ self.loaded_models[model_name] = model
138
+ Logger.info(f"โœ… Loaded {model_name}")
139
+ except Exception as e:
140
+ Logger.error(f"โŒ Failed to load {model_name}: {str(e)}")
141
+
142
+ Logger.info(f"๐ŸŽ‰ Preloaded {len(self.loaded_models)} models successfully!")
143
+
144
+ def ListModelsFromDisk(self):
145
+ """Get list of model files from disk"""
146
+ return sorted(
147
+ [File.stem for File in ModelDir.glob("*" + ModelFileType) if File.is_file()]
148
+ )
149
+
150
+ def ListModels(self):
151
+ """Get list of available models (preloaded or from disk)"""
152
+ if self.loaded_models:
153
+ return list(self.loaded_models.keys())
154
+ else:
155
+ return self.ListModelsFromDisk()
156
+
157
+ def LoadModel(self, ModelName):
158
+ """Get preloaded model - much faster than loading from disk"""
159
+ if ModelName in self.loaded_models:
160
+ Logger.info(f"โšก Using preloaded model: {ModelName}")
161
+ return self.loaded_models[ModelName]
162
+ else:
163
+ Logger.warning(f"๐Ÿ”„ Model {ModelName} not preloaded, loading from disk...")
164
+ try:
165
+ torch.cuda.empty_cache()
166
+ Model = (
167
+ ModelLoader()
168
+ .load_from_file(ModelDir / (ModelName + ModelFileType))
169
+ .to(Device)
170
+ .eval()
171
+ )
172
+ self.loaded_models[ModelName] = Model
173
+ Logger.info(f"๐Ÿค– Loaded Model {ModelName} Onto {str(Device).upper()}")
174
+ return Model
175
+ except Exception as e:
176
+ Logger.error(f"โŒ Failed to load {ModelName}: {str(e)}")
177
+ Logger.error(
178
+ f"๐Ÿ’ก Model file may be corrupted. Try downloading a fresh copy."
179
+ )
180
+ raise ValueError(f"Model {ModelName} failed to load: {str(e)}")
181
+
182
+ def UnloadModel(self):
183
+ if Device.type == "cuda":
184
+ torch.cuda.empty_cache()
185
+ Logger.info("๐Ÿค– Model Unloaded Successfully")
186
+
187
+ def CleanUp(self):
188
+ self.UnloadModel()
189
+ Logger.info("๐Ÿงน Temporary Files Cleaned Up")
190
+
191
+ @spaces.GPU
192
+ def UpscaleFullFrame(self, Model, Frame):
193
+ FrameRgb = cv2.cvtColor(Frame, cv2.COLOR_BGR2RGB)
194
+ FrameForTorch = FrameRgb.transpose(2, 0, 1)
195
+ FrameForTorch = (
196
+ torch.from_numpy(FrameForTorch).unsqueeze(0).to(Device).float() / 255.0
197
+ )
198
+ OutputFrame = Model(FrameForTorch)[0].cpu().numpy().transpose(1, 2, 0) * 255.0
199
+ OutputFrame = cv2.cvtColor(OutputFrame.astype("uint8"), cv2.COLOR_RGB2BGR)
200
+ return OutputFrame
201
+
202
+ @spaces.GPU
203
+ def UpscaleRegions(
204
+ self,
205
+ Model,
206
+ Frame,
207
+ PrevFrame,
208
+ UpscaledPrevFrame,
209
+ InputThreshold,
210
+ InputMinPercentage,
211
+ InputMaxRectangles,
212
+ InputPadding,
213
+ InputSegmentRows,
214
+ InputSegmentColumns,
215
+ ):
216
+ DiffResult = GetDifferenceRectangles(
217
+ PrevFrame,
218
+ Frame,
219
+ Threshold=InputThreshold,
220
+ Rows=InputSegmentRows,
221
+ Columns=InputSegmentColumns,
222
+ Padding=InputPadding,
223
+ )
224
+ SimilarityPercentage = DiffResult["SimilarPercentage"]
225
+ Rectangles = DiffResult["Rectangles"]
226
+ Cols = DiffResult["Columns"]
227
+ Rows = DiffResult["Rows"]
228
+ FrameHeight, FrameWidth = Frame.shape[:2]
229
+ SegmentWidth = FrameWidth // Cols
230
+ SegmentHeight = FrameHeight // Rows
231
+ UseRegions = False
232
+ RegionLog = "๐ŸŸฅ"
233
+ if (
234
+ SimilarityPercentage > InputMinPercentage
235
+ and len(Rectangles) < InputMaxRectangles
236
+ ):
237
+ UpscaleFactorY = UpscaledPrevFrame.shape[0] // FrameHeight
238
+ UpscaleFactorX = UpscaledPrevFrame.shape[1] // FrameWidth
239
+ OutputFrame = UpscaledPrevFrame.copy()
240
+ for X, Y, W, H in Rectangles:
241
+ X1 = X * SegmentWidth
242
+ Y1 = Y * SegmentHeight
243
+ X2 = FrameWidth if X + W == Cols else X1 + W * SegmentWidth
244
+ Y2 = FrameHeight if Y + H == Rows else Y1 + H * SegmentHeight
245
+ Region = Frame[Y1:Y2, X1:X2]
246
+ RegionRgb = cv2.cvtColor(Region, cv2.COLOR_BGR2RGB)
247
+ RegionTorch = (
248
+ torch.from_numpy(RegionRgb.transpose(2, 0, 1))
249
+ .unsqueeze(0)
250
+ .to(Device)
251
+ .float()
252
+ / 255.0
253
+ )
254
+ UpscaledRegion = (
255
+ Model(RegionTorch)[0].cpu().numpy().transpose(1, 2, 0) * 255.0
256
+ )
257
+ UpscaledRegion = cv2.cvtColor(
258
+ UpscaledRegion.astype("uint8"), cv2.COLOR_RGB2BGR
259
+ )
260
+ RegionHeight, RegionWidth = Region.shape[:2]
261
+ UpscaledRegion = cv2.resize(
262
+ UpscaledRegion,
263
+ (RegionWidth * UpscaleFactorX, RegionHeight * UpscaleFactorY),
264
+ interpolation=cv2.INTER_CUBIC,
265
+ )
266
+ UX1 = X1 * UpscaleFactorX
267
+ UY1 = Y1 * UpscaleFactorY
268
+ UX2 = UX1 + UpscaledRegion.shape[1]
269
+ UY2 = UY1 + UpscaledRegion.shape[0]
270
+ OutputFrame[UY1:UY2, UX1:UX2] = UpscaledRegion
271
+ RegionLog = "๐ŸŸฉ"
272
+ UseRegions = True
273
+ else:
274
+ OutputFrame = self.UpscaleFullFrame(Model, Frame)
275
+ return OutputFrame, SimilarityPercentage, Rectangles, RegionLog, UseRegions
276
+
277
+ @spaces.GPU
278
+ def Process(
279
+ self,
280
+ InputVideo,
281
+ InputModel,
282
+ InputUseRegions,
283
+ InputThreshold,
284
+ InputMinPercentage,
285
+ InputMaxRectangles,
286
+ InputPadding,
287
+ InputSegmentRows,
288
+ InputSegmentColumns,
289
+ InputFullFrameInterval,
290
+ InputMotionThreshold,
291
+ Progress=App.Progress(),
292
+ ):
293
+ if not InputVideo:
294
+ Logger.warning('โŒ No Video Provided')
295
+ App.Warning('โŒ No Video Provided')
296
+ return None, None
297
+
298
+ if not InputModel:
299
+ Logger.warning('โŒ No Model Selected')
300
+ App.Warning('โŒ No Model Selected - Please add .pth model files to the Models directory')
301
+ return None, None
302
+
303
+ Progress(0, desc='โš™๏ธ Loading Model')
304
+ Model = self.LoadModel(InputModel)
305
+
306
+ Logger.info(f'๐Ÿ“ผ Processing Video: {Path(InputVideo).name}')
307
+ Progress(0, desc='๐Ÿ“ผ Processing Video')
308
+ Video = cv2.VideoCapture(InputVideo)
309
+
310
+ # Check if video opened successfully
311
+ if not Video.isOpened():
312
+ Logger.error('โŒ Failed to open input video')
313
+ return None, None
314
+
315
+ FrameRate = Video.get(cv2.CAP_PROP_FPS)
316
+ FrameCount = int(Video.get(cv2.CAP_PROP_FRAME_COUNT))
317
+ Width = int(Video.get(cv2.CAP_PROP_FRAME_WIDTH))
318
+ Height = int(Video.get(cv2.CAP_PROP_FRAME_HEIGHT))
319
+
320
+ # Validate video properties
321
+ if FrameCount <= 0:
322
+ Logger.error('โŒ Invalid video: no frames detected')
323
+ Video.release()
324
+ return None, None
325
+
326
+ Logger.info(f'๐Ÿ“ Video Properties: {FrameCount} Frames, {FrameRate} FPS, {Width}x{Height}')
327
+
328
+ PerFrameProgress = 1 / FrameCount
329
+ FrameProgress = 0.0
330
+ StartTime = time.time()
331
+ Times = []
332
+
333
+ CurrentFrameIndex = 0
334
+ PrevFrame = None
335
+ UpscaledPrevFrame = None
336
+ PartialUpscaleCount = 0
337
+
338
+ while True:
339
+ Ret, Frame = Video.read()
340
+ if not Ret:
341
+ break
342
+
343
+ CurrentFrameIndex += 1
344
+
345
+ ForceFull = False
346
+ CopyPrevUpscaled = False
347
+ if CurrentFrameIndex == 1 or not InputUseRegions:
348
+ ForceFull = True
349
+ PartialUpscaleCount = 0
350
+ elif PartialUpscaleCount >= InputFullFrameInterval:
351
+ ForceFull = True
352
+ PartialUpscaleCount = 0
353
+
354
+ if PrevFrame is not None:
355
+ IsMotion, TotalMagnitude, DirectionAngle = DetectMotionWithOrb(PrevFrame, Frame, InputMotionThreshold)
356
+ if IsMotion:
357
+ ForceFull = True
358
+ PartialUpscaleCount = 0
359
+ Logger.info(f'๐ŸŸจ Frame {CurrentFrameIndex}: Motion Detected - Upscaling Full Frame')
360
+
361
+ if not ForceFull and PrevFrame is not None and UpscaledPrevFrame is not None:
362
+ DiffResult = GetDifferenceRectangles(
363
+ PrevFrame,
364
+ Frame,
365
+ Threshold=InputThreshold,
366
+ Rows=InputSegmentRows,
367
+ Columns=InputSegmentColumns,
368
+ Padding=InputPadding
369
+ )
370
+ SimilarityPercentage = DiffResult['SimilarPercentage']
371
+ if SimilarityPercentage == 100:
372
+ OutputFrame = UpscaledPrevFrame.copy()
373
+ RegionLog = '๐ŸŸฆ'
374
+ UseRegions = False
375
+ Rectangles = []
376
+ Logger.info(f'{RegionLog} Frame {CurrentFrameIndex}: 100% Similar - Copied Previous Upscaled Frame')
377
+ FrameProgress += PerFrameProgress
378
+ Progress(FrameProgress, desc=f'๐Ÿ“ฆ Processed Frame {CurrentFrameIndex}/{FrameCount}')
379
+ cv2.imwrite(f'{TempDir}/Upscaled_Frame_{CurrentFrameIndex:05d}.png', OutputFrame)
380
+ PrevFrame = Frame.copy()
381
+ UpscaledPrevFrame = OutputFrame.copy()
382
+ DeltaTime = time.time() - StartTime
383
+ Times.append(DeltaTime)
384
+ StartTime = time.time()
385
+ continue
386
+
387
+ if ForceFull:
388
+ OutputFrame = self.UpscaleFullFrame(Model, Frame)
389
+ SimilarityPercentage = 0
390
+ Rectangles = []
391
+ RegionLog = '๐ŸŸฅ'
392
+ UseRegions = False
393
+ else:
394
+ OutputFrame, SimilarityPercentage, Rectangles, RegionLog, UseRegions = self.UpscaleRegions(
395
+ Model, Frame, PrevFrame, UpscaledPrevFrame, InputThreshold, InputMinPercentage, InputMaxRectangles, InputPadding, InputSegmentRows, InputSegmentColumns
396
+ )
397
+ if UseRegions:
398
+ PartialUpscaleCount += 1
399
+ else:
400
+ PartialUpscaleCount = 0
401
+
402
+ if Times:
403
+ AverageTime = sum(Times) / len(Times)
404
+ Eta = HumanizeSeconds((FrameCount - CurrentFrameIndex) * AverageTime)
405
+ else:
406
+ Eta = 'Calculating...'
407
+
408
+ if UseRegions:
409
+ Logger.info(f'{RegionLog} Frame {CurrentFrameIndex}: {SimilarityPercentage:.2f}% Similar, {len(Rectangles)} Regions To Upscale')
410
+ else:
411
+ Logger.info(f'{RegionLog} Frame {CurrentFrameIndex}: Upscaling Full Frame')
412
+
413
+ # Save frame
414
+ cv2.imwrite(f'{TempDir}/Upscaled_Frame_{CurrentFrameIndex:05d}.png', OutputFrame)
415
+
416
+ # Update progress and timing
417
+ DeltaTime = time.time() - StartTime
418
+ Times.append(DeltaTime)
419
+ StartTime = time.time()
420
+ FrameProgress += PerFrameProgress
421
+
422
+ Progress(FrameProgress, desc=f'๐Ÿ“ฆ Processed Frame {CurrentFrameIndex}/{FrameCount} - {Eta}')
423
+
424
+ PrevFrame = Frame.copy()
425
+ UpscaledPrevFrame = OutputFrame.copy()
426
+
427
+ Video.release() # Close input video
428
+ Progress(1, desc='๐Ÿ“ฆ Creating Final Video...')
429
+
430
+ # Create output video from processed frames
431
+ Logger.info('๐ŸŽฌ Creating output video...')
432
+
433
+ # Check if any frames were processed
434
+ if CurrentFrameIndex == 0:
435
+ Logger.error('โŒ No frames were processed')
436
+ self.CleanUp()
437
+ return None, None
438
+
439
+ # Get first frame to determine output dimensions
440
+ FirstFramePath = f'{TempDir}/Upscaled_Frame_00001.png'
441
+ if not os.path.exists(FirstFramePath):
442
+ Logger.error('โŒ No processed frames found')
443
+ self.CleanUp()
444
+ return None, None
445
+
446
+ FirstFrame = cv2.imread(FirstFramePath)
447
+ if FirstFrame is None:
448
+ Logger.error('โŒ Could not read first processed frame')
449
+ self.CleanUp()
450
+ return None, None
451
+
452
+ OutputHeight, OutputWidth = FirstFrame.shape[:2]
453
+
454
+ # Create output video path
455
+ InputPath = Path(InputVideo)
456
+ OutputPath = TempDir / f'Upscaled_{InputPath.stem}_{InputModel}.mp4'
457
+
458
+ # Try different codecs for better compatibility
459
+ codecs_to_try = [
460
+ ('mp4v', cv2.VideoWriter.fourcc('m', 'p', '4', 'v')),
461
+ ('XVID', cv2.VideoWriter.fourcc('X', 'V', 'I', 'D')),
462
+ ('H264', cv2.VideoWriter.fourcc('H', '2', '6', '4')),
463
+ ('avc1', cv2.VideoWriter.fourcc('a', 'v', 'c', '1')),
464
+ ]
465
+
466
+ VideoWriter = None
467
+ successful_codec = None
468
+
469
+ for codec_name, fourcc in codecs_to_try:
470
+ try:
471
+ VideoWriter = cv2.VideoWriter(
472
+ str(OutputPath), fourcc, FrameRate, (OutputWidth, OutputHeight)
473
+ )
474
+ if VideoWriter.isOpened():
475
+ successful_codec = codec_name
476
+ Logger.info(f'โœ… Video writer initialized with codec: {codec_name}')
477
+ break
478
+ else:
479
+ VideoWriter.release()
480
+ VideoWriter = None
481
+ except Exception as e:
482
+ Logger.warning(f'โš ๏ธ Failed to initialize codec {codec_name}: {e}')
483
+ if VideoWriter:
484
+ VideoWriter.release()
485
+ VideoWriter = None
486
+
487
+ if VideoWriter is None:
488
+ Logger.error('โŒ Failed to initialize video writer with any codec')
489
+ self.CleanUp()
490
+ return None, None
491
+
492
+ # Write all frames to video
493
+ frames_written = 0
494
+ for i in range(1, CurrentFrameIndex + 1):
495
+ FramePath = f'{TempDir}/Upscaled_Frame_{i:05d}.png'
496
+ if os.path.exists(FramePath):
497
+ Frame = cv2.imread(FramePath)
498
+ if Frame is not None:
499
+ # Resize frame if dimensions don't match (safety check)
500
+ if Frame.shape[:2] != (OutputHeight, OutputWidth):
501
+ Frame = cv2.resize(Frame, (OutputWidth, OutputHeight))
502
+
503
+ VideoWriter.write(Frame)
504
+ frames_written += 1
505
+ # Clean up frame after writing
506
+ try:
507
+ os.remove(FramePath)
508
+ except Exception as e:
509
+ Logger.warning(f'โš ๏ธ Could not remove temp frame {FramePath}: {e}')
510
+ else:
511
+ Logger.warning(f'โš ๏ธ Could not read frame {FramePath}')
512
+ else:
513
+ Logger.warning(f'โš ๏ธ Frame {FramePath} not found')
514
+
515
+ VideoWriter.release()
516
+
517
+ if frames_written == 0:
518
+ Logger.error('โŒ No frames were written to output video')
519
+ self.CleanUp()
520
+ return None, None
521
+
522
+ Logger.info(f'โœ… Output video created: {OutputPath}')
523
+ Logger.info(f'๐Ÿ“ Output dimensions: {OutputWidth}x{OutputHeight} (vs input: {Width}x{Height})')
524
+ Logger.info(f'๐ŸŽฌ Frames written: {frames_written}/{CurrentFrameIndex} using codec: {successful_codec}')
525
+
526
+ self.CleanUp()
527
+ return str(OutputPath), str(OutputPath)
528
+
529
+
530
+ # ============================== #
531
+ # Global Upscaler Instance #
532
+ # ============================== #
533
+
534
+ # Create global upscaler instance with lazy loading for faster startup
535
+ upscaler = Upscaler(preload=False)
536
+
537
+ # ============================== #
538
+ # Simplified UI #
539
+ # ============================== #
540
+
541
+ with App.Blocks(title="Zero2x Video Upscaler", delete_cache=(-1, 1800)) as Interface:
542
+ App.Markdown("# ๐ŸŽž๏ธ Zero2x Video Upscaler")
543
+
544
+ # Check if models are available
545
+ try:
546
+ ModelNames = upscaler.ListModels()
547
+ if not ModelNames:
548
+ ModelNames = ["test_model"] # Fallback for testing
549
+ except Exception as e:
550
+ Logger.warning(f"Error loading models: {e}")
551
+ ModelNames = ["test_model"] # Fallback for testing
552
+
553
+ # Main Video Upscaling Interface
554
+ if not ModelNames or ModelNames == ["test_model"]:
555
+ App.Markdown(
556
+ """
557
+ ## โš ๏ธ No Models Found
558
+
559
+ **No upscaling models (.pth files) were found in the Models directory.**
560
+
561
+ To use this application, you need to:
562
+ 1. Download upscaling models (Real-ESRGAN, CUGAN, etc.)
563
+ 2. Place .pth model files in the `Models/` directory
564
+ 3. Restart the application
565
+ """
566
+ )
567
+
568
+ with App.Row():
569
+ with App.Column():
570
+ with App.Group():
571
+ InputVideo = App.Video(
572
+ label="Input Video", sources=["upload"], height=300
573
+ )
574
+
575
+ if ModelNames:
576
+ InputModel = App.Dropdown(
577
+ choices=ModelNames,
578
+ label="Select Model",
579
+ value=ModelNames[0] if ModelNames else None,
580
+ )
581
+ else:
582
+ InputModel = App.Dropdown(
583
+ choices=[],
584
+ label="Select Model (No models found)",
585
+ value=None,
586
+ interactive=False,
587
+ )
588
+
589
+ with App.Accordion(label="โš™๏ธ Advanced Settings", open=False):
590
+ with App.Group():
591
+ InputUseRegions = App.Checkbox(
592
+ label="Use Regions",
593
+ value=False,
594
+ info="Use regions to upscale only the different parts of the video (โšก๏ธ Experimental, Faster)",
595
+ interactive=bool(ModelNames),
596
+ )
597
+ InputThreshold = App.Slider(
598
+ label="Threshold",
599
+ value=2,
600
+ minimum=0,
601
+ maximum=10,
602
+ step=0.5,
603
+ info="Threshold for the SAD algorithm to detect different regions",
604
+ interactive=False,
605
+ )
606
+ InputPadding = App.Slider(
607
+ label="Padding",
608
+ value=1,
609
+ minimum=0,
610
+ maximum=5,
611
+ step=1,
612
+ info="Extra padding to include neighboring pixels in the SAD algorithm",
613
+ interactive=False,
614
+ )
615
+ InputMinPercentage = App.Slider(
616
+ label="Min Percentage",
617
+ value=50,
618
+ minimum=0,
619
+ maximum=100,
620
+ step=1,
621
+ info="Minimum percentage of similarity to consider upscaling the full frame",
622
+ interactive=False,
623
+ )
624
+ InputMaxRectangles = App.Slider(
625
+ label="Max Rectangles",
626
+ value=10,
627
+ minimum=1,
628
+ maximum=16,
629
+ step=1,
630
+ info="Maximum number of rectangles to consider upscaling the full frame",
631
+ interactive=False,
632
+ )
633
+ with App.Row():
634
+ InputSegmentRows = App.Slider(
635
+ label="Segment Rows",
636
+ value=32,
637
+ minimum=1,
638
+ maximum=64,
639
+ step=1,
640
+ info="Number of rows to segment the video into for processing",
641
+ interactive=False,
642
+ )
643
+ InputSegmentColumns = App.Slider(
644
+ label="Segment Columns",
645
+ value=48,
646
+ minimum=1,
647
+ maximum=64,
648
+ step=1,
649
+ info="Number of columns to segment the video into for processing",
650
+ interactive=False,
651
+ )
652
+ InputFullFrameInterval = App.Slider(
653
+ label="Full Frame Interval",
654
+ value=5,
655
+ minimum=1,
656
+ maximum=100,
657
+ step=1,
658
+ info="Force a full-frame upscale every N frames (set to 1 to always upscale full frame)",
659
+ interactive=False,
660
+ )
661
+ InputMotionThreshold = App.Slider(
662
+ label="Motion Threshold",
663
+ value=1,
664
+ minimum=0,
665
+ maximum=10,
666
+ step=0.5,
667
+ info="Threshold for the motion detection algorithm to consider a frame as different",
668
+ interactive=False,
669
+ )
670
+
671
+ if ModelNames and ModelNames != ["test_model"]:
672
+ SubmitButton = App.Button("๐Ÿš€ Upscale Video")
673
+ else:
674
+ SubmitButton = App.Button("โŒ No Models Available", interactive=False)
675
+
676
+ with App.Column(show_progress=True):
677
+ with App.Group():
678
+ OutputVideo = App.Video(
679
+ label="Output Video",
680
+ height=300,
681
+ interactive=False,
682
+ )
683
+ OutputDownload = App.DownloadButton(
684
+ label="๐Ÿ’พ Download Video", interactive=False
685
+ )
686
+
687
+ # About Section
688
+ with App.Accordion(label="โ„น๏ธ About Zero2x", open=False):
689
+ App.Markdown(
690
+ """
691
+ ## โœจ Zero2x Video Upscaler
692
+
693
+ **Zero2x** is an advanced video upscaling tool using AI models:
694
+ - ๐ŸŽฌ **AI Video Upscaling** using multiple deep learning models
695
+ - ๐Ÿง‘โ€๐Ÿ”ฌ **Advanced Techniques**: SAD algorithm for region detection, ORB motion detection
696
+ - ๐Ÿš€ **GPU Acceleration**: CUDA support with automatic memory management
697
+
698
+ ### ๐Ÿš€ Getting Started
699
+
700
+ 1. **Add Models**: Place .pth model files in the `WorkingModels/` directory
701
+ 2. **Upload Video**: Choose a video file to upscale
702
+ 3. **Configure Settings**: Adjust parameters as needed
703
+ 4. **Process**: Let the AI enhance your video!
704
+ """
705
+ )
706
+
707
+ # Event Handlers
708
+ def toggle_region_inputs(use_regions):
709
+ updates = []
710
+ for _ in range(8): # 8 sliders need updating
711
+ updates.append(App.update(interactive=use_regions))
712
+ return updates
713
+
714
+ # Wire up events
715
+ InputUseRegions.change(
716
+ fn=toggle_region_inputs,
717
+ inputs=[InputUseRegions],
718
+ outputs=[
719
+ InputThreshold,
720
+ InputMinPercentage,
721
+ InputMaxRectangles,
722
+ InputPadding,
723
+ InputSegmentRows,
724
+ InputSegmentColumns,
725
+ InputFullFrameInterval,
726
+ InputMotionThreshold,
727
+ ],
728
+ )
729
+
730
+ if ModelNames and ModelNames != ["test_model"] and upscaler:
731
+ SubmitButton.click(
732
+ fn=upscaler.Process,
733
+ inputs=[
734
+ InputVideo,
735
+ InputModel,
736
+ InputUseRegions,
737
+ InputThreshold,
738
+ InputMinPercentage,
739
+ InputMaxRectangles,
740
+ InputPadding,
741
+ InputSegmentRows,
742
+ InputSegmentColumns,
743
+ InputFullFrameInterval,
744
+ InputMotionThreshold,
745
+ ],
746
+ outputs=[OutputVideo, OutputDownload],
747
+ )
748
+
749
+ if __name__ == "__main__":
750
+ os.makedirs(ModelDir, exist_ok=True)
751
+ os.makedirs(TempDir, exist_ok=True)
752
+ Logger.info("๐Ÿš€ Starting Zero2x Video Upscaler")
753
+
754
+ # Launch configuration for Hugging Face deployment
755
+ Interface.launch(server_name="0.0.0.0", server_port=7860, share=True)