hadadrjt commited on
Commit
c34c995
·
1 Parent(s): 9cfa73b

image: Migrate to Node.js.

Browse files

Signed-off-by: Hadad <[email protected]>

.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules/
2
+ npm-debug.log*
3
+ yarn-debug.log*
4
+ yarn-error.log*
5
+ .DS_Store
6
+ *.log
7
+ logs/
8
+ *.pid
9
+ *.seed
10
+ *.pid.lock
11
+ .npm
12
+ .eslintcache
13
+ .node_repl_history
14
+ .yarn-integrity
15
+ dist/
16
+ build/
17
+ .vscode/
18
+ .idea/
19
+ *.swp
20
+ *.swo
21
+ *~
22
+ .cache/
23
+ tmp/
24
+ temp/
Dockerfile CHANGED
@@ -3,20 +3,14 @@
3
  # SPDX-License-Identifier: Apache-2.0
4
  #
5
 
6
- # Use a specific container image for the app
7
- FROM python:latest
8
 
9
- # Set the main working directory inside the container
10
  WORKDIR /app
11
 
12
- # Copy all files into the container
13
  COPY . .
14
 
15
- # Install all dependencies
16
- RUN pip install -r requirements.txt
17
 
18
- # Open the port so the app can be accessed
19
- EXPOSE 7860
20
 
21
- # Start the app
22
- CMD ["python", "app.py"]
 
3
  # SPDX-License-Identifier: Apache-2.0
4
  #
5
 
6
+ FROM node:latest
 
7
 
 
8
  WORKDIR /app
9
 
 
10
  COPY . .
11
 
12
+ RUN npm install
 
13
 
14
+ EXPOSE 3000
 
15
 
16
+ CMD ["npm", "start"]
 
README.md CHANGED
@@ -6,8 +6,8 @@ emoji: ⚡
6
  colorFrom: indigo
7
  colorTo: purple
8
  sdk: docker
9
- app_port: 7860
10
- pinned: false
11
  # Used to promote this Hugging Face Space
12
  models:
13
  - hadadrjt/JARVIS
@@ -81,6 +81,9 @@ models:
81
  - facebook/MobileLLM-R1-950M
82
  - Alibaba-NLP/Tongyi-DeepResearch-30B-A3B
83
  - openbmb/VoxCPM-0.5B
 
 
 
84
  # Used to promote this Hugging Face Space
85
  datasets:
86
  - fka/awesome-chatgpt-prompts
 
6
  colorFrom: indigo
7
  colorTo: purple
8
  sdk: docker
9
+ app_port: 3000
10
+ pinned: true
11
  # Used to promote this Hugging Face Space
12
  models:
13
  - hadadrjt/JARVIS
 
81
  - facebook/MobileLLM-R1-950M
82
  - Alibaba-NLP/Tongyi-DeepResearch-30B-A3B
83
  - openbmb/VoxCPM-0.5B
84
+ - Wan-AI/Wan2.2-Animate-14B
85
+ - decart-ai/Lucy-Edit-Dev
86
+ - ibm-granite/granite-docling-258M
87
  # Used to promote this Hugging Face Space
88
  datasets:
89
  - fka/awesome-chatgpt-prompts
app.py DELETED
@@ -1,84 +0,0 @@
1
- #
2
- # SPDX-FileCopyrightText: Hadad <[email protected]>
3
- # SPDX-License-Identifier: Apache-2.0
4
- #
5
-
6
- import os
7
- from openai import OpenAI, OpenAIError
8
- import base64
9
- from io import BytesIO
10
- from PIL import Image
11
- from config import MODEL, SIZE, DESCRIPTION, EXAMPLES
12
- import gradio as gr
13
-
14
- def playground(model, size, prompt, mode="run"):
15
- if not model or not size or not prompt or not prompt.strip():
16
- return gr.update(interactive=False) if mode == "helper" else None
17
- if mode == "helper":
18
- return gr.update(interactive=True)
19
-
20
- try:
21
- response = OpenAI(
22
- base_url=os.getenv("OPENAI_API_BASE_URL"),
23
- api_key=os.getenv("OPENAI_API_KEY")
24
- ).images.generate(
25
- model=model,
26
- prompt=prompt,
27
- size=size,
28
- n=1,
29
- response_format="b64_json"
30
- )
31
-
32
- except OpenAIError:
33
- raise gr.Error(
34
- "The server is currently busy. Please try again later, "
35
- "or consider switching to another model, "
36
- "as each model has a different inference speed."
37
- )
38
-
39
- return Image.open(
40
- BytesIO(
41
- base64.b64decode(
42
- response.data[0].b64_json
43
- )
44
- )
45
- )
46
-
47
- with gr.Blocks(theme=gr.themes.Soft()) as app:
48
- with gr.Sidebar(): gr.HTML(DESCRIPTION)
49
- image = gr.Image(label="GENERATED IMAGE", type="pil")
50
- model = gr.Dropdown(MODEL, label="MODEL")
51
- size = gr.Dropdown(SIZE, label="IMAGE SIZE")
52
- input = gr.Textbox(
53
- label="INSTRUCTIONS",
54
- placeholder="Insert your prompt here...",
55
- lines=2
56
- )
57
- submit = gr.Button("Generate", interactive=False)
58
- [
59
- (component.change if hasattr(component, "change") else component)(
60
- (lambda model, size, prompt:
61
- playground(model, size, prompt, "helper"))
62
- if hasattr(component, "change") else playground,
63
- [model, size, input],
64
- [submit] if hasattr(component, "change") else [image]
65
- )
66
- for component in [
67
- model,
68
- size,
69
- input,
70
- submit.click,
71
- input.submit
72
- ]
73
- ]
74
- gr.Examples(EXAMPLES, inputs=[model, size, input], outputs=[image],
75
- fn=playground, cache_examples=False, run_on_click=True)
76
-
77
- app.queue(
78
- max_size=3,
79
- default_concurrency_limit=3
80
- ).launch(
81
- show_api=False,
82
- server_name="0.0.0.0",
83
- pwa=True
84
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/css/styles.css ADDED
@@ -0,0 +1,523 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ * {
7
+ margin: 0;
8
+ padding: 0;
9
+ box-sizing: border-box;
10
+ -webkit-font-smoothing: antialiased;
11
+ -moz-osx-font-smoothing: grayscale;
12
+ }
13
+
14
+ :root {
15
+ --bg-primary: #0d0d0d;
16
+ --bg-secondary: #151515;
17
+ --bg-tertiary: #1c1c1c;
18
+ --bg-card: #181818;
19
+ --border: rgba(255, 255, 255, 0.05);
20
+ --border-hover: rgba(255, 255, 255, 0.1);
21
+ --text-primary: #fafafa;
22
+ --text-secondary: #9ca3af;
23
+ --text-muted: #6b7280;
24
+ --accent: #7c3aed;
25
+ --accent-hover: #6d28d9;
26
+ --danger: #ef4444;
27
+ }
28
+
29
+ body {
30
+ font-family: 'Inter', system-ui, sans-serif;
31
+ background: var(--bg-primary);
32
+ color: var(--text-primary);
33
+ min-height: 100vh;
34
+ line-height: 1.6;
35
+ overflow-x: hidden;
36
+ word-wrap: break-word;
37
+ word-break: break-word;
38
+ }
39
+
40
+ .gradient-bg {
41
+ background:
42
+ radial-gradient(
43
+ ellipse at top left,
44
+ rgba(124, 58, 237, 0.03) 0%,
45
+ transparent 50%
46
+ ),
47
+ radial-gradient(
48
+ ellipse at bottom right,
49
+ rgba(236, 72, 153, 0.03) 0%,
50
+ transparent 50%
51
+ );
52
+ position: fixed;
53
+ inset: 0;
54
+ z-index: -1;
55
+ }
56
+
57
+ .gradient-mesh {
58
+ background-image:
59
+ radial-gradient(
60
+ at 20% 40%,
61
+ hsla(280, 70%, 40%, 0.05) 0px,
62
+ transparent 50%
63
+ ),
64
+ radial-gradient(
65
+ at 80% 20%,
66
+ hsla(320, 70%, 40%, 0.04) 0px,
67
+ transparent 50%
68
+ ),
69
+ radial-gradient(
70
+ at 40% 80%,
71
+ hsla(200, 70%, 40%, 0.03) 0px,
72
+ transparent 50%
73
+ );
74
+ position: fixed;
75
+ inset: 0;
76
+ z-index: -1;
77
+ opacity: 0.8;
78
+ animation: meshFloat 100s ease-in-out infinite;
79
+ }
80
+
81
+ @keyframes meshFloat {
82
+ 0%, 100% {
83
+ transform: translate(0, 0) scale(1) rotate(0deg);
84
+ }
85
+ 33% {
86
+ transform: translate(-30px, -40px) scale(1.02) rotate(1deg);
87
+ }
88
+ 66% {
89
+ transform: translate(30px, -30px) scale(0.98) rotate(-1deg);
90
+ }
91
+ }
92
+
93
+ .card {
94
+ background: rgba(24, 24, 24, 0.4);
95
+ backdrop-filter: blur(60px);
96
+ border: 1px solid var(--border);
97
+ border-radius: clamp(16px, 2vw, 24px);
98
+ transition: all 0.8s cubic-bezier(0.23, 1, 0.320, 1);
99
+ position: relative;
100
+ overflow: hidden;
101
+ }
102
+
103
+ .card::before {
104
+ content: '';
105
+ position: absolute;
106
+ inset: 0;
107
+ background: radial-gradient(
108
+ 800px circle at var(--mouse-x) var(--mouse-y),
109
+ rgba(124, 58, 237, 0.02),
110
+ transparent 40%
111
+ );
112
+ opacity: 0;
113
+ transition: opacity 0.3s;
114
+ pointer-events: none;
115
+ }
116
+
117
+ .card:hover::before {
118
+ opacity: 1;
119
+ }
120
+
121
+ .card:hover {
122
+ border-color: var(--border-hover);
123
+ transform: translateY(-2px);
124
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
125
+ }
126
+
127
+ .btn {
128
+ border-radius: clamp(12px, 1.5vw, 16px);
129
+ font-weight: 600;
130
+ transition: all 0.5s cubic-bezier(0.23, 1, 0.320, 1);
131
+ position: relative;
132
+ overflow: hidden;
133
+ display: inline-flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ gap: clamp(8px, 1vw, 12px);
137
+ border: none;
138
+ cursor: pointer;
139
+ z-index: 1;
140
+ font-size: clamp(14px, 1.5vw, 16px);
141
+ }
142
+
143
+ .btn::after {
144
+ content: '';
145
+ position: absolute;
146
+ inset: 0;
147
+ background: linear-gradient(
148
+ 45deg,
149
+ transparent 30%,
150
+ rgba(255, 255, 255, 0.05) 50%,
151
+ transparent 70%
152
+ );
153
+ transform: translateX(-100%) skewX(-20deg);
154
+ transition: transform 1.5s;
155
+ }
156
+
157
+ .btn:hover::after {
158
+ transform: translateX(200%) skewX(-20deg);
159
+ }
160
+
161
+ .btn-primary {
162
+ background: linear-gradient(
163
+ 135deg,
164
+ var(--accent),
165
+ var(--accent-hover)
166
+ );
167
+ color: white;
168
+ padding: clamp(14px, 2vw, 20px) clamp(28px, 4vw, 48px);
169
+ box-shadow: 0 8px 32px rgba(124, 58, 237, 0.2);
170
+ }
171
+
172
+ .btn-primary:hover:not(:disabled) {
173
+ transform: translateY(-3px) scale(1.02);
174
+ box-shadow: 0 16px 40px rgba(124, 58, 237, 0.3);
175
+ }
176
+
177
+ .btn-primary:disabled {
178
+ opacity: 0.5;
179
+ cursor: not-allowed;
180
+ transform: none !important;
181
+ }
182
+
183
+ .btn-danger {
184
+ background: linear-gradient(135deg, var(--danger), #dc2626);
185
+ color: white;
186
+ padding: clamp(14px, 2vw, 20px) clamp(28px, 4vw, 48px);
187
+ }
188
+
189
+ .modal-overlay {
190
+ position: fixed;
191
+ inset: 0;
192
+ background: rgba(0, 0, 0, 0.96);
193
+ backdrop-filter: blur(40px);
194
+ display: flex;
195
+ align-items: center;
196
+ justify-content: center;
197
+ z-index: 10000;
198
+ animation: fadeIn 0.4s ease;
199
+ padding: clamp(16px, 2vw, 24px);
200
+ }
201
+
202
+ .modal-content {
203
+ background: linear-gradient(
204
+ 135deg,
205
+ rgba(28, 28, 28, 0.98),
206
+ rgba(24, 24, 24, 0.98)
207
+ );
208
+ border-radius: clamp(20px, 2.5vw, 32px);
209
+ padding: clamp(24px, 4vw, 48px);
210
+ max-width: min(90%, 720px);
211
+ width: 100%;
212
+ border: 1px solid rgba(255, 255, 255, 0.05);
213
+ box-shadow: 0 40px 100px rgba(0, 0, 0, 0.8);
214
+ animation: slideUp 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
215
+ position: relative;
216
+ overflow: hidden;
217
+ word-wrap: break-word;
218
+ word-break: break-word;
219
+ }
220
+
221
+ .modal-content::before {
222
+ content: '';
223
+ position: absolute;
224
+ top: -50%;
225
+ left: -50%;
226
+ width: 200%;
227
+ height: 200%;
228
+ background: conic-gradient(
229
+ from 0deg at 50% 50%,
230
+ rgba(124, 58, 237, 0.04),
231
+ rgba(236, 72, 153, 0.04),
232
+ rgba(59, 130, 246, 0.04),
233
+ rgba(124, 58, 237, 0.04)
234
+ );
235
+ animation: rotate 90s linear infinite;
236
+ }
237
+
238
+ @keyframes rotate {
239
+ from { transform: rotate(0deg); }
240
+ to { transform: rotate(360deg); }
241
+ }
242
+
243
+ @keyframes fadeIn {
244
+ from { opacity: 0; }
245
+ to { opacity: 1; }
246
+ }
247
+
248
+ @keyframes slideUp {
249
+ from {
250
+ opacity: 0;
251
+ transform: translateY(60px) scale(0.9);
252
+ }
253
+ to {
254
+ opacity: 1;
255
+ transform: translateY(0) scale(1);
256
+ }
257
+ }
258
+
259
+ .progress-bar {
260
+ width: 100%;
261
+ height: 4px;
262
+ background: rgba(124, 58, 237, 0.1);
263
+ border-radius: 2px;
264
+ overflow: hidden;
265
+ margin: 20px 0;
266
+ }
267
+
268
+ .progress-fill {
269
+ height: 100%;
270
+ background: linear-gradient(90deg, var(--accent), #ec4899);
271
+ transition: width 0.3s ease;
272
+ animation: shimmer 2s infinite;
273
+ }
274
+
275
+ @keyframes shimmer {
276
+ 0% { opacity: 0.8; }
277
+ 50% { opacity: 1; }
278
+ 100% { opacity: 0.8; }
279
+ }
280
+
281
+ .image-output-section {
282
+ min-height: clamp(120px, 15vh, 200px);
283
+ border: 2px dashed var(--border);
284
+ border-radius: clamp(20px, 2.5vw, 32px);
285
+ padding: clamp(32px, 4vw, 60px);
286
+ margin-bottom: clamp(32px, 4vw, 56px);
287
+ text-align: center;
288
+ transition: all 0.5s ease;
289
+ background: rgba(24, 24, 24, 0.2);
290
+ position: relative;
291
+ overflow: hidden;
292
+ }
293
+
294
+ .image-output-section.has-images {
295
+ border: none;
296
+ padding: 0;
297
+ background: transparent;
298
+ }
299
+
300
+ .image-grid {
301
+ display: grid;
302
+ gap: clamp(20px, 2.5vw, 36px);
303
+ grid-template-columns: repeat(
304
+ auto-fit,
305
+ minmax(
306
+ min(100%, clamp(280px, 35vw, 400px)),
307
+ 1fr
308
+ )
309
+ );
310
+ justify-content: center;
311
+ justify-items: center;
312
+ align-items: start;
313
+ }
314
+
315
+ .image-card {
316
+ position: relative;
317
+ border-radius: clamp(16px, 2vw, 24px);
318
+ overflow: hidden;
319
+ background: var(--bg-tertiary);
320
+ border: 1px solid var(--border);
321
+ transition: all 0.8s cubic-bezier(0.23, 1, 0.320, 1);
322
+ animation: scaleIn 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
323
+ width: 100%;
324
+ max-width: fit-content;
325
+ }
326
+
327
+ .image-card img {
328
+ width: 100%;
329
+ height: auto;
330
+ display: block;
331
+ object-fit: cover;
332
+ }
333
+
334
+ .image-card:hover {
335
+ transform: translateY(-8px) scale(1.02);
336
+ box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6);
337
+ border-color: rgba(124, 58, 237, 0.2);
338
+ }
339
+
340
+ @keyframes scaleIn {
341
+ from {
342
+ opacity: 0;
343
+ transform: scale(0.7) translateY(60px);
344
+ }
345
+ to {
346
+ opacity: 1;
347
+ transform: scale(1) translateY(0);
348
+ }
349
+ }
350
+
351
+ .image-actions {
352
+ position: absolute;
353
+ top: clamp(16px, 2vw, 24px);
354
+ right: clamp(16px, 2vw, 24px);
355
+ display: flex;
356
+ gap: clamp(10px, 1.5vw, 14px);
357
+ opacity: 0;
358
+ transform: translateY(-10px);
359
+ transition: all 0.3s ease;
360
+ }
361
+
362
+ .image-card:hover .image-actions {
363
+ opacity: 1;
364
+ transform: translateY(0);
365
+ }
366
+
367
+ .action-btn {
368
+ width: clamp(40px, 5vw, 52px);
369
+ height: clamp(40px, 5vw, 52px);
370
+ background: rgba(0, 0, 0, 0.85);
371
+ backdrop-filter: blur(40px);
372
+ border-radius: clamp(12px, 1.5vw, 18px);
373
+ display: flex;
374
+ align-items: center;
375
+ justify-content: center;
376
+ cursor: pointer;
377
+ transition: all 0.3s ease;
378
+ border: 1px solid rgba(255, 255, 255, 0.05);
379
+ color: white;
380
+ }
381
+
382
+ .action-btn:hover {
383
+ background: rgba(124, 58, 237, 0.2);
384
+ transform: scale(1.15) rotate(10deg);
385
+ border-color: rgba(124, 58, 237, 0.3);
386
+ }
387
+
388
+ .input-field {
389
+ background: rgba(28, 28, 28, 0.5);
390
+ border: 1px solid var(--border);
391
+ border-radius: clamp(12px, 1.5vw, 18px);
392
+ padding: clamp(16px, 2vw, 22px) clamp(20px, 2.5vw, 28px);
393
+ color: var(--text-primary);
394
+ width: 100%;
395
+ font-size: clamp(14px, 1.5vw, 16px);
396
+ transition: all 0.3s ease;
397
+ outline: none;
398
+ }
399
+
400
+ .input-field:focus {
401
+ border-color: rgba(124, 58, 237, 0.3);
402
+ box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.06);
403
+ background: rgba(28, 28, 28, 0.7);
404
+ transform: translateY(-1px);
405
+ }
406
+
407
+ .select-field {
408
+ appearance: none;
409
+ background-image: url(
410
+ "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill='%236b7280' d='M8 11L3 6h10z'/%3E%3C/svg%3E"
411
+ );
412
+ background-repeat: no-repeat;
413
+ background-position: right clamp(20px, 2.5vw, 28px) center;
414
+ padding-right: clamp(48px, 6vw, 64px);
415
+ cursor: pointer;
416
+ }
417
+
418
+ .textarea-field {
419
+ resize: vertical;
420
+ min-height: clamp(140px, 20vh, 200px);
421
+ font-family: 'JetBrains Mono', monospace;
422
+ line-height: 1.7;
423
+ word-wrap: break-word;
424
+ word-break: break-word;
425
+ white-space: pre-wrap;
426
+ overflow-wrap: break-word;
427
+ }
428
+
429
+ .example-card {
430
+ background: rgba(28, 28, 28, 0.3);
431
+ border: 1px solid var(--border);
432
+ border-radius: clamp(14px, 1.8vw, 20px);
433
+ padding: clamp(20px, 2.5vw, 32px);
434
+ cursor: pointer;
435
+ transition: all 0.5s cubic-bezier(0.23, 1, 0.320, 1);
436
+ position: relative;
437
+ overflow: hidden;
438
+ word-wrap: break-word;
439
+ word-break: break-word;
440
+ }
441
+
442
+ .example-card p {
443
+ word-wrap: break-word;
444
+ word-break: break-word;
445
+ overflow-wrap: break-word;
446
+ hyphens: auto;
447
+ }
448
+
449
+ .example-card::before {
450
+ content: '';
451
+ position: absolute;
452
+ inset: 0;
453
+ background: linear-gradient(
454
+ 135deg,
455
+ rgba(124, 58, 237, 0.05),
456
+ rgba(236, 72, 153, 0.05)
457
+ );
458
+ opacity: 0;
459
+ transition: opacity 0.5s;
460
+ }
461
+
462
+ .example-card:hover::before {
463
+ opacity: 1;
464
+ }
465
+
466
+ .example-card:hover {
467
+ transform: translateX(12px) scale(1.02);
468
+ border-color: rgba(124, 58, 237, 0.25);
469
+ box-shadow: 0 16px 60px rgba(0, 0, 0, 0.3);
470
+ }
471
+
472
+ .loading-spinner {
473
+ width: clamp(24px, 3vw, 32px);
474
+ height: clamp(24px, 3vw, 32px);
475
+ border: 3px solid rgba(255, 255, 255, 0.1);
476
+ border-top-color: white;
477
+ border-radius: 50%;
478
+ animation: spin 0.8s linear infinite;
479
+ }
480
+
481
+ @keyframes spin {
482
+ to { transform: rotate(360deg); }
483
+ }
484
+
485
+ .header-gradient {
486
+ background: linear-gradient(135deg, #7c3aed, #ec4899, #3b82f6);
487
+ -webkit-background-clip: text;
488
+ -webkit-text-fill-color: transparent;
489
+ background-clip: text;
490
+ font-weight: 900;
491
+ background-size: 200% 200%;
492
+ animation: gradientShift 12s ease infinite;
493
+ }
494
+
495
+ @keyframes gradientShift {
496
+ 0%, 100% {
497
+ background-position: 0% 50%;
498
+ }
499
+ 50% {
500
+ background-position: 100% 50%;
501
+ }
502
+ }
503
+
504
+ @media (max-width: 144px) {
505
+ body {
506
+ font-size: 10px;
507
+ }
508
+ .modal-content {
509
+ padding: 12px;
510
+ }
511
+ }
512
+
513
+ @media (min-width: 3840px) {
514
+ body {
515
+ font-size: 22px;
516
+ }
517
+ }
518
+
519
+ @media (min-width: 7680px) {
520
+ body {
521
+ font-size: 36px;
522
+ }
523
+ }
assets/css/webLoader.css ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ .modal-overlay {
7
+ position: fixed;
8
+ top: 0;
9
+ left: 0;
10
+ right: 0;
11
+ bottom: 0;
12
+ background: rgba(0, 0, 0, 0.8);
13
+ backdrop-filter: blur(10px);
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: center;
17
+ z-index: 9999;
18
+ padding: 20px;
19
+ }
20
+
21
+ .modal-content {
22
+ background: var(--card-bg);
23
+ border-radius: 24px;
24
+ padding: clamp(32px, 4vw, 48px);
25
+ max-width: min(90%, 600px);
26
+ width: 100%;
27
+ position: relative;
28
+ overflow: hidden;
29
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
30
+ }
31
+
32
+ .modal-content::before {
33
+ content: '';
34
+ position: absolute;
35
+ top: -50%;
36
+ left: -50%;
37
+ width: 200%;
38
+ height: 200%;
39
+ background: radial-gradient(
40
+ circle at center,
41
+ rgba(99, 102, 241, 0.1) 0%,
42
+ transparent 70%
43
+ );
44
+ animation: pulse 4s ease-in-out infinite;
45
+ }
46
+
47
+ .modal-inner {
48
+ position: relative;
49
+ z-index: 1;
50
+ }
51
+
52
+ .modal-title {
53
+ font-size: clamp(24px, 5vw, 48px);
54
+ margin-bottom: clamp(20px, 3vw, 40px);
55
+ text-align: center;
56
+ }
57
+
58
+ .modal-text {
59
+ color: var(--text-secondary);
60
+ line-height: 1.8;
61
+ margin-bottom: clamp(24px, 3vw, 40px);
62
+ font-size: clamp(14px, 1.8vw, 17px);
63
+ word-wrap: break-word;
64
+ word-break: break-word;
65
+ }
66
+
67
+ .modal-error-title {
68
+ font-size: clamp(20px, 3vw, 28px);
69
+ font-weight: 700;
70
+ color: var(--danger);
71
+ margin-bottom: clamp(16px, 2vw, 24px);
72
+ text-align: center;
73
+ }
74
+
75
+ .modal-error-text {
76
+ color: var(--text-secondary);
77
+ margin-bottom: clamp(20px, 3vw, 32px);
78
+ text-align: center;
79
+ font-size: clamp(14px, 1.8vw, 17px);
80
+ word-wrap: break-word;
81
+ word-break: break-word;
82
+ }
83
+
84
+ .header-main {
85
+ border-bottom: 1px solid var(--border);
86
+ padding: clamp(20px, 3vw, 32px) 0;
87
+ }
88
+
89
+ .header-title {
90
+ font-size: clamp(32px, 6vw, 64px);
91
+ }
92
+
93
+ .main-content {
94
+ padding: clamp(24px, 4vw, 48px) 16px;
95
+ max-width: min(100%, 1800px);
96
+ }
97
+
98
+ .loading-container {
99
+ padding: clamp(32px, 4vw, 60px);
100
+ }
101
+
102
+ .loading-text {
103
+ color: var(--text-secondary);
104
+ font-size: clamp(16px, 2vw, 20px);
105
+ font-weight: 600;
106
+ margin-bottom: 16px;
107
+ }
108
+
109
+ .progress-text {
110
+ color: var(--text-muted);
111
+ font-size: clamp(14px, 1.6vw, 16px);
112
+ }
113
+
114
+ .image-actions form {
115
+ margin: 0;
116
+ }
117
+
118
+ .image-info {
119
+ padding: clamp(20px, 2.5vw, 28px);
120
+ border-top: 1px solid var(--border);
121
+ }
122
+
123
+ .image-prompt {
124
+ font-size: clamp(13px, 1.5vw, 15px);
125
+ color: var(--text-muted);
126
+ overflow: hidden;
127
+ text-overflow: ellipsis;
128
+ white-space: nowrap;
129
+ margin-bottom: 8px;
130
+ font-weight: 500;
131
+ }
132
+
133
+ .image-meta {
134
+ font-size: clamp(12px, 1.4vw, 14px);
135
+ color: var(--text-muted);
136
+ }
137
+
138
+ .image-model {
139
+ font-weight: 700;
140
+ color: var(--accent);
141
+ }
142
+
143
+ .placeholder-icon {
144
+ margin: 0 auto clamp(20px, 2.5vw, 28px);
145
+ opacity: 0.15;
146
+ }
147
+
148
+ .placeholder-text {
149
+ color: var(--text-muted);
150
+ font-size: clamp(16px, 2vw, 20px);
151
+ font-weight: 500;
152
+ }
153
+
154
+ .form-section {
155
+ padding: clamp(32px, 4vw, 48px);
156
+ margin-bottom: clamp(32px, 4vw, 48px);
157
+ }
158
+
159
+ .form-grid {
160
+ margin-bottom: clamp(24px, 3vw, 36px);
161
+ }
162
+
163
+ .form-label {
164
+ display: block;
165
+ font-size: clamp(13px, 1.5vw, 15px);
166
+ font-weight: 700;
167
+ color: var(--text-secondary);
168
+ margin-bottom: clamp(10px, 1.2vw, 14px);
169
+ text-transform: uppercase;
170
+ letter-spacing: 0.05em;
171
+ }
172
+
173
+ .form-group {
174
+ margin-bottom: clamp(24px, 3vw, 36px);
175
+ }
176
+
177
+ .button-icon {
178
+ width: clamp(20px, 2.5vw, 28px);
179
+ height: clamp(20px, 2.5vw, 28px);
180
+ }
181
+
182
+ .examples-section {
183
+ padding: clamp(32px, 4vw, 48px);
184
+ }
185
+
186
+ .examples-title {
187
+ font-size: clamp(24px, 3vw, 32px);
188
+ font-weight: 800;
189
+ color: var(--text-secondary);
190
+ margin-bottom: clamp(24px, 3vw, 36px);
191
+ }
192
+
193
+ .example-text {
194
+ margin-bottom: clamp(12px, 1.5vw, 16px);
195
+ font-size: clamp(14px, 1.6vw, 16px);
196
+ line-height: 1.6;
197
+ font-weight: 500;
198
+ }
199
+
200
+ .example-meta {
201
+ font-size: clamp(12px, 1.4vw, 14px);
202
+ color: var(--text-muted);
203
+ font-weight: 700;
204
+ }
205
+
206
+ .example-model {
207
+ color: var(--accent);
208
+ }
209
+
210
+ .footer-main {
211
+ border-top: 1px solid var(--border);
212
+ padding: clamp(32px, 4vw, 48px) 0;
213
+ margin-top: clamp(80px, 10vw, 140px);
214
+ }
215
+
216
+ .footer-copyright {
217
+ font-size: clamp(13px, 1.5vw, 15px);
218
+ color: var(--text-muted);
219
+ font-weight: 500;
220
+ }
221
+
222
+ .footer-link {
223
+ display: flex;
224
+ align-items: center;
225
+ gap: clamp(10px, 1.5vw, 16px);
226
+ color: var(--text-muted);
227
+ text-decoration: none;
228
+ transition: all 0.3s;
229
+ font-size: clamp(13px, 1.5vw, 15px);
230
+ font-weight: 600;
231
+ }
232
+
233
+ .footer-link:hover {
234
+ color: var(--accent);
235
+ transform: translateX(-6px);
236
+ }
237
+
238
+ .footer-icon {
239
+ width: clamp(20px, 2.5vw, 26px);
240
+ height: clamp(20px, 2.5vw, 26px);
241
+ }
242
+
243
+ .modal-error-content {
244
+ max-width: min(90%, 480px);
245
+ }
246
+
247
+ .action-icon {
248
+ width: clamp(20px, 2.5vw, 28px);
249
+ height: clamp(20px, 2.5vw, 28px);
250
+ }
assets/plugins/imageGenerator.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ var isGenerating = false;
10
+ var requestId = '';
11
+
12
+ if (document && document.body) {
13
+ isGenerating = document.body.dataset.isGenerating === 'true';
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
+ })();
assets/plugins/webLoader.js ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ var noop = function () {};
10
+ var methods = [
11
+ 'log', 'error', 'warn', 'info', 'debug', 'trace',
12
+ 'dir', 'table', 'time', 'timeEnd', 'timeLog', 'group',
13
+ 'groupEnd', 'assert', 'count', 'countReset', 'profile',
14
+ 'profileEnd', 'clear', 'dirxml', 'exception',
15
+ 'groupCollapsed'
16
+ ];
17
+
18
+ var disableConsole = function () {
19
+ methods.forEach(function (m) {
20
+ try {
21
+ window.console[m] = noop;
22
+ } catch (e) {}
23
+ });
24
+
25
+ try {
26
+ Object.defineProperty(window, 'console', {
27
+ value: Object.freeze(Object.create(null)),
28
+ writable: false,
29
+ configurable: false
30
+ });
31
+ } catch (e) {}
32
+ };
33
+
34
+ disableConsole();
35
+
36
+ var InitWelcomeModal = function () {
37
+ try {
38
+ var hasShown = localStorage.getItem(
39
+ 'welcomeModalShown'
40
+ );
41
+ if (!hasShown) {
42
+ var modal = document.getElementById(
43
+ 'welcomeModal'
44
+ );
45
+ if (modal) {
46
+ modal.style.display = 'flex';
47
+ }
48
+ }
49
+ } catch (e) {
50
+ var modal = document.getElementById(
51
+ 'welcomeModal'
52
+ );
53
+ if (modal) {
54
+ modal.style.display = 'flex';
55
+ }
56
+ }
57
+ };
58
+
59
+ var closeWelcomeModal = function () {
60
+ var modal = document.getElementById('welcomeModal');
61
+ if (modal) {
62
+ modal.style.display = 'none';
63
+ try {
64
+ localStorage.setItem(
65
+ 'welcomeModalShown',
66
+ 'true'
67
+ );
68
+ } catch (e) {}
69
+ }
70
+ };
71
+
72
+ var closeErrorModal = function () {
73
+ var modal = document.getElementById('errorModal');
74
+ if (modal) {
75
+ modal.style.animation = 'fadeIn 0.5s ease reverse';
76
+ setTimeout(function () {
77
+ modal.remove();
78
+ }, 500);
79
+ }
80
+ };
81
+
82
+ var validateInputs = function () {
83
+ var model = document.getElementById('modelSelect');
84
+ var size = document.getElementById('sizeSelect');
85
+ var prompt = document.getElementById('promptInput');
86
+ var submitBtn = document.getElementById('submitBtn');
87
+
88
+ if (!model || !size || !prompt || !submitBtn) return;
89
+
90
+ var modelValue = model.value;
91
+ var sizeValue = size.value;
92
+ var promptValue = prompt.value
93
+ .trim()
94
+ .replace(/\s+/g, ' ')
95
+ .replace(/[\t\n\r]/g, '');
96
+
97
+ var isValid = !!modelValue &&
98
+ !!sizeValue &&
99
+ !!promptValue &&
100
+ promptValue.length > 0;
101
+
102
+ submitBtn.disabled = !isValid;
103
+ };
104
+
105
+ var triggerExample = function (prompt, model, size) {
106
+ var modelSelect = document.getElementById(
107
+ 'modelSelect'
108
+ );
109
+ var sizeSelect = document.getElementById(
110
+ 'sizeSelect'
111
+ );
112
+ var promptInput = document.getElementById(
113
+ 'promptInput'
114
+ );
115
+ var form = document.getElementById('generateForm');
116
+
117
+ if (modelSelect) modelSelect.value = model;
118
+ if (sizeSelect) sizeSelect.value = size;
119
+ if (promptInput) promptInput.value = prompt;
120
+
121
+ validateInputs();
122
+
123
+ if (form) {
124
+ setTimeout(function () {
125
+ var fa = document.getElementById('formAction');
126
+ if (fa) fa.value = 'generate';
127
+ form.submit();
128
+ }, 100);
129
+ }
130
+ };
131
+
132
+ var cancelGeneration = function () {
133
+ var form = document.getElementById('generateForm');
134
+ var fa = document.getElementById('formAction');
135
+ var cancelBtn = event && event.target
136
+ ? event.target.closest('button')
137
+ : null;
138
+
139
+ if (cancelBtn) {
140
+ cancelBtn.disabled = true;
141
+ cancelBtn.style.opacity = '0.6';
142
+ cancelBtn.style.cursor = 'not-allowed';
143
+
144
+ var btnText = cancelBtn.querySelector('span') ||
145
+ cancelBtn.childNodes[
146
+ cancelBtn.childNodes.length - 1
147
+ ];
148
+ if (btnText && btnText.nodeType === 3) {
149
+ btnText.textContent = 'Cancelling...';
150
+ } else if (!btnText) {
151
+ var textNode = Array.from(
152
+ cancelBtn.childNodes
153
+ ).find(function(node) {
154
+ return node.nodeType === 3 &&
155
+ node.textContent.trim();
156
+ });
157
+ if (textNode) {
158
+ textNode.textContent = 'Cancelling...';
159
+ }
160
+ }
161
+ }
162
+
163
+ if (fa) {
164
+ fa.value = 'cancel';
165
+
166
+ if (form) {
167
+ var hiddenCancel = document.createElement(
168
+ 'input'
169
+ );
170
+ hiddenCancel.type = 'hidden';
171
+ hiddenCancel.name = 'forceCancel';
172
+ hiddenCancel.value = 'true';
173
+ form.appendChild(hiddenCancel);
174
+
175
+ var timestamp = document.createElement('input');
176
+ timestamp.type = 'hidden';
177
+ timestamp.name = 'cancelTime';
178
+ timestamp.value = Date.now();
179
+ form.appendChild(timestamp);
180
+
181
+ try {
182
+ form.submit();
183
+ } catch (e) {
184
+ form.requestSubmit && form.requestSubmit();
185
+ }
186
+
187
+ setTimeout(function() {
188
+ if (form && fa) {
189
+ fa.value = 'cancel';
190
+ try {
191
+ form.submit();
192
+ } catch (e) {
193
+ window.location.reload();
194
+ }
195
+ }
196
+ }, 500);
197
+
198
+ setTimeout(function() {
199
+ window.location.reload();
200
+ }, 3000);
201
+ }
202
+ } else if (form) {
203
+ var newFA = document.createElement('input');
204
+ newFA.type = 'hidden';
205
+ newFA.id = 'formAction';
206
+ newFA.name = 'action';
207
+ newFA.value = 'cancel';
208
+ form.appendChild(newFA);
209
+
210
+ try {
211
+ form.submit();
212
+ } catch (e) {
213
+ window.location.reload();
214
+ }
215
+ } else {
216
+ var xhr = new XMLHttpRequest();
217
+ xhr.open('POST', window.location.href, true);
218
+ xhr.setRequestHeader('Content-Type',
219
+ 'application/x-www-form-urlencoded');
220
+ xhr.onload = function() {
221
+ window.location.reload();
222
+ };
223
+ xhr.onerror = function() {
224
+ window.location.reload();
225
+ };
226
+ xhr.send('action=cancel&forceCancel=true');
227
+ }
228
+
229
+ window.cancelRequested = true;
230
+
231
+ if (window.EventSource) {
232
+ try {
233
+ var sources = window.eventSources || [];
234
+ sources.forEach(function(source) {
235
+ if (source && source.close) {
236
+ source.close();
237
+ }
238
+ });
239
+ } catch (e) {}
240
+ }
241
+
242
+ if (window.AbortController &&
243
+ window.abortController) {
244
+ try {
245
+ window.abortController.abort();
246
+ } catch (e) {}
247
+ }
248
+
249
+ setTimeout(function() {
250
+ if (window.cancelRequested) {
251
+ window.stop && window.stop();
252
+ document.execCommand &&
253
+ document.execCommand('Stop');
254
+ }
255
+ }, 100);
256
+ };
257
+
258
+ var modelSelect = document.getElementById(
259
+ 'modelSelect'
260
+ );
261
+ var sizeSelect = document.getElementById(
262
+ 'sizeSelect'
263
+ );
264
+ var promptInput = document.getElementById(
265
+ 'promptInput'
266
+ );
267
+
268
+ if (modelSelect) {
269
+ modelSelect.addEventListener(
270
+ 'change',
271
+ validateInputs
272
+ );
273
+ }
274
+ if (sizeSelect) {
275
+ sizeSelect.addEventListener(
276
+ 'change',
277
+ validateInputs
278
+ );
279
+ }
280
+
281
+ if (promptInput) {
282
+ promptInput.addEventListener(
283
+ 'input',
284
+ validateInputs
285
+ );
286
+ promptInput.addEventListener('paste', function () {
287
+ setTimeout(validateInputs, 10);
288
+ });
289
+ promptInput.addEventListener(
290
+ 'keydown',
291
+ function (e) {
292
+ if (e.key === 'Enter' && e.ctrlKey) {
293
+ e.preventDefault();
294
+ var submitBtn = document.getElementById(
295
+ 'submitBtn'
296
+ );
297
+ if (submitBtn && !submitBtn.disabled) {
298
+ var form = document.getElementById(
299
+ 'generateForm'
300
+ );
301
+ if (form) form.submit();
302
+ }
303
+ }
304
+ }
305
+ );
306
+ }
307
+
308
+ window.closeWelcomeModal = closeWelcomeModal;
309
+ window.closeErrorModal = closeErrorModal;
310
+ window.triggerExample = triggerExample;
311
+ window.cancelGeneration = cancelGeneration;
312
+
313
+ validateInputs();
314
+
315
+ var cards = document.querySelectorAll('.card');
316
+ Array.prototype.forEach.call(cards, function (card) {
317
+ card.addEventListener('mousemove', function (e) {
318
+ var rect = card.getBoundingClientRect();
319
+ var x = e.clientX - rect.left;
320
+ var y = e.clientY - rect.top;
321
+ card.style.setProperty('--mouse-x', x + 'px');
322
+ card.style.setProperty('--mouse-y', y + 'px');
323
+ });
324
+ });
325
+
326
+ document.addEventListener('DOMContentLoaded',
327
+ function() {
328
+ InitWelcomeModal();
329
+ }
330
+ );
331
+
332
+ if (document.readyState === 'complete' ||
333
+ document.readyState === 'interactive') {
334
+ InitWelcomeModal();
335
+ }
336
+
337
+ setInterval(function () {
338
+ disableConsole();
339
+ }, 1000);
340
+ })();
config.js ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ export default {
7
+ server: {
8
+ port: 3000,
9
+ host: '0.0.0.0'
10
+ },
11
+ api: {
12
+ baseUrl: process.env.OPENAI_API_BASE_URL,
13
+ key: process.env.OPENAI_API_KEY,
14
+ timeout: 120000
15
+ },
16
+ storage: {
17
+ maxAge: 600000,
18
+ cleanupInterval: 60000
19
+ },
20
+ limits: {
21
+ bodySize: '2mb',
22
+ maxContentLength: Infinity
23
+ },
24
+ generation: {
25
+ progressInterval: 800,
26
+ startDelay: 100,
27
+ maxProgress: 90
28
+ },
29
+ paths: {
30
+ views: 'public',
31
+ mainView: 'webViewer'
32
+ }
33
+ };
config.py DELETED
@@ -1,57 +0,0 @@
1
- #
2
- # SPDX-FileCopyrightText: Hadad <[email protected]>
3
- # SPDX-License-Identifier: Apache-2.0
4
- #
5
-
6
- MODEL = [
7
- "flux", "kontext", "turbo"
8
- ]
9
-
10
- SIZE = [
11
- "256x256", "512x512", "1024x1024",
12
- "512x768", "768x1024", "1024x1536",
13
- "768x512", "1024x768", "1536x1024"
14
- ]
15
-
16
- EXAMPLES=[
17
- ["flux", "1024x1024", "A woman riding a horse in the forest"],
18
- ["kontext", "1024x1024",
19
- "A cozy cabin in the snowy mountains, warm lights glowing inside"],
20
- ["turbo", "1024x1024",
21
- "A cute cat astronaut floating in space, holding a little flag"],
22
- ["kontext", "1024x1024",
23
- "An ancient library filled with floating books and magical lights"],
24
- ["flux", "1024x1024",
25
- "Tropical beach with crystal-clear water and palm trees swaying"],
26
- ["flux", "1024x1024",
27
- "Cyberpunk street musician with neon tattoos"],
28
- ["flux", "1024x1024",
29
- "Abstract watercolor splash resembling a city skyline"],
30
- ["flux", "1024x1024",
31
- "Anatomical diagram of the human heart in textbook style"]
32
- ]
33
-
34
- DESCRIPTION = """
35
- Welcome to the <b>Image Generation Playground</b>, where imagination transforms
36
- into visual creations.
37
- <br><br>
38
-
39
- For your information, <b>image generation</b> and <b>OpenAI Audio TTS</b>
40
- is already built-in natively in the primary spaces.
41
- <br><br>
42
-
43
- For more advanced capabilities,
44
- please visit the <b><a href='https://umint-openwebui.hf.space'
45
- target='_blank'>UltimaX Intelligence</a></b> primary spaces.
46
- <br><br>
47
-
48
- Please consider reading the <b><a href=
49
- 'https://huggingface.co/spaces/umint/ai/discussions/37#68b55209c51ca52ed299db4c'
50
- target='_blank'>Terms of Use and Consequences of Violation</a></b>
51
- if you wish to proceed to the main Spaces.
52
- <br><br>
53
-
54
- <b>Like this project? Feel free to buy me a
55
- <a href='https://ko-fi.com/hadad' target='_blank'>
56
- coffee</a></b>.
57
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
package.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Image Generation Playground",
3
+ "version": "0.0.1",
4
+ "description": "Part of the UltimaX Intelligence ecosystem",
5
+ "type": "module",
6
+ "main": "server.js",
7
+ "scripts": {
8
+ "start": "node server.js"
9
+ },
10
+ "keywords": [
11
+ "Image Generation",
12
+ "playground",
13
+ "UltimaX Intelligence",
14
+ "umint",
15
+ "Pollinations.AI"
16
+ ],
17
+ "author": "Hadad Darajat",
18
+ "license": "Apache-2.0",
19
+ "dependencies": {
20
+ "axios": "latest",
21
+ "ejs": "latest",
22
+ "express": "latest"
23
+ },
24
+ "homepage": "https://umint-image.hf.space"
25
+ }
public/webViewer.ejs ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!--
2
+ SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ SPDX-License-Identifier: Apache-2.0
4
+ -->
5
+
6
+ <!DOCTYPE html>
7
+ <html lang="en">
8
+ <head>
9
+ <meta charset="UTF-8">
10
+ <meta name="viewport"
11
+ content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
12
+
13
+ <meta name="author" content="Hadad Darajat" />
14
+
15
+ <meta name="description"
16
+ content="UltimaX Intelligence is a free AI platform unifying
17
+ multiple premium AI models with an intuitive
18
+ ChatGPT-like interface. Powered by Pollinations
19
+ open-source AI and integrated with OpenWebUI,
20
+ it offers advanced features, no cost, no registration,
21
+ and ensures user privacy with temporary,
22
+ unsaved conversations." />
23
+
24
+ <meta name="keywords"
25
+ content="UltimaX Intelligence, free AI platform,
26
+ premium AI models, Pollinations AI, open-source AI,
27
+ Open-WebUI integration, ChatGPT alternative,
28
+ AI tools free, no registration AI, AI privacy,
29
+ temporary AI conversations, AI platform no login,
30
+ advanced AI features, AI community powered,
31
+ seamless AI experience" />
32
+
33
+ <meta property="og:domain" content="umint-ai.hf.space" />
34
+ <meta property="og:url" content="https://umint-ai.hf.space" />
35
+ <meta property="og:title" content="UltimaX Intelligence" />
36
+
37
+ <meta property="og:description"
38
+ content="UltimaX Intelligence is a free AI platform unifying
39
+ multiple premium AI models with an intuitive
40
+ ChatGPT-like interface. Powered by Pollinations
41
+ open-source AI and integrated with OpenWebUI,
42
+ it offers advanced features, no cost, no registration,
43
+ and ensures user privacy with temporary,
44
+ unsaved conversations." />
45
+
46
+ <meta property="og:image"
47
+ content="https://cdn-uploads.huggingface.co/production/uploads/686e28b405d4ddcdd96adeb2/i9iufR3L-rgj39mk_B9QW.jpeg" />
48
+
49
+ <meta name="twitter:card" content="summary_large_image" />
50
+ <meta name="twitter:domain" content="umint-ai.hf.space" />
51
+ <meta name="twitter:url" content="https://umint-ai.hf.space" />
52
+ <meta name="twitter:title" content="UltimaX Intelligence" />
53
+
54
+ <meta name="twitter:description"
55
+ content="UltimaX Intelligence is a free AI platform unifying
56
+ multiple premium AI models with an intuitive
57
+ ChatGPT-like interface. Powered by Pollinations
58
+ open-source AI and integrated with OpenWebUI,
59
+ it offers advanced features, no cost, no registration,
60
+ and ensures user privacy with temporary,
61
+ unsaved conversations." />
62
+
63
+ <meta name="twitter:image"
64
+ content="https://cdn-uploads.huggingface.co/production/uploads/686e28b405d4ddcdd96adeb2/i9iufR3L-rgj39mk_B9QW.jpeg" />
65
+
66
+ <link rel="icon"
67
+ href="https://umint-ai.hf.space/assets/images/favicon.ico"
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" rel="stylesheet">
74
+
75
+ <link rel="stylesheet"
76
+ href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
77
+
78
+ <link rel="stylesheet" href="/__public__/assets/css/styles.css" />
79
+ <link rel="stylesheet" href="/__public__/assets/css/webLoader.css" />
80
+ </head>
81
+ <body data-is-generating="<%= !!isGenerating %>"
82
+ data-request-id="<%= requestId || '' %>">
83
+ <div class="gradient-bg"></div>
84
+ <div class="gradient-mesh"></div>
85
+
86
+ <div id="welcomeModal" class="modal-overlay" style="display: none;">
87
+ <div class="modal-content">
88
+ <div class="modal-inner">
89
+ <h2 class="header-gradient animate__animated
90
+ animate__fadeInDown modal-title">
91
+ Hi there!
92
+ </h2>
93
+ <div class="animate__animated animate__fadeInUp modal-text">
94
+ Welcome to the <b>Image Generation Playground</b>,
95
+ where imagination transforms into visual creations.
96
+ <br><br>
97
+ For your information, <b>image generation</b> and
98
+ <b>OpenAI Audio TTS</b> is already built-in natively
99
+ in the primary spaces.
100
+ <br><br>
101
+ For more advanced capabilities, please visit the
102
+ <b><a href='https://umint-openwebui.hf.space'
103
+ target='_blank' style="color: var(--accent);
104
+ text-decoration: underline;">
105
+ UltimaX Intelligence</a></b> primary spaces.
106
+ <br><br>
107
+ Please consider reading the
108
+ <b><a href='https://huggingface.co/spaces/umint/ai/discussions/37'
109
+ target='_blank' style="color: var(--accent);
110
+ text-decoration: underline;">
111
+ Terms of Use and Consequences of Violation</a></b>
112
+ if you wish to proceed to the main Spaces.
113
+ <br><br>
114
+ <b>Like this project? Feel free to buy me a
115
+ <a href='https://ko-fi.com/hadad' target='_blank'
116
+ style="color: var(--accent);
117
+ text-decoration: underline;">coffee</a></b>.
118
+ </div>
119
+ <button onclick="closeWelcomeModal()"
120
+ class="btn btn-primary w-full animate__animated
121
+ animate__fadeInUp animate__delay-1s">
122
+ Get Started
123
+ </button>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <% if (error) { %>
129
+ <div id="errorModal" class="modal-overlay">
130
+ <div class="modal-content modal-error-content">
131
+ <div class="modal-inner">
132
+ <h3 class="modal-error-title">Error</h3>
133
+ <p class="modal-error-text"><%= error %></p>
134
+ <button onclick="closeErrorModal()"
135
+ class="btn btn-primary w-full">OK</button>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ <% } %>
140
+
141
+ <header class="header-main">
142
+ <div class="container mx-auto px-4">
143
+ <h1 class="header-gradient animate__animated
144
+ animate__fadeInDown header-title">
145
+ Image Generation Playground
146
+ </h1>
147
+ </div>
148
+ </header>
149
+
150
+ <main class="container mx-auto px-4 main-content">
151
+
152
+ <div class="image-output-section
153
+ <%= images && images.length > 0 ? 'has-images' : '' %>
154
+ animate__animated animate__fadeIn">
155
+ <% if (isGenerating) { %>
156
+ <div class="loading-container">
157
+ <div class="loading-spinner"
158
+ style="margin: 0 auto 20px;"></div>
159
+ <p class="loading-text">Generating your image...</p>
160
+ <div class="progress-bar">
161
+ <div class="progress-fill"
162
+ style="width: <%= progress %>%;"></div>
163
+ </div>
164
+ <p class="progress-text">
165
+ <%= progress.toFixed(0) %>% Complete
166
+ </p>
167
+ </div>
168
+ <% } else if (images && images.length > 0) { %>
169
+ <div class="image-grid">
170
+ <% images.forEach((image, index) => { %>
171
+ <div class="image-card">
172
+ <img src="data:image/png;base64,<%= image.base64 %>"
173
+ alt="<%= image.prompt %>">
174
+ <div class="image-actions">
175
+ <a href="data:image/png;base64,<%= image.base64 %>"
176
+ download="generated-<%= image.id %>.png"
177
+ class="action-btn">
178
+ <svg class="action-icon" viewBox="0 0 24 24"
179
+ fill="none">
180
+ <path d="M12 16L7 11L8.4 9.55L11 12.15V4H13V12.15L15.6 9.55L17 11L12 16Z"
181
+ fill="currentColor"/>
182
+ <path d="M4 20C3.45 20 2.98 19.8 2.59 19.41C2.2 19.02 2 18.55 2 18V15H4V18H20V15H22V18C22 18.55 21.8 19.02 21.41 19.41C21.02 19.8 20.55 20 20 20H4Z"
183
+ fill="currentColor"/>
184
+ </svg>
185
+ </a>
186
+ <form method="POST" action="/">
187
+ <input type="hidden" name="action" value="delete">
188
+ <input type="hidden" name="imageIndex"
189
+ value="<%= index %>">
190
+ <button type="submit" class="action-btn">
191
+ <svg class="action-icon" viewBox="0 0 24 24"
192
+ fill="none">
193
+ <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"
194
+ fill="currentColor"/>
195
+ </svg>
196
+ </button>
197
+ </form>
198
+ </div>
199
+ <div class="image-info">
200
+ <p class="image-prompt"><%= image.prompt %></p>
201
+ <p class="image-meta">
202
+ <span class="image-model">
203
+ <%= image.model.toUpperCase() %>
204
+ </span>
205
+ | <%= image.size %>
206
+ </p>
207
+ </div>
208
+ </div>
209
+ <% }); %>
210
+ </div>
211
+ <% } else { %>
212
+ <svg class="placeholder-icon" width="80" height="80"
213
+ viewBox="0 0 24 24" fill="none">
214
+ <path d="M21 3H3C2 3 1 4 1 5V19C1 20 2 21 3 21H21C22 21 23 20 23 19V5C23 4 22 3 21 3ZM21 19H3V5H21V19Z"
215
+ fill="currentColor"/>
216
+ <path d="M4.5 16.5L9 12L11.5 14.5L16 10L19.5 13.5"
217
+ stroke="currentColor" stroke-width="1.5"
218
+ stroke-linecap="round"/>
219
+ <circle cx="8" cy="8.5" r="1.5" fill="currentColor"/>
220
+ </svg>
221
+ <p class="placeholder-text">
222
+ No images generated yet. Start creating amazing visuals!
223
+ </p>
224
+ <% } %>
225
+ </div>
226
+
227
+ <section class="card animate__animated animate__fadeIn
228
+ animate__delay-1s form-section">
229
+ <form method="POST" action="/" id="generateForm">
230
+ <input type="hidden" name="action" value="generate"
231
+ id="formAction">
232
+
233
+ <div class="grid md:grid-cols-2 gap-6 form-grid">
234
+ <div>
235
+ <label class="form-label">Model</label>
236
+ <select name="model" id="modelSelect" required
237
+ class="input-field select-field"
238
+ <%= isGenerating ? 'disabled' : '' %>>
239
+ <option value="">Select a model</option>
240
+ <option value="flux">Flux</option>
241
+ <option value="kontext">Kontext</option>
242
+ <option value="turbo">Turbo</option>
243
+ <option value="nanobanana">Nano Banana</option>
244
+ </select>
245
+ </div>
246
+
247
+ <div>
248
+ <label class="form-label">Size</label>
249
+ <select name="size" id="sizeSelect" required
250
+ class="input-field select-field"
251
+ <%= isGenerating ? 'disabled' : '' %>>
252
+ <option value="">Select size</option>
253
+ <optgroup label="Square">
254
+ <option value="256x256">256×256</option>
255
+ <option value="512x512">512×512</option>
256
+ <option value="768x768">768×768</option>
257
+ <option value="1024x1024">1024×1024</option>
258
+ <option value="1536x1536">1536×1536</option>
259
+ <option value="2048x2048">2048×2048</option>
260
+ </optgroup>
261
+ <optgroup label="Portrait">
262
+ <option value="256x384">256×384</option>
263
+ <option value="384x512">384×512</option>
264
+ <option value="512x768">512×768</option>
265
+ <option value="768x1024">768×1024</option>
266
+ <option value="1024x1536">1024×1536</option>
267
+ <option value="1536x2048">1536×2048</option>
268
+ </optgroup>
269
+ <optgroup label="Landscape">
270
+ <option value="384x256">384×256</option>
271
+ <option value="512x384">512×384</option>
272
+ <option value="768x512">768×512</option>
273
+ <option value="1024x768">1024×768</option>
274
+ <option value="1536x1024">1536×1024</option>
275
+ <option value="2048x1536">2048×1536</option>
276
+ </optgroup>
277
+ </select>
278
+ </div>
279
+ </div>
280
+
281
+ <div class="form-group">
282
+ <label class="form-label">Prompt</label>
283
+ <textarea name="prompt" id="promptInput" required
284
+ placeholder="Describe the image you want to generate..."
285
+ class="input-field textarea-field"
286
+ <%= isGenerating ? 'disabled' : '' %>></textarea>
287
+ </div>
288
+
289
+ <div class="flex justify-center gap-4">
290
+ <% if (!isGenerating) { %>
291
+ <button type="submit" id="submitBtn" disabled
292
+ class="btn btn-primary">
293
+ <svg class="button-icon" viewBox="0 0 24 24"
294
+ fill="none">
295
+ <path d="M3 20V4L22 12L3 20ZM5 17L16.85 12L5 7V10.5L11 12L5 13.5V17Z"
296
+ fill="currentColor"/>
297
+ </svg>
298
+ Generate Image
299
+ </button>
300
+ <% } else { %>
301
+ <button type="button" onclick="cancelGeneration()"
302
+ class="btn btn-danger">
303
+ <svg class="button-icon" viewBox="0 0 24 24"
304
+ fill="none">
305
+ <rect x="4" y="4" width="16" height="16" rx="3"
306
+ fill="currentColor"/>
307
+ </svg>
308
+ Stop Generation
309
+ </button>
310
+ <% } %>
311
+ </div>
312
+ </form>
313
+ </section>
314
+
315
+ <section class="card animate__animated animate__fadeIn
316
+ animate__delay-1s examples-section">
317
+ <h3 class="examples-title">Example Prompts</h3>
318
+ <div class="grid lg:grid-cols-3 md:grid-cols-2 gap-4">
319
+ <div class="example-card"
320
+ onclick="triggerExample('A majestic mountain landscape at golden hour with dramatic clouds and vibrant colors reflecting on a crystal clear alpine lake', 'flux', '1536x1024')">
321
+ <p class="example-text">
322
+ A majestic mountain landscape at golden hour with
323
+ dramatic clouds and vibrant colors reflecting on a
324
+ crystal clear alpine lake
325
+ </p>
326
+ <p class="example-meta">
327
+ <span class="example-model">FLUX</span> | 1536×1024
328
+ </p>
329
+ </div>
330
+
331
+ <div class="example-card"
332
+ onclick="triggerExample('An enchanted forest with bioluminescent plants and magical floating particles, moonlight streaming through ancient mystical trees', 'kontext', '1024x1024')">
333
+ <p class="example-text">
334
+ An enchanted forest with bioluminescent plants and
335
+ magical floating particles, moonlight streaming
336
+ through ancient mystical trees
337
+ </p>
338
+ <p class="example-meta">
339
+ <span class="example-model">KONTEXT</span> | 1024×1024 | Sometimes the server is busy
340
+ </p>
341
+ </div>
342
+
343
+ <div class="example-card"
344
+ onclick="triggerExample('Underwater coral reef teeming with colorful tropical fish and sea life, sunbeams penetrating crystal clear turquoise water', 'turbo', '1024x768')">
345
+ <p class="example-text">
346
+ Underwater coral reef teeming with colorful tropical
347
+ fish and sea life, sunbeams penetrating crystal
348
+ clear turquoise water
349
+ </p>
350
+ <p class="example-meta">
351
+ <span class="example-model">TURBO</span> | 1024×768 | Sometimes the server is busy
352
+ </p>
353
+ </div>
354
+
355
+ <div class="example-card"
356
+ onclick="triggerExample('A woman riding a horse across an open field, cinematic and realistic style', 'nanobanana', '1024x768')">
357
+ <p class="example-text">
358
+ A woman riding a horse across an open field,
359
+ cinematic and realistic style
360
+ </p>
361
+ <p class="example-meta">
362
+ <span class="example-model">NANO BANANA</span> | 1024×768
363
+ </p>
364
+ </div>
365
+ </div>
366
+ </section>
367
+ </main>
368
+
369
+ <footer class="footer-main">
370
+ <div class="container mx-auto px-4 flex flex-col md:flex-row
371
+ justify-between items-center gap-6">
372
+ <div class="footer-copyright">© 2025 Hadad Darajat</div>
373
+ <a href="https://linkedin.com/in/hadadrjt" target="_blank"
374
+ class="footer-link">
375
+ <svg class="footer-icon" viewBox="0 0 24 24"
376
+ fill="currentColor">
377
+ <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
378
+ </svg>
379
+ Connect with LinkedIn
380
+ </a>
381
+ </div>
382
+ </footer>
383
+ <script defer src="/__public__/assets/plugins/imageGenerator.js"></script>
384
+ <script defer src="/__public__/assets/plugins/webLoader.js"></script>
385
+ </body>
386
+ </html>
requirements.txt DELETED
@@ -1,2 +0,0 @@
1
- gradio[oauth,mcp]
2
- openai
 
 
 
server.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ import express from 'express';
7
+ import { fileURLToPath } from 'url';
8
+ import path from 'path';
9
+ import config from './config.js';
10
+ import imageRoutes from
11
+ './src/routes/imageRoutes.js';
12
+ 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
+ "/__public__/assets",
31
+ express.static(path.resolve("assets"))
32
+ );
33
+
34
+ app.use(express.static(
35
+ path.join(__dirname, 'public')
36
+ ));
37
+
38
+ app.use('/', imageRoutes);
39
+
40
+ initCleanup();
41
+
42
+ app.listen(
43
+ config.server.port,
44
+ config.server.host
45
+ );
src/controllers/imageController.js ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import {
9
+ validateGenerationParams,
10
+ validateApiConfig,
11
+ validateImageIndex
12
+ } from '../utils/validators.js';
13
+ import {
14
+ getStorage,
15
+ setStorage,
16
+ getDefaultData
17
+ } from '../services/storageManager.js';
18
+ import {
19
+ generateImage,
20
+ cancelGeneration
21
+ } from '../services/imageGenerator.js';
22
+ import { renderView } from '../utils/viewRenderer.js';
23
+
24
+ export const renderHome = (req, res) => {
25
+ const requestId = req.query.rid || generateId();
26
+ const data = getStorage(requestId) || getDefaultData();
27
+
28
+ renderView(res, {
29
+ ...data,
30
+ requestId
31
+ });
32
+ };
33
+
34
+ export const handleAction = async (req, res) => {
35
+ const requestId = req.body.requestId || generateId();
36
+ const {
37
+ action,
38
+ prompt,
39
+ model,
40
+ size,
41
+ imageIndex
42
+ } = req.body;
43
+
44
+ let data = getStorage(requestId) || getDefaultData();
45
+
46
+ switch(action) {
47
+ case 'delete':
48
+ return handleDelete(
49
+ requestId,
50
+ imageIndex,
51
+ data,
52
+ res
53
+ );
54
+
55
+ case 'cancel':
56
+ return handleCancel(requestId, res);
57
+
58
+ case 'generate':
59
+ return handleGenerate(
60
+ requestId,
61
+ prompt,
62
+ model,
63
+ size,
64
+ data,
65
+ res
66
+ );
67
+
68
+ default:
69
+ return res.redirect(`/?rid=${requestId}`);
70
+ }
71
+ };
72
+
73
+ const handleDelete = (
74
+ requestId,
75
+ imageIndex,
76
+ data,
77
+ res
78
+ ) => {
79
+ if (validateImageIndex(
80
+ imageIndex,
81
+ data.images.length
82
+ )) {
83
+ data.images.splice(parseInt(imageIndex), 1);
84
+ setStorage(requestId, data);
85
+ }
86
+ return res.redirect(`/?rid=${requestId}`);
87
+ };
88
+
89
+ const handleCancel = (requestId, res) => {
90
+ cancelGeneration(requestId);
91
+ return res.redirect(`/?rid=${requestId}`);
92
+ };
93
+
94
+ const handleGenerate = async (
95
+ requestId,
96
+ prompt,
97
+ model,
98
+ size,
99
+ data,
100
+ res
101
+ ) => {
102
+ const trimmedPrompt = prompt?.trim() || '';
103
+
104
+ if (!validateGenerationParams(
105
+ trimmedPrompt,
106
+ model,
107
+ size
108
+ )) {
109
+ data.error = 'Please fill in all required fields';
110
+ setStorage(requestId, data);
111
+ return renderView(res, {
112
+ ...data,
113
+ requestId
114
+ });
115
+ }
116
+
117
+ if (!validateApiConfig()) {
118
+ data.error = 'The server is currently busy. ' +
119
+ 'Please try again later.';
120
+ setStorage(requestId, data);
121
+ return renderView(res, {
122
+ ...data,
123
+ requestId
124
+ });
125
+ }
126
+
127
+ data.isGenerating = true;
128
+ data.progress = 50;
129
+ data.error = null;
130
+ setStorage(requestId, data);
131
+
132
+ res.redirect(`/?rid=${requestId}`);
133
+
134
+ await generateImage(
135
+ requestId,
136
+ trimmedPrompt,
137
+ model,
138
+ size
139
+ );
140
+ };
src/middleware/viewEngine.js ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ import path from 'path';
7
+ import config from '../../config.js';
8
+
9
+ export const setupViewEngine = (app, rootDir) => {
10
+ app.set('view engine', 'ejs');
11
+ app.set('views', path.join(
12
+ rootDir,
13
+ config.paths.views
14
+ ));
15
+ };
src/routes/imageRoutes.js ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ import { Router } from 'express';
7
+ import {
8
+ renderHome,
9
+ handleAction
10
+ } from '../controllers/imageController.js';
11
+
12
+ const router = Router();
13
+
14
+ router.get('/', renderHome);
15
+ router.post('/', handleAction);
16
+
17
+ export default router;
src/services/imageGenerator.js ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ import axios from 'axios';
7
+ import config from '../../config.js';
8
+ import { generateId } from '../utils/idGenerator.js';
9
+ import {
10
+ getStorage,
11
+ setStorage,
12
+ getActiveGeneration,
13
+ setActiveGeneration,
14
+ deleteActiveGeneration
15
+ } from './storageManager.js';
16
+
17
+ const updateProgress = (requestId, progress) => {
18
+ const data = getStorage(requestId);
19
+ if (data) {
20
+ data.progress = progress;
21
+ setStorage(requestId, data);
22
+ }
23
+ };
24
+
25
+ const createProgressUpdater = (requestId) => {
26
+ return setInterval(() => {
27
+ const data = getStorage(requestId);
28
+ if (data &&
29
+ data.isGenerating &&
30
+ data.progress < config.generation.maxProgress) {
31
+ const increment = Math.random() * 20;
32
+ const newProgress = Math.min(
33
+ config.generation.maxProgress,
34
+ data.progress + increment
35
+ );
36
+ updateProgress(requestId, newProgress);
37
+ }
38
+ }, config.generation.progressInterval);
39
+ };
40
+
41
+ const createImageObject = (
42
+ base64Data,
43
+ prompt,
44
+ model,
45
+ size
46
+ ) => ({
47
+ id: generateId(),
48
+ base64: base64Data,
49
+ prompt,
50
+ model,
51
+ size
52
+ });
53
+
54
+ const callImageApi = async (
55
+ prompt,
56
+ model,
57
+ size,
58
+ signal
59
+ ) => {
60
+ return await axios.post(
61
+ config.api.baseUrl,
62
+ {
63
+ model,
64
+ prompt,
65
+ size,
66
+ response_format: 'b64_json',
67
+ n: 1
68
+ },
69
+ {
70
+ headers: {
71
+ 'Authorization': `Bearer ${config.api.key}`,
72
+ 'Content-Type': 'application/json'
73
+ },
74
+ signal,
75
+ timeout: config.api.timeout,
76
+ maxBodyLength: config.limits.maxContentLength,
77
+ maxContentLength: config.limits.maxContentLength
78
+ }
79
+ );
80
+ };
81
+
82
+ export const generateImage = async (
83
+ requestId,
84
+ prompt,
85
+ model,
86
+ size
87
+ ) => {
88
+ const controller = new AbortController();
89
+ setActiveGeneration(requestId, controller);
90
+
91
+ const progressInterval = createProgressUpdater(requestId);
92
+
93
+ setTimeout(async () => {
94
+ try {
95
+ const response = await callImageApi(
96
+ prompt,
97
+ model,
98
+ size,
99
+ controller.signal
100
+ );
101
+
102
+ const data = getStorage(requestId);
103
+ if (!data) return;
104
+
105
+ updateProgress(requestId, 100);
106
+
107
+ if (response.data?.data?.length > 0) {
108
+ const base64 = response.data.data[0].b64_json;
109
+ const newImage = createImageObject(
110
+ base64,
111
+ prompt,
112
+ model,
113
+ size
114
+ );
115
+
116
+ data.images.unshift(newImage);
117
+ }
118
+
119
+ data.isGenerating = false;
120
+ data.progress = 0;
121
+ setStorage(requestId, data);
122
+
123
+ } catch (error) {
124
+ const data = getStorage(requestId);
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(requestId, data);
136
+
137
+ } finally {
138
+ clearInterval(progressInterval);
139
+ deleteActiveGeneration(requestId);
140
+ }
141
+ }, config.generation.startDelay);
142
+ };
143
+
144
+ export const cancelGeneration = (requestId) => {
145
+ const controller = getActiveGeneration(requestId);
146
+ if (controller) {
147
+ controller.abort();
148
+ deleteActiveGeneration(requestId);
149
+
150
+ const data = getStorage(requestId);
151
+ if (data) {
152
+ data.isGenerating = false;
153
+ data.progress = 0;
154
+ setStorage(requestId, data);
155
+ }
156
+ }
157
+ };
src/services/storageManager.js ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ import config from '../../config.js';
7
+
8
+ const tempStorage = new Map();
9
+ const activeGenerations = new Map();
10
+
11
+ export const getStorage = (requestId) => {
12
+ const data = tempStorage.get(requestId);
13
+ if (data) {
14
+ data.lastAccess = Date.now();
15
+ }
16
+ return data;
17
+ };
18
+
19
+ export const setStorage = (requestId, data) => {
20
+ data.lastAccess = Date.now();
21
+ tempStorage.set(requestId, data);
22
+ };
23
+
24
+ export const deleteStorage = (requestId) => {
25
+ tempStorage.delete(requestId);
26
+ activeGenerations.delete(requestId);
27
+ };
28
+
29
+ export const getActiveGeneration = (requestId) => {
30
+ return activeGenerations.get(requestId);
31
+ };
32
+
33
+ export const setActiveGeneration = (
34
+ requestId,
35
+ controller
36
+ ) => {
37
+ activeGenerations.set(requestId, controller);
38
+ };
39
+
40
+ export const deleteActiveGeneration = (requestId) => {
41
+ activeGenerations.delete(requestId);
42
+ };
43
+
44
+ export const getDefaultData = () => ({
45
+ images: [],
46
+ isGenerating: false,
47
+ progress: 0,
48
+ error: null,
49
+ lastAccess: Date.now()
50
+ });
51
+
52
+ export const initCleanup = () => {
53
+ setInterval(() => {
54
+ const now = Date.now();
55
+ const maxAge = config.storage.maxAge;
56
+
57
+ for (const [key, value] of tempStorage.entries()) {
58
+ if (!value.lastAccess ||
59
+ now - value.lastAccess > maxAge) {
60
+ deleteStorage(key);
61
+ }
62
+ }
63
+ }, config.storage.cleanupInterval);
64
+ };
src/utils/idGenerator.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ export const generateId = () => {
7
+ return Math.random()
8
+ .toString(36)
9
+ .substring(2, 15);
10
+ };
src/utils/validators.js ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ import config from '../../config.js';
7
+
8
+ export const validatePrompt = (prompt) => {
9
+ if (!prompt) return false;
10
+
11
+ const trimmed = prompt.trim();
12
+ return trimmed &&
13
+ trimmed.replace(/\s+/g, '').length > 0;
14
+ };
15
+
16
+ export const validateGenerationParams = (
17
+ prompt,
18
+ model,
19
+ size
20
+ ) => {
21
+ return validatePrompt(prompt) &&
22
+ model &&
23
+ size;
24
+ };
25
+
26
+ export const validateApiConfig = () => {
27
+ return config.api.baseUrl &&
28
+ config.api.key;
29
+ };
30
+
31
+ export const validateImageIndex = (
32
+ index,
33
+ arrayLength
34
+ ) => {
35
+ const idx = parseInt(index);
36
+ return !isNaN(idx) &&
37
+ idx >= 0 &&
38
+ idx < arrayLength;
39
+ };
src/utils/viewRenderer.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ import config from '../../config.js';
7
+
8
+ export const renderView = (res, data) => {
9
+ res.render(config.paths.mainView, data);
10
+ };