Files
character-browser/routes/multi_char.py
2026-03-15 17:45:17 +00:00

154 lines
7.5 KiB
Python

import json
import logging
from flask import render_template, request, session
from models import Character, Action, Style, Scene, Detailer, Checkpoint
from services.prompts import build_multi_prompt, build_extras_prompt, _resolve_character
from services.workflow import _prepare_workflow
from services.job_queue import _enqueue_job, _make_finalize
from services.file_io import get_available_checkpoints
logger = logging.getLogger('gaze')
def register_routes(app):
@app.route('/multi', methods=['GET', 'POST'])
def multi_char():
characters = Character.query.order_by(Character.name).all()
checkpoints = get_available_checkpoints()
actions = Action.query.order_by(Action.name).all()
scenes = Scene.query.order_by(Scene.name).all()
styles = Style.query.order_by(Style.name).all()
detailers = Detailer.query.order_by(Detailer.name).all()
if not checkpoints:
checkpoints = ["Noob/oneObsession_v19Atypical.safetensors"]
if request.method == 'POST':
char_a_slug = request.form.get('char_a')
char_b_slug = request.form.get('char_b')
checkpoint = request.form.get('checkpoint')
custom_positive = request.form.get('positive_prompt', '')
custom_negative = request.form.get('negative_prompt', '')
action_slugs = request.form.getlist('action_slugs')
scene_slugs = request.form.getlist('scene_slugs')
style_slugs = request.form.getlist('style_slugs')
detailer_slugs = request.form.getlist('detailer_slugs')
width = request.form.get('width') or 1024
height = request.form.get('height') or 1024
char_a = _resolve_character(char_a_slug)
char_b = _resolve_character(char_b_slug)
if not char_a or not char_b:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return {'error': 'Both characters must be selected'}, 400
return render_template('multi_char.html', characters=characters, checkpoints=checkpoints,
actions=actions, scenes=scenes, styles=styles, detailers=detailers)
sel_actions = Action.query.filter(Action.slug.in_(action_slugs)).all() if action_slugs else []
sel_scenes = Scene.query.filter(Scene.slug.in_(scene_slugs)).all() if scene_slugs else []
sel_styles = Style.query.filter(Style.slug.in_(style_slugs)).all() if style_slugs else []
sel_detailers = Detailer.query.filter(Detailer.slug.in_(detailer_slugs)).all() if detailer_slugs else []
try:
with open('comfy_workflow.json', 'r') as f:
workflow = json.load(f)
# Build extras from mix-and-match selections
extras = build_extras_prompt(sel_actions, [], sel_scenes, sel_styles, sel_detailers)
if custom_positive:
extras = f"{custom_positive}, {extras}" if extras else custom_positive
# Build multi-character prompt
prompts = build_multi_prompt(char_a, char_b, extras_prompt=extras)
# Apply prompt overrides if provided
override_main = request.form.get('override_prompt', '').strip()
if override_main:
prompts['main'] = override_main
for key in ('char_a_main', 'char_b_main', 'char_a_face', 'char_b_face'):
override = request.form.get(f'override_{key}', '').strip()
if override:
prompts[key] = override
# Parse optional seed
seed_val = request.form.get('seed', '').strip()
fixed_seed = int(seed_val) if seed_val else None
# Prepare workflow with both character LoRAs
ckpt_obj = Checkpoint.query.filter_by(checkpoint_path=checkpoint).first() if checkpoint else None
workflow = _prepare_workflow(
workflow, char_a, prompts, checkpoint, custom_negative,
action=sel_actions[0] if sel_actions else None,
style=sel_styles[0] if sel_styles else None,
detailer=sel_detailers[0] if sel_detailers else None,
scene=sel_scenes[0] if sel_scenes else None,
width=width,
height=height,
checkpoint_data=ckpt_obj.data if ckpt_obj else None,
fixed_seed=fixed_seed,
character_b=char_b,
)
label = f"Multi: {char_a.name} + {char_b.name}"
slug_pair = f"{char_a.slug}_{char_b.slug}"
_finalize = _make_finalize('multi', slug_pair)
job = _enqueue_job(label, workflow, _finalize)
# Save to session
session['multi_char_a'] = char_a.slug
session['multi_char_b'] = char_b.slug
session.modified = True
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return {'status': 'queued', 'job_id': job['id']}
except Exception as e:
logger.exception("Multi-char generation error: %s", e)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return {'error': str(e)}, 500
return render_template('multi_char.html', characters=characters, checkpoints=checkpoints,
actions=actions, scenes=scenes, styles=styles, detailers=detailers,
selected_char_a=session.get('multi_char_a', ''),
selected_char_b=session.get('multi_char_b', ''))
@app.route('/multi/preview_prompt', methods=['POST'])
def multi_preview_prompt():
char_a_slug = request.form.get('char_a')
char_b_slug = request.form.get('char_b')
if not char_a_slug or not char_b_slug:
return {'error': 'Both characters must be selected'}, 400
char_a = _resolve_character(char_a_slug)
char_b = _resolve_character(char_b_slug)
if not char_a or not char_b:
return {'error': 'Character not found'}, 404
action_slugs = request.form.getlist('action_slugs')
scene_slugs = request.form.getlist('scene_slugs')
style_slugs = request.form.getlist('style_slugs')
detailer_slugs = request.form.getlist('detailer_slugs')
custom_positive = request.form.get('positive_prompt', '')
sel_actions = Action.query.filter(Action.slug.in_(action_slugs)).all() if action_slugs else []
sel_scenes = Scene.query.filter(Scene.slug.in_(scene_slugs)).all() if scene_slugs else []
sel_styles = Style.query.filter(Style.slug.in_(style_slugs)).all() if style_slugs else []
sel_detailers = Detailer.query.filter(Detailer.slug.in_(detailer_slugs)).all() if detailer_slugs else []
extras = build_extras_prompt(sel_actions, [], sel_scenes, sel_styles, sel_detailers)
if custom_positive:
extras = f"{custom_positive}, {extras}" if extras else custom_positive
prompts = build_multi_prompt(char_a, char_b, extras_prompt=extras)
return {
'prompt': prompts['main'],
'hand': prompts['hand'],
'char_a_main': prompts['char_a_main'],
'char_a_face': prompts['char_a_face'],
'char_b_main': prompts['char_b_main'],
'char_b_face': prompts['char_b_face'],
}