From 2c632bb562b8d40916b18e68ffd056f0925d5279 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 4 Jul 2026 11:17:56 +0200 Subject: [PATCH] refactor detailer Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 10 +- modules/detailer/__init__.py | 8 ++ modules/{ => detailer}/detailer.py | 224 +++-------------------------- modules/detailer/helper.py | 52 +++++++ modules/detailer/locateanything.py | 65 +++++++++ modules/detailer/models.py | 13 ++ modules/detailer/yolo.py | 170 ++++++++++++++++++++++ modules/ipadapter.py | 3 +- modules/processing.py | 4 +- modules/shared.py | 4 +- modules/ui_definitions.py | 1 - 11 files changed, 339 insertions(+), 215 deletions(-) create mode 100644 modules/detailer/__init__.py rename modules/{ => detailer}/detailer.py (74%) create mode 100644 modules/detailer/helper.py create mode 100644 modules/detailer/locateanything.py create mode 100644 modules/detailer/models.py create mode 100644 modules/detailer/yolo.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 74a408b51..f369be518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Change Log for SD.Next -## Update for 2026-07-03 +## Update for 2026-07-04 -### Highlights for 2026-07-03 +### Highlights for 2026-07-04 Service-pack update with number of fixes and quality-of-life improvements Plus few new models: **Krea 2**, **Photoroom PRXPixel**, **FLUX.2 Klein 9B KV** and some new community models @@ -11,7 +11,7 @@ Also couple of *experimental* features: see below for details... [Home](https://vladmandic.github.io/sdnext/) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) | [Sponsor](https://github.com/sponsors/vladmandic) -### Details for 2026-07-03 +### Details for 2026-07-04 - **Models** - [Krea 2](https://www.krea.ai/blog/krea-2-image-model) in *base* and *turbo* (distilled) variants @@ -38,6 +38,10 @@ Also couple of *experimental* features: see below for details... - prompt encode caching for pass-through text-encoders - reference models: validate and update info for all reference models add size preview before download for all reference models + - **CivitAI** download improvements + auto-select download path + improved search + validate downloads - **UI** - dynamic visibility of image controls - improve main panel positioning: *portrait/landscape* diff --git a/modules/detailer/__init__.py b/modules/detailer/__init__.py new file mode 100644 index 000000000..2b6901196 --- /dev/null +++ b/modules/detailer/__init__.py @@ -0,0 +1,8 @@ +from .models import detailer_models +from .helper import detailer_opt, DetailerResult, list_models +from .detailer import Detailer + + +def initialize(): + from modules import shared + shared.detailer = Detailer() diff --git a/modules/detailer.py b/modules/detailer/detailer.py similarity index 74% rename from modules/detailer.py rename to modules/detailer/detailer.py index 5588e2505..c7a6ef7ac 100644 --- a/modules/detailer.py +++ b/modules/detailer/detailer.py @@ -1,60 +1,17 @@ -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 - - -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://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 DetailerResult: - 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}' +from modules.detailer import DetailerResult, detailer_opt class Detailer(): def __init__(self): super().__init__() + self.model_name = None self.models = {} # cache loaded models self.list = {} self.ui_mode = True @@ -64,32 +21,9 @@ class Detailer(): 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.4.67', ignore=True, quiet=True) - install('omegaconf') - install('antlr4-python3-runtime') - def predict( self, + name: str, model, image: Image.Image, imgsz: int = 640, @@ -102,141 +36,24 @@ class Detailer(): offload: bool | None = None, p = None, ) -> list[DetailerResult]: - if augment is None: - augment = detailer_opt(p, 'detailer_augment') - if offload is None: - offload = shared.opts.detailer_unload + if 'LocateAnything' in name: + from modules.detailer import locateanything + return locateanything.predict(self, model, image, device=device, mask=mask, offload=offload, p=p) - 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 + from modules.detailer import yolo + return yolo.predict(self, model, image, imgsz=imgsz, half=half, device=device, agnostic=agnostic, retina=retina, mask=mask, augment=augment, offload=offload, p=p) - 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 = DetailerResult( - 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 enumerate(self): + from modules.detailer import list_models + return list_models(self) 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 + if 'LocateAnything' in model_name: + from modules.detailer import locateanything + return locateanything.load(model_name=model_name) + + from modules.detailer import yolo + return yolo.load(self, model_name=model_name) def merge(self, items: list[DetailerResult]) -> list[DetailerResult]: if items is None or len(items) == 0: @@ -333,7 +150,7 @@ class Detailer(): if image is None: image = Image.fromarray(np_image) - items = self.predict(model, image, p=p) + items = self.predict(name, model, image, p=p) if len(items) == 0: log.info(f'Detailer: model="{name}" no items detected') @@ -643,8 +460,3 @@ class Detailer(): if tab == 'extras': return enabled, prompt, negative, steps, strength, resolution, sampler_block return enabled, prompt, negative, steps, strength, resolution - - -def initialize(): - shared.detailer = Detailer() - # shared.detailers.append(shared.detailer) diff --git a/modules/detailer/helper.py b/modules/detailer/helper.py new file mode 100644 index 000000000..70bfae236 --- /dev/null +++ b/modules/detailer/helper.py @@ -0,0 +1,52 @@ +import os +from PIL import Image +from modules.logger import log + + +def list_models(self): + from modules.detailer import detailer_models + from modules import shared + self.list.clear() + files = [] + downloaded = 0 + for m in detailer_models: + 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 detailer_opt(p, attr, opts_attr=None): + """Read detailer param from processing object if set, otherwise fall back to shared.opts.""" + from modules import shared + 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) + + +class DetailerResult: + 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 __str__(self): + return f'DetailerResult(cls={self.cls} label={self.label} score={self.score:.2f} box={self.box} size={self.width}x{self.height} args={self.args})' diff --git a/modules/detailer/locateanything.py b/modules/detailer/locateanything.py new file mode 100644 index 000000000..6fb911197 --- /dev/null +++ b/modules/detailer/locateanything.py @@ -0,0 +1,65 @@ +from PIL import Image +from modules import shared, devices +from modules.detailer import DetailerResult, detailer_opt +from modules.logger import log + + +repo_id = 'nvidia/LocateAnything-3B' +processor = None +tokenizer = None + + +def dependencies(): + from installer import install + install('decord') + + +def load(model_name: str | None = None): + import transformers + global tokenizer, processor # pylint: disable=global-statement + load_kwargs = { + 'pretrained_model_name_or_path': repo_id, + 'cache_dir': shared.opts.hfcache_dir, + 'trust_remote_code': True, + } + dependencies() + tokenizer = transformers.AutoTokenizer.from_pretrained(**load_kwargs) + processor = transformers.AutoProcessor.from_pretrained(**load_kwargs) + model = transformers.AutoModel.from_pretrained(**load_kwargs, torch_dtype=devices.dtype) + model = model.to(devices.device).eval() + if shared.opts.detailer_unload: + model.to(devices.cpu) + log.info(f'Detailer model="{model_name}" cls={model.__class__.__name__} loaded') + return model_name, model + + +def predict( + self, + model, + image: Image.Image, + device = devices.device, + mask: bool = True, + offload: bool | None = None, + p = None, + ) -> list[DetailerResult]: + if offload is None: + offload = shared.opts.detailer_unload + log.info(f'Detailer cls="{model.__class__.__name__}" image={image} device={device} mask={mask} offload={offload}') + 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 + model = model.to(device) + prompt = detailer_opt(p, 'detailer_classes') or '' + log.debug(f'Detailer prompt="{prompt}"') + + # TODO locateanything: implement when compatible with transformers=5 + + if offload: + model.to(devices.cpu) + return result diff --git a/modules/detailer/models.py b/modules/detailer/models.py new file mode 100644 index 000000000..678ee77ee --- /dev/null +++ b/modules/detailer/models.py @@ -0,0 +1,13 @@ +detailer_models = [ # + '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', + # 'nvidia-LocateAnything-3B', +] diff --git a/modules/detailer/yolo.py b/modules/detailer/yolo.py new file mode 100644 index 000000000..45fa26fb4 --- /dev/null +++ b/modules/detailer/yolo.py @@ -0,0 +1,170 @@ +from typing import TYPE_CHECKING +import os +import threading +import numpy as np +from PIL import Image, ImageDraw +from modules.detailer import DetailerResult, detailer_opt +from modules.logger import log +from modules import shared, devices + + +load_lock = threading.Lock() + + +def dependencies(): + from installer import install + install('ultralytics==8.4.67', ignore=True, quiet=True) + install('omegaconf') + install('antlr4-python3-runtime') + + +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: + 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 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[DetailerResult]: + 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 = DetailerResult( + 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 diff --git a/modules/ipadapter.py b/modules/ipadapter.py index ec3020026..67469c453 100644 --- a/modules/ipadapter.py +++ b/modules/ipadapter.py @@ -127,9 +127,10 @@ def crop_images(images, crops): if crops[i]: cropped = [] for image in images[i]: - faces = shared.detailer.predict('face-yolo8n', image) + faces = shared.detailer.predict('face-yolo8n', 'face-yolo8n', image) if len(faces) > 0: cropped.append(faces[0].item) + log.debug(f'IP adapter crop: {faces[0]}') if len(cropped) == len(images[i]): images[i] = cropped else: diff --git a/modules/processing.py b/modules/processing.py index 6ba888f62..f25bdf512 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -320,13 +320,13 @@ def process_samples(p: StableDiffusionProcessing, samples): sample = shared.detailer.restore(sample, p) if isinstance(sample, list): if len(sample) > 0: - image = Image.fromarray(sample[0]) + image = sample[0] if isinstance(sample[0], Image.Image) else Image.fromarray(sample[0]) if len(sample) > 1: annotated = sample[1] if isinstance(sample[1], Image.Image) else Image.fromarray(sample[1]) out_images.append(annotated) out_infotexts.append("Detailer annotations") elif sample is not None: - image = Image.fromarray(sample) + image = sample if isinstance(sample, Image.Image) else Image.fromarray(sample) if p.color_corrections is not None and i < len(p.color_corrections): p.ops.append('color') diff --git a/modules/shared.py b/modules/shared.py index 59f149dd5..e9054160c 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -30,6 +30,7 @@ if TYPE_CHECKING: from diffusers import DiffusionPipeline from modules.shared_legacy import LegacyOption from modules.ui_extra_networks import ExtraNetworksPage + from modules.detailer import Detailer class Backend(Enum): @@ -48,8 +49,7 @@ listfiles = listdir xformers_available = False compiled_model_state = None sd_upscalers = [] -detailers = [] -detailer = None +detailer: Detailer | None = None tab_names = [] extra_networks: list[ExtraNetworksPage] = [] hypernetworks = {} diff --git a/modules/ui_definitions.py b/modules/ui_definitions.py index 00284ec26..7d0ac016e 100644 --- a/modules/ui_definitions.py +++ b/modules/ui_definitions.py @@ -800,7 +800,6 @@ def create_settings(cmd_opts): "uni_pc_variant": OptionInfo("bh2", "UniPC variant", gr.Radio, {"choices": ["bh1", "bh2", "vary_coeff"], "visible": False}), "uni_pc_skip_type": OptionInfo("time_uniform", "UniPC skip type", gr.Radio, {"choices": ["time_uniform", "time_quadratic", "logSNR"], "visible": False}), # detailer settings are handled separately - # "detailer_model": OptionInfo("Detailer", "Detailer model", gr.Radio, lambda: {"choices": [x.name() for x in shared.detailers], "visible": False}), "detailer_classes": OptionInfo("", "Detailer classes", gr.Textbox, {"visible": False}), "detailer_conf": OptionInfo(0.6, "Min confidence", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.05, "visible": False}), "detailer_max": OptionInfo(2, "Max detected", gr.Slider, {"minimum": 1, "maximum": 10, "step": 1, "visible": False}),