Nior18867 commited on
Commit
9a3789a
·
verified ·
1 Parent(s): 3aef124

Add wavespeed link to description

Browse files
Files changed (1) hide show
  1. app.py +458 -458
app.py CHANGED
@@ -1,458 +1,458 @@
1
- """
2
- Image Upscaler Comparison - Model Comparison
3
- Powered by WaveSpeed AI - Auto-generated by Space Generator
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
-
15
- MODEL_A_ENDPOINT = "wavespeed-ai/image-upscaler"
16
- MODEL_A_NAME = "Image Upscaler"
17
-
18
- MODEL_B_ENDPOINT = "wavespeed-ai/ultimate-image-upscaler"
19
- MODEL_B_NAME = "Ultimate Upscaler"
20
-
21
- POLL_INTERVAL = 1.5
22
- POLL_MAX_SECONDS = 120
23
-
24
-
25
-
26
- # ============ CSS ============
27
- CUSTOM_CSS = """
28
- /* ===== Base Styles ===== */
29
- html, body, .gradio-container {
30
- background: linear-gradient(145deg, #f5f3ff 0%, #ede9fe 50%, #e0e7ff 100%) !important;
31
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
32
- }
33
-
34
- .gradio-container {
35
- max-width: 1200px !important;
36
- margin: 0 auto !important;
37
- padding: 24px !important;
38
- }
39
-
40
- /* ===== Hero Section ===== */
41
- .hero-container {
42
- text-align: center;
43
- padding: 40px 20px 30px;
44
- }
45
-
46
- .hero-badge {
47
- display: inline-block;
48
- background: linear-gradient(135deg, #06b6d4, #0891b2);
49
- padding: 10px 24px;
50
- border-radius: 50px;
51
- font-size: 0.7rem;
52
- color: #fff;
53
- font-weight: 700;
54
- letter-spacing: 1.5px;
55
- margin-bottom: 16px;
56
- box-shadow: 0 4px 20px rgba(139, 92, 246, 0.35);
57
- }
58
-
59
- .hero-title {
60
- font-size: 2.5rem;
61
- font-weight: 800;
62
- margin: 0 0 12px 0;
63
- background: linear-gradient(135deg, #6d28d9 0%, #06b6d4 50%, #a78bfa 100%);
64
- -webkit-background-clip: text;
65
- -webkit-text-fill-color: transparent;
66
- background-clip: text;
67
- }
68
-
69
- .hero-desc {
70
- font-size: 1rem;
71
- color: #64748b;
72
- max-width: 100%;
73
- margin: 0 auto 16px;
74
- line-height: 1.6;
75
- }
76
-
77
- /* ===== Main Card ===== */
78
- .main-card {
79
- background: #ffffff;
80
- border: 1px solid rgba(139, 92, 246, 0.1);
81
- border-radius: 20px;
82
- padding: 24px;
83
- margin-bottom: 16px;
84
- box-shadow: 0 4px 24px rgba(139, 92, 246, 0.08);
85
- }
86
-
87
- /* ===== Model Labels ===== */
88
- .model-label {
89
- display: inline-block;
90
- padding: 6px 14px;
91
- border-radius: 8px;
92
- font-weight: 700;
93
- font-size: 0.85rem;
94
- margin-bottom: 12px;
95
- }
96
-
97
- .model-a-label {
98
- background: linear-gradient(135deg, #3b82f6, #2563eb);
99
- color: #fff;
100
- }
101
-
102
- .model-b-label {
103
- background: linear-gradient(135deg, #10b981, #059669);
104
- color: #fff;
105
- }
106
-
107
- /* ===== VS Divider ===== */
108
- .vs-divider {
109
- display: flex;
110
- align-items: center;
111
- justify-content: center;
112
- padding: 8px 0;
113
- }
114
-
115
- .vs-badge {
116
- background: linear-gradient(135deg, #06b6d4, #0891b2);
117
- color: #fff;
118
- padding: 8px 16px;
119
- border-radius: 20px;
120
- font-weight: 800;
121
- font-size: 0.9rem;
122
- box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
123
- }
124
-
125
- /* ===== API Key Section ===== */
126
- .api-key-row {
127
- display: flex;
128
- align-items: center;
129
- justify-content: space-between;
130
- margin-bottom: 12px;
131
- }
132
-
133
- .api-key-label {
134
- display: flex;
135
- align-items: center;
136
- gap: 10px;
137
- color: #1e293b;
138
- font-weight: 700;
139
- font-size: 1rem;
140
- }
141
-
142
- .get-key-btn {
143
- padding: 10px 20px;
144
- background: linear-gradient(135deg, #06b6d4, #0891b2);
145
- border: none;
146
- border-radius: 10px;
147
- color: #fff !important;
148
- text-decoration: none;
149
- font-weight: 600;
150
- font-size: 0.85rem;
151
- transition: all 0.25s ease;
152
- box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
153
- }
154
-
155
- .get-key-btn:hover {
156
- transform: translateY(-2px);
157
- box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
158
- }
159
-
160
- /* ===== Section Title ===== */
161
- .section-title {
162
- color: #1e293b;
163
- font-weight: 700;
164
- font-size: 1rem;
165
- display: flex;
166
- align-items: center;
167
- gap: 10px;
168
- margin-bottom: 12px;
169
- }
170
-
171
- /* ===== Upload Area ===== */
172
- .upload-area {
173
- border: 2px dashed rgba(139, 92, 246, 0.3) !important;
174
- border-radius: 16px !important;
175
- background: linear-gradient(145deg, #faf5ff 0%, #f5f3ff 100%) !important;
176
- min-height: 180px !important;
177
- }
178
-
179
- /* ===== Result Areas ===== */
180
- .result-area-a {
181
- border: 2px solid rgba(59, 130, 246, 0.3) !important;
182
- border-radius: 16px !important;
183
- background: rgba(239, 246, 255, 0.5) !important;
184
- min-height: 200px !important;
185
- }
186
-
187
- .result-area-b {
188
- border: 2px solid rgba(16, 185, 129, 0.3) !important;
189
- border-radius: 16px !important;
190
- background: rgba(236, 253, 245, 0.5) !important;
191
- min-height: 200px !important;
192
- }
193
-
194
- /* ===== Button Styling ===== */
195
- .compare-btn {
196
- width: 100%;
197
- margin-top: 16px !important;
198
- background: linear-gradient(135deg, #06b6d4, #0891b2) !important;
199
- border: none !important;
200
- color: #fff !important;
201
- font-weight: 700 !important;
202
- font-size: 1.1rem !important;
203
- padding: 16px 28px !important;
204
- border-radius: 12px !important;
205
- box-shadow: 0 4px 16px rgba(139, 92, 246, 0.35) !important;
206
- }
207
-
208
- .compare-btn:hover {
209
- transform: translateY(-2px) !important;
210
- box-shadow: 0 8px 24px rgba(139, 92, 246, 0.45) !important;
211
- }
212
-
213
- /* ===== CTA Section ===== */
214
- .cta-container {
215
- text-align: center;
216
- padding: 36px 28px;
217
- background: linear-gradient(135deg, #06b6d4 0%, #0891b2 50%, #6d28d9 100%);
218
- border-radius: 20px;
219
- margin-top: 8px;
220
- box-shadow: 0 8px 32px rgba(139, 92, 246, 0.35);
221
- }
222
-
223
- .cta-title {
224
- color: #fff;
225
- font-size: 1.4rem;
226
- font-weight: 800;
227
- margin: 0 0 8px 0;
228
- }
229
-
230
- .cta-desc {
231
- color: rgba(255, 255, 255, 0.9);
232
- font-size: 0.95rem;
233
- margin: 0 0 20px 0;
234
- }
235
-
236
- .cta-btn {
237
- display: inline-block;
238
- padding: 12px 32px;
239
- background: #fff;
240
- border-radius: 12px;
241
- color: #0891b2 !important;
242
- text-decoration: none;
243
- font-weight: 700;
244
- font-size: 0.95rem;
245
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
246
- }
247
-
248
- .cta-btn:hover {
249
- transform: translateY(-3px);
250
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
251
- }
252
-
253
- /* ===== Hide Elements ===== */
254
- footer { display: none !important; }
255
-
256
- /* ===== Input Styling ===== */
257
- .gradio-container input[type="password"],
258
- .gradio-container input[type="text"] {
259
- border: 2px solid #e2e8f0 !important;
260
- border-radius: 12px !important;
261
- padding: 12px 14px !important;
262
- font-size: 0.95rem !important;
263
- }
264
-
265
- .gradio-container input:focus {
266
- border-color: #06b6d4 !important;
267
- box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15) !important;
268
- }
269
- """
270
-
271
-
272
- # ============ API Functions ============
273
- def upload_image(api_key: str, file_path: str) -> str:
274
- headers = {"Authorization": f"Bearer {api_key.strip()}"}
275
- with open(file_path, "rb") as f:
276
- resp = requests.post(UPLOAD_ENDPOINT, headers=headers, files={"file": f}, timeout=60)
277
- if resp.status_code == 401:
278
- raise Exception("Invalid API Key")
279
- elif resp.status_code >= 400:
280
- raise Exception(f"Upload failed: {resp.status_code}")
281
- data = resp.json()
282
- if data.get("code") != 200:
283
- raise Exception(data.get("message", "Upload failed"))
284
- return data.get("data", {}).get("download_url")
285
-
286
-
287
- def call_api(api_key: str, endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]:
288
- headers = {"Authorization": f"Bearer {api_key.strip()}", "Content-Type": "application/json"}
289
- resp = requests.post(f"{WAVESPEED_API_BASE}/{endpoint}", json=payload, headers=headers, timeout=30)
290
- if resp.status_code == 401:
291
- raise Exception("Invalid API Key")
292
- elif resp.status_code == 429:
293
- raise Exception("Quota exceeded")
294
- elif resp.status_code >= 400:
295
- raise Exception(f"API error: {resp.status_code}")
296
- data = resp.json()
297
- if data.get("code") != 200:
298
- raise Exception(data.get("message", "Unknown error"))
299
- return data.get("data", {})
300
-
301
-
302
- def poll_result(api_key: str, request_id: str) -> Dict[str, Any]:
303
- headers = {"Authorization": f"Bearer {api_key.strip()}"}
304
- url = f"{WAVESPEED_API_BASE}/predictions/{request_id}/result"
305
- start_time = time.time()
306
- while time.time() - start_time < POLL_MAX_SECONDS:
307
- resp = requests.get(url, headers=headers, timeout=30)
308
- if resp.status_code >= 400:
309
- raise Exception("Failed to get result")
310
- result = resp.json().get("data", {})
311
- status = result.get("status", "")
312
- if status == "completed":
313
- return result
314
- elif status == "failed":
315
- raise Exception("Generation failed")
316
- time.sleep(POLL_INTERVAL)
317
- raise Exception("Timeout")
318
-
319
-
320
- def run_model(api_key: str, endpoint: str, payload: Dict[str, Any]):
321
- """运行单个模型"""
322
- try:
323
- result = call_api(api_key, endpoint, payload)
324
- request_id = result.get("id")
325
- if not request_id:
326
- return None
327
- final_result = poll_result(api_key, request_id)
328
- outputs = final_result.get("outputs", [])
329
- if outputs:
330
- return outputs[0]
331
- return None
332
- except Exception as e:
333
- return None
334
-
335
-
336
- def compare_models(api_key: str, image_path: str):
337
- """同时运行两个模型进行对比"""
338
- if not api_key or not api_key.strip():
339
- gr.Warning("Please enter your API Key")
340
- return None, None
341
- if not image_path:
342
- gr.Warning("Please upload an image")
343
- return None, None
344
-
345
- try:
346
- gr.Info("Uploading image...")
347
- image_url = upload_image(api_key, image_path)
348
- except Exception as e:
349
- gr.Warning(f"Upload failed: {e}")
350
- return None, None
351
-
352
- gr.Info(f"Running {MODEL_A_NAME}...")
353
- payload_a = {
354
- "image": image_url,
355
- "enable_base64_output": False,
356
- "enable_sync_mode": False
357
- }
358
- result_a = run_model(api_key, MODEL_A_ENDPOINT, payload_a)
359
-
360
- gr.Info(f"Running {MODEL_B_NAME}...")
361
- payload_b = {
362
- "image": image_url,
363
- "enable_base64_output": False,
364
- "enable_sync_mode": False
365
- }
366
- result_b = run_model(api_key, MODEL_B_ENDPOINT, payload_b)
367
-
368
- if result_a and result_b:
369
- gr.Info("Comparison complete!")
370
- elif result_a:
371
- gr.Warning(f"{MODEL_B_NAME} failed")
372
- elif result_b:
373
- gr.Warning(f"{MODEL_A_NAME} failed")
374
- else:
375
- gr.Warning("Both models failed")
376
-
377
- return result_a, result_b
378
-
379
-
380
- # ============ Gradio UI ============
381
- with gr.Blocks(css=CUSTOM_CSS, title="Image Upscaler Comparison") as demo:
382
-
383
- # Hero Section
384
- gr.HTML("""
385
- <div class="hero-container">
386
- <div class="hero-badge">WAVESPEED AI</div>
387
- <h1 class="hero-title">Image Upscaler Comparison</h1>
388
- <p class="hero-desc">Compare Image Upscaler vs Ultimate Upscaler side by side.</p>
389
- </div>
390
- """)
391
-
392
-
393
-
394
- # API Key Card
395
- with gr.Group(elem_classes="main-card"):
396
- gr.HTML("""
397
- <div class="api-key-row">
398
- <span class="api-key-label">
399
- <svg width="20" height="20" fill="#06b6d4" 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>
400
- API Key
401
- </span>
402
- <a href="https://wavespeed.ai/accesskey" target="_blank" class="get-key-btn">Get API Key</a>
403
- </div>
404
- """)
405
- api_key_input = gr.Textbox(
406
- placeholder="Enter your WaveSpeed API key",
407
- type="password",
408
- show_label=False
409
- )
410
-
411
- # Input Section
412
- with gr.Group(elem_classes="main-card"):
413
- gr.HTML("""
414
- <div class="section-title">
415
- <svg width="20" height="20" fill="#06b6d4" 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>
416
- Input
417
- </div>
418
- """)
419
- image_input = gr.Image(
420
- label="Upload Image",
421
- type="filepath",
422
- source="upload",
423
- elem_classes="upload-area"
424
- )
425
- compare_btn = gr.Button("🔄 Compare Models", variant="primary", elem_classes="compare-btn")
426
-
427
- # Results Section
428
- with gr.Group(elem_classes="main-card"):
429
- with gr.Row():
430
- with gr.Column(scale=1):
431
- gr.HTML(f'<span class="model-label model-a-label">{MODEL_A_NAME}</span>')
432
- output_a = gr.Image(label="", interactive=False, elem_classes="result-area-a")
433
-
434
- with gr.Column(scale=1):
435
- gr.HTML(f'<span class="model-label model-b-label">{MODEL_B_NAME}</span>')
436
- output_b = gr.Image(label="", interactive=False, elem_classes="result-area-b")
437
-
438
- # CTA Section
439
- gr.HTML("""
440
- <div class="cta-container">
441
- <h3 class="cta-title">Want More Features?</h3>
442
- <p class="cta-desc">Higher resolutions, batch processing, and 700+ AI models</p>
443
- <a href="https://wavespeed.ai/models" target="_blank" class="cta-btn">
444
- Explore WaveSpeed.ai
445
- </a>
446
- </div>
447
- """)
448
-
449
- # Event binding
450
- compare_btn.click(
451
- fn=compare_models,
452
- inputs=[api_key_input, image_input],
453
- outputs=[output_a, output_b],
454
- )
455
-
456
-
457
- if __name__ == "__main__":
458
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
+ """
2
+ Image Upscaler Comparison - Model Comparison
3
+ Powered by WaveSpeed AI - Auto-generated by Space Generator
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
+
15
+ MODEL_A_ENDPOINT = "wavespeed-ai/image-upscaler"
16
+ MODEL_A_NAME = "Image Upscaler"
17
+
18
+ MODEL_B_ENDPOINT = "wavespeed-ai/ultimate-image-upscaler"
19
+ MODEL_B_NAME = "Ultimate Upscaler"
20
+
21
+ POLL_INTERVAL = 1.5
22
+ POLL_MAX_SECONDS = 120
23
+
24
+
25
+
26
+ # ============ CSS ============
27
+ CUSTOM_CSS = """
28
+ /* ===== Base Styles ===== */
29
+ html, body, .gradio-container {
30
+ background: linear-gradient(145deg, #f5f3ff 0%, #ede9fe 50%, #e0e7ff 100%) !important;
31
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
32
+ }
33
+
34
+ .gradio-container {
35
+ max-width: 1200px !important;
36
+ margin: 0 auto !important;
37
+ padding: 24px !important;
38
+ }
39
+
40
+ /* ===== Hero Section ===== */
41
+ .hero-container {
42
+ text-align: center;
43
+ padding: 40px 20px 30px;
44
+ }
45
+
46
+ .hero-badge {
47
+ display: inline-block;
48
+ background: linear-gradient(135deg, #06b6d4, #0891b2);
49
+ padding: 10px 24px;
50
+ border-radius: 50px;
51
+ font-size: 0.7rem;
52
+ color: #fff;
53
+ font-weight: 700;
54
+ letter-spacing: 1.5px;
55
+ margin-bottom: 16px;
56
+ box-shadow: 0 4px 20px rgba(139, 92, 246, 0.35);
57
+ }
58
+
59
+ .hero-title {
60
+ font-size: 2.5rem;
61
+ font-weight: 800;
62
+ margin: 0 0 12px 0;
63
+ background: linear-gradient(135deg, #6d28d9 0%, #06b6d4 50%, #a78bfa 100%);
64
+ -webkit-background-clip: text;
65
+ -webkit-text-fill-color: transparent;
66
+ background-clip: text;
67
+ }
68
+
69
+ .hero-desc {
70
+ font-size: 1rem;
71
+ color: #64748b;
72
+ max-width: 100%;
73
+ margin: 0 auto 16px;
74
+ line-height: 1.6;
75
+ }
76
+
77
+ /* ===== Main Card ===== */
78
+ .main-card {
79
+ background: #ffffff;
80
+ border: 1px solid rgba(139, 92, 246, 0.1);
81
+ border-radius: 20px;
82
+ padding: 24px;
83
+ margin-bottom: 16px;
84
+ box-shadow: 0 4px 24px rgba(139, 92, 246, 0.08);
85
+ }
86
+
87
+ /* ===== Model Labels ===== */
88
+ .model-label {
89
+ display: inline-block;
90
+ padding: 6px 14px;
91
+ border-radius: 8px;
92
+ font-weight: 700;
93
+ font-size: 0.85rem;
94
+ margin-bottom: 12px;
95
+ }
96
+
97
+ .model-a-label {
98
+ background: linear-gradient(135deg, #3b82f6, #2563eb);
99
+ color: #fff;
100
+ }
101
+
102
+ .model-b-label {
103
+ background: linear-gradient(135deg, #10b981, #059669);
104
+ color: #fff;
105
+ }
106
+
107
+ /* ===== VS Divider ===== */
108
+ .vs-divider {
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ padding: 8px 0;
113
+ }
114
+
115
+ .vs-badge {
116
+ background: linear-gradient(135deg, #06b6d4, #0891b2);
117
+ color: #fff;
118
+ padding: 8px 16px;
119
+ border-radius: 20px;
120
+ font-weight: 800;
121
+ font-size: 0.9rem;
122
+ box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
123
+ }
124
+
125
+ /* ===== API Key Section ===== */
126
+ .api-key-row {
127
+ display: flex;
128
+ align-items: center;
129
+ justify-content: space-between;
130
+ margin-bottom: 12px;
131
+ }
132
+
133
+ .api-key-label {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 10px;
137
+ color: #1e293b;
138
+ font-weight: 700;
139
+ font-size: 1rem;
140
+ }
141
+
142
+ .get-key-btn {
143
+ padding: 10px 20px;
144
+ background: linear-gradient(135deg, #06b6d4, #0891b2);
145
+ border: none;
146
+ border-radius: 10px;
147
+ color: #fff !important;
148
+ text-decoration: none;
149
+ font-weight: 600;
150
+ font-size: 0.85rem;
151
+ transition: all 0.25s ease;
152
+ box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
153
+ }
154
+
155
+ .get-key-btn:hover {
156
+ transform: translateY(-2px);
157
+ box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
158
+ }
159
+
160
+ /* ===== Section Title ===== */
161
+ .section-title {
162
+ color: #1e293b;
163
+ font-weight: 700;
164
+ font-size: 1rem;
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 10px;
168
+ margin-bottom: 12px;
169
+ }
170
+
171
+ /* ===== Upload Area ===== */
172
+ .upload-area {
173
+ border: 2px dashed rgba(139, 92, 246, 0.3) !important;
174
+ border-radius: 16px !important;
175
+ background: linear-gradient(145deg, #faf5ff 0%, #f5f3ff 100%) !important;
176
+ min-height: 180px !important;
177
+ }
178
+
179
+ /* ===== Result Areas ===== */
180
+ .result-area-a {
181
+ border: 2px solid rgba(59, 130, 246, 0.3) !important;
182
+ border-radius: 16px !important;
183
+ background: rgba(239, 246, 255, 0.5) !important;
184
+ min-height: 200px !important;
185
+ }
186
+
187
+ .result-area-b {
188
+ border: 2px solid rgba(16, 185, 129, 0.3) !important;
189
+ border-radius: 16px !important;
190
+ background: rgba(236, 253, 245, 0.5) !important;
191
+ min-height: 200px !important;
192
+ }
193
+
194
+ /* ===== Button Styling ===== */
195
+ .compare-btn {
196
+ width: 100%;
197
+ margin-top: 16px !important;
198
+ background: linear-gradient(135deg, #06b6d4, #0891b2) !important;
199
+ border: none !important;
200
+ color: #fff !important;
201
+ font-weight: 700 !important;
202
+ font-size: 1.1rem !important;
203
+ padding: 16px 28px !important;
204
+ border-radius: 12px !important;
205
+ box-shadow: 0 4px 16px rgba(139, 92, 246, 0.35) !important;
206
+ }
207
+
208
+ .compare-btn:hover {
209
+ transform: translateY(-2px) !important;
210
+ box-shadow: 0 8px 24px rgba(139, 92, 246, 0.45) !important;
211
+ }
212
+
213
+ /* ===== CTA Section ===== */
214
+ .cta-container {
215
+ text-align: center;
216
+ padding: 36px 28px;
217
+ background: linear-gradient(135deg, #06b6d4 0%, #0891b2 50%, #6d28d9 100%);
218
+ border-radius: 20px;
219
+ margin-top: 8px;
220
+ box-shadow: 0 8px 32px rgba(139, 92, 246, 0.35);
221
+ }
222
+
223
+ .cta-title {
224
+ color: #fff;
225
+ font-size: 1.4rem;
226
+ font-weight: 800;
227
+ margin: 0 0 8px 0;
228
+ }
229
+
230
+ .cta-desc {
231
+ color: rgba(255, 255, 255, 0.9);
232
+ font-size: 0.95rem;
233
+ margin: 0 0 20px 0;
234
+ }
235
+
236
+ .cta-btn {
237
+ display: inline-block;
238
+ padding: 12px 32px;
239
+ background: #fff;
240
+ border-radius: 12px;
241
+ color: #0891b2 !important;
242
+ text-decoration: none;
243
+ font-weight: 700;
244
+ font-size: 0.95rem;
245
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
246
+ }
247
+
248
+ .cta-btn:hover {
249
+ transform: translateY(-3px);
250
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
251
+ }
252
+
253
+ /* ===== Hide Elements ===== */
254
+ footer { display: none !important; }
255
+
256
+ /* ===== Input Styling ===== */
257
+ .gradio-container input[type="password"],
258
+ .gradio-container input[type="text"] {
259
+ border: 2px solid #e2e8f0 !important;
260
+ border-radius: 12px !important;
261
+ padding: 12px 14px !important;
262
+ font-size: 0.95rem !important;
263
+ }
264
+
265
+ .gradio-container input:focus {
266
+ border-color: #06b6d4 !important;
267
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15) !important;
268
+ }
269
+ """
270
+
271
+
272
+ # ============ API Functions ============
273
+ def upload_image(api_key: str, file_path: str) -> str:
274
+ headers = {"Authorization": f"Bearer {api_key.strip()}"}
275
+ with open(file_path, "rb") as f:
276
+ resp = requests.post(UPLOAD_ENDPOINT, headers=headers, files={"file": f}, timeout=60)
277
+ if resp.status_code == 401:
278
+ raise Exception("Invalid API Key")
279
+ elif resp.status_code >= 400:
280
+ raise Exception(f"Upload failed: {resp.status_code}")
281
+ data = resp.json()
282
+ if data.get("code") != 200:
283
+ raise Exception(data.get("message", "Upload failed"))
284
+ return data.get("data", {}).get("download_url")
285
+
286
+
287
+ def call_api(api_key: str, endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]:
288
+ headers = {"Authorization": f"Bearer {api_key.strip()}", "Content-Type": "application/json"}
289
+ resp = requests.post(f"{WAVESPEED_API_BASE}/{endpoint}", json=payload, headers=headers, timeout=30)
290
+ if resp.status_code == 401:
291
+ raise Exception("Invalid API Key")
292
+ elif resp.status_code == 429:
293
+ raise Exception("Quota exceeded")
294
+ elif resp.status_code >= 400:
295
+ raise Exception(f"API error: {resp.status_code}")
296
+ data = resp.json()
297
+ if data.get("code") != 200:
298
+ raise Exception(data.get("message", "Unknown error"))
299
+ return data.get("data", {})
300
+
301
+
302
+ def poll_result(api_key: str, request_id: str) -> Dict[str, Any]:
303
+ headers = {"Authorization": f"Bearer {api_key.strip()}"}
304
+ url = f"{WAVESPEED_API_BASE}/predictions/{request_id}/result"
305
+ start_time = time.time()
306
+ while time.time() - start_time < POLL_MAX_SECONDS:
307
+ resp = requests.get(url, headers=headers, timeout=30)
308
+ if resp.status_code >= 400:
309
+ raise Exception("Failed to get result")
310
+ result = resp.json().get("data", {})
311
+ status = result.get("status", "")
312
+ if status == "completed":
313
+ return result
314
+ elif status == "failed":
315
+ raise Exception("Generation failed")
316
+ time.sleep(POLL_INTERVAL)
317
+ raise Exception("Timeout")
318
+
319
+
320
+ def run_model(api_key: str, endpoint: str, payload: Dict[str, Any]):
321
+ """运行单个模型"""
322
+ try:
323
+ result = call_api(api_key, endpoint, payload)
324
+ request_id = result.get("id")
325
+ if not request_id:
326
+ return None
327
+ final_result = poll_result(api_key, request_id)
328
+ outputs = final_result.get("outputs", [])
329
+ if outputs:
330
+ return outputs[0]
331
+ return None
332
+ except Exception as e:
333
+ return None
334
+
335
+
336
+ def compare_models(api_key: str, image_path: str):
337
+ """同时运行两个模型进行对比"""
338
+ if not api_key or not api_key.strip():
339
+ gr.Warning("Please enter your API Key")
340
+ return None, None
341
+ if not image_path:
342
+ gr.Warning("Please upload an image")
343
+ return None, None
344
+
345
+ try:
346
+ gr.Info("Uploading image...")
347
+ image_url = upload_image(api_key, image_path)
348
+ except Exception as e:
349
+ gr.Warning(f"Upload failed: {e}")
350
+ return None, None
351
+
352
+ gr.Info(f"Running {MODEL_A_NAME}...")
353
+ payload_a = {
354
+ "image": image_url,
355
+ "enable_base64_output": False,
356
+ "enable_sync_mode": False
357
+ }
358
+ result_a = run_model(api_key, MODEL_A_ENDPOINT, payload_a)
359
+
360
+ gr.Info(f"Running {MODEL_B_NAME}...")
361
+ payload_b = {
362
+ "image": image_url,
363
+ "enable_base64_output": False,
364
+ "enable_sync_mode": False
365
+ }
366
+ result_b = run_model(api_key, MODEL_B_ENDPOINT, payload_b)
367
+
368
+ if result_a and result_b:
369
+ gr.Info("Comparison complete!")
370
+ elif result_a:
371
+ gr.Warning(f"{MODEL_B_NAME} failed")
372
+ elif result_b:
373
+ gr.Warning(f"{MODEL_A_NAME} failed")
374
+ else:
375
+ gr.Warning("Both models failed")
376
+
377
+ return result_a, result_b
378
+
379
+
380
+ # ============ Gradio UI ============
381
+ with gr.Blocks(css=CUSTOM_CSS, title="Image Upscaler Comparison") as demo:
382
+
383
+ # Hero Section
384
+ gr.HTML("""
385
+ <div class="hero-container">
386
+ <div class="hero-badge">WAVESPEED AI</div>
387
+ <h1 class="hero-title">Image Upscaler Comparison</h1>
388
+ <p class="hero-desc">Compare Image Upscaler vs Ultimate Upscaler side by side. Try on <a href="https://wavespeed.ai" target="_blank" style="color: #8b5cf6; text-decoration: none; font-weight: 600;">wavespeed</a></p>
389
+ </div>
390
+ """)
391
+
392
+
393
+
394
+ # API Key Card
395
+ with gr.Group(elem_classes="main-card"):
396
+ gr.HTML("""
397
+ <div class="api-key-row">
398
+ <span class="api-key-label">
399
+ <svg width="20" height="20" fill="#06b6d4" 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>
400
+ API Key
401
+ </span>
402
+ <a href="https://wavespeed.ai/accesskey" target="_blank" class="get-key-btn">Get API Key</a>
403
+ </div>
404
+ """)
405
+ api_key_input = gr.Textbox(
406
+ placeholder="Enter your WaveSpeed API key",
407
+ type="password",
408
+ show_label=False
409
+ )
410
+
411
+ # Input Section
412
+ with gr.Group(elem_classes="main-card"):
413
+ gr.HTML("""
414
+ <div class="section-title">
415
+ <svg width="20" height="20" fill="#06b6d4" 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>
416
+ Input
417
+ </div>
418
+ """)
419
+ image_input = gr.Image(
420
+ label="Upload Image",
421
+ type="filepath",
422
+ source="upload",
423
+ elem_classes="upload-area"
424
+ )
425
+ compare_btn = gr.Button("🔄 Compare Models", variant="primary", elem_classes="compare-btn")
426
+
427
+ # Results Section
428
+ with gr.Group(elem_classes="main-card"):
429
+ with gr.Row():
430
+ with gr.Column(scale=1):
431
+ gr.HTML(f'<span class="model-label model-a-label">{MODEL_A_NAME}</span>')
432
+ output_a = gr.Image(label="", interactive=False, elem_classes="result-area-a")
433
+
434
+ with gr.Column(scale=1):
435
+ gr.HTML(f'<span class="model-label model-b-label">{MODEL_B_NAME}</span>')
436
+ output_b = gr.Image(label="", interactive=False, elem_classes="result-area-b")
437
+
438
+ # CTA Section
439
+ gr.HTML("""
440
+ <div class="cta-container">
441
+ <h3 class="cta-title">Want More Features?</h3>
442
+ <p class="cta-desc">Higher resolutions, batch processing, and 700+ AI models</p>
443
+ <a href="https://wavespeed.ai/models" target="_blank" class="cta-btn">
444
+ Explore WaveSpeed.ai
445
+ </a>
446
+ </div>
447
+ """)
448
+
449
+ # Event binding
450
+ compare_btn.click(
451
+ fn=compare_models,
452
+ inputs=[api_key_input, image_input],
453
+ outputs=[output_a, output_b],
454
+ )
455
+
456
+
457
+ if __name__ == "__main__":
458
+ demo.launch(server_name="0.0.0.0", server_port=7860)