237 lines
6.9 KiB
Python
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"}
|