Files

237 lines
6.9 KiB
Python

from __future__ import annotations
import hashlib
import os
from pathlib import Path
from typing import Any
try:
from .smart_metadata_reader.image_io import load_image_and_mask
from .smart_metadata_reader.metadata_reader import parse_metadata_bundle, read_metadata
except ImportError:
from smart_metadata_reader.image_io import load_image_and_mask
from smart_metadata_reader.metadata_reader import parse_metadata_bundle, read_metadata
try:
import folder_paths # type: ignore
except ImportError:
folder_paths = None # type: ignore
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp"}
class SmartMetadataReader:
RETURN_TYPES = (
"IMAGE",
"MASK",
"STRING",
"STRING",
"INT",
"INT",
"FLOAT",
"INT",
"INT",
"STRING",
"STRING",
"STRING",
)
RETURN_NAMES = (
"image",
"mask",
"positive",
"negative",
"seed",
"steps",
"cfg",
"width",
"height",
"model_name",
"filename",
"setting",
)
FUNCTION = "read_metadata"
CATEGORY = "metadata/image"
@classmethod
def INPUT_TYPES(cls) -> dict[str, dict[str, Any]]:
return {
"required": {
"image": (_input_images(), {"image_upload": True}),
"parameter_index": ("INT", {"default": 0, "min": 0}),
"prefer_cached_text": ("BOOLEAN", {"default": True}),
"include_raw_json": ("BOOLEAN", {"default": True}),
"max_depth": ("INT", {"default": 40, "min": 1, "max": 200}),
}
}
@classmethod
def IS_CHANGED(cls, image: str, **kwargs: Any) -> str:
del kwargs
image_path = _resolve_image_path_safely(image)
return _file_digest(image_path)
@classmethod
def VALIDATE_INPUTS(cls, image: str, **kwargs: Any) -> bool | str:
del kwargs
if folder_paths is not None and hasattr(folder_paths, "exists_annotated_filepath"):
try:
if folder_paths.exists_annotated_filepath(image):
return True
return f"Invalid image file: {image}"
except Exception:
pass
image_path = _resolve_image_path_safely(image)
if Path(image_path).exists():
return True
return f"Invalid image file: {image_path}"
def read_metadata(
self,
image: str,
parameter_index: int = 0,
prefer_cached_text: bool = True,
include_raw_json: bool = True,
max_depth: int = 40,
) -> tuple[Any, Any, str, str, int, int, float, int, int, str, str, str]:
image_path = _resolve_image_path(image)
image_tensor, mask_tensor, image_width, image_height = _load_image_or_raise(image_path)
filename = Path(image_path).name
try:
bundle = read_metadata(image_path)
result = parse_metadata_bundle(
bundle,
parameter_index=parameter_index,
prefer_cached_text=prefer_cached_text,
include_raw_json=include_raw_json,
max_depth=max_depth,
)
except Exception as exc:
setting = _failed_setting(filename, image_width, image_height, exc)
return (
image_tensor,
mask_tensor,
"",
"",
-1,
0,
0.0,
image_width,
image_height,
"",
filename,
setting,
)
return (
image_tensor,
mask_tensor,
result.positive,
result.negative,
result.seed,
result.steps,
result.cfg,
result.width or image_width,
result.height or image_height,
result.model_name,
result.filename or filename,
result.setting,
)
def _input_images() -> list[str]:
if folder_paths is None or not hasattr(folder_paths, "get_input_directory"):
return []
try:
input_dir_value = folder_paths.get_input_directory()
except Exception:
return []
if not input_dir_value:
return []
input_dir = Path(input_dir_value)
if not input_dir.is_dir():
return []
images: list[str] = []
for root, _dirs, files in os.walk(input_dir):
for filename in files:
if Path(filename).suffix.lower() not in IMAGE_EXTENSIONS:
continue
full_path = Path(root) / filename
relative_path = os.path.relpath(full_path, input_dir)
images.append(relative_path.replace(os.sep, "/"))
return sorted(images)
def _resolve_image_path(image: str) -> str:
if folder_paths is not None and hasattr(folder_paths, "get_annotated_filepath"):
return str(folder_paths.get_annotated_filepath(image))
if folder_paths is not None and hasattr(folder_paths, "get_input_directory"):
return str(Path(folder_paths.get_input_directory()) / image)
return os.fspath(image)
def _resolve_image_path_safely(image: str) -> str:
try:
return _resolve_image_path(image)
except Exception:
pass
if folder_paths is not None and hasattr(folder_paths, "get_input_directory"):
try:
input_dir = folder_paths.get_input_directory()
except Exception:
input_dir = None
if input_dir:
return str(Path(input_dir) / image)
return os.fspath(image)
def _load_image_or_raise(image_path: str) -> tuple[Any, Any, int, int]:
try:
return load_image_and_mask(image_path)
except Exception as exc:
raise RuntimeError(f"Failed to load image '{image_path}': {exc}") from exc
def _file_digest(image_path: str) -> str:
path = Path(image_path)
try:
hasher = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
hasher.update(chunk)
return hasher.hexdigest()
except OSError:
return f"missing:{image_path}"
def _failed_setting(filename: str, width: int, height: int, exc: Exception) -> str:
size = f"{width} x {height}" if width > 0 and height > 0 else "unknown"
return "\n".join(
[
f"Filename: {filename or 'unknown'}",
"Source: unknown",
"Model: unknown",
"VAE: unknown",
"LoRA: none",
"Seed: -1",
"Steps: 0",
"CFG: 0.0",
"Sampler: unknown",
"Scheduler: unknown",
f"Size: {size}",
"Status: FAILED",
"Confidence: 0.00",
f"Error: {exc}",
]
)
NODE_CLASS_MAPPINGS = {"SmartMetadataReader": SmartMetadataReader}
NODE_DISPLAY_NAME_MAPPINGS = {"SmartMetadataReader": "Smart Metadata Reader"}