"""Image-generation JSON-RPC handler (ws twin of the image_generate tool). Desktop plugins reach the backend only through ws JSON-RPC; the image generation capability existed solely as a model tool. ``image.generate`` lets UI surfaces (avatar pickers, artifact panes) generate directly. The result image is returned as a data URL (``image_data``): a remote desktop client cannot read a file path on the gateway host, and hosted result URLs are often CORS-opaque to a renderer canvas. Data URLs work identically over local and remote gateways. Handlers are rebound onto server.py's globals at install time (see method_ctx.py) — helpers must stay nested inside the handler body. """ from .method_ctx import HandlerRegistry _registry = HandlerRegistry() method = _registry.method @method("image.generate") def _(rid, params: dict) -> dict: """Generate an image with the configured backend. Params: ``prompt`` (required unless ``probe``), ``aspect_ratio`` (landscape|square|portrait), ``probe`` (return availability only), ``max_bytes`` (cap on the returned data URL payload, default 8MB). Result: ``{available, success, image, image_data, error}`` where ``image`` is the backend's URL/path and ``image_data`` is a data URL of the downloaded bytes (omitted when the download fails — callers should fall back to ``image``). """ def _availability() -> bool: try: from tools.image_generation_tool import check_image_generation_requirements return bool(check_image_generation_requirements()) except Exception: return False def _to_data_url(ref: str, cap: int): """Fetch a URL or read a local path into a data URL, size-capped.""" import base64 import mimetypes import os try: if ref.startswith(("http://", "https://")): import urllib.request req = urllib.request.Request(ref, headers={"User-Agent": "hermes-agent"}) with urllib.request.urlopen(req, timeout=60) as resp: if resp.length is not None and resp.length > cap: return None data = resp.read(cap + 1) mime = resp.headers.get_content_type() or "image/png" elif os.path.isfile(ref): if os.path.getsize(ref) > cap: return None with open(ref, "rb") as fh: data = fh.read(cap + 1) mime = mimetypes.guess_type(ref)[0] or "image/png" else: return None if len(data) > cap: return None if not mime.startswith("image/"): mime = "image/png" return f"data:{mime};base64,{base64.b64encode(data).decode('ascii')}" except Exception: return None available = _availability() if is_truthy_value(params.get("probe", False)): return _ok(rid, {"available": available}) if not available: return _ok( rid, { "available": False, "success": False, "error": "No image generation backend configured (run `hermes tools` to enable one).", }, ) prompt = str(params.get("prompt") or "").strip() if not prompt: return _err(rid, 4071, "prompt required") aspect = str(params.get("aspect_ratio") or "square").strip().lower() try: cap = min(int(params.get("max_bytes", 8_000_000) or 8_000_000), 16_000_000) except (TypeError, ValueError): cap = 8_000_000 try: from tools.image_generation_tool import _handle_image_generate # Full provider dispatcher — the same path the model tool takes: # source-image confinement, plugin-registered providers, managed # Krea routing, then the in-tree FAL fallback. Calling the FAL leaf # (image_generate_tool) directly here bypassed configured providers. raw = _handle_image_generate({"prompt": prompt, "aspect_ratio": aspect}) result = json.loads(raw) except Exception as e: return _err(rid, 5071, str(e)) if not result.get("success"): return _ok( rid, { "available": True, "success": False, "error": str(result.get("error") or "generation failed"), }, ) image_ref = str(result.get("image") or "") payload = {"available": True, "success": True, "image": image_ref} data_url = _to_data_url(image_ref, cap) if image_ref else None if data_url: payload["image_data"] = data_url return _ok(rid, payload) def register(server) -> None: _registry.install(server)