image: Production!
Browse filesSigned-off-by: Hadad <[email protected]>
- assets/plugins/imageGenerator.js +377 -11
- assets/plugins/loadParameter.js +76 -0
- assets/plugins/webLoader.js +105 -128
- config.js +5 -1
- example.js +43 -0
- model.js +15 -0
- package.json +3 -2
- public/webViewer.ejs +27 -92
- resolution.js +35 -0
- server.js +28 -6
- src/controllers/imageController.js +57 -31
- src/services/imageGenerator.js +38 -20
- src/services/storageManager.js +13 -13
- src/services/websocketManager.js +108 -0
assets/plugins/imageGenerator.js
CHANGED
@@ -6,17 +6,383 @@
|
|
6 |
(function () {
|
7 |
'use strict';
|
8 |
|
9 |
-
var
|
10 |
-
var
|
11 |
-
|
|
|
|
|
|
|
12 |
if (document && document.body) {
|
13 |
-
|
14 |
-
requestId = document.body.dataset.requestId || '';
|
15 |
-
}
|
16 |
-
|
17 |
-
if (isGenerating) {
|
18 |
-
setTimeout(function () {
|
19 |
-
window.location.href = '/?rid=' + encodeURIComponent(requestId);
|
20 |
-
}, 10000);
|
21 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
})();
|
|
|
6 |
(function () {
|
7 |
'use strict';
|
8 |
|
9 |
+
var ws = null;
|
10 |
+
var sessionId = '';
|
11 |
+
var reconnectTimer = null;
|
12 |
+
var isConnecting = false;
|
13 |
+
var currentImages = [];
|
14 |
+
|
15 |
if (document && document.body) {
|
16 |
+
sessionId = document.body.dataset.sessionId || '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
}
|
18 |
+
|
19 |
+
var connectWebSocket = function() {
|
20 |
+
if (isConnecting || (ws && ws.readyState === 1)) return;
|
21 |
+
|
22 |
+
isConnecting = true;
|
23 |
+
var protocol = window.location.protocol === 'https:'
|
24 |
+
? 'wss:' : 'ws:';
|
25 |
+
var wsUrl = protocol + '//' +
|
26 |
+
window.location.host;
|
27 |
+
|
28 |
+
ws = new WebSocket(wsUrl);
|
29 |
+
|
30 |
+
ws.onopen = function() {
|
31 |
+
isConnecting = false;
|
32 |
+
if (reconnectTimer) {
|
33 |
+
clearTimeout(reconnectTimer);
|
34 |
+
reconnectTimer = null;
|
35 |
+
}
|
36 |
+
|
37 |
+
ws.send(JSON.stringify({
|
38 |
+
type: 'register',
|
39 |
+
sessionId: sessionId
|
40 |
+
}));
|
41 |
+
};
|
42 |
+
|
43 |
+
ws.onmessage = function(event) {
|
44 |
+
try {
|
45 |
+
var data = JSON.parse(event.data);
|
46 |
+
|
47 |
+
if (!data.sessionId ||
|
48 |
+
data.sessionId !== sessionId) {
|
49 |
+
return;
|
50 |
+
}
|
51 |
+
|
52 |
+
handleWebSocketMessage(data);
|
53 |
+
} catch (e) {}
|
54 |
+
};
|
55 |
+
|
56 |
+
ws.onclose = function() {
|
57 |
+
isConnecting = false;
|
58 |
+
reconnectWebSocket();
|
59 |
+
};
|
60 |
+
|
61 |
+
ws.onerror = function() {
|
62 |
+
isConnecting = false;
|
63 |
+
if (ws) ws.close();
|
64 |
+
};
|
65 |
+
};
|
66 |
+
|
67 |
+
var reconnectWebSocket = function() {
|
68 |
+
if (reconnectTimer) return;
|
69 |
+
|
70 |
+
reconnectTimer = setTimeout(function() {
|
71 |
+
reconnectTimer = null;
|
72 |
+
connectWebSocket();
|
73 |
+
}, 1000);
|
74 |
+
};
|
75 |
+
|
76 |
+
var handleWebSocketMessage = function(data) {
|
77 |
+
if (!data || !data.type) return;
|
78 |
+
|
79 |
+
switch(data.type) {
|
80 |
+
case 'progressUpdate':
|
81 |
+
updateProgressUI(data.progress);
|
82 |
+
break;
|
83 |
+
|
84 |
+
case 'generationStarted':
|
85 |
+
showGeneratingUI();
|
86 |
+
break;
|
87 |
+
|
88 |
+
case 'generationComplete':
|
89 |
+
handleGenerationComplete(data.images);
|
90 |
+
break;
|
91 |
+
|
92 |
+
case 'generationError':
|
93 |
+
handleGenerationError(data.error);
|
94 |
+
break;
|
95 |
+
|
96 |
+
case 'generationCancelled':
|
97 |
+
handleGenerationCancelled();
|
98 |
+
break;
|
99 |
+
|
100 |
+
case 'imageDeleted':
|
101 |
+
handleImageDeleted(data.images);
|
102 |
+
break;
|
103 |
+
}
|
104 |
+
};
|
105 |
+
|
106 |
+
var updateProgressUI = function(progress) {
|
107 |
+
var progressFill = document.querySelector('.progress-fill');
|
108 |
+
var progressText = document.querySelector('.progress-text');
|
109 |
+
|
110 |
+
if (progressFill) {
|
111 |
+
progressFill.style.width = progress + '%';
|
112 |
+
}
|
113 |
+
|
114 |
+
if (progressText) {
|
115 |
+
progressText.textContent =
|
116 |
+
Math.floor(progress) + '% Complete';
|
117 |
+
}
|
118 |
+
};
|
119 |
+
|
120 |
+
var showGeneratingUI = function() {
|
121 |
+
var outputSection = document.querySelector(
|
122 |
+
'.image-output-section'
|
123 |
+
);
|
124 |
+
var form = document.getElementById('generateForm');
|
125 |
+
var inputs = form ?
|
126 |
+
form.querySelectorAll('input, select, textarea') : [];
|
127 |
+
|
128 |
+
Array.prototype.forEach.call(inputs, function(input) {
|
129 |
+
input.disabled = true;
|
130 |
+
});
|
131 |
+
|
132 |
+
if (outputSection) {
|
133 |
+
outputSection.classList.remove('has-images');
|
134 |
+
outputSection.innerHTML =
|
135 |
+
'<div class="loading-container">' +
|
136 |
+
'<div class="loading-spinner" ' +
|
137 |
+
'style="margin: 0 auto 20px;"></div>' +
|
138 |
+
'<p class="loading-text">Generating your image...</p>' +
|
139 |
+
'<div class="progress-bar">' +
|
140 |
+
'<div class="progress-fill" style="width: 0%;"></div>' +
|
141 |
+
'</div>' +
|
142 |
+
'<p class="progress-text">0% Complete</p>' +
|
143 |
+
'</div>';
|
144 |
+
}
|
145 |
+
|
146 |
+
updateButtonsForGeneration(true);
|
147 |
+
};
|
148 |
+
|
149 |
+
var hideGeneratingUI = function() {
|
150 |
+
var form = document.getElementById('generateForm');
|
151 |
+
var inputs = form ?
|
152 |
+
form.querySelectorAll('input, select, textarea') : [];
|
153 |
+
|
154 |
+
Array.prototype.forEach.call(inputs, function(input) {
|
155 |
+
input.disabled = false;
|
156 |
+
});
|
157 |
+
|
158 |
+
updateButtonsForGeneration(false);
|
159 |
+
window.validateInputs && window.validateInputs();
|
160 |
+
};
|
161 |
+
|
162 |
+
var resetToInitialState = function() {
|
163 |
+
hideGeneratingUI();
|
164 |
+
|
165 |
+
if (currentImages && currentImages.length > 0) {
|
166 |
+
displayImages(currentImages);
|
167 |
+
} else {
|
168 |
+
showPlaceholder();
|
169 |
+
}
|
170 |
+
};
|
171 |
+
|
172 |
+
var showPlaceholder = function() {
|
173 |
+
var outputSection = document.querySelector(
|
174 |
+
'.image-output-section'
|
175 |
+
);
|
176 |
+
|
177 |
+
if (!outputSection) return;
|
178 |
+
|
179 |
+
outputSection.classList.remove('has-images');
|
180 |
+
outputSection.innerHTML =
|
181 |
+
'<svg class="placeholder-icon" width="80" height="80" ' +
|
182 |
+
'viewBox="0 0 24 24" fill="none">' +
|
183 |
+
'<path d="M21 3H3C2 3 1 4 1 5V19C1 20 2 21 3 21H21C22 ' +
|
184 |
+
'21 23 20 23 19V5C23 4 22 3 21 3ZM21 19H3V5H21V19Z" ' +
|
185 |
+
'fill="currentColor"/>' +
|
186 |
+
'<path d="M4.5 16.5L9 12L11.5 14.5L16 10L19.5 13.5" ' +
|
187 |
+
'stroke="currentColor" stroke-width="1.5" ' +
|
188 |
+
'stroke-linecap="round"/>' +
|
189 |
+
'<circle cx="8" cy="8.5" r="1.5" fill="currentColor"/>' +
|
190 |
+
'</svg>' +
|
191 |
+
'<p class="placeholder-text">' +
|
192 |
+
'No images generated yet. Start creating amazing visuals!' +
|
193 |
+
'</p>';
|
194 |
+
};
|
195 |
+
|
196 |
+
var updateButtonsForGeneration = function(isGenerating) {
|
197 |
+
var buttonsContainer = document.querySelector(
|
198 |
+
'.flex.justify-center.gap-4'
|
199 |
+
);
|
200 |
+
|
201 |
+
if (!buttonsContainer) return;
|
202 |
+
|
203 |
+
if (isGenerating) {
|
204 |
+
buttonsContainer.innerHTML =
|
205 |
+
'<button type="button" onclick="cancelGeneration()" ' +
|
206 |
+
'class="btn btn-danger">' +
|
207 |
+
'<svg class="button-icon" viewBox="0 0 24 24" ' +
|
208 |
+
'fill="none">' +
|
209 |
+
'<rect x="4" y="4" width="16" height="16" rx="3" ' +
|
210 |
+
'fill="currentColor"/>' +
|
211 |
+
'</svg>' +
|
212 |
+
'Stop Generation' +
|
213 |
+
'</button>';
|
214 |
+
} else {
|
215 |
+
buttonsContainer.innerHTML =
|
216 |
+
'<button type="submit" id="submitBtn" disabled ' +
|
217 |
+
'class="btn btn-primary">' +
|
218 |
+
'<svg class="button-icon" viewBox="0 0 24 24" ' +
|
219 |
+
'fill="none">' +
|
220 |
+
'<path d="M3 20V4L22 12L3 20ZM5 17L16.85 12L5 7V10.5' +
|
221 |
+
'L11 12L5 13.5V17Z" fill="currentColor"/>' +
|
222 |
+
'</svg>' +
|
223 |
+
'Generate Image' +
|
224 |
+
'</button>';
|
225 |
+
}
|
226 |
+
};
|
227 |
+
|
228 |
+
var handleGenerationComplete = function(images) {
|
229 |
+
currentImages = images || [];
|
230 |
+
hideGeneratingUI();
|
231 |
+
displayImages(currentImages);
|
232 |
+
};
|
233 |
+
|
234 |
+
var handleGenerationError = function(error) {
|
235 |
+
resetToInitialState();
|
236 |
+
showErrorModal(error);
|
237 |
+
};
|
238 |
+
|
239 |
+
var handleGenerationCancelled = function() {
|
240 |
+
resetToInitialState();
|
241 |
+
};
|
242 |
+
|
243 |
+
var handleImageDeleted = function(images) {
|
244 |
+
currentImages = images || [];
|
245 |
+
displayImages(currentImages);
|
246 |
+
};
|
247 |
+
|
248 |
+
var displayImages = function(images) {
|
249 |
+
var outputSection = document.querySelector(
|
250 |
+
'.image-output-section'
|
251 |
+
);
|
252 |
+
|
253 |
+
if (!outputSection) return;
|
254 |
+
|
255 |
+
if (!images || images.length === 0) {
|
256 |
+
showPlaceholder();
|
257 |
+
} else {
|
258 |
+
outputSection.classList.add('has-images');
|
259 |
+
var html = '<div class="image-grid">';
|
260 |
+
|
261 |
+
images.forEach(function(image, index) {
|
262 |
+
html +=
|
263 |
+
'<div class="image-card">' +
|
264 |
+
'<img src="data:image/png;base64,' + image.base64 +
|
265 |
+
'" alt="' + image.prompt + '">' +
|
266 |
+
'<div class="image-actions">' +
|
267 |
+
'<a href="data:image/png;base64,' + image.base64 +
|
268 |
+
'" download="generated-' + image.id + '.png" ' +
|
269 |
+
'class="action-btn">' +
|
270 |
+
'<svg class="action-icon" viewBox="0 0 24 24" ' +
|
271 |
+
'fill="none">' +
|
272 |
+
'<path d="M12 16L7 11L8.4 9.55L11 12.15V4H13V12.15' +
|
273 |
+
'L15.6 9.55L17 11L12 16Z" fill="currentColor"/>' +
|
274 |
+
'<path d="M4 20C3.45 20 2.98 19.8 2.59 19.41C2.2 ' +
|
275 |
+
'19.02 2 18.55 2 18V15H4V18H20V15H22V18C22 18.55 ' +
|
276 |
+
'21.8 19.02 21.41 19.41C21.02 19.8 20.55 20 20 20H4Z" ' +
|
277 |
+
'fill="currentColor"/>' +
|
278 |
+
'</svg>' +
|
279 |
+
'</a>' +
|
280 |
+
'<button type="button" onclick="deleteImage(' +
|
281 |
+
index + ')" class="action-btn">' +
|
282 |
+
'<svg class="action-icon" viewBox="0 0 24 24" ' +
|
283 |
+
'fill="none">' +
|
284 |
+
'<path d="M18.3 5.71C17.91 5.32 17.28 5.32 16.89 ' +
|
285 |
+
'5.71L12 10.59L7.11 5.7C6.72 5.31 6.09 5.31 5.7 ' +
|
286 |
+
'5.7C5.31 6.09 5.31 6.72 5.7 7.11L10.59 12L5.7 ' +
|
287 |
+
'16.89C5.31 17.28 5.31 17.91 5.7 18.3C6.09 18.69 ' +
|
288 |
+
'6.72 18.69 7.11 18.3L12 13.41L16.89 18.3C17.28 ' +
|
289 |
+
'18.69 17.91 18.69 18.3 18.3C18.69 17.91 18.69 ' +
|
290 |
+
'17.28 18.3 16.89L13.41 12L18.3 7.11C18.68 6.73 ' +
|
291 |
+
'18.68 6.09 18.3 5.71Z" fill="currentColor"/>' +
|
292 |
+
'</svg>' +
|
293 |
+
'</button>' +
|
294 |
+
'</div>' +
|
295 |
+
'<div class="image-info">' +
|
296 |
+
'<p class="image-prompt">' + image.prompt + '</p>' +
|
297 |
+
'<p class="image-meta">' +
|
298 |
+
'<span class="image-model">' +
|
299 |
+
image.model.toUpperCase() + '</span> | ' +
|
300 |
+
image.size + '</p>' +
|
301 |
+
'</div>' +
|
302 |
+
'</div>';
|
303 |
+
});
|
304 |
+
|
305 |
+
html += '</div>';
|
306 |
+
outputSection.innerHTML = html;
|
307 |
+
}
|
308 |
+
};
|
309 |
+
|
310 |
+
var showErrorModal = function(error) {
|
311 |
+
var existingModal = document.getElementById('errorModal');
|
312 |
+
if (existingModal) existingModal.remove();
|
313 |
+
|
314 |
+
var modal = document.createElement('div');
|
315 |
+
modal.id = 'errorModal';
|
316 |
+
modal.className = 'modal-overlay';
|
317 |
+
modal.innerHTML =
|
318 |
+
'<div class="modal-content modal-error-content">' +
|
319 |
+
'<div class="modal-inner">' +
|
320 |
+
'<h3 class="modal-error-title">Error</h3>' +
|
321 |
+
'<p class="modal-error-text">' + error + '</p>' +
|
322 |
+
'<button onclick="closeErrorModal()" ' +
|
323 |
+
'class="btn btn-primary w-full">OK</button>' +
|
324 |
+
'</div>' +
|
325 |
+
'</div>';
|
326 |
+
|
327 |
+
document.body.appendChild(modal);
|
328 |
+
};
|
329 |
+
|
330 |
+
window.deleteImage = function(index) {
|
331 |
+
var xhr = new XMLHttpRequest();
|
332 |
+
xhr.open('POST', '/', true);
|
333 |
+
xhr.setRequestHeader(
|
334 |
+
'Content-Type',
|
335 |
+
'application/json'
|
336 |
+
);
|
337 |
+
|
338 |
+
xhr.onload = function() {
|
339 |
+
try {
|
340 |
+
var response = JSON.parse(xhr.responseText);
|
341 |
+
if (response.success) {
|
342 |
+
currentImages = response.images || [];
|
343 |
+
displayImages(currentImages);
|
344 |
+
}
|
345 |
+
} catch (e) {}
|
346 |
+
};
|
347 |
+
|
348 |
+
xhr.send(JSON.stringify({
|
349 |
+
action: 'delete',
|
350 |
+
sessionId: sessionId,
|
351 |
+
imageIndex: index
|
352 |
+
}));
|
353 |
+
};
|
354 |
+
|
355 |
+
var initializeImages = function() {
|
356 |
+
var outputSection = document.querySelector(
|
357 |
+
'.image-output-section'
|
358 |
+
);
|
359 |
+
|
360 |
+
if (outputSection && outputSection.classList.contains('has-images')) {
|
361 |
+
var imageCards = outputSection.querySelectorAll('.image-card');
|
362 |
+
if (imageCards && imageCards.length > 0) {
|
363 |
+
currentImages = [];
|
364 |
+
imageCards.forEach(function(card) {
|
365 |
+
var img = card.querySelector('img');
|
366 |
+
var prompt = card.querySelector('.image-prompt');
|
367 |
+
var model = card.querySelector('.image-model');
|
368 |
+
var meta = card.querySelector('.image-meta');
|
369 |
+
|
370 |
+
if (img && img.src && img.src.includes('base64,')) {
|
371 |
+
var base64 = img.src.split('base64,')[1];
|
372 |
+
var size = meta ? meta.textContent.split('|')[1] : '';
|
373 |
+
currentImages.push({
|
374 |
+
id: 'existing-' + Math.random().toString(36).substring(2, 15),
|
375 |
+
base64: base64,
|
376 |
+
prompt: prompt ? prompt.textContent : '',
|
377 |
+
model: model ? model.textContent.toLowerCase() : '',
|
378 |
+
size: size ? size.trim() : ''
|
379 |
+
});
|
380 |
+
}
|
381 |
+
});
|
382 |
+
}
|
383 |
+
}
|
384 |
+
};
|
385 |
+
|
386 |
+
connectWebSocket();
|
387 |
+
initializeImages();
|
388 |
})();
|
assets/plugins/loadParameter.js
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
//
|
2 |
+
// SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
// SPDX-License-Identifier: Apache-2.0
|
4 |
+
//
|
5 |
+
|
6 |
+
document.addEventListener('DOMContentLoaded', function() {
|
7 |
+
const modelSelect = document.getElementById('modelSelect');
|
8 |
+
const sizeSelect = document.getElementById('sizeSelect');
|
9 |
+
const examplesGrid = document.getElementById('examplesGrid');
|
10 |
+
|
11 |
+
if (typeof models !== 'undefined' && modelSelect) {
|
12 |
+
models.forEach(model => {
|
13 |
+
const option = document.createElement('option');
|
14 |
+
option.value = model.value;
|
15 |
+
option.textContent = model.label;
|
16 |
+
modelSelect.appendChild(option);
|
17 |
+
});
|
18 |
+
}
|
19 |
+
|
20 |
+
if (typeof sizes !== 'undefined' && sizeSelect) {
|
21 |
+
Object.keys(sizes).forEach(category => {
|
22 |
+
const optgroup = document.createElement('optgroup');
|
23 |
+
optgroup.label = category.charAt(0).toUpperCase() +
|
24 |
+
category.slice(1);
|
25 |
+
|
26 |
+
sizes[category].forEach(size => {
|
27 |
+
const option = document.createElement('option');
|
28 |
+
option.value = size.value;
|
29 |
+
option.textContent = size.label;
|
30 |
+
optgroup.appendChild(option);
|
31 |
+
});
|
32 |
+
|
33 |
+
sizeSelect.appendChild(optgroup);
|
34 |
+
});
|
35 |
+
}
|
36 |
+
|
37 |
+
if (typeof examples !== 'undefined' && examplesGrid) {
|
38 |
+
examples.forEach(example => {
|
39 |
+
const div = document.createElement('div');
|
40 |
+
div.className = 'example-card';
|
41 |
+
div.onclick = function() {
|
42 |
+
triggerExample(
|
43 |
+
example.prompt,
|
44 |
+
example.model,
|
45 |
+
example.size
|
46 |
+
);
|
47 |
+
};
|
48 |
+
|
49 |
+
const promptP = document.createElement('p');
|
50 |
+
promptP.className = 'example-text';
|
51 |
+
promptP.textContent = example.prompt;
|
52 |
+
|
53 |
+
const metaP = document.createElement('p');
|
54 |
+
metaP.className = 'example-meta';
|
55 |
+
|
56 |
+
const modelSpan = document.createElement('span');
|
57 |
+
modelSpan.className = 'example-model';
|
58 |
+
modelSpan.textContent = example.modelLabel;
|
59 |
+
|
60 |
+
metaP.appendChild(modelSpan);
|
61 |
+
metaP.appendChild(
|
62 |
+
document.createTextNode(' | ' + example.sizeLabel)
|
63 |
+
);
|
64 |
+
|
65 |
+
if (example.note) {
|
66 |
+
metaP.appendChild(
|
67 |
+
document.createTextNode(' | ' + example.note)
|
68 |
+
);
|
69 |
+
}
|
70 |
+
|
71 |
+
div.appendChild(promptP);
|
72 |
+
div.appendChild(metaP);
|
73 |
+
examplesGrid.appendChild(div);
|
74 |
+
});
|
75 |
+
}
|
76 |
+
});
|
assets/plugins/webLoader.js
CHANGED
@@ -23,14 +23,9 @@
|
|
23 |
});
|
24 |
}
|
25 |
|
26 |
-
if (encode ===
|
27 |
-
return clean;
|
28 |
-
}
|
29 |
-
|
30 |
-
if (typeof he !== 'undefined') {
|
31 |
clean = he.encode(clean, {
|
32 |
-
|
33 |
-
allowUnsafeSymbols: false
|
34 |
});
|
35 |
}
|
36 |
|
@@ -118,9 +113,14 @@
|
|
118 |
!!promptValue &&
|
119 |
promptValue.length > 0;
|
120 |
|
121 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
122 |
isValid = isValid &&
|
123 |
-
validator.isLength(promptValue, { min: 1 });
|
124 |
}
|
125 |
|
126 |
submitBtn.disabled = !isValid;
|
@@ -144,137 +144,89 @@
|
|
144 |
setTimeout(function () {
|
145 |
var fa = document.getElementById('formAction');
|
146 |
if (fa) fa.value = 'generate';
|
147 |
-
|
148 |
}, 100);
|
149 |
}
|
150 |
};
|
151 |
|
152 |
-
var
|
153 |
var form = document.getElementById('generateForm');
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
var btnText = cancelBtn.querySelector('span') ||
|
165 |
-
cancelBtn.childNodes[cancelBtn.childNodes.length - 1];
|
166 |
-
|
167 |
-
if (btnText && btnText.nodeType === 3) {
|
168 |
-
btnText.textContent = 'Cancelling...';
|
169 |
-
} else if (!btnText) {
|
170 |
-
var textNode = Array.from(cancelBtn.childNodes)
|
171 |
-
.find(function(node) {
|
172 |
-
return node.nodeType === 3 &&
|
173 |
-
node.textContent.trim();
|
174 |
-
});
|
175 |
-
if (textNode) {
|
176 |
-
textNode.textContent = 'Cancelling...';
|
177 |
-
}
|
178 |
-
}
|
179 |
-
}
|
180 |
-
|
181 |
-
if (fa) {
|
182 |
-
fa.value = 'cancel';
|
183 |
-
|
184 |
-
if (form) {
|
185 |
-
var hiddenCancel = document.createElement('input');
|
186 |
-
hiddenCancel.type = 'hidden';
|
187 |
-
hiddenCancel.name = 'forceCancel';
|
188 |
-
hiddenCancel.value = 'true';
|
189 |
-
form.appendChild(hiddenCancel);
|
190 |
-
|
191 |
-
var timestamp = document.createElement('input');
|
192 |
-
timestamp.type = 'hidden';
|
193 |
-
timestamp.name = 'cancelTime';
|
194 |
-
timestamp.value = Date.now();
|
195 |
-
form.appendChild(timestamp);
|
196 |
-
|
197 |
-
try {
|
198 |
-
form.submit();
|
199 |
-
} catch (e) {
|
200 |
-
form.requestSubmit && form.requestSubmit();
|
201 |
-
}
|
202 |
-
|
203 |
-
setTimeout(function() {
|
204 |
-
if (form && fa) {
|
205 |
-
fa.value = 'cancel';
|
206 |
-
try {
|
207 |
-
form.submit();
|
208 |
-
} catch (e) {
|
209 |
-
window.location.reload();
|
210 |
-
}
|
211 |
-
}
|
212 |
-
}, 500);
|
213 |
-
|
214 |
-
setTimeout(function() {
|
215 |
-
window.location.reload();
|
216 |
-
}, 3000);
|
217 |
-
}
|
218 |
-
} else if (form) {
|
219 |
-
var newFA = document.createElement('input');
|
220 |
-
newFA.type = 'hidden';
|
221 |
-
newFA.id = 'formAction';
|
222 |
-
newFA.name = 'action';
|
223 |
-
newFA.value = 'cancel';
|
224 |
-
form.appendChild(newFA);
|
225 |
-
|
226 |
-
try {
|
227 |
-
form.submit();
|
228 |
-
} catch (e) {
|
229 |
-
window.location.reload();
|
230 |
-
}
|
231 |
-
} else {
|
232 |
-
var xhr = new XMLHttpRequest();
|
233 |
-
xhr.open('POST', window.location.href, true);
|
234 |
-
xhr.setRequestHeader(
|
235 |
-
'Content-Type',
|
236 |
-
'application/x-www-form-urlencoded'
|
237 |
-
);
|
238 |
-
xhr.onload = function() {
|
239 |
-
window.location.reload();
|
240 |
-
};
|
241 |
-
xhr.onerror = function() {
|
242 |
-
window.location.reload();
|
243 |
-
};
|
244 |
-
xhr.send('action=cancel&forceCancel=true');
|
245 |
-
}
|
246 |
-
|
247 |
-
window.cancelRequested = true;
|
248 |
-
|
249 |
-
if (window.EventSource) {
|
250 |
-
try {
|
251 |
-
var sources = window.eventSources || [];
|
252 |
-
sources.forEach(function(source) {
|
253 |
-
if (source && source.close) {
|
254 |
-
source.close();
|
255 |
-
}
|
256 |
-
});
|
257 |
-
} catch (e) {}
|
258 |
}
|
259 |
-
|
260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
261 |
try {
|
262 |
-
|
|
|
|
|
|
|
263 |
} catch (e) {}
|
264 |
-
}
|
|
|
|
|
|
|
265 |
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
273 |
};
|
274 |
|
275 |
var modelSelect = document.getElementById('modelSelect');
|
276 |
var sizeSelect = document.getElementById('sizeSelect');
|
277 |
var promptInput = document.getElementById('promptInput');
|
|
|
278 |
|
279 |
if (modelSelect) {
|
280 |
modelSelect.addEventListener('change', validateInputs);
|
@@ -302,17 +254,40 @@
|
|
302 |
e.preventDefault();
|
303 |
var submitBtn = document.getElementById('submitBtn');
|
304 |
if (submitBtn && !submitBtn.disabled) {
|
305 |
-
|
306 |
-
if (form) form.submit();
|
307 |
}
|
308 |
}
|
309 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
310 |
}
|
311 |
|
312 |
window.closeWelcomeModal = closeWelcomeModal;
|
313 |
window.closeErrorModal = closeErrorModal;
|
314 |
window.triggerExample = triggerExample;
|
315 |
window.cancelGeneration = cancelGeneration;
|
|
|
316 |
|
317 |
validateInputs();
|
318 |
|
@@ -329,11 +304,13 @@
|
|
329 |
|
330 |
document.addEventListener('DOMContentLoaded', function() {
|
331 |
InitWelcomeModal();
|
|
|
332 |
});
|
333 |
|
334 |
if (document.readyState === 'complete' ||
|
335 |
document.readyState === 'interactive') {
|
336 |
InitWelcomeModal();
|
|
|
337 |
}
|
338 |
|
339 |
setInterval(function () {
|
|
|
23 |
});
|
24 |
}
|
25 |
|
26 |
+
if (encode === true && typeof he !== 'undefined') {
|
|
|
|
|
|
|
|
|
27 |
clean = he.encode(clean, {
|
28 |
+
encodeEverything: false
|
|
|
29 |
});
|
30 |
}
|
31 |
|
|
|
113 |
!!promptValue &&
|
114 |
promptValue.length > 0;
|
115 |
|
116 |
+
if (isValid) {
|
117 |
+
var hasNonWhitespace = /[^\s\t\n\r\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]/.test(promptValue);
|
118 |
+
isValid = isValid && hasNonWhitespace;
|
119 |
+
}
|
120 |
+
|
121 |
+
if (typeof validator !== 'undefined' && isValid) {
|
122 |
isValid = isValid &&
|
123 |
+
validator.isLength(promptValue.trim(), { min: 1 });
|
124 |
}
|
125 |
|
126 |
submitBtn.disabled = !isValid;
|
|
|
144 |
setTimeout(function () {
|
145 |
var fa = document.getElementById('formAction');
|
146 |
if (fa) fa.value = 'generate';
|
147 |
+
submitForm();
|
148 |
}, 100);
|
149 |
}
|
150 |
};
|
151 |
|
152 |
+
var submitForm = function() {
|
153 |
var form = document.getElementById('generateForm');
|
154 |
+
if (!form) return;
|
155 |
+
|
156 |
+
var formData = new FormData(form);
|
157 |
+
var promptValue = sanitizeInput(formData.get('prompt'));
|
158 |
+
|
159 |
+
var hasNonWhitespace = /[^\s\t\n\r\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]/.test(promptValue);
|
160 |
+
|
161 |
+
if (!promptValue || !hasNonWhitespace) {
|
162 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
163 |
}
|
164 |
+
|
165 |
+
var data = {
|
166 |
+
action: formData.get('action') || 'generate',
|
167 |
+
prompt: promptValue,
|
168 |
+
model: formData.get('model'),
|
169 |
+
size: formData.get('size'),
|
170 |
+
sessionId: document.body.dataset.sessionId || ''
|
171 |
+
};
|
172 |
+
|
173 |
+
var xhr = new XMLHttpRequest();
|
174 |
+
xhr.open('POST', '/', true);
|
175 |
+
xhr.setRequestHeader(
|
176 |
+
'Content-Type',
|
177 |
+
'application/json'
|
178 |
+
);
|
179 |
+
|
180 |
+
xhr.onload = function() {
|
181 |
try {
|
182 |
+
var response = JSON.parse(xhr.responseText);
|
183 |
+
if (!response.success && response.error) {
|
184 |
+
showErrorModal(response.error);
|
185 |
+
}
|
186 |
} catch (e) {}
|
187 |
+
};
|
188 |
+
|
189 |
+
xhr.send(JSON.stringify(data));
|
190 |
+
};
|
191 |
|
192 |
+
var cancelGeneration = function () {
|
193 |
+
var xhr = new XMLHttpRequest();
|
194 |
+
xhr.open('POST', '/', true);
|
195 |
+
xhr.setRequestHeader(
|
196 |
+
'Content-Type',
|
197 |
+
'application/json'
|
198 |
+
);
|
199 |
+
|
200 |
+
xhr.send(JSON.stringify({
|
201 |
+
action: 'cancel',
|
202 |
+
sessionId: document.body.dataset.sessionId || ''
|
203 |
+
}));
|
204 |
+
};
|
205 |
+
|
206 |
+
var showErrorModal = function(error) {
|
207 |
+
var existingModal = document.getElementById('errorModal');
|
208 |
+
if (existingModal) existingModal.remove();
|
209 |
+
|
210 |
+
var modal = document.createElement('div');
|
211 |
+
modal.id = 'errorModal';
|
212 |
+
modal.className = 'modal-overlay';
|
213 |
+
modal.innerHTML =
|
214 |
+
'<div class="modal-content modal-error-content">' +
|
215 |
+
'<div class="modal-inner">' +
|
216 |
+
'<h3 class="modal-error-title">Error</h3>' +
|
217 |
+
'<p class="modal-error-text">' + error + '</p>' +
|
218 |
+
'<button onclick="closeErrorModal()" ' +
|
219 |
+
'class="btn btn-primary w-full">OK</button>' +
|
220 |
+
'</div>' +
|
221 |
+
'</div>';
|
222 |
+
|
223 |
+
document.body.appendChild(modal);
|
224 |
};
|
225 |
|
226 |
var modelSelect = document.getElementById('modelSelect');
|
227 |
var sizeSelect = document.getElementById('sizeSelect');
|
228 |
var promptInput = document.getElementById('promptInput');
|
229 |
+
var form = document.getElementById('generateForm');
|
230 |
|
231 |
if (modelSelect) {
|
232 |
modelSelect.addEventListener('change', validateInputs);
|
|
|
254 |
e.preventDefault();
|
255 |
var submitBtn = document.getElementById('submitBtn');
|
256 |
if (submitBtn && !submitBtn.disabled) {
|
257 |
+
submitForm();
|
|
|
258 |
}
|
259 |
}
|
260 |
});
|
261 |
+
|
262 |
+
promptInput.addEventListener('keyup', function () {
|
263 |
+
validateInputs();
|
264 |
+
});
|
265 |
+
|
266 |
+
promptInput.addEventListener('blur', function () {
|
267 |
+
validateInputs();
|
268 |
+
});
|
269 |
+
}
|
270 |
+
|
271 |
+
if (form) {
|
272 |
+
form.addEventListener('submit', function(e) {
|
273 |
+
e.preventDefault();
|
274 |
+
|
275 |
+
var promptValue = sanitizeInput(promptInput ? promptInput.value : '');
|
276 |
+
var hasNonWhitespace = /[^\s\t\n\r\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]/.test(promptValue);
|
277 |
+
|
278 |
+
if (!promptValue || !hasNonWhitespace) {
|
279 |
+
return;
|
280 |
+
}
|
281 |
+
|
282 |
+
submitForm();
|
283 |
+
});
|
284 |
}
|
285 |
|
286 |
window.closeWelcomeModal = closeWelcomeModal;
|
287 |
window.closeErrorModal = closeErrorModal;
|
288 |
window.triggerExample = triggerExample;
|
289 |
window.cancelGeneration = cancelGeneration;
|
290 |
+
window.validateInputs = validateInputs;
|
291 |
|
292 |
validateInputs();
|
293 |
|
|
|
304 |
|
305 |
document.addEventListener('DOMContentLoaded', function() {
|
306 |
InitWelcomeModal();
|
307 |
+
validateInputs();
|
308 |
});
|
309 |
|
310 |
if (document.readyState === 'complete' ||
|
311 |
document.readyState === 'interactive') {
|
312 |
InitWelcomeModal();
|
313 |
+
validateInputs();
|
314 |
}
|
315 |
|
316 |
setInterval(function () {
|
config.js
CHANGED
@@ -23,11 +23,15 @@ export default {
|
|
23 |
},
|
24 |
generation: {
|
25 |
progressInterval: 800,
|
26 |
-
startDelay:
|
27 |
maxProgress: 99
|
28 |
},
|
29 |
paths: {
|
30 |
views: 'public',
|
31 |
mainView: 'webViewer'
|
|
|
|
|
|
|
|
|
32 |
}
|
33 |
};
|
|
|
23 |
},
|
24 |
generation: {
|
25 |
progressInterval: 800,
|
26 |
+
startDelay: 50,
|
27 |
maxProgress: 99
|
28 |
},
|
29 |
paths: {
|
30 |
views: 'public',
|
31 |
mainView: 'webViewer'
|
32 |
+
},
|
33 |
+
websocket: {
|
34 |
+
heartbeat: 30000,
|
35 |
+
reconnectDelay: 100
|
36 |
}
|
37 |
};
|
example.js
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
//
|
2 |
+
// SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
// SPDX-License-Identifier: Apache-2.0
|
4 |
+
//
|
5 |
+
|
6 |
+
const examples = [
|
7 |
+
{
|
8 |
+
prompt: 'A majestic mountain landscape at golden hour with dramatic clouds and vibrant colors reflecting on a crystal clear alpine lake',
|
9 |
+
model: 'flux',
|
10 |
+
size: '1536x1024',
|
11 |
+
modelLabel: 'FLUX',
|
12 |
+
sizeLabel: '1536×1024',
|
13 |
+
note: 'Recommended model'
|
14 |
+
},
|
15 |
+
{
|
16 |
+
prompt: 'An enchanted forest with bioluminescent plants and magical floating particles, moonlight streaming through ancient mystical trees',
|
17 |
+
model: 'kontext',
|
18 |
+
size: '1024x1024',
|
19 |
+
modelLabel: 'KONTEXT',
|
20 |
+
sizeLabel: '1024×1024',
|
21 |
+
note: 'Sometimes the server is busy'
|
22 |
+
},
|
23 |
+
{
|
24 |
+
prompt: 'Underwater coral reef teeming with colorful tropical fish and sea life, sunbeams penetrating crystal clear turquoise water',
|
25 |
+
model: 'turbo',
|
26 |
+
size: '1024x768',
|
27 |
+
modelLabel: 'TURBO',
|
28 |
+
sizeLabel: '1024×768',
|
29 |
+
note: 'Sometimes the server is busy'
|
30 |
+
},
|
31 |
+
{
|
32 |
+
prompt: 'A woman riding a horse across an open field, cinematic and realistic style',
|
33 |
+
model: 'nanobanana',
|
34 |
+
size: '1024x1024',
|
35 |
+
modelLabel: 'NANO BANANA',
|
36 |
+
sizeLabel: '1024×1024',
|
37 |
+
note: 'Sometimes the resolution is forced to be square'
|
38 |
+
}
|
39 |
+
];
|
40 |
+
|
41 |
+
if (typeof module !== 'undefined' && module.exports) {
|
42 |
+
module.exports = { examples };
|
43 |
+
}
|
model.js
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
//
|
2 |
+
// SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
// SPDX-License-Identifier: Apache-2.0
|
4 |
+
//
|
5 |
+
|
6 |
+
const models = [
|
7 |
+
{ value: 'flux', label: 'Flux' },
|
8 |
+
{ value: 'kontext', label: 'Kontext' },
|
9 |
+
{ value: 'turbo', label: 'Turbo' },
|
10 |
+
{ value: 'nanobanana', label: 'Nano Banana' }
|
11 |
+
];
|
12 |
+
|
13 |
+
if (typeof module !== 'undefined' && module.exports) {
|
14 |
+
module.exports = { models };
|
15 |
+
}
|
package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
{
|
2 |
"name": "Image Generation Playground",
|
3 |
-
"version": "0.0
|
4 |
"description": "Part of the UltimaX Intelligence ecosystem",
|
5 |
"type": "module",
|
6 |
"main": "server.js",
|
@@ -19,7 +19,8 @@
|
|
19 |
"dependencies": {
|
20 |
"axios": "latest",
|
21 |
"ejs": "latest",
|
22 |
-
"express": "latest"
|
|
|
23 |
},
|
24 |
"homepage": "https://umint-image.hf.space"
|
25 |
}
|
|
|
1 |
{
|
2 |
"name": "Image Generation Playground",
|
3 |
+
"version": "1.0.0",
|
4 |
"description": "Part of the UltimaX Intelligence ecosystem",
|
5 |
"type": "module",
|
6 |
"main": "server.js",
|
|
|
19 |
"dependencies": {
|
20 |
"axios": "latest",
|
21 |
"ejs": "latest",
|
22 |
+
"express": "latest",
|
23 |
+
"ws": "latest"
|
24 |
},
|
25 |
"homepage": "https://umint-image.hf.space"
|
26 |
}
|
public/webViewer.ejs
CHANGED
@@ -68,12 +68,17 @@
|
|
68 |
type="image/x-icon" />
|
69 |
|
70 |
<title>Image Generation Playground</title>
|
|
|
71 |
<script src="https://cdn.tailwindcss.com"></script>
|
72 |
|
73 |
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
|
|
74 |
|
75 |
<link rel="stylesheet"
|
76 |
-
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.
|
|
|
|
|
|
|
77 |
|
78 |
<link rel="stylesheet" href="/__public__/assets/css/styles.css" />
|
79 |
<link rel="stylesheet" href="/__public__/assets/css/webLoader.css" />
|
@@ -82,13 +87,17 @@
|
|
82 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/validator/13.15.15/validator.js"></script>
|
83 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/he/1.2.0/he.js"></script>
|
84 |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/xss.js"></script>
|
|
|
|
|
|
|
|
|
85 |
</head>
|
86 |
<body data-is-generating="<%= !!isGenerating %>"
|
87 |
-
data-
|
88 |
<div class="gradient-bg"></div>
|
89 |
<div class="gradient-mesh"></div>
|
90 |
|
91 |
-
|
92 |
<div class="modal-content">
|
93 |
<div class="modal-inner">
|
94 |
<h2 class="header-gradient animate__animated
|
@@ -188,18 +197,15 @@
|
|
188 |
fill="currentColor"/>
|
189 |
</svg>
|
190 |
</a>
|
191 |
-
<
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
<
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
</svg>
|
201 |
-
</button>
|
202 |
-
</form>
|
203 |
</div>
|
204 |
<div class="image-info">
|
205 |
<p class="image-prompt"><%= image.prompt %></p>
|
@@ -234,6 +240,8 @@
|
|
234 |
<form method="POST" action="/" id="generateForm">
|
235 |
<input type="hidden" name="action" value="generate"
|
236 |
id="formAction">
|
|
|
|
|
237 |
|
238 |
<div class="grid md:grid-cols-2 gap-6 form-grid">
|
239 |
<div>
|
@@ -242,10 +250,6 @@
|
|
242 |
class="input-field select-field"
|
243 |
<%= isGenerating ? 'disabled' : '' %>>
|
244 |
<option value="">Select a model</option>
|
245 |
-
<option value="flux">Flux</option>
|
246 |
-
<option value="kontext">Kontext</option>
|
247 |
-
<option value="turbo">Turbo</option>
|
248 |
-
<option value="nanobanana">Nano Banana</option>
|
249 |
</select>
|
250 |
</div>
|
251 |
|
@@ -255,30 +259,6 @@
|
|
255 |
class="input-field select-field"
|
256 |
<%= isGenerating ? 'disabled' : '' %>>
|
257 |
<option value="">Select size</option>
|
258 |
-
<optgroup label="Square">
|
259 |
-
<option value="256x256">256×256</option>
|
260 |
-
<option value="512x512">512×512</option>
|
261 |
-
<option value="768x768">768×768</option>
|
262 |
-
<option value="1024x1024">1024×1024</option>
|
263 |
-
<option value="1536x1536">1536×1536</option>
|
264 |
-
<option value="2048x2048">2048×2048</option>
|
265 |
-
</optgroup>
|
266 |
-
<optgroup label="Portrait">
|
267 |
-
<option value="256x384">256×384</option>
|
268 |
-
<option value="384x512">384×512</option>
|
269 |
-
<option value="512x768">512×768</option>
|
270 |
-
<option value="768x1024">768×1024</option>
|
271 |
-
<option value="1024x1536">1024×1536</option>
|
272 |
-
<option value="1536x2048">1536×2048</option>
|
273 |
-
</optgroup>
|
274 |
-
<optgroup label="Landscape">
|
275 |
-
<option value="384x256">384×256</option>
|
276 |
-
<option value="512x384">512×384</option>
|
277 |
-
<option value="768x512">768×512</option>
|
278 |
-
<option value="1024x768">1024×768</option>
|
279 |
-
<option value="1536x1024">1536×1024</option>
|
280 |
-
<option value="2048x1536">2048×1536</option>
|
281 |
-
</optgroup>
|
282 |
</select>
|
283 |
</div>
|
284 |
</div>
|
@@ -321,54 +301,8 @@
|
|
321 |
<section class="card animate__animated animate__fadeIn
|
322 |
animate__delay-1s examples-section">
|
323 |
<h3 class="examples-title">Example Prompts</h3>
|
324 |
-
<div class="grid lg:grid-cols-3 md:grid-cols-2 gap-4"
|
325 |
-
|
326 |
-
onclick="triggerExample('A majestic mountain landscape at golden hour with dramatic clouds and vibrant colors reflecting on a crystal clear alpine lake', 'flux', '1536x1024')">
|
327 |
-
<p class="example-text">
|
328 |
-
A majestic mountain landscape at golden hour with
|
329 |
-
dramatic clouds and vibrant colors reflecting on a
|
330 |
-
crystal clear alpine lake
|
331 |
-
</p>
|
332 |
-
<p class="example-meta">
|
333 |
-
<span class="example-model">FLUX</span> | 1536×1024
|
334 |
-
</p>
|
335 |
-
</div>
|
336 |
-
|
337 |
-
<div class="example-card"
|
338 |
-
onclick="triggerExample('An enchanted forest with bioluminescent plants and magical floating particles, moonlight streaming through ancient mystical trees', 'kontext', '1024x1024')">
|
339 |
-
<p class="example-text">
|
340 |
-
An enchanted forest with bioluminescent plants and
|
341 |
-
magical floating particles, moonlight streaming
|
342 |
-
through ancient mystical trees
|
343 |
-
</p>
|
344 |
-
<p class="example-meta">
|
345 |
-
<span class="example-model">KONTEXT</span> | 1024×1024 | Sometimes the server is busy
|
346 |
-
</p>
|
347 |
-
</div>
|
348 |
-
|
349 |
-
<div class="example-card"
|
350 |
-
onclick="triggerExample('Underwater coral reef teeming with colorful tropical fish and sea life, sunbeams penetrating crystal clear turquoise water', 'turbo', '1024x768')">
|
351 |
-
<p class="example-text">
|
352 |
-
Underwater coral reef teeming with colorful tropical
|
353 |
-
fish and sea life, sunbeams penetrating crystal
|
354 |
-
clear turquoise water
|
355 |
-
</p>
|
356 |
-
<p class="example-meta">
|
357 |
-
<span class="example-model">TURBO</span> | 1024×768 | Sometimes the server is busy
|
358 |
-
</p>
|
359 |
-
</div>
|
360 |
-
|
361 |
-
<div class="example-card"
|
362 |
-
onclick="triggerExample('A woman riding a horse across an open field, cinematic and realistic style', 'nanobanana', '1024x768')">
|
363 |
-
<p class="example-text">
|
364 |
-
A woman riding a horse across an open field,
|
365 |
-
cinematic and realistic style
|
366 |
-
</p>
|
367 |
-
<p class="example-meta">
|
368 |
-
<span class="example-model">NANO BANANA</span> | 1024×768
|
369 |
-
</p>
|
370 |
-
</div>
|
371 |
-
</div>
|
372 |
</section>
|
373 |
</main>
|
374 |
|
@@ -386,6 +320,7 @@
|
|
386 |
</a>
|
387 |
</div>
|
388 |
</footer>
|
|
|
389 |
<script defer src="/__public__/assets/plugins/imageGenerator.js"></script>
|
390 |
<script defer src="/__public__/assets/plugins/webLoader.js"></script>
|
391 |
</body>
|
|
|
68 |
type="image/x-icon" />
|
69 |
|
70 |
<title>Image Generation Playground</title>
|
71 |
+
|
72 |
<script src="https://cdn.tailwindcss.com"></script>
|
73 |
|
74 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
75 |
+
rel="stylesheet">
|
76 |
|
77 |
<link rel="stylesheet"
|
78 |
+
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.css"
|
79 |
+
integrity="sha512-phGxLIsvHFArdI7IyLjv14dchvbVkEDaH95efvAae/y2exeWBQCQDpNFbOTdV1p4/pIa/XtbuDCnfhDEIXhvGQ=="
|
80 |
+
crossorigin="anonymous"
|
81 |
+
referrerpolicy="no-referrer" />
|
82 |
|
83 |
<link rel="stylesheet" href="/__public__/assets/css/styles.css" />
|
84 |
<link rel="stylesheet" href="/__public__/assets/css/webLoader.css" />
|
|
|
87 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/validator/13.15.15/validator.js"></script>
|
88 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/he/1.2.0/he.js"></script>
|
89 |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/xss.js"></script>
|
90 |
+
|
91 |
+
<script src="/__public__/data/models.js"></script>
|
92 |
+
<script src="/__public__/data/resolution.js"></script>
|
93 |
+
<script src="/__public__/data/examples.js"></script>
|
94 |
</head>
|
95 |
<body data-is-generating="<%= !!isGenerating %>"
|
96 |
+
data-session-id="<%= sessionId || '' %>">
|
97 |
<div class="gradient-bg"></div>
|
98 |
<div class="gradient-mesh"></div>
|
99 |
|
100 |
+
<div id="welcomeModal" class="modal-overlay" style="display: none;">
|
101 |
<div class="modal-content">
|
102 |
<div class="modal-inner">
|
103 |
<h2 class="header-gradient animate__animated
|
|
|
197 |
fill="currentColor"/>
|
198 |
</svg>
|
199 |
</a>
|
200 |
+
<button type="button"
|
201 |
+
onclick="deleteImage(<%= index %>)"
|
202 |
+
class="action-btn">
|
203 |
+
<svg class="action-icon" viewBox="0 0 24 24"
|
204 |
+
fill="none">
|
205 |
+
<path d="M18.3 5.71C17.91 5.32 17.28 5.32 16.89 5.71L12 10.59L7.11 5.7C6.72 5.31 6.09 5.31 5.7 5.7C5.31 6.09 5.31 6.72 5.7 7.11L10.59 12L5.7 16.89C5.31 17.28 5.31 17.91 5.7 18.3C6.09 18.69 6.72 18.69 7.11 18.3L12 13.41L16.89 18.3C17.28 18.69 17.91 18.69 18.3 18.3C18.69 17.91 18.69 17.28 18.3 16.89L13.41 12L18.3 7.11C18.68 6.73 18.68 6.09 18.3 5.71Z"
|
206 |
+
fill="currentColor"/>
|
207 |
+
</svg>
|
208 |
+
</button>
|
|
|
|
|
|
|
209 |
</div>
|
210 |
<div class="image-info">
|
211 |
<p class="image-prompt"><%= image.prompt %></p>
|
|
|
240 |
<form method="POST" action="/" id="generateForm">
|
241 |
<input type="hidden" name="action" value="generate"
|
242 |
id="formAction">
|
243 |
+
<input type="hidden" name="sessionId"
|
244 |
+
value="<%= sessionId %>" />
|
245 |
|
246 |
<div class="grid md:grid-cols-2 gap-6 form-grid">
|
247 |
<div>
|
|
|
250 |
class="input-field select-field"
|
251 |
<%= isGenerating ? 'disabled' : '' %>>
|
252 |
<option value="">Select a model</option>
|
|
|
|
|
|
|
|
|
253 |
</select>
|
254 |
</div>
|
255 |
|
|
|
259 |
class="input-field select-field"
|
260 |
<%= isGenerating ? 'disabled' : '' %>>
|
261 |
<option value="">Select size</option>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
262 |
</select>
|
263 |
</div>
|
264 |
</div>
|
|
|
301 |
<section class="card animate__animated animate__fadeIn
|
302 |
animate__delay-1s examples-section">
|
303 |
<h3 class="examples-title">Example Prompts</h3>
|
304 |
+
<div class="grid lg:grid-cols-3 md:grid-cols-2 gap-4"
|
305 |
+
id="examplesGrid"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
306 |
</section>
|
307 |
</main>
|
308 |
|
|
|
320 |
</a>
|
321 |
</div>
|
322 |
</footer>
|
323 |
+
<script src="/__public__/assets/plugins/loadParameter.js"></script>
|
324 |
<script defer src="/__public__/assets/plugins/imageGenerator.js"></script>
|
325 |
<script defer src="/__public__/assets/plugins/webLoader.js"></script>
|
326 |
</body>
|
resolution.js
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
//
|
2 |
+
// SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
// SPDX-License-Identifier: Apache-2.0
|
4 |
+
//
|
5 |
+
|
6 |
+
const sizes = {
|
7 |
+
square: [
|
8 |
+
{ value: '256x256', label: '256×256' },
|
9 |
+
{ value: '512x512', label: '512×512' },
|
10 |
+
{ value: '768x768', label: '768×768' },
|
11 |
+
{ value: '1024x1024', label: '1024×1024' },
|
12 |
+
{ value: '1536x1536', label: '1536×1536' },
|
13 |
+
{ value: '2048x2048', label: '2048×2048' }
|
14 |
+
],
|
15 |
+
portrait: [
|
16 |
+
{ value: '256x384', label: '256×384' },
|
17 |
+
{ value: '384x512', label: '384×512' },
|
18 |
+
{ value: '512x768', label: '512×768' },
|
19 |
+
{ value: '768x1024', label: '768×1024' },
|
20 |
+
{ value: '1024x1536', label: '1024×1536' },
|
21 |
+
{ value: '1536x2048', label: '1536×2048' }
|
22 |
+
],
|
23 |
+
landscape: [
|
24 |
+
{ value: '384x256', label: '384×256' },
|
25 |
+
{ value: '512x384', label: '512×384' },
|
26 |
+
{ value: '768x512', label: '768×512' },
|
27 |
+
{ value: '1024x768', label: '1024×768' },
|
28 |
+
{ value: '1536x1024', label: '1536×1024' },
|
29 |
+
{ value: '2048x1536', label: '2048×1536' }
|
30 |
+
]
|
31 |
+
};
|
32 |
+
|
33 |
+
if (typeof module !== 'undefined' && module.exports) {
|
34 |
+
module.exports = { sizes };
|
35 |
+
}
|
server.js
CHANGED
@@ -4,6 +4,8 @@
|
|
4 |
//
|
5 |
|
6 |
import express from 'express';
|
|
|
|
|
7 |
import { fileURLToPath } from 'url';
|
8 |
import path from 'path';
|
9 |
import config from './config.js';
|
@@ -13,23 +15,43 @@ import { initCleanup } from
|
|
13 |
'./src/services/storageManager.js';
|
14 |
import { setupViewEngine } from
|
15 |
'./src/middleware/viewEngine.js';
|
|
|
|
|
16 |
|
17 |
const __filename = fileURLToPath(import.meta.url);
|
18 |
const __dirname = path.dirname(__filename);
|
19 |
-
|
20 |
const app = express();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
|
22 |
setupViewEngine(app, __dirname);
|
|
|
23 |
|
24 |
app.use(express.urlencoded({
|
25 |
extended: true,
|
26 |
limit: config.limits.bodySize
|
27 |
}));
|
28 |
|
29 |
-
app.use(
|
30 |
-
|
31 |
-
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
app.use(express.static(
|
35 |
path.join(__dirname, 'public')
|
@@ -39,7 +61,7 @@ app.use('/', imageRoutes);
|
|
39 |
|
40 |
initCleanup();
|
41 |
|
42 |
-
|
43 |
config.server.port,
|
44 |
config.server.host
|
45 |
);
|
|
|
4 |
//
|
5 |
|
6 |
import express from 'express';
|
7 |
+
import { createServer } from 'http';
|
8 |
+
import { WebSocketServer } from 'ws';
|
9 |
import { fileURLToPath } from 'url';
|
10 |
import path from 'path';
|
11 |
import config from './config.js';
|
|
|
15 |
'./src/services/storageManager.js';
|
16 |
import { setupViewEngine } from
|
17 |
'./src/middleware/viewEngine.js';
|
18 |
+
import { setupWebSocket } from
|
19 |
+
'./src/services/websocketManager.js';
|
20 |
|
21 |
const __filename = fileURLToPath(import.meta.url);
|
22 |
const __dirname = path.dirname(__filename);
|
|
|
23 |
const app = express();
|
24 |
+
const mapping = {
|
25 |
+
"/__public__/assets": "assets",
|
26 |
+
"/__public__/data/models.js": "model.js",
|
27 |
+
"/__public__/data/resolution.js": "resolution.js",
|
28 |
+
"/__public__/data/examples.js": "example.js"
|
29 |
+
};
|
30 |
+
const server = createServer(app);
|
31 |
+
const wss = new WebSocketServer({ server });
|
32 |
|
33 |
setupViewEngine(app, __dirname);
|
34 |
+
setupWebSocket(wss);
|
35 |
|
36 |
app.use(express.urlencoded({
|
37 |
extended: true,
|
38 |
limit: config.limits.bodySize
|
39 |
}));
|
40 |
|
41 |
+
app.use(express.json({
|
42 |
+
limit: config.limits.bodySize
|
43 |
+
}));
|
44 |
+
|
45 |
+
for (const [route, target]
|
46 |
+
of Object.entries(mapping)
|
47 |
+
) {
|
48 |
+
app.use(
|
49 |
+
route,
|
50 |
+
express.static(
|
51 |
+
path.resolve(target)
|
52 |
+
)
|
53 |
+
);
|
54 |
+
}
|
55 |
|
56 |
app.use(express.static(
|
57 |
path.join(__dirname, 'public')
|
|
|
61 |
|
62 |
initCleanup();
|
63 |
|
64 |
+
server.listen(
|
65 |
config.server.port,
|
66 |
config.server.host
|
67 |
);
|
src/controllers/imageController.js
CHANGED
@@ -20,19 +20,22 @@ import {
|
|
20 |
cancelGeneration
|
21 |
} from '../services/imageGenerator.js';
|
22 |
import { renderView } from '../utils/viewRenderer.js';
|
|
|
|
|
|
|
23 |
|
24 |
export const renderHome = (req, res) => {
|
25 |
-
const
|
26 |
-
const data =
|
27 |
|
28 |
renderView(res, {
|
29 |
...data,
|
30 |
-
|
31 |
});
|
32 |
};
|
33 |
|
34 |
export const handleAction = async (req, res) => {
|
35 |
-
const
|
36 |
const {
|
37 |
action,
|
38 |
prompt,
|
@@ -41,23 +44,23 @@ export const handleAction = async (req, res) => {
|
|
41 |
imageIndex
|
42 |
} = req.body;
|
43 |
|
44 |
-
let data = getStorage(
|
45 |
|
46 |
switch(action) {
|
47 |
case 'delete':
|
48 |
return handleDelete(
|
49 |
-
|
50 |
imageIndex,
|
51 |
data,
|
52 |
res
|
53 |
);
|
54 |
|
55 |
case 'cancel':
|
56 |
-
return handleCancel(
|
57 |
|
58 |
case 'generate':
|
59 |
return handleGenerate(
|
60 |
-
|
61 |
prompt,
|
62 |
model,
|
63 |
size,
|
@@ -66,12 +69,15 @@ export const handleAction = async (req, res) => {
|
|
66 |
);
|
67 |
|
68 |
default:
|
69 |
-
return res.
|
|
|
|
|
|
|
70 |
}
|
71 |
};
|
72 |
|
73 |
const handleDelete = (
|
74 |
-
|
75 |
imageIndex,
|
76 |
data,
|
77 |
res
|
@@ -81,18 +87,34 @@ const handleDelete = (
|
|
81 |
data.images.length
|
82 |
)) {
|
83 |
data.images.splice(parseInt(imageIndex), 1);
|
84 |
-
setStorage(
|
|
|
|
|
|
|
|
|
|
|
85 |
}
|
86 |
-
|
|
|
|
|
|
|
|
|
87 |
};
|
88 |
|
89 |
-
const handleCancel = (
|
90 |
-
cancelGeneration(
|
91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
};
|
93 |
|
94 |
const handleGenerate = async (
|
95 |
-
|
96 |
prompt,
|
97 |
model,
|
98 |
size,
|
@@ -106,33 +128,37 @@ const handleGenerate = async (
|
|
106 |
model,
|
107 |
size
|
108 |
)) {
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
...data,
|
113 |
-
requestId
|
114 |
});
|
115 |
}
|
116 |
|
117 |
if (!validateApiConfig()) {
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
...data,
|
123 |
-
requestId
|
124 |
});
|
125 |
}
|
126 |
|
127 |
data.isGenerating = true;
|
128 |
-
data.progress =
|
129 |
data.error = null;
|
130 |
-
setStorage(
|
131 |
|
132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
133 |
|
134 |
await generateImage(
|
135 |
-
|
136 |
trimmedPrompt,
|
137 |
model,
|
138 |
size
|
|
|
20 |
cancelGeneration
|
21 |
} from '../services/imageGenerator.js';
|
22 |
import { renderView } from '../utils/viewRenderer.js';
|
23 |
+
import {
|
24 |
+
sendToSession
|
25 |
+
} from '../services/websocketManager.js';
|
26 |
|
27 |
export const renderHome = (req, res) => {
|
28 |
+
const sessionId = generateId();
|
29 |
+
const data = getDefaultData();
|
30 |
|
31 |
renderView(res, {
|
32 |
...data,
|
33 |
+
sessionId
|
34 |
});
|
35 |
};
|
36 |
|
37 |
export const handleAction = async (req, res) => {
|
38 |
+
const sessionId = req.body.sessionId || generateId();
|
39 |
const {
|
40 |
action,
|
41 |
prompt,
|
|
|
44 |
imageIndex
|
45 |
} = req.body;
|
46 |
|
47 |
+
let data = getStorage(sessionId) || getDefaultData();
|
48 |
|
49 |
switch(action) {
|
50 |
case 'delete':
|
51 |
return handleDelete(
|
52 |
+
sessionId,
|
53 |
imageIndex,
|
54 |
data,
|
55 |
res
|
56 |
);
|
57 |
|
58 |
case 'cancel':
|
59 |
+
return handleCancel(sessionId, res);
|
60 |
|
61 |
case 'generate':
|
62 |
return handleGenerate(
|
63 |
+
sessionId,
|
64 |
prompt,
|
65 |
model,
|
66 |
size,
|
|
|
69 |
);
|
70 |
|
71 |
default:
|
72 |
+
return res.json({
|
73 |
+
success: false,
|
74 |
+
error: 'Invalid action'
|
75 |
+
});
|
76 |
}
|
77 |
};
|
78 |
|
79 |
const handleDelete = (
|
80 |
+
sessionId,
|
81 |
imageIndex,
|
82 |
data,
|
83 |
res
|
|
|
87 |
data.images.length
|
88 |
)) {
|
89 |
data.images.splice(parseInt(imageIndex), 1);
|
90 |
+
setStorage(sessionId, data);
|
91 |
+
|
92 |
+
sendToSession(sessionId, {
|
93 |
+
type: 'imageDeleted',
|
94 |
+
images: data.images
|
95 |
+
});
|
96 |
}
|
97 |
+
|
98 |
+
return res.json({
|
99 |
+
success: true,
|
100 |
+
images: data.images
|
101 |
+
});
|
102 |
};
|
103 |
|
104 |
+
const handleCancel = (sessionId, res) => {
|
105 |
+
cancelGeneration(sessionId);
|
106 |
+
|
107 |
+
sendToSession(sessionId, {
|
108 |
+
type: 'generationCancelled'
|
109 |
+
});
|
110 |
+
|
111 |
+
return res.json({
|
112 |
+
success: true
|
113 |
+
});
|
114 |
};
|
115 |
|
116 |
const handleGenerate = async (
|
117 |
+
sessionId,
|
118 |
prompt,
|
119 |
model,
|
120 |
size,
|
|
|
128 |
model,
|
129 |
size
|
130 |
)) {
|
131 |
+
return res.json({
|
132 |
+
success: false,
|
133 |
+
error: 'Please fill in all required fields'
|
|
|
|
|
134 |
});
|
135 |
}
|
136 |
|
137 |
if (!validateApiConfig()) {
|
138 |
+
return res.json({
|
139 |
+
success: false,
|
140 |
+
error: 'The server is currently busy. ' +
|
141 |
+
'Please try again later.'
|
|
|
|
|
142 |
});
|
143 |
}
|
144 |
|
145 |
data.isGenerating = true;
|
146 |
+
data.progress = 0;
|
147 |
data.error = null;
|
148 |
+
setStorage(sessionId, data);
|
149 |
|
150 |
+
sendToSession(sessionId, {
|
151 |
+
type: 'generationStarted',
|
152 |
+
progress: 0
|
153 |
+
});
|
154 |
+
|
155 |
+
res.json({
|
156 |
+
success: true,
|
157 |
+
sessionId
|
158 |
+
});
|
159 |
|
160 |
await generateImage(
|
161 |
+
sessionId,
|
162 |
trimmedPrompt,
|
163 |
model,
|
164 |
size
|
src/services/imageGenerator.js
CHANGED
@@ -13,18 +13,26 @@ import {
|
|
13 |
setActiveGeneration,
|
14 |
deleteActiveGeneration
|
15 |
} from './storageManager.js';
|
|
|
|
|
|
|
16 |
|
17 |
-
const updateProgress = (
|
18 |
-
const data = getStorage(
|
19 |
if (data) {
|
20 |
data.progress = progress;
|
21 |
-
setStorage(
|
|
|
|
|
|
|
|
|
|
|
22 |
}
|
23 |
};
|
24 |
|
25 |
-
const createProgressUpdater = (
|
26 |
return setInterval(() => {
|
27 |
-
const data = getStorage(
|
28 |
if (data &&
|
29 |
data.isGenerating &&
|
30 |
data.progress < config.generation.maxProgress) {
|
@@ -33,7 +41,7 @@ const createProgressUpdater = (requestId) => {
|
|
33 |
config.generation.maxProgress,
|
34 |
data.progress + increment
|
35 |
);
|
36 |
-
updateProgress(
|
37 |
}
|
38 |
}, config.generation.progressInterval);
|
39 |
};
|
@@ -80,15 +88,15 @@ const callImageApi = async (
|
|
80 |
};
|
81 |
|
82 |
export const generateImage = async (
|
83 |
-
|
84 |
prompt,
|
85 |
model,
|
86 |
size
|
87 |
) => {
|
88 |
const controller = new AbortController();
|
89 |
-
setActiveGeneration(
|
90 |
|
91 |
-
const progressInterval = createProgressUpdater(
|
92 |
|
93 |
setTimeout(async () => {
|
94 |
try {
|
@@ -99,10 +107,10 @@ export const generateImage = async (
|
|
99 |
controller.signal
|
100 |
);
|
101 |
|
102 |
-
const data = getStorage(
|
103 |
if (!data) return;
|
104 |
|
105 |
-
updateProgress(
|
106 |
|
107 |
if (response.data?.data?.length > 0) {
|
108 |
const base64 = response.data.data[0].b64_json;
|
@@ -118,40 +126,50 @@ export const generateImage = async (
|
|
118 |
|
119 |
data.isGenerating = false;
|
120 |
data.progress = 0;
|
121 |
-
setStorage(
|
|
|
|
|
|
|
|
|
|
|
122 |
|
123 |
} catch (error) {
|
124 |
-
const data = getStorage(
|
125 |
if (!data) return;
|
126 |
|
127 |
if (error.name !== 'CanceledError' &&
|
128 |
error.code !== 'ERR_CANCELED') {
|
129 |
data.error = 'The server is currently busy. ' +
|
130 |
'Please try again later.';
|
|
|
|
|
|
|
|
|
|
|
131 |
}
|
132 |
|
133 |
data.isGenerating = false;
|
134 |
data.progress = 0;
|
135 |
-
setStorage(
|
136 |
|
137 |
} finally {
|
138 |
clearInterval(progressInterval);
|
139 |
-
deleteActiveGeneration(
|
140 |
}
|
141 |
}, config.generation.startDelay);
|
142 |
};
|
143 |
|
144 |
-
export const cancelGeneration = (
|
145 |
-
const controller = getActiveGeneration(
|
146 |
if (controller) {
|
147 |
controller.abort();
|
148 |
-
deleteActiveGeneration(
|
149 |
|
150 |
-
const data = getStorage(
|
151 |
if (data) {
|
152 |
data.isGenerating = false;
|
153 |
data.progress = 0;
|
154 |
-
setStorage(
|
155 |
}
|
156 |
}
|
157 |
};
|
|
|
13 |
setActiveGeneration,
|
14 |
deleteActiveGeneration
|
15 |
} from './storageManager.js';
|
16 |
+
import {
|
17 |
+
sendToSession
|
18 |
+
} from './websocketManager.js';
|
19 |
|
20 |
+
const updateProgress = (sessionId, progress) => {
|
21 |
+
const data = getStorage(sessionId);
|
22 |
if (data) {
|
23 |
data.progress = progress;
|
24 |
+
setStorage(sessionId, data);
|
25 |
+
|
26 |
+
sendToSession(sessionId, {
|
27 |
+
type: 'progressUpdate',
|
28 |
+
progress
|
29 |
+
});
|
30 |
}
|
31 |
};
|
32 |
|
33 |
+
const createProgressUpdater = (sessionId) => {
|
34 |
return setInterval(() => {
|
35 |
+
const data = getStorage(sessionId);
|
36 |
if (data &&
|
37 |
data.isGenerating &&
|
38 |
data.progress < config.generation.maxProgress) {
|
|
|
41 |
config.generation.maxProgress,
|
42 |
data.progress + increment
|
43 |
);
|
44 |
+
updateProgress(sessionId, newProgress);
|
45 |
}
|
46 |
}, config.generation.progressInterval);
|
47 |
};
|
|
|
88 |
};
|
89 |
|
90 |
export const generateImage = async (
|
91 |
+
sessionId,
|
92 |
prompt,
|
93 |
model,
|
94 |
size
|
95 |
) => {
|
96 |
const controller = new AbortController();
|
97 |
+
setActiveGeneration(sessionId, controller);
|
98 |
|
99 |
+
const progressInterval = createProgressUpdater(sessionId);
|
100 |
|
101 |
setTimeout(async () => {
|
102 |
try {
|
|
|
107 |
controller.signal
|
108 |
);
|
109 |
|
110 |
+
const data = getStorage(sessionId);
|
111 |
if (!data) return;
|
112 |
|
113 |
+
updateProgress(sessionId, 100);
|
114 |
|
115 |
if (response.data?.data?.length > 0) {
|
116 |
const base64 = response.data.data[0].b64_json;
|
|
|
126 |
|
127 |
data.isGenerating = false;
|
128 |
data.progress = 0;
|
129 |
+
setStorage(sessionId, data);
|
130 |
+
|
131 |
+
sendToSession(sessionId, {
|
132 |
+
type: 'generationComplete',
|
133 |
+
images: data.images
|
134 |
+
});
|
135 |
|
136 |
} catch (error) {
|
137 |
+
const data = getStorage(sessionId);
|
138 |
if (!data) return;
|
139 |
|
140 |
if (error.name !== 'CanceledError' &&
|
141 |
error.code !== 'ERR_CANCELED') {
|
142 |
data.error = 'The server is currently busy. ' +
|
143 |
'Please try again later.';
|
144 |
+
|
145 |
+
sendToSession(sessionId, {
|
146 |
+
type: 'generationError',
|
147 |
+
error: data.error
|
148 |
+
});
|
149 |
}
|
150 |
|
151 |
data.isGenerating = false;
|
152 |
data.progress = 0;
|
153 |
+
setStorage(sessionId, data);
|
154 |
|
155 |
} finally {
|
156 |
clearInterval(progressInterval);
|
157 |
+
deleteActiveGeneration(sessionId);
|
158 |
}
|
159 |
}, config.generation.startDelay);
|
160 |
};
|
161 |
|
162 |
+
export const cancelGeneration = (sessionId) => {
|
163 |
+
const controller = getActiveGeneration(sessionId);
|
164 |
if (controller) {
|
165 |
controller.abort();
|
166 |
+
deleteActiveGeneration(sessionId);
|
167 |
|
168 |
+
const data = getStorage(sessionId);
|
169 |
if (data) {
|
170 |
data.isGenerating = false;
|
171 |
data.progress = 0;
|
172 |
+
setStorage(sessionId, data);
|
173 |
}
|
174 |
}
|
175 |
};
|
src/services/storageManager.js
CHANGED
@@ -8,37 +8,37 @@ import config from '../../config.js';
|
|
8 |
const tempStorage = new Map();
|
9 |
const activeGenerations = new Map();
|
10 |
|
11 |
-
export const getStorage = (
|
12 |
-
const data = tempStorage.get(
|
13 |
if (data) {
|
14 |
data.lastAccess = Date.now();
|
15 |
}
|
16 |
return data;
|
17 |
};
|
18 |
|
19 |
-
export const setStorage = (
|
20 |
data.lastAccess = Date.now();
|
21 |
-
tempStorage.set(
|
22 |
};
|
23 |
|
24 |
-
export const deleteStorage = (
|
25 |
-
tempStorage.delete(
|
26 |
-
activeGenerations.delete(
|
27 |
};
|
28 |
|
29 |
-
export const getActiveGeneration = (
|
30 |
-
return activeGenerations.get(
|
31 |
};
|
32 |
|
33 |
export const setActiveGeneration = (
|
34 |
-
|
35 |
controller
|
36 |
) => {
|
37 |
-
activeGenerations.set(
|
38 |
};
|
39 |
|
40 |
-
export const deleteActiveGeneration = (
|
41 |
-
activeGenerations.delete(
|
42 |
};
|
43 |
|
44 |
export const getDefaultData = () => ({
|
|
|
8 |
const tempStorage = new Map();
|
9 |
const activeGenerations = new Map();
|
10 |
|
11 |
+
export const getStorage = (sessionId) => {
|
12 |
+
const data = tempStorage.get(sessionId);
|
13 |
if (data) {
|
14 |
data.lastAccess = Date.now();
|
15 |
}
|
16 |
return data;
|
17 |
};
|
18 |
|
19 |
+
export const setStorage = (sessionId, data) => {
|
20 |
data.lastAccess = Date.now();
|
21 |
+
tempStorage.set(sessionId, data);
|
22 |
};
|
23 |
|
24 |
+
export const deleteStorage = (sessionId) => {
|
25 |
+
tempStorage.delete(sessionId);
|
26 |
+
activeGenerations.delete(sessionId);
|
27 |
};
|
28 |
|
29 |
+
export const getActiveGeneration = (sessionId) => {
|
30 |
+
return activeGenerations.get(sessionId);
|
31 |
};
|
32 |
|
33 |
export const setActiveGeneration = (
|
34 |
+
sessionId,
|
35 |
controller
|
36 |
) => {
|
37 |
+
activeGenerations.set(sessionId, controller);
|
38 |
};
|
39 |
|
40 |
+
export const deleteActiveGeneration = (sessionId) => {
|
41 |
+
activeGenerations.delete(sessionId);
|
42 |
};
|
43 |
|
44 |
export const getDefaultData = () => ({
|
src/services/websocketManager.js
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
//
|
2 |
+
// SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
// SPDX-License-Identifier: Apache-2.0
|
4 |
+
//
|
5 |
+
|
6 |
+
import config from '../../config.js';
|
7 |
+
import { generateId } from '../utils/idGenerator.js';
|
8 |
+
|
9 |
+
const clients = new Map();
|
10 |
+
|
11 |
+
export const setupWebSocket = (wss) => {
|
12 |
+
wss.on('connection', (ws) => {
|
13 |
+
const clientId = generateId();
|
14 |
+
let clientSessionId = null;
|
15 |
+
|
16 |
+
ws.clientId = clientId;
|
17 |
+
ws.isAlive = true;
|
18 |
+
|
19 |
+
ws.on('pong', () => {
|
20 |
+
ws.isAlive = true;
|
21 |
+
});
|
22 |
+
|
23 |
+
ws.on('message', (data) => {
|
24 |
+
try {
|
25 |
+
const message = JSON.parse(data.toString());
|
26 |
+
|
27 |
+
if (message.type === 'register' && message.sessionId) {
|
28 |
+
clientSessionId = message.sessionId;
|
29 |
+
|
30 |
+
if (clients.has(clientSessionId)) {
|
31 |
+
const oldClient = clients.get(clientSessionId);
|
32 |
+
if (oldClient && oldClient.readyState === 1) {
|
33 |
+
oldClient.close();
|
34 |
+
}
|
35 |
+
}
|
36 |
+
|
37 |
+
clients.set(clientSessionId, ws);
|
38 |
+
ws.sessionId = clientSessionId;
|
39 |
+
|
40 |
+
ws.send(JSON.stringify({
|
41 |
+
type: 'registered',
|
42 |
+
sessionId: clientSessionId
|
43 |
+
}));
|
44 |
+
}
|
45 |
+
|
46 |
+
handleWebSocketMessage(ws, message);
|
47 |
+
} catch (error) {}
|
48 |
+
});
|
49 |
+
|
50 |
+
ws.on('close', () => {
|
51 |
+
if (clientSessionId) {
|
52 |
+
clients.delete(clientSessionId);
|
53 |
+
}
|
54 |
+
});
|
55 |
+
|
56 |
+
ws.on('error', () => {
|
57 |
+
if (clientSessionId) {
|
58 |
+
clients.delete(clientSessionId);
|
59 |
+
}
|
60 |
+
});
|
61 |
+
|
62 |
+
ws.send(JSON.stringify({
|
63 |
+
type: 'connected',
|
64 |
+
clientId
|
65 |
+
}));
|
66 |
+
});
|
67 |
+
|
68 |
+
const interval = setInterval(() => {
|
69 |
+
wss.clients.forEach((ws) => {
|
70 |
+
if (ws.isAlive === false) {
|
71 |
+
return ws.terminate();
|
72 |
+
}
|
73 |
+
ws.isAlive = false;
|
74 |
+
ws.ping();
|
75 |
+
});
|
76 |
+
}, config.websocket.heartbeat);
|
77 |
+
|
78 |
+
wss.on('close', () => {
|
79 |
+
clearInterval(interval);
|
80 |
+
});
|
81 |
+
};
|
82 |
+
|
83 |
+
const handleWebSocketMessage = (ws, message) => {
|
84 |
+
if (message.type === 'ping') {
|
85 |
+
ws.send(JSON.stringify({ type: 'pong' }));
|
86 |
+
}
|
87 |
+
};
|
88 |
+
|
89 |
+
export const sendToSession = (sessionId, data) => {
|
90 |
+
const client = clients.get(sessionId);
|
91 |
+
if (client && client.readyState === 1) {
|
92 |
+
client.send(JSON.stringify({
|
93 |
+
...data,
|
94 |
+
sessionId
|
95 |
+
}));
|
96 |
+
}
|
97 |
+
};
|
98 |
+
|
99 |
+
export const broadcastToAll = (data) => {
|
100 |
+
clients.forEach((client, sessionId) => {
|
101 |
+
if (client.readyState === 1) {
|
102 |
+
client.send(JSON.stringify({
|
103 |
+
...data,
|
104 |
+
sessionId
|
105 |
+
}));
|
106 |
+
}
|
107 |
+
});
|
108 |
+
};
|