Files
character-browser/services/generation.py
Aodhan Collins 7d79e626a5 Add REST API for preset-based generation and fallback cover images
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>
2026-03-15 21:19:12 +00:00

201 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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