Files
automatic/scripts/postprocessing_detailer.py
T
CalamitousFelicitousness f9ab0bf04d feat(api): add detailer postprocess script and /sdapi/v1/detail endpoint
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
2026-06-01 00:25:59 +01:00

77 lines
3.7 KiB
Python

import numpy as np
from PIL import Image
from modules import scripts_postprocessing, shared
from modules.logger import log
class ScriptPostprocessingDetailer(scripts_postprocessing.ScriptPostprocessing):
name = "Detailer"
order = 15000
def ui(self):
# The detailer accordion (built by yolo.ui) now contains the Sampler sub-accordion too, so for 'extras'
# it returns a 7th element: a dict of the sampler-block controls. Spread it into the control map; their
# values are stamped onto the synthetic p in process()/make_processing(), applying to this pass only.
enabled, prompt, negative, steps, strength, resolution, sampler_block = shared.yolo.ui('extras')
return {
"enabled": enabled,
"prompt": prompt,
"negative": negative,
"steps": steps,
"strength": strength,
"resolution": resolution,
**sampler_block,
}
def process(self, pp: scripts_postprocessing.PostprocessedImage, # pylint: disable=arguments-differ
enabled=False, prompt='', negative='', steps=10, strength=0.3, resolution=1024,
sampler='Default', prediction='default', shift=3.0, cfg_scale=6.0, options=None, seed=-1):
if not enabled:
return pp
if shared.sd_model is None or not hasattr(shared.sd_model, 'sd_checkpoint_info'):
log.warning('Detailer postprocess: no base model selected')
pp.info["Detailer"] = "skipped (no base model selected)"
return pp
# The sampler block is stamped onto the synthetic p. The schedulers_* values become per-job overrides in
# processing_helpers (they beat the global opts for this pass only); a named sampler is required for them
# to take effect, 'Default' keeps the model scheduler. cfg_scale and hr_sampler_name apply directly.
options = options or []
overrides = {
'hr_sampler_name': sampler,
'schedulers_prediction_type': prediction,
'schedulers_shift': shift,
'cfg_scale': cfg_scale,
'schedulers_use_loworder': 'low order' in options,
'schedulers_use_thresholding': 'thresholding' in options,
'schedulers_dynamic_shift': 'dynamic' in options,
'schedulers_rescale_betas': 'rescale' in options,
}
log.info(f'Detailer postprocess: strength={strength} steps={steps} resolution={resolution} sampler={sampler} cfg={cfg_scale}')
p = shared.yolo.make_processing(pp.image, prompt=prompt, negative=negative, steps=steps, strength=strength, resolution=resolution, seed=int(seed) if seed is not None else -1, overrides=overrides)
try:
result = shared.yolo.restore(np.array(pp.image), p)
except Exception as e:
log.error(f'Detailer postprocess: {e}')
return pp
# restore() returns list[ndarray] (detailed image at [0], annotated debug at [1] when enabled)
# on success, or a single ndarray on early-return paths. The postprocessing pipeline is one
# image per input, so the annotated debug image is dropped here; use /sdapi/v1/detail for it.
if isinstance(result, list) and len(result) > 0:
pp.image = Image.fromarray(result[0])
elif isinstance(result, np.ndarray):
pp.image = Image.fromarray(result)
pp.info["Detailer"] = "Enabled"
pp.info["Detailer strength"] = strength
pp.info["Detailer steps"] = steps
pp.info["Detailer resolution"] = resolution
pp.info["Detailer sampler"] = sampler
if prompt:
pp.info["Detailer prompt"] = prompt
if negative:
pp.info["Detailer negative"] = negative
return pp