mirror of
https://github.com/vladmandic/automatic
synced 2026-08-25 22:20:46 +02:00
f9ab0bf04d
Surface YoloRestorer.restore() as a standalone operation: a Detailer
postprocessing script in the Process tab and a thin /sdapi/v1/detail
endpoint, neither requiring a base generation pass.
- modules/postprocess/yolo.py: YoloRestorer.make_processing() builds the
synthetic Img2Img processing object both entry points feed to restore(),
resolving the seed so the inpaint passes are reproducible
- modules/api/process.py: post_detail handler exposes the full detailer
parameter set and returns the detailed image plus optional annotations
as base64
- scripts/postprocessing_detailer.py: reuses shared.yolo.ui('extras') and
runs through make_processing()
- modules/postprocessing.py: run_extras takes a per-script script_args
dict, also letting the extras API drive other scripts such as Remove
background; omitting it leaves existing callers unchanged
- modules/api/models.py: ReqDetail / ResDetail
- modules/processing_info.py: guard create_infotext's Image/Hires CFG
reporting against an unset (None) cfg_image, matching the is-not-None
checks the other cfg_image readers use; the detailer inpaint pass runs
with it unset
- test/test-detailer-api.py: covers both paths; effect tests measure the
diff inside the detected region with extreme isolated parameter values,
and the suite disables model quantization for the run and restores the
original settings afterward
245 lines
15 KiB
Python
245 lines
15 KiB
Python
import os
|
|
from installer import git_commit
|
|
from modules import shared, sd_samplers_common, sd_vae
|
|
from modules.logger import log
|
|
from modules.processing_class import StableDiffusionProcessing
|
|
from modules.infotext import quote
|
|
|
|
|
|
args = {} # maintain history
|
|
infotext = '' # maintain history
|
|
debug = log.trace if os.environ.get('SD_PROCESS_DEBUG', None) is not None else lambda *args, **kwargs: None
|
|
|
|
|
|
def get_last_args():
|
|
return args, infotext
|
|
|
|
|
|
def create_infotext(p: StableDiffusionProcessing, all_prompts=None, all_seeds=None, all_subseeds=None, comments=None, iteration=0, position_in_batch=0, index=None, all_negative_prompts=None, grid=None):
|
|
global args, infotext # pylint: disable=global-statement
|
|
if p is None:
|
|
log.warning('Processing info: no data')
|
|
return ''
|
|
if not hasattr(shared.sd_model, 'sd_checkpoint_info'):
|
|
return ''
|
|
|
|
if index is None:
|
|
index = position_in_batch + iteration * p.batch_size
|
|
if all_prompts is None:
|
|
all_prompts = p.all_prompts or [p.prompt]
|
|
if all_negative_prompts is None:
|
|
all_negative_prompts = p.all_negative_prompts or [p.negative_prompt]
|
|
if all_seeds is None:
|
|
all_seeds = p.all_seeds or [p.seed]
|
|
if all_subseeds is None:
|
|
all_subseeds = p.all_subseeds or [p.subseed]
|
|
while len(all_prompts) <= index:
|
|
all_prompts.insert(0, p.prompt)
|
|
while len(all_seeds) <= index:
|
|
all_seeds.insert(0, int(p.seed))
|
|
while len(all_subseeds) <= index:
|
|
all_subseeds.insert(0, int(p.subseed))
|
|
while len(all_negative_prompts) <= index:
|
|
all_negative_prompts.insert(0, p.negative_prompt)
|
|
if p.all_templates is not None:
|
|
while len(p.all_templates) <= index:
|
|
p.all_templates.insert(0, '')
|
|
if p.all_negative_templates is not None:
|
|
while len(p.all_negative_templates) <= index:
|
|
p.all_negative_templates.insert(0, '')
|
|
|
|
comment = ', '.join(comments) if comments is not None and type(comments) is list else None
|
|
ops = list(set(p.ops))
|
|
args = {
|
|
# basic
|
|
"Steps": p.steps,
|
|
"Size": f"{p.width}x{p.height}" if hasattr(p, 'width') and hasattr(p, 'height') else None,
|
|
"Sampler": p.sampler_name if p.sampler_name != 'Default' else None,
|
|
"Scheduler": shared.sd_model.scheduler.__class__.__name__ if getattr(shared.sd_model, 'scheduler', None) is not None else None,
|
|
"Seed": all_seeds[index],
|
|
"Seed resize from": None if p.seed_resize_from_w <= 0 or p.seed_resize_from_h <= 0 else f"{p.seed_resize_from_w}x{p.seed_resize_from_h}",
|
|
"CFG scale": p.cfg_scale if p.cfg_scale > -1 else None,
|
|
"CFG rescale": p.cfg_rescale if p.cfg_rescale > -1 else None,
|
|
"CFG end": p.cfg_end if p.cfg_end < 1.0 else None,
|
|
"CFG true": p.cfg_true if p.cfg_true > 0 else None,
|
|
"CFG adaptive": p.cfg_adaptive if p.cfg_adaptive != 0.5 else None,
|
|
"CLiP-skip": p.clip_skip if p.clip_skip > 1 else None,
|
|
"Batch": f'{p.n_iter}x{p.batch_size}' if p.n_iter > 1 or p.batch_size > 1 else None,
|
|
"Refiner prompt": p.refiner_prompt if len(p.refiner_prompt) > 0 else None,
|
|
"Refiner negative": p.refiner_negative if len(p.refiner_negative) > 0 else None,
|
|
"Styles": "; ".join(p.styles) if p.styles is not None and len(p.styles) > 0 else None,
|
|
"App": 'SD.Next',
|
|
"Version": git_commit,
|
|
"Parser": (getattr(p, 'prompt_attention', None) or shared.opts.prompt_attention) if (getattr(p, 'prompt_attention', None) or shared.opts.prompt_attention) != 'native' else None,
|
|
"Comment": comment,
|
|
"Pipeline": shared.sd_model.__class__.__name__,
|
|
"TE": None if (shared.opts.sd_text_encoder is None or shared.opts.sd_text_encoder == 'Default') else shared.opts.sd_text_encoder,
|
|
"UNet": None if (shared.opts.sd_unet is None or shared.opts.sd_unet == 'Default') else shared.opts.sd_unet,
|
|
"Operations": '; '.join(ops).replace('"', '') if len(p.ops) > 0 else 'none',
|
|
}
|
|
if shared.opts.add_model_name_to_info:
|
|
if getattr(shared.sd_model, 'sd_checkpoint_info', None) is not None:
|
|
args["Model"] = shared.sd_model.sd_checkpoint_info.model_name.replace(',', '').replace(':', '')
|
|
if shared.opts.add_model_hash_to_info:
|
|
if getattr(p, 'sd_model_hash', None) is not None:
|
|
args["Model hash"] = p.sd_model_hash
|
|
elif getattr(shared.sd_model, 'sd_model_hash', None) is not None:
|
|
args["Model hash"] = shared.sd_model.sd_model_hash
|
|
if p.vae_type == 'Full':
|
|
args["VAE"] = (None if not shared.opts.add_model_name_to_info or sd_vae.loaded_vae_file is None else os.path.splitext(os.path.basename(sd_vae.loaded_vae_file))[0])
|
|
elif p.vae_type == 'Tiny':
|
|
args["VAE"] = 'TAESD'
|
|
elif p.vae_type == 'REPA-E':
|
|
args["VAE"] = 'REPA-E'
|
|
elif p.vae_type == 'Remote':
|
|
args["VAE"] = 'Remote'
|
|
if grid is None and (p.n_iter > 1 or p.batch_size > 1) and index >= 0:
|
|
args['Index'] = f'{p.iteration + 1}x{index + 1}'
|
|
if grid is not None:
|
|
args['Grid'] = grid
|
|
if 'txt2img' in p.ops:
|
|
args["Variation seed"] = all_subseeds[index] if p.subseed_strength > 0 else None
|
|
args["Variation strength"] = p.subseed_strength if p.subseed_strength > 0 else None
|
|
if 'hires' in p.ops or 'upscale' in p.ops:
|
|
is_resize = p.hr_resize_mode > 0 and (p.hr_upscaler != 'None' or p.hr_resize_mode == 5)
|
|
is_fixed = p.hr_resize_x > 0 or p.hr_resize_y > 0
|
|
args["Refine"] = p.enable_hr
|
|
if is_resize:
|
|
args["HiRes mode"] = p.hr_resize_mode
|
|
args["HiRes context"] = p.hr_resize_context if p.hr_resize_mode == 5 else None
|
|
args["Hires upscaler"] = p.hr_upscaler
|
|
if is_fixed:
|
|
args["Hires fixed"] = f"{p.hr_resize_x}x{p.hr_resize_y}"
|
|
else:
|
|
args["Hires scale"] = p.hr_scale
|
|
args["Hires size"] = f"{p.hr_upscale_to_x}x{p.hr_upscale_to_y}"
|
|
if p.hr_force or ('Latent' in p.hr_upscaler):
|
|
args["Hires force"] = p.hr_force
|
|
args["Hires steps"] = p.hr_second_pass_steps
|
|
args["Hires strength"] = p.hr_denoising_strength
|
|
args["Hires sampler"] = p.hr_sampler_name if p.hr_sampler_name != 'Default' else None
|
|
args["Hires CFG scale"] = p.cfg_image if (p.cfg_image is not None and p.cfg_image > -1) else None
|
|
if 'refine' in p.ops:
|
|
args["Refine"] = p.enable_hr
|
|
args["Refiner"] = None if (not shared.opts.add_model_name_to_info) or (not shared.sd_refiner) or (not shared.sd_refiner.sd_checkpoint_info.model_name) else shared.sd_refiner.sd_checkpoint_info.model_name.replace(',', '').replace(':', '')
|
|
args['Hires CFG scale'] = p.cfg_image if (p.cfg_image is not None and p.cfg_image > -1) else None
|
|
args['Refiner steps'] = p.refiner_steps
|
|
args['Refiner start'] = p.refiner_start
|
|
args["Hires steps"] = p.hr_second_pass_steps
|
|
args["Hires sampler"] = p.hr_sampler_name if p.hr_sampler_name != 'Default' else None
|
|
if ('img2img' in p.ops or 'inpaint' in p.ops) and ('txt2img' not in p.ops and 'hires' not in p.ops): # real img2img/inpaint
|
|
args["Init image size"] = f"{getattr(p, 'init_img_width', 0)}x{getattr(p, 'init_img_height', 0)}"
|
|
args["Init image hash"] = getattr(p, 'init_img_hash', None)
|
|
args['Image CFG scale'] = p.cfg_image if (p.cfg_image is not None and p.cfg_image > -1) else None
|
|
args["Mask weight"] = getattr(p, "inpainting_mask_weight", shared.opts.inpainting_mask_weight) if p.is_using_inpainting_conditioning else None
|
|
args["Denoising strength"] = getattr(p, 'denoising_strength', None)
|
|
if args["Size"] != args["Init image size"]:
|
|
args['Resize scale'] = float(getattr(p, 'scale_by', None)) if getattr(p, 'scale_by', None) != 1 else None
|
|
args['Resize mode'] = shared.resize_modes[p.resize_mode] if shared.resize_modes[p.resize_mode] != 'None' else None
|
|
if args["Size"] is None:
|
|
args["Size"] = args["Init image size"]
|
|
if p.resize_mode_before != 0 and p.resize_name_before != 'None' and hasattr(p, 'init_images') and p.init_images is not None and len(p.init_images) > 0:
|
|
args['Resize before'] = f"{p.width_before}x{p.height_before}"
|
|
args['Resize mode before'] = p.resize_mode_before
|
|
args['Resize name before'] = p.resize_name_before
|
|
args['Resize scale before'] = float(p.scale_by_before) if p.scale_by_before != 1.0 else None
|
|
if p.resize_mode_after != 0 and p.resize_name_after != 'None':
|
|
args['Resize after'] = f"{p.width_after}x{p.height_after}"
|
|
args['Resize mode after'] = p.resize_mode_after
|
|
args['Resize name after'] = p.resize_name_after
|
|
args['Resize scale after'] = float(p.scale_by_after) if p.scale_by_after != 1.0 else None
|
|
if p.resize_name_mask != 'None' and p.scale_by_mask != 1.0:
|
|
args['Resize mask'] = f"{p.width_mask}x{p.height_mask}"
|
|
args['Resize mode mask'] = p.resize_mode_mask
|
|
args['Resize name mask'] = p.resize_name_mask
|
|
args['Resize scale mask'] = float(p.scale_by_mask)
|
|
if 'detailer' in p.ops:
|
|
_d_models = getattr(p, 'detailer_models', None)
|
|
if _d_models is not None and len(_d_models) > 0:
|
|
args["Detailer"] = ', '.join(_d_models)
|
|
elif len(shared.opts.detailer_args) > 0:
|
|
args["Detailer"] = shared.opts.detailer_args
|
|
else:
|
|
args["Detailer"] = ', '.join(shared.opts.detailer_models)
|
|
args["Detailer steps"] = p.detailer_steps
|
|
args["Detailer strength"] = p.detailer_strength
|
|
args["Detailer resolution"] = p.detailer_resolution if p.detailer_resolution != 1024 else None
|
|
args["Detailer prompt"] = p.detailer_prompt if len(p.detailer_prompt) > 0 else None
|
|
args["Detailer negative"] = p.detailer_negative if len(p.detailer_negative) > 0 else None
|
|
if 'color' in p.ops:
|
|
args["Color correction"] = True
|
|
|
|
def get_opt(key):
|
|
val = getattr(p, key, None)
|
|
if val is not None:
|
|
return val
|
|
return getattr(shared.opts, key, None)
|
|
|
|
_token_method = get_opt('token_merging_method')
|
|
_tome = get_opt('tome_ratio')
|
|
_todo = get_opt('todo_ratio')
|
|
if _token_method == 'ToMe': # tome/todo
|
|
args['ToMe'] = _tome if _tome != 0 else None
|
|
elif _token_method == 'ToDo':
|
|
args['ToDo'] = _todo if _todo != 0 else None
|
|
if hasattr(shared.sd_model, 'embedding_db') and len(shared.sd_model.embedding_db.embeddings_used) > 0: # register used embeddings
|
|
args['Embeddings'] = ', '.join(shared.sd_model.embedding_db.embeddings_used)
|
|
|
|
# samplers
|
|
if getattr(p, 'sampler_name', None) is not None and p.sampler_name.lower() != 'default':
|
|
_eta_delta = get_opt('eta_noise_seed_delta')
|
|
args["Sampler eta delta"] = _eta_delta if _eta_delta != 0 and sd_samplers_common.is_sampler_using_eta_noise_seed_delta(p) else None
|
|
args["Sampler eta multiplier"] = p.initial_noise_multiplier if getattr(p, 'initial_noise_multiplier', 1.0) != 1.0 else None
|
|
args['Sampler timesteps'] = get_opt('schedulers_timesteps') if get_opt('schedulers_timesteps') != shared.opts.data_labels.get('schedulers_timesteps').default else None
|
|
args['Sampler spacing'] = get_opt('schedulers_timestep_spacing') if get_opt('schedulers_timestep_spacing') != shared.opts.data_labels.get('schedulers_timestep_spacing').default else None
|
|
args['Sampler sigma'] = get_opt('schedulers_sigma') if get_opt('schedulers_sigma') != shared.opts.data_labels.get('schedulers_sigma').default else None
|
|
args['Sampler order'] = get_opt('schedulers_solver_order') if get_opt('schedulers_solver_order') != shared.opts.data_labels.get('schedulers_solver_order').default else None
|
|
args['Sampler type'] = get_opt('schedulers_prediction_type') if get_opt('schedulers_prediction_type') != shared.opts.data_labels.get('schedulers_prediction_type').default else None
|
|
args['Sampler beta schedule'] = get_opt('schedulers_beta_schedule') if get_opt('schedulers_beta_schedule') != shared.opts.data_labels.get('schedulers_beta_schedule').default else None
|
|
args['Sampler low order'] = get_opt('schedulers_use_loworder') if get_opt('schedulers_use_loworder') != shared.opts.data_labels.get('schedulers_use_loworder').default else None
|
|
args['Sampler dynamic'] = get_opt('schedulers_use_thresholding') if get_opt('schedulers_use_thresholding') != shared.opts.data_labels.get('schedulers_use_thresholding').default else None
|
|
args['Sampler rescale'] = get_opt('schedulers_rescale_betas') if get_opt('schedulers_rescale_betas') != shared.opts.data_labels.get('schedulers_rescale_betas').default else None
|
|
args['Sampler beta start'] = get_opt('schedulers_beta_start') if get_opt('schedulers_beta_start') != shared.opts.data_labels.get('schedulers_beta_start').default else None
|
|
args['Sampler beta end'] = get_opt('schedulers_beta_end') if get_opt('schedulers_beta_end') != shared.opts.data_labels.get('schedulers_beta_end').default else None
|
|
args['Sampler range'] = get_opt('schedulers_timesteps_range') if get_opt('schedulers_timesteps_range') != shared.opts.data_labels.get('schedulers_timesteps_range').default else None
|
|
args['Sampler shift'] = get_opt('schedulers_shift') if get_opt('schedulers_shift') != shared.opts.data_labels.get('schedulers_shift').default else None
|
|
args['Sampler dynamic shift'] = get_opt('schedulers_dynamic_shift') if get_opt('schedulers_dynamic_shift') != shared.opts.data_labels.get('schedulers_dynamic_shift').default else None
|
|
|
|
# model specific
|
|
if shared.sd_model_type == 'h1':
|
|
args['LLM'] = None if shared.opts.model_h1_llama_repo == 'Default' else shared.opts.model_h1_llama_repo
|
|
|
|
args.update(p.extra_generation_params)
|
|
for k, v in args.copy().items():
|
|
if v is None:
|
|
del args[k]
|
|
if type(v) is float or type(v) is int:
|
|
if v <= -1:
|
|
del args[k]
|
|
if type(v) is list:
|
|
if len(v) == 0:
|
|
del args[k]
|
|
else:
|
|
args[k] = ', '.join([str(x) for x in v])
|
|
if type(v) is str:
|
|
if len(v) == 0 or v == '0x0':
|
|
del args[k]
|
|
debug(f'Infotext: args={args}')
|
|
params_text = ", ".join([k if k == v else f'{k}: {quote(v)}' for k, v in args.items()])
|
|
|
|
if hasattr(p, 'original_prompt'):
|
|
args['Original prompt'] = p.original_prompt
|
|
if hasattr(p, 'original_negative'):
|
|
args['Original negative'] = p.original_negative
|
|
|
|
template = p.all_templates[index] if p.all_templates is not None and p.all_templates[index] else None
|
|
negative_template = p.all_negative_templates[index] if p.all_negative_templates is not None and p.all_negative_templates[index] else None
|
|
template_text = f"\nTemplate: {template}" if template else ''
|
|
negative_template_text = f"\nNegative template: {negative_template}" if negative_template else ''
|
|
|
|
negative_prompt_text = f"\nNegative prompt: {all_negative_prompts[index] if all_negative_prompts[index] else ''}"
|
|
infotext = f"{all_prompts[index]}{negative_prompt_text}{template_text}{negative_template_text}\n{params_text}".strip()
|
|
|
|
debug(f'Infotext: "{infotext}"')
|
|
return infotext
|