Compare commits

..
Author SHA1 Message Date
xmarre 6153443986 Merge pull request #13 from xmarre/codex/fix-detailer-mask-for-slrd
Fix detailer hook to use effective guider semantics
2026-03-24 15:24:39 +01:00
xmarre 9f1870e350 Use effective guider in detailer hook sampling 2026-03-24 15:15:39 +01:00
xmarre 77b238a672 fix: align detailer hook noise and bump version 2026-03-24 14:16:01 +01:00
xmarre bf6c517a4c Bump version to 1.0.31 2026-03-24 13:52:09 +01:00
xmarre 0f076a886f Fix detailer hook mask support propagation 2026-03-24 13:51:36 +01:00
xmarre b166cd8741 Remove implicit detailer mask from SLRD config 2026-03-23 05:09:06 +01:00
xmarre 751b8e771c Bump version to 1.0.29 2026-03-23 04:03:51 +01:00
xmarre 3b5a6b7971 Preserve incoming noise for masked detailer runs 2026-03-23 04:03:25 +01:00
xmarre a8e94be4c4 Align impact masks and bump version 2026-03-23 01:49:14 +01:00
xmarre a2b713a9ea Bump version to 1.0.27 2026-03-23 01:08:07 +01:00
xmarre 0af4e4194a Restore denoise mask for impact sampler 2026-03-23 01:03:36 +01:00
xmarre dae4b0906e Fix detailer mask handling and bump version 2026-03-23 00:19:17 +01:00
xmarre e41982cdab Bump version to 1.0.25 2026-03-22 23:34:13 +01:00
xmarre e04f34897a Isolate detailer guider and preserve planner model_options 2026-03-22 23:33:28 +01:00
xmarre 132609e3d2 Bump version to 1.0.24 2026-03-22 22:40:36 +01:00
xmarre 0d4e8d20e2 Binarize detailer masks for SLRD support 2026-03-22 22:39:20 +01:00
xmarre a31f9be2d0 Bump version to 1.0.23 2026-03-22 21:16:46 +01:00
xmarre 25267e588a Merge pull request #12 from xmarre/codex/align-slrd-mask-with-sampler
Fix detailer SLRD masks to use the runtime latent mask
2026-03-22 21:15:35 +01:00
xmarre 2d4fe5ea47 Use runtime detailer masks for scale-locked sampling 2026-03-22 21:05:37 +01:00
xmarre d015307e1a Merge pull request #10 from xmarre/codex/fix-facedetailer-latent-mask-mixup
Restore proxy sampling path for SLRD Impact detailer
2026-03-15 14:46:30 +01:00
xmarre db6dfb5949 Restore Impact detailer proxy path 2026-03-15 14:43:40 +01:00
xmarre 6667f62b75 Merge branch 'main' into codex/fix-facedetailer-latent-mask-mixup 2026-03-15 14:34:22 +01:00
xmarre 189afe2e45 Merge pull request #11 from xmarre/coderabbitai/docstrings/420c8a4
📝 Add docstrings to `codex/fix-facedetailer-latent-mask-mixup`
2026-03-15 14:33:07 +01:00
coderabbitai[bot] 47aa1ddfbe 📝 Add docstrings to codex/fix-facedetailer-latent-mask-mixup
Docstrings generation was requested by @xmarre.

* https://github.com/xmarre/ComfyUI-ScaleLockedResidualDiffusion/pull/10#issuecomment-4062975146

The following files were modified:

* `nodes.py`
2026-03-15 13:29:48 +00:00
xmarre e933378b43 Keep planner on CFG guider 2026-03-15 14:19:04 +01:00
xmarre 420c8a490c Fix FaceDetailer mask state 2026-03-15 12:53:57 +01:00
xmarre 1ffbb6fa56 Fix FaceDetailer mask state 2026-03-15 12:53:19 +01:00
xmarre 3d64e1d3b1 Investigate mask-state bug in FaceD 2026-03-15 12:23:39 +01:00
xmarre 1b3c11ff35 Merge branch 'main' of https://github.com/xmarre/ComfyUI-ScaleLockedResidualDiffusion 2026-03-15 11:40:12 +01:00
xmarre 08d3c6adc0 Bump pyproject version 2026-03-15 11:39:32 +01:00
xmarre a948909964 Merge pull request #9 from xmarre/codex/apply-provided-patch
Fix guider selection for SLRD Impact detailer
2026-03-15 09:49:44 +01:00
3 changed files with 377 additions and 59 deletions
+6 -5
View File
@@ -55,13 +55,15 @@ One-node version of the modular custom-sampling workflow. Internally it now call
Experimental Impact Pack / FaceDetailer integration path. Experimental Impact Pack / FaceDetailer integration path.
This node returns a `DETAILER_HOOK` object that preserves the FaceDetailer crop mask, records the encoded crop latent as the anchor in `post_encode(...)`, and applies one-shot residual/manifold correction in `pre_decode(...)`. This node returns a `DETAILER_HOOK` object that captures the FaceDetailer crop mask in `post_upscale(...)` and drives the masked residual/manifold correction through the hook's sampler path.
This provider no longer advertises sampler-runtime controls that do not participate in its one-shot hook path. The exposed knobs are the ones that still affect the masked latent correction directly: The live hook path is `pre_ksample(...)` plus the custom sampler/runtime integration, not the inert `post_encode(...)` / `pre_decode(...)` pair.
This provider no longer advertises sampler-runtime controls that do not participate in its live sampler-driven hook path. The exposed knobs are the ones that still affect the masked latent correction directly:
- residual lock strength and cutoffs, - residual lock strength and cutoffs,
- optional manifold companding controls, - optional manifold companding controls,
- optional `lock_mask` / `manifold_mask`, which are intersected with the FaceDetailer mask. - optional `lock_mask` / `manifold_mask`, which are intersected with the FaceDetailer support mask.
Compatibility note: this provider's input signature changed when the inert detailer-only sampler/runtime controls were removed. Older saved workflows that used the previous `Scale-Locked Detailer Hook Provider` input surface will need to be re-wired to the current node inputs. Compatibility note: this provider's input signature changed when the inert detailer-only sampler/runtime controls were removed. Older saved workflows that used the previous `Scale-Locked Detailer Hook Provider` input surface will need to be re-wired to the current node inputs.
@@ -90,9 +92,8 @@ A public guider node alone does not make FaceDetailer use SLRD. `Scale-Locked De
The current hook implementation is duck-typed rather than source-verified against a live Impact Pack checkout: The current hook implementation is duck-typed rather than source-verified against a live Impact Pack checkout:
- FaceDetailer mask capture via `post_upscale(...)`, - FaceDetailer mask capture via `post_upscale(...)`,
- self-anchoring via `post_encode(...)`,
- alias-tolerant request capture via `pre_ksample(...)`, - alias-tolerant request capture via `pre_ksample(...)`,
- masked latent correction via `pre_decode(...)`. - masked latent correction via the custom sampler/runtime path.
Because Impact Pack's internal contracts can move, this integration should be treated as unverified runtime glue until it is exercised against the current Impact Pack source. Because Impact Pack's internal contracts can move, this integration should be treated as unverified runtime glue until it is exercised against the current Impact Pack source.
+370 -53
View File
@@ -9,11 +9,12 @@ import math
import torch import torch
import comfy.samplers import comfy.samplers
from .slrd_core import build_nested_noise, clone_latent, init_scale_lock_state, resize_4d_tensor from .slrd_core import build_nested_noise, clone_latent, init_scale_lock_state, resize_4d_tensor, resize_mask
from .slrd_runtime import ( from .slrd_runtime import (
LOCK_SCHEDULE_OPTIONS, LOCK_SCHEDULE_OPTIONS,
MID_SCHEDULE_OPTIONS, MID_SCHEDULE_OPTIONS,
ScaleLockConfig, ScaleLockConfig,
_resolve_sampler_device,
apply_scale_lock_to_noise_prediction, apply_scale_lock_to_noise_prediction,
build_runtime_context_from_advanced, build_runtime_context_from_advanced,
clean_latent, clean_latent,
@@ -28,6 +29,7 @@ from .slrd_runtime import (
sample_with_runtime, sample_with_runtime,
apply_scale_lock_to_guider, apply_scale_lock_to_guider,
clone_guider_for_scale_lock, clone_guider_for_scale_lock,
restore_original_predict_noise,
) )
@@ -654,6 +656,63 @@ def _combine_masks(*masks: torch.Tensor | None) -> torch.Tensor | None:
return None if combined is None else combined.clamp(0.0, 1.0) return None if combined is None else combined.clamp(0.0, 1.0)
_DETAILER_SUPPORT_MASK_EPS = 1e-3
def _detailer_support_mask_for_like(mask: torch.Tensor | None, like: torch.Tensor) -> torch.Tensor | None:
expanded = _expand_mask_for_like(mask, like)
if expanded is None:
return None
# Impact detailer masks are compositing weights. Preserving their soft latent-cell
# variation turns them into a spatially varying SLRD strength field, which causes
# blotchy low-frequency corrections inside smooth regions. SLRD should see a support
# mask here: on inside the selected region, off outside it.
return expanded.gt(_DETAILER_SUPPORT_MASK_EPS).to(dtype=like.dtype)
def _canonicalize_impact_noise_mask(mask: Any) -> torch.Tensor | None:
if not isinstance(mask, torch.Tensor):
return None
if mask.ndim == 4:
if mask.shape[1] == 0:
return None
return mask[:, :1, :, :].squeeze(1).contiguous()
if mask.ndim == 3:
return mask.contiguous()
if mask.ndim == 2:
return mask.unsqueeze(0).contiguous()
logger.warning(
"ScaleLockedDetailerHook: ignoring unsupported denoise_mask rank %s with shape %s.",
mask.ndim,
tuple(mask.shape),
)
return None
def _resolve_impact_sampling_mask(
latent_samples: torch.Tensor,
latent_mask: Any,
denoise_mask: Any,
) -> torch.Tensor | None:
target_hw = tuple(latent_samples.shape[-2:])
batch = latent_samples.shape[0]
def _normalize(mask: Any) -> torch.Tensor | None:
canonical = _canonicalize_impact_noise_mask(mask)
if canonical is None:
return None
resized = resize_mask(canonical, target_hw, batch, 1)
if resized is None:
return None
return resized[:, :1, :, :].squeeze(1).contiguous()
sampler_mask = _normalize(denoise_mask)
if sampler_mask is not None:
return sampler_mask
return _normalize(latent_mask)
def _impact_request_tuple_from_kwargs(kwargs: dict[str, Any]) -> tuple[Any, ...]: def _impact_request_tuple_from_kwargs(kwargs: dict[str, Any]) -> tuple[Any, ...]:
if not kwargs: if not kwargs:
return () return ()
@@ -719,6 +778,18 @@ def _clear_impact_ag_guider(owner) -> None:
def _resolve_impact_ag_guider_template(request: "_ImpactSampleRequest", model_wrap): def _resolve_impact_ag_guider_template(request: "_ImpactSampleRequest", model_wrap):
"""
Locate an existing impact guider template from the provided request or model wrapper and return it.
If a candidate guider is found on either model_wrap.model_patcher or request.model, the guider is returned and the internal `_ag_detailer_guider` attribute is cleared from both potential owners as a side effect.
Parameters:
request (_ImpactSampleRequest): Impact request that may contain a `model` owning a guider.
model_wrap: Model wrapper that may contain a `model_patcher` owning a guider.
Returns:
The found guider instance, or `None` if no guider template is present.
"""
owners = ( owners = (
getattr(model_wrap, "model_patcher", None), getattr(model_wrap, "model_patcher", None),
getattr(request, "model", None), getattr(request, "model", None),
@@ -734,8 +805,195 @@ def _resolve_impact_ag_guider_template(request: "_ImpactSampleRequest", model_wr
return candidate return candidate
return None return None
def _normalize_sampler_extra_args(
extra_args: dict[str, Any] | None,
sigmas: torch.Tensor,
target_device: torch.device,
) -> dict[str, Any]:
"""
Ensure the provided extra_args include a `model_options.sigmas` tensor moved to the target device.
If `extra_args` contains a `model_options` mapping, this function copies it, sets its `sigmas` entry to `sigmas` converted to `target_device` (non-blocking), and returns a shallow-copied dict with the updated `model_options`. If `extra_args` is None or `model_options` is not a dict, returns a shallow copy of `extra_args` (or an empty dict when None).
Parameters:
extra_args (dict[str, Any] | None): Extra sampler arguments that may include a `model_options` dict.
sigmas (torch.Tensor): Sigmas tensor to insert into `model_options`.
target_device (torch.device): Device to which `sigmas` will be moved.
Returns:
dict[str, Any]: A normalized copy of `extra_args` with `model_options.sigmas` set to `sigmas` on `target_device` when applicable.
"""
normalized = {} if extra_args is None else dict(extra_args)
model_options = normalized.get("model_options", None)
if isinstance(model_options, dict):
model_options = dict(model_options)
model_options["sigmas"] = sigmas.to(device=target_device, non_blocking=True)
normalized["model_options"] = model_options
return normalized
class _ScaleLockedPlannerGuiderProxy:
def __init__(self, model_wrap, extra_args: dict[str, Any] | None):
"""
Initialize the planner guider proxy which forwards sampling calls to the underlying model wrapper.
Parameters:
model_wrap: An object that exposes a sampler-compatible interface and may provide a `model_patcher` attribute.
extra_args (dict[str, Any] | None): Optional model/sampler options to be merged and stored for use when sampling; a shallow copy is made if provided.
"""
self.model_patcher = getattr(model_wrap, "model_patcher", None)
self._model_wrap = model_wrap
self._extra_args = {} if extra_args is None else dict(extra_args)
def sample(
self,
noise,
latent_samples,
sampler,
sigmas,
denoise_mask=None,
callback=None,
disable_pbar=False,
seed=None,
):
"""
Prepare inputs on the sampler's device/dtype, normalize extra args, and forward the call to the underlying sampler.
Parameters:
denoise_mask (torch.Tensor | None): Optional mask moved to the sampler device when provided.
callback (callable | None): Optional progress callback; if None a no-op callback is used.
seed: Ignored by this proxy.
Returns:
The value returned by the underlying sampler.sample(...) call.
"""
del seed
if callback is None:
def callback(*args, **kwargs):
return None
target_device = _resolve_sampler_device(self._model_wrap, latent_samples.device)
target_dtype = latent_samples.dtype
latent_samples = latent_samples.to(
device=target_device,
dtype=target_dtype,
non_blocking=True,
)
noise = noise.to(device=target_device, dtype=target_dtype, non_blocking=True)
sigmas = sigmas.to(device=target_device, non_blocking=True)
if isinstance(denoise_mask, torch.Tensor):
denoise_mask = denoise_mask.to(device=target_device, non_blocking=True)
sample_extra_args = _normalize_sampler_extra_args(self._extra_args, sigmas, target_device)
return sampler.sample(
self._model_wrap,
sigmas,
sample_extra_args,
callback,
noise,
latent_image=latent_samples,
denoise_mask=denoise_mask,
disable_pbar=disable_pbar,
)
class _ScaleLockedGuiderProxy:
def __init__(self, base_guider, runtime, config: ScaleLockConfig):
"""
Wraps a base guider and initializes its scale-lock state using the provided runtime context and configuration.
Parameters:
base_guider: The underlying guider object to delegate to; must implement the guider interface used during sampling.
runtime: Runtime context providing model, anchors_x0, and planner_sigmas required to initialize scale-lock state.
config (ScaleLockConfig): Configuration object containing all scale-lock parameters (strengths, cutoffs, schedules, manifold controls, spatial masks, etc.) used to initialize the guider's scale-lock behavior.
"""
self._base_guider = base_guider
init_scale_lock_state(
self,
model=runtime.model,
anchors_x0_cpu=runtime.anchors_x0,
planner_sigmas=runtime.planner_sigmas,
lock_strength=config.lock_strength,
lock_strength_start=config.lock_strength_start,
lock_strength_end=config.lock_strength_end,
cutoff=config.cutoff,
mid_cutoff=config.mid_cutoff,
mid_strength=config.mid_strength,
schedule=config.schedule,
schedule_power=config.schedule_power,
schedule_hold=config.schedule_hold,
mid_strength_start=config.mid_strength_start,
mid_strength_end=config.mid_strength_end,
mid_schedule=config.mid_schedule,
mid_schedule_power=config.mid_schedule_power,
mid_schedule_hold=config.mid_schedule_hold,
spatial_mask=config.spatial_mask,
manifold_enabled=config.manifold_enabled,
manifold_strength=config.manifold_strength,
manifold_strength_start=config.manifold_strength_start,
manifold_strength_end=config.manifold_strength_end,
manifold_schedule=config.manifold_schedule,
manifold_schedule_power=config.manifold_schedule_power,
manifold_schedule_hold=config.manifold_schedule_hold,
manifold_cutoff=config.manifold_cutoff,
manifold_radial_strength=config.manifold_radial_strength,
manifold_anisotropy=config.manifold_anisotropy,
manifold_translation_strength=config.manifold_translation_strength,
manifold_anchor_mix=config.manifold_anchor_mix,
manifold_mean_anchor_mix=config.manifold_mean_anchor_mix,
manifold_contrast_restore=config.manifold_contrast_restore,
manifold_energy_tether=config.manifold_energy_tether,
manifold_channel_tether=config.manifold_channel_tether,
manifold_energy_gain_cap=config.manifold_energy_gain_cap,
manifold_max_shift_px=config.manifold_max_shift_px,
manifold_spatial_mask=config.manifold_spatial_mask,
)
def __getattr__(self, name: str):
"""
Delegate attribute lookup to the wrapped base guider.
Parameters:
name (str): The attribute name to retrieve from the wrapped guider.
Returns:
Any: The value of the requested attribute from the wrapped base guider.
"""
return getattr(self._base_guider, name)
def __call__(self, x, timestep, model_options=None, seed=None):
"""
Run the wrapped guider to produce a noise prediction then apply scale-lock adjustments.
Parameters:
x (torch.Tensor): Input latent tensor for guidance.
timestep: Timestep value passed to the guider (e.g., scheduler timestep).
model_options (dict, optional): Additional model options forwarded to the base guider.
seed (int | None, optional): RNG seed forwarded to the base guider.
Returns:
torch.Tensor: Noise prediction after applying scale-lock modifications.
"""
if model_options is None:
model_options = {}
base_noise = self._base_guider(x, timestep, model_options=model_options, seed=seed)
return apply_scale_lock_to_noise_prediction(self, base_noise, x, timestep)
def _sync_impact_guider_conditions(guider, positive, negative) -> None: def _sync_impact_guider_conditions(guider, positive, negative) -> None:
"""
Synchronizes positive and negative conditioning on a guider object.
Attempts to apply the provided positive and negative conditioning to the guider.
If the guider exposes set_conds, that method is called with (positive, negative).
If it exposes inner_set_conds, that method is called with {"positive": positive, "negative": negative}.
If neither method is present, a warning is logged. Failures during the call are caught and logged as warnings.
Parameters:
guider (object): The guider instance to update; may implement `set_conds` or `inner_set_conds`.
positive: The positive conditioning to apply (type depends on guider implementation).
negative: The negative conditioning to apply (type depends on guider implementation).
"""
try: try:
if hasattr(guider, "set_conds"): if hasattr(guider, "set_conds"):
guider.set_conds(positive, negative) guider.set_conds(positive, negative)
@@ -818,6 +1076,25 @@ class _ScaleLockedImpactSampler:
self._hook = hook self._hook = hook
def sample(self, model_wrap, sigmas, extra_args, callback, noise, latent_image=None, denoise_mask=None, disable_pbar=False): def sample(self, model_wrap, sigmas, extra_args, callback, noise, latent_image=None, denoise_mask=None, disable_pbar=False):
"""
Dispatches a scale-locked sampling run through the underlying sampler using an effective guider and device-aware tensors.
Parameters:
model_wrap: Model wrapper or model object used to resolve device information and guider behavior.
sigmas (torch.Tensor): Noise schedule tensor for the sampler; will be moved to the target device.
extra_args (dict | None): Additional sampler/model options; may be normalized to include device-sharded sigmas.
callback: Progress/callback object passed through to the underlying sampler.
noise (torch.Tensor): Initial noise tensor for sampling; may be replaced with prepared high-resolution noise if shapes match.
latent_image (dict | torch.Tensor | None): Optional high-resolution latent to sample from; if a dict is provided its "samples" entry is used.
denoise_mask (torch.Tensor | None): Optional denoising mask to pass to the sampler.
disable_pbar (bool): If true, disables progress bar propagation to the underlying sampler.
Raises:
RuntimeError: If no pending impact sampling request was captured before this sampler was invoked.
Returns:
The value returned by the underlying sampler's sample(...) call.
"""
request = self._hook._pending_request request = self._hook._pending_request
if request is None: if request is None:
raise RuntimeError("ScaleLockedDetailerHook: sampler request was not captured before the custom sampler was used.") raise RuntimeError("ScaleLockedDetailerHook: sampler request was not captured before the custom sampler was used.")
@@ -827,66 +1104,74 @@ class _ScaleLockedImpactSampler:
planner_latent = clone_latent(request.latent) planner_latent = clone_latent(request.latent)
if latent_image is not None: if latent_image is not None:
planner_latent["samples"] = latent_image planner_latent["samples"] = latent_image
if isinstance(denoise_mask, torch.Tensor):
planner_latent["noise_mask"] = denoise_mask planner_mask = _resolve_impact_sampling_mask(
planner_latent["samples"],
planner_latent.get("noise_mask", None),
denoise_mask,
)
if planner_mask is not None:
planner_latent["noise_mask"] = planner_mask
else:
planner_latent.pop("noise_mask", None)
base_sampler = comfy.samplers.sampler_object(request.sampler_name) base_sampler = comfy.samplers.sampler_object(request.sampler_name)
guider_template = ( effective_guider = _build_impact_effective_guider(request, model_wrap, extra_args)
model_wrap
if _is_impact_guider_like(model_wrap)
else _resolve_impact_ag_guider_template(request, model_wrap)
)
planner_guider = _build_impact_effective_guider(
request,
model_wrap,
extra_args,
guider_template=guider_template,
)
state = self._hook._prepare_runtime_state_for_sampler( state = self._hook._prepare_runtime_state_for_sampler(
request=request, request=request,
guider=planner_guider, guider=effective_guider,
sampler=base_sampler, sampler=base_sampler,
sigmas=sigmas, sigmas=sigmas,
live_latent=planner_latent, live_latent=planner_latent,
runtime_mask=planner_mask,
) )
sample_guider = _build_impact_effective_guider( fallback_device = latent_image.device if latent_image is not None else noise.device
request, target_device = _resolve_sampler_device(model_wrap, fallback_device)
model_wrap, target_dtype = latent_image.dtype if latent_image is not None else noise.dtype
extra_args, sampling_mask = planner_mask
guider_template=guider_template,
prepared_noise = state.runtime.highres_noise
use_prepared_noise = tuple(prepared_noise.shape) == tuple(noise.shape)
if use_prepared_noise:
sampling_noise = prepared_noise.to(
device=target_device,
dtype=target_dtype,
non_blocking=True,
).clone()
else:
sampling_noise = noise.to(
device=target_device,
dtype=target_dtype,
non_blocking=True,
)
sampling_latent = latent_image if latent_image is not None else state.runtime.highres_latent["samples"]
sampling_latent = sampling_latent.to(
device=target_device,
dtype=target_dtype,
non_blocking=True,
) )
apply_scale_lock_to_guider(sample_guider, state.runtime, state.config)
target_device = state.runtime.highres_latent["samples"].device sigmas = sigmas.to(device=target_device, non_blocking=True)
target_dtype = state.runtime.highres_latent["samples"].dtype
sampling_noise = state.runtime.highres_noise
sampling_latent = state.runtime.highres_latent["samples"]
sampling_mask = state.runtime.highres_latent.get("noise_mask", denoise_mask)
if tuple(sampling_noise.shape) != tuple(noise.shape):
sampling_noise = noise
if latent_image is not None:
sampling_latent = latent_image
else:
sampling_latent = planner_latent["samples"]
sampling_mask = denoise_mask
sampling_noise = sampling_noise.to(device=target_device, dtype=target_dtype, non_blocking=True)
sampling_latent = sampling_latent.to(device=target_device, dtype=target_dtype, non_blocking=True)
if isinstance(sampling_mask, torch.Tensor): if isinstance(sampling_mask, torch.Tensor):
sampling_mask = sampling_mask.to(device=target_device, non_blocking=True) sampling_mask = sampling_mask.to(device=target_device, non_blocking=True)
return sample_guider.sample( apply_scale_lock_to_guider(effective_guider, state.runtime, state.config)
sampling_noise, try:
sampling_latent, return effective_guider.sample(
base_sampler, sampling_noise,
state.runtime.sigmas, sampling_latent,
denoise_mask=sampling_mask, base_sampler,
callback=callback, sigmas,
disable_pbar=disable_pbar, denoise_mask=sampling_mask,
seed=state.runtime.noise_seed, callback=callback,
) disable_pbar=disable_pbar,
seed=state.runtime.noise_seed,
)
finally:
restore_original_predict_noise(effective_guider)
finally: finally:
self._hook._clear_sampler_state() self._hook._clear_sampler_state()
@@ -924,13 +1209,30 @@ class _ScaleLockedDetailerHook:
stage, stage,
) )
def _effective_config_for_samples(self, samples: torch.Tensor) -> ScaleLockConfig: def _effective_config_for_samples(
self,
samples: torch.Tensor,
runtime_mask: torch.Tensor | None = None,
) -> ScaleLockConfig:
cfg = self._settings.config cfg = self._settings.config
face_mask = _expand_mask_for_like(self._upscale_mask, samples) detailer_support = _detailer_support_mask_for_like(
lock_mask = _combine_masks(face_mask, _expand_mask_for_like(cfg.spatial_mask, samples)) runtime_mask if runtime_mask is not None else self._upscale_mask,
manifold_source = cfg.manifold_spatial_mask if cfg.manifold_spatial_mask is not None else cfg.spatial_mask samples,
manifold_mask = _combine_masks(face_mask, _expand_mask_for_like(manifold_source, samples)) )
# In the detailer hook path, SLRD must not extend beyond the region that the
# detailer actually denoises/composites for this crop. Treat the live sampler mask
# or captured upscale mask as the default support mask, then let explicit user masks
# further restrict that support.
explicit_lock_mask = _expand_mask_for_like(cfg.spatial_mask, samples)
lock_mask = _combine_masks(detailer_support, explicit_lock_mask)
explicit_manifold_source = (
cfg.manifold_spatial_mask if cfg.manifold_spatial_mask is not None else cfg.spatial_mask
)
explicit_manifold_mask = _expand_mask_for_like(explicit_manifold_source, samples)
manifold_mask = _combine_masks(detailer_support, explicit_manifold_mask)
return replace(cfg, spatial_mask=lock_mask, manifold_spatial_mask=manifold_mask) return replace(cfg, spatial_mask=lock_mask, manifold_spatial_mask=manifold_mask)
def _prepare_runtime_state_for_sampler( def _prepare_runtime_state_for_sampler(
@@ -940,7 +1242,22 @@ class _ScaleLockedDetailerHook:
sampler, sampler,
sigmas, sigmas,
live_latent: dict[str, Any] | None = None, live_latent: dict[str, Any] | None = None,
runtime_mask: torch.Tensor | None = None,
) -> _ScaleLockedImpactRuntimeState: ) -> _ScaleLockedImpactRuntimeState:
"""
Builds and registers a scale-locked impact runtime state for the given impact sampling request.
Parameters:
request (_ImpactSampleRequest): Canonicalized impact sampling request containing model, seed, sampler name, and latent.
guider: Effective guider used for the planner runtime build.
sampler: Sampler instance to be used for planning and runtime creation.
sigmas (torch.Tensor): Noise schedule tensor to use for runtime construction.
live_latent (dict | None): Optional override latent to use for planning instead of request.latent.
runtime_mask (torch.Tensor | None): Optional latent-space mask actually used by the detailer sampler for this cycle.
Returns:
_ScaleLockedImpactRuntimeState: The created runtime state containing the original request, the prepared runtime context, and the effective ScaleLockConfig computed for the planner latent.
"""
guard_sampler_alignment(request.sampler_name, self._settings.sampler_guard) guard_sampler_alignment(request.sampler_name, self._settings.sampler_guard)
planner_noise = _ScaleLockedPlannerNoise(request.seed, disable_noise=not self._settings.add_noise) planner_noise = _ScaleLockedPlannerNoise(request.seed, disable_noise=not self._settings.add_noise)
planner_latent = request.latent if live_latent is None else live_latent planner_latent = request.latent if live_latent is None else live_latent
@@ -957,7 +1274,7 @@ class _ScaleLockedDetailerHook:
state = _ScaleLockedImpactRuntimeState( state = _ScaleLockedImpactRuntimeState(
request=request, request=request,
runtime=runtime, runtime=runtime,
config=self._effective_config_for_samples(runtime.highres_latent["samples"]), config=self._effective_config_for_samples(runtime.highres_latent["samples"], runtime_mask=runtime_mask),
) )
self._active_runtime = state self._active_runtime = state
return state return state
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "scale-locked-residual-diffusion" name = "scale-locked-residual-diffusion"
description = "A ComfyUI custom node pack implementing Scale-Locked Residual Diffusion for high-resolution composition and anatomy stability." description = "A ComfyUI custom node pack implementing Scale-Locked Residual Diffusion for high-resolution composition and anatomy stability."
version = "1.0.18" version = "1.0.32"
license = { file = "LICENSE" } license = { file = "LICENSE" }
[project.urls] [project.urls]