areebsatin commited on
Commit
d928682
·
1 Parent(s): fc53afe

Major redesign: Lumina Synth design system, fix upscaling, add progress animations, disable SSR

Browse files
Files changed (2) hide show
  1. app.py +546 -90
  2. core.py +95 -58
app.py CHANGED
@@ -1,153 +1,609 @@
1
- import gradio as gr
2
 
 
3
  from core import upscale_image, upscale_video
4
 
5
- # ─────────────────────────────────────────────
6
- # Gradio UI
7
- # ─────────────────────────────────────────────
8
  CSS = """
9
- @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;700;800&display=swap');
10
 
 
11
  :root {
12
- --bg: #0a0a0f;
13
- --surface: #111118;
14
- --border: #1e1e2e;
15
- --accent: #7c3aed;
16
- --accent2: #06b6d4;
17
- --text: #e2e8f0;
18
- --muted: #64748b;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  }
20
 
 
 
 
21
  body, .gradio-container {
22
  background: var(--bg) !important;
23
- font-family: 'Syne', sans-serif !important;
24
- color: var(--text) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
26
 
27
- h1 {
28
- font-size: 2.5rem !important;
29
- font-weight: 800 !important;
30
- background: linear-gradient(135deg, #7c3aed, #06b6d4);
31
- -webkit-background-clip: text;
32
- -webkit-text-fill-color: transparent;
33
  text-align: center;
34
- padding: 1rem 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  }
36
 
37
  .tab-nav button {
38
- font-family: 'Space Mono', monospace !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  background: var(--surface) !important;
40
- border: 1px solid var(--border) !important;
41
- color: var(--muted) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  padding: 8px 20px !important;
 
 
 
 
 
43
  }
44
 
45
- .tab-nav button.selected {
46
- background: var(--accent) !important;
47
- color: white !important;
48
- border-color: var(--accent) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  }
50
 
51
- .gr-button-primary {
52
- background: linear-gradient(135deg, #7c3aed, #06b6d4) !important;
 
53
  border: none !important;
54
- font-family: 'Space Mono', monospace !important;
55
- font-weight: 700 !important;
56
- letter-spacing: 0.05em !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  }
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  footer { display: none !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  """
61
 
62
- with gr.Blocks(css=CSS, title="4K Upscaler Pro") as demo:
63
- gr.Markdown("# ⬆ 4K Upscaler Pro")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  gr.Markdown(
65
- "<p style='text-align:center; color:#64748b; font-family:Space Mono,monospace'>"
66
- "AI-powered image & video upscaling • Up to 4K resolution • REST API available"
67
- "</p>"
68
  )
 
69
 
70
  with gr.Tabs():
71
- # ── IMAGE TAB ──
72
- with gr.TabItem("🖼 Image Upscaler"):
73
- with gr.Row():
74
- with gr.Column():
75
- img_input = gr.Image(type="filepath", label="Upload Image")
 
 
 
 
 
 
76
  img_method = gr.Radio(
77
- ["Real-ESRGAN (AI)", "Lanczos (Fast)"],
78
  value="Lanczos (Fast)",
79
  label="Upscaling Method",
80
  )
81
  img_target = gr.Dropdown(
82
- [
83
- "4K (3840×2160)",
84
- "2K (2560×1440)",
85
- "1080p (1920×1080)",
86
- "2x Original",
87
- ],
88
  value="4K (3840×2160)",
89
  label="Target Resolution",
90
  )
91
- img_btn = gr.Button("⬆ Upscale Image", variant="primary")
92
- with gr.Column():
93
- img_output = gr.Image(label="Upscaled Image")
94
- img_info = gr.Textbox(label="Info", interactive=False)
95
- img_download = gr.File(label="⬇ Download PNG")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  img_btn.click(
98
- upscale_image,
99
  inputs=[img_input, img_method, img_target],
100
  outputs=[img_output, img_info, img_download],
101
  )
102
 
103
- # ── VIDEO TAB ──
104
- with gr.TabItem("🎬 Video Upscaler"):
105
- with gr.Row():
106
- with gr.Column():
107
- vid_input = gr.Video(label="Upload Video")
 
108
  vid_method = gr.Radio(
109
- ["Real-ESRGAN (AI)", "Lanczos (Fast)"],
110
  value="Lanczos (Fast)",
111
  label="Upscaling Method",
112
  )
113
  vid_target = gr.Dropdown(
114
- [
115
- "4K (3840×2160)",
116
- "2K (2560×1440)",
117
- "1080p (1920×1080)",
118
- "2x Original",
119
- ],
120
  value="4K (3840×2160)",
121
  label="Target Resolution",
122
  )
123
- vid_btn = gr.Button("⬆ Upscale Video", variant="primary")
124
- with gr.Column():
125
- vid_output = gr.Video(label="Upscaled Video")
126
- vid_info = gr.Textbox(label="Info", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
  vid_btn.click(
129
- upscale_video,
130
  inputs=[vid_input, vid_method, vid_target],
131
  outputs=[vid_output, vid_info],
132
  )
133
 
134
- # ── API DOCS TAB ──
135
- with gr.TabItem("📡 API Docs"):
136
  gr.Markdown("""
137
  ## REST API — Use from n8n, curl, or any HTTP client
138
 
139
- Hugging Face Spaces expose a built-in API automatically via the `/api/predict` endpoint.
140
 
141
  ---
142
 
143
- ### 🖼 Image Upscaling API
144
 
145
- **POST** `https://<your-username>-4k-upscaler.hf.space/api/predict`
146
 
147
  ```json
148
  {
149
  "data": [
150
- "<base64_image_string>",
151
  "Lanczos (Fast)",
152
  "4K (3840×2160)"
153
  ]
@@ -158,7 +614,7 @@ Hugging Face Spaces expose a built-in API automatically via the `/api/predict` e
158
  ```json
159
  {
160
  "data": [
161
- "<base64_upscaled_image>",
162
  "✅ Upscaled: 1280×720 → 3840×2160",
163
  "<download_url>"
164
  ]
@@ -167,17 +623,17 @@ Hugging Face Spaces expose a built-in API automatically via the `/api/predict` e
167
 
168
  ---
169
 
170
- ### 🎬 Video Upscaling API
171
 
172
- **POST** `https://<your-username>-4k-upscaler.hf.space/api/predict`
173
 
174
- Use fn_index: 1 for video endpoint.
175
 
176
  ```json
177
  {
178
  "fn_index": 1,
179
  "data": [
180
- "<video_file_url_or_base64>",
181
  "Lanczos (Fast)",
182
  "4K (3840×2160)"
183
  ]
@@ -186,18 +642,18 @@ Use fn_index: 1 for video endpoint.
186
 
187
  ---
188
 
189
- ### 🔗 n8n Integration
190
 
191
- Use the **HTTP Request** node in n8n:
192
  - **Method:** POST
193
- - **URL:** `https://<your-username>-4k-upscaler.hf.space/api/predict`
194
  - **Body:** JSON with `data` array (see above)
195
  - **Headers:** `Content-Type: application/json`
196
 
197
- For authenticated spaces, add:
198
  - **Header:** `Authorization: Bearer <your_HF_token>`
199
  """)
200
 
201
- # Launch
202
  if __name__ == "__main__":
203
- demo.queue(max_size=5).launch()
 
1
+ """4K Upscaler Pro — Premium AI-powered upscaler with Lumina Synth design."""
2
 
3
+ import gradio as gr
4
  from core import upscale_image, upscale_video
5
 
6
+ # ─────────────────────────────────────────────────────────────────────
7
+ # Lumina Synth Design System — Generated by Stitch
8
+ # ─────────────────────────────────────────────────────────────────────
9
  CSS = """
10
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap');
11
 
12
+ /* ── Design Tokens ── */
13
  :root {
14
+ --bg: #0e0e13;
15
+ --surface: #131319;
16
+ --surface-high: #1f1f26;
17
+ --surface-highest: #25252d;
18
+ --surface-bright: #2c2b33;
19
+ --primary: #ba9eff;
20
+ --primary-dim: #8455ef;
21
+ --secondary: #53ddfc;
22
+ --secondary-dim: #40ceed;
23
+ --tertiary: #ec63ff;
24
+ --on-surface: #f9f5fd;
25
+ --on-surface-variant: #acaab1;
26
+ --outline: #76747b;
27
+ --outline-variant: #48474d;
28
+ --error: #ff6e84;
29
+ --gradient-primary: linear-gradient(135deg, #8455ef, #53ddfc);
30
+ --gradient-glow: linear-gradient(135deg, rgba(132,85,239,0.4), rgba(83,221,252,0.4));
31
+ --glass-bg: rgba(19, 19, 25, 0.75);
32
+ --glass-border: rgba(72, 71, 77, 0.25);
33
+ --radius-md: 16px;
34
+ --radius-lg: 24px;
35
+ --radius-full: 9999px;
36
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
37
  }
38
 
39
+ /* ── Global Reset ── */
40
+ *, *::before, *::after { box-sizing: border-box; }
41
+
42
  body, .gradio-container {
43
  background: var(--bg) !important;
44
+ font-family: 'Inter', -apple-system, sans-serif !important;
45
+ color: var(--on-surface) !important;
46
+ min-height: 100vh;
47
+ }
48
+
49
+ .gradio-container {
50
+ max-width: 1400px !important;
51
+ margin: 0 auto !important;
52
+ padding: 0 24px !important;
53
+ }
54
+
55
+ /* ── Ambient Background Glow ── */
56
+ .gradio-container::before {
57
+ content: '';
58
+ position: fixed;
59
+ top: -50%;
60
+ left: -50%;
61
+ width: 200%;
62
+ height: 200%;
63
+ background: radial-gradient(ellipse at 30% 20%, rgba(132,85,239,0.08) 0%, transparent 50%),
64
+ radial-gradient(ellipse at 70% 80%, rgba(83,221,252,0.05) 0%, transparent 50%);
65
+ pointer-events: none;
66
+ z-index: 0;
67
  }
68
 
69
+ /* ── Hero Title ── */
70
+ .hero-title {
 
 
 
 
71
  text-align: center;
72
+ padding: 48px 0 8px;
73
+ }
74
+
75
+ .hero-title h1 {
76
+ font-family: 'Space Grotesk', sans-serif !important;
77
+ font-size: 3.2rem !important;
78
+ font-weight: 700 !important;
79
+ letter-spacing: -0.03em !important;
80
+ background: var(--gradient-primary) !important;
81
+ -webkit-background-clip: text !important;
82
+ -webkit-text-fill-color: transparent !important;
83
+ background-clip: text !important;
84
+ line-height: 1.1 !important;
85
+ margin: 0 !important;
86
+ padding: 0 !important;
87
+ }
88
+
89
+ .hero-subtitle p {
90
+ text-align: center !important;
91
+ color: var(--on-surface-variant) !important;
92
+ font-size: 1.05rem !important;
93
+ font-weight: 400 !important;
94
+ letter-spacing: 0.01em !important;
95
+ margin-top: 8px !important;
96
+ padding-bottom: 24px !important;
97
+ }
98
+
99
+ /* ── Badge ── */
100
+ .status-badge p {
101
+ display: inline-block !important;
102
+ background: rgba(132, 85, 239, 0.15) !important;
103
+ border: 1px solid rgba(186, 158, 255, 0.2) !important;
104
+ color: var(--primary) !important;
105
+ font-family: 'Inter', sans-serif !important;
106
+ font-size: 0.7rem !important;
107
+ font-weight: 600 !important;
108
+ text-transform: uppercase !important;
109
+ letter-spacing: 0.08em !important;
110
+ padding: 4px 14px !important;
111
+ border-radius: var(--radius-full) !important;
112
+ text-align: center !important;
113
+ width: auto !important;
114
+ margin: 0 auto !important;
115
+ }
116
+
117
+ /* ── Tabs — Glowing Indicators ── */
118
+ .tab-nav {
119
+ display: flex !important;
120
+ justify-content: center !important;
121
+ gap: 4px !important;
122
+ padding: 6px !important;
123
+ background: var(--surface) !important;
124
+ border-radius: var(--radius-full) !important;
125
+ border: 1px solid var(--glass-border) !important;
126
+ margin-bottom: 32px !important;
127
+ width: fit-content !important;
128
+ margin-left: auto !important;
129
+ margin-right: auto !important;
130
  }
131
 
132
  .tab-nav button {
133
+ font-family: 'Inter', sans-serif !important;
134
+ font-size: 0.875rem !important;
135
+ font-weight: 500 !important;
136
+ color: var(--on-surface-variant) !important;
137
+ background: transparent !important;
138
+ border: none !important;
139
+ padding: 10px 28px !important;
140
+ border-radius: var(--radius-full) !important;
141
+ cursor: pointer !important;
142
+ transition: var(--transition) !important;
143
+ position: relative !important;
144
+ }
145
+
146
+ .tab-nav button:hover {
147
+ color: var(--on-surface) !important;
148
+ background: rgba(132, 85, 239, 0.08) !important;
149
+ }
150
+
151
+ .tab-nav button.selected {
152
+ background: var(--gradient-primary) !important;
153
+ color: #fff !important;
154
+ font-weight: 600 !important;
155
+ box-shadow: 0 0 20px rgba(132, 85, 239, 0.4),
156
+ 0 0 60px rgba(83, 221, 252, 0.15) !important;
157
+ }
158
+
159
+ /* ── Glass Panels ── */
160
+ .glass-panel {
161
+ background: var(--glass-bg) !important;
162
+ backdrop-filter: blur(24px) !important;
163
+ -webkit-backdrop-filter: blur(24px) !important;
164
+ border-radius: var(--radius-lg) !important;
165
+ border: 1px solid var(--glass-border) !important;
166
+ padding: 28px !important;
167
+ transition: var(--transition) !important;
168
+ position: relative !important;
169
+ overflow: hidden !important;
170
+ }
171
+
172
+ .glass-panel::before {
173
+ content: '';
174
+ position: absolute;
175
+ top: 0; left: 0; right: 0;
176
+ height: 1px;
177
+ background: linear-gradient(90deg, transparent, rgba(186,158,255,0.3), transparent);
178
+ pointer-events: none;
179
+ }
180
+
181
+ .glass-panel:hover {
182
+ border-color: rgba(132, 85, 239, 0.2) !important;
183
+ box-shadow: 0 0 40px rgba(132, 85, 239, 0.06) !important;
184
+ }
185
+
186
+ /* ── Section Labels ── */
187
+ .section-label p {
188
+ font-family: 'Inter', sans-serif !important;
189
+ font-size: 0.7rem !important;
190
+ font-weight: 600 !important;
191
+ text-transform: uppercase !important;
192
+ letter-spacing: 0.1em !important;
193
+ color: var(--on-surface-variant) !important;
194
+ margin-bottom: 12px !important;
195
+ }
196
+
197
+ /* ── Input Components ── */
198
+ .gradio-image, .gradio-video {
199
+ border-radius: var(--radius-md) !important;
200
+ overflow: hidden !important;
201
+ }
202
+
203
+ .gradio-image .image-container,
204
+ .gradio-image .upload-container {
205
  background: var(--surface) !important;
206
+ border: 1.5px dashed var(--outline-variant) !important;
207
+ border-radius: var(--radius-md) !important;
208
+ min-height: 280px !important;
209
+ transition: var(--transition) !important;
210
+ }
211
+
212
+ .gradio-image .upload-container:hover {
213
+ border-color: var(--secondary) !important;
214
+ background: var(--surface-high) !important;
215
+ box-shadow: 0 0 30px rgba(83, 221, 252, 0.08) !important;
216
+ }
217
+
218
+ /* ── Radio Buttons — Pill Style ── */
219
+ .gradio-radio label {
220
+ background: var(--surface-high) !important;
221
+ border: 1px solid transparent !important;
222
+ border-radius: var(--radius-full) !important;
223
  padding: 8px 20px !important;
224
+ color: var(--on-surface-variant) !important;
225
+ font-size: 0.85rem !important;
226
+ font-weight: 500 !important;
227
+ cursor: pointer !important;
228
+ transition: var(--transition) !important;
229
  }
230
 
231
+ .gradio-radio label:hover {
232
+ background: var(--surface-bright) !important;
233
+ color: var(--on-surface) !important;
234
+ }
235
+
236
+ .gradio-radio label.selected {
237
+ background: var(--primary-dim) !important;
238
+ color: #fff !important;
239
+ border-color: var(--primary) !important;
240
+ box-shadow: 0 0 15px rgba(132, 85, 239, 0.3) !important;
241
+ }
242
+
243
+ /* ── Dropdown ── */
244
+ .gradio-dropdown {
245
+ border-radius: var(--radius-md) !important;
246
+ }
247
+
248
+ .gradio-dropdown select, .gradio-dropdown input {
249
+ background: var(--surface-high) !important;
250
+ border: 1px solid var(--outline-variant) !important;
251
+ border-radius: var(--radius-md) !important;
252
+ color: var(--on-surface) !important;
253
+ padding: 10px 16px !important;
254
+ font-size: 0.875rem !important;
255
+ transition: var(--transition) !important;
256
+ }
257
+
258
+ .gradio-dropdown select:focus, .gradio-dropdown input:focus {
259
+ border-color: var(--primary) !important;
260
+ box-shadow: 0 0 0 3px rgba(132, 85, 239, 0.15) !important;
261
  }
262
 
263
+ /* ── Primary Button — Gradient with Glow ── */
264
+ .gr-button-primary, button.primary {
265
+ background: var(--gradient-primary) !important;
266
  border: none !important;
267
+ border-radius: var(--radius-full) !important;
268
+ color: #fff !important;
269
+ font-family: 'Inter', sans-serif !important;
270
+ font-weight: 600 !important;
271
+ font-size: 0.95rem !important;
272
+ letter-spacing: 0.02em !important;
273
+ padding: 14px 32px !important;
274
+ cursor: pointer !important;
275
+ transition: var(--transition) !important;
276
+ position: relative !important;
277
+ overflow: hidden !important;
278
+ text-shadow: 0 1px 2px rgba(0,0,0,0.3) !important;
279
+ }
280
+
281
+ .gr-button-primary::before, button.primary::before {
282
+ content: '';
283
+ position: absolute;
284
+ inset: 0;
285
+ background: linear-gradient(135deg, rgba(255,255,255,0.15), transparent);
286
+ pointer-events: none;
287
+ }
288
+
289
+ .gr-button-primary:hover, button.primary:hover {
290
+ transform: translateY(-1px) !important;
291
+ box-shadow: 0 8px 30px rgba(132, 85, 239, 0.4),
292
+ 0 0 60px rgba(83, 221, 252, 0.2) !important;
293
+ }
294
+
295
+ .gr-button-primary:active, button.primary:active {
296
+ transform: translateY(0) !important;
297
  }
298
 
299
+ /* ── Secondary Button ── */
300
+ .gr-button-secondary, button.secondary {
301
+ background: transparent !important;
302
+ border: 1px solid var(--outline-variant) !important;
303
+ border-radius: var(--radius-full) !important;
304
+ color: var(--secondary) !important;
305
+ font-weight: 500 !important;
306
+ padding: 12px 24px !important;
307
+ transition: var(--transition) !important;
308
+ }
309
+
310
+ .gr-button-secondary:hover, button.secondary:hover {
311
+ border-color: var(--secondary) !important;
312
+ background: rgba(83, 221, 252, 0.08) !important;
313
+ box-shadow: 0 0 20px rgba(83, 221, 252, 0.15) !important;
314
+ }
315
+
316
+ /* ── Info Textbox ── */
317
+ .gradio-textbox textarea, .gradio-textbox input {
318
+ background: var(--surface) !important;
319
+ border: 1px solid var(--outline-variant) !important;
320
+ border-radius: var(--radius-md) !important;
321
+ color: var(--on-surface) !important;
322
+ font-family: 'Inter', monospace !important;
323
+ font-size: 0.85rem !important;
324
+ line-height: 1.6 !important;
325
+ }
326
+
327
+ /* ── File Download ── */
328
+ .gradio-file {
329
+ border-radius: var(--radius-md) !important;
330
+ }
331
+
332
+ /* ── Progress Bar — Neon Gradient ── */
333
+ .progress-bar {
334
+ background: var(--surface) !important;
335
+ border-radius: var(--radius-full) !important;
336
+ overflow: hidden !important;
337
+ }
338
+
339
+ .progress-bar > div {
340
+ background: var(--gradient-primary) !important;
341
+ box-shadow: 0 0 15px rgba(132, 85, 239, 0.5) !important;
342
+ transition: width 0.4s ease !important;
343
+ }
344
+
345
+ /* Gradio's built-in progress */
346
+ .progress-level {
347
+ background: var(--gradient-primary) !important;
348
+ }
349
+
350
+ .progress-level span {
351
+ color: #fff !important;
352
+ font-family: 'Inter', sans-serif !important;
353
+ font-weight: 500 !important;
354
+ }
355
+
356
+ .meta-text span {
357
+ color: var(--on-surface-variant) !important;
358
+ font-size: 0.85rem !important;
359
+ }
360
+
361
+ /* ── Loading Shimmer Animation ── */
362
+ @keyframes shimmer {
363
+ 0% { background-position: -200% 0; }
364
+ 100% { background-position: 200% 0; }
365
+ }
366
+
367
+ @keyframes pulse-glow {
368
+ 0%, 100% { box-shadow: 0 0 20px rgba(132, 85, 239, 0.2); }
369
+ 50% { box-shadow: 0 0 40px rgba(132, 85, 239, 0.5), 0 0 80px rgba(83, 221, 252, 0.2); }
370
+ }
371
+
372
+ @keyframes spin {
373
+ to { transform: rotate(360deg); }
374
+ }
375
+
376
+ .generating {
377
+ animation: pulse-glow 2s ease-in-out infinite !important;
378
+ }
379
+
380
+ /* ── Scrollbar ── */
381
+ ::-webkit-scrollbar { width: 6px; }
382
+ ::-webkit-scrollbar-track { background: var(--bg); }
383
+ ::-webkit-scrollbar-thumb {
384
+ background: var(--outline-variant);
385
+ border-radius: 3px;
386
+ }
387
+ ::-webkit-scrollbar-thumb:hover { background: var(--outline); }
388
+
389
+ /* ── Labels ── */
390
+ label, .label-wrap {
391
+ color: var(--on-surface-variant) !important;
392
+ font-weight: 500 !important;
393
+ font-size: 0.85rem !important;
394
+ }
395
+
396
+ /* ── Markdown in API tab ── */
397
+ .prose {
398
+ color: var(--on-surface) !important;
399
+ }
400
+
401
+ .prose h2, .prose h3 {
402
+ font-family: 'Space Grotesk', sans-serif !important;
403
+ color: var(--on-surface) !important;
404
+ }
405
+
406
+ .prose code {
407
+ background: var(--surface-high) !important;
408
+ color: var(--secondary) !important;
409
+ padding: 2px 6px !important;
410
+ border-radius: 6px !important;
411
+ font-size: 0.85em !important;
412
+ }
413
+
414
+ .prose pre {
415
+ background: var(--surface) !important;
416
+ border: 1px solid var(--outline-variant) !important;
417
+ border-radius: var(--radius-md) !important;
418
+ padding: 16px !important;
419
+ }
420
+
421
+ .prose pre code {
422
+ background: transparent !important;
423
+ color: var(--on-surface) !important;
424
+ }
425
+
426
+ .prose hr {
427
+ border-color: var(--outline-variant) !important;
428
+ opacity: 0.3 !important;
429
+ }
430
+
431
+ .prose strong {
432
+ color: var(--primary) !important;
433
+ }
434
+
435
+ /* ── Footer Hide ── */
436
  footer { display: none !important; }
437
+
438
+ /* ── Result Card Styling ── */
439
+ .result-card {
440
+ background: var(--surface) !important;
441
+ border: 1px solid var(--glass-border) !important;
442
+ border-radius: var(--radius-lg) !important;
443
+ padding: 20px !important;
444
+ position: relative !important;
445
+ }
446
+
447
+ .result-card::after {
448
+ content: '';
449
+ position: absolute;
450
+ bottom: 0; left: 0; right: 0;
451
+ height: 1px;
452
+ background: linear-gradient(90deg, transparent, rgba(83, 221, 252, 0.3), transparent);
453
+ pointer-events: none;
454
+ }
455
  """
456
 
457
+ # ─────────────────────────────────────────────────────────────────────
458
+ # Build Gradio UI
459
+ # ─────────────────────────────────────────────────────────────────────
460
+
461
+ RESOLUTIONS = [
462
+ "4K (3840×2160)",
463
+ "2K (2560×1440)",
464
+ "1080p (1920×1080)",
465
+ "2x Original",
466
+ ]
467
+
468
+ METHODS = ["Real-ESRGAN (AI)", "Lanczos (Fast)"]
469
+
470
+
471
+ def process_image(image_path, method, target, progress=gr.Progress()):
472
+ """Wrapper that bridges Gradio progress to core function."""
473
+ return upscale_image(image_path, method, target, progress=progress)
474
+
475
+
476
+ def process_video(video_path, method, target, progress=gr.Progress()):
477
+ """Wrapper that bridges Gradio progress to core function."""
478
+ return upscale_video(video_path, method, target, progress=progress)
479
+
480
+
481
+ with gr.Blocks(
482
+ css=CSS,
483
+ title="4K Upscaler Pro — AI Image & Video Enhancement",
484
+ theme=gr.themes.Base(
485
+ primary_hue=gr.themes.colors.purple,
486
+ secondary_hue=gr.themes.colors.cyan,
487
+ neutral_hue=gr.themes.colors.gray,
488
+ font=gr.themes.GoogleFont("Inter"),
489
+ font_mono=gr.themes.GoogleFont("Space Mono"),
490
+ ),
491
+ ) as demo:
492
+
493
+ # ── Hero Section ──
494
+ gr.Markdown("# ⬆ 4K Upscaler Pro", elem_classes=["hero-title"])
495
  gr.Markdown(
496
+ "AI-powered image & video enhancement • Up to 4K resolution • REST API ready",
497
+ elem_classes=["hero-subtitle"],
 
498
  )
499
+ gr.Markdown("✦ POWERED BY REAL-ESRGAN", elem_classes=["status-badge"])
500
 
501
  with gr.Tabs():
502
+ # ━━━━━━━━━━━━ IMAGE TAB ━━━━━━━━━━━━
503
+ with gr.TabItem("🖼 Image Upscaler", id="image-tab"):
504
+ with gr.Row(equal_height=True):
505
+ with gr.Column(scale=1, elem_classes=["glass-panel"]):
506
+ gr.Markdown("**INPUT**", elem_classes=["section-label"])
507
+ img_input = gr.Image(
508
+ type="filepath",
509
+ label="Upload Image",
510
+ height=300,
511
+ sources=["upload", "clipboard"],
512
+ )
513
  img_method = gr.Radio(
514
+ METHODS,
515
  value="Lanczos (Fast)",
516
  label="Upscaling Method",
517
  )
518
  img_target = gr.Dropdown(
519
+ RESOLUTIONS,
 
 
 
 
 
520
  value="4K (3840×2160)",
521
  label="Target Resolution",
522
  )
523
+ img_btn = gr.Button(
524
+ "⬆ Upscale Image",
525
+ variant="primary",
526
+ size="lg",
527
+ )
528
+
529
+ with gr.Column(scale=1, elem_classes=["glass-panel"]):
530
+ gr.Markdown("**RESULT**", elem_classes=["section-label"])
531
+ img_output = gr.Image(
532
+ label="Upscaled Result",
533
+ height=300,
534
+ interactive=False,
535
+ )
536
+ img_info = gr.Textbox(
537
+ label="Processing Details",
538
+ interactive=False,
539
+ lines=3,
540
+ )
541
+ img_download = gr.File(label="⬇ Download PNG")
542
 
543
  img_btn.click(
544
+ process_image,
545
  inputs=[img_input, img_method, img_target],
546
  outputs=[img_output, img_info, img_download],
547
  )
548
 
549
+ # ━━━━━━━━━━━━ VIDEO TAB ━━━━━━━━━━━━
550
+ with gr.TabItem("🎬 Video Upscaler", id="video-tab"):
551
+ with gr.Row(equal_height=True):
552
+ with gr.Column(scale=1, elem_classes=["glass-panel"]):
553
+ gr.Markdown("**INPUT**", elem_classes=["section-label"])
554
+ vid_input = gr.Video(label="Upload Video", height=300)
555
  vid_method = gr.Radio(
556
+ METHODS,
557
  value="Lanczos (Fast)",
558
  label="Upscaling Method",
559
  )
560
  vid_target = gr.Dropdown(
561
+ RESOLUTIONS,
 
 
 
 
 
562
  value="4K (3840×2160)",
563
  label="Target Resolution",
564
  )
565
+ vid_btn = gr.Button(
566
+ "⬆ Upscale Video",
567
+ variant="primary",
568
+ size="lg",
569
+ )
570
+
571
+ with gr.Column(scale=1, elem_classes=["glass-panel"]):
572
+ gr.Markdown("**RESULT**", elem_classes=["section-label"])
573
+ vid_output = gr.Video(
574
+ label="Upscaled Result",
575
+ height=300,
576
+ interactive=False,
577
+ )
578
+ vid_info = gr.Textbox(
579
+ label="Processing Details",
580
+ interactive=False,
581
+ lines=3,
582
+ )
583
 
584
  vid_btn.click(
585
+ process_video,
586
  inputs=[vid_input, vid_method, vid_target],
587
  outputs=[vid_output, vid_info],
588
  )
589
 
590
+ # ━━━━━━━━━━━━ API DOCS TAB ━━━━━━━━━━━━
591
+ with gr.TabItem("📡 API Docs", id="api-tab"):
592
  gr.Markdown("""
593
  ## REST API — Use from n8n, curl, or any HTTP client
594
 
595
+ Hugging Face Spaces expose a built-in API automatically.
596
 
597
  ---
598
 
599
+ ### 🖼 Image Upscaling
600
 
601
+ **POST** `https://areebsa-upsclr.hf.space/api/predict`
602
 
603
  ```json
604
  {
605
  "data": [
606
+ "<base64_image_or_url>",
607
  "Lanczos (Fast)",
608
  "4K (3840×2160)"
609
  ]
 
614
  ```json
615
  {
616
  "data": [
617
+ "<upscaled_image_url>",
618
  "✅ Upscaled: 1280×720 → 3840×2160",
619
  "<download_url>"
620
  ]
 
623
 
624
  ---
625
 
626
+ ### 🎬 Video Upscaling
627
 
628
+ **POST** `https://areebsa-upsclr.hf.space/api/predict`
629
 
630
+ Use `fn_index: 1` for the video endpoint.
631
 
632
  ```json
633
  {
634
  "fn_index": 1,
635
  "data": [
636
+ "<video_url_or_path>",
637
  "Lanczos (Fast)",
638
  "4K (3840×2160)"
639
  ]
 
642
 
643
  ---
644
 
645
+ ### 🔗 n8n / Zapier Integration
646
 
647
+ Use the **HTTP Request** node:
648
  - **Method:** POST
649
+ - **URL:** `https://areebsa-upsclr.hf.space/api/predict`
650
  - **Body:** JSON with `data` array (see above)
651
  - **Headers:** `Content-Type: application/json`
652
 
653
+ For private spaces, add:
654
  - **Header:** `Authorization: Bearer <your_HF_token>`
655
  """)
656
 
657
+ # ── Launch ──
658
  if __name__ == "__main__":
659
+ demo.queue(max_size=5).launch(ssr_mode=False)
core.py CHANGED
@@ -1,5 +1,7 @@
 
 
 
1
  import tempfile
2
- from enum import Enum
3
 
4
  import cv2
5
  import numpy as np
@@ -14,102 +16,130 @@ except ImportError:
14
  REALESRGAN_AVAILABLE = False
15
 
16
 
17
- class TargetResolution(str, Enum):
18
- FOUR_K = "4K (3840×2160)"
19
- TWO_K = "2K (2560×1440)"
20
- TEN_EIGHTY = "1080p (1920×1080)"
21
- TWO_X = "2x Original"
 
 
22
 
23
 
24
- def get_dimensions(orig_w: int, orig_h: int, target: TargetResolution) -> tuple[int, int]:
25
  """Calculate target dimensions based on original size and target resolution."""
26
- if target == TargetResolution.FOUR_K:
27
- max_w, max_h = 3840, 2160
28
- elif target == TargetResolution.TWO_K:
29
- max_w, max_h = 2560, 1440
30
- elif target == TargetResolution.TEN_EIGHTY:
31
- max_w, max_h = 1920, 1080
32
- else: # TWO_X
33
  return orig_w * 2, orig_h * 2
 
 
 
34
 
35
- scale_w = max_w / orig_w
36
- scale_h = max_h / orig_h
37
- scale = min(scale_w, scale_h)
38
 
39
- return int(orig_w * scale), int(orig_h * scale)
 
40
 
41
 
42
- def load_upscaler(scale: int = 4) -> "RealESRGANer | None":
 
 
 
 
43
  if not REALESRGAN_AVAILABLE:
44
  return None
45
  model = RRDBNet(
46
- num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=scale
 
47
  )
48
  upscaler = RealESRGANer(
49
  scale=scale,
50
- model_path=f"https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x{scale}plus.pth",
 
 
 
51
  model=model,
52
  tile=512,
53
  tile_pad=10,
54
  pre_pad=0,
55
- half=False, # CPU-safe
56
  )
 
57
  return upscaler
58
 
59
 
60
- def opencv_upscale(img_np: np.ndarray, target_w: int, target_h: int) -> np.ndarray:
 
61
  return cv2.resize(img_np, (target_w, target_h), interpolation=cv2.INTER_LANCZOS4)
62
 
63
 
64
- def upscale_image(image, method, target_str):
65
-
66
- if image is None:
 
67
  return None, "❌ No image provided.", None
68
 
69
- if isinstance(image, str):
70
- image = Image.open(image)
71
-
72
- try:
73
- target = TargetResolution(target_str)
74
- except ValueError:
75
- target = TargetResolution.FOUR_K
76
 
 
77
  orig_w, orig_h = image.size
78
  img_np = np.array(image)
 
79
 
80
- target_w, target_h = get_dimensions(orig_w, orig_h, target)
 
 
 
81
 
82
  if method == "Real-ESRGAN (AI)" and REALESRGAN_AVAILABLE:
83
  try:
84
  upscaler = load_upscaler(scale=4)
 
 
85
  if upscaler:
86
  output, _ = upscaler.enhance(img_np, outscale=target_w / orig_w)
87
- output = cv2.resize(output, (target_w, target_h), interpolation=cv2.INTER_LANCZOS4)
 
 
88
  else:
89
  output = opencv_upscale(img_np, target_w, target_h)
90
- method = "Lanczos (fallback: ESRGAN uninitialized)"
91
  except Exception as e:
92
  output = opencv_upscale(img_np, target_w, target_h)
93
- method = f"Lanczos (fallback: {str(e)[:60]})"
94
  else:
 
 
95
  output = opencv_upscale(img_np, target_w, target_h)
96
- method = "Lanczos (Fast)"
 
 
 
97
 
98
  result_img = Image.fromarray(output)
99
- info = f"✅ Upscaled: {orig_w}×{orig_h} → {target_w}×{target_h} | Method: {method}"
 
100
 
101
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
102
- result_img.save(tmp.name, format="PNG")
103
- tmp_path = tmp.name
 
 
104
 
105
- return result_img, info, tmp_path
 
106
 
 
107
 
108
- def upscale_video(video_path, method, target_str, progress_callback=None):
109
 
 
 
 
110
  if not video_path:
111
  return None, "❌ No video provided."
112
 
 
 
 
113
  cap = cv2.VideoCapture(video_path)
114
  if not cap.isOpened():
115
  return None, "❌ Could not open video."
@@ -119,15 +149,9 @@ def upscale_video(video_path, method, target_str, progress_callback=None):
119
  orig_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
120
  fps = cap.get(cv2.CAP_PROP_FPS)
121
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
 
122
 
123
- try:
124
- target = TargetResolution(target_str)
125
- except ValueError:
126
- target = TargetResolution.FOUR_K
127
-
128
- target_w, target_h = get_dimensions(orig_w, orig_h, target)
129
-
130
- out_path = tempfile.mktemp(suffix=".mp4")
131
  fourcc = cv2.VideoWriter_fourcc(*"mp4v")
132
  writer = cv2.VideoWriter(out_path, fourcc, fps, (target_w, target_h))
133
 
@@ -151,9 +175,12 @@ def upscale_video(video_path, method, target_str, progress_callback=None):
151
 
152
  if upscaler:
153
  try:
154
- out_frame, _ = upscaler.enhance(frame_rgb, outscale=target_w / orig_w)
 
 
155
  out_frame = cv2.resize(
156
- out_frame, (target_w, target_h), interpolation=cv2.INTER_LANCZOS4
 
157
  )
158
  except Exception:
159
  out_frame = opencv_upscale(frame_rgb, target_w, target_h)
@@ -162,15 +189,25 @@ def upscale_video(video_path, method, target_str, progress_callback=None):
162
 
163
  writer.write(cv2.cvtColor(out_frame, cv2.COLOR_RGB2BGR))
164
  frame_idx += 1
165
- if total_frames > 0 and progress_callback:
166
- progress_callback(
167
- (frame_idx / total_frames), f"Processing frame {frame_idx}/{total_frames}"
 
 
 
168
  )
169
 
170
- info = f"✅ Video upscaled: {orig_w}×{orig_h} → {target_w}×{target_h} | {total_frames} frames @ {fps:.1f}fps"
 
 
 
 
171
  finally:
172
  writer.release()
173
  finally:
174
  cap.release()
175
 
 
 
 
176
  return out_path, info
 
1
+ """Core upscaling engine for 4K Upscaler Pro."""
2
+
3
+ import os
4
  import tempfile
 
5
 
6
  import cv2
7
  import numpy as np
 
16
  REALESRGAN_AVAILABLE = False
17
 
18
 
19
+ # ── Resolution targets ──────────────────────────────────────────────
20
+ RESOLUTIONS = {
21
+ "4K (3840×2160)": (3840, 2160),
22
+ "2K (2560×1440)": (2560, 1440),
23
+ "1080p (1920×1080)": (1920, 1080),
24
+ "2x Original": None,
25
+ }
26
 
27
 
28
+ def get_dimensions(orig_w, orig_h, target_str):
29
  """Calculate target dimensions based on original size and target resolution."""
30
+ dims = RESOLUTIONS.get(target_str)
31
+ if dims is None:
 
 
 
 
 
32
  return orig_w * 2, orig_h * 2
33
+ max_w, max_h = dims
34
+ scale = min(max_w / orig_w, max_h / orig_h)
35
+ return int(orig_w * scale), int(orig_h * scale)
36
 
 
 
 
37
 
38
+ # ── Model loader ────────────────────────────────────────────────────
39
+ _cached_upscaler = None
40
 
41
 
42
+ def load_upscaler(scale=4):
43
+ """Load Real-ESRGAN model (cached after first call)."""
44
+ global _cached_upscaler
45
+ if _cached_upscaler is not None:
46
+ return _cached_upscaler
47
  if not REALESRGAN_AVAILABLE:
48
  return None
49
  model = RRDBNet(
50
+ num_in_ch=3, num_out_ch=3, num_feat=64,
51
+ num_block=23, num_grow_ch=32, scale=scale,
52
  )
53
  upscaler = RealESRGANer(
54
  scale=scale,
55
+ model_path=(
56
+ f"https://github.com/xinntao/Real-ESRGAN/releases/"
57
+ f"download/v0.1.0/RealESRGAN_x{scale}plus.pth"
58
+ ),
59
  model=model,
60
  tile=512,
61
  tile_pad=10,
62
  pre_pad=0,
63
+ half=False,
64
  )
65
+ _cached_upscaler = upscaler
66
  return upscaler
67
 
68
 
69
+ def opencv_upscale(img_np, target_w, target_h):
70
+ """High-quality Lanczos resize."""
71
  return cv2.resize(img_np, (target_w, target_h), interpolation=cv2.INTER_LANCZOS4)
72
 
73
 
74
+ # ── Image upscaling ────────────────────────────────────────────────
75
+ def upscale_image(image_path, method, target_str, progress=None):
76
+ """Upscale a single image. Returns (output_path, info_text, download_path)."""
77
+ if image_path is None:
78
  return None, "❌ No image provided.", None
79
 
80
+ if progress:
81
+ progress(0.1, desc="📂 Loading image...")
 
 
 
 
 
82
 
83
+ image = Image.open(image_path).convert("RGB")
84
  orig_w, orig_h = image.size
85
  img_np = np.array(image)
86
+ target_w, target_h = get_dimensions(orig_w, orig_h, target_str)
87
 
88
+ if progress:
89
+ progress(0.3, desc=f"🔍 Scaling {orig_w}×{orig_h} → {target_w}×{target_h}...")
90
+
91
+ used_method = method
92
 
93
  if method == "Real-ESRGAN (AI)" and REALESRGAN_AVAILABLE:
94
  try:
95
  upscaler = load_upscaler(scale=4)
96
+ if progress:
97
+ progress(0.5, desc="🤖 Running Real-ESRGAN AI model...")
98
  if upscaler:
99
  output, _ = upscaler.enhance(img_np, outscale=target_w / orig_w)
100
+ output = cv2.resize(
101
+ output, (target_w, target_h), interpolation=cv2.INTER_LANCZOS4
102
+ )
103
  else:
104
  output = opencv_upscale(img_np, target_w, target_h)
105
+ used_method = "Lanczos (fallback)"
106
  except Exception as e:
107
  output = opencv_upscale(img_np, target_w, target_h)
108
+ used_method = f"Lanczos (fallback: {str(e)[:50]})"
109
  else:
110
+ if progress:
111
+ progress(0.5, desc="⚡ Applying Lanczos4 interpolation...")
112
  output = opencv_upscale(img_np, target_w, target_h)
113
+ used_method = "Lanczos (Fast)"
114
+
115
+ if progress:
116
+ progress(0.8, desc="💾 Saving result...")
117
 
118
  result_img = Image.fromarray(output)
119
+ out_path = os.path.join(tempfile.gettempdir(), "upscaled_output.png")
120
+ result_img.save(out_path, format="PNG", optimize=True)
121
 
122
+ info = (
123
+ f"✅ Upscaled: {orig_w}×{orig_h} → {target_w}×{target_h}\n"
124
+ f"📐 Method: {used_method}\n"
125
+ f"📄 Format: PNG | Size: {os.path.getsize(out_path) / 1024 / 1024:.1f} MB"
126
+ )
127
 
128
+ if progress:
129
+ progress(1.0, desc="✨ Done!")
130
 
131
+ return out_path, info, out_path
132
 
 
133
 
134
+ # ── Video upscaling ─────────────────────────────────────────────────
135
+ def upscale_video(video_path, method, target_str, progress=None):
136
+ """Upscale a video file. Returns (output_path, info_text)."""
137
  if not video_path:
138
  return None, "❌ No video provided."
139
 
140
+ if progress:
141
+ progress(0.05, desc="📂 Opening video...")
142
+
143
  cap = cv2.VideoCapture(video_path)
144
  if not cap.isOpened():
145
  return None, "❌ Could not open video."
 
149
  orig_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
150
  fps = cap.get(cv2.CAP_PROP_FPS)
151
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
152
+ target_w, target_h = get_dimensions(orig_w, orig_h, target_str)
153
 
154
+ out_path = os.path.join(tempfile.gettempdir(), "upscaled_video.mp4")
 
 
 
 
 
 
 
155
  fourcc = cv2.VideoWriter_fourcc(*"mp4v")
156
  writer = cv2.VideoWriter(out_path, fourcc, fps, (target_w, target_h))
157
 
 
175
 
176
  if upscaler:
177
  try:
178
+ out_frame, _ = upscaler.enhance(
179
+ frame_rgb, outscale=target_w / orig_w
180
+ )
181
  out_frame = cv2.resize(
182
+ out_frame, (target_w, target_h),
183
+ interpolation=cv2.INTER_LANCZOS4,
184
  )
185
  except Exception:
186
  out_frame = opencv_upscale(frame_rgb, target_w, target_h)
 
189
 
190
  writer.write(cv2.cvtColor(out_frame, cv2.COLOR_RGB2BGR))
191
  frame_idx += 1
192
+
193
+ if total_frames > 0 and progress:
194
+ pct = 0.1 + 0.85 * (frame_idx / total_frames)
195
+ progress(
196
+ pct,
197
+ desc=f"🎬 Processing frame {frame_idx}/{total_frames}",
198
  )
199
 
200
+ info = (
201
+ f"✅ Video upscaled: {orig_w}×{orig_h} → {target_w}×{target_h}\n"
202
+ f"🎞 {total_frames} frames @ {fps:.1f} fps\n"
203
+ f"📄 Size: {os.path.getsize(out_path) / 1024 / 1024:.1f} MB"
204
+ )
205
  finally:
206
  writer.release()
207
  finally:
208
  cap.release()
209
 
210
+ if progress:
211
+ progress(1.0, desc="✨ Done!")
212
+
213
  return out_path, info