REST API (routes/api.py): Three endpoints behind API key auth for programmatic image generation via presets — list presets, queue generation with optional overrides, and poll job status. Shared generation logic extracted from routes/presets.py into services/generation.py so both web UI and API use the same code path. Fallback covers: library index pages now show a random generated image at reduced opacity when no cover is assigned, instead of "No Image". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
201 lines
8.2 KiB
Python
201 lines
8.2 KiB
Python
import json
|
||
import random
|
||
import logging
|
||
|
||
from models import db, Character, Checkpoint, Preset
|
||
from services.prompts import build_prompt, _dedup_tags
|
||
from services.workflow import _prepare_workflow, _get_default_checkpoint
|
||
from services.job_queue import _enqueue_job, _make_finalize
|
||
from services.sync import _resolve_preset_entity, _resolve_preset_fields
|
||
|
||
logger = logging.getLogger('gaze')
|
||
|
||
|
||
def generate_from_preset(preset, overrides=None):
|
||
"""Execute preset-based generation.
|
||
|
||
Args:
|
||
preset: Preset ORM object
|
||
overrides: optional dict with keys:
|
||
checkpoint, extra_positive, extra_negative, seed, width, height, action
|
||
|
||
Returns:
|
||
job dict from _enqueue_job()
|
||
"""
|
||
if overrides is None:
|
||
overrides = {}
|
||
|
||
action = overrides.get('action', 'preview')
|
||
extra_positive = overrides.get('extra_positive', '').strip()
|
||
extra_negative = overrides.get('extra_negative', '').strip()
|
||
|
||
data = preset.data
|
||
|
||
# Resolve entities
|
||
char_cfg = data.get('character', {})
|
||
character = _resolve_preset_entity('character', char_cfg.get('character_id'))
|
||
if not character:
|
||
character = Character.query.order_by(db.func.random()).first()
|
||
|
||
outfit_cfg = data.get('outfit', {})
|
||
action_cfg = data.get('action', {})
|
||
style_cfg = data.get('style', {})
|
||
scene_cfg = data.get('scene', {})
|
||
detailer_cfg = data.get('detailer', {})
|
||
look_cfg = data.get('look', {})
|
||
ckpt_cfg = data.get('checkpoint', {})
|
||
|
||
outfit = _resolve_preset_entity('outfit', outfit_cfg.get('outfit_id'))
|
||
action_obj = _resolve_preset_entity('action', action_cfg.get('action_id'))
|
||
style_obj = _resolve_preset_entity('style', style_cfg.get('style_id'))
|
||
scene_obj = _resolve_preset_entity('scene', scene_cfg.get('scene_id'))
|
||
detailer_obj = _resolve_preset_entity('detailer', detailer_cfg.get('detailer_id'))
|
||
look_obj = _resolve_preset_entity('look', look_cfg.get('look_id'))
|
||
|
||
# Checkpoint: override > preset config > default
|
||
checkpoint_override = overrides.get('checkpoint', '').strip() if overrides.get('checkpoint') else ''
|
||
if checkpoint_override:
|
||
ckpt_obj = Checkpoint.query.filter_by(checkpoint_path=checkpoint_override).first()
|
||
ckpt_path = checkpoint_override
|
||
ckpt_data = ckpt_obj.data if ckpt_obj else None
|
||
else:
|
||
preset_ckpt = ckpt_cfg.get('checkpoint_path')
|
||
if preset_ckpt == 'random':
|
||
ckpt_obj = Checkpoint.query.order_by(db.func.random()).first()
|
||
ckpt_path = ckpt_obj.checkpoint_path if ckpt_obj else None
|
||
ckpt_data = ckpt_obj.data if ckpt_obj else None
|
||
elif preset_ckpt:
|
||
ckpt_obj = Checkpoint.query.filter_by(checkpoint_path=preset_ckpt).first()
|
||
ckpt_path = preset_ckpt
|
||
ckpt_data = ckpt_obj.data if ckpt_obj else None
|
||
else:
|
||
ckpt_path, ckpt_data = _get_default_checkpoint()
|
||
|
||
# Resolve selected fields from preset toggles
|
||
selected_fields = _resolve_preset_fields(data)
|
||
|
||
# Build combined data for prompt building
|
||
active_wardrobe = char_cfg.get('fields', {}).get('wardrobe', {}).get('outfit', 'default')
|
||
wardrobe_source = outfit.data.get('wardrobe', {}) if outfit else None
|
||
if wardrobe_source is None:
|
||
wardrobe_source = character.get_active_wardrobe() if character else {}
|
||
|
||
combined_data = {
|
||
'character_id': character.character_id if character else 'unknown',
|
||
'identity': character.data.get('identity', {}) if character else {},
|
||
'defaults': character.data.get('defaults', {}) if character else {},
|
||
'wardrobe': wardrobe_source,
|
||
'styles': character.data.get('styles', {}) if character else {},
|
||
'lora': (look_obj.data.get('lora', {}) if look_obj
|
||
else (character.data.get('lora', {}) if character else {})),
|
||
'tags': (character.data.get('tags', []) if character else []) + data.get('tags', []),
|
||
}
|
||
|
||
# Build extras prompt from secondary resources
|
||
extras_parts = []
|
||
if action_obj:
|
||
action_fields = action_cfg.get('fields', {})
|
||
from utils import _BODY_GROUP_KEYS
|
||
for key in _BODY_GROUP_KEYS:
|
||
val_cfg = action_fields.get(key, True)
|
||
if val_cfg == 'random':
|
||
val_cfg = random.choice([True, False])
|
||
if val_cfg:
|
||
val = action_obj.data.get('action', {}).get(key, '')
|
||
if val:
|
||
extras_parts.append(val)
|
||
if action_cfg.get('use_lora', True):
|
||
trg = action_obj.data.get('lora', {}).get('lora_triggers', '')
|
||
if trg:
|
||
extras_parts.append(trg)
|
||
extras_parts.extend(action_obj.data.get('tags', []))
|
||
if style_obj:
|
||
s = style_obj.data.get('style', {})
|
||
if s.get('artist_name'):
|
||
extras_parts.append(f"by {s['artist_name']}")
|
||
if s.get('artistic_style'):
|
||
extras_parts.append(s['artistic_style'])
|
||
if style_cfg.get('use_lora', True):
|
||
trg = style_obj.data.get('lora', {}).get('lora_triggers', '')
|
||
if trg:
|
||
extras_parts.append(trg)
|
||
if scene_obj:
|
||
scene_fields = scene_cfg.get('fields', {})
|
||
for key in ['background', 'foreground', 'furniture', 'colors', 'lighting', 'theme']:
|
||
val_cfg = scene_fields.get(key, True)
|
||
if val_cfg == 'random':
|
||
val_cfg = random.choice([True, False])
|
||
if val_cfg:
|
||
val = scene_obj.data.get('scene', {}).get(key, '')
|
||
if val:
|
||
extras_parts.append(val)
|
||
if scene_cfg.get('use_lora', True):
|
||
trg = scene_obj.data.get('lora', {}).get('lora_triggers', '')
|
||
if trg:
|
||
extras_parts.append(trg)
|
||
extras_parts.extend(scene_obj.data.get('tags', []))
|
||
if detailer_obj:
|
||
prompt_val = detailer_obj.data.get('prompt', '')
|
||
if isinstance(prompt_val, list):
|
||
extras_parts.extend(p for p in prompt_val if p)
|
||
elif prompt_val:
|
||
extras_parts.append(prompt_val)
|
||
if detailer_cfg.get('use_lora', True):
|
||
trg = detailer_obj.data.get('lora', {}).get('lora_triggers', '')
|
||
if trg:
|
||
extras_parts.append(trg)
|
||
|
||
with open('comfy_workflow.json', 'r') as f:
|
||
workflow = json.load(f)
|
||
|
||
prompts = build_prompt(combined_data, selected_fields, default_fields=None,
|
||
active_outfit=active_wardrobe)
|
||
if extras_parts:
|
||
extra_str = ', '.join(filter(None, extras_parts))
|
||
prompts['main'] = _dedup_tags(f"{prompts['main']}, {extra_str}" if prompts['main'] else extra_str)
|
||
|
||
if extra_positive:
|
||
prompts["main"] = f"{prompts['main']}, {extra_positive}"
|
||
|
||
# Parse optional seed
|
||
fixed_seed = overrides.get('seed')
|
||
if fixed_seed is not None:
|
||
fixed_seed = int(fixed_seed)
|
||
|
||
# Resolution: override > preset config > workflow default
|
||
res_cfg = data.get('resolution', {})
|
||
override_width = overrides.get('width')
|
||
override_height = overrides.get('height')
|
||
if override_width and override_height:
|
||
gen_width = int(override_width)
|
||
gen_height = int(override_height)
|
||
elif res_cfg.get('random', False):
|
||
_RES_OPTIONS = [
|
||
(1024, 1024), (1152, 896), (896, 1152), (1344, 768),
|
||
(768, 1344), (1280, 800), (800, 1280),
|
||
]
|
||
gen_width, gen_height = random.choice(_RES_OPTIONS)
|
||
else:
|
||
gen_width = res_cfg.get('width') or None
|
||
gen_height = res_cfg.get('height') or None
|
||
|
||
workflow = _prepare_workflow(
|
||
workflow, character, prompts,
|
||
checkpoint=ckpt_path, checkpoint_data=ckpt_data,
|
||
custom_negative=extra_negative or None,
|
||
outfit=outfit if outfit_cfg.get('use_lora', True) else None,
|
||
action=action_obj if action_cfg.get('use_lora', True) else None,
|
||
style=style_obj if style_cfg.get('use_lora', True) else None,
|
||
scene=scene_obj if scene_cfg.get('use_lora', True) else None,
|
||
detailer=detailer_obj if detailer_cfg.get('use_lora', True) else None,
|
||
look=look_obj,
|
||
fixed_seed=fixed_seed,
|
||
width=gen_width,
|
||
height=gen_height,
|
||
)
|
||
|
||
label = f"Preset: {preset.name} – {action}"
|
||
job = _enqueue_job(label, workflow, _make_finalize('presets', preset.slug, Preset, action))
|
||
|
||
return job
|