Merge pull request #5045 from vladmandic/master

refresh dev
This commit is contained in:
Vladimir Mandic
2026-08-22 11:30:32 +02:00
committed by GitHub
6 changed files with 99 additions and 28 deletions
+6 -6
View File
@@ -1,4 +1,4 @@
name: lint
name: Lint Project
on:
- push
@@ -15,13 +15,13 @@ jobs:
steps:
- name: checkout-code
uses: actions/checkout@main
uses: actions/checkout@v4
- name: install-uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: setup-python
uses: actions/setup-python@main
uses: actions/setup-python@v5
with:
python-version: 3.12.3
@@ -29,7 +29,7 @@ jobs:
run: uv pip install ruff pylint pre-commit --system
- name: setup-node
uses: actions/setup-node@main
uses: actions/setup-node@v4
with:
node-version: 24
@@ -44,7 +44,7 @@ jobs:
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: cache-pnpm-store
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
key: pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
@@ -55,7 +55,7 @@ jobs:
run: pnpm install --frozen-lockfile --unsafe-perm
- name: pre-commit
uses: pre-commit-ci/lite-action@v1.1.0
uses: pre-commit-ci/lite-action@v1.2.0
if: always()
with:
msg: apply code formatting and linting auto-fixes
+1 -1
View File
@@ -1,4 +1,4 @@
name: github-pages
name: Build GitHub Pages
on:
push:
+1 -1
View File
@@ -1,4 +1,4 @@
name: readme-sponsors
name: Edit Readme Sponsors
on:
workflow_dispatch:
+1 -1
View File
@@ -1,5 +1,5 @@
from .models import detailer_models
from .helper import DetailerResult, detailer_opt, list_models, get_mask
from .helper import detailer_opt, DetailerResult, list_models, assign_prompts, parse_prompt_lines
from .detailer import Detailer
+39 -19
View File
@@ -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():
@@ -195,6 +195,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)
@@ -222,22 +246,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,
@@ -295,13 +303,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)
@@ -354,6 +367,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:
+51
View File
@@ -1,8 +1,12 @@
import os
import re
from PIL import Image, ImageDraw
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
def get_mask(box: list[int], image: Image.Image, include_mask: bool = True) -> tuple[Image.Image | None, Image.Image]:
cropped = image.crop(box)
if not include_mask: