Spaces:
Build error
Build error
<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> |