mirror of
https://github.com/vladmandic/automatic
synced 2026-08-31 09:31:00 +02:00
e804d6df21
Reorder samplers_data_diffusers into recognizable solver-family groups (Euler, DPM/DPM++, UniPC/DEIS, Heun/KDPM2, ER-SDE, Classic, Distilled, Misc), each ending with its FlowMatch variants, and Res4Lyf as a fenced experimental section, so the dropdown is scannable. Dividers are SamplerData sentinels with U+2500 names: create_sampler keeps the current scheduler when one is selected, get_sampler_name falls back to Default, set_samplers and validate_sampler_name exclude them, and a visible_samplers() helper drops them from the xyz axes, detailer, and folder pickers. The main and refine dropdowns render them as section labels. No sampler is removed or renamed, so saved infotexts, styles, and API calls keep resolving.
652 lines
36 KiB
Python
652 lines
36 KiB
Python
from typing import TYPE_CHECKING
|
|
import os
|
|
import re
|
|
import threading
|
|
from copy import copy
|
|
import numpy as np
|
|
import gradio as gr
|
|
from PIL import Image, ImageDraw
|
|
from modules.logger import log
|
|
from modules import shared, processing, devices, processing_class, ui_common, ui_components, ui_symbols, images, extra_networks, sd_models
|
|
from modules.detailer import Detailer
|
|
|
|
|
|
def detailer_opt(p, attr, opts_attr=None):
|
|
"""Read detailer param from processing object if set, otherwise fall back to shared.opts."""
|
|
if p is not None:
|
|
val = getattr(p, attr, None)
|
|
if val is not None:
|
|
return val
|
|
return getattr(shared.opts, opts_attr or attr, None)
|
|
|
|
|
|
predefined = [ # <https://huggingface.co/vladmandic/yolo-detailers/tree/main>
|
|
'https://github.com/ultralytics/assets/releases/download/v8.3.0/yolo11m.pt',
|
|
'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/face-yolo8n.pt',
|
|
'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/face-yolo8m.pt',
|
|
'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/hand_yolov8n.pt',
|
|
'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/person_yolov8n-seg.pt',
|
|
'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/eyes-v1.pt',
|
|
'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/eyes-full-v1.pt',
|
|
'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/anzhc-eyes-seg.pt',
|
|
'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/anzhc-face-1024-seg-8n.pt',
|
|
'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/anzhc-head-seg-8n.pt',
|
|
]
|
|
load_lock = threading.Lock()
|
|
|
|
|
|
class YoloResult:
|
|
def __init__(self, cls: int, label: str, score: float, box: list[int], mask: Image.Image = None, item: Image.Image = None, width = 0, height = 0, args = None):
|
|
if args is None:
|
|
args = {}
|
|
self.cls = cls
|
|
self.label = label
|
|
self.score = score
|
|
self.box = box
|
|
self.mask = mask
|
|
self.item = item
|
|
self.width = width
|
|
self.height = height
|
|
self.args = args
|
|
|
|
def __repl__(self):
|
|
return f'cls={self.cls} label={self.label} score={self.score} box={self.box} mask={self.mask} item={self.item} size={self.width}x{self.height} args={self.args}'
|
|
|
|
|
|
class YoloRestorer(Detailer):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.models = {} # cache loaded models
|
|
self.list = {}
|
|
self.ui_mode = True
|
|
self.cmd_dir = shared.opts.yolo_dir
|
|
self.enumerate()
|
|
|
|
def name(self):
|
|
return "Detailer"
|
|
|
|
def enumerate(self):
|
|
self.list.clear()
|
|
files = []
|
|
downloaded = 0
|
|
for m in predefined:
|
|
name = os.path.splitext(os.path.basename(m))[0]
|
|
self.list[name] = m
|
|
files.append(name)
|
|
if os.path.exists(shared.opts.yolo_dir):
|
|
for f in os.listdir(shared.opts.yolo_dir):
|
|
if f.endswith('.pt'):
|
|
downloaded += 1
|
|
name = os.path.splitext(os.path.basename(f))[0]
|
|
if name not in files:
|
|
self.list[name] = os.path.join(shared.opts.yolo_dir, f)
|
|
log.info(f'Available Detailer: path="{shared.opts.yolo_dir}" items={len(list(self.list))} downloaded={downloaded}')
|
|
return list(self.list)
|
|
|
|
def dependencies(self):
|
|
from installer import install
|
|
install('ultralytics==8.3.40', ignore=True, quiet=True)
|
|
install('omegaconf')
|
|
install('antlr4-python3-runtime')
|
|
|
|
def predict(
|
|
self,
|
|
model,
|
|
image: Image.Image,
|
|
imgsz: int = 640,
|
|
half: bool = True,
|
|
device = devices.device,
|
|
agnostic: bool = False,
|
|
retina: bool = False,
|
|
mask: bool = True,
|
|
augment: bool | None = None,
|
|
offload: bool | None = None,
|
|
p = None,
|
|
) -> list[YoloResult]:
|
|
if augment is None:
|
|
augment = detailer_opt(p, 'detailer_augment')
|
|
if offload is None:
|
|
offload = shared.opts.detailer_unload
|
|
|
|
if model is None or (isinstance(model, str) and len(model) == 0):
|
|
model = 'yolo11m'
|
|
result = []
|
|
if isinstance(model, str):
|
|
cached = self.models.get(model, None)
|
|
if cached is None:
|
|
_, model = self.load(model)
|
|
else:
|
|
model = cached
|
|
if model is None:
|
|
return result
|
|
args = {
|
|
'conf': detailer_opt(p, 'detailer_conf'),
|
|
'iou': detailer_opt(p, 'detailer_iou'),
|
|
# 'max_det': detailer_opt(p, 'detailer_max'),
|
|
}
|
|
try:
|
|
if TYPE_CHECKING:
|
|
from ultralytics import YOLO # pylint: disable=import-outside-toplevel, unused-import
|
|
model: YOLO = model.to(device)
|
|
predictions = model.predict(
|
|
source=[image],
|
|
stream=False,
|
|
verbose=False,
|
|
imgsz=imgsz,
|
|
half=half,
|
|
device=device,
|
|
augment=augment,
|
|
agnostic_nms=agnostic,
|
|
retina_masks=retina,
|
|
**args
|
|
)
|
|
if offload:
|
|
model.to('cpu')
|
|
except Exception as e:
|
|
log.error(f'Detailer predict: {e}')
|
|
return result
|
|
|
|
classes = detailer_opt(p, 'detailer_classes') or ''
|
|
desired = classes.split(',')
|
|
desired = [d.lower().strip() for d in desired]
|
|
desired = [d for d in desired if len(d) > 0]
|
|
|
|
for prediction in predictions:
|
|
boxes = prediction.boxes.xyxy.detach().int().cpu().numpy() if prediction.boxes is not None else []
|
|
scores = prediction.boxes.conf.detach().float().cpu().numpy() if prediction.boxes is not None else []
|
|
classes = prediction.boxes.cls.detach().float().cpu().numpy() if prediction.boxes is not None else []
|
|
masks = prediction.masks.data.cpu().float().numpy() if prediction.masks is not None else []
|
|
if len(masks) < len(classes):
|
|
masks = len(classes) * [None]
|
|
for score, box, cls, seg in zip(scores, boxes, classes, masks, strict=False):
|
|
if seg is not None:
|
|
try:
|
|
seg = (255 * seg).astype(np.uint8)
|
|
seg = Image.fromarray(seg).resize(image.size).convert('L')
|
|
except Exception:
|
|
seg = None
|
|
cls = int(cls)
|
|
label = prediction.names[cls] if cls < len(prediction.names) else f'cls{cls}'
|
|
if len(desired) > 0 and label.lower() not in desired:
|
|
continue
|
|
box = box.tolist()
|
|
w, h = box[2] - box[0], box[3] - box[1]
|
|
x_size, y_size = w/image.width, h/image.height
|
|
opt_min = detailer_opt(p, 'detailer_min_size') or 0
|
|
opt_max = detailer_opt(p, 'detailer_max_size') or 1
|
|
min_size = opt_min if 0 <= opt_min <= 1 else 0
|
|
max_size = opt_max if 0 < opt_max <= 1 else 1
|
|
if x_size >= min_size and y_size >=min_size and x_size <= max_size and y_size <= max_size:
|
|
if mask:
|
|
if detailer_opt(p, 'detailer_segmentation') and seg is not None:
|
|
masked = seg
|
|
else:
|
|
masked = Image.new('L', image.size, 0)
|
|
draw = ImageDraw.Draw(masked)
|
|
draw.rectangle(box, fill="white", outline=None, width=0)
|
|
cropped = image.crop(box)
|
|
res = YoloResult(
|
|
cls=cls,
|
|
label=label,
|
|
score=round(score, 2),
|
|
box=box,
|
|
mask=masked,
|
|
item=cropped,
|
|
width=w,
|
|
height=h,
|
|
args=args,
|
|
)
|
|
result.append(res)
|
|
if len(result) >= (detailer_opt(p, 'detailer_max') or 2):
|
|
break
|
|
return result
|
|
|
|
def load(self, model_name: str | None = None):
|
|
with load_lock:
|
|
from modules import modelloader
|
|
model = None
|
|
if model_name is None:
|
|
model_name = list(self.list)[0]
|
|
if model_name in self.models:
|
|
return model_name, self.models[model_name]
|
|
else:
|
|
model_url = self.list.get(model_name, None)
|
|
if model_url is None:
|
|
log.error(f'Load: type=Detailer name="{model_name}" error="model not found"')
|
|
return None, None
|
|
file_name = os.path.basename(model_url)
|
|
model_file = None
|
|
try:
|
|
model_file = modelloader.load_file_from_url(url=model_url, model_dir=shared.opts.yolo_dir, file_name=file_name)
|
|
if model_file is None:
|
|
log.error(f'Load: type=Detailer name="{model_name}" url="{model_url}" error="failed to fetch model"')
|
|
elif model_file.endswith('.onnx'):
|
|
import onnxruntime as ort
|
|
options = ort.SessionOptions()
|
|
# options.graph_optimization_level = onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL
|
|
session = ort.InferenceSession(model_file, sess_options=options, providers=devices.onnx)
|
|
self.models[model_name] = session
|
|
return model_name, session
|
|
else:
|
|
self.dependencies()
|
|
import ultralytics
|
|
model = ultralytics.YOLO(model_file)
|
|
classes = list(model.names.values())
|
|
log.info(f'Load: type=Detailer name="{model_name}" model="{model_file}" ultralytics={ultralytics.__version__} classes={classes}')
|
|
self.models[model_name] = model
|
|
return model_name, model
|
|
except Exception as e:
|
|
log.error(f'Load: type=Detailer name="{model_name}" error="{e}"')
|
|
return None, None
|
|
|
|
def merge(self, items: list[YoloResult]) -> list[YoloResult]:
|
|
if items is None or len(items) == 0:
|
|
return None
|
|
box=[min(item.box[0] for item in items), min(item.box[1] for item in items), max(item.box[2] for item in items), max(item.box[3] for item in items)]
|
|
mask = Image.new('L', items[0].mask.size, 0)
|
|
for item in items:
|
|
mask = Image.fromarray(np.maximum(np.array(mask), np.array(item.mask)))
|
|
merged = YoloResult(
|
|
cls=items[0].cls,
|
|
label=items[0].label,
|
|
score=sum(item.score for item in items) / len(items),
|
|
box=box,
|
|
mask=mask,
|
|
item=None,
|
|
width=box[2] - box[0],
|
|
height=box[3] - box[1],
|
|
)
|
|
return [merged]
|
|
|
|
def draw_masks(self, image: Image.Image, items: list[YoloResult], p=None) -> Image.Image:
|
|
if not isinstance(image, Image.Image):
|
|
image = Image.fromarray(image)
|
|
image = image.convert('RGBA')
|
|
size = min(image.width, image.height) // 32
|
|
font = images.get_font(size)
|
|
color = (0, 190, 190)
|
|
log.debug(f'Detailer: draw={items}')
|
|
for i, item in enumerate(items):
|
|
if detailer_opt(p, 'detailer_segmentation') and item.mask is not None:
|
|
mask = item.mask.convert('L')
|
|
else:
|
|
mask = Image.new('L', image.size, 0)
|
|
draw_mask = ImageDraw.Draw(mask)
|
|
draw_mask.rectangle(item.box, fill="white", outline=None, width=0)
|
|
alpha = mask.point(lambda p: int(p * 0.5))
|
|
overlay = Image.new("RGBA", image.size, color + (0,))
|
|
overlay.putalpha(alpha)
|
|
image = Image.alpha_composite(image, overlay)
|
|
|
|
draw_text = ImageDraw.Draw(image)
|
|
draw_text.text((item.box[0] + 2, item.box[1] - size - 2), f'{i+1} {item.label} {item.score:.2f}', fill="black", font=font)
|
|
draw_text.text((item.box[0] + 0, item.box[1] - size - 4), f'{i+1} {item.label} {item.score:.2f}', fill="white", font=font)
|
|
image = image.convert("RGB")
|
|
return np.array(image)
|
|
|
|
def restore(self, np_image, p: processing.StableDiffusionProcessing = None):
|
|
if shared.state.interrupted or shared.state.skipped:
|
|
return np_image
|
|
if hasattr(p, 'recursion'):
|
|
return np_image
|
|
if not hasattr(p, 'detailer_active'):
|
|
p.detailer_active = 0
|
|
if np_image is None or p.detailer_active >= p.batch_size * p.n_iter:
|
|
return np_image
|
|
|
|
shared.sd_model = sd_models.set_diffuser_pipe(shared.sd_model, sd_models.DiffusersTaskType.INPAINTING)
|
|
if (sd_models.get_diffusers_task(shared.sd_model) != sd_models.DiffusersTaskType.INPAINTING) and (shared.sd_model.__class__.__name__ not in sd_models.pipe_switch_task_exclude):
|
|
log.error(f'Detailer: model="{shared.sd_model.__class__.__name__}" not compatible')
|
|
return np_image
|
|
|
|
models = []
|
|
if len(shared.opts.detailer_args) > 0:
|
|
models = [m.strip() for m in re.split(r'[\n,;]+', shared.opts.detailer_args)]
|
|
models = [m for m in models if len(m) > 0]
|
|
if len(models) == 0:
|
|
models = detailer_opt(p, 'detailer_models') or []
|
|
if len(models) == 0:
|
|
log.warning('Detailer: model=None')
|
|
return np_image
|
|
log.debug(f'Detailer: models={models}')
|
|
|
|
# create backups
|
|
orig_apply_overlay = shared.opts.mask_apply_overlay
|
|
orig_p = p.__dict__.copy()
|
|
orig_cls = p.__class__
|
|
models_used = []
|
|
np_images = []
|
|
annotated = Image.fromarray(np_image)
|
|
image = None
|
|
|
|
for i, model_val in enumerate(models):
|
|
if ':' in model_val:
|
|
model_name, model_args = model_val.split(':', 1)
|
|
else:
|
|
model_name, model_args = model_val, ''
|
|
model_args = [m.strip() for m in model_args.split(':')]
|
|
model_args = {k.strip(): v.strip() for k, v in (arg.split('=') for arg in model_args if '=' in arg)}
|
|
|
|
name, model = self.load(model_name)
|
|
if model is None:
|
|
log.warning(f'Detailer: model="{name}" not loaded')
|
|
continue
|
|
|
|
if image is None:
|
|
image = Image.fromarray(np_image)
|
|
items = self.predict(model, image, p=p)
|
|
|
|
if len(items) == 0:
|
|
log.info(f'Detailer: model="{name}" no items detected')
|
|
continue
|
|
|
|
if detailer_opt(p, 'detailer_merge') and len(items) > 1:
|
|
log.debug(f'Detailer: model="{name}" items={len(items)} merge')
|
|
items = self.merge(items)
|
|
|
|
shared.opts.data['mask_apply_overlay'] = True
|
|
orig_prompt: str = orig_p.get('all_prompts', [''])[0]
|
|
orig_negative: str = orig_p.get('all_negative_prompts', [''])[0]
|
|
prompt: str = orig_p.get('detailer_prompt', '')
|
|
negative: str = orig_p.get('detailer_negative', '')
|
|
if prompt is None or len(prompt) == 0:
|
|
prompt = orig_prompt
|
|
else:
|
|
prompt = prompt.replace('[PROMPT]', orig_prompt)
|
|
prompt = prompt.replace('[prompt]', orig_prompt)
|
|
if len(negative) == 0:
|
|
negative = orig_negative
|
|
else:
|
|
negative = negative.replace('[PROMPT]', orig_negative)
|
|
negative = negative.replace('[prompt]', orig_negative)
|
|
prompt_lines = 99 * [p.strip() for p in prompt.split('\n')]
|
|
negative_lines = 99 * [n.strip() for n in negative.split('\n')]
|
|
|
|
args = {
|
|
'detailer': True,
|
|
'batch_size': 1,
|
|
'n_iter': 1,
|
|
'prompt': prompt,
|
|
'negative_prompt': negative,
|
|
'denoising_strength': p.detailer_strength,
|
|
'sampler_name': orig_p.get('hr_sampler_name', 'default'),
|
|
'steps': p.detailer_steps,
|
|
'styles': [],
|
|
'inpaint_full_res': True,
|
|
'inpainting_mask_invert': 0,
|
|
'mask_blur': detailer_opt(p, 'detailer_blur'),
|
|
'inpaint_full_res_padding': detailer_opt(p, 'detailer_padding'),
|
|
'width': p.detailer_resolution,
|
|
'height': p.detailer_resolution,
|
|
'vae_type': orig_p.get('vae_type', 'Full'),
|
|
}
|
|
args.update(model_args)
|
|
if args['denoising_strength'] == 0:
|
|
log.debug(f'Detailer: model="{name}" strength=0 skip')
|
|
return np_image
|
|
control_pipeline = None
|
|
orig_class = shared.sd_model.__class__
|
|
if getattr(p, 'is_control', False):
|
|
from modules.control import run
|
|
control_pipeline = shared.sd_model
|
|
run.restore_pipeline()
|
|
|
|
p = processing_class.switch_class(p, processing.StableDiffusionProcessingImg2Img, args)
|
|
if hasattr(shared.sd_model, 'restore_pipeline'):
|
|
shared.sd_model.restore_pipeline()
|
|
p.detailer_active += 1 # set flag to avoid recursion
|
|
|
|
if p.steps < 1:
|
|
p.steps = orig_p.get('steps', 0)
|
|
|
|
# report = [{'label': i.label, 'score': i.score, 'size': f'{i.width}x{i.height}' } for i in items]
|
|
# log.info(f'Detailer: model="{name}" items={report} args={args}')
|
|
models_used.append(name)
|
|
|
|
mask_all = []
|
|
p.state = ''
|
|
pc = copy(p)
|
|
pc.ops.append('detailer')
|
|
|
|
orig_sigma_adjust: float = shared.opts.schedulers_sigma_adjust
|
|
orig_sigma_end: float = shared.opts.schedulers_sigma_adjust_max
|
|
shared.opts.schedulers_sigma_adjust = detailer_opt(p, 'detailer_sigma_adjust')
|
|
shared.opts.schedulers_sigma_adjust_max = detailer_opt(p, 'detailer_sigma_adjust_max')
|
|
|
|
if detailer_opt(p, 'detailer_sort'):
|
|
items = sorted(items, key=lambda x: x.box[0]) # sort items left-to-right to improve consistency
|
|
if detailer_opt(p, 'detailer_include_detections', 'detailer_save'):
|
|
annotated = self.draw_masks(annotated, items, p=p)
|
|
|
|
for j, item in enumerate(items):
|
|
if item.mask is None:
|
|
continue
|
|
pc.keep_prompts = True
|
|
shared.sd_model.fail_on_switch_error = True
|
|
pc.prompt = prompt_lines[i*len(items)+j]
|
|
pc.negative_prompt = negative_lines[i*len(items)+j]
|
|
pc.prompts = [pc.prompt]
|
|
pc.negative_prompts = [pc.negative_prompt]
|
|
pc.prompts, pc.network_data = extra_networks.parse_prompts(pc.prompts, pc.network_data)
|
|
pc.disable_extra_networks = True # disable processing_diffusers from handling network activation since its handled here
|
|
network_same = len(p.network_data.values()) == len(pc.network_data.values()) and all(x == y for x, y in zip(p.network_data.values(), pc.network_data.values()))
|
|
if not network_same:
|
|
extra_networks.activate(pc, pc.network_data)
|
|
log.debug(f'Detail: model="{i+1}:{name}" item={j+1}/{len(items)} box={item.box} label="{item.label}" score={item.score:.2f} seg={detailer_opt(p, "detailer_segmentation")} network={network_same} prompt="{pc.prompt}"')
|
|
pc.init_images = [image]
|
|
pc.image_mask = [item.mask]
|
|
pc.overlay_images = []
|
|
# explicitly disable for detailer pass
|
|
pc.enable_hr = False
|
|
pc.do_not_save_samples = True
|
|
pc.do_not_save_grid = True
|
|
# set recursion flag to avoid nested detailer calls
|
|
pc.recursion = True
|
|
|
|
# process
|
|
jobid = shared.state.begin('Detailer')
|
|
pp = processing.process_images_inner(pc)
|
|
if not network_same:
|
|
extra_networks.deactivate(pc, force=True)
|
|
shared.sd_model.fail_on_switch_error = False
|
|
shared.state.end(jobid)
|
|
|
|
del pc.recursion
|
|
if (pp is not None) and (pp.images is not None) and (len(pp.images) > 0):
|
|
image = pp.images[0] # update image to be reused for next item
|
|
if len(pp.images) > 1:
|
|
mask_all.append(pp.images[1])
|
|
|
|
shared.opts.schedulers_sigma_adjust = orig_sigma_adjust
|
|
shared.opts.schedulers_sigma_adjust_max = orig_sigma_end
|
|
|
|
# restore pipeline
|
|
if control_pipeline is not None:
|
|
shared.sd_model = control_pipeline
|
|
else:
|
|
shared.sd_model.__class__ = orig_class
|
|
p = processing_class.switch_class(p, orig_cls, orig_p)
|
|
p.init_images = orig_p.get('init_images', None)
|
|
p.image_mask = orig_p.get('image_mask', None)
|
|
p.state = orig_p.get('state', None)
|
|
p.ops = orig_p.get('ops', [])
|
|
shared.opts.data['mask_apply_overlay'] = orig_apply_overlay
|
|
|
|
if len(mask_all) > 0 and shared.opts.include_mask:
|
|
from modules.control.util import blend
|
|
p.image_mask = blend([np.array(m) for m in mask_all])
|
|
p.image_mask = Image.fromarray(p.image_mask)
|
|
|
|
if image is not None:
|
|
np_images.append(np.array(image))
|
|
if detailer_opt(p, 'detailer_include_detections', 'detailer_save') and annotated is not None:
|
|
np_images.append(annotated) # save debug image with boxes
|
|
return np_images
|
|
|
|
def make_processing(self, image, prompt='', negative='', steps=10, strength=0.3, resolution=1024, seed=-1, overrides=None):
|
|
"""Build a synthetic Img2Img processing object to run restore() standalone, with no base generation pass.
|
|
|
|
The primary params map to the detailer_* fields restore() reads directly. overrides is an optional
|
|
dict of the remaining detailer_* settings; None values are skipped and fall through to shared.opts
|
|
via detailer_opt(). The seed is resolved here so restore()'s inpaint passes are reproducible and the
|
|
effective value can be reported back.
|
|
"""
|
|
from modules.processing_helpers import get_fixed_seed
|
|
from modules.paths import resolve_output_path
|
|
seed = int(get_fixed_seed(seed))
|
|
outpath = resolve_output_path(shared.opts.outdir_samples, shared.opts.outdir_extras_samples)
|
|
p = processing.StableDiffusionProcessingImg2Img(
|
|
sd_model=shared.sd_model,
|
|
prompt=prompt or '',
|
|
negative_prompt=negative or '',
|
|
init_images=[image],
|
|
outpath_samples=outpath,
|
|
outpath_grids=outpath,
|
|
batch_size=1,
|
|
n_iter=1,
|
|
seed=seed,
|
|
width=image.width,
|
|
height=image.height,
|
|
detailer_enabled=True,
|
|
detailer_prompt=prompt or '',
|
|
detailer_negative=negative or '',
|
|
detailer_steps=steps,
|
|
detailer_strength=strength,
|
|
detailer_resolution=resolution,
|
|
)
|
|
for attr, val in (overrides or {}).items():
|
|
if val is not None:
|
|
setattr(p, attr, val)
|
|
# restore() at yolo.py reads all_prompts[0]/all_negative_prompts[0]; the rest avoid AttributeError downstream
|
|
p.all_prompts = [p.detailer_prompt or '']
|
|
p.all_negative_prompts = [p.detailer_negative or '']
|
|
p.all_seeds = [seed]
|
|
p.all_subseeds = [-1]
|
|
p.scripts = None
|
|
p.is_control = False
|
|
p.do_not_save_samples = True
|
|
p.do_not_save_grid = True
|
|
return p
|
|
|
|
def change_mode(self, dropdown, text):
|
|
self.ui_mode = not self.ui_mode
|
|
if self.ui_mode:
|
|
value = [val.split(':', 1)[0].strip() for val in text.split(',') if val.strip()]
|
|
return gr.update(visible=True, value=value), gr.update(visible=False), gr.update(visible=True)
|
|
else:
|
|
value = ', '.join(dropdown)
|
|
return gr.update(visible=False), gr.update(visible=True, value=value), gr.update(visible=False)
|
|
|
|
def ui(self, tab: str):
|
|
def ui_settings_change(merge, detailers, text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg):
|
|
shared.opts.detailer_merge = merge
|
|
shared.opts.detailer_models = detailers
|
|
shared.opts.detailer_args = text if not self.ui_mode else ''
|
|
shared.opts.detailer_classes = classes
|
|
shared.opts.detailer_padding = padding
|
|
shared.opts.detailer_blur = blur
|
|
shared.opts.detailer_conf = min_confidence
|
|
shared.opts.detailer_max = max_detected
|
|
shared.opts.detailer_min_size = min_size
|
|
shared.opts.detailer_max_size = max_size
|
|
shared.opts.detailer_iou = iou
|
|
shared.opts.detailer_sigma_adjust = renoise_value
|
|
shared.opts.detailer_sigma_adjust_max = renoise_end
|
|
shared.opts.detailer_save = save
|
|
shared.opts.detailer_sort = sort
|
|
shared.opts.detailer_segmentation = seg
|
|
# shared.opts.detailer_resolution = resolution
|
|
shared.opts.save(silent=True)
|
|
log.debug(f'Detailer settings: models={detailers} classes={classes} strength={strength} conf={min_confidence} max={max_detected} iou={iou} size={min_size}-{max_size} padding={padding} steps={steps} resolution={resolution} save={save} sort={sort} seg={seg}')
|
|
if not self.ui_mode:
|
|
log.debug(f'Detailer expert: {text}')
|
|
|
|
with gr.Accordion(open=False, label="Detailer", elem_id=f"{tab}_detailer_accordion", elem_classes=["small-accordion"]):
|
|
with gr.Row():
|
|
enabled = gr.Checkbox(label="Enable detailer pass", elem_id=f"{tab}_detailer_enabled", value=False)
|
|
with gr.Row():
|
|
seg = gr.Checkbox(label="Use segmentation", elem_id=f"{tab}_detailer_seg", value=shared.opts.detailer_segmentation, visible=True)
|
|
save = gr.Checkbox(label="Include detections", elem_id=f"{tab}_detailer_save", value=shared.opts.detailer_save, visible=True)
|
|
with gr.Row():
|
|
merge = gr.Checkbox(label="Merge detailers", elem_id=f"{tab}_detailer_merge", value=shared.opts.detailer_merge, visible=True)
|
|
sort = gr.Checkbox(label="Sort detections", elem_id=f"{tab}_detailer_sort", value=shared.opts.detailer_sort, visible=True)
|
|
with gr.Row():
|
|
detailers = gr.Dropdown(label="Detailer models", elem_id=f"{tab}_detailers", choices=list(self.list), value=shared.opts.detailer_models, multiselect=True, visible=True)
|
|
detailers_text = gr.Textbox(label="Detailer list", elem_id=f"{tab}_detailers_text", placeholder="Comma separated list of detailer models", lines=2, visible=False, interactive=True)
|
|
refresh_btn = ui_common.create_refresh_button(detailers, self.enumerate, lambda: {"choices": self.enumerate()}, 'yolo_models_refresh')
|
|
ui_mode = ui_components.ToolButton(value=ui_symbols.view, elem_id=f'{tab}_yolo_models_list')
|
|
ui_mode.click(fn=self.change_mode, inputs=[detailers, detailers_text], outputs=[detailers, detailers_text, refresh_btn])
|
|
with gr.Row():
|
|
classes = gr.Textbox(label="Detailer classes", placeholder="Classes", elem_id=f"{tab}_detailer_classes")
|
|
if tab == 'extras': # Process tab is standalone, there is no base prompt to fall back to
|
|
prompt_placeholder = 'detailer prompt, leave empty for none'
|
|
negative_placeholder = 'detailer negative prompt, leave empty for none'
|
|
else:
|
|
prompt_placeholder = 'detailer prompt or leave empty to use main prompt'
|
|
negative_placeholder = 'detailer prompt or leave empty to use main prompt'
|
|
with gr.Row():
|
|
prompt = gr.Textbox(label="Detailer prompt", value='', placeholder=prompt_placeholder, lines=2, elem_id=f"{tab}_detailer_prompt", elem_classes=["prompt"])
|
|
with gr.Row():
|
|
negative = gr.Textbox(label="Detailer negative prompt", value='', placeholder=negative_placeholder, lines=2, elem_id=f"{tab}_detailer_negative", elem_classes=["prompt"])
|
|
with gr.Row():
|
|
steps = gr.Slider(label="Detailer steps", elem_id=f"{tab}_detailer_steps", value=10, minimum=0, maximum=99, step=1)
|
|
strength = gr.Slider(label="Detailer strength", elem_id=f"{tab}_detailer_strength", value=0.3, minimum=0, maximum=1, step=0.01)
|
|
with gr.Row():
|
|
resolution = gr.Slider(label="Detailer resolution", elem_id=f"{tab}_detailer_resolution", value=1024, minimum=256, maximum=4096, step=8)
|
|
max_detected = gr.Slider(label="Max detected", elem_id=f"{tab}_detailer_max", value=shared.opts.detailer_max, minimum=1, maximum=10, step=1)
|
|
with gr.Row():
|
|
padding = gr.Slider(label="Edge padding", elem_id=f"{tab}_detailer_padding", value=shared.opts.detailer_padding, minimum=0, maximum=100, step=1)
|
|
blur = gr.Slider(label="Edge blur", elem_id=f"{tab}_detailer_blur", value=shared.opts.detailer_blur, minimum=0, maximum=100, step=1)
|
|
with gr.Row():
|
|
min_confidence = gr.Slider(label="Min confidence", elem_id=f"{tab}_detailer_conf", value=shared.opts.detailer_conf, minimum=0.0, maximum=1.0, step=0.05)
|
|
iou = gr.Slider(label="Max overlap", elem_id=f"{tab}_detailer_iou", value=shared.opts.detailer_iou, minimum=0, maximum=1.0, step=0.05)
|
|
with gr.Row():
|
|
min_size = shared.opts.detailer_min_size if shared.opts.detailer_min_size < 1 else 0.0
|
|
min_size = gr.Slider(label="Min size", elem_id=f"{tab}_detailer_min_size", value=min_size, minimum=0.0, maximum=1.0, step=0.05)
|
|
max_size = shared.opts.detailer_max_size if shared.opts.detailer_max_size < 1 and shared.opts.detailer_max_size > 0 else 1.0
|
|
max_size = gr.Slider(label="Max size", elem_id=f"{tab}_detailer_max_size", value=max_size, minimum=0.0, maximum=1.0, step=0.05)
|
|
with gr.Row(elem_classes=['flex-break']):
|
|
renoise_value = gr.Slider(minimum=0.5, maximum=1.5, step=0.01, label='Renoise', value=shared.opts.detailer_sigma_adjust, elem_id=f"{tab}_detailer_renoise")
|
|
renoise_end = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label='Renoise end', value=shared.opts.detailer_sigma_adjust_max, elem_id=f"{tab}_detailer_renoise_end")
|
|
sampler_block = None
|
|
if tab == 'extras': # fold the standalone sampler settings into the detailer accordion; values applied per-job in make_processing, never global opts
|
|
from modules import sd_samplers
|
|
sd_samplers.set_samplers()
|
|
sampler_choices = [s.name for s in sd_samplers.visible_samplers() if s.name != 'Same as primary']
|
|
with gr.Accordion('Sampler', open=False, elem_id=f"{tab}_detailer_sampler_accordion", elem_classes=["small-accordion"]):
|
|
with gr.Row():
|
|
d_sampler = gr.Dropdown(label='Sampling method', choices=sampler_choices, value='Default', elem_id=f"{tab}_detailer_sampler")
|
|
d_prediction = gr.Dropdown(label='Prediction method', choices=['default', 'epsilon', 'sample', 'v_prediction', 'flow_prediction'], value='default', elem_id=f"{tab}_detailer_prediction")
|
|
with gr.Row():
|
|
d_shift = gr.Slider(label='Flow shift', minimum=0, maximum=10, step=0.1, value=shared.opts.schedulers_shift, elem_id=f"{tab}_detailer_shift")
|
|
d_cfg = gr.Slider(label='Guidance scale', minimum=0, maximum=30, step=0.1, value=6.0, elem_id=f"{tab}_detailer_cfg")
|
|
with gr.Row():
|
|
d_options = gr.CheckboxGroup(label='Options', choices=['low order', 'thresholding', 'dynamic', 'rescale'], value=['low order'], elem_id=f"{tab}_detailer_options")
|
|
with gr.Row():
|
|
d_seed = gr.Number(label='Seed', value=-1, precision=0, elem_id=f"{tab}_detailer_seed")
|
|
sampler_block = {'sampler': d_sampler, 'prediction': d_prediction, 'shift': d_shift, 'cfg_scale': d_cfg, 'options': d_options, 'seed': d_seed}
|
|
|
|
merge.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[])
|
|
detailers.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[])
|
|
detailers_text.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[])
|
|
classes.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[])
|
|
padding.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[])
|
|
blur.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[])
|
|
min_confidence.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[])
|
|
max_detected.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[])
|
|
min_size.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[])
|
|
max_size.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[])
|
|
iou.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[])
|
|
resolution.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[])
|
|
save.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[])
|
|
sort.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[])
|
|
seg.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[])
|
|
if tab == 'extras':
|
|
return enabled, prompt, negative, steps, strength, resolution, sampler_block
|
|
return enabled, prompt, negative, steps, strength, resolution
|
|
|
|
|
|
def initialize():
|
|
shared.yolo = YoloRestorer()
|
|
shared.detailers.append(shared.yolo)
|