Initial implementation of Character Browser & Generator: Gallery, ComfyUI integration, progress tracking, and batch processing.

This commit is contained in:
Aodhan Collins
2026-02-08 01:54:18 +00:00
commit df82d4ec07
62 changed files with 3433 additions and 0 deletions

33
.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
pip-log.txt
pip-delete-this-directory.txt
.venv/
.env
.flaskenv
# SQLite Database
database.db
# Uploads / Generated Images
static/uploads/*
!static/uploads/.gitkeep
# IDEs / Editors
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log

67
README.md Normal file
View File

@@ -0,0 +1,67 @@
# Character Browser & Generator
A local web-based GUI for managing character profiles (JSON) and generating consistent images using ComfyUI.
## Features
- **Character Gallery**: Automatically scans your `characters/` folder and builds a searchable, sortable database.
- **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**:
- **SDXL Optimized**: Designed for high-quality SDXL workflows.
- **Localized ADetailer**: Automated Face and Hand detailing with focused prompts (e.g., only eye color and expression are sent to the face detailer).
- **LoRA Support**: Automatically detects and applies LoRAs specified in your character sheets.
- **Real-time Progress**: Live progress bars and queue status via WebSockets (with a reliable polling fallback).
- **Batch Processing**:
- **Fill Missing**: Generate covers for every character missing one with a single click.
- **Refresh All**: Unassign all current covers and generate a fresh set for the whole collection.
- **Advanced Generator**: A dedicated page to mix-and-match characters with different checkpoints (Illustrious/Noob support) and custom prompt additions.
- **Smart URLs**: Sanitized, human-readable URLs (slugs) that handle special characters and slashes gracefully.
## Prerequisites
1. **Python 3.10+**
2. **ComfyUI** running locally (default: `http://127.0.0.1:8188`).
3. **ComfyUI Custom Nodes**:
- [ComfyUI-Impact-Pack](https://github.com/ltdrdata/ComfyUI-Impact-Pack) (Required for FaceDetailer).
4. **Models**:
- Ensure your Checkpoints are in the folders defined in `app.py`.
- Face detection model: `bbox/face_yolov9c.pt` (or update `comfy_workflow.json`).
- Hand detection model: `bbox/hand_yolov8s.pt`.
## Setup & Installation
1. **Clone the repository** to your local machine.
2. **Configure Paths**: Open `app.py` and update the following variables to match your system:
```python
app.config['COMFYUI_URL'] = 'http://127.0.0.1:8188'
app.config['ILLUSTRIOUS_MODELS_DIR'] = '/path/to/your/Illustrious/models'
app.config['NOOB_MODELS_DIR'] = '/path/to/your/Noob/models'
```
3. **Launch**: Simply run the launch script. It will handle the virtual environment and dependencies automatically.
```bash
chmod +x launch.sh
./launch.sh
```
## Usage
### Gallery Management
- **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.
### Generation
- **Preview**: Generates an image and shows it to you without replacing your current cover.
- **Replace**: Generates an image and sets it as the character's official gallery cover.
- **Clean Start**: If you want to wipe the database and all generated images to start fresh:
```bash
./launch.sh --clean
```
## File Structure
- `/characters`: Your character JSON files.
- `/static/uploads`: Generated images (organized by character subfolders).
- `/templates`: HTML UI using Bootstrap 5.
- `app.py`: Flask backend and prompt-building logic.
- `comfy_workflow.json`: The API-format workflow used for generations.
- `models.py`: SQLite database schema.

544
app.py Normal file
View File

@@ -0,0 +1,544 @@
import os
import json
import time
import re
import requests
import random
from flask import Flask, render_template, request, redirect, url_for, flash, session
from werkzeug.utils import secure_filename
from models import db, Character
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = 'static/uploads'
app.config['SECRET_KEY'] = 'dev-key-123'
app.config['CHARACTERS_DIR'] = 'characters'
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['NOOB_MODELS_DIR'] = '/mnt/alexander/AITools/Image Models/Stable-diffusion/Noob/'
db.init_app(app)
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
def get_available_checkpoints():
checkpoints = []
# Scan Illustrious
if os.path.exists(app.config['ILLUSTRIOUS_MODELS_DIR']):
for f in os.listdir(app.config['ILLUSTRIOUS_MODELS_DIR']):
if f.endswith('.safetensors') or f.endswith('.ckpt'):
checkpoints.append(f"Illustrious/{f}")
# Scan Noob
if os.path.exists(app.config['NOOB_MODELS_DIR']):
for f in os.listdir(app.config['NOOB_MODELS_DIR']):
if f.endswith('.safetensors') or f.endswith('.ckpt'):
checkpoints.append(f"Noob/{f}")
return sorted(checkpoints)
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def build_prompt(data, selected_fields=None, default_fields=None):
def is_selected(section, key):
# Priority:
# 1. Manual selection from form (if list is not empty)
# 2. Database defaults (if they exist)
# 3. Select all (default behavior)
if selected_fields:
return f"{section}::{key}" in selected_fields
if default_fields:
return f"{section}::{key}" in default_fields
return True
identity = data.get('identity', {})
wardrobe = data.get('wardrobe', {})
# Pre-calculate Hand/Glove priority
hand_val = ""
if wardrobe.get('gloves') and is_selected('wardrobe', 'gloves'):
hand_val = wardrobe.get('gloves')
elif identity.get('hands') and is_selected('identity', 'hands'):
hand_val = identity.get('hands')
# 1. Main Prompt
parts = ["(solo:1.2)"]
# Use character_id (underscores to spaces) for tags compatibility
char_tag = data.get('character_id', '').replace('_', ' ')
if char_tag and is_selected('special', 'name'):
parts.append(char_tag)
for key in ['base_specs', 'hair', 'eyes', 'expression', 'distinguishing_marks']:
val = identity.get(key)
if val and is_selected('identity', key):
parts.append(val)
# Add hand priority value to main prompt
if hand_val:
parts.append(hand_val)
for key in ['outer_layer', 'inner_layer', 'lower_body', 'footwear', 'accessories']:
val = wardrobe.get(key)
if val and is_selected('wardrobe', key):
parts.append(val)
style = data.get('styles', {}).get('aesthetic')
if style and is_selected('styles', 'aesthetic'):
parts.append(f"{style} style")
tags = data.get('tags', [])
if tags and is_selected('special', 'tags'):
parts.extend(tags)
lora = data.get('lora', {})
if lora.get('lora_triggers') and is_selected('lora', 'lora_triggers'):
parts.append(lora.get('lora_triggers'))
# 2. Face Prompt: Tag, Eyes, Expression
face_parts = []
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('expression') and is_selected('identity', 'expression'): face_parts.append(identity.get('expression'))
# 3. Hand Prompt: Hand value (Gloves or Hands)
hand_parts = [hand_val] if hand_val else []
return {
"main": ", ".join(parts),
"face": ", ".join(face_parts),
"hand": ", ".join(hand_parts)
}
def queue_prompt(prompt_workflow, client_id=None):
p = {"prompt": prompt_workflow}
if client_id:
p["client_id"] = client_id
data = json.dumps(p).encode('utf-8')
response = requests.post(f"{app.config['COMFYUI_URL']}/prompt", data=data)
return response.json()
def get_history(prompt_id):
response = requests.get(f"{app.config['COMFYUI_URL']}/history/{prompt_id}")
return response.json()
def get_image(filename, subfolder, folder_type):
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
response = requests.get(f"{app.config['COMFYUI_URL']}/view", params=data)
return response.content
from sqlalchemy.orm.attributes import flag_modified
def sync_characters():
if not os.path.exists(app.config['CHARACTERS_DIR']):
return
current_ids = []
for filename in os.listdir(app.config['CHARACTERS_DIR']):
if filename.endswith('.json'):
file_path = os.path.join(app.config['CHARACTERS_DIR'], filename)
try:
with open(file_path, 'r') as f:
data = json.load(f)
char_id = data.get('character_id')
if not char_id:
continue
current_ids.append(char_id)
# Generate URL-safe slug: remove special characters from character_id
slug = re.sub(r'[^a-zA-Z0-9_]', '', char_id)
# Check if character already exists
character = Character.query.filter_by(character_id=char_id).first()
name = data.get('character_name', char_id.replace('_', ' ').title())
if character:
character.data = data
character.name = name
character.slug = slug
# Check if cover image still exists
if character.image_path:
full_img_path = os.path.join(app.config['UPLOAD_FOLDER'], character.image_path)
if not os.path.exists(full_img_path):
print(f"Image missing for {character.name}, clearing path.")
character.image_path = None
# Explicitly tell SQLAlchemy the JSON field was modified
flag_modified(character, "data")
else:
new_char = Character(
character_id=char_id,
slug=slug,
name=name,
data=data
)
db.session.add(new_char)
except Exception as e:
print(f"Error importing {filename}: {e}")
# Remove characters that are no longer in the folder
all_characters = Character.query.all()
for char in all_characters:
if char.character_id not in current_ids:
db.session.delete(char)
db.session.commit()
@app.route('/')
def index():
characters = Character.query.order_by(Character.name).all()
return render_template('index.html', characters=characters)
@app.route('/rescan', methods=['POST'])
def rescan():
sync_characters()
flash('Database synced with character files.')
return redirect(url_for('index'))
@app.route('/generator', methods=['GET', 'POST'])
def generator():
characters = Character.query.order_by(Character.name).all()
checkpoints = get_available_checkpoints()
if not checkpoints:
checkpoints = ["Noob/oneObsession_v19Atypical.safetensors"]
if request.method == 'POST':
char_slug = request.form.get('character')
checkpoint = request.form.get('checkpoint')
custom_positive = request.form.get('positive_prompt', '')
custom_negative = request.form.get('negative_prompt', '')
character = Character.query.filter_by(slug=char_slug).first_or_404()
try:
with open('comfy_workflow.json', 'r') as f:
workflow = json.load(f)
# Build base prompts from character defaults
prompts = build_prompt(character.data, default_fields=character.default_fields)
# Append custom additions to the "main" prompt
if custom_positive:
prompts["main"] = f"{prompts['main']}, {custom_positive}"
# Prepare workflow with custom checkpoint and negative prompt
workflow = _prepare_workflow(workflow, character, prompts, checkpoint, custom_negative)
print(f"Queueing generator prompt for {character.character_id}")
prompt_response = queue_prompt(workflow)
if 'prompt_id' not in prompt_response:
raise Exception(f"ComfyUI failed: {prompt_response.get('error', 'Unknown error')}")
prompt_id = prompt_response['prompt_id']
flash("Generation started...")
max_retries = 120
while max_retries > 0:
history = get_history(prompt_id)
if prompt_id in history:
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'])
char_folder = os.path.join(app.config['UPLOAD_FOLDER'], character.slug)
os.makedirs(char_folder, exist_ok=True)
filename = f"gen_{int(time.time())}.png"
file_path = os.path.join(char_folder, filename)
with open(file_path, 'wb') as f:
f.write(image_data)
relative_path = f"{character.slug}/{filename}"
return render_template('generator.html', characters=characters, checkpoints=checkpoints,
generated_image=relative_path, selected_char=char_slug, selected_ckpt=checkpoint)
time.sleep(2)
max_retries -= 1
flash("Generation timed out.")
except Exception as e:
print(f"Generator error: {e}")
flash(f"Error: {str(e)}")
return render_template('generator.html', characters=characters, checkpoints=checkpoints)
@app.route('/character/<path:slug>')
def detail(slug):
character = Character.query.filter_by(slug=slug).first_or_404()
# Load state from session
preferences = session.get(f'prefs_{slug}')
preview_image = session.get(f'preview_{slug}')
return render_template('detail.html', character=character, preferences=preferences, preview_image=preview_image)
@app.route('/character/<path:slug>/upload', methods=['POST'])
def upload_image(slug):
character = Character.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 character subfolder
char_folder = os.path.join(app.config['UPLOAD_FOLDER'], slug)
os.makedirs(char_folder, exist_ok=True)
filename = secure_filename(file.filename)
file_path = os.path.join(char_folder, filename)
file.save(file_path)
# Store relative path in DB
character.image_path = f"{slug}/{filename}"
db.session.commit()
flash('Image uploaded successfully!')
return redirect(url_for('detail', slug=slug))
@app.route('/character/<path:slug>/finalize_generation/<prompt_id>', methods=['POST'])
def finalize_generation(slug, prompt_id):
character = Character.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 character subfolder
char_folder = os.path.join(app.config['UPLOAD_FOLDER'], slug)
os.makedirs(char_folder, exist_ok=True)
filename = f"gen_{int(time.time())}.png"
file_path = os.path.join(char_folder, filename)
with open(file_path, 'wb') as f:
f.write(image_data)
print(f"Image saved to: {os.path.abspath(file_path)}")
# Handle actions
relative_path = f"{slug}/{filename}"
if action == 'replace':
character.image_path = relative_path
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 {'error': 'No image found in output'}, 404
except Exception as e:
print(f"Finalize error: {e}")
return {'error': str(e)}, 500
def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_negative=None):
# 1. Update prompts using replacement to preserve embeddings
workflow["6"]["inputs"]["text"] = workflow["6"]["inputs"]["text"].replace("{{POSITIVE_PROMPT}}", prompts["main"])
if custom_negative:
workflow["7"]["inputs"]["text"] = f"{workflow['7']['inputs']['text']}, {custom_negative}"
if "14" in workflow:
workflow["14"]["inputs"]["text"] = workflow["14"]["inputs"]["text"].replace("{{FACE_PROMPT}}", prompts["face"])
if "15" in workflow:
workflow["15"]["inputs"]["text"] = workflow["15"]["inputs"]["text"].replace("{{HAND_PROMPT}}", prompts["hand"])
print("--- DEBUG: COMFYUI PROMPTS ---")
print(f"Main Positive (6): {workflow['6']['inputs']['text']}")
print(f"Main Negative (7): {workflow['7']['inputs']['text']}")
if "14" in workflow:
print(f"Face Detailer (14): {workflow['14']['inputs']['text']}")
if "15" in workflow:
print(f"Hand Detailer (15): {workflow['15']['inputs']['text']}")
print("-------------------------------")
# 2. Update Checkpoint
if checkpoint:
workflow["4"]["inputs"]["ckpt_name"] = checkpoint
# 3. Handle LoRA
lora_data = character.data.get('lora', {})
lora_name = lora_data.get('lora_name')
model_source = ["4", 0]
clip_source = ["4", 1]
if lora_name and "16" in workflow:
workflow["16"]["inputs"]["lora_name"] = lora_name
workflow["16"]["inputs"]["strength_model"] = lora_data.get('lora_weight', 1.0)
workflow["16"]["inputs"]["strength_clip"] = lora_data.get('lora_weight', 1.0)
model_source = ["16", 0]
clip_source = ["16", 1]
# Apply connections to all model/clip consumers
workflow["3"]["inputs"]["model"] = model_source
workflow["11"]["inputs"]["model"] = model_source
workflow["13"]["inputs"]["model"] = model_source
workflow["6"]["inputs"]["clip"] = clip_source
workflow["7"]["inputs"]["clip"] = clip_source
workflow["11"]["inputs"]["clip"] = clip_source
workflow["13"]["inputs"]["clip"] = clip_source
workflow["14"]["inputs"]["clip"] = clip_source
workflow["15"]["inputs"]["clip"] = clip_source
# 4. Randomize seeds
gen_seed = random.randint(1, 10**15)
workflow["3"]["inputs"]["seed"] = gen_seed
if "11" in workflow: workflow["11"]["inputs"]["seed"] = gen_seed
if "13" in workflow: workflow["13"]["inputs"]["seed"] = gen_seed
return workflow
def _queue_generation(character, action='preview', selected_fields=None, client_id=None):
# 1. Load workflow
with open('comfy_workflow.json', 'r') as f:
workflow = json.load(f)
# 2. Build prompts
prompts = build_prompt(character.data, selected_fields, character.default_fields)
# 3. Prepare workflow
workflow = _prepare_workflow(workflow, character, prompts)
return queue_prompt(workflow, client_id=client_id)
@app.route('/get_missing_characters')
def get_missing_characters():
missing = Character.query.filter((Character.image_path == None) | (Character.image_path == '')).all()
return {'missing': [{'slug': c.slug, 'name': c.name} for c in missing]}
@app.route('/clear_all_covers', methods=['POST'])
def clear_all_covers():
characters = Character.query.all()
for char in characters:
char.image_path = None
db.session.commit()
return {'success': True}
@app.route('/generate_missing', methods=['POST'])
def generate_missing():
missing = Character.query.filter((Character.image_path == None) | (Character.image_path == '')).all()
if not missing:
flash("No characters missing cover images.")
return redirect(url_for('index'))
success_count = 0
for character in missing:
try:
print(f"Batch generating for: {character.name}")
prompt_response = _queue_generation(character, action='replace')
prompt_id = prompt_response['prompt_id']
# Simple synchronous wait for each
max_retries = 120
while max_retries > 0:
history = get_history(prompt_id)
if prompt_id in history:
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'])
char_folder = os.path.join(app.config['UPLOAD_FOLDER'], character.slug)
os.makedirs(char_folder, exist_ok=True)
filename = f"gen_{int(time.time())}.png"
file_path = os.path.join(char_folder, filename)
with open(file_path, 'wb') as f:
f.write(image_data)
character.image_path = f"{character.slug}/{filename}"
db.session.commit()
success_count += 1
break
break
time.sleep(2)
max_retries -= 1
except Exception as e:
print(f"Error generating for {character.name}: {e}")
flash(f"Batch generation complete. Generated {success_count} images.")
return redirect(url_for('index'))
@app.route('/check_status/<prompt_id>')
def check_status(prompt_id):
try:
history = get_history(prompt_id)
if prompt_id in history:
return {'status': 'finished'}
return {'status': 'pending'}
except Exception:
return {'status': 'error'}, 500
@app.route('/character/<path:slug>/generate', methods=['POST'])
def generate_image(slug):
character = Character.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')
# Save preferences
session[f'prefs_{slug}'] = selected_fields
# Queue generation using helper
prompt_response = _queue_generation(character, action, selected_fields, 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('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('detail', slug=slug))
@app.route('/character/<path:slug>/save_defaults', methods=['POST'])
def save_defaults(slug):
character = Character.query.filter_by(slug=slug).first_or_404()
selected_fields = request.form.getlist('include_field')
character.default_fields = selected_fields
db.session.commit()
flash('Default prompt selection saved for this character!')
return redirect(url_for('detail', slug=slug))
if __name__ == '__main__':
with app.app_context():
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
db.create_all()
sync_characters()
app.run(debug=True, port=5000)

View File

@@ -0,0 +1,38 @@
{
"character_id": "aerith_gainsborough",
"identity": {
"base_specs": "1girl, slender build, fair skin",
"hair": "long brown hair, braided, pink ribbon",
"eyes": "green eyes",
"expression": "cheerful expression",
"hands": "pink nails",
"arms": "",
"torso": "small breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "pink dress, red bolero jacket",
"lower_body": "long skirt",
"footwear": "brown boots",
"gloves": "",
"accessories": "gold bracelets, flower basket"
},
"styles": {
"aesthetic": "floral, gentle, final fantasy style",
"primary_color": "pink",
"secondary_color": "red",
"tertiary_color": "brown"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Final Fantasy VII"
]
}

View File

@@ -0,0 +1,39 @@
{
"character_id": "android_18",
"character_name": "Android 18",
"identity": {
"base_specs": "1girl, slender build, fair skin",
"hair": "shoulder-length blonde hair, tucked behind one ear",
"eyes": "blue eyes",
"expression": "cool, indifferent expression",
"hands": "blue nails",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "gold hoop earrings"
},
"wardrobe": {
"inner_layer": "black short-sleeved shirt",
"outer_layer": "blue denim vest, 'RR' text on back",
"lower_body": "blue denim skirt, black leggings",
"footwear": "brown boots",
"gloves": "",
"accessories": ""
},
"styles": {
"aesthetic": "90s casual, anime, dragon ball style",
"primary_color": "blue",
"secondary_color": "black",
"tertiary_color": "white"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Dragon Ball Z"
]
}

View File

@@ -0,0 +1,39 @@
{
"character_id": "anya_(spy_x_family)",
"character_name": "Anya Forger",
"identity": {
"base_specs": "1girl, small build, loli, fair skin",
"hair": "short pink hair, two small horns (hair ornaments)",
"eyes": "green eyes",
"expression": "smirk",
"hands": "pink nails",
"arms": "",
"torso": "flat chest",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "black Eden Academy uniform, gold trim",
"lower_body": "uniform skirt",
"footwear": "black shoes, white socks",
"gloves": "",
"accessories": "black and gold hair cones"
},
"styles": {
"aesthetic": "cute, academic, spy x family style",
"primary_color": "pink",
"secondary_color": "black",
"tertiary_color": "gold"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Spy x Family"
]
}

View File

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

39
characters/bulma.json Normal file
View File

@@ -0,0 +1,39 @@
{
"character_id": "bulma",
"character_name": "Bulma Briefs",
"identity": {
"base_specs": "1girl, slender build, fair skin",
"hair": "turquoise hair, ponytail",
"eyes": "blue eyes",
"expression": "energetic smile",
"hands": "turquoise nails",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "black playboy bunny",
"lower_body": "pantyhose",
"footwear": "red high heels",
"gloves": "detatched cuffs",
"accessories": "red hair ribbon, dragon radar"
},
"styles": {
"aesthetic": "retro-futuristic, anime, dragon ball style",
"primary_color": "pink",
"secondary_color": "turquoise",
"tertiary_color": "purple"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Dragon Ball"
]
}

39
characters/camilla.json Normal file
View File

@@ -0,0 +1,39 @@
{
"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",
"expression": "seductive smile",
"hands": "purple nails",
"arms": "",
"torso": "large breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "black headband with horns"
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "black armor, cleavage",
"lower_body": "black leggings, armored plates",
"footwear": "black armored boots",
"gloves": "",
"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"
]
}

39
characters/cammy.json Normal file
View File

@@ -0,0 +1,39 @@
{
"character_id": "cammy",
"character_name": "Cammy White",
"identity": {
"base_specs": "1girl, muscular build, fair skin",
"hair": "long blonde hair, twin braids",
"eyes": "blue eyes",
"expression": "serious look",
"hands": "green nails",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "scar on left cheek, green camouflage paint on legs"
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "green high-leg leotard",
"lower_body": "bare legs",
"footwear": "black combat boots, green socks",
"gloves": "red gauntlets",
"accessories": "red beret"
},
"styles": {
"aesthetic": "military, athletic, street fighter style",
"primary_color": "green",
"secondary_color": "red",
"tertiary_color": "black"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Street Fighter"
]
}

39
characters/chun_li.json Normal file
View File

@@ -0,0 +1,39 @@
{
"character_id": "chun_li",
"character_name": "Chun-Li",
"identity": {
"base_specs": "1girl, muscular build, fair skin, asian",
"hair": "black hair, hair buns",
"eyes": "brown eyes",
"expression": "determined smile",
"hands": "blue nails",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "thick thighs",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "blue qipao, gold embroidery, white accents",
"lower_body": "brown tights",
"footwear": "white combat boots",
"gloves": "",
"accessories": "white hair ribbons, spiked bracelets"
},
"styles": {
"aesthetic": "chinese style",
"primary_color": "blue",
"secondary_color": "white",
"tertiary_color": "gold"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Street Fighter"
]
}

39
characters/ciri.json Normal file
View File

@@ -0,0 +1,39 @@
{
"character_id": "ciri",
"character_name": "Ciri",
"identity": {
"base_specs": "1girl, athletic build",
"hair": "ashen grey hair, messy bun",
"eyes": "emerald green eyes, mascara",
"expression": "determined look",
"hands": "green nails",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "scar over eye"
},
"wardrobe": {
"inner_layer": "white blouse",
"outer_layer": "",
"lower_body": "brown leather trousers",
"footwear": "brown leather boots",
"gloves": "brown leather gloves",
"accessories": "silver sword on back, witcher medallion"
},
"styles": {
"aesthetic": "gritty, fantasy, witcher style",
"primary_color": "white",
"secondary_color": "brown",
"tertiary_color": "silver"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"The Witcher 3"
]
}

View File

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

39
characters/gold_city.json Normal file
View File

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

39
characters/gold_ship.json Normal file
View File

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

View File

@@ -0,0 +1,39 @@
{
"character_id": "hatsune_miku",
"character_name": "Hatsune Miku",
"identity": {
"base_specs": "1girl, slender build, fair skin",
"hair": "long turquoise hair, twin tails, floor-length",
"eyes": "turquoise eyes",
"expression": "cheerful smile",
"hands": "turquoise nails",
"arms": "01 tattoo on left shoulder",
"torso": "small breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "grey sleeveless shirt, turquoise tie",
"lower_body": "grey miniskirt, turquoise trim",
"footwear": "black thigh-high boots, turquoise trim",
"gloves": "black arm warmers, turquoise trim",
"accessories": "hair ornament, headset"
},
"styles": {
"aesthetic": "vocaloid, futuristic, anime style",
"primary_color": "teal",
"secondary_color": "grey",
"tertiary_color": "black"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Vocaloid"
]
}

View File

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

39
characters/jessie.json Normal file
View File

@@ -0,0 +1,39 @@
{
"character_id": "jessie_(pokemon)",
"character_name": "Jessie",
"identity": {
"base_specs": "1girl, slender build, fair skin",
"hair": "long magenta hair, curved back",
"eyes": "blue eyes",
"expression": "arrogant smirk",
"hands": "white nails",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "green earrings"
},
"wardrobe": {
"inner_layer": "black crop top",
"outer_layer": "white Team Rocket uniform jacket, bare stomach, red R logo",
"lower_body": "white miniskirt",
"footwear": "black thigh-high boots",
"gloves": "black elbow gloves",
"accessories": "green earrings"
},
"styles": {
"aesthetic": "villainous, anime, pokemon style",
"primary_color": "white",
"secondary_color": "magenta",
"tertiary_color": "black"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Pokemon"
]
}

39
characters/jinx.json Normal file
View File

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

View File

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

View File

@@ -0,0 +1,39 @@
{
"character_id": "kagari_atsuko",
"character_name": "Kagari Atsuko",
"identity": {
"base_specs": "1girl, slender build, fair skin",
"hair": "long brown hair, half-ponytail, bangs",
"eyes": "red eyes",
"expression": "determined smile",
"hands": "",
"arms": "",
"torso": "small breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "white shirt",
"outer_layer": "dark blue witch robes",
"lower_body": "dark blue skirt",
"footwear": "brown boots, white socks",
"gloves": "",
"accessories": "pointed witch hat, brown belt, magic wand"
},
"styles": {
"aesthetic": "fantasy, magical girl, little witch academia style",
"primary_color": "dark blue",
"secondary_color": "brown",
"tertiary_color": "red"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Little Witch Academia"
]
}

View File

@@ -0,0 +1,39 @@
{
"character_id": "k/da_all_out_ahri",
"character_name": "Ahri",
"identity": {
"base_specs": "1girl, slender build, fair skin, fox ears",
"hair": "long blonde hair, flowing",
"eyes": "yellow eyes",
"expression": "charming smile",
"hands": "silver nails",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "whisker markings on cheeks, crystal tails"
},
"wardrobe": {
"inner_layer": "silver crop top",
"outer_layer": "white and silver jacket",
"lower_body": "black leather shorts",
"footwear": "black thigh-high boots",
"gloves": "",
"accessories": "crystal heart, silver jewelry"
},
"styles": {
"aesthetic": "pop star, mystical, k/da style",
"primary_color": "silver",
"secondary_color": "white",
"tertiary_color": "blue"
},
"lora": {
"lora_name": "Illustrious/Looks/KDA AhriIlluLoRA.safetensors",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"League of Legends", "K/DA", "KDA", "K-Pop"
]
}

View File

@@ -0,0 +1,39 @@
{
"character_id": "k/da_all_out_akali",
"character_name": "Akali",
"identity": {
"base_specs": "1girl, athletic build, fair skin",
"hair": "long dark blue hair, blonde streaks, high ponytail",
"eyes": "blue eyes",
"expression": "cool, rebellious look",
"hands": "blue nails",
"arms": "tattoos on arms",
"torso": "small breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "black crop top",
"outer_layer": "blue and silver motorcycle jacket",
"lower_body": "black leather pants",
"footwear": "blue sneakers",
"gloves": "black fingerless gloves",
"accessories": "kama and kunai"
},
"styles": {
"aesthetic": "pop star, street, k/da style",
"primary_color": "blue",
"secondary_color": "purple",
"tertiary_color": "silver"
},
"lora": {
"lora_name": "Illustrious/Looks/KDAAkaliIlluLoRA.safetensors",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"League of Legends", "K/DA", "KDA", "K-Pop"
]
}

View File

@@ -0,0 +1,39 @@
{
"character_id": "k/da_all_out_evelynn",
"character_name": "Evelynn",
"identity": {
"base_specs": "1girl, curvaceous build, fair skin",
"hair": "light blue hair,",
"eyes": "yellow glowing eyes, slit pupils",
"expression": "seductive, confident look",
"hands": "metal claws",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "two long lashers (shadow tendrils)"
},
"wardrobe": {
"inner_layer": "black leather bra",
"outer_layer": "iridescent blue jacket, fur collar",
"lower_body": "black leather skirt",
"footwear": "black high-heeled boots",
"gloves": "",
"accessories": "diamond earrings"
},
"styles": {
"aesthetic": "pop star, glamorous, k/da style",
"primary_color": "blue",
"secondary_color": "purple",
"tertiary_color": "silver"
},
"lora": {
"lora_name": "Illustrious/Looks/KDA EvelynnIlluLoRA.safetensors",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"League of Legends", "K/DA", "KDA", "K-Pop"
]
}

View File

@@ -0,0 +1,39 @@
{
"character_id": "k/da_all_out_kai'sa",
"character_name": "Kai'Sa",
"identity": {
"base_specs": "1girl, athletic build, fair skin",
"hair": "long hair, purple hair, hair ornament, ponytail, green highlights",
"eyes": "purple eyes",
"expression": "focused expression",
"hands": "silver nails",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "silver bodysuit",
"outer_layer": "white and silver jacket",
"lower_body": "silver leggings",
"footwear": "silver high-heeled boots",
"gloves": "",
"accessories": "crystal shoulder pods"
},
"styles": {
"aesthetic": "pop star, futuristic, k/da style",
"primary_color": "silver",
"secondary_color": "white",
"tertiary_color": "purple"
},
"lora": {
"lora_name": "Illustrious/Looks/KDA KaisaIlluLoRA.safetensors",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"League of Legends", "K/DA", "KDA", "K-Pop"
]
}

View File

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

View File

@@ -0,0 +1,39 @@
{
"character_id": "lara_croft_classic",
"character_name": "Lara Croft",
"identity": {
"base_specs": "1girl, athletic build,",
"hair": "long brown hair, single braid",
"eyes": "brown eyes",
"expression": "light smile, raised eyebrow",
"hands": "",
"arms": "",
"torso": "large breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "teal tank top,",
"lower_body": "brown shorts",
"footwear": "brown combat boots, red laces",
"gloves": "black fingerless gloves",
"accessories": "dual thigh pistol holsters, brown leatherbackpack, red circular sunglasses"
},
"styles": {
"aesthetic": "adventure, retro, 90s style",
"primary_color": "teal",
"secondary_color": "brown",
"tertiary_color": "black"
},
"lora": {
"lora_name": "Illustrious/Looks/LaraCroft_ClassicV2_Illu_Dwnsty.safetensors",
"lora_weight": 0.8,
"lora_triggers": ""
},
"tags": [
"Tomb Raider"
]
}

View File

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

39
characters/lulu.json Normal file
View File

@@ -0,0 +1,39 @@
{
"character_id": "lulu (ff10)",
"character_name": "Lulu",
"identity": {
"base_specs": "1girl, curvaceous build, fair skin",
"hair": "long black hair, complex braids, hairpins",
"eyes": "red eyes",
"expression": "thinking, raised eyebrow",
"hands": "black nails",
"arms": "",
"torso": "large breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "dark purple lipstick"
},
"wardrobe": {
"inner_layer": "black corset",
"outer_layer": "black fur-trimmed dress, many belts on front",
"lower_body": "long skirt made of belts",
"footwear": "black boots",
"gloves": "",
"accessories": "moogle doll, silver jewelry"
},
"styles": {
"aesthetic": "gothic, ornate, final fantasy x style",
"primary_color": "black",
"secondary_color": "white",
"tertiary_color": "purple"
},
"lora": {
"lora_name": "Illustrious/Looks/Lulu DG illuLoRA_1337272.safetensors",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Final Fantasy X"
]
}

View File

@@ -0,0 +1,39 @@
{
"character_id": "majin_android_21",
"character_name": "Majin Android 21",
"identity": {
"base_specs": "1girl, curvaceous build, pink skin",
"hair": "long voluminous white hair",
"eyes": "red eyes, black sclera",
"expression": "evil smile",
"hands": "black claws, pink nails",
"arms": "",
"torso": "large breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "pink skin, long tail, pointy ears"
},
"wardrobe": {
"inner_layer": "black tube top",
"outer_layer": "",
"lower_body": "white harem pants",
"footwear": "black and yellow boots",
"gloves": "black sleeves",
"accessories": "gold bracelets, gold neck ring, hoop earrings"
},
"styles": {
"aesthetic": "supernatural, anime, dragon ball style",
"primary_color": "pink",
"secondary_color": "white",
"tertiary_color": "gold"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Dragon Ball FighterZ"
]
}

View File

@@ -0,0 +1,39 @@
{
"character_id": "marin_kitagawa",
"character_name": "Marin Kitagawa",
"identity": {
"base_specs": "1girl, slender build, fair skin, asian",
"hair": "long blonde hair, pink tips",
"eyes": "pink eyes (contacts)",
"expression": "excited smile",
"hands": "long pink nails",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "piercings"
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "white school shirt, loosely tied blue tie",
"lower_body": "blue plaid miniskirt",
"footwear": "black loafers, black socks",
"gloves": "",
"accessories": "choker, various bracelets"
},
"styles": {
"aesthetic": "gyaru, modern, anime style",
"primary_color": "white",
"secondary_color": "blue",
"tertiary_color": "pink"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"My Dress-Up Darling"
]
}

View File

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

39
characters/meiko.json Normal file
View File

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

39
characters/nessa.json Normal file
View File

@@ -0,0 +1,39 @@
{
"character_id": "nessa",
"character_name": "Nessa",
"identity": {
"base_specs": "1girl, athletic build, dark skin",
"hair": "long hair, light blue highlights",
"eyes": "blue eyes",
"expression": "confident smile",
"hands": "blue nails",
"arms": "",
"torso": "small breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "blue earrings"
},
"wardrobe": {
"inner_layer": "white and blue bikini top",
"outer_layer": "gym uniform, number 049",
"lower_body": "white and blue shorts",
"footwear": "blue and white sandals",
"gloves": "",
"accessories": "wristband, life buoy, pokeball"
},
"styles": {
"aesthetic": "sporty, aquatic, pokemon style",
"primary_color": "blue",
"secondary_color": "white",
"tertiary_color": "orange"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Pokemon"
]
}

View File

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

View File

@@ -0,0 +1,39 @@
{
"character_id": "princess_peach",
"character_name": "Princess Peach",
"identity": {
"base_specs": "1girl, slender build, fair skin",
"hair": "long blonde hair, voluminous, crown",
"eyes": "blue eyes, long eyelashes",
"expression": "gentle smile",
"hands": "",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "pink lips, blue earrings"
},
"wardrobe": {
"inner_layer": "white petticoat",
"outer_layer": "pink floor-length ball gown, puffy sleeves, dark pink panniers",
"lower_body": "long skirt",
"footwear": "red high heels",
"gloves": "white opera gloves",
"accessories": "gold crown with red and blue jewels, blue brooch"
},
"styles": {
"aesthetic": "royal, whimsical, nintendo style",
"primary_color": "pink",
"secondary_color": "gold",
"tertiary_color": "blue"
},
"lora": {
"lora_name": "Illustrious/Looks/Princess_Peach_Shiny_Style_V4.0_Illustrious_1652958.safetensors",
"lora_weight": 0.8,
"lora_triggers": "princess peach, crown, pink dress, shiny skin, royal elegance"
},
"tags": [
"Super Mario"
]
}

View File

@@ -0,0 +1,39 @@
{
"character_id": "princess_zelda_botw",
"character_name": "Princess Zelda",
"identity": {
"base_specs": "1girl, slender build, fair skin, pointed ears",
"hair": "long blonde hair, braided, gold hair clips",
"eyes": "green eyes",
"expression": "curious",
"hands": "gold nails",
"arms": "",
"torso": "small breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "tri-force symbol, elf ears"
},
"wardrobe": {
"inner_layer": "blue tunic",
"outer_layer": "blue champion's tunic, brown leather belts",
"lower_body": "tan trousers",
"footwear": "brown leather boots",
"gloves": "brown fingerless gloves",
"accessories": "sheikah slate, gold jewelry"
},
"styles": {
"aesthetic": "fantasy, adventurous, zelda style",
"primary_color": "blue",
"secondary_color": "gold",
"tertiary_color": "brown"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"The Legend of Zelda"
]
}

View File

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

39
characters/riju.json Normal file
View File

@@ -0,0 +1,39 @@
{
"character_id": "riju",
"character_name": "Riju",
"identity": {
"base_specs": "1girl, young, dark skin, gerudo",
"hair": "short red hair, braided ponytail, gold hair ornament",
"eyes": "green eyes",
"expression": "serious",
"hands": "",
"arms": "",
"torso": "small breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "darkblue lipstick,"
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "black top, blue sash",
"lower_body": "black skirt, pelvic curtain,",
"footwear": "gold high heels",
"gloves": "",
"accessories": "gold jewelry, earrings"
},
"styles": {
"aesthetic": "fantasy, desert, gerudo style",
"primary_color": "gold",
"secondary_color": "black",
"tertiary_color": "red"
},
"lora": {
"lora_name": "",
"lora_weight": 0.8,
"lora_triggers": ""
},
"tags": [
"The Legend of Zelda"
]
}

39
characters/rosalina.json Normal file
View File

@@ -0,0 +1,39 @@
{
"character_id": "rosalina",
"character_name": "Rosalina",
"identity": {
"base_specs": "1girl, tall, slender build, fair skin",
"hair": "long platinum blonde hair, side-swept bangs covering one eye",
"eyes": "light blue eyes",
"expression": "serene expression",
"hands": "turquoise nails",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "star-shaped earrings"
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "turquoise off-the-shoulder gown, silver trim",
"lower_body": "long skirt",
"footwear": "silver high heels",
"gloves": "",
"accessories": "silver crown with blue jewels, star wand, luma"
},
"styles": {
"aesthetic": "celestial, elegant, nintendo style",
"primary_color": "turquoise",
"secondary_color": "silver",
"tertiary_color": "yellow"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Super Mario"
]
}

View File

@@ -0,0 +1,39 @@
{
"character_id": "rouge_the_bat",
"character_name": "Rouge the Bat",
"identity": {
"base_specs": "1girl, anthro, bat girl, white fur",
"hair": "short white hair",
"eyes": "teal eyes",
"expression": "sly smirk",
"hands": "white gloves",
"arms": "",
"torso": "large breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "bat wings, eyeshadow"
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "black skin-tight jumpsuit, pink heart-shaped chest plate, bare shoulders, cleavage",
"lower_body": "jumpsuit",
"footwear": "white boots, pink heart motifs",
"gloves": "white gloves, pink cuffs",
"accessories": "blue eyeshadow"
},
"styles": {
"aesthetic": "jewels, museum,sleek, spy, sonic style",
"primary_color": "white",
"secondary_color": "pink",
"tertiary_color": "black"
},
"lora": {
"lora_name": "Illustrious/Looks/Rouge_the_bat_v2.safetensors",
"lora_weight": 0.8,
"lora_triggers": ""
},
"tags": [
"Sonic the Hedgehog"
]
}

View File

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

View File

@@ -0,0 +1,39 @@
{
"character_id": "samus_aran",
"character_name": "Samus Aran",
"identity": {
"base_specs": "1girl, athletic build, fair skin",
"hair": "long blonde hair, ponytail",
"eyes": "blue eyes",
"expression": "serious expression",
"hands": "blue nails",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "beauty mark on chin"
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "blue skin-tight bodysuit, pink symbols",
"lower_body": "bodysuit",
"footwear": "blue high-heeled boots",
"gloves": "zero suit",
"accessories": "paralyzer pistol"
},
"styles": {
"aesthetic": "sci-fi, sleek, metroid style",
"primary_color": "blue",
"secondary_color": "pink",
"tertiary_color": "yellow"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Metroid"
]
}

View File

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

39
characters/shantae.json Normal file
View File

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

View File

@@ -0,0 +1,39 @@
{
"character_id": "sucy_manbavaran",
"character_name": "Sucy Manbavaran",
"identity": {
"base_specs": "1girl, lanky build, pale skin",
"hair": "light purple hair, hair covering one eye",
"eyes": "red eyes",
"expression": "deadpan expression",
"hands": "black nails",
"arms": "",
"torso": "small breasts",
"pelvis": "narrow waist",
"legs": "",
"feet": "",
"distinguishing_marks": "dark circles under eyes"
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "dark purple witch robes",
"lower_body": "long skirt with frayed edges",
"footwear": "brown boots",
"gloves": "",
"accessories": "pointed witch hat, potion bottle"
},
"styles": {
"aesthetic": "gothic, whimsical, little witch academia style",
"primary_color": "purple",
"secondary_color": "mauve",
"tertiary_color": "green"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Little Witch Academia"
]
}

View File

@@ -0,0 +1,39 @@
{
"character_id": "tifa_lockhart",
"character_name": "Tifa Lockhart",
"identity": {
"base_specs": "1girl, athletic build, fair skin",
"hair": "long black hair, tied end",
"eyes": "red eyes",
"expression": "kind smile",
"hands": "dark red nails",
"arms": "",
"torso": "large breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "black sports bra",
"outer_layer": "white tank top, black suspenders",
"lower_body": "black miniskirt",
"footwear": "red boots, black socks",
"gloves": "red fingerless gloves",
"accessories": "silver earrings"
},
"styles": {
"aesthetic": "urban, martial arts, final fantasy style",
"primary_color": "white",
"secondary_color": "black",
"tertiary_color": "red"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Final Fantasy VII"
]
}

39
characters/tracer.json Normal file
View File

@@ -0,0 +1,39 @@
{
"character_id": "tracer",
"character_name": "Tracer",
"identity": {
"base_specs": "1girl, slender build, fair skin",
"hair": "short spiky brown hair",
"eyes": "brown eyes",
"expression": "energetic smile",
"hands": "",
"arms": "",
"torso": "small breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "freckles"
},
"wardrobe": {
"inner_layer": "orange leggings",
"outer_layer": "brown flight jacket, yellow vest",
"lower_body": "orange leggings",
"footwear": "white and orange sneakers",
"gloves": "",
"accessories": "chronal accelerator, yellow goggles"
},
"styles": {
"aesthetic": "sci-fi, pilot, overwatch style",
"primary_color": "orange",
"secondary_color": "brown",
"tertiary_color": "white"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Overwatch"
]
}

39
characters/urbosa.json Normal file
View File

@@ -0,0 +1,39 @@
{
"character_id": "urbosa",
"character_name": "Urbosa",
"identity": {
"base_specs": "1girl, tall, muscular, dark skin, gerudo",
"hair": "long red hair, wild hair",
"eyes": "green eyes",
"expression": "confident",
"hands": "gold nails",
"arms": "muscular arms",
"torso": "abs, mediumS breasts",
"pelvis": "wide hips",
"legs": "muscular legs",
"feet": "",
"distinguishing_marks": "dark blue lipstick, gerudo markings"
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "blue top, blue champion's skirt, green sash, green shoulder guards,",
"lower_body": "blue skirt",
"footwear": "gold heels",
"gloves": "",
"accessories": "gold jewelry, scimitar"
},
"styles": {
"aesthetic": "fantasy, warrior, gerudo style",
"primary_color": "gold",
"secondary_color": "blue",
"tertiary_color": "red"
},
"lora": {
"lora_name": "",
"lora_weight": 0.8,
"lora_triggers": ""
},
"tags": [
"The Legend of Zelda"
]
}

View File

@@ -0,0 +1,39 @@
{
"character_id": "widowmaker",
"character_name": "Widowmaker",
"identity": {
"base_specs": "1girl, slender build, blue skin",
"hair": "long purple hair, ponytail",
"eyes": "yellow eyes",
"expression": "cold expression",
"hands": "",
"arms": "spider tattoo on arm",
"torso": "large breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "blue skin"
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "purple tactical bodysuit, plunging neckline",
"lower_body": "bodysuit",
"footwear": "purple high-heeled boots",
"gloves": "purple gauntlets",
"accessories": "sniper visor, grappling hook"
},
"styles": {
"aesthetic": "sci-fi, assassin, overwatch style",
"primary_color": "purple",
"secondary_color": "black",
"tertiary_color": "pink"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Overwatch"
]
}

39
characters/yor_briar.json Normal file
View File

@@ -0,0 +1,39 @@
{
"character_id": "yor_briar",
"character_name": "Yor Briar",
"identity": {
"base_specs": "1girl, slender build, fair skin",
"hair": "long black hair, styled with gold headband",
"eyes": "red eyes",
"expression": "gentle yet mysterious smile",
"hands": "black nails",
"arms": "",
"torso": "medium breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "black backless halter dress, red rose pattern inside",
"lower_body": "black thigh-high boots",
"footwear": "black boots",
"gloves": "black fingerless gloves",
"accessories": "gold rose-themed headband, gold needle weapons"
},
"styles": {
"aesthetic": "elegant, assassin, spy x family style",
"primary_color": "black",
"secondary_color": "red",
"tertiary_color": "gold"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Spy x Family"
]
}

View File

@@ -0,0 +1,39 @@
{
"character_id": "y'shtola_rhul",
"character_name": "Y'shtola Rhul",
"identity": {
"base_specs": "1girl, miqo'te, slender build, fair skin, cat ears",
"hair": "short white hair, bangs",
"eyes": "blind, white eyes",
"expression": "stoic expression",
"hands": "black nails",
"arms": "",
"torso": "small breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "facial markings, cat tail"
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "black sorceress robes, fur trim",
"lower_body": "long skirt",
"footwear": "black boots",
"gloves": "",
"accessories": "wooden staff"
},
"styles": {
"aesthetic": "magical, scholarly, final fantasy xiv style",
"primary_color": "black",
"secondary_color": "white",
"tertiary_color": "purple"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Final Fantasy XIV"
]
}

View File

@@ -0,0 +1,39 @@
{
"character_id": "yuffie_kisaragi",
"character_name": "Yuffie Kisaragi",
"identity": {
"base_specs": "1girl, slender build, fair skin",
"hair": "short black hair, bob cut",
"eyes": "brown eyes",
"expression": "playful grin",
"hands": "",
"arms": "black sleeve on one arm",
"torso": "small breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": "headband"
},
"wardrobe": {
"inner_layer": "",
"outer_layer": "green turtleneck sweater vest, midriff",
"lower_body": "beige shorts",
"footwear": "boots, socks",
"gloves": "fingerless glove on one hand, large gauntlet on one arm",
"accessories": "shuriken"
},
"styles": {
"aesthetic": "ninja, adventurer, final fantasy style",
"primary_color": "green",
"secondary_color": "beige",
"tertiary_color": "black"
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
},
"tags": [
"Final Fantasy VII"
]
}

39
characters/yuna_ffx.json Normal file
View File

@@ -0,0 +1,39 @@
{
"character_id": "yuna_(ff10)",
"character_name": "Yuna",
"identity": {
"base_specs": "1girl, slender, fair skin",
"hair": "short brown hair, bob cut",
"eyes": "heterochromia, blue eye, green eye",
"expression": "gentle",
"hands": "",
"arms": "",
"torso": "small breasts",
"pelvis": "",
"legs": "",
"feet": "",
"distinguishing_marks": ""
},
"wardrobe": {
"inner_layer": "white kimono top, yellow obi",
"outer_layer": "",
"lower_body": "long blue skirt, floral pattern",
"footwear": "boots",
"gloves": "detached sleeves",
"accessories": "summoner staff, necklace"
},
"styles": {
"aesthetic": "fantasy, final fantasy x style",
"primary_color": "white",
"secondary_color": "blue",
"tertiary_color": "yellow"
},
"lora": {
"lora_name": "",
"lora_weight": 0.8,
"lora_triggers": ""
},
"tags": [
"Final Fantasy X"
]
}

173
comfy_workflow.json Normal file
View File

@@ -0,0 +1,173 @@
{
"3": {
"inputs": {
"seed": 8566257,
"steps": 20,
"cfg": 3.5,
"sampler_name": "euler_ancestral",
"scheduler": "normal",
"denoise": 1,
"model": ["4", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["5", 0]
},
"class_type": "KSampler"
},
"4": {
"inputs": {
"ckpt_name": "Noob/oneObsession_v19Atypical.safetensors"
},
"class_type": "CheckpointLoaderSimple"
},
"5": {
"inputs": {
"width": 1024,
"height": 1024,
"batch_size": 1
},
"class_type": "EmptyLatentImage"
},
"6": {
"inputs": {
"text": "embedding:Illustrious/lazypos_1733353, {{POSITIVE_PROMPT}}",
"clip": ["4", 1]
},
"class_type": "CLIPTextEncode"
},
"7": {
"inputs": {
"text": "embedding:Illustrious/lazyneg_1760455, embedding:Illustrious/lazyhand",
"clip": ["4", 1]
},
"class_type": "CLIPTextEncode"
},
"8": {
"inputs": {
"samples": ["3", 0],
"vae": ["4", 2]
},
"class_type": "VAEDecode"
},
"9": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": ["13", 0]
},
"class_type": "SaveImage"
},
"10": {
"inputs": {
"model_name": "bbox/face_yolov9c.pt"
},
"class_type": "UltralyticsDetectorProvider"
},
"11": {
"inputs": {
"guide_size": 384,
"guide_size_for": "bbox",
"max_size": 1024,
"seed": 123456,
"steps": 20,
"cfg": 3.5,
"sampler_name": "euler_ancestral",
"scheduler": "normal",
"denoise": 0.5,
"feather": 5,
"noise_mask": true,
"force_inpaint": true,
"bbox_threshold": 0.5,
"bbox_dilation": 10,
"bbox_crop_factor": 3,
"sam_threshold": 0.6,
"sam_dilation": 0,
"sam_mask_hint_threshold": 0.7,
"sam_mask_hint_use_negative": "False",
"sam_bbox_expansion": 0,
"sam_detection_hint": "none",
"sam_mask_hint_area": "bbox",
"drop_size": 10,
"wildcard": "",
"cycle": 1,
"inpaint_model": false,
"noise_mask_feather": 20,
"image": ["8", 0],
"model": ["4", 0],
"clip": ["4", 1],
"vae": ["4", 2],
"positive": ["14", 0],
"negative": ["7", 0],
"bbox_detector": ["10", 0]
},
"class_type": "FaceDetailer"
},
"12": {
"inputs": {
"model_name": "bbox/hand_yolov8s.pt"
},
"class_type": "UltralyticsDetectorProvider"
},
"13": {
"inputs": {
"guide_size": 384,
"guide_size_for": "bbox",
"max_size": 1024,
"seed": 123456,
"steps": 20,
"cfg": 3.5,
"sampler_name": "euler_ancestral",
"scheduler": "normal",
"denoise": 0.5,
"feather": 5,
"noise_mask": true,
"force_inpaint": true,
"bbox_threshold": 0.5,
"bbox_dilation": 10,
"bbox_crop_factor": 3,
"sam_threshold": 0.6,
"sam_dilation": 0,
"sam_mask_hint_threshold": 0.7,
"sam_mask_hint_use_negative": "False",
"sam_bbox_expansion": 0,
"sam_detection_hint": "none",
"sam_mask_hint_area": "bbox",
"drop_size": 10,
"wildcard": "",
"cycle": 1,
"inpaint_model": false,
"noise_mask_feather": 20,
"image": ["11", 0],
"model": ["4", 0],
"clip": ["4", 1],
"vae": ["4", 2],
"positive": ["15", 0],
"negative": ["7", 0],
"bbox_detector": ["12", 0]
},
"class_type": "FaceDetailer"
},
"14": {
"inputs": {
"text": "{{FACE_PROMPT}}",
"clip": ["4", 1]
},
"class_type": "CLIPTextEncode"
},
"15": {
"inputs": {
"text": "{{HAND_PROMPT}}",
"clip": ["4", 1]
},
"class_type": "CLIPTextEncode"
},
"16": {
"inputs": {
"lora_name": "",
"strength_model": 1.0,
"strength_clip": 1.0,
"model": ["4", 0],
"clip": ["4", 1]
},
"class_type": "LoraLoader"
}
}

32
launch.sh Normal file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
# Exit on error
set -e
VENV_DIR="venv"
echo "Checking for virtual environment..."
if [ ! -d "$VENV_DIR" ]; then
echo "Creating virtual environment..."
python3 -m venv "$VENV_DIR"
fi
echo "Activating virtual environment..."
source "$VENV_DIR/bin/activate"
if [ "$1" == "--clean" ]; then
echo "Performing clean start..."
echo "Removing database..."
rm -f database.db
echo "Clearing uploads..."
rm -rf static/uploads/*
fi
echo "Upgrading pip and setuptools..."
pip install --upgrade pip setuptools wheel
echo "Installing/Updating requirements..."
pip install -r requirements.txt
echo "Starting Character Browser..."
python3 app.py

15
models.py Normal file
View File

@@ -0,0 +1,15 @@
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class Character(db.Model):
id = db.Column(db.Integer, primary_key=True)
character_id = db.Column(db.String(100), unique=True, nullable=False)
slug = db.Column(db.String(100), unique=True, nullable=False)
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'<Character {self.character_id}>'

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
Flask
Flask-SQLAlchemy
Pillow
requests

288
templates/detail.html Normal file
View File

@@ -0,0 +1,288 @@
{% extends "layout.html" %}
{% block content %}
<div class="row">
<div class="col-md-4">
<div class="card mb-4">
<div class="img-container" style="height: auto; min-height: 400px;">
{% if character.image_path %}
<img src="{{ url_for('static', filename='uploads/' + character.image_path) }}" alt="{{ character.name }}" class="img-fluid">
{% else %}
<span class="text-muted">No Image Attached</span>
{% endif %}
</div>
<div class="card-body">
<form action="{{ url_for('upload_image', slug=character.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>
<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="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>
</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">Latest Preview</div>
<div class="card-body p-0">
<div class="img-container" style="height: auto; min-height: 400px;">
<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">Latest Preview</div>
<div class="card-body p-0">
<div class="img-container" style="height: auto; min-height: 400px;">
<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 character.default_fields is not none %}
{% if 'special::tags' in character.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 character.data.tags %}
<span class="badge bg-secondary">{{ tag }}</span>
{% endfor %}
</div>
</div>
</div>
<div class="col-md-8">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>{{ character.name }}</h1>
<a href="/" class="btn btn-outline-secondary">Back to Gallery</a>
</div>
<form id="generate-form" action="{{ url_for('generate_image', slug=character.slug) }}" method="post">
{% for section, details in character.data.items() %}
{% if section not in ['character_id', 'tags', 'name'] and details is mapping %}
<div class="card mb-4">
<div class="card-header bg-light text-capitalize"><strong>{{ section.replace('_', ' ') }}</strong></div>
<div class="card-body">
<dl class="row mb-0">
{% if section == 'identity' %}
<dt class="col-sm-4 text-capitalize">
<input class="form-check-input me-1" type="checkbox" name="include_field" value="special::name"
{% if preferences is not none %}
{% if 'special::name' in preferences %}checked{% endif %}
{% elif character.default_fields is not none %}
{% if 'special::name' in character.default_fields %}checked{% endif %}
{% else %}
checked
{% endif %}>
Character ID
</dt>
<dd class="col-sm-8">{{ character.character_id }}</dd>
{% endif %}
{% for key, value in details.items() %}
<dt class="col-sm-4 text-capitalize">
<input class="form-check-input me-1" type="checkbox" name="include_field" value="{{ section }}::{{ key }}"
{% if preferences is not none %}
{% if section + '::' + key in preferences %}checked{% endif %}
{% elif character.default_fields is not none %}
{% if section + '::' + key in character.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 %}
{% endfor %}
</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 = 'detail_view_' + 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' && submitter.value !== 'replace')) {
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.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 = `/character/{{ character.slug }}/finalize_generation/${promptId}`;
const formData = new FormData();
formData.append('action', action);
try {
const response = await fetch(url, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
if (action === 'preview') {
previewImg.src = data.image_url;
if (previewCard) previewCard.classList.remove('d-none');
} else {
// Reload for cover update
window.location.reload();
}
} else {
alert('Save failed: ' + data.error);
}
} catch (err) {
console.error(err);
alert('Finalize request failed');
} finally {
progressContainer.classList.add('d-none');
}
}
});
</script>
{% endblock %}

70
templates/generator.html Normal file
View File

@@ -0,0 +1,70 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-5">
<div class="card mb-4">
<div class="card-header bg-primary text-white">Generator Settings</div>
<div class="card-body">
<form action="{{ url_for('generator') }}" method="post">
<div class="mb-3">
<label for="character" class="form-label">Character</label>
<select class="form-select" id="character" name="character" required>
<option value="" disabled {% if not selected_char %}selected{% endif %}>Select a character...</option>
{% for char in characters %}
<option value="{{ char.slug }}" {% if selected_char == char.slug %}selected{% endif %}>{{ char.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="checkpoint" class="form-label">Checkpoint Model</label>
<select class="form-select" id="checkpoint" name="checkpoint" required>
{% for ckpt in checkpoints %}
<option value="{{ ckpt }}" {% if selected_ckpt == ckpt %}selected{% endif %}>{{ ckpt }}</option>
{% endfor %}
</select>
<div class="form-text">Listing models from Illustrious/ folder</div>
</div>
<div class="mb-3">
<label for="positive_prompt" class="form-label">Additional Positive Prompt</label>
<textarea class="form-control" id="positive_prompt" name="positive_prompt" rows="3" placeholder="e.g. sitting in a cafe, drinking coffee, daylight"></textarea>
</div>
<div class="mb-3">
<label for="negative_prompt" class="form-label">Additional Negative Prompt</label>
<textarea class="form-control" id="negative_prompt" name="negative_prompt" rows="3" placeholder="e.g. bad hands, extra digits"></textarea>
</div>
<button type="submit" class="btn btn-primary w-100">Generate</button>
</form>
</div>
</div>
</div>
<div class="col-md-7">
<div class="card">
<div class="card-header bg-dark text-white">Result</div>
<div class="card-body p-0 d-flex align-items-center justify-content-center" style="min-height: 500px; background-color: #eee;">
{% if generated_image %}
<div class="img-container w-100 h-100">
<img src="{{ url_for('static', filename='uploads/' + generated_image) }}" alt="Generated Result" class="img-fluid w-100">
</div>
{% else %}
<div class="text-center text-muted">
<p>Select settings and click Generate</p>
</div>
{% endif %}
</div>
{% if generated_image %}
<div class="card-footer">
<small class="text-muted">Saved to character gallery</small>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

178
templates/index.html Normal file
View File

@@ -0,0 +1,178 @@
{% extends "layout.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Character Gallery</h2>
<div class="d-flex">
<button id="batch-generate-btn" class="btn btn-outline-success me-2">Generate Missing Covers</button>
<button id="regenerate-all-btn" class="btn btn-outline-danger me-2">Regenerate All Covers</button>
<form action="{{ url_for('rescan') }}" method="post">
<button type="submit" class="btn btn-outline-primary">Rescan Character Files</button>
</form>
</div>
</div>
<!-- Batch Progress Bar -->
<div id="batch-progress-container" class="card mb-4 d-none">
<div class="card-body">
<h5 id="batch-status-text">Batch Generating...</h5>
<div class="progress mt-2" role="progressbar" aria-label="Batch Progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<div id="batch-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" style="width: 0%">0%</div>
</div>
<p id="current-char-name" class="small text-muted mt-2 mb-0"></p>
</div>
</div>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-4">
{% for char in characters %}
<div class="col" id="card-{{ char.slug }}">
<div class="card h-100 character-card" onclick="window.location.href='/character/{{ char.slug }}'">
<div class="img-container">
{% if char.image_path %}
<img id="img-{{ char.slug }}" src="{{ url_for('static', filename='uploads/' + char.image_path) }}" alt="{{ char.name }}">
<span id="no-img-{{ char.slug }}" class="text-muted d-none">No Image</span>
{% else %}
<img id="img-{{ char.slug }}" src="" alt="{{ char.name }}" class="d-none">
<span id="no-img-{{ char.slug }}" class="text-muted">No Image</span>
{% endif %}
</div>
<div class="card-body">
<h5 class="card-title text-center">{{ char.name }}</h5>
<p class="card-text small text-center text-muted">{{ char.data.tags | join(', ') }}</p>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', () => {
const batchBtn = document.getElementById('batch-generate-btn');
const regenAllBtn = document.getElementById('regenerate-all-btn');
const progressBar = document.getElementById('batch-progress-bar');
const container = document.getElementById('batch-progress-container');
const statusText = document.getElementById('batch-status-text');
const charNameText = document.getElementById('current-char-name');
const clientId = 'gallery_batch_' + Math.random().toString(36).substring(2, 15);
const socket = new WebSocket(`ws://127.0.0.1:8188/ws?clientId=${clientId}`);
let currentPromptId = null;
let resolveGeneration = null;
socket.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'executing') {
if (msg.data.node === null && msg.data.prompt_id === currentPromptId) {
if (resolveGeneration) resolveGeneration();
}
}
});
async function waitForCompletion(promptId) {
return new Promise((resolve) => {
const checkResolve = () => {
clearInterval(pollInterval);
resolve();
};
resolveGeneration = checkResolve;
const pollInterval = setInterval(async () => {
try {
const resp = await fetch(`/check_status/${promptId}`);
const data = await resp.json();
if (data.status === 'finished') {
checkResolve();
}
} catch (err) {}
}, 2000);
});
}
async function runBatch() {
const response = await fetch('/get_missing_characters');
const data = await response.json();
const missing = data.missing;
if (missing.length === 0) {
alert("No characters missing cover images.");
return;
}
batchBtn.disabled = true;
regenAllBtn.disabled = true;
container.classList.remove('d-none');
let completed = 0;
for (const char of missing) {
completed++;
const percent = Math.round((completed / missing.length) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
statusText.textContent = `Batch Generating: ${completed} / ${missing.length}`;
charNameText.textContent = `Current: ${char.name}`;
try {
const genResp = await fetch(`/character/${char.slug}/generate`, {
method: 'POST',
body: new URLSearchParams({ 'action': 'replace', 'client_id': clientId }),
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const genData = await genResp.json();
currentPromptId = genData.prompt_id;
await waitForCompletion(currentPromptId);
const finResp = await fetch(`/character/${char.slug}/finalize_generation/${currentPromptId}`, {
method: 'POST',
body: new URLSearchParams({ 'action': 'replace' })
});
const finData = await finResp.json();
if (finData.success) {
const img = document.getElementById(`img-${char.slug}`);
const noImgSpan = document.getElementById(`no-img-${char.slug}`);
if (img) {
img.src = finData.image_url;
img.classList.remove('d-none');
}
if (noImgSpan) noImgSpan.classList.add('d-none');
}
} catch (err) {
console.error(`Failed for ${char.name}:`, err);
}
}
statusText.textContent = "Batch Complete!";
charNameText.textContent = "";
batchBtn.disabled = false;
regenAllBtn.disabled = false;
setTimeout(() => { container.classList.add('d-none'); }, 5000);
}
batchBtn.addEventListener('click', async () => {
const response = await fetch('/get_missing_characters');
const data = await response.json();
if (data.missing.length === 0) {
alert("No characters missing cover images.");
return;
}
if (!confirm(`Generate cover images for ${data.missing.length} characters?`)) return;
runBatch();
});
regenAllBtn.addEventListener('click', async () => {
if (!confirm("This will unassign ALL current cover images and generate new ones for every character. Existing files will be kept on disk. Proceed?")) return;
const clearResp = await fetch('/clear_all_covers', { method: 'POST' });
if (clearResp.ok) {
// Update UI to show "No Image" for all
document.querySelectorAll('.img-container img').forEach(img => img.classList.add('d-none'));
document.querySelectorAll('.img-container .text-muted').forEach(span => span.classList.remove('d-none'));
runBatch();
}
});
});
</script>
{% endblock %}

41
templates/layout.html Normal file
View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Character Browser</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background-color: #f8f9fa; }
.character-card { transition: transform 0.2s; cursor: pointer; }
.character-card:hover { transform: scale(1.02); }
.img-container { height: 300px; overflow: hidden; background-color: #dee2e6; display: flex; align-items: center; justify-content: center; }
.img-container img { width: 100%; height: 100%; object-fit: cover; }
</style>
</head>
<body>
<nav class="navbar navbar-dark bg-dark mb-4">
<div class="container">
<a class="navbar-brand" href="/">Character Browser</a>
<div class="d-flex">
<a href="/generator" class="btn btn-outline-light me-2">Generator</a>
</div>
</div>
</nav>
<div class="container">
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-info">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>