- Viewer: send step images as temp files instead of base64 (dead send_json call removed; no history/WebSocket bloat) - Sampler: early stop returns the latest x0 and always saves the stop step; PSNR inf counts as converged - Sampler: ignore auto_stop when stop_threshold is unsuitable for PSNR/SSIM (avoids stopping at step 2) - Sampler: keep step images as uint8 in a local list; pop downscale_ratio_spacial; compat with older fix_empty_latent_channels - Fix display name mapping for the sampler node - Viewer JS: larger slider handle/track, wheel and keyboard stepping, clear timer on node removal, validate play speed - README: fix typos and document threshold semantics / controls Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
90 lines
2.8 KiB
Python
90 lines
2.8 KiB
Python
import glob
|
|
import os
|
|
import uuid
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
import folder_paths
|
|
|
|
_SUBFOLDER = "step_by_step"
|
|
|
|
|
|
def save_step_images(images, node_id, kind):
|
|
"""
|
|
画像テンソル [B,H,W,C] を temp フォルダに JPEG で保存し、フロントエンド用の参照リストを返す。
|
|
|
|
base64 を ui / WebSocket / 履歴に載せると、ステップ数ぶんの画像が毎回サーバーの履歴に残り
|
|
メッセージも巨大になるため、PreviewImage と同様にファイル参照だけを渡す。
|
|
同じノードの前回分は削除するので、実行を重ねてもファイルは増えない。
|
|
"""
|
|
out_dir = os.path.join(folder_paths.get_temp_directory(), _SUBFOLDER)
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
node_id = "".join(c for c in str(node_id) if c.isalnum() or c in "-_") or "0"
|
|
prefix = f"{kind}_{node_id}_"
|
|
|
|
for old in glob.glob(os.path.join(out_dir, prefix + "*")):
|
|
try:
|
|
os.remove(old)
|
|
except OSError:
|
|
pass
|
|
|
|
run_id = uuid.uuid4().hex[:8]
|
|
refs = []
|
|
for i, img in enumerate(images):
|
|
arr = np.clip(img.cpu().numpy() * 255.0, 0, 255).astype(np.uint8)
|
|
if arr.ndim == 3 and arr.shape[-1] == 1:
|
|
arr = arr[:, :, 0]
|
|
elif arr.ndim == 3 and arr.shape[-1] > 3:
|
|
arr = arr[:, :, :3] # アルファは JPEG に保存できない
|
|
filename = f"{prefix}{run_id}_{i:04d}.jpg"
|
|
Image.fromarray(arr).save(os.path.join(out_dir, filename), format="JPEG", quality=90)
|
|
refs.append({"filename": filename, "subfolder": _SUBFOLDER, "type": "temp"})
|
|
return refs
|
|
|
|
|
|
# 1. 再生用ビューアノード
|
|
class StepStepPlayer:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": {
|
|
"images": ("IMAGE",),
|
|
},
|
|
"hidden": {"unique_id": "UNIQUE_ID"}}
|
|
|
|
RETURN_TYPES = ()
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "view_images"
|
|
CATEGORY = "custom_nodes/viewers"
|
|
|
|
def view_images(self, images, unique_id=None):
|
|
return {"ui": {"step_images": save_step_images(images, unique_id, "player")}}
|
|
|
|
# 2. 比較用ビューアノード
|
|
class StepStepComparer:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": {
|
|
"images": ("IMAGE",),
|
|
},
|
|
"hidden": {"unique_id": "UNIQUE_ID"}}
|
|
|
|
RETURN_TYPES = ()
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "view_images"
|
|
CATEGORY = "custom_nodes/viewers"
|
|
|
|
def view_images(self, images, unique_id=None):
|
|
return {"ui": {"step_images": save_step_images(images, unique_id, "comparer")}}
|
|
|
|
# --- ノードの登録設定 ---
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"StepStepPlayer": StepStepPlayer,
|
|
"StepStepComparer": StepStepComparer
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"StepStepPlayer": "Step-by-Step Player",
|
|
"StepStepComparer": "Step-by-Step Comparer"
|
|
}
|