Nior18867 commited on
Commit
98d0477
·
verified ·
1 Parent(s): 53bfba2

Add wavespeed link to description

Browse files
Files changed (1) hide show
  1. app.py +524 -524
app.py CHANGED
@@ -1,524 +1,524 @@
1
- """
2
- Bria AI x WaveSpeed AI - Image Expand
3
- Clean Light Theme for Gradio 3.x
4
- """
5
-
6
- import gradio as gr
7
- import requests
8
- import time
9
- from typing import Dict, Any
10
-
11
- # ============ API Configuration ============
12
- WAVESPEED_API_BASE = "https://api.wavespeed.ai/api/v3"
13
- UPLOAD_ENDPOINT = "https://api.wavespeed.ai/api/v3/media/upload/binary"
14
- MODEL_ENDPOINT = "bria/expand"
15
- POLL_INTERVAL = 1.5
16
- POLL_MAX_SECONDS = 120
17
-
18
- # ============ Aspect Ratios ============
19
- ASPECT_RATIOS = ["1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16"]
20
-
21
- # ============ Enhanced Light Theme CSS ============
22
- CUSTOM_CSS = """
23
- /* ===== Base Styles ===== */
24
- html, body, .gradio-container {
25
- background: linear-gradient(145deg, #f5f3ff 0%, #ede9fe 50%, #e0e7ff 100%) !important;
26
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
27
- }
28
-
29
- .gradio-container {
30
- max-width: 960px !important;
31
- margin: 0 auto !important;
32
- padding: 24px !important;
33
- }
34
-
35
- /* ===== Hero Section ===== */
36
- .hero-container {
37
- text-align: center;
38
- padding: 48px 20px 36px;
39
- }
40
-
41
- .hero-badge {
42
- display: inline-block;
43
- background: linear-gradient(135deg, #8b5cf6, #7c3aed);
44
- padding: 10px 24px;
45
- border-radius: 50px;
46
- font-size: 0.7rem;
47
- color: #fff;
48
- font-weight: 700;
49
- letter-spacing: 1.5px;
50
- margin-bottom: 20px;
51
- box-shadow: 0 4px 20px rgba(139, 92, 246, 0.35);
52
- }
53
-
54
- .hero-title {
55
- font-size: 3rem;
56
- font-weight: 800;
57
- margin: 0 0 16px 0;
58
- background: linear-gradient(135deg, #6d28d9 0%, #8b5cf6 50%, #a78bfa 100%);
59
- -webkit-background-clip: text;
60
- -webkit-text-fill-color: transparent;
61
- background-clip: text;
62
- letter-spacing: -0.5px;
63
- }
64
-
65
- .hero-desc {
66
- font-size: 1.05rem;
67
- color: #64748b;
68
- max-width: 100%;
69
- margin: 0 auto 20px;
70
- line-height: 1.6;
71
- }
72
-
73
- .hero-badges {
74
- display: flex;
75
- gap: 28px;
76
- justify-content: center;
77
- flex-wrap: wrap;
78
- }
79
-
80
- .hero-badges span {
81
- display: flex;
82
- align-items: center;
83
- gap: 8px;
84
- color: #475569;
85
- font-size: 0.9rem;
86
- font-weight: 600;
87
- }
88
-
89
- /* ===== Main Card ===== */
90
- .main-card {
91
- background: #ffffff;
92
- border: 1px solid rgba(139, 92, 246, 0.1);
93
- border-radius: 20px;
94
- padding: 28px;
95
- margin-bottom: 20px;
96
- box-shadow: 0 4px 24px rgba(139, 92, 246, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);
97
- transition: box-shadow 0.3s ease;
98
- }
99
-
100
- .main-card:hover {
101
- box-shadow: 0 8px 32px rgba(139, 92, 246, 0.12), 0 2px 6px rgba(0, 0, 0, 0.04);
102
- }
103
-
104
- /* ===== API Key Section ===== */
105
- .api-key-row {
106
- display: flex;
107
- align-items: center;
108
- justify-content: space-between;
109
- margin-bottom: 12px;
110
- }
111
-
112
- .api-key-label {
113
- display: flex;
114
- align-items: center;
115
- gap: 10px;
116
- color: #1e293b;
117
- font-weight: 700;
118
- font-size: 1rem;
119
- }
120
-
121
- .get-key-btn {
122
- padding: 10px 20px;
123
- background: linear-gradient(135deg, #8b5cf6, #7c3aed);
124
- border: none;
125
- border-radius: 10px;
126
- color: #fff !important;
127
- text-decoration: none;
128
- font-weight: 600;
129
- font-size: 0.85rem;
130
- transition: all 0.25s ease;
131
- box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
132
- }
133
-
134
- .get-key-btn:hover {
135
- transform: translateY(-2px);
136
- box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
137
- color: #fff;
138
- }
139
-
140
- /* ===== Section Title ===== */
141
- .section-title {
142
- color: #1e293b;
143
- font-weight: 700;
144
- font-size: 1rem;
145
- display: flex;
146
- align-items: center;
147
- gap: 10px;
148
- margin-bottom: 16px;
149
- }
150
-
151
- /* ===== Image Upload Styling ===== */
152
- .upload-area {
153
- border: 2px dashed rgba(139, 92, 246, 0.3) !important;
154
- border-radius: 16px !important;
155
- background: linear-gradient(145deg, #faf5ff 0%, #f5f3ff 100%) !important;
156
- transition: all 0.3s ease !important;
157
- min-height: 220px !important;
158
- }
159
-
160
- .upload-area:hover {
161
- border-color: rgba(139, 92, 246, 0.5) !important;
162
- background: linear-gradient(145deg, #f5f3ff 0%, #ede9fe 100%) !important;
163
- }
164
-
165
- /* ===== Result Area ===== */
166
- .result-area {
167
- border: 2px solid rgba(139, 92, 246, 0.15) !important;
168
- border-radius: 16px !important;
169
- background: #fafafa !important;
170
- min-height: 220px !important;
171
- }
172
-
173
- /* ===== Radio Button Styling ===== */
174
- .aspect-ratio-group {
175
- margin-top: 16px;
176
- }
177
-
178
- .aspect-ratio-group .wrap {
179
- gap: 8px !important;
180
- }
181
-
182
- .aspect-ratio-group label {
183
- padding: 10px 16px !important;
184
- border-radius: 10px !important;
185
- border: 2px solid #e2e8f0 !important;
186
- background: #fff !important;
187
- font-weight: 600 !important;
188
- font-size: 0.85rem !important;
189
- color: #64748b !important;
190
- transition: all 0.2s ease !important;
191
- cursor: pointer !important;
192
- }
193
-
194
- .aspect-ratio-group label:hover {
195
- border-color: #a78bfa !important;
196
- color: #7c3aed !important;
197
- }
198
-
199
- .aspect-ratio-group label.selected {
200
- background: linear-gradient(135deg, #8b5cf6, #7c3aed) !important;
201
- border-color: transparent !important;
202
- color: #fff !important;
203
- }
204
-
205
- /* ===== Button Styling ===== */
206
- .primary-btn {
207
- width: 100%;
208
- margin-top: 20px !important;
209
- background: linear-gradient(135deg, #8b5cf6, #7c3aed) !important;
210
- border: none !important;
211
- color: #fff !important;
212
- font-weight: 700 !important;
213
- font-size: 1rem !important;
214
- padding: 14px 28px !important;
215
- border-radius: 12px !important;
216
- box-shadow: 0 4px 16px rgba(139, 92, 246, 0.35) !important;
217
- transition: all 0.25s ease !important;
218
- cursor: pointer !important;
219
- }
220
-
221
- .primary-btn:hover {
222
- transform: translateY(-2px) !important;
223
- box-shadow: 0 8px 24px rgba(139, 92, 246, 0.45) !important;
224
- }
225
-
226
- .primary-btn:active {
227
- transform: translateY(0) !important;
228
- }
229
-
230
- /* ===== CTA Section ===== */
231
- .cta-container {
232
- text-align: center;
233
- padding: 44px 32px;
234
- background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 50%, #6d28d9 100%);
235
- border-radius: 20px;
236
- margin-top: 8px;
237
- box-shadow: 0 8px 32px rgba(139, 92, 246, 0.35);
238
- position: relative;
239
- overflow: hidden;
240
- }
241
-
242
- .cta-container::before {
243
- content: '';
244
- position: absolute;
245
- top: -50%;
246
- right: -50%;
247
- width: 100%;
248
- height: 100%;
249
- background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
250
- pointer-events: none;
251
- }
252
-
253
- .cta-title {
254
- color: #fff;
255
- font-size: 1.5rem;
256
- font-weight: 800;
257
- margin: 0 0 8px 0;
258
- position: relative;
259
- }
260
-
261
- .cta-desc {
262
- color: rgba(255, 255, 255, 0.9);
263
- font-size: 1rem;
264
- margin: 0 0 24px 0;
265
- position: relative;
266
- }
267
-
268
- .cta-btn {
269
- display: inline-block;
270
- padding: 14px 36px;
271
- background: #fff;
272
- border-radius: 12px;
273
- color: #7c3aed !important;
274
- text-decoration: none;
275
- font-weight: 700;
276
- font-size: 1rem;
277
- transition: all 0.25s ease;
278
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
279
- position: relative;
280
- }
281
-
282
- .cta-btn:hover {
283
- transform: translateY(-3px);
284
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
285
- color: #6d28d9 !important;
286
- }
287
-
288
- /* ===== Hide Elements ===== */
289
- footer { display: none !important; }
290
-
291
- .hide-label > .label-wrap {
292
- display: none !important;
293
- }
294
-
295
- .hide-label span.svelte-1gfkn6j {
296
- display: none !important;
297
- }
298
-
299
- /* ===== Input Styling ===== */
300
- .gradio-container input[type="password"],
301
- .gradio-container input[type="text"] {
302
- border: 2px solid #e2e8f0 !important;
303
- border-radius: 12px !important;
304
- padding: 14px 16px !important;
305
- font-size: 0.95rem !important;
306
- transition: all 0.2s ease !important;
307
- }
308
-
309
- .gradio-container input[type="password"]:focus,
310
- .gradio-container input[type="text"]:focus {
311
- border-color: #8b5cf6 !important;
312
- box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15) !important;
313
- outline: none !important;
314
- }
315
-
316
- /* ===== Divider ===== */
317
- .divider {
318
- width: 1px;
319
- background: linear-gradient(180deg, transparent 0%, #e2e8f0 20%, #e2e8f0 80%, transparent 100%);
320
- margin: 0 24px;
321
- align-self: stretch;
322
- }
323
- """
324
-
325
-
326
- # ============ API Functions ============
327
- def upload_image(api_key: str, file_path: str) -> str:
328
- headers = {"Authorization": f"Bearer {api_key.strip()}"}
329
- with open(file_path, "rb") as f:
330
- resp = requests.post(UPLOAD_ENDPOINT, headers=headers, files={"file": f}, timeout=60)
331
- if resp.status_code == 401:
332
- raise Exception("Invalid API Key")
333
- elif resp.status_code >= 400:
334
- raise Exception(f"Upload failed: {resp.status_code}")
335
- data = resp.json()
336
- if data.get("code") != 200:
337
- raise Exception(data.get("message", "Upload failed"))
338
- return data.get("data", {}).get("download_url")
339
-
340
-
341
- def call_api(api_key: str, endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]:
342
- headers = {"Authorization": f"Bearer {api_key.strip()}", "Content-Type": "application/json"}
343
- resp = requests.post(f"{WAVESPEED_API_BASE}/{endpoint}", json=payload, headers=headers, timeout=30)
344
- if resp.status_code == 401:
345
- raise Exception("Invalid API Key")
346
- elif resp.status_code == 429:
347
- raise Exception("Quota exceeded")
348
- elif resp.status_code >= 400:
349
- raise Exception(f"API error: {resp.status_code}")
350
- data = resp.json()
351
- if data.get("code") != 200:
352
- raise Exception(data.get("message", "Unknown error"))
353
- return data.get("data", {})
354
-
355
-
356
- def poll_result(api_key: str, request_id: str) -> Dict[str, Any]:
357
- headers = {"Authorization": f"Bearer {api_key.strip()}"}
358
- url = f"{WAVESPEED_API_BASE}/predictions/{request_id}/result"
359
- start_time = time.time()
360
- while time.time() - start_time < POLL_MAX_SECONDS:
361
- resp = requests.get(url, headers=headers, timeout=30)
362
- if resp.status_code >= 400:
363
- raise Exception("Failed to get result")
364
- result = resp.json().get("data", {})
365
- status = result.get("status", "")
366
- if status == "completed":
367
- return result
368
- elif status == "failed":
369
- raise Exception("Generation failed")
370
- time.sleep(POLL_INTERVAL)
371
- raise Exception("Timeout")
372
-
373
-
374
- def expand_image(api_key: str, image_path: str, aspect_ratio: str):
375
- if not api_key or not api_key.strip():
376
- gr.Warning("Please enter your API Key")
377
- return None
378
- if not image_path:
379
- gr.Warning("Please upload an image")
380
- return None
381
-
382
- try:
383
- gr.Info("Uploading image...")
384
- image_url = upload_image(api_key, image_path)
385
-
386
- gr.Info("AI is expanding your image...")
387
- payload = {
388
- "image": image_url,
389
- "aspect_ratio": aspect_ratio,
390
- "enable_base64_output": False,
391
- "enable_sync_mode": False,
392
- }
393
- result = call_api(api_key, MODEL_ENDPOINT, payload)
394
- request_id = result.get("id")
395
- if not request_id:
396
- gr.Warning("Failed to start")
397
- return None
398
-
399
- final_result = poll_result(api_key, request_id)
400
- outputs = final_result.get("outputs", [])
401
- if outputs:
402
- gr.Info("Done!")
403
- return outputs[0]
404
- return None
405
-
406
- except Exception as e:
407
- error_msg = str(e).lower()
408
- if "invalid" in error_msg or "401" in error_msg:
409
- gr.Warning("Invalid API Key")
410
- elif "quota" in error_msg or "429" in error_msg:
411
- gr.Warning("Quota exceeded")
412
- elif "timeout" in error_msg:
413
- gr.Warning("Timeout - please try again")
414
- else:
415
- gr.Warning(str(e))
416
- return None
417
-
418
-
419
- # ============ Gradio UI ============
420
- with gr.Blocks(css=CUSTOM_CSS, title="Image Expand - Bria AI x WaveSpeed") as demo:
421
-
422
- # Hero Section
423
- gr.HTML("""
424
- <div class="hero-container">
425
- <div class="hero-badge">BRIA AI x WAVESPEED</div>
426
- <h1 class="hero-title">Image Expand</h1>
427
- <p class="hero-desc">
428
- Transform your images with AI-powered expansion. Intelligently extend any image to your desired aspect ratio.
429
- </p>
430
- <div class="hero-badges">
431
- <span>
432
- <svg width="18" height="18" fill="#10b981" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/></svg>
433
- Fast Processing
434
- </span>
435
- <span>
436
- <svg width="18" height="18" fill="#10b981" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/></svg>
437
- High Quality Output
438
- </span>
439
- <span>
440
- <svg width="18" height="18" fill="#10b981" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/></svg>
441
- Easy to Use
442
- </span>
443
- </div>
444
- </div>
445
- """)
446
-
447
- # API Key Card
448
- with gr.Group(elem_classes="main-card"):
449
- gr.HTML("""
450
- <div class="api-key-row">
451
- <span class="api-key-label">
452
- <svg width="20" height="20" fill="#8b5cf6" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clip-rule="evenodd"/></svg>
453
- API Key
454
- </span>
455
- <a href="https://wavespeed.ai/accesskey" target="_blank" class="get-key-btn">Get API Key</a>
456
- </div>
457
- """)
458
- api_key_input = gr.Textbox(
459
- placeholder="Enter your WaveSpeed API key",
460
- type="password",
461
- show_label=False
462
- )
463
-
464
- # Main Content Card
465
- with gr.Group(elem_classes="main-card"):
466
- with gr.Row():
467
- # Left Column - Input
468
- with gr.Column(scale=1):
469
- gr.HTML("""
470
- <div class="section-title">
471
- <svg width="20" height="20" fill="#8b5cf6" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"/></svg>
472
- Upload Image
473
- </div>
474
- """)
475
- image_input = gr.Image(
476
- label="",
477
- type="filepath",
478
- source="upload",
479
- elem_classes=["hide-label", "upload-area"]
480
- )
481
- aspect_ratio_input = gr.Radio(
482
- label="Aspect Ratio",
483
- choices=ASPECT_RATIOS,
484
- value="16:9",
485
- elem_classes="aspect-ratio-group"
486
- )
487
- submit_btn = gr.Button("Expand Image", variant="primary", elem_classes="primary-btn")
488
-
489
- # Right Column - Output
490
- with gr.Column(scale=1):
491
- gr.HTML("""
492
- <div class="section-title">
493
- <svg width="20" height="20" fill="#8b5cf6" viewBox="0 0 20 20"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/><path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/></svg>
494
- Result
495
- </div>
496
- """)
497
- output_image = gr.Image(
498
- label="",
499
- type="filepath",
500
- interactive=False,
501
- elem_classes=["hide-label", "result-area"]
502
- )
503
-
504
- # CTA Section
505
- gr.HTML("""
506
- <div class="cta-container">
507
- <h3 class="cta-title">Want More Features?</h3>
508
- <p class="cta-desc">Higher resolutions, batch processing, and 700+ AI models</p>
509
- <a href="https://wavespeed.ai/models" target="_blank" class="cta-btn">
510
- Explore WaveSpeed.ai
511
- </a>
512
- </div>
513
- """)
514
-
515
- # Event binding
516
- submit_btn.click(
517
- fn=expand_image,
518
- inputs=[api_key_input, image_input, aspect_ratio_input],
519
- outputs=output_image,
520
- )
521
-
522
-
523
- if __name__ == "__main__":
524
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
+ """
2
+ Bria AI x WaveSpeed AI - Image Expand
3
+ Clean Light Theme for Gradio 3.x
4
+ """
5
+
6
+ import gradio as gr
7
+ import requests
8
+ import time
9
+ from typing import Dict, Any
10
+
11
+ # ============ API Configuration ============
12
+ WAVESPEED_API_BASE = "https://api.wavespeed.ai/api/v3"
13
+ UPLOAD_ENDPOINT = "https://api.wavespeed.ai/api/v3/media/upload/binary"
14
+ MODEL_ENDPOINT = "bria/expand"
15
+ POLL_INTERVAL = 1.5
16
+ POLL_MAX_SECONDS = 120
17
+
18
+ # ============ Aspect Ratios ============
19
+ ASPECT_RATIOS = ["1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16"]
20
+
21
+ # ============ Enhanced Light Theme CSS ============
22
+ CUSTOM_CSS = """
23
+ /* ===== Base Styles ===== */
24
+ html, body, .gradio-container {
25
+ background: linear-gradient(145deg, #f5f3ff 0%, #ede9fe 50%, #e0e7ff 100%) !important;
26
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
27
+ }
28
+
29
+ .gradio-container {
30
+ max-width: 960px !important;
31
+ margin: 0 auto !important;
32
+ padding: 24px !important;
33
+ }
34
+
35
+ /* ===== Hero Section ===== */
36
+ .hero-container {
37
+ text-align: center;
38
+ padding: 48px 20px 36px;
39
+ }
40
+
41
+ .hero-badge {
42
+ display: inline-block;
43
+ background: linear-gradient(135deg, #8b5cf6, #7c3aed);
44
+ padding: 10px 24px;
45
+ border-radius: 50px;
46
+ font-size: 0.7rem;
47
+ color: #fff;
48
+ font-weight: 700;
49
+ letter-spacing: 1.5px;
50
+ margin-bottom: 20px;
51
+ box-shadow: 0 4px 20px rgba(139, 92, 246, 0.35);
52
+ }
53
+
54
+ .hero-title {
55
+ font-size: 3rem;
56
+ font-weight: 800;
57
+ margin: 0 0 16px 0;
58
+ background: linear-gradient(135deg, #6d28d9 0%, #8b5cf6 50%, #a78bfa 100%);
59
+ -webkit-background-clip: text;
60
+ -webkit-text-fill-color: transparent;
61
+ background-clip: text;
62
+ letter-spacing: -0.5px;
63
+ }
64
+
65
+ .hero-desc {
66
+ font-size: 1.05rem;
67
+ color: #64748b;
68
+ max-width: 100%;
69
+ margin: 0 auto 20px;
70
+ line-height: 1.6;
71
+ }
72
+
73
+ .hero-badges {
74
+ display: flex;
75
+ gap: 28px;
76
+ justify-content: center;
77
+ flex-wrap: wrap;
78
+ }
79
+
80
+ .hero-badges span {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 8px;
84
+ color: #475569;
85
+ font-size: 0.9rem;
86
+ font-weight: 600;
87
+ }
88
+
89
+ /* ===== Main Card ===== */
90
+ .main-card {
91
+ background: #ffffff;
92
+ border: 1px solid rgba(139, 92, 246, 0.1);
93
+ border-radius: 20px;
94
+ padding: 28px;
95
+ margin-bottom: 20px;
96
+ box-shadow: 0 4px 24px rgba(139, 92, 246, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);
97
+ transition: box-shadow 0.3s ease;
98
+ }
99
+
100
+ .main-card:hover {
101
+ box-shadow: 0 8px 32px rgba(139, 92, 246, 0.12), 0 2px 6px rgba(0, 0, 0, 0.04);
102
+ }
103
+
104
+ /* ===== API Key Section ===== */
105
+ .api-key-row {
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: space-between;
109
+ margin-bottom: 12px;
110
+ }
111
+
112
+ .api-key-label {
113
+ display: flex;
114
+ align-items: center;
115
+ gap: 10px;
116
+ color: #1e293b;
117
+ font-weight: 700;
118
+ font-size: 1rem;
119
+ }
120
+
121
+ .get-key-btn {
122
+ padding: 10px 20px;
123
+ background: linear-gradient(135deg, #8b5cf6, #7c3aed);
124
+ border: none;
125
+ border-radius: 10px;
126
+ color: #fff !important;
127
+ text-decoration: none;
128
+ font-weight: 600;
129
+ font-size: 0.85rem;
130
+ transition: all 0.25s ease;
131
+ box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
132
+ }
133
+
134
+ .get-key-btn:hover {
135
+ transform: translateY(-2px);
136
+ box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
137
+ color: #fff;
138
+ }
139
+
140
+ /* ===== Section Title ===== */
141
+ .section-title {
142
+ color: #1e293b;
143
+ font-weight: 700;
144
+ font-size: 1rem;
145
+ display: flex;
146
+ align-items: center;
147
+ gap: 10px;
148
+ margin-bottom: 16px;
149
+ }
150
+
151
+ /* ===== Image Upload Styling ===== */
152
+ .upload-area {
153
+ border: 2px dashed rgba(139, 92, 246, 0.3) !important;
154
+ border-radius: 16px !important;
155
+ background: linear-gradient(145deg, #faf5ff 0%, #f5f3ff 100%) !important;
156
+ transition: all 0.3s ease !important;
157
+ min-height: 220px !important;
158
+ }
159
+
160
+ .upload-area:hover {
161
+ border-color: rgba(139, 92, 246, 0.5) !important;
162
+ background: linear-gradient(145deg, #f5f3ff 0%, #ede9fe 100%) !important;
163
+ }
164
+
165
+ /* ===== Result Area ===== */
166
+ .result-area {
167
+ border: 2px solid rgba(139, 92, 246, 0.15) !important;
168
+ border-radius: 16px !important;
169
+ background: #fafafa !important;
170
+ min-height: 220px !important;
171
+ }
172
+
173
+ /* ===== Radio Button Styling ===== */
174
+ .aspect-ratio-group {
175
+ margin-top: 16px;
176
+ }
177
+
178
+ .aspect-ratio-group .wrap {
179
+ gap: 8px !important;
180
+ }
181
+
182
+ .aspect-ratio-group label {
183
+ padding: 10px 16px !important;
184
+ border-radius: 10px !important;
185
+ border: 2px solid #e2e8f0 !important;
186
+ background: #fff !important;
187
+ font-weight: 600 !important;
188
+ font-size: 0.85rem !important;
189
+ color: #64748b !important;
190
+ transition: all 0.2s ease !important;
191
+ cursor: pointer !important;
192
+ }
193
+
194
+ .aspect-ratio-group label:hover {
195
+ border-color: #a78bfa !important;
196
+ color: #7c3aed !important;
197
+ }
198
+
199
+ .aspect-ratio-group label.selected {
200
+ background: linear-gradient(135deg, #8b5cf6, #7c3aed) !important;
201
+ border-color: transparent !important;
202
+ color: #fff !important;
203
+ }
204
+
205
+ /* ===== Button Styling ===== */
206
+ .primary-btn {
207
+ width: 100%;
208
+ margin-top: 20px !important;
209
+ background: linear-gradient(135deg, #8b5cf6, #7c3aed) !important;
210
+ border: none !important;
211
+ color: #fff !important;
212
+ font-weight: 700 !important;
213
+ font-size: 1rem !important;
214
+ padding: 14px 28px !important;
215
+ border-radius: 12px !important;
216
+ box-shadow: 0 4px 16px rgba(139, 92, 246, 0.35) !important;
217
+ transition: all 0.25s ease !important;
218
+ cursor: pointer !important;
219
+ }
220
+
221
+ .primary-btn:hover {
222
+ transform: translateY(-2px) !important;
223
+ box-shadow: 0 8px 24px rgba(139, 92, 246, 0.45) !important;
224
+ }
225
+
226
+ .primary-btn:active {
227
+ transform: translateY(0) !important;
228
+ }
229
+
230
+ /* ===== CTA Section ===== */
231
+ .cta-container {
232
+ text-align: center;
233
+ padding: 44px 32px;
234
+ background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 50%, #6d28d9 100%);
235
+ border-radius: 20px;
236
+ margin-top: 8px;
237
+ box-shadow: 0 8px 32px rgba(139, 92, 246, 0.35);
238
+ position: relative;
239
+ overflow: hidden;
240
+ }
241
+
242
+ .cta-container::before {
243
+ content: '';
244
+ position: absolute;
245
+ top: -50%;
246
+ right: -50%;
247
+ width: 100%;
248
+ height: 100%;
249
+ background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
250
+ pointer-events: none;
251
+ }
252
+
253
+ .cta-title {
254
+ color: #fff;
255
+ font-size: 1.5rem;
256
+ font-weight: 800;
257
+ margin: 0 0 8px 0;
258
+ position: relative;
259
+ }
260
+
261
+ .cta-desc {
262
+ color: rgba(255, 255, 255, 0.9);
263
+ font-size: 1rem;
264
+ margin: 0 0 24px 0;
265
+ position: relative;
266
+ }
267
+
268
+ .cta-btn {
269
+ display: inline-block;
270
+ padding: 14px 36px;
271
+ background: #fff;
272
+ border-radius: 12px;
273
+ color: #7c3aed !important;
274
+ text-decoration: none;
275
+ font-weight: 700;
276
+ font-size: 1rem;
277
+ transition: all 0.25s ease;
278
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
279
+ position: relative;
280
+ }
281
+
282
+ .cta-btn:hover {
283
+ transform: translateY(-3px);
284
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
285
+ color: #6d28d9 !important;
286
+ }
287
+
288
+ /* ===== Hide Elements ===== */
289
+ footer { display: none !important; }
290
+
291
+ .hide-label > .label-wrap {
292
+ display: none !important;
293
+ }
294
+
295
+ .hide-label span.svelte-1gfkn6j {
296
+ display: none !important;
297
+ }
298
+
299
+ /* ===== Input Styling ===== */
300
+ .gradio-container input[type="password"],
301
+ .gradio-container input[type="text"] {
302
+ border: 2px solid #e2e8f0 !important;
303
+ border-radius: 12px !important;
304
+ padding: 14px 16px !important;
305
+ font-size: 0.95rem !important;
306
+ transition: all 0.2s ease !important;
307
+ }
308
+
309
+ .gradio-container input[type="password"]:focus,
310
+ .gradio-container input[type="text"]:focus {
311
+ border-color: #8b5cf6 !important;
312
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15) !important;
313
+ outline: none !important;
314
+ }
315
+
316
+ /* ===== Divider ===== */
317
+ .divider {
318
+ width: 1px;
319
+ background: linear-gradient(180deg, transparent 0%, #e2e8f0 20%, #e2e8f0 80%, transparent 100%);
320
+ margin: 0 24px;
321
+ align-self: stretch;
322
+ }
323
+ """
324
+
325
+
326
+ # ============ API Functions ============
327
+ def upload_image(api_key: str, file_path: str) -> str:
328
+ headers = {"Authorization": f"Bearer {api_key.strip()}"}
329
+ with open(file_path, "rb") as f:
330
+ resp = requests.post(UPLOAD_ENDPOINT, headers=headers, files={"file": f}, timeout=60)
331
+ if resp.status_code == 401:
332
+ raise Exception("Invalid API Key")
333
+ elif resp.status_code >= 400:
334
+ raise Exception(f"Upload failed: {resp.status_code}")
335
+ data = resp.json()
336
+ if data.get("code") != 200:
337
+ raise Exception(data.get("message", "Upload failed"))
338
+ return data.get("data", {}).get("download_url")
339
+
340
+
341
+ def call_api(api_key: str, endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]:
342
+ headers = {"Authorization": f"Bearer {api_key.strip()}", "Content-Type": "application/json"}
343
+ resp = requests.post(f"{WAVESPEED_API_BASE}/{endpoint}", json=payload, headers=headers, timeout=30)
344
+ if resp.status_code == 401:
345
+ raise Exception("Invalid API Key")
346
+ elif resp.status_code == 429:
347
+ raise Exception("Quota exceeded")
348
+ elif resp.status_code >= 400:
349
+ raise Exception(f"API error: {resp.status_code}")
350
+ data = resp.json()
351
+ if data.get("code") != 200:
352
+ raise Exception(data.get("message", "Unknown error"))
353
+ return data.get("data", {})
354
+
355
+
356
+ def poll_result(api_key: str, request_id: str) -> Dict[str, Any]:
357
+ headers = {"Authorization": f"Bearer {api_key.strip()}"}
358
+ url = f"{WAVESPEED_API_BASE}/predictions/{request_id}/result"
359
+ start_time = time.time()
360
+ while time.time() - start_time < POLL_MAX_SECONDS:
361
+ resp = requests.get(url, headers=headers, timeout=30)
362
+ if resp.status_code >= 400:
363
+ raise Exception("Failed to get result")
364
+ result = resp.json().get("data", {})
365
+ status = result.get("status", "")
366
+ if status == "completed":
367
+ return result
368
+ elif status == "failed":
369
+ raise Exception("Generation failed")
370
+ time.sleep(POLL_INTERVAL)
371
+ raise Exception("Timeout")
372
+
373
+
374
+ def expand_image(api_key: str, image_path: str, aspect_ratio: str):
375
+ if not api_key or not api_key.strip():
376
+ gr.Warning("Please enter your API Key")
377
+ return None
378
+ if not image_path:
379
+ gr.Warning("Please upload an image")
380
+ return None
381
+
382
+ try:
383
+ gr.Info("Uploading image...")
384
+ image_url = upload_image(api_key, image_path)
385
+
386
+ gr.Info("AI is expanding your image...")
387
+ payload = {
388
+ "image": image_url,
389
+ "aspect_ratio": aspect_ratio,
390
+ "enable_base64_output": False,
391
+ "enable_sync_mode": False,
392
+ }
393
+ result = call_api(api_key, MODEL_ENDPOINT, payload)
394
+ request_id = result.get("id")
395
+ if not request_id:
396
+ gr.Warning("Failed to start")
397
+ return None
398
+
399
+ final_result = poll_result(api_key, request_id)
400
+ outputs = final_result.get("outputs", [])
401
+ if outputs:
402
+ gr.Info("Done!")
403
+ return outputs[0]
404
+ return None
405
+
406
+ except Exception as e:
407
+ error_msg = str(e).lower()
408
+ if "invalid" in error_msg or "401" in error_msg:
409
+ gr.Warning("Invalid API Key")
410
+ elif "quota" in error_msg or "429" in error_msg:
411
+ gr.Warning("Quota exceeded")
412
+ elif "timeout" in error_msg:
413
+ gr.Warning("Timeout - please try again")
414
+ else:
415
+ gr.Warning(str(e))
416
+ return None
417
+
418
+
419
+ # ============ Gradio UI ============
420
+ with gr.Blocks(css=CUSTOM_CSS, title="Image Expand - Bria AI x WaveSpeed") as demo:
421
+
422
+ # Hero Section
423
+ gr.HTML("""
424
+ <div class="hero-container">
425
+ <div class="hero-badge">BRIA AI x WAVESPEED</div>
426
+ <h1 class="hero-title">Image Expand</h1>
427
+ <p class="hero-desc">
428
+ Transform your images with AI-powered expansion. Intelligently extend any image to your desired aspect ratio.
429
+ </p>
430
+ <div class="hero-badges">
431
+ <span>
432
+ <svg width="18" height="18" fill="#10b981" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/></svg>
433
+ Fast Processing
434
+ </span>
435
+ <span>
436
+ <svg width="18" height="18" fill="#10b981" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/></svg>
437
+ High Quality Output
438
+ </span>
439
+ <span>
440
+ <svg width="18" height="18" fill="#10b981" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/></svg>
441
+ Easy to Use
442
+ </span>
443
+ </div>
444
+ </div>
445
+ """)
446
+
447
+ # API Key Card
448
+ with gr.Group(elem_classes="main-card"):
449
+ gr.HTML("""
450
+ <div class="api-key-row">
451
+ <span class="api-key-label">
452
+ <svg width="20" height="20" fill="#8b5cf6" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clip-rule="evenodd"/></svg>
453
+ API Key
454
+ </span>
455
+ <a href="https://wavespeed.ai/accesskey" target="_blank" class="get-key-btn">Get API Key</a>
456
+ </div>
457
+ """)
458
+ api_key_input = gr.Textbox(
459
+ placeholder="Enter your WaveSpeed API key",
460
+ type="password",
461
+ show_label=False
462
+ )
463
+
464
+ # Main Content Card
465
+ with gr.Group(elem_classes="main-card"):
466
+ with gr.Row():
467
+ # Left Column - Input
468
+ with gr.Column(scale=1):
469
+ gr.HTML("""
470
+ <div class="section-title">
471
+ <svg width="20" height="20" fill="#8b5cf6" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"/></svg>
472
+ Upload Image
473
+ </div>
474
+ """)
475
+ image_input = gr.Image(
476
+ label="",
477
+ type="filepath",
478
+ source="upload",
479
+ elem_classes=["hide-label", "upload-area"]
480
+ )
481
+ aspect_ratio_input = gr.Radio(
482
+ label="Aspect Ratio",
483
+ choices=ASPECT_RATIOS,
484
+ value="16:9",
485
+ elem_classes="aspect-ratio-group"
486
+ )
487
+ submit_btn = gr.Button("Expand Image", variant="primary", elem_classes="primary-btn")
488
+
489
+ # Right Column - Output
490
+ with gr.Column(scale=1):
491
+ gr.HTML("""
492
+ <div class="section-title">
493
+ <svg width="20" height="20" fill="#8b5cf6" viewBox="0 0 20 20"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/><path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/></svg>
494
+ Result
495
+ </div>
496
+ """)
497
+ output_image = gr.Image(
498
+ label="",
499
+ type="filepath",
500
+ interactive=False,
501
+ elem_classes=["hide-label", "result-area"]
502
+ )
503
+
504
+ # CTA Section
505
+ gr.HTML("""
506
+ <div class="cta-container">
507
+ <h3 class="cta-title">Want More Features?</h3>
508
+ <p class="cta-desc">Higher resolutions, batch processing, and 700+ AI models</p>
509
+ <a href="https://wavespeed.ai/models" target="_blank" class="cta-btn">
510
+ Explore WaveSpeed.ai
511
+ </a>
512
+ </div>
513
+ """)
514
+
515
+ # Event binding
516
+ submit_btn.click(
517
+ fn=expand_image,
518
+ inputs=[api_key_input, image_input, aspect_ratio_input],
519
+ outputs=output_image,
520
+ )
521
+
522
+
523
+ if __name__ == "__main__":
524
+ demo.launch(server_name="0.0.0.0", server_port=7860)