Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled
Includes 30 custom nodes committed directly, 7 Civitai-exclusive loras stored via Git LFS, and a setup script that installs all dependencies and downloads HuggingFace-hosted models on vast.ai. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
791 lines
34 KiB
Python
791 lines
34 KiB
Python
import json
|
||
import os
|
||
from urllib.request import urlopen
|
||
import folder_paths
|
||
|
||
from .. import easyCache
|
||
from ..config import FOOOCUS_STYLES_DIR, MAX_SEED_NUM, PROMPT_TEMPLATE, RESOURCES_DIR
|
||
from ..libs.log import log_node_info
|
||
from ..libs.wildcards import WildcardProcessor, get_wildcard_list, process
|
||
|
||
from comfy_api.latest import io
|
||
|
||
|
||
# 正面提示词
|
||
class positivePrompt(io.ComfyNode):
|
||
|
||
@classmethod
|
||
def define_schema(cls):
|
||
return io.Schema(
|
||
node_id="easy positive",
|
||
category="EasyUse/Prompt",
|
||
inputs=[
|
||
io.String.Input("positive", default="", multiline=True, placeholder="Positive"),
|
||
],
|
||
outputs=[
|
||
io.String.Output(id="output_positive", display_name="positive"),
|
||
],
|
||
)
|
||
|
||
@classmethod
|
||
def execute(cls, positive):
|
||
return io.NodeOutput(positive)
|
||
|
||
# 通配符提示词
|
||
class wildcardsPrompt(io.ComfyNode):
|
||
|
||
@classmethod
|
||
def define_schema(cls):
|
||
wildcard_list = get_wildcard_list()
|
||
return io.Schema(
|
||
node_id="easy wildcards",
|
||
category="EasyUse/Prompt",
|
||
inputs=[
|
||
io.String.Input("text", default="", multiline=True, dynamic_prompts=False, placeholder="(Support wildcard)"),
|
||
io.Combo.Input("Select to add LoRA", options=["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras")),
|
||
io.Combo.Input("Select to add Wildcard", options=["Select the Wildcard to add to the text"] + wildcard_list),
|
||
io.Int.Input("seed", default=0, min=0, max=MAX_SEED_NUM),
|
||
io.Boolean.Input("multiline_mode", default=False),
|
||
],
|
||
outputs=[
|
||
io.String.Output(id="output_text", display_name="text", is_output_list=True),
|
||
io.String.Output(id="populated_text", display_name="populated_text", is_output_list=True),
|
||
],
|
||
hidden=[
|
||
io.Hidden.prompt,
|
||
io.Hidden.extra_pnginfo,
|
||
io.Hidden.unique_id,
|
||
],
|
||
)
|
||
|
||
@classmethod
|
||
def execute(cls, text, seed, multiline_mode, **kwargs):
|
||
prompt = cls.hidden.prompt
|
||
|
||
# Clean loaded_objects
|
||
if prompt:
|
||
easyCache.update_loaded_objects(prompt)
|
||
|
||
if multiline_mode:
|
||
populated_text = []
|
||
_text = []
|
||
text_lines = text.split("\n")
|
||
for t in text_lines:
|
||
_text.append(t)
|
||
populated_text.append(process(t, seed))
|
||
text = _text
|
||
else:
|
||
populated_text = [process(text, seed)]
|
||
text = [text]
|
||
return io.NodeOutput(text, populated_text, ui={"value": [seed]})
|
||
|
||
# 通配符提示词矩阵,会按顺序返回包含通配符的提示词所生成的所有可能
|
||
class wildcardsPromptMatrix(io.ComfyNode):
|
||
|
||
@classmethod
|
||
def define_schema(cls):
|
||
wildcard_list = get_wildcard_list()
|
||
return io.Schema(
|
||
node_id="easy wildcardsMatrix",
|
||
category="EasyUse/Prompt",
|
||
inputs=[
|
||
io.String.Input("text", default="", multiline=True, dynamic_prompts=False, placeholder="(Support Lora Block Weight and wildcard)"),
|
||
io.Combo.Input("Select to add LoRA", options=["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras")),
|
||
io.Combo.Input("Select to add Wildcard", options=["Select the Wildcard to add to the text"] + wildcard_list),
|
||
io.Int.Input("offset", default=0, min=0, max=MAX_SEED_NUM, step=1, control_after_generate=True),
|
||
io.Int.Input("output_limit", default=1, min=-1, step=1, tooltip="Output All Probilities", optional=True),
|
||
],
|
||
outputs=[
|
||
io.String.Output("populated_text", is_output_list=True),
|
||
io.Int.Output("total"),
|
||
io.Int.Output("factors", is_output_list=True),
|
||
],
|
||
hidden=[
|
||
io.Hidden.prompt,
|
||
io.Hidden.extra_pnginfo,
|
||
io.Hidden.unique_id,
|
||
],
|
||
)
|
||
|
||
@classmethod
|
||
def execute(cls, text, offset, output_limit=1, **kwargs):
|
||
prompt = cls.hidden.prompt
|
||
# Clean loaded_objects
|
||
if prompt:
|
||
easyCache.update_loaded_objects(prompt)
|
||
|
||
p = WildcardProcessor(text)
|
||
total = p.total()
|
||
limit = total if output_limit > total or output_limit == -1 else output_limit
|
||
offset = 0 if output_limit == -1 else offset
|
||
populated_text = p.getmany(limit, offset) if output_limit != 1 else [p.getn(offset)]
|
||
return io.NodeOutput(populated_text, p.total(), list(p.placeholder_choices.values()), ui={"value": [offset]})
|
||
|
||
# 负面提示词
|
||
class negativePrompt(io.ComfyNode):
|
||
|
||
@classmethod
|
||
def define_schema(cls):
|
||
return io.Schema(
|
||
node_id="easy negative",
|
||
category="EasyUse/Prompt",
|
||
inputs=[
|
||
io.String.Input("negative", default="", multiline=True, placeholder="Negative"),
|
||
],
|
||
outputs=[
|
||
io.String.Output(id="output_negative", display_name="negative"),
|
||
],
|
||
)
|
||
|
||
@classmethod
|
||
def execute(cls, negative):
|
||
return io.NodeOutput(negative)
|
||
|
||
# 风格提示词选择器
|
||
class stylesPromptSelector(io.ComfyNode):
|
||
|
||
@classmethod
|
||
def define_schema(cls):
|
||
styles = ["fooocus_styles"]
|
||
styles_dir = FOOOCUS_STYLES_DIR
|
||
for file_name in os.listdir(styles_dir):
|
||
file = os.path.join(styles_dir, file_name)
|
||
if os.path.isfile(file) and file_name.endswith(".json"):
|
||
if file_name != "fooocus_styles.json":
|
||
styles.append(file_name.split(".")[0])
|
||
|
||
return io.Schema(
|
||
node_id="easy stylesSelector",
|
||
category="EasyUse/Prompt",
|
||
inputs=[
|
||
io.Combo.Input("styles", options=styles, default="fooocus_styles"),
|
||
io.String.Input("positive", default="", force_input=True, optional=True),
|
||
io.String.Input("negative", default="", force_input=True, optional=True),
|
||
io.Custom(io_type="EASY_PROMPT_STYLES").Input("select_styles", optional=True),
|
||
],
|
||
outputs=[
|
||
io.String.Output(id="output_positive", display_name="positive"),
|
||
io.String.Output(id="output_negative", display_name="negative"),
|
||
],
|
||
hidden=[
|
||
io.Hidden.prompt,
|
||
io.Hidden.extra_pnginfo,
|
||
io.Hidden.unique_id,
|
||
],
|
||
)
|
||
|
||
@classmethod
|
||
def execute(cls, styles, positive='', negative='', select_styles=None, **kwargs):
|
||
values = []
|
||
all_styles = {}
|
||
positive_prompt, negative_prompt = '', negative
|
||
fooocus_custom_dir = os.path.join(FOOOCUS_STYLES_DIR, 'fooocus_styles.json')
|
||
if styles == "fooocus_styles" and not os.path.exists(fooocus_custom_dir):
|
||
file = os.path.join(RESOURCES_DIR, styles + '.json')
|
||
else:
|
||
file = os.path.join(FOOOCUS_STYLES_DIR, styles + '.json')
|
||
f = open(file, 'r', encoding='utf-8')
|
||
data = json.load(f)
|
||
f.close()
|
||
for d in data:
|
||
all_styles[d['name']] = d
|
||
# if my_unique_id in prompt:
|
||
# if prompt[my_unique_id]["inputs"]['select_styles']:
|
||
# values = prompt[my_unique_id]["inputs"]['select_styles'].split(',')
|
||
|
||
if isinstance(select_styles, str):
|
||
values = select_styles.split(',')
|
||
else:
|
||
values = select_styles if select_styles else []
|
||
|
||
has_prompt = False
|
||
if len(values) == 0:
|
||
return io.NodeOutput(positive, negative)
|
||
|
||
for index, val in enumerate(values):
|
||
if val not in all_styles:
|
||
continue
|
||
if 'prompt' in all_styles[val]:
|
||
if "{prompt}" in all_styles[val]['prompt'] and has_prompt == False:
|
||
positive_prompt = all_styles[val]['prompt'].replace('{prompt}', positive)
|
||
has_prompt = True
|
||
elif "{prompt}" in all_styles[val]['prompt']:
|
||
positive_prompt += ', ' + all_styles[val]['prompt'].replace(', {prompt}', '').replace('{prompt}', '')
|
||
else:
|
||
positive_prompt = all_styles[val]['prompt'] if positive_prompt == '' else positive_prompt + ', ' + all_styles[val]['prompt']
|
||
if 'negative_prompt' in all_styles[val]:
|
||
negative_prompt += ', ' + all_styles[val]['negative_prompt'] if negative_prompt else all_styles[val]['negative_prompt']
|
||
|
||
if has_prompt == False and positive:
|
||
positive_prompt = positive + positive_prompt + ', '
|
||
|
||
return io.NodeOutput(positive_prompt, negative_prompt)
|
||
|
||
#prompt
|
||
class prompt(io.ComfyNode):
|
||
@classmethod
|
||
def define_schema(cls):
|
||
return io.Schema(
|
||
node_id="easy prompt",
|
||
category="EasyUse/Prompt",
|
||
inputs=[
|
||
io.String.Input("text", default="", multiline=True, placeholder="Prompt"),
|
||
io.Combo.Input("prefix", options=["Select the prefix add to the text"] + PROMPT_TEMPLATE["prefix"], default="Select the prefix add to the text"),
|
||
io.Combo.Input("subject", options=["👤Select the subject add to the text"] + PROMPT_TEMPLATE["subject"], default="👤Select the subject add to the text"),
|
||
io.Combo.Input("action", options=["🎬Select the action add to the text"] + PROMPT_TEMPLATE["action"], default="🎬Select the action add to the text"),
|
||
io.Combo.Input("clothes", options=["👚Select the clothes add to the text"] + PROMPT_TEMPLATE["clothes"], default="👚Select the clothes add to the text"),
|
||
io.Combo.Input("environment", options=["☀️Select the illumination environment add to the text"] + PROMPT_TEMPLATE["environment"], default="☀️Select the illumination environment add to the text"),
|
||
io.Combo.Input("background", options=["🎞️Select the background add to the text"] + PROMPT_TEMPLATE["background"], default="🎞️Select the background add to the text"),
|
||
io.Combo.Input("nsfw", options=["🔞Select the nsfw add to the text"] + PROMPT_TEMPLATE["nsfw"], default="🔞️Select the nsfw add to the text"),
|
||
],
|
||
outputs=[
|
||
io.String.Output("prompt"),
|
||
],
|
||
hidden=[
|
||
io.Hidden.prompt,
|
||
io.Hidden.extra_pnginfo,
|
||
io.Hidden.unique_id,
|
||
],
|
||
)
|
||
|
||
@classmethod
|
||
def execute(cls, text, **kwargs):
|
||
return io.NodeOutput(text)
|
||
|
||
#promptList
|
||
class promptList(io.ComfyNode):
|
||
@classmethod
|
||
def define_schema(cls):
|
||
return io.Schema(
|
||
node_id="easy promptList",
|
||
category="EasyUse/Prompt",
|
||
inputs=[
|
||
io.String.Input("prompt_1", multiline=True, default=""),
|
||
io.String.Input("prompt_2", multiline=True, default=""),
|
||
io.String.Input("prompt_3", multiline=True, default=""),
|
||
io.String.Input("prompt_4", multiline=True, default=""),
|
||
io.String.Input("prompt_5", multiline=True, default=""),
|
||
io.Custom(io_type="LIST").Input("optional_prompt_list", optional=True),
|
||
],
|
||
outputs=[
|
||
io.Custom(io_type="LIST").Output("prompt_list"),
|
||
io.String.Output("prompt_strings", is_output_list=True),
|
||
],
|
||
)
|
||
|
||
@classmethod
|
||
def execute(cls, prompt_1="", prompt_2="", prompt_3="", prompt_4="", prompt_5="", optional_prompt_list=None, **kwargs):
|
||
prompts = []
|
||
|
||
if optional_prompt_list:
|
||
for l in optional_prompt_list:
|
||
prompts.append(l)
|
||
|
||
# Add individual prompts
|
||
for p in [prompt_1, prompt_2, prompt_3, prompt_4, prompt_5]:
|
||
if isinstance(p, str) and p != '':
|
||
prompts.append(p)
|
||
|
||
return io.NodeOutput(prompts, prompts)
|
||
|
||
#promptLine
|
||
class promptLine(io.ComfyNode):
|
||
|
||
@classmethod
|
||
def define_schema(cls):
|
||
return io.Schema(
|
||
node_id="easy promptLine",
|
||
category="EasyUse/Prompt",
|
||
inputs=[
|
||
io.String.Input("prompt", multiline=True, default="text"),
|
||
io.Int.Input("start_index", default=0, min=0, max=9999),
|
||
io.Int.Input("max_rows", default=1000, min=1, max=9999),
|
||
io.Boolean.Input("remove_empty_lines", default=True),
|
||
],
|
||
outputs=[
|
||
io.String.Output("STRING", is_output_list=True),
|
||
io.Combo.Output("COMBO", is_output_list=True),
|
||
],
|
||
hidden=[
|
||
io.Hidden.prompt,
|
||
io.Hidden.unique_id,
|
||
],
|
||
)
|
||
|
||
@classmethod
|
||
def execute(cls, prompt, start_index, max_rows, remove_empty_lines=True, **kwargs):
|
||
lines = prompt.split('\n')
|
||
|
||
if remove_empty_lines:
|
||
lines = [line for line in lines if line.strip()]
|
||
|
||
start_index = max(0, min(start_index, len(lines) - 1))
|
||
|
||
end_index = min(start_index + max_rows, len(lines))
|
||
|
||
rows = lines[start_index:end_index]
|
||
|
||
return io.NodeOutput(rows, rows)
|
||
|
||
import comfy.utils
|
||
from server import PromptServer
|
||
from ..libs.messages import MessageCancelled, Message
|
||
class promptAwait(io.ComfyNode):
|
||
|
||
@classmethod
|
||
def define_schema(cls):
|
||
return io.Schema(
|
||
node_id="easy promptAwait",
|
||
category="EasyUse/Prompt",
|
||
inputs=[
|
||
io.AnyType.Input("now"),
|
||
io.String.Input("prompt", multiline=True, default="", placeholder="Enter a prompt or use voice to enter to text"),
|
||
io.Custom(io_type="EASY_PROMPT_AWAIT_BAR").Input("toolbar"),
|
||
io.AnyType.Input("prev", optional=True),
|
||
],
|
||
outputs=[
|
||
io.AnyType.Output(id="output", display_name="output"),
|
||
io.String.Output(id="output_prompt", display_name="prompt"),
|
||
io.Boolean.Output("continue"),
|
||
io.Int.Output("seed"),
|
||
],
|
||
hidden=[
|
||
io.Hidden.prompt,
|
||
io.Hidden.unique_id,
|
||
io.Hidden.extra_pnginfo,
|
||
],
|
||
)
|
||
|
||
@classmethod
|
||
def execute(cls, now, prompt, toolbar, prev=None, **kwargs):
|
||
id = cls.hidden.unique_id
|
||
id = id.split('.')[len(id.split('.')) - 1] if "." in id else id
|
||
if ":" in id:
|
||
id = id.split(":")[0]
|
||
pbar = comfy.utils.ProgressBar(100)
|
||
pbar.update_absolute(30)
|
||
PromptServer.instance.send_sync('easyuse_prompt_await', {"id": id})
|
||
try:
|
||
res = Message.waitForMessage(id, asList=False)
|
||
if res is None or res == "-1":
|
||
result = (now, prompt, False, 0)
|
||
else:
|
||
input = now if res['select'] == 'now' or prev is None else prev
|
||
result = (input, res['prompt'], False if res['result'] == -1 else True, res['seed'] if res['unlock'] else res['last_seed'])
|
||
pbar.update_absolute(100)
|
||
return io.NodeOutput(*result)
|
||
except MessageCancelled:
|
||
pbar.update_absolute(100)
|
||
raise comfy.model_management.InterruptProcessingException()
|
||
|
||
class promptConcat(io.ComfyNode):
|
||
@classmethod
|
||
def define_schema(cls):
|
||
return io.Schema(
|
||
node_id="easy promptConcat",
|
||
category="EasyUse/Prompt",
|
||
inputs=[
|
||
io.String.Input("prompt1", multiline=False, default="", force_input=True, optional=True),
|
||
io.String.Input("prompt2", multiline=False, default="", force_input=True, optional=True),
|
||
io.String.Input("separator", multiline=False, default="", optional=True),
|
||
],
|
||
outputs=[
|
||
io.String.Output("prompt"),
|
||
],
|
||
)
|
||
|
||
@classmethod
|
||
def execute(cls, prompt1="", prompt2="", separator=""):
|
||
return io.NodeOutput(prompt1 + separator + prompt2)
|
||
|
||
class promptReplace(io.ComfyNode):
|
||
|
||
@classmethod
|
||
def define_schema(cls):
|
||
return io.Schema(
|
||
node_id="easy promptReplace",
|
||
category="EasyUse/Prompt",
|
||
inputs=[
|
||
io.String.Input("prompt", multiline=True, default="", force_input=True),
|
||
io.String.Input("find1", multiline=False, default="", optional=True),
|
||
io.String.Input("replace1", multiline=False, default="", optional=True),
|
||
io.String.Input("find2", multiline=False, default="", optional=True),
|
||
io.String.Input("replace2", multiline=False, default="", optional=True),
|
||
io.String.Input("find3", multiline=False, default="", optional=True),
|
||
io.String.Input("replace3", multiline=False, default="", optional=True),
|
||
],
|
||
outputs=[
|
||
io.String.Output(id="output_prompt",display_name="prompt"),
|
||
],
|
||
)
|
||
|
||
@classmethod
|
||
def execute(cls, prompt, find1="", replace1="", find2="", replace2="", find3="", replace3=""):
|
||
prompt = prompt.replace(find1, replace1)
|
||
prompt = prompt.replace(find2, replace2)
|
||
prompt = prompt.replace(find3, replace3)
|
||
|
||
return io.NodeOutput(prompt)
|
||
|
||
|
||
# 肖像大师
|
||
# Created by AI Wiz Art (Stefano Flore)
|
||
# Version: 2.2
|
||
# https://stefanoflore.it
|
||
# https://ai-wiz.art
|
||
class portraitMaster(io.ComfyNode):
|
||
|
||
@classmethod
|
||
def define_schema(cls):
|
||
max_float_value = 1.95
|
||
prompt_path = os.path.join(RESOURCES_DIR, 'portrait_prompt.json')
|
||
if not os.path.exists(prompt_path):
|
||
response = urlopen('https://raw.githubusercontent.com/yolain/ComfyUI-Easy-Use/main/resources/portrait_prompt.json')
|
||
temp_prompt = json.loads(response.read())
|
||
prompt_serialized = json.dumps(temp_prompt, indent=4)
|
||
with open(prompt_path, "w") as f:
|
||
f.write(prompt_serialized)
|
||
del response, temp_prompt
|
||
# Load local
|
||
with open(prompt_path, 'r') as f:
|
||
data = json.load(f)
|
||
|
||
inputs = []
|
||
# Shot
|
||
inputs.append(io.Combo.Input("shot", options=['-'] + data['shot_list']))
|
||
inputs.append(io.Float.Input("shot_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
# Gender and age
|
||
inputs.append(io.Combo.Input("gender", options=['-'] + data['gender_list'], default="Woman"))
|
||
inputs.append(io.Int.Input("age", default=30, min=18, max=90, step=1, display_mode=io.NumberDisplay.slider))
|
||
# Nationality
|
||
inputs.append(io.Combo.Input("nationality_1", options=['-'] + data['nationality_list'], default="Chinese"))
|
||
inputs.append(io.Combo.Input("nationality_2", options=['-'] + data['nationality_list']))
|
||
inputs.append(io.Float.Input("nationality_mix", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
# Body
|
||
inputs.append(io.Combo.Input("body_type", options=['-'] + data['body_type_list']))
|
||
inputs.append(io.Float.Input("body_type_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
inputs.append(io.Combo.Input("model_pose", options=['-'] + data['model_pose_list']))
|
||
inputs.append(io.Combo.Input("eyes_color", options=['-'] + data['eyes_color_list']))
|
||
# Face
|
||
inputs.append(io.Combo.Input("facial_expression", options=['-'] + data['face_expression_list']))
|
||
inputs.append(io.Float.Input("facial_expression_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
inputs.append(io.Combo.Input("face_shape", options=['-'] + data['face_shape_list']))
|
||
inputs.append(io.Float.Input("face_shape_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
inputs.append(io.Float.Input("facial_asymmetry", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
# Hair
|
||
inputs.append(io.Combo.Input("hair_style", options=['-'] + data['hair_style_list']))
|
||
inputs.append(io.Combo.Input("hair_color", options=['-'] + data['hair_color_list']))
|
||
inputs.append(io.Float.Input("disheveled", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
inputs.append(io.Combo.Input("beard", options=['-'] + data['beard_list']))
|
||
# Skin details
|
||
inputs.append(io.Float.Input("skin_details", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
inputs.append(io.Float.Input("skin_pores", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
inputs.append(io.Float.Input("dimples", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
inputs.append(io.Float.Input("freckles", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
inputs.append(io.Float.Input("moles", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
inputs.append(io.Float.Input("skin_imperfections", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
inputs.append(io.Float.Input("skin_acne", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
inputs.append(io.Float.Input("tanned_skin", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
# Eyes
|
||
inputs.append(io.Float.Input("eyes_details", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
inputs.append(io.Float.Input("iris_details", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
inputs.append(io.Float.Input("circular_iris", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
inputs.append(io.Float.Input("circular_pupil", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
# Light
|
||
inputs.append(io.Combo.Input("light_type", options=['-'] + data['light_type_list']))
|
||
inputs.append(io.Combo.Input("light_direction", options=['-'] + data['light_direction_list']))
|
||
inputs.append(io.Float.Input("light_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||
# Additional
|
||
inputs.append(io.Combo.Input("photorealism_improvement", options=["enable", "disable"]))
|
||
inputs.append(io.String.Input("prompt_start", multiline=True, default="raw photo, (realistic:1.5)"))
|
||
inputs.append(io.String.Input("prompt_additional", multiline=True, default=""))
|
||
inputs.append(io.String.Input("prompt_end", multiline=True, default=""))
|
||
inputs.append(io.String.Input("negative_prompt", multiline=True, default=""))
|
||
|
||
return io.Schema(
|
||
node_id="easy portraitMaster",
|
||
category="EasyUse/Prompt",
|
||
inputs=inputs,
|
||
outputs=[
|
||
io.String.Output("positive"),
|
||
io.String.Output("negative"),
|
||
],
|
||
)
|
||
|
||
@classmethod
|
||
def execute(cls, shot="-", shot_weight=1, gender="-", body_type="-", body_type_weight=0, eyes_color="-",
|
||
facial_expression="-", facial_expression_weight=0, face_shape="-", face_shape_weight=0,
|
||
nationality_1="-", nationality_2="-", nationality_mix=0.5, age=30, hair_style="-", hair_color="-",
|
||
disheveled=0, dimples=0, freckles=0, skin_pores=0, skin_details=0, moles=0, skin_imperfections=0,
|
||
wrinkles=0, tanned_skin=0, eyes_details=1, iris_details=1, circular_iris=1, circular_pupil=1,
|
||
facial_asymmetry=0, prompt_additional="", prompt_start="", prompt_end="", light_type="-",
|
||
light_direction="-", light_weight=0, negative_prompt="", photorealism_improvement="disable", beard="-",
|
||
model_pose="-", skin_acne=0):
|
||
|
||
prompt = []
|
||
|
||
if gender == "-":
|
||
gender = ""
|
||
else:
|
||
if age <= 25 and gender == 'Woman':
|
||
gender = 'girl'
|
||
if age <= 25 and gender == 'Man':
|
||
gender = 'boy'
|
||
gender = " " + gender + " "
|
||
|
||
if nationality_1 != '-' and nationality_2 != '-':
|
||
nationality = f"[{nationality_1}:{nationality_2}:{round(nationality_mix, 2)}]"
|
||
elif nationality_1 != '-':
|
||
nationality = nationality_1 + " "
|
||
elif nationality_2 != '-':
|
||
nationality = nationality_2 + " "
|
||
else:
|
||
nationality = ""
|
||
|
||
if prompt_start != "":
|
||
prompt.append(f"{prompt_start}")
|
||
|
||
if shot != "-" and shot_weight > 0:
|
||
prompt.append(f"({shot}:{round(shot_weight, 2)})")
|
||
|
||
prompt.append(f"({nationality}{gender}{round(age)}-years-old:1.5)")
|
||
|
||
if body_type != "-" and body_type_weight > 0:
|
||
prompt.append(f"({body_type}, {body_type} body:{round(body_type_weight, 2)})")
|
||
|
||
if model_pose != "-":
|
||
prompt.append(f"({model_pose}:1.5)")
|
||
|
||
if eyes_color != "-":
|
||
prompt.append(f"({eyes_color} eyes:1.25)")
|
||
|
||
if facial_expression != "-" and facial_expression_weight > 0:
|
||
prompt.append(
|
||
f"({facial_expression}, {facial_expression} expression:{round(facial_expression_weight, 2)})")
|
||
|
||
if face_shape != "-" and face_shape_weight > 0:
|
||
prompt.append(f"({face_shape} shape face:{round(face_shape_weight, 2)})")
|
||
|
||
if hair_style != "-":
|
||
prompt.append(f"({hair_style} hairstyle:1.25)")
|
||
|
||
if hair_color != "-":
|
||
prompt.append(f"({hair_color} hair:1.25)")
|
||
|
||
if beard != "-":
|
||
prompt.append(f"({beard}:1.15)")
|
||
|
||
if disheveled != "-" and disheveled > 0:
|
||
prompt.append(f"(disheveled:{round(disheveled, 2)})")
|
||
|
||
if prompt_additional != "":
|
||
prompt.append(f"{prompt_additional}")
|
||
|
||
if skin_details > 0:
|
||
prompt.append(f"(skin details, skin texture:{round(skin_details, 2)})")
|
||
|
||
if skin_pores > 0:
|
||
prompt.append(f"(skin pores:{round(skin_pores, 2)})")
|
||
|
||
if skin_imperfections > 0:
|
||
prompt.append(f"(skin imperfections:{round(skin_imperfections, 2)})")
|
||
|
||
if skin_acne > 0:
|
||
prompt.append(f"(acne, skin with acne:{round(skin_acne, 2)})")
|
||
|
||
if wrinkles > 0:
|
||
prompt.append(f"(skin imperfections:{round(wrinkles, 2)})")
|
||
|
||
if tanned_skin > 0:
|
||
prompt.append(f"(tanned skin:{round(tanned_skin, 2)})")
|
||
|
||
if dimples > 0:
|
||
prompt.append(f"(dimples:{round(dimples, 2)})")
|
||
|
||
if freckles > 0:
|
||
prompt.append(f"(freckles:{round(freckles, 2)})")
|
||
|
||
if moles > 0:
|
||
prompt.append(f"(skin pores:{round(moles, 2)})")
|
||
|
||
if eyes_details > 0:
|
||
prompt.append(f"(eyes details:{round(eyes_details, 2)})")
|
||
|
||
if iris_details > 0:
|
||
prompt.append(f"(iris details:{round(iris_details, 2)})")
|
||
|
||
if circular_iris > 0:
|
||
prompt.append(f"(circular iris:{round(circular_iris, 2)})")
|
||
|
||
if circular_pupil > 0:
|
||
prompt.append(f"(circular pupil:{round(circular_pupil, 2)})")
|
||
|
||
if facial_asymmetry > 0:
|
||
prompt.append(f"(facial asymmetry, face asymmetry:{round(facial_asymmetry, 2)})")
|
||
|
||
if light_type != '-' and light_weight > 0:
|
||
if light_direction != '-':
|
||
prompt.append(f"({light_type} {light_direction}:{round(light_weight, 2)})")
|
||
else:
|
||
prompt.append(f"({light_type}:{round(light_weight, 2)})")
|
||
|
||
if prompt_end != "":
|
||
prompt.append(f"{prompt_end}")
|
||
|
||
prompt = ", ".join(prompt)
|
||
prompt = prompt.lower()
|
||
|
||
if photorealism_improvement == "enable":
|
||
prompt = prompt + ", (professional photo, balanced photo, balanced exposure:1.2), (film grain:1.15)"
|
||
|
||
if photorealism_improvement == "enable":
|
||
negative_prompt = negative_prompt + ", (shinny skin, reflections on the skin, skin reflections:1.25)"
|
||
|
||
log_node_info("Portrait Master as generate the prompt:", prompt)
|
||
|
||
return io.NodeOutput(prompt, negative_prompt)
|
||
|
||
# 多角度
|
||
class multiAngle(io.ComfyNode):
|
||
|
||
@classmethod
|
||
def define_schema(cls):
|
||
return io.Schema(
|
||
node_id="easy multiAngle",
|
||
category="EasyUse/Prompt",
|
||
inputs=[
|
||
io.Custom(io_type="EASY_MULTI_ANGLE").Input("multi_angle", optional=True),
|
||
],
|
||
outputs=[
|
||
io.String.Output("prompt", is_output_list=True),
|
||
io.Custom(io_type="EASY_MULTI_ANGLE").Output("params"),
|
||
],
|
||
)
|
||
|
||
@classmethod
|
||
def execute(cls, multi_angle=None, **kwargs):
|
||
if multi_angle is None:
|
||
return io.NodeOutput([""])
|
||
|
||
if isinstance(multi_angle, str):
|
||
try:
|
||
multi_angle = json.loads(multi_angle)
|
||
except:
|
||
raise Exception(f"Invalid multi angle: {multi_angle}")
|
||
|
||
prompts = []
|
||
for angle_data in multi_angle:
|
||
rotate = angle_data.get("rotate", 0)
|
||
vertical = angle_data.get("vertical", 0)
|
||
zoom = angle_data.get("zoom", 5)
|
||
add_angle_prompt = angle_data.get("add_angle_prompt", True)
|
||
|
||
# Validate input ranges
|
||
rotate = max(0, min(360, int(rotate)))
|
||
vertical = max(-90, min(90, int(vertical)))
|
||
zoom = max(0.0, min(10.0, float(zoom)))
|
||
|
||
h_angle = rotate % 360
|
||
|
||
# Horizontal direction mapping
|
||
h_suffix = "" if add_angle_prompt else " quarter"
|
||
if h_angle < 22.5 or h_angle >= 337.5: h_direction = "front view"
|
||
elif h_angle < 67.5: h_direction = f"front-right{h_suffix} view"
|
||
elif h_angle < 112.5: h_direction = "right side view"
|
||
elif h_angle < 157.5: h_direction = f"back-right{h_suffix} view"
|
||
elif h_angle < 202.5: h_direction = "back view"
|
||
elif h_angle < 247.5: h_direction = f"back-left{h_suffix} view"
|
||
elif h_angle < 292.5: h_direction = "left side view"
|
||
else: h_direction = f"front-left{h_suffix} view"
|
||
|
||
# Vertical direction mapping
|
||
if add_angle_prompt:
|
||
if vertical == -90:
|
||
v_direction = "bottom-looking-up perspective, extreme worm's eye view, focus subject bottom"
|
||
elif vertical < -75:
|
||
v_direction = "bottom-looking-up perspective, extreme worm's eye view"
|
||
elif vertical < -45:
|
||
v_direction = "ultra-low angle"
|
||
elif vertical < -15:
|
||
v_direction = "low angle"
|
||
elif vertical < 15:
|
||
v_direction = "eye level"
|
||
elif vertical < 45:
|
||
v_direction = "high angle"
|
||
elif vertical < 75:
|
||
v_direction = "bird's eye view"
|
||
elif vertical < 90:
|
||
v_direction = "top-down perspective, looking straight down at the top of the subject"
|
||
else:
|
||
v_direction = "top-down perspective, looking straight down at the top of the subject, face not visible, focus on subject head"
|
||
else:
|
||
if vertical < -15:
|
||
v_direction = "low-angle shot"
|
||
elif vertical < 15:
|
||
v_direction = "eye-level shot"
|
||
elif vertical < 45:
|
||
v_direction = "elevated shot"
|
||
elif vertical < 75:
|
||
v_direction = "high-angle shot"
|
||
elif vertical < 90:
|
||
v_direction = "top-down perspective, looking straight down at the top of the subject"
|
||
else:
|
||
v_direction = "top-down perspective, looking straight down at the top of the subject, face not visible, focus on subject head"
|
||
|
||
# Distance/zoom mapping
|
||
if add_angle_prompt:
|
||
if zoom < 2: distance = "extreme wide shot"
|
||
elif zoom < 4: distance = "wide shot"
|
||
elif zoom < 6: distance = "medium shot"
|
||
elif zoom < 8: distance = "close-up"
|
||
else: distance = "extreme close-up"
|
||
else:
|
||
if zoom < 2: distance = "extreme wide shot"
|
||
elif zoom < 4: distance = "wide shot"
|
||
elif zoom < 6: distance = "medium shot"
|
||
elif zoom < 8: distance = "close-up"
|
||
else: distance = "extreme close-up"
|
||
|
||
# Build prompt
|
||
if add_angle_prompt:
|
||
prompt = f"{h_direction}, {v_direction}, {distance} (horizontal: {rotate}, vertical: {vertical}, zoom: {zoom:.1f})"
|
||
else:
|
||
prompt = f"{h_direction} {v_direction} {distance}"
|
||
|
||
prompts.append(prompt)
|
||
|
||
return io.NodeOutput(prompts, multi_angle)
|
||
|
||
|
||
NODE_CLASS_MAPPINGS = {
|
||
"easy positive": positivePrompt,
|
||
"easy negative": negativePrompt,
|
||
"easy wildcards": wildcardsPrompt,
|
||
"easy wildcardsMatrix": wildcardsPromptMatrix,
|
||
"easy prompt": prompt,
|
||
"easy promptList": promptList,
|
||
"easy promptLine": promptLine,
|
||
"easy promptAwait": promptAwait,
|
||
"easy promptConcat": promptConcat,
|
||
"easy promptReplace": promptReplace,
|
||
"easy stylesSelector": stylesPromptSelector,
|
||
"easy portraitMaster": portraitMaster,
|
||
"easy multiAngle": multiAngle,
|
||
}
|
||
|
||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||
"easy positive": "Positive",
|
||
"easy negative": "Negative",
|
||
"easy wildcards": "Wildcards",
|
||
"easy wildcardsMatrix": "Wildcards Matrix",
|
||
"easy prompt": "Prompt",
|
||
"easy promptList": "PromptList",
|
||
"easy promptLine": "PromptLine",
|
||
"easy promptAwait": "PromptAwait",
|
||
"easy promptConcat": "PromptConcat",
|
||
"easy promptReplace": "PromptReplace",
|
||
"easy stylesSelector": "Styles Selector",
|
||
"easy portraitMaster": "Portrait Master",
|
||
"easy multiAngle": "Multi Angle",
|
||
}
|