Nior18867 commited on
Commit
d045a17
·
verified ·
1 Parent(s): 93ead10

Add UTM tracking

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