Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>🚀 4K Upscaler</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: 'Inter', 'SF Pro Display', system-ui, -apple-system, sans-serif; | |
| background: #0a0a0a; | |
| color: #ffffff; min-height: 100vh; overflow-x: hidden; | |
| } | |
| .container { max-width: 1400px; margin: 0 auto; padding: 20px; } | |
| .header { text-align: center; margin-bottom: 40px; } | |
| .header h1 { | |
| font-size: 2.5rem; font-weight: 700; | |
| color: #ffffff; | |
| margin-bottom: 10px; | |
| letter-spacing: -0.02em; | |
| } | |
| .subtitle { | |
| font-size: 1.1rem; opacity: 0.7; margin-bottom: 20px; | |
| color: #cccccc; | |
| font-weight: 400; | |
| } | |
| .status-bar { | |
| display: flex; justify-content: center; gap: 15px; margin-bottom: 30px; | |
| flex-wrap: wrap; | |
| } | |
| .status-badge { | |
| padding: 8px 16px; background: #1a1a1a; | |
| border-radius: 6px; | |
| border: 1px solid #333333; | |
| font-size: 0.85rem; font-weight: 500; | |
| color: #ffffff; | |
| } | |
| .cards-grid { | |
| display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); | |
| gap: 20px; margin-bottom: 40px; | |
| } | |
| .card { | |
| background: #111111; | |
| border-radius: 8px; padding: 24px; | |
| border: 1px solid #333333; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); | |
| transition: all 0.2s ease; | |
| } | |
| .card:hover { | |
| border-color: #555555; | |
| box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6); | |
| } | |
| .card h3 { | |
| font-size: 1.2rem; margin-bottom: 16px; display: flex; | |
| align-items: center; gap: 8px; color: #ffffff; | |
| font-weight: 600; | |
| } | |
| .card-icon { font-size: 1.2rem; } | |
| .btn { | |
| background: #2d2d2d; | |
| border: 1px solid #404040; padding: 10px 16px; color: #ffffff; | |
| font-size: 0.9rem; font-weight: 500; cursor: pointer; | |
| transition: all 0.2s ease; margin: 4px; | |
| border-radius: 6px; min-width: 100px; | |
| } | |
| .btn:hover { | |
| background: #404040; | |
| border-color: #555555; | |
| } | |
| .btn:disabled { | |
| background: #1a1a1a; cursor: not-allowed; | |
| color: #666666; border-color: #2a2a2a; | |
| } | |
| .file-upload { | |
| border: 2px dashed #404040; | |
| border-radius: 8px; padding: 32px; text-align: center; | |
| background: #0f0f0f; cursor: pointer; | |
| transition: all 0.2s ease; margin-bottom: 16px; | |
| } | |
| .file-upload:hover { | |
| border-color: #666666; background: #151515; | |
| } | |
| .file-upload.dragover { | |
| border-color: #888888; background: #1a1a1a; | |
| } | |
| .file-input { display: none; } | |
| .progress-container { | |
| background: #1a1a1a; border-radius: 6px; | |
| padding: 16px; margin-top: 16px; display: none; | |
| border: 1px solid #333333; | |
| } | |
| .progress-bar { | |
| width: 100%; height: 6px; background: #2a2a2a; | |
| border-radius: 3px; overflow: hidden; margin-bottom: 8px; | |
| } | |
| .progress-fill { | |
| height: 100%; background: #ffffff; | |
| width: 0%; transition: width 0.3s ease; | |
| } | |
| .logs-container { | |
| background: #050505; border-radius: 6px; | |
| padding: 16px; max-height: 300px; overflow-y: auto; | |
| font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; font-size: 0.8rem; | |
| border: 1px solid #222222; | |
| } | |
| .log-entry { | |
| margin: 4px 0; padding: 4px 8px; border-radius: 3px; | |
| background: #0a0a0a; color: #cccccc; | |
| border-left: 2px solid #333333; | |
| } | |
| .results-grid { | |
| display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
| gap: 16px; margin-top: 16px; | |
| } | |
| .result-card { | |
| background: #111111; border-radius: 6px; | |
| padding: 16px; border: 1px solid #333333; | |
| } | |
| .result-preview { | |
| width: 100%; height: 180px; background: #0a0a0a; | |
| border-radius: 4px; margin-bottom: 12px; object-fit: cover; | |
| border: 1px solid #222222; | |
| } | |
| .processing-indicator { | |
| display: none; text-align: center; margin: 16px 0; | |
| } | |
| .spinner { | |
| border: 3px solid #333333; | |
| border-radius: 50%; border-top: 3px solid #ffffff; | |
| width: 32px; height: 32px; animation: spin 1s linear infinite; | |
| margin: 0 auto 8px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>4K Upscaler</h1> | |
| <div class="subtitle">Professional AI-powered image and video enhancement</div> | |
| </div> | |
| <div class="status-bar"> | |
| <div class="status-badge" id="gpu-status">Checking GPU...</div> | |
| <div class="status-badge" id="memory-status">Memory: --</div> | |
| <div class="status-badge" id="processing-status">Ready</div> | |
| </div> | |
| <div class="cards-grid"> | |
| <!-- Upload Card --> | |
| <div class="card"> | |
| <h3><span class="card-icon">⬆</span>Upload & Process</h3> | |
| <div class="file-upload" id="fileUpload"> | |
| <div style="font-size: 1.5rem; margin-bottom: 8px;">📁</div> | |
| <div style="font-size: 1rem; margin-bottom: 4px; font-weight: 500;">Drop files here or click to browse</div> | |
| <div style="opacity: 0.6; font-size: 0.85rem;">Supports: PNG, JPG, GIF, MP4, AVI, MOV, MKV</div> | |
| <input type="file" id="fileInput" class="file-input" accept=".png,.jpg,.jpeg,.gif,.mp4,.avi,.mov,.mkv" multiple> | |
| </div> | |
| <div class="processing-indicator" id="processingIndicator"> | |
| <div class="spinner"></div> | |
| <div>Processing your file...</div> | |
| </div> | |
| <div style="text-align: center;"> | |
| <button class="btn" onclick="clearCache()">Clear Cache</button> | |
| <button class="btn" onclick="optimizeGPU()">Optimize GPU</button> | |
| </div> | |
| </div> | |
| <!-- System Info Card --> | |
| <div class="card"> | |
| <h3><span class="card-icon">⚙</span>System Status</h3> | |
| <div id="systemInfo"> | |
| <div style="margin: 10px 0;">Loading system information...</div> | |
| </div> | |
| <div style="text-align: center; margin-top: 16px;"> | |
| <button class="btn" onclick="refreshSystemInfo()">Refresh</button> | |
| <button class="btn" onclick="toggleLogs()">View Logs</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Results Section --> | |
| <div class="card" id="resultsSection" style="display: none;"> | |
| <h3><span class="card-icon">✓</span>Processed Files</h3> | |
| <div id="resultsGrid" class="results-grid"></div> | |
| </div> | |
| <!-- Logs Section --> | |
| <div class="card" id="logsSection" style="display: none;"> | |
| <h3><span class="card-icon">□</span>Processing Logs</h3> | |
| <div class="logs-container" id="logsContainer"></div> | |
| <div style="text-align: center; margin-top: 16px;"> | |
| <button class="btn" onclick="clearLogs()">Clear Logs</button> | |
| <button class="btn" onclick="refreshLogs()">Refresh</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let isProcessing = false; | |
| let logsVisible = false; | |
| // Initialize app | |
| document.addEventListener('DOMContentLoaded', function() { | |
| setupFileUpload(); | |
| refreshSystemInfo(); | |
| startStatusUpdates(); | |
| }); | |
| function setupFileUpload() { | |
| const fileUpload = document.getElementById('fileUpload'); | |
| const fileInput = document.getElementById('fileInput'); | |
| fileUpload.addEventListener('click', () => fileInput.click()); | |
| fileUpload.addEventListener('dragover', handleDragOver); | |
| fileUpload.addEventListener('dragleave', handleDragLeave); | |
| fileUpload.addEventListener('drop', handleDrop); | |
| fileInput.addEventListener('change', handleFileSelect); | |
| } | |
| function handleDragOver(e) { | |
| e.preventDefault(); | |
| e.currentTarget.classList.add('dragover'); | |
| } | |
| function handleDragLeave(e) { | |
| e.preventDefault(); | |
| e.currentTarget.classList.remove('dragover'); | |
| } | |
| function handleDrop(e) { | |
| e.preventDefault(); | |
| e.currentTarget.classList.remove('dragover'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| processFiles(files); | |
| } | |
| } | |
| function handleFileSelect(e) { | |
| const files = e.target.files; | |
| if (files.length > 0) { | |
| processFiles(files); | |
| } | |
| } | |
| function processFiles(files) { | |
| if (isProcessing) { | |
| alert('Already processing a file. Please wait.'); | |
| return; | |
| } | |
| Array.from(files).forEach(file => uploadFile(file)); | |
| } | |
| function uploadFile(file) { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| isProcessing = true; | |
| document.getElementById('processingIndicator').style.display = 'block'; | |
| updateProcessingStatus('Processing...'); | |
| fetch('/api/upload', { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| console.log('Upload successful:', data); | |
| pollProcessingStatus(data.file_id, data.output_filename); | |
| } else { | |
| throw new Error(data.error || 'Upload failed'); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Upload error:', error); | |
| alert('Upload failed: ' + error.message); | |
| isProcessing = false; | |
| document.getElementById('processingIndicator').style.display = 'none'; | |
| updateProcessingStatus('Error'); | |
| }); | |
| } | |
| function pollProcessingStatus(fileId, outputFilename) { | |
| const pollInterval = setInterval(() => { | |
| fetch('/api/processing-status') | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (!data.processing) { | |
| clearInterval(pollInterval); | |
| isProcessing = false; | |
| document.getElementById('processingIndicator').style.display = 'none'; | |
| updateProcessingStatus('Complete'); | |
| // Check if file was processed successfully | |
| if (data.processed_files && data.processed_files.length > 0) { | |
| updateResults(data.processed_files); | |
| alert('File processed successfully! Check the results below.'); | |
| } | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('❌ Status poll error:', error); | |
| clearInterval(pollInterval); | |
| isProcessing = false; | |
| document.getElementById('processingIndicator').style.display = 'none'; | |
| }); | |
| }, 2000); | |
| } | |
| function updateResults(processedFiles) { | |
| const resultsSection = document.getElementById('resultsSection'); | |
| const resultsGrid = document.getElementById('resultsGrid'); | |
| resultsGrid.innerHTML = ''; | |
| processedFiles.forEach(file => { | |
| const resultCard = document.createElement('div'); | |
| resultCard.className = 'result-card'; | |
| resultCard.innerHTML = ` | |
| <div style="font-weight: 600; margin-bottom: 8px;">${file.input_file}</div> | |
| <div style="opacity: 0.7; margin-bottom: 8px; font-size: 0.9rem;"> | |
| ${file.original_size} → ${file.upscaled_size} | |
| </div> | |
| <div style="opacity: 0.6; font-size: 0.8rem; margin-bottom: 12px;"> | |
| ${file.timestamp} | |
| </div> | |
| <div style="text-align: center;"> | |
| <button class="btn" onclick="previewFile('${file.output_file}')">Preview</button> | |
| <button class="btn" onclick="downloadFile('${file.output_file}')">Download</button> | |
| </div> | |
| `; | |
| resultsGrid.appendChild(resultCard); | |
| }); | |
| resultsSection.style.display = 'block'; | |
| } | |
| function previewFile(filename) { | |
| window.open(`/api/preview/${filename}`, '_blank'); | |
| } | |
| function downloadFile(filename) { | |
| window.location.href = `/api/download/${filename}`; | |
| } | |
| function refreshSystemInfo() { | |
| fetch('/api/system') | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| displaySystemInfo(data.data); | |
| updateStatusBadges(data.data); | |
| } | |
| }) | |
| .catch(error => console.error('❌ System info error:', error)); | |
| } | |
| function displaySystemInfo(info) { | |
| const container = document.getElementById('systemInfo'); | |
| container.innerHTML = ` | |
| <div style="margin: 6px 0;"><strong>GPU:</strong> ${info.gpu_name || 'Not available'}</div> | |
| <div style="margin: 6px 0;"><strong>Memory:</strong> ${info.gpu_memory || 'N/A'}</div> | |
| <div style="margin: 6px 0;"><strong>CUDA:</strong> ${info.cuda_version || 'Not available'}</div> | |
| <div style="margin: 6px 0;"><strong>PyTorch:</strong> ${info.pytorch_version || 'N/A'}</div> | |
| <div style="margin: 6px 0;"><strong>Storage:</strong> ${info.storage_outputs || 'N/A'} used</div> | |
| `; | |
| } | |
| function updateStatusBadges(info) { | |
| document.getElementById('gpu-status').textContent = | |
| info.gpu_available ? `GPU: ${info.gpu_name}` : 'CPU Only'; | |
| document.getElementById('memory-status').textContent = | |
| `Memory: ${info.gpu_memory_used || '0MB'} / ${info.gpu_memory || 'N/A'}`; | |
| } | |
| function updateProcessingStatus(status) { | |
| document.getElementById('processing-status').textContent = status; | |
| } | |
| function startStatusUpdates() { | |
| setInterval(() => { | |
| if (!isProcessing) { | |
| fetch('/api/processing-status') | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.processed_files && data.processed_files.length > 0) { | |
| updateResults(data.processed_files); | |
| } | |
| }) | |
| .catch(error => console.error('❌ Status update error:', error)); | |
| } | |
| }, 5000); | |
| } | |
| function optimizeGPU() { | |
| fetch('/api/optimize-gpu', { method: 'POST' }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| alert('GPU optimized successfully!'); | |
| refreshSystemInfo(); | |
| } else { | |
| alert('GPU optimization failed: ' + (data.message || data.error)); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('GPU optimization error:', error); | |
| alert('Error optimizing GPU'); | |
| }); | |
| } | |
| function clearCache() { | |
| fetch('/api/clear-cache', { method: 'POST' }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| alert('Cache cleared successfully!'); | |
| document.getElementById('resultsSection').style.display = 'none'; | |
| refreshSystemInfo(); | |
| } else { | |
| alert('Failed to clear cache: ' + (data.message || data.error)); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Clear cache error:', error); | |
| alert('Error clearing cache'); | |
| }); | |
| } | |
| function toggleLogs() { | |
| const logsSection = document.getElementById('logsSection'); | |
| logsVisible = !logsVisible; | |
| if (logsVisible) { | |
| logsSection.style.display = 'block'; | |
| refreshLogs(); | |
| } else { | |
| logsSection.style.display = 'none'; | |
| } | |
| } | |
| function refreshLogs() { | |
| fetch('/api/logs') | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| const container = document.getElementById('logsContainer'); | |
| container.innerHTML = ''; | |
| data.logs.forEach(log => { | |
| const logEntry = document.createElement('div'); | |
| logEntry.className = 'log-entry'; | |
| logEntry.textContent = log; | |
| container.appendChild(logEntry); | |
| }); | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| }) | |
| .catch(error => console.error('❌ Logs error:', error)); | |
| } | |
| function clearLogs() { | |
| fetch('/api/clear-logs', { method: 'POST' }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| document.getElementById('logsContainer').innerHTML = ''; | |
| alert('Logs cleared successfully!'); | |
| } else { | |
| alert('Failed to clear logs'); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Clear logs error:', error); | |
| alert('Error clearing logs'); | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> |