Add outfit gallery and AI-powered creation for characters and outfits
- Add outfit gallery with CRUD operations (create, read, update, delete) - Add AI-powered profile generation for both characters and outfits - Add toggle to switch between AI generation and manual creation - Auto-generate filenames from names with incrementing for duplicates - Add 'full_body' and 'bottom' fields to wardrobe structure - Update all character and outfit JSON files with new wardrobe fields - Reorganize data into data/characters and data/clothing directories - Update README with new features and JSON structure documentation
This commit is contained in:
85
README.md
85
README.md
@@ -5,6 +5,8 @@ A local web-based GUI for managing character profiles (JSON) and generating cons
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Character Gallery**: Automatically scans your `characters/` folder and builds a searchable, sortable database.
|
- **Character Gallery**: Automatically scans your `characters/` folder and builds a searchable, sortable database.
|
||||||
|
- **Outfit Gallery**: Manage reusable outfit presets that can be applied to any character.
|
||||||
|
- **AI-Powered Creation**: Create new characters and outfits using AI to generate profiles from descriptions, or manually create blank templates.
|
||||||
- **Granular Prompt Control**: Every field in your character JSON (Identity, Wardrobe, Styles) has a checkbox. You decide exactly what is sent to the AI.
|
- **Granular Prompt Control**: Every field in your character JSON (Identity, Wardrobe, Styles) has a checkbox. You decide exactly what is sent to the AI.
|
||||||
- **ComfyUI Integration**:
|
- **ComfyUI Integration**:
|
||||||
- **SDXL Optimized**: Designed for high-quality SDXL workflows.
|
- **SDXL Optimized**: Designed for high-quality SDXL workflows.
|
||||||
@@ -45,6 +47,11 @@ A local web-based GUI for managing character profiles (JSON) and generating cons
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### Creating Characters & Outfits
|
||||||
|
- **AI Generation**: Toggle "Use AI to generate profile from description" on, then describe your character or outfit. The AI will generate a complete profile with appropriate tags.
|
||||||
|
- **Manual Creation**: Toggle AI generation off to create a blank template you can edit yourself.
|
||||||
|
- **Auto-naming**: Leave the filename field empty to auto-generate one from the name. If a file already exists, a number will be appended automatically.
|
||||||
|
|
||||||
### Gallery Management
|
### Gallery Management
|
||||||
- **Rescan**: Use the "Rescan Character Files" button if you've added new JSON files or manually edited them.
|
- **Rescan**: Use the "Rescan Character Files" button if you've added new JSON files or manually edited them.
|
||||||
- **Save Defaults**: On a character page, select your favorite prompt combination and click "Save as Default Selection" to remember it for future quick generations.
|
- **Save Defaults**: On a character page, select your favorite prompt combination and click "Save as Default Selection" to remember it for future quick generations.
|
||||||
@@ -57,9 +64,85 @@ A local web-based GUI for managing character profiles (JSON) and generating cons
|
|||||||
./launch.sh --clean
|
./launch.sh --clean
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## JSON Structure
|
||||||
|
|
||||||
|
### Character Profile
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"character_id": "example_character",
|
||||||
|
"character_name": "Example Character",
|
||||||
|
"identity": {
|
||||||
|
"base_specs": "1girl, slender build, fair skin",
|
||||||
|
"hair": "long blue hair",
|
||||||
|
"eyes": "blue eyes",
|
||||||
|
"hands": "",
|
||||||
|
"arms": "",
|
||||||
|
"torso": "",
|
||||||
|
"pelvis": "",
|
||||||
|
"legs": "",
|
||||||
|
"feet": "",
|
||||||
|
"extra": ""
|
||||||
|
},
|
||||||
|
"defaults": {
|
||||||
|
"expression": "smile",
|
||||||
|
"pose": "standing",
|
||||||
|
"scene": "simple background"
|
||||||
|
},
|
||||||
|
"wardrobe": {
|
||||||
|
"default": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
|
"top": "white blouse",
|
||||||
|
"bottom": "blue skirt",
|
||||||
|
"legwear": "black thighhighs",
|
||||||
|
"footwear": "black shoes",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": "ribbon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"styles": {
|
||||||
|
"aesthetic": "anime style",
|
||||||
|
"primary_color": "blue",
|
||||||
|
"secondary_color": "white",
|
||||||
|
"tertiary_color": ""
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 1.0,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": ["tag1", "tag2"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Outfit Profile
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"outfit_id": "school_uniform_01",
|
||||||
|
"outfit_name": "School Uniform",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
|
"top": "white blouse, sailor collar",
|
||||||
|
"bottom": "pleated skirt",
|
||||||
|
"legwear": "knee socks",
|
||||||
|
"footwear": "loafers",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": "ribbon tie"
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": ["school uniform", "uniform"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
- `/characters`: Your character JSON files.
|
- `/data/characters`: Your character JSON files.
|
||||||
|
- `/data/clothing`: Outfit preset JSON files.
|
||||||
- `/static/uploads`: Generated images (organized by character subfolders).
|
- `/static/uploads`: Generated images (organized by character subfolders).
|
||||||
- `/templates`: HTML UI using Bootstrap 5.
|
- `/templates`: HTML UI using Bootstrap 5.
|
||||||
- `app.py`: Flask backend and prompt-building logic.
|
- `app.py`: Flask backend and prompt-building logic.
|
||||||
|
|||||||
757
app.py
757
app.py
@@ -6,14 +6,15 @@ import requests
|
|||||||
import random
|
import random
|
||||||
from flask import Flask, render_template, request, redirect, url_for, flash, session
|
from flask import Flask, render_template, request, redirect, url_for, flash, session
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from models import db, Character, Settings
|
from models import db, Character, Settings, Outfit
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
app.config['UPLOAD_FOLDER'] = 'static/uploads'
|
app.config['UPLOAD_FOLDER'] = 'static/uploads'
|
||||||
app.config['SECRET_KEY'] = 'dev-key-123'
|
app.config['SECRET_KEY'] = 'dev-key-123'
|
||||||
app.config['CHARACTERS_DIR'] = 'characters'
|
app.config['CHARACTERS_DIR'] = 'data/characters'
|
||||||
|
app.config['CLOTHING_DIR'] = 'data/clothing'
|
||||||
app.config['COMFYUI_URL'] = 'http://127.0.0.1:8188'
|
app.config['COMFYUI_URL'] = 'http://127.0.0.1:8188'
|
||||||
app.config['ILLUSTRIOUS_MODELS_DIR'] = '/mnt/alexander/AITools/Image Models/Stable-diffusion/Illustrious/'
|
app.config['ILLUSTRIOUS_MODELS_DIR'] = '/mnt/alexander/AITools/Image Models/Stable-diffusion/Illustrious/'
|
||||||
app.config['NOOB_MODELS_DIR'] = '/mnt/alexander/AITools/Image Models/Stable-diffusion/Noob/'
|
app.config['NOOB_MODELS_DIR'] = '/mnt/alexander/AITools/Image Models/Stable-diffusion/Noob/'
|
||||||
@@ -32,6 +33,16 @@ def get_available_loras():
|
|||||||
loras.append(f"Illustrious/Looks/{f}")
|
loras.append(f"Illustrious/Looks/{f}")
|
||||||
return sorted(loras)
|
return sorted(loras)
|
||||||
|
|
||||||
|
def get_available_clothing_loras():
|
||||||
|
"""Get LoRAs from the Clothing directory for outfit LoRAs."""
|
||||||
|
clothing_lora_dir = '/mnt/alexander/AITools/Image Models/lora/Illustrious/Clothing/'
|
||||||
|
loras = []
|
||||||
|
if os.path.exists(clothing_lora_dir):
|
||||||
|
for f in os.listdir(clothing_lora_dir):
|
||||||
|
if f.endswith('.safetensors'):
|
||||||
|
loras.append(f"Illustrious/Clothing/{f}")
|
||||||
|
return sorted(loras)
|
||||||
|
|
||||||
def get_available_checkpoints():
|
def get_available_checkpoints():
|
||||||
checkpoints = []
|
checkpoints = []
|
||||||
|
|
||||||
@@ -78,9 +89,12 @@ def build_prompt(data, selected_fields=None, default_fields=None, active_outfit=
|
|||||||
defaults = data.get('defaults', {})
|
defaults = data.get('defaults', {})
|
||||||
|
|
||||||
# Pre-calculate Hand/Glove priority
|
# Pre-calculate Hand/Glove priority
|
||||||
|
# Priority: wardrobe gloves > wardrobe hands (outfit) > identity hands (character)
|
||||||
hand_val = ""
|
hand_val = ""
|
||||||
if wardrobe.get('gloves') and is_selected('wardrobe', 'gloves'):
|
if wardrobe.get('gloves') and is_selected('wardrobe', 'gloves'):
|
||||||
hand_val = wardrobe.get('gloves')
|
hand_val = wardrobe.get('gloves')
|
||||||
|
elif wardrobe.get('hands') and is_selected('wardrobe', 'hands'):
|
||||||
|
hand_val = wardrobe.get('hands')
|
||||||
elif identity.get('hands') and is_selected('identity', 'hands'):
|
elif identity.get('hands') and is_selected('identity', 'hands'):
|
||||||
hand_val = identity.get('hands')
|
hand_val = identity.get('hands')
|
||||||
|
|
||||||
@@ -123,11 +137,12 @@ def build_prompt(data, selected_fields=None, default_fields=None, active_outfit=
|
|||||||
if lora.get('lora_triggers') and is_selected('lora', 'lora_triggers'):
|
if lora.get('lora_triggers') and is_selected('lora', 'lora_triggers'):
|
||||||
parts.append(lora.get('lora_triggers'))
|
parts.append(lora.get('lora_triggers'))
|
||||||
|
|
||||||
# 2. Face Prompt: Tag, Eyes, Expression
|
# 2. Face Prompt: Tag, Eyes, Expression, Headwear
|
||||||
face_parts = []
|
face_parts = []
|
||||||
if char_tag and is_selected('special', 'name'): face_parts.append(char_tag)
|
if char_tag and is_selected('special', 'name'): face_parts.append(char_tag)
|
||||||
if identity.get('eyes') and is_selected('identity', 'eyes'): face_parts.append(identity.get('eyes'))
|
if identity.get('eyes') and is_selected('identity', 'eyes'): face_parts.append(identity.get('eyes'))
|
||||||
if defaults.get('expression') and is_selected('defaults', 'expression'): face_parts.append(defaults.get('expression'))
|
if defaults.get('expression') and is_selected('defaults', 'expression'): face_parts.append(defaults.get('expression'))
|
||||||
|
if wardrobe.get('headwear') and is_selected('wardrobe', 'headwear'): face_parts.append(wardrobe.get('headwear'))
|
||||||
|
|
||||||
# 3. Hand Prompt: Hand value (Gloves or Hands)
|
# 3. Hand Prompt: Hand value (Gloves or Hands)
|
||||||
hand_parts = [hand_val] if hand_val else []
|
hand_parts = [hand_val] if hand_val else []
|
||||||
@@ -217,6 +232,64 @@ def sync_characters():
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
def sync_outfits():
|
||||||
|
if not os.path.exists(app.config['CLOTHING_DIR']):
|
||||||
|
return
|
||||||
|
|
||||||
|
current_ids = []
|
||||||
|
|
||||||
|
for filename in os.listdir(app.config['CLOTHING_DIR']):
|
||||||
|
if filename.endswith('.json'):
|
||||||
|
file_path = os.path.join(app.config['CLOTHING_DIR'], filename)
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
outfit_id = data.get('outfit_id') or filename.replace('.json', '')
|
||||||
|
|
||||||
|
current_ids.append(outfit_id)
|
||||||
|
|
||||||
|
# Generate URL-safe slug: remove special characters from outfit_id
|
||||||
|
slug = re.sub(r'[^a-zA-Z0-9_]', '', outfit_id)
|
||||||
|
|
||||||
|
# Check if outfit already exists
|
||||||
|
outfit = Outfit.query.filter_by(outfit_id=outfit_id).first()
|
||||||
|
name = data.get('outfit_name', outfit_id.replace('_', ' ').title())
|
||||||
|
|
||||||
|
if outfit:
|
||||||
|
outfit.data = data
|
||||||
|
outfit.name = name
|
||||||
|
outfit.slug = slug
|
||||||
|
outfit.filename = filename
|
||||||
|
|
||||||
|
# Check if cover image still exists
|
||||||
|
if outfit.image_path:
|
||||||
|
full_img_path = os.path.join(app.config['UPLOAD_FOLDER'], outfit.image_path)
|
||||||
|
if not os.path.exists(full_img_path):
|
||||||
|
print(f"Image missing for {outfit.name}, clearing path.")
|
||||||
|
outfit.image_path = None
|
||||||
|
|
||||||
|
# Explicitly tell SQLAlchemy the JSON field was modified
|
||||||
|
flag_modified(outfit, "data")
|
||||||
|
else:
|
||||||
|
new_outfit = Outfit(
|
||||||
|
outfit_id=outfit_id,
|
||||||
|
slug=slug,
|
||||||
|
filename=filename,
|
||||||
|
name=name,
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
db.session.add(new_outfit)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error importing outfit {filename}: {e}")
|
||||||
|
|
||||||
|
# Remove outfits that are no longer in the folder
|
||||||
|
all_outfits = Outfit.query.all()
|
||||||
|
for outfit in all_outfits:
|
||||||
|
if outfit.outfit_id not in current_ids:
|
||||||
|
db.session.delete(outfit)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def call_llm(prompt, system_prompt="You are a creative assistant."):
|
def call_llm(prompt, system_prompt="You are a creative assistant."):
|
||||||
settings = Settings.query.first()
|
settings = Settings.query.first()
|
||||||
if not settings or not settings.openrouter_api_key:
|
if not settings or not settings.openrouter_api_key:
|
||||||
@@ -337,14 +410,14 @@ def generator():
|
|||||||
image_info = outputs[node_id]['images'][0]
|
image_info = outputs[node_id]['images'][0]
|
||||||
image_data = get_image(image_info['filename'], image_info['subfolder'], image_info['type'])
|
image_data = get_image(image_info['filename'], image_info['subfolder'], image_info['type'])
|
||||||
|
|
||||||
char_folder = os.path.join(app.config['UPLOAD_FOLDER'], character.slug)
|
char_folder = os.path.join(app.config['UPLOAD_FOLDER'], f"characters/{character.slug}")
|
||||||
os.makedirs(char_folder, exist_ok=True)
|
os.makedirs(char_folder, exist_ok=True)
|
||||||
filename = f"gen_{int(time.time())}.png"
|
filename = f"gen_{int(time.time())}.png"
|
||||||
file_path = os.path.join(char_folder, filename)
|
file_path = os.path.join(char_folder, filename)
|
||||||
with open(file_path, 'wb') as f:
|
with open(file_path, 'wb') as f:
|
||||||
f.write(image_data)
|
f.write(image_data)
|
||||||
|
|
||||||
relative_path = f"{character.slug}/{filename}"
|
relative_path = f"characters/{character.slug}/{filename}"
|
||||||
return render_template('generator.html', characters=characters, checkpoints=checkpoints,
|
return render_template('generator.html', characters=characters, checkpoints=checkpoints,
|
||||||
generated_image=relative_path, selected_char=char_slug, selected_ckpt=checkpoint)
|
generated_image=relative_path, selected_char=char_slug, selected_ckpt=checkpoint)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
@@ -370,18 +443,30 @@ def detail(slug):
|
|||||||
def create_character():
|
def create_character():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
name = request.form.get('name')
|
name = request.form.get('name')
|
||||||
slug = request.form.get('filename')
|
slug = request.form.get('filename', '').strip()
|
||||||
prompt = request.form.get('prompt')
|
prompt = request.form.get('prompt', '')
|
||||||
|
use_llm = request.form.get('use_llm') == 'on'
|
||||||
|
|
||||||
|
# Auto-generate slug from name if not provided
|
||||||
|
if not slug:
|
||||||
|
slug = re.sub(r'[^a-zA-Z0-9]+', '_', name.lower()).strip('_')
|
||||||
|
|
||||||
# Validate slug
|
# Validate slug
|
||||||
safe_slug = re.sub(r'[^a-zA-Z0-9_]', '', slug)
|
safe_slug = re.sub(r'[^a-zA-Z0-9_]', '', slug)
|
||||||
if not safe_slug:
|
if not safe_slug:
|
||||||
flash("Invalid filename.")
|
safe_slug = 'character'
|
||||||
return redirect(request.url)
|
|
||||||
|
|
||||||
# Check if exists
|
# Find available filename (increment if exists)
|
||||||
if os.path.exists(os.path.join(app.config['CHARACTERS_DIR'], f"{safe_slug}.json")):
|
base_slug = safe_slug
|
||||||
flash("Character with this filename already exists.")
|
counter = 1
|
||||||
|
while os.path.exists(os.path.join(app.config['CHARACTERS_DIR'], f"{safe_slug}.json")):
|
||||||
|
safe_slug = f"{base_slug}_{counter}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
# Check if LLM generation is requested
|
||||||
|
if use_llm:
|
||||||
|
if not prompt:
|
||||||
|
flash("Description is required when AI generation is enabled.")
|
||||||
return redirect(request.url)
|
return redirect(request.url)
|
||||||
|
|
||||||
# Generate JSON with LLM
|
# Generate JSON with LLM
|
||||||
@@ -408,8 +493,10 @@ def create_character():
|
|||||||
"scene": ""
|
"scene": ""
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
|
"full_body": "string (e.g. bodysuit, dress, full outfit description)",
|
||||||
"headwear": "string",
|
"headwear": "string",
|
||||||
"top": "string",
|
"top": "string",
|
||||||
|
"bottom": "string",
|
||||||
"legwear": "string",
|
"legwear": "string",
|
||||||
"footwear": "string",
|
"footwear": "string",
|
||||||
"hands": "string",
|
"hands": "string",
|
||||||
@@ -428,7 +515,7 @@ def create_character():
|
|||||||
},
|
},
|
||||||
"tags": ["string", "string"]
|
"tags": ["string", "string"]
|
||||||
}
|
}
|
||||||
Fill the fields based on the user's description. Use Danbooru-style tags for the values (e.g. 'long hair', 'blue eyes'). Keep values concise. Leave defaults fields empty."""
|
Fill the fields based on the user's description. Use Danbooru-style tags for the values (e.g. 'long hair', 'blue eyes'). Keep values concise. Use empty strings "" for fields that are not applicable or unknown - never use words like "none" or "n/a". Leave defaults fields empty."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
llm_response = call_llm(f"Create a character profile for '{name}' based on this description: {prompt}", system_prompt)
|
llm_response = call_llm(f"Create a character profile for '{name}' based on this description: {prompt}", system_prompt)
|
||||||
@@ -441,6 +528,57 @@ def create_character():
|
|||||||
char_data['character_id'] = safe_slug
|
char_data['character_id'] = safe_slug
|
||||||
char_data['character_name'] = name
|
char_data['character_name'] = name
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"LLM error: {e}")
|
||||||
|
flash(f"Failed to generate character profile: {e}")
|
||||||
|
return redirect(request.url)
|
||||||
|
else:
|
||||||
|
# Create blank character template
|
||||||
|
char_data = {
|
||||||
|
"character_id": safe_slug,
|
||||||
|
"character_name": name,
|
||||||
|
"identity": {
|
||||||
|
"base_specs": "",
|
||||||
|
"hair": "",
|
||||||
|
"eyes": "",
|
||||||
|
"hands": "",
|
||||||
|
"arms": "",
|
||||||
|
"torso": "",
|
||||||
|
"pelvis": "",
|
||||||
|
"legs": "",
|
||||||
|
"feet": "",
|
||||||
|
"extra": ""
|
||||||
|
},
|
||||||
|
"defaults": {
|
||||||
|
"expression": "",
|
||||||
|
"pose": "",
|
||||||
|
"scene": ""
|
||||||
|
},
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
|
"top": "",
|
||||||
|
"bottom": "",
|
||||||
|
"legwear": "",
|
||||||
|
"footwear": "",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": ""
|
||||||
|
},
|
||||||
|
"styles": {
|
||||||
|
"aesthetic": "",
|
||||||
|
"primary_color": "",
|
||||||
|
"secondary_color": "",
|
||||||
|
"tertiary_color": ""
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 1.0,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
# Save file
|
# Save file
|
||||||
file_path = os.path.join(app.config['CHARACTERS_DIR'], f"{safe_slug}.json")
|
file_path = os.path.join(app.config['CHARACTERS_DIR'], f"{safe_slug}.json")
|
||||||
with open(file_path, 'w') as f:
|
with open(file_path, 'w') as f:
|
||||||
@@ -461,7 +599,7 @@ def create_character():
|
|||||||
return redirect(url_for('detail', slug=safe_slug))
|
return redirect(url_for('detail', slug=safe_slug))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"LLM/Save error: {e}")
|
print(f"Save error: {e}")
|
||||||
flash(f"Failed to create character: {e}")
|
flash(f"Failed to create character: {e}")
|
||||||
return redirect(request.url)
|
return redirect(request.url)
|
||||||
|
|
||||||
@@ -698,7 +836,7 @@ def upload_image(slug):
|
|||||||
|
|
||||||
if file and allowed_file(file.filename):
|
if file and allowed_file(file.filename):
|
||||||
# Create character subfolder
|
# Create character subfolder
|
||||||
char_folder = os.path.join(app.config['UPLOAD_FOLDER'], slug)
|
char_folder = os.path.join(app.config['UPLOAD_FOLDER'], f"characters/{slug}")
|
||||||
os.makedirs(char_folder, exist_ok=True)
|
os.makedirs(char_folder, exist_ok=True)
|
||||||
|
|
||||||
filename = secure_filename(file.filename)
|
filename = secure_filename(file.filename)
|
||||||
@@ -706,7 +844,7 @@ def upload_image(slug):
|
|||||||
file.save(file_path)
|
file.save(file_path)
|
||||||
|
|
||||||
# Store relative path in DB
|
# Store relative path in DB
|
||||||
character.image_path = f"{slug}/{filename}"
|
character.image_path = f"characters/{slug}/{filename}"
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Image uploaded successfully!')
|
flash('Image uploaded successfully!')
|
||||||
|
|
||||||
@@ -729,7 +867,7 @@ def finalize_generation(slug, prompt_id):
|
|||||||
image_data = get_image(image_info['filename'], image_info['subfolder'], image_info['type'])
|
image_data = get_image(image_info['filename'], image_info['subfolder'], image_info['type'])
|
||||||
|
|
||||||
# Create character subfolder
|
# Create character subfolder
|
||||||
char_folder = os.path.join(app.config['UPLOAD_FOLDER'], slug)
|
char_folder = os.path.join(app.config['UPLOAD_FOLDER'], f"characters/{slug}")
|
||||||
os.makedirs(char_folder, exist_ok=True)
|
os.makedirs(char_folder, exist_ok=True)
|
||||||
|
|
||||||
filename = f"gen_{int(time.time())}.png"
|
filename = f"gen_{int(time.time())}.png"
|
||||||
@@ -739,16 +877,14 @@ def finalize_generation(slug, prompt_id):
|
|||||||
|
|
||||||
print(f"Image saved to: {os.path.abspath(file_path)}")
|
print(f"Image saved to: {os.path.abspath(file_path)}")
|
||||||
|
|
||||||
# Handle actions
|
# Handle actions - always save as preview
|
||||||
relative_path = f"{slug}/{filename}"
|
relative_path = f"characters/{slug}/{filename}"
|
||||||
|
session[f'preview_{slug}'] = relative_path
|
||||||
|
|
||||||
|
# If action is 'replace', also update the character's cover image immediately
|
||||||
if action == 'replace':
|
if action == 'replace':
|
||||||
character.image_path = relative_path
|
character.image_path = relative_path
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Cover image updated!')
|
|
||||||
else:
|
|
||||||
# Preview mode
|
|
||||||
session[f'preview_{slug}'] = relative_path
|
|
||||||
|
|
||||||
return {'success': True, 'image_url': url_for('static', filename=f'uploads/{relative_path}')}
|
return {'success': True, 'image_url': url_for('static', filename=f'uploads/{relative_path}')}
|
||||||
|
|
||||||
@@ -757,7 +893,21 @@ def finalize_generation(slug, prompt_id):
|
|||||||
print(f"Finalize error: {e}")
|
print(f"Finalize error: {e}")
|
||||||
return {'error': str(e)}, 500
|
return {'error': str(e)}, 500
|
||||||
|
|
||||||
def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_negative=None):
|
@app.route('/character/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||||
|
def replace_cover_from_preview(slug):
|
||||||
|
character = Character.query.filter_by(slug=slug).first_or_404()
|
||||||
|
preview_path = session.get(f'preview_{slug}')
|
||||||
|
|
||||||
|
if preview_path:
|
||||||
|
character.image_path = preview_path
|
||||||
|
db.session.commit()
|
||||||
|
flash('Cover image updated from preview!')
|
||||||
|
else:
|
||||||
|
flash('No preview image available', 'error')
|
||||||
|
|
||||||
|
return redirect(url_for('detail', slug=slug))
|
||||||
|
|
||||||
|
def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_negative=None, outfit=None):
|
||||||
# 1. Update prompts using replacement to preserve embeddings
|
# 1. Update prompts using replacement to preserve embeddings
|
||||||
workflow["6"]["inputs"]["text"] = workflow["6"]["inputs"]["text"].replace("{{POSITIVE_PROMPT}}", prompts["main"])
|
workflow["6"]["inputs"]["text"] = workflow["6"]["inputs"]["text"].replace("{{POSITIVE_PROMPT}}", prompts["main"])
|
||||||
|
|
||||||
@@ -782,19 +932,43 @@ def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_nega
|
|||||||
if checkpoint:
|
if checkpoint:
|
||||||
workflow["4"]["inputs"]["ckpt_name"] = checkpoint
|
workflow["4"]["inputs"]["ckpt_name"] = checkpoint
|
||||||
|
|
||||||
# 3. Handle LoRA
|
# 3. Handle LoRAs - Node 16 for character, Node 17 for outfit
|
||||||
lora_data = character.data.get('lora', {})
|
# Start with direct checkpoint connections
|
||||||
lora_name = lora_data.get('lora_name')
|
|
||||||
|
|
||||||
model_source = ["4", 0]
|
model_source = ["4", 0]
|
||||||
clip_source = ["4", 1]
|
clip_source = ["4", 1]
|
||||||
|
|
||||||
if lora_name and "16" in workflow:
|
# Character LoRA (Node 16)
|
||||||
workflow["16"]["inputs"]["lora_name"] = lora_name
|
char_lora_data = character.data.get('lora', {}) if character else {}
|
||||||
workflow["16"]["inputs"]["strength_model"] = lora_data.get('lora_weight', 1.0)
|
char_lora_name = char_lora_data.get('lora_name')
|
||||||
workflow["16"]["inputs"]["strength_clip"] = lora_data.get('lora_weight', 1.0)
|
|
||||||
|
if char_lora_name and "16" in workflow:
|
||||||
|
workflow["16"]["inputs"]["lora_name"] = char_lora_name
|
||||||
|
workflow["16"]["inputs"]["strength_model"] = char_lora_data.get('lora_weight', 1.0)
|
||||||
|
workflow["16"]["inputs"]["strength_clip"] = char_lora_data.get('lora_weight', 1.0)
|
||||||
|
workflow["16"]["inputs"]["model"] = ["4", 0] # From checkpoint
|
||||||
|
workflow["16"]["inputs"]["clip"] = ["4", 1] # From checkpoint
|
||||||
model_source = ["16", 0]
|
model_source = ["16", 0]
|
||||||
clip_source = ["16", 1]
|
clip_source = ["16", 1]
|
||||||
|
print(f"Character LoRA: {char_lora_name} @ {char_lora_data.get('lora_weight', 1.0)}")
|
||||||
|
|
||||||
|
# Outfit LoRA (Node 17) - chains from character LoRA or checkpoint
|
||||||
|
outfit_lora_data = outfit.data.get('lora', {}) if outfit else {}
|
||||||
|
outfit_lora_name = outfit_lora_data.get('lora_name')
|
||||||
|
|
||||||
|
if outfit_lora_name and "17" in workflow:
|
||||||
|
workflow["17"]["inputs"]["lora_name"] = outfit_lora_name
|
||||||
|
workflow["17"]["inputs"]["strength_model"] = outfit_lora_data.get('lora_weight', 0.8)
|
||||||
|
workflow["17"]["inputs"]["strength_clip"] = outfit_lora_data.get('lora_weight', 0.8)
|
||||||
|
# Chain from character LoRA (node 16) or checkpoint (node 4)
|
||||||
|
if char_lora_name and "16" in workflow:
|
||||||
|
workflow["17"]["inputs"]["model"] = ["16", 0]
|
||||||
|
workflow["17"]["inputs"]["clip"] = ["16", 1]
|
||||||
|
else:
|
||||||
|
workflow["17"]["inputs"]["model"] = ["4", 0]
|
||||||
|
workflow["17"]["inputs"]["clip"] = ["4", 1]
|
||||||
|
model_source = ["17", 0]
|
||||||
|
clip_source = ["17", 1]
|
||||||
|
print(f"Outfit LoRA: {outfit_lora_name} @ {outfit_lora_data.get('lora_weight', 0.8)}")
|
||||||
|
|
||||||
# Apply connections to all model/clip consumers
|
# Apply connections to all model/clip consumers
|
||||||
workflow["3"]["inputs"]["model"] = model_source
|
workflow["3"]["inputs"]["model"] = model_source
|
||||||
@@ -843,15 +1017,33 @@ def clear_all_covers():
|
|||||||
|
|
||||||
@app.route('/generate_missing', methods=['POST'])
|
@app.route('/generate_missing', methods=['POST'])
|
||||||
def generate_missing():
|
def generate_missing():
|
||||||
missing = Character.query.filter((Character.image_path == None) | (Character.image_path == '')).all()
|
# Query fresh from database for each check to avoid stale session issues
|
||||||
if not missing:
|
def get_missing_count():
|
||||||
|
return Character.query.filter((Character.image_path == None) | (Character.image_path == '')).count()
|
||||||
|
|
||||||
|
if get_missing_count() == 0:
|
||||||
flash("No characters missing cover images.")
|
flash("No characters missing cover images.")
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
success_count = 0
|
success_count = 0
|
||||||
for character in missing:
|
processed = 0
|
||||||
|
|
||||||
|
# Keep generating until no more missing
|
||||||
|
while get_missing_count() > 0:
|
||||||
|
# Get the next character in alphabetical order
|
||||||
|
character = Character.query.filter(
|
||||||
|
(Character.image_path == None) | (Character.image_path == '')
|
||||||
|
).order_by(Character.name).first()
|
||||||
|
|
||||||
|
if not character:
|
||||||
|
break
|
||||||
|
|
||||||
|
character_slug = character.slug
|
||||||
|
character_name = character.name
|
||||||
|
|
||||||
|
processed += 1
|
||||||
try:
|
try:
|
||||||
print(f"Batch generating for: {character.name}")
|
print(f"Batch generating for: {character_name}")
|
||||||
prompt_response = _queue_generation(character, action='replace')
|
prompt_response = _queue_generation(character, action='replace')
|
||||||
prompt_id = prompt_response['prompt_id']
|
prompt_id = prompt_response['prompt_id']
|
||||||
|
|
||||||
@@ -866,22 +1058,27 @@ def generate_missing():
|
|||||||
image_info = outputs[node_id]['images'][0]
|
image_info = outputs[node_id]['images'][0]
|
||||||
image_data = get_image(image_info['filename'], image_info['subfolder'], image_info['type'])
|
image_data = get_image(image_info['filename'], image_info['subfolder'], image_info['type'])
|
||||||
|
|
||||||
char_folder = os.path.join(app.config['UPLOAD_FOLDER'], character.slug)
|
char_folder = os.path.join(app.config['UPLOAD_FOLDER'], f"characters/{character_slug}")
|
||||||
os.makedirs(char_folder, exist_ok=True)
|
os.makedirs(char_folder, exist_ok=True)
|
||||||
filename = f"gen_{int(time.time())}.png"
|
filename = f"gen_{int(time.time())}.png"
|
||||||
file_path = os.path.join(char_folder, filename)
|
file_path = os.path.join(char_folder, filename)
|
||||||
with open(file_path, 'wb') as f:
|
with open(file_path, 'wb') as f:
|
||||||
f.write(image_data)
|
f.write(image_data)
|
||||||
|
|
||||||
character.image_path = f"{character.slug}/{filename}"
|
# Re-query the character to ensure it's attached to the session
|
||||||
|
character_to_update = Character.query.filter_by(slug=character_slug).first()
|
||||||
|
if character_to_update:
|
||||||
|
character_to_update.image_path = f"characters/{character_slug}/{filename}"
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
print(f"Saved cover for {character_name}: {character_to_update.image_path}")
|
||||||
success_count += 1
|
success_count += 1
|
||||||
break
|
break
|
||||||
break
|
break
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
max_retries -= 1
|
max_retries -= 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error generating for {character.name}: {e}")
|
print(f"Error generating for {character_name}: {e}")
|
||||||
|
db.session.rollback() # Rollback on error to ensure clean state
|
||||||
|
|
||||||
flash(f"Batch generation complete. Generated {success_count} images.")
|
flash(f"Batch generation complete. Generated {success_count} images.")
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
@@ -941,6 +1138,487 @@ def save_defaults(slug):
|
|||||||
flash('Default prompt selection saved for this character!')
|
flash('Default prompt selection saved for this character!')
|
||||||
return redirect(url_for('detail', slug=slug))
|
return redirect(url_for('detail', slug=slug))
|
||||||
|
|
||||||
|
# ============ OUTFIT ROUTES ============
|
||||||
|
|
||||||
|
@app.route('/outfits')
|
||||||
|
def outfits_index():
|
||||||
|
outfits = Outfit.query.order_by(Outfit.name).all()
|
||||||
|
return render_template('outfits/index.html', outfits=outfits)
|
||||||
|
|
||||||
|
@app.route('/outfits/rescan', methods=['POST'])
|
||||||
|
def rescan_outfits():
|
||||||
|
sync_outfits()
|
||||||
|
flash('Database synced with outfit files.')
|
||||||
|
return redirect(url_for('outfits_index'))
|
||||||
|
|
||||||
|
@app.route('/outfit/<path:slug>')
|
||||||
|
def outfit_detail(slug):
|
||||||
|
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||||
|
characters = Character.query.order_by(Character.name).all()
|
||||||
|
|
||||||
|
# Load state from session
|
||||||
|
preferences = session.get(f'prefs_outfit_{slug}')
|
||||||
|
preview_image = session.get(f'preview_outfit_{slug}')
|
||||||
|
selected_character = session.get(f'char_outfit_{slug}')
|
||||||
|
|
||||||
|
return render_template('outfits/detail.html', outfit=outfit, characters=characters,
|
||||||
|
preferences=preferences, preview_image=preview_image,
|
||||||
|
selected_character=selected_character)
|
||||||
|
|
||||||
|
@app.route('/outfit/<path:slug>/edit', methods=['GET', 'POST'])
|
||||||
|
def edit_outfit(slug):
|
||||||
|
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||||
|
loras = get_available_clothing_loras() # Use clothing LoRAs for outfits
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
# 1. Update basic fields
|
||||||
|
outfit.name = request.form.get('outfit_name')
|
||||||
|
|
||||||
|
# 2. Rebuild the data dictionary
|
||||||
|
new_data = outfit.data.copy()
|
||||||
|
new_data['outfit_name'] = outfit.name
|
||||||
|
|
||||||
|
# Update outfit_id if provided
|
||||||
|
new_outfit_id = request.form.get('outfit_id', outfit.outfit_id)
|
||||||
|
new_data['outfit_id'] = new_outfit_id
|
||||||
|
|
||||||
|
# Update wardrobe section
|
||||||
|
if 'wardrobe' in new_data:
|
||||||
|
for key in new_data['wardrobe'].keys():
|
||||||
|
form_key = f"wardrobe_{key}"
|
||||||
|
if form_key in request.form:
|
||||||
|
new_data['wardrobe'][key] = request.form.get(form_key)
|
||||||
|
|
||||||
|
# Update lora section
|
||||||
|
if 'lora' in new_data:
|
||||||
|
for key in new_data['lora'].keys():
|
||||||
|
form_key = f"lora_{key}"
|
||||||
|
if form_key in request.form:
|
||||||
|
val = request.form.get(form_key)
|
||||||
|
if key == 'lora_weight':
|
||||||
|
try: val = float(val)
|
||||||
|
except: val = 0.8
|
||||||
|
new_data['lora'][key] = val
|
||||||
|
|
||||||
|
# Update Tags (comma separated string to list)
|
||||||
|
tags_raw = request.form.get('tags', '')
|
||||||
|
new_data['tags'] = [t.strip() for f in tags_raw.split(',') for t in [f.strip()] if t]
|
||||||
|
|
||||||
|
outfit.data = new_data
|
||||||
|
flag_modified(outfit, "data")
|
||||||
|
|
||||||
|
# 3. Write back to JSON file
|
||||||
|
outfit_file = outfit.filename or f"{re.sub(r'[^a-zA-Z0-9_]', '', outfit.outfit_id)}.json"
|
||||||
|
file_path = os.path.join(app.config['CLOTHING_DIR'], outfit_file)
|
||||||
|
|
||||||
|
with open(file_path, 'w') as f:
|
||||||
|
json.dump(new_data, f, indent=2)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash('Outfit profile updated successfully!')
|
||||||
|
return redirect(url_for('outfit_detail', slug=slug))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Edit error: {e}")
|
||||||
|
flash(f"Error saving changes: {str(e)}")
|
||||||
|
|
||||||
|
return render_template('outfits/edit.html', outfit=outfit, loras=loras)
|
||||||
|
|
||||||
|
@app.route('/outfit/<path:slug>/upload', methods=['POST'])
|
||||||
|
def upload_outfit_image(slug):
|
||||||
|
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||||
|
|
||||||
|
if 'image' not in request.files:
|
||||||
|
flash('No file part')
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
file = request.files['image']
|
||||||
|
if file.filename == '':
|
||||||
|
flash('No selected file')
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
if file and allowed_file(file.filename):
|
||||||
|
# Create outfit subfolder
|
||||||
|
outfit_folder = os.path.join(app.config['UPLOAD_FOLDER'], f"outfits/{slug}")
|
||||||
|
os.makedirs(outfit_folder, exist_ok=True)
|
||||||
|
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
file_path = os.path.join(outfit_folder, filename)
|
||||||
|
file.save(file_path)
|
||||||
|
|
||||||
|
# Store relative path in DB
|
||||||
|
outfit.image_path = f"outfits/{slug}/{filename}"
|
||||||
|
db.session.commit()
|
||||||
|
flash('Image uploaded successfully!')
|
||||||
|
|
||||||
|
return redirect(url_for('outfit_detail', slug=slug))
|
||||||
|
|
||||||
|
@app.route('/outfit/<path:slug>/generate', methods=['POST'])
|
||||||
|
def generate_outfit_image(slug):
|
||||||
|
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get action type
|
||||||
|
action = request.form.get('action', 'preview')
|
||||||
|
client_id = request.form.get('client_id')
|
||||||
|
|
||||||
|
# Get selected fields
|
||||||
|
selected_fields = request.form.getlist('include_field')
|
||||||
|
|
||||||
|
# Get selected character (if any)
|
||||||
|
character_slug = request.form.get('character_slug', '')
|
||||||
|
character = None
|
||||||
|
|
||||||
|
# Handle random character selection
|
||||||
|
if character_slug == '__random__':
|
||||||
|
all_characters = Character.query.all()
|
||||||
|
if all_characters:
|
||||||
|
character = random.choice(all_characters)
|
||||||
|
character_slug = character.slug
|
||||||
|
elif character_slug:
|
||||||
|
character = Character.query.filter_by(slug=character_slug).first()
|
||||||
|
|
||||||
|
# Save preferences
|
||||||
|
session[f'prefs_outfit_{slug}'] = selected_fields
|
||||||
|
session[f'char_outfit_{slug}'] = character_slug
|
||||||
|
|
||||||
|
# Build combined data for prompt building
|
||||||
|
if character:
|
||||||
|
# Combine character identity/defaults with outfit wardrobe
|
||||||
|
combined_data = {
|
||||||
|
'character_id': character.character_id,
|
||||||
|
'identity': character.data.get('identity', {}),
|
||||||
|
'defaults': character.data.get('defaults', {}),
|
||||||
|
'wardrobe': outfit.data.get('wardrobe', {}), # Use outfit's wardrobe
|
||||||
|
'styles': character.data.get('styles', {}), # Use character's styles
|
||||||
|
'lora': outfit.data.get('lora', {}), # Use outfit's lora
|
||||||
|
'tags': outfit.data.get('tags', [])
|
||||||
|
}
|
||||||
|
|
||||||
|
# When character is selected, merge character identity fields into selected_fields
|
||||||
|
# so they are included in the prompt
|
||||||
|
if selected_fields:
|
||||||
|
# Add character identity fields to selection if not already present
|
||||||
|
for key in ['base_specs', 'hair', 'eyes', 'hands', 'arms', 'torso', 'pelvis', 'legs', 'feet', 'extra']:
|
||||||
|
if character.data.get('identity', {}).get(key):
|
||||||
|
field_key = f'identity::{key}'
|
||||||
|
if field_key not in selected_fields:
|
||||||
|
selected_fields.append(field_key)
|
||||||
|
# Add expression and pose, but NOT scene (outfit previews use simple background)
|
||||||
|
for key in ['expression', 'pose']:
|
||||||
|
if character.data.get('defaults', {}).get(key):
|
||||||
|
field_key = f'defaults::{key}'
|
||||||
|
if field_key not in selected_fields:
|
||||||
|
selected_fields.append(field_key)
|
||||||
|
# Always include character name
|
||||||
|
if 'special::name' not in selected_fields:
|
||||||
|
selected_fields.append('special::name')
|
||||||
|
|
||||||
|
default_fields = character.default_fields
|
||||||
|
else:
|
||||||
|
# Outfit only - no character
|
||||||
|
combined_data = {
|
||||||
|
'character_id': outfit.outfit_id,
|
||||||
|
'wardrobe': outfit.data.get('wardrobe', {}),
|
||||||
|
'lora': outfit.data.get('lora', {}),
|
||||||
|
'tags': outfit.data.get('tags', [])
|
||||||
|
}
|
||||||
|
default_fields = outfit.default_fields
|
||||||
|
|
||||||
|
# Queue generation
|
||||||
|
with open('comfy_workflow.json', 'r') as f:
|
||||||
|
workflow = json.load(f)
|
||||||
|
|
||||||
|
# Build prompts for combined data
|
||||||
|
prompts = build_prompt(combined_data, selected_fields, default_fields)
|
||||||
|
|
||||||
|
# Add colored simple background to the main prompt for outfit previews
|
||||||
|
# Use character's primary_color if available
|
||||||
|
if character:
|
||||||
|
primary_color = character.data.get('styles', {}).get('primary_color', '')
|
||||||
|
if primary_color:
|
||||||
|
prompts["main"] = f"{prompts['main']}, {primary_color} simple background"
|
||||||
|
else:
|
||||||
|
prompts["main"] = f"{prompts['main']}, simple background"
|
||||||
|
else:
|
||||||
|
prompts["main"] = f"{prompts['main']}, simple background"
|
||||||
|
|
||||||
|
# Prepare workflow - pass both character and outfit for dual LoRA support
|
||||||
|
workflow = _prepare_workflow(workflow, character, prompts, outfit=outfit)
|
||||||
|
|
||||||
|
prompt_response = queue_prompt(workflow, client_id=client_id)
|
||||||
|
|
||||||
|
if 'prompt_id' not in prompt_response:
|
||||||
|
raise Exception(f"ComfyUI failed: {prompt_response.get('error', 'Unknown error')}")
|
||||||
|
|
||||||
|
prompt_id = prompt_response['prompt_id']
|
||||||
|
|
||||||
|
# Return JSON if AJAX request
|
||||||
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
|
return {'status': 'queued', 'prompt_id': prompt_id}
|
||||||
|
|
||||||
|
return redirect(url_for('outfit_detail', slug=slug))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Generation error: {e}")
|
||||||
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
|
return {'error': str(e)}, 500
|
||||||
|
flash(f"Error during generation: {str(e)}")
|
||||||
|
return redirect(url_for('outfit_detail', slug=slug))
|
||||||
|
|
||||||
|
@app.route('/outfit/<path:slug>/finalize_generation/<prompt_id>', methods=['POST'])
|
||||||
|
def finalize_outfit_generation(slug, prompt_id):
|
||||||
|
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||||
|
action = request.form.get('action', 'preview')
|
||||||
|
|
||||||
|
try:
|
||||||
|
history = get_history(prompt_id)
|
||||||
|
if prompt_id not in history:
|
||||||
|
return {'error': 'History not found'}, 404
|
||||||
|
|
||||||
|
outputs = history[prompt_id]['outputs']
|
||||||
|
for node_id in outputs:
|
||||||
|
if 'images' in outputs[node_id]:
|
||||||
|
image_info = outputs[node_id]['images'][0]
|
||||||
|
image_data = get_image(image_info['filename'], image_info['subfolder'], image_info['type'])
|
||||||
|
|
||||||
|
# Create outfit subfolder
|
||||||
|
outfit_folder = os.path.join(app.config['UPLOAD_FOLDER'], f"outfits/{slug}")
|
||||||
|
os.makedirs(outfit_folder, exist_ok=True)
|
||||||
|
|
||||||
|
filename = f"gen_{int(time.time())}.png"
|
||||||
|
file_path = os.path.join(outfit_folder, filename)
|
||||||
|
with open(file_path, 'wb') as f:
|
||||||
|
f.write(image_data)
|
||||||
|
|
||||||
|
print(f"Image saved to: {os.path.abspath(file_path)}")
|
||||||
|
|
||||||
|
# Always save as preview
|
||||||
|
relative_path = f"outfits/{slug}/{filename}"
|
||||||
|
session[f'preview_outfit_{slug}'] = relative_path
|
||||||
|
|
||||||
|
return {'success': True, 'image_url': url_for('static', filename=f'uploads/{relative_path}')}
|
||||||
|
|
||||||
|
return {'error': 'No image found in output'}, 404
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Finalize error: {e}")
|
||||||
|
return {'error': str(e)}, 500
|
||||||
|
|
||||||
|
@app.route('/outfit/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||||
|
def replace_outfit_cover_from_preview(slug):
|
||||||
|
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||||
|
preview_path = session.get(f'preview_outfit_{slug}')
|
||||||
|
|
||||||
|
if preview_path:
|
||||||
|
outfit.image_path = preview_path
|
||||||
|
db.session.commit()
|
||||||
|
flash('Cover image updated from preview!')
|
||||||
|
else:
|
||||||
|
flash('No preview image available', 'error')
|
||||||
|
|
||||||
|
return redirect(url_for('outfit_detail', slug=slug))
|
||||||
|
|
||||||
|
@app.route('/outfit/create', methods=['GET', 'POST'])
|
||||||
|
def create_outfit():
|
||||||
|
if request.method == 'POST':
|
||||||
|
name = request.form.get('name')
|
||||||
|
slug = request.form.get('filename', '').strip()
|
||||||
|
prompt = request.form.get('prompt', '')
|
||||||
|
use_llm = request.form.get('use_llm') == 'on'
|
||||||
|
|
||||||
|
# Auto-generate slug from name if not provided
|
||||||
|
if not slug:
|
||||||
|
slug = re.sub(r'[^a-zA-Z0-9]+', '_', name.lower()).strip('_')
|
||||||
|
|
||||||
|
# Validate slug
|
||||||
|
safe_slug = re.sub(r'[^a-zA-Z0-9_]', '', slug)
|
||||||
|
if not safe_slug:
|
||||||
|
safe_slug = 'outfit'
|
||||||
|
|
||||||
|
# Find available filename (increment if exists)
|
||||||
|
base_slug = safe_slug
|
||||||
|
counter = 1
|
||||||
|
while os.path.exists(os.path.join(app.config['CLOTHING_DIR'], f"{safe_slug}.json")):
|
||||||
|
safe_slug = f"{base_slug}_{counter}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
# Check if LLM generation is requested
|
||||||
|
if use_llm:
|
||||||
|
if not prompt:
|
||||||
|
flash("Description is required when AI generation is enabled.")
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
# Generate JSON with LLM
|
||||||
|
system_prompt = """You are a JSON generator. output ONLY valid JSON matching this exact structure. Do not wrap in markdown blocks.
|
||||||
|
Structure:
|
||||||
|
{
|
||||||
|
"outfit_id": "WILL_BE_REPLACED",
|
||||||
|
"outfit_name": "WILL_BE_REPLACED",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "string (e.g. bodysuit, dress, full outfit description)",
|
||||||
|
"headwear": "string (e.g. hairband, cap)",
|
||||||
|
"top": "string (e.g. blouse, corset, jacket)",
|
||||||
|
"bottom": "string (e.g. skirt, pants, shorts)",
|
||||||
|
"legwear": "string (e.g. stockings, tights, socks)",
|
||||||
|
"footwear": "string (e.g. heels, boots, sneakers)",
|
||||||
|
"hands": "string (e.g. gloves, sleeves)",
|
||||||
|
"accessories": "string (e.g. necklace, belt, apron)"
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": ["string", "string"]
|
||||||
|
}
|
||||||
|
Fill the fields based on the user's description. Use Danbooru-style tags for the values (e.g. 'frilled skirt', 'lace stockings'). Keep values concise. Use empty strings "" for fields that are not applicable or unknown - never use words like "none" or "n/a". Leave lora fields empty - they can be configured later."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
llm_response = call_llm(f"Create an outfit profile for '{name}' based on this description: {prompt}", system_prompt)
|
||||||
|
|
||||||
|
# Clean response (remove markdown if present)
|
||||||
|
clean_json = llm_response.replace('```json', '').replace('```', '').strip()
|
||||||
|
outfit_data = json.loads(clean_json)
|
||||||
|
|
||||||
|
# Enforce IDs
|
||||||
|
outfit_data['outfit_id'] = safe_slug
|
||||||
|
outfit_data['outfit_name'] = name
|
||||||
|
|
||||||
|
# Ensure required fields exist
|
||||||
|
if 'wardrobe' not in outfit_data:
|
||||||
|
outfit_data['wardrobe'] = {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
|
"top": "",
|
||||||
|
"bottom": "",
|
||||||
|
"legwear": "",
|
||||||
|
"footwear": "",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": ""
|
||||||
|
}
|
||||||
|
if 'lora' not in outfit_data:
|
||||||
|
outfit_data['lora'] = {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
}
|
||||||
|
if 'tags' not in outfit_data:
|
||||||
|
outfit_data['tags'] = []
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"LLM error: {e}")
|
||||||
|
flash(f"Failed to generate outfit profile: {e}")
|
||||||
|
return redirect(request.url)
|
||||||
|
else:
|
||||||
|
# Create blank outfit template
|
||||||
|
outfit_data = {
|
||||||
|
"outfit_id": safe_slug,
|
||||||
|
"outfit_name": name,
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
|
"top": "",
|
||||||
|
"bottom": "",
|
||||||
|
"legwear": "",
|
||||||
|
"footwear": "",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": ""
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Save file
|
||||||
|
file_path = os.path.join(app.config['CLOTHING_DIR'], f"{safe_slug}.json")
|
||||||
|
with open(file_path, 'w') as f:
|
||||||
|
json.dump(outfit_data, f, indent=2)
|
||||||
|
|
||||||
|
# Add to DB
|
||||||
|
new_outfit = Outfit(
|
||||||
|
outfit_id=safe_slug,
|
||||||
|
slug=safe_slug,
|
||||||
|
filename=f"{safe_slug}.json",
|
||||||
|
name=name,
|
||||||
|
data=outfit_data
|
||||||
|
)
|
||||||
|
db.session.add(new_outfit)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash('Outfit created successfully!')
|
||||||
|
return redirect(url_for('outfit_detail', slug=safe_slug))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Save error: {e}")
|
||||||
|
flash(f"Failed to create outfit: {e}")
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
return render_template('outfits/create.html')
|
||||||
|
|
||||||
|
@app.route('/outfit/<path:slug>/save_defaults', methods=['POST'])
|
||||||
|
def save_outfit_defaults(slug):
|
||||||
|
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||||
|
selected_fields = request.form.getlist('include_field')
|
||||||
|
outfit.default_fields = selected_fields
|
||||||
|
db.session.commit()
|
||||||
|
flash('Default prompt selection saved for this outfit!')
|
||||||
|
return redirect(url_for('outfit_detail', slug=slug))
|
||||||
|
|
||||||
|
@app.route('/outfit/<path:slug>/clone', methods=['POST'])
|
||||||
|
def clone_outfit(slug):
|
||||||
|
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||||
|
|
||||||
|
# Find the next available number for the clone
|
||||||
|
base_id = outfit.outfit_id
|
||||||
|
# Extract base name without number suffix
|
||||||
|
import re
|
||||||
|
match = re.match(r'^(.+?)_(\d+)$', base_id)
|
||||||
|
if match:
|
||||||
|
base_name = match.group(1)
|
||||||
|
current_num = int(match.group(2))
|
||||||
|
else:
|
||||||
|
base_name = base_id
|
||||||
|
current_num = 1
|
||||||
|
|
||||||
|
# Find next available number
|
||||||
|
next_num = current_num + 1
|
||||||
|
while True:
|
||||||
|
new_id = f"{base_name}_{next_num:02d}"
|
||||||
|
new_filename = f"{new_id}.json"
|
||||||
|
new_path = os.path.join(app.config['CLOTHING_DIR'], new_filename)
|
||||||
|
if not os.path.exists(new_path):
|
||||||
|
break
|
||||||
|
next_num += 1
|
||||||
|
|
||||||
|
# Create new outfit data (copy of original)
|
||||||
|
new_data = outfit.data.copy()
|
||||||
|
new_data['outfit_id'] = new_id
|
||||||
|
new_data['outfit_name'] = f"{outfit.name} (Copy)"
|
||||||
|
|
||||||
|
# Save the new JSON file
|
||||||
|
with open(new_path, 'w') as f:
|
||||||
|
json.dump(new_data, f, indent=2)
|
||||||
|
|
||||||
|
# Create new outfit in database
|
||||||
|
new_slug = re.sub(r'[^a-zA-Z0-9_]', '', new_id)
|
||||||
|
new_outfit = Outfit(
|
||||||
|
outfit_id=new_id,
|
||||||
|
slug=new_slug,
|
||||||
|
filename=new_filename,
|
||||||
|
name=new_data['outfit_name'],
|
||||||
|
data=new_data
|
||||||
|
)
|
||||||
|
db.session.add(new_outfit)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'Outfit cloned as "{new_id}"!')
|
||||||
|
return redirect(url_for('outfit_detail', slug=new_slug))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||||
@@ -959,4 +1637,5 @@ if __name__ == '__main__':
|
|||||||
print(f"Migration note: {e}")
|
print(f"Migration note: {e}")
|
||||||
|
|
||||||
sync_characters()
|
sync_characters()
|
||||||
|
sync_outfits()
|
||||||
app.run(debug=True, port=5000)
|
app.run(debug=True, port=5000)
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
{
|
|
||||||
"character_id": "camilla_(fire_emblem)",
|
|
||||||
"character_name": "Camilla Nohr",
|
|
||||||
"identity": {
|
|
||||||
"base_specs": "1girl, curvaceous build, fair skin",
|
|
||||||
"hair": "long wavy lavender hair, hair covering one eye",
|
|
||||||
"eyes": "purple eyes",
|
|
||||||
"hands": "purple nails",
|
|
||||||
"arms": "",
|
|
||||||
"torso": "large breasts",
|
|
||||||
"pelvis": "",
|
|
||||||
"legs": "",
|
|
||||||
"feet": "",
|
|
||||||
"extra": "black headband with horns"
|
|
||||||
},
|
|
||||||
"defaults": {
|
|
||||||
"expression": "",
|
|
||||||
"pose": "",
|
|
||||||
"scene": ""
|
|
||||||
},
|
|
||||||
"wardrobe": {
|
|
||||||
"default": {
|
|
||||||
"headwear": "",
|
|
||||||
"top": "black armor, cleavage",
|
|
||||||
"legwear": "black leggings, armored plates",
|
|
||||||
"footwear": "black armored boots",
|
|
||||||
"hands": "",
|
|
||||||
"accessories": "purple cape, large axe"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"styles": {
|
|
||||||
"aesthetic": "dark fantasy, gothic, fire emblem style",
|
|
||||||
"primary_color": "black",
|
|
||||||
"secondary_color": "gold",
|
|
||||||
"tertiary_color": "purple"
|
|
||||||
},
|
|
||||||
"lora": {
|
|
||||||
"lora_name": "",
|
|
||||||
"lora_weight": 1.0,
|
|
||||||
"lora_triggers": ""
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"Fire Emblem"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -169,5 +169,15 @@
|
|||||||
"clip": ["4", 1]
|
"clip": ["4", 1]
|
||||||
},
|
},
|
||||||
"class_type": "LoraLoader"
|
"class_type": "LoraLoader"
|
||||||
|
},
|
||||||
|
"17": {
|
||||||
|
"inputs": {
|
||||||
|
"lora_name": "",
|
||||||
|
"strength_model": 0.8,
|
||||||
|
"strength_clip": 0.8,
|
||||||
|
"model": ["16", 0],
|
||||||
|
"clip": ["16", 1]
|
||||||
|
},
|
||||||
|
"class_type": "LoraLoader"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
data/actions/belly_dancing.json
Normal file
16
data/actions/belly_dancing.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"action_id": "belly_dancing",
|
||||||
|
"action_name": "Belly Dancing",
|
||||||
|
"action": {
|
||||||
|
"full_body": "belly dancing",
|
||||||
|
"head": "",
|
||||||
|
"eyes": "",
|
||||||
|
"arms": "hands above head",
|
||||||
|
"hands": "hands together",
|
||||||
|
"torso": "",
|
||||||
|
"pelvis": "shaking hips",
|
||||||
|
"legs": "",
|
||||||
|
"feet": "",
|
||||||
|
"additional": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,36 +13,40 @@
|
|||||||
"extra": "pink hair ribbon"
|
"extra": "pink hair ribbon"
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "gentle smile, looking at viewer",
|
||||||
"pose": "",
|
"pose": "handing flower to viewer",
|
||||||
"scene": ""
|
"scene": "city street, night"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "long pink dress",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "pink dress, red bolero jacket",
|
"top": "red bolero jacket",
|
||||||
"legwear": "long pink dress",
|
"bottom": "",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "brown boots",
|
"footwear": "brown boots",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "gold bracelets, flower basket"
|
"accessories": "gold bracelets, flower basket"
|
||||||
},
|
},
|
||||||
"red_dress": {
|
"red_dress": {
|
||||||
|
"full_body": "long dress, frilled dress, red dress",
|
||||||
"headwear": "red hair ribbons",
|
"headwear": "red hair ribbons",
|
||||||
"top": "long dress, frilled dress, red dress",
|
"top": "",
|
||||||
"legwear": "long dress, frilled dress, red dress",
|
"bottom": "",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "white high heels",
|
"footwear": "white high heels",
|
||||||
"hands": "red nails",
|
"hands": "red nails",
|
||||||
"accessories": "gold bracelets"
|
"accessories": "gold bracelets"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
"aesthetic": "floral, gentle, final fantasy style",
|
"aesthetic": "floral, final fantasy vii style",
|
||||||
"primary_color": "pink",
|
"primary_color": "pink",
|
||||||
"secondary_color": "red",
|
"secondary_color": "red",
|
||||||
"tertiary_color": "brown"
|
"tertiary_color": "brown"
|
||||||
},
|
},
|
||||||
"lora": {
|
"lora": {
|
||||||
"lora_name": "Illustrious/Looks/Aerith.safetensors",
|
"lora_name": "",
|
||||||
"lora_weight": 1.0,
|
"lora_weight": 1.0,
|
||||||
"lora_triggers": ""
|
"lora_triggers": ""
|
||||||
},
|
},
|
||||||
@@ -14,22 +14,24 @@
|
|||||||
"extra": ""
|
"extra": ""
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "neutral",
|
||||||
"pose": "",
|
"pose": "tucking hair behind ear",
|
||||||
"scene": ""
|
"scene": "wasteland, mountains, "
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "black long sleeved shirt, striped sleeves",
|
"full_body": "",
|
||||||
"top": "blue denim vest,",
|
"headwear": "",
|
||||||
"legwear": "blue denim skirt, black stockings",
|
"top": "blue denim vest,black long sleeved shirt, striped sleeves",
|
||||||
|
"bottom": "blue denim skirt",
|
||||||
|
"legwear": "black stockings",
|
||||||
"footwear": "brown boots",
|
"footwear": "brown boots",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "gold hoop earrings"
|
"accessories": "gold hoop earrings"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
"aesthetic": "wasteland, mountains, anime, dragon ball style",
|
"aesthetic": "anime, dragon ball style",
|
||||||
"primary_color": "blue",
|
"primary_color": "blue",
|
||||||
"secondary_color": "black",
|
"secondary_color": "black",
|
||||||
"tertiary_color": "white"
|
"tertiary_color": "white"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "black Eden Academy uniform, gold trim",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "black Eden Academy uniform, gold trim",
|
"top": "",
|
||||||
"legwear": "uniform skirt",
|
"bottom": "",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "black shoes, white socks",
|
"footwear": "black shoes, white socks",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "black and gold hair cones"
|
"accessories": "black and gold hair cones"
|
||||||
@@ -14,22 +14,24 @@
|
|||||||
"extra": ""
|
"extra": ""
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "thinking",
|
||||||
"pose": "",
|
"pose": "reading",
|
||||||
"scene": ""
|
"scene": "library, sun beam"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "white shirt",
|
"full_body": "tracen school uniform",
|
||||||
"top": "tracen school uniform",
|
"headwear": "",
|
||||||
"legwear": "pleated skirt",
|
"top": "",
|
||||||
|
"bottom": "",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "heeled shoes",
|
"footwear": "heeled shoes",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": ""
|
"accessories": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
"aesthetic": "library,intellectual,",
|
"aesthetic": "anime,umasumame",
|
||||||
"primary_color": "maroon",
|
"primary_color": "maroon",
|
||||||
"secondary_color": "white",
|
"secondary_color": "white",
|
||||||
"tertiary_color": "grey"
|
"tertiary_color": "grey"
|
||||||
@@ -20,8 +20,10 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "black playboy bunny",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "black playboy bunny",
|
"top": "",
|
||||||
|
"bottom": "",
|
||||||
"legwear": "pantyhose",
|
"legwear": "pantyhose",
|
||||||
"footwear": "red high heels",
|
"footwear": "red high heels",
|
||||||
"hands": "detatched cuffs",
|
"hands": "detatched cuffs",
|
||||||
@@ -20,10 +20,12 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "belt between breasts",
|
"full_body": "black armor, gold trim",
|
||||||
"top": "black armor, gold trim, cleavage",
|
"headwear": "",
|
||||||
"legwear": "purple sash, pelvic curtain, black panties",
|
"top": "belt between breasts, cleavage",
|
||||||
"footwear": "black armored thigh boots",
|
"bottom": "purple sash, pelvic curtain, black panties",
|
||||||
|
"legwear": "black armored thigh boots",
|
||||||
|
"footwear": "gold heels",
|
||||||
"hands": "purple velvet gloves",
|
"hands": "purple velvet gloves",
|
||||||
"accessories": "purple cape, large axe"
|
"accessories": "purple cape, large axe"
|
||||||
}
|
}
|
||||||
@@ -20,8 +20,10 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "green high-leg leotard",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "green high-leg leotard",
|
"top": "",
|
||||||
|
"bottom": "",
|
||||||
"legwear": "bare legs",
|
"legwear": "bare legs",
|
||||||
"footwear": "black combat boots, green socks",
|
"footwear": "black combat boots, green socks",
|
||||||
"hands": "red gauntlets",
|
"hands": "red gauntlets",
|
||||||
@@ -14,22 +14,24 @@
|
|||||||
"extra": ""
|
"extra": ""
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "confident",
|
||||||
"pose": "",
|
"pose": "fighting stance",
|
||||||
"scene": ""
|
"scene": "market, daytime"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "blue qipao, gold embroidery, white accents",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "blue qipao, gold embroidery, white accents, puffy shoulders",
|
"top": " puffy shoulders",
|
||||||
"legwear": "brown tights",
|
"bottom": "brown tights",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "white lace-up boots",
|
"footwear": "white lace-up boots",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "white hair ribbons, spiked bracelets"
|
"accessories": "white hair ribbons, spiked bracelets"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
"aesthetic": "chinese style, market,",
|
"aesthetic": "chinese style, ",
|
||||||
"primary_color": "blue",
|
"primary_color": "blue",
|
||||||
"secondary_color": "white",
|
"secondary_color": "white",
|
||||||
"tertiary_color": "gold"
|
"tertiary_color": "gold"
|
||||||
@@ -14,22 +14,24 @@
|
|||||||
"extra": "scar over eye"
|
"extra": "scar over eye"
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "serious",
|
||||||
"pose": "",
|
"pose": "fighting stance, holding sword",
|
||||||
"scene": ""
|
"scene": ""
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "white blouse",
|
"full_body": "",
|
||||||
"top": "",
|
"headwear": "",
|
||||||
"legwear": "brown leather trousers",
|
"top": "white blouse",
|
||||||
|
"bottom": "brown leather trousers",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "brown leather boots",
|
"footwear": "brown leather boots",
|
||||||
"hands": "brown leather gloves",
|
"hands": "brown leather gloves",
|
||||||
"accessories": "silver sword on back, witcher medallion"
|
"accessories": "silver sword on back, witcher medallion"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
"aesthetic": "gritty, fantasy, witcher style",
|
"aesthetic": "fantasy, witcher style",
|
||||||
"primary_color": "white",
|
"primary_color": "white",
|
||||||
"secondary_color": "brown",
|
"secondary_color": "brown",
|
||||||
"tertiary_color": "silver"
|
"tertiary_color": "silver"
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"base_specs": "1girl, milf, gyaru, tall",
|
"base_specs": "1girl, milf, gyaru, tall",
|
||||||
"hair": "blonde hair, long hair",
|
"hair": "blonde hair, long hair",
|
||||||
"eyes": "sharp eyes, black eyes, white pupil,",
|
"eyes": "sharp eyes, black eyes, white pupil,",
|
||||||
"hands": "painted nails",
|
"hands": "red nails",
|
||||||
"arms": "",
|
"arms": "",
|
||||||
"torso": "very large breasts",
|
"torso": "very large breasts",
|
||||||
"pelvis": "wide hips",
|
"pelvis": "wide hips",
|
||||||
@@ -14,18 +14,20 @@
|
|||||||
"extra": ""
|
"extra": ""
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "naughty face",
|
||||||
"pose": "",
|
"pose": "spread legs, leopard print panties",
|
||||||
"scene": ""
|
"scene": "sitting on couch, from below"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "cleavage",
|
"full_body": "",
|
||||||
"top": "light brown sweater, ",
|
"headwear": "",
|
||||||
"legwear": "black skirt",
|
"top": "light brown sweater, cleavage",
|
||||||
|
"bottom": "leopard print skirt",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "red high heels",
|
"footwear": "red high heels",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "necklace, rings"
|
"accessories": "necklace, rings,"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
@@ -14,22 +14,24 @@
|
|||||||
"extra": ""
|
"extra": ""
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "bored",
|
||||||
"pose": "",
|
"pose": "looking at phone",
|
||||||
"scene": ""
|
"scene": "sitting on bench"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "white shirt",
|
"full_body": "tracen school uniform",
|
||||||
"top": "tracen school uniform",
|
"headwear": "",
|
||||||
"legwear": "pleated skirt",
|
"top": "",
|
||||||
|
"bottom": "",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "heeled shoes",
|
"footwear": "heeled shoes",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "choker, earrings"
|
"accessories": "choker, earrings"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
"aesthetic": "shopping,modeling,school yard",
|
"aesthetic": "modeling,school yard",
|
||||||
"primary_color": "gold",
|
"primary_color": "gold",
|
||||||
"secondary_color": "white",
|
"secondary_color": "white",
|
||||||
"tertiary_color": "black"
|
"tertiary_color": "black"
|
||||||
@@ -14,15 +14,17 @@
|
|||||||
"extra": ""
|
"extra": ""
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "smile",
|
||||||
"pose": "",
|
"pose": "running toward viewer",
|
||||||
"scene": ""
|
"scene": "horse race track, sunshine"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "white shirt",
|
"full_body": "tracen school uniform",
|
||||||
"top": "tracen school uniform",
|
"headwear": "",
|
||||||
"legwear": "pleated skirt",
|
"top": "",
|
||||||
|
"bottom": "",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "heeled shoes",
|
"footwear": "heeled shoes",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "ear covers, hat"
|
"accessories": "ear covers, hat"
|
||||||
@@ -20,8 +20,10 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "grey sleeveless shirt, turquoise tie",
|
"top": "grey sleeveless shirt, turquoise tie",
|
||||||
|
"bottom": "",
|
||||||
"legwear": "grey miniskirt, turquoise trim",
|
"legwear": "grey miniskirt, turquoise trim",
|
||||||
"footwear": "black thigh-high boots, turquoise trim",
|
"footwear": "black thigh-high boots, turquoise trim",
|
||||||
"hands": "black arm warmers, turquoise trim",
|
"hands": "black arm warmers, turquoise trim",
|
||||||
@@ -14,22 +14,24 @@
|
|||||||
"extra": "heavy eyeliner, winged eyeliner"
|
"extra": "heavy eyeliner, winged eyeliner"
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "gentle smile",
|
||||||
"pose": "",
|
"pose": "hand in water",
|
||||||
"scene": ""
|
"scene": "sitting beside fountain, sunshine"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "teal crop top, tube top, off-shoulder, cleavage",
|
"full_body": "",
|
||||||
"top": "",
|
"headwear": "",
|
||||||
"legwear": "teal harem pants, baggy pants, sheer fabric",
|
"top": "teal crop top, tube top, off-shoulder, cleavage",
|
||||||
|
"bottom": "teal harem pants",
|
||||||
|
"legwear": " baggy pants, sheer fabric",
|
||||||
"footwear": "gold shoes, curling toes, pointed shoes",
|
"footwear": "gold shoes, curling toes, pointed shoes",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "gold hoop earrings, large gold necklace, blue headband, jewel on headband"
|
"accessories": "gold hoop earrings, large gold necklace, blue headband, jewel on headband"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
"aesthetic": "desert palace, large fountain, arabian, disney, cartoon, vibrant, ferns",
|
"aesthetic": "desert arabian, disney, cartoon, ",
|
||||||
"primary_color": "teal",
|
"primary_color": "teal",
|
||||||
"secondary_color": "gold",
|
"secondary_color": "gold",
|
||||||
"tertiary_color": "black"
|
"tertiary_color": "black"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "red sequin evening gown",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "red sequin dress, strapless, high slit, backless",
|
"top": "strapless, backless",
|
||||||
"legwear": "side slit,",
|
"bottom": "high slit",
|
||||||
|
"legwear": "side slit",
|
||||||
"footwear": "red high heels",
|
"footwear": "red high heels",
|
||||||
"hands": "purple opera gloves",
|
"hands": "purple opera gloves",
|
||||||
"accessories": "gold earrings, glitter"
|
"accessories": "gold earrings, glitter"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "black crop top",
|
"full_body": "",
|
||||||
"top": "white Team Rocket uniform jacket, bare stomach, red R logo",
|
"headwear": "",
|
||||||
"legwear": "white miniskirt",
|
"top": "black crop top,white Team Rocket uniform jacket, red R logo",
|
||||||
|
"bottom": "midriff, white miniskirt",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "black thigh-high boots",
|
"footwear": "black thigh-high boots",
|
||||||
"hands": "black elbow gloves",
|
"hands": "black elbow gloves",
|
||||||
"accessories": "green earrings"
|
"accessories": "green earrings"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "pink and black bikini, asymmetrical_bikini ",
|
"top": "pink and black bikini, asymmetrical_bikini ",
|
||||||
"legwear": "pink shorts, single pink stocking",
|
"bottom": "pink shorts",
|
||||||
|
"legwear": "single pink stocking",
|
||||||
"footwear": "combat boots",
|
"footwear": "combat boots",
|
||||||
"hands": "black fingerless gloves, fishnet elbow gloves,",
|
"hands": "black fingerless gloves, fishnet elbow gloves,",
|
||||||
"accessories": "ammo belts, choker, bullet necklace,"
|
"accessories": "ammo belts, choker, bullet necklace,"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "white shirt, sailor collar",
|
"full_body": "",
|
||||||
"top": "",
|
"headwear": "",
|
||||||
"legwear": "black shorts, yellow belt",
|
"top": "white shirt, sailor collar",
|
||||||
|
"bottom": "black shorts, yellow belt",
|
||||||
|
"legwear": "knee-high socks",
|
||||||
"footwear": "white shoes",
|
"footwear": "white shoes",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "headset, hair bow"
|
"accessories": "headset, hair bow"
|
||||||
@@ -20,10 +20,12 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "white shirt",
|
"full_body": "luna nova school uniform",
|
||||||
"top": "dark blue witch robes",
|
"headwear": "",
|
||||||
"legwear": "dark blue skirt",
|
"top": "white shirt,dark blue witch robes",
|
||||||
"footwear": "brown boots, white socks",
|
"bottom": "dark blue skirt",
|
||||||
|
"legwear": "white socks",
|
||||||
|
"footwear": "brown boots",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "pointed witch hat, brown belt, magic wand"
|
"accessories": "pointed witch hat, brown belt, magic wand"
|
||||||
}
|
}
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "silver crop top",
|
"full_body": "",
|
||||||
"top": "white and silver jacket",
|
"headwear": "",
|
||||||
"legwear": "black leather shorts",
|
"top": "silver crop top, white and silver jacket",
|
||||||
|
"bottom": "black leather shorts",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "black thigh-high boots",
|
"footwear": "black thigh-high boots",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "crystal heart, silver jewelry"
|
"accessories": "crystal heart, silver jewelry"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "black crop top",
|
"full_body": "",
|
||||||
"top": "blue and silver motorcycle jacket",
|
"headwear": "",
|
||||||
"legwear": "black leather pants",
|
"top": "black crop top, blue and silver motorcycle jacket",
|
||||||
|
"bottom": "black leather pants",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "blue sneakers",
|
"footwear": "blue sneakers",
|
||||||
"hands": "black fingerless gloves",
|
"hands": "black fingerless gloves",
|
||||||
"accessories": "kama and kunai"
|
"accessories": "kama and kunai"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "black leather bra",
|
"full_body": "",
|
||||||
"top": "iridescent blue jacket, fur collar",
|
"headwear": "",
|
||||||
"legwear": "black leather skirt",
|
"top": "black leather bra, iridescent blue jacket, fur collar",
|
||||||
|
"bottom": "black leather skirt",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "black high-heeled boots",
|
"footwear": "black high-heeled boots",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "diamond earrings"
|
"accessories": "diamond earrings"
|
||||||
@@ -20,8 +20,10 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "silver bodysuit",
|
"full_body": "silver bodysuit",
|
||||||
|
"headwear": "",
|
||||||
"top": "white and silver jacket",
|
"top": "white and silver jacket",
|
||||||
|
"bottom": "",
|
||||||
"legwear": "silver leggings",
|
"legwear": "silver leggings",
|
||||||
"footwear": "silver high-heeled boots",
|
"footwear": "silver high-heeled boots",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
@@ -14,15 +14,17 @@
|
|||||||
"extra": ""
|
"extra": ""
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "shy,",
|
||||||
"pose": "",
|
"pose": "holding notebook",
|
||||||
"scene": ""
|
"scene": "classroom"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "white shirt",
|
"full_body": "itan private high school uniform",
|
||||||
"top": "itan private high school uniform, blazer, striped bow tie",
|
"headwear": "",
|
||||||
"legwear": "plaid skirt",
|
"top": "blazer, striped bow tie, white shirt",
|
||||||
|
"bottom": "plaid skirt",
|
||||||
|
"legwear": "black pantyhose",
|
||||||
"footwear": "loafers",
|
"footwear": "loafers",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": ""
|
"accessories": ""
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "teal tank top,",
|
"top": "teal tank top,",
|
||||||
"legwear": "brown shorts",
|
"bottom": "brown shorts",
|
||||||
|
"legwear": "thigh holsters",
|
||||||
"footwear": "brown combat boots, red laces",
|
"footwear": "brown combat boots, red laces",
|
||||||
"hands": "black fingerless gloves",
|
"hands": "black fingerless gloves",
|
||||||
"accessories": "dual thigh pistol holsters, brown leatherbackpack, red round sunglasses"
|
"accessories": "dual thigh pistol holsters, brown leatherbackpack, red round sunglasses"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "",
|
||||||
"headwear": "purple dress, corset",
|
"headwear": "purple dress, corset",
|
||||||
"top": "purple shawl",
|
"top": "purple shawl",
|
||||||
"legwear": "slit skirt",
|
"bottom": "slit skirt",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "black heels",
|
"footwear": "black heels",
|
||||||
"hands": "purple gloves",
|
"hands": "purple gloves",
|
||||||
"accessories": "witch hat, rose, necklace"
|
"accessories": "witch hat, rose, necklace"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "black corset",
|
"full_body": "",
|
||||||
"top": "black fur-trimmed dress, many belts on front",
|
"headwear": "",
|
||||||
"legwear": "long skirt made of belts",
|
"top": "black fur-trimmed dress, many belts on front, black corset",
|
||||||
|
"bottom": "long skirt made of belts",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "black boots",
|
"footwear": "black boots",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "moogle doll, silver jewelry"
|
"accessories": "moogle doll, silver jewelry"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "black tube top",
|
"full_body": "",
|
||||||
"top": "",
|
"headwear": "",
|
||||||
"legwear": "white harem pants",
|
"top": "black tube top",
|
||||||
|
"bottom": "white harem pants",
|
||||||
|
"legwear": "baggy pants",
|
||||||
"footwear": "black and yellow boots",
|
"footwear": "black and yellow boots",
|
||||||
"hands": "black sleeves",
|
"hands": "black sleeves",
|
||||||
"accessories": "gold bracelets, gold neck ring, hoop earrings,pink donut"
|
"accessories": "gold bracelets, gold neck ring, hoop earrings,pink donut"
|
||||||
@@ -14,22 +14,34 @@
|
|||||||
"extra": "piercings"
|
"extra": "piercings"
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "happy",
|
||||||
"pose": "",
|
"pose": "v",
|
||||||
"scene": ""
|
"scene": "sewing room"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "black bikini with yellow flower print",
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
"top": "white school shirt, loosely tied blue tie",
|
"top": "white school shirt, loosely tied blue tie",
|
||||||
|
"bottom": "",
|
||||||
"legwear": "blue plaid miniskirt",
|
"legwear": "blue plaid miniskirt",
|
||||||
"footwear": "black loafers, black socks",
|
"footwear": "black loafers, black socks",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "choker, colored bracelets"
|
"accessories": "choker, colored bracelets"
|
||||||
|
},
|
||||||
|
"bikini": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
|
"top": "black bikini with yellow flower print",
|
||||||
|
"bottom": "",
|
||||||
|
"legwear": "",
|
||||||
|
"footwear": "barefoot",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": "choker, colored bracelets"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
"aesthetic": "gyaru, modern, anime style, sewing machine",
|
"aesthetic": "gyaru, modern, anime style,",
|
||||||
"primary_color": "white",
|
"primary_color": "white",
|
||||||
"secondary_color": "blue",
|
"secondary_color": "blue",
|
||||||
"tertiary_color": "pink"
|
"tertiary_color": "pink"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "crop top, detached sleeves, gold trim",
|
"top": "crop top, detached sleeves, gold trim",
|
||||||
"legwear": "side slit, lace-up skirt",
|
"bottom": "lace-up skirt",
|
||||||
|
"legwear": "side slit",
|
||||||
"footwear": "thinghighs, lace-up boots, gold boots, gold armlet",
|
"footwear": "thinghighs, lace-up boots, gold boots, gold armlet",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "headset"
|
"accessories": "headset"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "",
|
||||||
"headwear": "red crop top, sleeveless",
|
"headwear": "red crop top, sleeveless",
|
||||||
"top": "",
|
"top": "",
|
||||||
"legwear": "red skirt, mini skirt",
|
"bottom": "red skirt, mini skirt",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "brown boots",
|
"footwear": "brown boots",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "choker"
|
"accessories": "choker"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "white crop top, blue trim",
|
"full_body": "blue trim",
|
||||||
"top": "gym uniform, number '049'",
|
"headwear": "",
|
||||||
"legwear": "midriff, white and blue shorts, black trim",
|
"top": "white crop top, gym uniform, number '049'",
|
||||||
|
"bottom": "midriff,white and blue shorts, black trim",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "white and blue sandals, orange trim",
|
"footwear": "white and blue sandals, orange trim",
|
||||||
"hands": "fingerless gloves",
|
"hands": "fingerless gloves",
|
||||||
"accessories": "wristband, small life buoy, pokeball, gold hoop earrings"
|
"accessories": "wristband, small life buoy, pokeball, gold hoop earrings"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "black shirt",
|
"full_body": "",
|
||||||
"top": "blue military coat, fur collar",
|
"headwear": "",
|
||||||
"legwear": "black pants",
|
"top": "blue military coat, fur collar, black shirt",
|
||||||
|
"bottom": "black pants",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "black boots",
|
"footwear": "black boots",
|
||||||
"hands": "black gloves",
|
"hands": "black gloves",
|
||||||
"accessories": "sword"
|
"accessories": "sword"
|
||||||
@@ -14,15 +14,17 @@
|
|||||||
"extra": "pink lips, blue earrings"
|
"extra": "pink lips, blue earrings"
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "smile",
|
||||||
"pose": "",
|
"pose": "hands together",
|
||||||
"scene": ""
|
"scene": "throne room"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "white petticoat",
|
"full_body": "pink ball gown",
|
||||||
"top": "pink floor-length ball gown, puffy sleeves, dark pink panniers",
|
"headwear": "gold crown",
|
||||||
"legwear": "long skirt",
|
"top": "white petticoat, puffy sleeves, dark pink panniers",
|
||||||
|
"bottom": "",
|
||||||
|
"legwear": "floor length skirt",
|
||||||
"footwear": "red high heels",
|
"footwear": "red high heels",
|
||||||
"hands": "white opera gloves",
|
"hands": "white opera gloves",
|
||||||
"accessories": "gold crown with red and blue jewels, blue brooch"
|
"accessories": "gold crown with red and blue jewels, blue brooch"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "blue tunic",
|
"full_body": "",
|
||||||
"top": "blue champion's tunic, brown leather belts",
|
"headwear": "",
|
||||||
"legwear": "tan trousers",
|
"top": "blue champion's tunic, ",
|
||||||
|
"bottom": "brown leather belts, tan trousers",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "brown leather boots",
|
"footwear": "brown leather boots",
|
||||||
"hands": "brown fingerless gloves",
|
"hands": "brown fingerless gloves",
|
||||||
"accessories": "sheikah slate, gold jewelry"
|
"accessories": "sheikah slate, gold jewelry"
|
||||||
@@ -20,12 +20,14 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "white shirt",
|
"full_body": "tracen school uniform",
|
||||||
"top": "tracen school uniform",
|
"headwear": "hair flower, small hat",
|
||||||
"legwear": "pleated skirt",
|
"top": "white shirt",
|
||||||
|
"bottom": "pleated skirt",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "heeled shoes",
|
"footwear": "heeled shoes",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "blue rose, hair flower, small hat,"
|
"accessories": "blue rose"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "black top, gold trim",
|
"top": "black top, gold trim",
|
||||||
"legwear": "black sarong, pelvic curtain,",
|
"bottom": "black sarong, pelvic curtain",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "black high heels, gold trim",
|
"footwear": "black high heels, gold trim",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "gold jewelry, earrings,"
|
"accessories": "gold jewelry, earrings,"
|
||||||
@@ -20,8 +20,10 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "",
|
"full_body": "turquoise gown, silver trim",
|
||||||
"top": "turquoise off-the-shoulder gown, silver trim",
|
"headwear": "silver crown",
|
||||||
|
"top": "bare shoulders",
|
||||||
|
"bottom": "",
|
||||||
"legwear": "long skirt",
|
"legwear": "long skirt",
|
||||||
"footwear": "silver high heels",
|
"footwear": "silver high heels",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"identity": {
|
"identity": {
|
||||||
"base_specs": "1girl, anthro, bat girl, white fur",
|
"base_specs": "1girl, anthro, bat girl, white fur",
|
||||||
"hair": "short white hair",
|
"hair": "short white hair",
|
||||||
"eyes": "teal eyes",
|
"eyes": "teal eyes, blue eyeshadow",
|
||||||
"hands": "white gloves",
|
"hands": "white gloves",
|
||||||
"arms": "",
|
"arms": "",
|
||||||
"torso": "large breasts",
|
"torso": "large breasts",
|
||||||
@@ -20,12 +20,14 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "black skin-tight jumpsuit",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "black skin-tight jumpsuit, pink heart-shaped chest plate, bare shoulders, cleavage",
|
"top": "pink heart-shaped chest plate, bare shoulders, cleavage",
|
||||||
"legwear": "jumpsuit",
|
"bottom": "",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "white boots, pink heart motifs",
|
"footwear": "white boots, pink heart motifs",
|
||||||
"hands": "white gloves, pink cuffs",
|
"hands": "white gloves, pink cuffs",
|
||||||
"accessories": "blue eyeshadow"
|
"accessories": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
@@ -20,12 +20,14 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "long white dress, plunging neckline, black belt",
|
"full_body": "long white dress",
|
||||||
"top": "black and orange long sleeve jacket with purple trim,",
|
"headwear": "",
|
||||||
"legwear": "side_slit,, red trousers",
|
"top": "black and orange long sleeve jacket with purple trim, plunging neckline",
|
||||||
|
"bottom": "black belt, red trousers",
|
||||||
|
"legwear": "side slit",
|
||||||
"footwear": "",
|
"footwear": "",
|
||||||
"hands": "red gloves",
|
"hands": "red gloves, red gem on back of hand",
|
||||||
"accessories": "red gems, wristbands"
|
"accessories": "wristbands"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
@@ -20,8 +20,10 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "green bodysuit, catsuit, skin tight",
|
"full_body": "green bodysuit, catsuit, skin tight",
|
||||||
|
"headwear": "",
|
||||||
"top": "",
|
"top": "",
|
||||||
|
"bottom": "",
|
||||||
"legwear": "",
|
"legwear": "",
|
||||||
"footwear": "heels ",
|
"footwear": "heels ",
|
||||||
"hands": "green bodysuit",
|
"hands": "green bodysuit",
|
||||||
@@ -20,10 +20,12 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "blue skin-tight bodysuit, zero suit, pink lines",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "blue skin-tight bodysuit, pink symbols",
|
"top": "",
|
||||||
"legwear": "bodysuit",
|
"bottom": "",
|
||||||
"footwear": "blue high-heeled boots",
|
"legwear": "",
|
||||||
|
"footwear": "metal high-heeled boots, yellow glow",
|
||||||
"hands": "zero suit",
|
"hands": "zero suit",
|
||||||
"accessories": "paralyzer pistol"
|
"accessories": "paralyzer pistol"
|
||||||
}
|
}
|
||||||
@@ -14,29 +14,31 @@
|
|||||||
"extra": ""
|
"extra": ""
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "smile",
|
||||||
"pose": "",
|
"pose": "leaning back",
|
||||||
"scene": ""
|
"scene": "living room, couch, low lighting"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "grey t-shirt, white shirt",
|
"full_body": "",
|
||||||
|
"headwear": "grey t-shirt,",
|
||||||
"top": "",
|
"top": "",
|
||||||
"legwear": "blue jeans",
|
"bottom": "",
|
||||||
"footwear": "sneakers",
|
"legwear": "pajama pants",
|
||||||
|
"footwear": "barefoot",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "wristwatch"
|
"accessories": "wristwatch"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
"aesthetic": "casual, 2013 fashion, living room",
|
"aesthetic": "casual, 2013 fashion,",
|
||||||
"primary_color": "grey",
|
"primary_color": "grey",
|
||||||
"secondary_color": "blue",
|
"secondary_color": "blue",
|
||||||
"tertiary_color": "white"
|
"tertiary_color": "white"
|
||||||
},
|
},
|
||||||
"lora": {
|
"lora": {
|
||||||
"lora_name": "Illustrious/Looks/Sarah_Miller_Illustrious.safetensors",
|
"lora_name": "Illustrious/Looks/SarahMillerOGILf_1328931.safetensors",
|
||||||
"lora_weight": 0.8,
|
"lora_weight": 0.6,
|
||||||
"lora_triggers": ""
|
"lora_triggers": ""
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -14,15 +14,17 @@
|
|||||||
"extra": "red lipstick, heavy makeup"
|
"extra": "red lipstick, heavy makeup"
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "smirk",
|
||||||
"pose": "",
|
"pose": "stepping on viewer, from below",
|
||||||
"scene": ""
|
"scene": "penthouse office, night"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "red dress, formal dress, pencil dress",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "red dress, formal dress, pencil dress, sleeveless, chest cutout",
|
"top": "sleeveless, chest cutout",
|
||||||
"legwear": "long skirt, high slit, side slit,black stockings",
|
"bottom": "high slit",
|
||||||
|
"legwear": "side slit, black stockings",
|
||||||
"footwear": "high heels, red heels, stiletto heels",
|
"footwear": "high heels, red heels, stiletto heels",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "jewelry, gold earrings, necklace"
|
"accessories": "jewelry, gold earrings, necklace"
|
||||||
@@ -14,14 +14,16 @@
|
|||||||
"extra": ""
|
"extra": ""
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "smile",
|
||||||
"pose": "",
|
"pose": "belly dancing, hands above head",
|
||||||
"scene": ""
|
"scene": "desert town, oasis"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "gold trim",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "red bikini top, red harem pants, gold trim",
|
"top": "red bikini top",
|
||||||
|
"bottom": "red harem pants",
|
||||||
"legwear": "",
|
"legwear": "",
|
||||||
"footwear": "gold shoes",
|
"footwear": "gold shoes",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
@@ -20,10 +20,12 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "white top",
|
"full_body": "",
|
||||||
"top": "black corset,, clothing cutout, low cut, witch hat",
|
"headwear": "witch hat",
|
||||||
"legwear": "skirt, high slit, side slit",
|
"top": "black corset, white top, clothing cutout, low cut",
|
||||||
"footwear": "boots, ",
|
"bottom": "skirt, high slit, black skirt",
|
||||||
|
"legwear": "side slit",
|
||||||
|
"footwear": "boots",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "staff, necklace, bracelets, jewelry, wooden staff"
|
"accessories": "staff, necklace, bracelets, jewelry, wooden staff"
|
||||||
}
|
}
|
||||||
@@ -14,22 +14,24 @@
|
|||||||
"extra": "dark circles under eyes"
|
"extra": "dark circles under eyes"
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "neutral expression",
|
||||||
"pose": "",
|
"pose": "looking at mushroom",
|
||||||
"scene": ""
|
"scene": "dark forest"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "",
|
"full_body": "luna nova school uniform, dark purple witch robes",
|
||||||
"top": "dark purple witch robes",
|
"headwear": "pointed witch hat",
|
||||||
|
"top": "oversized sleeves",
|
||||||
|
"bottom": "",
|
||||||
"legwear": "long skirt with frayed edges",
|
"legwear": "long skirt with frayed edges",
|
||||||
"footwear": "brown boots",
|
"footwear": "brown boots",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "pointed witch hat, potion bottle"
|
"accessories": "potion bottle, mushroom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
"aesthetic": "mushroom, gothic, whimsical, little witch academia style",
|
"aesthetic": "gothic, magic, little witch academia style",
|
||||||
"primary_color": "purple",
|
"primary_color": "purple",
|
||||||
"secondary_color": "mauve",
|
"secondary_color": "mauve",
|
||||||
"tertiary_color": "green"
|
"tertiary_color": "green"
|
||||||
@@ -14,16 +14,18 @@
|
|||||||
"extra": ""
|
"extra": ""
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "smile",
|
||||||
"pose": "",
|
"pose": "leaning on counter, looking at viewer",
|
||||||
"scene": ""
|
"scene": "tavern, bartending"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "black sports bra",
|
"full_body": "",
|
||||||
"top": "white tank top, black suspenders",
|
"headwear": "",
|
||||||
"legwear": "black miniskirt",
|
"top": "white tank top, black sports bra,black suspenders",
|
||||||
"footwear": "red boots, thigh high black socks",
|
"bottom": "black miniskirt",
|
||||||
|
"legwear": "thigh high black socks",
|
||||||
|
"footwear": "red boots",
|
||||||
"hands": "red fingerless gloves",
|
"hands": "red fingerless gloves",
|
||||||
"accessories": "silver earrings"
|
"accessories": "silver earrings"
|
||||||
}
|
}
|
||||||
@@ -14,17 +14,19 @@
|
|||||||
"extra": "freckles"
|
"extra": "freckles"
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "smile",
|
||||||
"pose": "",
|
"pose": "dashing, blue afterimage",
|
||||||
"scene": ""
|
"scene": "futuristic city"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "orange leggings",
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
"top": "brown flight jacket, yellow vest",
|
"top": "brown flight jacket, yellow vest",
|
||||||
"legwear": "orange leggings",
|
"bottom": "orange leggings",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "white and orange sneakers",
|
"footwear": "white and orange sneakers",
|
||||||
"hands": "",
|
"hands": "fingerless gloves",
|
||||||
"accessories": "chronal accelerator, yellow goggles"
|
"accessories": "chronal accelerator, yellow goggles"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -20,8 +20,10 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "blue top, green sash, green shoulder guards,",
|
"top": "blue top, green sash, green shoulder guards,",
|
||||||
|
"bottom": "",
|
||||||
"legwear": "blue sarong",
|
"legwear": "blue sarong",
|
||||||
"footwear": "anklet, gold heels",
|
"footwear": "anklet, gold heels",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
@@ -14,15 +14,17 @@
|
|||||||
"extra": ""
|
"extra": ""
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "light smile",
|
||||||
"pose": "",
|
"pose": "sniping, prone",
|
||||||
"scene": ""
|
"scene": "rooftop, night"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "purple bodysuit, skintight",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "purple bodysuit, plunging neckline",
|
"top": " plunging neckline",
|
||||||
"legwear": "bodysuit",
|
"bottom": "",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "purple high-heeled boots",
|
"footwear": "purple high-heeled boots",
|
||||||
"hands": "purple gauntlets",
|
"hands": "purple gauntlets",
|
||||||
"accessories": "sniper rifle, visor"
|
"accessories": "sniper rifle, visor"
|
||||||
@@ -14,14 +14,16 @@
|
|||||||
"extra": ""
|
"extra": ""
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "serious",
|
||||||
"pose": "",
|
"pose": "leaping",
|
||||||
"scene": ""
|
"scene": "rooftop, night, red sky, blood splatter"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "black halter dress",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "black backless halter dress, red rose pattern inside",
|
"top": "backless, pattern inside",
|
||||||
|
"bottom": "",
|
||||||
"legwear": "black thigh-high boots",
|
"legwear": "black thigh-high boots",
|
||||||
"footwear": "black boots",
|
"footwear": "black boots",
|
||||||
"hands": "black fingerless gloves",
|
"hands": "black fingerless gloves",
|
||||||
@@ -29,7 +31,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
"aesthetic": "elegant, assassin, spy x family style",
|
"aesthetic": "elegant, assassin,red rose, spy x family style",
|
||||||
"primary_color": "black",
|
"primary_color": "black",
|
||||||
"secondary_color": "red",
|
"secondary_color": "red",
|
||||||
"tertiary_color": "gold"
|
"tertiary_color": "gold"
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "black sorceress robes, ",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "black sorceress robes, fur trim",
|
"top": "bare shoulders, fur trim",
|
||||||
"legwear": "long skirt",
|
"bottom": "",
|
||||||
|
"legwear": "",
|
||||||
"footwear": "black boots",
|
"footwear": "black boots",
|
||||||
"hands": "",
|
"hands": "",
|
||||||
"accessories": "wooden staff"
|
"accessories": "wooden staff"
|
||||||
@@ -14,22 +14,24 @@
|
|||||||
"extra": "headband"
|
"extra": "headband"
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "cheeky smile",
|
||||||
"pose": "",
|
"pose": "holding glass orb, materia",
|
||||||
"scene": ""
|
"scene": "forest, sunlight"
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
|
"full_body": "",
|
||||||
"headwear": "",
|
"headwear": "",
|
||||||
"top": "green turtleneck sweater vest, midriff",
|
"top": "green turtleneck sweater vest, midriff",
|
||||||
"legwear": "beige shorts",
|
"bottom": "beige shorts",
|
||||||
"footwear": "boots, socks",
|
"legwear": "single kneehigh sock",
|
||||||
|
"footwear": "boots, ",
|
||||||
"hands": "fingerless glove on one hand, large gauntlet on one arm",
|
"hands": "fingerless glove on one hand, large gauntlet on one arm",
|
||||||
"accessories": "shuriken"
|
"accessories": "shuriken"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
"aesthetic": "ninja, adventurer, final fantasy style",
|
"aesthetic": "ninja, japanese, final fantasy style",
|
||||||
"primary_color": "green",
|
"primary_color": "green",
|
||||||
"secondary_color": "beige",
|
"secondary_color": "beige",
|
||||||
"tertiary_color": "black"
|
"tertiary_color": "black"
|
||||||
@@ -14,22 +14,24 @@
|
|||||||
"extra": ""
|
"extra": ""
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"expression": "",
|
"expression": "serene",
|
||||||
"pose": "",
|
"pose": "dancing",
|
||||||
"scene": ""
|
"scene": "standing on water, sunset, pink sky, "
|
||||||
},
|
},
|
||||||
"wardrobe": {
|
"wardrobe": {
|
||||||
"default": {
|
"default": {
|
||||||
"headwear": "white kimono top, yellow obi",
|
"full_body": "",
|
||||||
"top": "",
|
"headwear": "",
|
||||||
"legwear": "long blue skirt, floral pattern",
|
"top": "white kimono top, yellow obi, detached sleeves",
|
||||||
"footwear": "boots",
|
"bottom": "long blue skirt, floral pattern",
|
||||||
"hands": "detached sleeves",
|
"legwear": "",
|
||||||
|
"footwear": "black boots",
|
||||||
|
"hands": "",
|
||||||
"accessories": "summoner staff, necklace"
|
"accessories": "summoner staff, necklace"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
"aesthetic": "sunset, pink sky, shrine maiden,fantasy, final fantasy x style",
|
"aesthetic": "shrine maiden,fantasy, final fantasy x style",
|
||||||
"primary_color": "white",
|
"primary_color": "white",
|
||||||
"secondary_color": "blue",
|
"secondary_color": "blue",
|
||||||
"tertiary_color": "yellow"
|
"tertiary_color": "yellow"
|
||||||
26
data/clothing/bikini_01.json
Normal file
26
data/clothing/bikini_01.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "bikini_01",
|
||||||
|
"outfit_name": "Bikini",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
|
"top": "bikini top",
|
||||||
|
"bottom": "bikini bottom",
|
||||||
|
"legwear": "",
|
||||||
|
"footwear": "barefoot",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": ""
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"bikini",
|
||||||
|
"swimsuit",
|
||||||
|
"navel",
|
||||||
|
"cleavage",
|
||||||
|
"summer"
|
||||||
|
]
|
||||||
|
}
|
||||||
26
data/clothing/bikini_02.json
Normal file
26
data/clothing/bikini_02.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "bikini_02",
|
||||||
|
"outfit_name": "Bikini (Slingshot)",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
|
"top": "slingshot swimsuit",
|
||||||
|
"bottom": "",
|
||||||
|
"legwear": "",
|
||||||
|
"footwear": "",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": ""
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"slingshot swimsuit",
|
||||||
|
"swimsuit",
|
||||||
|
"highleg",
|
||||||
|
"navel",
|
||||||
|
"revealing clothes"
|
||||||
|
]
|
||||||
|
}
|
||||||
27
data/clothing/cat_cosplay.json
Normal file
27
data/clothing/cat_cosplay.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "cat_cosplay",
|
||||||
|
"outfit_name": "Cat Cosplay",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "cat ears, nekomimi, hairband",
|
||||||
|
"top": "bikini top, string bikini",
|
||||||
|
"bottom": "bikini bottom, panties",
|
||||||
|
"legwear": "thighhighs",
|
||||||
|
"footwear": "high heels",
|
||||||
|
"hands": "paw gloves, animal hands",
|
||||||
|
"accessories": "collar, bell choker, cat tail"
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"cat girl",
|
||||||
|
"nekomimi",
|
||||||
|
"bikini",
|
||||||
|
"cosplay",
|
||||||
|
"animal ears",
|
||||||
|
"tail"
|
||||||
|
]
|
||||||
|
}
|
||||||
25
data/clothing/evening_gown.json
Normal file
25
data/clothing/evening_gown.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "evening_gown",
|
||||||
|
"outfit_name": "Evening Gown",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
|
"top": "evening gown",
|
||||||
|
"bottom": "",
|
||||||
|
"legwear": "",
|
||||||
|
"footwear": "high heels",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": ""
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"formal",
|
||||||
|
"elegant",
|
||||||
|
"long dress",
|
||||||
|
"flowing"
|
||||||
|
]
|
||||||
|
}
|
||||||
22
data/clothing/french_maid_01.json
Normal file
22
data/clothing/french_maid_01.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "french_maid_01",
|
||||||
|
"outfit_name": "French Maid",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "hairband",
|
||||||
|
"top": "corset, low cut top",
|
||||||
|
"bottom": "frilled skirt",
|
||||||
|
"legwear": "lace stockings",
|
||||||
|
"footwear": "heels",
|
||||||
|
"hands": "frilled sleeves",
|
||||||
|
"accessories": "apron"
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "Illustrious/Clothing/V2_Latex_Maid_Illustrious.safetensors",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"French Maid"
|
||||||
|
]
|
||||||
|
}
|
||||||
23
data/clothing/french_maid_02.json
Normal file
23
data/clothing/french_maid_02.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "french_maid_02",
|
||||||
|
"outfit_name": "French Maid (Latex)",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "hairband",
|
||||||
|
"top": "corset, low cut top",
|
||||||
|
"bottom": "frilled skirt",
|
||||||
|
"legwear": "lace stockings",
|
||||||
|
"footwear": "heels",
|
||||||
|
"hands": "frilled sleeves",
|
||||||
|
"accessories": "apron"
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "Illustrious/Clothing/V2_Latex_Maid_Illustrious.safetensors",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"French Maid",
|
||||||
|
"latex"
|
||||||
|
]
|
||||||
|
}
|
||||||
26
data/clothing/latex_outfit.json
Normal file
26
data/clothing/latex_outfit.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "latex_outfit",
|
||||||
|
"outfit_name": "Latex Outfit",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "latex cat ears",
|
||||||
|
"top": "latex bodysuit, breast cutout, zipper, sleeveless, highleg",
|
||||||
|
"bottom": "",
|
||||||
|
"legwear": "latex thighhighs, garter belt",
|
||||||
|
"footwear": "thigh boots, high heels, latex boots",
|
||||||
|
"hands": "elbow gloves, latex gloves",
|
||||||
|
"accessories": "choker, collar"
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "Illustrious/Clothing/latex_clothing.safetensors",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"black latex",
|
||||||
|
"shiny",
|
||||||
|
"fetish",
|
||||||
|
"tight clothing",
|
||||||
|
"erotic"
|
||||||
|
]
|
||||||
|
}
|
||||||
26
data/clothing/lingerie.json
Normal file
26
data/clothing/lingerie.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "lingerie",
|
||||||
|
"outfit_name": "Lingerie",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
|
"top": "lace bra",
|
||||||
|
"bottom": "lace panties",
|
||||||
|
"legwear": "lace stockings",
|
||||||
|
"footwear": "",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": ""
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"lingerie",
|
||||||
|
"lace",
|
||||||
|
"lace trim",
|
||||||
|
"underwear",
|
||||||
|
"matching set"
|
||||||
|
]
|
||||||
|
}
|
||||||
26
data/clothing/lingerie_02.json
Normal file
26
data/clothing/lingerie_02.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "lingerie_02",
|
||||||
|
"outfit_name": "Lingerie (Latex)",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
|
"top": "latex bra, latex choker",
|
||||||
|
"bottom": "latex panties",
|
||||||
|
"legwear": "latex stockings",
|
||||||
|
"footwear": "",
|
||||||
|
"hands": "latex elbow gloves",
|
||||||
|
"accessories": ""
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"lingerie",
|
||||||
|
"lace",
|
||||||
|
"lace trim",
|
||||||
|
"underwear",
|
||||||
|
"matching set"
|
||||||
|
]
|
||||||
|
}
|
||||||
27
data/clothing/mother.json
Normal file
27
data/clothing/mother.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "mother",
|
||||||
|
"outfit_name": "Mother",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
|
"top": "knit sweater, turtleneck, sweater, long sleeves",
|
||||||
|
"bottom": "long skirt, maxi skirt",
|
||||||
|
"legwear": "pantyhose",
|
||||||
|
"footwear": "flat shoes, slippers",
|
||||||
|
"hands": "wedding ring",
|
||||||
|
"accessories": "apron, necklace"
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"milf",
|
||||||
|
"mother",
|
||||||
|
"mature female",
|
||||||
|
"casual",
|
||||||
|
"domestic",
|
||||||
|
"knitwear"
|
||||||
|
]
|
||||||
|
}
|
||||||
22
data/clothing/nurse_01.json
Normal file
22
data/clothing/nurse_01.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "nurse_01",
|
||||||
|
"outfit_name": "Nurse",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "nurse cap",
|
||||||
|
"top": "nurse outfit",
|
||||||
|
"bottom": "short skirt",
|
||||||
|
"legwear": "stockings",
|
||||||
|
"footwear": "high heels",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": ""
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"nurse"
|
||||||
|
]
|
||||||
|
}
|
||||||
23
data/clothing/nurse_02.json
Normal file
23
data/clothing/nurse_02.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "nurse_02",
|
||||||
|
"outfit_name": "Nurse (Latex)",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "nurse cap",
|
||||||
|
"top": "latex nurse outfit",
|
||||||
|
"bottom": "short skirt",
|
||||||
|
"legwear": "stockings",
|
||||||
|
"footwear": "high heels",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": ""
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"nurse",
|
||||||
|
"latex"
|
||||||
|
]
|
||||||
|
}
|
||||||
20
data/clothing/pasties_01.json
Normal file
20
data/clothing/pasties_01.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "pasties_01",
|
||||||
|
"outfit_name": "Pasties",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
|
"top": "nipple pasties",
|
||||||
|
"bottom": "crotch pasties",
|
||||||
|
"legwear": "",
|
||||||
|
"footwear": "",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": ""
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
32
data/clothing/playboy_bunny.json
Normal file
32
data/clothing/playboy_bunny.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "playboy_bunny",
|
||||||
|
"outfit_name": "Playboy Bunny",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "playboy bunny, leotard, strapless leotard",
|
||||||
|
"headwear": "bunny ears, animal ears, headband",
|
||||||
|
"top": "",
|
||||||
|
"bottom": "",
|
||||||
|
"legwear": "pantyhose, fishnets, black pantyhose",
|
||||||
|
"footwear": "high heels",
|
||||||
|
"hands": "cuffs, wrist cuffs, white cuffs",
|
||||||
|
"accessories": "collar, bowtie, bunny tail"
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"playboy bunny",
|
||||||
|
"bunny ears",
|
||||||
|
"leotard",
|
||||||
|
"fishnets",
|
||||||
|
"collar",
|
||||||
|
"bowtie",
|
||||||
|
"cuffs",
|
||||||
|
"bunny tail",
|
||||||
|
"pantyhose",
|
||||||
|
"high heels",
|
||||||
|
"animal ears"
|
||||||
|
]
|
||||||
|
}
|
||||||
20
data/clothing/school_uniform_01.json
Normal file
20
data/clothing/school_uniform_01.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "school_uniform_01",
|
||||||
|
"outfit_name": "School Uniform (Western)",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "hairband",
|
||||||
|
"top": "white shirt, tie, blazer",
|
||||||
|
"bottom": "skirt",
|
||||||
|
"legwear": "thigh high socks",
|
||||||
|
"footwear": "black shoes",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": ""
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
23
data/clothing/school_uniform_02.json
Normal file
23
data/clothing/school_uniform_02.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "school_uniform_02",
|
||||||
|
"outfit_name": "School Uniform (Sailor)",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "hairband",
|
||||||
|
"top": "white shirt, sailor scarf",
|
||||||
|
"bottom": "pleated skirt",
|
||||||
|
"legwear": "knee high socks",
|
||||||
|
"footwear": "black shoes",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": ""
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"school uniform",
|
||||||
|
"sailor"
|
||||||
|
]
|
||||||
|
}
|
||||||
27
data/clothing/school_uniform_03.json
Normal file
27
data/clothing/school_uniform_03.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "school_uniform_03",
|
||||||
|
"outfit_name": "School Uniform (Latex Sailor)",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
|
"top": "latex serafuku, sailor collar, cropped shirt, midriff, short sleeves",
|
||||||
|
"bottom": "pleated skirt, miniskirt, latex skirt",
|
||||||
|
"legwear": "latex thighhighs",
|
||||||
|
"footwear": "high heels",
|
||||||
|
"hands": "",
|
||||||
|
"accessories": "neckerchief"
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"latex",
|
||||||
|
"school uniform",
|
||||||
|
"serafuku",
|
||||||
|
"shiny",
|
||||||
|
"midriff",
|
||||||
|
"navel"
|
||||||
|
]
|
||||||
|
}
|
||||||
20
data/clothing/school_uniform_04.json
Normal file
20
data/clothing/school_uniform_04.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"outfit_id": "school_uniform_04",
|
||||||
|
"outfit_name": "School Uniform (Erotic)",
|
||||||
|
"wardrobe": {
|
||||||
|
"full_body": "",
|
||||||
|
"headwear": "",
|
||||||
|
"top": "topless",
|
||||||
|
"bottom": "latex skirt, micro-skirt",
|
||||||
|
"legwear": "latex thigh highs",
|
||||||
|
"footwear": "high heels",
|
||||||
|
"hands": "latex elbow gloves",
|
||||||
|
"accessories": ""
|
||||||
|
},
|
||||||
|
"lora": {
|
||||||
|
"lora_name": "",
|
||||||
|
"lora_weight": 0.8,
|
||||||
|
"lora_triggers": ""
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
13
models.py
13
models.py
@@ -34,6 +34,19 @@ class Character(db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Character {self.character_id}>'
|
return f'<Character {self.character_id}>'
|
||||||
|
|
||||||
|
class Outfit(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
outfit_id = db.Column(db.String(100), unique=True, nullable=False)
|
||||||
|
slug = db.Column(db.String(100), unique=True, nullable=False)
|
||||||
|
filename = db.Column(db.String(255), nullable=True)
|
||||||
|
name = db.Column(db.String(100), nullable=False)
|
||||||
|
data = db.Column(db.JSON, nullable=False)
|
||||||
|
default_fields = db.Column(db.JSON, nullable=True)
|
||||||
|
image_path = db.Column(db.String(255), nullable=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Outfit {self.outfit_id}>'
|
||||||
|
|
||||||
class Settings(db.Model):
|
class Settings(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
openrouter_api_key = db.Column(db.String(255), nullable=True)
|
openrouter_api_key = db.Column(db.String(255), nullable=True)
|
||||||
|
|||||||
@@ -14,22 +14,32 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="filename" class="form-label">Filename (Slug)</label>
|
<label for="filename" class="form-label">Filename (Slug) <small class="text-muted">- optional, auto-generated if empty</small></label>
|
||||||
<input type="text" class="form-control" id="filename" name="filename" placeholder="e.g. cyberpunk_ninja" required>
|
<input type="text" class="form-control" id="filename" name="filename" placeholder="e.g. cyberpunk_ninja">
|
||||||
<div class="form-text">Used for the JSON file and URL. No spaces or special characters.</div>
|
<div class="form-text">Used for the JSON file and URL. No spaces or special characters. Auto-generated from name if left empty.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3 form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="use_llm" name="use_llm" checked>
|
||||||
|
<label class="form-check-label" for="use_llm">Use AI to generate profile from description</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" id="prompt-group">
|
||||||
<label for="prompt" class="form-label">Description / Concept</label>
|
<label for="prompt" class="form-label">Description / Concept</label>
|
||||||
<textarea class="form-control" id="prompt" name="prompt" rows="5" placeholder="Describe the character's appearance, clothing, style, and personality. The AI will generate the full profile based on this." required></textarea>
|
<textarea class="form-control" id="prompt" name="prompt" rows="5" placeholder="Describe the character's appearance, clothing, style, and personality. The AI will generate the full profile based on this."></textarea>
|
||||||
|
<div class="form-text">Required when AI generation is enabled.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info" id="ai-info">
|
||||||
<i class="bi bi-info-circle"></i> Once created, the system will automatically attempt to generate a cover image using the new profile.
|
<i class="bi bi-info-circle"></i> The AI will generate a complete character profile based on your description.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-secondary d-none" id="manual-info">
|
||||||
|
<i class="bi bi-info-circle"></i> A blank character profile will be created. You can edit it afterwards to add details.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-grid">
|
<div class="d-grid">
|
||||||
<button type="submit" class="btn btn-success btn-lg">Create & Generate</button>
|
<button type="submit" class="btn btn-success btn-lg" id="submit-btn">Create & Generate</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,4 +47,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('use_llm').addEventListener('change', function() {
|
||||||
|
const promptGroup = document.getElementById('prompt-group');
|
||||||
|
const aiInfo = document.getElementById('ai-info');
|
||||||
|
const manualInfo = document.getElementById('manual-info');
|
||||||
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
|
const promptInput = document.getElementById('prompt');
|
||||||
|
|
||||||
|
if (this.checked) {
|
||||||
|
promptGroup.classList.remove('d-none');
|
||||||
|
aiInfo.classList.remove('d-none');
|
||||||
|
manualInfo.classList.add('d-none');
|
||||||
|
submitBtn.textContent = 'Create & Generate';
|
||||||
|
promptInput.required = true;
|
||||||
|
} else {
|
||||||
|
promptGroup.classList.add('d-none');
|
||||||
|
aiInfo.classList.add('d-none');
|
||||||
|
manualInfo.classList.remove('d-none');
|
||||||
|
submitBtn.textContent = 'Create Character';
|
||||||
|
promptInput.required = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<!-- Image Modal -->
|
||||||
|
<div class="modal fade" id="imageModal" tabindex="-1" aria-labelledby="imageModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||||
|
<div class="modal-content bg-transparent border-0">
|
||||||
|
<div class="modal-body p-0 text-center">
|
||||||
|
<img id="modalImage" src="" alt="Enlarged Image" class="img-fluid" style="max-height: 90vh;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="img-container" style="height: auto; min-height: 400px;">
|
<div class="img-container" style="height: auto; min-height: 400px; cursor: pointer;" data-bs-toggle="modal" data-bs-target="#imageModal" onclick="showImage(this.querySelector('img').src)">
|
||||||
{% if character.image_path %}
|
{% if character.image_path %}
|
||||||
<img src="{{ url_for('static', filename='uploads/' + character.image_path) }}" alt="{{ character.name }}" class="img-fluid">
|
<img src="{{ url_for('static', filename='uploads/' + character.image_path) }}" alt="{{ character.name }}" class="img-fluid">
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -21,7 +32,6 @@
|
|||||||
</form>
|
</form>
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" name="action" value="preview" class="btn btn-success" form="generate-form">Generate Preview</button>
|
<button type="submit" name="action" value="preview" class="btn btn-success" form="generate-form">Generate Preview</button>
|
||||||
<button type="submit" name="action" value="replace" class="btn btn-outline-danger" form="generate-form">Generate & Replace Cover</button>
|
|
||||||
<button type="submit" form="generate-form" formaction="{{ url_for('save_defaults', slug=character.slug) }}" class="btn btn-sm btn-outline-secondary mt-2">Save as Default Selection</button>
|
<button type="submit" form="generate-form" formaction="{{ url_for('save_defaults', slug=character.slug) }}" class="btn btn-sm btn-outline-secondary mt-2">Save as Default Selection</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,18 +46,28 @@
|
|||||||
|
|
||||||
{% if preview_image %}
|
{% if preview_image %}
|
||||||
<div class="card mb-4 border-success">
|
<div class="card mb-4 border-success">
|
||||||
<div class="card-header bg-success text-white">Latest Preview</div>
|
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
||||||
|
<span>Latest Preview</span>
|
||||||
|
<form action="{{ url_for('replace_cover_from_preview', slug=character.slug) }}" method="post" class="m-0">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-light">Replace Cover</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="img-container" style="height: auto; min-height: 400px;">
|
<div class="img-container" style="height: auto; min-height: 400px; cursor: pointer;" data-bs-toggle="modal" data-bs-target="#imageModal" onclick="showImage(this.querySelector('img').src)">
|
||||||
<img id="preview-img" src="{{ url_for('static', filename='uploads/' + preview_image) }}" alt="Preview" class="img-fluid">
|
<img id="preview-img" src="{{ url_for('static', filename='uploads/' + preview_image) }}" alt="Preview" class="img-fluid">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="card mb-4 border-secondary d-none" id="preview-card">
|
<div class="card mb-4 border-secondary d-none" id="preview-card">
|
||||||
<div class="card-header bg-secondary text-white">Latest Preview</div>
|
<div class="card-header bg-secondary text-white d-flex justify-content-between align-items-center">
|
||||||
|
<span>Latest Preview</span>
|
||||||
|
<form action="{{ url_for('replace_cover_from_preview', slug=character.slug) }}" method="post" class="m-0" id="replace-cover-form">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn" disabled>Replace Cover</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="img-container" style="height: auto; min-height: 400px;">
|
<div class="img-container" style="height: auto; min-height: 400px; cursor: pointer;" data-bs-toggle="modal" data-bs-target="#imageModal" onclick="showImage(this.querySelector('img').src)">
|
||||||
<img id="preview-img" src="" alt="Preview" class="img-fluid">
|
<img id="preview-img" src="" alt="Preview" class="img-fluid">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -265,7 +285,7 @@
|
|||||||
form.addEventListener('submit', async (e) => {
|
form.addEventListener('submit', async (e) => {
|
||||||
// Only intercept generate actions
|
// Only intercept generate actions
|
||||||
const submitter = e.submitter;
|
const submitter = e.submitter;
|
||||||
if (!submitter || (submitter.value !== 'preview' && submitter.value !== 'replace')) {
|
if (!submitter || submitter.value !== 'preview') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,7 +340,7 @@
|
|||||||
progressLabel.textContent = 'Saving image...';
|
progressLabel.textContent = 'Saving image...';
|
||||||
const url = `/character/{{ character.slug }}/finalize_generation/${promptId}`;
|
const url = `/character/{{ character.slug }}/finalize_generation/${promptId}`;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('action', action);
|
formData.append('action', 'preview'); // Always save as preview
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -330,12 +350,19 @@
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
if (action === 'preview') {
|
// Update preview image
|
||||||
previewImg.src = data.image_url;
|
previewImg.src = data.image_url;
|
||||||
if (previewCard) previewCard.classList.remove('d-none');
|
if (previewCard) previewCard.classList.remove('d-none');
|
||||||
} else {
|
|
||||||
// Reload for cover update
|
// Enable the replace cover button if it exists
|
||||||
window.location.reload();
|
const replaceBtn = document.getElementById('replace-cover-btn');
|
||||||
|
if (replaceBtn) {
|
||||||
|
replaceBtn.disabled = false;
|
||||||
|
// Check if there's a form to update
|
||||||
|
const form = replaceBtn.closest('form');
|
||||||
|
if (form) {
|
||||||
|
form.action = `/character/{{ character.slug }}/replace_cover_from_preview`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert('Save failed: ' + data.error);
|
alert('Save failed: ' + data.error);
|
||||||
@@ -348,5 +375,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Image modal function
|
||||||
|
function showImage(src) {
|
||||||
|
document.getElementById('modalImage').src = src;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -40,6 +40,12 @@
|
|||||||
<h5 class="card-title text-center">{{ char.name }}</h5>
|
<h5 class="card-title text-center">{{ char.name }}</h5>
|
||||||
<p class="card-text small text-center text-muted">{{ char.data.tags | join(', ') }}</p>
|
<p class="card-text small text-center text-muted">{{ char.data.tags | join(', ') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% if char.data.lora.lora_name %}
|
||||||
|
{% set lora_name = char.data.lora.lora_name.split('/')[-1].replace('.safetensors', '') %}
|
||||||
|
<div class="card-footer text-center p-1">
|
||||||
|
<small class="text-muted" title="{{ char.data.lora.lora_name }}">{{ lora_name }}</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand" href="/">Character Browser</a>
|
<a class="navbar-brand" href="/">Character Browser</a>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
|
<a href="/" class="btn btn-outline-light me-2">Characters</a>
|
||||||
|
<a href="/outfits" class="btn btn-outline-light me-2">Outfits</a>
|
||||||
<a href="/create" class="btn btn-outline-success me-2">Create Character</a>
|
<a href="/create" class="btn btn-outline-success me-2">Create Character</a>
|
||||||
<a href="/generator" class="btn btn-outline-light me-2">Generator</a>
|
<a href="/generator" class="btn btn-outline-light me-2">Generator</a>
|
||||||
<a href="/settings" class="btn btn-outline-light">Settings</a>
|
<a href="/settings" class="btn btn-outline-light">Settings</a>
|
||||||
|
|||||||
74
templates/outfits/create.html
Normal file
74
templates/outfits/create.html
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-success text-white">Create New Outfit</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ url_for('create_outfit') }}" method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Outfit Name</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" placeholder="e.g. French Maid" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="filename" class="form-label">Filename (Slug) <small class="text-muted">- optional, auto-generated if empty</small></label>
|
||||||
|
<input type="text" class="form-control" id="filename" name="filename" placeholder="e.g. french_maid_01">
|
||||||
|
<div class="form-text">Used for the JSON file and URL. No spaces or special characters. Auto-generated from name if left empty.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="use_llm" name="use_llm" checked>
|
||||||
|
<label class="form-check-label" for="use_llm">Use AI to generate profile from description</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" id="prompt-group">
|
||||||
|
<label for="prompt" class="form-label">Description / Concept</label>
|
||||||
|
<textarea class="form-control" id="prompt" name="prompt" rows="5" placeholder="Describe the outfit's style, components, colors, and any special features. The AI will generate the full outfit profile based on this."></textarea>
|
||||||
|
<div class="form-text">Required when AI generation is enabled.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info" id="ai-info">
|
||||||
|
<i class="bi bi-info-circle"></i> The AI will generate a complete outfit profile with wardrobe items and tags based on your description.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-secondary d-none" id="manual-info">
|
||||||
|
<i class="bi bi-info-circle"></i> A blank outfit profile will be created. You can edit it afterwards to add details.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-success btn-lg" id="submit-btn">Create & Generate</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('use_llm').addEventListener('change', function() {
|
||||||
|
const promptGroup = document.getElementById('prompt-group');
|
||||||
|
const aiInfo = document.getElementById('ai-info');
|
||||||
|
const manualInfo = document.getElementById('manual-info');
|
||||||
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
|
const promptInput = document.getElementById('prompt');
|
||||||
|
|
||||||
|
if (this.checked) {
|
||||||
|
promptGroup.classList.remove('d-none');
|
||||||
|
aiInfo.classList.remove('d-none');
|
||||||
|
manualInfo.classList.add('d-none');
|
||||||
|
submitBtn.textContent = 'Create & Generate';
|
||||||
|
promptInput.required = true;
|
||||||
|
} else {
|
||||||
|
promptGroup.classList.add('d-none');
|
||||||
|
aiInfo.classList.add('d-none');
|
||||||
|
manualInfo.classList.remove('d-none');
|
||||||
|
submitBtn.textContent = 'Create Outfit';
|
||||||
|
promptInput.required = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
353
templates/outfits/detail.html
Normal file
353
templates/outfits/detail.html
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Image Modal -->
|
||||||
|
<div class="modal fade" id="imageModal" tabindex="-1" aria-labelledby="imageModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||||
|
<div class="modal-content bg-transparent border-0">
|
||||||
|
<div class="modal-body p-0 text-center">
|
||||||
|
<img id="modalImage" src="" alt="Enlarged Image" class="img-fluid" style="max-height: 90vh;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="img-container" style="height: auto; min-height: 400px; cursor: pointer;" data-bs-toggle="modal" data-bs-target="#imageModal" onclick="showImage(this.querySelector('img').src)">
|
||||||
|
{% if outfit.image_path %}
|
||||||
|
<img src="{{ url_for('static', filename='uploads/' + outfit.image_path) }}" alt="{{ outfit.name }}" class="img-fluid">
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">No Image Attached</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ url_for('upload_outfit_image', slug=outfit.slug) }}" method="post" enctype="multipart/form-data">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="image" class="form-label">Update Image</label>
|
||||||
|
<input class="form-control" type="file" id="image" name="image" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary w-100 mb-2">Upload</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{# Character Selector #}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="character_select" class="form-label">Preview with Character</label>
|
||||||
|
<select class="form-select" id="character_select" name="character_slug" form="generate-form">
|
||||||
|
<option value="">-- No Character (Outfit Only) --</option>
|
||||||
|
<option value="__random__" {% if selected_character == '__random__' %}selected{% endif %}>🎲 Random Character</option>
|
||||||
|
{% for char in characters %}
|
||||||
|
<option value="{{ char.slug }}" {% if selected_character == char.slug %}selected{% endif %}>{{ char.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="form-text">Select a character to preview this outfit on their model.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" name="action" value="preview" class="btn btn-success" form="generate-form">Generate Preview</button>
|
||||||
|
<button type="submit" form="generate-form" formaction="{{ url_for('save_outfit_defaults', slug=outfit.slug) }}" class="btn btn-sm btn-outline-secondary mt-2">Save as Default Selection</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="progress-container" class="mb-4 d-none">
|
||||||
|
<label id="progress-label" class="form-label">Generating...</label>
|
||||||
|
<div class="progress" role="progressbar" aria-label="Generation Progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
|
||||||
|
<div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" style="width: 0%">0%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if preview_image %}
|
||||||
|
<div class="card mb-4 border-success">
|
||||||
|
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
||||||
|
<span>Latest Preview</span>
|
||||||
|
<form action="{{ url_for('replace_outfit_cover_from_preview', slug=outfit.slug) }}" method="post" class="m-0">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-light">Replace Cover</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="img-container" style="height: auto; min-height: 400px; cursor: pointer;" data-bs-toggle="modal" data-bs-target="#imageModal" onclick="showImage(this.querySelector('img').src)">
|
||||||
|
<img id="preview-img" src="{{ url_for('static', filename='uploads/' + preview_image) }}" alt="Preview" class="img-fluid">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card mb-4 border-secondary d-none" id="preview-card">
|
||||||
|
<div class="card-header bg-secondary text-white d-flex justify-content-between align-items-center">
|
||||||
|
<span>Latest Preview</span>
|
||||||
|
<form action="{{ url_for('replace_outfit_cover_from_preview', slug=outfit.slug) }}" method="post" class="m-0" id="replace-cover-form">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn" disabled>Replace Cover</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="img-container" style="height: auto; min-height: 400px; cursor: pointer;" data-bs-toggle="modal" data-bs-target="#imageModal" onclick="showImage(this.querySelector('img').src)">
|
||||||
|
<img id="preview-img" src="" alt="Preview" class="img-fluid">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
||||||
|
<span>Tags</span>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" name="include_field" value="special::tags" id="includeTags" form="generate-form"
|
||||||
|
{% if preferences is not none %}
|
||||||
|
{% if 'special::tags' in preferences %}checked{% endif %}
|
||||||
|
{% elif outfit.default_fields is not none %}
|
||||||
|
{% if 'special::tags' in outfit.default_fields %}checked{% endif %}
|
||||||
|
{% else %}
|
||||||
|
checked
|
||||||
|
{% endif %}>
|
||||||
|
<label class="form-check-label text-white small" for="includeTags">Include</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% for tag in outfit.data.tags %}
|
||||||
|
<span class="badge bg-secondary">{{ tag }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">No tags</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="mb-0">{{ outfit.name }}</h1>
|
||||||
|
<a href="{{ url_for('edit_outfit', slug=outfit.slug) }}" class="btn btn-sm btn-link text-decoration-none">Edit Profile</a>
|
||||||
|
<form action="{{ url_for('clone_outfit', slug=outfit.slug) }}" method="post" style="display: inline;">
|
||||||
|
<button type="submit" class="btn btn-sm btn-link text-decoration-none">Clone Outfit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('outfits_index') }}" class="btn btn-outline-secondary">Back to Gallery</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="generate-form" action="{{ url_for('generate_outfit_image', slug=outfit.slug) }}" method="post">
|
||||||
|
{# Wardrobe section #}
|
||||||
|
{% set wardrobe = outfit.data.get('wardrobe', {}) %}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
|
<strong>Wardrobe</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
{% for key, value in wardrobe.items() %}
|
||||||
|
<dt class="col-sm-4 text-capitalize">
|
||||||
|
<input class="form-check-input me-1" type="checkbox" name="include_field" value="wardrobe::{{ key }}"
|
||||||
|
{% if preferences is not none %}
|
||||||
|
{% if 'wardrobe::' + key in preferences %}checked{% endif %}
|
||||||
|
{% elif outfit.default_fields is not none %}
|
||||||
|
{% if 'wardrobe::' + key in outfit.default_fields %}checked{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if value %}checked{% endif %}
|
||||||
|
{% endif %}>
|
||||||
|
{{ key.replace('_', ' ') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-8">{{ value if value else '--' }}</dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# LoRA section #}
|
||||||
|
{% set lora = outfit.data.get('lora', {}) %}
|
||||||
|
{% if lora %}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-light"><strong>LoRA</strong></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
{% for key, value in lora.items() %}
|
||||||
|
<dt class="col-sm-4 text-capitalize">
|
||||||
|
<input class="form-check-input me-1" type="checkbox" name="include_field" value="lora::{{ key }}"
|
||||||
|
{% if preferences is not none %}
|
||||||
|
{% if 'lora::' + key in preferences %}checked{% endif %}
|
||||||
|
{% elif outfit.default_fields is not none %}
|
||||||
|
{% if 'lora::' + key in outfit.default_fields %}checked{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if value %}checked{% endif %}
|
||||||
|
{% endif %}>
|
||||||
|
{{ key.replace('_', ' ') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-8">{{ value if value else '--' }}</dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const form = document.getElementById('generate-form');
|
||||||
|
const progressBar = document.getElementById('progress-bar');
|
||||||
|
const progressContainer = document.getElementById('progress-container');
|
||||||
|
const progressLabel = document.getElementById('progress-label');
|
||||||
|
const previewCard = document.getElementById('preview-card');
|
||||||
|
const previewImg = document.getElementById('preview-img');
|
||||||
|
|
||||||
|
// Generate a unique client ID
|
||||||
|
const clientId = 'outfit_detail_' + Math.random().toString(36).substring(2, 15);
|
||||||
|
|
||||||
|
// ComfyUI WebSocket
|
||||||
|
const socket = new WebSocket(`ws://127.0.0.1:8188/ws?clientId=${clientId}`);
|
||||||
|
|
||||||
|
let currentPromptId = null;
|
||||||
|
let currentAction = null;
|
||||||
|
|
||||||
|
socket.addEventListener('message', (event) => {
|
||||||
|
if (!currentPromptId) return;
|
||||||
|
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (msg.type === 'status') {
|
||||||
|
const queueRemaining = msg.data.status.exec_info.queue_remaining;
|
||||||
|
if (queueRemaining > 0) {
|
||||||
|
progressLabel.textContent = `Queue position: ${queueRemaining}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (msg.type === 'progress') {
|
||||||
|
const value = msg.data.value;
|
||||||
|
const max = msg.data.max;
|
||||||
|
const percent = Math.round((value / max) * 100);
|
||||||
|
progressBar.style.width = `${percent}%`;
|
||||||
|
progressBar.textContent = `${percent}%`;
|
||||||
|
}
|
||||||
|
else if (msg.type === 'executing') {
|
||||||
|
if (msg.data.node === null && msg.data.prompt_id === currentPromptId) {
|
||||||
|
// Execution finished via WebSocket
|
||||||
|
console.log('Finished via WebSocket');
|
||||||
|
if (resolveCompletion) resolveCompletion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let resolveCompletion = null;
|
||||||
|
async function waitForCompletion(promptId) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const checkResolve = () => {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
resolveCompletion = checkResolve;
|
||||||
|
|
||||||
|
// Fallback polling in case WebSocket is blocked (403)
|
||||||
|
const pollInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/check_status/${promptId}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.status === 'finished') {
|
||||||
|
console.log('Finished via Polling');
|
||||||
|
checkResolve();
|
||||||
|
}
|
||||||
|
} catch (err) { console.error('Polling error:', err); }
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
// Only intercept generate actions
|
||||||
|
const submitter = e.submitter;
|
||||||
|
if (!submitter || submitter.value !== 'preview') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
currentAction = submitter.value;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
formData.append('action', currentAction);
|
||||||
|
formData.append('client_id', clientId);
|
||||||
|
|
||||||
|
// UI Reset
|
||||||
|
progressContainer.classList.remove('d-none');
|
||||||
|
progressBar.style.width = '0%';
|
||||||
|
progressBar.textContent = '0%';
|
||||||
|
progressLabel.textContent = 'Starting...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(form.getAttribute('action'), {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
alert('Error: ' + data.error);
|
||||||
|
progressContainer.classList.add('d-none');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPromptId = data.prompt_id;
|
||||||
|
progressLabel.textContent = 'Queued...';
|
||||||
|
|
||||||
|
// Wait for completion (WebSocket or Polling)
|
||||||
|
await waitForCompletion(currentPromptId);
|
||||||
|
|
||||||
|
// Finalize
|
||||||
|
finalizeGeneration(currentPromptId, currentAction);
|
||||||
|
currentPromptId = null;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert('Request failed');
|
||||||
|
progressContainer.classList.add('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function finalizeGeneration(promptId, action) {
|
||||||
|
progressLabel.textContent = 'Saving image...';
|
||||||
|
const url = `/outfit/{{ outfit.slug }}/finalize_generation/${promptId}`;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'preview'); // Always save as preview
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Update preview image
|
||||||
|
previewImg.src = data.image_url;
|
||||||
|
if (previewCard) previewCard.classList.remove('d-none');
|
||||||
|
|
||||||
|
// Enable the replace cover button if it exists
|
||||||
|
const replaceBtn = document.getElementById('replace-cover-btn');
|
||||||
|
if (replaceBtn) {
|
||||||
|
replaceBtn.disabled = false;
|
||||||
|
// Check if there's a form to update
|
||||||
|
const form = replaceBtn.closest('form');
|
||||||
|
if (form) {
|
||||||
|
form.action = `/outfit/{{ outfit.slug }}/replace_cover_from_preview`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('Save failed: ' + data.error);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert('Finalize request failed');
|
||||||
|
} finally {
|
||||||
|
progressContainer.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Image modal function
|
||||||
|
function showImage(src) {
|
||||||
|
document.getElementById('modalImage').src = src;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
80
templates/outfits/edit.html
Normal file
80
templates/outfits/edit.html
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1>Edit Outfit: {{ outfit.name }}</h1>
|
||||||
|
<a href="{{ url_for('outfit_detail', slug=outfit.slug) }}" class="btn btn-outline-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="{{ url_for('edit_outfit', slug=outfit.slug) }}" method="post" id="main-form">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-dark text-white">Basic Information</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="outfit_name" class="form-label">Display Name</label>
|
||||||
|
<input type="text" class="form-control" id="outfit_name" name="outfit_name" value="{{ outfit.name }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="outfit_id" class="form-label">Outfit ID</label>
|
||||||
|
<input type="text" class="form-control" id="outfit_id" name="outfit_id" value="{{ outfit.outfit_id }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="tags" class="form-label">Tags (comma separated)</label>
|
||||||
|
<input type="text" class="form-control" id="tags" name="tags" value="{{ outfit.data.tags | join(', ') }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LoRA -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-info text-white">LoRA Settings</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label for="lora_lora_name" class="form-label">LoRA Name</label>
|
||||||
|
<select class="form-select" id="lora_lora_name" name="lora_lora_name">
|
||||||
|
<option value="">None</option>
|
||||||
|
{% for lora in loras %}
|
||||||
|
<option value="{{ lora }}" {% if outfit.data.lora.lora_name == lora %}selected{% endif %}>{{ lora }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="lora_lora_weight" class="form-label">Weight</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="lora_lora_weight" name="lora_lora_weight" value="{{ outfit.data.lora.lora_weight }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<label for="lora_lora_triggers" class="form-label">Triggers</label>
|
||||||
|
<input type="text" class="form-control" id="lora_lora_triggers" name="lora_lora_triggers" value="{{ outfit.data.lora.lora_triggers }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Wardrobe Section -->
|
||||||
|
{% set wardrobe = outfit.data.get('wardrobe', {}) %}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-light"><strong>Wardrobe</strong></div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% for key, value in wardrobe.items() %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="wardrobe_{{ key }}" class="form-label text-capitalize">{{ key.replace('_', ' ') }}</label>
|
||||||
|
<input type="text" class="form-control" id="wardrobe_{{ key }}" name="wardrobe_{{ key }}" value="{{ value }}">
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
<a href="{{ url_for('outfit_detail', slug=outfit.slug) }}" class="btn btn-secondary">Cancel</a>
|
||||||
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
41
templates/outfits/index.html
Normal file
41
templates/outfits/index.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2>Outfit Gallery</h2>
|
||||||
|
<div class="d-flex">
|
||||||
|
<a href="{{ url_for('create_outfit') }}" class="btn btn-success me-2">Create New Outfit</a>
|
||||||
|
<form action="{{ url_for('rescan_outfits') }}" method="post">
|
||||||
|
<button type="submit" class="btn btn-outline-primary">Rescan Outfit Files</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-4">
|
||||||
|
{% for outfit in outfits %}
|
||||||
|
<div class="col" id="card-{{ outfit.slug }}">
|
||||||
|
<div class="card h-100 character-card" onclick="window.location.href='/outfit/{{ outfit.slug }}'">
|
||||||
|
<div class="img-container">
|
||||||
|
{% if outfit.image_path %}
|
||||||
|
<img id="img-{{ outfit.slug }}" src="{{ url_for('static', filename='uploads/' + outfit.image_path) }}" alt="{{ outfit.name }}">
|
||||||
|
<span id="no-img-{{ outfit.slug }}" class="text-muted d-none">No Image</span>
|
||||||
|
{% else %}
|
||||||
|
<img id="img-{{ outfit.slug }}" src="" alt="{{ outfit.name }}" class="d-none">
|
||||||
|
<span id="no-img-{{ outfit.slug }}" class="text-muted">No Image</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title text-center">{{ outfit.name }}</h5>
|
||||||
|
<p class="card-text small text-center text-muted">{{ outfit.data.tags | join(', ') }}</p>
|
||||||
|
</div>
|
||||||
|
{% if outfit.data.lora.lora_name %}
|
||||||
|
{% set lora_name = outfit.data.lora.lora_name.split('/')[-1].replace('.safetensors', '') %}
|
||||||
|
<div class="card-footer text-center p-1">
|
||||||
|
<small class="text-muted" title="{{ outfit.data.lora.lora_name }}">{{ lora_name }}</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user