From 1ebe548ac7cafb844bfde46325f3f7aea81f7452 Mon Sep 17 00:00:00 2001 From: mrm987 <99248187+mrm987@users.noreply.github.com> Date: Sun, 28 Dec 2025 12:00:52 +0900 Subject: [PATCH] Add files via upload --- README.md | 6 +++ __init__.py | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7d824c9..13c3631 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,9 @@ For each prompt: | variety | BOOL | False | Enable variety (diverse compositions) | | decrisper | BOOL | False | Dynamic thresholding (for high CFG) | | free_only | BOOL | True | Force Opus free conditions | +| reference_image | IMAGE | None | Character reference image | +| char_ref_style_aware | BOOL | True | Copy style along with identity | +| char_ref_fidelity | FLOAT | 1.0 | How strictly to match the character | | enable_upscale | BOOL | True | Enable local upscale | | save_prefix | STRING | "NAI" | Output folder name | @@ -337,6 +340,9 @@ output/[save_prefix]/03_sad_00001.png | variety | BOOL | False | 다양성 모드 (구도 다양화) | | decrisper | BOOL | False | Dynamic Thresholding (고CFG용) | | free_only | BOOL | True | Opus 무료 조건 강제 | +| reference_image | IMAGE | None | 캐릭터 레퍼런스 이미지 | +| char_ref_style_aware | BOOL | True | 스타일도 함께 복사 | +| char_ref_fidelity | FLOAT | 1.0 | 캐릭터 일치도 (높을수록 정확) | | enable_upscale | BOOL | True | 로컬 업스케일 활성화 | | save_prefix | STRING | "NAI" | 저장 폴더명 | diff --git a/__init__.py b/__init__.py index c60cafe..6a38c54 100644 --- a/__init__.py +++ b/__init__.py @@ -3,6 +3,7 @@ import sys import subprocess import importlib import glob +import io import torch import numpy as np from PIL import Image @@ -530,7 +531,22 @@ class MultiPromptGenerator: print(f"[MultiPrompt] Complete! Generated {len(all_images)} images.") - # 마지막 프리뷰는 이미 send_preview로 전송됨 + # 마지막 이미지를 UI에 표시 + if all_images and enable_preview: + last_image = all_images[-1] + import time + temp_dir = folder_paths.get_temp_directory() + timestamp = int(time.time() * 1000) + preview_filename = f"multiprompt_final_{timestamp}.png" + preview_path = os.path.join(temp_dir, preview_filename) + + img = last_image[0].cpu().numpy() + img = (img * 255).astype(np.uint8) + pil_img = Image.fromarray(img) + pil_img.save(preview_path, compress_level=1) + + return {"ui": {"images": [{"filename": preview_filename, "subfolder": "", "type": "temp"}]}, "result": (all_images,)} + return {"ui": {"images": []}, "result": (all_images,)} def _get_next_counter(self, prefix): @@ -578,6 +594,54 @@ def load_nai_token(): return os.environ.get("NAI_ACCESS_TOKEN", None) +# Character Reference 헬퍼 함수들 +ACCEPTED_CR_SIZES = [(1024, 1536), (1536, 1024), (1472, 1472)] + +def _choose_cr_canvas(w, h): + """캐릭터 레퍼런스용 캔버스 크기 선택 (원본 비율에 가장 가까운 것)""" + aspect = w / h + best = None + best_diff = 9e9 + for cw, ch in ACCEPTED_CR_SIZES: + diff = abs((cw / ch) - aspect) + if diff < best_diff: + best_diff = diff + best = (cw, ch) + return best + +def pad_image_to_canvas(tensor_image, target_size): + """이미지를 캔버스 크기에 맞게 letterbox 패딩""" + _, H, W, C = tensor_image.shape + tw, th = target_size + arr = (tensor_image[0].cpu().numpy() * 255).clip(0, 255).astype(np.uint8) + mode = "RGBA" if (C == 4) else "RGB" + pil = Image.fromarray(arr) + + scale = min(tw / W, th / H) + new_w = max(1, int(W * scale)) + new_h = max(1, int(H * scale)) + pil_resized = pil.resize((new_w, new_h), Image.LANCZOS) + + if mode == "RGBA": + canvas = Image.new("RGBA", (tw, th), (0, 0, 0, 0)) + else: + canvas = Image.new("RGB", (tw, th), (0, 0, 0)) + offset = ((tw - new_w) // 2, (th - new_h) // 2) + canvas.paste(pil_resized, offset) + + out = np.array(canvas).astype(np.float32) / 255.0 + return torch.from_numpy(out)[None,] + +def image_to_base64(tensor_image): + """텐서 이미지를 base64로 변환""" + import base64 + arr = (tensor_image[0].cpu().numpy() * 255).clip(0, 255).astype(np.uint8) + pil = Image.fromarray(arr) + buffer = io.BytesIO() + pil.save(buffer, format="PNG") + return base64.b64encode(buffer.getvalue()).decode("utf-8") + + class NAIMultiPromptGenerator: """ NovelAI API를 사용하여 다중 프롬프트 이미지 생성 + 업스케일 + LUT @@ -612,6 +676,9 @@ class NAIMultiPromptGenerator: "free_only": ("BOOLEAN", {"default": True}), "cfg_rescale": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), "uncond_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.5, "step": 0.01}), + "reference_image": ("IMAGE",), + "char_ref_style_aware": ("BOOLEAN", {"default": True, "tooltip": "Copy style along with identity"}), + "char_ref_fidelity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "How strictly to match the character"}), "lut_name": (get_lut_files(),), "lut_strength": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 1.0, "step": 0.01}), "enable_preview": ("BOOLEAN", {"default": True}), @@ -629,7 +696,7 @@ class NAIMultiPromptGenerator: CATEGORY = "image/generation" OUTPUT_NODE = True - def call_nai_api(self, prompt, negative_prompt, width, height, seed, steps, cfg, sampler, scheduler, smea, nai_model, variety, decrisper, cfg_rescale, uncond_scale): + def call_nai_api(self, prompt, negative_prompt, width, height, seed, steps, cfg, sampler, scheduler, smea, nai_model, variety, decrisper, cfg_rescale, uncond_scale, reference_image=None, char_ref_style_aware=True, char_ref_fidelity=1.0): """NAI API 호출""" import requests from requests.adapters import HTTPAdapter @@ -694,6 +761,24 @@ class NAIMultiPromptGenerator: } } + # Character Reference 처리 + if reference_image is not None: + _, h_raw, w_raw, _ = reference_image.shape + canvas_w, canvas_h = _choose_cr_canvas(w_raw, h_raw) + padded = pad_image_to_canvas(reference_image, (canvas_w, canvas_h)) + base_caption = "character&style" if char_ref_style_aware else "character" + + params["director_reference_images"] = [image_to_base64(padded)] + params["director_reference_descriptions"] = [{ + "use_coords": False, + "use_order": False, + "legacy_uc": False, + "caption": {"base_caption": base_caption, "char_captions": []} + }] + params["director_reference_strength_values"] = [1.0] + params["director_reference_secondary_strength_values"] = [1.0 - char_ref_fidelity] + params["director_reference_information_extracted"] = [1.0] + # k_euler_ancestral + non-native scheduler if sampler == "k_euler_ancestral" and scheduler != "native": params["deliberate_euler_ancestral_bug"] = False @@ -793,6 +878,7 @@ class NAIMultiPromptGenerator: width, height, seed, steps, cfg, sampler, scheduler, smea, nai_model, save_prefix, skip_indices="", variety=False, decrisper=False, free_only=True, cfg_rescale=0.0, uncond_scale=1.0, + reference_image=None, char_ref_style_aware=True, char_ref_fidelity=1.0, lut_name="None", lut_strength=0.3, enable_preview=True, unique_id=None, prompt=None, extra_pnginfo=None): @@ -880,7 +966,8 @@ class NAIMultiPromptGenerator: image = self.call_nai_api( full_prompt, negative_prompt, width, height, current_seed, steps, cfg, sampler, scheduler, smea, - nai_model, variety, decrisper, cfg_rescale, uncond_scale + nai_model, variety, decrisper, cfg_rescale, uncond_scale, + reference_image, char_ref_style_aware, char_ref_fidelity ) # 프리뷰 @@ -898,6 +985,23 @@ class NAIMultiPromptGenerator: all_images.append(image) print(f"[NAI MultiPrompt] Complete! Generated {len(all_images)} images.") + + # 마지막 이미지를 UI에 표시 + if all_images and enable_preview: + last_image = all_images[-1] + import time + temp_dir = folder_paths.get_temp_directory() + timestamp = int(time.time() * 1000) + preview_filename = f"nai_multiprompt_final_{timestamp}.png" + preview_path = os.path.join(temp_dir, preview_filename) + + img = last_image[0].cpu().numpy() + img = (img * 255).astype(np.uint8) + pil_img = Image.fromarray(img) + pil_img.save(preview_path, compress_level=1) + + return {"ui": {"images": [{"filename": preview_filename, "subfolder": "", "type": "temp"}]}, "result": (all_images,)} + return {"ui": {"images": []}, "result": (all_images,)} def _get_next_counter(self, prefix):