Nior18867 commited on
Commit
275b75c
·
verified ·
1 Parent(s): 487d6b4

Fix: Docker python:3.10 + latest gradio

Browse files
Files changed (3) hide show
  1. Dockerfile +13 -0
  2. README.md +1 -2
  3. app.py +509 -509
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN pip install --no-cache-dir gradio requests>=2.28.0
6
+
7
+ COPY app.py .
8
+
9
+ EXPOSE 7860
10
+ ENV GRADIO_SERVER_NAME="0.0.0.0"
11
+ ENV GRADIO_SERVER_PORT="7860"
12
+
13
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -3,8 +3,7 @@ title: Fibo Edit
3
  emoji: 🖼️
4
  colorFrom: blue
5
  colorTo: purple
6
- sdk: gradio
7
- sdk_version: 5.0.0
8
  pinned: false
9
  license: mit
10
  short_description: AI-powered image editing with Bria Image 3.2.
 
3
  emoji: 🖼️
4
  colorFrom: blue
5
  colorTo: purple
6
+ sdk: docker
 
7
  pinned: false
8
  license: mit
9
  short_description: AI-powered image editing with Bria Image 3.2.
app.py CHANGED
@@ -1,509 +1,509 @@
1
- """
2
- Fibo 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 = "bria/fibo/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, #10b981, #059669);
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%, #10b981 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, #10b981, #059669);
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, #10b981, #059669) !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, #10b981 0%, #059669 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: #059669 !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: #10b981 !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
- print(f"[DEBUG] Payload: {payload}")
300
- resp = requests.post(f"{WAVESPEED_API_BASE}/{endpoint}", json=payload, headers=headers, timeout=30)
301
- print(f"[DEBUG] Status: {resp.status_code}")
302
- print(f"[DEBUG] Response: {resp.text[:500]}")
303
- if resp.status_code == 401:
304
- raise Exception("Invalid API Key")
305
- elif resp.status_code == 429:
306
- raise Exception("Quota exceeded")
307
- elif resp.status_code >= 400:
308
- raise Exception(f"API error {resp.status_code}: {resp.text[:200]}")
309
- data = resp.json()
310
- if data.get("code") != 200:
311
- raise Exception(data.get("message", "Unknown error"))
312
- return data.get("data", {})
313
-
314
-
315
- def poll_result(api_key: str, request_id: str) -> Dict[str, Any]:
316
- headers = {"Authorization": f"Bearer {api_key.strip()}"}
317
- url = f"{WAVESPEED_API_BASE}/predictions/{request_id}/result"
318
- start_time = time.time()
319
- while time.time() - start_time < POLL_MAX_SECONDS:
320
- resp = requests.get(url, headers=headers, timeout=30)
321
- if resp.status_code >= 400:
322
- raise Exception("Failed to get result")
323
- result = resp.json().get("data", {})
324
- status = result.get("status", "")
325
- if status == "completed":
326
- return result
327
- elif status == "failed":
328
- raise Exception("Generation failed")
329
- time.sleep(POLL_INTERVAL)
330
- raise Exception("Timeout")
331
-
332
-
333
- def download_video(url: str) -> str:
334
- """下载视频到临时文件,返回文件路径"""
335
- import tempfile
336
- import os
337
- try:
338
- resp = requests.get(url, timeout=120, stream=True)
339
- if resp.status_code != 200:
340
- return None
341
- # 从 URL 或 Content-Type 确定扩展名
342
- ext = ".mp4"
343
- if "webm" in url.lower() or "webm" in resp.headers.get("content-type", ""):
344
- ext = ".webm"
345
- # 创建临时文件
346
- fd, path = tempfile.mkstemp(suffix=ext)
347
- with os.fdopen(fd, 'wb') as f:
348
- for chunk in resp.iter_content(chunk_size=8192):
349
- f.write(chunk)
350
- return path
351
- except Exception as e:
352
- print(f"Download error: {e}")
353
- return None
354
-
355
-
356
- def process(api_key: str, image_path: str, prompt: str):
357
- if not api_key or not api_key.strip():
358
- gr.Warning("Please enter your API Key")
359
- return None
360
- if not image_path:
361
- gr.Warning("Please upload an image")
362
- return None
363
- if not prompt or not prompt.strip():
364
- gr.Warning("Please enter a prompt")
365
- return None
366
-
367
- try:
368
- gr.Info("Uploading image...")
369
- image_url = upload_image(api_key, image_path)
370
-
371
- gr.Info("Processing...")
372
- payload = {
373
- "images": [image_url],
374
- "prompt": prompt.strip(),
375
- "seed": 131192700,
376
- "enable_sync_mode": False,
377
- "structured_prompt": "",
378
- "enable_base64_output": False,
379
- }
380
- result = call_api(api_key, MODEL_ENDPOINT, payload)
381
- request_id = result.get("id")
382
- if not request_id:
383
- gr.Warning("Failed to start")
384
- return None
385
-
386
- final_result = poll_result(api_key, request_id)
387
- outputs = final_result.get("outputs", [])
388
- if outputs:
389
- gr.Info("Done!")
390
- return outputs[0]
391
- return None
392
-
393
- except Exception as e:
394
- error_msg = str(e).lower()
395
- if "invalid" in error_msg or "401" in error_msg:
396
- gr.Warning("Invalid API Key")
397
- elif "quota" in error_msg or "429" in error_msg:
398
- gr.Warning("Quota exceeded")
399
- elif "timeout" in error_msg:
400
- gr.Warning("Timeout - please try again")
401
- else:
402
- gr.Warning(str(e))
403
- return None
404
-
405
-
406
- # ============ Gradio UI ============
407
- with gr.Blocks(css=CUSTOM_CSS, title="Fibo Edit - WaveSpeed") as demo:
408
-
409
- # Hero Section
410
- gr.HTML("""
411
- <div class="hero-container">
412
- <div class="hero-badge">BRIA x WAVESPEED</div>
413
- <h1 class="hero-title">Fibo Edit</h1>
414
- <p class="hero-desc">AI-powered image editing with Bria Image 3.2.</p>
415
- <div class="hero-badges">
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
- Fast Processing
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
- High Quality
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
- Easy to Use
427
- </span>
428
- </div>
429
- </div>
430
- """)
431
-
432
-
433
-
434
- # API Key Card
435
- with gr.Group(elem_classes="main-card"):
436
- gr.HTML("""
437
- <div class="api-key-row">
438
- <span class="api-key-label">
439
- <svg width="20" height="20" fill="#10b981" 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>
440
- API Key
441
- </span>
442
- <a href="https://wavespeed.ai/accesskey" target="_blank" class="get-key-btn">Get API Key</a>
443
- </div>
444
- """)
445
- api_key_input = gr.Textbox(
446
- placeholder="Enter your WaveSpeed API key",
447
- type="password",
448
- show_label=False
449
- )
450
-
451
- # Main Content Card
452
- with gr.Group(elem_classes="main-card"):
453
- with gr.Row():
454
- # Left Column - Input
455
- with gr.Column(scale=1):
456
- gr.HTML("""
457
- <div class="section-title">
458
- <svg width="20" height="20" fill="#10b981" 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>
459
- Upload Image
460
- </div>
461
- """)
462
- image_input = gr.Image(
463
- label="Upload Image",
464
- type="filepath",
465
- elem_classes=["upload-area"]
466
- )
467
- prompt_input = gr.Textbox(
468
- label="Prompt",
469
- placeholder="Describe what you want...",
470
- lines=3
471
- )
472
- submit_btn = gr.Button("Process", variant="primary", elem_classes="primary-btn")
473
-
474
- # Right Column - Output
475
- with gr.Column(scale=1):
476
- gr.HTML("""
477
- <div class="section-title">
478
- <svg width="20" height="20" fill="#10b981" 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>
479
- Result
480
- </div>
481
- """)
482
- output_image = gr.Image(
483
- label="",
484
- type="filepath",
485
- interactive=False,
486
- elem_classes=["hide-label", "result-area"]
487
- )
488
-
489
- # CTA Section
490
- gr.HTML("""
491
- <div class="cta-container">
492
- <h3 class="cta-title">Want More Features?</h3>
493
- <p class="cta-desc">Higher resolutions, batch processing, and 700+ AI models</p>
494
- <a href="https://wavespeed.ai/models" target="_blank" class="cta-btn">
495
- Explore WaveSpeed.ai
496
- </a>
497
- </div>
498
- """)
499
-
500
- # Event binding
501
- submit_btn.click(
502
- fn=process,
503
- inputs=[api_key_input, image_input, prompt_input],
504
- outputs=output_image,
505
- )
506
-
507
-
508
- if __name__ == "__main__":
509
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
+ """
2
+ Fibo 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 = "bria/fibo/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, #10b981, #059669);
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%, #10b981 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, #10b981, #059669);
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, #10b981, #059669) !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, #10b981 0%, #059669 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: #059669 !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: #10b981 !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
+ print(f"[DEBUG] Payload: {payload}")
300
+ resp = requests.post(f"{WAVESPEED_API_BASE}/{endpoint}", json=payload, headers=headers, timeout=30)
301
+ print(f"[DEBUG] Status: {resp.status_code}")
302
+ print(f"[DEBUG] Response: {resp.text[:500]}")
303
+ if resp.status_code == 401:
304
+ raise Exception("Invalid API Key")
305
+ elif resp.status_code == 429:
306
+ raise Exception("Quota exceeded")
307
+ elif resp.status_code >= 400:
308
+ raise Exception(f"API error {resp.status_code}: {resp.text[:200]}")
309
+ data = resp.json()
310
+ if data.get("code") != 200:
311
+ raise Exception(data.get("message", "Unknown error"))
312
+ return data.get("data", {})
313
+
314
+
315
+ def poll_result(api_key: str, request_id: str) -> Dict[str, Any]:
316
+ headers = {"Authorization": f"Bearer {api_key.strip()}"}
317
+ url = f"{WAVESPEED_API_BASE}/predictions/{request_id}/result"
318
+ start_time = time.time()
319
+ while time.time() - start_time < POLL_MAX_SECONDS:
320
+ resp = requests.get(url, headers=headers, timeout=30)
321
+ if resp.status_code >= 400:
322
+ raise Exception("Failed to get result")
323
+ result = resp.json().get("data", {})
324
+ status = result.get("status", "")
325
+ if status == "completed":
326
+ return result
327
+ elif status == "failed":
328
+ raise Exception("Generation failed")
329
+ time.sleep(POLL_INTERVAL)
330
+ raise Exception("Timeout")
331
+
332
+
333
+ def download_video(url: str) -> str:
334
+ """下载视频到临时文件,返回文件路径"""
335
+ import tempfile
336
+ import os
337
+ try:
338
+ resp = requests.get(url, timeout=120, stream=True)
339
+ if resp.status_code != 200:
340
+ return None
341
+ # 从 URL 或 Content-Type 确定扩展名
342
+ ext = ".mp4"
343
+ if "webm" in url.lower() or "webm" in resp.headers.get("content-type", ""):
344
+ ext = ".webm"
345
+ # 创建临时文件
346
+ fd, path = tempfile.mkstemp(suffix=ext)
347
+ with os.fdopen(fd, 'wb') as f:
348
+ for chunk in resp.iter_content(chunk_size=8192):
349
+ f.write(chunk)
350
+ return path
351
+ except Exception as e:
352
+ print(f"Download error: {e}")
353
+ return None
354
+
355
+
356
+ def process(api_key: str, image_path: str, prompt: str):
357
+ if not api_key or not api_key.strip():
358
+ gr.Warning("Please enter your API Key")
359
+ return None
360
+ if not image_path:
361
+ gr.Warning("Please upload an image")
362
+ return None
363
+ if not prompt or not prompt.strip():
364
+ gr.Warning("Please enter a prompt")
365
+ return None
366
+
367
+ try:
368
+ gr.Info("Uploading image...")
369
+ image_url = upload_image(api_key, image_path)
370
+
371
+ gr.Info("Processing...")
372
+ payload = {
373
+ "images": [image_url],
374
+ "prompt": prompt.strip(),
375
+ "seed": 131192700,
376
+ "enable_sync_mode": False,
377
+ "structured_prompt": "",
378
+ "enable_base64_output": False,
379
+ }
380
+ result = call_api(api_key, MODEL_ENDPOINT, payload)
381
+ request_id = result.get("id")
382
+ if not request_id:
383
+ gr.Warning("Failed to start")
384
+ return None
385
+
386
+ final_result = poll_result(api_key, request_id)
387
+ outputs = final_result.get("outputs", [])
388
+ if outputs:
389
+ gr.Info("Done!")
390
+ return outputs[0]
391
+ return None
392
+
393
+ except Exception as e:
394
+ error_msg = str(e).lower()
395
+ if "invalid" in error_msg or "401" in error_msg:
396
+ gr.Warning("Invalid API Key")
397
+ elif "quota" in error_msg or "429" in error_msg:
398
+ gr.Warning("Quota exceeded")
399
+ elif "timeout" in error_msg:
400
+ gr.Warning("Timeout - please try again")
401
+ else:
402
+ gr.Warning(str(e))
403
+ return None
404
+
405
+
406
+ # ============ Gradio UI ============
407
+ with gr.Blocks(css=CUSTOM_CSS, title="Fibo Edit - WaveSpeed") as demo:
408
+
409
+ # Hero Section
410
+ gr.HTML("""
411
+ <div class="hero-container">
412
+ <div class="hero-badge">BRIA x WAVESPEED</div>
413
+ <h1 class="hero-title">Fibo Edit</h1>
414
+ <p class="hero-desc">AI-powered image editing with Bria Image 3.2.</p>
415
+ <div class="hero-badges">
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
+ Fast Processing
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
+ High Quality
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
+ Easy to Use
427
+ </span>
428
+ </div>
429
+ </div>
430
+ """)
431
+
432
+
433
+
434
+ # API Key Card
435
+ with gr.Group(elem_classes="main-card"):
436
+ gr.HTML("""
437
+ <div class="api-key-row">
438
+ <span class="api-key-label">
439
+ <svg width="20" height="20" fill="#10b981" 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>
440
+ API Key
441
+ </span>
442
+ <a href="https://wavespeed.ai/accesskey" target="_blank" class="get-key-btn">Get API Key</a>
443
+ </div>
444
+ """)
445
+ api_key_input = gr.Textbox(
446
+ placeholder="Enter your WaveSpeed API key",
447
+ type="password",
448
+ show_label=False
449
+ )
450
+
451
+ # Main Content Card
452
+ with gr.Group(elem_classes="main-card"):
453
+ with gr.Row():
454
+ # Left Column - Input
455
+ with gr.Column(scale=1):
456
+ gr.HTML("""
457
+ <div class="section-title">
458
+ <svg width="20" height="20" fill="#10b981" 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>
459
+ Upload Image
460
+ </div>
461
+ """)
462
+ image_input = gr.Image(
463
+ label="Upload Image",
464
+ type="filepath",
465
+ elem_classes=["upload-area"]
466
+ )
467
+ prompt_input = gr.Textbox(
468
+ label="Prompt",
469
+ placeholder="Describe what you want...",
470
+ lines=3
471
+ )
472
+ submit_btn = gr.Button("Process", variant="primary", elem_classes="primary-btn")
473
+
474
+ # Right Column - Output
475
+ with gr.Column(scale=1):
476
+ gr.HTML("""
477
+ <div class="section-title">
478
+ <svg width="20" height="20" fill="#10b981" 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>
479
+ Result
480
+ </div>
481
+ """)
482
+ output_image = gr.Image(
483
+ label="",
484
+ type="filepath",
485
+ interactive=False,
486
+ elem_classes=["hide-label", "result-area"]
487
+ )
488
+
489
+ # CTA Section
490
+ gr.HTML("""
491
+ <div class="cta-container">
492
+ <h3 class="cta-title">Want More Features?</h3>
493
+ <p class="cta-desc">Higher resolutions, batch processing, and 700+ AI models</p>
494
+ <a href="https://wavespeed.ai/models" target="_blank" class="cta-btn">
495
+ Explore WaveSpeed.ai
496
+ </a>
497
+ </div>
498
+ """)
499
+
500
+ # Event binding
501
+ submit_btn.click(
502
+ fn=process,
503
+ inputs=[api_key_input, image_input, prompt_input],
504
+ outputs=output_image,
505
+ )
506
+
507
+
508
+ if __name__ == "__main__":
509
+ demo.launch(server_name="0.0.0.0", server_port=7860)