Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a38915b354 |
315
app.py
315
app.py
@@ -385,6 +385,7 @@ def inject_default_checkpoint():
|
|||||||
@app.route('/set_default_checkpoint', methods=['POST'])
|
@app.route('/set_default_checkpoint', methods=['POST'])
|
||||||
def set_default_checkpoint():
|
def set_default_checkpoint():
|
||||||
session['default_checkpoint'] = request.form.get('checkpoint_path', '')
|
session['default_checkpoint'] = request.form.get('checkpoint_path', '')
|
||||||
|
session.modified = True
|
||||||
return {'status': 'ok'}
|
return {'status': 'ok'}
|
||||||
|
|
||||||
|
|
||||||
@@ -401,6 +402,27 @@ def api_status_comfyui():
|
|||||||
return {'status': 'error'}
|
return {'status': 'error'}
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/comfyui/loaded_checkpoint')
|
||||||
|
def api_comfyui_loaded_checkpoint():
|
||||||
|
"""Return the checkpoint name from the most recently completed ComfyUI job."""
|
||||||
|
url = app.config.get('COMFYUI_URL', 'http://127.0.0.1:8188')
|
||||||
|
try:
|
||||||
|
resp = requests.get(f'{url}/history', timeout=3)
|
||||||
|
if not resp.ok:
|
||||||
|
return {'checkpoint': None}
|
||||||
|
history = resp.json()
|
||||||
|
if not history:
|
||||||
|
return {'checkpoint': None}
|
||||||
|
# Sort by timestamp descending, take the most recent job
|
||||||
|
latest = max(history.values(), key=lambda j: j.get('status', {}).get('status_str', ''))
|
||||||
|
# Node "4" is the checkpoint loader in the workflow
|
||||||
|
nodes = latest.get('prompt', [None, None, {}])[2]
|
||||||
|
ckpt_name = nodes.get('4', {}).get('inputs', {}).get('ckpt_name')
|
||||||
|
return {'checkpoint': ckpt_name}
|
||||||
|
except Exception:
|
||||||
|
return {'checkpoint': None}
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/status/mcp')
|
@app.route('/api/status/mcp')
|
||||||
def api_status_mcp():
|
def api_status_mcp():
|
||||||
"""Return whether the danbooru-mcp Docker container is running."""
|
"""Return whether the danbooru-mcp Docker container is running."""
|
||||||
@@ -417,80 +439,39 @@ def api_status_mcp():
|
|||||||
|
|
||||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
||||||
|
|
||||||
def get_available_loras():
|
_LORA_DEFAULTS = {
|
||||||
loras = []
|
'characters': '/ImageModels/lora/Illustrious/Looks',
|
||||||
if os.path.exists(app.config['LORA_DIR']):
|
'outfits': '/ImageModels/lora/Illustrious/Clothing',
|
||||||
for f in os.listdir(app.config['LORA_DIR']):
|
'actions': '/ImageModels/lora/Illustrious/Poses',
|
||||||
if f.endswith('.safetensors'):
|
'styles': '/ImageModels/lora/Illustrious/Styles',
|
||||||
# Using the format seen in character JSONs
|
'scenes': '/ImageModels/lora/Illustrious/Backgrounds',
|
||||||
loras.append(f"Illustrious/Looks/{f}")
|
'detailers': '/ImageModels/lora/Illustrious/Detailers',
|
||||||
return sorted(loras)
|
}
|
||||||
|
|
||||||
def get_available_clothing_loras():
|
def get_available_loras(category):
|
||||||
"""Get LoRAs from the Clothing directory for outfit LoRAs."""
|
"""Return sorted list of LoRA paths for the given category.
|
||||||
clothing_lora_dir = '/ImageModels/lora/Illustrious/Clothing/'
|
category: one of 'characters','outfits','actions','styles','scenes','detailers'
|
||||||
loras = []
|
"""
|
||||||
if os.path.exists(clothing_lora_dir):
|
settings = Settings.query.first()
|
||||||
for f in os.listdir(clothing_lora_dir):
|
lora_dir = (getattr(settings, f'lora_dir_{category}', None) if settings else None) or _LORA_DEFAULTS.get(category, '')
|
||||||
if f.endswith('.safetensors'):
|
if not lora_dir or not os.path.isdir(lora_dir):
|
||||||
loras.append(f"Illustrious/Clothing/{f}")
|
return []
|
||||||
return sorted(loras)
|
subfolder = os.path.basename(lora_dir.rstrip('/'))
|
||||||
|
return sorted(f"Illustrious/{subfolder}/{f}" for f in os.listdir(lora_dir) if f.endswith('.safetensors'))
|
||||||
def get_available_action_loras():
|
|
||||||
"""Get LoRAs from the Poses directory for action LoRAs."""
|
|
||||||
poses_lora_dir = '/ImageModels/lora/Illustrious/Poses/'
|
|
||||||
loras = []
|
|
||||||
if os.path.exists(poses_lora_dir):
|
|
||||||
for f in os.listdir(poses_lora_dir):
|
|
||||||
if f.endswith('.safetensors'):
|
|
||||||
loras.append(f"Illustrious/Poses/{f}")
|
|
||||||
return sorted(loras)
|
|
||||||
|
|
||||||
def get_available_style_loras():
|
|
||||||
"""Get LoRAs from the Styles directory for style LoRAs."""
|
|
||||||
styles_lora_dir = '/ImageModels/lora/Illustrious/Styles/'
|
|
||||||
loras = []
|
|
||||||
if os.path.exists(styles_lora_dir):
|
|
||||||
for f in os.listdir(styles_lora_dir):
|
|
||||||
if f.endswith('.safetensors'):
|
|
||||||
loras.append(f"Illustrious/Styles/{f}")
|
|
||||||
return sorted(loras)
|
|
||||||
|
|
||||||
def get_available_detailer_loras():
|
|
||||||
"""Get LoRAs from the Detailers directory for detailer LoRAs."""
|
|
||||||
detailers_lora_dir = '/ImageModels/lora/Illustrious/Detailers/'
|
|
||||||
loras = []
|
|
||||||
if os.path.exists(detailers_lora_dir):
|
|
||||||
for f in os.listdir(detailers_lora_dir):
|
|
||||||
if f.endswith('.safetensors'):
|
|
||||||
loras.append(f"Illustrious/Detailers/{f}")
|
|
||||||
return sorted(loras)
|
|
||||||
|
|
||||||
def get_available_scene_loras():
|
|
||||||
"""Get LoRAs from the Backgrounds directory for scene LoRAs."""
|
|
||||||
backgrounds_lora_dir = '/ImageModels/lora/Illustrious/Backgrounds/'
|
|
||||||
loras = []
|
|
||||||
if os.path.exists(backgrounds_lora_dir):
|
|
||||||
for f in os.listdir(backgrounds_lora_dir):
|
|
||||||
if f.endswith('.safetensors'):
|
|
||||||
loras.append(f"Illustrious/Backgrounds/{f}")
|
|
||||||
return sorted(loras)
|
|
||||||
|
|
||||||
def get_available_checkpoints():
|
def get_available_checkpoints():
|
||||||
|
settings = Settings.query.first()
|
||||||
|
checkpoint_dirs_str = (settings.checkpoint_dirs if settings else None) or \
|
||||||
|
'/ImageModels/Stable-diffusion/Illustrious,/ImageModels/Stable-diffusion/Noob'
|
||||||
checkpoints = []
|
checkpoints = []
|
||||||
|
for ckpt_dir in checkpoint_dirs_str.split(','):
|
||||||
# Scan Illustrious
|
ckpt_dir = ckpt_dir.strip()
|
||||||
if os.path.exists(app.config['ILLUSTRIOUS_MODELS_DIR']):
|
if not ckpt_dir or not os.path.isdir(ckpt_dir):
|
||||||
for f in os.listdir(app.config['ILLUSTRIOUS_MODELS_DIR']):
|
continue
|
||||||
|
prefix = os.path.basename(ckpt_dir.rstrip('/'))
|
||||||
|
for f in os.listdir(ckpt_dir):
|
||||||
if f.endswith('.safetensors') or f.endswith('.ckpt'):
|
if f.endswith('.safetensors') or f.endswith('.ckpt'):
|
||||||
checkpoints.append(f"Illustrious/{f}")
|
checkpoints.append(f"{prefix}/{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)
|
return sorted(checkpoints)
|
||||||
|
|
||||||
def allowed_file(filename):
|
def allowed_file(filename):
|
||||||
@@ -1484,10 +1465,17 @@ def settings():
|
|||||||
settings.openrouter_model = request.form.get('model')
|
settings.openrouter_model = request.form.get('model')
|
||||||
settings.local_base_url = request.form.get('local_base_url')
|
settings.local_base_url = request.form.get('local_base_url')
|
||||||
settings.local_model = request.form.get('local_model')
|
settings.local_model = request.form.get('local_model')
|
||||||
|
settings.lora_dir_characters = request.form.get('lora_dir_characters') or settings.lora_dir_characters
|
||||||
|
settings.lora_dir_outfits = request.form.get('lora_dir_outfits') or settings.lora_dir_outfits
|
||||||
|
settings.lora_dir_actions = request.form.get('lora_dir_actions') or settings.lora_dir_actions
|
||||||
|
settings.lora_dir_styles = request.form.get('lora_dir_styles') or settings.lora_dir_styles
|
||||||
|
settings.lora_dir_scenes = request.form.get('lora_dir_scenes') or settings.lora_dir_scenes
|
||||||
|
settings.lora_dir_detailers = request.form.get('lora_dir_detailers') or settings.lora_dir_detailers
|
||||||
|
settings.checkpoint_dirs = request.form.get('checkpoint_dirs') or settings.checkpoint_dirs
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Settings updated successfully!')
|
flash('Settings updated successfully!')
|
||||||
return redirect(url_for('settings'))
|
return redirect(url_for('settings'))
|
||||||
|
|
||||||
return render_template('settings.html', settings=settings)
|
return render_template('settings.html', settings=settings)
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
@@ -1823,7 +1811,7 @@ def create_character():
|
|||||||
@app.route('/character/<path:slug>/edit', methods=['GET', 'POST'])
|
@app.route('/character/<path:slug>/edit', methods=['GET', 'POST'])
|
||||||
def edit_character(slug):
|
def edit_character(slug):
|
||||||
character = Character.query.filter_by(slug=slug).first_or_404()
|
character = Character.query.filter_by(slug=slug).first_or_404()
|
||||||
loras = get_available_loras()
|
loras = get_available_loras('characters')
|
||||||
char_looks = Look.query.filter_by(character_id=character.character_id).order_by(Look.name).all()
|
char_looks = Look.query.filter_by(character_id=character.character_id).order_by(Look.name).all()
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@@ -2080,15 +2068,13 @@ def upload_image(slug):
|
|||||||
@app.route('/character/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
@app.route('/character/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||||
def replace_cover_from_preview(slug):
|
def replace_cover_from_preview(slug):
|
||||||
character = Character.query.filter_by(slug=slug).first_or_404()
|
character = Character.query.filter_by(slug=slug).first_or_404()
|
||||||
preview_path = session.get(f'preview_{slug}')
|
preview_path = request.form.get('preview_path')
|
||||||
|
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||||
if preview_path:
|
|
||||||
character.image_path = preview_path
|
character.image_path = preview_path
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Cover image updated from preview!')
|
flash('Cover image updated!')
|
||||||
else:
|
else:
|
||||||
flash('No preview image available', 'error')
|
flash('No valid preview image selected.', 'error')
|
||||||
|
|
||||||
return redirect(url_for('detail', slug=slug))
|
return redirect(url_for('detail', slug=slug))
|
||||||
|
|
||||||
def _log_workflow_prompts(label, workflow):
|
def _log_workflow_prompts(label, workflow):
|
||||||
@@ -2278,19 +2264,6 @@ def _get_default_checkpoint():
|
|||||||
return None, None
|
return None, None
|
||||||
return ckpt.checkpoint_path, ckpt.data or {}
|
return ckpt.checkpoint_path, ckpt.data or {}
|
||||||
|
|
||||||
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 with active outfit
|
|
||||||
prompts = build_prompt(character.data, selected_fields, character.default_fields, character.active_outfit)
|
|
||||||
|
|
||||||
# 3. Prepare workflow
|
|
||||||
ckpt_path, ckpt_data = _get_default_checkpoint()
|
|
||||||
workflow = _prepare_workflow(workflow, character, prompts, checkpoint=ckpt_path, checkpoint_data=ckpt_data)
|
|
||||||
|
|
||||||
return queue_prompt(workflow, client_id=client_id)
|
|
||||||
@app.route('/get_missing_characters')
|
@app.route('/get_missing_characters')
|
||||||
def get_missing_characters():
|
def get_missing_characters():
|
||||||
missing = Character.query.filter((Character.image_path == None) | (Character.image_path == '')).all()
|
missing = Character.query.filter((Character.image_path == None) | (Character.image_path == '')).all()
|
||||||
@@ -2345,7 +2318,8 @@ def generate_image(slug):
|
|||||||
|
|
||||||
# Save preferences
|
# Save preferences
|
||||||
session[f'prefs_{slug}'] = selected_fields
|
session[f'prefs_{slug}'] = selected_fields
|
||||||
|
session.modified = True
|
||||||
|
|
||||||
# Build workflow
|
# Build workflow
|
||||||
with open('comfy_workflow.json', 'r') as f:
|
with open('comfy_workflow.json', 'r') as f:
|
||||||
workflow = json.load(f)
|
workflow = json.load(f)
|
||||||
@@ -2353,6 +2327,7 @@ def generate_image(slug):
|
|||||||
ckpt_path, ckpt_data = _get_default_checkpoint()
|
ckpt_path, ckpt_data = _get_default_checkpoint()
|
||||||
workflow = _prepare_workflow(workflow, character, prompts, checkpoint=ckpt_path, checkpoint_data=ckpt_data)
|
workflow = _prepare_workflow(workflow, character, prompts, checkpoint=ckpt_path, checkpoint_data=ckpt_data)
|
||||||
|
|
||||||
|
label = f"{character.name} – {action}"
|
||||||
job = _enqueue_job(label, workflow, _make_finalize('characters', slug, Character, action))
|
job = _enqueue_job(label, workflow, _make_finalize('characters', slug, Character, action))
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
return {'status': 'queued', 'job_id': job['id']}
|
return {'status': 'queued', 'job_id': job['id']}
|
||||||
@@ -2428,7 +2403,9 @@ def rescan_outfits():
|
|||||||
|
|
||||||
@app.route('/outfits/bulk_create', methods=['POST'])
|
@app.route('/outfits/bulk_create', methods=['POST'])
|
||||||
def bulk_create_outfits_from_loras():
|
def bulk_create_outfits_from_loras():
|
||||||
clothing_lora_dir = '/ImageModels/lora/Illustrious/Clothing/'
|
_s = Settings.query.first()
|
||||||
|
clothing_lora_dir = ((_s.lora_dir_outfits if _s else None) or '/ImageModels/lora/Illustrious/Clothing').rstrip('/')
|
||||||
|
_lora_subfolder = os.path.basename(clothing_lora_dir)
|
||||||
if not os.path.exists(clothing_lora_dir):
|
if not os.path.exists(clothing_lora_dir):
|
||||||
flash('Clothing LoRA directory not found.', 'error')
|
flash('Clothing LoRA directory not found.', 'error')
|
||||||
return redirect(url_for('outfits_index'))
|
return redirect(url_for('outfits_index'))
|
||||||
@@ -2489,7 +2466,7 @@ def bulk_create_outfits_from_loras():
|
|||||||
|
|
||||||
if 'lora' not in outfit_data:
|
if 'lora' not in outfit_data:
|
||||||
outfit_data['lora'] = {}
|
outfit_data['lora'] = {}
|
||||||
outfit_data['lora']['lora_name'] = f"Illustrious/Clothing/{filename}"
|
outfit_data['lora']['lora_name'] = f"Illustrious/{_lora_subfolder}/{filename}"
|
||||||
if not outfit_data['lora'].get('lora_triggers'):
|
if not outfit_data['lora'].get('lora_triggers'):
|
||||||
outfit_data['lora']['lora_triggers'] = name_base
|
outfit_data['lora']['lora_triggers'] = name_base
|
||||||
if outfit_data['lora'].get('lora_weight') is None:
|
if outfit_data['lora'].get('lora_weight') is None:
|
||||||
@@ -2548,7 +2525,7 @@ def outfit_detail(slug):
|
|||||||
@app.route('/outfit/<path:slug>/edit', methods=['GET', 'POST'])
|
@app.route('/outfit/<path:slug>/edit', methods=['GET', 'POST'])
|
||||||
def edit_outfit(slug):
|
def edit_outfit(slug):
|
||||||
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||||
loras = get_available_clothing_loras() # Use clothing LoRAs for outfits
|
loras = get_available_loras('outfits') # Use clothing LoRAs for outfits
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
try:
|
try:
|
||||||
@@ -2667,7 +2644,8 @@ def generate_outfit_image(slug):
|
|||||||
# Save preferences
|
# Save preferences
|
||||||
session[f'prefs_outfit_{slug}'] = selected_fields
|
session[f'prefs_outfit_{slug}'] = selected_fields
|
||||||
session[f'char_outfit_{slug}'] = character_slug
|
session[f'char_outfit_{slug}'] = character_slug
|
||||||
|
session.modified = True
|
||||||
|
|
||||||
# Build combined data for prompt building
|
# Build combined data for prompt building
|
||||||
if character:
|
if character:
|
||||||
# Combine character identity/defaults with outfit wardrobe
|
# Combine character identity/defaults with outfit wardrobe
|
||||||
@@ -2743,15 +2721,13 @@ def generate_outfit_image(slug):
|
|||||||
@app.route('/outfit/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
@app.route('/outfit/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||||
def replace_outfit_cover_from_preview(slug):
|
def replace_outfit_cover_from_preview(slug):
|
||||||
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||||
preview_path = session.get(f'preview_outfit_{slug}')
|
preview_path = request.form.get('preview_path')
|
||||||
|
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||||
if preview_path:
|
|
||||||
outfit.image_path = preview_path
|
outfit.image_path = preview_path
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Cover image updated from preview!')
|
flash('Cover image updated!')
|
||||||
else:
|
else:
|
||||||
flash('No preview image available', 'error')
|
flash('No valid preview image selected.', 'error')
|
||||||
|
|
||||||
return redirect(url_for('outfit_detail', slug=slug))
|
return redirect(url_for('outfit_detail', slug=slug))
|
||||||
|
|
||||||
@app.route('/outfit/create', methods=['GET', 'POST'])
|
@app.route('/outfit/create', methods=['GET', 'POST'])
|
||||||
@@ -2988,7 +2964,7 @@ def action_detail(slug):
|
|||||||
@app.route('/action/<path:slug>/edit', methods=['GET', 'POST'])
|
@app.route('/action/<path:slug>/edit', methods=['GET', 'POST'])
|
||||||
def edit_action(slug):
|
def edit_action(slug):
|
||||||
action = Action.query.filter_by(slug=slug).first_or_404()
|
action = Action.query.filter_by(slug=slug).first_or_404()
|
||||||
loras = get_available_action_loras()
|
loras = get_available_loras('actions')
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
try:
|
try:
|
||||||
@@ -3091,7 +3067,7 @@ def generate_action_image(slug):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Get action type
|
# Get action type
|
||||||
action_type = request.form.get('action', 'preview')
|
action = request.form.get('action', 'preview')
|
||||||
|
|
||||||
# Get selected fields
|
# Get selected fields
|
||||||
selected_fields = request.form.getlist('include_field')
|
selected_fields = request.form.getlist('include_field')
|
||||||
@@ -3107,7 +3083,8 @@ def generate_action_image(slug):
|
|||||||
# Save preferences
|
# Save preferences
|
||||||
session[f'char_action_{slug}'] = character_slug
|
session[f'char_action_{slug}'] = character_slug
|
||||||
session[f'prefs_action_{slug}'] = selected_fields
|
session[f'prefs_action_{slug}'] = selected_fields
|
||||||
|
session.modified = True
|
||||||
|
|
||||||
# Build combined data for prompt building
|
# Build combined data for prompt building
|
||||||
if character:
|
if character:
|
||||||
# Combine character identity/wardrobe with action details
|
# Combine character identity/wardrobe with action details
|
||||||
@@ -3245,8 +3222,8 @@ def generate_action_image(slug):
|
|||||||
workflow = _prepare_workflow(workflow, character, prompts, action=action_obj, checkpoint=ckpt_path, checkpoint_data=ckpt_data)
|
workflow = _prepare_workflow(workflow, character, prompts, action=action_obj, checkpoint=ckpt_path, checkpoint_data=ckpt_data)
|
||||||
|
|
||||||
char_label = character.name if character else 'no character'
|
char_label = character.name if character else 'no character'
|
||||||
label = f"Action: {action_obj.name} ({char_label}) – {action_type}"
|
label = f"Action: {action_obj.name} ({char_label}) – {action}"
|
||||||
job = _enqueue_job(label, workflow, _make_finalize('actions', slug, Action, action_type))
|
job = _enqueue_job(label, workflow, _make_finalize('actions', slug, Action, action))
|
||||||
|
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
return {'status': 'queued', 'job_id': job['id']}
|
return {'status': 'queued', 'job_id': job['id']}
|
||||||
@@ -3263,15 +3240,13 @@ def generate_action_image(slug):
|
|||||||
@app.route('/action/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
@app.route('/action/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||||
def replace_action_cover_from_preview(slug):
|
def replace_action_cover_from_preview(slug):
|
||||||
action = Action.query.filter_by(slug=slug).first_or_404()
|
action = Action.query.filter_by(slug=slug).first_or_404()
|
||||||
preview_path = session.get(f'preview_action_{slug}')
|
preview_path = request.form.get('preview_path')
|
||||||
|
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||||
if preview_path:
|
|
||||||
action.image_path = preview_path
|
action.image_path = preview_path
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Cover image updated from preview!')
|
flash('Cover image updated!')
|
||||||
else:
|
else:
|
||||||
flash('No preview image available', 'error')
|
flash('No valid preview image selected.', 'error')
|
||||||
|
|
||||||
return redirect(url_for('action_detail', slug=slug))
|
return redirect(url_for('action_detail', slug=slug))
|
||||||
|
|
||||||
@app.route('/action/<path:slug>/save_defaults', methods=['POST'])
|
@app.route('/action/<path:slug>/save_defaults', methods=['POST'])
|
||||||
@@ -3285,7 +3260,9 @@ def save_action_defaults(slug):
|
|||||||
|
|
||||||
@app.route('/actions/bulk_create', methods=['POST'])
|
@app.route('/actions/bulk_create', methods=['POST'])
|
||||||
def bulk_create_actions_from_loras():
|
def bulk_create_actions_from_loras():
|
||||||
actions_lora_dir = '/ImageModels/lora/Illustrious/Poses/'
|
_s = Settings.query.first()
|
||||||
|
actions_lora_dir = ((_s.lora_dir_actions if _s else None) or '/ImageModels/lora/Illustrious/Poses').rstrip('/')
|
||||||
|
_lora_subfolder = os.path.basename(actions_lora_dir)
|
||||||
if not os.path.exists(actions_lora_dir):
|
if not os.path.exists(actions_lora_dir):
|
||||||
flash('Actions LoRA directory not found.', 'error')
|
flash('Actions LoRA directory not found.', 'error')
|
||||||
return redirect(url_for('actions_index'))
|
return redirect(url_for('actions_index'))
|
||||||
@@ -3348,7 +3325,7 @@ def bulk_create_actions_from_loras():
|
|||||||
|
|
||||||
# Update lora dict safely
|
# Update lora dict safely
|
||||||
if 'lora' not in action_data: action_data['lora'] = {}
|
if 'lora' not in action_data: action_data['lora'] = {}
|
||||||
action_data['lora']['lora_name'] = f"Illustrious/Poses/{filename}"
|
action_data['lora']['lora_name'] = f"Illustrious/{_lora_subfolder}/{filename}"
|
||||||
|
|
||||||
# Fallbacks if LLM failed to extract metadata
|
# Fallbacks if LLM failed to extract metadata
|
||||||
if not action_data['lora'].get('lora_triggers'):
|
if not action_data['lora'].get('lora_triggers'):
|
||||||
@@ -3554,7 +3531,7 @@ def style_detail(slug):
|
|||||||
@app.route('/style/<path:slug>/edit', methods=['GET', 'POST'])
|
@app.route('/style/<path:slug>/edit', methods=['GET', 'POST'])
|
||||||
def edit_style(slug):
|
def edit_style(slug):
|
||||||
style = Style.query.filter_by(slug=slug).first_or_404()
|
style = Style.query.filter_by(slug=slug).first_or_404()
|
||||||
loras = get_available_style_loras()
|
loras = get_available_loras('styles')
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
try:
|
try:
|
||||||
@@ -3718,7 +3695,8 @@ def generate_style_image(slug):
|
|||||||
# Save preferences
|
# Save preferences
|
||||||
session[f'char_style_{slug}'] = character_slug
|
session[f'char_style_{slug}'] = character_slug
|
||||||
session[f'prefs_style_{slug}'] = selected_fields
|
session[f'prefs_style_{slug}'] = selected_fields
|
||||||
|
session.modified = True
|
||||||
|
|
||||||
# Build workflow using helper (returns workflow dict, not prompt_response)
|
# Build workflow using helper (returns workflow dict, not prompt_response)
|
||||||
workflow = _build_style_workflow(style_obj, character, selected_fields)
|
workflow = _build_style_workflow(style_obj, character, selected_fields)
|
||||||
|
|
||||||
@@ -3750,15 +3728,13 @@ def save_style_defaults(slug):
|
|||||||
@app.route('/style/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
@app.route('/style/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||||
def replace_style_cover_from_preview(slug):
|
def replace_style_cover_from_preview(slug):
|
||||||
style = Style.query.filter_by(slug=slug).first_or_404()
|
style = Style.query.filter_by(slug=slug).first_or_404()
|
||||||
preview_path = session.get(f'preview_style_{slug}')
|
preview_path = request.form.get('preview_path')
|
||||||
|
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||||
if preview_path:
|
|
||||||
style.image_path = preview_path
|
style.image_path = preview_path
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Cover image updated from preview!')
|
flash('Cover image updated!')
|
||||||
else:
|
else:
|
||||||
flash('No preview image available', 'error')
|
flash('No valid preview image selected.', 'error')
|
||||||
|
|
||||||
return redirect(url_for('style_detail', slug=slug))
|
return redirect(url_for('style_detail', slug=slug))
|
||||||
|
|
||||||
@app.route('/get_missing_styles')
|
@app.route('/get_missing_styles')
|
||||||
@@ -3819,7 +3795,9 @@ def generate_missing_styles():
|
|||||||
|
|
||||||
@app.route('/styles/bulk_create', methods=['POST'])
|
@app.route('/styles/bulk_create', methods=['POST'])
|
||||||
def bulk_create_styles_from_loras():
|
def bulk_create_styles_from_loras():
|
||||||
styles_lora_dir = '/ImageModels/lora/Illustrious/Styles/'
|
_s = Settings.query.first()
|
||||||
|
styles_lora_dir = ((_s.lora_dir_styles if _s else None) or '/ImageModels/lora/Illustrious/Styles').rstrip('/')
|
||||||
|
_lora_subfolder = os.path.basename(styles_lora_dir)
|
||||||
if not os.path.exists(styles_lora_dir):
|
if not os.path.exists(styles_lora_dir):
|
||||||
flash('Styles LoRA directory not found.', 'error')
|
flash('Styles LoRA directory not found.', 'error')
|
||||||
return redirect(url_for('styles_index'))
|
return redirect(url_for('styles_index'))
|
||||||
@@ -3877,7 +3855,7 @@ def bulk_create_styles_from_loras():
|
|||||||
style_data['style_name'] = style_name
|
style_data['style_name'] = style_name
|
||||||
|
|
||||||
if 'lora' not in style_data: style_data['lora'] = {}
|
if 'lora' not in style_data: style_data['lora'] = {}
|
||||||
style_data['lora']['lora_name'] = f"Illustrious/Styles/{filename}"
|
style_data['lora']['lora_name'] = f"Illustrious/{_lora_subfolder}/{filename}"
|
||||||
|
|
||||||
if not style_data['lora'].get('lora_triggers'):
|
if not style_data['lora'].get('lora_triggers'):
|
||||||
style_data['lora']['lora_triggers'] = name_base
|
style_data['lora']['lora_triggers'] = name_base
|
||||||
@@ -4059,7 +4037,7 @@ def scene_detail(slug):
|
|||||||
@app.route('/scene/<path:slug>/edit', methods=['GET', 'POST'])
|
@app.route('/scene/<path:slug>/edit', methods=['GET', 'POST'])
|
||||||
def edit_scene(slug):
|
def edit_scene(slug):
|
||||||
scene = Scene.query.filter_by(slug=slug).first_or_404()
|
scene = Scene.query.filter_by(slug=slug).first_or_404()
|
||||||
loras = get_available_scene_loras()
|
loras = get_available_loras('scenes')
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
try:
|
try:
|
||||||
@@ -4253,7 +4231,8 @@ def generate_scene_image(slug):
|
|||||||
# Save preferences
|
# Save preferences
|
||||||
session[f'char_scene_{slug}'] = character_slug
|
session[f'char_scene_{slug}'] = character_slug
|
||||||
session[f'prefs_scene_{slug}'] = selected_fields
|
session[f'prefs_scene_{slug}'] = selected_fields
|
||||||
|
session.modified = True
|
||||||
|
|
||||||
# Build workflow using helper
|
# Build workflow using helper
|
||||||
workflow = _queue_scene_generation(scene_obj, character, selected_fields)
|
workflow = _queue_scene_generation(scene_obj, character, selected_fields)
|
||||||
|
|
||||||
@@ -4285,20 +4264,20 @@ def save_scene_defaults(slug):
|
|||||||
@app.route('/scene/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
@app.route('/scene/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||||
def replace_scene_cover_from_preview(slug):
|
def replace_scene_cover_from_preview(slug):
|
||||||
scene = Scene.query.filter_by(slug=slug).first_or_404()
|
scene = Scene.query.filter_by(slug=slug).first_or_404()
|
||||||
preview_path = session.get(f'preview_scene_{slug}')
|
preview_path = request.form.get('preview_path')
|
||||||
|
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||||
if preview_path:
|
|
||||||
scene.image_path = preview_path
|
scene.image_path = preview_path
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Cover image updated from preview!')
|
flash('Cover image updated!')
|
||||||
else:
|
else:
|
||||||
flash('No preview image available', 'error')
|
flash('No valid preview image selected.', 'error')
|
||||||
|
|
||||||
return redirect(url_for('scene_detail', slug=slug))
|
return redirect(url_for('scene_detail', slug=slug))
|
||||||
|
|
||||||
@app.route('/scenes/bulk_create', methods=['POST'])
|
@app.route('/scenes/bulk_create', methods=['POST'])
|
||||||
def bulk_create_scenes_from_loras():
|
def bulk_create_scenes_from_loras():
|
||||||
backgrounds_lora_dir = '/ImageModels/lora/Illustrious/Backgrounds/'
|
_s = Settings.query.first()
|
||||||
|
backgrounds_lora_dir = ((_s.lora_dir_scenes if _s else None) or '/ImageModels/lora/Illustrious/Backgrounds').rstrip('/')
|
||||||
|
_lora_subfolder = os.path.basename(backgrounds_lora_dir)
|
||||||
if not os.path.exists(backgrounds_lora_dir):
|
if not os.path.exists(backgrounds_lora_dir):
|
||||||
flash('Backgrounds LoRA directory not found.', 'error')
|
flash('Backgrounds LoRA directory not found.', 'error')
|
||||||
return redirect(url_for('scenes_index'))
|
return redirect(url_for('scenes_index'))
|
||||||
@@ -4360,7 +4339,7 @@ def bulk_create_scenes_from_loras():
|
|||||||
scene_data['scene_name'] = scene_name
|
scene_data['scene_name'] = scene_name
|
||||||
|
|
||||||
if 'lora' not in scene_data: scene_data['lora'] = {}
|
if 'lora' not in scene_data: scene_data['lora'] = {}
|
||||||
scene_data['lora']['lora_name'] = f"Illustrious/Backgrounds/{filename}"
|
scene_data['lora']['lora_name'] = f"Illustrious/{_lora_subfolder}/{filename}"
|
||||||
|
|
||||||
if not scene_data['lora'].get('lora_triggers'):
|
if not scene_data['lora'].get('lora_triggers'):
|
||||||
scene_data['lora']['lora_triggers'] = name_base
|
scene_data['lora']['lora_triggers'] = name_base
|
||||||
@@ -4554,7 +4533,7 @@ def detailer_detail(slug):
|
|||||||
@app.route('/detailer/<path:slug>/edit', methods=['GET', 'POST'])
|
@app.route('/detailer/<path:slug>/edit', methods=['GET', 'POST'])
|
||||||
def edit_detailer(slug):
|
def edit_detailer(slug):
|
||||||
detailer = Detailer.query.filter_by(slug=slug).first_or_404()
|
detailer = Detailer.query.filter_by(slug=slug).first_or_404()
|
||||||
loras = get_available_detailer_loras()
|
loras = get_available_loras('detailers')
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
try:
|
try:
|
||||||
@@ -4712,8 +4691,8 @@ def generate_detailer_image(slug):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Get action type
|
# Get action type
|
||||||
action_type = request.form.get('action', 'preview')
|
action = request.form.get('action', 'preview')
|
||||||
|
|
||||||
# Get selected fields
|
# Get selected fields
|
||||||
selected_fields = request.form.getlist('include_field')
|
selected_fields = request.form.getlist('include_field')
|
||||||
|
|
||||||
@@ -4737,13 +4716,14 @@ def generate_detailer_image(slug):
|
|||||||
session[f'extra_pos_detailer_{slug}'] = extra_positive
|
session[f'extra_pos_detailer_{slug}'] = extra_positive
|
||||||
session[f'extra_neg_detailer_{slug}'] = extra_negative
|
session[f'extra_neg_detailer_{slug}'] = extra_negative
|
||||||
session[f'prefs_detailer_{slug}'] = selected_fields
|
session[f'prefs_detailer_{slug}'] = selected_fields
|
||||||
|
session.modified = True
|
||||||
|
|
||||||
# Build workflow using helper
|
# Build workflow using helper
|
||||||
workflow = _queue_detailer_generation(detailer_obj, character, selected_fields, action=action_obj, extra_positive=extra_positive, extra_negative=extra_negative)
|
workflow = _queue_detailer_generation(detailer_obj, character, selected_fields, action=action_obj, extra_positive=extra_positive, extra_negative=extra_negative)
|
||||||
|
|
||||||
char_label = character.name if character else 'no character'
|
char_label = character.name if character else 'no character'
|
||||||
label = f"Detailer: {detailer_obj.name} ({char_label}) – {action_type}"
|
label = f"Detailer: {detailer_obj.name} ({char_label}) – {action}"
|
||||||
job = _enqueue_job(label, workflow, _make_finalize('detailers', slug, Detailer, action_type))
|
job = _enqueue_job(label, workflow, _make_finalize('detailers', slug, Detailer, action))
|
||||||
|
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
return {'status': 'queued', 'job_id': job['id']}
|
return {'status': 'queued', 'job_id': job['id']}
|
||||||
@@ -4769,15 +4749,13 @@ def save_detailer_defaults(slug):
|
|||||||
@app.route('/detailer/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
@app.route('/detailer/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||||
def replace_detailer_cover_from_preview(slug):
|
def replace_detailer_cover_from_preview(slug):
|
||||||
detailer = Detailer.query.filter_by(slug=slug).first_or_404()
|
detailer = Detailer.query.filter_by(slug=slug).first_or_404()
|
||||||
preview_path = session.get(f'preview_detailer_{slug}')
|
preview_path = request.form.get('preview_path')
|
||||||
|
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||||
if preview_path:
|
|
||||||
detailer.image_path = preview_path
|
detailer.image_path = preview_path
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Cover image updated from preview!')
|
flash('Cover image updated!')
|
||||||
else:
|
else:
|
||||||
flash('No preview image available', 'error')
|
flash('No valid preview image selected.', 'error')
|
||||||
|
|
||||||
return redirect(url_for('detailer_detail', slug=slug))
|
return redirect(url_for('detailer_detail', slug=slug))
|
||||||
|
|
||||||
@app.route('/detailer/<path:slug>/save_json', methods=['POST'])
|
@app.route('/detailer/<path:slug>/save_json', methods=['POST'])
|
||||||
@@ -4798,7 +4776,9 @@ def save_detailer_json(slug):
|
|||||||
|
|
||||||
@app.route('/detailers/bulk_create', methods=['POST'])
|
@app.route('/detailers/bulk_create', methods=['POST'])
|
||||||
def bulk_create_detailers_from_loras():
|
def bulk_create_detailers_from_loras():
|
||||||
detailers_lora_dir = '/ImageModels/lora/Illustrious/Detailers/'
|
_s = Settings.query.first()
|
||||||
|
detailers_lora_dir = ((_s.lora_dir_detailers if _s else None) or '/ImageModels/lora/Illustrious/Detailers').rstrip('/')
|
||||||
|
_lora_subfolder = os.path.basename(detailers_lora_dir)
|
||||||
if not os.path.exists(detailers_lora_dir):
|
if not os.path.exists(detailers_lora_dir):
|
||||||
flash('Detailers LoRA directory not found.', 'error')
|
flash('Detailers LoRA directory not found.', 'error')
|
||||||
return redirect(url_for('detailers_index'))
|
return redirect(url_for('detailers_index'))
|
||||||
@@ -4856,7 +4836,7 @@ def bulk_create_detailers_from_loras():
|
|||||||
detailer_data['detailer_name'] = detailer_name
|
detailer_data['detailer_name'] = detailer_name
|
||||||
|
|
||||||
if 'lora' not in detailer_data: detailer_data['lora'] = {}
|
if 'lora' not in detailer_data: detailer_data['lora'] = {}
|
||||||
detailer_data['lora']['lora_name'] = f"Illustrious/Detailers/{filename}"
|
detailer_data['lora']['lora_name'] = f"Illustrious/{_lora_subfolder}/{filename}"
|
||||||
|
|
||||||
if not detailer_data['lora'].get('lora_triggers'):
|
if not detailer_data['lora'].get('lora_triggers'):
|
||||||
detailer_data['lora']['lora_triggers'] = name_base
|
detailer_data['lora']['lora_triggers'] = name_base
|
||||||
@@ -5092,6 +5072,7 @@ def generate_checkpoint_image(slug):
|
|||||||
character_slug = character.slug
|
character_slug = character.slug
|
||||||
|
|
||||||
session[f'char_checkpoint_{slug}'] = character_slug
|
session[f'char_checkpoint_{slug}'] = character_slug
|
||||||
|
session.modified = True
|
||||||
workflow = _build_checkpoint_workflow(ckpt, character)
|
workflow = _build_checkpoint_workflow(ckpt, character)
|
||||||
|
|
||||||
char_label = character.name if character else 'random'
|
char_label = character.name if character else 'random'
|
||||||
@@ -5102,7 +5083,7 @@ def generate_checkpoint_image(slug):
|
|||||||
return {'status': 'queued', 'job_id': job['id']}
|
return {'status': 'queued', 'job_id': job['id']}
|
||||||
return redirect(url_for('checkpoint_detail', slug=slug))
|
return redirect(url_for('checkpoint_detail', slug=slug))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Checkpoint generation error: {e}")
|
print(f"Generation error: {e}")
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
return {'error': str(e)}, 500
|
return {'error': str(e)}, 500
|
||||||
flash(f"Error during generation: {str(e)}")
|
flash(f"Error during generation: {str(e)}")
|
||||||
@@ -5111,13 +5092,13 @@ def generate_checkpoint_image(slug):
|
|||||||
@app.route('/checkpoint/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
@app.route('/checkpoint/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||||
def replace_checkpoint_cover_from_preview(slug):
|
def replace_checkpoint_cover_from_preview(slug):
|
||||||
ckpt = Checkpoint.query.filter_by(slug=slug).first_or_404()
|
ckpt = Checkpoint.query.filter_by(slug=slug).first_or_404()
|
||||||
preview_path = session.get(f'preview_checkpoint_{slug}')
|
preview_path = request.form.get('preview_path')
|
||||||
if preview_path:
|
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||||
ckpt.image_path = preview_path
|
ckpt.image_path = preview_path
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Cover image updated from preview!')
|
flash('Cover image updated!')
|
||||||
else:
|
else:
|
||||||
flash('No preview image available', 'error')
|
flash('No valid preview image selected.', 'error')
|
||||||
return redirect(url_for('checkpoint_detail', slug=slug))
|
return redirect(url_for('checkpoint_detail', slug=slug))
|
||||||
|
|
||||||
@app.route('/checkpoint/<path:slug>/save_json', methods=['POST'])
|
@app.route('/checkpoint/<path:slug>/save_json', methods=['POST'])
|
||||||
@@ -5281,7 +5262,7 @@ def look_detail(slug):
|
|||||||
def edit_look(slug):
|
def edit_look(slug):
|
||||||
look = Look.query.filter_by(slug=slug).first_or_404()
|
look = Look.query.filter_by(slug=slug).first_or_404()
|
||||||
characters = Character.query.order_by(Character.name).all()
|
characters = Character.query.order_by(Character.name).all()
|
||||||
loras = get_available_loras()
|
loras = get_available_loras('characters')
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
look.name = request.form.get('look_name', look.name)
|
look.name = request.form.get('look_name', look.name)
|
||||||
@@ -5363,6 +5344,7 @@ def generate_look_image(slug):
|
|||||||
|
|
||||||
session[f'prefs_look_{slug}'] = selected_fields
|
session[f'prefs_look_{slug}'] = selected_fields
|
||||||
session[f'char_look_{slug}'] = character_slug
|
session[f'char_look_{slug}'] = character_slug
|
||||||
|
session.modified = True
|
||||||
|
|
||||||
lora_triggers = look.data.get('lora', {}).get('lora_triggers', '')
|
lora_triggers = look.data.get('lora', {}).get('lora_triggers', '')
|
||||||
look_positive = look.data.get('positive', '')
|
look_positive = look.data.get('positive', '')
|
||||||
@@ -5412,7 +5394,7 @@ def generate_look_image(slug):
|
|||||||
return redirect(url_for('look_detail', slug=slug))
|
return redirect(url_for('look_detail', slug=slug))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Look generation error: {e}")
|
print(f"Generation error: {e}")
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
return {'error': str(e)}, 500
|
return {'error': str(e)}, 500
|
||||||
flash(f"Error during generation: {str(e)}")
|
flash(f"Error during generation: {str(e)}")
|
||||||
@@ -5421,13 +5403,13 @@ def generate_look_image(slug):
|
|||||||
@app.route('/look/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
@app.route('/look/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||||
def replace_look_cover_from_preview(slug):
|
def replace_look_cover_from_preview(slug):
|
||||||
look = Look.query.filter_by(slug=slug).first_or_404()
|
look = Look.query.filter_by(slug=slug).first_or_404()
|
||||||
preview_path = session.get(f'preview_look_{slug}')
|
preview_path = request.form.get('preview_path')
|
||||||
if preview_path:
|
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||||
look.image_path = preview_path
|
look.image_path = preview_path
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Cover image updated from preview!')
|
flash('Cover image updated!')
|
||||||
else:
|
else:
|
||||||
flash('No preview image available', 'error')
|
flash('No valid preview image selected.', 'error')
|
||||||
return redirect(url_for('look_detail', slug=slug))
|
return redirect(url_for('look_detail', slug=slug))
|
||||||
|
|
||||||
@app.route('/look/<path:slug>/save_defaults', methods=['POST'])
|
@app.route('/look/<path:slug>/save_defaults', methods=['POST'])
|
||||||
@@ -5458,7 +5440,7 @@ def save_look_json(slug):
|
|||||||
@app.route('/look/create', methods=['GET', 'POST'])
|
@app.route('/look/create', methods=['GET', 'POST'])
|
||||||
def create_look():
|
def create_look():
|
||||||
characters = Character.query.order_by(Character.name).all()
|
characters = Character.query.order_by(Character.name).all()
|
||||||
loras = get_available_loras()
|
loras = get_available_loras('characters')
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
name = request.form.get('name', '').strip()
|
name = request.form.get('name', '').strip()
|
||||||
look_id = re.sub(r'[^a-zA-Z0-9_]', '_', name.lower().replace(' ', '_'))
|
look_id = re.sub(r'[^a-zA-Z0-9_]', '_', name.lower().replace(' ', '_'))
|
||||||
@@ -5513,7 +5495,9 @@ def clear_all_look_covers():
|
|||||||
|
|
||||||
@app.route('/looks/bulk_create', methods=['POST'])
|
@app.route('/looks/bulk_create', methods=['POST'])
|
||||||
def bulk_create_looks_from_loras():
|
def bulk_create_looks_from_loras():
|
||||||
lora_dir = app.config['LORA_DIR']
|
_s = Settings.query.first()
|
||||||
|
lora_dir = ((_s.lora_dir_characters if _s else None) or app.config['LORA_DIR']).rstrip('/')
|
||||||
|
_lora_subfolder = os.path.basename(lora_dir)
|
||||||
if not os.path.exists(lora_dir):
|
if not os.path.exists(lora_dir):
|
||||||
flash('Looks LoRA directory not found.', 'error')
|
flash('Looks LoRA directory not found.', 'error')
|
||||||
return redirect(url_for('looks_index'))
|
return redirect(url_for('looks_index'))
|
||||||
@@ -5574,7 +5558,7 @@ def bulk_create_looks_from_loras():
|
|||||||
|
|
||||||
if 'lora' not in look_data:
|
if 'lora' not in look_data:
|
||||||
look_data['lora'] = {}
|
look_data['lora'] = {}
|
||||||
look_data['lora']['lora_name'] = f"Illustrious/Looks/{filename}"
|
look_data['lora']['lora_name'] = f"Illustrious/{_lora_subfolder}/{filename}"
|
||||||
if not look_data['lora'].get('lora_triggers'):
|
if not look_data['lora'].get('lora_triggers'):
|
||||||
look_data['lora']['lora_triggers'] = name_base
|
look_data['lora']['lora_triggers'] = name_base
|
||||||
if look_data['lora'].get('lora_weight') is None:
|
if look_data['lora'].get('lora_weight') is None:
|
||||||
@@ -6386,7 +6370,14 @@ if __name__ == '__main__':
|
|||||||
columns_to_add = [
|
columns_to_add = [
|
||||||
('llm_provider', "VARCHAR(50) DEFAULT 'openrouter'"),
|
('llm_provider', "VARCHAR(50) DEFAULT 'openrouter'"),
|
||||||
('local_base_url', "VARCHAR(255)"),
|
('local_base_url', "VARCHAR(255)"),
|
||||||
('local_model', "VARCHAR(100)")
|
('local_model', "VARCHAR(100)"),
|
||||||
|
('lora_dir_characters', "VARCHAR(500) DEFAULT '/ImageModels/lora/Illustrious/Looks'"),
|
||||||
|
('lora_dir_outfits', "VARCHAR(500) DEFAULT '/ImageModels/lora/Illustrious/Clothing'"),
|
||||||
|
('lora_dir_actions', "VARCHAR(500) DEFAULT '/ImageModels/lora/Illustrious/Poses'"),
|
||||||
|
('lora_dir_styles', "VARCHAR(500) DEFAULT '/ImageModels/lora/Illustrious/Styles'"),
|
||||||
|
('lora_dir_scenes', "VARCHAR(500) DEFAULT '/ImageModels/lora/Illustrious/Backgrounds'"),
|
||||||
|
('lora_dir_detailers', "VARCHAR(500) DEFAULT '/ImageModels/lora/Illustrious/Detailers'"),
|
||||||
|
('checkpoint_dirs', "VARCHAR(1000) DEFAULT '/ImageModels/Stable-diffusion/Illustrious,/ImageModels/Stable-diffusion/Noob'"),
|
||||||
]
|
]
|
||||||
for col_name, col_type in columns_to_add:
|
for col_name, col_type in columns_to_add:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -132,6 +132,15 @@ class Settings(db.Model):
|
|||||||
openrouter_model = db.Column(db.String(100), default='google/gemini-2.0-flash-001')
|
openrouter_model = db.Column(db.String(100), default='google/gemini-2.0-flash-001')
|
||||||
local_base_url = db.Column(db.String(255), nullable=True)
|
local_base_url = db.Column(db.String(255), nullable=True)
|
||||||
local_model = db.Column(db.String(100), nullable=True)
|
local_model = db.Column(db.String(100), nullable=True)
|
||||||
|
# LoRA directories (absolute paths on disk)
|
||||||
|
lora_dir_characters = db.Column(db.String(500), default='/ImageModels/lora/Illustrious/Looks')
|
||||||
|
lora_dir_outfits = db.Column(db.String(500), default='/ImageModels/lora/Illustrious/Clothing')
|
||||||
|
lora_dir_actions = db.Column(db.String(500), default='/ImageModels/lora/Illustrious/Poses')
|
||||||
|
lora_dir_styles = db.Column(db.String(500), default='/ImageModels/lora/Illustrious/Styles')
|
||||||
|
lora_dir_scenes = db.Column(db.String(500), default='/ImageModels/lora/Illustrious/Backgrounds')
|
||||||
|
lora_dir_detailers = db.Column(db.String(500), default='/ImageModels/lora/Illustrious/Detailers')
|
||||||
|
# Checkpoint scan directories (comma-separated list of absolute paths)
|
||||||
|
checkpoint_dirs = db.Column(db.String(1000), default='/ImageModels/Stable-diffusion/Illustrious,/ImageModels/Stable-diffusion/Noob')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Settings>'
|
return '<Settings>'
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
<div class="card mb-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)">
|
<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 action.image_path %}
|
{% if action.image_path %}
|
||||||
<img src="{{ url_for('static', filename='uploads/' + action.image_path) }}" alt="{{ action.name }}" class="img-fluid">
|
<img src="{{ url_for('static', filename='uploads/' + action.image_path) }}" alt="{{ action.name }}" class="img-fluid" data-preview-path="{{ action.image_path }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">No Image Attached</span>
|
<span class="text-muted">No Image Attached</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -97,35 +97,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if preview_image %}
|
<div class="card mb-4 {% if preview_image %}border-success{% else %}border-secondary d-none{% endif %}" id="preview-card">
|
||||||
<div class="card mb-4 border-success">
|
<div class="card-header {% if preview_image %}bg-success{% else %}bg-secondary{% endif %} text-white d-flex justify-content-between align-items-center" id="preview-card-header">
|
||||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
<span>Selected Preview</span>
|
||||||
<span>Latest Preview</span>
|
|
||||||
<form action="{{ url_for('replace_action_cover_from_preview', slug=action.slug) }}" method="post" class="m-0" id="replace-cover-form">
|
<form action="{{ url_for('replace_action_cover_from_preview', slug=action.slug) }}" method="post" class="m-0" id="replace-cover-form">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn">Replace Cover</button>
|
<input type="hidden" name="preview_path" id="preview-path" value="{{ preview_image or '' }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn" {% if not preview_image %}disabled{% endif %}>Replace Cover</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<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)">
|
<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) if preview_image else '' }}" alt="Preview" class="img-fluid">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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_action_cover_from_preview', slug=action.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 mb-4">
|
||||||
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
||||||
@@ -163,7 +148,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#jsonEditorModal">Edit JSON</button>
|
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#jsonEditorModal">Edit JSON</button>
|
||||||
<a href="{{ url_for('actions_index') }}" class="btn btn-outline-secondary">Back to Gallery</a>
|
<a href="{{ url_for('actions_index') }}" class="btn btn-outline-secondary">Back to Library</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -288,7 +273,8 @@
|
|||||||
class="img-fluid rounded"
|
class="img-fluid rounded"
|
||||||
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
||||||
onclick="showImage(this.src)"
|
onclick="showImage(this.src)"
|
||||||
data-bs-toggle="modal" data-bs-target="#imageModal">
|
data-bs-toggle="modal" data-bs-target="#imageModal"
|
||||||
|
data-preview-path="{{ img }}">
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="col-12 text-muted small" id="gallery-empty">No previews yet. Generate some!</div>
|
<div class="col-12 text-muted small" id="gallery-empty">No previews yet. Generate some!</div>
|
||||||
@@ -314,20 +300,30 @@
|
|||||||
const progressLabel = document.getElementById('progress-label');
|
const progressLabel = document.getElementById('progress-label');
|
||||||
const previewCard = document.getElementById('preview-card');
|
const previewCard = document.getElementById('preview-card');
|
||||||
const previewImg = document.getElementById('preview-img');
|
const previewImg = document.getElementById('preview-img');
|
||||||
|
const previewPath = document.getElementById('preview-path');
|
||||||
|
const replaceBtn = document.getElementById('replace-cover-btn');
|
||||||
|
const previewHeader = document.getElementById('preview-card-header');
|
||||||
const charSelect = document.getElementById('character_select');
|
const charSelect = document.getElementById('character_select');
|
||||||
const charContext = document.getElementById('character-context');
|
const charContext = document.getElementById('character-context');
|
||||||
|
|
||||||
// Toggle character context info
|
|
||||||
charSelect.addEventListener('change', () => {
|
charSelect.addEventListener('change', () => {
|
||||||
if (charSelect.value && charSelect.value !== '__random__') {
|
charContext.classList.toggle('d-none', !charSelect.value || charSelect.value === '__random__');
|
||||||
charContext.classList.remove('d-none');
|
|
||||||
} else {
|
|
||||||
charContext.classList.add('d-none');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let currentJobId = null;
|
function selectPreview(relativePath, imageUrl) {
|
||||||
let currentAction = null;
|
if (!relativePath) return;
|
||||||
|
previewImg.src = imageUrl;
|
||||||
|
previewPath.value = relativePath;
|
||||||
|
replaceBtn.disabled = false;
|
||||||
|
previewCard.classList.remove('d-none');
|
||||||
|
previewHeader.classList.replace('bg-secondary', 'bg-success');
|
||||||
|
previewCard.classList.replace('border-secondary', 'border-success');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
const img = e.target.closest('img[data-preview-path]');
|
||||||
|
if (img) selectPreview(img.dataset.previewPath, img.src);
|
||||||
|
});
|
||||||
|
|
||||||
async function waitForJob(jobId) {
|
async function waitForJob(jobId) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -335,17 +331,9 @@
|
|||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/queue/${jobId}/status`);
|
const resp = await fetch(`/api/queue/${jobId}/status`);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.status === 'done') {
|
if (data.status === 'done') { clearInterval(poll); resolve(data); }
|
||||||
clearInterval(poll);
|
else if (data.status === 'failed' || data.status === 'removed') { clearInterval(poll); reject(new Error(data.error || 'Job failed')); }
|
||||||
resolve(data);
|
else progressLabel.textContent = data.status === 'processing' ? 'Generating…' : 'Queued…';
|
||||||
} else if (data.status === 'failed' || data.status === 'removed') {
|
|
||||||
clearInterval(poll);
|
|
||||||
reject(new Error(data.error || 'Job failed'));
|
|
||||||
} else if (data.status === 'processing') {
|
|
||||||
progressLabel.textContent = 'Generating…';
|
|
||||||
} else {
|
|
||||||
progressLabel.textContent = 'Queued…';
|
|
||||||
}
|
|
||||||
} catch (err) { console.error('Poll error:', err); }
|
} catch (err) { console.error('Poll error:', err); }
|
||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
@@ -355,12 +343,10 @@
|
|||||||
const submitter = e.submitter;
|
const submitter = e.submitter;
|
||||||
if (!submitter || submitter.value !== 'preview') return;
|
if (!submitter || submitter.value !== 'preview') return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
currentAction = submitter.value;
|
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
formData.append('action', currentAction);
|
formData.append('action', 'preview');
|
||||||
progressContainer.classList.remove('d-none');
|
progressContainer.classList.remove('d-none');
|
||||||
progressBar.style.width = '100%';
|
progressBar.style.width = '100%'; progressBar.textContent = '';
|
||||||
progressBar.textContent = '';
|
|
||||||
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
||||||
progressLabel.textContent = 'Queuing…';
|
progressLabel.textContent = 'Queuing…';
|
||||||
try {
|
try {
|
||||||
@@ -368,31 +354,21 @@
|
|||||||
method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.error) { alert('Error: ' + data.error); progressContainer.classList.add('d-none'); return; }
|
if (data.error) { alert('Error: ' + data.error); return; }
|
||||||
currentJobId = data.job_id;
|
progressLabel.textContent = 'Queued…';
|
||||||
const jobResult = await waitForJob(currentJobId);
|
const jobResult = await waitForJob(data.job_id);
|
||||||
currentJobId = null;
|
if (jobResult.result?.image_url) {
|
||||||
if (jobResult.result && jobResult.result.image_url) {
|
selectPreview(jobResult.result.relative_path, jobResult.result.image_url);
|
||||||
previewImg.src = jobResult.result.image_url;
|
addToPreviewGallery(jobResult.result.image_url, jobResult.result.relative_path, '');
|
||||||
if (previewCard) previewCard.classList.remove('d-none');
|
|
||||||
const replaceBtn = document.getElementById('replace-cover-btn');
|
|
||||||
if (replaceBtn) replaceBtn.disabled = false;
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
|
||||||
console.error(err);
|
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
|
||||||
alert('Generation failed: ' + err.message);
|
|
||||||
} finally {
|
|
||||||
progressContainer.classList.add('d-none');
|
|
||||||
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Batch: Generate All Characters
|
|
||||||
const allCharacters = [
|
const allCharacters = [
|
||||||
{% for char in characters %}{ slug: "{{ char.slug }}", name: {{ char.name | tojson }} },
|
{% for char in characters %}{ slug: "{{ char.slug }}", name: {{ char.name | tojson }} },
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
];
|
];
|
||||||
|
|
||||||
let stopBatch = false;
|
let stopBatch = false;
|
||||||
const generateAllBtn = document.getElementById('generate-all-btn');
|
const generateAllBtn = document.getElementById('generate-all-btn');
|
||||||
const stopAllBtn = document.getElementById('stop-all-btn');
|
const stopAllBtn = document.getElementById('stop-all-btn');
|
||||||
@@ -400,7 +376,7 @@
|
|||||||
const batchLabel = document.getElementById('batch-label');
|
const batchLabel = document.getElementById('batch-label');
|
||||||
const batchBar = document.getElementById('batch-bar');
|
const batchBar = document.getElementById('batch-bar');
|
||||||
|
|
||||||
function addToPreviewGallery(imageUrl, charName) {
|
function addToPreviewGallery(imageUrl, relativePath, charName) {
|
||||||
const gallery = document.getElementById('preview-gallery');
|
const gallery = document.getElementById('preview-gallery');
|
||||||
const placeholder = document.getElementById('gallery-empty');
|
const placeholder = document.getElementById('gallery-empty');
|
||||||
if (placeholder) placeholder.remove();
|
if (placeholder) placeholder.remove();
|
||||||
@@ -411,8 +387,9 @@
|
|||||||
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
||||||
onclick="showImage(this.src)"
|
onclick="showImage(this.src)"
|
||||||
data-bs-toggle="modal" data-bs-target="#imageModal"
|
data-bs-toggle="modal" data-bs-target="#imageModal"
|
||||||
|
data-preview-path="${relativePath}"
|
||||||
title="${charName}">
|
title="${charName}">
|
||||||
<div class="position-absolute bottom-0 start-0 w-100 bg-dark bg-opacity-50 text-white p-1 rounded-bottom" style="font-size: 0.7rem; line-height: 1.2;">${charName}</div>
|
${charName ? `<div class="position-absolute bottom-0 start-0 w-100 bg-dark bg-opacity-50 text-white p-1 rounded-bottom" style="font-size: 0.7rem; line-height: 1.2;">${charName}</div>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
gallery.insertBefore(col, gallery.firstChild);
|
gallery.insertBefore(col, gallery.firstChild);
|
||||||
const badge = document.querySelector('#previews-tab .badge');
|
const badge = document.querySelector('#previews-tab .badge');
|
||||||
@@ -427,43 +404,37 @@
|
|||||||
stopAllBtn.classList.remove('d-none');
|
stopAllBtn.classList.remove('d-none');
|
||||||
batchProgress.classList.remove('d-none');
|
batchProgress.classList.remove('d-none');
|
||||||
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
|
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
|
||||||
for (let i = 0; i < allCharacters.length; i++) {
|
|
||||||
|
const genForm = document.getElementById('generate-form');
|
||||||
|
const formAction = genForm.getAttribute('action');
|
||||||
|
batchLabel.textContent = 'Queuing all characters…';
|
||||||
|
const pending = [];
|
||||||
|
for (const char of allCharacters) {
|
||||||
if (stopBatch) break;
|
if (stopBatch) break;
|
||||||
const char = allCharacters[i];
|
|
||||||
batchBar.style.width = `${Math.round((i / allCharacters.length) * 100)}%`;
|
|
||||||
batchLabel.textContent = `Generating ${char.name} (${i + 1} / ${allCharacters.length})`;
|
|
||||||
const genForm = document.getElementById('generate-form');
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
genForm.querySelectorAll('input[name="include_field"]:checked').forEach(cb => fd.append('include_field', cb.value));
|
genForm.querySelectorAll('input[name="include_field"]:checked').forEach(cb => fd.append('include_field', cb.value));
|
||||||
fd.append('character_slug', char.slug);
|
fd.append('character_slug', char.slug);
|
||||||
fd.append('action', 'preview');
|
fd.append('action', 'preview');
|
||||||
try {
|
try {
|
||||||
progressContainer.classList.remove('d-none');
|
const resp = await fetch(formAction, { method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||||
progressBar.style.width = '100%';
|
|
||||||
progressBar.textContent = '';
|
|
||||||
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
|
||||||
progressLabel.textContent = `${char.name}: Queuing…`;
|
|
||||||
const resp = await fetch(genForm.getAttribute('action'), {
|
|
||||||
method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
||||||
});
|
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.error) { console.error(`Error for ${char.name}:`, data.error); progressContainer.classList.add('d-none'); continue; }
|
if (!data.error) pending.push({ char, jobId: data.job_id });
|
||||||
currentJobId = data.job_id;
|
} catch (err) { console.error(`Submit error for ${char.name}:`, err); }
|
||||||
const jobResult = await waitForJob(currentJobId);
|
|
||||||
currentJobId = null;
|
|
||||||
if (jobResult.result && jobResult.result.image_url) {
|
|
||||||
addToPreviewGallery(jobResult.result.image_url, char.name);
|
|
||||||
previewImg.src = jobResult.result.image_url;
|
|
||||||
if (previewCard) previewCard.classList.remove('d-none');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Failed for ${char.name}:`, err);
|
|
||||||
currentJobId = null;
|
|
||||||
} finally {
|
|
||||||
progressContainer.classList.add('d-none');
|
|
||||||
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
batchBar.style.width = '0%';
|
||||||
|
let done = 0;
|
||||||
|
const total = pending.length;
|
||||||
|
batchLabel.textContent = `0 / ${total} complete`;
|
||||||
|
await Promise.all(pending.map(({ char, jobId }) =>
|
||||||
|
waitForJob(jobId).then(result => {
|
||||||
|
done++;
|
||||||
|
batchBar.style.width = `${Math.round((done / total) * 100)}%`;
|
||||||
|
batchLabel.textContent = `${done} / ${total} complete`;
|
||||||
|
if (result.result?.image_url) addToPreviewGallery(result.result.image_url, result.result.relative_path, char.name);
|
||||||
|
}).catch(err => { done++; console.error(`Failed for ${char.name}:`, err); })
|
||||||
|
));
|
||||||
|
|
||||||
batchBar.style.width = '100%';
|
batchBar.style.width = '100%';
|
||||||
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
|
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
|
||||||
generateAllBtn.disabled = false;
|
generateAllBtn.disabled = false;
|
||||||
@@ -474,10 +445,9 @@
|
|||||||
stopAllBtn.addEventListener('click', () => {
|
stopAllBtn.addEventListener('click', () => {
|
||||||
stopBatch = true;
|
stopBatch = true;
|
||||||
stopAllBtn.classList.add('d-none');
|
stopAllBtn.classList.add('d-none');
|
||||||
batchLabel.textContent = 'Stopping after current generation...';
|
batchLabel.textContent = 'Stopping…';
|
||||||
});
|
});
|
||||||
|
|
||||||
// JSON Editor
|
|
||||||
initJsonEditor('{{ url_for("save_action_json", slug=action.slug) }}');
|
initJsonEditor('{{ url_for("save_action_json", slug=action.slug) }}');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2>Action Gallery</h2>
|
<h2>Action Library</h2>
|
||||||
<div class="d-flex gap-1 align-items-center">
|
<div class="d-flex gap-1 align-items-center">
|
||||||
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for actions without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for actions without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all actions"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all actions"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
|
|||||||
@@ -47,7 +47,8 @@
|
|||||||
data-bs-toggle="modal" data-bs-target="#imageModal"
|
data-bs-toggle="modal" data-bs-target="#imageModal"
|
||||||
onclick="showImage(this.querySelector('img') ? this.querySelector('img').src : '')">
|
onclick="showImage(this.querySelector('img') ? this.querySelector('img').src : '')">
|
||||||
{% if ckpt.image_path %}
|
{% if ckpt.image_path %}
|
||||||
<img src="{{ url_for('static', filename='uploads/' + ckpt.image_path) }}" alt="{{ ckpt.name }}" class="img-fluid">
|
<img src="{{ url_for('static', filename='uploads/' + ckpt.image_path) }}" alt="{{ ckpt.name }}" class="img-fluid"
|
||||||
|
data-preview-path="{{ ckpt.image_path }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="d-flex align-items-center justify-content-center bg-light" style="height: 400px;">
|
<div class="d-flex align-items-center justify-content-center bg-light" style="height: 400px;">
|
||||||
<span class="text-muted">No Image Attached</span>
|
<span class="text-muted">No Image Attached</span>
|
||||||
@@ -89,39 +90,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if preview_image %}
|
<div class="card mb-4 {% if preview_image %}border-success{% else %}border-secondary d-none{% endif %}" id="preview-card">
|
||||||
<div class="card mb-4 border-success">
|
<div class="card-header {% if preview_image %}bg-success{% else %}bg-secondary{% endif %} text-white d-flex justify-content-between align-items-center p-2" id="preview-card-header">
|
||||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center p-2">
|
<small>Selected Preview</small>
|
||||||
<small>Latest Preview</small>
|
|
||||||
<form action="{{ url_for('replace_checkpoint_cover_from_preview', slug=ckpt.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 p-2">
|
|
||||||
<small>Latest Preview</small>
|
|
||||||
<form action="{{ url_for('replace_checkpoint_cover_from_preview', slug=ckpt.slug) }}" method="post" class="m-0" id="replace-cover-form">
|
<form action="{{ url_for('replace_checkpoint_cover_from_preview', slug=ckpt.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>
|
<input type="hidden" name="preview_path" id="preview-path" value="{{ preview_image or '' }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn" {% if not preview_image %}disabled{% endif %}>Replace Cover</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="img-container" style="height: auto; min-height: 400px; cursor: pointer;"
|
<div class="img-container" style="height: auto; min-height: 400px; cursor: pointer;"
|
||||||
data-bs-toggle="modal" data-bs-target="#imageModal"
|
data-bs-toggle="modal" data-bs-target="#imageModal"
|
||||||
onclick="showImage(this.querySelector('img').src)">
|
onclick="showImage(this.querySelector('img') ? this.querySelector('img').src : '')">
|
||||||
<img id="preview-img" src="" alt="Preview" class="img-fluid">
|
<img id="preview-img" src="{{ url_for('static', filename='uploads/' + preview_image) if preview_image else '' }}" alt="Preview" class="img-fluid">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
@@ -131,7 +115,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#jsonEditorModal">Edit JSON</button>
|
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#jsonEditorModal">Edit JSON</button>
|
||||||
<a href="{{ url_for('checkpoints_index') }}" class="btn btn-outline-secondary">Back to Gallery</a>
|
<a href="{{ url_for('checkpoints_index') }}" class="btn btn-outline-secondary">Back to Library</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -238,6 +222,7 @@
|
|||||||
<img src="{{ url_for('static', filename='uploads/' + img) }}"
|
<img src="{{ url_for('static', filename='uploads/' + img) }}"
|
||||||
class="img-fluid rounded"
|
class="img-fluid rounded"
|
||||||
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
||||||
|
data-preview-path="{{ img }}"
|
||||||
onclick="showImage(this.src)"
|
onclick="showImage(this.src)"
|
||||||
data-bs-toggle="modal" data-bs-target="#imageModal">
|
data-bs-toggle="modal" data-bs-target="#imageModal">
|
||||||
</div>
|
</div>
|
||||||
@@ -259,9 +244,25 @@
|
|||||||
const progressContainer = document.getElementById('progress-container');
|
const progressContainer = document.getElementById('progress-container');
|
||||||
const progressLabel = document.getElementById('progress-label');
|
const progressLabel = document.getElementById('progress-label');
|
||||||
const previewCard = document.getElementById('preview-card');
|
const previewCard = document.getElementById('preview-card');
|
||||||
|
const previewCardHeader = document.getElementById('preview-card-header');
|
||||||
const previewImg = document.getElementById('preview-img');
|
const previewImg = document.getElementById('preview-img');
|
||||||
|
const previewPath = document.getElementById('preview-path');
|
||||||
|
const replaceBtn = document.getElementById('replace-cover-btn');
|
||||||
|
|
||||||
let currentJobId = null;
|
function selectPreview(relativePath, imageUrl) {
|
||||||
|
if (!relativePath) return;
|
||||||
|
previewImg.src = imageUrl;
|
||||||
|
previewPath.value = relativePath;
|
||||||
|
replaceBtn.disabled = false;
|
||||||
|
previewCard.classList.remove('d-none');
|
||||||
|
previewCardHeader.classList.replace('bg-secondary', 'bg-success');
|
||||||
|
previewCard.classList.replace('border-secondary', 'border-success');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
const img = e.target.closest('img[data-preview-path]');
|
||||||
|
if (img) selectPreview(img.dataset.previewPath, img.src);
|
||||||
|
});
|
||||||
|
|
||||||
async function waitForJob(jobId) {
|
async function waitForJob(jobId) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -294,15 +295,8 @@
|
|||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.error) { alert('Error: ' + data.error); progressContainer.classList.add('d-none'); return; }
|
if (data.error) { alert('Error: ' + data.error); progressContainer.classList.add('d-none'); return; }
|
||||||
currentJobId = data.job_id;
|
const jobResult = await waitForJob(data.job_id);
|
||||||
const jobResult = await waitForJob(currentJobId);
|
if (jobResult.result?.image_url) selectPreview(jobResult.result.relative_path, jobResult.result.image_url);
|
||||||
currentJobId = null;
|
|
||||||
if (jobResult.result && jobResult.result.image_url) {
|
|
||||||
previewImg.src = jobResult.result.image_url;
|
|
||||||
if (previewCard) previewCard.classList.remove('d-none');
|
|
||||||
const replaceBtn = document.getElementById('replace-cover-btn');
|
|
||||||
if (replaceBtn) replaceBtn.disabled = false;
|
|
||||||
}
|
|
||||||
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
|
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
|
||||||
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
|
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
|
||||||
});
|
});
|
||||||
@@ -320,7 +314,7 @@
|
|||||||
const batchLabel = document.getElementById('batch-label');
|
const batchLabel = document.getElementById('batch-label');
|
||||||
const batchBar = document.getElementById('batch-bar');
|
const batchBar = document.getElementById('batch-bar');
|
||||||
|
|
||||||
function addToPreviewGallery(imageUrl, charName) {
|
function addToPreviewGallery(imageUrl, relativePath, charName) {
|
||||||
const gallery = document.getElementById('preview-gallery');
|
const gallery = document.getElementById('preview-gallery');
|
||||||
const placeholder = document.getElementById('gallery-empty');
|
const placeholder = document.getElementById('gallery-empty');
|
||||||
if (placeholder) placeholder.remove();
|
if (placeholder) placeholder.remove();
|
||||||
@@ -329,6 +323,7 @@
|
|||||||
col.innerHTML = `<div class="position-relative">
|
col.innerHTML = `<div class="position-relative">
|
||||||
<img src="${imageUrl}" class="img-fluid rounded"
|
<img src="${imageUrl}" class="img-fluid rounded"
|
||||||
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
||||||
|
data-preview-path="${relativePath}"
|
||||||
onclick="showImage(this.src)"
|
onclick="showImage(this.src)"
|
||||||
data-bs-toggle="modal" data-bs-target="#imageModal"
|
data-bs-toggle="modal" data-bs-target="#imageModal"
|
||||||
title="${charName}">
|
title="${charName}">
|
||||||
@@ -347,36 +342,36 @@
|
|||||||
stopAllBtn.classList.remove('d-none');
|
stopAllBtn.classList.remove('d-none');
|
||||||
batchProgress.classList.remove('d-none');
|
batchProgress.classList.remove('d-none');
|
||||||
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
|
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
|
||||||
for (let i = 0; i < allCharacters.length; i++) {
|
|
||||||
|
// Phase 1: submit all jobs immediately
|
||||||
|
const pending = [];
|
||||||
|
for (const char of allCharacters) {
|
||||||
if (stopBatch) break;
|
if (stopBatch) break;
|
||||||
const char = allCharacters[i];
|
|
||||||
batchBar.style.width = `${Math.round((i / allCharacters.length) * 100)}%`;
|
|
||||||
batchLabel.textContent = `Generating ${char.name} (${i + 1} / ${allCharacters.length})`;
|
|
||||||
const genForm = document.getElementById('generate-form');
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('character_slug', char.slug);
|
fd.append('character_slug', char.slug);
|
||||||
fd.append('action', 'preview');
|
fd.append('action', 'preview');
|
||||||
try {
|
try {
|
||||||
progressContainer.classList.remove('d-none');
|
const resp = await fetch(form.getAttribute('action'), {
|
||||||
progressBar.style.width = '100%'; progressBar.textContent = '';
|
|
||||||
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
|
||||||
progressLabel.textContent = `${char.name}: Queuing…`;
|
|
||||||
const resp = await fetch(genForm.getAttribute('action'), {
|
|
||||||
method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.error) { console.error(`Error for ${char.name}:`, data.error); progressContainer.classList.add('d-none'); continue; }
|
if (!data.error) pending.push({ char, jobId: data.job_id });
|
||||||
currentJobId = data.job_id;
|
} catch (err) { console.error(`Submit error for ${char.name}:`, err); }
|
||||||
const jobResult = await waitForJob(currentJobId);
|
|
||||||
currentJobId = null;
|
|
||||||
if (jobResult.result && jobResult.result.image_url) {
|
|
||||||
addToPreviewGallery(jobResult.result.image_url, char.name);
|
|
||||||
previewImg.src = jobResult.result.image_url;
|
|
||||||
if (previewCard) previewCard.classList.remove('d-none');
|
|
||||||
}
|
|
||||||
} catch (err) { console.error(`Failed for ${char.name}:`, err); currentJobId = null; }
|
|
||||||
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2: poll all in parallel
|
||||||
|
batchLabel.textContent = `0 / ${pending.length} complete`;
|
||||||
|
let done = 0;
|
||||||
|
const total = pending.length;
|
||||||
|
await Promise.all(pending.map(({ char, jobId }) =>
|
||||||
|
waitForJob(jobId).then(result => {
|
||||||
|
done++;
|
||||||
|
batchBar.style.width = `${Math.round((done / total) * 100)}%`;
|
||||||
|
batchLabel.textContent = `${done} / ${total} complete`;
|
||||||
|
if (result.result?.image_url) addToPreviewGallery(result.result.image_url, result.result.relative_path, char.name);
|
||||||
|
}).catch(err => { done++; console.error(`Failed for ${char.name}:`, err); })
|
||||||
|
));
|
||||||
|
|
||||||
batchBar.style.width = '100%';
|
batchBar.style.width = '100%';
|
||||||
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
|
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
|
||||||
generateAllBtn.disabled = false;
|
generateAllBtn.disabled = false;
|
||||||
@@ -387,7 +382,7 @@
|
|||||||
stopAllBtn.addEventListener('click', () => {
|
stopAllBtn.addEventListener('click', () => {
|
||||||
stopBatch = true;
|
stopBatch = true;
|
||||||
stopAllBtn.classList.add('d-none');
|
stopAllBtn.classList.add('d-none');
|
||||||
batchLabel.textContent = 'Stopping after current generation...';
|
batchLabel.textContent = 'Stopping after current submissions...';
|
||||||
});
|
});
|
||||||
|
|
||||||
// JSON Editor
|
// JSON Editor
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2>Checkpoint Gallery</h2>
|
<h2>Checkpoint Library</h2>
|
||||||
<div class="d-flex gap-1 align-items-center">
|
<div class="d-flex gap-1 align-items-center">
|
||||||
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for checkpoints without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for checkpoints without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all checkpoints"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all checkpoints"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<div class="card mb-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)">
|
<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" data-preview-path="{{ character.image_path }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">No Image Attached</span>
|
<span class="text-muted">No Image Attached</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -44,35 +44,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if preview_image %}
|
<div class="card mb-4 {% if preview_image %}border-success{% else %}border-secondary d-none{% endif %}" id="preview-card">
|
||||||
<div class="card mb-4 border-success">
|
<div class="card-header {% if preview_image %}bg-success{% else %}bg-secondary{% endif %} text-white d-flex justify-content-between align-items-center" id="preview-card-header">
|
||||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
<span>Selected Preview</span>
|
||||||
<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="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_cover_from_preview', slug=character.slug) }}" method="post" class="m-0" id="replace-cover-form">
|
<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>
|
<input type="hidden" name="preview_path" id="preview-path" value="{{ preview_image or '' }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn" {% if not preview_image %}disabled{% endif %}>Replace Cover</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<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)">
|
<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="{{ url_for('static', filename='uploads/' + preview_image) if preview_image else '' }}" alt="Preview" class="img-fluid">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
||||||
@@ -103,7 +88,7 @@
|
|||||||
<h1 class="mb-0">{{ character.name }}</h1>
|
<h1 class="mb-0">{{ character.name }}</h1>
|
||||||
<a href="{{ url_for('edit_character', slug=character.slug) }}" class="btn btn-sm btn-link text-decoration-none">Edit Profile</a>
|
<a href="{{ url_for('edit_character', slug=character.slug) }}" class="btn btn-sm btn-link text-decoration-none">Edit Profile</a>
|
||||||
</div>
|
</div>
|
||||||
<a href="/" class="btn btn-outline-secondary">Back to Gallery</a>
|
<a href="/" class="btn btn-outline-secondary">Back to Library</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Outfit Switcher -->
|
<!-- Outfit Switcher -->
|
||||||
@@ -227,9 +212,27 @@
|
|||||||
const progressLabel = document.getElementById('progress-label');
|
const progressLabel = document.getElementById('progress-label');
|
||||||
const previewCard = document.getElementById('preview-card');
|
const previewCard = document.getElementById('preview-card');
|
||||||
const previewImg = document.getElementById('preview-img');
|
const previewImg = document.getElementById('preview-img');
|
||||||
|
const previewPath = document.getElementById('preview-path');
|
||||||
|
const replaceBtn = document.getElementById('replace-cover-btn');
|
||||||
|
const previewHeader = document.getElementById('preview-card-header');
|
||||||
|
|
||||||
let currentJobId = null;
|
let currentJobId = null;
|
||||||
let currentAction = null;
|
|
||||||
|
function selectPreview(relativePath, imageUrl) {
|
||||||
|
if (!relativePath) return;
|
||||||
|
previewImg.src = imageUrl;
|
||||||
|
previewPath.value = relativePath;
|
||||||
|
replaceBtn.disabled = false;
|
||||||
|
previewCard.classList.remove('d-none');
|
||||||
|
previewHeader.classList.replace('bg-secondary', 'bg-success');
|
||||||
|
previewCard.classList.replace('border-secondary', 'border-success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clicking any image with data-preview-path selects it into the preview pane
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
const img = e.target.closest('img[data-preview-path]');
|
||||||
|
if (img) selectPreview(img.dataset.previewPath, img.src);
|
||||||
|
});
|
||||||
|
|
||||||
async function waitForJob(jobId) {
|
async function waitForJob(jobId) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -254,55 +257,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.addEventListener('submit', async (e) => {
|
form.addEventListener('submit', async (e) => {
|
||||||
// Only intercept generate actions
|
|
||||||
const submitter = e.submitter;
|
const submitter = e.submitter;
|
||||||
if (!submitter || submitter.value !== 'preview') {
|
if (!submitter || submitter.value !== 'preview') return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
currentAction = submitter.value;
|
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
formData.append('action', currentAction);
|
formData.append('action', 'preview');
|
||||||
|
|
||||||
// UI Reset
|
|
||||||
progressContainer.classList.remove('d-none');
|
progressContainer.classList.remove('d-none');
|
||||||
progressBar.style.width = '100%';
|
progressBar.style.width = '100%';
|
||||||
progressBar.textContent = '';
|
progressBar.textContent = '';
|
||||||
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
||||||
progressLabel.textContent = 'Queuing…';
|
progressLabel.textContent = 'Queuing…';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(form.getAttribute('action'), {
|
const response = await fetch(form.getAttribute('action'), {
|
||||||
method: 'POST',
|
method: 'POST', body: formData,
|
||||||
body: formData,
|
|
||||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
if (data.error) { alert('Error: ' + data.error); return; }
|
||||||
if (data.error) {
|
|
||||||
alert('Error: ' + data.error);
|
|
||||||
progressContainer.classList.add('d-none');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentJobId = data.job_id;
|
currentJobId = data.job_id;
|
||||||
progressLabel.textContent = 'Queued…';
|
progressLabel.textContent = 'Queued…';
|
||||||
|
|
||||||
// Wait for the background worker to finish
|
|
||||||
const jobResult = await waitForJob(currentJobId);
|
const jobResult = await waitForJob(currentJobId);
|
||||||
currentJobId = null;
|
currentJobId = null;
|
||||||
|
|
||||||
// Image is already saved — just display it
|
if (jobResult.result?.image_url) {
|
||||||
if (jobResult.result && jobResult.result.image_url) {
|
selectPreview(jobResult.result.relative_path, jobResult.result.image_url);
|
||||||
previewImg.src = jobResult.result.image_url;
|
|
||||||
if (previewCard) previewCard.classList.remove('d-none');
|
|
||||||
const replaceBtn = document.getElementById('replace-cover-btn');
|
|
||||||
if (replaceBtn) replaceBtn.disabled = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
alert('Generation failed: ' + err.message);
|
alert('Generation failed: ' + err.message);
|
||||||
@@ -312,8 +295,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Image modal function
|
|
||||||
function showImage(src) {
|
function showImage(src) {
|
||||||
document.getElementById('modalImage').src = src;
|
document.getElementById('modalImage').src = src;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
<div class="card mb-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)">
|
<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 detailer.image_path %}
|
{% if detailer.image_path %}
|
||||||
<img src="{{ url_for('static', filename='uploads/' + detailer.image_path) }}" alt="{{ detailer.name }}" class="img-fluid">
|
<img src="{{ url_for('static', filename='uploads/' + detailer.image_path) }}" alt="{{ detailer.name }}" class="img-fluid" data-preview-path="{{ detailer.image_path }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="d-flex align-items-center justify-content-center bg-light" style="height: 400px;">
|
<div class="d-flex align-items-center justify-content-center bg-light" style="height: 400px;">
|
||||||
<span class="text-muted">No Image Attached</span>
|
<span class="text-muted">No Image Attached</span>
|
||||||
@@ -121,35 +121,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if preview_image %}
|
<div class="card mb-4 {% if preview_image %}border-success{% else %}border-secondary d-none{% endif %}" id="preview-card">
|
||||||
<div class="card mb-4 border-success">
|
<div class="card-header {% if preview_image %}bg-success{% else %}bg-secondary{% endif %} text-white d-flex justify-content-between align-items-center p-2" id="preview-card-header">
|
||||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center p-2">
|
<small>Selected Preview</small>
|
||||||
<small>Latest Preview</small>
|
|
||||||
<form action="{{ url_for('replace_detailer_cover_from_preview', slug=detailer.slug) }}" method="post" class="m-0" id="replace-cover-form">
|
<form action="{{ url_for('replace_detailer_cover_from_preview', slug=detailer.slug) }}" method="post" class="m-0" id="replace-cover-form">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn">Replace Cover</button>
|
<input type="hidden" name="preview_path" id="preview-path" value="{{ preview_image or '' }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn" {% if not preview_image %}disabled{% endif %}>Replace Cover</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<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)">
|
<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) if preview_image else '' }}" alt="Preview" class="img-fluid">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 p-2">
|
|
||||||
<small>Latest Preview</small>
|
|
||||||
<form action="{{ url_for('replace_detailer_cover_from_preview', slug=detailer.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>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
@@ -162,7 +147,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#jsonEditorModal">Edit JSON</button>
|
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#jsonEditorModal">Edit JSON</button>
|
||||||
<a href="{{ url_for('detailers_index') }}" class="btn btn-outline-secondary">Back to Gallery</a>
|
<a href="{{ url_for('detailers_index') }}" class="btn btn-outline-secondary">Back to Library</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -257,7 +242,8 @@
|
|||||||
class="img-fluid rounded"
|
class="img-fluid rounded"
|
||||||
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
||||||
onclick="showImage(this.src)"
|
onclick="showImage(this.src)"
|
||||||
data-bs-toggle="modal" data-bs-target="#imageModal">
|
data-bs-toggle="modal" data-bs-target="#imageModal"
|
||||||
|
data-preview-path="{{ img }}">
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="col-12 text-muted small" id="gallery-empty">No previews yet. Generate some!</div>
|
<div class="col-12 text-muted small" id="gallery-empty">No previews yet. Generate some!</div>
|
||||||
@@ -283,21 +269,31 @@
|
|||||||
const progressLabel = document.getElementById('progress-label');
|
const progressLabel = document.getElementById('progress-label');
|
||||||
const previewCard = document.getElementById('preview-card');
|
const previewCard = document.getElementById('preview-card');
|
||||||
const previewImg = document.getElementById('preview-img');
|
const previewImg = document.getElementById('preview-img');
|
||||||
|
const previewPath = document.getElementById('preview-path');
|
||||||
|
const replaceBtn = document.getElementById('replace-cover-btn');
|
||||||
|
const previewHeader = document.getElementById('preview-card-header');
|
||||||
const charSelect = document.getElementById('character_select');
|
const charSelect = document.getElementById('character_select');
|
||||||
const charContext = document.getElementById('character-context');
|
const charContext = document.getElementById('character-context');
|
||||||
const actionSelect = document.getElementById('action_select');
|
const actionSelect = document.getElementById('action_select');
|
||||||
|
|
||||||
// Toggle character context info
|
|
||||||
charSelect.addEventListener('change', () => {
|
charSelect.addEventListener('change', () => {
|
||||||
if (charSelect.value && charSelect.value !== '__random__') {
|
charContext.classList.toggle('d-none', !charSelect.value || charSelect.value === '__random__');
|
||||||
charContext.classList.remove('d-none');
|
|
||||||
} else {
|
|
||||||
charContext.classList.add('d-none');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let currentJobId = null;
|
function selectPreview(relativePath, imageUrl) {
|
||||||
let currentAction = null;
|
if (!relativePath) return;
|
||||||
|
previewImg.src = imageUrl;
|
||||||
|
previewPath.value = relativePath;
|
||||||
|
replaceBtn.disabled = false;
|
||||||
|
previewCard.classList.remove('d-none');
|
||||||
|
previewHeader.classList.replace('bg-secondary', 'bg-success');
|
||||||
|
previewCard.classList.replace('border-secondary', 'border-success');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
const img = e.target.closest('img[data-preview-path]');
|
||||||
|
if (img) selectPreview(img.dataset.previewPath, img.src);
|
||||||
|
});
|
||||||
|
|
||||||
async function waitForJob(jobId) {
|
async function waitForJob(jobId) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -307,8 +303,7 @@
|
|||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.status === 'done') { clearInterval(poll); resolve(data); }
|
if (data.status === 'done') { clearInterval(poll); resolve(data); }
|
||||||
else if (data.status === 'failed' || data.status === 'removed') { clearInterval(poll); reject(new Error(data.error || 'Job failed')); }
|
else if (data.status === 'failed' || data.status === 'removed') { clearInterval(poll); reject(new Error(data.error || 'Job failed')); }
|
||||||
else if (data.status === 'processing') progressLabel.textContent = 'Generating…';
|
else progressLabel.textContent = data.status === 'processing' ? 'Generating…' : 'Queued…';
|
||||||
else progressLabel.textContent = 'Queued…';
|
|
||||||
} catch (err) { console.error('Poll error:', err); }
|
} catch (err) { console.error('Poll error:', err); }
|
||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
@@ -318,9 +313,8 @@
|
|||||||
const submitter = e.submitter;
|
const submitter = e.submitter;
|
||||||
if (!submitter || submitter.value !== 'preview') return;
|
if (!submitter || submitter.value !== 'preview') return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
currentAction = submitter.value;
|
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
formData.append('action', currentAction);
|
formData.append('action', 'preview');
|
||||||
progressContainer.classList.remove('d-none');
|
progressContainer.classList.remove('d-none');
|
||||||
progressBar.style.width = '100%'; progressBar.textContent = '';
|
progressBar.style.width = '100%'; progressBar.textContent = '';
|
||||||
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
||||||
@@ -330,26 +324,21 @@
|
|||||||
method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.error) { alert('Error: ' + data.error); progressContainer.classList.add('d-none'); return; }
|
if (data.error) { alert('Error: ' + data.error); return; }
|
||||||
currentJobId = data.job_id;
|
progressLabel.textContent = 'Queued…';
|
||||||
const jobResult = await waitForJob(currentJobId);
|
const jobResult = await waitForJob(data.job_id);
|
||||||
currentJobId = null;
|
if (jobResult.result?.image_url) {
|
||||||
if (jobResult.result && jobResult.result.image_url) {
|
selectPreview(jobResult.result.relative_path, jobResult.result.image_url);
|
||||||
previewImg.src = jobResult.result.image_url;
|
addToPreviewGallery(jobResult.result.image_url, jobResult.result.relative_path, '');
|
||||||
if (previewCard) previewCard.classList.remove('d-none');
|
|
||||||
const replaceBtn = document.getElementById('replace-cover-btn');
|
|
||||||
if (replaceBtn) replaceBtn.disabled = false;
|
|
||||||
}
|
}
|
||||||
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
|
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
|
||||||
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
|
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Batch: Generate All Characters
|
|
||||||
const allCharacters = [
|
const allCharacters = [
|
||||||
{% for char in characters %}{ slug: "{{ char.slug }}", name: {{ char.name | tojson }} },
|
{% for char in characters %}{ slug: "{{ char.slug }}", name: {{ char.name | tojson }} },
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
];
|
];
|
||||||
|
|
||||||
let stopBatch = false;
|
let stopBatch = false;
|
||||||
const generateAllBtn = document.getElementById('generate-all-btn');
|
const generateAllBtn = document.getElementById('generate-all-btn');
|
||||||
const stopAllBtn = document.getElementById('stop-all-btn');
|
const stopAllBtn = document.getElementById('stop-all-btn');
|
||||||
@@ -357,7 +346,7 @@
|
|||||||
const batchLabel = document.getElementById('batch-label');
|
const batchLabel = document.getElementById('batch-label');
|
||||||
const batchBar = document.getElementById('batch-bar');
|
const batchBar = document.getElementById('batch-bar');
|
||||||
|
|
||||||
function addToPreviewGallery(imageUrl, charName) {
|
function addToPreviewGallery(imageUrl, relativePath, charName) {
|
||||||
const gallery = document.getElementById('preview-gallery');
|
const gallery = document.getElementById('preview-gallery');
|
||||||
const placeholder = document.getElementById('gallery-empty');
|
const placeholder = document.getElementById('gallery-empty');
|
||||||
if (placeholder) placeholder.remove();
|
if (placeholder) placeholder.remove();
|
||||||
@@ -368,8 +357,9 @@
|
|||||||
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
||||||
onclick="showImage(this.src)"
|
onclick="showImage(this.src)"
|
||||||
data-bs-toggle="modal" data-bs-target="#imageModal"
|
data-bs-toggle="modal" data-bs-target="#imageModal"
|
||||||
|
data-preview-path="${relativePath}"
|
||||||
title="${charName}">
|
title="${charName}">
|
||||||
<div class="position-absolute bottom-0 start-0 w-100 bg-dark bg-opacity-50 text-white p-1 rounded-bottom" style="font-size: 0.7rem; line-height: 1.2;">${charName}</div>
|
${charName ? `<div class="position-absolute bottom-0 start-0 w-100 bg-dark bg-opacity-50 text-white p-1 rounded-bottom" style="font-size: 0.7rem; line-height: 1.2;">${charName}</div>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
gallery.insertBefore(col, gallery.firstChild);
|
gallery.insertBefore(col, gallery.firstChild);
|
||||||
const badge = document.querySelector('#previews-tab .badge');
|
const badge = document.querySelector('#previews-tab .badge');
|
||||||
@@ -384,12 +374,13 @@
|
|||||||
stopAllBtn.classList.remove('d-none');
|
stopAllBtn.classList.remove('d-none');
|
||||||
batchProgress.classList.remove('d-none');
|
batchProgress.classList.remove('d-none');
|
||||||
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
|
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
|
||||||
for (let i = 0; i < allCharacters.length; i++) {
|
|
||||||
|
const genForm = document.getElementById('generate-form');
|
||||||
|
const formAction = genForm.getAttribute('action');
|
||||||
|
batchLabel.textContent = 'Queuing all characters…';
|
||||||
|
const pending = [];
|
||||||
|
for (const char of allCharacters) {
|
||||||
if (stopBatch) break;
|
if (stopBatch) break;
|
||||||
const char = allCharacters[i];
|
|
||||||
batchBar.style.width = `${Math.round((i / allCharacters.length) * 100)}%`;
|
|
||||||
batchLabel.textContent = `Generating ${char.name} (${i + 1} / ${allCharacters.length})`;
|
|
||||||
const genForm = document.getElementById('generate-form');
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
genForm.querySelectorAll('input[name="include_field"]:checked').forEach(cb => fd.append('include_field', cb.value));
|
genForm.querySelectorAll('input[name="include_field"]:checked').forEach(cb => fd.append('include_field', cb.value));
|
||||||
fd.append('character_slug', char.slug);
|
fd.append('character_slug', char.slug);
|
||||||
@@ -398,26 +389,25 @@
|
|||||||
fd.append('extra_negative', document.getElementById('extra_negative').value);
|
fd.append('extra_negative', document.getElementById('extra_negative').value);
|
||||||
fd.append('action', 'preview');
|
fd.append('action', 'preview');
|
||||||
try {
|
try {
|
||||||
progressContainer.classList.remove('d-none');
|
const resp = await fetch(formAction, { method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||||
progressBar.style.width = '100%'; progressBar.textContent = '';
|
|
||||||
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
|
||||||
progressLabel.textContent = `${char.name}: Queuing…`;
|
|
||||||
const resp = await fetch(genForm.getAttribute('action'), {
|
|
||||||
method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
||||||
});
|
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.error) { console.error(`Error for ${char.name}:`, data.error); progressContainer.classList.add('d-none'); continue; }
|
if (!data.error) pending.push({ char, jobId: data.job_id });
|
||||||
currentJobId = data.job_id;
|
} catch (err) { console.error(`Submit error for ${char.name}:`, err); }
|
||||||
const jobResult = await waitForJob(currentJobId);
|
|
||||||
currentJobId = null;
|
|
||||||
if (jobResult.result && jobResult.result.image_url) {
|
|
||||||
addToPreviewGallery(jobResult.result.image_url, char.name);
|
|
||||||
previewImg.src = jobResult.result.image_url;
|
|
||||||
if (previewCard) previewCard.classList.remove('d-none');
|
|
||||||
}
|
|
||||||
} catch (err) { console.error(`Failed for ${char.name}:`, err); currentJobId = null; }
|
|
||||||
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
batchBar.style.width = '0%';
|
||||||
|
let done = 0;
|
||||||
|
const total = pending.length;
|
||||||
|
batchLabel.textContent = `0 / ${total} complete`;
|
||||||
|
await Promise.all(pending.map(({ char, jobId }) =>
|
||||||
|
waitForJob(jobId).then(result => {
|
||||||
|
done++;
|
||||||
|
batchBar.style.width = `${Math.round((done / total) * 100)}%`;
|
||||||
|
batchLabel.textContent = `${done} / ${total} complete`;
|
||||||
|
if (result.result?.image_url) addToPreviewGallery(result.result.image_url, result.result.relative_path, char.name);
|
||||||
|
}).catch(err => { done++; console.error(`Failed for ${char.name}:`, err); })
|
||||||
|
));
|
||||||
|
|
||||||
batchBar.style.width = '100%';
|
batchBar.style.width = '100%';
|
||||||
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
|
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
|
||||||
generateAllBtn.disabled = false;
|
generateAllBtn.disabled = false;
|
||||||
@@ -428,10 +418,9 @@
|
|||||||
stopAllBtn.addEventListener('click', () => {
|
stopAllBtn.addEventListener('click', () => {
|
||||||
stopBatch = true;
|
stopBatch = true;
|
||||||
stopAllBtn.classList.add('d-none');
|
stopAllBtn.classList.add('d-none');
|
||||||
batchLabel.textContent = 'Stopping after current generation...';
|
batchLabel.textContent = 'Stopping…';
|
||||||
});
|
});
|
||||||
|
|
||||||
// JSON Editor
|
|
||||||
initJsonEditor('{{ url_for("save_detailer_json", slug=detailer.slug) }}');
|
initJsonEditor('{{ url_for("save_detailer_json", slug=detailer.slug) }}');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2>Detailer Gallery</h2>
|
<h2>Detailer Library</h2>
|
||||||
<div class="d-flex gap-1 align-items-center">
|
<div class="d-flex gap-1 align-items-center">
|
||||||
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for detailers without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for detailers without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all detailers"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all detailers"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
<h4 class="mb-0">Gallery
|
<h4 class="mb-0">Image Gallery
|
||||||
<span class="text-muted fs-6 fw-normal ms-2">{{ total }} image{{ 's' if total != 1 else '' }}</span>
|
<span class="text-muted fs-6 fw-normal ms-2">{{ total }} image{{ 's' if total != 1 else '' }}</span>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2>Character Gallery</h2>
|
<h2>Character Library</h2>
|
||||||
<div class="d-flex gap-1 align-items-center">
|
<div class="d-flex gap-1 align-items-center">
|
||||||
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for characters without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for characters without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all characters"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all characters"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<div class="vr mx-1 d-none d-lg-block"></div>
|
<div class="vr mx-1 d-none d-lg-block"></div>
|
||||||
<a href="/create" class="btn btn-sm btn-outline-success">+ Character</a>
|
<a href="/create" class="btn btn-sm btn-outline-success">+ Character</a>
|
||||||
<a href="/generator" class="btn btn-sm btn-outline-light">Generator</a>
|
<a href="/generator" class="btn btn-sm btn-outline-light">Generator</a>
|
||||||
<a href="/gallery" class="btn btn-sm btn-outline-light">Gallery</a>
|
<a href="/gallery" class="btn btn-sm btn-outline-light">Image Gallery</a>
|
||||||
<a href="/settings" class="btn btn-sm btn-outline-light">Settings</a>
|
<a href="/settings" class="btn btn-sm btn-outline-light">Settings</a>
|
||||||
<div class="vr mx-1 d-none d-lg-block"></div>
|
<div class="vr mx-1 d-none d-lg-block"></div>
|
||||||
<!-- Queue indicator -->
|
<!-- Queue indicator -->
|
||||||
@@ -48,19 +48,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="default-checkpoint-bar border-bottom mb-4">
|
|
||||||
<div class="container d-flex align-items-center gap-2 py-2">
|
|
||||||
<small class="text-muted text-nowrap">Default checkpoint:</small>
|
|
||||||
<select id="defaultCheckpointSelect" class="form-select form-select-sm" style="max-width: 320px;">
|
|
||||||
<option value="">— workflow default —</option>
|
|
||||||
{% for ckpt in all_checkpoints %}
|
|
||||||
<option value="{{ ckpt.checkpoint_path }}"{% if ckpt.checkpoint_path == default_checkpoint_path %} selected{% endif %}>{{ ckpt.name }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<small id="checkpointSaveStatus" class="text-muted" style="opacity:0;transition:opacity 0.5s">Saved</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{% with messages = get_flashed_messages() %}
|
{% with messages = get_flashed_messages() %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
@@ -129,22 +116,44 @@
|
|||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => new bootstrap.Tooltip(el));
|
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => new bootstrap.Tooltip(el));
|
||||||
|
|
||||||
const ckptSelect = document.getElementById('defaultCheckpointSelect');
|
|
||||||
const saveStatus = document.getElementById('checkpointSaveStatus');
|
|
||||||
if (ckptSelect) {
|
|
||||||
ckptSelect.addEventListener('change', () => {
|
|
||||||
fetch('/set_default_checkpoint', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
|
||||||
body: 'checkpoint_path=' + encodeURIComponent(ckptSelect.value)
|
|
||||||
}).then(() => {
|
|
||||||
saveStatus.style.opacity = '1';
|
|
||||||
setTimeout(() => { saveStatus.style.opacity = '0'; }, 1500);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- Loaded checkpoint → ComfyUI tooltip ----
|
||||||
|
(function() {
|
||||||
|
let _loadedCheckpoint = null;
|
||||||
|
async function pollLoadedCheckpoint() {
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/comfyui/loaded_checkpoint', { cache: 'no-store' });
|
||||||
|
const data = await r.json();
|
||||||
|
_loadedCheckpoint = data.checkpoint || null;
|
||||||
|
} catch {
|
||||||
|
_loadedCheckpoint = null;
|
||||||
|
}
|
||||||
|
updateComfyTooltip();
|
||||||
|
}
|
||||||
|
function updateComfyTooltip() {
|
||||||
|
const el = document.getElementById('status-comfyui');
|
||||||
|
if (!el) return;
|
||||||
|
const dot = el.querySelector('.status-dot');
|
||||||
|
const online = dot && dot.classList.contains('status-ok');
|
||||||
|
let text = 'ComfyUI: ' + (online ? 'online' : 'offline');
|
||||||
|
if (_loadedCheckpoint) {
|
||||||
|
const parts = _loadedCheckpoint.split(/[/\\]/);
|
||||||
|
const name = parts[parts.length - 1].replace(/\.safetensors$/, '');
|
||||||
|
text += '\n' + name;
|
||||||
|
}
|
||||||
|
el.setAttribute('data-bs-title', text);
|
||||||
|
el.setAttribute('title', text);
|
||||||
|
const tip = bootstrap.Tooltip.getInstance(el);
|
||||||
|
if (tip) tip.setContent({ '.tooltip-inner': text });
|
||||||
|
}
|
||||||
|
// Hook into the existing status polling to refresh tooltip after status changes
|
||||||
|
window._updateComfyTooltip = updateComfyTooltip;
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
pollLoadedCheckpoint();
|
||||||
|
setInterval(pollLoadedCheckpoint, 30000);
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
// ---- Resource delete modal (category galleries) ----
|
// ---- Resource delete modal (category galleries) ----
|
||||||
@@ -340,14 +349,15 @@
|
|||||||
if (!el) return;
|
if (!el) return;
|
||||||
const dot = el.querySelector('.status-dot');
|
const dot = el.querySelector('.status-dot');
|
||||||
dot.className = 'status-dot ' + (ok ? 'status-ok' : 'status-error');
|
dot.className = 'status-dot ' + (ok ? 'status-ok' : 'status-error');
|
||||||
|
if (id === 'status-comfyui' && window._updateComfyTooltip) {
|
||||||
|
window._updateComfyTooltip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const tooltipText = label + ': ' + (ok ? 'online' : 'offline');
|
const tooltipText = label + ': ' + (ok ? 'online' : 'offline');
|
||||||
el.setAttribute('data-bs-title', tooltipText);
|
el.setAttribute('data-bs-title', tooltipText);
|
||||||
el.setAttribute('title', tooltipText);
|
el.setAttribute('title', tooltipText);
|
||||||
// Refresh tooltip instance if already initialised
|
|
||||||
const tip = bootstrap.Tooltip.getInstance(el);
|
const tip = bootstrap.Tooltip.getInstance(el);
|
||||||
if (tip) {
|
if (tip) tip.setContent({ '.tooltip-inner': tooltipText });
|
||||||
tip.setContent({ '.tooltip-inner': tooltipText });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollService(svc) {
|
async function pollService(svc) {
|
||||||
|
|||||||
@@ -43,9 +43,10 @@
|
|||||||
<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; cursor: pointer;" data-bs-toggle="modal" data-bs-target="#imageModal" onclick="showImage(this.querySelector('img').src)">
|
<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') ? this.querySelector('img').src : '')">
|
||||||
{% if look.image_path %}
|
{% if look.image_path %}
|
||||||
<img src="{{ url_for('static', filename='uploads/' + look.image_path) }}" alt="{{ look.name }}" class="img-fluid">
|
<img src="{{ url_for('static', filename='uploads/' + look.image_path) }}" alt="{{ look.name }}" class="img-fluid"
|
||||||
|
data-preview-path="{{ look.image_path }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">No Image Attached</span>
|
<span class="text-muted">No Image Attached</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -89,35 +90,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if preview_image %}
|
<div class="card mb-4 {% if preview_image %}border-success{% else %}border-secondary d-none{% endif %}" id="preview-card">
|
||||||
<div class="card mb-4 border-success">
|
<div class="card-header {% if preview_image %}bg-success{% else %}bg-secondary{% endif %} text-white d-flex justify-content-between align-items-center" id="preview-card-header">
|
||||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
<span>Selected Preview</span>
|
||||||
<span>Latest Preview</span>
|
|
||||||
<form action="{{ url_for('replace_look_cover_from_preview', slug=look.slug) }}" method="post" class="m-0" id="replace-cover-form">
|
<form action="{{ url_for('replace_look_cover_from_preview', slug=look.slug) }}" method="post" class="m-0" id="replace-cover-form">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn">Replace Cover</button>
|
<input type="hidden" name="preview_path" id="preview-path" value="{{ preview_image or '' }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn" {% if not preview_image %}disabled{% endif %}>Replace Cover</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<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)">
|
<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') ? 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) if preview_image else '' }}" alt="Preview" class="img-fluid">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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_look_cover_from_preview', slug=look.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 mb-4">
|
||||||
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
||||||
@@ -156,7 +142,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#jsonEditorModal">Edit JSON</button>
|
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#jsonEditorModal">Edit JSON</button>
|
||||||
<a href="{{ url_for('looks_index') }}" class="btn btn-outline-secondary">Back to Gallery</a>
|
<a href="{{ url_for('looks_index') }}" class="btn btn-outline-secondary">Back to Library</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -237,9 +223,25 @@
|
|||||||
const progressContainer = document.getElementById('progress-container');
|
const progressContainer = document.getElementById('progress-container');
|
||||||
const progressLabel = document.getElementById('progress-label');
|
const progressLabel = document.getElementById('progress-label');
|
||||||
const previewCard = document.getElementById('preview-card');
|
const previewCard = document.getElementById('preview-card');
|
||||||
|
const previewCardHeader = document.getElementById('preview-card-header');
|
||||||
const previewImg = document.getElementById('preview-img');
|
const previewImg = document.getElementById('preview-img');
|
||||||
|
const previewPath = document.getElementById('preview-path');
|
||||||
|
const replaceBtn = document.getElementById('replace-cover-btn');
|
||||||
|
|
||||||
let currentJobId = null;
|
function selectPreview(relativePath, imageUrl) {
|
||||||
|
if (!relativePath) return;
|
||||||
|
previewImg.src = imageUrl;
|
||||||
|
previewPath.value = relativePath;
|
||||||
|
replaceBtn.disabled = false;
|
||||||
|
previewCard.classList.remove('d-none');
|
||||||
|
previewCardHeader.classList.replace('bg-secondary', 'bg-success');
|
||||||
|
previewCard.classList.replace('border-secondary', 'border-success');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
const img = e.target.closest('img[data-preview-path]');
|
||||||
|
if (img) selectPreview(img.dataset.previewPath, img.src);
|
||||||
|
});
|
||||||
|
|
||||||
async function waitForJob(jobId) {
|
async function waitForJob(jobId) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -272,22 +274,15 @@
|
|||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.error) { alert('Error: ' + data.error); progressContainer.classList.add('d-none'); return; }
|
if (data.error) { alert('Error: ' + data.error); progressContainer.classList.add('d-none'); return; }
|
||||||
currentJobId = data.job_id;
|
const jobResult = await waitForJob(data.job_id);
|
||||||
const jobResult = await waitForJob(currentJobId);
|
if (jobResult.result?.image_url) selectPreview(jobResult.result.relative_path, jobResult.result.image_url);
|
||||||
currentJobId = null;
|
|
||||||
if (jobResult.result && jobResult.result.image_url) {
|
|
||||||
previewImg.src = jobResult.result.image_url;
|
|
||||||
if (previewCard) previewCard.classList.remove('d-none');
|
|
||||||
const replaceBtn = document.getElementById('replace-cover-btn');
|
|
||||||
if (replaceBtn) replaceBtn.disabled = false;
|
|
||||||
}
|
|
||||||
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
|
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
|
||||||
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
|
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function showImage(src) {
|
function showImage(src) {
|
||||||
document.getElementById('modalImage').src = src;
|
if (src) document.getElementById('modalImage').src = src;
|
||||||
}
|
}
|
||||||
|
|
||||||
initJsonEditor('{{ url_for("save_look_json", slug=look.slug) }}');
|
initJsonEditor('{{ url_for("save_look_json", slug=look.slug) }}');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2>Looks Gallery</h2>
|
<h2>Looks Library</h2>
|
||||||
<div class="d-flex gap-1 align-items-center">
|
<div class="d-flex gap-1 align-items-center">
|
||||||
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for looks without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for looks without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all looks"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all looks"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<div class="card mb-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)">
|
<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 %}
|
{% if outfit.image_path %}
|
||||||
<img src="{{ url_for('static', filename='uploads/' + outfit.image_path) }}" alt="{{ outfit.name }}" class="img-fluid">
|
<img src="{{ url_for('static', filename='uploads/' + outfit.image_path) }}" alt="{{ outfit.name }}" class="img-fluid" data-preview-path="{{ outfit.image_path }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">No Image Attached</span>
|
<span class="text-muted">No Image Attached</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -86,35 +86,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if preview_image %}
|
<div class="card mb-4 {% if preview_image %}border-success{% else %}border-secondary d-none{% endif %}" id="preview-card">
|
||||||
<div class="card mb-4 border-success">
|
<div class="card-header {% if preview_image %}bg-success{% else %}bg-secondary{% endif %} text-white d-flex justify-content-between align-items-center" id="preview-card-header">
|
||||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
<span>Selected Preview</span>
|
||||||
<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">
|
<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">Replace Cover</button>
|
<input type="hidden" name="preview_path" id="preview-path" value="{{ preview_image or '' }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn" {% if not preview_image %}disabled{% endif %}>Replace Cover</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<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)">
|
<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) if preview_image else '' }}" alt="Preview" class="img-fluid">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 mb-4">
|
||||||
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
||||||
@@ -152,7 +137,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#jsonEditorModal">Edit JSON</button>
|
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#jsonEditorModal">Edit JSON</button>
|
||||||
<a href="{{ url_for('outfits_index') }}" class="btn btn-outline-secondary">Back to Gallery</a>
|
<a href="{{ url_for('outfits_index') }}" class="btn btn-outline-secondary">Back to Library</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -245,7 +230,8 @@
|
|||||||
class="img-fluid rounded"
|
class="img-fluid rounded"
|
||||||
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
||||||
onclick="showImage(this.src)"
|
onclick="showImage(this.src)"
|
||||||
data-bs-toggle="modal" data-bs-target="#imageModal">
|
data-bs-toggle="modal" data-bs-target="#imageModal"
|
||||||
|
data-preview-path="{{ img }}">
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="col-12 text-muted small" id="gallery-empty">No previews yet. Generate some!</div>
|
<div class="col-12 text-muted small" id="gallery-empty">No previews yet. Generate some!</div>
|
||||||
@@ -271,9 +257,25 @@
|
|||||||
const progressLabel = document.getElementById('progress-label');
|
const progressLabel = document.getElementById('progress-label');
|
||||||
const previewCard = document.getElementById('preview-card');
|
const previewCard = document.getElementById('preview-card');
|
||||||
const previewImg = document.getElementById('preview-img');
|
const previewImg = document.getElementById('preview-img');
|
||||||
|
const previewPath = document.getElementById('preview-path');
|
||||||
let currentJobId = null;
|
const replaceBtn = document.getElementById('replace-cover-btn');
|
||||||
let currentAction = null;
|
const previewHeader = document.getElementById('preview-card-header');
|
||||||
|
|
||||||
|
function selectPreview(relativePath, imageUrl) {
|
||||||
|
if (!relativePath) return;
|
||||||
|
previewImg.src = imageUrl;
|
||||||
|
previewPath.value = relativePath;
|
||||||
|
replaceBtn.disabled = false;
|
||||||
|
previewCard.classList.remove('d-none');
|
||||||
|
previewHeader.classList.replace('bg-secondary', 'bg-success');
|
||||||
|
previewCard.classList.replace('border-secondary', 'border-success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clicking any image with data-preview-path selects it into the preview pane
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
const img = e.target.closest('img[data-preview-path]');
|
||||||
|
if (img) selectPreview(img.dataset.previewPath, img.src);
|
||||||
|
});
|
||||||
|
|
||||||
async function waitForJob(jobId) {
|
async function waitForJob(jobId) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -298,53 +300,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.addEventListener('submit', async (e) => {
|
form.addEventListener('submit', async (e) => {
|
||||||
// Only intercept generate actions
|
|
||||||
const submitter = e.submitter;
|
const submitter = e.submitter;
|
||||||
if (!submitter || submitter.value !== 'preview') {
|
if (!submitter || submitter.value !== 'preview') return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
currentAction = submitter.value;
|
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
formData.append('action', currentAction);
|
formData.append('action', 'preview');
|
||||||
|
|
||||||
// UI Reset
|
|
||||||
progressContainer.classList.remove('d-none');
|
progressContainer.classList.remove('d-none');
|
||||||
progressBar.style.width = '100%';
|
progressBar.style.width = '100%';
|
||||||
progressBar.textContent = '';
|
progressBar.textContent = '';
|
||||||
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
||||||
progressLabel.textContent = 'Queuing…';
|
progressLabel.textContent = 'Queuing…';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(form.getAttribute('action'), {
|
const response = await fetch(form.getAttribute('action'), {
|
||||||
method: 'POST',
|
method: 'POST', body: formData,
|
||||||
body: formData,
|
|
||||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
if (data.error) { alert('Error: ' + data.error); return; }
|
||||||
if (data.error) {
|
|
||||||
alert('Error: ' + data.error);
|
|
||||||
progressContainer.classList.add('d-none');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentJobId = data.job_id;
|
|
||||||
progressLabel.textContent = 'Queued…';
|
progressLabel.textContent = 'Queued…';
|
||||||
|
const jobResult = await waitForJob(data.job_id);
|
||||||
|
|
||||||
const jobResult = await waitForJob(currentJobId);
|
if (jobResult.result?.image_url) {
|
||||||
currentJobId = null;
|
selectPreview(jobResult.result.relative_path, jobResult.result.image_url);
|
||||||
|
addToPreviewGallery(jobResult.result.image_url, jobResult.result.relative_path, '');
|
||||||
if (jobResult.result && jobResult.result.image_url) {
|
|
||||||
previewImg.src = jobResult.result.image_url;
|
|
||||||
if (previewCard) previewCard.classList.remove('d-none');
|
|
||||||
const replaceBtn = document.getElementById('replace-cover-btn');
|
|
||||||
if (replaceBtn) replaceBtn.disabled = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
alert('Generation failed: ' + err.message);
|
alert('Generation failed: ' + err.message);
|
||||||
@@ -366,7 +349,7 @@
|
|||||||
const batchLabel = document.getElementById('batch-label');
|
const batchLabel = document.getElementById('batch-label');
|
||||||
const batchBar = document.getElementById('batch-bar');
|
const batchBar = document.getElementById('batch-bar');
|
||||||
|
|
||||||
function addToPreviewGallery(imageUrl, charName) {
|
function addToPreviewGallery(imageUrl, relativePath, charName) {
|
||||||
const gallery = document.getElementById('preview-gallery');
|
const gallery = document.getElementById('preview-gallery');
|
||||||
const placeholder = document.getElementById('gallery-empty');
|
const placeholder = document.getElementById('gallery-empty');
|
||||||
if (placeholder) placeholder.remove();
|
if (placeholder) placeholder.remove();
|
||||||
@@ -377,8 +360,9 @@
|
|||||||
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
||||||
onclick="showImage(this.src)"
|
onclick="showImage(this.src)"
|
||||||
data-bs-toggle="modal" data-bs-target="#imageModal"
|
data-bs-toggle="modal" data-bs-target="#imageModal"
|
||||||
|
data-preview-path="${relativePath}"
|
||||||
title="${charName}">
|
title="${charName}">
|
||||||
<div class="position-absolute bottom-0 start-0 w-100 bg-dark bg-opacity-50 text-white p-1 rounded-bottom" style="font-size: 0.7rem; line-height: 1.2;">${charName}</div>
|
${charName ? `<div class="position-absolute bottom-0 start-0 w-100 bg-dark bg-opacity-50 text-white p-1 rounded-bottom" style="font-size: 0.7rem; line-height: 1.2;">${charName}</div>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
gallery.insertBefore(col, gallery.firstChild);
|
gallery.insertBefore(col, gallery.firstChild);
|
||||||
const badge = document.querySelector('#previews-tab .badge');
|
const badge = document.querySelector('#previews-tab .badge');
|
||||||
@@ -393,43 +377,46 @@
|
|||||||
stopAllBtn.classList.remove('d-none');
|
stopAllBtn.classList.remove('d-none');
|
||||||
batchProgress.classList.remove('d-none');
|
batchProgress.classList.remove('d-none');
|
||||||
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
|
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
|
||||||
for (let i = 0; i < allCharacters.length; i++) {
|
|
||||||
|
const genForm = document.getElementById('generate-form');
|
||||||
|
const formAction = genForm.getAttribute('action');
|
||||||
|
|
||||||
|
// Phase 1: submit all jobs immediately
|
||||||
|
batchLabel.textContent = 'Queuing all characters…';
|
||||||
|
const pending = [];
|
||||||
|
for (const char of allCharacters) {
|
||||||
if (stopBatch) break;
|
if (stopBatch) break;
|
||||||
const char = allCharacters[i];
|
|
||||||
batchBar.style.width = `${Math.round((i / allCharacters.length) * 100)}%`;
|
|
||||||
batchLabel.textContent = `Generating ${char.name} (${i + 1} / ${allCharacters.length})`;
|
|
||||||
const genForm = document.getElementById('generate-form');
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
genForm.querySelectorAll('input[name="include_field"]:checked').forEach(cb => fd.append('include_field', cb.value));
|
genForm.querySelectorAll('input[name="include_field"]:checked').forEach(cb => fd.append('include_field', cb.value));
|
||||||
fd.append('character_slug', char.slug);
|
fd.append('character_slug', char.slug);
|
||||||
fd.append('action', 'preview');
|
fd.append('action', 'preview');
|
||||||
try {
|
try {
|
||||||
progressContainer.classList.remove('d-none');
|
const resp = await fetch(formAction, { method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||||
progressBar.style.width = '100%';
|
|
||||||
progressBar.textContent = '';
|
|
||||||
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
|
||||||
progressLabel.textContent = `${char.name}: Queuing…`;
|
|
||||||
const resp = await fetch(genForm.getAttribute('action'), {
|
|
||||||
method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
||||||
});
|
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.error) { console.error(`Error for ${char.name}:`, data.error); progressContainer.classList.add('d-none'); continue; }
|
if (!data.error) pending.push({ char, jobId: data.job_id });
|
||||||
currentJobId = data.job_id;
|
} catch (err) { console.error(`Submit error for ${char.name}:`, err); }
|
||||||
const jobResult = await waitForJob(currentJobId);
|
|
||||||
currentJobId = null;
|
|
||||||
if (jobResult.result && jobResult.result.image_url) {
|
|
||||||
addToPreviewGallery(jobResult.result.image_url, char.name);
|
|
||||||
previewImg.src = jobResult.result.image_url;
|
|
||||||
if (previewCard) previewCard.classList.remove('d-none');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Failed for ${char.name}:`, err);
|
|
||||||
currentJobId = null;
|
|
||||||
} finally {
|
|
||||||
progressContainer.classList.add('d-none');
|
|
||||||
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2: poll all in parallel
|
||||||
|
batchBar.style.width = '0%';
|
||||||
|
let done = 0;
|
||||||
|
const total = pending.length;
|
||||||
|
batchLabel.textContent = `0 / ${total} complete`;
|
||||||
|
|
||||||
|
await Promise.all(pending.map(({ char, jobId }) =>
|
||||||
|
waitForJob(jobId).then(result => {
|
||||||
|
done++;
|
||||||
|
batchBar.style.width = `${Math.round((done / total) * 100)}%`;
|
||||||
|
batchLabel.textContent = `${done} / ${total} complete`;
|
||||||
|
if (result.result?.image_url) {
|
||||||
|
addToPreviewGallery(result.result.image_url, result.result.relative_path, char.name);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
done++;
|
||||||
|
console.error(`Failed for ${char.name}:`, err);
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
batchBar.style.width = '100%';
|
batchBar.style.width = '100%';
|
||||||
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
|
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
|
||||||
generateAllBtn.disabled = false;
|
generateAllBtn.disabled = false;
|
||||||
@@ -440,7 +427,7 @@
|
|||||||
stopAllBtn.addEventListener('click', () => {
|
stopAllBtn.addEventListener('click', () => {
|
||||||
stopBatch = true;
|
stopBatch = true;
|
||||||
stopAllBtn.classList.add('d-none');
|
stopAllBtn.classList.add('d-none');
|
||||||
batchLabel.textContent = 'Stopping after current generation...';
|
batchLabel.textContent = 'Stopping…';
|
||||||
});
|
});
|
||||||
|
|
||||||
initJsonEditor('{{ url_for("save_outfit_json", slug=outfit.slug) }}');
|
initJsonEditor('{{ url_for("save_outfit_json", slug=outfit.slug) }}');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2>Outfit Gallery</h2>
|
<h2>Outfit Library</h2>
|
||||||
<div class="d-flex gap-1 align-items-center">
|
<div class="d-flex gap-1 align-items-center">
|
||||||
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for outfits without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for outfits without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all outfits"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all outfits"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
<div class="card mb-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)">
|
<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 scene.image_path %}
|
{% if scene.image_path %}
|
||||||
<img src="{{ url_for('static', filename='uploads/' + scene.image_path) }}" alt="{{ scene.name }}" class="img-fluid">
|
<img src="{{ url_for('static', filename='uploads/' + scene.image_path) }}" alt="{{ scene.name }}" class="img-fluid" data-preview-path="{{ scene.image_path }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="d-flex align-items-center justify-content-center bg-light" style="height: 400px;">
|
<div class="d-flex align-items-center justify-content-center bg-light" style="height: 400px;">
|
||||||
<span class="text-muted">No Image Attached</span>
|
<span class="text-muted">No Image Attached</span>
|
||||||
@@ -100,35 +100,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if preview_image %}
|
<div class="card mb-4 {% if preview_image %}border-success{% else %}border-secondary d-none{% endif %}" id="preview-card">
|
||||||
<div class="card mb-4 border-success">
|
<div class="card-header {% if preview_image %}bg-success{% else %}bg-secondary{% endif %} text-white d-flex justify-content-between align-items-center p-2" id="preview-card-header">
|
||||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center p-2">
|
<small>Selected Preview</small>
|
||||||
<small>Latest Preview</small>
|
|
||||||
<form action="{{ url_for('replace_scene_cover_from_preview', slug=scene.slug) }}" method="post" class="m-0" id="replace-cover-form">
|
<form action="{{ url_for('replace_scene_cover_from_preview', slug=scene.slug) }}" method="post" class="m-0" id="replace-cover-form">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn">Replace Cover</button>
|
<input type="hidden" name="preview_path" id="preview-path" value="{{ preview_image or '' }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn" {% if not preview_image %}disabled{% endif %}>Replace Cover</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<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)">
|
<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) if preview_image else '' }}" alt="Preview" class="img-fluid">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 p-2">
|
|
||||||
<small>Latest Preview</small>
|
|
||||||
<form action="{{ url_for('replace_scene_cover_from_preview', slug=scene.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>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
@@ -145,7 +130,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#jsonEditorModal">Edit JSON</button>
|
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#jsonEditorModal">Edit JSON</button>
|
||||||
<a href="{{ url_for('scenes_index') }}" class="btn btn-outline-secondary">Back to Gallery</a>
|
<a href="{{ url_for('scenes_index') }}" class="btn btn-outline-secondary">Back to Library</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -255,7 +240,8 @@
|
|||||||
class="img-fluid rounded"
|
class="img-fluid rounded"
|
||||||
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
||||||
onclick="showImage(this.src)"
|
onclick="showImage(this.src)"
|
||||||
data-bs-toggle="modal" data-bs-target="#imageModal">
|
data-bs-toggle="modal" data-bs-target="#imageModal"
|
||||||
|
data-preview-path="{{ img }}">
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="col-12 text-muted small" id="gallery-empty">No previews yet. Generate some!</div>
|
<div class="col-12 text-muted small" id="gallery-empty">No previews yet. Generate some!</div>
|
||||||
@@ -281,18 +267,30 @@
|
|||||||
const progressLabel = document.getElementById('progress-label');
|
const progressLabel = document.getElementById('progress-label');
|
||||||
const previewCard = document.getElementById('preview-card');
|
const previewCard = document.getElementById('preview-card');
|
||||||
const previewImg = document.getElementById('preview-img');
|
const previewImg = document.getElementById('preview-img');
|
||||||
|
const previewPath = document.getElementById('preview-path');
|
||||||
|
const replaceBtn = document.getElementById('replace-cover-btn');
|
||||||
|
const previewHeader = document.getElementById('preview-card-header');
|
||||||
const charSelect = document.getElementById('character_select');
|
const charSelect = document.getElementById('character_select');
|
||||||
const charContext = document.getElementById('character-context');
|
const charContext = document.getElementById('character-context');
|
||||||
|
|
||||||
charSelect.addEventListener('change', () => {
|
charSelect.addEventListener('change', () => {
|
||||||
if (charSelect.value && charSelect.value !== '__random__') {
|
charContext.classList.toggle('d-none', !charSelect.value || charSelect.value === '__random__');
|
||||||
charContext.classList.remove('d-none');
|
|
||||||
} else {
|
|
||||||
charContext.classList.add('d-none');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let currentJobId = null;
|
function selectPreview(relativePath, imageUrl) {
|
||||||
|
if (!relativePath) return;
|
||||||
|
previewImg.src = imageUrl;
|
||||||
|
previewPath.value = relativePath;
|
||||||
|
replaceBtn.disabled = false;
|
||||||
|
previewCard.classList.remove('d-none');
|
||||||
|
previewHeader.classList.replace('bg-secondary', 'bg-success');
|
||||||
|
previewCard.classList.replace('border-secondary', 'border-success');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
const img = e.target.closest('img[data-preview-path]');
|
||||||
|
if (img) selectPreview(img.dataset.previewPath, img.src);
|
||||||
|
});
|
||||||
|
|
||||||
async function waitForJob(jobId) {
|
async function waitForJob(jobId) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -302,8 +300,7 @@
|
|||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.status === 'done') { clearInterval(poll); resolve(data); }
|
if (data.status === 'done') { clearInterval(poll); resolve(data); }
|
||||||
else if (data.status === 'failed' || data.status === 'removed') { clearInterval(poll); reject(new Error(data.error || 'Job failed')); }
|
else if (data.status === 'failed' || data.status === 'removed') { clearInterval(poll); reject(new Error(data.error || 'Job failed')); }
|
||||||
else if (data.status === 'processing') progressLabel.textContent = 'Generating…';
|
else progressLabel.textContent = data.status === 'processing' ? 'Generating…' : 'Queued…';
|
||||||
else progressLabel.textContent = 'Queued…';
|
|
||||||
} catch (err) { console.error('Poll error:', err); }
|
} catch (err) { console.error('Poll error:', err); }
|
||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
@@ -324,26 +321,21 @@
|
|||||||
method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.error) { alert('Error: ' + data.error); progressContainer.classList.add('d-none'); return; }
|
if (data.error) { alert('Error: ' + data.error); return; }
|
||||||
currentJobId = data.job_id;
|
progressLabel.textContent = 'Queued…';
|
||||||
const jobResult = await waitForJob(currentJobId);
|
const jobResult = await waitForJob(data.job_id);
|
||||||
currentJobId = null;
|
if (jobResult.result?.image_url) {
|
||||||
if (jobResult.result && jobResult.result.image_url) {
|
selectPreview(jobResult.result.relative_path, jobResult.result.image_url);
|
||||||
previewImg.src = jobResult.result.image_url;
|
addToPreviewGallery(jobResult.result.image_url, jobResult.result.relative_path, '');
|
||||||
if (previewCard) previewCard.classList.remove('d-none');
|
|
||||||
const replaceBtn = document.getElementById('replace-cover-btn');
|
|
||||||
if (replaceBtn) replaceBtn.disabled = false;
|
|
||||||
}
|
}
|
||||||
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
|
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
|
||||||
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
|
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Batch: Generate All Characters
|
|
||||||
const allCharacters = [
|
const allCharacters = [
|
||||||
{% for char in characters %}{ slug: "{{ char.slug }}", name: {{ char.name | tojson }} },
|
{% for char in characters %}{ slug: "{{ char.slug }}", name: {{ char.name | tojson }} },
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
];
|
];
|
||||||
|
|
||||||
let stopBatch = false;
|
let stopBatch = false;
|
||||||
const generateAllBtn = document.getElementById('generate-all-btn');
|
const generateAllBtn = document.getElementById('generate-all-btn');
|
||||||
const stopAllBtn = document.getElementById('stop-all-btn');
|
const stopAllBtn = document.getElementById('stop-all-btn');
|
||||||
@@ -351,7 +343,7 @@
|
|||||||
const batchLabel = document.getElementById('batch-label');
|
const batchLabel = document.getElementById('batch-label');
|
||||||
const batchBar = document.getElementById('batch-bar');
|
const batchBar = document.getElementById('batch-bar');
|
||||||
|
|
||||||
function addToPreviewGallery(imageUrl, charName) {
|
function addToPreviewGallery(imageUrl, relativePath, charName) {
|
||||||
const gallery = document.getElementById('preview-gallery');
|
const gallery = document.getElementById('preview-gallery');
|
||||||
const placeholder = document.getElementById('gallery-empty');
|
const placeholder = document.getElementById('gallery-empty');
|
||||||
if (placeholder) placeholder.remove();
|
if (placeholder) placeholder.remove();
|
||||||
@@ -362,8 +354,9 @@
|
|||||||
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
||||||
onclick="showImage(this.src)"
|
onclick="showImage(this.src)"
|
||||||
data-bs-toggle="modal" data-bs-target="#imageModal"
|
data-bs-toggle="modal" data-bs-target="#imageModal"
|
||||||
|
data-preview-path="${relativePath}"
|
||||||
title="${charName}">
|
title="${charName}">
|
||||||
<div class="position-absolute bottom-0 start-0 w-100 bg-dark bg-opacity-50 text-white p-1 rounded-bottom" style="font-size: 0.7rem; line-height: 1.2;">${charName}</div>
|
${charName ? `<div class="position-absolute bottom-0 start-0 w-100 bg-dark bg-opacity-50 text-white p-1 rounded-bottom" style="font-size: 0.7rem; line-height: 1.2;">${charName}</div>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
gallery.insertBefore(col, gallery.firstChild);
|
gallery.insertBefore(col, gallery.firstChild);
|
||||||
const badge = document.querySelector('#previews-tab .badge');
|
const badge = document.querySelector('#previews-tab .badge');
|
||||||
@@ -378,37 +371,37 @@
|
|||||||
stopAllBtn.classList.remove('d-none');
|
stopAllBtn.classList.remove('d-none');
|
||||||
batchProgress.classList.remove('d-none');
|
batchProgress.classList.remove('d-none');
|
||||||
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
|
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
|
||||||
for (let i = 0; i < allCharacters.length; i++) {
|
|
||||||
|
const genForm = document.getElementById('generate-form');
|
||||||
|
const formAction = genForm.getAttribute('action');
|
||||||
|
batchLabel.textContent = 'Queuing all characters…';
|
||||||
|
const pending = [];
|
||||||
|
for (const char of allCharacters) {
|
||||||
if (stopBatch) break;
|
if (stopBatch) break;
|
||||||
const char = allCharacters[i];
|
|
||||||
batchBar.style.width = `${Math.round((i / allCharacters.length) * 100)}%`;
|
|
||||||
batchLabel.textContent = `Generating ${char.name} (${i + 1} / ${allCharacters.length})`;
|
|
||||||
const genForm = document.getElementById('generate-form');
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
genForm.querySelectorAll('input[name="include_field"]:checked').forEach(cb => fd.append('include_field', cb.value));
|
genForm.querySelectorAll('input[name="include_field"]:checked').forEach(cb => fd.append('include_field', cb.value));
|
||||||
fd.append('character_slug', char.slug);
|
fd.append('character_slug', char.slug);
|
||||||
fd.append('action', 'preview');
|
fd.append('action', 'preview');
|
||||||
try {
|
try {
|
||||||
progressContainer.classList.remove('d-none');
|
const resp = await fetch(formAction, { method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||||
progressBar.style.width = '100%'; progressBar.textContent = '';
|
|
||||||
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
|
||||||
progressLabel.textContent = `${char.name}: Queuing…`;
|
|
||||||
const resp = await fetch(genForm.getAttribute('action'), {
|
|
||||||
method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
||||||
});
|
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.error) { console.error(`Error for ${char.name}:`, data.error); progressContainer.classList.add('d-none'); continue; }
|
if (!data.error) pending.push({ char, jobId: data.job_id });
|
||||||
currentJobId = data.job_id;
|
} catch (err) { console.error(`Submit error for ${char.name}:`, err); }
|
||||||
const jobResult = await waitForJob(currentJobId);
|
|
||||||
currentJobId = null;
|
|
||||||
if (jobResult.result && jobResult.result.image_url) {
|
|
||||||
addToPreviewGallery(jobResult.result.image_url, char.name);
|
|
||||||
previewImg.src = jobResult.result.image_url;
|
|
||||||
if (previewCard) previewCard.classList.remove('d-none');
|
|
||||||
}
|
|
||||||
} catch (err) { console.error(`Failed for ${char.name}:`, err); currentJobId = null; }
|
|
||||||
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
batchBar.style.width = '0%';
|
||||||
|
let done = 0;
|
||||||
|
const total = pending.length;
|
||||||
|
batchLabel.textContent = `0 / ${total} complete`;
|
||||||
|
await Promise.all(pending.map(({ char, jobId }) =>
|
||||||
|
waitForJob(jobId).then(result => {
|
||||||
|
done++;
|
||||||
|
batchBar.style.width = `${Math.round((done / total) * 100)}%`;
|
||||||
|
batchLabel.textContent = `${done} / ${total} complete`;
|
||||||
|
if (result.result?.image_url) addToPreviewGallery(result.result.image_url, result.result.relative_path, char.name);
|
||||||
|
}).catch(err => { done++; console.error(`Failed for ${char.name}:`, err); })
|
||||||
|
));
|
||||||
|
|
||||||
batchBar.style.width = '100%';
|
batchBar.style.width = '100%';
|
||||||
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
|
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
|
||||||
generateAllBtn.disabled = false;
|
generateAllBtn.disabled = false;
|
||||||
@@ -419,10 +412,9 @@
|
|||||||
stopAllBtn.addEventListener('click', () => {
|
stopAllBtn.addEventListener('click', () => {
|
||||||
stopBatch = true;
|
stopBatch = true;
|
||||||
stopAllBtn.classList.add('d-none');
|
stopAllBtn.classList.add('d-none');
|
||||||
batchLabel.textContent = 'Stopping after current generation...';
|
batchLabel.textContent = 'Stopping…';
|
||||||
});
|
});
|
||||||
|
|
||||||
// JSON Editor
|
|
||||||
initJsonEditor('{{ url_for("save_scene_json", slug=scene.slug) }}');
|
initJsonEditor('{{ url_for("save_scene_json", slug=scene.slug) }}');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2>Scene Gallery</h2>
|
<h2>Scene Library</h2>
|
||||||
<div class="d-flex gap-1 align-items-center">
|
<div class="d-flex gap-1 align-items-center">
|
||||||
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for scenes without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for scenes without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all scenes"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all scenes"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
|
|||||||
@@ -73,6 +73,70 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Directory Settings -->
|
||||||
|
<h5 class="mb-3 text-primary">LoRA Directories</h5>
|
||||||
|
<p class="text-muted small">Absolute paths on disk where LoRA files are scanned for each category.</p>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="lora_dir_characters" class="form-label">Characters / Looks</label>
|
||||||
|
<input type="text" class="form-control" id="lora_dir_characters" name="lora_dir_characters"
|
||||||
|
value="{{ settings.lora_dir_characters or '/ImageModels/lora/Illustrious/Looks' }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="lora_dir_outfits" class="form-label">Outfits</label>
|
||||||
|
<input type="text" class="form-control" id="lora_dir_outfits" name="lora_dir_outfits"
|
||||||
|
value="{{ settings.lora_dir_outfits or '/ImageModels/lora/Illustrious/Clothing' }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="lora_dir_actions" class="form-label">Actions</label>
|
||||||
|
<input type="text" class="form-control" id="lora_dir_actions" name="lora_dir_actions"
|
||||||
|
value="{{ settings.lora_dir_actions or '/ImageModels/lora/Illustrious/Poses' }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="lora_dir_styles" class="form-label">Styles</label>
|
||||||
|
<input type="text" class="form-control" id="lora_dir_styles" name="lora_dir_styles"
|
||||||
|
value="{{ settings.lora_dir_styles or '/ImageModels/lora/Illustrious/Styles' }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="lora_dir_scenes" class="form-label">Scenes</label>
|
||||||
|
<input type="text" class="form-control" id="lora_dir_scenes" name="lora_dir_scenes"
|
||||||
|
value="{{ settings.lora_dir_scenes or '/ImageModels/lora/Illustrious/Backgrounds' }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="lora_dir_detailers" class="form-label">Detailers</label>
|
||||||
|
<input type="text" class="form-control" id="lora_dir_detailers" name="lora_dir_detailers"
|
||||||
|
value="{{ settings.lora_dir_detailers or '/ImageModels/lora/Illustrious/Detailers' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h5 class="mb-3 text-primary">Checkpoint Directories</h5>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="checkpoint_dirs" class="form-label">Checkpoint Scan Paths</label>
|
||||||
|
<input type="text" class="form-control" id="checkpoint_dirs" name="checkpoint_dirs"
|
||||||
|
value="{{ settings.checkpoint_dirs or '/ImageModels/Stable-diffusion/Illustrious,/ImageModels/Stable-diffusion/Noob' }}">
|
||||||
|
<div class="form-text">Comma-separated list of directories to scan for checkpoint files.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h5 class="mb-3 text-primary">Default Checkpoint</h5>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="default_checkpoint" class="form-label">Active Checkpoint</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<select class="form-select" id="default_checkpoint">
|
||||||
|
<option value="">— workflow default —</option>
|
||||||
|
{% for ckpt in all_checkpoints %}
|
||||||
|
<option value="{{ ckpt.checkpoint_path }}"{% if ckpt.checkpoint_path == default_checkpoint_path %} selected{% endif %}>{{ ckpt.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<span id="ckpt-save-status" class="input-group-text text-success" style="opacity:0;transition:opacity 0.5s">Saved</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Sets the checkpoint used for all generation requests. Saved immediately on change.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="d-grid mt-4">
|
<div class="d-grid mt-4">
|
||||||
<button type="submit" class="btn btn-primary btn-lg">Save All Settings</button>
|
<button type="submit" class="btn btn-primary btn-lg">Save All Settings</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,6 +216,22 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Default Checkpoint
|
||||||
|
const defaultCkptSelect = document.getElementById('default_checkpoint');
|
||||||
|
const ckptSaveStatus = document.getElementById('ckpt-save-status');
|
||||||
|
if (defaultCkptSelect) {
|
||||||
|
defaultCkptSelect.addEventListener('change', () => {
|
||||||
|
fetch('/set_default_checkpoint', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: 'checkpoint_path=' + encodeURIComponent(defaultCkptSelect.value)
|
||||||
|
}).then(() => {
|
||||||
|
ckptSaveStatus.style.opacity = '1';
|
||||||
|
setTimeout(() => { ckptSaveStatus.style.opacity = '0'; }, 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Local Model Loading
|
// Local Model Loading
|
||||||
const connectLocalBtn = document.getElementById('connect-local-btn');
|
const connectLocalBtn = document.getElementById('connect-local-btn');
|
||||||
const localModelSelect = document.getElementById('local_model');
|
const localModelSelect = document.getElementById('local_model');
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
<div class="card mb-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)">
|
<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 style.image_path %}
|
{% if style.image_path %}
|
||||||
<img src="{{ url_for('static', filename='uploads/' + style.image_path) }}" alt="{{ style.name }}" class="img-fluid">
|
<img src="{{ url_for('static', filename='uploads/' + style.image_path) }}" alt="{{ style.name }}" class="img-fluid" data-preview-path="{{ style.image_path }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="d-flex align-items-center justify-content-center bg-light" style="height: 400px;">
|
<div class="d-flex align-items-center justify-content-center bg-light" style="height: 400px;">
|
||||||
<span class="text-muted">No Image Attached</span>
|
<span class="text-muted">No Image Attached</span>
|
||||||
@@ -100,35 +100,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if preview_image %}
|
<div class="card mb-4 {% if preview_image %}border-success{% else %}border-secondary d-none{% endif %}" id="preview-card">
|
||||||
<div class="card mb-4 border-success">
|
<div class="card-header {% if preview_image %}bg-success{% else %}bg-secondary{% endif %} text-white d-flex justify-content-between align-items-center p-2" id="preview-card-header">
|
||||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center p-2">
|
<small>Selected Preview</small>
|
||||||
<small>Latest Preview</small>
|
|
||||||
<form action="{{ url_for('replace_style_cover_from_preview', slug=style.slug) }}" method="post" class="m-0" id="replace-cover-form">
|
<form action="{{ url_for('replace_style_cover_from_preview', slug=style.slug) }}" method="post" class="m-0" id="replace-cover-form">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn">Replace Cover</button>
|
<input type="hidden" name="preview_path" id="preview-path" value="{{ preview_image or '' }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn" {% if not preview_image %}disabled{% endif %}>Replace Cover</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<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)">
|
<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) if preview_image else '' }}" alt="Preview" class="img-fluid">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 p-2">
|
|
||||||
<small>Latest Preview</small>
|
|
||||||
<form action="{{ url_for('replace_style_cover_from_preview', slug=style.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>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
@@ -145,7 +130,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#jsonEditorModal">Edit JSON</button>
|
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#jsonEditorModal">Edit JSON</button>
|
||||||
<a href="{{ url_for('styles_index') }}" class="btn btn-outline-secondary">Back to Gallery</a>
|
<a href="{{ url_for('styles_index') }}" class="btn btn-outline-secondary">Back to Library</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -247,7 +232,8 @@
|
|||||||
class="img-fluid rounded"
|
class="img-fluid rounded"
|
||||||
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
||||||
onclick="showImage(this.src)"
|
onclick="showImage(this.src)"
|
||||||
data-bs-toggle="modal" data-bs-target="#imageModal">
|
data-bs-toggle="modal" data-bs-target="#imageModal"
|
||||||
|
data-preview-path="{{ img }}">
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="col-12 text-muted small" id="gallery-empty">No previews yet. Generate some!</div>
|
<div class="col-12 text-muted small" id="gallery-empty">No previews yet. Generate some!</div>
|
||||||
@@ -273,20 +259,30 @@
|
|||||||
const progressLabel = document.getElementById('progress-label');
|
const progressLabel = document.getElementById('progress-label');
|
||||||
const previewCard = document.getElementById('preview-card');
|
const previewCard = document.getElementById('preview-card');
|
||||||
const previewImg = document.getElementById('preview-img');
|
const previewImg = document.getElementById('preview-img');
|
||||||
|
const previewPath = document.getElementById('preview-path');
|
||||||
|
const replaceBtn = document.getElementById('replace-cover-btn');
|
||||||
|
const previewHeader = document.getElementById('preview-card-header');
|
||||||
const charSelect = document.getElementById('character_select');
|
const charSelect = document.getElementById('character_select');
|
||||||
const charContext = document.getElementById('character-context');
|
const charContext = document.getElementById('character-context');
|
||||||
|
|
||||||
// Toggle character context info
|
|
||||||
charSelect.addEventListener('change', () => {
|
charSelect.addEventListener('change', () => {
|
||||||
if (charSelect.value && charSelect.value !== '__random__') {
|
charContext.classList.toggle('d-none', !charSelect.value || charSelect.value === '__random__');
|
||||||
charContext.classList.remove('d-none');
|
|
||||||
} else {
|
|
||||||
charContext.classList.add('d-none');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let currentJobId = null;
|
function selectPreview(relativePath, imageUrl) {
|
||||||
let currentAction = null;
|
if (!relativePath) return;
|
||||||
|
previewImg.src = imageUrl;
|
||||||
|
previewPath.value = relativePath;
|
||||||
|
replaceBtn.disabled = false;
|
||||||
|
previewCard.classList.remove('d-none');
|
||||||
|
previewHeader.classList.replace('bg-secondary', 'bg-success');
|
||||||
|
previewCard.classList.replace('border-secondary', 'border-success');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
const img = e.target.closest('img[data-preview-path]');
|
||||||
|
if (img) selectPreview(img.dataset.previewPath, img.src);
|
||||||
|
});
|
||||||
|
|
||||||
async function waitForJob(jobId) {
|
async function waitForJob(jobId) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -296,8 +292,7 @@
|
|||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.status === 'done') { clearInterval(poll); resolve(data); }
|
if (data.status === 'done') { clearInterval(poll); resolve(data); }
|
||||||
else if (data.status === 'failed' || data.status === 'removed') { clearInterval(poll); reject(new Error(data.error || 'Job failed')); }
|
else if (data.status === 'failed' || data.status === 'removed') { clearInterval(poll); reject(new Error(data.error || 'Job failed')); }
|
||||||
else if (data.status === 'processing') progressLabel.textContent = 'Generating…';
|
else progressLabel.textContent = data.status === 'processing' ? 'Generating…' : 'Queued…';
|
||||||
else progressLabel.textContent = 'Queued…';
|
|
||||||
} catch (err) { console.error('Poll error:', err); }
|
} catch (err) { console.error('Poll error:', err); }
|
||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
@@ -307,9 +302,8 @@
|
|||||||
const submitter = e.submitter;
|
const submitter = e.submitter;
|
||||||
if (!submitter || submitter.value !== 'preview') return;
|
if (!submitter || submitter.value !== 'preview') return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
currentAction = submitter.value;
|
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
formData.append('action', currentAction);
|
formData.append('action', 'preview');
|
||||||
progressContainer.classList.remove('d-none');
|
progressContainer.classList.remove('d-none');
|
||||||
progressBar.style.width = '100%'; progressBar.textContent = '';
|
progressBar.style.width = '100%'; progressBar.textContent = '';
|
||||||
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
||||||
@@ -319,26 +313,21 @@
|
|||||||
method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.error) { alert('Error: ' + data.error); progressContainer.classList.add('d-none'); return; }
|
if (data.error) { alert('Error: ' + data.error); return; }
|
||||||
currentJobId = data.job_id;
|
progressLabel.textContent = 'Queued…';
|
||||||
const jobResult = await waitForJob(currentJobId);
|
const jobResult = await waitForJob(data.job_id);
|
||||||
currentJobId = null;
|
if (jobResult.result?.image_url) {
|
||||||
if (jobResult.result && jobResult.result.image_url) {
|
selectPreview(jobResult.result.relative_path, jobResult.result.image_url);
|
||||||
previewImg.src = jobResult.result.image_url;
|
addToPreviewGallery(jobResult.result.image_url, jobResult.result.relative_path, '');
|
||||||
if (previewCard) previewCard.classList.remove('d-none');
|
|
||||||
const replaceBtn = document.getElementById('replace-cover-btn');
|
|
||||||
if (replaceBtn) replaceBtn.disabled = false;
|
|
||||||
}
|
}
|
||||||
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
|
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
|
||||||
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
|
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Batch: Generate All Characters
|
|
||||||
const allCharacters = [
|
const allCharacters = [
|
||||||
{% for char in characters %}{ slug: "{{ char.slug }}", name: {{ char.name | tojson }} },
|
{% for char in characters %}{ slug: "{{ char.slug }}", name: {{ char.name | tojson }} },
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
];
|
];
|
||||||
|
|
||||||
let stopBatch = false;
|
let stopBatch = false;
|
||||||
const generateAllBtn = document.getElementById('generate-all-btn');
|
const generateAllBtn = document.getElementById('generate-all-btn');
|
||||||
const stopAllBtn = document.getElementById('stop-all-btn');
|
const stopAllBtn = document.getElementById('stop-all-btn');
|
||||||
@@ -346,7 +335,7 @@
|
|||||||
const batchLabel = document.getElementById('batch-label');
|
const batchLabel = document.getElementById('batch-label');
|
||||||
const batchBar = document.getElementById('batch-bar');
|
const batchBar = document.getElementById('batch-bar');
|
||||||
|
|
||||||
function addToPreviewGallery(imageUrl, charName) {
|
function addToPreviewGallery(imageUrl, relativePath, charName) {
|
||||||
const gallery = document.getElementById('preview-gallery');
|
const gallery = document.getElementById('preview-gallery');
|
||||||
const placeholder = document.getElementById('gallery-empty');
|
const placeholder = document.getElementById('gallery-empty');
|
||||||
if (placeholder) placeholder.remove();
|
if (placeholder) placeholder.remove();
|
||||||
@@ -357,8 +346,9 @@
|
|||||||
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
style="cursor: pointer; aspect-ratio: 1; object-fit: cover; width: 100%;"
|
||||||
onclick="showImage(this.src)"
|
onclick="showImage(this.src)"
|
||||||
data-bs-toggle="modal" data-bs-target="#imageModal"
|
data-bs-toggle="modal" data-bs-target="#imageModal"
|
||||||
|
data-preview-path="${relativePath}"
|
||||||
title="${charName}">
|
title="${charName}">
|
||||||
<div class="position-absolute bottom-0 start-0 w-100 bg-dark bg-opacity-50 text-white p-1 rounded-bottom" style="font-size: 0.7rem; line-height: 1.2;">${charName}</div>
|
${charName ? `<div class="position-absolute bottom-0 start-0 w-100 bg-dark bg-opacity-50 text-white p-1 rounded-bottom" style="font-size: 0.7rem; line-height: 1.2;">${charName}</div>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
gallery.insertBefore(col, gallery.firstChild);
|
gallery.insertBefore(col, gallery.firstChild);
|
||||||
const badge = document.querySelector('#previews-tab .badge');
|
const badge = document.querySelector('#previews-tab .badge');
|
||||||
@@ -373,37 +363,37 @@
|
|||||||
stopAllBtn.classList.remove('d-none');
|
stopAllBtn.classList.remove('d-none');
|
||||||
batchProgress.classList.remove('d-none');
|
batchProgress.classList.remove('d-none');
|
||||||
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
|
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
|
||||||
for (let i = 0; i < allCharacters.length; i++) {
|
|
||||||
|
const genForm = document.getElementById('generate-form');
|
||||||
|
const formAction = genForm.getAttribute('action');
|
||||||
|
batchLabel.textContent = 'Queuing all characters…';
|
||||||
|
const pending = [];
|
||||||
|
for (const char of allCharacters) {
|
||||||
if (stopBatch) break;
|
if (stopBatch) break;
|
||||||
const char = allCharacters[i];
|
|
||||||
batchBar.style.width = `${Math.round((i / allCharacters.length) * 100)}%`;
|
|
||||||
batchLabel.textContent = `Generating ${char.name} (${i + 1} / ${allCharacters.length})`;
|
|
||||||
const genForm = document.getElementById('generate-form');
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
genForm.querySelectorAll('input[name="include_field"]:checked').forEach(cb => fd.append('include_field', cb.value));
|
genForm.querySelectorAll('input[name="include_field"]:checked').forEach(cb => fd.append('include_field', cb.value));
|
||||||
fd.append('character_slug', char.slug);
|
fd.append('character_slug', char.slug);
|
||||||
fd.append('action', 'preview');
|
fd.append('action', 'preview');
|
||||||
try {
|
try {
|
||||||
progressContainer.classList.remove('d-none');
|
const resp = await fetch(formAction, { method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||||
progressBar.style.width = '100%'; progressBar.textContent = '';
|
|
||||||
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
|
||||||
progressLabel.textContent = `${char.name}: Queuing…`;
|
|
||||||
const resp = await fetch(genForm.getAttribute('action'), {
|
|
||||||
method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
||||||
});
|
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.error) { console.error(`Error for ${char.name}:`, data.error); progressContainer.classList.add('d-none'); continue; }
|
if (!data.error) pending.push({ char, jobId: data.job_id });
|
||||||
currentJobId = data.job_id;
|
} catch (err) { console.error(`Submit error for ${char.name}:`, err); }
|
||||||
const jobResult = await waitForJob(currentJobId);
|
|
||||||
currentJobId = null;
|
|
||||||
if (jobResult.result && jobResult.result.image_url) {
|
|
||||||
addToPreviewGallery(jobResult.result.image_url, char.name);
|
|
||||||
previewImg.src = jobResult.result.image_url;
|
|
||||||
if (previewCard) previewCard.classList.remove('d-none');
|
|
||||||
}
|
|
||||||
} catch (err) { console.error(`Failed for ${char.name}:`, err); currentJobId = null; }
|
|
||||||
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
batchBar.style.width = '0%';
|
||||||
|
let done = 0;
|
||||||
|
const total = pending.length;
|
||||||
|
batchLabel.textContent = `0 / ${total} complete`;
|
||||||
|
await Promise.all(pending.map(({ char, jobId }) =>
|
||||||
|
waitForJob(jobId).then(result => {
|
||||||
|
done++;
|
||||||
|
batchBar.style.width = `${Math.round((done / total) * 100)}%`;
|
||||||
|
batchLabel.textContent = `${done} / ${total} complete`;
|
||||||
|
if (result.result?.image_url) addToPreviewGallery(result.result.image_url, result.result.relative_path, char.name);
|
||||||
|
}).catch(err => { done++; console.error(`Failed for ${char.name}:`, err); })
|
||||||
|
));
|
||||||
|
|
||||||
batchBar.style.width = '100%';
|
batchBar.style.width = '100%';
|
||||||
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
|
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
|
||||||
generateAllBtn.disabled = false;
|
generateAllBtn.disabled = false;
|
||||||
@@ -414,10 +404,9 @@
|
|||||||
stopAllBtn.addEventListener('click', () => {
|
stopAllBtn.addEventListener('click', () => {
|
||||||
stopBatch = true;
|
stopBatch = true;
|
||||||
stopAllBtn.classList.add('d-none');
|
stopAllBtn.classList.add('d-none');
|
||||||
batchLabel.textContent = 'Stopping after current generation...';
|
batchLabel.textContent = 'Stopping…';
|
||||||
});
|
});
|
||||||
|
|
||||||
// JSON Editor
|
|
||||||
initJsonEditor('{{ url_for("save_style_json", slug=style.slug) }}');
|
initJsonEditor('{{ url_for("save_style_json", slug=style.slug) }}');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2>Style Gallery</h2>
|
<h2>Style Library</h2>
|
||||||
<div class="d-flex gap-1 align-items-center">
|
<div class="d-flex gap-1 align-items-center">
|
||||||
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for styles without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="batch-generate-btn" class="btn btn-sm btn-outline-success btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Generate cover images for styles without one"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all styles"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
<button id="regenerate-all-btn" class="btn btn-sm btn-outline-danger btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Regenerate cover images for all styles"><img src="{{ url_for('static', filename='icons/new-cover-batch.png') }}"></button>
|
||||||
|
|||||||
Reference in New Issue
Block a user