156 lines
5.8 KiB
Python
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))
|