Add cloud sprite providers and browser demos
This commit is contained in:
+110
@@ -0,0 +1,110 @@
|
||||
"""Minimal PixelLab v2 client for style-consistent pixel-art generation."""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from io import BytesIO
|
||||
from typing import Any, Optional
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
API_URL = "https://api.pixellab.ai/v2"
|
||||
|
||||
|
||||
class PixelLabError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class PixelLabClient:
|
||||
def __init__(self, api_key: Optional[str] = None, api_url: str = API_URL):
|
||||
self.api_key = api_key or os.environ.get("PIXELLAB_API_KEY")
|
||||
self.api_url = api_url.rstrip("/")
|
||||
|
||||
def _request(self, method: str, path: str, payload: Optional[dict] = None) -> dict:
|
||||
if not self.api_key:
|
||||
raise PixelLabError("PIXELLAB_API_KEY is not configured.")
|
||||
request = Request(
|
||||
f"{self.api_url}{path}",
|
||||
data=json.dumps(payload).encode() if payload else None,
|
||||
headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
|
||||
method=method,
|
||||
)
|
||||
try:
|
||||
with urlopen(request, timeout=125) as response:
|
||||
return json.loads(response.read().decode())
|
||||
except HTTPError as exc:
|
||||
raise PixelLabError(f"PixelLab API returned HTTP {exc.code}: {exc.read().decode(errors='replace')}") from exc
|
||||
except (URLError, TimeoutError) as exc:
|
||||
raise PixelLabError(f"Could not reach PixelLab API: {exc}") from exc
|
||||
|
||||
@staticmethod
|
||||
def _reference(value: str) -> dict:
|
||||
if value.startswith("data:"):
|
||||
raw = base64.b64decode(value.split(",", 1)[1])
|
||||
elif value.startswith(("https://", "http://")):
|
||||
with urlopen(value, timeout=60) as response:
|
||||
raw = response.read()
|
||||
else:
|
||||
with open(os.path.expanduser(value), "rb") as image_file:
|
||||
raw = image_file.read()
|
||||
with Image.open(BytesIO(raw)) as image:
|
||||
image = image.convert("RGBA")
|
||||
image.thumbnail((512, 512), Image.Resampling.LANCZOS)
|
||||
width, height = image.size
|
||||
output = BytesIO()
|
||||
image.save(output, format="PNG")
|
||||
encoded = "data:image/png;base64," + base64.b64encode(output.getvalue()).decode()
|
||||
return {"image": {"base64": encoded}, "width": width, "height": height}
|
||||
|
||||
def create_image(self, description: str, width: int, height: int, *, seed: Optional[int] = None,
|
||||
reference_images: Optional[list[str]] = None, no_background: bool = True) -> dict:
|
||||
refs = [self._reference(ref) for ref in (reference_images or [])]
|
||||
if refs:
|
||||
payload: dict[str, Any] = {
|
||||
"description": description, "image_size": {"width": width, "height": height},
|
||||
"style_images": refs[:4], "no_background": no_background,
|
||||
}
|
||||
if seed is not None:
|
||||
payload["seed"] = seed
|
||||
return self._request("POST", "/generate-with-style-v2", payload)
|
||||
payload = {"description": description, "image_size": {"width": width, "height": height},
|
||||
"no_background": no_background}
|
||||
if seed is not None:
|
||||
payload["seed"] = seed
|
||||
return self._request("POST", "/generate-image-v2", payload)
|
||||
|
||||
def get_status(self, job_id: str) -> dict:
|
||||
return self._request("GET", f"/background-jobs/{job_id}")
|
||||
|
||||
def wait_for_completion(self, job_id: str, timeout_seconds: int = 115) -> dict:
|
||||
deadline = time.monotonic() + timeout_seconds
|
||||
while True:
|
||||
status = self.get_status(job_id)
|
||||
state = status.get("status", "").lower()
|
||||
if state == "completed":
|
||||
return status
|
||||
if state in {"failed", "error", "cancelled"}:
|
||||
raise PixelLabError(str(status.get("last_response") or state))
|
||||
if time.monotonic() >= deadline:
|
||||
return status
|
||||
time.sleep(2)
|
||||
|
||||
@staticmethod
|
||||
def image_sources(response: dict) -> list[str]:
|
||||
sources: list[str] = []
|
||||
def visit(value: Any) -> None:
|
||||
if isinstance(value, dict):
|
||||
for key, item in value.items():
|
||||
if key in {"url", "base64", "b64_json"} and isinstance(item, str):
|
||||
sources.append(item)
|
||||
else:
|
||||
visit(item)
|
||||
elif isinstance(value, list):
|
||||
for item in value:
|
||||
visit(item)
|
||||
visit(response.get("last_response", response))
|
||||
return list(dict.fromkeys(sources))
|
||||
Reference in New Issue
Block a user