Files
jaidaken f09734b0ee
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
Add custom nodes, Civitai loras (LFS), and vast.ai setup script
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>
2026-02-09 00:56:42 +00:00

791 lines
34 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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",
}