Spaces:
Sleeping
Sleeping
nykadamec
commited on
Commit
Β·
18ea579
1
Parent(s):
3f578a8
test_api_1
Browse files- Dockerfile +31 -0
- package.json +37 -0
- public/index.html +279 -0
- server.js +247 -0
Dockerfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use Node.js 18 LTS as base image
|
| 2 |
+
FROM node:18-slim
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Install system dependencies for TensorFlow.js
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
| 9 |
+
python3 \
|
| 10 |
+
python3-pip \
|
| 11 |
+
build-essential \
|
| 12 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
# Copy package files
|
| 15 |
+
COPY package*.json ./
|
| 16 |
+
|
| 17 |
+
# Install Node.js dependencies
|
| 18 |
+
RUN npm ci --only=production
|
| 19 |
+
|
| 20 |
+
# Copy application code
|
| 21 |
+
COPY . .
|
| 22 |
+
|
| 23 |
+
# Expose port for Hugging Face Spaces
|
| 24 |
+
EXPOSE 7860
|
| 25 |
+
|
| 26 |
+
# Set environment variables
|
| 27 |
+
ENV NODE_ENV=production
|
| 28 |
+
ENV PORT=7860
|
| 29 |
+
|
| 30 |
+
# Start the server
|
| 31 |
+
CMD ["npm", "start"]
|
package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "upscaler-api",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "AI Image Upscaler API for Hugging Face Spaces",
|
| 5 |
+
"main": "server.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"start": "node server.js",
|
| 8 |
+
"dev": "node server.js"
|
| 9 |
+
},
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"express": "^4.18.2",
|
| 12 |
+
"multer": "^1.4.5-lts.1",
|
| 13 |
+
"cors": "^2.8.5",
|
| 14 |
+
"@tensorflow/tfjs": "^4.11.0",
|
| 15 |
+
"@tensorflow/tfjs-backend-wasm": "~4.11.0",
|
| 16 |
+
"@tensorflow/tfjs-backend-webgl": "~4.11.0",
|
| 17 |
+
"@tensorflow/tfjs-backend-cpu": "~4.11.0",
|
| 18 |
+
"@upscalerjs/esrgan-slim": "1.0.0-beta.12",
|
| 19 |
+
"@upscalerjs/esrgan-medium": "1.0.0-beta.13",
|
| 20 |
+
"@upscalerjs/esrgan-thick": "1.0.0-beta.16",
|
| 21 |
+
"upscaler": "1.0.0-beta.19",
|
| 22 |
+
"canvas": "^2.11.2",
|
| 23 |
+
"sharp": "^0.32.6"
|
| 24 |
+
},
|
| 25 |
+
"engines": {
|
| 26 |
+
"node": ">=18.0.0"
|
| 27 |
+
},
|
| 28 |
+
"keywords": [
|
| 29 |
+
"ai",
|
| 30 |
+
"upscaler",
|
| 31 |
+
"tensorflow",
|
| 32 |
+
"image-processing",
|
| 33 |
+
"api"
|
| 34 |
+
],
|
| 35 |
+
"author": "Upscale2 Team",
|
| 36 |
+
"license": "MIT"
|
| 37 |
+
}
|
public/index.html
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>AI Image Upscaler API Test</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 10 |
+
max-width: 800px;
|
| 11 |
+
margin: 0 auto;
|
| 12 |
+
padding: 20px;
|
| 13 |
+
background: #f5f5f5;
|
| 14 |
+
}
|
| 15 |
+
.container {
|
| 16 |
+
background: white;
|
| 17 |
+
padding: 30px;
|
| 18 |
+
border-radius: 12px;
|
| 19 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 20 |
+
}
|
| 21 |
+
h1 {
|
| 22 |
+
color: #333;
|
| 23 |
+
text-align: center;
|
| 24 |
+
margin-bottom: 30px;
|
| 25 |
+
}
|
| 26 |
+
.form-group {
|
| 27 |
+
margin-bottom: 20px;
|
| 28 |
+
}
|
| 29 |
+
label {
|
| 30 |
+
display: block;
|
| 31 |
+
margin-bottom: 5px;
|
| 32 |
+
font-weight: 600;
|
| 33 |
+
color: #555;
|
| 34 |
+
}
|
| 35 |
+
input, select {
|
| 36 |
+
width: 100%;
|
| 37 |
+
padding: 10px;
|
| 38 |
+
border: 2px solid #ddd;
|
| 39 |
+
border-radius: 6px;
|
| 40 |
+
font-size: 16px;
|
| 41 |
+
box-sizing: border-box;
|
| 42 |
+
}
|
| 43 |
+
input:focus, select:focus {
|
| 44 |
+
outline: none;
|
| 45 |
+
border-color: #007bff;
|
| 46 |
+
}
|
| 47 |
+
button {
|
| 48 |
+
background: #007bff;
|
| 49 |
+
color: white;
|
| 50 |
+
padding: 12px 24px;
|
| 51 |
+
border: none;
|
| 52 |
+
border-radius: 6px;
|
| 53 |
+
font-size: 16px;
|
| 54 |
+
cursor: pointer;
|
| 55 |
+
width: 100%;
|
| 56 |
+
margin-top: 10px;
|
| 57 |
+
}
|
| 58 |
+
button:hover {
|
| 59 |
+
background: #0056b3;
|
| 60 |
+
}
|
| 61 |
+
button:disabled {
|
| 62 |
+
background: #ccc;
|
| 63 |
+
cursor: not-allowed;
|
| 64 |
+
}
|
| 65 |
+
.result {
|
| 66 |
+
margin-top: 30px;
|
| 67 |
+
padding: 20px;
|
| 68 |
+
background: #f8f9fa;
|
| 69 |
+
border-radius: 6px;
|
| 70 |
+
border-left: 4px solid #007bff;
|
| 71 |
+
}
|
| 72 |
+
.error {
|
| 73 |
+
background: #f8d7da;
|
| 74 |
+
border-left-color: #dc3545;
|
| 75 |
+
color: #721c24;
|
| 76 |
+
}
|
| 77 |
+
.success {
|
| 78 |
+
background: #d4edda;
|
| 79 |
+
border-left-color: #28a745;
|
| 80 |
+
color: #155724;
|
| 81 |
+
}
|
| 82 |
+
.image-container {
|
| 83 |
+
display: flex;
|
| 84 |
+
gap: 20px;
|
| 85 |
+
margin-top: 20px;
|
| 86 |
+
flex-wrap: wrap;
|
| 87 |
+
}
|
| 88 |
+
.image-box {
|
| 89 |
+
flex: 1;
|
| 90 |
+
min-width: 300px;
|
| 91 |
+
}
|
| 92 |
+
.image-box h3 {
|
| 93 |
+
margin-top: 0;
|
| 94 |
+
color: #555;
|
| 95 |
+
}
|
| 96 |
+
.image-box img {
|
| 97 |
+
max-width: 100%;
|
| 98 |
+
height: auto;
|
| 99 |
+
border: 2px solid #ddd;
|
| 100 |
+
border-radius: 6px;
|
| 101 |
+
}
|
| 102 |
+
.metadata {
|
| 103 |
+
background: #e9ecef;
|
| 104 |
+
padding: 15px;
|
| 105 |
+
border-radius: 6px;
|
| 106 |
+
margin-top: 15px;
|
| 107 |
+
font-family: monospace;
|
| 108 |
+
font-size: 14px;
|
| 109 |
+
}
|
| 110 |
+
.loading {
|
| 111 |
+
text-align: center;
|
| 112 |
+
padding: 20px;
|
| 113 |
+
}
|
| 114 |
+
.spinner {
|
| 115 |
+
border: 4px solid #f3f3f3;
|
| 116 |
+
border-top: 4px solid #007bff;
|
| 117 |
+
border-radius: 50%;
|
| 118 |
+
width: 40px;
|
| 119 |
+
height: 40px;
|
| 120 |
+
animation: spin 1s linear infinite;
|
| 121 |
+
margin: 0 auto 10px;
|
| 122 |
+
}
|
| 123 |
+
@keyframes spin {
|
| 124 |
+
0% { transform: rotate(0deg); }
|
| 125 |
+
100% { transform: rotate(360deg); }
|
| 126 |
+
}
|
| 127 |
+
</style>
|
| 128 |
+
</head>
|
| 129 |
+
<body>
|
| 130 |
+
<div class="container">
|
| 131 |
+
<h1>π AI Image Upscaler API Test</h1>
|
| 132 |
+
|
| 133 |
+
<form id="upscaleForm">
|
| 134 |
+
<div class="form-group">
|
| 135 |
+
<label for="imageFile">Select Image:</label>
|
| 136 |
+
<input type="file" id="imageFile" accept="image/*" required>
|
| 137 |
+
</div>
|
| 138 |
+
|
| 139 |
+
<div class="form-group">
|
| 140 |
+
<label for="scale">Scale Factor:</label>
|
| 141 |
+
<select id="scale">
|
| 142 |
+
<option value="2">2x</option>
|
| 143 |
+
<option value="3">3x</option>
|
| 144 |
+
<option value="4">4x</option>
|
| 145 |
+
</select>
|
| 146 |
+
</div>
|
| 147 |
+
|
| 148 |
+
<div class="form-group">
|
| 149 |
+
<label for="modelType">AI Model:</label>
|
| 150 |
+
<select id="modelType">
|
| 151 |
+
<option value="esrgan-slim">ESRGAN Slim (Fast)</option>
|
| 152 |
+
<option value="esrgan-medium">ESRGAN Medium (Balanced)</option>
|
| 153 |
+
<option value="esrgan-thick">ESRGAN Thick (Best Quality)</option>
|
| 154 |
+
</select>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<div class="form-group">
|
| 158 |
+
<label for="patchSize">Patch Size:</label>
|
| 159 |
+
<select id="patchSize">
|
| 160 |
+
<option value="64">64</option>
|
| 161 |
+
<option value="96">96</option>
|
| 162 |
+
<option value="128" selected>128</option>
|
| 163 |
+
<option value="160">160</option>
|
| 164 |
+
<option value="192">192</option>
|
| 165 |
+
</select>
|
| 166 |
+
</div>
|
| 167 |
+
|
| 168 |
+
<div class="form-group">
|
| 169 |
+
<label for="padding">Padding:</label>
|
| 170 |
+
<select id="padding">
|
| 171 |
+
<option value="0">0</option>
|
| 172 |
+
<option value="4">4</option>
|
| 173 |
+
<option value="8" selected>8</option>
|
| 174 |
+
<option value="12">12</option>
|
| 175 |
+
<option value="16">16</option>
|
| 176 |
+
</select>
|
| 177 |
+
</div>
|
| 178 |
+
|
| 179 |
+
<button type="submit" id="submitBtn">Upscale Image</button>
|
| 180 |
+
</form>
|
| 181 |
+
|
| 182 |
+
<div id="result"></div>
|
| 183 |
+
</div>
|
| 184 |
+
|
| 185 |
+
<script>
|
| 186 |
+
document.getElementById('upscaleForm').addEventListener('submit', async (e) => {
|
| 187 |
+
e.preventDefault();
|
| 188 |
+
|
| 189 |
+
const fileInput = document.getElementById('imageFile');
|
| 190 |
+
const scale = document.getElementById('scale').value;
|
| 191 |
+
const modelType = document.getElementById('modelType').value;
|
| 192 |
+
const patchSize = document.getElementById('patchSize').value;
|
| 193 |
+
const padding = document.getElementById('padding').value;
|
| 194 |
+
const submitBtn = document.getElementById('submitBtn');
|
| 195 |
+
const resultDiv = document.getElementById('result');
|
| 196 |
+
|
| 197 |
+
if (!fileInput.files[0]) {
|
| 198 |
+
alert('Please select an image file');
|
| 199 |
+
return;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// Show loading state
|
| 203 |
+
submitBtn.disabled = true;
|
| 204 |
+
submitBtn.textContent = 'Processing...';
|
| 205 |
+
resultDiv.innerHTML = `
|
| 206 |
+
<div class="loading">
|
| 207 |
+
<div class="spinner"></div>
|
| 208 |
+
<p>Upscaling image with ${scale}x scale using ${modelType} model...</p>
|
| 209 |
+
</div>
|
| 210 |
+
`;
|
| 211 |
+
|
| 212 |
+
try {
|
| 213 |
+
const formData = new FormData();
|
| 214 |
+
formData.append('image', fileInput.files[0]);
|
| 215 |
+
formData.append('scale', scale);
|
| 216 |
+
formData.append('modelType', modelType);
|
| 217 |
+
formData.append('patchSize', patchSize);
|
| 218 |
+
formData.append('padding', padding);
|
| 219 |
+
|
| 220 |
+
const startTime = Date.now();
|
| 221 |
+
const response = await fetch('/upscale', {
|
| 222 |
+
method: 'POST',
|
| 223 |
+
body: formData
|
| 224 |
+
});
|
| 225 |
+
|
| 226 |
+
const result = await response.json();
|
| 227 |
+
const totalTime = Date.now() - startTime;
|
| 228 |
+
|
| 229 |
+
if (result.success) {
|
| 230 |
+
// Create original image URL
|
| 231 |
+
const originalImageUrl = URL.createObjectURL(fileInput.files[0]);
|
| 232 |
+
|
| 233 |
+
resultDiv.innerHTML = `
|
| 234 |
+
<div class="result success">
|
| 235 |
+
<h3>β
Upscaling Successful!</h3>
|
| 236 |
+
<div class="image-container">
|
| 237 |
+
<div class="image-box">
|
| 238 |
+
<h3>Original Image</h3>
|
| 239 |
+
<img src="${originalImageUrl}" alt="Original">
|
| 240 |
+
</div>
|
| 241 |
+
<div class="image-box">
|
| 242 |
+
<h3>Upscaled Image (${result.metadata.scale}x)</h3>
|
| 243 |
+
<img src="${result.result}" alt="Upscaled">
|
| 244 |
+
<a href="${result.result}" download="upscaled-${result.metadata.scale}x.png"
|
| 245 |
+
style="display: inline-block; margin-top: 10px; padding: 8px 16px; background: #007bff; color: white; text-decoration: none; border-radius: 4px;">
|
| 246 |
+
Download Upscaled Image
|
| 247 |
+
</a>
|
| 248 |
+
</div>
|
| 249 |
+
</div>
|
| 250 |
+
<div class="metadata">
|
| 251 |
+
<strong>Processing Details:</strong><br>
|
| 252 |
+
Scale: ${result.metadata.scale}x<br>
|
| 253 |
+
Model: ${result.metadata.modelType}<br>
|
| 254 |
+
Patch Size: ${result.metadata.patchSize}<br>
|
| 255 |
+
Padding: ${result.metadata.padding}<br>
|
| 256 |
+
Backend: ${result.metadata.backend}<br>
|
| 257 |
+
Server Processing Time: ${result.metadata.processingTime}ms<br>
|
| 258 |
+
Total Time: ${totalTime}ms
|
| 259 |
+
</div>
|
| 260 |
+
</div>
|
| 261 |
+
`;
|
| 262 |
+
} else {
|
| 263 |
+
throw new Error(result.error || 'Unknown error');
|
| 264 |
+
}
|
| 265 |
+
} catch (error) {
|
| 266 |
+
resultDiv.innerHTML = `
|
| 267 |
+
<div class="result error">
|
| 268 |
+
<h3>β Error</h3>
|
| 269 |
+
<p><strong>Failed to upscale image:</strong> ${error.message}</p>
|
| 270 |
+
</div>
|
| 271 |
+
`;
|
| 272 |
+
} finally {
|
| 273 |
+
submitBtn.disabled = false;
|
| 274 |
+
submitBtn.textContent = 'Upscale Image';
|
| 275 |
+
}
|
| 276 |
+
});
|
| 277 |
+
</script>
|
| 278 |
+
</body>
|
| 279 |
+
</html>
|
server.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require('express');
|
| 2 |
+
const multer = require('multer');
|
| 3 |
+
const cors = require('cors');
|
| 4 |
+
const sharp = require('sharp');
|
| 5 |
+
const tf = require('@tensorflow/tfjs');
|
| 6 |
+
require('@tensorflow/tfjs-backend-wasm');
|
| 7 |
+
require('@tensorflow/tfjs-backend-cpu');
|
| 8 |
+
const Upscaler = require('upscaler').default;
|
| 9 |
+
|
| 10 |
+
const app = express();
|
| 11 |
+
const PORT = process.env.PORT || 7860;
|
| 12 |
+
|
| 13 |
+
// Middleware
|
| 14 |
+
app.use(cors());
|
| 15 |
+
app.use(express.json({ limit: '50mb' }));
|
| 16 |
+
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
| 17 |
+
|
| 18 |
+
// Serve static files from public directory
|
| 19 |
+
app.use(express.static('public'));
|
| 20 |
+
|
| 21 |
+
// Configure multer for file uploads
|
| 22 |
+
const upload = multer({
|
| 23 |
+
storage: multer.memoryStorage(),
|
| 24 |
+
limits: {
|
| 25 |
+
fileSize: 10 * 1024 * 1024, // 10MB limit
|
| 26 |
+
},
|
| 27 |
+
fileFilter: (req, file, cb) => {
|
| 28 |
+
if (file.mimetype.startsWith('image/')) {
|
| 29 |
+
cb(null, true);
|
| 30 |
+
} else {
|
| 31 |
+
cb(new Error('Only image files are allowed'), false);
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
// Global upscaler instance
|
| 37 |
+
let upscalerInstance = null;
|
| 38 |
+
|
| 39 |
+
// Initialize TensorFlow.js backend
|
| 40 |
+
async function initializeTensorFlow() {
|
| 41 |
+
try {
|
| 42 |
+
console.log('Initializing TensorFlow.js...');
|
| 43 |
+
|
| 44 |
+
// Try to set WASM backend first, fallback to CPU
|
| 45 |
+
try {
|
| 46 |
+
await tf.setBackend('wasm');
|
| 47 |
+
await tf.ready();
|
| 48 |
+
console.log('TensorFlow.js initialized with WASM backend');
|
| 49 |
+
} catch (wasmError) {
|
| 50 |
+
console.warn('WASM backend failed, falling back to CPU:', wasmError.message);
|
| 51 |
+
await tf.setBackend('cpu');
|
| 52 |
+
await tf.ready();
|
| 53 |
+
console.log('TensorFlow.js initialized with CPU backend');
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
console.log('Current backend:', tf.getBackend());
|
| 57 |
+
return true;
|
| 58 |
+
} catch (error) {
|
| 59 |
+
console.error('Failed to initialize TensorFlow.js:', error);
|
| 60 |
+
return false;
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// Get model for scale and type
|
| 65 |
+
async function getModelForScaleAndType(scale, modelType) {
|
| 66 |
+
switch (modelType) {
|
| 67 |
+
case 'esrgan-slim':
|
| 68 |
+
const { x2: slimX2, x3: slimX3, x4: slimX4 } = require('@upscalerjs/esrgan-slim');
|
| 69 |
+
if (scale === 2) return slimX2;
|
| 70 |
+
if (scale === 3) return slimX3;
|
| 71 |
+
return slimX4;
|
| 72 |
+
|
| 73 |
+
case 'esrgan-medium':
|
| 74 |
+
const { x2: mediumX2, x3: mediumX3, x4: mediumX4 } = require('@upscalerjs/esrgan-medium');
|
| 75 |
+
if (scale === 2) return mediumX2;
|
| 76 |
+
if (scale === 3) return mediumX3;
|
| 77 |
+
return mediumX4;
|
| 78 |
+
|
| 79 |
+
case 'esrgan-thick':
|
| 80 |
+
const { x2: thickX2, x3: thickX3, x4: thickX4 } = require('@upscalerjs/esrgan-thick');
|
| 81 |
+
if (scale === 2) return thickX2;
|
| 82 |
+
if (scale === 3) return thickX3;
|
| 83 |
+
return thickX4;
|
| 84 |
+
|
| 85 |
+
default:
|
| 86 |
+
// Default to esrgan-slim
|
| 87 |
+
const { x2: defaultX2, x3: defaultX3, x4: defaultX4 } = require('@upscalerjs/esrgan-slim');
|
| 88 |
+
if (scale === 2) return defaultX2;
|
| 89 |
+
if (scale === 3) return defaultX3;
|
| 90 |
+
return defaultX4;
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Initialize upscaler with specific model
|
| 95 |
+
async function initializeUpscaler(scale = 2, modelType = 'esrgan-slim') {
|
| 96 |
+
try {
|
| 97 |
+
console.log(`Initializing upscaler with scale ${scale}x and model ${modelType}...`);
|
| 98 |
+
|
| 99 |
+
const model = await getModelForScaleAndType(scale, modelType);
|
| 100 |
+
upscalerInstance = new Upscaler({ model });
|
| 101 |
+
|
| 102 |
+
console.log('Upscaler initialized successfully');
|
| 103 |
+
return upscalerInstance;
|
| 104 |
+
} catch (error) {
|
| 105 |
+
console.error('Failed to initialize upscaler:', error);
|
| 106 |
+
throw error;
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// Convert buffer to base64 data URL
|
| 111 |
+
function bufferToDataURL(buffer, mimeType = 'image/png') {
|
| 112 |
+
const base64 = buffer.toString('base64');
|
| 113 |
+
return `data:${mimeType};base64,${base64}`;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// Health check endpoint
|
| 117 |
+
app.get('/', (req, res) => {
|
| 118 |
+
res.json({
|
| 119 |
+
status: 'ok',
|
| 120 |
+
message: 'AI Image Upscaler API',
|
| 121 |
+
backend: tf.getBackend(),
|
| 122 |
+
version: '1.0.0',
|
| 123 |
+
endpoints: {
|
| 124 |
+
upscale: 'POST /upscale',
|
| 125 |
+
health: 'GET /'
|
| 126 |
+
}
|
| 127 |
+
});
|
| 128 |
+
});
|
| 129 |
+
|
| 130 |
+
// Main upscale endpoint
|
| 131 |
+
app.post('/upscale', upload.single('image'), async (req, res) => {
|
| 132 |
+
try {
|
| 133 |
+
if (!req.file) {
|
| 134 |
+
return res.status(400).json({ error: 'No image file provided' });
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
const { scale = 2, modelType = 'esrgan-slim', patchSize = 128, padding = 8 } = req.body;
|
| 138 |
+
|
| 139 |
+
// Validate parameters
|
| 140 |
+
const validScales = [2, 3, 4];
|
| 141 |
+
const validModels = ['esrgan-slim', 'esrgan-medium', 'esrgan-thick'];
|
| 142 |
+
|
| 143 |
+
if (!validScales.includes(parseInt(scale))) {
|
| 144 |
+
return res.status(400).json({ error: 'Invalid scale. Must be 2, 3, or 4' });
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
if (!validModels.includes(modelType)) {
|
| 148 |
+
return res.status(400).json({ error: 'Invalid model type' });
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
console.log(`Processing image with scale ${scale}x, model ${modelType}`);
|
| 152 |
+
|
| 153 |
+
// Initialize upscaler if needed
|
| 154 |
+
if (!upscalerInstance) {
|
| 155 |
+
await initializeUpscaler(parseInt(scale), modelType);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// Convert image buffer to data URL
|
| 159 |
+
const inputDataURL = bufferToDataURL(req.file.buffer, req.file.mimetype);
|
| 160 |
+
|
| 161 |
+
// Perform upscaling
|
| 162 |
+
console.log('Starting upscaling...');
|
| 163 |
+
const startTime = Date.now();
|
| 164 |
+
|
| 165 |
+
const result = await upscalerInstance.upscale(inputDataURL, {
|
| 166 |
+
output: 'base64',
|
| 167 |
+
patchSize: parseInt(patchSize),
|
| 168 |
+
padding: parseInt(padding),
|
| 169 |
+
awaitNextFrame: true
|
| 170 |
+
});
|
| 171 |
+
|
| 172 |
+
const processingTime = Date.now() - startTime;
|
| 173 |
+
console.log(`Upscaling completed in ${processingTime}ms`);
|
| 174 |
+
|
| 175 |
+
// Return the upscaled image
|
| 176 |
+
res.json({
|
| 177 |
+
success: true,
|
| 178 |
+
result: result,
|
| 179 |
+
metadata: {
|
| 180 |
+
scale: parseInt(scale),
|
| 181 |
+
modelType: modelType,
|
| 182 |
+
patchSize: parseInt(patchSize),
|
| 183 |
+
padding: parseInt(padding),
|
| 184 |
+
processingTime: processingTime,
|
| 185 |
+
backend: tf.getBackend()
|
| 186 |
+
}
|
| 187 |
+
});
|
| 188 |
+
|
| 189 |
+
} catch (error) {
|
| 190 |
+
console.error('Upscaling error:', error);
|
| 191 |
+
res.status(500).json({
|
| 192 |
+
error: 'Failed to upscale image',
|
| 193 |
+
message: error.message,
|
| 194 |
+
backend: tf.getBackend()
|
| 195 |
+
});
|
| 196 |
+
}
|
| 197 |
+
});
|
| 198 |
+
|
| 199 |
+
// Error handling middleware
|
| 200 |
+
app.use((error, req, res, next) => {
|
| 201 |
+
if (error instanceof multer.MulterError) {
|
| 202 |
+
if (error.code === 'LIMIT_FILE_SIZE') {
|
| 203 |
+
return res.status(400).json({ error: 'File too large. Maximum size is 10MB' });
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
console.error('Unhandled error:', error);
|
| 208 |
+
res.status(500).json({ error: 'Internal server error' });
|
| 209 |
+
});
|
| 210 |
+
|
| 211 |
+
// Start server
|
| 212 |
+
async function startServer() {
|
| 213 |
+
try {
|
| 214 |
+
// Initialize TensorFlow.js
|
| 215 |
+
const tfInitialized = await initializeTensorFlow();
|
| 216 |
+
if (!tfInitialized) {
|
| 217 |
+
console.error('Failed to initialize TensorFlow.js. Exiting...');
|
| 218 |
+
process.exit(1);
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
// Start the server
|
| 222 |
+
app.listen(PORT, '0.0.0.0', () => {
|
| 223 |
+
console.log(`π Upscaler API server running on port ${PORT}`);
|
| 224 |
+
console.log(`π TensorFlow.js backend: ${tf.getBackend()}`);
|
| 225 |
+
console.log(`π Health check: http://localhost:${PORT}/`);
|
| 226 |
+
});
|
| 227 |
+
} catch (error) {
|
| 228 |
+
console.error('Failed to start server:', error);
|
| 229 |
+
process.exit(1);
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
// Handle graceful shutdown
|
| 234 |
+
process.on('SIGTERM', () => {
|
| 235 |
+
console.log('Received SIGTERM, shutting down gracefully...');
|
| 236 |
+
if (upscalerInstance) {
|
| 237 |
+
try {
|
| 238 |
+
upscalerInstance.dispose();
|
| 239 |
+
} catch (error) {
|
| 240 |
+
console.warn('Error disposing upscaler:', error);
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
process.exit(0);
|
| 244 |
+
});
|
| 245 |
+
|
| 246 |
+
// Start the server
|
| 247 |
+
startServer();
|