feat: add AODH Image Saver (Metadata), Lora Selector, Checkpoint Selector, and various node improvements

This commit is contained in:
Aodhan Collins
2026-02-06 03:41:15 +00:00
parent 2417dcc090
commit 644ab104d9
54 changed files with 1483 additions and 104 deletions

View File

@@ -12,8 +12,9 @@ The **Character JSON Reader** node allows you to load character definitions from
- **Automatic File Discovery**: Scans the `nodes/character_reader/characters/` folder for `.json` files. - **Automatic File Discovery**: Scans the `nodes/character_reader/characters/` folder for `.json` files.
- **Selection Modes**: - **Selection Modes**:
- `manual`: Select a specific file from the dropdown. - `manual`: Select a specific file from the dropdown.
- `sequential`: Cycles through files based on the `index` input. - `sequential`: Cycles through files, starting from the selected file.
- `random`: Picks a random file using the `index` as a seed. - `random`: Picks a random file. Uses a fresh random seed on every execution.
- **Repeat Count**: Keeps the same selection for a specified number of executions before changing.
- **Structured Outputs**: Provides 21 distinct output pins covering identity, wardrobe, and style. - **Structured Outputs**: Provides 21 distinct output pins covering identity, wardrobe, and style.
- **Prompt-Ready Formatting**: Automatically appends a comma to every non-empty output string. - **Prompt-Ready Formatting**: Automatically appends a comma to every non-empty output string.
- **Robustness**: Ensures all outputs are valid strings, even if fields are missing from the JSON. - **Robustness**: Ensures all outputs are valid strings, even if fields are missing from the JSON.
@@ -24,11 +25,77 @@ The **Resolution Reader** node reads resolution configurations from text files.
#### Features: #### Features:
- **Selection Modes**: - **Selection Modes**:
- `manual`: Uses the first line of the selected file. - `manual`: Uses the selected line from the dropdown.
- `sequential`: Cycles through lines in the file based on the `index` input. - `sequential`: Cycles through lines in the file, starting from the selected line.
- `random`: Picks a random line from the file using the `index` as a seed. - `random`: Picks a random line from the file. Uses a fresh random seed on every execution.
- **Repeat Count**: Keeps the same selection for a specified number of executions before changing.
- **Outputs**: Provides `width` (INT), `height` (INT), and `upscale` (FLOAT). - **Outputs**: Provides `width` (INT), `height` (INT), and `upscale` (FLOAT).
### Lora Selector
The **Lora Selector** node allows you to select a folder of Lora files (from `ComfyUI/models/Lora/Illustrious/`) and pick one `.safetensors` file at a time, either randomly or sequentially.
#### Features:
- **Folder Selection**: Scans `ComfyUI/models/Lora/Illustrious/` for subdirectories.
- **Selection Modes**:
- `Random`: Picks a random Lora from the folder. Uses a fresh random seed on every execution.
- `Sequential`: Cycles through the Lora files in the folder, starting from the `manual_index`.
- `Manual`: Selects the Lora at the specified `manual_index`.
- **Repeat Count**: Keeps the same selection for a specified number of executions before changing.
- **Manual Index**: Specifies the starting index for `Sequential` mode or the specific index for `Manual` mode.
- **Outputs**:
- `lora_name` (STRING): The relative path to the selected Lora.
- `total_loras` (INT): The total number of Lora files in the selected folder.
### Checkpoint Selector
The **Checkpoint Selector** node allows you to select a folder of Checkpoint files (from `ComfyUI/models/Stable-diffusion/`) and pick one `.safetensors` or `.ckpt` file at a time, either randomly or sequentially.
#### Features:
- **Folder Selection**: Scans `ComfyUI/models/Stable-diffusion/` (and other checkpoint paths) for subdirectories.
- **Selection Modes**:
- `Random`: Picks a random Checkpoint from the folder. Uses a fresh random seed on every execution.
- `Sequential`: Cycles through the Checkpoint files in the folder, starting from the `manual_index`.
- `Manual`: Selects the Checkpoint at the specified `manual_index`.
- **Repeat Count**: Keeps the same selection for a specified number of executions before changing.
- **Manual Index**: Specifies the starting index for `Sequential` mode or the specific index for `Manual` mode.
- **Outputs**:
- `checkpoint_name` (STRING): The relative path to the selected Checkpoint.
- `total_checkpoints` (INT): The total number of Checkpoint files in the selected folder.
### Checkpoint Loader (From String)
The **Checkpoint Loader (From String)** node loads a checkpoint model using a string input (e.g., from the **Checkpoint Selector** node).
#### Inputs:
- `ckpt_name` (STRING): The name/path of the checkpoint to load.
#### Outputs:
- `MODEL`: The loaded model.
- `CLIP`: The loaded CLIP model.
- `VAE`: The loaded VAE model.
### AODH Image Saver (Metadata)
The **AODH Image Saver (Metadata)** node saves images with comprehensive metadata in both A1111-compatible and extended ComfyUI formats.
#### Features:
- **Dynamic Directory Support**: Specify a `save_directory` with support for date formatting:
- `date:yyyy-MM-dd`: Replaces with formatted date (e.g., `2024-05-20`).
- Standard `strftime` patterns like `%Y-%m-%d`.
- **Comprehensive Metadata**: Embeds prompt, sampling parameters, model info, and LoRA details.
- **LoRA Handling**: Automatically strips directory paths from LoRA names for cleaner metadata.
- **Image Pass-through**: Includes an image output pin to continue the workflow after saving.
#### Inputs:
- `images` (IMAGE): The images to save.
- `filename_prefix` (STRING): The prefix for the saved files.
- `save_directory` (STRING): The folder to save images in (defaults to current date).
- Various optional metadata fields (prompts, checkpoint, lora, etc.).
#### Outputs:
- `images` (IMAGE): The input images passed through.
#### JSON Structure: #### JSON Structure:
Place your character JSON files in the `nodes/character_reader/characters/` directory. The expected format is: Place your character JSON files in the `nodes/character_reader/characters/` directory. The expected format is:

35
character_ids.txt Normal file
View File

@@ -0,0 +1,35 @@
aerith_gainsborough
android_18
anya_forger
bulma
camilla
cammy
chun_li
ciri
hatsune_miku
jessica_rabbit
jessie
jinx
k/da_all_out_ahri
k/da_all_out_akali
k/da_all_out_evelynn
k/da_all_out_kai'sa
komi_shouko
lara_croft_classic
lulu (ffx)
majin_android_21
marin_kitagawa
nessa
princess_peach
princess_zelda_botw
riju
rosalina
rouge_the_bat
ryouko_(tenchi_muyou!)
samus_aran
sucy_manbavaran
tifa_lockhart
urbosa
yor_briar
y'shtola_rhul
yuna_ffx

122
comfyui_metadata_spec.json Normal file
View File

@@ -0,0 +1,122 @@
{
"_description": "Sample metadata structure for ComfyUI-generated images with A1111-compatible format.\n This format allows the app to parse ComfyUI images using the same parser as A1111 images,\n while preserving ComfyUI-specific metadata for advanced features.\n \n A ComfyUI node should embed this metadata in the 'parameters' PNG text chunk.",
"parameters": "masterpiece, best quality, amazing quality, very aesthetic, high resolution, ultra-detailed, 1girl, solo, looking at viewer, smile, long hair, blue eyes, school uniform\nNegative prompt: embedding:Illustrious/lazyneg_1760455, artist name, signature, watermark, blurry, low quality, bad anatomy, text, censored, deformed, bad hand\nSteps: 30, Sampler: euler_ancestral, CFG scale: 6.0, Seed: 927008042550191, Size: 1280x800, Model: illustrij_v20.safetensors, Model hash: a1b2c3d4e5f6, Clip skip: 2, RNG: GPU, Tiling: false, Restore faces: false, Hires upscale: 1.5, Hires steps: 15, Hires upscaler: RealESRGAN_x4plus_anime_6B, Denoising strength: 0.4",
"comfyui": true,
"comfyui_metadata": {
"_description": "Extended metadata specific to ComfyUI workflow",
"workflow_name": "Character Generator",
"workflow_version": "1.0.0",
"generation": {
"checkpoint": "Illustrious/illustrij_v20.safetensors",
"checkpoint_hash": "a1b2c3d4e5f6",
"vae": "sdxl_vae.safetensors",
"clip_skip": 2,
"lora": [
{
"name": "Illustrious/Styles/Noob/jyojifuku_noobai_V1.0.safetensors",
"strength_model": 1.0,
"strength_clip": 1.0
}
]
},
"sampling": {
"sampler": "euler_ancestral",
"scheduler": "karras",
"steps": 30,
"cfg": 6.0,
"seed": 927008042550191,
"batch_size": 1
},
"resolution": {
"width": 1280,
"height": 800,
"upscale_factor": 1.5,
"upscaler": "RealESRGAN_x4plus_anime_6B",
"hires_steps": 15,
"denoise_strength": 0.4
},
"prompt_structure": {
"positive": {
"full": "masterpiece, best quality, amazing quality, very aesthetic, high resolution, ultra-detailed, 1girl, solo, looking at viewer, smile, long hair, blue eyes, school uniform",
"quality_tags": "masterpiece, best quality, amazing quality, very aesthetic, high resolution, ultra-detailed",
"character": {
"name": "sucy_manbavaran",
"description": "1girl, solo, looking at viewer, smile, long hair, blue eyes",
"outfit": "school uniform"
},
"style_tags": ""
},
"negative": {
"full": "embedding:Illustrious/lazyneg_1760455, artist name, signature, watermark, blurry, low quality, bad anatomy, text, censored, deformed, bad hand",
"embeddings": ["Illustrious/lazyneg_1760455"]
}
},
"post_processing": {
"detailers": [
{
"type": "FaceDetailer",
"enabled": true,
"guide_size": 512,
"steps": 20,
"denoise": 0.25
},
{
"type": "HandDetailer",
"enabled": true,
"guide_size": 512,
"steps": 24,
"denoise": 0.35
}
],
"color_match": {
"enabled": true,
"color_space": "LAB",
"luminance_factor": 1.0,
"color_intensity_factor": 1.05
}
},
"workflow": {
"nodes": {
"prompt_node_id": "374",
"prompt_node_type": "ShowText|pysssss",
"prompt_node_title": "Full Prompt",
"seed_node_id": "164",
"sampler_node_id": "169",
"checkpoint_loader_id": "332"
},
"groups": [
"Sampler",
"Character",
"FaceDetailer",
"HandDetailer",
"Upscaler"
]
}
},
"app_metadata": {
"_description": "Optional metadata for this app to enable advanced features",
"tags": ["1girl", "solo", "school uniform", "smile"],
"character": "sucy_manbavaran",
"rating": "safe",
"favorite": false,
"collections": ["Illustrious Characters"],
"custom_fields": {
"artist": "",
"source_url": "",
"notes": ""
}
}
}

44
extract_character_ids.py Normal file
View File

@@ -0,0 +1,44 @@
import json
import os
def extract_ids():
characters_dir = 'nodes/character_reader/characters'
output_file = 'character_ids.txt'
if not os.path.exists(characters_dir):
print(f"Directory not found: {characters_dir}")
return
character_ids = []
# List all files in the directory
files = os.listdir(characters_dir)
# Sort files to ensure consistent output order
files.sort()
for filename in files:
if filename.endswith('.json'):
filepath = os.path.join(characters_dir, filename)
try:
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)
if 'character_id' in data:
character_ids.append(data['character_id'])
else:
print(f"Warning: 'character_id' not found in {filename}")
except json.JSONDecodeError:
print(f"Error decoding JSON in {filename}")
except Exception as e:
print(f"Error reading {filename}: {e}")
# Write to output file
try:
with open(output_file, 'w', encoding='utf-8') as f:
for char_id in character_ids:
f.write(f"{char_id}\n")
print(f"Successfully wrote {len(character_ids)} character IDs to {output_file}")
except Exception as e:
print(f"Error writing to output file: {e}")
if __name__ == "__main__":
extract_ids()

View File

@@ -2,19 +2,31 @@ from .character_reader import NODE_CLASS_MAPPINGS as CR_CLASS, NODE_DISPLAY_NAME
from .resolution_reader import NODE_CLASS_MAPPINGS as RR_CLASS, NODE_DISPLAY_NAME_MAPPINGS as RR_DISPLAY from .resolution_reader import NODE_CLASS_MAPPINGS as RR_CLASS, NODE_DISPLAY_NAME_MAPPINGS as RR_DISPLAY
from .reenforcer import NODE_CLASS_MAPPINGS as RE_CLASS, NODE_DISPLAY_NAME_MAPPINGS as RE_DISPLAY from .reenforcer import NODE_CLASS_MAPPINGS as RE_CLASS, NODE_DISPLAY_NAME_MAPPINGS as RE_DISPLAY
from .lora_from_string import NODE_CLASS_MAPPINGS as LFS_CLASS, NODE_DISPLAY_NAME_MAPPINGS as LFS_DISPLAY from .lora_from_string import NODE_CLASS_MAPPINGS as LFS_CLASS, NODE_DISPLAY_NAME_MAPPINGS as LFS_DISPLAY
from .lora_selector import NODE_CLASS_MAPPINGS as LS_CLASS, NODE_DISPLAY_NAME_MAPPINGS as LS_DISPLAY
from .checkpoint_selector import NODE_CLASS_MAPPINGS as CS_CLASS, NODE_DISPLAY_NAME_MAPPINGS as CS_DISPLAY
from .checkpoint_from_string import NODE_CLASS_MAPPINGS as CFS_CLASS, NODE_DISPLAY_NAME_MAPPINGS as CFS_DISPLAY
from .metadata_saver import NODE_CLASS_MAPPINGS as MS_CLASS, NODE_DISPLAY_NAME_MAPPINGS as MS_DISPLAY
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
**CR_CLASS, **CR_CLASS,
**RR_CLASS, **RR_CLASS,
**RE_CLASS, **RE_CLASS,
**LFS_CLASS **LFS_CLASS,
**LS_CLASS,
**CS_CLASS,
**CFS_CLASS,
**MS_CLASS
} }
NODE_DISPLAY_NAME_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = {
**CR_DISPLAY, **CR_DISPLAY,
**RR_DISPLAY, **RR_DISPLAY,
**RE_DISPLAY, **RE_DISPLAY,
**LFS_DISPLAY **LFS_DISPLAY,
**LS_DISPLAY,
**CS_DISPLAY,
**CFS_DISPLAY,
**MS_DISPLAY
} }
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']

View File

@@ -30,45 +30,59 @@ class CharacterJsonReader:
"STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING",
"STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING",
"STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING",
"STRING", "FLOAT", "STRING" "STRING", "FLOAT", "STRING", "INT"
) )
RETURN_NAMES = ( RETURN_NAMES = (
"name", "base_specs", "hair", "eyes", "expression", "hands", "arms", "torso", "pelvis", "legs", "feet", "distinguishing_marks", "name", "base_specs", "hair", "eyes", "expression", "hands", "arms", "torso", "pelvis", "legs", "feet", "distinguishing_marks",
"inner_layer", "outer_layer", "lower_body", "footwear", "gloves", "accessories", "inner_layer", "outer_layer", "lower_body", "footwear", "gloves", "accessories",
"aesthetic", "primary_color", "secondary_color", "tertiary_color", "aesthetic", "primary_color", "secondary_color", "tertiary_color",
"lora_name", "lora_weight", "lora_triggers" "lora_name", "lora_weight", "lora_triggers", "total_characters"
) )
FUNCTION = "read_character" FUNCTION = "read_character"
CATEGORY = "AODH Pack" CATEGORY = "AODH Pack"
_current_index = 0 def __init__(self):
_current_count = 0 self.current_index = 0
_last_selection = None self.current_count = 0
self.last_selection = None
self.last_start_file = None
def read_character(self, character_file, selection_mode, repeat_count): def read_character(self, character_file, selection_mode, repeat_count):
repeat_count = max(1, repeat_count)
base_path = os.path.dirname(os.path.realpath(__file__)) base_path = os.path.dirname(os.path.realpath(__file__))
char_dir = os.path.join(base_path, "characters") char_dir = os.path.join(base_path, "characters")
characters = sorted([f for f in os.listdir(char_dir) if f.endswith('.json')]) characters = sorted([f for f in os.listdir(char_dir) if f.endswith('.json')])
if not characters: if not characters:
return ("",) * 22 + ("", 0.0, "") return ("",) * 22 + ("", 0.0, "", 0)
# Reset sequence if character_file changes
if self.last_start_file != character_file:
try:
self.current_index = characters.index(character_file)
except ValueError:
self.current_index = 0
self.current_count = 0
self.last_start_file = character_file
if selection_mode == "manual": if selection_mode == "manual":
selected_file = character_file selected_file = character_file
elif selection_mode == "sequential": elif selection_mode == "sequential":
if self.__class__._current_count >= repeat_count: if self.current_count >= repeat_count:
self.__class__._current_index += 1 self.current_index += 1
self.__class__._current_count = 0 self.current_count = 0
selected_file = characters[self.__class__._current_index % len(characters)] selected_file = characters[self.current_index % len(characters)]
self.__class__._current_count += 1 self.current_count += 1
elif selection_mode == "random": elif selection_mode == "random":
if self.__class__._current_count >= repeat_count or self.__class__._last_selection is None or self.__class__._last_selection not in characters: if self.current_count >= repeat_count or self.last_selection is None or self.last_selection not in characters:
self.__class__._last_selection = random.choice(characters) # Use a local Random instance to ensure randomness regardless of global seed
self.__class__._current_count = 0 rng = random.Random()
selected_file = self.__class__._last_selection self.last_selection = rng.choice(characters)
self.__class__._current_count += 1 self.current_count = 0
selected_file = self.last_selection
self.current_count += 1
else: else:
selected_file = character_file selected_file = character_file
@@ -114,4 +128,5 @@ class CharacterJsonReader:
get_val(lora, "lora_name", prepend_comma=False), get_val(lora, "lora_name", prepend_comma=False),
float(lora.get("lora_weight", 0.0) if lora.get("lora_weight") else 0.0), float(lora.get("lora_weight", 0.0) if lora.get("lora_weight") else 0.0),
get_val(lora, "lora_triggers"), get_val(lora, "lora_triggers"),
len(characters),
) )

View File

@@ -15,7 +15,7 @@
}, },
"wardrobe": { "wardrobe": {
"inner_layer": "black short-sleeved shirt", "inner_layer": "black short-sleeved shirt",
"outer_layer": "blue denim vest, Red Ribbon logo on back", "outer_layer": "blue denim vest, 'RR' text on back",
"lower_body": "blue denim skirt, black leggings", "lower_body": "blue denim skirt, black leggings",
"footwear": "brown boots", "footwear": "brown boots",
"gloves": "", "gloves": "",

View File

@@ -1,5 +1,5 @@
{ {
"character_id": "anya_forger", "character_id": "anya_(spy_x_family)",
"identity": { "identity": {
"base_specs": "1girl, small build, fair skin", "base_specs": "1girl, small build, fair skin",
"hair": "short pink hair, two small horns (hair ornaments)", "hair": "short pink hair, two small horns (hair ornaments)",

View File

@@ -0,0 +1,35 @@
{
"character_id": "biwa_hayahide_(Umamusume)",
"identity": {
"base_specs": "1girl, horse ears, horse tail, tall",
"hair": "long grey hair, wild hair",
"eyes": "purple eyes, red framed glasses",
"expression": "thinking",
"hands": "",
"arms": "",
"torso": "large breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "white shirt",
"outer_layer": "tracen school uniform",
"lower_body": "pleated skirt",
"footwear": "heeled shoes",
"gloves": "",
"accessories": ""
},
"styles": {
"aesthetic": "intellectual, cool",
"primary_color": "maroon",
"secondary_color": "white",
"tertiary_color": "grey"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
}
}

View File

@@ -15,10 +15,10 @@
}, },
"wardrobe": { "wardrobe": {
"inner_layer": "", "inner_layer": "",
"outer_layer": "pink dress, 'Bulma' text on front", "outer_layer": "black playboy bunny",
"lower_body": "dress", "lower_body": "pantyhose",
"footwear": "purple sneakers, white socks", "footwear": "red high heels",
"gloves": "purple gloves", "gloves": "detatched cuffs",
"accessories": "red hair ribbon, dragon radar" "accessories": "red hair ribbon, dragon radar"
}, },
"styles": { "styles": {

View File

@@ -1,5 +1,5 @@
{ {
"character_id": "camilla", "character_id": "camilla_(fire_emblem)",
"identity": { "identity": {
"base_specs": "1girl, curvaceous build, fair skin", "base_specs": "1girl, curvaceous build, fair skin",
"hair": "long wavy lavender hair, hair covering one eye", "hair": "long wavy lavender hair, hair covering one eye",

View File

@@ -1,7 +1,7 @@
{ {
"character_id": "chun_li", "character_id": "chun_li",
"identity": { "identity": {
"base_specs": "1girl, muscular build, fair skin", "base_specs": "1girl, muscular build, fair skin, asian",
"hair": "black hair, hair buns", "hair": "black hair, hair buns",
"eyes": "brown eyes", "eyes": "brown eyes",
"expression": "determined smile", "expression": "determined smile",
@@ -22,7 +22,7 @@
"accessories": "white hair ribbons, spiked bracelets" "accessories": "white hair ribbons, spiked bracelets"
}, },
"styles": { "styles": {
"aesthetic": "martial arts, traditional, street fighter style", "aesthetic": "chinese style",
"primary_color": "blue", "primary_color": "blue",
"secondary_color": "white", "secondary_color": "white",
"tertiary_color": "gold" "tertiary_color": "gold"

View File

@@ -1,11 +1,11 @@
{ {
"character_id": "ciri", "character_id": "ciri",
"identity": { "identity": {
"base_specs": "1girl, athletic build, pale skin", "base_specs": "1girl, athletic build",
"hair": "ashen grey hair, messy bun", "hair": "ashen grey hair, messy bun",
"eyes": "emerald green eyes. mascara", "eyes": "emerald green eyes, mascara",
"expression": "determined look", "expression": "determined look",
"hands": "brown nails", "hands": "green nails",
"arms": "", "arms": "",
"torso": "medium breasts", "torso": "medium breasts",
"pelvis": "", "pelvis": "",
@@ -15,7 +15,7 @@
}, },
"wardrobe": { "wardrobe": {
"inner_layer": "white blouse", "inner_layer": "white blouse",
"outer_layer": "brown leather vest, silver studs", "outer_layer": "",
"lower_body": "brown leather trousers", "lower_body": "brown leather trousers",
"footwear": "brown leather boots", "footwear": "brown leather boots",
"gloves": "brown leather gloves", "gloves": "brown leather gloves",

View File

@@ -0,0 +1,35 @@
{
"character_id": "delinquent_mother_flim13",
"identity": {
"base_specs": "1girl, milf, gyaru, tall",
"hair": "blonde hair, long hair",
"eyes": "sharp eyes",
"expression": "smirk, sharp teeth",
"hands": "painted nails",
"arms": "",
"torso": "very large breasts",
"pelvis": "wide hips",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "biege sweater, cleavage",
"outer_layer": "",
"lower_body": "pencil skirt",
"footwear": "high heels",
"gloves": "",
"accessories": "necklace, rings"
},
"styles": {
"aesthetic": "gyaru, milf, pink leopard print",
"primary_color": "pink",
"secondary_color": "black",
"tertiary_color": "gold"
},
"lora": {
"lora_name": "Illustrious/Looks/Gyaru_mom_Flim13_IL_V1.safetensors",
"lora_weight": 1.0,
"lora_triggers": ""
}
}

View File

@@ -0,0 +1,35 @@
{
"character_id": "gold_city_(Umamusume)",
"identity": {
"base_specs": "1girl, horse ears, horse tail, tall",
"hair": "blonde hair, wavy hair",
"eyes": "blue eyes",
"expression": "confident expression",
"hands": "",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "white shirt",
"outer_layer": "tracen school uniform",
"lower_body": "pleated skirt",
"footwear": "heeled shoes",
"gloves": "",
"accessories": "choker, earrings"
},
"styles": {
"aesthetic": "fashionable, model",
"primary_color": "gold",
"secondary_color": "white",
"tertiary_color": "black"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
}
}

View File

@@ -0,0 +1,35 @@
{
"character_id": "gold_ship_(Umamusume)",
"identity": {
"base_specs": "1girl, horse ears, horse tail, tall",
"hair": "grey hair, short hair",
"eyes": "red eyes",
"expression": "crazy expression, grin",
"hands": "",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "white shirt",
"outer_layer": "tracen school uniform",
"lower_body": "pleated skirt",
"footwear": "heeled shoes",
"gloves": "",
"accessories": "ear covers, hat"
},
"styles": {
"aesthetic": "energetic, sporty",
"primary_color": "red",
"secondary_color": "white",
"tertiary_color": "gold"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
}
}

View File

@@ -0,0 +1,35 @@
{
"character_id": "jessica_rabbit",
"identity": {
"base_specs": "1girl, voluptuous build, tall,",
"hair": "long red hair, side part, hair over one eye",
"eyes": "green eyes, heavy makeup, purple eyeshadow",
"expression": "seductive smile",
"hands": "purple elbow gloves",
"arms": "",
"torso": "large breasts",
"pelvis": "narrow waist",
"legs": "",
"feet": "",
"distinguishing_marks": "red lips"
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "red sequin dress, strapless, high slit, backless",
"lower_body": "side_slit,",
"footwear": "red high heels",
"gloves": "purple opera gloves",
"accessories": "gold earrings, glitter"
},
"styles": {
"aesthetic": "noir, cartoon, glamorous",
"primary_color": "red",
"secondary_color": "purple",
"tertiary_color": "gold"
},
"lora": {
"lora_name": "",
"lora_weight": 0.8,
"lora_triggers": ""
}
}

View File

@@ -18,8 +18,8 @@
"outer_layer": "white Team Rocket uniform jacket, bare stomach, red R logo", "outer_layer": "white Team Rocket uniform jacket, bare stomach, red R logo",
"lower_body": "white miniskirt", "lower_body": "white miniskirt",
"footwear": "black thigh-high boots", "footwear": "black thigh-high boots",
"gloves": "black gloves", "gloves": "black elbow gloves",
"accessories": "" "accessories": "green earrings"
}, },
"styles": { "styles": {
"aesthetic": "villainous, anime, pokemon style", "aesthetic": "villainous, anime, pokemon style",

View File

@@ -0,0 +1,35 @@
{
"character_id": "jinx",
"identity": {
"base_specs": "1girl, slender build, pale skin,",
"hair": "long aqua hair, twin braids, very long hair, bangs",
"eyes": "pink eyes, ",
"expression": "crazy eyes, manic grin, crazy smile",
"hands": "black and pink nails",
"arms": "",
"torso": "flat chest,",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "cloud tattoo,"
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "pink and black bikini, asymmetrical_bikini ",
"lower_body": "pink shorts, single pink stocking",
"footwear": "combat boots",
"gloves": "black fingerless gloves, fishnet elbow gloves,",
"accessories": "ammo belts, choker, bullet necklace,"
},
"styles": {
"aesthetic": "punk, chaotic,",
"primary_color": "pink",
"secondary_color": "black",
"tertiary_color": "aqua"
},
"lora": {
"lora_name": "Illustrious/Looks/jinx_default_lol-000021.safetensors",
"lora_weight": 0.8,
"lora_triggers": ""
}
}

View File

@@ -0,0 +1,35 @@
{
"character_id": "kagamine_rin",
"identity": {
"base_specs": "1girl, petite",
"hair": "blonde hair, short hair, hair bow",
"eyes": "blue eyes",
"expression": "smile, energetic",
"hands": "",
"arms": "detached sleeves",
"torso": "flat chest",
"pelvis": "",
"legs": "leg warmers",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "white shirt, sailor collar",
"outer_layer": "",
"lower_body": "black shorts, yellow belt",
"footwear": "white shoes",
"gloves": "",
"accessories": "headset, hair bow"
},
"styles": {
"aesthetic": "vocaloid, cyber",
"primary_color": "yellow",
"secondary_color": "white",
"tertiary_color": "black"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
}
}

View File

@@ -19,7 +19,7 @@
"lower_body": "black leather shorts", "lower_body": "black leather shorts",
"footwear": "black thigh-high boots", "footwear": "black thigh-high boots",
"gloves": "", "gloves": "",
"accessories": "crystal orb, silver jewelry" "accessories": "crystal heart, silver jewelry"
}, },
"styles": { "styles": {
"aesthetic": "pop star, mystical, k/da style", "aesthetic": "pop star, mystical, k/da style",

View File

@@ -5,7 +5,7 @@
"hair": "light blue hair,", "hair": "light blue hair,",
"eyes": "yellow glowing eyes, slit pupils", "eyes": "yellow glowing eyes, slit pupils",
"expression": "seductive, confident look", "expression": "seductive, confident look",
"hands": "long black claws, blue nails", "hands": "metal claws",
"arms": "", "arms": "",
"torso": "medium breasts", "torso": "medium breasts",
"pelvis": "", "pelvis": "",

View File

@@ -11,7 +11,7 @@
"pelvis": "", "pelvis": "",
"legs": "", "legs": "",
"feet": "", "feet": "",
"distinguishing_marks": "purple markings under eyes, floating crystal cannons" "distinguishing_marks": ""
}, },
"wardrobe": { "wardrobe": {
"inner_layer": "silver bodysuit", "inner_layer": "silver bodysuit",

View File

@@ -0,0 +1,35 @@
{
"character_id": "komi_shouko",
"identity": {
"base_specs": "1girl, slender build, pale skin, asian",
"hair": "long dark purple hair, hime cut,",
"eyes": "dark purple eyes,",
"expression": "neutral expression, stoic, cat ears",
"hands": "",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "black pantyhose",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "white shirt",
"outer_layer": "itan private high school uniform, blazer, striped bow tie",
"lower_body": "plaid skirt",
"footwear": "loafers",
"gloves": "",
"accessories": ""
},
"styles": {
"aesthetic": "anime, manga, clean lines",
"primary_color": "purple",
"secondary_color": "magenta",
"tertiary_color": "white"
},
"lora": {
"lora_name": "",
"lora_weight": 0.8,
"lora_triggers": "komi shouko, itan private high school uniform"
}
}

View File

@@ -1,13 +1,13 @@
{ {
"character_id": "lara_croft_classic", "character_id": "lara_croft_classic",
"identity": { "identity": {
"base_specs": "1girl, athletic build, tan skin", "base_specs": "1girl, athletic build,",
"hair": "long brown hair, single braid", "hair": "long brown hair, single braid",
"eyes": "brown eyes", "eyes": "brown eyes",
"expression": "determined", "expression": "light smile, raised eyebrow",
"hands": "", "hands": "",
"arms": "", "arms": "",
"torso": "", "torso": "large breasts",
"pelvis": "", "pelvis": "",
"legs": "", "legs": "",
"feet": "", "feet": "",
@@ -15,11 +15,11 @@
}, },
"wardrobe": { "wardrobe": {
"inner_layer": "", "inner_layer": "",
"outer_layer": "teal tank top, crop top", "outer_layer": "teal tank top,",
"lower_body": "brown shorts", "lower_body": "brown shorts",
"footwear": "brown combat boots, red laces", "footwear": "brown combat boots, red laces",
"gloves": "black fingerless gloves", "gloves": "black fingerless gloves",
"accessories": "dual thigh holsters, backpack, circular sunglasses" "accessories": "dual thigh pistol holsters, brown leatherbackpack, red circular sunglasses"
}, },
"styles": { "styles": {
"aesthetic": "adventure, retro, 90s style", "aesthetic": "adventure, retro, 90s style",
@@ -28,8 +28,8 @@
"tertiary_color": "black" "tertiary_color": "black"
}, },
"lora": { "lora": {
"lora_name": "lara_croft_classic", "lora_name": "Illustrious/Looks/LaraCroft_ClassicV2_Illu_Dwnsty.safetensors",
"lora_weight": 0.8, "lora_weight": 0.8,
"lora_triggers": "lara croft, classic outfit" "lora_triggers": ""
} }
} }

View File

@@ -0,0 +1,35 @@
{
"character_id": "lisa_(genshin_impact)",
"identity": {
"base_specs": "1girl, tall, mature female",
"hair": "brown hair, wavy hair, side ponytail",
"eyes": "green eyes",
"expression": "seductive smile",
"hands": "",
"arms": "detached sleeves",
"torso": "large breasts",
"pelvis": "wide hips",
"legs": "black pantyhose",
"feet": "",
"distinguishing_marks": "beauty mark"
},
"wardrobe": {
"inner_layer": "purple dress, corset",
"outer_layer": "purple shawl",
"lower_body": "slit skirt",
"footwear": "black heels",
"gloves": "purple gloves",
"accessories": "witch hat, rose, necklace"
},
"styles": {
"aesthetic": "genshin impact, witch, librarian",
"primary_color": "purple",
"secondary_color": "white",
"tertiary_color": "gold"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
}
}

View File

@@ -1,10 +1,10 @@
{ {
"character_id": "lulu (ffx)", "character_id": "lulu (ff10)",
"identity": { "identity": {
"base_specs": "1girl, curvaceous build, fair skin", "base_specs": "1girl, curvaceous build, fair skin",
"hair": "long black hair, complex braids, hairpins", "hair": "long black hair, complex braids, hairpins",
"eyes": "red eyes", "eyes": "red eyes",
"expression": "stern expression", "expression": "thinking, raised eyebrow",
"hands": "black nails", "hands": "black nails",
"arms": "", "arms": "",
"torso": "large breasts", "torso": "large breasts",

View File

@@ -4,22 +4,22 @@
"base_specs": "1girl, curvaceous build, pink skin", "base_specs": "1girl, curvaceous build, pink skin",
"hair": "long voluminous white hair", "hair": "long voluminous white hair",
"eyes": "red eyes, black sclera", "eyes": "red eyes, black sclera",
"expression": "playful yet menacing smile", "expression": "evil smile",
"hands": "black claws, pink nails", "hands": "black claws, pink nails",
"arms": "", "arms": "",
"torso": "medium breasts", "torso": "large breasts",
"pelvis": "", "pelvis": "",
"legs": "", "legs": "",
"feet": "", "feet": "",
"distinguishing_marks": "pink skin, long tail, pointed ears" "distinguishing_marks": "pink skin, long tail, pointy ears"
}, },
"wardrobe": { "wardrobe": {
"inner_layer": "black tube top", "inner_layer": "black tube top",
"outer_layer": "", "outer_layer": "",
"lower_body": "white baggy pants", "lower_body": "white harem pants",
"footwear": "black and yellow boots", "footwear": "black and yellow boots",
"gloves": "", "gloves": "black sleeves",
"accessories": "gold bracelets, gold neck ring" "accessories": "gold bracelets, gold neck ring, hoop earrings"
}, },
"styles": { "styles": {
"aesthetic": "supernatural, anime, dragon ball style", "aesthetic": "supernatural, anime, dragon ball style",

View File

@@ -1,7 +1,7 @@
{ {
"character_id": "marin_kitagawa", "character_id": "marin_kitagawa",
"identity": { "identity": {
"base_specs": "1girl, slender build, fair skin", "base_specs": "1girl, slender build, fair skin, asian",
"hair": "long blonde hair, pink tips", "hair": "long blonde hair, pink tips",
"eyes": "pink eyes (contacts)", "eyes": "pink eyes (contacts)",
"expression": "excited smile", "expression": "excited smile",

View File

@@ -0,0 +1,35 @@
{
"character_id": "megurine_luka",
"identity": {
"base_specs": "1girl, tall, mature female",
"hair": "pink hair, long hair",
"eyes": "blue eyes",
"expression": "light smile",
"hands": "",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "crop top, detached sleeves, gold trim",
"lower_body": "side slit, lace-up skirt",
"footwear": "thinghighs, lace-up boots, gold boots, gold armlet",
"gloves": "",
"accessories": "headset"
},
"styles": {
"aesthetic": "vocaloid, elegant",
"primary_color": "black",
"secondary_color": "gold",
"tertiary_color": "pink"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
}
}

View File

@@ -0,0 +1,35 @@
{
"character_id": "meiko",
"identity": {
"base_specs": "1girl, mature female",
"hair": "brown hair, short hair",
"eyes": "brown eyes",
"expression": "smile, confident",
"hands": "",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "red crop top, sleeveless",
"outer_layer": "",
"lower_body": "red skirt, mini skirt",
"footwear": "brown boots",
"gloves": "",
"accessories": "choker"
},
"styles": {
"aesthetic": "vocaloid, casual",
"primary_color": "red",
"secondary_color": "brown",
"tertiary_color": "black"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
}
}

View File

@@ -0,0 +1,35 @@
{
"character_id": "olivier_mira_armstrong",
"identity": {
"base_specs": "1girl, tall, mature female",
"hair": "blonde hair, long hair, hair over one eye",
"eyes": "blue eyes, sharp eyes",
"expression": "serious",
"hands": "",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "thick lips"
},
"wardrobe": {
"inner_layer": "black shirt",
"outer_layer": "blue military coat, fur collar",
"lower_body": "black pants",
"footwear": "black boots",
"gloves": "black gloves",
"accessories": "sword"
},
"styles": {
"aesthetic": "military, amestris uniform",
"primary_color": "blue",
"secondary_color": "black",
"tertiary_color": "gold"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
}
}

View File

@@ -5,7 +5,7 @@
"hair": "long blonde hair, voluminous, crown", "hair": "long blonde hair, voluminous, crown",
"eyes": "blue eyes, long eyelashes", "eyes": "blue eyes, long eyelashes",
"expression": "gentle smile", "expression": "gentle smile",
"hands": "pink nails", "hands": "",
"arms": "", "arms": "",
"torso": "medium breasts", "torso": "medium breasts",
"pelvis": "", "pelvis": "",

View File

@@ -4,7 +4,7 @@
"base_specs": "1girl, slender build, fair skin, pointed ears", "base_specs": "1girl, slender build, fair skin, pointed ears",
"hair": "long blonde hair, braided, gold hair clips", "hair": "long blonde hair, braided, gold hair clips",
"eyes": "green eyes", "eyes": "green eyes",
"expression": "determined look", "expression": "curious",
"hands": "gold nails", "hands": "gold nails",
"arms": "", "arms": "",
"torso": "small breasts", "torso": "small breasts",

View File

@@ -0,0 +1,35 @@
{
"character_id": "rice_shower_(Umamusume)",
"identity": {
"base_specs": "1girl, petite, horse ears, horse tail",
"hair": "long dark brown hair, bangs, hair over one eye",
"eyes": "purple eyes",
"expression": "shy expression",
"hands": "",
"arms": "",
"torso": "small breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "white shirt",
"outer_layer": "tracen school uniform",
"lower_body": "pleated skirt",
"footwear": "heeled shoes",
"gloves": "",
"accessories": "blue rose, hair flower, small hat, dagger"
},
"styles": {
"aesthetic": "gothic lolita, elegant",
"primary_color": "purple",
"secondary_color": "blue",
"tertiary_color": "black"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
}
}

View File

@@ -11,25 +11,25 @@
"pelvis": "", "pelvis": "",
"legs": "", "legs": "",
"feet": "", "feet": "",
"distinguishing_marks": "blue lipstick,gerudo markings" "distinguishing_marks": "darkblue lipstick,"
}, },
"wardrobe": { "wardrobe": {
"inner_layer": "", "inner_layer": "",
"outer_layer": "gerudo top, colorful sash", "outer_layer": "black top, blue sash",
"lower_body": "gerudo pants, puffy pants", "lower_body": "black skirt, pelvic curtain,",
"footwear": "sandals", "footwear": "gold high heels",
"gloves": "", "gloves": "",
"accessories": "gold jewelry, crown, earrings" "accessories": "gold jewelry, earrings"
}, },
"styles": { "styles": {
"aesthetic": "fantasy, desert, gerudo style", "aesthetic": "fantasy, desert, gerudo style",
"primary_color": "gold", "primary_color": "gold",
"secondary_color": "teal", "secondary_color": "black",
"tertiary_color": "red" "tertiary_color": "red"
}, },
"lora": { "lora": {
"lora_name": "riju_botw", "lora_name": "",
"lora_weight": 0.8, "lora_weight": 0.8,
"lora_triggers": "riju, gerudo" "lora_triggers": ""
} }
} }

View File

@@ -15,21 +15,21 @@
}, },
"wardrobe": { "wardrobe": {
"inner_layer": "", "inner_layer": "",
"outer_layer": "black skin-tight jumpsuit, pink heart-shaped chest plate", "outer_layer": "black skin-tight jumpsuit, pink heart-shaped chest plate, bare shoulders, cleavage",
"lower_body": "jumpsuit", "lower_body": "jumpsuit",
"footwear": "white boots, pink heart motifs", "footwear": "white boots, pink heart motifs",
"gloves": "white gloves, pink cuffs", "gloves": "white gloves, pink cuffs",
"accessories": "blue eyeshadow" "accessories": "blue eyeshadow"
}, },
"styles": { "styles": {
"aesthetic": "sleek, spy, sonic style", "aesthetic": "jewels, museum,sleek, spy, sonic style",
"primary_color": "white", "primary_color": "white",
"secondary_color": "pink", "secondary_color": "pink",
"tertiary_color": "black" "tertiary_color": "black"
}, },
"lora": { "lora": {
"lora_name": "", "lora_name": "Illustrious/Looks/Rouge_the_bat_v2.safetensors",
"lora_weight": 1.0, "lora_weight": 0.8,
"lora_triggers": "" "lora_triggers": ""
} }
} }

View File

@@ -0,0 +1,35 @@
{
"character_id": "ryouko_(tenchi_muyou!)",
"identity": {
"base_specs": "1girl, slim build,",
"hair": "long teal hair, spiky, voluminous",
"eyes": "golden eyes, cat-like pupils",
"expression": "confident smirk",
"hands": "",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "red gem on forehead,"
},
"wardrobe": {
"inner_layer": "long white dress, plunging neckline, black belt",
"outer_layer": "black and orange long sleeve jacket with purple trim,",
"lower_body": "side_slit,, red trousers",
"footwear": "",
"gloves": "red gloves",
"accessories": "red gems, wristbands"
},
"styles": {
"aesthetic": "90s anime, sci-fi",
"primary_color": "teal",
"secondary_color": "white",
"tertiary_color": "red"
},
"lora": {
"lora_name": "",
"lora_weight": 0.8,
"lora_triggers": "ryouko hakubi, space pirate"
}
}

View File

@@ -0,0 +1,35 @@
{
"character_id": "sarah_miller_(the_last_of_us)",
"identity": {
"base_specs": "1girl, loli, small build",
"hair": "blonde hair, short hair",
"eyes": "blue eyes",
"expression": "smile",
"hands": "",
"arms": "",
"torso": "flat chest",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "grey t-shirt, white shirt",
"outer_layer": "",
"lower_body": "blue jeans",
"footwear": "sneakers",
"gloves": "",
"accessories": "wristwatch"
},
"styles": {
"aesthetic": "casual, 2013 fashion",
"primary_color": "grey",
"secondary_color": "blue",
"tertiary_color": "white"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
}
}

View File

@@ -0,0 +1,35 @@
{
"character_id": "shantae",
"identity": {
"base_specs": "1girl, dark skin, pointy ears",
"hair": "purple hair, very long hair, ponytail",
"eyes": "blue eyes",
"expression": "smile, energetic",
"hands": "",
"arms": "gold bracelets",
"torso": "small breasts, perky breasts",
"pelvis": "wide hips",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "red bikini top, red harem pants, gold trim",
"lower_body": "",
"footwear": "gold shoes",
"gloves": "",
"accessories": "gold tiara, hoop earrings"
},
"styles": {
"aesthetic": "genie, dancer, arabian",
"primary_color": "red",
"secondary_color": "gold",
"tertiary_color": "purple"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
}
}

View File

@@ -2,13 +2,13 @@
"character_id": "sucy_manbavaran", "character_id": "sucy_manbavaran",
"identity": { "identity": {
"base_specs": "1girl, lanky build, pale skin", "base_specs": "1girl, lanky build, pale skin",
"hair": "mauve hair, hair covering one eye", "hair": "light purple hair, hair covering one eye",
"eyes": "droopy red eyes", "eyes": "red eyes",
"expression": "deadpan expression", "expression": "deadpan expression",
"hands": "purple nails", "hands": "black nails",
"arms": "", "arms": "",
"torso": "small breasts", "torso": "small breasts",
"pelvis": "", "pelvis": "narrow waist",
"legs": "", "legs": "",
"feet": "", "feet": "",
"distinguishing_marks": "dark circles under eyes" "distinguishing_marks": "dark circles under eyes"
@@ -16,7 +16,7 @@
"wardrobe": { "wardrobe": {
"inner_layer": "", "inner_layer": "",
"outer_layer": "dark purple witch robes", "outer_layer": "dark purple witch robes",
"lower_body": "long skirt", "lower_body": "long skirt with frayed edges",
"footwear": "brown boots", "footwear": "brown boots",
"gloves": "", "gloves": "",
"accessories": "pointed witch hat, potion bottle" "accessories": "pointed witch hat, potion bottle"

View File

@@ -7,16 +7,16 @@
"expression": "confident", "expression": "confident",
"hands": "gold nails", "hands": "gold nails",
"arms": "muscular arms", "arms": "muscular arms",
"torso": "abs", "torso": "abs, mediumS breasts",
"pelvis": "wide hips", "pelvis": "wide hips",
"legs": "muscular legs", "legs": "muscular legs",
"feet": "", "feet": "",
"distinguishing_marks": "blue lipstick, gerudo markings" "distinguishing_marks": "dark blue lipstick, gerudo markings"
}, },
"wardrobe": { "wardrobe": {
"inner_layer": "", "inner_layer": "",
"outer_layer": "gold breastplate, blue champion's skirt", "outer_layer": "blue top, blue champion's skirt, green sash, green shoulder guards,",
"lower_body": "", "lower_body": "blue skirt",
"footwear": "gold heels", "footwear": "gold heels",
"gloves": "", "gloves": "",
"accessories": "gold jewelry, scimitar" "accessories": "gold jewelry, scimitar"
@@ -28,8 +28,8 @@
"tertiary_color": "red" "tertiary_color": "red"
}, },
"lora": { "lora": {
"lora_name": "urbosa_botw", "lora_name": "",
"lora_weight": 0.8, "lora_weight": 0.8,
"lora_triggers": "urbosa, gerudo" "lora_triggers": ""
} }
} }

View File

@@ -3,7 +3,7 @@
"identity": { "identity": {
"base_specs": "1girl, miqo'te, slender build, fair skin, cat ears", "base_specs": "1girl, miqo'te, slender build, fair skin, cat ears",
"hair": "short white hair, bangs", "hair": "short white hair, bangs",
"eyes": "blind white eyes", "eyes": "blind, white eyes",
"expression": "stoic expression", "expression": "stoic expression",
"hands": "black nails", "hands": "black nails",
"arms": "", "arms": "",

View File

@@ -1,5 +1,5 @@
{ {
"character_id": "yuna_ffx", "character_id": "yuna_(ff10)",
"identity": { "identity": {
"base_specs": "1girl, slender, fair skin", "base_specs": "1girl, slender, fair skin",
"hair": "short brown hair, bob cut", "hair": "short brown hair, bob cut",
@@ -28,8 +28,8 @@
"tertiary_color": "yellow" "tertiary_color": "yellow"
}, },
"lora": { "lora": {
"lora_name": "yuna_ffx", "lora_name": "",
"lora_weight": 0.8, "lora_weight": 0.8,
"lora_triggers": "yuna, summoner outfit" "lora_triggers": ""
} }
} }

View File

@@ -0,0 +1,9 @@
from .checkpoint_from_string import CheckpointLoaderFromString
NODE_CLASS_MAPPINGS = {
"CheckpointLoaderFromString": CheckpointLoaderFromString
}
NODE_DISPLAY_NAME_MAPPINGS = {
"CheckpointLoaderFromString": "Checkpoint Loader (From String)"
}

View File

@@ -0,0 +1,19 @@
import folder_paths
import comfy.sd
class CheckpointLoaderFromString:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"ckpt_name": ("STRING", {"multiline": False, "forceInput": True}),
}
}
RETURN_TYPES = ("MODEL", "CLIP", "VAE")
FUNCTION = "load_checkpoint"
CATEGORY = "AODH Pack"
def load_checkpoint(self, ckpt_name):
ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name)
out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))
return out[:3]

View File

@@ -0,0 +1,9 @@
from .checkpoint_selector import CheckpointSelector
NODE_CLASS_MAPPINGS = {
"CheckpointSelector": CheckpointSelector
}
NODE_DISPLAY_NAME_MAPPINGS = {
"CheckpointSelector": "Checkpoint Selector"
}

View File

@@ -0,0 +1,120 @@
import os
import random
import folder_paths
class CheckpointSelector:
def __init__(self):
self.current_index = 0
self.current_count = 0
self.last_folder = None
self.last_selection = None
self.last_manual_index = 0
@classmethod
def INPUT_TYPES(s):
# Find folders in 'checkpoints' paths
checkpoint_paths = folder_paths.get_folder_paths("checkpoints")
checkpoint_folders = []
for path in checkpoint_paths:
if os.path.exists(path) and os.path.isdir(path):
# List subdirectories
try:
subdirs = [d for d in os.listdir(path) if os.path.isdir(os.path.join(path, d))]
checkpoint_folders.extend(subdirs)
except Exception:
pass
# Remove duplicates and sort
checkpoint_folders = sorted(list(set(checkpoint_folders)))
if not checkpoint_folders:
checkpoint_folders = ["None"]
return {
"required": {
"folder": (checkpoint_folders, ),
"mode": (["Random", "Sequential", "Manual"], {"default": "Random"}),
"repeat_count": ("INT", {"default": 1, "min": 1, "max": 100, "step": 1}),
"manual_index": ("INT", {"default": 0, "min": 0, "max": 10000, "step": 1}),
}
}
RETURN_TYPES = ("STRING", "INT")
RETURN_NAMES = ("checkpoint_name", "total_checkpoints")
FUNCTION = "select_checkpoint"
CATEGORY = "AODH Pack"
def select_checkpoint(self, folder, mode, repeat_count, manual_index):
repeat_count = max(1, repeat_count)
if folder == "None":
return ("", 0)
# Find the full path for the selected folder
checkpoint_paths = folder_paths.get_folder_paths("checkpoints")
target_path = None
for path in checkpoint_paths:
check_path = os.path.join(path, folder)
if os.path.exists(check_path) and os.path.isdir(check_path):
target_path = check_path
break
if not target_path:
return ("", 0)
# Find checkpoint files (.safetensors, .ckpt)
extensions = ('.safetensors', '.ckpt')
checkpoint_files = [f for f in os.listdir(target_path) if f.lower().endswith(extensions)]
checkpoint_files.sort()
count = len(checkpoint_files)
if count == 0:
return ("", 0)
selected_checkpoint = ""
# Reset state if folder changes
if self.last_folder != folder:
self.current_index = 0
self.current_count = 0
self.last_selection = None
self.last_folder = folder
# Reset state if manual_index changes (for Sequential mode)
if self.last_manual_index != manual_index:
self.current_index = 0
self.current_count = 0
self.last_manual_index = manual_index
if mode == "Random":
if self.current_count >= repeat_count or self.last_selection is None:
rng = random.Random()
self.last_selection = rng.choice(checkpoint_files)
self.current_count = 0
selected_checkpoint = self.last_selection
self.current_count += 1
elif mode == "Sequential":
if self.current_count >= repeat_count:
self.current_index += 1
self.current_count = 0
selected_checkpoint = checkpoint_files[(self.current_index + manual_index) % count]
self.current_count += 1
elif mode == "Manual":
selected_checkpoint = checkpoint_files[manual_index % count]
# Construct the relative path for ComfyUI Checkpoint loader
# Use forward slashes for consistency with ComfyUI paths
full_checkpoint_name = f"{folder}/{selected_checkpoint}"
return (full_checkpoint_name, count)
@classmethod
def IS_CHANGED(s, folder, mode, repeat_count, manual_index):
if mode == "Random" or mode == "Sequential":
return float("nan")
return f"{folder}_{manual_index}"

View File

@@ -0,0 +1,9 @@
from .lora_selector import LoraSelector
NODE_CLASS_MAPPINGS = {
"LoraSelector": LoraSelector
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LoraSelector": "Lora Selector"
}

View File

@@ -0,0 +1,121 @@
import os
import random
import folder_paths
class LoraSelector:
def __init__(self):
self.current_index = 0
self.current_count = 0
self.last_folder = None
self.last_selection = None
self.last_manual_index = 0
@classmethod
def INPUT_TYPES(s):
# Find 'Illustrious' folders in all lora paths
lora_paths = folder_paths.get_folder_paths("loras")
illustrious_folders = []
for path in lora_paths:
illustrious_path = os.path.join(path, "Illustrious")
if os.path.exists(illustrious_path) and os.path.isdir(illustrious_path):
# List subdirectories
try:
subdirs = [d for d in os.listdir(illustrious_path) if os.path.isdir(os.path.join(illustrious_path, d))]
illustrious_folders.extend(subdirs)
except Exception:
pass
# Remove duplicates and sort
illustrious_folders = sorted(list(set(illustrious_folders)))
if not illustrious_folders:
illustrious_folders = ["None"]
return {
"required": {
"folder": (illustrious_folders, ),
"mode": (["Random", "Sequential", "Manual"], {"default": "Random"}),
"repeat_count": ("INT", {"default": 1, "min": 1, "max": 100, "step": 1}),
"manual_index": ("INT", {"default": 0, "min": 0, "max": 10000, "step": 1}),
}
}
RETURN_TYPES = ("STRING", "INT")
RETURN_NAMES = ("lora_name", "total_loras")
FUNCTION = "select_lora"
CATEGORY = "AODH Pack"
def select_lora(self, folder, mode, repeat_count, manual_index):
repeat_count = max(1, repeat_count)
if folder == "None":
return ("", 0)
# Find the full path for the selected folder
lora_paths = folder_paths.get_folder_paths("loras")
target_path = None
for path in lora_paths:
check_path = os.path.join(path, "Illustrious", folder)
if os.path.exists(check_path) and os.path.isdir(check_path):
target_path = check_path
break
if not target_path:
return ("", 0)
# Find .safetensors files
lora_files = [f for f in os.listdir(target_path) if f.endswith('.safetensors')]
lora_files.sort() # Ensure consistent order
count = len(lora_files)
if count == 0:
return ("", 0)
selected_lora = ""
# Reset state if folder changes
if self.last_folder != folder:
self.current_index = 0
self.current_count = 0
self.last_selection = None
self.last_folder = folder
# Reset state if manual_index changes (for Sequential mode)
if self.last_manual_index != manual_index:
self.current_index = 0
self.current_count = 0
self.last_manual_index = manual_index
if mode == "Random":
if self.current_count >= repeat_count or self.last_selection is None:
# Use a local Random instance to ensure randomness regardless of global seed
rng = random.Random()
self.last_selection = rng.choice(lora_files)
self.current_count = 0
selected_lora = self.last_selection
self.current_count += 1
elif mode == "Sequential":
if self.current_count >= repeat_count:
self.current_index += 1
self.current_count = 0
selected_lora = lora_files[(self.current_index + manual_index) % count]
self.current_count += 1
elif mode == "Manual":
selected_lora = lora_files[manual_index % count]
# Construct the relative path for ComfyUI Lora loader
# Use forward slashes for consistency
full_lora_name = f"Illustrious/{folder}/{selected_lora}"
return (full_lora_name, count)
@classmethod
def IS_CHANGED(s, folder, mode, repeat_count, manual_index):
if mode == "Random" or mode == "Sequential":
return float("nan")
return f"{folder}_{manual_index}"

View File

@@ -0,0 +1,9 @@
from .metadata_saver import AodhMetadataImageSaver
NODE_CLASS_MAPPINGS = {
"AodhMetadataImageSaver": AodhMetadataImageSaver
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AodhMetadataImageSaver": "AODH Image Saver (Metadata)"
}

View File

@@ -0,0 +1,210 @@
import json
import os
import re
import numpy as np
from pathlib import Path
from datetime import datetime
from PIL import Image
from PIL.PngImagePlugin import PngInfo
import folder_paths
import comfy.samplers
class AodhMetadataImageSaver:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
self.prefix_append = ""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE", ),
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
},
"optional": {
"save_directory": ("STRING", {"default": "%Y-%m-%d"}),
# Generation parameters
"positive_prompt": ("STRING", {"default": "", "multiline": True}),
"negative_prompt": ("STRING", {"default": "", "multiline": True}),
"checkpoint_name": ("STRING", {"default": ""}),
"vae_name": ("STRING", {"default": ""}),
"clip_skip": ("INT", {"default": -2}),
"lora_name": ("STRING", {"default": ""}),
"lora_strength": ("FLOAT", {"default": 1.0}),
# Sampling
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 20}),
"cfg": ("FLOAT", {"default": 8.0}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, ),
# Resolution
"width": ("INT", {"default": 512}),
"height": ("INT", {"default": 512}),
"upscale_factor": ("FLOAT", {"default": 1.0}),
"upscaler_name": ("STRING", {"default": ""}),
"hires_steps": ("INT", {"default": 10}),
"denoise": ("FLOAT", {"default": 0.0}),
# Character / App Metadata
"character_name": ("STRING", {"default": ""}),
"tags": ("STRING", {"default": ""}),
"rating": (["safe", "questionable", "explicit"], {"default": "safe"}),
# Extension
"extension": (["png", "jpg", "webp"], {"default": "png"}),
"include_workflow": ("BOOLEAN", {"default": True}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("images",)
FUNCTION = "save_images"
OUTPUT_NODE = True
CATEGORY = "AODH Pack/Image"
def save_images(self, images, filename_prefix="ComfyUI", save_directory="%Y-%m-%d",
positive_prompt="", negative_prompt="",
checkpoint_name="", vae_name="", clip_skip=-2, lora_name="", lora_strength=1.0,
seed=0, steps=20, cfg=8.0, sampler_name="euler", scheduler="normal",
width=512, height=512, upscale_factor=1.0, upscaler_name="", hires_steps=10, denoise=0.0,
character_name="", tags="", rating="safe", extension="png", include_workflow=True,
prompt=None, extra_pnginfo=None):
now = datetime.now()
if save_directory:
# Handle date:yyyy-MM-dd format
if "date:" in save_directory:
def replace_date(match):
fmt = match.group(1)
# Convert common JS-like date formats to python strftime
fmt = fmt.replace("yyyy", "%Y").replace("MM", "%m").replace("dd", "%d")
fmt = fmt.replace("HH", "%H").replace("mm", "%M").replace("ss", "%S")
return now.strftime(fmt)
save_directory = re.sub(r"date:([\w\-\:]+)", replace_date, save_directory)
# Also handle direct strftime patterns if any % is present
if "%" in save_directory:
save_directory = now.strftime(save_directory)
filename_prefix = os.path.join(save_directory, filename_prefix)
full_output_folder, filename, counter, subfolder, filename_prefix = \
folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
results = list()
for image in images:
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata = PngInfo()
# 1. Construct A1111 Parameters String
# Format: Prompt
# Negative prompt: ...
# Steps: ..., Sampler: ..., CFG scale: ..., Seed: ..., Size: ...x..., Model: ..., ...
parameters_text = f"{positive_prompt}\nNegative prompt: {negative_prompt}\n"
parameters_text += f"Steps: {steps}, Sampler: {sampler_name}, Schedule: {scheduler}, CFG scale: {cfg}, Seed: {seed}, "
parameters_text += f"Size: {width}x{height}, Model: {checkpoint_name}"
if clip_skip != -2:
parameters_text += f", Clip skip: {clip_skip}"
if upscale_factor > 1.0:
parameters_text += f", Hires upscale: {upscale_factor}, Hires steps: {hires_steps}, Hires upscaler: {upscaler_name}, Denoising strength: {denoise}"
if lora_name:
display_lora_name = Path(lora_name).name
parameters_text += f", Lora: {display_lora_name}"
metadata.add_text("parameters", parameters_text)
# 2. Construct ComfyUI Metadata JSON (following spec)
comfy_meta = {
"_description": "Extended metadata specific to ComfyUI workflow",
"workflow_name": "AODH Generator", # Could be dynamic if we parsed extra_pnginfo
"workflow_version": "1.0.0",
"generation": {
"checkpoint": checkpoint_name,
"vae": vae_name,
"clip_skip": clip_skip,
"lora": [{"name": Path(lora_name).name, "strength_model": lora_strength}] if lora_name else []
},
"sampling": {
"sampler": sampler_name,
"scheduler": scheduler,
"steps": steps,
"cfg": cfg,
"seed": seed,
"batch_size": images.shape[0]
},
"resolution": {
"width": width,
"height": height,
"upscale_factor": upscale_factor,
"upscaler": upscaler_name,
"hires_steps": hires_steps,
"denoise_strength": denoise
},
"prompt_structure": {
"positive": {"full": positive_prompt},
"negative": {"full": negative_prompt}
},
"workflow": {}
}
# Embed raw workflow if available
if include_workflow:
if prompt is not None:
comfy_meta["workflow"]["execution"] = prompt
if extra_pnginfo is not None and "workflow" in extra_pnginfo:
comfy_meta["workflow"]["nodes"] = extra_pnginfo["workflow"]
# 3. App Metadata
app_meta = {
"tags": [t.strip() for t in tags.split(",") if t.strip()],
"character": character_name,
"rating": rating
}
# Add to main JSON
full_metadata = {
"parameters": parameters_text,
"source": "comfyui",
"comfyui": True,
"comfyui_metadata": comfy_meta,
"app_metadata": app_meta
}
# Embed as "comment" or a specific key.
# The spec says "A ComfyUI node should embed this metadata in the 'parameters' PNG text chunk."
# But usually JSON goes into a separate chunk or we rely on the parser to extract it from 'parameters' if it was formatted there.
# However, standard ComfyUI puts workflow in "workflow" and "prompt" chunks.
# The spec implies a specific JSON structure. We'll add it as a separate text chunk "comfyui_metadata_json"
# OR we can try to append it to parameters, but A1111 parsers might break.
# Let's put the full JSON in a "aodh_metadata" chunk for safety,
# and ALSO try to adhere to standard Comfy behavior by keeping workflow/prompt chunks.
if include_workflow:
if prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
# Add our custom full spec JSON
metadata.add_text("aodh_metadata", json.dumps(full_metadata, indent=2))
file = f"{filename}_{counter:05}_.{extension}"
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=4)
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type
})
counter += 1
return { "ui": { "images": results }, "result": (images,) }

View File

@@ -29,17 +29,20 @@ class ResolutionReader:
return float("nan") return float("nan")
return resolution return resolution
RETURN_TYPES = ("INT", "INT", "FLOAT") RETURN_TYPES = ("INT", "INT", "FLOAT", "INT")
RETURN_NAMES = ("width", "height", "upscale") RETURN_NAMES = ("width", "height", "upscale", "total_resolutions")
FUNCTION = "read_resolution" FUNCTION = "read_resolution"
CATEGORY = "AODH Pack" CATEGORY = "AODH Pack"
_current_index = 0 def __init__(self):
_current_count = 0 self.current_index = 0
_last_selection = None self.current_count = 0
self.last_selection = None
self.last_start_resolution = None
def read_resolution(self, resolution, selection_mode, repeat_count): def read_resolution(self, resolution, selection_mode, repeat_count):
repeat_count = max(1, repeat_count)
base_path = os.path.dirname(os.path.realpath(__file__)) base_path = os.path.dirname(os.path.realpath(__file__))
file_path = os.path.join(base_path, "resolutions", "resolutions.txt") file_path = os.path.join(base_path, "resolutions", "resolutions.txt")
@@ -49,22 +52,33 @@ class ResolutionReader:
lines = [line.strip() for line in f.readlines() if line.strip()] lines = [line.strip() for line in f.readlines() if line.strip()]
if not lines: if not lines:
return (0, 0, 0.0) return (0, 0, 0.0, 0)
# Reset sequence if resolution changes
if self.last_start_resolution != resolution:
try:
self.current_index = lines.index(resolution)
except ValueError:
self.current_index = 0
self.current_count = 0
self.last_start_resolution = resolution
if selection_mode == "manual": if selection_mode == "manual":
selected_line = resolution selected_line = resolution
elif selection_mode == "sequential": elif selection_mode == "sequential":
if self.__class__._current_count >= repeat_count: if self.current_count >= repeat_count:
self.__class__._current_index += 1 self.current_index += 1
self.__class__._current_count = 0 self.current_count = 0
selected_line = lines[self.__class__._current_index % len(lines)] selected_line = lines[self.current_index % len(lines)]
self.__class__._current_count += 1 self.current_count += 1
elif selection_mode == "random": elif selection_mode == "random":
if self.__class__._current_count >= repeat_count or self.__class__._last_selection is None: if self.current_count >= repeat_count or self.last_selection is None:
self.__class__._last_selection = random.choice(lines) # Use a local Random instance to ensure randomness regardless of global seed
self.__class__._current_count = 0 rng = random.Random()
selected_line = self.__class__._last_selection self.last_selection = rng.choice(lines)
self.__class__._current_count += 1 self.current_count = 0
selected_line = self.last_selection
self.current_count += 1
else: else:
selected_line = resolution selected_line = resolution
@@ -74,6 +88,6 @@ class ResolutionReader:
width = int(parts[0]) width = int(parts[0])
height = int(parts[1]) height = int(parts[1])
upscale = float(parts[2]) upscale = float(parts[2])
return (width, height, upscale) return (width, height, upscale, len(lines))
except (ValueError, IndexError): except (ValueError, IndexError):
return (0, 0, 0.0) return (0, 0, 0.0, len(lines))

View File

@@ -1,4 +1,8 @@
1280,720,3.0 1280,720,3.0
720,1280,3.0 720,1280,3.0
1376,576,2.5 1376,576,2.5
576,1376,2.5
1024,1024,2.0 1024,1024,2.0
1280,800,2.7
1125,750,2.0
750,1125,2.0