|
|
"""
|
|
|
Fibo Edit - Powered by WaveSpeed AI
|
|
|
Auto-generated by Space Generator
|
|
|
"""
|
|
|
|
|
|
import gradio as gr
|
|
|
import requests
|
|
|
import time
|
|
|
from typing import Dict, Any
|
|
|
|
|
|
|
|
|
WAVESPEED_API_BASE = "https://api.wavespeed.ai/api/v3"
|
|
|
UPLOAD_ENDPOINT = "https://api.wavespeed.ai/api/v3/media/upload/binary"
|
|
|
MODEL_ENDPOINT = "bria/fibo/edit"
|
|
|
POLL_INTERVAL = 1.5
|
|
|
POLL_MAX_SECONDS = 120
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CUSTOM_CSS = """
|
|
|
/* ===== Base Styles ===== */
|
|
|
html, body, .gradio-container {
|
|
|
background: linear-gradient(145deg, #f5f3ff 0%, #ede9fe 50%, #e0e7ff 100%) !important;
|
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
|
|
|
}
|
|
|
|
|
|
.gradio-container {
|
|
|
max-width: 960px !important;
|
|
|
margin: 0 auto !important;
|
|
|
padding: 24px !important;
|
|
|
}
|
|
|
|
|
|
/* ===== Hero Section ===== */
|
|
|
.hero-container {
|
|
|
text-align: center;
|
|
|
padding: 48px 20px 36px;
|
|
|
}
|
|
|
|
|
|
.hero-badge {
|
|
|
display: inline-block;
|
|
|
background: linear-gradient(135deg, #10b981, #059669);
|
|
|
padding: 10px 24px;
|
|
|
border-radius: 50px;
|
|
|
font-size: 0.7rem;
|
|
|
color: #fff;
|
|
|
font-weight: 700;
|
|
|
letter-spacing: 1.5px;
|
|
|
margin-bottom: 20px;
|
|
|
box-shadow: 0 4px 20px rgba(139, 92, 246, 0.35);
|
|
|
}
|
|
|
|
|
|
.hero-title {
|
|
|
font-size: 3rem;
|
|
|
font-weight: 800;
|
|
|
margin: 0 0 16px 0;
|
|
|
background: linear-gradient(135deg, #6d28d9 0%, #10b981 50%, #a78bfa 100%);
|
|
|
-webkit-background-clip: text;
|
|
|
-webkit-text-fill-color: transparent;
|
|
|
background-clip: text;
|
|
|
letter-spacing: -0.5px;
|
|
|
}
|
|
|
|
|
|
.hero-desc {
|
|
|
font-size: 1.05rem;
|
|
|
color: #64748b;
|
|
|
max-width: 100%;
|
|
|
margin: 0 auto 20px;
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.hero-badges {
|
|
|
display: flex;
|
|
|
gap: 28px;
|
|
|
justify-content: center;
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
.hero-badges span {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
color: #475569;
|
|
|
font-size: 0.9rem;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
/* ===== Hero Image ===== */
|
|
|
.hero-image {
|
|
|
max-width: 100%;
|
|
|
max-height: 300px;
|
|
|
border-radius: 16px;
|
|
|
margin: 24px auto;
|
|
|
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.15);
|
|
|
}
|
|
|
|
|
|
/* ===== Main Card ===== */
|
|
|
.main-card {
|
|
|
background: #ffffff;
|
|
|
border: 1px solid rgba(139, 92, 246, 0.1);
|
|
|
border-radius: 20px;
|
|
|
padding: 28px;
|
|
|
margin-bottom: 20px;
|
|
|
box-shadow: 0 4px 24px rgba(139, 92, 246, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);
|
|
|
transition: box-shadow 0.3s ease;
|
|
|
}
|
|
|
|
|
|
.main-card:hover {
|
|
|
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.12), 0 2px 6px rgba(0, 0, 0, 0.04);
|
|
|
}
|
|
|
|
|
|
/* ===== API Key Section ===== */
|
|
|
.api-key-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
margin-bottom: 12px;
|
|
|
}
|
|
|
|
|
|
.api-key-label {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
color: #1e293b;
|
|
|
font-weight: 700;
|
|
|
font-size: 1rem;
|
|
|
}
|
|
|
|
|
|
.get-key-btn {
|
|
|
padding: 10px 20px;
|
|
|
background: linear-gradient(135deg, #10b981, #059669);
|
|
|
border: none;
|
|
|
border-radius: 10px;
|
|
|
color: #fff !important;
|
|
|
text-decoration: none;
|
|
|
font-weight: 600;
|
|
|
font-size: 0.85rem;
|
|
|
transition: all 0.25s ease;
|
|
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
|
|
}
|
|
|
|
|
|
.get-key-btn:hover {
|
|
|
transform: translateY(-2px);
|
|
|
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
|
|
|
color: #fff;
|
|
|
}
|
|
|
|
|
|
/* ===== Section Title ===== */
|
|
|
.section-title {
|
|
|
color: #1e293b;
|
|
|
font-weight: 700;
|
|
|
font-size: 1rem;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
margin-bottom: 16px;
|
|
|
}
|
|
|
|
|
|
/* ===== Upload Area ===== */
|
|
|
.upload-area {
|
|
|
border: 2px dashed rgba(139, 92, 246, 0.3) !important;
|
|
|
border-radius: 16px !important;
|
|
|
background: linear-gradient(145deg, #faf5ff 0%, #f5f3ff 100%) !important;
|
|
|
transition: all 0.3s ease !important;
|
|
|
min-height: 220px !important;
|
|
|
}
|
|
|
|
|
|
.upload-area:hover {
|
|
|
border-color: rgba(139, 92, 246, 0.5) !important;
|
|
|
background: linear-gradient(145deg, #f5f3ff 0%, #ede9fe 100%) !important;
|
|
|
}
|
|
|
|
|
|
/* ===== Result Area ===== */
|
|
|
.result-area {
|
|
|
border: 2px solid rgba(139, 92, 246, 0.15) !important;
|
|
|
border-radius: 16px !important;
|
|
|
background: #fafafa !important;
|
|
|
min-height: 220px !important;
|
|
|
}
|
|
|
|
|
|
/* ===== Button Styling ===== */
|
|
|
.primary-btn {
|
|
|
width: 100%;
|
|
|
margin-top: 20px !important;
|
|
|
background: linear-gradient(135deg, #10b981, #059669) !important;
|
|
|
border: none !important;
|
|
|
color: #fff !important;
|
|
|
font-weight: 700 !important;
|
|
|
font-size: 1rem !important;
|
|
|
padding: 14px 28px !important;
|
|
|
border-radius: 12px !important;
|
|
|
box-shadow: 0 4px 16px rgba(139, 92, 246, 0.35) !important;
|
|
|
transition: all 0.25s ease !important;
|
|
|
cursor: pointer !important;
|
|
|
}
|
|
|
|
|
|
.primary-btn:hover {
|
|
|
transform: translateY(-2px) !important;
|
|
|
box-shadow: 0 8px 24px rgba(139, 92, 246, 0.45) !important;
|
|
|
}
|
|
|
|
|
|
/* ===== CTA Section ===== */
|
|
|
.cta-container {
|
|
|
text-align: center;
|
|
|
padding: 44px 32px;
|
|
|
background: linear-gradient(135deg, #10b981 0%, #059669 50%, #6d28d9 100%);
|
|
|
border-radius: 20px;
|
|
|
margin-top: 8px;
|
|
|
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.35);
|
|
|
position: relative;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.cta-container::before {
|
|
|
content: '';
|
|
|
position: absolute;
|
|
|
top: -50%;
|
|
|
right: -50%;
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
|
|
|
pointer-events: none;
|
|
|
}
|
|
|
|
|
|
.cta-title {
|
|
|
color: #fff;
|
|
|
font-size: 1.5rem;
|
|
|
font-weight: 800;
|
|
|
margin: 0 0 8px 0;
|
|
|
position: relative;
|
|
|
}
|
|
|
|
|
|
.cta-desc {
|
|
|
color: rgba(255, 255, 255, 0.9);
|
|
|
font-size: 1rem;
|
|
|
margin: 0 0 24px 0;
|
|
|
position: relative;
|
|
|
}
|
|
|
|
|
|
.cta-btn {
|
|
|
display: inline-block;
|
|
|
padding: 14px 36px;
|
|
|
background: #fff;
|
|
|
border-radius: 12px;
|
|
|
color: #059669 !important;
|
|
|
text-decoration: none;
|
|
|
font-weight: 700;
|
|
|
font-size: 1rem;
|
|
|
transition: all 0.25s ease;
|
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
|
position: relative;
|
|
|
}
|
|
|
|
|
|
.cta-btn:hover {
|
|
|
transform: translateY(-3px);
|
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
|
|
color: #6d28d9 !important;
|
|
|
}
|
|
|
|
|
|
/* ===== Hide Elements ===== */
|
|
|
footer { display: none !important; }
|
|
|
|
|
|
/* ===== Input Styling ===== */
|
|
|
.gradio-container input[type="password"],
|
|
|
.gradio-container input[type="text"] {
|
|
|
border: 2px solid #e2e8f0 !important;
|
|
|
border-radius: 12px !important;
|
|
|
padding: 14px 16px !important;
|
|
|
font-size: 0.95rem !important;
|
|
|
transition: all 0.2s ease !important;
|
|
|
}
|
|
|
|
|
|
.gradio-container input[type="password"]:focus,
|
|
|
.gradio-container input[type="text"]:focus {
|
|
|
border-color: #10b981 !important;
|
|
|
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15) !important;
|
|
|
outline: none !important;
|
|
|
}
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def upload_image(api_key: str, file_path: str) -> str:
|
|
|
headers = {"Authorization": f"Bearer {api_key.strip()}"}
|
|
|
with open(file_path, "rb") as f:
|
|
|
resp = requests.post(UPLOAD_ENDPOINT, headers=headers, files={"file": f}, timeout=60)
|
|
|
if resp.status_code == 401:
|
|
|
raise Exception("Invalid API Key")
|
|
|
elif resp.status_code >= 400:
|
|
|
raise Exception(f"Upload failed: {resp.status_code}")
|
|
|
data = resp.json()
|
|
|
if data.get("code") != 200:
|
|
|
raise Exception(data.get("message", "Upload failed"))
|
|
|
return data.get("data", {}).get("download_url")
|
|
|
|
|
|
|
|
|
def call_api(api_key: str, endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
headers = {"Authorization": f"Bearer {api_key.strip()}", "Content-Type": "application/json"}
|
|
|
print(f"[DEBUG] Payload: {payload}")
|
|
|
resp = requests.post(f"{WAVESPEED_API_BASE}/{endpoint}", json=payload, headers=headers, timeout=30)
|
|
|
print(f"[DEBUG] Status: {resp.status_code}")
|
|
|
print(f"[DEBUG] Response: {resp.text[:500]}")
|
|
|
if resp.status_code == 401:
|
|
|
raise Exception("Invalid API Key")
|
|
|
elif resp.status_code == 429:
|
|
|
raise Exception("Quota exceeded")
|
|
|
elif resp.status_code >= 400:
|
|
|
raise Exception(f"API error {resp.status_code}: {resp.text[:200]}")
|
|
|
data = resp.json()
|
|
|
if data.get("code") != 200:
|
|
|
raise Exception(data.get("message", "Unknown error"))
|
|
|
return data.get("data", {})
|
|
|
|
|
|
|
|
|
def poll_result(api_key: str, request_id: str) -> Dict[str, Any]:
|
|
|
headers = {"Authorization": f"Bearer {api_key.strip()}"}
|
|
|
url = f"{WAVESPEED_API_BASE}/predictions/{request_id}/result"
|
|
|
start_time = time.time()
|
|
|
while time.time() - start_time < POLL_MAX_SECONDS:
|
|
|
resp = requests.get(url, headers=headers, timeout=30)
|
|
|
if resp.status_code >= 400:
|
|
|
raise Exception("Failed to get result")
|
|
|
result = resp.json().get("data", {})
|
|
|
status = result.get("status", "")
|
|
|
if status == "completed":
|
|
|
return result
|
|
|
elif status == "failed":
|
|
|
raise Exception("Generation failed")
|
|
|
time.sleep(POLL_INTERVAL)
|
|
|
raise Exception("Timeout")
|
|
|
|
|
|
|
|
|
def download_video(url: str) -> str:
|
|
|
"""下载视频到临时文件,返回文件路径"""
|
|
|
import tempfile
|
|
|
import os
|
|
|
try:
|
|
|
resp = requests.get(url, timeout=120, stream=True)
|
|
|
if resp.status_code != 200:
|
|
|
return None
|
|
|
|
|
|
ext = ".mp4"
|
|
|
if "webm" in url.lower() or "webm" in resp.headers.get("content-type", ""):
|
|
|
ext = ".webm"
|
|
|
|
|
|
fd, path = tempfile.mkstemp(suffix=ext)
|
|
|
with os.fdopen(fd, 'wb') as f:
|
|
|
for chunk in resp.iter_content(chunk_size=8192):
|
|
|
f.write(chunk)
|
|
|
return path
|
|
|
except Exception as e:
|
|
|
print(f"Download error: {e}")
|
|
|
return None
|
|
|
|
|
|
|
|
|
def process(api_key: str, image_path: str, prompt: str):
|
|
|
if not api_key or not api_key.strip():
|
|
|
gr.Warning("Please enter your API Key")
|
|
|
return None
|
|
|
if not image_path:
|
|
|
gr.Warning("Please upload an image")
|
|
|
return None
|
|
|
if not prompt or not prompt.strip():
|
|
|
gr.Warning("Please enter a prompt")
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
gr.Info("Uploading image...")
|
|
|
image_url = upload_image(api_key, image_path)
|
|
|
|
|
|
gr.Info("Processing...")
|
|
|
payload = {
|
|
|
"images": [image_url],
|
|
|
"prompt": prompt.strip(),
|
|
|
"seed": 131192700,
|
|
|
"enable_sync_mode": False,
|
|
|
"structured_prompt": "",
|
|
|
"enable_base64_output": False,
|
|
|
}
|
|
|
result = call_api(api_key, MODEL_ENDPOINT, payload)
|
|
|
request_id = result.get("id")
|
|
|
if not request_id:
|
|
|
gr.Warning("Failed to start")
|
|
|
return None
|
|
|
|
|
|
final_result = poll_result(api_key, request_id)
|
|
|
outputs = final_result.get("outputs", [])
|
|
|
if outputs:
|
|
|
gr.Info("Done!")
|
|
|
return outputs[0]
|
|
|
return None
|
|
|
|
|
|
except Exception as e:
|
|
|
error_msg = str(e).lower()
|
|
|
if "invalid" in error_msg or "401" in error_msg:
|
|
|
gr.Warning("Invalid API Key")
|
|
|
elif "quota" in error_msg or "429" in error_msg:
|
|
|
gr.Warning("Quota exceeded")
|
|
|
elif "timeout" in error_msg:
|
|
|
gr.Warning("Timeout - please try again")
|
|
|
else:
|
|
|
gr.Warning(str(e))
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks(css=CUSTOM_CSS, title="Fibo Edit - WaveSpeed") as demo:
|
|
|
|
|
|
|
|
|
gr.HTML("""
|
|
|
<div class="hero-container">
|
|
|
<div class="hero-badge">BRIA x WAVESPEED</div>
|
|
|
<h1 class="hero-title">Fibo Edit</h1>
|
|
|
<p class="hero-desc">AI-powered image editing with Bria Image 3.2. Try on <a href="https://wavespeed.ai/models?utm_source=huggingface_space_fibo_edit" target="_blank" style="color: #8b5cf6; text-decoration: none; font-weight: 600;">wavespeed</a></p>
|
|
|
<div class="hero-badges">
|
|
|
<span>
|
|
|
<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>
|
|
|
Fast Processing
|
|
|
</span>
|
|
|
<span>
|
|
|
<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>
|
|
|
High Quality
|
|
|
</span>
|
|
|
<span>
|
|
|
<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>
|
|
|
Easy to Use
|
|
|
</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with gr.Group(elem_classes="main-card"):
|
|
|
gr.HTML("""
|
|
|
<div class="api-key-row">
|
|
|
<span class="api-key-label">
|
|
|
<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>
|
|
|
API Key
|
|
|
</span>
|
|
|
<a href="https://wavespeed.ai/models?utm_source=huggingface_space_fibo_edit" target="_blank" class="get-key-btn">Get API Key</a>
|
|
|
</div>
|
|
|
""")
|
|
|
api_key_input = gr.Textbox(
|
|
|
placeholder="Enter your WaveSpeed API key",
|
|
|
type="password",
|
|
|
show_label=False
|
|
|
)
|
|
|
|
|
|
|
|
|
with gr.Group(elem_classes="main-card"):
|
|
|
with gr.Row():
|
|
|
|
|
|
with gr.Column(scale=1):
|
|
|
gr.HTML("""
|
|
|
<div class="section-title">
|
|
|
<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>
|
|
|
Upload Image
|
|
|
</div>
|
|
|
""")
|
|
|
image_input = gr.Image(
|
|
|
label="Upload Image",
|
|
|
type="filepath",
|
|
|
source="upload",
|
|
|
elem_classes=["upload-area"]
|
|
|
)
|
|
|
prompt_input = gr.Textbox(
|
|
|
label="Prompt",
|
|
|
placeholder="Describe what you want...",
|
|
|
lines=3
|
|
|
)
|
|
|
submit_btn = gr.Button("Process", variant="primary", elem_classes="primary-btn")
|
|
|
|
|
|
|
|
|
with gr.Column(scale=1):
|
|
|
gr.HTML("""
|
|
|
<div class="section-title">
|
|
|
<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>
|
|
|
Result
|
|
|
</div>
|
|
|
""")
|
|
|
output_image = gr.Image(
|
|
|
label="",
|
|
|
type="filepath",
|
|
|
interactive=False,
|
|
|
elem_classes=["hide-label", "result-area"]
|
|
|
)
|
|
|
|
|
|
|
|
|
gr.HTML("""
|
|
|
<div class="cta-container">
|
|
|
<h3 class="cta-title">Want More Features?</h3>
|
|
|
<p class="cta-desc">Higher resolutions, batch processing, and 700+ AI models</p>
|
|
|
<a href="https://wavespeed.ai/models?utm_source=huggingface_space_fibo_edit" target="_blank" class="cta-btn">
|
|
|
Explore WaveSpeed.ai
|
|
|
</a>
|
|
|
</div>
|
|
|
""")
|
|
|
|
|
|
|
|
|
submit_btn.click(
|
|
|
fn=process,
|
|
|
inputs=[api_key_input, image_input, prompt_input],
|
|
|
outputs=output_image,
|
|
|
)
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
demo.launch(server_name="0.0.0.0", server_port=7860)
|
|
|
|