Files
LuqP2-ImageMetaHub-ComfyUI-…/metahub_input_node.py

185 lines
6.3 KiB
Python

"""
MetaHub Input Node for ComfyUI
Reads prepared Image MetaHub bridge assets from local disk.
"""
import hashlib
import json
from pathlib import Path
import numpy as np
import torch
from PIL import Image, ImageOps
BRIDGE_IMAGE_NAME = "image.png"
BRIDGE_MASK_NAME = "mask.png"
BRIDGE_METADATA_NAME = "metadata.json"
def resolve_default_bridge_dir() -> Path:
return Path.home() / "ImageMetaHub" / "comfyui_bridge"
def resolve_bridge_dir(bridge_dir: str = "") -> Path:
raw_dir = (bridge_dir or "").strip()
if not raw_dir:
return resolve_default_bridge_dir()
return Path(raw_dir).expanduser().resolve()
def sanitize_session_id(session_id: str = "latest") -> str:
normalized = (session_id or "latest").strip() or "latest"
if "/" in normalized or "\\" in normalized or normalized in {".", ".."}:
raise ValueError("MetaHub Input session_id cannot contain path separators.")
return normalized
def resolve_payload_dir(bridge_dir: str = "", session_id: str = "latest") -> Path:
root = resolve_bridge_dir(bridge_dir)
normalized_session = sanitize_session_id(session_id)
if normalized_session == "latest":
return root / "latest"
return root / "sessions" / normalized_session
def read_metadata(payload_dir: Path) -> tuple[dict, str]:
metadata_path = payload_dir / BRIDGE_METADATA_NAME
if not metadata_path.exists():
raise FileNotFoundError(
f"MetaHub Input could not find {metadata_path}. "
"Use Send to ComfyUI Bridge in Image MetaHub first, or set bridge_dir."
)
metadata_json = metadata_path.read_text(encoding="utf-8")
try:
metadata = json.loads(metadata_json)
except json.JSONDecodeError as error:
raise ValueError(f"MetaHub Input metadata.json is invalid JSON: {error}") from error
return metadata, metadata_json
def image_to_tensor(image_path: Path) -> torch.Tensor:
if not image_path.exists():
raise FileNotFoundError(f"MetaHub Input could not find prepared image: {image_path}")
image = ImageOps.exif_transpose(Image.open(image_path)).convert("RGB")
array = np.asarray(image).astype(np.float32) / 255.0
return torch.from_numpy(array)[None,]
def mask_to_tensor(mask_path: Path | None, width: int, height: int) -> torch.Tensor:
if mask_path is None or not mask_path.exists():
return torch.zeros((1, height, width), dtype=torch.float32)
mask = ImageOps.exif_transpose(Image.open(mask_path)).convert("L")
if mask.size != (width, height):
mask = mask.resize((width, height), Image.Resampling.NEAREST)
array = np.asarray(mask).astype(np.float32) / 255.0
return torch.from_numpy(array)[None,]
def file_digest(path: Path) -> str:
if not path.exists():
return f"missing:{path}"
digest = hashlib.sha256()
digest.update(str(path).encode("utf-8", errors="replace"))
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def payload_digest(bridge_dir: str = "", session_id: str = "latest") -> str:
try:
payload_dir = resolve_payload_dir(bridge_dir, session_id)
metadata_path = payload_dir / BRIDGE_METADATA_NAME
image_path = payload_dir / BRIDGE_IMAGE_NAME
digest_parts = [
file_digest(metadata_path),
file_digest(image_path),
]
try:
metadata, _metadata_json = read_metadata(payload_dir)
mask_available = bool(metadata.get("files", {}).get("mask", {}).get("available"))
except Exception:
mask_available = False
if mask_available:
digest_parts.append(file_digest(payload_dir / BRIDGE_MASK_NAME))
return "|".join(digest_parts)
except Exception as error:
return f"error:{type(error).__name__}:{error}"
class MetaHubInputNode:
"""
Loads the latest prepared Image MetaHub bridge image, optional mask, and metadata.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"bridge_dir": ("STRING", {
"default": "",
"tooltip": "Image MetaHub bridge directory. Blank uses ~/ImageMetaHub/comfyui_bridge.",
}),
"session_id": ("STRING", {
"default": "latest",
"tooltip": "Use latest, or a session id from bridge/sessions.",
}),
},
}
RETURN_TYPES = ("IMAGE", "MASK", "STRING", "FLOAT", "STRING", "STRING", "STRING", "INT", "INT")
RETURN_NAMES = ("image", "mask", "metadata_json", "denoise", "intent", "source_path", "session_id", "width", "height")
FUNCTION = "load_bridge"
CATEGORY = "image/load"
DESCRIPTION = "Load prepared Image MetaHub image, mask, and metadata from the local ComfyUI Bridge."
OUTPUT_NODE = False
@classmethod
def IS_CHANGED(cls, bridge_dir="", session_id="latest", **kwargs):
return payload_digest(bridge_dir, session_id)
def load_bridge(self, bridge_dir="", session_id="latest"):
payload_dir = resolve_payload_dir(bridge_dir, session_id)
metadata, metadata_json = read_metadata(payload_dir)
image_path = payload_dir / BRIDGE_IMAGE_NAME
image = image_to_tensor(image_path)
height = int(image.shape[1])
width = int(image.shape[2])
mask_available = bool(metadata.get("files", {}).get("mask", {}).get("available"))
mask_path = payload_dir / BRIDGE_MASK_NAME if mask_available else None
mask = mask_to_tensor(mask_path, width, height)
denoise = metadata.get("denoise", 0.65)
try:
denoise = float(denoise)
except (TypeError, ValueError):
denoise = 0.65
denoise = min(1.0, max(0.0, denoise))
intent = metadata.get("intent") or "img2img"
source_path = metadata.get("source", {}).get("path") or ""
resolved_session_id = metadata.get("session_id") or sanitize_session_id(session_id)
return (
image,
mask,
metadata_json,
denoise,
str(intent),
str(source_path),
str(resolved_session_id),
width,
height,
)
NODE_CLASS_MAPPINGS = {
"MetaHubInputNode": MetaHubInputNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MetaHubInputNode": "MetaHub Input",
}