Malaji71 commited on
Commit
636a001
·
0 Parent(s):

Upload simplified 4K upscaler with professional interface

Browse files
Files changed (6) hide show
  1. .DS_Store +0 -0
  2. README.md +11 -0
  3. app.py +605 -0
  4. requirements.txt +10 -0
  5. templates/.DS_Store +0 -0
  6. templates/index.html +487 -0
.DS_Store ADDED
Binary file (6.15 kB). View file
 
README.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: 4K4D Mia Research
3
+ emoji: 🐠
4
+ colorFrom: blue
5
+ colorTo: red
6
+ sdk: docker
7
+ pinned: false
8
+ license: apache-2.0
9
+ ---
10
+
11
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,605 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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
+
14
+ # Configuration
15
+ UPLOAD_FOLDER = '/data/uploads'
16
+ OUTPUT_FOLDER = '/data/outputs'
17
+
18
+ # Global application state
19
+ app_state = {
20
+ "cuda_available": torch.cuda.is_available(),
21
+ "processing_active": False,
22
+ "logs": [],
23
+ "processed_files": []
24
+ }
25
+
26
+ def ensure_directories():
27
+ """Create necessary directories"""
28
+ directories = [UPLOAD_FOLDER, OUTPUT_FOLDER]
29
+ for directory in directories:
30
+ try:
31
+ os.makedirs(directory, exist_ok=True)
32
+ print(f"✅ Directory verified: {directory}")
33
+ except Exception as e:
34
+ print(f"⚠️ Error creating directory {directory}: {e}")
35
+
36
+ def allowed_file(filename):
37
+ """Check if file has allowed extension"""
38
+ return '.' in filename and \
39
+ filename.rsplit('.', 1)[1].lower() in ['png', 'jpg', 'jpeg', 'gif', 'mp4', 'avi', 'mov', 'mkv']
40
+
41
+ def get_file_mimetype(filename):
42
+ """Get correct mimetype for file"""
43
+ mimetype, _ = mimetypes.guess_type(filename)
44
+ if mimetype is None:
45
+ ext = filename.lower().rsplit('.', 1)[1] if '.' in filename else ''
46
+ if ext in ['mp4', 'avi', 'mov', 'mkv']:
47
+ mimetype = f'video/{ext}'
48
+ elif ext in ['png', 'jpg', 'jpeg', 'gif']:
49
+ mimetype = f'image/{ext}'
50
+ else:
51
+ mimetype = 'application/octet-stream'
52
+ return mimetype
53
+
54
+ def log_message(message):
55
+ """Add message to log with timestamp"""
56
+ timestamp = datetime.now().strftime("%H:%M:%S")
57
+ app_state["logs"].append(f"[{timestamp}] {message}")
58
+ if len(app_state["logs"]) > 100:
59
+ app_state["logs"] = app_state["logs"][-100:]
60
+ print(f"[{timestamp}] {message}")
61
+
62
+ def optimize_gpu():
63
+ """Optimize GPU configuration for 4K upscaling"""
64
+ try:
65
+ if torch.cuda.is_available():
66
+ torch.backends.cudnn.benchmark = True
67
+ torch.backends.cudnn.allow_tf32 = True
68
+ torch.backends.cuda.matmul.allow_tf32 = True
69
+ torch.cuda.empty_cache()
70
+
71
+ # Test GPU
72
+ test_tensor = torch.randn(100, 100, device='cuda')
73
+ _ = torch.mm(test_tensor, test_tensor)
74
+
75
+ log_message("✅ GPU optimized for 4K upscaling")
76
+ return True
77
+ else:
78
+ log_message("⚠️ CUDA not available")
79
+ return False
80
+ except Exception as e:
81
+ log_message(f"❌ Error optimizing GPU: {str(e)}")
82
+ return False
83
+
84
+ def upscale_image_4k(input_path, output_path):
85
+ """Upscale image to 4K using neural methods"""
86
+ def process_worker():
87
+ try:
88
+ log_message(f"🎨 Starting 4K upscaling: {os.path.basename(input_path)}")
89
+ app_state["processing_active"] = True
90
+
91
+ # Read original image
92
+ image = cv2.imread(input_path)
93
+ if image is None:
94
+ log_message("❌ Error: Could not read image")
95
+ return
96
+
97
+ h, w = image.shape[:2]
98
+ log_message(f"📏 Original resolution: {w}x{h}")
99
+
100
+ # Check GPU memory availability
101
+ if torch.cuda.is_available():
102
+ device = torch.device('cuda')
103
+ available_memory = torch.cuda.get_device_properties(0).total_memory - torch.cuda.memory_allocated()
104
+ required_memory = w * h * 4 * 4 * 3 * 4 # Conservative estimation
105
+
106
+ if required_memory > available_memory * 0.8:
107
+ log_message(f"⚠️ Image too large for available GPU memory, using CPU")
108
+ device = torch.device('cpu')
109
+ else:
110
+ log_message(f"🚀 Using GPU: {torch.cuda.get_device_name()}")
111
+
112
+ if device.type == 'cuda':
113
+ # Convert image to normalized tensor
114
+ image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
115
+ image_tensor = torch.from_numpy(image_rgb).float().to(device) / 255.0
116
+ image_tensor = image_tensor.permute(2, 0, 1).unsqueeze(0) # BCHW format
117
+
118
+ log_message("🧠 Applying neural upscaling...")
119
+
120
+ # Progressive upscaling for better quality
121
+ target_h, target_w = h * 4, w * 4
122
+
123
+ with torch.no_grad():
124
+ # Step 1: 2x upscaling with bicubic
125
+ intermediate = torch.nn.functional.interpolate(
126
+ image_tensor,
127
+ size=(h * 2, w * 2),
128
+ mode='bicubic',
129
+ align_corners=False,
130
+ antialias=True
131
+ )
132
+
133
+ # Step 2: Final 2x upscaling with smoothing
134
+ upscaled = torch.nn.functional.interpolate(
135
+ intermediate,
136
+ size=(target_h, target_w),
137
+ mode='bicubic',
138
+ align_corners=False,
139
+ antialias=True
140
+ )
141
+
142
+ # Enhanced sharpening filters
143
+ kernel_size = 3
144
+ sigma = 0.5
145
+ kernel = torch.zeros((kernel_size, kernel_size), device=device)
146
+ center = kernel_size // 2
147
+
148
+ # Create inverted Gaussian kernel for sharpening
149
+ for i in range(kernel_size):
150
+ for j in range(kernel_size):
151
+ dist = ((i - center) ** 2 + (j - center) ** 2) ** 0.5
152
+ kernel[i, j] = torch.exp(-0.5 * (dist / sigma) ** 2)
153
+
154
+ kernel = kernel / kernel.sum()
155
+ sharpen_kernel = torch.zeros_like(kernel)
156
+ sharpen_kernel[center, center] = 2.0
157
+ sharpen_kernel = sharpen_kernel - kernel
158
+ sharpen_kernel = sharpen_kernel.unsqueeze(0).unsqueeze(0)
159
+
160
+ # Apply sharpening to each channel
161
+ enhanced_channels = []
162
+ for i in range(3):
163
+ channel = upscaled[:, i:i+1, :, :]
164
+ padded = torch.nn.functional.pad(channel, (1, 1, 1, 1), mode='reflect')
165
+ enhanced = torch.nn.functional.conv2d(padded, sharpen_kernel)
166
+ enhanced_channels.append(enhanced)
167
+
168
+ enhanced = torch.cat(enhanced_channels, dim=1)
169
+
170
+ # Light smoothing to reduce noise
171
+ gaussian_kernel = torch.tensor([
172
+ [1, 4, 6, 4, 1],
173
+ [4, 16, 24, 16, 4],
174
+ [6, 24, 36, 24, 6],
175
+ [4, 16, 24, 16, 4],
176
+ [1, 4, 6, 4, 1]
177
+ ], dtype=torch.float32, device=device).unsqueeze(0).unsqueeze(0) / 256.0
178
+
179
+ smoothed_channels = []
180
+ for i in range(3):
181
+ channel = enhanced[:, i:i+1, :, :]
182
+ padded = torch.nn.functional.pad(channel, (2, 2, 2, 2), mode='reflect')
183
+ smoothed = torch.nn.functional.conv2d(padded, gaussian_kernel)
184
+ smoothed_channels.append(smoothed)
185
+
186
+ smoothed = torch.cat(smoothed_channels, dim=1)
187
+
188
+ # Blend: 70% enhanced + 30% smoothed for quality/smoothness balance
189
+ final_result = 0.7 * enhanced + 0.3 * smoothed
190
+
191
+ # Clamp values and optimize contrast
192
+ final_result = torch.clamp(final_result, 0, 1)
193
+
194
+ # Adaptive contrast optimization
195
+ for i in range(3):
196
+ channel = final_result[:, i, :, :]
197
+ min_val = channel.min()
198
+ max_val = channel.max()
199
+ if max_val > min_val:
200
+ final_result[:, i, :, :] = (channel - min_val) / (max_val - min_val)
201
+
202
+ # Convert back to image
203
+ result_cpu = final_result.squeeze(0).permute(1, 2, 0).cpu().numpy()
204
+ result_image = (result_cpu * 255).astype(np.uint8)
205
+ result_bgr = cv2.cvtColor(result_image, cv2.COLOR_RGB2BGR)
206
+
207
+ # Save result
208
+ cv2.imwrite(output_path, result_bgr)
209
+ final_h, final_w = result_bgr.shape[:2]
210
+ log_message(f"✅ Upscaling completed: {final_w}x{final_h}")
211
+ log_message(f"📈 Scale factor: {final_w/w:.1f}x")
212
+
213
+ # Memory cleanup
214
+ del image_tensor, upscaled, enhanced, final_result
215
+ torch.cuda.empty_cache()
216
+
217
+ else:
218
+ # CPU fallback
219
+ log_message("⚠️ Using CPU - optimized processing")
220
+ target_h, target_w = h * 4, w * 4
221
+
222
+ # Progressive upscaling on CPU
223
+ intermediate = cv2.resize(image, (w * 2, h * 2), interpolation=cv2.INTER_CUBIC)
224
+ upscaled = cv2.resize(intermediate, (target_w, target_h), interpolation=cv2.INTER_CUBIC)
225
+
226
+ # Apply sharpening on CPU
227
+ kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]])
228
+ sharpened = cv2.filter2D(upscaled, -1, kernel)
229
+
230
+ # Blend for smoothing
231
+ final_result = cv2.addWeighted(upscaled, 0.7, sharpened, 0.3, 0)
232
+
233
+ cv2.imwrite(output_path, final_result)
234
+ log_message(f"✅ CPU upscaling completed: {target_w}x{target_h}")
235
+
236
+ # Add to processed files list
237
+ app_state["processed_files"].append({
238
+ "input_file": os.path.basename(input_path),
239
+ "output_file": os.path.basename(output_path),
240
+ "original_size": f"{w}x{h}",
241
+ "upscaled_size": f"{target_w}x{target_h}",
242
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
243
+ })
244
+
245
+ except Exception as e:
246
+ log_message(f"❌ Error in processing: {str(e)}")
247
+ finally:
248
+ app_state["processing_active"] = False
249
+ if torch.cuda.is_available():
250
+ torch.cuda.empty_cache()
251
+
252
+ thread = threading.Thread(target=process_worker)
253
+ thread.daemon = True
254
+ thread.start()
255
+
256
+ def upscale_video_4k(input_path, output_path):
257
+ """Upscale video to 4K frame by frame"""
258
+ def process_worker():
259
+ try:
260
+ log_message(f"🎬 Starting 4K video upscaling: {os.path.basename(input_path)}")
261
+ app_state["processing_active"] = True
262
+
263
+ # Open video
264
+ cap = cv2.VideoCapture(input_path)
265
+ if not cap.isOpened():
266
+ log_message("❌ Error: Could not open video")
267
+ return
268
+
269
+ # Get video properties
270
+ fps = int(cap.get(cv2.CAP_PROP_FPS))
271
+ frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
272
+ w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
273
+ h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
274
+ log_message(f"📹 Video: {w}x{h}, {fps}FPS, {frame_count} frames")
275
+
276
+ # Configure 4K output
277
+ target_w, target_h = w * 4, h * 4
278
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
279
+ out = cv2.VideoWriter(output_path, fourcc, fps, (target_w, target_h))
280
+
281
+ if torch.cuda.is_available():
282
+ device = torch.device('cuda')
283
+ log_message(f"🚀 Processing with GPU: {torch.cuda.get_device_name()}")
284
+
285
+ # Batch processing for efficiency
286
+ batch_size = 4
287
+ frame_batch = []
288
+ frame_num = 0
289
+
290
+ torch.backends.cudnn.benchmark = True
291
+
292
+ while True:
293
+ ret, frame = cap.read()
294
+ if not ret:
295
+ # Process remaining batch
296
+ if frame_batch:
297
+ process_frame_batch(frame_batch, out, device, target_h, target_w)
298
+ break
299
+
300
+ frame_num += 1
301
+ frame_batch.append(frame)
302
+
303
+ # Process when batch is full
304
+ if len(frame_batch) == batch_size:
305
+ process_frame_batch(frame_batch, out, device, target_h, target_w)
306
+ frame_batch = []
307
+
308
+ # Progress logging
309
+ if frame_num % 30 == 0:
310
+ progress = (frame_num / frame_count) * 100
311
+ log_message(f"🎞️ Processing frame {frame_num}/{frame_count} ({progress:.1f}%)")
312
+
313
+ # Periodic memory cleanup
314
+ if frame_num % 120 == 0:
315
+ torch.cuda.empty_cache()
316
+
317
+ cap.release()
318
+ out.release()
319
+ log_message(f"✅ 4K video completed: {target_w}x{target_h}")
320
+
321
+ # Add to processed files list
322
+ app_state["processed_files"].append({
323
+ "input_file": os.path.basename(input_path),
324
+ "output_file": os.path.basename(output_path),
325
+ "original_size": f"{w}x{h}",
326
+ "upscaled_size": f"{target_w}x{target_h}",
327
+ "frame_count": frame_count,
328
+ "fps": fps,
329
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
330
+ })
331
+
332
+ except Exception as e:
333
+ log_message(f"❌ Error processing video: {str(e)}")
334
+ finally:
335
+ app_state["processing_active"] = False
336
+ if torch.cuda.is_available():
337
+ torch.cuda.empty_cache()
338
+
339
+ thread = threading.Thread(target=process_worker)
340
+ thread.daemon = True
341
+ thread.start()
342
+
343
+ def process_frame_batch(frame_batch, out, device, target_h, target_w):
344
+ """Process batch of frames on GPU for efficiency"""
345
+ try:
346
+ with torch.no_grad():
347
+ # Convert batch to tensor
348
+ batch_tensors = []
349
+ for frame in frame_batch:
350
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
351
+ frame_tensor = torch.from_numpy(frame_rgb).float().to(device) / 255.0
352
+ frame_tensor = frame_tensor.permute(2, 0, 1) # CHW
353
+ batch_tensors.append(frame_tensor)
354
+
355
+ # Stack in batch
356
+ batch_tensor = torch.stack(batch_tensors, dim=0) # BCHW
357
+
358
+ # Upscale entire batch
359
+ upscaled_batch = torch.nn.functional.interpolate(
360
+ batch_tensor,
361
+ size=(target_h, target_w),
362
+ mode='bicubic',
363
+ align_corners=False,
364
+ antialias=True
365
+ )
366
+
367
+ # Convert each frame back
368
+ for i in range(upscaled_batch.shape[0]):
369
+ result_cpu = upscaled_batch[i].permute(1, 2, 0).cpu().numpy()
370
+ result_frame = (result_cpu * 255).astype(np.uint8)
371
+ result_bgr = cv2.cvtColor(result_frame, cv2.COLOR_RGB2BGR)
372
+ out.write(result_bgr)
373
+
374
+ except Exception as e:
375
+ log_message(f"❌ Error in batch processing: {str(e)}")
376
+ # Fallback: process frames individually
377
+ for frame in frame_batch:
378
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
379
+ frame_tensor = torch.from_numpy(frame_rgb).float().to(device) / 255.0
380
+ frame_tensor = frame_tensor.permute(2, 0, 1).unsqueeze(0)
381
+
382
+ upscaled = torch.nn.functional.interpolate(
383
+ frame_tensor,
384
+ size=(target_h, target_w),
385
+ mode='bicubic',
386
+ align_corners=False
387
+ )
388
+
389
+ result_cpu = upscaled.squeeze(0).permute(1, 2, 0).cpu().numpy()
390
+ result_frame = (result_cpu * 255).astype(np.uint8)
391
+ result_bgr = cv2.cvtColor(result_frame, cv2.COLOR_RGB2BGR)
392
+ out.write(result_bgr)
393
+
394
+ # Initialize directories
395
+ ensure_directories()
396
+
397
+ app = Flask(__name__)
398
+
399
+ @app.route('/')
400
+ def index():
401
+ return render_template('index.html')
402
+
403
+ @app.route('/api/system')
404
+ def api_system():
405
+ """Get system information"""
406
+ try:
407
+ info = {}
408
+
409
+ # GPU Info
410
+ if torch.cuda.is_available():
411
+ info["gpu_available"] = True
412
+ info["gpu_name"] = torch.cuda.get_device_name()
413
+
414
+ total_memory = torch.cuda.get_device_properties(0).total_memory
415
+ allocated_memory = torch.cuda.memory_allocated()
416
+
417
+ info["gpu_memory"] = f"{total_memory / (1024**3):.1f}GB"
418
+ info["gpu_memory_used"] = f"{allocated_memory / (1024**3):.1f}GB"
419
+ info["gpu_memory_free"] = f"{(total_memory - allocated_memory) / (1024**3):.1f}GB"
420
+ info["cuda_version"] = torch.version.cuda
421
+ info["pytorch_version"] = torch.__version__
422
+ else:
423
+ info["gpu_available"] = False
424
+ info["gpu_name"] = "No GPU detected"
425
+
426
+ # Storage info
427
+ if os.path.exists("/data"):
428
+ info["persistent_storage"] = True
429
+ try:
430
+ upload_size = sum(os.path.getsize(os.path.join(UPLOAD_FOLDER, f))
431
+ for f in os.listdir(UPLOAD_FOLDER) if os.path.isfile(os.path.join(UPLOAD_FOLDER, f)))
432
+ output_size = sum(os.path.getsize(os.path.join(OUTPUT_FOLDER, f))
433
+ for f in os.listdir(OUTPUT_FOLDER) if os.path.isfile(os.path.join(OUTPUT_FOLDER, f)))
434
+
435
+ info["storage_uploads"] = f"{upload_size / (1024**2):.1f}MB"
436
+ info["storage_outputs"] = f"{output_size / (1024**2):.1f}MB"
437
+ except:
438
+ info["storage_uploads"] = "N/A"
439
+ info["storage_outputs"] = "N/A"
440
+
441
+ return jsonify({"success": True, "data": info})
442
+ except Exception as e:
443
+ return jsonify({"success": False, "error": str(e)})
444
+
445
+ @app.route('/api/upload', methods=['POST'])
446
+ def api_upload():
447
+ """Upload and process file for 4K upscaling"""
448
+ try:
449
+ if 'file' not in request.files:
450
+ return jsonify({"success": False, "error": "No file provided"})
451
+
452
+ file = request.files['file']
453
+ if file.filename == '':
454
+ return jsonify({"success": False, "error": "No file selected"})
455
+
456
+ if file and allowed_file(file.filename):
457
+ file_id = str(uuid.uuid4())
458
+ filename = secure_filename(file.filename)
459
+ file_ext = filename.rsplit('.', 1)[1].lower()
460
+
461
+ input_filename = f"{file_id}_input.{file_ext}"
462
+ input_path = os.path.join(UPLOAD_FOLDER, input_filename)
463
+ file.save(input_path)
464
+
465
+ output_filename = f"{file_id}_4k.{file_ext}"
466
+ output_path = os.path.join(OUTPUT_FOLDER, output_filename)
467
+
468
+ if file_ext in ['png', 'jpg', 'jpeg', 'gif']:
469
+ upscale_image_4k(input_path, output_path)
470
+ media_type = "image"
471
+ elif file_ext in ['mp4', 'avi', 'mov', 'mkv']:
472
+ upscale_video_4k(input_path, output_path)
473
+ media_type = "video"
474
+
475
+ log_message(f"📤 File uploaded: {filename}")
476
+ log_message(f"🎯 Starting 4K transformation...")
477
+
478
+ return jsonify({
479
+ "success": True,
480
+ "file_id": file_id,
481
+ "filename": filename,
482
+ "output_filename": output_filename,
483
+ "media_type": media_type,
484
+ "message": "Upload successful, processing started"
485
+ })
486
+ else:
487
+ return jsonify({"success": False, "error": "File type not allowed"})
488
+ except Exception as e:
489
+ return jsonify({"success": False, "error": str(e)})
490
+
491
+ @app.route('/api/processing-status')
492
+ def api_processing_status():
493
+ """Get processing status"""
494
+ return jsonify({
495
+ "success": True,
496
+ "processing": app_state["processing_active"],
497
+ "processed_files": app_state["processed_files"]
498
+ })
499
+
500
+ @app.route('/api/download/<filename>')
501
+ def api_download(filename):
502
+ """Download processed file"""
503
+ try:
504
+ file_path = os.path.join(OUTPUT_FOLDER, filename)
505
+ if os.path.exists(file_path):
506
+ mimetype = get_file_mimetype(filename)
507
+ file_ext = filename.lower().rsplit('.', 1)[1] if '.' in filename else ''
508
+
509
+ if file_ext in ['mp4', 'avi', 'mov', 'mkv']:
510
+ return send_file(
511
+ file_path,
512
+ as_attachment=True,
513
+ download_name=f"4k_upscaled_{filename}",
514
+ mimetype=mimetype
515
+ )
516
+ else:
517
+ return send_file(
518
+ file_path,
519
+ as_attachment=True,
520
+ download_name=f"4k_upscaled_{filename}",
521
+ mimetype=mimetype
522
+ )
523
+ else:
524
+ return jsonify({"error": "File not found"}), 404
525
+ except Exception as e:
526
+ return jsonify({"error": str(e)}), 500
527
+
528
+ @app.route('/api/preview/<filename>')
529
+ def api_preview(filename):
530
+ """Preview processed file"""
531
+ try:
532
+ file_path = os.path.join(OUTPUT_FOLDER, filename)
533
+ if os.path.exists(file_path):
534
+ mimetype = get_file_mimetype(filename)
535
+ return send_file(file_path, mimetype=mimetype)
536
+ else:
537
+ return jsonify({"error": "File not found"}), 404
538
+ except Exception as e:
539
+ return jsonify({"error": str(e)}), 500
540
+
541
+ @app.route('/api/logs')
542
+ def api_logs():
543
+ """Get application logs"""
544
+ return jsonify({
545
+ "success": True,
546
+ "logs": app_state["logs"]
547
+ })
548
+
549
+ @app.route('/api/clear-logs', methods=['POST'])
550
+ def api_clear_logs():
551
+ """Clear application logs"""
552
+ app_state["logs"] = []
553
+ log_message("🧹 Logs cleared")
554
+ return jsonify({"success": True, "message": "Logs cleared"})
555
+
556
+ @app.route('/api/optimize-gpu', methods=['POST'])
557
+ def api_optimize_gpu():
558
+ """Optimize GPU for processing"""
559
+ try:
560
+ success = optimize_gpu()
561
+ if success:
562
+ return jsonify({"success": True, "message": "GPU optimized"})
563
+ else:
564
+ return jsonify({"success": False, "message": "GPU optimization failed"})
565
+ except Exception as e:
566
+ return jsonify({"success": False, "error": str(e)})
567
+
568
+ @app.route('/api/clear-cache', methods=['POST'])
569
+ def api_clear_cache():
570
+ """Clear GPU cache and processed files"""
571
+ try:
572
+ if torch.cuda.is_available():
573
+ torch.cuda.empty_cache()
574
+
575
+ app_state["processed_files"] = []
576
+ log_message("🧹 Cache and history cleared")
577
+
578
+ return jsonify({"success": True, "message": "Cache cleared"})
579
+ except Exception as e:
580
+ return jsonify({"success": False, "error": str(e)})
581
+
582
+ if __name__ == '__main__':
583
+ # Initialize system
584
+ log_message("🚀 4K Upscaler starting...")
585
+
586
+ try:
587
+ # Optimize GPU if available
588
+ if optimize_gpu():
589
+ log_message("✅ GPU optimized for 4K upscaling")
590
+ else:
591
+ log_message("⚠️ GPU optimization failed, using CPU fallback")
592
+
593
+ log_message("✅ 4K Upscaler ready")
594
+ log_message("📤 Upload images or videos to upscale to 4K resolution")
595
+
596
+ except Exception as e:
597
+ log_message(f"❌ Initialization error: {str(e)}")
598
+ log_message("⚠️ Starting in fallback mode...")
599
+
600
+ # Run application
601
+ try:
602
+ app.run(host='0.0.0.0', port=7860, debug=False, threaded=True)
603
+ except Exception as e:
604
+ log_message(f"❌ Server startup error: {str(e)}")
605
+ print(f"Critical error: {str(e)}")
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ flask>=2.0.0
2
+ requests>=2.28.0
3
+ opencv-python-headless
4
+ numpy>=1.21.0,<2.0.0
5
+ pillow>=9.0.0
6
+ tqdm
7
+ pyyaml
8
+ rich
9
+ click
10
+ packaging
templates/.DS_Store ADDED
Binary file (6.15 kB). View file
 
templates/index.html ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>🚀 4K Upscaler</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body {
10
+ font-family: 'Inter', 'SF Pro Display', system-ui, -apple-system, sans-serif;
11
+ background: #0a0a0a;
12
+ color: #ffffff; min-height: 100vh; overflow-x: hidden;
13
+ }
14
+ .container { max-width: 1400px; margin: 0 auto; padding: 20px; }
15
+ .header { text-align: center; margin-bottom: 40px; }
16
+ .header h1 {
17
+ font-size: 2.5rem; font-weight: 700;
18
+ color: #ffffff;
19
+ margin-bottom: 10px;
20
+ letter-spacing: -0.02em;
21
+ }
22
+ .subtitle {
23
+ font-size: 1.1rem; opacity: 0.7; margin-bottom: 20px;
24
+ color: #cccccc;
25
+ font-weight: 400;
26
+ }
27
+ .status-bar {
28
+ display: flex; justify-content: center; gap: 15px; margin-bottom: 30px;
29
+ flex-wrap: wrap;
30
+ }
31
+ .status-badge {
32
+ padding: 8px 16px; background: #1a1a1a;
33
+ border-radius: 6px;
34
+ border: 1px solid #333333;
35
+ font-size: 0.85rem; font-weight: 500;
36
+ color: #ffffff;
37
+ }
38
+ .cards-grid {
39
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
40
+ gap: 20px; margin-bottom: 40px;
41
+ }
42
+ .card {
43
+ background: #111111;
44
+ border-radius: 8px; padding: 24px;
45
+ border: 1px solid #333333;
46
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
47
+ transition: all 0.2s ease;
48
+ }
49
+ .card:hover {
50
+ border-color: #555555;
51
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);
52
+ }
53
+ .card h3 {
54
+ font-size: 1.2rem; margin-bottom: 16px; display: flex;
55
+ align-items: center; gap: 8px; color: #ffffff;
56
+ font-weight: 600;
57
+ }
58
+ .card-icon { font-size: 1.2rem; }
59
+ .btn {
60
+ background: #2d2d2d;
61
+ border: 1px solid #404040; padding: 10px 16px; color: #ffffff;
62
+ font-size: 0.9rem; font-weight: 500; cursor: pointer;
63
+ transition: all 0.2s ease; margin: 4px;
64
+ border-radius: 6px; min-width: 100px;
65
+ }
66
+ .btn:hover {
67
+ background: #404040;
68
+ border-color: #555555;
69
+ }
70
+ .btn:disabled {
71
+ background: #1a1a1a; cursor: not-allowed;
72
+ color: #666666; border-color: #2a2a2a;
73
+ }
74
+ .file-upload {
75
+ border: 2px dashed #404040;
76
+ border-radius: 8px; padding: 32px; text-align: center;
77
+ background: #0f0f0f; cursor: pointer;
78
+ transition: all 0.2s ease; margin-bottom: 16px;
79
+ }
80
+ .file-upload:hover {
81
+ border-color: #666666; background: #151515;
82
+ }
83
+ .file-upload.dragover {
84
+ border-color: #888888; background: #1a1a1a;
85
+ }
86
+ .file-input { display: none; }
87
+ .progress-container {
88
+ background: #1a1a1a; border-radius: 6px;
89
+ padding: 16px; margin-top: 16px; display: none;
90
+ border: 1px solid #333333;
91
+ }
92
+ .progress-bar {
93
+ width: 100%; height: 6px; background: #2a2a2a;
94
+ border-radius: 3px; overflow: hidden; margin-bottom: 8px;
95
+ }
96
+ .progress-fill {
97
+ height: 100%; background: #ffffff;
98
+ width: 0%; transition: width 0.3s ease;
99
+ }
100
+ .logs-container {
101
+ background: #050505; border-radius: 6px;
102
+ padding: 16px; max-height: 300px; overflow-y: auto;
103
+ font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; font-size: 0.8rem;
104
+ border: 1px solid #222222;
105
+ }
106
+ .log-entry {
107
+ margin: 4px 0; padding: 4px 8px; border-radius: 3px;
108
+ background: #0a0a0a; color: #cccccc;
109
+ border-left: 2px solid #333333;
110
+ }
111
+ .results-grid {
112
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
113
+ gap: 16px; margin-top: 16px;
114
+ }
115
+ .result-card {
116
+ background: #111111; border-radius: 6px;
117
+ padding: 16px; border: 1px solid #333333;
118
+ }
119
+ .result-preview {
120
+ width: 100%; height: 180px; background: #0a0a0a;
121
+ border-radius: 4px; margin-bottom: 12px; object-fit: cover;
122
+ border: 1px solid #222222;
123
+ }
124
+ .processing-indicator {
125
+ display: none; text-align: center; margin: 16px 0;
126
+ }
127
+ .spinner {
128
+ border: 3px solid #333333;
129
+ border-radius: 50%; border-top: 3px solid #ffffff;
130
+ width: 32px; height: 32px; animation: spin 1s linear infinite;
131
+ margin: 0 auto 8px;
132
+ }
133
+ @keyframes spin {
134
+ 0% { transform: rotate(0deg); }
135
+ 100% { transform: rotate(360deg); }
136
+ }
137
+ </style>
138
+ </head>
139
+ <body>
140
+ <div class="container">
141
+ <div class="header">
142
+ <h1>4K Upscaler</h1>
143
+ <div class="subtitle">Professional AI-powered image and video enhancement</div>
144
+ </div>
145
+
146
+ <div class="status-bar">
147
+ <div class="status-badge" id="gpu-status">Checking GPU...</div>
148
+ <div class="status-badge" id="memory-status">Memory: --</div>
149
+ <div class="status-badge" id="processing-status">Ready</div>
150
+ </div>
151
+
152
+ <div class="cards-grid">
153
+ <!-- Upload Card -->
154
+ <div class="card">
155
+ <h3><span class="card-icon">⬆</span>Upload & Process</h3>
156
+ <div class="file-upload" id="fileUpload">
157
+ <div style="font-size: 1.5rem; margin-bottom: 8px;">📁</div>
158
+ <div style="font-size: 1rem; margin-bottom: 4px; font-weight: 500;">Drop files here or click to browse</div>
159
+ <div style="opacity: 0.6; font-size: 0.85rem;">Supports: PNG, JPG, GIF, MP4, AVI, MOV, MKV</div>
160
+ <input type="file" id="fileInput" class="file-input" accept=".png,.jpg,.jpeg,.gif,.mp4,.avi,.mov,.mkv" multiple>
161
+ </div>
162
+ <div class="processing-indicator" id="processingIndicator">
163
+ <div class="spinner"></div>
164
+ <div>Processing your file...</div>
165
+ </div>
166
+ <div style="text-align: center;">
167
+ <button class="btn" onclick="clearCache()">Clear Cache</button>
168
+ <button class="btn" onclick="optimizeGPU()">Optimize GPU</button>
169
+ </div>
170
+ </div>
171
+
172
+ <!-- System Info Card -->
173
+ <div class="card">
174
+ <h3><span class="card-icon">⚙</span>System Status</h3>
175
+ <div id="systemInfo">
176
+ <div style="margin: 10px 0;">Loading system information...</div>
177
+ </div>
178
+ <div style="text-align: center; margin-top: 16px;">
179
+ <button class="btn" onclick="refreshSystemInfo()">Refresh</button>
180
+ <button class="btn" onclick="toggleLogs()">View Logs</button>
181
+ </div>
182
+ </div>
183
+ </div>
184
+
185
+ <!-- Results Section -->
186
+ <div class="card" id="resultsSection" style="display: none;">
187
+ <h3><span class="card-icon">✓</span>Processed Files</h3>
188
+ <div id="resultsGrid" class="results-grid"></div>
189
+ </div>
190
+
191
+ <!-- Logs Section -->
192
+ <div class="card" id="logsSection" style="display: none;">
193
+ <h3><span class="card-icon">□</span>Processing Logs</h3>
194
+ <div class="logs-container" id="logsContainer"></div>
195
+ <div style="text-align: center; margin-top: 16px;">
196
+ <button class="btn" onclick="clearLogs()">Clear Logs</button>
197
+ <button class="btn" onclick="refreshLogs()">Refresh</button>
198
+ </div>
199
+ </div>
200
+ </div>
201
+
202
+ <script>
203
+ let isProcessing = false;
204
+ let logsVisible = false;
205
+
206
+ // Initialize app
207
+ document.addEventListener('DOMContentLoaded', function() {
208
+ setupFileUpload();
209
+ refreshSystemInfo();
210
+ startStatusUpdates();
211
+ });
212
+
213
+ function setupFileUpload() {
214
+ const fileUpload = document.getElementById('fileUpload');
215
+ const fileInput = document.getElementById('fileInput');
216
+
217
+ fileUpload.addEventListener('click', () => fileInput.click());
218
+ fileUpload.addEventListener('dragover', handleDragOver);
219
+ fileUpload.addEventListener('dragleave', handleDragLeave);
220
+ fileUpload.addEventListener('drop', handleDrop);
221
+ fileInput.addEventListener('change', handleFileSelect);
222
+ }
223
+
224
+ function handleDragOver(e) {
225
+ e.preventDefault();
226
+ e.currentTarget.classList.add('dragover');
227
+ }
228
+
229
+ function handleDragLeave(e) {
230
+ e.preventDefault();
231
+ e.currentTarget.classList.remove('dragover');
232
+ }
233
+
234
+ function handleDrop(e) {
235
+ e.preventDefault();
236
+ e.currentTarget.classList.remove('dragover');
237
+ const files = e.dataTransfer.files;
238
+ if (files.length > 0) {
239
+ processFiles(files);
240
+ }
241
+ }
242
+
243
+ function handleFileSelect(e) {
244
+ const files = e.target.files;
245
+ if (files.length > 0) {
246
+ processFiles(files);
247
+ }
248
+ }
249
+
250
+ function processFiles(files) {
251
+ if (isProcessing) {
252
+ alert('Already processing a file. Please wait.');
253
+ return;
254
+ }
255
+
256
+ Array.from(files).forEach(file => uploadFile(file));
257
+ }
258
+
259
+ function uploadFile(file) {
260
+ const formData = new FormData();
261
+ formData.append('file', file);
262
+
263
+ isProcessing = true;
264
+ document.getElementById('processingIndicator').style.display = 'block';
265
+ updateProcessingStatus('Processing...');
266
+
267
+ fetch('/api/upload', {
268
+ method: 'POST',
269
+ body: formData
270
+ })
271
+ .then(response => response.json())
272
+ .then(data => {
273
+ if (data.success) {
274
+ console.log('Upload successful:', data);
275
+ pollProcessingStatus(data.file_id, data.output_filename);
276
+ } else {
277
+ throw new Error(data.error || 'Upload failed');
278
+ }
279
+ })
280
+ .catch(error => {
281
+ console.error('Upload error:', error);
282
+ alert('Upload failed: ' + error.message);
283
+ isProcessing = false;
284
+ document.getElementById('processingIndicator').style.display = 'none';
285
+ updateProcessingStatus('Error');
286
+ });
287
+ }
288
+
289
+ function pollProcessingStatus(fileId, outputFilename) {
290
+ const pollInterval = setInterval(() => {
291
+ fetch('/api/processing-status')
292
+ .then(response => response.json())
293
+ .then(data => {
294
+ if (!data.processing) {
295
+ clearInterval(pollInterval);
296
+ isProcessing = false;
297
+ document.getElementById('processingIndicator').style.display = 'none';
298
+ updateProcessingStatus('Complete');
299
+
300
+ // Check if file was processed successfully
301
+ if (data.processed_files && data.processed_files.length > 0) {
302
+ updateResults(data.processed_files);
303
+ alert('File processed successfully! Check the results below.');
304
+ }
305
+ }
306
+ })
307
+ .catch(error => {
308
+ console.error('❌ Status poll error:', error);
309
+ clearInterval(pollInterval);
310
+ isProcessing = false;
311
+ document.getElementById('processingIndicator').style.display = 'none';
312
+ });
313
+ }, 2000);
314
+ }
315
+
316
+ function updateResults(processedFiles) {
317
+ const resultsSection = document.getElementById('resultsSection');
318
+ const resultsGrid = document.getElementById('resultsGrid');
319
+
320
+ resultsGrid.innerHTML = '';
321
+
322
+ processedFiles.forEach(file => {
323
+ const resultCard = document.createElement('div');
324
+ resultCard.className = 'result-card';
325
+ resultCard.innerHTML = `
326
+ <div style="font-weight: 600; margin-bottom: 8px;">${file.input_file}</div>
327
+ <div style="opacity: 0.7; margin-bottom: 8px; font-size: 0.9rem;">
328
+ ${file.original_size} → ${file.upscaled_size}
329
+ </div>
330
+ <div style="opacity: 0.6; font-size: 0.8rem; margin-bottom: 12px;">
331
+ ${file.timestamp}
332
+ </div>
333
+ <div style="text-align: center;">
334
+ <button class="btn" onclick="previewFile('${file.output_file}')">Preview</button>
335
+ <button class="btn" onclick="downloadFile('${file.output_file}')">Download</button>
336
+ </div>
337
+ `;
338
+ resultsGrid.appendChild(resultCard);
339
+ });
340
+
341
+ resultsSection.style.display = 'block';
342
+ }
343
+
344
+ function previewFile(filename) {
345
+ window.open(`/api/preview/${filename}`, '_blank');
346
+ }
347
+
348
+ function downloadFile(filename) {
349
+ window.location.href = `/api/download/${filename}`;
350
+ }
351
+
352
+ function refreshSystemInfo() {
353
+ fetch('/api/system')
354
+ .then(response => response.json())
355
+ .then(data => {
356
+ if (data.success) {
357
+ displaySystemInfo(data.data);
358
+ updateStatusBadges(data.data);
359
+ }
360
+ })
361
+ .catch(error => console.error('❌ System info error:', error));
362
+ }
363
+
364
+ function displaySystemInfo(info) {
365
+ const container = document.getElementById('systemInfo');
366
+ container.innerHTML = `
367
+ <div style="margin: 6px 0;"><strong>GPU:</strong> ${info.gpu_name || 'Not available'}</div>
368
+ <div style="margin: 6px 0;"><strong>Memory:</strong> ${info.gpu_memory || 'N/A'}</div>
369
+ <div style="margin: 6px 0;"><strong>CUDA:</strong> ${info.cuda_version || 'Not available'}</div>
370
+ <div style="margin: 6px 0;"><strong>PyTorch:</strong> ${info.pytorch_version || 'N/A'}</div>
371
+ <div style="margin: 6px 0;"><strong>Storage:</strong> ${info.storage_outputs || 'N/A'} used</div>
372
+ `;
373
+ }
374
+
375
+ function updateStatusBadges(info) {
376
+ document.getElementById('gpu-status').textContent =
377
+ info.gpu_available ? `GPU: ${info.gpu_name}` : 'CPU Only';
378
+ document.getElementById('memory-status').textContent =
379
+ `Memory: ${info.gpu_memory_used || '0MB'} / ${info.gpu_memory || 'N/A'}`;
380
+ }
381
+
382
+ function updateProcessingStatus(status) {
383
+ document.getElementById('processing-status').textContent = status;
384
+ }
385
+
386
+ function startStatusUpdates() {
387
+ setInterval(() => {
388
+ if (!isProcessing) {
389
+ fetch('/api/processing-status')
390
+ .then(response => response.json())
391
+ .then(data => {
392
+ if (data.processed_files && data.processed_files.length > 0) {
393
+ updateResults(data.processed_files);
394
+ }
395
+ })
396
+ .catch(error => console.error('❌ Status update error:', error));
397
+ }
398
+ }, 5000);
399
+ }
400
+
401
+ function optimizeGPU() {
402
+ fetch('/api/optimize-gpu', { method: 'POST' })
403
+ .then(response => response.json())
404
+ .then(data => {
405
+ if (data.success) {
406
+ alert('GPU optimized successfully!');
407
+ refreshSystemInfo();
408
+ } else {
409
+ alert('GPU optimization failed: ' + (data.message || data.error));
410
+ }
411
+ })
412
+ .catch(error => {
413
+ console.error('GPU optimization error:', error);
414
+ alert('Error optimizing GPU');
415
+ });
416
+ }
417
+
418
+ function clearCache() {
419
+ fetch('/api/clear-cache', { method: 'POST' })
420
+ .then(response => response.json())
421
+ .then(data => {
422
+ if (data.success) {
423
+ alert('Cache cleared successfully!');
424
+ document.getElementById('resultsSection').style.display = 'none';
425
+ refreshSystemInfo();
426
+ } else {
427
+ alert('Failed to clear cache: ' + (data.message || data.error));
428
+ }
429
+ })
430
+ .catch(error => {
431
+ console.error('Clear cache error:', error);
432
+ alert('Error clearing cache');
433
+ });
434
+ }
435
+
436
+ function toggleLogs() {
437
+ const logsSection = document.getElementById('logsSection');
438
+ logsVisible = !logsVisible;
439
+
440
+ if (logsVisible) {
441
+ logsSection.style.display = 'block';
442
+ refreshLogs();
443
+ } else {
444
+ logsSection.style.display = 'none';
445
+ }
446
+ }
447
+
448
+ function refreshLogs() {
449
+ fetch('/api/logs')
450
+ .then(response => response.json())
451
+ .then(data => {
452
+ if (data.success) {
453
+ const container = document.getElementById('logsContainer');
454
+ container.innerHTML = '';
455
+
456
+ data.logs.forEach(log => {
457
+ const logEntry = document.createElement('div');
458
+ logEntry.className = 'log-entry';
459
+ logEntry.textContent = log;
460
+ container.appendChild(logEntry);
461
+ });
462
+
463
+ container.scrollTop = container.scrollHeight;
464
+ }
465
+ })
466
+ .catch(error => console.error('❌ Logs error:', error));
467
+ }
468
+
469
+ function clearLogs() {
470
+ fetch('/api/clear-logs', { method: 'POST' })
471
+ .then(response => response.json())
472
+ .then(data => {
473
+ if (data.success) {
474
+ document.getElementById('logsContainer').innerHTML = '';
475
+ alert('Logs cleared successfully!');
476
+ } else {
477
+ alert('Failed to clear logs');
478
+ }
479
+ })
480
+ .catch(error => {
481
+ console.error('Clear logs error:', error);
482
+ alert('Error clearing logs');
483
+ });
484
+ }
485
+ </script>
486
+ </body>
487
+ </html>