wavespeed/bria-expand
0
1"""Builds a Space UI from a declarative spec.2 3Shared verbatim by every Space in the wavespeed org. Generated from4_shared/spaceapp/ — edit there and re-run _shared/build_apps.py.5 6Handling of the user's API key7------------------------------8The key is typed into the browser and travels to this server on each request.9Keeping it from leaking takes more than masking the textbox, because Gradio has10several features that will happily persist or republish an input:11 12 * `type="password"` - not echoed back into the DOM.13 * `api_visibility="private"` - Gradio 6 otherwise documents this event on the14 app's public API page, generating client15 snippets that include every input, the key16 among them.17 * `analytics_enabled=False` - on Blocks, plus the env vars set in app.py18 before gradio is imported.19 * no `gr.State`/`gr.Examples` ever holds the key, so it is not serialised20 into the page or cached to disk.21 * every error is passed through `wavespeed.redact` before display, because a22 requests exception can stringify the Authorization header.23 24Gradio's flagging feature (which writes raw inputs to a CSV) belongs to25gr.Interface; this UI is built from gr.Blocks, which has no flagging, so there26is nothing to switch off there.27 28The key is a plain function argument: it lives for the duration of one request29and is not retained between them.30"""31 32from __future__ import annotations33 34import gradio as gr35 36import wavespeed as ws37 38SITE = "https://wavespeed.ai"39 40 41def link(path: str, campaign: str) -> str:42 """Build an outbound wavespeed.ai URL carrying UTM attribution.43 44 Without these, traffic from Hugging Face lands in analytics as plain45 referral with no way to tell which Space produced it.46 """47 sep = "&" if "?" in path else "?"48 return (49 f"{SITE}{path}{sep}utm_source=huggingface&utm_medium=space"50 f"&utm_campaign={campaign}"51 )52 53 54def _collect(spec, key, values, progress):55 """Turn UI values into an API payload, uploading any local files first."""56 payload = dict(spec.get("extra", {}))57 for field, value in zip(spec["fields"], values):58 kind, api_key_name = field["kind"], field["key"]59 60 if kind in ("image", "audio", "video"):61 if not value:62 if field.get("required", True):63 raise ws.WaveSpeedError(f"{field['label']} is required.")64 continue65 progress(0.1, desc=f"Uploading {field['label'].lower()}…")66 payload[api_key_name] = ws.upload(key, value)67 68 elif kind == "images":69 if not value:70 if field.get("required", True):71 raise ws.WaveSpeedError(f"{field['label']} is required.")72 continue73 progress(0.1, desc=f"Uploading {field['label'].lower()}…")74 payload[api_key_name] = [ws.upload(key, value)]75 76 elif kind == "prompt":77 text = (value or "").strip()78 if not text and field.get("required", True):79 raise ws.WaveSpeedError("Enter a prompt.")80 if text:81 payload[api_key_name] = text82 83 elif kind == "seed":84 # -1 means "let the service choose"; sending it would pin the seed.85 if value is not None and int(value) >= 0:86 payload[api_key_name] = int(value)87 88 elif value is not None and value != "":89 payload[api_key_name] = value90 91 return payload92 93 94def build(spec):95 """Return a configured gr.Blocks for this Space."""96 css = spec["css"]97 camp = spec["campaign"]98 outputs_are_video = spec["output"] == "video"99 100 with gr.Blocks(101 title=f"{spec['title']} - WaveSpeed AI",102 analytics_enabled=False,103 ) as demo:104 gr.HTML(105 f"""106 <div class="hero-container">107 <a class="hero-badge" href="{link('/', camp)}"108 target="_blank" rel="noopener">WAVESPEED AI</a>109 <h1 class="hero-title">{spec['title']}</h1>110 <p class="hero-desc">{spec['tagline']}</p>111 </div>112 """113 )114 115 with gr.Row(elem_classes="api-key-row"):116 api_key = gr.Textbox(117 label="WaveSpeed API key",118 placeholder="Paste your API key — it is used for this request only",119 type="password", # never echoed back to the page120 show_label=False,121 container=False,122 scale=4,123 )124 gr.HTML(125 f'<a class="get-key-btn" href="{link("/dashboard", camp)}" '126 'target="_blank" rel="noopener">Get a key</a>'127 )128 gr.Markdown(129 "Your key is sent only to `api.wavespeed.ai` to run this model. "130 "It is not stored, logged, or shared, and generations are billed to "131 "your own account.",132 elem_classes="key-note",133 )134 135 controls = []136 with gr.Row():137 with gr.Column(scale=1):138 for f in spec["fields"]:139 controls.append(_make_control(f))140 run_btn = gr.Button(141 spec.get("button", "Generate"),142 variant="primary",143 elem_classes="primary-btn",144 )145 with gr.Column(scale=1):146 if spec["output"] == "compare":147 outs = [148 gr.Image(label=m["label"], type="filepath")149 for m in spec["compare"]150 ]151 elif outputs_are_video:152 outs = [gr.Video(label="Result")]153 else:154 outs = [gr.Image(label="Result", type="filepath")]155 156 gr.HTML(157 f"""158 <div class="cta-container">159 <p class="cta-desc">Runs160 <a href="{link('/models/' + spec['model'], camp)}"161 target="_blank" rel="noopener"><code>{spec['model']}</code></a>162 on WaveSpeed ·163 <a href="{link('/models', camp)}" target="_blank"164 rel="noopener">Browse all models</a> ·165 <a href="{link('/docs', camp)}" target="_blank"166 rel="noopener">API docs</a>167 </p>168 </div>169 """170 )171 172 def _run(key, *values, progress=gr.Progress()):173 blank = [None] * len(outs)174 if not key or not key.strip():175 gr.Warning("Enter your WaveSpeed API key first.")176 return blank[0] if len(blank) == 1 else tuple(blank)177 try:178 payload = _collect(spec, key, values, progress)179 progress(0.3, desc="Submitting…")180 if spec["output"] == "compare":181 models = spec["compare"]182 results = []183 for i, m in enumerate(models):184 progress(185 0.3 + 0.6 * i / len(models),186 desc=f"Running {m['label']}…",187 )188 merged = dict(payload, **m.get("extra", {}))189 results.append(ws.run(key, m["model"], merged)[0])190 return tuple(results)191 outputs = ws.run(192 key, spec["model"], payload,193 on_tick=lambda s: progress(0.6, desc=f"Generating ({s})…"),194 )195 return outputs[0]196 except ws.WaveSpeedError as e:197 # Message is already redacted by the client.198 gr.Warning(str(e))199 except Exception as e: # noqa: BLE001 - never surface a raw trace200 gr.Warning(ws.redact(f"Unexpected error: {e}", key))201 return blank[0] if len(blank) == 1 else tuple(blank)202 203 run_btn.click(204 _run,205 inputs=[api_key, *controls],206 outputs=outs,207 # Keep this event off the public API page — its generated snippets208 # would include the api_key input.209 api_visibility="private",210 )211 212 return demo213 214 215def _make_control(f):216 # seed fields carry no explicit label; they get the default below.217 kind, label = f["kind"], f.get("label", "")218 if kind == "prompt":219 return gr.Textbox(220 label=label, placeholder=f.get("placeholder", ""),221 lines=f.get("lines", 3),222 )223 if kind == "image":224 return gr.Image(label=label, type="filepath")225 if kind == "images":226 return gr.Image(label=label, type="filepath")227 if kind == "audio":228 return gr.Audio(label=label, type="filepath")229 if kind == "video":230 return gr.Video(label=label)231 if kind == "choice":232 return gr.Dropdown(233 label=label, choices=f["choices"], value=f.get("default", f["choices"][0])234 )235 if kind == "bool":236 return gr.Checkbox(label=label, value=f.get("default", False))237 if kind == "seed":238 return gr.Number(label=f.get("label", "Seed (-1 = random)"), value=-1, precision=0)239 if kind == "slider":240 return gr.Slider(241 label=label, minimum=f["min"], maximum=f["max"],242 step=f.get("step", 1), value=f["default"],243 )244 raise ValueError(f"unknown field kind: {kind}")245 246 247def launch(demo, css):248 """Launch the app. Gradio 6 takes css here rather than on Blocks."""249 demo.launch(250 server_name="0.0.0.0",251 server_port=7860,252 css=css,253 quiet=True,254 )255 