mirror of
https://github.com/vladmandic/automatic
synced 2026-09-18 16:54:33 +02:00
Merge pull request #5035 from kirtasshh/claude/sdnext-detailer-class-prompts-sunhax
Detailer: per-class prompts via [CLASS=name] tags
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
from .models import detailer_models
|
||||
from .helper import detailer_opt, DetailerResult, list_models
|
||||
from .helper import detailer_opt, DetailerResult, list_models, assign_prompts, parse_prompt_lines
|
||||
from .detailer import Detailer
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 DetailerResult, detailer_opt
|
||||
from modules.detailer import DetailerResult, detailer_opt, assign_prompts, parse_prompt_lines
|
||||
|
||||
|
||||
class Detailer():
|
||||
@@ -135,6 +135,30 @@ class Detailer():
|
||||
annotated = Image.fromarray(np_image)
|
||||
image = None
|
||||
|
||||
# detailer_prompt/negative are the same for every model in the chain, so resolve them once
|
||||
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)
|
||||
|
||||
# track which '[CLASS=name]' tags get matched by any model in the chain, to warn on genuine typos only
|
||||
prompt_classes, _ = parse_prompt_lines(prompt)
|
||||
negative_classes, _ = parse_prompt_lines(negative)
|
||||
prompt_classes = set(prompt_classes.keys())
|
||||
negative_classes = set(negative_classes.keys())
|
||||
matched_prompt_classes = set()
|
||||
matched_negative_classes = set()
|
||||
|
||||
for i, model_val in enumerate(models):
|
||||
if ':' in model_val:
|
||||
model_name, model_args = model_val.split(':', 1)
|
||||
@@ -161,22 +185,6 @@ class Detailer():
|
||||
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,
|
||||
@@ -234,13 +242,18 @@ class Detailer():
|
||||
if detailer_opt(p, 'detailer_include_detections', 'detailer_save'):
|
||||
annotated = self.draw_masks(annotated, items, p=p)
|
||||
|
||||
labels_this_pass = {(item.label or '').strip().lower() for item in items}
|
||||
matched_prompt_classes |= (prompt_classes & labels_this_pass)
|
||||
matched_negative_classes |= (negative_classes & labels_this_pass)
|
||||
resolved_prompts = assign_prompts(prompt, items)
|
||||
resolved_negatives = assign_prompts(negative, items)
|
||||
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.prompt = resolved_prompts[j]
|
||||
pc.negative_prompt = resolved_negatives[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)
|
||||
@@ -293,6 +306,13 @@ class Detailer():
|
||||
p.image_mask = blend([np.array(m) for m in mask_all])
|
||||
p.image_mask = Image.fromarray(p.image_mask)
|
||||
|
||||
unmatched_prompt = prompt_classes - matched_prompt_classes
|
||||
if len(unmatched_prompt) > 0:
|
||||
log.warning(f'Detailer prompt: class tags did not match any detection across models={models}: unmatched={sorted(unmatched_prompt)}')
|
||||
unmatched_negative = negative_classes - matched_negative_classes
|
||||
if len(unmatched_negative) > 0:
|
||||
log.warning(f'Detailer negative: class tags did not match any detection across models={models}: unmatched={sorted(unmatched_negative)}')
|
||||
|
||||
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:
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import os
|
||||
import re
|
||||
from PIL import Image
|
||||
from modules.logger import log
|
||||
|
||||
|
||||
class_tag_re = re.compile(r'^\[class\s*=\s*([^\]]+)\]\s*(.*)$', re.IGNORECASE)
|
||||
|
||||
|
||||
def list_models(self):
|
||||
from modules.detailer import detailer_models
|
||||
from modules import shared
|
||||
@@ -34,6 +38,53 @@ def detailer_opt(p, attr, opts_attr=None):
|
||||
return getattr(shared.opts, opts_attr or attr, None)
|
||||
|
||||
|
||||
def parse_prompt_lines(text: str):
|
||||
"""Split a detailer prompt into class-tagged templates and positional fallback lines.
|
||||
|
||||
A line starting with '[CLASS=name]' or '[CLASS=name1,name2]' assigns its text to every
|
||||
detection whose label matches one of the given class names (case-insensitive). All
|
||||
other non-empty lines are kept, in order, as the legacy positional fallback used for
|
||||
detections that don't match any class tag.
|
||||
"""
|
||||
class_map: dict[str, str] = {}
|
||||
fallback: list[str] = []
|
||||
for line in (text or '').split('\n'):
|
||||
line = line.strip()
|
||||
if len(line) == 0:
|
||||
continue # blank spacer lines don't count as a fallback entry
|
||||
m = class_tag_re.match(line)
|
||||
if m:
|
||||
names = [n.strip().lower() for n in m.group(1).split(',') if n.strip()]
|
||||
for name in names:
|
||||
class_map[name] = m.group(2).strip()
|
||||
else:
|
||||
fallback.append(line)
|
||||
return class_map, fallback
|
||||
|
||||
|
||||
def assign_prompts(text: str, items: list) -> list[str]:
|
||||
"""Resolve a detailer prompt/negative-prompt string into one entry per detection.
|
||||
|
||||
Detections whose YOLO label matches a '[CLASS=name]' tag get that tag's text.
|
||||
Remaining detections fall back to the untagged lines, applied positionally in
|
||||
detection order and cycling if there are more detections than fallback lines
|
||||
(matching prior behavior when no class tags are used).
|
||||
"""
|
||||
class_map, fallback = parse_prompt_lines(text)
|
||||
if len(fallback) == 0:
|
||||
fallback = ['']
|
||||
resolved = []
|
||||
fallback_idx = 0
|
||||
for item in items:
|
||||
label = (getattr(item, 'label', None) or '').strip().lower()
|
||||
if label in class_map:
|
||||
resolved.append(class_map[label])
|
||||
else:
|
||||
resolved.append(fallback[fallback_idx % len(fallback)])
|
||||
fallback_idx += 1
|
||||
return resolved
|
||||
|
||||
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user