Saravutw commited on
Commit
9ba5c59
·
verified ·
1 Parent(s): d82c5f2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +829 -92
app.py CHANGED
@@ -1,96 +1,833 @@
 
1
  import torch
2
- from diffusers import AutoPipelineForText2Image
3
- import gradio as gr
4
-
5
- # เลือก device เป็น cuda ถ้ามี GPU เพื่อให้ offload ทำงานร่วมกับ GPU
6
- device = "cuda" if torch.cuda.is_available() else "cpu"
7
-
8
- print("โหลด FLUX model ด้วยการตั้งค่าสำหรับ offload...")
9
- pipe = AutoPipelineForText2Image.from_pretrained(
10
- "black-forest-labs/FLUX.1-dev",
11
- torch_dtype=torch.float16 # ลด precision เพื่อประหยัดหน่วยความจำ
12
- )
13
-
14
- # ถ้ามี GPU ให้ย้าย pipeline ไปยัง GPU ก่อนเปิด offload (diffusers จะจัดการย้ายพารามิเตอร์)
15
- if device == "cuda":
16
- pipe.to("cuda")
17
-
18
- # เปิดการประหยัดหน่วยความจำ
19
- # 1) attention slicing ลด peak memory ขณะ attention
20
- pipe.enable_attention_slicing()
21
- # 2) VAE slicing ลด memory ตอน decode
22
- try:
23
- pipe.enable_vae_slicing()
24
- except Exception:
25
- pass
26
-
27
- # 3) เปิด CPU offload: ย้ายพารามิเตอร์โมเดลไปเก็บใน CPU RAM แล้วโหลดไป GPU เฉพาะตอนใช้งาน
28
- # ถ้าต้องการให้ใช้โฟลเดอร์สำหรับ offload ชั่วคราว ให้กำหนด offload_folder
29
- try:
30
- pipe.enable_model_cpu_offload(
31
- gpu_id=0, # หมายเลข GPU ที่จะใช้ (ถ้ามีหลายตัว ปรับตามจริง)
32
- offload_folder="./offload", # ถ้าต้องการใช้ disk เป็นสำรอง
33
- pin_memory=True # pin memory ช่วยให้ย้ายข้อมูลเร็วขึ้น
34
- )
35
- except Exception:
36
- # ถ้าเวอร์ชัน diffusers ไม่มีฟังก์ชันนี้ ให้ลอง enable_sequential_cpu_offload
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  try:
38
- pipe.enable_sequential_cpu_offload()
39
- except Exception:
40
- pass
41
-
42
- # เพิ่มการตั้งค่าอื่น ๆ ที่ช่วยลด memory
43
- torch.backends.cudnn.benchmark = False
44
-
45
- # โหลด LoRA (ถ้าจำเป็น) ควรโหลดหลังจาก pipeline ถูกตั้งค่า offload แล้ว
46
- print("โหลด Uncensored LoRA...")
47
- pipe.load_lora_weights(
48
- 'Heartsync/Flux-NSFW-uncensored',
49
- weight_name='lora.safetensors',
50
- adapter_name="uncensored"
51
- )
52
-
53
- # ฟังก์ชันสร้างภาพ
54
- def generate_image(prompt, negative_prompt, guidance_scale, num_inference_steps, width, height, seed):
55
- # แนะนำ: ถ้าต้องการจำกัด VRAM ให้ลด width/height หรือใช้ smaller batch
56
- generator = torch.Generator(device="cpu").manual_seed(int(seed))
57
-
58
- # ถ้า GPU มี ให้ใช้ device="cuda" ในการเรียก แต่ pipeline จะจัดการ offload ให้
59
- out = pipe(
60
- prompt=prompt,
61
- negative_prompt=negative_prompt,
62
- guidance_scale=float(guidance_scale),
63
- num_inference_steps=int(num_inference_steps),
64
- width=int(width),
65
- height=int(height),
66
- generator=generator,
67
- )
68
- image = out.images[0]
69
-
70
- # เคลียร์ cache ของ CUDA ถ้ามี
71
- if torch.cuda.is_available():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  torch.cuda.empty_cache()
74
- except Exception:
75
- pass
76
-
77
- return image
78
-
79
- # Gradio interface (เหมือนเดิม)
80
- iface = gr.Interface(
81
- fn=generate_image,
82
- inputs=[
83
- gr.Textbox(label="Prompt", value="A woman in a sheer white dress standing on a beach at sunset..."),
84
- gr.Textbox(label="Negative Prompt", value="text, watermark, signature, cartoon, anime, illustration, painting, drawing, low quality, blurry"),
85
- gr.Slider(minimum=1.0, maximum=20.0, step=0.1, value=7.0, label="Guidance Scale"),
86
- gr.Slider(minimum=10, maximum=100, step=1, value=28, label="Number of Inference Steps"),
87
- gr.Slider(minimum=256, maximum=1024, step=64, value=1024, label="Width"),
88
- gr.Slider(minimum=256, maximum=1024, step=64, value=1024, label="Height"),
89
- gr.Slider(minimum=0, maximum=99999, step=1, value=42, label="Seed")
90
- ],
91
- outputs="image",
92
- title="FLUX.1-dev with Uncensored LoRA (Offload to RAM)",
93
- description="Generate images using FLUX.1-dev with CPU offload to keep GPU VRAM low."
94
- )
95
-
96
- iface.launch(share=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, jsonify, request, send_file
2
  import torch
3
+ import os
4
+ import time
5
+ import threading
6
+ from datetime import datetime, timedelta
7
+ import cv2
8
+ from werkzeug.utils import secure_filename
9
+ import uuid
10
+ import mimetypes
11
+ import numpy as np
12
+ from PIL import Image
13
+ import schedule
14
+
15
+ # Configuration
16
+ UPLOAD_FOLDER = '/data/uploads'
17
+ OUTPUT_FOLDER = '/data/outputs'
18
+ CLEANUP_INTERVAL_MINUTES = 10
19
+ FILE_MAX_AGE_HOURS = 1
20
+
21
+ # Global application state
22
+ app_state = {
23
+ "cuda_available": torch.cuda.is_available(),
24
+ "processing_active": False,
25
+ "logs": [],
26
+ "processed_files": [],
27
+ "cleanup_stats": {
28
+ "last_cleanup": None,
29
+ "files_deleted": 0,
30
+ "space_freed_mb": 0
31
+ }
32
+ }
33
+
34
+ def ensure_directories():
35
+ """Create necessary directories"""
36
+ directories = [UPLOAD_FOLDER, OUTPUT_FOLDER]
37
+ for directory in directories:
38
+ try:
39
+ os.makedirs(directory, exist_ok=True)
40
+ print(f"✅ Directory verified: {directory}")
41
+ except Exception as e:
42
+ print(f"⚠️ Error creating directory {directory}: {e}")
43
+
44
+ def allowed_file(filename):
45
+ """Check if file has allowed extension"""
46
+ return '.' in filename and \
47
+ filename.rsplit('.', 1)[1].lower() in ['png', 'jpg', 'jpeg', 'gif', 'mp4', 'avi', 'mov', 'mkv']
48
+
49
+ def get_file_mimetype(filename):
50
+ """Get correct mimetype for file"""
51
+ mimetype, _ = mimetypes.guess_type(filename)
52
+ if mimetype is None:
53
+ ext = filename.lower().rsplit('.', 1)[1] if '.' in filename else ''
54
+ if ext in ['mp4', 'avi', 'mov', 'mkv']:
55
+ mimetype = f'video/{ext}'
56
+ elif ext in ['png', 'jpg', 'jpeg', 'gif']:
57
+ mimetype = f'image/{ext}'
58
+ else:
59
+ mimetype = 'application/octet-stream'
60
+ return mimetype
61
+
62
+ def log_message(message):
63
+ """Add message to log with timestamp"""
64
+ timestamp = datetime.now().strftime("%H:%M:%S")
65
+ app_state["logs"].append(f"[{timestamp}] {message}")
66
+ if len(app_state["logs"]) > 100:
67
+ app_state["logs"] = app_state["logs"][-100:]
68
+ print(f"[{timestamp}] {message}")
69
+
70
+ def cleanup_old_files():
71
+ """Delete files older than FILE_MAX_AGE_HOURS"""
72
+ try:
73
+ current_time = datetime.now()
74
+ cutoff_time = current_time - timedelta(hours=FILE_MAX_AGE_HOURS)
75
+
76
+ files_deleted = 0
77
+ space_freed = 0
78
+
79
+ # Clean upload folder
80
+ for folder_path in [UPLOAD_FOLDER, OUTPUT_FOLDER]:
81
+ if not os.path.exists(folder_path):
82
+ continue
83
+
84
+ for filename in os.listdir(folder_path):
85
+ file_path = os.path.join(folder_path, filename)
86
+
87
+ if os.path.isfile(file_path):
88
+ try:
89
+ # Get file modification time
90
+ file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
91
+
92
+ if file_time < cutoff_time:
93
+ # Get file size before deletion
94
+ file_size = os.path.getsize(file_path)
95
+
96
+ # Delete the file
97
+ os.remove(file_path)
98
+
99
+ files_deleted += 1
100
+ space_freed += file_size
101
+
102
+ log_message(f"🗑️ Deleted old file: {filename} ({file_size / (1024*1024):.1f}MB)")
103
+
104
+ except Exception as e:
105
+ log_message(f"⚠️ Error deleting {filename}: {str(e)}")
106
+
107
+ # Update cleanup stats
108
+ app_state["cleanup_stats"]["last_cleanup"] = current_time.strftime("%Y-%m-%d %H:%M:%S")
109
+ app_state["cleanup_stats"]["files_deleted"] += files_deleted
110
+ app_state["cleanup_stats"]["space_freed_mb"] += space_freed / (1024*1024)
111
+
112
+ if files_deleted > 0:
113
+ log_message(f"🧹 Cleanup completed: {files_deleted} files deleted, {space_freed / (1024*1024):.1f}MB freed")
114
+ else:
115
+ log_message(f"🧹 Cleanup completed: No old files to delete")
116
+
117
+ # Clean up processed files list to remove references to deleted files
118
+ valid_processed_files = []
119
+ for file_info in app_state["processed_files"]:
120
+ output_path = os.path.join(OUTPUT_FOLDER, file_info["output_file"])
121
+ if os.path.exists(output_path):
122
+ valid_processed_files.append(file_info)
123
+
124
+ app_state["processed_files"] = valid_processed_files
125
+
126
+ except Exception as e:
127
+ log_message(f"❌ Error during cleanup: {str(e)}")
128
+
129
+ def run_scheduler():
130
+ """Run the file cleanup scheduler in background"""
131
+ def scheduler_worker():
132
+ while True:
133
+ try:
134
+ schedule.run_pending()
135
+ time.sleep(60) # Check every minute
136
+ except Exception as e:
137
+ log_message(f"❌ Scheduler error: {str(e)}")
138
+ time.sleep(300) # Wait 5 minutes before retrying
139
+
140
+ thread = threading.Thread(target=scheduler_worker, daemon=True)
141
+ thread.start()
142
+ log_message(f"🕒 File cleanup scheduler started (every {CLEANUP_INTERVAL_MINUTES} minutes)")
143
+
144
+ def optimize_gpu():
145
+ """Optimize GPU configuration for 4K upscaling"""
146
  try:
147
+ if torch.cuda.is_available():
148
+ torch.backends.cudnn.benchmark = True
149
+ torch.backends.cudnn.allow_tf32 = True
150
+ torch.backends.cuda.matmul.allow_tf32 = True
151
+ torch.cuda.empty_cache()
152
+
153
+ # Test GPU
154
+ test_tensor = torch.randn(100, 100, device='cuda')
155
+ _ = torch.mm(test_tensor, test_tensor)
156
+
157
+ log_message("✅ GPU optimized for 4K upscaling")
158
+ return True
159
+ else:
160
+ log_message("⚠️ CUDA not available")
161
+ return False
162
+ except Exception as e:
163
+ log_message(f"❌ Error optimizing GPU: {str(e)}")
164
+ return False
165
+
166
+ def upscale_image_4k(input_path, output_path):
167
+ """Upscale image to 4K using neural methods"""
168
+ def process_worker():
169
+ try:
170
+ log_message(f"🎨 Starting 4K upscaling: {os.path.basename(input_path)}")
171
+ app_state["processing_active"] = True
172
+
173
+ # Read original image
174
+ image = cv2.imread(input_path)
175
+ if image is None:
176
+ log_message("❌ Error: Could not read image")
177
+ return
178
+
179
+ h, w = image.shape[:2]
180
+ log_message(f"📏 Original resolution: {w}x{h}")
181
+
182
+ # Define target dimensions first
183
+ target_h, target_w = h * 4, w * 4
184
+
185
+ # Check GPU memory availability
186
+ if torch.cuda.is_available():
187
+ device = torch.device('cuda')
188
+ available_memory = torch.cuda.get_device_properties(0).total_memory - torch.cuda.memory_allocated()
189
+ required_memory = w * h * 4 * 4 * 3 * 4 # Conservative estimation
190
+
191
+ if required_memory > available_memory * 0.8:
192
+ log_message(f"⚠️ Image too large for available GPU memory, using CPU")
193
+ device = torch.device('cpu')
194
+ else:
195
+ log_message(f"🚀 Using GPU: {torch.cuda.get_device_name()}")
196
+
197
+ if device.type == 'cuda':
198
+ # Convert image to normalized tensor
199
+ image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
200
+ image_tensor = torch.from_numpy(image_rgb).float().to(device) / 255.0
201
+ image_tensor = image_tensor.permute(2, 0, 1).unsqueeze(0) # BCHW format
202
+
203
+ log_message("🧠 Applying neural upscaling...")
204
+
205
+ with torch.no_grad():
206
+ # Step 1: 2x upscaling with bicubic
207
+ intermediate = torch.nn.functional.interpolate(
208
+ image_tensor,
209
+ size=(h * 2, w * 2),
210
+ mode='bicubic',
211
+ align_corners=False,
212
+ antialias=True
213
+ )
214
+
215
+ # Step 2: Final 2x upscaling with smoothing
216
+ upscaled = torch.nn.functional.interpolate(
217
+ intermediate,
218
+ size=(target_h, target_w),
219
+ mode='bicubic',
220
+ align_corners=False,
221
+ antialias=True
222
+ )
223
+
224
+ # Enhanced sharpening filters
225
+ kernel_size = 3
226
+ sigma = 0.5
227
+ kernel = torch.zeros((kernel_size, kernel_size), device=device)
228
+ center = kernel_size // 2
229
+
230
+ # Create inverted Gaussian kernel for sharpening
231
+ for i in range(kernel_size):
232
+ for j in range(kernel_size):
233
+ dist = ((i - center) ** 2 + (j - center) ** 2) ** 0.5
234
+ kernel[i, j] = torch.exp(-0.5 * (dist / sigma) ** 2)
235
+
236
+ kernel = kernel / kernel.sum()
237
+ sharpen_kernel = torch.zeros_like(kernel)
238
+ sharpen_kernel[center, center] = 2.0
239
+ sharpen_kernel = sharpen_kernel - kernel
240
+ sharpen_kernel = sharpen_kernel.unsqueeze(0).unsqueeze(0)
241
+
242
+ # Apply sharpening to each channel
243
+ enhanced_channels = []
244
+ for i in range(3):
245
+ channel = upscaled[:, i:i+1, :, :]
246
+ padded = torch.nn.functional.pad(channel, (1, 1, 1, 1), mode='reflect')
247
+ enhanced = torch.nn.functional.conv2d(padded, sharpen_kernel)
248
+ enhanced_channels.append(enhanced)
249
+
250
+ enhanced = torch.cat(enhanced_channels, dim=1)
251
+
252
+ # Light smoothing to reduce noise
253
+ gaussian_kernel = torch.tensor([
254
+ [1, 4, 6, 4, 1],
255
+ [4, 16, 24, 16, 4],
256
+ [6, 24, 36, 24, 6],
257
+ [4, 16, 24, 16, 4],
258
+ [1, 4, 6, 4, 1]
259
+ ], dtype=torch.float32, device=device).unsqueeze(0).unsqueeze(0) / 256.0
260
+
261
+ smoothed_channels = []
262
+ for i in range(3):
263
+ channel = enhanced[:, i:i+1, :, :]
264
+ padded = torch.nn.functional.pad(channel, (2, 2, 2, 2), mode='reflect')
265
+ smoothed = torch.nn.functional.conv2d(padded, gaussian_kernel)
266
+ smoothed_channels.append(smoothed)
267
+
268
+ smoothed = torch.cat(smoothed_channels, dim=1)
269
+
270
+ # Blend: 70% enhanced + 30% smoothed for quality/smoothness balance
271
+ final_result = 0.7 * enhanced + 0.3 * smoothed
272
+
273
+ # Clamp values and optimize contrast
274
+ final_result = torch.clamp(final_result, 0, 1)
275
+
276
+ # Adaptive contrast optimization
277
+ for i in range(3):
278
+ channel = final_result[:, i, :, :]
279
+ min_val = channel.min()
280
+ max_val = channel.max()
281
+ if max_val > min_val:
282
+ final_result[:, i, :, :] = (channel - min_val) / (max_val - min_val)
283
+
284
+ # Convert back to image
285
+ result_cpu = final_result.squeeze(0).permute(1, 2, 0).cpu().numpy()
286
+ result_image = (result_cpu * 255).astype(np.uint8)
287
+ result_bgr = cv2.cvtColor(result_image, cv2.COLOR_RGB2BGR)
288
+
289
+ # Save result
290
+ cv2.imwrite(output_path, result_bgr)
291
+ final_h, final_w = result_bgr.shape[:2]
292
+ log_message(f"✅ Upscaling completed: {final_w}x{final_h}")
293
+ log_message(f"📈 Scale factor: {final_w/w:.1f}x")
294
+
295
+ # Memory cleanup
296
+ del image_tensor, upscaled, enhanced, final_result
297
+ torch.cuda.empty_cache()
298
+
299
+ else:
300
+ # CPU fallback
301
+ log_message("⚠️ Using CPU - optimized processing")
302
+
303
+ # Progressive upscaling on CPU
304
+ intermediate = cv2.resize(image, (w * 2, h * 2), interpolation=cv2.INTER_CUBIC)
305
+ upscaled = cv2.resize(intermediate, (target_w, target_h), interpolation=cv2.INTER_CUBIC)
306
+
307
+ # Apply sharpening on CPU
308
+ kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]])
309
+ sharpened = cv2.filter2D(upscaled, -1, kernel)
310
+
311
+ # Blend for smoothing
312
+ final_result = cv2.addWeighted(upscaled, 0.7, sharpened, 0.3, 0)
313
+
314
+ cv2.imwrite(output_path, final_result)
315
+ log_message(f"✅ CPU upscaling completed: {target_w}x{target_h}")
316
+ else:
317
+ # CPU only fallback (no CUDA available)
318
+ log_message("💻 Using CPU processing (CUDA not available)")
319
+
320
+ # Progressive upscaling on CPU
321
+ intermediate = cv2.resize(image, (w * 2, h * 2), interpolation=cv2.INTER_CUBIC)
322
+ upscaled = cv2.resize(intermediate, (target_w, target_h), interpolation=cv2.INTER_CUBIC)
323
+
324
+ # Apply sharpening on CPU
325
+ kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]])
326
+ sharpened = cv2.filter2D(upscaled, -1, kernel)
327
+
328
+ # Blend for smoothing
329
+ final_result = cv2.addWeighted(upscaled, 0.7, sharpened, 0.3, 0)
330
+
331
+ cv2.imwrite(output_path, final_result)
332
+ log_message(f"✅ CPU upscaling completed: {target_w}x{target_h}")
333
+
334
+ # Add to processed files list
335
+ app_state["processed_files"].append({
336
+ "input_file": os.path.basename(input_path),
337
+ "output_file": os.path.basename(output_path),
338
+ "original_size": f"{w}x{h}",
339
+ "upscaled_size": f"{target_w}x{target_h}",
340
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
341
+ })
342
+
343
+ except Exception as e:
344
+ log_message(f"❌ Error in processing: {str(e)}")
345
+ finally:
346
+ app_state["processing_active"] = False
347
+ if torch.cuda.is_available():
348
+ torch.cuda.empty_cache()
349
+
350
+ thread = threading.Thread(target=process_worker)
351
+ thread.daemon = True
352
+ thread.start()
353
+
354
+ def upscale_video_4k(input_path, output_path):
355
+ """Upscale video to 4K frame by frame"""
356
+ def process_worker():
357
+ try:
358
+ log_message(f"🎬 Starting 4K video upscaling: {os.path.basename(input_path)}")
359
+ app_state["processing_active"] = True
360
+
361
+ # Open video
362
+ cap = cv2.VideoCapture(input_path)
363
+ if not cap.isOpened():
364
+ log_message("❌ Error: Could not open video")
365
+ return
366
+
367
+ # Get video properties
368
+ fps = int(cap.get(cv2.CAP_PROP_FPS))
369
+ frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
370
+ w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
371
+ h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
372
+ log_message(f"📹 Video: {w}x{h}, {fps}FPS, {frame_count} frames")
373
+
374
+ # Configure 4K output
375
+ target_w, target_h = w * 4, h * 4
376
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
377
+ out = cv2.VideoWriter(output_path, fourcc, fps, (target_w, target_h))
378
+
379
+ if torch.cuda.is_available():
380
+ device = torch.device('cuda')
381
+ log_message(f"🚀 Processing with GPU: {torch.cuda.get_device_name()}")
382
+ process_frames_gpu(cap, out, device, target_h, target_w, frame_count)
383
+ else:
384
+ log_message("💻 Processing with CPU (may be slower)")
385
+ process_frames_cpu(cap, out, target_h, target_w, frame_count)
386
+
387
+ cap.release()
388
+ out.release()
389
+
390
+ # Verify the output file was created and has content
391
+ if os.path.exists(output_path):
392
+ file_size = os.path.getsize(output_path)
393
+ if file_size > 0:
394
+ log_message(f"✅ 4K video completed: {target_w}x{target_h}")
395
+ log_message(f"📁 Output file size: {file_size / (1024**2):.1f}MB")
396
+ else:
397
+ log_message(f"❌ Output file is empty: {output_path}")
398
+ raise Exception("Output video file is empty")
399
+ else:
400
+ log_message(f"❌ Output file not created: {output_path}")
401
+ raise Exception("Output video file was not created")
402
+
403
+ # Add to processed files list
404
+ app_state["processed_files"].append({
405
+ "input_file": os.path.basename(input_path),
406
+ "output_file": os.path.basename(output_path),
407
+ "original_size": f"{w}x{h}",
408
+ "upscaled_size": f"{target_w}x{target_h}",
409
+ "frame_count": frame_count,
410
+ "fps": fps,
411
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
412
+ })
413
+
414
+ except Exception as e:
415
+ log_message(f"❌ Error processing video: {str(e)}")
416
+ finally:
417
+ app_state["processing_active"] = False
418
+ if torch.cuda.is_available():
419
+ torch.cuda.empty_cache()
420
+
421
+ thread = threading.Thread(target=process_worker)
422
+ thread.daemon = True
423
+ thread.start()
424
+
425
+ def process_frames_cpu(cap, out, target_h, target_w, frame_count):
426
+ """Process video frames using CPU"""
427
+ frame_num = 0
428
+ while True:
429
+ ret, frame = cap.read()
430
+ if not ret:
431
+ break
432
+
433
+ frame_num += 1
434
+
435
+ # Simple CPU upscaling
436
+ upscaled_frame = cv2.resize(frame, (target_w, target_h), interpolation=cv2.INTER_CUBIC)
437
+ out.write(upscaled_frame)
438
+
439
+ # Progress logging
440
+ if frame_num % 30 == 0:
441
+ progress = (frame_num / frame_count) * 100
442
+ log_message(f"🎞️ Processing frame {frame_num}/{frame_count} ({progress:.1f}%)")
443
+
444
+ def process_frames_gpu(cap, out, device, target_h, target_w, frame_count):
445
+ """Process video frames using GPU with PyTorch"""
446
+ frame_num = 0
447
+ torch.backends.cudnn.benchmark = True
448
+
449
+ while True:
450
+ ret, frame = cap.read()
451
+ if not ret:
452
+ break
453
+
454
+ frame_num += 1
455
+
456
  try:
457
+ # Convert to tensor
458
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
459
+ frame_tensor = torch.from_numpy(frame_rgb).float().to(device) / 255.0
460
+ frame_tensor = frame_tensor.permute(2, 0, 1).unsqueeze(0)
461
+
462
+ with torch.no_grad():
463
+ upscaled = torch.nn.functional.interpolate(
464
+ frame_tensor,
465
+ size=(target_h, target_w),
466
+ mode='bicubic',
467
+ align_corners=False
468
+ )
469
+
470
+ # Convert back
471
+ result_cpu = upscaled.squeeze(0).permute(1, 2, 0).cpu().numpy()
472
+ result_frame = (result_cpu * 255).astype(np.uint8)
473
+ result_bgr = cv2.cvtColor(result_frame, cv2.COLOR_RGB2BGR)
474
+ out.write(result_bgr)
475
+
476
+ except Exception as e:
477
+ log_message(f"⚠️ GPU processing failed for frame {frame_num}, using CPU fallback")
478
+ # CPU fallback
479
+ upscaled_frame = cv2.resize(frame, (target_w, target_h), interpolation=cv2.INTER_CUBIC)
480
+ out.write(upscaled_frame)
481
+
482
+ # Progress logging
483
+ if frame_num % 30 == 0:
484
+ progress = (frame_num / frame_count) * 100
485
+ log_message(f"🎞️ Processing frame {frame_num}/{frame_count} ({progress:.1f}%)")
486
+
487
+ # Periodic memory cleanup
488
+ if frame_num % 60 == 0 and torch.cuda.is_available():
489
+ torch.cuda.empty_cache()
490
+
491
+ def process_frame_batch(frame_batch, out, device, target_h, target_w):
492
+ """Process batch of frames on GPU for efficiency"""
493
+ try:
494
+ with torch.no_grad():
495
+ # Convert batch to tensor
496
+ batch_tensors = []
497
+ for frame in frame_batch:
498
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
499
+ frame_tensor = torch.from_numpy(frame_rgb).float().to(device) / 255.0
500
+ frame_tensor = frame_tensor.permute(2, 0, 1) # CHW
501
+ batch_tensors.append(frame_tensor)
502
+
503
+ # Stack in batch
504
+ batch_tensor = torch.stack(batch_tensors, dim=0) # BCHW
505
+
506
+ # Upscale entire batch
507
+ upscaled_batch = torch.nn.functional.interpolate(
508
+ batch_tensor,
509
+ size=(target_h, target_w),
510
+ mode='bicubic',
511
+ align_corners=False,
512
+ antialias=True
513
+ )
514
+
515
+ # Convert each frame back
516
+ for i in range(upscaled_batch.shape[0]):
517
+ result_cpu = upscaled_batch[i].permute(1, 2, 0).cpu().numpy()
518
+ result_frame = (result_cpu * 255).astype(np.uint8)
519
+ result_bgr = cv2.cvtColor(result_frame, cv2.COLOR_RGB2BGR)
520
+ out.write(result_bgr)
521
+
522
+ except Exception as e:
523
+ log_message(f"❌ Error in batch processing: {str(e)}")
524
+ # Fallback: process frames individually
525
+ for frame in frame_batch:
526
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
527
+ frame_tensor = torch.from_numpy(frame_rgb).float().to(device) / 255.0
528
+ frame_tensor = frame_tensor.permute(2, 0, 1).unsqueeze(0)
529
+
530
+ upscaled = torch.nn.functional.interpolate(
531
+ frame_tensor,
532
+ size=(target_h, target_w),
533
+ mode='bicubic',
534
+ align_corners=False
535
+ )
536
+
537
+ result_cpu = upscaled.squeeze(0).permute(1, 2, 0).cpu().numpy()
538
+ result_frame = (result_cpu * 255).astype(np.uint8)
539
+ result_bgr = cv2.cvtColor(result_frame, cv2.COLOR_RGB2BGR)
540
+ out.write(result_bgr)
541
+
542
+ # Initialize directories
543
+ ensure_directories()
544
+
545
+ # Set up file cleanup scheduler
546
+ schedule.every(CLEANUP_INTERVAL_MINUTES).minutes.do(cleanup_old_files)
547
+
548
+ app = Flask(__name__)
549
+
550
+ @app.route('/')
551
+ def index():
552
+ return render_template('index.html')
553
+
554
+ @app.route('/api/system')
555
+ def api_system():
556
+ """Get system information"""
557
+ try:
558
+ info = {}
559
+
560
+ # GPU Info
561
+ if torch.cuda.is_available():
562
+ info["gpu_available"] = True
563
+ info["gpu_name"] = torch.cuda.get_device_name()
564
+
565
+ total_memory = torch.cuda.get_device_properties(0).total_memory
566
+ allocated_memory = torch.cuda.memory_allocated()
567
+
568
+ info["gpu_memory"] = f"{total_memory / (1024**3):.1f}GB"
569
+ info["gpu_memory_used"] = f"{allocated_memory / (1024**3):.1f}GB"
570
+ info["gpu_memory_free"] = f"{(total_memory - allocated_memory) / (1024**3):.1f}GB"
571
+ info["cuda_version"] = torch.version.cuda
572
+ info["pytorch_version"] = torch.__version__
573
+ else:
574
+ info["gpu_available"] = False
575
+ info["gpu_name"] = "CPU Only (No GPU detected)"
576
+ info["gpu_memory"] = "N/A"
577
+ info["gpu_memory_used"] = "N/A"
578
+ info["gpu_memory_free"] = "N/A"
579
+ info["cuda_version"] = "Not available"
580
+ info["pytorch_version"] = torch.__version__
581
+
582
+ # Storage info
583
+ if os.path.exists("/data"):
584
+ info["persistent_storage"] = True
585
+ try:
586
+ upload_files = os.listdir(UPLOAD_FOLDER) if os.path.exists(UPLOAD_FOLDER) else []
587
+ output_files = os.listdir(OUTPUT_FOLDER) if os.path.exists(OUTPUT_FOLDER) else []
588
+
589
+ upload_size = sum(os.path.getsize(os.path.join(UPLOAD_FOLDER, f))
590
+ for f in upload_files if os.path.isfile(os.path.join(UPLOAD_FOLDER, f)))
591
+ output_size = sum(os.path.getsize(os.path.join(OUTPUT_FOLDER, f))
592
+ for f in output_files if os.path.isfile(os.path.join(OUTPUT_FOLDER, f)))
593
+
594
+ info["storage_uploads"] = f"{upload_size / (1024**2):.1f}MB"
595
+ info["storage_outputs"] = f"{output_size / (1024**2):.1f}MB"
596
+ info["upload_files_count"] = len(upload_files)
597
+ info["output_files_count"] = len(output_files)
598
+
599
+ # Add cleanup info
600
+ info["cleanup_stats"] = app_state["cleanup_stats"]
601
+ info["cleanup_interval"] = f"{CLEANUP_INTERVAL_MINUTES} minutes"
602
+ info["file_max_age"] = f"{FILE_MAX_AGE_HOURS} hour(s)"
603
+
604
+ except Exception as e:
605
+ info["storage_uploads"] = f"Error: {str(e)}"
606
+ info["storage_outputs"] = "N/A"
607
+ info["upload_files_count"] = 0
608
+ info["output_files_count"] = 0
609
+ else:
610
+ info["persistent_storage"] = False
611
+
612
+ return jsonify({"success": True, "data": info})
613
+ except Exception as e:
614
+ return jsonify({"success": False, "error": str(e)})
615
+
616
+ @app.route('/api/upload', methods=['POST'])
617
+ def api_upload():
618
+ """Upload and process file for 4K upscaling"""
619
+ try:
620
+ if 'file' not in request.files:
621
+ return jsonify({"success": False, "error": "No file provided"})
622
+
623
+ file = request.files['file']
624
+ if file.filename == '':
625
+ return jsonify({"success": False, "error": "No file selected"})
626
+
627
+ if file and allowed_file(file.filename):
628
+ file_id = str(uuid.uuid4())
629
+ filename = secure_filename(file.filename)
630
+ file_ext = filename.rsplit('.', 1)[1].lower()
631
+
632
+ input_filename = f"{file_id}_input.{file_ext}"
633
+ input_path = os.path.join(UPLOAD_FOLDER, input_filename)
634
+ file.save(input_path)
635
+
636
+ output_filename = f"{file_id}_4k.{file_ext}"
637
+ output_path = os.path.join(OUTPUT_FOLDER, output_filename)
638
+
639
+ if file_ext in ['png', 'jpg', 'jpeg', 'gif']:
640
+ upscale_image_4k(input_path, output_path)
641
+ media_type = "image"
642
+ elif file_ext in ['mp4', 'avi', 'mov', 'mkv']:
643
+ upscale_video_4k(input_path, output_path)
644
+ media_type = "video"
645
+
646
+ log_message(f"📤 File uploaded: {filename}")
647
+ log_message(f"🎯 Starting 4K transformation...")
648
+
649
+ return jsonify({
650
+ "success": True,
651
+ "file_id": file_id,
652
+ "filename": filename,
653
+ "output_filename": output_filename,
654
+ "media_type": media_type,
655
+ "message": "Upload successful, processing started"
656
+ })
657
+ else:
658
+ return jsonify({"success": False, "error": "File type not allowed"})
659
+ except Exception as e:
660
+ return jsonify({"success": False, "error": str(e)})
661
+
662
+ @app.route('/api/processing-status')
663
+ def api_processing_status():
664
+ """Get processing status"""
665
+ return jsonify({
666
+ "success": True,
667
+ "processing": app_state["processing_active"],
668
+ "processed_files": app_state["processed_files"]
669
+ })
670
+
671
+ @app.route('/api/download/<filename>')
672
+ def api_download(filename):
673
+ """Download processed file"""
674
+ try:
675
+ file_path = os.path.join(OUTPUT_FOLDER, filename)
676
+ if os.path.exists(file_path):
677
+ mimetype = get_file_mimetype(filename)
678
+ file_ext = filename.lower().rsplit('.', 1)[1] if '.' in filename else ''
679
+
680
+ if file_ext in ['mp4', 'avi', 'mov', 'mkv']:
681
+ return send_file(
682
+ file_path,
683
+ as_attachment=True,
684
+ download_name=f"4k_upscaled_{filename}",
685
+ mimetype=mimetype
686
+ )
687
+ else:
688
+ return send_file(
689
+ file_path,
690
+ as_attachment=True,
691
+ download_name=f"4k_upscaled_{filename}",
692
+ mimetype=mimetype
693
+ )
694
+ else:
695
+ return jsonify({"error": "File not found"}), 404
696
+ except Exception as e:
697
+ return jsonify({"error": str(e)}), 500
698
+
699
+ @app.route('/api/preview/<filename>')
700
+ def api_preview(filename):
701
+ """Preview processed file"""
702
+ try:
703
+ file_path = os.path.join(OUTPUT_FOLDER, filename)
704
+ if os.path.exists(file_path):
705
+ mimetype = get_file_mimetype(filename)
706
+ return send_file(file_path, mimetype=mimetype)
707
+ else:
708
+ return jsonify({"error": "File not found"}), 404
709
+ except Exception as e:
710
+ return jsonify({"error": str(e)}), 500
711
+
712
+ @app.route('/api/logs')
713
+ def api_logs():
714
+ """Get application logs"""
715
+ return jsonify({
716
+ "success": True,
717
+ "logs": app_state["logs"]
718
+ })
719
+
720
+ @app.route('/api/clear-logs', methods=['POST'])
721
+ def api_clear_logs():
722
+ """Clear application logs"""
723
+ app_state["logs"] = []
724
+ log_message("🧹 Logs cleared")
725
+ return jsonify({"success": True, "message": "Logs cleared"})
726
+
727
+ @app.route('/api/optimize-gpu', methods=['POST'])
728
+ def api_optimize_gpu():
729
+ """Optimize GPU for processing"""
730
+ try:
731
+ success = optimize_gpu()
732
+ if success:
733
+ return jsonify({"success": True, "message": "GPU optimized"})
734
+ else:
735
+ return jsonify({"success": False, "message": "GPU optimization failed"})
736
+ except Exception as e:
737
+ return jsonify({"success": False, "error": str(e)})
738
+
739
+ @app.route('/api/clear-cache', methods=['POST'])
740
+ def api_clear_cache():
741
+ """Clear GPU cache and processed files"""
742
+ try:
743
+ if torch.cuda.is_available():
744
  torch.cuda.empty_cache()
745
+
746
+ app_state["processed_files"] = []
747
+ log_message("🧹 Cache and history cleared")
748
+
749
+ return jsonify({"success": True, "message": "Cache cleared"})
750
+ except Exception as e:
751
+ return jsonify({"success": False, "error": str(e)})
752
+
753
+ @app.route('/api/cleanup-now', methods=['POST'])
754
+ def api_cleanup_now():
755
+ """Manually trigger file cleanup"""
756
+ try:
757
+ cleanup_old_files()
758
+ return jsonify({"success": True, "message": "Manual cleanup completed"})
759
+ except Exception as e:
760
+ return jsonify({"success": False, "error": str(e)})
761
+
762
+ @app.route('/api/storage-stats')
763
+ def api_storage_stats():
764
+ """Get detailed storage statistics"""
765
+ try:
766
+ stats = {
767
+ "cleanup_stats": app_state["cleanup_stats"],
768
+ "current_files": {},
769
+ "total_storage_mb": 0
770
+ }
771
+
772
+ for folder_name, folder_path in [("uploads", UPLOAD_FOLDER), ("outputs", OUTPUT_FOLDER)]:
773
+ if os.path.exists(folder_path):
774
+ files = []
775
+ total_size = 0
776
+
777
+ for filename in os.listdir(folder_path):
778
+ file_path = os.path.join(folder_path, filename)
779
+ if os.path.isfile(file_path):
780
+ file_size = os.path.getsize(file_path)
781
+ file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
782
+
783
+ files.append({
784
+ "name": filename,
785
+ "size_mb": file_size / (1024*1024),
786
+ "created": file_time.strftime("%Y-%m-%d %H:%M:%S"),
787
+ "age_hours": (datetime.now() - file_time).total_seconds() / 3600
788
+ })
789
+ total_size += file_size
790
+
791
+ stats["current_files"][folder_name] = {
792
+ "files": files,
793
+ "count": len(files),
794
+ "total_size_mb": total_size / (1024*1024)
795
+ }
796
+ stats["total_storage_mb"] += total_size / (1024*1024)
797
+
798
+ return jsonify({"success": True, "data": stats})
799
+ except Exception as e:
800
+ return jsonify({"success": False, "error": str(e)})
801
+
802
+ if __name__ == '__main__':
803
+ # Initialize system
804
+ log_message("🚀 4K Upscaler starting...")
805
+
806
+ try:
807
+ # Start file cleanup scheduler
808
+ run_scheduler()
809
+
810
+ # Optimize GPU if available
811
+ if optimize_gpu():
812
+ log_message("✅ GPU optimized for 4K upscaling")
813
+ else:
814
+ log_message("⚠️ GPU optimization failed, using CPU fallback")
815
+
816
+ # Run initial cleanup
817
+ log_message("🧹 Running initial file cleanup...")
818
+ cleanup_old_files()
819
+
820
+ log_message("✅ 4K Upscaler ready")
821
+ log_message("📤 Upload images or videos to upscale to 4K resolution")
822
+ log_message(f"🗑️ Files will be automatically deleted after {FILE_MAX_AGE_HOURS} hour(s)")
823
+
824
+ except Exception as e:
825
+ log_message(f"❌ Initialization error: {str(e)}")
826
+ log_message("⚠️ Starting in fallback mode...")
827
+
828
+ # Run application
829
+ try:
830
+ app.run(host='0.0.0.0', port=7860, debug=False, threaded=True)
831
+ except Exception as e:
832
+ log_message(f"❌ Server startup error: {str(e)}")
833
+ print(f"Critical error: {str(e)}")