Files
hermes-agent/tui_gateway/methods_images.py
T
Teknium 99433742dc refactor(tui): extract the prompt turn into prompt_turn.py; compact methods_*/session_*/agent_callbacks; split _start_agent_build into scope/wiring helpers
- server.py 7663 -> 5319: _run_prompt_submit and its goal/loop/voice/scope
  phases move to tui_gateway/prompt_turn.py (bound via method_ctx.bind_module);
  _start_agent_build split into _bind/_release_build_profile_scopes,
  _deferred_build_agent_kwargs, _wire_session_agent, _start_session_services;
  _load_enabled_toolsets split (_enabled_mcp_server_names, _resolve_explicit_toolsets).
- methods_slash: _LIVE_SLASH_OUTPUT dispatch table; methods_tools: _SLASH_BUILTINS,
  _guarded; tool_progress: _PROGRESS_HANDLERS; methods_config_set:
  _REASONING_DISPLAY_WORDS; methods_voice: _VOICE_TOGGLE_ACTIONS.
- Unified: _denied_source (methods_session) replaces _WORKER_SOURCES in
  methods_profiles; _compress_live_with_feedback / _compute_host_slash shared by
  the slash mirror and /compress; _end_voice_chat shared by stop-phrase paths;
  _watcher_mtime_ns; _reaper_session_is_detached_idle; _notif_* helpers.
- Dead: _profile_dir_or_err, _resume_info, _slash_builtin_table (refs.py: 0 hits).
- acp_adapter/server.py: docstring compaction only (AST-identical).
- Every file semantically reviewed hunk-by-hunk for wire/log/lock/order parity.
2026-09-02 14:09:13 -07:00

120 lines
4.3 KiB
Python

"""Image-generation JSON-RPC handler (ws twin of the image_generate tool), so UI
surfaces (avatar pickers, artifact panes) can generate directly. The result is
returned as a data URL: a remote desktop can't read a gateway file path and hosted
URLs are often CORS-opaque to a renderer canvas.
Bodies are rebound onto server.py's globals at install time (see
method_ctx.bind_module), so they reference server.py globals bare.
"""
from .method_ctx import HandlerRegistry, bind_module
_registry = HandlerRegistry()
method = _registry.method
def _image_gen_available() -> bool:
try:
from tools.image_generation_tool import check_image_generation_requirements
return bool(check_image_generation_requirements())
except Exception:
return False
def _image_to_data_url(ref: str, cap: int):
"""Fetch a URL or read a local path into a data URL; None when missing, over *cap*, or failing."""
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
@method("image.generate")
def _(rid, params: dict) -> dict:
"""Params: ``prompt`` (required unless ``probe``), ``aspect_ratio``
(landscape|square|portrait), ``probe`` (availability only), ``max_bytes`` (cap
on the data URL, default 8MB, max 16MB). Result: ``{available, success, image,
image_data, error}`` — ``image_data`` is omitted when the download fails, so
callers fall back to ``image`` (the backend's URL/path)."""
available = _image_gen_available()
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 — same path as the model tool (source-image
# confinement, plugin providers, managed routing, FAL fallback); calling
# the FAL leaf directly 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 = _image_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:
"""Publish this module's helpers + handlers onto ``server``, rebound to its globals."""
bind_module(globals(), server, skip=("_",))