Files
Imagen/polza.py

156 lines
5.8 KiB
Python

"""Client for Polza.ai's media API.
The client deliberately uses the standard library so enabling cloud generation
does not add a runtime dependency to the local Diffusers installation.
"""
import base64
import json
import mimetypes
import os
import time
from pathlib import Path
from typing import Any, Optional
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
API_URL = "https://polza.ai/api/v1"
MAX_REFERENCE_BYTES = 50 * 1024 * 1024
class PolzaError(RuntimeError):
"""An API or transport error returned by Polza.ai."""
class PolzaClient:
def __init__(self, api_key: Optional[str] = None, api_url: str = API_URL):
self.api_key = api_key or os.environ.get("POLZA_API_KEY")
self.api_url = api_url.rstrip("/")
def _headers(self) -> dict[str, str]:
if not self.api_key:
raise PolzaError(
"POLZA_API_KEY is not configured. Set it in the MCP server environment."
)
return {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
def _request(self, method: str, path: str, payload: Optional[dict] = None) -> dict:
body = json.dumps(payload).encode("utf-8") if payload is not None else None
request = Request(
f"{self.api_url}{path}", data=body, headers=self._headers(), method=method
)
try:
with urlopen(request, timeout=125) as response:
result = json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
try:
detail = json.loads(detail).get("error", {}).get("message", detail)
except json.JSONDecodeError:
pass
raise PolzaError(f"Polza API returned HTTP {exc.code}: {detail}") from exc
except (URLError, TimeoutError) as exc:
raise PolzaError(f"Could not reach Polza API: {exc}") from exc
if result.get("error"):
error = result["error"]
raise PolzaError(error.get("message", str(error)))
return result
@staticmethod
def reference_payload(reference: str) -> dict[str, str]:
"""Convert an HTTPS URL, data URI, or local image path for Media API."""
if reference.startswith(("https://", "http://")):
return {"type": "url", "data": reference}
if reference.startswith("data:"):
return {"type": "base64", "data": reference}
path = Path(reference).expanduser()
if not path.is_file():
raise PolzaError(f"Reference image does not exist: {reference}")
if path.stat().st_size > MAX_REFERENCE_BYTES:
raise PolzaError(f"Reference image exceeds the 50 MB API limit: {reference}")
mime_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
encoded = base64.b64encode(path.read_bytes()).decode("ascii")
return {"type": "base64", "data": f"data:{mime_type};base64,{encoded}"}
def create_image(
self,
*,
model: str,
prompt: str,
reference_images: Optional[list[str]] = None,
count: int = 1,
aspect_ratio: Optional[str] = None,
seed: Optional[int] = None,
quality: Optional[str] = None,
output_format: str = "png",
background: Optional[str] = None,
provider: Optional[dict] = None,
wait: bool = True,
) -> dict:
if not 1 <= count <= 10:
raise PolzaError("count must be between 1 and 10")
input_data: dict[str, Any] = {
"prompt": prompt,
"max_images": count,
"output_format": output_format,
}
if reference_images:
input_data["images"] = [self.reference_payload(ref) for ref in reference_images]
for name, value in {
"aspect_ratio": aspect_ratio,
"seed": seed,
"quality": quality,
"background": background,
}.items():
if value is not None:
input_data[name] = value
payload: dict[str, Any] = {"model": model, "input": input_data, "async": not wait}
if provider:
payload["provider"] = provider
return self._request("POST", "/media", payload)
def get_status(self, generation_id: str) -> dict:
return self._request("GET", f"/media/{generation_id}")
def wait_for_completion(
self, generation_id: str, timeout_seconds: int = 115, poll_seconds: float = 2
) -> dict:
deadline = time.monotonic() + timeout_seconds
while True:
status = self.get_status(generation_id)
state = status.get("status", "").lower()
if state in {"completed", "succeeded", "success"}:
return status
if state in {"failed", "error", "cancelled", "canceled"}:
error = status.get("error") or {}
raise PolzaError(error.get("message", f"Generation {generation_id} {state}"))
if time.monotonic() >= deadline:
return status
time.sleep(poll_seconds)
@staticmethod
def image_sources(response: dict) -> list[str]:
"""Extract CDN URLs or base64 images from documented response variants."""
found: list[str] = []
def visit(value: Any) -> None:
if isinstance(value, dict):
for key, item in value.items():
if key in {"url", "b64_json", "base64"} and isinstance(item, str):
found.append(item)
elif key in {"data", "output", "images", "result", "results"}:
visit(item)
elif isinstance(value, list):
for item in value:
visit(item)
visit(response)
return list(dict.fromkeys(found))