CoolFace
Apppublic

wavespeed/fibo-edit

sourceHugging Facemitupdated 1mo agoView on Hugging Face
0likes
wavespeed.py152 linesDownload Raw Back to root
1"""Minimal WaveSpeed v3 API client.2 3Shared verbatim by every Space in the wavespeed org. Generated from4_shared/spaceapp/ — edit there and re-run _shared/build_apps.py, never edit the5copy inside a Space.6 7The user supplies their own API key through the UI. It is used to sign requests8to api.wavespeed.ai and nothing else: it is never logged, never written to9disk, never placed in a Gradio component value, and is stripped out of every10error message before that message can reach a browser (see `redact`). An11exception raised by `requests` can carry the full request headers in its text,12which is exactly how a key ends up in a user-visible traceback, so every raise13in this module goes through `redact` first.14 15API reference: https://wavespeed.ai/docs/rest-api16"""17 18from __future__ import annotations19 20import time21from typing import Any22 23import requests24 25API_BASE = "https://api.wavespeed.ai/api/v3"26UPLOAD_URL = f"{API_BASE}/media/upload/binary"27 28# The docs ask for >= 2s between polls of the same task, easing toward 5-10s29# for long jobs. Anything faster risks being throttled.30POLL_START = 2.031POLL_MAX = 8.032POLL_GROWTH = 1.2533POLL_TIMEOUT = 60034 35TERMINAL_OK = "completed"36TERMINAL_BAD = ("failed", "cancelled", "timeout")37 38 39class WaveSpeedError(Exception):40    """User-facing error. The message is always key-free."""41 42 43def redact(text: Any, key: str | None) -> str:44    """Remove the API key (and any bearer token) from text headed for a user."""45    s = str(text)46    if key:47        k = key.strip()48        if k:49            s = s.replace(k, "***")50            # Defend against a partially-quoted key in a repr.51            if len(k) > 12:52                s = s.replace(k[:12], "***")53    # Catch any Authorization header echoed by a library.54    import re55 56    s = re.sub(r"(?i)(bearer\s+)[A-Za-z0-9._\-]+", r"\1***", s)57    s = re.sub(r"(?i)('authorization':\s*')[^']*", r"\1***", s)58    return s59 60 61def _headers(key: str, json: bool = False) -> dict:62    h = {"Authorization": f"Bearer {key.strip()}"}63    if json:64        h["Content-Type"] = "application/json"65    return h66 67 68def _check(resp: requests.Response, key: str) -> dict:69    if resp.status_code == 401:70        raise WaveSpeedError("Invalid API key. Check the key and try again.")71    if resp.status_code == 402:72        raise WaveSpeedError("This account is out of credit.")73    if resp.status_code == 429:74        raise WaveSpeedError("Rate limit or quota exceeded. Wait and retry.")75    if resp.status_code >= 400:76        raise WaveSpeedError(77            redact(f"API error {resp.status_code}: {resp.text[:300]}", key)78        )79    try:80        body = resp.json()81    except ValueError:82        raise WaveSpeedError("API returned a non-JSON response.") from None83    if body.get("code") != 200:84        raise WaveSpeedError(redact(body.get("message", "Unknown API error"), key))85    return body.get("data", {}) or {}86 87 88def upload(key: str, path: str) -> str:89    """Upload a local file, returning the URL to reference it by."""90    try:91        with open(path, "rb") as fh:92            resp = requests.post(93                UPLOAD_URL, headers=_headers(key), files={"file": fh}, timeout=12094            )95    except requests.RequestException as e:96        raise WaveSpeedError(redact(f"Upload failed: {e}", key)) from None97    data = _check(resp, key)98    url = data.get("download_url") or data.get("url")99    if not url:100        raise WaveSpeedError("Upload succeeded but returned no URL.")101    return url102 103 104def submit(key: str, model: str, payload: dict) -> str:105    """Start a job and return its request id.106 107    Deliberately not retried: the docs warn that repeating a POST can bill the108    caller twice. Only the GET poll below is safe to retry.109    """110    try:111        resp = requests.post(112            f"{API_BASE}/{model}", headers=_headers(key, json=True), json=payload, timeout=60113        )114    except requests.RequestException as e:115        raise WaveSpeedError(redact(f"Could not reach the API: {e}", key)) from None116    data = _check(resp, key)117    rid = data.get("id")118    if not rid:119        raise WaveSpeedError("API accepted the request but returned no task id.")120    return rid121 122 123def poll(key: str, request_id: str, on_tick=None) -> list[str]:124    """Poll a task to completion and return its output URLs."""125    url = f"{API_BASE}/predictions/{request_id}/result"126    deadline = time.time() + POLL_TIMEOUT127    delay = POLL_START128    while time.time() < deadline:129        time.sleep(delay)130        delay = min(delay * POLL_GROWTH, POLL_MAX)131        try:132            resp = requests.get(url, headers=_headers(key), timeout=60)133        except requests.RequestException:134            continue  # transient; a GET is safe to repeat135        data = _check(resp, key)136        status = data.get("status", "")137        if status == TERMINAL_OK:138            outputs = data.get("outputs") or []139            if not outputs:140                raise WaveSpeedError("Generation finished but produced no output.")141            return outputs142        if status in TERMINAL_BAD:143            detail = redact(data.get("error") or status, key)144            raise WaveSpeedError(f"Generation {status}: {detail}")145        if on_tick:146            on_tick(status)147    raise WaveSpeedError("Timed out waiting for the result. The job may still finish.")148 149 150def run(key: str, model: str, payload: dict, on_tick=None) -> list[str]:151    return poll(key, submit(key, model, payload), on_tick=on_tick)152